From 9621e2627e15e562e2a17d3a9d79d7600db02a2a Mon Sep 17 00:00:00 2001 From: cophus Date: Sun, 30 Aug 2026 16:54:11 +0100 Subject: [PATCH 01/36] updates to acom + DDF clustering --- pyproject.toml | 2 + src/quantem/__init__.py | 1 + src/quantem/core/datastructures/__init__.py | 1 + src/quantem/core/datastructures/dataset.py | 34 + .../core/datastructures/dataset4dstem.py | 12 +- .../core/datastructures/polar4dstem.py | 237 +++ src/quantem/core/io/__init__.py | 1 + src/quantem/core/io/file_readers.py | 229 ++- src/quantem/core/io/serialize.py | 25 + src/quantem/core/utils/clustering.py | 235 +++ .../core/visualization/visualization_utils.py | 17 +- src/quantem/diffraction/__init__.py | 8 + src/quantem/diffraction/bloch.py | 277 +++ src/quantem/diffraction/bragg_vectors.py | 1571 ++++++++++++++++ .../bragg_vectors_visualization.py | 776 ++++++++ src/quantem/diffraction/calibration.py | 912 +++++++++ src/quantem/diffraction/crystal.py | 510 +++++ src/quantem/diffraction/data/lobato.json | 1650 +++++++++++++++++ src/quantem/diffraction/digital_dark_field.py | 199 ++ src/quantem/diffraction/disk_detection.py | 1149 ++++++++++++ src/quantem/diffraction/orientation.py | 1435 ++++++++++++++ .../diffraction/orientation_visualization.py | 832 +++++++++ src/quantem/diffraction/phase.py | 316 ++++ src/quantem/diffraction/rotations.py | 334 ++++ src/quantem/diffraction/strain.py | 975 ++++++++++ .../diffraction/strain_visualization.py | 499 +++++ .../diffraction/wk_scattering_factors.py | 588 ++++++ tests/core/test_clustering.py | 57 + tests/diffraction/test_bloch.py | 86 + tests/diffraction/test_calibration_refine.py | 70 + tests/diffraction/test_crystal.py | 73 + tests/diffraction/test_ellipse.py | 39 + tests/diffraction/test_orientation.py | 127 ++ tests/diffraction/test_rotation_convention.py | 65 + tests/diffraction/test_rotations.py | 104 ++ tests/diffraction/test_two_phase_map.py | 114 ++ 36 files changed, 13531 insertions(+), 29 deletions(-) create mode 100644 src/quantem/core/datastructures/polar4dstem.py create mode 100644 src/quantem/core/utils/clustering.py create mode 100644 src/quantem/diffraction/bloch.py create mode 100644 src/quantem/diffraction/bragg_vectors.py create mode 100644 src/quantem/diffraction/bragg_vectors_visualization.py create mode 100644 src/quantem/diffraction/calibration.py create mode 100644 src/quantem/diffraction/crystal.py create mode 100644 src/quantem/diffraction/data/lobato.json create mode 100644 src/quantem/diffraction/digital_dark_field.py create mode 100644 src/quantem/diffraction/disk_detection.py create mode 100644 src/quantem/diffraction/orientation.py create mode 100644 src/quantem/diffraction/orientation_visualization.py create mode 100644 src/quantem/diffraction/phase.py create mode 100644 src/quantem/diffraction/rotations.py create mode 100644 src/quantem/diffraction/strain.py create mode 100644 src/quantem/diffraction/strain_visualization.py create mode 100644 src/quantem/diffraction/wk_scattering_factors.py create mode 100644 tests/core/test_clustering.py create mode 100644 tests/diffraction/test_bloch.py create mode 100644 tests/diffraction/test_calibration_refine.py create mode 100644 tests/diffraction/test_crystal.py create mode 100644 tests/diffraction/test_ellipse.py create mode 100644 tests/diffraction/test_orientation.py create mode 100644 tests/diffraction/test_rotation_convention.py create mode 100644 tests/diffraction/test_rotations.py create mode 100644 tests/diffraction/test_two_phase_map.py diff --git a/pyproject.toml b/pyproject.toml index 4df9fc229..760e9ea65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,8 @@ dependencies = [ "optuna>=4.5.0", "hdf5plugin>=6.0.0", "torchinfo>=1.8.0", + "ase", + "spglib", ] [project.optional-dependencies] diff --git a/src/quantem/__init__.py b/src/quantem/__init__.py index ba70f629f..8db92d1f5 100644 --- a/src/quantem/__init__.py +++ b/src/quantem/__init__.py @@ -9,6 +9,7 @@ from quantem.core import visualization as visualization from quantem import imaging as imaging +from quantem import diffraction as diffraction from quantem import diffractive_imaging as diffractive_imaging __version__ = version("quantem") diff --git a/src/quantem/core/datastructures/__init__.py b/src/quantem/core/datastructures/__init__.py index dfb5b47ac..ac8f3d643 100644 --- a/src/quantem/core/datastructures/__init__.py +++ b/src/quantem/core/datastructures/__init__.py @@ -2,6 +2,7 @@ from quantem.core.datastructures.vector import Vector as Vector from quantem.core.datastructures.dataset4dstem import Dataset4dstem as Dataset4dstem +from quantem.core.datastructures.polar4dstem import Polar4dstem as Polar4dstem from quantem.core.datastructures.dataset4d import Dataset4d as Dataset4d from quantem.core.datastructures.dataset3d import Dataset3d as Dataset3d from quantem.core.datastructures.dataset2d import Dataset2d as Dataset2d diff --git a/src/quantem/core/datastructures/dataset.py b/src/quantem/core/datastructures/dataset.py index 947449784..1ef22b74e 100644 --- a/src/quantem/core/datastructures/dataset.py +++ b/src/quantem/core/datastructures/dataset.py @@ -191,6 +191,11 @@ def sampling(self) -> NDArray: def sampling(self, value: NDArray | tuple | list | float | int) -> None: self._sampling = validate_ndinfo(value, self.ndim, "sampling") + @property + def origin_units(self) -> NDArray: + # Origin expressed in physical units: origin * sampling + return np.asarray(self.origin) * np.asarray(self.sampling) + @property def units(self) -> list[str]: return self._units @@ -368,6 +373,35 @@ def _copy_custom_attributes(self, new_dataset: Self) -> None: # Skip attributes that can't be copied pass + def coords(self, axis: int) -> Any: + """ + Coordinate array for a given axis in pixel units. + + coords(d) = arange(shape[d]) - origin[d] + """ + axis = int(axis) + if axis < 0 or axis >= self.ndim: + raise ValueError(f"axis {axis} out of bounds for ndim={self.ndim}") + + xp = self._xp + n = int(self.shape[axis]) + origin_d = float(np.asarray(self.origin)[axis]) + + return xp.arange(n, dtype=float) - origin_d + + def coords_units(self, axis: int) -> Any: + """ + Coordinate array for a given axis in physical units. + + coords_units(d) = (arange(shape[d]) - origin[d]) * sampling[d] + """ + axis = int(axis) + if axis < 0 or axis >= self.ndim: + raise ValueError(f"axis {axis} out of bounds for ndim={self.ndim}") + + sampling_d = float(np.asarray(self.sampling)[axis]) + return self.coords(axis) * sampling_d + def mean(self, axes: int | tuple[int, ...] | None = None) -> Any: """ Computes and returns mean of the data array. diff --git a/src/quantem/core/datastructures/dataset4dstem.py b/src/quantem/core/datastructures/dataset4dstem.py index 004db4278..4a628eb7e 100644 --- a/src/quantem/core/datastructures/dataset4dstem.py +++ b/src/quantem/core/datastructures/dataset4dstem.py @@ -1,3 +1,4 @@ +from os import PathLike from typing import Any, Self import matplotlib.pyplot as plt @@ -8,6 +9,7 @@ from quantem.core.datastructures.dataset2d import Dataset2d from quantem.core.datastructures.dataset4d import Dataset4d +from quantem.core.datastructures.polar4dstem import dataset4dstem_polar_transform from quantem.core.utils.validators import ensure_valid_array from quantem.core.visualization import show_2d from quantem.core.visualization.visualization_utils import ScalebarConfig @@ -77,7 +79,7 @@ def __init__( _token : object | None, optional Token to prevent direct instantiation, by default None """ - mdata_keys_4dstem = ["r_to_q_rotation_cw_deg", "ellipticity"] + mdata_keys_4dstem = ["q_to_r_rotation_ccw_deg", "q_transpose", "ellipticity"] for k in mdata_keys_4dstem: if k not in metadata.keys(): metadata[k] = None @@ -97,15 +99,15 @@ def __init__( self._virtual_detectors = {} # Store detector information for regeneration @classmethod - def from_file(cls, file_path: str, file_type: str) -> "Dataset4dstem": + def from_file(cls, file_path: str | PathLike, file_type: str | None = None) -> "Dataset4dstem": """ Create a new Dataset4dstem from a file. Parameters ---------- - file_path : str + file_path : str | PathLike Path to the data file - file_type : str + file_type : str | None The type of file reader needed. See rosettasciio for supported formats https://hyperspy.org/rosettasciio/supported_formats/index.html @@ -798,3 +800,5 @@ def median_filter_masked_pixels(self, mask: np.ndarray, kernel_width: int = 3): self.array[:, :, index_x, index_y] = np.median( self.array[:, :, x_min:x_max, y_min:y_max], axis=(2, 3) ) + + polar_transform = dataset4dstem_polar_transform diff --git a/src/quantem/core/datastructures/polar4dstem.py b/src/quantem/core/datastructures/polar4dstem.py new file mode 100644 index 000000000..6619af5c9 --- /dev/null +++ b/src/quantem/core/datastructures/polar4dstem.py @@ -0,0 +1,237 @@ +import numpy as np +from numpy.typing import NDArray +from typing import Any, TYPE_CHECKING +from scipy.ndimage import map_coordinates + +if TYPE_CHECKING: + from .dataset4dstem import Dataset4dstem + +from quantem.core.datastructures.dataset4d import Dataset4d + + +class Polar4dstem(Dataset4d): + """4D-STEM dataset in polar coordinates (scan_y, scan_x, phi, r).""" + + def __init__( + self, + array: NDArray | Any, + name: str, + origin: NDArray | tuple | list | float | int, + sampling: NDArray | tuple | list | float | int, + units: list[str] | tuple | list, + signal_units: str = "arb. units", + metadata: dict | None = None, + _token: object | None = None, + ): + if metadata is None: + metadata = {} + mdata_keys_polar = [ + "polar_radial_min", + "polar_radial_max", + "polar_radial_step", + "polar_num_annular_bins", + "polar_two_fold_rotation_symmetry", + "polar_origin_row", + "polar_origin_col", + "polar_ellipse_params", + ] + for k in mdata_keys_polar: + if k not in metadata: + metadata[k] = None + super().__init__( + array=array, + name=name, + origin=origin, + sampling=sampling, + units=units, + signal_units=signal_units, + metadata=metadata, + _token=_token, + ) + + @classmethod + def from_array( + cls, + array: NDArray | Any, + name: str | None = None, + origin: NDArray | tuple | list | float | int | None = None, + sampling: NDArray | tuple | list | float | int | None = None, + units: list[str] | tuple | list | None = None, + signal_units: str = "arb. units", + metadata: dict | None = None, + ) -> "Polar4dstem": + array = np.asarray(array) + if array.ndim != 4: + raise ValueError("Polar4dstem.from_array expects a 4D array.") + if origin is None: + origin = np.zeros(4, dtype=float) + if sampling is None: + sampling = np.ones(4, dtype=float) + if units is None: + units = ["pixels", "pixels", "deg", "pixels"] + if metadata is None: + metadata = {} + return cls( + array=array, + name=name if name is not None else "Polar 4D-STEM dataset", + origin=origin, + sampling=sampling, + units=units, + signal_units=signal_units, + metadata=metadata, + _token=cls._token, + ) + + @property + def n_phi(self) -> int: + return int(self.array.shape[2]) + + @property + def n_r(self) -> int: + return int(self.array.shape[3]) + + +def _precompute_polar_coords( + ny: int, + nx: int, + origin_row: float, + origin_col: float, + ellipse_params: tuple[float, float, float] | None, + num_annular_bins: int, + radial_min: float, + radial_max: float | None, + radial_step: float, + two_fold_rotation_symmetry: bool, +) -> tuple[NDArray, NDArray, NDArray, float]: + origin_row = float(origin_row) + origin_col = float(origin_col) + if radial_step <= 0: + raise ValueError("radial_step must be > 0.") + if num_annular_bins < 1: + raise ValueError("num_annular_bins must be >= 1.") + if radial_max is None: + r_row_pos = origin_row + r_row_neg = (ny - 1) - origin_row + r_col_pos = origin_col + r_col_neg = (nx - 1) - origin_col + radial_max_eff = float(min(r_row_pos, r_row_neg, r_col_pos, r_col_neg)) + else: + radial_max_eff = float(radial_max) + if radial_max_eff <= radial_min: + radial_max_eff = radial_min + radial_step + radial_bins = np.arange(radial_min, radial_max_eff, radial_step, dtype=np.float64) + if radial_bins.size == 0: + radial_bins = np.array([radial_min], dtype=np.float64) + if two_fold_rotation_symmetry: + phi_range = np.pi + else: + phi_range = 2.0 * np.pi + phi_bins = np.linspace(0.0, phi_range, num_annular_bins, endpoint=False, dtype=np.float64) + phi_grid, r_grid = np.meshgrid(phi_bins, radial_bins, indexing="ij") + if ellipse_params is None: + x = r_grid * np.cos(phi_grid) + y = r_grid * np.sin(phi_grid) + else: + if len(ellipse_params) != 3: + raise ValueError("ellipse_params must be (a, b, theta_deg).") + a, b, theta_deg = ellipse_params + theta = np.deg2rad(theta_deg) + alpha = phi_grid - theta + u = (a / b) * r_grid * np.cos(alpha) + v_prime = r_grid * np.sin(alpha) + cos_t = np.cos(theta) + sin_t = np.sin(theta) + x = u * cos_t - v_prime * sin_t + y = u * sin_t + v_prime * cos_t + coords_y = y + origin_row + coords_x = x + origin_col + coords = np.stack((coords_y, coords_x), axis=0) + return coords, phi_bins, radial_bins, radial_max_eff + + +def dataset4dstem_polar_transform( + self: "Dataset4dstem", + origin_row: float | int | NDArray, + origin_col: float | int | NDArray, + ellipse_params: tuple[float, float, float] | None = None, + num_annular_bins: int = 180, + radial_min: float = 0.0, + radial_max: float | None = None, + radial_step: float = 1.0, + two_fold_rotation_symmetry: bool = False, + name: str | None = None, + signal_units: str | None = None, +) -> Polar4dstem: + if self.array.ndim != 4: + raise ValueError("polar_transform requires a 4D-STEM dataset (ndim=4).") + scan_y, scan_x, ny, nx = self.array.shape + origin_row_f = float(origin_row) + origin_col_f = float(origin_col) + coords, phi_bins, radial_bins, radial_max_eff = _precompute_polar_coords( + ny=ny, + nx=nx, + origin_row=origin_row_f, + origin_col=origin_col_f, + ellipse_params=ellipse_params, + num_annular_bins=num_annular_bins, + radial_min=radial_min, + radial_max=radial_max, + radial_step=radial_step, + two_fold_rotation_symmetry=two_fold_rotation_symmetry, + ) + n_phi = phi_bins.size + n_r = radial_bins.size + result_dtype = np.result_type(self.array.dtype, np.float32) + out = np.empty((scan_y, scan_x, n_phi, n_r), dtype=result_dtype) + for iy in range(scan_y): + for ix in range(scan_x): + dp = self.array[iy, ix] + out[iy, ix] = map_coordinates( + dp, + coords, + order=1, + mode="constant", + cval=0.0, + ) + if two_fold_rotation_symmetry: + phi_range = np.pi + else: + phi_range = 2.0 * np.pi + phi_step_deg = (phi_range / float(n_phi)) * (180.0 / np.pi) + sampling = np.zeros(4, dtype=float) + origin = np.zeros(4, dtype=float) + sampling[0:2] = np.asarray(self.sampling)[0:2] + sampling[2] = phi_step_deg + sampling[3] = float(np.asarray(self.sampling)[-1]) * radial_step + origin[0:2] = np.asarray(self.origin)[0:2] + origin[2] = 0.0 + origin[3] = radial_min * float(np.asarray(self.sampling)[-1]) + units = [ + self.units[0], + self.units[1], + "deg", + self.units[-1], + ] + metadata = dict(self.metadata) + metadata.update( + { + "polar_radial_min": float(radial_min), + "polar_radial_max": float(radial_max_eff), + "polar_radial_step": float(radial_step), + "polar_num_annular_bins": int(n_phi), + "polar_two_fold_rotation_symmetry": bool(two_fold_rotation_symmetry), + "polar_origin_row": origin_row_f, + "polar_origin_col": origin_col_f, + "polar_ellipse_params": tuple(ellipse_params) if ellipse_params is not None else None, + } + ) + return Polar4dstem( + array=out, + name=name if name is not None else f"{self.name}_polar", + origin=origin, + sampling=sampling, + units=units, + signal_units=signal_units if signal_units is not None else self.signal_units, + metadata=metadata, + _token=Polar4dstem._token, + ) diff --git a/src/quantem/core/io/__init__.py b/src/quantem/core/io/__init__.py index 2780eae4c..34038aab7 100644 --- a/src/quantem/core/io/__init__.py +++ b/src/quantem/core/io/__init__.py @@ -4,5 +4,6 @@ read_emdfile_to_4dstem as read_emdfile_to_4dstem, ) from quantem.core.io.serialize import AutoSerialize as AutoSerialize +from quantem.core.io.serialize import Bundle as Bundle from quantem.core.io.serialize import load as load from quantem.core.io.serialize import print_file as print_file diff --git a/src/quantem/core/io/file_readers.py b/src/quantem/core/io/file_readers.py index 4fe726458..6e9701720 100644 --- a/src/quantem/core/io/file_readers.py +++ b/src/quantem/core/io/file_readers.py @@ -4,6 +4,7 @@ from typing import Any import h5py +import numpy as np from quantem.core.datastructures import Dataset as Dataset from quantem.core.datastructures import Dataset2d as Dataset2d @@ -19,23 +20,47 @@ def read_4dstem( **kwargs, ) -> Dataset4dstem: """ - File reader for 4D-STEM data + File reader for 4D-STEM data. Parameters ---------- - file_path: str | PathLike - Path to data - file_type: str - The type of file reader needed. See rosettasciio for supported formats + file_path : str | PathLike + Path to data. + file_type : str, optional + The type of file reader needed. See RosettaSciIO for supported formats: https://hyperspy.org/rosettasciio/supported_formats/index.html - dataset_index: int, optional + dataset_index : int, optional Index of the dataset to load if file contains multiple datasets. If None, automatically selects the first 4D dataset found. +<<<<<<< HEAD +<<<<<<< HEAD + If no 4D dataset is found but a 3D stack exists, a 3D dataset can be + interpreted as 4D if `scan_length` is provided. + scan_length : int, optional + For 3D datasets shaped (n_frames, ny, nx) (after possibly moving the + scan axis to the front), interpret the data as a raster scan with shape + (scan_y, scan_x, ny, nx), where scan_y = n_frames // scan_length and + scan_x = scan_length. Required if you want to treat a 3D stack as 4D. + scan_axis : int, default 0 + Which axis of a 3D dataset is the scan/time axis before reshaping. + Must be 0 or 1. The specified axis is moved to axis 0 before the + (scan_y, scan_x) reshape. + transpose_scan_axes : bool, default False + Only used when interpreting a 3D dataset as 4D via `scan_length`. + If True, transpose the scan axes after reshaping so that + (scan_y, scan_x) -> (scan_x, scan_y). This effectively swaps the + interpretation of scan rows and columns in the final 4D array. + + **kwargs : dict + Additional keyword arguments to pass to the Dataset4dstem constructor. +======= +======= hot_pixel_filter: bool, optional If True, detect and replace hot detector pixels immediately after loading using `quantem.core.utils.filter.filter_hot_pixels` with its default parameters. For custom thresholds, call `filter_hot_pixels` directly on the array. +>>>>>>> dev **kwargs: dict Additional keyword arguments to pass to the file reader. @@ -51,9 +76,10 @@ def read_4dstem( Units for each dimension. If None, defaults to ["pixels"] * 4 signal_units : str, optional Units for the array values, by default "arb. units" +>>>>>>> upstream/fitting_models_clean Returns - -------- + ------- Dataset4dstem Examples @@ -76,6 +102,94 @@ def read_4dstem( ... hot_pixel_filter=True, ... ) """ + + def _reshape_3d_to_4d( + imported_data: dict, + *, + dataset_index_local: int | None, + scan_length_local: int, + scan_axis_local: int, + transpose_scan_axes_local: bool, + ) -> dict: + data = imported_data["data"] + if data.ndim != 3: + raise ValueError( + f"Expected 3D data to reshape, got ndim={data.ndim} " + f"with shape {data.shape}" + ) + + if scan_axis_local not in (0, 1): + raise ValueError(f"scan_axis must be 0 or 1, got {scan_axis_local}") + + # Move scan axis to front so it becomes the frame axis + if scan_axis_local != 0: + data = np.moveaxis(data, scan_axis_local, 0) + + n_frames, ny, nx = data.shape + + if scan_length_local <= 0: + raise ValueError(f"scan_length must be positive, got {scan_length_local}") + if n_frames % scan_length_local != 0: + raise ValueError( + f"scan_length={scan_length_local} is not compatible with n_frames={n_frames}; " + f"n_frames % scan_length = {n_frames % scan_length_local}" + ) + + scan_y = n_frames // scan_length_local + scan_x = scan_length_local + + data_4d = data.reshape(scan_y, scan_x, ny, nx) + + if transpose_scan_axes_local: + data_4d = np.transpose(data_4d, (1, 0, 2, 3)) + scan_y, scan_x = scan_x, scan_y + + old_axes = imported_data.get("axes", None) + if old_axes is None or len(old_axes) != 3: + raise ValueError( + "Expected 3 axes for 3D data when reshaping to 4D; " + f"got axes={old_axes}" + ) + + ax_scan_y = { + "scale": 1.0, + "offset": 0.0, + "units": "pixels", + "name": "scan_y", + } + ax_scan_x = { + "scale": 1.0, + "offset": 0.0, + "units": "pixels", + "name": "scan_x", + } + + ax_qy = dict(old_axes[1]) + ax_qx = dict(old_axes[2]) + + imported_data_4d = imported_data.copy() + imported_data_4d["data"] = data_4d + imported_data_4d["axes"] = [ax_scan_y, ax_scan_x, ax_qy, ax_qx] + + original_shape = imported_data["data"].shape + new_shape = data_4d.shape + if dataset_index_local is not None: + print( + f"Using 3D dataset {dataset_index_local} with shape {original_shape} " + f"interpreted as 4D with shape={new_shape} " + f"(scan_axis={scan_axis_local}, scan_length={scan_length_local}, " + f"transpose_scan_axes={transpose_scan_axes_local})." + ) + else: + print( + f"Using 3D dataset with shape {original_shape} " + f"interpreted as 4D with shape={new_shape} " + f"(scan_axis={scan_axis_local}, scan_length={scan_length_local}, " + f"transpose_scan_axes={transpose_scan_axes_local})." + ) + + return imported_data_4d + if file_type is None: file_type = Path(file_path).suffix.lower().lstrip(".") @@ -87,29 +201,98 @@ def read_4dstem( file_reader = importlib.import_module(f"rsciio.{file_type}").file_reader data_list = file_reader(file_path, **kwargs) - # If specific index provided, use it + if not data_list: + raise ValueError(f"No datasets returned by rsciio.{file_type} for '{file_path}'") + + # Case 1: dataset_index specified explicitly if dataset_index is not None: imported_data = data_list[dataset_index] - if imported_data["data"].ndim != 4: + ndim = imported_data["data"].ndim + + if ndim == 4: + # Use 4D as-is + pass + elif ndim == 3: + if scan_length is None: + raise ValueError( + f"Dataset at index {dataset_index} is 3D (shape={imported_data['data'].shape}). " + "To interpret it as 4D-STEM, please provide scan_length." + ) + imported_data = _reshape_3d_to_4d( + imported_data, + dataset_index_local=dataset_index, + scan_length_local=scan_length, + scan_axis_local=scan_axis, + transpose_scan_axes_local=transpose_scan_axes, + ) + else: raise ValueError( - f"Dataset at index {dataset_index} has {imported_data['data'].ndim} dimensions, " - f"expected 4D. Shape: {imported_data['data'].shape}" + f"Dataset at index {dataset_index} has ndim={ndim}, " + f"expected 4D or 3D. Shape: {imported_data['data'].shape}" ) + else: - # Automatically find first 4D dataset + # Case 2: auto-select dataset four_d_datasets = [(i, d) for i, d in enumerate(data_list) if d["data"].ndim == 4] - if len(four_d_datasets) == 0: - print(f"No 4D datasets found in {file_path}. Available datasets:") - for i, d in enumerate(data_list): - print(f" Dataset {i}: shape {d['data'].shape}, ndim={d['data'].ndim}") - raise ValueError("No 4D dataset found in file") - - dataset_index, imported_data = four_d_datasets[0] - - if len(data_list) > 1: - print( - f"File contains {len(data_list)} dataset(s). Using dataset {dataset_index} with shape {imported_data['data'].shape}" + if four_d_datasets: + dataset_index, imported_data = four_d_datasets[0] + if len(data_list) > 1: + print( + f"File contains {len(data_list)} dataset(s). Using 4D dataset " + f"{dataset_index} with shape {imported_data['data'].shape}" + ) + else: + three_d_datasets = [(i, d) for i, d in enumerate(data_list) if d["data"].ndim == 3] + + if not three_d_datasets: + print(f"No 4D datasets found in {file_path}. Available datasets:") + for i, d in enumerate(data_list): + print(f" Dataset {i}: shape {d['data'].shape}, ndim={d['data'].ndim}") + raise ValueError("No 4D or 3D dataset found in file") + + if scan_length is None: + print(f"No 4D datasets found in {file_path}. Available datasets:") + for i, d in enumerate(data_list): + print(f" Dataset {i}: shape {d['data'].shape}, ndim={d['data'].ndim}") + raise ValueError( + "File contains only 3D datasets. To interpret one as 4D-STEM, " + "please specify scan_length so that n_frames % scan_length == 0." + ) + + # Choose first 3D dataset compatible with scan_length along scan_axis + candidates: list[tuple[int, dict]] = [] + for i, d in three_d_datasets: + shape = d["data"].shape + if scan_axis < 0 or scan_axis > 2: + raise ValueError(f"scan_axis must be in [0, 2] for 3D data, got {scan_axis}") + n_frames_axis = shape[scan_axis] + if n_frames_axis % scan_length == 0: + candidates.append((i, d)) + + if not candidates: + print(f"3D datasets in {file_path}:") + for i, d in three_d_datasets: + print(f" Dataset {i}: shape {d['data'].shape}") + raise ValueError( + f"No 3D dataset has length along scan_axis={scan_axis} " + f"divisible by scan_length={scan_length}." + ) + + dataset_index, imported_data = candidates[0] + if len(candidates) > 1: + print( + f"Multiple 3D datasets compatible with scan_length={scan_length} " + f"along scan_axis={scan_axis}. Using dataset {dataset_index} " + f"with shape {imported_data['data'].shape}" + ) + + imported_data = _reshape_3d_to_4d( + imported_data, + dataset_index_local=dataset_index, + scan_length_local=scan_length, + scan_axis_local=scan_axis, + transpose_scan_axes_local=transpose_scan_axes, ) imported_axes = imported_data["axes"] diff --git a/src/quantem/core/io/serialize.py b/src/quantem/core/io/serialize.py index 4dd039840..8aa0bea8c 100644 --- a/src/quantem/core/io/serialize.py +++ b/src/quantem/core/io/serialize.py @@ -1494,3 +1494,28 @@ def _recurse(obj: Any, prefix: str = "", current_depth: int = 0, is_last: bool = ) _recurse(root) + + +class Bundle(AutoSerialize): + """A named collection of serializable objects, saved as one file. + + Groups any AutoSerialize objects (Datasets, Vectors, ...) plus plain + metadata values under attribute names:: + + bundle = Bundle(adf=dataset2d, peaks=vector, note="IM689") + bundle.save("data.zip") + b = load("data.zip"); b.adf, b.peaks + + """ + + def __init__(self, **objects): + for name, obj in objects.items(): + setattr(self, name, obj) + + def __repr__(self) -> str: + items = ", ".join( + f"{k}: {type(v).__name__}" + for k, v in vars(self).items() + if not k.startswith("_") + ) + return f"Bundle({items})" diff --git a/src/quantem/core/utils/clustering.py b/src/quantem/core/utils/clustering.py new file mode 100644 index 000000000..c2989fc09 --- /dev/null +++ b/src/quantem/core/utils/clustering.py @@ -0,0 +1,235 @@ +"""Density-based clustering (DBSCAN) in pure torch, and Vector helpers. + +No external clustering dependency: neighbors are found with blockwise +distance computations pruned by a sliding sorted window along the widest +dimension, and cluster connectivity is resolved by min-label propagation +with path compression. Runs on CPU or any torch device. +""" + +from __future__ import annotations + +import numpy as np +import torch + + +def dbscan( + points, + eps: float, + min_samples: int, + device: str | torch.device = "cpu", + block: int = 2048, + sort_by_size: bool = True, + max_rounds: int = 200, +) -> np.ndarray: + """DBSCAN cluster labels for a point set. + + Same semantics as the standard algorithm: points with at least + `min_samples` neighbors within `eps` (self included) are core points; + core points within `eps` of each other share a cluster; non-core points + within `eps` of a core point join that core's cluster (ties resolved to + the NEAREST core, deterministically); everything else is noise (-1). + + Parameters + ---------- + points : array-like (N, D) + Point coordinates. Scale the columns beforehand to weight + dimensions (the metric is plain Euclidean). + eps : float + Neighborhood radius. + min_samples : int + Neighbors (including the point itself) required for a core point. + device : str | torch.device, default="cpu" + Torch device for the distance computations. + block : int, default=2048 + Rows per distance block; lower to reduce memory. + sort_by_size : bool, default=True + Renumber clusters largest-first (label 0 is the biggest cluster). + + Returns + ------- + np.ndarray + (N,) integer labels, -1 for noise. + """ + pts = torch.as_tensor(np.asarray(points), dtype=torch.float32, device=device) + N, D = pts.shape + if N == 0: + return np.zeros(0, dtype=int) + + # sort along the widest dimension so neighbor candidates live in a + # contiguous window of the sorted order + spans = pts.max(dim=0).values - pts.min(dim=0).values + d0 = int(spans.argmax()) + order = torch.argsort(pts[:, d0]) + ps = pts[order] + key = ps[:, d0].contiguous() + + def block_candidates(i0: int, i1: int) -> tuple[int, int]: + lo = float(key[i0]) - eps + hi = float(key[i1 - 1]) + eps + j0 = int(torch.searchsorted(key, torch.tensor(lo, device=device))) + j1 = int(torch.searchsorted(key, torch.tensor(hi, device=device), right=True)) + return j0, j1 + + # pass 1: neighbor counts -> core points + counts = torch.zeros(N, dtype=torch.long, device=device) + for i0 in range(0, N, block): + i1 = min(i0 + block, N) + j0, j1 = block_candidates(i0, i1) + d = torch.cdist(ps[i0:i1], ps[j0:j1]) + counts[i0:i1] = (d <= eps).sum(dim=1) + core = counts >= min_samples + + # pass 2: min-label propagation among core points + labels = torch.arange(N, dtype=torch.long, device=device) + labels[~core] = -1 + for _ in range(max_rounds): + changed = False + for i0 in range(0, N, block): + i1 = min(i0 + block, N) + if not bool(core[i0:i1].any()): + continue + j0, j1 = block_candidates(i0, i1) + d = torch.cdist(ps[i0:i1], ps[j0:j1]) + adj = (d <= eps) & core[i0:i1, None] & core[None, j0:j1] + lab_nb = torch.where( + adj, labels[j0:j1][None, :], torch.full_like(d, N, dtype=torch.long) + ) + new = lab_nb.min(dim=1).values + cur = labels[i0:i1] + upd = core[i0:i1] & (new < cur) + if bool(upd.any()): + labels[i0:i1] = torch.where(upd, new, cur) + changed = True + # path compression: labels point at representative indices + for _ in range(32): + comp = torch.where(core, labels[labels.clamp_min(0)], labels) + if bool(torch.equal(comp, labels)): + break + labels = comp + if not changed: + break + + # pass 3: border points join the nearest core cluster within eps + for i0 in range(0, N, block): + i1 = min(i0 + block, N) + bmask = ~core[i0:i1] + if not bool(bmask.any()): + continue + j0, j1 = block_candidates(i0, i1) + d = torch.cdist(ps[i0:i1], ps[j0:j1]) + d = torch.where( + core[None, j0:j1], d, torch.full_like(d, torch.inf) + ) + d_min, j_min = d.min(dim=1) + near = bmask & (d_min <= eps) + if bool(near.any()): + lab = labels[i0:i1] + lab[near] = labels[j0 + j_min[near]] + labels[i0:i1] = lab + + # back to input order, compact label ids + out = torch.full((N,), -1, dtype=torch.long, device=device) + out[order] = labels + out_np = out.cpu().numpy() + uniq, inv = np.unique(out_np[out_np >= 0], return_inverse=True) + if uniq.size: + if sort_by_size: + sizes = np.bincount(inv) + rank = np.empty_like(sizes) + rank[np.argsort(sizes)[::-1]] = np.arange(sizes.size) + out_np[out_np >= 0] = rank[inv] + else: + out_np[out_np >= 0] = inv + return out_np + + +def cluster_vector( + vector, + fields, + eps: float, + min_samples: int, + field_scales=None, + scan_scales=None, + device: str | torch.device = "cpu", + label_field: str = "cluster", +): + """DBSCAN over the rows of a Vector, in any combination of field and + scan coordinates. + + Builds the clustering space from the named fields (optionally scaled per + field) plus, when `scan_scales` is given, the scan-grid indices of each + row (row, col of the cell it belongs to, scaled). Digital dark field + clustering is the special case fields=(qx, qy) with scan_scales set. + + Parameters + ---------- + vector : Vector + Ragged vector over a scan grid. + fields : sequence of str + Field names contributing dimensions. + eps, min_samples : + DBSCAN parameters (Euclidean metric in the scaled space). + field_scales : sequence of float | None + Multiplier per field; default 1. + scan_scales : (float, float) | None + If given, append (row * s0, col * s1) of each row's scan cell. + label_field : str, default="cluster" + Name of the label field on the returned Vector. + + Returns + ------- + labeled : Vector + Copy of `vector` with the integer labels appended as a new field + (-1 = noise). + labels : np.ndarray + The flat label array, aligned with vector.flatten(). + """ + flat = vector.select_fields(*fields).flatten().astype(float) + if field_scales is not None: + flat = flat * np.asarray(field_scales, dtype=float)[None, :] + dims = [flat] + if scan_scales is not None: + counts = np.asarray(vector.row_counts(), dtype=int) + shape = vector.shape[:2] + cell_r, cell_c = np.divmod(np.arange(counts.size), shape[1]) + rr = np.repeat(cell_r, counts) * float(scan_scales[0]) + cc = np.repeat(cell_c, counts) * float(scan_scales[1]) + dims.append(np.stack([rr, cc], axis=1)) + space = np.concatenate(dims, axis=1) + + labels = dbscan(space, eps=eps, min_samples=min_samples, device=device) + + labeled = vector.copy() + labeled.add_fields([label_field], units=["index"]) + full = labeled.flatten() + full[:, -1] = labels + labeled.set_flattened(full) + return labeled, labels + + +def filter_rows(vector, mask): + """Copy of a ragged Vector keeping only the flattened rows where mask. + + The scan-grid shape is unchanged; rows are dropped from their cells. + """ + mask = np.asarray(mask, dtype=bool) + counts = np.asarray(vector.row_counts(), dtype=int) + flat = vector.flatten() + starts = np.concatenate([[0], np.cumsum(counts)]) + shape = vector.shape[:2] + nested = [] + for r in range(shape[0]): + row = [] + for c in range(shape[1]): + k = r * shape[1] + c + sel = mask[starts[k]:starts[k + 1]] + row.append(flat[starts[k]:starts[k + 1]][sel]) + nested.append(row) + from quantem.core.datastructures.vector import Vector + + out = Vector.from_data( + nested, fields=list(vector.fields), units=list(vector.units), + name=vector.name, + ) + out.metadata.update(vector.metadata) + return out diff --git a/src/quantem/core/visualization/visualization_utils.py b/src/quantem/core/visualization/visualization_utils.py index 126cb0962..8e32bcc5f 100644 --- a/src/quantem/core/visualization/visualization_utils.py +++ b/src/quantem/core/visualization/visualization_utils.py @@ -347,6 +347,9 @@ def add_scalebar_to_ax( loc: str | int, fontsize: int = 12, bold: bool = True, + box: bool = False, + box_color: str = "black", + box_alpha: float = 0.5, ) -> None: """Add a scale bar to a matplotlib axis. @@ -375,6 +378,14 @@ def add_scalebar_to_ax( Font size of the scale bar label in points. bold : bool Whether to render the scale bar label in bold. + box : bool, default=False + Draw a translucent box behind the bar and label so it stays + readable on any image (e.g. white bar on a black box, or black bar + on a white box). + box_color : str, default="black" + Fill color of the box. + box_alpha : float, default=0.5 + Opacity of the box. """ from matplotlib.font_manager import FontProperties @@ -403,14 +414,16 @@ def add_scalebar_to_ax( length_px, label, loc, - pad=pad_px, + pad=pad_px if not box else max(pad_px, 0.35), color=color, - frameon=False, + frameon=box, label_top=label_top, size_vertical=int(width_px), fontproperties=fontprops, sep=2 if label_top else int(round(0.3 * fontsize)), ) + if box: + bar.patch.set(facecolor=box_color, edgecolor="none", alpha=box_alpha) ax.add_artist(bar) diff --git a/src/quantem/diffraction/__init__.py b/src/quantem/diffraction/__init__.py index e69de29bb..dce2f8608 100644 --- a/src/quantem/diffraction/__init__.py +++ b/src/quantem/diffraction/__init__.py @@ -0,0 +1,8 @@ +from quantem.diffraction.bragg_vectors import BraggVectors as BraggVectors +from quantem.diffraction.crystal import Crystal as Crystal +from quantem.diffraction.orientation import OrientationMap as OrientationMap +from quantem.diffraction.phase import PhaseMap as PhaseMap +from quantem.diffraction.strain import StrainMap as StrainMap +from quantem.diffraction import bloch as bloch +from quantem.diffraction import calibration as calibration +from quantem.diffraction import rotations as rotations diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py new file mode 100644 index 000000000..a15d2f5b0 --- /dev/null +++ b/src/quantem/diffraction/bloch.py @@ -0,0 +1,277 @@ +"""Dynamical (Bloch wave) diffraction for orientation and phase refinement. + +Second-pass refinement: kinematical matching fixes orientations from peak +positions (which dynamical scattering does not move), then this module +recomputes peak intensities with multiple scattering to refine specimen +thickness and phase assignment for the top candidates. + +Follows the Bloch wave formulation of De Graef (2003), ch. 5. The structure +matrix uses U_g = gamma_rel * F_g / pi with F_g the kinematical structure +factors (scattering amplitude per volume, 1/Angstrom^2), off-diagonals +U_(g-h) and diagonal 2 k0 s_g. Without absorption the matrix is Hermitian, +so one eigendecomposition per orientation gives the diffracted intensities +at every thickness essentially for free: + + psi(t) = C exp(2 pi i gamma t) C^-1 psi_0, A C = 2 k0 gamma C +""" + +from __future__ import annotations + +import numpy as np +import torch +from tqdm import tqdm + +from quantem.core.utils.utils import electron_wavelength_angstrom +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.rotations import qrotate + + +def relativistic_gamma(energy_ev: float) -> float: + """Relativistic mass factor 1 + eV / (m0 c^2).""" + return 1.0 + float(energy_ev) / 510998.95 + + +def dynamical_pattern( + crystal: Crystal, + orientation: torch.Tensor, + thicknesses_A: torch.Tensor | np.ndarray | float, + energy_ev: float = 300e3, + sg_max: float = 0.1, + k_max: float | None = None, +) -> dict[str, torch.Tensor]: + """Bloch-wave diffraction intensities for one orientation, all thicknesses. + + Parameters + ---------- + crystal : Crystal + With structure factors calculated. For accurate couplings, + calculate_structure_factors should cover 2x the k_max used here so + every difference vector g - h has a structure factor. + orientation : torch.Tensor + Unit quaternion (4,) rotating crystal vectors into the lab frame. + thicknesses_A : array-like or float + Specimen thicknesses in Angstroms. + sg_max : float, default=0.1 + Excitation error cutoff (1/Angstroms) for including a beam. + k_max : float | None + In-plane scattering vector cutoff for included beams. + + Returns + ------- + dict + 'qx', 'qy' (N,), 'hkl' (N, 3), 'intensity' (T, N) diffracted + intensities per thickness, 's_g' (N,). + """ + if crystal.g_vec is None: + raise RuntimeError("Run crystal.calculate_structure_factors() first.") + lam = electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + gamma_rel = relativistic_gamma(energy_ev) + + t = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) + + # beam selection in the lab frame + g_lab = qrotate(orientation, crystal.g_vec) + gz, g2 = g_lab[:, 2], (g_lab**2).sum(dim=1) + s_g = (2 * gz - lam * g2) / (2 - 2 * lam * gz) + sel = torch.abs(s_g) < sg_max + if k_max is not None: + sel &= crystal.g_len <= k_max + hkl_sel = crystal.hkl[sel] + g_sel = g_lab[sel] + s_sel = s_g[sel] + n = int(sel.sum()) + + # structure factor lookup for all difference vectors (h_i - h_j). + # Prefer the absorptive Weickenmeier-Kohl factors if the crystal has + # them (calculate_dynamical_structure_factors); they carry the + # relativistic and 1/pi factors already. Fall back to the kinematical + # (Lobato) factors, purely elastic. + absorptive = getattr(crystal, "U_dyn", None) is not None + if absorptive: + hkl_all = crystal.hkl_dyn + U_all = crystal.U_dyn + else: + hkl_all = crystal.hkl + U_all = crystal.struct_factors * (gamma_rel / np.pi) + key_mult = torch.tensor( + [1, 2 * int(hkl_all.abs().max()) + 1, (2 * int(hkl_all.abs().max()) + 1) ** 2], + dtype=torch.long, + ) + + def keys(h): + return (h * key_mult[None, :]).sum(dim=1) + + lut = {int(k): i for i, k in enumerate(keys(hkl_all))} + + # beams list includes the (000) beam at index 0 + hkl_beams = torch.cat([torch.zeros((1, 3), dtype=torch.long), hkl_sel]) + s_beams = torch.cat([torch.zeros(1, dtype=torch.float64), s_sel]) + nb = n + 1 + + diff = hkl_beams[:, None, :] - hkl_beams[None, :, :] # (nb, nb, 3) + diff_keys = (diff * key_mult[None, None, :]).sum(dim=-1) + U = torch.zeros((nb, nb), dtype=torch.complex128) + flat = diff_keys.reshape(-1) + idx = torch.tensor( + [lut.get(int(k), -1) for k in flat], dtype=torch.long + ).reshape(nb, nb) + has = idx >= 0 + U[has] = U_all[idx[has]] + + A = U.clone() + A.fill_diagonal_(0) + diag = (2 * k0 * s_beams).to(torch.complex128) + if absorptive: + # mean absorption: imaginary part of U_000 damps every beam + u0 = keys(torch.zeros((1, 3), dtype=torch.long)) + i0 = lut.get(int(u0[0]), -1) + if i0 >= 0: + diag = diag + 1j * U_all[i0].imag + A += torch.diag(diag) + + if absorptive: + # non-Hermitian: general eigendecomposition, complex gamma damps + evals, C = torch.linalg.eig(A) + gam = evals / (2 * k0) + else: + evals, C = torch.linalg.eigh(A) + gam = (evals.real / (2 * k0)).to(torch.complex128) + psi0 = torch.linalg.inv(C)[:, 0] # C^-1 @ e_0 + phase = torch.exp(2j * np.pi * gam[None, :] * t.to(torch.complex128)[:, None]) + psi = torch.einsum("ij,tj,j->ti", C, phase, psi0) # (T, nb) + intensity = torch.abs(psi[:, 1:]) ** 2 # drop the (000) beam + + return { + "qx": g_sel[:, 0], + "qy": g_sel[:, 1], + "hkl": hkl_sel, + "s_g": s_sel, + "intensity": intensity, + "thicknesses": t, + } + + +def refine_thickness( + phase_map, + thicknesses_A: np.ndarray | None = None, + pair_distance: float = 0.05, + power_intensity: float = 0.25, + sg_max: float = 0.1, + k_max: float | None = None, + min_number_peaks: int = 3, + progress_bar: bool = True, +): + """Second-pass thickness and phase refinement with dynamical intensities. + + For every probe position, the winning candidates of a fitted PhaseMap are + re-simulated with Bloch waves over a thickness grid. The peak pairing is + fixed (positions are kinematic); the intensity cost is evaluated for all + thicknesses from a single eigendecomposition per candidate, and the best + (thickness, candidate) combination updates the phase decision. + + Parameters + ---------- + phase_map : PhaseMap + A fitted PhaseMap (fit() has been run). + thicknesses_A : np.ndarray | None + Thickness grid in Angstroms; default 50 ... 1000 in 25 A steps. + + Returns + ------- + dict + 'thickness' (R, C) best-fit thickness map, 'cost' (R, C, F) dynamical + costs per candidate at its best thickness, 'phase_index' (R, C) + updated phase assignment. + """ + if thicknesses_A is None: + thicknesses_A = np.arange(50.0, 1000.0, 25.0) + t_grid = torch.as_tensor(thicknesses_A, dtype=torch.float64) + + oms = phase_map.orientation_maps + cands = phase_map.candidates + peaks = oms[0].peaks + R, C = peaks.shape[0], peaks.shape[1] + F = len(cands) + delta = pair_distance + + fields = peaks.fields + ix = [fields.index(f) for f in ("qx", "qy", "intensity")] + + cost_out = torch.full((R, C, F), torch.nan, dtype=torch.float64) + thick_out = torch.full((R, C, F), torch.nan, dtype=torch.float64) + + iterator = list(np.ndindex(R, C)) + if progress_bar: + iterator = tqdm(iterator, desc="dynamical refinement") + for rx, ry in iterator: + data = peaks[rx, ry].array + if data.shape[0] < min_number_peaks: + continue + qxy = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) + im = torch.as_tensor(data[:, ix[2]], dtype=torch.float64).clamp_min(0) + im = im**power_intensity + int_total = float(im.sum()) + + for f, (i_om, m) in enumerate(cands): + om = oms[i_om] + if om.corr[rx, ry, m] <= 0: + continue + # only refine candidates that won weight in the first pass + if phase_map.phase_weights is not None and float( + phase_map.phase_weights[rx, ry, f] + ) <= 0: + continue + sim = dynamical_pattern( + om.crystal, + om.quats[rx, ry, m], + t_grid, + energy_ev=om.energy_ev, + sg_max=sg_max, + k_max=k_max, + ) + sq = torch.stack((sim["qx"], sim["qy"]), dim=1) + if sq.shape[0] == 0: + continue + si = sim["intensity"] ** power_intensity # (T, N) + d = torch.cdist(sq, qxy) + d_min, j_min = d.min(dim=1) + pair = d_min < delta + frac = (d_min[pair] / delta).clamp(0, 1) + + a = si[:, pair] * (1 - frac)[None, :] # (T, P) + b = im[j_min[pair]][None, :] + w = (a * b).sum(dim=1) / (a * a).sum(dim=1).clamp_min(1e-12) # (T,) + w = w.clamp_min(0) + + c_paired = ( + (b - w[:, None] * a).abs() * (1 - frac)[None, :] + + w[:, None] * a * frac[None, :] + ).sum(dim=1) + c_unpaired_sim = 0.5 * w * si[:, ~pair].sum(dim=1) + matched = torch.zeros(im.shape[0], dtype=torch.bool) + matched[j_min[pair]] = True + c_unpaired_exp = 0.5 * float(im[~matched].sum()) + cost_t = (c_paired + c_unpaired_sim + c_unpaired_exp) / (int_total + 1e-12) + + t_best = int(cost_t.argmin()) + cost_out[rx, ry, f] = cost_t[t_best] + thick_out[rx, ry, f] = t_grid[t_best] + + # updated per-crystal phase decision from the dynamical costs + n_maps = len(oms) + cost_phase = torch.full((R, C, n_maps), torch.inf, dtype=torch.float64) + for f, (i_om, _) in enumerate(cands): + c = torch.nan_to_num(cost_out[..., f], nan=torch.inf) + cost_phase[..., i_om] = torch.minimum(cost_phase[..., i_om], c) + phase_index = cost_phase.argmin(dim=-1) + + f_best = torch.nan_to_num(cost_out, nan=torch.inf).argmin(dim=-1) + thickness = torch.gather(thick_out, 2, f_best[..., None]).squeeze(-1) + + return { + "thickness": thickness, + "cost": cost_out, + "phase_index": phase_index, + "thickness_per_candidate": thick_out, + } diff --git a/src/quantem/diffraction/bragg_vectors.py b/src/quantem/diffraction/bragg_vectors.py new file mode 100644 index 000000000..e241e58ad --- /dev/null +++ b/src/quantem/diffraction/bragg_vectors.py @@ -0,0 +1,1571 @@ +from __future__ import annotations + +import copy as _copy +from pathlib import Path +from typing import Any, Literal, Sequence, Union + +import numpy as np +import torch +from numpy.typing import NDArray + +from quantem.core.datastructures.dataset2d import Dataset2d +from quantem.core.datastructures.dataset4dstem import Dataset4dstem +from quantem.core.datastructures.vector import Vector +from quantem.core.io.serialize import AutoSerialize +from quantem.diffraction.bragg_vectors_visualization import ( + plot_basis_vectors, + plot_bvm, + plot_detection, + plot_diffraction_grid, + plot_lattice_fit, + plot_reference_lattice, + plot_template, +) +from quantem.diffraction.disk_detection import ( + cross_correlation, + detect_disks_batch, + estimate_central_beam, + make_template, + probe_centroid, + synthetic_probe, + template_fourier, +) +from quantem.diffraction.strain import StrainMap + +PEAK_FIELDS = ("q_row", "q_col", "intensity") + + +class BraggVectors(AutoSerialize): + """Correlation-based Bragg disk detection and lattice fitting for 4D-STEM. + + Workflow (each step writes state consumed by the next): + + 1. ``make_template_*`` – build a cross-correlation template, either from a + synthetic soft disk (:meth:`make_template_synthetic`), by averaging data + over an ROI (:meth:`make_template_from_data`), or from an explicit probe + image (:meth:`make_template_from_probe`). + 2. :meth:`detect_disks` – template-match every scan position; detected peaks + are stored in :attr:`peaks` (a :class:`Vector` of ``[q_row, q_col, + intensity]``, numpy-backed) and accumulated into the Bragg vector map + :attr:`bvm`. + 3. :meth:`choose_basis_vectors` – pick the lattice basis ``(origin, g1, g2)`` + from the BVM, automatically or by hand; also stores the numbered candidate + peaks. + 4. :meth:`index_peaks` – quick, lightweight: index just the picked candidate + peaks into a reference lattice (``reference_ab``/``reference_qpos``). + 5. :meth:`fit_lattice` – the heavy step: at every scan position, match the + detections to the reference within ``max_peak_shift``, intensity-weighted + least-squares fit the lattice vectors into ``u_array``/``v_array`` of shape + ``(scan_row, scan_col, 2)``, and compute the per-position ``mask_weight``. + 6. :meth:`calculate_strain_map` – hand the lattice vectors (and + ``mask_weight``) to a :class:`~quantem.diffraction.strain.StrainMap`. + + Detection runs in torch (CPU now, CUDA later); the ragged peak table is held + in a numpy-backed :class:`Vector`. The detector→scan rotation is read from + the parent dataset metadata (``q_to_r_rotation_ccw_deg`` + ``q_transpose``), + the single source of truth shared with the DPC/CoM workflow. + + Use :meth:`from_dataset` to construct an instance. + + Parameters + ---------- + dataset : Dataset4dstem + The 4D-STEM dataset to analyze. + device : str, default="cpu" + Torch device used for detection (e.g. ``"cpu"`` or ``"cuda"``). + """ + + _token = object() + + # nanobeam lattice vectors are measured in reciprocal space -> sign flip + real_space: bool = False + + def __init__( + self, + dataset: Dataset4dstem, + device: str = "cpu", + _token: object | None = None, + ): + if _token is not self._token: + raise RuntimeError("Use BraggVectors.from_dataset() to instantiate this class.") + super(BraggVectors, self).__init__() + self.dataset = dataset + self.device = device + + self.peaks: Vector | None = None + self.bvm: Dataset2d | None = None + + self.origin: np.ndarray | None = None + self.g1: np.ndarray | None = None + self.g2: np.ndarray | None = None + + # candidate peaks picked from the BVM in choose_basis_vectors(), and the + # reference lattice (those candidates indexed) set by index_peaks(). + self.candidates_rc: np.ndarray | None = None + self.candidates_intensity: np.ndarray | None = None + self.reference_ab: np.ndarray | None = None + self.reference_qpos: np.ndarray | None = None + self.reference_intensity: np.ndarray | None = None + + self.u_array: np.ndarray | None = None + self.v_array: np.ndarray | None = None + # per-position diagnostics from fit_lattice() + self.mask_weight: np.ndarray | None = None + self.fit_error: np.ndarray | None = None + + self._template: torch.Tensor | None = None + self._template_ft: torch.Tensor | None = None + self.metadata: dict[str, Any] = {} + + @classmethod + def from_dataset( + cls, dataset: Dataset4dstem, *, device: str = "cpu", name: str | None = None + ) -> "BraggVectors": + """Create a BraggVectors workflow bound to a 4D-STEM dataset. + + Parameters + ---------- + dataset : Dataset4dstem + The 4D-STEM dataset to analyze. + device : str, default="cpu" + Torch device used for detection (e.g. ``"cpu"`` or ``"cuda"``). + name : str, optional + If given, sets ``dataset.name``. + + Returns + ------- + BraggVectors + A new workflow instance bound to ``dataset``. + """ + if not isinstance(dataset, Dataset4dstem): + raise TypeError("BraggVectors.from_dataset expects a Dataset4dstem instance.") + if name is not None: + dataset.name = name + return cls(dataset=dataset, device=device, _token=cls._token) + + def save( + self, + path: str | Path, + mode: Literal["w", "o"] = "w", + store: Literal["auto", "zip", "dir"] = "auto", + skip: Union[str, type, Sequence[Union[str, type]]] = (), + compression_level: int | None = 4, + *, + include_dataset: bool = False, + ) -> None: + """Save the workflow to disk, excluding the raw 4D-STEM dataset by default. + + Overrides :meth:`~quantem.core.io.serialize.AutoSerialize.save` to drop + :attr:`dataset` — the raw 4D-STEM cube, which dominates the file size — from + serialization by default. The detected :attr:`peaks`, lattice fit + (:attr:`u_array`/:attr:`v_array`), Bragg vector map and all diagnostics are + kept, so the file holds the *results* of the workflow (orders of magnitude + smaller than the data) rather than the data itself. + + ``"dataset"`` is recorded in the file's skip metadata, so a reloaded workflow + simply has no ``dataset`` attribute. Re-attach one (``bv.dataset = ds``) before + calling methods that read the raw cube — :meth:`detect_disks`, + :meth:`correlation_map`, :meth:`make_template_from_data`, + :meth:`calculate_strain_map`, etc. Pass ``include_dataset=True`` to keep the + dataset in the file instead. + + Parameters + ---------- + path : str or Path + Target file path. Use a ``.zip`` extension for zip format, otherwise a + directory is written. + mode : {'w', 'o'}, default='w' + ``'w'`` writes only if the path does not exist; ``'o'`` overwrites. + store : {'auto', 'zip', 'dir'}, default='auto' + Storage format; ``'auto'`` infers from the file extension. + skip : str, type, or sequence of (str or type), default=() + Additional attribute names/types to skip during serialization, merged with + the default ``dataset`` exclusion. + compression_level : int or None, default=4 + Zstandard/Blosc compression level (0–9); ``0`` disables compression. + include_dataset : bool, default=False + If ``True``, keep the raw 4D-STEM :attr:`dataset` in the file (large). The + default ``False`` excludes it. + """ + if isinstance(skip, (str, type)): + skip = [skip] + else: + skip = list(skip) + if not include_dataset and "dataset" not in skip: + skip.append("dataset") + # Explicit (two-arg) super() rather than the bare super(): the zero-arg form + # needs a compiler-created __class__ closure cell that is absent when this + # method's source is re-exec'd from a string (Jupyter autoreload), which + # raises "super(): __class__ cell not found". The explicit form is immune. + super(BraggVectors, self).save( + path, + mode=mode, + store=store, + skip=skip, + compression_level=compression_level, + ) + + # ---- main methods ---- + + def make_template_synthetic( + self, + radius: float | None = None, + edge: float = 1.0, + center: tuple[float, float] | None = None, + subtract_mean: bool = False, + ) -> "BraggVectors": + """Build the template from a synthetic soft-edged disk. + + Parameters + ---------- + radius : float, optional + Disk radius in pixels. Defaults to a rough estimate from the mean + diffraction pattern + (:func:`~quantem.diffraction.disk_detection.estimate_central_beam`); + pass it explicitly when several disks share comparable intensity. + edge : float, default=1.0 + Width in pixels of the ``tanh`` edge falloff. + center : tuple of float, optional + ``(row, col)`` disk center; defaults to the detector center + ``(H // 2, W // 2)``. + subtract_mean : bool, default=False + If ``True``, make the template zero-sum. The default keeps the + unit-sum positive template, so correlation values stay positive and + roughly measure the probe-weighted counts under each peak. + + Returns + ------- + BraggVectors + ``self``, for method chaining. + """ + H, W = int(self.dataset.shape[-2]), int(self.dataset.shape[-1]) + if radius is None: + dp_mean = torch.as_tensor( + np.asarray(self.dataset.dp_mean.array), dtype=torch.float, device=self.device + ) + _, radius = estimate_central_beam(dp_mean) + if center is None: + center = (H // 2, W // 2) + probe = synthetic_probe((H, W), float(radius), edge=edge, center=center) + self._set_template(probe, center=center, subtract_mean=subtract_mean) + self.metadata["template"] = { + "kind": "synthetic", + "radius": float(radius), + "edge": float(edge), + "center": (float(center[0]), float(center[1])), + } + return self + + def make_template_from_data( + self, + roi: NDArray | None = None, + subtract_mean: bool = False, + center: tuple[float, float] | None = None, + ) -> "BraggVectors": + """Build the template by averaging diffraction patterns from the data. + + Parameters + ---------- + roi : np.ndarray, optional + ``(scan_row, scan_col)`` mask selecting scan positions to average — + ideally a vacuum / single-disk region so the unscattered probe is + isolated. ``None`` (default) averages the whole scan (the mean + diffraction pattern). + subtract_mean : bool, default=False + If ``True``, make the template zero-sum. The default keeps the + unit-sum positive template, so correlation values stay positive and + roughly measure the probe-weighted counts under each peak. + center : tuple of float, optional + ``(row, col)`` probe center rolled to the origin; defaults to the + probe's intensity centroid. + + Returns + ------- + BraggVectors + ``self``, for method chaining. + """ + data = torch.as_tensor( + np.asarray(self.dataset.array), dtype=torch.float, device=self.device + ) + if roi is None: + probe = data.mean(dim=(0, 1)) + else: + m = torch.as_tensor(np.asarray(roi) > 0, device=self.device) + if not bool(m.any()): + raise ValueError("roi selects no scan positions.") + probe = data[m].mean(dim=0) + if center is None: + center = probe_centroid(probe) + self._set_template(probe, center=center, subtract_mean=subtract_mean) + self.metadata["template"] = { + "kind": "from_data", + "roi": roi is not None, + "center": (float(center[0]), float(center[1])), + } + return self + + def make_template_from_probe( + self, + probe: NDArray | torch.Tensor, + center: tuple[float, float] | None = None, + subtract_mean: bool = False, + ) -> "BraggVectors": + """Build the template from an explicit probe image (e.g. a measured vacuum probe). + + Parameters + ---------- + probe : np.ndarray or torch.Tensor + Probe image; must match the diffraction-pattern shape. + center : tuple of float, optional + ``(row, col)`` probe center rolled to the origin; defaults to the + probe's intensity centroid. + subtract_mean : bool, default=False + If ``True``, make the template zero-sum. The default keeps the + unit-sum positive template, so correlation values stay positive and + roughly measure the probe-weighted counts under each peak. + + Returns + ------- + BraggVectors + ``self``, for method chaining. + """ + probe_t = torch.as_tensor(probe, dtype=torch.float, device=self.device) + if center is None and tuple(probe_t.shape) == tuple(self.dataset.shape[-2:]): + center = probe_centroid(probe_t) + self._set_template(probe_t, center=center, subtract_mean=subtract_mean) + self.metadata["template"] = { + "kind": "from_probe", + "center": (float(center[0]), float(center[1])), + } + return self + + @property + def template(self) -> np.ndarray | None: + """The correlation template, fftshifted to the image center for display (numpy). + + The ``make_template_*`` methods store the template corner-shifted (center + at the ``[0, 0]`` FFT origin) so correlation peaks land at absolute disk + positions; this property shifts it back to the center for plotting. + + Returns + ------- + np.ndarray or None + ``(H, W)`` center-shifted template, or ``None`` if no template has been + built yet. + """ + if self._template is None: + return None + return torch.fft.fftshift(self._template).detach().cpu().numpy() + + def correlation_map( + self, row: int, col: int, background_sigma: float | str | None = None + ) -> np.ndarray: + """Cross-correlation map of one diffraction pattern with the template (numpy). + + Peaks in the returned map sit at absolute disk positions (no fftshift + needed), matching what :meth:`detect_disks` searches. + + Parameters + ---------- + row : int + Scan row of the diffraction pattern to correlate. + col : int + Scan column of the diffraction pattern to correlate. + + Returns + ------- + np.ndarray + ``(H, W)`` real-space correlation map. + """ + if self._template_ft is None: + raise ValueError("Run a make_template_* method before correlation_map().") + dp = torch.as_tensor( + np.asarray(self.dataset.array[row, col]), dtype=torch.float, device=self.device + ) + corr, _ = cross_correlation( + dp, self._template_ft, self._resolve_background_sigma(background_sigma) + ) + return corr.detach().cpu().numpy() + + def detect_disks( + self, + *, + positions: list[tuple[int, int]] | None = None, + min_abs_intensity: float = 0.0, + min_spacing: float = 0.0, + edge_boundary: int = 1, + subpixel: str = "upsample", + upsample_factor: int = 16, + max_num_peaks: int = 1000, + background_sigma: float | str | None = None, + batch_size: int | None = None, + progressbar: bool = True, + ) -> Vector: + """Detect Bragg disks at every scan position (or a subset for testing). + + Pass ``positions`` to test detection hyperparameters on a handful of + patterns without scanning the full grid; the returned :class:`Vector` then + has shape ``(len(positions),)`` and the workflow state is left untouched. + With ``positions=None`` the full scan is processed, :attr:`peaks` and + :attr:`bvm` are populated, and the same Vector is returned. Patterns are + processed in batches (the FFTs and subpixel refinement run together across + the batch, which is far faster on a GPU); results are identical to detecting + each pattern on its own. + + Parameters + ---------- + positions : list of tuple of int, optional + ``(row, col)`` scan positions to test on. ``None`` (default) processes + the full scan and updates the workflow state. + min_abs_intensity : float, default=0.0 + Drop correlation peaks below this absolute intensity. + min_spacing : float, default=0.0 + Minimum spacing in pixels between kept peaks; closer / dimmer peaks are + suppressed. + edge_boundary : int, default=1 + Width in pixels of the border in which peaks are ignored. + subpixel : {"none", "parabolic", "upsample"}, default="upsample" + Subpixel refinement mode; see + :func:`~quantem.diffraction.disk_detection.detect_disks`. + upsample_factor : int, default=16 + Upsampling factor for the ``"upsample"`` subpixel refinement. + max_num_peaks : int, default=1000 + Maximum number of peaks to keep per pattern. + batch_size : int, optional + Number of patterns per batch. ``None`` (default) picks a size from the + detector dimensions. + progressbar : bool, default=True + If ``True``, show a tqdm progress bar over the full-scan detection. + + Returns + ------- + Vector + Detected peaks (``[q_row, q_col, intensity]``): shape ``(scan_row, + scan_col)`` for the full scan, or ``(len(positions),)`` for a test run. + """ + if self._template_ft is None: + raise ValueError("Run a make_template_* method before detect_disks().") + + detect_kwargs = dict( + min_abs_intensity=min_abs_intensity, + min_spacing=min_spacing, + edge_boundary=edge_boundary, + subpixel=subpixel, + upsample_factor=upsample_factor, + max_num_peaks=max_num_peaks, + background_sigma=self._resolve_background_sigma(background_sigma), + ) + + if positions is not None: + if len(positions) == 0: + raise ValueError("positions must contain at least one (row, col) to test on.") + coords = [(int(r), int(c)) for r, c in positions] + results = self._detect_positions(coords, detect_kwargs, batch_size, progressbar=False) + return Vector.from_data(results, fields=PEAK_FIELDS, name="bragg_peaks_test") + + scan_r, scan_c = int(self.dataset.shape[0]), int(self.dataset.shape[1]) + coords = list(np.ndindex(scan_r, scan_c)) + results = self._detect_positions( + coords, detect_kwargs, batch_size, progressbar=progressbar + ) + # Store every cell in a single bulk pass. Per-cell assignment + # (peaks[r, c] = arr) re-concatenates the entire backing buffer on every + # write -- O(N^2) over the scan, which is the stall at the end of + # detection. from_data stacks all cells with one _replace_cells call. + nested = [results[r * scan_c : (r + 1) * scan_c] for r in range(scan_r)] + peaks = Vector.from_data(nested, fields=PEAK_FIELDS, name="bragg_peaks") + + self.peaks = peaks + self.metadata["detect"] = detect_kwargs + self.compute_bvm() + return peaks + + def correct_peak_origins( + self, + origins: NDArray, + origin_ref: NDArray | tuple[float, float] | None = None, + *, + inplace: bool = False, + ) -> "BraggVectors": + """Shift the detected peak coordinates so all positions share one origin. + + Subtracts each scan position's measured diffraction origin (e.g. the + plane-fitted center-of-mass of the central beam -- the descan) from its + peaks and adds back a common reference ``origin_ref``, so the peak + coordinates from every position live in a single detector frame. The + diffraction data itself is untouched: this calibrates the measurements, + not the images. The Bragg vector map is recomputed from the corrected + peaks. + + By default a corrected *copy* of the workflow is returned and ``self`` + keeps the raw detections, so calling this repeatedly (e.g. re-running a + notebook cell) never double-applies the shift. + + Parameters + ---------- + origins : np.ndarray + ``(scan_row, scan_col, 2)`` per-position diffraction origins in + detector pixels (row, col). + origin_ref : array-like of float, optional + ``(row, col)`` common origin the corrected peaks are referred to. + Defaults to the scan-mean of ``origins``. + inplace : bool, default=False + If ``True``, correct ``self`` instead of returning a corrected copy. + + Returns + ------- + BraggVectors + The workflow holding the corrected peaks (a new instance unless + ``inplace=True``); its :attr:`bvm` is recomputed. + """ + if self.peaks is None: + raise ValueError("Run detect_disks() before correct_peak_origins().") + scan_shape = tuple(int(v) for v in self.dataset.shape[:2]) + origins = np.asarray(origins, dtype=float) + if origins.shape != scan_shape + (2,): + raise ValueError(f"origins must have shape {scan_shape + (2,)}, got {origins.shape}.") + if origin_ref is None: + origin_ref = origins.mean(axis=(0, 1)) + origin_ref = np.asarray(origin_ref, dtype=float).reshape(2) + + if inplace: + bv = self + else: + bv = type(self)(dataset=self.dataset, device=self.device, _token=type(self)._token) + bv._template = self._template + bv._template_ft = self._template_ft + bv.metadata = _copy.deepcopy(self.metadata) + bv.peaks = self.peaks.copy() + + peaks = bv.peaks + # rowwise transform on the flat peak table: one shift per scan cell, + # repeated per detected peak (cells and shifts share raster order) + flat = peaks.flatten() + counts = np.asarray(peaks.row_counts(), dtype=int) + shifts = np.repeat(origin_ref[None, :] - origins.reshape(-1, 2), counts, axis=0) + flat[:, :2] += shifts + peaks.set_flattened(flat) + + bv.metadata["origin_correction"] = { + "origin_ref": (float(origin_ref[0]), float(origin_ref[1])), + } + bv.compute_bvm() + return bv + + def compute_bvm(self, sampling: float = 1.0) -> Dataset2d: + """Accumulate all detected peaks into a Bragg vector map (intensity histogram). + + Parameters + ---------- + sampling : float, default=1.0 + Reciprocal-space sampling (per pixel) stored on the returned dataset. + + Returns + ------- + Dataset2d + ``(H, W)`` Bragg vector map, also stored on :attr:`bvm`. + """ + if self.peaks is None: + raise ValueError("Run detect_disks() before compute_bvm().") + H, W = (int(self.dataset.shape[-2]), int(self.dataset.shape[-1])) + flat = self.peaks.select_fields("q_row", "q_col", "intensity").flatten() + + bvm = np.zeros((H, W), dtype=float) + if flat.shape[0] > 0: + rows = np.clip(np.round(flat[:, 0]).astype(int), 0, H - 1) + cols = np.clip(np.round(flat[:, 1]).astype(int), 0, W - 1) + np.add.at(bvm, (rows, cols), flat[:, 2]) + + self.bvm = Dataset2d.from_array( + bvm, name="bragg_vector_map", sampling=(sampling, sampling), signal_units="intensity" + ) + return self.bvm + + def choose_basis_vectors( + self, + origin: int | tuple[float, float] | NDArray | None = None, + g1: int | tuple[float, float] | NDArray | None = None, + g2: int | tuple[float, float] | NDArray | None = None, + *, + num_candidates: int = 100, + min_spacing: float = 2.0, + min_abs_intensity: float = 0.0, + plot: bool = True, + returnfig: bool = False, + **show_kwargs, + ): + """Select the lattice basis ``(origin, g1, g2)`` from the Bragg vector map. + + Any of ``origin``/``g1``/``g2`` may be given explicitly; the rest are picked + automatically. The origin is the **brightest** candidate peak (the + unscattered central beam). With ``quality = intensity / distance`` rewarding + short, bright vectors, ``g1`` is then the highest-``quality`` peak and ``g2`` + is the highest ``quality * sin^2(theta)`` peak, where ``theta`` is its angle + to ``g1`` (the ``sin^2`` factor vanishes for peaks collinear with ``g1``). + + Each override accepts **either** form, told apart by shape: + + - a scalar **candidate index** (an ``int``) picks one of the numbered + candidate peaks drawn on the plot; + - a **``(row, col)`` vector** is taken literally — an absolute position for + ``origin``, an offset *from the origin* for ``g1``/``g2``. + + With ``plot=True`` (default) the Bragg vector map is shown with the + candidate peaks numbered and the chosen basis overlaid, so the index to + pass back here can be read straight off the figure. + + Parameters + ---------- + origin : int or tuple of float or np.ndarray, optional + Candidate index, or absolute ``(row, col)`` lattice origin. Picked + automatically if omitted. + g1 : int or tuple of float or np.ndarray, optional + Candidate index (vector taken as ``peak - origin``), or a ``(row, col)`` + offset *from the origin*. Picked automatically if omitted. + g2 : int or tuple of float or np.ndarray, optional + Candidate index (vector taken as ``peak - origin``), or a ``(row, col)`` + offset *from the origin*. Picked automatically if omitted. + num_candidates : int, default=100 + Number of brightest candidate peaks to consider (and to number on the + plot). + min_spacing : float, default=2.0 + Minimum spacing in pixels between candidate peaks. + min_abs_intensity : float, default=0.0 + Drop candidate peaks below this absolute intensity. + plot : bool, default=True + If ``True``, show the Bragg vector map with the numbered candidates and + the chosen basis overlaid via + :func:`~quantem.diffraction.bragg_vectors_visualization.plot_basis_vectors`. + returnfig : bool, default=False + If ``True``, return ``(fig, ax)`` from the overlay plot instead of + ``self`` (implies ``plot=True``). + **show_kwargs + Display-scaling options forwarded to the overlay plot's + :func:`~quantem.core.visualization.show_2d` call (e.g. ``norm``, + ``vmin``, ``vmax``, ``cmap``, ``lower_quantile``, ``upper_quantile``). + + Returns + ------- + BraggVectors or tuple + ``self`` for method chaining; or ``(fig, ax)`` when ``returnfig=True``. + """ + if self.bvm is None: + raise ValueError("Run detect_disks()/compute_bvm() before choosing basis vectors.") + + cand_rc, cand_int = self._bvm_candidates(num_candidates, min_spacing, min_abs_intensity) + if cand_rc.shape[0] == 0: + raise RuntimeError("No candidate peaks found in the Bragg vector map.") + # remember the numbered candidates so index_peaks() reuses this exact set. + self.candidates_rc = cand_rc + self.candidates_intensity = cand_int + + def _candidate(idx: int) -> NDArray: + i = int(idx) + if not -cand_rc.shape[0] <= i < cand_rc.shape[0]: + raise IndexError( + f"candidate index {i} out of range for {cand_rc.shape[0]} candidates " + f"(increase num_candidates or loosen min_spacing/min_abs_intensity)." + ) + return cand_rc[i] + + if origin is None: + # candidates are returned brightest-first, so [0] is the central beam. + origin_rc = cand_rc[0] + elif np.ndim(origin) == 0: + origin_rc = _candidate(origin) + else: + origin_rc = np.asarray(origin, dtype=float).reshape(2) + + rel = cand_rc - origin_rc + dist = np.linalg.norm(rel, axis=1) + valid = dist > max(1e-6, min_spacing) + # "shortest/brightest": reward bright peaks close to the origin. + quality = cand_int / (dist + 1e-12) + + if g1 is None: + if not valid.any(): + raise RuntimeError("Could not find a g1 candidate distinct from the origin.") + g1_rc = rel[int(np.argmax(np.where(valid, quality, -np.inf)))] + elif np.ndim(g1) == 0: + g1_rc = _candidate(g1) - origin_rc + else: + g1_rc = np.asarray(g1, dtype=float).reshape(2) + + if g2 is None: + # g2 = highest quality * sin^2(theta) where theta is the angle to g1; + # sin^2 = 1 - cos^2 vanishes for peaks collinear with g1. + g1n = g1_rc / (np.linalg.norm(g1_rc) + 1e-12) + cos = rel @ g1n / (dist + 1e-12) + sin2 = np.clip(1.0 - cos**2, 0.0, 1.0) + g2_score = np.where(valid, quality * sin2, -np.inf) + if g2_score.max() <= 0.0: + raise RuntimeError("Could not find a g2 candidate non-collinear with g1.") + g2_rc = rel[int(np.argmax(g2_score))] + elif np.ndim(g2) == 0: + g2_rc = _candidate(g2) - origin_rc + else: + g2_rc = np.asarray(g2, dtype=float).reshape(2) + + self.origin = np.asarray(origin_rc, dtype=float).reshape(2) + self.g1 = np.asarray(g1_rc, dtype=float).reshape(2) + self.g2 = np.asarray(g2_rc, dtype=float).reshape(2) + + if plot or returnfig: + fig, ax = plot_basis_vectors( + np.asarray(self.bvm.array), + cand_rc, + cand_int, + self.origin, + self.g1, + self.g2, + **show_kwargs, + ) + if returnfig: + return fig, ax + return self + + def index_peaks( + self, + *, + plot: bool = True, + returnfig: bool = False, + **show_kwargs, + ): + """Index the chosen candidate peaks into a reference lattice. + + Assigns integer Miller indices ``(a, b)`` to the numbered candidate peaks + picked in :meth:`choose_basis_vectors` — not the full per-position + detections; that heavy lifting happens in :meth:`fit_lattice`. Each + candidate gets ``[a, b] = round(B^-1 (q - origin))`` with ``B`` columns + ``g1, g2``; when two candidates round to the same ``(a, b)`` only the + brightest is kept. The result is a compact reference lattice stored on + :attr:`reference_ab` / :attr:`reference_qpos` / :attr:`reference_intensity` + which :meth:`fit_lattice` matches against at every scan position. This step + is quick and lightweight. + + With ``plot=True`` (default) the reference lattice is drawn over the Bragg + vector map, each site ringed and labelled with its ``(a, b)`` index. The ring + color encodes how far the picked candidate sits from its ideal lattice site + ``origin + a*g1 + b*g2``, so a mis-picked or duplicate candidate (which rings + far from zero offset) is easy to spot. + + Parameters + ---------- + plot : bool, default=True + If ``True``, show the reference lattice via + :func:`~quantem.diffraction.bragg_vectors_visualization.plot_reference_lattice`. + returnfig : bool, default=False + If ``True``, return ``(fig, ax)`` instead of ``self`` (implies ``plot``). + **show_kwargs + Display-scaling options forwarded to the plot's + :func:`~quantem.core.visualization.show_2d` call (e.g. ``norm``, + ``vmin``, ``vmax``, ``cmap``). + + Returns + ------- + BraggVectors or tuple + ``self`` for method chaining; or ``(fig, ax)`` when ``returnfig=True``. + """ + if self.candidates_rc is None or self.candidates_intensity is None: + raise ValueError("Run choose_basis_vectors() before index_peaks().") + if self.origin is None or self.g1 is None or self.g2 is None: + raise ValueError("Run choose_basis_vectors() before index_peaks().") + + cand_rc = self.candidates_rc + cand_int = self.candidates_intensity + ab = _index_directions(cand_rc, self.origin, self.g1, self.g2) + + # Dedupe by (a, b): candidates are brightest-first, so the first occurrence + # of each index is the brightest -- keep it, drop the dimmer duplicates. + seen: dict[tuple[int, int], int] = {} + for i, (a, b) in enumerate(ab): + key = (int(a), int(b)) + if key not in seen: + seen[key] = i + keep = np.array(sorted(seen.values()), dtype=int) + + self.reference_ab = ab[keep] + self.reference_qpos = cand_rc[keep] + self.reference_intensity = cand_int[keep] + + if not (plot or returnfig): + return self + + fig, ax = plot_reference_lattice( + np.asarray(self.bvm.array), + self.reference_qpos, + self.reference_ab, + self.origin, + self.g1, + self.g2, + **show_kwargs, + ) + if returnfig: + return fig, ax + return self + + def fit_lattice( + self, + min_num_peaks: int = 5, + max_peak_shift: float | None = None, + *, + progressbar: bool = True, + plot: bool = True, + returnfig: bool = False, + ): + """Per-position weighted least-squares fit of the lattice vectors. + + This is the heavy step. At every scan position the detected peaks are + matched to the *ideal* lattice sites from :meth:`index_peaks` — + ``origin + a*g1 + b*g2`` for each reference ``(a, b)`` — keeping a peak only + when it lands within ``max_peak_shift`` of its nearest ideal site (not the + measured candidate position), then ``q = x0 + a*g1 + b*g2`` is fit by + intensity-weighted least squares over the matched peaks. The fitted ``g1``/``g2`` go into :attr:`u_array`/ + :attr:`v_array` (shape ``(scan_row, scan_col, 2)``, row/col components); + positions with fewer than ``min_num_peaks`` matched peaks are left ``nan``. + + Two diagnostics are stored per position. :attr:`fit_error` is the RMS fit + residual over the *matched* peaks, in pixels. :attr:`mask_weight` is a lattice + *order parameter* in ``0``–``1``: every detected peak is snapped to the + nearest site of the just-fitted lattice and the intensity-weighted RMS of + those displacements (the zero beam excluded) is normalized by + ``sqrt(|g1 x g2| / 2*pi)`` — the RMS displacement expected from intensity + scattered at random with no lattice — as ``1 - RMS_all / rms_rand`` clipped to + ``[0, 1]``. Because it weighs *all* detected intensity against the lattice + (not just the matched peaks, as :attr:`fit_error` does), a clean single + crystal approaches ``1`` while positions with strong off-lattice intensity (a + second grain, a mis-index, many spurious peaks) fall toward ``0``; weak false + positives carry little intensity and barely move it. Positions with no valid + fit (vacuum, fewer than ``min_num_peaks``) are ``0``. It is the default + reference weighting handed to :meth:`calculate_strain_map`. + + Parameters + ---------- + min_num_peaks : int, default=5 + Minimum number of matched peaks required to fit a position; positions + with fewer are left ``nan``. + max_peak_shift : float, optional + Inclusion radius in pixels: a detected peak is kept only if it lands + within this distance of its nearest *ideal* lattice site + (``origin + a*g1 + b*g2``), excluding peaks that stray too far from where + the best-fit lattice predicts. Defaults to ``0.5 * min(|g1|, |g2|)`` — + half the shorter lattice spacing. + progressbar : bool, default=True + If ``True``, show a tqdm progress bar over the scan positions. + plot : bool, default=True + If ``True``, show the fit diagnostics (mask weight + RMS error) via + :func:`~quantem.diffraction.bragg_vectors_visualization.plot_lattice_fit`. + returnfig : bool, default=False + If ``True``, return ``(fig, ax)`` instead of ``self`` (implies ``plot``). + + Returns + ------- + BraggVectors or tuple + ``self`` for method chaining; or ``(fig, ax)`` when ``returnfig=True``. + """ + if self.peaks is None: + raise ValueError("Run detect_disks() before fit_lattice().") + if self.reference_ab is None or self.reference_qpos is None: + raise ValueError("Run index_peaks() before fit_lattice().") + + ref_ab = self.reference_ab.astype(float) + + # Ideal lattice sites for the reference (a, b) set: origin + a*g1 + b*g2. + # Per-position detections are matched to these *ideal* points (not the measured + # candidate positions), so a peak is included only when it lands within + # max_peak_shift of where the best-fit lattice predicts it should be. + o = np.asarray(self.origin, dtype=float).reshape(2) + g1 = np.asarray(self.g1, dtype=float).reshape(2) + g2 = np.asarray(self.g2, dtype=float).reshape(2) + ideal_qpos = o[None, :] + ref_ab[:, 0:1] * g1[None, :] + ref_ab[:, 1:2] * g2[None, :] + + if max_peak_shift is None: + max_peak_shift = 0.5 * float(min(np.linalg.norm(g1), np.linalg.norm(g2))) + + # Normalization scale for the mask weight: the intensity-weighted RMS + # peak-to-nearest-site displacement expected from intensity scattered at + # random (uniformly over a unit cell of area |g1 x g2|) -- i.e. with no + # lattice order at all. sqrt(A / 2pi) is the equal-area-disk value; the mask + # weight is then 1 - RMS_all / rms_rand, a lattice order parameter in [0, 1]. + cell_area = abs(float(g1[0] * g2[1] - g1[1] * g2[0])) + rms_rand = float(np.sqrt(cell_area / (2.0 * np.pi))) if cell_area > 0 else 1.0 + + scan_r, scan_c = int(self.dataset.shape[0]), int(self.dataset.shape[1]) + u_array = np.full((scan_r, scan_c, 2), np.nan, dtype=float) + v_array = np.full((scan_r, scan_c, 2), np.nan, dtype=float) + mask_weight = np.zeros((scan_r, scan_c), dtype=float) + fit_error = np.full((scan_r, scan_c), np.nan, dtype=float) + + fields = self.peaks.fields + i_qr, i_qc = fields.index("q_row"), fields.index("q_col") + i_int = fields.index("intensity") + + coords: Any = list(np.ndindex(scan_r, scan_c)) + if progressbar: + try: + from tqdm.auto import tqdm + + coords = tqdm(coords, desc="fit_lattice", leave=True) + except Exception: + pass + + for r, c in coords: + cell = self.peaks[r, c].array + if cell.shape[0] == 0: + continue + qpos = cell[:, [i_qr, i_qc]] + inten = cell[:, i_int] + + # nearest *ideal* lattice site for each detected peak, kept if close enough + d = np.linalg.norm(qpos[:, None, :] - ideal_qpos[None, :, :], axis=2) + nearest = np.argmin(d, axis=1) + matched = d[np.arange(d.shape[0]), nearest] <= max_peak_shift + + if int(matched.sum()) < min_num_peaks: + continue + + beta, rms = _fit_lattice_vectors( + qpos[matched, 0], + qpos[matched, 1], + ref_ab[nearest[matched], 0], + ref_ab[nearest[matched], 1], + inten[matched], + ) + if beta is None: + continue + u_array[r, c] = beta[1] + v_array[r, c] = beta[2] + fit_error[r, c] = rms + + # mask weight = lattice "order parameter": snap EVERY detected peak to the + # nearest site of the just-fitted lattice (beta = [x0, g1, g2]) and take + # the intensity-weighted RMS of those displacements, excluding the zero + # beam. Unlike fit_error (matched peaks only), this sees OFF-lattice + # intensity -- a second grain, a mis-index, or many spurious peaks drive + # it up -- while weak false positives, carrying little intensity, barely + # move it. Normalized by rms_rand: 1 (all intensity on the lattice) down + # to 0 (scattered as if there were no lattice). + x0 = beta[0] + mat = np.stack([beta[1], beta[2]]) # (2, 2): rows g1, g2 + ab = np.rint((qpos - x0[None, :]) @ np.linalg.inv(mat)) + sites = x0[None, :] + ab @ mat + disp = np.linalg.norm(qpos - sites, axis=1) + nonzero = ~np.all(ab == 0, axis=1) # drop the central (zero) beam + w = np.clip(inten[nonzero], 0.0, None) + wsum = float(w.sum()) + if wsum > 0: + rms_all = float(np.sqrt(np.sum(w * disp[nonzero] ** 2) / wsum)) + mask_weight[r, c] = float(np.clip(1.0 - rms_all / rms_rand, 0.0, 1.0)) + + self.u_array = u_array + self.v_array = v_array + self.mask_weight = mask_weight + self.fit_error = fit_error + self.metadata["fit"] = { + "min_num_peaks": int(min_num_peaks), + "max_peak_shift": float(max_peak_shift), + } + + if not (plot or returnfig): + return self + + fig, ax = plot_lattice_fit(mask_weight, fit_error) + if returnfig: + return fig, ax + return self + + def calculate_strain_map( + self, + u_ref: np.ndarray | None = None, + v_ref: np.ndarray | None = None, + mask: np.ndarray | None = None, + ) -> StrainMap: + """Build a :class:`StrainMap` from the fitted per-position lattice vectors. + + Parameters + ---------- + u_ref : np.ndarray, optional + ``(2,)`` reference for the first lattice vector. Defaults to the median + over the scan inside :class:`StrainMap`. + v_ref : np.ndarray, optional + ``(2,)`` reference for the second lattice vector. Defaults to the median + over the scan inside :class:`StrainMap`. + mask : np.ndarray, optional + ``(scan_row, scan_col)`` per-position weighting used when computing the + reference lattice. Defaults to :attr:`mask_weight` from + :meth:`fit_lattice` (the lattice order parameter — how well all detected + intensity snaps to the fitted lattice), so clean single-crystal positions + dominate the reference and positions with off-lattice intensity are + down-weighted. + + Returns + ------- + StrainMap + A strain map initialized from the fitted lattice vectors. + """ + if self.u_array is None or self.v_array is None: + raise ValueError("Run fit_lattice() before calculate_strain_map().") + + if mask is None: + mask = self.mask_weight + + ds_sampling = float(self.dataset.sampling[0]) + ds_units = str(self.dataset.units[0]) + + return StrainMap( + u_array=self.u_array, + v_array=self.v_array, + ds_shape=tuple(self.dataset.shape), + real_space=self.real_space, + u_ref=u_ref, + v_ref=v_ref, + mask=mask, + ds_sampling=ds_sampling, + ds_units=ds_units, + ) + + # ---- visualization ---- + + def show_template( + self, + position: tuple[int, int] = (0, 0), + *, + crop_factor: float | None = None, + returnfig: bool = False, + **kwargs, + ): + """Plot the mean diffraction pattern, the template, and one correlation map. + + Parameters + ---------- + position : tuple of int, default=(0, 0) + ``(row, col)`` scan position whose correlation map is shown. + crop_factor : float, optional + If given, zoom to a square window of half-width ``crop_factor * radius`` + about the central-beam center, where ``radius`` is the central-beam + radius (the synthetic template radius if known, else estimated from the + mean diffraction pattern). The mean-diffraction and correlation panels are + centered on the beam; the template panel on its own (fftshifted) center. + For example, ``crop_factor=2.0`` shows two beam radii either side of the + center. The window is clamped to the detector, so a large factor shows the + full image. ``None`` (default) shows the full panels. + returnfig : bool, default=False + If ``True``, return the ``(fig, ax)`` for further customization. + **kwargs + Extra keyword arguments forwarded to + :func:`~quantem.diffraction.bragg_vectors_visualization.plot_template`. + + Returns + ------- + tuple + ``(fig, ax)`` when ``returnfig=True``; otherwise nothing. + """ + if self._template is None: + raise ValueError("Run a make_template_* method before show_template().") + r, c = int(position[0]), int(position[1]) + dp_mean = np.asarray(self.dataset.dp_mean.array) + + crop = None + if crop_factor is not None: + # Center the mean-diffraction and correlation panels on the actual + # central-beam position rather than the geometric center (H//2, W//2): + # the unscattered beam -- and the correlation peak that matches it -- are + # generally offset by a few pixels from the detector center. + center, radius_est = estimate_central_beam(dp_mean) + radius = self.metadata.get("template", {}).get("radius") + if radius is None: + radius = radius_est + crop = (float(center[0]), float(center[1]), float(crop_factor) * float(radius)) + + fig, ax = plot_template( + dp_mean, + self.template, + self.correlation_map(r, c), + (r, c), + crop=crop, + **kwargs, + ) + if returnfig: + return fig, ax + + def show_diffraction( + self, + inds: list[tuple[int, int]], + image: np.ndarray | None = None, + *, + ncols: int = 4, + image_kwargs: dict | None = None, + marker_radius: float | None = None, + linewidth: float = 0.5, + sigma_plot: float | None = None, + returnfig: bool = False, + **show_kwargs, + ): + """Preview the diffraction patterns at ``inds``, optionally beside a navigation image. + + No detection is run; this is for choosing scan positions to tune on. The + patterns are tiled ``ncols`` wide and rendered with + :func:`~quantem.core.visualization.show_2d`; the navigation image is styled + independently (real space and reciprocal space rarely want the same + scaling). + + Parameters + ---------- + inds : list of tuple of int + ``(row, col)`` scan positions to preview. + image : np.ndarray or Dataset2d, optional + Real-space navigation image (e.g. a virtual dark-field image) shown on + the left with the positions marked. ``None`` (default) shows only the + diffraction tiles. + ncols : int, default=4 + Number of diffraction tiles per row. + image_kwargs : dict, optional + Keyword arguments styling the navigation image (passed to + :func:`~quantem.core.visualization.show_2d`). + marker_radius : float, optional + Radius in image pixels of the scan-position marker rings. + linewidth : float, default=0.5 + Stroke width of the scan-position markers. + sigma_plot : float, optional + Gaussian blur (sigma) applied to the *displayed* patterns only. + returnfig : bool, default=False + If ``True``, return the ``(fig, ax)`` for further customization. + **show_kwargs + Extra keyword arguments (e.g. ``norm``, ``cmap``, ``cbar``, ``axsize``) + styling the diffraction tiles via + :func:`~quantem.core.visualization.show_2d`. + + Returns + ------- + tuple + ``(fig, ax)`` when ``returnfig=True``; otherwise nothing. + """ + if image is None: + image_arr = None + image_title = "navigation image" + else: + image_title = getattr(image, "name", None) or "navigation image" + image_arr = np.asarray(image.array if hasattr(image, "array") else image) + dps = [np.asarray(self.dataset.array[r, c], dtype=float) for r, c in inds] + fig, ax = plot_diffraction_grid( + image_arr, + dps, + inds, + ncols=ncols, + image_title=image_title, + image_kwargs=image_kwargs, + marker_radius=marker_radius, + linewidth=linewidth, + sigma_plot=sigma_plot, + **show_kwargs, + ) + if returnfig: + return fig, ax + + def show_detection( + self, + positions: list[tuple[int, int]] | None = None, + *, + min_abs_intensity: float = 0.0, + min_spacing: float = 0.0, + edge_boundary: int = 1, + subpixel: str = "upsample", + upsample_factor: int = 16, + max_num_peaks: int = 1000, + background_sigma: float | str | None = None, + image: np.ndarray | None = None, + peak_radius: float = 6.0, + marker_radius: float | None = None, + linewidth: float = 1.0, + sigma_plot: float | None = None, + image_kwargs: dict | None = None, + returnfig: bool = False, + **plot_kwargs, + ): + """Detect on a few patterns and overlay the peaks, for tuning hyperparameters. + + The detection keywords match :meth:`detect_disks`. The workflow state + (:attr:`peaks`/:attr:`bvm`) is left untouched, so this is safe to re-run + while tuning; for the raw peaks, call :meth:`detect_disks` with the same + ``positions``. + + Parameters + ---------- + positions : list of tuple of int, optional + ``(row, col)`` scan positions to detect on. ``None`` (default) + auto-samples four positions spread across the scan. + min_abs_intensity : float, default=0.0 + Drop correlation peaks below this absolute intensity. + min_spacing : float, default=0.0 + Minimum spacing in pixels between kept peaks. + edge_boundary : int, default=1 + Width in pixels of the border in which peaks are ignored. + subpixel : {"none", "parabolic", "upsample"}, default="upsample" + Subpixel refinement mode; see :meth:`detect_disks`. + upsample_factor : int, default=16 + Upsampling factor for the ``"upsample"`` subpixel refinement. + max_num_peaks : int, default=1000 + Maximum number of peaks to keep per pattern. + image : np.ndarray or Dataset2d, optional + Real-space navigation image (e.g. a virtual dark-field image) shown on + the left with the chosen positions marked. ``None`` (default) shows only + the diffraction tiles. + peak_radius : float, default=6.0 + Radius in diffraction pixels of the cyan rings drawn at detected peaks. + marker_radius : float, optional + Radius in image pixels of the scan-position marker rings. + linewidth : float, default=1.0 + Stroke width of the peak and scan-position rings. + sigma_plot : float, optional + Gaussian blur (sigma) applied to the *displayed* patterns only; + detection still uses the raw data. + image_kwargs : dict, optional + Keyword arguments styling the navigation image (passed to + :func:`~quantem.core.visualization.show_2d`). + returnfig : bool, default=False + If ``True``, return the ``(fig, ax)`` for further customization. + **plot_kwargs + Extra keyword arguments (e.g. ``ncols``, ``norm``, ``cmap``, ``cbar``, + ``axsize``) styling the diffraction tiles via + :func:`~quantem.core.visualization.show_2d`. + + Returns + ------- + tuple + ``(fig, ax)`` when ``returnfig=True``; otherwise nothing. + """ + if positions is None: + positions = self._sample_positions() + sub = self.detect_disks( + positions=positions, + min_abs_intensity=min_abs_intensity, + min_spacing=min_spacing, + edge_boundary=edge_boundary, + subpixel=subpixel, + upsample_factor=upsample_factor, + max_num_peaks=max_num_peaks, + background_sigma=background_sigma, + progressbar=False, + ) + if image is None: + image_arr = None + image_title = "virtual image" + else: + image_title = getattr(image, "name", None) or "virtual image" + image_arr = np.asarray(image.array if hasattr(image, "array") else image) + dps = [np.asarray(self.dataset.array[r, c], dtype=float) for r, c in positions] + peaks = [sub[i].array for i in range(len(positions))] + fig, ax = plot_detection( + image_arr, + dps, + peaks, + positions, + peak_radius=peak_radius, + marker_radius=marker_radius, + linewidth=linewidth, + sigma_plot=sigma_plot, + image_title=image_title, + image_kwargs=image_kwargs, + **plot_kwargs, + ) + if returnfig: + return fig, ax + + def peak_histogram(self, *, returnfig: bool = False, **kwargs): + """Plot the Bragg vector map beside the per-position peak count. + + The Bragg vector map is the 2-D histogram of all detected peak positions + (intensity-weighted) accumulated over the scan; the second panel shows the + number of peaks detected at each scan position. + + Parameters + ---------- + returnfig : bool, default=False + If ``True``, return the ``(fig, ax)`` for further customization. + **kwargs + Extra keyword arguments forwarded to + :func:`~quantem.diffraction.bragg_vectors_visualization.plot_bvm`. + + Returns + ------- + tuple + ``(fig, ax)`` when ``returnfig=True``; otherwise nothing. + """ + if self.peaks is None or self.bvm is None: + raise ValueError("Run detect_disks() before peak_histogram().") + scan_r, scan_c = int(self.dataset.shape[0]), int(self.dataset.shape[1]) + # row_counts() reads cell_lengths directly; far cheaper than building a + # per-cell view (self.peaks[r, c]) just to read its row count. + counts = np.asarray(self.peaks.row_counts(), dtype=int).reshape(scan_r, scan_c) + fig, ax = plot_bvm(np.asarray(self.bvm.array), counts, **kwargs) + if returnfig: + return fig, ax + + # ---- helpers ---- + + def _resolve_background_sigma( + self, background_sigma: float | str | None + ) -> float | None: + """Resolve the ``background_sigma`` argument to a value in pixels. + + ``"auto"`` (the default everywhere) maps to twice the central-beam + radius: wide enough that the disk-scale correlation peaks pass + untouched, narrow enough to remove the zero-sum template's negative + moat around a bright unscattered beam -- which otherwise pushes weak + disk peaks below zero, where the correlation clamp erases them before + peak finding. Pass ``None`` to disable the background subtraction or a + float to set the scale explicitly. + """ + if background_sigma is None: + return None + if isinstance(background_sigma, str): + if background_sigma != "auto": + raise ValueError("background_sigma must be a float, None, or 'auto'.") + radius = self.metadata.get("template", {}).get("radius") + if radius is None: + dp_mean = np.asarray(self.dataset.dp_mean.array) + _, radius = estimate_central_beam(dp_mean) + return 2.0 * float(radius) + return float(background_sigma) + + def _set_template( + self, + probe: torch.Tensor, + center: tuple[float, float] | None, + subtract_mean: bool, + ) -> None: + """Validate the probe shape, then store the template and its conjugate FT. + + Parameters + ---------- + probe : torch.Tensor + Probe image; must match the diffraction-pattern shape. + center : tuple of float or None + ``(row, col)`` probe center rolled to the origin, or ``None`` for the + geometric center. + subtract_mean : bool + If ``True``, make the template zero-sum. + """ + dp_shape = tuple(self.dataset.shape[-2:]) + probe_t = torch.as_tensor(probe, dtype=torch.float, device=self.device) + if tuple(probe_t.shape) != dp_shape: + raise ValueError( + f"probe shape {tuple(probe_t.shape)} does not match diffraction pattern " + f"shape {dp_shape}." + ) + self._template = make_template(probe_t, center=center, subtract_mean=subtract_mean) + self._template_ft = template_fourier(self._template) + + def _sample_positions(self) -> list[tuple[int, int]]: + """Four scan positions spread across the field (quadrant centers). + + Returns + ------- + list of tuple of int + Up to four ``(row, col)`` scan positions at the quadrant centers. + """ + R, C = int(self.dataset.shape[0]), int(self.dataset.shape[1]) + rs = sorted({min(max(R // 4, 0), R - 1), min(max(3 * R // 4, 0), R - 1)}) + cs = sorted({min(max(C // 4, 0), C - 1), min(max(3 * C // 4, 0), C - 1)}) + return [(r, c) for r in rs for c in cs] + + def _detect_positions( + self, + coords: list[tuple[int, int]], + detect_kwargs: dict[str, Any], + batch_size: int | None, + *, + progressbar: bool, + ) -> list[NDArray]: + """Batched detection over a list of ``(row, col)`` scan positions. + + Patterns are stacked into chunks of ``batch_size`` and passed to + :func:`~quantem.diffraction.disk_detection.detect_disks_batch`. + + Parameters + ---------- + coords : list of tuple of int + ``(row, col)`` scan positions to detect on. + detect_kwargs : dict + Keyword arguments forwarded to + :func:`~quantem.diffraction.disk_detection.detect_disks_batch`. + batch_size : int or None + Number of patterns per batch. ``None`` picks a size from the detector + dimensions. + progressbar : bool + If ``True``, show a tqdm progress bar over the patterns. + + Returns + ------- + list of np.ndarray + One ``(M, 3)`` array of ``[q_row, q_col, intensity]`` per position, in + ``coords`` order. + """ + H, W = int(self.dataset.shape[-2]), int(self.dataset.shape[-1]) + if batch_size is None: + batch_size = int(min(1024, max(1, 16_000_000 // (H * W)))) + + it = range(0, len(coords), batch_size) + if progressbar: + try: + from tqdm.auto import tqdm + + bar = tqdm(total=len(coords), desc="detect_disks", leave=True) + except Exception: + bar = None + else: + bar = None + + results: list[NDArray] = [] + for start in it: + chunk = coords[start : start + batch_size] + dps = torch.stack( + [ + torch.as_tensor( + np.asarray(self.dataset.array[r, c]), + dtype=torch.float, + device=self.device, + ) + for r, c in chunk + ], + dim=0, + ) + out = detect_disks_batch(dps, self._template_ft, **detect_kwargs) + results.extend( + arr if arr.shape[0] else np.empty((0, len(PEAK_FIELDS)), dtype=float) + for arr in out + ) + if bar is not None: + bar.update(len(chunk)) + + if bar is not None: + bar.close() + return results + + def _bvm_candidates( + self, num_candidates: int, min_spacing: float, min_abs_intensity: float + ) -> tuple[NDArray, NDArray]: + """Find the brightest, well-separated local maxima in the Bragg vector map. + + Parameters + ---------- + num_candidates : int + Maximum number of candidate peaks to return. + min_spacing : float + Minimum spacing in pixels between candidates. + min_abs_intensity : float + Drop candidates below this absolute intensity. + + Returns + ------- + cand_rc : np.ndarray + ``(N, 2)`` ``[row, col]`` candidate positions, brightest first. + cand_int : np.ndarray + ``(N,)`` candidate intensities. + """ + from quantem.diffraction.disk_detection import _filter_maxima, _local_maxima + + bvm = torch.as_tensor(self.bvm.array, dtype=torch.float) + peaks = _local_maxima(bvm, edge_boundary=1) + peaks = _filter_maxima(peaks, min_abs_intensity, min_spacing, num_candidates) + arr = peaks.detach().cpu().numpy() + return arr[:, :2].astype(float), arr[:, 2].astype(float) + + +def _fractional_indices(q: NDArray, origin: NDArray, g1: NDArray, g2: NDArray) -> NDArray: + """Continuous (unrounded) ``(a, b)`` lattice coordinates of peaks ``q``. + + Solves ``B [a, b]^T = q - origin`` (least squares), with + ``B = [[g1_row, g2_row], [g1_col, g2_col]]``. + + Parameters + ---------- + q : np.ndarray + ``(N, 2)`` ``[row, col]`` peak positions. + origin : np.ndarray + ``(2,)`` lattice origin ``[row, col]``. + g1 : np.ndarray + ``(2,)`` first lattice vector ``[row, col]``. + g2 : np.ndarray + ``(2,)`` second lattice vector ``[row, col]``. + + Returns + ------- + np.ndarray + ``(N, 2)`` float array of continuous ``[a, b]`` coordinates. + """ + if q.shape[0] == 0: + return np.empty((0, 2), dtype=float) + beta = np.array([[g1[0], g2[0]], [g1[1], g2[1]]], dtype=float) + alpha = (q - origin[None, :]).T # (2, N) + return np.linalg.lstsq(beta, alpha, rcond=None)[0].T # (N, 2) + + +def _index_directions(q: NDArray, origin: NDArray, g1: NDArray, g2: NDArray) -> NDArray: + """Integer Miller indices for peaks ``q`` against basis ``(g1, g2)`` about ``origin``. + + Rounds the continuous coordinates from :func:`_fractional_indices`. + + Parameters + ---------- + q : np.ndarray + ``(N, 2)`` ``[row, col]`` peak positions. + origin : np.ndarray + ``(2,)`` lattice origin ``[row, col]``. + g1 : np.ndarray + ``(2,)`` first lattice vector ``[row, col]``. + g2 : np.ndarray + ``(2,)`` second lattice vector ``[row, col]``. + + Returns + ------- + np.ndarray + ``(N, 2)`` int array of ``[a, b]`` Miller indices. + """ + if q.shape[0] == 0: + return np.empty((0, 2), dtype=int) + return np.round(_fractional_indices(q, origin, g1, g2)).astype(int) + + +def _fit_lattice_vectors( + q_row: NDArray, + q_col: NDArray, + a: NDArray, + b: NDArray, + intensity: NDArray, +) -> tuple[NDArray | None, NDArray | None]: + """Intensity-weighted lattice fit ``q = x0 + a*g1 + b*g2`` for one pattern. + + Parameters + ---------- + q_row : np.ndarray + ``(N,)`` peak row positions. + q_col : np.ndarray + ``(N,)`` peak column positions. + a : np.ndarray + ``(N,)`` Miller index along ``g1``. + b : np.ndarray + ``(N,)`` Miller index along ``g2``. + intensity : np.ndarray + ``(N,)`` peak intensities, used as fit weights (``sqrt`` of the clamped + intensity). + + Returns + ------- + beta : np.ndarray or None + ``(3, 2)`` fit ``[x0; g1; g2]`` (row/col components per row), or ``None`` if + the fit is rank-deficient (e.g. all peaks share one lattice row). + rms : float + RMS fit residual in pixels (``nan`` when ``beta`` is ``None``). + """ + design = np.stack([np.ones_like(a), a, b], axis=1) # (N, 3) + target = np.stack([q_row, q_col], axis=1) # (N, 2) + w = np.sqrt(np.clip(intensity, 0.0, None))[:, None] + + if np.linalg.matrix_rank(design * w) < 3: + return None, float("nan") + + beta = np.linalg.lstsq(design * w, target * w, rcond=None)[0] # (3, 2): x0, g1, g2 + resid = target - design @ beta + rms = float(np.sqrt(np.mean(np.sum(resid**2, axis=1)))) + return beta, rms diff --git a/src/quantem/diffraction/bragg_vectors_visualization.py b/src/quantem/diffraction/bragg_vectors_visualization.py new file mode 100644 index 000000000..f2cb4af26 --- /dev/null +++ b/src/quantem/diffraction/bragg_vectors_visualization.py @@ -0,0 +1,776 @@ +from __future__ import annotations + +import matplotlib.pyplot as plt +import numpy as np + + +def plot_template( + dp_mean: np.ndarray, + template: np.ndarray, + corr_map: np.ndarray, + position: tuple[int, int], + *, + crop: tuple[float, float, float] | None = None, + figsize: tuple[float, float] = (13, 4), +): + """Mean diffraction pattern, the (centered) template, and one correlation map. + + Parameters + ---------- + dp_mean : np.ndarray + Mean diffraction pattern. + template : np.ndarray + The correlation template, centered for display. + corr_map : np.ndarray + Correlation map computed at ``position``. + position : tuple of int + ``(row, col)`` scan position the correlation map was computed at. + crop : tuple of float, optional + ``(center_row, center_col, half_width)`` zoom window (in pixels). The mean + diffraction and correlation panels are centered on ``(center_row, + center_col)`` -- the central-beam position -- while the template panel is + centered on its own array center (it is displayed fftshifted to there). The + view spans ``half_width`` either side of the center, clamped to each image's + bounds, so an over-large ``half_width`` just shows the full image. ``None`` + (default) shows the full panels. + figsize : tuple of float, default=(13, 4) + Figure size in inches. + + Returns + ------- + tuple + ``(fig, ax)`` with ``ax`` a length-3 array of axes. + """ + fig, ax = plt.subplots(1, 3, figsize=figsize) + ax[0].imshow(dp_mean, cmap="gray") + ax[0].set_title("mean diffraction") + ax[1].imshow(template, cmap="gray") + ax[1].set_title("template (centered)") + ax[2].imshow(corr_map, cmap="viridis") + ax[2].set_title(f"correlation @ {tuple(position)}") + for a in ax: + a.set_xticks([]) + a.set_yticks([]) + if crop is not None: + cr, cc, hw = float(crop[0]), float(crop[1]), float(crop[2]) + th, tw = template.shape[:2] + # The mean-diffraction beam and the correlation peak sit at the beam center + # (cr, cc); the displayed template is fftshifted to its own array center, so + # zoom that panel about its center instead. + panel_centers = ((cr, cc), (th / 2.0, tw / 2.0), (cr, cc)) + for a, img, (ecr, ecc) in zip(ax, (dp_mean, template, corr_map), panel_centers): + h, w = img.shape[:2] + a.set_xlim(max(ecc - hw, -0.5), min(ecc + hw, w - 0.5)) + a.set_ylim(min(ecr + hw, h - 0.5), max(ecr - hw, -0.5)) + fig.tight_layout() + return fig, ax + + +def _mark_positions(ax, positions, *, radius=None, linewidth=0.5): + """Overlay numbered red markers at each ``(row, col)`` scan position. + + Parameters + ---------- + ax : matplotlib.axes.Axes + Axis to draw the markers on. + positions : sequence of tuple of int + ``(row, col)`` scan positions to mark, numbered in order. + radius : float, optional + Marker radius in image pixels. If given, each marker is a ring of that + radius drawn as a circle patch in data coordinates; if ``None``, a fixed + screen-size scatter marker is used instead. + linewidth : float, default=0.5 + Ring stroke width. + """ + from matplotlib.patches import Circle + + for i, (r, c) in enumerate(positions): + if radius is None: + ax.scatter(c, r, s=70, facecolors="none", edgecolors="red", linewidths=linewidth) + else: + ax.add_patch( + Circle((c, r), radius=radius, fill=False, edgecolor="red", linewidth=linewidth) + ) + ax.annotate( + str(i), + (c, r), + color="red", + fontsize=11, + fontweight="bold", + xytext=(4, 4), + textcoords="offset points", + ) + + +def _blur(dp: np.ndarray, sigma: float | None) -> np.ndarray: + """Gaussian-blur a diffraction pattern for display only (passthrough if ``sigma`` falsy). + + Smoothing the *displayed* pattern (the detection still runs on the raw data) + makes it easier to judge by eye which correlation peaks sit on real disks and + which are noise. + + Parameters + ---------- + dp : np.ndarray + Diffraction pattern to blur. + sigma : float or None + Gaussian blur width in pixels. If falsy (``None``, ``0``, or negative), + ``dp`` is returned unchanged. + + Returns + ------- + np.ndarray + The blurred pattern, or ``dp`` unchanged when ``sigma`` is falsy. + """ + if not sigma or sigma <= 0: + return dp + from scipy.ndimage import gaussian_filter + + return gaussian_filter(np.asarray(dp, dtype=float), float(sigma)) + + +def _grid_axes(n, ncols, *, show_image, axsize=None, figsize=None): + """Figure with an optional left nav-image axis and a right ``nrows x ncols`` tile grid. + + With ``show_image`` a navigation-image axis is placed to the left of the tile + grid; otherwise the figure is just the grid. + + Parameters + ---------- + n : int + Number of tiles to lay out. + ncols : int + Number of tile columns, clamped to ``n``. + show_image : bool + If ``True``, add a navigation-image axis to the left of the tile grid. + axsize : tuple of float, optional + Per-tile size in inches, ``(w, h)``. Sizes the figure when ``figsize`` is + not given, so a larger ``axsize`` zooms every tile. + figsize : tuple of float, optional + Explicit figure size in inches; overrides the ``axsize``-derived size. + + Returns + ------- + tuple + ``(fig, ax_image, dp_axes, ncols)`` where ``ax_image`` is ``None`` when + ``show_image`` is ``False``, ``dp_axes`` is an ``(nrows, ncols)`` object + array of axes (trailing unused tiles already turned off), and ``ncols`` is + clamped to ``n``. + """ + ncols = max(1, min(ncols, n)) + nrows = int(np.ceil(n / ncols)) + + tile_w, tile_h = (float(axsize[0]), float(axsize[1])) if axsize is not None else (3.0, 3.2) + nav_w = (tile_w if axsize is not None else 4.0) if show_image else 0.0 + if figsize is None: + figsize = (nav_w + tile_w * ncols, max(tile_h, tile_h * nrows)) + + fig = plt.figure(figsize=figsize) + if show_image: + outer = fig.add_gridspec(1, 2, width_ratios=[nav_w, tile_w * ncols], wspace=0.12) + ax_image = fig.add_subplot(outer[0, 0]) + grid = outer[0, 1].subgridspec(nrows, ncols, wspace=0.08, hspace=0.2) + else: + ax_image = None + grid = fig.add_gridspec(nrows, ncols, wspace=0.08, hspace=0.2) + + dp_axes = np.empty((nrows, ncols), dtype=object) + for i in range(nrows * ncols): + a = fig.add_subplot(grid[i // ncols, i % ncols]) + dp_axes[i // ncols, i % ncols] = a + if i >= n: + a.axis("off") + return fig, ax_image, dp_axes, ncols + + +def plot_diffraction_grid( + image: np.ndarray | None, + dps: list[np.ndarray], + positions: list[tuple[int, int]], + *, + ncols: int = 4, + image_title: str = "navigation image", + image_kwargs: dict | None = None, + marker_radius: float | None = None, + linewidth: float = 0.5, + sigma_plot: float | None = None, + axsize: tuple[float, float] | None = None, + figsize: tuple[float, float] | None = None, + **show_kwargs, +): + """A tiled grid of the diffraction patterns at ``positions``, optionally beside a nav image. + + When ``image`` is given (e.g. a virtual dark-field image) it is drawn on the + left with each scan position marked as a numbered red marker at ``(x=col, + y=row)``; pass ``image=None`` to omit it and show only the grid. Right: the + diffraction patterns tiled ``ncols`` wide. Both are rendered with + :func:`~quantem.core.visualization.show_2d`, and the navigation image and the + diffraction tiles take separate, independent styling (``image_kwargs`` vs + ``show_kwargs``). + + Parameters + ---------- + image : np.ndarray or None + Navigation image (e.g. a virtual dark-field image) drawn at left, with the + scan positions marked. Pass ``None`` to show only the diffraction grid. + dps : list of np.ndarray + Diffraction patterns to tile, one per entry in ``positions``. + positions : list of tuple of int + ``(row, col)`` scan positions, used for the tile titles and the nav-image + markers. + ncols : int, default=4 + Number of columns in the diffraction-pattern tile grid. + image_title : str, default="navigation image" + Title for the navigation image. + image_kwargs : dict, optional + Extra keyword arguments (e.g. ``norm``, ``cmap``, ``scalebar``) for the + navigation image's :func:`show_2d` call, styling it independently of the + tiles. + marker_radius : float, optional + Scan-position marker radius in image pixels; ``None`` uses a fixed + screen-size marker. + linewidth : float, default=0.5 + Stroke width of the scan-position markers. + sigma_plot : float, optional + Gaussian blur width (pixels) applied to the *displayed* patterns only (the + data is untouched) to ease judging real features. + axsize : tuple of float, optional + Per-tile size in inches; a larger value zooms the tiles. + figsize : tuple of float, optional + Explicit figure size in inches. + **show_kwargs + Forwarded to :func:`show_2d` for the diffraction-pattern tiles (e.g. + ``norm``, ``cmap``, ``cbar``). + + Returns + ------- + tuple + ``(fig, (ax_image, dp_axes))`` with ``ax_image`` ``None`` when no image is + shown. + """ + from quantem.core.visualization import show_2d + + n = len(positions) + show_image = image is not None + fig, ax_image, dp_axes, ncols = _grid_axes( + n, ncols, show_image=show_image, axsize=axsize, figsize=figsize + ) + + if show_image: + image_show_kwargs = {"cmap": "gray", "title": image_title, **(image_kwargs or {})} + show_2d(np.asarray(image), figax=(fig, ax_image), **image_show_kwargs) + _mark_positions(ax_image, positions, radius=marker_radius, linewidth=linewidth) + + user_title = show_kwargs.pop("title", None) + for i in range(n): + r, c = positions[i] + title = user_title if user_title is not None else f"{i}: ({r},{c})" + show_2d( + _blur(dps[i], sigma_plot), + figax=(fig, dp_axes[i // ncols, i % ncols]), + title=title, + **show_kwargs, + ) + return fig, (ax_image, dp_axes) + + +def plot_detection( + image: np.ndarray | None, + dps: list[np.ndarray], + peaks: list[np.ndarray], + positions: list[tuple[int, int]], + *, + ncols: int = 4, + peak_radius: float = 6.0, + marker_radius: float | None = None, + linewidth: float = 0.5, + sigma_plot: float | None = None, + image_title: str = "virtual image", + image_kwargs: dict | None = None, + axsize: tuple[float, float] | None = None, + figsize: tuple[float, float] | None = None, + **show_kwargs, +): + """The diffraction patterns with detected peaks overlaid, optionally beside a nav image. + + When ``image`` is given (e.g. a virtual dark-field image) it is drawn on the + left with each chosen scan position drawn as a numbered red marker at ``(x=col, + y=row)``; pass ``image=None`` to omit it and show only the tiles. Right: the + diffraction patterns tiled ``ncols`` wide, each rendered with + :func:`~quantem.core.visualization.show_2d`, with the detected peaks overlaid as + cyan rings that trace the disks rather than obscuring them. + + Parameters + ---------- + image : np.ndarray or None + Navigation image (e.g. a virtual dark-field image) drawn at left, with the + scan positions marked. Pass ``None`` to show only the diffraction tiles. + dps : list of np.ndarray + Diffraction patterns to tile, one per entry in ``positions``. + peaks : list of np.ndarray + Detected peaks per pattern; ``peaks[i]`` is an ``(M, 3)`` array of + ``[q_row, q_col, intensity]``. Each peak is drawn as a cyan ring at + ``(x=q_col, y=q_row)``. + positions : list of tuple of int + ``(row, col)`` scan positions, used for the tile titles and the nav-image + markers. + ncols : int, default=4 + Number of columns in the diffraction-pattern tile grid. + peak_radius : float, default=6.0 + Radius of the cyan peak rings, in diffraction pixels. + marker_radius : float, optional + Scan-position marker radius in image pixels; ``None`` uses a fixed + screen-size marker. + linewidth : float, default=0.5 + Stroke width of both the scan-position markers and the peak rings. + sigma_plot : float, optional + Gaussian blur width (pixels) applied to the *displayed* patterns only + (detection still uses the raw data) to ease telling real disks from false + positives. + image_title : str, default="virtual image" + Title for the navigation image. + image_kwargs : dict, optional + Extra keyword arguments for the navigation image's :func:`show_2d` call, + styling it independently of the tiles. + axsize : tuple of float, optional + Per-tile size in inches; a larger value zooms the tiles. + figsize : tuple of float, optional + Explicit figure size in inches. + **show_kwargs + Forwarded to :func:`show_2d` for the diffraction-pattern tiles (e.g. + ``norm``, ``cmap``, ``cbar``). + + Returns + ------- + tuple + ``(fig, (ax_image, dp_axes))`` with ``ax_image`` ``None`` when no image is + shown. + """ + from matplotlib.patches import Circle + + from quantem.core.visualization import show_2d + + n = len(positions) + show_image = image is not None + fig, ax_image, dp_axes, ncols = _grid_axes( + n, ncols, show_image=show_image, axsize=axsize, figsize=figsize + ) + + if show_image: + image_show_kwargs = {"cmap": "gray", "title": image_title, **(image_kwargs or {})} + show_2d(np.asarray(image), figax=(fig, ax_image), **image_show_kwargs) + _mark_positions(ax_image, positions, radius=marker_radius, linewidth=linewidth) + + user_title = show_kwargs.pop("title", None) + for i in range(n): + a = dp_axes[i // ncols, i % ncols] + r, c = positions[i] + pk = peaks[i] + title = user_title if user_title is not None else f"{i}: ({r},{c}) n={pk.shape[0]}" + show_2d(_blur(dps[i], sigma_plot), figax=(fig, a), title=title, **show_kwargs) + for q in pk: + a.add_patch( + Circle( + (q[1], q[0]), + radius=peak_radius, + fill=False, + edgecolor="cyan", + linewidth=linewidth, + ) + ) + + return fig, (ax_image, dp_axes) + + +def plot_basis_vectors( + bvm: np.ndarray, + cand_rc: np.ndarray, + cand_int: np.ndarray, + origin: np.ndarray, + g1: np.ndarray, + g2: np.ndarray, + *, + cmap: str = "gray", + norm: str | dict = "log_auto", + zoom: bool = True, + figsize: tuple[float, float] = (6, 6), + **show_kwargs, +): + """The Bragg vector map with the candidate peaks, origin, and basis vectors overlaid. + + Every candidate peak is drawn as a numbered cyan ring; those numbers are the + indices accepted by + :meth:`~quantem.diffraction.bragg_vectors.BraggVectors.choose_basis_vectors` + for overriding ``origin``/``g1``/``g2`` by peak. The chosen origin is a green + marker and ``g1`` (red) / ``g2`` (blue) are arrows drawn from it, labelled at + their midpoints so the labels never sit on top of the candidate numbers. + + Parameters + ---------- + bvm : np.ndarray + Bragg vector map; rendered through + :func:`~quantem.core.visualization.show_2d` so its display scaling is + controlled by ``norm`` and ``show_kwargs``. + cand_rc : np.ndarray + ``(N, 2)`` ``[row, col]`` candidate peak positions, brightest first; the + ring labels are their row indices. + cand_int : np.ndarray + ``(N,)`` candidate intensities (unused for drawing; kept for parity with + the candidate API). + origin : np.ndarray + ``(row, col)`` chosen lattice origin. + g1 : np.ndarray + First lattice vector as a ``(row, col)`` offset from ``origin``. + g2 : np.ndarray + Second lattice vector as a ``(row, col)`` offset from ``origin``. + cmap : str, default="gray" + Colormap for the Bragg vector map; gray keeps the colored overlays legible. + norm : str or dict, default="log_auto" + Intensity scaling forwarded to :func:`show_2d` (e.g. ``"linear_auto"``, + ``"log_auto"``, ``"power_sqrt"``, or ``{"power": 0.5}``). + zoom : bool, default=True + If ``True``, frame the view to the candidate bounding box (plus a margin) + so the numbered peaks are large enough to read. + figsize : tuple of float, default=(6, 6) + Figure size in inches. + **show_kwargs + Extra keyword arguments forwarded to :func:`show_2d` for fine display + control (e.g. ``vmin``, ``vmax``, ``lower_quantile``, ``upper_quantile``). + + Returns + ------- + tuple + ``(fig, ax)``. + """ + import matplotlib.patheffects as path_effects + + from quantem.core.visualization import show_2d + + stroke = [path_effects.withStroke(linewidth=2.5, foreground="black")] + origin_color = (0.0, 0.7, 0.0) + g1_color = (1.0, 0.0, 0.0) + g2_color = (0.0, 0.7, 1.0) + + fig, ax = plt.subplots(figsize=figsize) + show_2d(np.asarray(bvm), figax=(fig, ax), cmap=cmap, norm=norm, **show_kwargs) + + cand_rc = np.asarray(cand_rc, dtype=float).reshape(-1, 2) + for i, (r, c) in enumerate(cand_rc): + ax.scatter(c, r, s=60, facecolors="none", edgecolors="cyan", linewidths=1.0, zorder=3) + ax.annotate( + str(i), + (c, r), + color="cyan", + fontsize=9, + fontweight="bold", + xytext=(4, 4), + textcoords="offset points", + path_effects=stroke, + zorder=4, + ) + + o = np.asarray(origin, dtype=float).reshape(2) + ax.scatter( + o[1], + o[0], + s=160, + marker="P", + facecolors=[origin_color], + edgecolors="white", + linewidths=1.5, + zorder=6, + ) + for g, label, color in ( + (np.asarray(g1, float), "g1", g1_color), + (np.asarray(g2, float), "g2", g2_color), + ): + tip = (o[1] + g[1], o[0] + g[0]) + ax.annotate( + "", + xy=tip, + xytext=(o[1], o[0]), + arrowprops=dict(arrowstyle="-|>", color=color, lw=2.4, shrinkA=0, shrinkB=0), + zorder=5, + ) + # Label at the arrow midpoint, nudged perpendicular to the shaft (in screen + # space) so it clears both the arrow and the candidate numbers on the peaks. + gnorm = float(np.hypot(g[0], g[1])) + 1e-12 + perp = (g[0] / gnorm * 15.0, g[1] / gnorm * 15.0) + mid = (o[1] + g[1] / 2.0, o[0] + g[0] / 2.0) + ax.annotate( + label, + mid, + color=color, + fontsize=14, + fontweight="bold", + xytext=perp, + textcoords="offset points", + ha="center", + va="center", + path_effects=stroke, + zorder=7, + ) + + if zoom and cand_rc.shape[0]: + rmin, cmin = cand_rc.min(axis=0) + rmax, cmax = cand_rc.max(axis=0) + margin = 0.12 * max(rmax - rmin, cmax - cmin, 1.0) + 6.0 + h, w = bvm.shape[:2] + ax.set_xlim(max(cmin - margin, -0.5), min(cmax + margin, w - 0.5)) + ax.set_ylim(min(rmax + margin, h - 0.5), max(rmin - margin, -0.5)) + + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_title("lattice basis (origin +, g1, g2; cyan = candidate index)") + fig.tight_layout() + return fig, ax + + +def plot_bvm( + bvm: np.ndarray, + counts: np.ndarray, + *, + figsize: tuple[float, float] = (10, 4), +): + """The Bragg vector map (log-scaled) beside the per-position peak count. + + Parameters + ---------- + bvm : np.ndarray + Bragg vector map; displayed log-scaled (``log1p``). + counts : np.ndarray + Per-position peak count, shape ``(scan_row, scan_col)``. + figsize : tuple of float, default=(10, 4) + Figure size in inches. + + Returns + ------- + tuple + ``(fig, ax)`` with ``ax`` a length-2 array of axes. + """ + fig, ax = plt.subplots(1, 2, figsize=figsize) + ax[0].imshow(np.log1p(bvm), cmap="inferno") + ax[0].set_title("Bragg vector map (log)") + im = ax[1].imshow(counts, cmap="viridis") + ax[1].set_title("peaks per position") + for a in ax: + a.set_xticks([]) + a.set_yticks([]) + fig.colorbar(im, ax=ax[1], fraction=0.046, pad=0.04) + fig.tight_layout() + return fig, ax + + +def plot_reference_lattice( + bvm: np.ndarray, + ref_qpos: np.ndarray, + ref_ab: np.ndarray, + origin: np.ndarray, + g1: np.ndarray, + g2: np.ndarray, + *, + cmap: str = "gray", + norm: str | dict = "log_auto", + zoom: bool = True, + figsize: tuple[float, float] = (6, 6), + **show_kwargs, +): + """The reference lattice from :meth:`BraggVectors.index_peaks`, drawn over the BVM. + + Each indexed reference site is a ring labelled with its ``(a, b)`` Miller index; + the chosen origin is a green marker and ``g1`` (red) / ``g2`` (blue) are arrows + from it. The ring color encodes how far the picked candidate sits from its *ideal* + lattice site ``origin + a*g1 + b*g2`` (the colorbar reads pixels), scaled to half + the shorter lattice spacing — the default :meth:`fit_lattice` match radius. A + mis-picked or duplicate candidate stands out as a ring far from zero offset (bright + color), sitting off the regular grid, or carrying an index that breaks the pattern. + + Parameters + ---------- + bvm : np.ndarray + Bragg vector map; rendered through + :func:`~quantem.core.visualization.show_2d` so its display scaling is + controlled by ``norm`` and ``show_kwargs``. + ref_qpos : np.ndarray + ``(N, 2)`` ``[row, col]`` reference site positions. + ref_ab : np.ndarray + ``(N, 2)`` integer ``[a, b]`` Miller indices for each site. + origin : np.ndarray + ``(row, col)`` chosen lattice origin. + g1 : np.ndarray + First lattice vector as a ``(row, col)`` offset from ``origin``. + g2 : np.ndarray + Second lattice vector as a ``(row, col)`` offset from ``origin``. + cmap : str, default="gray" + Colormap for the Bragg vector map; gray keeps the colored overlays legible. + norm : str or dict, default="log_auto" + Intensity scaling forwarded to :func:`show_2d` (e.g. ``"linear_auto"``, + ``"log_auto"``, ``"power_sqrt"``, or ``{"power": 0.5}``). + zoom : bool, default=True + If ``True``, frame the view to the reference bounding box (plus a margin) + so the labelled sites are large enough to read. + figsize : tuple of float, default=(6, 6) + Figure size in inches. + **show_kwargs + Extra keyword arguments forwarded to :func:`show_2d` for fine display + control (e.g. ``vmin``, ``vmax``, ``lower_quantile``, ``upper_quantile``). + + Returns + ------- + tuple + ``(fig, ax)``. + """ + import matplotlib.patheffects as path_effects + from matplotlib.cm import ScalarMappable + from matplotlib.colors import Normalize + + from quantem.core.visualization import show_2d + + stroke = [path_effects.withStroke(linewidth=2.5, foreground="black")] + origin_color = (0.0, 0.7, 0.0) + g1_color = (1.0, 0.0, 0.0) + g2_color = (0.0, 0.7, 1.0) + + ref_qpos = np.asarray(ref_qpos, dtype=float).reshape(-1, 2) + ref_ab = np.asarray(ref_ab, dtype=int).reshape(-1, 2) + o = np.asarray(origin, dtype=float).reshape(2) + g1 = np.asarray(g1, dtype=float).reshape(2) + g2 = np.asarray(g2, dtype=float).reshape(2) + + fig, ax = plt.subplots(figsize=figsize) + show_2d(np.asarray(bvm), figax=(fig, ax), cmap=cmap, norm=norm, **show_kwargs) + + # offset of each picked candidate from its ideal lattice site origin + a*g1 + b*g2; + # this is the QC indicator -- a mis-picked or strongly strained candidate rings + # far from zero, while a clean pick rings near it. + ideal_qpos = o[None, :] + ref_ab[:, 0:1] * g1[None, :] + ref_ab[:, 1:2] * g2[None, :] + offset = np.linalg.norm(ref_qpos - ideal_qpos, axis=1) + + # color the rings by that offset, scaled to half the shorter lattice spacing (the + # default fit_lattice match radius) so the colorbar previews which candidates sit + # near the inclusion tolerance. + radius = 0.5 * float(min(np.hypot(*g1), np.hypot(*g2))) + cmap_offset = plt.get_cmap("plasma") + norm_offset = Normalize(vmin=0.0, vmax=radius if radius > 0 else 1.0) + + ax.scatter( + ref_qpos[:, 1], + ref_qpos[:, 0], + s=80, + facecolors="none", + edgecolors=cmap_offset(norm_offset(offset)), + linewidths=1.8, + zorder=3, + ) + for (r, c), (a, b) in zip(ref_qpos, ref_ab): + ax.annotate( + f"{int(a)},{int(b)}", + (c, r), + color="white", + fontsize=9, + fontweight="bold", + xytext=(4, 4), + textcoords="offset points", + path_effects=stroke, + zorder=4, + ) + + ax.scatter( + o[1], + o[0], + s=160, + marker="P", + facecolors=[origin_color], + edgecolors="white", + linewidths=1.5, + zorder=6, + ) + for g, label, color in ((g1, "g1", g1_color), (g2, "g2", g2_color)): + ax.annotate( + "", + xy=(o[1] + g[1], o[0] + g[0]), + xytext=(o[1], o[0]), + arrowprops=dict(arrowstyle="-|>", color=color, lw=2.4, shrinkA=0, shrinkB=0), + zorder=5, + ) + gnorm = float(np.hypot(g[0], g[1])) + 1e-12 + ax.annotate( + label, + (o[1] + g[1] / 2.0, o[0] + g[0] / 2.0), + color=color, + fontsize=13, + fontweight="bold", + xytext=(g[0] / gnorm * 15.0, g[1] / gnorm * 15.0), + textcoords="offset points", + ha="center", + va="center", + path_effects=stroke, + zorder=7, + ) + + if zoom and ref_qpos.shape[0]: + rmin, cmin = ref_qpos.min(axis=0) + rmax, cmax = ref_qpos.max(axis=0) + margin = 0.12 * max(rmax - rmin, cmax - cmin, 1.0) + 6.0 + h, w = bvm.shape[:2] + ax.set_xlim(max(cmin - margin, -0.5), min(cmax + margin, w - 0.5)) + ax.set_ylim(min(rmax + margin, h - 0.5), max(rmin - margin, -0.5)) + + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_title("reference lattice (ring color = offset from ideal index; +origin, g1, g2)") + + sm = ScalarMappable(norm=norm_offset, cmap=cmap_offset) + sm.set_array([]) + cbar = fig.colorbar(sm, ax=ax, fraction=0.046, pad=0.04, extend="max") + cbar.set_label("peak offset from ideal index (px)") + + fig.tight_layout() + return fig, ax + + +def plot_lattice_fit( + mask_weight: np.ndarray, + fit_error: np.ndarray, + *, + figsize: tuple[float, float] = (11, 4.5), +): + """Per-position diagnostics from :meth:`BraggVectors.fit_lattice`. + + Left: the mask weight — a lattice *order parameter* per position (how well all + detected intensity snaps to the fitted lattice, intensity-weighted) — so ``0`` is + a position dominated by off-lattice intensity and ``1`` a clean single crystal. + This is the weighting handed to the strain reference. Right: the RMS lattice-fit + residual over the matched peaks, in pixels (low = a clean fit; high = a poorly + fit, overlapping, or strongly strained position). + + Parameters + ---------- + mask_weight : np.ndarray + ``(scan_row, scan_col)`` lattice-order-parameter weight in ``[0, 1]``. + fit_error : np.ndarray + ``(scan_row, scan_col)`` RMS fit residual in pixels (``nan`` where no fit + was made). + figsize : tuple of float, default=(11, 4.5) + Figure size in inches. + + Returns + ------- + tuple + ``(fig, ax)`` with ``ax`` a length-2 array of axes. + """ + fig, ax = plt.subplots(1, 2, figsize=figsize) + + im0 = ax[0].imshow(np.asarray(mask_weight), cmap="viridis", vmin=0.0, vmax=1.0) + ax[0].set_title("mask weight (lattice order)") + fig.colorbar(im0, ax=ax[0], fraction=0.046, pad=0.04) + + im1 = ax[1].imshow(np.asarray(fit_error), cmap="magma") + ax[1].set_title("fit RMS error (px)") + fig.colorbar(im1, ax=ax[1], fraction=0.046, pad=0.04) + + for a in ax: + a.set_xticks([]) + a.set_yticks([]) + fig.tight_layout() + return fig, ax \ No newline at end of file diff --git a/src/quantem/diffraction/calibration.py b/src/quantem/diffraction/calibration.py new file mode 100644 index 000000000..e54060f60 --- /dev/null +++ b/src/quantem/diffraction/calibration.py @@ -0,0 +1,912 @@ +"""Diffraction-space calibration against known crystal structures. + +Functions here operate on detected Bragg peaks (a quantem Vector) and refine +the reciprocal-space pixel size by comparing the radial peak histogram with +the ring positions of a reference crystal. +""" + +from __future__ import annotations + +import numpy as np +import torch + +from quantem.core.datastructures.vector import Vector +from quantem.diffraction.crystal import Crystal + + +def _measure_raw_origins(bragg_vectors, search_radius: float) -> np.ndarray: + """Brightest-peak origin per position, NaN where nothing is found.""" + peaks = bragg_vectors.peaks + scan_r, scan_c = peaks.shape[0], peaks.shape[1] + H, W = int(bragg_vectors.dataset.shape[-2]), int(bragg_vectors.dataset.shape[-1]) + c0 = np.array([H / 2, W / 2]) + meas = np.full((scan_r, scan_c, 2), np.nan) + for r in range(scan_r): + for c in range(scan_c): + arr = peaks[r, c].array + if arr.shape[0] == 0: + continue + d = np.hypot(arr[:, 0] - c0[0], arr[:, 1] - c0[1]) + near = d < search_radius + if not near.any(): + continue + sub = arr[near] + meas[r, c] = sub[np.argmax(sub[:, 2]), :2] + return meas + + +def plot_origin_fit(bragg_vectors, origins: np.ndarray, search_radius: float = 6.0): + """Diagnostic for measure_origins: measured vs fit vs residual, both axes.""" + import matplotlib.pyplot as plt + + meas = _measure_raw_origins(bragg_vectors, search_radius) + fig, axs = plt.subplots(2, 3, figsize=(13.5, 5.6)) + names = ["row", "col"] + for k in range(2): + m, f = meas[..., k], origins[..., k] + resid = m - f + center = np.nanmean(m) + span = max(np.nanstd(m) * 3, 1e-3) + for j, (img, title) in enumerate( + [ + (m, f"measured origin {names[k]} (px)"), + (f, f"plane fit {names[k]} (px)"), + (resid, f"residual {names[k]} (px)"), + ] + ): + c0 = 0.0 if j == 2 else center + sp = max(np.nanstd(resid) * 3, 1e-3) if j == 2 else span + im = axs[k, j].imshow( + img, cmap="RdBu_r", vmin=c0 - sp, vmax=c0 + sp, + interpolation="nearest", + ) + axs[k, j].set_title(title, fontsize=10) + axs[k, j].set_xticks([]) + axs[k, j].set_yticks([]) + fig.colorbar(im, ax=axs[k, j], shrink=0.85) + fig.tight_layout() + return fig, axs + + +def measure_origins( + bragg_vectors, + search_radius: float = 6.0, + robust: bool = True, + plot: bool = False, +): + """Per-position diffraction origin from the brightest central peak. + + At each scan position the most intense detected peak within + `search_radius` pixels of the detector center is taken as the direct + beam; a plane is fit over the scan (least squares, optionally with one + outlier-rejection pass) to model the descan. + + Parameters + ---------- + plot : bool, default=False + Show the fitted origin planes and the residuals of the measured + origins against the fit. + + Returns + ------- + np.ndarray + (scan_row, scan_col, 2) plane-fit origins, ready for + BraggVectors.correct_peak_origins(). With plot=True, also returns + (fig, axs). + """ + meas = _measure_raw_origins(bragg_vectors, search_radius) + scan_r, scan_c = meas.shape[0], meas.shape[1] + ry, rx = np.mgrid[0:scan_r, 0:scan_c] + + def plane(z, ok): + A = np.stack([np.ones(ok.sum()), ry[ok], rx[ok]], axis=1) + coef, *_ = np.linalg.lstsq(A, z[ok], rcond=None) + return coef[0] + coef[1] * ry + coef[2] * rx + + out = np.zeros((scan_r, scan_c, 2)) + for k in range(2): + z = meas[..., k] + ok = np.isfinite(z) + fit = plane(z, ok) + if robust: + resid = np.abs(z - fit) + thresh = 3 * np.nanmedian(resid[ok]) + 1e-9 + ok = ok & (resid < thresh) + fit = plane(z, ok) + out[..., k] = fit + + if plot: + import matplotlib.pyplot as plt + + fig, axs = plt.subplots(2, 3, figsize=(13.5, 5.6)) + names = ["row", "col"] + for k in range(2): + m, f = meas[..., k], out[..., k] + resid = m - f + center = np.nanmean(m) + span = max(np.nanstd(m) * 3, 1e-3) + for j, (img, title) in enumerate( + [ + (m, f"measured origin {names[k]} (px)"), + (f, f"plane fit {names[k]} (px)"), + (resid, f"residual {names[k]} (px)"), + ] + ): + c0 = 0.0 if j == 2 else center + s = max(np.nanstd(resid) * 3, 1e-3) if j == 2 else span + im = axs[k, j].imshow( + img, cmap="RdBu_r", vmin=c0 - s, vmax=c0 + s, + interpolation="nearest", + ) + axs[k, j].set_title(title, fontsize=10) + axs[k, j].set_xticks([]) + axs[k, j].set_yticks([]) + fig.colorbar(im, ax=axs[k, j], shrink=0.85) + fig.tight_layout() + return out, fig, axs + return out + + +def peaks_to_calibrated( + peaks_px, + pixel_size_inv_A: float, + rotation_ccw_deg: float = 0.0, + ellipse=None, + name: str = "bragg_peaks_calibrated", +): + """Convert origin-corrected pixel peaks to a calibrated (qx, qy) Vector. + + Parameters + ---------- + peaks_px : Vector + Peaks with fields (q_row, q_col, intensity) in detector pixels, + already origin-corrected so (0, 0) is the direct beam. + pixel_size_inv_A : float + Reciprocal pixel size in 1/Angstroms. + rotation_ccw_deg : float, default=0.0 + Diffraction-to-scan rotation: the detector coordinates are rotated + by this angle so the pattern axes align with the scan axes. With + this applied, in-plane orientations and strain axes are reported in + the image frame. + ellipse : array-like | None + [e11, e12] elliptic distortion correction from calibrate_ellipse(), + applied in the detector frame before the rotation. + + Returns + ------- + Vector + Fields (qx, qy, intensity) in 1/Angstroms. + """ + scan_r, scan_c = peaks_px.shape[0], peaks_px.shape[1] + flat = peaks_px.select_fields("q_row", "q_col", "intensity").flatten() + row_counts = np.asarray(peaks_px.row_counts(), dtype=int) + qrc = flat[:, :2] * pixel_size_inv_A + if ellipse is not None: + e11, e12 = float(ellipse[0]), float(ellipse[1]) + A = np.array([[1 + e11, e12], [e12, 1 - e11]]) + qrc = qrc @ A.T + if rotation_ccw_deg != 0.0: + th = np.deg2rad(rotation_ccw_deg) + rot = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]]) + qrc = qrc @ rot.T + data = np.column_stack([qrc, flat[:, 2]]) + cells = np.split(data, np.cumsum(row_counts)[:-1]) + nested = [cells[r * scan_c : (r + 1) * scan_c] for r in range(scan_r)] + out = Vector.from_data( + nested, + fields=["qx", "qy", "intensity"], + units=["A^-1", "A^-1", "counts"], + name=name, + ) + # record the detector-to-scan rotation so pattern-overlay plots can put + # peaks back into the raw detector frame + out.metadata["rotation_ccw_deg"] = float(rotation_ccw_deg) + return out + + +def scale_peaks(peaks, scale: float): + """Return a copy of a (qx, qy, intensity) Vector with q scaled.""" + out = peaks.copy() + flat = out.flatten() + flat[:, :2] *= scale + out.set_flattened(flat) + return out + + +def radial_histogram( + peaks: Vector, + k_min: float = 0.05, + k_max: float = 1.5, + k_step: float = 0.002, + bragg_k_power: float = 2.0, + bragg_intensity_power: float = 1.0, +) -> tuple[np.ndarray, np.ndarray]: + """Intensity-weighted histogram of Bragg peak radii over all positions. + + Returns + ------- + k : np.ndarray + Bin centers (1/Angstroms). + hist : np.ndarray + Weighted counts, with linear interpolation between adjacent bins. + """ + flat = peaks.select_fields("qx", "qy", "intensity").flatten() + qr = np.hypot(flat[:, 0], flat[:, 1]) + weight = flat[:, 2] ** bragg_intensity_power * qr**bragg_k_power + + k = np.arange(k_min, k_max, k_step) + frac = (qr - k_min) / k_step + i0 = np.floor(frac).astype(int) + w1 = frac - i0 + ok = (i0 >= 0) & (i0 < k.size - 1) + hist = np.bincount(i0[ok], weights=weight[ok] * (1 - w1[ok]), minlength=k.size) + hist += np.bincount(i0[ok] + 1, weights=weight[ok] * w1[ok], minlength=k.size) + return k, hist + + +def simulated_ring_profile( + crystal: Crystal, + k: np.ndarray, + k_broadening: float = 0.01, + bragg_k_power: float = 2.0, +) -> np.ndarray: + """1D ring profile of a crystal: Gaussians at |g| weighted by intensity.""" + g = crystal.g_len.numpy() + w = crystal.struct_factors_int.numpy() * g**bragg_k_power + prof = ( + w[None, :] * np.exp(-((k[:, None] - g[None, :]) ** 2) / (2 * k_broadening**2)) + ).sum(axis=1) + return prof + + +def calibrate_pixel_size_matching( + peaks, + crystal: Crystal | list[Crystal], + energy_ev: float, + scales: np.ndarray | None = None, + subsample: int = 8, + angle_step_deg: float = 3.0, + corr_kernel_size: float = 0.02, + min_number_peaks: int = 6, + plot: bool = False, + return_scores: bool = False, + returnfig: bool = False, +): + """Refine the pixel size by maximizing the orientation-match correlation. + + The 1D radial fit can be fooled by ring-ratio degeneracies (e.g. the + hexagonal-net radii shared by hcp prismatic rings and bcc {110}-family + rings). Full-pattern matching is not: for each candidate scale, a + subsampled grid of patterns is orientation-matched against the crystal + and the median normalized correlation scored. Peaks in the score curve + identify the true calibration. + + Parameters + ---------- + peaks : Vector + Calibrated peaks (qx, qy, intensity) in 1/Angstroms. + crystal : Crystal | list[Crystal] + Reference crystal(s). Pass ALL candidate phases for multi-phase + samples: with a single reference, a scale that maps the majority + phase's net onto the reference's (e.g. the bcc {110} ring onto the + hcp prismatic ring, ratio 0.90 for Ti) can win the scan. Scoring the + mean over phases of the per-phase median correlation removes the + false optimum, since the other phases index nothing at the impostor + scale. + energy_ev : float + Beam energy in eV. + scales : np.ndarray | None + Candidate scale factors; defaults to 0.90 ... 1.10 in 2% steps. + subsample : int, default=8 + Stride of the probe-position grid used for scoring. + + Returns + ------- + scale : float + Best scale factor (parabolic refinement over the score maximum). + With return_scores=True, also (scales, scores); with returnfig=True, + also (fig, ax). + """ + from quantem.diffraction.orientation import OrientationMap + + crystals = crystal if isinstance(crystal, (list, tuple)) else [crystal] + if scales is None: + scales = np.arange(0.90, 1.101, 0.02) + sub = peaks[::subsample, ::subsample] + scores = np.zeros(len(scales)) + for i, s in enumerate(scales): + test = scale_peaks(sub, float(s)) + per_phase = [] + for xtl in crystals: + om = OrientationMap.from_vectors(test, xtl, energy_ev=energy_ev) + # detector_q_max must stay OFF here: the auto footprint shrinks + # with the candidate scale, silently removing the unexplained + # high-q template shells that penalize too-small scales -- the + # score then rises monotonically as the pattern is compressed. + # a tight kernel is essential for a wide scale scan: the default + # matching kernel (0.05) hands partial credit to near-miss ring + # coincidences of an impostor scale, while matches at the true + # scale are exact to the detection noise (~0.005) + om.build_plan( + angle_step_zone_axis_deg=angle_step_deg, + angle_step_in_plane_deg=angle_step_deg, + corr_kernel_size=corr_kernel_size, + detector_q_max=None, + verbose=False, + ) + om.match_orientations(progress_bar=False, min_number_peaks=min_number_peaks) + corr = om.corr[..., 0] + per_phase.append(float(corr[corr > 0].median())) + scores[i] = float(np.mean(per_phase)) + + i_best = int(np.argmax(scores)) + scale = float(scales[i_best]) + if 0 < i_best < len(scales) - 1: + c0, c1, c2 = scores[i_best - 1 : i_best + 2] + denom = 4 * c1 - 2 * c0 - 2 * c2 + step = scales[1] - scales[0] + if abs(denom) > 1e-12: + scale += (c2 - c0) / denom * step + out: list = [scale] + if return_scores: + out += [scales, scores] + if plot: + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot(scales, scores, "k.-") + ax.axvline(scale, color="r", ls="--") + ax.set_xlabel("pixel size scale factor") + ax.set_ylabel("median correlation") + ax.set_title(f"best scale = {scale:.4f}") + if returnfig: + out += [fig, ax] + return tuple(out) if len(out) > 1 else out[0] + + +def calibrate_pixel_size( + peaks: Vector, + crystal: Crystal, + scale_range: tuple[float, float] = (0.8, 1.25), + scale_step: float = 5e-4, + k_min: float = 0.05, + k_max: float = 1.3, + k_broadening: float = 0.01, + bragg_k_power: float = 2.0, + plot: bool = False, + returnfig: bool = False, +): + """Refine the reciprocal pixel size against a reference crystal. + + Scans a multiplicative scale factor applied to the measured peak radii and + maximizes the normalized overlap between the measured radial histogram and + the crystal's simulated ring profile. Parabolic sub-step refinement of the + best scale. + + Parameters + ---------- + peaks : Vector + Calibrated peaks with fields (qx, qy, intensity) in 1/Angstroms. + crystal : Crystal + Reference crystal with structure factors calculated. Choose the + majority phase of the scan. + scale_range : tuple, default=(0.8, 1.25) + Search range of the scale factor. + plot : bool, default=False + Show the measured histogram against the crystal ring positions, + before and after applying the scale. + + Returns + ------- + scale : float + Multiply existing q values (and the pixel size) by this factor, + e.g. with scale_peaks(). With returnfig=True, also (fig, axs). + """ + k, hist = radial_histogram(peaks, k_min=k_min * scale_range[0], k_max=k_max / scale_range[0]) + scales = np.arange(scale_range[0], scale_range[1], scale_step) + score = np.zeros_like(scales) + for i, s in enumerate(scales): + prof = simulated_ring_profile(crystal, k * s, k_broadening, bragg_k_power) + keep = (k * s > k_min) & (k * s < k_max) + h, p = hist[keep], prof[keep] + denom = np.linalg.norm(h) * np.linalg.norm(p) + score[i] = (h * p).sum() / denom if denom > 0 else 0.0 + + i_best = int(np.argmax(score)) + scale = float(scales[i_best]) + if 0 < i_best < scales.size - 1: + c0, c1, c2 = score[i_best - 1 : i_best + 2] + denom = 4 * c1 - 2 * c0 - 2 * c2 + if abs(denom) > 1e-12: + scale += (c2 - c0) / denom * scale_step + + out: list = [scale] + if plot: + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(10, 4)) + prof = simulated_ring_profile( + crystal, k * scale, k_broadening / 2, bragg_k_power + ) + ax.fill_between( + k * scale, hist / hist.max(), color="r", alpha=0.75, lw=0, + label="measured (scaled)", + ) + ax.plot( + k * scale, prof / prof.max(), "k-", lw=1.0, + label=f"{crystal.name} rings", + ) + ax.set_ylabel("intensity (norm.)") + ax.set_xlabel("scattering vector (1/$\\mathrm{\\AA}$)") + ax.set_title(f"1D radial fit, scale = {scale:.4f}", fontsize=10) + ax.legend(loc="upper right", fontsize=9) + if returnfig: + out += [fig, ax] + return tuple(out) if len(out) > 1 else out[0] + + +def measure_scan_rotation( + dataset, + origins: np.ndarray | None = None, + mask_radius: float | None = None, + plot: bool = False, + returnfig: bool = False, +): + """Detector-to-scan rotation from the curl of the center-of-mass field. + + The center of mass of each diffraction pattern (about the fitted origin) + forms a vector field over the scan. In the correct common frame that + field is (approximately) a gradient field, so its curl vanishes; rotating + the detector axes by the unknown scan rotation and minimizing the summed + squared curl recovers the angle. + + The curl is invariant under 180-degree rotation, so the sign of the + measured field cannot distinguish theta from theta + 180. Both candidates + are returned; pick the one consistent with a known feature (e.g. a + Burgers orientation relationship, or the divergence sign convention of + DPC). The returned angle is ready to pass to peaks_to_calibrated() as + rotation_ccw_deg. + + Parameters + ---------- + dataset : Dataset4dstem + The 4D-STEM scan. + origins : np.ndarray | None + (scan_r, scan_c, 2) diffraction origins from measure_origins(); + defaults to the pattern center. + mask_radius : float | None + Restrict the center of mass to within this radius (pixels) of the + origin -- i.e. the DPC signal of the direct beam only, excluding the + Bragg disks. Recommended for crystalline data. + plot : bool, default=False + Plot the curl and divergence measures against the rotation angle. + + Returns + ------- + rotation_ccw_deg : float + Curl-minimizing rotation in [0, 180); the physical answer is either + this angle or this angle + 180. With returnfig=True, also (fig, ax). + """ + arr = np.asarray(dataset.array, dtype=float) + scan_r, scan_c, H, W = arr.shape + rows = np.arange(H)[:, None] + cols = np.arange(W)[None, :] + if origins is None: + origins = np.zeros((scan_r, scan_c, 2)) + origins[..., 0] = H / 2 + origins[..., 1] = W / 2 + if mask_radius is not None: + rr = rows[None, None] - origins[..., 0][..., None, None] + cc = cols[None, None] - origins[..., 1][..., None, None] + arr = arr * (rr**2 + cc**2 <= mask_radius**2) + tot = arr.sum(axis=(-2, -1)) + tot[tot <= 0] = 1.0 + com_r = (arr * rows).sum(axis=(-2, -1)) / tot - origins[..., 0] + com_c = (arr * cols).sum(axis=(-2, -1)) / tot - origins[..., 1] + + # spatial derivatives of both components over the scan + d_rr = np.gradient(com_r, axis=0) + d_rc = np.gradient(com_r, axis=1) + d_cr = np.gradient(com_c, axis=0) + d_cc = np.gradient(com_c, axis=1) + + theta = np.deg2rad(np.arange(0, 180, 0.25)) + ct, st = np.cos(theta)[:, None, None], np.sin(theta)[:, None, None] + # rotated field: (r', c') = (ct * r - st * c, st * r + ct * c) + curl = (st * d_rr + ct * d_cr) - (ct * d_rc - st * d_cc) + div = (ct * d_rr - st * d_cr) + (st * d_rc + ct * d_cc) + curl_sq = (curl**2).mean(axis=(1, 2)) + div_sq = (div**2).mean(axis=(1, 2)) + + i_best = int(np.argmin(curl_sq)) + rotation_ccw_deg = float(np.rad2deg(theta[i_best])) + + out: list = [rotation_ccw_deg] + if plot: + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(7, 4)) + deg = np.rad2deg(theta) + ax.plot(deg, curl_sq, "k-", label="mean squared curl") + ax.plot(deg, div_sq, "-", color="0.6", label="mean squared divergence") + ax.axvline(rotation_ccw_deg, color="r", ls="--") + ax.set_xlabel("rotation (degrees)") + ax.set_ylabel("field measure") + ax.set_title( + "scan rotation = %.1f deg (or %.1f)" + % (rotation_ccw_deg, rotation_ccw_deg + 180) + ) + ax.legend() + if returnfig: + out += [fig, ax] + return tuple(out) if len(out) > 1 else out[0] + + +def calibrate_ellipse( + peaks, + k_min: float = 0.15, + k_max: float = 1.4, + n_bins: int = 800, + bragg_k_power: float = 2.0, + bragg_intensity_power: float = 1.0, + plot: bool = False, + returnfig: bool = False, +): + """Elliptic distortion (e11, e12) from radial histogram sharpness. + + Fits the traceless linear distortion A = [[1 + e11, e12], [e12, 1 - e11]] + that, applied to the measured peaks, maximizes the sharpness of the + radial peak histogram: an elliptic distortion smears every diffraction + ring, and undoing it re-focuses them. The histogram is accumulated in + log-radius bins, where a pure scale change is only a translation -- so + the ellipse fit is independent of the pixel size, and the calibration + workflow stays sequential: rough scale, then ellipse, then absolute + scale by pattern matching. + + Parameters + ---------- + peaks : Vector + Calibrated peaks (qx, qy, intensity), approximate scale is fine. + k_min, k_max : float + Radial range (1/Angstroms) included in the sharpness measure. + plot : bool, default=False + Show the radial histogram before and after the correction. + + Returns + ------- + ellipse : np.ndarray + [e11, e12]; pass to peaks_to_calibrated(ellipse=...) or + apply_ellipse(). With returnfig=True, also (fig, ax). + """ + from scipy.optimize import minimize + + flat = peaks.select_fields("qx", "qy", "intensity").flatten() + q = flat[:, :2] + log_lo, log_hi = np.log(k_min), np.log(k_max) + bin_w = (log_hi - log_lo) / n_bins + + def histogram(e): + A = np.array([[1 + e[0], e[1]], [e[1], 1 - e[0]]]) + qe = q @ A.T + r = np.hypot(qe[:, 0], qe[:, 1]) + ok = (r > k_min) & (r < k_max) + w = flat[ok, 2] ** bragg_intensity_power * r[ok] ** bragg_k_power + f = (np.log(r[ok]) - log_lo) / bin_w + i0 = np.floor(f).astype(int) + w1 = f - i0 + h = np.bincount(i0, weights=w * (1 - w1), minlength=n_bins + 1) + h += np.bincount(i0 + 1, weights=w * w1, minlength=n_bins + 1) + return h + + def cost(e): + h = histogram(e) + total = h.sum() + if total <= 0: + return 0.0 + return -float((h**2).sum()) / total**2 + + res = minimize( + cost, + x0=np.zeros(2), + method="Nelder-Mead", + options={"xatol": 1e-5, "fatol": 1e-12, "maxiter": 400}, + ) + ellipse = res.x + + out: list = [ellipse] + if plot: + import matplotlib.pyplot as plt + + k_bins = np.exp(np.linspace(log_lo, log_hi, n_bins + 1)) + h0 = histogram(np.zeros(2)) + h1 = histogram(ellipse) + fig, ax = plt.subplots(figsize=(10, 4)) + ax.fill_between( + k_bins, h0 / h0.max(), color="0.7", lw=0, label="measured" + ) + ax.plot(k_bins, h1 / h1.max(), "r-", lw=1.0, label="ellipse corrected") + ax.set_xlabel("scattering vector (1/$\\mathrm{\\AA}$)") + ax.set_ylabel("intensity (norm.)") + mag = np.hypot(*ellipse) + ax.set_title( + "e11 = %.2e, e12 = %.2e (%.2f%% ellipticity)" + % (ellipse[0], ellipse[1], 200 * mag) + ) + ax.legend() + if returnfig: + out += [fig, ax] + return tuple(out) if len(out) > 1 else out[0] + + +def apply_ellipse(peaks, ellipse): + """Return a copy of (qx, qy, intensity) peaks with the ellipse applied.""" + out = peaks.copy() + flat = out.flatten() + e11, e12 = float(ellipse[0]), float(ellipse[1]) + A = np.array([[1 + e11, e12], [e12, 1 - e11]]) + flat[:, :2] = flat[:, :2] @ A.T + out.set_flattened(flat) + return out + + +def _hkl_label(hkl: np.ndarray, hexagonal: bool) -> str: + """Compact (hkl) / (hkil) plane label with unicode overbars.""" + + def digit(v: int) -> str: + v = int(round(v)) + txt = str(abs(v)) + return txt + "̅" if v < 0 else txt + + h, k, l = (int(round(v)) for v in hkl) + if hexagonal: + return "(" + digit(h) + digit(k) + digit(-(h + k)) + digit(l) + ")" + return "(" + digit(h) + digit(k) + digit(l) + ")" + + +def plot_ring_comparison( + peaks, + crystals, + k_min: float = 0.1, + k_max: float = 1.5, + k_broadening: float | None = None, + bragg_k_power: float = 2.0, + label_hkl: bool = True, + label_min_intensity: float = 0.05, + figax=None, +): + """Measured radial peak histogram against crystal ring positions. + + One panel per crystal: the measured histogram is the red fill, the + crystal's rings are black -- sharp vertical lines by default, or a + Gaussian profile of width `k_broadening` when set (use after + calibration, where the rings should sit inside the measured peaks). The + strongest rings are labeled by (hkl), 4-index (hkil) for hexagonal + crystals. + + Parameters + ---------- + peaks : Vector + Calibrated peaks (qx, qy, intensity) in 1/Angstroms. + crystals : Crystal | list[Crystal] + Reference crystal(s) with structure factors calculated. + k_broadening : float | None + None draws sharp lines at the ring positions; a value (1/Angstroms) + draws the broadened ring profile instead. + label_min_intensity : float, default=0.05 + Label rings whose summed intensity exceeds this fraction of the + strongest ring. + """ + import matplotlib.pyplot as plt + + xtls = crystals if isinstance(crystals, (list, tuple)) else [crystals] + k, hist = radial_histogram(peaks, k_min=k_min, k_max=k_max) + + n = len(xtls) + if figax is None: + fig, axs = plt.subplots(n, 1, figsize=(11, 3.6 * n), sharex=True, squeeze=False) + axs = axs[:, 0] + else: + fig, axs = figax + axs = np.atleast_1d(axs) + + for ci, (ax, xtl) in enumerate(zip(axs, xtls)): + ax.fill_between( + k, hist / hist.max(), color="r", alpha=0.75, lw=0, label="measured" + ) + hexagonal = xtl.laue_group in ("6/m", "6/mmm", "-3", "-3m") + g_len = xtl.g_len.numpy() + ints = xtl.struct_factors_int.numpy() * g_len**bragg_k_power + hkl_np = xtl.hkl.numpy() + shells = np.round(g_len / 0.01) * 0.01 + uniq = np.unique(shells) + shell_int = np.array([ints[shells == u].sum() for u in uniq]) + shell_int = shell_int / shell_int.max() + + if k_broadening is not None: + prof = simulated_ring_profile(xtl, k, k_broadening, bragg_k_power) + ax.plot( + k, prof / prof.max(), "k-", lw=1.0, label=f"{xtl.name} rings" + ) + else: + keep = (uniq > k_min) & (uniq < k_max) + ax.vlines( + uniq[keep], 0, shell_int[keep], colors="k", lw=1.0, + label=f"{xtl.name} rings", + ) + + if label_hkl: + rows = 0 + labeled_g: list[float] = [] + for u, si in zip(uniq, shell_int): + if u < k_min or u > k_max or si < label_min_intensity: + continue + if any(abs(u - g0) < 0.03 for g0 in labeled_g): + continue + in_shell = shells == u + idx = np.nonzero(in_shell)[0] + idx = idx[ints[idx] > 0.99 * ints[idx].max()] + key = [tuple(-hkl_np[i]) for i in idx] + best = idx[int(np.lexsort(np.array(key).T[::-1])[0])] + labeled_g.append(u) + y = 1.05 + 0.11 * (rows % 2) + rows += 1 + ax.text( + u, y, _hkl_label(hkl_np[best], hexagonal), + fontsize=8, ha="center", va="bottom", + ) + ax.set_ylabel("intensity (norm.)") + ax.set_ylim(0, 1.32) + ax.legend(loc="upper right", fontsize=9) + axs[-1].set_xlabel("scattering vector (1/$\\mathrm{\\AA}$)") + fig.tight_layout() + return fig, axs + + +def transform_peaks(peaks, M: np.ndarray): + """Return a copy of a (qx, qy, intensity) Vector with q mapped by M (2x2).""" + out = peaks.copy() + flat = out.flatten() + flat[:, :2] = flat[:, :2] @ np.asarray(M, dtype=float).T + out.set_flattened(flat) + return out + + +def refine_calibration( + strain_maps, + masks=None, + max_strain: float = 0.05, +): + """Global calibration residual from matched orientations. + + The per-position deformation A fitted by + OrientationMap.calculate_strain() maps ideal simulated peaks onto the + measured ones, so it contains both the local strain and any global + calibration error. The element-wise median of A over many differently + oriented grains (across all phases) averages the strain away and leaves + the calibration residual: scale and ellipticity. A global detector + rotation is NOT observable this way -- the in-plane refinement absorbs + it into every orientation, so the reported rotation_deg is ~0 by + construction; measure the scan rotation independently + (measure_scan_rotation, or a known texture). Apply the returned + correction with transform_peaks() and re-match to close the loop. + + Parameters + ---------- + strain_maps : list[StrainMap] + One per phase, from calculate_strain() on the SAME calibrated peaks. + masks : list[np.ndarray] | None + Per-phase inclusion masks (e.g. phase == i and reliable); defaults + to all positions where the strain fit succeeded. + max_strain : float, default=0.05 + Discard positions whose deformation differs from the identity by + more than this (failed fits, overlaps). + + Returns + ------- + dict with: + 'M' : the median deformation (2, 2), + 'correction' : inv(M), ready for transform_peaks(), + 'scale' : multiply the pixel size by this, + 'rotation_deg' : residual detector rotation, + 'ellipse' : (e11, e12) traceless ellipticity components, + 'num_positions' : positions used. + """ + As = [] + for i, sm in enumerate(strain_maps): + A = np.stack([sm.u_array, sm.v_array], axis=-1) # (R, C, 2, 2) + ok = np.isfinite(A).all(axis=(-2, -1)) + dev = np.abs(A - np.eye(2)).max(axis=(-2, -1)) + ok &= dev < max_strain + if masks is not None and masks[i] is not None: + ok &= np.asarray(masks[i]) > 0 + As.append(A[ok]) + A_all = np.concatenate(As, axis=0) + M = np.median(A_all, axis=0) + + scale = float(np.sqrt(np.abs(np.linalg.det(M)))) + theta = 0.5 * (M[1, 0] - M[0, 1]) / scale + sym = 0.5 * (M + M.T) / scale + e11 = float(0.5 * (sym[0, 0] - sym[1, 1])) + e12 = float(sym[0, 1]) + return { + "M": M, + "correction": np.linalg.inv(M), + "scale": scale, + "rotation_deg": float(np.rad2deg(theta)), + "ellipse": (e11, e12), + "num_positions": int(A_all.shape[0]), + } + + +def plot_bragg_rings( + peaks, + crystals, + n_rings: int = 8, + q_max: float | None = None, + bins: int = 400, + power: float = 0.25, + figax=None, +): + """2D histogram of all Bragg peaks with crystal rings overlaid. + + The Bragg vector map (histogram of every detected peak over the scan) + shows the calibration directly in 2D: the crystal's strongest rings are + drawn as thin circles, which should thread through the measured spot + density -- a radius mismatch is a pixel size error, and a direction- + dependent mismatch is elliptic distortion. + + Parameters + ---------- + peaks : Vector + Calibrated peaks (qx, qy, intensity) in 1/Angstroms. + crystals : Crystal | list[Crystal] + Reference crystal(s); the n_rings strongest rings of each are drawn + (solid, then dashed line styles). + n_rings : int, default=8 + Number of rings per crystal, strongest first. + """ + import matplotlib.pyplot as plt + + xtls = crystals if isinstance(crystals, (list, tuple)) else [crystals] + flat = peaks.select_fields("qx", "qy", "intensity").flatten() + if q_max is None: + q_max = float(np.hypot(flat[:, 0], flat[:, 1]).max()) * 1.02 + H, xe, ye = np.histogram2d( + flat[:, 0], + flat[:, 1], + bins=bins, + range=[[-q_max, q_max], [-q_max, q_max]], + ) + + if figax is None: + fig, ax = plt.subplots(figsize=(7.5, 7.5)) + else: + fig, ax = figax + ax.imshow( + H**power, + cmap="gray_r", + extent=(ye[0], ye[-1], xe[-1], xe[0]), + interpolation="nearest", + ) + styles = ["-", "--", ":"] + colors = ["r", "b", "g"] + th = np.linspace(0, 2 * np.pi, 361) + for ci, xtl in enumerate(xtls): + g_len = xtl.g_len.numpy() + ints = xtl.struct_factors_int.numpy() * g_len**2 + shells = np.round(g_len / 0.01) * 0.01 + uniq = np.unique(shells) + shell_int = np.array([ints[shells == u].sum() for u in uniq]) + keep = uniq < q_max + uniq, shell_int = uniq[keep], shell_int[keep] + order = np.argsort(shell_int)[::-1][:n_rings] + for k, u in enumerate(np.sort(uniq[order])): + ax.plot( + u * np.sin(th), u * np.cos(th), + ls=styles[ci % 3], color=colors[ci % 3], lw=0.5, alpha=0.6, + label=f"{xtl.name} rings" if k == 0 else None, + ) + ax.set_xlabel("$q_c$ (1/$\\mathrm{\\AA}$)") + ax.set_ylabel("$q_r$ (1/$\\mathrm{\\AA}$)") + ax.legend(loc="upper right", fontsize=9) + return fig, ax diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py new file mode 100644 index 000000000..9f2fbba6d --- /dev/null +++ b/src/quantem/diffraction/crystal.py @@ -0,0 +1,510 @@ +"""Crystal structures and kinematical diffraction for orientation mapping. + +A Crystal wraps an ase.Atoms object and computes the reciprocal lattice, +kinematical structure factors, symmetry operators (via spglib), and simulated +diffraction patterns for arbitrary orientations. All numerical state is stored +as torch tensors (float64) so downstream matching and refinement can run on +GPU and differentiate through the calculation. + +Conventions +----------- +- Real lattice vectors are rows of `lat_real` (Angstroms). +- Reciprocal lattice vectors are rows of `lat_recip` (1/Angstroms, no 2*pi). +- Structure factors follow F_hkl = (1/V) * sum_n f_n * exp(-2*pi*i * hkl.p_n), + so intensities have units of scattering amplitude per unit volume. +- Orientations are unit quaternions rotating crystal Cartesian vectors into + the lab frame (see quantem.diffraction.rotations). +""" + +from __future__ import annotations + +import json +from importlib import resources +from pathlib import Path + +import numpy as np +import torch +from ase import Atoms +from ase.data import chemical_symbols + +from quantem.core.utils.utils import electron_wavelength_angstrom +from quantem.diffraction.rotations import qrotate, symmetry_quaternions + +# Zone-axis fundamental wedge corners (Cartesian) for each Laue class, with +# display labels for the IPF legend. Hexagonal / trigonal labels use 4-index +# Miller-Bravais direction symbols. Any Laue class not listed falls back to +# hemisphere sampling, which is always sufficient (all Laue classes contain +# inversion) but redundant. +_SQRT3_2 = np.sqrt(3) / 2 +LAUE_WEDGES: dict[str, list[list[float]]] = { + "m-3m": [[0, 0, 1], [0, 1, 1], [1, 1, 1]], + "m-3": [[0, 0, 1], [1, 0, 0], [1, 1, 1]], + "6/mmm": [[0, 0, 1], [_SQRT3_2, 0.5, 0], [1, 0, 0]], + "6/m": [[0, 0, 1], [1, 0, 0], [0.5, _SQRT3_2, 0]], + "-3m": [[0, 0, 1], [1, 0, 0], [0.5, _SQRT3_2, 0]], + "4/mmm": [[0, 0, 1], [1, 0, 0], [1, 1, 0]], + "4/m": [[0, 0, 1], [1, 0, 0], [0, 1, 0]], + "mmm": [[0, 0, 1], [1, 0, 0], [0, 1, 0]], +} +LAUE_WEDGE_LABELS: dict[str, list[str]] = { + "m-3m": ["[001]", "[011]", "[111]"], + "m-3": ["[001]", "[100]", "[111]"], + "6/mmm": ["[0001]", "[10$\\bar{1}$0]", "[2$\\bar{1}\\bar{1}$0]"], + "6/m": ["[0001]", "[2$\\bar{1}\\bar{1}$0]", "[11$\\bar{2}$0]"], + "-3m": ["[0001]", "[2$\\bar{1}\\bar{1}$0]", "[11$\\bar{2}$0]"], + "4/mmm": ["[001]", "[100]", "[110]"], + "4/m": ["[001]", "[100]", "[010]"], + "mmm": ["[001]", "[100]", "[010]"], +} +# plain-text (unicode combining-overline) forms for terminal printing +_B = "\u0305" # combining overline, applies to the preceding character +LAUE_WEDGE_LABELS_TEXT: dict[str, list[str]] = { + "m-3m": ["[001]", "[011]", "[111]"], + "m-3": ["[001]", "[100]", "[111]"], + "6/mmm": ["[0001]", f"[101{_B}0]", f"[21{_B}1{_B}0]"], + "6/m": ["[0001]", f"[21{_B}1{_B}0]", f"[112{_B}0]"], + "-3m": ["[0001]", f"[21{_B}1{_B}0]", f"[112{_B}0]"], + "4/mmm": ["[001]", "[100]", "[110]"], + "4/m": ["[001]", "[100]", "[010]"], + "mmm": ["[001]", "[100]", "[010]"], +} + + +def miller_to_miller_bravais(uvw: np.ndarray) -> np.ndarray: + """Convert 3-index [u'v'w'] direction indices to 4-index [u v t w]. + + u = (2u' - v') / 3, v = (2v' - u') / 3, t = -(u + v), w = w', cleared to + the smallest integer form. + """ + uvw = np.atleast_2d(np.asarray(uvw, dtype=float)) + u = (2 * uvw[:, 0] - uvw[:, 1]) / 3 + v = (2 * uvw[:, 1] - uvw[:, 0]) / 3 + out = np.stack([u, v, -(u + v), uvw[:, 2]], axis=1) + # clear fractions and common factors + out = out * 3 + gcd = np.gcd.reduce(np.abs(np.round(out)).astype(int), axis=1) + gcd[gcd == 0] = 1 + out = out / gcd[:, None] + return out.astype(int).squeeze() + + +def miller_bravais_to_miller(uvtw: np.ndarray) -> np.ndarray: + """Convert 4-index [u v t w] direction indices to 3-index [u'v'w']. + + u' = 2u + v, v' = 2v + u, w' = w (t is redundant: t = -(u + v)). + """ + uvtw = np.atleast_2d(np.asarray(uvtw, dtype=float)) + out = np.stack( + [2 * uvtw[:, 0] + uvtw[:, 1], 2 * uvtw[:, 1] + uvtw[:, 0], uvtw[:, 3]], axis=1 + ) + gcd = np.gcd.reduce(np.abs(np.round(out)).astype(int), axis=1) + gcd[gcd == 0] = 1 + return (out / gcd[:, None]).astype(int).squeeze() + +# point group -> Laue class +_LAUE_CLASS = { + "1": "-1", "-1": "-1", + "2": "2/m", "m": "2/m", "2/m": "2/m", + "222": "mmm", "mm2": "mmm", "mmm": "mmm", + "4": "4/m", "-4": "4/m", "4/m": "4/m", + "422": "4/mmm", "4mm": "4/mmm", "-42m": "4/mmm", "4/mmm": "4/mmm", + "3": "-3", "-3": "-3", + "32": "-3m", "3m": "-3m", "-3m": "-3m", + "6": "6/m", "-6": "6/m", "6/m": "6/m", + "622": "6/mmm", "6mm": "6/mmm", "-6m2": "6/mmm", "6/mmm": "6/mmm", + "23": "m-3", "m-3": "m-3", + "432": "m-3m", "-43m": "m-3m", "m-3m": "m-3m", +} + + +def _load_lobato_params() -> dict[str, np.ndarray]: + with resources.files("quantem.diffraction").joinpath("data/lobato.json").open() as f: + raw = json.load(f) + return {sym: np.array(p) for sym, p in raw.items()} + + +_LOBATO: dict[str, np.ndarray] | None = None + + +def electron_scattering_factor(numbers: torch.Tensor, g: torch.Tensor) -> torch.Tensor: + """Lobato & Van Dyck (2014) electron scattering factors. + + Parameters + ---------- + numbers : torch.Tensor + Atomic numbers (N,). + g : torch.Tensor + Scattering vector magnitudes (M,) in 1/Angstroms. + + Returns + ------- + torch.Tensor + f_e(g) of shape (N, M) in Angstroms. + """ + global _LOBATO + if _LOBATO is None: + _LOBATO = _load_lobato_params() + g2 = (g**2)[None, :, None] # (1, M, 5) + a = torch.stack( + [ + torch.as_tensor(_LOBATO[chemical_symbols[int(z)]][0], dtype=g.dtype, device=g.device) + for z in numbers + ] + )[:, None, :] # (N, 1, 5) + b = torch.stack( + [ + torch.as_tensor(_LOBATO[chemical_symbols[int(z)]][1], dtype=g.dtype, device=g.device) + for z in numbers + ] + )[:, None, :] + return (a * (2.0 + b * g2) / (1.0 + b * g2) ** 2).sum(dim=-1) + + +class Crystal: + """A crystal structure with kinematical diffraction methods. + + Build with `from_ase` or `from_cif`, then call + `calculate_structure_factors` before generating patterns or orientation + plans. + """ + + def __init__( + self, + atoms: Atoms, + name: str | None = None, + symprec: float = 1e-4, + pseudo_symmetry_tol: float | None = None, + verbose: bool = True, + ): + self.atoms = atoms + self.name = name if name is not None else atoms.get_chemical_formula() + self._pseudo_symmetry_tol = pseudo_symmetry_tol + + self.lat_real = torch.as_tensor(atoms.cell[:], dtype=torch.float64) + self.positions_frac = torch.as_tensor( + atoms.get_scaled_positions(), dtype=torch.float64 + ) + self.numbers = torch.as_tensor(atoms.numbers, dtype=torch.long) + occupancy = atoms.arrays.get("occupancy", np.ones(len(atoms))) + self.occupancy = torch.as_tensor(np.asarray(occupancy, dtype=float)) + + self._setup_symmetry(symprec, pseudo_symmetry_tol) + if verbose: + print(self.symmetry_summary()) + + # populated by calculate_structure_factors + self.k_max: float | None = None + self.hkl: torch.Tensor | None = None + self.g_vec: torch.Tensor | None = None + self.g_len: torch.Tensor | None = None + self.struct_factors: torch.Tensor | None = None + self.struct_factors_int: torch.Tensor | None = None + + @classmethod + def from_ase(cls, atoms: Atoms, name: str | None = None, **kwargs) -> "Crystal": + return cls(atoms, name=name, **kwargs) + + @classmethod + def from_cif(cls, file_path: str | Path, name: str | None = None, **kwargs) -> "Crystal": + from ase.io import read + + atoms = read(file_path) + assert isinstance(atoms, Atoms) + return cls(atoms, name=name, **kwargs) + + @property + def volume(self) -> float: + return float(torch.abs(torch.linalg.det(self.lat_real))) + + @property + def lat_recip(self) -> torch.Tensor: + """Reciprocal lattice vectors as rows, no 2*pi factor.""" + return torch.linalg.inv(self.lat_real).T + + def _setup_symmetry(self, symprec: float, pseudo_symmetry_tol: float | None) -> None: + """Detect the true symmetry group, and optionally a pseudo-symmetry group. + + The true group (at `symprec`) is stored for reporting and refinement. + When `pseudo_symmetry_tol` is set, the symmetry is re-detected at that + looser tolerance: nearly-degenerate cells (e.g. an orthorhombic cell + with a = 4.000, b = 4.001, c = 4.002 Angstroms) are idealized to their + higher-symmetry parent, and *matching* uses that group --- orientations + that no experiment could distinguish are never sampled separately. + """ + import spglib + + cell = ( + self.lat_real.numpy(), + self.positions_frac.numpy(), + self.numbers.numpy(), + ) + dataset = spglib.get_symmetry_dataset(cell, symprec=symprec) + self.spacegroup: str = f"{dataset.international} ({dataset.number})" + pg = spglib.get_pointgroup(dataset.rotations)[0].strip() + self.pointgroup: str = pg + self.laue_group: str = _LAUE_CLASS.get(pg, "-1") + self.sym_quats = symmetry_quaternions(dataset.rotations, self.lat_real.numpy()) + + if pseudo_symmetry_tol is not None and pseudo_symmetry_tol > symprec: + ds_pseudo = spglib.get_symmetry_dataset(cell, symprec=pseudo_symmetry_tol) + pg_pseudo = spglib.get_pointgroup(ds_pseudo.rotations)[0].strip() + self.pointgroup_matching: str = pg_pseudo + self.laue_group_matching: str = _LAUE_CLASS.get(pg_pseudo, "-1") + self.sym_quats_matching = symmetry_quaternions( + ds_pseudo.rotations, self.lat_real.numpy() + ) + else: + self.pointgroup_matching = pg + self.laue_group_matching = self.laue_group + self.sym_quats_matching = self.sym_quats + + def zone_axis_wedge(self) -> torch.Tensor | None: + """Fundamental zone-axis wedge corners (3, 3) Cartesian, or None. + + None means the Laue class has no simple 3-corner wedge and the + orientation plan should sample the full hemisphere. + """ + corners = LAUE_WEDGES.get(self.laue_group) + if corners is None: + return None + c = torch.tensor(corners, dtype=torch.float64) + return c / torch.linalg.norm(c, dim=-1, keepdim=True) + + def zone_axis_wedge_labels(self) -> list[str] | None: + """Direction labels of the wedge corners (4-index for hex/trigonal).""" + return LAUE_WEDGE_LABELS.get(self.laue_group) + + def symmetry_summary(self) -> str: + """Human-readable symmetry report, including any pseudo-symmetry.""" + import re + + # subscript the space group screw/glide digits: P6_3/mmc -> P6[sub3]/mmc + subs = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉") + sg = re.sub(r"_(\d)", lambda m: m.group(1).translate(subs), self.spacegroup) + lines = [ + f"{self.name}", + f" space group {sg}", + f" point group {self.pointgroup} (Laue class {self.laue_group})", + ] + if self.pointgroup_matching != self.pointgroup: + lines += [ + f" pseudo-symmetry {self.pointgroup_matching} " + f"(Laue class {self.laue_group_matching}) " + "-- used for orientation matching", + ] + elif self._pseudo_symmetry_tol is not None: + lines += [ + " pseudo-symmetry none found at tol = " + f"{self._pseudo_symmetry_tol:g} A", + ] + else: + lines += [" pseudo-symmetry not checked (set pseudo_symmetry_tol)"] + # matching line reflects the symmetry actually used, after any + # pseudo-symmetry reduction + labels = LAUE_WEDGE_LABELS_TEXT.get(self.laue_group_matching) + wedge_txt = ( + f"zone axis wedge {labels[0]}, {labels[1]}, {labels[2]}" + if labels is not None + else "full hemisphere" + ) + lines += [ + f" matching {self.sym_quats_matching.shape[0]} proper " + f"rotations, {wedge_txt}" + ] + return "\n".join(lines) + + def calculate_structure_factors( + self, + k_max: float = 1.5, + tol_structure_factor: float = 1e-4, + thermal_sigma: float | dict[str, float] | None = None, + ) -> "Crystal": + """Kinematical structure factors for all reflections with |g| <= k_max. + + Parameters + ---------- + k_max : float, default=1.5 + Maximum scattering vector magnitude, 1/Angstroms. + tol_structure_factor : float, default=1e-4 + Discard reflections with |F| below this threshold. + thermal_sigma : float | dict[str, float] | None + RMS thermal displacement (Angstroms), scalar or per-element, + applied as a Debye-Waller factor. + + Returns + ------- + Crystal + self, for chaining. + """ + self.k_max = float(k_max) + recip = self.lat_recip + + # index range: project k_max onto each reciprocal cell direction + k_len = torch.linalg.norm(recip, dim=1) + n_max = torch.ceil(k_max / k_len * 2).to(torch.long) + ranges = [torch.arange(-int(n), int(n) + 1) for n in n_max] + hkl = torch.cartesian_prod(*ranges).to(torch.float64) + g_vec = hkl @ recip + g_len = torch.linalg.norm(g_vec, dim=1) + keep = (g_len <= k_max) & (g_len > 0) + hkl, g_vec, g_len = hkl[keep], g_vec[keep], g_len[keep] + + f_e = electron_scattering_factor(self.numbers, g_len) # (N_atoms, N_g) + + if thermal_sigma is not None: + if isinstance(thermal_sigma, dict): + sigma = torch.tensor( + [thermal_sigma[chemical_symbols[int(z)]] for z in self.numbers], + dtype=torch.float64, + ) + else: + sigma = torch.full((len(self.numbers),), float(thermal_sigma)) + dwf = torch.exp(-0.5 * (2 * np.pi * sigma[:, None] * g_len[None, :]) ** 2) + f_e = f_e * dwf + + phase = torch.exp(-2j * np.pi * (self.positions_frac @ hkl.T)) # (N_atoms, N_g) + F = (f_e * self.occupancy[:, None] * phase).sum(dim=0) / self.volume + + keep = torch.abs(F) > tol_structure_factor + self.hkl = hkl[keep].to(torch.long) + self.g_vec = g_vec[keep] + self.g_len = g_len[keep] + self.struct_factors = F[keep] + self.struct_factors_int = torch.abs(F[keep]) ** 2 + return self + + def calculate_dynamical_structure_factors( + self, + energy_ev: float, + thermal_sigma: float | dict[str, float] = 0.05, + k_max: float | None = None, + include_core: bool = True, + include_phonon: bool = True, + ) -> "Crystal": + """Absorptive structure factors for Bloch wave calculations. + + Uses the Weickenmeier-Kohl parameterization (Acta Cryst. A47, 590 + (1991)): the elastic part is Debye-Waller damped, and the imaginary + (absorptive) part includes core-loss and phonon/TDS contributions. + The returned factors are relativistically corrected and already carry + the 1/pi convention of the Bloch structure matrix, i.e. they are the + U_g of De Graef ch. 5 after division by the unit cell volume. + + All reflections up to k_max are kept, including kinematically + forbidden ones (their U_g can be nonzero through absorption and they + are required as coupling vectors g - h). + + Parameters + ---------- + energy_ev : float + Beam energy in eV. + thermal_sigma : float | dict[str, float], default=0.05 + RMS thermal displacement (Angstroms), scalar or per-element. + k_max : float | None + Maximum |g| of stored factors; defaults to the kinematical k_max. + For Bloch calculations with beams out to k, this should be 2k so + every coupling vector is covered. + """ + from quantem.diffraction.wk_scattering_factors import compute_WK_factor + + if k_max is None: + if self.k_max is None: + raise RuntimeError("Provide k_max or run calculate_structure_factors.") + k_max = self.k_max + recip = self.lat_recip + k_len = torch.linalg.norm(recip, dim=1) + n_max = torch.ceil(k_max / k_len * 2).to(torch.long) + ranges = [torch.arange(-int(n), int(n) + 1) for n in n_max] + hkl = torch.cartesian_prod(*ranges).to(torch.float64) + g_vec = hkl @ recip + g_len = torch.linalg.norm(g_vec, dim=1) + keep = g_len <= k_max + hkl, g_len = hkl[keep], g_len[keep] + + g_np = g_len.numpy() + if isinstance(thermal_sigma, dict): + sigma_per_atom = np.array( + [thermal_sigma[chemical_symbols[int(z)]] for z in self.numbers] + ) + else: + sigma_per_atom = np.full(len(self.numbers), float(thermal_sigma)) + + # one WK evaluation per unique (Z, sigma) pair + f_atoms = np.zeros((len(self.numbers), g_np.size), dtype=np.complex128) + cache: dict[tuple[int, float], np.ndarray] = {} + for i, (z, sig) in enumerate(zip(self.numbers.tolist(), sigma_per_atom)): + key = (int(z), float(sig)) + if key not in cache: + cache[key] = compute_WK_factor( + g_np, + int(z), + energy_ev, + thermal_sigma=float(sig), + include_core=include_core, + include_phonon=include_phonon, + ) + f_atoms[i] = cache[key] + + phase = np.exp(-2j * np.pi * (self.positions_frac.numpy() @ hkl.numpy().T)) + occ = self.occupancy.numpy()[:, None] + U = (f_atoms * occ * phase).sum(axis=0) / self.volume + + self.hkl_dyn = hkl.to(torch.long) + self.g_len_dyn = g_len + self.U_dyn = torch.as_tensor(U, dtype=torch.complex128) + self.dyn_energy_ev = float(energy_ev) + return self + + def generate_pattern( + self, + orientation: torch.Tensor, + energy_ev: float = 300e3, + sigma_excitation: float = 0.02, + tol_excitation_mult: float = 3.0, + k_max: float | None = None, + ) -> dict[str, torch.Tensor]: + """Kinematical diffraction pattern for one orientation. + + Parameters + ---------- + orientation : torch.Tensor + Unit quaternion (4,) rotating crystal vectors into the lab frame. + energy_ev : float, default=300e3 + Beam energy in eV. + sigma_excitation : float, default=0.02 + Excitation error tolerance (1/Angstroms) in the shape-factor + envelope exp(-s_g^2 / 2 sigma^2). + tol_excitation_mult : float, default=3.0 + Include reflections with |s_g| below this multiple of sigma. + k_max : float | None + Optionally trim the pattern below the structure-factor k_max. + + Returns + ------- + dict with 'qx', 'qy', 'intensity', 'hkl', 's_g' tensors. + """ + if self.g_vec is None: + raise RuntimeError("Run calculate_structure_factors first.") + lam = electron_wavelength_angstrom(energy_ev) + g = qrotate(orientation, self.g_vec) + gz, g2 = g[:, 2], (g**2).sum(dim=1) + s_g = (2 * gz - lam * g2) / (2 - 2 * lam * gz) + keep = torch.abs(s_g) < sigma_excitation * tol_excitation_mult + if k_max is not None: + keep &= self.g_len <= k_max + intensity = self.struct_factors_int[keep] * torch.exp( + -(s_g[keep] ** 2) / (2 * sigma_excitation**2) + ) + return { + "qx": g[keep, 0], + "qy": g[keep, 1], + "intensity": intensity, + "hkl": self.hkl[keep], + "s_g": s_g[keep], + } + + def __repr__(self) -> str: + return ( + f"Crystal({self.name}, {len(self.numbers)} atoms, " + f"spacegroup {self.spacegroup}, pointgroup {self.pointgroup})" + ) diff --git a/src/quantem/diffraction/data/lobato.json b/src/quantem/diffraction/data/lobato.json new file mode 100644 index 000000000..41487366d --- /dev/null +++ b/src/quantem/diffraction/data/lobato.json @@ -0,0 +1,1650 @@ +{ + "H": [ + [ + 0.00647384848835291, + -0.490192576780229, + 0.573284160390876, + -0.37940330148399, + 0.554426474774079 + ], + [ + 2.78519885379148, + 2.77620428330644, + 2.77538591050625, + 2.76759302867258, + 2.76511897642927 + ] + ], + "He": [ + [ + 3.05745116099835, + -62.0044779127325, + 64.0055537084614, + -5.0013257854278, + 0.151798828700526 + ], + [ + 1.08967248726078, + 0.939838798143121, + 0.925289034386265, + 0.82294749870865, + 0.577393110675402 + ] + ], + "Li": [ + [ + 3.92622272886147, + -4.54861962639998, + 2.19335312878658, + 0.0699451265033965, + 0.00209864224851937 + ], + [ + 8.1427601351728, + 4.98941077007855, + 4.1442899923941, + 0.40192231506568, + 0.156479034719823 + ] + ], + "Be": [ + [ + 3.39824970557054, + -1.90866886095696, + 0.0390702117539227, + -0.0111631010210714, + 0.00946204465357523 + ], + [ + 4.44270178622409, + 3.32451542526423, + 0.189772880348214, + 0.0871918614644603, + 0.082780906004134 + ] + ], + "B": [ + [ + 1.47279248639329, + -0.401933042199387, + 0.305998956982689, + 0.0196144217173168, + 0.0009771771060882 + ], + [ + 3.74974048281819, + 0.588066536139673, + 0.515639613103011, + 0.121377570080603, + 0.0680982412160313 + ] + ], + "C": [ + [ + 124.466088621343, + -220.352857078963, + 195.235352280479, + -98.1079361269799, + 0.0142023041213623 + ], + [ + 2.42120849256005, + 2.30537943752425, + 2.04851932106564, + 1.93352552917547, + 0.0768976818478339 + ] + ], + "N": [ + [ + 58.1327150702556, + -147.542409087812, + 130.143065649639, + -39.6195674084154, + 0.010595776333148 + ], + [ + 1.70044856413471, + 1.5590385260174, + 1.41576827473146, + 1.27841818205455, + 0.0565587798474805 + ] + ], + "O": [ + [ + 29.9474045242362, + -77.6101266255278, + 99.8817764623144, + -51.2127005505673, + 0.00819618954446032 + ], + [ + 1.3028398788001, + 1.15794105258309, + 1.00988549338025, + 0.943327971433266, + 0.0433197611321825 + ] + ], + "F": [ + [ + 0.948984894503524, + -30.1333923043554, + 52.7965078127338, + -22.7062703795272, + 0.00656997664531441 + ], + [ + 1.45882933198645, + 0.68877999318768, + 0.654239869346695, + 0.614836130811994, + 0.0342837419495011 + ] + ], + "Ne": [ + [ + 0.582741192220907, + 0.370676561841054, + -0.546744967350809, + 0.414052682480208, + 0.00519903080863993 + ], + [ + 1.28118573143877, + 0.444520897170477, + 0.198650875510481, + 0.185477246656276, + 0.0275738382033885 + ] + ], + "Na": [ + [ + 23.6700603946792, + -21.8531786159742, + 0.592499448108946, + -0.0244652290310244, + 0.00483950221706535 + ], + [ + 8.45148773514603, + 8.04096600474298, + 0.624996000526315, + 0.132450394947296, + 0.0233994362049878 + ] + ], + "Mg": [ + [ + 4.85501047687149, + -2.66220906476843, + 0.478001236085108, + -0.0702307064692064, + 0.00398905828104019 + ], + [ + 5.94639273842456, + 4.17130312520697, + 0.398269808150374, + 0.161886185837474, + 0.0195345056363103 + ] + ], + "Al": [ + [ + 2.83409561607507, + -4.28004133378261, + 4.42191680548311, + -0.03457744718964, + 0.00352385941406079 + ], + [ + 6.66235023980533, + 0.551294722224021, + 0.509328963445973, + 0.111784837425331, + 0.0167602351805257 + ] + ], + "Si": [ + [ + 2.87189142611612, + -2.06173501195173, + 2.17114024204478, + -0.0663073633058801, + 0.00301070709670513 + ], + [ + 5.08487103642989, + 0.429178185305126, + 0.366485434192162, + 0.119710611296903, + 0.0143994536128397 + ] + ], + "P": [ + [ + 2.79151840023151, + -4.36506837823822, + 4.43558455516699, + -0.0809635773399473, + 0.0026790001796644 + ], + [ + 3.90065961865466, + 0.32982596837715, + 0.306089956505888, + 0.108083232545972, + 0.0125894495331186 + ] + ], + "S": [ + [ + 2.67971415610199, + -0.474252822230755, + 0.514835948989687, + -0.0958360024990722, + 0.00248871963818935 + ], + [ + 3.06889121199971, + 0.378216702185809, + 0.188721811902548, + 0.0923370590031494, + 0.0111920877241144 + ] + ], + "Cl": [ + [ + 2.5662483998002, + -0.338876350828591, + 1.14584558755515, + -0.923109316547079, + 0.00229168002041042 + ], + [ + 2.41594920365612, + 0.421414239310216, + 0.10959240497583, + 0.0990955458226753, + 0.00999665948927521 + ] + ], + "Ar": [ + [ + 2.45981746414068, + -0.364198177076995, + 0.250584477222474, + -0.0577437029544345, + 0.00230143866828583 + ], + [ + 1.94004631988856, + 0.399241067884398, + 0.117472406274412, + 0.0567803726023621, + 0.00915579832920708 + ] + ], + "K": [ + [ + 5.81107878601454, + -50.2537096539422, + 48.8609412059842, + 0.0740628592048382, + 0.00072780273862622 + ], + [ + 12.6691483399036, + 3.95641039698166, + 3.68385059577154, + 0.107458517569562, + 0.00665576789391501 + ] + ], + "Ca": [ + [ + 21.1781161524159, + -339.043824317468, + 322.756958523296, + 0.0650077673896996, + 0.00065587436657859 + ], + [ + 6.39608619431736, + 3.74024713891749, + 3.64888449922605, + 0.0945090634514673, + 0.00598520619883758 + ] + ], + "Sc": [ + [ + 12.6035186572148, + -276.875382053702, + 268.871603907342, + 0.0556824178897458, + 0.0005770712551454 + ], + [ + 6.15625615363852, + 3.08873554266679, + 3.02727663298548, + 0.0818874748375215, + 0.00538289832050797 + ] + ], + "Ti": [ + [ + 8.57595775238129, + -210.331563465304, + 206.097172601541, + 0.0477773948977261, + 0.00050571648448032 + ], + [ + 6.00780668875581, + 2.60285856745213, + 2.55352345051105, + 0.0711429484024153, + 0.00485628439383726 + ] + ], + "V": [ + [ + 6.52768433234789, + -200.430576829172, + 198.015053889994, + 0.0413911518064005, + 0.00044745502432986 + ], + [ + 5.83552479352448, + 2.23255952381127, + 2.19786018594229, + 0.0623973876828547, + 0.00440383649079958 + ] + ], + "Cr": [ + [ + 3.02831784843691, + -95.5393933081432, + 96.1761562352198, + 0.0359777315957987, + 0.00039149289071842 + ], + [ + 8.35911504314631, + 1.80263790264103, + 1.77509488957047, + 0.0548144412143113, + 0.00399828968916034 + ] + ], + "Mn": [ + [ + 4.37417550633122, + -160.925510918779, + 160.273308060322, + 0.0312303861049162, + 0.00034696602102094 + ], + [ + 5.51031705504928, + 1.68798216402433, + 1.66614047777702, + 0.0483370390321277, + 0.00364746960053068 + ] + ], + "Fe": [ + [ + 3.79810090836859, + -91.6893549381687, + 91.4454252155429, + 0.0272754344027604, + 0.00030337985438377 + ], + [ + 5.31712645899433, + 1.49713094884748, + 1.4680924180441, + 0.0427247850128954, + 0.00332791855231874 + ] + ], + "Co": [ + [ + 3.33037874467544, + -77.0017596472967, + 77.0725221790516, + 0.0239904669040569, + 0.00026825666552394 + ], + [ + 5.18135964579543, + 1.32915122265139, + 1.30284928963228, + 0.0380645486826371, + 0.0030501000799157 + ] + ], + "Ni": [ + [ + 2.96908078725286, + -75.747706912904, + 76.0398287625307, + 0.0210162113692967, + 0.00023115175113515 + ], + [ + 5.0418094910938, + 1.18275507921629, + 1.16216545846629, + 0.0337479087883035, + 0.0027868086207074 + ] + ], + "Cu": [ + [ + 1.75207145212145, + -43.0410523492124, + 44.0705915543573, + 0.0186876154088144, + 0.00020172732479162 + ], + [ + 6.18750497986187, + 1.00266263628976, + 0.98538431135303, + 0.0302984703916117, + 0.00255855598748879 + ] + ], + "Zn": [ + [ + 2.46637110499459, + -61.4678541332537, + 62.0176945237481, + 0.0164160173931416, + 0.00017248711759538 + ], + [ + 4.91028078493815, + 0.967898520322992, + 0.951283834775355, + 0.0269600967667566, + 0.00234109611046253 + ] + ], + "Ga": [ + [ + 2.76010203108428, + -34.4452614207467, + 35.2262267244016, + 0.0132067196999419, + 0.00012594556092825 + ], + [ + 6.10128224537662, + 0.765143313553464, + 0.751328623382859, + 0.0224879634317251, + 0.00206737374278712 + ] + ], + "Ge": [ + [ + 3.1824163526, + -52.4514037811166, + 52.9690827162218, + 0.0114096168591864, + 9.509543581451e-05 + ], + [ + 5.01719040860914, + 0.712395764437798, + 0.702280192528194, + 0.0196747295694015, + 0.00184146614494011 + ] + ], + "As": [ + [ + 3.45642969119604, + -33.3176044431721, + 33.5712193855332, + 0.00979002295640342, + 6.534348622652e-05 + ], + [ + 4.01358016032945, + 0.662355778050629, + 0.645771941056073, + 0.0170919353231011, + 0.0016030160283942 + ] + ], + "Se": [ + [ + 3.64905047801926, + -43.6851662221238, + 43.6920288601154, + 0.00844902284199144, + 3.7861147411e-05 + ], + [ + 3.25043267112593, + 0.609666201650289, + 0.596971300802123, + 0.0148554512740362, + 0.00133625587356563 + ] + ], + "Br": [ + [ + 3.83846312224289, + -52.2723471011233, + 51.9861279495659, + 0.00733955989338044, + 1.646942079513e-05 + ], + [ + 2.61189470573232, + 0.566195062874718, + 0.555279326699873, + 0.012974646943244, + 0.00102986536855875 + ] + ], + "Kr": [ + [ + 4.02541030310827, + -46.3042332078929, + 45.7213681904101, + 0.00635337959625315, + 1.33477845256e-06 + ], + [ + 2.1364838144374, + 0.526591166454131, + 0.514136784464577, + 0.0112807242242006, + 0.00048808985794097 + ] + ], + "Rb": [ + [ + 3.38975351595394, + 2.14348348679172, + 0.354322603510978, + 0.00374009340085664, + 3.0034250234e-07 + ], + [ + 20.5744814368171, + 1.91079945218516, + 0.197410589396603, + 0.00813459465343988, + 0.00029268579105695 + ] + ], + "Sr": [ + [ + 4.77092509299826, + 1.47597850155283, + 0.304451355544188, + 0.00359474981916728, + 3.0008554923e-07 + ], + [ + 13.3668881330412, + 1.33738379577196, + 0.177532369437782, + 0.00779105033001826, + 0.00028225513964854 + ] + ], + "Y": [ + [ + 4.60721019875234, + 1.4280185103984, + 0.295581045577753, + 0.00338997840852895, + 2.6686297497e-07 + ], + [ + 10.8686905557151, + 1.31137455873124, + 0.168022870784711, + 0.00735964545350478, + 0.00026234328102139 + ] + ], + "Zr": [ + [ + 4.31175453406871, + 1.49331578039339, + 0.281236050128884, + 0.00309337738455747, + 2.5802444851e-07 + ], + [ + 9.4589658051749, + 1.33063622845245, + 0.156506871444453, + 0.00682410523811735, + 0.0002498188791107 + ] + ], + "Nb": [ + [ + 3.11179039113495, + 2.20259060945803, + 0.27033077491002, + 0.00268799194751098, + 2.3254948335e-07 + ], + [ + 10.6903141343023, + 1.65316356158933, + 0.145115185718969, + 0.0061395635158285, + 0.00023224023593767 + ] + ], + "Mo": [ + [ + 2.83105968432053, + 2.34858137489674, + 0.245105888429696, + 0.00235282108218515, + 2.3127083834e-07 + ], + [ + 10.435719575895, + 1.60482868674597, + 0.131696934774606, + 0.00554977901383345, + 0.0002227474637649 + ] + ], + "Tc": [ + [ + 2.57179859323352, + 2.45633741957203, + 0.220658440881371, + 0.00205531418012438, + 2.3213294779e-07 + ], + [ + 10.1643117131377, + 1.5344191924455, + 0.11913861975411, + 0.00501853240882988, + 0.00021459037912086 + ] + ], + "Ru": [ + [ + 2.33230030353946, + 2.53578025489049, + 0.198208042136026, + 0.00176120130460053, + 1.9812941155e-07 + ], + [ + 9.92167460159959, + 1.45585668808788, + 0.107682187873349, + 0.00447243545654967, + 0.0001965747162325 + ] + ], + "Rh": [ + [ + 2.1135253485947, + 2.58636316155013, + 0.177063913863686, + 0.00149737051003056, + 2.0548144556e-07 + ], + [ + 9.65913725859762, + 1.37106656934329, + 0.0970353027584648, + 0.00397128509088792, + 0.0001913551907378 + ] + ], + "Pd": [ + [ + 0.642159796182687, + 2.97914814426328, + 0.16815442600427, + 0.00133744213878407, + 1.9141096825e-07 + ], + [ + 5.97479750263406, + 1.43359432541277, + 0.0909868401172677, + 0.00362410137116162, + 0.00018068891441605 + ] + ], + "Ag": [ + [ + 1.55317216680389, + 2.63930364699988, + 0.142015486956788, + 0.0010085046009976, + 1.9463842997e-07 + ], + [ + 8.15620235758956, + 1.21600887481801, + 0.0790098864915873, + 0.00296547901363949, + 0.00017459309591915 + ] + ], + "Cd": [ + [ + 61.530785199286, + -78.6016741201582, + 21.550129260277, + 0.137685015664191, + 0.00032464493094652 + ], + [ + 3.11468102533247, + 2.76016983388574, + 1.93551312324722, + 0.0722468347260293, + 0.00117001629624684 + ] + ], + "In": [ + [ + 4.22232177901524, + -26.4121318353202, + 27.2852852708746, + 0.121617901884138, + 0.00030688354637153 + ], + [ + 6.07265510403227, + 1.64550178959387, + 1.52257074939506, + 0.0656507914695225, + 0.00112093851473035 + ] + ], + "Sn": [ + [ + 5.14222074642053, + -25.4945413762576, + 25.7414487548601, + 0.111778227592199, + 0.00029364738473774 + ], + [ + 5.27272636474484, + 1.53194959209148, + 1.40257525040087, + 0.0611685387263086, + 0.00107560815394522 + ] + ], + "Sb": [ + [ + 6.24164031831883, + -93.3868724419552, + 92.6332875829884, + 0.103412692221298, + 0.00028184842689439 + ], + [ + 4.26984108082687, + 1.3940774614025, + 1.35566854910434, + 0.0572667949652199, + 0.00103307954282067 + ] + ], + "Te": [ + [ + 7.37743301813403, + -126.025106931628, + 124.128405004095, + 0.0959978371818569, + 0.00027107221639888 + ], + [ + 3.46917757783897, + 1.29759810886736, + 1.26771067646066, + 0.0537771750179423, + 0.00099308457702918 + ] + ], + "I": [ + [ + 9.64400666272123, + -122.924435350112, + 118.682564816567, + 0.0895025947025539, + 0.00026127612149048 + ], + [ + 2.72645545366544, + 1.23723425850866, + 1.20062036872249, + 0.0506678682285199, + 0.0009554378833835 + ] + ], + "Xe": [ + [ + 15.545174967486, + -118.241027856744, + 108.009524963197, + 0.0836259341992221, + 0.00025199186242907 + ], + [ + 2.10637340865492, + 1.20860376129512, + 1.15395270567214, + 0.0478189391274824, + 0.00091988625759261 + ] + ], + "Cs": [ + [ + 4.28708739181692, + 3.23250665422965, + 0.674029533561706, + 0.0618908083387697, + 0.00023561205295219 + ], + [ + 22.6587870798541, + 2.23797386470064, + 0.368995568664316, + 0.0402266575298484, + 0.00088376189087804 + ] + ], + "Ba": [ + [ + 6.24475187390461, + 2.35172271416388, + 0.474279373222252, + 0.0638113874286849, + 0.00023465128056279 + ], + [ + 15.1431354190985, + 1.45379000604814, + 0.320835646387801, + 0.0404354532094699, + 0.00085408113128003 + ] + ], + "La": [ + [ + 6.09788179599509, + 2.19495164736675, + 0.548172791960022, + 0.0616669573222662, + 0.00022680735586471 + ], + [ + 12.4288544315854, + 1.50535992352023, + 0.333738039717124, + 0.0387744553499998, + 0.00082404988406487 + ] + ], + "Ce": [ + [ + 5.7952687964724, + 2.37022664107843, + 0.471398756901114, + 0.0574368260587866, + 0.00021897948925966 + ], + [ + 14.2801055058256, + 1.35969015719134, + 0.302017349663514, + 0.0366436798147007, + 0.00079543452651837 + ] + ], + "Pr": [ + [ + 5.60406255377525, + 2.35796259561812, + 0.476001098572882, + 0.0544123374315247, + 0.00021141460220515 + ], + [ + 13.9517490230574, + 1.31239754917439, + 0.294933701192956, + 0.0348601542462324, + 0.00076826168266181 + ] + ], + "Nd": [ + [ + 5.4290839196977, + 2.3368732536088, + 0.483373554104881, + 0.0514654964557479, + 0.00020377613286376 + ], + [ + 13.650364942766, + 1.26759841390319, + 0.288610681576929, + 0.0331291807446609, + 0.00074234848380127 + ] + ], + "Pm": [ + [ + 5.26774445089444, + 2.30855813326305, + 0.493265479026012, + 0.0486356279741696, + 0.00019630884232068 + ], + [ + 13.3602096827394, + 1.22585856586941, + 0.282919632127866, + 0.0314735397127137, + 0.00071765802506962 + ] + ], + "Sm": [ + [ + 5.12680428517077, + 2.2692553400838, + 0.504209300453302, + 0.0456923992416222, + 0.00018867505049389 + ], + [ + 13.1581501026129, + 1.18129508243337, + 0.277107038219062, + 0.0297957604549544, + 0.0006940110991991 + ] + ], + "Eu": [ + [ + 4.97962359749809, + 2.24183087455669, + 0.512933961402893, + 0.0429801848498334, + 0.00018138169248579 + ], + [ + 12.8392663078033, + 1.14705446420486, + 0.270387160935243, + 0.0282418714952602, + 0.00067148660317399 + ] + ], + "Gd": [ + [ + 5.07835830045611, + 1.95744027181659, + 0.592825983221375, + 0.0419502034143925, + 0.00017524109152832 + ], + [ + 10.5132725469047, + 1.11764941281524, + 0.284386741833675, + 0.0272663327641347, + 0.0006503108607073 + ] + ], + "Tb": [ + [ + 4.711616366573, + 2.17261950786578, + 0.539717601342865, + 0.0379796004547811, + 0.00016692376356484 + ], + [ + 12.3109415948539, + 1.08743796767492, + 0.259804965509275, + 0.0253289923895189, + 0.00062927856696191 + ] + ], + "Dy": [ + [ + 4.59075504485146, + 2.13572373036567, + 0.551355560094152, + 0.0356058406718513, + 0.00015982401685679 + ], + [ + 12.0656740623369, + 1.05811737115864, + 0.253994438918661, + 0.0239508739649649, + 0.00060948274843329 + ] + ], + "Ho": [ + [ + 4.4840011062671, + 2.08904347078539, + 0.565932618316104, + 0.0332703590211063, + 0.00015244561028289 + ], + [ + 11.874925069157, + 1.02828493708789, + 0.248907807908076, + 0.0225844065532147, + 0.00059037444515942 + ] + ], + "Er": [ + [ + 4.37665140786963, + 2.04645150383634, + 0.580662860591587, + 0.0312388528327766, + 0.00014537486966254 + ], + [ + 11.6653080717139, + 1.00314769675495, + 0.244153292686312, + 0.0213618580000731, + 0.00057211033845477 + ] + ], + "Tm": [ + [ + 4.28308318247419, + 1.9953800145373, + 0.597053571332504, + 0.0290951668869559, + 0.00013806476903752 + ], + [ + 11.4961905956683, + 0.97703910201882, + 0.239429132975829, + 0.0200912233327255, + 0.00055437713849227 + ] + ], + "Yb": [ + [ + 4.19563840737123, + 1.94333285978645, + 0.612446617285465, + 0.0271515207694932, + 0.00013059478735193 + ], + [ + 11.4107750456652, + 0.949011924454877, + 0.234950326535304, + 0.0189051576280975, + 0.00053723364920971 + ] + ], + "Lu": [ + [ + 4.35692593296384, + 1.69589204777315, + 0.66390452006847, + 0.0263020018760281, + 0.00012549731849982 + ], + [ + 9.29434514718568, + 0.91050004595742, + 0.238746595911376, + 0.0182098542546462, + 0.00052155937196356 + ] + ], + "Hf": [ + [ + 4.33138405664923, + 1.5276486472868, + 0.735795922891238, + 0.0249526323201402, + 0.00011874085258106 + ], + [ + 7.87684433810288, + 0.942515642627748, + 0.241699480039806, + 0.0172899894448509, + 0.00050583463131353 + ] + ], + "Ta": [ + [ + 4.19726001319642, + 1.46854806774708, + 0.783931248211088, + 0.0232494094864395, + 0.00011126135860729 + ], + [ + 6.93674024894457, + 1.01725276618348, + 0.23894893971507, + 0.0162332433075381, + 0.00049023900402115 + ] + ], + "W": [ + [ + 3.97629715819077, + 1.52292684257341, + 0.797706246390561, + 0.0213666741558685, + 0.00010307868938572 + ], + [ + 6.29685779228774, + 1.11289951157691, + 0.231057040678473, + 0.0151013545567204, + 0.00047468247710503 + ] + ], + "Re": [ + [ + 3.75144381439811, + 1.62768802977632, + 0.781756755454271, + 0.0195170803912291, + 9.431998042934e-05 + ], + [ + 5.79754636082953, + 1.1822363107771, + 0.21991358602477, + 0.0139810455481456, + 0.00045912712582984 + ] + ], + "Os": [ + [ + 3.48401517338711, + 1.79377920416965, + 0.74487835669192, + 0.0175427785162434, + 8.448723515689e-05 + ], + [ + 5.43998846080953, + 1.22792134140128, + 0.206208856992909, + 0.0127899401721146, + 0.00044303222678748 + ] + ], + "Ir": [ + [ + 1.599565781988442, + 2.975344521941205, + 6.950926783668822e-01, + 1.487796274586880e-02, + 6.905495791015833e-05 + ], + [ + 5.792444473855675, + 1.553009829732260, + 1.886263359366482e-01, + 1.117634706624533e-02, + 4.227723616555514e-04 + ] + ], + "Pt": [ + [ + 2.04021563975728, + 2.89922634624825, + 0.636344083815797, + 0.0132071919595408, + 5.673821925001e-05 + ], + [ + 6.65819429609627, + 1.41337923778932, + 0.174000104502179, + 0.010068780453021, + 0.00040307710569222 + ] + ], + "Au": [ + [ + 1.6759346706487, + 3.00486602969729, + 0.595340013161635, + 0.0117163186623094, + 4.296782976398e-05 + ], + [ + 5.52231093211402, + 1.38007223007196, + 0.162229237655945, + 0.00901814890416575, + 0.00037927766747767 + ] + ], + "Hg": [ + [ + 2.23522850443105, + 2.68276638651994, + 0.555194926212433, + 0.0107273354366258, + 3.284739920463e-05 + ], + [ + 5.0203098896024, + 1.23077590583777, + 0.152248122992863, + 0.0082839911690621, + 0.00035624193893176 + ] + ], + "Tl": [ + [ + 2.8034273741251, + 2.7188278796602, + 0.522475915391999, + 0.00984537836971042, + 2.345245265083e-05 + ], + [ + 6.55876872804534, + 1.16972422516667, + 0.143556691800178, + 0.00761976526230039, + 0.00032962767390299 + ] + ], + "Pb": [ + [ + 3.60861020997771, + 2.45056774737198, + 0.478639500107257, + 0.00887214235169215, + 1.040019329095e-05 + ], + [ + 6.58162594621962, + 1.02772852660588, + 0.13353368063472, + 0.00684861238948429, + 0.00027638887545667 + ] + ], + "Bi": [ + [ + 4.24209901057315, + 2.09994342055827, + 0.432836631379922, + 0.00802021570381059, + 7.2178503085e-07 + ], + [ + 5.7528011553608, + 0.873901489195461, + 0.123599956180524, + 0.0061760033305857, + 0.00014149517761723 + ] + ], + "Po": [ + [ + 4.63620095668962, + 1.78063311426987, + 0.403730443952738, + 0.00768539554668674, + 8.954159028e-08 + ], + [ + 4.88825299780982, + 0.756310526880074, + 0.117302364452249, + 0.00593034394242894, + 7.66668663873e-05 + ] + ], + "At": [ + [ + 4.9659225059507, + 1.43815561507033, + 0.371299200579298, + 0.00747256428450201, + 1.1411528415e-07 + ], + [ + 4.09129378787496, + 0.629228996602551, + 0.111096678695535, + 0.00577235010347761, + 8.085118938794e-05 + ] + ], + "Rn": [ + [ + 5.30615614475033, + 1.11733160236072, + 0.315858723110854, + 0.0072034224231021, + 1.0735533055e-07 + ], + [ + 3.48800735481655, + 0.481190741149771, + 0.101874467945753, + 0.00559245374417078, + 7.795066735407e-05 + ] + ], + "Fr": [ + [ + 4.52053399042336, + 4.10695397909124, + 0.713946878503728, + 0.0169294027687453, + 8.574921291917e-05 + ], + [ + 19.4482234232948, + 1.89824673155996, + 0.169553563595341, + 0.0114819567548934, + 0.00034612203825798 + ] + ], + "Ra": [ + [ + 6.52401072001873, + 3.20787080745661, + 0.540478774349637, + 0.00878278806898853, + 6.91010602949e-06 + ], + [ + 14.0092554298974, + 1.32635035961665, + 0.131408568386036, + 0.00628647434520409, + 0.00022783998145895 + ] + ], + "Ac": [ + [ + 6.89602853559619, + 2.83514154536523, + 0.503506881888815, + 0.00802294510966706, + 9.204009548e-08 + ], + [ + 11.0763825670398, + 1.17132616290503, + 0.123494651391799, + 0.00573515595790497, + 7.197075418025e-05 + ] + ], + "Th": [ + [ + 7.0937490016263, + 2.52912373903194, + 0.482107819888889, + 0.00771936674145111, + 7.271141994e-08 + ], + [ + 9.09473795165944, + 1.06391667033161, + 0.118619446621877, + 0.00553945708895034, + 6.584947305285e-05 + ] + ], + "Pa": [ + [ + 6.43401324797284, + 2.97099970535788, + 0.460796651787848, + 0.00724033125775455, + 6.362366643e-08 + ], + [ + 10.2596851307297, + 1.13177452306163, + 0.112775935362382, + 0.00527459583353132, + 6.192638240888e-05 + ] + ], + "U": [ + [ + 6.21070826784019, + 3.03934453825623, + 0.437339984439162, + 0.00680714764147316, + 6.182293277e-08 + ], + [ + 10.0214105859162, + 1.10399880155883, + 0.107218973162453, + 0.00502432130850682, + 6.005069941972e-05 + ] + ], + "Np": [ + [ + 6.00431598308986, + 3.09431654531418, + 0.412808487716417, + 0.00640891657880066, + 6.730073762e-08 + ], + [ + 9.81793046719106, + 1.06978705068029, + 0.101635291448645, + 0.00479524846888725, + 6.028065987265e-05 + ] + ], + "Pu": [ + [ + 5.20061716810304, + 3.49849440367144, + 0.40541493113132, + 0.00622343700826529, + 6.008593295e-08 + ], + [ + 11.2038310194406, + 1.12846369546928, + 0.0989333472686131, + 0.0046591743197795, + 5.725906389623e-05 + ] + ], + "Am": [ + [ + 5.02533860036029, + 3.51843988285154, + 0.381950349446272, + 0.00582110208096211, + 6.526093388e-08 + ], + [ + 10.9772190697628, + 1.0847721483263, + 0.0936746874853835, + 0.00442682346873485, + 5.738568370505e-05 + ] + ], + "Cm": [ + [ + 5.34656100260619, + 3.2246846656091, + 0.349461752625445, + 0.00529251756748815, + 6.159176302e-08 + ], + [ + 9.23118379752449, + 0.972836229193835, + 0.0870596220966562, + 0.00413011964465032, + 5.494638944497e-05 + ] + ], + "Bk": [ + [ + 5.22582350334004, + 3.22818873995295, + 0.327098878823851, + 0.00488882575592239, + 5.212722694e-08 + ], + [ + 9.07135706618718, + 0.931124277456634, + 0.0820720602059845, + 0.00389029655993416, + 5.103679766366e-05 + ] + ], + "Cf": [ + [ + 4.58641247753547, + 3.51869595665101, + 0.319261714201205, + 0.00472979647985625, + 5.513244808e-08 + ], + [ + 10.3186102092335, + 0.957278837829232, + 0.079680743144388, + 0.00377193199996848, + 5.09750860376e-05 + ] + ], + "Es": [ + [ + 4.45799480675412, + 3.50867212637662, + 0.301375380528315, + 0.00440763746214481, + 4.887879032e-08 + ], + [ + 10.0890693383401, + 0.919446447361622, + 0.0756419177786329, + 0.00357026465124809, + 4.804951743958e-05 + ] + ], + "Fm": [ + [ + 4.3389757640117, + 3.49185098896403, + 0.281471099016286, + 0.00400209271499046, + 5.529298082e-08 + ], + [ + 9.96330937223836, + 0.878083956982974, + 0.0711637115768777, + 0.00332152404215077, + 4.851031794604e-05 + ] + ], + "Md": [ + [ + 4.22729470403101, + 3.47249227510661, + 0.264822229496898, + 0.00369072877833192, + 6.258714262e-08 + ], + [ + 9.72400602040031, + 0.84287377596021, + 0.0673534743974851, + 0.0031236460626332, + 4.912170970176e-05 + ] + ], + "No": [ + [ + 4.1095170244302, + 3.4579913252275, + 0.247087351222386, + 0.00330423920940995, + 5.991049262e-08 + ], + [ + 9.6773599451012, + 0.806940042470817, + 0.0632815436954167, + 0.00287544639641209, + 4.706791536976e-05 + ] + ], + "Lr": [ + [ + 4.52147421198378, + 3.20212985587804, + 0.223028726956451, + 0.00281716453892029, + 4.06427954e-08 + ], + [ + 8.28309906861142, + 0.731918958125393, + 0.0580942773018655, + 0.00256168016047444, + 4.03816515529e-05 + ] + ] +} diff --git a/src/quantem/diffraction/digital_dark_field.py b/src/quantem/diffraction/digital_dark_field.py new file mode 100644 index 000000000..a09746cb5 --- /dev/null +++ b/src/quantem/diffraction/digital_dark_field.py @@ -0,0 +1,199 @@ +"""Clustering-based digital dark field imaging. + +Implements the workflow of MacLaren and co-workers: DBSCAN in the joint +(diffraction, scan) space groups the detected Bragg peaks into single-spot, +single-crystallite clusters (L1); clustering the real-space centers of mass +of those clusters groups the g-vectors of each grain (L2); clustering the +remaining unindexed peaks in diffraction space alone isolates ring-like +nanocrystalline or amorphous components (L3). Each cluster's summed +intensity per probe position is a digital dark field image. + +The clustering itself is the generic quantem.core.utils.clustering.dbscan / +cluster_vector; this module holds the diffraction-specific pieces: centers +of mass, DDF image formation, and composite color rendering. +""" + +from __future__ import annotations + +import numpy as np + +from quantem.core.utils.clustering import cluster_vector, dbscan # noqa: F401 + + +def _scan_cells(vector) -> np.ndarray: + """(N, 2) scan (row, col) of every flattened row of a ragged Vector.""" + counts = np.asarray(vector.row_counts(), dtype=int) + shape = vector.shape[:2] + cell_r, cell_c = np.divmod(np.arange(counts.size), shape[1]) + return np.stack( + [np.repeat(cell_r, counts), np.repeat(cell_c, counts)], axis=1 + ) + + +def cluster_coms( + labeled, + label_field: str = "cluster", + intensity_field: str = "intensity", + weighted: bool = True, +): + """Real-space center of mass of every cluster. + + Parameters + ---------- + labeled : Vector + Vector carrying a cluster label field (from cluster_vector). + weighted : bool, default=True + Weight the center of mass by peak intensity. + + Returns + ------- + coms : np.ndarray + (K, 2) scan-coordinate centers of mass, ordered by cluster id. + sizes : np.ndarray + (K,) number of peaks per cluster. + """ + fields = labeled.fields + flat = labeled.flatten() + labels = flat[:, fields.index(label_field)].astype(int) + w = flat[:, fields.index(intensity_field)].clip(min=0) if weighted else None + rc = _scan_cells(labeled).astype(float) + + n = labels.max() + 1 + coms = np.zeros((n, 2)) + sizes = np.zeros(n, dtype=int) + for k in range(n): + m = labels == k + sizes[k] = int(m.sum()) + if sizes[k] == 0: + coms[k] = np.nan + continue + wk = w[m] if w is not None else np.ones(sizes[k]) + wk = wk / max(wk.sum(), 1e-12) + coms[k] = (rc[m] * wk[:, None]).sum(axis=0) + return coms, sizes + + +def ddf_images( + labeled, + cluster_ids, + label_field: str = "cluster", + intensity_field: str = "intensity", +) -> np.ndarray: + """Digital dark field images: per-cluster summed intensity per position. + + Returns + ------- + np.ndarray + (len(cluster_ids), scan_row, scan_col) images. + """ + fields = labeled.fields + flat = labeled.flatten() + labels = flat[:, fields.index(label_field)].astype(int) + inten = flat[:, fields.index(intensity_field)].clip(min=0) + rc = _scan_cells(labeled) + R, C = labeled.shape[:2] + + out = np.zeros((len(cluster_ids), R, C)) + for i, k in enumerate(np.atleast_1d(cluster_ids)): + m = labels == k + np.add.at(out[i], (rc[m, 0], rc[m, 1]), inten[m]) + return out + + +def composite_ddf( + images: np.ndarray, + colors=None, + gamma: float = 0.33, + normalize: str = "each", +) -> np.ndarray: + """Blend a stack of DDF images into one RGB composite. + + Parameters + ---------- + images : np.ndarray + (K, R, C) cluster images. + colors : array-like | None + (K, 3) RGB color per image; defaults to evenly spaced hues. + gamma : float, default=0.33 + Power scaling applied to each normalized image before coloring. + normalize : {"each", "global"} + Normalize each image to its own maximum, or all to the stack max. + + Returns + ------- + np.ndarray + (R, C, 3) RGB image in [0, 1]. + """ + from matplotlib.colors import hsv_to_rgb + + K = images.shape[0] + if colors is None: + hues = np.linspace(0, 1, K, endpoint=False) + colors = hsv_to_rgb( + np.stack([hues, np.ones(K), np.ones(K)], axis=1) + ) + colors = np.asarray(colors, dtype=float) + + if normalize == "global": + norm = np.full(K, max(float(images.max()), 1e-12)) + else: + norm = np.maximum(images.reshape(K, -1).max(axis=1), 1e-12) + scaled = (images / norm[:, None, None]) ** gamma + rgb = np.einsum("krc,kj->rcj", scaled, colors) + return np.clip(rgb, 0, 1) + + +def color_wheel(n: int = 256, saturation: float = 1.0) -> np.ndarray: + """(n, n, 4) RGBA hue wheel for labeling composite images.""" + from matplotlib.colors import hsv_to_rgb + + y, x = np.mgrid[-1 : 1 : n * 1j, -1 : 1 : n * 1j] + r = np.hypot(x, y) + hue = (np.arctan2(y, x) / (2 * np.pi)) % 1.0 + hsv = np.stack( + [hue, np.full_like(hue, saturation), np.clip(r, 0, 1)], axis=-1 + ) + rgba = np.concatenate( + [hsv_to_rgb(hsv), (r <= 1.0)[..., None].astype(float)], axis=-1 + ) + return rgba + + +def plot_cluster_scatter( + labeled, + q_fields=("qx", "qy"), + label_field: str = "cluster", + max_clusters: int | None = None, + show_noise: bool = True, + point_size: float = 2.0, + figax=None, +): + """All peaks in diffraction space, colored by cluster (noise in gray).""" + import matplotlib.pyplot as plt + + fields = labeled.fields + flat = labeled.flatten() + labels = flat[:, fields.index(label_field)].astype(int) + qx = flat[:, fields.index(q_fields[0])] + qy = flat[:, fields.index(q_fields[1])] + + if figax is None: + fig, ax = plt.subplots(figsize=(6.5, 6.5)) + else: + fig, ax = figax + if show_noise: + m = labels < 0 + ax.scatter(qy[m], qx[m], s=point_size, color="0.85", lw=0) + n = labels.max() + 1 + ids = range(n if max_clusters is None else min(n, max_clusters)) + cmap = plt.get_cmap("hsv") + rng = np.random.default_rng(0) + hues = rng.permutation(np.linspace(0, 1, len(list(ids)), endpoint=False)) + for k in ids: + m = labels == k + ax.scatter(qy[m], qx[m], s=point_size, color=cmap(hues[k]), lw=0) + ax.set_aspect("equal") + ax.invert_yaxis() + ax.set_xlabel("$q_c$") + ax.set_ylabel("$q_r$") + return fig, ax diff --git a/src/quantem/diffraction/disk_detection.py b/src/quantem/diffraction/disk_detection.py new file mode 100644 index 000000000..5ec5f00f6 --- /dev/null +++ b/src/quantem/diffraction/disk_detection.py @@ -0,0 +1,1149 @@ +from __future__ import annotations + +import numpy as np +import torch + +SUBPIXEL_MODES = ("none", "parabolic", "upsample") + + +def make_template( + probe: torch.Tensor, + center: tuple[float, float] | None = None, + subtract_mean: bool = False, +) -> torch.Tensor: + """Build a cross-correlation template from a (vacuum) probe image. + + The probe is normalized to unit sum and rolled so its center sits at the array + origin ``[0, 0]`` (FFT corner), so correlation peaks land at absolute disk + positions. + + Parameters + ---------- + probe : torch.Tensor + ``(H, W)`` probe / vacuum disk image. + center : tuple of float, optional + ``(row, col)`` probe center rolled to the origin; defaults to the geometric + center ``(H // 2, W // 2)``. + subtract_mean : bool, default=False + If ``True``, make the template zero-sum — a band-pass kernel that suppresses + the uniform background in the correlation. + + Returns + ------- + torch.Tensor + ``(H, W)`` template, corner-centered (and zero-sum when ``subtract_mean``). + """ + probe = torch.as_tensor(probe) + total = probe.sum() + if total != 0: + probe = probe / total + + H, W = probe.shape + if center is None: + cr, cc = H // 2, W // 2 + else: + cr, cc = int(round(float(center[0]))), int(round(float(center[1]))) + + template = torch.roll(probe, shifts=(-cr, -cc), dims=(0, 1)) + if subtract_mean: + template = template - template.mean() + return template + + +def synthetic_probe( + shape: tuple[int, int], + radius: float, + edge: float = 1.0, + center: tuple[float, float] | None = None, +) -> torch.Tensor: + """Soft-edged disk for a synthetic correlation template. + + Returns ``0.5 - 0.5*tanh((r - radius)/edge)``: a disk of the given ``radius`` + (pixels) with a ``tanh`` falloff over ``edge`` pixels. + + Parameters + ---------- + shape : tuple of int + ``(H, W)`` output shape in pixels. + radius : float + Disk radius in pixels. + edge : float, default=1.0 + Width in pixels of the ``tanh`` edge falloff. + center : tuple of float, optional + ``(row, col)`` disk center; defaults to the geometric center + ``((H - 1) / 2, (W - 1) / 2)``. + + Returns + ------- + torch.Tensor + ``(H, W)`` soft-edged disk image. + """ + H, W = int(shape[0]), int(shape[1]) + if center is None: + cr, cc = (H - 1) / 2.0, (W - 1) / 2.0 + else: + cr, cc = float(center[0]), float(center[1]) + rows = torch.arange(H, dtype=torch.float).view(H, 1) + cols = torch.arange(W, dtype=torch.float).view(1, W) + rr = torch.sqrt((rows - cr) ** 2 + (cols - cc) ** 2) + edge = max(float(edge), 1e-6) + return 0.5 - 0.5 * torch.tanh((rr - float(radius)) / edge) + + +def _central_blob(image: torch.Tensor, threshold: float) -> tuple[np.ndarray | None, np.ndarray]: + """Mask of the connected bright region containing the brightest pixel. + + Thresholds ``image`` at ``threshold * max`` and keeps only the connected + component holding the brightest pixel — normally the central (unscattered) + disk of a mean diffraction pattern or vacuum probe — so other diffracted disks + are excluded. + + Parameters + ---------- + image : torch.Tensor + ``(H, W)`` image, e.g. a mean diffraction pattern or vacuum probe. + threshold : float + Fraction of the peak intensity (after min-subtraction) used to threshold + the image before connected-component labeling. + + Returns + ------- + blob_mask : np.ndarray or None + Boolean ``(H, W)`` mask of the central component, or ``None`` for an empty + / flat image. + img : np.ndarray + The min-subtracted image. + """ + from scipy import ndimage + + img = np.asarray(torch.as_tensor(image, dtype=torch.float).detach().cpu()) + img = img - img.min() + peak = float(img.max()) + if peak <= 0: + return None, img + labels, n = ndimage.label(img >= threshold * peak) + if n == 0: + return None, img + peak_label = int(labels[np.unravel_index(int(np.argmax(img)), img.shape)]) + return labels == peak_label, img + + +def estimate_central_beam( + image: torch.Tensor, + threshold: float = 0.5, + plot_result: bool = False, + **kwargs, +) -> tuple[tuple[float, float], float]: + """Center ``(row, col)`` and radius (pixels) of the central (direct) beam. + + Locates the connected bright region containing the brightest pixel (see + :func:`_central_blob`) — the unscattered / direct beam of a mean diffraction + pattern — and returns its intensity-weighted center together with an + area-equivalent radius (``A = pi r^2``). Other diffracted disks are excluded, so + the estimate holds whether the pattern shows one disk or many. Falls back to the + geometric center and unit radius for an empty / flat image. + + Parameters + ---------- + image : torch.Tensor or Dataset2d + ``(H, W)`` image — a raw array / tensor or a :class:`Dataset2d` (e.g. + ``dataset.dp_mean``). + threshold : float, default=0.5 + Fraction of the peak intensity used to threshold the image when isolating + the central beam (see :func:`_central_blob`). + plot_result : bool, default=False + If ``True``, show the image in greyscale with the fitted beam drawn as a red + circle. + **kwargs + Extra keyword arguments (e.g. ``norm``, ``cbar``, ``scalebar``) forwarded to + :func:`~quantem.core.visualization.show_2d` when ``plot_result=True``. + + Returns + ------- + center : tuple of float + ``(row, col)`` intensity-weighted center of the central beam. + radius : float + Area-equivalent radius in pixels (``A = pi r^2``). + """ + arr = image.array if hasattr(image, "array") else image + blob, img = _central_blob(arr, threshold) + if blob is None: + center = (img.shape[0] / 2.0, img.shape[1] / 2.0) + radius = 1.0 + else: + radius = float(np.sqrt(max(float(blob.sum()), 1.0) / np.pi)) + w = img * blob + total = float(w.sum()) + if total <= 0: + rr, cc = np.nonzero(blob) + center = (float(rr.mean()), float(cc.mean())) + else: + rows = np.arange(img.shape[0])[:, None] + cols = np.arange(img.shape[1])[None, :] + center = (float((w * rows).sum() / total), float((w * cols).sum() / total)) + + if plot_result: + from matplotlib.patches import Circle + + from quantem.core.visualization import show_2d + + show_kwargs = {"cmap": "gray", "title": "central beam", **kwargs} + _fig, ax = show_2d(arr, **show_kwargs) + ax.add_patch( + Circle((center[1], center[0]), radius, fill=False, edgecolor="red", linewidth=1.5) + ) + + return center, radius + + +def probe_centroid(probe: torch.Tensor) -> tuple[float, float]: + """Intensity-weighted ``(row, col)`` centroid of a probe image. + + Parameters + ---------- + probe : torch.Tensor + ``(H, W)`` probe image. Negative values are clamped to zero before + weighting. + + Returns + ------- + tuple of float + ``(row, col)`` intensity-weighted centroid; the geometric center for a + non-positive image. + """ + p = torch.clamp(torch.as_tensor(probe, dtype=torch.float), min=0.0) + total = p.sum() + if total <= 0: + return (p.shape[0] / 2.0, p.shape[1] / 2.0) + rows = torch.arange(p.shape[0], dtype=torch.float, device=p.device).view(-1, 1) + cols = torch.arange(p.shape[1], dtype=torch.float, device=p.device).view(1, -1) + return (float((p * rows).sum() / total), float((p * cols).sum() / total)) + + +def template_fourier(template: torch.Tensor) -> torch.Tensor: + """Pre-compute the conjugate FT of a template for repeated correlation. + + Parameters + ---------- + template : torch.Tensor + ``(H, W)`` corner-centered correlation template. + + Returns + ------- + torch.Tensor + ``(H, W)`` complex ``conj(fft2(template))``, ready to multiply against + ``fft2(dp)``. + """ + return torch.conj(torch.fft.fft2(template)) + + +def _background_highpass( + shape: tuple[int, int], + background_sigma: float, + device, + rfft: bool = False, +) -> torch.Tensor: + """Fourier-domain high-pass ``1 - G`` removing correlation background. + + ``G`` is the transform of a real-space Gaussian of standard deviation + ``background_sigma`` pixels, so multiplying the Fourier product by ``1 - G`` + subtracts a Gaussian-smoothed copy of the correlation map -- the slowly + varying background (the zero-sum template's negative moat around the bright + central beam) that otherwise pushes weak disk peaks below zero, where the + ``relu`` clamp erases them before peak finding. + + Parameters + ---------- + shape : tuple of int + ``(H, W)`` detector shape. + background_sigma : float + Real-space standard deviation of the subtracted background, in pixels. + device + Torch device for the filter tensor. + rfft : bool, default=False + If ``True``, return the ``(H, W // 2 + 1)`` half-plane filter for + ``rfft2`` products instead of the full ``(H, W)`` filter. + + Returns + ------- + torch.Tensor + The ``1 - G`` filter in the requested Fourier layout. + """ + H, W = int(shape[0]), int(shape[1]) + qr = torch.fft.fftfreq(H, device=device, dtype=torch.float)[:, None] + if rfft: + qc = torch.fft.rfftfreq(W, device=device, dtype=torch.float)[None, :] + else: + qc = torch.fft.fftfreq(W, device=device, dtype=torch.float)[None, :] + g = torch.exp(-2.0 * (torch.pi**2) * (float(background_sigma) ** 2) * (qr**2 + qc**2)) + return 1.0 - g + + +def cross_correlation( + dp: torch.Tensor, + template_ft: torch.Tensor, + background_sigma: float | None = None, +) -> tuple[torch.Tensor, torch.Tensor]: + """Cross-correlate a diffraction pattern with a template. + + Parameters + ---------- + dp : torch.Tensor + ``(H, W)`` diffraction pattern. + template_ft : torch.Tensor + ``(H, W)`` pre-computed template FT from :func:`template_fourier`. + + Returns + ------- + corr_map : torch.Tensor + ``(H, W)`` real-space correlation map ``relu(real(ifft2(m)))`` (used for + peak finding). + m : torch.Tensor + ``(H, W)`` Fourier-domain product ``fft2(dp) * template_ft`` (used for DFT + subpixel refinement). + """ + dp = torch.as_tensor(dp) + m = torch.fft.fft2(dp) * template_ft + if background_sigma is not None and background_sigma > 0: + m = m * _background_highpass(m.shape[-2:], background_sigma, m.device) + corr_map = torch.clamp(torch.fft.ifft2(m).real, min=0.0) + return corr_map, m + + +def cross_correlation_batch( + dps: torch.Tensor, + template_ft: torch.Tensor, + background_sigma: float | None = None, +) -> tuple[torch.Tensor, torch.Tensor]: + """Cross-correlate a stack of diffraction patterns with one template. + + Batched form of :func:`cross_correlation`. ``fft2`` acts on the trailing two + axes and the ``(H, W)`` ``template_ft`` broadcasts over the batch, so the result + for each pattern is bit-identical to :func:`cross_correlation`. + + Parameters + ---------- + dps : torch.Tensor + ``(B, H, W)`` stack of diffraction patterns. + template_ft : torch.Tensor + ``(H, W)`` pre-computed template FT from :func:`template_fourier`. + + Returns + ------- + corr_map : torch.Tensor + ``(B, H, W)`` real-space correlation maps. + m : torch.Tensor + ``(B, H, W)`` Fourier-domain products. + """ + dps = torch.as_tensor(dps) + m = torch.fft.fft2(dps) * template_ft + if background_sigma is not None and background_sigma > 0: + m = m * _background_highpass(m.shape[-2:], background_sigma, m.device) + corr_map = torch.clamp(torch.fft.ifft2(m).real, min=0.0) + return corr_map, m + + +def _corr_map_rfft( + dps: torch.Tensor, + template_ft: torch.Tensor, + background_sigma: float | None = None, +) -> torch.Tensor: + """Real-FFT correlation map(s), used when no Fourier product is needed downstream. + + For real ``dps`` and a real template the Fourier product is conjugate-symmetric, + so ``relu(real(ifft2(fft2(dps) * template_ft)))`` is reproduced exactly by an + ``rfft2`` / ``irfft2`` pair at roughly half the FFT cost. Only the correlation map + is returned — the complex product (needed solely for DFT upsampling) is skipped. + + Parameters + ---------- + dps : torch.Tensor + ``(H, W)`` or ``(B, H, W)`` diffraction pattern(s). + template_ft : torch.Tensor + ``(H, W)`` pre-computed template FT from :func:`template_fourier`. + + Returns + ------- + torch.Tensor + ``(H, W)`` or ``(B, H, W)`` real-space correlation map(s), ``relu``-clamped. + """ + dps = torch.as_tensor(dps) + H, W = dps.shape[-2], dps.shape[-1] + prod = torch.fft.rfft2(dps) * template_ft[..., : W // 2 + 1] + if background_sigma is not None and background_sigma > 0: + prod = prod * _background_highpass((H, W), background_sigma, prod.device, rfft=True) + corr_map = torch.fft.irfft2(prod, s=(H, W)) + return torch.clamp(corr_map, min=0.0) + + +def detect_disks( + dp: torch.Tensor, + template_ft: torch.Tensor, + *, + min_abs_intensity: float = 0.0, + min_spacing: float = 0.0, + edge_boundary: int = 1, + subpixel: str = "upsample", + upsample_factor: int = 16, + max_num_peaks: int = 1000, + background_sigma: float | None = None, +) -> np.ndarray: + """Detect Bragg disks in one diffraction pattern by template matching. + + Parameters + ---------- + dp : torch.Tensor + ``(H, W)`` diffraction pattern. + template_ft : torch.Tensor + ``(H, W)`` pre-computed template FT from :func:`template_fourier`. + min_abs_intensity : float, default=0.0 + Drop correlation peaks below this absolute intensity. + min_spacing : float, default=0.0 + Minimum spacing in pixels between kept peaks; closer / dimmer peaks are + suppressed. + edge_boundary : int, default=1 + Width in pixels of the border in which peaks are ignored. + subpixel : {"none", "parabolic", "upsample"}, default="upsample" + ``"none"`` returns pixel-resolution peaks; ``"parabolic"`` adds a 3-point + quadratic refinement; ``"upsample"`` further refines each peak by + Guizar-Sicairos DFT upsampling. + upsample_factor : int, default=16 + Upsampling factor for the ``"upsample"`` subpixel refinement. + max_num_peaks : int, default=1000 + Maximum number of peaks to keep (after intensity sorting). + + Returns + ------- + np.ndarray + ``(M, 3)`` array of ``[q_row, q_col, intensity]`` rows, sorted by descending + intensity. + """ + if subpixel not in SUBPIXEL_MODES: + raise ValueError(f"subpixel must be in {SUBPIXEL_MODES}, got {subpixel!r}") + + corr_map, m = cross_correlation(dp, template_ft, background_sigma) + + peaks = _local_maxima(corr_map, edge_boundary) + peaks = _filter_maxima(peaks, min_abs_intensity, min_spacing, max_num_peaks) + + if peaks.shape[0] == 0 or subpixel == "none": + return _to_numpy(peaks) + + peaks = _refine_parabolic(corr_map, peaks) + + if subpixel == "parabolic": + return _to_numpy(peaks) + + peaks = _refine_dft(m, peaks, upsample_factor) + return _to_numpy(peaks) + + +def detect_disks_batch( + dps: torch.Tensor, + template_ft: torch.Tensor, + *, + min_abs_intensity: float = 0.0, + min_spacing: float = 0.0, + edge_boundary: int = 1, + subpixel: str = "upsample", + upsample_factor: int = 16, + max_num_peaks: int = 1000, + background_sigma: float | None = None, +) -> list[np.ndarray]: + """Detect Bragg disks across a stack of diffraction patterns (batched). + + Batched equivalent of :func:`detect_disks`: cross-correlation, peak extraction, + ``min_spacing`` suppression, and subpixel refinement are all batched across + patterns. The local-maxima search and greedy ``min_spacing`` suppression are + vectorized over the whole stack (no per-pattern Python loop) but reproduce the + per-pattern greedy result bit-for-bit, so each output matches :func:`detect_disks` + for that pattern. When ``subpixel`` is not ``"upsample"`` the correlation maps are + formed with a real FFT (``rfft2``), skipping the complex Fourier product that only + DFT upsampling needs. + + Parameters + ---------- + dps : torch.Tensor + ``(B, H, W)`` stack of diffraction patterns. + template_ft : torch.Tensor + ``(H, W)`` pre-computed template FT from :func:`template_fourier`. + min_abs_intensity : float, default=0.0 + Drop correlation peaks below this absolute intensity. + min_spacing : float, default=0.0 + Minimum spacing in pixels between kept peaks; closer / dimmer peaks are + suppressed. + edge_boundary : int, default=1 + Width in pixels of the border in which peaks are ignored. + subpixel : {"none", "parabolic", "upsample"}, default="upsample" + ``"none"`` returns pixel-resolution peaks; ``"parabolic"`` adds a 3-point + quadratic refinement; ``"upsample"`` further refines each peak by + Guizar-Sicairos DFT upsampling. + upsample_factor : int, default=16 + Upsampling factor for the ``"upsample"`` subpixel refinement. + max_num_peaks : int, default=1000 + Maximum number of peaks to keep per pattern (after intensity sorting). + + Returns + ------- + list of np.ndarray + Length-``B`` list of ``(M, 3)`` arrays of ``[q_row, q_col, intensity]`` + rows, each sorted by descending intensity. + """ + if subpixel not in SUBPIXEL_MODES: + raise ValueError(f"subpixel must be in {SUBPIXEL_MODES}, got {subpixel!r}") + + if subpixel == "upsample": + corr_map, m = cross_correlation_batch(dps, template_ft, background_sigma) + else: + corr_map = _corr_map_rfft(dps, template_ft, background_sigma) + m = None + + peaks_all, bidx, counts = _detect_peaks_batched( + corr_map, edge_boundary, min_abs_intensity, min_spacing, max_num_peaks + ) + + if subpixel == "none" or peaks_all.shape[0] == 0: + return _split_by_counts(peaks_all, counts) + + peaks_all = _refine_parabolic_batched(corr_map, peaks_all, bidx) + if subpixel == "upsample": + peaks_all = _refine_dft_batched(m, peaks_all, bidx, upsample_factor) + + return _split_by_counts(peaks_all, counts) + + +# ---- helpers ---- + + +def _local_maxima(corr_map: torch.Tensor, edge_boundary: int) -> torch.Tensor: + """Find 8-neighbor local maxima, sorted by descending intensity. + + Parameters + ---------- + corr_map : torch.Tensor + ``(H, W)`` correlation map. + edge_boundary : int + Width in pixels of the border in which maxima are ignored. + + Returns + ------- + torch.Tensor + ``(K, 3)`` tensor of ``[row, col, intensity]`` maxima, sorted by descending + intensity. + """ + is_max = _local_maxima_mask(corr_map, edge_boundary) + return _extract_maxima(corr_map, is_max) + + +def _local_maxima_mask(a: torch.Tensor, edge_boundary: int) -> torch.Tensor: + """Boolean 8-neighbor local-maxima mask of ``a``. + + Works on a single ``(H, W)`` map or a batch ``(B, H, W)`` — the neighbor + comparisons and edge masking use the trailing two axes — so the same code drives + the single-pattern and batched detection paths bit-identically. + + Parameters + ---------- + a : torch.Tensor + ``(H, W)`` or ``(B, H, W)`` correlation map(s). + edge_boundary : int + Width in pixels of the border (clamped to at least 1) set to ``False``. + + Returns + ------- + torch.Tensor + Boolean mask the same shape as ``a``, ``True`` at 8-neighbor local maxima. + """ + is_max = ( + (a >= torch.roll(a, (-1, 0), dims=(-2, -1))) + & (a > torch.roll(a, (1, 0), dims=(-2, -1))) + & (a >= torch.roll(a, (0, -1), dims=(-2, -1))) + & (a > torch.roll(a, (0, 1), dims=(-2, -1))) + & (a >= torch.roll(a, (-1, -1), dims=(-2, -1))) + & (a > torch.roll(a, (-1, 1), dims=(-2, -1))) + & (a >= torch.roll(a, (1, -1), dims=(-2, -1))) + & (a > torch.roll(a, (1, 1), dims=(-2, -1))) + ) + + eb = max(1, int(edge_boundary)) + is_max[..., :eb, :] = False + is_max[..., -eb:, :] = False + is_max[..., :, :eb] = False + is_max[..., :, -eb:] = False + return is_max + + +def _extract_maxima(a: torch.Tensor, is_max: torch.Tensor) -> torch.Tensor: + """Gather masked maxima of one ``(H, W)`` map into a descending-sorted ``(K, 3)``. + + Rows are taken in row-major ``nonzero`` order, then stably sorted by descending + intensity — matching the original single-pattern behaviour. + + Parameters + ---------- + a : torch.Tensor + ``(H, W)`` correlation map. + is_max : torch.Tensor + Boolean ``(H, W)`` local-maxima mask from :func:`_local_maxima_mask`. + + Returns + ------- + torch.Tensor + ``(K, 3)`` tensor of ``[row, col, intensity]`` maxima, sorted by descending + intensity. + """ + rows, cols = torch.nonzero(is_max, as_tuple=True) + intensity = a[rows, cols] + order = torch.argsort(intensity, descending=True) + return torch.stack((rows[order].to(a.dtype), cols[order].to(a.dtype), intensity[order]), dim=1) + + +def _filter_maxima( + peaks: torch.Tensor, + min_abs_intensity: float, + min_spacing: float, + max_num_peaks: int, +) -> torch.Tensor: + """Drop dim peaks, suppress peaks closer than ``min_spacing``, cap the count. + + Parameters + ---------- + peaks : torch.Tensor + ``(K, 3)`` ``[row, col, intensity]`` peaks, sorted by descending intensity. + min_abs_intensity : float + Drop peaks below this absolute intensity (ignored when ``<= 0``). + min_spacing : float + Minimum spacing in pixels; for each kept peak, dimmer peaks within this + distance are suppressed (ignored when ``<= 0``). + max_num_peaks : int + Maximum number of peaks to keep; the brightest are retained. + + Returns + ------- + torch.Tensor + ``(M, 3)`` filtered peaks. + """ + if peaks.shape[0] == 0: + return peaks + + if min_abs_intensity > 0: + peaks = peaks[peaks[:, 2] >= min_abs_intensity] + + if min_spacing > 0 and peaks.shape[0] > 1: + keep = torch.ones(peaks.shape[0], dtype=torch.bool, device=peaks.device) + rc = peaks[:, :2] + for i in range(peaks.shape[0]): + if not keep[i]: + continue + d2 = ((rc - rc[i]) ** 2).sum(dim=1) + too_close = d2 < min_spacing**2 + too_close[: i + 1] = False + keep[too_close] = False + peaks = peaks[keep] + + if max_num_peaks is not None and peaks.shape[0] > max_num_peaks: + peaks = peaks[:max_num_peaks] + + return peaks + + +def _detect_peaks_batched( + corr: torch.Tensor, + edge_boundary: int, + min_abs_intensity: float, + min_spacing: float, + max_num_peaks: int, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Extract, suppress, and cap peaks across a whole ``(B, H, W)`` stack at once. + + Vectorized replacement for the per-pattern ``_extract_maxima`` + ``_filter_maxima`` + loop. Local maxima are found and intensity-thresholded for the whole stack, then + the greedy ``min_spacing`` suppression runs as a single loop over the *rank* axis + (brightest-first) that is batched over every pattern. Suppressing peak ``k``'s + fainter neighbours only when ``k`` is still kept reproduces the per-pattern greedy + result of :func:`_filter_maxima` exactly, with no Python loop over patterns. + + Parameters + ---------- + corr : torch.Tensor + ``(B, H, W)`` correlation maps. + edge_boundary : int + Width in pixels of the border in which maxima are ignored. + min_abs_intensity : float + Drop peaks below this absolute intensity (ignored when ``<= 0``). + min_spacing : float + Minimum spacing in pixels; for each kept peak, fainter peaks within this + distance are suppressed (ignored when ``<= 0``). + max_num_peaks : int + Maximum number of peaks kept per pattern (the brightest are retained). + + Returns + ------- + peaks : torch.Tensor + ``(T, 3)`` ``[row, col, intensity]`` peaks pooled over all patterns, grouped + by ascending pattern index and sorted by descending intensity within a pattern. + bidx : torch.Tensor + ``(T,)`` pattern index of each peak. + counts : torch.Tensor + ``(B,)`` number of peaks kept per pattern. + """ + B = corr.shape[0] + device = corr.device + dtype = corr.dtype + + mask = _local_maxima_mask(corr, edge_boundary) + if min_abs_intensity > 0: + mask = mask & (corr >= min_abs_intensity) + + idx = torch.nonzero(mask) # (T0, 3): [b, row, col] + counts = torch.zeros(B, dtype=torch.long, device=device) + if idx.shape[0] == 0: + return corr.new_zeros((0, 3)), torch.zeros(0, dtype=torch.long, device=device), counts + + bcand, rcand, ccand = idx[:, 0], idx[:, 1], idx[:, 2] + inten = corr[bcand, rcand, ccand] + + # Group candidates by pattern, descending intensity within each pattern. A global + # descending-intensity sort followed by a stable sort on the pattern index keeps the + # within-pattern order identical to the per-pattern argsort in _extract_maxima. + o1 = torch.argsort(inten, descending=True) + order = o1[torch.argsort(bcand[o1], stable=True)] + bcand, rcand, ccand, inten = bcand[order], rcand[order], ccand[order], inten[order] + + counts = torch.bincount(bcand, minlength=B) + kmax = int(counts.max()) + + # Pack candidates into a dense (B, kmax) grid indexed by [pattern, brightness rank]. + starts = torch.zeros(B, dtype=torch.long, device=device) + starts[1:] = torch.cumsum(counts, 0)[:-1] + rank = torch.arange(bcand.shape[0], device=device) - starts[bcand] + flat = bcand * kmax + rank + + rows = torch.zeros(B * kmax, dtype=dtype, device=device) + cols = torch.zeros(B * kmax, dtype=dtype, device=device) + vals = torch.zeros(B * kmax, dtype=dtype, device=device) + valid = torch.zeros(B * kmax, dtype=torch.bool, device=device) + rows[flat] = rcand.to(dtype) + cols[flat] = ccand.to(dtype) + vals[flat] = inten + valid[flat] = True + rows, cols, valid = rows.view(B, kmax), cols.view(B, kmax), valid.view(B, kmax) + + keep = valid.clone() + if min_spacing > 0 and kmax > 1: + s2 = float(min_spacing) ** 2 + for k in range(kmax - 1): + active = keep[:, k] & valid[:, k] # (B,) brightest unsuppressed peak at rank k + dr = rows[:, k + 1 :] - rows[:, k : k + 1] + dc = cols[:, k + 1 :] - cols[:, k : k + 1] + suppress = ((dr * dr + dc * dc) < s2) & active[:, None] & valid[:, k + 1 :] + keep[:, k + 1 :] &= ~suppress + + if max_num_peaks is not None: + kept_rank = torch.cumsum(keep.to(torch.long), dim=1) - 1 + keep &= kept_rank < max_num_peaks + + final = (keep & valid).view(-1) + sel = torch.nonzero(final, as_tuple=True)[0] # row-major: pattern-major, rank-minor + peaks = torch.stack((rows.view(-1)[sel], cols.view(-1)[sel], vals[sel]), dim=1) + bidx = torch.div(sel, kmax, rounding_mode="floor") + counts = torch.bincount(bidx, minlength=B) + return peaks, bidx, counts + + +def _split_by_counts(peaks: torch.Tensor, counts: torch.Tensor) -> list[np.ndarray]: + """Split a pattern-grouped ``(T, 3)`` peak stack into one ``(M, 3)`` array per pattern. + + Parameters + ---------- + peaks : torch.Tensor + ``(T, 3)`` peaks ordered by ascending pattern index (contiguous per pattern). + counts : torch.Tensor + ``(B,)`` number of peaks belonging to each pattern, in order. + + Returns + ------- + list of np.ndarray + Length-``B`` list of ``(M, 3)`` arrays. + """ + out = [] + start = 0 + for cnt in counts.tolist(): + out.append(_to_numpy(peaks[start : start + cnt])) + start += cnt + return out + + +def _refine_parabolic(corr_map: torch.Tensor, peaks: torch.Tensor) -> torch.Tensor: + """3-point quadratic subpixel refinement of every peak (vectorized over peaks). + + Parameters + ---------- + corr_map : torch.Tensor + ``(H, W)`` correlation map the peaks were found in. + peaks : torch.Tensor + ``(M, 3)`` ``[row, col, intensity]`` peaks to refine. + + Returns + ------- + torch.Tensor + ``(M, 3)`` peaks with subpixel ``[row, col]`` and bilinearly interpolated + intensity. + """ + if peaks.shape[0] == 0: + return peaks + a = corr_map + H, W = a.shape + out = peaks.clone() + zero = torch.zeros(out.shape[0], device=a.device, dtype=a.dtype) + + r = out[:, 0].round().long().clamp(0, H - 1) + c = out[:, 1].round().long().clamp(0, W - 1) + + r_in = (r > 0) & (r < H - 1) + ix0, ix1, ix2 = a[(r - 1).clamp(0, H - 1), c], a[r, c], a[(r + 1).clamp(0, H - 1), c] + denom_r = 4.0 * ix1 - 2.0 * ix2 - 2.0 * ix0 + dr = torch.where(r_in & (denom_r != 0), (ix2 - ix0) / denom_r, zero) + + c_in = (c > 0) & (c < W - 1) + iy0, iy1, iy2 = a[r, (c - 1).clamp(0, W - 1)], a[r, c], a[r, (c + 1).clamp(0, W - 1)] + denom_c = 4.0 * iy1 - 2.0 * iy2 - 2.0 * iy0 + dc = torch.where(c_in & (denom_c != 0), (iy2 - iy0) / denom_c, zero) + + r_sub = r.to(a.dtype) + dr + c_sub = c.to(a.dtype) + dc + out[:, 0] = r_sub + out[:, 1] = c_sub + out[:, 2] = _bilinear(a, r_sub, c_sub) + return out + + +def _refine_parabolic_batched( + corr: torch.Tensor, peaks: torch.Tensor, bidx: torch.Tensor +) -> torch.Tensor: + """3-point quadratic refinement of peaks pooled across a batch of correlation maps. + + Batched form of :func:`_refine_parabolic`. Reading ``corr[bidx, r, c]`` gathers + exactly the values the single-pattern path would read, so each peak refines + identically. + + Parameters + ---------- + corr : torch.Tensor + ``(B, H, W)`` correlation maps. + peaks : torch.Tensor + ``(T, 3)`` ``[row, col, intensity]`` peaks pooled over all patterns. + bidx : torch.Tensor + ``(T,)`` batch index selecting each peak's correlation map. + + Returns + ------- + torch.Tensor + ``(T, 3)`` peaks with subpixel ``[row, col]`` and bilinearly interpolated + intensity. + """ + if peaks.shape[0] == 0: + return peaks + H, W = corr.shape[-2], corr.shape[-1] + dtype = corr.dtype + out = peaks.clone() + zero = torch.zeros(out.shape[0], device=corr.device, dtype=dtype) + + r = out[:, 0].round().long().clamp(0, H - 1) + c = out[:, 1].round().long().clamp(0, W - 1) + + r_in = (r > 0) & (r < H - 1) + ix0 = corr[bidx, (r - 1).clamp(0, H - 1), c] + ix1 = corr[bidx, r, c] + ix2 = corr[bidx, (r + 1).clamp(0, H - 1), c] + denom_r = 4.0 * ix1 - 2.0 * ix2 - 2.0 * ix0 + dr = torch.where(r_in & (denom_r != 0), (ix2 - ix0) / denom_r, zero) + + c_in = (c > 0) & (c < W - 1) + iy0 = corr[bidx, r, (c - 1).clamp(0, W - 1)] + iy1 = corr[bidx, r, c] + iy2 = corr[bidx, r, (c + 1).clamp(0, W - 1)] + denom_c = 4.0 * iy1 - 2.0 * iy2 - 2.0 * iy0 + dc = torch.where(c_in & (denom_c != 0), (iy2 - iy0) / denom_c, zero) + + r_sub = r.to(dtype) + dr + c_sub = c.to(dtype) + dc + out[:, 0] = r_sub + out[:, 1] = c_sub + out[:, 2] = _bilinear_batched(corr, bidx, r_sub, c_sub) + return out + + +def _refine_dft(m: torch.Tensor, peaks: torch.Tensor, upsample_factor: int) -> torch.Tensor: + """Guizar-Sicairos DFT upsampling refinement of every peak (vectorized over peaks). + + Each peak is rounded to half-pixel precision (matching py4DSTEM multicorr) before + upsampling, then all peaks are refined together with one batched DFT upsampling. + + Parameters + ---------- + m : torch.Tensor + ``(H, W)`` Fourier-domain correlation product from :func:`cross_correlation`. + peaks : torch.Tensor + ``(M, 3)`` ``[row, col, intensity]`` peaks to refine. + upsample_factor : int + DFT upsampling factor. + + Returns + ------- + torch.Tensor + ``(M, 3)`` peaks with DFT-refined ``[row, col]`` (intensity unchanged). + """ + if peaks.shape[0] == 0: + return peaks + out = peaks.clone() + xy = torch.round(out[:, :2] * 2.0) / 2.0 + refined = _upsampled_correlation_batch(m, int(upsample_factor), xy) + out[:, :2] = refined + return out + + +def _refine_dft_batched( + m: torch.Tensor, peaks: torch.Tensor, bidx: torch.Tensor, upsample_factor: int +) -> torch.Tensor: + """DFT-upsampling refinement of peaks pooled across a batch of correlation products. + + Batched form of :func:`_refine_dft`. Peaks are processed in sub-chunks so the + gathered per-peak product ``m[bidx]`` (``(chunk, H, W)`` complex) stays within a + fixed memory budget regardless of how many peaks were found. + + Parameters + ---------- + m : torch.Tensor + ``(B, H, W)`` Fourier-domain correlation products. + peaks : torch.Tensor + ``(T, 3)`` ``[row, col, intensity]`` peaks pooled over all patterns. + bidx : torch.Tensor + ``(T,)`` batch index selecting each peak's correlation product. + upsample_factor : int + DFT upsampling factor. + + Returns + ------- + torch.Tensor + ``(T, 3)`` peaks with DFT-refined ``[row, col]`` (intensity unchanged). + """ + if peaks.shape[0] == 0: + return peaks + out = peaks.clone() + M, N = m.shape[-2], m.shape[-1] + xy = torch.round(out[:, :2] * 2.0) / 2.0 + cap = max(1, 8_000_000 // (M * N)) + total = peaks.shape[0] + for start in range(0, total, cap): + stop = min(start + cap, total) + b = bidx[start:stop] + out[start:stop, :2] = _upsampled_correlation_batch( + m[b], int(upsample_factor), xy[start:stop] + ) + return out + + +def _upsampled_correlation_batch( + m: torch.Tensor, upsample_factor: int, xy: torch.Tensor +) -> torch.Tensor: + """Batched DFT upsampling of the correlation peak for many shifts at once. + + Vectorizes :func:`~quantem.core.utils.imaging_utils.upsampled_correlation_torch` + over the shifts ``xy`` with batched matmuls. Two exact speedups are applied: (1) ``m`` + is conjugate-symmetric (the diffraction pattern and template are real), so only the + non-negative half of the row frequencies is contracted — halving the dominant matmul — + with a rank-1 correction on the Nyquist column; (2) the per-peak DFT kernels are + factored into shared base kernels times per-peak phases, so ``torch.exp`` runs on far + fewer elements. Both are algebraically identical to the full-spectrum DFT upsample. + + Parameters + ---------- + m : torch.Tensor + ``(M, N)`` or ``(K, M, N)`` Fourier-domain correlation product(s). A single + ``(M, N)`` product broadcasts over all ``K`` shifts. + upsample_factor : int + DFT upsampling factor. + xy : torch.Tensor + ``(K, 2)`` ``[row, col]`` peak shifts at half-pixel precision. + + Returns + ------- + torch.Tensor + ``(K, 2)`` DFT-refined ``[row, col]`` positions. + """ + import math + + device = m.device + dtype = torch.get_default_dtype() + uf = float(upsample_factor) + M, N = m.shape[-2], m.shape[-1] + + xy = torch.round(xy * uf) / uf + global_shift = math.floor(math.ceil(uf * 1.5) / 2.0) + upsample_center = global_shift - uf * xy # (K, 2): [row, col] + + num = int(math.ceil(1.5 * uf)) + half = M // 2 + 1 + col_freq = (torch.fft.ifftshift(torch.arange(N, device=device)) - math.floor(N / 2)).to(dtype) + row_freq = (torch.fft.ifftshift(torch.arange(M, device=device)) - math.floor(M / 2)).to(dtype) + + # ``m = F(dp) * conj(F(template))`` is 2D conjugate-symmetric for real ``dp`` and + # template, so the upper-half row frequencies are redundant. Contract only the + # non-negative half (rows ``u = 0 .. M // 2``) and fold the conjugate upper rows + # back in with a weight of 2 — 1 for the self-paired DC row and, when ``M`` is even, + # the Nyquist row. This halves the dominant matmul over the row axis. + row_freq = row_freq[:half] + row_weight = torch.full((half,), 2.0, device=device, dtype=dtype) + row_weight[0] = 1.0 + if M % 2 == 0: + row_weight[-1] = 1.0 + + base = torch.arange(num, device=device, dtype=dtype) + factor_col = -2j * math.pi / (N * uf) + factor_row = -2j * math.pi / (M * uf) + + # Factor each kernel ``exp(f · freq · (base - center))`` into a peak-independent base + # kernel ``exp(f · freq · base)`` times a per-peak phase ``exp(-f · freq · center)``. + # The base kernels are tiny and built once; the phases fold into ``m`` and the + # intermediate product, so ``torch.exp`` runs on ~20x fewer elements. + base_col = torch.exp(factor_col * (col_freq[:, None] * base[None, :])) # (N, num) + base_row = torch.exp(factor_row * (base[:, None] * row_freq[None, :]))[None] # (1, num, half) + phase_col = torch.exp(-factor_col * (col_freq[None, :] * upsample_center[:, 1:2])) # (K, N) + phase_row = torch.exp(-factor_row * (upsample_center[:, 0:1] * row_freq[None, :])) # (K, half) + + mc_half = m.conj()[..., :half, :] # (K, half, N) — non-negative row frequencies only + # Fold the row phase and conjugate-symmetry weight into ``m`` so the shared base row + # kernel is reused across peaks; then contract rows, apply the column phase, and + # contract columns against the shared base column kernel. + prod = torch.matmul(base_row, mc_half * (row_weight * phase_row)[:, :, None]) # (K, num, N) + up = torch.matmul(prod * phase_col[:, None, :], base_col) # (K, num, num) + + if N % 2 == 0: + # Row-only folding is exact for every column except the Nyquist column ``v = N/2``, + # whose conjugate partner stays in the same column. Correct it with an exact rank-1 + # update ``pr_diff ⊗ col_kern[:, N/2]``, where ``pr_diff = -2i Im(S)`` and ``S`` sums + # only the kept interior rows. + nyq = N // 2 + interior = row_weight - 1.0 # 1 on interior rows, 0 on the DC / Nyquist rows + s = torch.matmul(base_row, (mc_half[..., nyq] * (interior * phase_row)).unsqueeze(-1)) + pr_diff = -2j * s[..., 0].imag # (K, num) + col_nyq = base_col[nyq, :][None, :] * phase_col[:, nyq : nyq + 1] # (K, num) + up = up + pr_diff[:, :, None] * col_nyq[:, None, :] + + image_up = up.real + + K = xy.shape[0] + kidx = torch.arange(K, device=device) + idx = torch.argmax(image_up.reshape(K, -1), dim=1) + sub_r = torch.div(idx, num, rounding_mode="floor") + sub_c = idx % num + + # 3-point parabolic refinement around the upsampled maximum (interior only) + interior = (sub_r > 0) & (sub_r < num - 1) & (sub_c > 0) & (sub_c < num - 1) + rr = sub_r.clamp(1, num - 2) + cc = sub_c.clamp(1, num - 2) + c11 = image_up[kidx, rr, cc] + c21, c01 = image_up[kidx, rr + 1, cc], image_up[kidx, rr - 1, cc] + c12, c10 = image_up[kidx, rr, cc + 1], image_up[kidx, rr, cc - 1] + zero = torch.zeros(K, device=device, dtype=dtype) + denom_x = 4.0 * c11 - 2.0 * c21 - 2.0 * c01 + denom_y = 4.0 * c11 - 2.0 * c12 - 2.0 * c10 + dx = torch.where(interior & (denom_x != 0), (c21 - c01) / denom_x, zero) + dy = torch.where(interior & (denom_y != 0), (c12 - c10) / denom_y, zero) + + sub = torch.stack([sub_r.to(dtype), sub_c.to(dtype)], dim=1) - global_shift + return xy + (sub + torch.stack([dx, dy], dim=1)) / uf + + +def _bilinear(a: torch.Tensor, r: torch.Tensor, c: torch.Tensor) -> torch.Tensor: + """Bilinear interpolation of ``a`` at fractional ``(r, c)`` (vectorized over peaks). + + Parameters + ---------- + a : torch.Tensor + ``(H, W)`` map to sample. + r : torch.Tensor + ``(K,)`` fractional row coordinates. + c : torch.Tensor + ``(K,)`` fractional column coordinates. + + Returns + ------- + torch.Tensor + ``(K,)`` interpolated values. + """ + H, W = a.shape + r0 = torch.floor(r).long() + c0 = torch.floor(c).long() + r1 = (r0 + 1).clamp(max=H - 1) + c1 = (c0 + 1).clamp(max=W - 1) + r0 = r0.clamp(0, H - 1) + c0 = c0.clamp(0, W - 1) + dr = r - r0.to(a.dtype) + dc = c - c0.to(a.dtype) + return ( + (1 - dr) * (1 - dc) * a[r0, c0] + + (1 - dr) * dc * a[r0, c1] + + dr * (1 - dc) * a[r1, c0] + + dr * dc * a[r1, c1] + ) + + +def _bilinear_batched( + corr: torch.Tensor, bidx: torch.Tensor, r: torch.Tensor, c: torch.Tensor +) -> torch.Tensor: + """Bilinear interpolation of a batch ``corr`` ``(B, H, W)`` at per-peak ``(r, c)``. + + Batched form of :func:`_bilinear`. The four corner samples are gathered as + ``corr[bidx, r0, c0]`` etc., matching the single-pattern interpolation + value-for-value. + + Parameters + ---------- + corr : torch.Tensor + ``(B, H, W)`` maps to sample. + bidx : torch.Tensor + ``(T,)`` batch index selecting each peak's map. + r : torch.Tensor + ``(T,)`` fractional row coordinates. + c : torch.Tensor + ``(T,)`` fractional column coordinates. + + Returns + ------- + torch.Tensor + ``(T,)`` interpolated values. + """ + H, W = corr.shape[-2], corr.shape[-1] + dtype = corr.dtype + r0 = torch.floor(r).long() + c0 = torch.floor(c).long() + r1 = (r0 + 1).clamp(max=H - 1) + c1 = (c0 + 1).clamp(max=W - 1) + r0 = r0.clamp(0, H - 1) + c0 = c0.clamp(0, W - 1) + dr = r - r0.to(dtype) + dc = c - c0.to(dtype) + return ( + (1 - dr) * (1 - dc) * corr[bidx, r0, c0] + + (1 - dr) * dc * corr[bidx, r0, c1] + + dr * (1 - dc) * corr[bidx, r1, c0] + + dr * dc * corr[bidx, r1, c1] + ) + + +def _to_numpy(peaks: torch.Tensor) -> np.ndarray: + """Convert a peaks tensor to a contiguous ``(M, 3)`` float64 numpy array. + + Parameters + ---------- + peaks : torch.Tensor + ``(M, 3)`` (or flat) peaks tensor on any device. + + Returns + ------- + np.ndarray + ``(M, 3)`` ``float64`` array of ``[q_row, q_col, intensity]`` rows. + """ + return peaks.detach().cpu().numpy().astype(np.float64).reshape(-1, 3) diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py new file mode 100644 index 000000000..76eb4cb2f --- /dev/null +++ b/src/quantem/diffraction/orientation.py @@ -0,0 +1,1435 @@ +"""Orientation mapping of crystalline 4D-STEM data. + +OrientationMap matches measured Bragg peaks (a quantem Vector) against a +library of simulated kinematical patterns from a Crystal, using sparse polar +correlation (Ophus et al., Microsc. Microanal. 28, 390 (2022)) implemented as +batched torch operations. + +The method: + +1. Sample zone axes over the symmetry-reduced fundamental wedge (or the + hemisphere), build a polar-coordinate reference library P(zone, shell, + gamma) where shells are the reciprocal-lattice radii of the crystal. +2. Convert measured peaks at each probe position into the same sparse polar + representation X(shell, gamma). +3. Correlate over in-plane angle gamma by FFT, over all zones at once, using + one batched matrix multiplication per gamma frequency. The mirror channel + (conjugate FFT) tests inversion-related orientations at no library cost. +4. Optionally refine the best zone axes on a finer local grid. + +Orientations are unit quaternions; see quantem.diffraction.rotations. +""" + +from __future__ import annotations + +import numpy as np +import torch +from tqdm import tqdm + +from quantem.core.datastructures.vector import Vector +from quantem.core.io.serialize import AutoSerialize +from quantem.core.utils.utils import electron_wavelength_angstrom +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.rotations import ( + misorientation_angle_deg, + qconj, + qmult, + qnormalize, + qrotate, + quat_from_axis_angle, + quat_from_zone_axis, + sample_zone_axes, +) + + +def fibonacci_hemisphere(n_points: int, dtype=torch.float64) -> torch.Tensor: + """Spherical Fibonacci sampling of the upper hemisphere, (N, 3).""" + i = torch.arange(n_points, dtype=dtype) + 0.5 + z = i / n_points # (0, 1): upper hemisphere + phi = i * (np.pi * (3 - np.sqrt(5))) + r = torch.sqrt(1 - z**2) + return torch.stack((r * torch.cos(phi), r * torch.sin(phi), z), dim=-1) + + +class OrientationMap(AutoSerialize): + """Match crystal orientations to Bragg peaks at every probe position. + + Workflow:: + + om = OrientationMap.from_vectors(peaks, crystal, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=2.0, angle_step_in_plane_deg=2.0) + om.match_orientations(num_matches=1) + om.plot_orientation() + + The object is both the engine and the result: after `match`, `quats` + holds (R, C, M, 4) orientation quaternions, `corr` the correlation + scores, and `mirror` the inversion flags. + """ + + _token = object() + + def __init__( + self, + peaks: Vector, + crystal: Crystal, + energy_ev: float, + _token: object | None = None, + ): + if _token is not self._token: + raise RuntimeError("Use OrientationMap.from_vectors() to construct.") + self.peaks = peaks + self.crystal = crystal + self.energy_ev = float(energy_ev) + self.wavelength = electron_wavelength_angstrom(energy_ev) + + # plan state + self.zone_axes: torch.Tensor | None = None + self.zone_quats: torch.Tensor | None = None + self.plan_fft: torch.Tensor | None = None + self.shell_radii: torch.Tensor | None = None + + # results + self.quats: torch.Tensor | None = None + self.corr: torch.Tensor | None = None + self.corr_second: torch.Tensor | None = None + self.reliability: torch.Tensor | None = None + self.mirror: torch.Tensor | None = None + + @classmethod + def from_vectors( + cls, + peaks: Vector, + crystal: Crystal, + energy_ev: float = 300e3, + ) -> "OrientationMap": + """Create from detected Bragg peaks. + + Parameters + ---------- + peaks : Vector + Ragged peak table over scan positions with fields including + ('qx', 'qy', 'intensity') in calibrated 1/Angstrom units. + crystal : Crystal + Candidate crystal with structure factors already calculated. + energy_ev : float, default=300e3 + Beam energy in eV. + """ + if crystal.g_vec is None: + raise RuntimeError("Run crystal.calculate_structure_factors() first.") + return cls(peaks, crystal, energy_ev, _token=cls._token) + + # ------------------------------------------------------------------ + # orientation plan + # ------------------------------------------------------------------ + + def build_plan( + self, + angle_step_zone_axis_deg: float = 2.0, + angle_step_in_plane_deg: float = 2.0, + corr_kernel_size: float = 0.05, + sigma_excitation: float = 0.04, + power_radial: float = 1.0, + power_intensity: float = 0.25, + tol_shell_distance: float = 0.01, + detector_q_max: float | tuple[float, float] | str | None = "auto", + device: str | torch.device = "cpu", + verbose: bool = True, + ) -> "OrientationMap": + """Build the polar correlation library over the fundamental wedge. + + Parameters + ---------- + angle_step_zone_axis_deg : float, default=2.0 + Angular step between sampled zone axes. + angle_step_in_plane_deg : float, default=2.0 + Angular step of the in-plane (gamma) axis; the number of gamma + samples is round(360 / step). + corr_kernel_size : float, default=0.05 + Correlation kernel size delta (1/Angstroms): azimuthal extent of + each reference peak and radial tolerance for shell assignment. + sigma_excitation : float, default=0.04 + Excitation error envelope of the library (1/Angstroms). Keep this + about 2x the physical excitation tolerance: orientations halfway + between sampled zones shift s_g by ~ (step/2) * k, and a wider + envelope keeps their library intensities from collapsing. + power_radial, power_intensity : float + Weighting prefactor q^power_radial * |V_g|^power_intensity for + library peaks. power_intensity=0 matches on positions only + (best for strongly dynamical data). + tol_shell_distance : float, default=0.01 + Reciprocal lattice radii closer than this merge into one shell. + detector_q_max : float | tuple | "auto" | None, default="auto" + Half-width of the square detector (1/Angstroms), scalar or + (row_max, col_max). Library reflections beyond the detector edge + cannot be measured, and which ones fall off depends on the + in-plane rotation; the correlation is normalized by the masked + template norm at every in-plane angle, so orientations with + strong reflections outside the detector are not penalized. + "auto" measures the detector footprint from the peaks themselves + (largest |q| along each detector axis, undoing any + detector-to-scan rotation recorded on the peaks). None disables + the correction. + device : str | torch.device, default="cpu" + Device for the library and the correlation compute. + verbose : bool, default=True + Print the symmetry actually used for matching (including any + pseudo-symmetry reduction) and the plan size. + """ + crystal = self.crystal + self.device = torch.device(device) + self.corr_kernel_size = float(corr_kernel_size) + self.sigma_excitation = float(sigma_excitation) + self.power_radial = float(power_radial) + self.power_intensity = float(power_intensity) + + # zone axis sampling over the matching (pseudo-symmetry-reduced) wedge + wedge = crystal.zone_axis_wedge() + if wedge is None: + n_zones = int(np.ceil(2 * np.pi / np.deg2rad(angle_step_zone_axis_deg) ** 2)) + za = fibonacci_hemisphere(n_zones) + else: + za, _ = sample_zone_axes(wedge, angle_step_zone_axis_deg) + self.zone_axes = za + self.zone_quats = quat_from_zone_axis(za) + self.zone_step_deg = float(angle_step_zone_axis_deg) + + # symmetry-complete neighbor sets for sub-grid zone refinement: a + # zone on the wedge boundary only has in-wedge grid neighbors on one + # side, and a one-sided correlation centroid would drag it inward; + # the symmetry images of the grid across the boundary restore the + # missing side. + from quantem.diffraction.rotations import quat_to_matrix + + Rs = quat_to_matrix(crystal.sym_quats_matching) + images = torch.einsum("sij,zj->szi", Rs, za) + images = torch.cat([images, -images], dim=0).reshape(-1, 3) # (S2*Z, 3) + img_zone = torch.arange(za.shape[0]).repeat( + 2 * crystal.sym_quats_matching.shape[0] + ) + # deduplicate coincident image positions (keep one per position/zone) + key = torch.cat( + [torch.round(images / 1e-6) * 1e-6, img_zone[:, None].to(images.dtype)], + dim=1, + ) + _, first = np.unique(key.numpy(), axis=0, return_index=True) + images = images[torch.as_tensor(np.sort(first))] + img_zone = img_zone[torch.as_tensor(np.sort(first))] + + cos_lim = np.cos(np.deg2rad(1.6 * self.zone_step_deg)) + nbr_idx_list, nbr_pos_list = [], [] + dots = images @ za.T # (M, Z) + for i in range(za.shape[0]): + sel = torch.nonzero(dots[:, i] > cos_lim).squeeze(1) + nbr_idx_list.append(img_zone[sel]) + nbr_pos_list.append(images[sel]) + K = max(len(v) for v in nbr_idx_list) + Z = za.shape[0] + self.zone_nbr_idx = torch.zeros((Z, K), dtype=torch.long) + self.zone_nbr_pos = torch.zeros((Z, K, 3), dtype=torch.float64) + self.zone_nbr_valid = torch.zeros((Z, K), dtype=torch.bool) + for i, (idx, pos) in enumerate(zip(nbr_idx_list, nbr_pos_list)): + k = len(idx) + self.zone_nbr_idx[i, :k] = idx + self.zone_nbr_pos[i, :k] = pos + self.zone_nbr_valid[i, :k] = True + + # radial shells from unique reciprocal lattice vector lengths + g_len = crystal.g_len + radii = torch.unique(torch.round(g_len / tol_shell_distance) * tol_shell_distance) + self.shell_radii = radii + self.num_gamma = int(round(360 / angle_step_in_plane_deg)) + self.gamma = torch.linspace( + 0, 2 * np.pi, self.num_gamma + 1, dtype=torch.float64 + )[:-1] + + plan = self._build_reference(self.zone_quats) + # store conj(fft) along gamma so matching is a single complex matmul + self.plan_fft = torch.conj(torch.fft.fft(plan, dim=-1)).to(self.device) + + # square-detector aperture correction: the masked template norm at + # every in-plane shift is the circular correlation of the squared + # plan with the polar detector mask. The mask lives in the DETECTOR + # frame: any detector-to-scan rotation recorded on the peaks rotates + # the square aperture in the calibrated (qx, qy) frame. + rot_deg = float(self.peaks.metadata.get("rotation_ccw_deg", 0.0) or 0.0) + if isinstance(detector_q_max, str) and detector_q_max == "auto": + flat = self.peaks.select_fields("qx", "qy", "intensity").flatten() + if flat.shape[0] == 0: + detector_q_max = None + else: + th_b = np.deg2rad(-rot_deg) + rb = np.array( + [[np.cos(th_b), -np.sin(th_b)], [np.sin(th_b), np.cos(th_b)]] + ) + det_rc = flat[:, :2] @ rb.T + detector_q_max = ( + float(np.abs(det_rc[:, 0]).max()) + self.corr_kernel_size, + float(np.abs(det_rc[:, 1]).max()) + self.corr_kernel_size, + ) + if detector_q_max is not None: + if np.isscalar(detector_q_max): + qx_max = qy_max = float(detector_q_max) + else: + qx_max, qy_max = (float(v) for v in detector_q_max) + r = self.shell_radii[:, None] + g = self.gamma[None, :] - np.deg2rad(rot_deg) + mask = ( + (torch.abs(r * torch.cos(g)) <= qx_max) + & (torch.abs(r * torch.sin(g)) <= qy_max) + ).to(torch.float64) + self.detector_mask = mask # (S, G) + plan_sq_fft = torch.conj(torch.fft.fft(plan**2, dim=-1)) + mask_fft = torch.fft.fft(mask, dim=-1) + # norm^2 per (zone, shift), direct and mirrored channels + n2 = torch.fft.ifft( + torch.einsum("zsg,sg->zg", plan_sq_fft, mask_fft), dim=-1 + ).real.clamp_min(0) + n2_m = torch.fft.ifft( + torch.einsum("zsg,sg->zg", plan_sq_fft, torch.conj(mask_fft)), dim=-1 + ).real.clamp_min(0) + # fraction of template weight on the detector; used to suppress + # zones that are mostly unmeasurable at a given rotation + full = (plan**2).sum(dim=(1, 2))[:, None].clamp_min(1e-12) + self.plan_norm_shift = torch.stack( + [torch.sqrt(n2), torch.sqrt(n2_m)] + ).to(self.device) # (2, Z, G) + self.plan_frac_shift = torch.stack([n2 / full, n2_m / full]).to(self.device) + else: + self.detector_mask = None + self.plan_norm_shift = None + self.plan_frac_shift = None + if verbose: + print(crystal.symmetry_summary()) + print( + " orientation plan %d zone axes x %d in-plane angles, " + "%d radial shells" + % ( + self.zone_axes.shape[0], + self.gamma.shape[0], + self.shell_radii.shape[0], + ) + ) + return self + + def _deposit_polar( + self, + qr: torch.Tensor, + qphi: torch.Tensor, + amp: torch.Tensor, + out: torch.Tensor, + ) -> torch.Tensor: + """Deposit peaks into a polar image with the shared correlation kernel. + + Every peak spreads as a Gaussian of width delta in both the radial + direction (across shells) and arc length (along gamma). The library + and the experimental patterns use this same kernel, so the normalized + correlation of a pattern with itself is exactly 1. + + Parameters + ---------- + qr, qphi, amp : torch.Tensor + Peak radii, azimuths, amplitudes, flat (K,). + out : torch.Tensor + (S, G) accumulator, modified in place. + """ + radii = self.shell_radii.to(qr.dtype) + delta = self.corr_kernel_size + G = self.num_gamma + + dr = qr[:, None] - radii[None, :] # (K, S) + k_idx, s_idx = torch.nonzero(dr.abs() < 3 * delta, as_tuple=True) + if k_idx.numel() == 0: + return out + w_r = torch.exp(-(dr[k_idx, s_idx] ** 2) / (2 * delta**2)) * amp[k_idx] + + gamma = self.gamma.to(qr.dtype) + dg = qphi[k_idx, None] - gamma[None, :] + dg = (dg + np.pi) % (2 * np.pi) - np.pi + arc = dg * qr[k_idx, None] + w = w_r[:, None] * torch.exp(-(arc**2) / (2 * delta**2)) + out.index_add_(0, s_idx, w) + return out + + def _build_reference(self, zone_quats: torch.Tensor) -> torch.Tensor: + """Polar reference library (Z, S, G) for the given zone-axis quats.""" + crystal = self.crystal + lam = self.wavelength + g = crystal.g_vec # (N, 3) + delta = self.corr_kernel_size + + gr = qrotate(zone_quats[:, None, :], g[None, :, :]) # (Z, N, 3) + gz = gr[..., 2] + g2 = (gr**2).sum(-1) + s_g = (2 * gz - lam * g2) / (2 - 2 * lam * gz) + amp = torch.exp(-(s_g**2) / (2 * self.sigma_excitation**2)) + amp = amp * (s_g.abs() < delta * 4) + + weight = ( + crystal.g_len**self.power_radial + * crystal.struct_factors_int**self.power_intensity + ) + vals = amp * weight[None, :] # (Z, N) + qr = torch.hypot(gr[..., 0], gr[..., 1]) + qphi = torch.atan2(gr[..., 1], gr[..., 0]) + + Z = zone_quats.shape[0] + plan = torch.zeros((Z, self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64) + for z in range(Z): + keep = vals[z] > 1e-8 + self._deposit_polar(qr[z, keep], qphi[z, keep], vals[z, keep], plan[z]) + + norm = torch.linalg.norm(plan.reshape(Z, -1), dim=1).clamp_min(1e-12) + return plan / norm[:, None, None] + + # ------------------------------------------------------------------ + # experimental polar images + # ------------------------------------------------------------------ + + def _polar_image( + self, qx: torch.Tensor, qy: torch.Tensor, intensity: torch.Tensor + ) -> torch.Tensor: + """Sparse polar image (S, G) of one measured pattern.""" + qr = torch.hypot(qx, qy) + qphi = torch.atan2(qy, qx) + amp = intensity.clamp_min(0) ** (self.power_intensity) * qr**self.power_radial + out = torch.zeros( + (self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64 + ) + return self._deposit_polar(qr, qphi, amp, out) + + # ------------------------------------------------------------------ + # matching + # ------------------------------------------------------------------ + + def match_orientations( + self, + num_matches: int = 1, + include_mirror: bool = True, + min_number_peaks: int = 3, + min_angle_between_matches_deg: float = 15.0, + subpixel_gamma: bool = True, + subpixel_zone: bool = True, + min_detector_fraction: float = 0.3, + batch_size: int = 128, + progress_bar: bool = True, + ) -> "OrientationMap": + """Match all probe positions against the orientation plan. + + Patterns are processed in batches: the polar images are stacked, and + the correlation over all zones and in-plane angles reduces to one + complex matrix product per gamma frequency plus a batched inverse FFT. + + Correlation scores are normalized to [0, 1]: the library slices are + unit vectors and the experimental polar image is divided by its own + norm, so `corr` is a cosine similarity comparable across patterns, + crystals, and datasets. After the best match, the highest correlation + among zone axes at least `min_angle_between_matches_deg` away is + stored in `corr_second`; `reliability = corr - corr_second` is the + primary confidence metric. + + Parameters + ---------- + num_matches : int, default=1 + Number of orientations to return per probe position; matches + after the first suppress zones within + `min_angle_between_matches_deg` of earlier matches. + include_mirror : bool, default=True + Also correlate against the in-plane mirrored pattern, testing + inversion-related (opposite hemisphere) zone axes at no library + cost. Exact in the flat-Ewald / Friedel limit. + min_number_peaks : int, default=3 + Skip positions with fewer detected peaks. + min_angle_between_matches_deg : float, default=15.0 + Exclusion radius (degrees, zone-axis distance) around earlier + matches, both for later matches and for the second-best score + used in `reliability`. + subpixel_gamma : bool, default=True + Parabolic sub-bin refinement of the in-plane angle. + subpixel_zone : bool, default=True + Sub-grid refinement of the zone axis: the correlation-weighted + centroid of the best zone and its grid neighbors. Removes the + zone-axis quantization of the plan (the in-plane angle is + already continuous through subpixel_gamma). + batch_size : int, default=128 + Number of patterns correlated at once. + """ + if self.plan_fft is None: + raise RuntimeError("Run build_plan() first.") + peaks = self.peaks + shape = peaks.shape + R, C = shape[0], shape[1] + M = num_matches + device = self.device + G = self.num_gamma + Z = self.zone_axes.shape[0] + + quats = torch.zeros((R, C, M, 4), dtype=torch.float64) + quats[..., 0] = 1.0 + corr_out = torch.zeros((R, C, M), dtype=torch.float64) + corr_second = torch.zeros((R, C), dtype=torch.float64) + mirror_out = torch.zeros((R, C, M), dtype=torch.bool) + + fields = peaks.fields + ix = [fields.index(f) for f in ("qx", "qy", "intensity")] + + # zone-pair angular distances, for the exclusion ball around matches + za = self.zone_axes.to(device) + zone_ang = torch.rad2deg(torch.acos((za @ za.T).clamp(-1, 1))) # (Z, Z) + + plan_fft = self.plan_fft # (Z, S, G) complex + valid_rc = [ + (rx, ry) + for rx, ry in np.ndindex(R, C) + if peaks[rx, ry].array.shape[0] >= min_number_peaks + ] + batches = [ + valid_rc[i : i + batch_size] for i in range(0, len(valid_rc), batch_size) + ] + if progress_bar: + batches = tqdm(batches, desc=f"matching {self.crystal.name}") + + gamma_grid = self.gamma + for batch in batches: + ims = [] + for rx, ry in batch: + data = peaks[rx, ry].array + qx = torch.as_tensor(data[:, ix[0]], dtype=torch.float64) + qy = torch.as_tensor(data[:, ix[1]], dtype=torch.float64) + ii = torch.as_tensor(data[:, ix[2]], dtype=torch.float64) + ims.append(self._polar_image(qx, qy, ii)) + im_stack = torch.stack(ims).to(device) + norms = torch.linalg.norm(im_stack.reshape(len(ims), -1), dim=1).clamp_min( + 1e-12 + ) + im_fft = torch.fft.fft(im_stack, dim=-1) # (B, S, G) + + # contract shells: (B, Z, G) per channel + cc = torch.einsum("zsg,bsg->bzg", plan_fft, im_fft) + channels = [cc] + if include_mirror: + channels.append(torch.einsum("zsg,bsg->bzg", plan_fft, torch.conj(im_fft))) + corr = torch.fft.ifft(torch.stack(channels, dim=1), dim=-1).real + # normalize: library slices are unit vectors, so dividing by the + # experimental norm makes corr a cosine similarity in [0, 1] + corr = corr / norms[:, None, None, None] + if self.plan_norm_shift is not None: + # square-detector correction: renormalize by the on-detector + # template norm at each in-plane shift, and suppress + # rotations where most of the template is unmeasurable + n_ch = corr.shape[1] + corr = corr / self.plan_norm_shift[None, :n_ch].clamp_min(1e-3) + corr = corr.masked_fill( + self.plan_frac_shift[None, :n_ch] < min_detector_fraction, 0.0 + ) + # corr: (B, ch, Z, G) + B = corr.shape[0] + + for m in range(M): + if m > 0: + # suppress zones near earlier matches, per pattern + for b in range(B): + for mm in range(m): + rx, ry = batch[b] + # zone index of previous match not stored; use angle + # to previous zone axis + zprev = self._zprev[b][mm] + corr[b, :, zone_ang[zprev] < min_angle_between_matches_deg, :] = -torch.inf + flat_idx = corr.reshape(B, -1).argmax(dim=1) + n_ch = corr.shape[1] + ch_i = flat_idx // (Z * G) + z_i = (flat_idx // G) % Z + g_i = flat_idx % G + c_val = corr.reshape(B, -1).gather(1, flat_idx[:, None]).squeeze(1) + + gamma = gamma_grid[g_i.cpu()].clone() + if subpixel_gamma: + b_ar = torch.arange(B, device=corr.device) + c1 = c_val + c0 = corr[b_ar, ch_i, z_i, (g_i - 1) % G] + c2 = corr[b_ar, ch_i, z_i, (g_i + 1) % G] + denom = 4 * c1 - 2 * c0 - 2 * c2 + dg = torch.where( + denom.abs() > 1e-12, + (c2 - c0) / denom, + torch.zeros_like(denom), + ) * (2 * np.pi / G) + gamma = gamma + dg.double().cpu() + + is_mirror = ch_i.cpu() == 1 + q_zone = self.zone_quats[z_i.cpu()] + + if subpixel_zone: + # sub-grid zone axis: correlation-weighted centroid over + # the symmetry-complete neighborhood of the best zone + # (see build_plan; images across the wedge boundary keep + # the centroid unbiased for boundary zones) + b_ar = torch.arange(B, device=corr.device) + corr_z = corr[b_ar, ch_i].amax(dim=-1).cpu() # (B, Z) + zi_cpu = z_i.cpu() + n_idx = self.zone_nbr_idx[zi_cpu] # (B, K) + n_pos = self.zone_nbr_pos[zi_cpu] # (B, K, 3) + n_ok = self.zone_nbr_valid[zi_cpu] # (B, K) + c_n = corr_z.gather(1, n_idx) # (B, K) + c_floor = corr_z.gather(1, zi_cpu[:, None]) * 0.7 + wgt = (c_n - c_floor).clamp_min(0) * n_ok + za_ref = (wgt[:, :, None] * n_pos).sum(dim=1) + za_ref = za_ref / torch.linalg.norm( + za_ref, dim=-1, keepdim=True + ).clamp_min(1e-12) + za_old = self.zone_axes[z_i.cpu()] + axis = torch.cross(za_ref, za_old, dim=-1) + sin_t = torch.linalg.norm(axis, dim=-1) + ang_t = torch.atan2(sin_t, (za_ref * za_old).sum(-1)) + ok_t = sin_t > 1e-12 + dq = torch.zeros((B, 4), dtype=torch.float64) + dq[:, 0] = 1.0 + if bool(ok_t.any()): + dq[ok_t] = quat_from_axis_angle( + axis[ok_t] / sin_t[ok_t, None], ang_t[ok_t] + ) + # rotate za_ref -> za_old in the crystal frame: R' = R S + q_zone = qmult(q_zone, dq) + + q_flip = torch.tensor([0.0, 1.0, 0.0, 0.0], dtype=torch.float64) + q_zone = torch.where( + is_mirror[:, None], qmult(q_flip, q_zone), q_zone + ) + gamma = torch.where(is_mirror, -gamma - np.pi, gamma) + half = gamma / 2 + zeros = torch.zeros_like(half) + q_spin = torch.stack( + (torch.cos(half), zeros, zeros, torch.sin(half)), dim=-1 + ) + q = qmult(q_spin, q_zone) + + for b, (rx, ry) in enumerate(batch): + if torch.isfinite(c_val[b]): + quats[rx, ry, m] = q[b] + corr_out[rx, ry, m] = c_val[b].double().cpu() + mirror_out[rx, ry, m] = bool(is_mirror[b]) + + if m == 0: + # second-best score outside the exclusion ball around the + # best zone axis -> reliability = corr - corr_second + far = zone_ang[z_i] >= min_angle_between_matches_deg # (B, Z) + c2 = ( + corr.masked_fill(~far[:, None, :, None], -torch.inf) + .reshape(B, -1) + .amax(dim=1) + ) + for b, (rx, ry) in enumerate(batch): + if torch.isfinite(c2[b]): + corr_second[rx, ry] = c2[b].double().cpu() + + if M > 1: + if m == 0: + self._zprev = [[] for _ in range(B)] + for b in range(B): + self._zprev[b].append(int(z_i[b])) + + self.quats = quats + self.corr = corr_out + self.corr_second = corr_second + self.reliability = corr_out[..., 0] - corr_second + self.mirror = mirror_out + return self + + # ------------------------------------------------------------------ + # sub-grid refinement + # ------------------------------------------------------------------ + + def refine_orientations( + self, + num_iterations: int = 5, + pair_distance: float | None = None, + sigma_excitation: float | None = None, + min_pairs: int = 3, + refine_tilt: bool = False, + refine_zone: bool = True, + zone_search_deg: float = 1.5, + sigma_envelope: float | None = None, + zone_max_total_deg: float | None = None, + batched: bool = True, + neighbor_rescue: bool = True, + rescue_threshold_deg: float = 2.0, + progress_bar: bool = True, + ) -> "OrientationMap": + """Refine matched orientations by least squares on paired peak positions. + + For each probe position and match, the simulated pattern is paired to + the measured peaks (nearest neighbor within `pair_distance`), and the + small rotation minimizing the weighted in-plane residuals is solved in + closed form and applied; repeated for `num_iterations` rounds with + re-pairing. This removes the in-plane quantization of the orientation + plan (typically to well below 0.1 degrees). + + By default only the in-plane rotation is refined. Zero-layer peak + positions carry almost no information about out-of-plane tilt (the + tilt terms in the position residual are proportional to g_z, which is + near zero for excited reflections), so fitting the full rotation from + positions is ill-conditioned and amplifies detection noise into + spurious tilts. Tilt is constrained by the diffracted *intensities* + (which reflections are excited) and belongs to the dynamical + refinement pass. + + Parameters + ---------- + num_iterations : int, default=5 + Pairing + rotation solve rounds. + pair_distance : float | None + Maximum pairing distance (1/Angstroms); defaults to the plan's + corr_kernel_size. + sigma_excitation : float | None + Excitation error envelope used for simulation; defaults to the + plan's value. + min_pairs : int, default=3 + Skip positions with fewer paired peaks. + refine_tilt : bool, default=False + Also solve the two tilt components from peak positions. Only + meaningful for noise-free simulated data. + refine_zone : bool, default=True + Refine the zone-axis tilt from the intensity envelope (the Laue + circle): the tilt that concentrates the measured intensity on + the Ewald sphere, searched over +/- zone_search_deg with + parabolic sub-stepping. Removes the zone-axis quantization of + the orientation plan. + zone_search_deg : float, default=1.5 + Half-range of the envelope tilt search, in degrees. + zone_max_total_deg : float | None + Trust region: cap on the cumulative envelope tilt applied to + each orientation, relative to its matched start. The coarse + match is grid-accurate to about half the zone-axis step, so tilt + corrections beyond that scale are noise walking the orientation + out of its basin. Defaults to 0.375 * the plan's zone step. + sigma_envelope : float | None + Excitation-error width of the envelope objective; defaults to + half the plan's sigma_excitation (the plan value is widened for + grid robustness). + neighbor_rescue : bool, default=True + Second pass over positions whose best match disagrees with every + neighbor by more than rescue_threshold_deg: re-refine from each + distinct neighbor orientation and keep the highest-scoring + result (score = total paired measured intensity). Repairs + isolated wrong local optima such as near-degenerate variants. + rescue_threshold_deg : float, default=2.0 + Minimum-neighbor misorientation that triggers the rescue pass. + """ + assert self.quats is not None + delta = pair_distance if pair_distance is not None else self.corr_kernel_size + sigma = ( + sigma_excitation if sigma_excitation is not None else self.sigma_excitation + ) + peaks = self.peaks + R, C, M = self.quats.shape[:3] + fields = peaks.fields + ix = [fields.index(f) for f in ("qx", "qy", "intensity")] + g_all = self.crystal.g_vec + lam = self.wavelength + sigma_env = sigma_envelope if sigma_envelope is not None else sigma / 2 + f_all = self.crystal.struct_factors_int.to(torch.float64) + tg = torch.deg2rad( + torch.linspace(-zone_search_deg, zone_search_deg, 17, dtype=torch.float64) + ) + eye3 = torch.eye(3, dtype=torch.float64) + tilt_cap = np.deg2rad( + zone_max_total_deg + if zone_max_total_deg is not None + else 0.375 * self.zone_step_deg + ) + + def refine_single(q, q_exp, w_exp): + """Refine one orientation; return (q, pairing score).""" + score = 0.0 + tilt_total = torch.zeros(2, dtype=torch.float64) + for _ in range(num_iterations): + g = qrotate(q, g_all) + gz, g2 = g[:, 2], (g**2).sum(dim=1) + s_g = (2 * gz - lam * g2) / (2 - 2 * lam * gz) + sel = torch.abs(s_g) < 2 * sigma + g_sel = g[sel] + if g_sel.shape[0] == 0: + return q, score + d = torch.cdist(g_sel[:, :2], q_exp) + d_min, j_min = d.min(dim=1) + pair = d_min < delta + if int(pair.sum()) < min_pairs: + return q, score + gp = g_sel[pair] + tgt = q_exp[j_min[pair]] + w = w_exp[j_min[pair]] * (1 - d_min[pair] / delta) + score = float(w.sum()) + # solve min sum w | tgt - (g + omega x g)_xy |^2 for omega + r = tgt - gp[:, :2] # (P, 2) + if refine_tilt: + A = torch.zeros((gp.shape[0], 2, 3), dtype=torch.float64) + A[:, 0, 1] = gp[:, 2] + A[:, 0, 2] = -gp[:, 1] + A[:, 1, 0] = -gp[:, 2] + A[:, 1, 2] = gp[:, 0] + Aw = A * w[:, None, None] + AtA = torch.einsum("pki,pkj->ij", Aw, A) + Atr = torch.einsum("pki,pk->i", Aw, r) + omega = torch.linalg.solve(AtA + 1e-12 * eye3, Atr) + else: + # in-plane only: residual model r = omega_z * (-g_y, g_x) + a = torch.stack((-gp[:, 1], gp[:, 0]), dim=1) # (P, 2) + num = (w[:, None] * a * r).sum() + den = (w[:, None] * a * a).sum().clamp_min(1e-12) + omega = torch.tensor( + [0.0, 0.0, float(num / den)], dtype=torch.float64 + ) + angle = torch.linalg.norm(omega) + if angle > 1e-10: + dq = quat_from_axis_angle(omega / angle, angle) + q = qmult(dq, q) + + if refine_zone: + # continuous zone-axis tilt from the intensity envelope: + # a small lab-frame tilt (wx, wy) shifts every excitation + # error by s(w) = s0 + wx*gy - wy*gx; maximize the + # normalized cosine between the measured intensities and + # the predicted |F|^2 * envelope over a grid with + # parabolic sub-stepping (the Laue-circle fit -- peak + # positions carry no tilt information, the excitation + # pattern does) + s0 = s_g[sel][pair] + a1 = gp[:, 1] + a2 = -gp[:, 0] + f_p = f_all[sel][pair] + S = ( + s0[:, None, None] + + tg[None, :, None] * a1[:, None, None] + + tg[None, None, :] * a2[:, None, None] + ) + pred = f_p[:, None, None] * torch.exp( + -(S**2) / (2 * sigma_env**2) + ) + E = (w[:, None, None] * pred).sum(dim=0) / ( + (pred**2).sum(dim=0).sqrt().clamp_min(1e-12) + ) + ij = int(E.argmax()) + i0, j0 = ij // 17, ij % 17 + wx, wy = float(tg[i0]), float(tg[j0]) + step = float(tg[1] - tg[0]) + if 0 < i0 < 16: + c0, c1, c2 = ( + float(E[i0 - 1, j0]), + float(E[i0, j0]), + float(E[i0 + 1, j0]), + ) + den = 2 * c1 - c0 - c2 + if abs(den) > 1e-12: + wx += 0.5 * (c2 - c0) / den * step + if 0 < j0 < 16: + c0, c1, c2 = ( + float(E[i0, j0 - 1]), + float(E[i0, j0]), + float(E[i0, j0 + 1]), + ) + den = 2 * c1 - c0 - c2 + if abs(den) > 1e-12: + wy += 0.5 * (c2 - c0) / den * step + # trust region on the cumulative tilt from the start + prop = tilt_total + torch.tensor( + [wx, wy], dtype=torch.float64 + ) + over = float(torch.linalg.norm(prop)) - tilt_cap + if over > 0: + prop = prop * tilt_cap / float(torch.linalg.norm(prop)) + step = prop - tilt_total + tilt_total = prop + tilt = torch.tensor( + [float(step[0]), float(step[1]), 0.0], + dtype=torch.float64, + ) + t_ang = torch.linalg.norm(tilt) + if t_ang > 1e-10: + dq = quat_from_axis_angle(tilt / t_ang, t_ang) + q = qmult(dq, q) + return q, score + + def get_exp(rx, ry): + data = peaks[rx, ry].array + if data.shape[0] < min_pairs: + return None, None + q_exp = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) + w_exp = torch.as_tensor(data[:, ix[2]], dtype=torch.float64).clamp_min(0) + w_exp = w_exp / w_exp.max().clamp_min(1e-12) + return q_exp, w_exp + + scores = torch.zeros((R, C), dtype=torch.float64) + if batched and not refine_tilt: + self._refine_batched( + scores, + delta=delta, + sigma=sigma, + sigma_env=sigma_env, + tg=tg, + tilt_cap=tilt_cap, + num_iterations=num_iterations, + min_pairs=min_pairs, + refine_zone=refine_zone, + progress_bar=progress_bar, + ) + else: + iterator = list(np.ndindex(R, C)) + if progress_bar: + iterator = tqdm(iterator, desc="refining orientations") + for rx, ry in iterator: + q_exp, w_exp = get_exp(rx, ry) + if q_exp is None: + continue + for m in range(M): + if self.corr[rx, ry, m] <= 0: + continue + q, sc = refine_single(self.quats[rx, ry, m], q_exp, w_exp) + self.quats[rx, ry, m] = q + if m == 0: + scores[rx, ry] = sc + + if neighbor_rescue: + # a wrong local optimum (e.g. a near-degenerate variant) shows as + # a discontinuity: retry those positions from each distinct + # neighbor orientation and keep the best-scoring result + q0 = self.quats[..., 0, :] + miso_min = torch.full((R, C), torch.inf, dtype=torch.float64) + for dr, dc in ((0, 1), (1, 0)): + a = q0[: R - dr, : C - dc] + b = q0[dr:, dc:] + mm = misorientation_angle_deg( + a.reshape(-1, 4), b.reshape(-1, 4), self.crystal.sym_quats + ).reshape(R - dr, C - dc) + miso_min[: R - dr, : C - dc] = torch.minimum( + miso_min[: R - dr, : C - dc], mm + ) + miso_min[dr:, dc:] = torch.minimum(miso_min[dr:, dc:], mm) + retry = torch.nonzero(miso_min > rescue_threshold_deg) + it2 = retry.tolist() + if progress_bar and len(it2): + it2 = tqdm(it2, desc="neighbor rescue") + n_rescued = 0 + for rx, ry in it2: + q_exp, w_exp = get_exp(rx, ry) + if q_exp is None: + continue + best_q = self.quats[rx, ry, 0] + best_s = float(scores[rx, ry]) + cands = [] + for dr in (-1, 0, 1): + for dc in (-1, 0, 1): + nr, nc = rx + dr, ry + dc + if (dr == 0 and dc == 0) or not ( + 0 <= nr < R and 0 <= nc < C + ): + continue + qn = self.quats[nr, nc, 0] + if all( + float( + misorientation_angle_deg( + qn, c, self.crystal.sym_quats + ) + ) + > 0.5 + for c in cands + ): + cands.append(qn) + for qc in cands: + q, sc = refine_single(qc.clone(), q_exp, w_exp) + if sc > best_s * 1.02: + best_q, best_s = q, sc + if best_s > float(scores[rx, ry]): + n_rescued += 1 + self.quats[rx, ry, 0] = best_q + scores[rx, ry] = best_s + return self + + # ------------------------------------------------------------------ + # forward simulation of a match + # ------------------------------------------------------------------ + + def _refine_batched( + self, + scores: torch.Tensor, + delta: float, + sigma: float, + sigma_env: float, + tg: torch.Tensor, + tilt_cap: float, + num_iterations: int, + min_pairs: int, + refine_zone: bool, + progress_bar: bool, + chunk: int = 64, + ) -> None: + """Chunk-vectorized in-plane + envelope refinement (all positions).""" + from quantem.diffraction.rotations import quat_to_matrix + + peaks = self.peaks + R, C, M = self.quats.shape[:3] + fields = peaks.fields + ix = [fields.index(f) for f in ("qx", "qy", "intensity")] + g_all = self.crystal.g_vec # (G, 3) + f_all = self.crystal.struct_factors_int.to(torch.float64) + lam = self.wavelength + n_tg = tg.shape[0] + + # flatten measured peaks once, padded per position + cells = [peaks[r, c].array for r, c in np.ndindex(R, C)] + counts = np.array([c.shape[0] for c in cells]) + Pmax = max(1, counts.max()) + N = R * C + q_exp = torch.full((N, Pmax, 2), 1e6, dtype=torch.float64) + w_exp = torch.zeros((N, Pmax), dtype=torch.float64) + for i, arr in enumerate(cells): + n = arr.shape[0] + if n == 0: + continue + q_exp[i, :n] = torch.as_tensor(arr[:, ix[:2]], dtype=torch.float64) + wi = torch.as_tensor(arr[:, ix[2]], dtype=torch.float64).clamp_min(0) + w_exp[i, :n] = wi / wi.max().clamp_min(1e-12) + + quats = self.quats.reshape(N, M, 4) + corr = self.corr.reshape(N, M) + valid_pos = torch.as_tensor(counts >= min_pairs) + + chunks = range(0, N, chunk) + if progress_bar: + chunks = tqdm(chunks, desc="refining orientations (batched)") + for i0 in chunks: + i1 = min(i0 + chunk, N) + B = i1 - i0 + qe = q_exp[i0:i1] # (B, P, 2) + we = w_exp[i0:i1] # (B, P) + for m in range(M): + act = valid_pos[i0:i1] & (corr[i0:i1, m] > 0) + if not bool(act.any()): + continue + q = quats[i0:i1, m].clone() # (B, 4) + tilt_total = torch.zeros((B, 2), dtype=torch.float64) + sc = torch.zeros(B, dtype=torch.float64) + for _ in range(num_iterations): + Rm = quat_to_matrix(q) # (B, 3, 3) + g = torch.einsum("bij,gj->bgi", Rm, g_all) # (B, G, 3) + gz, g2 = g[..., 2], (g**2).sum(dim=-1) + s_g = (2 * gz - lam * g2) / (2 - 2 * lam * gz) + sel = torch.abs(s_g) < 2 * sigma # (B, G) + d = torch.cdist(g[..., :2], qe) # (B, G, P) + d_min, j_min = d.min(dim=-1) # (B, G) + pair = sel & (d_min < delta) + w_g = torch.gather(we, 1, j_min) * (1 - d_min / delta).clamp_min(0) + w_g = w_g * pair # (B, G) + n_pair = pair.sum(dim=1) + ok = act & (n_pair >= min_pairs) + if not bool(ok.any()): + break + sc = torch.where(ok, w_g.sum(dim=1), sc) + tgt = torch.gather( + qe, 1, j_min[..., None].expand(-1, -1, 2) + ) # (B, G, 2) + r_vec = tgt - g[..., :2] + # in-plane closed form + a_vec = torch.stack((-g[..., 1], g[..., 0]), dim=-1) + num = (w_g[..., None] * a_vec * r_vec).sum(dim=(1, 2)) + den = (w_g[..., None] * a_vec * a_vec).sum(dim=(1, 2)) + wz = torch.where(ok, num / den.clamp_min(1e-12), torch.zeros_like(num)) + half = wz / 2 + dq = torch.stack( + ( + torch.cos(half), + torch.zeros_like(half), + torch.zeros_like(half), + torch.sin(half), + ), + dim=-1, + ) + q = torch.where(ok[:, None], qmult(dq, q), q) + + if refine_zone: + # sparse over paired reflections only + idx_b, idx_g = torch.nonzero(pair, as_tuple=True) + s0f = s_g[idx_b, idx_g] + gyf = g[idx_b, idx_g, 1] + gxf = g[idx_b, idx_g, 0] + ff = f_all[idx_g] + wf = w_g[idx_b, idx_g] + S = ( + s0f[:, None, None] + + tg[None, :, None] * gyf[:, None, None] + - tg[None, None, :] * gxf[:, None, None] + ) # (Np, T, T) + pred = ff[:, None, None] * torch.exp( + -(S**2) / (2 * sigma_env**2) + ) + E_num = torch.zeros( + (B, n_tg, n_tg), dtype=torch.float64 + ).index_add_(0, idx_b, wf[:, None, None] * pred) + E_den = torch.zeros( + (B, n_tg, n_tg), dtype=torch.float64 + ).index_add_(0, idx_b, pred**2) + E = E_num / E_den.sqrt().clamp_min(1e-12) # (B, T, T) + flat_ij = E.reshape(B, -1).argmax(dim=1) + i_b, j_b = flat_ij // n_tg, flat_ij % n_tg + step = float(tg[1] - tg[0]) + wx = tg[i_b].clone() + wy = tg[j_b].clone() + # parabolic sub-stepping where interior + b_ar = torch.arange(B) + for axis, idx, wv in ((0, i_b, wx), (1, j_b, wy)): + interior = (idx > 0) & (idx < n_tg - 1) + if not bool(interior.any()): + continue + if axis == 0: + c0 = E[b_ar, (idx - 1).clamp(0), j_b] + c1 = E[b_ar, idx, j_b] + c2 = E[b_ar, (idx + 1).clamp(max=n_tg - 1), j_b] + else: + c0 = E[b_ar, i_b, (idx - 1).clamp(0)] + c1 = E[b_ar, i_b, idx] + c2 = E[b_ar, i_b, (idx + 1).clamp(max=n_tg - 1)] + den2 = 2 * c1 - c0 - c2 + shift = torch.where( + interior & (den2.abs() > 1e-12), + 0.5 * (c2 - c0) / den2 * step, + torch.zeros_like(c1), + ) + wv += shift + prop = tilt_total + torch.stack((wx, wy), dim=-1) + norm = torch.linalg.norm(prop, dim=-1) + scale_f = torch.where( + norm > tilt_cap, tilt_cap / norm.clamp_min(1e-12), + torch.ones_like(norm), + ) + prop = prop * scale_f[:, None] + step_t = torch.where( + ok[:, None], prop - tilt_total, torch.zeros_like(prop) + ) + tilt_total = torch.where(ok[:, None], prop, tilt_total) + t_ang = torch.linalg.norm(step_t, dim=-1) + axis_v = torch.zeros((B, 3), dtype=torch.float64) + nz = t_ang > 1e-10 + if bool(nz.any()): + axis_v[nz, 0] = step_t[nz, 0] / t_ang[nz] + axis_v[nz, 1] = step_t[nz, 1] / t_ang[nz] + dq_t = quat_from_axis_angle(axis_v[nz], t_ang[nz]) + q_nz = q[nz] + q[nz] = qmult(dq_t, q_nz) + quats[i0:i1, m] = torch.where(act[:, None], q, quats[i0:i1, m]) + if m == 0: + scores.reshape(-1)[i0:i1] = torch.where( + act, sc, scores.reshape(-1)[i0:i1] + ) + self.quats = quats.reshape(R, C, M, 4) + + def generate_pattern(self, rx: int, ry: int, match: int = 0, **kwargs): + """Simulated pattern for the matched orientation at (rx, ry).""" + assert self.quats is not None + return self.crystal.generate_pattern( + self.quats[rx, ry, match], + energy_ev=self.energy_ev, + sigma_excitation=self.sigma_excitation, + **kwargs, + ) + + def match_residual( + self, + other: "OrientationMap", + delete_radius: float = 0.04, + min_number_peaks: int = 3, + min_corr_other: float = 0.0, + progress_bar: bool = True, + ) -> "OrientationMap": + """Re-match this crystal on the peaks another crystal cannot explain. + + For overlapping patterns (e.g. a thin lath on a matrix), the direct + match of the minority phase is poisoned by the majority phase's + peaks. Here the majority candidate's simulated pattern is used to + delete its measured peaks at each position, and this crystal is + re-matched against the residual peaks only. Where the residual match + beats this map's stored second match, it replaces it (match index 1), + so the joint phase fit sees one clean candidate per phase. + + Parameters + ---------- + other : OrientationMap + The matched map of the (locally dominant) other crystal. + delete_radius : float, default=0.04 + Measured peaks within this distance (1/Angstroms) of one of the + other crystal's simulated peaks are removed. + min_corr_other : float, default=0.0 + Skip positions where the other crystal's correlation is below + this (nothing trustworthy to delete). + """ + assert self.quats is not None and other.quats is not None + peaks = self.peaks + R, C = peaks.shape[0], peaks.shape[1] + fields = peaks.fields + ix = [fields.index(f) for f in ("qx", "qy", "intensity")] + + residual = Vector.from_shape( + (R, C), fields=["qx", "qy", "intensity"], + units=["A^-1", "A^-1", "counts"], name="residual_peaks", + ) + cells = [] + for rx, ry in np.ndindex(R, C): + data = peaks[rx, ry].array + if data.shape[0] < min_number_peaks or other.corr[rx, ry, 0] <= min_corr_other: + cells.append(np.zeros((0, 3))) + continue + sim = other.generate_pattern(rx, ry) + sq = torch.stack((sim["qx"], sim["qy"]), dim=1) + if sq.shape[0] == 0: + cells.append(data[:, ix]) + continue + qxy = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) + d_min = torch.cdist(qxy, sq).min(dim=1).values + keep = (d_min > delete_radius).numpy() + cells.append(data[keep][:, ix]) + nested = [cells[r * C : (r + 1) * C] for r in range(R)] + residual = Vector.from_data( + nested, fields=["qx", "qy", "intensity"], + units=["A^-1", "A^-1", "counts"], name="residual_peaks", + ) + + om_res = OrientationMap.from_vectors(residual, self.crystal, self.energy_ev) + for attr in ( + "device", "corr_kernel_size", "sigma_excitation", "power_radial", + "power_intensity", "zone_axes", "zone_quats", "zone_step_deg", + "zone_nbr_idx", "zone_nbr_pos", "zone_nbr_valid", + "plan_fft", "shell_radii", "num_gamma", "gamma", "detector_mask", + "plan_norm_shift", "plan_frac_shift", + ): + setattr(om_res, attr, getattr(self, attr)) + om_res.match_orientations( + num_matches=1, + min_number_peaks=min_number_peaks, + progress_bar=progress_bar, + ) + om_res.refine_orientations(progress_bar=progress_bar) + + # replace the stored second match where the residual match is better + if self.quats.shape[2] < 2: + pad_q = torch.zeros((R, C, 1, 4), dtype=torch.float64) + pad_q[..., 0] = 1.0 + self.quats = torch.cat([self.quats, pad_q], dim=2) + self.corr = torch.cat( + [self.corr, torch.zeros((R, C, 1), dtype=torch.float64)], dim=2 + ) + self.mirror = torch.cat( + [self.mirror, torch.zeros((R, C, 1), dtype=torch.bool)], dim=2 + ) + better = om_res.corr[..., 0] > self.corr[..., 1] + self.quats[..., 1, :] = torch.where( + better[..., None], om_res.quats[..., 0, :], self.quats[..., 1, :] + ) + self.corr[..., 1] = torch.where(better, om_res.corr[..., 0], self.corr[..., 1]) + self.mirror[..., 1] = torch.where( + better, om_res.mirror[..., 0], self.mirror[..., 1] + ) + return self + + def cluster_orientations( + self, + mask: np.ndarray | None = None, + threshold_deg: float = 5.0, + min_cluster_size: int = 10, + match: int = 0, + ) -> dict: + """Greedy clustering of the matched orientations into variants. + + Positions are visited in order of decreasing correlation; each seed + collects every unassigned position within `threshold_deg` + (symmetry-reduced misorientation) into a cluster. Follows the variant + analysis of MacLaren et al., J. Microscopy 295, 131 (2024). + + Parameters + ---------- + mask : np.ndarray | None + Boolean or weight mask of positions to include (e.g. phase mask). + threshold_deg : float, default=5.0 + Misorientation radius of a cluster. + min_cluster_size : int, default=10 + Smaller clusters are discarded (labels stay -1). + + Returns + ------- + dict + 'labels' (R, C) int tensor, -1 = unassigned; 'mean_quats' + (K, 4) cluster mean orientations; 'sizes' (K,) member counts. + """ + assert self.quats is not None + R, C = self.quats.shape[:2] + q = self.quats[..., match, :].reshape(-1, 4) + corr = self.corr[..., match].reshape(-1) + ok = corr > 0 + if mask is not None: + ok &= torch.as_tensor(np.asarray(mask, dtype=float).reshape(-1)) > 0.5 + + labels = torch.full((R * C,), -1, dtype=torch.long) + sym = self.crystal.sym_quats + unassigned = ok.clone() + means, sizes = [], [] + k = 0 + while unassigned.any(): + seed = int(torch.where(unassigned, corr, torch.full_like(corr, -1)).argmax()) + miso = misorientation_angle_deg(q[seed][None], q, sym) + members = unassigned & (miso < threshold_deg) + unassigned &= ~members + if int(members.sum()) < min_cluster_size: + continue + labels[members] = k + # symmetry-align members to the seed, then average + qm = q[members] + dq = qmult(qconj(q[seed])[None], qm) + dq_sym = qmult(dq[:, None, :], sym) + best = dq_sym[..., 0].abs().argmax(dim=1) + dq_best = dq_sym[torch.arange(qm.shape[0]), best] + sign = torch.where(dq_best[:, :1] < 0, -1.0, 1.0) + q_aligned = qmult(q[seed][None], dq_best * sign) + means.append(qnormalize(q_aligned.mean(dim=0))) + sizes.append(int(members.sum())) + k += 1 + # order clusters by size, largest first + if means: + order = torch.argsort(torch.tensor(sizes), descending=True) + relabel = torch.full((len(sizes),), -1, dtype=torch.long) + relabel[order] = torch.arange(len(sizes)) + labels = torch.where(labels >= 0, relabel[labels.clamp_min(0)], labels) + means = [means[int(i)] for i in order] + sizes = [sizes[int(i)] for i in order] + return { + "labels": labels.reshape(R, C), + "mean_quats": torch.stack(means) if means else torch.zeros((0, 4)), + "sizes": torch.tensor(sizes), + } + + def calculate_strain( + self, + match: int = 0, + pair_distance: float | None = None, + min_pairs: int = 5, + mask: np.ndarray | None = None, + ds_sampling: float | None = None, + ds_units: str | None = None, + progress_bar: bool = True, + ): + """Per-position strain from measured vs simulated peak positions. + + At each probe position the refined orientation's simulated pattern is + paired to the measured peaks and the in-plane deformation A + minimizing sum w |A q_sim - q_meas|^2 is solved in closed form. The + strain is referenced to the crystal's ideal lattice, so unlike + lattice-vector strain mapping it is absolute, not relative to a + reference region. + + Returns + ------- + StrainMap + The columns of A enter as per-position reciprocal lattice + vectors with the identity as the fixed reference, so all + StrainMap machinery applies: `plot_strain(rotation_angle=...)` + for user-chosen u/v directions, `rotate_strain`, masking, and + scale bars. `num_pairs` (R, C) is attached as an attribute. + """ + from quantem.diffraction.strain import StrainMap + + assert self.quats is not None + delta = pair_distance if pair_distance is not None else self.corr_kernel_size + peaks = self.peaks + R, C = peaks.shape[0], peaks.shape[1] + fields = peaks.fields + ix = [fields.index(f) for f in ("qx", "qy", "intensity")] + + A_map = np.full((R, C, 2, 2), np.nan) + num_pairs = np.zeros((R, C), dtype=int) + + iterator = list(np.ndindex(R, C)) + if progress_bar: + iterator = tqdm(iterator, desc="strain mapping") + for rx, ry in iterator: + if mask is not None and not mask[rx, ry]: + continue + if self.corr[rx, ry, match] <= 0: + continue + data = peaks[rx, ry].array + if data.shape[0] < min_pairs: + continue + q_exp = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) + w_exp = torch.as_tensor(data[:, ix[2]], dtype=torch.float64).clamp_min(0) + sim = self.generate_pattern(rx, ry, match=match) + sq = torch.stack((sim["qx"], sim["qy"]), dim=1) + if sq.shape[0] == 0: + continue + d = torch.cdist(sq, q_exp) + d_min, j_min = d.min(dim=1) + pair = d_min < delta + n = int(pair.sum()) + if n < min_pairs: + continue + qs = sq[pair] + qm = q_exp[j_min[pair]] + w = w_exp[j_min[pair]] * (1 - d_min[pair] / delta) + # A = (sum w qm qs^T) (sum w qs qs^T)^-1 + M1 = torch.einsum("p,pi,pj->ij", w, qm, qs) + M2 = torch.einsum("p,pi,pj->ij", w, qs, qs) + A = M1 @ torch.linalg.inv(M2 + 1e-12 * torch.eye(2, dtype=torch.float64)) + A_map[rx, ry] = A.numpy() + num_pairs[rx, ry] = n + + # columns of A are the measured images of the reciprocal unit basis; + # StrainMap's reciprocal-space branch (U_ref @ inv(U) = F^T) then + # yields the real-space strain with the shared sign conventions + sm = StrainMap( + u_array=A_map[..., :, 0], + v_array=A_map[..., :, 1], + ds_shape=(R, C), + real_space=False, + u_ref=np.array([1.0, 0.0]), + v_ref=np.array([0.0, 1.0]), + mask=None if mask is None else np.asarray(mask, dtype=float), + ds_sampling=ds_sampling, + ds_units=ds_units, + ) + sm.num_pairs = num_pairs + return sm + + def in_plane_angle_deg( + self, match: int = 0, mod_deg: float | None = None + ) -> torch.Tensor: + """In-plane angle of the crystal a-axis at every position (degrees). + + The angle of the projected crystal [100] Cartesian axis, measured + from the scan column axis toward the row axis. `mod_deg` wraps the + angle by the crystal's in-plane symmetry (60 for hexagonal basal, 90 + for cubic <100> zones); None returns the full range. + """ + from quantem.diffraction.rotations import quat_to_matrix + + assert self.quats is not None + R = quat_to_matrix(self.quats[..., match, :]) + a_lab = R[..., :, 0] # crystal x-axis in the lab frame + ang = torch.rad2deg(torch.atan2(a_lab[..., 0], a_lab[..., 1])) + if mod_deg is not None: + ang = ang % mod_deg + return ang + + def plot_orientation(self, direction: str = "z", match: int = 0, **kwargs): + """IPF-colored orientation map; see orientation_visualization.""" + from quantem.diffraction.orientation_visualization import plot_orientation_map + + return plot_orientation_map(self, direction=direction, match=match, **kwargs) + + def plot_pole_figure(self, pole=(0, 0, 1), match: int = 0, **kwargs): + """Stereographic pole figure; see orientation_visualization.""" + from quantem.diffraction.orientation_visualization import plot_pole_figure + + return plot_pole_figure(self, pole=pole, match=match, **kwargs) + + def misorientation_map(self, reference: torch.Tensor | None = None) -> torch.Tensor: + """Misorientation angle (deg) of match 0 to a reference orientation.""" + assert self.quats is not None + q = self.quats[..., 0, :] + if reference is None: + reference = torch.tensor([1.0, 0, 0, 0], dtype=torch.float64) + return misorientation_angle_deg( + reference, q, self.crystal.sym_quats_matching + ) diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py new file mode 100644 index 000000000..f45af5189 --- /dev/null +++ b/src/quantem/diffraction/orientation_visualization.py @@ -0,0 +1,832 @@ +"""Visualization of orientation maps: IPF maps, pattern overlays, pole figures.""" + +from __future__ import annotations + +import numpy as np +import torch + +from quantem.core.visualization.visualization_utils import add_scalebar_to_ax +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.rotations import quat_to_matrix + +# one color per candidate phase, used consistently across every plot +DEFAULT_PHASE_COLORS = np.array( + [ + [1.00, 0.80, 0.25], # gold + [0.25, 0.80, 0.90], # cyan + [0.45, 0.80, 0.50], # green + [0.85, 0.50, 0.80], # purple + ] +) +ORIGIN_COLOR = "#2ca02c" +MEASURED_COLOR = "0.15" +IPF_GAMMA = 0.4 # <1 expands the white / mixed-color regions of the wedge +# additive corner colors: full red, green capped to avoid the fluorescent +# look, blue lifted off pure dark blue; pairwise sums give near-max-chroma +# yellow / cyan / violet and the three together give white +IPF_CORNER_COLORS = np.array( + [ + [1.00, 0.00, 0.00], + [0.00, 0.70, 0.00], + [0.00, 0.30, 1.00], + ] +) + + +def _bary_to_rgb(w: np.ndarray) -> np.ndarray: + """Barycentric wedge weights (..., 3) to RGB via the additive anchors.""" + w = np.clip(w, 0, None) + w = w / np.clip(w.max(axis=-1, keepdims=True), 1e-12, None) + w = w**IPF_GAMMA + return np.clip(w @ IPF_CORNER_COLORS, 0, 1) + + +def _parse_direction(direction) -> torch.Tensor: + """Lab direction for IPF coloring: 'z' (beam), 'r' (scan row), 'c' (scan + col), an in-plane angle in degrees (measured from the column axis toward + the row axis), or an explicit [row, col] / [row, col, z] vector.""" + if isinstance(direction, str): + return { + "z": torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64), + "r": torch.tensor([1.0, 0.0, 0.0], dtype=torch.float64), + "c": torch.tensor([0.0, 1.0, 0.0], dtype=torch.float64), + # back-compat aliases + "x": torch.tensor([1.0, 0.0, 0.0], dtype=torch.float64), + "y": torch.tensor([0.0, 1.0, 0.0], dtype=torch.float64), + }[direction] + if isinstance(direction, (int, float)): + th = np.deg2rad(float(direction)) + return torch.tensor([np.sin(th), np.cos(th), 0.0], dtype=torch.float64) + v = torch.as_tensor(direction, dtype=torch.float64).reshape(-1) + if v.numel() == 2: + v = torch.cat([v, torch.zeros(1, dtype=torch.float64)]) + return v / torch.linalg.norm(v) + + +def _reduce_to_wedge(vectors: torch.Tensor, crystal: Crystal) -> torch.Tensor: + """Map crystal-frame directions into the fundamental zone-axis wedge. + + Applies all proper symmetry rotations to +/- v and returns, per input + vector, the orbit member inside the wedge (all barycentric coordinates + with respect to the wedge corners non-negative). + """ + corners = crystal.zone_axis_wedge() + if corners is None: + # hemisphere fallback: canonicalize to upper hemisphere only + v = vectors.clone() + v[v[..., 2] < 0] *= -1 + return v + Rs = quat_to_matrix(crystal.sym_quats) # (S, 3, 3) + v = vectors.reshape(-1, 3) + orbit = torch.cat( + [torch.einsum("sij,nj->nsi", Rs, v), torch.einsum("sij,nj->nsi", Rs, -v)], + dim=1, + ) # (N, 2S, 3) + A_inv = torch.linalg.inv(corners.to(vectors.dtype).T) + w = torch.einsum("ij,nsj->nsi", A_inv, orbit) + inside = (w > -1e-6).all(dim=-1) + idx = inside.to(torch.float64).argmax(dim=1) + out = orbit[torch.arange(v.shape[0]), idx] + return out.reshape(vectors.shape) + + +def ipf_color( + orientations: torch.Tensor, + crystal: Crystal, + direction: str | torch.Tensor = "z", +) -> np.ndarray: + """Inverse pole figure RGB colors for orientations. + + Parameters + ---------- + orientations : torch.Tensor + Quaternions (..., 4). + crystal : Crystal + Provides symmetry and the fundamental wedge. + direction : {"x", "y", "z"} | torch.Tensor, default="z" + Lab direction whose crystal-frame coordinates are colored; "z" is the + beam direction (zone-axis map). + + Returns + ------- + np.ndarray + RGB array (..., 3) in [0, 1]. + """ + direction = _parse_direction(direction) + R = quat_to_matrix(orientations) # v_lab = R v_crystal + v_crystal = torch.einsum("...ji,j->...i", R, direction) + v = _reduce_to_wedge(v_crystal, crystal) + + corners = crystal.zone_axis_wedge() + if corners is None: + # hemisphere: hue from azimuth, saturation from polar angle + from matplotlib.colors import hsv_to_rgb + + az = (torch.atan2(v[..., 1], v[..., 0]) / (2 * np.pi)) % 1.0 + pol = torch.acos(v[..., 2].clamp(-1, 1)) / (np.pi / 2) + hsv = torch.stack((az, pol.clamp(0, 1), torch.ones_like(az)), dim=-1) + return hsv_to_rgb(hsv.numpy()) + + A_inv = torch.linalg.inv(corners.to(v.dtype).T) + w = torch.einsum("ij,...j->...i", A_inv, v) + return _bary_to_rgb(w.numpy()) + + +def wedge_legend( + crystal: Crystal, + ax, + n: int = 120, + labels: bool = True, + orientation: str = "horizontal", + fontsize: int = 11, +) -> None: + """Draw the labeled IPF color triangle for the crystal's fundamental wedge. + + Corner direction labels use 4-index Miller-Bravais symbols for hexagonal + and trigonal crystals. orientation="vertical" rotates the wedge 90 + degrees to fill a tall side panel. + """ + corners = crystal.zone_axis_wedge() + if corners is None: + ax.axis("off") + return + c = corners.numpy() + # vertical: rotate so the [001]/[0001] corner sits at the TOP of the + # tall panel with the wedge hanging straight down (the rotation aligns + # the wedge's angular bisector with the downward direction) + if orientation == "vertical": + az = [ + np.arctan2(c[k, 1] / (1 + c[k, 2]), c[k, 0] / (1 + c[k, 2])) + for k in (1, 2) + ] + th = -np.pi / 2 - (az[0] + az[1]) / 2 + else: + th = 0.0 + rot = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]]) + cxy = np.stack( + [c[:, 0] / (1 + c[:, 2]), c[:, 1] / (1 + c[:, 2])], axis=1 + ) @ rot.T + cx, cy = cxy[:, 0], cxy[:, 1] + + # rasterize the wedge interior: invert the stereographic projection on a + # pixel grid and alpha-mask outside the wedge, so no color spills past + # the outline + m = 8 + x0, x1 = cx.min() - 0.02, cx.max() + 0.02 + y0, y1 = cy.min() - 0.02, cy.max() + 0.02 + X, Y = np.meshgrid( + np.linspace(x0, x1, n * m), np.linspace(y0, y1, n * m), indexing="xy" + ) + Xu = np.cos(th) * X + np.sin(th) * Y + Yu = -np.sin(th) * X + np.cos(th) * Y + denom = 1 + Xu**2 + Yu**2 + V = np.stack( + [2 * Xu / denom, 2 * Yu / denom, (1 - Xu**2 - Yu**2) / denom], axis=-1 + ) + A_inv = np.linalg.inv(c.T) + W = V @ A_inv.T + inside = (W > -1e-9).all(axis=-1) + rgba = np.zeros(X.shape + (4,)) + rgba[..., :3] = _bary_to_rgb(W) + rgba[..., 3] = inside + ax.imshow(rgba, extent=(x0, x1, y0, y1), origin="lower", interpolation="nearest") + # black outline along the wedge edges (stereographic great-circle arcs) + tt = np.linspace(0, 1, 60)[:, None] + for i0, i1 in ((0, 1), (1, 2), (2, 0)): + e = c[i0][None, :] * (1 - tt) + c[i1][None, :] * tt + e = e / np.linalg.norm(e, axis=1, keepdims=True) + exy = np.stack( + [e[:, 0] / (1 + e[:, 2]), e[:, 1] / (1 + e[:, 2])], axis=1 + ) @ rot.T + ax.plot(exy[:, 0], exy[:, 1], color="k", lw=1.2) + if labels: + names = crystal.zone_axis_wedge_labels() or ["", "", ""] + center = np.array([cx.mean(), cy.mean()]) + for xi, yi, name in zip(cx, cy, names): + out = np.array([xi, yi]) - center + norm = np.linalg.norm(out) + off = out / norm * 0.08 if norm > 1e-6 else np.array([0, -0.08]) + ha = "left" if off[0] > 0.02 else ("right" if off[0] < -0.02 else "center") + va = "bottom" if off[1] > 0.02 else ("top" if off[1] < -0.02 else "center") + ax.text(xi + off[0], yi + off[1], name, fontsize=fontsize, ha=ha, va=va) + span_x = cx.max() - cx.min() + span_y = cy.max() - cy.min() + pad = 0.45 * max(span_x, span_y, 0.2) + ax.set_xlim(cx.min() - pad, cx.max() + pad) + ax.set_ylim(cy.min() - pad, cy.max() + pad) + ax.set_aspect("equal") + ax.axis("off") + + +def plot_orientation_map( + om, + direction: str = "z", + match: int = 0, + mask: np.ndarray | None = None, + scalebar: dict | None = None, + figax=None, + legend: bool = True, + axsize: tuple[float, float] = (9.0, 4.5), +): + """IPF-colored orientation map with the wedge legend in an adjacent panel. + + Parameters + ---------- + om : OrientationMap + Matched orientation map. + direction : {"x", "y", "z"}, default="z" + Lab direction to color ("z" = zone axis). + match : int, default=0 + Which match index to plot. + mask : np.ndarray | None + Multiplied into the RGB image (e.g. a phase or reliability mask). + scalebar : dict | None + Real-space scale bar, e.g. {"sampling": 30, "units": "A"}. + figax : (fig, (ax_map, ax_legend)) | (fig, ax_map) | None + Existing axes; with a single axis the legend is skipped. + """ + import matplotlib.pyplot as plt + + assert om.quats is not None + rgb = ipf_color(om.quats[..., match, :], om.crystal, direction) + if mask is not None: + rgb = rgb * np.asarray(mask, dtype=float)[..., None] + + ax_leg = None + if figax is None: + if legend: + fig, (ax, ax_leg) = plt.subplots( + 1, + 2, + figsize=(axsize[0] * 1.3, axsize[1]), + gridspec_kw={"width_ratios": [4, 1]}, + ) + else: + fig, ax = plt.subplots(figsize=axsize) + else: + fig, axs = figax + if isinstance(axs, (tuple, list, np.ndarray)) and len(axs) == 2: + ax, ax_leg = axs + else: + ax = axs + ax.imshow(rgb, interpolation="nearest") + ax.set_xticks([]) + ax.set_yticks([]) + if isinstance(direction, str) and direction == "z": + ax.set_title(f"{om.crystal.name} out-of-plane orientation") + else: + # arrow for the colored in-plane direction lives in the title, + # like the strain-map axis annotations + if isinstance(direction, str): + arrow = { + "r": r"$\downarrow$", + "c": r"$\rightarrow$", + "x": r"$\downarrow$", + "y": r"$\rightarrow$", + }.get(direction, "") + label = {"x": "r", "y": "c"}.get(direction, direction) + ax.set_title( + f"{om.crystal.name} in-plane orientation {label} {arrow}" + ) + elif isinstance(direction, (int, float)): + ax.set_title( + f"{om.crystal.name} in-plane orientation ({direction:g}\u00b0)" + ) + else: + ax.set_title(f"{om.crystal.name} in-plane orientation") + if scalebar is not None: + add_scalebar_to_ax( + ax, + array_size=rgb.shape[1], + sampling=scalebar.get("sampling", 1.0), + length_units=scalebar.get("length", None), + units=scalebar.get("units", "pixels"), + width_px=rgb.shape[0] / 40, + pad_px=rgb.shape[0] / 80, + color=scalebar.get("color", "white"), + loc="lower right", + ) + if legend and ax_leg is not None: + wedge_legend(om.crystal, ax_leg, orientation="vertical") + return fig, ax + + +def plot_pattern_matches( + orientation_maps, + positions, + dataset=None, + pixel_size: float | None = None, + origins: np.ndarray | None = None, + matches=(0, 1), + colors=None, + power: float = 0.4, + q_max_plot: float | None = None, + scalebar: bool = True, + show_measured: bool = True, + marker_scale: float = 250.0, + axsize: tuple[float, float] = (3.1, 3.1), +): + """Candidate matches side by side, py4DSTEM style. + + One row per probe position; one column per (crystal, match) candidate, + so alpha and beta fits sit next to each other for direct comparison. + Measured peaks are solid gray disks with area proportional to intensity; + each candidate's simulation is drawn as colored crosses (red, then blue + by default) also sized by intensity. With `dataset` given, the raw + pattern is shown behind the crosses instead of the gray disks. + + Parameters + ---------- + orientation_maps : OrientationMap | list[OrientationMap] + Matched orientation maps sharing the same peaks. + positions : list[tuple[int, int]] + (row, col) probe positions to plot. + dataset : Dataset4dstem | None + If given, the diffraction pattern is shown behind the overlay and + the gray measured disks are omitted. + pixel_size : float | None + Reciprocal pixel size (1/Angstroms per pixel); required with dataset. + origins : np.ndarray | None + (scan_r, scan_c, 2) fitted origins from measure_origins(); aligns + the background pattern with the origin-corrected peaks. + matches : tuple[int, ...], default=(0, 1) + Match indices per crystal. + colors : list | None + One color per crystal; defaults to red, blue, green, purple. + """ + import matplotlib.pyplot as plt + + oms = ( + list(orientation_maps) + if isinstance(orientation_maps, (list, tuple)) + else [orientation_maps] + ) + if colors is None: + colors = ["#d62728", "#1f77b4", "#2ca02c", "#9467bd"] + peaks = oms[0].peaks + fields = peaks.fields + ix = [fields.index(f) for f in ("qx", "qy", "intensity")] + # peaks may be rotated into the scan frame; the raw detector image is + # not, so rotate all overlay coordinates back to the detector frame + rot_deg = float(peaks.metadata.get("rotation_ccw_deg", 0.0) or 0.0) + th = np.deg2rad(-rot_deg) + rot_back = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]]) + + panels = [(i, m) for i in range(len(oms)) for m in matches] + n_r, n_c = len(positions), len(panels) + fig, axs = plt.subplots( + n_r, + n_c, + figsize=(axsize[0] * n_c, axsize[1] * n_r + 0.2), + squeeze=False, + ) + ordinal = ["1st", "2nd", "3rd"] + [f"{k + 1}th" for k in range(3, 9)] + for pi, (rx, ry) in enumerate(positions): + data = peaks[rx, ry].array.copy() + rc = data[:, [ix[0], ix[1]]] @ rot_back.T + data[:, ix[0]] = rc[:, 0] + data[:, ix[1]] = rc[:, 1] + w_meas = data[:, ix[2]].clip(min=0) + w_meas = w_meas / max(w_meas.max(), 1e-12) + if q_max_plot is None: + if dataset is not None and pixel_size is not None: + q_lim = dataset.shape[-1] / 2 * pixel_size + else: + qr = np.hypot(data[:, ix[0]], data[:, ix[1]]) + q_lim = 1.1 * qr.max() if qr.size else 1.0 + else: + q_lim = q_max_plot + for ci, (i_om, m) in enumerate(panels): + om = oms[i_om] + ax = axs[pi, ci] + if dataset is not None and pixel_size is not None: + H, W = dataset.shape[-2], dataset.shape[-1] + if origins is not None: + o_r, o_c = origins[rx, ry] + else: + o_r, o_c = H / 2, W / 2 + # pixel j has center (j - origin) * pixel_size; array edges + # sit half a pixel beyond the first/last centers + ax.imshow( + np.asarray(dataset.array[rx, ry]) ** power, + cmap="gray_r", + extent=( + (-0.5 - o_c) * pixel_size, + (W - 0.5 - o_c) * pixel_size, + (H - 0.5 - o_r) * pixel_size, + (-0.5 - o_r) * pixel_size, + ), + ) + elif show_measured: + ax.scatter( + data[:, ix[1]], + data[:, ix[0]], + s=marker_scale * w_meas, + color="0.75", + lw=0, + ) + sim = om.generate_pattern(rx, ry, match=m) + I = sim["intensity"].numpy() + sim_rc = ( + np.stack([sim["qx"].numpy(), sim["qy"].numpy()], axis=1) + @ rot_back.T + ) + if I.size: + ax.scatter( + sim_rc[:, 1], + sim_rc[:, 0], + s=marker_scale * I / I.max(), + marker="+", + color=colors[i_om % len(colors)], + lw=1.8, + ) + ax.set_xlim(-q_lim, q_lim) + ax.set_ylim(q_lim, -q_lim) + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_aspect("equal") + ax.set_title( + "%s %s\ncorr = %.2f" + % (om.crystal.name, ordinal[m], float(om.corr[rx, ry, m])), + fontsize=9, + ) + if scalebar and pi == n_r - 1 and ci == 0: + add_scalebar_to_ax( + ax, + array_size=2 * q_lim, + sampling=1.0, + length_units=0.5, + units="A^-1", + width_px=q_lim / 45, + pad_px=q_lim / 60, + color="black", + loc="lower right", + fontsize=9, + ) + fig.tight_layout() + return fig, axs + +def plot_cluster_map( + om, + clusters: dict, + colors: np.ndarray | None = None, + scalebar: dict | None = None, + figax=None, +): + """Map of orientation clusters (variants), one color per cluster.""" + import matplotlib.pyplot as plt + + labels = clusters["labels"].numpy() + if colors is None: + colors = CLUSTER_COLORS + K = int(labels.max()) + 1 + rgb = np.zeros(labels.shape + (3,)) + for k in range(K): + rgb[labels == k] = colors[k % len(colors)] + + if figax is None: + fig, ax = plt.subplots(figsize=(9, 4.5)) + else: + fig, ax = figax + ax.imshow(rgb, interpolation="nearest") + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_title(f"{om.crystal.name} orientation clusters") + handles = [ + plt.Line2D([0], [0], marker="s", ls="", color=colors[k % len(colors)], + label=f"{k + 1} ({int(clusters['sizes'][k])} px)") + for k in range(K) + ] + ax.legend(handles=handles, loc="center left", bbox_to_anchor=(1.01, 0.5), fontsize=8) + if scalebar is not None: + add_scalebar_to_ax( + ax, + array_size=rgb.shape[1], + sampling=scalebar.get("sampling", 1.0), + length_units=scalebar.get("length", None), + units=scalebar.get("units", "pixels"), + width_px=rgb.shape[0] / 40, + pad_px=rgb.shape[0] / 80, + color=scalebar.get("color", "white"), + loc="lower right", + ) + return fig, ax + + +def _pole_points( + quats: torch.Tensor, crystal: Crystal, pole, mask=None +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Stereographic (x, y, weight) of all symmetry-equivalent poles.""" + q = quats.reshape(-1, 4) + p = torch.as_tensor(pole, dtype=torch.float64) + p = p / torch.linalg.norm(p) + Rs = quat_to_matrix(crystal.sym_quats) + fam = torch.einsum("sij,j->si", Rs, p) + fam = torch.unique(torch.round(fam / 1e-6) * 1e-6, dim=0) + fam = torch.cat([fam, -fam]) + n_fam = fam.shape[0] + R = quat_to_matrix(q) + poles_lab = torch.einsum("nij,sj->nsi", R, fam) + if mask is not None: + w = torch.as_tensor(np.asarray(mask, dtype=float)).reshape(-1) + else: + w = torch.ones(q.shape[0], dtype=torch.float64) + w_all = w[:, None].expand(-1, n_fam).reshape(-1) + src_all = ( + torch.arange(q.shape[0])[:, None].expand(-1, n_fam).reshape(-1) + ) + v = poles_lab.reshape(-1, 3) + keep = (v[:, 2] > -1e-8) & (w_all > 0) + v, w_keep, src = v[keep], w_all[keep], src_all[keep] + x = (v[:, 0] / (1 + v[:, 2])).numpy() + y = (v[:, 1] / (1 + v[:, 2])).numpy() + return x, y, w_keep.numpy(), src.numpy() + + +def _pole_scatter_xy(quats: torch.Tensor, crystal: Crystal, pole) -> np.ndarray: + """Stereographic (x, y) of all symmetry-equivalent poles for orientations.""" + p = torch.as_tensor(pole, dtype=torch.float64) + p = p / torch.linalg.norm(p) + Rs = quat_to_matrix(crystal.sym_quats) + fam = torch.einsum("sij,j->si", Rs, p) + fam = torch.unique(torch.round(fam / 1e-6) * 1e-6, dim=0) + fam = torch.cat([fam, -fam]) + R = quat_to_matrix(torch.atleast_2d(quats)) + v = torch.einsum("nij,sj->nsi", R, fam).reshape(-1, 3) + v = v[v[:, 2] > -1e-8] + x = (v[:, 0] / (1 + v[:, 2])).numpy() + y = (v[:, 1] / (1 + v[:, 2])).numpy() + return np.stack((x, y), axis=1) + + +def plot_cluster_pole_figure( + om, + clusters: dict, + pole, + pole_label: str = "", + overlay: dict | None = None, + colors: np.ndarray | None = None, + figax=None, +): + """Pole figure of the cluster mean orientations, one color per cluster. + + Parameters + ---------- + om : OrientationMap + Provides the crystal symmetry of the clustered phase. + clusters : dict + Output of OrientationMap.cluster_orientations(). + pole : array-like + Crystal-Cartesian pole direction of the plotted family. + overlay : dict | None + Second pole family drawn as open markers, e.g. + {"quats": q_beta_mean, "crystal": ti_beta, "pole": (1, 1, 0), + "label": "<110> beta"} -- the standard Burgers relationship check. + """ + import matplotlib.pyplot as plt + + if colors is None: + colors = CLUSTER_COLORS + if figax is None: + fig, ax = plt.subplots(figsize=(6.5, 6.5)) + else: + fig, ax = figax + + theta = np.linspace(0, 2 * np.pi, 361) + for pol_deg in range(15, 91, 15): + r = np.tan(np.deg2rad(pol_deg) / 2) + lw = 1.0 if pol_deg == 90 else 0.4 + ax.plot(r * np.cos(theta), r * np.sin(theta), color="0.75", lw=lw) + for az in range(0, 180, 15): + ca, sa = np.cos(np.deg2rad(az)), np.sin(np.deg2rad(az)) + ax.plot([-ca, ca], [-sa, sa], color="0.85", lw=0.4) + + K = clusters["mean_quats"].shape[0] + for k in range(K): + xy = _pole_scatter_xy(clusters["mean_quats"][k], om.crystal, pole) + ax.scatter( + xy[:, 0], xy[:, 1], s=60, marker="h", + color=colors[k % len(colors)], edgecolors="k", lw=0.4, + label=f"{pole_label} {k + 1}", + ) + if overlay is not None: + xy = _pole_scatter_xy( + overlay["quats"], overlay["crystal"], overlay["pole"] + ) + ax.scatter( + xy[:, 0], xy[:, 1], s=70, marker="D", facecolors="none", + edgecolors="k", lw=1.0, label=overlay.get("label", "overlay"), + ) + ax.set_xlim(-1.15, 1.15) + ax.set_ylim(-1.15, 1.15) + ax.set_aspect("equal") + ax.set_xticks([]) + ax.set_yticks([]) + ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5), fontsize=8) + ax.set_title(f"{om.crystal.name} cluster pole figure") + return fig, ax + + +def plot_pole_figure( + om, + pole: list[float] | torch.Tensor = (0, 0, 1), + match: int = 0, + mask: np.ndarray | None = None, + bins: int = 181, + color_by: str = "density", + int_range: tuple[float, float] = (0.0, 1.0), + smooth_sigma: float = 1.5, + label: str | None = None, + grid: bool = True, + overlay: dict | None = None, + figax=None, +): + """Stereographic pole figure of a crystal direction family over the map. + + For every probe position, all symmetry equivalents of `pole` are rotated + into the lab frame; upper-hemisphere poles are projected + stereographically and accumulated into a 2D histogram. + + Parameters + ---------- + om : OrientationMap + Matched orientation map. + pole : array-like, default=(0, 0, 1) + Crystal direction (Cartesian) of the pole family. + mask : np.ndarray | None + Per-position weights (e.g. phase mask). + bins : int, default=181 + Histogram bins across the stereographic disk. + color_by : {"density", "ipf"}, default="density" + "density": grayscale-to-color histogram. "ipf": each contribution is + colored by the IPF (zone axis) color of its probe position, and the + histogram density sets the brightness, black background. + int_range : tuple, default=(0.0, 1.0) + Density display range as fractions of the maximum bin: values below + the lower limit saturate to black, above the upper limit to full + brightness. + label : str | None + Annotation for the pole family, e.g. "(0001)" or "{110}". + grid : bool, default=True + Draw polar-angle circles and azimuth spokes every 30 degrees. + """ + import matplotlib.pyplot as plt + + assert om.quats is not None + qmap = om.quats[..., match, :] + x, y, wk, src = _pole_points(qmap, om.crystal, pole, mask) + + rng = [[-1.05, 1.05], [-1.05, 1.05]] + H, xe, ye = np.histogram2d(x, y, bins=bins, range=rng, weights=wk) + if smooth_sigma > 0: + from scipy.ndimage import gaussian_filter + + H = gaussian_filter(H, smooth_sigma) + lo, hi = int_range + # normalize against a high percentile of the occupied bins, not the single + # hottest bin -- one large uniform grain would otherwise black out the rest + occupied = H[H > 0] + h_ref = np.percentile(occupied, 98) if occupied.size else 1.0 + Hn = np.clip((H / max(h_ref, 1e-12) - lo) / max(hi - lo, 1e-12), 0, 1) + + if color_by == "ipf": + # white background: blend from white toward the per-position IPF + # color as the histogram density rises + rgb_pos = ipf_color(om.quats[..., match, :], om.crystal, "z").reshape(-1, 3) + rgb_all = rgb_pos[src] + img = np.zeros((bins, bins, 3)) + cnt = np.zeros((bins, bins)) + ii = np.clip(((x - rng[0][0]) / (rng[0][1] - rng[0][0]) * bins).astype(int), 0, bins - 1) + jj = np.clip(((y - rng[1][0]) / (rng[1][1] - rng[1][0]) * bins).astype(int), 0, bins - 1) + for k in range(3): + np.add.at(img[..., k], (ii, jj), rgb_all[:, k] * wk) + np.add.at(cnt, (ii, jj), wk) + if smooth_sigma > 0: + from scipy.ndimage import gaussian_filter + + for k in range(3): + img[..., k] = gaussian_filter(img[..., k], smooth_sigma) + cnt = gaussian_filter(cnt, smooth_sigma) + img = img / np.maximum(cnt[..., None], 1e-12) + # white background blending toward the IPF color as density rises -- + # keeps dark corner colors (blue) legible + disp = 1.0 - Hn[..., None] * (1.0 - img) + else: + import matplotlib.cm as cm + + # white -> yellow -> red -> black with increasing density + disp = cm.hot_r(Hn)[..., :3] + + # display in the image frame: horizontal = c (col, rightward), vertical = + # r (row, downward), matching the orientation maps -- H is indexed + # [row-bin, col-bin] so no transpose, origin upper + yy, xx = np.meshgrid( + 0.5 * (ye[:-1] + ye[1:]), 0.5 * (xe[:-1] + xe[1:]), indexing="ij" + ) + disp = disp.copy() + disp[(xx**2 + yy**2).T > 1.0] = 1.0 + + ax_leg = None + if figax is None: + if color_by == "ipf": + fig, (ax, ax_leg) = plt.subplots( + 1, 2, figsize=(7.2, 5.5), gridspec_kw={"width_ratios": [4, 1]} + ) + else: + fig, ax = plt.subplots(figsize=(5.5, 5.5)) + else: + fig, axs = figax + if isinstance(axs, (tuple, list, np.ndarray)) and len(np.atleast_1d(axs)) == 2: + ax, ax_leg = axs + else: + ax = axs + ax.imshow( + disp, + extent=(ye[0], ye[-1], xe[-1], xe[0]), + interpolation="nearest", + ) + if grid: + theta = np.linspace(0, 2 * np.pi, 361) + for pol_deg in (30, 60, 90): + r = np.tan(np.deg2rad(pol_deg) / 2) + lw = 1.0 if pol_deg == 90 else 0.5 + ax.plot(r * np.cos(theta), r * np.sin(theta), color="0.65", lw=lw) + if pol_deg < 90: + ax.text( + r * np.cos(np.deg2rad(45)), + r * np.sin(np.deg2rad(45)), + f"{pol_deg}°", + color="0.45", + fontsize=7, + ha="center", + va="center", + ) + for az in range(0, 180, 30): + ca, sa = np.cos(np.deg2rad(az)), np.sin(np.deg2rad(az)) + ax.plot([-ca, ca], [-sa, sa], color="0.85", lw=0.4) + # compact scan-axes glyph, top-left corner: the pole figure is in + # the scan (image) frame -- c rightward, r downward + gx, gy = -1.06, -1.06 + for dx, dy, lbl, ha, va in ( + (0.22, 0.0, "c", "left", "center"), + (0.0, 0.22, "r", "center", "top"), + ): + ax.annotate( + "", xy=(gx + dx, gy + dy), xytext=(gx, gy), + arrowprops=dict(arrowstyle="-|>", color="0.3", lw=1.2), + annotation_clip=False, + ) + ax.text( + gx + dx * 1.25, gy + dy * 1.25, lbl, + fontsize=9, ha=ha, va=va, color="0.3", + ) + ax.text( + gx - 0.03, gy - 0.06, "scan axes", fontsize=7, ha="left", + va="bottom", color="0.45", + ) + if overlay is not None: + if "om" in overlay: + # raw-histogram contour of another map's pole family + o_om = overlay["om"] + ox, oy, ow, _ = _pole_points( + o_om.quats[..., overlay.get("match", 0), :], + o_om.crystal, + overlay["pole"], + overlay.get("mask"), + ) + Ho, oxe, oye = np.histogram2d(ox, oy, bins=bins, range=rng, weights=ow) + if (Ho > 0).any(): + from scipy.ndimage import gaussian_filter + + Ho = gaussian_filter(Ho, max(smooth_sigma, 1.0)) + lev = np.percentile(Ho[Ho > 0], 99) * np.array([0.3, 0.7]) + xc = 0.5 * (oxe[:-1] + oxe[1:]) + yc = 0.5 * (oye[:-1] + oye[1:]) + # image frame: horizontal = col bins, vertical = row bins + ax.contour( + yc, xc, Ho, levels=lev, colors="k", + linewidths=[0.7, 1.3], alpha=0.85, + ) + ax.plot([], [], color="k", lw=1.2, label=overlay.get("label", "overlay")) + else: + oxy = _pole_scatter_xy(overlay["quats"], overlay["crystal"], overlay["pole"]) + ax.scatter( + oxy[:, 1], oxy[:, 0], s=80, marker="D", facecolors="none", + edgecolors="k", lw=1.2, label=overlay.get("label", "overlay"), + ) + ax.legend(loc="upper right", fontsize=8) + ax.set_xlim(-1.12, 1.12) + ax.set_ylim(1.2, -1.2) + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_frame_on(False) + title = f"{om.crystal.name} pole figure" + if label is not None: + title += f" {label}" + ax.set_title(title) + if ax_leg is not None: + if color_by == "ipf": + wedge_legend(om.crystal, ax_leg, orientation="vertical") + else: + ax_leg.axis("off") + return fig, ax diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py new file mode 100644 index 000000000..a36d5f5d1 --- /dev/null +++ b/src/quantem/diffraction/phase.py @@ -0,0 +1,316 @@ +"""Phase mapping from matched crystal orientations. + +PhaseMap compares candidate crystal patterns (each carrying orientations +matched and refined by OrientationMap) against the measured Bragg peaks at +every probe position. Every subset of candidates up to `max_patterns` is +scored as a joint model: the candidate pattern weights are solved by +non-negative least squares on the paired peak intensities, and the model cost +extends Diebold et al., Microsc. Microanal. 31, ozaf019 (2025): + + c(S) = sum_exp_peaks |I_m - sum_f w_f I_pred,f| + sum_f w_f I_unpaired,f + + penalty * (|S| - 1) + +normalized by the total measured intensity. Unpaired experimental intensity +appears in the first term (its prediction is zero); unpaired simulated +intensity is charged in full. The best subset answers orientation/phase +ambiguity directly: one orientation, two orientations of one phase, or two +phases, whichever explains the pattern best. Reliability is the cost gap +between the best models with and without the winning phase. +""" + +from __future__ import annotations + +from itertools import combinations + +import numpy as np +import torch +from tqdm import tqdm + +from quantem.core.io.serialize import AutoSerialize +from quantem.diffraction.orientation import OrientationMap + + +class PhaseMap(AutoSerialize): + """Assign best-fit phases to every probe position. + + Workflow:: + + pm = PhaseMap.from_orientation_maps([om_alpha, om_beta]) + pm.fit() + pm.plot_phase() + + Candidates are all (crystal, match) pairs of the input OrientationMaps, + so two matched orientations of one crystal compete on equal footing with + one orientation of each of two crystals. + """ + + _token = object() + + def __init__(self, orientation_maps: list[OrientationMap], _token=None): + if _token is not self._token: + raise RuntimeError("Use PhaseMap.from_orientation_maps().") + self.orientation_maps = orientation_maps + self.names = [om.crystal.name for om in orientation_maps] + # candidate list: (map index, match index) + self.candidates: list[tuple[int, int]] = [] + for i, om in enumerate(orientation_maps): + assert om.quats is not None + for m in range(om.quats.shape[2]): + self.candidates.append((i, m)) + + self.phase_weights: torch.Tensor | None = None + self.costs_single: torch.Tensor | None = None + self.cost_best: torch.Tensor | None = None + self.phase_index: torch.Tensor | None = None + self.reliability: torch.Tensor | None = None + + @classmethod + def from_orientation_maps(cls, orientation_maps: list[OrientationMap]) -> "PhaseMap": + """Create from OrientationMaps that share the same peaks.""" + p0 = orientation_maps[0].peaks + for om in orientation_maps: + if om.peaks.shape != p0.shape: + raise ValueError("All OrientationMaps must share the same scan shape.") + if om.quats is None: + raise RuntimeError(f"OrientationMap for {om.crystal.name}: run match() first.") + return cls(orientation_maps, _token=cls._token) + + def fit( + self, + pair_distance: float = 0.05, + power_intensity: float = 0.25, + max_patterns: int = 2, + complexity_penalty: float = 0.02, + weight_unmatched_sim: float = 0.5, + weight_overprediction: float = 1.0, + min_sim_intensity_rel: float = 0.02, + k_max: float | None = None, + min_number_peaks: int = 3, + progress_bar: bool = True, + ) -> "PhaseMap": + """Score all candidate subsets at every probe position. + + Parameters + ---------- + pair_distance : float, default=0.05 + Pairing distance delta (1/Angstroms) between simulated and + measured peaks. + power_intensity : float, default=0.25 + Intensities are raised to this power before comparison. + max_patterns : int, default=2 + Maximum number of candidate patterns fit simultaneously. + complexity_penalty : float, default=0.02 + Added cost per extra pattern in a model; sets how much better a + two-pattern fit must be to beat a single-pattern fit. + weight_unmatched_sim : float, default=0.5 + Cost weight of simulated intensity with no experimental partner. + weight_overprediction : float, default=1.0 + Cost weight of predicted intensity in excess of the measured + value on paired peaks. Unexplained measured intensity always + costs in full (coverage), but the symmetric intensity-mismatch + term assumes kinematical intensities are trustworthy; on + non-precession data with strong dynamical scattering, lowering + this weight makes the decision coverage-driven and removes the + bias toward sparse templates. + min_sim_intensity_rel : float, default=0.02 + Simulated reflections weaker than this fraction of the pattern + maximum are dropped before comparison: kinematically weak spots + are frequently unobservable and should not penalize a phase whose + structure factors happen to include many of them. + k_max : float | None + Restrict the comparison below this scattering vector. + """ + from scipy.optimize import nnls + + oms = self.orientation_maps + peaks = oms[0].peaks + R, C = peaks.shape[0], peaks.shape[1] + cands = self.candidates + F = len(cands) + delta = pair_distance + + fields = peaks.fields + ix = [fields.index(f) for f in ("qx", "qy", "intensity")] + + subsets = [s for n in range(1, max_patterns + 1) for s in combinations(range(F), n)] + + costs_single = torch.full((R, C, F), torch.nan, dtype=torch.float64) + cost_best = torch.full((R, C), torch.nan, dtype=torch.float64) + weights_out = torch.zeros((R, C, F), dtype=torch.float64) + reliability = torch.zeros((R, C), dtype=torch.float64) + best_subset = torch.full((R, C), -1, dtype=torch.long) + + iterator = list(np.ndindex(R, C)) + if progress_bar: + iterator = tqdm(iterator, desc="phase mapping") + for rx, ry in iterator: + data = peaks[rx, ry].array + if data.shape[0] < min_number_peaks: + continue + qxy = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) + im = torch.as_tensor(data[:, ix[2]], dtype=torch.float64).clamp_min(0) + if k_max is not None: + keep = torch.linalg.norm(qxy, dim=1) <= k_max + qxy, im = qxy[keep], im[keep] + im = im**power_intensity + n_exp = im.shape[0] + int_total = float(im.sum()) + + # per-candidate predicted intensity on each experimental peak, + # and unpaired simulated intensity + pred = np.zeros((n_exp, F)) + unpaired_sim = np.zeros(F) + for f, (i_om, m) in enumerate(cands): + om = oms[i_om] + if om.corr[rx, ry, m] <= 0: + continue + sim = om.generate_pattern(rx, ry, match=m, k_max=k_max) + sq = torch.stack((sim["qx"], sim["qy"]), dim=1) + s_raw = sim["intensity"] + if sq.shape[0] == 0: + continue + vis = s_raw > min_sim_intensity_rel * s_raw.max() + sq, s_raw = sq[vis], s_raw[vis] + si = s_raw**power_intensity + d = torch.cdist(sq, qxy) + d_min, j_min = d.min(dim=1) + pair = d_min < delta + frac = (d_min[pair] / delta).clamp(0, 1) + np.add.at( + pred[:, f], + j_min[pair].numpy(), + (si[pair] * (1 - frac)).numpy(), + ) + unpaired_sim[f] = weight_unmatched_sim * ( + float(si[~pair].sum()) + float((si[pair] * frac).sum()) + ) + + im_np = im.numpy() + results = [] + for s in subsets: + cols = [f for f in s if pred[:, f].any() or unpaired_sim[f] > 0] + if len(cols) == 0: + continue + B = pred[:, cols] + w, _ = nnls(B, im_np) + model = B @ w + under = np.maximum(im_np - model, 0).sum() # unexplained measured + over = np.maximum(model - im_np, 0).sum() # overpredicted paired + cost = ( + under + weight_overprediction * over + (w * unpaired_sim[cols]).sum() + ) / (int_total + 1e-12) + complexity_penalty * (len(cols) - 1) + results.append((cost, s, cols, w)) + if not results: + continue + results.sort(key=lambda r: r[0]) + c_best, s_best, cols_best, w_best = results[0] + cost_best[rx, ry] = c_best + best_subset[rx, ry] = subsets.index(s_best) + for f, w in zip(cols_best, w_best): + weights_out[rx, ry, f] = w + for cost, s, _, _ in results: + if len(s) == 1: + costs_single[rx, ry, s[0]] = cost + + # reliability: cost gap to the best model containing NO candidate + # of the dominant crystal (candidates of one crystal can be + # near-duplicates, e.g. after residual re-matching) + f_dom = cols_best[int(np.argmax(w_best))] + i_dom = cands[f_dom][0] + others = [ + c + for c, s, _, _ in results + if all(cands[f][0] != i_dom for f in s) + ] + reliability[rx, ry] = (min(others) - c_best) if others else torch.nan + + self.costs_single = costs_single + self.cost_best = cost_best + self.phase_weights = weights_out + self.reliability = reliability + self.best_subset = best_subset + + # dominant phase: candidate weights summed per crystal + n_maps = len(oms) + w_phase = torch.zeros((R, C, n_maps), dtype=torch.float64) + for f, (i_om, _) in enumerate(cands): + w_phase[..., i_om] += weights_out[..., f] + self.phase_index = w_phase.argmax(dim=-1) + self.phase_fractions = w_phase / w_phase.sum(dim=-1, keepdim=True).clamp_min(1e-12) + return self + + def plot_phase( + self, + phase_colors: np.ndarray | None = None, + reliability_range: tuple[float, float] = (0.0, 0.1), + scalebar: dict | None = None, + figax=None, + ): + """Dominant-phase map, colored by phase and shaded by reliability. + + Parameters + ---------- + phase_colors : np.ndarray | None + One RGB color per phase; defaults to the shared palette used by + the pattern overlay plots (gold, light blue, ...). + reliability_range : tuple, default=(0.0, 0.1) + Reliability values mapped to black ... full color. + scalebar : dict | None + Real-space scale bar, e.g. {"sampling": 30, "units": "A"}. + """ + import matplotlib.pyplot as plt + + from quantem.core.visualization.visualization_utils import add_scalebar_to_ax + from quantem.diffraction.orientation_visualization import DEFAULT_PHASE_COLORS + + assert self.phase_index is not None and self.reliability is not None + if phase_colors is None: + phase_colors = DEFAULT_PHASE_COLORS[: len(self.names)] + lo, hi = reliability_range + rel = np.nan_to_num(self.reliability.numpy(), nan=0.0) + alpha = ((rel - lo) / (hi - lo)).clip(0, 1) + rgb = phase_colors[self.phase_index.numpy()] * alpha[..., None] + + if figax is None: + fig, ax = plt.subplots(figsize=(9, 4.5)) + else: + fig, ax = figax + ax.imshow(rgb, interpolation="nearest") + ax.set_xticks([]) + ax.set_yticks([]) + if scalebar is not None: + add_scalebar_to_ax( + ax, + array_size=rgb.shape[1], + sampling=scalebar.get("sampling", 1.0), + length_units=scalebar.get("length", None), + units=scalebar.get("units", "pixels"), + width_px=rgb.shape[0] / 40, + pad_px=rgb.shape[0] / 80, + color=scalebar.get("color", "white"), + loc="lower right", + ) + handles = [ + plt.Line2D([0], [0], marker="s", ls="", color=c, label=n) + for c, n in zip(phase_colors, self.names) + ] + ax.legend(handles=handles, loc="upper right", fontsize=8) + # stacked reliability colorbars, black -> phase color + from matplotlib.cm import ScalarMappable + from matplotlib.colors import LinearSegmentedColormap, Normalize + + n_ph = len(phase_colors) + for k, color in enumerate(phase_colors): + cmap_k = LinearSegmentedColormap.from_list( + f"rel{k}", [(0, 0, 0), tuple(color)] + ) + cax = ax.inset_axes([1.02 + 0.025 * k, 0.05, 0.025, 0.9]) + cb = fig.colorbar( + ScalarMappable(norm=Normalize(lo, hi), cmap=cmap_k), cax=cax + ) + if k < n_ph - 1: + cb.set_ticks([]) + else: + cb.set_ticks([lo, hi]) + cb.set_label("reliability", fontsize=9) + return fig, ax diff --git a/src/quantem/diffraction/rotations.py b/src/quantem/diffraction/rotations.py new file mode 100644 index 000000000..77b378bdc --- /dev/null +++ b/src/quantem/diffraction/rotations.py @@ -0,0 +1,334 @@ +"""Quaternion rotation utilities for orientation mapping. + +All orientations in quantem.diffraction are represented as unit quaternions, +stored as torch tensors of shape (..., 4) in scalar-first order (w, x, y, z). +Rotation matrices, Euler angles, and axis-angle forms are provided only as +conversions at the boundaries. + +Convention +---------- +A quaternion q represents the rotation of crystal-frame vectors into the +laboratory (beam) frame:: + + v_lab = R(q) @ v_crystal + +The electron beam travels along -z in the lab frame. The zone axis --- the +beam direction expressed in crystal Cartesian coordinates --- is therefore +the third row of R(q):: + + zone_axis = R(q).T @ [0, 0, 1] + +Euler angles use the Z-X-Z convention (Rowenhorst et al., 2015). +""" + +from __future__ import annotations + +import numpy as np +import torch + + +def qmult(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + """Hamilton product of quaternions, broadcasting over leading dims.""" + aw, ax, ay, az = a.unbind(-1) + bw, bx, by, bz = b.unbind(-1) + return torch.stack( + ( + aw * bw - ax * bx - ay * by - az * bz, + aw * bx + ax * bw + ay * bz - az * by, + aw * by - ax * bz + ay * bw + az * bx, + aw * bz + ax * by - ay * bx + az * bw, + ), + dim=-1, + ) + + +def qconj(q: torch.Tensor) -> torch.Tensor: + """Quaternion conjugate (inverse for unit quaternions).""" + w, x, y, z = q.unbind(-1) + return torch.stack((w, -x, -y, -z), dim=-1) + + +def qnormalize(q: torch.Tensor) -> torch.Tensor: + """Normalize to unit length, with w >= 0 canonicalization.""" + q = q / torch.linalg.norm(q, dim=-1, keepdim=True) + return torch.where(q[..., :1] < 0, -q, q) + + +def qrotate(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor: + """Rotate vectors v (..., 3) by quaternions q (..., 4).""" + qv = torch.cat((torch.zeros_like(v[..., :1]), v), dim=-1) + return qmult(qmult(q, qv), qconj(q))[..., 1:] + + +def quat_to_matrix(q: torch.Tensor) -> torch.Tensor: + """Convert quaternions (..., 4) to rotation matrices (..., 3, 3).""" + w, x, y, z = q.unbind(-1) + two = 2.0 + R = torch.stack( + ( + 1 - two * (y * y + z * z), + two * (x * y - w * z), + two * (x * z + w * y), + two * (x * y + w * z), + 1 - two * (x * x + z * z), + two * (y * z - w * x), + two * (x * z - w * y), + two * (y * z + w * x), + 1 - two * (x * x + y * y), + ), + dim=-1, + ) + return R.reshape(q.shape[:-1] + (3, 3)) + + +def quat_from_matrix(R: torch.Tensor) -> torch.Tensor: + """Convert rotation matrices (..., 3, 3) to unit quaternions (..., 4). + + Uses the numerically stable branch selection of Shepperd's method, + vectorized over leading dimensions. + """ + batch_shape = R.shape[:-2] + R = R.reshape(-1, 3, 3) + m00, m01, m02 = R[:, 0, 0], R[:, 0, 1], R[:, 0, 2] + m10, m11, m12 = R[:, 1, 0], R[:, 1, 1], R[:, 1, 2] + m20, m21, m22 = R[:, 2, 0], R[:, 2, 1], R[:, 2, 2] + + # four candidate solutions, one per branch + q_w = torch.stack((1 + m00 + m11 + m22, m21 - m12, m02 - m20, m10 - m01), dim=-1) + q_x = torch.stack((m21 - m12, 1 + m00 - m11 - m22, m01 + m10, m02 + m20), dim=-1) + q_y = torch.stack((m02 - m20, m01 + m10, 1 - m00 + m11 - m22, m12 + m21), dim=-1) + q_z = torch.stack((m10 - m01, m02 + m20, m12 + m21, 1 - m00 - m11 + m22), dim=-1) + q_all = torch.stack((q_w, q_x, q_y, q_z), dim=1) # (N, 4, 4) + + trace_terms = torch.stack( + (1 + m00 + m11 + m22, 1 + m00 - m11 - m22, 1 - m00 + m11 - m22, 1 - m00 - m11 + m22), + dim=-1, + ) + branch = trace_terms.argmax(dim=-1) + q = q_all[torch.arange(R.shape[0], device=R.device), branch] + return qnormalize(q).reshape(batch_shape + (4,)) + + +def quat_from_axis_angle(axis: torch.Tensor, angle: torch.Tensor) -> torch.Tensor: + """Quaternion for rotation of `angle` (radians) about `axis` (..., 3).""" + axis = axis / torch.linalg.norm(axis, dim=-1, keepdim=True) + angle = torch.as_tensor(angle, dtype=axis.dtype, device=axis.device) + half = angle[..., None] / 2 + return torch.cat((torch.cos(half), torch.sin(half) * axis), dim=-1) + + +def quat_to_axis_angle(q: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """Return (axis (..., 3), angle (...,)) of unit quaternions.""" + q = qnormalize(q) + angle = 2 * torch.acos(q[..., 0].clamp(-1, 1)) + sin_half = torch.sqrt((1 - q[..., 0] ** 2).clamp_min(1e-24)) + axis = q[..., 1:] / sin_half[..., None] + return axis, angle + + +def quat_from_euler_zxz(angles: torch.Tensor) -> torch.Tensor: + """Quaternion from Z-X-Z Euler angles (..., 3) in radians.""" + a, b, c = angles.unbind(-1) + z = torch.zeros_like(a) + qa = torch.stack((torch.cos(a / 2), z, z, torch.sin(a / 2)), dim=-1) + qb = torch.stack((torch.cos(b / 2), torch.sin(b / 2), z, z), dim=-1) + qc = torch.stack((torch.cos(c / 2), z, z, torch.sin(c / 2)), dim=-1) + return qmult(qmult(qa, qb), qc) + + +def quat_to_euler_zxz(q: torch.Tensor) -> torch.Tensor: + """Z-X-Z Euler angles (..., 3) in radians from unit quaternions.""" + R = quat_to_matrix(q) + beta = torch.acos(R[..., 2, 2].clamp(-1, 1)) + alpha = torch.atan2(R[..., 0, 2], -R[..., 1, 2]) + gamma = torch.atan2(R[..., 2, 0], R[..., 2, 1]) + # gimbal-locked cases: fold everything into alpha + locked = torch.sin(beta).abs() < 1e-8 + alpha_locked = torch.atan2(R[..., 1, 0], R[..., 0, 0]) + alpha = torch.where(locked, alpha_locked, alpha) + gamma = torch.where(locked, torch.zeros_like(gamma), gamma) + return torch.stack((alpha, beta, gamma), dim=-1) + + +def quat_from_zone_axis( + zone_axis: torch.Tensor, + in_plane_deg: torch.Tensor | float = 0.0, +) -> torch.Tensor: + """Orientation with the given crystal direction along the beam. + + Parameters + ---------- + zone_axis : torch.Tensor + Crystal-frame Cartesian direction(s) (..., 3) to place along the beam. + in_plane_deg : torch.Tensor | float, default=0.0 + Additional in-plane rotation of the pattern, degrees. + + Returns + ------- + torch.Tensor + Quaternions (..., 4) such that quat_to_matrix(q).T @ [0,0,1] == zone_axis. + """ + v = zone_axis / torch.linalg.norm(zone_axis, dim=-1, keepdim=True) + zhat = torch.zeros_like(v) + zhat[..., 2] = 1.0 + # minimal rotation taking zone axis to z + axis = torch.cross(v, zhat, dim=-1) + sin_t = torch.linalg.norm(axis, dim=-1) + cos_t = v[..., 2] + angle = torch.atan2(sin_t, cos_t) + # antiparallel / parallel cases: rotate about x + fallback = torch.zeros_like(v) + fallback[..., 0] = 1.0 + axis = torch.where(sin_t[..., None] < 1e-12, fallback, axis) + q_tilt = quat_from_axis_angle(axis, angle) + in_plane = torch.deg2rad( + torch.as_tensor(in_plane_deg, dtype=v.dtype, device=v.device) + ).broadcast_to(v.shape[:-1]) + z3 = torch.zeros_like(in_plane) + q_spin = torch.stack( + (torch.cos(in_plane / 2), z3, z3, torch.sin(in_plane / 2)), dim=-1 + ) + return qnormalize(qmult(q_spin, q_tilt)) + + +def zone_axis_from_quat(q: torch.Tensor) -> torch.Tensor: + """Beam direction in crystal Cartesian coordinates (third row of R).""" + return quat_to_matrix(q)[..., 2, :] + + +def misorientation_angle_deg( + qa: torch.Tensor, + qb: torch.Tensor, + sym_ops: torch.Tensor | None = None, +) -> torch.Tensor: + """Misorientation angle between orientations, minimized over symmetry. + + Parameters + ---------- + qa, qb : torch.Tensor + Quaternions (..., 4), broadcastable against each other. + sym_ops : torch.Tensor | None + Proper rotation symmetry quaternions (S, 4) of the crystal. If None, + the raw rotation angle between qa and qb is returned. + + Returns + ------- + torch.Tensor + Misorientation angles in degrees (...,). + """ + dq = qmult(qconj(qa), qb) + if sym_ops is None: + w = dq[..., 0].abs().clamp(-1, 1) + else: + dq_sym = qmult(dq[..., None, :], sym_ops) # (..., S, 4) + w = dq_sym[..., 0].abs().amax(dim=-1).clamp(-1, 1) + return torch.rad2deg(2 * torch.acos(w)) + + +def misorientation_axis_angle( + qa: torch.Tensor, + qb: torch.Tensor, + sym_ops: torch.Tensor | None = None, +) -> tuple[torch.Tensor, torch.Tensor]: + """Symmetry-reduced misorientation axis (crystal frame) and angle. + + Returns the rotation axis (..., 3) in crystal Cartesian coordinates and + the angle (...,) in degrees, minimized over the symmetry operators. + """ + dq = qmult(qconj(qa), qb) + if sym_ops is not None: + dq_sym = qmult(dq[..., None, :], sym_ops) # (..., S, 4) + best = dq_sym[..., 0].abs().argmax(dim=-1) + dq = torch.gather( + dq_sym, -2, best[..., None, None].expand(*best.shape, 1, 4) + ).squeeze(-2) + dq = qnormalize(dq) + axis, angle = quat_to_axis_angle(dq) + return axis, torch.rad2deg(angle) + + +def slerp(v0: torch.Tensor, v1: torch.Tensor, t: torch.Tensor) -> torch.Tensor: + """Spherical linear interpolation between unit vectors v0 and v1.""" + v0 = v0 / torch.linalg.norm(v0, dim=-1, keepdim=True) + v1 = v1 / torch.linalg.norm(v1, dim=-1, keepdim=True) + omega = torch.acos((v0 * v1).sum(-1, keepdim=True).clamp(-1, 1)) + so = torch.sin(omega) + t = t[..., None] + small = so.abs() < 1e-12 + w0 = torch.where(small, 1 - t, torch.sin((1 - t) * omega) / so) + w1 = torch.where(small, t, torch.sin(t * omega) / so) + return w0 * v0 + w1 * v1 + + +def sample_zone_axes( + corners: torch.Tensor, + step_deg: float, +) -> tuple[torch.Tensor, torch.Tensor]: + """Triangular SLERP grid of unit zone-axis vectors inside a spherical triangle. + + Parameters + ---------- + corners : torch.Tensor + (3, 3) rows are the Cartesian corner directions of the fundamental + zone-axis wedge, e.g. [001], [011], [111] for m-3m. + step_deg : float + Approximate angular step between neighboring zone axes, degrees. + + Returns + ------- + vectors : torch.Tensor + (N, 3) unit vectors sampling the wedge. + inds : torch.Tensor + (N, 2) integer (row, col) indices in the triangular grid. + """ + c = corners / torch.linalg.norm(corners, dim=-1, keepdim=True) + a01 = torch.rad2deg(torch.acos((c[0] * c[1]).sum().clamp(-1, 1))) + a02 = torch.rad2deg(torch.acos((c[0] * c[2]).sum().clamp(-1, 1))) + n_steps = int(torch.ceil(torch.maximum(a01, a02) / step_deg).item()) + n_steps = max(n_steps, 1) + + vecs, inds = [], [] + for i in range(n_steps + 1): + t = torch.tensor(i / n_steps, dtype=c.dtype, device=c.device) + pv = slerp(c[0], c[1], t) + pw = slerp(c[0], c[2], t) + if i == 0: + vecs.append(pv[None]) + inds.append(torch.tensor([[0, 0]])) + continue + s = torch.linspace(0, 1, i + 1, dtype=c.dtype, device=c.device) + row = slerp(pv.expand(i + 1, 3), pw.expand(i + 1, 3), s) + row = row / torch.linalg.norm(row, dim=-1, keepdim=True) + vecs.append(row) + inds.append(torch.stack((torch.full((i + 1,), i), torch.arange(i + 1)), dim=-1)) + return torch.cat(vecs), torch.cat(inds).to(torch.long) + + +def symmetry_quaternions( + rotations: np.ndarray, + lat_real: np.ndarray, +) -> torch.Tensor: + """Convert spglib integer rotation matrices to Cartesian quaternions. + + Parameters + ---------- + rotations : np.ndarray + (S, 3, 3) integer rotation matrices in the lattice basis, as returned + by spglib (improper operations are discarded). + lat_real : np.ndarray + (3, 3) real-space lattice vectors as rows. + + Returns + ------- + torch.Tensor + (S', 4) unique proper-rotation quaternions in Cartesian coordinates, + float64. + """ + A = torch.as_tensor(lat_real, dtype=torch.float64).T # columns are a, b, c + W = torch.as_tensor(np.array(rotations), dtype=torch.float64) + R_cart = A @ W @ torch.linalg.inv(A) + proper = torch.linalg.det(R_cart) > 0 + q = quat_from_matrix(R_cart[proper]) + # deduplicate (q and -q are the same rotation; qnormalize fixed the sign) + q_unique = torch.unique(torch.round(q / 1e-6) * 1e-6, dim=0) + return qnormalize(q_unique) diff --git a/src/quantem/diffraction/strain.py b/src/quantem/diffraction/strain.py new file mode 100644 index 000000000..0c000e364 --- /dev/null +++ b/src/quantem/diffraction/strain.py @@ -0,0 +1,975 @@ +from __future__ import annotations + +import warnings + +import numpy as np +from numpy.lib.stride_tricks import sliding_window_view +from numpy.typing import NDArray + +from quantem.core.datastructures.dataset2d import Dataset2d +from quantem.core.io.serialize import AutoSerialize +from quantem.diffraction.strain_visualization import ( + plot_strain_panels, + plot_strain_precision_histogram, +) + + +class StrainMap(AutoSerialize): + """Strain tensor maps fit from per-position lattice vectors. + + Stores the reference-frame strain components ``e_rr`` (row), ``e_cc`` (col), + ``e_rc`` (shear), and ``phi`` (infinitesimal rotation). The reference lattice + is the median of the fitted ``g_u``/``g_v`` over a mask/ROI; the strain tensor + is recomputed by :meth:`update_reference`. + + Two measurement modalities are supported and give identical strain for the same + deformation, so correlation and cepstral maps can be compared directly: + reciprocal-space Bragg vectors (``real_space=False``, nanobeam correlation) and + real-space cepstral/autocorrelation vectors (``real_space=True``). + + Parameters + ---------- + u_array : np.ndarray + Per-position first lattice vector, shape ``(scan_row, scan_col, 2)``. + v_array : np.ndarray + Per-position second lattice vector, shape ``(scan_row, scan_col, 2)``. + ds_shape : tuple of int + Shape of the parent scan grid, used to size the strain maps. + real_space : bool + ``False`` for reciprocal-space (Bragg/correlation) lattice vectors; ``True`` + for real-space (cepstral autocorrelation / DPC) vectors. Both modalities are + arranged to yield matching strain (see :func:`_strain_tensor`). + u_ref : np.ndarray, optional + Fixed reference for ``u``; if omitted the median over the mask/ROI is used. + A value supplied here persists across re-fits. + v_ref : np.ndarray, optional + Fixed reference for ``v``; if omitted the median over the mask/ROI is used. + A value supplied here persists across re-fits. + mask : np.ndarray, optional + ``(scan_row, scan_col)`` weighting/ROI mask; defaults to all ones (the full + scan). Normalized to ``[0, 1]`` on assignment. + ds_sampling : float, optional + Real-space scan sampling (step size); defaults to ``1.0``. + ds_units : str, optional + Units for ``ds_sampling``; defaults to ``"pixels"``. + """ + + mask: np.ndarray | None = None + real_space: bool = False + + e_rr: Dataset2d + e_cc: Dataset2d + e_rc: Dataset2d + phi: Dataset2d + + u_ref: np.ndarray | None = None + v_ref: np.ndarray | None = None + u_array: np.ndarray + v_array: np.ndarray + + ds_sampling: float = 1.0 + ds_units: str = "pixels" + ds_shape: tuple[int, ...] + + def __init__( + self, + u_array: np.ndarray, + v_array: np.ndarray, + ds_shape: tuple[int, ...], + real_space: bool, + u_ref: np.ndarray | None = None, + v_ref: np.ndarray | None = None, + mask: np.ndarray | None = None, + ds_sampling: float | None = None, + ds_units: str | None = None, + q_to_r_rotation_ccw_deg: float = 0.0, + q_transpose: bool = False, + ): + super().__init__() + self.u_array = u_array + self.v_array = v_array + + self.q_to_r_rotation_ccw_deg = q_to_r_rotation_ccw_deg + self.q_transpose = q_transpose + + self.u_array = _raw_vec_to_display(self.u_array, rotation_ccw_deg = q_to_r_rotation_ccw_deg, transpose=q_transpose) + self.v_array = _raw_vec_to_display(self.v_array, rotation_ccw_deg = q_to_r_rotation_ccw_deg, transpose=q_transpose) + + self.ds_shape = ds_shape + self.real_space = real_space + + self.ds_sampling = 1.0 if ds_sampling is None else ds_sampling + self.ds_units = "pixels" if ds_units is None else ds_units + + m = np.ones(ds_shape[:2], dtype=float) if mask is None else np.asarray(mask, dtype=float) + m_lo = np.nanmin(m) + m_hi = np.nanmax(m) + if not (np.isfinite(m_lo) and np.isfinite(m_hi)) or m_hi <= m_lo: + m = np.ones_like(m) + elif m_lo < 0.0 or m_hi > 1.0: + m = (m - m_lo) / (m_hi - m_lo) + self.mask = m + + # user-supplied reference vectors persist across re-fits (None = use median) + self._u_ref_fixed = None if u_ref is None else _raw_vec_to_display(np.asarray(u_ref, dtype=float), + rotation_ccw_deg=q_to_r_rotation_ccw_deg, + transpose=q_transpose) + self._v_ref_fixed = None if v_ref is None else _raw_vec_to_display(np.asarray(v_ref, dtype=float), + rotation_ccw_deg=q_to_r_rotation_ccw_deg, + transpose=q_transpose) + self.u_ref = None + self.v_ref = None + + self.update_reference() + + # ---- main methods ---- + + def update_reference( + self, + strain_mask: np.ndarray | None = None, + u_ref: np.ndarray | None = None, + v_ref: np.ndarray | None = None, + plot_strain_roi: bool = False, + define_in_rotated_frame: bool = False, + **plot_kwargs, + ) -> "StrainMap": + """(Re)compute the reference lattice and strain tensor maps. + + Reference precedence: explicit ``u_ref``/``v_ref`` argument > vectors fixed at + construction > median over ``strain_mask`` (if given) else over ``self.mask`` + else the global median. + + Parameters + ---------- + strain_mask : np.ndarray, optional + ``(scan_row, scan_col)`` ROI selecting the positions used to compute the + median reference lattice. If omitted, ``self.mask`` (else the global + median) is used. + u_ref : np.ndarray, optional + Explicit reference for ``u``; overrides both the construction-time fixed + value and the median. + v_ref : np.ndarray, optional + Explicit reference for ``v``; overrides both the construction-time fixed + value and the median. + plot_strain_roi : bool, default=False + If ``True``, show the recomputed strain via :meth:`plot_strain_roi` + (color-scaled to the ROI) so the chosen reference region can be checked + for flatness. + define_in_rotated_frame: bool, default = False + If ''True'' means the u_ref and v_ref passed into the function is defined in the rotated detector frame + **plot_kwargs + Forwarded to :meth:`plot_strain_roi` when ``plot_strain_roi=True``. + + Returns + ------- + StrainMap + ``self``, with the reference lattice and strain maps recomputed. + """ + u_med, v_med = _reference_lattice(self.u_array, self.v_array, self.mask, strain_mask) + + if u_ref is not None: + if define_in_rotated_frame: + self.u_ref = np.asarray(u_ref, dtype=float) + else: + self.u_ref = _raw_vec_to_display( + np.asarray(u_ref, dtype=float), + rotation_ccw_deg=self.q_to_r_rotation_ccw_deg, + transpose=self.q_transpose) + elif self._u_ref_fixed is not None: + self.u_ref = self._u_ref_fixed + else: + self.u_ref = u_med + + if v_ref is not None: + if define_in_rotated_frame: + self.v_ref = np.asarray(v_ref, dtype=float) + else: + self.v_ref = _raw_vec_to_display( + np.asarray(v_ref, dtype=float), + rotation_ccw_deg=self.q_to_r_rotation_ccw_deg, + transpose=self.q_transpose) + elif self._v_ref_fixed is not None: + self.v_ref = self._v_ref_fixed + else: + self.v_ref = v_med + + e_rr, e_cc, e_rc, phi = _strain_tensor( + self.u_array, self.v_array, self.u_ref, self.v_ref, self.real_space + ) + self.e_rr = Dataset2d.from_array(e_rr, name="strain e_rr", signal_units="fractional") + self.e_cc = Dataset2d.from_array(e_cc, name="strain e_cc", signal_units="fractional") + self.e_rc = Dataset2d.from_array(e_rc, name="strain e_rc", signal_units="fractional") + self.phi = Dataset2d.from_array(phi, name="strain rotation", signal_units="radians") + + if plot_strain_roi: + self.plot_strain_roi(strain_mask=strain_mask, **plot_kwargs) + return self + + def rotate_strain( + self, rotation_angle: float = 0.0 + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Tensor-rotate the strain into a frame rotated by ``rotation_angle`` (degrees). + + The rotation field ``phi`` is invariant under frame rotation and is not + transformed. + + Parameters + ---------- + rotation_angle : float, default=0.0 + Frame rotation angle, in degrees. + + Returns + ------- + tuple of np.ndarray + ``(e_uu, e_vv, e_uv)`` strain components in the rotated frame. + """ + return _rotate_strain_tensor( + self.e_rr.array, self.e_cc.array, self.e_rc.array, rotation_angle + ) + + def plot_strain_roi( + self, + strain_mask: np.ndarray | None = None, + plot_rotation: bool = True, + cmap_strain: str = "RdBu_r", + cmap_rotation: str = "PiYG", + strain_range_percent: tuple[float, float] | None = None, + rotation_range_degrees: tuple[float, float] | None = None, + transpose_image: bool = False, + rotate_title: bool = False, + plot_dilation: bool = False, + layout: str = "horizontal", + arrow_style: str = "title", + figsize: tuple[float, float] | None = None, + **kwargs, + ): + """Plot the strain in the raw row/col reference frame, color-scaled to the ROI. + + The color range is symmetric about zero and set by the largest absolute + strain (and rotation) *inside the reference ROI* — ``strain_mask`` if given, + else ``self.mask`` — so a well-chosen, strain-free reference region reads as + flat (near mid-color) and any residual gradient or tilt stands out. The ROI + itself is drawn in color while everything outside it is shown in greyscale, + so the chosen reference region is obvious at a glance. Unlike + :meth:`plot_strain`, no display rotation is applied: the panels show the raw + ``e_rr``/``e_cc``/``e_rc`` that :meth:`update_reference` just computed. + + Parameters + ---------- + strain_mask : np.ndarray, optional + ROI defining the color range (and the reference region). If omitted, + ``self.mask`` is used. + plot_rotation : bool, default=True + Whether to include the rotation (``phi``) panel. + cmap_strain : str, default="RdBu_r" + Colormap for the strain panels. + cmap_rotation : str, default="PiYG" + Colormap for the rotation panel. + strain_range_percent : tuple of float, default=(-3.0, 3.0) + Symmetric color range for the strain panels, in percent. + rotation_range_degrees : tuple of float, default=(-2.0, 2.0) + Symmetric color range for the rotation panel, in degrees. + transpose_image: bool, default = False + If ''True'' transpose the real space image before plotting strain + rotate_title: bool, default = False + If ''True'', rotates panel titles by 90 degrees. + plot_dilation: bool, default = False + If ''True'' plots euu + evv, and euv instead of euu, evv, euv + layout : {"horizontal", "vertical"}, default="horizontal" + Panel arrangement. + arrow_style: str, default="title" + Plots the directional arrows along with the strain titles. + Alternatively can be "legend" where it plots it on the side + figsize : tuple of float, optional + Figure size in inches; if omitted it is derived from the layout. + **kwargs + Forwarded to + :func:`~quantem.diffraction.strain_visualization.plot_strain_panels`. + + Returns + ------- + tuple + ``(fig, ax)`` from :func:`plot_strain_panels`. + """ + + if arrow_style not in ("title", "legend"): + raise ValueError("arrow_style must be 'title' or 'legend'") + + roi_src = self.mask if strain_mask is None else strain_mask + e_rr, e_cc, e_rc, phi = ( + self.e_rr.array, + self.e_cc.array, + self.e_rc.array, + self.phi.array, + ) + + inside = np.asarray(roi_src) > 0 if roi_src is not None else np.ones(e_rr.shape, bool) + if not inside.any(): + inside = np.ones(e_rr.shape, bool) + + strain_stack = np.stack([e_rr[inside], e_cc[inside], e_rc[inside]]) + smax = float(np.nanmax(np.abs(strain_stack))) * 100.0 + rmax = float(np.rad2deg(np.nanmax(np.abs(phi[inside])))) + smax = smax if smax > 0 else 1e-6 + rmax = rmax if rmax > 0 else 1e-6 + + return plot_strain_panels( + e_rr, + e_cc, + e_rc, + phi, + self.mask, + self.u_ref, + self.v_ref, + self.ds_shape, + ds_sampling=self.ds_sampling, + ds_units=self.ds_units, + strain_range_percent=(-smax, smax) if strain_range_percent is None else strain_range_percent, + rotation_range_degrees=(-rmax, rmax) if rotation_range_degrees is None else rotation_range_degrees, + roi=inside, + plot_rotation=plot_rotation, + cmap_strain=cmap_strain, + cmap_rotation=cmap_rotation, + layout=layout, + transpose_image = transpose_image, + rotate_title = rotate_title, + plot_dilation = plot_dilation, + figsize=figsize, + panel_titles=( + r"$\epsilon_{rr}$ $\updownarrow$", + r"$\epsilon_{cc}$ $\leftrightarrow$", + r"$\epsilon_{rc}$ $\nwarrow\!\!\!\!\!\!\!\!\!\:\searrow$", + ), + arrow_style = arrow_style, + **kwargs, + ) + + def plot_strain( + self, + rotation_angle: float = 0.0, + strain_range_percent: tuple[float, float] = (-3.0, 3.0), + rotation_range_degrees: tuple[float, float] = (-2.0, 2.0), + mask_range: tuple[float, float] = (0.0, 1.0), + plot_rotation: bool = True, + plot_gvecs: bool = False, + plot_scalebar: bool = False, + cmap_strain: str = "RdBu_r", + cmap_rotation: str = "PiYG", + transpose_image: bool = False, + transpose_strain: bool = False, + rotate_title: bool = False, + plot_dilation: bool = False, + layout: str = "horizontal", + arrow_style: str = "title", + figsize: tuple[float, float] | None = None, + **kwargs, + ): + """Plot the strain (rotated into the display frame) and rotation panels. + + Parameters + ---------- + rotation_angle : float, default=0.0 + Angle (degrees) by which the strain tensor is rotated into the display + frame before plotting. + strain_range_percent : tuple of float, default=(-3.0, 3.0) + Symmetric color range for the strain panels, in percent. + rotation_range_degrees : tuple of float, default=(-2.0, 2.0) + Symmetric color range for the rotation panel, in degrees. + mask_range : tuple of float, default=(0.0, 1.0) + ``(low, high)`` window remapping the mask brightness: positions with + mask ``>= high`` are shown at full color, ``<= low`` are black, and + values between ramp linearly from black to full. The default leaves the + normalized mask unchanged. + plot_rotation : bool, default=True + Whether to include the rotation (``phi``) panel. + plot_gvecs : bool, default=False + Whether to overlay the reference lattice vectors. + plot_scalebar : bool, default=False + Whether to draw a real-space scale bar. + cmap_strain : str, default="RdBu_r" + Colormap for the strain panels. + cmap_rotation : str, default="PiYG" + Colormap for the rotation panel. + transpose_image: bool, default = False + If ''True'' transpose the real space image before plotting strain + transpose_strain : bool, default=False + If ``True``, transpose the detector (row/col) axes before rotating, + matching the DPC convention (see + :func:`~quantem.diffraction.strain_autocorrelation._raw_vec_to_display`): + transpose first, then rotate. This swaps the normal strain components, + leaves the shear unchanged, and reverses the sign of the rotation field. + rotate_title: bool, default = False + If ''True'', rotates panel titles by 90 degrees. + plot_dilation: bool, default = False + If ''True'' plots euu + evv, and euv instead of euu, evv, euv + layout : {"horizontal", "vertical"}, default="horizontal" + Panel arrangement. + figsize : tuple of float, optional + Figure size in inches; if omitted it is derived from the layout. + arrow_style: str, default="title" + Plots the directional arrows along with the strain titles. + Alternatively can be "legend" where it plots it on the side + **kwargs + Forwarded to + :func:`~quantem.diffraction.strain_visualization.plot_strain_panels`. + + Returns + ------- + tuple + ``(fig, ax)`` from :func:`plot_strain_panels`. + """ + if arrow_style not in ("title", "legend"): + raise ValueError("arrow_style must be 'title' or 'legend'") + + e_rr = self.e_rr.array + e_cc = self.e_cc.array + e_rc = self.e_rc.array + phi = self.phi.array + if transpose_strain: + # Detector-axis transpose, applied BEFORE the rotation to match the DPC + # convention shared across quantem (see _raw_vec_to_display): swapping the + # (row, col) axes swaps the normal strains, keeps the shear unchanged, and + # reverses the sense of the rotation field. + e_rr, e_cc = e_cc, e_rr + phi = -phi + e_uu, e_vv, e_uv = _rotate_strain_tensor(e_rr, e_cc, e_rc, rotation_angle) + return plot_strain_panels( + e_uu, + e_vv, + e_uv, + phi, + self.mask, + self.u_ref, + self.v_ref, + self.ds_shape, + ds_sampling=self.ds_sampling, + ds_units=self.ds_units, + strain_range_percent=strain_range_percent, + rotation_range_degrees=rotation_range_degrees, + mask_range=mask_range, + plot_rotation=plot_rotation, + plot_gvecs=plot_gvecs, + plot_scalebar=plot_scalebar, + cmap_strain=cmap_strain, + cmap_rotation=cmap_rotation, + layout=layout, + transpose_image = transpose_image, + rotate_title = rotate_title, + plot_dilation = plot_dilation, + figsize=figsize, + strain_rotation_angle=rotation_angle, + arrow_style = arrow_style, + **kwargs, + ) + + def estimate_strain_precision( + self, + mask_range: tuple[float, float] = (0.0, 1.0), + rotation_angle: float = 0.0, + window: int = 5, + mask_threshold: float = 0.5, + min_neighbors: int = 3, + component: str = "combined", + bins: int = 50, + bounds: tuple[float, float] | None = None, + plot: bool = True, + returnfig: bool = False, + ): + """Estimate strain *precision* (random scatter) from local median deviations. + + This measures repeatability, not accuracy. Without a ground truth (e.g. a + simulation) it cannot detect systematic error — only how far each position + scatters from its local neighborhood. For every position the deviation from + the median of its surrounding well-indexed neighbors is + + ``error(r, c) = | strain(r, c) - median( strain over neighbors with + scaled mask > mask_threshold ) |`` + + computed for each tensor component (the center position is excluded from its + own median). The three strain components are reduced to one rotation-invariant + number via the Frobenius norm of the symmetric strain-tensor deviation, + + ``combined = sqrt(d_uu**2 + d_vv**2 + 2*d_uv**2)``, + + (equivalently the root-sum-square of the principal-strain deviations) so a + single strain precision can be quoted and compared between datasets. Rotation + precision is reported separately, not folded into ``combined``. + + Each component's precision is summarized by the mask-weighted **median** of its + per-position deviations — the center of the histogram bulk. The median is used + (not the mean or RMS) because a handful of bad-fit pixels form a heavy tail that + would drag a second moment far to the right of where the distribution actually + sits, leaving the reported number disconnected from the histogram; the median + ignores that tail. A weighted histogram of the chosen component is shown, marked + with its median. + + Parameters + ---------- + mask_range : tuple of float, default=(0.0, 1.0) + ``(low, high)`` window remapping :attr:`mask` to ``[0, 1]`` (same + convention as :meth:`plot_strain`); the remapped mask both selects which + positions are trusted (``> mask_threshold`` -- used as neighbors *and* as + the set the precision is computed over) and weights the histogram and the + median. + rotation_angle : float, default=0.0 + Frame rotation (degrees) applied before measuring per-component precision, + matching :meth:`plot_strain`. ``0`` reports the raw row/col frame + (``e_uu == e_rr`` ...). The combined number is rotation-invariant. + window : int, default=5 + Odd edge length (px) of the neighborhood bounding box; the footprint is + the inscribed disk of radius ``window / 2`` (3 -> 8 neighbors, 5 -> 20, + 7 -> 36). A pure linear strain ramp cancels in the (symmetric) median, so + larger windows mostly just steady the median — at the cost of blurring + *curved* strain and biasing the masked edges. ``5`` roughly halves the + noise-floor over-estimate of ``3`` (~9% -> ~4%) while staying local. + mask_threshold : float, default=0.5 + A position is trusted only if its scaled mask exceeds this value. Trusted + positions are the ones used as local-median neighbors *and* the ones whose + deviations enter the reported median and histogram; sub-threshold positions + are excluded from both (not merely down-weighted), so a poorly-indexed + pixel cannot leak its scatter into the precision. + min_neighbors : int, default=3 + Minimum number of valid neighbors required; positions with fewer get no + precision estimate (``nan``, dropped from the statistics). + component : {"combined","e_uu","e_vv","e_uv","rotation"}, default="combined" + Which error distribution to histogram. + bins : int, default=50 + Number of histogram bins, or a sequence of explicit bin edges. With a + bin *count* and no ``bounds``, the range defaults to ``[0, weighted 99th + percentile]`` of the trusted deviations -- robust to the heavy outlier + tail, which otherwise sets the range to its max and crushes the bulk into + the first bin. Passing explicit edges (or ``bounds``) overrides this. + bounds : tuple of float, optional + ``(low, high)`` histogram range in display units (percent for strain, + degrees for rotation). Fix it to compare datasets on the same axis, or to + see the full tail. Values outside the range are left out of the bars (no + overflow spike); the median is computed from all trusted positions + regardless, and ``out_of_range_fraction`` records how much was off-range. + plot : bool, default=True + If ``True``, draw the weighted precision histogram. + returnfig : bool, default=False + If ``True``, return ``(fig, ax)`` instead of the results dict. + + Returns + ------- + dict or tuple + A results dict with the ``precision`` (mask-weighted median local + deviation) per component and ``combined`` (strain in percent, rotation in + degrees), the normalized ``counts`` and ``edges`` of the histogrammed + ``component`` (and ``counts_raw``, the weighted bin sums), + ``out_of_range_fraction`` (weighted mass outside the histogram range, + excluded from the bars), and the chosen settings; or ``(fig, ax)`` when + ``returnfig=True``. + """ + if window < 3 or window % 2 == 0: + raise ValueError("window must be an odd integer >= 3.") + valid_components = ("combined", "e_uu", "e_vv", "e_uv", "rotation") + if component not in valid_components: + raise ValueError(f"component must be one of {valid_components}.") + + # number of neighbors in the circular footprint (matches _local_masked_median) + p = window // 2 + oy, ox = np.ogrid[-p : p + 1, -p : p + 1] + n_neighbors = int(np.sum((oy ** 2 + ox ** 2) <= (window / 2.0) ** 2) - 1) + + # per-component fields in the (optionally rotated) display frame; phi is + # rotation-invariant and is carried through unchanged + e_uu, e_vv, e_uv = self.rotate_strain(rotation_angle) + fields = {"e_uu": e_uu, "e_vv": e_vv, "e_uv": e_uv, "rotation": self.phi.array} + + # remap the mask exactly as plot_strain does, then use it both to select + # neighbors (> mask_threshold) and to weight the histogram / mean + low, high = float(mask_range[0]), float(mask_range[1]) + m = np.asarray(self.mask, dtype=float) + if high > low: + scaled = np.clip((m - low) / (high - low), 0.0, 1.0) + else: + scaled = (m >= high).astype(float) + valid = scaled > float(mask_threshold) + + # per-component local-median deviation, native units (fractional / radians) + dev = { + name: np.abs(field - _local_masked_median(field, valid, window, min_neighbors)) + for name, field in fields.items() + } + # single rotation-invariant number: Frobenius norm of the symmetric + # strain-tensor deviation (== root-sum-square of the principal-strain + # deviations). Rotation is reported separately, not folded in: in nanobeam + # data it is partly a systematic (tilt/descan) and would mix radians into a + # percent figure. + dev["combined"] = np.sqrt( + dev["e_uu"] ** 2 + dev["e_vv"] ** 2 + 2.0 * dev["e_uv"] ** 2 + ) + + # display-unit scaling: strain -> percent, rotation -> degrees + scale = { + "e_uu": 100.0, + "e_vv": 100.0, + "e_uv": 100.0, + "rotation": float(np.rad2deg(1.0)), + "combined": 100.0, + } + + # Precision = the weighted MEDIAN of each per-position deviation distribution, + # in display units. Restricted to trusted positions (valid == scaled > + # mask_threshold, the SAME set used to pick neighbors) and mask-weighted within + # it -- otherwise sub-threshold junk pixels, already excluded as neighbors, + # would leak in. The median sits at the center of the histogram bulk and is + # immune to the heavy outlier tail that a mean / RMS would chase out to the + # right (a few bad-fit pixels dominate a second moment but not the median). + def _weighted_median(err_native: np.ndarray, factor: float) -> float: + e = err_native * factor + use = np.isfinite(e) & valid + return _weighted_quantile(e[use], scaled[use], 0.5) + + precision = {name: _weighted_median(dev[name], scale[name]) for name in scale} + + # weighted histogram of the chosen component, over the same trusted positions + # as the median above. This is purely a picture of the common error values, so + # anything beyond the bin range is left OUT of the bars -- no overflow spike at + # the edge to crush the bulk. Nothing is lost: the median is computed from all + # trusted positions regardless. Bars are normalized by the total trusted + # weight, so each bar is the true fraction of all trusted positions and the + # off-range mass simply isn't drawn (the bars sum to 1 - out_of_range_fraction). + e = dev[component] * scale[component] + use = np.isfinite(e) & valid # trusted positions only, consistent with median + e_f = e[use] + w_f = scaled[use] + # Default histogram range: a robust weighted upper percentile, NOT the raw + # max. A handful of bad-fit positions can reach tens of percent; used as the + # range they crush the entire bulk into the first bin and leave the rest of + # the axis empty (a spurious "spike at 0" plus a far outlier spike). Capping + # at the weighted 99th percentile keeps the common error values readable; the + # few positions past it spill into out_of_range_fraction (reported, not + # drawn). An explicit `bounds`, or passing bin EDGES as `bins`, overrides it. + if bounds is None and np.ndim(bins) == 0 and e_f.size and float(w_f.sum()) > 0: + hi_default = _weighted_quantile(e_f, w_f, 0.99) + if np.isfinite(hi_default) and hi_default > 0: + bounds = (0.0, hi_default) + edges = np.histogram_bin_edges(e_f, bins=bins, range=bounds) + lo, hi = float(edges[0]), float(edges[-1]) + wtot = float(w_f.sum()) + frac_below = float(w_f[e_f < lo].sum()) / wtot if wtot > 0 else 0.0 + frac_above = float(w_f[e_f > hi].sum()) / wtot if wtot > 0 else 0.0 + out_of_range_fraction = frac_below + frac_above + counts_raw, edges = np.histogram(e_f, bins=edges, weights=w_f) + counts = counts_raw / wtot if wtot > 0 else counts_raw + + unit = "°" if component == "rotation" else "%" + result = { + "precision": precision, + "component": component, + "unit": unit, + "counts": counts, + "counts_raw": counts_raw, + "edges": edges, + "out_of_range_fraction": out_of_range_fraction, + "window": int(window), + "n_neighbors": n_neighbors, + "mask_threshold": float(mask_threshold), + "mask_range": (low, high), + "rotation_angle": float(rotation_angle), + } + + print("Strain precision (median local deviation, mask-weighted)") + print( + f" reference={n_neighbors} neighbors (disk, window={window}) " + f"mask>{mask_threshold:g} min_neighbors={min_neighbors} " + f"rotation_angle={rotation_angle:g} deg" + ) + for name in ("e_uu", "e_vv", "e_uv"): + print(f" {name:<9}: {precision[name]:7.4f} %") + print(f" {'rotation':<9}: {precision['rotation']:7.4f} deg") + print( + f" {'combined':<9}: {precision['combined']:7.4f} % " + "(strain-only Frobenius norm; rotation excluded)" + ) + + if not (plot or returnfig): + return result + + fig, ax = plot_strain_precision_histogram(edges, counts, precision, component, unit) + if returnfig: + return fig, ax + return result + + +# ---- module-level fitting functions ---- + + +def _weighted_quantile(values: np.ndarray, weights: np.ndarray, q: float) -> float: + """Weighted ``q``-quantile of ``values`` (``q`` in ``[0, 1]``); ``nan`` if no weight. + + Uses cumulative-weight interpolation with weights centered on each sorted sample, + so with uniform weights it tracks ``np.quantile``'s linear interpolation and is + robust to a heavy upper tail (the median ignores how far the outliers reach). + """ + values = np.asarray(values, dtype=float) + weights = np.asarray(weights, dtype=float) + total = float(weights.sum()) + if values.size == 0 or total <= 0: + return float("nan") + order = np.argsort(values) + v = values[order] + w = weights[order] + cw = np.cumsum(w) - 0.5 * w + return float(np.interp(q * total, cw, v)) + + +def _local_masked_median( + field: np.ndarray, + valid: np.ndarray, + window: int, + min_neighbors: int, +) -> np.ndarray: + """Median of each position's surrounding neighbors over valid (masked) pixels. + + The center position is excluded ("surrounding" only); a neighbor contributes + only where ``valid`` is True and the field is finite. Neighbors are taken over a + circular (isotropic) footprint of radius ``window / 2`` inscribed in the + ``window`` x ``window`` box — a disk avoids the square's far corners, which + over-weight the diagonals and sample the most strain-different points. Positions + left with fewer than ``min_neighbors`` contributing neighbors return ``nan``. + + Parameters + ---------- + field : np.ndarray + ``(scan_row, scan_col)`` field to take local medians of. + valid : np.ndarray + ``(scan_row, scan_col)`` boolean mask of usable neighbor positions. + window : int + Odd edge length of the bounding box; the footprint is the disk of radius + ``window / 2`` within it (3 -> 8 neighbors, 5 -> 20, 7 -> 36). + min_neighbors : int + Minimum contributing neighbors required, else ``nan``. + + Returns + ------- + np.ndarray + ``(scan_row, scan_col)`` local masked median (``nan`` where undefined). + """ + p = window // 2 + fpad = np.pad(np.asarray(field, dtype=float), p, mode="constant", constant_values=np.nan) + vpad = np.pad(np.asarray(valid, dtype=bool), p, mode="constant", constant_values=False) + + # writable per-position (window, window) neighborhoods + fw = sliding_window_view(fpad, (window, window)).copy() + vw = sliding_window_view(vpad, (window, window)) + fw[~vw] = np.nan + fw[:, :, p, p] = np.nan # exclude the center position from its own median + # restrict the square box to a circular footprint of radius window/2 + oy, ox = np.ogrid[-p : p + 1, -p : p + 1] + outside = (oy ** 2 + ox ** 2) > (window / 2.0) ** 2 + fw[:, :, outside] = np.nan + + flat = fw.reshape(fw.shape[0], fw.shape[1], -1) + count = np.sum(np.isfinite(flat), axis=-1) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=RuntimeWarning) + med = np.nanmedian(flat, axis=-1) + med[count < min_neighbors] = np.nan + return med + + +def _reference_lattice( + u_array: np.ndarray, + v_array: np.ndarray, + mask: np.ndarray | None = None, + strain_mask: np.ndarray | None = None, +) -> tuple[np.ndarray, np.ndarray]: + """Weighted-median reference lattice vectors, else the global median. + + The reference is the per-component **weighted median** of the lattice vectors. + Weights come from ``strain_mask`` if given, else the continuous ``mask`` + (the ``[0, 1]`` per-position weight from :meth:`create_mask` / ``fit_lattice``): + strong, well-indexed positions dominate the reference and weak / vacuum / bad-fit + positions are down-weighted. A boolean ROI (weights in ``{0, 1}``) reduces to the + plain median over the selected positions, so an explicit ``strain_mask`` behaves + as before. The weighted median (not ``mask == 1``) is used because a continuous + weight rarely hits *exactly* 1 -- the old exact-equality test collapsed a min-max + normalized mask to its single global-max position and made the reference one + arbitrary pixel. + + Parameters + ---------- + u_array : np.ndarray + Per-position first lattice vector, shape ``(scan_row, scan_col, 2)``. + v_array : np.ndarray + Per-position second lattice vector, shape ``(scan_row, scan_col, 2)``. + mask : np.ndarray, optional + ``(scan_row, scan_col)`` per-position weight in ``[0, 1]``. Used as the median + weights when ``strain_mask`` is not given. + strain_mask : np.ndarray, optional + ``(scan_row, scan_col)`` ROI / weight taking precedence over ``mask``. + + Returns + ------- + tuple of np.ndarray + ``(u_ref, v_ref)``, each a length-2 reference vector. + """ + if strain_mask is not None: + w = np.asarray(strain_mask, dtype=float).reshape(-1) + elif mask is not None: + w = np.asarray(mask, dtype=float).reshape(-1) + else: + w = None + + u_flat = u_array.reshape(-1, 2) + v_flat = v_array.reshape(-1, 2) + + def _wmed(vals: np.ndarray) -> float: + # weighted median over finite, positively-weighted positions; positions + # fit_lattice could not fit are NaN and must be dropped, else the reference + # (and the whole strain map) collapses to NaN. Falls back to the unweighted + # nan-median when no weight is given or none survives. + finite = np.isfinite(vals) + ww = np.ones_like(vals) if w is None else w + use = finite & (ww > 0) + if not use.any(): + return float(np.nanmedian(vals)) if finite.any() else float("nan") + return _weighted_quantile(vals[use], ww[use], 0.5) + + u_ref = np.array((_wmed(u_flat[:, 0]), _wmed(u_flat[:, 1])), dtype=float) + v_ref = np.array((_wmed(v_flat[:, 0]), _wmed(v_flat[:, 1])), dtype=float) + return u_ref, v_ref + + +def _strain_tensor( + u_array: np.ndarray, + v_array: np.ndarray, + u_ref: np.ndarray, + v_ref: np.ndarray, + real_space: bool, +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Per-position strain tensor from lattice vectors relative to a reference. + + Two measurement modalities are supported and are arranged to give *identical* + strain for the same physical deformation, so correlation (Bragg) and cepstral + (autocorrelation) maps can be compared directly: + + * ``real_space=False`` -- reciprocal-space lattice vectors (nanobeam Bragg + disks), which contract under tension. The per-position transform is + ``strain_trans = U_ref @ inv(U)``. + * ``real_space=True`` -- real-space lattice vectors (cepstral / Patterson + autocorrelation peaks, or DPC), which expand under tension. The transform is + ``strain_trans = (U @ inv(U_ref)).T``. + + Both expressions evaluate to ``F.T`` (the transpose of the real-space deformation + gradient), so the normal strains, shear, and rotation come out the same + regardless of modality, and the reciprocal-space sign convention (``const = -1``, + which sets the shear/rotation handedness) is shared. + + Parameters + ---------- + u_array : np.ndarray + Per-position first lattice vector, shape ``(scan_row, scan_col, 2)``. + v_array : np.ndarray + Per-position second lattice vector, shape ``(scan_row, scan_col, 2)``. + u_ref : np.ndarray + Reference first lattice vector (length 2). + v_ref : np.ndarray + Reference second lattice vector (length 2). + real_space : bool + ``False`` for reciprocal-space (Bragg/correlation) vectors; ``True`` for + real-space (cepstral autocorrelation / DPC) vectors. Selects the per-position + transform above; both yield matching strain. + + Returns + ------- + tuple of np.ndarray + ``(e_rr, e_cc, e_rc, phi)``, each of shape ``(scan_row, scan_col)``. + """ + scan_r, scan_c = u_array.shape[0], u_array.shape[1] + Uref = np.stack((u_ref, v_ref), axis=1).astype(float) + strain_trans = np.zeros((scan_r, scan_c, 2, 2)) + + # For real-space vectors the reference is inverted once (it is shared by every + # position); a non-finite or singular reference leaves the whole map undefined. + Uref_inv = None + if real_space and np.all(np.isfinite(Uref)) and abs(np.linalg.det(Uref)) >= 1e-12: + Uref_inv = np.linalg.inv(Uref) + + for r in range(scan_r): + for c in range(scan_c): + U = np.stack((u_array[r, c, :], v_array[r, c, :]), axis=1) + # Positions fit_lattice could not fit are NaN; a degenerate (collinear) + # fit is singular. Either way there is no meaningful inverse -- leave the + # strain NaN (masked out downstream) rather than feeding NaN into pinv, + # whose SVD does not converge and raises LinAlgError. + if not np.all(np.isfinite(U)) or abs(np.linalg.det(U)) < 1e-12: + strain_trans[r, c, :, :] = np.nan + continue + if real_space: + # real-space vectors expand under tension: (U @ U_ref^-1).T == F.T + if Uref_inv is None: + strain_trans[r, c, :, :] = np.nan + else: + strain_trans[r, c, :, :] = (U @ Uref_inv).T + else: + # reciprocal-space vectors contract under tension: U_ref @ U^-1 == F.T + strain_trans[r, c, :, :] = Uref @ np.linalg.inv(U) + + # const = -1 is the reciprocal-space (nanobeam) shear/rotation convention. Both + # modalities reduce strain_trans to F.T above, so the convention is shared. + const = 1 if real_space else -1 + e_rr = strain_trans[:, :, 0, 0] - 1 + e_cc = strain_trans[:, :, 1, 1] - 1 + e_rc = strain_trans[:, :, 1, 0] * 0.5 * const + strain_trans[:, :, 0, 1] * 0.5 * const + phi = strain_trans[:, :, 1, 0] * -0.5 * const + strain_trans[:, :, 0, 1] * 0.5 * const + return e_rr, e_cc, e_rc, phi + + +def _rotate_strain_tensor( + e_rr: np.ndarray, + e_cc: np.ndarray, + e_rc: np.ndarray, + rotation_angle: float, + real_space=False +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Rotate a 2D strain tensor by ``rotation_angle`` (degrees). + + Parameters + ---------- + e_rr : np.ndarray + Row-row (normal) strain component. + e_cc : np.ndarray + Column-column (normal) strain component. + e_rc : np.ndarray + Row-column (shear) strain component. + rotation_angle : float + Frame rotation angle, in degrees. + real_space: bool + Tells whether vector is defined in real or reciprocal space + Returns + ------- + tuple of np.ndarray + ``(e_uu, e_vv, e_uv)`` in the rotated frame. + """ + angle = np.deg2rad(rotation_angle) + c = np.cos(angle) + s = np.sin(angle) + sign = -1.0 if real_space else 1.0 + e_uu = e_rr * (c * c) + sign * 2.0 * e_rc * (c * s) + e_cc * (s * s) + e_vv = e_rr * (s * s) - sign * 2.0 * e_rc * (c * s) + e_cc * (c * c) + e_uv = sign * (e_cc - e_rr) * (c * s) + e_rc * (c * c - s * s) + return e_uu, e_vv, e_uv + +def _raw_vec_to_display(vec_rc: NDArray, *, rotation_ccw_deg: float, transpose: bool) -> NDArray: + """Map a raw-detector ``(row, col)`` vector into the rotated display frame. + + Applies the optional axis transpose, then a counter-clockwise rotation of + ``rotation_ccw_deg``. Inverse of :func:`_display_vec_to_raw`. + """ + v = np.asarray(vec_rc, dtype=float) + dr, dc = v[..., 0], v[..., 1] + + if transpose: + dr, dc = dc, dr + + theta = np.deg2rad(rotation_ccw_deg) + ct = np.cos(theta) + st = np.sin(theta) + + dr2 = ct * dr - st * dc + dc2 = st * dr + ct * dc + return np.stack((dr2, dc2), axis=-1) \ No newline at end of file diff --git a/src/quantem/diffraction/strain_visualization.py b/src/quantem/diffraction/strain_visualization.py new file mode 100644 index 000000000..17fa5adcd --- /dev/null +++ b/src/quantem/diffraction/strain_visualization.py @@ -0,0 +1,499 @@ +from __future__ import annotations + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.cm import ScalarMappable +from matplotlib.colors import Normalize +from matplotlib.patches import FancyArrowPatch +from matplotlib.ticker import FuncFormatter, MaxNLocator + +from quantem.core.visualization.visualization_utils import ScalebarConfig, add_scalebar_to_ax + + +def plot_strain_panels( + e_uu: np.ndarray, + e_vv: np.ndarray, + e_uv: np.ndarray, + rotation: np.ndarray, + mask: np.ndarray | None, + u_ref: np.ndarray | None, + v_ref: np.ndarray | None, + ds_shape: tuple[int, ...], + ds_sampling: float = 1.0, + ds_units: str = "pixels", + strain_range_percent: tuple[float, float] = (-3.0, 3.0), + rotation_range_degrees: tuple[float, float] = (-2.0, 2.0), + mask_range: tuple[float, float] = (0.0, 1.0), + roi: np.ndarray | None = None, + plot_rotation: bool = True, + plot_gvecs: bool = False, + plot_scalebar: bool = False, + cmap_strain: str = "RdBu_r", + cmap_rotation: str = "PiYG", + layout: str = "horizontal", + transpose_image: bool = False, + rotate_title: bool = False, + plot_dilation: bool = False, + figsize: tuple[float, float] | None = None, + panel_titles: tuple[str, str, str] | None = None, + strain_rotation_angle: float = 0.0, + arrow_style: str = "title", + **kwargs, +): + """Render strain (e_uu, e_vv, e_uv) and rotation panels. + + Strain arrays are fractional (multiplied by 100 for display); ``rotation`` is + in radians (converted to degrees for display). ``panel_titles`` overrides the + three strain-panel titles (e.g. to label the raw row/col reference frame). + + The mask modulates panel brightness (black where masked out). ``mask_range`` + ``(low, high)`` remaps it linearly before display: mask values ``>= high`` show + full color, ``<= low`` go black, and values between ramp from black to full. + The default ``(0.0, 1.0)`` leaves the already-normalized mask unchanged. + + When ``roi`` (a boolean ``(scan_row, scan_col)`` array) is given, positions + inside it are drawn in color and positions outside it in greyscale (the same + field, desaturated), so a chosen reference region stands out from its context. + """ + if mask is None: + mask = np.ones(ds_shape[:2]) + + # remap the mask brightness onto the [low, high] window: <= low -> black, + # >= high -> full color, linear between. default (0, 1) is a no-op. + low, high = float(mask_range[0]), float(mask_range[1]) + if high > low: + mask = np.clip((np.asarray(mask, dtype=float) - low) / (high - low), 0.0, 1.0) + else: + mask = (np.asarray(mask, dtype=float) >= high).astype(float) + + if cmap_rotation is None: + cmap_rotation = cmap_strain + + if layout not in ["horizontal", "vertical"]: + raise ValueError("layout must be 'horizontal' or 'vertical'") + + ncols = 4 if plot_rotation else 3 + is_horizontal = layout == "horizontal" + if plot_dilation: + ncols=3 + plot_rotation = True + + n_strain = 2 if plot_dilation else 3 + + if figsize is None: + figsize = (8, 3) if is_horizontal else (6, 6) + + if is_horizontal: + fig, ax = plt.subplots(1, ncols, figsize=figsize) + else: + fig, ax = plt.subplots(ncols, 1, figsize=figsize) + + cm_strain = plt.get_cmap(cmap_strain).copy() + cm_strain.set_bad(color="black") + cm_rot = plt.get_cmap(cmap_rotation).copy() + cm_rot.set_bad(color="black") + + euu_pct = e_uu * 100 + evv_pct = e_vv * 100 + euv_pct = e_uv * 100 + rot_deg = np.rad2deg(rotation) + + roi_bool = None if roi is None else np.asarray(roi).astype(bool) + gray_cm = plt.get_cmap("gray").copy() + gray_cm.set_bad(color="black") + + def _roi_compose(norm_vals, color_cm): + """Color the field inside the ROI; show it in greyscale outside the ROI.""" + rgb = color_cm(norm_vals)[:, :, :3] + if roi_bool is None: + return rgb + rgb_gray = gray_cm(norm_vals)[:, :, :3] + return np.where(roi_bool[:, :, np.newaxis], rgb, rgb_gray) + + norm_strain = Normalize(vmin=strain_range_percent[0], vmax=strain_range_percent[1]) + euu_disp = _roi_compose(norm_strain(euu_pct), cm_strain) + evv_disp = _roi_compose(norm_strain(evv_pct), cm_strain) + euv_disp = _roi_compose(norm_strain(euv_pct), cm_strain) + + if transpose_image: + euu_disp = euu_disp.transpose(1,0,2) + evv_disp = evv_disp.transpose(1,0,2) + euv_disp = euv_disp.transpose(1,0,2) + mask = mask.T + + if plot_dilation: + etot_pct = (e_uu + e_vv) * 100 + etot_disp = _roi_compose(norm_strain(etot_pct), cm_strain) + if transpose_image: + etot_disp = etot_disp.transpose(1,0,2) + ax[0].imshow(etot_disp * mask[:, :, np.newaxis]) + ax[1].imshow(euv_disp * mask[:, :, np.newaxis]) + else: + ax[0].imshow(euu_disp * mask[:, :, np.newaxis]) + ax[1].imshow(evv_disp * mask[:, :, np.newaxis]) + ax[2].imshow(euv_disp * mask[:, :, np.newaxis]) + + + ref_dim = figsize[1] if is_horizontal else figsize[0] + fs_threshold = 3.0 + fs_scale = min(1.0, max(0.5, ref_dim / fs_threshold)) + title_fs = 16 * fs_scale + tick_fs = 12 * fs_scale + title_val = 'vertical' if rotate_title else 'horizontal' + if panel_titles is None: + if plot_dilation: + panel_titles = ( + r"$\epsilon_{uu} + \epsilon_{vv}$", + r"$\epsilon_{uv}$", + "", + ) + title_arrow_angles = (None, -45 + strain_rotation_angle, None) + else: + panel_titles = ( + r"$\epsilon_{uu}$", + r"$\epsilon_{vv}$", + r"$\epsilon_{uv}$", + ) + title_arrow_angles = (0 + strain_rotation_angle, 90 + strain_rotation_angle, -45 + strain_rotation_angle) + else: + title_arrow_angles = (None, None, None) + + + if plot_rotation: + norm_rot = Normalize(vmin=rotation_range_degrees[0], vmax=rotation_range_degrees[1]) + rot_disp = _roi_compose(norm_rot(rot_deg), cm_rot) + if transpose_image: rot_disp = rot_disp.transpose(1,0,2) + ax[-1].imshow(rot_disp * mask[:, :, np.newaxis]) + if arrow_style == "title": + ax[-1].set_title(r"$\phi$ $\circlearrowleft$", fontsize=title_fs, rotation=title_val) + else: + ax[-1].set_title(r"$\phi$", fontsize=title_fs, rotation=title_val) + + + for a in ax: + a.set_xticks([]) + a.set_yticks([]) + a.set_facecolor("black") + a.set_aspect("equal") + a.set_anchor("W" if not is_horizontal else "C") + + if plot_scalebar: + scalebar_kwargs = {} + for key, value in kwargs.items(): + if key.startswith("scalebar_"): + scalebar_key = key[len("scalebar_"):] + scalebar_kwargs[scalebar_key] = value + + # default: white bar on a translucent black box, readable on the + # diverging strain colormaps; override with scalebar_color / + # scalebar_box / scalebar_box_color / scalebar_box_alpha + box = scalebar_kwargs.pop("box", True) + box_color = scalebar_kwargs.pop("box_color", "black") + box_alpha = scalebar_kwargs.pop("box_alpha", 0.45) + scalebar_defaults = { + "sampling": ds_sampling, + "units": ds_units, + "length": None, + "width_px": 1, + "pad_px": 0.5, + "color": "white" if box and box_color == "black" else "black", + "loc": "lower left", + "fontsize": 12, + "bold": True, + } + scalebar_defaults.update(scalebar_kwargs) + scalebar_config = ScalebarConfig(**scalebar_defaults) + add_scalebar_to_ax( + ax[0], + array_size=int(ds_shape[0]), + sampling=scalebar_config.sampling, + length_units=scalebar_config.length, + units=scalebar_config.units, + width_px=scalebar_config.width_px, + pad_px=scalebar_config.pad_px, + color=scalebar_config.color, + loc=scalebar_config.loc, + fontsize=scalebar_config.fontsize, + bold=scalebar_config.bold, + box=box, + box_color=box_color, + box_alpha=box_alpha, + ) + + cb_size = 0.02 + cb_pad = 0.03 + cb_min_len = 0.16 + + def _finalize_layout(): + # set_aspect("equal") only resizes/recenters each panel at draw time, so + # get_position() before a draw returns stale boxes -- placing the colorbars + # and g-vector compass off those boxes then spills them off the figure. + # Settle the layout cheaply first so every box read below is the real one. + try: + fig.draw_without_rendering() + except AttributeError: # matplotlib < 3.5 + fig.canvas.draw() + + need_side_panel = plot_gvecs or arrow_style == "legend" + if is_horizontal: + # Reserve a bottom band wide enough for the colorbar + its tick labels and + # title (fontsize 16) and a right band for the rotation-panel gap; widen the + # right band when the g-vector compass is drawn in it. These keep the figure + # usable when saved "as is" (no bbox_inches='tight'). + right = 0.72 if need_side_panel else 0.93 + fig.subplots_adjust(left=0.04, right=right, top=0.88, bottom=0.24, wspace=0.05) + if plot_rotation: + # nudge the rotation panel right for a visual gap from the strain panels; + # 0.03 stays inside the reserved right band so nothing is clipped. + pos3 = ax[-1].get_position() + ax[-1].set_position([pos3.x0 + 0.03, pos3.y0, pos3.width, pos3.height]) + _finalize_layout() + + cb_orientation = "horizontal" + b0 = ax[0].get_position() + b2 = ax[n_strain - 1].get_position() + cb_y = b2.y0 - cb_pad - cb_size + strain_cb_pos = [b0.x0, cb_y, b2.x1 - b0.x0, cb_size] + + if plot_rotation: + b3 = ax[-1].get_position() + rot_cb_w = max(b3.x1 - b3.x0, cb_min_len) + rot_cb_cx = 0.5 * (b3.x0 + b3.x1) + rot_cb_x0 = min(max(rot_cb_cx - 0.5 * rot_cb_w, 0.0), 0.99 - rot_cb_w) + rot_cb_pos = [rot_cb_x0, cb_y, rot_cb_w, cb_size] + last_pos = b3 + else: + rot_cb_pos = None + last_pos = b2 + + else: + # Top band for the panel titles, right band for the vertical colorbars + labels. + right = 0.55 if need_side_panel else 0.80 + fig.subplots_adjust(left=0.04, right=right, top=0.92, bottom=0.06, hspace=0.15) + _finalize_layout() + + cb_orientation = "vertical" + b0 = ax[0].get_position() + b2 = ax[n_strain - 1].get_position() + title_gap = 0.15 if arrow_style == "title" else cb_pad + cb_x0 = b0.x1 + title_gap + strain_cb_pos = [cb_x0, b2.y0, cb_size, b0.y1 - b2.y0] + + if plot_rotation: + b3 = ax[-1].get_position() + rot_cb_h = max(b3.y1 - b3.y0, cb_min_len) + rot_cb_cy = 0.5 * (b3.y0 + b3.y1) + rot_cb_y0 = min(max(rot_cb_cy - 0.5 * rot_cb_h, 0.0), 0.99 - rot_cb_h) + rot_cb_pos = [cb_x0, rot_cb_y0, cb_size, rot_cb_h] + last_pos = b3 + else: + rot_cb_pos = None + last_pos = b2 + + cax1 = fig.add_axes(strain_cb_pos) + sm_strain = ScalarMappable(norm=norm_strain, cmap=cm_strain) + cbar1 = fig.colorbar(sm_strain, cax=cax1, orientation=cb_orientation) + cbar1.set_label("Strain", fontsize=title_fs) + cbar1.formatter = FuncFormatter(lambda v, _pos: f"{v:g}%") + cbar1.update_ticks() + cbar1.ax.tick_params(labelsize=tick_fs) + + if plot_rotation and rot_cb_pos is not None: + cax2 = fig.add_axes(rot_cb_pos) + sm_rot = ScalarMappable(norm=norm_rot, cmap=cm_rot) + cbar2 = fig.colorbar(sm_rot, cax=cax2, orientation=cb_orientation) + cbar2.set_label("Rotation", fontsize=title_fs) + cbar2.formatter = FuncFormatter(lambda v, _pos: f"{v:g}°") + cbar2.locator = MaxNLocator(nbins=2) + cbar2.update_ticks() + cbar2.ax.tick_params(labelsize=tick_fs) + + + def _add_title_arrow(ax, angle_deg, gap_pt=4.0, color="black", fontsize=None): + fs = fontsize if fontsize is not None else title_fs + try: + ax.figure.draw_without_rendering() + except AttributeError: # matplotlib < 3.5 + ax.figure.canvas.draw() + renderer = ax.figure.canvas.get_renderer() + bbox_ax = ax.title.get_window_extent(renderer=renderer).transformed(ax.transAxes.inverted()) + y = 0.5 * (bbox_ax.y0 + bbox_ax.y1) + ax.annotate( + "\u2194", + xy=(bbox_ax.x1, y), xycoords=ax.transAxes, + xytext=(gap_pt + fs / 2.0, 0), textcoords="offset points", + ha="center", va="center", + rotation=angle_deg, rotation_mode="anchor", + fontsize=fs, color=color, + annotation_clip=False, + ) + + def _add_arrow_legend(fig, x0, y_top, entries, plot_rotation, fontsize, color="black"): + fig_w_in, fig_h_in = figsize + row_h_in = fontsize * 1.6 / 72.0 + box_w_in = 1.6 + n_rows = len(entries) + 1 + (2 if plot_rotation else 0) + row_h = row_h_in / fig_h_in + box_h = row_h * n_rows + box_w = min(box_w_in / fig_w_in, 0.99 - x0) + leg_ax = fig.add_axes([x0, y_top - box_h, box_w, box_h]) + leg_ax.set_xlim(0, 1) + leg_ax.set_ylim(0, 1) + leg_ax.axis("off") + + dy = 1.0 / n_rows + y = 1.0 - dy / 2 + leg_ax.text(0.0, y, "Strain", fontsize=fontsize, fontweight="bold", ha="left", va="center") + for label, angle_deg in entries: + y -= dy + leg_ax.text(0.15, y, "\u2194", rotation=angle_deg, rotation_mode="anchor", + ha="center", va="center", fontsize=fontsize, color=color) + leg_ax.text(0.32, y, label, fontsize=fontsize, ha="left", va="center") + + if plot_rotation: + y -= dy + leg_ax.text(0.0, y, "Rotation", fontsize=fontsize, fontweight="bold", ha="left", va="center") + y -= dy + leg_ax.text(0.15, y, "\u21ba", fontsize=fontsize, ha="center", va="center") + leg_ax.text(0.32, y, r"$\phi$", fontsize=fontsize, ha="left", va="center") + return box_h + + for i in range(n_strain): + ax[i].set_title(panel_titles[i], fontsize=title_fs, rotation=title_val) + angle = title_arrow_angles[i] + if arrow_style == "title" and angle is not None: + _add_title_arrow(ax[i], angle, color="black") + + if is_horizontal: + _finalize_layout() + renderer = fig.canvas.get_renderer() + panel_edge = last_pos.x1 + if plot_rotation: + title_edge = ax[-1].title.get_window_extent(renderer=renderer).transformed(fig.transFigure.inverted()).x1 + panel_edge = max(panel_edge, title_edge) + margin_x0 = panel_edge + 0.03 + else: + _finalize_layout() + renderer = fig.canvas.get_renderer() + margin_x0 = cax1.get_tightbbox(renderer).transformed(fig.transFigure.inverted()).x1 + 0.02 + if plot_rotation and rot_cb_pos is not None: + rot_edge = cax2.get_tightbbox(renderer).transformed(fig.transFigure.inverted()).x1 + margin_x0 = max(margin_x0, rot_edge + 0.02) + top_bound = 0.88 if is_horizontal else 0.92 + bottom_bound = 0.24 if is_horizontal else 0.06 + center_y = 0.5 * (top_bound + bottom_bound) + + entries = [] + leg_h = 0.0 + if arrow_style == "legend": + entries = [(panel_titles[i], title_arrow_angles[i]) for i in range(n_strain) + if title_arrow_angles[i] is not None] + n_rows = len(entries) + 1 + (2 if plot_rotation else 0) + leg_h = (title_fs * 1.6 / 72.0 / figsize[1]) * n_rows + + show_gvecs = plot_gvecs and u_ref is not None and v_ref is not None + if plot_gvecs and not show_gvecs: + print("Warning: u_ref and v_ref not found. Call fit_strain() first.") + fig_aspect = figsize[0] / figsize[1] + gvec_w = min(0.99 - margin_x0, 0.15) if show_gvecs else 0.0 + gvec_h = gvec_w * fig_aspect if show_gvecs else 0.0 + + gap = 0.03 if (leg_h > 0 and gvec_h > 0) else 0.0 + total_needed = leg_h + gap + gvec_h + available_span = top_bound - bottom_bound + side_scale = min(1.0, available_span / total_needed) if total_needed > 0 else 1.0 + leg_h *= side_scale + gvec_w *= side_scale + gvec_h *= side_scale + legend_fontsize = title_fs * side_scale + + y_top = center_y + (leg_h + gap * side_scale + gvec_h) / 2.0 + + if leg_h > 0: + _add_arrow_legend(fig, margin_x0, y_top, entries, plot_rotation=plot_rotation, + fontsize=legend_fontsize, color="black") + y_top -= leg_h + gap * side_scale + + if show_gvecs: + ref_ax = fig.add_axes([margin_x0, y_top - gvec_h, gvec_w, gvec_h]) + ref_ax.set_xlim(-1.5, 1.5) + ref_ax.set_ylim(-1.5, 1.5) + ref_ax.set_aspect("equal") + ref_ax.axis("off") + u_norm = u_ref / np.linalg.norm(u_ref) + v_norm = v_ref / np.linalg.norm(v_ref) + u_row, u_col = u_norm + v_row, v_col = v_norm + arrow_props_ref = dict(arrowstyle="->", lw=3, mutation_scale=25) + ref_ax.add_patch(FancyArrowPatch((0, 0), (u_col, -u_row), color="darkred", **arrow_props_ref)) + ref_ax.add_patch(FancyArrowPatch((0, 0), (v_col, -v_row), color="darkblue", **arrow_props_ref)) + ref_ax.text(u_col * 1.3, -u_row * 1.3, r"$\mathbf{g}_{1}$", fontsize=14, fontweight="bold", + color="darkred", ha="center", va="center") + ref_ax.text(v_col * 1.3, -v_row * 1.3, r"$\mathbf{g}_{2}$", fontsize=14, fontweight="bold", + color="darkblue", ha="center", va="center") + + return fig, ax + + +def plot_strain_precision_histogram( + edges: np.ndarray, + counts: np.ndarray, + precision: dict[str, float], + component: str, + unit: str, + *, + figsize: tuple[float, float] = (6.0, 4.0), +): + """Weighted histogram of the local-deviation strain precision. + + ``edges``/``counts`` describe the (mask-weighted, normalized) distribution of the + chosen ``component`` deviation in display units (``unit``). ``precision`` is the + weighted-median local deviation per component (used for the annotation box); the + plotted component's median is marked with a solid line. + """ + fig, ax = plt.subplots(figsize=figsize) + edges = np.asarray(edges, dtype=float) + counts = np.asarray(counts, dtype=float) + centers = 0.5 * (edges[:-1] + edges[1:]) + widths = np.diff(edges) + + ax.bar(centers, counts, width=widths, align="center", + color="#4C72B0", edgecolor="white", linewidth=0.3) + + median_value = precision[component] + if np.isfinite(median_value): + ax.axvline(median_value, color="crimson", ls="-", lw=2) + # label the line inline -- a legend box here would sit on top of the info box. + # Put the text on whichever side of the line keeps it clear of the right box. + span = float(edges[-1] - edges[0]) + on_right = span > 0 and (median_value - edges[0]) / span > 0.5 + ax.annotate( + f"median = {median_value:.3g} {unit}", + xy=(median_value, 0.96), xycoords=("data", "axes fraction"), + xytext=(-6 if on_right else 6, 0), textcoords="offset points", + ha="right" if on_right else "left", va="top", + color="crimson", fontsize=9, + ) + + label = "combined" if component == "combined" else component + ax.set_xlabel(f"{label} deviation ({unit})", fontsize=12) + ax.set_ylabel("weighted fraction", fontsize=12) + ax.set_title("Strain precision (median local deviation)", fontsize=13) + ax.tick_params(labelsize=10) + + annotation = "\n".join( + [ + r"median:", + rf" $\epsilon_{{uu}}$: {precision['e_uu']:.3g} %", + rf" $\epsilon_{{vv}}$: {precision['e_vv']:.3g} %", + rf" $\epsilon_{{uv}}$: {precision['e_uv']:.3g} %", + rf" rotation: {precision['rotation']:.3g} °", + rf" combined: {precision['combined']:.3g} %", + ] + ) + ax.text(0.97, 0.97, annotation, transform=ax.transAxes, ha="right", va="top", + fontsize=9, family="monospace", + bbox=dict(boxstyle="round", fc="white", ec="0.7", alpha=0.9)) + + fig.tight_layout() + return fig, ax \ No newline at end of file diff --git a/src/quantem/diffraction/wk_scattering_factors.py b/src/quantem/diffraction/wk_scattering_factors.py new file mode 100644 index 000000000..09419e127 --- /dev/null +++ b/src/quantem/diffraction/wk_scattering_factors.py @@ -0,0 +1,588 @@ +import numpy as np +from scipy.special import expi + +# from functools import lru_cache + +from quantem.core.utils.utils import electron_wavelength_angstrom + +""" +Weickenmeier-Kohl absorptive electron scattering factors. + +Elastic form factors use the 8-parameter fit of Weickenmeier & Kohl, +Acta Cryst. A47, 590 (1991); the absorptive (core-loss and phonon/TDS) +parts are computed analytically from the same fit. This implementation +was adapted by SE Zeltmann for py4DSTEM from EMsoftLib/others.f90 by +Marc De Graef, who adapted it from Weickenmeier's original F77 code; +vendored here from py4DSTEM with only the import adjusted. +""" + + +def compute_WK_factor( + g: np.ndarray, + Z: int, + accelerating_voltage: float, + thermal_sigma: float = None, + include_core: bool = True, + include_phonon: bool = True, + verbose=False, +) -> np.complex128: + """ + Compute the Weickenmeier-Kohl atomic scattering factors, using the parameterization + of the elastic part and computation of the inelastic part found in EMsoftLib/others.f90. + Return value should be in Å. + + This implementation always returns the absorptive, relativistically corrected factors. + + Currently this is mostly a direct translation of the Fortran code, along with + the accompanying comments from the original in quotation marks. Colin Ophus + vectorized it around v0.13.17. Currently it is only vectorized over `g` (i.e. + `Z` and all other args must be a single value.) + + This method uses an 8-parameter fit to the elastic form factors, and then computes the + absorptive form factors using an analytic solution based on that fitting function. + + Args: (note that these values cannot be arrays: the code is not vectorized) + g (float/ndarray): Scattering vector magnitude in the crystallographic/py4DSTEM + convention, 1/d_hkl in units of 1/Å + Z (int): Atomic number. Data are available for H thru Cf (1 thru 98) + accelerating_voltage (float): Accelerating voltage in eV. + thermal_sigma (float): RMS atomic displacement for TDS, in Å + (This is often written as 〈u〉in papers) + include_core (bool): If True, include the core loss contribution to the absorptive + form factors. + include_phonon (bool): If True, include the phonon/TDS contribution to the + absorptive form factors. + Returns: + Fscatt (np.complex128): The computed atomic form factor + """ + + # the WK Fortran code works in weird units: + # lowercase "g", our input, is the standard crystallographic quantity, in Å^-1 + # uppercase "G" is the "G" in others.f90:FSCATT, g * 2π + # uppercase "S" is the "S" in others.f90:FSCATT, G / 4π = g / 2 + G = g * 2.0 * np.pi + S = g / 2.0 + + if verbose: + print(f"S:{S}") + + accelerating_voltage_kV = accelerating_voltage / 1.0e3 + + if thermal_sigma is not None: + UL = thermal_sigma + DWF = np.exp(-0.5 * UL**2 * G**2) + else: + UL = 0.0 + DWF = 1.0 + + if verbose: + print(f"DWF:{DWF}") + + A = WK_A_param[int(Z) - 1] + B = WK_B_param[int(Z) - 1] + + if verbose: + print(f"A:{A}") + print(f"B:{B}") + + # WEKO(A,B,S) + # NOTE: the py4DSTEM version this was vendored from used `argu >= 1.0` + # for the middle branch, silently dropping every term with argu in + # [0.1, 1) and producing non-monotonic form factors at small g. The + # original EMsoftLib/others.f90 WEKO uses ARGU >= 0.1, restored here. + WK = np.zeros_like(S) + for i in range(4): + argu = B[i] * S**2 + sub = argu < 0.1 + WK[sub] += A[i] * B[i] * (1.0 - 0.5 * argu[sub]) + sub = np.logical_and(argu >= 0.1, argu <= 20.0) + WK[sub] += A[i] * (1.0 - np.exp(-argu[sub])) / S[sub] ** 2 + sub = argu > 20.0 + WK[sub] += A[i] / S[sub] ** 2 + + Freal = 4.0 * np.pi * DWF * WK + + if verbose: + print(f"Freal:{Freal}") + + ################################################# + # calculate "core" contribution, following FCORE: + k0 = ( + 2.0 * np.pi / electron_wavelength_angstrom(accelerating_voltage) + ) # remember, physicist units here + + if include_core: + # "CALCULATE CHARACTERISTIC ENERGY LOSS AND ANGLE" + DE = 6.0e-3 * Z + theta_e = ( + DE + / (2.0 * accelerating_voltage_kV) + * (2.0 * accelerating_voltage_kV + 1022.0) + / (accelerating_voltage_kV + 1022.0) + ) + + # "SCREENING PARAMETER OF YUKAWA POTENTIAL" + R = 0.885 * 0.5289 / Z ** (1.0 / 3.0) + + # "CALCULATE NORMALISING ANGLE" + TA = 1.0 / (k0 * R) + + # "CALCULATE BRAGG ANGLE" + TB = G / (2.0 * k0) + + # "NORMALIZE" + OMEGA = 2.0 * TB / TA + KAPPA = theta_e / TA + + K2 = KAPPA * KAPPA + O2 = OMEGA * OMEGA + + X1 = ( + OMEGA + / ((1.0 + O2) * np.sqrt(O2 + 4.0 * K2)) + * np.log((OMEGA + np.sqrt(O2 + 4.0 * K2)) / (2.0 * KAPPA)) + ) + X2 = ( + 1.0 + / np.sqrt((1.0 + O2) * (1.0 + O2) + 4.0 * K2 * O2) + * np.log( + (1.0 + 2.0 * K2 + O2 + np.sqrt((1.0 + O2) * (1.0 + O2) + 4.0 * K2 * O2)) + / (2.0 * KAPPA * np.sqrt(1.0 + K2)) + ) + ) + + X3 = np.zeros_like(OMEGA) + sub = OMEGA > 1e-2 + X3[sub] = ( + 1.0 + / (OMEGA[sub] * np.sqrt(O2[sub] + 4.0 * (1.0 + K2))) + * np.log( + (OMEGA[sub] + np.sqrt(O2[sub] + 4.0 * (1.0 + K2))) + / (2.0 * np.sqrt(1.0 + K2)) + ) + ) + sub = np.logical_not(sub) + X3[sub] = 1.0 / (4.0 * (1.0 + K2)) + + HI = 2 * Z / (TA * TA) * (-X1 + X2 - X3) + + A0 = 0.5289 + Fcore = 4.0 / (A0 * A0) * 2.0 * np.pi / (k0 * k0) * HI + + if verbose: + print(f"Fcore:{Fcore}") + else: + Fcore = 0.0 + + ########################################################## + # calculate phonon contribution, following FPHON(G,UL,A,B) + Fphon = 0.0 + if include_phonon: + U2 = UL**2 + + A1 = A * (4.0 * np.pi) ** 2 + B1 = B / (4.0 * np.pi) ** 2 + + for jj in range(4): + for ii in range(jj + 1): + Fphon += ( + (2.0 if jj != ii else 1.0) + * A1[jj] + * A1[ii] + * (DWF * RI1(B1[ii], B1[jj], G) - RI2(B1[ii], B1[jj], G, UL)) + ) + if verbose: + print(f"Fphon:{Fphon}") + + Fimag = (Fcore * DWF) + Fphon + + # perform relativistic correction + gamma = (accelerating_voltage_kV + 511.0) / (511.0) + + if verbose: + print(f"gamma:{gamma}") + + Fscatt = np.complex128((Freal * gamma) + (1.0j * (Fimag * gamma**2 / k0))) + + if verbose: + print(f"Fscatt:{Fscatt}") + + return ( + Fscatt * 0.4787801 * 0.664840340614319 / (4.0 * np.pi) + ) # convert to Å, and remove extra physicist factors, as performed in diffraction.f90:427,576,630 + + +############################################## +# Helper integral functions for DW calculation + + +def RI1(BI, BJ, G): + # "ERSTES INTEGRAL FUER DIE ABSORPTIONSPOTENTIALE" + eps = np.max([BI, BJ]) * G**2 + + ri1 = np.zeros_like(G) + + sub = eps <= 0.1 + ri1[sub] = np.pi * (BI * np.log((BI + BJ) / BI) + BJ * np.log((BI + BJ) / BJ)) + + sub = np.logical_and(eps <= 0.1, G > 0.0) + temp = 0.5 * BI**2 * np.log(BI / (BI + BJ)) + 0.5 * BJ**2 * np.log(BJ / (BI + BJ)) + temp += 0.75 * (BI**2 + BJ**2) - 0.25 * (BI + BJ) ** 2 + temp -= 0.5 * (BI - BJ) ** 2 + ri1[sub] += np.pi * G[sub] ** 2 * temp + + sub = eps > 0.1 + ri1[sub] = ( + 2.0 * 0.5772157 + + np.log(BI * G[sub] ** 2) + + np.log(BJ * G[sub] ** 2) + - 2.0 * expi(-BI * BJ * G[sub] ** 2 / (BI + BJ)) + ) + + ri1[sub] += RIH1( + BI * G[sub] ** 2, BI * G[sub] ** 2 * BI / (BI + BJ), BI * G[sub] ** 2 + ) + + ri1[sub] += RIH1( + BJ * G[sub] ** 2, BJ * G[sub] ** 2 * BJ / (BI + BJ), BJ * G[sub] ** 2 + ) + ri1[sub] *= np.pi / G[sub] ** 2 + + return ri1 + + +def RI2(BI, BJ, G, U): + # "ZWEITES INTEGRAL FUER DIE ABSORPTIONSPOTENTIALE" + U2 = U**2 + U22 = 0.5 * U2 + G2 = G**2 + BIUH = BI + 0.5 * U2 + BJUH = BJ + 0.5 * U2 + BIU = BI + U2 + BJU = BJ + U2 + + # "IST DIE ASYMPTOTISCHE ENTWICKLUNG ANWENDBAR?"" + EPS = np.max([BI, BJ, U2]) + EPS = EPS * G2 + + ri2 = np.zeros_like(G) + + sub = EPS <= 0.1 + ri2[sub] = (BI + U2) * np.log((BI + BJ + U2) / (BI + U2)) + BJ * np.log( + (BI + BJ + U2) / (BJ + U2) + ) + if U2 > 0.0: + ri2[sub] += U2 * np.log(U2 / (BJ + U2)) + ri2[sub] *= np.pi + + if U2 > 0.0: + TEMP = 0.5 * U22 * U22 * np.log(BIU * BJU / (U2 * U2)) + else: + TEMP = 0.0 + TEMP = TEMP + 0.5 * BIUH * BIUH * np.log(BIU / (BIUH + BJUH)) + TEMP = TEMP + 0.5 * BJUH * BJUH * np.log(BJU / (BIUH + BJUH)) + TEMP = TEMP + 0.25 * BIU * BIU + 0.5 * BI * BI + TEMP = TEMP + 0.25 * BJU * BJU + 0.5 * BJ * BJ + TEMP = TEMP - 0.25 * (BIUH + BJUH) * (BIUH + BJUH) + TEMP = TEMP - 0.5 * ((BI * BIU - BJ * BJU) / (BIUH + BJUH)) ** 2 + TEMP = TEMP - U22 * U22 + ri2[sub] += np.pi * G2[sub] * TEMP + + sub = EPS > 0.1 + ri2[sub] = expi(-0.5 * U2 * G2[sub] * BIUH / BIU) + expi( + -0.5 * U2 * G2[sub] * BJUH / BJU + ) + ri2[sub] -= expi(-BIUH * BJUH * G2[sub] / (BIUH + BJUH)) + expi( + -0.25 * U2 * G2[sub] + ) + ri2[sub] *= 2.0 + X1 = 0.5 * U2 * G2[sub] + X2 = 0.25 * U2 * G2[sub] + X3 = 0.25 * U2 * U2 * G2[sub] / BIU + ri2[sub] += RIH1(X1, X2, X3) + + X1 = 0.5 * U2 * G2[sub] + X2 = 0.25 * U2 * G2[sub] + X3 = 0.25 * U2 * U2 * G2[sub] / BJU + ri2[sub] += RIH1(X1, X2, X3) + + X1 = BIUH * G2[sub] + X2 = BIUH * BIUH * G2[sub] / (BIUH + BJUH) + X3 = BIUH * BIUH * G2[sub] / BIU + ri2[sub] += RIH1(X1, X2, X3) + + X1 = BJUH * G2[sub] + X2 = BJUH * BJUH * G2[sub] / (BIUH + BJUH) + X3 = BJUH * BJUH * G2[sub] / BJU + ri2[sub] += RIH1(X1, X2, X3) + + ri2[sub] *= np.pi / G2[sub] + + return ri2 + + +def RIH1(X1, X2, X3): + # "WERTET DEN AUSDRUCK EXP(-X1) * ( EI(X2)-EI(X3) ) AUS" + rih1 = np.zeros(X1.shape) + + sub = np.logical_and(X2 <= 20.0, X3 <= 20.0) + rih1[sub] = np.exp(-X1[sub]) * (expi(X2[sub]) - expi(X3[sub])) + + sub = np.logical_and(X2 > 20.0, X3 <= 20.0) + rih1[sub] = np.exp(X2[sub] - X1[sub]) * RIH2(X2[sub]) / X2[sub] - np.exp( + -X1[sub] + ) * expi(X3[sub]) + + sub = np.logical_and(X2 <= 20.0, X3 > 20.0) + rih1[sub] = ( + np.exp(-X1[sub]) * expi(X2[sub]) + - np.exp(X3[sub] - X1[sub]) * RIH2(X3[sub]) / X3[sub] + ) + + sub = np.logical_and(X2 > 20.0, X3 > 20.0) + rih1[sub] = ( + np.exp(X2[sub] - X1[sub]) * RIH2(X2[sub]) / X2[sub] + - np.exp(X3[sub] - X1[sub]) * RIH2(X3[sub]) / X3[sub] + ) + + return rih1 + + +def RIH2(X): + """ + WERTET X*EXP(-X)*EI(X) AUS FUER GROSSE X + DURCH INTERPOLATION DER TABELLE ... AUS ABRAMOWITZ + """ + idx = np.floor(200.0 / X).astype("int") + + sig = RIH2_tabulated_data[idx] + 200.0 * ( + RIH2_tabulated_data[idx + 1] - RIH2_tabulated_data[idx] + ) * ((1.0 / X) - 0.5e-3 * idx) + + return sig + + +# NOTE - This function is present in EMSoftLib but apparently not used. +def RIH3(X): + # "WERTET DEN AUSDRUCK EXP(-X) * EI(X) AUS" + if X <= 20.0: + return np.exp(-X) * expi(X) + else: + return RIH2(X) / X + + +################## +# TABULATED DATA # +################## + +# fmt:off + +RIH2_tabulated_data = np.array([1.000000,1.005051,1.010206,1.015472,1.020852, + 1.026355,1.031985,1.037751,1.043662,1.049726, + 1.055956,1.062364,1.068965,1.075780,1.082830, + 1.090140,1.097737,1.105647,1.113894,1.122497, + 1.131470]) + + +WK_A_param = np.array([ + 0.00427, 0.00957, 0.00802, 0.00209, + 0.01217, 0.02616,-0.00884, 0.01841, + 0.00251, 0.03576, 0.00988, 0.02370, + 0.01596, 0.02959, 0.04024, 0.01001, + 0.03652, 0.01140, 0.05677, 0.01506, + 0.04102, 0.04911, 0.05296, 0.00061, + 0.04123, 0.05740, 0.06529, 0.00373, + 0.03547, 0.03133, 0.10865, 0.01615, + 0.03957, 0.07225, 0.09581, 0.00792, + 0.02597, 0.02197, 0.13762, 0.05394, + 0.03283, 0.08858, 0.11688, 0.02516, + 0.03833, 0.17124, 0.03649, 0.04134, + 0.04388, 0.17743, 0.05047, 0.03957, + 0.03812, 0.17833, 0.06280, 0.05605, + 0.04166, 0.17817, 0.09479, 0.04463, + 0.04003, 0.18346, 0.12218, 0.03753, + 0.04245, 0.17645, 0.15814, 0.03011, + 0.05011, 0.16667, 0.17074, 0.04358, + 0.04058, 0.17582, 0.20943, 0.02922, + 0.04001, 0.17416, 0.20986, 0.05497, + 0.09685, 0.14777, 0.20981, 0.04852, + 0.06667, 0.17356, 0.22710, 0.05957, + 0.05118, 0.16791, 0.26700, 0.06476, + 0.03204, 0.18460, 0.30764, 0.05052, + 0.03866, 0.17782, 0.31329, 0.06898, + 0.05455, 0.16660, 0.33208, 0.06947, + 0.05942, 0.17472, 0.34423, 0.06828, + 0.06049, 0.16600, 0.37302, 0.07109, + 0.08034, 0.15838, 0.40116, 0.05467, + 0.02948, 0.19200, 0.42222, 0.07480, + 0.16157, 0.32976, 0.18964, 0.06148, + 0.16184, 0.35705, 0.17618, 0.07133, + 0.06190, 0.18452, 0.41600, 0.12793, + 0.15913, 0.41583, 0.13385, 0.10549, + 0.16514, 0.41202, 0.12900, 0.13209, + 0.15798, 0.41181, 0.14254, 0.14987, + 0.16535, 0.44674, 0.24245, 0.03161, + 0.16039, 0.44470, 0.24661, 0.05840, + 0.16619, 0.44376, 0.25613, 0.06797, + 0.16794, 0.44505, 0.27188, 0.07313, + 0.16552, 0.45008, 0.30474, 0.06161, + 0.17327, 0.44679, 0.32441, 0.06143, + 0.16424, 0.45046, 0.33749, 0.07766, + 0.18750, 0.44919, 0.36323, 0.05388, + 0.16081, 0.45211, 0.40343, 0.06140, + 0.16599, 0.43951, 0.41478, 0.08142, + 0.16547, 0.44658, 0.45401, 0.05959, + 0.17154, 0.43689, 0.46392, 0.07725, + 0.15752, 0.44821, 0.48186, 0.08596, + 0.15732, 0.44563, 0.48507, 0.10948, + 0.16971, 0.42742, 0.48779, 0.13653, + 0.14927, 0.43729, 0.49444, 0.16440, + 0.18053, 0.44724, 0.48163, 0.15995, + 0.13141, 0.43855, 0.50035, 0.22299, + 0.31397, 0.55648, 0.39828, 0.04852, + 0.32756, 0.53927, 0.39830, 0.07607, + 0.30887, 0.53804, 0.42265, 0.09559, + 0.28398, 0.53568, 0.46662, 0.10282, + 0.35160, 0.56889, 0.42010, 0.07246, + 0.33810, 0.58035, 0.44442, 0.07413, + 0.35449, 0.59626, 0.43868, 0.07152, + 0.35559, 0.60598, 0.45165, 0.07168, + 0.38379, 0.64088, 0.41710, 0.06708, + 0.40352, 0.64303, 0.40488, 0.08137, + 0.36838, 0.64761, 0.47222, 0.06854, + 0.38514, 0.68422, 0.44359, 0.06775, + 0.37280, 0.67528, 0.47337, 0.08320, + 0.39335, 0.70093, 0.46774, 0.06658, + 0.40587, 0.71223, 0.46598, 0.06847, + 0.39728, 0.73368, 0.47795, 0.06759, + 0.40697, 0.73576, 0.47481, 0.08291, + 0.40122, 0.78861, 0.44658, 0.08799, + 0.41127, 0.76965, 0.46563, 0.10180, + 0.39978, 0.77171, 0.48541, 0.11540, + 0.39130, 0.80752, 0.48702, 0.11041, + 0.40436, 0.80701, 0.48445, 0.12438, + 0.38816, 0.80163, 0.51922, 0.13514, + 0.39551, 0.80409, 0.53365, 0.13485, + 0.40850, 0.83052, 0.53325, 0.11978, + 0.40092, 0.85415, 0.53346, 0.12747, + 0.41872, 0.88168, 0.54551, 0.09404, + 0.43358, 0.88007, 0.52966, 0.12059, + 0.40858, 0.87837, 0.56392, 0.13698, + 0.41637, 0.85094, 0.57749, 0.16700, + 0.38951, 0.83297, 0.60557, 0.20770, + 0.41677, 0.88094, 0.55170, 0.21029, + 0.50089, 1.00860, 0.51420, 0.05996, + 0.47470, 0.99363, 0.54721, 0.09206, + 0.47810, 0.98385, 0.54905, 0.12055, + 0.47903, 0.97455, 0.55883, 0.14309, + 0.48351, 0.98292, 0.58877, 0.12425, + 0.48664, 0.98057, 0.61483, 0.12136, + 0.46078, 0.97139, 0.66506, 0.13012, + 0.49148, 0.98583, 0.67674, 0.09725, + 0.50865, 0.98574, 0.68109, 0.09977, + 0.46259, 0.97882, 0.73056, 0.12723, + 0.46221, 0.95749, 0.76259, 0.14086, + 0.48500, 0.95602, 0.77234, 0.13374, + ]).reshape(98,4) + + +WK_B_param = np.array([ + 4.17218, 16.05892, 26.78365, 69.45643, + 1.83008, 7.20225, 16.13585, 18.75551, + 0.02620, 2.00907, 10.80597,130.49226, + 0.38968, 1.99268, 46.86913,108.84167, + 0.50627, 3.68297, 27.90586, 74.98296, + 0.41335, 10.98289, 34.80286,177.19113, + 0.29792, 7.84094, 22.58809, 72.59254, + 0.17964, 2.60856, 11.79972, 38.02912, + 0.16403, 3.96612, 12.43903, 40.05053, + 0.09101, 0.41253, 5.02463, 17.52954, + 0.06008, 2.07182, 7.64444,146.00952, + 0.07424, 2.87177, 18.06729, 97.00854, + 0.09086, 2.53252, 30.43883, 98.26737, + 0.05396, 1.86461, 22.54263, 72.43144, + 0.05564, 1.62500, 24.45354, 64.38264, + 0.05214, 1.40793, 23.35691, 53.59676, + 0.04643, 1.15677, 19.34091, 52.88785, + 0.07991, 1.01436, 15.67109, 39.60819, + 0.03352, 0.82984, 14.13679,200.97722, + 0.02289, 0.71288, 11.18914,135.02390, + 0.12527, 1.34248, 12.43524,131.71112, + 0.05198, 0.86467, 10.59984,103.56776, + 0.03786, 0.57160, 8.30305, 91.78068, + 0.00240, 0.44931, 7.92251, 86.64058, + 0.01836, 0.41203, 6.73736, 76.30466, + 0.03947, 0.43294, 6.26864, 71.29470, + 0.03962, 0.43253, 6.05175, 68.72437, + 0.03558, 0.39976, 5.36660, 62.46894, + 0.05475, 0.45736, 5.38252, 60.43276, + 0.00137, 0.26535, 4.48040, 54.26088, + 0.10455, 2.18391, 9.04125, 75.16958, + 0.09890, 2.06856, 9.89926, 68.13783, + 0.01642, 0.32542, 3.51888, 44.50604, + 0.07669, 1.89297, 11.31554, 46.32082, + 0.08199, 1.76568, 9.87254, 38.10640, + 0.06939, 1.53446, 8.98025, 33.04365, + 0.07044, 1.59236, 17.53592,215.26198, + 0.06199, 1.41265, 14.33812,152.80257, + 0.06364, 1.34205, 13.66551,125.72522, + 0.06565, 1.25292, 13.09355,109.50252, + 0.05921, 1.15624, 13.24924, 98.69958, + 0.06162, 1.11236, 12.76149, 90.92026, + 0.05081, 0.99771, 11.28925, 84.28943, + 0.05120, 1.08672, 12.23172, 85.27316, + 0.04662, 0.85252, 10.51121, 74.53949, + 0.04933, 0.79381, 9.30944, 41.17414, + 0.04481, 0.75608, 9.34354, 67.91975, + 0.04867, 0.71518, 8.40595, 64.24400, + 0.03672, 0.64379, 7.83687, 73.37281, + 0.03308, 0.60931, 7.04977, 64.83582, + 0.04023, 0.58192, 6.29247, 55.57061, + 0.02842, 0.50687, 5.60835, 48.28004, + 0.03830, 0.58340, 6.47550, 47.08820, + 0.02097, 0.41007, 4.52105, 37.18178, + 0.07813, 1.45053, 15.05933,199.48830, + 0.08444, 1.40227, 13.12939,160.56676, + 0.07206, 1.19585, 11.55866,127.31371, + 0.05717, 0.98756, 9.95556,117.31874, + 0.08249, 1.43427, 12.37363,150.55968, + 0.07081, 1.31033, 11.44403,144.17706, + 0.07442, 1.38680, 11.54391,143.72185, + 0.07155, 1.34703, 11.00432,140.09138, + 0.07794, 1.55042, 11.89283,142.79585, + 0.08508, 1.60712, 11.45367,116.64063, + 0.06520, 1.32571, 10.16884,134.69034, + 0.06850, 1.43566, 10.57719,131.88972, + 0.06264, 1.26756, 9.46411,107.50194, + 0.06750, 1.35829, 9.76480,127.40374, + 0.06958, 1.38750, 9.41888,122.10940, + 0.06574, 1.31578, 9.13448,120.98209, + 0.06517, 1.29452, 8.67569,100.34878, + 0.06213, 1.30860, 9.18871, 91.20213, + 0.06292, 1.23499, 8.42904, 77.59815, + 0.05693, 1.15762, 7.83077, 67.14066, + 0.05145, 1.11240, 8.33441, 65.71782, + 0.05573, 1.11159, 8.00221, 57.35021, + 0.04855, 0.99356, 7.38693, 51.75829, + 0.04981, 0.97669, 7.38024, 44.52068, + 0.05151, 1.00803, 8.03707, 45.01758, + 0.04693, 0.98398, 7.83562, 46.51474, + 0.05161, 1.02127, 9.18455, 64.88177, + 0.05154, 1.03252, 8.49678, 58.79463, + 0.04200, 0.90939, 7.71158, 57.79178, + 0.04661, 0.87289, 6.84038, 51.36000, + 0.04168, 0.73697, 5.86112, 43.78613, + 0.04488, 0.83871, 6.44020, 43.51940, + 0.05786, 1.20028, 13.85073,172.15909, + 0.05239, 1.03225, 11.49796,143.12303, + 0.05167, 0.98867, 10.52682,112.18267, + 0.04931, 0.95698, 9.61135, 95.44649, + 0.04748, 0.93369, 9.89867,102.06961, + 0.04660, 0.89912, 9.69785,100.23434, + 0.04323, 0.78798, 8.71624, 92.30811, + 0.04641, 0.85867, 9.51157,111.02754, + 0.04918, 0.87026, 9.41105,104.98576, + 0.03904, 0.72797, 8.00506, 86.41747, + 0.03969, 0.68167, 7.29607, 75.72682, + 0.04291, 0.69956, 7.38554, 77.18528, + ]).reshape(98,4) diff --git a/tests/core/test_clustering.py b/tests/core/test_clustering.py new file mode 100644 index 000000000..318ceced8 --- /dev/null +++ b/tests/core/test_clustering.py @@ -0,0 +1,57 @@ +"""Torch DBSCAN against unambiguous synthetic ground truth.""" + +import numpy as np + +from quantem.core.utils.clustering import cluster_vector, dbscan, filter_rows + + +def test_dbscan_blobs(): + rng = np.random.default_rng(0) + centers = np.array([[0, 0], [10, 0], [0, 10], [30, 30]], dtype=float) + pts = np.concatenate( + [c + rng.normal(0, 0.5, (200, 2)) for c in centers] + + [rng.uniform(-5, 40, (40, 2))] # sparse background + ) + labels = dbscan(pts, eps=1.5, min_samples=10) + # four clusters, sorted by size; blob members agree internally + assert labels.max() + 1 == 4 + for k in range(4): + blob = labels[200 * k : 200 * (k + 1)] + vals, cnt = np.unique(blob[blob >= 0], return_counts=True) + assert cnt.max() > 190 # one dominant label per blob + assert (labels[800:] == -1).mean() > 0.8 # background mostly noise + + +def test_cluster_vector_and_filter(): + from quantem.core.datastructures.vector import Vector + + rng = np.random.default_rng(1) + R, C = 4, 5 + nested = [] + for r in range(R): + row = [] + for c in range(C): + # one tight cluster at (1,2) plus background + n = 30 if (r, c) == (1, 2) else 3 + q = ( + rng.normal(0, 0.01, (n, 2)) + [0.5, -0.3] + if (r, c) == (1, 2) + else rng.uniform(-1, 1, (n, 2)) + ) + inten = rng.uniform(1, 2, (n, 1)) + row.append(np.concatenate([q, inten], axis=1)) + nested.append(row) + vec = Vector.from_data( + nested, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + labeled, labels = cluster_vector( + vec, fields=("qx", "qy"), eps=0.05, min_samples=10 + ) + assert "cluster" in labeled.fields + assert labels.max() == 0 # exactly one cluster found + got = labeled[1, 2].array + assert (got[:, -1] == 0).sum() >= 28 + + kept = filter_rows(vec, labels == 0) + assert kept.total_rows == int((labels == 0).sum()) + assert kept.shape[:2] == vec.shape[:2] diff --git a/tests/diffraction/test_bloch.py b/tests/diffraction/test_bloch.py new file mode 100644 index 000000000..7dc8f0cb6 --- /dev/null +++ b/tests/diffraction/test_bloch.py @@ -0,0 +1,86 @@ +"""Tests for quantem.diffraction.bloch.""" + +import numpy as np +import pytest +import torch +from ase.build import bulk + +from quantem.core.datastructures.vector import Vector +from quantem.diffraction.bloch import dynamical_pattern, refine_thickness +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.orientation import OrientationMap +from quantem.diffraction.phase import PhaseMap +from quantem.diffraction.rotations import quat_from_zone_axis + + +@pytest.fixture(scope="module") +def ti_beta(): + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True), name="Ti beta") + # 2x coverage so all coupling vectors g - h have structure factors + xtl.calculate_structure_factors(k_max=3.0, tol_structure_factor=1e-6) + return xtl + + +def test_flux_conservation(ti_beta): + q = quat_from_zone_axis(torch.tensor([0.0, 1.0, 1.0], dtype=torch.float64)) + p = dynamical_pattern( + ti_beta, q, np.arange(50, 1500, 50.0), energy_ev=200e3, sg_max=0.08, k_max=1.5 + ) + total = p["intensity"].sum(dim=1) + assert float(total.max()) <= 1.0 + 1e-6 + + +def test_thin_limit_matches_kinematical(ti_beta): + q = quat_from_zone_axis(torch.tensor([0.0, 1.0, 1.0], dtype=torch.float64)) + p = dynamical_pattern(ti_beta, q, 25.0, energy_ev=200e3, sg_max=0.08, k_max=1.5) + kin = ti_beta.generate_pattern(q, energy_ev=200e3, sigma_excitation=0.02) + top_dyn = set(map(tuple, p["hkl"][p["intensity"][0].argsort(descending=True)[:4]].tolist())) + top_kin = set(map(tuple, kin["hkl"][kin["intensity"].argsort(descending=True)[:4]].tolist())) + assert top_dyn == top_kin + + +def test_thickness_recovery(ti_beta): + """Simulate dynamical peaks at a known thickness, recover it.""" + t_true = 600.0 + torch.manual_seed(0) + zones = torch.tensor( + [[0.1, 0.9, 1.0], [0.3, 0.5, 1.0], [0.05, 1.0, 1.1]], dtype=torch.float64 + ) + N = zones.shape[0] + peaks = Vector.from_shape( + (1, N), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + q_true = quat_from_zone_axis(zones) + for i in range(N): + p = dynamical_pattern( + ti_beta, q_true[i], t_true, energy_ev=200e3, sg_max=0.06, k_max=1.5 + ) + keep = p["intensity"][0] > 1e-4 + peaks[0, i] = np.stack( + [ + p["qx"][keep].numpy(), + p["qy"][keep].numpy(), + p["intensity"][0][keep].numpy(), + ], + axis=1, + ) + + om = OrientationMap.from_vectors(peaks, ti_beta, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=2.0, angle_step_in_plane_deg=2.0) + om.match_orientations(progress_bar=False) + # thickness oscillations are sensitive to ~1 degree tilt errors, beyond + # what kinematical matching provides for dynamical patterns; test the + # thickness scan itself with the true orientations (dynamical tilt + # refinement is the future joint pass) + om.quats[0, :, 0] = q_true + + pm = PhaseMap.from_orientation_maps([om]) + pm.fit(max_patterns=1, progress_bar=False) + res = refine_thickness( + pm, + thicknesses_A=np.arange(100, 1200, 50.0), + sg_max=0.06, + progress_bar=False, + ) + t_fit = res["thickness"][0].numpy() + assert (np.abs(t_fit - t_true) <= 50.0).all() diff --git a/tests/diffraction/test_calibration_refine.py b/tests/diffraction/test_calibration_refine.py new file mode 100644 index 000000000..971470543 --- /dev/null +++ b/tests/diffraction/test_calibration_refine.py @@ -0,0 +1,70 @@ +"""Recovery of a global calibration residual from matched orientations.""" + +import numpy as np +import torch +from ase.build import bulk + +from quantem.core.datastructures.vector import Vector +from quantem.diffraction import calibration +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.orientation import OrientationMap +from quantem.diffraction.rotations import qnormalize + + +def test_refine_calibration_recovers_distortion(): + torch.manual_seed(1) + rng = np.random.default_rng(1) + xtl = Crystal.from_ase( + bulk("Ti", "hcp", a=2.9505, c=4.6855), name="Ti alpha", verbose=False + ) + xtl.calculate_structure_factors(k_max=1.5) + + # global calibration error: 1.5% scale, 0.8% ellipticity, 0.3 deg rotation + scale_true = 1.015 + e11_true, e12_true = 0.008, -0.004 + th = np.deg2rad(0.3) + M_true = ( + scale_true + * np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]]) + @ np.array([[1 + e11_true, e12_true], [e12_true, 1 - e11_true]]) + ) + + R, C = 6, 10 + q_true = qnormalize(torch.randn(R * C, 4, dtype=torch.float64)) + cells = [] + for i in range(R * C): + p = xtl.generate_pattern(q_true[i], energy_ev=200e3, sigma_excitation=0.02) + q2 = np.stack([p["qx"].numpy(), p["qy"].numpy()], axis=1) @ M_true.T + q2 += rng.normal(0, 0.002, q2.shape) + cells.append(np.column_stack([q2, p["intensity"].numpy()])) + nested = [cells[r * C : (r + 1) * C] for r in range(R)] + peaks = Vector.from_data( + nested, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(power_intensity=0.0) + om.match_orientations(progress_bar=False) + om.refine_orientations(progress_bar=False, neighbor_rescue=False) + sm = om.calculate_strain(progress_bar=False) + + res = calibration.refine_calibration([sm]) + assert res["num_positions"] > 30 + assert abs(res["scale"] - scale_true) < 3e-3 + # a global rotation is absorbed into the refined orientations and must + # read as ~0 here (it is measured independently via the scan rotation) + assert abs(res["rotation_deg"]) < 0.1 + assert abs(res["ellipse"][0] - e11_true) < 2e-3 + assert abs(res["ellipse"][1] - e12_true) < 2e-3 + + # applying the correction must bring the residual to the identity + peaks_fixed = calibration.transform_peaks(peaks, res["correction"]) + om2 = OrientationMap.from_vectors(peaks_fixed, xtl, energy_ev=200e3) + om2.build_plan(power_intensity=0.0) + om2.match_orientations(progress_bar=False) + om2.refine_orientations(progress_bar=False, neighbor_rescue=False) + sm2 = om2.calculate_strain(progress_bar=False) + res2 = calibration.refine_calibration([sm2]) + assert abs(res2["scale"] - 1.0) < 2e-3 + assert abs(res2["ellipse"][0]) < 1.5e-3 + assert abs(res2["ellipse"][1]) < 1.5e-3 diff --git a/tests/diffraction/test_crystal.py b/tests/diffraction/test_crystal.py new file mode 100644 index 000000000..109999318 --- /dev/null +++ b/tests/diffraction/test_crystal.py @@ -0,0 +1,73 @@ +"""Tests for quantem.diffraction.crystal.""" + +import numpy as np +import pytest +import torch +from ase import Atoms +from ase.build import bulk + +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.rotations import quat_from_zone_axis + + +@pytest.fixture +def ti_beta(): + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True), name="Ti beta") + xtl.calculate_structure_factors(k_max=1.5) + return xtl + + +def test_symmetry_detection(ti_beta): + assert ti_beta.pointgroup == "m-3m" + assert ti_beta.laue_group == "m-3m" + assert ti_beta.sym_quats.shape[0] == 24 # proper rotations of m-3m + + +def test_hcp_symmetry(): + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.95, c=4.686)) + assert xtl.pointgroup == "6/mmm" + assert xtl.sym_quats.shape[0] == 12 + + +def test_bcc_absences(ti_beta): + # h + k + l odd forbidden in bcc + parity = ti_beta.hkl.sum(dim=1) % 2 + assert (parity == 0).all() + + +def test_ring_positions(ti_beta): + # (110) ring at sqrt(2)/a + g110 = np.sqrt(2) / 3.31 + assert np.isclose(float(ti_beta.g_len.min()), g110, atol=1e-6) + + +def test_pseudo_symmetry(): + ortho = Atoms("Au", positions=[[0, 0, 0]], cell=[4.000, 4.001, 4.002], pbc=True) + exact = Crystal.from_ase(ortho) + pseudo = Crystal.from_ase(ortho, pseudo_symmetry_tol=0.01) + assert exact.pointgroup_matching == "mmm" + assert pseudo.pointgroup_matching == "m-3m" + assert pseudo.sym_quats_matching.shape[0] == 24 + # exact group is retained for reporting/refinement + assert pseudo.pointgroup == "mmm" + + +def test_zone_axis_wedge_anchored_001(ti_beta): + wedge = ti_beta.zone_axis_wedge() + assert torch.allclose( + wedge[0], torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64) + ) + + +def test_generate_pattern(ti_beta): + q = quat_from_zone_axis(torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64)) + p = ti_beta.generate_pattern(q, energy_ev=200e3) + # [001] zone: peaks on a square grid of 110-type spacings + assert p["qx"].shape[0] > 4 + qr = torch.hypot(p["qx"], p["qy"]) + assert float(qr.min()) > 0.4 # no direct beam + # pattern symmetric under 90 degree rotation + rot = torch.stack((-p["qy"], p["qx"]), dim=1) + orig = torch.stack((p["qx"], p["qy"]), dim=1) + d = torch.cdist(rot, orig).min(dim=1).values + assert float(d.max()) < 1e-6 diff --git a/tests/diffraction/test_ellipse.py b/tests/diffraction/test_ellipse.py new file mode 100644 index 000000000..f0cf162b4 --- /dev/null +++ b/tests/diffraction/test_ellipse.py @@ -0,0 +1,39 @@ +"""Elliptic distortion recovery from radial histogram sharpness.""" + +import numpy as np +import torch +from ase.build import bulk + +from quantem.core.datastructures.vector import Vector +from quantem.diffraction import calibration +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.rotations import qnormalize + + +def test_ellipse_recovery(): + torch.manual_seed(0) + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.9505, c=4.6855), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + + e_true = np.array([0.012, -0.008]) + A = np.array([[1 + e_true[0], e_true[1]], [e_true[1], 1 - e_true[0]]]) + A_inv = np.linalg.inv(A) + + rng = np.random.default_rng(0) + cells = [] + for i in range(60): + q = qnormalize(torch.randn(4, dtype=torch.float64)) + pat = xtl.generate_pattern(q, energy_ev=200e3, sigma_excitation=0.02) + qxy = np.stack([pat["qx"].numpy(), pat["qy"].numpy()], axis=1) + # distort (the inverse of the correction) plus detection noise + qxy = qxy @ A_inv.T + rng.normal(0, 0.003, qxy.shape) + cells.append(np.column_stack([qxy, pat["intensity"].numpy()])) + peaks = Vector.from_data( + [cells], + fields=["qx", "qy", "intensity"], + units=["A^-1", "A^-1", "counts"], + name="synthetic", + ) + + e_fit = calibration.calibrate_ellipse(peaks) + assert np.allclose(e_fit, e_true, atol=2e-3), (e_fit, e_true) diff --git a/tests/diffraction/test_orientation.py b/tests/diffraction/test_orientation.py new file mode 100644 index 000000000..ddb589753 --- /dev/null +++ b/tests/diffraction/test_orientation.py @@ -0,0 +1,127 @@ +"""Round-trip tests for quantem.diffraction.orientation.""" + +import numpy as np +import pytest +import torch +from ase.build import bulk + +from quantem.core.datastructures.vector import Vector +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.orientation import OrientationMap +from quantem.diffraction.rotations import misorientation_angle_deg, qnormalize + + +def _make_peaks(xtl, q_true, sigma=0.02): + N = q_true.shape[0] + peaks = Vector.from_shape( + (1, N), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(N): + p = xtl.generate_pattern(q_true[i], energy_ev=200e3, sigma_excitation=sigma) + peaks[0, i] = np.stack( + [p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()], axis=1 + ) + return peaks + + +@pytest.mark.parametrize( + "builder,kwargs", + [ + (bulk, dict(name="Ti", crystalstructure="bcc", a=3.31, cubic=True)), + (bulk, dict(name="Ti", crystalstructure="hcp", a=2.95, c=4.686)), + ], +) +def test_roundtrip_matching(builder, kwargs): + torch.manual_seed(3) + xtl = Crystal.from_ase(builder(**kwargs)) + xtl.calculate_structure_factors(k_max=1.5) + N = 15 + q_true = qnormalize(torch.randn(N, 4, dtype=torch.float64)) + peaks = _make_peaks(xtl, q_true) + + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan( + angle_step_zone_axis_deg=2.0, angle_step_in_plane_deg=2.0, power_intensity=0.0 + ) + om.match_orientations(progress_bar=False) + # noiseless synthetic data: the envelope tilt is exact, so allow the + # full grid-scale correction (the default trust region is sized for + # noisy measured intensities) + om.refine_orientations(zone_max_total_deg=1.5, progress_bar=False) + + err = misorientation_angle_deg(q_true, om.quats[0, :, 0], xtl.sym_quats).numpy() + # majority recovered to well below the grid step; a small number of + # kinematically (near-)degenerate orientations may land elsewhere + assert np.median(err) < 0.1 + assert (err < 1.0).mean() >= 0.7 + + +def test_normalized_scores_and_reliability(): + torch.manual_seed(0) + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True)) + xtl.calculate_structure_factors(k_max=1.5) + q_true = qnormalize(torch.randn(6, 4, dtype=torch.float64)) + peaks = _make_peaks(xtl, q_true) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=3.0, angle_step_in_plane_deg=3.0) + om.match_orientations(progress_bar=False) + + assert float(om.corr.max()) <= 1.0 + 1e-9 + assert float(om.corr.min()) >= 0.0 + assert om.reliability is not None + assert (om.reliability[0] > 0).all() + + +def test_mirror_channel(): + """Orientations in the opposite hemisphere are matched via the mirror.""" + torch.manual_seed(5) + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True)) + xtl.calculate_structure_factors(k_max=1.5) + q_true = qnormalize(torch.randn(10, 4, dtype=torch.float64)) + peaks = _make_peaks(xtl, q_true) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=2.0, angle_step_in_plane_deg=2.0) + om.match_orientations(progress_bar=False) + om.refine_orientations(progress_bar=False) + err = misorientation_angle_deg(q_true, om.quats[0, :, 0], xtl.sym_quats).numpy() + used_mirror = om.mirror[0, :, 0].numpy() + # both channels appear and mirror matches are as accurate as direct ones + assert used_mirror.any() + assert (~used_mirror).any() + ok = err < 5 + assert ok.mean() >= 0.7 + assert np.median(err[ok & used_mirror]) < 0.5 + + +def test_square_detector_correction(): + """Peaks clipped by a square detector: the aperture-normalized match + recovers the orientation as well as the unclipped case.""" + torch.manual_seed(7) + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.95, c=4.686)) + xtl.calculate_structure_factors(k_max=1.5) + q_true = qnormalize(torch.randn(10, 4, dtype=torch.float64)) + q_det = 0.9 # detector half-width < k_max: corners clipped + + peaks = Vector.from_shape( + (1, 10), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(10): + p = xtl.generate_pattern(q_true[i], energy_ev=200e3, sigma_excitation=0.02) + keep = (p["qx"].abs() < q_det) & (p["qy"].abs() < q_det) + peaks[0, i] = np.stack( + [p["qx"][keep].numpy(), p["qy"][keep].numpy(), p["intensity"][keep].numpy()], + axis=1, + ) + + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan( + angle_step_zone_axis_deg=2.0, + angle_step_in_plane_deg=2.0, + detector_q_max=q_det, + ) + om.match_orientations(progress_bar=False) + err = misorientation_angle_deg(q_true, om.quats[0, :, 0], xtl.sym_quats).numpy() + assert (err < 5).mean() >= 0.7 + # with the aperture correction, kernel leakage at the hard detector edge + # can push the normalized score a few percent above 1 + assert float(om.corr.max()) <= 1.05 diff --git a/tests/diffraction/test_rotation_convention.py b/tests/diffraction/test_rotation_convention.py new file mode 100644 index 000000000..29cd69c9c --- /dev/null +++ b/tests/diffraction/test_rotation_convention.py @@ -0,0 +1,65 @@ +"""Self-consistency of the detector-to-scan rotation convention. + +Simulates a cubic crystal rotated in-plane by a known angle, records its +peaks in a detector frame that is rotated relative to the scan frame, and +checks that peaks_to_calibrated(rotation_ccw_deg=...) plus orientation +matching recovers the ground-truth in-plane angle in the scan frame. +""" + +import numpy as np +import pytest +import torch +from ase.build import bulk + +from quantem.core.datastructures.vector import Vector +from quantem.diffraction import calibration +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.orientation import OrientationMap +from quantem.diffraction.rotations import quat_from_axis_angle + + +@pytest.mark.parametrize("rot_scan_deg", [0.0, 34.0, -34.0]) +def test_rotation_roundtrip(rot_scan_deg): + xtl = Crystal.from_ase(bulk("Al", "fcc", a=4.05, cubic=True), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + + # ground truth: [001] zone, 30 deg in-plane rotation, in the SCAN frame + angle_true = 30.0 + q_true = quat_from_axis_angle( + torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64), + torch.tensor(np.deg2rad(angle_true), dtype=torch.float64), + ) + pat = xtl.generate_pattern(q_true, energy_ev=200e3, sigma_excitation=0.02) + q_scan = np.stack([pat["qx"].numpy(), pat["qy"].numpy()], axis=1) + + # what the detector records: scan-frame vectors rotated by -rot_scan + # (peaks_to_calibrated undoes this with rotation_ccw_deg=+rot_scan) + th = np.deg2rad(-rot_scan_deg) + rot_back = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]]) + q_det = q_scan @ rot_back.T + + pixel_size = 0.01 + data = np.column_stack([q_det / pixel_size, pat["intensity"].numpy()]) + peaks_px = Vector.from_data( + [[data]], + fields=["q_row", "q_col", "intensity"], + units=["px", "px", "counts"], + name="synthetic", + ) + peaks = calibration.peaks_to_calibrated( + peaks_px, pixel_size, rotation_ccw_deg=rot_scan_deg + ) + + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(power_intensity=0.0) + om.match_orientations(progress_bar=False) + om.refine_orientations(progress_bar=False) + + # the recovered orientation must equal the scan-frame ground truth + # (modulo crystal symmetry) -- independent of the detector rotation + from quantem.diffraction.rotations import misorientation_angle_deg + + err = float( + misorientation_angle_deg(q_true, om.quats[0, 0, 0], xtl.sym_quats) + ) + assert err < 1.0, f"misorientation {err:.2f} deg at rot {rot_scan_deg}" diff --git a/tests/diffraction/test_rotations.py b/tests/diffraction/test_rotations.py new file mode 100644 index 000000000..381950dc8 --- /dev/null +++ b/tests/diffraction/test_rotations.py @@ -0,0 +1,104 @@ +"""Tests for quantem.diffraction.rotations.""" + +import numpy as np +import pytest +import torch + +from quantem.diffraction.rotations import ( + misorientation_angle_deg, + qconj, + qmult, + qnormalize, + qrotate, + quat_from_axis_angle, + quat_from_euler_zxz, + quat_from_matrix, + quat_from_zone_axis, + quat_to_euler_zxz, + quat_to_matrix, + sample_zone_axes, + zone_axis_from_quat, +) + + +@pytest.fixture +def random_quats(): + torch.manual_seed(0) + return qnormalize(torch.randn(100, 4, dtype=torch.float64)) + + +def test_matrix_roundtrip(random_quats): + R = quat_to_matrix(random_quats) + assert torch.allclose(quat_from_matrix(R), random_quats, atol=1e-10) + + +def test_euler_roundtrip(random_quats): + e = quat_to_euler_zxz(random_quats) + assert torch.allclose( + qnormalize(quat_from_euler_zxz(e)), random_quats, atol=1e-8 + ) + + +def test_rotate_matches_matrix(random_quats): + v = torch.randn(100, 3, dtype=torch.float64) + R = quat_to_matrix(random_quats) + assert torch.allclose( + qrotate(random_quats, v), (R @ v[..., None]).squeeze(-1), atol=1e-10 + ) + + +def test_mult_conj_identity(random_quats): + q = random_quats + ident = qmult(q, qconj(q)) + expect = torch.zeros_like(q) + expect[:, 0] = 1.0 + assert torch.allclose(ident, expect, atol=1e-10) + + +def test_zone_axis_roundtrip(): + torch.manual_seed(1) + v = torch.randn(50, 3, dtype=torch.float64) + v = v / torch.linalg.norm(v, dim=-1, keepdim=True) + q = quat_from_zone_axis(v, in_plane_deg=25.0) + assert torch.allclose(zone_axis_from_quat(q), v, atol=1e-10) + + +def test_axis_angle(): + axis = torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64) + q = quat_from_axis_angle(axis, torch.tensor(np.pi / 2, dtype=torch.float64)) + v = torch.tensor([1.0, 0.0, 0.0], dtype=torch.float64) + assert torch.allclose( + qrotate(q, v), torch.tensor([0.0, 1.0, 0.0], dtype=torch.float64), atol=1e-9 + ) + + +def test_misorientation_symmetry(): + # 90 degree rotation about z is a cubic symmetry: misorientation 0 + from ase.build import bulk + + from quantem.diffraction.crystal import Crystal + + xtl = Crystal.from_ase(bulk("Au", "fcc", a=4.08, cubic=True)) + qa = torch.tensor([1.0, 0, 0, 0], dtype=torch.float64) + qb = quat_from_axis_angle( + torch.tensor([0.0, 0, 1.0], dtype=torch.float64), + torch.tensor(np.pi / 2, dtype=torch.float64), + ) + ang = misorientation_angle_deg(qa, qb, xtl.sym_quats) + assert float(ang) < 1e-4 + ang_nosym = misorientation_angle_deg(qa, qb) + assert abs(float(ang_nosym) - 90.0) < 1e-6 + + +def test_sample_zone_axes_wedge(): + corners = torch.tensor( + [[0, 0, 1], [0, 1, 1], [1, 1, 1]], dtype=torch.float64 + ) + corners = corners / torch.linalg.norm(corners, dim=-1, keepdim=True) + v, inds = sample_zone_axes(corners, 2.0) + assert torch.allclose( + torch.linalg.norm(v, dim=-1), torch.ones(v.shape[0], dtype=v.dtype) + ) + # corners present + for c in corners: + assert (torch.linalg.norm(v - c, dim=-1).min() < 1e-8) diff --git a/tests/diffraction/test_two_phase_map.py b/tests/diffraction/test_two_phase_map.py new file mode 100644 index 000000000..b2991df91 --- /dev/null +++ b/tests/diffraction/test_two_phase_map.py @@ -0,0 +1,114 @@ +"""End-to-end two-phase mapping on a synthetic alpha/beta titanium scan. + +Left region: bcc beta in a fixed orientation. Right region: hcp alpha in a +Burgers-related orientation ((110)beta || (0001)alpha). A two-column band in +the middle contains both patterns superimposed, as at a lath boundary. +""" + +import numpy as np +import torch +from ase.build import bulk + +from quantem.core.datastructures.vector import Vector +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.orientation import OrientationMap +from quantem.diffraction.phase import PhaseMap +from quantem.diffraction.rotations import ( + misorientation_angle_deg, + qmult, + quat_from_axis_angle, +) + + +def _pattern(xtl, q, rng): + p = xtl.generate_pattern(q, energy_ev=200e3, sigma_excitation=0.02) + arr = np.column_stack( + [p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()] + ) + arr[:, :2] += rng.normal(0, 0.003, (arr.shape[0], 2)) + arr[:, 2] *= rng.lognormal(0, 0.3, arr.shape[0]) + return arr + + +def test_two_phase_map(): + torch.manual_seed(2) + rng = np.random.default_rng(2) + ti_a = Crystal.from_ase( + bulk("Ti", "hcp", a=2.9505, c=4.6855), name="Ti alpha", verbose=False + ).calculate_structure_factors(k_max=1.5) + ti_b = Crystal.from_ase( + bulk("Ti", "bcc", a=3.26, cubic=True), name="Ti beta", verbose=False + ).calculate_structure_factors(k_max=1.5) + + # beta along [111] zone; alpha along [0001]: the Burgers-related pair + # shares the hexagonal net, the hard case for phase mapping + q_beta = quat_from_axis_angle( + torch.tensor([1.0, -1.0, 0.0], dtype=torch.float64) + / np.sqrt(2), + torch.tensor(np.arccos(1 / np.sqrt(3)), dtype=torch.float64), + ) + q_alpha = qmult( + quat_from_axis_angle( + torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64), + torch.tensor(np.deg2rad(14.0), dtype=torch.float64), + ), + torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=torch.float64), + ) + + R, C = 6, 11 + band = (5, 6) # columns with both phases + cells = [] + truth = np.zeros((R, C), dtype=int) # 0 alpha, 1 beta + for r in range(R): + row = [] + for c in range(C): + if c < band[0]: + arr = _pattern(ti_b, q_beta, rng) + truth[r, c] = 1 + elif c > band[1]: + arr = _pattern(ti_a, q_alpha, rng) + truth[r, c] = 0 + else: + a = _pattern(ti_a, q_alpha, rng) + b = _pattern(ti_b, q_beta, rng) + a[:, 2] *= 0.5 + b[:, 2] *= 0.5 + arr = np.concatenate([a, b]) + truth[r, c] = 2 + row.append(arr) + cells.append(row) + peaks = Vector.from_data( + cells, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + + oms = [] + for xtl in (ti_a, ti_b): + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan() + om.match_orientations(num_matches=2, progress_bar=False) + om.refine_orientations(progress_bar=False) + oms.append(om) + + # orientation recovery in the pure regions + err_a = misorientation_angle_deg( + q_alpha, oms[0].quats[:, band[1] + 1 :, 0].reshape(-1, 4), ti_a.sym_quats + ).numpy() + err_b = misorientation_angle_deg( + q_beta, oms[1].quats[:, : band[0], 0].reshape(-1, 4), ti_b.sym_quats + ).numpy() + assert np.median(err_a) < 1.0 + assert np.median(err_b) < 1.0 + + pm = PhaseMap.from_orientation_maps(oms) + pm.fit(max_patterns=2, progress_bar=False) + pi = pm.phase_index.numpy() + + pure_a = pi[:, band[1] + 1 :] + pure_b = pi[:, : band[0]] + assert (pure_a == 0).mean() > 0.85 + assert (pure_b == 1).mean() > 0.85 + + # overlap band: every position must be assigned one of the two true + # phases with a valid orientation (either is acceptable) + band_pi = pi[:, band[0] : band[1] + 1] + assert np.isin(band_pi, [0, 1]).all() From 1396e24abfcd3031590af0b7369f1b47d3a8757d Mon Sep 17 00:00:00 2001 From: maclariz Date: Tue, 1 Sep 2026 16:49:18 +0100 Subject: [PATCH 02/36] Edits to DDF and clustering Some corrections to the new DDF and clustering routines. "noise" was renamed to "unclustered". An additional function added to put grain labels into a vector. The dbscan was rewritten with help from Claude to eliminate some inefficiencies that made it slow on CPU. An updated ipynb will be sent to accompany this PR. --- src/quantem/core/utils/clustering.py | 65 +++++----- src/quantem/diffraction/digital_dark_field.py | 113 ++++++++++++++++-- uv.lock | 58 +++++++++ 3 files changed, 192 insertions(+), 44 deletions(-) diff --git a/src/quantem/core/utils/clustering.py b/src/quantem/core/utils/clustering.py index c2989fc09..21d44baae 100644 --- a/src/quantem/core/utils/clustering.py +++ b/src/quantem/core/utils/clustering.py @@ -2,14 +2,17 @@ No external clustering dependency: neighbors are found with blockwise distance computations pruned by a sliding sorted window along the widest -dimension, and cluster connectivity is resolved by min-label propagation -with path compression. Runs on CPU or any torch device. +dimension. Cluster connectivity among core points is resolved in a single +pass with scipy's sparse connected-components graph routine, rather than +iterative label propagation. Runs on CPU or any torch device. """ from __future__ import annotations import numpy as np +import scipy.sparse as sp import torch +from scipy.sparse.csgraph import connected_components def dbscan( @@ -19,7 +22,6 @@ def dbscan( device: str | torch.device = "cpu", block: int = 2048, sort_by_size: bool = True, - max_rounds: int = 200, ) -> np.ndarray: """DBSCAN cluster labels for a point set. @@ -79,35 +81,34 @@ def block_candidates(i0: int, i1: int) -> tuple[int, int]: counts[i0:i1] = (d <= eps).sum(dim=1) core = counts >= min_samples - # pass 2: min-label propagation among core points - labels = torch.arange(N, dtype=torch.long, device=device) - labels[~core] = -1 - for _ in range(max_rounds): - changed = False - for i0 in range(0, N, block): - i1 = min(i0 + block, N) - if not bool(core[i0:i1].any()): - continue - j0, j1 = block_candidates(i0, i1) - d = torch.cdist(ps[i0:i1], ps[j0:j1]) - adj = (d <= eps) & core[i0:i1, None] & core[None, j0:j1] - lab_nb = torch.where( - adj, labels[j0:j1][None, :], torch.full_like(d, N, dtype=torch.long) - ) - new = lab_nb.min(dim=1).values - cur = labels[i0:i1] - upd = core[i0:i1] & (new < cur) - if bool(upd.any()): - labels[i0:i1] = torch.where(upd, new, cur) - changed = True - # path compression: labels point at representative indices - for _ in range(32): - comp = torch.where(core, labels[labels.clamp_min(0)], labels) - if bool(torch.equal(comp, labels)): - break - labels = comp - if not changed: - break + # pass 2: connect core points within eps of each other, once, then + # resolve clusters as connected components (single pass, no iterative + # relabeling rounds) + edge_rows: list[torch.Tensor] = [] + edge_cols: list[torch.Tensor] = [] + for i0 in range(0, N, block): + i1 = min(i0 + block, N) + if not bool(core[i0:i1].any()): + continue + j0, j1 = block_candidates(i0, i1) + d = torch.cdist(ps[i0:i1], ps[j0:j1]) + adj = (d <= eps) & core[i0:i1, None] & core[None, j0:j1] + ii, jj = torch.nonzero(adj, as_tuple=True) + edge_rows.append(ii + i0) + edge_cols.append(jj + j0) + + labels = torch.full((N,), -1, dtype=torch.long, device=device) + if edge_rows: + rows = torch.cat(edge_rows).cpu().numpy() + cols = torch.cat(edge_cols).cpu().numpy() + graph = sp.coo_matrix( + (np.ones(rows.shape[0], dtype=np.int8), (rows, cols)), shape=(N, N) + ) + _, comp = connected_components(graph, directed=False) + core_np = core.cpu().numpy() + labels = torch.as_tensor( + np.where(core_np, comp, -1), dtype=torch.long, device=device + ) # pass 3: border points join the nearest core cluster within eps for i0 in range(0, N, block): diff --git a/src/quantem/diffraction/digital_dark_field.py b/src/quantem/diffraction/digital_dark_field.py index a09746cb5..43dae7431 100644 --- a/src/quantem/diffraction/digital_dark_field.py +++ b/src/quantem/diffraction/digital_dark_field.py @@ -17,6 +17,10 @@ import numpy as np +import matplotlib.pyplot as plt +from matplotlib.colors import hsv_to_rgb + +from quantem.core.datastructures.vector import Vector from quantem.core.utils.clustering import cluster_vector, dbscan # noqa: F401 @@ -124,7 +128,6 @@ def composite_ddf( np.ndarray (R, C, 3) RGB image in [0, 1]. """ - from matplotlib.colors import hsv_to_rgb K = images.shape[0] if colors is None: @@ -144,8 +147,21 @@ def composite_ddf( def color_wheel(n: int = 256, saturation: float = 1.0) -> np.ndarray: - """(n, n, 4) RGBA hue wheel for labeling composite images.""" - from matplotlib.colors import hsv_to_rgb + """ + (n, n, 4) RGBA hue wheel for labeling composite images. + + Parameters + n: int + Size of the output image. + saturation: float + Saturation of the hue wheel (0=gray, 1=full color). + + Returns + ------- + np.ndarray + (n, n, 4) RGBA image in [0, 1]. + """ + y, x = np.mgrid[-1 : 1 : n * 1j, -1 : 1 : n * 1j] r = np.hypot(x, y) @@ -163,13 +179,45 @@ def plot_cluster_scatter( labeled, q_fields=("qx", "qy"), label_field: str = "cluster", + specific_cluster: int | None = None, max_clusters: int | None = None, - show_noise: bool = True, + show_unclustered: bool = True, point_size: float = 2.0, + alpha: float = 0.2, figax=None, ): - """All peaks in diffraction space, colored by cluster (noise in gray).""" - import matplotlib.pyplot as plt + """ + All peaks in diffraction space, colored by cluster (unclustered in gray). + + Parameters + ---------- + labeled : Vector + Labeled diffraction data. + q_fields: tuple of str + Field names for the diffraction-space coordinates to plot. + label_field: str + Field name for the cluster label. + specific_cluster: int | None + If given, plot only this cluster (unclustered points are not shown). + max_clusters: int | None + Maximum number of clusters to plot (for large datasets). + show_unclustered: bool + Whether to show unclustered points (label < 0) in gray. + point_size: float + Size of the scatter points. + alpha: float + Transparency of the scatter points. Recommended to be << 1.0 for large datasets so only dense + regions are visible. + figax: tuple of (matplotlib.figure.Figure, matplotlib.axes.Axes) | None + If provided, plot into this figure and axes instead of creating a new one. + + Returns + ------- + fig: matplotlib.figure.Figure + The figure containing the scatter plot. + ax: matplotlib.axes.Axes + The axes containing the scatter plot. + """ fields = labeled.fields flat = labeled.flatten() @@ -181,7 +229,20 @@ def plot_cluster_scatter( fig, ax = plt.subplots(figsize=(6.5, 6.5)) else: fig, ax = figax - if show_noise: + q_max = np.max(np.abs([qx, qy]))*1.05 + ax.set_xlim(-q_max, q_max) + ax.set_ylim(-q_max, q_max) + ax.set_aspect("equal") + ax.invert_yaxis() + ax.set_xlabel("$q_y$") + ax.set_ylabel("$q_x$") + + if specific_cluster is not None: + m = labels == specific_cluster + ax.scatter(qy[m], qx[m], s=point_size, color="C0", lw=0, alpha=alpha) + return fig, ax + + if show_unclustered: m = labels < 0 ax.scatter(qy[m], qx[m], s=point_size, color="0.85", lw=0) n = labels.max() + 1 @@ -191,9 +252,37 @@ def plot_cluster_scatter( hues = rng.permutation(np.linspace(0, 1, len(list(ids)), endpoint=False)) for k in ids: m = labels == k - ax.scatter(qy[m], qx[m], s=point_size, color=cmap(hues[k]), lw=0) - ax.set_aspect("equal") - ax.invert_yaxis() - ax.set_xlabel("$q_c$") - ax.set_ylabel("$q_r$") + ax.scatter(qy[m], qx[m], s=point_size, color=cmap(hues[k]), lw=0, alpha=alpha) return fig, ax + +def assign_grain_labels(L1, L2_labels,label_field: str = "cluster"): + """Assign L2 grain labels to L1 clusters based on their centers of mass. + + Parameters + ---------- + L1 : Vector + Labeled diffraction data with L1 cluster labels. + L2_labels : np.ndarray + (K,) array of L2 grain labels corresponding to each L1 cluster. + + Returns + ------- + L2 : Vector + Labeled diffraction data with L2 grain labels assigned. + L2_labels : np.ndarray + Updated array of L2 grain labels. + """ + fields = L1.fields + flat = L1.flatten() + L1_labels = flat[:, fields.index(label_field)].astype(int) + L1_unique = np.unique(L1_labels) + + L1_to_L2 = np.insert(L2_labels,0,-2) + mapper = dict(zip(L1_unique, L1_to_L2)) + L2_labels_full = np.array([mapper[label] for label in L1_labels]) + + L2 = L1.copy() + L2.add_fields("grain_label", values = L2_labels_full) + + # Return the new Vector with L2 labels + return L2 \ No newline at end of file diff --git a/uv.lock b/uv.lock index 36a6293e2..d235f5f72 100644 --- a/uv.lock +++ b/uv.lock @@ -128,6 +128,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, ] +[[package]] +name = "ase" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/36/cfe17324ea701d2caa479f25b134c54aeb36998443fd88c13e4f1af06f52/ase-3.29.0.tar.gz", hash = "sha256:ef4e2caa38169e3fbbc4164764a060d1877a6692519a4bed82521328eeb0d9aa", size = 2466051, upload-time = "2026-06-21T16:03:06.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/a6/a3d29378618d3d9ba0962473f8bbf09a5ad15eb8b210c5153fb88d0b8aad/ase-3.29.0-py3-none-any.whl", hash = "sha256:7b9dd103f007810339c24acfee2f6b677c0c48443b21d3c98e52959246cf4ebf", size = 3014724, upload-time = "2026-06-21T16:03:03.794Z" }, +] + [[package]] name = "asttokens" version = "3.0.2" @@ -2916,6 +2933,7 @@ name = "quantem" version = "0.1.9" source = { editable = "." } dependencies = [ + { name = "ase" }, { name = "cmasher" }, { name = "colorspacious" }, { name = "dill" }, @@ -2929,6 +2947,7 @@ dependencies = [ { name = "scikit-image" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "spglib" }, { name = "tensorboard" }, { name = "torch" }, { name = "torchinfo" }, @@ -2961,6 +2980,7 @@ test = [ [package.metadata] requires-dist = [ + { name = "ase" }, { name = "cmasher", specifier = ">=1.9.2" }, { name = "colorspacious" }, { name = "dill" }, @@ -2973,6 +2993,7 @@ requires-dist = [ { name = "rosettasciio", specifier = ">=0.8.0" }, { name = "scikit-image", specifier = ">=0.25.2" }, { name = "scipy" }, + { name = "spglib" }, { name = "tensorboard", specifier = ">=2.19.0" }, { name = "torch", specifier = ">=2.7.0" }, { name = "torchinfo", specifier = ">=1.8.0" }, @@ -3520,6 +3541,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" }, ] +[[package]] +name = "spglib" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/06/7964acb4c444191376bd87f91579475fbe7623ca943cce40cee8fb7f2c36/spglib-2.7.0.tar.gz", hash = "sha256:c40907a42c9dc45572f46740bf95412f84fb0eda30267e31665d104a4bde6627", size = 2366134, upload-time = "2025-12-29T09:48:26.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/a8/d841ae7743c58227af277f7f16aa844376fa11c426090d6ae35e7e93af76/spglib-2.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cf0ff80c01d8631ef4b9f1b78da79ff2044834e6e2d870f7f20c8579c921136", size = 910793, upload-time = "2025-12-29T09:47:32.063Z" }, + { url = "https://files.pythonhosted.org/packages/e8/02/11baf94cf682cdaafa046b72d4b2adcf944e19e2b2741454e329dedb2fc2/spglib-2.7.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7b29d2cfca6ac53e927686ca0b91257126e47f6abfa26451723a5cd40070352", size = 944977, upload-time = "2025-12-29T09:47:33.638Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fa/6d1bc8f8cb08945ca8c37c95b42bf336b6b9a8a737eced1ce64f0cebe9ce/spglib-2.7.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f892ecce2dd1bc636b14a4e5bc13aabb73b008bd37a4d23636882c8971c432a0", size = 960531, upload-time = "2025-12-29T09:47:36.932Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/2fd5e33b431cd0afcdd441bd10704c11cdf74c09b721249297284e5bf0b2/spglib-2.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:468879702577124dcde0607a75396576e256f1cfa2d8fe48da4a928fbb27abc6", size = 669827, upload-time = "2025-12-29T09:47:38.47Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e9/4e07c9c1bda40df54e09bd686eae0dc13d46e76a5ef4d43582971a86eb32/spglib-2.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:ceb6730a2324d0c83579c803f3782e28bd41e79bbfe0c3dfdbf30e3d3a6320e5", size = 649076, upload-time = "2025-12-29T09:47:40.367Z" }, + { url = "https://files.pythonhosted.org/packages/68/0e/36720beeca8452530e50ab8a16b91e8721e34c0f97fd25e9c4ddd8b9324b/spglib-2.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ef70132e23dfcc7ab6813742e0edab3f9906e61cd11c857f014bd5610a8bc88c", size = 911009, upload-time = "2025-12-29T09:47:42.238Z" }, + { url = "https://files.pythonhosted.org/packages/47/a0/24df91cbde6a3237d54cfb21602cc8ebb4102cd4e3ec9497c66135c2b190/spglib-2.7.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59f134e74f7f488de4bf5579ee6a35af25cb2c478c138de664fea1e14f3efbaf", size = 946821, upload-time = "2025-12-29T09:47:44.548Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/75ac6f7ade28019b216c7333322f2886e1c0105202cd74506f530664bf26/spglib-2.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6913906fd9108e7bb2ce06a810513a95a82d801530f10230979bf3427bb7e771", size = 962531, upload-time = "2025-12-29T09:47:46.3Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/4e283139af178bb445eedff281a90e66ceff1b814ace70a9d90a2197acc3/spglib-2.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:d5729ff0040baae764c17249302cd99f0eb4e73449612a8c69d3e60a215f062e", size = 671111, upload-time = "2025-12-29T09:47:48.14Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/20e52d46e33bf69ceba4fc86602f006c06ce4ab10e3c930f4722fb270b02/spglib-2.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:54f4b6e789475384c62e759c618172707f261c0eae8017949fe4994b6b8cc779", size = 646679, upload-time = "2025-12-29T09:47:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1c/a0fe8c0523a0e7d608f49f09895e5c599329265c9bfacd269a21458b7564/spglib-2.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab061ea6a3c3c25a1d0018b09c333c0458792036d3f45d892bd52793ed1f1bda", size = 911085, upload-time = "2025-12-29T09:47:51.606Z" }, + { url = "https://files.pythonhosted.org/packages/2a/34/cb3c522c4aaf6ce319b37bbec71d373b9e2cf0bcfe7d42c365cd6c113b4b/spglib-2.7.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be28673e90f7a6c7770f73c57e529d2bdbb373d06d26ee5e90991b548e9238aa", size = 946857, upload-time = "2025-12-29T09:47:53.059Z" }, + { url = "https://files.pythonhosted.org/packages/9e/64/3b1213f2f655ff143ed142292b47ec3f1f9bda8641e659a7e33c4cf0e8a9/spglib-2.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f627a4ed6f2396ed6e3e8eaf33a53ad143c8ffb8756a84a640f4569ac5ffa2a7", size = 962470, upload-time = "2025-12-29T09:47:54.878Z" }, + { url = "https://files.pythonhosted.org/packages/5c/3a/c51883ce739a00f9f60196f3dcb4ed91b690299a4ec64defd8ec5b2c5899/spglib-2.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:c76411bc1b96cd87c8733994747c7692512b583bb4ef89a65463ff4255221c11", size = 671073, upload-time = "2025-12-29T09:47:56.887Z" }, + { url = "https://files.pythonhosted.org/packages/35/78/3f9ec6ae93a48527dce0eceb6eeab74e6ad1fb2977adb5cbdfc03d43193c/spglib-2.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:0d8ecf030d13d67c4cc272423e5652b74eda57f86a0b118e007f6d12974cc256", size = 646711, upload-time = "2025-12-29T09:47:58.697Z" }, + { url = "https://files.pythonhosted.org/packages/1b/47/86e3c15c3e1c252bde40a794eea4742c142f23fc5f9c3d7551f083c1fa20/spglib-2.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:95e3dd7ef992ff8a88f6ef2e5909aaa60ecb479004cc1f73c1e6285d54227960", size = 911712, upload-time = "2025-12-29T09:48:01.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/61/ab2447bb47fa69934adc2fc2d13f771dedd3b2fd3171c95307446c948f01/spglib-2.7.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97e0fcea2db3915bd973fdd2cc0a757b1f99bda71ce815da333d75ad1ffc3eb1", size = 947528, upload-time = "2025-12-29T09:48:03.258Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/898d9e005131b0b1c7e5dce2b79f36aeb20ec4d3a88cca596b522a0fa4df/spglib-2.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39b978c08ef2ebc0eaba833c488fc4c0f9b1fc0f50d4a8584f176741eea69376", size = 962474, upload-time = "2025-12-29T09:48:05.617Z" }, + { url = "https://files.pythonhosted.org/packages/c8/56/7b25ee5348722dc93ca245ed950f1a89f8a944906140629055f394c072a4/spglib-2.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:5f334b4b66c8aafd583fafab5b15a56e27efdd2dc6cb1064dfcd0fe59ae130f4", size = 679679, upload-time = "2025-12-29T09:48:15.393Z" }, + { url = "https://files.pythonhosted.org/packages/20/37/eda9a34f25b13e47298fa1b94cc4dfd8b0fcfc46c7d63ea046aa1bf91fe7/spglib-2.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:b032842fc223de46d2ef7d220459e1a61ed90329ac2e72818c605f1fc87451b8", size = 656403, upload-time = "2025-12-29T09:48:17.027Z" }, + { url = "https://files.pythonhosted.org/packages/39/af/1c8d0f98d07969b7fa7323d522732124d88caf4ee3b680ef59120bd7b229/spglib-2.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b7e29c796cfdadcc3857aef330acc19b9bc50c83e9911fb23b28390e7c80bae5", size = 920791, upload-time = "2025-12-29T09:48:07.085Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c6/89a3f31f831efc4108a19f110873559990b72186745cd3e151de28b256cc/spglib-2.7.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b6ca88bb6e604bc8f63efe87b3b2470c2e25f56988b775bd332cefa8866f5c5", size = 946881, upload-time = "2025-12-29T09:48:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/1ca63db2cebd381bd6b27ae309f25d270e70928359a6f0360db09b77894e/spglib-2.7.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50629939a9cd6fa3df5a12f6f025ceb3c78534284f875371574c360e4ccaf5e1", size = 963803, upload-time = "2025-12-29T09:48:12.478Z" }, + { url = "https://files.pythonhosted.org/packages/28/97/459b37c3802633f77c883883c75f5d4429b601ae8d930410b999c4e1dafb/spglib-2.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:cb77daaf9dd5d48d523a888f37cebd47fa63ff28dfcf1aac2b031b914f9ed55a", size = 696536, upload-time = "2025-12-29T09:48:13.885Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.51" From eedf281754b665a7de4bad41c423a4d295819526 Mon Sep 17 00:00:00 2001 From: cophus Date: Tue, 8 Sep 2026 09:13:58 +0200 Subject: [PATCH 03/36] adding bloch wave functions --- src/quantem/diffraction/bloch.py | 819 +++++++++++++++++++++++++++++-- tests/diffraction/test_cbed.py | 134 +++++ 2 files changed, 914 insertions(+), 39 deletions(-) create mode 100644 tests/diffraction/test_cbed.py diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py index a15d2f5b0..2391af513 100644 --- a/src/quantem/diffraction/bloch.py +++ b/src/quantem/diffraction/bloch.py @@ -23,7 +23,7 @@ from quantem.core.utils.utils import electron_wavelength_angstrom from quantem.diffraction.crystal import Crystal -from quantem.diffraction.rotations import qrotate +from quantem.diffraction.rotations import qrotate, sample_zone_axes def relativistic_gamma(energy_ev: float) -> float: @@ -31,6 +31,59 @@ def relativistic_gamma(energy_ev: float) -> float: return 1.0 + float(energy_ev) / 510998.95 +def _coupling_matrix( + crystal: Crystal, hkl_beams: torch.Tensor, gamma_rel: float +) -> tuple[torch.Tensor, float, bool]: + """Off-diagonal Bloch coupling matrix U_(g-h) for a beam list. + + Prefers the absorptive Weickenmeier-Kohl factors when the crystal has + them (calculate_dynamical_structure_factors); they carry the + relativistic and 1/pi factors already. Falls back to the kinematical + (Lobato) factors, purely elastic. + + Returns + ------- + U : torch.Tensor + (nb, nb) complex coupling matrix with zero diagonal. + u0_imag : float + Imaginary part of U_000 (mean absorption), 0 without absorption. + absorptive : bool + Whether absorptive factors were used. + """ + absorptive = getattr(crystal, "U_dyn", None) is not None + if absorptive: + hkl_all = crystal.hkl_dyn + U_all = crystal.U_dyn + else: + hkl_all = crystal.hkl + U_all = crystal.struct_factors * (gamma_rel / np.pi) + span = 2 * int(hkl_all.abs().max()) + 1 + key_mult = torch.tensor([1, span, span**2], dtype=torch.long) + + def keys(h): + return (h * key_mult[None, :]).sum(dim=1) + + lut = {int(k): i for i, k in enumerate(keys(hkl_all))} + + nb = hkl_beams.shape[0] + diff = hkl_beams[:, None, :] - hkl_beams[None, :, :] # (nb, nb, 3) + diff_keys = (diff * key_mult[None, None, :]).sum(dim=-1) + U = torch.zeros((nb, nb), dtype=torch.complex128) + idx = torch.tensor( + [lut.get(int(k), -1) for k in diff_keys.reshape(-1)], dtype=torch.long + ).reshape(nb, nb) + has = idx >= 0 + U[has] = U_all[idx[has]] + U.fill_diagonal_(0) + + u0_imag = 0.0 + if absorptive: + i0 = lut.get(0, -1) + if i0 >= 0: + u0_imag = float(U_all[i0].imag) + return U, u0_imag, absorptive + + def dynamical_pattern( crystal: Crystal, orientation: torch.Tensor, @@ -82,52 +135,16 @@ def dynamical_pattern( s_sel = s_g[sel] n = int(sel.sum()) - # structure factor lookup for all difference vectors (h_i - h_j). - # Prefer the absorptive Weickenmeier-Kohl factors if the crystal has - # them (calculate_dynamical_structure_factors); they carry the - # relativistic and 1/pi factors already. Fall back to the kinematical - # (Lobato) factors, purely elastic. - absorptive = getattr(crystal, "U_dyn", None) is not None - if absorptive: - hkl_all = crystal.hkl_dyn - U_all = crystal.U_dyn - else: - hkl_all = crystal.hkl - U_all = crystal.struct_factors * (gamma_rel / np.pi) - key_mult = torch.tensor( - [1, 2 * int(hkl_all.abs().max()) + 1, (2 * int(hkl_all.abs().max()) + 1) ** 2], - dtype=torch.long, - ) - - def keys(h): - return (h * key_mult[None, :]).sum(dim=1) - - lut = {int(k): i for i, k in enumerate(keys(hkl_all))} - # beams list includes the (000) beam at index 0 hkl_beams = torch.cat([torch.zeros((1, 3), dtype=torch.long), hkl_sel]) s_beams = torch.cat([torch.zeros(1, dtype=torch.float64), s_sel]) - nb = n + 1 - - diff = hkl_beams[:, None, :] - hkl_beams[None, :, :] # (nb, nb, 3) - diff_keys = (diff * key_mult[None, None, :]).sum(dim=-1) - U = torch.zeros((nb, nb), dtype=torch.complex128) - flat = diff_keys.reshape(-1) - idx = torch.tensor( - [lut.get(int(k), -1) for k in flat], dtype=torch.long - ).reshape(nb, nb) - has = idx >= 0 - U[has] = U_all[idx[has]] + U, u0_imag, absorptive = _coupling_matrix(crystal, hkl_beams, gamma_rel) A = U.clone() - A.fill_diagonal_(0) diag = (2 * k0 * s_beams).to(torch.complex128) if absorptive: # mean absorption: imaginary part of U_000 damps every beam - u0 = keys(torch.zeros((1, 3), dtype=torch.long)) - i0 = lut.get(int(u0[0]), -1) - if i0 >= 0: - diag = diag + 1j * U_all[i0].imag + diag = diag + 1j * u0_imag A += torch.diag(diag) if absorptive: @@ -275,3 +292,727 @@ def refine_thickness( "phase_index": phase_index, "thickness_per_candidate": thick_out, } + + +def _cbed_amplitudes( + crystal: Crystal, + orientation: torch.Tensor, + tilts: torch.Tensor, + thicknesses_A: torch.Tensor, + energy_ev: float, + sg_max: float, + k_max: float | None, + tilt_batch: int = 64, + progress_bar: bool = False, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Bloch intensities of every beam at every incident tilt. + + The coupling matrix is built once; only the diagonal (excitation + errors) changes with tilt, and the eigendecompositions are batched + over tilt chunks. + + Parameters + ---------- + tilts : torch.Tensor + (M, 2) in-plane incident wavevector components (1/Angstroms). + + Returns + ------- + intensity : torch.Tensor + (M, T, nb) beam intensities per tilt and thickness; beam 0 is the + direct (000) beam. + g_xy : torch.Tensor + (nb, 2) in-plane reciprocal vectors of the beams (000 first). + hkl_beams : torch.Tensor + (nb, 3) Miller indices of the beams. + """ + lam = electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + gamma_rel = relativistic_gamma(energy_ev) + t_thick = torch.atleast_1d( + torch.as_tensor(thicknesses_A, dtype=torch.float64) + ) + + # beam selection: near the Ewald sphere for ANY tilt in the aperture -- + # a tilt t shifts s_g by at most |t| * |g| / k0 to leading order + g_lab = qrotate(orientation, crystal.g_vec) + gz, g2 = g_lab[:, 2], (g_lab**2).sum(dim=1) + s0 = (2 * gz - lam * g2) / (2 - 2 * lam * gz) + alpha_max = float(torch.linalg.norm(tilts, dim=1).max()) / k0 + sel = torch.abs(s0) < sg_max + alpha_max * crystal.g_len + if k_max is not None: + sel &= crystal.g_len <= k_max + hkl_beams = torch.cat( + [torch.zeros((1, 3), dtype=torch.long), crystal.hkl[sel]] + ) + g_beams = torch.cat( + [torch.zeros((1, 3), dtype=torch.float64), g_lab[sel]] + ) + nb = hkl_beams.shape[0] + + U, u0_imag, absorptive = _coupling_matrix(crystal, hkl_beams, gamma_rel) + + gx, gy, gzb = g_beams[:, 0], g_beams[:, 1], g_beams[:, 2] + g2b = (g_beams**2).sum(dim=1) + M = tilts.shape[0] + out = torch.zeros((M, t_thick.shape[0], nb), dtype=torch.float64) + chunks = range(0, M, tilt_batch) + if progress_bar: + chunks = tqdm(chunks, desc="Bloch tilts") + for m0 in chunks: + m1 = min(m0 + tilt_batch, M) + tt = tilts[m0:m1] # (B, 2) + kz = torch.sqrt(k0**2 - (tt**2).sum(dim=1)) # (B,) + # s_g for incident k = (tx, ty, -kz), surface normal along z + num = ( + 2 * kz[:, None] * gzb[None, :] + - 2 * (tt[:, 0, None] * gx[None, :] + tt[:, 1, None] * gy[None, :]) + - g2b[None, :] + ) + den = 2 * (kz[:, None] - gzb[None, :]) + s_t = num / den # (B, nb) + + out[m0:m1] = _bloch_solve(U, u0_imag, absorptive, s_t, k0, t_thick) + return out, g_beams[:, :2], hkl_beams + + +def _bloch_solve( + U: torch.Tensor, + u0_imag: float, + absorptive: bool, + s_t: torch.Tensor, + k0: float, + t_thick: torch.Tensor, + perturbative: bool = False, +) -> torch.Tensor: + """Batched Bloch solve: intensities (B, T, nb) for excitation errors s_t + (B, nb) with a shared coupling matrix U (nb, nb). + + With perturbative=True the Hermitian part is diagonalized (eigh, much + faster and better batched than the general complex eig) and the weak + absorption enters first order: gamma_imag = diag(C^dagger U'' C)/(2 k0). + Standard for master-pattern computations; the absorptive parts of U are + a few percent of the elastic parts, so the first-order error is small. + """ + nb = U.shape[0] + if absorptive and perturbative: + H = 0.5 * (U + U.conj().T) + W = (U - H) / 1j # Hermitian absorptive part (off-diagonal) + A = H[None].expand(s_t.shape[0], nb, nb).clone() + A += torch.diag_embed((2 * k0 * s_t).to(torch.complex128)) + evals, C = torch.linalg.eigh(A) + gam_r = evals / (2 * k0) # (B, nb) real + CW = torch.einsum("bji,jk,bki->bi", C.conj(), W, C).real + gam_i = (CW + u0_imag) / (2 * k0) # (B, nb) + gam = gam_r.to(torch.complex128) + 1j * gam_i + psi0 = C.conj().transpose(1, 2)[:, :, 0] # unitary: C^-1 = C^dagger + else: + diag = (2 * k0 * s_t).to(torch.complex128) + if absorptive: + diag = diag + 1j * u0_imag + A = U[None].expand(s_t.shape[0], nb, nb).clone() + A += torch.diag_embed(diag) + if absorptive: + evals, C = torch.linalg.eig(A) + gam = evals / (2 * k0) + else: + evals, C = torch.linalg.eigh(A) + gam = (evals.real / (2 * k0)).to(torch.complex128) + psi0 = torch.linalg.inv(C)[:, :, 0] # (B, nb) + phase = torch.exp( + 2j * np.pi * gam[:, None, :] * t_thick.to(torch.complex128)[None, :, None] + ) # (B, T, nb) + psi = torch.einsum("bij,btj,bj->bti", C, phase, psi0) + return torch.abs(psi) ** 2 + + +def tilt_grid(semiconv_mrad: float, energy_ev: float, n_rings: int = 8): + """Concentric-ring sampling of the illumination aperture. + + Returns (M, 2) in-plane incident wavevectors (1/Angstroms) covering the + disk of semiangle `semiconv_mrad`, with approximately uniform density. + """ + lam = electron_wavelength_angstrom(energy_ev) + alpha_k = semiconv_mrad * 1e-3 / lam + pts = [(0.0, 0.0)] + for r in range(1, n_rings + 1): + rad = alpha_k * r / n_rings + n_az = int(np.ceil(2 * np.pi * r)) + th = 2 * np.pi * (np.arange(n_az) + 0.5 * (r % 2)) / n_az + pts += [(rad * np.cos(a), rad * np.sin(a)) for a in th] + return torch.tensor(pts, dtype=torch.float64) + + +def calculate_cbed( + crystal: Crystal, + orientation: torch.Tensor, + thicknesses_A, + energy_ev: float = 300e3, + semiconv_mrad: float = 3.0, + n_rings: int = 8, + sg_max: float = 0.1, + k_max: float | None = None, + pixel_size: float | None = None, + q_max_plot: float | None = None, + tilt_batch: int = 64, +) -> dict: + """Simulate a CBED pattern with Bloch waves. + + Every incident direction inside the aperture is an independent plane + wave (incoherent illumination): its Bloch intensities are placed at + g + t in the detector plane, filling each diffraction disk with the + rocking-curve intensity variation. Uses the absorptive + Weickenmeier-Kohl structure factors when the crystal carries them. + + Parameters + ---------- + crystal : Crystal + With structure factors calculated (cover 2x k_max so every + difference vector g - h has a coupling). + orientation : torch.Tensor + Unit quaternion (4,). + thicknesses_A : float | array-like + One or more specimen thicknesses in Angstroms. + semiconv_mrad : float, default=3.0 + Convergence semiangle. Disks overlap when it exceeds half the + smallest g spacing times the wavelength. + n_rings : int, default=8 + Radial sampling rings across the aperture (~200 tilts at 8). + sg_max : float, default=0.1 + Excitation error cutoff for beam selection (widened automatically + by the aperture tilt range). + k_max : float | None + In-plane cutoff for included beams. + pixel_size : float | None + Detector sampling (1/Angstroms per pixel); default disk radius / 12. + q_max_plot : float | None + Half-width of the detector; default covers all beams plus a disk. + + Returns + ------- + dict with 'pattern' ((T, H, W), squeezed to (H, W) for one thickness), + 'sampling' (1/Angstroms per pixel), 'disk_radius' (1/Angstroms), + 'thicknesses', 'hkl', 'g_xy'. + """ + lam = electron_wavelength_angstrom(energy_ev) + alpha_k = semiconv_mrad * 1e-3 / lam + tilts = tilt_grid(semiconv_mrad, energy_ev, n_rings=n_rings) + t_thick = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) + + inten, g_xy, hkl_beams = _cbed_amplitudes( + crystal, orientation, tilts, t_thick, energy_ev, sg_max, k_max, tilt_batch + ) + + if pixel_size is None: + pixel_size = alpha_k / 12 + if q_max_plot is None: + q_max_plot = float(torch.linalg.norm(g_xy, dim=1).max()) + 2 * alpha_k + half = int(np.ceil(q_max_plot / pixel_size)) + H = 2 * half + 1 + + # deposit every (beam, tilt) sample with bilinear weights + qx = (g_xy[:, 0][None, :] + tilts[:, 0][:, None]).numpy() # (M, nb) + qy = (g_xy[:, 1][None, :] + tilts[:, 1][:, None]).numpy() + fx = qx / pixel_size + half + fy = qy / pixel_size + half + ix0 = np.floor(fx).astype(int) + iy0 = np.floor(fy).astype(int) + wx = fx - ix0 + wy = fy - iy0 + + T = t_thick.shape[0] + pattern = np.zeros((T, H, H)) + I = inten.numpy() # (M, T, nb) + for dx in (0, 1): + for dy in (0, 1): + w = (wx if dx else 1 - wx) * (wy if dy else 1 - wy) + jx = np.clip(ix0 + dx, 0, H - 1) + jy = np.clip(iy0 + dy, 0, H - 1) + for ti in range(T): + np.add.at(pattern[ti], (jx, jy), w * I[:, ti, :]) + pattern /= tilts.shape[0] + + return { + "pattern": pattern[0] if T == 1 else pattern, + "sampling": float(pixel_size), + "disk_radius": float(alpha_k), + "thicknesses": t_thick.numpy(), + "hkl": hkl_beams.numpy(), + "g_xy": g_xy.numpy(), + } + + +def calculate_lacbed( + crystal: Crystal, + orientation: torch.Tensor, + thicknesses_A, + hkl, + energy_ev: float = 300e3, + semiconv_mrad: float = 10.0, + n_pixels: int = 48, + sg_max: float = 0.1, + k_max: float | None = None, + tilt_batch: int = 64, +) -> dict: + """Large-angle CBED: one reflection's rocking surface over the aperture. + + The intensity of the chosen reflection is mapped over the incident-tilt + disk on a square grid (parallax / LACBED view of a single disk, without + the geometric overlap of neighboring disks). + + Returns + ------- + dict with 'disk' ((T, n, n) squeezed), 'tilt_max' (1/Angstroms), + 'thicknesses'. Pixels outside the aperture are NaN. + """ + lam = electron_wavelength_angstrom(energy_ev) + alpha_k = semiconv_mrad * 1e-3 / lam + ax = torch.linspace(-alpha_k, alpha_k, n_pixels, dtype=torch.float64) + ty, tx = torch.meshgrid(ax, ax, indexing="ij") + inside = (tx**2 + ty**2) <= alpha_k**2 + tilts = torch.stack([tx[inside], ty[inside]], dim=1) + t_thick = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) + + inten, _, hkl_beams = _cbed_amplitudes( + crystal, orientation, tilts, t_thick, energy_ev, sg_max, k_max, tilt_batch + ) + match = (hkl_beams == torch.as_tensor(hkl, dtype=torch.long)[None, :]).all(dim=1) + if not bool(match.any()): + raise ValueError(f"reflection {tuple(hkl)} is not among the excited beams") + b = int(match.nonzero()[0]) + + T = t_thick.shape[0] + disk = np.full((T, n_pixels, n_pixels), np.nan) + m = inside.numpy() + for ti in range(T): + plane = np.full((n_pixels, n_pixels), np.nan) + plane[m] = inten[:, ti, b].numpy() + disk[ti] = plane + return { + "disk": disk[0] if T == 1 else disk, + "tilt_max": float(alpha_k), + "thicknesses": t_thick.numpy(), + } + + +def calculate_cbed_library( + crystal: Crystal, + orientations: torch.Tensor, + thickness_A: float, + energy_ev: float = 300e3, + semiconv_mrad: float = 3.0, + k_max: float | None = None, + q_max_plot: float | None = None, + pixel_size: float | None = None, + progress_bar: bool = True, + **kwargs, +) -> dict: + """A stack of simulated CBED patterns on one common detector grid. + + The starting point for CBED orientation matching: all patterns share + the same sampling and extent, ready for polar transformation and + correlation. One entry per orientation. + + Returns + ------- + dict with 'patterns' (N, H, W), 'quats' (N, 4), 'sampling', + 'disk_radius', 'thickness_A'. + """ + lam = electron_wavelength_angstrom(energy_ev) + alpha_k = semiconv_mrad * 1e-3 / lam + if pixel_size is None: + pixel_size = alpha_k / 12 + if q_max_plot is None: + base = k_max if k_max is not None else float(crystal.k_max) / 2 + q_max_plot = base + 2 * alpha_k + + quats = torch.atleast_2d(torch.as_tensor(orientations, dtype=torch.float64)) + pats = [] + it = range(quats.shape[0]) + if progress_bar: + it = tqdm(it, desc="CBED library") + for i in it: + res = calculate_cbed( + crystal, + quats[i], + thickness_A, + energy_ev=energy_ev, + semiconv_mrad=semiconv_mrad, + k_max=k_max, + pixel_size=pixel_size, + q_max_plot=q_max_plot, + **kwargs, + ) + pats.append(res["pattern"]) + return { + "patterns": np.stack(pats), + "quats": quats.numpy(), + "sampling": float(pixel_size), + "disk_radius": float(alpha_k), + "thickness_A": float(thickness_A), + } + + +def calculate_kossel( + crystal: Crystal, + orientation: torch.Tensor, + thicknesses_A, + energy_ev: float = 300e3, + semiconv_mrad: float = 40.0, + n_pixels: int = 192, + sg_max: float = 0.05, + k_max: float | None = None, + tilt_batch: int = 64, + progress_bar: bool = True, +) -> dict: + """Wide-angle convergent beam (Kossel) pattern with Bloch waves. + + At convergence angles far beyond the Bragg angles the diffraction disks + overlap completely and the pattern becomes a continuous map of + deficiency and excess lines (the Kossel regime of CBED; the bright + field disk alone is the LACBED view). One Bloch computation over the + incident-tilt grid yields both: + + - 'bright_field': the (000) beam intensity at each incident tilt -- + the deficiency (dark) line system, every line at a Bragg condition. + - 'pattern': the full detector intensity, the incoherent sum of every + diffracted cone shifted by its g -- deficiency lines from the direct + beam plus the excess (bright) lines of the diffracted beams. + + Line positions are exact; line profiles carry the many-beam dynamical + structure, with the deficiency/excess asymmetry from the absorptive + structure factors when the crystal has them. + + Parameters + ---------- + crystal : Crystal + With structure factors calculated (cover 2x k_max for couplings), + and ideally calculate_dynamical_structure_factors for absorption. + orientation : torch.Tensor + Unit quaternion (4,). + thicknesses_A : float | array-like + One or more thicknesses in Angstroms. + semiconv_mrad : float, default=40.0 + Convergence semiangle; the pattern covers this angular radius. + n_pixels : int, default=192 + Detector pixels across the pattern (also the tilt sampling; the + 1-2 mrad dynamical line widths need ~0.5 mrad per pixel). + sg_max : float, default=0.05 + Excitation error cutoff; the beam list is widened by the aperture + automatically. + k_max : float | None + In-plane cutoff for included reflections. + + Returns + ------- + dict with 'bright_field' and 'pattern' ((T, n, n), squeezed for one + thickness; NaN / 0 outside the aperture), 'sampling' (1/Angstroms per + pixel), 'mrad_per_pixel', 'thicknesses', 'hkl'. + """ + lam = electron_wavelength_angstrom(energy_ev) + alpha_k = semiconv_mrad * 1e-3 / lam + ax = torch.linspace(-alpha_k, alpha_k, n_pixels, dtype=torch.float64) + px = float(ax[1] - ax[0]) + ty, tx = torch.meshgrid(ax, ax, indexing="ij") + inside = (tx**2 + ty**2) <= alpha_k**2 + tilts = torch.stack([tx[inside], ty[inside]], dim=1) + t_thick = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) + T = t_thick.shape[0] + + inten, g_xy, hkl_beams = _cbed_amplitudes( + crystal, orientation, tilts, t_thick, energy_ev, sg_max, k_max, + tilt_batch, progress_bar=progress_bar, + ) + I = inten.numpy() # (M, T, nb) + m = inside.numpy() + + # bright field: beam 0 on the tilt grid directly + bright = np.full((T, n_pixels, n_pixels), np.nan) + for ti in range(T): + plane = np.full((n_pixels, n_pixels), np.nan) + plane[m] = I[:, ti, 0] + bright[ti] = plane + + # full pattern: every diffracted cone shifted by its g, bilinear deposit + pattern = np.zeros((T, n_pixels, n_pixels)) + tx_in = tilts[:, 0].numpy() + ty_in = tilts[:, 1].numpy() + g_np = g_xy.numpy() + for b in range(g_np.shape[0]): + fx = (tx_in + g_np[b, 0] + alpha_k) / px + fy = (ty_in + g_np[b, 1] + alpha_k) / px + ix0 = np.floor(fx).astype(int) + iy0 = np.floor(fy).astype(int) + wx = fx - ix0 + wy = fy - iy0 + for dx in (0, 1): + for dy in (0, 1): + jx = ix0 + dx + jy = iy0 + dy + ok = (jx >= 0) & (jx < n_pixels) & (jy >= 0) & (jy < n_pixels) + w = (wx if dx else 1 - wx) * (wy if dy else 1 - wy) + for ti in range(T): + np.add.at( + pattern[ti], (jx[ok], jy[ok]), (w * I[:, ti, b])[ok] + ) + pattern[:, ~m] = 0.0 + + return { + "bright_field": bright[0] if T == 1 else bright, + "pattern": pattern[0] if T == 1 else pattern, + "sampling": px, + "mrad_per_pixel": px * lam * 1e3, + "thicknesses": t_thick.numpy(), + "hkl": hkl_beams.numpy(), + } + + +def calculate_kossel_master( + crystal: Crystal, + thicknesses_A, + energy_ev: float = 300e3, + angle_step_mrad: float = 1.0, + sg_max: float = 0.05, + k_max: float | None = None, + theta_max_deg: float = 90.0, + chunk: int = 256, + perturbative: bool = True, + progress_bar: bool = True, +) -> dict: + """Kossel master pattern: the dynamical bright field over all directions. + + The bright field intensity depends only on the incident beam direction + in the CRYSTAL frame (each incident plane wave is independent), so one + Bloch computation over the symmetry-reduced direction wedge gives the + pattern for every specimen orientation at once -- the EMsoft master + pattern strategy. Patterns for arbitrary orientations, convergence + angles, and all precomputed thicknesses are then interpolation lookups + via kossel_from_master(), microseconds instead of a fresh dynamical + calculation. + + The wedge samples are expanded by the crystal's proper rotations plus + inversion and rasterized onto a Lambert azimuthal equal-area grid of + the upper hemisphere. (The inversion expansion assumes Friedel symmetry + of the bright field; for non-centrosymmetric crystals with absorption + this neglects a small polarity contrast.) + + Parameters + ---------- + crystal : Crystal + With structure factors calculated (cover 2x k_max), and ideally + calculate_dynamical_structure_factors for absorption. + thicknesses_A : float | array-like + Thickness grid; all thicknesses share the eigendecompositions, so a + thickness AXIS is nearly free -- precompute the matching range here. + angle_step_mrad : float, default=1.0 + Angular sampling of the wedge. The dynamical line widths are + 1-2 mrad; 0.5 for production masters, 1-2 for quick looks. + theta_max_deg : float, default=90.0 + Polar cutoff of the WEDGE samples. Keep at 90 unless the wedge's + far corners are never observed: cutting the wedge leaves coverage + holes at all their symmetry equivalents. + + Returns + ------- + dict with 'lambert' (T, n, n) master on the equal-area grid (NaN where + unsampled), 'rho_max', 'thicknesses', 'energy_ev', and the raw wedge + 'directions' / 'intensity'. + """ + lam = electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + gamma_rel = relativistic_gamma(energy_ev) + t_thick = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) + T = t_thick.shape[0] + + wedge = crystal.zone_axis_wedge() + step_deg = np.rad2deg(angle_step_mrad * 1e-3) + if wedge is None: + n_dirs = int(np.ceil(2 * np.pi / np.deg2rad(step_deg) ** 2)) + dirs = fibonacci_hemisphere(n_dirs) + else: + dirs, _ = sample_zone_axes(wedge, step_deg) + keep = dirs[:, 2] >= np.cos(np.deg2rad(theta_max_deg)) + dirs = dirs[keep] + N = dirs.shape[0] + + g = crystal.g_vec # crystal frame, orientation is identity + gz, g2 = g[:, 2], (g**2).sum(dim=1) + g_len = crystal.g_len + + out = torch.zeros((N, T), dtype=torch.float64) + chunks = range(0, N, chunk) + if progress_bar: + chunks = tqdm(chunks, desc="Kossel master") + for c0 in chunks: + c1 = min(c0 + chunk, N) + d = dirs[c0:c1] # (B, 3) beam directions in the crystal frame + d_c = d.mean(dim=0) + d_c = d_c / torch.linalg.norm(d_c) + radius = float(torch.arccos((d @ d_c).clamp(-1, 1)).max()) + + # normal-tracking geometry: each sample is computed with the foil + # normal along the sampled direction (the crystal is conceptually + # re-tilted per sample). The slab problem is then a function of the + # crystal-frame beam direction ALONE, which is what makes the + # symmetry expansion below exact; a pattern lookup only ever probes + # directions within the convergence semiangle of the true normal, + # so the approximation error is O(alpha^2). + u_c = g @ d_c + s_c = (2 * k0 * u_c - g2) / (2 * (k0 - u_c)) + sel = torch.abs(s_c) < sg_max + (radius + 1e-4) * g_len + if k_max is not None: + sel &= g_len <= k_max + hkl_beams = torch.cat( + [torch.zeros((1, 3), dtype=torch.long), crystal.hkl[sel]] + ) + g_b = torch.cat([torch.zeros((1, 3), dtype=torch.float64), g[sel]]) + U, u0_imag, absorptive = _coupling_matrix(crystal, hkl_beams, gamma_rel) + + g2b = (g_b**2).sum(dim=1) + u = torch.einsum("bk,nk->bn", d, g_b) # g . d_hat per sample + s_t = (2 * k0 * u - g2b[None, :]) / (2 * (k0 - u)) + I = _bloch_solve( + U, u0_imag, absorptive, s_t, k0, t_thick, perturbative=perturbative + ) + out[c0:c1] = I[:, :, 0] + + # symmetry expansion and Lambert raster: with normal-tracking geometry + # the intensity is a function of the crystal-frame beam direction only, + # so proper rotations apply directly; the reversed beam (with reversed + # normal) gives the same bright field by reciprocity. + from quantem.diffraction.rotations import quat_to_matrix + + Rs = quat_to_matrix(crystal.sym_quats_matching) + d_all = torch.einsum("sij,nj->sni", Rs, dirs).reshape(-1, 3) + I_all = out[None, :, :].expand(Rs.shape[0], -1, -1).reshape(-1, T) + d_all = torch.cat([d_all, -d_all]) + I_all = torch.cat([I_all, I_all]) + up = d_all[:, 2] >= 0 + d_all, I_all = d_all[up], I_all[up] + + # grid always spans the full hemisphere: symmetry expansion moves wedge + # samples to any polar angle, and clipping them onto a smaller rim + # corrupts the equatorial region + rho_max = float(np.sqrt(2.0)) + step = angle_step_mrad * 1e-3 + half = int(np.ceil(rho_max / step)) + n = 2 * half + 1 + rho = torch.sqrt((2 * (1 - d_all[:, 2])).clamp_min(0)) + dxy = torch.linalg.norm(d_all[:, :2], dim=1).clamp_min(1e-12) + px_x = (d_all[:, 0] / dxy * rho / step + half).numpy() + px_y = (d_all[:, 1] / dxy * rho / step + half).numpy() + + acc = np.zeros((T, n, n)) + wgt = np.zeros((n, n)) + ix0 = np.floor(px_x).astype(int) + iy0 = np.floor(px_y).astype(int) + wx = px_x - ix0 + wy = px_y - iy0 + I_np = I_all.numpy() + for dx in (0, 1): + for dy in (0, 1): + jx = np.clip(ix0 + dx, 0, n - 1) + jy = np.clip(iy0 + dy, 0, n - 1) + w = (wx if dx else 1 - wx) * (wy if dy else 1 - wy) + np.add.at(wgt, (jx, jy), w) + for ti in range(T): + np.add.at(acc[ti], (jx, jy), w * I_np[:, ti]) + lambert = np.where(wgt[None] > 1e-6, acc / np.maximum(wgt[None], 1e-6), np.nan) + + # fill raster holes (unhit pixels between splatted samples) from their + # neighbors so bilinear lookups never touch NaN inside the disk + yy, xx = np.mgrid[0:n, 0:n] + in_disk = ((xx - half) ** 2 + (yy - half) ** 2) <= (rho_max / step) ** 2 + for ti in range(lambert.shape[0]): + L = lambert[ti] + for _ in range(4): + holes = np.isnan(L) & in_disk + if not holes.any(): + break + Lp = np.pad(L, 1, constant_values=np.nan) + stack = np.stack( + [Lp[1 + dy : n + 1 + dy, 1 + dx : n + 1 + dx] + for dy in (-1, 0, 1) for dx in (-1, 0, 1)] + ) + with np.errstate(all="ignore"): + fill = np.nanmean(stack, axis=0) + L[holes] = fill[holes] + lambert[ti] = L + + return { + "lambert": lambert, + "rho_max": rho_max, + "step": step, + "thicknesses": t_thick.numpy(), + "energy_ev": float(energy_ev), + "directions": dirs.numpy(), + "intensity": out.numpy(), + } + + +def kossel_from_master( + master: dict, + orientation: torch.Tensor, + semiconv_mrad: float = 40.0, + n_pixels: int = 192, +) -> dict: + """Extract a bright field Kossel pattern from a master pattern. + + Interpolation only -- microseconds per pattern per thickness. The + detector tilt grid is mapped into the crystal frame by the orientation + and looked up on the master's Lambert grid. + + Returns + ------- + dict with 'bright_field' ((T, n, n), squeezed), 'mrad_per_pixel', + 'thicknesses'. + """ + lam = electron_wavelength_angstrom(master["energy_ev"]) + k0 = 1.0 / lam + alpha_k = semiconv_mrad * 1e-3 / lam + ax = torch.linspace(-alpha_k, alpha_k, n_pixels, dtype=torch.float64) + ty, tx = torch.meshgrid(ax, ax, indexing="ij") + inside = (tx**2 + ty**2) <= alpha_k**2 + tz = torch.sqrt((k0**2 - tx**2 - ty**2).clamp_min(0)) + d_lab = torch.stack([tx, ty, tz], dim=-1) / k0 # incident directions + + from quantem.diffraction.rotations import quat_to_matrix + + R = quat_to_matrix( + torch.atleast_2d(torch.as_tensor(orientation, dtype=torch.float64))[0] + ).to(torch.float64) + d_c = torch.einsum("ji,rcj->rci", R, d_lab) # crystal frame, R^T d + d_c = torch.where(d_c[..., 2:3] < 0, -d_c, d_c) # reciprocity fold + + step = master["step"] + lambert = master["lambert"] + half = (lambert.shape[-1] - 1) // 2 + rho = torch.sqrt((2 * (1 - d_c[..., 2])).clamp_min(0)) + dxy = torch.linalg.norm(d_c[..., :2], dim=-1).clamp_min(1e-12) + fx = (d_c[..., 0] / dxy * rho / step + half).numpy() + fy = (d_c[..., 1] / dxy * rho / step + half).numpy() + + T = lambert.shape[0] + n_l = lambert.shape[-1] + ix0 = np.clip(np.floor(fx).astype(int), 0, n_l - 2) + iy0 = np.clip(np.floor(fy).astype(int), 0, n_l - 2) + wx = np.clip(fx - ix0, 0, 1) + wy = np.clip(fy - iy0, 0, 1) + bf = np.full((T, n_pixels, n_pixels), np.nan) + m = inside.numpy() + for ti in range(T): + L = lambert[ti] + val = ( + L[ix0, iy0] * (1 - wx) * (1 - wy) + + L[ix0 + 1, iy0] * wx * (1 - wy) + + L[ix0, iy0 + 1] * (1 - wx) * wy + + L[ix0 + 1, iy0 + 1] * wx * wy + ) + val[~m] = np.nan + bf[ti] = val + + return { + "bright_field": bf[0] if T == 1 else bf, + "mrad_per_pixel": 2 * semiconv_mrad / n_pixels, + "thicknesses": master["thicknesses"], + } diff --git a/tests/diffraction/test_cbed.py b/tests/diffraction/test_cbed.py new file mode 100644 index 000000000..da35405ae --- /dev/null +++ b/tests/diffraction/test_cbed.py @@ -0,0 +1,134 @@ +"""CBED simulation against the single-beam Bloch solver and physics checks.""" + +import numpy as np +import torch +from ase.build import bulk + +from quantem.diffraction import bloch +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.rotations import quat_from_zone_axis + + +def _si(absorptive: bool) -> Crystal: + si = Crystal.from_ase( + bulk("Si", "diamond", a=5.431, cubic=True), name="Si", verbose=False + ) + si.calculate_structure_factors(k_max=3.0) + if absorptive: + si.calculate_dynamical_structure_factors(energy_ev=200e3, k_max=3.0) + return si + + +def _zone_110() -> torch.Tensor: + return quat_from_zone_axis(torch.tensor([[1.0, 1.0, 0.0]]) / np.sqrt(2))[0] + + +def test_zero_tilt_matches_dynamical_pattern(): + si = _si(absorptive=True) + q = _zone_110() + t = torch.tensor([800.0]) + tilts = torch.zeros((1, 2), dtype=torch.float64) + I, g_xy, hkl = bloch._cbed_amplitudes( + si, q, tilts, t, 200e3, sg_max=0.08, k_max=1.3 + ) + ref = bloch.dynamical_pattern( + si, q, t, energy_ev=200e3, sg_max=0.08, k_max=1.3 + ) + # same beams (000 first in CBED) and identical intensities + assert hkl.shape[0] == ref["hkl"].shape[0] + 1 + assert torch.allclose( + I[0, 0, 1:], ref["intensity"][0], rtol=1e-10, atol=1e-12 + ) + + +def test_unitarity_without_absorption(): + si = _si(absorptive=False) + q = _zone_110() + tilts = bloch.tilt_grid(2.0, 200e3, n_rings=2) + I, _, _ = bloch._cbed_amplitudes( + si, q, tilts, torch.tensor([500.0, 1500.0]), 200e3, sg_max=0.08, k_max=1.3 + ) + # Hermitian structure matrix: evolution is unitary in the beam space + total = I.sum(dim=-1) + assert torch.allclose(total, torch.ones_like(total), atol=1e-8) + + +def test_lacbed_centrosymmetric_disk(): + si = _si(absorptive=True) + q = _zone_110() + res = bloch.calculate_lacbed( + si, q, 800.0, hkl=(0, 0, 0), + energy_ev=200e3, semiconv_mrad=6.0, n_pixels=24, sg_max=0.08, k_max=1.3, + ) + disk = res["disk"] + # Si is centrosymmetric: the bright field rocking surface at a zone axis + # is inversion symmetric, I(t) = I(-t). Small residuals come from beam + # truncation at the s_g cutoff (|s_g| differs slightly for +g and -g), + # so the tolerance is physical rather than numerical. + flipped = disk[::-1, ::-1] + m = np.isfinite(disk) & np.isfinite(flipped) + assert m.sum() > 100 + assert np.allclose(disk[m], flipped[m], rtol=2e-3, atol=1e-5) + + +def test_cbed_library_common_grid(): + si = _si(absorptive=True) + q0 = _zone_110() + quats = torch.stack([q0, q0]) + lib = bloch.calculate_cbed_library( + si, quats, thickness_A=600.0, + energy_ev=200e3, semiconv_mrad=3.0, k_max=1.0, progress_bar=False, + ) + assert lib["patterns"].shape[0] == 2 + assert lib["patterns"].shape[1] == lib["patterns"].shape[2] + assert np.allclose(lib["patterns"][0], lib["patterns"][1]) + assert lib["patterns"][0].max() > 0 + + +def test_kossel_bright_field_matches_lacbed(): + si = _si(absorptive=True) + q = _zone_110() + kw = dict(energy_ev=200e3, semiconv_mrad=15.0, sg_max=0.06, k_max=1.2) + kos = bloch.calculate_kossel( + si, q, 900.0, n_pixels=32, progress_bar=False, **kw + ) + lac = bloch.calculate_lacbed(si, q, 900.0, hkl=(0, 0, 0), n_pixels=32, **kw) + a, b = kos["bright_field"], lac["disk"] + m = np.isfinite(a) & np.isfinite(b) + assert m.sum() > 300 + assert np.allclose(a[m], b[m], rtol=1e-10, atol=1e-12) + + # the summed pattern includes the direct beam plus every diffracted + # cone, so inside the aperture it can only exceed the bright field + pat = kos["pattern"] + assert (pat[m] >= a[m] - 1e-9).mean() > 0.99 + assert np.all(pat[~np.isfinite(a)] == 0) + + +def test_master_pattern_lookup(): + from scipy.ndimage import gaussian_filter + + si = _si(absorptive=True) + q = _zone_110() + # the master stores the pattern at its own angular resolution + # (angle_step_mrad); compare against the direct calculation blurred to + # the same resolution + master = bloch.calculate_kossel_master( + si, [800.0], energy_ev=200e3, + angle_step_mrad=2.0, sg_max=0.06, k_max=1.0, progress_bar=False, + ) + fast = bloch.kossel_from_master(master, q, semiconv_mrad=25.0, n_pixels=48) + direct = bloch.calculate_kossel( + si, q, 800.0, energy_ev=200e3, semiconv_mrad=25.0, + n_pixels=48, sg_max=0.06, k_max=1.0, progress_bar=False, + ) + a, b = fast["bright_field"], direct["bright_field"] + m = np.isfinite(a) & np.isfinite(b) + assert m.sum() > 1000 + # blur both to the master's angular resolution before comparing + px_mrad = 2 * 25.0 / 48 + sigma = 2.0 / px_mrad / 2.355 + af = gaussian_filter(np.nan_to_num(a), sigma) + bf = gaussian_filter(np.nan_to_num(b), sigma) + cc = np.corrcoef(af[m], bf[m])[0, 1] + assert cc > 0.9 From c3deefe0b08108d3e06cba4d15f05528f34d6777 Mon Sep 17 00:00:00 2001 From: cophus Date: Tue, 8 Sep 2026 16:59:21 +0200 Subject: [PATCH 04/36] Adding dynamical calculations, refinement --- src/quantem/diffraction/bloch.py | 1947 +++++++++++++++-- src/quantem/diffraction/calibration.py | 81 +- src/quantem/diffraction/crystal.py | 211 +- src/quantem/diffraction/defaults.py | 45 + src/quantem/diffraction/orientation.py | 403 ++-- .../diffraction/orientation_visualization.py | 143 +- src/quantem/diffraction/phase.py | 109 +- src/quantem/diffraction/rotations.py | 160 +- tests/diffraction/test_bloch.py | 504 ++++- tests/diffraction/test_cbed.py | 134 -- tests/diffraction/test_crystal.py | 120 +- tests/diffraction/test_orientation.py | 146 +- tests/diffraction/test_rotations.py | 54 +- 13 files changed, 3404 insertions(+), 653 deletions(-) create mode 100644 src/quantem/diffraction/defaults.py delete mode 100644 tests/diffraction/test_cbed.py diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py index 2391af513..b71df3b8c 100644 --- a/src/quantem/diffraction/bloch.py +++ b/src/quantem/diffraction/bloch.py @@ -17,12 +17,22 @@ from __future__ import annotations +import warnings + import numpy as np import torch from tqdm import tqdm from quantem.core.utils.utils import electron_wavelength_angstrom from quantem.diffraction.crystal import Crystal +from quantem.diffraction.defaults import ( + MIN_NUMBER_PEAKS, + MIN_SIM_INTENSITY_REL, + PAIR_DISTANCE, + POWER_INTENSITY, + SG_MAX, + resolve, +) from quantem.diffraction.rotations import qrotate, sample_zone_axes @@ -89,7 +99,7 @@ def dynamical_pattern( orientation: torch.Tensor, thicknesses_A: torch.Tensor | np.ndarray | float, energy_ev: float = 300e3, - sg_max: float = 0.1, + sg_max: float = SG_MAX, k_max: float | None = None, ) -> dict[str, torch.Tensor]: """Bloch-wave diffraction intensities for one orientation, all thicknesses. @@ -133,7 +143,6 @@ def dynamical_pattern( hkl_sel = crystal.hkl[sel] g_sel = g_lab[sel] s_sel = s_g[sel] - n = int(sel.sum()) # beams list includes the (000) beam at index 0 hkl_beams = torch.cat([torch.zeros((1, 3), dtype=torch.long), hkl_sel]) @@ -172,15 +181,19 @@ def dynamical_pattern( def refine_thickness( phase_map, thicknesses_A: np.ndarray | None = None, - pair_distance: float = 0.05, - power_intensity: float = 0.25, - sg_max: float = 0.1, + pair_distance: float | None = None, + power_intensity: float | None = None, + sg_max: float = SG_MAX, k_max: float | None = None, - min_number_peaks: int = 3, + min_number_peaks: int | None = None, progress_bar: bool = True, ): """Second-pass thickness and phase refinement with dynamical intensities. + Parameters left as None inherit the phase fit's values (see + PhaseMap.fit); the resolved values are recorded in + phase_map.metadata['thickness']. + For every probe position, the winning candidates of a fitted PhaseMap are re-simulated with Bloch waves over a thickness grid. The peak pairing is fixed (positions are kinematic); the intensity cost is evaluated for all @@ -206,6 +219,21 @@ def refine_thickness( t_grid = torch.as_tensor(thicknesses_A, dtype=torch.float64) oms = phase_map.orientation_maps + fit_md = phase_map.metadata.get("fit") if hasattr(phase_map, "metadata") else None + pair_distance = resolve(pair_distance, "pair_distance", fit_md, default=PAIR_DISTANCE) + power_intensity = resolve(power_intensity, "power_intensity", fit_md, default=POWER_INTENSITY) + min_number_peaks = resolve( + min_number_peaks, "min_number_peaks", fit_md, default=MIN_NUMBER_PEAKS + ) + if hasattr(phase_map, "metadata"): + phase_map.metadata["thickness"] = dict( + thicknesses_A=np.asarray(thicknesses_A, dtype=float).tolist(), + pair_distance=float(pair_distance), + power_intensity=float(power_intensity), + sg_max=float(sg_max), + k_max=k_max, + min_number_peaks=int(min_number_peaks), + ) cands = phase_map.candidates peaks = oms[0].peaks R, C = peaks.shape[0], peaks.shape[1] @@ -235,9 +263,10 @@ def refine_thickness( if om.corr[rx, ry, m] <= 0: continue # only refine candidates that won weight in the first pass - if phase_map.phase_weights is not None and float( - phase_map.phase_weights[rx, ry, f] - ) <= 0: + if ( + phase_map.phase_weights is not None + and float(phase_map.phase_weights[rx, ry, f]) <= 0 + ): continue sim = dynamical_pattern( om.crystal, @@ -262,8 +291,7 @@ def refine_thickness( w = w.clamp_min(0) c_paired = ( - (b - w[:, None] * a).abs() * (1 - frac)[None, :] - + w[:, None] * a * frac[None, :] + (b - w[:, None] * a).abs() * (1 - frac)[None, :] + w[:, None] * a * frac[None, :] ).sum(dim=1) c_unpaired_sim = 0.5 * w * si[:, ~pair].sum(dim=1) matched = torch.zeros(im.shape[0], dtype=torch.bool) @@ -304,6 +332,8 @@ def _cbed_amplitudes( k_max: float | None, tilt_batch: int = 64, progress_bar: bool = False, + fast_absorption: bool = False, + deform: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Bloch intensities of every beam at every incident tilt. @@ -315,6 +345,10 @@ def _cbed_amplitudes( ---------- tilts : torch.Tensor (M, 2) in-plane incident wavevector components (1/Angstroms). + deform : torch.Tensor | None + (3, 3) deformation applied to the lab-frame reciprocal lattice + (g' = deform @ g), for a strained cell; the structure factors are + those of the ideal cell. Returns ------- @@ -329,25 +363,21 @@ def _cbed_amplitudes( lam = electron_wavelength_angstrom(energy_ev) k0 = 1.0 / lam gamma_rel = relativistic_gamma(energy_ev) - t_thick = torch.atleast_1d( - torch.as_tensor(thicknesses_A, dtype=torch.float64) - ) + t_thick = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) # beam selection: near the Ewald sphere for ANY tilt in the aperture -- # a tilt t shifts s_g by at most |t| * |g| / k0 to leading order g_lab = qrotate(orientation, crystal.g_vec) + if deform is not None: + g_lab = g_lab @ deform.to(torch.float64).T gz, g2 = g_lab[:, 2], (g_lab**2).sum(dim=1) s0 = (2 * gz - lam * g2) / (2 - 2 * lam * gz) alpha_max = float(torch.linalg.norm(tilts, dim=1).max()) / k0 sel = torch.abs(s0) < sg_max + alpha_max * crystal.g_len if k_max is not None: sel &= crystal.g_len <= k_max - hkl_beams = torch.cat( - [torch.zeros((1, 3), dtype=torch.long), crystal.hkl[sel]] - ) - g_beams = torch.cat( - [torch.zeros((1, 3), dtype=torch.float64), g_lab[sel]] - ) + hkl_beams = torch.cat([torch.zeros((1, 3), dtype=torch.long), crystal.hkl[sel]]) + g_beams = torch.cat([torch.zeros((1, 3), dtype=torch.float64), g_lab[sel]]) nb = hkl_beams.shape[0] U, u0_imag, absorptive = _coupling_matrix(crystal, hkl_beams, gamma_rel) @@ -372,7 +402,9 @@ def _cbed_amplitudes( den = 2 * (kz[:, None] - gzb[None, :]) s_t = num / den # (B, nb) - out[m0:m1] = _bloch_solve(U, u0_imag, absorptive, s_t, k0, t_thick) + out[m0:m1] = _bloch_solve( + U, u0_imag, absorptive, s_t, k0, t_thick, fast_absorption=fast_absorption + ) return out, g_beams[:, :2], hkl_beams @@ -383,19 +415,19 @@ def _bloch_solve( s_t: torch.Tensor, k0: float, t_thick: torch.Tensor, - perturbative: bool = False, + fast_absorption: bool = False, ) -> torch.Tensor: """Batched Bloch solve: intensities (B, T, nb) for excitation errors s_t (B, nb) with a shared coupling matrix U (nb, nb). - With perturbative=True the Hermitian part is diagonalized (eigh, much + With fast_absorption=True the Hermitian part is diagonalized (eigh, much faster and better batched than the general complex eig) and the weak absorption enters first order: gamma_imag = diag(C^dagger U'' C)/(2 k0). Standard for master-pattern computations; the absorptive parts of U are a few percent of the elastic parts, so the first-order error is small. """ nb = U.shape[0] - if absorptive and perturbative: + if absorptive and fast_absorption: H = 0.5 * (U + U.conj().T) W = (U - H) / 1j # Hermitian absorptive part (off-diagonal) A = H[None].expand(s_t.shape[0], nb, nb).clone() @@ -522,14 +554,14 @@ def calculate_cbed( T = t_thick.shape[0] pattern = np.zeros((T, H, H)) - I = inten.numpy() # (M, T, nb) + inten_np = inten.numpy() # (M, T, nb) for dx in (0, 1): for dy in (0, 1): w = (wx if dx else 1 - wx) * (wy if dy else 1 - wy) jx = np.clip(ix0 + dx, 0, H - 1) jy = np.clip(iy0 + dy, 0, H - 1) for ti in range(T): - np.add.at(pattern[ti], (jx, jy), w * I[:, ti, :]) + np.add.at(pattern[ti], (jx, jy), w * inten_np[:, ti, :]) pattern /= tilts.shape[0] return { @@ -663,6 +695,7 @@ def calculate_kossel( sg_max: float = 0.05, k_max: float | None = None, tilt_batch: int = 64, + fast_absorption: bool = False, progress_bar: bool = True, ) -> dict: """Wide-angle convergent beam (Kossel) pattern with Bloch waves. @@ -720,17 +753,25 @@ def calculate_kossel( T = t_thick.shape[0] inten, g_xy, hkl_beams = _cbed_amplitudes( - crystal, orientation, tilts, t_thick, energy_ev, sg_max, k_max, - tilt_batch, progress_bar=progress_bar, + crystal, + orientation, + tilts, + t_thick, + energy_ev, + sg_max, + k_max, + tilt_batch, + progress_bar=progress_bar, + fast_absorption=fast_absorption, ) - I = inten.numpy() # (M, T, nb) + inten_np = inten.numpy() # (M, T, nb) m = inside.numpy() # bright field: beam 0 on the tilt grid directly bright = np.full((T, n_pixels, n_pixels), np.nan) for ti in range(T): plane = np.full((n_pixels, n_pixels), np.nan) - plane[m] = I[:, ti, 0] + plane[m] = inten_np[:, ti, 0] bright[ti] = plane # full pattern: every diffracted cone shifted by its g, bilinear deposit @@ -752,9 +793,7 @@ def calculate_kossel( ok = (jx >= 0) & (jx < n_pixels) & (jy >= 0) & (jy < n_pixels) w = (wx if dx else 1 - wx) * (wy if dy else 1 - wy) for ti in range(T): - np.add.at( - pattern[ti], (jx[ok], jy[ok]), (w * I[:, ti, b])[ok] - ) + np.add.at(pattern[ti], (jx[ok], jy[ok]), (w * inten_np[:, ti, b])[ok]) pattern[:, ~m] = 0.0 return { @@ -767,7 +806,78 @@ def calculate_kossel( } -def calculate_kossel_master( +def _lambert_raster( + crystal: Crystal, dirs: torch.Tensor, values: torch.Tensor, step: float +) -> np.ndarray: + """Expand wedge samples by the crystal symmetry (plus inversion) and + splat them bilinearly onto a Lambert equal-area grid of the upper + hemisphere. values is (N, T); returns (T, n, n) with NaN where unhit + (raster holes inside the disk are filled from their neighbors).""" + from quantem.diffraction.rotations import quat_to_matrix + + T = values.shape[1] + Rs = quat_to_matrix(crystal.sym_quats_matching) + d_all = torch.einsum("sij,nj->sni", Rs, dirs).reshape(-1, 3) + I_all = values[None, :, :].expand(Rs.shape[0], -1, -1).reshape(-1, T) + d_all = torch.cat([d_all, -d_all]) + I_all = torch.cat([I_all, I_all]) + up = d_all[:, 2] >= 0 + d_all, I_all = d_all[up], I_all[up] + + # grid always spans the full hemisphere: symmetry expansion moves wedge + # samples to any polar angle, and clipping them onto a smaller rim + # corrupts the equatorial region + rho_max = float(np.sqrt(2.0)) + half = int(np.ceil(rho_max / step)) + n = 2 * half + 1 + rho = torch.sqrt((2 * (1 - d_all[:, 2])).clamp_min(0)) + dxy = torch.linalg.norm(d_all[:, :2], dim=1).clamp_min(1e-12) + px_x = (d_all[:, 0] / dxy * rho / step + half).numpy() + px_y = (d_all[:, 1] / dxy * rho / step + half).numpy() + + acc = np.zeros((T, n, n)) + wgt = np.zeros((n, n)) + ix0 = np.floor(px_x).astype(int) + iy0 = np.floor(px_y).astype(int) + wx = px_x - ix0 + wy = px_y - iy0 + I_np = I_all.numpy() + for dx in (0, 1): + for dy in (0, 1): + jx = np.clip(ix0 + dx, 0, n - 1) + jy = np.clip(iy0 + dy, 0, n - 1) + w = (wx if dx else 1 - wx) * (wy if dy else 1 - wy) + np.add.at(wgt, (jx, jy), w) + for ti in range(T): + np.add.at(acc[ti], (jx, jy), w * I_np[:, ti]) + lambert = np.where(wgt[None] > 1e-6, acc / np.maximum(wgt[None], 1e-6), np.nan) + + # fill raster holes (unhit pixels between splatted samples) from their + # neighbors so bilinear lookups never touch NaN inside the disk + yy, xx = np.mgrid[0:n, 0:n] + in_disk = ((xx - half) ** 2 + (yy - half) ** 2) <= (rho_max / step) ** 2 + for ti in range(T): + L = lambert[ti] + for _ in range(4): + holes = np.isnan(L) & in_disk + if not holes.any(): + break + Lp = np.pad(L, 1, constant_values=np.nan) + stack = np.stack( + [ + Lp[1 + dy : n + 1 + dy, 1 + dx : n + 1 + dx] + for dy in (-1, 0, 1) + for dx in (-1, 0, 1) + ] + ) + with np.errstate(all="ignore"): + fill = np.nanmean(stack, axis=0) + L[holes] = fill[holes] + lambert[ti] = L + return lambert + + +def calculate_kossel_reference( crystal: Crystal, thicknesses_A, energy_ev: float = 300e3, @@ -776,19 +886,19 @@ def calculate_kossel_master( k_max: float | None = None, theta_max_deg: float = 90.0, chunk: int = 256, - perturbative: bool = True, + fast_absorption: bool = True, progress_bar: bool = True, ) -> dict: - """Kossel master pattern: the dynamical bright field over all directions. + """Kossel reference pattern: the dynamical bright field over all directions. The bright field intensity depends only on the incident beam direction in the CRYSTAL frame (each incident plane wave is independent), so one Bloch computation over the symmetry-reduced direction wedge gives the - pattern for every specimen orientation at once -- the EMsoft master - pattern strategy. Patterns for arbitrary orientations, convergence - angles, and all precomputed thicknesses are then interpolation lookups - via kossel_from_master(), microseconds instead of a fresh dynamical - calculation. + pattern for every specimen orientation at once (called a master pattern + in parts of the EBSD literature). Patterns for arbitrary orientations, + convergence angles, and all precomputed thicknesses are then + interpolation lookups via kossel_from_reference(), microseconds instead + of a fresh dynamical calculation. The wedge samples are expanded by the crystal's proper rotations plus inversion and rasterized onto a Lambert azimuthal equal-area grid of @@ -824,9 +934,14 @@ def calculate_kossel_master( t_thick = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) T = t_thick.shape[0] + msg = crystal.matching_symmetry_warning() + if msg is not None: + warnings.warn(msg, stacklevel=2) wedge = crystal.zone_axis_wedge() step_deg = np.rad2deg(angle_step_mrad * 1e-3) if wedge is None: + from quantem.diffraction.orientation import fibonacci_hemisphere + n_dirs = int(np.ceil(2 * np.pi / np.deg2rad(step_deg) ** 2)) dirs = fibonacci_hemisphere(n_dirs) else: @@ -836,7 +951,7 @@ def calculate_kossel_master( N = dirs.shape[0] g = crystal.g_vec # crystal frame, orientation is identity - gz, g2 = g[:, 2], (g**2).sum(dim=1) + g2 = (g**2).sum(dim=1) g_len = crystal.g_len out = torch.zeros((N, T), dtype=torch.float64) @@ -862,82 +977,25 @@ def calculate_kossel_master( sel = torch.abs(s_c) < sg_max + (radius + 1e-4) * g_len if k_max is not None: sel &= g_len <= k_max - hkl_beams = torch.cat( - [torch.zeros((1, 3), dtype=torch.long), crystal.hkl[sel]] - ) + hkl_beams = torch.cat([torch.zeros((1, 3), dtype=torch.long), crystal.hkl[sel]]) g_b = torch.cat([torch.zeros((1, 3), dtype=torch.float64), g[sel]]) U, u0_imag, absorptive = _coupling_matrix(crystal, hkl_beams, gamma_rel) g2b = (g_b**2).sum(dim=1) u = torch.einsum("bk,nk->bn", d, g_b) # g . d_hat per sample s_t = (2 * k0 * u - g2b[None, :]) / (2 * (k0 - u)) - I = _bloch_solve( - U, u0_imag, absorptive, s_t, k0, t_thick, perturbative=perturbative + inten_b = _bloch_solve( + U, u0_imag, absorptive, s_t, k0, t_thick, fast_absorption=fast_absorption ) - out[c0:c1] = I[:, :, 0] + out[c0:c1] = inten_b[:, :, 0] # symmetry expansion and Lambert raster: with normal-tracking geometry # the intensity is a function of the crystal-frame beam direction only, # so proper rotations apply directly; the reversed beam (with reversed # normal) gives the same bright field by reciprocity. - from quantem.diffraction.rotations import quat_to_matrix - - Rs = quat_to_matrix(crystal.sym_quats_matching) - d_all = torch.einsum("sij,nj->sni", Rs, dirs).reshape(-1, 3) - I_all = out[None, :, :].expand(Rs.shape[0], -1, -1).reshape(-1, T) - d_all = torch.cat([d_all, -d_all]) - I_all = torch.cat([I_all, I_all]) - up = d_all[:, 2] >= 0 - d_all, I_all = d_all[up], I_all[up] - - # grid always spans the full hemisphere: symmetry expansion moves wedge - # samples to any polar angle, and clipping them onto a smaller rim - # corrupts the equatorial region - rho_max = float(np.sqrt(2.0)) step = angle_step_mrad * 1e-3 - half = int(np.ceil(rho_max / step)) - n = 2 * half + 1 - rho = torch.sqrt((2 * (1 - d_all[:, 2])).clamp_min(0)) - dxy = torch.linalg.norm(d_all[:, :2], dim=1).clamp_min(1e-12) - px_x = (d_all[:, 0] / dxy * rho / step + half).numpy() - px_y = (d_all[:, 1] / dxy * rho / step + half).numpy() - - acc = np.zeros((T, n, n)) - wgt = np.zeros((n, n)) - ix0 = np.floor(px_x).astype(int) - iy0 = np.floor(px_y).astype(int) - wx = px_x - ix0 - wy = px_y - iy0 - I_np = I_all.numpy() - for dx in (0, 1): - for dy in (0, 1): - jx = np.clip(ix0 + dx, 0, n - 1) - jy = np.clip(iy0 + dy, 0, n - 1) - w = (wx if dx else 1 - wx) * (wy if dy else 1 - wy) - np.add.at(wgt, (jx, jy), w) - for ti in range(T): - np.add.at(acc[ti], (jx, jy), w * I_np[:, ti]) - lambert = np.where(wgt[None] > 1e-6, acc / np.maximum(wgt[None], 1e-6), np.nan) - - # fill raster holes (unhit pixels between splatted samples) from their - # neighbors so bilinear lookups never touch NaN inside the disk - yy, xx = np.mgrid[0:n, 0:n] - in_disk = ((xx - half) ** 2 + (yy - half) ** 2) <= (rho_max / step) ** 2 - for ti in range(lambert.shape[0]): - L = lambert[ti] - for _ in range(4): - holes = np.isnan(L) & in_disk - if not holes.any(): - break - Lp = np.pad(L, 1, constant_values=np.nan) - stack = np.stack( - [Lp[1 + dy : n + 1 + dy, 1 + dx : n + 1 + dx] - for dy in (-1, 0, 1) for dx in (-1, 0, 1)] - ) - with np.errstate(all="ignore"): - fill = np.nanmean(stack, axis=0) - L[holes] = fill[holes] - lambert[ti] = L + lambert = _lambert_raster(crystal, dirs, out, step) + rho_max = float(np.sqrt(2.0)) return { "lambert": lambert, @@ -945,18 +1003,46 @@ def calculate_kossel_master( "step": step, "thicknesses": t_thick.numpy(), "energy_ev": float(energy_ev), + "k_max": k_max, "directions": dirs.numpy(), "intensity": out.numpy(), } -def kossel_from_master( +def _lambert_lookup(lambert: np.ndarray, step: float, d_c: torch.Tensor) -> np.ndarray: + """Bilinear lookup of a (T, n, n) Lambert grid at crystal-frame + directions d_c (..., 3); returns (T, ...). Directions are folded to + the upper hemisphere (reciprocity).""" + d_c = torch.where(d_c[..., 2:3] < 0, -d_c, d_c) + half = (lambert.shape[-1] - 1) // 2 + rho = torch.sqrt((2 * (1 - d_c[..., 2])).clamp_min(0)) + dxy = torch.linalg.norm(d_c[..., :2], dim=-1).clamp_min(1e-12) + fx = (d_c[..., 0] / dxy * rho / step + half).numpy() + fy = (d_c[..., 1] / dxy * rho / step + half).numpy() + n_l = lambert.shape[-1] + ix0 = np.clip(np.floor(fx).astype(int), 0, n_l - 2) + iy0 = np.clip(np.floor(fy).astype(int), 0, n_l - 2) + wx = np.clip(fx - ix0, 0, 1) + wy = np.clip(fy - iy0, 0, 1) + out = np.zeros((lambert.shape[0],) + fx.shape) + for ti in range(lambert.shape[0]): + L = lambert[ti] + out[ti] = ( + L[ix0, iy0] * (1 - wx) * (1 - wy) + + L[ix0 + 1, iy0] * wx * (1 - wy) + + L[ix0, iy0 + 1] * (1 - wx) * wy + + L[ix0 + 1, iy0 + 1] * wx * wy + ) + return out + + +def kossel_from_reference( master: dict, orientation: torch.Tensor, semiconv_mrad: float = 40.0, n_pixels: int = 192, ) -> dict: - """Extract a bright field Kossel pattern from a master pattern. + """Extract a bright field Kossel pattern from a reference pattern. Interpolation only -- microseconds per pattern per thickness. The detector tilt grid is mapped into the crystal frame by the orientation @@ -968,51 +1054,1626 @@ def kossel_from_master( 'thicknesses'. """ lam = electron_wavelength_angstrom(master["energy_ev"]) + d_c, inside, _, _ = _detector_directions( + lam, orientation, semiconv_mrad, False, n_pixels, 1, 1 + ) + bf = _lambert_lookup(master["lambert"], master["step"], d_c) + bf[:, ~inside.numpy()] = np.nan + T = bf.shape[0] + + return { + "bright_field": bf[0] if T == 1 else bf, + "mrad_per_pixel": 2 * semiconv_mrad / n_pixels, + "thicknesses": master["thicknesses"], + } + + +def plot_kossel_reference( + master: dict, + crystal: Crystal, + thickness_index: int = 0, + max_index: int = 2, + theta_max_label_deg: float = 75.0, + min_crossing: float = 1.0, + min_crossing_rim: float = 0.3, + sigma_mrad: float = 10.0, + lines: dict | None = None, + theta_circles=(), + label_color=(0.9, 0.0, 0.0), + label_fontsize: float = 12, + stroke_color=(1.0, 1.0, 1.0, 0.7), + stroke_width: float = 6.0, + upsample: int = 2, + cmap: str = "gray", + axsize: tuple[float, float] = (9.0, 9.0), + filename: str | None = None, + figax=None, +): + """The reference pattern with polar angle circles and low index zone labels. + + Every symmetry copy of the zone axes with direction indices up to + `max_index` is labeled with its own signed indices (4-index for + hexagonal and trigonal crystals). The circles and labels are vector + graphics; saving to a PDF via `filename` keeps them sharp at any zoom, + with the pattern embedded as a smoothly interpolated image. + + Parameters + ---------- + master : dict + From calculate_kossel_reference(). + crystal : Crystal + The crystal the reference was computed for. + max_index : int, default=2 + Largest direction index to label. + theta_max_label_deg : float, default=75.0 + Zones between this polar angle and the equator are left unlabeled + (rim clutter); the equatorial zones themselves are labeled just + outside the disk edge. + min_crossing : float, default=1.0 + Only label a zone whose crossing strength reaches this value. Each + Kossel band is a pair of lines at +-theta_B about the zone plane, + so the rows in a zone (zone law g . [uvw] = 0) form a rosette + around the zone axis rather than lines through it. The crossing + strength sums, over the rows in the zone, the line depth weighted + by exp(-theta_B^2 / 2 sigma^2), and subtracts the strongest row: + a zone on a single band scores zero (such as <221> or <223> in + diamond, which contain only the 220 row), and a rosette of several + strong rows with small Bragg angles scores high. In silicon at + 200 kV the default keeps <001>, <011>, <111>, <112> and <013>, + and drops <113> (0.6, its 422 and 620 rows sit 11-15 mrad out), + <123> (0.65) and <233> (0.9). + min_crossing_rim : float, default=0.3 + The same threshold for the equatorial zones labeled outside the + disk, where there is room for weaker crossings: keeps <120> and + <130> in silicon and drops <230>, which is a single 400 band. + sigma_mrad : float, default=10.0 + Rosette scale of the crossing strength: rows with Bragg angles + beyond this contribute little, since their band edges are too far + from the zone axis to read as a crossing. + lines : dict | None + Line set from kossel_lines() for the crossing strength; computed + from the crystal and the reference's thickness and k_max if + omitted. + theta_circles : sequence, default=() + Polar angles (degrees) at which to draw dashed circles; off by + default. + label_color, label_fontsize, stroke_color, stroke_width : + Zone label styling: text color, size, and the translucent outline + drawn behind each label. + upsample : int, default=2 + Bilinear upsampling factor of the displayed pattern. + filename : str | None + If given, save the figure (PDF recommended). + """ + import matplotlib.pyplot as plt + from matplotlib import patheffects + + from quantem.diffraction.crystal import miller_to_miller_bravais + from quantem.diffraction.rotations import quat_to_matrix + + L = master["lambert"][thickness_index] + step = master["step"] + half = (L.shape[-1] - 1) // 2 + if upsample > 1: + from scipy.ndimage import zoom + + L = zoom(np.nan_to_num(L, nan=np.nanmax(L)), upsample, order=1) + scale = upsample if upsample > 1 else 1 + + if figax is None: + fig, ax = plt.subplots(figsize=axsize) + else: + fig, ax = figax + ax.imshow(L, cmap=cmap, interpolation="bilinear") + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_frame_on(False) + + def to_px(v): + return (v / step + half) * scale + (scale - 1) / 2 + + phi = np.linspace(0, 2 * np.pi, 721) + for theta_deg in theta_circles: + r = 2 * np.sin(np.deg2rad(theta_deg) / 2) / step * scale + c = to_px(0.0) + ax.plot(c + r * np.cos(phi), c + r * np.sin(phi), ls="--", color="0.45", lw=0.7) + ax.text( + c, + c - r, + " %d°" % theta_deg, + color="0.35", + fontsize=9, + va="bottom", + ) + + # unique low index zone directions, expanded over the crystal symmetry + hexagonal = crystal.hexagonal_matching + A_T = crystal.lat_real.numpy().T # d_cartesian = A_T @ [u, v, w] + A_T_inv = np.linalg.inv(A_T) + Rs = quat_to_matrix(crystal.sym_quats_matching).numpy() + + # crossing strength from the line set: per row, the deepest line + # weighted by its Bragg angle + if lines is None: + lines = kossel_lines( + crystal, + master["thicknesses"][thickness_index], + energy_ev=master["energy_ev"], + k_max=master.get("k_max", 1.2), + ) + ti = 0 + else: + ti = thickness_index + g_hat = lines["g_hat"].numpy() + n_rows = g_hat.shape[0] + row_of = lines["line_row"].numpy() + line_w = lines["line_depth"].numpy()[:, ti] * np.exp( + -0.5 * (lines["line_u"].numpy() / (sigma_mrad * 1e-3)) ** 2 + ) + row_weight = np.zeros(n_rows) + np.maximum.at(row_weight, row_of, line_w) + + def crossing_strength(dc): + w = row_weight[np.abs(g_hat @ dc) < 1e-4] + return float(w.sum() - w.max()) if w.size else 0.0 + + # one label per distinct crystallographic direction, keyed by its + # canonical index tuple so a direction reached by several symmetry + # operations (common at the equatorial rim) is drawn only once + # integer index-space representation of each symmetry rotation, so the + # zone index of a symmetry copy is computed by exact integer arithmetic + # rather than by rounding a projected direction (which can alias a + # high-index direction onto a low-index label) + M = [np.rint(A_T_inv @ R @ A_T).astype(int) for R in Rs] + + placed: dict[tuple, tuple] = {} + rng = range(-max_index, max_index + 1) + for u in rng: + for v in rng: + for w in rng: + uvw = np.array([u, v, w]) + if not uvw.any() or np.gcd.reduce(np.abs(uvw)) != 1: + continue + d = A_T @ uvw + d = d / np.linalg.norm(d) + for R, Mi in zip(Rs, M): + for sgn in (1, -1): + dc = sgn * (R @ d) + idx = sgn * (Mi @ uvw) + # fold to the upper hemisphere (the reference is + # stored there); flip the index to match + if dc[2] < 0: + dc = -dc + idx = -idx + rim = dc[2] < np.sin(np.deg2rad(1.0)) + if not rim and dc[2] < np.cos(np.deg2rad(theta_max_label_deg)): + continue + if crossing_strength(dc) < (min_crossing_rim if rim else min_crossing): + continue + key = tuple(int(k) for k in idx) + if key in placed: + continue + ks = np.atleast_2d(miller_to_miller_bravais(idx))[0] if hexagonal else idx + txt = ( + "$[" + + "".join((r"\bar{%d\!}" % abs(k)) if k < 0 else str(k) for k in ks) + + "]$" + ) + rho = np.sqrt(max(2 * (1 - dc[2]), 0)) + if rim: + rho = np.sqrt(2.0) * 1.07 # just outside the disk + dxy = max(np.hypot(dc[0], dc[1]), 1e-12) + px = to_px(dc[0] / dxy * rho) + py = to_px(dc[1] / dxy * rho) + placed[key] = (px, py, txt) + + for px, py, txt in placed.values(): + t = ax.text( + py, + px, + txt, + color=label_color, + fontsize=label_fontsize, + ha="center", + va="center", + ) + t.set_path_effects( + [patheffects.withStroke(linewidth=stroke_width, foreground=stroke_color)] + ) + n_px = L.shape[-1] + ax.set_xlim(-0.06 * n_px, 1.06 * n_px) + ax.set_ylim(1.06 * n_px, -0.06 * n_px) + if filename is not None: + fig.savefig(filename, bbox_inches="tight", dpi=300) + return fig, ax + + +def kossel_polar_from_reference( + master: dict, + orientation: torch.Tensor, + semiconv_mrad: float = 40.0, + n_radial: int = 64, + n_azimuthal: int = 180, +) -> dict: + """A bright field Kossel pattern sampled directly on a polar grid. + + Dictionary matching correlates over the in-plane rotation, which is a + cyclic shift of the azimuthal axis in polar coordinates: sampling the + master directly at the polar detector positions avoids the intermediate + Cartesian raster and its interpolation. + + Returns + ------- + dict with 'polar' ((T, n_azimuthal, n_radial), squeezed; rows are + azimuth, columns radius, matching the quantem polar transform + convention), 'radii_mrad', 'azimuth_rad', 'thicknesses'. + """ + lam = electron_wavelength_angstrom(master["energy_ev"]) + d_c, _, axes, _ = _detector_directions( + lam, orientation, semiconv_mrad, True, 1, n_radial, n_azimuthal + ) + out = _lambert_lookup(master["lambert"], master["step"], d_c) + T = out.shape[0] + return { + "polar": out[0] if T == 1 else out, + "radii_mrad": axes["radii_mrad"], + "azimuth_rad": axes["azimuth_rad"], + "thicknesses": master["thicknesses"], + } + + +def kossel_lines( + crystal: Crystal, + thicknesses_A, + energy_ev: float = 300e3, + k_max: float = 1.2, + u_step_mrad: float = 0.05, + u_tail_mrad: float = 150.0, + min_depth: float = 0.005, + fast_absorption: bool = False, +) -> dict: + """Vector representation of the Kossel lines: one profile per systematic row. + + The bright field depends on the beam direction d (a unit vector, the + anti-propagation direction in the crystal frame) only through the + projections u = d . g_hat onto the row normals. For each systematic row + {n g} the profile is a Bloch calculation over the row beams alone versus + the signed projection u, which places the deficiency line of reflection + +n g at u = +n lambda |g| / 2 and that of -n g at u = -n lambda |g| / 2: + the two lines of a Kossel band, 2 theta_B apart, and their higher + orders, all with their dynamical widths and thickness fringes. Rows + combine multiplicatively as independent attenuation channels; the + many-beam coupling between different rows at the zone axis crossings is + the one approximation. + + A row profile is a smooth function of a continuous variable, so patterns + rendered from the line set (render_kossel_lines) are exact in geometry + and free of raster interpolation at any pixel size, in Cartesian or + polar coordinates. + + Parameters + ---------- + k_max : float, default=1.2 + Reflections with |g| up to this are included; a row keeps every + order |n| |g| <= k_max. + u_step_mrad : float, default=0.05 + Profile sampling; the line widths are 1-2 mrad. + u_tail_mrad : float, default=150.0 + Profile extent beyond the outermost line of each row. The rocking + curve tails fall off as 1 / (s xi)^2 and are still ~1% at 40 mrad + for the strong reflections, so the window has to be wide for the + far-from-line background to be the true mean-absorption level. + min_depth : float, default=0.005 + Lines (and rows) whose deepest deficit at any thickness is below + this fraction of the background are dropped. + + Returns + ------- + dict with, per row, 'g_hat' (L, 3) crystal-frame unit normals, 'g_len' + (L,), 'hkl_row' (L, 3), 'log_trans' (L, T, n_u) log transmission + versus 'u' (n_u,) (0 far from the lines), 'background' (T,) the + far-from-line bright field; and per line 'line_row' (K,) row index, + 'line_order' (K,) the order n, 'line_hkl' (K, 3), 'line_u' (K,) the + cone position u = n lambda |g| / 2, 'line_depth' (K, T) the deepest + deficit fraction and 'line_width_mrad' (K, T) the equivalent width + (integrated deficit over depth). + """ + if crystal.g_vec is None: + raise RuntimeError("Run crystal.calculate_structure_factors() first.") + lam = electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + gamma_rel = relativistic_gamma(energy_ev) + t_thick = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) + + # unique rows: group reflections by ray direction (g and -g together), + # keep the shortest g of each as the row vector + g_all = crystal.g_vec + g_len = crystal.g_len + hkl = crystal.hkl + sel = (g_len <= k_max) & (g_len > 1e-8) + idx = torch.nonzero(sel).squeeze(1) + idx = idx[torch.argsort(g_len[idx])] + rows: list[int] = [] + dirs: list[torch.Tensor] = [] + for i in idx.tolist(): + d = g_all[i] / g_len[i] + if any(float(torch.abs(d @ e)) > 0.9999 for e in dirs): + continue + rows.append(i) + dirs.append(d) + + u_max = 0.5 * lam * k_max + u_tail_mrad * 1e-3 + du = u_step_mrad * 1e-3 + n_half = int(np.ceil(u_max / du)) + u = torch.arange(-n_half, n_half + 1, dtype=torch.float64) * du + + g_hat_out, g_len_out, hkl_out, lt_out, bg_rows = [], [], [], [], [] + l_row, l_order, l_hkl, l_u, l_depth, l_width = [], [], [], [], [], [] + for i in rows: + g1 = float(g_len[i]) + h1 = hkl[i] + n_ord = int(np.floor(k_max / g1 + 1e-9)) + ns = torch.arange(-n_ord, n_ord + 1, dtype=torch.long) + ns = ns[torch.argsort((ns != 0).to(torch.long), stable=True)] # 000 first + hkl_beams = ns[:, None] * h1[None, :] + U, u0_imag, absorptive = _coupling_matrix(crystal, hkl_beams, gamma_rel) + # projection of each row beam on the beam direction: n |g| u, and + # the same excitation error geometry as the reference pattern + # (foil normal along the beam) + ng = ns.to(torch.float64) * g1 + uu = u[:, None] * ng[None, :] + s_t = (2 * k0 * uu - ng[None, :] ** 2) / (2 * (k0 - uu)) + inten_b = _bloch_solve( + U, u0_imag, absorptive, s_t, k0, t_thick, fast_absorption=fast_absorption + ) + bf = inten_b[:, :, 0].transpose(0, 1) # (T, n_u) + bg = 0.5 * (bf[:, 0] + bf[:, -1]) # (T,) far-from-line level + trans = (bf / bg[:, None]).clamp_min(1e-6) + deficit = 1 - trans + + # per-line depth and width, each order in its own window of half + # the order spacing on either side of its cone + lines_here = [] + for n in range(-n_ord, n_ord + 1): + if n == 0: + continue + u_n = n * lam * g1 / 2 + win = torch.abs(u - u_n) <= lam * g1 / 4 + dep = deficit[:, win].amax(dim=1) # (T,) + if float(dep.max()) < min_depth: + continue + width = deficit[:, win].clamp_min(0).sum(dim=1) * du / dep.clamp_min(1e-9) + lines_here.append((n, u_n, dep, width * 1e3)) + if not lines_here: + continue + row_id = len(g_hat_out) + g_hat_out.append(g_all[i] / g_len[i]) + g_len_out.append(g1) + hkl_out.append(h1) + lt_out.append(torch.log(trans)) + bg_rows.append(bg) + for n, u_n, dep, width in lines_here: + l_row.append(row_id) + l_order.append(n) + l_hkl.append(n * h1) + l_u.append(u_n) + l_depth.append(dep) + l_width.append(width) + + return { + "g_hat": torch.stack(g_hat_out), + "g_len": torch.tensor(g_len_out, dtype=torch.float64), + "hkl_row": torch.stack(hkl_out), + "u": u, + "log_trans": torch.stack(lt_out), # (L, T, n_u) + "background": torch.stack(bg_rows).mean(dim=0), # (T,) + "line_row": torch.tensor(l_row, dtype=torch.long), + "line_order": torch.tensor(l_order, dtype=torch.long), + "line_hkl": torch.stack(l_hkl), + "line_u": torch.tensor(l_u, dtype=torch.float64), + "line_depth": torch.stack(l_depth), # (K, T) + "line_width_mrad": torch.stack(l_width), # (K, T) + "energy_ev": float(energy_ev), + "thicknesses": t_thick.numpy(), + } + + +def _detector_directions( + lam: float, + orientation: torch.Tensor, + semiconv_mrad: float, + polar: bool, + n_pixels: int, + n_radial: int, + n_azimuthal: int, +): + """Crystal-frame anti-propagation directions of a Cartesian or polar + detector grid, plus the grid axes. Polar grids follow the quantem + convention: rows are azimuth, columns radius.""" + from quantem.diffraction.rotations import quat_to_matrix + k0 = 1.0 / lam alpha_k = semiconv_mrad * 1e-3 / lam - ax = torch.linspace(-alpha_k, alpha_k, n_pixels, dtype=torch.float64) - ty, tx = torch.meshgrid(ax, ax, indexing="ij") - inside = (tx**2 + ty**2) <= alpha_k**2 + if polar: + r = torch.linspace(0, alpha_k, n_radial + 1, dtype=torch.float64)[1:] + phi = torch.arange(n_azimuthal, dtype=torch.float64) * (2 * np.pi / n_azimuthal) + tx = r[None, :] * torch.cos(phi)[:, None] + ty = r[None, :] * torch.sin(phi)[:, None] + inside = torch.ones_like(tx, dtype=torch.bool) + axes = {"radii_mrad": (r * lam * 1e3).numpy(), "azimuth_rad": phi.numpy()} + else: + ax = torch.linspace(-alpha_k, alpha_k, n_pixels, dtype=torch.float64) + ty, tx = torch.meshgrid(ax, ax, indexing="ij") + inside = (tx**2 + ty**2) <= alpha_k**2 + axes = {"mrad_per_pixel": 2 * semiconv_mrad / n_pixels} tz = torch.sqrt((k0**2 - tx**2 - ty**2).clamp_min(0)) - d_lab = torch.stack([tx, ty, tz], dim=-1) / k0 # incident directions + # the beam landing at detector tilt +t propagates along (t, -tz); the + # line set and the reference parameterize the anti-propagation direction + d_lab = torch.stack([-tx, -ty, tz], dim=-1) / k0 + R = quat_to_matrix(torch.atleast_2d(torch.as_tensor(orientation, dtype=torch.float64))[0]).to( + torch.float64 + ) + d_c = torch.einsum("ji,rcj->rci", R, d_lab) # crystal frame, R^T d + return d_c, inside, axes, R + + +def _lines_bright_field(lines: dict, d_c: torch.Tensor) -> torch.Tensor: + """Line-model bright field at crystal-frame directions d_c (..., 3): + product of the row transmissions read at u = d . g_hat; returns + (..., T).""" + u = torch.einsum("...i,li->...l", d_c, lines["g_hat"]) # (.., L) + u_ax = lines["u"] + n_u = u_ax.shape[0] + du = float(u_ax[1] - u_ax[0]) + lt = lines["log_trans"].permute(0, 2, 1) # (L, n_u, T) + f = ((u - float(u_ax[0])) / du).clamp(0, n_u - 1 - 1e-9) + i0 = f.floor().to(torch.long) + w = (f - i0)[..., None] + L_idx = torch.arange(lt.shape[0]).reshape((1,) * (u.dim() - 1) + (-1,)) + v = lt[L_idx, i0] * (1 - w) + lt[L_idx, (i0 + 1).clamp(max=n_u - 1)] * w + return lines["background"] * torch.exp(v.sum(dim=-2)) + + +def kossel_reference_residual(master: dict, lines: dict, crystal: Crystal) -> dict: + """Add the many-beam residual of the line model to a reference pattern. + + The line model is evaluated at the reference's own wedge samples and + rasterized onto the same Lambert grid, and the difference (reference + minus line model) is stored as master['residual']. It is zero away + from the zone axes, where the rows are independent, and carries the + many-beam correction of the zone axis rosettes. render_kossel_lines() + adds it by lookup when given the reference. + """ + if not np.allclose(master["thicknesses"], lines["thicknesses"]): + raise ValueError("reference and line set must share the thickness grid") + dirs = torch.as_tensor(master["directions"], dtype=torch.float64) + I_lines = _lines_bright_field(lines, dirs) # (N, T) + lambert_lines = _lambert_raster(crystal, dirs, I_lines, master["step"]) + master["residual"] = np.nan_to_num(master["lambert"] - lambert_lines, nan=0.0) + return master + +def render_kossel_lines( + lines: dict, + orientation: torch.Tensor, + semiconv_mrad: float = 40.0, + n_pixels: int = 256, + polar: bool = False, + n_radial: int = 64, + n_azimuthal: int = 180, + reference: dict | None = None, +) -> dict: + """Render the bright field from the Kossel line set, all thicknesses. + + Every detector direction is projected on every row normal and the row + profiles are read there: one evaluation per pixel and row, no raster + in between, so the result is smooth at any resolution in Cartesian or + polar coordinates. A polar pattern is sampled directly at the polar + detector positions. + + Parameters + ---------- + reference : dict | None + A reference pattern carrying the many-beam residual from + kossel_reference_residual(). If given, the residual is added to + the rendered pattern: the line model then also carries the + many-beam intensity of the zone axis rosettes (which the + independent-row product gets too dark), while the lines themselves + keep their exact analytic geometry. + + Returns + ------- + dict with 'bright_field' ((T, n, n), squeezed; NaN outside the + aperture) or, with polar=True, 'polar' ((T, n_azimuthal, n_radial), + squeezed; rows are azimuth, columns radius), plus the grid axes and + 'thicknesses'. + """ + lam = electron_wavelength_angstrom(lines["energy_ev"]) + d_c, inside, axes, _ = _detector_directions( + lam, orientation, semiconv_mrad, polar, n_pixels, n_radial, n_azimuthal + ) + bf = _lines_bright_field(lines, d_c).permute(2, 0, 1).numpy() # (T, ..) + if reference is not None: + if "residual" not in reference: + raise ValueError( + "reference has no many-beam residual: run " + "kossel_reference_residual(reference, lines, crystal) first." + ) + bf = bf + _lambert_lookup(reference["residual"], reference["step"], d_c) + bf[:, ~inside.numpy()] = np.nan + T = bf.shape[0] + + out = {"polar" if polar else "bright_field": bf[0] if T == 1 else bf} + out.update(axes) + out["thicknesses"] = lines["thicknesses"] + return out + + +def kossel_line_segments( + lines: dict, + orientation: torch.Tensor, + semiconv_mrad: float = 40.0, + thickness_index: int = 0, +) -> dict: + """The Kossel lines crossing the aperture as vector segments. + + Each line is the cone d . g_hat = u of its reflection, which within + the aperture is a straight line in the detector tilt plane (the + curvature term is |g_z| alpha^2 / 2, below 0.1 mrad at 40 mrad). The + end points on the aperture edge are computed exactly from the cone. + + Returns + ------- + dict of arrays over the K visible lines. Cartesian positions are + (row, col) tilt angles in mrad, matching the image axes of + render_kossel_lines: 'start_mrad', 'stop_mrad' (K, 2) the end points + on the aperture edge; 'normal' (K, 2) the unit normal of the line and + 'distance_mrad' (K,) its signed distance from the optic axis, so the + line is the set of points with p . normal = distance. Polar positions + are (azimuth_rad, radius_mrad): 'start_polar', 'stop_polar' (K, 2), the + end points at radius = semiconv_mrad; in between the line follows + radius = distance / cos(azimuth - azimuth_normal). Also 'hkl' (K, 3), + 'depth' (K,) the deficit fraction and 'width_mrad' (K,) the equivalent + width at the chosen thickness. + """ from quantem.diffraction.rotations import quat_to_matrix - R = quat_to_matrix( - torch.atleast_2d(torch.as_tensor(orientation, dtype=torch.float64))[0] - ).to(torch.float64) - d_c = torch.einsum("ji,rcj->rci", R, d_lab) # crystal frame, R^T d - d_c = torch.where(d_c[..., 2:3] < 0, -d_c, d_c) # reciprocity fold + alpha = semiconv_mrad * 1e-3 + R = ( + quat_to_matrix(torch.atleast_2d(torch.as_tensor(orientation, dtype=torch.float64))[0]) + .to(torch.float64) + .numpy() + ) + g_lab = (R @ lines["g_hat"].numpy().T).T # d_lab . g_lab = d_c . g_c + rows = lines["line_row"].numpy() + u_k = lines["line_u"].numpy() + g = g_lab[rows] # (K, 3) + # cone in tilt angles theta = (theta_x, theta_y), d_lab = (-theta, sqrt(1 - theta^2)): + # -g_x theta_x - g_y theta_y + g_z sqrt(1 - theta^2) = u + gxy = np.hypot(g[:, 0], g[:, 1]) + ok = gxy > 1e-9 + phi_g = np.arctan2(g[:, 1], g[:, 0]) + cz = np.sqrt(1 - alpha**2) + # on the aperture edge theta = alpha (cos phi, sin phi): + # cos(phi - phi_g) = (g_z cz - u) / (|g_xy| alpha) + c = np.where(ok, (g[:, 2] * cz - u_k) / np.maximum(gxy * alpha, 1e-12), 2.0) + ok &= np.abs(c) < 1 + dphi = np.arccos(np.clip(c[ok], -1, 1)) + phi_a = phi_g[ok] + dphi + phi_b = phi_g[ok] - dphi + # small-angle line: (g_x, g_y) . theta = g_z - u + p = (g[ok, 2] - u_k[ok]) / gxy[ok] # signed distance (rad) along -normal + normal = np.stack([g[ok, 1], g[ok, 0]], axis=1) / gxy[ok, None] # (row, col) + + def pt(phi): + # (row, col) = (theta_y, theta_x) in mrad + return np.stack([alpha * np.sin(phi), alpha * np.cos(phi)], axis=1) * 1e3 + + ti = thickness_index + return { + "hkl": lines["line_hkl"].numpy()[ok], + "start_mrad": pt(phi_a), + "stop_mrad": pt(phi_b), + "start_polar": np.stack( + [np.mod(phi_a, 2 * np.pi), np.full(phi_a.shape, semiconv_mrad)], axis=1 + ), + "stop_polar": np.stack( + [np.mod(phi_b, 2 * np.pi), np.full(phi_b.shape, semiconv_mrad)], axis=1 + ), + "normal": normal, + "distance_mrad": p * 1e3, + "depth": lines["line_depth"].numpy()[ok, ti], + "width_mrad": lines["line_width_mrad"].numpy()[ok, ti], + } - step = master["step"] - lambert = master["lambert"] - half = (lambert.shape[-1] - 1) // 2 - rho = torch.sqrt((2 * (1 - d_c[..., 2])).clamp_min(0)) - dxy = torch.linalg.norm(d_c[..., :2], dim=-1).clamp_min(1e-12) - fx = (d_c[..., 0] / dxy * rho / step + half).numpy() - fy = (d_c[..., 1] / dxy * rho / step + half).numpy() - T = lambert.shape[0] - n_l = lambert.shape[-1] - ix0 = np.clip(np.floor(fx).astype(int), 0, n_l - 2) - iy0 = np.clip(np.floor(fy).astype(int), 0, n_l - 2) - wx = np.clip(fx - ix0, 0, 1) - wy = np.clip(fy - iy0, 0, 1) - bf = np.full((T, n_pixels, n_pixels), np.nan) - m = inside.numpy() - for ti in range(T): - L = lambert[ti] - val = ( - L[ix0, iy0] * (1 - wx) * (1 - wy) - + L[ix0 + 1, iy0] * wx * (1 - wy) - + L[ix0, iy0 + 1] * (1 - wx) * wy - + L[ix0 + 1, iy0 + 1] * wx * wy +def overlay_kossel_segments( + ax, + segments: dict, + semiconv_mrad: float, + n_pixels: int | None = None, + polar: bool = False, + n_radial: int | None = None, + n_azimuthal: int | None = None, + color=(0.9, 0.0, 0.0), + width_scale: float = 1.0, + min_depth: float = 0.05, +): + """Draw the vector line segments over a rendered pattern. + + Line width is the equivalent width of each line in pixels (times + width_scale) and the opacity is its depth. On a Cartesian axis the + segments run between their aperture-edge end points; on a polar axis + (rows azimuth, columns radius) each straight line becomes the curve + radius = distance / cos(azimuth - azimuth_normal), drawn from end + point to end point and split at the azimuth wrap. + """ + sel = segments["depth"] >= min_depth + n_lines = int(sel.sum()) + p = segments["distance_mrad"][sel] + nrm = segments["normal"][sel] + start = segments["start_mrad"][sel] + stop = segments["stop_mrad"][sel] + dep = segments["depth"][sel] + wid = segments["width_mrad"][sel] + if polar: + px_r = n_radial / semiconv_mrad + px_phi = n_azimuthal / (2 * np.pi) + tang = np.stack([-nrm[:, 1], nrm[:, 0]], axis=1) + t_edge = np.sqrt(np.maximum(semiconv_mrad**2 - p**2, 0)) + t = np.linspace(-1, 1, 400) + for k in range(n_lines): + pts = p[k] * nrm[k][None, :] + (t * t_edge[k])[:, None] * tang[k][None, :] + r = np.hypot(pts[:, 0], pts[:, 1]) + phi = np.mod(np.arctan2(pts[:, 0], pts[:, 1]), 2 * np.pi) + jumps = np.abs(np.diff(phi)) > np.pi + phi = np.ma.array(phi, mask=np.r_[False, jumps]) + ax.plot( + r * px_r - 0.5, + phi * px_phi - 0.5, + color=color, + lw=wid[k] * px_r * width_scale, + alpha=float(dep[k]), + solid_capstyle="butt", + ) + else: + px = n_pixels / (2 * semiconv_mrad) + for k in range(n_lines): + ax.plot( + [ + (start[k, 1] + semiconv_mrad) * px - 0.5, + (stop[k, 1] + semiconv_mrad) * px - 0.5, + ], + [ + (start[k, 0] + semiconv_mrad) * px - 0.5, + (stop[k, 0] + semiconv_mrad) * px - 0.5, + ], + color=color, + lw=wid[k] * px * width_scale, + alpha=float(dep[k]), + solid_capstyle="butt", + ) + return ax + + +def dynamical_tilt_set( + energy_ev: float, + precession_deg: float = 0.0, + n_precession: int = 8, + semiconv_mrad: float = 0.0, + n_disk_rings: int = 2, + maped_tilts_deg=None, +) -> torch.Tensor: + """Incident beam tilts (M, 2) in 1/Angstroms whose Bloch intensities are + averaged to model one measured pattern: a ring for precession, a filled + disk for the convergence angle, an explicit list for MAPED, or their + combination (ring x disk). Zero tilt alone when none apply.""" + lam = electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + if maped_tilts_deg is not None: + ring = k0 * torch.sin(torch.deg2rad(torch.as_tensor(maped_tilts_deg, dtype=torch.float64))) + elif precession_deg > 0: + phi = torch.arange(n_precession, dtype=torch.float64) * (2 * np.pi / n_precession) + r = k0 * np.sin(np.deg2rad(precession_deg)) + ring = torch.stack([r * torch.cos(phi), r * torch.sin(phi)], dim=1) + else: + ring = torch.zeros((1, 2), dtype=torch.float64) + if semiconv_mrad > 0: + disk = tilt_grid(semiconv_mrad, energy_ev, n_rings=n_disk_rings) + else: + disk = torch.zeros((1, 2), dtype=torch.float64) + return (ring[:, None, :] + disk[None, :, :]).reshape(-1, 2) + + +def _dynamical_cost(si, sq, qxy, im, delta, min_sim_rel_p: float = 0.0): + """Intensity cost (M, T) of simulated beams (M, T, N) at positions sq + (N, 2) against measured peaks (P, 2) with intensities im (P,), with a + free scale per (tilt, thickness). Pairing is by position (within + delta); the position residuals themselves do not enter, they belong + to the deformation fit. Unpaired simulated beams weaker than + min_sim_rel_p times the strongest simulated beam (in the compared + power-law intensities) are ignored: they are the beams a detector + would not see, and with power_intensity < 1 they would otherwise + dominate the unpaired term.""" + d = torch.cdist(sq, qxy) + d_min, j_min = d.min(dim=1) + pair = d_min < delta + if int(pair.sum()) == 0: + return None, pair, j_min, d_min + a = si[:, :, pair] + b = im[j_min[pair]][None, None, :] + w = ((a * b).sum(dim=2) / (a * a).sum(dim=2).clamp_min(1e-12)).clamp_min(0)[:, :, None] + c_paired = (b - w * a).abs().sum(dim=2) + s_unp = si[:, :, ~pair] + strong = s_unp > min_sim_rel_p * si.amax(dim=2, keepdim=True) + c_unpaired_sim = 0.5 * w[:, :, 0] * (s_unp * strong).sum(dim=2) + matched = torch.zeros(im.shape[0], dtype=torch.bool) + matched[j_min[pair]] = True + # the measured direct beam is not a diffracted intensity: leave it out + # of the unexplained-measured term and of the normalization + direct = torch.linalg.norm(qxy, dim=1) < delta + matched |= direct + c_unpaired_exp = 0.5 * float(im[~matched].sum()) + norm = float(im[~direct].sum()) + 1e-12 + cost = (c_paired + c_unpaired_sim + c_unpaired_exp) / norm + return cost, pair, j_min, d_min + + +def _fit_deformation(sq, qxy, w_exp, delta): + """Symmetric in-plane deformation S and in-plane rotation angle wz + (radians) from the paired positions: A = (sum w qm qs^T)(sum w qs qs^T)^-1 + with measured = A ideal, split by polar decomposition A = S Q.""" + d = torch.cdist(sq, qxy) + d_min, j_min = d.min(dim=1) + pair = d_min < delta + if int(pair.sum()) < 3: + return None, 0.0, pair + qs = sq[pair] + qm = qxy[j_min[pair]] + w = w_exp[j_min[pair]] * (1 - d_min[pair] / delta).clamp_min(0) + M1 = torch.einsum("p,pi,pj->ij", w, qm, qs) + M2 = torch.einsum("p,pi,pj->ij", w, qs, qs) + A = M1 @ torch.linalg.inv(M2 + 1e-12 * torch.eye(2, dtype=torch.float64)) + U_, _, Vh_ = torch.linalg.svd(A) + Q = U_ @ Vh_ + if torch.linalg.det(Q) < 0: + return None, 0.0, pair + S = A @ Q.T + S = 0.5 * (S + S.T) + wz = float(torch.atan2(Q[1, 0], Q[0, 0])) + return S, wz, pair + + +def refine_dynamical( + phase_map, + thicknesses_A: np.ndarray | None = None, + tilt_stages=((0.3, 0.05), (0.06, 0.01)), + precession_deg: float | None = None, + n_precession: int = 32, + semiconv_mrad: float | None = None, + maped_tilts_deg=None, + refine_deformation: bool = True, + pair_distance: float | None = None, + power_intensity: float | None = None, + min_sim_intensity_rel: float | None = None, + sg_max: float = SG_MAX, + k_max: float | None = None, + min_number_peaks: int | None = None, + mask: np.ndarray | None = None, + fast_absorption: bool = True, + update_orientations: bool = True, + progress_bar: bool = True, +) -> dict: + """Dynamical refinement on the Bragg vectors: orientation, thickness, + in-plane deformation and candidate, pixel by pixel. + + Starting from the kinematically matched orientation of each candidate, + the crystal is re-initialized at every trial orientation of a + coarse-to-fine tilt grid and its diffracted intensities computed with + Bloch waves, averaged over the precession ring, the convergence disk or + the MAPED tilt list, for all thicknesses at once (one batched + eigendecomposition per stage). The peak pairing is fixed by the + positions, which the tilt does not move; the intensity cost is + minimized over (tilt, thickness), and the tilt is interpolated + parabolically at the finest stage. At the refined orientation the + symmetric in-plane deformation of the tilted cell is solved in closed + form from the paired positions (weighted least squares), and its + antisymmetric part, an in-plane rotation, is folded into the + orientation. The candidate with the lowest cost decides the phase. + + The intensities are far more tilt-sensitive than the positions: at + 500 A the rocking curve width is ~2e-3 1/A, so a 0.05 degree tilt + error is already visible in the weak beams. The default stages search + +-0.3 degrees at 0.05 and then +-0.06 at 0.01 degrees, 338 trial + orientations per candidate, and should start from orientations + refined by refine_orientations(). + + Parameters left as None inherit from the previous stages: the + pairing distance, intensity power, weak-beam cut and peak minimum from + the phase fit, and the precession and convergence angles from the + OrientationMaps (from_vectors). The resolved values are recorded in + phase_map.metadata['dynamical'] and returned under 'metadata'. + + Parameters + ---------- + phase_map : PhaseMap + A fitted PhaseMap (fit() has been run). + thicknesses_A : np.ndarray | None + Thickness grid in Angstroms; default 50 ... 2000 in 25 A steps (the + thickness axis is free: all thicknesses come from one + eigendecomposition). + tilt_stages : sequence of (half_range_deg, step_deg) + Successive tilt grids, each centered on the previous optimum. + precession_deg, n_precession : float | None, int + Precession semi-angle (inherited from the OrientationMap) and the + number of azimuthal samples on the ring. Uniform sampling is the + Gauss-Chebyshev quadrature of the ring integral; the rocking + curves oscillate at pi t rho k0 g along the ring, so 32 or more + samples are needed at 500 A and 0.5 degrees. + semiconv_mrad : float | None + Convergence semiangle (inherited); the intensities are averaged + over the disk. + maped_tilts_deg : array-like | None + Explicit (M, 2) beam tilt list (degrees) for MAPED, overriding + precession. + min_sim_intensity_rel : float, default=0.02 + Unpaired simulated beams weaker than this fraction of the + strongest simulated beam do not count against a candidate (the + detector would not have seen them). + refine_deformation : bool, default=True + Solve the symmetric in-plane deformation and the in-plane rotation + from the paired positions before the intensity search; the + deformation is applied to the tilted cell in the Bloch calculation + and the rotation folded into the orientation. + mask : np.ndarray | None + (R, C) boolean; only these positions are refined. + fast_absorption : bool, default=True + First-order treatment of absorption (Hermitian eigh, ~4x faster, + 0.5% rms intensity error); the tilt and thickness are never + treated perturbatively. + update_orientations : bool, default=True + Write the refined quaternions back into the OrientationMaps. + + Returns + ------- + dict with 'thickness' (R, C) at the winning candidate, 'tilt_deg' + (R, C, 2) its tilt correction, 'quats' (R, C, F, 4) refined + orientations, 'deformation' (R, C, F, 2, 2) symmetric in-plane + deformation A of the tilted cell in the calibrated frame (measured + reciprocal positions = A x ideal), 'cost' (R, C, F), 'cost_zero_tilt' + (R, C, F) the cost at the matched orientation (its difference to + 'cost' is the gain of the tilt search; a small gain means the + intensities do not constrain the tilt), 'phase_index' (R, C), + 'candidate' (R, C), 'thickness_per_candidate' (R, C, F). + """ + from quantem.diffraction.rotations import qmult, quat_from_axis_angle + + if thicknesses_A is None: + thicknesses_A = np.arange(50.0, 2000.0, 25.0) + t_grid = torch.as_tensor(thicknesses_A, dtype=torch.float64) + T = t_grid.shape[0] + + oms = phase_map.orientation_maps + fit_md = phase_map.metadata.get("fit") if hasattr(phase_map, "metadata") else None + om_md = oms[0].metadata if hasattr(oms[0], "metadata") else None + pair_distance = resolve(pair_distance, "pair_distance", fit_md, default=PAIR_DISTANCE) + power_intensity = resolve(power_intensity, "power_intensity", fit_md, default=POWER_INTENSITY) + min_sim_intensity_rel = resolve( + min_sim_intensity_rel, "min_sim_intensity_rel", fit_md, default=MIN_SIM_INTENSITY_REL + ) + min_number_peaks = resolve( + min_number_peaks, "min_number_peaks", fit_md, default=MIN_NUMBER_PEAKS + ) + precession_deg = float(resolve(precession_deg, "precession_deg", om_md, default=0.0)) + semiconv_mrad = float(resolve(semiconv_mrad, "semiconv_mrad", om_md, default=0.0)) + used = dict( + thicknesses_A=np.asarray(thicknesses_A, dtype=float).tolist(), + tilt_stages=[tuple(float(v) for v in st) for st in tilt_stages], + precession_deg=precession_deg, + n_precession=int(n_precession), + semiconv_mrad=semiconv_mrad, + maped_tilts_deg=maped_tilts_deg, + refine_deformation=bool(refine_deformation), + pair_distance=float(pair_distance), + power_intensity=float(power_intensity), + min_sim_intensity_rel=float(min_sim_intensity_rel), + sg_max=float(sg_max), + k_max=k_max, + min_number_peaks=int(min_number_peaks), + fast_absorption=bool(fast_absorption), + ) + if hasattr(phase_map, "metadata"): + phase_map.metadata["dynamical"] = used + cands = phase_map.candidates + peaks = oms[0].peaks + R, C = peaks.shape[0], peaks.shape[1] + F = len(cands) + delta = pair_distance + energy_ev = oms[0].energy_ev + lam = electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + fields = peaks.fields + ix = [fields.index(f) for f in ("qx", "qy", "intensity")] + + ring = dynamical_tilt_set( + energy_ev, precession_deg, n_precession, semiconv_mrad, maped_tilts_deg=maped_tilts_deg + ) # (Mr, 2) + Mr = ring.shape[0] + + def stage_grid(center, half, step): + n = int(round(2 * half / step)) + 1 + tg = torch.linspace(-half, half, n, dtype=torch.float64) + wx_g, wy_g = torch.meshgrid(tg, tg, indexing="ij") + w = torch.stack([wx_g.reshape(-1), wy_g.reshape(-1)], dim=1) + center[None, :] + return w, n, tg + + cost_out = torch.full((R, C, F), torch.nan, dtype=torch.float64) + cost0_out = torch.full((R, C, F), torch.nan, dtype=torch.float64) + thick_out = torch.full((R, C, F), torch.nan, dtype=torch.float64) + tilt_out = torch.zeros((R, C, F, 2), dtype=torch.float64) + quat_out = torch.zeros((R, C, F, 4), dtype=torch.float64) + quat_out[..., 0] = 1.0 + deform_out = torch.zeros((R, C, F, 2, 2), dtype=torch.float64) + deform_out[..., 0, 0] = 1.0 + deform_out[..., 1, 1] = 1.0 + + iterator = list(np.ndindex(R, C)) + if mask is not None: + iterator = [(r, c) for r, c in iterator if mask[r, c]] + if progress_bar: + iterator = tqdm(iterator, desc="dynamical refinement") + for rx, ry in iterator: + data = peaks[rx, ry].array + if data.shape[0] < min_number_peaks: + continue + qxy = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) + im = torch.as_tensor(data[:, ix[2]], dtype=torch.float64).clamp_min(0) + im = im**power_intensity + w_exp = im / im.max().clamp_min(1e-12) + + for f, (i_om, m) in enumerate(cands): + om = oms[i_om] + if om.corr[rx, ry, m] <= 0: + continue + if ( + phase_map.phase_weights is not None + and float(phase_map.phase_weights[rx, ry, f]) <= 0 + ): + continue + q0 = om.quats[rx, ry, m] + quat_out[rx, ry, f] = q0 + S = None + deform3 = None + if refine_deformation: + # in-plane deformation and rotation from the positions first: + # the rotation is folded into the orientation and the + # symmetric deformation applied to the tilted cell, so the + # intensity search below sees the strained lattice and a + # pairing free of position residuals + g0 = qrotate(q0, om.crystal.g_vec) + near = ( + torch.abs((2 * g0[:, 2] - lam * (g0**2).sum(1)) / (2 - 2 * lam * g0[:, 2])) + < sg_max + ) + S, wz, _ = _fit_deformation(g0[near, :2], qxy, w_exp, delta) + if S is not None: + half_z = torch.tensor(wz / 2, dtype=torch.float64) + dqz = torch.stack( + [torch.cos(half_z), torch.zeros(()), torch.zeros(()), torch.sin(half_z)] + ).to(torch.float64) + q0 = qmult(dqz, q0) + deform3 = torch.eye(3, dtype=torch.float64) + deform3[:2, :2] = S + deform_out[rx, ry, f] = S + center = torch.zeros(2, dtype=torch.float64) + best = None + for half, step in tilt_stages: + half = np.deg2rad(half) + step = np.deg2rad(step) + w_grid, n, tg = stage_grid(center, half, step) + Mt = w_grid.shape[0] + # crystal tilt (wx, wy) about the in-plane axes shifts s_g by + # wx g_y - wy g_x; the same excitation errors come from a + # beam tilt k0 (wy, -wx) in the fixed-normal Bloch geometry, + # so every trial orientation is a full re-solve of the Bloch + # problem with the coupling matrix shared + trial = k0 * torch.stack([w_grid[:, 1], -w_grid[:, 0]], dim=1) + tilts = (trial[:, None, :] + ring[None, :, :]).reshape(-1, 2) + inten, g_xy, _ = _cbed_amplitudes( + om.crystal, + q0, + tilts, + t_grid, + energy_ev, + sg_max, + k_max, + tilt_batch=max(64, Mr * 8), + progress_bar=False, + fast_absorption=fast_absorption, + deform=deform3, + ) + inten = inten.reshape(Mt, Mr, T, -1).mean(dim=1) # (Mt, T, nb) + si = inten[:, :, 1:] ** power_intensity + sq = g_xy[1:] + if sq.shape[0] == 0: + break + cost, pair, j_min, d_min = _dynamical_cost( + si, sq, qxy, im, delta, min_sim_intensity_rel**power_intensity + ) + if cost is None: + break + flat = int(cost.argmin()) + m_best, t_best = flat // T, flat % T + i_b, j_b = m_best // n, m_best % n + if half == np.deg2rad(tilt_stages[0][0]): + # untilted reference: the best thickness at the matched + # orientation, for the gain the tilt search achieves + m0 = int(((w_grid**2).sum(1)).argmin()) + cost0_out[rx, ry, f] = float(cost[m0].min()) + cost_t = cost[:, t_best].reshape(n, n) + wx, wy = float(w_grid[m_best, 0]), float(w_grid[m_best, 1]) + if 0 < i_b < n - 1: + c0, c1, c2 = cost_t[i_b - 1, j_b], cost_t[i_b, j_b], cost_t[i_b + 1, j_b] + den = float(c0 - 2 * c1 + c2) + if den > 1e-12: + wx += 0.5 * float(c0 - c2) / den * step + if 0 < j_b < n - 1: + c0, c1, c2 = cost_t[i_b, j_b - 1], cost_t[i_b, j_b], cost_t[i_b, j_b + 1] + den = float(c0 - 2 * c1 + c2) + if den > 1e-12: + wy += 0.5 * float(c0 - c2) / den * step + center = torch.tensor([wx, wy], dtype=torch.float64) + best = ( + float(cost[m_best, t_best]), + float(t_grid[t_best]), + wx, + wy, + sq, + pair, + j_min, + d_min, + ) + if best is None: + continue + c_best, t_fit, wx, wy, sq, pair, j_min, d_min = best + cost_out[rx, ry, f] = c_best + thick_out[rx, ry, f] = t_fit + tilt_out[rx, ry, f, 0] = wx + tilt_out[rx, ry, f, 1] = wy + q = q0 + ang = float(np.hypot(wx, wy)) + if ang > 1e-12: + axis = torch.tensor([wx / ang, wy / ang, 0.0], dtype=torch.float64) + q = qmult(quat_from_axis_angle(axis, torch.tensor(ang, dtype=torch.float64)), q0) + + quat_out[rx, ry, f] = q + + n_maps = len(oms) + cost_f = torch.nan_to_num(cost_out, nan=torch.inf) + cost_phase = torch.full((R, C, n_maps), torch.inf, dtype=torch.float64) + for f, (i_om, _) in enumerate(cands): + cost_phase[..., i_om] = torch.minimum(cost_phase[..., i_om], cost_f[..., f]) + phase_index = cost_phase.argmin(dim=-1) + f_best = cost_f.argmin(dim=-1) + thickness = torch.gather(thick_out, 2, f_best[..., None]).squeeze(-1) + tilt_deg = torch.rad2deg( + torch.gather(tilt_out, 2, f_best[..., None, None].expand(R, C, 1, 2)).squeeze(2) + ) + + if update_orientations: + for f, (i_om, m) in enumerate(cands): + done = torch.isfinite(cost_out[..., f]) + oms[i_om].quats[..., m, :][done] = quat_out[..., f, :][done] + + return { + "thickness": thickness, + "tilt_deg": tilt_deg, + "quats": quat_out, + "deformation": deform_out, + "cost": cost_out, + "cost_zero_tilt": cost0_out, + "phase_index": phase_index, + "candidate": f_best, + "thickness_per_candidate": thick_out, + "metadata": used, + } + + +def strain_crystal_frame(deformation: torch.Tensor, quats: torch.Tensor) -> dict: + """Strain tensor components in the crystal Cartesian frame. + + The measured in-plane reciprocal deformation A (2, 2) of the tilted + cell (measured = A x ideal) is the reciprocal image of the real-space + deformation F = A^-T restricted to the beam-normal plane; only that + in-plane part is observable from one projection, and the components + along the beam are set to zero before the tensor is rotated into the + crystal frame with the 3x3 orientation matrix R (v_lab = R v_crystal): + eps_crystal = R^T eps_lab R. The crystal axes are the Cartesian frame + of the cell (x along a, z along c; for hexagonal cells 'b' is the + in-basal-plane direction perpendicular to a). + + Parameters + ---------- + deformation : torch.Tensor + (..., 2, 2) symmetric in-plane deformation from refine_dynamical + (or the columns of OrientationMap.calculate_strain's A). + quats : torch.Tensor + (..., 4) orientations. + + Returns + ------- + dict of (...) tensors 'aa', 'bb', 'cc', 'ab', 'ac', 'bc' (strain + components) and 'eps_crystal' (..., 3, 3). + """ + from quantem.diffraction.rotations import quat_to_matrix + + A = deformation.to(torch.float64) + Fp = torch.linalg.inv(A).transpose(-1, -2) # real-space in-plane deformation + eps2 = 0.5 * (Fp + Fp.transpose(-1, -2)) - torch.eye(2, dtype=torch.float64) + eps_lab = torch.zeros(A.shape[:-2] + (3, 3), dtype=torch.float64) + eps_lab[..., :2, :2] = eps2 + Rm = quat_to_matrix(quats.to(torch.float64)) + eps_c = torch.einsum("...ji,...jk,...kl->...il", Rm, eps_lab, Rm) + return { + "aa": eps_c[..., 0, 0], + "bb": eps_c[..., 1, 1], + "cc": eps_c[..., 2, 2], + "ab": eps_c[..., 0, 1], + "ac": eps_c[..., 0, 2], + "bc": eps_c[..., 1, 2], + "eps_crystal": eps_c, + } + + +def plot_strain_crystal_frame( + strain: dict, + mask: np.ndarray | None = None, + strain_range_percent: tuple[float, float] = (-2.0, 2.0), + scalebar=None, + axsize: tuple[float, float] = (4.0, 4.0), + cmap: str = "RdBu_r", +): + """Six strain components in the crystal frame as maps. + + Normal strains along the crystal a, b and c axes on the top row and + the ab, ac and bc shears below, in percent, masked where the fit is + not trusted. Components with a c (beam-direction) index are the + rotated in-plane measurement only; see strain_crystal_frame(). + """ + from quantem.core.visualization import show_2d + + keys = [["aa", "bb", "cc"], ["ab", "ac", "bc"]] + names = [["ε_aa", "ε_bb", "ε_cc"], ["ε_ab", "ε_ac", "ε_bc"]] + m = 1.0 if mask is None else np.asarray(mask, dtype=float) + imgs = [[np.asarray(strain[k]) * 100 * m for k in row] for row in keys] + lo, hi = strain_range_percent + return show_2d( + imgs, + title=[[n + " (%)" for n in row] for row in names], + cmap=cmap, + cbar=True, + norm={"interval_type": "manual", "vmin": lo, "vmax": hi}, + scalebar=scalebar, + axsize=axsize, + ) + + +# ---------------------------------------------------------------------- +# image-based dynamical refinement (the final step, on the pattern pixels) +# ---------------------------------------------------------------------- + + +def _q_to_pixels( + q_xy: torch.Tensor, origin_rc, pixel_size: float, rotation_ccw_deg: float, ellipse +): + """Calibrated (qx, qy) [row, col frame] -> detector pixel (row, col): + undo the scan rotation and the ellipse correction of + calibration.peaks_to_calibrated, then scale and shift to the origin.""" + q = q_xy.to(torch.float64) + if rotation_ccw_deg: + th = np.deg2rad(-rotation_ccw_deg) + rot = torch.tensor( + [[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]], dtype=torch.float64 ) - val[~m] = np.nan - bf[ti] = val + q = q @ rot.T + if ellipse is not None: + e11, e12 = float(ellipse[0]), float(ellipse[1]) + A = torch.tensor([[1 + e11, e12], [e12, 1 - e11]], dtype=torch.float64) + q = q @ torch.linalg.inv(A).T + return q / pixel_size + torch.as_tensor(origin_rc, dtype=torch.float64)[None, :] + + +def render_disks( + centers_px: torch.Tensor, + intensities: torch.Tensor, + shape: tuple[int, int], + disk_radius_px: float, + edge_px: float, +) -> torch.Tensor: + """Sum of soft-edged disks: (..., ny, nx) images for intensities (..., N) + at centers (N, 2) [row, col]. The edge is a logistic of width edge_px + (the disk profile of a defocused or blurred aperture).""" + ny, nx = shape + rows = torch.arange(ny, dtype=torch.float64) + cols = torch.arange(nx, dtype=torch.float64) + d = torch.sqrt( + (rows[None, :, None] - centers_px[:, 0, None, None]) ** 2 + + (cols[None, None, :] - centers_px[:, 1, None, None]) ** 2 + ) # (N, ny, nx) + disks = torch.sigmoid((disk_radius_px - d) / max(edge_px, 1e-3)) + return torch.einsum("...n,nyx->...yx", intensities.to(torch.float64), disks) + + +def render_pattern_image( + crystal: Crystal, + orientation: torch.Tensor, + thicknesses_A, + energy_ev: float, + shape: tuple[int, int], + origin_rc, + pixel_size: float, + rotation_ccw_deg: float = 0.0, + ellipse=None, + deform: torch.Tensor | None = None, + disk_radius_px: float = 3.0, + edge_px: float = 1.0, + tilts: torch.Tensor | None = None, + trial_tilts: torch.Tensor | None = None, + sg_max: float = SG_MAX, + k_max: float | None = None, + fast_absorption: bool = True, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Dynamical diffraction pattern images: Bloch intensities (averaged over + the precession / convergence tilt set `tilts`) rendered as disks on the + detector grid, for every trial orientation tilt and every thickness. + + Returns (images (M, T, ny, nx), centers_px (N, 2), intensities + (M, T, N)); M is the number of trial tilts (1 when None).""" + t_grid = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) + lam = electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + ring = torch.zeros((1, 2), dtype=torch.float64) if tilts is None else tilts + Mr = ring.shape[0] + if trial_tilts is None: + trial = torch.zeros((1, 2), dtype=torch.float64) + else: + trial = k0 * torch.stack([trial_tilts[:, 1], -trial_tilts[:, 0]], dim=1) + Mt = trial.shape[0] + all_tilts = (trial[:, None, :] + ring[None, :, :]).reshape(-1, 2) + inten, g_xy, _ = _cbed_amplitudes( + crystal, + orientation, + all_tilts, + t_grid, + energy_ev, + sg_max, + k_max, + tilt_batch=max(64, Mr * 8), + progress_bar=False, + fast_absorption=fast_absorption, + deform=deform, + ) + inten = inten.reshape(Mt, Mr, t_grid.shape[0], -1).mean(dim=1) # (Mt, T, nb) + centers = _q_to_pixels(g_xy, origin_rc, pixel_size, rotation_ccw_deg, ellipse) + images = render_disks(centers, inten, shape, disk_radius_px, edge_px) + return images, centers, inten + +def _image_cost( + meas: torch.Tensor, sims: torch.Tensor, mask: torch.Tensor, power: float +) -> torch.Tensor: + """Normalized residual of the measured image (ny, nx) against each + simulated one (..., ny, nx). The intensity scale and a constant + background are solved by least squares in the raw domain over the + mask; the residual is then taken between the power-law images so the + weak diffracted disks weigh as they do in the Bragg-vector cost.""" + m = mask.to(torch.float64) + n = m.sum().clamp_min(1) + y = meas * m + x = sims * m + sx = x.sum(dim=(-2, -1)) + sy = y.sum() + sxx = (x * x).sum(dim=(-2, -1)) + sxy = (x * y).sum(dim=(-2, -1)) + den = (n * sxx - sx * sx).clamp_min(1e-12) + a = ((n * sxy - sx * sy) / den).clamp_min(0) + b = ((sy - a * sx) / n).clamp_min(0) + model = (a[..., None, None] * sims + b[..., None, None]).clamp_min(0) ** power + yp = meas.clamp_min(0) ** power + resid = (((yp - model) * m) ** 2).sum(dim=(-2, -1)) + return resid / ((yp * m) ** 2).sum().clamp_min(1e-12) + + +def _image_mask(shape, origin, r_max_px, exclude_direct_px): + yy, xx = np.mgrid[0 : shape[0], 0 : shape[1]] + r = np.hypot(yy - origin[0], xx - origin[1]) + m = np.ones(shape, dtype=bool) if r_max_px is None else r <= r_max_px + if exclude_direct_px is not None and exclude_direct_px > 0: + m &= r > exclude_direct_px + return torch.as_tensor(m) + + +def fit_disk_shape( + dataset, + phase_map, + result: dict, + origins: np.ndarray, + pixel_size: float, + rotation_ccw_deg: float = 0.0, + ellipse=None, + positions=None, + n_positions: int = 20, + radii_px=None, + edges_px=None, + power_intensity: float | None = None, + r_max_px: float | None = None, + exclude_direct_px: float | None = None, + sg_max: float = SG_MAX, + k_max: float | None = None, + fast_absorption: bool = True, + progress_bar: bool = True, +) -> dict: + """Global disk radius and edge width from the best-fit patterns. + + The convergence disk shape is a property of the illumination, not of + the position, so it is fit once: on the `n_positions` positions with + the lowest dynamical cost (or the given `positions`), the rendered + pattern at the refined orientation, thickness and deformation is + compared with the measured image over a grid of (radius, edge), and + the pair minimizing the summed image cost is returned for + refine_dynamical_image() to use. + """ + oms = phase_map.orientation_maps + cands = phase_map.candidates + md = result.get("metadata", {}) + energy_ev = oms[0].energy_ev + power_intensity = float( + resolve(power_intensity, "power_intensity", md, default=POWER_INTENSITY) + ) + tilts = dynamical_tilt_set( + energy_ev, + md.get("precession_deg", 0.0), + md.get("n_precession", 32), + md.get("semiconv_mrad", 0.0), + maped_tilts_deg=md.get("maped_tilts_deg"), + ) + if radii_px is None: + radii_px = np.arange(1.5, 6.01, 0.5) + if edges_px is None: + edges_px = np.array([0.5, 0.75, 1.0, 1.5, 2.0]) + cost = torch.nan_to_num(result["cost"], nan=torch.inf).amin(dim=-1) + if positions is None: + flat = torch.argsort(cost.reshape(-1))[:n_positions] + positions = [ + (int(i) // cost.shape[1], int(i) % cost.shape[1]) + for i in flat + if torch.isfinite(cost.reshape(-1)[i]) + ] + shape = tuple(dataset.shape[-2:]) + if exclude_direct_px is None: + exclude_direct_px = 1.5 * float(np.max(radii_px)) + total = torch.zeros((len(radii_px), len(edges_px)), dtype=torch.float64) + it = tqdm(positions, desc="disk shape") if progress_bar else positions + for rx, ry in it: + f = int(result["candidate"][rx, ry]) + i_om, m = cands[f] + om = oms[i_om] + q = result["quats"][rx, ry, f] + t = float(result["thickness_per_candidate"][rx, ry, f]) + d3 = torch.eye(3, dtype=torch.float64) + d3[:2, :2] = result["deformation"][rx, ry, f] + o = origins[rx, ry] + meas = torch.as_tensor(np.asarray(dataset.array[rx, ry], dtype=float)).clamp_min(0) + mask = _image_mask(shape, o, r_max_px, exclude_direct_px) + _, centers, inten = render_pattern_image( + om.crystal, + q, + [t], + energy_ev, + shape, + o, + pixel_size, + rotation_ccw_deg, + ellipse, + d3, + 1.0, + 1.0, + tilts, + None, + sg_max, + k_max, + fast_absorption, + ) + for i, r in enumerate(radii_px): + for j, e in enumerate(edges_px): + sim = render_disks(centers, inten[0, 0], shape, float(r), float(e)) + total[i, j] += _image_cost(meas, sim, mask, power_intensity) + k = int(total.argmin()) + i, j = k // len(edges_px), k % len(edges_px) return { - "bright_field": bf[0] if T == 1 else bf, - "mrad_per_pixel": 2 * semiconv_mrad / n_pixels, - "thicknesses": master["thicknesses"], + "disk_radius_px": float(radii_px[i]), + "edge_px": float(edges_px[j]), + "cost": total, + "radii_px": np.asarray(radii_px), + "edges_px": np.asarray(edges_px), + "positions": positions, + } + + +def refine_dynamical_image( + dataset, + phase_map, + result: dict, + origins: np.ndarray, + pixel_size: float, + disk_radius_px: float, + edge_px: float, + rotation_ccw_deg: float = 0.0, + ellipse=None, + thickness_half_range_A: float = 100.0, + thickness_step_A: float = 10.0, + tilt_stage=(0.03, 0.01), + power_intensity: float | None = None, + r_max_px: float | None = None, + exclude_direct_px: float | None = None, + mask: np.ndarray | None = None, + sg_max: float = SG_MAX, + k_max: float | None = None, + fast_absorption: bool = True, + update_orientations: bool = True, + progress_bar: bool = True, +) -> dict: + """Final dynamical refinement against the diffraction images. + + Starting from the Bragg-vector solution of refine_dynamical (winning + candidate, orientation, thickness, in-plane deformation), every pixel + of the measured pattern is compared with a rendered pattern: Bloch + intensities averaged over the precession / convergence tilt set, + drawn as disks of the global radius and edge width from + fit_disk_shape(), with a free intensity scale and constant + background. The thickness and the orientation tilt are re-searched + on a local grid (thickness +- thickness_half_range_A, tilt +- the + stage half-range), the deformation and in-plane rotation are kept + from the position fit. The image cost is the residual after the + linear fit, normalized by the image power, so it is comparable + across positions. The direct beam disk is excluded from the cost + (exclude_direct_px, default 1.5 disk radii): it carries most of the + counts, its measured intensity is the least reliable (saturation, + detector response), and a fraction of a percent of model error on it + would outweigh every diffracted disk. Run this when the Bragg-vector + refinement is not accurate enough; it costs one rendered image per + trial (tilt, thickness) on top of the Bloch solves. + + Returns + ------- + dict with 'thickness' (R, C), 'tilt_deg' (R, C, 2) the additional + tilt over the Bragg-vector result, 'quats' (R, C, 4), 'cost' (R, C) + the normalized image residual, and 'metadata'. + """ + from quantem.diffraction.rotations import qmult, quat_from_axis_angle + + oms = phase_map.orientation_maps + cands = phase_map.candidates + md = result.get("metadata", {}) + energy_ev = oms[0].energy_ev + power_intensity = float( + resolve(power_intensity, "power_intensity", md, default=POWER_INTENSITY) + ) + if exclude_direct_px is None: + exclude_direct_px = 1.5 * disk_radius_px + tilts = dynamical_tilt_set( + energy_ev, + md.get("precession_deg", 0.0), + md.get("n_precession", 32), + md.get("semiconv_mrad", 0.0), + maped_tilts_deg=md.get("maped_tilts_deg"), + ) + R, C = result["thickness"].shape + shape = tuple(dataset.shape[-2:]) + half, step = (np.deg2rad(v) for v in tilt_stage) + n = int(round(2 * half / step)) + 1 + tg = torch.linspace(-half, half, n, dtype=torch.float64) + wx_g, wy_g = torch.meshgrid(tg, tg, indexing="ij") + w_grid = torch.stack([wx_g.reshape(-1), wy_g.reshape(-1)], dim=1) + + thickness = torch.full((R, C), torch.nan, dtype=torch.float64) + tilt_out = torch.zeros((R, C, 2), dtype=torch.float64) + quat_out = torch.zeros((R, C, 4), dtype=torch.float64) + quat_out[..., 0] = 1.0 + cost_out = torch.full((R, C), torch.nan, dtype=torch.float64) + + iterator = list(np.ndindex(R, C)) + if mask is not None: + iterator = [(r, c) for r, c in iterator if mask[r, c]] + if progress_bar: + iterator = tqdm(iterator, desc="image refinement") + for rx, ry in iterator: + f = int(result["candidate"][rx, ry]) + if not torch.isfinite(result["cost"][rx, ry, f]): + continue + i_om, m = cands[f] + om = oms[i_om] + q0 = result["quats"][rx, ry, f] + t0 = float(result["thickness_per_candidate"][rx, ry, f]) + d3 = torch.eye(3, dtype=torch.float64) + d3[:2, :2] = result["deformation"][rx, ry, f] + o = origins[rx, ry] + meas = torch.as_tensor(np.asarray(dataset.array[rx, ry], dtype=float)).clamp_min(0) + pmask = _image_mask(shape, o, r_max_px, exclude_direct_px) + t_grid = np.arange( + max(thickness_step_A, t0 - thickness_half_range_A), + t0 + thickness_half_range_A + 1e-6, + thickness_step_A, + ) + images, _, _ = render_pattern_image( + om.crystal, + q0, + t_grid, + energy_ev, + shape, + o, + pixel_size, + rotation_ccw_deg, + ellipse, + d3, + disk_radius_px, + edge_px, + tilts, + w_grid, + sg_max, + k_max, + fast_absorption, + ) + cost = _image_cost(meas, images, pmask, power_intensity) # (M, T) + T = len(t_grid) + flat = int(cost.argmin()) + m_best, t_best = flat // T, flat % T + i_b, j_b = m_best // n, m_best % n + wx, wy = float(w_grid[m_best, 0]), float(w_grid[m_best, 1]) + cost_t = cost[:, t_best].reshape(n, n) + if 0 < i_b < n - 1: + c0, c1, c2 = cost_t[i_b - 1, j_b], cost_t[i_b, j_b], cost_t[i_b + 1, j_b] + den = float(c0 - 2 * c1 + c2) + if den > 1e-12: + wx += 0.5 * float(c0 - c2) / den * step + if 0 < j_b < n - 1: + c0, c1, c2 = cost_t[i_b, j_b - 1], cost_t[i_b, j_b], cost_t[i_b, j_b + 1] + den = float(c0 - 2 * c1 + c2) + if den > 1e-12: + wy += 0.5 * float(c0 - c2) / den * step + q = q0 + ang = float(np.hypot(wx, wy)) + if ang > 1e-12: + axis = torch.tensor([wx / ang, wy / ang, 0.0], dtype=torch.float64) + q = qmult(quat_from_axis_angle(axis, torch.tensor(ang, dtype=torch.float64)), q0) + thickness[rx, ry] = float(t_grid[t_best]) + tilt_out[rx, ry, 0] = wx + tilt_out[rx, ry, 1] = wy + quat_out[rx, ry] = q + cost_out[rx, ry] = cost[m_best, t_best] + if update_orientations: + om.quats[rx, ry, m] = q + + used = dict( + disk_radius_px=float(disk_radius_px), + edge_px=float(edge_px), + thickness_half_range_A=float(thickness_half_range_A), + thickness_step_A=float(thickness_step_A), + tilt_stage=tuple(float(v) for v in tilt_stage), + power_intensity=float(power_intensity), + r_max_px=r_max_px, + exclude_direct_px=float(exclude_direct_px), + sg_max=float(sg_max), + k_max=k_max, + fast_absorption=bool(fast_absorption), + inherited=dict(md), + ) + if hasattr(phase_map, "metadata"): + phase_map.metadata["dynamical_image"] = used + return { + "thickness": thickness, + "tilt_deg": torch.rad2deg(tilt_out), + "quats": quat_out, + "cost": cost_out, + "metadata": used, } diff --git a/src/quantem/diffraction/calibration.py b/src/quantem/diffraction/calibration.py index e54060f60..f72673171 100644 --- a/src/quantem/diffraction/calibration.py +++ b/src/quantem/diffraction/calibration.py @@ -8,10 +8,10 @@ from __future__ import annotations import numpy as np -import torch from quantem.core.datastructures.vector import Vector from quantem.diffraction.crystal import Crystal +from quantem.diffraction.defaults import MIN_NUMBER_PEAKS def _measure_raw_origins(bragg_vectors, search_radius: float) -> np.ndarray: @@ -57,7 +57,10 @@ def plot_origin_fit(bragg_vectors, origins: np.ndarray, search_radius: float = 6 c0 = 0.0 if j == 2 else center sp = max(np.nanstd(resid) * 3, 1e-3) if j == 2 else span im = axs[k, j].imshow( - img, cmap="RdBu_r", vmin=c0 - sp, vmax=c0 + sp, + img, + cmap="RdBu_r", + vmin=c0 - sp, + vmax=c0 + sp, interpolation="nearest", ) axs[k, j].set_title(title, fontsize=10) @@ -135,7 +138,10 @@ def plane(z, ok): c0 = 0.0 if j == 2 else center s = max(np.nanstd(resid) * 3, 1e-3) if j == 2 else span im = axs[k, j].imshow( - img, cmap="RdBu_r", vmin=c0 - s, vmax=c0 + s, + img, + cmap="RdBu_r", + vmin=c0 - s, + vmax=c0 + s, interpolation="nearest", ) axs[k, j].set_title(title, fontsize=10) @@ -253,9 +259,9 @@ def simulated_ring_profile( """1D ring profile of a crystal: Gaussians at |g| weighted by intensity.""" g = crystal.g_len.numpy() w = crystal.struct_factors_int.numpy() * g**bragg_k_power - prof = ( - w[None, :] * np.exp(-((k[:, None] - g[None, :]) ** 2) / (2 * k_broadening**2)) - ).sum(axis=1) + prof = (w[None, :] * np.exp(-((k[:, None] - g[None, :]) ** 2) / (2 * k_broadening**2))).sum( + axis=1 + ) return prof @@ -267,7 +273,7 @@ def calibrate_pixel_size_matching( subsample: int = 8, angle_step_deg: float = 3.0, corr_kernel_size: float = 0.02, - min_number_peaks: int = 6, + min_number_peaks: int = MIN_NUMBER_PEAKS, plot: bool = False, return_scores: bool = False, returnfig: bool = False, @@ -425,15 +431,20 @@ def calibrate_pixel_size( import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(10, 4)) - prof = simulated_ring_profile( - crystal, k * scale, k_broadening / 2, bragg_k_power - ) + prof = simulated_ring_profile(crystal, k * scale, k_broadening / 2, bragg_k_power) ax.fill_between( - k * scale, hist / hist.max(), color="r", alpha=0.75, lw=0, + k * scale, + hist / hist.max(), + color="r", + alpha=0.75, + lw=0, label="measured (scaled)", ) ax.plot( - k * scale, prof / prof.max(), "k-", lw=1.0, + k * scale, + prof / prof.max(), + "k-", + lw=1.0, label=f"{crystal.name} rings", ) ax.set_ylabel("intensity (norm.)") @@ -533,8 +544,7 @@ def measure_scan_rotation( ax.set_xlabel("rotation (degrees)") ax.set_ylabel("field measure") ax.set_title( - "scan rotation = %.1f deg (or %.1f)" - % (rotation_ccw_deg, rotation_ccw_deg + 180) + "scan rotation = %.1f deg (or %.1f)" % (rotation_ccw_deg, rotation_ccw_deg + 180) ) ax.legend() if returnfig: @@ -621,16 +631,13 @@ def cost(e): h0 = histogram(np.zeros(2)) h1 = histogram(ellipse) fig, ax = plt.subplots(figsize=(10, 4)) - ax.fill_between( - k_bins, h0 / h0.max(), color="0.7", lw=0, label="measured" - ) + ax.fill_between(k_bins, h0 / h0.max(), color="0.7", lw=0, label="measured") ax.plot(k_bins, h1 / h1.max(), "r-", lw=1.0, label="ellipse corrected") ax.set_xlabel("scattering vector (1/$\\mathrm{\\AA}$)") ax.set_ylabel("intensity (norm.)") mag = np.hypot(*ellipse) ax.set_title( - "e11 = %.2e, e12 = %.2e (%.2f%% ellipticity)" - % (ellipse[0], ellipse[1], 200 * mag) + "e11 = %.2e, e12 = %.2e (%.2f%% ellipticity)" % (ellipse[0], ellipse[1], 200 * mag) ) ax.legend() if returnfig: @@ -657,10 +664,10 @@ def digit(v: int) -> str: txt = str(abs(v)) return txt + "̅" if v < 0 else txt - h, k, l = (int(round(v)) for v in hkl) + h, k, ll = (int(round(v)) for v in hkl) if hexagonal: - return "(" + digit(h) + digit(k) + digit(-(h + k)) + digit(l) + ")" - return "(" + digit(h) + digit(k) + digit(l) + ")" + return "(" + digit(h) + digit(k) + digit(-(h + k)) + digit(ll) + ")" + return "(" + digit(h) + digit(k) + digit(ll) + ")" def plot_ring_comparison( @@ -710,9 +717,7 @@ def plot_ring_comparison( axs = np.atleast_1d(axs) for ci, (ax, xtl) in enumerate(zip(axs, xtls)): - ax.fill_between( - k, hist / hist.max(), color="r", alpha=0.75, lw=0, label="measured" - ) + ax.fill_between(k, hist / hist.max(), color="r", alpha=0.75, lw=0, label="measured") hexagonal = xtl.laue_group in ("6/m", "6/mmm", "-3", "-3m") g_len = xtl.g_len.numpy() ints = xtl.struct_factors_int.numpy() * g_len**bragg_k_power @@ -724,13 +729,15 @@ def plot_ring_comparison( if k_broadening is not None: prof = simulated_ring_profile(xtl, k, k_broadening, bragg_k_power) - ax.plot( - k, prof / prof.max(), "k-", lw=1.0, label=f"{xtl.name} rings" - ) + ax.plot(k, prof / prof.max(), "k-", lw=1.0, label=f"{xtl.name} rings") else: keep = (uniq > k_min) & (uniq < k_max) ax.vlines( - uniq[keep], 0, shell_int[keep], colors="k", lw=1.0, + uniq[keep], + 0, + shell_int[keep], + colors="k", + lw=1.0, label=f"{xtl.name} rings", ) @@ -751,8 +758,12 @@ def plot_ring_comparison( y = 1.05 + 0.11 * (rows % 2) rows += 1 ax.text( - u, y, _hkl_label(hkl_np[best], hexagonal), - fontsize=8, ha="center", va="bottom", + u, + y, + _hkl_label(hkl_np[best], hexagonal), + fontsize=8, + ha="center", + va="bottom", ) ax.set_ylabel("intensity (norm.)") ax.set_ylim(0, 1.32) @@ -902,8 +913,12 @@ def plot_bragg_rings( order = np.argsort(shell_int)[::-1][:n_rings] for k, u in enumerate(np.sort(uniq[order])): ax.plot( - u * np.sin(th), u * np.cos(th), - ls=styles[ci % 3], color=colors[ci % 3], lw=0.5, alpha=0.6, + u * np.sin(th), + u * np.cos(th), + ls=styles[ci % 3], + color=colors[ci % 3], + lw=0.5, + alpha=0.6, label=f"{xtl.name} rings" if k == 0 else None, ) ax.set_xlabel("$q_c$ (1/$\\mathrm{\\AA}$)") diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py index 9f2fbba6d..8ccbff8d6 100644 --- a/src/quantem/diffraction/crystal.py +++ b/src/quantem/diffraction/crystal.py @@ -28,46 +28,42 @@ from ase.data import chemical_symbols from quantem.core.utils.utils import electron_wavelength_angstrom +from quantem.diffraction.defaults import SIGMA_EXCITATION from quantem.diffraction.rotations import qrotate, symmetry_quaternions -# Zone-axis fundamental wedge corners (Cartesian) for each Laue class, with -# display labels for the IPF legend. Hexagonal / trigonal labels use 4-index -# Miller-Bravais direction symbols. Any Laue class not listed falls back to -# hemisphere sampling, which is always sufficient (all Laue classes contain -# inversion) but redundant. -_SQRT3_2 = np.sqrt(3) / 2 -LAUE_WEDGES: dict[str, list[list[float]]] = { - "m-3m": [[0, 0, 1], [0, 1, 1], [1, 1, 1]], - "m-3": [[0, 0, 1], [1, 0, 0], [1, 1, 1]], - "6/mmm": [[0, 0, 1], [_SQRT3_2, 0.5, 0], [1, 0, 0]], - "6/m": [[0, 0, 1], [1, 0, 0], [0.5, _SQRT3_2, 0]], - "-3m": [[0, 0, 1], [1, 0, 0], [0.5, _SQRT3_2, 0]], - "4/mmm": [[0, 0, 1], [1, 0, 0], [1, 1, 0]], - "4/m": [[0, 0, 1], [1, 0, 0], [0, 1, 0]], - "mmm": [[0, 0, 1], [1, 0, 0], [0, 1, 0]], -} -LAUE_WEDGE_LABELS: dict[str, list[str]] = { - "m-3m": ["[001]", "[011]", "[111]"], - "m-3": ["[001]", "[100]", "[111]"], - "6/mmm": ["[0001]", "[10$\\bar{1}$0]", "[2$\\bar{1}\\bar{1}$0]"], - "6/m": ["[0001]", "[2$\\bar{1}\\bar{1}$0]", "[11$\\bar{2}$0]"], - "-3m": ["[0001]", "[2$\\bar{1}\\bar{1}$0]", "[11$\\bar{2}$0]"], - "4/mmm": ["[001]", "[100]", "[110]"], - "4/m": ["[001]", "[100]", "[010]"], - "mmm": ["[001]", "[100]", "[010]"], -} -# plain-text (unicode combining-overline) forms for terminal printing -_B = "\u0305" # combining overline, applies to the preceding character -LAUE_WEDGE_LABELS_TEXT: dict[str, list[str]] = { - "m-3m": ["[001]", "[011]", "[111]"], - "m-3": ["[001]", "[100]", "[111]"], - "6/mmm": ["[0001]", f"[101{_B}0]", f"[21{_B}1{_B}0]"], - "6/m": ["[0001]", f"[21{_B}1{_B}0]", f"[112{_B}0]"], - "-3m": ["[0001]", f"[21{_B}1{_B}0]", f"[112{_B}0]"], - "4/mmm": ["[001]", "[100]", "[110]"], - "4/m": ["[001]", "[100]", "[010]"], - "mmm": ["[001]", "[100]", "[010]"], -} +# unicode combining overline, applies to the preceding character +_B = "\u0305" + + +def direction_indices( + lat_real: torch.Tensor | np.ndarray, d, max_multiple: int = 12 +) -> np.ndarray | None: + """Smallest integer [uvw] along a Cartesian direction, or None if the + direction is not a lattice direction with indices up to max_multiple.""" + A_T_inv = np.linalg.inv(np.asarray(lat_real, dtype=float).T) + v = A_T_inv @ np.asarray(d, dtype=float) + v = v / np.abs(v).max() + for m in range(1, max_multiple + 1): + w = v * m + if np.allclose(w, np.round(w), atol=2e-3): + ints = np.round(w).astype(int) + g = np.gcd.reduce(np.abs(ints)) + return ints // max(g, 1) + return None + + +def format_direction(uvw, hexagonal: bool = False, mathtext: bool = True) -> str: + """Direction label such as [011] or [10-10], with overlines on negative + indices (mathtext for figures, combining overlines for text).""" + if uvw is None: + return "" + ks = miller_to_miller_bravais(uvw) if hexagonal else np.asarray(uvw) + ks = np.atleast_1d(ks) + if mathtext: + body = "".join(str(k) if k >= 0 else "$\\bar{%d}$" % -k for k in ks) + else: + body = "".join(str(k) if k >= 0 else "%d%s" % (-k, _B) for k in ks) + return "[" + body + "]" def miller_to_miller_bravais(uvw: np.ndarray) -> np.ndarray: @@ -94,26 +90,46 @@ def miller_bravais_to_miller(uvtw: np.ndarray) -> np.ndarray: u' = 2u + v, v' = 2v + u, w' = w (t is redundant: t = -(u + v)). """ uvtw = np.atleast_2d(np.asarray(uvtw, dtype=float)) - out = np.stack( - [2 * uvtw[:, 0] + uvtw[:, 1], 2 * uvtw[:, 1] + uvtw[:, 0], uvtw[:, 3]], axis=1 - ) + out = np.stack([2 * uvtw[:, 0] + uvtw[:, 1], 2 * uvtw[:, 1] + uvtw[:, 0], uvtw[:, 3]], axis=1) gcd = np.gcd.reduce(np.abs(np.round(out)).astype(int), axis=1) gcd[gcd == 0] = 1 return (out / gcd[:, None]).astype(int).squeeze() + # point group -> Laue class _LAUE_CLASS = { - "1": "-1", "-1": "-1", - "2": "2/m", "m": "2/m", "2/m": "2/m", - "222": "mmm", "mm2": "mmm", "mmm": "mmm", - "4": "4/m", "-4": "4/m", "4/m": "4/m", - "422": "4/mmm", "4mm": "4/mmm", "-42m": "4/mmm", "4/mmm": "4/mmm", - "3": "-3", "-3": "-3", - "32": "-3m", "3m": "-3m", "-3m": "-3m", - "6": "6/m", "-6": "6/m", "6/m": "6/m", - "622": "6/mmm", "6mm": "6/mmm", "-6m2": "6/mmm", "6/mmm": "6/mmm", - "23": "m-3", "m-3": "m-3", - "432": "m-3m", "-43m": "m-3m", "m-3m": "m-3m", + "1": "-1", + "-1": "-1", + "2": "2/m", + "m": "2/m", + "2/m": "2/m", + "222": "mmm", + "mm2": "mmm", + "mmm": "mmm", + "4": "4/m", + "-4": "4/m", + "4/m": "4/m", + "422": "4/mmm", + "4mm": "4/mmm", + "-42m": "4/mmm", + "4/mmm": "4/mmm", + "3": "-3", + "-3": "-3", + "32": "-3m", + "3m": "-3m", + "-3m": "-3m", + "6": "6/m", + "-6": "6/m", + "6/m": "6/m", + "622": "6/mmm", + "6mm": "6/mmm", + "-6m2": "6/mmm", + "6/mmm": "6/mmm", + "23": "m-3", + "m-3": "m-3", + "432": "m-3m", + "-43m": "m-3m", + "m-3m": "m-3m", } @@ -173,17 +189,30 @@ def __init__( atoms: Atoms, name: str | None = None, symprec: float = 1e-4, - pseudo_symmetry_tol: float | None = None, + pseudo_symmetry_tol: float | None = 0.1, verbose: bool = True, ): + """ + Parameters + ---------- + symprec : float, default=1e-4 + spglib tolerance (Angstroms) for the cell's own symmetry. + pseudo_symmetry_tol : float | None, default=0.1 + Tolerance (Angstroms) at which the symmetry is re-detected for + orientation matching. Cells within this distance of a higher + symmetry (a few percent of strain on a 5 Angstrom cell) are + matched with the parent group, so variants no experiment can + separate are never sampled as distinct orientations. The + library builders warn when this differs from the cell's own + symmetry; pass None to match with the exact symmetry. + """ self.atoms = atoms self.name = name if name is not None else atoms.get_chemical_formula() self._pseudo_symmetry_tol = pseudo_symmetry_tol + self._wedge_cache: torch.Tensor | None | str = "unset" self.lat_real = torch.as_tensor(atoms.cell[:], dtype=torch.float64) - self.positions_frac = torch.as_tensor( - atoms.get_scaled_positions(), dtype=torch.float64 - ) + self.positions_frac = torch.as_tensor(atoms.get_scaled_positions(), dtype=torch.float64) self.numbers = torch.as_tensor(atoms.numbers, dtype=torch.long) occupancy = atoms.arrays.get("occupancy", np.ones(len(atoms))) self.occupancy = torch.as_tensor(np.asarray(occupancy, dtype=float)) @@ -245,8 +274,13 @@ def _setup_symmetry(self, symprec: float, pseudo_symmetry_tol: float | None) -> self.laue_group: str = _LAUE_CLASS.get(pg, "-1") self.sym_quats = symmetry_quaternions(dataset.rotations, self.lat_real.numpy()) + ds_pseudo = None if pseudo_symmetry_tol is not None and pseudo_symmetry_tol > symprec: - ds_pseudo = spglib.get_symmetry_dataset(cell, symprec=pseudo_symmetry_tol) + try: + ds_pseudo = spglib.get_symmetry_dataset(cell, symprec=pseudo_symmetry_tol) + except Exception: + ds_pseudo = None + if ds_pseudo is not None: pg_pseudo = spglib.get_pointgroup(ds_pseudo.rotations)[0].strip() self.pointgroup_matching: str = pg_pseudo self.laue_group_matching: str = _LAUE_CLASS.get(pg_pseudo, "-1") @@ -261,18 +295,55 @@ def _setup_symmetry(self, symprec: float, pseudo_symmetry_tol: float | None) -> def zone_axis_wedge(self) -> torch.Tensor | None: """Fundamental zone-axis wedge corners (3, 3) Cartesian, or None. - None means the Laue class has no simple 3-corner wedge and the - orientation plan should sample the full hemisphere. + Built from the symmetry operations actually used for matching (the + pseudo-symmetry group when one was found), so the wedge is right + for every crystal setting. None means the Laue class (-1 or 2/m) + has no 3-corner wedge and libraries sample the full hemisphere. """ - corners = LAUE_WEDGES.get(self.laue_group) + if isinstance(self._wedge_cache, str): + from quantem.diffraction.rotations import fundamental_zone_axis_wedge + + self._wedge_cache = fundamental_zone_axis_wedge(self.sym_quats_matching) + return self._wedge_cache + + @property + def hexagonal_matching(self) -> bool: + """Whether the matching Laue class uses 4-index direction symbols.""" + return self.laue_group_matching in ("6/m", "6/mmm", "-3", "-3m") + + def zone_axis_wedge_labels(self, mathtext: bool = True) -> list[str] | None: + """Direction labels of the wedge corners (4-index for hexagonal and + trigonal crystals), indexed from the corner directions themselves.""" + corners = self.zone_axis_wedge() if corners is None: return None - c = torch.tensor(corners, dtype=torch.float64) - return c / torch.linalg.norm(c, dim=-1, keepdim=True) + return [ + format_direction( + direction_indices(self.lat_real, c.numpy()), + hexagonal=self.hexagonal_matching, + mathtext=mathtext, + ) + for c in corners + ] - def zone_axis_wedge_labels(self) -> list[str] | None: - """Direction labels of the wedge corners (4-index for hex/trigonal).""" - return LAUE_WEDGE_LABELS.get(self.laue_group) + def matching_symmetry_warning(self) -> str | None: + """Message when the matching (pseudo) symmetry differs from the + cell's own symmetry, or None when they agree.""" + if self.pointgroup_matching == self.pointgroup: + return None + n_extra = self.sym_quats_matching.shape[0] // max(self.sym_quats.shape[0], 1) + return ( + f"{self.name}: orientation libraries are built with the " + f"pseudo-symmetry point group {self.pointgroup_matching} (Laue " + f"class {self.laue_group_matching}, found at pseudo_symmetry_tol = " + f"{self._pseudo_symmetry_tol:g} A), while the cell's own symmetry " + f"is {self.pointgroup} (Laue class {self.laue_group}). Orientations " + f"related by the extra operations give the same library entry, so " + f"the {n_extra} variants they generate are reported as one and the " + f"distortion between them is not resolved. To match with the exact " + f"symmetry, build the Crystal with pseudo_symmetry_tol=None (or a " + f"tolerance below the distortion)." + ) def symmetry_summary(self) -> str: """Human-readable symmetry report, including any pseudo-symmetry.""" @@ -294,22 +365,20 @@ def symmetry_summary(self) -> str: ] elif self._pseudo_symmetry_tol is not None: lines += [ - " pseudo-symmetry none found at tol = " - f"{self._pseudo_symmetry_tol:g} A", + f" pseudo-symmetry none found at tol = {self._pseudo_symmetry_tol:g} A", ] else: lines += [" pseudo-symmetry not checked (set pseudo_symmetry_tol)"] # matching line reflects the symmetry actually used, after any # pseudo-symmetry reduction - labels = LAUE_WEDGE_LABELS_TEXT.get(self.laue_group_matching) + labels = self.zone_axis_wedge_labels(mathtext=False) wedge_txt = ( f"zone axis wedge {labels[0]}, {labels[1]}, {labels[2]}" if labels is not None else "full hemisphere" ) lines += [ - f" matching {self.sym_quats_matching.shape[0]} proper " - f"rotations, {wedge_txt}" + f" matching {self.sym_quats_matching.shape[0]} proper rotations, {wedge_txt}" ] return "\n".join(lines) @@ -459,7 +528,7 @@ def generate_pattern( self, orientation: torch.Tensor, energy_ev: float = 300e3, - sigma_excitation: float = 0.02, + sigma_excitation: float = SIGMA_EXCITATION, tol_excitation_mult: float = 3.0, k_max: float | None = None, ) -> dict[str, torch.Tensor]: diff --git a/src/quantem/diffraction/defaults.py b/src/quantem/diffraction/defaults.py new file mode 100644 index 000000000..3c14197ee --- /dev/null +++ b/src/quantem/diffraction/defaults.py @@ -0,0 +1,45 @@ +"""Shared defaults of the matching, phase and refinement chain. + +Every function takes these as keyword arguments, so they can be changed per +call; keeping one value for each across the chain makes the matching, the +phase fit, the strain and the dynamical refinement compare the same peaks +in the same way. +""" + +# peak pairing distance between simulated and measured peaks, and the width +# of the polar correlation kernel (1/Angstroms) +PAIR_DISTANCE = 0.05 + +# intensities are compared as I ** POWER_INTENSITY; 0.25 flattens the +# dynamic range so weak reflections count, 0 compares positions only +POWER_INTENSITY = 0.25 + +# excitation-error envelope of the kinematical patterns (1/Angstroms); +# about twice the physical width, so orientations halfway between library +# zones keep their intensities +SIGMA_EXCITATION = 0.04 + +# excitation-error cutoff of the Bloch wave beam list (1/Angstroms) +SG_MAX = 0.1 + +# simulated reflections weaker than this fraction of the strongest one are +# treated as unobservable and do not count against a candidate +MIN_SIM_INTENSITY_REL = 0.02 + +# detected peaks (including the direct beam) a pattern needs to be matched +# or refined, and paired reflections a least-squares refinement needs +MIN_NUMBER_PEAKS = 5 +MIN_PAIRS = 4 + + +def resolve(value, key: str, *sources: dict | None, default=None): + """First non-None of: the explicit `value`, `key` in each metadata + source (dicts, searched in order, None sources skipped), the default. + The refinement stages call this so a parameter left as None inherits + the value the previous stage used.""" + if value is not None: + return value + for src in sources: + if src is not None and src.get(key) is not None: + return src[key] + return default diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index 76eb4cb2f..892484eb5 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -22,6 +22,8 @@ from __future__ import annotations +import warnings + import numpy as np import torch from tqdm import tqdm @@ -30,6 +32,14 @@ from quantem.core.io.serialize import AutoSerialize from quantem.core.utils.utils import electron_wavelength_angstrom from quantem.diffraction.crystal import Crystal +from quantem.diffraction.defaults import ( + MIN_NUMBER_PEAKS, + MIN_PAIRS, + PAIR_DISTANCE, + POWER_INTENSITY, + SIGMA_EXCITATION, + resolve, +) from quantem.diffraction.rotations import ( misorientation_angle_deg, qconj, @@ -39,6 +49,7 @@ quat_from_axis_angle, quat_from_zone_axis, sample_zone_axes, + symmetry_reduced_zone_angles, ) @@ -81,6 +92,12 @@ def __init__( self.crystal = crystal self.energy_ev = float(energy_ev) self.wavelength = electron_wavelength_angstrom(energy_ev) + # processing hyperparameters of every stage, recorded as they run; + # later stages inherit from these when an argument is left as None + self.metadata: dict = { + "energy_ev": self.energy_ev, + "peaks": dict(getattr(peaks, "metadata", {}) or {}), + } # plan state self.zone_axes: torch.Tensor | None = None @@ -101,6 +118,8 @@ def from_vectors( peaks: Vector, crystal: Crystal, energy_ev: float = 300e3, + precession_deg: float = 0.0, + semiconv_mrad: float = 0.0, ) -> "OrientationMap": """Create from detected Bragg peaks. @@ -111,12 +130,19 @@ def from_vectors( ('qx', 'qy', 'intensity') in calibrated 1/Angstrom units. crystal : Crystal Candidate crystal with structure factors already calculated. + precession_deg, semiconv_mrad : float + Precession semi-angle and convergence semiangle of the + experiment, recorded for the dynamical refinements (which + average the intensities over them) and inherited by them. energy_ev : float, default=300e3 Beam energy in eV. """ if crystal.g_vec is None: raise RuntimeError("Run crystal.calculate_structure_factors() first.") - return cls(peaks, crystal, energy_ev, _token=cls._token) + om = cls(peaks, crystal, energy_ev, _token=cls._token) + om.metadata["precession_deg"] = float(precession_deg) + om.metadata["semiconv_mrad"] = float(semiconv_mrad) + return om # ------------------------------------------------------------------ # orientation plan @@ -124,12 +150,12 @@ def from_vectors( def build_plan( self, - angle_step_zone_axis_deg: float = 2.0, - angle_step_in_plane_deg: float = 2.0, - corr_kernel_size: float = 0.05, - sigma_excitation: float = 0.04, + angle_step_zone_axis_deg: float = 1.0, + angle_step_in_plane_deg: float = 5.0, + corr_kernel_size: float = PAIR_DISTANCE, + sigma_excitation: float = SIGMA_EXCITATION, power_radial: float = 1.0, - power_intensity: float = 0.25, + power_intensity: float = POWER_INTENSITY, tol_shell_distance: float = 0.01, detector_q_max: float | tuple[float, float] | str | None = "auto", device: str | torch.device = "cpu", @@ -139,11 +165,17 @@ def build_plan( Parameters ---------- - angle_step_zone_axis_deg : float, default=2.0 - Angular step between sampled zone axes. - angle_step_in_plane_deg : float, default=2.0 + angle_step_zone_axis_deg : float, default=1.0 + Angular step between sampled zone axes. The zone axis is the + coordinate the correlation search cannot refine continuously + (only by the neighbor-weighted centroid), so it is sampled + finely; the wedge sampling is isotropic at this step. + angle_step_in_plane_deg : float, default=5.0 Angular step of the in-plane (gamma) axis; the number of gamma - samples is round(360 / step). + samples is round(360 / step). The in-plane angle is refined + continuously (parabolic sub-bin interpolation, then least + squares on the paired peaks in refine_orientations), so a + coarse step costs little accuracy and keeps the library small. corr_kernel_size : float, default=0.05 Correlation kernel size delta (1/Angstroms): azimuthal extent of each reference peak and radial tolerance for shell assignment. @@ -170,19 +202,29 @@ def build_plan( detector-to-scan rotation recorded on the peaks). None disables the correction. device : str | torch.device, default="cpu" - Device for the library and the correlation compute. + Device for the library and the correlation compute. On Apple + silicon 'mps' runs the correlation in float32 (about 1.5x faster + than the CPU); the refinements that follow stay on the CPU. verbose : bool, default=True Print the symmetry actually used for matching (including any pseudo-symmetry reduction) and the plan size. """ crystal = self.crystal self.device = torch.device(device) + # MPS has no float64: the correlation runs in float32 there (the + # cosine similarities are insensitive to it); results are returned + # in float64 either way + self.dtype = torch.float32 if self.device.type == "mps" else torch.float64 + self.cdtype = torch.complex64 if self.dtype == torch.float32 else torch.complex128 self.corr_kernel_size = float(corr_kernel_size) self.sigma_excitation = float(sigma_excitation) self.power_radial = float(power_radial) self.power_intensity = float(power_intensity) # zone axis sampling over the matching (pseudo-symmetry-reduced) wedge + msg = crystal.matching_symmetry_warning() + if msg is not None: + warnings.warn(msg, stacklevel=2) wedge = crystal.zone_axis_wedge() if wedge is None: n_zones = int(np.ceil(2 * np.pi / np.deg2rad(angle_step_zone_axis_deg) ** 2)) @@ -203,9 +245,7 @@ def build_plan( Rs = quat_to_matrix(crystal.sym_quats_matching) images = torch.einsum("sij,zj->szi", Rs, za) images = torch.cat([images, -images], dim=0).reshape(-1, 3) # (S2*Z, 3) - img_zone = torch.arange(za.shape[0]).repeat( - 2 * crystal.sym_quats_matching.shape[0] - ) + img_zone = torch.arange(za.shape[0]).repeat(2 * crystal.sym_quats_matching.shape[0]) # deduplicate coincident image positions (keep one per position/zone) key = torch.cat( [torch.round(images / 1e-6) * 1e-6, img_zone[:, None].to(images.dtype)], @@ -238,13 +278,11 @@ def build_plan( radii = torch.unique(torch.round(g_len / tol_shell_distance) * tol_shell_distance) self.shell_radii = radii self.num_gamma = int(round(360 / angle_step_in_plane_deg)) - self.gamma = torch.linspace( - 0, 2 * np.pi, self.num_gamma + 1, dtype=torch.float64 - )[:-1] + self.gamma = torch.linspace(0, 2 * np.pi, self.num_gamma + 1, dtype=torch.float64)[:-1] plan = self._build_reference(self.zone_quats) # store conj(fft) along gamma so matching is a single complex matmul - self.plan_fft = torch.conj(torch.fft.fft(plan, dim=-1)).to(self.device) + self.plan_fft = torch.conj(torch.fft.fft(plan, dim=-1)).to(self.cdtype).to(self.device) # square-detector aperture correction: the masked template norm at # every in-plane shift is the circular correlation of the squared @@ -258,9 +296,7 @@ def build_plan( detector_q_max = None else: th_b = np.deg2rad(-rot_deg) - rb = np.array( - [[np.cos(th_b), -np.sin(th_b)], [np.sin(th_b), np.cos(th_b)]] - ) + rb = np.array([[np.cos(th_b), -np.sin(th_b)], [np.sin(th_b), np.cos(th_b)]]) det_rc = flat[:, :2] @ rb.T detector_q_max = ( float(np.abs(det_rc[:, 0]).max()) + self.corr_kernel_size, @@ -274,8 +310,7 @@ def build_plan( r = self.shell_radii[:, None] g = self.gamma[None, :] - np.deg2rad(rot_deg) mask = ( - (torch.abs(r * torch.cos(g)) <= qx_max) - & (torch.abs(r * torch.sin(g)) <= qy_max) + (torch.abs(r * torch.cos(g)) <= qx_max) & (torch.abs(r * torch.sin(g)) <= qy_max) ).to(torch.float64) self.detector_mask = mask # (S, G) plan_sq_fft = torch.conj(torch.fft.fft(plan**2, dim=-1)) @@ -290,14 +325,29 @@ def build_plan( # fraction of template weight on the detector; used to suppress # zones that are mostly unmeasurable at a given rotation full = (plan**2).sum(dim=(1, 2))[:, None].clamp_min(1e-12) - self.plan_norm_shift = torch.stack( - [torch.sqrt(n2), torch.sqrt(n2_m)] - ).to(self.device) # (2, Z, G) - self.plan_frac_shift = torch.stack([n2 / full, n2_m / full]).to(self.device) + self.plan_norm_shift = ( + torch.stack([torch.sqrt(n2), torch.sqrt(n2_m)]).to(self.dtype).to(self.device) + ) # (2, Z, G) + self.plan_frac_shift = ( + torch.stack([n2 / full, n2_m / full]).to(self.dtype).to(self.device) + ) else: self.detector_mask = None self.plan_norm_shift = None self.plan_frac_shift = None + self.metadata["plan"] = dict( + angle_step_zone_axis_deg=float(angle_step_zone_axis_deg), + angle_step_in_plane_deg=float(angle_step_in_plane_deg), + corr_kernel_size=self.corr_kernel_size, + pair_distance=self.corr_kernel_size, + sigma_excitation=self.sigma_excitation, + power_radial=self.power_radial, + power_intensity=self.power_intensity, + tol_shell_distance=float(tol_shell_distance), + detector_q_max=None + if detector_q_max is None + else tuple(np.atleast_1d(detector_q_max).tolist()), + ) if verbose: print(crystal.symmetry_summary()) print( @@ -317,8 +367,9 @@ def _deposit_polar( qphi: torch.Tensor, amp: torch.Tensor, out: torch.Tensor, + image: torch.Tensor | None = None, ) -> torch.Tensor: - """Deposit peaks into a polar image with the shared correlation kernel. + """Deposit peaks into polar images with the shared correlation kernel. Every peak spreads as a Gaussian of width delta in both the radial direction (across shells) and arc length (along gamma). The library @@ -330,24 +381,34 @@ def _deposit_polar( qr, qphi, amp : torch.Tensor Peak radii, azimuths, amplitudes, flat (K,). out : torch.Tensor - (S, G) accumulator, modified in place. + (S, G) accumulator, or (N, S, G) when `image` is given; modified + in place. + image : torch.Tensor | None + (K,) image index of every peak, so a whole batch of patterns + (or a whole library) is deposited in one call. """ radii = self.shell_radii.to(qr.dtype) delta = self.corr_kernel_size - G = self.num_gamma + S = radii.shape[0] dr = qr[:, None] - radii[None, :] # (K, S) - k_idx, s_idx = torch.nonzero(dr.abs() < 3 * delta, as_tuple=True) - if k_idx.numel() == 0: + k_all, s_all = torch.nonzero(dr.abs() < 3 * delta, as_tuple=True) + if k_all.numel() == 0: return out - w_r = torch.exp(-(dr[k_idx, s_idx] ** 2) / (2 * delta**2)) * amp[k_idx] - gamma = self.gamma.to(qr.dtype) - dg = qphi[k_idx, None] - gamma[None, :] - dg = (dg + np.pi) % (2 * np.pi) - np.pi - arc = dg * qr[k_idx, None] - w = w_r[:, None] * torch.exp(-(arc**2) / (2 * delta**2)) - out.index_add_(0, s_idx, w) + flat = out.view(-1, out.shape[-1]) + # chunked so the (entries, G) weight array stays a few tens of MB + chunk = max(1, 4_000_000 // gamma.shape[0]) + for c0 in range(0, k_all.numel(), chunk): + k_idx = k_all[c0 : c0 + chunk] + s_idx = s_all[c0 : c0 + chunk] + w_r = torch.exp(-(dr[k_idx, s_idx] ** 2) / (2 * delta**2)) * amp[k_idx] + dg = qphi[k_idx, None] - gamma[None, :] + dg = (dg + np.pi) % (2 * np.pi) - np.pi + arc = dg * qr[k_idx, None] + w = w_r[:, None] * torch.exp(-(arc**2) / (2 * delta**2)) + rows = s_idx if image is None else image[k_idx] * S + s_idx + flat.index_add_(0, rows, w) return out def _build_reference(self, zone_quats: torch.Tensor) -> torch.Tensor: @@ -365,8 +426,7 @@ def _build_reference(self, zone_quats: torch.Tensor) -> torch.Tensor: amp = amp * (s_g.abs() < delta * 4) weight = ( - crystal.g_len**self.power_radial - * crystal.struct_factors_int**self.power_intensity + crystal.g_len**self.power_radial * crystal.struct_factors_int**self.power_intensity ) vals = amp * weight[None, :] # (Z, N) qr = torch.hypot(gr[..., 0], gr[..., 1]) @@ -374,9 +434,10 @@ def _build_reference(self, zone_quats: torch.Tensor) -> torch.Tensor: Z = zone_quats.shape[0] plan = torch.zeros((Z, self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64) - for z in range(Z): - keep = vals[z] > 1e-8 - self._deposit_polar(qr[z, keep], qphi[z, keep], vals[z, keep], plan[z]) + z_idx, n_idx = torch.nonzero(vals > 1e-8, as_tuple=True) + self._deposit_polar( + qr[z_idx, n_idx], qphi[z_idx, n_idx], vals[z_idx, n_idx], plan, image=z_idx + ) norm = torch.linalg.norm(plan.reshape(Z, -1), dim=1).clamp_min(1e-12) return plan / norm[:, None, None] @@ -392,10 +453,26 @@ def _polar_image( qr = torch.hypot(qx, qy) qphi = torch.atan2(qy, qx) amp = intensity.clamp_min(0) ** (self.power_intensity) * qr**self.power_radial + out = torch.zeros((self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64) + return self._deposit_polar(qr, qphi, amp, out) + + def _polar_images(self, arrays: list[np.ndarray], ix: list[int]) -> torch.Tensor: + """Sparse polar images (B, S, G) of a batch of measured patterns, + deposited in one call.""" + data = np.concatenate([a[:, ix] for a in arrays], axis=0) + image = torch.repeat_interleave( + torch.arange(len(arrays)), torch.tensor([a.shape[0] for a in arrays]) + ) + qx = torch.as_tensor(data[:, 0], dtype=torch.float64) + qy = torch.as_tensor(data[:, 1], dtype=torch.float64) + intensity = torch.as_tensor(data[:, 2], dtype=torch.float64) + qr = torch.hypot(qx, qy) + qphi = torch.atan2(qy, qx) + amp = intensity.clamp_min(0) ** (self.power_intensity) * qr**self.power_radial out = torch.zeros( - (self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64 + (len(arrays), self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64 ) - return self._deposit_polar(qr, qphi, amp, out) + return self._deposit_polar(qr, qphi, amp, out, image=image) # ------------------------------------------------------------------ # matching @@ -405,7 +482,7 @@ def match_orientations( self, num_matches: int = 1, include_mirror: bool = True, - min_number_peaks: int = 3, + min_number_peaks: int | None = None, min_angle_between_matches_deg: float = 15.0, subpixel_gamma: bool = True, subpixel_zone: bool = True, @@ -437,8 +514,9 @@ def match_orientations( Also correlate against the in-plane mirrored pattern, testing inversion-related (opposite hemisphere) zone axes at no library cost. Exact in the flat-Ewald / Friedel limit. - min_number_peaks : int, default=3 - Skip positions with fewer detected peaks. + min_number_peaks : int | None + Skip positions with fewer detected peaks (including the direct + beam); defaults to MIN_NUMBER_PEAKS (5). min_angle_between_matches_deg : float, default=15.0 Exclusion radius (degrees, zone-axis distance) around earlier matches, both for later matches and for the second-best score @@ -455,6 +533,16 @@ def match_orientations( """ if self.plan_fft is None: raise RuntimeError("Run build_plan() first.") + min_number_peaks = resolve(min_number_peaks, "min_number_peaks", default=MIN_NUMBER_PEAKS) + self.metadata["match"] = dict( + num_matches=int(num_matches), + include_mirror=bool(include_mirror), + min_number_peaks=int(min_number_peaks), + min_angle_between_matches_deg=float(min_angle_between_matches_deg), + subpixel_gamma=bool(subpixel_gamma), + subpixel_zone=bool(subpixel_zone), + min_detector_fraction=float(min_detector_fraction), + ) peaks = self.peaks shape = peaks.shape R, C = shape[0], shape[1] @@ -472,9 +560,16 @@ def match_orientations( fields = peaks.fields ix = [fields.index(f) for f in ("qx", "qy", "intensity")] - # zone-pair angular distances, for the exclusion ball around matches - za = self.zone_axes.to(device) - zone_ang = torch.rad2deg(torch.acos((za @ za.T).clamp(-1, 1))) # (Z, Z) + # zone-pair angular distances for the exclusion ball around matches, + # minimized over the matching symmetry: a redundant library + # (hemisphere fallback, pseudo-symmetry) holds symmetry copies of + # every zone, and those must not count as the "second best" match + dtype = getattr(self, "dtype", torch.float64) + zone_ang = ( + symmetry_reduced_zone_angles(self.zone_axes, self.crystal.sym_quats_matching) + .to(dtype) + .to(device) + ) # (Z, Z) plan_fft = self.plan_fft # (Z, S, G) complex valid_rc = [ @@ -482,26 +577,22 @@ def match_orientations( for rx, ry in np.ndindex(R, C) if peaks[rx, ry].array.shape[0] >= min_number_peaks ] - batches = [ - valid_rc[i : i + batch_size] for i in range(0, len(valid_rc), batch_size) - ] + batches = [valid_rc[i : i + batch_size] for i in range(0, len(valid_rc), batch_size)] if progress_bar: batches = tqdm(batches, desc=f"matching {self.crystal.name}") gamma_grid = self.gamma for batch in batches: - ims = [] - for rx, ry in batch: - data = peaks[rx, ry].array - qx = torch.as_tensor(data[:, ix[0]], dtype=torch.float64) - qy = torch.as_tensor(data[:, ix[1]], dtype=torch.float64) - ii = torch.as_tensor(data[:, ix[2]], dtype=torch.float64) - ims.append(self._polar_image(qx, qy, ii)) - im_stack = torch.stack(ims).to(device) - norms = torch.linalg.norm(im_stack.reshape(len(ims), -1), dim=1).clamp_min( - 1e-12 + im_stack = ( + self._polar_images([peaks[rx, ry].array for rx, ry in batch], ix) + .to(dtype) + .to(device) ) - im_fft = torch.fft.fft(im_stack, dim=-1) # (B, S, G) + norms = torch.linalg.norm(im_stack.reshape(len(batch), -1), dim=1).clamp_min(1e-12) + with warnings.catch_warnings(): + # torch's MPS FFT emits an internal out-tensor resize notice + warnings.simplefilter("ignore", UserWarning) + im_fft = torch.fft.fft(im_stack, dim=-1) # (B, S, G) # contract shells: (B, Z, G) per channel cc = torch.einsum("zsg,bsg->bzg", plan_fft, im_fft) @@ -533,7 +624,9 @@ def match_orientations( # zone index of previous match not stored; use angle # to previous zone axis zprev = self._zprev[b][mm] - corr[b, :, zone_ang[zprev] < min_angle_between_matches_deg, :] = -torch.inf + corr[ + b, :, zone_ang[zprev] < min_angle_between_matches_deg, : + ] = -torch.inf flat_idx = corr.reshape(B, -1).argmax(dim=1) n_ch = corr.shape[1] ch_i = flat_idx // (Z * G) @@ -553,7 +646,7 @@ def match_orientations( (c2 - c0) / denom, torch.zeros_like(denom), ) * (2 * np.pi / G) - gamma = gamma + dg.double().cpu() + gamma = gamma + dg.cpu().double() is_mirror = ch_i.cpu() == 1 q_zone = self.zone_quats[z_i.cpu()] @@ -564,7 +657,7 @@ def match_orientations( # (see build_plan; images across the wedge boundary keep # the centroid unbiased for boundary zones) b_ar = torch.arange(B, device=corr.device) - corr_z = corr[b_ar, ch_i].amax(dim=-1).cpu() # (B, Z) + corr_z = corr[b_ar, ch_i].amax(dim=-1).cpu().double() # (B, Z) zi_cpu = z_i.cpu() n_idx = self.zone_nbr_idx[zi_cpu] # (B, K) n_pos = self.zone_nbr_pos[zi_cpu] # (B, K, 3) @@ -573,9 +666,9 @@ def match_orientations( c_floor = corr_z.gather(1, zi_cpu[:, None]) * 0.7 wgt = (c_n - c_floor).clamp_min(0) * n_ok za_ref = (wgt[:, :, None] * n_pos).sum(dim=1) - za_ref = za_ref / torch.linalg.norm( - za_ref, dim=-1, keepdim=True - ).clamp_min(1e-12) + za_ref = za_ref / torch.linalg.norm(za_ref, dim=-1, keepdim=True).clamp_min( + 1e-12 + ) za_old = self.zone_axes[z_i.cpu()] axis = torch.cross(za_ref, za_old, dim=-1) sin_t = torch.linalg.norm(axis, dim=-1) @@ -591,21 +684,17 @@ def match_orientations( q_zone = qmult(q_zone, dq) q_flip = torch.tensor([0.0, 1.0, 0.0, 0.0], dtype=torch.float64) - q_zone = torch.where( - is_mirror[:, None], qmult(q_flip, q_zone), q_zone - ) + q_zone = torch.where(is_mirror[:, None], qmult(q_flip, q_zone), q_zone) gamma = torch.where(is_mirror, -gamma - np.pi, gamma) half = gamma / 2 zeros = torch.zeros_like(half) - q_spin = torch.stack( - (torch.cos(half), zeros, zeros, torch.sin(half)), dim=-1 - ) + q_spin = torch.stack((torch.cos(half), zeros, zeros, torch.sin(half)), dim=-1) q = qmult(q_spin, q_zone) for b, (rx, ry) in enumerate(batch): if torch.isfinite(c_val[b]): quats[rx, ry, m] = q[b] - corr_out[rx, ry, m] = c_val[b].double().cpu() + corr_out[rx, ry, m] = c_val[b].cpu().double() mirror_out[rx, ry, m] = bool(is_mirror[b]) if m == 0: @@ -619,7 +708,7 @@ def match_orientations( ) for b, (rx, ry) in enumerate(batch): if torch.isfinite(c2[b]): - corr_second[rx, ry] = c2[b].double().cpu() + corr_second[rx, ry] = c2[b].cpu().double() if M > 1: if m == 0: @@ -643,7 +732,7 @@ def refine_orientations( num_iterations: int = 5, pair_distance: float | None = None, sigma_excitation: float | None = None, - min_pairs: int = 3, + min_pairs: int | None = None, refine_tilt: bool = False, refine_zone: bool = True, zone_search_deg: float = 1.5, @@ -682,8 +771,8 @@ def refine_orientations( sigma_excitation : float | None Excitation error envelope used for simulation; defaults to the plan's value. - min_pairs : int, default=3 - Skip positions with fewer paired peaks. + min_pairs : int | None + Skip positions with fewer paired peaks; defaults to MIN_PAIRS (4). refine_tilt : bool, default=False Also solve the two tilt components from peak positions. Only meaningful for noise-free simulated data. @@ -715,9 +804,24 @@ def refine_orientations( Minimum-neighbor misorientation that triggers the rescue pass. """ assert self.quats is not None - delta = pair_distance if pair_distance is not None else self.corr_kernel_size - sigma = ( - sigma_excitation if sigma_excitation is not None else self.sigma_excitation + plan_md = self.metadata.get("plan") + delta = resolve(pair_distance, "pair_distance", plan_md, default=self.corr_kernel_size) + sigma = resolve( + sigma_excitation, "sigma_excitation", plan_md, default=self.sigma_excitation + ) + min_pairs = resolve(min_pairs, "min_pairs", default=MIN_PAIRS) + self.metadata["refine"] = dict( + num_iterations=int(num_iterations), + pair_distance=float(delta), + sigma_excitation=float(sigma), + min_pairs=int(min_pairs), + refine_tilt=bool(refine_tilt), + refine_zone=bool(refine_zone), + zone_search_deg=float(zone_search_deg), + zone_max_total_deg=zone_max_total_deg, + sigma_envelope=sigma_envelope, + neighbor_rescue=bool(neighbor_rescue), + rescue_threshold_deg=float(rescue_threshold_deg), ) peaks = self.peaks R, C, M = self.quats.shape[:3] @@ -732,9 +836,7 @@ def refine_orientations( ) eye3 = torch.eye(3, dtype=torch.float64) tilt_cap = np.deg2rad( - zone_max_total_deg - if zone_max_total_deg is not None - else 0.375 * self.zone_step_deg + zone_max_total_deg if zone_max_total_deg is not None else 0.375 * self.zone_step_deg ) def refine_single(q, q_exp, w_exp): @@ -775,9 +877,7 @@ def refine_single(q, q_exp, w_exp): a = torch.stack((-gp[:, 1], gp[:, 0]), dim=1) # (P, 2) num = (w[:, None] * a * r).sum() den = (w[:, None] * a * a).sum().clamp_min(1e-12) - omega = torch.tensor( - [0.0, 0.0, float(num / den)], dtype=torch.float64 - ) + omega = torch.tensor([0.0, 0.0, float(num / den)], dtype=torch.float64) angle = torch.linalg.norm(omega) if angle > 1e-10: dq = quat_from_axis_angle(omega / angle, angle) @@ -801,9 +901,7 @@ def refine_single(q, q_exp, w_exp): + tg[None, :, None] * a1[:, None, None] + tg[None, None, :] * a2[:, None, None] ) - pred = f_p[:, None, None] * torch.exp( - -(S**2) / (2 * sigma_env**2) - ) + pred = f_p[:, None, None] * torch.exp(-(S**2) / (2 * sigma_env**2)) E = (w[:, None, None] * pred).sum(dim=0) / ( (pred**2).sum(dim=0).sqrt().clamp_min(1e-12) ) @@ -830,9 +928,7 @@ def refine_single(q, q_exp, w_exp): if abs(den) > 1e-12: wy += 0.5 * (c2 - c0) / den * step # trust region on the cumulative tilt from the start - prop = tilt_total + torch.tensor( - [wx, wy], dtype=torch.float64 - ) + prop = tilt_total + torch.tensor([wx, wy], dtype=torch.float64) over = float(torch.linalg.norm(prop)) - tilt_cap if over > 0: prop = prop * tilt_cap / float(torch.linalg.norm(prop)) @@ -899,9 +995,7 @@ def get_exp(rx, ry): mm = misorientation_angle_deg( a.reshape(-1, 4), b.reshape(-1, 4), self.crystal.sym_quats ).reshape(R - dr, C - dc) - miso_min[: R - dr, : C - dc] = torch.minimum( - miso_min[: R - dr, : C - dc], mm - ) + miso_min[: R - dr, : C - dc] = torch.minimum(miso_min[: R - dr, : C - dc], mm) miso_min[dr:, dc:] = torch.minimum(miso_min[dr:, dc:], mm) retry = torch.nonzero(miso_min > rescue_threshold_deg) it2 = retry.tolist() @@ -918,18 +1012,11 @@ def get_exp(rx, ry): for dr in (-1, 0, 1): for dc in (-1, 0, 1): nr, nc = rx + dr, ry + dc - if (dr == 0 and dc == 0) or not ( - 0 <= nr < R and 0 <= nc < C - ): + if (dr == 0 and dc == 0) or not (0 <= nr < R and 0 <= nc < C): continue qn = self.quats[nr, nc, 0] if all( - float( - misorientation_angle_deg( - qn, c, self.crystal.sym_quats - ) - ) - > 0.5 + float(misorientation_angle_deg(qn, c, self.crystal.sym_quats)) > 0.5 for c in cands ): cands.append(qn) @@ -1023,9 +1110,7 @@ def _refine_batched( if not bool(ok.any()): break sc = torch.where(ok, w_g.sum(dim=1), sc) - tgt = torch.gather( - qe, 1, j_min[..., None].expand(-1, -1, 2) - ) # (B, G, 2) + tgt = torch.gather(qe, 1, j_min[..., None].expand(-1, -1, 2)) # (B, G, 2) r_vec = tgt - g[..., :2] # in-plane closed form a_vec = torch.stack((-g[..., 1], g[..., 0]), dim=-1) @@ -1057,15 +1142,13 @@ def _refine_batched( + tg[None, :, None] * gyf[:, None, None] - tg[None, None, :] * gxf[:, None, None] ) # (Np, T, T) - pred = ff[:, None, None] * torch.exp( - -(S**2) / (2 * sigma_env**2) + pred = ff[:, None, None] * torch.exp(-(S**2) / (2 * sigma_env**2)) + E_num = torch.zeros((B, n_tg, n_tg), dtype=torch.float64).index_add_( + 0, idx_b, wf[:, None, None] * pred + ) + E_den = torch.zeros((B, n_tg, n_tg), dtype=torch.float64).index_add_( + 0, idx_b, pred**2 ) - E_num = torch.zeros( - (B, n_tg, n_tg), dtype=torch.float64 - ).index_add_(0, idx_b, wf[:, None, None] * pred) - E_den = torch.zeros( - (B, n_tg, n_tg), dtype=torch.float64 - ).index_add_(0, idx_b, pred**2) E = E_num / E_den.sqrt().clamp_min(1e-12) # (B, T, T) flat_ij = E.reshape(B, -1).argmax(dim=1) i_b, j_b = flat_ij // n_tg, flat_ij % n_tg @@ -1096,7 +1179,8 @@ def _refine_batched( prop = tilt_total + torch.stack((wx, wy), dim=-1) norm = torch.linalg.norm(prop, dim=-1) scale_f = torch.where( - norm > tilt_cap, tilt_cap / norm.clamp_min(1e-12), + norm > tilt_cap, + tilt_cap / norm.clamp_min(1e-12), torch.ones_like(norm), ) prop = prop * scale_f[:, None] @@ -1115,9 +1199,7 @@ def _refine_batched( q[nz] = qmult(dq_t, q_nz) quats[i0:i1, m] = torch.where(act[:, None], q, quats[i0:i1, m]) if m == 0: - scores.reshape(-1)[i0:i1] = torch.where( - act, sc, scores.reshape(-1)[i0:i1] - ) + scores.reshape(-1)[i0:i1] = torch.where(act, sc, scores.reshape(-1)[i0:i1]) self.quats = quats.reshape(R, C, M, 4) def generate_pattern(self, rx: int, ry: int, match: int = 0, **kwargs): @@ -1134,7 +1216,7 @@ def match_residual( self, other: "OrientationMap", delete_radius: float = 0.04, - min_number_peaks: int = 3, + min_number_peaks: int = MIN_NUMBER_PEAKS, min_corr_other: float = 0.0, progress_bar: bool = True, ) -> "OrientationMap": @@ -1166,8 +1248,10 @@ def match_residual( ix = [fields.index(f) for f in ("qx", "qy", "intensity")] residual = Vector.from_shape( - (R, C), fields=["qx", "qy", "intensity"], - units=["A^-1", "A^-1", "counts"], name="residual_peaks", + (R, C), + fields=["qx", "qy", "intensity"], + units=["A^-1", "A^-1", "counts"], + name="residual_peaks", ) cells = [] for rx, ry in np.ndindex(R, C): @@ -1186,17 +1270,32 @@ def match_residual( cells.append(data[keep][:, ix]) nested = [cells[r * C : (r + 1) * C] for r in range(R)] residual = Vector.from_data( - nested, fields=["qx", "qy", "intensity"], - units=["A^-1", "A^-1", "counts"], name="residual_peaks", + nested, + fields=["qx", "qy", "intensity"], + units=["A^-1", "A^-1", "counts"], + name="residual_peaks", ) om_res = OrientationMap.from_vectors(residual, self.crystal, self.energy_ev) for attr in ( - "device", "corr_kernel_size", "sigma_excitation", "power_radial", - "power_intensity", "zone_axes", "zone_quats", "zone_step_deg", - "zone_nbr_idx", "zone_nbr_pos", "zone_nbr_valid", - "plan_fft", "shell_radii", "num_gamma", "gamma", "detector_mask", - "plan_norm_shift", "plan_frac_shift", + "device", + "corr_kernel_size", + "sigma_excitation", + "power_radial", + "power_intensity", + "zone_axes", + "zone_quats", + "zone_step_deg", + "zone_nbr_idx", + "zone_nbr_pos", + "zone_nbr_valid", + "plan_fft", + "shell_radii", + "num_gamma", + "gamma", + "detector_mask", + "plan_norm_shift", + "plan_frac_shift", ): setattr(om_res, attr, getattr(self, attr)) om_res.match_orientations( @@ -1211,20 +1310,14 @@ def match_residual( pad_q = torch.zeros((R, C, 1, 4), dtype=torch.float64) pad_q[..., 0] = 1.0 self.quats = torch.cat([self.quats, pad_q], dim=2) - self.corr = torch.cat( - [self.corr, torch.zeros((R, C, 1), dtype=torch.float64)], dim=2 - ) - self.mirror = torch.cat( - [self.mirror, torch.zeros((R, C, 1), dtype=torch.bool)], dim=2 - ) + self.corr = torch.cat([self.corr, torch.zeros((R, C, 1), dtype=torch.float64)], dim=2) + self.mirror = torch.cat([self.mirror, torch.zeros((R, C, 1), dtype=torch.bool)], dim=2) better = om_res.corr[..., 0] > self.corr[..., 1] self.quats[..., 1, :] = torch.where( better[..., None], om_res.quats[..., 0, :], self.quats[..., 1, :] ) self.corr[..., 1] = torch.where(better, om_res.corr[..., 0], self.corr[..., 1]) - self.mirror[..., 1] = torch.where( - better, om_res.mirror[..., 0], self.mirror[..., 1] - ) + self.mirror[..., 1] = torch.where(better, om_res.mirror[..., 0], self.mirror[..., 1]) return self def cluster_orientations( @@ -1306,7 +1399,7 @@ def calculate_strain( self, match: int = 0, pair_distance: float | None = None, - min_pairs: int = 5, + min_pairs: int | None = None, mask: np.ndarray | None = None, ds_sampling: float | None = None, ds_units: str | None = None, @@ -1333,7 +1426,17 @@ def calculate_strain( from quantem.diffraction.strain import StrainMap assert self.quats is not None - delta = pair_distance if pair_distance is not None else self.corr_kernel_size + delta = resolve( + pair_distance, + "pair_distance", + self.metadata.get("refine"), + self.metadata.get("plan"), + default=self.corr_kernel_size, + ) + min_pairs = resolve(min_pairs, "min_pairs", self.metadata.get("refine"), default=MIN_PAIRS) + self.metadata["strain"] = dict( + match=int(match), pair_distance=float(delta), min_pairs=int(min_pairs) + ) peaks = self.peaks R, C = peaks.shape[0], peaks.shape[1] fields = peaks.fields @@ -1392,9 +1495,7 @@ def calculate_strain( sm.num_pairs = num_pairs return sm - def in_plane_angle_deg( - self, match: int = 0, mod_deg: float | None = None - ) -> torch.Tensor: + def in_plane_angle_deg(self, match: int = 0, mod_deg: float | None = None) -> torch.Tensor: """In-plane angle of the crystal a-axis at every position (degrees). The angle of the projected crystal [100] Cartesian axis, measured @@ -1430,6 +1531,4 @@ def misorientation_map(self, reference: torch.Tensor | None = None) -> torch.Ten q = self.quats[..., 0, :] if reference is None: reference = torch.tensor([1.0, 0, 0, 0], dtype=torch.float64) - return misorientation_angle_deg( - reference, q, self.crystal.sym_quats_matching - ) + return misorientation_angle_deg(reference, q, self.crystal.sym_quats_matching) diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py index f45af5189..29d3e4763 100644 --- a/src/quantem/diffraction/orientation_visualization.py +++ b/src/quantem/diffraction/orientation_visualization.py @@ -20,7 +20,20 @@ ) ORIGIN_COLOR = "#2ca02c" MEASURED_COLOR = "0.15" -IPF_GAMMA = 0.4 # <1 expands the white / mixed-color regions of the wedge +IPF_GAMMA = 0.4 +# cluster / grain label colors (tab10 cycle) +CLUSTER_COLORS = [ + (0.122, 0.467, 0.706), + (1.0, 0.498, 0.055), + (0.173, 0.627, 0.173), + (0.839, 0.153, 0.157), + (0.580, 0.404, 0.741), + (0.549, 0.337, 0.294), + (0.890, 0.467, 0.761), + (0.498, 0.498, 0.498), + (0.737, 0.741, 0.133), + (0.090, 0.745, 0.812), +] # <1 expands the white / mixed-color regions of the wedge # additive corner colors: full red, green capped to avoid the fluorescent # look, blue lifted off pure dark blue; pairwise sums give near-max-chroma # yellow / cyan / violet and the three together give white @@ -76,7 +89,7 @@ def _reduce_to_wedge(vectors: torch.Tensor, crystal: Crystal) -> torch.Tensor: v = vectors.clone() v[v[..., 2] < 0] *= -1 return v - Rs = quat_to_matrix(crystal.sym_quats) # (S, 3, 3) + Rs = quat_to_matrix(crystal.sym_quats_matching) # (S, 3, 3) v = vectors.reshape(-1, 3) orbit = torch.cat( [torch.einsum("sij,nj->nsi", Rs, v), torch.einsum("sij,nj->nsi", Rs, -v)], @@ -155,17 +168,12 @@ def wedge_legend( # tall panel with the wedge hanging straight down (the rotation aligns # the wedge's angular bisector with the downward direction) if orientation == "vertical": - az = [ - np.arctan2(c[k, 1] / (1 + c[k, 2]), c[k, 0] / (1 + c[k, 2])) - for k in (1, 2) - ] + az = [np.arctan2(c[k, 1] / (1 + c[k, 2]), c[k, 0] / (1 + c[k, 2])) for k in (1, 2)] th = -np.pi / 2 - (az[0] + az[1]) / 2 else: th = 0.0 rot = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]]) - cxy = np.stack( - [c[:, 0] / (1 + c[:, 2]), c[:, 1] / (1 + c[:, 2])], axis=1 - ) @ rot.T + cxy = np.stack([c[:, 0] / (1 + c[:, 2]), c[:, 1] / (1 + c[:, 2])], axis=1) @ rot.T cx, cy = cxy[:, 0], cxy[:, 1] # rasterize the wedge interior: invert the stereographic projection on a @@ -174,15 +182,11 @@ def wedge_legend( m = 8 x0, x1 = cx.min() - 0.02, cx.max() + 0.02 y0, y1 = cy.min() - 0.02, cy.max() + 0.02 - X, Y = np.meshgrid( - np.linspace(x0, x1, n * m), np.linspace(y0, y1, n * m), indexing="xy" - ) + X, Y = np.meshgrid(np.linspace(x0, x1, n * m), np.linspace(y0, y1, n * m), indexing="xy") Xu = np.cos(th) * X + np.sin(th) * Y Yu = -np.sin(th) * X + np.cos(th) * Y denom = 1 + Xu**2 + Yu**2 - V = np.stack( - [2 * Xu / denom, 2 * Yu / denom, (1 - Xu**2 - Yu**2) / denom], axis=-1 - ) + V = np.stack([2 * Xu / denom, 2 * Yu / denom, (1 - Xu**2 - Yu**2) / denom], axis=-1) A_inv = np.linalg.inv(c.T) W = V @ A_inv.T inside = (W > -1e-9).all(axis=-1) @@ -195,9 +199,7 @@ def wedge_legend( for i0, i1 in ((0, 1), (1, 2), (2, 0)): e = c[i0][None, :] * (1 - tt) + c[i1][None, :] * tt e = e / np.linalg.norm(e, axis=1, keepdims=True) - exy = np.stack( - [e[:, 0] / (1 + e[:, 2]), e[:, 1] / (1 + e[:, 2])], axis=1 - ) @ rot.T + exy = np.stack([e[:, 0] / (1 + e[:, 2]), e[:, 1] / (1 + e[:, 2])], axis=1) @ rot.T ax.plot(exy[:, 0], exy[:, 1], color="k", lw=1.2) if labels: names = crystal.zone_axis_wedge_labels() or ["", "", ""] @@ -285,13 +287,9 @@ def plot_orientation_map( "y": r"$\rightarrow$", }.get(direction, "") label = {"x": "r", "y": "c"}.get(direction, direction) - ax.set_title( - f"{om.crystal.name} in-plane orientation {label} {arrow}" - ) + ax.set_title(f"{om.crystal.name} in-plane orientation {label} {arrow}") elif isinstance(direction, (int, float)): - ax.set_title( - f"{om.crystal.name} in-plane orientation ({direction:g}\u00b0)" - ) + ax.set_title(f"{om.crystal.name} in-plane orientation ({direction:g}\u00b0)") else: ax.set_title(f"{om.crystal.name} in-plane orientation") if scalebar is not None: @@ -426,16 +424,13 @@ def plot_pattern_matches( lw=0, ) sim = om.generate_pattern(rx, ry, match=m) - I = sim["intensity"].numpy() - sim_rc = ( - np.stack([sim["qx"].numpy(), sim["qy"].numpy()], axis=1) - @ rot_back.T - ) - if I.size: + inten = sim["intensity"].numpy() + sim_rc = np.stack([sim["qx"].numpy(), sim["qy"].numpy()], axis=1) @ rot_back.T + if inten.size: ax.scatter( sim_rc[:, 1], sim_rc[:, 0], - s=marker_scale * I / I.max(), + s=marker_scale * inten / inten.max(), marker="+", color=colors[i_om % len(colors)], lw=1.8, @@ -446,8 +441,7 @@ def plot_pattern_matches( ax.set_yticks([]) ax.set_aspect("equal") ax.set_title( - "%s %s\ncorr = %.2f" - % (om.crystal.name, ordinal[m], float(om.corr[rx, ry, m])), + "%s %s\ncorr = %.2f" % (om.crystal.name, ordinal[m], float(om.corr[rx, ry, m])), fontsize=9, ) if scalebar and pi == n_r - 1 and ci == 0: @@ -466,6 +460,7 @@ def plot_pattern_matches( fig.tight_layout() return fig, axs + def plot_cluster_map( om, clusters: dict, @@ -493,8 +488,14 @@ def plot_cluster_map( ax.set_yticks([]) ax.set_title(f"{om.crystal.name} orientation clusters") handles = [ - plt.Line2D([0], [0], marker="s", ls="", color=colors[k % len(colors)], - label=f"{k + 1} ({int(clusters['sizes'][k])} px)") + plt.Line2D( + [0], + [0], + marker="s", + ls="", + color=colors[k % len(colors)], + label=f"{k + 1} ({int(clusters['sizes'][k])} px)", + ) for k in range(K) ] ax.legend(handles=handles, loc="center left", bbox_to_anchor=(1.01, 0.5), fontsize=8) @@ -532,9 +533,7 @@ def _pole_points( else: w = torch.ones(q.shape[0], dtype=torch.float64) w_all = w[:, None].expand(-1, n_fam).reshape(-1) - src_all = ( - torch.arange(q.shape[0])[:, None].expand(-1, n_fam).reshape(-1) - ) + src_all = torch.arange(q.shape[0])[:, None].expand(-1, n_fam).reshape(-1) v = poles_lab.reshape(-1, 3) keep = (v[:, 2] > -1e-8) & (w_all > 0) v, w_keep, src = v[keep], w_all[keep], src_all[keep] @@ -605,17 +604,26 @@ def plot_cluster_pole_figure( for k in range(K): xy = _pole_scatter_xy(clusters["mean_quats"][k], om.crystal, pole) ax.scatter( - xy[:, 0], xy[:, 1], s=60, marker="h", - color=colors[k % len(colors)], edgecolors="k", lw=0.4, + xy[:, 0], + xy[:, 1], + s=60, + marker="h", + color=colors[k % len(colors)], + edgecolors="k", + lw=0.4, label=f"{pole_label} {k + 1}", ) if overlay is not None: - xy = _pole_scatter_xy( - overlay["quats"], overlay["crystal"], overlay["pole"] - ) + xy = _pole_scatter_xy(overlay["quats"], overlay["crystal"], overlay["pole"]) ax.scatter( - xy[:, 0], xy[:, 1], s=70, marker="D", facecolors="none", - edgecolors="k", lw=1.0, label=overlay.get("label", "overlay"), + xy[:, 0], + xy[:, 1], + s=70, + marker="D", + facecolors="none", + edgecolors="k", + lw=1.0, + label=overlay.get("label", "overlay"), ) ax.set_xlim(-1.15, 1.15) ax.set_ylim(-1.15, 1.15) @@ -720,9 +728,7 @@ def plot_pole_figure( # display in the image frame: horizontal = c (col, rightward), vertical = # r (row, downward), matching the orientation maps -- H is indexed # [row-bin, col-bin] so no transpose, origin upper - yy, xx = np.meshgrid( - 0.5 * (ye[:-1] + ye[1:]), 0.5 * (xe[:-1] + xe[1:]), indexing="ij" - ) + yy, xx = np.meshgrid(0.5 * (ye[:-1] + ye[1:]), 0.5 * (xe[:-1] + xe[1:]), indexing="ij") disp = disp.copy() disp[(xx**2 + yy**2).T > 1.0] = 1.0 @@ -772,17 +778,29 @@ def plot_pole_figure( (0.0, 0.22, "r", "center", "top"), ): ax.annotate( - "", xy=(gx + dx, gy + dy), xytext=(gx, gy), + "", + xy=(gx + dx, gy + dy), + xytext=(gx, gy), arrowprops=dict(arrowstyle="-|>", color="0.3", lw=1.2), annotation_clip=False, ) ax.text( - gx + dx * 1.25, gy + dy * 1.25, lbl, - fontsize=9, ha=ha, va=va, color="0.3", + gx + dx * 1.25, + gy + dy * 1.25, + lbl, + fontsize=9, + ha=ha, + va=va, + color="0.3", ) ax.text( - gx - 0.03, gy - 0.06, "scan axes", fontsize=7, ha="left", - va="bottom", color="0.45", + gx - 0.03, + gy - 0.06, + "scan axes", + fontsize=7, + ha="left", + va="bottom", + color="0.45", ) if overlay is not None: if "om" in overlay: @@ -804,15 +822,26 @@ def plot_pole_figure( yc = 0.5 * (oye[:-1] + oye[1:]) # image frame: horizontal = col bins, vertical = row bins ax.contour( - yc, xc, Ho, levels=lev, colors="k", - linewidths=[0.7, 1.3], alpha=0.85, + yc, + xc, + Ho, + levels=lev, + colors="k", + linewidths=[0.7, 1.3], + alpha=0.85, ) ax.plot([], [], color="k", lw=1.2, label=overlay.get("label", "overlay")) else: oxy = _pole_scatter_xy(overlay["quats"], overlay["crystal"], overlay["pole"]) ax.scatter( - oxy[:, 1], oxy[:, 0], s=80, marker="D", facecolors="none", - edgecolors="k", lw=1.2, label=overlay.get("label", "overlay"), + oxy[:, 1], + oxy[:, 0], + s=80, + marker="D", + facecolors="none", + edgecolors="k", + lw=1.2, + label=overlay.get("label", "overlay"), ) ax.legend(loc="upper right", fontsize=8) ax.set_xlim(-1.12, 1.12) diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py index a36d5f5d1..604a29fe7 100644 --- a/src/quantem/diffraction/phase.py +++ b/src/quantem/diffraction/phase.py @@ -27,6 +27,13 @@ from tqdm import tqdm from quantem.core.io.serialize import AutoSerialize +from quantem.diffraction.defaults import ( + MIN_NUMBER_PEAKS, + MIN_SIM_INTENSITY_REL, + PAIR_DISTANCE, + POWER_INTENSITY, + resolve, +) from quantem.diffraction.orientation import OrientationMap @@ -58,6 +65,8 @@ def __init__(self, orientation_maps: list[OrientationMap], _token=None): for m in range(om.quats.shape[2]): self.candidates.append((i, m)) + # hyperparameters inherited from the maps and recorded per stage + self.metadata: dict = {"orientation_maps": [dict(om.metadata) for om in orientation_maps]} self.phase_weights: torch.Tensor | None = None self.costs_single: torch.Tensor | None = None self.cost_best: torch.Tensor | None = None @@ -77,26 +86,32 @@ def from_orientation_maps(cls, orientation_maps: list[OrientationMap]) -> "Phase def fit( self, - pair_distance: float = 0.05, - power_intensity: float = 0.25, + pair_distance: float | None = None, + power_intensity: float | None = None, max_patterns: int = 2, complexity_penalty: float = 0.02, weight_unmatched_sim: float = 0.5, weight_overprediction: float = 1.0, - min_sim_intensity_rel: float = 0.02, + min_sim_intensity_rel: float | None = None, k_max: float | None = None, - min_number_peaks: int = 3, + min_number_peaks: int | None = None, progress_bar: bool = True, ) -> "PhaseMap": """Score all candidate subsets at every probe position. + Parameters left as None inherit the values the orientation + matching used (its plan kernel, intensity power and peak minimum), + so the phase decision compares the same peaks the same way; the + resolved values are recorded in `metadata['fit']`. + Parameters ---------- - pair_distance : float, default=0.05 + pair_distance : float | None Pairing distance delta (1/Angstroms) between simulated and - measured peaks. - power_intensity : float, default=0.25 - Intensities are raised to this power before comparison. + measured peaks; inherits the plan's correlation kernel. + power_intensity : float | None + Intensities are raised to this power before comparison; + inherits the plan's value. max_patterns : int, default=2 Maximum number of candidate patterns fit simultaneously. complexity_penalty : float, default=0.02 @@ -123,6 +138,29 @@ def fit( from scipy.optimize import nnls oms = self.orientation_maps + plan_md = oms[0].metadata.get("plan") + match_md = oms[0].metadata.get("match") + pair_distance = resolve(pair_distance, "pair_distance", plan_md, default=PAIR_DISTANCE) + power_intensity = resolve( + power_intensity, "power_intensity", plan_md, default=POWER_INTENSITY + ) + min_sim_intensity_rel = resolve( + min_sim_intensity_rel, "min_sim_intensity_rel", default=MIN_SIM_INTENSITY_REL + ) + min_number_peaks = resolve( + min_number_peaks, "min_number_peaks", match_md, default=MIN_NUMBER_PEAKS + ) + self.metadata["fit"] = dict( + pair_distance=float(pair_distance), + power_intensity=float(power_intensity), + max_patterns=int(max_patterns), + complexity_penalty=float(complexity_penalty), + weight_unmatched_sim=float(weight_unmatched_sim), + weight_overprediction=float(weight_overprediction), + min_sim_intensity_rel=float(min_sim_intensity_rel), + k_max=k_max, + min_number_peaks=int(min_number_peaks), + ) peaks = oms[0].peaks R, C = peaks.shape[0], peaks.shape[1] cands = self.candidates @@ -196,9 +234,9 @@ def fit( model = B @ w under = np.maximum(im_np - model, 0).sum() # unexplained measured over = np.maximum(model - im_np, 0).sum() # overpredicted paired - cost = ( - under + weight_overprediction * over + (w * unpaired_sim[cols]).sum() - ) / (int_total + 1e-12) + complexity_penalty * (len(cols) - 1) + cost = (under + weight_overprediction * over + (w * unpaired_sim[cols]).sum()) / ( + int_total + 1e-12 + ) + complexity_penalty * (len(cols) - 1) results.append((cost, s, cols, w)) if not results: continue @@ -217,11 +255,7 @@ def fit( # near-duplicates, e.g. after residual re-matching) f_dom = cols_best[int(np.argmax(w_best))] i_dom = cands[f_dom][0] - others = [ - c - for c, s, _, _ in results - if all(cands[f][0] != i_dom for f in s) - ] + others = [c for c, s, _, _ in results if all(cands[f][0] != i_dom for f in s)] reliability[rx, ry] = (min(others) - c_best) if others else torch.nan self.costs_single = costs_single @@ -239,6 +273,41 @@ def fit( self.phase_fractions = w_phase / w_phase.sum(dim=-1, keepdim=True).clamp_min(1e-12) return self + def apply_dynamical(self, result: dict) -> "PhaseMap": + """Take the phase decision from a dynamical refinement. + + A phase map can be built from the orientation maps at any stage: + after matching, after refine_orientations, or after + bloch.refine_dynamical, whose per-candidate intensity costs decide + the phase here. The reliability becomes the cost gap between the + best candidates of the winning crystal and of the runner-up + crystal, and the kinematical result is kept under + `metadata['kinematical']`. + """ + cost = torch.nan_to_num(result["cost"], nan=torch.inf) + n_maps = len(self.orientation_maps) + R, C = cost.shape[:2] + cost_phase = torch.full((R, C, n_maps), torch.inf, dtype=cost.dtype) + for f, (i_om, _) in enumerate(self.candidates): + cost_phase[..., i_om] = torch.minimum(cost_phase[..., i_om], cost[..., f]) + order = cost_phase.sort(dim=-1).values + reliability = torch.where( + torch.isfinite(order[..., 0]), + (order[..., 1] - order[..., 0]).clamp_min(0) + if n_maps > 1 + else torch.zeros_like(order[..., 0]), + torch.full_like(order[..., 0], torch.nan), + ) + self.metadata["kinematical"] = { + "phase_index": self.phase_index, + "reliability": self.reliability, + } + self.phase_index = result["phase_index"] + self.reliability = reliability + self.cost_best = order[..., 0] + self.metadata["dynamical_applied"] = dict(result.get("metadata", {})) + return self + def plot_phase( self, phase_colors: np.ndarray | None = None, @@ -301,13 +370,9 @@ def plot_phase( n_ph = len(phase_colors) for k, color in enumerate(phase_colors): - cmap_k = LinearSegmentedColormap.from_list( - f"rel{k}", [(0, 0, 0), tuple(color)] - ) + cmap_k = LinearSegmentedColormap.from_list(f"rel{k}", [(0, 0, 0), tuple(color)]) cax = ax.inset_axes([1.02 + 0.025 * k, 0.05, 0.025, 0.9]) - cb = fig.colorbar( - ScalarMappable(norm=Normalize(lo, hi), cmap=cmap_k), cax=cax - ) + cb = fig.colorbar(ScalarMappable(norm=Normalize(lo, hi), cmap=cmap_k), cax=cax) if k < n_ph - 1: cb.set_ticks([]) else: diff --git a/src/quantem/diffraction/rotations.py b/src/quantem/diffraction/rotations.py index 77b378bdc..2e0e6d841 100644 --- a/src/quantem/diffraction/rotations.py +++ b/src/quantem/diffraction/rotations.py @@ -185,9 +185,7 @@ def quat_from_zone_axis( torch.as_tensor(in_plane_deg, dtype=v.dtype, device=v.device) ).broadcast_to(v.shape[:-1]) z3 = torch.zeros_like(in_plane) - q_spin = torch.stack( - (torch.cos(in_plane / 2), z3, z3, torch.sin(in_plane / 2)), dim=-1 - ) + q_spin = torch.stack((torch.cos(in_plane / 2), z3, z3, torch.sin(in_plane / 2)), dim=-1) return qnormalize(qmult(q_spin, q_tilt)) @@ -239,9 +237,7 @@ def misorientation_axis_angle( if sym_ops is not None: dq_sym = qmult(dq[..., None, :], sym_ops) # (..., S, 4) best = dq_sym[..., 0].abs().argmax(dim=-1) - dq = torch.gather( - dq_sym, -2, best[..., None, None].expand(*best.shape, 1, 4) - ).squeeze(-2) + dq = torch.gather(dq_sym, -2, best[..., None, None].expand(*best.shape, 1, 4)).squeeze(-2) dq = qnormalize(dq) axis, angle = quat_to_axis_angle(dq) return axis, torch.rad2deg(angle) @@ -264,7 +260,13 @@ def sample_zone_axes( corners: torch.Tensor, step_deg: float, ) -> tuple[torch.Tensor, torch.Tensor]: - """Triangular SLERP grid of unit zone-axis vectors inside a spherical triangle. + """Isotropic SLERP grid of unit zone-axis vectors inside a spherical triangle. + + Rows run from corner 0 toward the opposite edge; the number of points + in each row follows the row's own arc length, so the spacing is close + to `step_deg` in every direction whatever the apex angle of the wedge + (a 30 degree hexagonal wedge and a 120 degree trigonal wedge get the + same density). Parameters ---------- @@ -272,7 +274,7 @@ def sample_zone_axes( (3, 3) rows are the Cartesian corner directions of the fundamental zone-axis wedge, e.g. [001], [011], [111] for m-3m. step_deg : float - Approximate angular step between neighboring zone axes, degrees. + Angular step between neighboring zone axes, degrees. Returns ------- @@ -296,14 +298,143 @@ def sample_zone_axes( vecs.append(pv[None]) inds.append(torch.tensor([[0, 0]])) continue - s = torch.linspace(0, 1, i + 1, dtype=c.dtype, device=c.device) - row = slerp(pv.expand(i + 1, 3), pw.expand(i + 1, 3), s) + arc = torch.rad2deg(torch.acos((pv * pw).sum().clamp(-1, 1))) + n_i = max(1, int(torch.ceil(arc / step_deg).item())) + s = torch.linspace(0, 1, n_i + 1, dtype=c.dtype, device=c.device) + row = slerp(pv.expand(n_i + 1, 3), pw.expand(n_i + 1, 3), s) row = row / torch.linalg.norm(row, dim=-1, keepdim=True) vecs.append(row) - inds.append(torch.stack((torch.full((i + 1,), i), torch.arange(i + 1)), dim=-1)) + inds.append(torch.stack((torch.full((n_i + 1,), i), torch.arange(n_i + 1)), dim=-1)) return torch.cat(vecs), torch.cat(inds).to(torch.long) +def symmetry_axes(sym_quats: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """Distinct rotation axes of a proper point group and their orders. + + Returns (axes (A, 3) unit vectors, orders (A,) long): each axis once, + with the highest rotation order about it (a 4-fold axis is listed as + order 4, not also as 2). + """ + axis, angle = quat_to_axis_angle(sym_quats) + keep = angle > 1e-6 + axis, angle = axis[keep], angle[keep] + order = torch.round(2 * np.pi / angle).to(torch.long) + # orient each axis to a canonical hemisphere so +-axis merge + sign = torch.sign(axis[:, 2] + 1e-3 * axis[:, 1] + 1e-6 * axis[:, 0]) + sign[sign == 0] = 1 + axis = axis * sign[:, None] + out_axes, out_orders = [], [] + for a, n in zip(axis, order): + for k, b in enumerate(out_axes): + if float(torch.abs(a @ b)) > 1 - 1e-6: + out_orders[k] = max(out_orders[k], int(n)) + break + else: + out_axes.append(a) + out_orders.append(int(n)) + if not out_axes: + return torch.zeros((0, 3), dtype=torch.float64), torch.zeros(0, dtype=torch.long) + return torch.stack(out_axes), torch.tensor(out_orders, dtype=torch.long) + + +def _rotate_about(v: torch.Tensor, axis: torch.Tensor, angle_rad: float) -> torch.Tensor: + q = quat_from_axis_angle(axis, torch.tensor(angle_rad, dtype=v.dtype)) + return qrotate(q, v) + + +def _closest(cands: torch.Tensor, prefer: torch.Tensor) -> torch.Tensor: + """The candidate (sign chosen freely) closest to the preferred direction, + with deterministic tie-breaking toward +z, then +y, then +x.""" + signed = torch.cat([cands, -cands]) + key = signed @ prefer + 1e-3 * signed[:, 2] + 1e-6 * signed[:, 1] + 1e-9 * signed[:, 0] + return signed[int(torch.argmax(key))] + + +def fundamental_zone_axis_wedge(sym_quats: torch.Tensor) -> torch.Tensor | None: + """Fundamental zone-axis wedge of a Laue group from its proper rotations. + + Zone axes are directions modulo inversion (Friedel), so the wedge is a + fundamental domain of the Laue group on the projective hemisphere, + built from the actual symmetry axes in the crystal's Cartesian frame + rather than from a table keyed on the Laue class. This makes it correct + for every setting: for -3m the wedge is bounded by the mirror planes + (perpendicular to the in-plane 2-fold axes), which is 30 degrees away + from a wedge bounded by the 2-fold axes themselves; for a cell in a + non-standard Cartesian setting the corners follow the axes wherever + they point. + + Returns (3, 3) corner directions, or None for Laue classes -1 and 2/m + whose fundamental domain is not a spherical triangle (sample the + hemisphere instead). + """ + axes, orders = symmetry_axes(sym_quats) + n_ops = sym_quats.shape[0] + x, y, z = (torch.eye(3, dtype=torch.float64)[i] for i in range(3)) + if n_ops <= 2: + return None + + three = axes[orders == 3] + if three.shape[0] >= 4: # cubic + four = axes[orders == 4] + two = axes[orders == 2] + if four.shape[0] > 0: # m-3m: 4-fold, <110> 2-fold, 3-fold + c0 = _closest(four, z) + c2 = _closest(three, c0) + c1 = _closest(two, c0 + c2) + else: # m-3: two cubic 2-fold axes and the 3-fold between them + c0 = _closest(two, z) + rest = two[torch.abs(two @ c0) < 0.5] + c1 = _closest(rest, x) + c2 = _closest(three, c0 + c1) + return torch.stack([c0, c1, c2]) + + n_max = int(orders.max()) + if n_max in (3, 4, 6): # uniaxial classes + c0 = _closest(axes[orders == n_max], z) + in_plane = axes[(orders == 2) & (torch.abs(axes @ c0) < 1e-6)] + # a direction perpendicular to the axis, nearest +x + ref = x - (x @ c0) * c0 + if torch.linalg.norm(ref) < 1e-6: + ref = y - (y @ c0) * c0 + ref = ref / torch.linalg.norm(ref) + if in_plane.shape[0] == 0: # 6/m, 4/m, -3: any 360/n sector + c2 = ref + c1 = _rotate_about(c2, c0, 2 * np.pi / n_max) + elif n_max == 3: # -3m: the sector between adjacent mirror planes, + # which are perpendicular to the in-plane 2-fold axes + a = _closest(in_plane, ref) + c2 = _rotate_about(a, c0, np.pi / 2) + c1 = _rotate_about(a, c0, 5 * np.pi / 6) + else: # 6/mmm, 4/mmm: between adjacent in-plane 2-fold axes + c2 = _closest(in_plane, ref) + c1 = _rotate_about(c2, c0, np.pi / n_max) + return torch.stack([c0, c1, c2]) + + if n_max == 2 and axes.shape[0] == 3: # mmm: the three 2-fold axes + c0 = _closest(axes, z) + rest = axes[torch.abs(axes @ c0) < 0.5] + c1 = _closest(rest, x) + c2 = _closest(rest[torch.abs(rest @ c1) < 0.5], y) + return torch.stack([c0, c1, c2]) + return None + + +def symmetry_reduced_zone_angles( + zone_axes: torch.Tensor, sym_quats: torch.Tensor, chunk: int = 8 +) -> torch.Tensor: + """(Z, Z) angular distances between zone axes, minimized over the + symmetry operations and the inversion (zone axes are directions modulo + sign). Symmetry-equivalent zones are at distance zero, so an exclusion + ball around a match also excludes its symmetry copies.""" + Rs = quat_to_matrix(sym_quats).to(zone_axes.dtype) + best = torch.full((zone_axes.shape[0],) * 2, -1.0, dtype=zone_axes.dtype) + for s0 in range(0, Rs.shape[0], chunk): + imgs = torch.einsum("sij,zj->szi", Rs[s0 : s0 + chunk], zone_axes) + dots = torch.einsum("szi,wi->szw", imgs, zone_axes).abs().amax(dim=0) + best = torch.maximum(best, dots) + return torch.rad2deg(torch.acos(best.clamp(-1, 1))) + + def symmetry_quaternions( rotations: np.ndarray, lat_real: np.ndarray, @@ -328,7 +459,12 @@ def symmetry_quaternions( W = torch.as_tensor(np.array(rotations), dtype=torch.float64) R_cart = A @ W @ torch.linalg.inv(A) proper = torch.linalg.det(R_cart) > 0 - q = quat_from_matrix(R_cart[proper]) + # for a pseudo-symmetry group the lattice is slightly distorted from the + # ideal one, so A W A^-1 is only approximately orthogonal: take the + # nearest rotation (polar decomposition) so the operators are exact + # rotations and the wedge and misorientation math stay consistent + U, _, Vh = torch.linalg.svd(R_cart[proper]) + q = quat_from_matrix(U @ Vh) # deduplicate (q and -q are the same rotation; qnormalize fixed the sign) q_unique = torch.unique(torch.round(q / 1e-6) * 1e-6, dim=0) return qnormalize(q_unique) diff --git a/tests/diffraction/test_bloch.py b/tests/diffraction/test_bloch.py index 7dc8f0cb6..960219e23 100644 --- a/tests/diffraction/test_bloch.py +++ b/tests/diffraction/test_bloch.py @@ -6,6 +6,7 @@ from ase.build import bulk from quantem.core.datastructures.vector import Vector +from quantem.diffraction import bloch from quantem.diffraction.bloch import dynamical_pattern, refine_thickness from quantem.diffraction.crystal import Crystal from quantem.diffraction.orientation import OrientationMap @@ -43,18 +44,14 @@ def test_thickness_recovery(ti_beta): """Simulate dynamical peaks at a known thickness, recover it.""" t_true = 600.0 torch.manual_seed(0) - zones = torch.tensor( - [[0.1, 0.9, 1.0], [0.3, 0.5, 1.0], [0.05, 1.0, 1.1]], dtype=torch.float64 - ) + zones = torch.tensor([[0.1, 0.9, 1.0], [0.3, 0.5, 1.0], [0.05, 1.0, 1.1]], dtype=torch.float64) N = zones.shape[0] peaks = Vector.from_shape( (1, N), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" ) q_true = quat_from_zone_axis(zones) for i in range(N): - p = dynamical_pattern( - ti_beta, q_true[i], t_true, energy_ev=200e3, sg_max=0.06, k_max=1.5 - ) + p = dynamical_pattern(ti_beta, q_true[i], t_true, energy_ev=200e3, sg_max=0.06, k_max=1.5) keep = p["intensity"][0] > 1e-4 peaks[0, i] = np.stack( [ @@ -84,3 +81,498 @@ def test_thickness_recovery(ti_beta): ) t_fit = res["thickness"][0].numpy() assert (np.abs(t_fit - t_true) <= 50.0).all() + + +# ---------------------------------------------------------------------- +# CBED / LACBED / Kossel / master pattern +# ---------------------------------------------------------------------- + + +def _si(absorptive: bool) -> Crystal: + si = Crystal.from_ase(bulk("Si", "diamond", a=5.431, cubic=True), name="Si", verbose=False) + si.calculate_structure_factors(k_max=3.0) + if absorptive: + si.calculate_dynamical_structure_factors(energy_ev=200e3, k_max=3.0) + return si + + +def _zone_110() -> torch.Tensor: + return quat_from_zone_axis(torch.tensor([[1.0, 1.0, 0.0]]) / np.sqrt(2))[0] + + +def test_zero_tilt_matches_dynamical_pattern(): + si = _si(absorptive=True) + q = _zone_110() + t = torch.tensor([800.0]) + tilts = torch.zeros((1, 2), dtype=torch.float64) + inten, g_xy, hkl = bloch._cbed_amplitudes(si, q, tilts, t, 200e3, sg_max=0.08, k_max=1.3) + ref = bloch.dynamical_pattern(si, q, t, energy_ev=200e3, sg_max=0.08, k_max=1.3) + # same beams (000 first in CBED) and identical intensities + assert hkl.shape[0] == ref["hkl"].shape[0] + 1 + assert torch.allclose(inten[0, 0, 1:], ref["intensity"][0], rtol=1e-10, atol=1e-12) + + +def test_unitarity_without_absorption(): + si = _si(absorptive=False) + q = _zone_110() + tilts = bloch.tilt_grid(2.0, 200e3, n_rings=2) + inten, _, _ = bloch._cbed_amplitudes( + si, q, tilts, torch.tensor([500.0, 1500.0]), 200e3, sg_max=0.08, k_max=1.3 + ) + # Hermitian structure matrix: evolution is unitary in the beam space + total = inten.sum(dim=-1) + assert torch.allclose(total, torch.ones_like(total), atol=1e-8) + + +def test_lacbed_centrosymmetric_disk(): + si = _si(absorptive=True) + q = _zone_110() + res = bloch.calculate_lacbed( + si, + q, + 800.0, + hkl=(0, 0, 0), + energy_ev=200e3, + semiconv_mrad=6.0, + n_pixels=24, + sg_max=0.08, + k_max=1.3, + ) + disk = res["disk"] + # Si is centrosymmetric: the bright field rocking surface at a zone axis + # is inversion symmetric, I(t) = I(-t). Small residuals come from beam + # truncation at the s_g cutoff (|s_g| differs slightly for +g and -g), + # so the tolerance is physical rather than numerical. + flipped = disk[::-1, ::-1] + m = np.isfinite(disk) & np.isfinite(flipped) + assert m.sum() > 100 + assert np.allclose(disk[m], flipped[m], rtol=2e-3, atol=1e-5) + + +def test_cbed_library_common_grid(): + si = _si(absorptive=True) + q0 = _zone_110() + quats = torch.stack([q0, q0]) + lib = bloch.calculate_cbed_library( + si, + quats, + thickness_A=600.0, + energy_ev=200e3, + semiconv_mrad=3.0, + k_max=1.0, + progress_bar=False, + ) + assert lib["patterns"].shape[0] == 2 + assert lib["patterns"].shape[1] == lib["patterns"].shape[2] + assert np.allclose(lib["patterns"][0], lib["patterns"][1]) + assert lib["patterns"][0].max() > 0 + + +def test_kossel_bright_field_matches_lacbed(): + si = _si(absorptive=True) + q = _zone_110() + kw = dict(energy_ev=200e3, semiconv_mrad=15.0, sg_max=0.06, k_max=1.2) + kos = bloch.calculate_kossel(si, q, 900.0, n_pixels=32, progress_bar=False, **kw) + lac = bloch.calculate_lacbed(si, q, 900.0, hkl=(0, 0, 0), n_pixels=32, **kw) + a, b = kos["bright_field"], lac["disk"] + m = np.isfinite(a) & np.isfinite(b) + assert m.sum() > 300 + assert np.allclose(a[m], b[m], rtol=1e-10, atol=1e-12) + + # the summed pattern includes the direct beam plus every diffracted + # cone, so inside the aperture it can only exceed the bright field + pat = kos["pattern"] + assert (pat[m] >= a[m] - 1e-9).mean() > 0.99 + assert np.all(pat[~np.isfinite(a)] == 0) + + +def test_reference_pattern_lookup(): + from scipy.ndimage import gaussian_filter + + si = _si(absorptive=True) + q = _zone_110() + # the master stores the pattern at its own angular resolution + # (angle_step_mrad); compare against the direct calculation blurred to + # the same resolution + master = bloch.calculate_kossel_reference( + si, + [800.0], + energy_ev=200e3, + angle_step_mrad=2.0, + sg_max=0.06, + k_max=1.0, + progress_bar=False, + ) + fast = bloch.kossel_from_reference(master, q, semiconv_mrad=25.0, n_pixels=48) + direct = bloch.calculate_kossel( + si, + q, + 800.0, + energy_ev=200e3, + semiconv_mrad=25.0, + n_pixels=48, + sg_max=0.06, + k_max=1.0, + progress_bar=False, + ) + a, b = fast["bright_field"], direct["bright_field"] + m = np.isfinite(a) & np.isfinite(b) + assert m.sum() > 1000 + # blur both to the master's angular resolution before comparing + px_mrad = 2 * 25.0 / 48 + sigma = 2.0 / px_mrad / 2.355 + af = gaussian_filter(np.nan_to_num(a), sigma) + bf = gaussian_filter(np.nan_to_num(b), sigma) + cc = np.corrcoef(af[m], bf[m])[0, 1] + assert cc > 0.9 + + # off-zone orientation: catches in-plane sign errors that zone-axis + # symmetry hides (the reference stores the ANTI-propagation direction) + from quantem.diffraction.rotations import qmult, quat_from_axis_angle + + tilt = quat_from_axis_angle( + torch.tensor([1.0, 0.3, 0.0], dtype=torch.float64) / np.hypot(1, 0.3), + torch.tensor(np.deg2rad(5.0), dtype=torch.float64), + ) + q2 = qmult(tilt, q) + fast2 = bloch.kossel_from_reference(master, q2, semiconv_mrad=25.0, n_pixels=48) + direct2 = bloch.calculate_kossel( + si, + q2, + 800.0, + energy_ev=200e3, + semiconv_mrad=25.0, + n_pixels=48, + sg_max=0.06, + k_max=1.0, + progress_bar=False, + ) + a2, b2 = fast2["bright_field"], direct2["bright_field"] + m2 = np.isfinite(a2) & np.isfinite(b2) + af2 = gaussian_filter(np.nan_to_num(a2), sigma) + bf2 = gaussian_filter(np.nan_to_num(b2), sigma) + assert np.corrcoef(af2[m2], bf2[m2])[0, 1] > 0.9 + + +def _tilted_110(): + from quantem.diffraction.rotations import qmult, quat_from_axis_angle + + tilt = quat_from_axis_angle( + torch.tensor([1.0, 0.3, 0.0], dtype=torch.float64) / np.hypot(1, 0.3), + torch.tensor(np.deg2rad(5.0), dtype=torch.float64), + ) + return qmult(tilt, _zone_110()) + + +def test_kossel_lines_render_matches_direct(): + si = _si(absorptive=True) + q = _tilted_110() + kw = dict(energy_ev=200e3, semiconv_mrad=25.0, sg_max=0.06, k_max=1.0) + direct = bloch.calculate_kossel(si, q, 800.0, n_pixels=48, progress_bar=False, **kw)[ + "bright_field" + ] + lines = bloch.kossel_lines(si, 800.0, energy_ev=200e3, k_max=1.0) + # every line is a band edge: the +g and -g cones of a row sit at + # +-theta_B, never on the zone plane + first = lines["line_order"] == 1 + assert torch.all(lines["line_u"][first] > 0) + assert torch.all(lines["line_u"][lines["line_order"] == -1] < 0) + r = bloch.render_kossel_lines(lines, q, semiconv_mrad=25.0, n_pixels=48) + a = r["bright_field"] + m = np.isfinite(a) & np.isfinite(direct) + assert m.sum() > 1000 + assert np.corrcoef(a[m], direct[m])[0, 1] > 0.98 + + # polar rendering samples the same function: its first ring must + # agree with the Cartesian pattern evaluated at those angles + pol = bloch.render_kossel_lines( + lines, q, semiconv_mrad=25.0, polar=True, n_radial=10, n_azimuthal=12 + )["polar"] + assert pol.shape == (12, 10) + assert np.all(np.isfinite(pol)) + assert pol.min() > 0 and pol.max() < 1.5 * float(lines["background"][0]) + + +def test_kossel_line_segments_on_cones(): + from quantem.diffraction.rotations import quat_to_matrix + + si = _si(absorptive=True) + q = _tilted_110() + lines = bloch.kossel_lines(si, 800.0, energy_ev=200e3, k_max=1.0) + alpha = 25.0 + seg = bloch.kossel_line_segments(lines, q, semiconv_mrad=alpha) + n = seg["depth"].shape[0] + assert n >= 3 + R = quat_to_matrix(q).numpy() + g_c = lines["g_hat"].numpy() + hkl_row = lines["hkl_row"].numpy() + for k in range(n): + # row of this line from its hkl (an integer multiple of the row vector) + h = seg["hkl"][k] + ri = next(i for i in range(g_c.shape[0]) if np.all(np.cross(hkl_row[i], h) == 0)) + n_ord = int(np.round(np.dot(h, hkl_row[ri]) / np.dot(hkl_row[ri], hkl_row[ri]))) + u = n_ord * bloch.electron_wavelength_angstrom(200e3) * float(lines["g_len"][ri]) / 2 + g_lab = R @ g_c[ri] + for key in ("start_mrad", "stop_mrad"): + row, col = seg[key][k] * 1e-3 + assert np.isclose(np.hypot(row, col), alpha * 1e-3) + d_lab = np.array([-col, -row, np.sqrt(1 - row**2 - col**2)]) + assert abs(d_lab @ g_lab - u) < 1e-9 + # polar end points are the same points + phi, r = seg["start_polar"][k] + assert np.isclose(r, alpha) + assert np.allclose([alpha * np.sin(phi), alpha * np.cos(phi)], seg["start_mrad"][k]) + assert seg["width_mrad"][k] > 0 and 0 < seg["depth"][k] <= 1 + + +def test_reference_residual_hybrid(): + from scipy.ndimage import gaussian_filter + + si = _si(absorptive=True) + q = _zone_110() + master = bloch.calculate_kossel_reference( + si, + [800.0], + energy_ev=200e3, + angle_step_mrad=2.0, + sg_max=0.06, + k_max=1.0, + progress_bar=False, + ) + assert master["k_max"] == 1.0 + lines = bloch.kossel_lines(si, 800.0, energy_ev=200e3, k_max=1.0) + bloch.kossel_reference_residual(master, lines, si) + assert master["residual"].shape == master["lambert"].shape + assert np.all(np.isfinite(master["residual"])) + direct = bloch.calculate_kossel( + si, + q, + 800.0, + energy_ev=200e3, + semiconv_mrad=25.0, + n_pixels=48, + sg_max=0.06, + k_max=1.0, + progress_bar=False, + )["bright_field"] + plain = bloch.render_kossel_lines(lines, q, semiconv_mrad=25.0, n_pixels=48) + hybrid = bloch.render_kossel_lines(lines, q, semiconv_mrad=25.0, n_pixels=48, reference=master) + m = np.isfinite(direct) + sigma = 2.0 / (2 * 25.0 / 48) / 2.355 + bf = gaussian_filter(np.nan_to_num(direct), sigma) + + def cc(x): + return np.corrcoef(gaussian_filter(np.nan_to_num(x), sigma)[m], bf[m])[0, 1] + + # on the zone axis the many-beam residual must improve the line model + assert cc(hybrid["bright_field"]) > cc(plain["bright_field"]) + assert cc(hybrid["bright_field"]) > 0.9 + + +def test_refine_dynamical_recovery(): + """Bragg-vector dynamical refinement: thickness, tilt, in-plane strain and + rotation recovered from noise-free patterns of a strained, tilted cell.""" + from quantem.core.datastructures.vector import Vector + from quantem.diffraction.orientation import OrientationMap + from quantem.diffraction.phase import PhaseMap + from quantem.diffraction.rotations import ( + misorientation_angle_deg, + qmult, + qnormalize, + quat_from_axis_angle, + ) + + energy_ev = 200e3 + xtl = _si(absorptive=True) + torch.manual_seed(0) + rng = np.random.default_rng(0) + N = 6 + q_true = qnormalize(torch.randn(N, 4, dtype=torch.float64)) + t_true = torch.tensor([300.0, 450.0, 600.0, 300.0, 450.0, 600.0]) + A_true = torch.tensor([[1.010, 0.003], [0.003, 0.995]], dtype=torch.float64) + rot = np.deg2rad(0.3) + qz = torch.tensor([np.cos(rot / 2), 0.0, 0.0, np.sin(rot / 2)], dtype=torch.float64) + q_expect = torch.stack([qmult(qz, q_true[i]) for i in range(N)]) + deform3 = torch.eye(3, dtype=torch.float64) + deform3[:2, :2] = A_true + peaks = Vector.from_shape( + (1, N), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(N): + inten, g_xy, _ = bloch._cbed_amplitudes( + xtl, + q_expect[i], + torch.zeros((1, 2), dtype=torch.float64), + t_true[i : i + 1], + energy_ev, + 0.06, + 1.0, + progress_bar=False, + deform=deform3, + ) + inten_np = inten[0, 0, 1:].numpy() + keep = inten_np > 1e-3 * inten_np.max() + peaks[0, i] = np.column_stack([g_xy[1:].numpy()[keep], inten_np[keep]]) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=energy_ev) + om.build_plan(angle_step_zone_axis_deg=3.0, angle_step_in_plane_deg=5.0, verbose=False) + om.match_orientations(progress_bar=False) + # start 0.15 degrees off the truth about random in-plane axes, unstrained + phis = rng.uniform(0, 2 * np.pi, N) + om.quats[0, :, 0] = torch.stack( + [ + qmult( + quat_from_axis_angle( + torch.tensor([np.cos(p), np.sin(p), 0.0], dtype=torch.float64), + torch.tensor(np.deg2rad(0.15), dtype=torch.float64), + ), + q_true[i], + ) + for i, p in enumerate(phis) + ] + ) + om.corr[0, :, 0] = 1.0 + pm = PhaseMap.from_orientation_maps([om]) + pm.fit(progress_bar=False) + res = bloch.refine_dynamical( + pm, + thicknesses_A=np.arange(150, 800, 25.0), + tilt_stages=((0.25, 0.025), (0.04, 0.005)), + power_intensity=0.5, + sg_max=0.06, + k_max=1.0, + progress_bar=False, + ) + # positions with too few beams cannot constrain a 2x2 deformation + valid = np.array([peaks[0, i].array.shape[0] >= 6 for i in range(N)]) + assert valid.sum() >= 4 + err = misorientation_angle_deg(q_expect, om.quats[0, :, 0], xtl.sym_quats).numpy()[valid] + t_err = np.abs(res["thickness"][0].numpy() - t_true.numpy())[valid] + A_err = (res["deformation"][0, valid, 0] - A_true[None]).abs().max() + assert float(A_err) < 1e-3 + assert np.median(err) < 0.03 + assert (t_err <= 25).sum() >= valid.sum() - 1 + # crystal-frame strain of an unstrained position is zero, of a strained + # one has the right magnitude + sc = bloch.strain_crystal_frame(res["deformation"][0, :, 0], om.quats[0, :, 0]) + eps = sc["eps_crystal"] + assert torch.allclose(eps, eps.transpose(-1, -2)) + assert float(eps.abs().max()) < 0.02 + + +def test_image_refinement_round_trip(): + """Rendered patterns with known disk shape: fit_disk_shape recovers the + radius and edge, and the image refinement keeps a correct thickness.""" + from types import SimpleNamespace + + from quantem.core.datastructures.vector import Vector + from quantem.diffraction.orientation import OrientationMap + from quantem.diffraction.phase import PhaseMap + from quantem.diffraction.rotations import qnormalize + + energy_ev = 200e3 + xtl = _si(absorptive=True) + torch.manual_seed(2) + rng = np.random.default_rng(2) + N = 4 + q_true = qnormalize(torch.randn(N, 4, dtype=torch.float64)) + t_true = torch.tensor([300.0, 450.0, 600.0, 400.0]) + shape = (64, 64) + pixel_size, rot, ellipse = 0.04, 15.0, (0.003, -0.002) + disk_r, edge = 3.0, 0.75 + origins = np.full((1, N, 2), 32.0) + imgs = np.zeros((1, N) + shape) + peaks = Vector.from_shape( + (1, N), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(N): + im, _, _ = bloch.render_pattern_image( + xtl, + q_true[i], + [float(t_true[i])], + energy_ev, + shape, + origins[0, i], + pixel_size, + rot, + ellipse, + None, + disk_r, + edge, + sg_max=0.06, + k_max=1.0, + ) + imgs[0, i] = rng.poisson(im[0, 0].numpy() * 1e5 + 20) + inten, g_xy, _ = bloch._cbed_amplitudes( + xtl, + q_true[i], + torch.zeros((1, 2), dtype=torch.float64), + t_true[i : i + 1], + energy_ev, + 0.06, + 1.0, + progress_bar=False, + fast_absorption=True, + ) + inten_np = inten[0, 0, 1:].numpy() + keep = inten_np > 1e-3 * inten_np.max() + peaks[0, i] = np.column_stack([g_xy[1:].numpy()[keep], inten_np[keep]]) + peaks.metadata["rotation_ccw_deg"] = rot + dataset = SimpleNamespace(array=imgs, shape=imgs.shape) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=energy_ev) + om.build_plan(angle_step_zone_axis_deg=3.0, angle_step_in_plane_deg=5.0, verbose=False) + om.match_orientations(progress_bar=False) + om.quats[0, :, 0] = q_true + om.corr[0, :, 0] = 1.0 + pm = PhaseMap.from_orientation_maps([om]) + pm.fit(progress_bar=False) + res = bloch.refine_dynamical( + pm, + thicknesses_A=np.arange(200, 700, 50.0), + tilt_stages=((0.05, 0.05),), + power_intensity=0.5, + sg_max=0.06, + k_max=1.0, + progress_bar=False, + ) + valid = [i for i in range(N) if peaks[0, i].array.shape[0] >= 6] + assert len(valid) >= 2 + shape_fit = bloch.fit_disk_shape( + dataset, + pm, + res, + origins, + pixel_size, + rot, + ellipse, + positions=[(0, i) for i in valid], + radii_px=np.array([2.0, 2.5, 3.0, 3.5, 4.0]), + edges_px=np.array([0.5, 0.75, 1.0, 1.5]), + sg_max=0.06, + k_max=1.0, + progress_bar=False, + ) + assert shape_fit["disk_radius_px"] == disk_r + assert shape_fit["edge_px"] == edge + img = bloch.refine_dynamical_image( + dataset, + pm, + res, + origins, + pixel_size, + disk_r, + edge, + rot, + ellipse, + thickness_half_range_A=50, + thickness_step_A=25, + tilt_stage=(0.02, 0.01), + sg_max=0.06, + k_max=1.0, + progress_bar=False, + ) + t_err = np.abs(img["thickness"][0].numpy() - t_true.numpy())[valid] + assert (t_err <= 25).sum() >= len(valid) - 1 + assert np.all(np.isfinite(img["cost"][0].numpy()[valid])) + assert pm.metadata["dynamical_image"]["disk_radius_px"] == disk_r + pm.apply_dynamical(res) + assert pm.metadata["dynamical_applied"]["precession_deg"] == 0.0 diff --git a/tests/diffraction/test_cbed.py b/tests/diffraction/test_cbed.py deleted file mode 100644 index da35405ae..000000000 --- a/tests/diffraction/test_cbed.py +++ /dev/null @@ -1,134 +0,0 @@ -"""CBED simulation against the single-beam Bloch solver and physics checks.""" - -import numpy as np -import torch -from ase.build import bulk - -from quantem.diffraction import bloch -from quantem.diffraction.crystal import Crystal -from quantem.diffraction.rotations import quat_from_zone_axis - - -def _si(absorptive: bool) -> Crystal: - si = Crystal.from_ase( - bulk("Si", "diamond", a=5.431, cubic=True), name="Si", verbose=False - ) - si.calculate_structure_factors(k_max=3.0) - if absorptive: - si.calculate_dynamical_structure_factors(energy_ev=200e3, k_max=3.0) - return si - - -def _zone_110() -> torch.Tensor: - return quat_from_zone_axis(torch.tensor([[1.0, 1.0, 0.0]]) / np.sqrt(2))[0] - - -def test_zero_tilt_matches_dynamical_pattern(): - si = _si(absorptive=True) - q = _zone_110() - t = torch.tensor([800.0]) - tilts = torch.zeros((1, 2), dtype=torch.float64) - I, g_xy, hkl = bloch._cbed_amplitudes( - si, q, tilts, t, 200e3, sg_max=0.08, k_max=1.3 - ) - ref = bloch.dynamical_pattern( - si, q, t, energy_ev=200e3, sg_max=0.08, k_max=1.3 - ) - # same beams (000 first in CBED) and identical intensities - assert hkl.shape[0] == ref["hkl"].shape[0] + 1 - assert torch.allclose( - I[0, 0, 1:], ref["intensity"][0], rtol=1e-10, atol=1e-12 - ) - - -def test_unitarity_without_absorption(): - si = _si(absorptive=False) - q = _zone_110() - tilts = bloch.tilt_grid(2.0, 200e3, n_rings=2) - I, _, _ = bloch._cbed_amplitudes( - si, q, tilts, torch.tensor([500.0, 1500.0]), 200e3, sg_max=0.08, k_max=1.3 - ) - # Hermitian structure matrix: evolution is unitary in the beam space - total = I.sum(dim=-1) - assert torch.allclose(total, torch.ones_like(total), atol=1e-8) - - -def test_lacbed_centrosymmetric_disk(): - si = _si(absorptive=True) - q = _zone_110() - res = bloch.calculate_lacbed( - si, q, 800.0, hkl=(0, 0, 0), - energy_ev=200e3, semiconv_mrad=6.0, n_pixels=24, sg_max=0.08, k_max=1.3, - ) - disk = res["disk"] - # Si is centrosymmetric: the bright field rocking surface at a zone axis - # is inversion symmetric, I(t) = I(-t). Small residuals come from beam - # truncation at the s_g cutoff (|s_g| differs slightly for +g and -g), - # so the tolerance is physical rather than numerical. - flipped = disk[::-1, ::-1] - m = np.isfinite(disk) & np.isfinite(flipped) - assert m.sum() > 100 - assert np.allclose(disk[m], flipped[m], rtol=2e-3, atol=1e-5) - - -def test_cbed_library_common_grid(): - si = _si(absorptive=True) - q0 = _zone_110() - quats = torch.stack([q0, q0]) - lib = bloch.calculate_cbed_library( - si, quats, thickness_A=600.0, - energy_ev=200e3, semiconv_mrad=3.0, k_max=1.0, progress_bar=False, - ) - assert lib["patterns"].shape[0] == 2 - assert lib["patterns"].shape[1] == lib["patterns"].shape[2] - assert np.allclose(lib["patterns"][0], lib["patterns"][1]) - assert lib["patterns"][0].max() > 0 - - -def test_kossel_bright_field_matches_lacbed(): - si = _si(absorptive=True) - q = _zone_110() - kw = dict(energy_ev=200e3, semiconv_mrad=15.0, sg_max=0.06, k_max=1.2) - kos = bloch.calculate_kossel( - si, q, 900.0, n_pixels=32, progress_bar=False, **kw - ) - lac = bloch.calculate_lacbed(si, q, 900.0, hkl=(0, 0, 0), n_pixels=32, **kw) - a, b = kos["bright_field"], lac["disk"] - m = np.isfinite(a) & np.isfinite(b) - assert m.sum() > 300 - assert np.allclose(a[m], b[m], rtol=1e-10, atol=1e-12) - - # the summed pattern includes the direct beam plus every diffracted - # cone, so inside the aperture it can only exceed the bright field - pat = kos["pattern"] - assert (pat[m] >= a[m] - 1e-9).mean() > 0.99 - assert np.all(pat[~np.isfinite(a)] == 0) - - -def test_master_pattern_lookup(): - from scipy.ndimage import gaussian_filter - - si = _si(absorptive=True) - q = _zone_110() - # the master stores the pattern at its own angular resolution - # (angle_step_mrad); compare against the direct calculation blurred to - # the same resolution - master = bloch.calculate_kossel_master( - si, [800.0], energy_ev=200e3, - angle_step_mrad=2.0, sg_max=0.06, k_max=1.0, progress_bar=False, - ) - fast = bloch.kossel_from_master(master, q, semiconv_mrad=25.0, n_pixels=48) - direct = bloch.calculate_kossel( - si, q, 800.0, energy_ev=200e3, semiconv_mrad=25.0, - n_pixels=48, sg_max=0.06, k_max=1.0, progress_bar=False, - ) - a, b = fast["bright_field"], direct["bright_field"] - m = np.isfinite(a) & np.isfinite(b) - assert m.sum() > 1000 - # blur both to the master's angular resolution before comparing - px_mrad = 2 * 25.0 / 48 - sigma = 2.0 / px_mrad / 2.355 - af = gaussian_filter(np.nan_to_num(a), sigma) - bf = gaussian_filter(np.nan_to_num(b), sigma) - cc = np.corrcoef(af[m], bf[m])[0, 1] - assert cc > 0.9 diff --git a/tests/diffraction/test_crystal.py b/tests/diffraction/test_crystal.py index 109999318..4a9426eb4 100644 --- a/tests/diffraction/test_crystal.py +++ b/tests/diffraction/test_crystal.py @@ -43,7 +43,7 @@ def test_ring_positions(ti_beta): def test_pseudo_symmetry(): ortho = Atoms("Au", positions=[[0, 0, 0]], cell=[4.000, 4.001, 4.002], pbc=True) - exact = Crystal.from_ase(ortho) + exact = Crystal.from_ase(ortho, pseudo_symmetry_tol=None) pseudo = Crystal.from_ase(ortho, pseudo_symmetry_tol=0.01) assert exact.pointgroup_matching == "mmm" assert pseudo.pointgroup_matching == "m-3m" @@ -54,9 +54,7 @@ def test_pseudo_symmetry(): def test_zone_axis_wedge_anchored_001(ti_beta): wedge = ti_beta.zone_axis_wedge() - assert torch.allclose( - wedge[0], torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64) - ) + assert torch.allclose(wedge[0], torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64)) def test_generate_pattern(ti_beta): @@ -71,3 +69,117 @@ def test_generate_pattern(ti_beta): orig = torch.stack((p["qx"], p["qy"]), dim=1) d = torch.cdist(rot, orig).min(dim=1).values assert float(d.max()) < 1e-6 + + +def _images_in_wedge(xtl, n=3000, tol=1e-9): + """Count, per random direction, its symmetry images inside the wedge.""" + from quantem.diffraction.rotations import quat_to_matrix + + rng = np.random.default_rng(0) + d = rng.normal(size=(n, 3)) + d = torch.as_tensor(d / np.linalg.norm(d, axis=1, keepdims=True)) + c = xtl.zone_axis_wedge() + Rs = quat_to_matrix(xtl.sym_quats_matching) + imgs = torch.einsum("sij,nj->nsi", Rs, d) + imgs = torch.cat([imgs, -imgs], dim=1).reshape(-1, 3) + ok = torch.ones(imgs.shape[0], dtype=torch.bool) + for i in range(3): + nrm = torch.cross(c[i], c[(i + 1) % 3], dim=0) + ok &= (imgs @ nrm) * torch.sign(nrm @ c[(i + 2) % 3]) >= -tol + return ok.reshape(n, -1).sum(dim=1) + + +@pytest.mark.parametrize( + "label,spacegroup,symbols,basis,cellpar,laue", + [ + ("Si", 227, ["Si"], [(0, 0, 0)], [5.43] * 3 + [90] * 3, "m-3m"), + ( + "pyrite", + 205, + ["Fe", "S"], + [(0, 0, 0), (0.385, 0.385, 0.385)], + [5.42] * 3 + [90] * 3, + "m-3", + ), + ("Ti", 194, ["Ti"], [(1 / 3, 2 / 3, 0.25)], [2.95, 2.95, 4.68, 90, 90, 120], "6/mmm"), + ( + "CdI2 -3m1", + 164, + ["Cd", "I"], + [(0, 0, 0), (1 / 3, 2 / 3, 0.25)], + [4.24, 4.24, 6.84, 90, 90, 120], + "-3m", + ), + ("Bi R-3m", 166, ["Bi"], [(0, 0, 0.234)], [4.55, 4.55, 11.86, 90, 90, 120], "-3m"), + ( + "P-31m", + 162, + ["Cu", "O"], + [(1 / 3, 2 / 3, 0), (0.4, 0, 0.3)], + [5.0, 5.0, 7.0, 90, 90, 120], + "-3m", + ), + ( + "ilmenite", + 148, + ["Fe", "Ti", "O"], + [(0, 0, 0.355), (0, 0, 0.146), (0.317, 0.023, 0.245)], + [5.09, 5.09, 14.09, 90, 90, 120], + "-3", + ), + ( + "rutile", + 136, + ["Ti", "O"], + [(0, 0, 0), (0.305, 0.305, 0)], + [4.59, 4.59, 2.96, 90, 90, 90], + "4/mmm", + ), + ( + "Pnma", + 62, + ["Fe", "C"], + [(0.18, 0.25, 0.33), (0.04, 0.25, 0.87)], + [5.0, 6.7, 4.5, 90, 90, 90], + "mmm", + ), + ], +) +def test_wedge_is_fundamental_domain(label, spacegroup, symbols, basis, cellpar, laue): + from ase.spacegroup import crystal as ase_crystal + + atoms = ase_crystal(symbols, basis=basis, spacegroup=spacegroup, cellpar=cellpar) + xtl = Crystal.from_ase(atoms, name=label, verbose=False) + assert xtl.laue_group_matching == laue + # exactly one symmetry image of every direction lies in the wedge: the + # wedge covers all of orientation space once (the -3m1 setting used to + # get a wedge rotated by 30 degrees, covering half the directions twice) + hits = _images_in_wedge(xtl) + assert int(hits.min()) == 1 and int(hits.max()) == 1 + labels = xtl.zone_axis_wedge_labels() + assert len(labels) == 3 and all(len(t) > 2 for t in labels) + + +def test_wedge_follows_cell_setting(): + # a rotated Cartesian setting moves the symmetry axes; the wedge follows + atoms = bulk("Si", "diamond", a=5.431, cubic=True) + atoms.rotate(37, "z", rotate_cell=True) + atoms.rotate(20, "x", rotate_cell=True) + xtl = Crystal.from_ase(atoms, verbose=False) + hits = _images_in_wedge(xtl) + assert int(hits.min()) == 1 and int(hits.max()) == 1 + assert xtl.zone_axis_wedge_labels(mathtext=False) == ["[001]", "[011]", "[111]"] + + +def test_pseudo_symmetry_default_and_warning(): + ortho = Atoms("Au", positions=[[0, 0, 0]], cell=[4.000, 4.001, 4.002], pbc=True) + xtl = Crystal.from_ase(ortho, verbose=False) # default tolerance 0.1 A + assert xtl.pointgroup == "mmm" and xtl.pointgroup_matching == "m-3m" + msg = xtl.matching_symmetry_warning() + assert msg is not None and "pseudo_symmetry_tol=None" in msg + # the pseudo group's operators are exact rotations (orthonormalized), so + # its wedge is a fundamental domain up to the cell distortion + hits = _images_in_wedge(xtl, tol=1e-3) + assert int(hits.min()) >= 1 + exact = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True), verbose=False) + assert exact.matching_symmetry_warning() is None diff --git a/tests/diffraction/test_orientation.py b/tests/diffraction/test_orientation.py index ddb589753..b83ba1f4a 100644 --- a/tests/diffraction/test_orientation.py +++ b/tests/diffraction/test_orientation.py @@ -18,9 +18,7 @@ def _make_peaks(xtl, q_true, sigma=0.02): ) for i in range(N): p = xtl.generate_pattern(q_true[i], energy_ev=200e3, sigma_excitation=sigma) - peaks[0, i] = np.stack( - [p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()], axis=1 - ) + peaks[0, i] = np.stack([p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()], axis=1) return peaks @@ -40,9 +38,7 @@ def test_roundtrip_matching(builder, kwargs): peaks = _make_peaks(xtl, q_true) om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) - om.build_plan( - angle_step_zone_axis_deg=2.0, angle_step_in_plane_deg=2.0, power_intensity=0.0 - ) + om.build_plan(angle_step_zone_axis_deg=2.0, angle_step_in_plane_deg=2.0, power_intensity=0.0) om.match_orientations(progress_bar=False) # noiseless synthetic data: the envelope tilt is exact, so allow the # full grid-scale correction (the default trust region is sized for @@ -125,3 +121,141 @@ def test_square_detector_correction(): # with the aperture correction, kernel leakage at the hard detector edge # can push the normalized score a few percent above 1 assert float(om.corr.max()) <= 1.05 + + +def _ase(spacegroup, symbols, basis, cellpar): + from ase.spacegroup import crystal as ase_crystal + + return ase_crystal(symbols, basis=basis, spacegroup=spacegroup, cellpar=cellpar) + + +@pytest.mark.parametrize( + "label,atoms,step", + [ + ( + "Bi -3m", + lambda: _ase(166, ["Bi"], [(0, 0, 0.234)], [4.55, 4.55, 11.86, 90, 90, 120]), + 2.0, + ), + # ilmenite's projections are nearly mirror symmetric, so the flipped + # orientation is a close rival and needs the finer zone grid + ( + "ilmenite -3", + lambda: _ase( + 148, + ["Fe", "Ti", "O"], + [(0, 0, 0.355), (0, 0, 0.146), (0.317, 0.023, 0.245)], + [5.09, 5.09, 14.09, 90, 90, 120], + ), + 1.0, + ), + ], +) +def test_roundtrip_low_symmetry(label, atoms, step): + # low-symmetry crystals see errors that cubic and hexagonal symmetry + # hides: a wrong wedge (trigonal) or a redundant library + torch.manual_seed(5) + xtl = Crystal.from_ase(atoms(), name=label, verbose=False) + xtl.calculate_structure_factors(k_max=1.3) + N = 20 + q_true = qnormalize(torch.randn(N, 4, dtype=torch.float64)) + peaks = _make_peaks(xtl, q_true) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=step, angle_step_in_plane_deg=2.0, verbose=False) + om.match_orientations(progress_bar=False) + err = misorientation_angle_deg(q_true, om.quats[0, :, 0], xtl.sym_quats).numpy() + assert (err < 2.5).mean() >= 0.8 + # symmetry copies of the best zone must not count as the second best + assert float(np.median(om.reliability[0].numpy())) > 0.02 + + +def test_reliability_with_hemisphere_library(): + # Laue 2/m has no wedge: the hemisphere library holds every zone twice, + # and reliability must still see past the symmetry copy + torch.manual_seed(2) + atoms = _ase( + 14, + ["Zr", "O", "O"], + [(0.275, 0.040, 0.208), (0.070, 0.332, 0.345), (0.450, 0.758, 0.479)], + [5.15, 5.21, 5.32, 90, 99.2, 90], + ) + xtl = Crystal.from_ase(atoms, name="ZrO2", pseudo_symmetry_tol=None, verbose=False) + xtl.calculate_structure_factors(k_max=1.2) + assert xtl.zone_axis_wedge() is None + q_true = qnormalize(torch.randn(8, 4, dtype=torch.float64)) + peaks = _make_peaks(xtl, q_true) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=3.0, angle_step_in_plane_deg=3.0, verbose=False) + om.match_orientations(progress_bar=False) + assert float(np.median(om.reliability[0].numpy())) > 0.02 + + +def test_pseudo_symmetry_warning_on_plan(): + import warnings + + from ase import Atoms + + ortho = Atoms("Au", positions=[[0, 0, 0]], cell=[4.000, 4.001, 4.002], pbc=True) + xtl = Crystal.from_ase(ortho, verbose=False) + xtl.calculate_structure_factors(k_max=1.2) + q_true = qnormalize(torch.randn(2, 4, dtype=torch.float64)) + peaks = _make_peaks(xtl, q_true) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + om.build_plan(angle_step_zone_axis_deg=5.0, angle_step_in_plane_deg=5.0, verbose=False) + assert any("pseudo-symmetry" in str(x.message) for x in w) + + +def test_metadata_inheritance(): + # each stage records its hyperparameters; later stages inherit what is + # left as None, so one tuned value propagates through the whole chain + from quantem.diffraction import bloch + from quantem.diffraction.phase import PhaseMap + + torch.manual_seed(1) + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + q_true = qnormalize(torch.randn(4, 4, dtype=torch.float64)) + peaks = _make_peaks(xtl, q_true) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3, precession_deg=0.7) + om.build_plan( + angle_step_zone_axis_deg=3.0, corr_kernel_size=0.04, power_intensity=0.3, verbose=False + ) + om.match_orientations(progress_bar=False) + om.refine_orientations(progress_bar=False) + assert om.metadata["plan"]["pair_distance"] == 0.04 + assert om.metadata["refine"]["pair_distance"] == 0.04 + assert om.metadata["match"]["min_number_peaks"] == 5 + pm = PhaseMap.from_orientation_maps([om]) + pm.fit(progress_bar=False) + assert pm.metadata["fit"]["pair_distance"] == 0.04 + assert pm.metadata["fit"]["power_intensity"] == 0.3 + assert pm.metadata["fit"]["min_number_peaks"] == 5 + xtl.calculate_dynamical_structure_factors(energy_ev=200e3, k_max=2.0) + res = bloch.refine_dynamical( + pm, + thicknesses_A=[300.0, 400.0], + tilt_stages=((0.1, 0.1),), + n_precession=4, + k_max=1.0, + mask=np.array([[True, False, False, False]]), + progress_bar=False, + ) + md = res["metadata"] + assert md["precession_deg"] == 0.7 and md["pair_distance"] == 0.04 + assert md["power_intensity"] == 0.3 and md["min_number_peaks"] == 5 + assert pm.metadata["dynamical"] is md + # explicit values still win + res2 = bloch.refine_dynamical( + pm, + thicknesses_A=[300.0], + tilt_stages=((0.1, 0.1),), + n_precession=4, + precession_deg=0.0, + pair_distance=0.06, + k_max=1.0, + mask=np.array([[True, False, False, False]]), + progress_bar=False, + ) + assert res2["metadata"]["precession_deg"] == 0.0 and res2["metadata"]["pair_distance"] == 0.06 diff --git a/tests/diffraction/test_rotations.py b/tests/diffraction/test_rotations.py index 381950dc8..1941a986a 100644 --- a/tests/diffraction/test_rotations.py +++ b/tests/diffraction/test_rotations.py @@ -34,17 +34,13 @@ def test_matrix_roundtrip(random_quats): def test_euler_roundtrip(random_quats): e = quat_to_euler_zxz(random_quats) - assert torch.allclose( - qnormalize(quat_from_euler_zxz(e)), random_quats, atol=1e-8 - ) + assert torch.allclose(qnormalize(quat_from_euler_zxz(e)), random_quats, atol=1e-8) def test_rotate_matches_matrix(random_quats): v = torch.randn(100, 3, dtype=torch.float64) R = quat_to_matrix(random_quats) - assert torch.allclose( - qrotate(random_quats, v), (R @ v[..., None]).squeeze(-1), atol=1e-10 - ) + assert torch.allclose(qrotate(random_quats, v), (R @ v[..., None]).squeeze(-1), atol=1e-10) def test_mult_conj_identity(random_quats): @@ -91,14 +87,46 @@ def test_misorientation_symmetry(): def test_sample_zone_axes_wedge(): - corners = torch.tensor( - [[0, 0, 1], [0, 1, 1], [1, 1, 1]], dtype=torch.float64 - ) + corners = torch.tensor([[0, 0, 1], [0, 1, 1], [1, 1, 1]], dtype=torch.float64) corners = corners / torch.linalg.norm(corners, dim=-1, keepdim=True) v, inds = sample_zone_axes(corners, 2.0) - assert torch.allclose( - torch.linalg.norm(v, dim=-1), torch.ones(v.shape[0], dtype=v.dtype) - ) + assert torch.allclose(torch.linalg.norm(v, dim=-1), torch.ones(v.shape[0], dtype=v.dtype)) # corners present for c in corners: - assert (torch.linalg.norm(v - c, dim=-1).min() < 1e-8) + assert torch.linalg.norm(v - c, dim=-1).min() < 1e-8 + + +def test_sample_zone_axes_isotropic(): + # the requested step holds in every direction, whatever the apex angle + for apex_deg in (30.0, 120.0): + a = np.deg2rad(apex_deg) + corners = torch.tensor( + [[0, 0, 1], [1, 0, 0], [np.cos(a), np.sin(a), 0]], dtype=torch.float64 + ) + v, _ = sample_zone_axes(corners, 2.0) + dots = (v @ v.T).clamp(-1, 1) + ang = torch.rad2deg(torch.acos(dots)) + ang.fill_diagonal_(1e9) + nn = ang.min(dim=1).values + # points on the equator row: spacing within 25% of the step + eq = v[:, 2].abs() < 1e-9 + assert nn[eq].min() > 1.5 and nn[eq].max() < 2.5 + + +def test_symmetry_reduced_zone_angles(): + from ase.build import bulk + + from quantem.diffraction.crystal import Crystal + from quantem.diffraction.rotations import symmetry_reduced_zone_angles + + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True), verbose=False) + za = torch.tensor( + [[0, 0, 1.0], [1.0, 0, 0], [0, 1.0, 0], [1.0, 1.0, 1.0], [-1.0, 1.0, 1.0]], + dtype=torch.float64, + ) + za = za / torch.linalg.norm(za, dim=1, keepdim=True) + ang = symmetry_reduced_zone_angles(za, xtl.sym_quats) + # cubic axes are symmetry equivalent, as are the two <111> directions + assert float(ang[0, 1]) < 1e-6 and float(ang[0, 2]) < 1e-6 + assert float(ang[3, 4]) < 1e-6 + assert abs(float(ang[0, 3]) - 54.7356) < 1e-3 From 7aabfdc5e0b6da763d01a08a61aeb037e20034b1 Mon Sep 17 00:00:00 2001 From: cophus Date: Tue, 8 Sep 2026 20:11:26 +0200 Subject: [PATCH 05/36] various fixes --- src/quantem/diffraction/bloch.py | 668 ++++++++++++++++++++---- src/quantem/diffraction/crystal.py | 87 ++- src/quantem/diffraction/illumination.py | 202 +++++++ src/quantem/diffraction/orientation.py | 86 ++- tests/diffraction/test_bloch.py | 269 ++++++++++ tests/diffraction/test_illumination.py | 141 +++++ tests/diffraction/test_orientation.py | 64 +++ 7 files changed, 1395 insertions(+), 122 deletions(-) create mode 100644 src/quantem/diffraction/illumination.py create mode 100644 tests/diffraction/test_illumination.py diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py index b71df3b8c..2f02c7855 100644 --- a/src/quantem/diffraction/bloch.py +++ b/src/quantem/diffraction/bloch.py @@ -67,17 +67,21 @@ def _coupling_matrix( else: hkl_all = crystal.hkl U_all = crystal.struct_factors * (gamma_rel / np.pi) - span = 2 * int(hkl_all.abs().max()) + 1 + nb = hkl_beams.shape[0] + diff = hkl_beams[:, None, :] - hkl_beams[None, :, :] # (nb, nb, 3) + # integer key that is injective over the union of the stored indices + # and the queried differences, so a difference outside the stored box + # can never alias a stored factor (a scalar key over the stored box + # alone did: (2,0,0) unstored returned the factor of (-1,1,0)) + m = max(int(hkl_all.abs().max()), int(diff.abs().max())) + span = 2 * m + 1 key_mult = torch.tensor([1, span, span**2], dtype=torch.long) def keys(h): - return (h * key_mult[None, :]).sum(dim=1) + return ((h + m) * key_mult).sum(dim=-1) lut = {int(k): i for i, k in enumerate(keys(hkl_all))} - - nb = hkl_beams.shape[0] - diff = hkl_beams[:, None, :] - hkl_beams[None, :, :] # (nb, nb, 3) - diff_keys = (diff * key_mult[None, None, :]).sum(dim=-1) + diff_keys = keys(diff) U = torch.zeros((nb, nb), dtype=torch.complex128) idx = torch.tensor( [lut.get(int(k), -1) for k in diff_keys.reshape(-1)], dtype=torch.long @@ -88,12 +92,118 @@ def keys(h): u0_imag = 0.0 if absorptive: - i0 = lut.get(0, -1) + i0 = lut.get(int(keys(torch.zeros(3, dtype=torch.long))), -1) if i0 >= 0: u0_imag = float(U_all[i0].imag) return U, u0_imag, absorptive +_coverage_warned: set = set() + + +def _beam_universe(crystal: Crystal) -> tuple[torch.Tensor, torch.Tensor]: + """Reciprocal lattice points that can carry dynamical intensity. + + With absorptive factors present, every index of the stored factor set + (calculate_dynamical_structure_factors), including reflections whose + structure factor is zero: those fill by multiple scattering through + intermediate beams and would never enter the Bloch state if the beams + were taken from the kinematical list, which drops them. Without + absorptive factors, the kinematical list. Returns (hkl (N, 3) long, + g crystal-frame (N, 3)) without the 000 beam. + """ + if getattr(crystal, "U_dyn", None) is not None: + hkl = crystal.hkl_dyn + g = hkl.to(torch.float64) @ crystal.lat_recip + keep = torch.linalg.norm(g, dim=1) > 1e-9 + # points of the conventional index box that are not reciprocal + # lattice points of the primitive cell (centering absences: odd + # h+k+l in bcc, mixed parity in fcc) are never excited and are + # dropped; glide and screw absences such as Si 200 and 222 are + # lattice points and stay + keep &= _primitive_lattice_mask(crystal, g) + return hkl[keep], g[keep] + return crystal.hkl, crystal.g_vec + + +def _primitive_lattice_mask(crystal: Crystal, g: torch.Tensor) -> torch.Tensor: + """True where the Cartesian reciprocal vectors g are points of the + primitive reciprocal lattice of the crystal (integer coordinates + g . a_i for the primitive real-space vectors a_i).""" + lat_p = getattr(crystal, "_primitive_lattice", None) + if lat_p is None: + import spglib + + cell = ( + crystal.lat_real.numpy(), + crystal.positions_frac.numpy(), + crystal.numbers.numpy(), + ) + prim = spglib.standardize_cell(cell, to_primitive=True, no_idealize=True) + lat_p = np.asarray(crystal.lat_real.numpy() if prim is None else prim[0], dtype=float) + crystal._primitive_lattice = lat_p + m = g.to(torch.float64) @ torch.as_tensor(lat_p, dtype=torch.float64).T + return (m - torch.round(m)).abs().amax(dim=1) < 1e-6 + + +def _check_dynamical_factors(crystal: Crystal, energy_ev: float, g_max_beams: float) -> None: + """Warn once per crystal when the absorptive factors were computed at + another energy or do not cover every coupling g - h of the beam list + (which needs factors out to twice the largest beam).""" + if getattr(crystal, "U_dyn", None) is None: + return + key = id(crystal) + if key in _coverage_warned: + return + e_dyn = getattr(crystal, "dyn_energy_ev", None) + k_dyn = getattr(crystal, "dyn_k_max", None) + msgs = [] + if e_dyn is not None and abs(e_dyn - energy_ev) > 1.0: + msgs.append( + f"dynamical structure factors were computed at {e_dyn:.0f} eV, the " + f"calculation runs at {energy_ev:.0f} eV" + ) + if k_dyn is not None and 2 * g_max_beams > k_dyn + 1e-9: + msgs.append( + f"dynamical structure factors extend to {k_dyn:.2f} 1/A but the beam " + f"list reaches {g_max_beams:.2f} 1/A, so couplings beyond " + f"{k_dyn:.2f} 1/A are missing (treated as zero); recompute with " + f"k_max >= {2 * g_max_beams:.2f}" + ) + if msgs: + _coverage_warned.add(key) + warnings.warn(f"{crystal.name}: " + "; ".join(msgs), stacklevel=3) + + +def select_dynamical_beams( + crystal: Crystal, + orientation: torch.Tensor, + energy_ev: float, + alpha_max_rad: float = 0.0, + sg_max: float = SG_MAX, + k_max: float | None = None, + deform: torch.Tensor | None = None, +) -> torch.Tensor: + """Beam list (nb, 3) hkl, 000 first, for a Bloch calculation at an + orientation and every incident direction within alpha_max_rad of the + optic axis: reflections with |s_g| < sg_max + alpha_max |g| (a tilt t + shifts s_g by at most |t| |g| / k0 to leading order). One list computed + with the largest tilt of a refinement search keeps every stage of that + search in the same truncated system.""" + lam = electron_wavelength_angstrom(energy_ev) + hkl_u, g_u = _beam_universe(crystal) + g_lab = qrotate(orientation, g_u) + if deform is not None: + g_lab = g_lab @ deform.to(torch.float64).T + g_len = torch.linalg.norm(g_lab, dim=1) + gz, g2 = g_lab[:, 2], (g_lab**2).sum(dim=1) + s0 = (2 * gz - lam * g2) / (2 - 2 * lam * gz) + sel = torch.abs(s0) < sg_max + alpha_max_rad * g_len + if k_max is not None: + sel &= g_len <= k_max + return torch.cat([torch.zeros((1, 3), dtype=torch.long), hkl_u[sel]]) + + def dynamical_pattern( crystal: Crystal, orientation: torch.Tensor, @@ -133,16 +243,21 @@ def dynamical_pattern( t = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) - # beam selection in the lab frame - g_lab = qrotate(orientation, crystal.g_vec) + # beam selection in the lab frame, from every lattice point that can + # carry dynamical intensity + hkl_u, g_u = _beam_universe(crystal) + g_lab = qrotate(orientation, g_u) gz, g2 = g_lab[:, 2], (g_lab**2).sum(dim=1) s_g = (2 * gz - lam * g2) / (2 - 2 * lam * gz) sel = torch.abs(s_g) < sg_max if k_max is not None: - sel &= crystal.g_len <= k_max - hkl_sel = crystal.hkl[sel] + sel &= torch.linalg.norm(g_lab, dim=1) <= k_max + hkl_sel = hkl_u[sel] g_sel = g_lab[sel] s_sel = s_g[sel] + _check_dynamical_factors( + crystal, energy_ev, float(torch.linalg.norm(g_sel, dim=1).max()) if g_sel.shape[0] else 0.0 + ) # beams list includes the (000) beam at index 0 hkl_beams = torch.cat([torch.zeros((1, 3), dtype=torch.long), hkl_sel]) @@ -334,6 +449,7 @@ def _cbed_amplitudes( progress_bar: bool = False, fast_absorption: bool = False, deform: torch.Tensor | None = None, + beams: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Bloch intensities of every beam at every incident tilt. @@ -349,6 +465,10 @@ def _cbed_amplitudes( (3, 3) deformation applied to the lab-frame reciprocal lattice (g' = deform @ g), for a strained cell; the structure factors are those of the ideal cell. + beams : torch.Tensor | None + Explicit beam list (nb, 3) hkl with 000 first, e.g. from + select_dynamical_beams(); when None the list is selected here from + the tilts given. Returns ------- @@ -365,20 +485,22 @@ def _cbed_amplitudes( gamma_rel = relativistic_gamma(energy_ev) t_thick = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) - # beam selection: near the Ewald sphere for ANY tilt in the aperture -- - # a tilt t shifts s_g by at most |t| * |g| / k0 to leading order - g_lab = qrotate(orientation, crystal.g_vec) + # beam selection: near the Ewald sphere for ANY tilt in the aperture + if beams is None: + alpha_max = float(torch.linalg.norm(tilts, dim=1).max()) / k0 + hkl_beams = select_dynamical_beams( + crystal, orientation, energy_ev, alpha_max, sg_max, k_max, deform + ) + else: + hkl_beams = beams + g_beams = qrotate(orientation, hkl_beams[1:].to(torch.float64) @ crystal.lat_recip) if deform is not None: - g_lab = g_lab @ deform.to(torch.float64).T - gz, g2 = g_lab[:, 2], (g_lab**2).sum(dim=1) - s0 = (2 * gz - lam * g2) / (2 - 2 * lam * gz) - alpha_max = float(torch.linalg.norm(tilts, dim=1).max()) / k0 - sel = torch.abs(s0) < sg_max + alpha_max * crystal.g_len - if k_max is not None: - sel &= crystal.g_len <= k_max - hkl_beams = torch.cat([torch.zeros((1, 3), dtype=torch.long), crystal.hkl[sel]]) - g_beams = torch.cat([torch.zeros((1, 3), dtype=torch.float64), g_lab[sel]]) + g_beams = g_beams @ deform.to(torch.float64).T + g_beams = torch.cat([torch.zeros((1, 3), dtype=torch.float64), g_beams]) nb = hkl_beams.shape[0] + _check_dynamical_factors( + crystal, energy_ev, float(torch.linalg.norm(g_beams, dim=1).max()) if nb > 1 else 0.0 + ) U, u0_imag, absorptive = _coupling_matrix(crystal, hkl_beams, gamma_rel) @@ -950,9 +1072,9 @@ def calculate_kossel_reference( dirs = dirs[keep] N = dirs.shape[0] - g = crystal.g_vec # crystal frame, orientation is identity + hkl_u, g = _beam_universe(crystal) # crystal frame, orientation is identity g2 = (g**2).sum(dim=1) - g_len = crystal.g_len + g_len = torch.linalg.norm(g, dim=1) out = torch.zeros((N, T), dtype=torch.float64) chunks = range(0, N, chunk) @@ -977,7 +1099,7 @@ def calculate_kossel_reference( sel = torch.abs(s_c) < sg_max + (radius + 1e-4) * g_len if k_max is not None: sel &= g_len <= k_max - hkl_beams = torch.cat([torch.zeros((1, 3), dtype=torch.long), crystal.hkl[sel]]) + hkl_beams = torch.cat([torch.zeros((1, 3), dtype=torch.long), hkl_u[sel]]) g_b = torch.cat([torch.zeros((1, 3), dtype=torch.float64), g[sel]]) U, u0_imag, absorptive = _coupling_matrix(crystal, hkl_beams, gamma_rel) @@ -1387,9 +1509,8 @@ def kossel_lines( # unique rows: group reflections by ray direction (g and -g together), # keep the shortest g of each as the row vector - g_all = crystal.g_vec - g_len = crystal.g_len - hkl = crystal.hkl + hkl, g_all = _beam_universe(crystal) + g_len = torch.linalg.norm(g_all, dim=1) sel = (g_len <= k_max) & (g_len > 1e-8) idx = torch.nonzero(sel).squeeze(1) idx = idx[torch.argsort(g_len[idx])] @@ -1756,57 +1877,215 @@ def overlay_kossel_segments( return ax -def dynamical_tilt_set( +def average_bloch_fourier( + crystal: Crystal, + orientation: torch.Tensor, + trial_tilts: torch.Tensor, + thicknesses_A, + energy_ev: float, + precession_deg: float, + sg_max: float = SG_MAX, + k_max: float | None = None, + deform: torch.Tensor | None = None, + beams: torch.Tensor | None = None, + n_harmonics: int = 48, + n_geometry: int = 128, + n_matrix_harmonics: int | None = None, +) -> tuple[torch.Tensor, torch.Tensor]: + """Precession-averaged Bloch intensities by harmonic propagation. + + On the precession ring the structure matrix is a Fourier series in the + azimuth, A(phi) = sum_m A_m exp(i m phi); for a ring centered on the + optic axis only m = 0, +-1 are nonzero and the coefficients are exact, + + A_0 = U + diag(2 k0 c_g + i U0''), + A_(+1) = diag[-k0 r (g_x - i g_y) / (K - g_z)], A_(-1) = conj., + + with r = k0 sin(theta_p), K = sqrt(k0^2 - r^2), c_g the ring-centered + excitation error. Expanding the wave function in azimuthal modes, + psi(phi, z) = sum_n x_n(z) exp(i n phi), the Bloch equation couples + neighboring modes, dx_n/dz = (i pi / k0) sum_m A_m x_(n-m), starting + from x_0(0) = e_000, and the ring average of the intensity is the + incoherent sum over modes, I_g = sum_n |x_(n,g)|^2, exactly (Parseval). + The mode chain is truncated at |n| <= n_harmonics (48 reproduces a + converged quadrature to 1e-15 for silicon at 600 A and 0.4 degrees; 24 + leaves 1e-7) and a uniformly spaced thickness grid comes from one + action of the matrix exponential of the block-tridiagonal generator + at all its time points. For a ring displaced by a trial tilt + the coefficients are no longer three: the exact excitation errors on + n_geometry azimuths are Fourier transformed and n_matrix_harmonics + of them kept (default n_harmonics // 3); both counts and n_harmonics + must be converged for the result to be exact. + + The absorption is the full complex matrix (there is no first-order + variant here). Cost: a sparse block matrix of size (2 n_harmonics + + 1) x nb per trial tilt and one Krylov exponential action over the + thickness grid; see the benchmark in the tests for how it compares + with the batched eigensolves of the quadrature, which reuse one + eigendecomposition for every thickness. + + Returns (intensities (M_trial, T, nb) with the direct beam first, + g_xy (nb, 2)). + """ + from scipy.sparse import csr_matrix, diags, kron + from scipy.sparse.linalg import expm_multiply + + lam = electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + gamma_rel = relativistic_gamma(energy_ev) + t_grid = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) + r = k0 * np.sin(np.deg2rad(precession_deg)) + K = np.sqrt(k0**2 - r**2) + trial = torch.atleast_2d(torch.as_tensor(trial_tilts, dtype=torch.float64)) + if beams is None: + alpha_max = (float(torch.linalg.norm(trial, dim=1).max()) + r) / k0 + beams = select_dynamical_beams( + crystal, orientation, energy_ev, alpha_max, sg_max, k_max, deform + ) + g_beams = qrotate(orientation, beams[1:].to(torch.float64) @ crystal.lat_recip) + if deform is not None: + g_beams = g_beams @ deform.to(torch.float64).T + g_beams = torch.cat([torch.zeros((1, 3), dtype=torch.float64), g_beams]).numpy() + nb = g_beams.shape[0] + U, u0_imag, _ = _coupling_matrix(crystal, beams, gamma_rel) + U_np = U.numpy() + L = int(n_harmonics) + nm = 2 * L + 1 + H = n_harmonics // 3 if n_matrix_harmonics is None else int(n_matrix_harmonics) + g2 = (g_beams**2).sum(axis=1) + + out = np.zeros((trial.shape[0], t_grid.shape[0], nb)) + for it, t0 in enumerate(trial.numpy()): + if np.hypot(*t0) < 1e-12: + den = K - g_beams[:, 2] + c = (2 * K * g_beams[:, 2] - g2) / (2 * den) + coeff = { + 0: U_np + np.diag(2 * k0 * c + 1j * u0_imag), + 1: np.diag(-k0 * r * (g_beams[:, 0] - 1j * g_beams[:, 1]) / den), + -1: np.diag(-k0 * r * (g_beams[:, 0] + 1j * g_beams[:, 1]) / den), + } + else: + Q = int(n_geometry) + phi = 2 * np.pi * np.arange(Q) / Q + t = t0[None, :] + r * np.stack([np.cos(phi), np.sin(phi)], axis=1) + kz = np.sqrt(k0**2 - (t**2).sum(1))[:, None] + s_phi = (2 * kz * g_beams[:, 2] - 2 * (t @ g_beams[:, :2].T) - g2) / ( + 2 * (kz - g_beams[:, 2]) + ) + d = np.fft.fft(2 * k0 * s_phi, axis=0) / Q + coeff = {m: np.diag(d[m % Q]) for m in range(-H, H + 1)} + coeff[0] = coeff[0] + U_np + 1j * u0_imag * np.eye(nb) + operator = csr_matrix((nm * nb, nm * nb), dtype=complex) + for m, A in coeff.items(): + if abs(m) > 2 * L: + continue + shift = diags(np.ones(nm - abs(m)), -m, shape=(nm, nm), format="csr") + operator = operator + kron(shift, csr_matrix(A), format="csr") + generator = (1j * np.pi / k0) * operator + x0 = np.zeros(nm * nb, dtype=complex) + x0[L * nb] = 1.0 + zs = t_grid.numpy() + uniform = zs.shape[0] > 1 and np.allclose(np.diff(zs), zs[1] - zs[0]) + if uniform: + x = expm_multiply( + generator, + x0, + start=float(zs[0]), + stop=float(zs[-1]), + num=zs.shape[0], + endpoint=True, + ).reshape(zs.shape[0], nm, nb) + out[it] = (np.abs(x) ** 2).sum(axis=1) + else: + for iz, z in enumerate(zs): + x = expm_multiply(z * generator, x0).reshape(nm, nb) + out[it, iz] = (np.abs(x) ** 2).sum(axis=0) + return torch.as_tensor(out), torch.as_tensor(g_beams[:, :2]) + + +def illumination_nodes( energy_ev: float, precession_deg: float = 0.0, - n_precession: int = 8, + n_precession: int = 32, semiconv_mrad: float = 0.0, - n_disk_rings: int = 2, + n_disk_radial: int = 4, + n_disk_azimuthal: int = 16, maped_tilts_deg=None, -) -> torch.Tensor: - """Incident beam tilts (M, 2) in 1/Angstroms whose Bloch intensities are - averaged to model one measured pattern: a ring for precession, a filled - disk for the convergence angle, an explicit list for MAPED, or their - combination (ring x disk). Zero tilt alone when none apply.""" + maped_weights=None, +) -> tuple[torch.Tensor, torch.Tensor]: + """Incident beam tilts (M, 2) in 1/Angstroms and their weights (M,) + whose Bloch intensities are averaged to model one measured pattern. + + Precession is a ring of radius k0 sin(theta_p) sampled uniformly in + azimuth (the Gauss-Chebyshev quadrature of the ring integral); the + convergence disk of radius k0 sin(alpha) is sampled with Gauss-Legendre + nodes in (radius / R)^2 and uniform azimuth, which integrates the + uniform-area measure exactly for polynomials (an equal-weight ring + grid gives the disk a second moment of 0.70 R^2 instead of 0.50 R^2). + MAPED is an explicit tilt list with exposure weights. Ring and disk + combine as a product measure. Zero tilt with unit weight when none + apply. + """ lam = electron_wavelength_angstrom(energy_ev) k0 = 1.0 / lam if maped_tilts_deg is not None: ring = k0 * torch.sin(torch.deg2rad(torch.as_tensor(maped_tilts_deg, dtype=torch.float64))) + if maped_weights is None: + w_ring = torch.full((ring.shape[0],), 1.0 / ring.shape[0], dtype=torch.float64) + else: + w_ring = torch.as_tensor(maped_weights, dtype=torch.float64) + w_ring = w_ring / w_ring.sum() elif precession_deg > 0: phi = torch.arange(n_precession, dtype=torch.float64) * (2 * np.pi / n_precession) r = k0 * np.sin(np.deg2rad(precession_deg)) ring = torch.stack([r * torch.cos(phi), r * torch.sin(phi)], dim=1) + w_ring = torch.full((n_precession,), 1.0 / n_precession, dtype=torch.float64) else: ring = torch.zeros((1, 2), dtype=torch.float64) + w_ring = torch.ones(1, dtype=torch.float64) if semiconv_mrad > 0: - disk = tilt_grid(semiconv_mrad, energy_ev, n_rings=n_disk_rings) + R = k0 * np.sin(semiconv_mrad * 1e-3) + x, w = np.polynomial.legendre.leggauss(n_disk_radial) + radius = R * np.sqrt((x + 1) / 2) + psi = 2 * np.pi * np.arange(n_disk_azimuthal) / n_disk_azimuthal + disk = torch.as_tensor( + (radius[:, None, None] * np.stack([np.cos(psi), np.sin(psi)], axis=1)[None]).reshape( + -1, 2 + ) + ) + w_disk = torch.as_tensor(np.repeat(w / 2 / n_disk_azimuthal, n_disk_azimuthal)) else: disk = torch.zeros((1, 2), dtype=torch.float64) - return (ring[:, None, :] + disk[None, :, :]).reshape(-1, 2) - - -def _dynamical_cost(si, sq, qxy, im, delta, min_sim_rel_p: float = 0.0): - """Intensity cost (M, T) of simulated beams (M, T, N) at positions sq - (N, 2) against measured peaks (P, 2) with intensities im (P,), with a - free scale per (tilt, thickness). Pairing is by position (within - delta); the position residuals themselves do not enter, they belong - to the deformation fit. Unpaired simulated beams weaker than - min_sim_rel_p times the strongest simulated beam (in the compared - power-law intensities) are ignored: they are the beams a detector - would not see, and with power_intensity < 1 they would otherwise - dominate the unpaired term.""" + w_disk = torch.ones(1, dtype=torch.float64) + tilts = (ring[:, None, :] + disk[None, :, :]).reshape(-1, 2) + weights = (w_ring[:, None] * w_disk[None, :]).reshape(-1) + return tilts, weights + + +def _dynamical_cost(inten, sq, qxy, im, delta, power: float, min_sim_rel: float = 0.0): + """Intensity cost (M, T) of simulated raw intensities (M, T, N) at + positions sq (N, 2) against measured peaks (P, 2) with intensities im + (P,) already raised to `power`, with a free scale per (tilt, + thickness). Both sides are compared as I ** power; the power is applied + here, after any illumination averaging, never before it. Pairing is by + position (within delta); the position residuals themselves do not + enter, they belong to the deformation fit. Unpaired simulated beams + weaker than min_sim_rel times the strongest simulated beam (a + visibility mask on the RAW intensities, so it is defined for power = + 0 as well) are ignored: they are the beams a detector would not see, + and with power < 1 they would otherwise dominate the unpaired term.""" d = torch.cdist(sq, qxy) d_min, j_min = d.min(dim=1) pair = d_min < delta if int(pair.sum()) == 0: return None, pair, j_min, d_min + visible = inten > min_sim_rel * inten.amax(dim=2, keepdim=True) + si = inten.clamp_min(0) ** power a = si[:, :, pair] b = im[j_min[pair]][None, None, :] w = ((a * b).sum(dim=2) / (a * a).sum(dim=2).clamp_min(1e-12)).clamp_min(0)[:, :, None] c_paired = (b - w * a).abs().sum(dim=2) - s_unp = si[:, :, ~pair] - strong = s_unp > min_sim_rel_p * si.amax(dim=2, keepdim=True) - c_unpaired_sim = 0.5 * w[:, :, 0] * (s_unp * strong).sum(dim=2) + c_unpaired_sim = 0.5 * w[:, :, 0] * (si[:, :, ~pair] * visible[:, :, ~pair]).sum(dim=2) matched = torch.zeros(im.shape[0], dtype=torch.bool) matched[j_min[pair]] = True # the measured direct beam is not a diffracted intensity: leave it out @@ -1831,6 +2110,11 @@ def _fit_deformation(sq, qxy, w_exp, delta): qs = sq[pair] qm = qxy[j_min[pair]] w = w_exp[j_min[pair]] * (1 - d_min[pair] / delta).clamp_min(0) + # the paired positions must span the plane: collinear pairs (one + # systematic row) leave the deformation across the row undetermined + sv = torch.linalg.svdvals(torch.sqrt(w)[:, None] * qs) + if float(sv[-1]) < 0.2 * float(sv[0]): + return None, 0.0, pair M1 = torch.einsum("p,pi,pj->ij", w, qm, qs) M2 = torch.einsum("p,pi,pj->ij", w, qs, qs) A = M1 @ torch.linalg.inv(M2 + 1e-12 * torch.eye(2, dtype=torch.float64)) @@ -1851,6 +2135,8 @@ def refine_dynamical( precession_deg: float | None = None, n_precession: int = 32, semiconv_mrad: float | None = None, + n_disk_radial: int = 4, + n_disk_azimuthal: int = 16, maped_tilts_deg=None, refine_deformation: bool = True, pair_distance: float | None = None, @@ -1862,6 +2148,8 @@ def refine_dynamical( mask: np.ndarray | None = None, fast_absorption: bool = True, update_orientations: bool = True, + require_phase_weight: bool = True, + ring_method: str = "quadrature", progress_bar: bool = True, ) -> dict: """Dynamical refinement on the Bragg vectors: orientation, thickness, @@ -1912,7 +2200,8 @@ def refine_dynamical( samples are needed at 500 A and 0.5 degrees. semiconv_mrad : float | None Convergence semiangle (inherited); the intensities are averaged - over the disk. + over the disk with n_disk_radial Gauss-Legendre radii times + n_disk_azimuthal azimuths (64 nodes by default, times the ring). maped_tilts_deg : array-like | None Explicit (M, 2) beam tilt list (degrees) for MAPED, overriding precession. @@ -1933,6 +2222,19 @@ def refine_dynamical( treated perturbatively. update_orientations : bool, default=True Write the refined quaternions back into the OrientationMaps. + require_phase_weight : bool, default=True + Refine only candidates that carried weight in the kinematical + phase fit; False refines every matched candidate, so the dynamical + pass can rescue a candidate the kinematical model rejected. + ring_method : {"quadrature", "fourier"} + How the precession ring is integrated: azimuthal quadrature + (n_precession nodes, the production path), or the exact harmonic + propagation of average_bloch_fourier() with the crystal rotated to + every trial orientation, so each ring is centered and the form is + exact. The quadrature with 48 nodes reproduces the exact result to + 1e-15 (silicon, 0.4 degrees, 600 A) at 30-50x lower cost (43 beams, + 169 trials, 78 thicknesses: 3-10 s against 160 s), so the harmonic + path is a reference, not the production setting. Returns ------- @@ -1943,7 +2245,9 @@ def refine_dynamical( reciprocal positions = A x ideal), 'cost' (R, C, F), 'cost_zero_tilt' (R, C, F) the cost at the matched orientation (its difference to 'cost' is the gain of the tilt search; a small gain means the - intensities do not constrain the tilt), 'phase_index' (R, C), + intensities do not constrain the tilt), 'quats_base' (R, C, F, 4) the + matched orientation with the in-plane rotation folded in, from which + 'tilt_deg' leads to 'quats', 'phase_index' (R, C), 'candidate' (R, C), 'thickness_per_candidate' (R, C, F). """ from quantem.diffraction.rotations import qmult, quat_from_axis_angle @@ -1972,6 +2276,8 @@ def refine_dynamical( precession_deg=precession_deg, n_precession=int(n_precession), semiconv_mrad=semiconv_mrad, + n_disk_radial=int(n_disk_radial), + n_disk_azimuthal=int(n_disk_azimuthal), maped_tilts_deg=maped_tilts_deg, refine_deformation=bool(refine_deformation), pair_distance=float(pair_distance), @@ -1981,6 +2287,8 @@ def refine_dynamical( k_max=k_max, min_number_peaks=int(min_number_peaks), fast_absorption=bool(fast_absorption), + require_phase_weight=bool(require_phase_weight), + ring_method=str(ring_method), ) if hasattr(phase_map, "metadata"): phase_map.metadata["dynamical"] = used @@ -1995,10 +2303,17 @@ def refine_dynamical( fields = peaks.fields ix = [fields.index(f) for f in ("qx", "qy", "intensity")] - ring = dynamical_tilt_set( - energy_ev, precession_deg, n_precession, semiconv_mrad, maped_tilts_deg=maped_tilts_deg - ) # (Mr, 2) + ring, w_ring = illumination_nodes( + energy_ev, + precession_deg, + n_precession, + semiconv_mrad, + n_disk_radial, + n_disk_azimuthal, + maped_tilts_deg=maped_tilts_deg, + ) # (Mr, 2), (Mr,) Mr = ring.shape[0] + alpha_ill = float(torch.linalg.norm(ring, dim=1).max()) / k0 def stage_grid(center, half, step): n = int(round(2 * half / step)) + 1 @@ -2013,6 +2328,7 @@ def stage_grid(center, half, step): tilt_out = torch.zeros((R, C, F, 2), dtype=torch.float64) quat_out = torch.zeros((R, C, F, 4), dtype=torch.float64) quat_out[..., 0] = 1.0 + quat_base = quat_out.clone() deform_out = torch.zeros((R, C, F, 2, 2), dtype=torch.float64) deform_out[..., 0, 0] = 1.0 deform_out[..., 1, 1] = 1.0 @@ -2036,7 +2352,8 @@ def stage_grid(center, half, step): if om.corr[rx, ry, m] <= 0: continue if ( - phase_map.phase_weights is not None + require_phase_weight + and phase_map.phase_weights is not None and float(phase_map.phase_weights[rx, ry, f]) <= 0 ): continue @@ -2065,6 +2382,20 @@ def stage_grid(center, half, step): deform3 = torch.eye(3, dtype=torch.float64) deform3[:2, :2] = S deform_out[rx, ry, f] = S + # one beam list for the whole search of this candidate: every + # trial center, the illumination and the deformation are inside + # its selection, so all stages compare the same truncated system + beam_list = select_dynamical_beams( + om.crystal, + q0, + energy_ev, + np.deg2rad(tilt_stages[0][0]) * np.sqrt(2) + alpha_ill, + sg_max, + k_max, + deform3, + ) + if beam_list.shape[0] < 2: + continue center = torch.zeros(2, dtype=torch.float64) best = None for half, step in tilt_stages: @@ -2079,26 +2410,71 @@ def stage_grid(center, half, step): # problem with the coupling matrix shared trial = k0 * torch.stack([w_grid[:, 1], -w_grid[:, 0]], dim=1) tilts = (trial[:, None, :] + ring[None, :, :]).reshape(-1, 2) - inten, g_xy, _ = _cbed_amplitudes( - om.crystal, - q0, - tilts, - t_grid, - energy_ev, - sg_max, - k_max, - tilt_batch=max(64, Mr * 8), - progress_bar=False, - fast_absorption=fast_absorption, - deform=deform3, - ) - inten = inten.reshape(Mt, Mr, T, -1).mean(dim=1) # (Mt, T, nb) - si = inten[:, :, 1:] ** power_intensity + if ring_method == "fourier" and precession_deg > 0 and semiconv_mrad <= 0: + # exact reference path: the crystal is rotated to every + # trial orientation, so each precession ring is centered + # and its three-coefficient harmonic form is exact; the + # positions are those of the untilted orientation + inten = [] + for wt in w_grid: + angw = float(torch.linalg.norm(wt)) + q_t = q0 + if angw > 1e-12: + q_t = qmult( + quat_from_axis_angle( + torch.tensor( + [wt[0] / angw, wt[1] / angw, 0.0], dtype=torch.float64 + ), + torch.tensor(angw, dtype=torch.float64), + ), + q0, + ) + i_t, _ = average_bloch_fourier( + om.crystal, + q_t, + torch.zeros((1, 2), dtype=torch.float64), + t_grid, + energy_ev, + precession_deg, + sg_max, + k_max, + deform=deform3, + beams=beam_list, + ) + inten.append(i_t[0]) + inten = torch.stack(inten) + g_xy = torch.cat( + [ + torch.zeros((1, 3), dtype=torch.float64), + qrotate(q0, beam_list[1:].to(torch.float64) @ om.crystal.lat_recip) + @ ( + deform3 + if deform3 is not None + else torch.eye(3, dtype=torch.float64) + ).T, + ] + )[:, :2] + else: + inten, g_xy, _ = _cbed_amplitudes( + om.crystal, + q0, + tilts, + t_grid, + energy_ev, + sg_max, + k_max, + tilt_batch=max(64, Mr * 8), + progress_bar=False, + fast_absorption=fast_absorption, + deform=deform3, + beams=beam_list, + ) + inten = (inten.reshape(Mt, Mr, T, -1) * w_ring[None, :, None, None]).sum(dim=1) sq = g_xy[1:] if sq.shape[0] == 0: break cost, pair, j_min, d_min = _dynamical_cost( - si, sq, qxy, im, delta, min_sim_intensity_rel**power_intensity + inten[:, :, 1:], sq, qxy, im, delta, power_intensity, min_sim_intensity_rel ) if cost is None: break @@ -2136,16 +2512,42 @@ def stage_grid(center, half, step): if best is None: continue c_best, t_fit, wx, wy, sq, pair, j_min, d_min = best - cost_out[rx, ry, f] = c_best - thick_out[rx, ry, f] = t_fit - tilt_out[rx, ry, f, 0] = wx - tilt_out[rx, ry, f, 1] = wy q = q0 ang = float(np.hypot(wx, wy)) if ang > 1e-12: axis = torch.tensor([wx / ang, wy / ang, 0.0], dtype=torch.float64) q = qmult(quat_from_axis_angle(axis, torch.tensor(ang, dtype=torch.float64)), q0) - + # the reported solution is the crystal rotated by the interpolated + # tilt with the beam along the foil normal: evaluate that model + # once more so the stored cost and thickness belong to the stored + # orientation (the beam-offset search geometry is paraxially, not + # exactly, equivalent to it) + inten, g_xy, _ = _cbed_amplitudes( + om.crystal, + q, + ring, + t_grid, + energy_ev, + sg_max, + k_max, + tilt_batch=max(64, Mr * 8), + progress_bar=False, + fast_absorption=fast_absorption, + deform=deform3, + beams=beam_list, + ) + inten = (inten * w_ring[:, None, None]).sum(dim=0, keepdim=True) + cost, _, _, _ = _dynamical_cost( + inten[:, :, 1:], g_xy[1:], qxy, im, delta, power_intensity, min_sim_intensity_rel + ) + if cost is not None: + t_best = int(cost[0].argmin()) + c_best, t_fit = float(cost[0, t_best]), float(t_grid[t_best]) + cost_out[rx, ry, f] = c_best + thick_out[rx, ry, f] = t_fit + tilt_out[rx, ry, f, 0] = wx + tilt_out[rx, ry, f, 1] = wy + quat_base[rx, ry, f] = q0 quat_out[rx, ry, f] = q n_maps = len(oms) @@ -2172,6 +2574,7 @@ def stage_grid(center, half, step): "deformation": deform_out, "cost": cost_out, "cost_zero_tilt": cost0_out, + "quats_base": quat_base, "phase_index": phase_index, "candidate": f_best, "thickness_per_candidate": thick_out, @@ -2322,6 +2725,8 @@ def render_pattern_image( sg_max: float = SG_MAX, k_max: float | None = None, fast_absorption: bool = True, + tilt_weights: torch.Tensor | None = None, + beams: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Dynamical diffraction pattern images: Bloch intensities (averaged over the precession / convergence tilt set `tilts`) rendered as disks on the @@ -2334,6 +2739,9 @@ def render_pattern_image( k0 = 1.0 / lam ring = torch.zeros((1, 2), dtype=torch.float64) if tilts is None else tilts Mr = ring.shape[0] + w_ring = ( + torch.full((Mr,), 1.0 / Mr, dtype=torch.float64) if tilt_weights is None else tilt_weights + ) if trial_tilts is None: trial = torch.zeros((1, 2), dtype=torch.float64) else: @@ -2352,36 +2760,67 @@ def render_pattern_image( progress_bar=False, fast_absorption=fast_absorption, deform=deform, + beams=beams, ) - inten = inten.reshape(Mt, Mr, t_grid.shape[0], -1).mean(dim=1) # (Mt, T, nb) + inten = (inten.reshape(Mt, Mr, t_grid.shape[0], -1) * w_ring[None, :, None, None]).sum(dim=1) centers = _q_to_pixels(g_xy, origin_rc, pixel_size, rotation_ccw_deg, ellipse) images = render_disks(centers, inten, shape, disk_radius_px, edge_px) return images, centers, inten def _image_cost( - meas: torch.Tensor, sims: torch.Tensor, mask: torch.Tensor, power: float + meas: torch.Tensor, + sims: torch.Tensor, + mask: torch.Tensor, + power: float, + background: str = "constant", + radius: torch.Tensor | None = None, ) -> torch.Tensor: """Normalized residual of the measured image (ny, nx) against each - simulated one (..., ny, nx). The intensity scale and a constant - background are solved by least squares in the raw domain over the - mask; the residual is then taken between the power-law images so the - weak diffracted disks weigh as they do in the Bragg-vector cost.""" + simulated one (..., ny, nx). The intensity scale and the background + are solved by least squares in the raw domain over the mask; the + residual is then taken between the power-law images so the weak + diffracted disks weigh as they do in the Bragg-vector cost. + + background : {"constant", "radial"} + A constant, or a quadratic in the distance from the direct beam + (b0 + b1 r + b2 r^2, with `radius` in pixels), the smooth + diffuse-scattering floor of a diffraction pattern. + """ m = mask.to(torch.float64) - n = m.sum().clamp_min(1) - y = meas * m - x = sims * m - sx = x.sum(dim=(-2, -1)) - sy = y.sum() - sxx = (x * x).sum(dim=(-2, -1)) - sxy = (x * y).sum(dim=(-2, -1)) - den = (n * sxx - sx * sx).clamp_min(1e-12) - a = ((n * sxy - sx * sy) / den).clamp_min(0) - b = ((sy - a * sx) / n).clamp_min(0) - model = (a[..., None, None] * sims + b[..., None, None]).clamp_min(0) ** power - yp = meas.clamp_min(0) ** power - resid = (((yp - model) * m) ** 2).sum(dim=(-2, -1)) - return resid / ((yp * m) ** 2).sum().clamp_min(1e-12) + lead = sims.shape[:-2] + sel = m.reshape(-1) > 0 + x = sims.reshape(-1, sims.shape[-2] * sims.shape[-1])[:, sel].to(torch.float64) # (K, npix) + y = meas.reshape(-1)[sel].to(torch.float64) + ones = torch.ones_like(y) + if background == "radial" and radius is not None: + r = radius.reshape(-1)[sel].to(torch.float64) + r = r / r.max().clamp_min(1e-12) + basis = torch.stack([ones, r, r * r], dim=1) # (npix, 3) + else: + basis = ones[:, None] + # normal equations for [scale, background coefficients], per image + nb = basis.shape[1] + G_bb = basis.T @ basis + G_xb = x @ basis # (K, nb) + G_xx = (x * x).sum(dim=1) + rhs_x = x @ y + rhs_b = basis.T @ y + K = x.shape[0] + A = torch.zeros((K, nb + 1, nb + 1), dtype=torch.float64) + A[:, 0, 0] = G_xx + A[:, 0, 1:] = G_xb + A[:, 1:, 0] = G_xb + A[:, 1:, 1:] = G_bb[None] + rhs = torch.cat([rhs_x[:, None], rhs_b[None].expand(K, -1)], dim=1) + A = A + 1e-12 * torch.eye(nb + 1, dtype=torch.float64)[None] + coef = torch.linalg.solve(A, rhs[:, :, None])[:, :, 0] + a = coef[:, 0].clamp_min(0) + bg = (basis @ coef[:, 1:].T).T + model = (a[:, None] * x + bg).clamp_min(0) ** power + yp = y.clamp_min(0) ** power + resid = ((yp[None] - model) ** 2).sum(dim=1) + return (resid / (yp * yp).sum().clamp_min(1e-12)).reshape(lead) def _image_mask(shape, origin, r_max_px, exclude_direct_px): @@ -2408,6 +2847,7 @@ def fit_disk_shape( power_intensity: float | None = None, r_max_px: float | None = None, exclude_direct_px: float | None = None, + background: str = "constant", sg_max: float = SG_MAX, k_max: float | None = None, fast_absorption: bool = True, @@ -2430,11 +2870,13 @@ def fit_disk_shape( power_intensity = float( resolve(power_intensity, "power_intensity", md, default=POWER_INTENSITY) ) - tilts = dynamical_tilt_set( + tilts, tilt_w = illumination_nodes( energy_ev, md.get("precession_deg", 0.0), md.get("n_precession", 32), md.get("semiconv_mrad", 0.0), + md.get("n_disk_radial", 4), + md.get("n_disk_azimuthal", 16), maped_tilts_deg=md.get("maped_tilts_deg"), ) if radii_px is None: @@ -2465,6 +2907,11 @@ def fit_disk_shape( o = origins[rx, ry] meas = torch.as_tensor(np.asarray(dataset.array[rx, ry], dtype=float)).clamp_min(0) mask = _image_mask(shape, o, r_max_px, exclude_direct_px) + radius = torch.as_tensor( + np.hypot( + *(np.mgrid[0 : shape[0], 0 : shape[1]] - np.asarray(o, dtype=float)[:, None, None]) + ) + ) _, centers, inten = render_pattern_image( om.crystal, q, @@ -2483,11 +2930,12 @@ def fit_disk_shape( sg_max, k_max, fast_absorption, + tilt_weights=tilt_w, ) for i, r in enumerate(radii_px): for j, e in enumerate(edges_px): sim = render_disks(centers, inten[0, 0], shape, float(r), float(e)) - total[i, j] += _image_cost(meas, sim, mask, power_intensity) + total[i, j] += _image_cost(meas, sim, mask, power_intensity, background, radius) k = int(total.argmin()) i, j = k // len(edges_px), k % len(edges_px) return { @@ -2516,6 +2964,7 @@ def refine_dynamical_image( power_intensity: float | None = None, r_max_px: float | None = None, exclude_direct_px: float | None = None, + background: str = "constant", mask: np.ndarray | None = None, sg_max: float = SG_MAX, k_max: float | None = None, @@ -2561,11 +3010,13 @@ def refine_dynamical_image( ) if exclude_direct_px is None: exclude_direct_px = 1.5 * disk_radius_px - tilts = dynamical_tilt_set( + tilts, tilt_w = illumination_nodes( energy_ev, md.get("precession_deg", 0.0), md.get("n_precession", 32), md.get("semiconv_mrad", 0.0), + md.get("n_disk_radial", 4), + md.get("n_disk_azimuthal", 16), maped_tilts_deg=md.get("maped_tilts_deg"), ) R, C = result["thickness"].shape @@ -2600,6 +3051,11 @@ def refine_dynamical_image( o = origins[rx, ry] meas = torch.as_tensor(np.asarray(dataset.array[rx, ry], dtype=float)).clamp_min(0) pmask = _image_mask(shape, o, r_max_px, exclude_direct_px) + radius = torch.as_tensor( + np.hypot( + *(np.mgrid[0 : shape[0], 0 : shape[1]] - np.asarray(o, dtype=float)[:, None, None]) + ) + ) t_grid = np.arange( max(thickness_step_A, t0 - thickness_half_range_A), t0 + thickness_half_range_A + 1e-6, @@ -2623,8 +3079,9 @@ def refine_dynamical_image( sg_max, k_max, fast_absorption, + tilt_weights=tilt_w, ) - cost = _image_cost(meas, images, pmask, power_intensity) # (M, T) + cost = _image_cost(meas, images, pmask, power_intensity, background, radius) # (M, T) T = len(t_grid) flat = int(cost.argmin()) m_best, t_best = flat // T, flat % T @@ -2663,6 +3120,7 @@ def refine_dynamical_image( power_intensity=float(power_intensity), r_max_px=r_max_px, exclude_direct_px=float(exclude_direct_px), + background=str(background), sg_max=float(sg_max), k_max=k_max, fast_absorption=bool(fast_absorption), diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py index 8ccbff8d6..859f57ee9 100644 --- a/src/quantem/diffraction/crystal.py +++ b/src/quantem/diffraction/crystal.py @@ -27,7 +27,6 @@ from ase import Atoms from ase.data import chemical_symbols -from quantem.core.utils.utils import electron_wavelength_angstrom from quantem.diffraction.defaults import SIGMA_EXCITATION from quantem.diffraction.rotations import qrotate, symmetry_quaternions @@ -522,6 +521,7 @@ def calculate_dynamical_structure_factors( self.g_len_dyn = g_len self.U_dyn = torch.as_tensor(U, dtype=torch.complex128) self.dyn_energy_ev = float(energy_ev) + self.dyn_k_max = float(k_max) return self def generate_pattern( @@ -531,9 +531,21 @@ def generate_pattern( sigma_excitation: float = SIGMA_EXCITATION, tol_excitation_mult: float = 3.0, k_max: float | None = None, + precession_deg: float = 0.0, + semiconv_mrad: float = 0.0, + excitation_model: str = "gaussian", + thickness_A: float | None = None, ) -> dict[str, torch.Tensor]: """Kinematical diffraction pattern for one orientation. + The intensity of each reflection is |F_g|^2 times a Gaussian + excitation envelope of width sigma_excitation, averaged exactly over + the illumination when a precession angle or a convergence + semiangle is given (quantem.diffraction.illumination): the + precession ring sweeps the excitation error of reflection g by + +- a_g = r |g_xy| / |K - g_z| about its central value c_g, and the + averaged envelope is the Bessel transform G(c_g, a_g, b_g; sigma). + Parameters ---------- orientation : torch.Tensor @@ -547,29 +559,80 @@ def generate_pattern( Include reflections with |s_g| below this multiple of sigma. k_max : float | None Optionally trim the pattern below the structure-factor k_max. + precession_deg, semiconv_mrad : float + Precession semi-angle (degrees) and convergence semiangle + (mrad) of the illumination the intensities are averaged over. + excitation_model : {"gaussian", "slab"} + "gaussian" is the empirical envelope of width sigma_excitation + used by the orientation library. "slab" is the finite-thickness + first Born rocking curve, (pi |U_g| z / k0)^2 sinc(s_g z)^2 + with U_g = gamma_rel F_g / pi, averaged over the illumination + the same way; it needs thickness_A and is the kinematical limit + of the Bloch wave calculation for thin crystals. + thickness_A : float | None + Thickness for the slab model (Angstroms). Returns ------- - dict with 'qx', 'qy', 'intensity', 'hkl', 's_g' tensors. + dict with 'qx', 'qy', 'intensity', 'hkl', 's_g' (the central + excitation error), 'a' and 'b' (ring and disk sweep amplitudes). """ if self.g_vec is None: raise RuntimeError("Run calculate_structure_factors first.") - lam = electron_wavelength_angstrom(energy_ev) - g = qrotate(orientation, self.g_vec) - gz, g2 = g[:, 2], (g**2).sum(dim=1) - s_g = (2 * gz - lam * g2) / (2 - 2 * lam * gz) - keep = torch.abs(s_g) < sigma_excitation * tol_excitation_mult - if k_max is not None: - keep &= self.g_len <= k_max - intensity = self.struct_factors_int[keep] * torch.exp( - -(s_g[keep] ** 2) / (2 * sigma_excitation**2) + from quantem.diffraction.illumination import ( + averaged_gaussian_intensity_envelope, + excitation_coefficients, + slab_envelope, ) + + g = qrotate(orientation, self.g_vec) + if excitation_model == "slab": + if thickness_A is None: + raise ValueError("the slab excitation model needs thickness_A") + c, a, b = excitation_coefficients(g, energy_ev, precession_deg, semiconv_mrad) + # the sinc^2 tails are algebraic: keep everything whose main + # lobe (width 1/z) plus illumination sweep is within the tolerance + width = tol_excitation_mult / float(thickness_A) + c_t = torch.as_tensor(c, dtype=torch.float64) + a_t = torch.as_tensor(a, dtype=torch.float64) + b_t = torch.as_tensor(b, dtype=torch.float64) + keep = torch.abs(c_t) < a_t + b_t + width + if k_max is not None: + keep &= self.g_len <= k_max + env = slab_envelope( + c[keep.numpy()], a[keep.numpy()], b[keep.numpy()], float(thickness_A) + ) + from quantem.core.utils.utils import electron_wavelength_angstrom + + lam = electron_wavelength_angstrom(energy_ev) + gamma_rel = 1.0 + float(energy_ev) / 510998.95 + u_abs = torch.abs(self.struct_factors[keep]) * (gamma_rel / np.pi) + intensity = (np.pi * u_abs * float(thickness_A) * lam) ** 2 * torch.as_tensor( + env, dtype=torch.float64 + ) + else: + env, c, a, b = averaged_gaussian_intensity_envelope( + g, energy_ev, sigma_excitation, precession_deg, semiconv_mrad + ) + c_t = torch.as_tensor(c, dtype=torch.float64) + a_t = torch.as_tensor(a, dtype=torch.float64) + b_t = torch.as_tensor(b, dtype=torch.float64) + # the full illumination support enters the selection, not only + # the central excitation error + keep = torch.abs(c_t) < a_t + b_t + sigma_excitation * tol_excitation_mult + if k_max is not None: + keep &= self.g_len <= k_max + intensity = ( + self.struct_factors_int[keep] * torch.as_tensor(env, dtype=torch.float64)[keep] + ) return { "qx": g[keep, 0], "qy": g[keep, 1], "intensity": intensity, "hkl": self.hkl[keep], - "s_g": s_g[keep], + "s_g": c_t[keep], + "a": a_t[keep], + "b": b_t[keep], } def __repr__(self) -> str: diff --git a/src/quantem/diffraction/illumination.py b/src/quantem/diffraction/illumination.py new file mode 100644 index 000000000..60949f4af --- /dev/null +++ b/src/quantem/diffraction/illumination.py @@ -0,0 +1,202 @@ +"""Illumination-averaged excitation envelopes for kinematical patterns. + +Precession rotates the incident beam on a cone and a convergent probe fills +a disk of directions; the recorded intensity of a reflection is the +average of its rocking curve over that support. For a ring centered on the +optic axis the excitation error of reflection g is exactly +s_g(phi) = c_g + a_g cos(phi - delta_g), and for a small disk it is affine +in the incident direction to leading order, so the average of a Gaussian +excitation envelope over ring and disk reduces to one scalar transform, + + G(c, a, b; sigma) = sqrt(2/pi) int_0^inf exp(-x^2/2) cos(c x / sigma) + J0(a x / sigma) jinc(b x / sigma) dx, + +with jinc(x) = 2 J1(x) / x; G(c, 0, 0) = exp(-c^2 / 2 sigma^2) recovers the +static envelope. The functions here evaluate G for whole reflection lists +at once (vectorized Gauss-Legendre quadrature of the transform, exact to +~1e-9 over the parameter range of electron diffraction), give the ring and +disk coefficients (c, a, b) from the geometry, and are shared by the +pattern simulation, the orientation library and the refinements. +""" + +from __future__ import annotations + +import numpy as np +import torch +from scipy.special import j0, j1 + +from quantem.core.utils.utils import electron_wavelength_angstrom + +_X_NODES, _X_WEIGHTS = np.polynomial.legendre.leggauss(400) +_X_MAX = 9.0 +_X = 0.5 * _X_MAX * (_X_NODES + 1) +_W = 0.5 * _X_MAX * _X_WEIGHTS * np.exp(-0.5 * _X**2) * np.sqrt(2 / np.pi) + + +def _jinc(x: np.ndarray) -> np.ndarray: + out = np.ones_like(x) + nz = np.abs(x) > 1e-12 + out[nz] = 2 * j1(x[nz]) / x[nz] + return out + + +def gaussian_envelope(c, a, b, sigma: float) -> np.ndarray: + """Illumination-averaged Gaussian excitation envelope G(c, a, b; sigma). + + Parameters + ---------- + c, a, b : array-like + Central excitation error, ring amplitude and disk amplitude of each + reflection (1/Angstroms), from `excitation_coefficients`. + sigma : float + Width of the excitation envelope (1/Angstroms). + + Returns + ------- + np.ndarray + The averaged envelope in [0, 1], same shape as c. + """ + c = np.asarray(c, dtype=float) + a = np.broadcast_to(np.asarray(a, dtype=float), c.shape) + b = np.broadcast_to(np.asarray(b, dtype=float), c.shape) + x = _X / sigma + integrand = np.cos(c[..., None] * x) * j0(a[..., None] * x) * _jinc(b[..., None] * x) + return np.clip(integrand @ _W, 0.0, 1.0) + + +def excitation_coefficients( + g_lab: torch.Tensor | np.ndarray, + energy_ev: float, + precession_deg: float = 0.0, + semiconv_mrad: float = 0.0, +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Central excitation error and illumination amplitudes of each reflection. + + For a precession ring of radius r = k0 sin(theta_p) centered on the optic + axis, with K = sqrt(k0^2 - r^2), + + c_g = (2 K g_z - |g|^2) / (2 (K - g_z)), a_g = r |g_xy| / |K - g_z|, + + which is exact: s_g(phi) = c_g + a_g cos(phi - delta_g). The convergence + disk of radius R = k0 sin(alpha) adds b_g = R |g_xy| / |K - g_z| under + the same affine model. Without illumination, c_g is the static + excitation error and a_g = b_g = 0. + + Returns (c, a, b) as float arrays (N,). + """ + g = np.asarray( + g_lab.detach().cpu().numpy() if isinstance(g_lab, torch.Tensor) else g_lab, dtype=float + ) + lam = electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + r = k0 * np.sin(np.deg2rad(precession_deg)) + R = k0 * np.sin(semiconv_mrad * 1e-3) + K = np.sqrt(k0**2 - r**2) + den = K - g[:, 2] + g2 = (g**2).sum(axis=1) + gxy = np.hypot(g[:, 0], g[:, 1]) + c = (2 * K * g[:, 2] - g2) / (2 * den) + a = r * gxy / np.abs(den) + b = R * gxy / np.abs(den) + return c, a, b + + +def averaged_gaussian_intensity_envelope( + g_lab, energy_ev: float, sigma: float, precession_deg: float = 0.0, semiconv_mrad: float = 0.0 +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Envelope, central excitation error and support half-width of every + reflection under the illumination: returns (envelope, c, a, b).""" + c, a, b = excitation_coefficients(g_lab, energy_ev, precession_deg, semiconv_mrad) + if precession_deg <= 0 and semiconv_mrad <= 0: + return np.exp(-0.5 * (c / sigma) ** 2), c, a, b + return gaussian_envelope(c, a, b, sigma), c, a, b + + +def ring_disk_quadrature(r: float, R: float, n_phi: int = 128, n_r: int = 8, n_psi: int = 32): + """Positive angular quadrature of the ring x disk illumination, the + reference against which the analytic envelope is checked: returns + in-plane tilts (M, 2) and normalized weights (M,).""" + phi = 2 * np.pi * np.arange(n_phi) / n_phi if r > 0 else np.zeros(1) + ring = r * np.column_stack((np.cos(phi), np.sin(phi))) + if R > 0: + x, w = np.polynomial.legendre.leggauss(n_r) + radius = R * np.sqrt((x + 1) / 2) + psi = 2 * np.pi * np.arange(n_psi) / n_psi + disk = (radius[:, None, None] * np.column_stack((np.cos(psi), np.sin(psi)))[None]).reshape( + -1, 2 + ) + wd = np.repeat(w / 2 / n_psi, n_psi) + else: + disk, wd = np.zeros((1, 2)), np.ones(1) + t = (ring[:, None] + disk[None]).reshape(-1, 2) + weights = np.tile(wd, len(ring)) / len(ring) + return t, weights + + +def gaussian_envelope_ring_series(c, a, sigma: float, n_terms: int = 6) -> np.ndarray: + """Ring-averaged Gaussian envelope, b = 0, by the Bessel series + + G = exp(-c^2/2 sigma^2 - v) [I0(u) I0(v) + 2 sum_n (-1)^n I_2n(u) I_n(v)], + u = c a / sigma^2, v = a^2 / 4 sigma^2, + + which converges in a few terms for v < 1 (the precession sweep of the + excitation error smaller than the envelope width, the electron + diffraction regime); larger v falls back to the transform. Accepts + numpy arrays or torch tensors of any shape (a broadcast to c).""" + from scipy.special import iv + + is_torch = isinstance(c, torch.Tensor) + c_np = np.asarray(c.detach().cpu().numpy() if is_torch else c, dtype=float) + a_np = np.broadcast_to( + np.asarray(a.detach().cpu().numpy() if isinstance(a, torch.Tensor) else a, dtype=float), + c_np.shape, + ) + u = c_np * a_np / sigma**2 + v = a_np**2 / (4 * sigma**2) + total = iv(0, u) * iv(0, v) + for n in range(1, n_terms + 1): + total = total + 2 * (-1) ** n * iv(2 * n, u) * iv(n, v) + out = np.exp(-0.5 * (c_np / sigma) ** 2 - v) * total + big = v > 1.0 + if np.any(big): + out[big] = gaussian_envelope(c_np[big], a_np[big], 0.0, sigma) + out = np.clip(out, 0.0, 1.0) + return torch.as_tensor(out, dtype=torch.float64) if is_torch else out + + +def excitation_amplitudes( + g_lab: torch.Tensor, energy_ev: float, precession_deg: float, semiconv_mrad: float +): + """Ring and disk amplitudes (a, b) as torch tensors for lab-frame g of + any leading shape (..., 3); zero tensors when no illumination.""" + lam = electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + r = k0 * np.sin(np.deg2rad(precession_deg)) + R = k0 * np.sin(semiconv_mrad * 1e-3) + K = np.sqrt(k0**2 - r**2) + den = (K - g_lab[..., 2]).abs().clamp_min(1e-12) + gxy = torch.hypot(g_lab[..., 0], g_lab[..., 1]) + return r * gxy / den, R * gxy / den + + +_V_NODES, _V_WEIGHTS = np.polynomial.legendre.leggauss(200) +_V = 0.5 * (_V_NODES + 1) +_WV = 0.5 * _V_WEIGHTS * 2 * (1 - _V) + + +def slab_envelope(c, a, b, thickness_A: float) -> np.ndarray: + """Illumination-averaged finite-thickness (first Born) rocking curve, + + S(c, a, b; z) = 2 int_0^1 (1 - v) cos(2 pi c z v) J0(2 pi a z v) + jinc(2 pi b z v) dv, + + which reduces to sinc(c z)^2 without illumination; the Born intensity + of reflection g is (pi |U_g| z / k0)^2 S. Vectorized Gauss-Legendre + quadrature over v, exact to ~1e-10 for the phase ranges of electron + diffraction (c z below ~20).""" + c = np.asarray(c, dtype=float) + a = np.broadcast_to(np.asarray(a, dtype=float), c.shape) + b = np.broadcast_to(np.asarray(b, dtype=float), c.shape) + x = 2 * np.pi * thickness_A * _V + integrand = np.cos(c[..., None] * x) * j0(a[..., None] * x) * _jinc(b[..., None] * x) + return np.clip(integrand @ _WV, 0.0, 1.0) diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index 892484eb5..4e089e192 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -183,7 +183,11 @@ def build_plan( Excitation error envelope of the library (1/Angstroms). Keep this about 2x the physical excitation tolerance: orientations halfway between sampled zones shift s_g by ~ (step/2) * k, and a wider - envelope keeps their library intensities from collapsing. + envelope keeps their library intensities from collapsing. With a + precession angle or convergence semiangle recorded by + from_vectors, the envelope of every library reflection is its + exact average over that illumination + (quantem.diffraction.illumination). power_radial, power_intensity : float Weighting prefactor q^power_radial * |V_g|^power_intensity for library peaks. power_intensity=0 matches on positions only @@ -336,6 +340,9 @@ def build_plan( self.plan_norm_shift = None self.plan_frac_shift = None self.metadata["plan"] = dict( + excitation_model="gaussian", + precession_deg=float(self.metadata.get("precession_deg", 0.0) or 0.0), + semiconv_mrad=float(self.metadata.get("semiconv_mrad", 0.0) or 0.0), angle_step_zone_axis_deg=float(angle_step_zone_axis_deg), angle_step_in_plane_deg=float(angle_step_in_plane_deg), corr_kernel_size=self.corr_kernel_size, @@ -422,8 +429,26 @@ def _build_reference(self, zone_quats: torch.Tensor) -> torch.Tensor: gz = gr[..., 2] g2 = (gr**2).sum(-1) s_g = (2 * gz - lam * g2) / (2 - 2 * lam * gz) - amp = torch.exp(-(s_g**2) / (2 * self.sigma_excitation**2)) - amp = amp * (s_g.abs() < delta * 4) + prec = float(self.metadata.get("precession_deg", 0.0) or 0.0) + conv = float(self.metadata.get("semiconv_mrad", 0.0) or 0.0) + if prec > 0 or conv > 0: + # excitation envelope averaged over the illumination + # (quantem.diffraction.illumination); the peak weighting below + # keeps the established correlation-library semantics + from quantem.diffraction.illumination import ( + excitation_amplitudes, + gaussian_envelope, + ) + + a_r, b_r = excitation_amplitudes(gr, self.energy_ev, prec, conv) + amp = torch.as_tensor( + gaussian_envelope(s_g.numpy(), a_r.numpy(), b_r.numpy(), self.sigma_excitation), + dtype=torch.float64, + ) + amp = amp * (s_g.abs() < a_r + b_r + delta * 4) + else: + amp = torch.exp(-(s_g**2) / (2 * self.sigma_excitation**2)) + amp = amp * (s_g.abs() < delta * 4) weight = ( crystal.g_len**self.power_radial * crystal.struct_factors_int**self.power_intensity @@ -830,6 +855,31 @@ def refine_orientations( g_all = self.crystal.g_vec lam = self.wavelength sigma_env = sigma_envelope if sigma_envelope is not None else sigma / 2 + prec_ill = float(self.metadata.get("precession_deg", 0.0) or 0.0) + conv_ill = float(self.metadata.get("semiconv_mrad", 0.0) or 0.0) + + def envelope(S, g_rows): + # Laue-circle envelope of the paired reflections at shifted + # excitation errors S (P, T, T), averaged over the illumination + # recorded on this map (ring: Bessel series; disk: transform) + if prec_ill <= 0 and conv_ill <= 0: + return torch.exp(-(S**2) / (2 * sigma_env**2)) + from quantem.diffraction.illumination import ( + excitation_amplitudes, + gaussian_envelope, + gaussian_envelope_ring_series, + ) + + a_r, b_r = excitation_amplitudes(g_rows, self.energy_ev, prec_ill, conv_ill) + if conv_ill <= 0: + return gaussian_envelope_ring_series(S, a_r[:, None, None], sigma_env) + return torch.as_tensor( + gaussian_envelope( + S.numpy(), a_r[:, None, None].numpy(), b_r[:, None, None].numpy(), sigma_env + ), + dtype=torch.float64, + ) + f_all = self.crystal.struct_factors_int.to(torch.float64) tg = torch.deg2rad( torch.linspace(-zone_search_deg, zone_search_deg, 17, dtype=torch.float64) @@ -901,7 +951,7 @@ def refine_single(q, q_exp, w_exp): + tg[None, :, None] * a1[:, None, None] + tg[None, None, :] * a2[:, None, None] ) - pred = f_p[:, None, None] * torch.exp(-(S**2) / (2 * sigma_env**2)) + pred = f_p[:, None, None] * envelope(S, g_sel[pair]) E = (w[:, None, None] * pred).sum(dim=0) / ( (pred**2).sum(dim=0).sqrt().clamp_min(1e-12) ) @@ -1059,6 +1109,30 @@ def _refine_batched( f_all = self.crystal.struct_factors_int.to(torch.float64) lam = self.wavelength n_tg = tg.shape[0] + prec_ill = float(self.metadata.get("precession_deg", 0.0) or 0.0) + conv_ill = float(self.metadata.get("semiconv_mrad", 0.0) or 0.0) + + def envelope(S, g_rows): + # Laue-circle envelope of the paired reflections at shifted + # excitation errors S (P, T, T), averaged over the illumination + # recorded on this map (ring: Bessel series; disk: transform) + if prec_ill <= 0 and conv_ill <= 0: + return torch.exp(-(S**2) / (2 * sigma_env**2)) + from quantem.diffraction.illumination import ( + excitation_amplitudes, + gaussian_envelope, + gaussian_envelope_ring_series, + ) + + a_r, b_r = excitation_amplitudes(g_rows, self.energy_ev, prec_ill, conv_ill) + if conv_ill <= 0: + return gaussian_envelope_ring_series(S, a_r[:, None, None], sigma_env) + return torch.as_tensor( + gaussian_envelope( + S.numpy(), a_r[:, None, None].numpy(), b_r[:, None, None].numpy(), sigma_env + ), + dtype=torch.float64, + ) # flatten measured peaks once, padded per position cells = [peaks[r, c].array for r, c in np.ndindex(R, C)] @@ -1142,7 +1216,7 @@ def _refine_batched( + tg[None, :, None] * gyf[:, None, None] - tg[None, None, :] * gxf[:, None, None] ) # (Np, T, T) - pred = ff[:, None, None] * torch.exp(-(S**2) / (2 * sigma_env**2)) + pred = ff[:, None, None] * envelope(S, g[idx_b, idx_g]) E_num = torch.zeros((B, n_tg, n_tg), dtype=torch.float64).index_add_( 0, idx_b, wf[:, None, None] * pred ) @@ -1205,6 +1279,8 @@ def _refine_batched( def generate_pattern(self, rx: int, ry: int, match: int = 0, **kwargs): """Simulated pattern for the matched orientation at (rx, ry).""" assert self.quats is not None + kwargs.setdefault("precession_deg", self.metadata.get("precession_deg", 0.0)) + kwargs.setdefault("semiconv_mrad", self.metadata.get("semiconv_mrad", 0.0)) return self.crystal.generate_pattern( self.quats[rx, ry, match], energy_ev=self.energy_ev, diff --git a/tests/diffraction/test_bloch.py b/tests/diffraction/test_bloch.py index 960219e23..c4fa06c77 100644 --- a/tests/diffraction/test_bloch.py +++ b/tests/diffraction/test_bloch.py @@ -576,3 +576,272 @@ def test_image_refinement_round_trip(): assert pm.metadata["dynamical_image"]["disk_radius_px"] == disk_r pm.apply_dynamical(res) assert pm.metadata["dynamical_applied"]["precession_deg"] == 0.0 + + +def test_coupling_lookup_cannot_alias(): + # a difference vector outside the stored factor box must come back as a + # missing factor (zero), never as another reflection's factor + from types import SimpleNamespace + + crystal = SimpleNamespace( + hkl_dyn=torch.tensor([[1, 0, 0], [-1, 0, 0], [-1, 1, 0]]), + U_dyn=torch.tensor([1, 1, 7], dtype=torch.complex128), + ) + U, _, _ = bloch._coupling_matrix(crystal, torch.tensor([[1, 0, 0], [-1, 0, 0]]), 1.0) + assert U[0, 1] == 0 and U[1, 0] == 0 + + +def test_illumination_nodes_moments(): + # the convergence disk is integrated with the uniform-area measure: the + # second moment of a disk of radius R is R^2 / 4 per axis; the ring is + # normalized and its mean vanishes + lam = bloch.electron_wavelength_angstrom(200e3) + k0 = 1.0 / lam + t, w = bloch.illumination_nodes(200e3, semiconv_mrad=5.0, n_disk_radial=3, n_disk_azimuthal=16) + R = k0 * np.sin(5e-3) + assert np.isclose(float(w.sum()), 1.0) + assert np.isclose(float((w * t[:, 0] ** 2).sum()), R**2 / 4, rtol=1e-10) + t, w = bloch.illumination_nodes(200e3, precession_deg=0.5, n_precession=16) + assert np.isclose(float(w.sum()), 1.0) and float(t.mean(0).abs().max()) < 1e-12 + assert np.allclose(torch.linalg.norm(t, dim=1).numpy(), k0 * np.sin(np.deg2rad(0.5))) + t, w = bloch.illumination_nodes(200e3) + assert t.shape == (1, 2) and float(w[0]) == 1.0 + + +def test_mean_absorption_and_forbidden_beam(): + """Pure mean absorption damps the total intensity as exp(-2 pi u0 z/k0); + a glide-forbidden reflection (Si 200) acquires intensity through double + diffraction, which requires it to be in the beam list.""" + si = _si(absorptive=True) + q = _zone_110() + z = torch.tensor([400.0, 800.0], dtype=torch.float64) + inten, g_xy, hkl = bloch._cbed_amplitudes( + si, q, torch.zeros((1, 2), dtype=torch.float64), z, 200e3, sg_max=0.06, k_max=1.0 + ) + keys = [tuple(h) for h in hkl.tolist()] + assert (0, 0, 2) in keys or (2, 0, 0) in keys or (0, 2, 0) in keys + i200 = next(i for i, h in enumerate(keys) if sorted(abs(v) for v in h) == [0, 0, 2]) + assert float(inten[0, 1, i200]) > 1e-4 # populated by multiple scattering + # mean absorption alone: strip the off-diagonal absorptive part + U, u0, absorptive = bloch._coupling_matrix(si, hkl, bloch.relativistic_gamma(200e3)) + Uel = 0.5 * (U + U.conj().T) + lam = bloch.electron_wavelength_angstrom(200e3) + k0 = 1.0 / lam + s_t = torch.zeros((1, hkl.shape[0]), dtype=torch.float64) + gl = bloch.qrotate(q, hkl[1:].to(torch.float64) @ si.lat_recip) + s_t[0, 1:] = (2 * gl[:, 2] - lam * (gl**2).sum(1)) / (2 - 2 * lam * gl[:, 2]) + inten_np = bloch._bloch_solve(Uel, u0, True, s_t, k0, z, fast_absorption=False) + total = inten_np[0].sum(dim=1).numpy() + assert np.allclose(total, np.exp(-2 * np.pi * u0 * z.numpy() / k0), rtol=1e-8) + + +def test_fourier_ring_matches_quadrature(): + """The harmonic propagation of the centered precession ring reproduces a + converged azimuthal quadrature, with the full complex coupling.""" + si = _si(absorptive=True) + q = _zone_110() + z = torch.tensor([300.0, 600.0]) + trial = torch.zeros((1, 2), dtype=torch.float64) + beams = bloch.select_dynamical_beams(si, q, 200e3, np.deg2rad(0.4), 0.06, 1.0) + ring, w = bloch.illumination_nodes(200e3, precession_deg=0.4, n_precession=96) + inten, g_xy, _ = bloch._cbed_amplitudes( + si, q, ring, z, 200e3, 0.06, 1.0, tilt_batch=128, beams=beams + ) + ref = (inten * w[:, None, None]).sum(0) + got, g2 = bloch.average_bloch_fourier(si, q, trial, z, 200e3, 0.4, 0.06, 1.0, beams=beams) + assert np.allclose(g2.numpy(), g_xy.numpy()) + assert np.allclose(got[0].numpy(), ref.numpy(), atol=1e-11, rtol=1e-9) + # a displaced ring through the sampled coefficients + trial = torch.tensor([[0.15, -0.1]], dtype=torch.float64) + inten, _, _ = bloch._cbed_amplitudes( + si, q, ring + trial, z, 200e3, 0.06, 1.0, tilt_batch=128, beams=beams + ) + ref = (inten * w[:, None, None]).sum(0) + got, _ = bloch.average_bloch_fourier( + si, q, trial, z, 200e3, 0.4, 0.06, 1.0, beams=beams, n_geometry=128 + ) + assert np.allclose(got[0].numpy(), ref.numpy(), atol=1e-9, rtol=1e-7) + + +def test_refine_dynamical_reported_cost_reproducible(): + """The stored cost and thickness belong to the stored orientation.""" + from quantem.core.datastructures.vector import Vector + from quantem.diffraction.orientation import OrientationMap + from quantem.diffraction.phase import PhaseMap + from quantem.diffraction.rotations import qnormalize + + energy_ev = 200e3 + xtl = _si(absorptive=True) + torch.manual_seed(3) + q_true = qnormalize(torch.randn(3, 4, dtype=torch.float64)) + peaks = Vector.from_shape( + (1, 3), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(3): + inten, g_xy, _ = bloch._cbed_amplitudes( + xtl, + q_true[i], + torch.zeros((1, 2), dtype=torch.float64), + torch.tensor([450.0]), + energy_ev, + 0.06, + 1.0, + progress_bar=False, + ) + inten_np = inten[0, 0, 1:].numpy() + keep = inten_np > 1e-3 * inten_np.max() + peaks[0, i] = np.column_stack([g_xy[1:].numpy()[keep], inten_np[keep]]) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=energy_ev) + om.build_plan(angle_step_zone_axis_deg=3.0, verbose=False) + om.match_orientations(progress_bar=False) + om.quats[0, :, 0] = q_true + om.corr[0, :, 0] = 1.0 + pm = PhaseMap.from_orientation_maps([om]) + pm.fit(progress_bar=False) + res = bloch.refine_dynamical( + pm, + thicknesses_A=np.arange(300, 600, 50.0), + tilt_stages=((0.1, 0.05),), + sg_max=0.06, + k_max=1.0, + progress_bar=False, + ) + for i in range(3): + if not torch.isfinite(res["cost"][0, i, 0]) or peaks[0, i].array.shape[0] < 5: + continue + q = res["quats"][0, i, 0] + d3 = torch.eye(3, dtype=torch.float64) + d3[:2, :2] = res["deformation"][0, i, 0] + beams = bloch.select_dynamical_beams( + xtl, res["quats_base"][0, i, 0], energy_ev, np.deg2rad(0.1) * np.sqrt(2), 0.06, 1.0, d3 + ) + inten, g_xy, _ = bloch._cbed_amplitudes( + xtl, + q, + torch.zeros((1, 2), dtype=torch.float64), + np.arange(300, 600, 50.0), + energy_ev, + 0.06, + 1.0, + fast_absorption=True, + deform=d3, + beams=beams, + ) + data = peaks[0, i].array + qxy = torch.as_tensor(data[:, :2]) + im = torch.as_tensor(data[:, 2]).clamp_min(0) ** 0.25 + cost, _, _, _ = bloch._dynamical_cost(inten[:, :, 1:], g_xy[1:], qxy, im, 0.05, 0.25, 0.02) + t_idx = int(np.argmin(np.abs(np.arange(300, 600, 50.0) - float(res["thickness"][0, i])))) + assert np.isclose(float(cost[0, t_idx]), float(res["cost"][0, i, 0]), rtol=1e-6, atol=1e-9) + + +def test_image_cost_radial_background(): + """A quadratic radial floor is removed by the radial background model + and biases the constant one.""" + torch.manual_seed(0) + shape = (48, 48) + yy, xx = np.mgrid[0:48, 0:48] + radius = torch.as_tensor(np.hypot(yy - 24.0, xx - 24.0)) + centers = torch.tensor([[24.0, 24.0], [30.0, 35.0], [15.0, 20.0], [36.0, 12.0]]) + inten = torch.tensor([0.8, 0.05, 0.02, 0.01], dtype=torch.float64) + sim = bloch.render_disks(centers, inten, shape, 3.0, 0.7) + sim_wrong = bloch.render_disks( + centers, inten * torch.tensor([1.0, 0.5, 2.0, 1.0]), shape, 3.0, 0.7 + ) + floor = 5.0 + 0.2 * radius - 0.004 * radius**2 + meas = 1000 * sim + floor + mask = bloch._image_mask(shape, (24.0, 24.0), None, 4.5) + c_const = bloch._image_cost(meas, torch.stack([sim, sim_wrong]), mask, 0.5, "constant") + c_rad = bloch._image_cost(meas, torch.stack([sim, sim_wrong]), mask, 0.5, "radial", radius) + assert float(c_rad[0]) < 1e-12 # exact model with the right background + assert float(c_const[0]) > 1e-4 # the constant background cannot absorb it + assert float(c_rad[1]) > float(c_rad[0]) + + +def test_refine_dynamical_with_precession_and_convergence(): + """End-to-end recovery with a precession ring and a convergence disk: + the ground truth is integrated with denser illumination nodes than + the model uses.""" + from quantem.core.datastructures.vector import Vector + from quantem.diffraction.orientation import OrientationMap + from quantem.diffraction.phase import PhaseMap + from quantem.diffraction.rotations import ( + misorientation_angle_deg, + qmult, + qnormalize, + quat_from_axis_angle, + ) + + energy_ev = 200e3 + xtl = _si(absorptive=True) + torch.manual_seed(5) + rng = np.random.default_rng(5) + N = 3 + q_true = qnormalize(torch.randn(N, 4, dtype=torch.float64)) + t_true = torch.tensor([300.0, 450.0, 600.0]) + ring, w = bloch.illumination_nodes( + energy_ev, + precession_deg=0.4, + n_precession=32, + semiconv_mrad=1.5, + n_disk_radial=3, + n_disk_azimuthal=12, + ) + peaks = Vector.from_shape( + (1, N), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(N): + inten, g_xy, _ = bloch._cbed_amplitudes( + xtl, + q_true[i], + ring, + t_true[i : i + 1], + energy_ev, + 0.06, + 1.0, + tilt_batch=256, + progress_bar=False, + ) + I_avg = (inten[:, 0, 1:] * w[:, None]).sum(0).numpy() + keep = I_avg > 1e-3 * I_avg.max() + peaks[0, i] = np.column_stack([g_xy[1:].numpy()[keep], I_avg[keep]]) + om = OrientationMap.from_vectors( + peaks, xtl, energy_ev=energy_ev, precession_deg=0.4, semiconv_mrad=1.5 + ) + om.build_plan(angle_step_zone_axis_deg=3.0, verbose=False) + om.match_orientations(progress_bar=False) + phis = rng.uniform(0, 2 * np.pi, N) + om.quats[0, :, 0] = torch.stack( + [ + qmult( + quat_from_axis_angle( + torch.tensor([np.cos(p), np.sin(p), 0.0], dtype=torch.float64), + torch.tensor(np.deg2rad(0.12), dtype=torch.float64), + ), + q_true[i], + ) + for i, p in enumerate(phis) + ] + ) + om.corr[0, :, 0] = 1.0 + pm = PhaseMap.from_orientation_maps([om]) + pm.fit(progress_bar=False) + res = bloch.refine_dynamical( + pm, + thicknesses_A=np.arange(200, 700, 25.0), + tilt_stages=((0.15, 0.05), (0.03, 0.01)), + n_precession=12, + n_disk_radial=2, + n_disk_azimuthal=6, + power_intensity=0.5, + sg_max=0.06, + k_max=1.0, + progress_bar=False, + ) + assert res["metadata"]["precession_deg"] == 0.4 and res["metadata"]["semiconv_mrad"] == 1.5 + valid = np.array([peaks[0, i].array.shape[0] >= 6 for i in range(N)]) + assert valid.sum() >= 2 + err = misorientation_angle_deg(q_true, om.quats[0, :, 0], xtl.sym_quats).numpy()[valid] + t_err = np.abs(res["thickness"][0].numpy() - t_true.numpy())[valid] + assert np.median(err) < 0.03 + assert (t_err <= 25).sum() >= valid.sum() - 1 diff --git a/tests/diffraction/test_illumination.py b/tests/diffraction/test_illumination.py new file mode 100644 index 000000000..b83bc0991 --- /dev/null +++ b/tests/diffraction/test_illumination.py @@ -0,0 +1,141 @@ +"""Illumination-averaged excitation envelopes (precession ring, convergence disk).""" + +import numpy as np +import torch +from ase.build import bulk + +from quantem.diffraction import bloch +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.illumination import ( + excitation_coefficients, + gaussian_envelope, + gaussian_envelope_ring_series, + ring_disk_quadrature, + slab_envelope, +) + + +def _quad_reference(fn, c, a, b, n_phi=256, n_r=12, n_psi=64): + """Positive angular quadrature of fn(s) over s = c + ring_x + disk_x.""" + t, w = ring_disk_quadrature(a, b, n_phi=n_phi, n_r=n_r, n_psi=n_psi) + return w @ fn(c + t[:, 0]) + + +def test_gaussian_envelope_limits_and_quadrature(): + sigma = 0.025 + c = np.linspace(-0.1, 0.1, 21) + # no illumination: the static envelope, exactly + assert np.allclose(gaussian_envelope(c, 0.0, 0.0, sigma), np.exp(-0.5 * (c / sigma) ** 2)) + # ring, disk, and both, against positive quadrature of the static envelope + for a, b in ((0.08, 0.0), (0.0, 0.02), (0.08, 0.02), (0.01, 0.005)): + for ci in (0.0, 0.03, 0.07): + ref = _quad_reference(lambda s: np.exp(-0.5 * (s / sigma) ** 2), ci, a, b) + assert abs(gaussian_envelope(ci, a, b, sigma) - ref) < 1e-9 + # the ring series agrees with the transform + assert np.allclose( + gaussian_envelope_ring_series(c, 0.009, 0.04), + gaussian_envelope(c, 0.009, 0.0, 0.04), + atol=1e-11, + ) + # torch in, torch out + out = gaussian_envelope_ring_series(torch.as_tensor(c), torch.full((21,), 0.009), 0.04) + assert isinstance(out, torch.Tensor) and out.shape == (21,) + + +def test_slab_envelope_limits_and_quadrature(): + z = 80.0 + c = np.linspace(-0.1, 0.1, 21) + assert np.allclose(slab_envelope(c, 0.0, 0.0, z), np.sinc(c * z) ** 2, atol=1e-10) + assert np.isclose(slab_envelope(np.array([0.05]), 0.0, 0.0, 0.0)[0], 1.0) + for a, b in ((0.08, 0.0), (0.0, 0.02), (0.08, 0.02)): + for ci in (0.0, 0.03): + ref = _quad_reference(lambda s: np.sinc(s * z) ** 2, ci, a, b) + assert abs(slab_envelope(np.array([ci]), a, b, z)[0] - ref) < 1e-9 + + +def test_centered_ring_coefficients_exact(): + # s_g(phi) = c_g + a_g cos(phi - delta_g) exactly for a centered ring + energy_ev, prec = 200e3, 0.5 + lam = bloch.electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + r = k0 * np.sin(np.deg2rad(prec)) + g = np.array([[0.8, 0.1, 0.02], [-0.3, 0.65, -0.015], [0.2, -0.9, 0.05]]) + c, a, b = excitation_coefficients(g, energy_ev, prec, 0.0) + assert np.all(b == 0) + phi = 2 * np.pi * (np.arange(360) + 0.3) / 360 + t = r * np.stack([np.cos(phi), np.sin(phi)], axis=1) + kz = np.sqrt(k0**2 - (t**2).sum(1))[:, None] + s = (2 * kz * g[:, 2] - 2 * (t @ g[:, :2].T) - (g**2).sum(1)) / (2 * (kz - g[:, 2])) + delta = np.arctan2(-g[:, 1], -g[:, 0]) + model = c[None] + a[None] * np.cos(phi[:, None] - delta[None]) + assert np.abs(s - model).max() < 1e-12 + # zero illumination: c is the static excitation error + c0, a0, b0 = excitation_coefficients(g, energy_ev, 0.0, 0.0) + s0 = (2 * k0 * g[:, 2] - (g**2).sum(1)) / (2 * (k0 - g[:, 2])) + assert np.allclose(c0, s0) and np.all(a0 == 0) and np.all(b0 == 0) + + +def test_slab_pattern_is_thin_bloch_limit(): + """The slab model with elastic couplings equals the Bloch calculation + for a very thin crystal (first Born), including the 000-relative + normalization, at zero and at nonzero precession.""" + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True), verbose=False) + xtl.calculate_structure_factors(k_max=2.0) # elastic Lobato factors only + torch.manual_seed(1) + from quantem.diffraction.rotations import qnormalize + + q = qnormalize(torch.randn(4, dtype=torch.float64)) + z = 6.0 + for prec in (0.0, 0.4): + pat = xtl.generate_pattern( + q, + 200e3, + tol_excitation_mult=4.0, + k_max=1.0, + precession_deg=prec, + excitation_model="slab", + thickness_A=z, + ) + ring, w = bloch.illumination_nodes(200e3, precession_deg=prec, n_precession=32) + inten, g_xy, hkl = bloch._cbed_amplitudes( + xtl, q, ring, torch.tensor([z]), 200e3, 0.3, 1.0, tilt_batch=64 + ) + dyn = (inten[:, 0, :] * w[:, None]).sum(0) + lut = {tuple(h): i for i, h in enumerate(hkl.tolist())} + common = [k for k, h in enumerate(pat["hkl"].tolist()) if tuple(h) in lut] + idx = [lut[tuple(pat["hkl"][k].tolist())] for k in common] + born = pat["intensity"].numpy()[common] + strong = born > 0.2 * born.max() + assert strong.sum() >= 4 + ratio = dyn[idx].numpy()[strong] / born[strong] + # first Born holds to a few percent for the strong reflections at + # 6 A (the remainder is the second-order multi-beam term); the + # illumination average is shared exactly by both sides + assert np.abs(ratio - 1).max() < 0.05 + + +def test_refine_batched_matches_loop(): + from quantem.core.datastructures.vector import Vector + from quantem.diffraction.orientation import OrientationMap + from quantem.diffraction.rotations import misorientation_angle_deg, qnormalize + + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.95, c=4.686), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + torch.manual_seed(2) + N = 6 + q_true = qnormalize(torch.randn(N, 4, dtype=torch.float64)) + peaks = Vector.from_shape( + (1, N), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(N): + p = xtl.generate_pattern(q_true[i], 200e3, sigma_excitation=0.02, precession_deg=0.5) + peaks[0, i] = np.stack([p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()], axis=1) + out = [] + for batched in (True, False): + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3, precession_deg=0.5) + om.build_plan(angle_step_zone_axis_deg=3.0, verbose=False) + om.match_orientations(progress_bar=False) + om.refine_orientations(batched=batched, neighbor_rescue=False, progress_bar=False) + out.append(om.quats[0, :, 0].clone()) + d = misorientation_angle_deg(out[0], out[1], xtl.sym_quats).numpy() + assert d.max() < 1e-4 diff --git a/tests/diffraction/test_orientation.py b/tests/diffraction/test_orientation.py index b83ba1f4a..5f47e0902 100644 --- a/tests/diffraction/test_orientation.py +++ b/tests/diffraction/test_orientation.py @@ -259,3 +259,67 @@ def test_metadata_inheritance(): progress_bar=False, ) assert res2["metadata"]["precession_deg"] == 0.0 and res2["metadata"]["pair_distance"] == 0.06 + + +def test_precession_envelope_matches_quadrature(): + # the analytic ring-averaged envelope equals the positive quadrature of + # the static envelope over the exact excitation errors on the ring + from quantem.diffraction.illumination import ring_disk_quadrature + from quantem.diffraction.rotations import qrotate + + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + torch.manual_seed(4) + q = qnormalize(torch.randn(4, dtype=torch.float64)) + energy_ev, sigma, prec = 200e3, 0.04, 0.6 + from quantem.core.utils.utils import electron_wavelength_angstrom + + lam = electron_wavelength_angstrom(energy_ev) + k0 = 1.0 / lam + pat = xtl.generate_pattern(q, energy_ev, sigma_excitation=sigma, precession_deg=prec) + g = qrotate(q, xtl.g_vec) + hkl_map = {tuple(h): i for i, h in enumerate(xtl.hkl.tolist())} + idx = torch.tensor([hkl_map[tuple(h)] for h in pat["hkl"].tolist()]) + gs = g[idx].numpy() + r = k0 * np.sin(np.deg2rad(prec)) + t, w = ring_disk_quadrature(r, 0.0, n_phi=256) + kz = np.sqrt(k0**2 - (t**2).sum(1))[:, None] + s = (2 * kz * gs[:, 2] - 2 * (t @ gs[:, :2].T) - (gs**2).sum(1)) / (2 * (kz - gs[:, 2])) + ref = (w[:, None] * np.exp(-0.5 * (s / sigma) ** 2)).sum(0) * xtl.struct_factors_int[ + idx + ].numpy() + assert np.allclose(pat["intensity"].numpy(), ref, rtol=1e-6, atol=1e-9) + # without precession the static envelope is recovered exactly + pat0 = xtl.generate_pattern(q, energy_ev, sigma_excitation=sigma) + s0 = pat0["s_g"].numpy() + assert np.allclose( + pat0["intensity"].numpy(), + xtl.struct_factors_int[[hkl_map[tuple(h)] for h in pat0["hkl"].tolist()]].numpy() + * np.exp(-0.5 * (s0 / sigma) ** 2), + ) + + +def test_roundtrip_with_precession(): + # library, matching and refinement with the precession-averaged + # envelope: patterns simulated with precession are recovered + torch.manual_seed(6) + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.95, c=4.686), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + N = 12 + q_true = qnormalize(torch.randn(N, 4, dtype=torch.float64)) + peaks = Vector.from_shape( + (1, N), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(N): + p = xtl.generate_pattern( + q_true[i], energy_ev=200e3, sigma_excitation=0.02, precession_deg=0.7 + ) + peaks[0, i] = np.stack([p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()], axis=1) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3, precession_deg=0.7) + om.build_plan(angle_step_zone_axis_deg=2.0, verbose=False) + om.match_orientations(progress_bar=False) + om.refine_orientations(zone_max_total_deg=1.5, progress_bar=False) + err = misorientation_angle_deg(q_true, om.quats[0, :, 0], xtl.sym_quats).numpy() + assert np.median(err) < 0.3 + assert (err < 1.5).mean() >= 0.75 + assert om.metadata["precession_deg"] == 0.7 From 9f1043aefe736e9da5bb1a3dd27b945a918a1fdf Mon Sep 17 00:00:00 2001 From: cophus Date: Wed, 9 Sep 2026 05:30:34 +0200 Subject: [PATCH 06/36] Adding diffraction widget --- src/quantem/diffraction/bloch.py | 598 ++++++++++++-------- src/quantem/diffraction/crystal.py | 151 +++-- src/quantem/diffraction/illumination.py | 33 ++ src/quantem/diffraction/orientation.py | 22 +- tests/diffraction/test_bloch.py | 144 ++++- tests/diffraction/test_crystal.py | 28 +- widget/js/colormaps.ts | 6 + widget/js/diffsim/crystal3d.ts | 328 +++++++++++ widget/js/diffsim/index.tsx | 705 ++++++++++++++++++++++++ widget/js/diffsim/math.ts | 194 +++++++ widget/js/diffsim/pattern.ts | 296 ++++++++++ widget/js/diffsim/physics.ts | 375 +++++++++++++ widget/src/quantem/widget/__init__.py | 3 +- widget/src/quantem/widget/diffsim.py | 519 +++++++++++++++++ 14 files changed, 3133 insertions(+), 269 deletions(-) create mode 100644 widget/js/diffsim/crystal3d.ts create mode 100644 widget/js/diffsim/index.tsx create mode 100644 widget/js/diffsim/math.ts create mode 100644 widget/js/diffsim/pattern.ts create mode 100644 widget/js/diffsim/physics.ts create mode 100644 widget/src/quantem/widget/diffsim.py diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py index 2f02c7855..142d73db0 100644 --- a/src/quantem/diffraction/bloch.py +++ b/src/quantem/diffraction/bloch.py @@ -2131,9 +2131,10 @@ def _fit_deformation(sq, qxy, w_exp, delta): def refine_dynamical( phase_map, thicknesses_A: np.ndarray | None = None, - tilt_stages=((0.3, 0.05), (0.06, 0.01)), + tilt_stages=((0.3, 0.1), (0.1, 0.025), (0.03, 0.006)), precession_deg: float | None = None, n_precession: int = 32, + n_precession_search: int | None = None, semiconv_mrad: float | None = None, n_disk_radial: int = 4, n_disk_azimuthal: int = 16, @@ -2149,7 +2150,10 @@ def refine_dynamical( fast_absorption: bool = True, update_orientations: bool = True, require_phase_weight: bool = True, - ring_method: str = "quadrature", + warm_start: bool = True, + neighbor_rescue: bool = True, + rescue_thickness_A: float = 100.0, + rescue_tilt_deg: float = 0.05, progress_bar: bool = True, ) -> dict: """Dynamical refinement on the Bragg vectors: orientation, thickness, @@ -2172,9 +2176,15 @@ def refine_dynamical( The intensities are far more tilt-sensitive than the positions: at 500 A the rocking curve width is ~2e-3 1/A, so a 0.05 degree tilt error is already visible in the weak beams. The default stages search - +-0.3 degrees at 0.05 and then +-0.06 at 0.01 degrees, 338 trial - orientations per candidate, and should start from orientations - refined by refine_orientations(). + +-0.3 degrees at 0.1, +-0.1 at 0.025 and +-0.03 at 0.006 degrees, 251 + trial orientations per candidate, and should start from orientations + refined by refine_orientations(). The compute goes into the Bloch + eigensolves, one per trial orientation, illumination node and + candidate; the search stages use fewer precession nodes and the + first-order absorption, and the reported solution is then evaluated + once with the full node count and the exact absorption, so the stored + cost, thickness and orientation are at full accuracy while the search + costs a fraction of a full-accuracy grid. Parameters left as None inherit from the previous stages: the pairing distance, intensity power, weak-beam cut and peak minimum from @@ -2194,10 +2204,15 @@ def refine_dynamical( Successive tilt grids, each centered on the previous optimum. precession_deg, n_precession : float | None, int Precession semi-angle (inherited from the OrientationMap) and the - number of azimuthal samples on the ring. Uniform sampling is the - Gauss-Chebyshev quadrature of the ring integral; the rocking - curves oscillate at pi t rho k0 g along the ring, so 32 or more - samples are needed at 500 A and 0.5 degrees. + number of azimuthal samples on the ring for the last stage and the + final evaluation. Uniform sampling is the Gauss-Chebyshev + quadrature of the ring integral; the rocking curves oscillate at + pi t rho k0 g along the ring, so 32 or more samples are needed at + 500 A and 0.5 degrees. + n_precession_search : int | None + Ring samples for the search stages before the last one; defaults + to half of n_precession (at least 8). The coarse stages only have + to find the basin, which the reduced sampling does. semiconv_mrad : float | None Convergence semiangle (inherited); the intensities are averaged over the disk with n_disk_radial Gauss-Legendre radii times @@ -2217,24 +2232,30 @@ def refine_dynamical( mask : np.ndarray | None (R, C) boolean; only these positions are refined. fast_absorption : bool, default=True - First-order treatment of absorption (Hermitian eigh, ~4x faster, - 0.5% rms intensity error); the tilt and thickness are never - treated perturbatively. + First-order treatment of absorption during the search (Hermitian + eigh, ~4x faster, 0.5% rms intensity error); the final evaluation + of the reported solution always uses the exact complex absorption. + The tilt and thickness are never treated perturbatively. update_orientations : bool, default=True Write the refined quaternions back into the OrientationMaps. require_phase_weight : bool, default=True Refine only candidates that carried weight in the kinematical phase fit; False refines every matched candidate, so the dynamical pass can rescue a candidate the kinematical model rejected. - ring_method : {"quadrature", "fourier"} - How the precession ring is integrated: azimuthal quadrature - (n_precession nodes, the production path), or the exact harmonic - propagation of average_bloch_fourier() with the crystal rotated to - every trial orientation, so each ring is centered and the form is - exact. The quadrature with 48 nodes reproduces the exact result to - 1e-15 (silicon, 0.4 degrees, 600 A) at 30-50x lower cost (43 beams, - 169 trials, 78 thicknesses: 3-10 s against 160 s), so the harmonic - path is a reference, not the production setting. + warm_start : bool, default=True + Start each position from the refined orientation of an already + refined neighbor (above or to the left, same candidate) and skip + the coarsest stage. Neighbors within one grain share their + orientation to well inside the fine stages, so this removes about + half of the eigensolves; the in-plane deformation and rotation are + still fit from the position's own peaks, and the final evaluation + is unchanged. + neighbor_rescue : bool, default=True + Second pass: positions whose winning solution differs from a + 4-neighbor of the same crystal by more than rescue_thickness_A or + rescue_tilt_deg are refined again from that neighbor's solution, + and the lower cost is kept. Repairs isolated wrong basins + (thickness aliases, tilt minima at a grid edge). Returns ------- @@ -2247,10 +2268,11 @@ def refine_dynamical( 'cost' is the gain of the tilt search; a small gain means the intensities do not constrain the tilt), 'quats_base' (R, C, F, 4) the matched orientation with the in-plane rotation folded in, from which - 'tilt_deg' leads to 'quats', 'phase_index' (R, C), + 'tilt_deg' leads to 'quats', 'warm_started' (R, C, F) and 'rescued' + (R, C) flags, 'phase_index' (R, C), 'candidate' (R, C), 'thickness_per_candidate' (R, C, F). """ - from quantem.diffraction.rotations import qmult, quat_from_axis_angle + from quantem.diffraction.rotations import misorientation_angle_deg, qmult, quat_from_axis_angle if thicknesses_A is None: thicknesses_A = np.arange(50.0, 2000.0, 25.0) @@ -2270,11 +2292,14 @@ def refine_dynamical( ) precession_deg = float(resolve(precession_deg, "precession_deg", om_md, default=0.0)) semiconv_mrad = float(resolve(semiconv_mrad, "semiconv_mrad", om_md, default=0.0)) + if n_precession_search is None: + n_precession_search = max(8, n_precession // 2) used = dict( thicknesses_A=np.asarray(thicknesses_A, dtype=float).tolist(), tilt_stages=[tuple(float(v) for v in st) for st in tilt_stages], precession_deg=precession_deg, n_precession=int(n_precession), + n_precession_search=int(n_precession_search), semiconv_mrad=semiconv_mrad, n_disk_radial=int(n_disk_radial), n_disk_azimuthal=int(n_disk_azimuthal), @@ -2288,7 +2313,10 @@ def refine_dynamical( min_number_peaks=int(min_number_peaks), fast_absorption=bool(fast_absorption), require_phase_weight=bool(require_phase_weight), - ring_method=str(ring_method), + warm_start=bool(warm_start), + neighbor_rescue=bool(neighbor_rescue), + rescue_thickness_A=float(rescue_thickness_A), + rescue_tilt_deg=float(rescue_tilt_deg), ) if hasattr(phase_map, "metadata"): phase_map.metadata["dynamical"] = used @@ -2314,6 +2342,16 @@ def refine_dynamical( ) # (Mr, 2), (Mr,) Mr = ring.shape[0] alpha_ill = float(torch.linalg.norm(ring, dim=1).max()) / k0 + ring_s, w_ring_s = illumination_nodes( + energy_ev, + precession_deg, + n_precession_search, + semiconv_mrad, + n_disk_radial, + n_disk_azimuthal, + maped_tilts_deg=maped_tilts_deg, + ) + Mr_s = ring_s.shape[0] def stage_grid(center, half, step): n = int(round(2 * half / step)) + 1 @@ -2332,6 +2370,169 @@ def stage_grid(center, half, step): deform_out = torch.zeros((R, C, F, 2, 2), dtype=torch.float64) deform_out[..., 0, 0] = 1.0 deform_out[..., 1, 1] = 1.0 + warm_out = torch.zeros((R, C, F), dtype=torch.bool) + rescued_out = torch.zeros((R, C), dtype=torch.bool) + + def refine_from(crystal, q_start, qxy, im, w_exp, stages): + """Search from q_start: in-plane deformation and rotation from the + positions, one beam list, the tilt stages, and the exact final + evaluation. Returns None or a dict with the solution.""" + q0 = q_start + S = None + deform3 = None + if refine_deformation: + # in-plane deformation and rotation from the positions first: + # the rotation is folded into the orientation and the + # symmetric deformation applied to the tilted cell, so the + # intensity search below sees the strained lattice and a + # pairing free of position residuals + g0 = qrotate(q0, crystal.g_vec) + near = ( + torch.abs((2 * g0[:, 2] - lam * (g0**2).sum(1)) / (2 - 2 * lam * g0[:, 2])) + < sg_max + ) + S, wz, _ = _fit_deformation(g0[near, :2], qxy, w_exp, delta) + if S is not None: + half_z = torch.tensor(wz / 2, dtype=torch.float64) + dqz = torch.stack( + [torch.cos(half_z), torch.zeros(()), torch.zeros(()), torch.sin(half_z)] + ).to(torch.float64) + q0 = qmult(dqz, q0) + deform3 = torch.eye(3, dtype=torch.float64) + deform3[:2, :2] = S + # one beam list for the whole search of this candidate: every + # trial center, the illumination and the deformation are inside + # its selection, so all stages compare the same truncated system + beam_list = select_dynamical_beams( + crystal, + q0, + energy_ev, + np.deg2rad(stages[0][0]) * np.sqrt(2) + alpha_ill, + sg_max, + k_max, + deform3, + ) + if beam_list.shape[0] < 2: + return None + center = torch.zeros(2, dtype=torch.float64) + best = None + cost0 = float("nan") + n_stages = len(stages) + for i_stage, (half, step) in enumerate(stages): + half = np.deg2rad(half) + step = np.deg2rad(step) + w_grid, n, tg = stage_grid(center, half, step) + Mt = w_grid.shape[0] + last = i_stage == n_stages - 1 + nodes, w_nodes, M_nodes = (ring, w_ring, Mr) if last else (ring_s, w_ring_s, Mr_s) + # crystal tilt (wx, wy) about the in-plane axes shifts s_g by + # wx g_y - wy g_x; the same excitation errors come from a beam + # tilt k0 (wy, -wx) in the fixed-normal Bloch geometry, so every + # trial orientation is a full re-solve of the Bloch problem + # with the coupling matrix shared + trial = k0 * torch.stack([w_grid[:, 1], -w_grid[:, 0]], dim=1) + tilts = (trial[:, None, :] + nodes[None, :, :]).reshape(-1, 2) + inten, g_xy, _ = _cbed_amplitudes( + crystal, + q0, + tilts, + t_grid, + energy_ev, + sg_max, + k_max, + tilt_batch=max(64, M_nodes * 8), + progress_bar=False, + fast_absorption=fast_absorption, + deform=deform3, + beams=beam_list, + ) + inten = (inten.reshape(Mt, M_nodes, T, -1) * w_nodes[None, :, None, None]).sum(dim=1) + sq = g_xy[1:] + if sq.shape[0] == 0: + break + cost, pair, j_min, d_min = _dynamical_cost( + inten[:, :, 1:], sq, qxy, im, delta, power_intensity, min_sim_intensity_rel + ) + if cost is None: + break + flat = int(cost.argmin()) + m_best, t_best = flat // T, flat % T + i_b, j_b = m_best // n, m_best % n + if i_stage == 0: + # untilted reference: the best thickness at the start + # orientation, for the gain the tilt search achieves + m0 = int(((w_grid**2).sum(1)).argmin()) + cost0 = float(cost[m0].min()) + cost_t = cost[:, t_best].reshape(n, n) + wx, wy = float(w_grid[m_best, 0]), float(w_grid[m_best, 1]) + if 0 < i_b < n - 1: + c0, c1, c2 = cost_t[i_b - 1, j_b], cost_t[i_b, j_b], cost_t[i_b + 1, j_b] + den = float(c0 - 2 * c1 + c2) + if den > 1e-12: + wx += 0.5 * float(c0 - c2) / den * step + if 0 < j_b < n - 1: + c0, c1, c2 = cost_t[i_b, j_b - 1], cost_t[i_b, j_b], cost_t[i_b, j_b + 1] + den = float(c0 - 2 * c1 + c2) + if den > 1e-12: + wy += 0.5 * float(c0 - c2) / den * step + center = torch.tensor([wx, wy], dtype=torch.float64) + best = (float(cost[m_best, t_best]), float(t_grid[t_best]), wx, wy) + if best is None: + return None + c_best, t_fit, wx, wy = best + q = q0 + ang = float(np.hypot(wx, wy)) + if ang > 1e-12: + axis = torch.tensor([wx / ang, wy / ang, 0.0], dtype=torch.float64) + q = qmult(quat_from_axis_angle(axis, torch.tensor(ang, dtype=torch.float64)), q0) + # the reported solution is the crystal rotated by the interpolated + # tilt with the beam along the foil normal: evaluate that model once + # more, with the full illumination and the exact absorption, so the + # stored cost and thickness belong to the stored orientation at full + # accuracy (the beam-offset search geometry is paraxially, not + # exactly, equivalent to it) + inten, g_xy, _ = _cbed_amplitudes( + crystal, + q, + ring, + t_grid, + energy_ev, + sg_max, + k_max, + tilt_batch=max(64, Mr * 8), + progress_bar=False, + fast_absorption=False, + deform=deform3, + beams=beam_list, + ) + inten = (inten * w_ring[:, None, None]).sum(dim=0, keepdim=True) + cost, _, _, _ = _dynamical_cost( + inten[:, :, 1:], g_xy[1:], qxy, im, delta, power_intensity, min_sim_intensity_rel + ) + if cost is not None: + t_best = int(cost[0].argmin()) + c_best, t_fit = float(cost[0, t_best]), float(t_grid[t_best]) + return dict(cost=c_best, t=t_fit, wx=wx, wy=wy, q=q, q0=q0, S=S, cost0=cost0) + + def store(rx, ry, f, sol): + cost_out[rx, ry, f] = sol["cost"] + thick_out[rx, ry, f] = sol["t"] + tilt_out[rx, ry, f, 0] = sol["wx"] + tilt_out[rx, ry, f, 1] = sol["wy"] + quat_base[rx, ry, f] = sol["q0"] + quat_out[rx, ry, f] = sol["q"] + if sol["S"] is not None: + deform_out[rx, ry, f] = sol["S"] + if np.isfinite(sol["cost0"]): + cost0_out[rx, ry, f] = sol["cost0"] + + def peaks_at(rx, ry): + data = peaks[rx, ry].array + if data.shape[0] < min_number_peaks: + return None + qxy = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) + im = torch.as_tensor(data[:, ix[2]], dtype=torch.float64).clamp_min(0) ** power_intensity + return qxy, im, im / im.max().clamp_min(1e-12) iterator = list(np.ndindex(R, C)) if mask is not None: @@ -2339,14 +2540,10 @@ def stage_grid(center, half, step): if progress_bar: iterator = tqdm(iterator, desc="dynamical refinement") for rx, ry in iterator: - data = peaks[rx, ry].array - if data.shape[0] < min_number_peaks: + pk = peaks_at(rx, ry) + if pk is None: continue - qxy = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) - im = torch.as_tensor(data[:, ix[2]], dtype=torch.float64).clamp_min(0) - im = im**power_intensity - w_exp = im / im.max().clamp_min(1e-12) - + qxy, im, w_exp = pk for f, (i_om, m) in enumerate(cands): om = oms[i_om] if om.corr[rx, ry, m] <= 0: @@ -2357,198 +2554,75 @@ def stage_grid(center, half, step): and float(phase_map.phase_weights[rx, ry, f]) <= 0 ): continue - q0 = om.quats[rx, ry, m] - quat_out[rx, ry, f] = q0 - S = None - deform3 = None - if refine_deformation: - # in-plane deformation and rotation from the positions first: - # the rotation is folded into the orientation and the - # symmetric deformation applied to the tilted cell, so the - # intensity search below sees the strained lattice and a - # pairing free of position residuals - g0 = qrotate(q0, om.crystal.g_vec) - near = ( - torch.abs((2 * g0[:, 2] - lam * (g0**2).sum(1)) / (2 - 2 * lam * g0[:, 2])) - < sg_max - ) - S, wz, _ = _fit_deformation(g0[near, :2], qxy, w_exp, delta) - if S is not None: - half_z = torch.tensor(wz / 2, dtype=torch.float64) - dqz = torch.stack( - [torch.cos(half_z), torch.zeros(()), torch.zeros(()), torch.sin(half_z)] - ).to(torch.float64) - q0 = qmult(dqz, q0) - deform3 = torch.eye(3, dtype=torch.float64) - deform3[:2, :2] = S - deform_out[rx, ry, f] = S - # one beam list for the whole search of this candidate: every - # trial center, the illumination and the deformation are inside - # its selection, so all stages compare the same truncated system - beam_list = select_dynamical_beams( - om.crystal, - q0, - energy_ev, - np.deg2rad(tilt_stages[0][0]) * np.sqrt(2) + alpha_ill, - sg_max, - k_max, - deform3, - ) - if beam_list.shape[0] < 2: - continue - center = torch.zeros(2, dtype=torch.float64) - best = None - for half, step in tilt_stages: - half = np.deg2rad(half) - step = np.deg2rad(step) - w_grid, n, tg = stage_grid(center, half, step) - Mt = w_grid.shape[0] - # crystal tilt (wx, wy) about the in-plane axes shifts s_g by - # wx g_y - wy g_x; the same excitation errors come from a - # beam tilt k0 (wy, -wx) in the fixed-normal Bloch geometry, - # so every trial orientation is a full re-solve of the Bloch - # problem with the coupling matrix shared - trial = k0 * torch.stack([w_grid[:, 1], -w_grid[:, 0]], dim=1) - tilts = (trial[:, None, :] + ring[None, :, :]).reshape(-1, 2) - if ring_method == "fourier" and precession_deg > 0 and semiconv_mrad <= 0: - # exact reference path: the crystal is rotated to every - # trial orientation, so each precession ring is centered - # and its three-coefficient harmonic form is exact; the - # positions are those of the untilted orientation - inten = [] - for wt in w_grid: - angw = float(torch.linalg.norm(wt)) - q_t = q0 - if angw > 1e-12: - q_t = qmult( - quat_from_axis_angle( - torch.tensor( - [wt[0] / angw, wt[1] / angw, 0.0], dtype=torch.float64 - ), - torch.tensor(angw, dtype=torch.float64), - ), - q0, - ) - i_t, _ = average_bloch_fourier( - om.crystal, - q_t, - torch.zeros((1, 2), dtype=torch.float64), - t_grid, - energy_ev, - precession_deg, - sg_max, - k_max, - deform=deform3, - beams=beam_list, - ) - inten.append(i_t[0]) - inten = torch.stack(inten) - g_xy = torch.cat( - [ - torch.zeros((1, 3), dtype=torch.float64), - qrotate(q0, beam_list[1:].to(torch.float64) @ om.crystal.lat_recip) - @ ( - deform3 - if deform3 is not None - else torch.eye(3, dtype=torch.float64) - ).T, - ] - )[:, :2] - else: - inten, g_xy, _ = _cbed_amplitudes( - om.crystal, - q0, - tilts, - t_grid, - energy_ev, - sg_max, - k_max, - tilt_batch=max(64, Mr * 8), - progress_bar=False, - fast_absorption=fast_absorption, - deform=deform3, - beams=beam_list, + q_start = om.quats[rx, ry, m] + quat_out[rx, ry, f] = q_start + stages = tilt_stages + if warm_start and len(tilt_stages) > 1: + # an already refined neighbor of the same candidate (raster + # order: above or to the left) is a start inside the fine + # stages' reach; its solution costs one coarse stage less + for nr, nc in ((rx - 1, ry), (rx, ry - 1)): + if nr < 0 or nc < 0 or not torch.isfinite(cost_out[nr, nc, f]): + continue + # same grain: the kinematically matched orientations of + # the two positions agree within the coarse stage + miso = float( + misorientation_angle_deg( + om.quats[rx, ry, m][None], + om.quats[nr, nc, m][None], + om.crystal.sym_quats, + )[0] ) - inten = (inten.reshape(Mt, Mr, T, -1) * w_ring[None, :, None, None]).sum(dim=1) - sq = g_xy[1:] - if sq.shape[0] == 0: - break - cost, pair, j_min, d_min = _dynamical_cost( - inten[:, :, 1:], sq, qxy, im, delta, power_intensity, min_sim_intensity_rel - ) - if cost is None: - break - flat = int(cost.argmin()) - m_best, t_best = flat // T, flat % T - i_b, j_b = m_best // n, m_best % n - if half == np.deg2rad(tilt_stages[0][0]): - # untilted reference: the best thickness at the matched - # orientation, for the gain the tilt search achieves - m0 = int(((w_grid**2).sum(1)).argmin()) - cost0_out[rx, ry, f] = float(cost[m0].min()) - cost_t = cost[:, t_best].reshape(n, n) - wx, wy = float(w_grid[m_best, 0]), float(w_grid[m_best, 1]) - if 0 < i_b < n - 1: - c0, c1, c2 = cost_t[i_b - 1, j_b], cost_t[i_b, j_b], cost_t[i_b + 1, j_b] - den = float(c0 - 2 * c1 + c2) - if den > 1e-12: - wx += 0.5 * float(c0 - c2) / den * step - if 0 < j_b < n - 1: - c0, c1, c2 = cost_t[i_b, j_b - 1], cost_t[i_b, j_b], cost_t[i_b, j_b + 1] - den = float(c0 - 2 * c1 + c2) - if den > 1e-12: - wy += 0.5 * float(c0 - c2) / den * step - center = torch.tensor([wx, wy], dtype=torch.float64) - best = ( - float(cost[m_best, t_best]), - float(t_grid[t_best]), - wx, - wy, - sq, - pair, - j_min, - d_min, + if miso < tilt_stages[0][0]: + q_start = quat_out[nr, nc, f] + stages = tilt_stages[1:] + warm_out[rx, ry, f] = True + break + sol = refine_from(om.crystal, q_start, qxy, im, w_exp, stages) + if sol is None: + continue + store(rx, ry, f, sol) + + if neighbor_rescue and len(tilt_stages) > 1: + cost_f0 = torch.nan_to_num(cost_out, nan=torch.inf) + f_win = cost_f0.argmin(dim=-1) + done = torch.isfinite(cost_out).any(dim=-1) + rescue_list = [] + for rx, ry in np.ndindex(R, C): + if not done[rx, ry]: + continue + f = int(f_win[rx, ry]) + i_om = cands[f][0] + starts = [] + for nr, nc in ((rx - 1, ry), (rx + 1, ry), (rx, ry - 1), (rx, ry + 1)): + if not (0 <= nr < R and 0 <= nc < C) or not done[nr, nc]: + continue + fn = int(f_win[nr, nc]) + if cands[fn][0] != i_om: + continue + dt = abs(float(thick_out[nr, nc, fn]) - float(thick_out[rx, ry, f])) + dtilt = float( + torch.rad2deg(torch.linalg.norm(tilt_out[nr, nc, fn] - tilt_out[rx, ry, f])) ) - if best is None: + if dt > rescue_thickness_A or dtilt > rescue_tilt_deg: + starts.append((nr, nc, fn)) + if starts: + rescue_list.append((rx, ry, f, starts)) + it = tqdm(rescue_list, desc="neighbor rescue") if progress_bar else rescue_list + for rx, ry, f, starts in it: + pk = peaks_at(rx, ry) + if pk is None: continue - c_best, t_fit, wx, wy, sq, pair, j_min, d_min = best - q = q0 - ang = float(np.hypot(wx, wy)) - if ang > 1e-12: - axis = torch.tensor([wx / ang, wy / ang, 0.0], dtype=torch.float64) - q = qmult(quat_from_axis_angle(axis, torch.tensor(ang, dtype=torch.float64)), q0) - # the reported solution is the crystal rotated by the interpolated - # tilt with the beam along the foil normal: evaluate that model - # once more so the stored cost and thickness belong to the stored - # orientation (the beam-offset search geometry is paraxially, not - # exactly, equivalent to it) - inten, g_xy, _ = _cbed_amplitudes( - om.crystal, - q, - ring, - t_grid, - energy_ev, - sg_max, - k_max, - tilt_batch=max(64, Mr * 8), - progress_bar=False, - fast_absorption=fast_absorption, - deform=deform3, - beams=beam_list, - ) - inten = (inten * w_ring[:, None, None]).sum(dim=0, keepdim=True) - cost, _, _, _ = _dynamical_cost( - inten[:, :, 1:], g_xy[1:], qxy, im, delta, power_intensity, min_sim_intensity_rel - ) - if cost is not None: - t_best = int(cost[0].argmin()) - c_best, t_fit = float(cost[0, t_best]), float(t_grid[t_best]) - cost_out[rx, ry, f] = c_best - thick_out[rx, ry, f] = t_fit - tilt_out[rx, ry, f, 0] = wx - tilt_out[rx, ry, f, 1] = wy - quat_base[rx, ry, f] = q0 - quat_out[rx, ry, f] = q + qxy, im, w_exp = pk + crystal = oms[cands[f][0]].crystal + for nr, nc, fn in starts: + sol = refine_from(crystal, quat_out[nr, nc, fn], qxy, im, w_exp, tilt_stages[1:]) + if sol is not None and sol["cost"] < float(cost_out[rx, ry, f]) - 1e-9: + cost0_keep = float(cost0_out[rx, ry, f]) + store(rx, ry, f, sol) + if np.isfinite(cost0_keep): + cost0_out[rx, ry, f] = cost0_keep + rescued_out[rx, ry] = True n_maps = len(oms) cost_f = torch.nan_to_num(cost_out, nan=torch.inf) @@ -2575,6 +2649,8 @@ def stage_grid(center, half, step): "cost": cost_out, "cost_zero_tilt": cost0_out, "quats_base": quat_base, + "warm_started": warm_out, + "rescued": rescued_out, "phase_index": phase_index, "candidate": f_best, "thickness_per_candidate": thick_out, @@ -2582,6 +2658,88 @@ def stage_grid(center, half, step): } +def dynamical_maps(result: dict, phase_map, crystal_index: int | None = None) -> dict: + """Maps of the winning candidate of a refine_dynamical() result. + + Returns 'thickness' (A), 'tilt_deg' (magnitude of the tilt correction), + 'gain' (cost at the start orientation minus the final cost), 'cost', + 'phase_index', 'mask' (positions refined, and of the given crystal when + crystal_index is set), 'quats' (R, C, 4), 'deformation' (R, C, 2, 2) + and 'strain', the crystal-frame strain components of + strain_crystal_frame(); unrefined or masked positions are NaN. + """ + cand = result["candidate"] + R, C = cand.shape + idx4 = cand[..., None, None] + quats = torch.gather(result["quats"], 2, idx4.expand(R, C, 1, 4)).squeeze(2) + deform = torch.gather( + result["deformation"], 2, cand[..., None, None, None].expand(R, C, 1, 2, 2) + ).squeeze(2) + cost = torch.gather(result["cost"], 2, cand[..., None]).squeeze(-1) + cost0 = torch.gather(result["cost_zero_tilt"], 2, cand[..., None]).squeeze(-1) + mask = torch.isfinite(cost) + if crystal_index is not None: + i_om = torch.tensor([c[0] for c in phase_map.candidates]) + mask &= i_om[cand] == crystal_index + nan = torch.full((R, C), torch.nan, dtype=torch.float64) + strain = strain_crystal_frame(deform, quats) + out = { + "thickness": torch.where(mask, result["thickness"], nan), + "tilt_deg": torch.where(mask, torch.linalg.norm(result["tilt_deg"], dim=-1), nan), + "gain": torch.where(mask, cost0 - cost, nan), + "cost": torch.where(mask, cost, nan), + "phase_index": result["phase_index"], + "mask": mask, + "quats": quats, + "deformation": deform, + "strain": {k: torch.where(mask, v, nan) for k, v in strain.items() if k != "eps_crystal"}, + } + return out + + +def plot_dynamical_maps( + maps: dict, + scalebar=None, + thickness_range_A: tuple[float, float] = (0.0, 2000.0), + tilt_range_deg: tuple[float, float] = (0.0, 0.3), + gain_range: tuple[float, float] = (0.0, 0.05), + axsize: tuple[float, float] = (4.0, 4.0), +): + """Thickness, tilt correction, gain of the tilt search and final cost + of a dynamical refinement, from dynamical_maps().""" + from quantem.core.visualization import show_2d + + imgs = [ + [np.nan_to_num(maps["thickness"].numpy()), np.nan_to_num(maps["tilt_deg"].numpy())], + [np.nan_to_num(maps["gain"].numpy()), np.nan_to_num(maps["cost"].numpy())], + ] + cmax = ( + float(np.nanmax(maps["cost"].numpy())) if np.isfinite(maps["cost"].numpy()).any() else 1.0 + ) + return show_2d( + imgs, + title=[["thickness (A)", "tilt correction (deg)"], ["gain of the tilt search", "cost"]], + cmap=[["viridis", "magma"], ["magma", "gray_r"]], + cbar=True, + norm=[ + [ + { + "interval_type": "manual", + "vmin": thickness_range_A[0], + "vmax": thickness_range_A[1], + }, + {"interval_type": "manual", "vmin": tilt_range_deg[0], "vmax": tilt_range_deg[1]}, + ], + [ + {"interval_type": "manual", "vmin": gain_range[0], "vmax": gain_range[1]}, + {"interval_type": "manual", "vmin": 0.0, "vmax": cmax}, + ], + ], + scalebar=scalebar, + axsize=axsize, + ) + + def strain_crystal_frame(deformation: torch.Tensor, quats: torch.Tensor) -> dict: """Strain tensor components in the crystal Cartesian frame. diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py index 859f57ee9..a8eefd9da 100644 --- a/src/quantem/diffraction/crystal.py +++ b/src/quantem/diffraction/crystal.py @@ -188,7 +188,8 @@ def __init__( atoms: Atoms, name: str | None = None, symprec: float = 1e-4, - pseudo_symmetry_tol: float | None = 0.1, + pseudo_symmetry_tol: float | None = 0.01, + pseudo_symmetry_intensity_tol: float = 0.05, verbose: bool = True, ): """ @@ -196,18 +197,29 @@ def __init__( ---------- symprec : float, default=1e-4 spglib tolerance (Angstroms) for the cell's own symmetry. - pseudo_symmetry_tol : float | None, default=0.1 - Tolerance (Angstroms) at which the symmetry is re-detected for - orientation matching. Cells within this distance of a higher - symmetry (a few percent of strain on a 5 Angstrom cell) are - matched with the parent group, so variants no experiment can - separate are never sampled as distinct orientations. The - library builders warn when this differs from the cell's own - symmetry; pass None to match with the exact symmetry. + pseudo_symmetry_tol : float | None, default=0.01 + Dimensionless distance tolerance for the symmetry used in + orientation matching: a fraction of the shortest lattice vector + within which atoms and lattice vectors are allowed to deviate + from a higher-symmetry parent (a 4 A cell with an atom at + (0.5, 0.5, 0.50001) is body centered at any tolerance above + 1e-5). Cells within it are matched with the parent group, so + variants no experiment can separate are never sampled as + distinct orientations; the library builders warn when the + matching group differs from the cell's own. None matches with + the exact symmetry. + pseudo_symmetry_intensity_tol : float, default=0.05 + Dimensionless intensity tolerance of the same decision: the + extra operations of the parent group must map the kinematical + intensities of the reflections they relate onto each other + within this fraction of the strongest reflection, otherwise the + patterns are distinguishable and the parent group is rejected. """ self.atoms = atoms self.name = name if name is not None else atoms.get_chemical_formula() self._pseudo_symmetry_tol = pseudo_symmetry_tol + self._pseudo_symmetry_intensity_tol = float(pseudo_symmetry_intensity_tol) + self.pseudo_symmetry_report: dict = {} self._wedge_cache: torch.Tensor | None | str = "unset" self.lat_real = torch.as_tensor(atoms.cell[:], dtype=torch.float64) @@ -216,7 +228,7 @@ def __init__( occupancy = atoms.arrays.get("occupancy", np.ones(len(atoms))) self.occupancy = torch.as_tensor(np.asarray(occupancy, dtype=float)) - self._setup_symmetry(symprec, pseudo_symmetry_tol) + self._setup_symmetry(symprec, pseudo_symmetry_tol, pseudo_symmetry_intensity_tol) if verbose: print(self.symmetry_summary()) @@ -249,18 +261,42 @@ def lat_recip(self) -> torch.Tensor: """Reciprocal lattice vectors as rows, no 2*pi factor.""" return torch.linalg.inv(self.lat_real).T - def _setup_symmetry(self, symprec: float, pseudo_symmetry_tol: float | None) -> None: + def _quick_intensities(self, k_max: float = 1.2) -> tuple[torch.Tensor, torch.Tensor]: + """Kinematical |F|^2 of every reflection with |g| <= k_max (hkl, I), + for the pseudo-symmetry intensity check; no thermal factors.""" + recip = self.lat_recip + k_len = torch.linalg.norm(recip, dim=1) + n_max = torch.ceil(k_max / k_len * 2).to(torch.long) + ranges = [torch.arange(-int(n), int(n) + 1) for n in n_max] + hkl = torch.cartesian_prod(*ranges).to(torch.float64) + g_vec = hkl @ recip + g_len = torch.linalg.norm(g_vec, dim=1) + keep = (g_len <= k_max) & (g_len > 0) + hkl, g_len = hkl[keep], g_len[keep] + f_e = electron_scattering_factor(self.numbers, g_len) + phase = torch.exp(-2j * np.pi * (self.positions_frac @ hkl.T)) + F = (f_e * self.occupancy[:, None] * phase).sum(dim=0) / self.volume + return hkl.to(torch.long), torch.abs(F) ** 2 + + def _setup_symmetry( + self, symprec: float, pseudo_symmetry_tol: float | None, intensity_tol: float + ) -> None: """Detect the true symmetry group, and optionally a pseudo-symmetry group. The true group (at `symprec`) is stored for reporting and refinement. - When `pseudo_symmetry_tol` is set, the symmetry is re-detected at that - looser tolerance: nearly-degenerate cells (e.g. an orthorhombic cell - with a = 4.000, b = 4.001, c = 4.002 Angstroms) are idealized to their - higher-symmetry parent, and *matching* uses that group --- orientations - that no experiment could distinguish are never sampled separately. + The pseudo-symmetry group is detected at a distance tolerance of + `pseudo_symmetry_tol` times the shortest lattice vector and kept + only if its extra operations relate reflections of equal kinematical + intensity to within `intensity_tol` of the strongest reflection: + two orientations are merged only when no experiment could tell + their patterns apart, in position or in intensity. Matching uses + that group, so nearly-degenerate cells are idealized to their + higher-symmetry parent. """ import spglib + from quantem.diffraction.rotations import quat_to_matrix + cell = ( self.lat_real.numpy(), self.positions_frac.numpy(), @@ -273,23 +309,53 @@ def _setup_symmetry(self, symprec: float, pseudo_symmetry_tol: float | None) -> self.laue_group: str = _LAUE_CLASS.get(pg, "-1") self.sym_quats = symmetry_quaternions(dataset.rotations, self.lat_real.numpy()) - ds_pseudo = None - if pseudo_symmetry_tol is not None and pseudo_symmetry_tol > symprec: - try: - ds_pseudo = spglib.get_symmetry_dataset(cell, symprec=pseudo_symmetry_tol) - except Exception: - ds_pseudo = None - if ds_pseudo is not None: - pg_pseudo = spglib.get_pointgroup(ds_pseudo.rotations)[0].strip() - self.pointgroup_matching: str = pg_pseudo - self.laue_group_matching: str = _LAUE_CLASS.get(pg_pseudo, "-1") - self.sym_quats_matching = symmetry_quaternions( - ds_pseudo.rotations, self.lat_real.numpy() - ) - else: - self.pointgroup_matching = pg - self.laue_group_matching = self.laue_group - self.sym_quats_matching = self.sym_quats + self.pointgroup_matching = pg + self.laue_group_matching = self.laue_group + self.sym_quats_matching = self.sym_quats + if pseudo_symmetry_tol is None: + return + a_min = float(torch.linalg.norm(self.lat_real, dim=1).min()) + symprec_pseudo = float(pseudo_symmetry_tol) * a_min + self.pseudo_symmetry_report = {"distance_A": symprec_pseudo} + if symprec_pseudo <= symprec: + return + try: + ds_pseudo = spglib.get_symmetry_dataset(cell, symprec=symprec_pseudo) + except Exception: + ds_pseudo = None + if ds_pseudo is None: + return + pg_pseudo = spglib.get_pointgroup(ds_pseudo.rotations)[0].strip() + quats_pseudo = symmetry_quaternions(ds_pseudo.rotations, self.lat_real.numpy()) + if quats_pseudo.shape[0] <= self.sym_quats.shape[0]: + return + + # intensity check on the extra operations: |F|^2 of every reflection + # against |F|^2 of its image, relative to the strongest reflection + hkl, inten = self._quick_intensities() + lut = {tuple(h): i for i, h in enumerate(hkl.tolist())} + g = hkl.to(torch.float64) @ self.lat_recip + i_max = float(inten.max()) + Rs = quat_to_matrix(quats_pseudo) + Rs_true = quat_to_matrix(self.sym_quats) + worst = 0.0 + for R in Rs: + if any(float((R - Rt).abs().max()) < 1e-6 for Rt in Rs_true): + continue + g_img = g @ R.T + hkl_img = torch.round(g_img @ self.lat_real.T).to(torch.long) + idx = torch.tensor([lut.get(tuple(h), -1) for h in hkl_img.tolist()]) + ok = idx >= 0 + diff = (inten[ok] - inten[idx[ok]]).abs() / i_max + worst = max(worst, float(diff.max()) if ok.any() else 0.0) + self.pseudo_symmetry_report["intensity_mismatch"] = worst + self.pseudo_symmetry_report["candidate"] = pg_pseudo + if worst > intensity_tol: + self.pseudo_symmetry_report["rejected"] = True + return + self.pointgroup_matching = pg_pseudo + self.laue_group_matching = _LAUE_CLASS.get(pg_pseudo, "-1") + self.sym_quats_matching = quats_pseudo def zone_axis_wedge(self) -> torch.Tensor | None: """Fundamental zone-axis wedge corners (3, 3) Cartesian, or None. @@ -335,7 +401,9 @@ def matching_symmetry_warning(self) -> str | None: f"{self.name}: orientation libraries are built with the " f"pseudo-symmetry point group {self.pointgroup_matching} (Laue " f"class {self.laue_group_matching}, found at pseudo_symmetry_tol = " - f"{self._pseudo_symmetry_tol:g} A), while the cell's own symmetry " + f"{self._pseudo_symmetry_tol:g} of the shortest lattice vector, " + f"intensities matching within {self.pseudo_symmetry_report.get('intensity_mismatch', 0.0):.1%}), " + f"while the cell's own symmetry " f"is {self.pointgroup} (Laue class {self.laue_group}). Orientations " f"related by the extra operations give the same library entry, so " f"the {n_extra} variants they generate are reported as one and the " @@ -363,9 +431,18 @@ def symmetry_summary(self) -> str: "-- used for orientation matching", ] elif self._pseudo_symmetry_tol is not None: - lines += [ - f" pseudo-symmetry none found at tol = {self._pseudo_symmetry_tol:g} A", - ] + rep = self.pseudo_symmetry_report + if rep.get("rejected"): + lines += [ + f" pseudo-symmetry {rep['candidate']} within {self._pseudo_symmetry_tol:g} of " + f"the lattice, rejected: intensities differ by " + f"{rep['intensity_mismatch']:.1%} (tol {self._pseudo_symmetry_intensity_tol:.0%})", + ] + else: + lines += [ + f" pseudo-symmetry none found at tol = {self._pseudo_symmetry_tol:g} " + f"({rep.get('distance_A', 0.0):.3f} A)", + ] else: lines += [" pseudo-symmetry not checked (set pseudo_symmetry_tol)"] # matching line reflects the symmetry actually used, after any diff --git a/src/quantem/diffraction/illumination.py b/src/quantem/diffraction/illumination.py index 60949f4af..af6fa9d5b 100644 --- a/src/quantem/diffraction/illumination.py +++ b/src/quantem/diffraction/illumination.py @@ -200,3 +200,36 @@ def slab_envelope(c, a, b, thickness_A: float) -> np.ndarray: x = 2 * np.pi * thickness_A * _V integrand = np.cos(c[..., None] * x) * j0(a[..., None] * x) * _jinc(b[..., None] * x) return np.clip(integrand @ _WV, 0.0, 1.0) + + +def gaussian_envelope_ring_torch(c: torch.Tensor, a: torch.Tensor, sigma: float) -> torch.Tensor: + """Ring-averaged Gaussian envelope (b = 0) in torch, for the refinement + loops: the Bessel series of gaussian_envelope_ring_series truncated + after the I_4 term, with I_n(u) from the I_0 / I_1 recurrences (and + their small-argument series where the recurrence would cancel). Below + v = a^2 / 4 sigma^2 = 0.3 the truncation error is under 1e-5; larger + sweeps fall back to the reference series.""" + c = c.to(torch.float64) + a = torch.as_tensor(a, dtype=torch.float64) + u = c * a / sigma**2 + v = a * a / (4 * sigma**2) + i0u = torch.special.i0(u) + i1u = torch.special.i1(u) + small2 = u.abs() < 1e-3 + u_safe = torch.where(small2, torch.ones_like(u), u) + i2u = torch.where(small2, u * u / 8, i0u - 2 * i1u / u_safe) + small4 = u.abs() < 5e-2 + i3u = torch.where(small4, u**3 / 48, i1u - 4 * i2u / u_safe) + i4u = torch.where(small4, u**4 / 384, i2u - 6 * i3u / u_safe) + i0v = torch.special.i0(v) + i1v = torch.special.i1(v) + smallv = v < 1e-3 + v_safe = torch.where(smallv, torch.ones_like(v), v) + i2v = torch.where(smallv, v * v / 8, i0v - 2 * i1v / v_safe) + out = torch.exp(-0.5 * (c / sigma) ** 2 - v) * (i0u * i0v - 2 * i2u * i1v + 2 * i4u * i2v) + big = v > 0.3 + if bool(big.any()): + out = out.clone() + ref = gaussian_envelope_ring_series(c[big], torch.broadcast_to(a, c.shape)[big], sigma) + out[big] = ref.to(out.dtype) + return out.clamp(0.0, 1.0) diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index 4e089e192..b27404030 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -438,13 +438,19 @@ def _build_reference(self, zone_quats: torch.Tensor) -> torch.Tensor: from quantem.diffraction.illumination import ( excitation_amplitudes, gaussian_envelope, + gaussian_envelope_ring_torch, ) a_r, b_r = excitation_amplitudes(gr, self.energy_ev, prec, conv) - amp = torch.as_tensor( - gaussian_envelope(s_g.numpy(), a_r.numpy(), b_r.numpy(), self.sigma_excitation), - dtype=torch.float64, - ) + if conv <= 0: + amp = gaussian_envelope_ring_torch(s_g, a_r, self.sigma_excitation) + else: + amp = torch.as_tensor( + gaussian_envelope( + s_g.numpy(), a_r.numpy(), b_r.numpy(), self.sigma_excitation + ), + dtype=torch.float64, + ) amp = amp * (s_g.abs() < a_r + b_r + delta * 4) else: amp = torch.exp(-(s_g**2) / (2 * self.sigma_excitation**2)) @@ -867,12 +873,12 @@ def envelope(S, g_rows): from quantem.diffraction.illumination import ( excitation_amplitudes, gaussian_envelope, - gaussian_envelope_ring_series, + gaussian_envelope_ring_torch, ) a_r, b_r = excitation_amplitudes(g_rows, self.energy_ev, prec_ill, conv_ill) if conv_ill <= 0: - return gaussian_envelope_ring_series(S, a_r[:, None, None], sigma_env) + return gaussian_envelope_ring_torch(S, a_r[:, None, None], sigma_env) return torch.as_tensor( gaussian_envelope( S.numpy(), a_r[:, None, None].numpy(), b_r[:, None, None].numpy(), sigma_env @@ -1121,12 +1127,12 @@ def envelope(S, g_rows): from quantem.diffraction.illumination import ( excitation_amplitudes, gaussian_envelope, - gaussian_envelope_ring_series, + gaussian_envelope_ring_torch, ) a_r, b_r = excitation_amplitudes(g_rows, self.energy_ev, prec_ill, conv_ill) if conv_ill <= 0: - return gaussian_envelope_ring_series(S, a_r[:, None, None], sigma_env) + return gaussian_envelope_ring_torch(S, a_r[:, None, None], sigma_env) return torch.as_tensor( gaussian_envelope( S.numpy(), a_r[:, None, None].numpy(), b_r[:, None, None].numpy(), sigma_env diff --git a/tests/diffraction/test_bloch.py b/tests/diffraction/test_bloch.py index c4fa06c77..e37f382b5 100644 --- a/tests/diffraction/test_bloch.py +++ b/tests/diffraction/test_bloch.py @@ -723,7 +723,7 @@ def test_refine_dynamical_reported_cost_reproducible(): energy_ev, 0.06, 1.0, - fast_absorption=True, + fast_absorption=False, deform=d3, beams=beams, ) @@ -831,6 +831,7 @@ def test_refine_dynamical_with_precession_and_convergence(): thicknesses_A=np.arange(200, 700, 25.0), tilt_stages=((0.15, 0.05), (0.03, 0.01)), n_precession=12, + n_precession_search=12, n_disk_radial=2, n_disk_azimuthal=6, power_intensity=0.5, @@ -845,3 +846,144 @@ def test_refine_dynamical_with_precession_and_convergence(): t_err = np.abs(res["thickness"][0].numpy() - t_true.numpy())[valid] assert np.median(err) < 0.03 assert (t_err <= 25).sum() >= valid.sum() - 1 + + +def _smooth_map_setup(n, tilt_start_deg, corrupt=None): + """A 1 x n 'map' of one grain: orientations a few hundredths of a degree + apart, thickness varying slowly, strained cell; starts tilted by + tilt_start_deg (and one position by `corrupt` degrees).""" + from quantem.core.datastructures.vector import Vector + from quantem.diffraction.orientation import OrientationMap + from quantem.diffraction.phase import PhaseMap + from quantem.diffraction.rotations import qmult, quat_from_axis_angle + + energy_ev = 200e3 + xtl = _si(absorptive=True) + torch.manual_seed(7) + rng = np.random.default_rng(7) + # a well-populated pattern: 1.5 degrees off the [110] zone axis + base = qmult( + quat_from_axis_angle( + torch.tensor([0.6, 0.8, 0.0], dtype=torch.float64), + torch.tensor(np.deg2rad(1.5), dtype=torch.float64), + ), + _zone_110(), + ) + q_true = torch.stack( + [ + qmult( + quat_from_axis_angle( + torch.tensor([1.0, 0.3, 0.0], dtype=torch.float64) / np.hypot(1, 0.3), + torch.tensor(np.deg2rad(0.03 * i), dtype=torch.float64), + ), + base, + ) + for i in range(n) + ] + ) + t_true = torch.tensor([400.0 + 25.0 * i for i in range(n)]) + A_true = torch.tensor([[1.008, 0.002], [0.002, 0.996]], dtype=torch.float64) + deform3 = torch.eye(3, dtype=torch.float64) + deform3[:2, :2] = A_true + peaks = Vector.from_shape( + (1, n), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(n): + inten, g_xy, _ = bloch._cbed_amplitudes( + xtl, + q_true[i], + torch.zeros((1, 2), dtype=torch.float64), + t_true[i : i + 1], + energy_ev, + 0.06, + 1.0, + progress_bar=False, + deform=deform3, + ) + inten_np = inten[0, 0, 1:].numpy() + keep = inten_np > 1e-3 * inten_np.max() + peaks[0, i] = np.column_stack([g_xy[1:].numpy()[keep], inten_np[keep]]) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=energy_ev) + om.build_plan(angle_step_zone_axis_deg=3.0, verbose=False) + om.match_orientations(progress_bar=False) + phis = rng.uniform(0, 2 * np.pi, n) + starts = [] + for i, p in enumerate(phis): + ang = tilt_start_deg if (corrupt is None or i != corrupt[0]) else corrupt[1] + starts.append( + qmult( + quat_from_axis_angle( + torch.tensor([np.cos(p), np.sin(p), 0.0], dtype=torch.float64), + torch.tensor(np.deg2rad(ang), dtype=torch.float64), + ), + q_true[i], + ) + ) + om.quats[0, :, 0] = torch.stack(starts) + om.corr[0, :, 0] = 1.0 + pm = PhaseMap.from_orientation_maps([om]) + pm.fit(progress_bar=False) + return xtl, om, pm, q_true, t_true, A_true + + +def test_refine_dynamical_warm_start_matches_cold(): + from quantem.diffraction.rotations import misorientation_angle_deg + + n = 5 + kw = dict( + thicknesses_A=np.arange(300, 700, 25.0), + tilt_stages=((0.25, 0.05), (0.04, 0.01)), + power_intensity=0.5, + sg_max=0.06, + k_max=1.0, + neighbor_rescue=False, + progress_bar=False, + ) + out = {} + for warm in (False, True): + xtl, om, pm, q_true, t_true, A_true = _smooth_map_setup(n, 0.1) + res = bloch.refine_dynamical(pm, warm_start=warm, **kw) + out[warm] = (res, om.quats[0, :, 0].clone(), q_true, t_true, xtl) + res_c, q_c, q_true, t_true, xtl = out[False] + res_w, q_w, _, _, _ = out[True] + assert not res_c["warm_started"].any() + assert res_w["warm_started"][0, 1:, 0].all() and not res_w["warm_started"][0, 0, 0] + # same solution from both routes, both correct + d = misorientation_angle_deg(q_c, q_w, xtl.sym_quats).numpy() + assert d.max() < 0.03 + assert np.allclose(res_c["thickness"][0].numpy(), res_w["thickness"][0].numpy()) + err = misorientation_angle_deg(q_true, q_w, xtl.sym_quats).numpy() + assert err.max() < 0.03 + assert np.abs(res_w["thickness"][0].numpy() - t_true.numpy()).max() <= 25 + + +def test_refine_dynamical_neighbor_rescue(): + from quantem.diffraction.rotations import misorientation_angle_deg + + n = 5 + # position 2 starts 0.45 degrees off: outside the coarse stage's reach, + # so its cold search settles in a wrong basin; its neighbors are right + xtl, om, pm, q_true, t_true, A_true = _smooth_map_setup(n, 0.1, corrupt=(2, 0.45)) + kw = dict( + thicknesses_A=np.arange(300, 700, 25.0), + tilt_stages=((0.25, 0.05), (0.04, 0.01)), + power_intensity=0.5, + sg_max=0.06, + k_max=1.0, + warm_start=False, + progress_bar=False, + ) + res = bloch.refine_dynamical(pm, neighbor_rescue=False, **kw) + err0 = misorientation_angle_deg(q_true, om.quats[0, :, 0], xtl.sym_quats).numpy() + assert err0[2] > 0.1 # the cold start fails there + xtl, om, pm, q_true, t_true, A_true = _smooth_map_setup(n, 0.1, corrupt=(2, 0.45)) + res = bloch.refine_dynamical(pm, neighbor_rescue=True, **kw) + err1 = misorientation_angle_deg(q_true, om.quats[0, :, 0], xtl.sym_quats).numpy() + assert bool(res["rescued"][0, 2]) + assert err1[2] < 0.03 + assert abs(float(res["thickness"][0, 2]) - float(t_true[2])) <= 25 + # map-level outputs + maps = bloch.dynamical_maps(res, pm, crystal_index=0) + assert maps["mask"][0].all() + assert set(maps["strain"]) == {"aa", "bb", "cc", "ab", "ac", "bc"} + assert torch.isfinite(maps["gain"][0]).all() diff --git a/tests/diffraction/test_crystal.py b/tests/diffraction/test_crystal.py index 4a9426eb4..a6183b8e4 100644 --- a/tests/diffraction/test_crystal.py +++ b/tests/diffraction/test_crystal.py @@ -44,7 +44,7 @@ def test_ring_positions(ti_beta): def test_pseudo_symmetry(): ortho = Atoms("Au", positions=[[0, 0, 0]], cell=[4.000, 4.001, 4.002], pbc=True) exact = Crystal.from_ase(ortho, pseudo_symmetry_tol=None) - pseudo = Crystal.from_ase(ortho, pseudo_symmetry_tol=0.01) + pseudo = Crystal.from_ase(ortho, pseudo_symmetry_tol=0.01) # 0.04 A on a 4 A cell assert exact.pointgroup_matching == "mmm" assert pseudo.pointgroup_matching == "m-3m" assert pseudo.sym_quats_matching.shape[0] == 24 @@ -173,8 +173,9 @@ def test_wedge_follows_cell_setting(): def test_pseudo_symmetry_default_and_warning(): ortho = Atoms("Au", positions=[[0, 0, 0]], cell=[4.000, 4.001, 4.002], pbc=True) - xtl = Crystal.from_ase(ortho, verbose=False) # default tolerance 0.1 A + xtl = Crystal.from_ase(ortho, verbose=False) # default tolerance 1% of the cell assert xtl.pointgroup == "mmm" and xtl.pointgroup_matching == "m-3m" + assert xtl.pseudo_symmetry_report["intensity_mismatch"] < 0.05 msg = xtl.matching_symmetry_warning() assert msg is not None and "pseudo_symmetry_tol=None" in msg # the pseudo group's operators are exact rotations (orthonormalized), so @@ -183,3 +184,26 @@ def test_pseudo_symmetry_default_and_warning(): assert int(hits.min()) >= 1 exact = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True), verbose=False) assert exact.matching_symmetry_warning() is None + + +def test_pseudo_symmetry_dimensionless_and_intensity_check(): + # an almost body-centered cell: the center atom 0.002 A off (0.5, 0.5, 0.5) + # is body centered at the default tolerance, and its 100/010/001 + # patterns are identical within any measurable intensity + almost_bcc = Atoms( + "Fe2", scaled_positions=[[0, 0, 0], [0.5, 0.5, 0.5005]], cell=[4.0, 4.0, 4.0], pbc=True + ) + xtl = Crystal.from_ase(almost_bcc, verbose=False) + assert xtl.pointgroup_matching == "m-3m" + assert xtl.pseudo_symmetry_report["intensity_mismatch"] < 1e-3 + # the same cell at an unmeasurably tight distance tolerance keeps its + # own (lower) symmetry; the tolerance is a fraction of the lattice + tight = Crystal.from_ase(almost_bcc, pseudo_symmetry_tol=1e-7, verbose=False) + assert tight.pointgroup_matching == tight.pointgroup + # a candidate whose intensities do not match within the intensity + # tolerance is rejected and the cell keeps its own symmetry + strict = Crystal.from_ase(almost_bcc, pseudo_symmetry_intensity_tol=1e-9, verbose=False) + assert strict.pseudo_symmetry_report.get("candidate") == "m-3m" + assert strict.pseudo_symmetry_report.get("rejected") is True + assert strict.pointgroup_matching == strict.pointgroup + assert "rejected" in strict.symmetry_summary() diff --git a/widget/js/colormaps.ts b/widget/js/colormaps.ts index 40a940b27..a7df441ce 100644 --- a/widget/js/colormaps.ts +++ b/widget/js/colormaps.ts @@ -29,6 +29,12 @@ const COLORMAP_POINTS: Record = { [255, 0, 0], [255, 255, 0], [0, 255, 0], [0, 255, 255], [0, 0, 255], [255, 0, 255], [255, 0, 0], ], + turbo_black: [ + [0, 0, 0], [21, 18, 47], [48, 57, 135], [71, 110, 230], [69, 138, 252], [56, 165, 251], + [37, 192, 231], [24, 215, 202], [32, 234, 172], [63, 246, 138], [105, 253, 102], [146, 255, 71], + [177, 249, 54], [205, 236, 52], [229, 217, 56], [246, 195, 58], [254, 167, 50], [252, 135, 37], + [244, 102, 23], [231, 73, 12], [212, 51, 5], [188, 32, 2], [158, 16, 1], [122, 4, 3], + ], turbo: [ [48, 18, 59], [69, 55, 161], [66, 107, 230], [30, 162, 230], [29, 212, 169], [79, 241, 89], [175, 240, 32], [244, 195, 12], diff --git a/widget/js/diffsim/crystal3d.ts b/widget/js/diffsim/crystal3d.ts new file mode 100644 index 000000000..2c4800c4d --- /dev/null +++ b/widget/js/diffsim/crystal3d.ts @@ -0,0 +1,328 @@ +/** + * Orthographic renderer of the unit cell (or a block of cells) on a 2D + * canvas. The view looks along the lab z axis (the beam), so the drawing + * shares its axes with the diffraction pattern; viewX = -1 mirrors x for the + * view from the detector side. Optional coordination polyhedra are convex + * hulls of the nearest neighbours of each centre atom. + */ + +import { Quat, Vec3, matVec, quatToMatrix } from "./math"; +import type { CrystalData } from "./physics"; + +export interface CellStyle { + dark: boolean; + showAxes: boolean; + showLabels: boolean; + atomScale: number; // covalent radius multiplier + viewX: number; // +1 gun side (beam into the screen), -1 detector side (beam toward the viewer) +} + +interface Atom { + pos: Vec3; // Cartesian, crystal frame, relative to the block centre + color: string; + radius: number; + symbol: string; +} + +interface Face { + verts: Vec3[]; // polygon, crystal frame relative to the block centre + color: string; +} + +function rgb(c: number[]): string { + return `rgb(${Math.round(c[0] * 255)},${Math.round(c[1] * 255)},${Math.round(c[2] * 255)})`; +} + +export interface CellGeometry { + atoms: Atom[]; + corners: Vec3[]; // 8 corners of the block relative to the centre + edges: [number, number][]; + innerEdges: [Vec3, Vec3][]; // cell boundaries inside the block + faces: Face[]; + axes: Vec3[]; // a, b, c (one cell) from the origin corner + origin: Vec3; + radius: number; // bounding radius, A +} + +/** + * Geometry of an na x nb x nc block of cells. Atoms on the block boundary + * are repeated (the corner atoms of fcc all appear). Polyhedra are drawn + * around every species except the most numerous one (cations in an oxide; + * every atom of an elemental crystal), using neighbours within + * 1.2 (r_i + r_j) of the covalent radii, including atoms outside the block. + */ +export function cellGeometry(c: CrystalData, nCells: [number, number, number] = [1, 1, 1], polyhedra = false): CellGeometry { + const cell = c.cell; + const [na, nb, nc] = nCells.map((n) => Math.max(1, Math.min(6, Math.round(n)))) as [number, number, number]; + const cart = (f: number[]): Vec3 => [ + f[0] * cell[0][0] + f[1] * cell[1][0] + f[2] * cell[2][0], + f[0] * cell[0][1] + f[1] * cell[1][1] + f[2] * cell[2][1], + f[0] * cell[0][2] + f[1] * cell[1][2] + f[2] * cell[2][2], + ]; + const center = cart([na / 2, nb / 2, nc / 2]); + const rel = (v: Vec3): Vec3 => [v[0] - center[0], v[1] - center[1], v[2] - center[2]]; + const corners: Vec3[] = []; + for (let i = 0; i < 8; i++) corners.push(rel(cart([(i & 1) * na, ((i >> 1) & 1) * nb, ((i >> 2) & 1) * nc]))); + const edges: [number, number][] = []; + for (let i = 0; i < 8; i++) for (let j = i + 1; j < 8; j++) { + const d = i ^ j; + if (d === 1 || d === 2 || d === 4) edges.push([i, j]); + } + const innerEdges: [Vec3, Vec3][] = []; + const n3 = [na, nb, nc]; + for (let ax = 0; ax < 3; ax++) { + const [u, v] = [(ax + 1) % 3, (ax + 2) % 3]; + for (let iu = 0; iu <= n3[u]; iu++) for (let iv = 0; iv <= n3[v]; iv++) { + const onBoundary = (iu === 0 || iu === n3[u]) && (iv === 0 || iv === n3[v]); + if (onBoundary) continue; + const f0 = [0, 0, 0], f1 = [0, 0, 0]; + f0[u] = iu; f0[v] = iv; f1[u] = iu; f1[v] = iv; f1[ax] = n3[ax]; + innerEdges.push([rel(cart(f0)), rel(cart(f1))]); + } + } + + // atoms inside the block (boundary included) and a halo of images for neighbour search + const eps = 1e-4; + const atoms: Atom[] = []; + const halo: { pos: Vec3; species: number }[] = []; + const inBlock: boolean[] = []; + const nSpec = c.positions_frac.length; + for (let n = 0; n < nSpec; n++) { + const f = c.positions_frac[n].map((x) => x - Math.floor(x + eps)); + for (let sx = -1; sx <= na + 1; sx++) for (let sy = -1; sy <= nb + 1; sy++) for (let sz = -1; sz <= nc + 1; sz++) { + const g = [f[0] + sx, f[1] + sy, f[2] + sz]; + const inside = g[0] <= na + eps && g[1] <= nb + eps && g[2] <= nc + eps && g[0] >= -eps && g[1] >= -eps && g[2] >= -eps; + const p = rel(cart(g)); + halo.push({ pos: p, species: n }); + inBlock.push(inside); + if (inside) atoms.push({ pos: p, color: rgb(c.colors[n]), radius: c.radii[n], symbol: c.symbols[n] }); + } + } + + const faces: Face[] = []; + if (polyhedra) { + const counts = new Map(); + for (const s of c.symbols) counts.set(s, (counts.get(s) || 0) + 1); + const species = [...counts.keys()]; + let centres = new Set(species); + if (species.length > 1) { + const most = species.reduce((a, b) => ((counts.get(a) || 0) >= (counts.get(b) || 0) ? a : b)); + centres = new Set(species.filter((s) => s !== most)); + } + const cutoffMax = 1.2 * 2 * Math.max(...c.radii); + for (let i = 0; i < halo.length; i++) { + if (!inBlock[i]) continue; + const si = halo[i].species; + if (!centres.has(c.symbols[si])) continue; + const pi = halo[i].pos; + const nbr: Vec3[] = []; + for (let j = 0; j < halo.length; j++) { + if (j === i) continue; + const sj = halo[j].species; + if (species.length > 1 && c.symbols[sj] === c.symbols[si]) continue; + const pj = halo[j].pos; + const d = Math.hypot(pj[0] - pi[0], pj[1] - pi[1], pj[2] - pi[2]); + if (d > cutoffMax) continue; + if (d <= 1.2 * (c.radii[si] + c.radii[sj])) nbr.push(pj); + } + if (nbr.length >= 4 && nbr.length <= 14) { + for (const poly of convexHullFaces(nbr, pi)) faces.push({ verts: poly, color: rgb(c.colors[si]) }); + } + } + } + + let radius = 0; + for (const p of corners) radius = Math.max(radius, Math.hypot(p[0], p[1], p[2])); + return { + atoms, corners, edges, innerEdges, faces, + axes: [cell[0] as Vec3, cell[1] as Vec3, cell[2] as Vec3], origin: corners[0], radius: radius + 0.5, + }; +} + +/** Faces of the convex hull of a few points (brute force, n <= 14), as outward-ordered polygons. */ +function convexHullFaces(pts: Vec3[], inside: Vec3): Vec3[][] { + const n = pts.length; + const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; + const cross = (a: Vec3, b: Vec3): Vec3 => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; + const dot = (a: Vec3, b: Vec3) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + const planes = new Map }>(); + let scale = 0; + for (const p of pts) scale = Math.max(scale, Math.hypot(...sub(p, inside))); + const tol = 1e-3 * scale; + for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) for (let k = j + 1; k < n; k++) { + let nrm = cross(sub(pts[j], pts[i]), sub(pts[k], pts[i])); + const L = Math.hypot(...nrm); + if (L < 1e-9) continue; + nrm = [nrm[0] / L, nrm[1] / L, nrm[2] / L]; + if (dot(nrm, sub(pts[i], inside)) < 0) nrm = [-nrm[0], -nrm[1], -nrm[2]]; + const off = dot(nrm, pts[i]); + let ok = true; + const on: number[] = []; + for (let m = 0; m < n; m++) { + const d = dot(nrm, pts[m]) - off; + if (d > tol) { ok = false; break; } + if (Math.abs(d) <= tol) on.push(m); + } + if (!ok) continue; + const key = `${nrm.map((v) => v.toFixed(2)).join(",")}|${off.toFixed(2)}`; + const entry = planes.get(key) || { normal: nrm, verts: new Set() }; + for (const m of on) entry.verts.add(m); + planes.set(key, entry); + } + const out: Vec3[][] = []; + for (const { normal, verts } of planes.values()) { + const vs = [...verts].map((m) => pts[m]); + if (vs.length < 3) continue; + const cen: Vec3 = [0, 0, 0]; + for (const v of vs) { cen[0] += v[0] / vs.length; cen[1] += v[1] / vs.length; cen[2] += v[2] / vs.length; } + const e1 = sub(vs[0], cen); + const e2 = cross(normal, e1); + vs.sort((a, b) => Math.atan2(dot(sub(a, cen), e2), dot(sub(a, cen), e1)) - Math.atan2(dot(sub(b, cen), e2), dot(sub(b, cen), e1))); + out.push(vs); + } + return out; +} + +export function drawCell( + canvas: HTMLCanvasElement, geom: CellGeometry, q: Quat, size: number, style: CellStyle, +) { + const dpr = window.devicePixelRatio || 1; + if (canvas.width !== size * dpr || canvas.height !== size * dpr) { + canvas.width = size * dpr; + canvas.height = size * dpr; + } + const ctx = canvas.getContext("2d"); + if (!ctx) return; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, size, size); + const R = quatToMatrix(q); + const scale = (0.5 * size * 0.82) / geom.radius; // px per A + const cx = size / 2, cy = size / 2; + const proj = (v: Vec3): [number, number, number] => { + const w = matVec(R, v); + return [cx + style.viewX * w[0] * scale, cy - w[1] * scale, style.viewX * w[2]]; + }; + const edgeColor = style.dark ? "rgba(220,220,220," : "rgba(40,40,40,"; + const depthFrac = (z: number) => Math.min(1, Math.max(0, 0.5 + z / (2 * geom.radius))); + const pc = geom.corners.map(proj); + type Prim = { z: number; draw: () => void }; + const prims: Prim[] = []; + for (const [i, j] of geom.edges) { + const a = pc[i], b = pc[j]; + const z = 0.5 * (a[2] + b[2]); + prims.push({ + z, + draw: () => { + ctx.strokeStyle = edgeColor + (0.35 + 0.5 * depthFrac(z)) + ")"; + ctx.lineWidth = 1.2; + ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke(); + }, + }); + } + for (const [p0, p1] of geom.innerEdges) { + const a = proj(p0), b = proj(p1); + const z = 0.5 * (a[2] + b[2]); + prims.push({ + z, + draw: () => { + ctx.strokeStyle = edgeColor + (0.12 + 0.2 * depthFrac(z)) + ")"; + ctx.lineWidth = 0.8; + ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke(); + }, + }); + } + for (const face of geom.faces) { + const pv = face.verts.map(proj); + let z = 0; + for (const p of pv) z += p[2] / pv.length; + prims.push({ + z: z - 1e-3, + draw: () => { + ctx.beginPath(); + ctx.moveTo(pv[0][0], pv[0][1]); + for (let i = 1; i < pv.length; i++) ctx.lineTo(pv[i][0], pv[i][1]); + ctx.closePath(); + ctx.fillStyle = face.color; + ctx.globalAlpha = 0.18 + 0.17 * depthFrac(z); + ctx.fill(); + ctx.globalAlpha = 0.6; + ctx.strokeStyle = face.color; + ctx.lineWidth = 0.8; + ctx.stroke(); + ctx.globalAlpha = 1; + }, + }); + } + const atomScale = style.atomScale * (geom.faces.length ? 0.6 : 1); + for (const at of geom.atoms) { + const p = proj(at.pos); + const r = Math.max(1.5, at.radius * atomScale * scale); + prims.push({ + z: p[2], + draw: () => { + const grad = ctx.createRadialGradient(p[0] - 0.35 * r, p[1] - 0.35 * r, 0.1 * r, p[0], p[1], r); + grad.addColorStop(0, lighten(at.color, 0.55)); + grad.addColorStop(0.7, at.color); + grad.addColorStop(1, lighten(at.color, -0.45)); + ctx.fillStyle = grad; + ctx.globalAlpha = 0.55 + 0.45 * depthFrac(p[2]); + ctx.beginPath(); ctx.arc(p[0], p[1], r, 0, 2 * Math.PI); ctx.fill(); + ctx.globalAlpha = 1; + ctx.strokeStyle = style.dark ? "rgba(0,0,0,0.6)" : "rgba(0,0,0,0.35)"; + ctx.lineWidth = 0.8; + ctx.stroke(); + }, + }); + } + prims.sort((a, b) => a.z - b.z); + for (const p of prims) p.draw(); + + if (style.showAxes) { + const o = proj(geom.origin); + const names = ["a", "b", "c"]; + const cols = ["#e53935", "#43a047", "#1e88e5"]; + for (let i = 0; i < 3; i++) { + const ax = geom.axes[i]; + const tip = proj([geom.origin[0] + ax[0], geom.origin[1] + ax[1], geom.origin[2] + ax[2]]); + ctx.strokeStyle = cols[i]; + ctx.lineWidth = 2.5; + ctx.beginPath(); ctx.moveTo(o[0], o[1]); ctx.lineTo(tip[0], tip[1]); ctx.stroke(); + if (style.showLabels) { + const dx = tip[0] - o[0], dy = tip[1] - o[1]; + const L = Math.hypot(dx, dy) || 1; + ctx.fillStyle = cols[i]; + ctx.font = "bold 14px sans-serif"; + ctx.textAlign = "center"; ctx.textBaseline = "middle"; + ctx.fillText(names[i], tip[0] + (dx / L) * 11, tip[1] + (dy / L) * 11); + } + } + } + // lab axes in the corner + const ox = 22, oy = size - 22, L = 18; + ctx.strokeStyle = style.dark ? "#aaa" : "#555"; + ctx.fillStyle = style.dark ? "#aaa" : "#555"; + ctx.lineWidth = 1.2; + ctx.font = "10px sans-serif"; + ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(ox + L, oy); ctx.stroke(); + ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(ox, oy - L); ctx.stroke(); + ctx.textAlign = "left"; ctx.textBaseline = "middle"; + ctx.fillText("x", ox + L + 3, oy); + ctx.textAlign = "center"; ctx.textBaseline = "bottom"; + ctx.fillText("y", ox, oy - L - 2); + ctx.beginPath(); ctx.arc(ox, oy, 3.5, 0, 2 * Math.PI); ctx.stroke(); + ctx.textAlign = "left"; ctx.textBaseline = "top"; + if (style.viewX > 0) { + ctx.beginPath(); ctx.moveTo(ox - 2.5, oy - 2.5); ctx.lineTo(ox + 2.5, oy + 2.5); ctx.moveTo(ox - 2.5, oy + 2.5); ctx.lineTo(ox + 2.5, oy - 2.5); ctx.stroke(); + ctx.fillText("beam into screen (view from gun)", ox + 6, oy + 4); + } else { + ctx.beginPath(); ctx.arc(ox, oy, 1.2, 0, 2 * Math.PI); ctx.fill(); + ctx.fillText("beam toward you (view from detector)", ox + 6, oy + 4); + } +} + +function lighten(color: string, amount: number): string { + const m = color.match(/rgb\((\d+),(\d+),(\d+)\)/); + if (!m) return color; + const f = (v: number) => Math.max(0, Math.min(255, Math.round(amount >= 0 ? v + (255 - v) * amount : v * (1 + amount)))); + return `rgb(${f(+m[1])},${f(+m[2])},${f(+m[3])})`; +} diff --git a/widget/js/diffsim/index.tsx b/widget/js/diffsim/index.tsx new file mode 100644 index 000000000..40d0b56e7 --- /dev/null +++ b/widget/js/diffsim/index.tsx @@ -0,0 +1,705 @@ +/** + * DiffractionSim: unit cell (left) and its diffraction pattern (right), both + * following the same orientation. Drag the cell to tilt the crystal and the + * pattern follows live. Every calculation (kinematical and Bloch wave + * intensities, CBED disks, Kossel lines) runs here in the browser, so the + * widget also works as a standalone HTML page. + */ + +import * as React from "react"; +import { createRender, useModel, useModelState } from "@anywidget/react"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import Select from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import Switch from "@mui/material/Switch"; +import Slider from "@mui/material/Slider"; +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import ToggleButton from "@mui/material/ToggleButton"; +import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; +import Tooltip from "@mui/material/Tooltip"; +import { useTheme } from "../theme"; +import { COLORMAP_NAMES } from "../colormaps"; +import { downloadBlob } from "../format"; +import { Quat, Vec3, directionIndices, matTVec, qmult, qnormalize, quatFromAxisAngle, quatFromZoneAxis, quatToMatrix } from "./math"; +import { + Reflection, blochIntensities, blochSolve, kinematicalTilted, kosselLines, kosselLookup, labReflections, + parseCrystal, parseKossel, hybridBeams, slabIntensities, +} from "./physics"; +import { cellGeometry, drawCell } from "./crystal3d"; +import { + Frame, cbedImage, drawImage, drawKikuchiOverlay, drawKosselLines, drawMarkers, histogramBins, nanobeamImage, + setupCanvas, tiltGrid, toPx, +} from "./pattern"; + +const DIRECT: Reflection = { index: -1, hkl: [0, 0, 0], g: [0, 0, 0], gLen: 0, s: 0 }; +const QUALITY: Record = { + fast: { grid: 5, beams: 24, nanobeam: 40 }, + medium: { grid: 7, beams: 36, nanobeam: 64 }, + fine: { grid: 9, beams: 56, nanobeam: 96 }, +}; + +// --------------------------------------------------------------------------- +function Histogram({ bins, vminPct, vmaxPct, onRangeChange, width = 130, height = 40, dark, lo, hi }: { + bins: number[]; vminPct: number; vmaxPct: number; onRangeChange: (a: number, b: number) => void; + width?: number; height?: number; dark: boolean; lo: number; hi: number; +}) { + const canvasRef = React.useRef(null); + const c = dark ? { bg: "#1a1a1a", on: "#888", off: "#444", border: "#333" } : { bg: "#f0f0f0", on: "#666", off: "#bbb", border: "#ccc" }; + React.useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + const dpr = window.devicePixelRatio || 1; + canvas.width = width * dpr; canvas.height = height * dpr; + ctx.scale(dpr, dpr); + ctx.fillStyle = c.bg; ctx.fillRect(0, 0, width, height); + const nb = 64, ratio = Math.floor(bins.length / nb); + const red: number[] = []; + for (let i = 0; i < nb; i++) { let s = 0; for (let j = 0; j < ratio; j++) s += bins[i * ratio + j] || 0; red.push(s); } + const mx = Math.max(...red.map((v) => Math.log1p(v)), 1e-3); + const bw = width / nb; + const b0 = Math.floor((vminPct / 100) * nb), b1 = Math.floor((vmaxPct / 100) * nb); + for (let i = 0; i < nb; i++) { + const h = (Math.log1p(red[i]) / mx) * (height - 2); + ctx.fillStyle = i >= b0 && i <= b1 ? c.on : c.off; + ctx.fillRect(i * bw + 0.5, height - h, Math.max(1, bw - 1), h); + } + }, [bins, vminPct, vmaxPct, width, height, dark]); + const fmt = (pct: number) => { const v = lo + (pct / 100) * (hi - lo); return Math.abs(v) >= 1000 || (Math.abs(v) < 0.01 && v !== 0) ? v.toExponential(1) : v.toFixed(2); }; + return ( + + + { const [a, b] = v as number[]; onRangeChange(Math.min(a, b - 1), Math.max(b, a + 1)); }} + min={0} max={100} size="small" valueLabelDisplay="auto" valueLabelFormat={fmt} + sx={{ width, py: 0, "& .MuiSlider-thumb": { width: 8, height: 8 }, "& .MuiSlider-rail": { height: 2 }, "& .MuiSlider-track": { height: 2 }, "& .MuiSlider-valueLabel": { fontSize: 10, padding: "2px 4px" } }} + /> + + {fmt(vminPct)} + {fmt(vmaxPct)} + + + ); +} + +function LabeledSlider({ label, value, onChange, min, max, step, fmt, width = 200, disabled }: { + label: string; value: number; onChange: (v: number) => void; min: number; max: number; step: number; + fmt: (v: number) => string; width?: number; disabled?: boolean; +}) { + return ( + + + {label} + {fmt(value)} + + onChange(v as number)} + sx={{ py: 0.5, "& .MuiSlider-thumb": { width: 12, height: 12 } }} /> + + ); +} + +function parseZoneAxis(text: string): Vec3 | null { + const t = text.trim().replace(/[\[\]()]/g, ""); + let parts: string[]; + if (/[\s,]/.test(t)) parts = t.split(/[\s,]+/).filter(Boolean); + else parts = t.match(/-?\d/g) || []; + if (parts.length !== 3) return null; + const v = parts.map(Number); + if (v.some((x) => !isFinite(x)) || v.every((x) => x === 0)) return null; + return v as Vec3; +} + +function fmtIndices(v: [number, number, number] | null, brackets = "[]"): string { + if (!v) return "—"; + return brackets[0] + v.map((h) => (h < 0 ? `${-h}̅` : `${h}`)).join("") + brackets[1]; +} + +// --------------------------------------------------------------------------- +function DiffSim() { + const model = useModel(); + const { themeInfo, colors } = useTheme(); + const dark = themeInfo.theme === "dark"; + const standalone = !!model.get("standalone"); + const embedded: Record = (model.get("embedded_presets") as Record) || {}; + + const [crystalJson, setCrystalJson] = useModelState("crystal_json"); + const [kosselJson] = useModelState("kossel_json"); + const [presets] = useModelState("presets"); + const [preset, setPreset] = useModelState("preset"); + const [energy, setEnergy] = useModelState("energy_ev"); + const [orientation, setOrientation] = useModelState("orientation"); + const [mode, setMode] = useModelState("mode"); + const [render, setRender] = useModelState("render"); + const [dynamical, setDynamical] = useModelState("dynamical"); + const [thickness, setThickness] = useModelState("thickness_A"); + const [semiconv, setSemiconv] = useModelState("semiconv_mrad"); + const [sigma, setSigma] = useModelState("sigma_excitation"); + const [stepDeg, setStepDeg] = useModelState("rotation_step_deg"); + const [scaling, setScaling] = useModelState("scaling"); + const [power, setPower] = useModelState("power"); + const [cmap, setCmap] = useModelState("cmap"); + const [vminPct, setVminPct] = useModelState("vmin_pct"); + const [vmaxPct, setVmaxPct] = useModelState("vmax_pct"); + const [showLabels, setShowLabels] = useModelState("show_labels"); + const [showCellAxes, setShowCellAxes] = useModelState("show_cell_axes"); + const [nCells, setNCells] = useModelState("n_cells"); + const [polyhedra, setPolyhedra] = useModelState("polyhedra"); + const [sizePref] = useModelState("size"); + const [status] = useModelState("status"); + + const crystal = React.useMemo(() => parseCrystal(crystalJson), [crystalJson]); + const kossel = React.useMemo(() => parseKossel(kosselJson), [kosselJson]); + const nCellsSafe: [number, number, number] = [nCells?.[0] || 1, nCells?.[1] || 1, nCells?.[2] || 1]; + const geom = React.useMemo(() => (crystal ? cellGeometry(crystal, nCellsSafe, polyhedra) : null), [crystal, nCellsSafe.join(","), polyhedra]); + + // local view state + const [patternRange, setPatternRange] = useModelState("pattern_range"); + const [fieldMrad, setFieldMrad] = useModelState("field_mrad"); + const [SG_MAX] = useModelState("sg_max"); + const [quality, setQuality] = useModelState("quality"); + const [kikuchi, setKikuchi] = useModelState("show_kikuchi"); + const [viewFrom] = useModelState("view_from"); + const viewX = viewFrom === "gun" ? 1 : -1; + const qMaxDisp = Math.min(Math.max(patternRange || 0, 0.2), crystal?.k_max ?? 4); + const setQMaxDisp = setPatternRange; + const [zoneText, setZoneText] = React.useState(""); + const [dragging, setDragging] = React.useState(false); + const [winW, setWinW] = React.useState(typeof window !== "undefined" ? window.innerWidth : 1200); + React.useEffect(() => { + const f = () => setWinW(window.innerWidth); + window.addEventListener("resize", f); + return () => window.removeEventListener("resize", f); + }, []); + const S = Math.max(220, Math.min(sizePref, winW - 40)); + + // orientation: local quaternion for smooth dragging, pushed to the model with a throttle + const [quat, setQuatLocal] = React.useState(orientation as Quat); + const quatRef = React.useRef(quat); + React.useEffect(() => { const q = orientation as Quat; quatRef.current = q; setQuatLocal(q); }, [orientation.join(",")]); + const pushTimer = React.useRef(null); + const setQuat = React.useCallback((q: Quat, immediate = false) => { + quatRef.current = q; + setQuatLocal(q); + const push = () => { pushTimer.current = null; setOrientation([...quatRef.current]); }; + if (immediate) { if (pushTimer.current) window.clearTimeout(pushTimer.current); push(); } + else if (!pushTimer.current) pushTimer.current = window.setTimeout(push, 200); + }, [setOrientation]); + + // axis given in SCREEN coordinates (x right, y up, z toward the viewer); mapped to the lab frame by the view + const rotateLab = React.useCallback((axis: Vec3, deg: number, immediate = true) => { + const dq = quatFromAxisAngle([viewX * axis[0], axis[1], viewX * axis[2]], (deg * Math.PI) / 180); + setQuat(qnormalize(qmult(dq, quatRef.current)), immediate); + }, [setQuat, viewX]); + + // ---- pointer handling on the cell canvas (mouse and touch) ------------- + const cellRef = React.useRef(null); + const pointers = React.useRef>(new Map()); + const onPointerDown = (e: React.PointerEvent) => { + (e.target as HTMLElement).setPointerCapture?.(e.pointerId); + pointers.current.set(e.pointerId, [e.clientX, e.clientY]); + setDragging(true); + }; + const onPointerMove = (e: React.PointerEvent) => { + const prev = pointers.current.get(e.pointerId); + if (!prev) return; + const cur: [number, number] = [e.clientX, e.clientY]; + if (pointers.current.size >= 2) { + // two fingers: twist about the beam axis + const other = [...pointers.current.entries()].find(([id]) => id !== e.pointerId); + if (other) { + const [ox, oy] = other[1]; + const a0 = Math.atan2(prev[1] - oy, prev[0] - ox); + const a1 = Math.atan2(cur[1] - oy, cur[0] - ox); + let da = a1 - a0; + if (da > Math.PI) da -= 2 * Math.PI; + if (da < -Math.PI) da += 2 * Math.PI; + rotateLab([0, 0, 1], (-da * 180) / Math.PI, false); + } + } else { + const dx = cur[0] - prev[0], dy = cur[1] - prev[1]; + const degPerPx = 180 / S; + const ang = Math.hypot(dx, dy) * degPerPx; + if (ang > 0) rotateLab([dy, dx, 0], ang, false); // trackball: the face nearest the viewer follows the pointer + } + pointers.current.set(e.pointerId, cur); + }; + const onPointerUp = (e: React.PointerEvent) => { + pointers.current.delete(e.pointerId); + if (pointers.current.size === 0) { setDragging(false); setQuat(quatRef.current, true); } + }; + + // ---- pointer handling on the pattern canvas --------------------------------- + // Dragging the pattern by dq (1/A, or rad in Kossel mode) moves the zone + // axis so the pattern follows: the Laue circle centre sits at q = -k0 delta + // for a zone axis tilted by delta, so the crystal tilts by -dq/k0. + const shiftPattern = React.useCallback((dqx: number, dqy: number, inverseAngstrom: boolean, immediate: boolean) => { + const k0 = crystal ? 1 / crystal.wavelength : 1; + const ax = inverseAngstrom ? dqx / k0 : dqx, ay = inverseAngstrom ? dqy / k0 : dqy; + const ang = Math.hypot(ax, ay); + if (ang <= 0) return; + const dq = quatFromAxisAngle([ay, -ax, 0], ang); + setQuat(qnormalize(qmult(dq, quatRef.current)), immediate); + }, [crystal, setQuat]); + const patPointers = React.useRef>(new Map()); + const patScale = React.useRef(1); // px per unit of the current frame + const onPatDown = (e: React.PointerEvent) => { + (e.target as HTMLElement).setPointerCapture?.(e.pointerId); + patPointers.current.set(e.pointerId, [e.clientX, e.clientY]); + setDragging(true); + }; + const onPatMove = (e: React.PointerEvent) => { + const prev = patPointers.current.get(e.pointerId); + if (!prev) return; + const cur: [number, number] = [e.clientX, e.clientY]; + if (patPointers.current.size >= 2) { + const other = [...patPointers.current.entries()].find(([id]) => id !== e.pointerId); + if (other) { + const [ox, oy] = other[1]; + let da = Math.atan2(cur[1] - oy, cur[0] - ox) - Math.atan2(prev[1] - oy, prev[0] - ox); + if (da > Math.PI) da -= 2 * Math.PI; + if (da < -Math.PI) da += 2 * Math.PI; + rotateLab([0, 0, 1], (-da * 180) / Math.PI, false); + } + } else { + const dx = (viewX * (cur[0] - prev[0])) / patScale.current, dy = -(cur[1] - prev[1]) / patScale.current; + shiftPattern(dx, dy, mode !== "kossel", false); + } + patPointers.current.set(e.pointerId, cur); + }; + const onPatUp = (e: React.PointerEvent) => { + patPointers.current.delete(e.pointerId); + if (patPointers.current.size === 0) { setDragging(false); setQuat(quatRef.current, true); } + }; + const onPatDoubleClick = (e: React.MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const x = e.clientX - rect.left, y = e.clientY - rect.top; + const qx = (viewX * (x - rect.width / 2)) / patScale.current, qy = -(y - rect.height / 2) / patScale.current; + shiftPattern(-qx, -qy, mode !== "kossel", true); + }; + + // ---- derived geometry --------------------------------------------------- + const R = React.useMemo(() => quatToMatrix(quat), [quat]); + const zoneAxis = React.useMemo(() => { + if (!crystal) return null; + const dc = matTVec(R, [0, 0, 1]); + return directionIndices(crystal.cell, dc); + }, [crystal, R]); + const k0 = crystal ? 1 / crystal.wavelength : 0; + const qual = QUALITY[quality] || QUALITY.medium; + + // ---- nanobeam ----------------------------------------------------------- + const nb = React.useMemo(() => { + if (!crystal || mode !== "nanobeam") return { beams: [] as Reflection[], nDyn: 0 }; + if (dynamical) return hybridBeams(crystal, quat, qMaxDisp, SG_MAX, dragging ? Math.min(qual.nanobeam, 40) : qual.nanobeam); + return { beams: [DIRECT, ...labReflections(crystal, quat, qMaxDisp)], nDyn: 0 }; + }, [crystal, quat, qMaxDisp, mode, dynamical, dragging, qual, SG_MAX]); + const nbBeams = nb.beams; + const nbSol = React.useMemo(() => (crystal && mode === "nanobeam" && dynamical && nb.nDyn ? blochSolve(crystal, nbBeams.slice(0, nb.nDyn)) : null), [crystal, nbBeams, nb.nDyn, mode, dynamical]); + const nbInten = React.useMemo(() => { + if (!crystal || mode !== "nanobeam") return new Float64Array(0); + if (dynamical && nbSol) { + const out = new Float64Array(nbBeams.length); + out.set(blochIntensities(nbSol, thickness)); + slabIntensities(crystal, nbBeams, nb.nDyn, [0, 0], thickness, out); + return out; + } + return kinematicalTilted(crystal, nbBeams, [0, 0], sigma); + }, [crystal, nbBeams, nb.nDyn, nbSol, mode, dynamical, thickness, sigma]); + + // ---- CBED --------------------------------------------------------------- + const alpha = semiconv * 1e-3; + const cbed = React.useMemo(() => { + if (!crystal || mode !== "cbed") return null; + const Rk = k0 * Math.sin(alpha); + const grid = tiltGrid(Rk, dragging ? 5 : qual.grid); + if (!dynamical) return { grid, beams: [DIRECT, ...labReflections(crystal, quat, qMaxDisp)], nDyn: 0, sols: null }; + const { beams, nDyn } = hybridBeams(crystal, quat, qMaxDisp, SG_MAX, dragging ? Math.min(qual.beams, 24) : qual.beams, Math.sin(alpha)); + const dyn = beams.slice(0, nDyn); + const sols = grid.tilts.map((t) => blochSolve(crystal, dyn, t)); + return { grid, beams, nDyn, sols }; + }, [crystal, quat, qMaxDisp, mode, dynamical, alpha, k0, dragging, qual, SG_MAX]); + const cbedInten = React.useMemo(() => { + if (!crystal || !cbed) return null; + if (cbed.sols) { + return cbed.sols.map((sol, i) => { + const out = new Float64Array(cbed.beams.length); + out.set(blochIntensities(sol, thickness)); + slabIntensities(crystal, cbed.beams, cbed.nDyn, cbed.grid.tilts[i], thickness, out); + return out; + }); + } + return cbed.grid.tilts.map((t) => kinematicalTilted(crystal, cbed.beams, t, sigma)); + }, [crystal, cbed, thickness, sigma]); + + // ---- Kossel ------------------------------------------------------------- + const fieldRad = fieldMrad * 1e-3; + const lines = React.useMemo(() => { + if (!crystal || (mode !== "kossel" && !(mode === "nanobeam" && kikuchi))) return []; + const fov = mode === "kossel" ? fieldRad : qMaxDisp / k0; + return kosselLines(crystal, quat, Math.min(crystal.k_max, 2.5), fov); + }, [crystal, quat, mode, fieldRad, kikuchi, qMaxDisp, k0]); + + // ---- pixel image of the current mode -------------------------------------- + const frame: Frame = React.useMemo(() => ({ size: S, qMax: mode === "kossel" ? fieldRad : qMaxDisp, viewX }), [S, mode, fieldRad, qMaxDisp, viewX]); + patScale.current = (0.5 * S * 0.92) / frame.qMax; + const pixelMode = (mode === "nanobeam" && render === "pixels") || mode === "cbed" || (mode === "kossel" && render === "pixels"); + const image = React.useMemo(() => { + if (!crystal || !pixelMode) return null; + if (mode === "nanobeam") return nanobeamImage(frame, nbBeams, nbInten, Math.max(1.5, S / 200)); + if (mode === "cbed" && cbed && cbedInten) return cbedImage(frame, cbed.beams, cbed.grid, cbedInten); + if (mode === "kossel" && kossel) return kosselLookup(kossel, quat, fieldRad, S, thickness, viewX); + return null; + }, [crystal, pixelMode, mode, frame, nbBeams, nbInten, cbed, cbedInten, kossel, quat, fieldRad, S, thickness, viewX]); + const display = React.useMemo(() => { + if (!image) return null; + let data = image; + if (scaling === "log") { + let mx = 0; + for (let i = 0; i < image.length; i++) if (isFinite(image[i])) mx = Math.max(mx, image[i]); + const eps = 1e-4 * (mx || 1); + data = new Float32Array(image.length); + for (let i = 0; i < image.length; i++) data[i] = isFinite(image[i]) ? Math.log10(Math.max(image[i], 0) + eps) : NaN; + } else if (scaling === "power") { + const pw = Math.min(Math.max(power || 0.5, 0.05), 1); + data = new Float32Array(image.length); + for (let i = 0; i < image.length; i++) data[i] = isFinite(image[i]) ? Math.pow(Math.max(image[i], 0), pw) : NaN; + } + let lo = Infinity, hi = -Infinity; + for (let i = 0; i < data.length; i++) { const v = data[i]; if (isFinite(v)) { if (v < lo) lo = v; if (v > hi) hi = v; } } + if (!(hi > lo)) { lo = 0; hi = 1; } + return { data, lo, hi, bins: histogramBins(data, lo, hi) }; + }, [image, scaling, power]); + + // ---- drawing -------------------------------------------------------------- + React.useEffect(() => { + const canvas = cellRef.current; + if (!canvas || !geom) return; + drawCell(canvas, geom, quat, S, { dark, showAxes: showCellAxes, showLabels: showLabels, atomScale: 0.45, viewX }); + }, [geom, quat, S, dark, showCellAxes, showLabels, viewX]); + + const patRef = React.useRef(null); + React.useEffect(() => { + const canvas = patRef.current; + if (!canvas || !crystal) return; + const ctx = setupCanvas(canvas, S); + if (!ctx) return; + if (display) { + const vmin = display.lo + (vminPct / 100) * (display.hi - display.lo); + const vmax = display.lo + (vmaxPct / 100) * (display.hi - display.lo); + drawImage(ctx, frame, display.data, cmap, vmin, vmax, dark, mode === "kossel" ? "rad" : "Å⁻¹", mode === "kossel" ? 0.01 : 1); + if (mode === "nanobeam" && kikuchi) drawKikuchiOverlay(ctx, frame, lines, k0, true); + if (mode === "nanobeam" && showLabels) labelBeams(ctx, frame, nbBeams, nbInten, dark, true); + } else if (mode === "nanobeam") { + drawMarkers(ctx, frame, nbBeams, nbInten, dark, showLabels, !dynamical); + if (kikuchi) drawKikuchiOverlay(ctx, frame, lines, k0, dark); + } else if (mode === "kossel") { + if (render === "pixels" && !kossel) { + ctx.fillStyle = dark ? "#000" : "#fff"; ctx.fillRect(0, 0, S, S); + ctx.fillStyle = dark ? "#ccc" : "#333"; ctx.font = "13px sans-serif"; ctx.textAlign = "center"; + ctx.fillText("no Kossel reference pattern loaded", S / 2, S / 2 - 10); + ctx.fillText(standalone ? "(export the page after compute_kossel_reference)" : "press “compute reference” below", S / 2, S / 2 + 10); + } else { + drawKosselLines(ctx, frame, lines, dark, showLabels, 0.02); + } + } else if (mode === "cbed") { + ctx.fillStyle = dark ? "#000" : "#fff"; ctx.fillRect(0, 0, S, S); + } + }, [crystal, display, frame, mode, render, dark, cmap, vminPct, vmaxPct, nbBeams, nbInten, showLabels, dynamical, kikuchi, lines, k0, kossel, S, standalone]); + + // ---- actions --------------------------------------------------------------- + const goZoneAxis = () => { + const uvw = parseZoneAxis(zoneText); + if (!uvw || !crystal) return; + const c = crystal.cell; + const d: Vec3 = [ + uvw[0] * c[0][0] + uvw[1] * c[1][0] + uvw[2] * c[2][0], + uvw[0] * c[0][1] + uvw[1] * c[1][1] + uvw[2] * c[2][1], + uvw[0] * c[0][2] + uvw[1] * c[1][2] + uvw[2] * c[2][2], + ]; + setQuat(quatFromZoneAxis(d), true); + }; + const choosePreset = (name: string) => { + if (standalone) { + const data = embedded[name]; + if (data) { setCrystalJson(JSON.stringify(data)); setPreset(name); } + } else { + setPreset(name); + } + }; + const savePng = () => { + const a = cellRef.current, b = patRef.current; + if (!a || !b) return; + const off = document.createElement("canvas"); + off.width = a.width + b.width + 8; off.height = Math.max(a.height, b.height); + const ctx = off.getContext("2d"); + if (!ctx) return; + ctx.fillStyle = dark ? "#1e1e1e" : "#fff"; ctx.fillRect(0, 0, off.width, off.height); + ctx.drawImage(a, 0, 0); ctx.drawImage(b, a.width + 8, 0); + off.toBlob((blob) => { if (blob) downloadBlob(blob, `${crystal?.name || "crystal"}_${mode}.png`); }); + }; + const exportHtml = async () => { + const res = await fetch(import.meta.url); + const bundle = await res.text(); + const keys = ["crystal_json", "presets", "preset", "energy_ev", "k_max", "orientation", "mode", "render", "dynamical", "thickness_A", + "semiconv_mrad", "sigma_excitation", "rotation_step_deg", "pattern_range", "field_mrad", "sg_max", "quality", "show_kikuchi", "view_from", + "scaling", "power", "cmap", "vmin_pct", "vmax_pct", "show_labels", + "show_cell_axes", "n_cells", "polyhedra", "size", "kossel_json", "status", "widget_version"]; + const state: Record = {}; + for (const k of keys) state[k] = model.get(k); + state.orientation = [...quatRef.current]; + const emb: Record = { ...embedded }; + if (crystal && crystalJson) emb[preset || crystal.name] = JSON.parse(crystalJson); + state.embedded_presets = emb; + state.presets = Object.keys(emb); + if (!state.preset) state.preset = crystal?.name || ""; + downloadBlob(new Blob([standaloneHtml(bundle, state, `quantEM diffraction simulator: ${crystal?.name || ""}`)], { type: "text/html" }), + `${crystal?.name || "crystal"}_diffsim.html`); + }; + + const presetNames = standalone ? Object.keys(embedded) : presets; + const ctl = { + fontSize: 12, height: 30, bgcolor: colors.controlBg, color: colors.text, + "& .MuiSelect-select": { py: 0.4, fontSize: 12 }, + "& .MuiSvgIcon-root": { color: colors.textMuted }, + "& .MuiOutlinedInput-notchedOutline": { borderColor: colors.border }, + "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: colors.accent }, + "&.Mui-disabled": { color: colors.textMuted, "& .MuiSelect-select": { WebkitTextFillColor: colors.textMuted } }, + }; + const menuProps = { PaperProps: { sx: { bgcolor: colors.controlBg, color: colors.text, border: `1px solid ${colors.border}` } }, sx: { zIndex: 9999 } }; + const tbg = { + "& .MuiToggleButton-root": { + px: 1, py: 0.3, fontSize: 11, textTransform: "none", color: colors.textMuted, borderColor: colors.border, bgcolor: colors.controlBg, + "&.Mui-selected": { color: colors.accent, bgcolor: dark ? "#2e3a48" : "#e3eefc" }, + "&:hover": { bgcolor: dark ? "#333" : "#e8e8e8" }, + }, + }; + const tf = { + "& input": { fontSize: 12, py: 0.6, color: colors.text }, + "& input::placeholder": { color: colors.textMuted, opacity: 1 }, + "& .MuiOutlinedInput-notchedOutline": { borderColor: colors.border }, + "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: colors.accent }, + bgcolor: colors.controlBg, + }; + const btn = { fontSize: 11, height: 30, color: colors.accent, borderColor: colors.border, textTransform: "none" as const, "&:hover": { borderColor: colors.accent } }; + const sw = { "& .MuiSwitch-track": { bgcolor: dark ? "#777" : undefined } }; + const panelW = S; + const nDyn = mode === "nanobeam" ? nb.nDyn : mode === "cbed" && cbed ? cbed.nDyn : 0; + const nBeams = mode === "nanobeam" ? nbBeams.length : mode === "cbed" && cbed ? cbed.beams.length : lines.length; + + if (!crystal || !geom) { + return loading crystal…; + } + + return ( + + {/* top bar */} + + Diffraction simulator + + + + + setZoneText(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") goZoneAxis(); }} + sx={{ width: 150, ...tf }} /> + + + + + + + + {/* left: unit cell */} + + + + {(["x", "y", "z"] as const).map((ax, i) => ( + + rotateLab([+(i === 0), +(i === 1), +(i === 2)], -stepDeg)}>{ax} − + rotateLab([+(i === 0), +(i === 1), +(i === 2)], stepDeg)}>{ax} + + + ))} + setStepDeg(Math.max(0.01, Number(e.target.value) || 0.01))} + inputProps={{ step: 1, min: 0.01, max: 180, style: { fontSize: 11, padding: "4px 6px", width: 42 } }} sx={tf} /> + ° + + + + {crystal.name} · {crystal.spacegroup || crystal.pointgroup} + zone axis {fmtIndices(zoneAxis)} + + + setShowCellAxes(e.target.checked)} /> + cell axes + setShowLabels(e.target.checked)} /> + labels + setPolyhedra(e.target.checked)} /> + polyhedra + + + cells + {[0, 1, 2].map((i) => ( + { const v = [...nCellsSafe]; v[i] = Math.max(1, Math.min(6, Math.round(Number(e.target.value) || 1))); setNCells(v); }} + inputProps={{ min: 1, max: 6, step: 1, style: { fontSize: 11, padding: "3px 4px", width: 26 } }} sx={tf} /> + ))} + along a, b, c + + drag the cell (near face follows) or the pattern (tilt map follows) · double-click a point of the pattern to centre it · two fingers twist · buttons rotate about the screen axes + + + {/* right: pattern */} + + + + v && setMode(v)} sx={tbg}> + nanobeam + CBED + Kossel / LACBED + + {mode !== "cbed" && ( + v && setRender(v)} sx={tbg}> + {mode === "kossel" ? "lines" : "markers"} + pixels + + )} + {mode !== "kossel" && ( + + setDynamical(e.target.checked)} /> + dynamical + + )} + + + `${v.toFixed(0)} Å`} + disabled={mode !== "kossel" ? !dynamical : render !== "pixels"} /> + {mode === "cbed" && `${v.toFixed(1)} mrad`} />} + {mode === "kossel" && `${v.toFixed(0)} mrad`} />} + {mode !== "kossel" && `${v.toFixed(2)} Å⁻¹`} />} + {mode !== "kossel" && !dynamical && `${v.toFixed(3)} Å⁻¹`} />} + + + {display && ( + { setVminPct(a); setVmaxPct(b); }} dark={dark} lo={display.lo} hi={display.hi} /> + )} + {pixelMode && ( + + + + {scaling === "power" && ( + v.toFixed(2)} width={110} /> + )} + + )} + {mode === "nanobeam" && ( + + setKikuchi(e.target.checked)} /> + Kikuchi lines + + )} + {mode !== "kossel" && dynamical && ( + + quality + + + )} + {mode === "kossel" && render === "pixels" && !kossel && !standalone && ( + + )} + + + {(energy / 1e3).toFixed(0)} keV · λ = {(crystal.wavelength * 100).toFixed(3)} pm · {nBeams} {mode === "kossel" ? "lines" : "beams"} + {mode !== "kossel" && dynamical ? ` · ${nDyn} Bloch beams (|s| < ${SG_MAX} Å⁻¹${crystal.absorptive ? ", absorptive" : ""}), thin-slab intensities for the rest` : ""} + {mode === "cbed" ? " · disks summed incoherently where they overlap" : ""} + {status ? ` · ${status}` : ""} + + + + + ); +} + +function labelBeams(ctx: CanvasRenderingContext2D, f: Frame, beams: Reflection[], inten: Float64Array, dark: boolean, onImage: boolean) { + let iMax = 0; + for (let i = 0; i < beams.length; i++) iMax = Math.max(iMax, inten[i]); + if (iMax <= 0) return; + ctx.font = `${Math.max(9, Math.round(f.size / 38))}px sans-serif`; + ctx.textAlign = "center"; ctx.textBaseline = "bottom"; + ctx.fillStyle = onImage ? "#ffd54f" : dark ? "#ffd54f" : "#c62828"; + let count = 0; + for (let i = 0; i < beams.length && count < 40; i++) { + if (inten[i] / iMax < 0.08) continue; + const [x, y] = toPx(f, beams[i].g[0], beams[i].g[1]); + ctx.fillText(beams[i].hkl.map((h) => (h < 0 ? `${-h}̅` : `${h}`)).join(""), x, y - 6); + count++; + } +} + +function standaloneHtml(bundle: string, state: Record, title: string): string { + const bytes = new TextEncoder().encode(bundle); + let bin = ""; + for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); + const b64 = btoa(bin); + const stateJson = JSON.stringify(state).replace(/<\//g, "<\\/"); + const safeTitle = title.replace(/[<>&]/g, ""); + return ` + + + + +${safeTitle} + + + +
+ + + +`; +} + +export const render = createRender(DiffSim); diff --git a/widget/js/diffsim/math.ts b/widget/js/diffsim/math.ts new file mode 100644 index 000000000..325722a0e --- /dev/null +++ b/widget/js/diffsim/math.ts @@ -0,0 +1,194 @@ +/** + * Small numerical kit for the diffraction simulator: quaternions (scalar + * first, the quantem convention: v_lab = R(q) v_crystal), base64 float32 + * decoding, and a complex Hermitian eigensolver (cyclic Jacobi) for the + * Bloch wave calculation in the browser. + */ + +export type Quat = [number, number, number, number]; +export type Vec3 = [number, number, number]; + +export function qmult(a: Quat, b: Quat): Quat { + const [aw, ax, ay, az] = a; + const [bw, bx, by, bz] = b; + return [ + aw * bw - ax * bx - ay * by - az * bz, + aw * bx + ax * bw + ay * bz - az * by, + aw * by - ax * bz + ay * bw + az * bx, + aw * bz + ax * by - ay * bx + az * bw, + ]; +} + +export function qnormalize(q: Quat): Quat { + const n = Math.hypot(q[0], q[1], q[2], q[3]) || 1; + const s = q[0] < 0 ? -1 / n : 1 / n; + return [q[0] * s, q[1] * s, q[2] * s, q[3] * s]; +} + +export function quatFromAxisAngle(axis: Vec3, angle: number): Quat { + const n = Math.hypot(axis[0], axis[1], axis[2]) || 1; + const s = Math.sin(angle / 2) / n; + return [Math.cos(angle / 2), axis[0] * s, axis[1] * s, axis[2] * s]; +} + +/** Row-major 3x3 rotation matrix R with v_lab = R v_crystal. */ +export function quatToMatrix(q: Quat): number[] { + const [w, x, y, z] = q; + return [ + 1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w), + 2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w), + 2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y), + ]; +} + +export function matVec(R: number[], v: Vec3): Vec3 { + return [ + R[0] * v[0] + R[1] * v[1] + R[2] * v[2], + R[3] * v[0] + R[4] * v[1] + R[5] * v[2], + R[6] * v[0] + R[7] * v[1] + R[8] * v[2], + ]; +} + +/** R^T v: lab to crystal frame. */ +export function matTVec(R: number[], v: Vec3): Vec3 { + return [ + R[0] * v[0] + R[3] * v[1] + R[6] * v[2], + R[1] * v[0] + R[4] * v[1] + R[7] * v[2], + R[2] * v[0] + R[5] * v[1] + R[8] * v[2], + ]; +} + +/** Quaternion putting the crystal-frame unit direction d along +z (lab). */ +export function quatFromZoneAxis(d: Vec3, inPlaneDeg = 0): Quat { + const n = Math.hypot(d[0], d[1], d[2]) || 1; + const v: Vec3 = [d[0] / n, d[1] / n, d[2] / n]; + const axis: Vec3 = [v[1], -v[0], 0]; // v x z + const sinT = Math.hypot(axis[0], axis[1]); + const angle = Math.atan2(sinT, v[2]); + const qTilt = sinT < 1e-12 ? quatFromAxisAngle([1, 0, 0], v[2] > 0 ? 0 : Math.PI) : quatFromAxisAngle(axis, angle); + const qSpin = quatFromAxisAngle([0, 0, 1], (inPlaneDeg * Math.PI) / 180); + return qnormalize(qmult(qSpin, qTilt)); +} + +export function decodeF32(b64: string): Float32Array { + if (!b64) return new Float32Array(0); + const bin = atob(b64); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + return new Float32Array(bytes.buffer); +} + +/** Smallest integer direction indices [uvw] with u a + v b + w c along d (crystal Cartesian), or null. */ +export function directionIndices(cell: number[][], d: Vec3, maxMult = 8): [number, number, number] | null { + // fractional coordinates of d: solve d = cell^T uvw -> uvw = inv(cell^T) d + const m = invert3(transpose3(cell)); + if (!m) return null; + const f = matVec(m, d); + const fm = Math.max(Math.abs(f[0]), Math.abs(f[1]), Math.abs(f[2])) || 1; + const v = f.map((x) => x / fm); + for (let mult = 1; mult <= maxMult; mult++) { + const w = v.map((x) => x * mult); + if (w.every((x) => Math.abs(x - Math.round(x)) < 0.02)) { + const ints = w.map((x) => Math.round(x)) as [number, number, number]; + const g = gcd3(ints); + return ints.map((x) => x / g) as [number, number, number]; + } + } + return null; +} + +function gcd(a: number, b: number): number { + a = Math.abs(a); b = Math.abs(b); + while (b) [a, b] = [b, a % b]; + return a; +} +function gcd3(v: [number, number, number]): number { + return Math.max(1, gcd(gcd(v[0], v[1]), v[2])); +} + +export function transpose3(a: number[][]): number[] { + return [a[0][0], a[1][0], a[2][0], a[0][1], a[1][1], a[2][1], a[0][2], a[1][2], a[2][2]]; +} + +export function invert3(m: number[]): number[] | null { + const [a, b, c, d, e, f, g, h, i] = m; + const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g; + const det = a * A + b * B + c * C; + if (Math.abs(det) < 1e-14) return null; + const inv = [ + A, -(b * i - c * h), b * f - c * e, + B, a * i - c * g, -(a * f - c * d), + C, -(a * h - b * g), a * e - b * d, + ]; + return inv.map((x) => x / det); +} + +/** + * Eigendecomposition of a complex Hermitian matrix by cyclic Jacobi + * rotations. re/im are row-major n x n; returns eigenvalues and the + * eigenvectors as columns (vecRe[i*n + j] = component i of eigenvector j). + * O(n^3) per sweep, a handful of sweeps: milliseconds for n ~ 100. + */ +export function eighComplex(re: Float64Array, im: Float64Array, n: number) { + const a = Float64Array.from(re); + const b = Float64Array.from(im); + const vr = new Float64Array(n * n); + const vi = new Float64Array(n * n); + for (let i = 0; i < n; i++) vr[i * n + i] = 1; + const idx = (i: number, j: number) => i * n + j; + for (let sweep = 0; sweep < 60; sweep++) { + let off = 0; + for (let p = 0; p < n; p++) for (let q = p + 1; q < n; q++) off += a[idx(p, q)] ** 2 + b[idx(p, q)] ** 2; + if (off < 1e-24) break; + for (let p = 0; p < n - 1; p++) { + for (let q = p + 1; q < n; q++) { + const apq_r = a[idx(p, q)], apq_i = b[idx(p, q)]; + const mag = Math.hypot(apq_r, apq_i); + if (mag < 1e-300) continue; + // phase rotation of column q (and its row) makes a_pq real positive + const cph = apq_r / mag, sph = apq_i / mag; // e^{i phi} = (cph, sph) + // column q *= e^{-i phi}; row q *= e^{i phi} + for (let k = 0; k < n; k++) { + const kr = a[idx(k, q)], ki = b[idx(k, q)]; + a[idx(k, q)] = kr * cph + ki * sph; + b[idx(k, q)] = ki * cph - kr * sph; + } + for (let k = 0; k < n; k++) { + const kr = a[idx(q, k)], ki = b[idx(q, k)]; + a[idx(q, k)] = kr * cph - ki * sph; + b[idx(q, k)] = ki * cph + kr * sph; + } + for (let k = 0; k < n; k++) { + const kr = vr[idx(k, q)], ki = vi[idx(k, q)]; + vr[idx(k, q)] = kr * cph + ki * sph; + vi[idx(k, q)] = ki * cph - kr * sph; + } + // real Jacobi rotation in the (p, q) plane + const app = a[idx(p, p)], aqq = a[idx(q, q)], apq = a[idx(p, q)]; + const theta = 0.5 * Math.atan2(2 * apq, aqq - app); + const c = Math.cos(theta), s = Math.sin(theta); + for (let k = 0; k < n; k++) { + // columns + const pr = a[idx(k, p)], pi = b[idx(k, p)], qr = a[idx(k, q)], qi = b[idx(k, q)]; + a[idx(k, p)] = c * pr - s * qr; b[idx(k, p)] = c * pi - s * qi; + a[idx(k, q)] = s * pr + c * qr; b[idx(k, q)] = s * pi + c * qi; + } + for (let k = 0; k < n; k++) { + // rows + const pr = a[idx(p, k)], pi = b[idx(p, k)], qr = a[idx(q, k)], qi = b[idx(q, k)]; + a[idx(p, k)] = c * pr - s * qr; b[idx(p, k)] = c * pi - s * qi; + a[idx(q, k)] = s * pr + c * qr; b[idx(q, k)] = s * pi + c * qi; + } + for (let k = 0; k < n; k++) { + const pr = vr[idx(k, p)], pi = vi[idx(k, p)], qr = vr[idx(k, q)], qi = vi[idx(k, q)]; + vr[idx(k, p)] = c * pr - s * qr; vi[idx(k, p)] = c * pi - s * qi; + vr[idx(k, q)] = s * pr + c * qr; vi[idx(k, q)] = s * pi + c * qi; + } + a[idx(p, q)] = 0; b[idx(p, q)] = 0; a[idx(q, p)] = 0; b[idx(q, p)] = 0; + } + } + } + const vals = new Float64Array(n); + for (let i = 0; i < n; i++) vals[i] = a[idx(i, i)]; + return { vals, vecRe: vr, vecIm: vi }; +} diff --git a/widget/js/diffsim/pattern.ts b/widget/js/diffsim/pattern.ts new file mode 100644 index 000000000..29d95fadc --- /dev/null +++ b/widget/js/diffsim/pattern.ts @@ -0,0 +1,296 @@ +/** + * Pattern renderers for the simulator: nanobeam markers or pixels, CBED + * disks, Kossel lines and the Kossel reference lookup. Every renderer works + * in canvas coordinates with q_x to the right and q_y up. + */ + +import { COLORMAPS, applyColormap } from "../colormaps"; +import type { Reflection } from "./physics"; +import type { KosselLine } from "./physics"; + +export interface Frame { + size: number; // canvas CSS px (square) + qMax: number; // 1/A at the edge (nanobeam / CBED) or rad (Kossel) + viewX: number; // +1: seen from the gun side (lab x to the right); -1: from the detector side (mirrored) +} + +export function scaleOf(f: Frame): number { + return (0.5 * f.size * 0.92) / f.qMax; // px per unit +} + +/** Lab (x, y) in pattern units to canvas px. */ +export function toPx(f: Frame, x: number, y: number): [number, number] { + const s = scaleOf(f); + return [f.size / 2 + f.viewX * x * s, f.size / 2 - y * s]; +} + +/** Canvas px to lab (x, y) in pattern units. */ +export function fromPx(f: Frame, px: number, py: number): [number, number] { + const s = scaleOf(f); + return [(f.viewX * (px - f.size / 2)) / s, -(py - f.size / 2) / s]; +} + +export function setupCanvas(canvas: HTMLCanvasElement, size: number): CanvasRenderingContext2D | null { + const dpr = window.devicePixelRatio || 1; + if (canvas.width !== size * dpr || canvas.height !== size * dpr) { + canvas.width = size * dpr; + canvas.height = size * dpr; + } + const ctx = canvas.getContext("2d"); + if (!ctx) return null; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + return ctx; +} + +/** Nanobeam pattern as markers with area ~ sqrt(intensity). */ +export function drawMarkers( + ctx: CanvasRenderingContext2D, f: Frame, beams: Reflection[], inten: Float64Array, dark: boolean, + labels: boolean, kinematic: boolean, +) { + const s = scaleOf(f); + ctx.fillStyle = dark ? "#000" : "#fff"; + ctx.fillRect(0, 0, f.size, f.size); + let iMax = 0; + for (let i = 0; i < beams.length; i++) if (beams[i].index >= 0 || !kinematic) iMax = Math.max(iMax, inten[i]); + if (iMax <= 0) iMax = 1; + // marker radius capped so neighbouring spots of the densest net do not merge + let gMin = Infinity; + for (const b of beams) if (b.index >= 0 && b.gLen > 1e-6) gMin = Math.min(gMin, b.gLen); + const rMax = Math.min(0.055 * f.size, isFinite(gMin) ? 0.42 * gMin * s : Infinity); + const fg = dark ? "#fff" : "#000"; + const strong: { x: number; y: number; r: number; hkl: number[] }[] = []; + for (let i = 0; i < beams.length; i++) { + const b = beams[i]; + const rel = inten[i] / iMax; + const [x, y] = toPx(f, b.g[0], b.g[1]); + if (b.index < 0 && kinematic) { + ctx.strokeStyle = fg; ctx.lineWidth = 1.5; + ctx.beginPath(); ctx.arc(x, y, rMax * 0.9, 0, 2 * Math.PI); ctx.stroke(); + strong.push({ x, y, r: rMax * 0.9, hkl: b.hkl }); + continue; + } + if (rel < 1e-6) continue; + const r = rMax * Math.pow(rel, 0.25); + ctx.fillStyle = fg; + ctx.globalAlpha = 0.9; + ctx.beginPath(); ctx.arc(x, y, Math.max(r, 0.6), 0, 2 * Math.PI); ctx.fill(); + ctx.globalAlpha = 1; + if (rel > 0.08) strong.push({ x, y, r, hkl: b.hkl }); + } + if (labels) { + ctx.font = `${Math.max(9, Math.round(f.size / 38))}px sans-serif`; + ctx.textAlign = "center"; ctx.textBaseline = "bottom"; + ctx.fillStyle = dark ? "#ffd54f" : "#c62828"; + for (const p of strong.slice(0, 40)) { + ctx.fillText(hklText(p.hkl), p.x, p.y - p.r - 2); + } + } + // scale bar of 1 1/A + drawScaleBar(ctx, f, s, "Å⁻¹", dark, 1); +} + +export function hklText(hkl: number[]): string { + return hkl.map((h) => (h < 0 ? `${-h}̅` : `${h}`)).join(""); +} + +function drawScaleBar(ctx: CanvasRenderingContext2D, f: Frame, s: number, unit: string, dark: boolean, value: number) { + let v = value; + while (v * s > 0.4 * f.size) v /= 2; + while (v * s < 0.12 * f.size) v *= 2; + const L = v * s; + const x0 = f.size - L - 14, y0 = f.size - 14; + ctx.strokeStyle = dark ? "#eee" : "#222"; + ctx.fillStyle = dark ? "#eee" : "#222"; + ctx.lineWidth = 3; + ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x0 + L, y0); ctx.stroke(); + ctx.font = "11px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "bottom"; + const label = v >= 1 ? `${+v.toFixed(2)} ${unit}` : `${+(v * 1000).toFixed(0)} m${unit}`; + ctx.fillText(label, x0 + L / 2, y0 - 4); +} + +/** Splat beams as Gaussian spots into a float image (canvas order). */ +export function nanobeamImage(f: Frame, beams: Reflection[], inten: Float64Array, spotPx: number): Float32Array { + const n = f.size; + const img = new Float32Array(n * n); + const sig = spotPx; + const w = Math.ceil(3.5 * sig); + for (let i = 0; i < beams.length; i++) { + if (inten[i] <= 0) continue; + const [x, y] = toPx(f, beams[i].g[0], beams[i].g[1]); + const amp = inten[i] / (2 * Math.PI * sig * sig); + const x0 = Math.max(0, Math.floor(x - w)), x1 = Math.min(n - 1, Math.ceil(x + w)); + const y0 = Math.max(0, Math.floor(y - w)), y1 = Math.min(n - 1, Math.ceil(y + w)); + for (let py = y0; py <= y1; py++) { + const dy = py + 0.5 - y; + for (let px = x0; px <= x1; px++) { + const dx = px + 0.5 - x; + img[py * n + px] += amp * Math.exp(-(dx * dx + dy * dy) / (2 * sig * sig)); + } + } + } + return img; +} + +export interface TiltGrid { + n: number; // grid points per side + h: number; // spacing, 1/A + R: number; // disk radius, 1/A + tilts: [number, number][]; // grid points within R + h (row-major over the n x n grid, NaN-free) + index: Int32Array; // n*n -> position in tilts or -1 +} + +export function tiltGrid(R: number, n: number): TiltGrid { + const h = (2 * R) / (n - 1); + const tilts: [number, number][] = []; + const index = new Int32Array(n * n).fill(-1); + for (let j = 0; j < n; j++) { + for (let i = 0; i < n; i++) { + const tx = -R + i * h, ty = -R + j * h; + if (Math.hypot(tx, ty) <= R + 1.01 * h) { + index[j * n + i] = tilts.length; + tilts.push([tx, ty]); + } + } + } + return { n, h, R, tilts, index }; +} + +/** + * CBED image: for every beam a disk of radius R centered on g, with the + * intensity at each pixel interpolated bilinearly from the tilt grid. + * intensities[t][b] is the intensity of beam b at tilt t. + */ +export function cbedImage(f: Frame, beams: Reflection[], grid: TiltGrid, intensities: Float64Array[]): Float32Array { + const n = f.size; + const img = new Float32Array(n * n); + const s = scaleOf(f); + const Rpx = grid.R * s; + const lookup = (b: number, tx: number, ty: number): number => { + const fx = (tx + grid.R) / grid.h, fy = (ty + grid.R) / grid.h; + const i0 = Math.min(Math.max(Math.floor(fx), 0), grid.n - 2); + const j0 = Math.min(Math.max(Math.floor(fy), 0), grid.n - 2); + const wx = Math.min(Math.max(fx - i0, 0), 1), wy = Math.min(Math.max(fy - j0, 0), 1); + const v = (i: number, j: number) => { + const k = grid.index[j * grid.n + i]; + return k < 0 ? 0 : intensities[k][b]; + }; + return v(i0, j0) * (1 - wx) * (1 - wy) + v(i0 + 1, j0) * wx * (1 - wy) + v(i0, j0 + 1) * (1 - wx) * wy + v(i0 + 1, j0 + 1) * wx * wy; + }; + for (let b = 0; b < beams.length; b++) { + const [cx, cy] = toPx(f, beams[b].g[0], beams[b].g[1]); + if (cx < -Rpx || cy < -Rpx || cx > n + Rpx || cy > n + Rpx) continue; + let anyInt = 0; + for (const arr of intensities) if (arr[b] > 1e-7) { anyInt = 1; break; } + if (!anyInt) continue; + const x0 = Math.max(0, Math.floor(cx - Rpx - 1)), x1 = Math.min(n - 1, Math.ceil(cx + Rpx + 1)); + const y0 = Math.max(0, Math.floor(cy - Rpx - 1)), y1 = Math.min(n - 1, Math.ceil(cy + Rpx + 1)); + for (let py = y0; py <= y1; py++) { + const dy = (py + 0.5 - cy); + for (let px = x0; px <= x1; px++) { + const dx = (px + 0.5 - cx); + const rr = Math.hypot(dx, dy); + if (rr > Rpx + 0.5) continue; + const edge = Math.min(1, Rpx + 0.5 - rr); // anti-aliased rim + const tx = (f.viewX * dx) / s, ty = -dy / s; + img[py * n + px] += edge * lookup(b, tx, ty); + } + } + } + return img; +} + +/** Colormapped float image onto the canvas, with a percentile contrast window. */ +export function drawImage( + ctx: CanvasRenderingContext2D, f: Frame, img: Float32Array, cmap: string, vmin: number, vmax: number, + dark: boolean, unit: string, barValue: number, +) { + const n = f.size; + const lut = COLORMAPS[cmap] || COLORMAPS[Object.keys(COLORMAPS)[0]]; + const rgba = new Uint8ClampedArray(n * n * 4); + const clean = new Float32Array(n * n); + for (let i = 0; i < n * n; i++) clean[i] = isFinite(img[i]) ? img[i] : vmin; + applyColormap(clean, rgba, lut, vmin, vmax); + for (let i = 0; i < n * n; i++) if (!isFinite(img[i])) rgba[4 * i + 3] = 0; + const off = document.createElement("canvas"); + off.width = n; off.height = n; + const octx = off.getContext("2d"); + if (!octx) return; + octx.putImageData(new ImageData(rgba, n, n), 0, 0); + ctx.fillStyle = dark ? "#000" : "#fff"; + ctx.fillRect(0, 0, n, n); + ctx.imageSmoothingEnabled = false; + ctx.drawImage(off, 0, 0, n, n); + drawScaleBar(ctx, f, scaleOf(f), unit, dark, barValue); +} + +/** Bright field Kossel pattern as vector lines (deficient lines dark). */ +export function drawKosselLines( + ctx: CanvasRenderingContext2D, f: Frame, lines: KosselLine[], dark: boolean, labels: boolean, minStrength: number, +) { + const n = f.size, s = scaleOf(f); + const cx = n / 2, cy = n / 2, Rpx = f.qMax * s; + ctx.fillStyle = dark ? "#000" : "#fff"; + ctx.fillRect(0, 0, n, n); + ctx.save(); + ctx.beginPath(); ctx.arc(cx, cy, Rpx, 0, 2 * Math.PI); ctx.clip(); + ctx.fillStyle = dark ? "#bdbdbd" : "#e0e0e0"; + ctx.fill(); + const sorted = [...lines].filter((l) => l.strength >= minStrength).sort((a, b) => a.strength - b.strength); + const L = 2 * f.qMax; + for (const l of sorted) { + const [nx, ny] = l.normal; + // line p . n = distance, direction t = (-ny, nx); drawn between two lab points + const [x0, y0] = toPx(f, nx * l.distance - ny * L, ny * l.distance + nx * L); + const [x1, y1] = toPx(f, nx * l.distance + ny * L, ny * l.distance - nx * L); + ctx.strokeStyle = dark ? `rgba(20,20,20,${0.25 + 0.75 * l.strength})` : `rgba(30,30,30,${0.2 + 0.8 * l.strength})`; + ctx.lineWidth = Math.max(1, l.width * s); + ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); + } + if (labels) { + ctx.font = `${Math.max(9, Math.round(n / 40))}px sans-serif`; + ctx.fillStyle = dark ? "#ffd54f" : "#c62828"; + ctx.textAlign = "center"; ctx.textBaseline = "middle"; + const strong = sorted.filter((l) => l.strength > 0.3 && Math.abs(l.distance) < f.qMax * 0.95).slice(-24); + for (const l of strong) { + const [nx, ny] = l.normal; + // label near the rim along the line + const t = Math.sqrt(Math.max(f.qMax * f.qMax * 0.8 - l.distance * l.distance, 0)); + const [x, y] = toPx(f, nx * l.distance - ny * t, ny * l.distance + nx * t); + ctx.fillText(hklText(l.hkl), x, y); + } + } + ctx.restore(); + drawScaleBar(ctx, f, s, "rad", dark, 0.01); +} + +/** Kikuchi line pairs overlaid on a nanobeam pattern (deficient dark, excess bright). */ +export function drawKikuchiOverlay(ctx: CanvasRenderingContext2D, f: Frame, lines: KosselLine[], k0: number, dark: boolean) { + const L = 3 * f.qMax; + for (const l of lines) { + if (l.strength < 0.25) continue; + const d = l.distance * k0; // 1/A + const [nx, ny] = l.normal; + const draw = (dist: number, color: string) => { + const [x0, y0] = toPx(f, nx * dist - ny * L, ny * dist + nx * L); + const [x1, y1] = toPx(f, nx * dist + ny * L, ny * dist - nx * L); + ctx.strokeStyle = color; + ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); + }; + draw(d, dark ? `rgba(120,170,255,${0.3 + 0.5 * l.strength})` : `rgba(30,90,200,${0.3 + 0.5 * l.strength})`); + draw(d + l.gxy, dark ? `rgba(255,140,120,${0.3 + 0.5 * l.strength})` : `rgba(200,60,40,${0.3 + 0.5 * l.strength})`); + } +} + +/** 256-bin histogram of the finite values. */ +export function histogramBins(img: Float32Array, lo: number, hi: number): number[] { + const bins = new Array(256).fill(0); + const range = hi > lo ? hi - lo : 1; + for (let i = 0; i < img.length; i++) { + const v = img[i]; + if (!isFinite(v)) continue; + const b = Math.min(255, Math.max(0, Math.floor(((v - lo) / range) * 255))); + bins[b]++; + } + return bins; +} diff --git a/widget/js/diffsim/physics.ts b/widget/js/diffsim/physics.ts new file mode 100644 index 000000000..65dc56fe8 --- /dev/null +++ b/widget/js/diffsim/physics.ts @@ -0,0 +1,375 @@ +/** + * Diffraction physics for the simulator, all in the browser: reciprocal + * lattice geometry, kinematical intensities, the Bloch wave calculation + * (structure matrix, eigendecomposition, thickness dependence, first-order + * absorption), CBED tilt sampling, Kossel line geometry and the reference + * pattern lookup. + */ + +import { Quat, Vec3, decodeF32, eighComplex, matVec, quatToMatrix } from "./math"; + +export interface CrystalData { + name: string; + spacegroup: string; + pointgroup: string; + cell: number[][]; + recip: number[][]; + positions_frac: number[][]; + numbers: number[]; + symbols: string[]; + colors: number[][]; + radii: number[]; + hkl: number[][]; + g: Float32Array; // (N, 3) crystal frame + F2: Float32Array; + U_re: Float32Array; + U_im: Float32Array; + couplingRe: Map; + couplingIm: Map; + u0_imag: number; + absorptive: boolean; + energy_ev: number; + wavelength: number; + k_max: number; + hexagonal: boolean; +} + +export function parseCrystal(json: string): CrystalData | null { + if (!json || json === "{}") return null; + const o = JSON.parse(json); + // reflection indices packed as int16 triplets; g rebuilt from the reciprocal cell + const bin = atob(o.hkl_i16 || ""); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + const hi16 = new Int16Array(bytes.buffer); + const n = hi16.length / 3; + const hkl: number[][] = new Array(n); + const g = new Float32Array(3 * n); + const B: number[][] = o.recip; + for (let i = 0; i < n; i++) { + const h = hi16[3 * i], k = hi16[3 * i + 1], l = hi16[3 * i + 2]; + hkl[i] = [h, k, l]; + g[3 * i] = h * B[0][0] + k * B[1][0] + l * B[2][0]; + g[3 * i + 1] = h * B[0][1] + k * B[1][1] + l * B[2][1]; + g[3 * i + 2] = h * B[0][2] + k * B[1][2] + l * B[2][2]; + } + const U_re = decodeF32(o.U_re), U_im = decodeF32(o.U_im); + const couplingRe = new Map(); + const couplingIm = new Map(); + for (let i = 0; i < n; i++) { + const key = `${hkl[i][0]},${hkl[i][1]},${hkl[i][2]}`; + couplingRe.set(key, U_re[i]); + couplingIm.set(key, U_im[i]); + } + return { + name: o.name, spacegroup: o.spacegroup, pointgroup: o.pointgroup, + cell: o.cell, recip: o.recip, positions_frac: o.positions_frac, numbers: o.numbers, + symbols: o.symbols, colors: o.colors, radii: o.radii, hkl, + g, F2: decodeF32(o.F2), U_re, U_im, + couplingRe, couplingIm, u0_imag: o.u0_imag, absorptive: o.absorptive, + energy_ev: o.energy_ev, wavelength: o.wavelength, k_max: o.k_max, hexagonal: o.hexagonal, + }; +} + +export interface Reflection { + index: number; + hkl: number[]; + g: Vec3; // lab frame + gLen: number; + s: number; // excitation error at zero tilt +} + +/** Lab-frame reflections within kMax, with excitation errors for the beam along -z. */ +export function labReflections(c: CrystalData, q: Quat, kMax: number): Reflection[] { + const R = quatToMatrix(q); + const lam = c.wavelength; + const out: Reflection[] = []; + const n = c.hkl.length; + for (let i = 0; i < n; i++) { + const gc: Vec3 = [c.g[3 * i], c.g[3 * i + 1], c.g[3 * i + 2]]; + const gLen = Math.hypot(gc[0], gc[1], gc[2]); + if (gLen > kMax) continue; + const g = matVec(R, gc); + const g2 = gLen * gLen; + const s = (2 * g[2] - lam * g2) / (2 - 2 * lam * g[2]); + out.push({ index: i, hkl: c.hkl[i], g, gLen, s }); + } + return out; +} + +/** Kinematical intensities |F|^2 exp(-s^2 / 2 sigma^2). */ +export function kinematicalIntensities(c: CrystalData, refl: Reflection[], sigma: number): Float64Array { + const out = new Float64Array(refl.length); + for (let i = 0; i < refl.length; i++) { + const r = refl[i]; + out[i] = c.F2[r.index] * Math.exp(-(r.s * r.s) / (2 * sigma * sigma)); + } + return out; +} + +export interface BlochSolution { + beams: Reflection[]; // beam 0 is the direct beam (hkl 000) + n: number; + gammaRe: Float64Array; // 1/A + gammaIm: Float64Array; + vecRe: Float64Array; // columns = eigenvectors + vecIm: Float64Array; + psi0Re: Float64Array; // conj(C_0j) + psi0Im: Float64Array; +} + +const DIRECT: Reflection = { index: -1, hkl: [0, 0, 0], g: [0, 0, 0], gLen: 0, s: 0 }; + +function tiltedExcitation(r: Reflection, kz: number, tilt: [number, number]): number { + const g2 = r.gLen * r.gLen; + const num = 2 * kz * r.g[2] - 2 * (tilt[0] * r.g[0] + tilt[1] * r.g[1]) - g2; + return num / (2 * (kz - r.g[2])); +} + +/** + * Beams entering the Bloch calculation: the direct beam plus every + * reflection within sgMax of the Ewald sphere at zero tilt (sgMax widened + * by the tilt range when a convergent beam is sampled), capped at maxBeams + * by keeping the beams with the largest |U_g| / |s_g|. + */ +export function selectBeams(c: CrystalData, q: Quat, kMax: number, sgMax: number, maxBeams: number, tiltRange = 0): Reflection[] { + const all = labReflections(c, q, kMax); + const widen = tiltRange * kMax; // max |delta s| = sin(alpha) |g| + let beams = all.filter((r) => Math.abs(r.s) < sgMax + widen); + if (beams.length > maxBeams) { + const score = (r: Reflection) => Math.hypot(c.U_re[r.index], c.U_im[r.index]) / (Math.abs(r.s) + 1e-4); + beams.sort((a, b) => score(b) - score(a)); + beams = beams.slice(0, maxBeams); + } + return [DIRECT, ...beams]; +} + +/** + * Bloch wave eigenproblem for a fixed beam list and an incident beam with + * in-plane wavevector tilt (1/A). The structure matrix has 2 k0 s_g on the + * diagonal and the couplings U_(g-h) off it; the Hermitian part is + * diagonalized and the absorption enters to first order (the + * fast_absorption path of quantem). + */ +export function blochSolve(c: CrystalData, beams: Reflection[], tilt: [number, number] = [0, 0]): BlochSolution { + const k0 = 1 / c.wavelength; + const kz = Math.sqrt(Math.max(k0 * k0 - tilt[0] ** 2 - tilt[1] ** 2, 1e-12)); + const n = beams.length; + const sg = beams.map((r) => (r.index < 0 ? 0 : tiltedExcitation(r, kz, tilt))); + const hRe = new Float64Array(n * n); + const hIm = new Float64Array(n * n); + const wRe = new Float64Array(n * n); + const wIm = new Float64Array(n * n); + for (let i = 0; i < n; i++) { + hRe[i * n + i] = 2 * k0 * sg[i]; + for (let j = 0; j < n; j++) { + if (i === j) continue; + const d0 = beams[i].hkl[0] - beams[j].hkl[0], d1 = beams[i].hkl[1] - beams[j].hkl[1], d2 = beams[i].hkl[2] - beams[j].hkl[2]; + const ur = c.couplingRe.get(`${d0},${d1},${d2}`) ?? 0; + const ui = c.couplingIm.get(`${d0},${d1},${d2}`) ?? 0; + const tr = c.couplingRe.get(`${-d0},${-d1},${-d2}`) ?? 0; + const ti = c.couplingIm.get(`${-d0},${-d1},${-d2}`) ?? 0; + // A_ij = U_(gi - gj) = (ur, ui); conj(A_ji) = (tr, -ti) + // H = (A + A^dagger) / 2, W = (A - H) / i + const hr = 0.5 * (ur + tr), hi = 0.5 * (ui - ti); + hRe[i * n + j] = hr; + hIm[i * n + j] = hi; + wRe[i * n + j] = ui - hi; + wIm[i * n + j] = -(ur - hr); + } + } + const { vals, vecRe, vecIm } = eighComplex(hRe, hIm, n); + const gammaRe = new Float64Array(n); + const gammaIm = new Float64Array(n); + for (let j = 0; j < n; j++) { + gammaRe[j] = vals[j] / (2 * k0); + let acc = 0; + for (let i = 0; i < n; i++) { + let sr = 0, si = 0; + for (let k = 0; k < n; k++) { + const wr = wRe[i * n + k], wi = wIm[i * n + k]; + if (wr === 0 && wi === 0) continue; + const cr = vecRe[k * n + j], ci = vecIm[k * n + j]; + sr += wr * cr - wi * ci; + si += wr * ci + wi * cr; + } + acc += vecRe[i * n + j] * sr + vecIm[i * n + j] * si; + } + gammaIm[j] = (acc + c.u0_imag) / (2 * k0); + } + const psi0Re = new Float64Array(n); + const psi0Im = new Float64Array(n); + for (let j = 0; j < n; j++) { + psi0Re[j] = vecRe[j]; + psi0Im[j] = -vecIm[j]; + } + return { beams, n, gammaRe, gammaIm, vecRe, vecIm, psi0Re, psi0Im }; +} + +/** + * Beam list for the hybrid pattern: the direct beam, then the Bloch set + * (within sgMax of the Ewald sphere, capped), then every other reflection + * within kMax. The first nDyn beams enter the Bloch calculation; the rest + * take thin-slab intensities (slabIntensities). + */ +export function hybridBeams(c: CrystalData, q: Quat, kMax: number, sgMax: number, maxBeams: number, tiltRange = 0): { beams: Reflection[]; nDyn: number } { + const dyn = selectBeams(c, q, kMax, sgMax, maxBeams, tiltRange); + const inDyn = new Set(); + for (const b of dyn) inDyn.add(b.index); + const rest = labReflections(c, q, kMax).filter((r) => !inDyn.has(r.index)); + return { beams: [...dyn, ...rest], nDyn: dyn.length }; +} + +/** + * Thin-slab (first Born) intensities I_g = (pi |U_g| t / k0)^2 sinc^2(pi s_g t) + * for beams[from..] at an incident tilt: the weak-beam limit of the Bloch + * result, in the same normalization (fraction of the incident intensity). + */ +export function slabIntensities(c: CrystalData, beams: Reflection[], from: number, tilt: [number, number], thickness: number, out: Float64Array) { + const k0 = 1 / c.wavelength; + const kz = Math.sqrt(Math.max(k0 * k0 - tilt[0] ** 2 - tilt[1] ** 2, 1e-12)); + for (let i = from; i < beams.length; i++) { + const r = beams[i]; + const s = tilt[0] === 0 && tilt[1] === 0 ? r.s : tiltedExcitation(r, kz, tilt); + const u = Math.hypot(c.U_re[r.index], c.U_im[r.index]); + const x = Math.PI * s * thickness; + const sinc = Math.abs(x) < 1e-8 ? 1 : Math.sin(x) / x; + const amp = (Math.PI * u * thickness) / k0; + out[i] = amp * amp * sinc * sinc; + } +} + +/** Kinematical intensities of a beam list at an incident tilt. */ +export function kinematicalTilted(c: CrystalData, beams: Reflection[], tilt: [number, number], sigma: number): Float64Array { + const k0 = 1 / c.wavelength; + const kz = Math.sqrt(Math.max(k0 * k0 - tilt[0] ** 2 - tilt[1] ** 2, 1e-12)); + const out = new Float64Array(beams.length); + for (let i = 0; i < beams.length; i++) { + const r = beams[i]; + if (r.index < 0) { out[i] = 1; continue; } + const s = tiltedExcitation(r, kz, tilt); + out[i] = c.F2[r.index] * Math.exp(-(s * s) / (2 * sigma * sigma)); + } + return out; +} + +/** Beam intensities at a thickness (A) from a Bloch solution. */ +export function blochIntensities(sol: BlochSolution, thickness: number): Float64Array { + const { n, gammaRe, gammaIm, vecRe, vecIm, psi0Re, psi0Im } = sol; + const phRe = new Float64Array(n); + const phIm = new Float64Array(n); + for (let j = 0; j < n; j++) { + const amp = Math.exp(-2 * Math.PI * gammaIm[j] * thickness); + const ph = 2 * Math.PI * gammaRe[j] * thickness; + const er = amp * Math.cos(ph), ei = amp * Math.sin(ph); + // e^{2 pi i gamma t} * conj(C_0j) + phRe[j] = er * psi0Re[j] - ei * psi0Im[j]; + phIm[j] = er * psi0Im[j] + ei * psi0Re[j]; + } + const out = new Float64Array(n); + for (let g = 0; g < n; g++) { + let sr = 0, si = 0; + for (let j = 0; j < n; j++) { + const cr = vecRe[g * n + j], ci = vecIm[g * n + j]; + sr += cr * phRe[j] - ci * phIm[j]; + si += cr * phIm[j] + ci * phRe[j]; + } + out[g] = sr * sr + si * si; + } + return out; +} + +export interface KosselLine { + hkl: number[]; + normal: [number, number]; // unit, in the tilt plane (rad) + distance: number; // rad, line is {theta : theta . normal = distance} + width: number; // rad + strength: number; // relative 0..1 + gxy: number; // |g_xy|, 1/A (excess Kikuchi line offset) +} + +/** + * Kossel (deficiency) lines of every reflection in the tilt plane: + * g_xy . theta = g_z - lambda |g|^2 / 2, a straight line at small angles. + * The width is the two-beam rocking width |U_g| / (k0 |g_xy|). + */ +export function kosselLines(c: CrystalData, q: Quat, kMax: number, fieldRad: number): KosselLine[] { + const R = quatToMatrix(q); + const lam = c.wavelength; + const k0 = 1 / lam; + const lines: KosselLine[] = []; + let uMax = 1e-12; + for (let i = 0; i < c.hkl.length; i++) uMax = Math.max(uMax, Math.hypot(c.U_re[i], c.U_im[i])); + for (let i = 0; i < c.hkl.length; i++) { + const gc: Vec3 = [c.g[3 * i], c.g[3 * i + 1], c.g[3 * i + 2]]; + const gLen = Math.hypot(gc[0], gc[1], gc[2]); + if (gLen > kMax) continue; + const g = matVec(R, gc); + const gxy = Math.hypot(g[0], g[1]); + if (gxy < 1e-6) continue; + const dist = (g[2] - (lam * gLen * gLen) / 2) / gxy; + if (Math.abs(dist) > fieldRad * 1.5) continue; + const u = Math.hypot(c.U_re[i], c.U_im[i]); + if (u < 1e-4 * uMax) continue; + lines.push({ hkl: c.hkl[i], normal: [g[0] / gxy, g[1] / gxy], distance: dist, width: u / (k0 * gxy), strength: u / uMax, gxy }); + } + return lines; +} + +export interface KosselReference { + shape: number[]; // (T, n, n) + step: number; + thicknesses: number[]; + data: Float32Array; +} + +export function parseKossel(json: string): KosselReference | null { + if (!json || json === "{}") return null; + const o = JSON.parse(json); + return { shape: o.shape, step: o.step, thicknesses: o.thicknesses, data: decodeF32(o.data) }; +} + +/** Bright field Kossel image (size x size) over +-fieldRad by lookup in the reference. */ +export function kosselLookup(ref: KosselReference, q: Quat, fieldRad: number, size: number, thickness: number, viewX = 1): Float32Array { + const R = quatToMatrix(q); + const [T, n] = ref.shape; + const half = (n - 1) / 2; + // nearest thickness with linear blend + let ti = 0; + for (let k = 0; k < T; k++) if (Math.abs(ref.thicknesses[k] - thickness) < Math.abs(ref.thicknesses[ti] - thickness)) ti = k; + const plane = ti * n * n; + const out = new Float32Array(size * size); + for (let r = 0; r < size; r++) { + const ty = -((r + 0.5) / size - 0.5) * 2 * fieldRad; // canvas rows run downward + for (let col = 0; col < size; col++) { + const tx = viewX * ((col + 0.5) / size - 0.5) * 2 * fieldRad; + if (tx * tx + ty * ty > fieldRad * fieldRad) { out[r * size + col] = NaN; continue; } + // incident wavevector is (t, -kz) in quantem (beam travels along -z); the + // reference is indexed by the anti-propagation direction (-t, +kz)/k0 + const dl: Vec3 = [-tx, -ty, Math.sqrt(Math.max(1 - tx * tx - ty * ty, 0))]; + let dc: Vec3 = [R[0] * dl[0] + R[3] * dl[1] + R[6] * dl[2], R[1] * dl[0] + R[4] * dl[1] + R[7] * dl[2], R[2] * dl[0] + R[5] * dl[1] + R[8] * dl[2]]; + if (dc[2] < 0) dc = [-dc[0], -dc[1], -dc[2]]; + const rho = Math.sqrt(Math.max(2 * (1 - dc[2]), 0)); + const dxy = Math.max(Math.hypot(dc[0], dc[1]), 1e-12); + const fx = (dc[0] / dxy) * rho / ref.step + half; + const fy = (dc[1] / dxy) * rho / ref.step + half; + const ix = Math.min(Math.max(Math.floor(fx), 0), n - 2), iy = Math.min(Math.max(Math.floor(fy), 0), n - 2); + const wx = Math.min(Math.max(fx - ix, 0), 1), wy = Math.min(Math.max(fy - iy, 0), 1); + const v = ref.data[plane + ix * n + iy] * (1 - wx) * (1 - wy) + ref.data[plane + (ix + 1) * n + iy] * wx * (1 - wy) + + ref.data[plane + ix * n + iy + 1] * (1 - wx) * wy + ref.data[plane + (ix + 1) * n + iy + 1] * wx * wy; + out[r * size + col] = v; + } + } + return out; +} + +/** Percentile-based display range of finite values. */ +export function percentiles(data: Float32Array | Float64Array, pLow: number, pHigh: number): [number, number] { + const vals: number[] = []; + for (let i = 0; i < data.length; i++) if (isFinite(data[i])) vals.push(data[i]); + if (!vals.length) return [0, 1]; + vals.sort((a, b) => a - b); + const lo = vals[Math.min(vals.length - 1, Math.floor((pLow / 100) * (vals.length - 1)))]; + const hi = vals[Math.min(vals.length - 1, Math.floor((pHigh / 100) * (vals.length - 1)))]; + return hi > lo ? [lo, hi] : [lo, lo + 1e-12]; +} diff --git a/widget/src/quantem/widget/__init__.py b/widget/src/quantem/widget/__init__.py index 96d8aebc3..5024fb6ea 100644 --- a/widget/src/quantem/widget/__init__.py +++ b/widget/src/quantem/widget/__init__.py @@ -1,5 +1,6 @@ from importlib.metadata import PackageNotFoundError, version +from quantem.widget.diffsim import DiffractionSim from quantem.widget.show2d import Show2D from quantem.widget.show4dstem import Show4DSTEM @@ -9,4 +10,4 @@ # Source-tree imports (e.g. `PYTHONPATH=src pytest`) skip pip install. __version__ = "0.0.0+local" -__all__ = ["Show2D", "Show4DSTEM"] +__all__ = ["DiffractionSim", "Show2D", "Show4DSTEM"] diff --git a/widget/src/quantem/widget/diffsim.py b/widget/src/quantem/widget/diffsim.py new file mode 100644 index 000000000..4c1cd883a --- /dev/null +++ b/widget/src/quantem/widget/diffsim.py @@ -0,0 +1,519 @@ +""" +DiffractionSim: interactive crystal and diffraction simulation for teaching. + +Left panel: the unit cell in 3D, rotated by dragging (mouse or touch) or by +buttons about the screen axes. Right panel: the diffraction pattern of the +same orientation, updated live: nanobeam (kinematical markers, or Bloch wave +intensities that follow the thickness slider), CBED disks, or Kossel / +LACBED lines. Every simulation runs in the browser, so the widget can be +exported as a single HTML file and embedded in a web page without Python. +""" + +from __future__ import annotations + +import base64 +import json +import pathlib +import re + +import anywidget +import numpy as np +import torch +import traitlets + +_STATIC = pathlib.Path(__file__).parent / "static" / "diffsim.js" + +# ASE bulk structures offered in the crystal menu +PRESETS: dict[str, dict] = { + "Si (diamond cubic)": dict(name="Si", crystalstructure="diamond", a=5.431, cubic=True), + "Ge (diamond cubic)": dict(name="Ge", crystalstructure="diamond", a=5.658, cubic=True), + "Al (fcc)": dict(name="Al", crystalstructure="fcc", a=4.05, cubic=True), + "Cu (fcc)": dict(name="Cu", crystalstructure="fcc", a=3.615, cubic=True), + "Au (fcc)": dict(name="Au", crystalstructure="fcc", a=4.078, cubic=True), + "Fe (bcc)": dict(name="Fe", crystalstructure="bcc", a=2.866, cubic=True), + "W (bcc)": dict(name="W", crystalstructure="bcc", a=3.165, cubic=True), + "Ti (hcp)": dict(name="Ti", crystalstructure="hcp", a=2.9505, c=4.6855), + "Mg (hcp)": dict(name="Mg", crystalstructure="hcp", a=3.209, c=5.211), + "GaAs (zincblende)": dict(name="GaAs", crystalstructure="zincblende", a=5.653, cubic=True), + "NaCl (rocksalt)": dict(name="NaCl", crystalstructure="rocksalt", a=5.64, cubic=True), + "SrTiO3 (perovskite)": dict( + symbols=["Sr", "Ti", "O"], + basis=[(0, 0, 0), (0.5, 0.5, 0.5), (0.5, 0.5, 0)], + spacegroup=221, + cellpar=[3.905, 3.905, 3.905, 90, 90, 90], + ), + "Al2O3 (corundum)": dict( + symbols=["Al", "O"], + basis=[(0, 0, 0.3523), (0.3064, 0, 0.25)], + spacegroup=167, + cellpar=[4.7602, 4.7602, 12.9933, 90, 90, 120], + ), + "SiO2 (alpha quartz)": dict( + symbols=["Si", "O"], + basis=[(0.4697, 0, 0), (0.4135, 0.2669, 0.1191)], + spacegroup=154, + cellpar=[4.9134, 4.9134, 5.4052, 90, 90, 120], + ), + "alpha-Mn (58 atoms)": dict( + symbols=["Mn", "Mn", "Mn", "Mn"], + basis=[ + (0, 0, 0), + (0.3175, 0.3175, 0.3175), + (0.3570, 0.3570, 0.0348), + (0.0896, 0.0896, 0.2820), + ], + spacegroup=217, + cellpar=[8.911, 8.911, 8.911, 90, 90, 90], + ), +} + + +def _preset_atoms(key: str): + from ase.build import bulk + + spec = PRESETS[key] + if "spacegroup" in spec: + from ase.spacegroup import crystal as ase_crystal + + return ase_crystal( + spec["symbols"], + basis=spec["basis"], + spacegroup=spec["spacegroup"], + cellpar=spec["cellpar"], + ) + return bulk(**spec) + + +def _f32_b64(a) -> str: + return base64.b64encode( + np.ascontiguousarray(np.asarray(a, dtype=np.float32)).tobytes() + ).decode() + + +def prepare_crystal(crystal, energy_ev: float, k_max: float) -> dict: + """Everything the browser needs to draw the cell and simulate patterns. + + One reflection list: the points of the primitive reciprocal lattice + within k_max (glide-forbidden reflections such as Si 200 and 222 are + present with zero kinematical intensity and fill by multiple scattering + in the Bloch calculation). Per reflection the kinematical |F_g|^2 + (Lobato) and the Bloch coupling U_g (absorptive Weickenmeier-Kohl + factors at this energy when available, else gamma F_g / pi). The browser + builds the structure matrix from the same list, so couplings between + beams further apart than k_max are taken as zero; at the default + k_max = 4 1/A those factors are below 2% of U_000 for every element. + Indices travel as int16, g is rebuilt from the reciprocal cell. + """ + from ase.data import chemical_symbols, covalent_radii + from ase.data.colors import jmol_colors + + from quantem.core.utils.utils import electron_wavelength_angstrom + from quantem.diffraction import bloch + + if crystal.g_vec is None or crystal.k_max is None or crystal.k_max < k_max: + crystal.calculate_structure_factors(k_max=k_max) + have_dyn = ( + getattr(crystal, "U_dyn", None) is not None + and abs(getattr(crystal, "dyn_energy_ev", -1) - energy_ev) < 1 + and getattr(crystal, "dyn_k_max", 0) >= k_max + ) + if not have_dyn: + try: + crystal.calculate_dynamical_structure_factors(energy_ev=energy_ev, k_max=k_max) + have_dyn = True + except Exception: + have_dyn = False + hkl_u, g_u = bloch._beam_universe(crystal) + keep = torch.linalg.norm(g_u, dim=1) <= k_max + hkl = hkl_u[keep] + gamma_rel = bloch.relativistic_gamma(energy_ev) + U_g, u0_imag = _coupling_vector(crystal, hkl, gamma_rel) + # kinematical |F|^2 (Lobato) on the same list + lut = {tuple(h): i for i, h in enumerate(crystal.hkl.tolist())} + F2 = torch.zeros(hkl.shape[0], dtype=torch.float64) + for i, h in enumerate(hkl.tolist()): + j = lut.get(tuple(h)) + if j is not None: + F2[i] = crystal.struct_factors_int[j] + numbers = crystal.numbers.numpy() + hkl_i16 = np.ascontiguousarray(hkl.numpy().astype(np.int16)) + return { + "name": crystal.name, + "spacegroup": getattr(crystal, "spacegroup", ""), + "pointgroup": getattr(crystal, "pointgroup", ""), + "cell": crystal.lat_real.numpy().tolist(), + "recip": crystal.lat_recip.numpy().tolist(), + "positions_frac": crystal.positions_frac.numpy().tolist(), + "numbers": numbers.tolist(), + "symbols": [chemical_symbols[int(z)] for z in numbers], + "colors": [jmol_colors[int(z)].tolist() for z in numbers], + "radii": [float(covalent_radii[int(z)]) for z in numbers], + "hkl_i16": base64.b64encode(hkl_i16.tobytes()).decode(), + "F2": _f32_b64(F2.numpy()), + "U_re": _f32_b64(U_g.real.numpy()), + "U_im": _f32_b64(U_g.imag.numpy()), + "u0_imag": float(u0_imag), + "absorptive": bool(have_dyn), + "n_reflections": int(hkl.shape[0]), + "energy_ev": float(energy_ev), + "wavelength": float(electron_wavelength_angstrom(energy_ev)), + "k_max": float(k_max), + "hexagonal": bool(getattr(crystal, "hexagonal_matching", False)), + } + + +def _coupling_vector(crystal, hkl: torch.Tensor, gamma_rel: float) -> tuple[torch.Tensor, float]: + """U_g for a list of hkl (zero where no factor is stored) and the mean + absorption U_000''. Same factor choice as bloch._coupling_matrix, but a + vector lookup instead of the (N, N) difference matrix.""" + if getattr(crystal, "U_dyn", None) is not None: + hkl_all, U_all = crystal.hkl_dyn, crystal.U_dyn + else: + hkl_all, U_all = crystal.hkl, crystal.struct_factors * (gamma_rel / np.pi) + lut = {tuple(h): i for i, h in enumerate(hkl_all.tolist())} + idx = torch.tensor([lut.get(tuple(h), -1) for h in hkl.tolist()], dtype=torch.long) + U = torch.zeros(hkl.shape[0], dtype=torch.complex128) + has = idx >= 0 + U[has] = U_all[idx[has]] + i0 = lut.get((0, 0, 0), -1) + u0_imag = ( + float(U_all[i0].imag) if (i0 >= 0 and getattr(crystal, "U_dyn", None) is not None) else 0.0 + ) + return U, u0_imag + + +def prepare_kossel_reference( + crystal, energy_ev: float, thicknesses_A, angle_step_mrad=3.0, k_max=1.0 +): + """Bright field Kossel reference on the Lambert grid, for the pixel + rendering of the Kossel / LACBED mode (a lookup in the browser).""" + from quantem.diffraction import bloch + + master = bloch.calculate_kossel_reference( + crystal, + list(thicknesses_A), + energy_ev=energy_ev, + angle_step_mrad=angle_step_mrad, + sg_max=0.05, + k_max=k_max, + progress_bar=False, + ) + lam = np.nan_to_num(master["lambert"], nan=float(np.nanmax(master["lambert"]))) + return { + "shape": list(lam.shape), + "step": float(master["step"]), + "thicknesses": [float(t) for t in master["thicknesses"]], + "data": _f32_b64(lam), + } + + +class DiffractionSim(anywidget.AnyWidget): + """Interactive unit cell and diffraction pattern simulator. + + Parameters + ---------- + crystal : Crystal | ase.Atoms | str | None + A quantem Crystal, an ASE Atoms object, a CIF path, or the name of a + preset (see DiffractionSim.presets_available()). None starts with silicon. + energy_ev : float, default=200e3 + Beam energy. + k_max : float, default=4.0 + Largest scattering vector in the pattern (1/Angstroms). + zone_axis : sequence of 3 | None + Initial zone axis along the beam; None keeps the identity + orientation (c axis along the beam). + thickness_A, semiconv_mrad, sigma_excitation : float + Initial values of the thickness, convergence semiangle and + excitation envelope sliders. + pattern_range : float | None + Scattering vector at the edge of the nanobeam / CBED panel + (1/Angstroms); None shows everything out to k_max. + field_mrad : float, default=50 + Half angle of the Kossel / LACBED field of view. + sg_max : float, default=0.05 + Excitation error cutoff (1/Angstroms) selecting the Bloch beams; + reflections outside it take thin-slab intensities. + quality : {"fast", "medium", "fine"} + Bloch beam cap and CBED tilt sampling. + show_kikuchi : bool + Overlay the Kikuchi line pairs on the nanobeam pattern. + scaling : {"linear", "power", "log"} + Intensity scaling of the pixel renderings; "power" raises the + intensities to `power` (default 0.5). + cmap : str + Colormap of the pixel renderings, e.g. "inferno", "turbo_black", "gray". + view_from : {"detector", "gun"} + Viewpoint shared by both panels (a launch argument, no UI control). "detector" looks up the column from + the detector side: the exit face of the cell is nearest you and tilts + together with the Laue circle and Kikuchi pattern. "gun" is the + operator's view down the column; there the entrance face is nearest + and tilts opposite to the pattern (the Laue center marks where the + zone axis exits toward the detector). + mode : {"nanobeam", "cbed", "kossel"} + render : {"markers", "pixels"} + Nanobeam: markers sized by intensity, or a pixelated pattern. + Kossel: vector lines, or the pixel lookup of the reference pattern + (compute_kossel_reference()). + n_cells : sequence of 3 int, default=(1, 1, 1) + Block of cells drawn in the left panel (up to 6 per axis). + polyhedra : bool + Draw coordination polyhedra (convex hull of the nearest neighbours) + around every species except the most numerous one; around every + atom of an elemental crystal. + size : int + Height of the panels in CSS pixels. + + Examples + -------- + >>> from quantem.widget import DiffractionSim + >>> w = DiffractionSim("Si (diamond cubic)", zone_axis=[1, 1, 0]) + >>> w + >>> w.export_html("si_110.html") # standalone page, no Python needed + """ + + _esm = _STATIC + + crystal_json = traitlets.Unicode("{}").tag(sync=True) + presets = traitlets.List(trait=traitlets.Unicode(), default_value=list(PRESETS)).tag(sync=True) + preset = traitlets.Unicode("").tag(sync=True) + energy_ev = traitlets.Float(200e3).tag(sync=True) + k_max = traitlets.Float(4.0).tag(sync=True) + orientation = traitlets.List(trait=traitlets.Float(), default_value=[1.0, 0.0, 0.0, 0.0]).tag( + sync=True + ) + mode = traitlets.Unicode("nanobeam").tag(sync=True) + render = traitlets.Unicode("markers").tag(sync=True) + dynamical = traitlets.Bool(True).tag(sync=True) + thickness_A = traitlets.Float(500.0).tag(sync=True) + semiconv_mrad = traitlets.Float(2.0).tag(sync=True) + sigma_excitation = traitlets.Float(0.02).tag(sync=True) + rotation_step_deg = traitlets.Float(15.0).tag(sync=True) + pattern_range = traitlets.Float(4.0).tag(sync=True) + field_mrad = traitlets.Float(50.0).tag(sync=True) + sg_max = traitlets.Float(0.05).tag(sync=True) + quality = traitlets.Unicode("medium").tag(sync=True) + show_kikuchi = traitlets.Bool(False).tag(sync=True) + view_from = traitlets.Unicode("detector").tag(sync=True) + scaling = traitlets.Unicode("linear").tag(sync=True) + power = traitlets.Float(0.5).tag(sync=True) + cmap = traitlets.Unicode("inferno").tag(sync=True) + vmin_pct = traitlets.Float(0.0).tag(sync=True) + vmax_pct = traitlets.Float(100.0).tag(sync=True) + show_labels = traitlets.Bool(True).tag(sync=True) + show_cell_axes = traitlets.Bool(True).tag(sync=True) + n_cells = traitlets.List(trait=traitlets.Int(), default_value=[1, 1, 1]).tag(sync=True) + polyhedra = traitlets.Bool(False).tag(sync=True) + size = traitlets.Int(420).tag(sync=True) + kossel_json = traitlets.Unicode("{}").tag(sync=True) + status = traitlets.Unicode("").tag(sync=True) + widget_version = traitlets.Unicode("0.1").tag(sync=True) + + def __init__(self, crystal=None, zone_axis=None, pattern_range=None, **kwargs): + if "n_cells" in kwargs: + kwargs["n_cells"] = [int(n) for n in kwargs["n_cells"]] + super().__init__(**kwargs) + self.pattern_range = float(pattern_range) if pattern_range is not None else self.k_max + self._crystal = None + self._kossel_cache: dict = {} + if crystal is None: + crystal = "Si (diamond cubic)" + self.set_crystal(crystal) + if zone_axis is not None: + self.set_zone_axis(zone_axis) + self.observe(self._on_preset, names="preset") + self.observe(self._on_physics, names=["energy_ev", "k_max"]) + self.on_msg(self._on_message) + + # ------------------------------------------------------------------ + @staticmethod + def presets_available() -> list[str]: + return list(PRESETS) + + @property + def crystal(self): + return self._crystal + + def set_crystal(self, crystal) -> "DiffractionSim": + """Load a Crystal, ASE Atoms, CIF path or preset name.""" + from ase import Atoms + + from quantem.diffraction.crystal import Crystal + + preset_name = "" + if isinstance(crystal, str): + if crystal in PRESETS: + preset_name = crystal + xtl = Crystal.from_ase( + _preset_atoms(crystal), name=crystal.split(" (")[0], verbose=False + ) + else: + xtl = Crystal.from_cif(crystal, verbose=False) + elif isinstance(crystal, Atoms): + xtl = Crystal.from_ase(crystal, verbose=False) + else: + xtl = crystal + self._crystal = xtl + self.crystal_json = json.dumps(prepare_crystal(xtl, self.energy_ev, self.k_max)) + self.kossel_json = "{}" + if preset_name and self.preset != preset_name: + self.preset = preset_name + return self + + def set_zone_axis(self, zone_axis, in_plane_deg: float = 0.0) -> "DiffractionSim": + """Put a crystal direction [uvw] along the beam.""" + from quantem.diffraction.rotations import quat_from_zone_axis + + d = torch.as_tensor(zone_axis, dtype=torch.float64) @ self._crystal.lat_real + q = quat_from_zone_axis(d[None], in_plane_deg)[0] + self.orientation = [float(v) for v in q] + return self + + def compute_kossel_reference( + self, thicknesses_A=(300.0, 600.0, 1000.0), angle_step_mrad=3.0, k_max=1.0 + ): + """Precompute the Kossel reference pattern for the pixel rendering of + the Kossel / LACBED mode (about a minute for silicon at 3 mrad).""" + key = ( + round(self.energy_ev), + tuple(float(t) for t in thicknesses_A), + angle_step_mrad, + k_max, + ) + if key not in self._kossel_cache: + self.status = "computing Kossel reference pattern..." + self._kossel_cache[key] = prepare_kossel_reference( + self._crystal, self.energy_ev, thicknesses_A, angle_step_mrad, k_max + ) + self.status = "" + self.kossel_json = json.dumps(self._kossel_cache[key]) + return self + + # ------------------------------------------------------------------ + def _on_preset(self, change): + name = change["new"] + if ( + name in PRESETS + and self._crystal is not None + and self._crystal.name != name.split(" (")[0] + ): + self.set_crystal(name) + + def _on_physics(self, change): + if change["name"] == "k_max" and self.pattern_range > self.k_max: + self.pattern_range = self.k_max + if self._crystal is not None: + self.crystal_json = json.dumps( + prepare_crystal(self._crystal, self.energy_ev, self.k_max) + ) + self.kossel_json = "{}" + + def _on_message(self, widget, content, buffers): + if isinstance(content, dict) and content.get("type") == "kossel_reference": + self.compute_kossel_reference() + + # ------------------------------------------------------------------ + def state_dict(self) -> dict: + keys = [ + "crystal_json", + "presets", + "preset", + "energy_ev", + "k_max", + "orientation", + "mode", + "render", + "dynamical", + "thickness_A", + "semiconv_mrad", + "sigma_excitation", + "rotation_step_deg", + "pattern_range", + "field_mrad", + "sg_max", + "quality", + "show_kikuchi", + "view_from", + "scaling", + "power", + "cmap", + "vmin_pct", + "vmax_pct", + "show_labels", + "show_cell_axes", + "n_cells", + "polyhedra", + "size", + "kossel_json", + "status", + "widget_version", + ] + return {k: getattr(self, k) for k in keys} + + def export_html( + self, path, title: str | None = None, presets: list[str] | None = None + ) -> pathlib.Path: + """Write a standalone HTML page of the widget with its current state. + + The page carries the compiled widget, the crystal data and (when + computed) the Kossel reference, and runs entirely in the browser: + rotating the cell, changing thickness or mode needs no Python. + `presets` lists additional crystals to embed so the crystal menu + works offline (each adds its reflection list to the file). + """ + state = self.state_dict() + embedded = {} + for name in presets or []: + from quantem.diffraction.crystal import Crystal + + xtl = Crystal.from_ase(_preset_atoms(name), name=name.split(" (")[0], verbose=False) + embedded[name] = prepare_crystal(xtl, self.energy_ev, self.k_max) + if self.preset and self.preset not in embedded: + embedded[self.preset] = json.loads(self.crystal_json) + state["embedded_presets"] = embedded + bundle = _STATIC.read_text() + title = title or f"quantEM diffraction simulator: {self._crystal.name}" + html = _standalone_html(bundle, state, title) + out = pathlib.Path(path) + out.write_text(html) + return out + + +def _standalone_html(bundle_js: str, state: dict, title: str) -> str: + state_json = json.dumps(state).replace("&]", "", title) + return f""" + + + + +{safe_title} + + + +
+ + + +""" From d50fec5f4bb8b2bcaafcfeb572adb8021fbdd377 Mon Sep 17 00:00:00 2001 From: cophus Date: Wed, 9 Sep 2026 13:32:56 +0200 Subject: [PATCH 07/36] Widget fixes --- src/quantem/diffraction/bloch.py | 69 +- src/quantem/diffraction/orientation.py | 118 ++- .../diffraction/orientation_visualization.py | 13 +- widget/js/colormaps.ts | 1 + widget/js/diffsim-web/index.ts | 678 ++++++++++++++++++ widget/js/diffsim-web/presets.ts | 14 + widget/js/diffsim/index.tsx | 401 +++++++---- widget/js/diffsim/math.ts | 40 ++ widget/js/diffsim/pattern.ts | 158 +++- widget/js/diffsim/physics.ts | 116 ++- widget/src/quantem/widget/diffsim.py | 38 +- 11 files changed, 1460 insertions(+), 186 deletions(-) create mode 100644 widget/js/diffsim-web/index.ts create mode 100644 widget/js/diffsim-web/presets.ts diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py index 142d73db0..e2c0ecb87 100644 --- a/src/quantem/diffraction/bloch.py +++ b/src/quantem/diffraction/bloch.py @@ -2154,6 +2154,7 @@ def refine_dynamical( neighbor_rescue: bool = True, rescue_thickness_A: float = 100.0, rescue_tilt_deg: float = 0.05, + rescue_max_starts: int = 2, progress_bar: bool = True, ) -> dict: """Dynamical refinement on the Bragg vectors: orientation, thickness, @@ -2250,6 +2251,9 @@ def refine_dynamical( half of the eigensolves; the in-plane deformation and rotation are still fit from the position's own peaks, and the final evaluation is unchanged. + rescue_max_starts : int, default=2 + Neighbor solutions tried per rescued position, lowest cost first, + skipping neighbors whose solution repeats one already tried. neighbor_rescue : bool, default=True Second pass: positions whose winning solution differs from a 4-neighbor of the same crystal by more than rescue_thickness_A or @@ -2317,6 +2321,7 @@ def refine_dynamical( neighbor_rescue=bool(neighbor_rescue), rescue_thickness_A=float(rescue_thickness_A), rescue_tilt_deg=float(rescue_tilt_deg), + rescue_max_starts=int(rescue_max_starts), ) if hasattr(phase_map, "metadata"): phase_map.metadata["dynamical"] = used @@ -2363,6 +2368,7 @@ def stage_grid(center, half, step): cost_out = torch.full((R, C, F), torch.nan, dtype=torch.float64) cost0_out = torch.full((R, C, F), torch.nan, dtype=torch.float64) thick_out = torch.full((R, C, F), torch.nan, dtype=torch.float64) + tcontrast_out = torch.full((R, C, F), torch.nan, dtype=torch.float64) tilt_out = torch.zeros((R, C, F, 2), dtype=torch.float64) quat_out = torch.zeros((R, C, F, 4), dtype=torch.float64) quat_out[..., 0] = 1.0 @@ -2509,14 +2515,22 @@ def refine_from(crystal, q_start, qxy, im, w_exp, stages): cost, _, _, _ = _dynamical_cost( inten[:, :, 1:], g_xy[1:], qxy, im, delta, power_intensity, min_sim_intensity_rel ) + t_contrast = float("nan") if cost is not None: t_best = int(cost[0].argmin()) c_best, t_fit = float(cost[0, t_best]), float(t_grid[t_best]) - return dict(cost=c_best, t=t_fit, wx=wx, wy=wy, q=q, q0=q0, S=S, cost0=cost0) + # how much the cost varies over the thickness grid at this + # orientation: a flat curve means the thickness is not + # determined by these intensities (precession, few beams) + t_contrast = float(cost[0].max() - cost[0].min()) + return dict( + cost=c_best, t=t_fit, wx=wx, wy=wy, q=q, q0=q0, S=S, cost0=cost0, t_contrast=t_contrast + ) def store(rx, ry, f, sol): cost_out[rx, ry, f] = sol["cost"] thick_out[rx, ry, f] = sol["t"] + tcontrast_out[rx, ry, f] = sol.get("t_contrast", float("nan")) tilt_out[rx, ry, f, 0] = sol["wx"] tilt_out[rx, ry, f, 1] = sol["wy"] quat_base[rx, ry, f] = sol["q0"] @@ -2605,9 +2619,33 @@ def peaks_at(rx, ry): torch.rad2deg(torch.linalg.norm(tilt_out[nr, nc, fn] - tilt_out[rx, ry, f])) ) if dt > rescue_thickness_A or dtilt > rescue_tilt_deg: - starts.append((nr, nc, fn)) + starts.append((float(cost_out[nr, nc, fn]), nr, nc, fn)) if starts: - rescue_list.append((rx, ry, f, starts)) + # lowest-cost neighbors first, one start per distinct + # solution, at most rescue_max_starts (each start is a full + # fine-stage search) + starts.sort(key=lambda x: x[0]) + kept: list = [] + for c_n, nr, nc, fn in starts: + dup = False + for _, kr, kc, kf in kept: + if ( + abs(float(thick_out[nr, nc, fn]) - float(thick_out[kr, kc, kf])) + <= rescue_thickness_A + and float( + torch.rad2deg( + torch.linalg.norm(tilt_out[nr, nc, fn] - tilt_out[kr, kc, kf]) + ) + ) + <= rescue_tilt_deg + ): + dup = True + break + if not dup: + kept.append((c_n, nr, nc, fn)) + if len(kept) >= max(1, rescue_max_starts): + break + rescue_list.append((rx, ry, f, [(nr, nc, fn) for _, nr, nc, fn in kept])) it = tqdm(rescue_list, desc="neighbor rescue") if progress_bar else rescue_list for rx, ry, f, starts in it: pk = peaks_at(rx, ry) @@ -2632,6 +2670,7 @@ def peaks_at(rx, ry): phase_index = cost_phase.argmin(dim=-1) f_best = cost_f.argmin(dim=-1) thickness = torch.gather(thick_out, 2, f_best[..., None]).squeeze(-1) + thickness_contrast = torch.gather(tcontrast_out, 2, f_best[..., None]).squeeze(-1) tilt_deg = torch.rad2deg( torch.gather(tilt_out, 2, f_best[..., None, None].expand(R, C, 1, 2)).squeeze(2) ) @@ -2643,6 +2682,7 @@ def peaks_at(rx, ry): return { "thickness": thickness, + "thickness_contrast": thickness_contrast, "tilt_deg": tilt_deg, "quats": quat_out, "deformation": deform_out, @@ -2658,15 +2698,24 @@ def peaks_at(rx, ry): } -def dynamical_maps(result: dict, phase_map, crystal_index: int | None = None) -> dict: +def dynamical_maps( + result: dict, + phase_map, + crystal_index: int | None = None, + min_thickness_contrast: float = 0.02, +) -> dict: """Maps of the winning candidate of a refine_dynamical() result. Returns 'thickness' (A), 'tilt_deg' (magnitude of the tilt correction), 'gain' (cost at the start orientation minus the final cost), 'cost', 'phase_index', 'mask' (positions refined, and of the given crystal when crystal_index is set), 'quats' (R, C, 4), 'deformation' (R, C, 2, 2) - and 'strain', the crystal-frame strain components of - strain_crystal_frame(); unrefined or masked positions are NaN. + 'thickness_contrast' (range of the cost over the thickness grid at the + refined orientation) and 'strain', the crystal-frame strain components + of strain_crystal_frame(); unrefined or masked positions are NaN. The + thickness is NaN where the contrast is below min_thickness_contrast: + a flat cost curve, typical of precessed data with few beams, does not + determine the thickness and the grid minimum there is not a measurement. """ cand = result["candidate"] R, C = cand.shape @@ -2677,6 +2726,9 @@ def dynamical_maps(result: dict, phase_map, crystal_index: int | None = None) -> ).squeeze(2) cost = torch.gather(result["cost"], 2, cand[..., None]).squeeze(-1) cost0 = torch.gather(result["cost_zero_tilt"], 2, cand[..., None]).squeeze(-1) + tcon = result.get("thickness_contrast") + if tcon is None: + tcon = torch.full_like(cost, torch.nan) mask = torch.isfinite(cost) if crystal_index is not None: i_om = torch.tensor([c[0] for c in phase_map.candidates]) @@ -2684,9 +2736,12 @@ def dynamical_maps(result: dict, phase_map, crystal_index: int | None = None) -> nan = torch.full((R, C), torch.nan, dtype=torch.float64) strain = strain_crystal_frame(deform, quats) out = { - "thickness": torch.where(mask, result["thickness"], nan), + "thickness": torch.where( + mask & ~(tcon < min_thickness_contrast), result["thickness"], nan + ), "tilt_deg": torch.where(mask, torch.linalg.norm(result["tilt_deg"], dim=-1), nan), "gain": torch.where(mask, cost0 - cost, nan), + "thickness_contrast": torch.where(mask, tcon, nan), "cost": torch.where(mask, cost, nan), "phase_index": result["phase_index"], "mask": mask, diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index b27404030..f0738208a 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -62,6 +62,75 @@ def fibonacci_hemisphere(n_points: int, dtype=torch.float64) -> torch.Tensor: return torch.stack((r * torch.cos(phi), r * torch.sin(phi), z), dim=-1) +def _zone_peak_parabolic( + za: torch.Tensor, + n_pos: torch.Tensor, + c_n: torch.Tensor, + n_ok: torch.Tensor, + step_rad: float, +) -> torch.Tensor: + """Sub-grid zone axis from the correlations of a zone and its neighbors. + + A quadratic surface c(x, y) is fit by least squares to the correlation + over the neighborhood in the tangent plane of the best zone (x, y in + radians); its vertex is the refined zone axis when it lies within one + grid step of the node and the surface is concave. Otherwise the + correlation-weighted centroid of the neighbors above 70 % of the best + value is used, and the node itself when neither applies. + + Parameters + ---------- + za : (B, 3) best zone axes; n_pos : (B, K, 3) neighbor directions + (the best zone included); c_n : (B, K) their correlations; n_ok : (B, K) + validity; step_rad : zone grid step. + """ + B, K = c_n.shape + # tangent frame at the node + ref = torch.where( + za[:, 2:3].abs() < 0.9, + torch.tensor([0.0, 0.0, 1.0], dtype=za.dtype).expand(B, 3), + torch.tensor([1.0, 0.0, 0.0], dtype=za.dtype).expand(B, 3), + ) + e1 = torch.cross(za, ref, dim=-1) + e1 = e1 / torch.linalg.norm(e1, dim=-1, keepdim=True).clamp_min(1e-12) + e2 = torch.cross(za, e1, dim=-1) + d = n_pos - za[:, None, :] + x = (d * e1[:, None, :]).sum(-1) + y = (d * e2[:, None, :]).sum(-1) + c_best = c_n.amax(dim=1, keepdim=True) + w = n_ok.to(za.dtype) + out = za.clone() + # centroid fallback (the previous estimator) + wgt = (c_n - 0.7 * c_best).clamp_min(0) * w + cen = (wgt[:, :, None] * n_pos).sum(1) + cen_ok = torch.linalg.norm(cen, dim=-1) > 1e-12 + cen = cen / torch.linalg.norm(cen, dim=-1, keepdim=True).clamp_min(1e-12) + out[cen_ok] = cen[cen_ok] + # quadratic fit where at least 6 valid neighbors exist + A = torch.stack([torch.ones_like(x), x, y, x * x, x * y, y * y], dim=-1) * w[:, :, None] + b = (c_n - c_best) * w + enough = w.sum(1) >= 6 + if bool(enough.any()): + At = A.transpose(1, 2) + AtA = At @ A + 1e-12 * torch.eye(6, dtype=za.dtype) + coef = torch.linalg.solve(AtA, (At @ b[:, :, None]))[..., 0] # (B, 6) + cb, cc, cd, ce, cf = coef[:, 1], coef[:, 2], coef[:, 3], coef[:, 4], coef[:, 5] + H = torch.stack([torch.stack([2 * cd, ce], -1), torch.stack([ce, 2 * cf], -1)], -2) + det = 4 * cd * cf - ce * ce + concave = (cd < 0) & (cf < 0) & (det > 0) + grad = torch.stack([cb, cc], -1) + vert = torch.zeros_like(grad) + ok = enough & concave + if bool(ok.any()): + vert[ok] = -torch.linalg.solve(H[ok], grad[ok][..., None])[..., 0] + inside = ok & (torch.linalg.norm(vert, dim=-1) <= step_rad) + if bool(inside.any()): + v = za + vert[:, 0:1] * e1 + vert[:, 1:2] * e2 + v = v / torch.linalg.norm(v, dim=-1, keepdim=True).clamp_min(1e-12) + out[inside] = v[inside] + return out + + class OrientationMap(AutoSerialize): """Match crystal orientations to Bragg peaks at every probe position. @@ -555,10 +624,8 @@ def match_orientations( subpixel_gamma : bool, default=True Parabolic sub-bin refinement of the in-plane angle. subpixel_zone : bool, default=True - Sub-grid refinement of the zone axis: the correlation-weighted - centroid of the best zone and its grid neighbors. Removes the - zone-axis quantization of the plan (the in-plane angle is - already continuous through subpixel_gamma). + Sub-grid zone axis from a quadratic fit of the correlation over + the best zone and its grid neighbors (centroid fallback). batch_size : int, default=128 Number of patterns correlated at once. """ @@ -694,13 +761,10 @@ def match_orientations( n_pos = self.zone_nbr_pos[zi_cpu] # (B, K, 3) n_ok = self.zone_nbr_valid[zi_cpu] # (B, K) c_n = corr_z.gather(1, n_idx) # (B, K) - c_floor = corr_z.gather(1, zi_cpu[:, None]) * 0.7 - wgt = (c_n - c_floor).clamp_min(0) * n_ok - za_ref = (wgt[:, :, None] * n_pos).sum(dim=1) - za_ref = za_ref / torch.linalg.norm(za_ref, dim=-1, keepdim=True).clamp_min( - 1e-12 - ) za_old = self.zone_axes[z_i.cpu()] + za_ref = _zone_peak_parabolic( + za_old, n_pos, c_n, n_ok, np.deg2rad(self.zone_step_deg) + ) axis = torch.cross(za_ref, za_old, dim=-1) sin_t = torch.linalg.norm(axis, dim=-1) ang_t = torch.atan2(sin_t, (za_ref * za_old).sum(-1)) @@ -769,6 +833,7 @@ def refine_orientations( zone_search_deg: float = 1.5, sigma_envelope: float | None = None, zone_max_total_deg: float | None = None, + power_intensity: float | None = None, batched: bool = True, neighbor_rescue: bool = True, rescue_threshold_deg: float = 2.0, @@ -817,6 +882,11 @@ def refine_orientations( Half-range of the envelope tilt search, in degrees. zone_max_total_deg : float | None Trust region: cap on the cumulative envelope tilt applied to + power_intensity : float | None + Power applied to the measured and predicted intensities in the + tilt envelope fit, inherited from the plan (0.25 by default). + Linear intensities let the strongest reflections dominate and, + on dynamical data, drive the fit to the edge of the search range. each orientation, relative to its matched start. The coarse match is grid-accurate to about half the zone-axis step, so tilt corrections beyond that scale are noise walking the orientation @@ -850,6 +920,7 @@ def refine_orientations( refine_zone=bool(refine_zone), zone_search_deg=float(zone_search_deg), zone_max_total_deg=zone_max_total_deg, + power_intensity=power_intensity, sigma_envelope=sigma_envelope, neighbor_rescue=bool(neighbor_rescue), rescue_threshold_deg=float(rescue_threshold_deg), @@ -861,6 +932,16 @@ def refine_orientations( g_all = self.crystal.g_vec lam = self.wavelength sigma_env = sigma_envelope if sigma_envelope is not None else sigma / 2 + power_env = resolve( + power_intensity, + "power_intensity", + self.metadata.get("plan", {}), + default=POWER_INTENSITY, + ) + if power_env <= 0: + # a positions-only plan (power 0) carries no intensity weighting; + # the envelope fit still needs one + power_env = POWER_INTENSITY prec_ill = float(self.metadata.get("precession_deg", 0.0) or 0.0) conv_ill = float(self.metadata.get("semiconv_mrad", 0.0) or 0.0) @@ -892,7 +973,7 @@ def envelope(S, g_rows): ) eye3 = torch.eye(3, dtype=torch.float64) tilt_cap = np.deg2rad( - zone_max_total_deg if zone_max_total_deg is not None else 0.375 * self.zone_step_deg + zone_max_total_deg if zone_max_total_deg is not None else 0.75 * self.zone_step_deg ) def refine_single(q, q_exp, w_exp): @@ -957,8 +1038,11 @@ def refine_single(q, q_exp, w_exp): + tg[None, :, None] * a1[:, None, None] + tg[None, None, :] * a2[:, None, None] ) - pred = f_p[:, None, None] * envelope(S, g_sel[pair]) - E = (w[:, None, None] * pred).sum(dim=0) / ( + pred = (f_p[:, None, None] * envelope(S, g_sel[pair])).clamp_min( + 0 + ) ** power_env + w_env = w**power_env + E = (w_env[:, None, None] * pred).sum(dim=0) / ( (pred**2).sum(dim=0).sqrt().clamp_min(1e-12) ) ij = int(E.argmax()) @@ -1021,6 +1105,7 @@ def get_exp(rx, ry): num_iterations=num_iterations, min_pairs=min_pairs, refine_zone=refine_zone, + power_env=power_env, progress_bar=progress_bar, ) else: @@ -1103,6 +1188,7 @@ def _refine_batched( refine_zone: bool, progress_bar: bool, chunk: int = 64, + power_env: float = POWER_INTENSITY, ) -> None: """Chunk-vectorized in-plane + envelope refinement (all positions).""" from quantem.diffraction.rotations import quat_to_matrix @@ -1222,9 +1308,11 @@ def envelope(S, g_rows): + tg[None, :, None] * gyf[:, None, None] - tg[None, None, :] * gxf[:, None, None] ) # (Np, T, T) - pred = ff[:, None, None] * envelope(S, g[idx_b, idx_g]) + pred = (ff[:, None, None] * envelope(S, g[idx_b, idx_g])).clamp_min( + 0 + ) ** power_env E_num = torch.zeros((B, n_tg, n_tg), dtype=torch.float64).index_add_( - 0, idx_b, wf[:, None, None] * pred + 0, idx_b, (wf**power_env)[:, None, None] * pred ) E_den = torch.zeros((B, n_tg, n_tg), dtype=torch.float64).index_add_( 0, idx_b, pred**2 diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py index 29d3e4763..6edf3bdeb 100644 --- a/src/quantem/diffraction/orientation_visualization.py +++ b/src/quantem/diffraction/orientation_visualization.py @@ -229,6 +229,8 @@ def plot_orientation_map( figax=None, legend: bool = True, axsize: tuple[float, float] = (9.0, 4.5), + crop: tuple[int, int, int, int] | None = None, + title: str | None = None, ): """IPF-colored orientation map with the wedge legend in an adjacent panel. @@ -246,6 +248,10 @@ def plot_orientation_map( Real-space scale bar, e.g. {"sampling": 30, "units": "A"}. figax : (fig, (ax_map, ax_legend)) | (fig, ax_map) | None Existing axes; with a single axis the legend is skipped. + crop : (r0, r1, c0, c1) | None + Show only this window of the map (rows r0:r1, columns c0:c1). + title : str | None + Replaces the default title (crystal name and colored direction). """ import matplotlib.pyplot as plt @@ -253,6 +259,9 @@ def plot_orientation_map( rgb = ipf_color(om.quats[..., match, :], om.crystal, direction) if mask is not None: rgb = rgb * np.asarray(mask, dtype=float)[..., None] + if crop is not None: + r0, r1, c0, c1 = crop + rgb = rgb[r0:r1, c0:c1] ax_leg = None if figax is None: @@ -274,7 +283,9 @@ def plot_orientation_map( ax.imshow(rgb, interpolation="nearest") ax.set_xticks([]) ax.set_yticks([]) - if isinstance(direction, str) and direction == "z": + if title is not None: + ax.set_title(title) + elif isinstance(direction, str) and direction == "z": ax.set_title(f"{om.crystal.name} out-of-plane orientation") else: # arrow for the colored in-plane direction lives in the title, diff --git a/widget/js/colormaps.ts b/widget/js/colormaps.ts index a7df441ce..18a796a9f 100644 --- a/widget/js/colormaps.ts +++ b/widget/js/colormaps.ts @@ -25,6 +25,7 @@ const COLORMAP_POINTS: Record = { [255, 87, 0], [255, 173, 0], [255, 255, 0], [255, 255, 128], [255, 255, 255], ], gray: [[0, 0, 0], [255, 255, 255]], + gray_r: [[255, 255, 255], [0, 0, 0]], hsv: [ [255, 0, 0], [255, 255, 0], [0, 255, 0], [0, 255, 255], [0, 0, 255], [255, 0, 255], [255, 0, 0], diff --git a/widget/js/diffsim-web/index.ts b/widget/js/diffsim-web/index.ts new file mode 100644 index 000000000..be4883eb3 --- /dev/null +++ b/widget/js/diffsim-web/index.ts @@ -0,0 +1,678 @@ +// diffraction-sim.js — interactive electron diffraction simulator for the +// website (MyST anywidget directive, no framework). Built from the quantEM +// widget sources: widget/js/diffsim-web/index.ts in the quantem repository +// (`npm run build` writes dist/diffraction-sim.js). +// +// Left panel: the unit cell, drawn as seen from the detector side (the beam +// comes toward you). Drag to tilt, two fingers to twist, buttons for 15 deg +// steps. Right panel: the diffraction pattern of the same orientation, +// computed live in the browser: nanobeam disks (Bloch wave intensities for +// the beams near the Ewald sphere, thin-slab intensities for the rest), +// CBED disks, or the Kossel / Kikuchi line pattern. Drag the pattern to move +// the tilt map with the pointer; double-click a point to tilt the crystal by +// that angle (the clicked direction moves onto the optic axis). +// +// Directive options (all optional): +// preset (structure name), zone_axis [u,v,w] or [u,v,t,w], thickness_A, semiconv_mrad, precession_deg, +// pattern_range (1/A), mode ("nanobeam" | "cbed" | "kossel"), dynamical, +// size (px per panel), show_labels (cell axes), show_hkl, show_kikuchi, polyhedra, n_cells, power +// (brightness exponent, default 0.5 = square root of the intensity), vmax +// (upper end of the contrast window, default 0.5 of the strongest diffracted beam). + +import { Quat, Vec3, directionIndices, fourToThree, matTVec, parseDirection, qmult, qnormalize, quatFromAxisAngle, quatFromZoneAxis, quatToMatrix, threeToFour } from "../diffsim/math"; +import { + CrystalData, NanobeamSolution, Reflection, blochIntensities, blochSolve, hybridBeams, kinematicalTilted, kosselLines, + labReflections, nanobeamIntensities, nanobeamSolve, parseCrystal, precessionTilts, slabIntensities, +} from "../diffsim/physics"; +import { cellGeometry, drawCell } from "../diffsim/crystal3d"; +import { + Frame, cbedImage, drawDisks, drawEwaldPanel, drawImage, drawKikuchiOverlay, drawKosselLines, setupCanvas, tiltGrid, toPx, +} from "../diffsim/pattern"; +import { ENERGY_EV, PRESETS } from "./presets"; + +const DIRECT: Reflection = { index: -1, hkl: [0, 0, 0], g: [0, 0, 0], gLen: 0, s: 0 }; +const SG_MAX = 0.05; +const VIEW_X = -1; // detector-side view: cell and pattern move together +const MAX_BEAMS = 48; +const MAX_BEAMS_DRAG = 28; +const CBED_GRID = 7; +const CBED_GRID_DRAG = 5; + +interface Model { get(key: string): unknown } + +function fmtIndices(v: [number, number, number] | null, hexagonal = false): string { + if (!v) return "—"; + const idx: number[] = hexagonal ? threeToFour(v) : v; + return "[" + idx.map((h) => (h < 0 ? `${-h}̅` : `${h}`)).join("") + "]"; +} + +function detectDark(): boolean { + const de = document.documentElement; + if (de.classList.contains("dark")) return true; + if (de.classList.contains("light")) return false; + try { + const m = getComputedStyle(document.body).backgroundColor.match(/\d+/g); + if (m && m.length >= 3) return (0.299 * +m[0] + 0.587 * +m[1] + 0.114 * +m[2]) / 255 < 0.5; + } catch (e) { /* ignore */ } + return !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches); +} + +export function render({ model, el }: { model: Model; el: HTMLElement }) { + const opt = (key: string, fallback: T): T => { + const v = model && typeof model.get === "function" ? (model.get(key) as T | undefined) : undefined; + return v === undefined || v === null ? fallback : v; + }; + const id = "dsim-" + Math.random().toString(36).slice(2, 8); + const names = Object.keys(PRESETS); + + // ---------------------------------------------------------------- state + const state = { + preset: names.includes(opt("preset", "")) ? opt("preset", "") : names[0], + mode: opt("mode", "nanobeam") as string, + dynamical: opt("dynamical", true) as boolean, + thickness: opt("thickness_A", 400) as number, + semiconv: opt("semiconv_mrad", 3) as number, + precession: opt("precession_deg", 0) as number, + fieldMrad: opt("field_mrad", 50) as number, + patternRange: opt("pattern_range", 3.0) as number, + stepDeg: 15, + showLabels: opt("show_labels", true) as boolean, + showHkl: opt("show_hkl", true) as boolean, + showAppearance: false, + kikuchi: opt("show_kikuchi", false) as boolean, + polyhedra: opt("polyhedra", false) as boolean, + nCells: (opt("n_cells", [1, 1, 1]) as number[]).slice(0, 3) as [number, number, number], + sizePref: opt("size", 400) as number, + power: opt("power", 0.5) as number, // brightness ~ intensity^power + vmin: 0, // contrast window on the scaled intensities (1 = strongest diffracted beam) + vmax: opt("vmax", 0.5) as number, + energy: opt("energy_ev", ENERGY_EV) as number, + showEwald: opt("show_ewald", true) as boolean, + quat: [1, 0, 0, 0] as Quat, + dragging: false, + }; + const ENERGIES = [60e3, 80e3, 100e3, 120e3, 200e3, 300e3]; + if (!ENERGIES.includes(state.energy)) ENERGIES.push(state.energy); + ENERGIES.sort((a, b) => a - b); + let crystal: CrystalData = parseCrystal(PRESETS[state.preset], state.energy)!; + let geom = cellGeometry(crystal, state.nCells, state.polyhedra); + const k0 = () => 1 / crystal.wavelength; + + const setZoneAxis = (uvw: Vec3) => { + const c = crystal.cell; + const d: Vec3 = [ + uvw[0] * c[0][0] + uvw[1] * c[1][0] + uvw[2] * c[2][0], + uvw[0] * c[0][1] + uvw[1] * c[1][1] + uvw[2] * c[2][1], + uvw[0] * c[0][2] + uvw[1] * c[1][2] + uvw[2] * c[2][2], + ]; + state.quat = quatFromZoneAxis(d); + }; + const za = opt("zone_axis", null) as number[] | null; + if (za && za.length === 4) setZoneAxis(fourToThree(za)); // Miller-Bravais [u v t w] + else if (za && za.length === 3) setZoneAxis(za as Vec3); + else setZoneAxis(crystal.hexagonal ? [0, 0, 1] : [1, 1, 0]); + + // ---------------------------------------------------------------- DOM + const style = document.createElement("style"); + style.textContent = ` + .${id}-wrap { background: var(--${id}-bg, #111); color: var(--${id}-fg, #ccc); border: 1px solid var(--${id}-border, #444); + border-radius: 8px; padding: 10px 14px 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 13px; max-width: 100%; box-sizing: border-box; } + .${id}-title { font-size: 16px; font-weight: 600; color: var(--${id}-title, #ddd); margin-bottom: 6px; text-align: center; } + .${id}-top { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; justify-content: center; margin-bottom: 8px; } + .${id}-panels { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; } + .${id}-panel { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 0 0 auto; } + .${id}-canvas { display: block; border-radius: 4px; touch-action: none; cursor: grab; background: #000; } + .${id}-canvas.cell { background: var(--${id}-cellbg, #161616); border: 1px solid var(--${id}-border, #444); } + .${id}-row { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; } + .${id}-hint { font-size: 11px; color: var(--${id}-dim, #777); line-height: 1.35; } + .${id}-mono { font-family: ui-monospace, Menlo, monospace; font-size: 12px; } + .${id}-btn { padding: 4px 9px; border: 1px solid var(--${id}-border, #444); background: var(--${id}-btnbg, #222); + color: var(--${id}-fg, #ccc); border-radius: 4px; cursor: pointer; font-size: 12px; line-height: 1.2; } + .${id}-btn:hover { background: var(--${id}-btnhover, #333); } + .${id}-btn.active { background: #1a4d2e; border-color: #00cc66; color: #00ff88; } + .${id}-group { display: inline-flex; } + .${id}-group .${id}-btn { border-radius: 0; margin-left: -1px; } + .${id}-group .${id}-btn:first-child { border-radius: 4px 0 0 4px; margin-left: 0; } + .${id}-group .${id}-btn:last-child { border-radius: 0 4px 4px 0; } + .${id}-wrap select, .${id}-wrap input[type=text], .${id}-wrap input[type=number] { font-size: 12px; padding: 3px 6px; border-radius: 4px; + border: 1px solid var(--${id}-border, #444); background: var(--${id}-btnbg, #222); color: var(--${id}-fg, #ccc); } + .${id}-slider { flex: 1; min-width: 130px; max-width: 200px; } + .${id}-slider label { display: flex; justify-content: space-between; font-size: 11px; color: var(--${id}-label, #aaa); } + .${id}-slider input[type=range] { width: 100%; accent-color: #00cc66; margin: 2px 0 0; } + .${id}-check { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; } + .${id}-check input { accent-color: #00cc66; } + .${id}-appearance { padding: 6px 8px; border: 1px solid var(--${id}-border, #444); border-radius: 6px; } + .${id}-hist { display: block; border: 1px solid var(--${id}-border, #444); border-radius: 3px; cursor: ew-resize; touch-action: none; } + .${id}-histwrap { display: flex; flex-direction: column; gap: 2px; } + .${id}-histwrap .${id}-hint { display: flex; justify-content: space-between; font-family: ui-monospace, Menlo, monospace; } + `; + el.appendChild(style); + + const wrap = document.createElement("div"); + wrap.className = `${id}-wrap`; + const presetOptions = names.map((n) => ``).join(""); + wrap.innerHTML = ` +
Electron diffraction simulator
+
+ + +
go
+
reset
+ +
+
+
+ + +
+
x −
x +
+
y −
y +
+
z −
z +
+ ° +
+
+ + +
+
+ + + + +
+
Drag to tilt the crystal (the near face follows the pointer). Shift-drag, or two fingers, twist about the beam. The beam comes toward you.
+
+
+ +
+
+
nanobeam
+
CBED
+
Kossel lines
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
appearance ▾
+
+ +
+
Drag the pattern to move the tilt map. Double-click a disk to tilt to its two-beam condition, or empty space to put the Laue circle centre there.
+
+
+ `; + el.appendChild(wrap); + const $ = (sel: string) => wrap.querySelector(sel) as T; + const cellCanvas = $(`#${id}-cell`); + const ewaldCanvas = $(`#${id}-ewaldc`); + const patCanvas = $(`#${id}-pat`); + + // ---------------------------------------------------------------- theme + const palettes = { + dark: { bg: "#111", fg: "#ccc", title: "#ddd", label: "#aaa", dim: "#777", btnbg: "#222", btnhover: "#333", border: "#444", cellbg: "#161616" }, + light: { bg: "#f4f6f3", fg: "#333", title: "#1f1f1f", label: "#556", dim: "#777", btnbg: "#ffffff", btnhover: "#e9ebe6", border: "#d3d6d0", cellbg: "#fbfbfa" }, + }; + let dark = detectDark(); + const applyTheme = () => { + dark = detectDark(); + const p = palettes[dark ? "dark" : "light"]; + for (const k in p) wrap.style.setProperty(`--${id}-${k}`, (p as Record)[k]); + drawAll(); + }; + const mo = new MutationObserver(() => applyTheme()); + mo.observe(document.documentElement, { attributes: true }); + if (window.matchMedia) window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyTheme); + + // ---------------------------------------------------------------- size + // Layout: left column = cell (Sc square) above the Ewald panel (Sc x Se); + // right column = pattern square whose side S matches the left column's + // height, so S = Sc + gap + Se with Se = Sc / 2. Stacked on narrow screens. + let S = 400, Sc = 260, Se = 130; + const GAP = 8; + const computeSize = () => { + const w = wrap.clientWidth - 30; + const sideBySide = w >= 2.5 * 180 + 2 * GAP + 12; + if (sideBySide) { + Sc = Math.floor(Math.min((w - 12 - GAP) / 2.5, state.sizePref / 1.5)); + Se = state.showEwald ? Math.round(Sc / 2) : 0; + S = Sc + (Se ? GAP + Se : 0); + } else { + Sc = Math.max(180, Math.min(state.sizePref, w)); + Se = state.showEwald ? Math.round(Sc / 2) : 0; + S = Sc; + } + cellCanvas.style.width = `${Sc}px`; cellCanvas.style.height = `${Sc}px`; + ewaldCanvas.style.display = Se ? "block" : "none"; + ewaldCanvas.style.width = `${Sc}px`; ewaldCanvas.style.height = `${Se}px`; + patCanvas.style.width = `${S}px`; patCanvas.style.height = `${S}px`; + const panels = wrap.querySelectorAll(`.${id}-panel`); + if (panels[0]) panels[0].style.width = `${Sc}px`; + if (panels[1]) panels[1].style.width = `${S}px`; + }; + + // ---------------------------------------------------------------- physics cache + let nb: { beams: Reflection[]; nDyn: number } = { beams: [], nDyn: 0 }; + let nbSolution: NanobeamSolution | null = null; + let nbTilts: [number, number][] = [[0, 0]]; + let cbed: { grid: ReturnType; beams: Reflection[]; nDyn: number; sols: ReturnType[] | null } | null = null; + let lines: ReturnType = []; + let geomKey = ""; + + const frame = (): Frame => ({ size: S, qMax: state.mode === "kossel" ? state.fieldMrad * 1e-3 : state.patternRange, viewX: VIEW_X }); + + // quality while dragging adapts to the device: the beam cap and the + // precession node count shrink when a recompute takes too long + let dragBeams = MAX_BEAMS_DRAG; + let dragNodes = 8; + const solveOrientation = () => { + // everything that depends on the orientation (not on thickness) + const q = state.quat; + const alpha = state.semiconv * 1e-3; + const maxBeams = state.dragging ? dragBeams : MAX_BEAMS; + if (state.mode === "nanobeam") { + nbTilts = precessionTilts(k0(), state.precession, state.dragging ? dragNodes : 16); + if (state.dynamical) { + nbSolution = nanobeamSolve(crystal, q, state.patternRange, SG_MAX, maxBeams, nbTilts); + nb = { beams: nbSolution.beams, nDyn: Math.round(nbSolution.nDynMean) }; + } else { + nb = { beams: [DIRECT, ...labReflections(crystal, q, state.patternRange)], nDyn: 0 }; + nbSolution = null; + } + } else if (state.mode === "cbed") { + const Rk = k0() * Math.sin(alpha); + const grid = tiltGrid(Rk, state.dragging ? CBED_GRID_DRAG : CBED_GRID); + if (state.dynamical) { + const { beams, nDyn } = hybridBeams(crystal, q, state.patternRange, SG_MAX, state.dragging ? 20 : 32, Math.sin(alpha)); + const dyn = beams.slice(0, nDyn); + cbed = { grid, beams, nDyn, sols: grid.tilts.map((t) => blochSolve(crystal, dyn, t)) }; + } else { + cbed = { grid, beams: [DIRECT, ...labReflections(crystal, q, state.patternRange)], nDyn: 0, sols: null }; + } + } + if (state.mode === "kossel" || (state.mode === "nanobeam" && state.kikuchi)) { + const fov = state.mode === "kossel" ? state.fieldMrad * 1e-3 : state.patternRange / k0(); + lines = kosselLines(crystal, q, Math.min(crystal.k_max, 2.5), fov); + } + }; + + const intensitiesNanobeam = (): Float64Array => { + if (state.dynamical && nbSolution) return nanobeamIntensities(crystal, nbSolution, state.thickness); + const out = new Float64Array(nb.beams.length); + for (const t of nbTilts) { + const v = kinematicalTilted(crystal, nb.beams, t, 0.02); + for (let i = 0; i < out.length; i++) out[i] += v[i] / nbTilts.length; + } + return out; + }; + + // ---------------------------------------------------------------- drawing + const drawCellPanel = () => { + const key = `${state.preset}|${state.nCells.join(",")}|${state.polyhedra}`; + if (key !== geomKey) { geom = cellGeometry(crystal, state.nCells, state.polyhedra); geomKey = key; } + drawCell(cellCanvas, geom, state.quat, Sc, { dark, showAxes: true, showLabels: state.showLabels, atomScale: 0.45, viewX: VIEW_X }); + if (state.showEwald && Se > 0) { + const dpr = window.devicePixelRatio || 1; + if (ewaldCanvas.width !== Sc * dpr || ewaldCanvas.height !== Se * dpr) { ewaldCanvas.width = Sc * dpr; ewaldCanvas.height = Se * dpr; } + const ctx = ewaldCanvas.getContext("2d"); + if (ctx) { + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + const refl = state.mode === "nanobeam" && nb.beams.length ? nb.beams : labReflections(crystal, state.quat, state.patternRange); + drawEwaldPanel(ctx, Sc, Se, refl, k0(), state.patternRange, SG_MAX, VIEW_X, dark, state.mode === "nanobeam" ? state.precession : 0); + } + } + const R = quatToMatrix(state.quat); + $(`#${id}-za`).textContent = "zone axis " + fmtIndices(directionIndices(crystal.cell, matTVec(R, [0, 0, 1])), crystal.hexagonal); + $(`#${id}-zone`).placeholder = crystal.hexagonal ? "0 0 0 1" : "1 1 0"; + $(`#${id}-info`).textContent = `${crystal.name} · ${crystal.spacegroup || crystal.pointgroup}`; + }; + + // ---- histogram of the scaled intensities with a draggable contrast window + const histCanvas = $(`#${id}-hist`); + let histBins = new Array(64).fill(0); + const histogramOf = (vals: ArrayLike) => { + const bins = new Array(64).fill(0); + for (let i = 0; i < vals.length; i++) { + const v = vals[i]; + if (!(v >= 0)) continue; + bins[Math.min(63, Math.floor(v * 63.999))]++; + } + histBins = bins; + drawHistogram(); + }; + const drawHistogram = () => { + const dpr = window.devicePixelRatio || 1; + const W = 160, H = 40; + if (histCanvas.width !== W * dpr) { histCanvas.width = W * dpr; histCanvas.height = H * dpr; } + histCanvas.style.width = `${W}px`; histCanvas.style.height = `${H}px`; + const c = histCanvas.getContext("2d"); + if (!c) return; + c.setTransform(dpr, 0, 0, dpr, 0, 0); + c.fillStyle = dark ? "#1a1a1a" : "#f0f0f0"; + c.fillRect(0, 0, W, H); + const mx = Math.max(1e-3, ...histBins.map((v) => Math.log1p(v))); + const bw = W / 64; + for (let i = 0; i < 64; i++) { + const h = (Math.log1p(histBins[i]) / mx) * (H - 2); + const x = (i + 0.5) / 64; + c.fillStyle = x >= state.vmin && x <= state.vmax ? (dark ? "#9a9a9a" : "#666") : (dark ? "#444" : "#c4c4c4"); + c.fillRect(i * bw + 0.5, H - h, Math.max(1, bw - 1), h); + } + c.strokeStyle = "#00cc66"; c.lineWidth = 2; + for (const v of [state.vmin, state.vmax]) { c.beginPath(); c.moveTo(v * W, 0); c.lineTo(v * W, H); c.stroke(); } + $(`#${id}-hist-lo`).textContent = state.vmin.toFixed(2); + $(`#${id}-hist-hi`).textContent = state.vmax.toFixed(2); + }; + let histHandle: "lo" | "hi" | null = null; + histCanvas.addEventListener("pointerdown", (e) => { + const rect = histCanvas.getBoundingClientRect(); + const x = (e.clientX - rect.left) / rect.width; + histHandle = Math.abs(x - state.vmin) <= Math.abs(x - state.vmax) ? "lo" : "hi"; + histCanvas.setPointerCapture?.(e.pointerId); + e.preventDefault(); + }); + histCanvas.addEventListener("pointermove", (e) => { + if (!histHandle) return; + const rect = histCanvas.getBoundingClientRect(); + const x = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width)); + if (histHandle === "lo") state.vmin = Math.min(x, state.vmax - 0.02); + else state.vmax = Math.max(x, state.vmin + 0.02); + drawPattern(); + }); + const histUp = () => { histHandle = null; }; + histCanvas.addEventListener("pointerup", histUp); + histCanvas.addEventListener("pointercancel", histUp); + + const drawPattern = () => { + const ctx = setupCanvas(patCanvas, S); + if (!ctx) return; + const f = frame(); + let status = ""; + if (state.mode === "nanobeam") { + const inten = intensitiesNanobeam(); + drawDisks(ctx, f, nb.beams, inten, dark, state.showHkl, k0() * Math.sin(state.semiconv * 1e-3), state.power, state.vmin, state.vmax); + if (state.kikuchi) drawKikuchiOverlay(ctx, f, lines, k0(), dark); + let iMax = 0; + for (let i = 0; i < inten.length; i++) if (nb.beams[i].index >= 0) iMax = Math.max(iMax, inten[i]); + const scaled = new Float32Array(inten.length); + for (let i = 0; i < inten.length; i++) scaled[i] = inten[i] > 1e-6 * iMax ? Math.pow(inten[i] / (iMax || 1), state.power) : -1; + histogramOf(scaled); + const prec = state.precession > 0 ? `; precession ${state.precession.toFixed(2)}° averaged over ${nbTilts.length} ring nodes` : ""; + status = state.dynamical + ? `${nb.nDyn} Bloch beams (|s| < ${SG_MAX} Å⁻¹, absorptive), thin-slab intensities for the other ${nb.beams.length - nb.nDyn}${prec}` + : `kinematical: |F|² with a Gaussian excitation envelope (σ = 0.02 Å⁻¹)`; + } else if (state.mode === "cbed" && cbed) { + const inten = cbed.sols + ? cbed.sols.map((sol, i) => { + const out = new Float64Array(cbed!.beams.length); + out.set(blochIntensities(sol, state.thickness)); + slabIntensities(crystal, cbed!.beams, cbed!.nDyn, cbed!.grid.tilts[i], state.thickness, out); + return out; + }) + : cbed.grid.tilts.map((t) => kinematicalTilted(crystal, cbed!.beams, t, 0.02)); + const img = cbedImage(f, cbed.beams, cbed.grid, inten); + // normalise to the brightest pixel outside the direct disk + const Rpx = cbed.grid.R * (0.5 * S * 0.92) / f.qMax + 1.5; + let hi = 0; + for (let py = 0; py < S; py++) for (let px = 0; px < S; px++) { + if (Math.hypot(px + 0.5 - S / 2, py + 0.5 - S / 2) <= Rpx) continue; + const v = img[py * S + px]; + if (v > hi) hi = v; + } + if (!(hi > 0)) hi = 1; + // power-law scaling and the contrast window, like the disks + const disp = new Float32Array(img.length); + for (let i = 0; i < img.length; i++) disp[i] = Math.min(1, Math.pow(Math.max(img[i], 0) / hi, state.power)); + histogramOf(disp); + drawImage(ctx, f, disp, dark ? "gray" : "gray_r", state.vmin, state.vmax, dark, "Å⁻¹", 1); + status = `${cbed.nDyn} Bloch beams × ${cbed.grid.tilts.length} incident tilts per disk; disks summed where they overlap`; + } else if (state.mode === "kossel") { + drawKosselLines(ctx, f, lines, dark, state.showHkl, 0.02); + status = `deficient line of every reflection: line width = two-beam rocking width |U_g| / (k₀|g|), darkness ∝ |U_g|`; + } + $(`#${id}-status`).textContent = status; + }; + + const drawAll = () => { drawCellPanel(); drawPattern(); }; + const recompute = () => { + const t0 = performance.now(); + solveOrientation(); + drawAll(); + if (state.dragging) { + // keep dragging responsive: aim for well under 100 ms per recompute + const dt = performance.now() - t0; + if (dt > 90) { dragBeams = Math.max(12, Math.round(dragBeams * 0.7)); dragNodes = Math.max(4, dragNodes - 2); } + else if (dt < 30) { dragBeams = Math.min(MAX_BEAMS_DRAG, dragBeams + 4); dragNodes = Math.min(8, dragNodes + 1); } + } + }; + + // ---------------------------------------------------------------- interaction + const rotateScreen = (axis: Vec3, deg: number) => { + // axis in screen coordinates (x right, y up, z toward the viewer) -> lab + const dq = quatFromAxisAngle([VIEW_X * axis[0], axis[1], VIEW_X * axis[2]], (deg * Math.PI) / 180); + state.quat = qnormalize(qmult(dq, state.quat)); + }; + const shiftPattern = (dqx: number, dqy: number, inverseAngstrom: boolean) => { + const ax = inverseAngstrom ? dqx / k0() : dqx, ay = inverseAngstrom ? dqy / k0() : dqy; + const ang = Math.hypot(ax, ay); + if (ang <= 0) return; + state.quat = qnormalize(qmult(quatFromAxisAngle([ay, -ax, 0], ang), state.quat)); + }; + let raf = 0; + const scheduleRecompute = () => { + if (raf) return; + raf = requestAnimationFrame(() => { raf = 0; recompute(); }); + }; + const endDrag = () => { + if (!state.dragging) return; + state.dragging = false; + cellCanvas.style.cursor = patCanvas.style.cursor = "grab"; + recompute(); // full quality + }; + + const attachDrag = (canvas: HTMLCanvasElement, onMove: (dx: number, dy: number) => void) => { + const pointers = new Map(); + canvas.addEventListener("pointerdown", (e) => { + canvas.setPointerCapture?.(e.pointerId); + pointers.set(e.pointerId, [e.clientX, e.clientY]); + state.dragging = true; + canvas.style.cursor = "grabbing"; + e.preventDefault(); + }); + canvas.addEventListener("pointermove", (e) => { + const prev = pointers.get(e.pointerId); + if (!prev) return; + const cur: [number, number] = [e.clientX, e.clientY]; + if (pointers.size >= 2) { + const other = [...pointers.entries()].find(([pid]) => pid !== e.pointerId); + if (other) { + const [ox, oy] = other[1]; + let da = Math.atan2(cur[1] - oy, cur[0] - ox) - Math.atan2(prev[1] - oy, prev[0] - ox); + if (da > Math.PI) da -= 2 * Math.PI; + if (da < -Math.PI) da += 2 * Math.PI; + rotateScreen([0, 0, 1], (-da * 180) / Math.PI); + } + } else if (e.shiftKey) { + // shift-drag twists about the beam: the desktop version of the two-finger gesture + const rect = canvas.getBoundingClientRect(); + const cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2; + let da = Math.atan2(cur[1] - cy, cur[0] - cx) - Math.atan2(prev[1] - cy, prev[0] - cx); + if (da > Math.PI) da -= 2 * Math.PI; + if (da < -Math.PI) da += 2 * Math.PI; + rotateScreen([0, 0, 1], (-da * 180) / Math.PI); + } else { + onMove(cur[0] - prev[0], cur[1] - prev[1]); + } + pointers.set(e.pointerId, cur); + scheduleRecompute(); + }); + const up = (e: PointerEvent) => { pointers.delete(e.pointerId); if (pointers.size === 0) endDrag(); }; + canvas.addEventListener("pointerup", up); + canvas.addEventListener("pointercancel", up); + canvas.addEventListener("pointerleave", up); + }; + attachDrag(cellCanvas, (dx, dy) => { + const ang = Math.hypot(dx, dy) * (180 / Sc); + if (ang > 0) rotateScreen([dy, dx, 0], ang); // trackball: the near face follows the pointer + }); + attachDrag(patCanvas, (dx, dy) => { + const f = frame(); + const sc = (0.5 * S * 0.92) / f.qMax; + shiftPattern((VIEW_X * dx) / sc, -dy / sc, state.mode !== "kossel"); + }); + // Double-click on a visible disk: tilt the crystal to the exact Bragg + // condition of that reflection (two-beam: Laue circle through 000 and g; + // w = s_g (-g_y, g_x) / |g_xy|^2 zeroes s_g). On empty space the + // Laue-circle centre moves to the clicked point (same sense); in Kossel + // mode the clicked direction of the tilt map moves onto the axis. + patCanvas.addEventListener("dblclick", (e) => { + const rect = patCanvas.getBoundingClientRect(); + const x = e.clientX - rect.left, y = e.clientY - rect.top; + const f = frame(); + const sc = (0.5 * S * 0.92) / f.qMax; + if (state.mode === "nanobeam") { + const inten = intensitiesNanobeam(); + let iMax = 0; + for (let i = 1; i < nb.beams.length; i++) iMax = Math.max(iMax, inten[i]); + const snap = Math.max(8, 1.5 * k0() * Math.sin(state.semiconv * 1e-3) * sc); + // several reflections of different g_z share one spot (in hcp the first + // HOLZ layer is only 0.21 1/A up): take the candidate under the click + // that needs the smallest tilt to reach Bragg, never more than 5 degrees + let best: Reflection | null = null, bestTilt = (5 * Math.PI) / 180; + for (let i = 0; i < nb.beams.length; i++) { + const b = nb.beams[i]; + if (b.index < 0 || !(inten[i] > 1e-4 * iMax)) continue; + const [px, py] = toPx(f, b.g[0], b.g[1]); + if (Math.hypot(px - x, py - y) > snap) continue; + const gxy = Math.hypot(b.g[0], b.g[1]); + if (gxy < 1e-6) continue; + const tilt = Math.abs(b.s) / gxy; + if (tilt < bestTilt) { bestTilt = tilt; best = b; } + } + if (best) { + const gxy2 = best.g[0] ** 2 + best.g[1] ** 2; + const wx = (-best.s * best.g[1]) / gxy2, wy = (best.s * best.g[0]) / gxy2; + state.quat = qnormalize(qmult(quatFromAxisAngle([wx, wy, 0], Math.hypot(wx, wy)), state.quat)); + recompute(); + return; + } + shiftPattern((VIEW_X * (x - rect.width / 2)) / sc, -(y - rect.height / 2) / sc, true); + } else { + shiftPattern(-(VIEW_X * (x - rect.width / 2)) / sc, (y - rect.height / 2) / sc, state.mode !== "kossel"); + } + recompute(); + }); + + // ---------------------------------------------------------------- controls + wrap.querySelectorAll(`[data-rot]`).forEach((b) => { + b.addEventListener("click", () => { + const key = b.dataset.rot!; + const axis: Vec3 = [+(key[0] === "x"), +(key[0] === "y"), +(key[0] === "z")]; + rotateScreen(axis, key[1] === "+" ? state.stepDeg : -state.stepDeg); + recompute(); + }); + }); + $(`#${id}-step`).addEventListener("change", (e) => { state.stepDeg = Math.max(0.1, +(e.target as HTMLInputElement).value || 15); }); + const goZone = () => { + const v = parseDirection($(`#${id}-zone`).value); // 3 or 4 (Miller-Bravais) indices + if (!v) return; + setZoneAxis(v); + recompute(); + }; + $(`#${id}-go`).addEventListener("click", goZone); + $(`#${id}-zone`).addEventListener("keydown", (e) => { if (e.key === "Enter") goZone(); }); + $(`#${id}-reset`).addEventListener("click", () => { setZoneAxis(crystal.hexagonal ? [0, 0, 1] : [1, 1, 0]); recompute(); }); + $(`#${id}-preset`).addEventListener("change", (e) => { + state.preset = (e.target as HTMLSelectElement).value; + crystal = parseCrystal(PRESETS[state.preset], state.energy)!; + const range = $(`#${id}-range`); + range.max = String(crystal.k_max); + if (state.patternRange > crystal.k_max) { state.patternRange = crystal.k_max; range.value = String(crystal.k_max); } + setZoneAxis(crystal.hexagonal ? [0, 0, 1] : [1, 1, 0]); + recompute(); + }); + const modeButtons = wrap.querySelectorAll(`[data-mode]`); + const updateModeUI = () => { + modeButtons.forEach((b) => b.classList.toggle("active", b.dataset.mode === state.mode)); + const show = (sel: string, on: boolean) => { $(sel).style.display = on ? "" : "none"; }; + show(`#${id}-thickwrap`, state.mode !== "kossel" && state.dynamical); + show(`#${id}-convwrap`, state.mode !== "kossel"); + show(`#${id}-rangewrap`, state.mode !== "kossel"); + show(`#${id}-fieldwrap`, state.mode === "kossel"); + show(`#${id}-dynwrap`, state.mode !== "kossel"); + show(`#${id}-kikwrap`, state.mode === "nanobeam"); + show(`#${id}-precwrap`, state.mode === "nanobeam"); + show(`#${id}-approw`, state.mode !== "kossel"); + show(`#${id}-apppanel`, state.mode !== "kossel" && state.showAppearance); + }; + modeButtons.forEach((b) => b.addEventListener("click", () => { state.mode = b.dataset.mode!; updateModeUI(); recompute(); })); + $(`#${id}-dyn`).addEventListener("change", (e) => { state.dynamical = (e.target as HTMLInputElement).checked; updateModeUI(); recompute(); }); + $(`#${id}-kik`).addEventListener("change", (e) => { state.kikuchi = (e.target as HTMLInputElement).checked; recompute(); }); + $(`#${id}-hkl`).addEventListener("change", (e) => { state.showHkl = (e.target as HTMLInputElement).checked; drawPattern(); }); + $(`#${id}-apptoggle`).addEventListener("click", () => { + state.showAppearance = !state.showAppearance; + $(`#${id}-apptoggle`).textContent = state.showAppearance ? "appearance ▴" : "appearance ▾"; + updateModeUI(); + if (state.showAppearance) drawPattern(); + }); + $(`#${id}-labels`).addEventListener("change", (e) => { state.showLabels = (e.target as HTMLInputElement).checked; drawAll(); }); + $(`#${id}-poly`).addEventListener("change", (e) => { state.polyhedra = (e.target as HTMLInputElement).checked; drawAll(); }); + $(`#${id}-ewald`).addEventListener("change", (e) => { state.showEwald = (e.target as HTMLInputElement).checked; computeSize(); drawAll(); }); + $(`#${id}-energy`).addEventListener("change", (e) => { + state.energy = +(e.target as HTMLSelectElement).value; + crystal = parseCrystal(PRESETS[state.preset], state.energy)!; + recompute(); + }); + $(`#${id}-ncell`).addEventListener("change", (e) => { + const n = Math.max(1, Math.min(3, Math.round(+(e.target as HTMLInputElement).value || 1))); + state.nCells = [n, n, n]; drawAll(); + }); + const slider = (sel: string, valSel: string, fmt: (v: number) => string, apply: (v: number) => void, heavy: boolean) => { + const inp = $(sel); + const out = $(valSel); + const update = () => { const v = +inp.value; out.textContent = fmt(v); apply(v); }; + inp.addEventListener("input", () => { update(); if (heavy) scheduleRecompute(); else drawPattern(); }); + update(); + }; + slider(`#${id}-thick`, `#${id}-thick-val`, (v) => `${v.toFixed(0)} Å`, (v) => { state.thickness = v; }, false); + slider(`#${id}-pow`, `#${id}-pow-val`, (v) => `p = ${v.toFixed(2)}`, (v) => { state.power = v; }, false); + slider(`#${id}-conv`, `#${id}-conv-val`, (v) => `${v.toFixed(1)} mrad`, (v) => { state.semiconv = v; }, true); + slider(`#${id}-prec`, `#${id}-prec-val`, (v) => (v > 0 ? `${v.toFixed(2)}°` : "off"), (v) => { state.precession = v; }, true); + slider(`#${id}-range`, `#${id}-range-val`, (v) => `${v.toFixed(2)} Å⁻¹`, (v) => { state.patternRange = v; }, true); + slider(`#${id}-field`, `#${id}-field-val`, (v) => `${v.toFixed(0)} mrad`, (v) => { state.fieldMrad = v; }, true); + // the convergence slider only changes the disk radius in nanobeam mode: no re-solve needed there + $(`#${id}-conv`).addEventListener("input", () => { if (state.mode === "nanobeam") drawPattern(); }); + + // ---------------------------------------------------------------- go + updateModeUI(); + computeSize(); + applyTheme(); + recompute(); + const ro = new ResizeObserver(() => { const old = S + Sc; computeSize(); if (S + Sc !== old) drawAll(); }); + ro.observe(wrap); + return () => { mo.disconnect(); ro.disconnect(); }; +} + +export default { render }; diff --git a/widget/js/diffsim-web/presets.ts b/widget/js/diffsim-web/presets.ts new file mode 100644 index 000000000..5527bfff4 --- /dev/null +++ b/widget/js/diffsim-web/presets.ts @@ -0,0 +1,14 @@ +// Generated by scripts/diffsim_presets.py: crystal data for the website +// simulator (200 keV, k_max 3.0 1/A). Do not edit by hand. +export const ENERGY_EV = 200000; +export const PRESETS: Record = { + "Si (diamond cubic)": "{\"name\":\"Si\",\"spacegroup\":\"Fd-3m (227)\",\"pointgroup\":\"m-3m\",\"cell\":[[5.431,0.0,0.0],[0.0,5.431,0.0],[0.0,0.0,5.431]],\"recip\":[[0.18412815319462345,0.0,0.0],[0.0,0.18412815319462345,0.0],[0.0,0.0,0.18412815319462345]],\"positions_frac\":[[0.0,0.0,0.0],[0.24999999999999997,0.24999999999999997,0.24999999999999997],[0.0,0.49999999999999994,0.49999999999999994],[0.24999999999999997,0.7499999999999999,0.7499999999999999],[0.49999999999999994,0.0,0.49999999999999994],[0.7499999999999999,0.24999999999999997,0.7499999999999999],[0.49999999999999994,0.49999999999999994,0.0],[0.7499999999999999,0.7499999999999999,0.24999999999999997]],\"numbers\":[14,14,14,14,14,14,14,14],\"symbols\":[\"Si\",\"Si\",\"Si\",\"Si\",\"Si\",\"Si\",\"Si\",\"Si\"],\"colors\":[[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627]],\"radii\":[1.11,1.11,1.11,1.11,1.11,1.11,1.11,1.11],\"hkl_i16\":\"8P/+//7/8P/+/wAA8P/+/wIA8P8AAP7/8P8AAAAA8P8AAAIA8P8CAP7/8P8CAAAA8P8CAAIA8f/7//3/8f/7////8f/7/wEA8f/7/wMA8f/9//v/8f/9//3/8f/9////8f/9/wEA8f/9/wMA8f/9/wUA8f////v/8f////3/8f//////8f///wEA8f///wMA8f///wUA8f8BAPv/8f8BAP3/8f8BAP//8f8BAAEA8f8BAAMA8f8BAAUA8f8DAPv/8f8DAP3/8f8DAP//8f8DAAEA8f8DAAMA8f8DAAUA8f8FAP3/8f8FAP//8f8FAAEA8f8FAAMA8v/4//7/8v/4/wAA8v/4/wIA8v/6//z/8v/6//7/8v/6/wAA8v/6/wIA8v/6/wQA8v/8//r/8v/8//z/8v/8//7/8v/8/wAA8v/8/wIA8v/8/wQA8v/8/wYA8v/+//j/8v/+//r/8v/+//z/8v/+//7/8v/+/wAA8v/+/wIA8v/+/wQA8v/+/wYA8v/+/wgA8v8AAPj/8v8AAPr/8v8AAPz/8v8AAP7/8v8AAAAA8v8AAAIA8v8AAAQA8v8AAAYA8v8AAAgA8v8CAPj/8v8CAPr/8v8CAPz/8v8CAP7/8v8CAAAA8v8CAAIA8v8CAAQA8v8CAAYA8v8CAAgA8v8EAPr/8v8EAPz/8v8EAP7/8v8EAAAA8v8EAAIA8v8EAAQA8v8EAAYA8v8GAPz/8v8GAP7/8v8GAAAA8v8GAAIA8v8GAAQA8v8IAP7/8v8IAAAA8v8IAAIA8//3//3/8//3////8//3/wEA8//3/wMA8//5//v/8//5//3/8//5////8//5/wEA8//5/wMA8//5/wUA8//7//n/8//7//v/8//7//3/8//7////8//7/wEA8//7/wMA8//7/wUA8//7/wcA8//9//f/8//9//n/8//9//v/8//9//3/8//9////8//9/wEA8//9/wMA8//9/wUA8//9/wcA8//9/wkA8/////f/8/////n/8/////v/8/////3/8///////8////wEA8////wMA8////wUA8////wcA8////wkA8/8BAPf/8/8BAPn/8/8BAPv/8/8BAP3/8/8BAP//8/8BAAEA8/8BAAMA8/8BAAUA8/8BAAcA8/8BAAkA8/8DAPf/8/8DAPn/8/8DAPv/8/8DAP3/8/8DAP//8/8DAAEA8/8DAAMA8/8DAAUA8/8DAAcA8/8DAAkA8/8FAPn/8/8FAPv/8/8FAP3/8/8FAP//8/8FAAEA8/8FAAMA8/8FAAUA8/8FAAcA8/8HAPv/8/8HAP3/8/8HAP//8/8HAAEA8/8HAAMA8/8HAAUA8/8JAP3/8/8JAP//8/8JAAEA8/8JAAMA9P/2//z/9P/2//7/9P/2/wAA9P/2/wIA9P/2/wQA9P/4//r/9P/4//z/9P/4//7/9P/4/wAA9P/4/wIA9P/4/wQA9P/4/wYA9P/6//j/9P/6//r/9P/6//z/9P/6//7/9P/6/wAA9P/6/wIA9P/6/wQA9P/6/wYA9P/6/wgA9P/8//b/9P/8//j/9P/8//r/9P/8//z/9P/8//7/9P/8/wAA9P/8/wIA9P/8/wQA9P/8/wYA9P/8/wgA9P/8/woA9P/+//b/9P/+//j/9P/+//r/9P/+//z/9P/+//7/9P/+/wAA9P/+/wIA9P/+/wQA9P/+/wYA9P/+/wgA9P/+/woA9P8AAPb/9P8AAPj/9P8AAPr/9P8AAPz/9P8AAP7/9P8AAAAA9P8AAAIA9P8AAAQA9P8AAAYA9P8AAAgA9P8AAAoA9P8CAPb/9P8CAPj/9P8CAPr/9P8CAPz/9P8CAP7/9P8CAAAA9P8CAAIA9P8CAAQA9P8CAAYA9P8CAAgA9P8CAAoA9P8EAPb/9P8EAPj/9P8EAPr/9P8EAPz/9P8EAP7/9P8EAAAA9P8EAAIA9P8EAAQA9P8EAAYA9P8EAAgA9P8EAAoA9P8GAPj/9P8GAPr/9P8GAPz/9P8GAP7/9P8GAAAA9P8GAAIA9P8GAAQA9P8GAAYA9P8GAAgA9P8IAPr/9P8IAPz/9P8IAP7/9P8IAAAA9P8IAAIA9P8IAAQA9P8IAAYA9P8KAPz/9P8KAP7/9P8KAAAA9P8KAAIA9P8KAAQA9f/1//3/9f/1////9f/1/wEA9f/1/wMA9f/3//n/9f/3//v/9f/3//3/9f/3////9f/3/wEA9f/3/wMA9f/3/wUA9f/3/wcA9f/5//f/9f/5//n/9f/5//v/9f/5//3/9f/5////9f/5/wEA9f/5/wMA9f/5/wUA9f/5/wcA9f/5/wkA9f/7//f/9f/7//n/9f/7//v/9f/7//3/9f/7////9f/7/wEA9f/7/wMA9f/7/wUA9f/7/wcA9f/7/wkA9f/9//X/9f/9//f/9f/9//n/9f/9//v/9f/9//3/9f/9////9f/9/wEA9f/9/wMA9f/9/wUA9f/9/wcA9f/9/wkA9f/9/wsA9f////X/9f////f/9f////n/9f////v/9f////3/9f//////9f///wEA9f///wMA9f///wUA9f///wcA9f///wkA9f///wsA9f8BAPX/9f8BAPf/9f8BAPn/9f8BAPv/9f8BAP3/9f8BAP//9f8BAAEA9f8BAAMA9f8BAAUA9f8BAAcA9f8BAAkA9f8BAAsA9f8DAPX/9f8DAPf/9f8DAPn/9f8DAPv/9f8DAP3/9f8DAP//9f8DAAEA9f8DAAMA9f8DAAUA9f8DAAcA9f8DAAkA9f8DAAsA9f8FAPf/9f8FAPn/9f8FAPv/9f8FAP3/9f8FAP//9f8FAAEA9f8FAAMA9f8FAAUA9f8FAAcA9f8FAAkA9f8HAPf/9f8HAPn/9f8HAPv/9f8HAP3/9f8HAP//9f8HAAEA9f8HAAMA9f8HAAUA9f8HAAcA9f8HAAkA9f8JAPn/9f8JAPv/9f8JAP3/9f8JAP//9f8JAAEA9f8JAAMA9f8JAAUA9f8JAAcA9f8LAP3/9f8LAP//9f8LAAEA9f8LAAMA9v/0//z/9v/0//7/9v/0/wAA9v/0/wIA9v/0/wQA9v/2//j/9v/2//r/9v/2//z/9v/2//7/9v/2/wAA9v/2/wIA9v/2/wQA9v/2/wYA9v/2/wgA9v/4//b/9v/4//j/9v/4//r/9v/4//z/9v/4//7/9v/4/wAA9v/4/wIA9v/4/wQA9v/4/wYA9v/4/wgA9v/4/woA9v/6//b/9v/6//j/9v/6//r/9v/6//z/9v/6//7/9v/6/wAA9v/6/wIA9v/6/wQA9v/6/wYA9v/6/wgA9v/6/woA9v/8//T/9v/8//b/9v/8//j/9v/8//r/9v/8//z/9v/8//7/9v/8/wAA9v/8/wIA9v/8/wQA9v/8/wYA9v/8/wgA9v/8/woA9v/8/wwA9v/+//T/9v/+//b/9v/+//j/9v/+//r/9v/+//z/9v/+//7/9v/+/wAA9v/+/wIA9v/+/wQA9v/+/wYA9v/+/wgA9v/+/woA9v/+/wwA9v8AAPT/9v8AAPb/9v8AAPj/9v8AAPr/9v8AAPz/9v8AAP7/9v8AAAAA9v8AAAIA9v8AAAQA9v8AAAYA9v8AAAgA9v8AAAoA9v8AAAwA9v8CAPT/9v8CAPb/9v8CAPj/9v8CAPr/9v8CAPz/9v8CAP7/9v8CAAAA9v8CAAIA9v8CAAQA9v8CAAYA9v8CAAgA9v8CAAoA9v8CAAwA9v8EAPT/9v8EAPb/9v8EAPj/9v8EAPr/9v8EAPz/9v8EAP7/9v8EAAAA9v8EAAIA9v8EAAQA9v8EAAYA9v8EAAgA9v8EAAoA9v8EAAwA9v8GAPb/9v8GAPj/9v8GAPr/9v8GAPz/9v8GAP7/9v8GAAAA9v8GAAIA9v8GAAQA9v8GAAYA9v8GAAgA9v8GAAoA9v8IAPb/9v8IAPj/9v8IAPr/9v8IAPz/9v8IAP7/9v8IAAAA9v8IAAIA9v8IAAQA9v8IAAYA9v8IAAgA9v8IAAoA9v8KAPj/9v8KAPr/9v8KAPz/9v8KAP7/9v8KAAAA9v8KAAIA9v8KAAQA9v8KAAYA9v8KAAgA9v8MAPz/9v8MAP7/9v8MAAAA9v8MAAIA9v8MAAQA9//z//3/9//z////9//z/wEA9//z/wMA9//1//n/9//1//v/9//1//3/9//1////9//1/wEA9//1/wMA9//1/wUA9//1/wcA9//3//f/9//3//n/9//3//v/9//3//3/9//3////9//3/wEA9//3/wMA9//3/wUA9//3/wcA9//3/wkA9//5//X/9//5//f/9//5//n/9//5//v/9//5//3/9//5////9//5/wEA9//5/wMA9//5/wUA9//5/wcA9//5/wkA9//5/wsA9//7//X/9//7//f/9//7//n/9//7//v/9//7//3/9//7////9//7/wEA9//7/wMA9//7/wUA9//7/wcA9//7/wkA9//7/wsA9//9//P/9//9//X/9//9//f/9//9//n/9//9//v/9//9//3/9//9////9//9/wEA9//9/wMA9//9/wUA9//9/wcA9//9/wkA9//9/wsA9//9/w0A9/////P/9/////X/9/////f/9/////n/9/////v/9/////3/9///////9////wEA9////wMA9////wUA9////wcA9////wkA9////wsA9////w0A9/8BAPP/9/8BAPX/9/8BAPf/9/8BAPn/9/8BAPv/9/8BAP3/9/8BAP//9/8BAAEA9/8BAAMA9/8BAAUA9/8BAAcA9/8BAAkA9/8BAAsA9/8BAA0A9/8DAPP/9/8DAPX/9/8DAPf/9/8DAPn/9/8DAPv/9/8DAP3/9/8DAP//9/8DAAEA9/8DAAMA9/8DAAUA9/8DAAcA9/8DAAkA9/8DAAsA9/8DAA0A9/8FAPX/9/8FAPf/9/8FAPn/9/8FAPv/9/8FAP3/9/8FAP//9/8FAAEA9/8FAAMA9/8FAAUA9/8FAAcA9/8FAAkA9/8FAAsA9/8HAPX/9/8HAPf/9/8HAPn/9/8HAPv/9/8HAP3/9/8HAP//9/8HAAEA9/8HAAMA9/8HAAUA9/8HAAcA9/8HAAkA9/8HAAsA9/8JAPf/9/8JAPn/9/8JAPv/9/8JAP3/9/8JAP//9/8JAAEA9/8JAAMA9/8JAAUA9/8JAAcA9/8JAAkA9/8LAPn/9/8LAPv/9/8LAP3/9/8LAP//9/8LAAEA9/8LAAMA9/8LAAUA9/8LAAcA9/8NAP3/9/8NAP//9/8NAAEA9/8NAAMA+P/y//7/+P/y/wAA+P/y/wIA+P/0//r/+P/0//z/+P/0//7/+P/0/wAA+P/0/wIA+P/0/wQA+P/0/wYA+P/2//b/+P/2//j/+P/2//r/+P/2//z/+P/2//7/+P/2/wAA+P/2/wIA+P/2/wQA+P/2/wYA+P/2/wgA+P/2/woA+P/4//b/+P/4//j/+P/4//r/+P/4//z/+P/4//7/+P/4/wAA+P/4/wIA+P/4/wQA+P/4/wYA+P/4/wgA+P/4/woA+P/6//T/+P/6//b/+P/6//j/+P/6//r/+P/6//z/+P/6//7/+P/6/wAA+P/6/wIA+P/6/wQA+P/6/wYA+P/6/wgA+P/6/woA+P/6/wwA+P/8//T/+P/8//b/+P/8//j/+P/8//r/+P/8//z/+P/8//7/+P/8/wAA+P/8/wIA+P/8/wQA+P/8/wYA+P/8/wgA+P/8/woA+P/8/wwA+P/+//L/+P/+//T/+P/+//b/+P/+//j/+P/+//r/+P/+//z/+P/+//7/+P/+/wAA+P/+/wIA+P/+/wQA+P/+/wYA+P/+/wgA+P/+/woA+P/+/wwA+P/+/w4A+P8AAPL/+P8AAPT/+P8AAPb/+P8AAPj/+P8AAPr/+P8AAPz/+P8AAP7/+P8AAAAA+P8AAAIA+P8AAAQA+P8AAAYA+P8AAAgA+P8AAAoA+P8AAAwA+P8AAA4A+P8CAPL/+P8CAPT/+P8CAPb/+P8CAPj/+P8CAPr/+P8CAPz/+P8CAP7/+P8CAAAA+P8CAAIA+P8CAAQA+P8CAAYA+P8CAAgA+P8CAAoA+P8CAAwA+P8CAA4A+P8EAPT/+P8EAPb/+P8EAPj/+P8EAPr/+P8EAPz/+P8EAP7/+P8EAAAA+P8EAAIA+P8EAAQA+P8EAAYA+P8EAAgA+P8EAAoA+P8EAAwA+P8GAPT/+P8GAPb/+P8GAPj/+P8GAPr/+P8GAPz/+P8GAP7/+P8GAAAA+P8GAAIA+P8GAAQA+P8GAAYA+P8GAAgA+P8GAAoA+P8GAAwA+P8IAPb/+P8IAPj/+P8IAPr/+P8IAPz/+P8IAP7/+P8IAAAA+P8IAAIA+P8IAAQA+P8IAAYA+P8IAAgA+P8IAAoA+P8KAPb/+P8KAPj/+P8KAPr/+P8KAPz/+P8KAP7/+P8KAAAA+P8KAAIA+P8KAAQA+P8KAAYA+P8KAAgA+P8KAAoA+P8MAPr/+P8MAPz/+P8MAP7/+P8MAAAA+P8MAAIA+P8MAAQA+P8MAAYA+P8OAP7/+P8OAAAA+P8OAAIA+f/z//v/+f/z//3/+f/z////+f/z/wEA+f/z/wMA+f/z/wUA+f/1//f/+f/1//n/+f/1//v/+f/1//3/+f/1////+f/1/wEA+f/1/wMA+f/1/wUA+f/1/wcA+f/1/wkA+f/3//X/+f/3//f/+f/3//n/+f/3//v/+f/3//3/+f/3////+f/3/wEA+f/3/wMA+f/3/wUA+f/3/wcA+f/3/wkA+f/3/wsA+f/5//X/+f/5//f/+f/5//n/+f/5//v/+f/5//3/+f/5////+f/5/wEA+f/5/wMA+f/5/wUA+f/5/wcA+f/5/wkA+f/5/wsA+f/7//P/+f/7//X/+f/7//f/+f/7//n/+f/7//v/+f/7//3/+f/7////+f/7/wEA+f/7/wMA+f/7/wUA+f/7/wcA+f/7/wkA+f/7/wsA+f/7/w0A+f/9//P/+f/9//X/+f/9//f/+f/9//n/+f/9//v/+f/9//3/+f/9////+f/9/wEA+f/9/wMA+f/9/wUA+f/9/wcA+f/9/wkA+f/9/wsA+f/9/w0A+f////P/+f////X/+f////f/+f////n/+f////v/+f////3/+f//////+f///wEA+f///wMA+f///wUA+f///wcA+f///wkA+f///wsA+f///w0A+f8BAPP/+f8BAPX/+f8BAPf/+f8BAPn/+f8BAPv/+f8BAP3/+f8BAP//+f8BAAEA+f8BAAMA+f8BAAUA+f8BAAcA+f8BAAkA+f8BAAsA+f8BAA0A+f8DAPP/+f8DAPX/+f8DAPf/+f8DAPn/+f8DAPv/+f8DAP3/+f8DAP//+f8DAAEA+f8DAAMA+f8DAAUA+f8DAAcA+f8DAAkA+f8DAAsA+f8DAA0A+f8FAPP/+f8FAPX/+f8FAPf/+f8FAPn/+f8FAPv/+f8FAP3/+f8FAP//+f8FAAEA+f8FAAMA+f8FAAUA+f8FAAcA+f8FAAkA+f8FAAsA+f8FAA0A+f8HAPX/+f8HAPf/+f8HAPn/+f8HAPv/+f8HAP3/+f8HAP//+f8HAAEA+f8HAAMA+f8HAAUA+f8HAAcA+f8HAAkA+f8HAAsA+f8JAPX/+f8JAPf/+f8JAPn/+f8JAPv/+f8JAP3/+f8JAP//+f8JAAEA+f8JAAMA+f8JAAUA+f8JAAcA+f8JAAkA+f8JAAsA+f8LAPf/+f8LAPn/+f8LAPv/+f8LAP3/+f8LAP//+f8LAAEA+f8LAAMA+f8LAAUA+f8LAAcA+f8LAAkA+f8NAPv/+f8NAP3/+f8NAP//+f8NAAEA+f8NAAMA+f8NAAUA+v/y//z/+v/y//7/+v/y/wAA+v/y/wIA+v/y/wQA+v/0//j/+v/0//r/+v/0//z/+v/0//7/+v/0/wAA+v/0/wIA+v/0/wQA+v/0/wYA+v/0/wgA+v/2//b/+v/2//j/+v/2//r/+v/2//z/+v/2//7/+v/2/wAA+v/2/wIA+v/2/wQA+v/2/wYA+v/2/wgA+v/2/woA+v/4//T/+v/4//b/+v/4//j/+v/4//r/+v/4//z/+v/4//7/+v/4/wAA+v/4/wIA+v/4/wQA+v/4/wYA+v/4/wgA+v/4/woA+v/4/wwA+v/6//T/+v/6//b/+v/6//j/+v/6//r/+v/6//z/+v/6//7/+v/6/wAA+v/6/wIA+v/6/wQA+v/6/wYA+v/6/wgA+v/6/woA+v/6/wwA+v/8//L/+v/8//T/+v/8//b/+v/8//j/+v/8//r/+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/8/wYA+v/8/wgA+v/8/woA+v/8/wwA+v/8/w4A+v/+//L/+v/+//T/+v/+//b/+v/+//j/+v/+//r/+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v/+/wYA+v/+/wgA+v/+/woA+v/+/wwA+v/+/w4A+v8AAPL/+v8AAPT/+v8AAPb/+v8AAPj/+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8AAAgA+v8AAAoA+v8AAAwA+v8AAA4A+v8CAPL/+v8CAPT/+v8CAPb/+v8CAPj/+v8CAPr/+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8CAAYA+v8CAAgA+v8CAAoA+v8CAAwA+v8CAA4A+v8EAPL/+v8EAPT/+v8EAPb/+v8EAPj/+v8EAPr/+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8EAAYA+v8EAAgA+v8EAAoA+v8EAAwA+v8EAA4A+v8GAPT/+v8GAPb/+v8GAPj/+v8GAPr/+v8GAPz/+v8GAP7/+v8GAAAA+v8GAAIA+v8GAAQA+v8GAAYA+v8GAAgA+v8GAAoA+v8GAAwA+v8IAPT/+v8IAPb/+v8IAPj/+v8IAPr/+v8IAPz/+v8IAP7/+v8IAAAA+v8IAAIA+v8IAAQA+v8IAAYA+v8IAAgA+v8IAAoA+v8IAAwA+v8KAPb/+v8KAPj/+v8KAPr/+v8KAPz/+v8KAP7/+v8KAAAA+v8KAAIA+v8KAAQA+v8KAAYA+v8KAAgA+v8KAAoA+v8MAPj/+v8MAPr/+v8MAPz/+v8MAP7/+v8MAAAA+v8MAAIA+v8MAAQA+v8MAAYA+v8MAAgA+v8OAPz/+v8OAP7/+v8OAAAA+v8OAAIA+v8OAAQA+//x//3/+//x////+//x/wEA+//x/wMA+//z//n/+//z//v/+//z//3/+//z////+//z/wEA+//z/wMA+//z/wUA+//z/wcA+//1//f/+//1//n/+//1//v/+//1//3/+//1////+//1/wEA+//1/wMA+//1/wUA+//1/wcA+//1/wkA+//3//X/+//3//f/+//3//n/+//3//v/+//3//3/+//3////+//3/wEA+//3/wMA+//3/wUA+//3/wcA+//3/wkA+//3/wsA+//5//P/+//5//X/+//5//f/+//5//n/+//5//v/+//5//3/+//5////+//5/wEA+//5/wMA+//5/wUA+//5/wcA+//5/wkA+//5/wsA+//5/w0A+//7//P/+//7//X/+//7//f/+//7//n/+//7//v/+//7//3/+//7////+//7/wEA+//7/wMA+//7/wUA+//7/wcA+//7/wkA+//7/wsA+//7/w0A+//9//H/+//9//P/+//9//X/+//9//f/+//9//n/+//9//v/+//9//3/+//9////+//9/wEA+//9/wMA+//9/wUA+//9/wcA+//9/wkA+//9/wsA+//9/w0A+//9/w8A+/////H/+/////P/+/////X/+/////f/+/////n/+/////v/+/////3/+///////+////wEA+////wMA+////wUA+////wcA+////wkA+////wsA+////w0A+////w8A+/8BAPH/+/8BAPP/+/8BAPX/+/8BAPf/+/8BAPn/+/8BAPv/+/8BAP3/+/8BAP//+/8BAAEA+/8BAAMA+/8BAAUA+/8BAAcA+/8BAAkA+/8BAAsA+/8BAA0A+/8BAA8A+/8DAPH/+/8DAPP/+/8DAPX/+/8DAPf/+/8DAPn/+/8DAPv/+/8DAP3/+/8DAP//+/8DAAEA+/8DAAMA+/8DAAUA+/8DAAcA+/8DAAkA+/8DAAsA+/8DAA0A+/8DAA8A+/8FAPP/+/8FAPX/+/8FAPf/+/8FAPn/+/8FAPv/+/8FAP3/+/8FAP//+/8FAAEA+/8FAAMA+/8FAAUA+/8FAAcA+/8FAAkA+/8FAAsA+/8FAA0A+/8HAPP/+/8HAPX/+/8HAPf/+/8HAPn/+/8HAPv/+/8HAP3/+/8HAP//+/8HAAEA+/8HAAMA+/8HAAUA+/8HAAcA+/8HAAkA+/8HAAsA+/8HAA0A+/8JAPX/+/8JAPf/+/8JAPn/+/8JAPv/+/8JAP3/+/8JAP//+/8JAAEA+/8JAAMA+/8JAAUA+/8JAAcA+/8JAAkA+/8JAAsA+/8LAPf/+/8LAPn/+/8LAPv/+/8LAP3/+/8LAP//+/8LAAEA+/8LAAMA+/8LAAUA+/8LAAcA+/8LAAkA+/8NAPn/+/8NAPv/+/8NAP3/+/8NAP//+/8NAAEA+/8NAAMA+/8NAAUA+/8NAAcA+/8PAP3/+/8PAP//+/8PAAEA+/8PAAMA/P/y//r//P/y//z//P/y//7//P/y/wAA/P/y/wIA/P/y/wQA/P/y/wYA/P/0//b//P/0//j//P/0//r//P/0//z//P/0//7//P/0/wAA/P/0/wIA/P/0/wQA/P/0/wYA/P/0/wgA/P/0/woA/P/2//T//P/2//b//P/2//j//P/2//r//P/2//z//P/2//7//P/2/wAA/P/2/wIA/P/2/wQA/P/2/wYA/P/2/wgA/P/2/woA/P/2/wwA/P/4//T//P/4//b//P/4//j//P/4//r//P/4//z//P/4//7//P/4/wAA/P/4/wIA/P/4/wQA/P/4/wYA/P/4/wgA/P/4/woA/P/4/wwA/P/6//L//P/6//T//P/6//b//P/6//j//P/6//r//P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/6/wYA/P/6/wgA/P/6/woA/P/6/wwA/P/6/w4A/P/8//L//P/8//T//P/8//b//P/8//j//P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/8/wgA/P/8/woA/P/8/wwA/P/8/w4A/P/+//L//P/+//T//P/+//b//P/+//j//P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P/+/wgA/P/+/woA/P/+/wwA/P/+/w4A/P8AAPL//P8AAPT//P8AAPb//P8AAPj//P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8AAAgA/P8AAAoA/P8AAAwA/P8AAA4A/P8CAPL//P8CAPT//P8CAPb//P8CAPj//P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8CAAgA/P8CAAoA/P8CAAwA/P8CAA4A/P8EAPL//P8EAPT//P8EAPb//P8EAPj//P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8EAAgA/P8EAAoA/P8EAAwA/P8EAA4A/P8GAPL//P8GAPT//P8GAPb//P8GAPj//P8GAPr//P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8GAAYA/P8GAAgA/P8GAAoA/P8GAAwA/P8GAA4A/P8IAPT//P8IAPb//P8IAPj//P8IAPr//P8IAPz//P8IAP7//P8IAAAA/P8IAAIA/P8IAAQA/P8IAAYA/P8IAAgA/P8IAAoA/P8IAAwA/P8KAPT//P8KAPb//P8KAPj//P8KAPr//P8KAPz//P8KAP7//P8KAAAA/P8KAAIA/P8KAAQA/P8KAAYA/P8KAAgA/P8KAAoA/P8KAAwA/P8MAPb//P8MAPj//P8MAPr//P8MAPz//P8MAP7//P8MAAAA/P8MAAIA/P8MAAQA/P8MAAYA/P8MAAgA/P8MAAoA/P8OAPr//P8OAPz//P8OAP7//P8OAAAA/P8OAAIA/P8OAAQA/P8OAAYA/f/x//v//f/x//3//f/x/////f/x/wEA/f/x/wMA/f/x/wUA/f/z//f//f/z//n//f/z//v//f/z//3//f/z/////f/z/wEA/f/z/wMA/f/z/wUA/f/z/wcA/f/z/wkA/f/1//X//f/1//f//f/1//n//f/1//v//f/1//3//f/1/////f/1/wEA/f/1/wMA/f/1/wUA/f/1/wcA/f/1/wkA/f/1/wsA/f/3//P//f/3//X//f/3//f//f/3//n//f/3//v//f/3//3//f/3/////f/3/wEA/f/3/wMA/f/3/wUA/f/3/wcA/f/3/wkA/f/3/wsA/f/3/w0A/f/5//P//f/5//X//f/5//f//f/5//n//f/5//v//f/5//3//f/5/////f/5/wEA/f/5/wMA/f/5/wUA/f/5/wcA/f/5/wkA/f/5/wsA/f/5/w0A/f/7//H//f/7//P//f/7//X//f/7//f//f/7//n//f/7//v//f/7//3//f/7/////f/7/wEA/f/7/wMA/f/7/wUA/f/7/wcA/f/7/wkA/f/7/wsA/f/7/w0A/f/7/w8A/f/9//H//f/9//P//f/9//X//f/9//f//f/9//n//f/9//v//f/9//3//f/9/////f/9/wEA/f/9/wMA/f/9/wUA/f/9/wcA/f/9/wkA/f/9/wsA/f/9/w0A/f/9/w8A/f////H//f////P//f////X//f////f//f////n//f////v//f////3//f///////f///wEA/f///wMA/f///wUA/f///wcA/f///wkA/f///wsA/f///w0A/f///w8A/f8BAPH//f8BAPP//f8BAPX//f8BAPf//f8BAPn//f8BAPv//f8BAP3//f8BAP///f8BAAEA/f8BAAMA/f8BAAUA/f8BAAcA/f8BAAkA/f8BAAsA/f8BAA0A/f8BAA8A/f8DAPH//f8DAPP//f8DAPX//f8DAPf//f8DAPn//f8DAPv//f8DAP3//f8DAP///f8DAAEA/f8DAAMA/f8DAAUA/f8DAAcA/f8DAAkA/f8DAAsA/f8DAA0A/f8DAA8A/f8FAPH//f8FAPP//f8FAPX//f8FAPf//f8FAPn//f8FAPv//f8FAP3//f8FAP///f8FAAEA/f8FAAMA/f8FAAUA/f8FAAcA/f8FAAkA/f8FAAsA/f8FAA0A/f8FAA8A/f8HAPP//f8HAPX//f8HAPf//f8HAPn//f8HAPv//f8HAP3//f8HAP///f8HAAEA/f8HAAMA/f8HAAUA/f8HAAcA/f8HAAkA/f8HAAsA/f8HAA0A/f8JAPP//f8JAPX//f8JAPf//f8JAPn//f8JAPv//f8JAP3//f8JAP///f8JAAEA/f8JAAMA/f8JAAUA/f8JAAcA/f8JAAkA/f8JAAsA/f8JAA0A/f8LAPX//f8LAPf//f8LAPn//f8LAPv//f8LAP3//f8LAP///f8LAAEA/f8LAAMA/f8LAAUA/f8LAAcA/f8LAAkA/f8LAAsA/f8NAPf//f8NAPn//f8NAPv//f8NAP3//f8NAP///f8NAAEA/f8NAAMA/f8NAAUA/f8NAAcA/f8NAAkA/f8PAPv//f8PAP3//f8PAP///f8PAAEA/f8PAAMA/f8PAAUA/v/w//7//v/w/wAA/v/w/wIA/v/y//j//v/y//r//v/y//z//v/y//7//v/y/wAA/v/y/wIA/v/y/wQA/v/y/wYA/v/y/wgA/v/0//b//v/0//j//v/0//r//v/0//z//v/0//7//v/0/wAA/v/0/wIA/v/0/wQA/v/0/wYA/v/0/wgA/v/0/woA/v/2//T//v/2//b//v/2//j//v/2//r//v/2//z//v/2//7//v/2/wAA/v/2/wIA/v/2/wQA/v/2/wYA/v/2/wgA/v/2/woA/v/2/wwA/v/4//L//v/4//T//v/4//b//v/4//j//v/4//r//v/4//z//v/4//7//v/4/wAA/v/4/wIA/v/4/wQA/v/4/wYA/v/4/wgA/v/4/woA/v/4/wwA/v/4/w4A/v/6//L//v/6//T//v/6//b//v/6//j//v/6//r//v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/6/wYA/v/6/wgA/v/6/woA/v/6/wwA/v/6/w4A/v/8//L//v/8//T//v/8//b//v/8//j//v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/8/wgA/v/8/woA/v/8/wwA/v/8/w4A/v/+//D//v/+//L//v/+//T//v/+//b//v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v/+/woA/v/+/wwA/v/+/w4A/v/+/xAA/v8AAPD//v8AAPL//v8AAPT//v8AAPb//v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8AAAoA/v8AAAwA/v8AAA4A/v8AABAA/v8CAPD//v8CAPL//v8CAPT//v8CAPb//v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8CAAoA/v8CAAwA/v8CAA4A/v8CABAA/v8EAPL//v8EAPT//v8EAPb//v8EAPj//v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8EAAgA/v8EAAoA/v8EAAwA/v8EAA4A/v8GAPL//v8GAPT//v8GAPb//v8GAPj//v8GAPr//v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8GAAYA/v8GAAgA/v8GAAoA/v8GAAwA/v8GAA4A/v8IAPL//v8IAPT//v8IAPb//v8IAPj//v8IAPr//v8IAPz//v8IAP7//v8IAAAA/v8IAAIA/v8IAAQA/v8IAAYA/v8IAAgA/v8IAAoA/v8IAAwA/v8IAA4A/v8KAPT//v8KAPb//v8KAPj//v8KAPr//v8KAPz//v8KAP7//v8KAAAA/v8KAAIA/v8KAAQA/v8KAAYA/v8KAAgA/v8KAAoA/v8KAAwA/v8MAPb//v8MAPj//v8MAPr//v8MAPz//v8MAP7//v8MAAAA/v8MAAIA/v8MAAQA/v8MAAYA/v8MAAgA/v8MAAoA/v8OAPj//v8OAPr//v8OAPz//v8OAP7//v8OAAAA/v8OAAIA/v8OAAQA/v8OAAYA/v8OAAgA/v8QAP7//v8QAAAA/v8QAAIA///x//v////x//3////x///////x/wEA///x/wMA///x/wUA///z//f////z//n////z//v////z//3////z///////z/wEA///z/wMA///z/wUA///z/wcA///z/wkA///1//X////1//f////1//n////1//v////1//3////1///////1/wEA///1/wMA///1/wUA///1/wcA///1/wkA///1/wsA///3//P////3//X////3//f////3//n////3//v////3//3////3///////3/wEA///3/wMA///3/wUA///3/wcA///3/wkA///3/wsA///3/w0A///5//P////5//X////5//f////5//n////5//v////5//3////5///////5/wEA///5/wMA///5/wUA///5/wcA///5/wkA///5/wsA///5/w0A///7//H////7//P////7//X////7//f////7//n////7//v////7//3////7///////7/wEA///7/wMA///7/wUA///7/wcA///7/wkA///7/wsA///7/w0A///7/w8A///9//H////9//P////9//X////9//f////9//n////9//v////9//3////9///////9/wEA///9/wMA///9/wUA///9/wcA///9/wkA///9/wsA///9/w0A///9/w8A//////H///////P///////X///////f///////n///////v///////3//////////////wEA/////wMA/////wUA/////wcA/////wkA/////wsA/////w0A/////w8A//8BAPH///8BAPP///8BAPX///8BAPf///8BAPn///8BAPv///8BAP3///8BAP////8BAAEA//8BAAMA//8BAAUA//8BAAcA//8BAAkA//8BAAsA//8BAA0A//8BAA8A//8DAPH///8DAPP///8DAPX///8DAPf///8DAPn///8DAPv///8DAP3///8DAP////8DAAEA//8DAAMA//8DAAUA//8DAAcA//8DAAkA//8DAAsA//8DAA0A//8DAA8A//8FAPH///8FAPP///8FAPX///8FAPf///8FAPn///8FAPv///8FAP3///8FAP////8FAAEA//8FAAMA//8FAAUA//8FAAcA//8FAAkA//8FAAsA//8FAA0A//8FAA8A//8HAPP///8HAPX///8HAPf///8HAPn///8HAPv///8HAP3///8HAP////8HAAEA//8HAAMA//8HAAUA//8HAAcA//8HAAkA//8HAAsA//8HAA0A//8JAPP///8JAPX///8JAPf///8JAPn///8JAPv///8JAP3///8JAP////8JAAEA//8JAAMA//8JAAUA//8JAAcA//8JAAkA//8JAAsA//8JAA0A//8LAPX///8LAPf///8LAPn///8LAPv///8LAP3///8LAP////8LAAEA//8LAAMA//8LAAUA//8LAAcA//8LAAkA//8LAAsA//8NAPf///8NAPn///8NAPv///8NAP3///8NAP////8NAAEA//8NAAMA//8NAAUA//8NAAcA//8NAAkA//8PAPv///8PAP3///8PAP////8PAAEA//8PAAMA//8PAAUAAADw//7/AADw/wAAAADw/wIAAADy//j/AADy//r/AADy//z/AADy//7/AADy/wAAAADy/wIAAADy/wQAAADy/wYAAADy/wgAAAD0//b/AAD0//j/AAD0//r/AAD0//z/AAD0//7/AAD0/wAAAAD0/wIAAAD0/wQAAAD0/wYAAAD0/wgAAAD0/woAAAD2//T/AAD2//b/AAD2//j/AAD2//r/AAD2//z/AAD2//7/AAD2/wAAAAD2/wIAAAD2/wQAAAD2/wYAAAD2/wgAAAD2/woAAAD2/wwAAAD4//L/AAD4//T/AAD4//b/AAD4//j/AAD4//r/AAD4//z/AAD4//7/AAD4/wAAAAD4/wIAAAD4/wQAAAD4/wYAAAD4/wgAAAD4/woAAAD4/wwAAAD4/w4AAAD6//L/AAD6//T/AAD6//b/AAD6//j/AAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD6/wgAAAD6/woAAAD6/wwAAAD6/w4AAAD8//L/AAD8//T/AAD8//b/AAD8//j/AAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD8/wgAAAD8/woAAAD8/wwAAAD8/w4AAAD+//D/AAD+//L/AAD+//T/AAD+//b/AAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD+/woAAAD+/wwAAAD+/w4AAAD+/xAAAAAAAPD/AAAAAPL/AAAAAPT/AAAAAPb/AAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAAAAAoAAAAAAAwAAAAAAA4AAAAAABAAAAACAPD/AAACAPL/AAACAPT/AAACAPb/AAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAACAAoAAAACAAwAAAACAA4AAAACABAAAAAEAPL/AAAEAPT/AAAEAPb/AAAEAPj/AAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAEAAgAAAAEAAoAAAAEAAwAAAAEAA4AAAAGAPL/AAAGAPT/AAAGAPb/AAAGAPj/AAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAGAAgAAAAGAAoAAAAGAAwAAAAGAA4AAAAIAPL/AAAIAPT/AAAIAPb/AAAIAPj/AAAIAPr/AAAIAPz/AAAIAP7/AAAIAAAAAAAIAAIAAAAIAAQAAAAIAAYAAAAIAAgAAAAIAAoAAAAIAAwAAAAIAA4AAAAKAPT/AAAKAPb/AAAKAPj/AAAKAPr/AAAKAPz/AAAKAP7/AAAKAAAAAAAKAAIAAAAKAAQAAAAKAAYAAAAKAAgAAAAKAAoAAAAKAAwAAAAMAPb/AAAMAPj/AAAMAPr/AAAMAPz/AAAMAP7/AAAMAAAAAAAMAAIAAAAMAAQAAAAMAAYAAAAMAAgAAAAMAAoAAAAOAPj/AAAOAPr/AAAOAPz/AAAOAP7/AAAOAAAAAAAOAAIAAAAOAAQAAAAOAAYAAAAOAAgAAAAQAP7/AAAQAAAAAAAQAAIAAQDx//v/AQDx//3/AQDx////AQDx/wEAAQDx/wMAAQDx/wUAAQDz//f/AQDz//n/AQDz//v/AQDz//3/AQDz////AQDz/wEAAQDz/wMAAQDz/wUAAQDz/wcAAQDz/wkAAQD1//X/AQD1//f/AQD1//n/AQD1//v/AQD1//3/AQD1////AQD1/wEAAQD1/wMAAQD1/wUAAQD1/wcAAQD1/wkAAQD1/wsAAQD3//P/AQD3//X/AQD3//f/AQD3//n/AQD3//v/AQD3//3/AQD3////AQD3/wEAAQD3/wMAAQD3/wUAAQD3/wcAAQD3/wkAAQD3/wsAAQD3/w0AAQD5//P/AQD5//X/AQD5//f/AQD5//n/AQD5//v/AQD5//3/AQD5////AQD5/wEAAQD5/wMAAQD5/wUAAQD5/wcAAQD5/wkAAQD5/wsAAQD5/w0AAQD7//H/AQD7//P/AQD7//X/AQD7//f/AQD7//n/AQD7//v/AQD7//3/AQD7////AQD7/wEAAQD7/wMAAQD7/wUAAQD7/wcAAQD7/wkAAQD7/wsAAQD7/w0AAQD7/w8AAQD9//H/AQD9//P/AQD9//X/AQD9//f/AQD9//n/AQD9//v/AQD9//3/AQD9////AQD9/wEAAQD9/wMAAQD9/wUAAQD9/wcAAQD9/wkAAQD9/wsAAQD9/w0AAQD9/w8AAQD///H/AQD///P/AQD///X/AQD///f/AQD///n/AQD///v/AQD///3/AQD/////AQD//wEAAQD//wMAAQD//wUAAQD//wcAAQD//wkAAQD//wsAAQD//w0AAQD//w8AAQABAPH/AQABAPP/AQABAPX/AQABAPf/AQABAPn/AQABAPv/AQABAP3/AQABAP//AQABAAEAAQABAAMAAQABAAUAAQABAAcAAQABAAkAAQABAAsAAQABAA0AAQABAA8AAQADAPH/AQADAPP/AQADAPX/AQADAPf/AQADAPn/AQADAPv/AQADAP3/AQADAP//AQADAAEAAQADAAMAAQADAAUAAQADAAcAAQADAAkAAQADAAsAAQADAA0AAQADAA8AAQAFAPH/AQAFAPP/AQAFAPX/AQAFAPf/AQAFAPn/AQAFAPv/AQAFAP3/AQAFAP//AQAFAAEAAQAFAAMAAQAFAAUAAQAFAAcAAQAFAAkAAQAFAAsAAQAFAA0AAQAFAA8AAQAHAPP/AQAHAPX/AQAHAPf/AQAHAPn/AQAHAPv/AQAHAP3/AQAHAP//AQAHAAEAAQAHAAMAAQAHAAUAAQAHAAcAAQAHAAkAAQAHAAsAAQAHAA0AAQAJAPP/AQAJAPX/AQAJAPf/AQAJAPn/AQAJAPv/AQAJAP3/AQAJAP//AQAJAAEAAQAJAAMAAQAJAAUAAQAJAAcAAQAJAAkAAQAJAAsAAQAJAA0AAQALAPX/AQALAPf/AQALAPn/AQALAPv/AQALAP3/AQALAP//AQALAAEAAQALAAMAAQALAAUAAQALAAcAAQALAAkAAQALAAsAAQANAPf/AQANAPn/AQANAPv/AQANAP3/AQANAP//AQANAAEAAQANAAMAAQANAAUAAQANAAcAAQANAAkAAQAPAPv/AQAPAP3/AQAPAP//AQAPAAEAAQAPAAMAAQAPAAUAAgDw//7/AgDw/wAAAgDw/wIAAgDy//j/AgDy//r/AgDy//z/AgDy//7/AgDy/wAAAgDy/wIAAgDy/wQAAgDy/wYAAgDy/wgAAgD0//b/AgD0//j/AgD0//r/AgD0//z/AgD0//7/AgD0/wAAAgD0/wIAAgD0/wQAAgD0/wYAAgD0/wgAAgD0/woAAgD2//T/AgD2//b/AgD2//j/AgD2//r/AgD2//z/AgD2//7/AgD2/wAAAgD2/wIAAgD2/wQAAgD2/wYAAgD2/wgAAgD2/woAAgD2/wwAAgD4//L/AgD4//T/AgD4//b/AgD4//j/AgD4//r/AgD4//z/AgD4//7/AgD4/wAAAgD4/wIAAgD4/wQAAgD4/wYAAgD4/wgAAgD4/woAAgD4/wwAAgD4/w4AAgD6//L/AgD6//T/AgD6//b/AgD6//j/AgD6//r/AgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD6/wYAAgD6/wgAAgD6/woAAgD6/wwAAgD6/w4AAgD8//L/AgD8//T/AgD8//b/AgD8//j/AgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD8/wgAAgD8/woAAgD8/wwAAgD8/w4AAgD+//D/AgD+//L/AgD+//T/AgD+//b/AgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD+/woAAgD+/wwAAgD+/w4AAgD+/xAAAgAAAPD/AgAAAPL/AgAAAPT/AgAAAPb/AgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgAAAAoAAgAAAAwAAgAAAA4AAgAAABAAAgACAPD/AgACAPL/AgACAPT/AgACAPb/AgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgACAAoAAgACAAwAAgACAA4AAgACABAAAgAEAPL/AgAEAPT/AgAEAPb/AgAEAPj/AgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAEAAgAAgAEAAoAAgAEAAwAAgAEAA4AAgAGAPL/AgAGAPT/AgAGAPb/AgAGAPj/AgAGAPr/AgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAGAAYAAgAGAAgAAgAGAAoAAgAGAAwAAgAGAA4AAgAIAPL/AgAIAPT/AgAIAPb/AgAIAPj/AgAIAPr/AgAIAPz/AgAIAP7/AgAIAAAAAgAIAAIAAgAIAAQAAgAIAAYAAgAIAAgAAgAIAAoAAgAIAAwAAgAIAA4AAgAKAPT/AgAKAPb/AgAKAPj/AgAKAPr/AgAKAPz/AgAKAP7/AgAKAAAAAgAKAAIAAgAKAAQAAgAKAAYAAgAKAAgAAgAKAAoAAgAKAAwAAgAMAPb/AgAMAPj/AgAMAPr/AgAMAPz/AgAMAP7/AgAMAAAAAgAMAAIAAgAMAAQAAgAMAAYAAgAMAAgAAgAMAAoAAgAOAPj/AgAOAPr/AgAOAPz/AgAOAP7/AgAOAAAAAgAOAAIAAgAOAAQAAgAOAAYAAgAOAAgAAgAQAP7/AgAQAAAAAgAQAAIAAwDx//v/AwDx//3/AwDx////AwDx/wEAAwDx/wMAAwDx/wUAAwDz//f/AwDz//n/AwDz//v/AwDz//3/AwDz////AwDz/wEAAwDz/wMAAwDz/wUAAwDz/wcAAwDz/wkAAwD1//X/AwD1//f/AwD1//n/AwD1//v/AwD1//3/AwD1////AwD1/wEAAwD1/wMAAwD1/wUAAwD1/wcAAwD1/wkAAwD1/wsAAwD3//P/AwD3//X/AwD3//f/AwD3//n/AwD3//v/AwD3//3/AwD3////AwD3/wEAAwD3/wMAAwD3/wUAAwD3/wcAAwD3/wkAAwD3/wsAAwD3/w0AAwD5//P/AwD5//X/AwD5//f/AwD5//n/AwD5//v/AwD5//3/AwD5////AwD5/wEAAwD5/wMAAwD5/wUAAwD5/wcAAwD5/wkAAwD5/wsAAwD5/w0AAwD7//H/AwD7//P/AwD7//X/AwD7//f/AwD7//n/AwD7//v/AwD7//3/AwD7////AwD7/wEAAwD7/wMAAwD7/wUAAwD7/wcAAwD7/wkAAwD7/wsAAwD7/w0AAwD7/w8AAwD9//H/AwD9//P/AwD9//X/AwD9//f/AwD9//n/AwD9//v/AwD9//3/AwD9////AwD9/wEAAwD9/wMAAwD9/wUAAwD9/wcAAwD9/wkAAwD9/wsAAwD9/w0AAwD9/w8AAwD///H/AwD///P/AwD///X/AwD///f/AwD///n/AwD///v/AwD///3/AwD/////AwD//wEAAwD//wMAAwD//wUAAwD//wcAAwD//wkAAwD//wsAAwD//w0AAwD//w8AAwABAPH/AwABAPP/AwABAPX/AwABAPf/AwABAPn/AwABAPv/AwABAP3/AwABAP//AwABAAEAAwABAAMAAwABAAUAAwABAAcAAwABAAkAAwABAAsAAwABAA0AAwABAA8AAwADAPH/AwADAPP/AwADAPX/AwADAPf/AwADAPn/AwADAPv/AwADAP3/AwADAP//AwADAAEAAwADAAMAAwADAAUAAwADAAcAAwADAAkAAwADAAsAAwADAA0AAwADAA8AAwAFAPH/AwAFAPP/AwAFAPX/AwAFAPf/AwAFAPn/AwAFAPv/AwAFAP3/AwAFAP//AwAFAAEAAwAFAAMAAwAFAAUAAwAFAAcAAwAFAAkAAwAFAAsAAwAFAA0AAwAFAA8AAwAHAPP/AwAHAPX/AwAHAPf/AwAHAPn/AwAHAPv/AwAHAP3/AwAHAP//AwAHAAEAAwAHAAMAAwAHAAUAAwAHAAcAAwAHAAkAAwAHAAsAAwAHAA0AAwAJAPP/AwAJAPX/AwAJAPf/AwAJAPn/AwAJAPv/AwAJAP3/AwAJAP//AwAJAAEAAwAJAAMAAwAJAAUAAwAJAAcAAwAJAAkAAwAJAAsAAwAJAA0AAwALAPX/AwALAPf/AwALAPn/AwALAPv/AwALAP3/AwALAP//AwALAAEAAwALAAMAAwALAAUAAwALAAcAAwALAAkAAwALAAsAAwANAPf/AwANAPn/AwANAPv/AwANAP3/AwANAP//AwANAAEAAwANAAMAAwANAAUAAwANAAcAAwANAAkAAwAPAPv/AwAPAP3/AwAPAP//AwAPAAEAAwAPAAMAAwAPAAUABADy//r/BADy//z/BADy//7/BADy/wAABADy/wIABADy/wQABADy/wYABAD0//b/BAD0//j/BAD0//r/BAD0//z/BAD0//7/BAD0/wAABAD0/wIABAD0/wQABAD0/wYABAD0/wgABAD0/woABAD2//T/BAD2//b/BAD2//j/BAD2//r/BAD2//z/BAD2//7/BAD2/wAABAD2/wIABAD2/wQABAD2/wYABAD2/wgABAD2/woABAD2/wwABAD4//T/BAD4//b/BAD4//j/BAD4//r/BAD4//z/BAD4//7/BAD4/wAABAD4/wIABAD4/wQABAD4/wYABAD4/wgABAD4/woABAD4/wwABAD6//L/BAD6//T/BAD6//b/BAD6//j/BAD6//r/BAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD6/wYABAD6/wgABAD6/woABAD6/wwABAD6/w4ABAD8//L/BAD8//T/BAD8//b/BAD8//j/BAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD8/wgABAD8/woABAD8/wwABAD8/w4ABAD+//L/BAD+//T/BAD+//b/BAD+//j/BAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD+/wgABAD+/woABAD+/wwABAD+/w4ABAAAAPL/BAAAAPT/BAAAAPb/BAAAAPj/BAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAAAAAgABAAAAAoABAAAAAwABAAAAA4ABAACAPL/BAACAPT/BAACAPb/BAACAPj/BAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAACAAgABAACAAoABAACAAwABAACAA4ABAAEAPL/BAAEAPT/BAAEAPb/BAAEAPj/BAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAEAAgABAAEAAoABAAEAAwABAAEAA4ABAAGAPL/BAAGAPT/BAAGAPb/BAAGAPj/BAAGAPr/BAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAGAAYABAAGAAgABAAGAAoABAAGAAwABAAGAA4ABAAIAPT/BAAIAPb/BAAIAPj/BAAIAPr/BAAIAPz/BAAIAP7/BAAIAAAABAAIAAIABAAIAAQABAAIAAYABAAIAAgABAAIAAoABAAIAAwABAAKAPT/BAAKAPb/BAAKAPj/BAAKAPr/BAAKAPz/BAAKAP7/BAAKAAAABAAKAAIABAAKAAQABAAKAAYABAAKAAgABAAKAAoABAAKAAwABAAMAPb/BAAMAPj/BAAMAPr/BAAMAPz/BAAMAP7/BAAMAAAABAAMAAIABAAMAAQABAAMAAYABAAMAAgABAAMAAoABAAOAPr/BAAOAPz/BAAOAP7/BAAOAAAABAAOAAIABAAOAAQABAAOAAYABQDx//3/BQDx////BQDx/wEABQDx/wMABQDz//n/BQDz//v/BQDz//3/BQDz////BQDz/wEABQDz/wMABQDz/wUABQDz/wcABQD1//f/BQD1//n/BQD1//v/BQD1//3/BQD1////BQD1/wEABQD1/wMABQD1/wUABQD1/wcABQD1/wkABQD3//X/BQD3//f/BQD3//n/BQD3//v/BQD3//3/BQD3////BQD3/wEABQD3/wMABQD3/wUABQD3/wcABQD3/wkABQD3/wsABQD5//P/BQD5//X/BQD5//f/BQD5//n/BQD5//v/BQD5//3/BQD5////BQD5/wEABQD5/wMABQD5/wUABQD5/wcABQD5/wkABQD5/wsABQD5/w0ABQD7//P/BQD7//X/BQD7//f/BQD7//n/BQD7//v/BQD7//3/BQD7////BQD7/wEABQD7/wMABQD7/wUABQD7/wcABQD7/wkABQD7/wsABQD7/w0ABQD9//H/BQD9//P/BQD9//X/BQD9//f/BQD9//n/BQD9//v/BQD9//3/BQD9////BQD9/wEABQD9/wMABQD9/wUABQD9/wcABQD9/wkABQD9/wsABQD9/w0ABQD9/w8ABQD///H/BQD///P/BQD///X/BQD///f/BQD///n/BQD///v/BQD///3/BQD/////BQD//wEABQD//wMABQD//wUABQD//wcABQD//wkABQD//wsABQD//w0ABQD//w8ABQABAPH/BQABAPP/BQABAPX/BQABAPf/BQABAPn/BQABAPv/BQABAP3/BQABAP//BQABAAEABQABAAMABQABAAUABQABAAcABQABAAkABQABAAsABQABAA0ABQABAA8ABQADAPH/BQADAPP/BQADAPX/BQADAPf/BQADAPn/BQADAPv/BQADAP3/BQADAP//BQADAAEABQADAAMABQADAAUABQADAAcABQADAAkABQADAAsABQADAA0ABQADAA8ABQAFAPP/BQAFAPX/BQAFAPf/BQAFAPn/BQAFAPv/BQAFAP3/BQAFAP//BQAFAAEABQAFAAMABQAFAAUABQAFAAcABQAFAAkABQAFAAsABQAFAA0ABQAHAPP/BQAHAPX/BQAHAPf/BQAHAPn/BQAHAPv/BQAHAP3/BQAHAP//BQAHAAEABQAHAAMABQAHAAUABQAHAAcABQAHAAkABQAHAAsABQAHAA0ABQAJAPX/BQAJAPf/BQAJAPn/BQAJAPv/BQAJAP3/BQAJAP//BQAJAAEABQAJAAMABQAJAAUABQAJAAcABQAJAAkABQAJAAsABQALAPf/BQALAPn/BQALAPv/BQALAP3/BQALAP//BQALAAEABQALAAMABQALAAUABQALAAcABQALAAkABQANAPn/BQANAPv/BQANAP3/BQANAP//BQANAAEABQANAAMABQANAAUABQANAAcABQAPAP3/BQAPAP//BQAPAAEABQAPAAMABgDy//z/BgDy//7/BgDy/wAABgDy/wIABgDy/wQABgD0//j/BgD0//r/BgD0//z/BgD0//7/BgD0/wAABgD0/wIABgD0/wQABgD0/wYABgD0/wgABgD2//b/BgD2//j/BgD2//r/BgD2//z/BgD2//7/BgD2/wAABgD2/wIABgD2/wQABgD2/wYABgD2/wgABgD2/woABgD4//T/BgD4//b/BgD4//j/BgD4//r/BgD4//z/BgD4//7/BgD4/wAABgD4/wIABgD4/wQABgD4/wYABgD4/wgABgD4/woABgD4/wwABgD6//T/BgD6//b/BgD6//j/BgD6//r/BgD6//z/BgD6//7/BgD6/wAABgD6/wIABgD6/wQABgD6/wYABgD6/wgABgD6/woABgD6/wwABgD8//L/BgD8//T/BgD8//b/BgD8//j/BgD8//r/BgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD8/wYABgD8/wgABgD8/woABgD8/wwABgD8/w4ABgD+//L/BgD+//T/BgD+//b/BgD+//j/BgD+//r/BgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD+/wYABgD+/wgABgD+/woABgD+/wwABgD+/w4ABgAAAPL/BgAAAPT/BgAAAPb/BgAAAPj/BgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgAAAAgABgAAAAoABgAAAAwABgAAAA4ABgACAPL/BgACAPT/BgACAPb/BgACAPj/BgACAPr/BgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgACAAYABgACAAgABgACAAoABgACAAwABgACAA4ABgAEAPL/BgAEAPT/BgAEAPb/BgAEAPj/BgAEAPr/BgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAEAAYABgAEAAgABgAEAAoABgAEAAwABgAEAA4ABgAGAPT/BgAGAPb/BgAGAPj/BgAGAPr/BgAGAPz/BgAGAP7/BgAGAAAABgAGAAIABgAGAAQABgAGAAYABgAGAAgABgAGAAoABgAGAAwABgAIAPT/BgAIAPb/BgAIAPj/BgAIAPr/BgAIAPz/BgAIAP7/BgAIAAAABgAIAAIABgAIAAQABgAIAAYABgAIAAgABgAIAAoABgAIAAwABgAKAPb/BgAKAPj/BgAKAPr/BgAKAPz/BgAKAP7/BgAKAAAABgAKAAIABgAKAAQABgAKAAYABgAKAAgABgAKAAoABgAMAPj/BgAMAPr/BgAMAPz/BgAMAP7/BgAMAAAABgAMAAIABgAMAAQABgAMAAYABgAMAAgABgAOAPz/BgAOAP7/BgAOAAAABgAOAAIABgAOAAQABwDz//v/BwDz//3/BwDz////BwDz/wEABwDz/wMABwDz/wUABwD1//f/BwD1//n/BwD1//v/BwD1//3/BwD1////BwD1/wEABwD1/wMABwD1/wUABwD1/wcABwD1/wkABwD3//X/BwD3//f/BwD3//n/BwD3//v/BwD3//3/BwD3////BwD3/wEABwD3/wMABwD3/wUABwD3/wcABwD3/wkABwD3/wsABwD5//X/BwD5//f/BwD5//n/BwD5//v/BwD5//3/BwD5////BwD5/wEABwD5/wMABwD5/wUABwD5/wcABwD5/wkABwD5/wsABwD7//P/BwD7//X/BwD7//f/BwD7//n/BwD7//v/BwD7//3/BwD7////BwD7/wEABwD7/wMABwD7/wUABwD7/wcABwD7/wkABwD7/wsABwD7/w0ABwD9//P/BwD9//X/BwD9//f/BwD9//n/BwD9//v/BwD9//3/BwD9////BwD9/wEABwD9/wMABwD9/wUABwD9/wcABwD9/wkABwD9/wsABwD9/w0ABwD///P/BwD///X/BwD///f/BwD///n/BwD///v/BwD///3/BwD/////BwD//wEABwD//wMABwD//wUABwD//wcABwD//wkABwD//wsABwD//w0ABwABAPP/BwABAPX/BwABAPf/BwABAPn/BwABAPv/BwABAP3/BwABAP//BwABAAEABwABAAMABwABAAUABwABAAcABwABAAkABwABAAsABwABAA0ABwADAPP/BwADAPX/BwADAPf/BwADAPn/BwADAPv/BwADAP3/BwADAP//BwADAAEABwADAAMABwADAAUABwADAAcABwADAAkABwADAAsABwADAA0ABwAFAPP/BwAFAPX/BwAFAPf/BwAFAPn/BwAFAPv/BwAFAP3/BwAFAP//BwAFAAEABwAFAAMABwAFAAUABwAFAAcABwAFAAkABwAFAAsABwAFAA0ABwAHAPX/BwAHAPf/BwAHAPn/BwAHAPv/BwAHAP3/BwAHAP//BwAHAAEABwAHAAMABwAHAAUABwAHAAcABwAHAAkABwAHAAsABwAJAPX/BwAJAPf/BwAJAPn/BwAJAPv/BwAJAP3/BwAJAP//BwAJAAEABwAJAAMABwAJAAUABwAJAAcABwAJAAkABwAJAAsABwALAPf/BwALAPn/BwALAPv/BwALAP3/BwALAP//BwALAAEABwALAAMABwALAAUABwALAAcABwALAAkABwANAPv/BwANAP3/BwANAP//BwANAAEABwANAAMABwANAAUACADy//7/CADy/wAACADy/wIACAD0//r/CAD0//z/CAD0//7/CAD0/wAACAD0/wIACAD0/wQACAD0/wYACAD2//b/CAD2//j/CAD2//r/CAD2//z/CAD2//7/CAD2/wAACAD2/wIACAD2/wQACAD2/wYACAD2/wgACAD2/woACAD4//b/CAD4//j/CAD4//r/CAD4//z/CAD4//7/CAD4/wAACAD4/wIACAD4/wQACAD4/wYACAD4/wgACAD4/woACAD6//T/CAD6//b/CAD6//j/CAD6//r/CAD6//z/CAD6//7/CAD6/wAACAD6/wIACAD6/wQACAD6/wYACAD6/wgACAD6/woACAD6/wwACAD8//T/CAD8//b/CAD8//j/CAD8//r/CAD8//z/CAD8//7/CAD8/wAACAD8/wIACAD8/wQACAD8/wYACAD8/wgACAD8/woACAD8/wwACAD+//L/CAD+//T/CAD+//b/CAD+//j/CAD+//r/CAD+//z/CAD+//7/CAD+/wAACAD+/wIACAD+/wQACAD+/wYACAD+/wgACAD+/woACAD+/wwACAD+/w4ACAAAAPL/CAAAAPT/CAAAAPb/CAAAAPj/CAAAAPr/CAAAAPz/CAAAAP7/CAAAAAAACAAAAAIACAAAAAQACAAAAAYACAAAAAgACAAAAAoACAAAAAwACAAAAA4ACAACAPL/CAACAPT/CAACAPb/CAACAPj/CAACAPr/CAACAPz/CAACAP7/CAACAAAACAACAAIACAACAAQACAACAAYACAACAAgACAACAAoACAACAAwACAACAA4ACAAEAPT/CAAEAPb/CAAEAPj/CAAEAPr/CAAEAPz/CAAEAP7/CAAEAAAACAAEAAIACAAEAAQACAAEAAYACAAEAAgACAAEAAoACAAEAAwACAAGAPT/CAAGAPb/CAAGAPj/CAAGAPr/CAAGAPz/CAAGAP7/CAAGAAAACAAGAAIACAAGAAQACAAGAAYACAAGAAgACAAGAAoACAAGAAwACAAIAPb/CAAIAPj/CAAIAPr/CAAIAPz/CAAIAP7/CAAIAAAACAAIAAIACAAIAAQACAAIAAYACAAIAAgACAAIAAoACAAKAPb/CAAKAPj/CAAKAPr/CAAKAPz/CAAKAP7/CAAKAAAACAAKAAIACAAKAAQACAAKAAYACAAKAAgACAAKAAoACAAMAPr/CAAMAPz/CAAMAP7/CAAMAAAACAAMAAIACAAMAAQACAAMAAYACAAOAP7/CAAOAAAACAAOAAIACQDz//3/CQDz////CQDz/wEACQDz/wMACQD1//n/CQD1//v/CQD1//3/CQD1////CQD1/wEACQD1/wMACQD1/wUACQD1/wcACQD3//f/CQD3//n/CQD3//v/CQD3//3/CQD3////CQD3/wEACQD3/wMACQD3/wUACQD3/wcACQD3/wkACQD5//X/CQD5//f/CQD5//n/CQD5//v/CQD5//3/CQD5////CQD5/wEACQD5/wMACQD5/wUACQD5/wcACQD5/wkACQD5/wsACQD7//X/CQD7//f/CQD7//n/CQD7//v/CQD7//3/CQD7////CQD7/wEACQD7/wMACQD7/wUACQD7/wcACQD7/wkACQD7/wsACQD9//P/CQD9//X/CQD9//f/CQD9//n/CQD9//v/CQD9//3/CQD9////CQD9/wEACQD9/wMACQD9/wUACQD9/wcACQD9/wkACQD9/wsACQD9/w0ACQD///P/CQD///X/CQD///f/CQD///n/CQD///v/CQD///3/CQD/////CQD//wEACQD//wMACQD//wUACQD//wcACQD//wkACQD//wsACQD//w0ACQABAPP/CQABAPX/CQABAPf/CQABAPn/CQABAPv/CQABAP3/CQABAP//CQABAAEACQABAAMACQABAAUACQABAAcACQABAAkACQABAAsACQABAA0ACQADAPP/CQADAPX/CQADAPf/CQADAPn/CQADAPv/CQADAP3/CQADAP//CQADAAEACQADAAMACQADAAUACQADAAcACQADAAkACQADAAsACQADAA0ACQAFAPX/CQAFAPf/CQAFAPn/CQAFAPv/CQAFAP3/CQAFAP//CQAFAAEACQAFAAMACQAFAAUACQAFAAcACQAFAAkACQAFAAsACQAHAPX/CQAHAPf/CQAHAPn/CQAHAPv/CQAHAP3/CQAHAP//CQAHAAEACQAHAAMACQAHAAUACQAHAAcACQAHAAkACQAHAAsACQAJAPf/CQAJAPn/CQAJAPv/CQAJAP3/CQAJAP//CQAJAAEACQAJAAMACQAJAAUACQAJAAcACQAJAAkACQALAPn/CQALAPv/CQALAP3/CQALAP//CQALAAEACQALAAMACQALAAUACQALAAcACQANAP3/CQANAP//CQANAAEACQANAAMACgD0//z/CgD0//7/CgD0/wAACgD0/wIACgD0/wQACgD2//j/CgD2//r/CgD2//z/CgD2//7/CgD2/wAACgD2/wIACgD2/wQACgD2/wYACgD2/wgACgD4//b/CgD4//j/CgD4//r/CgD4//z/CgD4//7/CgD4/wAACgD4/wIACgD4/wQACgD4/wYACgD4/wgACgD4/woACgD6//b/CgD6//j/CgD6//r/CgD6//z/CgD6//7/CgD6/wAACgD6/wIACgD6/wQACgD6/wYACgD6/wgACgD6/woACgD8//T/CgD8//b/CgD8//j/CgD8//r/CgD8//z/CgD8//7/CgD8/wAACgD8/wIACgD8/wQACgD8/wYACgD8/wgACgD8/woACgD8/wwACgD+//T/CgD+//b/CgD+//j/CgD+//r/CgD+//z/CgD+//7/CgD+/wAACgD+/wIACgD+/wQACgD+/wYACgD+/wgACgD+/woACgD+/wwACgAAAPT/CgAAAPb/CgAAAPj/CgAAAPr/CgAAAPz/CgAAAP7/CgAAAAAACgAAAAIACgAAAAQACgAAAAYACgAAAAgACgAAAAoACgAAAAwACgACAPT/CgACAPb/CgACAPj/CgACAPr/CgACAPz/CgACAP7/CgACAAAACgACAAIACgACAAQACgACAAYACgACAAgACgACAAoACgACAAwACgAEAPT/CgAEAPb/CgAEAPj/CgAEAPr/CgAEAPz/CgAEAP7/CgAEAAAACgAEAAIACgAEAAQACgAEAAYACgAEAAgACgAEAAoACgAEAAwACgAGAPb/CgAGAPj/CgAGAPr/CgAGAPz/CgAGAP7/CgAGAAAACgAGAAIACgAGAAQACgAGAAYACgAGAAgACgAGAAoACgAIAPb/CgAIAPj/CgAIAPr/CgAIAPz/CgAIAP7/CgAIAAAACgAIAAIACgAIAAQACgAIAAYACgAIAAgACgAIAAoACgAKAPj/CgAKAPr/CgAKAPz/CgAKAP7/CgAKAAAACgAKAAIACgAKAAQACgAKAAYACgAKAAgACgAMAPz/CgAMAP7/CgAMAAAACgAMAAIACgAMAAQACwD1//3/CwD1////CwD1/wEACwD1/wMACwD3//n/CwD3//v/CwD3//3/CwD3////CwD3/wEACwD3/wMACwD3/wUACwD3/wcACwD5//f/CwD5//n/CwD5//v/CwD5//3/CwD5////CwD5/wEACwD5/wMACwD5/wUACwD5/wcACwD5/wkACwD7//f/CwD7//n/CwD7//v/CwD7//3/CwD7////CwD7/wEACwD7/wMACwD7/wUACwD7/wcACwD7/wkACwD9//X/CwD9//f/CwD9//n/CwD9//v/CwD9//3/CwD9////CwD9/wEACwD9/wMACwD9/wUACwD9/wcACwD9/wkACwD9/wsACwD///X/CwD///f/CwD///n/CwD///v/CwD///3/CwD/////CwD//wEACwD//wMACwD//wUACwD//wcACwD//wkACwD//wsACwABAPX/CwABAPf/CwABAPn/CwABAPv/CwABAP3/CwABAP//CwABAAEACwABAAMACwABAAUACwABAAcACwABAAkACwABAAsACwADAPX/CwADAPf/CwADAPn/CwADAPv/CwADAP3/CwADAP//CwADAAEACwADAAMACwADAAUACwADAAcACwADAAkACwADAAsACwAFAPf/CwAFAPn/CwAFAPv/CwAFAP3/CwAFAP//CwAFAAEACwAFAAMACwAFAAUACwAFAAcACwAFAAkACwAHAPf/CwAHAPn/CwAHAPv/CwAHAP3/CwAHAP//CwAHAAEACwAHAAMACwAHAAUACwAHAAcACwAHAAkACwAJAPn/CwAJAPv/CwAJAP3/CwAJAP//CwAJAAEACwAJAAMACwAJAAUACwAJAAcACwALAP3/CwALAP//CwALAAEACwALAAMADAD2//z/DAD2//7/DAD2/wAADAD2/wIADAD2/wQADAD4//r/DAD4//z/DAD4//7/DAD4/wAADAD4/wIADAD4/wQADAD4/wYADAD6//j/DAD6//r/DAD6//z/DAD6//7/DAD6/wAADAD6/wIADAD6/wQADAD6/wYADAD6/wgADAD8//b/DAD8//j/DAD8//r/DAD8//z/DAD8//7/DAD8/wAADAD8/wIADAD8/wQADAD8/wYADAD8/wgADAD8/woADAD+//b/DAD+//j/DAD+//r/DAD+//z/DAD+//7/DAD+/wAADAD+/wIADAD+/wQADAD+/wYADAD+/wgADAD+/woADAAAAPb/DAAAAPj/DAAAAPr/DAAAAPz/DAAAAP7/DAAAAAAADAAAAAIADAAAAAQADAAAAAYADAAAAAgADAAAAAoADAACAPb/DAACAPj/DAACAPr/DAACAPz/DAACAP7/DAACAAAADAACAAIADAACAAQADAACAAYADAACAAgADAACAAoADAAEAPb/DAAEAPj/DAAEAPr/DAAEAPz/DAAEAP7/DAAEAAAADAAEAAIADAAEAAQADAAEAAYADAAEAAgADAAEAAoADAAGAPj/DAAGAPr/DAAGAPz/DAAGAP7/DAAGAAAADAAGAAIADAAGAAQADAAGAAYADAAGAAgADAAIAPr/DAAIAPz/DAAIAP7/DAAIAAAADAAIAAIADAAIAAQADAAIAAYADAAKAPz/DAAKAP7/DAAKAAAADAAKAAIADAAKAAQADQD3//3/DQD3////DQD3/wEADQD3/wMADQD5//v/DQD5//3/DQD5////DQD5/wEADQD5/wMADQD5/wUADQD7//n/DQD7//v/DQD7//3/DQD7////DQD7/wEADQD7/wMADQD7/wUADQD7/wcADQD9//f/DQD9//n/DQD9//v/DQD9//3/DQD9////DQD9/wEADQD9/wMADQD9/wUADQD9/wcADQD9/wkADQD///f/DQD///n/DQD///v/DQD///3/DQD/////DQD//wEADQD//wMADQD//wUADQD//wcADQD//wkADQABAPf/DQABAPn/DQABAPv/DQABAP3/DQABAP//DQABAAEADQABAAMADQABAAUADQABAAcADQABAAkADQADAPf/DQADAPn/DQADAPv/DQADAP3/DQADAP//DQADAAEADQADAAMADQADAAUADQADAAcADQADAAkADQAFAPn/DQAFAPv/DQAFAP3/DQAFAP//DQAFAAEADQAFAAMADQAFAAUADQAFAAcADQAHAPv/DQAHAP3/DQAHAP//DQAHAAEADQAHAAMADQAHAAUADQAJAP3/DQAJAP//DQAJAAEADQAJAAMADgD4//7/DgD4/wAADgD4/wIADgD6//z/DgD6//7/DgD6/wAADgD6/wIADgD6/wQADgD8//r/DgD8//z/DgD8//7/DgD8/wAADgD8/wIADgD8/wQADgD8/wYADgD+//j/DgD+//r/DgD+//z/DgD+//7/DgD+/wAADgD+/wIADgD+/wQADgD+/wYADgD+/wgADgAAAPj/DgAAAPr/DgAAAPz/DgAAAP7/DgAAAAAADgAAAAIADgAAAAQADgAAAAYADgAAAAgADgACAPj/DgACAPr/DgACAPz/DgACAP7/DgACAAAADgACAAIADgACAAQADgACAAYADgACAAgADgAEAPr/DgAEAPz/DgAEAP7/DgAEAAAADgAEAAIADgAEAAQADgAEAAYADgAGAPz/DgAGAP7/DgAGAAAADgAGAAIADgAGAAQADgAIAP7/DgAIAAAADgAIAAIADwD7//3/DwD7////DwD7/wEADwD7/wMADwD9//v/DwD9//3/DwD9////DwD9/wEADwD9/wMADwD9/wUADwD///v/DwD///3/DwD/////DwD//wEADwD//wMADwD//wUADwABAPv/DwABAP3/DwABAP//DwABAAEADwABAAMADwABAAUADwADAPv/DwADAP3/DwADAP//DwADAAEADwADAAMADwADAAUADwAFAP3/DwAFAP//DwAFAAEADwAFAAMAEAD+//7/EAD+/wAAEAD+/wIAEAAAAP7/EAAAAAAAEAAAAAIAEAACAP7/EAACAAAAEAACAAIA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"T9kAOdl4viJP2QA52Xi+IvhHBTm0dsUiT9kAObR2xSJP2QA53BpvuoLmjTrLw3m6kQCIOtwab7o/LJQ6EaCIuk3amjoPkIK6kQCIOoLmjToRoIi6MPqhOg4bj7pN2po6y8N5usvDebpN2po6DhuPujD6oToRoIi6guaNOpEAiDoPkIK6TdqaOhGgiLo/LJQ63BpvupEAiDrLw3m6guaNOtwab7pP2QA5mxknI0/ZADm+5Qk5JzcaI/O5EzkXuGoivuUJOb7lCTmOxT4iCG8eOQM1ISMIbx458lGWIr7lCTlP2QA5JzcaIwhvHjlV5voi/yIqOXcXLiMIbx45qguBIk/ZADmbGScj87kTOQM1ISP/Iio5gyDkIv8iKjm+fa4i87kTOastOSJP2QA5rYmHIghvHjnENxgj/yIqObYUtyIIbx45VKIKIk/ZADm+5Qk5JrtgIghvHjm+fa4iCG8eOfFxgSK+5Qk5vuUJOdujhSLzuRM5VKIKIr7lCTlP2QA5qy05Ik/ZADmRAIg6y8N5uoLmjTrcGm+6PyyUOg4bj7qXlqk6QguWujD6oToPkIK6PyyUOkILlrrMdbo6xRWuumXWwzpDeqW6l5apOg+QgrqRAIg6DhuPusx1ujrfXre68tDYOv5owboS7s06Q3qlujD6oTrcGm+6y8N5upeWqTrFFa668tDYOpFKzLo9luQ6/mjBumXWwzpCC5a6guaNOoLmjTpCC5a6ZdbDOv5owbo9luQ6kUrMuvLQ2DrFFa66l5apOsvDebrcGm+6MPqhOkN6pboS7s06/mjBuvLQ2DrfXre6zHW6Og4bj7qRAIg6D5CCupeWqTpDeqW6ZdbDOsUVrrrMdbo6QguWuj8slDoPkIK6MPqhOkILlrqXlqk6DhuPuj8slDrcGm+6guaNOsvDebqRAIg6joMbI77lCTmpj0cjvuUJOXzxAiPXmCkjpPYYOTHT5SIRJyQ5PHhAI6T2GDn73MUi15gpIwhvHjkATP0iAPo2OUqKBSMA+jY5ZodiIghvHjlXIJEijoMbI6T2GDkATP0iJuA9OdONTSN9wkw5sglbIybgPTnLAp4ipPYYOb3wWCK+5Qk5MdPlIgD6NjloMYMjXc5UOQQVNiNdzlQ5cDsMIwD6NjnVGqMivuUJOamPRyMRJyQ5SooFI33CTDkEFTYjsU1dOUVuDSN9wkw5U+StIhEnJDkOCwMivuUJOcvRFiMA+jY5sglbI13OVDlFbg0jXc5UOVki3iIA+jY5mMwnIr7lCTnmH+YipPYYORuyqiIm4D05Khn8In3CTDlZIt4iJuA9OYGCciKk9hg5V4TsIfDkqyIIbx45Rn+pIgD6NjlT5K0iAPo2OYuJWyIIbx45h9gAIuDQmSKk9hg50zqOIhEnJDlgaR0ipPYYOQeTvCHOs3givuUJOQ4LAyK+5Qk5tTQioYLmjToPkIK6PyyUOsvDebrLw3m6MPqhOmt7nbrMdbo6Q3qlurW6sToOG4+6guaNOsvDebqXlqk6xRWuuvLQ2DqRSsy6PZbkOv5owbpl1sM6QguWuoLmjTow+qE6xRWuuj2W5Dq4AOW64SwHO8oX87pWOP86kUrMumXWwzoOG4+6guaNOmt7nbry0Ng6uADluqp0Dzsjywm7II4YO+BGAbtWOP86/mjBurW6sTrLw3m6D5CCusx1ujqRSsy64SwHOyPLCbuqmSI7NTgTuyCOGDvKF/O6PZbkOkN6pbo/LJQ6PyyUOkN6pbo9luQ6yhfzuiCOGDs1OBO7qpkiOyPLCbvhLAc7kUrMusx1ujoPkIK6y8N5urW6sTr+aMG6Vjj/OuBGAbsgjhg7I8sJu6p0Dzu4AOW68tDYOmt7nbqC5o06DhuPumXWwzqRSsy6Vjj/OsoX87rhLAc7uADluj2W5DrFFa66MPqhOoLmjTpCC5a6ZdbDOv5owbo9luQ6kUrMuvLQ2DrFFa66l5apOsvDebqC5o06DhuPurW6sTpDeqW6zHW6Omt7nbow+qE6y8N5usvDebo/LJQ6D5CCuoLmjTqOgxsjvuUJOamPRyO+5Qk5fPECI0/ZADmQIQ0jCG8eOQ8NUCP/Iio5YHq5IghvHjk6h48iT9kAOU/ZADmErTkj/yIqOSWIDCOKIEU5nJpNI4ogRTm+Owsj/yIqOf864SJP2QA5kCENI/8iKjkAt5ojXc5UOb0BdSNnS2Y5MMJJI13OVDl5xrsi/yIqOf4HDSKOgxsjCG8eOe9ociNdzlQ5m1vKI6z3eTmPlDcjrPd5OdvlKCNdzlQ5cRuhIghvHjmhJtAhvuUJOQ8NUCOKIEU5vQF1I6z3eTkmqkQjFy2IOSWIDCOs93k587VxIoogRTmrgPghvuUJOamPRyP/Iio5nJpNI2dLZjmPlDcjFy2IOX8+AiIXLYg5zy8yomdLZjm3oQyi/yIqOei916C+5Qk5xjnlIoogRTnvBBcjrPd5OZ7q7CIXLYg5OU15oKz3eTl490KhiiBFOY4YfqG+5Qk55h/mIghvHjkEV1mhXc5UOaPEwSKs93k5zy8yoqz3eTkDNaGiXc5UOSc3GqIIbx45C+C7ojqHjyL/Iio52TabIl3OVDlAxT+gZ0tmOe1mpaFdzlQ5nRv0of8iKjm3oYyiT9kAOefuXiL/Iio5GVZcIoogRTm3oQyiiiBFOQhitaH/Iio5FRp5ok/ZADlP2QA5QWMxIghvHjmOGP6h/yIqOaqx/x4Ibx45VXGDok/ZADlsMLAhvuUJOei916C+5Qk5zrN4opEAiDrLw3m6guaNOtwab7rLw3m6MPqhOmt7nbrMdbo6Q3qlurW6sToOG4+6guaNOg+Qgrq1urE63163uj2W5DqkHdi631jxOpFKzLoS7s06a3uduj8slDrLw3m6tbqxOv5owbpWOP864EYBuyCOGDsjywm7qnQPO7gA5bry0Ng6a3uduoLmjTow+qE63163ulY4/zojywm7W78tO+hyKbuYMTo7GrUduyCOGDu4AOW6Eu7NOg4bj7qRAIg6a3uduj2W5DrgRgG7W78tO6iwNrsbElg7BsFFu/AwSDsatR27qnQPO5FKzLq1urE63BpvusvDebrMdbo6pB3YuiCOGDvocim7GxJYO3wTV7sqR2o7BsFFu5gxOjsjywm731jxOkN6pbqC5o06guaNOkN6pbrfWPE6I8sJu5gxOjsGwUW7KkdqO3wTV7sbElg76HIpuyCOGDukHdi6zHW6OsvDebrcGm+6tbqxOpFKzLqqdA87GrUdu/AwSDsGwUW7GxJYO6iwNrtbvy074EYBuz2W5Dpre526kQCIOg4bj7oS7s06uADluiCOGDsatR27mDE6O+hyKbtbvy07I8sJu1Y4/zrfXre6MPqhOoLmjTpre5268tDYOrgA5bqqdA87I8sJuyCOGDvgRgG7Vjj/Ov5owbq1urE6y8N5uj8slDpre526Eu7NOpFKzLrfWPE6pB3Yuj2W5DrfXre6tbqxOg+QgrqC5o06DhuPurW6sTpDeqW6zHW6Omt7nbow+qE6y8N5utwab7qC5o06y8N5upEAiDpP2QA5mxknI0/ZADnXmCkjpPYYOTHT5SIRJyQ5tVbaIqT2GDn73MUiT9kAOYStOSP/Iio5JYgMI4ogRTmcmk0jiiBFOb47CyP/Iio5/zrhIk/ZADkco9sikGcwOZh+VSOxTV05uNRgI2LUbzmorzIjsU1dOc6zeCKQZzA5Khn8IdeYKSP/Iio5mH5VI2dLZjlZP6kjFy2IOSWIDCMXLYg5pyLdImdLZjknilki/yIqOTlN+Z+k9hg5JYgMI7FNXTlZP6kjbWWOORlWXCNVb5w52Zp2I21ljjl5xrsisU1dOS2GZiKk9hg5T9kAOTHT5SKKIEU5uNRgIxctiDkZVlwjuHekOUDFvyK4d6Q5zj6BIhctiDm3oYyhiiBFOZTBSyFP2QA5mxknIxEnJDmcmk0jYtRvOSWIDCNVb5w5QMW/IsdkrTk3OhAjVW+cOcygjyJi1G85ylVdIREnJDmrLTmiT9kAOdQJhyKKIEU5qK8yIxctiDlDEj8juHekOTc6ECO4d6Q5vr/RIhctiDmUkEQiiiBFOcNoH6JP2QA5pPYYOfaeFSKxTV05BfVKI21ljjlvbBMiVW+cOSr3wyJtZY45RW4NIrFNXTl490KipPYYOfvcxSL/Iio5vDL8ImdLZjk42AEjFy2IOcygjyIXLYg5TFtLImdLZjnd53Wi/yIqObRlKaKjcYIikGcwOcrxgSKxTV05mJsgomLUbzmYmyAisU1dOcrxgaKQZzA5o3GCok/ZADlgvFwi/yIqOei91yCKIEU5ylVdIYogRTlxG6Gi/yIqOXBMqKJP2QA5DG2RIKT2GDnuNZ4gESckOaS1cqKk9hg59DGrok/ZADmrLTmiT9kAOT8slDoOG4+6l5apOkILlrow+qE6D5CCusvDebqXlqk6xRWuuvLQ2DqRSsy6PZbkOv5owbpl1sM6QguWuoLmjTrLw3m6tbqxOv5owbpWOP864EYBuyCOGDsjywm7qnQPO7gA5bry0Ng6a3uduoLmjTqXlqk6/mjBuuEsBzs1OBO7mDE6O6iwNrvwMEg76HIpu6qZIjvKF/O68tDYOkILlro/LJQ6xRWuulY4/zo1OBO78DBIO3wTV7urbn87GUNruypHajuosDa7qpkiO7gA5bpl1sM6D5CCug4bj7ry0Ng64EYBu5gxOjt8E1e7xTaMO0AWkLveTps77peBuypHajvocim7qnQPO/5owbow+qE6l5apOpFKzLogjhg7qLA2u6tufztAFpC7OBSuO2crorveTps7GUNru/AwSDsjywm7PZbkOkILlrpCC5a6PZbkOiPLCbvwMEg7GUNru95OmztnK6K7OBSuO0AWkLurbn87qLA2uyCOGDuRSsy6l5apOjD6oTr+aMG6qnQPO+hyKbsqR2o77peBu95OmztAFpC7xTaMO3wTV7uYMTo74EYBu/LQ2DoOG4+6D5CCumXWwzq4AOW6qpkiO6iwNrsqR2o7GUNru6tufzt8E1e78DBIOzU4E7tWOP86xRWuuj8slDpCC5a68tDYOsoX87qqmSI76HIpu/AwSDuosDa7mDE6OzU4E7vhLAc7/mjBupeWqTqC5o06a3uduvLQ2Dq4AOW6qnQPOyPLCbsgjhg74EYBu1Y4/zr+aMG6tbqxOsvDebqC5o06QguWumXWwzr+aMG6PZbkOpFKzLry0Ng6xRWuupeWqTrLw3m6D5CCujD6oTpCC5a6l5apOg4bj7o/LJQ6vuUJOSc3GiPzuRM5F7hqIr7lCTnXmCkjCG8eOQBM/SIA+jY5SooFIwD6Njlmh2IiCG8eOVcgkSKQIQ0j/yIqOQC3miNdzlQ5vQF1I2dLZjkwwkkjXc5UOXnGuyL/Iio5/gcNIteYKSP/Iio5mH5VI2dLZjlZP6kjFy2IOSWIDCMXLYg5pyLdImdLZjknilki/yIqOTlN+Z8Ibx45ALeaI2dLZjlHfIkjcx6VOezIMyO4d6Q50zqOI3MelTkRvN0iZ0tmOe1mJSEIbx45vuUJOQBM/SJdzlQ5WT+pI3MelTlStsYjoHq3OUNxrSOgerc5EViCI3MelTnKM6UiXc5UOdsnzCG+5Qk5JzcaIwD6Njm9AXUjFy2IOezIMyOgerc5u16WIxgr0TmqoGMjoHq3OZf9riIXLYg50P4qogD6Njkijswf87kTOUqKBSNnS2Y5JYgMI7h3pDlDca0jGCvROVWTuyMYK9E5rmkcI7h3pDmsT/GhZ0tmOQHqbiHzuRM5F7hqIgD6Njm7/ycjFy2IOWBpnSOgerc5qqBjIxgr0TlYvgIjoHq3OZCFaKIXLYg5XF7BIQD6NjmUbgyivuUJOWaHYiJdzlQ5ZbhpI3MelTnqKkIjoHq3Oa5pHCOgerc5lMFLonMelTnvBJchXc5UOU+GkqK+5Qk5CG8eOZOfEyNnS2Y58lEWI3MelTlxCgUjuHekOZCFaKJzHpU5VrXzIWdLZjkF9UqiCG8eOcpVXSL/Iio5XAuCImdLZjnr+ToiFy2IOaxP8aEXLYg5+F4/omdLZjlAxT+i/yIqOak8CKIr6HQi/yIqOYolgCJdzlQ5HlLpIWdLZjmyxzchXc5UOQX1SqL/Iio57MizogkxLiIIbx45xvdBIgD6NjkB6m4hAPo2OU+GkqIIbx45/9aFob7lCTkijsyh87kTObPpb6K+5Qk53BpvuoLmjTrLw3m6kQCIOj8slDpCC5a6zHW6OsUVrrpl1sM6Q3qlupeWqToPkIK6MPqhOsUVrro9luQ6uADluuEsBzvKF/O6Vjj/OpFKzLpl1sM6DhuPujD6oTrfXre6Vjj/OiPLCbtbvy076HIpu5gxOjsatR27II4YO7gA5boS7s06DhuPuj8slDrFFa66Vjj/OjU4E7vwMEg7fBNXu6tufzsZQ2u7KkdqO6iwNruqmSI7uADlumXWwzoPkIK6QguWuj2W5Dojywm78DBIOxlDa7veTps7ZyuiuzgUrjtAFpC7q25/O6iwNrsgjhg7kUrMupeWqTrcGm+6zHW6OrgA5bpbvy07fBNXu95OmztJqLm79rDnO3n12btfYcY7QBaQuypHajsatR27Vjj/OkN6pbqRAIg6guaNOsUVrrrhLAc76HIpu6tufztnK6K79rDnO9YhBbwjrQw8efXZuzgUrjsZQ2u7mDE6O8oX87pl1sM6y8N5usvDebpl1sM6yhfzupgxOjsZQ2u7OBSuO3n12bsjrQw81iEFvPaw5ztnK6K7q25/O+hyKbvhLAc7xRWuuoLmjTqRAIg6Q3qlulY4/zoatR27KkdqO0AWkLtfYcY7efXZu/aw5ztJqLm73k6bO3wTV7tbvy07uADlusx1ujrcGm+6l5apOpFKzLogjhg7qLA2u6tufztAFpC7OBSuO2crorveTps7GUNru/AwSDsjywm7PZbkOkILlroPkIK6ZdbDOrgA5bqqmSI7qLA2uypHajsZQ2u7q25/O3wTV7vwMEg7NTgTu1Y4/zrFFa66PyyUOg4bj7oS7s06uADluiCOGDsatR27mDE6O+hyKbtbvy07I8sJu1Y4/zrfXre6MPqhOg4bj7pl1sM6kUrMulY4/zrKF/O64SwHO7gA5bo9luQ6xRWuujD6oToPkIK6l5apOkN6pbpl1sM6xRWuusx1ujpCC5a6PyyUOpEAiDrLw3m6guaNOtwab7q+5Qk5jsU+IghvHjkDNSEjCG8eOfJRliK+5Qk5joMbI6T2GDkATP0iJuA9OdONTSN9wkw5sglbIybgPTnLAp4ipPYYOb3wWCKOgxsjCG8eOe9ociNdzlQ5m1vKI6z3eTmPlDcjrPd5OdvlKCNdzlQ5cRuhIghvHjmhJtAhpPYYOSWIDCOxTV05WT+pI21ljjkZVlwjVW+cOdmadiNtZY45eca7IrFNXTkthmYipPYYOb7lCTkATP0iXc5UOVk/qSNzHpU5UrbGI6B6tzl2SDUjoHq3ORzl/iJzHpU5Io7MHl3OVDlDASMgvuUJOY7FPiIm4D05m1vKI21ljjmIF4QjHSfDOTn6OSNGAOM5CGI1Ih0nwznUCYcibWWOOUMBo6Im4D05X5qkIQhvHjnTjU0jrPd5ORlWXCOgerc5Ofo5I9f9+zli1gck1/37OTMvNCOgerc5yOjyoKz3eTlxCoUhCG8eOQM1ISN9wkw5j5Q3I1VvnDl2SDUjRgDjOWLWByRJwRI65f0tI0YA4zlCMiqiVW+cOfheP6J9wkw5umE2oghvHjmyCVsjrPd5OUMSPyOgerc5vL2EI9f9+znl/S0j1/37OQK556Ggerc5b2wToqz3eTkJAKeiCG8eOSa7YCIm4D053/AEI21ljjklyi8hHSfDOXfVCiNGAOM5d9UKox0nwzklyi+hbWWOOd/wBKMm4D05Jrtgor7lCTnB+7QiXc5UObH4PiJzHpU5O+vqIqB6tzlCMiqioHq3Odk2m6JzHpU5BfVKo13OVDldgHmivuUJOaT2GDl490IisU1dOUVuDaJtZY45KvfDolVvnDlvbBOibWWOOQX1SqOxTV059p4VoqT2GDmrLTkiCG8eOTlNeaFdzlQ5dbtfoaz3eTn4Xj+irPd5Od/wBKNdzlQ59p4VoghvHjl88QKjt6EMIKT2GDlcXsGgJuA9OdKcHCB9wkw5CQCnoibgPTldgHmipPYYOXzxAqO+5Qk5EovWoQhvHjm6YTaiCG8eOSa7YKK+5Qk53Bpvuj8slDoRoIi6TdqaOg+QgrqRAIg6kQCIOg4bj7rMdbo63163uvLQ2Dr+aMG6Eu7NOkN6pbow+qE63BpvuoLmjTpre5268tDYOrgA5bqqdA87I8sJuyCOGDvgRgG7Vjj/Ov5owbq1urE6y8N5upEAiDpre526PZbkOuBGAbtbvy07qLA2uxsSWDsGwUW78DBIOxq1HbuqdA87kUrMurW6sTrcGm+6DhuPuvLQ2DrgRgG7mDE6O3wTV7vFNow7QBaQu95Omzvul4G7KkdqO+hyKbuqdA87/mjBujD6oTrcGm+6zHW6OrgA5bpbvy07fBNXu95OmztJqLm79rDnO3n12btfYcY7QBaQuypHajsatR27Vjj/OkN6pbqRAIg6PyyUOt9et7qqdA87qLA2u8U2jDtJqLm7I60MPPY0Lrx/yzY81iEFvF9hxjvul4G78DBIO+BGAbsS7s06D5CCuhGgiLry0Ng6I8sJuxsSWDtAFpC79rDnO/Y0Lryr/Yc8Z6mCvH/LNjx59dm73k6bOwbBRbsgjhg7/mjBuk3amjpN2po6/mjBuiCOGDsGwUW73k6bO3n12bt/yzY8Z6mCvKv9hzz2NC689rDnO0AWkLsbElg7I8sJu/LQ2DoRoIi6D5CCuhLuzTrgRgG78DBIO+6XgbtfYcY71iEFvH/LNjz2NC68I60MPEmoubvFNow7qLA2u6p0DzvfXre6PyyUOpEAiDpDeqW6Vjj/Ohq1HbsqR2o7QBaQu19hxjt59dm79rDnO0moubveTps7fBNXu1u/LTu4AOW6zHW6Otwab7ow+qE6/mjBuqp0Dzvocim7KkdqO+6XgbveTps7QBaQu8U2jDt8E1e7mDE6O+BGAbvy0Ng6DhuPutwab7q1urE6kUrMuqp0DzsatR278DBIOwbBRbsbElg7qLA2u1u/LTvgRgG7PZbkOmt7nbqRAIg6y8N5urW6sTr+aMG6Vjj/OuBGAbsgjhg7I8sJu6p0Dzu4AOW68tDYOmt7nbqC5o063BpvujD6oTpDeqW6Eu7NOv5owbry0Ng63163usx1ujoOG4+6kQCIOpEAiDoPkIK6TdqaOhGgiLo/LJQ63Bpvuk/ZADnZeL4iT9kAOU/ZADknNxojCG8eOVXm+iL/Iio5dxcuIwhvHjmqC4EiT9kAOb7lCTkx0+UiAPo2OWgxgyNdzlQ5BBU2I13OVDlwOwwjAPo2OdUaoyK+5Qk5vuUJOQ8NUCOKIEU5vQF1I6z3eTkmqkQjFy2IOWjSlCKs93k5cDsMIoogRTl3KEqhvuUJOU/ZADkx0+UiiiBFObjUYCMXLYg5GVZcI7h3pDlAxb8iuHekOc4+gSIXLYg5t6GMoYogRTmUwUshT9kAOSc3GiMA+jY5vQF1IxctiDnsyDMjoHq3ObteliMYK9E5qqBjI6B6tzmX/a4iFy2IOdD+KqIA+jY5Io7MHwhvHjnTjU0jrPd5ORlWXCOgerc5Ofo5I9f9+zli1gck1/37OTMvNCOgerc5yOjyoKz3eTlxCoUhCG8eOU/ZADlV5voiXc5UOSaqRCO4d6Q5u16WI9f9+zkSOBckG9JCOrH4PiPX/fs5l1Duorh3pDkijkyiXc5UObYUt6JP2QA52Xi+Iv8iKjkEFTYjFy2IOUDFvyIYK9E5YtYHJBvSQjrYuuEjG9JCOuX9LaMYK9E5NzoQoxctiDnWTXei/yIqObR2xaJP2QA5xDcYI13OVDlqgSIjuHekOeh7NCPX/fs5fFVeIxvSQjp8VV6j1/37Oeh7NKO4d6Q5aoEio13OVDnENxijT9kAOQhvHjkJAKcirPd5OW9sEyKgerc5ArnnIdf9+znl/S2j1/37Oby9hKOgerc5QxI/o6z3eTmyCVujCG8eOXTsZiIA+jY5mjmSIhctiDmgiN4ioHq3OV38siEYK9E56Hs0o6B6tznsl6yjFy2IOfbgOKMA+jY5rYmHok/ZADnDaB8iiiBFOZSQRKIXLYg5vr/Rorh3pDk3OhCjuHekOUMSP6MXLYg5qK8yo4ogRTnUCYeiT9kAOb7lCTnuNZ4giiBFOT/2xiGs93k5QMW/nxctiDme6uyirPd5OfbgOKOKIEU5lnwIo77lCTm+5Qk5mWqZngD6Njl1aCCiXc5UOdZNd6JdzlQ5fkEiowD6NjnL0RajvuUJOU/ZADlUogqiCG8eObYUt6L/Iio5xDcYowhvHjmtiYeiT9kAOU/ZADm0dsWiT9kAOYLmjToRoIi6MPqhOg4bj7pN2po6y8N5usvDebqXlqk6xRWuuvLQ2DqRSsy6PZbkOv5owbpl1sM6QguWuoLmjToPkIK6zHW6OpFKzLrhLAc7I8sJu6qZIjs1OBO7II4YO8oX87o9luQ6Q3qluj8slDrLw3m6zHW6OqQd2Logjhg76HIpuxsSWDt8E1e7KkdqOwbBRbuYMTo7I8sJu99Y8TpDeqW6guaNOpeWqTqRSsy6II4YO6iwNrurbn87QBaQuzgUrjtnK6K73k6bOxlDa7vwMEg7I8sJuz2W5DpCC5a6guaNOsUVrrrhLAc76HIpu6tufztnK6K79rDnO9YhBbwjrQw8efXZuzgUrjsZQ2u7mDE6O8oX87pl1sM6y8N5uhGgiLry0Ng6I8sJuxsSWDtAFpC79rDnO/Y0Lryr/Yc8Z6mCvH/LNjx59dm73k6bOwbBRbsgjhg7/mjBuk3amjow+qE6kUrMuqqZIjt8E1e7OBSuO9YhBbyr/Yc8xpwQvS4rFT1nqYK8I60MPGcrorsqR2o7NTgTuz2W5DoOG4+6DhuPuj2W5Do1OBO7KkdqO2crorsjrQw8Z6mCvC4rFT3GnBC9q/2HPNYhBbw4FK47fBNXu6qZIjuRSsy6MPqhOk3amjr+aMG6II4YOwbBRbveTps7efXZu3/LNjxnqYK8q/2HPPY0Lrz2sOc7QBaQuxsSWDsjywm78tDYOhGgiLrLw3m6ZdbDOsoX87qYMTo7GUNruzgUrjt59dm7I60MPNYhBbz2sOc7Zyuiu6tufzvocim74SwHO8UVrrqC5o06QguWuj2W5Dojywm78DBIOxlDa7veTps7ZyuiuzgUrjtAFpC7q25/O6iwNrsgjhg7kUrMupeWqTqC5o06Q3qlut9Y8Tojywm7mDE6OwbBRbsqR2o7fBNXuxsSWDvocim7II4YO6Qd2LrMdbo6y8N5uj8slDpDeqW6PZbkOsoX87ogjhg7NTgTu6qZIjsjywm74SwHO5FKzLrMdbo6D5CCuoLmjTpCC5a6ZdbDOv5owbo9luQ6kUrMuvLQ2DrFFa66l5apOsvDebrLw3m6TdqaOg4bj7ow+qE6EaCIuoLmjTrZeL4i+EcFObR2xSKbGScj87kTOQM1ISP/Iio5gyDkIv8iKjm+fa4i87kTOastOSKpj0cjESckOUqKBSN9wkw5BBU2I7FNXTlFbg0jfcJMOVPkrSIRJyQ5DgsDIqmPRyP/Iio5nJpNI2dLZjmPlDcjFy2IOX8+AiIXLYg5zy8yomdLZjm3oQyi/yIqOei916CbGScjESckOZyaTSNi1G85JYgMI1VvnDlAxb8ix2StOTc6ECNVb5w5zKCPImLUbznKVV0hESckOastOaLzuRM5SooFI2dLZjkliAwjuHekOUNxrSMYK9E5VZO7Ixgr0TmuaRwjuHekOaxP8aFnS2Y5AepuIfO5EzkDNSEjfcJMOY+UNyNVb5w5dkg1I0YA4zli1gckScESOuX9LSNGAOM5QjIqolVvnDn4Xj+ifcJMObphNqLZeL4i/yIqOQQVNiMXLYg5QMW/Ihgr0Tli1gckG9JCOti64SMb0kI65f0toxgr0Tk3OhCjFy2IOdZNd6L/Iio5tHbFovhHBTmDIOQisU1dOX8+AiLHZK05VZO7I0nBEjrYuuEj2Lrho0nBEjpVk7ujx2StOX8+AqKxTV05gyDkovhHBTm0dsUi/yIqOdZNdyIXLYg5NzoQIxgr0Tnl/S0jG9JCOti64aMb0kI6YtYHpBgr0TlAxb+iFy2IOQQVNqP/Iio52Xi+orphNiJ9wkw5+F4/IlVvnDlCMioiRgDjOeX9LaNJwRI6YtYHpEYA4zl2SDWjVW+cOY+UN6N9wkw5AzUho/O5EzkB6m6hZ0tmOaxP8SG4d6Q5rmkcoxgr0TlVk7ujGCvROUNxraO4d6Q5JYgMo2dLZjlKigWj87kTOastOSIRJyQ5ylVdoWLUbznMoI+iVW+cOTc6EKPHZK05QMW/olVvnDkliAyjYtRvOZyaTaMRJyQ5mxkno+i91yD/Iio5t6EMImdLZjnPLzIiFy2IOX8+AqIXLYg5j5Q3o2dLZjmcmk2j/yIqOamPR6MOCwOiESckOVPkraJ9wkw5RW4No7FNXTkEFTajfcJMOUqKBaMRJyQ5qY9Ho6stOaLzuRM5vn2uov8iKjmDIOSi/yIqOQM1IaPzuRM5mxkno7R2xaL4RwU52Xi+osvDebpN2po6DhuPujD6oToRoIi6guaNOoLmjTpCC5a6ZdbDOv5owbo9luQ6kUrMuvLQ2DrFFa66l5apOsvDebo/LJQ6Q3qluj2W5DrKF/O6II4YOzU4E7uqmSI7I8sJu+EsBzuRSsy6zHW6Og+QgrqC5o06Q3qlut9Y8Tojywm7mDE6OwbBRbsqR2o7fBNXuxsSWDvocim7II4YO6Qd2LrMdbo6y8N5ukILlro9luQ6I8sJu/AwSDsZQ2u73k6bO2crors4FK47QBaQu6tufzuosDa7II4YO5FKzLqXlqk6y8N5umXWwzrKF/O6mDE6OxlDa7s4FK47efXZuyOtDDzWIQW89rDnO2crorurbn876HIpu+EsBzvFFa66guaNOk3amjr+aMG6II4YOwbBRbveTps7efXZu3/LNjxnqYK8q/2HPPY0Lrz2sOc7QBaQuxsSWDsjywm78tDYOhGgiLoOG4+6PZbkOjU4E7sqR2o7ZyuiuyOtDDxnqYK8LisVPcacEL2r/Yc81iEFvDgUrjt8E1e7qpkiO5FKzLow+qE6MPqhOpFKzLqqmSI7fBNXuzgUrjvWIQW8q/2HPMacEL0uKxU9Z6mCvCOtDDxnK6K7KkdqOzU4E7s9luQ6DhuPuhGgiLry0Ng6I8sJuxsSWDtAFpC79rDnO/Y0Lryr/Yc8Z6mCvH/LNjx59dm73k6bOwbBRbsgjhg7/mjBuk3amjqC5o06xRWuuuEsBzvocim7q25/O2crorv2sOc71iEFvCOtDDx59dm7OBSuOxlDa7uYMTo7yhfzumXWwzrLw3m6l5apOpFKzLogjhg7qLA2u6tufztAFpC7OBSuO2crorveTps7GUNru/AwSDsjywm7PZbkOkILlrrLw3m6zHW6OqQd2Logjhg76HIpuxsSWDt8E1e7KkdqOwbBRbuYMTo7I8sJu99Y8TpDeqW6guaNOg+QgrrMdbo6kUrMuuEsBzsjywm7qpkiOzU4E7sgjhg7yhfzuj2W5DpDeqW6PyyUOsvDebqXlqk6xRWuuvLQ2DqRSsy6PZbkOv5owbpl1sM6QguWuoLmjTqC5o06EaCIujD6oToOG4+6TdqaOsvDebpP2QA5tHbFIk/ZADlP2QA5rYmHIghvHjnENxgj/yIqObYUtyIIbx45VKIKIk/ZADm+5Qk5y9EWIwD6Njl+QSIjXc5UOdZNdyJdzlQ5dWggIgD6NjmZapkevuUJOb7lCTmWfAgjiiBFOfbgOCOs93k5nursIhctiDlAxb8frPd5OT/2xqGKIEU57jWeoL7lCTlP2QA51AmHIoogRTmorzIjFy2IOUMSPyO4d6Q5NzoQI7h3pDm+v9EiFy2IOZSQRCKKIEU5w2gfok/ZADmtiYciAPo2OfbgOCMXLYg57JesI6B6tznoezQjGCvROV38sqGgerc5oIjeohctiDmaOZKiAPo2OXTsZqIIbx45sglbI6z3eTlDEj8joHq3Oby9hCPX/fs55f0tI9f9+zkCueehoHq3OW9sE6Ks93k5CQCnoghvHjlP2QA5xDcYI13OVDlqgSIjuHekOeh7NCPX/fs5fFVeIxvSQjp8VV6j1/37Oeh7NKO4d6Q5aoEio13OVDnENxijT9kAObR2xSL/Iio51k13IhctiDk3OhAjGCvROeX9LSMb0kI62LrhoxvSQjpi1gekGCvROUDFv6IXLYg5BBU2o/8iKjnZeL6iT9kAObYUtyJdzlQ5Io5MIrh3pDmXUO4i1/37ObH4PqMb0kI6EjgXpNf9+zm7XpajuHekOSaqRKNdzlQ5Veb6ok/ZADkIbx45cQqFoaz3eTnI6PIgoHq3OTMvNKPX/fs5YtYHpNf9+zk5+jmjoHq3ORlWXKOs93k5041NowhvHjkijsyfAPo2OdD+KiIXLYg5l/2uoqB6tzmqoGOjGCvRObtelqOgerc57MgzoxctiDm9AXWjAPo2OSc3GqNP2QA5lMFLoYogRTm3oYwhFy2IOc4+gaK4d6Q5QMW/orh3pDkZVlyjFy2IObjUYKOKIEU5MdPlok/ZADm+5Qk5dyhKIYogRTlwOwyirPd5OWjSlKIXLYg5JqpEo6z3eTm9AXWjiiBFOQ8NUKO+5Qk5vuUJOdUao6IA+jY5cDsMo13OVDkEFTajXc5UOWgxg6MA+jY5MdPlor7lCTlP2QA5qguBoghvHjl3Fy6j/yIqOVXm+qIIbx45Jzcao0/ZADlP2QA52Xi+ok/ZADmRAIg6D5CCuk3amjoRoIi6PyyUOtwab7rcGm+6MPqhOkN6pboS7s06/mjBuvLQ2DrfXre6zHW6Og4bj7qRAIg6y8N5urW6sTr+aMG6Vjj/OuBGAbsgjhg7I8sJu6p0Dzu4AOW68tDYOmt7nbqC5o063BpvurW6sTqRSsy6qnQPOxq1HbvwMEg7BsFFuxsSWDuosDa7W78tO+BGAbs9luQ6a3udupEAiDow+qE6/mjBuqp0Dzvocim7KkdqO+6XgbveTps7QBaQu8U2jDt8E1e7mDE6O+BGAbvy0Ng6DhuPupEAiDpDeqW6Vjj/Ohq1HbsqR2o7QBaQu19hxjt59dm79rDnO0moubveTps7fBNXu1u/LTu4AOW6zHW6Otwab7oPkIK6Eu7NOuBGAbvwMEg77peBu19hxjvWIQW8f8s2PPY0LrwjrQw8Sai5u8U2jDuosDa7qnQPO99et7o/LJQ6TdqaOv5owbogjhg7BsFFu95Omzt59dm7f8s2PGepgryr/Yc89jQuvPaw5ztAFpC7GxJYOyPLCbvy0Ng6EaCIuhGgiLry0Ng6I8sJuxsSWDtAFpC79rDnO/Y0Lryr/Yc8Z6mCvH/LNjx59dm73k6bOwbBRbsgjhg7/mjBuk3amjo/LJQ63163uqp0DzuosDa7xTaMO0moubsjrQw89jQuvH/LNjzWIQW8X2HGO+6XgbvwMEg74EYBuxLuzToPkIK63Bpvusx1ujq4AOW6W78tO3wTV7veTps7Sai5u/aw5zt59dm7X2HGO0AWkLsqR2o7GrUdu1Y4/zpDeqW6kQCIOg4bj7ry0Ng64EYBu5gxOjt8E1e7xTaMO0AWkLveTps77peBuypHajvocim7qnQPO/5owbow+qE6kQCIOmt7nbo9luQ64EYBu1u/LTuosDa7GxJYOwbBRbvwMEg7GrUdu6p0DzuRSsy6tbqxOtwab7qC5o06a3uduvLQ2Dq4AOW6qnQPOyPLCbsgjhg74EYBu1Y4/zr+aMG6tbqxOsvDebqRAIg6DhuPusx1ujrfXre68tDYOv5owboS7s06Q3qlujD6oTrcGm+63Bpvuj8slDoRoIi6TdqaOg+QgrqRAIg6vuUJOSa7YCIIbx45umE2IghvHjkSi9YhvuUJOXzxAiOk9hg5XYB5IibgPTkJAKcifcJMOdKcHKAm4D05XF7BIKT2GDm3oQygfPECIwhvHjn2nhUiXc5UOd/wBCOs93k5+F4/Iqz3eTl1u18hXc5UOTlNeSEIbx45qy05oqT2GDn2nhUisU1dOQX1SiNtZY45b2wTIlVvnDkq98MibWWOOUVuDSKxTV05ePdCoqT2GDm+5Qk5XYB5Il3OVDkF9Uojcx6VOdk2myKgerc5QjIqIqB6tzk76+qicx6VObH4PqJdzlQ5wfu0or7lCTkmu2AiJuA9Od/wBCNtZY45JcovIR0nwzl31QojRgDjOXfVCqMdJ8M5JcovoW1ljjnf8ASjJuA9OSa7YKIIbx45CQCnIqz3eTlvbBMioHq3OQK55yHX/fs55f0to9f9+zm8vYSjoHq3OUMSP6Os93k5sglbowhvHjm6YTYifcJMOfhePyJVb5w5QjIqIkYA4znl/S2jScESOmLWB6RGAOM5dkg1o1VvnDmPlDejfcJMOQM1IaMIbx45cQqFoaz3eTnI6PIgoHq3OTMvNKPX/fs5YtYHpNf9+zk5+jmjoHq3ORlWXKOs93k5041NowhvHjlfmqShJuA9OUMBoyJtZY451AmHoh0nwzkIYjWiRgDjOTn6OaMdJ8M5iBeEo21ljjmbW8qjJuA9OY7FPqK+5Qk5QwEjoF3OVDkijsyecx6VORzl/qKgerc5dkg1o6B6tzlStsajcx6VOVk/qaNdzlQ5AEz9or7lCTmk9hg5LYZmorFNXTl5xruibWWOOdmadqNVb5w5GVZco21ljjlZP6mjsU1dOSWIDKOk9hg5oSbQoQhvHjlxG6GiXc5UOdvlKKOs93k5j5Q3o6z3eTmbW8qjXc5UOe9ocqMIbx45joMbo73wWKKk9hg5ywKeoibgPTmyCVujfcJMOdONTaMm4D05AEz9oqT2GDmOgxujvuUJOfJRlqIIbx45AzUhowhvHjmOxT6ivuUJOZEAiDrLw3m6guaNOtwab7oPkIK6l5apOkN6pbpl1sM6xRWuusx1ujpCC5a6PyyUOg4bj7pl1sM6kUrMulY4/zrKF/O64SwHO7gA5bo9luQ6xRWuujD6oToOG4+6Eu7NOrgA5bogjhg7GrUdu5gxOjvocim7W78tOyPLCbtWOP863163ujD6oToPkIK6ZdbDOrgA5bqqmSI7qLA2uypHajsZQ2u7q25/O3wTV7vwMEg7NTgTu1Y4/zrFFa66PyyUOpeWqTqRSsy6II4YO6iwNrurbn87QBaQuzgUrjtnK6K73k6bOxlDa7vwMEg7I8sJuz2W5DpCC5a6kQCIOkN6pbpWOP86GrUduypHajtAFpC7X2HGO3n12bv2sOc7Sai5u95Omzt8E1e7W78tO7gA5brMdbo63BpvusvDebpl1sM6yhfzupgxOjsZQ2u7OBSuO3n12bsjrQw81iEFvPaw5ztnK6K7q25/O+hyKbvhLAc7xRWuuoLmjTqC5o06xRWuuuEsBzvocim7q25/O2crorv2sOc71iEFvCOtDDx59dm7OBSuOxlDa7uYMTo7yhfzumXWwzrLw3m63Bpvusx1ujq4AOW6W78tO3wTV7veTps7Sai5u/aw5zt59dm7X2HGO0AWkLsqR2o7GrUdu1Y4/zpDeqW6kQCIOkILlro9luQ6I8sJu/AwSDsZQ2u73k6bO2crors4FK47QBaQu6tufzuosDa7II4YO5FKzLqXlqk6PyyUOsUVrrpWOP86NTgTu/AwSDt8E1e7q25/OxlDa7sqR2o7qLA2u6qZIju4AOW6ZdbDOg+Qgrow+qE63163ulY4/zojywm7W78tO+hyKbuYMTo7GrUduyCOGDu4AOW6Eu7NOg4bj7ow+qE6xRWuuj2W5Dq4AOW64SwHO8oX87pWOP86kUrMumXWwzoOG4+6PyyUOkILlrrMdbo6xRWuumXWwzpDeqW6l5apOg+QgrrcGm+6guaNOsvDebqRAIg6vuUJObPpbyLzuRM5Io7MIb7lCTn/1oUhCG8eOU+GkiIA+jY5AepuoQD6NjnG90GiCG8eOQkxLqLsyLMi/yIqOQX1SiJdzlQ5ssc3oWdLZjkeUumhXc5UOYolgKL/Iio5K+h0oqk8CCL/Iio5QMU/ImdLZjn4Xj8iFy2IOaxP8SEXLYg56/k6omdLZjlcC4Ki/yIqOcpVXaIIbx45BfVKImdLZjlWtfOhcx6VOZCFaCK4d6Q5cQoFo3MelTnyURajZ0tmOZOfE6MIbx45vuUJOU+GkiJdzlQ57wSXoXMelTmUwUsioHq3Oa5pHKOgerc56ipCo3MelTlluGmjXc5UOWaHYqK+5Qk5lG4MIgD6NjlcXsGhFy2IOZCFaCKgerc5WL4Coxgr0TmqoGOjoHq3OWBpnaMXLYg5u/8nowD6NjkXuGqi87kTOQHqbqFnS2Y5rE/xIbh3pDmuaRyjGCvROVWTu6MYK9E5Q3Gto7h3pDkliAyjZ0tmOUqKBaPzuRM5Io7MnwD6NjnQ/ioiFy2IOZf9rqKgerc5qqBjoxgr0Tm7XpajoHq3OezIM6MXLYg5vQF1owD6NjknNxqjvuUJOdsnzKFdzlQ5yjOlonMelTkRWIKjoHq3OUNxraOgerc5UrbGo3MelTlZP6mjXc5UOQBM/aK+5Qk5CG8eOe1mJaFnS2Y5EbzdonMelTnTOo6juHekOezIM6NzHpU5R3yJo2dLZjkAt5qjCG8eOTlN+R//Iio5J4pZomdLZjmnIt2iFy2IOSWIDKMXLYg5WT+po2dLZjmYflWj/yIqOdeYKaP+Bw2i/yIqOXnGu6JdzlQ5MMJJo2dLZjm9AXWjXc5UOQC3mqP/Iio5kCENo1cgkaIIbx45ZodiogD6NjlKigWjAPo2OQBM/aIIbx4515gpo77lCTkXuGqi87kTOSc3GqO+5Qk5D5CCujD6oTpCC5a6l5apOg4bj7o/LJQ6guaNOkILlrpl1sM6/mjBuj2W5DqRSsy68tDYOsUVrrqXlqk6y8N5uoLmjTpre5268tDYOrgA5bqqdA87I8sJuyCOGDvgRgG7Vjj/Ov5owbq1urE6y8N5ukILlrry0Ng6yhfzuqqZIjvocim78DBIO6iwNruYMTo7NTgTu+EsBzv+aMG6l5apOg+Qgrpl1sM6uADluqqZIjuosDa7KkdqOxlDa7urbn87fBNXu/AwSDs1OBO7Vjj/OsUVrro/LJQ6MPqhOv5owbqqdA876HIpuypHajvul4G73k6bO0AWkLvFNow7fBNXu5gxOjvgRgG78tDYOg4bj7pCC5a6PZbkOiPLCbvwMEg7GUNru95OmztnK6K7OBSuO0AWkLurbn87qLA2uyCOGDuRSsy6l5apOpeWqTqRSsy6II4YO6iwNrurbn87QBaQuzgUrjtnK6K73k6bOxlDa7vwMEg7I8sJuz2W5DpCC5a6DhuPuvLQ2DrgRgG7mDE6O3wTV7vFNow7QBaQu95Omzvul4G7KkdqO+hyKbuqdA87/mjBujD6oTo/LJQ6xRWuulY4/zo1OBO78DBIO3wTV7urbn87GUNruypHajuosDa7qpkiO7gA5bpl1sM6D5CCupeWqTr+aMG64SwHOzU4E7uYMTo7qLA2u/AwSDvocim7qpkiO8oX87ry0Ng6QguWusvDebq1urE6/mjBulY4/zrgRgG7II4YOyPLCbuqdA87uADluvLQ2Dpre526guaNOsvDebqXlqk6xRWuuvLQ2DqRSsy6PZbkOv5owbpl1sM6QguWuoLmjTo/LJQ6DhuPupeWqTpCC5a6MPqhOg+QgrpP2QA5qy05Ik/ZADn0MasipPYYOaS1ciIRJyQ57jWeoKT2GDkMbZGgT9kAOXBMqCL/Iio5cRuhIoogRTnKVV2hiiBFOei916D/Iio5YLxcok/ZADmjcYIikGcwOcrxgSKxTV05mJsgomLUbzmYmyAisU1dOcrxgaKQZzA5o3GCorRlKSL/Iio53ed1ImdLZjlMW0uiFy2IOcygj6IXLYg5ONgBo2dLZjm8Mvyi/yIqOfvcxaKk9hg5ePdCIrFNXTlFbg2ibWWOOSr3w6JVb5w5b2wTom1ljjkF9UqjsU1dOfaeFaKk9hg5T9kAOcNoHyKKIEU5lJBEohctiDm+v9GiuHekOTc6EKO4d6Q5QxI/oxctiDmorzKjiiBFOdQJh6JP2QA5qy05IhEnJDnKVV2hYtRvOcygj6JVb5w5NzoQo8dkrTlAxb+iVW+cOSWIDKNi1G85nJpNoxEnJDmbGSejT9kAOZTBS6GKIEU5t6GMIRctiDnOPoGiuHekOUDFv6K4d6Q5GVZcoxctiDm41GCjiiBFOTHT5aJP2QA5pPYYOS2GZqKxTV05eca7om1ljjnZmnajVW+cORlWXKNtZY45WT+po7FNXTkliAyjpPYYOTlN+R//Iio5J4pZomdLZjmnIt2iFy2IOSWIDKMXLYg5WT+po2dLZjmYflWj/yIqOdeYKaMqGfyhkGcwOc6zeKKxTV05qK8yo2LUbzm41GCjsU1dOZh+VaOQZzA5HKPbok/ZADn/OuGi/yIqOb47C6OKIEU5nJpNo4ogRTkliAyj/yIqOYStOaNP2QA5+9zFoqT2GDm1VtqiESckOTHT5aKk9hg515gpo0/ZADmbGSejT9kAOdwab7qC5o06y8N5upEAiDqC5o06DhuPurW6sTpDeqW6zHW6Omt7nbow+qE6y8N5uj8slDpre526Eu7NOpFKzLrfWPE6pB3Yuj2W5DrfXre6tbqxOg+QgrqC5o06a3uduvLQ2Dq4AOW6qnQPOyPLCbsgjhg74EYBu1Y4/zr+aMG6tbqxOsvDeboOG4+6Eu7NOrgA5bogjhg7GrUdu5gxOjvocim7W78tOyPLCbtWOP863163ujD6oTrcGm+6tbqxOpFKzLqqdA87GrUdu/AwSDsGwUW7GxJYO6iwNrtbvy074EYBuz2W5Dpre526kQCIOoLmjTpDeqW631jxOiPLCbuYMTo7BsFFuypHajt8E1e7GxJYO+hyKbsgjhg7pB3Yusx1ujrLw3m6y8N5usx1ujqkHdi6II4YO+hyKbsbElg7fBNXuypHajsGwUW7mDE6OyPLCbvfWPE6Q3qluoLmjTqRAIg6a3uduj2W5DrgRgG7W78tO6iwNrsbElg7BsFFu/AwSDsatR27qnQPO5FKzLq1urE63BpvujD6oTrfXre6Vjj/OiPLCbtbvy076HIpu5gxOjsatR27II4YO7gA5boS7s06DhuPusvDebq1urE6/mjBulY4/zrgRgG7II4YOyPLCbuqdA87uADluvLQ2Dpre526guaNOg+Qgrq1urE63163uj2W5DqkHdi631jxOpFKzLoS7s06a3uduj8slDrLw3m6MPqhOmt7nbrMdbo6Q3qlurW6sToOG4+6guaNOpEAiDrLw3m6guaNOtwab7rOs3givuUJOei91yC+5Qk5bDCwoU/ZADlVcYMiCG8eOaqx/57/Iio5jhj+IQhvHjlBYzGiT9kAOU/ZADkVGnki/yIqOQhitSGKIEU5t6EMIoogRTkZVlyi/yIqOefuXqJP2QA5t6GMIv8iKjmdG/QhXc5UOe1mpSFnS2Y5QMU/IF3OVDnZNpui/yIqOTqHj6IL4LsiCG8eOSc3GiJdzlQ5AzWhIqz3eTnPLzIirPd5OaPEwaJdzlQ5BFdZIQhvHjnmH+aivuUJOY4YfiGKIEU5ePdCIaz3eTk5TXkgFy2IOZ7q7KKs93k57wQXo4ogRTnGOeWivuUJOei91yD/Iio5t6EMImdLZjnPLzIiFy2IOX8+AqIXLYg5j5Q3o2dLZjmcmk2j/yIqOamPR6O+5Qk5q4D4oYogRTnztXGirPd5OSWIDKMXLYg5JqpEo6z3eTm9AXWjiiBFOQ8NUKO+5Qk5oSbQoQhvHjlxG6GiXc5UOdvlKKOs93k5j5Q3o6z3eTmbW8qjXc5UOe9ocqMIbx45joMbo/4HDaL/Iio5eca7ol3OVDkwwkmjZ0tmOb0BdaNdzlQ5ALeao/8iKjmQIQ2jT9kAOf864aL/Iio5vjsLo4ogRTmcmk2jiiBFOSWIDKP/Iio5hK05o0/ZADlP2QA5OoePoghvHjlgermi/yIqOQ8NUKMIbx45kCENo0/ZADl88QKjvuUJOamPR6O+5Qk5joMbo8vDebo/LJQ6D5CCuoLmjTqC5o06DhuPurW6sTpDeqW6zHW6Omt7nbow+qE6y8N5uoLmjTpCC5a6ZdbDOv5owbo9luQ6kUrMuvLQ2DrFFa66l5apOsvDeboOG4+6ZdbDOpFKzLpWOP86yhfzuuEsBzu4AOW6PZbkOsUVrrow+qE6y8N5urW6sTr+aMG6Vjj/OuBGAbsgjhg7I8sJu6p0Dzu4AOW68tDYOmt7nbqC5o06PyyUOkN6pbo9luQ6yhfzuiCOGDs1OBO7qpkiOyPLCbvhLAc7kUrMusx1ujoPkIK6D5CCusx1ujqRSsy64SwHOyPLCbuqmSI7NTgTuyCOGDvKF/O6PZbkOkN6pbo/LJQ6guaNOmt7nbry0Ng6uADluqp0Dzsjywm7II4YO+BGAbtWOP86/mjBurW6sTrLw3m6MPqhOsUVrro9luQ6uADluuEsBzvKF/O6Vjj/OpFKzLpl1sM6DhuPusvDebqXlqk6xRWuuvLQ2DqRSsy6PZbkOv5owbpl1sM6QguWuoLmjTrLw3m6MPqhOmt7nbrMdbo6Q3qlurW6sToOG4+6guaNOoLmjToPkIK6PyyUOsvDebq1NCIhvuUJOQ4LA6K+5Qk5zrN4ogeTvKGk9hg5YGkdohEnJDnTOo6ipPYYOeDQmaKH2ACiCG8eOYuJW6IA+jY5U+StogD6NjlGf6miCG8eOfDkq6JXhOyhpPYYOYGCcqIm4D05WSLeon3CTDkqGfyiJuA9ORuyqqKk9hg55h/mor7lCTmYzCeiAPo2OVki3qJdzlQ5RW4No13OVDmyCVujAPo2OcvRFqO+5Qk5DgsDohEnJDlT5K2ifcJMOUVuDaOxTV05BBU2o33CTDlKigWjESckOamPR6O+5Qk51RqjogD6NjlwOwyjXc5UOQQVNqNdzlQ5aDGDowD6Njkx0+WivuUJOb3wWKKk9hg5ywKeoibgPTmyCVujfcJMOdONTaMm4D05AEz9oqT2GDmOgxujVyCRoghvHjlmh2KiAPo2OUqKBaMA+jY5AEz9oghvHjnXmCmj+9zFoqT2GDk8eECjESckOTHT5aKk9hg515gpo3zxAqO+5Qk5qY9Ho77lCTmOgxuj3BpvuoLmjTrLw3m6kQCIOg+Qgrow+qE6QguWupeWqToOG4+6PyyUOg+QgrqXlqk6Q3qlumXWwzrFFa66zHW6OkILlro/LJQ63BpvujD6oTpDeqW6Eu7NOv5owbry0Ng63163usx1ujoOG4+6kQCIOoLmjTpCC5a6ZdbDOv5owbo9luQ6kUrMuvLQ2DrFFa66l5apOsvDebrLw3m6l5apOsUVrrry0Ng6kUrMuj2W5Dr+aMG6ZdbDOkILlrqC5o06kQCIOg4bj7rMdbo63163uvLQ2Dr+aMG6Eu7NOkN6pbow+qE63Bpvuj8slDpCC5a6zHW6OsUVrrpl1sM6Q3qlupeWqToPkIK6PyyUOg4bj7qXlqk6QguWujD6oToPkIK6kQCIOsvDebqC5o063Bpvuk/ZADmrLTmiT9kAOb7lCTlUogqi87kTOdujhaK+5Qk5vuUJOfFxgaIIbx45vn2uoghvHjkmu2CivuUJOU/ZADlUogqiCG8eObYUt6L/Iio5xDcYowhvHjmtiYeiT9kAOastOaLzuRM5vn2uov8iKjmDIOSi/yIqOQM1IaPzuRM5mxkno0/ZADmqC4GiCG8eOXcXLqP/Iio5Veb6oghvHjknNxqjT9kAOb7lCTnyUZaiCG8eOQM1IaMIbx45jsU+or7lCTm+5Qk5F7hqovO5EzknNxqjvuUJOU/ZADmbGSejT9kAOZEAiDrLw3m6guaNOtwab7qRAIg6D5CCuk3amjoRoIi6PyyUOtwab7rLw3m6TdqaOg4bj7ow+qE6EaCIuoLmjTqC5o06EaCIujD6oToOG4+6TdqaOsvDebrcGm+6PyyUOhGgiLpN2po6D5CCupEAiDrcGm+6guaNOsvDebqRAIg6T9kAObR2xaJP2QA5tHbFovhHBTnZeL6iT9kAOdl4vqJP2QA5\",\"u0_imag\":0.004360592167337278,\"absorptive\":true,\"n_reflections\":4572,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", + "GaAs (zincblende)": "{\"name\":\"GaAs\",\"spacegroup\":\"F-43m (216)\",\"pointgroup\":\"-43m\",\"cell\":[[5.653,0.0,0.0],[0.0,5.653,0.0],[0.0,0.0,5.653]],\"recip\":[[0.17689722271360342,0.0,0.0],[0.0,0.17689722271360342,0.0],[0.0,0.0,0.17689722271360342]],\"positions_frac\":[[0.0,0.0,0.0],[0.25,0.25,0.25],[0.0,0.5,0.5],[0.25,0.7500000000000001,0.7500000000000001],[0.5,0.0,0.5],[0.7500000000000001,0.25,0.7500000000000001],[0.5,0.5,0.0],[0.7500000000000001,0.7500000000000001,0.25]],\"numbers\":[31,33,31,33,31,33,31,33],\"symbols\":[\"Ga\",\"As\",\"Ga\",\"As\",\"Ga\",\"As\",\"Ga\",\"As\"],\"colors\":[[0.761,0.561,0.561],[0.741,0.502,0.89],[0.761,0.561,0.561],[0.741,0.502,0.89],[0.761,0.561,0.561],[0.741,0.502,0.89],[0.761,0.561,0.561],[0.741,0.502,0.89]],\"radii\":[1.22,1.19,1.22,1.19,1.22,1.19,1.22,1.19],\"hkl_i16\":\"8P/8//7/8P/8/wAA8P/8/wIA8P/+//z/8P/+//7/8P/+/wAA8P/+/wIA8P/+/wQA8P8AAPz/8P8AAP7/8P8AAAAA8P8AAAIA8P8AAAQA8P8CAPz/8P8CAP7/8P8CAAAA8P8CAAIA8P8CAAQA8P8EAP7/8P8EAAAA8P8EAAIA8f/5//3/8f/5////8f/5/wEA8f/5/wMA8f/7//v/8f/7//3/8f/7////8f/7/wEA8f/7/wMA8f/7/wUA8f/9//n/8f/9//v/8f/9//3/8f/9////8f/9/wEA8f/9/wMA8f/9/wUA8f/9/wcA8f////n/8f////v/8f////3/8f//////8f///wEA8f///wMA8f///wUA8f///wcA8f8BAPn/8f8BAPv/8f8BAP3/8f8BAP//8f8BAAEA8f8BAAMA8f8BAAUA8f8BAAcA8f8DAPn/8f8DAPv/8f8DAP3/8f8DAP//8f8DAAEA8f8DAAMA8f8DAAUA8f8DAAcA8f8FAPv/8f8FAP3/8f8FAP//8f8FAAEA8f8FAAMA8f8FAAUA8f8HAP3/8f8HAP//8f8HAAEA8f8HAAMA8v/4//z/8v/4//7/8v/4/wAA8v/4/wIA8v/4/wQA8v/6//r/8v/6//z/8v/6//7/8v/6/wAA8v/6/wIA8v/6/wQA8v/6/wYA8v/8//j/8v/8//r/8v/8//z/8v/8//7/8v/8/wAA8v/8/wIA8v/8/wQA8v/8/wYA8v/8/wgA8v/+//j/8v/+//r/8v/+//z/8v/+//7/8v/+/wAA8v/+/wIA8v/+/wQA8v/+/wYA8v/+/wgA8v8AAPj/8v8AAPr/8v8AAPz/8v8AAP7/8v8AAAAA8v8AAAIA8v8AAAQA8v8AAAYA8v8AAAgA8v8CAPj/8v8CAPr/8v8CAPz/8v8CAP7/8v8CAAAA8v8CAAIA8v8CAAQA8v8CAAYA8v8CAAgA8v8EAPj/8v8EAPr/8v8EAPz/8v8EAP7/8v8EAAAA8v8EAAIA8v8EAAQA8v8EAAYA8v8EAAgA8v8GAPr/8v8GAPz/8v8GAP7/8v8GAAAA8v8GAAIA8v8GAAQA8v8GAAYA8v8IAPz/8v8IAP7/8v8IAAAA8v8IAAIA8v8IAAQA8//3//v/8//3//3/8//3////8//3/wEA8//3/wMA8//3/wUA8//5//n/8//5//v/8//5//3/8//5////8//5/wEA8//5/wMA8//5/wUA8//5/wcA8//7//f/8//7//n/8//7//v/8//7//3/8//7////8//7/wEA8//7/wMA8//7/wUA8//7/wcA8//7/wkA8//9//f/8//9//n/8//9//v/8//9//3/8//9////8//9/wEA8//9/wMA8//9/wUA8//9/wcA8//9/wkA8/////f/8/////n/8/////v/8/////3/8///////8////wEA8////wMA8////wUA8////wcA8////wkA8/8BAPf/8/8BAPn/8/8BAPv/8/8BAP3/8/8BAP//8/8BAAEA8/8BAAMA8/8BAAUA8/8BAAcA8/8BAAkA8/8DAPf/8/8DAPn/8/8DAPv/8/8DAP3/8/8DAP//8/8DAAEA8/8DAAMA8/8DAAUA8/8DAAcA8/8DAAkA8/8FAPf/8/8FAPn/8/8FAPv/8/8FAP3/8/8FAP//8/8FAAEA8/8FAAMA8/8FAAUA8/8FAAcA8/8FAAkA8/8HAPn/8/8HAPv/8/8HAP3/8/8HAP//8/8HAAEA8/8HAAMA8/8HAAUA8/8HAAcA8/8JAPv/8/8JAP3/8/8JAP//8/8JAAEA8/8JAAMA8/8JAAUA9P/2//r/9P/2//z/9P/2//7/9P/2/wAA9P/2/wIA9P/2/wQA9P/2/wYA9P/4//j/9P/4//r/9P/4//z/9P/4//7/9P/4/wAA9P/4/wIA9P/4/wQA9P/4/wYA9P/4/wgA9P/6//b/9P/6//j/9P/6//r/9P/6//z/9P/6//7/9P/6/wAA9P/6/wIA9P/6/wQA9P/6/wYA9P/6/wgA9P/6/woA9P/8//b/9P/8//j/9P/8//r/9P/8//z/9P/8//7/9P/8/wAA9P/8/wIA9P/8/wQA9P/8/wYA9P/8/wgA9P/8/woA9P/+//b/9P/+//j/9P/+//r/9P/+//z/9P/+//7/9P/+/wAA9P/+/wIA9P/+/wQA9P/+/wYA9P/+/wgA9P/+/woA9P8AAPb/9P8AAPj/9P8AAPr/9P8AAPz/9P8AAP7/9P8AAAAA9P8AAAIA9P8AAAQA9P8AAAYA9P8AAAgA9P8AAAoA9P8CAPb/9P8CAPj/9P8CAPr/9P8CAPz/9P8CAP7/9P8CAAAA9P8CAAIA9P8CAAQA9P8CAAYA9P8CAAgA9P8CAAoA9P8EAPb/9P8EAPj/9P8EAPr/9P8EAPz/9P8EAP7/9P8EAAAA9P8EAAIA9P8EAAQA9P8EAAYA9P8EAAgA9P8EAAoA9P8GAPb/9P8GAPj/9P8GAPr/9P8GAPz/9P8GAP7/9P8GAAAA9P8GAAIA9P8GAAQA9P8GAAYA9P8GAAgA9P8GAAoA9P8IAPj/9P8IAPr/9P8IAPz/9P8IAP7/9P8IAAAA9P8IAAIA9P8IAAQA9P8IAAYA9P8IAAgA9P8KAPr/9P8KAPz/9P8KAP7/9P8KAAAA9P8KAAIA9P8KAAQA9P8KAAYA9f/1//v/9f/1//3/9f/1////9f/1/wEA9f/1/wMA9f/1/wUA9f/3//f/9f/3//n/9f/3//v/9f/3//3/9f/3////9f/3/wEA9f/3/wMA9f/3/wUA9f/3/wcA9f/3/wkA9f/5//f/9f/5//n/9f/5//v/9f/5//3/9f/5////9f/5/wEA9f/5/wMA9f/5/wUA9f/5/wcA9f/5/wkA9f/7//X/9f/7//f/9f/7//n/9f/7//v/9f/7//3/9f/7////9f/7/wEA9f/7/wMA9f/7/wUA9f/7/wcA9f/7/wkA9f/7/wsA9f/9//X/9f/9//f/9f/9//n/9f/9//v/9f/9//3/9f/9////9f/9/wEA9f/9/wMA9f/9/wUA9f/9/wcA9f/9/wkA9f/9/wsA9f////X/9f////f/9f////n/9f////v/9f////3/9f//////9f///wEA9f///wMA9f///wUA9f///wcA9f///wkA9f///wsA9f8BAPX/9f8BAPf/9f8BAPn/9f8BAPv/9f8BAP3/9f8BAP//9f8BAAEA9f8BAAMA9f8BAAUA9f8BAAcA9f8BAAkA9f8BAAsA9f8DAPX/9f8DAPf/9f8DAPn/9f8DAPv/9f8DAP3/9f8DAP//9f8DAAEA9f8DAAMA9f8DAAUA9f8DAAcA9f8DAAkA9f8DAAsA9f8FAPX/9f8FAPf/9f8FAPn/9f8FAPv/9f8FAP3/9f8FAP//9f8FAAEA9f8FAAMA9f8FAAUA9f8FAAcA9f8FAAkA9f8FAAsA9f8HAPf/9f8HAPn/9f8HAPv/9f8HAP3/9f8HAP//9f8HAAEA9f8HAAMA9f8HAAUA9f8HAAcA9f8HAAkA9f8JAPf/9f8JAPn/9f8JAPv/9f8JAP3/9f8JAP//9f8JAAEA9f8JAAMA9f8JAAUA9f8JAAcA9f8JAAkA9f8LAPv/9f8LAP3/9f8LAP//9f8LAAEA9f8LAAMA9f8LAAUA9v/0//r/9v/0//z/9v/0//7/9v/0/wAA9v/0/wIA9v/0/wQA9v/0/wYA9v/2//j/9v/2//r/9v/2//z/9v/2//7/9v/2/wAA9v/2/wIA9v/2/wQA9v/2/wYA9v/2/wgA9v/4//b/9v/4//j/9v/4//r/9v/4//z/9v/4//7/9v/4/wAA9v/4/wIA9v/4/wQA9v/4/wYA9v/4/wgA9v/4/woA9v/6//T/9v/6//b/9v/6//j/9v/6//r/9v/6//z/9v/6//7/9v/6/wAA9v/6/wIA9v/6/wQA9v/6/wYA9v/6/wgA9v/6/woA9v/6/wwA9v/8//T/9v/8//b/9v/8//j/9v/8//r/9v/8//z/9v/8//7/9v/8/wAA9v/8/wIA9v/8/wQA9v/8/wYA9v/8/wgA9v/8/woA9v/8/wwA9v/+//T/9v/+//b/9v/+//j/9v/+//r/9v/+//z/9v/+//7/9v/+/wAA9v/+/wIA9v/+/wQA9v/+/wYA9v/+/wgA9v/+/woA9v/+/wwA9v8AAPT/9v8AAPb/9v8AAPj/9v8AAPr/9v8AAPz/9v8AAP7/9v8AAAAA9v8AAAIA9v8AAAQA9v8AAAYA9v8AAAgA9v8AAAoA9v8AAAwA9v8CAPT/9v8CAPb/9v8CAPj/9v8CAPr/9v8CAPz/9v8CAP7/9v8CAAAA9v8CAAIA9v8CAAQA9v8CAAYA9v8CAAgA9v8CAAoA9v8CAAwA9v8EAPT/9v8EAPb/9v8EAPj/9v8EAPr/9v8EAPz/9v8EAP7/9v8EAAAA9v8EAAIA9v8EAAQA9v8EAAYA9v8EAAgA9v8EAAoA9v8EAAwA9v8GAPT/9v8GAPb/9v8GAPj/9v8GAPr/9v8GAPz/9v8GAP7/9v8GAAAA9v8GAAIA9v8GAAQA9v8GAAYA9v8GAAgA9v8GAAoA9v8GAAwA9v8IAPb/9v8IAPj/9v8IAPr/9v8IAPz/9v8IAP7/9v8IAAAA9v8IAAIA9v8IAAQA9v8IAAYA9v8IAAgA9v8IAAoA9v8KAPj/9v8KAPr/9v8KAPz/9v8KAP7/9v8KAAAA9v8KAAIA9v8KAAQA9v8KAAYA9v8KAAgA9v8MAPr/9v8MAPz/9v8MAP7/9v8MAAAA9v8MAAIA9v8MAAQA9v8MAAYA9//z//v/9//z//3/9//z////9//z/wEA9//z/wMA9//z/wUA9//1//f/9//1//n/9//1//v/9//1//3/9//1////9//1/wEA9//1/wMA9//1/wUA9//1/wcA9//1/wkA9//3//X/9//3//f/9//3//n/9//3//v/9//3//3/9//3////9//3/wEA9//3/wMA9//3/wUA9//3/wcA9//3/wkA9//3/wsA9//5//X/9//5//f/9//5//n/9//5//v/9//5//3/9//5////9//5/wEA9//5/wMA9//5/wUA9//5/wcA9//5/wkA9//5/wsA9//7//P/9//7//X/9//7//f/9//7//n/9//7//v/9//7//3/9//7////9//7/wEA9//7/wMA9//7/wUA9//7/wcA9//7/wkA9//7/wsA9//7/w0A9//9//P/9//9//X/9//9//f/9//9//n/9//9//v/9//9//3/9//9////9//9/wEA9//9/wMA9//9/wUA9//9/wcA9//9/wkA9//9/wsA9//9/w0A9/////P/9/////X/9/////f/9/////n/9/////v/9/////3/9///////9////wEA9////wMA9////wUA9////wcA9////wkA9////wsA9////w0A9/8BAPP/9/8BAPX/9/8BAPf/9/8BAPn/9/8BAPv/9/8BAP3/9/8BAP//9/8BAAEA9/8BAAMA9/8BAAUA9/8BAAcA9/8BAAkA9/8BAAsA9/8BAA0A9/8DAPP/9/8DAPX/9/8DAPf/9/8DAPn/9/8DAPv/9/8DAP3/9/8DAP//9/8DAAEA9/8DAAMA9/8DAAUA9/8DAAcA9/8DAAkA9/8DAAsA9/8DAA0A9/8FAPP/9/8FAPX/9/8FAPf/9/8FAPn/9/8FAPv/9/8FAP3/9/8FAP//9/8FAAEA9/8FAAMA9/8FAAUA9/8FAAcA9/8FAAkA9/8FAAsA9/8FAA0A9/8HAPX/9/8HAPf/9/8HAPn/9/8HAPv/9/8HAP3/9/8HAP//9/8HAAEA9/8HAAMA9/8HAAUA9/8HAAcA9/8HAAkA9/8HAAsA9/8JAPX/9/8JAPf/9/8JAPn/9/8JAPv/9/8JAP3/9/8JAP//9/8JAAEA9/8JAAMA9/8JAAUA9/8JAAcA9/8JAAkA9/8JAAsA9/8LAPf/9/8LAPn/9/8LAPv/9/8LAP3/9/8LAP//9/8LAAEA9/8LAAMA9/8LAAUA9/8LAAcA9/8LAAkA9/8NAPv/9/8NAP3/9/8NAP//9/8NAAEA9/8NAAMA9/8NAAUA+P/y//z/+P/y//7/+P/y/wAA+P/y/wIA+P/y/wQA+P/0//j/+P/0//r/+P/0//z/+P/0//7/+P/0/wAA+P/0/wIA+P/0/wQA+P/0/wYA+P/0/wgA+P/2//b/+P/2//j/+P/2//r/+P/2//z/+P/2//7/+P/2/wAA+P/2/wIA+P/2/wQA+P/2/wYA+P/2/wgA+P/2/woA+P/4//T/+P/4//b/+P/4//j/+P/4//r/+P/4//z/+P/4//7/+P/4/wAA+P/4/wIA+P/4/wQA+P/4/wYA+P/4/wgA+P/4/woA+P/4/wwA+P/6//T/+P/6//b/+P/6//j/+P/6//r/+P/6//z/+P/6//7/+P/6/wAA+P/6/wIA+P/6/wQA+P/6/wYA+P/6/wgA+P/6/woA+P/6/wwA+P/8//L/+P/8//T/+P/8//b/+P/8//j/+P/8//r/+P/8//z/+P/8//7/+P/8/wAA+P/8/wIA+P/8/wQA+P/8/wYA+P/8/wgA+P/8/woA+P/8/wwA+P/8/w4A+P/+//L/+P/+//T/+P/+//b/+P/+//j/+P/+//r/+P/+//z/+P/+//7/+P/+/wAA+P/+/wIA+P/+/wQA+P/+/wYA+P/+/wgA+P/+/woA+P/+/wwA+P/+/w4A+P8AAPL/+P8AAPT/+P8AAPb/+P8AAPj/+P8AAPr/+P8AAPz/+P8AAP7/+P8AAAAA+P8AAAIA+P8AAAQA+P8AAAYA+P8AAAgA+P8AAAoA+P8AAAwA+P8AAA4A+P8CAPL/+P8CAPT/+P8CAPb/+P8CAPj/+P8CAPr/+P8CAPz/+P8CAP7/+P8CAAAA+P8CAAIA+P8CAAQA+P8CAAYA+P8CAAgA+P8CAAoA+P8CAAwA+P8CAA4A+P8EAPL/+P8EAPT/+P8EAPb/+P8EAPj/+P8EAPr/+P8EAPz/+P8EAP7/+P8EAAAA+P8EAAIA+P8EAAQA+P8EAAYA+P8EAAgA+P8EAAoA+P8EAAwA+P8EAA4A+P8GAPT/+P8GAPb/+P8GAPj/+P8GAPr/+P8GAPz/+P8GAP7/+P8GAAAA+P8GAAIA+P8GAAQA+P8GAAYA+P8GAAgA+P8GAAoA+P8GAAwA+P8IAPT/+P8IAPb/+P8IAPj/+P8IAPr/+P8IAPz/+P8IAP7/+P8IAAAA+P8IAAIA+P8IAAQA+P8IAAYA+P8IAAgA+P8IAAoA+P8IAAwA+P8KAPb/+P8KAPj/+P8KAPr/+P8KAPz/+P8KAP7/+P8KAAAA+P8KAAIA+P8KAAQA+P8KAAYA+P8KAAgA+P8KAAoA+P8MAPj/+P8MAPr/+P8MAPz/+P8MAP7/+P8MAAAA+P8MAAIA+P8MAAQA+P8MAAYA+P8MAAgA+P8OAPz/+P8OAP7/+P8OAAAA+P8OAAIA+P8OAAQA+f/x//3/+f/x////+f/x/wEA+f/x/wMA+f/z//n/+f/z//v/+f/z//3/+f/z////+f/z/wEA+f/z/wMA+f/z/wUA+f/z/wcA+f/1//f/+f/1//n/+f/1//v/+f/1//3/+f/1////+f/1/wEA+f/1/wMA+f/1/wUA+f/1/wcA+f/1/wkA+f/3//X/+f/3//f/+f/3//n/+f/3//v/+f/3//3/+f/3////+f/3/wEA+f/3/wMA+f/3/wUA+f/3/wcA+f/3/wkA+f/3/wsA+f/5//P/+f/5//X/+f/5//f/+f/5//n/+f/5//v/+f/5//3/+f/5////+f/5/wEA+f/5/wMA+f/5/wUA+f/5/wcA+f/5/wkA+f/5/wsA+f/5/w0A+f/7//P/+f/7//X/+f/7//f/+f/7//n/+f/7//v/+f/7//3/+f/7////+f/7/wEA+f/7/wMA+f/7/wUA+f/7/wcA+f/7/wkA+f/7/wsA+f/7/w0A+f/9//H/+f/9//P/+f/9//X/+f/9//f/+f/9//n/+f/9//v/+f/9//3/+f/9////+f/9/wEA+f/9/wMA+f/9/wUA+f/9/wcA+f/9/wkA+f/9/wsA+f/9/w0A+f/9/w8A+f////H/+f////P/+f////X/+f////f/+f////n/+f////v/+f////3/+f//////+f///wEA+f///wMA+f///wUA+f///wcA+f///wkA+f///wsA+f///w0A+f///w8A+f8BAPH/+f8BAPP/+f8BAPX/+f8BAPf/+f8BAPn/+f8BAPv/+f8BAP3/+f8BAP//+f8BAAEA+f8BAAMA+f8BAAUA+f8BAAcA+f8BAAkA+f8BAAsA+f8BAA0A+f8BAA8A+f8DAPH/+f8DAPP/+f8DAPX/+f8DAPf/+f8DAPn/+f8DAPv/+f8DAP3/+f8DAP//+f8DAAEA+f8DAAMA+f8DAAUA+f8DAAcA+f8DAAkA+f8DAAsA+f8DAA0A+f8DAA8A+f8FAPP/+f8FAPX/+f8FAPf/+f8FAPn/+f8FAPv/+f8FAP3/+f8FAP//+f8FAAEA+f8FAAMA+f8FAAUA+f8FAAcA+f8FAAkA+f8FAAsA+f8FAA0A+f8HAPP/+f8HAPX/+f8HAPf/+f8HAPn/+f8HAPv/+f8HAP3/+f8HAP//+f8HAAEA+f8HAAMA+f8HAAUA+f8HAAcA+f8HAAkA+f8HAAsA+f8HAA0A+f8JAPX/+f8JAPf/+f8JAPn/+f8JAPv/+f8JAP3/+f8JAP//+f8JAAEA+f8JAAMA+f8JAAUA+f8JAAcA+f8JAAkA+f8JAAsA+f8LAPf/+f8LAPn/+f8LAPv/+f8LAP3/+f8LAP//+f8LAAEA+f8LAAMA+f8LAAUA+f8LAAcA+f8LAAkA+f8NAPn/+f8NAPv/+f8NAP3/+f8NAP//+f8NAAEA+f8NAAMA+f8NAAUA+f8NAAcA+f8PAP3/+f8PAP//+f8PAAEA+f8PAAMA+v/y//r/+v/y//z/+v/y//7/+v/y/wAA+v/y/wIA+v/y/wQA+v/y/wYA+v/0//b/+v/0//j/+v/0//r/+v/0//z/+v/0//7/+v/0/wAA+v/0/wIA+v/0/wQA+v/0/wYA+v/0/wgA+v/0/woA+v/2//T/+v/2//b/+v/2//j/+v/2//r/+v/2//z/+v/2//7/+v/2/wAA+v/2/wIA+v/2/wQA+v/2/wYA+v/2/wgA+v/2/woA+v/2/wwA+v/4//T/+v/4//b/+v/4//j/+v/4//r/+v/4//z/+v/4//7/+v/4/wAA+v/4/wIA+v/4/wQA+v/4/wYA+v/4/wgA+v/4/woA+v/4/wwA+v/6//L/+v/6//T/+v/6//b/+v/6//j/+v/6//r/+v/6//z/+v/6//7/+v/6/wAA+v/6/wIA+v/6/wQA+v/6/wYA+v/6/wgA+v/6/woA+v/6/wwA+v/6/w4A+v/8//L/+v/8//T/+v/8//b/+v/8//j/+v/8//r/+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/8/wYA+v/8/wgA+v/8/woA+v/8/wwA+v/8/w4A+v/+//L/+v/+//T/+v/+//b/+v/+//j/+v/+//r/+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v/+/wYA+v/+/wgA+v/+/woA+v/+/wwA+v/+/w4A+v8AAPL/+v8AAPT/+v8AAPb/+v8AAPj/+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8AAAgA+v8AAAoA+v8AAAwA+v8AAA4A+v8CAPL/+v8CAPT/+v8CAPb/+v8CAPj/+v8CAPr/+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8CAAYA+v8CAAgA+v8CAAoA+v8CAAwA+v8CAA4A+v8EAPL/+v8EAPT/+v8EAPb/+v8EAPj/+v8EAPr/+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8EAAYA+v8EAAgA+v8EAAoA+v8EAAwA+v8EAA4A+v8GAPL/+v8GAPT/+v8GAPb/+v8GAPj/+v8GAPr/+v8GAPz/+v8GAP7/+v8GAAAA+v8GAAIA+v8GAAQA+v8GAAYA+v8GAAgA+v8GAAoA+v8GAAwA+v8GAA4A+v8IAPT/+v8IAPb/+v8IAPj/+v8IAPr/+v8IAPz/+v8IAP7/+v8IAAAA+v8IAAIA+v8IAAQA+v8IAAYA+v8IAAgA+v8IAAoA+v8IAAwA+v8KAPT/+v8KAPb/+v8KAPj/+v8KAPr/+v8KAPz/+v8KAP7/+v8KAAAA+v8KAAIA+v8KAAQA+v8KAAYA+v8KAAgA+v8KAAoA+v8KAAwA+v8MAPb/+v8MAPj/+v8MAPr/+v8MAPz/+v8MAP7/+v8MAAAA+v8MAAIA+v8MAAQA+v8MAAYA+v8MAAgA+v8MAAoA+v8OAPr/+v8OAPz/+v8OAP7/+v8OAAAA+v8OAAIA+v8OAAQA+v8OAAYA+//x//v/+//x//3/+//x////+//x/wEA+//x/wMA+//x/wUA+//z//f/+//z//n/+//z//v/+//z//3/+//z////+//z/wEA+//z/wMA+//z/wUA+//z/wcA+//z/wkA+//1//X/+//1//f/+//1//n/+//1//v/+//1//3/+//1////+//1/wEA+//1/wMA+//1/wUA+//1/wcA+//1/wkA+//1/wsA+//3//P/+//3//X/+//3//f/+//3//n/+//3//v/+//3//3/+//3////+//3/wEA+//3/wMA+//3/wUA+//3/wcA+//3/wkA+//3/wsA+//3/w0A+//5//P/+//5//X/+//5//f/+//5//n/+//5//v/+//5//3/+//5////+//5/wEA+//5/wMA+//5/wUA+//5/wcA+//5/wkA+//5/wsA+//5/w0A+//7//H/+//7//P/+//7//X/+//7//f/+//7//n/+//7//v/+//7//3/+//7////+//7/wEA+//7/wMA+//7/wUA+//7/wcA+//7/wkA+//7/wsA+//7/w0A+//7/w8A+//9//H/+//9//P/+//9//X/+//9//f/+//9//n/+//9//v/+//9//3/+//9////+//9/wEA+//9/wMA+//9/wUA+//9/wcA+//9/wkA+//9/wsA+//9/w0A+//9/w8A+/////H/+/////P/+/////X/+/////f/+/////n/+/////v/+/////3/+///////+////wEA+////wMA+////wUA+////wcA+////wkA+////wsA+////w0A+////w8A+/8BAPH/+/8BAPP/+/8BAPX/+/8BAPf/+/8BAPn/+/8BAPv/+/8BAP3/+/8BAP//+/8BAAEA+/8BAAMA+/8BAAUA+/8BAAcA+/8BAAkA+/8BAAsA+/8BAA0A+/8BAA8A+/8DAPH/+/8DAPP/+/8DAPX/+/8DAPf/+/8DAPn/+/8DAPv/+/8DAP3/+/8DAP//+/8DAAEA+/8DAAMA+/8DAAUA+/8DAAcA+/8DAAkA+/8DAAsA+/8DAA0A+/8DAA8A+/8FAPH/+/8FAPP/+/8FAPX/+/8FAPf/+/8FAPn/+/8FAPv/+/8FAP3/+/8FAP//+/8FAAEA+/8FAAMA+/8FAAUA+/8FAAcA+/8FAAkA+/8FAAsA+/8FAA0A+/8FAA8A+/8HAPP/+/8HAPX/+/8HAPf/+/8HAPn/+/8HAPv/+/8HAP3/+/8HAP//+/8HAAEA+/8HAAMA+/8HAAUA+/8HAAcA+/8HAAkA+/8HAAsA+/8HAA0A+/8JAPP/+/8JAPX/+/8JAPf/+/8JAPn/+/8JAPv/+/8JAP3/+/8JAP//+/8JAAEA+/8JAAMA+/8JAAUA+/8JAAcA+/8JAAkA+/8JAAsA+/8JAA0A+/8LAPX/+/8LAPf/+/8LAPn/+/8LAPv/+/8LAP3/+/8LAP//+/8LAAEA+/8LAAMA+/8LAAUA+/8LAAcA+/8LAAkA+/8LAAsA+/8NAPf/+/8NAPn/+/8NAPv/+/8NAP3/+/8NAP//+/8NAAEA+/8NAAMA+/8NAAUA+/8NAAcA+/8NAAkA+/8PAPv/+/8PAP3/+/8PAP//+/8PAAEA+/8PAAMA+/8PAAUA/P/w//7//P/w/wAA/P/w/wIA/P/y//j//P/y//r//P/y//z//P/y//7//P/y/wAA/P/y/wIA/P/y/wQA/P/y/wYA/P/y/wgA/P/0//b//P/0//j//P/0//r//P/0//z//P/0//7//P/0/wAA/P/0/wIA/P/0/wQA/P/0/wYA/P/0/wgA/P/0/woA/P/2//T//P/2//b//P/2//j//P/2//r//P/2//z//P/2//7//P/2/wAA/P/2/wIA/P/2/wQA/P/2/wYA/P/2/wgA/P/2/woA/P/2/wwA/P/4//L//P/4//T//P/4//b//P/4//j//P/4//r//P/4//z//P/4//7//P/4/wAA/P/4/wIA/P/4/wQA/P/4/wYA/P/4/wgA/P/4/woA/P/4/wwA/P/4/w4A/P/6//L//P/6//T//P/6//b//P/6//j//P/6//r//P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/6/wYA/P/6/wgA/P/6/woA/P/6/wwA/P/6/w4A/P/8//L//P/8//T//P/8//b//P/8//j//P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/8/wgA/P/8/woA/P/8/wwA/P/8/w4A/P/+//D//P/+//L//P/+//T//P/+//b//P/+//j//P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P/+/wgA/P/+/woA/P/+/wwA/P/+/w4A/P/+/xAA/P8AAPD//P8AAPL//P8AAPT//P8AAPb//P8AAPj//P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8AAAgA/P8AAAoA/P8AAAwA/P8AAA4A/P8AABAA/P8CAPD//P8CAPL//P8CAPT//P8CAPb//P8CAPj//P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8CAAgA/P8CAAoA/P8CAAwA/P8CAA4A/P8CABAA/P8EAPL//P8EAPT//P8EAPb//P8EAPj//P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8EAAgA/P8EAAoA/P8EAAwA/P8EAA4A/P8GAPL//P8GAPT//P8GAPb//P8GAPj//P8GAPr//P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8GAAYA/P8GAAgA/P8GAAoA/P8GAAwA/P8GAA4A/P8IAPL//P8IAPT//P8IAPb//P8IAPj//P8IAPr//P8IAPz//P8IAP7//P8IAAAA/P8IAAIA/P8IAAQA/P8IAAYA/P8IAAgA/P8IAAoA/P8IAAwA/P8IAA4A/P8KAPT//P8KAPb//P8KAPj//P8KAPr//P8KAPz//P8KAP7//P8KAAAA/P8KAAIA/P8KAAQA/P8KAAYA/P8KAAgA/P8KAAoA/P8KAAwA/P8MAPb//P8MAPj//P8MAPr//P8MAPz//P8MAP7//P8MAAAA/P8MAAIA/P8MAAQA/P8MAAYA/P8MAAgA/P8MAAoA/P8OAPj//P8OAPr//P8OAPz//P8OAP7//P8OAAAA/P8OAAIA/P8OAAQA/P8OAAYA/P8OAAgA/P8QAP7//P8QAAAA/P8QAAIA/f/x//n//f/x//v//f/x//3//f/x/////f/x/wEA/f/x/wMA/f/x/wUA/f/x/wcA/f/z//f//f/z//n//f/z//v//f/z//3//f/z/////f/z/wEA/f/z/wMA/f/z/wUA/f/z/wcA/f/z/wkA/f/1//X//f/1//f//f/1//n//f/1//v//f/1//3//f/1/////f/1/wEA/f/1/wMA/f/1/wUA/f/1/wcA/f/1/wkA/f/1/wsA/f/3//P//f/3//X//f/3//f//f/3//n//f/3//v//f/3//3//f/3/////f/3/wEA/f/3/wMA/f/3/wUA/f/3/wcA/f/3/wkA/f/3/wsA/f/3/w0A/f/5//H//f/5//P//f/5//X//f/5//f//f/5//n//f/5//v//f/5//3//f/5/////f/5/wEA/f/5/wMA/f/5/wUA/f/5/wcA/f/5/wkA/f/5/wsA/f/5/w0A/f/5/w8A/f/7//H//f/7//P//f/7//X//f/7//f//f/7//n//f/7//v//f/7//3//f/7/////f/7/wEA/f/7/wMA/f/7/wUA/f/7/wcA/f/7/wkA/f/7/wsA/f/7/w0A/f/7/w8A/f/9//H//f/9//P//f/9//X//f/9//f//f/9//n//f/9//v//f/9//3//f/9/////f/9/wEA/f/9/wMA/f/9/wUA/f/9/wcA/f/9/wkA/f/9/wsA/f/9/w0A/f/9/w8A/f////H//f////P//f////X//f////f//f////n//f////v//f////3//f///////f///wEA/f///wMA/f///wUA/f///wcA/f///wkA/f///wsA/f///w0A/f///w8A/f8BAPH//f8BAPP//f8BAPX//f8BAPf//f8BAPn//f8BAPv//f8BAP3//f8BAP///f8BAAEA/f8BAAMA/f8BAAUA/f8BAAcA/f8BAAkA/f8BAAsA/f8BAA0A/f8BAA8A/f8DAPH//f8DAPP//f8DAPX//f8DAPf//f8DAPn//f8DAPv//f8DAP3//f8DAP///f8DAAEA/f8DAAMA/f8DAAUA/f8DAAcA/f8DAAkA/f8DAAsA/f8DAA0A/f8DAA8A/f8FAPH//f8FAPP//f8FAPX//f8FAPf//f8FAPn//f8FAPv//f8FAP3//f8FAP///f8FAAEA/f8FAAMA/f8FAAUA/f8FAAcA/f8FAAkA/f8FAAsA/f8FAA0A/f8FAA8A/f8HAPH//f8HAPP//f8HAPX//f8HAPf//f8HAPn//f8HAPv//f8HAP3//f8HAP///f8HAAEA/f8HAAMA/f8HAAUA/f8HAAcA/f8HAAkA/f8HAAsA/f8HAA0A/f8HAA8A/f8JAPP//f8JAPX//f8JAPf//f8JAPn//f8JAPv//f8JAP3//f8JAP///f8JAAEA/f8JAAMA/f8JAAUA/f8JAAcA/f8JAAkA/f8JAAsA/f8JAA0A/f8LAPX//f8LAPf//f8LAPn//f8LAPv//f8LAP3//f8LAP///f8LAAEA/f8LAAMA/f8LAAUA/f8LAAcA/f8LAAkA/f8LAAsA/f8NAPf//f8NAPn//f8NAPv//f8NAP3//f8NAP///f8NAAEA/f8NAAMA/f8NAAUA/f8NAAcA/f8NAAkA/f8PAPn//f8PAPv//f8PAP3//f8PAP///f8PAAEA/f8PAAMA/f8PAAUA/f8PAAcA/v/w//z//v/w//7//v/w/wAA/v/w/wIA/v/w/wQA/v/y//j//v/y//r//v/y//z//v/y//7//v/y/wAA/v/y/wIA/v/y/wQA/v/y/wYA/v/y/wgA/v/0//b//v/0//j//v/0//r//v/0//z//v/0//7//v/0/wAA/v/0/wIA/v/0/wQA/v/0/wYA/v/0/wgA/v/0/woA/v/2//T//v/2//b//v/2//j//v/2//r//v/2//z//v/2//7//v/2/wAA/v/2/wIA/v/2/wQA/v/2/wYA/v/2/wgA/v/2/woA/v/2/wwA/v/4//L//v/4//T//v/4//b//v/4//j//v/4//r//v/4//z//v/4//7//v/4/wAA/v/4/wIA/v/4/wQA/v/4/wYA/v/4/wgA/v/4/woA/v/4/wwA/v/4/w4A/v/6//L//v/6//T//v/6//b//v/6//j//v/6//r//v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/6/wYA/v/6/wgA/v/6/woA/v/6/wwA/v/6/w4A/v/8//D//v/8//L//v/8//T//v/8//b//v/8//j//v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/8/wgA/v/8/woA/v/8/wwA/v/8/w4A/v/8/xAA/v/+//D//v/+//L//v/+//T//v/+//b//v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v/+/woA/v/+/wwA/v/+/w4A/v/+/xAA/v8AAPD//v8AAPL//v8AAPT//v8AAPb//v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8AAAoA/v8AAAwA/v8AAA4A/v8AABAA/v8CAPD//v8CAPL//v8CAPT//v8CAPb//v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8CAAoA/v8CAAwA/v8CAA4A/v8CABAA/v8EAPD//v8EAPL//v8EAPT//v8EAPb//v8EAPj//v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8EAAgA/v8EAAoA/v8EAAwA/v8EAA4A/v8EABAA/v8GAPL//v8GAPT//v8GAPb//v8GAPj//v8GAPr//v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8GAAYA/v8GAAgA/v8GAAoA/v8GAAwA/v8GAA4A/v8IAPL//v8IAPT//v8IAPb//v8IAPj//v8IAPr//v8IAPz//v8IAP7//v8IAAAA/v8IAAIA/v8IAAQA/v8IAAYA/v8IAAgA/v8IAAoA/v8IAAwA/v8IAA4A/v8KAPT//v8KAPb//v8KAPj//v8KAPr//v8KAPz//v8KAP7//v8KAAAA/v8KAAIA/v8KAAQA/v8KAAYA/v8KAAgA/v8KAAoA/v8KAAwA/v8MAPb//v8MAPj//v8MAPr//v8MAPz//v8MAP7//v8MAAAA/v8MAAIA/v8MAAQA/v8MAAYA/v8MAAgA/v8MAAoA/v8OAPj//v8OAPr//v8OAPz//v8OAP7//v8OAAAA/v8OAAIA/v8OAAQA/v8OAAYA/v8OAAgA/v8QAPz//v8QAP7//v8QAAAA/v8QAAIA/v8QAAQA///x//n////x//v////x//3////x///////x/wEA///x/wMA///x/wUA///x/wcA///z//f////z//n////z//v////z//3////z///////z/wEA///z/wMA///z/wUA///z/wcA///z/wkA///1//X////1//f////1//n////1//v////1//3////1///////1/wEA///1/wMA///1/wUA///1/wcA///1/wkA///1/wsA///3//P////3//X////3//f////3//n////3//v////3//3////3///////3/wEA///3/wMA///3/wUA///3/wcA///3/wkA///3/wsA///3/w0A///5//H////5//P////5//X////5//f////5//n////5//v////5//3////5///////5/wEA///5/wMA///5/wUA///5/wcA///5/wkA///5/wsA///5/w0A///5/w8A///7//H////7//P////7//X////7//f////7//n////7//v////7//3////7///////7/wEA///7/wMA///7/wUA///7/wcA///7/wkA///7/wsA///7/w0A///7/w8A///9//H////9//P////9//X////9//f////9//n////9//v////9//3////9///////9/wEA///9/wMA///9/wUA///9/wcA///9/wkA///9/wsA///9/w0A///9/w8A//////H///////P///////X///////f///////n///////v///////3//////////////wEA/////wMA/////wUA/////wcA/////wkA/////wsA/////w0A/////w8A//8BAPH///8BAPP///8BAPX///8BAPf///8BAPn///8BAPv///8BAP3///8BAP////8BAAEA//8BAAMA//8BAAUA//8BAAcA//8BAAkA//8BAAsA//8BAA0A//8BAA8A//8DAPH///8DAPP///8DAPX///8DAPf///8DAPn///8DAPv///8DAP3///8DAP////8DAAEA//8DAAMA//8DAAUA//8DAAcA//8DAAkA//8DAAsA//8DAA0A//8DAA8A//8FAPH///8FAPP///8FAPX///8FAPf///8FAPn///8FAPv///8FAP3///8FAP////8FAAEA//8FAAMA//8FAAUA//8FAAcA//8FAAkA//8FAAsA//8FAA0A//8FAA8A//8HAPH///8HAPP///8HAPX///8HAPf///8HAPn///8HAPv///8HAP3///8HAP////8HAAEA//8HAAMA//8HAAUA//8HAAcA//8HAAkA//8HAAsA//8HAA0A//8HAA8A//8JAPP///8JAPX///8JAPf///8JAPn///8JAPv///8JAP3///8JAP////8JAAEA//8JAAMA//8JAAUA//8JAAcA//8JAAkA//8JAAsA//8JAA0A//8LAPX///8LAPf///8LAPn///8LAPv///8LAP3///8LAP////8LAAEA//8LAAMA//8LAAUA//8LAAcA//8LAAkA//8LAAsA//8NAPf///8NAPn///8NAPv///8NAP3///8NAP////8NAAEA//8NAAMA//8NAAUA//8NAAcA//8NAAkA//8PAPn///8PAPv///8PAP3///8PAP////8PAAEA//8PAAMA//8PAAUA//8PAAcAAADw//z/AADw//7/AADw/wAAAADw/wIAAADw/wQAAADy//j/AADy//r/AADy//z/AADy//7/AADy/wAAAADy/wIAAADy/wQAAADy/wYAAADy/wgAAAD0//b/AAD0//j/AAD0//r/AAD0//z/AAD0//7/AAD0/wAAAAD0/wIAAAD0/wQAAAD0/wYAAAD0/wgAAAD0/woAAAD2//T/AAD2//b/AAD2//j/AAD2//r/AAD2//z/AAD2//7/AAD2/wAAAAD2/wIAAAD2/wQAAAD2/wYAAAD2/wgAAAD2/woAAAD2/wwAAAD4//L/AAD4//T/AAD4//b/AAD4//j/AAD4//r/AAD4//z/AAD4//7/AAD4/wAAAAD4/wIAAAD4/wQAAAD4/wYAAAD4/wgAAAD4/woAAAD4/wwAAAD4/w4AAAD6//L/AAD6//T/AAD6//b/AAD6//j/AAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD6/wgAAAD6/woAAAD6/wwAAAD6/w4AAAD8//D/AAD8//L/AAD8//T/AAD8//b/AAD8//j/AAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD8/wgAAAD8/woAAAD8/wwAAAD8/w4AAAD8/xAAAAD+//D/AAD+//L/AAD+//T/AAD+//b/AAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD+/woAAAD+/wwAAAD+/w4AAAD+/xAAAAAAAPD/AAAAAPL/AAAAAPT/AAAAAPb/AAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAAAAAoAAAAAAAwAAAAAAA4AAAAAABAAAAACAPD/AAACAPL/AAACAPT/AAACAPb/AAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAACAAoAAAACAAwAAAACAA4AAAACABAAAAAEAPD/AAAEAPL/AAAEAPT/AAAEAPb/AAAEAPj/AAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAEAAgAAAAEAAoAAAAEAAwAAAAEAA4AAAAEABAAAAAGAPL/AAAGAPT/AAAGAPb/AAAGAPj/AAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAGAAgAAAAGAAoAAAAGAAwAAAAGAA4AAAAIAPL/AAAIAPT/AAAIAPb/AAAIAPj/AAAIAPr/AAAIAPz/AAAIAP7/AAAIAAAAAAAIAAIAAAAIAAQAAAAIAAYAAAAIAAgAAAAIAAoAAAAIAAwAAAAIAA4AAAAKAPT/AAAKAPb/AAAKAPj/AAAKAPr/AAAKAPz/AAAKAP7/AAAKAAAAAAAKAAIAAAAKAAQAAAAKAAYAAAAKAAgAAAAKAAoAAAAKAAwAAAAMAPb/AAAMAPj/AAAMAPr/AAAMAPz/AAAMAP7/AAAMAAAAAAAMAAIAAAAMAAQAAAAMAAYAAAAMAAgAAAAMAAoAAAAOAPj/AAAOAPr/AAAOAPz/AAAOAP7/AAAOAAAAAAAOAAIAAAAOAAQAAAAOAAYAAAAOAAgAAAAQAPz/AAAQAP7/AAAQAAAAAAAQAAIAAAAQAAQAAQDx//n/AQDx//v/AQDx//3/AQDx////AQDx/wEAAQDx/wMAAQDx/wUAAQDx/wcAAQDz//f/AQDz//n/AQDz//v/AQDz//3/AQDz////AQDz/wEAAQDz/wMAAQDz/wUAAQDz/wcAAQDz/wkAAQD1//X/AQD1//f/AQD1//n/AQD1//v/AQD1//3/AQD1////AQD1/wEAAQD1/wMAAQD1/wUAAQD1/wcAAQD1/wkAAQD1/wsAAQD3//P/AQD3//X/AQD3//f/AQD3//n/AQD3//v/AQD3//3/AQD3////AQD3/wEAAQD3/wMAAQD3/wUAAQD3/wcAAQD3/wkAAQD3/wsAAQD3/w0AAQD5//H/AQD5//P/AQD5//X/AQD5//f/AQD5//n/AQD5//v/AQD5//3/AQD5////AQD5/wEAAQD5/wMAAQD5/wUAAQD5/wcAAQD5/wkAAQD5/wsAAQD5/w0AAQD5/w8AAQD7//H/AQD7//P/AQD7//X/AQD7//f/AQD7//n/AQD7//v/AQD7//3/AQD7////AQD7/wEAAQD7/wMAAQD7/wUAAQD7/wcAAQD7/wkAAQD7/wsAAQD7/w0AAQD7/w8AAQD9//H/AQD9//P/AQD9//X/AQD9//f/AQD9//n/AQD9//v/AQD9//3/AQD9////AQD9/wEAAQD9/wMAAQD9/wUAAQD9/wcAAQD9/wkAAQD9/wsAAQD9/w0AAQD9/w8AAQD///H/AQD///P/AQD///X/AQD///f/AQD///n/AQD///v/AQD///3/AQD/////AQD//wEAAQD//wMAAQD//wUAAQD//wcAAQD//wkAAQD//wsAAQD//w0AAQD//w8AAQABAPH/AQABAPP/AQABAPX/AQABAPf/AQABAPn/AQABAPv/AQABAP3/AQABAP//AQABAAEAAQABAAMAAQABAAUAAQABAAcAAQABAAkAAQABAAsAAQABAA0AAQABAA8AAQADAPH/AQADAPP/AQADAPX/AQADAPf/AQADAPn/AQADAPv/AQADAP3/AQADAP//AQADAAEAAQADAAMAAQADAAUAAQADAAcAAQADAAkAAQADAAsAAQADAA0AAQADAA8AAQAFAPH/AQAFAPP/AQAFAPX/AQAFAPf/AQAFAPn/AQAFAPv/AQAFAP3/AQAFAP//AQAFAAEAAQAFAAMAAQAFAAUAAQAFAAcAAQAFAAkAAQAFAAsAAQAFAA0AAQAFAA8AAQAHAPH/AQAHAPP/AQAHAPX/AQAHAPf/AQAHAPn/AQAHAPv/AQAHAP3/AQAHAP//AQAHAAEAAQAHAAMAAQAHAAUAAQAHAAcAAQAHAAkAAQAHAAsAAQAHAA0AAQAHAA8AAQAJAPP/AQAJAPX/AQAJAPf/AQAJAPn/AQAJAPv/AQAJAP3/AQAJAP//AQAJAAEAAQAJAAMAAQAJAAUAAQAJAAcAAQAJAAkAAQAJAAsAAQAJAA0AAQALAPX/AQALAPf/AQALAPn/AQALAPv/AQALAP3/AQALAP//AQALAAEAAQALAAMAAQALAAUAAQALAAcAAQALAAkAAQALAAsAAQANAPf/AQANAPn/AQANAPv/AQANAP3/AQANAP//AQANAAEAAQANAAMAAQANAAUAAQANAAcAAQANAAkAAQAPAPn/AQAPAPv/AQAPAP3/AQAPAP//AQAPAAEAAQAPAAMAAQAPAAUAAQAPAAcAAgDw//z/AgDw//7/AgDw/wAAAgDw/wIAAgDw/wQAAgDy//j/AgDy//r/AgDy//z/AgDy//7/AgDy/wAAAgDy/wIAAgDy/wQAAgDy/wYAAgDy/wgAAgD0//b/AgD0//j/AgD0//r/AgD0//z/AgD0//7/AgD0/wAAAgD0/wIAAgD0/wQAAgD0/wYAAgD0/wgAAgD0/woAAgD2//T/AgD2//b/AgD2//j/AgD2//r/AgD2//z/AgD2//7/AgD2/wAAAgD2/wIAAgD2/wQAAgD2/wYAAgD2/wgAAgD2/woAAgD2/wwAAgD4//L/AgD4//T/AgD4//b/AgD4//j/AgD4//r/AgD4//z/AgD4//7/AgD4/wAAAgD4/wIAAgD4/wQAAgD4/wYAAgD4/wgAAgD4/woAAgD4/wwAAgD4/w4AAgD6//L/AgD6//T/AgD6//b/AgD6//j/AgD6//r/AgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD6/wYAAgD6/wgAAgD6/woAAgD6/wwAAgD6/w4AAgD8//D/AgD8//L/AgD8//T/AgD8//b/AgD8//j/AgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD8/wgAAgD8/woAAgD8/wwAAgD8/w4AAgD8/xAAAgD+//D/AgD+//L/AgD+//T/AgD+//b/AgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD+/woAAgD+/wwAAgD+/w4AAgD+/xAAAgAAAPD/AgAAAPL/AgAAAPT/AgAAAPb/AgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgAAAAoAAgAAAAwAAgAAAA4AAgAAABAAAgACAPD/AgACAPL/AgACAPT/AgACAPb/AgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgACAAoAAgACAAwAAgACAA4AAgACABAAAgAEAPD/AgAEAPL/AgAEAPT/AgAEAPb/AgAEAPj/AgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAEAAgAAgAEAAoAAgAEAAwAAgAEAA4AAgAEABAAAgAGAPL/AgAGAPT/AgAGAPb/AgAGAPj/AgAGAPr/AgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAGAAYAAgAGAAgAAgAGAAoAAgAGAAwAAgAGAA4AAgAIAPL/AgAIAPT/AgAIAPb/AgAIAPj/AgAIAPr/AgAIAPz/AgAIAP7/AgAIAAAAAgAIAAIAAgAIAAQAAgAIAAYAAgAIAAgAAgAIAAoAAgAIAAwAAgAIAA4AAgAKAPT/AgAKAPb/AgAKAPj/AgAKAPr/AgAKAPz/AgAKAP7/AgAKAAAAAgAKAAIAAgAKAAQAAgAKAAYAAgAKAAgAAgAKAAoAAgAKAAwAAgAMAPb/AgAMAPj/AgAMAPr/AgAMAPz/AgAMAP7/AgAMAAAAAgAMAAIAAgAMAAQAAgAMAAYAAgAMAAgAAgAMAAoAAgAOAPj/AgAOAPr/AgAOAPz/AgAOAP7/AgAOAAAAAgAOAAIAAgAOAAQAAgAOAAYAAgAOAAgAAgAQAPz/AgAQAP7/AgAQAAAAAgAQAAIAAgAQAAQAAwDx//n/AwDx//v/AwDx//3/AwDx////AwDx/wEAAwDx/wMAAwDx/wUAAwDx/wcAAwDz//f/AwDz//n/AwDz//v/AwDz//3/AwDz////AwDz/wEAAwDz/wMAAwDz/wUAAwDz/wcAAwDz/wkAAwD1//X/AwD1//f/AwD1//n/AwD1//v/AwD1//3/AwD1////AwD1/wEAAwD1/wMAAwD1/wUAAwD1/wcAAwD1/wkAAwD1/wsAAwD3//P/AwD3//X/AwD3//f/AwD3//n/AwD3//v/AwD3//3/AwD3////AwD3/wEAAwD3/wMAAwD3/wUAAwD3/wcAAwD3/wkAAwD3/wsAAwD3/w0AAwD5//H/AwD5//P/AwD5//X/AwD5//f/AwD5//n/AwD5//v/AwD5//3/AwD5////AwD5/wEAAwD5/wMAAwD5/wUAAwD5/wcAAwD5/wkAAwD5/wsAAwD5/w0AAwD5/w8AAwD7//H/AwD7//P/AwD7//X/AwD7//f/AwD7//n/AwD7//v/AwD7//3/AwD7////AwD7/wEAAwD7/wMAAwD7/wUAAwD7/wcAAwD7/wkAAwD7/wsAAwD7/w0AAwD7/w8AAwD9//H/AwD9//P/AwD9//X/AwD9//f/AwD9//n/AwD9//v/AwD9//3/AwD9////AwD9/wEAAwD9/wMAAwD9/wUAAwD9/wcAAwD9/wkAAwD9/wsAAwD9/w0AAwD9/w8AAwD///H/AwD///P/AwD///X/AwD///f/AwD///n/AwD///v/AwD///3/AwD/////AwD//wEAAwD//wMAAwD//wUAAwD//wcAAwD//wkAAwD//wsAAwD//w0AAwD//w8AAwABAPH/AwABAPP/AwABAPX/AwABAPf/AwABAPn/AwABAPv/AwABAP3/AwABAP//AwABAAEAAwABAAMAAwABAAUAAwABAAcAAwABAAkAAwABAAsAAwABAA0AAwABAA8AAwADAPH/AwADAPP/AwADAPX/AwADAPf/AwADAPn/AwADAPv/AwADAP3/AwADAP//AwADAAEAAwADAAMAAwADAAUAAwADAAcAAwADAAkAAwADAAsAAwADAA0AAwADAA8AAwAFAPH/AwAFAPP/AwAFAPX/AwAFAPf/AwAFAPn/AwAFAPv/AwAFAP3/AwAFAP//AwAFAAEAAwAFAAMAAwAFAAUAAwAFAAcAAwAFAAkAAwAFAAsAAwAFAA0AAwAFAA8AAwAHAPH/AwAHAPP/AwAHAPX/AwAHAPf/AwAHAPn/AwAHAPv/AwAHAP3/AwAHAP//AwAHAAEAAwAHAAMAAwAHAAUAAwAHAAcAAwAHAAkAAwAHAAsAAwAHAA0AAwAHAA8AAwAJAPP/AwAJAPX/AwAJAPf/AwAJAPn/AwAJAPv/AwAJAP3/AwAJAP//AwAJAAEAAwAJAAMAAwAJAAUAAwAJAAcAAwAJAAkAAwAJAAsAAwAJAA0AAwALAPX/AwALAPf/AwALAPn/AwALAPv/AwALAP3/AwALAP//AwALAAEAAwALAAMAAwALAAUAAwALAAcAAwALAAkAAwALAAsAAwANAPf/AwANAPn/AwANAPv/AwANAP3/AwANAP//AwANAAEAAwANAAMAAwANAAUAAwANAAcAAwANAAkAAwAPAPn/AwAPAPv/AwAPAP3/AwAPAP//AwAPAAEAAwAPAAMAAwAPAAUAAwAPAAcABADw//7/BADw/wAABADw/wIABADy//j/BADy//r/BADy//z/BADy//7/BADy/wAABADy/wIABADy/wQABADy/wYABADy/wgABAD0//b/BAD0//j/BAD0//r/BAD0//z/BAD0//7/BAD0/wAABAD0/wIABAD0/wQABAD0/wYABAD0/wgABAD0/woABAD2//T/BAD2//b/BAD2//j/BAD2//r/BAD2//z/BAD2//7/BAD2/wAABAD2/wIABAD2/wQABAD2/wYABAD2/wgABAD2/woABAD2/wwABAD4//L/BAD4//T/BAD4//b/BAD4//j/BAD4//r/BAD4//z/BAD4//7/BAD4/wAABAD4/wIABAD4/wQABAD4/wYABAD4/wgABAD4/woABAD4/wwABAD4/w4ABAD6//L/BAD6//T/BAD6//b/BAD6//j/BAD6//r/BAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD6/wYABAD6/wgABAD6/woABAD6/wwABAD6/w4ABAD8//L/BAD8//T/BAD8//b/BAD8//j/BAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD8/wgABAD8/woABAD8/wwABAD8/w4ABAD+//D/BAD+//L/BAD+//T/BAD+//b/BAD+//j/BAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD+/wgABAD+/woABAD+/wwABAD+/w4ABAD+/xAABAAAAPD/BAAAAPL/BAAAAPT/BAAAAPb/BAAAAPj/BAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAAAAAgABAAAAAoABAAAAAwABAAAAA4ABAAAABAABAACAPD/BAACAPL/BAACAPT/BAACAPb/BAACAPj/BAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAACAAgABAACAAoABAACAAwABAACAA4ABAACABAABAAEAPL/BAAEAPT/BAAEAPb/BAAEAPj/BAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAEAAgABAAEAAoABAAEAAwABAAEAA4ABAAGAPL/BAAGAPT/BAAGAPb/BAAGAPj/BAAGAPr/BAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAGAAYABAAGAAgABAAGAAoABAAGAAwABAAGAA4ABAAIAPL/BAAIAPT/BAAIAPb/BAAIAPj/BAAIAPr/BAAIAPz/BAAIAP7/BAAIAAAABAAIAAIABAAIAAQABAAIAAYABAAIAAgABAAIAAoABAAIAAwABAAIAA4ABAAKAPT/BAAKAPb/BAAKAPj/BAAKAPr/BAAKAPz/BAAKAP7/BAAKAAAABAAKAAIABAAKAAQABAAKAAYABAAKAAgABAAKAAoABAAKAAwABAAMAPb/BAAMAPj/BAAMAPr/BAAMAPz/BAAMAP7/BAAMAAAABAAMAAIABAAMAAQABAAMAAYABAAMAAgABAAMAAoABAAOAPj/BAAOAPr/BAAOAPz/BAAOAP7/BAAOAAAABAAOAAIABAAOAAQABAAOAAYABAAOAAgABAAQAP7/BAAQAAAABAAQAAIABQDx//v/BQDx//3/BQDx////BQDx/wEABQDx/wMABQDx/wUABQDz//f/BQDz//n/BQDz//v/BQDz//3/BQDz////BQDz/wEABQDz/wMABQDz/wUABQDz/wcABQDz/wkABQD1//X/BQD1//f/BQD1//n/BQD1//v/BQD1//3/BQD1////BQD1/wEABQD1/wMABQD1/wUABQD1/wcABQD1/wkABQD1/wsABQD3//P/BQD3//X/BQD3//f/BQD3//n/BQD3//v/BQD3//3/BQD3////BQD3/wEABQD3/wMABQD3/wUABQD3/wcABQD3/wkABQD3/wsABQD3/w0ABQD5//P/BQD5//X/BQD5//f/BQD5//n/BQD5//v/BQD5//3/BQD5////BQD5/wEABQD5/wMABQD5/wUABQD5/wcABQD5/wkABQD5/wsABQD5/w0ABQD7//H/BQD7//P/BQD7//X/BQD7//f/BQD7//n/BQD7//v/BQD7//3/BQD7////BQD7/wEABQD7/wMABQD7/wUABQD7/wcABQD7/wkABQD7/wsABQD7/w0ABQD7/w8ABQD9//H/BQD9//P/BQD9//X/BQD9//f/BQD9//n/BQD9//v/BQD9//3/BQD9////BQD9/wEABQD9/wMABQD9/wUABQD9/wcABQD9/wkABQD9/wsABQD9/w0ABQD9/w8ABQD///H/BQD///P/BQD///X/BQD///f/BQD///n/BQD///v/BQD///3/BQD/////BQD//wEABQD//wMABQD//wUABQD//wcABQD//wkABQD//wsABQD//w0ABQD//w8ABQABAPH/BQABAPP/BQABAPX/BQABAPf/BQABAPn/BQABAPv/BQABAP3/BQABAP//BQABAAEABQABAAMABQABAAUABQABAAcABQABAAkABQABAAsABQABAA0ABQABAA8ABQADAPH/BQADAPP/BQADAPX/BQADAPf/BQADAPn/BQADAPv/BQADAP3/BQADAP//BQADAAEABQADAAMABQADAAUABQADAAcABQADAAkABQADAAsABQADAA0ABQADAA8ABQAFAPH/BQAFAPP/BQAFAPX/BQAFAPf/BQAFAPn/BQAFAPv/BQAFAP3/BQAFAP//BQAFAAEABQAFAAMABQAFAAUABQAFAAcABQAFAAkABQAFAAsABQAFAA0ABQAFAA8ABQAHAPP/BQAHAPX/BQAHAPf/BQAHAPn/BQAHAPv/BQAHAP3/BQAHAP//BQAHAAEABQAHAAMABQAHAAUABQAHAAcABQAHAAkABQAHAAsABQAHAA0ABQAJAPP/BQAJAPX/BQAJAPf/BQAJAPn/BQAJAPv/BQAJAP3/BQAJAP//BQAJAAEABQAJAAMABQAJAAUABQAJAAcABQAJAAkABQAJAAsABQAJAA0ABQALAPX/BQALAPf/BQALAPn/BQALAPv/BQALAP3/BQALAP//BQALAAEABQALAAMABQALAAUABQALAAcABQALAAkABQALAAsABQANAPf/BQANAPn/BQANAPv/BQANAP3/BQANAP//BQANAAEABQANAAMABQANAAUABQANAAcABQANAAkABQAPAPv/BQAPAP3/BQAPAP//BQAPAAEABQAPAAMABQAPAAUABgDy//r/BgDy//z/BgDy//7/BgDy/wAABgDy/wIABgDy/wQABgDy/wYABgD0//b/BgD0//j/BgD0//r/BgD0//z/BgD0//7/BgD0/wAABgD0/wIABgD0/wQABgD0/wYABgD0/wgABgD0/woABgD2//T/BgD2//b/BgD2//j/BgD2//r/BgD2//z/BgD2//7/BgD2/wAABgD2/wIABgD2/wQABgD2/wYABgD2/wgABgD2/woABgD2/wwABgD4//T/BgD4//b/BgD4//j/BgD4//r/BgD4//z/BgD4//7/BgD4/wAABgD4/wIABgD4/wQABgD4/wYABgD4/wgABgD4/woABgD4/wwABgD6//L/BgD6//T/BgD6//b/BgD6//j/BgD6//r/BgD6//z/BgD6//7/BgD6/wAABgD6/wIABgD6/wQABgD6/wYABgD6/wgABgD6/woABgD6/wwABgD6/w4ABgD8//L/BgD8//T/BgD8//b/BgD8//j/BgD8//r/BgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD8/wYABgD8/wgABgD8/woABgD8/wwABgD8/w4ABgD+//L/BgD+//T/BgD+//b/BgD+//j/BgD+//r/BgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD+/wYABgD+/wgABgD+/woABgD+/wwABgD+/w4ABgAAAPL/BgAAAPT/BgAAAPb/BgAAAPj/BgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgAAAAgABgAAAAoABgAAAAwABgAAAA4ABgACAPL/BgACAPT/BgACAPb/BgACAPj/BgACAPr/BgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgACAAYABgACAAgABgACAAoABgACAAwABgACAA4ABgAEAPL/BgAEAPT/BgAEAPb/BgAEAPj/BgAEAPr/BgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAEAAYABgAEAAgABgAEAAoABgAEAAwABgAEAA4ABgAGAPL/BgAGAPT/BgAGAPb/BgAGAPj/BgAGAPr/BgAGAPz/BgAGAP7/BgAGAAAABgAGAAIABgAGAAQABgAGAAYABgAGAAgABgAGAAoABgAGAAwABgAGAA4ABgAIAPT/BgAIAPb/BgAIAPj/BgAIAPr/BgAIAPz/BgAIAP7/BgAIAAAABgAIAAIABgAIAAQABgAIAAYABgAIAAgABgAIAAoABgAIAAwABgAKAPT/BgAKAPb/BgAKAPj/BgAKAPr/BgAKAPz/BgAKAP7/BgAKAAAABgAKAAIABgAKAAQABgAKAAYABgAKAAgABgAKAAoABgAKAAwABgAMAPb/BgAMAPj/BgAMAPr/BgAMAPz/BgAMAP7/BgAMAAAABgAMAAIABgAMAAQABgAMAAYABgAMAAgABgAMAAoABgAOAPr/BgAOAPz/BgAOAP7/BgAOAAAABgAOAAIABgAOAAQABgAOAAYABwDx//3/BwDx////BwDx/wEABwDx/wMABwDz//n/BwDz//v/BwDz//3/BwDz////BwDz/wEABwDz/wMABwDz/wUABwDz/wcABwD1//f/BwD1//n/BwD1//v/BwD1//3/BwD1////BwD1/wEABwD1/wMABwD1/wUABwD1/wcABwD1/wkABwD3//X/BwD3//f/BwD3//n/BwD3//v/BwD3//3/BwD3////BwD3/wEABwD3/wMABwD3/wUABwD3/wcABwD3/wkABwD3/wsABwD5//P/BwD5//X/BwD5//f/BwD5//n/BwD5//v/BwD5//3/BwD5////BwD5/wEABwD5/wMABwD5/wUABwD5/wcABwD5/wkABwD5/wsABwD5/w0ABwD7//P/BwD7//X/BwD7//f/BwD7//n/BwD7//v/BwD7//3/BwD7////BwD7/wEABwD7/wMABwD7/wUABwD7/wcABwD7/wkABwD7/wsABwD7/w0ABwD9//H/BwD9//P/BwD9//X/BwD9//f/BwD9//n/BwD9//v/BwD9//3/BwD9////BwD9/wEABwD9/wMABwD9/wUABwD9/wcABwD9/wkABwD9/wsABwD9/w0ABwD9/w8ABwD///H/BwD///P/BwD///X/BwD///f/BwD///n/BwD///v/BwD///3/BwD/////BwD//wEABwD//wMABwD//wUABwD//wcABwD//wkABwD//wsABwD//w0ABwD//w8ABwABAPH/BwABAPP/BwABAPX/BwABAPf/BwABAPn/BwABAPv/BwABAP3/BwABAP//BwABAAEABwABAAMABwABAAUABwABAAcABwABAAkABwABAAsABwABAA0ABwABAA8ABwADAPH/BwADAPP/BwADAPX/BwADAPf/BwADAPn/BwADAPv/BwADAP3/BwADAP//BwADAAEABwADAAMABwADAAUABwADAAcABwADAAkABwADAAsABwADAA0ABwADAA8ABwAFAPP/BwAFAPX/BwAFAPf/BwAFAPn/BwAFAPv/BwAFAP3/BwAFAP//BwAFAAEABwAFAAMABwAFAAUABwAFAAcABwAFAAkABwAFAAsABwAFAA0ABwAHAPP/BwAHAPX/BwAHAPf/BwAHAPn/BwAHAPv/BwAHAP3/BwAHAP//BwAHAAEABwAHAAMABwAHAAUABwAHAAcABwAHAAkABwAHAAsABwAHAA0ABwAJAPX/BwAJAPf/BwAJAPn/BwAJAPv/BwAJAP3/BwAJAP//BwAJAAEABwAJAAMABwAJAAUABwAJAAcABwAJAAkABwAJAAsABwALAPf/BwALAPn/BwALAPv/BwALAP3/BwALAP//BwALAAEABwALAAMABwALAAUABwALAAcABwALAAkABwANAPn/BwANAPv/BwANAP3/BwANAP//BwANAAEABwANAAMABwANAAUABwANAAcABwAPAP3/BwAPAP//BwAPAAEABwAPAAMACADy//z/CADy//7/CADy/wAACADy/wIACADy/wQACAD0//j/CAD0//r/CAD0//z/CAD0//7/CAD0/wAACAD0/wIACAD0/wQACAD0/wYACAD0/wgACAD2//b/CAD2//j/CAD2//r/CAD2//z/CAD2//7/CAD2/wAACAD2/wIACAD2/wQACAD2/wYACAD2/wgACAD2/woACAD4//T/CAD4//b/CAD4//j/CAD4//r/CAD4//z/CAD4//7/CAD4/wAACAD4/wIACAD4/wQACAD4/wYACAD4/wgACAD4/woACAD4/wwACAD6//T/CAD6//b/CAD6//j/CAD6//r/CAD6//z/CAD6//7/CAD6/wAACAD6/wIACAD6/wQACAD6/wYACAD6/wgACAD6/woACAD6/wwACAD8//L/CAD8//T/CAD8//b/CAD8//j/CAD8//r/CAD8//z/CAD8//7/CAD8/wAACAD8/wIACAD8/wQACAD8/wYACAD8/wgACAD8/woACAD8/wwACAD8/w4ACAD+//L/CAD+//T/CAD+//b/CAD+//j/CAD+//r/CAD+//z/CAD+//7/CAD+/wAACAD+/wIACAD+/wQACAD+/wYACAD+/wgACAD+/woACAD+/wwACAD+/w4ACAAAAPL/CAAAAPT/CAAAAPb/CAAAAPj/CAAAAPr/CAAAAPz/CAAAAP7/CAAAAAAACAAAAAIACAAAAAQACAAAAAYACAAAAAgACAAAAAoACAAAAAwACAAAAA4ACAACAPL/CAACAPT/CAACAPb/CAACAPj/CAACAPr/CAACAPz/CAACAP7/CAACAAAACAACAAIACAACAAQACAACAAYACAACAAgACAACAAoACAACAAwACAACAA4ACAAEAPL/CAAEAPT/CAAEAPb/CAAEAPj/CAAEAPr/CAAEAPz/CAAEAP7/CAAEAAAACAAEAAIACAAEAAQACAAEAAYACAAEAAgACAAEAAoACAAEAAwACAAEAA4ACAAGAPT/CAAGAPb/CAAGAPj/CAAGAPr/CAAGAPz/CAAGAP7/CAAGAAAACAAGAAIACAAGAAQACAAGAAYACAAGAAgACAAGAAoACAAGAAwACAAIAPT/CAAIAPb/CAAIAPj/CAAIAPr/CAAIAPz/CAAIAP7/CAAIAAAACAAIAAIACAAIAAQACAAIAAYACAAIAAgACAAIAAoACAAIAAwACAAKAPb/CAAKAPj/CAAKAPr/CAAKAPz/CAAKAP7/CAAKAAAACAAKAAIACAAKAAQACAAKAAYACAAKAAgACAAKAAoACAAMAPj/CAAMAPr/CAAMAPz/CAAMAP7/CAAMAAAACAAMAAIACAAMAAQACAAMAAYACAAMAAgACAAOAPz/CAAOAP7/CAAOAAAACAAOAAIACAAOAAQACQDz//v/CQDz//3/CQDz////CQDz/wEACQDz/wMACQDz/wUACQD1//f/CQD1//n/CQD1//v/CQD1//3/CQD1////CQD1/wEACQD1/wMACQD1/wUACQD1/wcACQD1/wkACQD3//X/CQD3//f/CQD3//n/CQD3//v/CQD3//3/CQD3////CQD3/wEACQD3/wMACQD3/wUACQD3/wcACQD3/wkACQD3/wsACQD5//X/CQD5//f/CQD5//n/CQD5//v/CQD5//3/CQD5////CQD5/wEACQD5/wMACQD5/wUACQD5/wcACQD5/wkACQD5/wsACQD7//P/CQD7//X/CQD7//f/CQD7//n/CQD7//v/CQD7//3/CQD7////CQD7/wEACQD7/wMACQD7/wUACQD7/wcACQD7/wkACQD7/wsACQD7/w0ACQD9//P/CQD9//X/CQD9//f/CQD9//n/CQD9//v/CQD9//3/CQD9////CQD9/wEACQD9/wMACQD9/wUACQD9/wcACQD9/wkACQD9/wsACQD9/w0ACQD///P/CQD///X/CQD///f/CQD///n/CQD///v/CQD///3/CQD/////CQD//wEACQD//wMACQD//wUACQD//wcACQD//wkACQD//wsACQD//w0ACQABAPP/CQABAPX/CQABAPf/CQABAPn/CQABAPv/CQABAP3/CQABAP//CQABAAEACQABAAMACQABAAUACQABAAcACQABAAkACQABAAsACQABAA0ACQADAPP/CQADAPX/CQADAPf/CQADAPn/CQADAPv/CQADAP3/CQADAP//CQADAAEACQADAAMACQADAAUACQADAAcACQADAAkACQADAAsACQADAA0ACQAFAPP/CQAFAPX/CQAFAPf/CQAFAPn/CQAFAPv/CQAFAP3/CQAFAP//CQAFAAEACQAFAAMACQAFAAUACQAFAAcACQAFAAkACQAFAAsACQAFAA0ACQAHAPX/CQAHAPf/CQAHAPn/CQAHAPv/CQAHAP3/CQAHAP//CQAHAAEACQAHAAMACQAHAAUACQAHAAcACQAHAAkACQAHAAsACQAJAPX/CQAJAPf/CQAJAPn/CQAJAPv/CQAJAP3/CQAJAP//CQAJAAEACQAJAAMACQAJAAUACQAJAAcACQAJAAkACQAJAAsACQALAPf/CQALAPn/CQALAPv/CQALAP3/CQALAP//CQALAAEACQALAAMACQALAAUACQALAAcACQALAAkACQANAPv/CQANAP3/CQANAP//CQANAAEACQANAAMACQANAAUACgD0//r/CgD0//z/CgD0//7/CgD0/wAACgD0/wIACgD0/wQACgD0/wYACgD2//j/CgD2//r/CgD2//z/CgD2//7/CgD2/wAACgD2/wIACgD2/wQACgD2/wYACgD2/wgACgD4//b/CgD4//j/CgD4//r/CgD4//z/CgD4//7/CgD4/wAACgD4/wIACgD4/wQACgD4/wYACgD4/wgACgD4/woACgD6//T/CgD6//b/CgD6//j/CgD6//r/CgD6//z/CgD6//7/CgD6/wAACgD6/wIACgD6/wQACgD6/wYACgD6/wgACgD6/woACgD6/wwACgD8//T/CgD8//b/CgD8//j/CgD8//r/CgD8//z/CgD8//7/CgD8/wAACgD8/wIACgD8/wQACgD8/wYACgD8/wgACgD8/woACgD8/wwACgD+//T/CgD+//b/CgD+//j/CgD+//r/CgD+//z/CgD+//7/CgD+/wAACgD+/wIACgD+/wQACgD+/wYACgD+/wgACgD+/woACgD+/wwACgAAAPT/CgAAAPb/CgAAAPj/CgAAAPr/CgAAAPz/CgAAAP7/CgAAAAAACgAAAAIACgAAAAQACgAAAAYACgAAAAgACgAAAAoACgAAAAwACgACAPT/CgACAPb/CgACAPj/CgACAPr/CgACAPz/CgACAP7/CgACAAAACgACAAIACgACAAQACgACAAYACgACAAgACgACAAoACgACAAwACgAEAPT/CgAEAPb/CgAEAPj/CgAEAPr/CgAEAPz/CgAEAP7/CgAEAAAACgAEAAIACgAEAAQACgAEAAYACgAEAAgACgAEAAoACgAEAAwACgAGAPT/CgAGAPb/CgAGAPj/CgAGAPr/CgAGAPz/CgAGAP7/CgAGAAAACgAGAAIACgAGAAQACgAGAAYACgAGAAgACgAGAAoACgAGAAwACgAIAPb/CgAIAPj/CgAIAPr/CgAIAPz/CgAIAP7/CgAIAAAACgAIAAIACgAIAAQACgAIAAYACgAIAAgACgAIAAoACgAKAPj/CgAKAPr/CgAKAPz/CgAKAP7/CgAKAAAACgAKAAIACgAKAAQACgAKAAYACgAKAAgACgAMAPr/CgAMAPz/CgAMAP7/CgAMAAAACgAMAAIACgAMAAQACgAMAAYACwD1//v/CwD1//3/CwD1////CwD1/wEACwD1/wMACwD1/wUACwD3//f/CwD3//n/CwD3//v/CwD3//3/CwD3////CwD3/wEACwD3/wMACwD3/wUACwD3/wcACwD3/wkACwD5//f/CwD5//n/CwD5//v/CwD5//3/CwD5////CwD5/wEACwD5/wMACwD5/wUACwD5/wcACwD5/wkACwD7//X/CwD7//f/CwD7//n/CwD7//v/CwD7//3/CwD7////CwD7/wEACwD7/wMACwD7/wUACwD7/wcACwD7/wkACwD7/wsACwD9//X/CwD9//f/CwD9//n/CwD9//v/CwD9//3/CwD9////CwD9/wEACwD9/wMACwD9/wUACwD9/wcACwD9/wkACwD9/wsACwD///X/CwD///f/CwD///n/CwD///v/CwD///3/CwD/////CwD//wEACwD//wMACwD//wUACwD//wcACwD//wkACwD//wsACwABAPX/CwABAPf/CwABAPn/CwABAPv/CwABAP3/CwABAP//CwABAAEACwABAAMACwABAAUACwABAAcACwABAAkACwABAAsACwADAPX/CwADAPf/CwADAPn/CwADAPv/CwADAP3/CwADAP//CwADAAEACwADAAMACwADAAUACwADAAcACwADAAkACwADAAsACwAFAPX/CwAFAPf/CwAFAPn/CwAFAPv/CwAFAP3/CwAFAP//CwAFAAEACwAFAAMACwAFAAUACwAFAAcACwAFAAkACwAFAAsACwAHAPf/CwAHAPn/CwAHAPv/CwAHAP3/CwAHAP//CwAHAAEACwAHAAMACwAHAAUACwAHAAcACwAHAAkACwAJAPf/CwAJAPn/CwAJAPv/CwAJAP3/CwAJAP//CwAJAAEACwAJAAMACwAJAAUACwAJAAcACwAJAAkACwALAPv/CwALAP3/CwALAP//CwALAAEACwALAAMACwALAAUADAD2//r/DAD2//z/DAD2//7/DAD2/wAADAD2/wIADAD2/wQADAD2/wYADAD4//j/DAD4//r/DAD4//z/DAD4//7/DAD4/wAADAD4/wIADAD4/wQADAD4/wYADAD4/wgADAD6//b/DAD6//j/DAD6//r/DAD6//z/DAD6//7/DAD6/wAADAD6/wIADAD6/wQADAD6/wYADAD6/wgADAD6/woADAD8//b/DAD8//j/DAD8//r/DAD8//z/DAD8//7/DAD8/wAADAD8/wIADAD8/wQADAD8/wYADAD8/wgADAD8/woADAD+//b/DAD+//j/DAD+//r/DAD+//z/DAD+//7/DAD+/wAADAD+/wIADAD+/wQADAD+/wYADAD+/wgADAD+/woADAAAAPb/DAAAAPj/DAAAAPr/DAAAAPz/DAAAAP7/DAAAAAAADAAAAAIADAAAAAQADAAAAAYADAAAAAgADAAAAAoADAACAPb/DAACAPj/DAACAPr/DAACAPz/DAACAP7/DAACAAAADAACAAIADAACAAQADAACAAYADAACAAgADAACAAoADAAEAPb/DAAEAPj/DAAEAPr/DAAEAPz/DAAEAP7/DAAEAAAADAAEAAIADAAEAAQADAAEAAYADAAEAAgADAAEAAoADAAGAPb/DAAGAPj/DAAGAPr/DAAGAPz/DAAGAP7/DAAGAAAADAAGAAIADAAGAAQADAAGAAYADAAGAAgADAAGAAoADAAIAPj/DAAIAPr/DAAIAPz/DAAIAP7/DAAIAAAADAAIAAIADAAIAAQADAAIAAYADAAIAAgADAAKAPr/DAAKAPz/DAAKAP7/DAAKAAAADAAKAAIADAAKAAQADAAKAAYADQD3//v/DQD3//3/DQD3////DQD3/wEADQD3/wMADQD3/wUADQD5//n/DQD5//v/DQD5//3/DQD5////DQD5/wEADQD5/wMADQD5/wUADQD5/wcADQD7//f/DQD7//n/DQD7//v/DQD7//3/DQD7////DQD7/wEADQD7/wMADQD7/wUADQD7/wcADQD7/wkADQD9//f/DQD9//n/DQD9//v/DQD9//3/DQD9////DQD9/wEADQD9/wMADQD9/wUADQD9/wcADQD9/wkADQD///f/DQD///n/DQD///v/DQD///3/DQD/////DQD//wEADQD//wMADQD//wUADQD//wcADQD//wkADQABAPf/DQABAPn/DQABAPv/DQABAP3/DQABAP//DQABAAEADQABAAMADQABAAUADQABAAcADQABAAkADQADAPf/DQADAPn/DQADAPv/DQADAP3/DQADAP//DQADAAEADQADAAMADQADAAUADQADAAcADQADAAkADQAFAPf/DQAFAPn/DQAFAPv/DQAFAP3/DQAFAP//DQAFAAEADQAFAAMADQAFAAUADQAFAAcADQAFAAkADQAHAPn/DQAHAPv/DQAHAP3/DQAHAP//DQAHAAEADQAHAAMADQAHAAUADQAHAAcADQAJAPv/DQAJAP3/DQAJAP//DQAJAAEADQAJAAMADQAJAAUADgD4//z/DgD4//7/DgD4/wAADgD4/wIADgD4/wQADgD6//r/DgD6//z/DgD6//7/DgD6/wAADgD6/wIADgD6/wQADgD6/wYADgD8//j/DgD8//r/DgD8//z/DgD8//7/DgD8/wAADgD8/wIADgD8/wQADgD8/wYADgD8/wgADgD+//j/DgD+//r/DgD+//z/DgD+//7/DgD+/wAADgD+/wIADgD+/wQADgD+/wYADgD+/wgADgAAAPj/DgAAAPr/DgAAAPz/DgAAAP7/DgAAAAAADgAAAAIADgAAAAQADgAAAAYADgAAAAgADgACAPj/DgACAPr/DgACAPz/DgACAP7/DgACAAAADgACAAIADgACAAQADgACAAYADgACAAgADgAEAPj/DgAEAPr/DgAEAPz/DgAEAP7/DgAEAAAADgAEAAIADgAEAAQADgAEAAYADgAEAAgADgAGAPr/DgAGAPz/DgAGAP7/DgAGAAAADgAGAAIADgAGAAQADgAGAAYADgAIAPz/DgAIAP7/DgAIAAAADgAIAAIADgAIAAQADwD5//3/DwD5////DwD5/wEADwD5/wMADwD7//v/DwD7//3/DwD7////DwD7/wEADwD7/wMADwD7/wUADwD9//n/DwD9//v/DwD9//3/DwD9////DwD9/wEADwD9/wMADwD9/wUADwD9/wcADwD///n/DwD///v/DwD///3/DwD/////DwD//wEADwD//wMADwD//wUADwD//wcADwABAPn/DwABAPv/DwABAP3/DwABAP//DwABAAEADwABAAMADwABAAUADwABAAcADwADAPn/DwADAPv/DwADAP3/DwADAP//DwADAAEADwADAAMADwADAAUADwADAAcADwAFAPv/DwAFAP3/DwAFAP//DwAFAAEADwAFAAMADwAFAAUADwAHAP3/DwAHAP//DwAHAAEADwAHAAMAEAD8//7/EAD8/wAAEAD8/wIAEAD+//z/EAD+//7/EAD+/wAAEAD+/wIAEAD+/wQAEAAAAPz/EAAAAP7/EAAAAAAAEAAAAAIAEAAAAAQAEAACAPz/EAACAP7/EAACAAAAEAACAAIAEAACAAQAEAAEAP7/EAAEAAAAEAAEAAIA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.005782605554528505,\"absorptive\":true,\"n_reflections\":5028,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", + "Al (fcc)": "{\"name\":\"Al\",\"spacegroup\":\"Fm-3m (225)\",\"pointgroup\":\"m-3m\",\"cell\":[[4.05,0.0,0.0],[0.0,4.05,0.0],[0.0,0.0,4.05]],\"recip\":[[0.2469135802469136,0.0,0.0],[0.0,0.2469135802469136,0.0],[0.0,0.0,0.2469135802469136]],\"positions_frac\":[[0.0,0.0,0.0],[0.0,0.5,0.5],[0.5,0.0,0.5],[0.5,0.5,0.0]],\"numbers\":[13,13,13,13],\"symbols\":[\"Al\",\"Al\",\"Al\",\"Al\"],\"colors\":[[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651]],\"radii\":[1.21,1.21,1.21,1.21],\"hkl_i16\":\"9P8AAAAA9f/7////9f/7/wEA9f/9//3/9f/9////9f/9/wEA9f/9/wMA9f////v/9f////3/9f//////9f///wEA9f///wMA9f///wUA9f8BAPv/9f8BAP3/9f8BAP//9f8BAAEA9f8BAAMA9f8BAAUA9f8DAP3/9f8DAP//9f8DAAEA9f8DAAMA9f8FAP//9f8FAAEA9v/6//7/9v/6/wAA9v/6/wIA9v/8//z/9v/8//7/9v/8/wAA9v/8/wIA9v/8/wQA9v/+//r/9v/+//z/9v/+//7/9v/+/wAA9v/+/wIA9v/+/wQA9v/+/wYA9v8AAPr/9v8AAPz/9v8AAP7/9v8AAAAA9v8AAAIA9v8AAAQA9v8AAAYA9v8CAPr/9v8CAPz/9v8CAP7/9v8CAAAA9v8CAAIA9v8CAAQA9v8CAAYA9v8EAPz/9v8EAP7/9v8EAAAA9v8EAAIA9v8EAAQA9v8GAP7/9v8GAAAA9v8GAAIA9//5//3/9//5////9//5/wEA9//5/wMA9//7//v/9//7//3/9//7////9//7/wEA9//7/wMA9//7/wUA9//9//n/9//9//v/9//9//3/9//9////9//9/wEA9//9/wMA9//9/wUA9//9/wcA9/////n/9/////v/9/////3/9///////9////wEA9////wMA9////wUA9////wcA9/8BAPn/9/8BAPv/9/8BAP3/9/8BAP//9/8BAAEA9/8BAAMA9/8BAAUA9/8BAAcA9/8DAPn/9/8DAPv/9/8DAP3/9/8DAP//9/8DAAEA9/8DAAMA9/8DAAUA9/8DAAcA9/8FAPv/9/8FAP3/9/8FAP//9/8FAAEA9/8FAAMA9/8FAAUA9/8HAP3/9/8HAP//9/8HAAEA9/8HAAMA+P/4//z/+P/4//7/+P/4/wAA+P/4/wIA+P/4/wQA+P/6//r/+P/6//z/+P/6//7/+P/6/wAA+P/6/wIA+P/6/wQA+P/6/wYA+P/8//j/+P/8//r/+P/8//z/+P/8//7/+P/8/wAA+P/8/wIA+P/8/wQA+P/8/wYA+P/8/wgA+P/+//j/+P/+//r/+P/+//z/+P/+//7/+P/+/wAA+P/+/wIA+P/+/wQA+P/+/wYA+P/+/wgA+P8AAPj/+P8AAPr/+P8AAPz/+P8AAP7/+P8AAAAA+P8AAAIA+P8AAAQA+P8AAAYA+P8AAAgA+P8CAPj/+P8CAPr/+P8CAPz/+P8CAP7/+P8CAAAA+P8CAAIA+P8CAAQA+P8CAAYA+P8CAAgA+P8EAPj/+P8EAPr/+P8EAPz/+P8EAP7/+P8EAAAA+P8EAAIA+P8EAAQA+P8EAAYA+P8EAAgA+P8GAPr/+P8GAPz/+P8GAP7/+P8GAAAA+P8GAAIA+P8GAAQA+P8GAAYA+P8IAPz/+P8IAP7/+P8IAAAA+P8IAAIA+P8IAAQA+f/3//3/+f/3////+f/3/wEA+f/3/wMA+f/5//n/+f/5//v/+f/5//3/+f/5////+f/5/wEA+f/5/wMA+f/5/wUA+f/5/wcA+f/7//n/+f/7//v/+f/7//3/+f/7////+f/7/wEA+f/7/wMA+f/7/wUA+f/7/wcA+f/9//f/+f/9//n/+f/9//v/+f/9//3/+f/9////+f/9/wEA+f/9/wMA+f/9/wUA+f/9/wcA+f/9/wkA+f////f/+f////n/+f////v/+f////3/+f//////+f///wEA+f///wMA+f///wUA+f///wcA+f///wkA+f8BAPf/+f8BAPn/+f8BAPv/+f8BAP3/+f8BAP//+f8BAAEA+f8BAAMA+f8BAAUA+f8BAAcA+f8BAAkA+f8DAPf/+f8DAPn/+f8DAPv/+f8DAP3/+f8DAP//+f8DAAEA+f8DAAMA+f8DAAUA+f8DAAcA+f8DAAkA+f8FAPn/+f8FAPv/+f8FAP3/+f8FAP//+f8FAAEA+f8FAAMA+f8FAAUA+f8FAAcA+f8HAPn/+f8HAPv/+f8HAP3/+f8HAP//+f8HAAEA+f8HAAMA+f8HAAUA+f8HAAcA+f8JAP3/+f8JAP//+f8JAAEA+f8JAAMA+v/2//7/+v/2/wAA+v/2/wIA+v/4//r/+v/4//z/+v/4//7/+v/4/wAA+v/4/wIA+v/4/wQA+v/4/wYA+v/6//j/+v/6//r/+v/6//z/+v/6//7/+v/6/wAA+v/6/wIA+v/6/wQA+v/6/wYA+v/6/wgA+v/8//j/+v/8//r/+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/8/wYA+v/8/wgA+v/+//b/+v/+//j/+v/+//r/+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v/+/wYA+v/+/wgA+v/+/woA+v8AAPb/+v8AAPj/+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8AAAgA+v8AAAoA+v8CAPb/+v8CAPj/+v8CAPr/+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8CAAYA+v8CAAgA+v8CAAoA+v8EAPj/+v8EAPr/+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8EAAYA+v8EAAgA+v8GAPj/+v8GAPr/+v8GAPz/+v8GAP7/+v8GAAAA+v8GAAIA+v8GAAQA+v8GAAYA+v8GAAgA+v8IAPr/+v8IAPz/+v8IAP7/+v8IAAAA+v8IAAIA+v8IAAQA+v8IAAYA+v8KAP7/+v8KAAAA+v8KAAIA+//1////+//1/wEA+//3//v/+//3//3/+//3////+//3/wEA+//3/wMA+//3/wUA+//5//n/+//5//v/+//5//3/+//5////+//5/wEA+//5/wMA+//5/wUA+//5/wcA+//7//f/+//7//n/+//7//v/+//7//3/+//7////+//7/wEA+//7/wMA+//7/wUA+//7/wcA+//7/wkA+//9//f/+//9//n/+//9//v/+//9//3/+//9////+//9/wEA+//9/wMA+//9/wUA+//9/wcA+//9/wkA+/////X/+/////f/+/////n/+/////v/+/////3/+///////+////wEA+////wMA+////wUA+////wcA+////wkA+////wsA+/8BAPX/+/8BAPf/+/8BAPn/+/8BAPv/+/8BAP3/+/8BAP//+/8BAAEA+/8BAAMA+/8BAAUA+/8BAAcA+/8BAAkA+/8BAAsA+/8DAPf/+/8DAPn/+/8DAPv/+/8DAP3/+/8DAP//+/8DAAEA+/8DAAMA+/8DAAUA+/8DAAcA+/8DAAkA+/8FAPf/+/8FAPn/+/8FAPv/+/8FAP3/+/8FAP//+/8FAAEA+/8FAAMA+/8FAAUA+/8FAAcA+/8FAAkA+/8HAPn/+/8HAPv/+/8HAP3/+/8HAP//+/8HAAEA+/8HAAMA+/8HAAUA+/8HAAcA+/8JAPv/+/8JAP3/+/8JAP//+/8JAAEA+/8JAAMA+/8JAAUA+/8LAP//+/8LAAEA/P/2//z//P/2//7//P/2/wAA/P/2/wIA/P/2/wQA/P/4//j//P/4//r//P/4//z//P/4//7//P/4/wAA/P/4/wIA/P/4/wQA/P/4/wYA/P/4/wgA/P/6//j//P/6//r//P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/6/wYA/P/6/wgA/P/8//b//P/8//j//P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/8/wgA/P/8/woA/P/+//b//P/+//j//P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P/+/wgA/P/+/woA/P8AAPb//P8AAPj//P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8AAAgA/P8AAAoA/P8CAPb//P8CAPj//P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8CAAgA/P8CAAoA/P8EAPb//P8EAPj//P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8EAAgA/P8EAAoA/P8GAPj//P8GAPr//P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8GAAYA/P8GAAgA/P8IAPj//P8IAPr//P8IAPz//P8IAP7//P8IAAAA/P8IAAIA/P8IAAQA/P8IAAYA/P8IAAgA/P8KAPz//P8KAP7//P8KAAAA/P8KAAIA/P8KAAQA/f/1//3//f/1/////f/1/wEA/f/1/wMA/f/3//n//f/3//v//f/3//3//f/3/////f/3/wEA/f/3/wMA/f/3/wUA/f/3/wcA/f/5//f//f/5//n//f/5//v//f/5//3//f/5/////f/5/wEA/f/5/wMA/f/5/wUA/f/5/wcA/f/5/wkA/f/7//f//f/7//n//f/7//v//f/7//3//f/7/////f/7/wEA/f/7/wMA/f/7/wUA/f/7/wcA/f/7/wkA/f/9//X//f/9//f//f/9//n//f/9//v//f/9//3//f/9/////f/9/wEA/f/9/wMA/f/9/wUA/f/9/wcA/f/9/wkA/f/9/wsA/f////X//f////f//f////n//f////v//f////3//f///////f///wEA/f///wMA/f///wUA/f///wcA/f///wkA/f///wsA/f8BAPX//f8BAPf//f8BAPn//f8BAPv//f8BAP3//f8BAP///f8BAAEA/f8BAAMA/f8BAAUA/f8BAAcA/f8BAAkA/f8BAAsA/f8DAPX//f8DAPf//f8DAPn//f8DAPv//f8DAP3//f8DAP///f8DAAEA/f8DAAMA/f8DAAUA/f8DAAcA/f8DAAkA/f8DAAsA/f8FAPf//f8FAPn//f8FAPv//f8FAP3//f8FAP///f8FAAEA/f8FAAMA/f8FAAUA/f8FAAcA/f8FAAkA/f8HAPf//f8HAPn//f8HAPv//f8HAP3//f8HAP///f8HAAEA/f8HAAMA/f8HAAUA/f8HAAcA/f8HAAkA/f8JAPn//f8JAPv//f8JAP3//f8JAP///f8JAAEA/f8JAAMA/f8JAAUA/f8JAAcA/f8LAP3//f8LAP///f8LAAEA/f8LAAMA/v/2//r//v/2//z//v/2//7//v/2/wAA/v/2/wIA/v/2/wQA/v/2/wYA/v/4//j//v/4//r//v/4//z//v/4//7//v/4/wAA/v/4/wIA/v/4/wQA/v/4/wYA/v/4/wgA/v/6//b//v/6//j//v/6//r//v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/6/wYA/v/6/wgA/v/6/woA/v/8//b//v/8//j//v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/8/wgA/v/8/woA/v/+//b//v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v/+/woA/v8AAPb//v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8AAAoA/v8CAPb//v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8CAAoA/v8EAPb//v8EAPj//v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8EAAgA/v8EAAoA/v8GAPb//v8GAPj//v8GAPr//v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8GAAYA/v8GAAgA/v8GAAoA/v8IAPj//v8IAPr//v8IAPz//v8IAP7//v8IAAAA/v8IAAIA/v8IAAQA/v8IAAYA/v8IAAgA/v8KAPr//v8KAPz//v8KAP7//v8KAAAA/v8KAAIA/v8KAAQA/v8KAAYA///1//v////1//3////1///////1/wEA///1/wMA///1/wUA///3//n////3//v////3//3////3///////3/wEA///3/wMA///3/wUA///3/wcA///5//f////5//n////5//v////5//3////5///////5/wEA///5/wMA///5/wUA///5/wcA///5/wkA///7//X////7//f////7//n////7//v////7//3////7///////7/wEA///7/wMA///7/wUA///7/wcA///7/wkA///7/wsA///9//X////9//f////9//n////9//v////9//3////9///////9/wEA///9/wMA///9/wUA///9/wcA///9/wkA///9/wsA//////X///////f///////n///////v///////3//////////////wEA/////wMA/////wUA/////wcA/////wkA/////wsA//8BAPX///8BAPf///8BAPn///8BAPv///8BAP3///8BAP////8BAAEA//8BAAMA//8BAAUA//8BAAcA//8BAAkA//8BAAsA//8DAPX///8DAPf///8DAPn///8DAPv///8DAP3///8DAP////8DAAEA//8DAAMA//8DAAUA//8DAAcA//8DAAkA//8DAAsA//8FAPX///8FAPf///8FAPn///8FAPv///8FAP3///8FAP////8FAAEA//8FAAMA//8FAAUA//8FAAcA//8FAAkA//8FAAsA//8HAPf///8HAPn///8HAPv///8HAP3///8HAP////8HAAEA//8HAAMA//8HAAUA//8HAAcA//8HAAkA//8JAPn///8JAPv///8JAP3///8JAP////8JAAEA//8JAAMA//8JAAUA//8JAAcA//8LAPv///8LAP3///8LAP////8LAAEA//8LAAMA//8LAAUAAAD0/wAAAAD2//r/AAD2//z/AAD2//7/AAD2/wAAAAD2/wIAAAD2/wQAAAD2/wYAAAD4//j/AAD4//r/AAD4//z/AAD4//7/AAD4/wAAAAD4/wIAAAD4/wQAAAD4/wYAAAD4/wgAAAD6//b/AAD6//j/AAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD6/wgAAAD6/woAAAD8//b/AAD8//j/AAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD8/wgAAAD8/woAAAD+//b/AAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD+/woAAAAAAPT/AAAAAPb/AAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAAAAAoAAAAAAAwAAAACAPb/AAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAACAAoAAAAEAPb/AAAEAPj/AAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAEAAgAAAAEAAoAAAAGAPb/AAAGAPj/AAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAGAAgAAAAGAAoAAAAIAPj/AAAIAPr/AAAIAPz/AAAIAP7/AAAIAAAAAAAIAAIAAAAIAAQAAAAIAAYAAAAIAAgAAAAKAPr/AAAKAPz/AAAKAP7/AAAKAAAAAAAKAAIAAAAKAAQAAAAKAAYAAAAMAAAAAQD1//v/AQD1//3/AQD1////AQD1/wEAAQD1/wMAAQD1/wUAAQD3//n/AQD3//v/AQD3//3/AQD3////AQD3/wEAAQD3/wMAAQD3/wUAAQD3/wcAAQD5//f/AQD5//n/AQD5//v/AQD5//3/AQD5////AQD5/wEAAQD5/wMAAQD5/wUAAQD5/wcAAQD5/wkAAQD7//X/AQD7//f/AQD7//n/AQD7//v/AQD7//3/AQD7////AQD7/wEAAQD7/wMAAQD7/wUAAQD7/wcAAQD7/wkAAQD7/wsAAQD9//X/AQD9//f/AQD9//n/AQD9//v/AQD9//3/AQD9////AQD9/wEAAQD9/wMAAQD9/wUAAQD9/wcAAQD9/wkAAQD9/wsAAQD///X/AQD///f/AQD///n/AQD///v/AQD///3/AQD/////AQD//wEAAQD//wMAAQD//wUAAQD//wcAAQD//wkAAQD//wsAAQABAPX/AQABAPf/AQABAPn/AQABAPv/AQABAP3/AQABAP//AQABAAEAAQABAAMAAQABAAUAAQABAAcAAQABAAkAAQABAAsAAQADAPX/AQADAPf/AQADAPn/AQADAPv/AQADAP3/AQADAP//AQADAAEAAQADAAMAAQADAAUAAQADAAcAAQADAAkAAQADAAsAAQAFAPX/AQAFAPf/AQAFAPn/AQAFAPv/AQAFAP3/AQAFAP//AQAFAAEAAQAFAAMAAQAFAAUAAQAFAAcAAQAFAAkAAQAFAAsAAQAHAPf/AQAHAPn/AQAHAPv/AQAHAP3/AQAHAP//AQAHAAEAAQAHAAMAAQAHAAUAAQAHAAcAAQAHAAkAAQAJAPn/AQAJAPv/AQAJAP3/AQAJAP//AQAJAAEAAQAJAAMAAQAJAAUAAQAJAAcAAQALAPv/AQALAP3/AQALAP//AQALAAEAAQALAAMAAQALAAUAAgD2//r/AgD2//z/AgD2//7/AgD2/wAAAgD2/wIAAgD2/wQAAgD2/wYAAgD4//j/AgD4//r/AgD4//z/AgD4//7/AgD4/wAAAgD4/wIAAgD4/wQAAgD4/wYAAgD4/wgAAgD6//b/AgD6//j/AgD6//r/AgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD6/wYAAgD6/wgAAgD6/woAAgD8//b/AgD8//j/AgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD8/wgAAgD8/woAAgD+//b/AgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD+/woAAgAAAPb/AgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgAAAAoAAgACAPb/AgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgACAAoAAgAEAPb/AgAEAPj/AgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAEAAgAAgAEAAoAAgAGAPb/AgAGAPj/AgAGAPr/AgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAGAAYAAgAGAAgAAgAGAAoAAgAIAPj/AgAIAPr/AgAIAPz/AgAIAP7/AgAIAAAAAgAIAAIAAgAIAAQAAgAIAAYAAgAIAAgAAgAKAPr/AgAKAPz/AgAKAP7/AgAKAAAAAgAKAAIAAgAKAAQAAgAKAAYAAwD1//3/AwD1////AwD1/wEAAwD1/wMAAwD3//n/AwD3//v/AwD3//3/AwD3////AwD3/wEAAwD3/wMAAwD3/wUAAwD3/wcAAwD5//f/AwD5//n/AwD5//v/AwD5//3/AwD5////AwD5/wEAAwD5/wMAAwD5/wUAAwD5/wcAAwD5/wkAAwD7//f/AwD7//n/AwD7//v/AwD7//3/AwD7////AwD7/wEAAwD7/wMAAwD7/wUAAwD7/wcAAwD7/wkAAwD9//X/AwD9//f/AwD9//n/AwD9//v/AwD9//3/AwD9////AwD9/wEAAwD9/wMAAwD9/wUAAwD9/wcAAwD9/wkAAwD9/wsAAwD///X/AwD///f/AwD///n/AwD///v/AwD///3/AwD/////AwD//wEAAwD//wMAAwD//wUAAwD//wcAAwD//wkAAwD//wsAAwABAPX/AwABAPf/AwABAPn/AwABAPv/AwABAP3/AwABAP//AwABAAEAAwABAAMAAwABAAUAAwABAAcAAwABAAkAAwABAAsAAwADAPX/AwADAPf/AwADAPn/AwADAPv/AwADAP3/AwADAP//AwADAAEAAwADAAMAAwADAAUAAwADAAcAAwADAAkAAwADAAsAAwAFAPf/AwAFAPn/AwAFAPv/AwAFAP3/AwAFAP//AwAFAAEAAwAFAAMAAwAFAAUAAwAFAAcAAwAFAAkAAwAHAPf/AwAHAPn/AwAHAPv/AwAHAP3/AwAHAP//AwAHAAEAAwAHAAMAAwAHAAUAAwAHAAcAAwAHAAkAAwAJAPn/AwAJAPv/AwAJAP3/AwAJAP//AwAJAAEAAwAJAAMAAwAJAAUAAwAJAAcAAwALAP3/AwALAP//AwALAAEAAwALAAMABAD2//z/BAD2//7/BAD2/wAABAD2/wIABAD2/wQABAD4//j/BAD4//r/BAD4//z/BAD4//7/BAD4/wAABAD4/wIABAD4/wQABAD4/wYABAD4/wgABAD6//j/BAD6//r/BAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD6/wYABAD6/wgABAD8//b/BAD8//j/BAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD8/wgABAD8/woABAD+//b/BAD+//j/BAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD+/wgABAD+/woABAAAAPb/BAAAAPj/BAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAAAAAgABAAAAAoABAACAPb/BAACAPj/BAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAACAAgABAACAAoABAAEAPb/BAAEAPj/BAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAEAAgABAAEAAoABAAGAPj/BAAGAPr/BAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAGAAYABAAGAAgABAAIAPj/BAAIAPr/BAAIAPz/BAAIAP7/BAAIAAAABAAIAAIABAAIAAQABAAIAAYABAAIAAgABAAKAPz/BAAKAP7/BAAKAAAABAAKAAIABAAKAAQABQD1////BQD1/wEABQD3//v/BQD3//3/BQD3////BQD3/wEABQD3/wMABQD3/wUABQD5//n/BQD5//v/BQD5//3/BQD5////BQD5/wEABQD5/wMABQD5/wUABQD5/wcABQD7//f/BQD7//n/BQD7//v/BQD7//3/BQD7////BQD7/wEABQD7/wMABQD7/wUABQD7/wcABQD7/wkABQD9//f/BQD9//n/BQD9//v/BQD9//3/BQD9////BQD9/wEABQD9/wMABQD9/wUABQD9/wcABQD9/wkABQD///X/BQD///f/BQD///n/BQD///v/BQD///3/BQD/////BQD//wEABQD//wMABQD//wUABQD//wcABQD//wkABQD//wsABQABAPX/BQABAPf/BQABAPn/BQABAPv/BQABAP3/BQABAP//BQABAAEABQABAAMABQABAAUABQABAAcABQABAAkABQABAAsABQADAPf/BQADAPn/BQADAPv/BQADAP3/BQADAP//BQADAAEABQADAAMABQADAAUABQADAAcABQADAAkABQAFAPf/BQAFAPn/BQAFAPv/BQAFAP3/BQAFAP//BQAFAAEABQAFAAMABQAFAAUABQAFAAcABQAFAAkABQAHAPn/BQAHAPv/BQAHAP3/BQAHAP//BQAHAAEABQAHAAMABQAHAAUABQAHAAcABQAJAPv/BQAJAP3/BQAJAP//BQAJAAEABQAJAAMABQAJAAUABQALAP//BQALAAEABgD2//7/BgD2/wAABgD2/wIABgD4//r/BgD4//z/BgD4//7/BgD4/wAABgD4/wIABgD4/wQABgD4/wYABgD6//j/BgD6//r/BgD6//z/BgD6//7/BgD6/wAABgD6/wIABgD6/wQABgD6/wYABgD6/wgABgD8//j/BgD8//r/BgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD8/wYABgD8/wgABgD+//b/BgD+//j/BgD+//r/BgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD+/wYABgD+/wgABgD+/woABgAAAPb/BgAAAPj/BgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgAAAAgABgAAAAoABgACAPb/BgACAPj/BgACAPr/BgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgACAAYABgACAAgABgACAAoABgAEAPj/BgAEAPr/BgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAEAAYABgAEAAgABgAGAPj/BgAGAPr/BgAGAPz/BgAGAP7/BgAGAAAABgAGAAIABgAGAAQABgAGAAYABgAGAAgABgAIAPr/BgAIAPz/BgAIAP7/BgAIAAAABgAIAAIABgAIAAQABgAIAAYABgAKAP7/BgAKAAAABgAKAAIABwD3//3/BwD3////BwD3/wEABwD3/wMABwD5//n/BwD5//v/BwD5//3/BwD5////BwD5/wEABwD5/wMABwD5/wUABwD5/wcABwD7//n/BwD7//v/BwD7//3/BwD7////BwD7/wEABwD7/wMABwD7/wUABwD7/wcABwD9//f/BwD9//n/BwD9//v/BwD9//3/BwD9////BwD9/wEABwD9/wMABwD9/wUABwD9/wcABwD9/wkABwD///f/BwD///n/BwD///v/BwD///3/BwD/////BwD//wEABwD//wMABwD//wUABwD//wcABwD//wkABwABAPf/BwABAPn/BwABAPv/BwABAP3/BwABAP//BwABAAEABwABAAMABwABAAUABwABAAcABwABAAkABwADAPf/BwADAPn/BwADAPv/BwADAP3/BwADAP//BwADAAEABwADAAMABwADAAUABwADAAcABwADAAkABwAFAPn/BwAFAPv/BwAFAP3/BwAFAP//BwAFAAEABwAFAAMABwAFAAUABwAFAAcABwAHAPn/BwAHAPv/BwAHAP3/BwAHAP//BwAHAAEABwAHAAMABwAHAAUABwAHAAcABwAJAP3/BwAJAP//BwAJAAEABwAJAAMACAD4//z/CAD4//7/CAD4/wAACAD4/wIACAD4/wQACAD6//r/CAD6//z/CAD6//7/CAD6/wAACAD6/wIACAD6/wQACAD6/wYACAD8//j/CAD8//r/CAD8//z/CAD8//7/CAD8/wAACAD8/wIACAD8/wQACAD8/wYACAD8/wgACAD+//j/CAD+//r/CAD+//z/CAD+//7/CAD+/wAACAD+/wIACAD+/wQACAD+/wYACAD+/wgACAAAAPj/CAAAAPr/CAAAAPz/CAAAAP7/CAAAAAAACAAAAAIACAAAAAQACAAAAAYACAAAAAgACAACAPj/CAACAPr/CAACAPz/CAACAP7/CAACAAAACAACAAIACAACAAQACAACAAYACAACAAgACAAEAPj/CAAEAPr/CAAEAPz/CAAEAP7/CAAEAAAACAAEAAIACAAEAAQACAAEAAYACAAEAAgACAAGAPr/CAAGAPz/CAAGAP7/CAAGAAAACAAGAAIACAAGAAQACAAGAAYACAAIAPz/CAAIAP7/CAAIAAAACAAIAAIACAAIAAQACQD5//3/CQD5////CQD5/wEACQD5/wMACQD7//v/CQD7//3/CQD7////CQD7/wEACQD7/wMACQD7/wUACQD9//n/CQD9//v/CQD9//3/CQD9////CQD9/wEACQD9/wMACQD9/wUACQD9/wcACQD///n/CQD///v/CQD///3/CQD/////CQD//wEACQD//wMACQD//wUACQD//wcACQABAPn/CQABAPv/CQABAP3/CQABAP//CQABAAEACQABAAMACQABAAUACQABAAcACQADAPn/CQADAPv/CQADAP3/CQADAP//CQADAAEACQADAAMACQADAAUACQADAAcACQAFAPv/CQAFAP3/CQAFAP//CQAFAAEACQAFAAMACQAFAAUACQAHAP3/CQAHAP//CQAHAAEACQAHAAMACgD6//7/CgD6/wAACgD6/wIACgD8//z/CgD8//7/CgD8/wAACgD8/wIACgD8/wQACgD+//r/CgD+//z/CgD+//7/CgD+/wAACgD+/wIACgD+/wQACgD+/wYACgAAAPr/CgAAAPz/CgAAAP7/CgAAAAAACgAAAAIACgAAAAQACgAAAAYACgACAPr/CgACAPz/CgACAP7/CgACAAAACgACAAIACgACAAQACgACAAYACgAEAPz/CgAEAP7/CgAEAAAACgAEAAIACgAEAAQACgAGAP7/CgAGAAAACgAGAAIACwD7////CwD7/wEACwD9//3/CwD9////CwD9/wEACwD9/wMACwD///v/CwD///3/CwD/////CwD//wEACwD//wMACwD//wUACwABAPv/CwABAP3/CwABAP//CwABAAEACwABAAMACwABAAUACwADAP3/CwADAP//CwADAAEACwADAAMACwAFAP//CwAFAAEADAAAAAAA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"VjcJOfQhBjn0IQY55YwOOTOhFzkzoRc55YwOOfQhBjkzoRc5mnIhOZpyITkzoRc59CEGOfQhBjkzoRc5mnIhOZpyITkzoRc59CEGOeWMDjkzoRc5M6EXOeWMDjn0IQY59CEGOaN2DTmr3xE5o3YNOcJ0Fjl7ViU5ErcqOXtWJTnCdBY5o3YNOXtWJTnsLDY5Pko8OewsNjl7ViU5o3YNOavfETkStyo5Pko8OTOvQjk+Sjw5ErcqOavfETmjdg05e1YlOewsNjk+Sjw57Cw2OXtWJTmjdg05wnQWOXtWJTkStyo5e1YlOcJ0Fjmjdg05q98ROaN2DTnljA45M6EXOTOhFznljA45M6EXOUkYLDnNrTc5za03OUkYLDkzoRc55YwOOUkYLDk6VEQ53zNSOd8zUjk6VEQ5SRgsOeWMDjkzoRc5za03Od8zUjm2fmE5tn5hOd8zUjnNrTc5M6EXOTOhFznNrTc53zNSObZ+YTm2fmE53zNSOc2tNzkzoRc55YwOOUkYLDk6VEQ53zNSOd8zUjk6VEQ5SRgsOeWMDjkzoRc5SRgsOc2tNznNrTc5SRgsOTOhFznljA45M6EXOTOhFznljA45VjcJOcJ0FjloOBs5wnQWOVY3CTmr3xE5ErcqOT5KPDkzr0I5Pko8ORK3Kjmr3xE5VjcJORK3KjnyYEk5fH9fOcqkZzl8f1858mBJORK3KjlWNwk5wnQWOT5KPDl8f1856U15OUh0gTnpTXk5fH9fOT5KPDnCdBY5aDgbOTOvQjnKpGc5SHSBOcuMhjlIdIE5yqRnOTOvQjloOBs5wnQWOT5KPDl8f1856U15OUh0gTnpTXk5fH9fOT5KPDnCdBY5VjcJORK3KjnyYEk5fH9fOcqkZzl8f1858mBJORK3KjlWNwk5q98RORK3Kjk+Sjw5M69COT5KPDkStyo5q98ROVY3CTnCdBY5aDgbOcJ0FjlWNwk55YwOOTOhFzkzoRc55YwOOfQhBjmaciE5za03OTpURDk6VEQ5za03OZpyITn0IQY5mnIhOTpURDm2fmE5AHRyOQB0cjm2fmE5OlREOZpyITnljA45za03ObZ+YTkOs4I5O2KNOTtijTkOs4I5tn5hOc2tNznljA45M6EXOTpURDkAdHI5O2KNOaWWmTmllpk5O2KNOQB0cjk6VEQ5M6EXOTOhFzk6VEQ5AHRyOTtijTmllpk5pZaZOTtijTkAdHI5OlREOTOhFznljA45za03ObZ+YTkOs4I5O2KNOTtijTkOs4I5tn5hOc2tNznljA45mnIhOTpURDm2fmE5AHRyOQB0cjm2fmE5OlREOZpyITn0IQY5mnIhOc2tNzk6VEQ5OlREOc2tNzmaciE59CEGOeWMDjkzoRc5M6EXOeWMDjmjdg05q98ROaN2DTmr3xE5ErcqOT5KPDkzr0I5Pko8ORK3Kjmr3xE5q98ROewsNjlQwlc5bTtwOelNeTltO3A5UMJXOewsNjmr3xE5ErcqOVDCVzlIdIE59cKROT/3lzn1wpE5SHSBOVDCVzkStyo5o3YNOT5KPDltO3A59cKROYLhpTmyxa05guGlOfXCkTltO3A5Pko8OaN2DTmr3xE5M69COelNeTk/95c5ssWtOXd2tjmyxa05P/eXOelNeTkzr0I5q98ROaN2DTk+Sjw5bTtwOfXCkTmC4aU5ssWtOYLhpTn1wpE5bTtwOT5KPDmjdg05ErcqOVDCVzlIdIE59cKROT/3lzn1wpE5SHSBOVDCVzkStyo5q98ROewsNjlQwlc5bTtwOelNeTltO3A5UMJXOewsNjmr3xE5q98RORK3Kjk+Sjw5M69COT5KPDkStyo5q98ROaN2DTmr3xE5o3YNOfQhBjn0IQY5M6EXOUkYLDnNrTc5za03OUkYLDkzoRc5mnIhOTpURDm2fmE5AHRyOQB0cjm2fmE5OlREOZpyITkzoRc5OlREOQB0cjk7Yo05pZaZOaWWmTk7Yo05AHRyOTpURDkzoRc5SRgsObZ+YTk7Yo05iMmnOUDIuDlAyLg5iMmnOTtijTm2fmE5SRgsOfQhBjnNrTc5AHRyOaWWmTlAyLg5mivOOZorzjlAyLg5pZaZOQB0cjnNrTc59CEGOfQhBjnNrTc5AHRyOaWWmTlAyLg5mivOOZorzjlAyLg5pZaZOQB0cjnNrTc59CEGOUkYLDm2fmE5O2KNOYjJpzlAyLg5QMi4OYjJpzk7Yo05tn5hOUkYLDkzoRc5OlREOQB0cjk7Yo05pZaZOaWWmTk7Yo05AHRyOTpURDkzoRc5mnIhOTpURDm2fmE5AHRyOQB0cjm2fmE5OlREOZpyITkzoRc5SRgsOc2tNznNrTc5SRgsOTOhFzn0IQY59CEGOcJ0Fjl7ViU5ErcqOXtWJTnCdBY5VjcJORK3KjnyYEk5fH9fOcqkZzl8f1858mBJORK3KjlWNwk5ErcqOVDCVzlIdIE59cKROT/3lzn1wpE5SHSBOVDCVzkStyo5wnQWOfJgSTlIdIE5XKWeOXd2tjnpJ8A5d3a2OVylnjlIdIE58mBJOcJ0Fjl7ViU5fH9fOfXCkTl3drY5gfvXOfiC5zmB+9c5d3a2OfXCkTl8f185e1YlORK3KjnKpGc5P/eXOeknwDn4guc5iYL7OfiC5znpJ8A5P/eXOcqkZzkStyo5e1YlOXx/Xzn1wpE5d3a2OYH71zn4guc5gfvXOXd2tjn1wpE5fH9fOXtWJTnCdBY58mBJOUh0gTlcpZ45d3a2OeknwDl3drY5XKWeOUh0gTnyYEk5wnQWORK3KjlQwlc5SHSBOfXCkTk/95c59cKROUh0gTlQwlc5ErcqOVY3CTkStyo58mBJOXx/XznKpGc5fH9fOfJgSTkStyo5VjcJOcJ0Fjl7ViU5ErcqOXtWJTnCdBY55YwOOTOhFzkzoRc55YwOOeWMDjlJGCw5OlREOd8zUjnfM1I5OlREOUkYLDnljA455YwOOc2tNzm2fmE5DrOCOTtijTk7Yo05DrOCObZ+YTnNrTc55YwOOUkYLDm2fmE5O2KNOYjJpzlAyLg5QMi4OYjJpzk7Yo05tn5hOUkYLDnljA45OlREOQ6zgjmIyac5mivOOc/96znP/es5mivOOYjJpzkOs4I5OlREOeWMDjkzoRc53zNSOTtijTlAyLg5z/3rOTC3EDowtxA6z/3rOUDIuDk7Yo053zNSOTOhFzkzoRc53zNSOTtijTlAyLg5z/3rOTC3EDowtxA6z/3rOUDIuDk7Yo053zNSOTOhFznljA45OlREOQ6zgjmIyac5mivOOc/96znP/es5mivOOYjJpzkOs4I5OlREOeWMDjlJGCw5tn5hOTtijTmIyac5QMi4OUDIuDmIyac5O2KNObZ+YTlJGCw55YwOOc2tNzm2fmE5DrOCOTtijTk7Yo05DrOCObZ+YTnNrTc55YwOOeWMDjlJGCw5OlREOd8zUjnfM1I5OlREOUkYLDnljA455YwOOTOhFzkzoRc55YwOOaN2DTl7ViU57Cw2OT5KPDnsLDY5e1YlOaN2DTnCdBY5Pko8OXx/XznpTXk5SHSBOelNeTl8f185Pko8OcJ0Fjmjdg05Pko8OW07cDn1wpE5guGlObLFrTmC4aU59cKROW07cDk+Sjw5o3YNOXtWJTl8f1859cKROXd2tjmB+9c5+ILnOYH71zl3drY59cKROXx/Xzl7ViU57Cw2OelNeTmC4aU5gfvXORn/CzrCHSQ6Gf8LOoH71zmC4aU56U15OewsNjk+Sjw5SHSBObLFrTn4guc5wh0kOl7eXDrCHSQ6+ILnObLFrTlIdIE5Pko8OewsNjnpTXk5guGlOYH71zkZ/ws6wh0kOhn/CzqB+9c5guGlOelNeTnsLDY5e1YlOXx/Xzn1wpE5d3a2OYH71zn4guc5gfvXOXd2tjn1wpE5fH9fOXtWJTmjdg05Pko8OW07cDn1wpE5guGlObLFrTmC4aU59cKROW07cDk+Sjw5o3YNOcJ0Fjk+Sjw5fH9fOelNeTlIdIE56U15OXx/Xzk+Sjw5wnQWOaN2DTl7ViU57Cw2OT5KPDnsLDY5e1YlOaN2DTn0IQY5M6EXOZpyITmaciE5M6EXOfQhBjkzoRc5za03Od8zUjm2fmE5tn5hOd8zUjnNrTc5M6EXOTOhFzk6VEQ5AHRyOTtijTmllpk5pZaZOTtijTkAdHI5OlREOTOhFzn0IQY5za03OQB0cjmllpk5QMi4OZorzjmaK845QMi4OaWWmTkAdHI5za03OfQhBjkzoRc53zNSOTtijTlAyLg5z/3rOTC3EDowtxA6z/3rOUDIuDk7Yo053zNSOTOhFzmaciE5tn5hOaWWmTmaK845MLcQOkC4ejpAuHo6MLcQOporzjmllpk5tn5hOZpyITmaciE5tn5hOaWWmTmaK845MLcQOkC4ejpAuHo6MLcQOporzjmllpk5tn5hOZpyITkzoRc53zNSOTtijTlAyLg5z/3rOTC3EDowtxA6z/3rOUDIuDk7Yo053zNSOTOhFzn0IQY5za03OQB0cjmllpk5QMi4OZorzjmaK845QMi4OaWWmTkAdHI5za03OfQhBjkzoRc5OlREOQB0cjk7Yo05pZaZOaWWmTk7Yo05AHRyOTpURDkzoRc5M6EXOc2tNznfM1I5tn5hObZ+YTnfM1I5za03OTOhFzn0IQY5M6EXOZpyITmaciE5M6EXOfQhBjlWNwk5q98RORK3Kjk+Sjw5M69COT5KPDkStyo5q98ROWg4Gzkzr0I5yqRnOUh0gTnLjIY5SHSBOcqkZzkzr0I5aDgbOavfETkzr0I56U15OT/3lzmyxa05d3a2ObLFrTk/95c56U15OTOvQjmr3xE5ErcqOcqkZzk/95c56SfAOfiC5zmJgvs5+ILnOeknwDk/95c5yqRnORK3Kjk+Sjw5SHSBObLFrTn4guc5wh0kOl7eXDrCHSQ6+ILnObLFrTlIdIE5Pko8OVY3CTkzr0I5y4yGOXd2tjmJgvs5Xt5cOl7eXDqJgvs5d3a2OcuMhjkzr0I5VjcJOT5KPDlIdIE5ssWtOfiC5znCHSQ6Xt5cOsIdJDr4guc5ssWtOUh0gTk+Sjw5ErcqOcqkZzk/95c56SfAOfiC5zmJgvs5+ILnOeknwDk/95c5yqRnORK3Kjmr3xE5M69COelNeTk/95c5ssWtOXd2tjmyxa05P/eXOelNeTkzr0I5q98ROWg4Gzkzr0I5yqRnOUh0gTnLjIY5SHSBOcqkZzkzr0I5aDgbOavfETkStyo5Pko8OTOvQjk+Sjw5ErcqOavfETlWNwk59CEGOTOhFzmaciE5mnIhOTOhFzn0IQY5M6EXOc2tNznfM1I5tn5hObZ+YTnfM1I5za03OTOhFzkzoRc5OlREOQB0cjk7Yo05pZaZOaWWmTk7Yo05AHRyOTpURDkzoRc59CEGOc2tNzkAdHI5pZaZOUDIuDmaK845mivOOUDIuDmllpk5AHRyOc2tNzn0IQY5M6EXOd8zUjk7Yo05QMi4Oc/96zkwtxA6MLcQOs/96zlAyLg5O2KNOd8zUjkzoRc5mnIhObZ+YTmllpk5mivOOTC3EDpAuHo6QLh6OjC3EDqaK845pZaZObZ+YTmaciE5mnIhObZ+YTmllpk5mivOOTC3EDpAuHo6QLh6OjC3EDqaK845pZaZObZ+YTmaciE5M6EXOd8zUjk7Yo05QMi4Oc/96zkwtxA6MLcQOs/96zlAyLg5O2KNOd8zUjkzoRc59CEGOc2tNzkAdHI5pZaZOUDIuDmaK845mivOOUDIuDmllpk5AHRyOc2tNzn0IQY5M6EXOTpURDkAdHI5O2KNOaWWmTmllpk5O2KNOQB0cjk6VEQ5M6EXOTOhFznNrTc53zNSObZ+YTm2fmE53zNSOc2tNzkzoRc59CEGOTOhFzmaciE5mnIhOTOhFzn0IQY5o3YNOXtWJTnsLDY5Pko8OewsNjl7ViU5o3YNOcJ0Fjk+Sjw5fH9fOelNeTlIdIE56U15OXx/Xzk+Sjw5wnQWOaN2DTk+Sjw5bTtwOfXCkTmC4aU5ssWtOYLhpTn1wpE5bTtwOT5KPDmjdg05e1YlOXx/Xzn1wpE5d3a2OYH71zn4guc5gfvXOXd2tjn1wpE5fH9fOXtWJTnsLDY56U15OYLhpTmB+9c5Gf8LOsIdJDoZ/ws6gfvXOYLhpTnpTXk57Cw2OT5KPDlIdIE5ssWtOfiC5znCHSQ6Xt5cOsIdJDr4guc5ssWtOUh0gTk+Sjw57Cw2OelNeTmC4aU5gfvXORn/CzrCHSQ6Gf8LOoH71zmC4aU56U15OewsNjl7ViU5fH9fOfXCkTl3drY5gfvXOfiC5zmB+9c5d3a2OfXCkTl8f185e1YlOaN2DTk+Sjw5bTtwOfXCkTmC4aU5ssWtOYLhpTn1wpE5bTtwOT5KPDmjdg05wnQWOT5KPDl8f1856U15OUh0gTnpTXk5fH9fOT5KPDnCdBY5o3YNOXtWJTnsLDY5Pko8OewsNjl7ViU5o3YNOeWMDjkzoRc5M6EXOeWMDjnljA45SRgsOTpURDnfM1I53zNSOTpURDlJGCw55YwOOeWMDjnNrTc5tn5hOQ6zgjk7Yo05O2KNOQ6zgjm2fmE5za03OeWMDjlJGCw5tn5hOTtijTmIyac5QMi4OUDIuDmIyac5O2KNObZ+YTlJGCw55YwOOTpURDkOs4I5iMmnOZorzjnP/es5z/3rOZorzjmIyac5DrOCOTpURDnljA45M6EXOd8zUjk7Yo05QMi4Oc/96zkwtxA6MLcQOs/96zlAyLg5O2KNOd8zUjkzoRc5M6EXOd8zUjk7Yo05QMi4Oc/96zkwtxA6MLcQOs/96zlAyLg5O2KNOd8zUjkzoRc55YwOOTpURDkOs4I5iMmnOZorzjnP/es5z/3rOZorzjmIyac5DrOCOTpURDnljA45SRgsObZ+YTk7Yo05iMmnOUDIuDlAyLg5iMmnOTtijTm2fmE5SRgsOeWMDjnNrTc5tn5hOQ6zgjk7Yo05O2KNOQ6zgjm2fmE5za03OeWMDjnljA45SRgsOTpURDnfM1I53zNSOTpURDlJGCw55YwOOeWMDjkzoRc5M6EXOeWMDjnCdBY5e1YlORK3Kjl7ViU5wnQWOVY3CTkStyo58mBJOXx/XznKpGc5fH9fOfJgSTkStyo5VjcJORK3KjlQwlc5SHSBOfXCkTk/95c59cKROUh0gTlQwlc5ErcqOcJ0FjnyYEk5SHSBOVylnjl3drY56SfAOXd2tjlcpZ45SHSBOfJgSTnCdBY5e1YlOXx/Xzn1wpE5d3a2OYH71zn4guc5gfvXOXd2tjn1wpE5fH9fOXtWJTkStyo5yqRnOT/3lznpJ8A5+ILnOYmC+zn4guc56SfAOT/3lznKpGc5ErcqOXtWJTl8f1859cKROXd2tjmB+9c5+ILnOYH71zl3drY59cKROXx/Xzl7ViU5wnQWOfJgSTlIdIE5XKWeOXd2tjnpJ8A5d3a2OVylnjlIdIE58mBJOcJ0FjkStyo5UMJXOUh0gTn1wpE5P/eXOfXCkTlIdIE5UMJXORK3KjlWNwk5ErcqOfJgSTl8f185yqRnOXx/XznyYEk5ErcqOVY3CTnCdBY5e1YlORK3Kjl7ViU5wnQWOfQhBjn0IQY5M6EXOUkYLDnNrTc5za03OUkYLDkzoRc5mnIhOTpURDm2fmE5AHRyOQB0cjm2fmE5OlREOZpyITkzoRc5OlREOQB0cjk7Yo05pZaZOaWWmTk7Yo05AHRyOTpURDkzoRc5SRgsObZ+YTk7Yo05iMmnOUDIuDlAyLg5iMmnOTtijTm2fmE5SRgsOfQhBjnNrTc5AHRyOaWWmTlAyLg5mivOOZorzjlAyLg5pZaZOQB0cjnNrTc59CEGOfQhBjnNrTc5AHRyOaWWmTlAyLg5mivOOZorzjlAyLg5pZaZOQB0cjnNrTc59CEGOUkYLDm2fmE5O2KNOYjJpzlAyLg5QMi4OYjJpzk7Yo05tn5hOUkYLDkzoRc5OlREOQB0cjk7Yo05pZaZOaWWmTk7Yo05AHRyOTpURDkzoRc5mnIhOTpURDm2fmE5AHRyOQB0cjm2fmE5OlREOZpyITkzoRc5SRgsOc2tNznNrTc5SRgsOTOhFzn0IQY59CEGOaN2DTmr3xE5o3YNOavfETkStyo5Pko8OTOvQjk+Sjw5ErcqOavfETmr3xE57Cw2OVDCVzltO3A56U15OW07cDlQwlc57Cw2OavfETkStyo5UMJXOUh0gTn1wpE5P/eXOfXCkTlIdIE5UMJXORK3Kjmjdg05Pko8OW07cDn1wpE5guGlObLFrTmC4aU59cKROW07cDk+Sjw5o3YNOavfETkzr0I56U15OT/3lzmyxa05d3a2ObLFrTk/95c56U15OTOvQjmr3xE5o3YNOT5KPDltO3A59cKROYLhpTmyxa05guGlOfXCkTltO3A5Pko8OaN2DTkStyo5UMJXOUh0gTn1wpE5P/eXOfXCkTlIdIE5UMJXORK3Kjmr3xE57Cw2OVDCVzltO3A56U15OW07cDlQwlc57Cw2OavfETmr3xE5ErcqOT5KPDkzr0I5Pko8ORK3Kjmr3xE5o3YNOavfETmjdg055YwOOTOhFzkzoRc55YwOOfQhBjmaciE5za03OTpURDk6VEQ5za03OZpyITn0IQY5mnIhOTpURDm2fmE5AHRyOQB0cjm2fmE5OlREOZpyITnljA45za03ObZ+YTkOs4I5O2KNOTtijTkOs4I5tn5hOc2tNznljA45M6EXOTpURDkAdHI5O2KNOaWWmTmllpk5O2KNOQB0cjk6VEQ5M6EXOTOhFzk6VEQ5AHRyOTtijTmllpk5pZaZOTtijTkAdHI5OlREOTOhFznljA45za03ObZ+YTkOs4I5O2KNOTtijTkOs4I5tn5hOc2tNznljA45mnIhOTpURDm2fmE5AHRyOQB0cjm2fmE5OlREOZpyITn0IQY5mnIhOc2tNzk6VEQ5OlREOc2tNzmaciE59CEGOeWMDjkzoRc5M6EXOeWMDjlWNwk5wnQWOWg4GznCdBY5VjcJOavfETkStyo5Pko8OTOvQjk+Sjw5ErcqOavfETlWNwk5ErcqOfJgSTl8f185yqRnOXx/XznyYEk5ErcqOVY3CTnCdBY5Pko8OXx/XznpTXk5SHSBOelNeTl8f185Pko8OcJ0FjloOBs5M69COcqkZzlIdIE5y4yGOUh0gTnKpGc5M69COWg4GznCdBY5Pko8OXx/XznpTXk5SHSBOelNeTl8f185Pko8OcJ0FjlWNwk5ErcqOfJgSTl8f185yqRnOXx/XznyYEk5ErcqOVY3CTmr3xE5ErcqOT5KPDkzr0I5Pko8ORK3Kjmr3xE5VjcJOcJ0FjloOBs5wnQWOVY3CTnljA45M6EXOTOhFznljA45M6EXOUkYLDnNrTc5za03OUkYLDkzoRc55YwOOUkYLDk6VEQ53zNSOd8zUjk6VEQ5SRgsOeWMDjkzoRc5za03Od8zUjm2fmE5tn5hOd8zUjnNrTc5M6EXOTOhFznNrTc53zNSObZ+YTm2fmE53zNSOc2tNzkzoRc55YwOOUkYLDk6VEQ53zNSOd8zUjk6VEQ5SRgsOeWMDjkzoRc5SRgsOc2tNznNrTc5SRgsOTOhFznljA45M6EXOTOhFznljA45o3YNOavfETmjdg05wnQWOXtWJTkStyo5e1YlOcJ0Fjmjdg05e1YlOewsNjk+Sjw57Cw2OXtWJTmjdg05q98RORK3Kjk+Sjw5M69COT5KPDkStyo5q98ROaN2DTl7ViU57Cw2OT5KPDnsLDY5e1YlOaN2DTnCdBY5e1YlORK3Kjl7ViU5wnQWOaN2DTmr3xE5o3YNOfQhBjn0IQY55YwOOTOhFzkzoRc55YwOOfQhBjkzoRc5mnIhOZpyITkzoRc59CEGOfQhBjkzoRc5mnIhOZpyITkzoRc59CEGOeWMDjkzoRc5M6EXOeWMDjn0IQY59CEGOVY3CTk=\",\"u0_imag\":0.005121869242937061,\"absorptive\":true,\"n_reflections\":1892,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", + "Au (fcc)": "{\"name\":\"Au\",\"spacegroup\":\"Fm-3m (225)\",\"pointgroup\":\"m-3m\",\"cell\":[[4.078,0.0,0.0],[0.0,4.078,0.0],[0.0,0.0,4.078]],\"recip\":[[0.24521824423737124,0.0,0.0],[0.0,0.24521824423737124,0.0],[0.0,0.0,0.24521824423737124]],\"positions_frac\":[[0.0,0.0,0.0],[0.0,0.5,0.5],[0.5,0.0,0.5],[0.5,0.5,0.0]],\"numbers\":[79,79,79,79],\"symbols\":[\"Au\",\"Au\",\"Au\",\"Au\"],\"colors\":[[1.0,0.82,0.137],[1.0,0.82,0.137],[1.0,0.82,0.137],[1.0,0.82,0.137]],\"radii\":[1.36,1.36,1.36,1.36],\"hkl_i16\":\"9P/+/wAA9P8AAP7/9P8AAAAA9P8AAAIA9P8CAAAA9f/7////9f/7/wEA9f/9//3/9f/9////9f/9/wEA9f/9/wMA9f////v/9f////3/9f//////9f///wEA9f///wMA9f///wUA9f8BAPv/9f8BAP3/9f8BAP//9f8BAAEA9f8BAAMA9f8BAAUA9f8DAP3/9f8DAP//9f8DAAEA9f8DAAMA9f8FAP//9f8FAAEA9v/6//7/9v/6/wAA9v/6/wIA9v/8//z/9v/8//7/9v/8/wAA9v/8/wIA9v/8/wQA9v/+//r/9v/+//z/9v/+//7/9v/+/wAA9v/+/wIA9v/+/wQA9v/+/wYA9v8AAPr/9v8AAPz/9v8AAP7/9v8AAAAA9v8AAAIA9v8AAAQA9v8AAAYA9v8CAPr/9v8CAPz/9v8CAP7/9v8CAAAA9v8CAAIA9v8CAAQA9v8CAAYA9v8EAPz/9v8EAP7/9v8EAAAA9v8EAAIA9v8EAAQA9v8GAP7/9v8GAAAA9v8GAAIA9//5//3/9//5////9//5/wEA9//5/wMA9//7//v/9//7//3/9//7////9//7/wEA9//7/wMA9//7/wUA9//9//n/9//9//v/9//9//3/9//9////9//9/wEA9//9/wMA9//9/wUA9//9/wcA9/////n/9/////v/9/////3/9///////9////wEA9////wMA9////wUA9////wcA9/8BAPn/9/8BAPv/9/8BAP3/9/8BAP//9/8BAAEA9/8BAAMA9/8BAAUA9/8BAAcA9/8DAPn/9/8DAPv/9/8DAP3/9/8DAP//9/8DAAEA9/8DAAMA9/8DAAUA9/8DAAcA9/8FAPv/9/8FAP3/9/8FAP//9/8FAAEA9/8FAAMA9/8FAAUA9/8HAP3/9/8HAP//9/8HAAEA9/8HAAMA+P/4//z/+P/4//7/+P/4/wAA+P/4/wIA+P/4/wQA+P/6//r/+P/6//z/+P/6//7/+P/6/wAA+P/6/wIA+P/6/wQA+P/6/wYA+P/8//j/+P/8//r/+P/8//z/+P/8//7/+P/8/wAA+P/8/wIA+P/8/wQA+P/8/wYA+P/8/wgA+P/+//j/+P/+//r/+P/+//z/+P/+//7/+P/+/wAA+P/+/wIA+P/+/wQA+P/+/wYA+P/+/wgA+P8AAPj/+P8AAPr/+P8AAPz/+P8AAP7/+P8AAAAA+P8AAAIA+P8AAAQA+P8AAAYA+P8AAAgA+P8CAPj/+P8CAPr/+P8CAPz/+P8CAP7/+P8CAAAA+P8CAAIA+P8CAAQA+P8CAAYA+P8CAAgA+P8EAPj/+P8EAPr/+P8EAPz/+P8EAP7/+P8EAAAA+P8EAAIA+P8EAAQA+P8EAAYA+P8EAAgA+P8GAPr/+P8GAPz/+P8GAP7/+P8GAAAA+P8GAAIA+P8GAAQA+P8GAAYA+P8IAPz/+P8IAP7/+P8IAAAA+P8IAAIA+P8IAAQA+f/3//3/+f/3////+f/3/wEA+f/3/wMA+f/5//n/+f/5//v/+f/5//3/+f/5////+f/5/wEA+f/5/wMA+f/5/wUA+f/5/wcA+f/7//n/+f/7//v/+f/7//3/+f/7////+f/7/wEA+f/7/wMA+f/7/wUA+f/7/wcA+f/9//f/+f/9//n/+f/9//v/+f/9//3/+f/9////+f/9/wEA+f/9/wMA+f/9/wUA+f/9/wcA+f/9/wkA+f////f/+f////n/+f////v/+f////3/+f//////+f///wEA+f///wMA+f///wUA+f///wcA+f///wkA+f8BAPf/+f8BAPn/+f8BAPv/+f8BAP3/+f8BAP//+f8BAAEA+f8BAAMA+f8BAAUA+f8BAAcA+f8BAAkA+f8DAPf/+f8DAPn/+f8DAPv/+f8DAP3/+f8DAP//+f8DAAEA+f8DAAMA+f8DAAUA+f8DAAcA+f8DAAkA+f8FAPn/+f8FAPv/+f8FAP3/+f8FAP//+f8FAAEA+f8FAAMA+f8FAAUA+f8FAAcA+f8HAPn/+f8HAPv/+f8HAP3/+f8HAP//+f8HAAEA+f8HAAMA+f8HAAUA+f8HAAcA+f8JAP3/+f8JAP//+f8JAAEA+f8JAAMA+v/2//7/+v/2/wAA+v/2/wIA+v/4//r/+v/4//z/+v/4//7/+v/4/wAA+v/4/wIA+v/4/wQA+v/4/wYA+v/6//j/+v/6//r/+v/6//z/+v/6//7/+v/6/wAA+v/6/wIA+v/6/wQA+v/6/wYA+v/6/wgA+v/8//j/+v/8//r/+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/8/wYA+v/8/wgA+v/+//b/+v/+//j/+v/+//r/+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v/+/wYA+v/+/wgA+v/+/woA+v8AAPb/+v8AAPj/+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8AAAgA+v8AAAoA+v8CAPb/+v8CAPj/+v8CAPr/+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8CAAYA+v8CAAgA+v8CAAoA+v8EAPj/+v8EAPr/+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8EAAYA+v8EAAgA+v8GAPj/+v8GAPr/+v8GAPz/+v8GAP7/+v8GAAAA+v8GAAIA+v8GAAQA+v8GAAYA+v8GAAgA+v8IAPr/+v8IAPz/+v8IAP7/+v8IAAAA+v8IAAIA+v8IAAQA+v8IAAYA+v8KAP7/+v8KAAAA+v8KAAIA+//1////+//1/wEA+//3//v/+//3//3/+//3////+//3/wEA+//3/wMA+//3/wUA+//5//n/+//5//v/+//5//3/+//5////+//5/wEA+//5/wMA+//5/wUA+//5/wcA+//7//f/+//7//n/+//7//v/+//7//3/+//7////+//7/wEA+//7/wMA+//7/wUA+//7/wcA+//7/wkA+//9//f/+//9//n/+//9//v/+//9//3/+//9////+//9/wEA+//9/wMA+//9/wUA+//9/wcA+//9/wkA+/////X/+/////f/+/////n/+/////v/+/////3/+///////+////wEA+////wMA+////wUA+////wcA+////wkA+////wsA+/8BAPX/+/8BAPf/+/8BAPn/+/8BAPv/+/8BAP3/+/8BAP//+/8BAAEA+/8BAAMA+/8BAAUA+/8BAAcA+/8BAAkA+/8BAAsA+/8DAPf/+/8DAPn/+/8DAPv/+/8DAP3/+/8DAP//+/8DAAEA+/8DAAMA+/8DAAUA+/8DAAcA+/8DAAkA+/8FAPf/+/8FAPn/+/8FAPv/+/8FAP3/+/8FAP//+/8FAAEA+/8FAAMA+/8FAAUA+/8FAAcA+/8FAAkA+/8HAPn/+/8HAPv/+/8HAP3/+/8HAP//+/8HAAEA+/8HAAMA+/8HAAUA+/8HAAcA+/8JAPv/+/8JAP3/+/8JAP//+/8JAAEA+/8JAAMA+/8JAAUA+/8LAP//+/8LAAEA/P/2//z//P/2//7//P/2/wAA/P/2/wIA/P/2/wQA/P/4//j//P/4//r//P/4//z//P/4//7//P/4/wAA/P/4/wIA/P/4/wQA/P/4/wYA/P/4/wgA/P/6//j//P/6//r//P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/6/wYA/P/6/wgA/P/8//b//P/8//j//P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/8/wgA/P/8/woA/P/+//b//P/+//j//P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P/+/wgA/P/+/woA/P8AAPb//P8AAPj//P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8AAAgA/P8AAAoA/P8CAPb//P8CAPj//P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8CAAgA/P8CAAoA/P8EAPb//P8EAPj//P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8EAAgA/P8EAAoA/P8GAPj//P8GAPr//P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8GAAYA/P8GAAgA/P8IAPj//P8IAPr//P8IAPz//P8IAP7//P8IAAAA/P8IAAIA/P8IAAQA/P8IAAYA/P8IAAgA/P8KAPz//P8KAP7//P8KAAAA/P8KAAIA/P8KAAQA/f/1//3//f/1/////f/1/wEA/f/1/wMA/f/3//n//f/3//v//f/3//3//f/3/////f/3/wEA/f/3/wMA/f/3/wUA/f/3/wcA/f/5//f//f/5//n//f/5//v//f/5//3//f/5/////f/5/wEA/f/5/wMA/f/5/wUA/f/5/wcA/f/5/wkA/f/7//f//f/7//n//f/7//v//f/7//3//f/7/////f/7/wEA/f/7/wMA/f/7/wUA/f/7/wcA/f/7/wkA/f/9//X//f/9//f//f/9//n//f/9//v//f/9//3//f/9/////f/9/wEA/f/9/wMA/f/9/wUA/f/9/wcA/f/9/wkA/f/9/wsA/f////X//f////f//f////n//f////v//f////3//f///////f///wEA/f///wMA/f///wUA/f///wcA/f///wkA/f///wsA/f8BAPX//f8BAPf//f8BAPn//f8BAPv//f8BAP3//f8BAP///f8BAAEA/f8BAAMA/f8BAAUA/f8BAAcA/f8BAAkA/f8BAAsA/f8DAPX//f8DAPf//f8DAPn//f8DAPv//f8DAP3//f8DAP///f8DAAEA/f8DAAMA/f8DAAUA/f8DAAcA/f8DAAkA/f8DAAsA/f8FAPf//f8FAPn//f8FAPv//f8FAP3//f8FAP///f8FAAEA/f8FAAMA/f8FAAUA/f8FAAcA/f8FAAkA/f8HAPf//f8HAPn//f8HAPv//f8HAP3//f8HAP///f8HAAEA/f8HAAMA/f8HAAUA/f8HAAcA/f8HAAkA/f8JAPn//f8JAPv//f8JAP3//f8JAP///f8JAAEA/f8JAAMA/f8JAAUA/f8JAAcA/f8LAP3//f8LAP///f8LAAEA/f8LAAMA/v/0/wAA/v/2//r//v/2//z//v/2//7//v/2/wAA/v/2/wIA/v/2/wQA/v/2/wYA/v/4//j//v/4//r//v/4//z//v/4//7//v/4/wAA/v/4/wIA/v/4/wQA/v/4/wYA/v/4/wgA/v/6//b//v/6//j//v/6//r//v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/6/wYA/v/6/wgA/v/6/woA/v/8//b//v/8//j//v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/8/wgA/v/8/woA/v/+//b//v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v/+/woA/v8AAPT//v8AAPb//v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8AAAoA/v8AAAwA/v8CAPb//v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8CAAoA/v8EAPb//v8EAPj//v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8EAAgA/v8EAAoA/v8GAPb//v8GAPj//v8GAPr//v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8GAAYA/v8GAAgA/v8GAAoA/v8IAPj//v8IAPr//v8IAPz//v8IAP7//v8IAAAA/v8IAAIA/v8IAAQA/v8IAAYA/v8IAAgA/v8KAPr//v8KAPz//v8KAP7//v8KAAAA/v8KAAIA/v8KAAQA/v8KAAYA/v8MAAAA///1//v////1//3////1///////1/wEA///1/wMA///1/wUA///3//n////3//v////3//3////3///////3/wEA///3/wMA///3/wUA///3/wcA///5//f////5//n////5//v////5//3////5///////5/wEA///5/wMA///5/wUA///5/wcA///5/wkA///7//X////7//f////7//n////7//v////7//3////7///////7/wEA///7/wMA///7/wUA///7/wcA///7/wkA///7/wsA///9//X////9//f////9//n////9//v////9//3////9///////9/wEA///9/wMA///9/wUA///9/wcA///9/wkA///9/wsA//////X///////f///////n///////v///////3//////////////wEA/////wMA/////wUA/////wcA/////wkA/////wsA//8BAPX///8BAPf///8BAPn///8BAPv///8BAP3///8BAP////8BAAEA//8BAAMA//8BAAUA//8BAAcA//8BAAkA//8BAAsA//8DAPX///8DAPf///8DAPn///8DAPv///8DAP3///8DAP////8DAAEA//8DAAMA//8DAAUA//8DAAcA//8DAAkA//8DAAsA//8FAPX///8FAPf///8FAPn///8FAPv///8FAP3///8FAP////8FAAEA//8FAAMA//8FAAUA//8FAAcA//8FAAkA//8FAAsA//8HAPf///8HAPn///8HAPv///8HAP3///8HAP////8HAAEA//8HAAMA//8HAAUA//8HAAcA//8HAAkA//8JAPn///8JAPv///8JAP3///8JAP////8JAAEA//8JAAMA//8JAAUA//8JAAcA//8LAPv///8LAP3///8LAP////8LAAEA//8LAAMA//8LAAUAAAD0//7/AAD0/wAAAAD0/wIAAAD2//r/AAD2//z/AAD2//7/AAD2/wAAAAD2/wIAAAD2/wQAAAD2/wYAAAD4//j/AAD4//r/AAD4//z/AAD4//7/AAD4/wAAAAD4/wIAAAD4/wQAAAD4/wYAAAD4/wgAAAD6//b/AAD6//j/AAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD6/wgAAAD6/woAAAD8//b/AAD8//j/AAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD8/wgAAAD8/woAAAD+//T/AAD+//b/AAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD+/woAAAD+/wwAAAAAAPT/AAAAAPb/AAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAAAAAoAAAAAAAwAAAACAPT/AAACAPb/AAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAACAAoAAAACAAwAAAAEAPb/AAAEAPj/AAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAEAAgAAAAEAAoAAAAGAPb/AAAGAPj/AAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAGAAgAAAAGAAoAAAAIAPj/AAAIAPr/AAAIAPz/AAAIAP7/AAAIAAAAAAAIAAIAAAAIAAQAAAAIAAYAAAAIAAgAAAAKAPr/AAAKAPz/AAAKAP7/AAAKAAAAAAAKAAIAAAAKAAQAAAAKAAYAAAAMAP7/AAAMAAAAAAAMAAIAAQD1//v/AQD1//3/AQD1////AQD1/wEAAQD1/wMAAQD1/wUAAQD3//n/AQD3//v/AQD3//3/AQD3////AQD3/wEAAQD3/wMAAQD3/wUAAQD3/wcAAQD5//f/AQD5//n/AQD5//v/AQD5//3/AQD5////AQD5/wEAAQD5/wMAAQD5/wUAAQD5/wcAAQD5/wkAAQD7//X/AQD7//f/AQD7//n/AQD7//v/AQD7//3/AQD7////AQD7/wEAAQD7/wMAAQD7/wUAAQD7/wcAAQD7/wkAAQD7/wsAAQD9//X/AQD9//f/AQD9//n/AQD9//v/AQD9//3/AQD9////AQD9/wEAAQD9/wMAAQD9/wUAAQD9/wcAAQD9/wkAAQD9/wsAAQD///X/AQD///f/AQD///n/AQD///v/AQD///3/AQD/////AQD//wEAAQD//wMAAQD//wUAAQD//wcAAQD//wkAAQD//wsAAQABAPX/AQABAPf/AQABAPn/AQABAPv/AQABAP3/AQABAP//AQABAAEAAQABAAMAAQABAAUAAQABAAcAAQABAAkAAQABAAsAAQADAPX/AQADAPf/AQADAPn/AQADAPv/AQADAP3/AQADAP//AQADAAEAAQADAAMAAQADAAUAAQADAAcAAQADAAkAAQADAAsAAQAFAPX/AQAFAPf/AQAFAPn/AQAFAPv/AQAFAP3/AQAFAP//AQAFAAEAAQAFAAMAAQAFAAUAAQAFAAcAAQAFAAkAAQAFAAsAAQAHAPf/AQAHAPn/AQAHAPv/AQAHAP3/AQAHAP//AQAHAAEAAQAHAAMAAQAHAAUAAQAHAAcAAQAHAAkAAQAJAPn/AQAJAPv/AQAJAP3/AQAJAP//AQAJAAEAAQAJAAMAAQAJAAUAAQAJAAcAAQALAPv/AQALAP3/AQALAP//AQALAAEAAQALAAMAAQALAAUAAgD0/wAAAgD2//r/AgD2//z/AgD2//7/AgD2/wAAAgD2/wIAAgD2/wQAAgD2/wYAAgD4//j/AgD4//r/AgD4//z/AgD4//7/AgD4/wAAAgD4/wIAAgD4/wQAAgD4/wYAAgD4/wgAAgD6//b/AgD6//j/AgD6//r/AgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD6/wYAAgD6/wgAAgD6/woAAgD8//b/AgD8//j/AgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD8/wgAAgD8/woAAgD+//b/AgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD+/woAAgAAAPT/AgAAAPb/AgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgAAAAoAAgAAAAwAAgACAPb/AgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgACAAoAAgAEAPb/AgAEAPj/AgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAEAAgAAgAEAAoAAgAGAPb/AgAGAPj/AgAGAPr/AgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAGAAYAAgAGAAgAAgAGAAoAAgAIAPj/AgAIAPr/AgAIAPz/AgAIAP7/AgAIAAAAAgAIAAIAAgAIAAQAAgAIAAYAAgAIAAgAAgAKAPr/AgAKAPz/AgAKAP7/AgAKAAAAAgAKAAIAAgAKAAQAAgAKAAYAAgAMAAAAAwD1//3/AwD1////AwD1/wEAAwD1/wMAAwD3//n/AwD3//v/AwD3//3/AwD3////AwD3/wEAAwD3/wMAAwD3/wUAAwD3/wcAAwD5//f/AwD5//n/AwD5//v/AwD5//3/AwD5////AwD5/wEAAwD5/wMAAwD5/wUAAwD5/wcAAwD5/wkAAwD7//f/AwD7//n/AwD7//v/AwD7//3/AwD7////AwD7/wEAAwD7/wMAAwD7/wUAAwD7/wcAAwD7/wkAAwD9//X/AwD9//f/AwD9//n/AwD9//v/AwD9//3/AwD9////AwD9/wEAAwD9/wMAAwD9/wUAAwD9/wcAAwD9/wkAAwD9/wsAAwD///X/AwD///f/AwD///n/AwD///v/AwD///3/AwD/////AwD//wEAAwD//wMAAwD//wUAAwD//wcAAwD//wkAAwD//wsAAwABAPX/AwABAPf/AwABAPn/AwABAPv/AwABAP3/AwABAP//AwABAAEAAwABAAMAAwABAAUAAwABAAcAAwABAAkAAwABAAsAAwADAPX/AwADAPf/AwADAPn/AwADAPv/AwADAP3/AwADAP//AwADAAEAAwADAAMAAwADAAUAAwADAAcAAwADAAkAAwADAAsAAwAFAPf/AwAFAPn/AwAFAPv/AwAFAP3/AwAFAP//AwAFAAEAAwAFAAMAAwAFAAUAAwAFAAcAAwAFAAkAAwAHAPf/AwAHAPn/AwAHAPv/AwAHAP3/AwAHAP//AwAHAAEAAwAHAAMAAwAHAAUAAwAHAAcAAwAHAAkAAwAJAPn/AwAJAPv/AwAJAP3/AwAJAP//AwAJAAEAAwAJAAMAAwAJAAUAAwAJAAcAAwALAP3/AwALAP//AwALAAEAAwALAAMABAD2//z/BAD2//7/BAD2/wAABAD2/wIABAD2/wQABAD4//j/BAD4//r/BAD4//z/BAD4//7/BAD4/wAABAD4/wIABAD4/wQABAD4/wYABAD4/wgABAD6//j/BAD6//r/BAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD6/wYABAD6/wgABAD8//b/BAD8//j/BAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD8/wgABAD8/woABAD+//b/BAD+//j/BAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD+/wgABAD+/woABAAAAPb/BAAAAPj/BAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAAAAAgABAAAAAoABAACAPb/BAACAPj/BAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAACAAgABAACAAoABAAEAPb/BAAEAPj/BAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAEAAgABAAEAAoABAAGAPj/BAAGAPr/BAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAGAAYABAAGAAgABAAIAPj/BAAIAPr/BAAIAPz/BAAIAP7/BAAIAAAABAAIAAIABAAIAAQABAAIAAYABAAIAAgABAAKAPz/BAAKAP7/BAAKAAAABAAKAAIABAAKAAQABQD1////BQD1/wEABQD3//v/BQD3//3/BQD3////BQD3/wEABQD3/wMABQD3/wUABQD5//n/BQD5//v/BQD5//3/BQD5////BQD5/wEABQD5/wMABQD5/wUABQD5/wcABQD7//f/BQD7//n/BQD7//v/BQD7//3/BQD7////BQD7/wEABQD7/wMABQD7/wUABQD7/wcABQD7/wkABQD9//f/BQD9//n/BQD9//v/BQD9//3/BQD9////BQD9/wEABQD9/wMABQD9/wUABQD9/wcABQD9/wkABQD///X/BQD///f/BQD///n/BQD///v/BQD///3/BQD/////BQD//wEABQD//wMABQD//wUABQD//wcABQD//wkABQD//wsABQABAPX/BQABAPf/BQABAPn/BQABAPv/BQABAP3/BQABAP//BQABAAEABQABAAMABQABAAUABQABAAcABQABAAkABQABAAsABQADAPf/BQADAPn/BQADAPv/BQADAP3/BQADAP//BQADAAEABQADAAMABQADAAUABQADAAcABQADAAkABQAFAPf/BQAFAPn/BQAFAPv/BQAFAP3/BQAFAP//BQAFAAEABQAFAAMABQAFAAUABQAFAAcABQAFAAkABQAHAPn/BQAHAPv/BQAHAP3/BQAHAP//BQAHAAEABQAHAAMABQAHAAUABQAHAAcABQAJAPv/BQAJAP3/BQAJAP//BQAJAAEABQAJAAMABQAJAAUABQALAP//BQALAAEABgD2//7/BgD2/wAABgD2/wIABgD4//r/BgD4//z/BgD4//7/BgD4/wAABgD4/wIABgD4/wQABgD4/wYABgD6//j/BgD6//r/BgD6//z/BgD6//7/BgD6/wAABgD6/wIABgD6/wQABgD6/wYABgD6/wgABgD8//j/BgD8//r/BgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD8/wYABgD8/wgABgD+//b/BgD+//j/BgD+//r/BgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD+/wYABgD+/wgABgD+/woABgAAAPb/BgAAAPj/BgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgAAAAgABgAAAAoABgACAPb/BgACAPj/BgACAPr/BgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgACAAYABgACAAgABgACAAoABgAEAPj/BgAEAPr/BgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAEAAYABgAEAAgABgAGAPj/BgAGAPr/BgAGAPz/BgAGAP7/BgAGAAAABgAGAAIABgAGAAQABgAGAAYABgAGAAgABgAIAPr/BgAIAPz/BgAIAP7/BgAIAAAABgAIAAIABgAIAAQABgAIAAYABgAKAP7/BgAKAAAABgAKAAIABwD3//3/BwD3////BwD3/wEABwD3/wMABwD5//n/BwD5//v/BwD5//3/BwD5////BwD5/wEABwD5/wMABwD5/wUABwD5/wcABwD7//n/BwD7//v/BwD7//3/BwD7////BwD7/wEABwD7/wMABwD7/wUABwD7/wcABwD9//f/BwD9//n/BwD9//v/BwD9//3/BwD9////BwD9/wEABwD9/wMABwD9/wUABwD9/wcABwD9/wkABwD///f/BwD///n/BwD///v/BwD///3/BwD/////BwD//wEABwD//wMABwD//wUABwD//wcABwD//wkABwABAPf/BwABAPn/BwABAPv/BwABAP3/BwABAP//BwABAAEABwABAAMABwABAAUABwABAAcABwABAAkABwADAPf/BwADAPn/BwADAPv/BwADAP3/BwADAP//BwADAAEABwADAAMABwADAAUABwADAAcABwADAAkABwAFAPn/BwAFAPv/BwAFAP3/BwAFAP//BwAFAAEABwAFAAMABwAFAAUABwAFAAcABwAHAPn/BwAHAPv/BwAHAP3/BwAHAP//BwAHAAEABwAHAAMABwAHAAUABwAHAAcABwAJAP3/BwAJAP//BwAJAAEABwAJAAMACAD4//z/CAD4//7/CAD4/wAACAD4/wIACAD4/wQACAD6//r/CAD6//z/CAD6//7/CAD6/wAACAD6/wIACAD6/wQACAD6/wYACAD8//j/CAD8//r/CAD8//z/CAD8//7/CAD8/wAACAD8/wIACAD8/wQACAD8/wYACAD8/wgACAD+//j/CAD+//r/CAD+//z/CAD+//7/CAD+/wAACAD+/wIACAD+/wQACAD+/wYACAD+/wgACAAAAPj/CAAAAPr/CAAAAPz/CAAAAP7/CAAAAAAACAAAAAIACAAAAAQACAAAAAYACAAAAAgACAACAPj/CAACAPr/CAACAPz/CAACAP7/CAACAAAACAACAAIACAACAAQACAACAAYACAACAAgACAAEAPj/CAAEAPr/CAAEAPz/CAAEAP7/CAAEAAAACAAEAAIACAAEAAQACAAEAAYACAAEAAgACAAGAPr/CAAGAPz/CAAGAP7/CAAGAAAACAAGAAIACAAGAAQACAAGAAYACAAIAPz/CAAIAP7/CAAIAAAACAAIAAIACAAIAAQACQD5//3/CQD5////CQD5/wEACQD5/wMACQD7//v/CQD7//3/CQD7////CQD7/wEACQD7/wMACQD7/wUACQD9//n/CQD9//v/CQD9//3/CQD9////CQD9/wEACQD9/wMACQD9/wUACQD9/wcACQD///n/CQD///v/CQD///3/CQD/////CQD//wEACQD//wMACQD//wUACQD//wcACQABAPn/CQABAPv/CQABAP3/CQABAP//CQABAAEACQABAAMACQABAAUACQABAAcACQADAPn/CQADAPv/CQADAP3/CQADAP//CQADAAEACQADAAMACQADAAUACQADAAcACQAFAPv/CQAFAP3/CQAFAP//CQAFAAEACQAFAAMACQAFAAUACQAHAP3/CQAHAP//CQAHAAEACQAHAAMACgD6//7/CgD6/wAACgD6/wIACgD8//z/CgD8//7/CgD8/wAACgD8/wIACgD8/wQACgD+//r/CgD+//z/CgD+//7/CgD+/wAACgD+/wIACgD+/wQACgD+/wYACgAAAPr/CgAAAPz/CgAAAP7/CgAAAAAACgAAAAIACgAAAAQACgAAAAYACgACAPr/CgACAPz/CgACAP7/CgACAAAACgACAAIACgACAAQACgACAAYACgAEAPz/CgAEAP7/CgAEAAAACgAEAAIACgAEAAQACgAGAP7/CgAGAAAACgAGAAIACwD7////CwD7/wEACwD9//3/CwD9////CwD9/wEACwD9/wMACwD///v/CwD///3/CwD/////CwD//wEACwD//wMACwD//wUACwABAPv/CwABAP3/CwABAP//CwABAAEACwABAAMACwABAAUACwADAP3/CwADAP//CwADAAEACwADAAMACwAFAP//CwAFAAEADAD+/wAADAAAAP7/DAAAAAAADAAAAAIADAACAAAA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.01772783003829353,\"absorptive\":true,\"n_reflections\":1916,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", + "Fe (bcc)": "{\"name\":\"Fe\",\"spacegroup\":\"Im-3m (229)\",\"pointgroup\":\"m-3m\",\"cell\":[[2.866,0.0,0.0],[0.0,2.866,0.0],[0.0,0.0,2.866]],\"recip\":[[0.34891835310537334,0.0,0.0],[0.0,0.34891835310537334,0.0],[0.0,0.0,0.34891835310537334]],\"positions_frac\":[[0.0,0.0,0.0],[0.5,0.5,0.5]],\"numbers\":[26,26],\"symbols\":[\"Fe\",\"Fe\"],\"colors\":[[0.878,0.4,0.2],[0.878,0.4,0.2]],\"radii\":[1.32,1.32],\"hkl_i16\":\"+P/+//7/+P/+/wAA+P/+/wIA+P//////+P///wEA+P8AAP7/+P8AAAAA+P8AAAIA+P8BAP//+P8BAAEA+P8CAP7/+P8CAAAA+P8CAAIA+f/8////+f/8/wEA+f/9//7/+f/9/wAA+f/9/wIA+f/+//3/+f/+////+f/+/wEA+f/+/wMA+f////z/+f////7/+f///wAA+f///wIA+f///wQA+f8AAP3/+f8AAP//+f8AAAEA+f8AAAMA+f8BAPz/+f8BAP7/+f8BAAAA+f8BAAIA+f8BAAQA+f8CAP3/+f8CAP//+f8CAAEA+f8CAAMA+f8DAP7/+f8DAAAA+f8DAAIA+f8EAP//+f8EAAEA+v/6/wAA+v/7//3/+v/7////+v/7/wEA+v/7/wMA+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/9//v/+v/9//3/+v/9////+v/9/wEA+v/9/wMA+v/9/wUA+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v////v/+v////3/+v//////+v///wEA+v///wMA+v///wUA+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8BAPv/+v8BAP3/+v8BAP//+v8BAAEA+v8BAAMA+v8BAAUA+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8DAPv/+v8DAP3/+v8DAP//+v8DAAEA+v8DAAMA+v8DAAUA+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8FAP3/+v8FAP//+v8FAAEA+v8FAAMA+v8GAAAA+//6//3/+//6////+//6/wEA+//6/wMA+//7//z/+//7//7/+//7/wAA+//7/wIA+//7/wQA+//8//v/+//8//3/+//8////+//8/wEA+//8/wMA+//8/wUA+//9//r/+//9//z/+//9//7/+//9/wAA+//9/wIA+//9/wQA+//9/wYA+//+//v/+//+//3/+//+////+//+/wEA+//+/wMA+//+/wUA+/////r/+/////z/+/////7/+////wAA+////wIA+////wQA+////wYA+/8AAPv/+/8AAP3/+/8AAP//+/8AAAEA+/8AAAMA+/8AAAUA+/8BAPr/+/8BAPz/+/8BAP7/+/8BAAAA+/8BAAIA+/8BAAQA+/8BAAYA+/8CAPv/+/8CAP3/+/8CAP//+/8CAAEA+/8CAAMA+/8CAAUA+/8DAPr/+/8DAPz/+/8DAP7/+/8DAAAA+/8DAAIA+/8DAAQA+/8DAAYA+/8EAPv/+/8EAP3/+/8EAP//+/8EAAEA+/8EAAMA+/8EAAUA+/8FAPz/+/8FAP7/+/8FAAAA+/8FAAIA+/8FAAQA+/8GAP3/+/8GAP//+/8GAAEA+/8GAAMA/P/5/////P/5/wEA/P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/7//v//P/7//3//P/7/////P/7/wEA/P/7/wMA/P/7/wUA/P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/9//v//P/9//3//P/9/////P/9/wEA/P/9/wMA/P/9/wUA/P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P////n//P////v//P////3//P///////P///wEA/P///wMA/P///wUA/P///wcA/P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8BAPn//P8BAPv//P8BAP3//P8BAP///P8BAAEA/P8BAAMA/P8BAAUA/P8BAAcA/P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8DAPv//P8DAP3//P8DAP///P8DAAEA/P8DAAMA/P8DAAUA/P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8FAPv//P8FAP3//P8FAP///P8FAAEA/P8FAAMA/P8FAAUA/P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8HAP///P8HAAEA/f/5//7//f/5/wAA/f/5/wIA/f/6//v//f/6//3//f/6/////f/6/wEA/f/6/wMA/f/6/wUA/f/7//r//f/7//z//f/7//7//f/7/wAA/f/7/wIA/f/7/wQA/f/7/wYA/f/8//v//f/8//3//f/8/////f/8/wEA/f/8/wMA/f/8/wUA/f/9//r//f/9//z//f/9//7//f/9/wAA/f/9/wIA/f/9/wQA/f/9/wYA/f/+//n//f/+//v//f/+//3//f/+/////f/+/wEA/f/+/wMA/f/+/wUA/f/+/wcA/f////r//f////z//f////7//f///wAA/f///wIA/f///wQA/f///wYA/f8AAPn//f8AAPv//f8AAP3//f8AAP///f8AAAEA/f8AAAMA/f8AAAUA/f8AAAcA/f8BAPr//f8BAPz//f8BAP7//f8BAAAA/f8BAAIA/f8BAAQA/f8BAAYA/f8CAPn//f8CAPv//f8CAP3//f8CAP///f8CAAEA/f8CAAMA/f8CAAUA/f8CAAcA/f8DAPr//f8DAPz//f8DAP7//f8DAAAA/f8DAAIA/f8DAAQA/f8DAAYA/f8EAPv//f8EAP3//f8EAP///f8EAAEA/f8EAAMA/f8EAAUA/f8FAPr//f8FAPz//f8FAP7//f8FAAAA/f8FAAIA/f8FAAQA/f8FAAYA/f8GAPv//f8GAP3//f8GAP///f8GAAEA/f8GAAMA/f8GAAUA/f8HAP7//f8HAAAA/f8HAAIA/v/4//7//v/4/wAA/v/4/wIA/v/5//3//v/5/////v/5/wEA/v/5/wMA/v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/7//v//v/7//3//v/7/////v/7/wEA/v/7/wMA/v/7/wUA/v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/9//n//v/9//v//v/9//3//v/9/////v/9/wEA/v/9/wMA/v/9/wUA/v/9/wcA/v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v////n//v////v//v////3//v///////v///wEA/v///wMA/v///wUA/v///wcA/v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8BAPn//v8BAPv//v8BAP3//v8BAP///v8BAAEA/v8BAAMA/v8BAAUA/v8BAAcA/v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8DAPn//v8DAPv//v8DAP3//v8DAP///v8DAAEA/v8DAAMA/v8DAAUA/v8DAAcA/v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8FAPv//v8FAP3//v8FAP///v8FAAEA/v8FAAMA/v8FAAUA/v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8HAP3//v8HAP///v8HAAEA/v8HAAMA/v8IAP7//v8IAAAA/v8IAAIA///4///////4/wEA///5//z////5//7////5/wAA///5/wIA///5/wQA///6//v////6//3////6///////6/wEA///6/wMA///6/wUA///7//r////7//z////7//7////7/wAA///7/wIA///7/wQA///7/wYA///8//n////8//v////8//3////8///////8/wEA///8/wMA///8/wUA///8/wcA///9//r////9//z////9//7////9/wAA///9/wIA///9/wQA///9/wYA///+//n////+//v////+//3////+///////+/wEA///+/wMA///+/wUA///+/wcA//////j///////r///////z///////7//////wAA/////wIA/////wQA/////wYA/////wgA//8AAPn///8AAPv///8AAP3///8AAP////8AAAEA//8AAAMA//8AAAUA//8AAAcA//8BAPj///8BAPr///8BAPz///8BAP7///8BAAAA//8BAAIA//8BAAQA//8BAAYA//8BAAgA//8CAPn///8CAPv///8CAP3///8CAP////8CAAEA//8CAAMA//8CAAUA//8CAAcA//8DAPr///8DAPz///8DAP7///8DAAAA//8DAAIA//8DAAQA//8DAAYA//8EAPn///8EAPv///8EAP3///8EAP////8EAAEA//8EAAMA//8EAAUA//8EAAcA//8FAPr///8FAPz///8FAP7///8FAAAA//8FAAIA//8FAAQA//8FAAYA//8GAPv///8GAP3///8GAP////8GAAEA//8GAAMA//8GAAUA//8HAPz///8HAP7///8HAAAA//8HAAIA//8HAAQA//8IAP////8IAAEAAAD4//7/AAD4/wAAAAD4/wIAAAD5//3/AAD5////AAD5/wEAAAD5/wMAAAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD7//v/AAD7//3/AAD7////AAD7/wEAAAD7/wMAAAD7/wUAAAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD9//n/AAD9//v/AAD9//3/AAD9////AAD9/wEAAAD9/wMAAAD9/wUAAAD9/wcAAAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD///n/AAD///v/AAD///3/AAD/////AAD//wEAAAD//wMAAAD//wUAAAD//wcAAAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAABAPn/AAABAPv/AAABAP3/AAABAP//AAABAAEAAAABAAMAAAABAAUAAAABAAcAAAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAADAPn/AAADAPv/AAADAP3/AAADAP//AAADAAEAAAADAAMAAAADAAUAAAADAAcAAAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAFAPv/AAAFAP3/AAAFAP//AAAFAAEAAAAFAAMAAAAFAAUAAAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAHAP3/AAAHAP//AAAHAAEAAAAHAAMAAAAIAP7/AAAIAAAAAAAIAAIAAQD4////AQD4/wEAAQD5//z/AQD5//7/AQD5/wAAAQD5/wIAAQD5/wQAAQD6//v/AQD6//3/AQD6////AQD6/wEAAQD6/wMAAQD6/wUAAQD7//r/AQD7//z/AQD7//7/AQD7/wAAAQD7/wIAAQD7/wQAAQD7/wYAAQD8//n/AQD8//v/AQD8//3/AQD8////AQD8/wEAAQD8/wMAAQD8/wUAAQD8/wcAAQD9//r/AQD9//z/AQD9//7/AQD9/wAAAQD9/wIAAQD9/wQAAQD9/wYAAQD+//n/AQD+//v/AQD+//3/AQD+////AQD+/wEAAQD+/wMAAQD+/wUAAQD+/wcAAQD///j/AQD///r/AQD///z/AQD///7/AQD//wAAAQD//wIAAQD//wQAAQD//wYAAQD//wgAAQAAAPn/AQAAAPv/AQAAAP3/AQAAAP//AQAAAAEAAQAAAAMAAQAAAAUAAQAAAAcAAQABAPj/AQABAPr/AQABAPz/AQABAP7/AQABAAAAAQABAAIAAQABAAQAAQABAAYAAQABAAgAAQACAPn/AQACAPv/AQACAP3/AQACAP//AQACAAEAAQACAAMAAQACAAUAAQACAAcAAQADAPr/AQADAPz/AQADAP7/AQADAAAAAQADAAIAAQADAAQAAQADAAYAAQAEAPn/AQAEAPv/AQAEAP3/AQAEAP//AQAEAAEAAQAEAAMAAQAEAAUAAQAEAAcAAQAFAPr/AQAFAPz/AQAFAP7/AQAFAAAAAQAFAAIAAQAFAAQAAQAFAAYAAQAGAPv/AQAGAP3/AQAGAP//AQAGAAEAAQAGAAMAAQAGAAUAAQAHAPz/AQAHAP7/AQAHAAAAAQAHAAIAAQAHAAQAAQAIAP//AQAIAAEAAgD4//7/AgD4/wAAAgD4/wIAAgD5//3/AgD5////AgD5/wEAAgD5/wMAAgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD7//v/AgD7//3/AgD7////AgD7/wEAAgD7/wMAAgD7/wUAAgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD9//n/AgD9//v/AgD9//3/AgD9////AgD9/wEAAgD9/wMAAgD9/wUAAgD9/wcAAgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD///n/AgD///v/AgD///3/AgD/////AgD//wEAAgD//wMAAgD//wUAAgD//wcAAgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgABAPn/AgABAPv/AgABAP3/AgABAP//AgABAAEAAgABAAMAAgABAAUAAgABAAcAAgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgADAPn/AgADAPv/AgADAP3/AgADAP//AgADAAEAAgADAAMAAgADAAUAAgADAAcAAgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAFAPv/AgAFAP3/AgAFAP//AgAFAAEAAgAFAAMAAgAFAAUAAgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAHAP3/AgAHAP//AgAHAAEAAgAHAAMAAgAIAP7/AgAIAAAAAgAIAAIAAwD5//7/AwD5/wAAAwD5/wIAAwD6//v/AwD6//3/AwD6////AwD6/wEAAwD6/wMAAwD6/wUAAwD7//r/AwD7//z/AwD7//7/AwD7/wAAAwD7/wIAAwD7/wQAAwD7/wYAAwD8//v/AwD8//3/AwD8////AwD8/wEAAwD8/wMAAwD8/wUAAwD9//r/AwD9//z/AwD9//7/AwD9/wAAAwD9/wIAAwD9/wQAAwD9/wYAAwD+//n/AwD+//v/AwD+//3/AwD+////AwD+/wEAAwD+/wMAAwD+/wUAAwD+/wcAAwD///r/AwD///z/AwD///7/AwD//wAAAwD//wIAAwD//wQAAwD//wYAAwAAAPn/AwAAAPv/AwAAAP3/AwAAAP//AwAAAAEAAwAAAAMAAwAAAAUAAwAAAAcAAwABAPr/AwABAPz/AwABAP7/AwABAAAAAwABAAIAAwABAAQAAwABAAYAAwACAPn/AwACAPv/AwACAP3/AwACAP//AwACAAEAAwACAAMAAwACAAUAAwACAAcAAwADAPr/AwADAPz/AwADAP7/AwADAAAAAwADAAIAAwADAAQAAwADAAYAAwAEAPv/AwAEAP3/AwAEAP//AwAEAAEAAwAEAAMAAwAEAAUAAwAFAPr/AwAFAPz/AwAFAP7/AwAFAAAAAwAFAAIAAwAFAAQAAwAFAAYAAwAGAPv/AwAGAP3/AwAGAP//AwAGAAEAAwAGAAMAAwAGAAUAAwAHAP7/AwAHAAAAAwAHAAIABAD5////BAD5/wEABAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD7//v/BAD7//3/BAD7////BAD7/wEABAD7/wMABAD7/wUABAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD9//v/BAD9//3/BAD9////BAD9/wEABAD9/wMABAD9/wUABAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD///n/BAD///v/BAD///3/BAD/////BAD//wEABAD//wMABAD//wUABAD//wcABAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAABAPn/BAABAPv/BAABAP3/BAABAP//BAABAAEABAABAAMABAABAAUABAABAAcABAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAADAPv/BAADAP3/BAADAP//BAADAAEABAADAAMABAADAAUABAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAFAPv/BAAFAP3/BAAFAP//BAAFAAEABAAFAAMABAAFAAUABAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAHAP//BAAHAAEABQD6//3/BQD6////BQD6/wEABQD6/wMABQD7//z/BQD7//7/BQD7/wAABQD7/wIABQD7/wQABQD8//v/BQD8//3/BQD8////BQD8/wEABQD8/wMABQD8/wUABQD9//r/BQD9//z/BQD9//7/BQD9/wAABQD9/wIABQD9/wQABQD9/wYABQD+//v/BQD+//3/BQD+////BQD+/wEABQD+/wMABQD+/wUABQD///r/BQD///z/BQD///7/BQD//wAABQD//wIABQD//wQABQD//wYABQAAAPv/BQAAAP3/BQAAAP//BQAAAAEABQAAAAMABQAAAAUABQABAPr/BQABAPz/BQABAP7/BQABAAAABQABAAIABQABAAQABQABAAYABQACAPv/BQACAP3/BQACAP//BQACAAEABQACAAMABQACAAUABQADAPr/BQADAPz/BQADAP7/BQADAAAABQADAAIABQADAAQABQADAAYABQAEAPv/BQAEAP3/BQAEAP//BQAEAAEABQAEAAMABQAEAAUABQAFAPz/BQAFAP7/BQAFAAAABQAFAAIABQAFAAQABQAGAP3/BQAGAP//BQAGAAEABQAGAAMABgD6/wAABgD7//3/BgD7////BgD7/wEABgD7/wMABgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD9//v/BgD9//3/BgD9////BgD9/wEABgD9/wMABgD9/wUABgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD///v/BgD///3/BgD/////BgD//wEABgD//wMABgD//wUABgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgABAPv/BgABAP3/BgABAP//BgABAAEABgABAAMABgABAAUABgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgADAPv/BgADAP3/BgADAP//BgADAAEABgADAAMABgADAAUABgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAFAP3/BgAFAP//BgAFAAEABgAFAAMABgAGAAAABwD8////BwD8/wEABwD9//7/BwD9/wAABwD9/wIABwD+//3/BwD+////BwD+/wEABwD+/wMABwD///z/BwD///7/BwD//wAABwD//wIABwD//wQABwAAAP3/BwAAAP//BwAAAAEABwAAAAMABwABAPz/BwABAP7/BwABAAAABwABAAIABwABAAQABwACAP3/BwACAP//BwACAAEABwACAAMABwADAP7/BwADAAAABwADAAIABwAEAP//BwAEAAEACAD+//7/CAD+/wAACAD+/wIACAD/////CAD//wEACAAAAP7/CAAAAAAACAAAAAIACAABAP//CAABAAEACAACAP7/CAACAAAACAACAAIA\",\"F2\":\"rpLOOdq/5Dmuks45OEDxOThA8Tnav+Q569f+Odq/5Dk4QPE5OEDxOa6Szjnav+Q5rpLOOThA8Tk4QPE5idQGOjDOFzqJ1AY6idQGOlNWLDpTViw6idQGOjhA8TlTViw6sINFOlNWLDo4QPE5MM4XOrCDRTqwg0U6MM4XOjhA8TlTViw6sINFOlNWLDo4QPE5idQGOlNWLDpTViw6idQGOonUBjowzhc6idQGOjhA8Tk4QPE5rpLOOUA52TmJ1AY6idQGOkA52Tnav+Q5NZEhOvpCODo1kSE62r/kOUA52TlTViw6XeFkOl3hZDpTViw6QDnZOTWRITp3jHc6XV6SOneMdzo1kSE6idQGOl3hZDrxJKA68SSgOl3hZDqJ1AY6rpLOOfpCODpdXpI6/QawOl1ekjr6Qjg6rpLOOYnUBjpd4WQ68SSgOvEkoDpd4WQ6idQGOjWRITp3jHc6XV6SOneMdzo1kSE6QDnZOVNWLDpd4WQ6XeFkOlNWLDpAOdk52r/kOTWRITr6Qjg6NZEhOtq/5DlAOdk5idQGOonUBjpAOdk5rpLOOUA52TmJ1AY6idQGOkA52Tk4QPE5U1YsOrCDRTpTViw6OEDxOThA8Tmwg0U6MleGOjJXhjqwg0U6OEDxOUA52Tmwg0U68SSgOoZ5wjrxJKA6sINFOkA52TlTViw68SSgOv2X8Tr9l/E68SSgOlNWLDqJ1AY6MleGOv2X8TrJZRo7/ZfxOjJXhjqJ1AY6sINFOoZ5wjrJZRo7yWUaO4Z5wjqwg0U6idQGOjJXhjr9l/E6yWUaO/2X8ToyV4Y6idQGOlNWLDrxJKA6/ZfxOv2X8TrxJKA6U1YsOkA52Tmwg0U68SSgOoZ5wjrxJKA6sINFOkA52Tk4QPE5sINFOjJXhjoyV4Y6sINFOjhA8Tk4QPE5U1YsOrCDRTpTViw6OEDxOUA52TmJ1AY6idQGOkA52Tk4QPE5OEDxOdq/5Dk1kSE6+kI4OjWRITrav+Q5OEDxObCDRToyV4Y6MleGOrCDRTo4QPE52r/kOb9NVDr9BrA6GhPYOv0GsDq/TVQ62r/kObCDRTqGecI6yWUaO8llGjuGecI6sINFOjWRITr9BrA6lNowO23KbzuU2jA7/QawOjWRITo4QPE5MleGOsllGjuzcI47s3COO8llGjsyV4Y6OEDxOfpCODoaE9g6bcpvOyQQrDttym87GhPYOvpCODo4QPE5MleGOsllGjuzcI47s3COO8llGjsyV4Y6OEDxOTWRITr9BrA6lNowO23KbzuU2jA7/QawOjWRITqwg0U6hnnCOsllGjvJZRo7hnnCOrCDRTrav+Q5v01UOv0GsDoaE9g6/QawOr9NVDrav+Q5OEDxObCDRToyV4Y6MleGOrCDRTo4QPE52r/kOTWRITr6Qjg6NZEhOtq/5Dk4QPE5OEDxOYnUBjowzhc6idQGOkA52TlTViw6XeFkOl3hZDpTViw6QDnZOUA52Tmwg0U68SSgOoZ5wjrxJKA6sINFOkA52Tmwg0U6hnnCOsllGjvJZRo7hnnCOrCDRTpTViw6hnnCOjeyTDuzcI47N7JMO4Z5wjpTViw6idQGOvEkoDo3skw7UwrUO1MK1Ds3skw78SSgOonUBjpd4WQ6yWUaO1MK1DtaXC48UwrUO8llGjtd4WQ6MM4XOoZ5wjqzcI47WlwuPFpcLjyzcI47hnnCOjDOFzpd4WQ6yWUaO1MK1DtaXC48UwrUO8llGjtd4WQ6idQGOvEkoDo3skw7UwrUO1MK1Ds3skw78SSgOonUBjpTViw6hnnCOjeyTDuzcI47N7JMO4Z5wjpTViw6sINFOoZ5wjrJZRo7yWUaO4Z5wjqwg0U6QDnZObCDRTrxJKA6hnnCOvEkoDqwg0U6QDnZOUA52TlTViw6XeFkOl3hZDpTViw6QDnZOYnUBjowzhc6idQGOq6Szjnav+Q5rpLOOYnUBjpTViw6U1YsOonUBjo1kSE6d4x3Ol1ekjp3jHc6NZEhOlNWLDrxJKA6/ZfxOv2X8TrxJKA6U1YsOjWRITr9BrA6lNowO23KbzuU2jA7/QawOjWRITqJ1AY68SSgOjeyTDtTCtQ7UwrUOzeyTDvxJKA6idQGOq6Szjl3jHc6lNowO0vjBTzfZmw8S+MFPJTaMDt3jHc6rpLOOVNWLDr9l/E6UwrUO9igqTzYoKk8UwrUO/2X8TpTViw62r/kOV1ekjptym8732ZsPPCtBT3fZmw8bcpvO11ekjrav+Q5U1YsOv2X8TpTCtQ72KCpPNigqTxTCtQ7/ZfxOlNWLDquks45d4x3OpTaMDtL4wU832ZsPEvjBTyU2jA7d4x3Oq6SzjmJ1AY68SSgOjeyTDtTCtQ7UwrUOzeyTDvxJKA6idQGOjWRITr9BrA6lNowO23KbzuU2jA7/QawOjWRITpTViw68SSgOv2X8Tr9l/E68SSgOlNWLDo1kSE6d4x3Ol1ekjp3jHc6NZEhOonUBjpTViw6U1YsOonUBjquks452r/kOa6Szjk4QPE5OEDxOThA8TlTViw6sINFOlNWLDo4QPE5idQGOl3hZDrxJKA68SSgOl3hZDqJ1AY6idQGOjJXhjr9l/E6yWUaO/2X8ToyV4Y6idQGOjhA8TkyV4Y6yWUaO7NwjjuzcI47yWUaOzJXhjo4QPE5XeFkOsllGjtTCtQ7WlwuPFMK1DvJZRo7XeFkOlNWLDr9l/E6UwrUO9igqTzYoKk8UwrUO/2X8TpTViw6OEDxOfEkoDqzcI472KCpPJWKgz3YoKk8s3COO/EkoDo4QPE5sINFOsllGjtaXC48lYqDPZWKgz1aXC48yWUaO7CDRTo4QPE58SSgOrNwjjvYoKk8lYqDPdigqTyzcI478SSgOjhA8TlTViw6/ZfxOlMK1DvYoKk82KCpPFMK1Dv9l/E6U1YsOl3hZDrJZRo7UwrUO1pcLjxTCtQ7yWUaO13hZDo4QPE5MleGOsllGjuzcI47s3COO8llGjsyV4Y6OEDxOYnUBjoyV4Y6/ZfxOsllGjv9l/E6MleGOonUBjqJ1AY6XeFkOvEkoDrxJKA6XeFkOonUBjo4QPE5U1YsOrCDRTpTViw6OEDxOThA8Tk4QPE52r/kOevX/jnav+Q5MM4XOrCDRTqwg0U6MM4XOq6Szjn6Qjg6XV6SOv0GsDpdXpI6+kI4Oq6Szjmwg0U6hnnCOsllGjvJZRo7hnnCOrCDRTr6Qjg6GhPYOm3KbzskEKw7bcpvOxoT2Dr6Qjg6MM4XOoZ5wjqzcI47WlwuPFpcLjyzcI47hnnCOjDOFzrav+Q5XV6SOm3KbzvfZmw88K0FPd9mbDxtym87XV6SOtq/5Dmwg0U6yWUaO1pcLjyVioM9lYqDPVpcLjzJZRo7sINFOuvX/jn9BrA6JBCsO/CtBT3wrQU9JBCsO/0GsDrr1/45sINFOsllGjtaXC48lYqDPZWKgz1aXC48yWUaO7CDRTrav+Q5XV6SOm3KbzvfZmw88K0FPd9mbDxtym87XV6SOtq/5Dkwzhc6hnnCOrNwjjtaXC48WlwuPLNwjjuGecI6MM4XOvpCODoaE9g6bcpvOyQQrDttym87GhPYOvpCODqwg0U6hnnCOsllGjvJZRo7hnnCOrCDRTquks45+kI4Ol1ekjr9BrA6XV6SOvpCODquks45MM4XOrCDRTqwg0U6MM4XOtq/5Dnr1/452r/kOThA8Tk4QPE5OEDxOVNWLDqwg0U6U1YsOjhA8TmJ1AY6XeFkOvEkoDrxJKA6XeFkOonUBjqJ1AY6MleGOv2X8TrJZRo7/ZfxOjJXhjqJ1AY6OEDxOTJXhjrJZRo7s3COO7NwjjvJZRo7MleGOjhA8Tld4WQ6yWUaO1MK1DtaXC48UwrUO8llGjtd4WQ6U1YsOv2X8TpTCtQ72KCpPNigqTxTCtQ7/ZfxOlNWLDo4QPE58SSgOrNwjjvYoKk8lYqDPdigqTyzcI478SSgOjhA8Tmwg0U6yWUaO1pcLjyVioM9lYqDPVpcLjzJZRo7sINFOjhA8TnxJKA6s3COO9igqTyVioM92KCpPLNwjjvxJKA6OEDxOVNWLDr9l/E6UwrUO9igqTzYoKk8UwrUO/2X8TpTViw6XeFkOsllGjtTCtQ7WlwuPFMK1DvJZRo7XeFkOjhA8TkyV4Y6yWUaO7NwjjuzcI47yWUaOzJXhjo4QPE5idQGOjJXhjr9l/E6yWUaO/2X8ToyV4Y6idQGOonUBjpd4WQ68SSgOvEkoDpd4WQ6idQGOjhA8TlTViw6sINFOlNWLDo4QPE5OEDxOThA8Tmuks452r/kOa6SzjmJ1AY6U1YsOlNWLDqJ1AY6NZEhOneMdzpdXpI6d4x3OjWRITpTViw68SSgOv2X8Tr9l/E68SSgOlNWLDo1kSE6/QawOpTaMDttym87lNowO/0GsDo1kSE6idQGOvEkoDo3skw7UwrUO1MK1Ds3skw78SSgOonUBjquks45d4x3OpTaMDtL4wU832ZsPEvjBTyU2jA7d4x3Oq6SzjlTViw6/ZfxOlMK1DvYoKk82KCpPFMK1Dv9l/E6U1YsOtq/5DldXpI6bcpvO99mbDzwrQU932ZsPG3KbztdXpI62r/kOVNWLDr9l/E6UwrUO9igqTzYoKk8UwrUO/2X8TpTViw6rpLOOXeMdzqU2jA7S+MFPN9mbDxL4wU8lNowO3eMdzquks45idQGOvEkoDo3skw7UwrUO1MK1Ds3skw78SSgOonUBjo1kSE6/QawOpTaMDttym87lNowO/0GsDo1kSE6U1YsOvEkoDr9l/E6/ZfxOvEkoDpTViw6NZEhOneMdzpdXpI6d4x3OjWRITqJ1AY6U1YsOlNWLDqJ1AY6rpLOOdq/5Dmuks45idQGOjDOFzqJ1AY6QDnZOVNWLDpd4WQ6XeFkOlNWLDpAOdk5QDnZObCDRTrxJKA6hnnCOvEkoDqwg0U6QDnZObCDRTqGecI6yWUaO8llGjuGecI6sINFOlNWLDqGecI6N7JMO7Nwjjs3skw7hnnCOlNWLDqJ1AY68SSgOjeyTDtTCtQ7UwrUOzeyTDvxJKA6idQGOl3hZDrJZRo7UwrUO1pcLjxTCtQ7yWUaO13hZDowzhc6hnnCOrNwjjtaXC48WlwuPLNwjjuGecI6MM4XOl3hZDrJZRo7UwrUO1pcLjxTCtQ7yWUaO13hZDqJ1AY68SSgOjeyTDtTCtQ7UwrUOzeyTDvxJKA6idQGOlNWLDqGecI6N7JMO7Nwjjs3skw7hnnCOlNWLDqwg0U6hnnCOsllGjvJZRo7hnnCOrCDRTpAOdk5sINFOvEkoDqGecI68SSgOrCDRTpAOdk5QDnZOVNWLDpd4WQ6XeFkOlNWLDpAOdk5idQGOjDOFzqJ1AY6OEDxOThA8Tnav+Q5NZEhOvpCODo1kSE62r/kOThA8Tmwg0U6MleGOjJXhjqwg0U6OEDxOdq/5Dm/TVQ6/QawOhoT2Dr9BrA6v01UOtq/5Dmwg0U6hnnCOsllGjvJZRo7hnnCOrCDRTo1kSE6/QawOpTaMDttym87lNowO/0GsDo1kSE6OEDxOTJXhjrJZRo7s3COO7NwjjvJZRo7MleGOjhA8Tn6Qjg6GhPYOm3KbzskEKw7bcpvOxoT2Dr6Qjg6OEDxOTJXhjrJZRo7s3COO7NwjjvJZRo7MleGOjhA8Tk1kSE6/QawOpTaMDttym87lNowO/0GsDo1kSE6sINFOoZ5wjrJZRo7yWUaO4Z5wjqwg0U62r/kOb9NVDr9BrA6GhPYOv0GsDq/TVQ62r/kOThA8Tmwg0U6MleGOjJXhjqwg0U6OEDxOdq/5Dk1kSE6+kI4OjWRITrav+Q5OEDxOThA8TlAOdk5idQGOonUBjpAOdk5OEDxOVNWLDqwg0U6U1YsOjhA8Tk4QPE5sINFOjJXhjoyV4Y6sINFOjhA8TlAOdk5sINFOvEkoDqGecI68SSgOrCDRTpAOdk5U1YsOvEkoDr9l/E6/ZfxOvEkoDpTViw6idQGOjJXhjr9l/E6yWUaO/2X8ToyV4Y6idQGOrCDRTqGecI6yWUaO8llGjuGecI6sINFOonUBjoyV4Y6/ZfxOsllGjv9l/E6MleGOonUBjpTViw68SSgOv2X8Tr9l/E68SSgOlNWLDpAOdk5sINFOvEkoDqGecI68SSgOrCDRTpAOdk5OEDxObCDRToyV4Y6MleGOrCDRTo4QPE5OEDxOVNWLDqwg0U6U1YsOjhA8TlAOdk5idQGOonUBjpAOdk5rpLOOUA52TmJ1AY6idQGOkA52Tnav+Q5NZEhOvpCODo1kSE62r/kOUA52TlTViw6XeFkOl3hZDpTViw6QDnZOTWRITp3jHc6XV6SOneMdzo1kSE6idQGOl3hZDrxJKA68SSgOl3hZDqJ1AY6rpLOOfpCODpdXpI6/QawOl1ekjr6Qjg6rpLOOYnUBjpd4WQ68SSgOvEkoDpd4WQ6idQGOjWRITp3jHc6XV6SOneMdzo1kSE6QDnZOVNWLDpd4WQ6XeFkOlNWLDpAOdk52r/kOTWRITr6Qjg6NZEhOtq/5DlAOdk5idQGOonUBjpAOdk5rpLOOThA8Tk4QPE5idQGOjDOFzqJ1AY6idQGOlNWLDpTViw6idQGOjhA8TlTViw6sINFOlNWLDo4QPE5MM4XOrCDRTqwg0U6MM4XOjhA8TlTViw6sINFOlNWLDo4QPE5idQGOlNWLDpTViw6idQGOonUBjowzhc6idQGOjhA8Tk4QPE5rpLOOdq/5Dmuks45OEDxOThA8Tnav+Q569f+Odq/5Dk4QPE5OEDxOa6Szjnav+Q5rpLOOQ==\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.009831241654189844,\"absorptive\":true,\"n_reflections\":1288,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", + "Ti (hcp)": "{\"name\":\"Ti\",\"spacegroup\":\"P6_3/mmc (194)\",\"pointgroup\":\"6/mmm\",\"cell\":[[2.9505,0.0,0.0],[-1.47525,2.555207953865986,0.0],[0.0,0.0,4.6855]],\"recip\":[[0.3389256058295204,0.19567878976093062,0.0],[0.0,0.39135757952186123,0.0],[0.0,0.0,0.21342439440828084]],\"positions_frac\":[[0.0,0.0,0.0],[0.3333333333333333,0.6666666666666666,0.49999999999999994]],\"numbers\":[22,22],\"symbols\":[\"Ti\",\"Ti\"],\"colors\":[[0.749,0.761,0.78],[0.749,0.761,0.78]],\"radii\":[1.6,1.6],\"hkl_i16\":\"+P8BAP7/+P8BAP//+P8BAAAA+P8BAAEA+P8BAAIA+P8CAPz/+P8CAP3/+P8CAP7/+P8CAP//+P8CAAAA+P8CAAEA+P8CAAIA+P8CAAMA+P8CAAQA+P8DAPv/+P8DAPz/+P8DAP3/+P8DAP7/+P8DAP//+P8DAAAA+P8DAAEA+P8DAAIA+P8DAAMA+P8DAAQA+P8DAAUA+P8EAPr/+P8EAPv/+P8EAPz/+P8EAP3/+P8EAP7/+P8EAP//+P8EAAAA+P8EAAEA+P8EAAIA+P8EAAMA+P8EAAQA+P8EAAUA+P8EAAYA+P8FAPv/+P8FAPz/+P8FAP3/+P8FAP7/+P8FAP//+P8FAAAA+P8FAAEA+P8FAAIA+P8FAAMA+P8FAAQA+P8FAAUA+P8GAPz/+P8GAP3/+P8GAP7/+P8GAP//+P8GAAAA+P8GAAEA+P8GAAIA+P8GAAMA+P8GAAQA+P8HAP7/+P8HAP//+P8HAAAA+P8HAAEA+P8HAAIA+f////7/+f//////+f///wAA+f///wEA+f///wIA+f8AAPv/+f8AAPz/+f8AAP3/+f8AAP7/+f8AAP//+f8AAAAA+f8AAAEA+f8AAAIA+f8AAAMA+f8AAAQA+f8AAAUA+f8BAPn/+f8BAPr/+f8BAPv/+f8BAPz/+f8BAP3/+f8BAP7/+f8BAP//+f8BAAAA+f8BAAEA+f8BAAIA+f8BAAMA+f8BAAQA+f8BAAUA+f8BAAYA+f8BAAcA+f8CAPj/+f8CAPn/+f8CAPr/+f8CAPv/+f8CAPz/+f8CAP3/+f8CAP7/+f8CAP//+f8CAAAA+f8CAAEA+f8CAAIA+f8CAAMA+f8CAAQA+f8CAAUA+f8CAAYA+f8CAAcA+f8CAAgA+f8DAPj/+f8DAPn/+f8DAPr/+f8DAPv/+f8DAPz/+f8DAP3/+f8DAP7/+f8DAP//+f8DAAAA+f8DAAEA+f8DAAIA+f8DAAMA+f8DAAQA+f8DAAUA+f8DAAYA+f8DAAcA+f8DAAgA+f8EAPj/+f8EAPn/+f8EAPr/+f8EAPv/+f8EAPz/+f8EAP3/+f8EAP7/+f8EAP//+f8EAAAA+f8EAAEA+f8EAAIA+f8EAAMA+f8EAAQA+f8EAAUA+f8EAAYA+f8EAAcA+f8EAAgA+f8FAPj/+f8FAPn/+f8FAPr/+f8FAPv/+f8FAPz/+f8FAP3/+f8FAP7/+f8FAP//+f8FAAAA+f8FAAEA+f8FAAIA+f8FAAMA+f8FAAQA+f8FAAUA+f8FAAYA+f8FAAcA+f8FAAgA+f8GAPn/+f8GAPr/+f8GAPv/+f8GAPz/+f8GAP3/+f8GAP7/+f8GAP//+f8GAAAA+f8GAAEA+f8GAAIA+f8GAAMA+f8GAAQA+f8GAAUA+f8GAAYA+f8GAAcA+f8HAPv/+f8HAPz/+f8HAP3/+f8HAP7/+f8HAP//+f8HAAAA+f8HAAEA+f8HAAIA+f8HAAMA+f8HAAQA+f8HAAUA+f8IAP7/+f8IAP//+f8IAAAA+f8IAAEA+f8IAAIA+v/+//z/+v/+//3/+v/+//7/+v/+////+v/+/wAA+v/+/wEA+v/+/wIA+v/+/wMA+v/+/wQA+v////n/+v////r/+v////v/+v////z/+v////3/+v////7/+v//////+v///wAA+v///wEA+v///wIA+v///wMA+v///wQA+v///wUA+v///wYA+v///wcA+v8AAPj/+v8AAPn/+v8AAPr/+v8AAPv/+v8AAPz/+v8AAP3/+v8AAP7/+v8AAP//+v8AAAAA+v8AAAEA+v8AAAIA+v8AAAMA+v8AAAQA+v8AAAUA+v8AAAYA+v8AAAcA+v8AAAgA+v8BAPf/+v8BAPj/+v8BAPn/+v8BAPr/+v8BAPv/+v8BAPz/+v8BAP3/+v8BAP7/+v8BAP//+v8BAAAA+v8BAAEA+v8BAAIA+v8BAAMA+v8BAAQA+v8BAAUA+v8BAAYA+v8BAAcA+v8BAAgA+v8BAAkA+v8CAPb/+v8CAPf/+v8CAPj/+v8CAPn/+v8CAPr/+v8CAPv/+v8CAPz/+v8CAP3/+v8CAP7/+v8CAP//+v8CAAAA+v8CAAEA+v8CAAIA+v8CAAMA+v8CAAQA+v8CAAUA+v8CAAYA+v8CAAcA+v8CAAgA+v8CAAkA+v8CAAoA+v8DAPb/+v8DAPf/+v8DAPj/+v8DAPn/+v8DAPr/+v8DAPv/+v8DAPz/+v8DAP3/+v8DAP7/+v8DAP//+v8DAAAA+v8DAAEA+v8DAAIA+v8DAAMA+v8DAAQA+v8DAAUA+v8DAAYA+v8DAAcA+v8DAAgA+v8DAAkA+v8DAAoA+v8EAPb/+v8EAPf/+v8EAPj/+v8EAPn/+v8EAPr/+v8EAPv/+v8EAPz/+v8EAP3/+v8EAP7/+v8EAP//+v8EAAAA+v8EAAEA+v8EAAIA+v8EAAMA+v8EAAQA+v8EAAUA+v8EAAYA+v8EAAcA+v8EAAgA+v8EAAkA+v8EAAoA+v8FAPf/+v8FAPj/+v8FAPn/+v8FAPr/+v8FAPv/+v8FAPz/+v8FAP3/+v8FAP7/+v8FAP//+v8FAAAA+v8FAAEA+v8FAAIA+v8FAAMA+v8FAAQA+v8FAAUA+v8FAAYA+v8FAAcA+v8FAAgA+v8FAAkA+v8GAPj/+v8GAPn/+v8GAPr/+v8GAPv/+v8GAPz/+v8GAP3/+v8GAP7/+v8GAP//+v8GAAAA+v8GAAEA+v8GAAIA+v8GAAMA+v8GAAQA+v8GAAUA+v8GAAYA+v8GAAcA+v8GAAgA+v8HAPn/+v8HAPr/+v8HAPv/+v8HAPz/+v8HAP3/+v8HAP7/+v8HAP//+v8HAAAA+v8HAAEA+v8HAAIA+v8HAAMA+v8HAAQA+v8HAAUA+v8HAAYA+v8HAAcA+v8IAPz/+v8IAP3/+v8IAP7/+v8IAP//+v8IAAAA+v8IAAEA+v8IAAIA+v8IAAMA+v8IAAQA+//9//v/+//9//z/+//9//3/+//9//7/+//9////+//9/wAA+//9/wEA+//9/wIA+//9/wMA+//9/wQA+//9/wUA+//+//j/+//+//n/+//+//r/+//+//v/+//+//z/+//+//3/+//+//7/+//+////+//+/wAA+//+/wEA+//+/wIA+//+/wMA+//+/wQA+//+/wUA+//+/wYA+//+/wcA+//+/wgA+/////f/+/////j/+/////n/+/////r/+/////v/+/////z/+/////3/+/////7/+///////+////wAA+////wEA+////wIA+////wMA+////wQA+////wUA+////wYA+////wcA+////wgA+////wkA+/8AAPb/+/8AAPf/+/8AAPj/+/8AAPn/+/8AAPr/+/8AAPv/+/8AAPz/+/8AAP3/+/8AAP7/+/8AAP//+/8AAAAA+/8AAAEA+/8AAAIA+/8AAAMA+/8AAAQA+/8AAAUA+/8AAAYA+/8AAAcA+/8AAAgA+/8AAAkA+/8AAAoA+/8BAPX/+/8BAPb/+/8BAPf/+/8BAPj/+/8BAPn/+/8BAPr/+/8BAPv/+/8BAPz/+/8BAP3/+/8BAP7/+/8BAP//+/8BAAAA+/8BAAEA+/8BAAIA+/8BAAMA+/8BAAQA+/8BAAUA+/8BAAYA+/8BAAcA+/8BAAgA+/8BAAkA+/8BAAoA+/8BAAsA+/8CAPX/+/8CAPb/+/8CAPf/+/8CAPj/+/8CAPn/+/8CAPr/+/8CAPv/+/8CAPz/+/8CAP3/+/8CAP7/+/8CAP//+/8CAAAA+/8CAAEA+/8CAAIA+/8CAAMA+/8CAAQA+/8CAAUA+/8CAAYA+/8CAAcA+/8CAAgA+/8CAAkA+/8CAAoA+/8CAAsA+/8DAPX/+/8DAPb/+/8DAPf/+/8DAPj/+/8DAPn/+/8DAPr/+/8DAPv/+/8DAPz/+/8DAP3/+/8DAP7/+/8DAP//+/8DAAAA+/8DAAEA+/8DAAIA+/8DAAMA+/8DAAQA+/8DAAUA+/8DAAYA+/8DAAcA+/8DAAgA+/8DAAkA+/8DAAoA+/8DAAsA+/8EAPX/+/8EAPb/+/8EAPf/+/8EAPj/+/8EAPn/+/8EAPr/+/8EAPv/+/8EAPz/+/8EAP3/+/8EAP7/+/8EAP//+/8EAAAA+/8EAAEA+/8EAAIA+/8EAAMA+/8EAAQA+/8EAAUA+/8EAAYA+/8EAAcA+/8EAAgA+/8EAAkA+/8EAAoA+/8EAAsA+/8FAPb/+/8FAPf/+/8FAPj/+/8FAPn/+/8FAPr/+/8FAPv/+/8FAPz/+/8FAP3/+/8FAP7/+/8FAP//+/8FAAAA+/8FAAEA+/8FAAIA+/8FAAMA+/8FAAQA+/8FAAUA+/8FAAYA+/8FAAcA+/8FAAgA+/8FAAkA+/8FAAoA+/8GAPf/+/8GAPj/+/8GAPn/+/8GAPr/+/8GAPv/+/8GAPz/+/8GAP3/+/8GAP7/+/8GAP//+/8GAAAA+/8GAAEA+/8GAAIA+/8GAAMA+/8GAAQA+/8GAAUA+/8GAAYA+/8GAAcA+/8GAAgA+/8GAAkA+/8HAPj/+/8HAPn/+/8HAPr/+/8HAPv/+/8HAPz/+/8HAP3/+/8HAP7/+/8HAP//+/8HAAAA+/8HAAEA+/8HAAIA+/8HAAMA+/8HAAQA+/8HAAUA+/8HAAYA+/8HAAcA+/8HAAgA+/8IAPv/+/8IAPz/+/8IAP3/+/8IAP7/+/8IAP//+/8IAAAA+/8IAAEA+/8IAAIA+/8IAAMA+/8IAAQA+/8IAAUA/P/8//r//P/8//v//P/8//z//P/8//3//P/8//7//P/8/////P/8/wAA/P/8/wEA/P/8/wIA/P/8/wMA/P/8/wQA/P/8/wUA/P/8/wYA/P/9//j//P/9//n//P/9//r//P/9//v//P/9//z//P/9//3//P/9//7//P/9/////P/9/wAA/P/9/wEA/P/9/wIA/P/9/wMA/P/9/wQA/P/9/wUA/P/9/wYA/P/9/wcA/P/9/wgA/P/+//b//P/+//f//P/+//j//P/+//n//P/+//r//P/+//v//P/+//z//P/+//3//P/+//7//P/+/////P/+/wAA/P/+/wEA/P/+/wIA/P/+/wMA/P/+/wQA/P/+/wUA/P/+/wYA/P/+/wcA/P/+/wgA/P/+/wkA/P/+/woA/P////X//P////b//P////f//P////j//P////n//P////r//P////v//P////z//P////3//P////7//P///////P///wAA/P///wEA/P///wIA/P///wMA/P///wQA/P///wUA/P///wYA/P///wcA/P///wgA/P///wkA/P///woA/P///wsA/P8AAPX//P8AAPb//P8AAPf//P8AAPj//P8AAPn//P8AAPr//P8AAPv//P8AAPz//P8AAP3//P8AAP7//P8AAP///P8AAAAA/P8AAAEA/P8AAAIA/P8AAAMA/P8AAAQA/P8AAAUA/P8AAAYA/P8AAAcA/P8AAAgA/P8AAAkA/P8AAAoA/P8AAAsA/P8BAPT//P8BAPX//P8BAPb//P8BAPf//P8BAPj//P8BAPn//P8BAPr//P8BAPv//P8BAPz//P8BAP3//P8BAP7//P8BAP///P8BAAAA/P8BAAEA/P8BAAIA/P8BAAMA/P8BAAQA/P8BAAUA/P8BAAYA/P8BAAcA/P8BAAgA/P8BAAkA/P8BAAoA/P8BAAsA/P8BAAwA/P8CAPT//P8CAPX//P8CAPb//P8CAPf//P8CAPj//P8CAPn//P8CAPr//P8CAPv//P8CAPz//P8CAP3//P8CAP7//P8CAP///P8CAAAA/P8CAAEA/P8CAAIA/P8CAAMA/P8CAAQA/P8CAAUA/P8CAAYA/P8CAAcA/P8CAAgA/P8CAAkA/P8CAAoA/P8CAAsA/P8CAAwA/P8DAPT//P8DAPX//P8DAPb//P8DAPf//P8DAPj//P8DAPn//P8DAPr//P8DAPv//P8DAPz//P8DAP3//P8DAP7//P8DAP///P8DAAAA/P8DAAEA/P8DAAIA/P8DAAMA/P8DAAQA/P8DAAUA/P8DAAYA/P8DAAcA/P8DAAgA/P8DAAkA/P8DAAoA/P8DAAsA/P8DAAwA/P8EAPX//P8EAPb//P8EAPf//P8EAPj//P8EAPn//P8EAPr//P8EAPv//P8EAPz//P8EAP3//P8EAP7//P8EAP///P8EAAAA/P8EAAEA/P8EAAIA/P8EAAMA/P8EAAQA/P8EAAUA/P8EAAYA/P8EAAcA/P8EAAgA/P8EAAkA/P8EAAoA/P8EAAsA/P8FAPX//P8FAPb//P8FAPf//P8FAPj//P8FAPn//P8FAPr//P8FAPv//P8FAPz//P8FAP3//P8FAP7//P8FAP///P8FAAAA/P8FAAEA/P8FAAIA/P8FAAMA/P8FAAQA/P8FAAUA/P8FAAYA/P8FAAcA/P8FAAgA/P8FAAkA/P8FAAoA/P8FAAsA/P8GAPb//P8GAPf//P8GAPj//P8GAPn//P8GAPr//P8GAPv//P8GAPz//P8GAP3//P8GAP7//P8GAP///P8GAAAA/P8GAAEA/P8GAAIA/P8GAAMA/P8GAAQA/P8GAAUA/P8GAAYA/P8GAAcA/P8GAAgA/P8GAAkA/P8GAAoA/P8HAPj//P8HAPn//P8HAPr//P8HAPv//P8HAPz//P8HAP3//P8HAP7//P8HAP///P8HAAAA/P8HAAEA/P8HAAIA/P8HAAMA/P8HAAQA/P8HAAUA/P8HAAYA/P8HAAcA/P8HAAgA/P8IAPr//P8IAPv//P8IAPz//P8IAP3//P8IAP7//P8IAP///P8IAAAA/P8IAAEA/P8IAAIA/P8IAAMA/P8IAAQA/P8IAAUA/P8IAAYA/f/7//v//f/7//z//f/7//3//f/7//7//f/7/////f/7/wAA/f/7/wEA/f/7/wIA/f/7/wMA/f/7/wQA/f/7/wUA/f/8//j//f/8//n//f/8//r//f/8//v//f/8//z//f/8//3//f/8//7//f/8/////f/8/wAA/f/8/wEA/f/8/wIA/f/8/wMA/f/8/wQA/f/8/wUA/f/8/wYA/f/8/wcA/f/8/wgA/f/9//b//f/9//f//f/9//j//f/9//n//f/9//r//f/9//v//f/9//z//f/9//3//f/9//7//f/9/////f/9/wAA/f/9/wEA/f/9/wIA/f/9/wMA/f/9/wQA/f/9/wUA/f/9/wYA/f/9/wcA/f/9/wgA/f/9/wkA/f/9/woA/f/+//X//f/+//b//f/+//f//f/+//j//f/+//n//f/+//r//f/+//v//f/+//z//f/+//3//f/+//7//f/+/////f/+/wAA/f/+/wEA/f/+/wIA/f/+/wMA/f/+/wQA/f/+/wUA/f/+/wYA/f/+/wcA/f/+/wgA/f/+/wkA/f/+/woA/f/+/wsA/f////T//f////X//f////b//f////f//f////j//f////n//f////r//f////v//f////z//f////3//f////7//f///////f///wAA/f///wEA/f///wIA/f///wMA/f///wQA/f///wUA/f///wYA/f///wcA/f///wgA/f///wkA/f///woA/f///wsA/f///wwA/f8AAPT//f8AAPX//f8AAPb//f8AAPf//f8AAPj//f8AAPn//f8AAPr//f8AAPv//f8AAPz//f8AAP3//f8AAP7//f8AAP///f8AAAAA/f8AAAEA/f8AAAIA/f8AAAMA/f8AAAQA/f8AAAUA/f8AAAYA/f8AAAcA/f8AAAgA/f8AAAkA/f8AAAoA/f8AAAsA/f8AAAwA/f8BAPP//f8BAPT//f8BAPX//f8BAPb//f8BAPf//f8BAPj//f8BAPn//f8BAPr//f8BAPv//f8BAPz//f8BAP3//f8BAP7//f8BAP///f8BAAAA/f8BAAEA/f8BAAIA/f8BAAMA/f8BAAQA/f8BAAUA/f8BAAYA/f8BAAcA/f8BAAgA/f8BAAkA/f8BAAoA/f8BAAsA/f8BAAwA/f8BAA0A/f8CAPP//f8CAPT//f8CAPX//f8CAPb//f8CAPf//f8CAPj//f8CAPn//f8CAPr//f8CAPv//f8CAPz//f8CAP3//f8CAP7//f8CAP///f8CAAAA/f8CAAEA/f8CAAIA/f8CAAMA/f8CAAQA/f8CAAUA/f8CAAYA/f8CAAcA/f8CAAgA/f8CAAkA/f8CAAoA/f8CAAsA/f8CAAwA/f8CAA0A/f8DAPT//f8DAPX//f8DAPb//f8DAPf//f8DAPj//f8DAPn//f8DAPr//f8DAPv//f8DAPz//f8DAP3//f8DAP7//f8DAP///f8DAAAA/f8DAAEA/f8DAAIA/f8DAAMA/f8DAAQA/f8DAAUA/f8DAAYA/f8DAAcA/f8DAAgA/f8DAAkA/f8DAAoA/f8DAAsA/f8DAAwA/f8EAPT//f8EAPX//f8EAPb//f8EAPf//f8EAPj//f8EAPn//f8EAPr//f8EAPv//f8EAPz//f8EAP3//f8EAP7//f8EAP///f8EAAAA/f8EAAEA/f8EAAIA/f8EAAMA/f8EAAQA/f8EAAUA/f8EAAYA/f8EAAcA/f8EAAgA/f8EAAkA/f8EAAoA/f8EAAsA/f8EAAwA/f8FAPX//f8FAPb//f8FAPf//f8FAPj//f8FAPn//f8FAPr//f8FAPv//f8FAPz//f8FAP3//f8FAP7//f8FAP///f8FAAAA/f8FAAEA/f8FAAIA/f8FAAMA/f8FAAQA/f8FAAUA/f8FAAYA/f8FAAcA/f8FAAgA/f8FAAkA/f8FAAoA/f8FAAsA/f8GAPb//f8GAPf//f8GAPj//f8GAPn//f8GAPr//f8GAPv//f8GAPz//f8GAP3//f8GAP7//f8GAP///f8GAAAA/f8GAAEA/f8GAAIA/f8GAAMA/f8GAAQA/f8GAAUA/f8GAAYA/f8GAAcA/f8GAAgA/f8GAAkA/f8GAAoA/f8HAPj//f8HAPn//f8HAPr//f8HAPv//f8HAPz//f8HAP3//f8HAP7//f8HAP///f8HAAAA/f8HAAEA/f8HAAIA/f8HAAMA/f8HAAQA/f8HAAUA/f8HAAYA/f8HAAcA/f8HAAgA/f8IAPv//f8IAPz//f8IAP3//f8IAP7//f8IAP///f8IAAAA/f8IAAEA/f8IAAIA/f8IAAMA/f8IAAQA/f8IAAUA/v/6//z//v/6//3//v/6//7//v/6/////v/6/wAA/v/6/wEA/v/6/wIA/v/6/wMA/v/6/wQA/v/7//j//v/7//n//v/7//r//v/7//v//v/7//z//v/7//3//v/7//7//v/7/////v/7/wAA/v/7/wEA/v/7/wIA/v/7/wMA/v/7/wQA/v/7/wUA/v/7/wYA/v/7/wcA/v/7/wgA/v/8//b//v/8//f//v/8//j//v/8//n//v/8//r//v/8//v//v/8//z//v/8//3//v/8//7//v/8/////v/8/wAA/v/8/wEA/v/8/wIA/v/8/wMA/v/8/wQA/v/8/wUA/v/8/wYA/v/8/wcA/v/8/wgA/v/8/wkA/v/8/woA/v/9//X//v/9//b//v/9//f//v/9//j//v/9//n//v/9//r//v/9//v//v/9//z//v/9//3//v/9//7//v/9/////v/9/wAA/v/9/wEA/v/9/wIA/v/9/wMA/v/9/wQA/v/9/wUA/v/9/wYA/v/9/wcA/v/9/wgA/v/9/wkA/v/9/woA/v/9/wsA/v/+//T//v/+//X//v/+//b//v/+//f//v/+//j//v/+//n//v/+//r//v/+//v//v/+//z//v/+//3//v/+//7//v/+/////v/+/wAA/v/+/wEA/v/+/wIA/v/+/wMA/v/+/wQA/v/+/wUA/v/+/wYA/v/+/wcA/v/+/wgA/v/+/wkA/v/+/woA/v/+/wsA/v/+/wwA/v////P//v////T//v////X//v////b//v////f//v////j//v////n//v////r//v////v//v////z//v////3//v////7//v///////v///wAA/v///wEA/v///wIA/v///wMA/v///wQA/v///wUA/v///wYA/v///wcA/v///wgA/v///wkA/v///woA/v///wsA/v///wwA/v///w0A/v8AAPP//v8AAPT//v8AAPX//v8AAPb//v8AAPf//v8AAPj//v8AAPn//v8AAPr//v8AAPv//v8AAPz//v8AAP3//v8AAP7//v8AAP///v8AAAAA/v8AAAEA/v8AAAIA/v8AAAMA/v8AAAQA/v8AAAUA/v8AAAYA/v8AAAcA/v8AAAgA/v8AAAkA/v8AAAoA/v8AAAsA/v8AAAwA/v8AAA0A/v8BAPP//v8BAPT//v8BAPX//v8BAPb//v8BAPf//v8BAPj//v8BAPn//v8BAPr//v8BAPv//v8BAPz//v8BAP3//v8BAP7//v8BAP///v8BAAAA/v8BAAEA/v8BAAIA/v8BAAMA/v8BAAQA/v8BAAUA/v8BAAYA/v8BAAcA/v8BAAgA/v8BAAkA/v8BAAoA/v8BAAsA/v8BAAwA/v8BAA0A/v8CAPP//v8CAPT//v8CAPX//v8CAPb//v8CAPf//v8CAPj//v8CAPn//v8CAPr//v8CAPv//v8CAPz//v8CAP3//v8CAP7//v8CAP///v8CAAAA/v8CAAEA/v8CAAIA/v8CAAMA/v8CAAQA/v8CAAUA/v8CAAYA/v8CAAcA/v8CAAgA/v8CAAkA/v8CAAoA/v8CAAsA/v8CAAwA/v8CAA0A/v8DAPP//v8DAPT//v8DAPX//v8DAPb//v8DAPf//v8DAPj//v8DAPn//v8DAPr//v8DAPv//v8DAPz//v8DAP3//v8DAP7//v8DAP///v8DAAAA/v8DAAEA/v8DAAIA/v8DAAMA/v8DAAQA/v8DAAUA/v8DAAYA/v8DAAcA/v8DAAgA/v8DAAkA/v8DAAoA/v8DAAsA/v8DAAwA/v8DAA0A/v8EAPT//v8EAPX//v8EAPb//v8EAPf//v8EAPj//v8EAPn//v8EAPr//v8EAPv//v8EAPz//v8EAP3//v8EAP7//v8EAP///v8EAAAA/v8EAAEA/v8EAAIA/v8EAAMA/v8EAAQA/v8EAAUA/v8EAAYA/v8EAAcA/v8EAAgA/v8EAAkA/v8EAAoA/v8EAAsA/v8EAAwA/v8FAPX//v8FAPb//v8FAPf//v8FAPj//v8FAPn//v8FAPr//v8FAPv//v8FAPz//v8FAP3//v8FAP7//v8FAP///v8FAAAA/v8FAAEA/v8FAAIA/v8FAAMA/v8FAAQA/v8FAAUA/v8FAAYA/v8FAAcA/v8FAAgA/v8FAAkA/v8FAAoA/v8FAAsA/v8GAPb//v8GAPf//v8GAPj//v8GAPn//v8GAPr//v8GAPv//v8GAPz//v8GAP3//v8GAP7//v8GAP///v8GAAAA/v8GAAEA/v8GAAIA/v8GAAMA/v8GAAQA/v8GAAUA/v8GAAYA/v8GAAcA/v8GAAgA/v8GAAkA/v8GAAoA/v8HAPj//v8HAPn//v8HAPr//v8HAPv//v8HAPz//v8HAP3//v8HAP7//v8HAP///v8HAAAA/v8HAAEA/v8HAAIA/v8HAAMA/v8HAAQA/v8HAAUA/v8HAAYA/v8HAAcA/v8HAAgA/v8IAPz//v8IAP3//v8IAP7//v8IAP///v8IAAAA/v8IAAEA/v8IAAIA/v8IAAMA/v8IAAQA///5//7////5///////5/wAA///5/wEA///5/wIA///6//n////6//r////6//v////6//z////6//3////6//7////6///////6/wAA///6/wEA///6/wIA///6/wMA///6/wQA///6/wUA///6/wYA///6/wcA///7//f////7//j////7//n////7//r////7//v////7//z////7//3////7//7////7///////7/wAA///7/wEA///7/wIA///7/wMA///7/wQA///7/wUA///7/wYA///7/wcA///7/wgA///7/wkA///8//X////8//b////8//f////8//j////8//n////8//r////8//v////8//z////8//3////8//7////8///////8/wAA///8/wEA///8/wIA///8/wMA///8/wQA///8/wUA///8/wYA///8/wcA///8/wgA///8/wkA///8/woA///8/wsA///9//T////9//X////9//b////9//f////9//j////9//n////9//r////9//v////9//z////9//3////9//7////9///////9/wAA///9/wEA///9/wIA///9/wMA///9/wQA///9/wUA///9/wYA///9/wcA///9/wgA///9/wkA///9/woA///9/wsA///9/wwA///+//P////+//T////+//X////+//b////+//f////+//j////+//n////+//r////+//v////+//z////+//3////+//7////+///////+/wAA///+/wEA///+/wIA///+/wMA///+/wQA///+/wUA///+/wYA///+/wcA///+/wgA///+/wkA///+/woA///+/wsA///+/wwA///+/w0A//////P///////T///////X///////b///////f///////j///////n///////r///////v///////z///////3///////7//////////////wAA/////wEA/////wIA/////wMA/////wQA/////wUA/////wYA/////wcA/////wgA/////wkA/////woA/////wsA/////wwA/////w0A//8AAPP///8AAPT///8AAPX///8AAPb///8AAPf///8AAPj///8AAPn///8AAPr///8AAPv///8AAPz///8AAP3///8AAP7///8AAP////8AAAAA//8AAAEA//8AAAIA//8AAAMA//8AAAQA//8AAAUA//8AAAYA//8AAAcA//8AAAgA//8AAAkA//8AAAoA//8AAAsA//8AAAwA//8AAA0A//8BAPP///8BAPT///8BAPX///8BAPb///8BAPf///8BAPj///8BAPn///8BAPr///8BAPv///8BAPz///8BAP3///8BAP7///8BAP////8BAAAA//8BAAEA//8BAAIA//8BAAMA//8BAAQA//8BAAUA//8BAAYA//8BAAcA//8BAAgA//8BAAkA//8BAAoA//8BAAsA//8BAAwA//8BAA0A//8CAPP///8CAPT///8CAPX///8CAPb///8CAPf///8CAPj///8CAPn///8CAPr///8CAPv///8CAPz///8CAP3///8CAP7///8CAP////8CAAAA//8CAAEA//8CAAIA//8CAAMA//8CAAQA//8CAAUA//8CAAYA//8CAAcA//8CAAgA//8CAAkA//8CAAoA//8CAAsA//8CAAwA//8CAA0A//8DAPP///8DAPT///8DAPX///8DAPb///8DAPf///8DAPj///8DAPn///8DAPr///8DAPv///8DAPz///8DAP3///8DAP7///8DAP////8DAAAA//8DAAEA//8DAAIA//8DAAMA//8DAAQA//8DAAUA//8DAAYA//8DAAcA//8DAAgA//8DAAkA//8DAAoA//8DAAsA//8DAAwA//8DAA0A//8EAPT///8EAPX///8EAPb///8EAPf///8EAPj///8EAPn///8EAPr///8EAPv///8EAPz///8EAP3///8EAP7///8EAP////8EAAAA//8EAAEA//8EAAIA//8EAAMA//8EAAQA//8EAAUA//8EAAYA//8EAAcA//8EAAgA//8EAAkA//8EAAoA//8EAAsA//8EAAwA//8FAPX///8FAPb///8FAPf///8FAPj///8FAPn///8FAPr///8FAPv///8FAPz///8FAP3///8FAP7///8FAP////8FAAAA//8FAAEA//8FAAIA//8FAAMA//8FAAQA//8FAAUA//8FAAYA//8FAAcA//8FAAgA//8FAAkA//8FAAoA//8FAAsA//8GAPf///8GAPj///8GAPn///8GAPr///8GAPv///8GAPz///8GAP3///8GAP7///8GAP////8GAAAA//8GAAEA//8GAAIA//8GAAMA//8GAAQA//8GAAUA//8GAAYA//8GAAcA//8GAAgA//8GAAkA//8HAPn///8HAPr///8HAPv///8HAPz///8HAP3///8HAP7///8HAP////8HAAAA//8HAAEA//8HAAIA//8HAAMA//8HAAQA//8HAAUA//8HAAYA//8HAAcA//8IAP7///8IAP////8IAAAA//8IAAEA//8IAAIAAAD5//v/AAD5//z/AAD5//3/AAD5//7/AAD5////AAD5/wAAAAD5/wEAAAD5/wIAAAD5/wMAAAD5/wQAAAD5/wUAAAD6//j/AAD6//n/AAD6//r/AAD6//v/AAD6//z/AAD6//3/AAD6//7/AAD6////AAD6/wAAAAD6/wEAAAD6/wIAAAD6/wMAAAD6/wQAAAD6/wUAAAD6/wYAAAD6/wcAAAD6/wgAAAD7//b/AAD7//f/AAD7//j/AAD7//n/AAD7//r/AAD7//v/AAD7//z/AAD7//3/AAD7//7/AAD7////AAD7/wAAAAD7/wEAAAD7/wIAAAD7/wMAAAD7/wQAAAD7/wUAAAD7/wYAAAD7/wcAAAD7/wgAAAD7/wkAAAD7/woAAAD8//X/AAD8//b/AAD8//f/AAD8//j/AAD8//n/AAD8//r/AAD8//v/AAD8//z/AAD8//3/AAD8//7/AAD8////AAD8/wAAAAD8/wEAAAD8/wIAAAD8/wMAAAD8/wQAAAD8/wUAAAD8/wYAAAD8/wcAAAD8/wgAAAD8/wkAAAD8/woAAAD8/wsAAAD9//T/AAD9//X/AAD9//b/AAD9//f/AAD9//j/AAD9//n/AAD9//r/AAD9//v/AAD9//z/AAD9//3/AAD9//7/AAD9////AAD9/wAAAAD9/wEAAAD9/wIAAAD9/wMAAAD9/wQAAAD9/wUAAAD9/wYAAAD9/wcAAAD9/wgAAAD9/wkAAAD9/woAAAD9/wsAAAD9/wwAAAD+//P/AAD+//T/AAD+//X/AAD+//b/AAD+//f/AAD+//j/AAD+//n/AAD+//r/AAD+//v/AAD+//z/AAD+//3/AAD+//7/AAD+////AAD+/wAAAAD+/wEAAAD+/wIAAAD+/wMAAAD+/wQAAAD+/wUAAAD+/wYAAAD+/wcAAAD+/wgAAAD+/wkAAAD+/woAAAD+/wsAAAD+/wwAAAD+/w0AAAD///P/AAD///T/AAD///X/AAD///b/AAD///f/AAD///j/AAD///n/AAD///r/AAD///v/AAD///z/AAD///3/AAD///7/AAD/////AAD//wAAAAD//wEAAAD//wIAAAD//wMAAAD//wQAAAD//wUAAAD//wYAAAD//wcAAAD//wgAAAD//wkAAAD//woAAAD//wsAAAD//wwAAAD//w0AAAAAAPL/AAAAAPP/AAAAAPT/AAAAAPX/AAAAAPb/AAAAAPf/AAAAAPj/AAAAAPn/AAAAAPr/AAAAAPv/AAAAAPz/AAAAAP3/AAAAAP7/AAAAAP//AAAAAAEAAAAAAAIAAAAAAAMAAAAAAAQAAAAAAAUAAAAAAAYAAAAAAAcAAAAAAAgAAAAAAAkAAAAAAAoAAAAAAAsAAAAAAAwAAAAAAA0AAAAAAA4AAAABAPP/AAABAPT/AAABAPX/AAABAPb/AAABAPf/AAABAPj/AAABAPn/AAABAPr/AAABAPv/AAABAPz/AAABAP3/AAABAP7/AAABAP//AAABAAAAAAABAAEAAAABAAIAAAABAAMAAAABAAQAAAABAAUAAAABAAYAAAABAAcAAAABAAgAAAABAAkAAAABAAoAAAABAAsAAAABAAwAAAABAA0AAAACAPP/AAACAPT/AAACAPX/AAACAPb/AAACAPf/AAACAPj/AAACAPn/AAACAPr/AAACAPv/AAACAPz/AAACAP3/AAACAP7/AAACAP//AAACAAAAAAACAAEAAAACAAIAAAACAAMAAAACAAQAAAACAAUAAAACAAYAAAACAAcAAAACAAgAAAACAAkAAAACAAoAAAACAAsAAAACAAwAAAACAA0AAAADAPT/AAADAPX/AAADAPb/AAADAPf/AAADAPj/AAADAPn/AAADAPr/AAADAPv/AAADAPz/AAADAP3/AAADAP7/AAADAP//AAADAAAAAAADAAEAAAADAAIAAAADAAMAAAADAAQAAAADAAUAAAADAAYAAAADAAcAAAADAAgAAAADAAkAAAADAAoAAAADAAsAAAADAAwAAAAEAPX/AAAEAPb/AAAEAPf/AAAEAPj/AAAEAPn/AAAEAPr/AAAEAPv/AAAEAPz/AAAEAP3/AAAEAP7/AAAEAP//AAAEAAAAAAAEAAEAAAAEAAIAAAAEAAMAAAAEAAQAAAAEAAUAAAAEAAYAAAAEAAcAAAAEAAgAAAAEAAkAAAAEAAoAAAAEAAsAAAAFAPb/AAAFAPf/AAAFAPj/AAAFAPn/AAAFAPr/AAAFAPv/AAAFAPz/AAAFAP3/AAAFAP7/AAAFAP//AAAFAAAAAAAFAAEAAAAFAAIAAAAFAAMAAAAFAAQAAAAFAAUAAAAFAAYAAAAFAAcAAAAFAAgAAAAFAAkAAAAFAAoAAAAGAPj/AAAGAPn/AAAGAPr/AAAGAPv/AAAGAPz/AAAGAP3/AAAGAP7/AAAGAP//AAAGAAAAAAAGAAEAAAAGAAIAAAAGAAMAAAAGAAQAAAAGAAUAAAAGAAYAAAAGAAcAAAAGAAgAAAAHAPv/AAAHAPz/AAAHAP3/AAAHAP7/AAAHAP//AAAHAAAAAAAHAAEAAAAHAAIAAAAHAAMAAAAHAAQAAAAHAAUAAQD4//7/AQD4////AQD4/wAAAQD4/wEAAQD4/wIAAQD5//n/AQD5//r/AQD5//v/AQD5//z/AQD5//3/AQD5//7/AQD5////AQD5/wAAAQD5/wEAAQD5/wIAAQD5/wMAAQD5/wQAAQD5/wUAAQD5/wYAAQD5/wcAAQD6//f/AQD6//j/AQD6//n/AQD6//r/AQD6//v/AQD6//z/AQD6//3/AQD6//7/AQD6////AQD6/wAAAQD6/wEAAQD6/wIAAQD6/wMAAQD6/wQAAQD6/wUAAQD6/wYAAQD6/wcAAQD6/wgAAQD6/wkAAQD7//X/AQD7//b/AQD7//f/AQD7//j/AQD7//n/AQD7//r/AQD7//v/AQD7//z/AQD7//3/AQD7//7/AQD7////AQD7/wAAAQD7/wEAAQD7/wIAAQD7/wMAAQD7/wQAAQD7/wUAAQD7/wYAAQD7/wcAAQD7/wgAAQD7/wkAAQD7/woAAQD7/wsAAQD8//T/AQD8//X/AQD8//b/AQD8//f/AQD8//j/AQD8//n/AQD8//r/AQD8//v/AQD8//z/AQD8//3/AQD8//7/AQD8////AQD8/wAAAQD8/wEAAQD8/wIAAQD8/wMAAQD8/wQAAQD8/wUAAQD8/wYAAQD8/wcAAQD8/wgAAQD8/wkAAQD8/woAAQD8/wsAAQD8/wwAAQD9//P/AQD9//T/AQD9//X/AQD9//b/AQD9//f/AQD9//j/AQD9//n/AQD9//r/AQD9//v/AQD9//z/AQD9//3/AQD9//7/AQD9////AQD9/wAAAQD9/wEAAQD9/wIAAQD9/wMAAQD9/wQAAQD9/wUAAQD9/wYAAQD9/wcAAQD9/wgAAQD9/wkAAQD9/woAAQD9/wsAAQD9/wwAAQD9/w0AAQD+//P/AQD+//T/AQD+//X/AQD+//b/AQD+//f/AQD+//j/AQD+//n/AQD+//r/AQD+//v/AQD+//z/AQD+//3/AQD+//7/AQD+////AQD+/wAAAQD+/wEAAQD+/wIAAQD+/wMAAQD+/wQAAQD+/wUAAQD+/wYAAQD+/wcAAQD+/wgAAQD+/wkAAQD+/woAAQD+/wsAAQD+/wwAAQD+/w0AAQD///P/AQD///T/AQD///X/AQD///b/AQD///f/AQD///j/AQD///n/AQD///r/AQD///v/AQD///z/AQD///3/AQD///7/AQD/////AQD//wAAAQD//wEAAQD//wIAAQD//wMAAQD//wQAAQD//wUAAQD//wYAAQD//wcAAQD//wgAAQD//wkAAQD//woAAQD//wsAAQD//wwAAQD//w0AAQAAAPP/AQAAAPT/AQAAAPX/AQAAAPb/AQAAAPf/AQAAAPj/AQAAAPn/AQAAAPr/AQAAAPv/AQAAAPz/AQAAAP3/AQAAAP7/AQAAAP//AQAAAAAAAQAAAAEAAQAAAAIAAQAAAAMAAQAAAAQAAQAAAAUAAQAAAAYAAQAAAAcAAQAAAAgAAQAAAAkAAQAAAAoAAQAAAAsAAQAAAAwAAQAAAA0AAQABAPP/AQABAPT/AQABAPX/AQABAPb/AQABAPf/AQABAPj/AQABAPn/AQABAPr/AQABAPv/AQABAPz/AQABAP3/AQABAP7/AQABAP//AQABAAAAAQABAAEAAQABAAIAAQABAAMAAQABAAQAAQABAAUAAQABAAYAAQABAAcAAQABAAgAAQABAAkAAQABAAoAAQABAAsAAQABAAwAAQABAA0AAQACAPP/AQACAPT/AQACAPX/AQACAPb/AQACAPf/AQACAPj/AQACAPn/AQACAPr/AQACAPv/AQACAPz/AQACAP3/AQACAP7/AQACAP//AQACAAAAAQACAAEAAQACAAIAAQACAAMAAQACAAQAAQACAAUAAQACAAYAAQACAAcAAQACAAgAAQACAAkAAQACAAoAAQACAAsAAQACAAwAAQACAA0AAQADAPT/AQADAPX/AQADAPb/AQADAPf/AQADAPj/AQADAPn/AQADAPr/AQADAPv/AQADAPz/AQADAP3/AQADAP7/AQADAP//AQADAAAAAQADAAEAAQADAAIAAQADAAMAAQADAAQAAQADAAUAAQADAAYAAQADAAcAAQADAAgAAQADAAkAAQADAAoAAQADAAsAAQADAAwAAQAEAPX/AQAEAPb/AQAEAPf/AQAEAPj/AQAEAPn/AQAEAPr/AQAEAPv/AQAEAPz/AQAEAP3/AQAEAP7/AQAEAP//AQAEAAAAAQAEAAEAAQAEAAIAAQAEAAMAAQAEAAQAAQAEAAUAAQAEAAYAAQAEAAcAAQAEAAgAAQAEAAkAAQAEAAoAAQAEAAsAAQAFAPf/AQAFAPj/AQAFAPn/AQAFAPr/AQAFAPv/AQAFAPz/AQAFAP3/AQAFAP7/AQAFAP//AQAFAAAAAQAFAAEAAQAFAAIAAQAFAAMAAQAFAAQAAQAFAAUAAQAFAAYAAQAFAAcAAQAFAAgAAQAFAAkAAQAGAPn/AQAGAPr/AQAGAPv/AQAGAPz/AQAGAP3/AQAGAP7/AQAGAP//AQAGAAAAAQAGAAEAAQAGAAIAAQAGAAMAAQAGAAQAAQAGAAUAAQAGAAYAAQAGAAcAAQAHAP7/AQAHAP//AQAHAAAAAQAHAAEAAQAHAAIAAgD4//z/AgD4//3/AgD4//7/AgD4////AgD4/wAAAgD4/wEAAgD4/wIAAgD4/wMAAgD4/wQAAgD5//j/AgD5//n/AgD5//r/AgD5//v/AgD5//z/AgD5//3/AgD5//7/AgD5////AgD5/wAAAgD5/wEAAgD5/wIAAgD5/wMAAgD5/wQAAgD5/wUAAgD5/wYAAgD5/wcAAgD5/wgAAgD6//b/AgD6//f/AgD6//j/AgD6//n/AgD6//r/AgD6//v/AgD6//z/AgD6//3/AgD6//7/AgD6////AgD6/wAAAgD6/wEAAgD6/wIAAgD6/wMAAgD6/wQAAgD6/wUAAgD6/wYAAgD6/wcAAgD6/wgAAgD6/wkAAgD6/woAAgD7//X/AgD7//b/AgD7//f/AgD7//j/AgD7//n/AgD7//r/AgD7//v/AgD7//z/AgD7//3/AgD7//7/AgD7////AgD7/wAAAgD7/wEAAgD7/wIAAgD7/wMAAgD7/wQAAgD7/wUAAgD7/wYAAgD7/wcAAgD7/wgAAgD7/wkAAgD7/woAAgD7/wsAAgD8//T/AgD8//X/AgD8//b/AgD8//f/AgD8//j/AgD8//n/AgD8//r/AgD8//v/AgD8//z/AgD8//3/AgD8//7/AgD8////AgD8/wAAAgD8/wEAAgD8/wIAAgD8/wMAAgD8/wQAAgD8/wUAAgD8/wYAAgD8/wcAAgD8/wgAAgD8/wkAAgD8/woAAgD8/wsAAgD8/wwAAgD9//P/AgD9//T/AgD9//X/AgD9//b/AgD9//f/AgD9//j/AgD9//n/AgD9//r/AgD9//v/AgD9//z/AgD9//3/AgD9//7/AgD9////AgD9/wAAAgD9/wEAAgD9/wIAAgD9/wMAAgD9/wQAAgD9/wUAAgD9/wYAAgD9/wcAAgD9/wgAAgD9/wkAAgD9/woAAgD9/wsAAgD9/wwAAgD9/w0AAgD+//P/AgD+//T/AgD+//X/AgD+//b/AgD+//f/AgD+//j/AgD+//n/AgD+//r/AgD+//v/AgD+//z/AgD+//3/AgD+//7/AgD+////AgD+/wAAAgD+/wEAAgD+/wIAAgD+/wMAAgD+/wQAAgD+/wUAAgD+/wYAAgD+/wcAAgD+/wgAAgD+/wkAAgD+/woAAgD+/wsAAgD+/wwAAgD+/w0AAgD///P/AgD///T/AgD///X/AgD///b/AgD///f/AgD///j/AgD///n/AgD///r/AgD///v/AgD///z/AgD///3/AgD///7/AgD/////AgD//wAAAgD//wEAAgD//wIAAgD//wMAAgD//wQAAgD//wUAAgD//wYAAgD//wcAAgD//wgAAgD//wkAAgD//woAAgD//wsAAgD//wwAAgD//w0AAgAAAPP/AgAAAPT/AgAAAPX/AgAAAPb/AgAAAPf/AgAAAPj/AgAAAPn/AgAAAPr/AgAAAPv/AgAAAPz/AgAAAP3/AgAAAP7/AgAAAP//AgAAAAAAAgAAAAEAAgAAAAIAAgAAAAMAAgAAAAQAAgAAAAUAAgAAAAYAAgAAAAcAAgAAAAgAAgAAAAkAAgAAAAoAAgAAAAsAAgAAAAwAAgAAAA0AAgABAPP/AgABAPT/AgABAPX/AgABAPb/AgABAPf/AgABAPj/AgABAPn/AgABAPr/AgABAPv/AgABAPz/AgABAP3/AgABAP7/AgABAP//AgABAAAAAgABAAEAAgABAAIAAgABAAMAAgABAAQAAgABAAUAAgABAAYAAgABAAcAAgABAAgAAgABAAkAAgABAAoAAgABAAsAAgABAAwAAgABAA0AAgACAPT/AgACAPX/AgACAPb/AgACAPf/AgACAPj/AgACAPn/AgACAPr/AgACAPv/AgACAPz/AgACAP3/AgACAP7/AgACAP//AgACAAAAAgACAAEAAgACAAIAAgACAAMAAgACAAQAAgACAAUAAgACAAYAAgACAAcAAgACAAgAAgACAAkAAgACAAoAAgACAAsAAgACAAwAAgADAPX/AgADAPb/AgADAPf/AgADAPj/AgADAPn/AgADAPr/AgADAPv/AgADAPz/AgADAP3/AgADAP7/AgADAP//AgADAAAAAgADAAEAAgADAAIAAgADAAMAAgADAAQAAgADAAUAAgADAAYAAgADAAcAAgADAAgAAgADAAkAAgADAAoAAgADAAsAAgAEAPb/AgAEAPf/AgAEAPj/AgAEAPn/AgAEAPr/AgAEAPv/AgAEAPz/AgAEAP3/AgAEAP7/AgAEAP//AgAEAAAAAgAEAAEAAgAEAAIAAgAEAAMAAgAEAAQAAgAEAAUAAgAEAAYAAgAEAAcAAgAEAAgAAgAEAAkAAgAEAAoAAgAFAPj/AgAFAPn/AgAFAPr/AgAFAPv/AgAFAPz/AgAFAP3/AgAFAP7/AgAFAP//AgAFAAAAAgAFAAEAAgAFAAIAAgAFAAMAAgAFAAQAAgAFAAUAAgAFAAYAAgAFAAcAAgAFAAgAAgAGAPz/AgAGAP3/AgAGAP7/AgAGAP//AgAGAAAAAgAGAAEAAgAGAAIAAgAGAAMAAgAGAAQAAwD4//v/AwD4//z/AwD4//3/AwD4//7/AwD4////AwD4/wAAAwD4/wEAAwD4/wIAAwD4/wMAAwD4/wQAAwD4/wUAAwD5//j/AwD5//n/AwD5//r/AwD5//v/AwD5//z/AwD5//3/AwD5//7/AwD5////AwD5/wAAAwD5/wEAAwD5/wIAAwD5/wMAAwD5/wQAAwD5/wUAAwD5/wYAAwD5/wcAAwD5/wgAAwD6//b/AwD6//f/AwD6//j/AwD6//n/AwD6//r/AwD6//v/AwD6//z/AwD6//3/AwD6//7/AwD6////AwD6/wAAAwD6/wEAAwD6/wIAAwD6/wMAAwD6/wQAAwD6/wUAAwD6/wYAAwD6/wcAAwD6/wgAAwD6/wkAAwD6/woAAwD7//X/AwD7//b/AwD7//f/AwD7//j/AwD7//n/AwD7//r/AwD7//v/AwD7//z/AwD7//3/AwD7//7/AwD7////AwD7/wAAAwD7/wEAAwD7/wIAAwD7/wMAAwD7/wQAAwD7/wUAAwD7/wYAAwD7/wcAAwD7/wgAAwD7/wkAAwD7/woAAwD7/wsAAwD8//T/AwD8//X/AwD8//b/AwD8//f/AwD8//j/AwD8//n/AwD8//r/AwD8//v/AwD8//z/AwD8//3/AwD8//7/AwD8////AwD8/wAAAwD8/wEAAwD8/wIAAwD8/wMAAwD8/wQAAwD8/wUAAwD8/wYAAwD8/wcAAwD8/wgAAwD8/wkAAwD8/woAAwD8/wsAAwD8/wwAAwD9//T/AwD9//X/AwD9//b/AwD9//f/AwD9//j/AwD9//n/AwD9//r/AwD9//v/AwD9//z/AwD9//3/AwD9//7/AwD9////AwD9/wAAAwD9/wEAAwD9/wIAAwD9/wMAAwD9/wQAAwD9/wUAAwD9/wYAAwD9/wcAAwD9/wgAAwD9/wkAAwD9/woAAwD9/wsAAwD9/wwAAwD+//P/AwD+//T/AwD+//X/AwD+//b/AwD+//f/AwD+//j/AwD+//n/AwD+//r/AwD+//v/AwD+//z/AwD+//3/AwD+//7/AwD+////AwD+/wAAAwD+/wEAAwD+/wIAAwD+/wMAAwD+/wQAAwD+/wUAAwD+/wYAAwD+/wcAAwD+/wgAAwD+/wkAAwD+/woAAwD+/wsAAwD+/wwAAwD+/w0AAwD///P/AwD///T/AwD///X/AwD///b/AwD///f/AwD///j/AwD///n/AwD///r/AwD///v/AwD///z/AwD///3/AwD///7/AwD/////AwD//wAAAwD//wEAAwD//wIAAwD//wMAAwD//wQAAwD//wUAAwD//wYAAwD//wcAAwD//wgAAwD//wkAAwD//woAAwD//wsAAwD//wwAAwD//w0AAwAAAPT/AwAAAPX/AwAAAPb/AwAAAPf/AwAAAPj/AwAAAPn/AwAAAPr/AwAAAPv/AwAAAPz/AwAAAP3/AwAAAP7/AwAAAP//AwAAAAAAAwAAAAEAAwAAAAIAAwAAAAMAAwAAAAQAAwAAAAUAAwAAAAYAAwAAAAcAAwAAAAgAAwAAAAkAAwAAAAoAAwAAAAsAAwAAAAwAAwABAPT/AwABAPX/AwABAPb/AwABAPf/AwABAPj/AwABAPn/AwABAPr/AwABAPv/AwABAPz/AwABAP3/AwABAP7/AwABAP//AwABAAAAAwABAAEAAwABAAIAAwABAAMAAwABAAQAAwABAAUAAwABAAYAAwABAAcAAwABAAgAAwABAAkAAwABAAoAAwABAAsAAwABAAwAAwACAPX/AwACAPb/AwACAPf/AwACAPj/AwACAPn/AwACAPr/AwACAPv/AwACAPz/AwACAP3/AwACAP7/AwACAP//AwACAAAAAwACAAEAAwACAAIAAwACAAMAAwACAAQAAwACAAUAAwACAAYAAwACAAcAAwACAAgAAwACAAkAAwACAAoAAwACAAsAAwADAPb/AwADAPf/AwADAPj/AwADAPn/AwADAPr/AwADAPv/AwADAPz/AwADAP3/AwADAP7/AwADAP//AwADAAAAAwADAAEAAwADAAIAAwADAAMAAwADAAQAAwADAAUAAwADAAYAAwADAAcAAwADAAgAAwADAAkAAwADAAoAAwAEAPj/AwAEAPn/AwAEAPr/AwAEAPv/AwAEAPz/AwAEAP3/AwAEAP7/AwAEAP//AwAEAAAAAwAEAAEAAwAEAAIAAwAEAAMAAwAEAAQAAwAEAAUAAwAEAAYAAwAEAAcAAwAEAAgAAwAFAPv/AwAFAPz/AwAFAP3/AwAFAP7/AwAFAP//AwAFAAAAAwAFAAEAAwAFAAIAAwAFAAMAAwAFAAQAAwAFAAUABAD4//r/BAD4//v/BAD4//z/BAD4//3/BAD4//7/BAD4////BAD4/wAABAD4/wEABAD4/wIABAD4/wMABAD4/wQABAD4/wUABAD4/wYABAD5//j/BAD5//n/BAD5//r/BAD5//v/BAD5//z/BAD5//3/BAD5//7/BAD5////BAD5/wAABAD5/wEABAD5/wIABAD5/wMABAD5/wQABAD5/wUABAD5/wYABAD5/wcABAD5/wgABAD6//b/BAD6//f/BAD6//j/BAD6//n/BAD6//r/BAD6//v/BAD6//z/BAD6//3/BAD6//7/BAD6////BAD6/wAABAD6/wEABAD6/wIABAD6/wMABAD6/wQABAD6/wUABAD6/wYABAD6/wcABAD6/wgABAD6/wkABAD6/woABAD7//X/BAD7//b/BAD7//f/BAD7//j/BAD7//n/BAD7//r/BAD7//v/BAD7//z/BAD7//3/BAD7//7/BAD7////BAD7/wAABAD7/wEABAD7/wIABAD7/wMABAD7/wQABAD7/wUABAD7/wYABAD7/wcABAD7/wgABAD7/wkABAD7/woABAD7/wsABAD8//X/BAD8//b/BAD8//f/BAD8//j/BAD8//n/BAD8//r/BAD8//v/BAD8//z/BAD8//3/BAD8//7/BAD8////BAD8/wAABAD8/wEABAD8/wIABAD8/wMABAD8/wQABAD8/wUABAD8/wYABAD8/wcABAD8/wgABAD8/wkABAD8/woABAD8/wsABAD9//T/BAD9//X/BAD9//b/BAD9//f/BAD9//j/BAD9//n/BAD9//r/BAD9//v/BAD9//z/BAD9//3/BAD9//7/BAD9////BAD9/wAABAD9/wEABAD9/wIABAD9/wMABAD9/wQABAD9/wUABAD9/wYABAD9/wcABAD9/wgABAD9/wkABAD9/woABAD9/wsABAD9/wwABAD+//T/BAD+//X/BAD+//b/BAD+//f/BAD+//j/BAD+//n/BAD+//r/BAD+//v/BAD+//z/BAD+//3/BAD+//7/BAD+////BAD+/wAABAD+/wEABAD+/wIABAD+/wMABAD+/wQABAD+/wUABAD+/wYABAD+/wcABAD+/wgABAD+/wkABAD+/woABAD+/wsABAD+/wwABAD///T/BAD///X/BAD///b/BAD///f/BAD///j/BAD///n/BAD///r/BAD///v/BAD///z/BAD///3/BAD///7/BAD/////BAD//wAABAD//wEABAD//wIABAD//wMABAD//wQABAD//wUABAD//wYABAD//wcABAD//wgABAD//wkABAD//woABAD//wsABAD//wwABAAAAPX/BAAAAPb/BAAAAPf/BAAAAPj/BAAAAPn/BAAAAPr/BAAAAPv/BAAAAPz/BAAAAP3/BAAAAP7/BAAAAP//BAAAAAAABAAAAAEABAAAAAIABAAAAAMABAAAAAQABAAAAAUABAAAAAYABAAAAAcABAAAAAgABAAAAAkABAAAAAoABAAAAAsABAABAPX/BAABAPb/BAABAPf/BAABAPj/BAABAPn/BAABAPr/BAABAPv/BAABAPz/BAABAP3/BAABAP7/BAABAP//BAABAAAABAABAAEABAABAAIABAABAAMABAABAAQABAABAAUABAABAAYABAABAAcABAABAAgABAABAAkABAABAAoABAABAAsABAACAPb/BAACAPf/BAACAPj/BAACAPn/BAACAPr/BAACAPv/BAACAPz/BAACAP3/BAACAP7/BAACAP//BAACAAAABAACAAEABAACAAIABAACAAMABAACAAQABAACAAUABAACAAYABAACAAcABAACAAgABAACAAkABAACAAoABAADAPj/BAADAPn/BAADAPr/BAADAPv/BAADAPz/BAADAP3/BAADAP7/BAADAP//BAADAAAABAADAAEABAADAAIABAADAAMABAADAAQABAADAAUABAADAAYABAADAAcABAADAAgABAAEAPr/BAAEAPv/BAAEAPz/BAAEAP3/BAAEAP7/BAAEAP//BAAEAAAABAAEAAEABAAEAAIABAAEAAMABAAEAAQABAAEAAUABAAEAAYABQD4//v/BQD4//z/BQD4//3/BQD4//7/BQD4////BQD4/wAABQD4/wEABQD4/wIABQD4/wMABQD4/wQABQD4/wUABQD5//j/BQD5//n/BQD5//r/BQD5//v/BQD5//z/BQD5//3/BQD5//7/BQD5////BQD5/wAABQD5/wEABQD5/wIABQD5/wMABQD5/wQABQD5/wUABQD5/wYABQD5/wcABQD5/wgABQD6//f/BQD6//j/BQD6//n/BQD6//r/BQD6//v/BQD6//z/BQD6//3/BQD6//7/BQD6////BQD6/wAABQD6/wEABQD6/wIABQD6/wMABQD6/wQABQD6/wUABQD6/wYABQD6/wcABQD6/wgABQD6/wkABQD7//b/BQD7//f/BQD7//j/BQD7//n/BQD7//r/BQD7//v/BQD7//z/BQD7//3/BQD7//7/BQD7////BQD7/wAABQD7/wEABQD7/wIABQD7/wMABQD7/wQABQD7/wUABQD7/wYABQD7/wcABQD7/wgABQD7/wkABQD7/woABQD8//X/BQD8//b/BQD8//f/BQD8//j/BQD8//n/BQD8//r/BQD8//v/BQD8//z/BQD8//3/BQD8//7/BQD8////BQD8/wAABQD8/wEABQD8/wIABQD8/wMABQD8/wQABQD8/wUABQD8/wYABQD8/wcABQD8/wgABQD8/wkABQD8/woABQD8/wsABQD9//X/BQD9//b/BQD9//f/BQD9//j/BQD9//n/BQD9//r/BQD9//v/BQD9//z/BQD9//3/BQD9//7/BQD9////BQD9/wAABQD9/wEABQD9/wIABQD9/wMABQD9/wQABQD9/wUABQD9/wYABQD9/wcABQD9/wgABQD9/wkABQD9/woABQD9/wsABQD+//X/BQD+//b/BQD+//f/BQD+//j/BQD+//n/BQD+//r/BQD+//v/BQD+//z/BQD+//3/BQD+//7/BQD+////BQD+/wAABQD+/wEABQD+/wIABQD+/wMABQD+/wQABQD+/wUABQD+/wYABQD+/wcABQD+/wgABQD+/wkABQD+/woABQD+/wsABQD///X/BQD///b/BQD///f/BQD///j/BQD///n/BQD///r/BQD///v/BQD///z/BQD///3/BQD///7/BQD/////BQD//wAABQD//wEABQD//wIABQD//wMABQD//wQABQD//wUABQD//wYABQD//wcABQD//wgABQD//wkABQD//woABQD//wsABQAAAPb/BQAAAPf/BQAAAPj/BQAAAPn/BQAAAPr/BQAAAPv/BQAAAPz/BQAAAP3/BQAAAP7/BQAAAP//BQAAAAAABQAAAAEABQAAAAIABQAAAAMABQAAAAQABQAAAAUABQAAAAYABQAAAAcABQAAAAgABQAAAAkABQAAAAoABQABAPf/BQABAPj/BQABAPn/BQABAPr/BQABAPv/BQABAPz/BQABAP3/BQABAP7/BQABAP//BQABAAAABQABAAEABQABAAIABQABAAMABQABAAQABQABAAUABQABAAYABQABAAcABQABAAgABQABAAkABQACAPj/BQACAPn/BQACAPr/BQACAPv/BQACAPz/BQACAP3/BQACAP7/BQACAP//BQACAAAABQACAAEABQACAAIABQACAAMABQACAAQABQACAAUABQACAAYABQACAAcABQACAAgABQADAPv/BQADAPz/BQADAP3/BQADAP7/BQADAP//BQADAAAABQADAAEABQADAAIABQADAAMABQADAAQABQADAAUABgD4//z/BgD4//3/BgD4//7/BgD4////BgD4/wAABgD4/wEABgD4/wIABgD4/wMABgD4/wQABgD5//n/BgD5//r/BgD5//v/BgD5//z/BgD5//3/BgD5//7/BgD5////BgD5/wAABgD5/wEABgD5/wIABgD5/wMABgD5/wQABgD5/wUABgD5/wYABgD5/wcABgD6//j/BgD6//n/BgD6//r/BgD6//v/BgD6//z/BgD6//3/BgD6//7/BgD6////BgD6/wAABgD6/wEABgD6/wIABgD6/wMABgD6/wQABgD6/wUABgD6/wYABgD6/wcABgD6/wgABgD7//f/BgD7//j/BgD7//n/BgD7//r/BgD7//v/BgD7//z/BgD7//3/BgD7//7/BgD7////BgD7/wAABgD7/wEABgD7/wIABgD7/wMABgD7/wQABgD7/wUABgD7/wYABgD7/wcABgD7/wgABgD7/wkABgD8//b/BgD8//f/BgD8//j/BgD8//n/BgD8//r/BgD8//v/BgD8//z/BgD8//3/BgD8//7/BgD8////BgD8/wAABgD8/wEABgD8/wIABgD8/wMABgD8/wQABgD8/wUABgD8/wYABgD8/wcABgD8/wgABgD8/wkABgD8/woABgD9//b/BgD9//f/BgD9//j/BgD9//n/BgD9//r/BgD9//v/BgD9//z/BgD9//3/BgD9//7/BgD9////BgD9/wAABgD9/wEABgD9/wIABgD9/wMABgD9/wQABgD9/wUABgD9/wYABgD9/wcABgD9/wgABgD9/wkABgD9/woABgD+//b/BgD+//f/BgD+//j/BgD+//n/BgD+//r/BgD+//v/BgD+//z/BgD+//3/BgD+//7/BgD+////BgD+/wAABgD+/wEABgD+/wIABgD+/wMABgD+/wQABgD+/wUABgD+/wYABgD+/wcABgD+/wgABgD+/wkABgD+/woABgD///f/BgD///j/BgD///n/BgD///r/BgD///v/BgD///z/BgD///3/BgD///7/BgD/////BgD//wAABgD//wEABgD//wIABgD//wMABgD//wQABgD//wUABgD//wYABgD//wcABgD//wgABgD//wkABgAAAPj/BgAAAPn/BgAAAPr/BgAAAPv/BgAAAPz/BgAAAP3/BgAAAP7/BgAAAP//BgAAAAAABgAAAAEABgAAAAIABgAAAAMABgAAAAQABgAAAAUABgAAAAYABgAAAAcABgAAAAgABgABAPn/BgABAPr/BgABAPv/BgABAPz/BgABAP3/BgABAP7/BgABAP//BgABAAAABgABAAEABgABAAIABgABAAMABgABAAQABgABAAUABgABAAYABgABAAcABgACAPz/BgACAP3/BgACAP7/BgACAP//BgACAAAABgACAAEABgACAAIABgACAAMABgACAAQABwD4//7/BwD4////BwD4/wAABwD4/wEABwD4/wIABwD5//v/BwD5//z/BwD5//3/BwD5//7/BwD5////BwD5/wAABwD5/wEABwD5/wIABwD5/wMABwD5/wQABwD5/wUABwD6//n/BwD6//r/BwD6//v/BwD6//z/BwD6//3/BwD6//7/BwD6////BwD6/wAABwD6/wEABwD6/wIABwD6/wMABwD6/wQABwD6/wUABwD6/wYABwD6/wcABwD7//j/BwD7//n/BwD7//r/BwD7//v/BwD7//z/BwD7//3/BwD7//7/BwD7////BwD7/wAABwD7/wEABwD7/wIABwD7/wMABwD7/wQABwD7/wUABwD7/wYABwD7/wcABwD7/wgABwD8//j/BwD8//n/BwD8//r/BwD8//v/BwD8//z/BwD8//3/BwD8//7/BwD8////BwD8/wAABwD8/wEABwD8/wIABwD8/wMABwD8/wQABwD8/wUABwD8/wYABwD8/wcABwD8/wgABwD9//j/BwD9//n/BwD9//r/BwD9//v/BwD9//z/BwD9//3/BwD9//7/BwD9////BwD9/wAABwD9/wEABwD9/wIABwD9/wMABwD9/wQABwD9/wUABwD9/wYABwD9/wcABwD9/wgABwD+//j/BwD+//n/BwD+//r/BwD+//v/BwD+//z/BwD+//3/BwD+//7/BwD+////BwD+/wAABwD+/wEABwD+/wIABwD+/wMABwD+/wQABwD+/wUABwD+/wYABwD+/wcABwD+/wgABwD///n/BwD///r/BwD///v/BwD///z/BwD///3/BwD///7/BwD/////BwD//wAABwD//wEABwD//wIABwD//wMABwD//wQABwD//wUABwD//wYABwD//wcABwAAAPv/BwAAAPz/BwAAAP3/BwAAAP7/BwAAAP//BwAAAAAABwAAAAEABwAAAAIABwAAAAMABwAAAAQABwAAAAUABwABAP7/BwABAP//BwABAAAABwABAAEABwABAAIACAD5//7/CAD5////CAD5/wAACAD5/wEACAD5/wIACAD6//z/CAD6//3/CAD6//7/CAD6////CAD6/wAACAD6/wEACAD6/wIACAD6/wMACAD6/wQACAD7//v/CAD7//z/CAD7//3/CAD7//7/CAD7////CAD7/wAACAD7/wEACAD7/wIACAD7/wMACAD7/wQACAD7/wUACAD8//r/CAD8//v/CAD8//z/CAD8//3/CAD8//7/CAD8////CAD8/wAACAD8/wEACAD8/wIACAD8/wMACAD8/wQACAD8/wUACAD8/wYACAD9//v/CAD9//z/CAD9//3/CAD9//7/CAD9////CAD9/wAACAD9/wEACAD9/wIACAD9/wMACAD9/wQACAD9/wUACAD+//z/CAD+//3/CAD+//7/CAD+////CAD+/wAACAD+/wEACAD+/wIACAD+/wMACAD+/wQACAD///7/CAD/////CAD//wAACAD//wEACAD//wIA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.006001374253468719,\"absorptive\":true,\"n_reflections\":3994,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":true}", + "NaCl (rocksalt)": "{\"name\":\"NaCl\",\"spacegroup\":\"Fm-3m (225)\",\"pointgroup\":\"m-3m\",\"cell\":[[5.64,0.0,0.0],[0.0,5.64,0.0],[0.0,0.0,5.64]],\"recip\":[[0.1773049645390071,0.0,0.0],[0.0,0.1773049645390071,0.0],[0.0,0.0,0.1773049645390071]],\"positions_frac\":[[0.0,0.0,0.0],[0.5,0.0,0.0],[0.0,0.5,0.5],[0.5,0.5,0.5],[0.5,0.0,0.5],[0.0,0.0,0.5],[0.5,0.5,0.0],[0.0,0.5,0.0]],\"numbers\":[11,17,11,17,11,17,11,17],\"symbols\":[\"Na\",\"Cl\",\"Na\",\"Cl\",\"Na\",\"Cl\",\"Na\",\"Cl\"],\"colors\":[[0.671,0.361,0.949],[0.122,0.941,0.122],[0.671,0.361,0.949],[0.122,0.941,0.122],[0.671,0.361,0.949],[0.122,0.941,0.122],[0.671,0.361,0.949],[0.122,0.941,0.122]],\"radii\":[1.66,1.02,1.66,1.02,1.66,1.02,1.66,1.02],\"hkl_i16\":\"8P/8//7/8P/8/wAA8P/8/wIA8P/+//z/8P/+//7/8P/+/wAA8P/+/wIA8P/+/wQA8P8AAPz/8P8AAP7/8P8AAAAA8P8AAAIA8P8AAAQA8P8CAPz/8P8CAP7/8P8CAAAA8P8CAAIA8P8CAAQA8P8EAP7/8P8EAAAA8P8EAAIA8f/5//3/8f/5////8f/5/wEA8f/5/wMA8f/7//v/8f/7//3/8f/7////8f/7/wEA8f/7/wMA8f/7/wUA8f/9//n/8f/9//v/8f/9//3/8f/9////8f/9/wEA8f/9/wMA8f/9/wUA8f/9/wcA8f////n/8f////v/8f////3/8f//////8f///wEA8f///wMA8f///wUA8f///wcA8f8BAPn/8f8BAPv/8f8BAP3/8f8BAP//8f8BAAEA8f8BAAMA8f8BAAUA8f8BAAcA8f8DAPn/8f8DAPv/8f8DAP3/8f8DAP//8f8DAAEA8f8DAAMA8f8DAAUA8f8DAAcA8f8FAPv/8f8FAP3/8f8FAP//8f8FAAEA8f8FAAMA8f8FAAUA8f8HAP3/8f8HAP//8f8HAAEA8f8HAAMA8v/4//z/8v/4//7/8v/4/wAA8v/4/wIA8v/4/wQA8v/6//r/8v/6//z/8v/6//7/8v/6/wAA8v/6/wIA8v/6/wQA8v/6/wYA8v/8//j/8v/8//r/8v/8//z/8v/8//7/8v/8/wAA8v/8/wIA8v/8/wQA8v/8/wYA8v/8/wgA8v/+//j/8v/+//r/8v/+//z/8v/+//7/8v/+/wAA8v/+/wIA8v/+/wQA8v/+/wYA8v/+/wgA8v8AAPj/8v8AAPr/8v8AAPz/8v8AAP7/8v8AAAAA8v8AAAIA8v8AAAQA8v8AAAYA8v8AAAgA8v8CAPj/8v8CAPr/8v8CAPz/8v8CAP7/8v8CAAAA8v8CAAIA8v8CAAQA8v8CAAYA8v8CAAgA8v8EAPj/8v8EAPr/8v8EAPz/8v8EAP7/8v8EAAAA8v8EAAIA8v8EAAQA8v8EAAYA8v8EAAgA8v8GAPr/8v8GAPz/8v8GAP7/8v8GAAAA8v8GAAIA8v8GAAQA8v8GAAYA8v8IAPz/8v8IAP7/8v8IAAAA8v8IAAIA8v8IAAQA8//3//v/8//3//3/8//3////8//3/wEA8//3/wMA8//3/wUA8//5//n/8//5//v/8//5//3/8//5////8//5/wEA8//5/wMA8//5/wUA8//5/wcA8//7//f/8//7//n/8//7//v/8//7//3/8//7////8//7/wEA8//7/wMA8//7/wUA8//7/wcA8//7/wkA8//9//f/8//9//n/8//9//v/8//9//3/8//9////8//9/wEA8//9/wMA8//9/wUA8//9/wcA8//9/wkA8/////f/8/////n/8/////v/8/////3/8///////8////wEA8////wMA8////wUA8////wcA8////wkA8/8BAPf/8/8BAPn/8/8BAPv/8/8BAP3/8/8BAP//8/8BAAEA8/8BAAMA8/8BAAUA8/8BAAcA8/8BAAkA8/8DAPf/8/8DAPn/8/8DAPv/8/8DAP3/8/8DAP//8/8DAAEA8/8DAAMA8/8DAAUA8/8DAAcA8/8DAAkA8/8FAPf/8/8FAPn/8/8FAPv/8/8FAP3/8/8FAP//8/8FAAEA8/8FAAMA8/8FAAUA8/8FAAcA8/8FAAkA8/8HAPn/8/8HAPv/8/8HAP3/8/8HAP//8/8HAAEA8/8HAAMA8/8HAAUA8/8HAAcA8/8JAPv/8/8JAP3/8/8JAP//8/8JAAEA8/8JAAMA8/8JAAUA9P/2//r/9P/2//z/9P/2//7/9P/2/wAA9P/2/wIA9P/2/wQA9P/2/wYA9P/4//j/9P/4//r/9P/4//z/9P/4//7/9P/4/wAA9P/4/wIA9P/4/wQA9P/4/wYA9P/4/wgA9P/6//b/9P/6//j/9P/6//r/9P/6//z/9P/6//7/9P/6/wAA9P/6/wIA9P/6/wQA9P/6/wYA9P/6/wgA9P/6/woA9P/8//b/9P/8//j/9P/8//r/9P/8//z/9P/8//7/9P/8/wAA9P/8/wIA9P/8/wQA9P/8/wYA9P/8/wgA9P/8/woA9P/+//b/9P/+//j/9P/+//r/9P/+//z/9P/+//7/9P/+/wAA9P/+/wIA9P/+/wQA9P/+/wYA9P/+/wgA9P/+/woA9P8AAPb/9P8AAPj/9P8AAPr/9P8AAPz/9P8AAP7/9P8AAAAA9P8AAAIA9P8AAAQA9P8AAAYA9P8AAAgA9P8AAAoA9P8CAPb/9P8CAPj/9P8CAPr/9P8CAPz/9P8CAP7/9P8CAAAA9P8CAAIA9P8CAAQA9P8CAAYA9P8CAAgA9P8CAAoA9P8EAPb/9P8EAPj/9P8EAPr/9P8EAPz/9P8EAP7/9P8EAAAA9P8EAAIA9P8EAAQA9P8EAAYA9P8EAAgA9P8EAAoA9P8GAPb/9P8GAPj/9P8GAPr/9P8GAPz/9P8GAP7/9P8GAAAA9P8GAAIA9P8GAAQA9P8GAAYA9P8GAAgA9P8GAAoA9P8IAPj/9P8IAPr/9P8IAPz/9P8IAP7/9P8IAAAA9P8IAAIA9P8IAAQA9P8IAAYA9P8IAAgA9P8KAPr/9P8KAPz/9P8KAP7/9P8KAAAA9P8KAAIA9P8KAAQA9P8KAAYA9f/1//v/9f/1//3/9f/1////9f/1/wEA9f/1/wMA9f/1/wUA9f/3//f/9f/3//n/9f/3//v/9f/3//3/9f/3////9f/3/wEA9f/3/wMA9f/3/wUA9f/3/wcA9f/3/wkA9f/5//f/9f/5//n/9f/5//v/9f/5//3/9f/5////9f/5/wEA9f/5/wMA9f/5/wUA9f/5/wcA9f/5/wkA9f/7//X/9f/7//f/9f/7//n/9f/7//v/9f/7//3/9f/7////9f/7/wEA9f/7/wMA9f/7/wUA9f/7/wcA9f/7/wkA9f/7/wsA9f/9//X/9f/9//f/9f/9//n/9f/9//v/9f/9//3/9f/9////9f/9/wEA9f/9/wMA9f/9/wUA9f/9/wcA9f/9/wkA9f/9/wsA9f////X/9f////f/9f////n/9f////v/9f////3/9f//////9f///wEA9f///wMA9f///wUA9f///wcA9f///wkA9f///wsA9f8BAPX/9f8BAPf/9f8BAPn/9f8BAPv/9f8BAP3/9f8BAP//9f8BAAEA9f8BAAMA9f8BAAUA9f8BAAcA9f8BAAkA9f8BAAsA9f8DAPX/9f8DAPf/9f8DAPn/9f8DAPv/9f8DAP3/9f8DAP//9f8DAAEA9f8DAAMA9f8DAAUA9f8DAAcA9f8DAAkA9f8DAAsA9f8FAPX/9f8FAPf/9f8FAPn/9f8FAPv/9f8FAP3/9f8FAP//9f8FAAEA9f8FAAMA9f8FAAUA9f8FAAcA9f8FAAkA9f8FAAsA9f8HAPf/9f8HAPn/9f8HAPv/9f8HAP3/9f8HAP//9f8HAAEA9f8HAAMA9f8HAAUA9f8HAAcA9f8HAAkA9f8JAPf/9f8JAPn/9f8JAPv/9f8JAP3/9f8JAP//9f8JAAEA9f8JAAMA9f8JAAUA9f8JAAcA9f8JAAkA9f8LAPv/9f8LAP3/9f8LAP//9f8LAAEA9f8LAAMA9f8LAAUA9v/0//r/9v/0//z/9v/0//7/9v/0/wAA9v/0/wIA9v/0/wQA9v/0/wYA9v/2//j/9v/2//r/9v/2//z/9v/2//7/9v/2/wAA9v/2/wIA9v/2/wQA9v/2/wYA9v/2/wgA9v/4//b/9v/4//j/9v/4//r/9v/4//z/9v/4//7/9v/4/wAA9v/4/wIA9v/4/wQA9v/4/wYA9v/4/wgA9v/4/woA9v/6//T/9v/6//b/9v/6//j/9v/6//r/9v/6//z/9v/6//7/9v/6/wAA9v/6/wIA9v/6/wQA9v/6/wYA9v/6/wgA9v/6/woA9v/6/wwA9v/8//T/9v/8//b/9v/8//j/9v/8//r/9v/8//z/9v/8//7/9v/8/wAA9v/8/wIA9v/8/wQA9v/8/wYA9v/8/wgA9v/8/woA9v/8/wwA9v/+//T/9v/+//b/9v/+//j/9v/+//r/9v/+//z/9v/+//7/9v/+/wAA9v/+/wIA9v/+/wQA9v/+/wYA9v/+/wgA9v/+/woA9v/+/wwA9v8AAPT/9v8AAPb/9v8AAPj/9v8AAPr/9v8AAPz/9v8AAP7/9v8AAAAA9v8AAAIA9v8AAAQA9v8AAAYA9v8AAAgA9v8AAAoA9v8AAAwA9v8CAPT/9v8CAPb/9v8CAPj/9v8CAPr/9v8CAPz/9v8CAP7/9v8CAAAA9v8CAAIA9v8CAAQA9v8CAAYA9v8CAAgA9v8CAAoA9v8CAAwA9v8EAPT/9v8EAPb/9v8EAPj/9v8EAPr/9v8EAPz/9v8EAP7/9v8EAAAA9v8EAAIA9v8EAAQA9v8EAAYA9v8EAAgA9v8EAAoA9v8EAAwA9v8GAPT/9v8GAPb/9v8GAPj/9v8GAPr/9v8GAPz/9v8GAP7/9v8GAAAA9v8GAAIA9v8GAAQA9v8GAAYA9v8GAAgA9v8GAAoA9v8GAAwA9v8IAPb/9v8IAPj/9v8IAPr/9v8IAPz/9v8IAP7/9v8IAAAA9v8IAAIA9v8IAAQA9v8IAAYA9v8IAAgA9v8IAAoA9v8KAPj/9v8KAPr/9v8KAPz/9v8KAP7/9v8KAAAA9v8KAAIA9v8KAAQA9v8KAAYA9v8KAAgA9v8MAPr/9v8MAPz/9v8MAP7/9v8MAAAA9v8MAAIA9v8MAAQA9v8MAAYA9//z//v/9//z//3/9//z////9//z/wEA9//z/wMA9//z/wUA9//1//f/9//1//n/9//1//v/9//1//3/9//1////9//1/wEA9//1/wMA9//1/wUA9//1/wcA9//1/wkA9//3//X/9//3//f/9//3//n/9//3//v/9//3//3/9//3////9//3/wEA9//3/wMA9//3/wUA9//3/wcA9//3/wkA9//3/wsA9//5//X/9//5//f/9//5//n/9//5//v/9//5//3/9//5////9//5/wEA9//5/wMA9//5/wUA9//5/wcA9//5/wkA9//5/wsA9//7//P/9//7//X/9//7//f/9//7//n/9//7//v/9//7//3/9//7////9//7/wEA9//7/wMA9//7/wUA9//7/wcA9//7/wkA9//7/wsA9//7/w0A9//9//P/9//9//X/9//9//f/9//9//n/9//9//v/9//9//3/9//9////9//9/wEA9//9/wMA9//9/wUA9//9/wcA9//9/wkA9//9/wsA9//9/w0A9/////P/9/////X/9/////f/9/////n/9/////v/9/////3/9///////9////wEA9////wMA9////wUA9////wcA9////wkA9////wsA9////w0A9/8BAPP/9/8BAPX/9/8BAPf/9/8BAPn/9/8BAPv/9/8BAP3/9/8BAP//9/8BAAEA9/8BAAMA9/8BAAUA9/8BAAcA9/8BAAkA9/8BAAsA9/8BAA0A9/8DAPP/9/8DAPX/9/8DAPf/9/8DAPn/9/8DAPv/9/8DAP3/9/8DAP//9/8DAAEA9/8DAAMA9/8DAAUA9/8DAAcA9/8DAAkA9/8DAAsA9/8DAA0A9/8FAPP/9/8FAPX/9/8FAPf/9/8FAPn/9/8FAPv/9/8FAP3/9/8FAP//9/8FAAEA9/8FAAMA9/8FAAUA9/8FAAcA9/8FAAkA9/8FAAsA9/8FAA0A9/8HAPX/9/8HAPf/9/8HAPn/9/8HAPv/9/8HAP3/9/8HAP//9/8HAAEA9/8HAAMA9/8HAAUA9/8HAAcA9/8HAAkA9/8HAAsA9/8JAPX/9/8JAPf/9/8JAPn/9/8JAPv/9/8JAP3/9/8JAP//9/8JAAEA9/8JAAMA9/8JAAUA9/8JAAcA9/8JAAkA9/8JAAsA9/8LAPf/9/8LAPn/9/8LAPv/9/8LAP3/9/8LAP//9/8LAAEA9/8LAAMA9/8LAAUA9/8LAAcA9/8LAAkA9/8NAPv/9/8NAP3/9/8NAP//9/8NAAEA9/8NAAMA9/8NAAUA+P/y//z/+P/y//7/+P/y/wAA+P/y/wIA+P/y/wQA+P/0//j/+P/0//r/+P/0//z/+P/0//7/+P/0/wAA+P/0/wIA+P/0/wQA+P/0/wYA+P/0/wgA+P/2//b/+P/2//j/+P/2//r/+P/2//z/+P/2//7/+P/2/wAA+P/2/wIA+P/2/wQA+P/2/wYA+P/2/wgA+P/2/woA+P/4//T/+P/4//b/+P/4//j/+P/4//r/+P/4//z/+P/4//7/+P/4/wAA+P/4/wIA+P/4/wQA+P/4/wYA+P/4/wgA+P/4/woA+P/4/wwA+P/6//T/+P/6//b/+P/6//j/+P/6//r/+P/6//z/+P/6//7/+P/6/wAA+P/6/wIA+P/6/wQA+P/6/wYA+P/6/wgA+P/6/woA+P/6/wwA+P/8//L/+P/8//T/+P/8//b/+P/8//j/+P/8//r/+P/8//z/+P/8//7/+P/8/wAA+P/8/wIA+P/8/wQA+P/8/wYA+P/8/wgA+P/8/woA+P/8/wwA+P/8/w4A+P/+//L/+P/+//T/+P/+//b/+P/+//j/+P/+//r/+P/+//z/+P/+//7/+P/+/wAA+P/+/wIA+P/+/wQA+P/+/wYA+P/+/wgA+P/+/woA+P/+/wwA+P/+/w4A+P8AAPL/+P8AAPT/+P8AAPb/+P8AAPj/+P8AAPr/+P8AAPz/+P8AAP7/+P8AAAAA+P8AAAIA+P8AAAQA+P8AAAYA+P8AAAgA+P8AAAoA+P8AAAwA+P8AAA4A+P8CAPL/+P8CAPT/+P8CAPb/+P8CAPj/+P8CAPr/+P8CAPz/+P8CAP7/+P8CAAAA+P8CAAIA+P8CAAQA+P8CAAYA+P8CAAgA+P8CAAoA+P8CAAwA+P8CAA4A+P8EAPL/+P8EAPT/+P8EAPb/+P8EAPj/+P8EAPr/+P8EAPz/+P8EAP7/+P8EAAAA+P8EAAIA+P8EAAQA+P8EAAYA+P8EAAgA+P8EAAoA+P8EAAwA+P8EAA4A+P8GAPT/+P8GAPb/+P8GAPj/+P8GAPr/+P8GAPz/+P8GAP7/+P8GAAAA+P8GAAIA+P8GAAQA+P8GAAYA+P8GAAgA+P8GAAoA+P8GAAwA+P8IAPT/+P8IAPb/+P8IAPj/+P8IAPr/+P8IAPz/+P8IAP7/+P8IAAAA+P8IAAIA+P8IAAQA+P8IAAYA+P8IAAgA+P8IAAoA+P8IAAwA+P8KAPb/+P8KAPj/+P8KAPr/+P8KAPz/+P8KAP7/+P8KAAAA+P8KAAIA+P8KAAQA+P8KAAYA+P8KAAgA+P8KAAoA+P8MAPj/+P8MAPr/+P8MAPz/+P8MAP7/+P8MAAAA+P8MAAIA+P8MAAQA+P8MAAYA+P8MAAgA+P8OAPz/+P8OAP7/+P8OAAAA+P8OAAIA+P8OAAQA+f/x//3/+f/x////+f/x/wEA+f/x/wMA+f/z//n/+f/z//v/+f/z//3/+f/z////+f/z/wEA+f/z/wMA+f/z/wUA+f/z/wcA+f/1//f/+f/1//n/+f/1//v/+f/1//3/+f/1////+f/1/wEA+f/1/wMA+f/1/wUA+f/1/wcA+f/1/wkA+f/3//X/+f/3//f/+f/3//n/+f/3//v/+f/3//3/+f/3////+f/3/wEA+f/3/wMA+f/3/wUA+f/3/wcA+f/3/wkA+f/3/wsA+f/5//P/+f/5//X/+f/5//f/+f/5//n/+f/5//v/+f/5//3/+f/5////+f/5/wEA+f/5/wMA+f/5/wUA+f/5/wcA+f/5/wkA+f/5/wsA+f/5/w0A+f/7//P/+f/7//X/+f/7//f/+f/7//n/+f/7//v/+f/7//3/+f/7////+f/7/wEA+f/7/wMA+f/7/wUA+f/7/wcA+f/7/wkA+f/7/wsA+f/7/w0A+f/9//H/+f/9//P/+f/9//X/+f/9//f/+f/9//n/+f/9//v/+f/9//3/+f/9////+f/9/wEA+f/9/wMA+f/9/wUA+f/9/wcA+f/9/wkA+f/9/wsA+f/9/w0A+f/9/w8A+f////H/+f////P/+f////X/+f////f/+f////n/+f////v/+f////3/+f//////+f///wEA+f///wMA+f///wUA+f///wcA+f///wkA+f///wsA+f///w0A+f///w8A+f8BAPH/+f8BAPP/+f8BAPX/+f8BAPf/+f8BAPn/+f8BAPv/+f8BAP3/+f8BAP//+f8BAAEA+f8BAAMA+f8BAAUA+f8BAAcA+f8BAAkA+f8BAAsA+f8BAA0A+f8BAA8A+f8DAPH/+f8DAPP/+f8DAPX/+f8DAPf/+f8DAPn/+f8DAPv/+f8DAP3/+f8DAP//+f8DAAEA+f8DAAMA+f8DAAUA+f8DAAcA+f8DAAkA+f8DAAsA+f8DAA0A+f8DAA8A+f8FAPP/+f8FAPX/+f8FAPf/+f8FAPn/+f8FAPv/+f8FAP3/+f8FAP//+f8FAAEA+f8FAAMA+f8FAAUA+f8FAAcA+f8FAAkA+f8FAAsA+f8FAA0A+f8HAPP/+f8HAPX/+f8HAPf/+f8HAPn/+f8HAPv/+f8HAP3/+f8HAP//+f8HAAEA+f8HAAMA+f8HAAUA+f8HAAcA+f8HAAkA+f8HAAsA+f8HAA0A+f8JAPX/+f8JAPf/+f8JAPn/+f8JAPv/+f8JAP3/+f8JAP//+f8JAAEA+f8JAAMA+f8JAAUA+f8JAAcA+f8JAAkA+f8JAAsA+f8LAPf/+f8LAPn/+f8LAPv/+f8LAP3/+f8LAP//+f8LAAEA+f8LAAMA+f8LAAUA+f8LAAcA+f8LAAkA+f8NAPn/+f8NAPv/+f8NAP3/+f8NAP//+f8NAAEA+f8NAAMA+f8NAAUA+f8NAAcA+f8PAP3/+f8PAP//+f8PAAEA+f8PAAMA+v/y//r/+v/y//z/+v/y//7/+v/y/wAA+v/y/wIA+v/y/wQA+v/y/wYA+v/0//b/+v/0//j/+v/0//r/+v/0//z/+v/0//7/+v/0/wAA+v/0/wIA+v/0/wQA+v/0/wYA+v/0/wgA+v/0/woA+v/2//T/+v/2//b/+v/2//j/+v/2//r/+v/2//z/+v/2//7/+v/2/wAA+v/2/wIA+v/2/wQA+v/2/wYA+v/2/wgA+v/2/woA+v/2/wwA+v/4//T/+v/4//b/+v/4//j/+v/4//r/+v/4//z/+v/4//7/+v/4/wAA+v/4/wIA+v/4/wQA+v/4/wYA+v/4/wgA+v/4/woA+v/4/wwA+v/6//L/+v/6//T/+v/6//b/+v/6//j/+v/6//r/+v/6//z/+v/6//7/+v/6/wAA+v/6/wIA+v/6/wQA+v/6/wYA+v/6/wgA+v/6/woA+v/6/wwA+v/6/w4A+v/8//L/+v/8//T/+v/8//b/+v/8//j/+v/8//r/+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/8/wYA+v/8/wgA+v/8/woA+v/8/wwA+v/8/w4A+v/+//L/+v/+//T/+v/+//b/+v/+//j/+v/+//r/+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v/+/wYA+v/+/wgA+v/+/woA+v/+/wwA+v/+/w4A+v8AAPL/+v8AAPT/+v8AAPb/+v8AAPj/+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8AAAgA+v8AAAoA+v8AAAwA+v8AAA4A+v8CAPL/+v8CAPT/+v8CAPb/+v8CAPj/+v8CAPr/+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8CAAYA+v8CAAgA+v8CAAoA+v8CAAwA+v8CAA4A+v8EAPL/+v8EAPT/+v8EAPb/+v8EAPj/+v8EAPr/+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8EAAYA+v8EAAgA+v8EAAoA+v8EAAwA+v8EAA4A+v8GAPL/+v8GAPT/+v8GAPb/+v8GAPj/+v8GAPr/+v8GAPz/+v8GAP7/+v8GAAAA+v8GAAIA+v8GAAQA+v8GAAYA+v8GAAgA+v8GAAoA+v8GAAwA+v8GAA4A+v8IAPT/+v8IAPb/+v8IAPj/+v8IAPr/+v8IAPz/+v8IAP7/+v8IAAAA+v8IAAIA+v8IAAQA+v8IAAYA+v8IAAgA+v8IAAoA+v8IAAwA+v8KAPT/+v8KAPb/+v8KAPj/+v8KAPr/+v8KAPz/+v8KAP7/+v8KAAAA+v8KAAIA+v8KAAQA+v8KAAYA+v8KAAgA+v8KAAoA+v8KAAwA+v8MAPb/+v8MAPj/+v8MAPr/+v8MAPz/+v8MAP7/+v8MAAAA+v8MAAIA+v8MAAQA+v8MAAYA+v8MAAgA+v8MAAoA+v8OAPr/+v8OAPz/+v8OAP7/+v8OAAAA+v8OAAIA+v8OAAQA+v8OAAYA+//x//v/+//x//3/+//x////+//x/wEA+//x/wMA+//x/wUA+//z//f/+//z//n/+//z//v/+//z//3/+//z////+//z/wEA+//z/wMA+//z/wUA+//z/wcA+//z/wkA+//1//X/+//1//f/+//1//n/+//1//v/+//1//3/+//1////+//1/wEA+//1/wMA+//1/wUA+//1/wcA+//1/wkA+//1/wsA+//3//P/+//3//X/+//3//f/+//3//n/+//3//v/+//3//3/+//3////+//3/wEA+//3/wMA+//3/wUA+//3/wcA+//3/wkA+//3/wsA+//3/w0A+//5//P/+//5//X/+//5//f/+//5//n/+//5//v/+//5//3/+//5////+//5/wEA+//5/wMA+//5/wUA+//5/wcA+//5/wkA+//5/wsA+//5/w0A+//7//H/+//7//P/+//7//X/+//7//f/+//7//n/+//7//v/+//7//3/+//7////+//7/wEA+//7/wMA+//7/wUA+//7/wcA+//7/wkA+//7/wsA+//7/w0A+//7/w8A+//9//H/+//9//P/+//9//X/+//9//f/+//9//n/+//9//v/+//9//3/+//9////+//9/wEA+//9/wMA+//9/wUA+//9/wcA+//9/wkA+//9/wsA+//9/w0A+//9/w8A+/////H/+/////P/+/////X/+/////f/+/////n/+/////v/+/////3/+///////+////wEA+////wMA+////wUA+////wcA+////wkA+////wsA+////w0A+////w8A+/8BAPH/+/8BAPP/+/8BAPX/+/8BAPf/+/8BAPn/+/8BAPv/+/8BAP3/+/8BAP//+/8BAAEA+/8BAAMA+/8BAAUA+/8BAAcA+/8BAAkA+/8BAAsA+/8BAA0A+/8BAA8A+/8DAPH/+/8DAPP/+/8DAPX/+/8DAPf/+/8DAPn/+/8DAPv/+/8DAP3/+/8DAP//+/8DAAEA+/8DAAMA+/8DAAUA+/8DAAcA+/8DAAkA+/8DAAsA+/8DAA0A+/8DAA8A+/8FAPH/+/8FAPP/+/8FAPX/+/8FAPf/+/8FAPn/+/8FAPv/+/8FAP3/+/8FAP//+/8FAAEA+/8FAAMA+/8FAAUA+/8FAAcA+/8FAAkA+/8FAAsA+/8FAA0A+/8FAA8A+/8HAPP/+/8HAPX/+/8HAPf/+/8HAPn/+/8HAPv/+/8HAP3/+/8HAP//+/8HAAEA+/8HAAMA+/8HAAUA+/8HAAcA+/8HAAkA+/8HAAsA+/8HAA0A+/8JAPP/+/8JAPX/+/8JAPf/+/8JAPn/+/8JAPv/+/8JAP3/+/8JAP//+/8JAAEA+/8JAAMA+/8JAAUA+/8JAAcA+/8JAAkA+/8JAAsA+/8JAA0A+/8LAPX/+/8LAPf/+/8LAPn/+/8LAPv/+/8LAP3/+/8LAP//+/8LAAEA+/8LAAMA+/8LAAUA+/8LAAcA+/8LAAkA+/8LAAsA+/8NAPf/+/8NAPn/+/8NAPv/+/8NAP3/+/8NAP//+/8NAAEA+/8NAAMA+/8NAAUA+/8NAAcA+/8NAAkA+/8PAPv/+/8PAP3/+/8PAP//+/8PAAEA+/8PAAMA+/8PAAUA/P/w//7//P/w/wAA/P/w/wIA/P/y//j//P/y//r//P/y//z//P/y//7//P/y/wAA/P/y/wIA/P/y/wQA/P/y/wYA/P/y/wgA/P/0//b//P/0//j//P/0//r//P/0//z//P/0//7//P/0/wAA/P/0/wIA/P/0/wQA/P/0/wYA/P/0/wgA/P/0/woA/P/2//T//P/2//b//P/2//j//P/2//r//P/2//z//P/2//7//P/2/wAA/P/2/wIA/P/2/wQA/P/2/wYA/P/2/wgA/P/2/woA/P/2/wwA/P/4//L//P/4//T//P/4//b//P/4//j//P/4//r//P/4//z//P/4//7//P/4/wAA/P/4/wIA/P/4/wQA/P/4/wYA/P/4/wgA/P/4/woA/P/4/wwA/P/4/w4A/P/6//L//P/6//T//P/6//b//P/6//j//P/6//r//P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/6/wYA/P/6/wgA/P/6/woA/P/6/wwA/P/6/w4A/P/8//L//P/8//T//P/8//b//P/8//j//P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/8/wgA/P/8/woA/P/8/wwA/P/8/w4A/P/+//D//P/+//L//P/+//T//P/+//b//P/+//j//P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P/+/wgA/P/+/woA/P/+/wwA/P/+/w4A/P/+/xAA/P8AAPD//P8AAPL//P8AAPT//P8AAPb//P8AAPj//P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8AAAgA/P8AAAoA/P8AAAwA/P8AAA4A/P8AABAA/P8CAPD//P8CAPL//P8CAPT//P8CAPb//P8CAPj//P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8CAAgA/P8CAAoA/P8CAAwA/P8CAA4A/P8CABAA/P8EAPL//P8EAPT//P8EAPb//P8EAPj//P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8EAAgA/P8EAAoA/P8EAAwA/P8EAA4A/P8GAPL//P8GAPT//P8GAPb//P8GAPj//P8GAPr//P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8GAAYA/P8GAAgA/P8GAAoA/P8GAAwA/P8GAA4A/P8IAPL//P8IAPT//P8IAPb//P8IAPj//P8IAPr//P8IAPz//P8IAP7//P8IAAAA/P8IAAIA/P8IAAQA/P8IAAYA/P8IAAgA/P8IAAoA/P8IAAwA/P8IAA4A/P8KAPT//P8KAPb//P8KAPj//P8KAPr//P8KAPz//P8KAP7//P8KAAAA/P8KAAIA/P8KAAQA/P8KAAYA/P8KAAgA/P8KAAoA/P8KAAwA/P8MAPb//P8MAPj//P8MAPr//P8MAPz//P8MAP7//P8MAAAA/P8MAAIA/P8MAAQA/P8MAAYA/P8MAAgA/P8MAAoA/P8OAPj//P8OAPr//P8OAPz//P8OAP7//P8OAAAA/P8OAAIA/P8OAAQA/P8OAAYA/P8OAAgA/P8QAP7//P8QAAAA/P8QAAIA/f/x//n//f/x//v//f/x//3//f/x/////f/x/wEA/f/x/wMA/f/x/wUA/f/x/wcA/f/z//f//f/z//n//f/z//v//f/z//3//f/z/////f/z/wEA/f/z/wMA/f/z/wUA/f/z/wcA/f/z/wkA/f/1//X//f/1//f//f/1//n//f/1//v//f/1//3//f/1/////f/1/wEA/f/1/wMA/f/1/wUA/f/1/wcA/f/1/wkA/f/1/wsA/f/3//P//f/3//X//f/3//f//f/3//n//f/3//v//f/3//3//f/3/////f/3/wEA/f/3/wMA/f/3/wUA/f/3/wcA/f/3/wkA/f/3/wsA/f/3/w0A/f/5//H//f/5//P//f/5//X//f/5//f//f/5//n//f/5//v//f/5//3//f/5/////f/5/wEA/f/5/wMA/f/5/wUA/f/5/wcA/f/5/wkA/f/5/wsA/f/5/w0A/f/5/w8A/f/7//H//f/7//P//f/7//X//f/7//f//f/7//n//f/7//v//f/7//3//f/7/////f/7/wEA/f/7/wMA/f/7/wUA/f/7/wcA/f/7/wkA/f/7/wsA/f/7/w0A/f/7/w8A/f/9//H//f/9//P//f/9//X//f/9//f//f/9//n//f/9//v//f/9//3//f/9/////f/9/wEA/f/9/wMA/f/9/wUA/f/9/wcA/f/9/wkA/f/9/wsA/f/9/w0A/f/9/w8A/f////H//f////P//f////X//f////f//f////n//f////v//f////3//f///////f///wEA/f///wMA/f///wUA/f///wcA/f///wkA/f///wsA/f///w0A/f///w8A/f8BAPH//f8BAPP//f8BAPX//f8BAPf//f8BAPn//f8BAPv//f8BAP3//f8BAP///f8BAAEA/f8BAAMA/f8BAAUA/f8BAAcA/f8BAAkA/f8BAAsA/f8BAA0A/f8BAA8A/f8DAPH//f8DAPP//f8DAPX//f8DAPf//f8DAPn//f8DAPv//f8DAP3//f8DAP///f8DAAEA/f8DAAMA/f8DAAUA/f8DAAcA/f8DAAkA/f8DAAsA/f8DAA0A/f8DAA8A/f8FAPH//f8FAPP//f8FAPX//f8FAPf//f8FAPn//f8FAPv//f8FAP3//f8FAP///f8FAAEA/f8FAAMA/f8FAAUA/f8FAAcA/f8FAAkA/f8FAAsA/f8FAA0A/f8FAA8A/f8HAPH//f8HAPP//f8HAPX//f8HAPf//f8HAPn//f8HAPv//f8HAP3//f8HAP///f8HAAEA/f8HAAMA/f8HAAUA/f8HAAcA/f8HAAkA/f8HAAsA/f8HAA0A/f8HAA8A/f8JAPP//f8JAPX//f8JAPf//f8JAPn//f8JAPv//f8JAP3//f8JAP///f8JAAEA/f8JAAMA/f8JAAUA/f8JAAcA/f8JAAkA/f8JAAsA/f8JAA0A/f8LAPX//f8LAPf//f8LAPn//f8LAPv//f8LAP3//f8LAP///f8LAAEA/f8LAAMA/f8LAAUA/f8LAAcA/f8LAAkA/f8LAAsA/f8NAPf//f8NAPn//f8NAPv//f8NAP3//f8NAP///f8NAAEA/f8NAAMA/f8NAAUA/f8NAAcA/f8NAAkA/f8PAPn//f8PAPv//f8PAP3//f8PAP///f8PAAEA/f8PAAMA/f8PAAUA/f8PAAcA/v/w//z//v/w//7//v/w/wAA/v/w/wIA/v/w/wQA/v/y//j//v/y//r//v/y//z//v/y//7//v/y/wAA/v/y/wIA/v/y/wQA/v/y/wYA/v/y/wgA/v/0//b//v/0//j//v/0//r//v/0//z//v/0//7//v/0/wAA/v/0/wIA/v/0/wQA/v/0/wYA/v/0/wgA/v/0/woA/v/2//T//v/2//b//v/2//j//v/2//r//v/2//z//v/2//7//v/2/wAA/v/2/wIA/v/2/wQA/v/2/wYA/v/2/wgA/v/2/woA/v/2/wwA/v/4//L//v/4//T//v/4//b//v/4//j//v/4//r//v/4//z//v/4//7//v/4/wAA/v/4/wIA/v/4/wQA/v/4/wYA/v/4/wgA/v/4/woA/v/4/wwA/v/4/w4A/v/6//L//v/6//T//v/6//b//v/6//j//v/6//r//v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/6/wYA/v/6/wgA/v/6/woA/v/6/wwA/v/6/w4A/v/8//D//v/8//L//v/8//T//v/8//b//v/8//j//v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/8/wgA/v/8/woA/v/8/wwA/v/8/w4A/v/8/xAA/v/+//D//v/+//L//v/+//T//v/+//b//v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v/+/woA/v/+/wwA/v/+/w4A/v/+/xAA/v8AAPD//v8AAPL//v8AAPT//v8AAPb//v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8AAAoA/v8AAAwA/v8AAA4A/v8AABAA/v8CAPD//v8CAPL//v8CAPT//v8CAPb//v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8CAAoA/v8CAAwA/v8CAA4A/v8CABAA/v8EAPD//v8EAPL//v8EAPT//v8EAPb//v8EAPj//v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8EAAgA/v8EAAoA/v8EAAwA/v8EAA4A/v8EABAA/v8GAPL//v8GAPT//v8GAPb//v8GAPj//v8GAPr//v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8GAAYA/v8GAAgA/v8GAAoA/v8GAAwA/v8GAA4A/v8IAPL//v8IAPT//v8IAPb//v8IAPj//v8IAPr//v8IAPz//v8IAP7//v8IAAAA/v8IAAIA/v8IAAQA/v8IAAYA/v8IAAgA/v8IAAoA/v8IAAwA/v8IAA4A/v8KAPT//v8KAPb//v8KAPj//v8KAPr//v8KAPz//v8KAP7//v8KAAAA/v8KAAIA/v8KAAQA/v8KAAYA/v8KAAgA/v8KAAoA/v8KAAwA/v8MAPb//v8MAPj//v8MAPr//v8MAPz//v8MAP7//v8MAAAA/v8MAAIA/v8MAAQA/v8MAAYA/v8MAAgA/v8MAAoA/v8OAPj//v8OAPr//v8OAPz//v8OAP7//v8OAAAA/v8OAAIA/v8OAAQA/v8OAAYA/v8OAAgA/v8QAPz//v8QAP7//v8QAAAA/v8QAAIA/v8QAAQA///x//n////x//v////x//3////x///////x/wEA///x/wMA///x/wUA///x/wcA///z//f////z//n////z//v////z//3////z///////z/wEA///z/wMA///z/wUA///z/wcA///z/wkA///1//X////1//f////1//n////1//v////1//3////1///////1/wEA///1/wMA///1/wUA///1/wcA///1/wkA///1/wsA///3//P////3//X////3//f////3//n////3//v////3//3////3///////3/wEA///3/wMA///3/wUA///3/wcA///3/wkA///3/wsA///3/w0A///5//H////5//P////5//X////5//f////5//n////5//v////5//3////5///////5/wEA///5/wMA///5/wUA///5/wcA///5/wkA///5/wsA///5/w0A///5/w8A///7//H////7//P////7//X////7//f////7//n////7//v////7//3////7///////7/wEA///7/wMA///7/wUA///7/wcA///7/wkA///7/wsA///7/w0A///7/w8A///9//H////9//P////9//X////9//f////9//n////9//v////9//3////9///////9/wEA///9/wMA///9/wUA///9/wcA///9/wkA///9/wsA///9/w0A///9/w8A//////H///////P///////X///////f///////n///////v///////3//////////////wEA/////wMA/////wUA/////wcA/////wkA/////wsA/////w0A/////w8A//8BAPH///8BAPP///8BAPX///8BAPf///8BAPn///8BAPv///8BAP3///8BAP////8BAAEA//8BAAMA//8BAAUA//8BAAcA//8BAAkA//8BAAsA//8BAA0A//8BAA8A//8DAPH///8DAPP///8DAPX///8DAPf///8DAPn///8DAPv///8DAP3///8DAP////8DAAEA//8DAAMA//8DAAUA//8DAAcA//8DAAkA//8DAAsA//8DAA0A//8DAA8A//8FAPH///8FAPP///8FAPX///8FAPf///8FAPn///8FAPv///8FAP3///8FAP////8FAAEA//8FAAMA//8FAAUA//8FAAcA//8FAAkA//8FAAsA//8FAA0A//8FAA8A//8HAPH///8HAPP///8HAPX///8HAPf///8HAPn///8HAPv///8HAP3///8HAP////8HAAEA//8HAAMA//8HAAUA//8HAAcA//8HAAkA//8HAAsA//8HAA0A//8HAA8A//8JAPP///8JAPX///8JAPf///8JAPn///8JAPv///8JAP3///8JAP////8JAAEA//8JAAMA//8JAAUA//8JAAcA//8JAAkA//8JAAsA//8JAA0A//8LAPX///8LAPf///8LAPn///8LAPv///8LAP3///8LAP////8LAAEA//8LAAMA//8LAAUA//8LAAcA//8LAAkA//8LAAsA//8NAPf///8NAPn///8NAPv///8NAP3///8NAP////8NAAEA//8NAAMA//8NAAUA//8NAAcA//8NAAkA//8PAPn///8PAPv///8PAP3///8PAP////8PAAEA//8PAAMA//8PAAUA//8PAAcAAADw//z/AADw//7/AADw/wAAAADw/wIAAADw/wQAAADy//j/AADy//r/AADy//z/AADy//7/AADy/wAAAADy/wIAAADy/wQAAADy/wYAAADy/wgAAAD0//b/AAD0//j/AAD0//r/AAD0//z/AAD0//7/AAD0/wAAAAD0/wIAAAD0/wQAAAD0/wYAAAD0/wgAAAD0/woAAAD2//T/AAD2//b/AAD2//j/AAD2//r/AAD2//z/AAD2//7/AAD2/wAAAAD2/wIAAAD2/wQAAAD2/wYAAAD2/wgAAAD2/woAAAD2/wwAAAD4//L/AAD4//T/AAD4//b/AAD4//j/AAD4//r/AAD4//z/AAD4//7/AAD4/wAAAAD4/wIAAAD4/wQAAAD4/wYAAAD4/wgAAAD4/woAAAD4/wwAAAD4/w4AAAD6//L/AAD6//T/AAD6//b/AAD6//j/AAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD6/wgAAAD6/woAAAD6/wwAAAD6/w4AAAD8//D/AAD8//L/AAD8//T/AAD8//b/AAD8//j/AAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD8/wgAAAD8/woAAAD8/wwAAAD8/w4AAAD8/xAAAAD+//D/AAD+//L/AAD+//T/AAD+//b/AAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD+/woAAAD+/wwAAAD+/w4AAAD+/xAAAAAAAPD/AAAAAPL/AAAAAPT/AAAAAPb/AAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAAAAAoAAAAAAAwAAAAAAA4AAAAAABAAAAACAPD/AAACAPL/AAACAPT/AAACAPb/AAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAACAAoAAAACAAwAAAACAA4AAAACABAAAAAEAPD/AAAEAPL/AAAEAPT/AAAEAPb/AAAEAPj/AAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAEAAgAAAAEAAoAAAAEAAwAAAAEAA4AAAAEABAAAAAGAPL/AAAGAPT/AAAGAPb/AAAGAPj/AAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAGAAgAAAAGAAoAAAAGAAwAAAAGAA4AAAAIAPL/AAAIAPT/AAAIAPb/AAAIAPj/AAAIAPr/AAAIAPz/AAAIAP7/AAAIAAAAAAAIAAIAAAAIAAQAAAAIAAYAAAAIAAgAAAAIAAoAAAAIAAwAAAAIAA4AAAAKAPT/AAAKAPb/AAAKAPj/AAAKAPr/AAAKAPz/AAAKAP7/AAAKAAAAAAAKAAIAAAAKAAQAAAAKAAYAAAAKAAgAAAAKAAoAAAAKAAwAAAAMAPb/AAAMAPj/AAAMAPr/AAAMAPz/AAAMAP7/AAAMAAAAAAAMAAIAAAAMAAQAAAAMAAYAAAAMAAgAAAAMAAoAAAAOAPj/AAAOAPr/AAAOAPz/AAAOAP7/AAAOAAAAAAAOAAIAAAAOAAQAAAAOAAYAAAAOAAgAAAAQAPz/AAAQAP7/AAAQAAAAAAAQAAIAAAAQAAQAAQDx//n/AQDx//v/AQDx//3/AQDx////AQDx/wEAAQDx/wMAAQDx/wUAAQDx/wcAAQDz//f/AQDz//n/AQDz//v/AQDz//3/AQDz////AQDz/wEAAQDz/wMAAQDz/wUAAQDz/wcAAQDz/wkAAQD1//X/AQD1//f/AQD1//n/AQD1//v/AQD1//3/AQD1////AQD1/wEAAQD1/wMAAQD1/wUAAQD1/wcAAQD1/wkAAQD1/wsAAQD3//P/AQD3//X/AQD3//f/AQD3//n/AQD3//v/AQD3//3/AQD3////AQD3/wEAAQD3/wMAAQD3/wUAAQD3/wcAAQD3/wkAAQD3/wsAAQD3/w0AAQD5//H/AQD5//P/AQD5//X/AQD5//f/AQD5//n/AQD5//v/AQD5//3/AQD5////AQD5/wEAAQD5/wMAAQD5/wUAAQD5/wcAAQD5/wkAAQD5/wsAAQD5/w0AAQD5/w8AAQD7//H/AQD7//P/AQD7//X/AQD7//f/AQD7//n/AQD7//v/AQD7//3/AQD7////AQD7/wEAAQD7/wMAAQD7/wUAAQD7/wcAAQD7/wkAAQD7/wsAAQD7/w0AAQD7/w8AAQD9//H/AQD9//P/AQD9//X/AQD9//f/AQD9//n/AQD9//v/AQD9//3/AQD9////AQD9/wEAAQD9/wMAAQD9/wUAAQD9/wcAAQD9/wkAAQD9/wsAAQD9/w0AAQD9/w8AAQD///H/AQD///P/AQD///X/AQD///f/AQD///n/AQD///v/AQD///3/AQD/////AQD//wEAAQD//wMAAQD//wUAAQD//wcAAQD//wkAAQD//wsAAQD//w0AAQD//w8AAQABAPH/AQABAPP/AQABAPX/AQABAPf/AQABAPn/AQABAPv/AQABAP3/AQABAP//AQABAAEAAQABAAMAAQABAAUAAQABAAcAAQABAAkAAQABAAsAAQABAA0AAQABAA8AAQADAPH/AQADAPP/AQADAPX/AQADAPf/AQADAPn/AQADAPv/AQADAP3/AQADAP//AQADAAEAAQADAAMAAQADAAUAAQADAAcAAQADAAkAAQADAAsAAQADAA0AAQADAA8AAQAFAPH/AQAFAPP/AQAFAPX/AQAFAPf/AQAFAPn/AQAFAPv/AQAFAP3/AQAFAP//AQAFAAEAAQAFAAMAAQAFAAUAAQAFAAcAAQAFAAkAAQAFAAsAAQAFAA0AAQAFAA8AAQAHAPH/AQAHAPP/AQAHAPX/AQAHAPf/AQAHAPn/AQAHAPv/AQAHAP3/AQAHAP//AQAHAAEAAQAHAAMAAQAHAAUAAQAHAAcAAQAHAAkAAQAHAAsAAQAHAA0AAQAHAA8AAQAJAPP/AQAJAPX/AQAJAPf/AQAJAPn/AQAJAPv/AQAJAP3/AQAJAP//AQAJAAEAAQAJAAMAAQAJAAUAAQAJAAcAAQAJAAkAAQAJAAsAAQAJAA0AAQALAPX/AQALAPf/AQALAPn/AQALAPv/AQALAP3/AQALAP//AQALAAEAAQALAAMAAQALAAUAAQALAAcAAQALAAkAAQALAAsAAQANAPf/AQANAPn/AQANAPv/AQANAP3/AQANAP//AQANAAEAAQANAAMAAQANAAUAAQANAAcAAQANAAkAAQAPAPn/AQAPAPv/AQAPAP3/AQAPAP//AQAPAAEAAQAPAAMAAQAPAAUAAQAPAAcAAgDw//z/AgDw//7/AgDw/wAAAgDw/wIAAgDw/wQAAgDy//j/AgDy//r/AgDy//z/AgDy//7/AgDy/wAAAgDy/wIAAgDy/wQAAgDy/wYAAgDy/wgAAgD0//b/AgD0//j/AgD0//r/AgD0//z/AgD0//7/AgD0/wAAAgD0/wIAAgD0/wQAAgD0/wYAAgD0/wgAAgD0/woAAgD2//T/AgD2//b/AgD2//j/AgD2//r/AgD2//z/AgD2//7/AgD2/wAAAgD2/wIAAgD2/wQAAgD2/wYAAgD2/wgAAgD2/woAAgD2/wwAAgD4//L/AgD4//T/AgD4//b/AgD4//j/AgD4//r/AgD4//z/AgD4//7/AgD4/wAAAgD4/wIAAgD4/wQAAgD4/wYAAgD4/wgAAgD4/woAAgD4/wwAAgD4/w4AAgD6//L/AgD6//T/AgD6//b/AgD6//j/AgD6//r/AgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD6/wYAAgD6/wgAAgD6/woAAgD6/wwAAgD6/w4AAgD8//D/AgD8//L/AgD8//T/AgD8//b/AgD8//j/AgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD8/wgAAgD8/woAAgD8/wwAAgD8/w4AAgD8/xAAAgD+//D/AgD+//L/AgD+//T/AgD+//b/AgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD+/woAAgD+/wwAAgD+/w4AAgD+/xAAAgAAAPD/AgAAAPL/AgAAAPT/AgAAAPb/AgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgAAAAoAAgAAAAwAAgAAAA4AAgAAABAAAgACAPD/AgACAPL/AgACAPT/AgACAPb/AgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgACAAoAAgACAAwAAgACAA4AAgACABAAAgAEAPD/AgAEAPL/AgAEAPT/AgAEAPb/AgAEAPj/AgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAEAAgAAgAEAAoAAgAEAAwAAgAEAA4AAgAEABAAAgAGAPL/AgAGAPT/AgAGAPb/AgAGAPj/AgAGAPr/AgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAGAAYAAgAGAAgAAgAGAAoAAgAGAAwAAgAGAA4AAgAIAPL/AgAIAPT/AgAIAPb/AgAIAPj/AgAIAPr/AgAIAPz/AgAIAP7/AgAIAAAAAgAIAAIAAgAIAAQAAgAIAAYAAgAIAAgAAgAIAAoAAgAIAAwAAgAIAA4AAgAKAPT/AgAKAPb/AgAKAPj/AgAKAPr/AgAKAPz/AgAKAP7/AgAKAAAAAgAKAAIAAgAKAAQAAgAKAAYAAgAKAAgAAgAKAAoAAgAKAAwAAgAMAPb/AgAMAPj/AgAMAPr/AgAMAPz/AgAMAP7/AgAMAAAAAgAMAAIAAgAMAAQAAgAMAAYAAgAMAAgAAgAMAAoAAgAOAPj/AgAOAPr/AgAOAPz/AgAOAP7/AgAOAAAAAgAOAAIAAgAOAAQAAgAOAAYAAgAOAAgAAgAQAPz/AgAQAP7/AgAQAAAAAgAQAAIAAgAQAAQAAwDx//n/AwDx//v/AwDx//3/AwDx////AwDx/wEAAwDx/wMAAwDx/wUAAwDx/wcAAwDz//f/AwDz//n/AwDz//v/AwDz//3/AwDz////AwDz/wEAAwDz/wMAAwDz/wUAAwDz/wcAAwDz/wkAAwD1//X/AwD1//f/AwD1//n/AwD1//v/AwD1//3/AwD1////AwD1/wEAAwD1/wMAAwD1/wUAAwD1/wcAAwD1/wkAAwD1/wsAAwD3//P/AwD3//X/AwD3//f/AwD3//n/AwD3//v/AwD3//3/AwD3////AwD3/wEAAwD3/wMAAwD3/wUAAwD3/wcAAwD3/wkAAwD3/wsAAwD3/w0AAwD5//H/AwD5//P/AwD5//X/AwD5//f/AwD5//n/AwD5//v/AwD5//3/AwD5////AwD5/wEAAwD5/wMAAwD5/wUAAwD5/wcAAwD5/wkAAwD5/wsAAwD5/w0AAwD5/w8AAwD7//H/AwD7//P/AwD7//X/AwD7//f/AwD7//n/AwD7//v/AwD7//3/AwD7////AwD7/wEAAwD7/wMAAwD7/wUAAwD7/wcAAwD7/wkAAwD7/wsAAwD7/w0AAwD7/w8AAwD9//H/AwD9//P/AwD9//X/AwD9//f/AwD9//n/AwD9//v/AwD9//3/AwD9////AwD9/wEAAwD9/wMAAwD9/wUAAwD9/wcAAwD9/wkAAwD9/wsAAwD9/w0AAwD9/w8AAwD///H/AwD///P/AwD///X/AwD///f/AwD///n/AwD///v/AwD///3/AwD/////AwD//wEAAwD//wMAAwD//wUAAwD//wcAAwD//wkAAwD//wsAAwD//w0AAwD//w8AAwABAPH/AwABAPP/AwABAPX/AwABAPf/AwABAPn/AwABAPv/AwABAP3/AwABAP//AwABAAEAAwABAAMAAwABAAUAAwABAAcAAwABAAkAAwABAAsAAwABAA0AAwABAA8AAwADAPH/AwADAPP/AwADAPX/AwADAPf/AwADAPn/AwADAPv/AwADAP3/AwADAP//AwADAAEAAwADAAMAAwADAAUAAwADAAcAAwADAAkAAwADAAsAAwADAA0AAwADAA8AAwAFAPH/AwAFAPP/AwAFAPX/AwAFAPf/AwAFAPn/AwAFAPv/AwAFAP3/AwAFAP//AwAFAAEAAwAFAAMAAwAFAAUAAwAFAAcAAwAFAAkAAwAFAAsAAwAFAA0AAwAFAA8AAwAHAPH/AwAHAPP/AwAHAPX/AwAHAPf/AwAHAPn/AwAHAPv/AwAHAP3/AwAHAP//AwAHAAEAAwAHAAMAAwAHAAUAAwAHAAcAAwAHAAkAAwAHAAsAAwAHAA0AAwAHAA8AAwAJAPP/AwAJAPX/AwAJAPf/AwAJAPn/AwAJAPv/AwAJAP3/AwAJAP//AwAJAAEAAwAJAAMAAwAJAAUAAwAJAAcAAwAJAAkAAwAJAAsAAwAJAA0AAwALAPX/AwALAPf/AwALAPn/AwALAPv/AwALAP3/AwALAP//AwALAAEAAwALAAMAAwALAAUAAwALAAcAAwALAAkAAwALAAsAAwANAPf/AwANAPn/AwANAPv/AwANAP3/AwANAP//AwANAAEAAwANAAMAAwANAAUAAwANAAcAAwANAAkAAwAPAPn/AwAPAPv/AwAPAP3/AwAPAP//AwAPAAEAAwAPAAMAAwAPAAUAAwAPAAcABADw//7/BADw/wAABADw/wIABADy//j/BADy//r/BADy//z/BADy//7/BADy/wAABADy/wIABADy/wQABADy/wYABADy/wgABAD0//b/BAD0//j/BAD0//r/BAD0//z/BAD0//7/BAD0/wAABAD0/wIABAD0/wQABAD0/wYABAD0/wgABAD0/woABAD2//T/BAD2//b/BAD2//j/BAD2//r/BAD2//z/BAD2//7/BAD2/wAABAD2/wIABAD2/wQABAD2/wYABAD2/wgABAD2/woABAD2/wwABAD4//L/BAD4//T/BAD4//b/BAD4//j/BAD4//r/BAD4//z/BAD4//7/BAD4/wAABAD4/wIABAD4/wQABAD4/wYABAD4/wgABAD4/woABAD4/wwABAD4/w4ABAD6//L/BAD6//T/BAD6//b/BAD6//j/BAD6//r/BAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD6/wYABAD6/wgABAD6/woABAD6/wwABAD6/w4ABAD8//L/BAD8//T/BAD8//b/BAD8//j/BAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD8/wgABAD8/woABAD8/wwABAD8/w4ABAD+//D/BAD+//L/BAD+//T/BAD+//b/BAD+//j/BAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD+/wgABAD+/woABAD+/wwABAD+/w4ABAD+/xAABAAAAPD/BAAAAPL/BAAAAPT/BAAAAPb/BAAAAPj/BAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAAAAAgABAAAAAoABAAAAAwABAAAAA4ABAAAABAABAACAPD/BAACAPL/BAACAPT/BAACAPb/BAACAPj/BAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAACAAgABAACAAoABAACAAwABAACAA4ABAACABAABAAEAPL/BAAEAPT/BAAEAPb/BAAEAPj/BAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAEAAgABAAEAAoABAAEAAwABAAEAA4ABAAGAPL/BAAGAPT/BAAGAPb/BAAGAPj/BAAGAPr/BAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAGAAYABAAGAAgABAAGAAoABAAGAAwABAAGAA4ABAAIAPL/BAAIAPT/BAAIAPb/BAAIAPj/BAAIAPr/BAAIAPz/BAAIAP7/BAAIAAAABAAIAAIABAAIAAQABAAIAAYABAAIAAgABAAIAAoABAAIAAwABAAIAA4ABAAKAPT/BAAKAPb/BAAKAPj/BAAKAPr/BAAKAPz/BAAKAP7/BAAKAAAABAAKAAIABAAKAAQABAAKAAYABAAKAAgABAAKAAoABAAKAAwABAAMAPb/BAAMAPj/BAAMAPr/BAAMAPz/BAAMAP7/BAAMAAAABAAMAAIABAAMAAQABAAMAAYABAAMAAgABAAMAAoABAAOAPj/BAAOAPr/BAAOAPz/BAAOAP7/BAAOAAAABAAOAAIABAAOAAQABAAOAAYABAAOAAgABAAQAP7/BAAQAAAABAAQAAIABQDx//v/BQDx//3/BQDx////BQDx/wEABQDx/wMABQDx/wUABQDz//f/BQDz//n/BQDz//v/BQDz//3/BQDz////BQDz/wEABQDz/wMABQDz/wUABQDz/wcABQDz/wkABQD1//X/BQD1//f/BQD1//n/BQD1//v/BQD1//3/BQD1////BQD1/wEABQD1/wMABQD1/wUABQD1/wcABQD1/wkABQD1/wsABQD3//P/BQD3//X/BQD3//f/BQD3//n/BQD3//v/BQD3//3/BQD3////BQD3/wEABQD3/wMABQD3/wUABQD3/wcABQD3/wkABQD3/wsABQD3/w0ABQD5//P/BQD5//X/BQD5//f/BQD5//n/BQD5//v/BQD5//3/BQD5////BQD5/wEABQD5/wMABQD5/wUABQD5/wcABQD5/wkABQD5/wsABQD5/w0ABQD7//H/BQD7//P/BQD7//X/BQD7//f/BQD7//n/BQD7//v/BQD7//3/BQD7////BQD7/wEABQD7/wMABQD7/wUABQD7/wcABQD7/wkABQD7/wsABQD7/w0ABQD7/w8ABQD9//H/BQD9//P/BQD9//X/BQD9//f/BQD9//n/BQD9//v/BQD9//3/BQD9////BQD9/wEABQD9/wMABQD9/wUABQD9/wcABQD9/wkABQD9/wsABQD9/w0ABQD9/w8ABQD///H/BQD///P/BQD///X/BQD///f/BQD///n/BQD///v/BQD///3/BQD/////BQD//wEABQD//wMABQD//wUABQD//wcABQD//wkABQD//wsABQD//w0ABQD//w8ABQABAPH/BQABAPP/BQABAPX/BQABAPf/BQABAPn/BQABAPv/BQABAP3/BQABAP//BQABAAEABQABAAMABQABAAUABQABAAcABQABAAkABQABAAsABQABAA0ABQABAA8ABQADAPH/BQADAPP/BQADAPX/BQADAPf/BQADAPn/BQADAPv/BQADAP3/BQADAP//BQADAAEABQADAAMABQADAAUABQADAAcABQADAAkABQADAAsABQADAA0ABQADAA8ABQAFAPH/BQAFAPP/BQAFAPX/BQAFAPf/BQAFAPn/BQAFAPv/BQAFAP3/BQAFAP//BQAFAAEABQAFAAMABQAFAAUABQAFAAcABQAFAAkABQAFAAsABQAFAA0ABQAFAA8ABQAHAPP/BQAHAPX/BQAHAPf/BQAHAPn/BQAHAPv/BQAHAP3/BQAHAP//BQAHAAEABQAHAAMABQAHAAUABQAHAAcABQAHAAkABQAHAAsABQAHAA0ABQAJAPP/BQAJAPX/BQAJAPf/BQAJAPn/BQAJAPv/BQAJAP3/BQAJAP//BQAJAAEABQAJAAMABQAJAAUABQAJAAcABQAJAAkABQAJAAsABQAJAA0ABQALAPX/BQALAPf/BQALAPn/BQALAPv/BQALAP3/BQALAP//BQALAAEABQALAAMABQALAAUABQALAAcABQALAAkABQALAAsABQANAPf/BQANAPn/BQANAPv/BQANAP3/BQANAP//BQANAAEABQANAAMABQANAAUABQANAAcABQANAAkABQAPAPv/BQAPAP3/BQAPAP//BQAPAAEABQAPAAMABQAPAAUABgDy//r/BgDy//z/BgDy//7/BgDy/wAABgDy/wIABgDy/wQABgDy/wYABgD0//b/BgD0//j/BgD0//r/BgD0//z/BgD0//7/BgD0/wAABgD0/wIABgD0/wQABgD0/wYABgD0/wgABgD0/woABgD2//T/BgD2//b/BgD2//j/BgD2//r/BgD2//z/BgD2//7/BgD2/wAABgD2/wIABgD2/wQABgD2/wYABgD2/wgABgD2/woABgD2/wwABgD4//T/BgD4//b/BgD4//j/BgD4//r/BgD4//z/BgD4//7/BgD4/wAABgD4/wIABgD4/wQABgD4/wYABgD4/wgABgD4/woABgD4/wwABgD6//L/BgD6//T/BgD6//b/BgD6//j/BgD6//r/BgD6//z/BgD6//7/BgD6/wAABgD6/wIABgD6/wQABgD6/wYABgD6/wgABgD6/woABgD6/wwABgD6/w4ABgD8//L/BgD8//T/BgD8//b/BgD8//j/BgD8//r/BgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD8/wYABgD8/wgABgD8/woABgD8/wwABgD8/w4ABgD+//L/BgD+//T/BgD+//b/BgD+//j/BgD+//r/BgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD+/wYABgD+/wgABgD+/woABgD+/wwABgD+/w4ABgAAAPL/BgAAAPT/BgAAAPb/BgAAAPj/BgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgAAAAgABgAAAAoABgAAAAwABgAAAA4ABgACAPL/BgACAPT/BgACAPb/BgACAPj/BgACAPr/BgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgACAAYABgACAAgABgACAAoABgACAAwABgACAA4ABgAEAPL/BgAEAPT/BgAEAPb/BgAEAPj/BgAEAPr/BgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAEAAYABgAEAAgABgAEAAoABgAEAAwABgAEAA4ABgAGAPL/BgAGAPT/BgAGAPb/BgAGAPj/BgAGAPr/BgAGAPz/BgAGAP7/BgAGAAAABgAGAAIABgAGAAQABgAGAAYABgAGAAgABgAGAAoABgAGAAwABgAGAA4ABgAIAPT/BgAIAPb/BgAIAPj/BgAIAPr/BgAIAPz/BgAIAP7/BgAIAAAABgAIAAIABgAIAAQABgAIAAYABgAIAAgABgAIAAoABgAIAAwABgAKAPT/BgAKAPb/BgAKAPj/BgAKAPr/BgAKAPz/BgAKAP7/BgAKAAAABgAKAAIABgAKAAQABgAKAAYABgAKAAgABgAKAAoABgAKAAwABgAMAPb/BgAMAPj/BgAMAPr/BgAMAPz/BgAMAP7/BgAMAAAABgAMAAIABgAMAAQABgAMAAYABgAMAAgABgAMAAoABgAOAPr/BgAOAPz/BgAOAP7/BgAOAAAABgAOAAIABgAOAAQABgAOAAYABwDx//3/BwDx////BwDx/wEABwDx/wMABwDz//n/BwDz//v/BwDz//3/BwDz////BwDz/wEABwDz/wMABwDz/wUABwDz/wcABwD1//f/BwD1//n/BwD1//v/BwD1//3/BwD1////BwD1/wEABwD1/wMABwD1/wUABwD1/wcABwD1/wkABwD3//X/BwD3//f/BwD3//n/BwD3//v/BwD3//3/BwD3////BwD3/wEABwD3/wMABwD3/wUABwD3/wcABwD3/wkABwD3/wsABwD5//P/BwD5//X/BwD5//f/BwD5//n/BwD5//v/BwD5//3/BwD5////BwD5/wEABwD5/wMABwD5/wUABwD5/wcABwD5/wkABwD5/wsABwD5/w0ABwD7//P/BwD7//X/BwD7//f/BwD7//n/BwD7//v/BwD7//3/BwD7////BwD7/wEABwD7/wMABwD7/wUABwD7/wcABwD7/wkABwD7/wsABwD7/w0ABwD9//H/BwD9//P/BwD9//X/BwD9//f/BwD9//n/BwD9//v/BwD9//3/BwD9////BwD9/wEABwD9/wMABwD9/wUABwD9/wcABwD9/wkABwD9/wsABwD9/w0ABwD9/w8ABwD///H/BwD///P/BwD///X/BwD///f/BwD///n/BwD///v/BwD///3/BwD/////BwD//wEABwD//wMABwD//wUABwD//wcABwD//wkABwD//wsABwD//w0ABwD//w8ABwABAPH/BwABAPP/BwABAPX/BwABAPf/BwABAPn/BwABAPv/BwABAP3/BwABAP//BwABAAEABwABAAMABwABAAUABwABAAcABwABAAkABwABAAsABwABAA0ABwABAA8ABwADAPH/BwADAPP/BwADAPX/BwADAPf/BwADAPn/BwADAPv/BwADAP3/BwADAP//BwADAAEABwADAAMABwADAAUABwADAAcABwADAAkABwADAAsABwADAA0ABwADAA8ABwAFAPP/BwAFAPX/BwAFAPf/BwAFAPn/BwAFAPv/BwAFAP3/BwAFAP//BwAFAAEABwAFAAMABwAFAAUABwAFAAcABwAFAAkABwAFAAsABwAFAA0ABwAHAPP/BwAHAPX/BwAHAPf/BwAHAPn/BwAHAPv/BwAHAP3/BwAHAP//BwAHAAEABwAHAAMABwAHAAUABwAHAAcABwAHAAkABwAHAAsABwAHAA0ABwAJAPX/BwAJAPf/BwAJAPn/BwAJAPv/BwAJAP3/BwAJAP//BwAJAAEABwAJAAMABwAJAAUABwAJAAcABwAJAAkABwAJAAsABwALAPf/BwALAPn/BwALAPv/BwALAP3/BwALAP//BwALAAEABwALAAMABwALAAUABwALAAcABwALAAkABwANAPn/BwANAPv/BwANAP3/BwANAP//BwANAAEABwANAAMABwANAAUABwANAAcABwAPAP3/BwAPAP//BwAPAAEABwAPAAMACADy//z/CADy//7/CADy/wAACADy/wIACADy/wQACAD0//j/CAD0//r/CAD0//z/CAD0//7/CAD0/wAACAD0/wIACAD0/wQACAD0/wYACAD0/wgACAD2//b/CAD2//j/CAD2//r/CAD2//z/CAD2//7/CAD2/wAACAD2/wIACAD2/wQACAD2/wYACAD2/wgACAD2/woACAD4//T/CAD4//b/CAD4//j/CAD4//r/CAD4//z/CAD4//7/CAD4/wAACAD4/wIACAD4/wQACAD4/wYACAD4/wgACAD4/woACAD4/wwACAD6//T/CAD6//b/CAD6//j/CAD6//r/CAD6//z/CAD6//7/CAD6/wAACAD6/wIACAD6/wQACAD6/wYACAD6/wgACAD6/woACAD6/wwACAD8//L/CAD8//T/CAD8//b/CAD8//j/CAD8//r/CAD8//z/CAD8//7/CAD8/wAACAD8/wIACAD8/wQACAD8/wYACAD8/wgACAD8/woACAD8/wwACAD8/w4ACAD+//L/CAD+//T/CAD+//b/CAD+//j/CAD+//r/CAD+//z/CAD+//7/CAD+/wAACAD+/wIACAD+/wQACAD+/wYACAD+/wgACAD+/woACAD+/wwACAD+/w4ACAAAAPL/CAAAAPT/CAAAAPb/CAAAAPj/CAAAAPr/CAAAAPz/CAAAAP7/CAAAAAAACAAAAAIACAAAAAQACAAAAAYACAAAAAgACAAAAAoACAAAAAwACAAAAA4ACAACAPL/CAACAPT/CAACAPb/CAACAPj/CAACAPr/CAACAPz/CAACAP7/CAACAAAACAACAAIACAACAAQACAACAAYACAACAAgACAACAAoACAACAAwACAACAA4ACAAEAPL/CAAEAPT/CAAEAPb/CAAEAPj/CAAEAPr/CAAEAPz/CAAEAP7/CAAEAAAACAAEAAIACAAEAAQACAAEAAYACAAEAAgACAAEAAoACAAEAAwACAAEAA4ACAAGAPT/CAAGAPb/CAAGAPj/CAAGAPr/CAAGAPz/CAAGAP7/CAAGAAAACAAGAAIACAAGAAQACAAGAAYACAAGAAgACAAGAAoACAAGAAwACAAIAPT/CAAIAPb/CAAIAPj/CAAIAPr/CAAIAPz/CAAIAP7/CAAIAAAACAAIAAIACAAIAAQACAAIAAYACAAIAAgACAAIAAoACAAIAAwACAAKAPb/CAAKAPj/CAAKAPr/CAAKAPz/CAAKAP7/CAAKAAAACAAKAAIACAAKAAQACAAKAAYACAAKAAgACAAKAAoACAAMAPj/CAAMAPr/CAAMAPz/CAAMAP7/CAAMAAAACAAMAAIACAAMAAQACAAMAAYACAAMAAgACAAOAPz/CAAOAP7/CAAOAAAACAAOAAIACAAOAAQACQDz//v/CQDz//3/CQDz////CQDz/wEACQDz/wMACQDz/wUACQD1//f/CQD1//n/CQD1//v/CQD1//3/CQD1////CQD1/wEACQD1/wMACQD1/wUACQD1/wcACQD1/wkACQD3//X/CQD3//f/CQD3//n/CQD3//v/CQD3//3/CQD3////CQD3/wEACQD3/wMACQD3/wUACQD3/wcACQD3/wkACQD3/wsACQD5//X/CQD5//f/CQD5//n/CQD5//v/CQD5//3/CQD5////CQD5/wEACQD5/wMACQD5/wUACQD5/wcACQD5/wkACQD5/wsACQD7//P/CQD7//X/CQD7//f/CQD7//n/CQD7//v/CQD7//3/CQD7////CQD7/wEACQD7/wMACQD7/wUACQD7/wcACQD7/wkACQD7/wsACQD7/w0ACQD9//P/CQD9//X/CQD9//f/CQD9//n/CQD9//v/CQD9//3/CQD9////CQD9/wEACQD9/wMACQD9/wUACQD9/wcACQD9/wkACQD9/wsACQD9/w0ACQD///P/CQD///X/CQD///f/CQD///n/CQD///v/CQD///3/CQD/////CQD//wEACQD//wMACQD//wUACQD//wcACQD//wkACQD//wsACQD//w0ACQABAPP/CQABAPX/CQABAPf/CQABAPn/CQABAPv/CQABAP3/CQABAP//CQABAAEACQABAAMACQABAAUACQABAAcACQABAAkACQABAAsACQABAA0ACQADAPP/CQADAPX/CQADAPf/CQADAPn/CQADAPv/CQADAP3/CQADAP//CQADAAEACQADAAMACQADAAUACQADAAcACQADAAkACQADAAsACQADAA0ACQAFAPP/CQAFAPX/CQAFAPf/CQAFAPn/CQAFAPv/CQAFAP3/CQAFAP//CQAFAAEACQAFAAMACQAFAAUACQAFAAcACQAFAAkACQAFAAsACQAFAA0ACQAHAPX/CQAHAPf/CQAHAPn/CQAHAPv/CQAHAP3/CQAHAP//CQAHAAEACQAHAAMACQAHAAUACQAHAAcACQAHAAkACQAHAAsACQAJAPX/CQAJAPf/CQAJAPn/CQAJAPv/CQAJAP3/CQAJAP//CQAJAAEACQAJAAMACQAJAAUACQAJAAcACQAJAAkACQAJAAsACQALAPf/CQALAPn/CQALAPv/CQALAP3/CQALAP//CQALAAEACQALAAMACQALAAUACQALAAcACQALAAkACQANAPv/CQANAP3/CQANAP//CQANAAEACQANAAMACQANAAUACgD0//r/CgD0//z/CgD0//7/CgD0/wAACgD0/wIACgD0/wQACgD0/wYACgD2//j/CgD2//r/CgD2//z/CgD2//7/CgD2/wAACgD2/wIACgD2/wQACgD2/wYACgD2/wgACgD4//b/CgD4//j/CgD4//r/CgD4//z/CgD4//7/CgD4/wAACgD4/wIACgD4/wQACgD4/wYACgD4/wgACgD4/woACgD6//T/CgD6//b/CgD6//j/CgD6//r/CgD6//z/CgD6//7/CgD6/wAACgD6/wIACgD6/wQACgD6/wYACgD6/wgACgD6/woACgD6/wwACgD8//T/CgD8//b/CgD8//j/CgD8//r/CgD8//z/CgD8//7/CgD8/wAACgD8/wIACgD8/wQACgD8/wYACgD8/wgACgD8/woACgD8/wwACgD+//T/CgD+//b/CgD+//j/CgD+//r/CgD+//z/CgD+//7/CgD+/wAACgD+/wIACgD+/wQACgD+/wYACgD+/wgACgD+/woACgD+/wwACgAAAPT/CgAAAPb/CgAAAPj/CgAAAPr/CgAAAPz/CgAAAP7/CgAAAAAACgAAAAIACgAAAAQACgAAAAYACgAAAAgACgAAAAoACgAAAAwACgACAPT/CgACAPb/CgACAPj/CgACAPr/CgACAPz/CgACAP7/CgACAAAACgACAAIACgACAAQACgACAAYACgACAAgACgACAAoACgACAAwACgAEAPT/CgAEAPb/CgAEAPj/CgAEAPr/CgAEAPz/CgAEAP7/CgAEAAAACgAEAAIACgAEAAQACgAEAAYACgAEAAgACgAEAAoACgAEAAwACgAGAPT/CgAGAPb/CgAGAPj/CgAGAPr/CgAGAPz/CgAGAP7/CgAGAAAACgAGAAIACgAGAAQACgAGAAYACgAGAAgACgAGAAoACgAGAAwACgAIAPb/CgAIAPj/CgAIAPr/CgAIAPz/CgAIAP7/CgAIAAAACgAIAAIACgAIAAQACgAIAAYACgAIAAgACgAIAAoACgAKAPj/CgAKAPr/CgAKAPz/CgAKAP7/CgAKAAAACgAKAAIACgAKAAQACgAKAAYACgAKAAgACgAMAPr/CgAMAPz/CgAMAP7/CgAMAAAACgAMAAIACgAMAAQACgAMAAYACwD1//v/CwD1//3/CwD1////CwD1/wEACwD1/wMACwD1/wUACwD3//f/CwD3//n/CwD3//v/CwD3//3/CwD3////CwD3/wEACwD3/wMACwD3/wUACwD3/wcACwD3/wkACwD5//f/CwD5//n/CwD5//v/CwD5//3/CwD5////CwD5/wEACwD5/wMACwD5/wUACwD5/wcACwD5/wkACwD7//X/CwD7//f/CwD7//n/CwD7//v/CwD7//3/CwD7////CwD7/wEACwD7/wMACwD7/wUACwD7/wcACwD7/wkACwD7/wsACwD9//X/CwD9//f/CwD9//n/CwD9//v/CwD9//3/CwD9////CwD9/wEACwD9/wMACwD9/wUACwD9/wcACwD9/wkACwD9/wsACwD///X/CwD///f/CwD///n/CwD///v/CwD///3/CwD/////CwD//wEACwD//wMACwD//wUACwD//wcACwD//wkACwD//wsACwABAPX/CwABAPf/CwABAPn/CwABAPv/CwABAP3/CwABAP//CwABAAEACwABAAMACwABAAUACwABAAcACwABAAkACwABAAsACwADAPX/CwADAPf/CwADAPn/CwADAPv/CwADAP3/CwADAP//CwADAAEACwADAAMACwADAAUACwADAAcACwADAAkACwADAAsACwAFAPX/CwAFAPf/CwAFAPn/CwAFAPv/CwAFAP3/CwAFAP//CwAFAAEACwAFAAMACwAFAAUACwAFAAcACwAFAAkACwAFAAsACwAHAPf/CwAHAPn/CwAHAPv/CwAHAP3/CwAHAP//CwAHAAEACwAHAAMACwAHAAUACwAHAAcACwAHAAkACwAJAPf/CwAJAPn/CwAJAPv/CwAJAP3/CwAJAP//CwAJAAEACwAJAAMACwAJAAUACwAJAAcACwAJAAkACwALAPv/CwALAP3/CwALAP//CwALAAEACwALAAMACwALAAUADAD2//r/DAD2//z/DAD2//7/DAD2/wAADAD2/wIADAD2/wQADAD2/wYADAD4//j/DAD4//r/DAD4//z/DAD4//7/DAD4/wAADAD4/wIADAD4/wQADAD4/wYADAD4/wgADAD6//b/DAD6//j/DAD6//r/DAD6//z/DAD6//7/DAD6/wAADAD6/wIADAD6/wQADAD6/wYADAD6/wgADAD6/woADAD8//b/DAD8//j/DAD8//r/DAD8//z/DAD8//7/DAD8/wAADAD8/wIADAD8/wQADAD8/wYADAD8/wgADAD8/woADAD+//b/DAD+//j/DAD+//r/DAD+//z/DAD+//7/DAD+/wAADAD+/wIADAD+/wQADAD+/wYADAD+/wgADAD+/woADAAAAPb/DAAAAPj/DAAAAPr/DAAAAPz/DAAAAP7/DAAAAAAADAAAAAIADAAAAAQADAAAAAYADAAAAAgADAAAAAoADAACAPb/DAACAPj/DAACAPr/DAACAPz/DAACAP7/DAACAAAADAACAAIADAACAAQADAACAAYADAACAAgADAACAAoADAAEAPb/DAAEAPj/DAAEAPr/DAAEAPz/DAAEAP7/DAAEAAAADAAEAAIADAAEAAQADAAEAAYADAAEAAgADAAEAAoADAAGAPb/DAAGAPj/DAAGAPr/DAAGAPz/DAAGAP7/DAAGAAAADAAGAAIADAAGAAQADAAGAAYADAAGAAgADAAGAAoADAAIAPj/DAAIAPr/DAAIAPz/DAAIAP7/DAAIAAAADAAIAAIADAAIAAQADAAIAAYADAAIAAgADAAKAPr/DAAKAPz/DAAKAP7/DAAKAAAADAAKAAIADAAKAAQADAAKAAYADQD3//v/DQD3//3/DQD3////DQD3/wEADQD3/wMADQD3/wUADQD5//n/DQD5//v/DQD5//3/DQD5////DQD5/wEADQD5/wMADQD5/wUADQD5/wcADQD7//f/DQD7//n/DQD7//v/DQD7//3/DQD7////DQD7/wEADQD7/wMADQD7/wUADQD7/wcADQD7/wkADQD9//f/DQD9//n/DQD9//v/DQD9//3/DQD9////DQD9/wEADQD9/wMADQD9/wUADQD9/wcADQD9/wkADQD///f/DQD///n/DQD///v/DQD///3/DQD/////DQD//wEADQD//wMADQD//wUADQD//wcADQD//wkADQABAPf/DQABAPn/DQABAPv/DQABAP3/DQABAP//DQABAAEADQABAAMADQABAAUADQABAAcADQABAAkADQADAPf/DQADAPn/DQADAPv/DQADAP3/DQADAP//DQADAAEADQADAAMADQADAAUADQADAAcADQADAAkADQAFAPf/DQAFAPn/DQAFAPv/DQAFAP3/DQAFAP//DQAFAAEADQAFAAMADQAFAAUADQAFAAcADQAFAAkADQAHAPn/DQAHAPv/DQAHAP3/DQAHAP//DQAHAAEADQAHAAMADQAHAAUADQAHAAcADQAJAPv/DQAJAP3/DQAJAP//DQAJAAEADQAJAAMADQAJAAUADgD4//z/DgD4//7/DgD4/wAADgD4/wIADgD4/wQADgD6//r/DgD6//z/DgD6//7/DgD6/wAADgD6/wIADgD6/wQADgD6/wYADgD8//j/DgD8//r/DgD8//z/DgD8//7/DgD8/wAADgD8/wIADgD8/wQADgD8/wYADgD8/wgADgD+//j/DgD+//r/DgD+//z/DgD+//7/DgD+/wAADgD+/wIADgD+/wQADgD+/wYADgD+/wgADgAAAPj/DgAAAPr/DgAAAPz/DgAAAP7/DgAAAAAADgAAAAIADgAAAAQADgAAAAYADgAAAAgADgACAPj/DgACAPr/DgACAPz/DgACAP7/DgACAAAADgACAAIADgACAAQADgACAAYADgACAAgADgAEAPj/DgAEAPr/DgAEAPz/DgAEAP7/DgAEAAAADgAEAAIADgAEAAQADgAEAAYADgAEAAgADgAGAPr/DgAGAPz/DgAGAP7/DgAGAAAADgAGAAIADgAGAAQADgAGAAYADgAIAPz/DgAIAP7/DgAIAAAADgAIAAIADgAIAAQADwD5//3/DwD5////DwD5/wEADwD5/wMADwD7//v/DwD7//3/DwD7////DwD7/wEADwD7/wMADwD7/wUADwD9//n/DwD9//v/DwD9//3/DwD9////DwD9/wEADwD9/wMADwD9/wUADwD9/wcADwD///n/DwD///v/DwD///3/DwD/////DwD//wEADwD//wMADwD//wUADwD//wcADwABAPn/DwABAPv/DwABAP3/DwABAP//DwABAAEADwABAAMADwABAAUADwABAAcADwADAPn/DwADAPv/DwADAP3/DwADAP//DwADAAEADwADAAMADwADAAUADwADAAcADwAFAPv/DwAFAP3/DwAFAP//DwAFAAEADwAFAAMADwAFAAUADwAHAP3/DwAHAP//DwAHAAEADwAHAAMAEAD8//7/EAD8/wAAEAD8/wIAEAD+//z/EAD+//7/EAD+/wAAEAD+/wIAEAD+/wQAEAAAAPz/EAAAAP7/EAAAAAAAEAAAAAIAEAAAAAQAEAACAPz/EAACAP7/EAACAAAAEAACAAIAEAACAAQAEAAEAP7/EAAEAAAAEAAEAAIA\",\"F2\":\"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0nkENvnXOjZC14I2BSDANv3oMTekgQc4tw4TOdq/1jnav9Y5tw4TOaSBBzj96DE3BSDANkLXgjb51zo20nkENjDaCjZt50Q2/oaLNlxx1jaozmk3wrt/ONq/1jk0Ypo6NGKaOtq/1jnCu384qM5pN1xx1jb+hos2bedENjDaCjYw2go2bedENv6GizZccdY2qM5pN8K7fzjav9Y5NGKaOjRimjrav9Y5wrt/OKjOaTdccdY2/oaLNm3nRDYw2go20nkENvnXOjZC14I2BSDANv3oMTekgQc4tw4TOdq/1jnav9Y5tw4TOaSBBzj96DE3BSDANkLXgjb51zo20nkENh+h8TWftyg2GDdoNooCoTYXzfQ2qM5pN6SBBzjCu384wrt/OKSBBziozmk3F830NooCoTYYN2g2n7coNh+h8TWDY9M1VKERNm3nRDZC14I2j+OuNhfN9Db96DE3qM5pN6jOaTf96DE3F830No/jrjZC14I2bedENlShETaDY9M1H6HxNQuGIDZRvU82QteCNooCoTYFIMA2XHHWNlxx1jYFIMA2igKhNkLXgjZRvU82C4YgNh+h8TWs8fw1C4YgNm3nRDYYN2g2QteCNv6Gizb+hos2QteCNhg3aDZt50Q2C4YgNqzx/DUfofE1VKERNp+3KDb51zo2bedENm3nRDb51zo2n7coNlShETYfofE1g2PTNR+h8TXSeQQ2MNoKNjDaCjbSeQQ2H6HxNYNj0zWA1xc4V54kOIg8KThXniQ4gNcXOFeeJDjRf0k4aftrONiMgjiSL4c42IyCOGn7azjRf0k4V54kONVQODiM73M4bHKcOHD0vjhugtk4vZ7jOG6C2Thw9L44bHKcOIzvczjVUDg41VA4ONiMgjgKMLc4pR36OJLyITlPyUA5uB1NOU/JQDmS8iE5pR36OAowtzjYjII41VA4OFeeJDiM73M4CjC3OI8YCjm4HU05KQ2ROcyDuTkFEMs5zIO5OSkNkTm4HU05jxgKOQowtziM73M4V54kONF/SThscpw4pR36OLgdTTnsPao5TooJOmn3RjqznmU6afdGOk6KCTrsPao5uB1NOaUd+jhscpw40X9JOIDXFzhp+2s4cPS+OJLyITkpDZE5TooJOqFBhjpvye86/oMZO2/J7zqhQYY6TooJOikNkTmS8iE5cPS+OGn7aziA1xc4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pIEHOMK7fzjCu384pIEHOKjOaTcXzfQ2igKhNhg3aDaftyg2H6HxNdX15jULhiA2GXZbNhpxlTZccdY2/egxN4O/pzekgQc4pIEHOIO/pzf96DE3XHHWNhpxlTYZdls2C4YgNtX15jWDY9M1VKERNm3nRDZC14I2j+OuNhfN9Db96DE3qM5pN6jOaTf96DE3F830No/jrjZC14I2bedENlShETaDY9M1rPH8NZ+3KDYZdls2/oaLNo/jrjZccdY2F830NhfN9DZccdY2j+OuNv6GizYZdls2n7coNqzx/DWDY9M1MNoKNq14MTYZdls2QteCNhpxlTaKAqE2igKhNhpxlTZC14I2GXZbNq14MTYw2go2g2PTNbPk3DUw2go2n7coNm3nRDYZdls2GDdoNhg3aDYZdls2bedENp+3KDYw2go2s+TcNYNj0zWs8fw1VKERNguGIDaftyg2n7coNguGIDZUoRE2rPH8NYNj0zWDY9M11fXmNR+h8TUfofE11fXmNYNj0zXDLyA41VA4ONF/SThhw0840X9JONVQODjDLyA4QekTOJTKPThp+2s41RKMOGxynDj6jKI4bHKcONUSjDhp+2s4lMo9OEHpEzhB6RM40X9JOJIvhziR4684boLZOKUd+jhzVAM5pR36OG6C2TiR4684ki+HONF/SThB6RM4lMo9OJIvhzhw9L44c1QDOTtTKzm4HU05irZaObgdTTk7Uys5c1QDOXD0vjiSL4c4lMo9OMMvIDhp+2s4keOvOHNUAzlPyUA5qpSGOew9qjnMg7k57D2qOaqUhjlPyUA5c1QDOZHjrzhp+2s4wy8gONVQODjVEow4boLZODtTKzmqlIY5BRDLOU6KCTqWNho6TooJOgUQyzmqlIY5O1MrOW6C2TjVEow41VA4ONF/SThscpw4pR36OLgdTTnsPao5TooJOmn3RjqznmU6afdGOk6KCTrsPao5uB1NOaUd+jhscpw40X9JOGHDTzj6jKI4c1QDOYq2WjnMg7k5ljYaOrOeZTqhQYY6s55lOpY2GjrMg7k5irZaOXNUAzn6jKI4YcNPONF/SThscpw4pR36OLgdTTnsPao5TooJOmn3RjqznmU6afdGOk6KCTrsPao5uB1NOaUd+jhscpw40X9JONVQODjVEow4boLZODtTKzmqlIY5BRDLOU6KCTqWNho6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\",\"U_re\":\"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dL0VO9qT9zqSpQM7gTYgOwF4PTtA0Fc7rIxqO0FjcTusjGo7QNBXOwF4PTuBNiA7kqUDO+RtBjsZ8yM7RFFCO0HIXTtBY3E7EY94O0FjcTtByF07RFFCOxnzIzvkbQY7kqUDO4E2IDsBeD07QNBXO6yMajtBY3E7rIxqO0DQVzsBeD07gTYgO5KlAzvak/c6dL0VO8n/LzsRXUc7QNBXO0HIXTtA0Fc7EV1HO8n/Lzt0vRU72pP3OjZF4DrkbQY7OJocO8n/LzsBeD07RFFCOwF4PTvJ/y87OJocO+RtBjs2ReA6izHpOuRtBjt0vRU7gTYgOxnzIzuBNiA7dL0VO+RtBjuLMek6NkXgOtqT9zqSpQM75G0GO5KlAzvak/c6NkXgOtmmvLln+Mm5CxXRuQsV0bln+Mm52aa8uX8pw7kahNi5sm7ouWL28Lli9vC5sm7ouRqE2Ll/KcO52aa8uRqE2Lli9vC56agBuiuaBrormga66agBumL28LkahNi52aa8uWf4ybmybui56agBul/PC7oUTxG6FE8Rul/PC7rpqAG6sm7ouWf4ybkLFdG5YvbwuSuaBroUTxG6IiEXuiIhF7oUTxG6K5oGumL28LkLFdG5CxXRuWL28Lkrmga6FE8RuiIhF7oiIRe6FE8RuiuaBrpi9vC5CxXRuWf4ybmybui56agBul/PC7oUTxG6FE8Rul/PC7rpqAG6sm7ouWf4ybnZpry5GoTYuWL28LnpqAG6K5oGuiuaBrrpqAG6YvbwuRqE2LnZpry5fynDuRqE2Lmybui5YvbwuWL28Lmybui5GoTYuX8pw7nZpry5Z/jJuQsV0bkLFdG5Z/jJudmmvLmhq+Q6maTyOtqT9zqZpPI6oavkOqTZ7TqSpQM74EIMO6NRDzvgQgw7kqUDO6TZ7Tqhq+Q6kqUDOxZ6Ejs4mhw7gTYgOziaHDsWehI7kqUDO6Gr5DqZpPI64EIMOziaHDt20ic7CdYrO3bSJzs4mhw74EIMO5mk8jrak/c6o1EPO4E2IDsJ1is7yf8vOwnWKzuBNiA7o1EPO9qT9zqZpPI64EIMOziaHDt20ic7CdYrO3bSJzs4mhw74EIMO5mk8jqhq+Q6kqUDOxZ6Ejs4mhw7gTYgOziaHDsWehI7kqUDO6Gr5Dqk2e06kqUDO+BCDDujUQ874EIMO5KlAzuk2e06oavkOpmk8jrak/c6maTyOqGr5DoTZ7a52aa8udmmvLkTZ7a52aa8uWf4ybkLFdG5CxXRuWf4ybnZpry5E2e2uWf4ybkahNi5vkrgub5K4LkahNi5Z/jJuRNntrnZpry5CxXRub5K4Lmybui5sm7oub5K4LkLFdG52aa8udmmvLkLFdG5vkrgubJu6Lmybui5vkrguQsV0bnZpry5E2e2uWf4ybkahNi5vkrgub5K4LkahNi5Z/jJuRNntrnZpry5Z/jJuQsV0bkLFdG5Z/jJudmmvLkTZ7a52aa8udmmvLkTZ7a5oavkOosx6Tqhq+Q6oavkOpmk8jrak/c6maTyOqGr5DqLMek62pP3Ou+o/Drak/c6izHpOqGr5DqZpPI62pP3Opmk8jqhq+Q6oavkOosx6Tqhq+Q6\",\"U_im\":\"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\",\"u0_imag\":0.0038940976084273944,\"absorptive\":true,\"n_reflections\":5028,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", + "SrTiO3 (perovskite)": "{\"name\":\"SrTiO3\",\"spacegroup\":\"Pm-3m (221)\",\"pointgroup\":\"m-3m\",\"cell\":[[3.905,0.0,0.0],[0.0,3.905,0.0],[0.0,0.0,3.905]],\"recip\":[[0.2560819462227913,0.0,0.0],[0.0,0.2560819462227913,0.0],[0.0,0.0,0.2560819462227913]],\"positions_frac\":[[0.0,0.0,0.0],[0.49999999999999994,0.49999999999999994,0.49999999999999994],[0.49999999999999994,0.49999999999999994,0.0],[0.0,0.49999999999999994,0.49999999999999994],[0.49999999999999994,0.0,0.49999999999999994]],\"numbers\":[38,22,8,8,8],\"symbols\":[\"Sr\",\"Ti\",\"O\",\"O\",\"O\"],\"colors\":[[0.0,1.0,0.0],[0.749,0.761,0.78],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051]],\"radii\":[1.95,1.6,0.66,0.66,0.66],\"hkl_i16\":\"9f/8/wAA9f/9//7/9f/9////9f/9/wAA9f/9/wEA9f/9/wIA9f/+//3/9f/+//7/9f/+////9f/+/wAA9f/+/wEA9f/+/wIA9f/+/wMA9f////3/9f////7/9f//////9f///wAA9f///wEA9f///wIA9f///wMA9f8AAPz/9f8AAP3/9f8AAP7/9f8AAP//9f8AAAAA9f8AAAEA9f8AAAIA9f8AAAMA9f8AAAQA9f8BAP3/9f8BAP7/9f8BAP//9f8BAAAA9f8BAAEA9f8BAAIA9f8BAAMA9f8CAP3/9f8CAP7/9f8CAP//9f8CAAAA9f8CAAEA9f8CAAIA9f8CAAMA9f8DAP7/9f8DAP//9f8DAAAA9f8DAAEA9f8DAAIA9f8EAAAA9v/6////9v/6/wAA9v/6/wEA9v/7//3/9v/7//7/9v/7////9v/7/wAA9v/7/wEA9v/7/wIA9v/7/wMA9v/8//z/9v/8//3/9v/8//7/9v/8////9v/8/wAA9v/8/wEA9v/8/wIA9v/8/wMA9v/8/wQA9v/9//v/9v/9//z/9v/9//3/9v/9//7/9v/9////9v/9/wAA9v/9/wEA9v/9/wIA9v/9/wMA9v/9/wQA9v/9/wUA9v/+//v/9v/+//z/9v/+//3/9v/+//7/9v/+////9v/+/wAA9v/+/wEA9v/+/wIA9v/+/wMA9v/+/wQA9v/+/wUA9v////r/9v////v/9v////z/9v////3/9v////7/9v//////9v///wAA9v///wEA9v///wIA9v///wMA9v///wQA9v///wUA9v///wYA9v8AAPr/9v8AAPv/9v8AAPz/9v8AAP3/9v8AAP7/9v8AAP//9v8AAAAA9v8AAAEA9v8AAAIA9v8AAAMA9v8AAAQA9v8AAAUA9v8AAAYA9v8BAPr/9v8BAPv/9v8BAPz/9v8BAP3/9v8BAP7/9v8BAP//9v8BAAAA9v8BAAEA9v8BAAIA9v8BAAMA9v8BAAQA9v8BAAUA9v8BAAYA9v8CAPv/9v8CAPz/9v8CAP3/9v8CAP7/9v8CAP//9v8CAAAA9v8CAAEA9v8CAAIA9v8CAAMA9v8CAAQA9v8CAAUA9v8DAPv/9v8DAPz/9v8DAP3/9v8DAP7/9v8DAP//9v8DAAAA9v8DAAEA9v8DAAIA9v8DAAMA9v8DAAQA9v8DAAUA9v8EAPz/9v8EAP3/9v8EAP7/9v8EAP//9v8EAAAA9v8EAAEA9v8EAAIA9v8EAAMA9v8EAAQA9v8FAP3/9v8FAP7/9v8FAP//9v8FAAAA9v8FAAEA9v8FAAIA9v8FAAMA9v8GAP//9v8GAAAA9v8GAAEA9//5//7/9//5////9//5/wAA9//5/wEA9//5/wIA9//6//z/9//6//3/9//6//7/9//6////9//6/wAA9//6/wEA9//6/wIA9//6/wMA9//6/wQA9//7//v/9//7//z/9//7//3/9//7//7/9//7////9//7/wAA9//7/wEA9//7/wIA9//7/wMA9//7/wQA9//7/wUA9//8//r/9//8//v/9//8//z/9//8//3/9//8//7/9//8////9//8/wAA9//8/wEA9//8/wIA9//8/wMA9//8/wQA9//8/wUA9//8/wYA9//9//r/9//9//v/9//9//z/9//9//3/9//9//7/9//9////9//9/wAA9//9/wEA9//9/wIA9//9/wMA9//9/wQA9//9/wUA9//9/wYA9//+//n/9//+//r/9//+//v/9//+//z/9//+//3/9//+//7/9//+////9//+/wAA9//+/wEA9//+/wIA9//+/wMA9//+/wQA9//+/wUA9//+/wYA9//+/wcA9/////n/9/////r/9/////v/9/////z/9/////3/9/////7/9///////9////wAA9////wEA9////wIA9////wMA9////wQA9////wUA9////wYA9////wcA9/8AAPn/9/8AAPr/9/8AAPv/9/8AAPz/9/8AAP3/9/8AAP7/9/8AAP//9/8AAAAA9/8AAAEA9/8AAAIA9/8AAAMA9/8AAAQA9/8AAAUA9/8AAAYA9/8AAAcA9/8BAPn/9/8BAPr/9/8BAPv/9/8BAPz/9/8BAP3/9/8BAP7/9/8BAP//9/8BAAAA9/8BAAEA9/8BAAIA9/8BAAMA9/8BAAQA9/8BAAUA9/8BAAYA9/8BAAcA9/8CAPn/9/8CAPr/9/8CAPv/9/8CAPz/9/8CAP3/9/8CAP7/9/8CAP//9/8CAAAA9/8CAAEA9/8CAAIA9/8CAAMA9/8CAAQA9/8CAAUA9/8CAAYA9/8CAAcA9/8DAPr/9/8DAPv/9/8DAPz/9/8DAP3/9/8DAP7/9/8DAP//9/8DAAAA9/8DAAEA9/8DAAIA9/8DAAMA9/8DAAQA9/8DAAUA9/8DAAYA9/8EAPr/9/8EAPv/9/8EAPz/9/8EAP3/9/8EAP7/9/8EAP//9/8EAAAA9/8EAAEA9/8EAAIA9/8EAAMA9/8EAAQA9/8EAAUA9/8EAAYA9/8FAPv/9/8FAPz/9/8FAP3/9/8FAP7/9/8FAP//9/8FAAAA9/8FAAEA9/8FAAIA9/8FAAMA9/8FAAQA9/8FAAUA9/8GAPz/9/8GAP3/9/8GAP7/9/8GAP//9/8GAAAA9/8GAAEA9/8GAAIA9/8GAAMA9/8GAAQA9/8HAP7/9/8HAP//9/8HAAAA9/8HAAEA9/8HAAIA+P/4//3/+P/4//7/+P/4////+P/4/wAA+P/4/wEA+P/4/wIA+P/4/wMA+P/5//z/+P/5//3/+P/5//7/+P/5////+P/5/wAA+P/5/wEA+P/5/wIA+P/5/wMA+P/5/wQA+P/6//r/+P/6//v/+P/6//z/+P/6//3/+P/6//7/+P/6////+P/6/wAA+P/6/wEA+P/6/wIA+P/6/wMA+P/6/wQA+P/6/wUA+P/6/wYA+P/7//r/+P/7//v/+P/7//z/+P/7//3/+P/7//7/+P/7////+P/7/wAA+P/7/wEA+P/7/wIA+P/7/wMA+P/7/wQA+P/7/wUA+P/7/wYA+P/8//n/+P/8//r/+P/8//v/+P/8//z/+P/8//3/+P/8//7/+P/8////+P/8/wAA+P/8/wEA+P/8/wIA+P/8/wMA+P/8/wQA+P/8/wUA+P/8/wYA+P/8/wcA+P/9//j/+P/9//n/+P/9//r/+P/9//v/+P/9//z/+P/9//3/+P/9//7/+P/9////+P/9/wAA+P/9/wEA+P/9/wIA+P/9/wMA+P/9/wQA+P/9/wUA+P/9/wYA+P/9/wcA+P/9/wgA+P/+//j/+P/+//n/+P/+//r/+P/+//v/+P/+//z/+P/+//3/+P/+//7/+P/+////+P/+/wAA+P/+/wEA+P/+/wIA+P/+/wMA+P/+/wQA+P/+/wUA+P/+/wYA+P/+/wcA+P/+/wgA+P////j/+P////n/+P////r/+P////v/+P////z/+P////3/+P////7/+P//////+P///wAA+P///wEA+P///wIA+P///wMA+P///wQA+P///wUA+P///wYA+P///wcA+P///wgA+P8AAPj/+P8AAPn/+P8AAPr/+P8AAPv/+P8AAPz/+P8AAP3/+P8AAP7/+P8AAP//+P8AAAAA+P8AAAEA+P8AAAIA+P8AAAMA+P8AAAQA+P8AAAUA+P8AAAYA+P8AAAcA+P8AAAgA+P8BAPj/+P8BAPn/+P8BAPr/+P8BAPv/+P8BAPz/+P8BAP3/+P8BAP7/+P8BAP//+P8BAAAA+P8BAAEA+P8BAAIA+P8BAAMA+P8BAAQA+P8BAAUA+P8BAAYA+P8BAAcA+P8BAAgA+P8CAPj/+P8CAPn/+P8CAPr/+P8CAPv/+P8CAPz/+P8CAP3/+P8CAP7/+P8CAP//+P8CAAAA+P8CAAEA+P8CAAIA+P8CAAMA+P8CAAQA+P8CAAUA+P8CAAYA+P8CAAcA+P8CAAgA+P8DAPj/+P8DAPn/+P8DAPr/+P8DAPv/+P8DAPz/+P8DAP3/+P8DAP7/+P8DAP//+P8DAAAA+P8DAAEA+P8DAAIA+P8DAAMA+P8DAAQA+P8DAAUA+P8DAAYA+P8DAAcA+P8DAAgA+P8EAPn/+P8EAPr/+P8EAPv/+P8EAPz/+P8EAP3/+P8EAP7/+P8EAP//+P8EAAAA+P8EAAEA+P8EAAIA+P8EAAMA+P8EAAQA+P8EAAUA+P8EAAYA+P8EAAcA+P8FAPr/+P8FAPv/+P8FAPz/+P8FAP3/+P8FAP7/+P8FAP//+P8FAAAA+P8FAAEA+P8FAAIA+P8FAAMA+P8FAAQA+P8FAAUA+P8FAAYA+P8GAPr/+P8GAPv/+P8GAPz/+P8GAP3/+P8GAP7/+P8GAP//+P8GAAAA+P8GAAEA+P8GAAIA+P8GAAMA+P8GAAQA+P8GAAUA+P8GAAYA+P8HAPz/+P8HAP3/+P8HAP7/+P8HAP//+P8HAAAA+P8HAAEA+P8HAAIA+P8HAAMA+P8HAAQA+P8IAP3/+P8IAP7/+P8IAP//+P8IAAAA+P8IAAEA+P8IAAIA+P8IAAMA+f/3//7/+f/3////+f/3/wAA+f/3/wEA+f/3/wIA+f/4//z/+f/4//3/+f/4//7/+f/4////+f/4/wAA+f/4/wEA+f/4/wIA+f/4/wMA+f/4/wQA+f/5//r/+f/5//v/+f/5//z/+f/5//3/+f/5//7/+f/5////+f/5/wAA+f/5/wEA+f/5/wIA+f/5/wMA+f/5/wQA+f/5/wUA+f/5/wYA+f/6//n/+f/6//r/+f/6//v/+f/6//z/+f/6//3/+f/6//7/+f/6////+f/6/wAA+f/6/wEA+f/6/wIA+f/6/wMA+f/6/wQA+f/6/wUA+f/6/wYA+f/6/wcA+f/7//n/+f/7//r/+f/7//v/+f/7//z/+f/7//3/+f/7//7/+f/7////+f/7/wAA+f/7/wEA+f/7/wIA+f/7/wMA+f/7/wQA+f/7/wUA+f/7/wYA+f/7/wcA+f/8//j/+f/8//n/+f/8//r/+f/8//v/+f/8//z/+f/8//3/+f/8//7/+f/8////+f/8/wAA+f/8/wEA+f/8/wIA+f/8/wMA+f/8/wQA+f/8/wUA+f/8/wYA+f/8/wcA+f/8/wgA+f/9//j/+f/9//n/+f/9//r/+f/9//v/+f/9//z/+f/9//3/+f/9//7/+f/9////+f/9/wAA+f/9/wEA+f/9/wIA+f/9/wMA+f/9/wQA+f/9/wUA+f/9/wYA+f/9/wcA+f/9/wgA+f/+//f/+f/+//j/+f/+//n/+f/+//r/+f/+//v/+f/+//z/+f/+//3/+f/+//7/+f/+////+f/+/wAA+f/+/wEA+f/+/wIA+f/+/wMA+f/+/wQA+f/+/wUA+f/+/wYA+f/+/wcA+f/+/wgA+f/+/wkA+f////f/+f////j/+f////n/+f////r/+f////v/+f////z/+f////3/+f////7/+f//////+f///wAA+f///wEA+f///wIA+f///wMA+f///wQA+f///wUA+f///wYA+f///wcA+f///wgA+f///wkA+f8AAPf/+f8AAPj/+f8AAPn/+f8AAPr/+f8AAPv/+f8AAPz/+f8AAP3/+f8AAP7/+f8AAP//+f8AAAAA+f8AAAEA+f8AAAIA+f8AAAMA+f8AAAQA+f8AAAUA+f8AAAYA+f8AAAcA+f8AAAgA+f8AAAkA+f8BAPf/+f8BAPj/+f8BAPn/+f8BAPr/+f8BAPv/+f8BAPz/+f8BAP3/+f8BAP7/+f8BAP//+f8BAAAA+f8BAAEA+f8BAAIA+f8BAAMA+f8BAAQA+f8BAAUA+f8BAAYA+f8BAAcA+f8BAAgA+f8BAAkA+f8CAPf/+f8CAPj/+f8CAPn/+f8CAPr/+f8CAPv/+f8CAPz/+f8CAP3/+f8CAP7/+f8CAP//+f8CAAAA+f8CAAEA+f8CAAIA+f8CAAMA+f8CAAQA+f8CAAUA+f8CAAYA+f8CAAcA+f8CAAgA+f8CAAkA+f8DAPj/+f8DAPn/+f8DAPr/+f8DAPv/+f8DAPz/+f8DAP3/+f8DAP7/+f8DAP//+f8DAAAA+f8DAAEA+f8DAAIA+f8DAAMA+f8DAAQA+f8DAAUA+f8DAAYA+f8DAAcA+f8DAAgA+f8EAPj/+f8EAPn/+f8EAPr/+f8EAPv/+f8EAPz/+f8EAP3/+f8EAP7/+f8EAP//+f8EAAAA+f8EAAEA+f8EAAIA+f8EAAMA+f8EAAQA+f8EAAUA+f8EAAYA+f8EAAcA+f8EAAgA+f8FAPn/+f8FAPr/+f8FAPv/+f8FAPz/+f8FAP3/+f8FAP7/+f8FAP//+f8FAAAA+f8FAAEA+f8FAAIA+f8FAAMA+f8FAAQA+f8FAAUA+f8FAAYA+f8FAAcA+f8GAPn/+f8GAPr/+f8GAPv/+f8GAPz/+f8GAP3/+f8GAP7/+f8GAP//+f8GAAAA+f8GAAEA+f8GAAIA+f8GAAMA+f8GAAQA+f8GAAUA+f8GAAYA+f8GAAcA+f8HAPr/+f8HAPv/+f8HAPz/+f8HAP3/+f8HAP7/+f8HAP//+f8HAAAA+f8HAAEA+f8HAAIA+f8HAAMA+f8HAAQA+f8HAAUA+f8HAAYA+f8IAPz/+f8IAP3/+f8IAP7/+f8IAP//+f8IAAAA+f8IAAEA+f8IAAIA+f8IAAMA+f8IAAQA+f8JAP7/+f8JAP//+f8JAAAA+f8JAAEA+f8JAAIA+v/2////+v/2/wAA+v/2/wEA+v/3//z/+v/3//3/+v/3//7/+v/3////+v/3/wAA+v/3/wEA+v/3/wIA+v/3/wMA+v/3/wQA+v/4//r/+v/4//v/+v/4//z/+v/4//3/+v/4//7/+v/4////+v/4/wAA+v/4/wEA+v/4/wIA+v/4/wMA+v/4/wQA+v/4/wUA+v/4/wYA+v/5//n/+v/5//r/+v/5//v/+v/5//z/+v/5//3/+v/5//7/+v/5////+v/5/wAA+v/5/wEA+v/5/wIA+v/5/wMA+v/5/wQA+v/5/wUA+v/5/wYA+v/5/wcA+v/6//j/+v/6//n/+v/6//r/+v/6//v/+v/6//z/+v/6//3/+v/6//7/+v/6////+v/6/wAA+v/6/wEA+v/6/wIA+v/6/wMA+v/6/wQA+v/6/wUA+v/6/wYA+v/6/wcA+v/6/wgA+v/7//j/+v/7//n/+v/7//r/+v/7//v/+v/7//z/+v/7//3/+v/7//7/+v/7////+v/7/wAA+v/7/wEA+v/7/wIA+v/7/wMA+v/7/wQA+v/7/wUA+v/7/wYA+v/7/wcA+v/7/wgA+v/8//f/+v/8//j/+v/8//n/+v/8//r/+v/8//v/+v/8//z/+v/8//3/+v/8//7/+v/8////+v/8/wAA+v/8/wEA+v/8/wIA+v/8/wMA+v/8/wQA+v/8/wUA+v/8/wYA+v/8/wcA+v/8/wgA+v/8/wkA+v/9//f/+v/9//j/+v/9//n/+v/9//r/+v/9//v/+v/9//z/+v/9//3/+v/9//7/+v/9////+v/9/wAA+v/9/wEA+v/9/wIA+v/9/wMA+v/9/wQA+v/9/wUA+v/9/wYA+v/9/wcA+v/9/wgA+v/9/wkA+v/+//f/+v/+//j/+v/+//n/+v/+//r/+v/+//v/+v/+//z/+v/+//3/+v/+//7/+v/+////+v/+/wAA+v/+/wEA+v/+/wIA+v/+/wMA+v/+/wQA+v/+/wUA+v/+/wYA+v/+/wcA+v/+/wgA+v/+/wkA+v////b/+v////f/+v////j/+v////n/+v////r/+v////v/+v////z/+v////3/+v////7/+v//////+v///wAA+v///wEA+v///wIA+v///wMA+v///wQA+v///wUA+v///wYA+v///wcA+v///wgA+v///wkA+v///woA+v8AAPb/+v8AAPf/+v8AAPj/+v8AAPn/+v8AAPr/+v8AAPv/+v8AAPz/+v8AAP3/+v8AAP7/+v8AAP//+v8AAAAA+v8AAAEA+v8AAAIA+v8AAAMA+v8AAAQA+v8AAAUA+v8AAAYA+v8AAAcA+v8AAAgA+v8AAAkA+v8AAAoA+v8BAPb/+v8BAPf/+v8BAPj/+v8BAPn/+v8BAPr/+v8BAPv/+v8BAPz/+v8BAP3/+v8BAP7/+v8BAP//+v8BAAAA+v8BAAEA+v8BAAIA+v8BAAMA+v8BAAQA+v8BAAUA+v8BAAYA+v8BAAcA+v8BAAgA+v8BAAkA+v8BAAoA+v8CAPf/+v8CAPj/+v8CAPn/+v8CAPr/+v8CAPv/+v8CAPz/+v8CAP3/+v8CAP7/+v8CAP//+v8CAAAA+v8CAAEA+v8CAAIA+v8CAAMA+v8CAAQA+v8CAAUA+v8CAAYA+v8CAAcA+v8CAAgA+v8CAAkA+v8DAPf/+v8DAPj/+v8DAPn/+v8DAPr/+v8DAPv/+v8DAPz/+v8DAP3/+v8DAP7/+v8DAP//+v8DAAAA+v8DAAEA+v8DAAIA+v8DAAMA+v8DAAQA+v8DAAUA+v8DAAYA+v8DAAcA+v8DAAgA+v8DAAkA+v8EAPf/+v8EAPj/+v8EAPn/+v8EAPr/+v8EAPv/+v8EAPz/+v8EAP3/+v8EAP7/+v8EAP//+v8EAAAA+v8EAAEA+v8EAAIA+v8EAAMA+v8EAAQA+v8EAAUA+v8EAAYA+v8EAAcA+v8EAAgA+v8EAAkA+v8FAPj/+v8FAPn/+v8FAPr/+v8FAPv/+v8FAPz/+v8FAP3/+v8FAP7/+v8FAP//+v8FAAAA+v8FAAEA+v8FAAIA+v8FAAMA+v8FAAQA+v8FAAUA+v8FAAYA+v8FAAcA+v8FAAgA+v8GAPj/+v8GAPn/+v8GAPr/+v8GAPv/+v8GAPz/+v8GAP3/+v8GAP7/+v8GAP//+v8GAAAA+v8GAAEA+v8GAAIA+v8GAAMA+v8GAAQA+v8GAAUA+v8GAAYA+v8GAAcA+v8GAAgA+v8HAPn/+v8HAPr/+v8HAPv/+v8HAPz/+v8HAP3/+v8HAP7/+v8HAP//+v8HAAAA+v8HAAEA+v8HAAIA+v8HAAMA+v8HAAQA+v8HAAUA+v8HAAYA+v8HAAcA+v8IAPr/+v8IAPv/+v8IAPz/+v8IAP3/+v8IAP7/+v8IAP//+v8IAAAA+v8IAAEA+v8IAAIA+v8IAAMA+v8IAAQA+v8IAAUA+v8IAAYA+v8JAPz/+v8JAP3/+v8JAP7/+v8JAP//+v8JAAAA+v8JAAEA+v8JAAIA+v8JAAMA+v8JAAQA+v8KAP//+v8KAAAA+v8KAAEA+//2//3/+//2//7/+//2////+//2/wAA+//2/wEA+//2/wIA+//2/wMA+//3//v/+//3//z/+//3//3/+//3//7/+//3////+//3/wAA+//3/wEA+//3/wIA+//3/wMA+//3/wQA+//3/wUA+//4//r/+//4//v/+//4//z/+//4//3/+//4//7/+//4////+//4/wAA+//4/wEA+//4/wIA+//4/wMA+//4/wQA+//4/wUA+//4/wYA+//5//n/+//5//r/+//5//v/+//5//z/+//5//3/+//5//7/+//5////+//5/wAA+//5/wEA+//5/wIA+//5/wMA+//5/wQA+//5/wUA+//5/wYA+//5/wcA+//6//j/+//6//n/+//6//r/+//6//v/+//6//z/+//6//3/+//6//7/+//6////+//6/wAA+//6/wEA+//6/wIA+//6/wMA+//6/wQA+//6/wUA+//6/wYA+//6/wcA+//6/wgA+//7//f/+//7//j/+//7//n/+//7//r/+//7//v/+//7//z/+//7//3/+//7//7/+//7////+//7/wAA+//7/wEA+//7/wIA+//7/wMA+//7/wQA+//7/wUA+//7/wYA+//7/wcA+//7/wgA+//7/wkA+//8//f/+//8//j/+//8//n/+//8//r/+//8//v/+//8//z/+//8//3/+//8//7/+//8////+//8/wAA+//8/wEA+//8/wIA+//8/wMA+//8/wQA+//8/wUA+//8/wYA+//8/wcA+//8/wgA+//8/wkA+//9//b/+//9//f/+//9//j/+//9//n/+//9//r/+//9//v/+//9//z/+//9//3/+//9//7/+//9////+//9/wAA+//9/wEA+//9/wIA+//9/wMA+//9/wQA+//9/wUA+//9/wYA+//9/wcA+//9/wgA+//9/wkA+//9/woA+//+//b/+//+//f/+//+//j/+//+//n/+//+//r/+//+//v/+//+//z/+//+//3/+//+//7/+//+////+//+/wAA+//+/wEA+//+/wIA+//+/wMA+//+/wQA+//+/wUA+//+/wYA+//+/wcA+//+/wgA+//+/wkA+//+/woA+/////b/+/////f/+/////j/+/////n/+/////r/+/////v/+/////z/+/////3/+/////7/+///////+////wAA+////wEA+////wIA+////wMA+////wQA+////wUA+////wYA+////wcA+////wgA+////wkA+////woA+/8AAPb/+/8AAPf/+/8AAPj/+/8AAPn/+/8AAPr/+/8AAPv/+/8AAPz/+/8AAP3/+/8AAP7/+/8AAP//+/8AAAAA+/8AAAEA+/8AAAIA+/8AAAMA+/8AAAQA+/8AAAUA+/8AAAYA+/8AAAcA+/8AAAgA+/8AAAkA+/8AAAoA+/8BAPb/+/8BAPf/+/8BAPj/+/8BAPn/+/8BAPr/+/8BAPv/+/8BAPz/+/8BAP3/+/8BAP7/+/8BAP//+/8BAAAA+/8BAAEA+/8BAAIA+/8BAAMA+/8BAAQA+/8BAAUA+/8BAAYA+/8BAAcA+/8BAAgA+/8BAAkA+/8BAAoA+/8CAPb/+/8CAPf/+/8CAPj/+/8CAPn/+/8CAPr/+/8CAPv/+/8CAPz/+/8CAP3/+/8CAP7/+/8CAP//+/8CAAAA+/8CAAEA+/8CAAIA+/8CAAMA+/8CAAQA+/8CAAUA+/8CAAYA+/8CAAcA+/8CAAgA+/8CAAkA+/8CAAoA+/8DAPb/+/8DAPf/+/8DAPj/+/8DAPn/+/8DAPr/+/8DAPv/+/8DAPz/+/8DAP3/+/8DAP7/+/8DAP//+/8DAAAA+/8DAAEA+/8DAAIA+/8DAAMA+/8DAAQA+/8DAAUA+/8DAAYA+/8DAAcA+/8DAAgA+/8DAAkA+/8DAAoA+/8EAPf/+/8EAPj/+/8EAPn/+/8EAPr/+/8EAPv/+/8EAPz/+/8EAP3/+/8EAP7/+/8EAP//+/8EAAAA+/8EAAEA+/8EAAIA+/8EAAMA+/8EAAQA+/8EAAUA+/8EAAYA+/8EAAcA+/8EAAgA+/8EAAkA+/8FAPf/+/8FAPj/+/8FAPn/+/8FAPr/+/8FAPv/+/8FAPz/+/8FAP3/+/8FAP7/+/8FAP//+/8FAAAA+/8FAAEA+/8FAAIA+/8FAAMA+/8FAAQA+/8FAAUA+/8FAAYA+/8FAAcA+/8FAAgA+/8FAAkA+/8GAPj/+/8GAPn/+/8GAPr/+/8GAPv/+/8GAPz/+/8GAP3/+/8GAP7/+/8GAP//+/8GAAAA+/8GAAEA+/8GAAIA+/8GAAMA+/8GAAQA+/8GAAUA+/8GAAYA+/8GAAcA+/8GAAgA+/8HAPn/+/8HAPr/+/8HAPv/+/8HAPz/+/8HAP3/+/8HAP7/+/8HAP//+/8HAAAA+/8HAAEA+/8HAAIA+/8HAAMA+/8HAAQA+/8HAAUA+/8HAAYA+/8HAAcA+/8IAPr/+/8IAPv/+/8IAPz/+/8IAP3/+/8IAP7/+/8IAP//+/8IAAAA+/8IAAEA+/8IAAIA+/8IAAMA+/8IAAQA+/8IAAUA+/8IAAYA+/8JAPv/+/8JAPz/+/8JAP3/+/8JAP7/+/8JAP//+/8JAAAA+/8JAAEA+/8JAAIA+/8JAAMA+/8JAAQA+/8JAAUA+/8KAP3/+/8KAP7/+/8KAP//+/8KAAAA+/8KAAEA+/8KAAIA+/8KAAMA/P/1/wAA/P/2//z//P/2//3//P/2//7//P/2/////P/2/wAA/P/2/wEA/P/2/wIA/P/2/wMA/P/2/wQA/P/3//r//P/3//v//P/3//z//P/3//3//P/3//7//P/3/////P/3/wAA/P/3/wEA/P/3/wIA/P/3/wMA/P/3/wQA/P/3/wUA/P/3/wYA/P/4//n//P/4//r//P/4//v//P/4//z//P/4//3//P/4//7//P/4/////P/4/wAA/P/4/wEA/P/4/wIA/P/4/wMA/P/4/wQA/P/4/wUA/P/4/wYA/P/4/wcA/P/5//j//P/5//n//P/5//r//P/5//v//P/5//z//P/5//3//P/5//7//P/5/////P/5/wAA/P/5/wEA/P/5/wIA/P/5/wMA/P/5/wQA/P/5/wUA/P/5/wYA/P/5/wcA/P/5/wgA/P/6//f//P/6//j//P/6//n//P/6//r//P/6//v//P/6//z//P/6//3//P/6//7//P/6/////P/6/wAA/P/6/wEA/P/6/wIA/P/6/wMA/P/6/wQA/P/6/wUA/P/6/wYA/P/6/wcA/P/6/wgA/P/6/wkA/P/7//f//P/7//j//P/7//n//P/7//r//P/7//v//P/7//z//P/7//3//P/7//7//P/7/////P/7/wAA/P/7/wEA/P/7/wIA/P/7/wMA/P/7/wQA/P/7/wUA/P/7/wYA/P/7/wcA/P/7/wgA/P/7/wkA/P/8//b//P/8//f//P/8//j//P/8//n//P/8//r//P/8//v//P/8//z//P/8//3//P/8//7//P/8/////P/8/wAA/P/8/wEA/P/8/wIA/P/8/wMA/P/8/wQA/P/8/wUA/P/8/wYA/P/8/wcA/P/8/wgA/P/8/wkA/P/8/woA/P/9//b//P/9//f//P/9//j//P/9//n//P/9//r//P/9//v//P/9//z//P/9//3//P/9//7//P/9/////P/9/wAA/P/9/wEA/P/9/wIA/P/9/wMA/P/9/wQA/P/9/wUA/P/9/wYA/P/9/wcA/P/9/wgA/P/9/wkA/P/9/woA/P/+//b//P/+//f//P/+//j//P/+//n//P/+//r//P/+//v//P/+//z//P/+//3//P/+//7//P/+/////P/+/wAA/P/+/wEA/P/+/wIA/P/+/wMA/P/+/wQA/P/+/wUA/P/+/wYA/P/+/wcA/P/+/wgA/P/+/wkA/P/+/woA/P////b//P////f//P////j//P////n//P////r//P////v//P////z//P////3//P////7//P///////P///wAA/P///wEA/P///wIA/P///wMA/P///wQA/P///wUA/P///wYA/P///wcA/P///wgA/P///wkA/P///woA/P8AAPX//P8AAPb//P8AAPf//P8AAPj//P8AAPn//P8AAPr//P8AAPv//P8AAPz//P8AAP3//P8AAP7//P8AAP///P8AAAAA/P8AAAEA/P8AAAIA/P8AAAMA/P8AAAQA/P8AAAUA/P8AAAYA/P8AAAcA/P8AAAgA/P8AAAkA/P8AAAoA/P8AAAsA/P8BAPb//P8BAPf//P8BAPj//P8BAPn//P8BAPr//P8BAPv//P8BAPz//P8BAP3//P8BAP7//P8BAP///P8BAAAA/P8BAAEA/P8BAAIA/P8BAAMA/P8BAAQA/P8BAAUA/P8BAAYA/P8BAAcA/P8BAAgA/P8BAAkA/P8BAAoA/P8CAPb//P8CAPf//P8CAPj//P8CAPn//P8CAPr//P8CAPv//P8CAPz//P8CAP3//P8CAP7//P8CAP///P8CAAAA/P8CAAEA/P8CAAIA/P8CAAMA/P8CAAQA/P8CAAUA/P8CAAYA/P8CAAcA/P8CAAgA/P8CAAkA/P8CAAoA/P8DAPb//P8DAPf//P8DAPj//P8DAPn//P8DAPr//P8DAPv//P8DAPz//P8DAP3//P8DAP7//P8DAP///P8DAAAA/P8DAAEA/P8DAAIA/P8DAAMA/P8DAAQA/P8DAAUA/P8DAAYA/P8DAAcA/P8DAAgA/P8DAAkA/P8DAAoA/P8EAPb//P8EAPf//P8EAPj//P8EAPn//P8EAPr//P8EAPv//P8EAPz//P8EAP3//P8EAP7//P8EAP///P8EAAAA/P8EAAEA/P8EAAIA/P8EAAMA/P8EAAQA/P8EAAUA/P8EAAYA/P8EAAcA/P8EAAgA/P8EAAkA/P8EAAoA/P8FAPf//P8FAPj//P8FAPn//P8FAPr//P8FAPv//P8FAPz//P8FAP3//P8FAP7//P8FAP///P8FAAAA/P8FAAEA/P8FAAIA/P8FAAMA/P8FAAQA/P8FAAUA/P8FAAYA/P8FAAcA/P8FAAgA/P8FAAkA/P8GAPf//P8GAPj//P8GAPn//P8GAPr//P8GAPv//P8GAPz//P8GAP3//P8GAP7//P8GAP///P8GAAAA/P8GAAEA/P8GAAIA/P8GAAMA/P8GAAQA/P8GAAUA/P8GAAYA/P8GAAcA/P8GAAgA/P8GAAkA/P8HAPj//P8HAPn//P8HAPr//P8HAPv//P8HAPz//P8HAP3//P8HAP7//P8HAP///P8HAAAA/P8HAAEA/P8HAAIA/P8HAAMA/P8HAAQA/P8HAAUA/P8HAAYA/P8HAAcA/P8HAAgA/P8IAPn//P8IAPr//P8IAPv//P8IAPz//P8IAP3//P8IAP7//P8IAP///P8IAAAA/P8IAAEA/P8IAAIA/P8IAAMA/P8IAAQA/P8IAAUA/P8IAAYA/P8IAAcA/P8JAPr//P8JAPv//P8JAPz//P8JAP3//P8JAP7//P8JAP///P8JAAAA/P8JAAEA/P8JAAIA/P8JAAMA/P8JAAQA/P8JAAUA/P8JAAYA/P8KAPz//P8KAP3//P8KAP7//P8KAP///P8KAAAA/P8KAAEA/P8KAAIA/P8KAAMA/P8KAAQA/P8LAAAA/f/1//7//f/1/////f/1/wAA/f/1/wEA/f/1/wIA/f/2//v//f/2//z//f/2//3//f/2//7//f/2/////f/2/wAA/f/2/wEA/f/2/wIA/f/2/wMA/f/2/wQA/f/2/wUA/f/3//r//f/3//v//f/3//z//f/3//3//f/3//7//f/3/////f/3/wAA/f/3/wEA/f/3/wIA/f/3/wMA/f/3/wQA/f/3/wUA/f/3/wYA/f/4//j//f/4//n//f/4//r//f/4//v//f/4//z//f/4//3//f/4//7//f/4/////f/4/wAA/f/4/wEA/f/4/wIA/f/4/wMA/f/4/wQA/f/4/wUA/f/4/wYA/f/4/wcA/f/4/wgA/f/5//j//f/5//n//f/5//r//f/5//v//f/5//z//f/5//3//f/5//7//f/5/////f/5/wAA/f/5/wEA/f/5/wIA/f/5/wMA/f/5/wQA/f/5/wUA/f/5/wYA/f/5/wcA/f/5/wgA/f/6//f//f/6//j//f/6//n//f/6//r//f/6//v//f/6//z//f/6//3//f/6//7//f/6/////f/6/wAA/f/6/wEA/f/6/wIA/f/6/wMA/f/6/wQA/f/6/wUA/f/6/wYA/f/6/wcA/f/6/wgA/f/6/wkA/f/7//b//f/7//f//f/7//j//f/7//n//f/7//r//f/7//v//f/7//z//f/7//3//f/7//7//f/7/////f/7/wAA/f/7/wEA/f/7/wIA/f/7/wMA/f/7/wQA/f/7/wUA/f/7/wYA/f/7/wcA/f/7/wgA/f/7/wkA/f/7/woA/f/8//b//f/8//f//f/8//j//f/8//n//f/8//r//f/8//v//f/8//z//f/8//3//f/8//7//f/8/////f/8/wAA/f/8/wEA/f/8/wIA/f/8/wMA/f/8/wQA/f/8/wUA/f/8/wYA/f/8/wcA/f/8/wgA/f/8/wkA/f/8/woA/f/9//b//f/9//f//f/9//j//f/9//n//f/9//r//f/9//v//f/9//z//f/9//3//f/9//7//f/9/////f/9/wAA/f/9/wEA/f/9/wIA/f/9/wMA/f/9/wQA/f/9/wUA/f/9/wYA/f/9/wcA/f/9/wgA/f/9/wkA/f/9/woA/f/+//X//f/+//b//f/+//f//f/+//j//f/+//n//f/+//r//f/+//v//f/+//z//f/+//3//f/+//7//f/+/////f/+/wAA/f/+/wEA/f/+/wIA/f/+/wMA/f/+/wQA/f/+/wUA/f/+/wYA/f/+/wcA/f/+/wgA/f/+/wkA/f/+/woA/f/+/wsA/f////X//f////b//f////f//f////j//f////n//f////r//f////v//f////z//f////3//f////7//f///////f///wAA/f///wEA/f///wIA/f///wMA/f///wQA/f///wUA/f///wYA/f///wcA/f///wgA/f///wkA/f///woA/f///wsA/f8AAPX//f8AAPb//f8AAPf//f8AAPj//f8AAPn//f8AAPr//f8AAPv//f8AAPz//f8AAP3//f8AAP7//f8AAP///f8AAAAA/f8AAAEA/f8AAAIA/f8AAAMA/f8AAAQA/f8AAAUA/f8AAAYA/f8AAAcA/f8AAAgA/f8AAAkA/f8AAAoA/f8AAAsA/f8BAPX//f8BAPb//f8BAPf//f8BAPj//f8BAPn//f8BAPr//f8BAPv//f8BAPz//f8BAP3//f8BAP7//f8BAP///f8BAAAA/f8BAAEA/f8BAAIA/f8BAAMA/f8BAAQA/f8BAAUA/f8BAAYA/f8BAAcA/f8BAAgA/f8BAAkA/f8BAAoA/f8BAAsA/f8CAPX//f8CAPb//f8CAPf//f8CAPj//f8CAPn//f8CAPr//f8CAPv//f8CAPz//f8CAP3//f8CAP7//f8CAP///f8CAAAA/f8CAAEA/f8CAAIA/f8CAAMA/f8CAAQA/f8CAAUA/f8CAAYA/f8CAAcA/f8CAAgA/f8CAAkA/f8CAAoA/f8CAAsA/f8DAPb//f8DAPf//f8DAPj//f8DAPn//f8DAPr//f8DAPv//f8DAPz//f8DAP3//f8DAP7//f8DAP///f8DAAAA/f8DAAEA/f8DAAIA/f8DAAMA/f8DAAQA/f8DAAUA/f8DAAYA/f8DAAcA/f8DAAgA/f8DAAkA/f8DAAoA/f8EAPb//f8EAPf//f8EAPj//f8EAPn//f8EAPr//f8EAPv//f8EAPz//f8EAP3//f8EAP7//f8EAP///f8EAAAA/f8EAAEA/f8EAAIA/f8EAAMA/f8EAAQA/f8EAAUA/f8EAAYA/f8EAAcA/f8EAAgA/f8EAAkA/f8EAAoA/f8FAPb//f8FAPf//f8FAPj//f8FAPn//f8FAPr//f8FAPv//f8FAPz//f8FAP3//f8FAP7//f8FAP///f8FAAAA/f8FAAEA/f8FAAIA/f8FAAMA/f8FAAQA/f8FAAUA/f8FAAYA/f8FAAcA/f8FAAgA/f8FAAkA/f8FAAoA/f8GAPf//f8GAPj//f8GAPn//f8GAPr//f8GAPv//f8GAPz//f8GAP3//f8GAP7//f8GAP///f8GAAAA/f8GAAEA/f8GAAIA/f8GAAMA/f8GAAQA/f8GAAUA/f8GAAYA/f8GAAcA/f8GAAgA/f8GAAkA/f8HAPj//f8HAPn//f8HAPr//f8HAPv//f8HAPz//f8HAP3//f8HAP7//f8HAP///f8HAAAA/f8HAAEA/f8HAAIA/f8HAAMA/f8HAAQA/f8HAAUA/f8HAAYA/f8HAAcA/f8HAAgA/f8IAPj//f8IAPn//f8IAPr//f8IAPv//f8IAPz//f8IAP3//f8IAP7//f8IAP///f8IAAAA/f8IAAEA/f8IAAIA/f8IAAMA/f8IAAQA/f8IAAUA/f8IAAYA/f8IAAcA/f8IAAgA/f8JAPr//f8JAPv//f8JAPz//f8JAP3//f8JAP7//f8JAP///f8JAAAA/f8JAAEA/f8JAAIA/f8JAAMA/f8JAAQA/f8JAAUA/f8JAAYA/f8KAPv//f8KAPz//f8KAP3//f8KAP7//f8KAP///f8KAAAA/f8KAAEA/f8KAAIA/f8KAAMA/f8KAAQA/f8KAAUA/f8LAP7//f8LAP///f8LAAAA/f8LAAEA/f8LAAIA/v/1//3//v/1//7//v/1/////v/1/wAA/v/1/wEA/v/1/wIA/v/1/wMA/v/2//v//v/2//z//v/2//3//v/2//7//v/2/////v/2/wAA/v/2/wEA/v/2/wIA/v/2/wMA/v/2/wQA/v/2/wUA/v/3//n//v/3//r//v/3//v//v/3//z//v/3//3//v/3//7//v/3/////v/3/wAA/v/3/wEA/v/3/wIA/v/3/wMA/v/3/wQA/v/3/wUA/v/3/wYA/v/3/wcA/v/4//j//v/4//n//v/4//r//v/4//v//v/4//z//v/4//3//v/4//7//v/4/////v/4/wAA/v/4/wEA/v/4/wIA/v/4/wMA/v/4/wQA/v/4/wUA/v/4/wYA/v/4/wcA/v/4/wgA/v/5//f//v/5//j//v/5//n//v/5//r//v/5//v//v/5//z//v/5//3//v/5//7//v/5/////v/5/wAA/v/5/wEA/v/5/wIA/v/5/wMA/v/5/wQA/v/5/wUA/v/5/wYA/v/5/wcA/v/5/wgA/v/5/wkA/v/6//f//v/6//j//v/6//n//v/6//r//v/6//v//v/6//z//v/6//3//v/6//7//v/6/////v/6/wAA/v/6/wEA/v/6/wIA/v/6/wMA/v/6/wQA/v/6/wUA/v/6/wYA/v/6/wcA/v/6/wgA/v/6/wkA/v/7//b//v/7//f//v/7//j//v/7//n//v/7//r//v/7//v//v/7//z//v/7//3//v/7//7//v/7/////v/7/wAA/v/7/wEA/v/7/wIA/v/7/wMA/v/7/wQA/v/7/wUA/v/7/wYA/v/7/wcA/v/7/wgA/v/7/wkA/v/7/woA/v/8//b//v/8//f//v/8//j//v/8//n//v/8//r//v/8//v//v/8//z//v/8//3//v/8//7//v/8/////v/8/wAA/v/8/wEA/v/8/wIA/v/8/wMA/v/8/wQA/v/8/wUA/v/8/wYA/v/8/wcA/v/8/wgA/v/8/wkA/v/8/woA/v/9//X//v/9//b//v/9//f//v/9//j//v/9//n//v/9//r//v/9//v//v/9//z//v/9//3//v/9//7//v/9/////v/9/wAA/v/9/wEA/v/9/wIA/v/9/wMA/v/9/wQA/v/9/wUA/v/9/wYA/v/9/wcA/v/9/wgA/v/9/wkA/v/9/woA/v/9/wsA/v/+//X//v/+//b//v/+//f//v/+//j//v/+//n//v/+//r//v/+//v//v/+//z//v/+//3//v/+//7//v/+/////v/+/wAA/v/+/wEA/v/+/wIA/v/+/wMA/v/+/wQA/v/+/wUA/v/+/wYA/v/+/wcA/v/+/wgA/v/+/wkA/v/+/woA/v/+/wsA/v////X//v////b//v////f//v////j//v////n//v////r//v////v//v////z//v////3//v////7//v///////v///wAA/v///wEA/v///wIA/v///wMA/v///wQA/v///wUA/v///wYA/v///wcA/v///wgA/v///wkA/v///woA/v///wsA/v8AAPX//v8AAPb//v8AAPf//v8AAPj//v8AAPn//v8AAPr//v8AAPv//v8AAPz//v8AAP3//v8AAP7//v8AAP///v8AAAAA/v8AAAEA/v8AAAIA/v8AAAMA/v8AAAQA/v8AAAUA/v8AAAYA/v8AAAcA/v8AAAgA/v8AAAkA/v8AAAoA/v8AAAsA/v8BAPX//v8BAPb//v8BAPf//v8BAPj//v8BAPn//v8BAPr//v8BAPv//v8BAPz//v8BAP3//v8BAP7//v8BAP///v8BAAAA/v8BAAEA/v8BAAIA/v8BAAMA/v8BAAQA/v8BAAUA/v8BAAYA/v8BAAcA/v8BAAgA/v8BAAkA/v8BAAoA/v8BAAsA/v8CAPX//v8CAPb//v8CAPf//v8CAPj//v8CAPn//v8CAPr//v8CAPv//v8CAPz//v8CAP3//v8CAP7//v8CAP///v8CAAAA/v8CAAEA/v8CAAIA/v8CAAMA/v8CAAQA/v8CAAUA/v8CAAYA/v8CAAcA/v8CAAgA/v8CAAkA/v8CAAoA/v8CAAsA/v8DAPX//v8DAPb//v8DAPf//v8DAPj//v8DAPn//v8DAPr//v8DAPv//v8DAPz//v8DAP3//v8DAP7//v8DAP///v8DAAAA/v8DAAEA/v8DAAIA/v8DAAMA/v8DAAQA/v8DAAUA/v8DAAYA/v8DAAcA/v8DAAgA/v8DAAkA/v8DAAoA/v8DAAsA/v8EAPb//v8EAPf//v8EAPj//v8EAPn//v8EAPr//v8EAPv//v8EAPz//v8EAP3//v8EAP7//v8EAP///v8EAAAA/v8EAAEA/v8EAAIA/v8EAAMA/v8EAAQA/v8EAAUA/v8EAAYA/v8EAAcA/v8EAAgA/v8EAAkA/v8EAAoA/v8FAPb//v8FAPf//v8FAPj//v8FAPn//v8FAPr//v8FAPv//v8FAPz//v8FAP3//v8FAP7//v8FAP///v8FAAAA/v8FAAEA/v8FAAIA/v8FAAMA/v8FAAQA/v8FAAUA/v8FAAYA/v8FAAcA/v8FAAgA/v8FAAkA/v8FAAoA/v8GAPf//v8GAPj//v8GAPn//v8GAPr//v8GAPv//v8GAPz//v8GAP3//v8GAP7//v8GAP///v8GAAAA/v8GAAEA/v8GAAIA/v8GAAMA/v8GAAQA/v8GAAUA/v8GAAYA/v8GAAcA/v8GAAgA/v8GAAkA/v8HAPf//v8HAPj//v8HAPn//v8HAPr//v8HAPv//v8HAPz//v8HAP3//v8HAP7//v8HAP///v8HAAAA/v8HAAEA/v8HAAIA/v8HAAMA/v8HAAQA/v8HAAUA/v8HAAYA/v8HAAcA/v8HAAgA/v8HAAkA/v8IAPj//v8IAPn//v8IAPr//v8IAPv//v8IAPz//v8IAP3//v8IAP7//v8IAP///v8IAAAA/v8IAAEA/v8IAAIA/v8IAAMA/v8IAAQA/v8IAAUA/v8IAAYA/v8IAAcA/v8IAAgA/v8JAPn//v8JAPr//v8JAPv//v8JAPz//v8JAP3//v8JAP7//v8JAP///v8JAAAA/v8JAAEA/v8JAAIA/v8JAAMA/v8JAAQA/v8JAAUA/v8JAAYA/v8JAAcA/v8KAPv//v8KAPz//v8KAP3//v8KAP7//v8KAP///v8KAAAA/v8KAAEA/v8KAAIA/v8KAAMA/v8KAAQA/v8KAAUA/v8LAP3//v8LAP7//v8LAP///v8LAAAA/v8LAAEA/v8LAAIA/v8LAAMA///1//3////1//7////1///////1/wAA///1/wEA///1/wIA///1/wMA///2//r////2//v////2//z////2//3////2//7////2///////2/wAA///2/wEA///2/wIA///2/wMA///2/wQA///2/wUA///2/wYA///3//n////3//r////3//v////3//z////3//3////3//7////3///////3/wAA///3/wEA///3/wIA///3/wMA///3/wQA///3/wUA///3/wYA///3/wcA///4//j////4//n////4//r////4//v////4//z////4//3////4//7////4///////4/wAA///4/wEA///4/wIA///4/wMA///4/wQA///4/wUA///4/wYA///4/wcA///4/wgA///5//f////5//j////5//n////5//r////5//v////5//z////5//3////5//7////5///////5/wAA///5/wEA///5/wIA///5/wMA///5/wQA///5/wUA///5/wYA///5/wcA///5/wgA///5/wkA///6//b////6//f////6//j////6//n////6//r////6//v////6//z////6//3////6//7////6///////6/wAA///6/wEA///6/wIA///6/wMA///6/wQA///6/wUA///6/wYA///6/wcA///6/wgA///6/wkA///6/woA///7//b////7//f////7//j////7//n////7//r////7//v////7//z////7//3////7//7////7///////7/wAA///7/wEA///7/wIA///7/wMA///7/wQA///7/wUA///7/wYA///7/wcA///7/wgA///7/wkA///7/woA///8//b////8//f////8//j////8//n////8//r////8//v////8//z////8//3////8//7////8///////8/wAA///8/wEA///8/wIA///8/wMA///8/wQA///8/wUA///8/wYA///8/wcA///8/wgA///8/wkA///8/woA///9//X////9//b////9//f////9//j////9//n////9//r////9//v////9//z////9//3////9//7////9///////9/wAA///9/wEA///9/wIA///9/wMA///9/wQA///9/wUA///9/wYA///9/wcA///9/wgA///9/wkA///9/woA///9/wsA///+//X////+//b////+//f////+//j////+//n////+//r////+//v////+//z////+//3////+//7////+///////+/wAA///+/wEA///+/wIA///+/wMA///+/wQA///+/wUA///+/wYA///+/wcA///+/wgA///+/wkA///+/woA///+/wsA//////X///////b///////f///////j///////n///////r///////v///////z///////3///////7//////////////wAA/////wEA/////wIA/////wMA/////wQA/////wUA/////wYA/////wcA/////wgA/////wkA/////woA/////wsA//8AAPX///8AAPb///8AAPf///8AAPj///8AAPn///8AAPr///8AAPv///8AAPz///8AAP3///8AAP7///8AAP////8AAAAA//8AAAEA//8AAAIA//8AAAMA//8AAAQA//8AAAUA//8AAAYA//8AAAcA//8AAAgA//8AAAkA//8AAAoA//8AAAsA//8BAPX///8BAPb///8BAPf///8BAPj///8BAPn///8BAPr///8BAPv///8BAPz///8BAP3///8BAP7///8BAP////8BAAAA//8BAAEA//8BAAIA//8BAAMA//8BAAQA//8BAAUA//8BAAYA//8BAAcA//8BAAgA//8BAAkA//8BAAoA//8BAAsA//8CAPX///8CAPb///8CAPf///8CAPj///8CAPn///8CAPr///8CAPv///8CAPz///8CAP3///8CAP7///8CAP////8CAAAA//8CAAEA//8CAAIA//8CAAMA//8CAAQA//8CAAUA//8CAAYA//8CAAcA//8CAAgA//8CAAkA//8CAAoA//8CAAsA//8DAPX///8DAPb///8DAPf///8DAPj///8DAPn///8DAPr///8DAPv///8DAPz///8DAP3///8DAP7///8DAP////8DAAAA//8DAAEA//8DAAIA//8DAAMA//8DAAQA//8DAAUA//8DAAYA//8DAAcA//8DAAgA//8DAAkA//8DAAoA//8DAAsA//8EAPb///8EAPf///8EAPj///8EAPn///8EAPr///8EAPv///8EAPz///8EAP3///8EAP7///8EAP////8EAAAA//8EAAEA//8EAAIA//8EAAMA//8EAAQA//8EAAUA//8EAAYA//8EAAcA//8EAAgA//8EAAkA//8EAAoA//8FAPb///8FAPf///8FAPj///8FAPn///8FAPr///8FAPv///8FAPz///8FAP3///8FAP7///8FAP////8FAAAA//8FAAEA//8FAAIA//8FAAMA//8FAAQA//8FAAUA//8FAAYA//8FAAcA//8FAAgA//8FAAkA//8FAAoA//8GAPb///8GAPf///8GAPj///8GAPn///8GAPr///8GAPv///8GAPz///8GAP3///8GAP7///8GAP////8GAAAA//8GAAEA//8GAAIA//8GAAMA//8GAAQA//8GAAUA//8GAAYA//8GAAcA//8GAAgA//8GAAkA//8GAAoA//8HAPf///8HAPj///8HAPn///8HAPr///8HAPv///8HAPz///8HAP3///8HAP7///8HAP////8HAAAA//8HAAEA//8HAAIA//8HAAMA//8HAAQA//8HAAUA//8HAAYA//8HAAcA//8HAAgA//8HAAkA//8IAPj///8IAPn///8IAPr///8IAPv///8IAPz///8IAP3///8IAP7///8IAP////8IAAAA//8IAAEA//8IAAIA//8IAAMA//8IAAQA//8IAAUA//8IAAYA//8IAAcA//8IAAgA//8JAPn///8JAPr///8JAPv///8JAPz///8JAP3///8JAP7///8JAP////8JAAAA//8JAAEA//8JAAIA//8JAAMA//8JAAQA//8JAAUA//8JAAYA//8JAAcA//8KAPr///8KAPv///8KAPz///8KAP3///8KAP7///8KAP////8KAAAA//8KAAEA//8KAAIA//8KAAMA//8KAAQA//8KAAUA//8KAAYA//8LAP3///8LAP7///8LAP////8LAAAA//8LAAEA//8LAAIA//8LAAMAAAD1//z/AAD1//3/AAD1//7/AAD1////AAD1/wAAAAD1/wEAAAD1/wIAAAD1/wMAAAD1/wQAAAD2//r/AAD2//v/AAD2//z/AAD2//3/AAD2//7/AAD2////AAD2/wAAAAD2/wEAAAD2/wIAAAD2/wMAAAD2/wQAAAD2/wUAAAD2/wYAAAD3//n/AAD3//r/AAD3//v/AAD3//z/AAD3//3/AAD3//7/AAD3////AAD3/wAAAAD3/wEAAAD3/wIAAAD3/wMAAAD3/wQAAAD3/wUAAAD3/wYAAAD3/wcAAAD4//j/AAD4//n/AAD4//r/AAD4//v/AAD4//z/AAD4//3/AAD4//7/AAD4////AAD4/wAAAAD4/wEAAAD4/wIAAAD4/wMAAAD4/wQAAAD4/wUAAAD4/wYAAAD4/wcAAAD4/wgAAAD5//f/AAD5//j/AAD5//n/AAD5//r/AAD5//v/AAD5//z/AAD5//3/AAD5//7/AAD5////AAD5/wAAAAD5/wEAAAD5/wIAAAD5/wMAAAD5/wQAAAD5/wUAAAD5/wYAAAD5/wcAAAD5/wgAAAD5/wkAAAD6//b/AAD6//f/AAD6//j/AAD6//n/AAD6//r/AAD6//v/AAD6//z/AAD6//3/AAD6//7/AAD6////AAD6/wAAAAD6/wEAAAD6/wIAAAD6/wMAAAD6/wQAAAD6/wUAAAD6/wYAAAD6/wcAAAD6/wgAAAD6/wkAAAD6/woAAAD7//b/AAD7//f/AAD7//j/AAD7//n/AAD7//r/AAD7//v/AAD7//z/AAD7//3/AAD7//7/AAD7////AAD7/wAAAAD7/wEAAAD7/wIAAAD7/wMAAAD7/wQAAAD7/wUAAAD7/wYAAAD7/wcAAAD7/wgAAAD7/wkAAAD7/woAAAD8//X/AAD8//b/AAD8//f/AAD8//j/AAD8//n/AAD8//r/AAD8//v/AAD8//z/AAD8//3/AAD8//7/AAD8////AAD8/wAAAAD8/wEAAAD8/wIAAAD8/wMAAAD8/wQAAAD8/wUAAAD8/wYAAAD8/wcAAAD8/wgAAAD8/wkAAAD8/woAAAD8/wsAAAD9//X/AAD9//b/AAD9//f/AAD9//j/AAD9//n/AAD9//r/AAD9//v/AAD9//z/AAD9//3/AAD9//7/AAD9////AAD9/wAAAAD9/wEAAAD9/wIAAAD9/wMAAAD9/wQAAAD9/wUAAAD9/wYAAAD9/wcAAAD9/wgAAAD9/wkAAAD9/woAAAD9/wsAAAD+//X/AAD+//b/AAD+//f/AAD+//j/AAD+//n/AAD+//r/AAD+//v/AAD+//z/AAD+//3/AAD+//7/AAD+////AAD+/wAAAAD+/wEAAAD+/wIAAAD+/wMAAAD+/wQAAAD+/wUAAAD+/wYAAAD+/wcAAAD+/wgAAAD+/wkAAAD+/woAAAD+/wsAAAD///X/AAD///b/AAD///f/AAD///j/AAD///n/AAD///r/AAD///v/AAD///z/AAD///3/AAD///7/AAD/////AAD//wAAAAD//wEAAAD//wIAAAD//wMAAAD//wQAAAD//wUAAAD//wYAAAD//wcAAAD//wgAAAD//wkAAAD//woAAAD//wsAAAAAAPX/AAAAAPb/AAAAAPf/AAAAAPj/AAAAAPn/AAAAAPr/AAAAAPv/AAAAAPz/AAAAAP3/AAAAAP7/AAAAAP//AAAAAAEAAAAAAAIAAAAAAAMAAAAAAAQAAAAAAAUAAAAAAAYAAAAAAAcAAAAAAAgAAAAAAAkAAAAAAAoAAAAAAAsAAAABAPX/AAABAPb/AAABAPf/AAABAPj/AAABAPn/AAABAPr/AAABAPv/AAABAPz/AAABAP3/AAABAP7/AAABAP//AAABAAAAAAABAAEAAAABAAIAAAABAAMAAAABAAQAAAABAAUAAAABAAYAAAABAAcAAAABAAgAAAABAAkAAAABAAoAAAABAAsAAAACAPX/AAACAPb/AAACAPf/AAACAPj/AAACAPn/AAACAPr/AAACAPv/AAACAPz/AAACAP3/AAACAP7/AAACAP//AAACAAAAAAACAAEAAAACAAIAAAACAAMAAAACAAQAAAACAAUAAAACAAYAAAACAAcAAAACAAgAAAACAAkAAAACAAoAAAACAAsAAAADAPX/AAADAPb/AAADAPf/AAADAPj/AAADAPn/AAADAPr/AAADAPv/AAADAPz/AAADAP3/AAADAP7/AAADAP//AAADAAAAAAADAAEAAAADAAIAAAADAAMAAAADAAQAAAADAAUAAAADAAYAAAADAAcAAAADAAgAAAADAAkAAAADAAoAAAADAAsAAAAEAPX/AAAEAPb/AAAEAPf/AAAEAPj/AAAEAPn/AAAEAPr/AAAEAPv/AAAEAPz/AAAEAP3/AAAEAP7/AAAEAP//AAAEAAAAAAAEAAEAAAAEAAIAAAAEAAMAAAAEAAQAAAAEAAUAAAAEAAYAAAAEAAcAAAAEAAgAAAAEAAkAAAAEAAoAAAAEAAsAAAAFAPb/AAAFAPf/AAAFAPj/AAAFAPn/AAAFAPr/AAAFAPv/AAAFAPz/AAAFAP3/AAAFAP7/AAAFAP//AAAFAAAAAAAFAAEAAAAFAAIAAAAFAAMAAAAFAAQAAAAFAAUAAAAFAAYAAAAFAAcAAAAFAAgAAAAFAAkAAAAFAAoAAAAGAPb/AAAGAPf/AAAGAPj/AAAGAPn/AAAGAPr/AAAGAPv/AAAGAPz/AAAGAP3/AAAGAP7/AAAGAP//AAAGAAAAAAAGAAEAAAAGAAIAAAAGAAMAAAAGAAQAAAAGAAUAAAAGAAYAAAAGAAcAAAAGAAgAAAAGAAkAAAAGAAoAAAAHAPf/AAAHAPj/AAAHAPn/AAAHAPr/AAAHAPv/AAAHAPz/AAAHAP3/AAAHAP7/AAAHAP//AAAHAAAAAAAHAAEAAAAHAAIAAAAHAAMAAAAHAAQAAAAHAAUAAAAHAAYAAAAHAAcAAAAHAAgAAAAHAAkAAAAIAPj/AAAIAPn/AAAIAPr/AAAIAPv/AAAIAPz/AAAIAP3/AAAIAP7/AAAIAP//AAAIAAAAAAAIAAEAAAAIAAIAAAAIAAMAAAAIAAQAAAAIAAUAAAAIAAYAAAAIAAcAAAAIAAgAAAAJAPn/AAAJAPr/AAAJAPv/AAAJAPz/AAAJAP3/AAAJAP7/AAAJAP//AAAJAAAAAAAJAAEAAAAJAAIAAAAJAAMAAAAJAAQAAAAJAAUAAAAJAAYAAAAJAAcAAAAKAPr/AAAKAPv/AAAKAPz/AAAKAP3/AAAKAP7/AAAKAP//AAAKAAAAAAAKAAEAAAAKAAIAAAAKAAMAAAAKAAQAAAAKAAUAAAAKAAYAAAALAPz/AAALAP3/AAALAP7/AAALAP//AAALAAAAAAALAAEAAAALAAIAAAALAAMAAAALAAQAAQD1//3/AQD1//7/AQD1////AQD1/wAAAQD1/wEAAQD1/wIAAQD1/wMAAQD2//r/AQD2//v/AQD2//z/AQD2//3/AQD2//7/AQD2////AQD2/wAAAQD2/wEAAQD2/wIAAQD2/wMAAQD2/wQAAQD2/wUAAQD2/wYAAQD3//n/AQD3//r/AQD3//v/AQD3//z/AQD3//3/AQD3//7/AQD3////AQD3/wAAAQD3/wEAAQD3/wIAAQD3/wMAAQD3/wQAAQD3/wUAAQD3/wYAAQD3/wcAAQD4//j/AQD4//n/AQD4//r/AQD4//v/AQD4//z/AQD4//3/AQD4//7/AQD4////AQD4/wAAAQD4/wEAAQD4/wIAAQD4/wMAAQD4/wQAAQD4/wUAAQD4/wYAAQD4/wcAAQD4/wgAAQD5//f/AQD5//j/AQD5//n/AQD5//r/AQD5//v/AQD5//z/AQD5//3/AQD5//7/AQD5////AQD5/wAAAQD5/wEAAQD5/wIAAQD5/wMAAQD5/wQAAQD5/wUAAQD5/wYAAQD5/wcAAQD5/wgAAQD5/wkAAQD6//b/AQD6//f/AQD6//j/AQD6//n/AQD6//r/AQD6//v/AQD6//z/AQD6//3/AQD6//7/AQD6////AQD6/wAAAQD6/wEAAQD6/wIAAQD6/wMAAQD6/wQAAQD6/wUAAQD6/wYAAQD6/wcAAQD6/wgAAQD6/wkAAQD6/woAAQD7//b/AQD7//f/AQD7//j/AQD7//n/AQD7//r/AQD7//v/AQD7//z/AQD7//3/AQD7//7/AQD7////AQD7/wAAAQD7/wEAAQD7/wIAAQD7/wMAAQD7/wQAAQD7/wUAAQD7/wYAAQD7/wcAAQD7/wgAAQD7/wkAAQD7/woAAQD8//b/AQD8//f/AQD8//j/AQD8//n/AQD8//r/AQD8//v/AQD8//z/AQD8//3/AQD8//7/AQD8////AQD8/wAAAQD8/wEAAQD8/wIAAQD8/wMAAQD8/wQAAQD8/wUAAQD8/wYAAQD8/wcAAQD8/wgAAQD8/wkAAQD8/woAAQD9//X/AQD9//b/AQD9//f/AQD9//j/AQD9//n/AQD9//r/AQD9//v/AQD9//z/AQD9//3/AQD9//7/AQD9////AQD9/wAAAQD9/wEAAQD9/wIAAQD9/wMAAQD9/wQAAQD9/wUAAQD9/wYAAQD9/wcAAQD9/wgAAQD9/wkAAQD9/woAAQD9/wsAAQD+//X/AQD+//b/AQD+//f/AQD+//j/AQD+//n/AQD+//r/AQD+//v/AQD+//z/AQD+//3/AQD+//7/AQD+////AQD+/wAAAQD+/wEAAQD+/wIAAQD+/wMAAQD+/wQAAQD+/wUAAQD+/wYAAQD+/wcAAQD+/wgAAQD+/wkAAQD+/woAAQD+/wsAAQD///X/AQD///b/AQD///f/AQD///j/AQD///n/AQD///r/AQD///v/AQD///z/AQD///3/AQD///7/AQD/////AQD//wAAAQD//wEAAQD//wIAAQD//wMAAQD//wQAAQD//wUAAQD//wYAAQD//wcAAQD//wgAAQD//wkAAQD//woAAQD//wsAAQAAAPX/AQAAAPb/AQAAAPf/AQAAAPj/AQAAAPn/AQAAAPr/AQAAAPv/AQAAAPz/AQAAAP3/AQAAAP7/AQAAAP//AQAAAAAAAQAAAAEAAQAAAAIAAQAAAAMAAQAAAAQAAQAAAAUAAQAAAAYAAQAAAAcAAQAAAAgAAQAAAAkAAQAAAAoAAQAAAAsAAQABAPX/AQABAPb/AQABAPf/AQABAPj/AQABAPn/AQABAPr/AQABAPv/AQABAPz/AQABAP3/AQABAP7/AQABAP//AQABAAAAAQABAAEAAQABAAIAAQABAAMAAQABAAQAAQABAAUAAQABAAYAAQABAAcAAQABAAgAAQABAAkAAQABAAoAAQABAAsAAQACAPX/AQACAPb/AQACAPf/AQACAPj/AQACAPn/AQACAPr/AQACAPv/AQACAPz/AQACAP3/AQACAP7/AQACAP//AQACAAAAAQACAAEAAQACAAIAAQACAAMAAQACAAQAAQACAAUAAQACAAYAAQACAAcAAQACAAgAAQACAAkAAQACAAoAAQACAAsAAQADAPX/AQADAPb/AQADAPf/AQADAPj/AQADAPn/AQADAPr/AQADAPv/AQADAPz/AQADAP3/AQADAP7/AQADAP//AQADAAAAAQADAAEAAQADAAIAAQADAAMAAQADAAQAAQADAAUAAQADAAYAAQADAAcAAQADAAgAAQADAAkAAQADAAoAAQADAAsAAQAEAPb/AQAEAPf/AQAEAPj/AQAEAPn/AQAEAPr/AQAEAPv/AQAEAPz/AQAEAP3/AQAEAP7/AQAEAP//AQAEAAAAAQAEAAEAAQAEAAIAAQAEAAMAAQAEAAQAAQAEAAUAAQAEAAYAAQAEAAcAAQAEAAgAAQAEAAkAAQAEAAoAAQAFAPb/AQAFAPf/AQAFAPj/AQAFAPn/AQAFAPr/AQAFAPv/AQAFAPz/AQAFAP3/AQAFAP7/AQAFAP//AQAFAAAAAQAFAAEAAQAFAAIAAQAFAAMAAQAFAAQAAQAFAAUAAQAFAAYAAQAFAAcAAQAFAAgAAQAFAAkAAQAFAAoAAQAGAPb/AQAGAPf/AQAGAPj/AQAGAPn/AQAGAPr/AQAGAPv/AQAGAPz/AQAGAP3/AQAGAP7/AQAGAP//AQAGAAAAAQAGAAEAAQAGAAIAAQAGAAMAAQAGAAQAAQAGAAUAAQAGAAYAAQAGAAcAAQAGAAgAAQAGAAkAAQAGAAoAAQAHAPf/AQAHAPj/AQAHAPn/AQAHAPr/AQAHAPv/AQAHAPz/AQAHAP3/AQAHAP7/AQAHAP//AQAHAAAAAQAHAAEAAQAHAAIAAQAHAAMAAQAHAAQAAQAHAAUAAQAHAAYAAQAHAAcAAQAHAAgAAQAHAAkAAQAIAPj/AQAIAPn/AQAIAPr/AQAIAPv/AQAIAPz/AQAIAP3/AQAIAP7/AQAIAP//AQAIAAAAAQAIAAEAAQAIAAIAAQAIAAMAAQAIAAQAAQAIAAUAAQAIAAYAAQAIAAcAAQAIAAgAAQAJAPn/AQAJAPr/AQAJAPv/AQAJAPz/AQAJAP3/AQAJAP7/AQAJAP//AQAJAAAAAQAJAAEAAQAJAAIAAQAJAAMAAQAJAAQAAQAJAAUAAQAJAAYAAQAJAAcAAQAKAPr/AQAKAPv/AQAKAPz/AQAKAP3/AQAKAP7/AQAKAP//AQAKAAAAAQAKAAEAAQAKAAIAAQAKAAMAAQAKAAQAAQAKAAUAAQAKAAYAAQALAP3/AQALAP7/AQALAP//AQALAAAAAQALAAEAAQALAAIAAQALAAMAAgD1//3/AgD1//7/AgD1////AgD1/wAAAgD1/wEAAgD1/wIAAgD1/wMAAgD2//v/AgD2//z/AgD2//3/AgD2//7/AgD2////AgD2/wAAAgD2/wEAAgD2/wIAAgD2/wMAAgD2/wQAAgD2/wUAAgD3//n/AgD3//r/AgD3//v/AgD3//z/AgD3//3/AgD3//7/AgD3////AgD3/wAAAgD3/wEAAgD3/wIAAgD3/wMAAgD3/wQAAgD3/wUAAgD3/wYAAgD3/wcAAgD4//j/AgD4//n/AgD4//r/AgD4//v/AgD4//z/AgD4//3/AgD4//7/AgD4////AgD4/wAAAgD4/wEAAgD4/wIAAgD4/wMAAgD4/wQAAgD4/wUAAgD4/wYAAgD4/wcAAgD4/wgAAgD5//f/AgD5//j/AgD5//n/AgD5//r/AgD5//v/AgD5//z/AgD5//3/AgD5//7/AgD5////AgD5/wAAAgD5/wEAAgD5/wIAAgD5/wMAAgD5/wQAAgD5/wUAAgD5/wYAAgD5/wcAAgD5/wgAAgD5/wkAAgD6//f/AgD6//j/AgD6//n/AgD6//r/AgD6//v/AgD6//z/AgD6//3/AgD6//7/AgD6////AgD6/wAAAgD6/wEAAgD6/wIAAgD6/wMAAgD6/wQAAgD6/wUAAgD6/wYAAgD6/wcAAgD6/wgAAgD6/wkAAgD7//b/AgD7//f/AgD7//j/AgD7//n/AgD7//r/AgD7//v/AgD7//z/AgD7//3/AgD7//7/AgD7////AgD7/wAAAgD7/wEAAgD7/wIAAgD7/wMAAgD7/wQAAgD7/wUAAgD7/wYAAgD7/wcAAgD7/wgAAgD7/wkAAgD7/woAAgD8//b/AgD8//f/AgD8//j/AgD8//n/AgD8//r/AgD8//v/AgD8//z/AgD8//3/AgD8//7/AgD8////AgD8/wAAAgD8/wEAAgD8/wIAAgD8/wMAAgD8/wQAAgD8/wUAAgD8/wYAAgD8/wcAAgD8/wgAAgD8/wkAAgD8/woAAgD9//X/AgD9//b/AgD9//f/AgD9//j/AgD9//n/AgD9//r/AgD9//v/AgD9//z/AgD9//3/AgD9//7/AgD9////AgD9/wAAAgD9/wEAAgD9/wIAAgD9/wMAAgD9/wQAAgD9/wUAAgD9/wYAAgD9/wcAAgD9/wgAAgD9/wkAAgD9/woAAgD9/wsAAgD+//X/AgD+//b/AgD+//f/AgD+//j/AgD+//n/AgD+//r/AgD+//v/AgD+//z/AgD+//3/AgD+//7/AgD+////AgD+/wAAAgD+/wEAAgD+/wIAAgD+/wMAAgD+/wQAAgD+/wUAAgD+/wYAAgD+/wcAAgD+/wgAAgD+/wkAAgD+/woAAgD+/wsAAgD///X/AgD///b/AgD///f/AgD///j/AgD///n/AgD///r/AgD///v/AgD///z/AgD///3/AgD///7/AgD/////AgD//wAAAgD//wEAAgD//wIAAgD//wMAAgD//wQAAgD//wUAAgD//wYAAgD//wcAAgD//wgAAgD//wkAAgD//woAAgD//wsAAgAAAPX/AgAAAPb/AgAAAPf/AgAAAPj/AgAAAPn/AgAAAPr/AgAAAPv/AgAAAPz/AgAAAP3/AgAAAP7/AgAAAP//AgAAAAAAAgAAAAEAAgAAAAIAAgAAAAMAAgAAAAQAAgAAAAUAAgAAAAYAAgAAAAcAAgAAAAgAAgAAAAkAAgAAAAoAAgAAAAsAAgABAPX/AgABAPb/AgABAPf/AgABAPj/AgABAPn/AgABAPr/AgABAPv/AgABAPz/AgABAP3/AgABAP7/AgABAP//AgABAAAAAgABAAEAAgABAAIAAgABAAMAAgABAAQAAgABAAUAAgABAAYAAgABAAcAAgABAAgAAgABAAkAAgABAAoAAgABAAsAAgACAPX/AgACAPb/AgACAPf/AgACAPj/AgACAPn/AgACAPr/AgACAPv/AgACAPz/AgACAP3/AgACAP7/AgACAP//AgACAAAAAgACAAEAAgACAAIAAgACAAMAAgACAAQAAgACAAUAAgACAAYAAgACAAcAAgACAAgAAgACAAkAAgACAAoAAgACAAsAAgADAPX/AgADAPb/AgADAPf/AgADAPj/AgADAPn/AgADAPr/AgADAPv/AgADAPz/AgADAP3/AgADAP7/AgADAP//AgADAAAAAgADAAEAAgADAAIAAgADAAMAAgADAAQAAgADAAUAAgADAAYAAgADAAcAAgADAAgAAgADAAkAAgADAAoAAgADAAsAAgAEAPb/AgAEAPf/AgAEAPj/AgAEAPn/AgAEAPr/AgAEAPv/AgAEAPz/AgAEAP3/AgAEAP7/AgAEAP//AgAEAAAAAgAEAAEAAgAEAAIAAgAEAAMAAgAEAAQAAgAEAAUAAgAEAAYAAgAEAAcAAgAEAAgAAgAEAAkAAgAEAAoAAgAFAPb/AgAFAPf/AgAFAPj/AgAFAPn/AgAFAPr/AgAFAPv/AgAFAPz/AgAFAP3/AgAFAP7/AgAFAP//AgAFAAAAAgAFAAEAAgAFAAIAAgAFAAMAAgAFAAQAAgAFAAUAAgAFAAYAAgAFAAcAAgAFAAgAAgAFAAkAAgAFAAoAAgAGAPf/AgAGAPj/AgAGAPn/AgAGAPr/AgAGAPv/AgAGAPz/AgAGAP3/AgAGAP7/AgAGAP//AgAGAAAAAgAGAAEAAgAGAAIAAgAGAAMAAgAGAAQAAgAGAAUAAgAGAAYAAgAGAAcAAgAGAAgAAgAGAAkAAgAHAPf/AgAHAPj/AgAHAPn/AgAHAPr/AgAHAPv/AgAHAPz/AgAHAP3/AgAHAP7/AgAHAP//AgAHAAAAAgAHAAEAAgAHAAIAAgAHAAMAAgAHAAQAAgAHAAUAAgAHAAYAAgAHAAcAAgAHAAgAAgAHAAkAAgAIAPj/AgAIAPn/AgAIAPr/AgAIAPv/AgAIAPz/AgAIAP3/AgAIAP7/AgAIAP//AgAIAAAAAgAIAAEAAgAIAAIAAgAIAAMAAgAIAAQAAgAIAAUAAgAIAAYAAgAIAAcAAgAIAAgAAgAJAPn/AgAJAPr/AgAJAPv/AgAJAPz/AgAJAP3/AgAJAP7/AgAJAP//AgAJAAAAAgAJAAEAAgAJAAIAAgAJAAMAAgAJAAQAAgAJAAUAAgAJAAYAAgAJAAcAAgAKAPv/AgAKAPz/AgAKAP3/AgAKAP7/AgAKAP//AgAKAAAAAgAKAAEAAgAKAAIAAgAKAAMAAgAKAAQAAgAKAAUAAgALAP3/AgALAP7/AgALAP//AgALAAAAAgALAAEAAgALAAIAAgALAAMAAwD1//7/AwD1////AwD1/wAAAwD1/wEAAwD1/wIAAwD2//v/AwD2//z/AwD2//3/AwD2//7/AwD2////AwD2/wAAAwD2/wEAAwD2/wIAAwD2/wMAAwD2/wQAAwD2/wUAAwD3//r/AwD3//v/AwD3//z/AwD3//3/AwD3//7/AwD3////AwD3/wAAAwD3/wEAAwD3/wIAAwD3/wMAAwD3/wQAAwD3/wUAAwD3/wYAAwD4//j/AwD4//n/AwD4//r/AwD4//v/AwD4//z/AwD4//3/AwD4//7/AwD4////AwD4/wAAAwD4/wEAAwD4/wIAAwD4/wMAAwD4/wQAAwD4/wUAAwD4/wYAAwD4/wcAAwD4/wgAAwD5//j/AwD5//n/AwD5//r/AwD5//v/AwD5//z/AwD5//3/AwD5//7/AwD5////AwD5/wAAAwD5/wEAAwD5/wIAAwD5/wMAAwD5/wQAAwD5/wUAAwD5/wYAAwD5/wcAAwD5/wgAAwD6//f/AwD6//j/AwD6//n/AwD6//r/AwD6//v/AwD6//z/AwD6//3/AwD6//7/AwD6////AwD6/wAAAwD6/wEAAwD6/wIAAwD6/wMAAwD6/wQAAwD6/wUAAwD6/wYAAwD6/wcAAwD6/wgAAwD6/wkAAwD7//b/AwD7//f/AwD7//j/AwD7//n/AwD7//r/AwD7//v/AwD7//z/AwD7//3/AwD7//7/AwD7////AwD7/wAAAwD7/wEAAwD7/wIAAwD7/wMAAwD7/wQAAwD7/wUAAwD7/wYAAwD7/wcAAwD7/wgAAwD7/wkAAwD7/woAAwD8//b/AwD8//f/AwD8//j/AwD8//n/AwD8//r/AwD8//v/AwD8//z/AwD8//3/AwD8//7/AwD8////AwD8/wAAAwD8/wEAAwD8/wIAAwD8/wMAAwD8/wQAAwD8/wUAAwD8/wYAAwD8/wcAAwD8/wgAAwD8/wkAAwD8/woAAwD9//b/AwD9//f/AwD9//j/AwD9//n/AwD9//r/AwD9//v/AwD9//z/AwD9//3/AwD9//7/AwD9////AwD9/wAAAwD9/wEAAwD9/wIAAwD9/wMAAwD9/wQAAwD9/wUAAwD9/wYAAwD9/wcAAwD9/wgAAwD9/wkAAwD9/woAAwD+//X/AwD+//b/AwD+//f/AwD+//j/AwD+//n/AwD+//r/AwD+//v/AwD+//z/AwD+//3/AwD+//7/AwD+////AwD+/wAAAwD+/wEAAwD+/wIAAwD+/wMAAwD+/wQAAwD+/wUAAwD+/wYAAwD+/wcAAwD+/wgAAwD+/wkAAwD+/woAAwD+/wsAAwD///X/AwD///b/AwD///f/AwD///j/AwD///n/AwD///r/AwD///v/AwD///z/AwD///3/AwD///7/AwD/////AwD//wAAAwD//wEAAwD//wIAAwD//wMAAwD//wQAAwD//wUAAwD//wYAAwD//wcAAwD//wgAAwD//wkAAwD//woAAwD//wsAAwAAAPX/AwAAAPb/AwAAAPf/AwAAAPj/AwAAAPn/AwAAAPr/AwAAAPv/AwAAAPz/AwAAAP3/AwAAAP7/AwAAAP//AwAAAAAAAwAAAAEAAwAAAAIAAwAAAAMAAwAAAAQAAwAAAAUAAwAAAAYAAwAAAAcAAwAAAAgAAwAAAAkAAwAAAAoAAwAAAAsAAwABAPX/AwABAPb/AwABAPf/AwABAPj/AwABAPn/AwABAPr/AwABAPv/AwABAPz/AwABAP3/AwABAP7/AwABAP//AwABAAAAAwABAAEAAwABAAIAAwABAAMAAwABAAQAAwABAAUAAwABAAYAAwABAAcAAwABAAgAAwABAAkAAwABAAoAAwABAAsAAwACAPX/AwACAPb/AwACAPf/AwACAPj/AwACAPn/AwACAPr/AwACAPv/AwACAPz/AwACAP3/AwACAP7/AwACAP//AwACAAAAAwACAAEAAwACAAIAAwACAAMAAwACAAQAAwACAAUAAwACAAYAAwACAAcAAwACAAgAAwACAAkAAwACAAoAAwACAAsAAwADAPb/AwADAPf/AwADAPj/AwADAPn/AwADAPr/AwADAPv/AwADAPz/AwADAP3/AwADAP7/AwADAP//AwADAAAAAwADAAEAAwADAAIAAwADAAMAAwADAAQAAwADAAUAAwADAAYAAwADAAcAAwADAAgAAwADAAkAAwADAAoAAwAEAPb/AwAEAPf/AwAEAPj/AwAEAPn/AwAEAPr/AwAEAPv/AwAEAPz/AwAEAP3/AwAEAP7/AwAEAP//AwAEAAAAAwAEAAEAAwAEAAIAAwAEAAMAAwAEAAQAAwAEAAUAAwAEAAYAAwAEAAcAAwAEAAgAAwAEAAkAAwAEAAoAAwAFAPb/AwAFAPf/AwAFAPj/AwAFAPn/AwAFAPr/AwAFAPv/AwAFAPz/AwAFAP3/AwAFAP7/AwAFAP//AwAFAAAAAwAFAAEAAwAFAAIAAwAFAAMAAwAFAAQAAwAFAAUAAwAFAAYAAwAFAAcAAwAFAAgAAwAFAAkAAwAFAAoAAwAGAPf/AwAGAPj/AwAGAPn/AwAGAPr/AwAGAPv/AwAGAPz/AwAGAP3/AwAGAP7/AwAGAP//AwAGAAAAAwAGAAEAAwAGAAIAAwAGAAMAAwAGAAQAAwAGAAUAAwAGAAYAAwAGAAcAAwAGAAgAAwAGAAkAAwAHAPj/AwAHAPn/AwAHAPr/AwAHAPv/AwAHAPz/AwAHAP3/AwAHAP7/AwAHAP//AwAHAAAAAwAHAAEAAwAHAAIAAwAHAAMAAwAHAAQAAwAHAAUAAwAHAAYAAwAHAAcAAwAHAAgAAwAIAPj/AwAIAPn/AwAIAPr/AwAIAPv/AwAIAPz/AwAIAP3/AwAIAP7/AwAIAP//AwAIAAAAAwAIAAEAAwAIAAIAAwAIAAMAAwAIAAQAAwAIAAUAAwAIAAYAAwAIAAcAAwAIAAgAAwAJAPr/AwAJAPv/AwAJAPz/AwAJAP3/AwAJAP7/AwAJAP//AwAJAAAAAwAJAAEAAwAJAAIAAwAJAAMAAwAJAAQAAwAJAAUAAwAJAAYAAwAKAPv/AwAKAPz/AwAKAP3/AwAKAP7/AwAKAP//AwAKAAAAAwAKAAEAAwAKAAIAAwAKAAMAAwAKAAQAAwAKAAUAAwALAP7/AwALAP//AwALAAAAAwALAAEAAwALAAIABAD1/wAABAD2//z/BAD2//3/BAD2//7/BAD2////BAD2/wAABAD2/wEABAD2/wIABAD2/wMABAD2/wQABAD3//r/BAD3//v/BAD3//z/BAD3//3/BAD3//7/BAD3////BAD3/wAABAD3/wEABAD3/wIABAD3/wMABAD3/wQABAD3/wUABAD3/wYABAD4//n/BAD4//r/BAD4//v/BAD4//z/BAD4//3/BAD4//7/BAD4////BAD4/wAABAD4/wEABAD4/wIABAD4/wMABAD4/wQABAD4/wUABAD4/wYABAD4/wcABAD5//j/BAD5//n/BAD5//r/BAD5//v/BAD5//z/BAD5//3/BAD5//7/BAD5////BAD5/wAABAD5/wEABAD5/wIABAD5/wMABAD5/wQABAD5/wUABAD5/wYABAD5/wcABAD5/wgABAD6//f/BAD6//j/BAD6//n/BAD6//r/BAD6//v/BAD6//z/BAD6//3/BAD6//7/BAD6////BAD6/wAABAD6/wEABAD6/wIABAD6/wMABAD6/wQABAD6/wUABAD6/wYABAD6/wcABAD6/wgABAD6/wkABAD7//f/BAD7//j/BAD7//n/BAD7//r/BAD7//v/BAD7//z/BAD7//3/BAD7//7/BAD7////BAD7/wAABAD7/wEABAD7/wIABAD7/wMABAD7/wQABAD7/wUABAD7/wYABAD7/wcABAD7/wgABAD7/wkABAD8//b/BAD8//f/BAD8//j/BAD8//n/BAD8//r/BAD8//v/BAD8//z/BAD8//3/BAD8//7/BAD8////BAD8/wAABAD8/wEABAD8/wIABAD8/wMABAD8/wQABAD8/wUABAD8/wYABAD8/wcABAD8/wgABAD8/wkABAD8/woABAD9//b/BAD9//f/BAD9//j/BAD9//n/BAD9//r/BAD9//v/BAD9//z/BAD9//3/BAD9//7/BAD9////BAD9/wAABAD9/wEABAD9/wIABAD9/wMABAD9/wQABAD9/wUABAD9/wYABAD9/wcABAD9/wgABAD9/wkABAD9/woABAD+//b/BAD+//f/BAD+//j/BAD+//n/BAD+//r/BAD+//v/BAD+//z/BAD+//3/BAD+//7/BAD+////BAD+/wAABAD+/wEABAD+/wIABAD+/wMABAD+/wQABAD+/wUABAD+/wYABAD+/wcABAD+/wgABAD+/wkABAD+/woABAD///b/BAD///f/BAD///j/BAD///n/BAD///r/BAD///v/BAD///z/BAD///3/BAD///7/BAD/////BAD//wAABAD//wEABAD//wIABAD//wMABAD//wQABAD//wUABAD//wYABAD//wcABAD//wgABAD//wkABAD//woABAAAAPX/BAAAAPb/BAAAAPf/BAAAAPj/BAAAAPn/BAAAAPr/BAAAAPv/BAAAAPz/BAAAAP3/BAAAAP7/BAAAAP//BAAAAAAABAAAAAEABAAAAAIABAAAAAMABAAAAAQABAAAAAUABAAAAAYABAAAAAcABAAAAAgABAAAAAkABAAAAAoABAAAAAsABAABAPb/BAABAPf/BAABAPj/BAABAPn/BAABAPr/BAABAPv/BAABAPz/BAABAP3/BAABAP7/BAABAP//BAABAAAABAABAAEABAABAAIABAABAAMABAABAAQABAABAAUABAABAAYABAABAAcABAABAAgABAABAAkABAABAAoABAACAPb/BAACAPf/BAACAPj/BAACAPn/BAACAPr/BAACAPv/BAACAPz/BAACAP3/BAACAP7/BAACAP//BAACAAAABAACAAEABAACAAIABAACAAMABAACAAQABAACAAUABAACAAYABAACAAcABAACAAgABAACAAkABAACAAoABAADAPb/BAADAPf/BAADAPj/BAADAPn/BAADAPr/BAADAPv/BAADAPz/BAADAP3/BAADAP7/BAADAP//BAADAAAABAADAAEABAADAAIABAADAAMABAADAAQABAADAAUABAADAAYABAADAAcABAADAAgABAADAAkABAADAAoABAAEAPb/BAAEAPf/BAAEAPj/BAAEAPn/BAAEAPr/BAAEAPv/BAAEAPz/BAAEAP3/BAAEAP7/BAAEAP//BAAEAAAABAAEAAEABAAEAAIABAAEAAMABAAEAAQABAAEAAUABAAEAAYABAAEAAcABAAEAAgABAAEAAkABAAEAAoABAAFAPf/BAAFAPj/BAAFAPn/BAAFAPr/BAAFAPv/BAAFAPz/BAAFAP3/BAAFAP7/BAAFAP//BAAFAAAABAAFAAEABAAFAAIABAAFAAMABAAFAAQABAAFAAUABAAFAAYABAAFAAcABAAFAAgABAAFAAkABAAGAPf/BAAGAPj/BAAGAPn/BAAGAPr/BAAGAPv/BAAGAPz/BAAGAP3/BAAGAP7/BAAGAP//BAAGAAAABAAGAAEABAAGAAIABAAGAAMABAAGAAQABAAGAAUABAAGAAYABAAGAAcABAAGAAgABAAGAAkABAAHAPj/BAAHAPn/BAAHAPr/BAAHAPv/BAAHAPz/BAAHAP3/BAAHAP7/BAAHAP//BAAHAAAABAAHAAEABAAHAAIABAAHAAMABAAHAAQABAAHAAUABAAHAAYABAAHAAcABAAHAAgABAAIAPn/BAAIAPr/BAAIAPv/BAAIAPz/BAAIAP3/BAAIAP7/BAAIAP//BAAIAAAABAAIAAEABAAIAAIABAAIAAMABAAIAAQABAAIAAUABAAIAAYABAAIAAcABAAJAPr/BAAJAPv/BAAJAPz/BAAJAP3/BAAJAP7/BAAJAP//BAAJAAAABAAJAAEABAAJAAIABAAJAAMABAAJAAQABAAJAAUABAAJAAYABAAKAPz/BAAKAP3/BAAKAP7/BAAKAP//BAAKAAAABAAKAAEABAAKAAIABAAKAAMABAAKAAQABAALAAAABQD2//3/BQD2//7/BQD2////BQD2/wAABQD2/wEABQD2/wIABQD2/wMABQD3//v/BQD3//z/BQD3//3/BQD3//7/BQD3////BQD3/wAABQD3/wEABQD3/wIABQD3/wMABQD3/wQABQD3/wUABQD4//r/BQD4//v/BQD4//z/BQD4//3/BQD4//7/BQD4////BQD4/wAABQD4/wEABQD4/wIABQD4/wMABQD4/wQABQD4/wUABQD4/wYABQD5//n/BQD5//r/BQD5//v/BQD5//z/BQD5//3/BQD5//7/BQD5////BQD5/wAABQD5/wEABQD5/wIABQD5/wMABQD5/wQABQD5/wUABQD5/wYABQD5/wcABQD6//j/BQD6//n/BQD6//r/BQD6//v/BQD6//z/BQD6//3/BQD6//7/BQD6////BQD6/wAABQD6/wEABQD6/wIABQD6/wMABQD6/wQABQD6/wUABQD6/wYABQD6/wcABQD6/wgABQD7//f/BQD7//j/BQD7//n/BQD7//r/BQD7//v/BQD7//z/BQD7//3/BQD7//7/BQD7////BQD7/wAABQD7/wEABQD7/wIABQD7/wMABQD7/wQABQD7/wUABQD7/wYABQD7/wcABQD7/wgABQD7/wkABQD8//f/BQD8//j/BQD8//n/BQD8//r/BQD8//v/BQD8//z/BQD8//3/BQD8//7/BQD8////BQD8/wAABQD8/wEABQD8/wIABQD8/wMABQD8/wQABQD8/wUABQD8/wYABQD8/wcABQD8/wgABQD8/wkABQD9//b/BQD9//f/BQD9//j/BQD9//n/BQD9//r/BQD9//v/BQD9//z/BQD9//3/BQD9//7/BQD9////BQD9/wAABQD9/wEABQD9/wIABQD9/wMABQD9/wQABQD9/wUABQD9/wYABQD9/wcABQD9/wgABQD9/wkABQD9/woABQD+//b/BQD+//f/BQD+//j/BQD+//n/BQD+//r/BQD+//v/BQD+//z/BQD+//3/BQD+//7/BQD+////BQD+/wAABQD+/wEABQD+/wIABQD+/wMABQD+/wQABQD+/wUABQD+/wYABQD+/wcABQD+/wgABQD+/wkABQD+/woABQD///b/BQD///f/BQD///j/BQD///n/BQD///r/BQD///v/BQD///z/BQD///3/BQD///7/BQD/////BQD//wAABQD//wEABQD//wIABQD//wMABQD//wQABQD//wUABQD//wYABQD//wcABQD//wgABQD//wkABQD//woABQAAAPb/BQAAAPf/BQAAAPj/BQAAAPn/BQAAAPr/BQAAAPv/BQAAAPz/BQAAAP3/BQAAAP7/BQAAAP//BQAAAAAABQAAAAEABQAAAAIABQAAAAMABQAAAAQABQAAAAUABQAAAAYABQAAAAcABQAAAAgABQAAAAkABQAAAAoABQABAPb/BQABAPf/BQABAPj/BQABAPn/BQABAPr/BQABAPv/BQABAPz/BQABAP3/BQABAP7/BQABAP//BQABAAAABQABAAEABQABAAIABQABAAMABQABAAQABQABAAUABQABAAYABQABAAcABQABAAgABQABAAkABQABAAoABQACAPb/BQACAPf/BQACAPj/BQACAPn/BQACAPr/BQACAPv/BQACAPz/BQACAP3/BQACAP7/BQACAP//BQACAAAABQACAAEABQACAAIABQACAAMABQACAAQABQACAAUABQACAAYABQACAAcABQACAAgABQACAAkABQACAAoABQADAPb/BQADAPf/BQADAPj/BQADAPn/BQADAPr/BQADAPv/BQADAPz/BQADAP3/BQADAP7/BQADAP//BQADAAAABQADAAEABQADAAIABQADAAMABQADAAQABQADAAUABQADAAYABQADAAcABQADAAgABQADAAkABQADAAoABQAEAPf/BQAEAPj/BQAEAPn/BQAEAPr/BQAEAPv/BQAEAPz/BQAEAP3/BQAEAP7/BQAEAP//BQAEAAAABQAEAAEABQAEAAIABQAEAAMABQAEAAQABQAEAAUABQAEAAYABQAEAAcABQAEAAgABQAEAAkABQAFAPf/BQAFAPj/BQAFAPn/BQAFAPr/BQAFAPv/BQAFAPz/BQAFAP3/BQAFAP7/BQAFAP//BQAFAAAABQAFAAEABQAFAAIABQAFAAMABQAFAAQABQAFAAUABQAFAAYABQAFAAcABQAFAAgABQAFAAkABQAGAPj/BQAGAPn/BQAGAPr/BQAGAPv/BQAGAPz/BQAGAP3/BQAGAP7/BQAGAP//BQAGAAAABQAGAAEABQAGAAIABQAGAAMABQAGAAQABQAGAAUABQAGAAYABQAGAAcABQAGAAgABQAHAPn/BQAHAPr/BQAHAPv/BQAHAPz/BQAHAP3/BQAHAP7/BQAHAP//BQAHAAAABQAHAAEABQAHAAIABQAHAAMABQAHAAQABQAHAAUABQAHAAYABQAHAAcABQAIAPr/BQAIAPv/BQAIAPz/BQAIAP3/BQAIAP7/BQAIAP//BQAIAAAABQAIAAEABQAIAAIABQAIAAMABQAIAAQABQAIAAUABQAIAAYABQAJAPv/BQAJAPz/BQAJAP3/BQAJAP7/BQAJAP//BQAJAAAABQAJAAEABQAJAAIABQAJAAMABQAJAAQABQAJAAUABQAKAP3/BQAKAP7/BQAKAP//BQAKAAAABQAKAAEABQAKAAIABQAKAAMABgD2////BgD2/wAABgD2/wEABgD3//z/BgD3//3/BgD3//7/BgD3////BgD3/wAABgD3/wEABgD3/wIABgD3/wMABgD3/wQABgD4//r/BgD4//v/BgD4//z/BgD4//3/BgD4//7/BgD4////BgD4/wAABgD4/wEABgD4/wIABgD4/wMABgD4/wQABgD4/wUABgD4/wYABgD5//n/BgD5//r/BgD5//v/BgD5//z/BgD5//3/BgD5//7/BgD5////BgD5/wAABgD5/wEABgD5/wIABgD5/wMABgD5/wQABgD5/wUABgD5/wYABgD5/wcABgD6//j/BgD6//n/BgD6//r/BgD6//v/BgD6//z/BgD6//3/BgD6//7/BgD6////BgD6/wAABgD6/wEABgD6/wIABgD6/wMABgD6/wQABgD6/wUABgD6/wYABgD6/wcABgD6/wgABgD7//j/BgD7//n/BgD7//r/BgD7//v/BgD7//z/BgD7//3/BgD7//7/BgD7////BgD7/wAABgD7/wEABgD7/wIABgD7/wMABgD7/wQABgD7/wUABgD7/wYABgD7/wcABgD7/wgABgD8//f/BgD8//j/BgD8//n/BgD8//r/BgD8//v/BgD8//z/BgD8//3/BgD8//7/BgD8////BgD8/wAABgD8/wEABgD8/wIABgD8/wMABgD8/wQABgD8/wUABgD8/wYABgD8/wcABgD8/wgABgD8/wkABgD9//f/BgD9//j/BgD9//n/BgD9//r/BgD9//v/BgD9//z/BgD9//3/BgD9//7/BgD9////BgD9/wAABgD9/wEABgD9/wIABgD9/wMABgD9/wQABgD9/wUABgD9/wYABgD9/wcABgD9/wgABgD9/wkABgD+//f/BgD+//j/BgD+//n/BgD+//r/BgD+//v/BgD+//z/BgD+//3/BgD+//7/BgD+////BgD+/wAABgD+/wEABgD+/wIABgD+/wMABgD+/wQABgD+/wUABgD+/wYABgD+/wcABgD+/wgABgD+/wkABgD///b/BgD///f/BgD///j/BgD///n/BgD///r/BgD///v/BgD///z/BgD///3/BgD///7/BgD/////BgD//wAABgD//wEABgD//wIABgD//wMABgD//wQABgD//wUABgD//wYABgD//wcABgD//wgABgD//wkABgD//woABgAAAPb/BgAAAPf/BgAAAPj/BgAAAPn/BgAAAPr/BgAAAPv/BgAAAPz/BgAAAP3/BgAAAP7/BgAAAP//BgAAAAAABgAAAAEABgAAAAIABgAAAAMABgAAAAQABgAAAAUABgAAAAYABgAAAAcABgAAAAgABgAAAAkABgAAAAoABgABAPb/BgABAPf/BgABAPj/BgABAPn/BgABAPr/BgABAPv/BgABAPz/BgABAP3/BgABAP7/BgABAP//BgABAAAABgABAAEABgABAAIABgABAAMABgABAAQABgABAAUABgABAAYABgABAAcABgABAAgABgABAAkABgABAAoABgACAPf/BgACAPj/BgACAPn/BgACAPr/BgACAPv/BgACAPz/BgACAP3/BgACAP7/BgACAP//BgACAAAABgACAAEABgACAAIABgACAAMABgACAAQABgACAAUABgACAAYABgACAAcABgACAAgABgACAAkABgADAPf/BgADAPj/BgADAPn/BgADAPr/BgADAPv/BgADAPz/BgADAP3/BgADAP7/BgADAP//BgADAAAABgADAAEABgADAAIABgADAAMABgADAAQABgADAAUABgADAAYABgADAAcABgADAAgABgADAAkABgAEAPf/BgAEAPj/BgAEAPn/BgAEAPr/BgAEAPv/BgAEAPz/BgAEAP3/BgAEAP7/BgAEAP//BgAEAAAABgAEAAEABgAEAAIABgAEAAMABgAEAAQABgAEAAUABgAEAAYABgAEAAcABgAEAAgABgAEAAkABgAFAPj/BgAFAPn/BgAFAPr/BgAFAPv/BgAFAPz/BgAFAP3/BgAFAP7/BgAFAP//BgAFAAAABgAFAAEABgAFAAIABgAFAAMABgAFAAQABgAFAAUABgAFAAYABgAFAAcABgAFAAgABgAGAPj/BgAGAPn/BgAGAPr/BgAGAPv/BgAGAPz/BgAGAP3/BgAGAP7/BgAGAP//BgAGAAAABgAGAAEABgAGAAIABgAGAAMABgAGAAQABgAGAAUABgAGAAYABgAGAAcABgAGAAgABgAHAPn/BgAHAPr/BgAHAPv/BgAHAPz/BgAHAP3/BgAHAP7/BgAHAP//BgAHAAAABgAHAAEABgAHAAIABgAHAAMABgAHAAQABgAHAAUABgAHAAYABgAHAAcABgAIAPr/BgAIAPv/BgAIAPz/BgAIAP3/BgAIAP7/BgAIAP//BgAIAAAABgAIAAEABgAIAAIABgAIAAMABgAIAAQABgAIAAUABgAIAAYABgAJAPz/BgAJAP3/BgAJAP7/BgAJAP//BgAJAAAABgAJAAEABgAJAAIABgAJAAMABgAJAAQABgAKAP//BgAKAAAABgAKAAEABwD3//7/BwD3////BwD3/wAABwD3/wEABwD3/wIABwD4//z/BwD4//3/BwD4//7/BwD4////BwD4/wAABwD4/wEABwD4/wIABwD4/wMABwD4/wQABwD5//r/BwD5//v/BwD5//z/BwD5//3/BwD5//7/BwD5////BwD5/wAABwD5/wEABwD5/wIABwD5/wMABwD5/wQABwD5/wUABwD5/wYABwD6//n/BwD6//r/BwD6//v/BwD6//z/BwD6//3/BwD6//7/BwD6////BwD6/wAABwD6/wEABwD6/wIABwD6/wMABwD6/wQABwD6/wUABwD6/wYABwD6/wcABwD7//n/BwD7//r/BwD7//v/BwD7//z/BwD7//3/BwD7//7/BwD7////BwD7/wAABwD7/wEABwD7/wIABwD7/wMABwD7/wQABwD7/wUABwD7/wYABwD7/wcABwD8//j/BwD8//n/BwD8//r/BwD8//v/BwD8//z/BwD8//3/BwD8//7/BwD8////BwD8/wAABwD8/wEABwD8/wIABwD8/wMABwD8/wQABwD8/wUABwD8/wYABwD8/wcABwD8/wgABwD9//j/BwD9//n/BwD9//r/BwD9//v/BwD9//z/BwD9//3/BwD9//7/BwD9////BwD9/wAABwD9/wEABwD9/wIABwD9/wMABwD9/wQABwD9/wUABwD9/wYABwD9/wcABwD9/wgABwD+//f/BwD+//j/BwD+//n/BwD+//r/BwD+//v/BwD+//z/BwD+//3/BwD+//7/BwD+////BwD+/wAABwD+/wEABwD+/wIABwD+/wMABwD+/wQABwD+/wUABwD+/wYABwD+/wcABwD+/wgABwD+/wkABwD///f/BwD///j/BwD///n/BwD///r/BwD///v/BwD///z/BwD///3/BwD///7/BwD/////BwD//wAABwD//wEABwD//wIABwD//wMABwD//wQABwD//wUABwD//wYABwD//wcABwD//wgABwD//wkABwAAAPf/BwAAAPj/BwAAAPn/BwAAAPr/BwAAAPv/BwAAAPz/BwAAAP3/BwAAAP7/BwAAAP//BwAAAAAABwAAAAEABwAAAAIABwAAAAMABwAAAAQABwAAAAUABwAAAAYABwAAAAcABwAAAAgABwAAAAkABwABAPf/BwABAPj/BwABAPn/BwABAPr/BwABAPv/BwABAPz/BwABAP3/BwABAP7/BwABAP//BwABAAAABwABAAEABwABAAIABwABAAMABwABAAQABwABAAUABwABAAYABwABAAcABwABAAgABwABAAkABwACAPf/BwACAPj/BwACAPn/BwACAPr/BwACAPv/BwACAPz/BwACAP3/BwACAP7/BwACAP//BwACAAAABwACAAEABwACAAIABwACAAMABwACAAQABwACAAUABwACAAYABwACAAcABwACAAgABwACAAkABwADAPj/BwADAPn/BwADAPr/BwADAPv/BwADAPz/BwADAP3/BwADAP7/BwADAP//BwADAAAABwADAAEABwADAAIABwADAAMABwADAAQABwADAAUABwADAAYABwADAAcABwADAAgABwAEAPj/BwAEAPn/BwAEAPr/BwAEAPv/BwAEAPz/BwAEAP3/BwAEAP7/BwAEAP//BwAEAAAABwAEAAEABwAEAAIABwAEAAMABwAEAAQABwAEAAUABwAEAAYABwAEAAcABwAEAAgABwAFAPn/BwAFAPr/BwAFAPv/BwAFAPz/BwAFAP3/BwAFAP7/BwAFAP//BwAFAAAABwAFAAEABwAFAAIABwAFAAMABwAFAAQABwAFAAUABwAFAAYABwAFAAcABwAGAPn/BwAGAPr/BwAGAPv/BwAGAPz/BwAGAP3/BwAGAP7/BwAGAP//BwAGAAAABwAGAAEABwAGAAIABwAGAAMABwAGAAQABwAGAAUABwAGAAYABwAGAAcABwAHAPr/BwAHAPv/BwAHAPz/BwAHAP3/BwAHAP7/BwAHAP//BwAHAAAABwAHAAEABwAHAAIABwAHAAMABwAHAAQABwAHAAUABwAHAAYABwAIAPz/BwAIAP3/BwAIAP7/BwAIAP//BwAIAAAABwAIAAEABwAIAAIABwAIAAMABwAIAAQABwAJAP7/BwAJAP//BwAJAAAABwAJAAEABwAJAAIACAD4//3/CAD4//7/CAD4////CAD4/wAACAD4/wEACAD4/wIACAD4/wMACAD5//z/CAD5//3/CAD5//7/CAD5////CAD5/wAACAD5/wEACAD5/wIACAD5/wMACAD5/wQACAD6//r/CAD6//v/CAD6//z/CAD6//3/CAD6//7/CAD6////CAD6/wAACAD6/wEACAD6/wIACAD6/wMACAD6/wQACAD6/wUACAD6/wYACAD7//r/CAD7//v/CAD7//z/CAD7//3/CAD7//7/CAD7////CAD7/wAACAD7/wEACAD7/wIACAD7/wMACAD7/wQACAD7/wUACAD7/wYACAD8//n/CAD8//r/CAD8//v/CAD8//z/CAD8//3/CAD8//7/CAD8////CAD8/wAACAD8/wEACAD8/wIACAD8/wMACAD8/wQACAD8/wUACAD8/wYACAD8/wcACAD9//j/CAD9//n/CAD9//r/CAD9//v/CAD9//z/CAD9//3/CAD9//7/CAD9////CAD9/wAACAD9/wEACAD9/wIACAD9/wMACAD9/wQACAD9/wUACAD9/wYACAD9/wcACAD9/wgACAD+//j/CAD+//n/CAD+//r/CAD+//v/CAD+//z/CAD+//3/CAD+//7/CAD+////CAD+/wAACAD+/wEACAD+/wIACAD+/wMACAD+/wQACAD+/wUACAD+/wYACAD+/wcACAD+/wgACAD///j/CAD///n/CAD///r/CAD///v/CAD///z/CAD///3/CAD///7/CAD/////CAD//wAACAD//wEACAD//wIACAD//wMACAD//wQACAD//wUACAD//wYACAD//wcACAD//wgACAAAAPj/CAAAAPn/CAAAAPr/CAAAAPv/CAAAAPz/CAAAAP3/CAAAAP7/CAAAAP//CAAAAAAACAAAAAEACAAAAAIACAAAAAMACAAAAAQACAAAAAUACAAAAAYACAAAAAcACAAAAAgACAABAPj/CAABAPn/CAABAPr/CAABAPv/CAABAPz/CAABAP3/CAABAP7/CAABAP//CAABAAAACAABAAEACAABAAIACAABAAMACAABAAQACAABAAUACAABAAYACAABAAcACAABAAgACAACAPj/CAACAPn/CAACAPr/CAACAPv/CAACAPz/CAACAP3/CAACAP7/CAACAP//CAACAAAACAACAAEACAACAAIACAACAAMACAACAAQACAACAAUACAACAAYACAACAAcACAACAAgACAADAPj/CAADAPn/CAADAPr/CAADAPv/CAADAPz/CAADAP3/CAADAP7/CAADAP//CAADAAAACAADAAEACAADAAIACAADAAMACAADAAQACAADAAUACAADAAYACAADAAcACAADAAgACAAEAPn/CAAEAPr/CAAEAPv/CAAEAPz/CAAEAP3/CAAEAP7/CAAEAP//CAAEAAAACAAEAAEACAAEAAIACAAEAAMACAAEAAQACAAEAAUACAAEAAYACAAEAAcACAAFAPr/CAAFAPv/CAAFAPz/CAAFAP3/CAAFAP7/CAAFAP//CAAFAAAACAAFAAEACAAFAAIACAAFAAMACAAFAAQACAAFAAUACAAFAAYACAAGAPr/CAAGAPv/CAAGAPz/CAAGAP3/CAAGAP7/CAAGAP//CAAGAAAACAAGAAEACAAGAAIACAAGAAMACAAGAAQACAAGAAUACAAGAAYACAAHAPz/CAAHAP3/CAAHAP7/CAAHAP//CAAHAAAACAAHAAEACAAHAAIACAAHAAMACAAHAAQACAAIAP3/CAAIAP7/CAAIAP//CAAIAAAACAAIAAEACAAIAAIACAAIAAMACQD5//7/CQD5////CQD5/wAACQD5/wEACQD5/wIACQD6//z/CQD6//3/CQD6//7/CQD6////CQD6/wAACQD6/wEACQD6/wIACQD6/wMACQD6/wQACQD7//v/CQD7//z/CQD7//3/CQD7//7/CQD7////CQD7/wAACQD7/wEACQD7/wIACQD7/wMACQD7/wQACQD7/wUACQD8//r/CQD8//v/CQD8//z/CQD8//3/CQD8//7/CQD8////CQD8/wAACQD8/wEACQD8/wIACQD8/wMACQD8/wQACQD8/wUACQD8/wYACQD9//r/CQD9//v/CQD9//z/CQD9//3/CQD9//7/CQD9////CQD9/wAACQD9/wEACQD9/wIACQD9/wMACQD9/wQACQD9/wUACQD9/wYACQD+//n/CQD+//r/CQD+//v/CQD+//z/CQD+//3/CQD+//7/CQD+////CQD+/wAACQD+/wEACQD+/wIACQD+/wMACQD+/wQACQD+/wUACQD+/wYACQD+/wcACQD///n/CQD///r/CQD///v/CQD///z/CQD///3/CQD///7/CQD/////CQD//wAACQD//wEACQD//wIACQD//wMACQD//wQACQD//wUACQD//wYACQD//wcACQAAAPn/CQAAAPr/CQAAAPv/CQAAAPz/CQAAAP3/CQAAAP7/CQAAAP//CQAAAAAACQAAAAEACQAAAAIACQAAAAMACQAAAAQACQAAAAUACQAAAAYACQAAAAcACQABAPn/CQABAPr/CQABAPv/CQABAPz/CQABAP3/CQABAP7/CQABAP//CQABAAAACQABAAEACQABAAIACQABAAMACQABAAQACQABAAUACQABAAYACQABAAcACQACAPn/CQACAPr/CQACAPv/CQACAPz/CQACAP3/CQACAP7/CQACAP//CQACAAAACQACAAEACQACAAIACQACAAMACQACAAQACQACAAUACQACAAYACQACAAcACQADAPr/CQADAPv/CQADAPz/CQADAP3/CQADAP7/CQADAP//CQADAAAACQADAAEACQADAAIACQADAAMACQADAAQACQADAAUACQADAAYACQAEAPr/CQAEAPv/CQAEAPz/CQAEAP3/CQAEAP7/CQAEAP//CQAEAAAACQAEAAEACQAEAAIACQAEAAMACQAEAAQACQAEAAUACQAEAAYACQAFAPv/CQAFAPz/CQAFAP3/CQAFAP7/CQAFAP//CQAFAAAACQAFAAEACQAFAAIACQAFAAMACQAFAAQACQAFAAUACQAGAPz/CQAGAP3/CQAGAP7/CQAGAP//CQAGAAAACQAGAAEACQAGAAIACQAGAAMACQAGAAQACQAHAP7/CQAHAP//CQAHAAAACQAHAAEACQAHAAIACgD6////CgD6/wAACgD6/wEACgD7//3/CgD7//7/CgD7////CgD7/wAACgD7/wEACgD7/wIACgD7/wMACgD8//z/CgD8//3/CgD8//7/CgD8////CgD8/wAACgD8/wEACgD8/wIACgD8/wMACgD8/wQACgD9//v/CgD9//z/CgD9//3/CgD9//7/CgD9////CgD9/wAACgD9/wEACgD9/wIACgD9/wMACgD9/wQACgD9/wUACgD+//v/CgD+//z/CgD+//3/CgD+//7/CgD+////CgD+/wAACgD+/wEACgD+/wIACgD+/wMACgD+/wQACgD+/wUACgD///r/CgD///v/CgD///z/CgD///3/CgD///7/CgD/////CgD//wAACgD//wEACgD//wIACgD//wMACgD//wQACgD//wUACgD//wYACgAAAPr/CgAAAPv/CgAAAPz/CgAAAP3/CgAAAP7/CgAAAP//CgAAAAAACgAAAAEACgAAAAIACgAAAAMACgAAAAQACgAAAAUACgAAAAYACgABAPr/CgABAPv/CgABAPz/CgABAP3/CgABAP7/CgABAP//CgABAAAACgABAAEACgABAAIACgABAAMACgABAAQACgABAAUACgABAAYACgACAPv/CgACAPz/CgACAP3/CgACAP7/CgACAP//CgACAAAACgACAAEACgACAAIACgACAAMACgACAAQACgACAAUACgADAPv/CgADAPz/CgADAP3/CgADAP7/CgADAP//CgADAAAACgADAAEACgADAAIACgADAAMACgADAAQACgADAAUACgAEAPz/CgAEAP3/CgAEAP7/CgAEAP//CgAEAAAACgAEAAEACgAEAAIACgAEAAMACgAEAAQACgAFAP3/CgAFAP7/CgAFAP//CgAFAAAACgAFAAEACgAFAAIACgAFAAMACgAGAP//CgAGAAAACgAGAAEACwD8/wAACwD9//7/CwD9////CwD9/wAACwD9/wEACwD9/wIACwD+//3/CwD+//7/CwD+////CwD+/wAACwD+/wEACwD+/wIACwD+/wMACwD///3/CwD///7/CwD/////CwD//wAACwD//wEACwD//wIACwD//wMACwAAAPz/CwAAAP3/CwAAAP7/CwAAAP//CwAAAAAACwAAAAEACwAAAAIACwAAAAMACwAAAAQACwABAP3/CwABAP7/CwABAP//CwABAAAACwABAAEACwABAAIACwABAAMACwACAP3/CwACAP7/CwACAP//CwACAAAACwACAAEACwACAAIACwACAAMACwADAP7/CwADAP//CwADAAAACwADAAEACwADAAIACwAEAAAA\",\"F2\":\"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\",\"U_re\":\"mZudObFDEztSs/A6Hy8ZO1Kz8DqxQxM7sUMTOxejqjmRch87zJ6xOZFyHzsXo6o5sUMTO1Kz8DqRch87GPACO0sVJjsY8AI7kXIfO1Kz8DqZm505Hy8ZO8yesTlLFSY70e24OUsVJjvMnrE5Hy8ZO5mbnTlSs/A6kXIfOxjwAjtLFSY7GPACO5FyHztSs/A6sUMTOxejqjmRch87zJ6xOZFyHzsXo6o5sUMTO7FDEztSs/A6Hy8ZO1Kz8DqxQxM7mZudOZmbnTmh4m47mZudObFDEzsXo6o5kXIfO8yesTmRch87F6OqObFDEzvwmHg7zJ6xORe7jDv5lcA5ueySO/mVwDkXu4w7zJ6xOfCYeDuxQxM7zJ6xOWgfLTvPkMg5yY48O/Pi0DnJjjw7z5DIOWgfLTvMnrE5sUMTOxejqjkXu4w7z5DIOZ+aoDvoi9k5Si2oO+iL2TmfmqA7z5DIORe7jDsXo6o5mZudOZFyHzv5lcA5yY48O+iL2TnoGE47voniOegYTjvoi9k5yY48O/mVwDmRch87mZudOaHibjvMnrE5ueySO/Pi0DlKLag7voniOfFOsDu+ieI5Si2oO/Pi0Dm57JI7zJ6xOaHibjuZm505kXIfO/mVwDnJjjw76IvZOegYTju+ieI56BhOO+iL2TnJjjw7+ZXAOZFyHzuZm505F6OqORe7jDvPkMg5n5qgO+iL2TlKLag76IvZOZ+aoDvPkMg5F7uMOxejqjmxQxM7zJ6xOWgfLTvPkMg5yY48O/Pi0DnJjjw7z5DIOWgfLTvMnrE5sUMTO/CYeDvMnrE5F7uMO/mVwDm57JI7+ZXAORe7jDvMnrE58Jh4O7FDEzsXo6o5kXIfO8yesTmRch87F6OqObFDEzuZm505oeJuO5mbnTmxQxM7UrPwOh8vGTtSs/A6sUMTO0T4ozmRch870e24OWgfLTv5lcA5aB8tO9HtuDmRch87RPijOVKz8DpLFSY7KfQOO8mOPDvTrBw7OQpFO9OsHDvJjjw7KfQOO0sVJjtSs/A6RPijOUsVJjvPkMg5OQpFO76J4jlLyFc7Lc3rOUvIVzu+ieI5OQpFO8+QyDlLFSY7RPijOZFyHzsp9A47OQpFO4F2LDuELGI7ecc+O85XbTt5xz47hCxiO4F2LDs5CkU7KfQOO5FyHzuxQxM70e24OcmOPDu+ieI5hCxiOyND/zkyYXk7AakEOjJheTsjQ/85hCxiO76J4jnJjjw70e24ObFDEztSs/A6aB8tO9OsHDtLyFc7ecc+OzJheTuFQVQ72jGDO4VBVDsyYXk7ecc+O0vIVzvTrBw7aB8tO1Kz8DofLxk7+ZXAOTkKRTstzes5zldtOwGpBDraMYM7YsAJOtoxgzsBqQQ6zldtOy3N6zk5CkU7+ZXAOR8vGTtSs/A6aB8tO9OsHDtLyFc7ecc+OzJheTuFQVQ72jGDO4VBVDsyYXk7ecc+O0vIVzvTrBw7aB8tO1Kz8DqxQxM70e24OcmOPDu+ieI5hCxiOyND/zkyYXk7AakEOjJheTsjQ/85hCxiO76J4jnJjjw70e24ObFDEzuRch87KfQOOzkKRTuBdiw7hCxiO3nHPjvOV207ecc+O4QsYjuBdiw7OQpFOyn0DjuRch87RPijOUsVJjvPkMg5OQpFO76J4jlLyFc7Lc3rOUvIVzu+ieI5OQpFO8+QyDlLFSY7RPijOVKz8DpLFSY7KfQOO8mOPDvTrBw7OQpFO9OsHDvJjjw7KfQOO0sVJjtSs/A6RPijOZFyHzvR7bg5aB8tO/mVwDloHy070e24OZFyHztE+KM5sUMTO1Kz8DofLxk7UrPwOrFDEzuZm5058Jh4OxejqjmmcoE7F6OqOfCYeDuZm505F6OqOUsVJjv5lcA5hpk0O8+QyDmGmTQ7+ZXAOUsVJjsXo6o5oeJuO8yesTm57JI78+LQOUotqDu+ieI58U6wO76J4jlKLag78+LQObnskjvMnrE5oeJuO8yesTmGmTQ76IvZOUvIVzuVZ/U5zldtOyND/znOV207lWf1OUvIVzvoi9k5hpk0O8yesTkXo6o5ueySO+iL2TnNDrk7I0P/Oa7S1ztiwAk64enjO2LACTqu0tc7I0P/Oc0OuTvoi9k5ueySOxejqjmZm505SxUmO/Pi0DlLyFc7I0P/OdoxgztT2g46I+2RO5PkEzoj7ZE7U9oOOtoxgzsjQ/85S8hXO/Pi0DlLFSY7mZudOfCYeDv5lcA5Si2oO5Vn9Tmu0tc7U9oOOmOp/ztwxhg60NgHPHDGGDpjqf87U9oOOq7S1zuVZ/U5Si2oO/mVwDnwmHg7F6OqOYaZNDu+ieI5zldtO2LACToj7ZE7cMYYOqOIozuPXh06o4ijO3DGGDoj7ZE7YsAJOs5XbTu+ieI5hpk0OxejqjmmcoE7z5DIOfFOsDsjQ/854enjO5PkEzrQ2Ac8j14dOmq+EDyPXh060NgHPJPkEzrh6eM7I0P/OfFOsDvPkMg5pnKBOxejqjmGmTQ7voniOc5XbTtiwAk6I+2RO3DGGDqjiKM7j14dOqOIoztwxhg6I+2RO2LACTrOV207voniOYaZNDsXo6o58Jh4O/mVwDlKLag7lWf1Oa7S1ztT2g46Y6n/O3DGGDrQ2Ac8cMYYOmOp/ztT2g46rtLXO5Vn9TlKLag7+ZXAOfCYeDuZm505SxUmO/Pi0DlLyFc7I0P/OdoxgztT2g46I+2RO5PkEzoj7ZE7U9oOOtoxgzsjQ/85S8hXO/Pi0DlLFSY7mZudORejqjm57JI76IvZOc0OuTsjQ/85rtLXO2LACTrh6eM7YsAJOq7S1zsjQ/85zQ65O+iL2Tm57JI7F6OqOcyesTmGmTQ76IvZOUvIVzuVZ/U5zldtOyND/znOV207lWf1OUvIVzvoi9k5hpk0O8yesTmh4m47zJ6xObnskjvz4tA5Si2oO76J4jnxTrA7voniOUotqDvz4tA5ueySO8yesTmh4m47F6OqOUsVJjv5lcA5hpk0O8+QyDmGmTQ7+ZXAOUsVJjsXo6o5mZudOfCYeDsXo6o5pnKBOxejqjnwmHg7mZudObFDEztSs/A6Hy8ZO1Kz8DqxQxM7F6OqOUsVJjv5lcA5hpk0O8+QyDmGmTQ7+ZXAOUsVJjsXo6o5sUMTOxjwAjuGmTQ706wcO+gYTjuBdiw7S8hXO4F2LDvoGE4706wcO4aZNDsY8AI7sUMTO7FDEzvR7bg5yY48O76J4jmELGI7I0P/OTJheTsBqQQ6MmF5OyND/zmELGI7voniOcmOPDvR7bg5sUMTOxjwAjvJjjw7gXYsO85XbTuFQVQ7qT+KO3+8bTsj7ZE7f7xtO6k/ijuFQVQ7zldtO4F2LDvJjjw7GPACOxejqjmGmTQ7voniOc5XbTtiwAk6I+2RO3DGGDqjiKM7j14dOqOIoztwxhg6I+2RO2LACTrOV207voniOYaZNDsXo6o5SxUmO9OsHDuELGI7hUFUOyPtkTvMNYY75rWtO/MJmTswBbk78wmZO+a1rTvMNYY7I+2RO4VBVDuELGI706wcO0sVJjuxQxM7+ZXAOegYTjsjQ/85qT+KO3DGGDrmta07l+0kOsmuxTvMVCc6ya7FO5ftJDrmta07cMYYOqk/ijsjQ/856BhOO/mVwDmxQxM7UrPwOoaZNDuBdiw7MmF5O3+8bTujiKM78wmZO8muxTu8u7A7BfzTO7y7sDvJrsU78wmZO6OIozt/vG07MmF5O4F2LDuGmTQ7UrPwOh8vGTvPkMg5S8hXOwGpBDoj7ZE7j14dOjAFuTvMVCc6BfzTO3pEKDoF/NM7zFQnOjAFuTuPXh06I+2ROwGpBDpLyFc7z5DIOR8vGTtSs/A6hpk0O4F2LDsyYXk7f7xtO6OIozvzCZk7ya7FO7y7sDsF/NM7vLuwO8muxTvzCZk7o4ijO3+8bTsyYXk7gXYsO4aZNDtSs/A6sUMTO/mVwDnoGE47I0P/Oak/ijtwxhg65rWtO5ftJDrJrsU7zFQnOsmuxTuX7SQ65rWtO3DGGDqpP4o7I0P/OegYTjv5lcA5sUMTO0sVJjvTrBw7hCxiO4VBVDsj7ZE7zDWGO+a1rTvzCZk7MAW5O/MJmTvmta07zDWGOyPtkTuFQVQ7hCxiO9OsHDtLFSY7F6OqOYaZNDu+ieI5zldtO2LACToj7ZE7cMYYOqOIozuPXh06o4ijO3DGGDoj7ZE7YsAJOs5XbTu+ieI5hpk0OxejqjkY8AI7yY48O4F2LDvOV207hUFUO6k/ijt/vG07I+2RO3+8bTupP4o7hUFUO85XbTuBdiw7yY48OxjwAjuxQxM70e24OcmOPDu+ieI5hCxiOyND/zkyYXk7AakEOjJheTsjQ/85hCxiO76J4jnJjjw70e24ObFDEzuxQxM7GPACO4aZNDvTrBw76BhOO4F2LDtLyFc7gXYsO+gYTjvTrBw7hpk0OxjwAjuxQxM7F6OqOUsVJjv5lcA5hpk0O8+QyDmGmTQ7+ZXAOUsVJjsXo6o5sUMTO1Kz8DofLxk7UrPwOrFDEzuZm505oeJuO5mbnTlE+KM5kXIfO9HtuDloHy07+ZXAOWgfLTvR7bg5kXIfO0T4ozmh4m47zJ6xObnskjvz4tA5Si2oO76J4jnxTrA7voniOUotqDvz4tA5ueySO8yesTmh4m47sUMTO9HtuDnJjjw7voniOYQsYjsjQ/85MmF5OwGpBDoyYXk7I0P/OYQsYju+ieI5yY48O9HtuDmxQxM7oeJuO9HtuDmfmqA7Lc3rOaK5zDtiwAk6CyPxO5PkEzpjqf87k+QTOgsj8TtiwAk6ornMOy3N6zmfmqA70e24OaHibjvMnrE5yY48Oy3N6zkyYXk7U9oOOmJRmjuPXh065rWtO+t/ITrmta07j14dOmJRmjtT2g46MmF5Oy3N6znJjjw7zJ6xOUT4ozm57JI7voniOaK5zDtT2g460NgHPOt/ITr02yU8zFQnOmuKMjzMVCc69NslPOt/ITrQ2Ac8U9oOOqK5zDu+ieI5ueySO0T4ozmRch878+LQOYQsYjtiwAk6YlGaO+t/ITrJrsU7ekQoOmhO5DtHISc6aE7kO3pEKDrJrsU7638hOmJRmjtiwAk6hCxiO/Pi0DmRch870e24OUotqDsjQ/85CyPxO49eHTr02yU8ekQoOsznUTy0FCM6DbBlPLQUIzrM51E8ekQoOvTbJTyPXh06CyPxOyND/zlKLag70e24OZmbnTloHy07voniOTJheTuT5BM65rWtO8xUJzpoTuQ7tBQjOlGlBjzB9Ro6UaUGPLQUIzpoTuQ7zFQnOua1rTuT5BM6MmF5O76J4jloHy07mZudOaHibjv5lcA58U6wOwGpBDpjqf87638hOmuKMjxHISc6DbBlPMH1GjoSUX08wfUaOg2wZTxHISc6a4oyPOt/ITpjqf87AakEOvFOsDv5lcA5oeJuO5mbnTloHy07voniOTJheTuT5BM65rWtO8xUJzpoTuQ7tBQjOlGlBjzB9Ro6UaUGPLQUIzpoTuQ7zFQnOua1rTuT5BM6MmF5O76J4jloHy07mZudOdHtuDlKLag7I0P/OQsj8TuPXh069NslPHpEKDrM51E8tBQjOg2wZTy0FCM6zOdRPHpEKDr02yU8j14dOgsj8TsjQ/85Si2oO9HtuDmRch878+LQOYQsYjtiwAk6YlGaO+t/ITrJrsU7ekQoOmhO5DtHISc6aE7kO3pEKDrJrsU7638hOmJRmjtiwAk6hCxiO/Pi0DmRch87RPijObnskju+ieI5ornMO1PaDjrQ2Ac8638hOvTbJTzMVCc6a4oyPMxUJzr02yU8638hOtDYBzxT2g46ornMO76J4jm57JI7RPijOcyesTnJjjw7Lc3rOTJheTtT2g46YlGaO49eHTrmta07638hOua1rTuPXh06YlGaO1PaDjoyYXk7Lc3rOcmOPDvMnrE5oeJuO9HtuDmfmqA7Lc3rOaK5zDtiwAk6CyPxO5PkEzpjqf87k+QTOgsj8TtiwAk6ornMOy3N6zmfmqA70e24OaHibjuxQxM70e24OcmOPDu+ieI5hCxiOyND/zkyYXk7AakEOjJheTsjQ/85hCxiO76J4jnJjjw70e24ObFDEzuh4m47zJ6xObnskjvz4tA5Si2oO76J4jnxTrA7voniOUotqDvz4tA5ueySO8yesTmh4m47RPijOZFyHzvR7bg5aB8tO/mVwDloHy070e24OZFyHztE+KM5mZudOaHibjuZm505sUMTOxejqjmRch87zJ6xOZFyHzsXo6o5sUMTO1Kz8DpLFSY7KfQOO8mOPDvTrBw7OQpFO9OsHDvJjjw7KfQOO0sVJjtSs/A6zJ6xOYaZNDvoi9k5S8hXO5Vn9TnOV207I0P/Oc5XbTuVZ/U5S8hXO+iL2TmGmTQ7zJ6xORjwAjvJjjw7gXYsO85XbTuFQVQ7qT+KO3+8bTsj7ZE7f7xtO6k/ijuFQVQ7zldtO4F2LDvJjjw7GPACO8yesTnJjjw7Lc3rOTJheTtT2g46YlGaO49eHTrmta07638hOua1rTuPXh06YlGaO1PaDjoyYXk7Lc3rOcmOPDvMnrE5UrPwOoaZNDuBdiw7MmF5O3+8bTujiKM78wmZO8muxTu8u7A7BfzTO7y7sDvJrsU78wmZO6OIozt/vG07MmF5O4F2LDuGmTQ7UrPwOksVJjvoi9k5zldtO1PaDjqjiKM7l+0kOgX80ztHISc6vCr3O7QUIzq8Kvc7RyEnOgX80zuX7SQ6o4ijO1PaDjrOV2076IvZOUsVJjuxQxM7KfQOO0vIVzuFQVQ7YlGaO/MJmTsF/NM7cprPO1GlBjwz6Pk7udwTPDPo+TtRpQY8cprPOwX80zvzCZk7YlGaO4VBVDtLyFc7KfQOO7FDEzsXo6o5yY48O5Vn9TmpP4o7j14dOsmuxTtHISc6UaUGPLIuDTr1AiQ8Bg3vOfUCJDyyLg06UaUGPEchJzrJrsU7j14dOqk/ijuVZ/U5yY48OxejqjmRch8706wcO85XbTt/vG075rWtO7y7sDu8Kvc7M+j5O/UCJDyRRhw82Es4PJFGHDz1AiQ8M+j5O7wq9zu8u7A75rWtO3+8bTvOV20706wcO5FyHzvMnrE5OQpFOyND/zkj7ZE7638hOgX80zu0FCM6udwTPAYN7znYSzg8Hz+uOdhLODwGDe85udwTPLQUIzoF/NM7638hOiPtkTsjQ/85OQpFO8yesTmRch8706wcO85XbTt/vG075rWtO7y7sDu8Kvc7M+j5O/UCJDyRRhw82Es4PJFGHDz1AiQ8M+j5O7wq9zu8u7A75rWtO3+8bTvOV20706wcO5FyHzsXo6o5yY48O5Vn9TmpP4o7j14dOsmuxTtHISc6UaUGPLIuDTr1AiQ8Bg3vOfUCJDyyLg06UaUGPEchJzrJrsU7j14dOqk/ijuVZ/U5yY48OxejqjmxQxM7KfQOO0vIVzuFQVQ7YlGaO/MJmTsF/NM7cprPO1GlBjwz6Pk7udwTPDPo+TtRpQY8cprPOwX80zvzCZk7YlGaO4VBVDtLyFc7KfQOO7FDEztLFSY76IvZOc5XbTtT2g46o4ijO5ftJDoF/NM7RyEnOrwq9zu0FCM6vCr3O0chJzoF/NM7l+0kOqOIoztT2g46zldtO+iL2TlLFSY7UrPwOoaZNDuBdiw7MmF5O3+8bTujiKM78wmZO8muxTu8u7A7BfzTO7y7sDvJrsU78wmZO6OIozt/vG07MmF5O4F2LDuGmTQ7UrPwOsyesTnJjjw7Lc3rOTJheTtT2g46YlGaO49eHTrmta07638hOua1rTuPXh06YlGaO1PaDjoyYXk7Lc3rOcmOPDvMnrE5GPACO8mOPDuBdiw7zldtO4VBVDupP4o7f7xtOyPtkTt/vG07qT+KO4VBVDvOV207gXYsO8mOPDsY8AI7zJ6xOYaZNDvoi9k5S8hXO5Vn9TnOV207I0P/Oc5XbTuVZ/U5S8hXO+iL2TmGmTQ7zJ6xOVKz8DpLFSY7KfQOO8mOPDvTrBw7OQpFO9OsHDvJjjw7KfQOO0sVJjtSs/A6sUMTOxejqjmRch87zJ6xOZFyHzsXo6o5sUMTO5mbnTnwmHg7zJ6xORe7jDv5lcA5ueySO/mVwDkXu4w7zJ6xOfCYeDtE+KM5SxUmO8+QyDk5CkU7voniOUvIVzstzes5S8hXO76J4jk5CkU7z5DIOUsVJjtE+KM5F6OqObnskjvoi9k5zQ65OyND/zmu0tc7YsAJOuHp4ztiwAk6rtLXOyND/znNDrk76IvZObnskjsXo6o5F6OqOYaZNDu+ieI5zldtO2LACToj7ZE7cMYYOqOIozuPXh06o4ijO3DGGDoj7ZE7YsAJOs5XbTu+ieI5hpk0OxejqjlE+KM5ueySO76J4jmiucw7U9oOOtDYBzzrfyE69NslPMxUJzprijI8zFQnOvTbJTzrfyE60NgHPFPaDjqiucw7voniObnskjtE+KM5SxUmO+iL2TnOV207U9oOOqOIozuX7SQ6BfzTO0chJzq8Kvc7tBQjOrwq9ztHISc6BfzTO5ftJDqjiKM7U9oOOs5XbTvoi9k5SxUmO/CYeDvPkMg5zQ65O2LACTrQ2Ac8l+0kOpcRQTy0FCM6ElF9PLIuDTpKDY08si4NOhJRfTy0FCM6lxFBPJftJDrQ2Ac8YsAJOs0OuTvPkMg58Jh4O8yesTk5CkU7I0P/OSPtkTvrfyE6BfzTO7QUIzq53BM8Bg3vOdhLODwfP6452Es4PAYN7zm53BM8tBQjOgX80zvrfyE6I+2ROyND/zk5CkU7zJ6xORe7jDu+ieI5rtLXO3DGGDr02yU8RyEnOhJRfTwGDe855Ry2PKSrITmH4NQ8pKshOeUctjwGDe85ElF9PEchJzr02yU8cMYYOq7S1zu+ieI5F7uMO/mVwDlLyFc7YsAJOqOIozvMVCc6vCr3O7IuDTrYSzg8pKshOS12djyHlcK4LXZ2PKSrITnYSzg8si4NOrwq9zvMVCc6o4ijO2LACTpLyFc7+ZXAOZmbnTm57JI7Lc3rOeHp4zuPXh06a4oyPLQUIzpKDY08Hz+uOYfg1DyHlcK4grb/PIeVwriH4NQ8Hz+uOUoNjTy0FCM6a4oyPI9eHTrh6eM7Lc3rObnskjuZm505+ZXAOUvIVztiwAk6o4ijO8xUJzq8Kvc7si4NOthLODykqyE5LXZ2PIeVwrgtdnY8pKshOdhLODyyLg06vCr3O8xUJzqjiKM7YsAJOkvIVzv5lcA5F7uMO76J4jmu0tc7cMYYOvTbJTxHISc6ElF9PAYN7znlHLY8pKshOYfg1DykqyE55Ry2PAYN7zkSUX08RyEnOvTbJTxwxhg6rtLXO76J4jkXu4w7zJ6xOTkKRTsjQ/85I+2RO+t/IToF/NM7tBQjOrncEzwGDe852Es4PB8/rjnYSzg8Bg3vObncEzy0FCM6BfzTO+t/IToj7ZE7I0P/OTkKRTvMnrE58Jh4O8+QyDnNDrk7YsAJOtDYBzyX7SQ6lxFBPLQUIzoSUX08si4NOkoNjTyyLg06ElF9PLQUIzqXEUE8l+0kOtDYBzxiwAk6zQ65O8+QyDnwmHg7SxUmO+iL2TnOV207U9oOOqOIozuX7SQ6BfzTO0chJzq8Kvc7tBQjOrwq9ztHISc6BfzTO5ftJDqjiKM7U9oOOs5XbTvoi9k5SxUmO0T4ozm57JI7voniOaK5zDtT2g460NgHPOt/ITr02yU8zFQnOmuKMjzMVCc69NslPOt/ITrQ2Ac8U9oOOqK5zDu+ieI5ueySO0T4ozkXo6o5hpk0O76J4jnOV207YsAJOiPtkTtwxhg6o4ijO49eHTqjiKM7cMYYOiPtkTtiwAk6zldtO76J4jmGmTQ7F6OqORejqjm57JI76IvZOc0OuTsjQ/85rtLXO2LACTrh6eM7YsAJOq7S1zsjQ/85zQ65O+iL2Tm57JI7F6OqOUT4ozlLFSY7z5DIOTkKRTu+ieI5S8hXOy3N6zlLyFc7voniOTkKRTvPkMg5SxUmO0T4oznwmHg7zJ6xORe7jDv5lcA5ueySO/mVwDkXu4w7zJ6xOfCYeDuZm505sUMTO1Kz8DofLxk7UrPwOrFDEzuxQxM7zJ6xOWgfLTvPkMg5yY48O/Pi0DnJjjw7z5DIOWgfLTvMnrE5sUMTO5FyHzsp9A47OQpFO4F2LDuELGI7ecc+O85XbTt5xz47hCxiO4F2LDs5CkU7KfQOO5FyHzuZm505SxUmO/Pi0DlLyFc7I0P/OdoxgztT2g46I+2RO5PkEzoj7ZE7U9oOOtoxgzsjQ/85S8hXO/Pi0DlLFSY7mZudOUsVJjvTrBw7hCxiO4VBVDsj7ZE7zDWGO+a1rTvzCZk7MAW5O/MJmTvmta07zDWGOyPtkTuFQVQ7hCxiO9OsHDtLFSY7kXIfO/Pi0DmELGI7YsAJOmJRmjvrfyE6ya7FO3pEKDpoTuQ7RyEnOmhO5Dt6RCg6ya7FO+t/ITpiUZo7YsAJOoQsYjvz4tA5kXIfO7FDEzsp9A47S8hXO4VBVDtiUZo78wmZOwX80ztyms87UaUGPDPo+Tu53BM8M+j5O1GlBjxyms87BfzTO/MJmTtiUZo7hUFUO0vIVzsp9A47sUMTO8yesTk5CkU7I0P/OSPtkTvrfyE6BfzTO7QUIzq53BM8Bg3vOdhLODwfP6452Es4PAYN7zm53BM8tBQjOgX80zvrfyE6I+2ROyND/zk5CkU7zJ6xOWgfLTuBdiw72jGDO8w1hjvJrsU7cprPO7ncEzyRRhw8AqtSPFC8UDwtdnY8ULxQPAKrUjyRRhw8udwTPHKazzvJrsU7zDWGO9oxgzuBdiw7aB8tO7FDEzvPkMg5hCxiO1PaDjrmta07ekQoOlGlBjwGDe85AqtSPIeVwriu5pQ8e/Xaua7mlDyHlcK4AqtSPAYN7zlRpQY8ekQoOua1rTtT2g46hCxiO8+QyDmxQxM7UrPwOsmOPDt5xz47I+2RO/MJmTtoTuQ7M+j5O9hLODxQvFA8ruaUPNOwnTxepbw807CdPK7mlDxQvFA82Es4PDPo+TtoTuQ78wmZOyPtkTt5xz47yY48O1Kz8DofLxk78+LQOc5XbTuT5BM6MAW5O0chJzq53BM8Hz+uOS12djx79dq5XqW8PA7KVbpepbw8e/XauS12djwfP645udwTPEchJzowBbk7k+QTOs5XbTvz4tA5Hy8ZO1Kz8DrJjjw7ecc+OyPtkTvzCZk7aE7kOzPo+TvYSzg8ULxQPK7mlDzTsJ08XqW8PNOwnTyu5pQ8ULxQPNhLODwz6Pk7aE7kO/MJmTsj7ZE7ecc+O8mOPDtSs/A6sUMTO8+QyDmELGI7U9oOOua1rTt6RCg6UaUGPAYN7zkCq1I8h5XCuK7mlDx79dq5ruaUPIeVwrgCq1I8Bg3vOVGlBjx6RCg65rWtO1PaDjqELGI7z5DIObFDEztoHy07gXYsO9oxgzvMNYY7ya7FO3Kazzu53BM8kUYcPAKrUjxQvFA8LXZ2PFC8UDwCq1I8kUYcPLncEzxyms87ya7FO8w1hjvaMYM7gXYsO2gfLTvMnrE5OQpFOyND/zkj7ZE7638hOgX80zu0FCM6udwTPAYN7znYSzg8Hz+uOdhLODwGDe85udwTPLQUIzoF/NM7638hOiPtkTsjQ/85OQpFO8yesTmxQxM7KfQOO0vIVzuFQVQ7YlGaO/MJmTsF/NM7cprPO1GlBjwz6Pk7udwTPDPo+TtRpQY8cprPOwX80zvzCZk7YlGaO4VBVDtLyFc7KfQOO7FDEzuRch878+LQOYQsYjtiwAk6YlGaO+t/ITrJrsU7ekQoOmhO5DtHISc6aE7kO3pEKDrJrsU7638hOmJRmjtiwAk6hCxiO/Pi0DmRch87SxUmO9OsHDuELGI7hUFUOyPtkTvMNYY75rWtO/MJmTswBbk78wmZO+a1rTvMNYY7I+2RO4VBVDuELGI706wcO0sVJjuZm505SxUmO/Pi0DlLyFc7I0P/OdoxgztT2g46I+2RO5PkEzoj7ZE7U9oOOtoxgzsjQ/85S8hXO/Pi0DlLFSY7mZudOZFyHzsp9A47OQpFO4F2LDuELGI7ecc+O85XbTt5xz47hCxiO4F2LDs5CkU7KfQOO5FyHzuxQxM7zJ6xOWgfLTvPkMg5yY48O/Pi0DnJjjw7z5DIOWgfLTvMnrE5sUMTO7FDEztSs/A6Hy8ZO1Kz8DqxQxM7sUMTOxejqjmRch87zJ6xOZFyHzsXo6o5sUMTOxejqjkXu4w7z5DIOZ+aoDvoi9k5Si2oO+iL2TmfmqA7z5DIORe7jDsXo6o5sUMTO9HtuDnJjjw7voniOYQsYjsjQ/85MmF5OwGpBDoyYXk7I0P/OYQsYju+ieI5yY48O9HtuDmxQxM78Jh4O/mVwDlKLag7lWf1Oa7S1ztT2g46Y6n/O3DGGDrQ2Ac8cMYYOmOp/ztT2g46rtLXO5Vn9TlKLag7+ZXAOfCYeDuxQxM7+ZXAOegYTjsjQ/85qT+KO3DGGDrmta07l+0kOsmuxTvMVCc6ya7FO5ftJDrmta07cMYYOqk/ijsjQ/856BhOO/mVwDmxQxM70e24OUotqDsjQ/85CyPxO49eHTr02yU8ekQoOsznUTy0FCM6DbBlPLQUIzrM51E8ekQoOvTbJTyPXh06CyPxOyND/zlKLag70e24ORejqjnJjjw7lWf1Oak/ijuPXh06ya7FO0chJzpRpQY8si4NOvUCJDwGDe859QIkPLIuDTpRpQY8RyEnOsmuxTuPXh06qT+KO5Vn9TnJjjw7F6OqORe7jDu+ieI5rtLXO3DGGDr02yU8RyEnOhJRfTwGDe855Ry2PKSrITmH4NQ8pKshOeUctjwGDe85ElF9PEchJzr02yU8cMYYOq7S1zu+ieI5F7uMO7FDEzvPkMg5hCxiO1PaDjrmta07ekQoOlGlBjwGDe85AqtSPIeVwriu5pQ8e/Xaua7mlDyHlcK4AqtSPAYN7zlRpQY8ekQoOua1rTtT2g46hCxiO8+QyDmxQxM7F6OqOZ+aoDsjQ/85Y6n/O5ftJDrM51E8si4NOuUctjyHlcK4amsfPQ7KVboR8FE9DspVumprHz2HlcK45Ry2PLIuDTrM51E8l+0kOmOp/zsjQ/85n5qgOxejqjmRch876IvZOTJheTtwxhg6ya7FO7QUIzr1AiQ8pKshOa7mlDwOylW62IsBPY+RqrrYiwE9DspVuq7mlDykqyE59QIkPLQUIzrJrsU7cMYYOjJheTvoi9k5kXIfO8yesTlKLag7AakEOtDYBzzMVCc6DbBlPAYN7zmH4NQ8e/XauRHwUT2Pkaq6+B2ZPY+RqroR8FE9e/XauYfg1DwGDe85DbBlPMxUJzrQ2Ac8AakEOkotqDvMnrE5kXIfO+iL2TkyYXk7cMYYOsmuxTu0FCM69QIkPKSrITmu5pQ8DspVutiLAT2Pkaq62IsBPQ7KVbqu5pQ8pKshOfUCJDy0FCM6ya7FO3DGGDoyYXk76IvZOZFyHzsXo6o5n5qgOyND/zljqf87l+0kOsznUTyyLg065Ry2PIeVwrhqax89DspVuhHwUT0OylW6amsfPYeVwrjlHLY8si4NOsznUTyX7SQ6Y6n/OyND/zmfmqA7F6OqObFDEzvPkMg5hCxiO1PaDjrmta07ekQoOlGlBjwGDe85AqtSPIeVwriu5pQ8e/Xaua7mlDyHlcK4AqtSPAYN7zlRpQY8ekQoOua1rTtT2g46hCxiO8+QyDmxQxM7F7uMO76J4jmu0tc7cMYYOvTbJTxHISc6ElF9PAYN7znlHLY8pKshOYfg1DykqyE55Ry2PAYN7zkSUX08RyEnOvTbJTxwxhg6rtLXO76J4jkXu4w7F6OqOcmOPDuVZ/U5qT+KO49eHTrJrsU7RyEnOlGlBjyyLg069QIkPAYN7zn1AiQ8si4NOlGlBjxHISc6ya7FO49eHTqpP4o7lWf1OcmOPDsXo6o50e24OUotqDsjQ/85CyPxO49eHTr02yU8ekQoOsznUTy0FCM6DbBlPLQUIzrM51E8ekQoOvTbJTyPXh06CyPxOyND/zlKLag70e24ObFDEzv5lcA56BhOOyND/zmpP4o7cMYYOua1rTuX7SQ6ya7FO8xUJzrJrsU7l+0kOua1rTtwxhg6qT+KOyND/znoGE47+ZXAObFDEzvwmHg7+ZXAOUotqDuVZ/U5rtLXO1PaDjpjqf87cMYYOtDYBzxwxhg6Y6n/O1PaDjqu0tc7lWf1OUotqDv5lcA58Jh4O7FDEzvR7bg5yY48O76J4jmELGI7I0P/OTJheTsBqQQ6MmF5OyND/zmELGI7voniOcmOPDvR7bg5sUMTOxejqjkXu4w7z5DIOZ+aoDvoi9k5Si2oO+iL2TmfmqA7z5DIORe7jDsXo6o5sUMTOxejqjmRch87zJ6xOZFyHzsXo6o5sUMTO1Kz8DqRch87GPACO0sVJjsY8AI7kXIfO1Kz8DqZm505kXIfO/mVwDnJjjw76IvZOegYTju+ieI56BhOO+iL2TnJjjw7+ZXAOZFyHzuZm505UrPwOmgfLTvTrBw7S8hXO3nHPjsyYXk7hUFUO9oxgzuFQVQ7MmF5O3nHPjtLyFc706wcO2gfLTtSs/A6F6OqOYaZNDu+ieI5zldtO2LACToj7ZE7cMYYOqOIozuPXh06o4ijO3DGGDoj7ZE7YsAJOs5XbTu+ieI5hpk0OxejqjlSs/A6hpk0O4F2LDsyYXk7f7xtO6OIozvzCZk7ya7FO7y7sDsF/NM7vLuwO8muxTvzCZk7o4ijO3+8bTsyYXk7gXYsO4aZNDtSs/A6mZudOWgfLTu+ieI5MmF5O5PkEzrmta07zFQnOmhO5Du0FCM6UaUGPMH1GjpRpQY8tBQjOmhO5DvMVCc65rWtO5PkEzoyYXk7voniOWgfLTuZm505kXIfO9OsHDvOV207f7xtO+a1rTu8u7A7vCr3OzPo+Tv1AiQ8kUYcPNhLODyRRhw89QIkPDPo+Tu8Kvc7vLuwO+a1rTt/vG07zldtO9OsHDuRch87+ZXAOUvIVztiwAk6o4ijO8xUJzq8Kvc7si4NOthLODykqyE5LXZ2PIeVwrgtdnY8pKshOdhLODyyLg06vCr3O8xUJzqjiKM7YsAJOkvIVzv5lcA5UrPwOsmOPDt5xz47I+2RO/MJmTtoTuQ7M+j5O9hLODxQvFA8ruaUPNOwnTxepbw807CdPK7mlDxQvFA82Es4PDPo+TtoTuQ78wmZOyPtkTt5xz47yY48O1Kz8DqRch876IvZOTJheTtwxhg6ya7FO7QUIzr1AiQ8pKshOa7mlDwOylW62IsBPY+RqrrYiwE9DspVuq7mlDykqyE59QIkPLQUIzrJrsU7cMYYOjJheTvoi9k5kXIfOxjwAjvoGE47hUFUO6OIozu8u7A7UaUGPJFGHDwtdnY807CdPNiLAT3aLxo9kbpnPdovGj3YiwE907CdPC12djyRRhw8UaUGPLy7sDujiKM7hUFUO+gYTjsY8AI7SxUmO76J4jnaMYM7j14dOgX80zvB9Ro62Es4PIeVwrhepbw8j5GqupG6Zz2Fv9Q4kbpnPY+Rqrpepbw8h5XCuNhLODzB9Ro6BfzTO49eHTraMYM7voniOUsVJjsY8AI76BhOO4VBVDujiKM7vLuwO1GlBjyRRhw8LXZ2PNOwnTzYiwE92i8aPZG6Zz3aLxo92IsBPdOwnTwtdnY8kUYcPFGlBjy8u7A7o4ijO4VBVDvoGE47GPACO5FyHzvoi9k5MmF5O3DGGDrJrsU7tBQjOvUCJDykqyE5ruaUPA7KVbrYiwE9j5GqutiLAT0OylW6ruaUPKSrITn1AiQ8tBQjOsmuxTtwxhg6MmF5O+iL2TmRch87UrPwOsmOPDt5xz47I+2RO/MJmTtoTuQ7M+j5O9hLODxQvFA8ruaUPNOwnTxepbw807CdPK7mlDxQvFA82Es4PDPo+TtoTuQ78wmZOyPtkTt5xz47yY48O1Kz8Dr5lcA5S8hXO2LACTqjiKM7zFQnOrwq9zuyLg062Es4PKSrITktdnY8h5XCuC12djykqyE52Es4PLIuDTq8Kvc7zFQnOqOIoztiwAk6S8hXO/mVwDmRch8706wcO85XbTt/vG075rWtO7y7sDu8Kvc7M+j5O/UCJDyRRhw82Es4PJFGHDz1AiQ8M+j5O7wq9zu8u7A75rWtO3+8bTvOV20706wcO5FyHzuZm505aB8tO76J4jkyYXk7k+QTOua1rTvMVCc6aE7kO7QUIzpRpQY8wfUaOlGlBjy0FCM6aE7kO8xUJzrmta07k+QTOjJheTu+ieI5aB8tO5mbnTlSs/A6hpk0O4F2LDsyYXk7f7xtO6OIozvzCZk7ya7FO7y7sDsF/NM7vLuwO8muxTvzCZk7o4ijO3+8bTsyYXk7gXYsO4aZNDtSs/A6F6OqOYaZNDu+ieI5zldtO2LACToj7ZE7cMYYOqOIozuPXh06o4ijO3DGGDoj7ZE7YsAJOs5XbTu+ieI5hpk0OxejqjlSs/A6aB8tO9OsHDtLyFc7ecc+OzJheTuFQVQ72jGDO4VBVDsyYXk7ecc+O0vIVzvTrBw7aB8tO1Kz8DqZm505kXIfO/mVwDnJjjw76IvZOegYTju+ieI56BhOO+iL2TnJjjw7+ZXAOZFyHzuZm505UrPwOpFyHzsY8AI7SxUmOxjwAjuRch87UrPwOpmbnTkfLxk7zJ6xOUsVJjvR7bg5SxUmO8yesTkfLxk7mZudOaHibjvMnrE5ueySO/Pi0DlKLag7voniOfFOsDu+ieI5Si2oO/Pi0Dm57JI7zJ6xOaHibjsfLxk7+ZXAOTkKRTstzes5zldtOwGpBDraMYM7YsAJOtoxgzsBqQQ6zldtOy3N6zk5CkU7+ZXAOR8vGTumcoE7z5DIOfFOsDsjQ/854enjO5PkEzrQ2Ac8j14dOmq+EDyPXh060NgHPJPkEzrh6eM7I0P/OfFOsDvPkMg5pnKBOx8vGTvPkMg5S8hXOwGpBDoj7ZE7j14dOjAFuTvMVCc6BfzTO3pEKDoF/NM7zFQnOjAFuTuPXh06I+2ROwGpBDpLyFc7z5DIOR8vGTuh4m47+ZXAOfFOsDsBqQQ6Y6n/O+t/ITprijI8RyEnOg2wZTzB9Ro6ElF9PMH1GjoNsGU8RyEnOmuKMjzrfyE6Y6n/OwGpBDrxTrA7+ZXAOaHibjvMnrE5OQpFOyND/zkj7ZE7638hOgX80zu0FCM6udwTPAYN7znYSzg8Hz+uOdhLODwGDe85udwTPLQUIzoF/NM7638hOiPtkTsjQ/85OQpFO8yesTmZm505ueySOy3N6znh6eM7j14dOmuKMjy0FCM6Sg2NPB8/rjmH4NQ8h5XCuIK2/zyHlcK4h+DUPB8/rjlKDY08tBQjOmuKMjyPXh064enjOy3N6zm57JI7mZudOR8vGTvz4tA5zldtO5PkEzowBbk7RyEnOrncEzwfP645LXZ2PHv12rlepbw8DspVul6lvDx79dq5LXZ2PB8/rjm53BM8RyEnOjAFuTuT5BM6zldtO/Pi0DkfLxk7zJ6xOUotqDsBqQQ60NgHPMxUJzoNsGU8Bg3vOYfg1Dx79dq5EfBRPY+Rqrr4HZk9j5GquhHwUT179dq5h+DUPAYN7zkNsGU8zFQnOtDYBzwBqQQ6Si2oO8yesTlLFSY7voniOdoxgzuPXh06BfzTO8H1GjrYSzg8h5XCuF6lvDyPkaq6kbpnPYW/1DiRumc9j5Gqul6lvDyHlcK42Es4PMH1GjoF/NM7j14dOtoxgzu+ieI5SxUmO9HtuDnxTrA7YsAJOmq+EDx6RCg6ElF9PB8/rjmCtv88DspVuvgdmT2Fv9Q4hb/UOPgdmT0OylW6grb/PB8/rjkSUX08ekQoOmq+EDxiwAk68U6wO9HtuDlLFSY7voniOdoxgzuPXh06BfzTO8H1GjrYSzg8h5XCuF6lvDyPkaq6kbpnPYW/1DiRumc9j5Gqul6lvDyHlcK42Es4PMH1GjoF/NM7j14dOtoxgzu+ieI5SxUmO8yesTlKLag7AakEOtDYBzzMVCc6DbBlPAYN7zmH4NQ8e/XauRHwUT2Pkaq6+B2ZPY+RqroR8FE9e/XauYfg1DwGDe85DbBlPMxUJzrQ2Ac8AakEOkotqDvMnrE5Hy8ZO/Pi0DnOV207k+QTOjAFuTtHISc6udwTPB8/rjktdnY8e/XauV6lvDwOylW6XqW8PHv12rktdnY8Hz+uObncEzxHISc6MAW5O5PkEzrOV2078+LQOR8vGTuZm505ueySOy3N6znh6eM7j14dOmuKMjy0FCM6Sg2NPB8/rjmH4NQ8h5XCuIK2/zyHlcK4h+DUPB8/rjlKDY08tBQjOmuKMjyPXh064enjOy3N6zm57JI7mZudOcyesTk5CkU7I0P/OSPtkTvrfyE6BfzTO7QUIzq53BM8Bg3vOdhLODwfP6452Es4PAYN7zm53BM8tBQjOgX80zvrfyE6I+2ROyND/zk5CkU7zJ6xOaHibjv5lcA58U6wOwGpBDpjqf87638hOmuKMjxHISc6DbBlPMH1GjoSUX08wfUaOg2wZTxHISc6a4oyPOt/ITpjqf87AakEOvFOsDv5lcA5oeJuOx8vGTvPkMg5S8hXOwGpBDoj7ZE7j14dOjAFuTvMVCc6BfzTO3pEKDoF/NM7zFQnOjAFuTuPXh06I+2ROwGpBDpLyFc7z5DIOR8vGTumcoE7z5DIOfFOsDsjQ/854enjO5PkEzrQ2Ac8j14dOmq+EDyPXh060NgHPJPkEzrh6eM7I0P/OfFOsDvPkMg5pnKBOx8vGTv5lcA5OQpFOy3N6znOV207AakEOtoxgztiwAk62jGDOwGpBDrOV207Lc3rOTkKRTv5lcA5Hy8ZO6HibjvMnrE5ueySO/Pi0DlKLag7voniOfFOsDu+ieI5Si2oO/Pi0Dm57JI7zJ6xOaHibjuZm505Hy8ZO8yesTlLFSY70e24OUsVJjvMnrE5Hy8ZO5mbnTlSs/A6kXIfOxjwAjtLFSY7GPACO5FyHztSs/A6mZudOZFyHzv5lcA5yY48O+iL2TnoGE47voniOegYTjvoi9k5yY48O/mVwDmRch87mZudOVKz8DpoHy0706wcO0vIVzt5xz47MmF5O4VBVDvaMYM7hUFUOzJheTt5xz47S8hXO9OsHDtoHy07UrPwOhejqjmGmTQ7voniOc5XbTtiwAk6I+2RO3DGGDqjiKM7j14dOqOIoztwxhg6I+2RO2LACTrOV207voniOYaZNDsXo6o5UrPwOoaZNDuBdiw7MmF5O3+8bTujiKM78wmZO8muxTu8u7A7BfzTO7y7sDvJrsU78wmZO6OIozt/vG07MmF5O4F2LDuGmTQ7UrPwOpmbnTloHy07voniOTJheTuT5BM65rWtO8xUJzpoTuQ7tBQjOlGlBjzB9Ro6UaUGPLQUIzpoTuQ7zFQnOua1rTuT5BM6MmF5O76J4jloHy07mZudOZFyHzvTrBw7zldtO3+8bTvmta07vLuwO7wq9zsz6Pk79QIkPJFGHDzYSzg8kUYcPPUCJDwz6Pk7vCr3O7y7sDvmta07f7xtO85XbTvTrBw7kXIfO/mVwDlLyFc7YsAJOqOIozvMVCc6vCr3O7IuDTrYSzg8pKshOS12djyHlcK4LXZ2PKSrITnYSzg8si4NOrwq9zvMVCc6o4ijO2LACTpLyFc7+ZXAOVKz8DrJjjw7ecc+OyPtkTvzCZk7aE7kOzPo+TvYSzg8ULxQPK7mlDzTsJ08XqW8PNOwnTyu5pQ8ULxQPNhLODwz6Pk7aE7kO/MJmTsj7ZE7ecc+O8mOPDtSs/A6kXIfO+iL2TkyYXk7cMYYOsmuxTu0FCM69QIkPKSrITmu5pQ8DspVutiLAT2Pkaq62IsBPQ7KVbqu5pQ8pKshOfUCJDy0FCM6ya7FO3DGGDoyYXk76IvZOZFyHzsY8AI76BhOO4VBVDujiKM7vLuwO1GlBjyRRhw8LXZ2PNOwnTzYiwE92i8aPZG6Zz3aLxo92IsBPdOwnTwtdnY8kUYcPFGlBjy8u7A7o4ijO4VBVDvoGE47GPACO0sVJju+ieI52jGDO49eHToF/NM7wfUaOthLODyHlcK4XqW8PI+RqrqRumc9hb/UOJG6Zz2Pkaq6XqW8PIeVwrjYSzg8wfUaOgX80zuPXh062jGDO76J4jlLFSY7GPACO+gYTjuFQVQ7o4ijO7y7sDtRpQY8kUYcPC12djzTsJ082IsBPdovGj2Rumc92i8aPdiLAT3TsJ08LXZ2PJFGHDxRpQY8vLuwO6OIozuFQVQ76BhOOxjwAjuRch876IvZOTJheTtwxhg6ya7FO7QUIzr1AiQ8pKshOa7mlDwOylW62IsBPY+RqrrYiwE9DspVuq7mlDykqyE59QIkPLQUIzrJrsU7cMYYOjJheTvoi9k5kXIfO1Kz8DrJjjw7ecc+OyPtkTvzCZk7aE7kOzPo+TvYSzg8ULxQPK7mlDzTsJ08XqW8PNOwnTyu5pQ8ULxQPNhLODwz6Pk7aE7kO/MJmTsj7ZE7ecc+O8mOPDtSs/A6+ZXAOUvIVztiwAk6o4ijO8xUJzq8Kvc7si4NOthLODykqyE5LXZ2PIeVwrgtdnY8pKshOdhLODyyLg06vCr3O8xUJzqjiKM7YsAJOkvIVzv5lcA5kXIfO9OsHDvOV207f7xtO+a1rTu8u7A7vCr3OzPo+Tv1AiQ8kUYcPNhLODyRRhw89QIkPDPo+Tu8Kvc7vLuwO+a1rTt/vG07zldtO9OsHDuRch87mZudOWgfLTu+ieI5MmF5O5PkEzrmta07zFQnOmhO5Du0FCM6UaUGPMH1GjpRpQY8tBQjOmhO5DvMVCc65rWtO5PkEzoyYXk7voniOWgfLTuZm505UrPwOoaZNDuBdiw7MmF5O3+8bTujiKM78wmZO8muxTu8u7A7BfzTO7y7sDvJrsU78wmZO6OIozt/vG07MmF5O4F2LDuGmTQ7UrPwOhejqjmGmTQ7voniOc5XbTtiwAk6I+2RO3DGGDqjiKM7j14dOqOIoztwxhg6I+2RO2LACTrOV207voniOYaZNDsXo6o5UrPwOmgfLTvTrBw7S8hXO3nHPjsyYXk7hUFUO9oxgzuFQVQ7MmF5O3nHPjtLyFc706wcO2gfLTtSs/A6mZudOZFyHzv5lcA5yY48O+iL2TnoGE47voniOegYTjvoi9k5yY48O/mVwDmRch87mZudOVKz8DqRch87GPACO0sVJjsY8AI7kXIfO1Kz8DqxQxM7F6OqOZFyHzvMnrE5kXIfOxejqjmxQxM7F6OqORe7jDvPkMg5n5qgO+iL2TlKLag76IvZOZ+aoDvPkMg5F7uMOxejqjmxQxM70e24OcmOPDu+ieI5hCxiOyND/zkyYXk7AakEOjJheTsjQ/85hCxiO76J4jnJjjw70e24ObFDEzvwmHg7+ZXAOUotqDuVZ/U5rtLXO1PaDjpjqf87cMYYOtDYBzxwxhg6Y6n/O1PaDjqu0tc7lWf1OUotqDv5lcA58Jh4O7FDEzv5lcA56BhOOyND/zmpP4o7cMYYOua1rTuX7SQ6ya7FO8xUJzrJrsU7l+0kOua1rTtwxhg6qT+KOyND/znoGE47+ZXAObFDEzvR7bg5Si2oOyND/zkLI/E7j14dOvTbJTx6RCg6zOdRPLQUIzoNsGU8tBQjOsznUTx6RCg69NslPI9eHToLI/E7I0P/OUotqDvR7bg5F6OqOcmOPDuVZ/U5qT+KO49eHTrJrsU7RyEnOlGlBjyyLg069QIkPAYN7zn1AiQ8si4NOlGlBjxHISc6ya7FO49eHTqpP4o7lWf1OcmOPDsXo6o5F7uMO76J4jmu0tc7cMYYOvTbJTxHISc6ElF9PAYN7znlHLY8pKshOYfg1DykqyE55Ry2PAYN7zkSUX08RyEnOvTbJTxwxhg6rtLXO76J4jkXu4w7sUMTO8+QyDmELGI7U9oOOua1rTt6RCg6UaUGPAYN7zkCq1I8h5XCuK7mlDx79dq5ruaUPIeVwrgCq1I8Bg3vOVGlBjx6RCg65rWtO1PaDjqELGI7z5DIObFDEzsXo6o5n5qgOyND/zljqf87l+0kOsznUTyyLg065Ry2PIeVwrhqax89DspVuhHwUT0OylW6amsfPYeVwrjlHLY8si4NOsznUTyX7SQ6Y6n/OyND/zmfmqA7F6OqOZFyHzvoi9k5MmF5O3DGGDrJrsU7tBQjOvUCJDykqyE5ruaUPA7KVbrYiwE9j5GqutiLAT0OylW6ruaUPKSrITn1AiQ8tBQjOsmuxTtwxhg6MmF5O+iL2TmRch87zJ6xOUotqDsBqQQ60NgHPMxUJzoNsGU8Bg3vOYfg1Dx79dq5EfBRPY+Rqrr4HZk9j5GquhHwUT179dq5h+DUPAYN7zkNsGU8zFQnOtDYBzwBqQQ6Si2oO8yesTmRch876IvZOTJheTtwxhg6ya7FO7QUIzr1AiQ8pKshOa7mlDwOylW62IsBPY+RqrrYiwE9DspVuq7mlDykqyE59QIkPLQUIzrJrsU7cMYYOjJheTvoi9k5kXIfOxejqjmfmqA7I0P/OWOp/zuX7SQ6zOdRPLIuDTrlHLY8h5XCuGprHz0OylW6EfBRPQ7KVbpqax89h5XCuOUctjyyLg06zOdRPJftJDpjqf87I0P/OZ+aoDsXo6o5sUMTO8+QyDmELGI7U9oOOua1rTt6RCg6UaUGPAYN7zkCq1I8h5XCuK7mlDx79dq5ruaUPIeVwrgCq1I8Bg3vOVGlBjx6RCg65rWtO1PaDjqELGI7z5DIObFDEzsXu4w7voniOa7S1ztwxhg69NslPEchJzoSUX08Bg3vOeUctjykqyE5h+DUPKSrITnlHLY8Bg3vORJRfTxHISc69NslPHDGGDqu0tc7voniORe7jDsXo6o5yY48O5Vn9TmpP4o7j14dOsmuxTtHISc6UaUGPLIuDTr1AiQ8Bg3vOfUCJDyyLg06UaUGPEchJzrJrsU7j14dOqk/ijuVZ/U5yY48OxejqjnR7bg5Si2oOyND/zkLI/E7j14dOvTbJTx6RCg6zOdRPLQUIzoNsGU8tBQjOsznUTx6RCg69NslPI9eHToLI/E7I0P/OUotqDvR7bg5sUMTO/mVwDnoGE47I0P/Oak/ijtwxhg65rWtO5ftJDrJrsU7zFQnOsmuxTuX7SQ65rWtO3DGGDqpP4o7I0P/OegYTjv5lcA5sUMTO/CYeDv5lcA5Si2oO5Vn9Tmu0tc7U9oOOmOp/ztwxhg60NgHPHDGGDpjqf87U9oOOq7S1zuVZ/U5Si2oO/mVwDnwmHg7sUMTO9HtuDnJjjw7voniOYQsYjsjQ/85MmF5OwGpBDoyYXk7I0P/OYQsYju+ieI5yY48O9HtuDmxQxM7F6OqORe7jDvPkMg5n5qgO+iL2TlKLag76IvZOZ+aoDvPkMg5F7uMOxejqjmxQxM7F6OqOZFyHzvMnrE5kXIfOxejqjmxQxM7sUMTO1Kz8DofLxk7UrPwOrFDEzuxQxM7zJ6xOWgfLTvPkMg5yY48O/Pi0DnJjjw7z5DIOWgfLTvMnrE5sUMTO5FyHzsp9A47OQpFO4F2LDuELGI7ecc+O85XbTt5xz47hCxiO4F2LDs5CkU7KfQOO5FyHzuZm505SxUmO/Pi0DlLyFc7I0P/OdoxgztT2g46I+2RO5PkEzoj7ZE7U9oOOtoxgzsjQ/85S8hXO/Pi0DlLFSY7mZudOUsVJjvTrBw7hCxiO4VBVDsj7ZE7zDWGO+a1rTvzCZk7MAW5O/MJmTvmta07zDWGOyPtkTuFQVQ7hCxiO9OsHDtLFSY7kXIfO/Pi0DmELGI7YsAJOmJRmjvrfyE6ya7FO3pEKDpoTuQ7RyEnOmhO5Dt6RCg6ya7FO+t/ITpiUZo7YsAJOoQsYjvz4tA5kXIfO7FDEzsp9A47S8hXO4VBVDtiUZo78wmZOwX80ztyms87UaUGPDPo+Tu53BM8M+j5O1GlBjxyms87BfzTO/MJmTtiUZo7hUFUO0vIVzsp9A47sUMTO8yesTk5CkU7I0P/OSPtkTvrfyE6BfzTO7QUIzq53BM8Bg3vOdhLODwfP6452Es4PAYN7zm53BM8tBQjOgX80zvrfyE6I+2ROyND/zk5CkU7zJ6xOWgfLTuBdiw72jGDO8w1hjvJrsU7cprPO7ncEzyRRhw8AqtSPFC8UDwtdnY8ULxQPAKrUjyRRhw8udwTPHKazzvJrsU7zDWGO9oxgzuBdiw7aB8tO7FDEzvPkMg5hCxiO1PaDjrmta07ekQoOlGlBjwGDe85AqtSPIeVwriu5pQ8e/Xaua7mlDyHlcK4AqtSPAYN7zlRpQY8ekQoOua1rTtT2g46hCxiO8+QyDmxQxM7UrPwOsmOPDt5xz47I+2RO/MJmTtoTuQ7M+j5O9hLODxQvFA8ruaUPNOwnTxepbw807CdPK7mlDxQvFA82Es4PDPo+TtoTuQ78wmZOyPtkTt5xz47yY48O1Kz8DofLxk78+LQOc5XbTuT5BM6MAW5O0chJzq53BM8Hz+uOS12djx79dq5XqW8PA7KVbpepbw8e/XauS12djwfP645udwTPEchJzowBbk7k+QTOs5XbTvz4tA5Hy8ZO1Kz8DrJjjw7ecc+OyPtkTvzCZk7aE7kOzPo+TvYSzg8ULxQPK7mlDzTsJ08XqW8PNOwnTyu5pQ8ULxQPNhLODwz6Pk7aE7kO/MJmTsj7ZE7ecc+O8mOPDtSs/A6sUMTO8+QyDmELGI7U9oOOua1rTt6RCg6UaUGPAYN7zkCq1I8h5XCuK7mlDx79dq5ruaUPIeVwrgCq1I8Bg3vOVGlBjx6RCg65rWtO1PaDjqELGI7z5DIObFDEztoHy07gXYsO9oxgzvMNYY7ya7FO3Kazzu53BM8kUYcPAKrUjxQvFA8LXZ2PFC8UDwCq1I8kUYcPLncEzxyms87ya7FO8w1hjvaMYM7gXYsO2gfLTvMnrE5OQpFOyND/zkj7ZE7638hOgX80zu0FCM6udwTPAYN7znYSzg8Hz+uOdhLODwGDe85udwTPLQUIzoF/NM7638hOiPtkTsjQ/85OQpFO8yesTmxQxM7KfQOO0vIVzuFQVQ7YlGaO/MJmTsF/NM7cprPO1GlBjwz6Pk7udwTPDPo+TtRpQY8cprPOwX80zvzCZk7YlGaO4VBVDtLyFc7KfQOO7FDEzuRch878+LQOYQsYjtiwAk6YlGaO+t/ITrJrsU7ekQoOmhO5DtHISc6aE7kO3pEKDrJrsU7638hOmJRmjtiwAk6hCxiO/Pi0DmRch87SxUmO9OsHDuELGI7hUFUOyPtkTvMNYY75rWtO/MJmTswBbk78wmZO+a1rTvMNYY7I+2RO4VBVDuELGI706wcO0sVJjuZm505SxUmO/Pi0DlLyFc7I0P/OdoxgztT2g46I+2RO5PkEzoj7ZE7U9oOOtoxgzsjQ/85S8hXO/Pi0DlLFSY7mZudOZFyHzsp9A47OQpFO4F2LDuELGI7ecc+O85XbTt5xz47hCxiO4F2LDs5CkU7KfQOO5FyHzuxQxM7zJ6xOWgfLTvPkMg5yY48O/Pi0DnJjjw7z5DIOWgfLTvMnrE5sUMTO7FDEztSs/A6Hy8ZO1Kz8DqxQxM7mZudOfCYeDvMnrE5F7uMO/mVwDm57JI7+ZXAORe7jDvMnrE58Jh4O0T4ozlLFSY7z5DIOTkKRTu+ieI5S8hXOy3N6zlLyFc7voniOTkKRTvPkMg5SxUmO0T4ozkXo6o5ueySO+iL2TnNDrk7I0P/Oa7S1ztiwAk64enjO2LACTqu0tc7I0P/Oc0OuTvoi9k5ueySOxejqjkXo6o5hpk0O76J4jnOV207YsAJOiPtkTtwxhg6o4ijO49eHTqjiKM7cMYYOiPtkTtiwAk6zldtO76J4jmGmTQ7F6OqOUT4ozm57JI7voniOaK5zDtT2g460NgHPOt/ITr02yU8zFQnOmuKMjzMVCc69NslPOt/ITrQ2Ac8U9oOOqK5zDu+ieI5ueySO0T4ozlLFSY76IvZOc5XbTtT2g46o4ijO5ftJDoF/NM7RyEnOrwq9zu0FCM6vCr3O0chJzoF/NM7l+0kOqOIoztT2g46zldtO+iL2TlLFSY78Jh4O8+QyDnNDrk7YsAJOtDYBzyX7SQ6lxFBPLQUIzoSUX08si4NOkoNjTyyLg06ElF9PLQUIzqXEUE8l+0kOtDYBzxiwAk6zQ65O8+QyDnwmHg7zJ6xOTkKRTsjQ/85I+2RO+t/IToF/NM7tBQjOrncEzwGDe852Es4PB8/rjnYSzg8Bg3vObncEzy0FCM6BfzTO+t/IToj7ZE7I0P/OTkKRTvMnrE5F7uMO76J4jmu0tc7cMYYOvTbJTxHISc6ElF9PAYN7znlHLY8pKshOYfg1DykqyE55Ry2PAYN7zkSUX08RyEnOvTbJTxwxhg6rtLXO76J4jkXu4w7+ZXAOUvIVztiwAk6o4ijO8xUJzq8Kvc7si4NOthLODykqyE5LXZ2PIeVwrgtdnY8pKshOdhLODyyLg06vCr3O8xUJzqjiKM7YsAJOkvIVzv5lcA5mZudObnskjstzes54enjO49eHTprijI8tBQjOkoNjTwfP645h+DUPIeVwriCtv88h5XCuIfg1DwfP645Sg2NPLQUIzprijI8j14dOuHp4zstzes5ueySO5mbnTn5lcA5S8hXO2LACTqjiKM7zFQnOrwq9zuyLg062Es4PKSrITktdnY8h5XCuC12djykqyE52Es4PLIuDTq8Kvc7zFQnOqOIoztiwAk6S8hXO/mVwDkXu4w7voniOa7S1ztwxhg69NslPEchJzoSUX08Bg3vOeUctjykqyE5h+DUPKSrITnlHLY8Bg3vORJRfTxHISc69NslPHDGGDqu0tc7voniORe7jDvMnrE5OQpFOyND/zkj7ZE7638hOgX80zu0FCM6udwTPAYN7znYSzg8Hz+uOdhLODwGDe85udwTPLQUIzoF/NM7638hOiPtkTsjQ/85OQpFO8yesTnwmHg7z5DIOc0OuTtiwAk60NgHPJftJDqXEUE8tBQjOhJRfTyyLg06Sg2NPLIuDToSUX08tBQjOpcRQTyX7SQ60NgHPGLACTrNDrk7z5DIOfCYeDtLFSY76IvZOc5XbTtT2g46o4ijO5ftJDoF/NM7RyEnOrwq9zu0FCM6vCr3O0chJzoF/NM7l+0kOqOIoztT2g46zldtO+iL2TlLFSY7RPijObnskju+ieI5ornMO1PaDjrQ2Ac8638hOvTbJTzMVCc6a4oyPMxUJzr02yU8638hOtDYBzxT2g46ornMO76J4jm57JI7RPijORejqjmGmTQ7voniOc5XbTtiwAk6I+2RO3DGGDqjiKM7j14dOqOIoztwxhg6I+2RO2LACTrOV207voniOYaZNDsXo6o5F6OqObnskjvoi9k5zQ65OyND/zmu0tc7YsAJOuHp4ztiwAk6rtLXOyND/znNDrk76IvZObnskjsXo6o5RPijOUsVJjvPkMg5OQpFO76J4jlLyFc7Lc3rOUvIVzu+ieI5OQpFO8+QyDlLFSY7RPijOfCYeDvMnrE5F7uMO/mVwDm57JI7+ZXAORe7jDvMnrE58Jh4O5mbnTmxQxM7F6OqOZFyHzvMnrE5kXIfOxejqjmxQxM7UrPwOksVJjsp9A47yY48O9OsHDs5CkU706wcO8mOPDsp9A47SxUmO1Kz8DrMnrE5hpk0O+iL2TlLyFc7lWf1Oc5XbTsjQ/85zldtO5Vn9TlLyFc76IvZOYaZNDvMnrE5GPACO8mOPDuBdiw7zldtO4VBVDupP4o7f7xtOyPtkTt/vG07qT+KO4VBVDvOV207gXYsO8mOPDsY8AI7zJ6xOcmOPDstzes5MmF5O1PaDjpiUZo7j14dOua1rTvrfyE65rWtO49eHTpiUZo7U9oOOjJheTstzes5yY48O8yesTlSs/A6hpk0O4F2LDsyYXk7f7xtO6OIozvzCZk7ya7FO7y7sDsF/NM7vLuwO8muxTvzCZk7o4ijO3+8bTsyYXk7gXYsO4aZNDtSs/A6SxUmO+iL2TnOV207U9oOOqOIozuX7SQ6BfzTO0chJzq8Kvc7tBQjOrwq9ztHISc6BfzTO5ftJDqjiKM7U9oOOs5XbTvoi9k5SxUmO7FDEzsp9A47S8hXO4VBVDtiUZo78wmZOwX80ztyms87UaUGPDPo+Tu53BM8M+j5O1GlBjxyms87BfzTO/MJmTtiUZo7hUFUO0vIVzsp9A47sUMTOxejqjnJjjw7lWf1Oak/ijuPXh06ya7FO0chJzpRpQY8si4NOvUCJDwGDe859QIkPLIuDTpRpQY8RyEnOsmuxTuPXh06qT+KO5Vn9TnJjjw7F6OqOZFyHzvTrBw7zldtO3+8bTvmta07vLuwO7wq9zsz6Pk79QIkPJFGHDzYSzg8kUYcPPUCJDwz6Pk7vCr3O7y7sDvmta07f7xtO85XbTvTrBw7kXIfO8yesTk5CkU7I0P/OSPtkTvrfyE6BfzTO7QUIzq53BM8Bg3vOdhLODwfP6452Es4PAYN7zm53BM8tBQjOgX80zvrfyE6I+2ROyND/zk5CkU7zJ6xOZFyHzvTrBw7zldtO3+8bTvmta07vLuwO7wq9zsz6Pk79QIkPJFGHDzYSzg8kUYcPPUCJDwz6Pk7vCr3O7y7sDvmta07f7xtO85XbTvTrBw7kXIfOxejqjnJjjw7lWf1Oak/ijuPXh06ya7FO0chJzpRpQY8si4NOvUCJDwGDe859QIkPLIuDTpRpQY8RyEnOsmuxTuPXh06qT+KO5Vn9TnJjjw7F6OqObFDEzsp9A47S8hXO4VBVDtiUZo78wmZOwX80ztyms87UaUGPDPo+Tu53BM8M+j5O1GlBjxyms87BfzTO/MJmTtiUZo7hUFUO0vIVzsp9A47sUMTO0sVJjvoi9k5zldtO1PaDjqjiKM7l+0kOgX80ztHISc6vCr3O7QUIzq8Kvc7RyEnOgX80zuX7SQ6o4ijO1PaDjrOV2076IvZOUsVJjtSs/A6hpk0O4F2LDsyYXk7f7xtO6OIozvzCZk7ya7FO7y7sDsF/NM7vLuwO8muxTvzCZk7o4ijO3+8bTsyYXk7gXYsO4aZNDtSs/A6zJ6xOcmOPDstzes5MmF5O1PaDjpiUZo7j14dOua1rTvrfyE65rWtO49eHTpiUZo7U9oOOjJheTstzes5yY48O8yesTkY8AI7yY48O4F2LDvOV207hUFUO6k/ijt/vG07I+2RO3+8bTupP4o7hUFUO85XbTuBdiw7yY48OxjwAjvMnrE5hpk0O+iL2TlLyFc7lWf1Oc5XbTsjQ/85zldtO5Vn9TlLyFc76IvZOYaZNDvMnrE5UrPwOksVJjsp9A47yY48O9OsHDs5CkU706wcO8mOPDsp9A47SxUmO1Kz8DqxQxM7F6OqOZFyHzvMnrE5kXIfOxejqjmxQxM7mZudOaHibjuZm505RPijOZFyHzvR7bg5aB8tO/mVwDloHy070e24OZFyHztE+KM5oeJuO8yesTm57JI78+LQOUotqDu+ieI58U6wO76J4jlKLag78+LQObnskjvMnrE5oeJuO7FDEzvR7bg5yY48O76J4jmELGI7I0P/OTJheTsBqQQ6MmF5OyND/zmELGI7voniOcmOPDvR7bg5sUMTO6HibjvR7bg5n5qgOy3N6zmiucw7YsAJOgsj8TuT5BM6Y6n/O5PkEzoLI/E7YsAJOqK5zDstzes5n5qgO9HtuDmh4m47zJ6xOcmOPDstzes5MmF5O1PaDjpiUZo7j14dOua1rTvrfyE65rWtO49eHTpiUZo7U9oOOjJheTstzes5yY48O8yesTlE+KM5ueySO76J4jmiucw7U9oOOtDYBzzrfyE69NslPMxUJzprijI8zFQnOvTbJTzrfyE60NgHPFPaDjqiucw7voniObnskjtE+KM5kXIfO/Pi0DmELGI7YsAJOmJRmjvrfyE6ya7FO3pEKDpoTuQ7RyEnOmhO5Dt6RCg6ya7FO+t/ITpiUZo7YsAJOoQsYjvz4tA5kXIfO9HtuDlKLag7I0P/OQsj8TuPXh069NslPHpEKDrM51E8tBQjOg2wZTy0FCM6zOdRPHpEKDr02yU8j14dOgsj8TsjQ/85Si2oO9HtuDmZm505aB8tO76J4jkyYXk7k+QTOua1rTvMVCc6aE7kO7QUIzpRpQY8wfUaOlGlBjy0FCM6aE7kO8xUJzrmta07k+QTOjJheTu+ieI5aB8tO5mbnTmh4m47+ZXAOfFOsDsBqQQ6Y6n/O+t/ITprijI8RyEnOg2wZTzB9Ro6ElF9PMH1GjoNsGU8RyEnOmuKMjzrfyE6Y6n/OwGpBDrxTrA7+ZXAOaHibjuZm505aB8tO76J4jkyYXk7k+QTOua1rTvMVCc6aE7kO7QUIzpRpQY8wfUaOlGlBjy0FCM6aE7kO8xUJzrmta07k+QTOjJheTu+ieI5aB8tO5mbnTnR7bg5Si2oOyND/zkLI/E7j14dOvTbJTx6RCg6zOdRPLQUIzoNsGU8tBQjOsznUTx6RCg69NslPI9eHToLI/E7I0P/OUotqDvR7bg5kXIfO/Pi0DmELGI7YsAJOmJRmjvrfyE6ya7FO3pEKDpoTuQ7RyEnOmhO5Dt6RCg6ya7FO+t/ITpiUZo7YsAJOoQsYjvz4tA5kXIfO0T4ozm57JI7voniOaK5zDtT2g460NgHPOt/ITr02yU8zFQnOmuKMjzMVCc69NslPOt/ITrQ2Ac8U9oOOqK5zDu+ieI5ueySO0T4oznMnrE5yY48Oy3N6zkyYXk7U9oOOmJRmjuPXh065rWtO+t/ITrmta07j14dOmJRmjtT2g46MmF5Oy3N6znJjjw7zJ6xOaHibjvR7bg5n5qgOy3N6zmiucw7YsAJOgsj8TuT5BM6Y6n/O5PkEzoLI/E7YsAJOqK5zDstzes5n5qgO9HtuDmh4m47sUMTO9HtuDnJjjw7voniOYQsYjsjQ/85MmF5OwGpBDoyYXk7I0P/OYQsYju+ieI5yY48O9HtuDmxQxM7oeJuO8yesTm57JI78+LQOUotqDu+ieI58U6wO76J4jlKLag78+LQObnskjvMnrE5oeJuO0T4ozmRch870e24OWgfLTv5lcA5aB8tO9HtuDmRch87RPijOZmbnTmh4m47mZudObFDEztSs/A6Hy8ZO1Kz8DqxQxM7F6OqOUsVJjv5lcA5hpk0O8+QyDmGmTQ7+ZXAOUsVJjsXo6o5sUMTOxjwAjuGmTQ706wcO+gYTjuBdiw7S8hXO4F2LDvoGE4706wcO4aZNDsY8AI7sUMTO7FDEzvR7bg5yY48O76J4jmELGI7I0P/OTJheTsBqQQ6MmF5OyND/zmELGI7voniOcmOPDvR7bg5sUMTOxjwAjvJjjw7gXYsO85XbTuFQVQ7qT+KO3+8bTsj7ZE7f7xtO6k/ijuFQVQ7zldtO4F2LDvJjjw7GPACOxejqjmGmTQ7voniOc5XbTtiwAk6I+2RO3DGGDqjiKM7j14dOqOIoztwxhg6I+2RO2LACTrOV207voniOYaZNDsXo6o5SxUmO9OsHDuELGI7hUFUOyPtkTvMNYY75rWtO/MJmTswBbk78wmZO+a1rTvMNYY7I+2RO4VBVDuELGI706wcO0sVJjuxQxM7+ZXAOegYTjsjQ/85qT+KO3DGGDrmta07l+0kOsmuxTvMVCc6ya7FO5ftJDrmta07cMYYOqk/ijsjQ/856BhOO/mVwDmxQxM7UrPwOoaZNDuBdiw7MmF5O3+8bTujiKM78wmZO8muxTu8u7A7BfzTO7y7sDvJrsU78wmZO6OIozt/vG07MmF5O4F2LDuGmTQ7UrPwOh8vGTvPkMg5S8hXOwGpBDoj7ZE7j14dOjAFuTvMVCc6BfzTO3pEKDoF/NM7zFQnOjAFuTuPXh06I+2ROwGpBDpLyFc7z5DIOR8vGTtSs/A6hpk0O4F2LDsyYXk7f7xtO6OIozvzCZk7ya7FO7y7sDsF/NM7vLuwO8muxTvzCZk7o4ijO3+8bTsyYXk7gXYsO4aZNDtSs/A6sUMTO/mVwDnoGE47I0P/Oak/ijtwxhg65rWtO5ftJDrJrsU7zFQnOsmuxTuX7SQ65rWtO3DGGDqpP4o7I0P/OegYTjv5lcA5sUMTO0sVJjvTrBw7hCxiO4VBVDsj7ZE7zDWGO+a1rTvzCZk7MAW5O/MJmTvmta07zDWGOyPtkTuFQVQ7hCxiO9OsHDtLFSY7F6OqOYaZNDu+ieI5zldtO2LACToj7ZE7cMYYOqOIozuPXh06o4ijO3DGGDoj7ZE7YsAJOs5XbTu+ieI5hpk0OxejqjkY8AI7yY48O4F2LDvOV207hUFUO6k/ijt/vG07I+2RO3+8bTupP4o7hUFUO85XbTuBdiw7yY48OxjwAjuxQxM70e24OcmOPDu+ieI5hCxiOyND/zkyYXk7AakEOjJheTsjQ/85hCxiO76J4jnJjjw70e24ObFDEzuxQxM7GPACO4aZNDvTrBw76BhOO4F2LDtLyFc7gXYsO+gYTjvTrBw7hpk0OxjwAjuxQxM7F6OqOUsVJjv5lcA5hpk0O8+QyDmGmTQ7+ZXAOUsVJjsXo6o5sUMTO1Kz8DofLxk7UrPwOrFDEzuZm5058Jh4OxejqjmmcoE7F6OqOfCYeDuZm505F6OqOUsVJjv5lcA5hpk0O8+QyDmGmTQ7+ZXAOUsVJjsXo6o5oeJuO8yesTm57JI78+LQOUotqDu+ieI58U6wO76J4jlKLag78+LQObnskjvMnrE5oeJuO8yesTmGmTQ76IvZOUvIVzuVZ/U5zldtOyND/znOV207lWf1OUvIVzvoi9k5hpk0O8yesTkXo6o5ueySO+iL2TnNDrk7I0P/Oa7S1ztiwAk64enjO2LACTqu0tc7I0P/Oc0OuTvoi9k5ueySOxejqjmZm505SxUmO/Pi0DlLyFc7I0P/OdoxgztT2g46I+2RO5PkEzoj7ZE7U9oOOtoxgzsjQ/85S8hXO/Pi0DlLFSY7mZudOfCYeDv5lcA5Si2oO5Vn9Tmu0tc7U9oOOmOp/ztwxhg60NgHPHDGGDpjqf87U9oOOq7S1zuVZ/U5Si2oO/mVwDnwmHg7F6OqOYaZNDu+ieI5zldtO2LACToj7ZE7cMYYOqOIozuPXh06o4ijO3DGGDoj7ZE7YsAJOs5XbTu+ieI5hpk0OxejqjmmcoE7z5DIOfFOsDsjQ/854enjO5PkEzrQ2Ac8j14dOmq+EDyPXh060NgHPJPkEzrh6eM7I0P/OfFOsDvPkMg5pnKBOxejqjmGmTQ7voniOc5XbTtiwAk6I+2RO3DGGDqjiKM7j14dOqOIoztwxhg6I+2RO2LACTrOV207voniOYaZNDsXo6o58Jh4O/mVwDlKLag7lWf1Oa7S1ztT2g46Y6n/O3DGGDrQ2Ac8cMYYOmOp/ztT2g46rtLXO5Vn9TlKLag7+ZXAOfCYeDuZm505SxUmO/Pi0DlLyFc7I0P/OdoxgztT2g46I+2RO5PkEzoj7ZE7U9oOOtoxgzsjQ/85S8hXO/Pi0DlLFSY7mZudORejqjm57JI76IvZOc0OuTsjQ/85rtLXO2LACTrh6eM7YsAJOq7S1zsjQ/85zQ65O+iL2Tm57JI7F6OqOcyesTmGmTQ76IvZOUvIVzuVZ/U5zldtOyND/znOV207lWf1OUvIVzvoi9k5hpk0O8yesTmh4m47zJ6xObnskjvz4tA5Si2oO76J4jnxTrA7voniOUotqDvz4tA5ueySO8yesTmh4m47F6OqOUsVJjv5lcA5hpk0O8+QyDmGmTQ7+ZXAOUsVJjsXo6o5mZudOfCYeDsXo6o5pnKBOxejqjnwmHg7mZudObFDEztSs/A6Hy8ZO1Kz8DqxQxM7RPijOZFyHzvR7bg5aB8tO/mVwDloHy070e24OZFyHztE+KM5UrPwOksVJjsp9A47yY48O9OsHDs5CkU706wcO8mOPDsp9A47SxUmO1Kz8DpE+KM5SxUmO8+QyDk5CkU7voniOUvIVzstzes5S8hXO76J4jk5CkU7z5DIOUsVJjtE+KM5kXIfOyn0Djs5CkU7gXYsO4QsYjt5xz47zldtO3nHPjuELGI7gXYsOzkKRTsp9A47kXIfO7FDEzvR7bg5yY48O76J4jmELGI7I0P/OTJheTsBqQQ6MmF5OyND/zmELGI7voniOcmOPDvR7bg5sUMTO1Kz8DpoHy0706wcO0vIVzt5xz47MmF5O4VBVDvaMYM7hUFUOzJheTt5xz47S8hXO9OsHDtoHy07UrPwOh8vGTv5lcA5OQpFOy3N6znOV207AakEOtoxgztiwAk62jGDOwGpBDrOV207Lc3rOTkKRTv5lcA5Hy8ZO1Kz8DpoHy0706wcO0vIVzt5xz47MmF5O4VBVDvaMYM7hUFUOzJheTt5xz47S8hXO9OsHDtoHy07UrPwOrFDEzvR7bg5yY48O76J4jmELGI7I0P/OTJheTsBqQQ6MmF5OyND/zmELGI7voniOcmOPDvR7bg5sUMTO5FyHzsp9A47OQpFO4F2LDuELGI7ecc+O85XbTt5xz47hCxiO4F2LDs5CkU7KfQOO5FyHztE+KM5SxUmO8+QyDk5CkU7voniOUvIVzstzes5S8hXO76J4jk5CkU7z5DIOUsVJjtE+KM5UrPwOksVJjsp9A47yY48O9OsHDs5CkU706wcO8mOPDsp9A47SxUmO1Kz8DpE+KM5kXIfO9HtuDloHy07+ZXAOWgfLTvR7bg5kXIfO0T4ozmxQxM7UrPwOh8vGTtSs/A6sUMTO5mbnTmh4m47mZudObFDEzsXo6o5kXIfO8yesTmRch87F6OqObFDEzvwmHg7zJ6xORe7jDv5lcA5ueySO/mVwDkXu4w7zJ6xOfCYeDuxQxM7zJ6xOWgfLTvPkMg5yY48O/Pi0DnJjjw7z5DIOWgfLTvMnrE5sUMTOxejqjkXu4w7z5DIOZ+aoDvoi9k5Si2oO+iL2TmfmqA7z5DIORe7jDsXo6o5mZudOZFyHzv5lcA5yY48O+iL2TnoGE47voniOegYTjvoi9k5yY48O/mVwDmRch87mZudOaHibjvMnrE5ueySO/Pi0DlKLag7voniOfFOsDu+ieI5Si2oO/Pi0Dm57JI7zJ6xOaHibjuZm505kXIfO/mVwDnJjjw76IvZOegYTju+ieI56BhOO+iL2TnJjjw7+ZXAOZFyHzuZm505F6OqORe7jDvPkMg5n5qgO+iL2TlKLag76IvZOZ+aoDvPkMg5F7uMOxejqjmxQxM7zJ6xOWgfLTvPkMg5yY48O/Pi0DnJjjw7z5DIOWgfLTvMnrE5sUMTO/CYeDvMnrE5F7uMO/mVwDm57JI7+ZXAORe7jDvMnrE58Jh4O7FDEzsXo6o5kXIfO8yesTmRch87F6OqObFDEzuZm505oeJuO5mbnTmZm505sUMTO1Kz8DofLxk7UrPwOrFDEzuxQxM7F6OqOZFyHzvMnrE5kXIfOxejqjmxQxM7UrPwOpFyHzsY8AI7SxUmOxjwAjuRch87UrPwOpmbnTkfLxk7zJ6xOUsVJjvR7bg5SxUmO8yesTkfLxk7mZudOVKz8DqRch87GPACO0sVJjsY8AI7kXIfO1Kz8DqxQxM7F6OqOZFyHzvMnrE5kXIfOxejqjmxQxM7sUMTO1Kz8DofLxk7UrPwOrFDEzuZm505\",\"U_im\":\"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\",\"u0_imag\":0.007982493960435978,\"absorptive\":true,\"n_reflections\":6762,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", + "Al2O3 (corundum)": "{\"name\":\"Al2O3\",\"spacegroup\":\"R-3c (167)\",\"pointgroup\":\"-3m\",\"cell\":[[4.7602,0.0,0.0],[-2.380099999999999,4.122454127094685,0.0],[0.0,0.0,12.9933]],\"recip\":[[0.2100752069240788,0.12128697726768317,0.0],[0.0,0.24257395453536645,0.0],[0.0,0.0,0.07696274233643494]],\"positions_frac\":[[0.0,0.0,0.3523],[0.0,0.0,0.1477],[0.6666666666666665,0.3333333333333333,0.6856333333333333],[0.6666666666666665,0.3333333333333333,0.48103333333333326],[0.3333333333333333,0.6666666666666666,0.018966666666666576],[0.3333333333333333,0.6666666666666666,0.8143666666666665],[0.0,0.0,0.6476999999999999],[0.0,0.0,0.8523000000000001],[0.6666666666666665,0.3333333333333333,0.9810333333333334],[0.6666666666666665,0.3333333333333333,0.18563333333333332],[0.3333333333333333,0.6666666666666666,0.3143666666666667],[0.3333333333333333,0.6666666666666666,0.5189666666666666],[0.3064,0.0,0.25],[0.0,0.3064,0.25],[0.6936,0.6936,0.25],[0.9730666666666666,0.3333333333333333,0.5833333333333333],[0.6666666666666666,0.6397333333333333,0.5833333333333333],[0.3602666666666666,0.02693333333333331,0.5833333333333333],[0.6397333333333332,0.6666666666666666,0.9166666666666667],[0.33333333333333337,0.9730666666666666,0.9166666666666667],[0.0269333333333333,0.3602666666666666,0.9166666666666667],[0.6936,0.0,0.75],[0.0,0.6936,0.75],[0.3064,0.3064,0.75],[0.3602666666666666,0.3333333333333333,0.08333333333333333],[0.6666666666666666,0.02693333333333331,0.08333333333333333],[0.9730666666666666,0.6397333333333333,0.08333333333333333],[0.0269333333333333,0.6666666666666666,0.41666666666666663],[0.3333333333333333,0.3602666666666666,0.41666666666666663],[0.6397333333333332,0.9730666666666666,0.41666666666666663]],\"numbers\":[13,13,13,13,13,13,13,13,13,13,13,13,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8],\"symbols\":[\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\"],\"colors\":[[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051]],\"radii\":[1.21,1.21,1.21,1.21,1.21,1.21,1.21,1.21,1.21,1.21,1.21,1.21,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66],\"hkl_i16\":\"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+P/8/wIA+P/8/wUA+P/8/wgA+P/8/wsA+P/8/w4A+P/8/xEA+P/8/xQA+P/9/+n/+P/9/+z/+P/9/+//+P/9//L/+P/9//X/+P/9//j/+P/9//v/+P/9//7/+P/9/wEA+P/9/wQA+P/9/wcA+P/9/woA+P/9/w0A+P/9/xAA+P/9/xMA+P/9/xYA+P/+/+j/+P/+/+v/+P/+/+7/+P/+//H/+P/+//T/+P/+//f/+P/+//r/+P/+//3/+P/+/wAA+P/+/wMA+P/+/wYA+P/+/wkA+P/+/wwA+P/+/w8A+P/+/xIA+P/+/xUA+P/+/xgA+P///+T/+P///+f/+P///+r/+P///+3/+P////D/+P////P/+P////b/+P////n/+P////z/+P//////+P///wIA+P///wUA+P///wgA+P///wsA+P///w4A+P///xEA+P///xQA+P///xcA+P///xoA+P8AAOP/+P8AAOb/+P8AAOn/+P8AAOz/+P8AAO//+P8AAPL/+P8AAPX/+P8AAPj/+P8AAPv/+P8AAP7/+P8AAAEA+P8AAAQA+P8AAAcA+P8AAAoA+P8AAA0A+P8AABAA+P8AABMA+P8AABYA+P8AABkA+P8AABwA+P8BAOL/+P8BAOX/+P8BAOj/+P8BAOv/+P8BAO7/+P8BAPH/+P8BAPT/+P8BAPf/+P8BAPr/+P8BAP3/+P8BAAAA+P8BAAMA+P8BAAYA+P8BAAkA+P8BAAwA+P8BAA8A+P8BABIA+P8BABUA+P8BABgA+P8BABsA+P8BAB4A+P8CAOH/+P8CAOT/+P8CAOf/+P8CAOr/+P8CAO3/+P8CAPD/+P8CAPP/+P8CAPb/+P8CAPn/+P8CAPz/+P8CAP//+P8CAAIA+P8CAAUA+P8CAAgA+P8CAAsA+P8CAA4A+P8CABEA+P8CABQA+P8CABcA+P8CABoA+P8CAB0A+P8DAOD/+P8DAOP/+P8DAOb/+P8DAOn/+P8DAOz/+P8DAO//+P8DAPL/+P8DAPX/+P8DAPj/+P8DAPv/+P8DAP7/+P8DAAEA+P8DAAQA+P8DAAcA+P8DAAoA+P8DAA0A+P8DABAA+P8DABMA+P8DABYA+P8DABkA+P8DABwA+P8DAB8A+P8EAOL/+P8EAOX/+P8EAOj/+P8EAOv/+P8EAO7/+P8EAPH/+P8EAPT/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+f8AAAsA+f8AAA4A+f8AABEA+f8AABQA+f8AABcA+f8AABoA+f8AAB0A+f8AACAA+f8BAOD/+f8BAOP/+f8BAOb/+f8BAOn/+f8BAOz/+f8BAO//+f8BAPL/+f8BAPX/+f8BAPj/+f8BAPv/+f8BAP7/+f8BAAEA+f8BAAQA+f8BAAcA+f8BAAoA+f8BAA0A+f8BABAA+f8BABMA+f8BABYA+f8BABkA+f8BABwA+f8BAB8A+f8CAN//+f8CAOL/+f8CAOX/+f8CAOj/+f8CAOv/+f8CAO7/+f8CAPH/+f8CAPT/+f8CAPf/+f8CAPr/+f8CAP3/+f8CAAAA+f8CAAMA+f8CAAYA+f8CAAkA+f8CAAwA+f8CAA8A+f8CABIA+f8CABUA+f8CABgA+f8CABsA+f8CAB4A+f8CACEA+f8DAOH/+f8DAOT/+f8DAOf/+f8DAOr/+f8DAO3/+f8DAPD/+f8DAPP/+f8DAPb/+f8DAPn/+f8DAPz/+f8DAP//+f8DAAIA+f8DAAUA+f8DAAgA+f8DAAsA+f8DAA4A+f8DABEA+f8DABQA+f8DABcA+f8DABoA+f8DAB0A+f8DACAA+f8EAOD/+f8EAOP/+f8EAOb/+f8EAOn/+f8EAOz/+f8EAO//+f8EAPL/+f8EAPX/+f8EAPj/+f8EAPv/+f8EAP7/+f8EAAEA+f8EAAQA+f8EAAcA+f8EAAoA+f8EAA0A+f8EABAA+f8EABMA+f8EABYA+f8EABkA+f8EABwA+f8EAB8A+f8FAN//+f8FAOL/+f8FAOX/+f8FAOj/+f8FAOv/+f8FAO7/+f8FAPH/+f8FAPT/+f8FAPf/+f8FAPr/+f8FAP3/+f8FAAAA+f8FAAMA+f8FAAYA+f8FAAkA+f8FAAwA+f8FAA8A+f8FABIA+f8FABUA+f8FABgA+f8FABsA+f8FAB4A+f8FACEA+f8GAOH/+f8GAOT/+f8GAOf/+f8GAOr/+f8GAO3/+f8GAPD/+f8GAPP/+f8GAPb/+f8GAPn/+f8GAPz/+f8GAP//+f8GAAIA+f8GAAUA+f8GAAgA+f8GAAsA+f8GAA4A+f8GABEA+f8GABQA+f8GABcA+f8GABoA+f8GAB0A+f8GACAA+f8HAOD/+f8HAOP/+f8HAOb/+f8HAOn/+f8HAOz/+f8HAO//+f8HAPL/+f8HAPX/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+v8CAPL/+v8CAPX/+v8CAPj/+v8CAPv/+v8CAP7/+v8CAAEA+v8CAAQA+v8CAAcA+v8CAAoA+v8CAA0A+v8CABAA+v8CABMA+v8CABYA+v8CABkA+v8CABwA+v8CAB8A+v8CACIA+v8DAN//+v8DAOL/+v8DAOX/+v8DAOj/+v8DAOv/+v8DAO7/+v8DAPH/+v8DAPT/+v8DAPf/+v8DAPr/+v8DAP3/+v8DAAAA+v8DAAMA+v8DAAYA+v8DAAkA+v8DAAwA+v8DAA8A+v8DABIA+v8DABUA+v8DABgA+v8DABsA+v8DAB4A+v8DACEA+v8EAN7/+v8EAOH/+v8EAOT/+v8EAOf/+v8EAOr/+v8EAO3/+v8EAPD/+v8EAPP/+v8EAPb/+v8EAPn/+v8EAPz/+v8EAP//+v8EAAIA+v8EAAUA+v8EAAgA+v8EAAsA+v8EAA4A+v8EABEA+v8EABQA+v8EABcA+v8EABoA+v8EAB0A+v8EACAA+v8EACMA+v8FAOD/+v8FAOP/+v8FAOb/+v8FAOn/+v8FAOz/+v8FAO//+v8FAPL/+v8FAPX/+v8FAPj/+v8FAPv/+v8FAP7/+v8FAAEA+v8FAAQA+v8FAAcA+v8FAAoA+v8FAA0A+v8FABAA+v8FABMA+v8FABYA+v8FABkA+v8FABwA+v8FAB8A+v8FACIA+v8GAN//+v8GAOL/+v8GAOX/+v8GAOj/+v8GAOv/+v8GAO7/+v8GAPH/+v8GAPT/+v8GAPf/+v8GAPr/+v8GAP3/+v8GAAAA+v8GAAMA+v8GAAYA+v8GAAkA+v8GAAwA+v8GAA8A+v8GABIA+v8GABUA+v8GABgA+v8GABsA+v8GAB4A+v8GACEA+v8HAOH/+v8HAOT/+v8HAOf/+v8HAOr/+v8HAO3/+v8HAPD/+v8HAPP/+v8HAPb/+v8HAPn/+v8HAPz/+v8HAP//+v8HAAIA+v8HAAUA+v8HAAgA+v8HAAsA+v8HAA4A+v8HABEA+v8HABQA+v8HABcA+v8HABoA+v8HAB0A+v8HACAA+v8IAOP/+v8IAOb/+v8IAOn/+v8IAOz/+v8IAO//+v8IAPL/+v8IAPX/+v8IAPj/+v8IAPv/+v8IAP7/+v8IAAEA+v8IAAQA+v8IAAcA+v8IAAoA+v8IAA0A+v8IABAA+v8IABMA+v8IABYA+v8IABkA+v8IABwA+v8IAB8A+v8JAOX/+v8JAOj/+v8JAOv/+v8JAO7/+v8JAPH/+v8JAPT/+v8JAPf/+v8JAPr/+v8JAP3/+v8JAAAA+v8JAAMA+v8JAAYA+v8JAAkA+v8JAAwA+v8JAA8A+v8JABIA+v8JABUA+v8JABgA+v8JABsA+v8KAOf/+v8KAOr/+v8KAO3/+v8KAPD/+v8KAPP/+v8KAPb/+v8KAPn/+v8KAPz/+v8KAP//+v8KAAIA+v8KAAUA+v8KAAgA+v8KAAsA+v8KAA4A+v8KABEA+v8KABQA+v8KABcA+v8KABoA+v8LAOn/+v8LAOz/+v8LAO//+v8LAPL/+v8LAPX/+v8LAPj/+v8LAPv/+v8LAP7/+v8LAAEA+v8LAAQA+v8LAAcA+v8LAAoA+v8LAA0A+v8LABAA+v8LABMA+v8LABYA+v8MAOv/+v8MAO7/+v8MAPH/+v8MAPT/+v8MAPf/+v8MAPr/+v8MAP3/+v8MAAAA+v8MAAMA+v8MAAYA+v8MAAkA+v8MAAwA+v8MAA8A+v8MABIA+v8MABUA+v8NAPD/+v8NAPP/+v8NAPb/+v8NAPn/+v8NAPz/+v8NAP//+v8NAAIA+v8NAAUA+v8NAAgA+v8NAAsA+v8NAA4A+v8OAPv/+v8OAP7/+v8OAAEA+v8OAAQA+v8OAAcA+//3//7/+//3/wEA+//3/wQA+//4//H/+//4//T/+//4//f/+//4//r/+//4//3/+//4/wAA+//4/wMA+//4/wYA+//4/wkA+//4/wwA+//4/w8A+//5/+3/+//5//D/+//5//P/+//5//b/+//5//n/+//5//z/+//5////+//5/wIA+//5/wUA+//5/wgA+//5/wsA+//5/w4A+//5/xEA+//5/xQA+//6/+n/+//6/+z/+//6/+//+//6//L/+//6//X/+//6//j/+//6//v/+//6//7/+//6/wEA+//6/wQA+//6/wcA+//6/woA+//6/w0A+//6/xAA+//6/xMA+//6/xYA+//7/+X/+//7/+j/+//7/+v/+//7/+7/+//7//H/+//7//T/+//7//f/+//7//r/+//7//3/+//7/wAA+//7/wMA+//7/wYA+//7/wkA+//7/wwA+//7/w8A+//7/xIA+//7/xUA+//7/xgA+//7/xsA+//8/+T/+//8/+f/+//8/+r/+//8/+3/+//8//D/+//8//P/+//8//b/+//8//n/+//8//z/+//8////+//8/wIA+//8/wUA+//8/wgA+//8/wsA+//8/w4A+//8/xEA+//8/xQA+//8/xcA+//8/xoA+//8/x0A+//9/+D/+//9/+P/+//9/+b/+//9/+n/+//9/+z/+//9/+//+//9//L/+//9//X/+//9//j/+//9//v/+//9//7/+//9/wEA+//9/wQA+//9/wcA+//9/woA+//9/w0A+//9/xAA+//9/xMA+//9/xYA+//9/xkA+//9/xwA+//9/x8A+//+/9//+//+/+L/+//+/+X/+//+/+j/+//+/+v/+//+/+7/+//+//H/+//+//T/+//+//f/+//+//r/+//+//3/+//+/wAA+//+/wMA+//+/wYA+//+/wkA+//+/wwA+//+/w8A+//+/xIA+//+/xUA+//+/xgA+//+/xsA+//+/x4A+//+/yEA+////97/+////+H/+////+T/+////+f/+////+r/+////+3/+/////D/+/////P/+/////b/+/////n/+/////z/+///////+////wIA+////wUA+////wgA+////wsA+////w4A+////xEA+////xQA+////xcA+////xoA+////x0A+////yAA+/8AAN3/+/8AAOD/+/8AAOP/+/8AAOb/+/8AAOn/+/8AAOz/+/8AAO//+/8AAPL/+/8AAPX/+/8AAPj/+/8AAPv/+/8AAP7/+/8AAAEA+/8AAAQA+/8AAAcA+/8AAAoA+/8AAA0A+/8AABAA+/8AABMA+/8AABYA+/8AABkA+/8AABwA+/8AAB8A+/8AACIA+/8BANz/+/8BAN//+/8BAOL/+/8BAOX/+/8BAOj/+/8BAOv/+/8BAO7/+/8BAPH/+/8BAPT/+/8BAPf/+/8BAPr/+/8BAP3/+/8BAAAA+/8BAAMA+/8BAAYA+/8BAAkA+/8BAAwA+/8BAA8A+/8BABIA+/8BABUA+/8BABgA+/8BABsA+/8BAB4A+/8BACEA+/8BACQA+/8CAN7/+/8CAOH/+/8CAOT/+/8CAOf/+/8CAOr/+/8CAO3/+/8CAPD/+/8CAPP/+/8CAPb/+/8CAPn/+/8CAPz/+/8CAP//+/8CAAIA+/8CAAUA+/8CAAgA+/8CAAsA+/8CAA4A+/8CABEA+/8CABQA+/8CABcA+/8CABoA+/8CAB0A+/8CACAA+/8CACMA+/8DAN3/+/8DAOD/+/8DAOP/+/8DAOb/+/8DAOn/+/8DAOz/+/8DAO//+/8DAPL/+/8DAPX/+/8DAPj/+/8DAPv/+/8DAP7/+/8DAAEA+/8DAAQA+/8DAAcA+/8DAAoA+/8DAA0A+/8DABAA+/8DABMA+/8DABYA+/8DABkA+/8DABwA+/8DAB8A+/8DACIA+/8EANz/+/8EAN//+/8EAOL/+/8EAOX/+/8EAOj/+/8EAOv/+/8EAO7/+/8EAPH/+/8EAPT/+/8EAPf/+/8EAPr/+/8EAP3/+/8EAAAA+/8EAAMA+/8EAAYA+/8EAAkA+/8EAAwA+/8EAA8A+/8EABIA+/8EABUA+/8EABgA+/8EABsA+/8EAB4A+/8EACEA+/8EACQA+/8FAN7/+/8FAOH/+/8FAOT/+/8FAOf/+/8FAOr/+/8FAO3/+/8FAPD/+/8FAPP/+/8FAPb/+/8FAPn/+/8FAPz/+/8FAP//+/8FAAIA+/8FAAUA+/8FAAgA+/8FAAsA+/8FAA4A+/8FABEA+/8FABQA+/8FABcA+/8FABoA+/8FAB0A+/8FACAA+/8FACMA+/8GAOD/+/8GAOP/+/8GAOb/+/8GAOn/+/8GAOz/+/8GAO//+/8GAPL/+/8GAPX/+/8GAPj/+/8GAPv/+/8GAP7/+/8GAAEA+/8GAAQA+/8GAAcA+/8GAAoA+/8GAA0A+/8GABAA+/8GABMA+/8GABYA+/8GABkA+/8GABwA+/8GAB8A+/8GACIA+/8HAN//+/8HAOL/+/8HAOX/+/8HAOj/+/8HAOv/+/8HAO7/+/8HAPH/+/8HAPT/+/8HAPf/+/8HAPr/+/8HAP3/+/8HAAAA+/8HAAMA+/8HAAYA+/8HAAkA+/8HAAwA+/8HAA8A+/8HABIA+/8HABUA+/8HABgA+/8HABsA+/8HAB4A+/8HACEA+/8IAOH/+/8IAOT/+/8IAOf/+/8IAOr/+/8IAO3/+/8IAPD/+/8IAPP/+/8IAPb/+/8IAPn/+/8IAPz/+/8IAP//+/8IAAIA+/8IAAUA+/8IAAgA+/8IAAsA+/8IAA4A+/8IABEA+/8IABQA+/8IABcA+/8IABoA+/8IAB0A+/8IACAA+/8JAOP/+/8JAOb/+/8JAOn/+/8JAOz/+/8JAO//+/8JAPL/+/8JAPX/+/8JAPj/+/8JAPv/+/8JAP7/+/8JAAEA+/8JAAQA+/8JAAcA+/8JAAoA+/8JAA0A+/8JABAA+/8JABMA+/8JABYA+/8JABkA+/8JABwA+/8KAOX/+/8KAOj/+/8KAOv/+/8KAO7/+/8KAPH/+/8KAPT/+/8KAPf/+/8KAPr/+/8KAP3/+/8KAAAA+/8KAAMA+/8KAAYA+/8KAAkA+/8KAAwA+/8KAA8A+/8KABIA+/8KABUA+/8KABgA+/8KABsA+/8LAOr/+/8LAO3/+/8LAPD/+/8LAPP/+/8LAPb/+/8LAPn/+/8LAPz/+/8LAP//+/8LAAIA+/8LAAUA+/8LAAgA+/8LAAsA+/8LAA4A+/8LABEA+/8LABQA+/8LABcA+/8MAOz/+/8MAO//+/8MAPL/+/8MAPX/+/8MAPj/+/8MAPv/+/8MAP7/+/8MAAEA+/8MAAQA+/8MAAcA+/8MAAoA+/8MAA0A+/8MABAA+/8MABMA+/8NAPH/+/8NAPT/+/8NAPf/+/8NAPr/+/8NAP3/+/8NAAAA+/8NAAMA+/8NAAYA+/8NAAkA+/8NAAwA+/8NAA8A+/8OAPz/+/8OAP//+/8OAAIA/P/3//P//P/3//b//P/3//n//P/3//z//P/3/////P/3/wIA/P/3/wUA/P/3/wgA/P/3/wsA/P/3/w4A/P/4/+z//P/4/+///P/4//L//P/4//X//P/4//j//P/4//v//P/4//7//P/4/wEA/P/4/wQA/P/4/wcA/P/4/woA/P/4/w0A/P/4/xAA/P/4/xMA/P/5/+j//P/5/+v//P/5/+7//P/5//H//P/5//T//P/5//f//P/5//r//P/5//3//P/5/wAA/P/5/wMA/P/5/wYA/P/5/wkA/P/5/wwA/P/5/w8A/P/5/xIA/P/5/xUA/P/5/xgA/P/6/+f//P/6/+r//P/6/+3//P/6//D//P/6//P//P/6//b//P/6//n//P/6//z//P/6/////P/6/wIA/P/6/wUA/P/6/wgA/P/6/wsA/P/6/w4A/P/6/xEA/P/6/xQA/P/6/xcA/P/6/xoA/P/7/+P//P/7/+b//P/7/+n//P/7/+z//P/7/+///P/7//L//P/7//X//P/7//j//P/7//v//P/7//7//P/7/wEA/P/7/wQA/P/7/wcA/P/7/woA/P/7/w0A/P/7/xAA/P/7/xMA/P/7/xYA/P/7/xkA/P/7/xwA/P/8/+L//P/8/+X//P/8/+j//P/8/+v//P/8/+7//P/8//H//P/8//T//P/8//f//P/8//r//P/8//3//P/8/wAA/P/8/wMA/P/8/wYA/P/8/wkA/P/8/wwA/P/8/w8A/P/8/xIA/P/8/xUA/P/8/xgA/P/8/xsA/P/8/x4A/P/9/+H//P/9/+T//P/9/+f//P/9/+r//P/9/+3//P/9//D//P/9//P//P/9//b//P/9//n//P/9//z//P/9/////P/9/wIA/P/9/wUA/P/9/wgA/P/9/wsA/P/9/w4A/P/9/xEA/P/9/xQA/P/9/xcA/P/9/xoA/P/9/x0A/P/9/yAA/P/+/93//P/+/+D//P/+/+P//P/+/+b//P/+/+n//P/+/+z//P/+/+///P/+//L//P/+//X//P/+//j//P/+//v//P/+//7//P/+/wEA/P/+/wQA/P/+/wcA/P/+/woA/P/+/w0A/P/+/xAA/P/+/xMA/P/+/xYA/P/+/xkA/P/+/xwA/P/+/x8A/P/+/yIA/P///9z//P///9///P///+L//P///+X//P///+j//P///+v//P///+7//P////H//P////T//P////f//P////r//P////3//P///wAA/P///wMA/P///wYA/P///wkA/P///wwA/P///w8A/P///xIA/P///xUA/P///xgA/P///xsA/P///x4A/P///yEA/P///yQA/P8AAN7//P8AAOH//P8AAOT//P8AAOf//P8AAOr//P8AAO3//P8AAPD//P8AAPP//P8AAPb//P8AAPn//P8AAPz//P8AAP///P8AAAIA/P8AAAUA/P8AAAgA/P8AAAsA/P8AAA4A/P8AABEA/P8AABQA/P8AABcA/P8AABoA/P8AAB0A/P8AACAA/P8AACMA/P8BAN3//P8BAOD//P8BAOP//P8BAOb//P8BAOn//P8BAOz//P8BAO///P8BAPL//P8BAPX//P8BAPj//P8BAPv//P8BAP7//P8BAAEA/P8BAAQA/P8BAAcA/P8BAAoA/P8BAA0A/P8BABAA/P8BABMA/P8BABYA/P8BABkA/P8BABwA/P8BAB8A/P8BACIA/P8BACUA/P8CANz//P8CAN///P8CAOL//P8CAOX//P8CAOj//P8CAOv//P8CAO7//P8CAPH//P8CAPT//P8CAPf//P8CAPr//P8CAP3//P8CAAAA/P8CAAMA/P8CAAYA/P8CAAkA/P8CAAwA/P8CAA8A/P8CABIA/P8CABUA/P8CABgA/P8CABsA/P8CAB4A/P8CACEA/P8CACQA/P8DANv//P8DAN7//P8DAOH//P8DAOT//P8DAOf//P8DAOr//P8DAO3//P8DAPD//P8DAPP//P8DAPb//P8DAPn//P8DAPz//P8DAP///P8DAAIA/P8DAAUA/P8DAAgA/P8DAAsA/P8DAA4A/P8DABEA/P8DABQA/P8DABcA/P8DABoA/P8DAB0A/P8DACAA/P8DACMA/P8EAN3//P8EAOD//P8EAOP//P8EAOb//P8EAOn//P8EAOz//P8EAO///P8EAPL//P8EAPX//P8EAPj//P8EAPv//P8EAP7//P8EAAEA/P8EAAQA/P8EAAcA/P8EAAoA/P8EAA0A/P8EABAA/P8EABMA/P8EABYA/P8EABkA/P8EABwA/P8EAB8A/P8EACIA/P8FANz//P8FAN///P8FAOL//P8FAOX//P8FAOj//P8FAOv//P8FAO7//P8FAPH//P8FAPT//P8FAPf//P8FAPr//P8FAP3//P8FAAAA/P8FAAMA/P8FAAYA/P8FAAkA/P8FAAwA/P8FAA8A/P8FABIA/P8FABUA/P8FABgA/P8FABsA/P8FAB4A/P8FACEA/P8FACQA/P8GAN7//P8GAOH//P8GAOT//P8GAOf//P8GAOr//P8GAO3//P8GAPD//P8GAPP//P8GAPb//P8GAPn//P8GAPz//P8GAP///P8GAAIA/P8GAAUA/P8GAAgA/P8GAAsA/P8GAA4A/P8GABEA/P8GABQA/P8GABcA/P8GABoA/P8GAB0A/P8GACAA/P8GACMA/P8HAOD//P8HAOP//P8HAOb//P8HAOn//P8HAOz//P8HAO///P8HAPL//P8HAPX//P8HAPj//P8HAPv//P8HAP7//P8HAAEA/P8HAAQA/P8HAAcA/P8HAAoA/P8HAA0A/P8HABAA/P8HABMA/P8HABYA/P8HABkA/P8HABwA/P8HAB8A/P8IAOL//P8IAOX//P8IAOj//P8IAOv//P8IAO7//P8IAPH//P8IAPT//P8IAPf//P8IAPr//P8IAP3//P8IAAAA/P8IAAMA/P8IAAYA/P8IAAkA/P8IAAwA/P8IAA8A/P8IABIA/P8IABUA/P8IABgA/P8IABsA/P8IAB4A/P8JAOT//P8JAOf//P8JAOr//P8JAO3//P8JAPD//P8JAPP//P8JAPb//P8JAPn//P8JAPz//P8JAP///P8JAAIA/P8JAAUA/P8JAAgA/P8JAAsA/P8JAA4A/P8JABEA/P8JABQA/P8JABcA/P8JABoA/P8JAB0A/P8KAOb//P8KAOn//P8KAOz//P8KAO///P8KAPL//P8KAPX//P8KAPj//P8KAPv//P8KAP7//P8KAAEA/P8KAAQA/P8KAAcA/P8KAAoA/P8KAA0A/P8KABAA/P8KABMA/P8KABYA/P8KABkA/P8LAOj//P8LAOv//P8LAO7//P8LAPH//P8LAPT//P8LAPf//P8LAPr//P8LAP3//P8LAAAA/P8LAAMA/P8LAAYA/P8LAAkA/P8LAAwA/P8LAA8A/P8LABIA/P8LABUA/P8LABgA/P8MAO3//P8MAPD//P8MAPP//P8MAPb//P8MAPn//P8MAPz//P8MAP///P8MAAIA/P8MAAUA/P8MAAgA/P8MAAsA/P8MAA4A/P8MABEA/P8MABQA/P8NAPL//P8NAPX//P8NAPj//P8NAPv//P8NAP7//P8NAAEA/P8NAAQA/P8NAAcA/P8NAAoA/P8NAA0A/f/2//X//f/2//j//f/2//v//f/2//7//f/2/wEA/f/2/wQA/f/2/wcA/f/2/woA/f/3/+7//f/3//H//f/3//T//f/3//f//f/3//r//f/3//3//f/3/wAA/f/3/wMA/f/3/wYA/f/3/wkA/f/3/wwA/f/3/w8A/f/3/xIA/f/4/+r//f/4/+3//f/4//D//f/4//P//f/4//b//f/4//n//f/4//z//f/4/////f/4/wIA/f/4/wUA/f/4/wgA/f/4/wsA/f/4/w4A/f/4/xEA/f/4/xQA/f/4/xcA/f/5/+b//f/5/+n//f/5/+z//f/5/+///f/5//L//f/5//X//f/5//j//f/5//v//f/5//7//f/5/wEA/f/5/wQA/f/5/wcA/f/5/woA/f/5/w0A/f/5/xAA/f/5/xMA/f/5/xYA/f/5/xkA/f/6/+X//f/6/+j//f/6/+v//f/6/+7//f/6//H//f/6//T//f/6//f//f/6//r//f/6//3//f/6/wAA/f/6/wMA/f/6/wYA/f/6/wkA/f/6/wwA/f/6/w8A/f/6/xIA/f/6/xUA/f/6/xgA/f/6/xsA/f/7/+H//f/7/+T//f/7/+f//f/7/+r//f/7/+3//f/7//D//f/7//P//f/7//b//f/7//n//f/7//z//f/7/////f/7/wIA/f/7/wUA/f/7/wgA/f/7/wsA/f/7/w4A/f/7/xEA/f/7/xQA/f/7/xcA/f/7/xoA/f/7/x0A/f/7/yAA/f/8/+D//f/8/+P//f/8/+b//f/8/+n//f/8/+z//f/8/+///f/8//L//f/8//X//f/8//j//f/8//v//f/8//7//f/8/wEA/f/8/wQA/f/8/wcA/f/8/woA/f/8/w0A/f/8/xAA/f/8/xMA/f/8/xYA/f/8/xkA/f/8/xwA/f/8/x8A/f/9/9///f/9/+L//f/9/+X//f/9/+j//f/9/+v//f/9/+7//f/9//H//f/9//T//f/9//f//f/9//r//f/9//3//f/9/wAA/f/9/wMA/f/9/wYA/f/9/wkA/f/9/wwA/f/9/w8A/f/9/xIA/f/9/xUA/f/9/xgA/f/9/xsA/f/9/x4A/f/9/yEA/f/+/97//f/+/+H//f/+/+T//f/+/+f//f/+/+r//f/+/+3//f/+//D//f/+//P//f/+//b//f/+//n//f/+//z//f/+/////f/+/wIA/f/+/wUA/f/+/wgA/f/+/wsA/f/+/w4A/f/+/xEA/f/+/xQA/f/+/xcA/f/+/xoA/f/+/x0A/f/+/yAA/f/+/yMA/f///93//f///+D//f///+P//f///+b//f///+n//f///+z//f///+///f////L//f////X//f////j//f////v//f////7//f///wEA/f///wQA/f///wcA/f///woA/f///w0A/f///xAA/f///xMA/f///xYA/f///xkA/f///xwA/f///x8A/f///yIA/f///yUA/f8AANz//f8AAN///f8AAOL//f8AAOX//f8AAOj//f8AAOv//f8AAO7//f8AAPH//f8AAPT//f8AAPf//f8AAPr//f8AAP3//f8AAAAA/f8AAAMA/f8AAAYA/f8AAAkA/f8AAAwA/f8AAA8A/f8AABIA/f8AABUA/f8AABgA/f8AABsA/f8AAB4A/f8AACEA/f8AACQA/f8BANv//f8BAN7//f8BAOH//f8BAOT//f8BAOf//f8BAOr//f8BAO3//f8BAPD//f8BAPP//f8BAPb//f8BAPn//f8BAPz//f8BAP///f8BAAIA/f8BAAUA/f8BAAgA/f8BAAsA/f8BAA4A/f8BABEA/f8BABQA/f8BABcA/f8BABoA/f8BAB0A/f8BACAA/f8BACMA/f8BACYA/f8CANr//f8CAN3//f8CAOD//f8CAOP//f8CAOb//f8CAOn//f8CAOz//f8CAO///f8CAPL//f8CAPX//f8CAPj//f8CAPv//f8CAP7//f8CAAEA/f8CAAQA/f8CAAcA/f8CAAoA/f8CAA0A/f8CABAA/f8CABMA/f8CABYA/f8CABkA/f8CABwA/f8CAB8A/f8CACIA/f8CACUA/f8DANz//f8DAN///f8DAOL//f8DAOX//f8DAOj//f8DAOv//f8DAO7//f8DAPH//f8DAPT//f8DAPf//f8DAPr//f8DAP3//f8DAAAA/f8DAAMA/f8DAAYA/f8DAAkA/f8DAAwA/f8DAA8A/f8DABIA/f8DABUA/f8DABgA/f8DABsA/f8DAB4A/f8DACEA/f8DACQA/f8EANv//f8EAN7//f8EAOH//f8EAOT//f8EAOf//f8EAOr//f8EAO3//f8EAPD//f8EAPP//f8EAPb//f8EAPn//f8EAPz//f8EAP///f8EAAIA/f8EAAUA/f8EAAgA/f8EAAsA/f8EAA4A/f8EABEA/f8EABQA/f8EABcA/f8EABoA/f8EAB0A/f8EACAA/f8EACMA/f8FAN3//f8FAOD//f8FAOP//f8FAOb//f8FAOn//f8FAOz//f8FAO///f8FAPL//f8FAPX//f8FAPj//f8FAPv//f8FAP7//f8FAAEA/f8FAAQA/f8FAAcA/f8FAAoA/f8FAA0A/f8FABAA/f8FABMA/f8FABYA/f8FABkA/f8FABwA/f8FAB8A/f8FACIA/f8GAN///f8GAOL//f8GAOX//f8GAOj//f8GAOv//f8GAO7//f8GAPH//f8GAPT//f8GAPf//f8GAPr//f8GAP3//f8GAAAA/f8GAAMA/f8GAAYA/f8GAAkA/f8GAAwA/f8GAA8A/f8GABIA/f8GABUA/f8GABgA/f8GABsA/f8GAB4A/f8GACEA/f8HAOH//f8HAOT//f8HAOf//f8HAOr//f8HAO3//f8HAPD//f8HAPP//f8HAPb//f8HAPn//f8HAPz//f8HAP///f8HAAIA/f8HAAUA/f8HAAgA/f8HAAsA/f8HAA4A/f8HABEA/f8HABQA/f8HABcA/f8HABoA/f8HAB0A/f8HACAA/f8IAOD//f8IAOP//f8IAOb//f8IAOn//f8IAOz//f8IAO///f8IAPL//f8IAPX//f8IAPj//f8IAPv//f8IAP7//f8IAAEA/f8IAAQA/f8IAAcA/f8IAAoA/f8IAA0A/f8IABAA/f8IABMA/f8IABYA/f8IABkA/f8IABwA/f8IAB8A/f8JAOX//f8JAOj//f8JAOv//f8JAO7//f8JAPH//f8JAPT//f8JAPf//f8JAPr//f8JAP3//f8JAAAA/f8JAAMA/f8JAAYA/f8JAAkA/f8JAAwA/f8JAA8A/f8JABIA/f8JABUA/f8JABgA/f8JABsA/f8KAOf//f8KAOr//f8KAO3//f8KAPD//f8KAPP//f8KAPb//f8KAPn//f8KAPz//f8KAP///f8KAAIA/f8KAAUA/f8KAAgA/f8KAAsA/f8KAA4A/f8KABEA/f8KABQA/f8KABcA/f8KABoA/f8LAOn//f8LAOz//f8LAO///f8LAPL//f8LAPX//f8LAPj//f8LAPv//f8LAP7//f8LAAEA/f8LAAQA/f8LAAcA/f8LAAoA/f8LAA0A/f8LABAA/f8LABMA/f8LABYA/f8MAO7//f8MAPH//f8MAPT//f8MAPf//f8MAPr//f8MAP3//f8MAAAA/f8MAAMA/f8MAAYA/f8MAAkA/f8MAAwA/f8MAA8A/f8MABIA/f8NAPb//f8NAPn//f8NAPz//f8NAP///f8NAAIA/f8NAAUA/f8NAAgA/f8NAAsA/v/1//r//v/1//3//v/1/wAA/v/1/wMA/v/1/wYA/v/2//D//v/2//P//v/2//b//v/2//n//v/2//z//v/2/////v/2/wIA/v/2/wUA/v/2/wgA/v/2/wsA/v/2/w4A/v/3/+z//v/3/+///v/3//L//v/3//X//v/3//j//v/3//v//v/3//7//v/3/wEA/v/3/wQA/v/3/wcA/v/3/woA/v/3/w0A/v/3/xAA/v/3/xMA/v/3/xYA/v/4/+j//v/4/+v//v/4/+7//v/4//H//v/4//T//v/4//f//v/4//r//v/4//3//v/4/wAA/v/4/wMA/v/4/wYA/v/4/wkA/v/4/wwA/v/4/w8A/v/4/xIA/v/4/xUA/v/4/xgA/v/5/+T//v/5/+f//v/5/+r//v/5/+3//v/5//D//v/5//P//v/5//b//v/5//n//v/5//z//v/5/////v/5/wIA/v/5/wUA/v/5/wgA/v/5/wsA/v/5/w4A/v/5/xEA/v/5/xQA/v/5/xcA/v/5/xoA/v/5/x0A/v/6/+P//v/6/+b//v/6/+n//v/6/+z//v/6/+///v/6//L//v/6//X//v/6//j//v/6//v//v/6//7//v/6/wEA/v/6/wQA/v/6/wcA/v/6/woA/v/6/w0A/v/6/xAA/v/6/xMA/v/6/xYA/v/6/xkA/v/6/xwA/v/6/x8A/v/7/9///v/7/+L//v/7/+X//v/7/+j//v/7/+v//v/7/+7//v/7//H//v/7//T//v/7//f//v/7//r//v/7//3//v/7/wAA/v/7/wMA/v/7/wYA/v/7/wkA/v/7/wwA/v/7/w8A/v/7/xIA/v/7/xUA/v/7/xgA/v/7/xsA/v/7/x4A/v/7/yEA/v/8/97//v/8/+H//v/8/+T//v/8/+f//v/8/+r//v/8/+3//v/8//D//v/8//P//v/8//b//v/8//n//v/8//z//v/8/////v/8/wIA/v/8/wUA/v/8/wgA/v/8/wsA/v/8/w4A/v/8/xEA/v/8/xQA/v/8/xcA/v/8/xoA/v/8/x0A/v/8/yAA/v/8/yMA/v/9/93//v/9/+D//v/9/+P//v/9/+b//v/9/+n//v/9/+z//v/9/+///v/9//L//v/9//X//v/9//j//v/9//v//v/9//7//v/9/wEA/v/9/wQA/v/9/wcA/v/9/woA/v/9/w0A/v/9/xAA/v/9/xMA/v/9/xYA/v/9/xkA/v/9/xwA/v/9/x8A/v/9/yIA/v/+/9z//v/+/9///v/+/+L//v/+/+X//v/+/+j//v/+/+v//v/+/+7//v/+//H//v/+//T//v/+//f//v/+//r//v/+//3//v/+/wAA/v/+/wMA/v/+/wYA/v/+/wkA/v/+/wwA/v/+/w8A/v/+/xIA/v/+/xUA/v/+/xgA/v/+/xsA/v/+/x4A/v/+/yEA/v/+/yQA/v///9v//v///97//v///+H//v///+T//v///+f//v///+r//v///+3//v////D//v////P//v////b//v////n//v////z//v///////v///wIA/v///wUA/v///wgA/v///wsA/v///w4A/v///xEA/v///xQA/v///xcA/v///xoA/v///x0A/v///yAA/v///yMA/v///yYA/v8AANr//v8AAN3//v8AAOD//v8AAOP//v8AAOb//v8AAOn//v8AAOz//v8AAO///v8AAPL//v8AAPX//v8AAPj//v8AAPv//v8AAP7//v8AAAEA/v8AAAQA/v8AAAcA/v8AAAoA/v8AAA0A/v8AABAA/v8AABMA/v8AABYA/v8AABkA/v8AABwA/v8AAB8A/v8AACIA/v8AACUA/v8BANz//v8BAN///v8BAOL//v8BAOX//v8BAOj//v8BAOv//v8BAO7//v8BAPH//v8BAPT//v8BAPf//v8BAPr//v8BAP3//v8BAAAA/v8BAAMA/v8BAAYA/v8BAAkA/v8BAAwA/v8BAA8A/v8BABIA/v8BABUA/v8BABgA/v8BABsA/v8BAB4A/v8BACEA/v8BACQA/v8CANv//v8CAN7//v8CAOH//v8CAOT//v8CAOf//v8CAOr//v8CAO3//v8CAPD//v8CAPP//v8CAPb//v8CAPn//v8CAPz//v8CAP///v8CAAIA/v8CAAUA/v8CAAgA/v8CAAsA/v8CAA4A/v8CABEA/v8CABQA/v8CABcA/v8CABoA/v8CAB0A/v8CACAA/v8CACMA/v8CACYA/v8DANr//v8DAN3//v8DAOD//v8DAOP//v8DAOb//v8DAOn//v8DAOz//v8DAO///v8DAPL//v8DAPX//v8DAPj//v8DAPv//v8DAP7//v8DAAEA/v8DAAQA/v8DAAcA/v8DAAoA/v8DAA0A/v8DABAA/v8DABMA/v8DABYA/v8DABkA/v8DABwA/v8DAB8A/v8DACIA/v8DACUA/v8EANz//v8EAN///v8EAOL//v8EAOX//v8EAOj//v8EAOv//v8EAO7//v8EAPH//v8EAPT//v8EAPf//v8EAPr//v8EAP3//v8EAAAA/v8EAAMA/v8EAAYA/v8EAAkA/v8EAAwA/v8EAA8A/v8EABIA/v8EABUA/v8EABgA/v8EABsA/v8EAB4A/v8EACEA/v8EACQA/v8FAN7//v8FAOH//v8FAOT//v8FAOf//v8FAOr//v8FAO3//v8FAPD//v8FAPP//v8FAPb//v8FAPn//v8FAPz//v8FAP///v8FAAIA/v8FAAUA/v8FAAgA/v8FAAsA/v8FAA4A/v8FABEA/v8FABQA/v8FABcA/v8FABoA/v8FAB0A/v8FACAA/v8FACMA/v8GAN3//v8GAOD//v8GAOP//v8GAOb//v8GAOn//v8GAOz//v8GAO///v8GAPL//v8GAPX//v8GAPj//v8GAPv//v8GAP7//v8GAAEA/v8GAAQA/v8GAAcA/v8GAAoA/v8GAA0A/v8GABAA/v8GABMA/v8GABYA/v8GABkA/v8GABwA/v8GAB8A/v8GACIA/v8HAN///v8HAOL//v8HAOX//v8HAOj//v8HAOv//v8HAO7//v8HAPH//v8HAPT//v8HAPf//v8HAPr//v8HAP3//v8HAAAA/v8HAAMA/v8HAAYA/v8HAAkA/v8HAAwA/v8HAA8A/v8HABIA/v8HABUA/v8HABgA/v8HABsA/v8HAB4A/v8HACEA/v8IAOH//v8IAOT//v8IAOf//v8IAOr//v8IAO3//v8IAPD//v8IAPP//v8IAPb//v8IAPn//v8IAPz//v8IAP///v8IAAIA/v8IAAUA/v8IAAgA/v8IAAsA/v8IAA4A/v8IABEA/v8IABQA/v8IABcA/v8IABoA/v8IAB0A/v8JAOP//v8JAOb//v8JAOn//v8JAOz//v8JAO///v8JAPL//v8JAPX//v8JAPj//v8JAPv//v8JAP7//v8JAAEA/v8JAAQA/v8JAAcA/v8JAAoA/v8JAA0A/v8JABAA/v8JABMA/v8JABYA/v8JABkA/v8JABwA/v8KAOj//v8KAOv//v8KAO7//v8KAPH//v8KAPT//v8KAPf//v8KAPr//v8KAP3//v8KAAAA/v8KAAMA/v8KAAYA/v8KAAkA/v8KAAwA/v8KAA8A/v8KABIA/v8KABUA/v8KABgA/v8LAOr//v8LAO3//v8LAPD//v8LAPP//v8LAPb//v8LAPn//v8LAPz//v8LAP///v8LAAIA/v8LAAUA/v8LAAgA/v8LAAsA/v8LAA4A/v8LABEA/v8LABQA/v8MAPL//v8MAPX//v8MAPj//v8MAPv//v8MAP7//v8MAAEA/v8MAAQA/v8MAAcA/v8MAAoA/v8MAA0A/v8MABAA/v8NAPr//v8NAP3//v8NAAAA/v8NAAMA/v8NAAYA///1//L////1//X////1//j////1//v////1//7////1/wEA///1/wQA///1/wcA///1/woA///1/w0A///2/+7////2//H////2//T////2//f////2//r////2//3////2/wAA///2/wMA///2/wYA///2/wkA///2/wwA///2/w8A///2/xIA///3/+r////3/+3////3//D////3//P////3//b////3//n////3//z////3///////3/wIA///3/wUA///3/wgA///3/wsA///3/w4A///3/xEA///3/xQA///3/xcA///4/+b////4/+n////4/+z////4/+/////4//L////4//X////4//j////4//v////4//7////4/wEA///4/wQA///4/wcA///4/woA///4/w0A///4/xAA///4/xMA///4/xYA///4/xkA///4/xwA///5/+L////5/+X////5/+j////5/+v////5/+7////5//H////5//T////5//f////5//r////5//3////5/wAA///5/wMA///5/wYA///5/wkA///5/wwA///5/w8A///5/xIA///5/xUA///5/xgA///5/xsA///5/x4A///6/+H////6/+T////6/+f////6/+r////6/+3////6//D////6//P////6//b////6//n////6//z////6///////6/wIA///6/wUA///6/wgA///6/wsA///6/w4A///6/xEA///6/xQA///6/xcA///6/xoA///6/x0A///6/yAA///7/+D////7/+P////7/+b////7/+n////7/+z////7/+/////7//L////7//X////7//j////7//v////7//7////7/wEA///7/wQA///7/wcA///7/woA///7/w0A///7/xAA///7/xMA///7/xYA///7/xkA///7/xwA///7/x8A///7/yIA///8/9z////8/9/////8/+L////8/+X////8/+j////8/+v////8/+7////8//H////8//T////8//f////8//r////8//3////8/wAA///8/wMA///8/wYA///8/wkA///8/wwA///8/w8A///8/xIA///8/xUA///8/xgA///8/xsA///8/x4A///8/yEA///8/yQA///9/9v////9/97////9/+H////9/+T////9/+f////9/+r////9/+3////9//D////9//P////9//b////9//n////9//z////9///////9/wIA///9/wUA///9/wgA///9/wsA///9/w4A///9/xEA///9/xQA///9/xcA///9/xoA///9/x0A///9/yAA///9/yMA///+/9r////+/93////+/+D////+/+P////+/+b////+/+n////+/+z////+/+/////+//L////+//X////+//j////+//v////+//7////+/wEA///+/wQA///+/wcA///+/woA///+/w0A///+/xAA///+/xMA///+/xYA///+/xkA///+/xwA///+/x8A///+/yIA///+/yUA/////9z//////9///////+L//////+X//////+j//////+v//////+7///////H///////T///////f///////r///////3//////wAA/////wMA/////wYA/////wkA/////wwA/////w8A/////xIA/////xUA/////xgA/////xsA/////x4A/////yEA/////yQA//8AANv///8AAN7///8AAOH///8AAOT///8AAOf///8AAOr///8AAO3///8AAPD///8AAPP///8AAPb///8AAPn///8AAPz///8AAP////8AAAIA//8AAAUA//8AAAgA//8AAAsA//8AAA4A//8AABEA//8AABQA//8AABcA//8AABoA//8AAB0A//8AACAA//8AACMA//8AACYA//8BANr///8BAN3///8BAOD///8BAOP///8BAOb///8BAOn///8BAOz///8BAO////8BAPL///8BAPX///8BAPj///8BAPv///8BAP7///8BAAEA//8BAAQA//8BAAcA//8BAAoA//8BAA0A//8BABAA//8BABMA//8BABYA//8BABkA//8BABwA//8BAB8A//8BACIA//8BACUA//8CANz///8CAN////8CAOL///8CAOX///8CAOj///8CAOv///8CAO7///8CAPH///8CAPT///8CAPf///8CAPr///8CAP3///8CAAAA//8CAAMA//8CAAYA//8CAAkA//8CAAwA//8CAA8A//8CABIA//8CABUA//8CABgA//8CABsA//8CAB4A//8CACEA//8CACQA//8DANv///8DAN7///8DAOH///8DAOT///8DAOf///8DAOr///8DAO3///8DAPD///8DAPP///8DAPb///8DAPn///8DAPz///8DAP////8DAAIA//8DAAUA//8DAAgA//8DAAsA//8DAA4A//8DABEA//8DABQA//8DABcA//8DABoA//8DAB0A//8DACAA//8DACMA//8DACYA//8EAN3///8EAOD///8EAOP///8EAOb///8EAOn///8EAOz///8EAO////8EAPL///8EAPX///8EAPj///8EAPv///8EAP7///8EAAEA//8EAAQA//8EAAcA//8EAAoA//8EAA0A//8EABAA//8EABMA//8EABYA//8EABkA//8EABwA//8EAB8A//8EACIA//8EACUA//8FANz///8FAN////8FAOL///8FAOX///8FAOj///8FAOv///8FAO7///8FAPH///8FAPT///8FAPf///8FAPr///8FAP3///8FAAAA//8FAAMA//8FAAYA//8FAAkA//8FAAwA//8FAA8A//8FABIA//8FABUA//8FABgA//8FABsA//8FAB4A//8FACEA//8FACQA//8GAN7///8GAOH///8GAOT///8GAOf///8GAOr///8GAO3///8GAPD///8GAPP///8GAPb///8GAPn///8GAPz///8GAP////8GAAIA//8GAAUA//8GAAgA//8GAAsA//8GAA4A//8GABEA//8GABQA//8GABcA//8GABoA//8GAB0A//8GACAA//8HAOD///8HAOP///8HAOb///8HAOn///8HAOz///8HAO////8HAPL///8HAPX///8HAPj///8HAPv///8HAP7///8HAAEA//8HAAQA//8HAAcA//8HAAoA//8HAA0A//8HABAA//8HABMA//8HABYA//8HABkA//8HABwA//8HAB8A//8IAOL///8IAOX///8IAOj///8IAOv///8IAO7///8IAPH///8IAPT///8IAPf///8IAPr///8IAP3///8IAAAA//8IAAMA//8IAAYA//8IAAkA//8IAAwA//8IAA8A//8IABIA//8IABUA//8IABgA//8IABsA//8IAB4A//8JAOT///8JAOf///8JAOr///8JAO3///8JAPD///8JAPP///8JAPb///8JAPn///8JAPz///8JAP////8JAAIA//8JAAUA//8JAAgA//8JAAsA//8JAA4A//8JABEA//8JABQA//8JABcA//8JABoA//8KAOn///8KAOz///8KAO////8KAPL///8KAPX///8KAPj///8KAPv///8KAP7///8KAAEA//8KAAQA//8KAAcA//8KAAoA//8KAA0A//8KABAA//8KABMA//8KABYA//8LAO7///8LAPH///8LAPT///8LAPf///8LAPr///8LAP3///8LAAAA//8LAAMA//8LAAYA//8LAAkA//8LAAwA//8LAA8A//8LABIA//8MAPP///8MAPb///8MAPn///8MAPz///8MAP////8MAAIA//8MAAUA//8MAAgA//8MAAsA//8MAA4AAAD0//f/AAD0//r/AAD0//3/AAD0/wAAAAD0/wMAAAD0/wYAAAD0/wkAAAD1//D/AAD1//P/AAD1//b/AAD1//n/AAD1//z/AAD1////AAD1/wIAAAD1/wUAAAD1/wgAAAD1/wsAAAD1/w4AAAD1/xEAAAD2/+z/AAD2/+//AAD2//L/AAD2//X/AAD2//j/AAD2//v/AAD2//7/AAD2/wEAAAD2/wQAAAD2/wcAAAD2/woAAAD2/w0AAAD2/xAAAAD2/xMAAAD2/xYAAAD3/+j/AAD3/+v/AAD3/+7/AAD3//H/AAD3//T/AAD3//f/AAD3//r/AAD3//3/AAD3/wAAAAD3/wMAAAD3/wYAAAD3/wkAAAD3/wwAAAD3/w8AAAD3/xIAAAD3/xUAAAD3/xgAAAD4/+T/AAD4/+f/AAD4/+r/AAD4/+3/AAD4//D/AAD4//P/AAD4//b/AAD4//n/AAD4//z/AAD4////AAD4/wIAAAD4/wUAAAD4/wgAAAD4/wsAAAD4/w4AAAD4/xEAAAD4/xQAAAD4/xcAAAD4/xoAAAD4/x0AAAD5/+D/AAD5/+P/AAD5/+b/AAD5/+n/AAD5/+z/AAD5/+//AAD5//L/AAD5//X/AAD5//j/AAD5//v/AAD5//7/AAD5/wEAAAD5/wQAAAD5/wcAAAD5/woAAAD5/w0AAAD5/xAAAAD5/xMAAAD5/xYAAAD5/xkAAAD5/xwAAAD5/x8AAAD6/9//AAD6/+L/AAD6/+X/AAD6/+j/AAD6/+v/AAD6/+7/AAD6//H/AAD6//T/AAD6//f/AAD6//r/AAD6//3/AAD6/wAAAAD6/wMAAAD6/wYAAAD6/wkAAAD6/wwAAAD6/w8AAAD6/xIAAAD6/xUAAAD6/xgAAAD6/xsAAAD6/x4AAAD6/yEAAAD7/97/AAD7/+H/AAD7/+T/AAD7/+f/AAD7/+r/AAD7/+3/AAD7//D/AAD7//P/AAD7//b/AAD7//n/AAD7//z/AAD7////AAD7/wIAAAD7/wUAAAD7/wgAAAD7/wsAAAD7/w4AAAD7/xEAAAD7/xQAAAD7/xcAAAD7/xoAAAD7/x0AAAD7/yAAAAD7/yMAAAD8/93/AAD8/+D/AAD8/+P/AAD8/+b/AAD8/+n/AAD8/+z/AAD8/+//AAD8//L/AAD8//X/AAD8//j/AAD8//v/AAD8//7/AAD8/wEAAAD8/wQAAAD8/wcAAAD8/woAAAD8/w0AAAD8/xAAAAD8/xMAAAD8/xYAAAD8/xkAAAD8/xwAAAD8/x8AAAD8/yIAAAD9/9z/AAD9/9//AAD9/+L/AAD9/+X/AAD9/+j/AAD9/+v/AAD9/+7/AAD9//H/AAD9//T/AAD9//f/AAD9//r/AAD9//3/AAD9/wAAAAD9/wMAAAD9/wYAAAD9/wkAAAD9/wwAAAD9/w8AAAD9/xIAAAD9/xUAAAD9/xgAAAD9/xsAAAD9/x4AAAD9/yEAAAD9/yQAAAD+/9v/AAD+/97/AAD+/+H/AAD+/+T/AAD+/+f/AAD+/+r/AAD+/+3/AAD+//D/AAD+//P/AAD+//b/AAD+//n/AAD+//z/AAD+////AAD+/wIAAAD+/wUAAAD+/wgAAAD+/wsAAAD+/w4AAAD+/xEAAAD+/xQAAAD+/xcAAAD+/xoAAAD+/x0AAAD+/yAAAAD+/yMAAAD+/yYAAAD//9r/AAD//93/AAD//+D/AAD//+P/AAD//+b/AAD//+n/AAD//+z/AAD//+//AAD///L/AAD///X/AAD///j/AAD///v/AAD///7/AAD//wEAAAD//wQAAAD//wcAAAD//woAAAD//w0AAAD//xAAAAD//xMAAAD//xYAAAD//xkAAAD//xwAAAD//x8AAAD//yIAAAD//yUAAAAAANz/AAAAAN//AAAAAOL/AAAAAOX/AAAAAOj/AAAAAOv/AAAAAO7/AAAAAPH/AAAAAPT/AAAAAPf/AAAAAPr/AAAAAP3/AAAAAAMAAAAAAAYAAAAAAAkAAAAAAAwAAAAAAA8AAAAAABIAAAAAABUAAAAAABgAAAAAABsAAAAAAB4AAAAAACEAAAAAACQAAAABANv/AAABAN7/AAABAOH/AAABAOT/AAABAOf/AAABAOr/AAABAO3/AAABAPD/AAABAPP/AAABAPb/AAABAPn/AAABAPz/AAABAP//AAABAAIAAAABAAUAAAABAAgAAAABAAsAAAABAA4AAAABABEAAAABABQAAAABABcAAAABABoAAAABAB0AAAABACAAAAABACMAAAABACYAAAACANr/AAACAN3/AAACAOD/AAACAOP/AAACAOb/AAACAOn/AAACAOz/AAACAO//AAACAPL/AAACAPX/AAACAPj/AAACAPv/AAACAP7/AAACAAEAAAACAAQAAAACAAcAAAACAAoAAAACAA0AAAACABAAAAACABMAAAACABYAAAACABkAAAACABwAAAACAB8AAAACACIAAAACACUAAAADANz/AAADAN//AAADAOL/AAADAOX/AAADAOj/AAADAOv/AAADAO7/AAADAPH/AAADAPT/AAADAPf/AAADAPr/AAADAP3/AAADAAAAAAADAAMAAAADAAYAAAADAAkAAAADAAwAAAADAA8AAAADABIAAAADABUAAAADABgAAAADABsAAAADAB4AAAADACEAAAADACQAAAAEAN7/AAAEAOH/AAAEAOT/AAAEAOf/AAAEAOr/AAAEAO3/AAAEAPD/AAAEAPP/AAAEAPb/AAAEAPn/AAAEAPz/AAAEAP//AAAEAAIAAAAEAAUAAAAEAAgAAAAEAAsAAAAEAA4AAAAEABEAAAAEABQAAAAEABcAAAAEABoAAAAEAB0AAAAEACAAAAAEACMAAAAFAN3/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AQACAPb/AQACAPn/AQACAPz/AQACAP//AQACAAIAAQACAAUAAQACAAgAAQACAAsAAQACAA4AAQACABEAAQACABQAAQACABcAAQACABoAAQACAB0AAQACACAAAQACACMAAQACACYAAQADAN3/AQADAOD/AQADAOP/AQADAOb/AQADAOn/AQADAOz/AQADAO//AQADAPL/AQADAPX/AQADAPj/AQADAPv/AQADAP7/AQADAAEAAQADAAQAAQADAAcAAQADAAoAAQADAA0AAQADABAAAQADABMAAQADABYAAQADABkAAQADABwAAQADAB8AAQADACIAAQADACUAAQAEANz/AQAEAN//AQAEAOL/AQAEAOX/AQAEAOj/AQAEAOv/AQAEAO7/AQAEAPH/AQAEAPT/AQAEAPf/AQAEAPr/AQAEAP3/AQAEAAAAAQAEAAMAAQAEAAYAAQAEAAkAAQAEAAwAAQAEAA8AAQAEABIAAQAEABUAAQAEABgAAQAEABsAAQAEAB4AAQAEACEAAQAEACQAAQAFAN7/AQAFAOH/AQAFAOT/AQAFAOf/AQAFAOr/AQAFAO3/AQAFAPD/AQAFAPP/AQAFAPb/AQAFAPn/AQAFAPz/AQAFAP//AQAFAAIAAQAFAAUAAQAFAAgAAQAFAAsAAQAFAA4AAQAFABEAAQAFABQAAQAFABcAAQAFABoAAQAFAB0AAQAFACAAAQAGAOD/AQAGAOP/AQAGAOb/AQAGAOn/AQAGAOz/AQAGAO//AQAGAPL/AQAGAPX/AQAGAPj/AQAGAPv/AQAGAP7/AQAGAAEAAQAGAAQAAQAGAAcAAQAGAAoAAQAGAA0AAQAGABAAAQAGABMAAQAGABYAAQAGABkAAQAGABwAAQAGAB8AAQAHAOL/AQAHAOX/AQAHAOj/AQAHAOv/AQAHAO7/AQAHAPH/AQAHAPT/AQAHAPf/AQAHAPr/AQAHAP3/AQAHAAAAAQAHAAMAAQAHAAYAAQAHAAkAAQAHAAwAAQAHAA8AAQAHABIAAQAHABUAAQAHABgAAQAHABsAAQAHAB4AAQAIAOT/AQAIAOf/AQAIAOr/AQAIAO3/AQAIAPD/AQAIAPP/AQAIAPb/AQAIAPn/AQAIAPz/AQAIAP//AQAIAAIAAQAIAAUAAQAIAAgAAQAIAAsAAQAIAA4AAQAIABEAAQAIABQAAQAIABcAAQAIABoAAQAJAOn/AQAJAOz/AQAJAO//AQAJAPL/AQAJAPX/AQAJAPj/AQAJAPv/AQAJAP7/AQAJAAEAAQAJAAQAAQAJAAcAAQAJAAoAAQAJAA0AAQAJABAAAQAJABMAAQAJABYAAQAKAO7/AQAKAPH/AQAKAPT/AQAKAPf/AQAKAPr/AQAKAP3/AQAKAAAAAQAKAAMAAQAKAAYAAQAKAAkAAQAKAAwAAQAKAA8AAQAKABIAAQALAPP/AQALAPb/AQALAPn/AQALAPz/AQALAP//AQALAAIAAQALAAUAAQALAAgAAQALAAsAAQALAA4AAgDz//r/AgDz//3/AgDz/wAAAgDz/wMAAgDz/wYAAgD0//D/AgD0//P/AgD0//b/AgD0//n/AgD0//z/AgD0////AgD0/wIAAgD0/wUAAgD0/wgAAgD0/wsAAgD0/w4AAgD1/+z/AgD1/+//AgD1//L/AgD1//X/AgD1//j/AgD1//v/AgD1//7/AgD1/wEAAgD1/wQAAgD1/wcAAgD1/woAAgD1/w0AAgD1/xAAAgD1/xMAAgD1/xYAAgD2/+j/AgD2/+v/AgD2/+7/AgD2//H/AgD2//T/AgD2//f/AgD2//r/AgD2//3/AgD2/wAAAgD2/wMAAgD2/wYAAgD2/wkAAgD2/wwAAgD2/w8AAgD2/xIAAgD2/xUAAgD2/xgAAgD3/+T/AgD3/+f/AgD3/+r/AgD3/+3/AgD3//D/AgD3//P/AgD3//b/AgD3//n/AgD3//z/AgD3////AgD3/wIAAgD3/wUAAgD3/wgAAgD3/wsAAgD3/w4AAgD3/xEAAgD3/xQAAgD3/xcAAgD3/xoAAgD3/x0AAgD4/+P/AgD4/+b/AgD4/+n/AgD4/+z/AgD4/+//AgD4//L/AgD4//X/AgD4//j/AgD4//v/AgD4//7/AgD4/wEAAgD4/wQAAgD4/wcAAgD4/woAAgD4/w0AAgD4/xAAAgD4/xMAAgD4/xYAAgD4/xkAAgD4/xwAAgD4/x8AAgD5/9//AgD5/+L/AgD5/+X/AgD5/+j/AgD5/+v/AgD5/+7/AgD5//H/AgD5//T/AgD5//f/AgD5//r/AgD5//3/AgD5/wAAAgD5/wMAAgD5/wYAAgD5/wkAAgD5/wwAAgD5/w8AAgD5/xIAAgD5/xUAAgD5/xgAAgD5/xsAAgD5/x4AAgD5/yEAAgD6/97/AgD6/+H/AgD6/+T/AgD6/+f/AgD6/+r/AgD6/+3/AgD6//D/AgD6//P/AgD6//b/AgD6//n/AgD6//z/AgD6////AgD6/wIAAgD6/wUAAgD6/wgAAgD6/wsAAgD6/w4AAgD6/xEAAgD6/xQAAgD6/xcAAgD6/xoAAgD6/x0AAgD6/yAAAgD6/yMAAgD7/93/AgD7/+D/AgD7/+P/AgD7/+b/AgD7/+n/AgD7/+z/AgD7/+//AgD7//L/AgD7//X/AgD7//j/AgD7//v/AgD7//7/AgD7/wEAAgD7/wQAAgD7/wcAAgD7/woAAgD7/w0AAgD7/xAAAgD7/xMAAgD7/xYAAgD7/xkAAgD7/xwAAgD7/x8AAgD7/yIAAgD8/9z/AgD8/9//AgD8/+L/AgD8/+X/AgD8/+j/AgD8/+v/AgD8/+7/AgD8//H/AgD8//T/AgD8//f/AgD8//r/AgD8//3/AgD8/wAAAgD8/wMAAgD8/wYAAgD8/wkAAgD8/wwAAgD8/w8AAgD8/xIAAgD8/xUAAgD8/xgAAgD8/xsAAgD8/x4AAgD8/yEAAgD8/yQAAgD9/9v/AgD9/97/AgD9/+H/AgD9/+T/AgD9/+f/AgD9/+r/AgD9/+3/AgD9//D/AgD9//P/AgD9//b/AgD9//n/AgD9//z/AgD9////AgD9/wIAAgD9/wUAAgD9/wgAAgD9/wsAAgD9/w4AAgD9/xEAAgD9/xQAAgD9/xcAAgD9/xoAAgD9/x0AAgD9/yAAAgD9/yMAAgD9/yYAAgD+/9r/AgD+/93/AgD+/+D/AgD+/+P/AgD+/+b/AgD+/+n/AgD+/+z/AgD+/+//AgD+//L/AgD+//X/AgD+//j/AgD+//v/AgD+//7/AgD+/wEAAgD+/wQAAgD+/wcAAgD+/woAAgD+/w0AAgD+/xAAAgD+/xMAAgD+/xYAAgD+/xkAAgD+/xwAAgD+/x8AAgD+/yIAAgD+/yUAAgD//9z/AgD//9//AgD//+L/AgD//+X/AgD//+j/AgD//+v/AgD//+7/AgD///H/AgD///T/AgD///f/AgD///r/AgD///3/AgD//wAAAgD//wMAAgD//wYAAgD//wkAAgD//wwAAgD//w8AAgD//xIAAgD//xUAAgD//xgAAgD//xsAAgD//x4AAgD//yEAAgD//yQAAgAAANv/AgAAAN7/AgAAAOH/AgAAAOT/AgAAAOf/AgAAAOr/AgAAAO3/AgAAAPD/AgAAAPP/AgAAAPb/AgAAAPn/AgAAAPz/AgAAAP//AgAAAAIAAgAAAAUAAgAAAAgAAgAAAAsAAgAAAA4AAgAAABEAAgAAABQAAgAAABcAAgAAABoAAgAAAB0AAgAAACAAAgAAACMAAgAAACYAAgABANr/AgABAN3/AgABAOD/AgABAOP/AgABAOb/AgABAOn/AgABAOz/AgABAO//AgABAPL/AgABAPX/AgABAPj/AgABAPv/AgABAP7/AgABAAEAAgABAAQAAgABAAcAAgABAAoAAgABAA0AAgABABAAAgABABMAAgABABYAAgABABkAAgABABwAAgABAB8AAgABACIAAgABACUAAgACANz/AgACAN//AgACAOL/AgACAOX/AgACAOj/AgACAOv/AgACAO7/AgACAPH/AgACAPT/AgACAPf/AgACAPr/AgACAP3/AgACAAAAAgACAAMAAgACAAYAAgACAAkAAgACAAwAAgACAA8AAgACABIAAgACABUAAgACABgAAgACABsAAgACAB4AAgACACEAAgACACQAAgADAN7/AgADAOH/AgADAOT/AgADAOf/AgADAOr/AgADAO3/AgADAPD/AgADAPP/AgADAPb/AgADAPn/AgADAPz/AgADAP//AgADAAIAAgADAAUAAgADAAgAAgADAAsAAgADAA4AAgADABEAAgADABQAAgADABcAAgADABoAAgADAB0AAgADACAAAgADACMAAgAEAN3/AgAEAOD/AgAEAOP/AgAEAOb/AgAEAOn/AgAEAOz/AgAEAO//AgAEAPL/AgAEAPX/AgAEAPj/AgAEAPv/AgAEAP7/AgAEAAEAAgAEAAQAAgAEAAcAAgAEAAoAAgAEAA0AAgAEABAAAgAEABMAAgAEABYAAgAEABkAAgAEABwAAgAEAB8AAgAEACIAAgAFAN//AgAFAOL/AgAFAOX/AgAFAOj/AgAFAOv/AgAFAO7/AgAFAPH/AgAFAPT/AgAFAPf/AgAFAPr/AgAFAP3/AgAFAAAAAgAFAAMAAgAFAAYAAgAFAAkAAgAFAAwAAgAFAA8AAgAFABIAAgAFABUAAgAFABgAAgAFABsAAgAFAB4AAgAFACEAAgAGAOH/AgAGAOT/AgAGAOf/AgAGAOr/AgAGAO3/AgAGAPD/AgAGAPP/AgAGAPb/AgAGAPn/AgAGAPz/AgAGAP//AgAGAAIAAgAGAAUAAgAGAAgAAgAGAAsAAgAGAA4AAgAGABEAAgAGABQAAgAGABcAAgAGABoAAgAGAB0AAgAHAOP/AgAHAOb/AgAHAOn/AgAHAOz/AgAHAO//AgAHAPL/AgAHAPX/AgAHAPj/AgAHAPv/AgAHAP7/AgAHAAEAAgAHAAQAAgAHAAcAAgAHAAoAAgAHAA0AAgAHABAAAgAHABMAAgAHABYAAgAHABkAAgAHABwAAgAIAOj/AgAIAOv/AgAIAO7/AgAIAPH/AgAIAPT/AgAIAPf/AgAIAPr/AgAIAP3/AgAIAAAAAgAIAAMAAgAIAAYAAgAIAAkAAgAIAAwAAgAIAA8AAgAIABIAAgAIABUAAgAIABgAAgAJAOr/AgAJAO3/AgAJAPD/AgAJAPP/AgAJAPb/AgAJAPn/AgAJAPz/AgAJAP//AgAJAAIAAgAJAAUAAgAJAAgAAgAJAAsAAgAJAA4AAgAJABEAAgAJABQAAgAKAPL/AgAKAPX/AgAKAPj/AgAKAPv/AgAKAP7/AgAKAAEAAgAKAAQAAgAKAAcAAgAKAAoAAgAKAA0AAgAKABAAAgALAPr/AgALAP3/AgALAAAAAgALAAMAAgALAAYAAwDz//X/AwDz//j/AwDz//v/AwDz//7/AwDz/wEAAwDz/wQAAwDz/wcAAwDz/woAAwD0/+7/AwD0//H/AwD0//T/AwD0//f/AwD0//r/AwD0//3/AwD0/wAAAwD0/wMAAwD0/wYAAwD0/wkAAwD0/wwAAwD0/w8AAwD0/xIAAwD1/+r/AwD1/+3/AwD1//D/AwD1//P/AwD1//b/AwD1//n/AwD1//z/AwD1////AwD1/wIAAwD1/wUAAwD1/wgAAwD1/wsAAwD1/w4AAwD1/xEAAwD1/xQAAwD1/xcAAwD2/+b/AwD2/+n/AwD2/+z/AwD2/+//AwD2//L/AwD2//X/AwD2//j/AwD2//v/AwD2//7/AwD2/wEAAwD2/wQAAwD2/wcAAwD2/woAAwD2/w0AAwD2/xAAAwD2/xMAAwD2/xYAAwD2/xkAAwD3/+X/AwD3/+j/AwD3/+v/AwD3/+7/AwD3//H/AwD3//T/AwD3//f/AwD3//r/AwD3//3/AwD3/wAAAwD3/wMAAwD3/wYAAwD3/wkAAwD3/wwAAwD3/w8AAwD3/xIAAwD3/xUAAwD3/xgAAwD3/xsAAwD4/+H/AwD4/+T/AwD4/+f/AwD4/+r/AwD4/+3/AwD4//D/AwD4//P/AwD4//b/AwD4//n/AwD4//z/AwD4////AwD4/wIAAwD4/wUAAwD4/wgAAwD4/wsAAwD4/w4AAwD4/xEAAwD4/xQAAwD4/xcAAwD4/xoAAwD4/x0AAwD4/yAAAwD5/+D/AwD5/+P/AwD5/+b/AwD5/+n/AwD5/+z/AwD5/+//AwD5//L/AwD5//X/AwD5//j/AwD5//v/AwD5//7/AwD5/wEAAwD5/wQAAwD5/wcAAwD5/woAAwD5/w0AAwD5/xAAAwD5/xMAAwD5/xYAAwD5/xkAAwD5/xwAAwD5/x8AAwD6/9//AwD6/+L/AwD6/+X/AwD6/+j/AwD6/+v/AwD6/+7/AwD6//H/AwD6//T/AwD6//f/AwD6//r/AwD6//3/AwD6/wAAAwD6/wMAAwD6/wYAAwD6/wkAAwD6/wwAAwD6/w8AAwD6/xIAAwD6/xUAAwD6/xgAAwD6/xsAAwD6/x4AAwD6/yEAAwD7/97/AwD7/+H/AwD7/+T/AwD7/+f/AwD7/+r/AwD7/+3/AwD7//D/AwD7//P/AwD7//b/AwD7//n/AwD7//z/AwD7////AwD7/wIAAwD7/wUAAwD7/wgAAwD7/wsAAwD7/w4AAwD7/xEAAwD7/xQAAwD7/xcAAwD7/xoAAwD7/x0AAwD7/yAAAwD7/yMAAwD8/93/AwD8/+D/AwD8/+P/AwD8/+b/AwD8/+n/AwD8/+z/AwD8/+//AwD8//L/AwD8//X/AwD8//j/AwD8//v/AwD8//7/AwD8/wEAAwD8/wQAAwD8/wcAAwD8/woAAwD8/w0AAwD8/xAAAwD8/xMAAwD8/xYAAwD8/xkAAwD8/xwAAwD8/x8AAwD8/yIAAwD8/yUAAwD9/9z/AwD9/9//AwD9/+L/AwD9/+X/AwD9/+j/AwD9/+v/AwD9/+7/AwD9//H/AwD9//T/AwD9//f/AwD9//r/AwD9//3/AwD9/wAAAwD9/wMAAwD9/wYAAwD9/wkAAwD9/wwAAwD9/w8AAwD9/xIAAwD9/xUAAwD9/xgAAwD9/xsAAwD9/x4AAwD9/yEAAwD9/yQAAwD+/9v/AwD+/97/AwD+/+H/AwD+/+T/AwD+/+f/AwD+/+r/AwD+/+3/AwD+//D/AwD+//P/AwD+//b/AwD+//n/AwD+//z/AwD+////AwD+/wIAAwD+/wUAAwD+/wgAAwD+/wsAAwD+/w4AAwD+/xEAAwD+/xQAAwD+/xcAAwD+/xoAAwD+/x0AAwD+/yAAAwD+/yMAAwD+/yYAAwD//9r/AwD//93/AwD//+D/AwD//+P/AwD//+b/AwD//+n/AwD//+z/AwD//+//AwD///L/AwD///X/AwD///j/AwD///v/AwD///7/AwD//wEAAwD//wQAAwD//wcAAwD//woAAwD//w0AAwD//xAAAwD//xMAAwD//xYAAwD//xkAAwD//xwAAwD//x8AAwD//yIAAwD//yUAAwAAANz/AwAAAN//AwAAAOL/AwAAAOX/AwAAAOj/AwAAAOv/AwAAAO7/AwAAAPH/AwAAAPT/AwAAAPf/AwAAAPr/AwAAAP3/AwAAAAAAAwAAAAMAAwAAAAYAAwAAAAkAAwAAAAwAAwAAAA8AAwAAABIAAwAAABUAAwAAABgAAwAAABsAAwAAAB4AAwAAACEAAwAAACQAAwABANv/AwABAN7/AwABAOH/AwABAOT/AwABAOf/AwABAOr/AwABAO3/AwABAPD/AwABAPP/AwABAPb/AwABAPn/AwABAPz/AwABAP//AwABAAIAAwABAAUAAwABAAgAAwABAAsAAwABAA4AAwABABEAAwABABQAAwABABcAAwABABoAAwABAB0AAwABACAAAwABACMAAwACAN3/AwACAOD/AwACAOP/AwACAOb/AwACAOn/AwACAOz/AwACAO//AwACAPL/AwACAPX/AwACAPj/AwACAPv/AwACAP7/AwACAAEAAwACAAQAAwACAAcAAwACAAoAAwACAA0AAwACABAAAwACABMAAwACABYAAwACABkAAwACABwAAwACAB8AAwACACIAAwADAN//AwADAOL/AwADAOX/AwADAOj/AwADAOv/AwADAO7/AwADAPH/AwADAPT/AwADAPf/AwADAPr/AwADAP3/AwADAAAAAwADAAMAAwADAAYAAwADAAkAAwADAAwAAwADAA8AAwADABIAAwADABUAAwADABgAAwADABsAAwADAB4AAwADACEAAwAEAOH/AwAEAOT/AwAEAOf/AwAEAOr/AwAEAO3/AwAEAPD/AwAEAPP/AwAEAPb/AwAEAPn/AwAEAPz/AwAEAP//AwAEAAIAAwAEAAUAAwAEAAgAAwAEAAsAAwAEAA4AAwAEABEAAwAEABQAAwAEABcAAwAEABoAAwAEAB0AAwAEACAAAwAFAOD/AwAFAOP/AwAFAOb/AwAFAOn/AwAFAOz/AwAFAO//AwAFAPL/AwAFAPX/AwAFAPj/AwAFAPv/AwAFAP7/AwAFAAEAAwAFAAQAAwAFAAcAAwAFAAoAAwAFAA0AAwAFABAAAwAFABMAAwAFABYAAwAFABkAAwAFABwAAwAFAB8AAwAGAOX/AwAGAOj/AwAGAOv/AwAGAO7/AwAGAPH/AwAGAPT/AwAGAPf/AwAGAPr/AwAGAP3/AwAGAAAAAwAGAAMAAwAGAAYAAwAGAAkAAwAGAAwAAwAGAA8AAwAGABIAAwAGABUAAwAGABgAAwAGABsAAwAHAOf/AwAHAOr/AwAHAO3/AwAHAPD/AwAHAPP/AwAHAPb/AwAHAPn/AwAHAPz/AwAHAP//AwAHAAIAAwAHAAUAAwAHAAgAAwAHAAsAAwAHAA4AAwAHABEAAwAHABQAAwAHABcAAwAHABoAAwAIAOn/AwAIAOz/AwAIAO//AwAIAPL/AwAIAPX/AwAIAPj/AwAIAPv/AwAIAP7/AwAIAAEAAwAIAAQAAwAIAAcAAwAIAAoAAwAIAA0AAwAIABAAAwAIABMAAwAIABYAAwAJAO7/AwAJAPH/AwAJAPT/AwAJAPf/AwAJAPr/AwAJAP3/AwAJAAAAAwAJAAMAAwAJAAYAAwAJAAkAAwAJAAwAAwAJAA8AAwAJABIAAwAKAPb/AwAKAPn/AwAKAPz/AwAKAP//AwAKAAIAAwAKAAUAAwAKAAgAAwAKAAsABADz//P/BADz//b/BADz//n/BADz//z/BADz////BADz/wIABADz/wUABADz/wgABADz/wsABADz/w4ABAD0/+z/BAD0/+//BAD0//L/BAD0//X/BAD0//j/BAD0//v/BAD0//7/BAD0/wEABAD0/wQABAD0/wcABAD0/woABAD0/w0ABAD0/xAABAD0/xMABAD1/+j/BAD1/+v/BAD1/+7/BAD1//H/BAD1//T/BAD1//f/BAD1//r/BAD1//3/BAD1/wAABAD1/wMABAD1/wYABAD1/wkABAD1/wwABAD1/w8ABAD1/xIABAD1/xUABAD1/xgABAD2/+f/BAD2/+r/BAD2/+3/BAD2//D/BAD2//P/BAD2//b/BAD2//n/BAD2//z/BAD2////BAD2/wIABAD2/wUABAD2/wgABAD2/wsABAD2/w4ABAD2/xEABAD2/xQABAD2/xcABAD2/xoABAD3/+P/BAD3/+b/BAD3/+n/BAD3/+z/BAD3/+//BAD3//L/BAD3//X/BAD3//j/BAD3//v/BAD3//7/BAD3/wEABAD3/wQABAD3/wcABAD3/woABAD3/w0ABAD3/xAABAD3/xMABAD3/xYABAD3/xkABAD3/xwABAD4/+L/BAD4/+X/BAD4/+j/BAD4/+v/BAD4/+7/BAD4//H/BAD4//T/BAD4//f/BAD4//r/BAD4//3/BAD4/wAABAD4/wMABAD4/wYABAD4/wkABAD4/wwABAD4/w8ABAD4/xIABAD4/xUABAD4/xgABAD4/xsABAD4/x4ABAD5/+H/BAD5/+T/BAD5/+f/BAD5/+r/BAD5/+3/BAD5//D/BAD5//P/BAD5//b/BAD5//n/BAD5//z/BAD5////BAD5/wIABAD5/wUABAD5/wgABAD5/wsABAD5/w4ABAD5/xEABAD5/xQABAD5/xcABAD5/xoABAD5/x0ABAD5/yAABAD6/93/BAD6/+D/BAD6/+P/BAD6/+b/BAD6/+n/BAD6/+z/BAD6/+//BAD6//L/BAD6//X/BAD6//j/BAD6//v/BAD6//7/BAD6/wEABAD6/wQABAD6/wcABAD6/woABAD6/w0ABAD6/xAABAD6/xMABAD6/xYABAD6/xkABAD6/xwABAD6/x8ABAD6/yIABAD7/9z/BAD7/9//BAD7/+L/BAD7/+X/BAD7/+j/BAD7/+v/BAD7/+7/BAD7//H/BAD7//T/BAD7//f/BAD7//r/BAD7//3/BAD7/wAABAD7/wMABAD7/wYABAD7/wkABAD7/wwABAD7/w8ABAD7/xIABAD7/xUABAD7/xgABAD7/xsABAD7/x4ABAD7/yEABAD7/yQABAD8/97/BAD8/+H/BAD8/+T/BAD8/+f/BAD8/+r/BAD8/+3/BAD8//D/BAD8//P/BAD8//b/BAD8//n/BAD8//z/BAD8////BAD8/wIABAD8/wUABAD8/wgABAD8/wsABAD8/w4ABAD8/xEABAD8/xQABAD8/xcABAD8/xoABAD8/x0ABAD8/yAABAD8/yMABAD9/93/BAD9/+D/BAD9/+P/BAD9/+b/BAD9/+n/BAD9/+z/BAD9/+//BAD9//L/BAD9//X/BAD9//j/BAD9//v/BAD9//7/BAD9/wEABAD9/wQABAD9/wcABAD9/woABAD9/w0ABAD9/xAABAD9/xMABAD9/xYABAD9/xkABAD9/xwABAD9/x8ABAD9/yIABAD9/yUABAD+/9z/BAD+/9//BAD+/+L/BAD+/+X/BAD+/+j/BAD+/+v/BAD+/+7/BAD+//H/BAD+//T/BAD+//f/BAD+//r/BAD+//3/BAD+/wAABAD+/wMABAD+/wYABAD+/wkABAD+/wwABAD+/w8ABAD+/xIABAD+/xUABAD+/xgABAD+/xsABAD+/x4ABAD+/yEABAD+/yQABAD//9v/BAD//97/BAD//+H/BAD//+T/BAD//+f/BAD//+r/BAD//+3/BAD///D/BAD///P/BAD///b/BAD///n/BAD///z/BAD/////BAD//wIABAD//wUABAD//wgABAD//wsABAD//w4ABAD//xEABAD//xQABAD//xcABAD//xoABAD//x0ABAD//yAABAD//yMABAAAAN3/BAAAAOD/BAAAAOP/BAAAAOb/BAAAAOn/BAAAAOz/BAAAAO//BAAAAPL/BAAAAPX/BAAAAPj/BAAAAPv/BAAAAP7/BAAAAAEABAAAAAQABAAAAAcABAAAAAoABAAAAA0ABAAAABAABAAAABMABAAAABYABAAAABkABAAAABwABAAAAB8ABAAAACIABAABANz/BAABAN//BAABAOL/BAABAOX/BAABAOj/BAABAOv/BAABAO7/BAABAPH/BAABAPT/BAABAPf/BAABAPr/BAABAP3/BAABAAAABAABAAMABAABAAYABAABAAkABAABAAwABAABAA8ABAABABIABAABABUABAABABgABAABABsABAABAB4ABAABACEABAABACQABAACAN7/BAACAOH/BAACAOT/BAACAOf/BAACAOr/BAACAO3/BAACAPD/BAACAPP/BAACAPb/BAACAPn/BAACAPz/BAACAP//BAACAAIABAACAAUABAACAAgABAACAAsABAACAA4ABAACABEABAACABQABAACABcABAACABoABAACAB0ABAACACAABAACACMABAADAOD/BAADAOP/BAADAOb/BAADAOn/BAADAOz/BAADAO//BAADAPL/BAADAPX/BAADAPj/BAADAPv/BAADAP7/BAADAAEABAADAAQABAADAAcABAADAAoABAADAA0ABAADABAABAADABMABAADABYABAADABkABAADABwABAADAB8ABAAEAOL/BAAEAOX/BAAEAOj/BAAEAOv/BAAEAO7/BAAEAPH/BAAEAPT/BAAEAPf/BAAEAPr/BAAEAP3/BAAEAAAABAAEAAMABAAEAAYABAAEAAkABAAEAAwABAAEAA8ABAAEABIABAAEABUABAAEABgABAAEABsABAAEAB4ABAAFAOT/BAAFAOf/BAAFAOr/BAAFAO3/BAAFAPD/BAAFAPP/BAAFAPb/BAAFAPn/BAAFAPz/BAAFAP//BAAFAAIABAAFAAUABAAFAAgABAAFAAsABAAFAA4ABAAFABEABAAFABQABAAFABcABAAFABoABAAFAB0ABAAGAOb/BAAGAOn/BAAGAOz/BAAGAO//BAAGAPL/BAAGAPX/BAAGAPj/BAAGAPv/BAAGAP7/BAAGAAEABAAGAAQABAAGAAcABAAGAAoABAAGAA0ABAAGABAABAAGABMABAAGABYABAAGABkABAAHAOj/BAAHAOv/BAAHAO7/BAAHAPH/BAAHAPT/BAAHAPf/BAAHAPr/BAAHAP3/BAAHAAAABAAHAAMABAAHAAYABAAHAAkABAAHAAwABAAHAA8ABAAHABIABAAHABUABAAHABgABAAIAO3/BAAIAPD/BAAIAPP/BAAIAPb/BAAIAPn/BAAIAPz/BAAIAP//BAAIAAIABAAIAAUABAAIAAgABAAIAAsABAAIAA4ABAAIABEABAAIABQABAAJAPL/BAAJAPX/BAAJAPj/BAAJAPv/BAAJAP7/BAAJAAEABAAJAAQABAAJAAcABAAJAAoABAAJAA0ABQDy//7/BQDy/wEABQDy/wQABQDz//H/BQDz//T/BQDz//f/BQDz//r/BQDz//3/BQDz/wAABQDz/wMABQDz/wYABQDz/wkABQDz/wwABQDz/w8ABQD0/+3/BQD0//D/BQD0//P/BQD0//b/BQD0//n/BQD0//z/BQD0////BQD0/wIABQD0/wUABQD0/wgABQD0/wsABQD0/w4ABQD0/xEABQD0/xQABQD1/+n/BQD1/+z/BQD1/+//BQD1//L/BQD1//X/BQD1//j/BQD1//v/BQD1//7/BQD1/wEABQD1/wQABQD1/wcABQD1/woABQD1/w0ABQD1/xAABQD1/xMABQD1/xYABQD2/+X/BQD2/+j/BQD2/+v/BQD2/+7/BQD2//H/BQD2//T/BQD2//f/BQD2//r/BQD2//3/BQD2/wAABQD2/wMABQD2/wYABQD2/wkABQD2/wwABQD2/w8ABQD2/xIABQD2/xUABQD2/xgABQD2/xsABQD3/+T/BQD3/+f/BQD3/+r/BQD3/+3/BQD3//D/BQD3//P/BQD3//b/BQD3//n/BQD3//z/BQD3////BQD3/wIABQD3/wUABQD3/wgABQD3/wsABQD3/w4ABQD3/xEABQD3/xQABQD3/xcABQD3/xoABQD3/x0ABQD4/+D/BQD4/+P/BQD4/+b/BQD4/+n/BQD4/+z/BQD4/+//BQD4//L/BQD4//X/BQD4//j/BQD4//v/BQD4//7/BQD4/wEABQD4/wQABQD4/wcABQD4/woABQD4/w0ABQD4/xAABQD4/xMABQD4/xYABQD4/xkABQD4/xwABQD4/x8ABQD5/9//BQD5/+L/BQD5/+X/BQD5/+j/BQD5/+v/BQD5/+7/BQD5//H/BQD5//T/BQD5//f/BQD5//r/BQD5//3/BQD5/wAABQD5/wMABQD5/wYABQD5/wkABQD5/wwABQD5/w8ABQD5/xIABQD5/xUABQD5/xgABQD5/xsABQD5/x4ABQD5/yEABQD6/97/BQD6/+H/BQD6/+T/BQD6/+f/BQD6/+r/BQD6/+3/BQD6//D/BQD6//P/BQD6//b/BQD6//n/BQD6//z/BQD6////BQD6/wIABQD6/wUABQD6/wgABQD6/wsABQD6/w4ABQD6/xEABQD6/xQABQD6/xcABQD6/xoABQD6/x0ABQD6/yAABQD7/93/BQD7/+D/BQD7/+P/BQD7/+b/BQD7/+n/BQD7/+z/BQD7/+//BQD7//L/BQD7//X/BQD7//j/BQD7//v/BQD7//7/BQD7/wEABQD7/wQABQD7/wcABQD7/woABQD7/w0ABQD7/xAABQD7/xMABQD7/xYABQD7/xkABQD7/xwABQD7/x8ABQD7/yIABQD8/9z/BQD8/9//BQD8/+L/BQD8/+X/BQD8/+j/BQD8/+v/BQD8/+7/BQD8//H/BQD8//T/BQD8//f/BQD8//r/BQD8//3/BQD8/wAABQD8/wMABQD8/wYABQD8/wkABQD8/wwABQD8/w8ABQD8/xIABQD8/xUABQD8/xgABQD8/xsABQD8/x4ABQD8/yEABQD8/yQABQD9/97/BQD9/+H/BQD9/+T/BQD9/+f/BQD9/+r/BQD9/+3/BQD9//D/BQD9//P/BQD9//b/BQD9//n/BQD9//z/BQD9////BQD9/wIABQD9/wUABQD9/wgABQD9/wsABQD9/w4ABQD9/xEABQD9/xQABQD9/xcABQD9/xoABQD9/x0ABQD9/yAABQD9/yMABQD+/93/BQD+/+D/BQD+/+P/BQD+/+b/BQD+/+n/BQD+/+z/BQD+/+//BQD+//L/BQD+//X/BQD+//j/BQD+//v/BQD+//7/BQD+/wEABQD+/wQABQD+/wcABQD+/woABQD+/w0ABQD+/xAABQD+/xMABQD+/xYABQD+/xkABQD+/xwABQD+/x8ABQD+/yIABQD//9z/BQD//9//BQD//+L/BQD//+X/BQD//+j/BQD//+v/BQD//+7/BQD///H/BQD///T/BQD///f/BQD///r/BQD///3/BQD//wAABQD//wMABQD//wYABQD//wkABQD//wwABQD//w8ABQD//xIABQD//xUABQD//xgABQD//xsABQD//x4ABQD//yEABQD//yQABQAAAN7/BQAAAOH/BQAAAOT/BQAAAOf/BQAAAOr/BQAAAO3/BQAAAPD/BQAAAPP/BQAAAPb/BQAAAPn/BQAAAPz/BQAAAP//BQAAAAIABQAAAAUABQAAAAgABQAAAAsABQAAAA4ABQAAABEABQAAABQABQAAABcABQAAABoABQAAAB0ABQAAACAABQAAACMABQABAOD/BQABAOP/BQABAOb/BQABAOn/BQABAOz/BQABAO//BQABAPL/BQABAPX/BQABAPj/BQABAPv/BQABAP7/BQABAAEABQABAAQABQABAAcABQABAAoABQABAA0ABQABABAABQABABMABQABABYABQABABkABQABABwABQABAB8ABQABACIABQACAN//BQACAOL/BQACAOX/BQACAOj/BQACAOv/BQACAO7/BQACAPH/BQACAPT/BQACAPf/BQACAPr/BQACAP3/BQACAAAABQACAAMABQACAAYABQACAAkABQACAAwABQACAA8ABQACABIABQACABUABQACABgABQACABsABQACAB4ABQACACEABQADAOH/BQADAOT/BQADAOf/BQADAOr/BQADAO3/BQADAPD/BQADAPP/BQADAPb/BQADAPn/BQADAPz/BQADAP//BQADAAIABQADAAUABQADAAgABQADAAsABQADAA4ABQADABEABQADABQABQADABcABQADABoABQADAB0ABQADACAABQAEAOP/BQAEAOb/BQAEAOn/BQAEAOz/BQAEAO//BQAEAPL/BQAEAPX/BQAEAPj/BQAEAPv/BQAEAP7/BQAEAAEABQAEAAQABQAEAAcABQAEAAoABQAEAA0ABQAEABAABQAEABMABQAEABYABQAEABkABQAEABwABQAFAOX/BQAFAOj/BQAFAOv/BQAFAO7/BQAFAPH/BQAFAPT/BQAFAPf/BQAFAPr/BQAFAP3/BQAFAAAABQAFAAMABQAFAAYABQAFAAkABQAFAAwABQAFAA8ABQAFABIABQAFABUABQAFABgABQAFABsABQAGAOr/BQAGAO3/BQAGAPD/BQAGAPP/BQAGAPb/BQAGAPn/BQAGAPz/BQAGAP//BQAGAAIABQAGAAUABQAGAAgABQAGAAsABQAGAA4ABQAGABEABQAGABQABQAGABcABQAHAOz/BQAHAO//BQAHAPL/BQAHAPX/BQAHAPj/BQAHAPv/BQAHAP7/BQAHAAEABQAHAAQABQAHAAcABQAHAAoABQAHAA0ABQAHABAABQAHABMABQAIAPH/BQAIAPT/BQAIAPf/BQAIAPr/BQAIAP3/BQAIAAAABQAIAAMABQAIAAYABQAIAAkABQAIAAwABQAIAA8ABQAJAPz/BQAJAP//BQAJAAIABgDy//n/BgDy//z/BgDy////BgDy/wIABgDy/wUABgDz//L/BgDz//X/BgDz//j/BgDz//v/BgDz//7/BgDz/wEABgDz/wQABgDz/wcABgDz/woABgDz/w0ABgDz/xAABgD0/+v/BgD0/+7/BgD0//H/BgD0//T/BgD0//f/BgD0//r/BgD0//3/BgD0/wAABgD0/wMABgD0/wYABgD0/wkABgD0/wwABgD0/w8ABgD0/xIABgD0/xUABgD1/+r/BgD1/+3/BgD1//D/BgD1//P/BgD1//b/BgD1//n/BgD1//z/BgD1////BgD1/wIABgD1/wUABgD1/wgABgD1/wsABgD1/w4ABgD1/xEABgD1/xQABgD1/xcABgD2/+b/BgD2/+n/BgD2/+z/BgD2/+//BgD2//L/BgD2//X/BgD2//j/BgD2//v/BgD2//7/BgD2/wEABgD2/wQABgD2/wcABgD2/woABgD2/w0ABgD2/xAABgD2/xMABgD2/xYABgD2/xkABgD3/+X/BgD3/+j/BgD3/+v/BgD3/+7/BgD3//H/BgD3//T/BgD3//f/BgD3//r/BgD3//3/BgD3/wAABgD3/wMABgD3/wYABgD3/wkABgD3/wwABgD3/w8ABgD3/xIABgD3/xUABgD3/xgABgD3/xsABgD4/+H/BgD4/+T/BgD4/+f/BgD4/+r/BgD4/+3/BgD4//D/BgD4//P/BgD4//b/BgD4//n/BgD4//z/BgD4////BgD4/wIABgD4/wUABgD4/wgABgD4/wsABgD4/w4ABgD4/xEABgD4/xQABgD4/xcABgD4/xoABgD4/x0ABgD5/+D/BgD5/+P/BgD5/+b/BgD5/+n/BgD5/+z/BgD5/+//BgD5//L/BgD5//X/BgD5//j/BgD5//v/BgD5//7/BgD5/wEABgD5/wQABgD5/wcABgD5/woABgD5/w0ABgD5/xAABgD5/xMABgD5/xYABgD5/xkABgD5/xwABgD5/x8ABgD6/9//BgD6/+L/BgD6/+X/BgD6/+j/BgD6/+v/BgD6/+7/BgD6//H/BgD6//T/BgD6//f/BgD6//r/BgD6//3/BgD6/wAABgD6/wMABgD6/wYABgD6/wkABgD6/wwABgD6/w8ABgD6/xIABgD6/xUABgD6/xgABgD6/xsABgD6/x4ABgD6/yEABgD7/97/BgD7/+H/BgD7/+T/BgD7/+f/BgD7/+r/BgD7/+3/BgD7//D/BgD7//P/BgD7//b/BgD7//n/BgD7//z/BgD7////BgD7/wIABgD7/wUABgD7/wgABgD7/wsABgD7/w4ABgD7/xEABgD7/xQABgD7/xcABgD7/xoABgD7/x0ABgD7/yAABgD8/93/BgD8/+D/BgD8/+P/BgD8/+b/BgD8/+n/BgD8/+z/BgD8/+//BgD8//L/BgD8//X/BgD8//j/BgD8//v/BgD8//7/BgD8/wEABgD8/wQABgD8/wcABgD8/woABgD8/w0ABgD8/xAABgD8/xMABgD8/xYABgD8/xkABgD8/xwABgD8/x8ABgD8/yIABgD9/9//BgD9/+L/BgD9/+X/BgD9/+j/BgD9/+v/BgD9/+7/BgD9//H/BgD9//T/BgD9//f/BgD9//r/BgD9//3/BgD9/wAABgD9/wMABgD9/wYABgD9/wkABgD9/wwABgD9/w8ABgD9/xIABgD9/xUABgD9/xgABgD9/xsABgD9/x4ABgD9/yEABgD+/97/BgD+/+H/BgD+/+T/BgD+/+f/BgD+/+r/BgD+/+3/BgD+//D/BgD+//P/BgD+//b/BgD+//n/BgD+//z/BgD+////BgD+/wIABgD+/wUABgD+/wgABgD+/wsABgD+/w4ABgD+/xEABgD+/xQABgD+/xcABgD+/xoABgD+/x0ABgD+/yAABgD+/yMABgD//+D/BgD//+P/BgD//+b/BgD//+n/BgD//+z/BgD//+//BgD///L/BgD///X/BgD///j/BgD///v/BgD///7/BgD//wEABgD//wQABgD//wcABgD//woABgD//w0ABgD//xAABgD//xMABgD//xYABgD//xkABgD//xwABgD//x8ABgD//yIABgAAAN//BgAAAOL/BgAAAOX/BgAAAOj/BgAAAOv/BgAAAO7/BgAAAPH/BgAAAPT/BgAAAPf/BgAAAPr/BgAAAP3/BgAAAAAABgAAAAMABgAAAAYABgAAAAkABgAAAAwABgAAAA8ABgAAABIABgAAABUABgAAABgABgAAABsABgAAAB4ABgAAACEABgABAOH/BgABAOT/BgABAOf/BgABAOr/BgABAO3/BgABAPD/BgABAPP/BgABAPb/BgABAPn/BgABAPz/BgABAP//BgABAAIABgABAAUABgABAAgABgABAAsABgABAA4ABgABABEABgABABQABgABABcABgABABoABgABAB0ABgABACAABgACAOP/BgACAOb/BgACAOn/BgACAOz/BgACAO//BgACAPL/BgACAPX/BgACAPj/BgACAPv/BgACAP7/BgACAAEABgACAAQABgACAAcABgACAAoABgACAA0ABgACABAABgACABMABgACABYABgACABkABgACABwABgACAB8ABgADAOX/BgADAOj/BgADAOv/BgADAO7/BgADAPH/BgADAPT/BgADAPf/BgADAPr/BgADAP3/BgADAAAABgADAAMABgADAAYABgADAAkABgADAAwABgADAA8ABgADABIABgADABUABgADABgABgADABsABgAEAOf/BgAEAOr/BgAEAO3/BgAEAPD/BgAEAPP/BgAEAPb/BgAEAPn/BgAEAPz/BgAEAP//BgAEAAIABgAEAAUABgAEAAgABgAEAAsABgAEAA4ABgAEABEABgAEABQABgAEABcABgAEABoABgAFAOn/BgAFAOz/BgAFAO//BgAFAPL/BgAFAPX/BgAFAPj/BgAFAPv/BgAFAP7/BgAFAAEABgAFAAQABgAFAAcABgAFAAoABgAFAA0ABgAFABAABgAFABMABgAFABYABgAGAOv/BgAGAO7/BgAGAPH/BgAGAPT/BgAGAPf/BgAGAPr/BgAGAP3/BgAGAAAABgAGAAMABgAGAAYABgAGAAkABgAGAAwABgAGAA8ABgAGABIABgAGABUABgAHAPD/BgAHAPP/BgAHAPb/BgAHAPn/BgAHAPz/BgAHAP//BgAHAAIABgAHAAUABgAHAAgABgAHAAsABgAHAA4ABgAIAPv/BgAIAP7/BgAIAAEABgAIAAQABgAIAAcABwDy//r/BwDy//3/BwDy/wAABwDy/wMABwDy/wYABwDz//D/BwDz//P/BwDz//b/BwDz//n/BwDz//z/BwDz////BwDz/wIABwDz/wUABwDz/wgABwDz/wsABwDz/w4ABwD0/+z/BwD0/+//BwD0//L/BwD0//X/BwD0//j/BwD0//v/BwD0//7/BwD0/wEABwD0/wQABwD0/wcABwD0/woABwD0/w0ABwD0/xAABwD0/xMABwD1/+j/BwD1/+v/BwD1/+7/BwD1//H/BwD1//T/BwD1//f/BwD1//r/BwD1//3/BwD1/wAABwD1/wMABwD1/wYABwD1/wkABwD1/wwABwD1/w8ABwD1/xIABwD1/xUABwD1/xgABwD2/+f/BwD2/+r/BwD2/+3/BwD2//D/BwD2//P/BwD2//b/BwD2//n/BwD2//z/BwD2////BwD2/wIABwD2/wUABwD2/wgABwD2/wsABwD2/w4ABwD2/xEABwD2/xQABwD2/xcABwD2/xoABwD3/+P/BwD3/+b/BwD3/+n/BwD3/+z/BwD3/+//BwD3//L/BwD3//X/BwD3//j/BwD3//v/BwD3//7/BwD3/wEABwD3/wQABwD3/wcABwD3/woABwD3/w0ABwD3/xAABwD3/xMABwD3/xYABwD3/xkABwD3/xwABwD4/+L/BwD4/+X/BwD4/+j/BwD4/+v/BwD4/+7/BwD4//H/BwD4//T/BwD4//f/BwD4//r/BwD4//3/BwD4/wAABwD4/wMABwD4/wYABwD4/wkABwD4/wwABwD4/w8ABwD4/xIABwD4/xUABwD4/xgABwD4/xsABwD4/x4ABwD5/+H/BwD5/+T/BwD5/+f/BwD5/+r/BwD5/+3/BwD5//D/BwD5//P/BwD5//b/BwD5//n/BwD5//z/BwD5////BwD5/wIABwD5/wUABwD5/wgABwD5/wsABwD5/w4ABwD5/xEABwD5/xQABwD5/xcABwD5/xoABwD5/x0ABwD5/yAABwD6/+D/BwD6/+P/BwD6/+b/BwD6/+n/BwD6/+z/BwD6/+//BwD6//L/BwD6//X/BwD6//j/BwD6//v/BwD6//7/BwD6/wEABwD6/wQABwD6/wcABwD6/woABwD6/w0ABwD6/xAABwD6/xMABwD6/xYABwD6/xkABwD6/xwABwD6/x8ABwD7/9//BwD7/+L/BwD7/+X/BwD7/+j/BwD7/+v/BwD7/+7/BwD7//H/BwD7//T/BwD7//f/BwD7//r/BwD7//3/BwD7/wAABwD7/wMABwD7/wYABwD7/wkABwD7/wwABwD7/w8ABwD7/xIABwD7/xUABwD7/xgABwD7/xsABwD7/x4ABwD7/yEABwD8/+H/BwD8/+T/BwD8/+f/BwD8/+r/BwD8/+3/BwD8//D/BwD8//P/BwD8//b/BwD8//n/BwD8//z/BwD8////BwD8/wIABwD8/wUABwD8/wgABwD8/wsABwD8/w4ABwD8/xEABwD8/xQABwD8/xcABwD8/xoABwD8/x0ABwD8/yAABwD9/+D/BwD9/+P/BwD9/+b/BwD9/+n/BwD9/+z/BwD9/+//BwD9//L/BwD9//X/BwD9//j/BwD9//v/BwD9//7/BwD9/wEABwD9/wQABwD9/wcABwD9/woABwD9/w0ABwD9/xAABwD9/xMABwD9/xYABwD9/xkABwD9/xwABwD9/x8ABwD+/9//BwD+/+L/BwD+/+X/BwD+/+j/BwD+/+v/BwD+/+7/BwD+//H/BwD+//T/BwD+//f/BwD+//r/BwD+//3/BwD+/wAABwD+/wMABwD+/wYABwD+/wkABwD+/wwABwD+/w8ABwD+/xIABwD+/xUABwD+/xgABwD+/xsABwD+/x4ABwD+/yEABwD//+H/BwD//+T/BwD//+f/BwD//+r/BwD//+3/BwD///D/BwD///P/BwD///b/BwD///n/BwD///z/BwD/////BwD//wIABwD//wUABwD//wgABwD//wsABwD//w4ABwD//xEABwD//xQABwD//xcABwD//xoABwD//x0ABwD//yAABwAAAOD/BwAAAOP/BwAAAOb/BwAAAOn/BwAAAOz/BwAAAO//BwAAAPL/BwAAAPX/BwAAAPj/BwAAAPv/BwAAAP7/BwAAAAEABwAAAAQABwAAAAcABwAAAAoABwAAAA0ABwAAABAABwAAABMABwAAABYABwAAABkABwAAABwABwAAAB8ABwABAOL/BwABAOX/BwABAOj/BwABAOv/BwABAO7/BwABAPH/BwABAPT/BwABAPf/BwABAPr/BwABAP3/BwABAAAABwABAAMABwABAAYABwABAAkABwABAAwABwABAA8ABwABABIABwABABUABwABABgABwABABsABwABAB4ABwACAOT/BwACAOf/BwACAOr/BwACAO3/BwACAPD/BwACAPP/BwACAPb/BwACAPn/BwACAPz/BwACAP//BwACAAIABwACAAUABwACAAgABwACAAsABwACAA4ABwACABEABwACABQABwACABcABwACABoABwACAB0ABwADAOb/BwADAOn/BwADAOz/BwADAO//BwADAPL/BwADAPX/BwADAPj/BwADAPv/BwADAP7/BwADAAEABwADAAQABwADAAcABwADAAoABwADAA0ABwADABAABwADABMABwADABYABwADABkABwAEAOj/BwAEAOv/BwAEAO7/BwAEAPH/BwAEAPT/BwAEAPf/BwAEAPr/BwAEAP3/BwAEAAAABwAEAAMABwAEAAYABwAEAAkABwAEAAwABwAEAA8ABwAEABIABwAEABUABwAEABgABwAFAO3/BwAFAPD/BwAFAPP/BwAFAPb/BwAFAPn/BwAFAPz/BwAFAP//BwAFAAIABwAFAAUABwAFAAgABwAFAAsABwAFAA4ABwAFABEABwAFABQABwAGAPL/BwAGAPX/BwAGAPj/BwAGAPv/BwAGAP7/BwAGAAEABwAGAAQABwAGAAcABwAGAAoABwAGAA0ABwAGABAABwAHAPr/BwAHAP3/BwAHAAAABwAHAAMABwAHAAYACADy//v/CADy//7/CADy/wEACADy/wQACADy/wcACADz//H/CADz//T/CADz//f/CADz//r/CADz//3/CADz/wAACADz/wMACADz/wYACADz/wkACADz/wwACADz/w8ACAD0/+3/CAD0//D/CAD0//P/CAD0//b/CAD0//n/CAD0//z/CAD0////CAD0/wIACAD0/wUACAD0/wgACAD0/wsACAD0/w4ACAD0/xEACAD0/xQACAD1/+n/CAD1/+z/CAD1/+//CAD1//L/CAD1//X/CAD1//j/CAD1//v/CAD1//7/CAD1/wEACAD1/wQACAD1/wcACAD1/woACAD1/w0ACAD1/xAACAD1/xMACAD1/xYACAD2/+j/CAD2/+v/CAD2/+7/CAD2//H/CAD2//T/CAD2//f/CAD2//r/CAD2//3/CAD2/wAACAD2/wMACAD2/wYACAD2/wkACAD2/wwACAD2/w8ACAD2/xIACAD2/xUACAD2/xgACAD3/+T/CAD3/+f/CAD3/+r/CAD3/+3/CAD3//D/CAD3//P/CAD3//b/CAD3//n/CAD3//z/CAD3////CAD3/wIACAD3/wUACAD3/wgACAD3/wsACAD3/w4ACAD3/xEACAD3/xQACAD3/xcACAD3/xoACAD4/+P/CAD4/+b/CAD4/+n/CAD4/+z/CAD4/+//CAD4//L/CAD4//X/CAD4//j/CAD4//v/CAD4//7/CAD4/wEACAD4/wQACAD4/wcACAD4/woACAD4/w0ACAD4/xAACAD4/xMACAD4/xYACAD4/xkACAD4/xwACAD5/+L/CAD5/+X/CAD5/+j/CAD5/+v/CAD5/+7/CAD5//H/CAD5//T/CAD5//f/CAD5//r/CAD5//3/CAD5/wAACAD5/wMACAD5/wYACAD5/wkACAD5/wwACAD5/w8ACAD5/xIACAD5/xUACAD5/xgACAD5/xsACAD5/x4ACAD6/+H/CAD6/+T/CAD6/+f/CAD6/+r/CAD6/+3/CAD6//D/CAD6//P/CAD6//b/CAD6//n/CAD6//z/CAD6////CAD6/wIACAD6/wUACAD6/wgACAD6/wsACAD6/w4ACAD6/xEACAD6/xQACAD6/xcACAD6/xoACAD6/x0ACAD7/+D/CAD7/+P/CAD7/+b/CAD7/+n/CAD7/+z/CAD7/+//CAD7//L/CAD7//X/CAD7//j/CAD7//v/CAD7//7/CAD7/wEACAD7/wQACAD7/wcACAD7/woACAD7/w0ACAD7/xAACAD7/xMACAD7/xYACAD7/xkACAD7/xwACAD7/x8ACAD8/+L/CAD8/+X/CAD8/+j/CAD8/+v/CAD8/+7/CAD8//H/CAD8//T/CAD8//f/CAD8//r/CAD8//3/CAD8/wAACAD8/wMACAD8/wYACAD8/wkACAD8/wwACAD8/w8ACAD8/xIACAD8/xUACAD8/xgACAD8/xsACAD8/x4ACAD9/+H/CAD9/+T/CAD9/+f/CAD9/+r/CAD9/+3/CAD9//D/CAD9//P/CAD9//b/CAD9//n/CAD9//z/CAD9////CAD9/wIACAD9/wUACAD9/wgACAD9/wsACAD9/w4ACAD9/xEACAD9/xQACAD9/xcACAD9/xoACAD9/x0ACAD9/yAACAD+/+P/CAD+/+b/CAD+/+n/CAD+/+z/CAD+/+//CAD+//L/CAD+//X/CAD+//j/CAD+//v/CAD+//7/CAD+/wEACAD+/wQACAD+/wcACAD+/woACAD+/w0ACAD+/xAACAD+/xMACAD+/xYACAD+/xkACAD+/xwACAD+/x8ACAD//+L/CAD//+X/CAD//+j/CAD//+v/CAD//+7/CAD///H/CAD///T/CAD///f/CAD///r/CAD///3/CAD//wAACAD//wMACAD//wYACAD//wkACAD//wwACAD//w8ACAD//xIACAD//xUACAD//xgACAD//xsACAD//x4ACAAAAOT/CAAAAOf/CAAAAOr/CAAAAO3/CAAAAPD/CAAAAPP/CAAAAPb/CAAAAPn/CAAAAPz/CAAAAP//CAAAAAIACAAAAAUACAAAAAgACAAAAAsACAAAAA4ACAAAABEACAAAABQACAAAABcACAAAABoACAAAAB0ACAABAOb/CAABAOn/CAABAOz/CAABAO//CAABAPL/CAABAPX/CAABAPj/CAABAPv/CAABAP7/CAABAAEACAABAAQACAABAAcACAABAAoACAABAA0ACAABABAACAABABMACAABABYACAABABkACAABABwACAACAOj/CAACAOv/CAACAO7/CAACAPH/CAACAPT/CAACAPf/CAACAPr/CAACAP3/CAACAAAACAACAAMACAACAAYACAACAAkACAACAAwACAACAA8ACAACABIACAACABUACAACABgACAADAOr/CAADAO3/CAADAPD/CAADAPP/CAADAPb/CAADAPn/CAADAPz/CAADAP//CAADAAIACAADAAUACAADAAgACAADAAsACAADAA4ACAADABEACAADABQACAADABcACAAEAOz/CAAEAO//CAAEAPL/CAAEAPX/CAAEAPj/CAAEAPv/CAAEAP7/CAAEAAEACAAEAAQACAAEAAcACAAEAAoACAAEAA0ACAAEABAACAAEABMACAAFAPH/CAAFAPT/CAAFAPf/CAAFAPr/CAAFAP3/CAAFAAAACAAFAAMACAAFAAYACAAFAAkACAAFAAwACAAFAA8ACAAGAPn/CAAGAPz/CAAGAP//CAAGAAIACAAGAAUACQDy//z/CQDy////CQDy/wIACQDz//L/CQDz//X/CQDz//j/CQDz//v/CQDz//7/CQDz/wEACQDz/wQACQDz/wcACQDz/woACQDz/w0ACQD0/+7/CQD0//H/CQD0//T/CQD0//f/CQD0//r/CQD0//3/CQD0/wAACQD0/wMACQD0/wYACQD0/wkACQD0/wwACQD0/w8ACQD0/xIACQD1/+r/CQD1/+3/CQD1//D/CQD1//P/CQD1//b/CQD1//n/CQD1//z/CQD1////CQD1/wIACQD1/wUACQD1/wgACQD1/wsACQD1/w4ACQD1/xEACQD1/xQACQD2/+n/CQD2/+z/CQD2/+//CQD2//L/CQD2//X/CQD2//j/CQD2//v/CQD2//7/CQD2/wEACQD2/wQACQD2/wcACQD2/woACQD2/w0ACQD2/xAACQD2/xMACQD2/xYACQD3/+j/CQD3/+v/CQD3/+7/CQD3//H/CQD3//T/CQD3//f/CQD3//r/CQD3//3/CQD3/wAACQD3/wMACQD3/wYACQD3/wkACQD3/wwACQD3/w8ACQD3/xIACQD3/xUACQD3/xgACQD4/+T/CQD4/+f/CQD4/+r/CQD4/+3/CQD4//D/CQD4//P/CQD4//b/CQD4//n/CQD4//z/CQD4////CQD4/wIACQD4/wUACQD4/wgACQD4/wsACQD4/w4ACQD4/xEACQD4/xQACQD4/xcACQD4/xoACQD5/+P/CQD5/+b/CQD5/+n/CQD5/+z/CQD5/+//CQD5//L/CQD5//X/CQD5//j/CQD5//v/CQD5//7/CQD5/wEACQD5/wQACQD5/wcACQD5/woACQD5/w0ACQD5/xAACQD5/xMACQD5/xYACQD5/xkACQD5/xwACQD6/+X/CQD6/+j/CQD6/+v/CQD6/+7/CQD6//H/CQD6//T/CQD6//f/CQD6//r/CQD6//3/CQD6/wAACQD6/wMACQD6/wYACQD6/wkACQD6/wwACQD6/w8ACQD6/xIACQD6/xUACQD6/xgACQD6/xsACQD7/+T/CQD7/+f/CQD7/+r/CQD7/+3/CQD7//D/CQD7//P/CQD7//b/CQD7//n/CQD7//z/CQD7////CQD7/wIACQD7/wUACQD7/wgACQD7/wsACQD7/w4ACQD7/xEACQD7/xQACQD7/xcACQD7/xoACQD7/x0ACQD8/+P/CQD8/+b/CQD8/+n/CQD8/+z/CQD8/+//CQD8//L/CQD8//X/CQD8//j/CQD8//v/CQD8//7/CQD8/wEACQD8/wQACQD8/wcACQD8/woACQD8/w0ACQD8/xAACQD8/xMACQD8/xYACQD8/xkACQD8/xwACQD9/+X/CQD9/+j/CQD9/+v/CQD9/+7/CQD9//H/CQD9//T/CQD9//f/CQD9//r/CQD9//3/CQD9/wAACQD9/wMACQD9/wYACQD9/wkACQD9/wwACQD9/w8ACQD9/xIACQD9/xUACQD9/xgACQD9/xsACQD+/+T/CQD+/+f/CQD+/+r/CQD+/+3/CQD+//D/CQD+//P/CQD+//b/CQD+//n/CQD+//z/CQD+////CQD+/wIACQD+/wUACQD+/wgACQD+/wsACQD+/w4ACQD+/xEACQD+/xQACQD+/xcACQD+/xoACQD+/x0ACQD//+b/CQD//+n/CQD//+z/CQD//+//CQD///L/CQD///X/CQD///j/CQD///v/CQD///7/CQD//wEACQD//wQACQD//wcACQD//woACQD//w0ACQD//xAACQD//xMACQD//xYACQD//xkACQD//xwACQAAAOj/CQAAAOv/CQAAAO7/CQAAAPH/CQAAAPT/CQAAAPf/CQAAAPr/CQAAAP3/CQAAAAAACQAAAAMACQAAAAYACQAAAAkACQAAAAwACQAAAA8ACQAAABIACQAAABUACQAAABgACQABAOr/CQABAO3/CQABAPD/CQABAPP/CQABAPb/CQABAPn/CQABAPz/CQABAP//CQABAAIACQABAAUACQABAAgACQABAAsACQABAA4ACQABABEACQABABQACQABABcACQACAOz/CQACAO//CQACAPL/CQACAPX/CQACAPj/CQACAPv/CQACAP7/CQACAAEACQACAAQACQACAAcACQACAAoACQACAA0ACQACABAACQACABMACQACABYACQADAO7/CQADAPH/CQADAPT/CQADAPf/CQADAPr/CQADAP3/CQADAAAACQADAAMACQADAAYACQADAAkACQADAAwACQADAA8ACQADABIACQAEAPP/CQAEAPb/CQAEAPn/CQAEAPz/CQAEAP//CQAEAAIACQAEAAUACQAEAAgACQAEAAsACQAEAA4ACQAFAP7/CQAFAAEACQAFAAQACgDz//b/CgDz//n/CgDz//z/CgDz////CgDz/wIACgDz/wUACgDz/wgACgDz/wsACgD0//L/CgD0//X/CgD0//j/CgD0//v/CgD0//7/CgD0/wEACgD0/wQACgD0/wcACgD0/woACgD0/w0ACgD0/xAACgD1/+7/CgD1//H/CgD1//T/CgD1//f/CgD1//r/CgD1//3/CgD1/wAACgD1/wMACgD1/wYACgD1/wkACgD1/wwACgD1/w8ACgD1/xIACgD2/+r/CgD2/+3/CgD2//D/CgD2//P/CgD2//b/CgD2//n/CgD2//z/CgD2////CgD2/wIACgD2/wUACgD2/wgACgD2/wsACgD2/w4ACgD2/xEACgD2/xQACgD3/+n/CgD3/+z/CgD3/+//CgD3//L/CgD3//X/CgD3//j/CgD3//v/CgD3//7/CgD3/wEACgD3/wQACgD3/wcACgD3/woACgD3/w0ACgD3/xAACgD3/xMACgD3/xYACgD4/+j/CgD4/+v/CgD4/+7/CgD4//H/CgD4//T/CgD4//f/CgD4//r/CgD4//3/CgD4/wAACgD4/wMACgD4/wYACgD4/wkACgD4/wwACgD4/w8ACgD4/xIACgD4/xUACgD4/xgACgD5/+f/CgD5/+r/CgD5/+3/CgD5//D/CgD5//P/CgD5//b/CgD5//n/CgD5//z/CgD5////CgD5/wIACgD5/wUACgD5/wgACgD5/wsACgD5/w4ACgD5/xEACgD5/xQACgD5/xcACgD5/xoACgD6/+b/CgD6/+n/CgD6/+z/CgD6/+//CgD6//L/CgD6//X/CgD6//j/CgD6//v/CgD6//7/CgD6/wEACgD6/wQACgD6/wcACgD6/woACgD6/w0ACgD6/xAACgD6/xMACgD6/xYACgD6/xkACgD7/+X/CgD7/+j/CgD7/+v/CgD7/+7/CgD7//H/CgD7//T/CgD7//f/CgD7//r/CgD7//3/CgD7/wAACgD7/wMACgD7/wYACgD7/wkACgD7/wwACgD7/w8ACgD7/xIACgD7/xUACgD7/xgACgD7/xsACgD8/+f/CgD8/+r/CgD8/+3/CgD8//D/CgD8//P/CgD8//b/CgD8//n/CgD8//z/CgD8////CgD8/wIACgD8/wUACgD8/wgACgD8/wsACgD8/w4ACgD8/xEACgD8/xQACgD8/xcACgD8/xoACgD9/+b/CgD9/+n/CgD9/+z/CgD9/+//CgD9//L/CgD9//X/CgD9//j/CgD9//v/CgD9//7/CgD9/wEACgD9/wQACgD9/wcACgD9/woACgD9/w0ACgD9/xAACgD9/xMACgD9/xYACgD9/xkACgD+/+j/CgD+/+v/CgD+/+7/CgD+//H/CgD+//T/CgD+//f/CgD+//r/CgD+//3/CgD+/wAACgD+/wMACgD+/wYACgD+/wkACgD+/wwACgD+/w8ACgD+/xIACgD+/xUACgD+/xgACgD//+r/CgD//+3/CgD///D/CgD///P/CgD///b/CgD///n/CgD///z/CgD/////CgD//wIACgD//wUACgD//wgACgD//wsACgD//w4ACgD//xEACgD//xQACgD//xcACgAAAOz/CgAAAO//CgAAAPL/CgAAAPX/CgAAAPj/CgAAAPv/CgAAAP7/CgAAAAEACgAAAAQACgAAAAcACgAAAAoACgAAAA0ACgAAABAACgAAABMACgAAABYACgABAO7/CgABAPH/CgABAPT/CgABAPf/CgABAPr/CgABAP3/CgABAAAACgABAAMACgABAAYACgABAAkACgABAAwACgABAA8ACgABABIACgACAPD/CgACAPP/CgACAPb/CgACAPn/CgACAPz/CgACAP//CgACAAIACgACAAUACgACAAgACgACAAsACgACAA4ACgADAPX/CgADAPj/CgADAPv/CgADAP7/CgADAAEACgADAAQACgADAAcACgADAAoACwDz//r/CwDz//3/CwDz/wAACwDz/wMACwDz/wYACwD0//P/CwD0//b/CwD0//n/CwD0//z/CwD0////CwD0/wIACwD0/wUACwD0/wgACwD0/wsACwD0/w4ACwD1/+//CwD1//L/CwD1//X/CwD1//j/CwD1//v/CwD1//7/CwD1/wEACwD1/wQACwD1/wcACwD1/woACwD1/w0ACwD1/xAACwD2/+7/CwD2//H/CwD2//T/CwD2//f/CwD2//r/CwD2//3/CwD2/wAACwD2/wMACwD2/wYACwD2/wkACwD2/wwACwD2/w8ACwD2/xIACwD3/+r/CwD3/+3/CwD3//D/CwD3//P/CwD3//b/CwD3//n/CwD3//z/CwD3////CwD3/wIACwD3/wUACwD3/wgACwD3/wsACwD3/w4ACwD3/xEACwD3/xQACwD4/+n/CwD4/+z/CwD4/+//CwD4//L/CwD4//X/CwD4//j/CwD4//v/CwD4//7/CwD4/wEACwD4/wQACwD4/wcACwD4/woACwD4/w0ACwD4/xAACwD4/xMACwD4/xYACwD5/+j/CwD5/+v/CwD5/+7/CwD5//H/CwD5//T/CwD5//f/CwD5//r/CwD5//3/CwD5/wAACwD5/wMACwD5/wYACwD5/wkACwD5/wwACwD5/w8ACwD5/xIACwD5/xUACwD5/xgACwD6/+r/CwD6/+3/CwD6//D/CwD6//P/CwD6//b/CwD6//n/CwD6//z/CwD6////CwD6/wIACwD6/wUACwD6/wgACwD6/wsACwD6/w4ACwD6/xEACwD6/xQACwD6/xcACwD7/+n/CwD7/+z/CwD7/+//CwD7//L/CwD7//X/CwD7//j/CwD7//v/CwD7//7/CwD7/wEACwD7/wQACwD7/wcACwD7/woACwD7/w0ACwD7/xAACwD7/xMACwD7/xYACwD8/+j/CwD8/+v/CwD8/+7/CwD8//H/CwD8//T/CwD8//f/CwD8//r/CwD8//3/CwD8/wAACwD8/wMACwD8/wYACwD8/wkACwD8/wwACwD8/w8ACwD8/xIACwD8/xUACwD8/xgACwD9/+r/CwD9/+3/CwD9//D/CwD9//P/CwD9//b/CwD9//n/CwD9//z/CwD9////CwD9/wIACwD9/wUACwD9/wgACwD9/wsACwD9/w4ACwD9/xEACwD9/xQACwD9/xcACwD+/+z/CwD+/+//CwD+//L/CwD+//X/CwD+//j/CwD+//v/CwD+//7/CwD+/wEACwD+/wQACwD+/wcACwD+/woACwD+/w0ACwD+/xAACwD+/xMACwD+/xYACwD//+7/CwD///H/CwD///T/CwD///f/CwD///r/CwD///3/CwD//wAACwD//wMACwD//wYACwD//wkACwD//wwACwD//w8ACwD//xIACwAAAPD/CwAAAPP/CwAAAPb/CwAAAPn/CwAAAPz/CwAAAP//CwAAAAIACwAAAAUACwAAAAgACwAAAAsACwAAAA4ACwAAABEACwABAPL/CwABAPX/CwABAPj/CwABAPv/CwABAP7/CwABAAEACwABAAQACwABAAcACwABAAoACwABAA0ACwACAPr/CwACAP3/CwACAAAACwACAAMACwACAAYADAD0//f/DAD0//r/DAD0//3/DAD0/wAADAD0/wMADAD0/wYADAD0/wkADAD1//P/DAD1//b/DAD1//n/DAD1//z/DAD1////DAD1/wIADAD1/wUADAD1/wgADAD1/wsADAD1/w4ADAD2//L/DAD2//X/DAD2//j/DAD2//v/DAD2//7/DAD2/wEADAD2/wQADAD2/wcADAD2/woADAD2/w0ADAD2/xAADAD3/+7/DAD3//H/DAD3//T/DAD3//f/DAD3//r/DAD3//3/DAD3/wAADAD3/wMADAD3/wYADAD3/wkADAD3/wwADAD3/w8ADAD3/xIADAD4/+3/DAD4//D/DAD4//P/DAD4//b/DAD4//n/DAD4//z/DAD4////DAD4/wIADAD4/wUADAD4/wgADAD4/wsADAD4/w4ADAD4/xEADAD4/xQADAD5/+z/DAD5/+//DAD5//L/DAD5//X/DAD5//j/DAD5//v/DAD5//7/DAD5/wEADAD5/wQADAD5/wcADAD5/woADAD5/w0ADAD5/xAADAD5/xMADAD6/+v/DAD6/+7/DAD6//H/DAD6//T/DAD6//f/DAD6//r/DAD6//3/DAD6/wAADAD6/wMADAD6/wYADAD6/wkADAD6/wwADAD6/w8ADAD6/xIADAD6/xUADAD7/+3/DAD7//D/DAD7//P/DAD7//b/DAD7//n/DAD7//z/DAD7////DAD7/wIADAD7/wUADAD7/wgADAD7/wsADAD7/w4ADAD7/xEADAD7/xQADAD8/+z/DAD8/+//DAD8//L/DAD8//X/DAD8//j/DAD8//v/DAD8//7/DAD8/wEADAD8/wQADAD8/wcADAD8/woADAD8/w0ADAD8/xAADAD8/xMADAD9/+7/DAD9//H/DAD9//T/DAD9//f/DAD9//r/DAD9//3/DAD9/wAADAD9/wMADAD9/wYADAD9/wkADAD9/wwADAD9/w8ADAD9/xIADAD+//D/DAD+//P/DAD+//b/DAD+//n/DAD+//z/DAD+////DAD+/wIADAD+/wUADAD+/wgADAD+/wsADAD+/w4ADAD///L/DAD///X/DAD///j/DAD///v/DAD///7/DAD//wEADAD//wQADAD//wcADAD//woADAD//w0ADAAAAPf/DAAAAPr/DAAAAP3/DAAAAAAADAAAAAMADAAAAAYADAAAAAkADQD1//r/DQD1//3/DQD1/wAADQD1/wMADQD1/wYADQD2//b/DQD2//n/DQD2//z/DQD2////DQD2/wIADQD2/wUADQD2/wgADQD2/wsADQD3//L/DQD3//X/DQD3//j/DQD3//v/DQD3//7/DQD3/wEADQD3/wQADQD3/wcADQD3/woADQD3/w0ADQD4//H/DQD4//T/DQD4//f/DQD4//r/DQD4//3/DQD4/wAADQD4/wMADQD4/wYADQD4/wkADQD4/wwADQD4/w8ADQD5//D/DQD5//P/DQD5//b/DQD5//n/DQD5//z/DQD5////DQD5/wIADQD5/wUADQD5/wgADQD5/wsADQD5/w4ADQD6//L/DQD6//X/DQD6//j/DQD6//v/DQD6//7/DQD6/wEADQD6/wQADQD6/wcADQD6/woADQD6/w0ADQD6/xAADQD7//H/DQD7//T/DQD7//f/DQD7//r/DQD7//3/DQD7/wAADQD7/wMADQD7/wYADQD7/wkADQD7/wwADQD7/w8ADQD8//P/DQD8//b/DQD8//n/DQD8//z/DQD8////DQD8/wIADQD8/wUADQD8/wgADQD8/wsADQD8/w4ADQD9//X/DQD9//j/DQD9//v/DQD9//7/DQD9/wEADQD9/wQADQD9/wcADQD9/woADQD+//r/DQD+//3/DQD+/wAADQD+/wMADQD+/wYADgD3//z/DgD3////DgD3/wIADgD4//v/DgD4//7/DgD4/wEADgD4/wQADgD4/wcADgD5//r/DgD5//3/DgD5/wAADgD5/wMADgD5/wYADgD6//n/DgD6//z/DgD6////DgD6/wIADgD6/wUADgD7//7/DgD7/wEADgD7/wQA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.009214336277859584,\"absorptive\":true,\"n_reflections\":9672,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":true}", +}; diff --git a/widget/js/diffsim/index.tsx b/widget/js/diffsim/index.tsx index 40d0b56e7..ff532efa6 100644 --- a/widget/js/diffsim/index.tsx +++ b/widget/js/diffsim/index.tsx @@ -23,15 +23,15 @@ import Tooltip from "@mui/material/Tooltip"; import { useTheme } from "../theme"; import { COLORMAP_NAMES } from "../colormaps"; import { downloadBlob } from "../format"; -import { Quat, Vec3, directionIndices, matTVec, qmult, qnormalize, quatFromAxisAngle, quatFromZoneAxis, quatToMatrix } from "./math"; +import { Quat, Vec3, directionIndices, matTVec, parseDirection, qmult, qnormalize, quatFromAxisAngle, quatFromZoneAxis, quatToMatrix, threeToFour } from "./math"; import { Reflection, blochIntensities, blochSolve, kinematicalTilted, kosselLines, kosselLookup, labReflections, - parseCrystal, parseKossel, hybridBeams, slabIntensities, + nanobeamIntensities, nanobeamSolve, parseCrystal, parseKossel, hybridBeams, precessionTilts, slabIntensities, } from "./physics"; import { cellGeometry, drawCell } from "./crystal3d"; import { - Frame, cbedImage, drawImage, drawKikuchiOverlay, drawKosselLines, drawMarkers, histogramBins, nanobeamImage, - setupCanvas, tiltGrid, toPx, + Frame, cbedImage, drawDisks, drawEwaldPanel, drawImage, drawKikuchiOverlay, drawKosselLines, drawMarkers, histogramBins, + nanobeamImage, setupCanvas, tiltGrid, toPx, } from "./pattern"; const DIRECT: Reflection = { index: -1, hkl: [0, 0, 0], g: [0, 0, 0], gLen: 0, s: 0 }; @@ -104,20 +104,10 @@ function LabeledSlider({ label, value, onChange, min, max, step, fmt, width = 20 ); } -function parseZoneAxis(text: string): Vec3 | null { - const t = text.trim().replace(/[\[\]()]/g, ""); - let parts: string[]; - if (/[\s,]/.test(t)) parts = t.split(/[\s,]+/).filter(Boolean); - else parts = t.match(/-?\d/g) || []; - if (parts.length !== 3) return null; - const v = parts.map(Number); - if (v.some((x) => !isFinite(x)) || v.every((x) => x === 0)) return null; - return v as Vec3; -} - -function fmtIndices(v: [number, number, number] | null, brackets = "[]"): string { +function fmtIndices(v: [number, number, number] | null, hexagonal = false): string { if (!v) return "—"; - return brackets[0] + v.map((h) => (h < 0 ? `${-h}̅` : `${h}`)).join("") + brackets[1]; + const idx: number[] = hexagonal ? threeToFour(v) : v; + return "[" + idx.map((h) => (h < 0 ? `${-h}̅` : `${h}`)).join("") + "]"; } // --------------------------------------------------------------------------- @@ -139,17 +129,23 @@ function DiffSim() { const [dynamical, setDynamical] = useModelState("dynamical"); const [thickness, setThickness] = useModelState("thickness_A"); const [semiconv, setSemiconv] = useModelState("semiconv_mrad"); + const [precession, setPrecession] = useModelState("precession_deg"); + const [nPrecession] = useModelState("n_precession"); const [sigma, setSigma] = useModelState("sigma_excitation"); const [stepDeg, setStepDeg] = useModelState("rotation_step_deg"); const [scaling, setScaling] = useModelState("scaling"); const [power, setPower] = useModelState("power"); const [cmap, setCmap] = useModelState("cmap"); + const [markerPower, setMarkerPower] = useModelState("marker_power"); + const [markerSize, setMarkerSize] = useModelState("marker_size"); const [vminPct, setVminPct] = useModelState("vmin_pct"); const [vmaxPct, setVmaxPct] = useModelState("vmax_pct"); const [showLabels, setShowLabels] = useModelState("show_labels"); + const [showHkl, setShowHkl] = useModelState("show_hkl"); const [showCellAxes, setShowCellAxes] = useModelState("show_cell_axes"); const [nCells, setNCells] = useModelState("n_cells"); const [polyhedra, setPolyhedra] = useModelState("polyhedra"); + const [showEwald, setShowEwald] = useModelState("show_ewald"); const [sizePref] = useModelState("size"); const [status] = useModelState("status"); @@ -176,7 +172,11 @@ function DiffSim() { window.addEventListener("resize", f); return () => window.removeEventListener("resize", f); }, []); + // pattern square S on the right; on the left the cell square Sc above the Ewald panel (Sc x Se), S = Sc + gap + Se const S = Math.max(220, Math.min(sizePref, winW - 40)); + const GAP = 8; + const Sc = showEwald ? Math.round((S - GAP) / 1.5) : S; + const Se = showEwald ? S - GAP - Sc : 0; // orientation: local quaternion for smooth dragging, pushed to the model with a throttle const [quat, setQuatLocal] = React.useState(orientation as Quat); @@ -221,9 +221,17 @@ function DiffSim() { if (da < -Math.PI) da += 2 * Math.PI; rotateLab([0, 0, 1], (-da * 180) / Math.PI, false); } + } else if (e.shiftKey) { + // shift-drag: twist about the beam (the desktop version of the two-finger gesture) + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2; + let da = Math.atan2(cur[1] - cy, cur[0] - cx) - Math.atan2(prev[1] - cy, prev[0] - cx); + if (da > Math.PI) da -= 2 * Math.PI; + if (da < -Math.PI) da += 2 * Math.PI; + rotateLab([0, 0, 1], (-da * 180) / Math.PI, false); } else { const dx = cur[0] - prev[0], dy = cur[1] - prev[1]; - const degPerPx = 180 / S; + const degPerPx = 180 / Sc; const ang = Math.hypot(dx, dy) * degPerPx; if (ang > 0) rotateLab([dy, dx, 0], ang, false); // trackball: the face nearest the viewer follows the pointer } @@ -266,6 +274,13 @@ function DiffSim() { if (da < -Math.PI) da += 2 * Math.PI; rotateLab([0, 0, 1], (-da * 180) / Math.PI, false); } + } else if (e.shiftKey) { + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2; + let da = Math.atan2(cur[1] - cy, cur[0] - cx) - Math.atan2(prev[1] - cy, prev[0] - cx); + if (da > Math.PI) da -= 2 * Math.PI; + if (da < -Math.PI) da += 2 * Math.PI; + rotateLab([0, 0, 1], (-da * 180) / Math.PI, false); } else { const dx = (viewX * (cur[0] - prev[0])) / patScale.current, dy = -(cur[1] - prev[1]) / patScale.current; shiftPattern(dx, dy, mode !== "kossel", false); @@ -276,11 +291,45 @@ function DiffSim() { patPointers.current.delete(e.pointerId); if (patPointers.current.size === 0) { setDragging(false); setQuat(quatRef.current, true); } }; + // Double-click on a visible disk: tilt the crystal to the exact Bragg + // condition of that reflection (two-beam: the Laue circle through 000 and + // g). A crystal tilt (wx, wy) about the lab axes changes s_g by + // wx g_y - wy g_x, so w = s_g (-g_y, g_x) / |g_xy|^2 zeroes it. On empty + // space the Laue-circle centre moves to the clicked point (same sense: + // the zone axis tilts away from the click by q / k0); in Kossel mode the + // clicked direction of the tilt map moves onto the axis. const onPatDoubleClick = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left, y = e.clientY - rect.top; + if (mode === "nanobeam" && crystal) { + const snapPx = Math.max(8, 1.2 * (render === "disks" ? k0 * Math.sin(alpha) * patScale.current : markerSize * (S / 420))); + // candidates under the click: several reflections of different g_z + // share one spot (in hcp the first HOLZ layer is only 0.21 1/A up), so + // take the one that needs the SMALLEST tilt to reach Bragg, and never + // jump by more than 5 degrees + let best: Reflection | null = null, bestTilt = (5 * Math.PI) / 180; + let iMax = 0; + for (let i = 1; i < nbBeams.length; i++) iMax = Math.max(iMax, nbInten[i] || 0); + for (let i = 0; i < nbBeams.length; i++) { + const b = nbBeams[i]; + if (b.index < 0 || !(nbInten[i] > 1e-4 * iMax)) continue; + const [px, py] = toPx(frame, b.g[0], b.g[1]); + if (Math.hypot(px - x, py - y) > snapPx) continue; + const gxy = Math.hypot(b.g[0], b.g[1]); + if (gxy < 1e-6) continue; + const tilt = Math.abs(b.s) / gxy; + if (tilt < bestTilt) { bestTilt = tilt; best = b; } + } + if (best) { + const gxy2 = best.g[0] ** 2 + best.g[1] ** 2; + const wx = (-best.s * best.g[1]) / gxy2, wy = (best.s * best.g[0]) / gxy2; + setQuat(qnormalize(qmult(quatFromAxisAngle([wx, wy, 0], Math.hypot(wx, wy)), quatRef.current)), true); + return; + } + } const qx = (viewX * (x - rect.width / 2)) / patScale.current, qy = -(y - rect.height / 2) / patScale.current; - shiftPattern(-qx, -qy, mode !== "kossel", true); + if (mode === "kossel") shiftPattern(-qx, -qy, false, true); + else shiftPattern(qx, qy, true, true); }; // ---- derived geometry --------------------------------------------------- @@ -294,23 +343,31 @@ function DiffSim() { const qual = QUALITY[quality] || QUALITY.medium; // ---- nanobeam ----------------------------------------------------------- - const nb = React.useMemo(() => { - if (!crystal || mode !== "nanobeam") return { beams: [] as Reflection[], nDyn: 0 }; - if (dynamical) return hybridBeams(crystal, quat, qMaxDisp, SG_MAX, dragging ? Math.min(qual.nanobeam, 40) : qual.nanobeam); - return { beams: [DIRECT, ...labReflections(crystal, quat, qMaxDisp)], nDyn: 0 }; - }, [crystal, quat, qMaxDisp, mode, dynamical, dragging, qual, SG_MAX]); - const nbBeams = nb.beams; - const nbSol = React.useMemo(() => (crystal && mode === "nanobeam" && dynamical && nb.nDyn ? blochSolve(crystal, nbBeams.slice(0, nb.nDyn)) : null), [crystal, nbBeams, nb.nDyn, mode, dynamical]); + // one Bloch solution per precession node (a single untilted node without precession) + const precNodes = React.useMemo(() => { + const n = dragging ? Math.max(6, Math.round((nPrecession || 24) / 2)) : nPrecession || 24; + return precessionTilts(k0, precession || 0, n); + }, [k0, precession, nPrecession, dragging]); + const nbSolution = React.useMemo(() => { + if (!crystal || mode !== "nanobeam" || !dynamical) return null; + return nanobeamSolve(crystal, quat, qMaxDisp, SG_MAX, dragging ? Math.min(qual.nanobeam, 40) : qual.nanobeam, precNodes); + }, [crystal, quat, qMaxDisp, mode, dynamical, dragging, qual, SG_MAX, precNodes]); + const nbBeams = React.useMemo(() => { + if (!crystal || mode !== "nanobeam") return []; + if (nbSolution) return nbSolution.beams; + return [DIRECT, ...labReflections(crystal, quat, qMaxDisp)]; + }, [crystal, quat, qMaxDisp, mode, nbSolution]); + const nb = { beams: nbBeams, nDyn: nbSolution ? Math.round(nbSolution.nDynMean) : 0 }; const nbInten = React.useMemo(() => { if (!crystal || mode !== "nanobeam") return new Float64Array(0); - if (dynamical && nbSol) { - const out = new Float64Array(nbBeams.length); - out.set(blochIntensities(nbSol, thickness)); - slabIntensities(crystal, nbBeams, nb.nDyn, [0, 0], thickness, out); - return out; + if (nbSolution) return nanobeamIntensities(crystal, nbSolution, thickness); + const out = new Float64Array(nbBeams.length); + for (const t of precNodes) { + const v = kinematicalTilted(crystal, nbBeams, t, sigma); + for (let i = 0; i < out.length; i++) out[i] += v[i] / precNodes.length; } - return kinematicalTilted(crystal, nbBeams, [0, 0], sigma); - }, [crystal, nbBeams, nb.nDyn, nbSol, mode, dynamical, thickness, sigma]); + return out; + }, [crystal, nbBeams, nbSolution, precNodes, mode, thickness, sigma]); // ---- CBED --------------------------------------------------------------- const alpha = semiconv * 1e-3; @@ -379,9 +436,22 @@ function DiffSim() { // ---- drawing -------------------------------------------------------------- React.useEffect(() => { const canvas = cellRef.current; - if (!canvas || !geom) return; - drawCell(canvas, geom, quat, S, { dark, showAxes: showCellAxes, showLabels: showLabels, atomScale: 0.45, viewX }); - }, [geom, quat, S, dark, showCellAxes, showLabels, viewX]); + if (!canvas || !geom || !crystal) return; + drawCell(canvas, geom, quat, Sc, { dark, showAxes: showCellAxes, showLabels: showLabels, atomScale: 0.45, viewX }); + }, [geom, quat, Sc, dark, showCellAxes, showLabels, viewX]); + + const ewaldRef = React.useRef(null); + React.useEffect(() => { + const canvas = ewaldRef.current; + if (!canvas || !crystal || !showEwald || Se <= 0) return; + const dpr = window.devicePixelRatio || 1; + if (canvas.width !== Sc * dpr || canvas.height !== Se * dpr) { canvas.width = Sc * dpr; canvas.height = Se * dpr; } + const ctx = canvas.getContext("2d"); + if (!ctx) return; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + const refl = mode === "nanobeam" && nbBeams.length ? nbBeams : labReflections(crystal, quat, qMaxDisp); + drawEwaldPanel(ctx, Sc, Se, refl, k0, qMaxDisp, SG_MAX, viewX, dark, mode === "nanobeam" ? precession || 0 : 0); + }, [quat, Sc, Se, dark, viewX, showEwald, crystal, mode, nbBeams, qMaxDisp, k0, SG_MAX, precession]); const patRef = React.useRef(null); React.useEffect(() => { @@ -394,9 +464,12 @@ function DiffSim() { const vmax = display.lo + (vmaxPct / 100) * (display.hi - display.lo); drawImage(ctx, frame, display.data, cmap, vmin, vmax, dark, mode === "kossel" ? "rad" : "Å⁻¹", mode === "kossel" ? 0.01 : 1); if (mode === "nanobeam" && kikuchi) drawKikuchiOverlay(ctx, frame, lines, k0, true); - if (mode === "nanobeam" && showLabels) labelBeams(ctx, frame, nbBeams, nbInten, dark, true); + if (mode === "nanobeam" && showHkl) labelBeams(ctx, frame, nbBeams, nbInten, dark, true); + } else if (mode === "nanobeam" && render === "disks") { + drawDisks(ctx, frame, nbBeams, nbInten, dark, showHkl, k0 * Math.sin(alpha), markerPower); + if (kikuchi) drawKikuchiOverlay(ctx, frame, lines, k0, dark); } else if (mode === "nanobeam") { - drawMarkers(ctx, frame, nbBeams, nbInten, dark, showLabels, !dynamical); + drawMarkers(ctx, frame, nbBeams, nbInten, dark, showHkl, !dynamical, markerPower, markerSize); if (kikuchi) drawKikuchiOverlay(ctx, frame, lines, k0, dark); } else if (mode === "kossel") { if (render === "pixels" && !kossel) { @@ -405,16 +478,16 @@ function DiffSim() { ctx.fillText("no Kossel reference pattern loaded", S / 2, S / 2 - 10); ctx.fillText(standalone ? "(export the page after compute_kossel_reference)" : "press “compute reference” below", S / 2, S / 2 + 10); } else { - drawKosselLines(ctx, frame, lines, dark, showLabels, 0.02); + drawKosselLines(ctx, frame, lines, dark, showHkl, 0.02); } } else if (mode === "cbed") { ctx.fillStyle = dark ? "#000" : "#fff"; ctx.fillRect(0, 0, S, S); } - }, [crystal, display, frame, mode, render, dark, cmap, vminPct, vmaxPct, nbBeams, nbInten, showLabels, dynamical, kikuchi, lines, k0, kossel, S, standalone]); + }, [crystal, display, frame, mode, render, dark, cmap, vminPct, vmaxPct, nbBeams, nbInten, showHkl, dynamical, kikuchi, lines, k0, kossel, S, standalone, markerPower, markerSize, alpha]); // ---- actions --------------------------------------------------------------- const goZoneAxis = () => { - const uvw = parseZoneAxis(zoneText); + const uvw = parseDirection(zoneText); if (!uvw || !crystal) return; const c = crystal.cell; const d: Vec3 = [ @@ -447,9 +520,9 @@ function DiffSim() { const res = await fetch(import.meta.url); const bundle = await res.text(); const keys = ["crystal_json", "presets", "preset", "energy_ev", "k_max", "orientation", "mode", "render", "dynamical", "thickness_A", - "semiconv_mrad", "sigma_excitation", "rotation_step_deg", "pattern_range", "field_mrad", "sg_max", "quality", "show_kikuchi", "view_from", - "scaling", "power", "cmap", "vmin_pct", "vmax_pct", "show_labels", - "show_cell_axes", "n_cells", "polyhedra", "size", "kossel_json", "status", "widget_version"]; + "semiconv_mrad", "precession_deg", "n_precession", "sigma_excitation", "rotation_step_deg", "pattern_range", "field_mrad", "sg_max", "quality", "show_kikuchi", "view_from", + "scaling", "power", "cmap", "marker_power", "marker_size", "vmin_pct", "vmax_pct", "show_labels", + "show_cell_axes", "show_hkl", "n_cells", "polyhedra", "show_ewald", "size", "kossel_json", "status", "widget_version"]; const state: Record = {}; for (const k of keys) state[k] = model.get(k); state.orientation = [...quatRef.current]; @@ -488,7 +561,6 @@ function DiffSim() { }; const btn = { fontSize: 11, height: 30, color: colors.accent, borderColor: colors.border, textTransform: "none" as const, "&:hover": { borderColor: colors.accent } }; const sw = { "& .MuiSwitch-track": { bgcolor: dark ? "#777" : undefined } }; - const panelW = S; const nDyn = mode === "nanobeam" ? nb.nDyn : mode === "cbed" && cbed ? cbed.nDyn : 0; const nBeams = mode === "nanobeam" ? nbBeams.length : mode === "cbed" && cbed ? cbed.beams.length : lines.length; @@ -512,7 +584,7 @@ function DiffSim() { ))} - setZoneText(e.target.value)} + setZoneText(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") goZoneAxis(); }} sx={{ width: 150, ...tf }} /> @@ -521,123 +593,158 @@ function DiffSim() { + {/* panels: cell (with the Ewald view below it) and the pattern */} - {/* left: unit cell */} - - + - - {(["x", "y", "z"] as const).map((ax, i) => ( - - rotateLab([+(i === 0), +(i === 1), +(i === 2)], -stepDeg)}>{ax} − - rotateLab([+(i === 0), +(i === 1), +(i === 2)], stepDeg)}>{ax} + - - ))} - setStepDeg(Math.max(0.01, Number(e.target.value) || 0.01))} - inputProps={{ step: 1, min: 0.01, max: 180, style: { fontSize: 11, padding: "4px 6px", width: 42 } }} sx={tf} /> - ° - - - - {crystal.name} · {crystal.spacegroup || crystal.pointgroup} - zone axis {fmtIndices(zoneAxis)} - - - setShowCellAxes(e.target.checked)} /> - cell axes - setShowLabels(e.target.checked)} /> - labels - setPolyhedra(e.target.checked)} /> - polyhedra - - - cells - {[0, 1, 2].map((i) => ( - { const v = [...nCellsSafe]; v[i] = Math.max(1, Math.min(6, Math.round(Number(e.target.value) || 1))); setNCells(v); }} - inputProps={{ min: 1, max: 6, step: 1, style: { fontSize: 11, padding: "3px 4px", width: 26 } }} sx={tf} /> - ))} - along a, b, c - - drag the cell (near face follows) or the pattern (tilt map follows) · double-click a point of the pattern to centre it · two fingers twist · buttons rotate about the screen axes + {showEwald && Se > 0 && ( + + )} - - {/* right: pattern */} - + - - v && setMode(v)} sx={tbg}> - nanobeam - CBED - Kossel / LACBED + + + + {/* controls, full width under the panels */} + + {/* crystal row */} + + crystal + {(["x", "y", "z"] as const).map((ax, i) => ( + + rotateLab([+(i === 0), +(i === 1), +(i === 2)], -stepDeg)}>{ax} − + rotateLab([+(i === 0), +(i === 1), +(i === 2)], stepDeg)}>{ax} + - {mode !== "cbed" && ( - v && setRender(v)} sx={tbg}> - {mode === "kossel" ? "lines" : "markers"} - pixels - - )} - {mode !== "kossel" && ( - - setDynamical(e.target.checked)} /> - dynamical - - )} - - - `${v.toFixed(0)} Å`} - disabled={mode !== "kossel" ? !dynamical : render !== "pixels"} /> - {mode === "cbed" && `${v.toFixed(1)} mrad`} />} - {mode === "kossel" && `${v.toFixed(0)} mrad`} />} - {mode !== "kossel" && `${v.toFixed(2)} Å⁻¹`} />} - {mode !== "kossel" && !dynamical && `${v.toFixed(3)} Å⁻¹`} />} + ))} + setStepDeg(Math.max(0.01, Number(e.target.value) || 0.01))} + inputProps={{ step: 1, min: 0.01, max: 180, style: { fontSize: 11, padding: "4px 6px", width: 42 } }} sx={tf} /> + ° + + zone axis {fmtIndices(zoneAxis, crystal.hexagonal)} + {crystal.name} · {crystal.spacegroup || crystal.pointgroup} + + + setShowCellAxes(e.target.checked)} /> + cell axes + setShowLabels(e.target.checked)} /> + axis labels + setPolyhedra(e.target.checked)} /> + polyhedra + setShowEwald(e.target.checked)} /> + Ewald sphere + cells + {[0, 1, 2].map((i) => ( + { const v = [...nCellsSafe]; v[i] = Math.max(1, Math.min(6, Math.round(Number(e.target.value) || 1))); setNCells(v); }} + inputProps={{ min: 1, max: 6, step: 1, style: { fontSize: 11, padding: "3px 4px", width: 26 } }} sx={tf} /> + ))} + along a, b, c + + + {/* pattern row */} + + pattern + v && setMode(v)} sx={tbg}> + nanobeam + CBED + Kossel / LACBED + + {mode !== "cbed" && ( + v && setRender(v)} sx={tbg}> + {mode === "kossel" ? "lines" : "markers"} + {mode === "nanobeam" && disks} + pixels + + )} + {mode !== "kossel" && ( + + setDynamical(e.target.checked)} /> + dynamical + + )} + + setShowHkl(e.target.checked)} /> + hkl labels - - {display && ( - { setVminPct(a); setVmaxPct(b); }} dark={dark} lo={display.lo} hi={display.hi} /> - )} - {pixelMode && ( + {mode === "nanobeam" && ( + + setKikuchi(e.target.checked)} /> + Kikuchi lines + + )} + {mode !== "kossel" && dynamical && ( + + quality + + + )} + {mode === "kossel" && render === "pixels" && !kossel && !standalone && ( + + )} + + + {/* physics sliders */} + + `${v.toFixed(0)} Å`} width={180} + disabled={mode !== "kossel" ? !dynamical : render !== "pixels"} /> + {(mode === "cbed" || (mode === "nanobeam" && render === "disks")) && `${v.toFixed(1)} mrad`} width={180} />} + {mode === "nanobeam" && (v > 0 ? `${v.toFixed(2)}°` : "off")} width={180} />} + {mode !== "kossel" && `${v.toFixed(2)} Å⁻¹`} width={180} />} + {mode === "kossel" && `${v.toFixed(0)} mrad`} width={180} />} + {mode !== "kossel" && !dynamical && `${v.toFixed(3)} Å⁻¹`} width={180} />} + + + {/* display row */} + + {mode === "nanobeam" && render === "markers" && ( + <> + `p = ${v.toFixed(2)}`} width={180} /> + `${v.toFixed(0)} px`} width={140} /> + + )} + {mode === "nanobeam" && render === "disks" && ( + `p = ${v.toFixed(2)}`} width={180} /> + )} + {pixelMode && ( + <> - + intensity scaling - {scaling === "power" && ( - v.toFixed(2)} width={110} /> - )} - )} - {mode === "nanobeam" && ( - - setKikuchi(e.target.checked)} /> - Kikuchi lines - - )} - {mode !== "kossel" && dynamical && ( - - quality - setCmap(e.target.value as string)} sx={{ ...ctl, minWidth: 110 }} MenuProps={menuProps}> + {COLORMAP_NAMES.map((n) => {n})} - )} - {mode === "kossel" && render === "pixels" && !kossel && !standalone && ( - - )} - - - {(energy / 1e3).toFixed(0)} keV · λ = {(crystal.wavelength * 100).toFixed(3)} pm · {nBeams} {mode === "kossel" ? "lines" : "beams"} - {mode !== "kossel" && dynamical ? ` · ${nDyn} Bloch beams (|s| < ${SG_MAX} Å⁻¹${crystal.absorptive ? ", absorptive" : ""}), thin-slab intensities for the rest` : ""} - {mode === "cbed" ? " · disks summed incoherently where they overlap" : ""} - {status ? ` · ${status}` : ""} - - - + + )} + {display && ( + { setVminPct(a); setVmaxPct(b); }} dark={dark} lo={display.lo} hi={display.hi} /> + )} + + + + {(energy / 1e3).toFixed(0)} keV · λ = {(crystal.wavelength * 100).toFixed(3)} pm · {nBeams} {mode === "kossel" ? "lines" : "beams"} + {mode !== "kossel" && dynamical ? ` · ${nDyn} Bloch beams (|s| < ${SG_MAX} Å⁻¹${crystal.absorptive ? ", absorptive" : ""}), thin-slab intensities for the rest` : ""} + {mode === "cbed" ? " · disks summed incoherently where they overlap" : ""} + {mode === "nanobeam" && precession > 0 ? ` · precession ${precession.toFixed(2)}°, ${precNodes.length} ring nodes` : ""} + {status ? ` · ${status}` : ""} + + drag the cell (near face follows) or the pattern (tilt map follows) · shift-drag or two fingers twist about the beam · double-click a disk for its two-beam condition, or empty space to put the Laue circle centre there · buttons rotate about the screen axes + ); } diff --git a/widget/js/diffsim/math.ts b/widget/js/diffsim/math.ts index 325722a0e..82105f7cd 100644 --- a/widget/js/diffsim/math.ts +++ b/widget/js/diffsim/math.ts @@ -90,6 +90,15 @@ export function directionIndices(cell: number[][], d: Vec3, maxMult = 8): [numbe const w = v.map((x) => x * mult); if (w.every((x) => Math.abs(x - Math.round(x)) < 0.02)) { const ints = w.map((x) => Math.round(x)) as [number, number, number]; + // accept only if the integer direction is within 0.3 degrees of d + const c = cell; + const v: Vec3 = [ + ints[0] * c[0][0] + ints[1] * c[1][0] + ints[2] * c[2][0], + ints[0] * c[0][1] + ints[1] * c[1][1] + ints[2] * c[2][1], + ints[0] * c[0][2] + ints[1] * c[1][2] + ints[2] * c[2][2], + ]; + const cosang = (v[0] * d[0] + v[1] * d[1] + v[2] * d[2]) / ((Math.hypot(...v) * Math.hypot(...d)) || 1); + if (Math.abs(cosang) < Math.cos((0.3 * Math.PI) / 180)) return null; const g = gcd3(ints); return ints.map((x) => x / g) as [number, number, number]; } @@ -97,6 +106,37 @@ export function directionIndices(cell: number[][], d: Vec3, maxMult = 8): [numbe return null; } +/** Miller-Bravais direction [u v t w] -> three-index [U V W] = [2u+v, u+2v, w]. */ +export function fourToThree(v: number[]): [number, number, number] { + const [u, vv, , w] = v; + return [2 * u + vv, u + 2 * vv, w]; +} + +/** Three-index direction [U V W] of a hexagonal cell -> smallest integer [u v t w]. */ +export function threeToFour(d: [number, number, number]): [number, number, number, number] { + const [U, V, W] = d; + // u = (2U - V)/3, v = (2V - U)/3, t = -(u + v): scale by 3 and reduce + const ints = [2 * U - V, 2 * V - U, -(U + V), 3 * W]; + const g = Math.max(1, ints.reduce((a, b) => gcd(a, b), 0)); + return ints.map((x) => x / g) as [number, number, number, number]; +} + +/** Parse "1 1 0", "110", "1,-1,0", "[1-10]" or a 4-index "0001" / "1 0 -1 0"; 4 indices are Miller-Bravais. */ +export function parseDirection(text: string): [number, number, number] | null { + const t = text.trim().replace(/[\[\]()]/g, ""); + let parts: string[]; + if (/[\s,]/.test(t)) parts = t.split(/[\s,]+/).filter(Boolean); + else parts = t.match(/-?\d/g) || []; + if (parts.length !== 3 && parts.length !== 4) return null; + const v = parts.map(Number); + if (v.some((x) => !isFinite(x)) || v.every((x) => x === 0)) return null; + if (v.length === 4) { + if (Math.abs(v[0] + v[1] + v[2]) > 1e-9) return null; // u + v + t must vanish + return fourToThree(v); + } + return v as [number, number, number]; +} + function gcd(a: number, b: number): number { a = Math.abs(a); b = Math.abs(b); while (b) [a, b] = [b, a % b]; diff --git a/widget/js/diffsim/pattern.ts b/widget/js/diffsim/pattern.ts index 29d95fadc..3bd01980f 100644 --- a/widget/js/diffsim/pattern.ts +++ b/widget/js/diffsim/pattern.ts @@ -8,6 +8,8 @@ import { COLORMAPS, applyColormap } from "../colormaps"; import type { Reflection } from "./physics"; import type { KosselLine } from "./physics"; +const MAX_LABELS = 20; // strongest reflections labelled + export interface Frame { size: number; // canvas CSS px (square) qMax: number; // 1/A at the edge (nanobeam / CBED) or rad (Kossel) @@ -42,46 +44,51 @@ export function setupCanvas(canvas: HTMLCanvasElement, size: number): CanvasRend return ctx; } -/** Nanobeam pattern as markers with area ~ sqrt(intensity). */ +/** + * Nanobeam pattern as markers. Marker AREA scales as intensity^markerPower + * (0.5 = sqrt intensity, the default); markerSize is the radius (px) of + * the strongest beam, capped so the densest net does not merge. + */ export function drawMarkers( ctx: CanvasRenderingContext2D, f: Frame, beams: Reflection[], inten: Float64Array, dark: boolean, - labels: boolean, kinematic: boolean, + labels: boolean, kinematic: boolean, markerPower = 0.5, markerSize = 20, ) { const s = scaleOf(f); ctx.fillStyle = dark ? "#000" : "#fff"; ctx.fillRect(0, 0, f.size, f.size); + // normalise to the strongest diffracted beam: the direct beam saturates and the weak spots stay visible let iMax = 0; - for (let i = 0; i < beams.length; i++) if (beams[i].index >= 0 || !kinematic) iMax = Math.max(iMax, inten[i]); + for (let i = 0; i < beams.length; i++) if (beams[i].index >= 0) iMax = Math.max(iMax, inten[i]); if (iMax <= 0) iMax = 1; // marker radius capped so neighbouring spots of the densest net do not merge let gMin = Infinity; for (const b of beams) if (b.index >= 0 && b.gLen > 1e-6) gMin = Math.min(gMin, b.gLen); - const rMax = Math.min(0.055 * f.size, isFinite(gMin) ? 0.42 * gMin * s : Infinity); + const rMax = Math.min(markerSize * (f.size / 420), isFinite(gMin) ? 0.42 * gMin * s : Infinity); const fg = dark ? "#fff" : "#000"; - const strong: { x: number; y: number; r: number; hkl: number[] }[] = []; + const strong: { x: number; y: number; r: number; hkl: number[]; rel: number }[] = []; for (let i = 0; i < beams.length; i++) { const b = beams[i]; - const rel = inten[i] / iMax; + const rel = Math.min(1, inten[i] / iMax); const [x, y] = toPx(f, b.g[0], b.g[1]); if (b.index < 0 && kinematic) { ctx.strokeStyle = fg; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(x, y, rMax * 0.9, 0, 2 * Math.PI); ctx.stroke(); - strong.push({ x, y, r: rMax * 0.9, hkl: b.hkl }); + strong.push({ x, y, r: rMax * 0.9, hkl: b.hkl, rel: 2 }); continue; } if (rel < 1e-6) continue; - const r = rMax * Math.pow(rel, 0.25); + const r = rMax * Math.pow(rel, 0.5 * markerPower); // area ~ I^markerPower ctx.fillStyle = fg; ctx.globalAlpha = 0.9; ctx.beginPath(); ctx.arc(x, y, Math.max(r, 0.6), 0, 2 * Math.PI); ctx.fill(); ctx.globalAlpha = 1; - if (rel > 0.08) strong.push({ x, y, r, hkl: b.hkl }); + if (rel > 0.08) strong.push({ x, y, r, hkl: b.hkl, rel: b.index < 0 ? 2 : rel }); } if (labels) { ctx.font = `${Math.max(9, Math.round(f.size / 38))}px sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "bottom"; ctx.fillStyle = dark ? "#ffd54f" : "#c62828"; - for (const p of strong.slice(0, 40)) { + for (const p of strong.sort((a, b) => b.rel - a.rel).slice(0, MAX_LABELS)) { ctx.fillText(hklText(p.hkl), p.x, p.y - p.r - 2); } } @@ -89,10 +96,141 @@ export function drawMarkers( drawScaleBar(ctx, f, s, "Å⁻¹", dark, 1); } +/** + * Nanobeam pattern as disks of the physical convergence angle: filled + * circles of radius k0 sin(alpha) at every beam, brightness (I/Imax)^power. + * Overlapping disks add; the direct beam is drawn like the others. + */ +export function drawDisks( + ctx: CanvasRenderingContext2D, f: Frame, beams: Reflection[], inten: Float64Array, dark: boolean, + labels: boolean, radiusQ: number, power = 0.5, vmin = 0, vmax = 1, +) { + const s = scaleOf(f); + // dark theme: bright disks on black, adding where they overlap; light + // theme: the inverted greyscale, dark disks on white, multiplying + ctx.fillStyle = dark ? "#000" : "#fff"; + ctx.fillRect(0, 0, f.size, f.size); + // normalise to the strongest diffracted beam (the direct beam saturates) + let iMax = 0; + for (let i = 0; i < beams.length; i++) if (beams[i].index >= 0) iMax = Math.max(iMax, inten[i]); + if (iMax <= 0) iMax = 1; + const r = Math.max(1.2, radiusQ * s); + const strong: { x: number; y: number; hkl: number[]; rel: number }[] = []; + ctx.globalCompositeOperation = dark ? "lighter" : "multiply"; + for (let i = 0; i < beams.length; i++) { + const rel = Math.min(1, inten[i] / iMax); + if (rel < 1e-6) continue; + const [x, y] = toPx(f, beams[i].g[0], beams[i].g[1]); + if (x < -r || y < -r || x > f.size + r || y > f.size + r) continue; + const v = Math.min(1, Math.max(0, (Math.pow(rel, power) - vmin) / Math.max(vmax - vmin, 1e-6))); // contrast window on I^power + const c = Math.round(255 * (dark ? v : 1 - v)); + ctx.fillStyle = `rgb(${c},${c},${c})`; + ctx.beginPath(); ctx.arc(x, y, r, 0, 2 * Math.PI); ctx.fill(); + if (rel > 0.08) strong.push({ x, y, hkl: beams[i].hkl, rel: beams[i].index < 0 ? 2 : rel }); + } + ctx.globalCompositeOperation = "source-over"; + if (labels) { + ctx.font = `${Math.max(9, Math.round(f.size / 38))}px sans-serif`; + ctx.textAlign = "center"; ctx.textBaseline = "bottom"; + ctx.fillStyle = dark ? "#ffd54f" : "#c62828"; + for (const p of strong.sort((a, b) => b.rel - a.rel).slice(0, MAX_LABELS)) ctx.fillText(hklText(p.hkl), p.x, p.y - r - 2); + } + drawScaleBar(ctx, f, s, "Å⁻¹", dark, 1); +} + export function hklText(hkl: number[]): string { return hkl.map((h) => (h < 0 ? `${-h}̅` : `${h}`)).join(""); } +/** + * Side view of the Ewald sphere, filling a W x H canvas: the lab x-z + * plane (x mirrored by viewX like the pattern, +z = upstream at the top), + * the reciprocal lattice points with |g_y| below a slab width as dots, the + * sphere z = k0 - sqrt(k0^2 - x^2) through the origin, and the excited + * reflections (|s| < sgMax) highlighted. The z axis is stretched so the + * sphere's sagitta over the pattern range fills the inset. + */ +export function drawEwaldPanel( + ctx: CanvasRenderingContext2D, W: number, H: number, refl: Reflection[], k0: number, qMax: number, sgMax: number, + viewX: number, dark: boolean, precDeg = 0, +) { + const size = Math.max(W, 2 * H); // reference for the font size + const x0 = 0, y0 = 0; + const fg = dark ? "#d8d8d8" : "#222"; + ctx.save(); + ctx.fillStyle = dark ? "#141414" : "#fafafa"; + ctx.fillRect(0, 0, W, H); + ctx.beginPath(); ctx.rect(x0, y0, W, H); ctx.clip(); + const sinP = Math.sin((precDeg * Math.PI) / 180); + const sag = (qMax * qMax) / (2 * k0) + qMax * sinP; // sphere height over the pattern range, incl. the precession tilt + const zHalf = Math.max(sag * 1.15, 4 * sgMax); + const sx = (W * 0.46) / qMax; // px per 1/A along x + const sz = (H * 0.4) / zHalf; // px per 1/A along z (stretched) + const cx = x0 + W / 2, cy = y0 + H * 0.64; // origin: lower middle + const X = (x: number) => cx + viewX * x * sx; + const Z = (z: number) => cy - z * sz; + // sphere for an incident beam with in-plane wavevector tx: centre (-tx, kz), through the origin + const sphereZ = (x: number, tx: number) => { + const kz = Math.sqrt(Math.max(k0 * k0 - tx * tx, 0)); + return kz - Math.sqrt(Math.max(k0 * k0 - (x + tx) * (x + tx), 0)); + }; + const xs: number[] = []; + for (let i = 0; i <= 60; i++) xs.push(-qMax * 1.08 + (2.16 * qMax * i) / 60); + const orange = dark ? "#e0b060" : "#c07a00"; + if (sinP > 0) { + // precession: the sphere sweeps the band between its two extreme tilts in this plane + const tA = k0 * sinP, tB = -k0 * sinP; + ctx.fillStyle = dark ? "rgba(224,176,96,0.22)" : "rgba(192,122,0,0.18)"; + ctx.beginPath(); + xs.forEach((x, i) => { const z = sphereZ(x, tA); if (i === 0) ctx.moveTo(X(x), Z(z)); else ctx.lineTo(X(x), Z(z)); }); + for (let i = xs.length - 1; i >= 0; i--) ctx.lineTo(X(xs[i]), Z(sphereZ(xs[i], tB))); + ctx.closePath(); ctx.fill(); + ctx.strokeStyle = orange; ctx.lineWidth = 0.9; + for (const t of [tA, tB]) { + ctx.beginPath(); + xs.forEach((x, i) => { const z = sphereZ(x, t); if (i === 0) ctx.moveTo(X(x), Z(z)); else ctx.lineTo(X(x), Z(z)); }); + ctx.stroke(); + } + // the beam cone: the two extreme incident directions, from the top to the origin + ctx.strokeStyle = dark ? "#9ad" : "#37c"; ctx.lineWidth = 1; + const zTop = (cy - y0 - 4) / sz; + for (const t of [tA, tB]) { + const kz = Math.sqrt(Math.max(k0 * k0 - t * t, 0)); + ctx.beginPath(); ctx.moveTo(X((-t / kz) * zTop), Z(zTop)); ctx.lineTo(cx, cy); ctx.stroke(); + } + } + // untilted beam arrow (travels -z: from the top toward the origin) and sphere + ctx.strokeStyle = dark ? "#9ad" : "#37c"; ctx.fillStyle = ctx.strokeStyle; ctx.lineWidth = 1.3; + ctx.beginPath(); ctx.moveTo(cx, y0 + 4); ctx.lineTo(cx, cy - 3); ctx.stroke(); + ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx - 3.5, cy - 7); ctx.lineTo(cx + 3.5, cy - 7); ctx.closePath(); ctx.fill(); + ctx.strokeStyle = orange; ctx.lineWidth = 1.4; + ctx.beginPath(); + xs.forEach((x, i) => { const z = sphereZ(x, 0); if (i === 0) ctx.moveTo(X(x), Z(z)); else ctx.lineTo(X(x), Z(z)); }); + ctx.stroke(); + // reciprocal lattice points in a slab about the x-z plane + const slab = 0.12 * qMax; + for (const r of refl) { + if (Math.abs(r.g[1]) > slab || Math.abs(r.g[0]) > qMax * 1.08 || Math.abs(r.g[2]) > zHalf * 1.3) continue; + const excited = Math.abs(r.s) < sgMax + Math.abs(r.g[0]) * sinP; // within the swept band + ctx.fillStyle = excited ? "#00cc66" : (dark ? "#8a8a8a" : "#777"); + ctx.beginPath(); ctx.arc(X(r.g[0]), Z(r.g[2]), excited ? 2.6 : 1.6, 0, 2 * Math.PI); ctx.fill(); + } + ctx.fillStyle = dark ? "#eee" : "#111"; + ctx.beginPath(); ctx.arc(cx, cy, 2.4, 0, 2 * Math.PI); ctx.fill(); + // labels on an opaque strip so the cone lines do not run through them + const fontPx = Math.max(10, Math.round(size / 36)); + ctx.font = `${fontPx}px sans-serif`; ctx.textAlign = "left"; ctx.textBaseline = "top"; + const line1 = `Ewald sphere, side view, z ×${Math.round(sz / sx)}`; + const line2 = sinP > 0 ? `precession ±${precDeg.toFixed(2)}°` : ""; + const tw = Math.max(ctx.measureText(line1).width, line2 ? ctx.measureText(line2).width : 0); + ctx.fillStyle = dark ? "rgba(20,20,20,0.9)" : "rgba(250,250,250,0.92)"; + ctx.fillRect(x0 + 1, y0 + 1, tw + 9, (line2 ? 2.5 : 1.2) * fontPx + 6); + ctx.fillStyle = fg; + ctx.fillText(line1, x0 + 5, y0 + 4); + if (line2) ctx.fillText(line2, x0 + 5, y0 + 5 + 1.4 * fontPx); + ctx.restore(); +} + function drawScaleBar(ctx: CanvasRenderingContext2D, f: Frame, s: number, unit: string, dark: boolean, value: number) { let v = value; while (v * s > 0.4 * f.size) v /= 2; diff --git a/widget/js/diffsim/physics.ts b/widget/js/diffsim/physics.ts index 65dc56fe8..1745202c4 100644 --- a/widget/js/diffsim/physics.ts +++ b/widget/js/diffsim/physics.ts @@ -34,9 +34,31 @@ export interface CrystalData { hexagonal: boolean; } -export function parseCrystal(json: string): CrystalData | null { +/** Relativistic electron wavelength (A) for a beam energy in eV. */ +export function electronWavelength(energyEv: number): number { + return 12.2643 / Math.sqrt(energyEv * (1 + 0.97845e-6 * energyEv)); +} + +/** Relativistic mass factor 1 + E / (m0 c^2). */ +export function relativisticGamma(energyEv: number): number { + return 1 + energyEv / 510998.95; +} + +/** + * Parse the crystal data. With energyEv the stored couplings (computed at + * the data's energy) are rescaled by the ratio of relativistic mass factors + * and the wavelength is recomputed: exact for the elastic potential, an + * approximation for the absorptive part. Kinematical |F|^2 is unchanged. + */ +export function parseCrystal(json: string, energyEv?: number): CrystalData | null { if (!json || json === "{}") return null; const o = JSON.parse(json); + const scaleU = energyEv && Math.abs(energyEv - o.energy_ev) > 1 ? relativisticGamma(energyEv) / relativisticGamma(o.energy_ev) : 1; + if (scaleU !== 1) { + o.energy_ev = energyEv; + o.wavelength = electronWavelength(energyEv!); + o.u0_imag = o.u0_imag * scaleU; + } // reflection indices packed as int16 triplets; g rebuilt from the reciprocal cell const bin = atob(o.hkl_i16 || ""); const bytes = new Uint8Array(bin.length); @@ -54,6 +76,7 @@ export function parseCrystal(json: string): CrystalData | null { g[3 * i + 2] = h * B[0][2] + k * B[1][2] + l * B[2][2]; } const U_re = decodeF32(o.U_re), U_im = decodeF32(o.U_im); + if (scaleU !== 1) for (let i = 0; i < U_re.length; i++) { U_re[i] *= scaleU; U_im[i] *= scaleU; } const couplingRe = new Map(); const couplingIm = new Map(); for (let i = 0; i < n; i++) { @@ -253,6 +276,97 @@ export function kinematicalTilted(c: CrystalData, beams: Reflection[], tilt: [nu return out; } +/** + * Incident-beam tilts (1/A) sampling a precession cone of half angle + * precDeg: n points evenly spaced on the ring of radius k0 sin(phi), the + * Gauss-Chebyshev quadrature of the azimuthal average. No precession + * returns the single untilted beam. + */ +export function precessionTilts(k0: number, precDeg: number, n: number): [number, number][] { + if (!(precDeg > 0) || n < 1) return [[0, 0]]; + const r = k0 * Math.sin((precDeg * Math.PI) / 180); + const out: [number, number][] = []; + for (let i = 0; i < n; i++) { + const th = (2 * Math.PI * (i + 0.5)) / n; + out.push([r * Math.cos(th), r * Math.sin(th)]); + } + return out; +} + +/** + * Hybrid nanobeam intensities averaged over incident tilts: Bloch + * intensities of the first nDyn beams from one solution per tilt, thin-slab + * intensities for the rest, mean over the tilts. + */ +export function averagedIntensities( + c: CrystalData, beams: Reflection[], nDyn: number, sols: (BlochSolution | null)[], tilts: [number, number][], thickness: number, +): Float64Array { + const out = new Float64Array(beams.length); + const tmp = new Float64Array(beams.length); + for (let t = 0; t < tilts.length; t++) { + tmp.fill(0); + const sol = sols[t]; + if (sol) tmp.set(blochIntensities(sol, thickness)); + slabIntensities(c, beams, sol ? nDyn : 0, tilts[t], thickness, tmp); + for (let i = 0; i < beams.length; i++) out[i] += tmp[i] / tilts.length; + } + return out; +} + +export interface NanobeamSolution { + beams: Reflection[]; // DIRECT + every reflection within kMax (draw list) + nodes: { tilt: [number, number]; sol: BlochSolution; pos: Int32Array }[]; // pos: position of each Bloch beam in beams + nDynMean: number; +} + +/** + * Nanobeam pattern averaged over incident tilts (precession ring, or the + * single untilted beam). Each node selects its own Bloch set from the + * reflections within sgMax of ITS Ewald sphere (capped at maxBeams by + * |U_g| / |s_g|), so a 3 degree precession cone excites the right beams at + * every azimuth; every other reflection takes the thin-slab intensity. + */ +export function nanobeamSolve(c: CrystalData, q: Quat, kMax: number, sgMax: number, maxBeams: number, tilts: [number, number][]): NanobeamSolution { + const k0 = 1 / c.wavelength; + const all = labReflections(c, q, kMax); + const beams = [DIRECT, ...all]; + const nodes: NanobeamSolution["nodes"] = []; + let nSum = 0; + for (const tilt of tilts) { + const kz = Math.sqrt(Math.max(k0 * k0 - tilt[0] ** 2 - tilt[1] ** 2, 1e-12)); + const cand: { i: number; s: number; score: number }[] = []; + for (let i = 0; i < all.length; i++) { + const st = tiltedExcitation(all[i], kz, tilt); + if (Math.abs(st) < sgMax) { + const r = all[i]; + cand.push({ i, s: st, score: Math.hypot(c.U_re[r.index], c.U_im[r.index]) / (Math.abs(st) + 1e-4) }); + } + } + if (cand.length > maxBeams) { cand.sort((a, b) => b.score - a.score); cand.length = maxBeams; } + const dyn = [DIRECT, ...cand.map((x) => all[x.i])]; + const pos = new Int32Array(dyn.length); + pos[0] = 0; + cand.forEach((x, j) => { pos[j + 1] = x.i + 1; }); + nodes.push({ tilt, sol: blochSolve(c, dyn, tilt), pos }); + nSum += cand.length; + } + return { beams, nodes, nDynMean: nSum / Math.max(1, tilts.length) }; +} + +/** Intensities of a NanobeamSolution at a thickness (A): mean over the nodes. */ +export function nanobeamIntensities(c: CrystalData, ns: NanobeamSolution, thickness: number): Float64Array { + const out = new Float64Array(ns.beams.length); + const tmp = new Float64Array(ns.beams.length); + for (const node of ns.nodes) { + slabIntensities(c, ns.beams, 1, node.tilt, thickness, tmp); // every diffracted beam, then overwrite the Bloch ones + tmp[0] = 0; + const bi = blochIntensities(node.sol, thickness); + for (let j = 0; j < node.pos.length; j++) tmp[node.pos[j]] = bi[j]; + for (let i = 0; i < out.length; i++) out[i] += tmp[i] / ns.nodes.length; + } + return out; +} + /** Beam intensities at a thickness (A) from a Bloch solution. */ export function blochIntensities(sol: BlochSolution, thickness: number): Float64Array { const { n, gammaRe, gammaIm, vecRe, vecIm, psi0Re, psi0Im } = sol; diff --git a/widget/src/quantem/widget/diffsim.py b/widget/src/quantem/widget/diffsim.py index 4c1cd883a..c7001ad91 100644 --- a/widget/src/quantem/widget/diffsim.py +++ b/widget/src/quantem/widget/diffsim.py @@ -225,9 +225,13 @@ class DiffractionSim(anywidget.AnyWidget): thickness_A, semiconv_mrad, sigma_excitation : float Initial values of the thickness, convergence semiangle and excitation envelope sliders. + precession_deg : float, default=0 + Precession half angle for the nanobeam pattern; intensities are + averaged over n_precession incident tilts on the precession ring + (24 by default, half of that while dragging). pattern_range : float | None Scattering vector at the edge of the nanobeam / CBED panel - (1/Angstroms); None shows everything out to k_max. + (1/Angstroms); None uses 3, or k_max when that is smaller. field_mrad : float, default=50 Half angle of the Kossel / LACBED field of view. sg_max : float, default=0.05 @@ -242,6 +246,11 @@ class DiffractionSim(anywidget.AnyWidget): intensities to `power` (default 0.5). cmap : str Colormap of the pixel renderings, e.g. "inferno", "turbo_black", "gray". + marker_power : float, default=0.5 + Marker area scales as intensity**marker_power (0.5: sqrt intensity). + marker_size : float, default=20 + Radius in pixels of the strongest marker (at size 420), capped so + the densest net of spots does not merge. view_from : {"detector", "gun"} Viewpoint shared by both panels (a launch argument, no UI control). "detector" looks up the column from the detector side: the exit face of the cell is nearest you and tilts @@ -250,8 +259,9 @@ class DiffractionSim(anywidget.AnyWidget): and tilts opposite to the pattern (the Laue center marks where the zone axis exits toward the detector). mode : {"nanobeam", "cbed", "kossel"} - render : {"markers", "pixels"} - Nanobeam: markers sized by intensity, or a pixelated pattern. + render : {"markers", "disks", "pixels"} + Nanobeam: markers sized by intensity, disks of the convergence + semiangle with brightness by intensity, or a pixelated pattern. Kossel: vector lines, or the pixel lookup of the reference pattern (compute_kossel_reference()). n_cells : sequence of 3 int, default=(1, 1, 1) @@ -260,6 +270,10 @@ class DiffractionSim(anywidget.AnyWidget): Draw coordination polyhedra (convex hull of the nearest neighbours) around every species except the most numerous one; around every atom of an elemental crystal. + show_ewald : bool, default=True + Side-view inset of the Ewald sphere on the cell panel: the reciprocal + lattice points near the x-z plane, the sphere through the origin + (z stretched), and the excited reflections in green. size : int Height of the panels in CSS pixels. @@ -286,9 +300,11 @@ class DiffractionSim(anywidget.AnyWidget): dynamical = traitlets.Bool(True).tag(sync=True) thickness_A = traitlets.Float(500.0).tag(sync=True) semiconv_mrad = traitlets.Float(2.0).tag(sync=True) + precession_deg = traitlets.Float(0.0).tag(sync=True) + n_precession = traitlets.Int(24).tag(sync=True) sigma_excitation = traitlets.Float(0.02).tag(sync=True) rotation_step_deg = traitlets.Float(15.0).tag(sync=True) - pattern_range = traitlets.Float(4.0).tag(sync=True) + pattern_range = traitlets.Float(3.0).tag(sync=True) field_mrad = traitlets.Float(50.0).tag(sync=True) sg_max = traitlets.Float(0.05).tag(sync=True) quality = traitlets.Unicode("medium").tag(sync=True) @@ -296,13 +312,17 @@ class DiffractionSim(anywidget.AnyWidget): view_from = traitlets.Unicode("detector").tag(sync=True) scaling = traitlets.Unicode("linear").tag(sync=True) power = traitlets.Float(0.5).tag(sync=True) + marker_power = traitlets.Float(0.5).tag(sync=True) + marker_size = traitlets.Float(20.0).tag(sync=True) cmap = traitlets.Unicode("inferno").tag(sync=True) vmin_pct = traitlets.Float(0.0).tag(sync=True) vmax_pct = traitlets.Float(100.0).tag(sync=True) show_labels = traitlets.Bool(True).tag(sync=True) + show_hkl = traitlets.Bool(True).tag(sync=True) show_cell_axes = traitlets.Bool(True).tag(sync=True) n_cells = traitlets.List(trait=traitlets.Int(), default_value=[1, 1, 1]).tag(sync=True) polyhedra = traitlets.Bool(False).tag(sync=True) + show_ewald = traitlets.Bool(True).tag(sync=True) size = traitlets.Int(420).tag(sync=True) kossel_json = traitlets.Unicode("{}").tag(sync=True) status = traitlets.Unicode("").tag(sync=True) @@ -312,7 +332,9 @@ def __init__(self, crystal=None, zone_axis=None, pattern_range=None, **kwargs): if "n_cells" in kwargs: kwargs["n_cells"] = [int(n) for n in kwargs["n_cells"]] super().__init__(**kwargs) - self.pattern_range = float(pattern_range) if pattern_range is not None else self.k_max + self.pattern_range = ( + float(pattern_range) if pattern_range is not None else min(3.0, self.k_max) + ) self._crystal = None self._kossel_cache: dict = {} if crystal is None: @@ -425,6 +447,8 @@ def state_dict(self) -> dict: "dynamical", "thickness_A", "semiconv_mrad", + "precession_deg", + "n_precession", "sigma_excitation", "rotation_step_deg", "pattern_range", @@ -435,13 +459,17 @@ def state_dict(self) -> dict: "view_from", "scaling", "power", + "marker_power", + "marker_size", "cmap", "vmin_pct", "vmax_pct", "show_labels", + "show_hkl", "show_cell_axes", "n_cells", "polyhedra", + "show_ewald", "size", "kossel_json", "status", From ff57d6ceacb7f1359c13377b26a6b15223f06140 Mon Sep 17 00:00:00 2001 From: Colin Ophus Date: Wed, 16 Sep 2026 13:37:39 -0700 Subject: [PATCH 08/36] Diffraction widget: CIF loader, faded Kikuchi overlay, continuous rotation buttons --- widget/js/diffsim-web/cif.ts | 341 +++++++++++++++++++++++++++ widget/js/diffsim-web/index.ts | 116 +++++++-- widget/js/diffsim-web/lobato.ts | 107 +++++++++ widget/js/diffsim/index.tsx | 60 ++++- widget/js/diffsim/pattern.ts | 27 ++- widget/js/diffsim/physics.ts | 18 +- widget/scripts/build.mjs | 7 +- widget/src/quantem/widget/diffsim.py | 2 + 8 files changed, 639 insertions(+), 39 deletions(-) create mode 100644 widget/js/diffsim-web/cif.ts create mode 100644 widget/js/diffsim-web/lobato.ts diff --git a/widget/js/diffsim-web/cif.ts b/widget/js/diffsim-web/cif.ts new file mode 100644 index 000000000..8b2da47d8 --- /dev/null +++ b/widget/js/diffsim-web/cif.ts @@ -0,0 +1,341 @@ +/** + * CIF reader for the browser simulator: cell, symmetry operations and atom + * sites from a CIF text, expanded to the full cell, with kinematical + * structure factors (Lobato & Van Dyck electron scattering factors) and + * Bloch couplings on the reciprocal lattice out to k_max. The absorptive + * part of the potential is not computed here: it is approximated as a fixed + * fraction of the elastic coupling (ABSORPTION_FRACTION), which damps the + * thickness fringes at a plausible rate; the preset structures carry the + * Weickenmeier-Kohl factors from quantem instead. + */ + +import type { CrystalData } from "../diffsim/physics"; +import { relativisticGamma, electronWavelength } from "../diffsim/physics"; +import { LOBATO } from "./lobato"; + +const ABSORPTION_FRACTION = 0.08; + +const SYMBOLS = ["", "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr"]; + +// jmol colors (fraction of 255) and covalent radii (A) for the drawn atoms +const JMOL: Record = { + H: [1, 1, 1], He: [0.85, 1, 1], Li: [0.8, 0.5, 1], Be: [0.76, 1, 0], B: [1, 0.71, 0.71], C: [0.56, 0.56, 0.56], N: [0.19, 0.31, 0.97], O: [1, 0.05, 0.05], + F: [0.56, 0.88, 0.31], Ne: [0.7, 0.89, 0.96], Na: [0.67, 0.36, 0.95], Mg: [0.54, 1, 0], Al: [0.75, 0.65, 0.65], Si: [0.94, 0.78, 0.63], P: [1, 0.5, 0], S: [1, 1, 0.19], + Cl: [0.12, 0.94, 0.12], Ar: [0.5, 0.82, 0.89], K: [0.56, 0.25, 0.83], Ca: [0.24, 1, 0], Sc: [0.9, 0.9, 0.9], Ti: [0.75, 0.76, 0.78], V: [0.65, 0.65, 0.67], Cr: [0.54, 0.6, 0.78], + Mn: [0.61, 0.48, 0.78], Fe: [0.88, 0.4, 0.2], Co: [0.94, 0.56, 0.63], Ni: [0.31, 0.82, 0.31], Cu: [0.78, 0.5, 0.2], Zn: [0.49, 0.5, 0.69], Ga: [0.76, 0.56, 0.56], Ge: [0.4, 0.56, 0.56], + As: [0.74, 0.5, 0.89], Se: [1, 0.63, 0], Br: [0.65, 0.16, 0.16], Kr: [0.36, 0.72, 0.82], Rb: [0.44, 0.18, 0.69], Sr: [0, 1, 0], Y: [0.58, 1, 1], Zr: [0.58, 0.88, 0.88], + Nb: [0.45, 0.76, 0.79], Mo: [0.33, 0.71, 0.71], Tc: [0.23, 0.62, 0.62], Ru: [0.14, 0.56, 0.56], Rh: [0.04, 0.49, 0.55], Pd: [0, 0.41, 0.52], Ag: [0.75, 0.75, 0.75], Cd: [1, 0.85, 0.56], + In: [0.65, 0.46, 0.45], Sn: [0.4, 0.5, 0.5], Sb: [0.62, 0.39, 0.71], Te: [0.83, 0.48, 0], I: [0.58, 0, 0.58], Xe: [0.26, 0.62, 0.69], Cs: [0.34, 0.09, 0.56], Ba: [0, 0.79, 0], + La: [0.44, 0.83, 1], Ce: [1, 1, 0.78], Pr: [0.85, 1, 0.78], Nd: [0.78, 1, 0.78], Sm: [0.56, 1, 0.78], Eu: [0.38, 1, 0.78], Gd: [0.27, 1, 0.78], Tb: [0.19, 1, 0.78], Dy: [0.12, 1, 0.78], + Ho: [0, 1, 0.61], Er: [0, 0.9, 0.46], Tm: [0, 0.83, 0.32], Yb: [0, 0.75, 0.22], Lu: [0, 0.67, 0.14], Hf: [0.3, 0.76, 1], Ta: [0.3, 0.65, 1], W: [0.13, 0.58, 0.84], Re: [0.15, 0.49, 0.67], + Os: [0.15, 0.4, 0.59], Ir: [0.09, 0.33, 0.53], Pt: [0.82, 0.82, 0.88], Au: [1, 0.82, 0.14], Hg: [0.72, 0.72, 0.82], Tl: [0.65, 0.33, 0.3], Pb: [0.34, 0.35, 0.38], Bi: [0.62, 0.31, 0.71], + Th: [0, 0.73, 1], U: [0, 0.56, 1], +}; +const RADII: Record = { + H: 0.31, He: 0.28, Li: 1.28, Be: 0.96, B: 0.84, C: 0.76, N: 0.71, O: 0.66, F: 0.57, Ne: 0.58, Na: 1.66, Mg: 1.41, Al: 1.21, Si: 1.11, P: 1.07, S: 1.05, Cl: 1.02, Ar: 1.06, + K: 2.03, Ca: 1.76, Sc: 1.7, Ti: 1.6, V: 1.53, Cr: 1.39, Mn: 1.39, Fe: 1.32, Co: 1.26, Ni: 1.24, Cu: 1.32, Zn: 1.22, Ga: 1.22, Ge: 1.2, As: 1.19, Se: 1.2, Br: 1.2, Kr: 1.16, + Rb: 2.2, Sr: 1.95, Y: 1.9, Zr: 1.75, Nb: 1.64, Mo: 1.54, Tc: 1.47, Ru: 1.46, Rh: 1.42, Pd: 1.39, Ag: 1.45, Cd: 1.44, In: 1.42, Sn: 1.39, Sb: 1.39, Te: 1.38, I: 1.39, Xe: 1.4, + Cs: 2.44, Ba: 2.15, La: 2.07, Ce: 2.04, Pr: 2.03, Nd: 2.01, Sm: 1.98, Eu: 1.98, Gd: 1.96, Tb: 1.94, Dy: 1.92, Ho: 1.92, Er: 1.89, Tm: 1.9, Yb: 1.87, Lu: 1.87, Hf: 1.75, + Ta: 1.7, W: 1.62, Re: 1.51, Os: 1.44, Ir: 1.41, Pt: 1.36, Au: 1.36, Hg: 1.32, Tl: 1.45, Pb: 1.46, Bi: 1.48, Th: 2.06, U: 1.96, +}; + +// --------------------------------------------------------------------------- +// CIF text -> tokens, data items, loops +// --------------------------------------------------------------------------- +function tokenize(text: string): string[] { + const out: string[] = []; + const lines = text.replace(/\r\n?/g, "\n").split("\n"); + let i = 0; + while (i < lines.length) { + let line = lines[i]; + if (line.startsWith(";")) { + // multi-line text field + const parts = [line.slice(1)]; + i++; + while (i < lines.length && !lines[i].startsWith(";")) { parts.push(lines[i]); i++; } + out.push(parts.join("\n")); + i++; + continue; + } + const hash = line.indexOf("#"); + if (hash >= 0 && !/['"]/.test(line.slice(0, hash))) line = line.slice(0, hash); + const re = /'([^']*)'|"([^"]*)"|(\S+)/g; + let m: RegExpExecArray | null; + while ((m = re.exec(line))) out.push(m[1] ?? m[2] ?? m[3]); + i++; + } + return out; +} + +interface CifBlock { items: Record; loops: Record[] } + +function parseBlocks(text: string): CifBlock { + const tok = tokenize(text); + const block: CifBlock = { items: {}, loops: [] }; + let i = 0; + // use the first data block that has atom sites + while (i < tok.length) { + const t = tok[i]; + if (t.toLowerCase().startsWith("data_") || t.toLowerCase() === "stop_" || t.toLowerCase().startsWith("save_")) { i++; continue; } + if (t.toLowerCase() === "loop_") { + i++; + const names: string[] = []; + while (i < tok.length && tok[i].startsWith("_")) { names.push(tok[i].toLowerCase()); i++; } + const loop: Record = {}; + for (const n of names) loop[n] = []; + let k = 0; + while (i < tok.length && !tok[i].startsWith("_") && !/^(loop_|data_|stop_|save_)/i.test(tok[i])) { + loop[names[k % names.length]].push(tok[i]); + k++; i++; + } + block.loops.push(loop); + continue; + } + if (t.startsWith("_")) { + block.items[t.toLowerCase()] = tok[i + 1] ?? ""; + i += 2; + continue; + } + i++; + } + return block; +} + +const num = (s: string | undefined, fallback = NaN): number => { + if (s === undefined) return fallback; + const v = parseFloat(s.replace(/\(.*\)/, "")); + return isFinite(v) ? v : fallback; +}; + +// --------------------------------------------------------------------------- +// symmetry operations "x, y+1/2, -z" -> rotation matrix + translation +// --------------------------------------------------------------------------- +interface SymOp { R: number[][]; t: number[] } + +function parseSymop(s: string): SymOp | null { + const parts = s.toLowerCase().replace(/\s+/g, "").split(","); + if (parts.length !== 3) return null; + const R: number[][] = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; + const t = [0, 0, 0]; + for (let r = 0; r < 3; r++) { + // split into signed terms + const terms = parts[r].replace(/-/g, "+-").split("+").filter(Boolean); + for (const term of terms) { + const m = term.match(/^([+-]?\d*\.?\d*(?:\/\d+)?)\*?([xyz])?$/); + if (!m) return null; + let coef = 1; + const cs = m[1]; + if (cs && cs !== "+" && cs !== "-") { + coef = cs.includes("/") ? parseFloat(cs.split("/")[0]) / parseFloat(cs.split("/")[1]) : parseFloat(cs); + } else if (cs === "-") coef = -1; + if (m[2]) R[r]["xyz".indexOf(m[2])] += coef; + else t[r] += coef; + } + } + return { R, t }; +} + +// --------------------------------------------------------------------------- +export interface ParsedCif { + name: string; + cellpar: [number, number, number, number, number, number]; + cell: number[][]; // rows a, b, c (A), a along x, b in the xy plane + symbols: string[]; + positions: number[][]; // fractional, full cell + occupancy: number[]; + spacegroup: string; + centering: number[][]; // pure translations (centering vectors) +} + +export function parseCif(text: string, name = "CIF"): ParsedCif { + const blk = parseBlocks(text); + const it = blk.items; + const cellpar = [it["_cell_length_a"], it["_cell_length_b"], it["_cell_length_c"], it["_cell_angle_alpha"], it["_cell_angle_beta"], it["_cell_angle_gamma"]].map((v) => num(v)); + if (cellpar.some((v) => !isFinite(v))) throw new Error("CIF: missing cell parameters"); + const [a, b, c, al, be, ga] = cellpar; + const d2r = Math.PI / 180; + const cosA = Math.cos(al * d2r), cosB = Math.cos(be * d2r), cosG = Math.cos(ga * d2r), sinG = Math.sin(ga * d2r); + const cx = c * cosB, cy = (c * (cosA - cosB * cosG)) / sinG; + const cz = Math.sqrt(Math.max(c * c - cx * cx - cy * cy, 0)); + const cell = [[a, 0, 0], [b * cosG, b * sinG, 0], [cx, cy, cz]]; + + // atom sites + const atomLoop = blk.loops.find((l) => "_atom_site_fract_x" in l); + if (!atomLoop) throw new Error("CIF: no _atom_site_fract_x loop"); + const n = atomLoop["_atom_site_fract_x"].length; + const typeCol = atomLoop["_atom_site_type_symbol"] || atomLoop["_atom_site_label"]; + const occCol = atomLoop["_atom_site_occupancy"]; + const base: { sym: string; p: number[]; occ: number }[] = []; + for (let i = 0; i < n; i++) { + const raw = (typeCol?.[i] || "").replace(/[^A-Za-z]/g, ""); + let sym = raw.slice(0, 2); + if (!(sym in LOBATO)) sym = raw.slice(0, 1); + if (!(sym in LOBATO)) throw new Error(`CIF: unknown element "${raw}"`); + const p = [num(atomLoop["_atom_site_fract_x"][i]), num(atomLoop["_atom_site_fract_y"][i]), num(atomLoop["_atom_site_fract_z"][i])]; + if (p.some((v) => !isFinite(v))) continue; + base.push({ sym, p, occ: occCol ? num(occCol[i], 1) : 1 }); + } + + // symmetry operations + const symLoop = blk.loops.find((l) => "_symmetry_equiv_pos_as_xyz" in l || "_space_group_symop_operation_xyz" in l); + const opStrings = symLoop ? symLoop["_symmetry_equiv_pos_as_xyz"] || symLoop["_space_group_symop_operation_xyz"] : ["x,y,z"]; + const ops = opStrings.map(parseSymop).filter((o): o is SymOp => !!o); + if (!ops.length) ops.push({ R: [[1, 0, 0], [0, 1, 0], [0, 0, 1]], t: [0, 0, 0] }); + const spacegroup = it["_symmetry_space_group_name_h-m"] || it["_space_group_name_h-m_alt"] || (it["_symmetry_int_tables_number"] ? `#${it["_symmetry_int_tables_number"]}` : symLoop ? "" : "P1 (no symmetry operations in file)"); + + // expand to the full cell + const symbols: string[] = []; + const positions: number[][] = []; + const occupancy: number[] = []; + const wrap = (v: number) => ((v % 1) + 1) % 1; + for (const at of base) { + for (const op of ops) { + const q = [0, 1, 2].map((r) => wrap(op.R[r][0] * at.p[0] + op.R[r][1] * at.p[1] + op.R[r][2] * at.p[2] + op.t[r])); + let dup = false; + for (let j = 0; j < positions.length; j++) { + if (symbols[j] !== at.sym) continue; + const d = positions[j].map((v, k) => Math.abs(wrap(v - q[k] + 0.5) - 0.5)); + if (d.every((v) => v < 1e-3)) { dup = true; break; } + } + if (!dup) { symbols.push(at.sym); positions.push(q); occupancy.push(at.occ); } + } + } + // centering translations: from the symmetry list, and also any of the + // standard centerings that map the expanded atom list onto itself (a P1 + // listing of a conventional cell carries them only implicitly); the + // reflections they extinguish never carry intensity and are dropped + const centering = ops.filter((o) => o.R.every((row, r) => row.every((v, cc) => v === (r === cc ? 1 : 0))) && o.t.some((v) => Math.abs(wrap(v)) > 1e-6)).map((o) => o.t.map(wrap)); + const mapsOntoItself = (t: number[]) => positions.every((pos, j) => { + const q = pos.map((v, k) => wrap(v + t[k])); + return positions.some((p2, j2) => symbols[j2] === symbols[j] && p2.every((v, k) => Math.abs(wrap(v - q[k] + 0.5) - 0.5) < 1e-3)); + }); + for (const t of [[0, 0.5, 0.5], [0.5, 0, 0.5], [0.5, 0.5, 0], [0.5, 0.5, 0.5], [2 / 3, 1 / 3, 1 / 3], [1 / 3, 2 / 3, 2 / 3]]) { + if (!centering.some((c) => c.every((v, k) => Math.abs(v - t[k]) < 1e-6)) && mapsOntoItself(t)) centering.push(t); + } + return { name, cellpar: cellpar as ParsedCif["cellpar"], cell, symbols, positions, occupancy, spacegroup, centering }; +} + +// --------------------------------------------------------------------------- +// structure factors -> CrystalData +// --------------------------------------------------------------------------- +function scatteringFactor(sym: string, g: number): number { + const [a, b] = LOBATO[sym]; + const g2 = g * g; + let f = 0; + for (let i = 0; i < 5; i++) { + const d = 1 + b[i] * g2; + f += (a[i] * (2 + b[i] * g2)) / (d * d); + } + return f; +} + +function inv3(m: number[][]): number[][] { + const [[a, b, c], [d, e, f], [g, h, i]] = m; + const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g; + const det = a * A + b * B + c * C; + return [ + [A / det, -(b * i - c * h) / det, (b * f - c * e) / det], + [B / det, (a * i - c * g) / det, -(a * f - c * d) / det], + [C / det, -(a * h - b * g) / det, (a * e - b * d) / det], + ]; +} + +const MAX_REFLECTIONS = 30000; // keeps the live solve responsive for large cells + +/** k_max (1/A) at which a cell of this volume (primitive lattice points per + * centering) reaches the reflection budget, capped at the requested value. */ +export function kMaxForCell(p: ParsedCif, kMaxRequested: number): number { + const cell = p.cell; + const volume = Math.abs(cell[0][0] * (cell[1][1] * cell[2][2] - cell[1][2] * cell[2][1]) - cell[0][1] * (cell[1][0] * cell[2][2] - cell[1][2] * cell[2][0]) + cell[0][2] * (cell[1][0] * cell[2][1] - cell[1][1] * cell[2][0])); + const vPrim = volume / (p.centering.length + 1); + // N(k) = 4/3 pi k^3 V_prim + const kBudget = Math.cbrt((3 * MAX_REFLECTIONS) / (4 * Math.PI * vPrim)); + return Math.min(kMaxRequested, Math.floor(kBudget * 20) / 20); +} + +/** Build the simulator's crystal data from a parsed CIF at an energy and k_max (1/A). */ +export function crystalFromCif(p: ParsedCif, energyEv: number, kMaxRequested: number): CrystalData { + const kMax = kMaxForCell(p, kMaxRequested); + const cell = p.cell; + const inv = inv3(cell); // columns of inv = reciprocal vectors; recip rows b_i = inv^T rows + const recip = [[inv[0][0], inv[1][0], inv[2][0]], [inv[0][1], inv[1][1], inv[2][1]], [inv[0][2], inv[1][2], inv[2][2]]]; + const volume = Math.abs(cell[0][0] * (cell[1][1] * cell[2][2] - cell[1][2] * cell[2][1]) - cell[0][1] * (cell[1][0] * cell[2][2] - cell[1][2] * cell[2][0]) + cell[0][2] * (cell[1][0] * cell[2][1] - cell[1][1] * cell[2][0])); + const gamma = relativisticGamma(energyEv); + const species = [...new Set(p.symbols)]; + const nmax = recip.map((b) => Math.ceil(kMax / Math.hypot(b[0], b[1], b[2])) + 1); + const hkl: number[][] = []; + const g = new Float32Array(0); + const gList: number[] = []; + const F2: number[] = []; + const Ure: number[] = []; + const Uim: number[] = []; + const fCache = new Map(); + // extinguished by a centering translation when g . t is not an integer + const isCentered = (h: number, k: number, l: number) => p.centering.some((t) => { const x = h * t[0] + k * t[1] + l * t[2]; return Math.abs(x - Math.round(x)) > 1e-6; }); + for (let h = -nmax[0]; h <= nmax[0]; h++) for (let k = -nmax[1]; k <= nmax[1]; k++) for (let l = -nmax[2]; l <= nmax[2]; l++) { + if (h === 0 && k === 0 && l === 0) continue; + const gx = h * recip[0][0] + k * recip[1][0] + l * recip[2][0]; + const gy = h * recip[0][1] + k * recip[1][1] + l * recip[2][1]; + const gz = h * recip[0][2] + k * recip[1][2] + l * recip[2][2]; + const gl = Math.hypot(gx, gy, gz); + if (gl > kMax || isCentered(h, k, l)) continue; + // structure factor F = sum f_j occ_j exp(-2 pi i g.r_j) / V (quantem convention) + let re = 0, im = 0; + const gk = gl.toFixed(5); + for (const s of species) { + const key = s + gk; + let f = fCache.get(key); + if (f === undefined) { f = scatteringFactor(s, gl); fCache.set(key, f); } + for (let j = 0; j < p.symbols.length; j++) { + if (p.symbols[j] !== s) continue; + const ph = -2 * Math.PI * (h * p.positions[j][0] + k * p.positions[j][1] + l * p.positions[j][2]); + re += f * p.occupancy[j] * Math.cos(ph); + im += f * p.occupancy[j] * Math.sin(ph); + } + } + re /= volume; im /= volume; + hkl.push([h, k, l]); + gList.push(gx, gy, gz); + F2.push(re * re + im * im); + // coupling U = gamma F / pi, plus an absorptive part i * fraction * U (same phase) + const ur = (gamma * re) / Math.PI, ui = (gamma * im) / Math.PI; + Ure.push(ur - ABSORPTION_FRACTION * ui); + Uim.push(ui + ABSORPTION_FRACTION * ur); + } + void g; + let f0 = 0; + for (let j = 0; j < p.symbols.length; j++) f0 += scatteringFactor(p.symbols[j], 0) * p.occupancy[j]; + const u0 = (gamma * f0) / (Math.PI * volume); + const couplingRe = new Map(); + const couplingIm = new Map(); + for (let i = 0; i < hkl.length; i++) { + couplingRe.set(`${hkl[i][0]},${hkl[i][1]},${hkl[i][2]}`, Ure[i]); + couplingIm.set(`${hkl[i][0]},${hkl[i][1]},${hkl[i][2]}`, Uim[i]); + } + const [a, b, , , , ga] = p.cellpar; + return { + name: p.name, + spacegroup: p.spacegroup, + pointgroup: "", + cell, + recip, + positions_frac: p.positions, + numbers: p.symbols.map((s) => Math.max(1, SYMBOLS.indexOf(s))), + symbols: p.symbols, + colors: p.symbols.map((s) => JMOL[s] || [0.7, 0.7, 0.7]), + radii: p.symbols.map((s) => RADII[s] || 1.4), + hkl, + g: Float32Array.from(gList), + F2: Float32Array.from(F2), + U_re: Float32Array.from(Ure), + U_im: Float32Array.from(Uim), + couplingRe, + couplingIm, + u0_imag: ABSORPTION_FRACTION * u0, + absorptive: true, + energy_ev: energyEv, + wavelength: electronWavelength(energyEv), + k_max: kMax, + hexagonal: Math.abs(a - b) < 1e-3 * a && Math.abs(ga - 120) < 0.05, + }; +} diff --git a/widget/js/diffsim-web/index.ts b/widget/js/diffsim-web/index.ts index be4883eb3..8531e89ea 100644 --- a/widget/js/diffsim-web/index.ts +++ b/widget/js/diffsim-web/index.ts @@ -29,12 +29,14 @@ import { Frame, cbedImage, drawDisks, drawEwaldPanel, drawImage, drawKikuchiOverlay, drawKosselLines, setupCanvas, tiltGrid, toPx, } from "../diffsim/pattern"; import { ENERGY_EV, PRESETS } from "./presets"; +import { ParsedCif, crystalFromCif, parseCif } from "./cif"; const DIRECT: Reflection = { index: -1, hkl: [0, 0, 0], g: [0, 0, 0], gLen: 0, s: 0 }; const SG_MAX = 0.05; const VIEW_X = -1; // detector-side view: cell and pattern move together const MAX_BEAMS = 48; const MAX_BEAMS_DRAG = 28; +const SPIN_FPS = 20; // recompute rate while spinning (battery) const CBED_GRID = 7; const CBED_GRID_DRAG = 5; @@ -76,6 +78,9 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { fieldMrad: opt("field_mrad", 50) as number, patternRange: opt("pattern_range", 3.0) as number, stepDeg: 15, + spinSpeed: opt("rotation_speed_deg", 6) as number, // deg/s for the continuous rotation buttons + spinX: false, // continuous slow rotation about the screen x axis (vertical motion) + spinY: false, // ... about the screen y axis (horizontal motion) showLabels: opt("show_labels", true) as boolean, showHkl: opt("show_hkl", true) as boolean, showAppearance: false, @@ -90,11 +95,18 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { showEwald: opt("show_ewald", true) as boolean, quat: [1, 0, 0, 0] as Quat, dragging: false, + spinning: false, }; const ENERGIES = [60e3, 80e3, 100e3, 120e3, 200e3, 300e3]; if (!ENERGIES.includes(state.energy)) ENERGIES.push(state.energy); ENERGIES.sort((a, b) => a - b); - let crystal: CrystalData = parseCrystal(PRESETS[state.preset], state.energy)!; + const cifs = new Map(); // structures loaded from CIF files, by menu name + const loadCrystal = (name: string, energy: number): CrystalData => { + const cif = cifs.get(name); + if (cif) return crystalFromCif(cif, energy, 3.0); + return parseCrystal(PRESETS[name], energy)!; + }; + let crystal: CrystalData = loadCrystal(state.preset, state.energy); let geom = cellGeometry(crystal, state.nCells, state.polyhedra); const k0 = () => 1 / crystal.wavelength; @@ -118,7 +130,10 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { .${id}-wrap { background: var(--${id}-bg, #111); color: var(--${id}-fg, #ccc); border: 1px solid var(--${id}-border, #444); border-radius: 8px; padding: 10px 14px 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 13px; max-width: 100%; box-sizing: border-box; } - .${id}-title { font-size: 16px; font-weight: 600; color: var(--${id}-title, #ddd); margin-bottom: 6px; text-align: center; } + .${id}-titlerow { position: relative; display: flex; align-items: center; justify-content: center; margin-bottom: 6px; min-height: 28px; } + .${id}-title { font-size: 16px; font-weight: 600; color: var(--${id}-title, #ddd); text-align: center; } + .${id}-titlerow .${id}-btn { position: absolute; right: 0; top: 50%; transform: translateY(-50%); } + .${id}-file { display: none; } .${id}-top { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; justify-content: center; margin-bottom: 8px; } .${id}-panels { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; } .${id}-panel { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 0 0 auto; } @@ -153,7 +168,11 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { wrap.className = `${id}-wrap`; const presetOptions = names.map((n) => ``).join(""); wrap.innerHTML = ` -
Electron diffraction simulator
+
+
Electron diffraction simulator
+
load CIF…
+ +
@@ -170,6 +189,8 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) {
y −
y +
z −
z +
° +
↔
↕
+ ${state.spinSpeed}°/s
@@ -291,25 +312,26 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { // everything that depends on the orientation (not on thickness) const q = state.quat; const alpha = state.semiconv * 1e-3; - const maxBeams = state.dragging ? dragBeams : MAX_BEAMS; + const maxBeams = (state.dragging || state.spinning) ? dragBeams : MAX_BEAMS; if (state.mode === "nanobeam") { - nbTilts = precessionTilts(k0(), state.precession, state.dragging ? dragNodes : 16); + nbTilts = precessionTilts(k0(), state.precession, (state.dragging || state.spinning) ? dragNodes : 16); if (state.dynamical) { - nbSolution = nanobeamSolve(crystal, q, state.patternRange, SG_MAX, maxBeams, nbTilts); + // the physics always uses every reflection the crystal carries; the pattern range only crops the drawing + nbSolution = nanobeamSolve(crystal, q, crystal.k_max, SG_MAX, maxBeams, nbTilts); nb = { beams: nbSolution.beams, nDyn: Math.round(nbSolution.nDynMean) }; } else { - nb = { beams: [DIRECT, ...labReflections(crystal, q, state.patternRange)], nDyn: 0 }; + nb = { beams: [DIRECT, ...labReflections(crystal, q, crystal.k_max)], nDyn: 0 }; nbSolution = null; } } else if (state.mode === "cbed") { const Rk = k0() * Math.sin(alpha); - const grid = tiltGrid(Rk, state.dragging ? CBED_GRID_DRAG : CBED_GRID); + const grid = tiltGrid(Rk, (state.dragging || state.spinning) ? CBED_GRID_DRAG : CBED_GRID); if (state.dynamical) { - const { beams, nDyn } = hybridBeams(crystal, q, state.patternRange, SG_MAX, state.dragging ? 20 : 32, Math.sin(alpha)); + const { beams, nDyn } = hybridBeams(crystal, q, crystal.k_max, SG_MAX, (state.dragging || state.spinning) ? 20 : 32, Math.sin(alpha)); const dyn = beams.slice(0, nDyn); cbed = { grid, beams, nDyn, sols: grid.tilts.map((t) => blochSolve(crystal, dyn, t)) }; } else { - cbed = { grid, beams: [DIRECT, ...labReflections(crystal, q, state.patternRange)], nDyn: 0, sols: null }; + cbed = { grid, beams: [DIRECT, ...labReflections(crystal, q, crystal.k_max)], nDyn: 0, sols: null }; } } if (state.mode === "kossel" || (state.mode === "nanobeam" && state.kikuchi)) { @@ -339,7 +361,7 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { const ctx = ewaldCanvas.getContext("2d"); if (ctx) { ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - const refl = state.mode === "nanobeam" && nb.beams.length ? nb.beams : labReflections(crystal, state.quat, state.patternRange); + const refl = state.mode === "nanobeam" && nb.beams.length ? nb.beams : labReflections(crystal, state.quat, crystal.k_max); drawEwaldPanel(ctx, Sc, Se, refl, k0(), state.patternRange, SG_MAX, VIEW_X, dark, state.mode === "nanobeam" ? state.precession : 0); } } @@ -460,7 +482,7 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { const t0 = performance.now(); solveOrientation(); drawAll(); - if (state.dragging) { + if (state.dragging || state.spinning) { // keep dragging responsive: aim for well under 100 ms per recompute const dt = performance.now() - t0; if (dt > 90) { dragBeams = Math.max(12, Math.round(dragBeams * 0.7)); dragNodes = Math.max(4, dragNodes - 2); } @@ -595,6 +617,39 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { }); }); $(`#${id}-step`).addEventListener("change", (e) => { state.stepDeg = Math.max(0.1, +(e.target as HTMLInputElement).value || 15); }); + // continuous slow rotation (toggle buttons): state.spinSpeed deg/s about the screen + // axes, recomputed at drag quality no more than SPIN_FPS times a second + let spinRaf = 0, spinLast = 0, spinDue = 0, spinPendX = 0, spinPendY = 0; + const spinTick = (t: number) => { + spinRaf = 0; + if (!state.spinX && !state.spinY) { + if (state.spinning) { state.spinning = false; recompute(); } // back to full quality + return; + } + const dt = spinLast ? Math.min(0.1, (t - spinLast) / 1000) : 0; + spinLast = t; + if (state.spinY) spinPendY += state.spinSpeed * dt; + if (state.spinX) spinPendX += state.spinSpeed * dt; + if (t >= spinDue) { + spinDue = t + 1000 / SPIN_FPS; + if (spinPendY) rotateScreen([0, 1, 0], spinPendY); + if (spinPendX) rotateScreen([1, 0, 0], spinPendX); + spinPendX = spinPendY = 0; + recompute(); + } + spinRaf = requestAnimationFrame(spinTick); + }; + $(`#${id}-spin`).addEventListener("input", (e) => { + state.spinSpeed = Math.max(1, +(e.target as HTMLInputElement).value || 6); + $(`#${id}-spinv`).textContent = `${state.spinSpeed}°/s`; + }); + wrap.querySelectorAll(`[data-spin]`).forEach((b) => { + b.addEventListener("click", () => { + if (b.dataset.spin === "x") state.spinX = !state.spinX; else state.spinY = !state.spinY; + b.classList.toggle("active", b.dataset.spin === "x" ? state.spinX : state.spinY); + if ((state.spinX || state.spinY) && !spinRaf) { state.spinning = true; spinLast = 0; spinRaf = requestAnimationFrame(spinTick); } + }); + }); const goZone = () => { const v = parseDirection($(`#${id}-zone`).value); // 3 or 4 (Miller-Bravais) indices if (!v) return; @@ -604,9 +659,42 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { $(`#${id}-go`).addEventListener("click", goZone); $(`#${id}-zone`).addEventListener("keydown", (e) => { if (e.key === "Enter") goZone(); }); $(`#${id}-reset`).addEventListener("click", () => { setZoneAxis(crystal.hexagonal ? [0, 0, 1] : [1, 1, 0]); recompute(); }); + // CIF upload: parse in the browser, add to the structure menu, select it + const fileInput = $(`#${id}-ciffile`); + $(`#${id}-cifbtn`).addEventListener("click", () => fileInput.click()); + fileInput.addEventListener("change", async () => { + const file = fileInput.files?.[0]; + if (!file) return; + try { + const text = await file.text(); + const parsed = parseCif(text, file.name.replace(/\.cif$/i, "")); + const menuName = `${parsed.name} (CIF)`; + cifs.set(menuName, parsed); + const sel = $(`#${id}-preset`); + if (![...sel.options].some((o) => o.value === menuName)) { + const opt = document.createElement("option"); + opt.value = menuName; opt.textContent = menuName; + sel.appendChild(opt); + } + sel.value = menuName; + state.preset = menuName; + crystal = loadCrystal(menuName, state.energy); + const range = $(`#${id}-range`); + range.max = String(crystal.k_max); + if (state.patternRange > crystal.k_max) { state.patternRange = crystal.k_max; range.value = String(crystal.k_max); } + range.dispatchEvent(new Event("input")); // refresh the slider label + geomKey = ""; + setZoneAxis(crystal.hexagonal ? [0, 0, 1] : [1, 1, 0]); + recompute(); + $(`#${id}-status`).textContent = `${menuName}: ${parsed.symbols.length} atoms in the cell, ${parsed.spacegroup || "symmetry from the file"}, ${crystal.hkl.length} reflections out to ${crystal.k_max.toFixed(2)} Å⁻¹${crystal.k_max < 3 ? " (reduced for this cell size)" : ""}; absorption approximated as 8 % of the potential`; + } catch (err) { + $(`#${id}-status`).textContent = `could not read ${file.name}: ${(err as Error).message}`; + } + fileInput.value = ""; + }); $(`#${id}-preset`).addEventListener("change", (e) => { state.preset = (e.target as HTMLSelectElement).value; - crystal = parseCrystal(PRESETS[state.preset], state.energy)!; + crystal = loadCrystal(state.preset, state.energy); const range = $(`#${id}-range`); range.max = String(crystal.k_max); if (state.patternRange > crystal.k_max) { state.patternRange = crystal.k_max; range.value = String(crystal.k_max); } @@ -642,7 +730,7 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { $(`#${id}-ewald`).addEventListener("change", (e) => { state.showEwald = (e.target as HTMLInputElement).checked; computeSize(); drawAll(); }); $(`#${id}-energy`).addEventListener("change", (e) => { state.energy = +(e.target as HTMLSelectElement).value; - crystal = parseCrystal(PRESETS[state.preset], state.energy)!; + crystal = loadCrystal(state.preset, state.energy); recompute(); }); $(`#${id}-ncell`).addEventListener("change", (e) => { diff --git a/widget/js/diffsim-web/lobato.ts b/widget/js/diffsim-web/lobato.ts new file mode 100644 index 000000000..d237d03c4 --- /dev/null +++ b/widget/js/diffsim-web/lobato.ts @@ -0,0 +1,107 @@ +// Generated from quantem/diffraction/data/lobato.json: Lobato & Van Dyck (2014) +// electron scattering factor parameters, 5 (a_i, b_i) pairs per element. +export const LOBATO: Record = { + H: [[0.006473848488, -0.4901925768, 0.5732841604, -0.3794033015, 0.5544264748], [2.785198854, 2.776204283, 2.775385911, 2.767593029, 2.765118976]], + He: [[3.057451161, -62.00447791, 64.00555371, -5.001325785, 0.1517988287], [1.089672487, 0.9398387981, 0.9252890344, 0.8229474987, 0.5773931107]], + Li: [[3.926222729, -4.548619626, 2.193353129, 0.0699451265, 0.002098642249], [8.142760135, 4.98941077, 4.144289992, 0.4019223151, 0.1564790347]], + Be: [[3.398249706, -1.908668861, 0.03907021175, -0.01116310102, 0.009462044654], [4.442701786, 3.324515425, 0.1897728803, 0.08719186146, 0.082780906]], + B: [[1.472792486, -0.4019330422, 0.305998957, 0.01961442172, 0.0009771771061], [3.749740483, 0.5880665361, 0.5156396131, 0.1213775701, 0.06809824122]], + C: [[124.4660886, -220.3528571, 195.2353523, -98.10793613, 0.01420230412], [2.421208493, 2.305379438, 2.048519321, 1.933525529, 0.07689768185]], + N: [[58.13271507, -147.5424091, 130.1430656, -39.61956741, 0.01059577633], [1.700448564, 1.559038526, 1.415768275, 1.278418182, 0.05655877985]], + O: [[29.94740452, -77.61012663, 99.88177646, -51.21270055, 0.008196189544], [1.302839879, 1.157941053, 1.009885493, 0.9433279714, 0.04331976113]], + F: [[0.9489848945, -30.1333923, 52.79650781, -22.70627038, 0.006569976645], [1.458829332, 0.6887799932, 0.6542398693, 0.6148361308, 0.03428374195]], + Ne: [[0.5827411922, 0.3706765618, -0.5467449674, 0.4140526825, 0.005199030809], [1.281185731, 0.4445208972, 0.1986508755, 0.1854772467, 0.0275738382]], + Na: [[23.67006039, -21.85317862, 0.5924994481, -0.02446522903, 0.004839502217], [8.451487735, 8.040966005, 0.6249960005, 0.1324503949, 0.0233994362]], + Mg: [[4.855010477, -2.662209065, 0.4780012361, -0.07023070647, 0.003989058281], [5.946392738, 4.171303125, 0.3982698082, 0.1618861858, 0.01953450564]], + Al: [[2.834095616, -4.280041334, 4.421916805, -0.03457744719, 0.003523859414], [6.66235024, 0.5512947222, 0.5093289634, 0.1117848374, 0.01676023518]], + Si: [[2.871891426, -2.061735012, 2.171140242, -0.06630736331, 0.003010707097], [5.084871036, 0.4291781853, 0.3664854342, 0.1197106113, 0.01439945361]], + P: [[2.7915184, -4.365068378, 4.435584555, -0.08096357734, 0.00267900018], [3.900659619, 0.3298259684, 0.3060899565, 0.1080832325, 0.01258944953]], + S: [[2.679714156, -0.4742528222, 0.514835949, -0.0958360025, 0.002488719638], [3.068891212, 0.3782167022, 0.1887218119, 0.092337059, 0.01119208772]], + Cl: [[2.5662484, -0.3388763508, 1.145845588, -0.9231093165, 0.00229168002], [2.415949204, 0.4214142393, 0.109592405, 0.09909554582, 0.009996659489]], + Ar: [[2.459817464, -0.3641981771, 0.2505844772, -0.05774370295, 0.002301438668], [1.94004632, 0.3992410679, 0.1174724063, 0.0567803726, 0.009155798329]], + K: [[5.811078786, -50.25370965, 48.86094121, 0.0740628592, 0.0007278027386], [12.66914834, 3.956410397, 3.683850596, 0.1074585176, 0.006655767894]], + Ca: [[21.17811615, -339.0438243, 322.7569585, 0.06500776739, 0.0006558743666], [6.396086194, 3.740247139, 3.648884499, 0.09450906345, 0.005985206199]], + Sc: [[12.60351866, -276.8753821, 268.8716039, 0.05568241789, 0.0005770712551], [6.156256154, 3.088735543, 3.027276633, 0.08188747484, 0.005382898321]], + Ti: [[8.575957752, -210.3315635, 206.0971726, 0.0477773949, 0.0005057164845], [6.007806689, 2.602858567, 2.553523451, 0.0711429484, 0.004856284394]], + V: [[6.527684332, -200.4305768, 198.0150539, 0.04139115181, 0.0004474550243], [5.835524794, 2.232559524, 2.197860186, 0.06239738768, 0.004403836491]], + Cr: [[3.028317848, -95.53939331, 96.17615624, 0.0359777316, 0.0003914928907], [8.359115043, 1.802637903, 1.77509489, 0.05481444121, 0.003998289689]], + Mn: [[4.374175506, -160.9255109, 160.2733081, 0.0312303861, 0.000346966021], [5.510317055, 1.687982164, 1.666140478, 0.04833703903, 0.003647469601]], + Fe: [[3.798100908, -91.68935494, 91.44542522, 0.0272754344, 0.0003033798544], [5.317126459, 1.497130949, 1.468092418, 0.04272478501, 0.003327918552]], + Co: [[3.330378745, -77.00175965, 77.07252218, 0.0239904669, 0.0002682566655], [5.181359646, 1.329151223, 1.30284929, 0.03806454868, 0.00305010008]], + Ni: [[2.969080787, -75.74770691, 76.03982876, 0.02101621137, 0.0002311517511], [5.041809491, 1.182755079, 1.162165458, 0.03374790879, 0.002786808621]], + Cu: [[1.752071452, -43.04105235, 44.07059155, 0.01868761541, 0.0002017273248], [6.18750498, 1.002662636, 0.9853843114, 0.03029847039, 0.002558555987]], + Zn: [[2.466371105, -61.46785413, 62.01769452, 0.01641601739, 0.0001724871176], [4.910280785, 0.9678985203, 0.9512838348, 0.02696009677, 0.00234109611]], + Ga: [[2.760102031, -34.44526142, 35.22622672, 0.0132067197, 0.0001259455609], [6.101282245, 0.7651433136, 0.7513286234, 0.02248796343, 0.002067373743]], + Ge: [[3.182416353, -52.45140378, 52.96908272, 0.01140961686, 9.509543581e-05], [5.017190409, 0.7123957644, 0.7022801925, 0.01967472957, 0.001841466145]], + As: [[3.456429691, -33.31760444, 33.57121939, 0.009790022956, 6.534348623e-05], [4.01358016, 0.6623557781, 0.6457719411, 0.01709193532, 0.001603016028]], + Se: [[3.649050478, -43.68516622, 43.69202886, 0.008449022842, 3.786114741e-05], [3.250432671, 0.6096662017, 0.5969713008, 0.01485545127, 0.001336255874]], + Br: [[3.838463122, -52.2723471, 51.98612795, 0.007339559893, 1.64694208e-05], [2.611894706, 0.5661950629, 0.5552793267, 0.01297464694, 0.001029865369]], + Kr: [[4.025410303, -46.30423321, 45.72136819, 0.006353379596, 1.334778453e-06], [2.136483814, 0.5265911665, 0.5141367845, 0.01128072422, 0.0004880898579]], + Rb: [[3.389753516, 2.143483487, 0.3543226035, 0.003740093401, 3.003425023e-07], [20.57448144, 1.910799452, 0.1974105894, 0.008134594653, 0.0002926857911]], + Sr: [[4.770925093, 1.475978502, 0.3044513555, 0.003594749819, 3.000855492e-07], [13.36688813, 1.337383796, 0.1775323694, 0.00779105033, 0.0002822551396]], + Y: [[4.607210199, 1.42801851, 0.2955810456, 0.003389978409, 2.66862975e-07], [10.86869056, 1.311374559, 0.1680228708, 0.007359645454, 0.000262343281]], + Zr: [[4.311754534, 1.49331578, 0.2812360501, 0.003093377385, 2.580244485e-07], [9.458965805, 1.330636228, 0.1565068714, 0.006824105238, 0.0002498188791]], + Nb: [[3.111790391, 2.202590609, 0.2703307749, 0.002687991948, 2.325494834e-07], [10.69031413, 1.653163562, 0.1451151857, 0.006139563516, 0.0002322402359]], + Mo: [[2.831059684, 2.348581375, 0.2451058884, 0.002352821082, 2.312708383e-07], [10.43571958, 1.604828687, 0.1316969348, 0.005549779014, 0.0002227474638]], + Tc: [[2.571798593, 2.45633742, 0.2206584409, 0.00205531418, 2.321329478e-07], [10.16431171, 1.534419192, 0.1191386198, 0.005018532409, 0.0002145903791]], + Ru: [[2.332300304, 2.535780255, 0.1982080421, 0.001761201305, 1.981294116e-07], [9.921674602, 1.455856688, 0.1076821879, 0.004472435457, 0.0001965747162]], + Rh: [[2.113525349, 2.586363162, 0.1770639139, 0.00149737051, 2.054814456e-07], [9.659137259, 1.371066569, 0.09703530276, 0.003971285091, 0.0001913551907]], + Pd: [[0.6421597962, 2.979148144, 0.168154426, 0.001337442139, 1.914109683e-07], [5.974797503, 1.433594325, 0.09098684012, 0.003624101371, 0.0001806889144]], + Ag: [[1.553172167, 2.639303647, 0.142015487, 0.001008504601, 1.9463843e-07], [8.156202358, 1.216008875, 0.07900988649, 0.002965479014, 0.0001745930959]], + Cd: [[61.5307852, -78.60167412, 21.55012926, 0.1376850157, 0.0003246449309], [3.114681025, 2.760169834, 1.935513123, 0.07224683473, 0.001170016296]], + In: [[4.222321779, -26.41213184, 27.28528527, 0.1216179019, 0.0003068835464], [6.072655104, 1.64550179, 1.522570749, 0.06565079147, 0.001120938515]], + Sn: [[5.142220746, -25.49454138, 25.74144875, 0.1117782276, 0.0002936473847], [5.272726365, 1.531949592, 1.40257525, 0.06116853873, 0.001075608154]], + Sb: [[6.241640318, -93.38687244, 92.63328758, 0.1034126922, 0.0002818484269], [4.269841081, 1.394077461, 1.355668549, 0.05726679497, 0.001033079543]], + Te: [[7.377433018, -126.0251069, 124.128405, 0.09599783718, 0.0002710722164], [3.469177578, 1.297598109, 1.267710676, 0.05377717502, 0.000993084577]], + I: [[9.644006663, -122.9244354, 118.6825648, 0.0895025947, 0.0002612761215], [2.726455454, 1.237234259, 1.200620369, 0.05066786823, 0.0009554378834]], + Xe: [[15.54517497, -118.2410279, 108.009525, 0.0836259342, 0.0002519918624], [2.106373409, 1.208603761, 1.153952706, 0.04781893913, 0.0009198862576]], + Cs: [[4.287087392, 3.232506654, 0.6740295336, 0.06189080834, 0.000235612053], [22.65878708, 2.237973865, 0.3689955687, 0.04022665753, 0.0008837618909]], + Ba: [[6.244751874, 2.351722714, 0.4742793732, 0.06381138743, 0.0002346512806], [15.14313542, 1.453790006, 0.3208356464, 0.04043545321, 0.0008540811313]], + La: [[6.097881796, 2.194951647, 0.548172792, 0.06166695732, 0.0002268073559], [12.42885443, 1.505359924, 0.3337380397, 0.03877445535, 0.0008240498841]], + Ce: [[5.795268796, 2.370226641, 0.4713987569, 0.05743682606, 0.0002189794893], [14.28010551, 1.359690157, 0.3020173497, 0.03664367981, 0.0007954345265]], + Pr: [[5.604062554, 2.357962596, 0.4760010986, 0.05441233743, 0.0002114146022], [13.95174902, 1.312397549, 0.2949337012, 0.03486015425, 0.0007682616827]], + Nd: [[5.42908392, 2.336873254, 0.4833735541, 0.05146549646, 0.0002037761329], [13.65036494, 1.267598414, 0.2886106816, 0.03312918074, 0.0007423484838]], + Pm: [[5.267744451, 2.308558133, 0.493265479, 0.04863562797, 0.0001963088423], [13.36020968, 1.225858566, 0.2829196321, 0.03147353971, 0.0007176580251]], + Sm: [[5.126804285, 2.26925534, 0.5042093005, 0.04569239924, 0.0001886750505], [13.1581501, 1.181295082, 0.2771070382, 0.02979576045, 0.0006940110992]], + Eu: [[4.979623597, 2.241830875, 0.5129339614, 0.04298018485, 0.0001813816925], [12.83926631, 1.147054464, 0.2703871609, 0.0282418715, 0.0006714866032]], + Gd: [[5.0783583, 1.957440272, 0.5928259832, 0.04195020341, 0.0001752410915], [10.51327255, 1.117649413, 0.2843867418, 0.02726633276, 0.0006503108607]], + Tb: [[4.711616367, 2.172619508, 0.5397176013, 0.03797960045, 0.0001669237636], [12.31094159, 1.087437968, 0.2598049655, 0.02532899239, 0.000629278567]], + Dy: [[4.590755045, 2.13572373, 0.5513555601, 0.03560584067, 0.0001598240169], [12.06567406, 1.058117371, 0.2539944389, 0.02395087396, 0.0006094827484]], + Ho: [[4.484001106, 2.089043471, 0.5659326183, 0.03327035902, 0.0001524456103], [11.87492507, 1.028284937, 0.2489078079, 0.02258440655, 0.0005903744452]], + Er: [[4.376651408, 2.046451504, 0.5806628606, 0.03123885283, 0.0001453748697], [11.66530807, 1.003147697, 0.2441532927, 0.021361858, 0.0005721103385]], + Tm: [[4.283083182, 1.995380015, 0.5970535713, 0.02909516689, 0.000138064769], [11.4961906, 0.977039102, 0.239429133, 0.02009122333, 0.0005543771385]], + Yb: [[4.195638407, 1.94333286, 0.6124466173, 0.02715152077, 0.0001305947874], [11.41077505, 0.9490119245, 0.2349503265, 0.01890515763, 0.0005372336492]], + Lu: [[4.356925933, 1.695892048, 0.6639045201, 0.02630200188, 0.0001254973185], [9.294345147, 0.910500046, 0.2387465959, 0.01820985425, 0.000521559372]], + Hf: [[4.331384057, 1.527648647, 0.7357959229, 0.02495263232, 0.0001187408526], [7.876844338, 0.9425156426, 0.24169948, 0.01728998944, 0.0005058346313]], + Ta: [[4.197260013, 1.468548068, 0.7839312482, 0.02324940949, 0.0001112613586], [6.936740249, 1.017252766, 0.2389489397, 0.01623324331, 0.000490239004]], + W: [[3.976297158, 1.522926843, 0.7977062464, 0.02136667416, 0.0001030786894], [6.296857792, 1.112899512, 0.2310570407, 0.01510135456, 0.0004746824771]], + Re: [[3.751443814, 1.62768803, 0.7817567555, 0.01951708039, 9.431998043e-05], [5.797546361, 1.182236311, 0.219913586, 0.01398104555, 0.0004591271258]], + Os: [[3.484015173, 1.793779204, 0.7448783567, 0.01754277852, 8.448723516e-05], [5.439988461, 1.227921341, 0.206208857, 0.01278994017, 0.0004430322268]], + Ir: [[1.599565782, 2.975344522, 0.6950926784, 0.01487796275, 6.905495791e-05], [5.792444474, 1.55300983, 0.1886263359, 0.01117634707, 0.0004227723617]], + Pt: [[2.04021564, 2.899226346, 0.6363440838, 0.01320719196, 5.673821925e-05], [6.658194296, 1.413379238, 0.1740001045, 0.01006878045, 0.0004030771057]], + Au: [[1.675934671, 3.00486603, 0.5953400132, 0.01171631866, 4.296782976e-05], [5.522310932, 1.38007223, 0.1622292377, 0.009018148904, 0.0003792776675]], + Hg: [[2.235228504, 2.682766387, 0.5551949262, 0.01072733544, 3.28473992e-05], [5.02030989, 1.230775906, 0.152248123, 0.008283991169, 0.0003562419389]], + Tl: [[2.803427374, 2.71882788, 0.5224759154, 0.00984537837, 2.345245265e-05], [6.558768728, 1.169724225, 0.1435566918, 0.007619765262, 0.0003296276739]], + Pb: [[3.60861021, 2.450567747, 0.4786395001, 0.008872142352, 1.040019329e-05], [6.581625946, 1.027728527, 0.1335336806, 0.006848612389, 0.0002763888755]], + Bi: [[4.242099011, 2.099943421, 0.4328366314, 0.008020215704, 7.217850309e-07], [5.752801155, 0.8739014892, 0.1235999562, 0.006176003331, 0.0001414951776]], + Po: [[4.636200957, 1.780633114, 0.403730444, 0.007685395547, 8.954159028e-08], [4.888252998, 0.7563105269, 0.1173023645, 0.005930343942, 7.666686639e-05]], + At: [[4.965922506, 1.438155615, 0.3712992006, 0.007472564285, 1.141152842e-07], [4.091293788, 0.6292289966, 0.1110966787, 0.005772350103, 8.085118939e-05]], + Rn: [[5.306156145, 1.117331602, 0.3158587231, 0.007203422423, 1.073553306e-07], [3.488007355, 0.4811907411, 0.1018744679, 0.005592453744, 7.795066735e-05]], + Fr: [[4.52053399, 4.106953979, 0.7139468785, 0.01692940277, 8.574921292e-05], [19.44822342, 1.898246732, 0.1695535636, 0.01148195675, 0.0003461220383]], + Ra: [[6.52401072, 3.207870807, 0.5404787743, 0.008782788069, 6.910106029e-06], [14.00925543, 1.32635036, 0.1314085684, 0.006286474345, 0.0002278399815]], + Ac: [[6.896028536, 2.835141545, 0.5035068819, 0.00802294511, 9.204009548e-08], [11.07638257, 1.171326163, 0.1234946514, 0.005735155958, 7.197075418e-05]], + Th: [[7.093749002, 2.529123739, 0.4821078199, 0.007719366741, 7.271141994e-08], [9.094737952, 1.06391667, 0.1186194466, 0.005539457089, 6.584947305e-05]], + Pa: [[6.434013248, 2.970999705, 0.4607966518, 0.007240331258, 6.362366643e-08], [10.25968513, 1.131774523, 0.1127759354, 0.005274595834, 6.192638241e-05]], + U: [[6.210708268, 3.039344538, 0.4373399844, 0.006807147641, 6.182293277e-08], [10.02141059, 1.103998802, 0.1072189732, 0.005024321309, 6.005069942e-05]], + Np: [[6.004315983, 3.094316545, 0.4128084877, 0.006408916579, 6.730073762e-08], [9.817930467, 1.069787051, 0.1016352914, 0.004795248469, 6.028065987e-05]], + Pu: [[5.200617168, 3.498494404, 0.4054149311, 0.006223437008, 6.008593295e-08], [11.20383102, 1.128463695, 0.09893334727, 0.00465917432, 5.72590639e-05]], + Am: [[5.0253386, 3.518439883, 0.3819503494, 0.005821102081, 6.526093388e-08], [10.97721907, 1.084772148, 0.09367468749, 0.004426823469, 5.738568371e-05]], + Cm: [[5.346561003, 3.224684666, 0.3494617526, 0.005292517567, 6.159176302e-08], [9.231183798, 0.9728362292, 0.0870596221, 0.004130119645, 5.494638944e-05]], + Bk: [[5.225823503, 3.22818874, 0.3270988788, 0.004888825756, 5.212722694e-08], [9.071357066, 0.9311242775, 0.08207206021, 0.00389029656, 5.103679766e-05]], + Cf: [[4.586412478, 3.518695957, 0.3192617142, 0.00472979648, 5.513244808e-08], [10.31861021, 0.9572788378, 0.07968074314, 0.003771932, 5.097508604e-05]], + Es: [[4.457994807, 3.508672126, 0.3013753805, 0.004407637462, 4.887879032e-08], [10.08906934, 0.9194464474, 0.07564191778, 0.003570264651, 4.804951744e-05]], + Fm: [[4.338975764, 3.491850989, 0.281471099, 0.004002092715, 5.529298082e-08], [9.963309372, 0.878083957, 0.07116371158, 0.003321524042, 4.851031795e-05]], + Md: [[4.227294704, 3.472492275, 0.2648222295, 0.003690728778, 6.258714262e-08], [9.72400602, 0.842873776, 0.0673534744, 0.003123646063, 4.91217097e-05]], + No: [[4.109517024, 3.457991325, 0.2470873512, 0.003304239209, 5.991049262e-08], [9.677359945, 0.8069400425, 0.0632815437, 0.002875446396, 4.706791537e-05]], + Lr: [[4.521474212, 3.202129856, 0.223028727, 0.002817164539, 4.06427954e-08], [8.283099069, 0.7319189581, 0.0580942773, 0.00256168016, 4.038165155e-05]], +}; diff --git a/widget/js/diffsim/index.tsx b/widget/js/diffsim/index.tsx index ff532efa6..aee8073a6 100644 --- a/widget/js/diffsim/index.tsx +++ b/widget/js/diffsim/index.tsx @@ -35,6 +35,7 @@ import { } from "./pattern"; const DIRECT: Reflection = { index: -1, hkl: [0, 0, 0], g: [0, 0, 0], gLen: 0, s: 0 }; +const SPIN_FPS = 20; // orientation update rate while spinning const QUALITY: Record = { fast: { grid: 5, beams: 24, nanobeam: 40 }, medium: { grid: 7, beams: 36, nanobeam: 64 }, @@ -133,6 +134,7 @@ function DiffSim() { const [nPrecession] = useModelState("n_precession"); const [sigma, setSigma] = useModelState("sigma_excitation"); const [stepDeg, setStepDeg] = useModelState("rotation_step_deg"); + const [spinSpeed, setSpinSpeed] = useModelState("rotation_speed_deg"); // deg/s for the continuous rotation buttons const [scaling, setScaling] = useModelState("scaling"); const [power, setPower] = useModelState("power"); const [cmap, setCmap] = useModelState("cmap"); @@ -165,7 +167,11 @@ function DiffSim() { const qMaxDisp = Math.min(Math.max(patternRange || 0, 0.2), crystal?.k_max ?? 4); const setQMaxDisp = setPatternRange; const [zoneText, setZoneText] = React.useState(""); - const [dragging, setDragging] = React.useState(false); + const [ptrDrag, setDragging] = React.useState(false); + // continuous slow rotation about the screen axes (toggle buttons next to the step box) + const [spin, setSpin] = React.useState<{ x: boolean; y: boolean }>({ x: false, y: false }); + const spinning = spin.x || spin.y; + const dragging = ptrDrag || spinning; // reduced quality while the orientation is changing const [winW, setWinW] = React.useState(typeof window !== "undefined" ? window.innerWidth : 1200); React.useEffect(() => { const f = () => setWinW(window.innerWidth); @@ -197,6 +203,28 @@ function DiffSim() { setQuat(qnormalize(qmult(dq, quatRef.current)), immediate); }, [setQuat, viewX]); + const spinRef = React.useRef({ ...spin, speed: spinSpeed || 6 }); + spinRef.current = { ...spin, speed: spinSpeed || 6 }; + React.useEffect(() => { + if (!spinning) return; + let raf = 0, last = 0, due = 0, pendX = 0, pendY = 0; + const tick = (t: number) => { + const dt = last ? Math.min(0.1, (t - last) / 1000) : 0; + last = t; + if (spinRef.current.y) pendY += spinRef.current.speed * dt; + if (spinRef.current.x) pendX += spinRef.current.speed * dt; + if (t >= due) { + due = t + 1000 / SPIN_FPS; + if (pendY) rotateLab([0, 1, 0], pendY, false); + if (pendX) rotateLab([1, 0, 0], pendX, false); + pendX = pendY = 0; + } + raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); + }, [spinning, rotateLab]); + // ---- pointer handling on the cell canvas (mouse and touch) ------------- const cellRef = React.useRef(null); const pointers = React.useRef>(new Map()); @@ -350,13 +378,14 @@ function DiffSim() { }, [k0, precession, nPrecession, dragging]); const nbSolution = React.useMemo(() => { if (!crystal || mode !== "nanobeam" || !dynamical) return null; - return nanobeamSolve(crystal, quat, qMaxDisp, SG_MAX, dragging ? Math.min(qual.nanobeam, 40) : qual.nanobeam, precNodes); - }, [crystal, quat, qMaxDisp, mode, dynamical, dragging, qual, SG_MAX, precNodes]); + // the physics uses every reflection the crystal carries; the pattern range only crops the drawing + return nanobeamSolve(crystal, quat, crystal.k_max, SG_MAX, dragging ? Math.min(qual.nanobeam, 40) : qual.nanobeam, precNodes); + }, [crystal, quat, mode, dynamical, dragging, qual, SG_MAX, precNodes]); const nbBeams = React.useMemo(() => { if (!crystal || mode !== "nanobeam") return []; if (nbSolution) return nbSolution.beams; - return [DIRECT, ...labReflections(crystal, quat, qMaxDisp)]; - }, [crystal, quat, qMaxDisp, mode, nbSolution]); + return [DIRECT, ...labReflections(crystal, quat, crystal.k_max)]; + }, [crystal, quat, mode, nbSolution]); const nb = { beams: nbBeams, nDyn: nbSolution ? Math.round(nbSolution.nDynMean) : 0 }; const nbInten = React.useMemo(() => { if (!crystal || mode !== "nanobeam") return new Float64Array(0); @@ -375,8 +404,8 @@ function DiffSim() { if (!crystal || mode !== "cbed") return null; const Rk = k0 * Math.sin(alpha); const grid = tiltGrid(Rk, dragging ? 5 : qual.grid); - if (!dynamical) return { grid, beams: [DIRECT, ...labReflections(crystal, quat, qMaxDisp)], nDyn: 0, sols: null }; - const { beams, nDyn } = hybridBeams(crystal, quat, qMaxDisp, SG_MAX, dragging ? Math.min(qual.beams, 24) : qual.beams, Math.sin(alpha)); + if (!dynamical) return { grid, beams: [DIRECT, ...labReflections(crystal, quat, crystal.k_max)], nDyn: 0, sols: null }; + const { beams, nDyn } = hybridBeams(crystal, quat, crystal.k_max, SG_MAX, dragging ? Math.min(qual.beams, 24) : qual.beams, Math.sin(alpha)); const dyn = beams.slice(0, nDyn); const sols = grid.tilts.map((t) => blochSolve(crystal, dyn, t)); return { grid, beams, nDyn, sols }; @@ -449,7 +478,7 @@ function DiffSim() { const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - const refl = mode === "nanobeam" && nbBeams.length ? nbBeams : labReflections(crystal, quat, qMaxDisp); + const refl = mode === "nanobeam" && nbBeams.length ? nbBeams : labReflections(crystal, quat, crystal.k_max); drawEwaldPanel(ctx, Sc, Se, refl, k0, qMaxDisp, SG_MAX, viewX, dark, mode === "nanobeam" ? precession || 0 : 0); }, [quat, Sc, Se, dark, viewX, showEwald, crystal, mode, nbBeams, qMaxDisp, k0, SG_MAX, precession]); @@ -520,7 +549,7 @@ function DiffSim() { const res = await fetch(import.meta.url); const bundle = await res.text(); const keys = ["crystal_json", "presets", "preset", "energy_ev", "k_max", "orientation", "mode", "render", "dynamical", "thickness_A", - "semiconv_mrad", "precession_deg", "n_precession", "sigma_excitation", "rotation_step_deg", "pattern_range", "field_mrad", "sg_max", "quality", "show_kikuchi", "view_from", + "semiconv_mrad", "precession_deg", "n_precession", "sigma_excitation", "rotation_step_deg", "rotation_speed_deg", "pattern_range", "field_mrad", "sg_max", "quality", "show_kikuchi", "view_from", "scaling", "power", "cmap", "marker_power", "marker_size", "vmin_pct", "vmax_pct", "show_labels", "show_cell_axes", "show_hkl", "n_cells", "polyhedra", "show_ewald", "size", "kossel_json", "status", "widget_version"]; const state: Record = {}; @@ -596,14 +625,14 @@ function DiffSim() { {/* panels: cell (with the Ewald view below it) and the pattern */} - {showEwald && Se > 0 && ( )} - @@ -622,6 +651,15 @@ function DiffSim() { setStepDeg(Math.max(0.01, Number(e.target.value) || 0.01))} inputProps={{ step: 1, min: 0.01, max: 180, style: { fontSize: 11, padding: "4px 6px", width: 42 } }} sx={tf} /> ° + + setSpin((s) => ({ ...s, y: !s.y }))}>↔ + setSpin((s) => ({ ...s, x: !s.x }))}>↕ + + + setSpinSpeed(v as number)} + sx={{ py: 0.5, "& .MuiSlider-thumb": { width: 12, height: 12 } }} /> + + {Math.round(spinSpeed || 6)}°/s zone axis {fmtIndices(zoneAxis, crystal.hexagonal)} {crystal.name} · {crystal.spacegroup || crystal.pointgroup} diff --git a/widget/js/diffsim/pattern.ts b/widget/js/diffsim/pattern.ts index 3bd01980f..d70c94eee 100644 --- a/widget/js/diffsim/pattern.ts +++ b/widget/js/diffsim/pattern.ts @@ -380,8 +380,8 @@ export function drawKosselLines( // line p . n = distance, direction t = (-ny, nx); drawn between two lab points const [x0, y0] = toPx(f, nx * l.distance - ny * L, ny * l.distance + nx * L); const [x1, y1] = toPx(f, nx * l.distance + ny * L, ny * l.distance - nx * L); - ctx.strokeStyle = dark ? `rgba(20,20,20,${0.25 + 0.75 * l.strength})` : `rgba(30,30,30,${0.2 + 0.8 * l.strength})`; - ctx.lineWidth = Math.max(1, l.width * s); + ctx.strokeStyle = dark ? `rgba(20,20,20,${Math.min(1, 0.1 + 0.9 * l.strength)})` : `rgba(30,30,30,${Math.min(1, 0.1 + 0.9 * l.strength)})`; + ctx.lineWidth = Math.max(0.6, l.width * s); ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); } if (labels) { @@ -401,22 +401,33 @@ export function drawKosselLines( drawScaleBar(ctx, f, s, "rad", dark, 0.01); } -/** Kikuchi line pairs overlaid on a nanobeam pattern (deficient dark, excess bright). */ +/** + * Kikuchi line pairs overlaid on a nanobeam pattern (deficient blue, excess + * red). Opacity and width grow with the cube of the strength relative to the + * strongest line of the cell, so only the few major lines read as heavy and + * the rest fade into the background; at most the MAX_KIKUCHI strongest pairs + * are drawn, weakest first. + */ +const MAX_KIKUCHI = 40; export function drawKikuchiOverlay(ctx: CanvasRenderingContext2D, f: Frame, lines: KosselLine[], k0: number, dark: boolean) { const L = 3 * f.qMax; - for (const l of lines) { - if (l.strength < 0.25) continue; + const shown = lines.filter((l) => l.strength > 0).sort((a, b) => b.strength - a.strength).slice(0, MAX_KIKUCHI).reverse(); + const sMax = shown.length ? shown[shown.length - 1].strength : 1; + for (const l of shown) { const d = l.distance * k0; // 1/A const [nx, ny] = l.normal; + const r3 = (l.strength / sMax) ** 3; + const alpha = (0.05 + 0.5 * r3).toFixed(3); + const width = 0.5 + 1.3 * r3; const draw = (dist: number, color: string) => { const [x0, y0] = toPx(f, nx * dist - ny * L, ny * dist + nx * L); const [x1, y1] = toPx(f, nx * dist + ny * L, ny * dist - nx * L); ctx.strokeStyle = color; - ctx.lineWidth = 1; + ctx.lineWidth = width; ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); }; - draw(d, dark ? `rgba(120,170,255,${0.3 + 0.5 * l.strength})` : `rgba(30,90,200,${0.3 + 0.5 * l.strength})`); - draw(d + l.gxy, dark ? `rgba(255,140,120,${0.3 + 0.5 * l.strength})` : `rgba(200,60,40,${0.3 + 0.5 * l.strength})`); + draw(d, dark ? `rgba(120,170,255,${alpha})` : `rgba(30,90,200,${alpha})`); + draw(d + l.gxy, dark ? `rgba(255,140,120,${alpha})` : `rgba(200,60,40,${alpha})`); } } diff --git a/widget/js/diffsim/physics.ts b/widget/js/diffsim/physics.ts index 1745202c4..3ec544202 100644 --- a/widget/js/diffsim/physics.ts +++ b/widget/js/diffsim/physics.ts @@ -407,13 +407,19 @@ export interface KosselLine { * g_xy . theta = g_z - lambda |g|^2 / 2, a straight line at small angles. * The width is the two-beam rocking width |U_g| / (k0 |g_xy|). */ +/** Coupling of a strong reflection (Si 111 at 200 keV is 0.05 1/A^2): line darkness is measured against it. */ +export const U_REF_LINES = 0.04; +const MAX_LINES = 200; // strongest lines kept: enough for the rosettes of small cells, readable for large ones + export function kosselLines(c: CrystalData, q: Quat, kMax: number, fieldRad: number): KosselLine[] { const R = quatToMatrix(q); const lam = c.wavelength; const k0 = 1 / lam; const lines: KosselLine[] = []; - let uMax = 1e-12; - for (let i = 0; i < c.hkl.length; i++) uMax = Math.max(uMax, Math.hypot(c.U_re[i], c.U_im[i])); + // strength is ABSOLUTE (|U_g| / U_REF_LINES, capped at 1): a weakly + // scattering cell gives faint lines, and only the strongest MAX_LINES are + // kept so a large cell does not draw a hundred thousand of them + const uMin = 0.02 * U_REF_LINES; for (let i = 0; i < c.hkl.length; i++) { const gc: Vec3 = [c.g[3 * i], c.g[3 * i + 1], c.g[3 * i + 2]]; const gLen = Math.hypot(gc[0], gc[1], gc[2]); @@ -424,8 +430,12 @@ export function kosselLines(c: CrystalData, q: Quat, kMax: number, fieldRad: num const dist = (g[2] - (lam * gLen * gLen) / 2) / gxy; if (Math.abs(dist) > fieldRad * 1.5) continue; const u = Math.hypot(c.U_re[i], c.U_im[i]); - if (u < 1e-4 * uMax) continue; - lines.push({ hkl: c.hkl[i], normal: [g[0] / gxy, g[1] / gxy], distance: dist, width: u / (k0 * gxy), strength: u / uMax, gxy }); + if (u < uMin) continue; + lines.push({ hkl: c.hkl[i], normal: [g[0] / gxy, g[1] / gxy], distance: dist, width: u / (k0 * gxy), strength: Math.min(1, u / U_REF_LINES), gxy }); + } + if (lines.length > MAX_LINES) { + lines.sort((a, b) => b.strength - a.strength); + lines.length = MAX_LINES; } return lines; } diff --git a/widget/scripts/build.mjs b/widget/scripts/build.mjs index 7c8daea95..5f02583be 100644 --- a/widget/scripts/build.mjs +++ b/widget/scripts/build.mjs @@ -9,6 +9,9 @@ const watch = process.argv.includes("--watch"); const widgets = [ { name: "show2d" }, { name: "show4dstem" }, + { name: "diffsim" }, + // framework-free build of the diffraction simulator for web pages (MyST anywidget directive) + { name: "diffsim-web", entry: "js/diffsim-web/index.ts", outfile: "dist/diffraction-sim.js" }, ]; rmSync("src/quantem/widget/static", { recursive: true, force: true }); @@ -29,8 +32,8 @@ const baseOpts = { for (const w of widgets) { const opts = { ...baseOpts, - entryPoints: [`js/${w.name}/index.tsx`], - outfile: `src/quantem/widget/static/${w.name}.js`, + entryPoints: [w.entry || `js/${w.name}/index.tsx`], + outfile: w.outfile || `src/quantem/widget/static/${w.name}.js`, }; if (watch) { const ctx = await context(opts); diff --git a/widget/src/quantem/widget/diffsim.py b/widget/src/quantem/widget/diffsim.py index c7001ad91..393cf04d5 100644 --- a/widget/src/quantem/widget/diffsim.py +++ b/widget/src/quantem/widget/diffsim.py @@ -304,6 +304,7 @@ class DiffractionSim(anywidget.AnyWidget): n_precession = traitlets.Int(24).tag(sync=True) sigma_excitation = traitlets.Float(0.02).tag(sync=True) rotation_step_deg = traitlets.Float(15.0).tag(sync=True) + rotation_speed_deg = traitlets.Float(6.0).tag(sync=True) # continuous rotation buttons, deg/s pattern_range = traitlets.Float(3.0).tag(sync=True) field_mrad = traitlets.Float(50.0).tag(sync=True) sg_max = traitlets.Float(0.05).tag(sync=True) @@ -451,6 +452,7 @@ def state_dict(self) -> dict: "n_precession", "sigma_excitation", "rotation_step_deg", + "rotation_speed_deg", "pattern_range", "field_mrad", "sg_max", From 859ce33526753c6c3b3f1ea55355f0f862972c92 Mon Sep 17 00:00:00 2001 From: cophus Date: Sat, 19 Sep 2026 14:53:13 -0700 Subject: [PATCH 09/36] Many fixes --- src/quantem/diffraction/bloch.py | 22 +- src/quantem/diffraction/bragg_vectors.py | 68 ++++- src/quantem/diffraction/crystal.py | 216 ++++++++++++++-- src/quantem/diffraction/disk_detection.py | 109 +++++++- src/quantem/diffraction/illumination.py | 9 +- src/quantem/diffraction/orientation.py | 241 ++++++++++++++++-- .../diffraction/orientation_visualization.py | 57 ++++- src/quantem/diffraction/phase.py | 15 +- src/quantem/diffraction/rotations.py | 41 +++ tests/diffraction/test_crystal.py | 139 ++++++++++ tests/diffraction/test_disk_detection.py | 78 ++++++ tests/diffraction/test_orientation.py | 159 ++++++++++++ 12 files changed, 1064 insertions(+), 90 deletions(-) create mode 100644 tests/diffraction/test_disk_detection.py diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py index e2c0ecb87..89814d97f 100644 --- a/src/quantem/diffraction/bloch.py +++ b/src/quantem/diffraction/bloch.py @@ -2231,7 +2231,10 @@ def refine_dynamical( deformation is applied to the tilted cell in the Bloch calculation and the rotation folded into the orientation. mask : np.ndarray | None - (R, C) boolean; only these positions are refined. + Positions to refine: an (R, C) boolean mask or a list of + (row, col), as for OrientationMap.match_orientations. None + (default) refines every position the orientation maps reached, so + a staged test run on a few positions carries through. fast_absorption : bool, default=True First-order treatment of absorption during the search (Hermitian eigh, ~4x faster, 0.5% rms intensity error); the final evaluation @@ -2548,9 +2551,13 @@ def peaks_at(rx, ry): im = torch.as_tensor(data[:, ix[2]], dtype=torch.float64).clamp_min(0) ** power_intensity return qxy, im, im / im.max().clamp_min(1e-12) - iterator = list(np.ndindex(R, C)) - if mask is not None: - iterator = [(r, c) for r, c in iterator if mask[r, c]] + from quantem.diffraction.orientation import position_mask + + mask_rc = position_mask(mask, (R, C)) + for om in oms: + if om.computed is not None: + mask_rc = mask_rc & om.computed + iterator = [(r, c) for r, c in np.ndindex(R, C) if mask_rc[r, c]] if progress_bar: iterator = tqdm(iterator, desc="dynamical refinement") for rx, ry in iterator: @@ -3246,9 +3253,10 @@ def refine_dynamical_image( quat_out[..., 0] = 1.0 cost_out = torch.full((R, C), torch.nan, dtype=torch.float64) - iterator = list(np.ndindex(R, C)) - if mask is not None: - iterator = [(r, c) for r, c in iterator if mask[r, c]] + from quantem.diffraction.orientation import position_mask + + mask_rc = position_mask(mask, (R, C)) + iterator = [(r, c) for r, c in np.ndindex(R, C) if mask_rc[r, c]] if progress_bar: iterator = tqdm(iterator, desc="image refinement") for rx, ry in iterator: diff --git a/src/quantem/diffraction/bragg_vectors.py b/src/quantem/diffraction/bragg_vectors.py index e241e58ad..4a59c9f78 100644 --- a/src/quantem/diffraction/bragg_vectors.py +++ b/src/quantem/diffraction/bragg_vectors.py @@ -358,7 +358,12 @@ def template(self) -> np.ndarray | None: return torch.fft.fftshift(self._template).detach().cpu().numpy() def correlation_map( - self, row: int, col: int, background_sigma: float | str | None = None + self, + row: int, + col: int, + background_sigma: float | str | None = None, + corr_power: float = 1.0, + sigma_cc: float | None = None, ) -> np.ndarray: """Cross-correlation map of one diffraction pattern with the template (numpy). @@ -371,6 +376,9 @@ def correlation_map( Scan row of the diffraction pattern to correlate. col : int Scan column of the diffraction pattern to correlate. + background_sigma, corr_power, sigma_cc + Correlation options, as for :meth:`detect_disks`; pass the same + values to see the map the detection actually searches. Returns ------- @@ -383,7 +391,11 @@ def correlation_map( np.asarray(self.dataset.array[row, col]), dtype=torch.float, device=self.device ) corr, _ = cross_correlation( - dp, self._template_ft, self._resolve_background_sigma(background_sigma) + dp, + self._template_ft, + self._resolve_background_sigma(background_sigma), + corr_power, + sigma_cc, ) return corr.detach().cpu().numpy() @@ -398,6 +410,8 @@ def detect_disks( upsample_factor: int = 16, max_num_peaks: int = 1000, background_sigma: float | str | None = None, + corr_power: float = 1.0, + sigma_cc: float | None = None, batch_size: int | None = None, progressbar: bool = True, ) -> Vector: @@ -431,6 +445,26 @@ def detect_disks( Upsampling factor for the ``"upsample"`` subpixel refinement. max_num_peaks : int, default=1000 Maximum number of peaks to keep per pattern. + background_sigma : float | "auto" | None, default=None + Width in pixels of a Fourier high-pass applied to the + cross-correlation before peak finding: a copy of the correlation + map smoothed by a Gaussian of this width is subtracted from it. + The filter is isotropic in the Fourier domain and needs no origin, + so it is not a radial background fit. Its purpose is the negative + moat a zero-sum template leaves around the bright unscattered + beam, which pushes weak disk peaks below zero where the + correlation clamp erases them. Set it a little wider than a disk + so the disk-scale peaks pass untouched; "auto" uses twice the + central beam radius. + corr_power : float, default=1.0 + Correlation type: 1 is the plain cross-correlation, 0 the phase + correlation, and values in between the hybrid correlation. Lowering + it equalizes weak and strong disks, which finds many more weak + reflections on a bright background; the reported intensities are + then compressed, so check the effect before using them as weights. + sigma_cc : float | None + Gaussian smoothing of the correlation map (pixels) before peak + finding; merges the speckle of a noisy disk into one maximum. batch_size : int, optional Number of patterns per batch. ``None`` (default) picks a size from the detector dimensions. @@ -454,6 +488,8 @@ def detect_disks( upsample_factor=upsample_factor, max_num_peaks=max_num_peaks, background_sigma=self._resolve_background_sigma(background_sigma), + corr_power=corr_power, + sigma_cc=sigma_cc, ) if positions is not None: @@ -1173,6 +1209,8 @@ def show_detection( upsample_factor: int = 16, max_num_peaks: int = 1000, background_sigma: float | str | None = None, + corr_power: float = 1.0, + sigma_cc: float | None = None, image: np.ndarray | None = None, peak_radius: float = 6.0, marker_radius: float | None = None, @@ -1206,6 +1244,26 @@ def show_detection( Upsampling factor for the ``"upsample"`` subpixel refinement. max_num_peaks : int, default=1000 Maximum number of peaks to keep per pattern. + background_sigma : float | "auto" | None, default=None + Width in pixels of a Fourier high-pass applied to the + cross-correlation before peak finding: a copy of the correlation + map smoothed by a Gaussian of this width is subtracted from it. + The filter is isotropic in the Fourier domain and needs no origin, + so it is not a radial background fit. Its purpose is the negative + moat a zero-sum template leaves around the bright unscattered + beam, which pushes weak disk peaks below zero where the + correlation clamp erases them. Set it a little wider than a disk + so the disk-scale peaks pass untouched; "auto" uses twice the + central beam radius. + corr_power : float, default=1.0 + Correlation type: 1 is the plain cross-correlation, 0 the phase + correlation, and values in between the hybrid correlation. Lowering + it equalizes weak and strong disks, which finds many more weak + reflections on a bright background; the reported intensities are + then compressed, so check the effect before using them as weights. + sigma_cc : float | None + Gaussian smoothing of the correlation map (pixels) before peak + finding; merges the speckle of a noisy disk into one maximum. image : np.ndarray or Dataset2d, optional Real-space navigation image (e.g. a virtual dark-field image) shown on the left with the chosen positions marked. ``None`` (default) shows only @@ -1245,6 +1303,8 @@ def show_detection( upsample_factor=upsample_factor, max_num_peaks=max_num_peaks, background_sigma=background_sigma, + corr_power=corr_power, + sigma_cc=sigma_cc, progressbar=False, ) if image is None: @@ -1303,9 +1363,7 @@ def peak_histogram(self, *, returnfig: bool = False, **kwargs): # ---- helpers ---- - def _resolve_background_sigma( - self, background_sigma: float | str | None - ) -> float | None: + def _resolve_background_sigma(self, background_sigma: float | str | None) -> float | None: """Resolve the ``background_sigma`` argument to a value in pixels. ``"auto"`` (the default everywhere) maps to twice the central-beam diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py index a8eefd9da..044f538ea 100644 --- a/src/quantem/diffraction/crystal.py +++ b/src/quantem/diffraction/crystal.py @@ -295,8 +295,6 @@ def _setup_symmetry( """ import spglib - from quantem.diffraction.rotations import quat_to_matrix - cell = ( self.lat_real.numpy(), self.positions_frac.numpy(), @@ -319,43 +317,213 @@ def _setup_symmetry( self.pseudo_symmetry_report = {"distance_A": symprec_pseudo} if symprec_pseudo <= symprec: return + + # Two independent routes to a higher matching symmetry. + # + # "relaxed positions": the group spglib finds when every atom is + # allowed to move by symprec_pseudo, which catches a cell that is a + # slightly distorted child of a higher-symmetry parent. + # + # "lattice": the point group of the lattice alone, ignoring the + # basis. A structure whose symmetry is broken only by weakly + # scattering atoms (lithium and oxygen against a transition metal) + # or by a faint superstructure sits exactly here: no relaxation of + # the positions recovers the parent, because the atoms are already + # where they belong, but the diffraction still has the symmetry of + # the heavy sublattice. Both candidate sets are filtered by the same + # intensity test, so an operation is adopted only when it leaves the + # kinematical pattern unchanged. + candidates: list[tuple[str, np.ndarray]] = [] try: - ds_pseudo = spglib.get_symmetry_dataset(cell, symprec=symprec_pseudo) + ds_relaxed = spglib.get_symmetry_dataset(cell, symprec=symprec_pseudo) except Exception: - ds_pseudo = None - if ds_pseudo is None: - return - pg_pseudo = spglib.get_pointgroup(ds_pseudo.rotations)[0].strip() - quats_pseudo = symmetry_quaternions(ds_pseudo.rotations, self.lat_real.numpy()) - if quats_pseudo.shape[0] <= self.sym_quats.shape[0]: + ds_relaxed = None + if ds_relaxed is not None: + candidates.append(("relaxed positions", ds_relaxed.rotations)) + lattice_cell = ( + self.lat_real.numpy(), + np.zeros((1, 3)), + np.ones(1, dtype=int), + ) + try: + ds_lattice = spglib.get_symmetry_dataset(lattice_cell, symprec=symprec_pseudo) + except Exception: + ds_lattice = None + if ds_lattice is not None: + candidates.append(("lattice", ds_lattice.rotations)) + + best = None + for route, rotations in candidates: + quats = symmetry_quaternions(rotations, self.lat_real.numpy()) + if quats.shape[0] <= self.sym_quats.shape[0]: + continue + pg_cand = spglib.get_pointgroup(rotations)[0].strip() + accepted, worst = self._intensity_preserving_subgroup(quats, intensity_tol) + if self.pseudo_symmetry_report.get("candidate") is None or accepted is not None: + self.pseudo_symmetry_report.setdefault("candidate", pg_cand) + self.pseudo_symmetry_report.setdefault("intensity_mismatch", worst) + if accepted is None: + continue + if best is None or accepted.shape[0] > best[1].shape[0]: + best = (route, accepted, pg_cand, worst, quats.shape[0]) + + if best is None: + if "candidate" in self.pseudo_symmetry_report: + self.pseudo_symmetry_report["rejected"] = True return + route, accepted, pg_cand, worst, n_cand = best + self.pseudo_symmetry_report.update( + candidate=pg_cand, intensity_mismatch=worst, route=route, rejected=False + ) + self.sym_quats_matching = accepted + # name the accepted group by the candidate symbol when every one of + # its rotations survived the intensity test, otherwise by its size + if accepted.shape[0] == n_cand: + self.pointgroup_matching = pg_cand + self.laue_group_matching = _LAUE_CLASS.get(pg_cand, self.laue_group) + else: + self.pointgroup_matching = f"{accepted.shape[0]} rotations" + self.laue_group_matching = self.laue_group + + def _intensity_preserving_subgroup( + self, quats: torch.Tensor, intensity_tol: float + ) -> tuple[torch.Tensor | None, float]: + """Largest subgroup of `quats` that leaves the kinematical intensities + invariant, or None when nothing beyond the true symmetry survives. + + Every candidate operation is applied to the reflection list and the + intensity of each reflection compared with the intensity of its + image, relative to the strongest reflection. Operations that pass + are kept; the survivors are then closed under composition (dropping + the worst offender until they are), because a set of operations that + is not a group cannot be used to fold orientations. + + Returns the accepted quaternions and the worst mismatch among the + operations that were tested. + """ + from quantem.diffraction.rotations import quat_to_matrix - # intensity check on the extra operations: |F|^2 of every reflection - # against |F|^2 of its image, relative to the strongest reflection hkl, inten = self._quick_intensities() lut = {tuple(h): i for i, h in enumerate(hkl.tolist())} g = hkl.to(torch.float64) @ self.lat_recip i_max = float(inten.max()) - Rs = quat_to_matrix(quats_pseudo) + Rs = quat_to_matrix(quats) Rs_true = quat_to_matrix(self.sym_quats) + + def is_true(R): + return any(float((R - Rt).abs().max()) < 1e-6 for Rt in Rs_true) + + mismatch = torch.zeros(quats.shape[0], dtype=torch.float64) worst = 0.0 - for R in Rs: - if any(float((R - Rt).abs().max()) < 1e-6 for Rt in Rs_true): + for i, R in enumerate(Rs): + if is_true(R): continue g_img = g @ R.T hkl_img = torch.round(g_img @ self.lat_real.T).to(torch.long) idx = torch.tensor([lut.get(tuple(h), -1) for h in hkl_img.tolist()]) ok = idx >= 0 - diff = (inten[ok] - inten[idx[ok]]).abs() / i_max - worst = max(worst, float(diff.max()) if ok.any() else 0.0) - self.pseudo_symmetry_report["intensity_mismatch"] = worst - self.pseudo_symmetry_report["candidate"] = pg_pseudo - if worst > intensity_tol: - self.pseudo_symmetry_report["rejected"] = True - return - self.pointgroup_matching = pg_pseudo - self.laue_group_matching = _LAUE_CLASS.get(pg_pseudo, "-1") - self.sym_quats_matching = quats_pseudo + m = float((inten[ok] - inten[idx[ok]]).abs().max()) / i_max if bool(ok.any()) else 0.0 + mismatch[i] = m + worst = max(worst, m) + + keep = mismatch <= intensity_tol + # close under composition: a product of kept operations must also be + # kept, or the set is not a group + for _ in range(quats.shape[0]): + idx = torch.nonzero(keep).squeeze(1) + if idx.numel() <= self.sym_quats.shape[0]: + return None, worst + R_keep = Rs[idx] + prod = torch.einsum("aij,bjk->abik", R_keep, R_keep).reshape(-1, 3, 3) + d = (prod[:, None] - R_keep[None]).abs().amax(dim=(-1, -2)) + closed = bool((d.min(dim=1).values < 1e-6).all()) + if closed: + return quats[idx], worst + drop = idx[int(torch.argmax(mismatch[idx]))] + keep[drop] = False + return None, worst + + def projected_rotation_order( + self, + zone_axis, + k_max: float | None = None, + tol_zone: float = 0.02, + intensity_tol: float = 0.05, + ): + """Apparent rotational symmetry of the zero-layer pattern, per zone axis. + + A zone-layer pattern can be more symmetric about the beam than the + crystal is, and where it is, the in-plane orientation cannot be + indexed. Body-centered cubic along <111> is the standard case: the + zero-layer net of {110} reflections is hexagonal, so the pattern + repeats every 60 degrees while the crystal repeats every 120, and the + two orientations 60 degrees apart give the same peak positions and the + same kinematical intensities. Only the higher-order Laue zones or the + dynamical intensities separate them. + + Returned is the largest n in (6, 4, 3, 2, 1) for which rotating the + zero-layer reflections by 360/n about the zone axis reproduces the + set, in position and in kinematical intensity. Fold an in-plane angle + or color by 360/n to get a map that is continuous across the + ambiguity, and use `n` against the crystal's own rotational order + about the same axis to see where indexing is degenerate. + + Parameters + ---------- + zone_axis : array-like + Cartesian zone axis (3,), or a stack of them (..., 3); need not + be normalized. + k_max : float | None + Only reflections within this scattering vector are tested; + defaults to the crystal's own k_max. + tol_zone : float, default=0.02 + Half-thickness of the zero layer (1/Angstroms): reflections with + |g . zone_axis| below this count as zero layer. + intensity_tol : float, default=0.05 + A reflection and its image must agree in |F|^2 to within this + fraction of the strongest zero-layer reflection. + + Returns + ------- + int | np.ndarray + The order n, scalar for a single zone axis. + """ + if self.g_vec is None: + raise RuntimeError("Run calculate_structure_factors() first.") + axes = torch.as_tensor(np.asarray(zone_axis, dtype=float), dtype=torch.float64) + single = axes.ndim == 1 + axes = axes.reshape(-1, 3) + axes = axes / torch.linalg.norm(axes, dim=1, keepdim=True).clamp_min(1e-12) + + g = self.g_vec + inten = self.struct_factors_int.to(torch.float64) + if k_max is not None: + sel = self.g_len <= float(k_max) + g, inten = g[sel], inten[sel] + + out = np.ones(axes.shape[0], dtype=int) + eye = torch.eye(3, dtype=torch.float64) + for i, u in enumerate(axes): + zol = torch.abs(g @ u) <= tol_zone + gz, iz = g[zol], inten[zol] + if gz.shape[0] < 3: + continue + i_max = float(iz.max()).__abs__() or 1.0 + ux = torch.tensor( + [[0.0, -u[2], u[1]], [u[2], 0.0, -u[0]], [-u[1], u[0], 0.0]], + dtype=torch.float64, + ) + for n in (6, 4, 3, 2): + th = 2 * np.pi / n + R = eye + np.sin(th) * ux + (1 - np.cos(th)) * (ux @ ux) # Rodrigues + d = torch.cdist(gz @ R.T, gz) + dmin, j = d.min(dim=1) + if float(dmin.max()) > tol_zone: + continue + if float((iz - iz[j]).abs().max()) / i_max <= intensity_tol: + out[i] = n + break + return int(out[0]) if single else out def zone_axis_wedge(self) -> torch.Tensor | None: """Fundamental zone-axis wedge corners (3, 3) Cartesian, or None. diff --git a/src/quantem/diffraction/disk_detection.py b/src/quantem/diffraction/disk_detection.py index 5ec5f00f6..7459756d9 100644 --- a/src/quantem/diffraction/disk_detection.py +++ b/src/quantem/diffraction/disk_detection.py @@ -279,10 +279,68 @@ def _background_highpass( return 1.0 - g +def _smoothing_lowpass( + shape: tuple[int, int], + sigma: float, + device, + rfft: bool = False, +) -> torch.Tensor: + """Fourier-domain Gaussian low-pass of width ``sigma`` pixels. + + Smoothing the correlation map before peak finding merges the speckle of a + noisy disk into one maximum, which is what stops a weak disk from being + split into several sub-threshold peaks. + """ + H, W = int(shape[0]), int(shape[1]) + qr = torch.fft.fftfreq(H, device=device, dtype=torch.float)[:, None] + qc = ( + torch.fft.rfftfreq(W, device=device, dtype=torch.float)[None, :] + if rfft + else torch.fft.fftfreq(W, device=device, dtype=torch.float)[None, :] + ) + return torch.exp(-2.0 * (torch.pi**2) * (float(sigma) ** 2) * (qr**2 + qc**2)) + + +def _apply_corr_power(m: torch.Tensor, corr_power: float) -> torch.Tensor: + """Hybrid correlation: keep the phase, raise the magnitude to ``corr_power``. + + ``corr_power=1`` is the plain cross-correlation, ``0`` the phase + correlation, and values in between the hybrid correlation. Dividing out + part of the magnitude equalizes the weak and strong reflections, so a + faint disk on a bright background produces a peak of the same height as a + strong one; this is what makes the phase and hybrid correlations find far + more weak disks than the plain product. The cost is that the peak height + is no longer proportional to the disk intensity -- with ``corr_power`` + below 1 the reported intensities are compressed, which matters when they + are used downstream as weights (orientation matching, strain). + """ + if corr_power == 1.0: + return m + mag = torch.abs(m) + return m * mag.clamp_min(1e-12) ** (float(corr_power) - 1.0) + + +def _fourier_filter( + m: torch.Tensor, + shape: tuple[int, int], + background_sigma: float | None, + sigma_cc: float | None, + rfft: bool = False, +) -> torch.Tensor: + """Apply the background high-pass and the smoothing low-pass to a product.""" + if background_sigma is not None and background_sigma > 0: + m = m * _background_highpass(shape, background_sigma, m.device, rfft=rfft) + if sigma_cc is not None and sigma_cc > 0: + m = m * _smoothing_lowpass(shape, sigma_cc, m.device, rfft=rfft) + return m + + def cross_correlation( dp: torch.Tensor, template_ft: torch.Tensor, background_sigma: float | None = None, + corr_power: float = 1.0, + sigma_cc: float | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: """Cross-correlate a diffraction pattern with a template. @@ -303,9 +361,8 @@ def cross_correlation( subpixel refinement). """ dp = torch.as_tensor(dp) - m = torch.fft.fft2(dp) * template_ft - if background_sigma is not None and background_sigma > 0: - m = m * _background_highpass(m.shape[-2:], background_sigma, m.device) + m = _apply_corr_power(torch.fft.fft2(dp) * template_ft, corr_power) + m = _fourier_filter(m, m.shape[-2:], background_sigma, sigma_cc) corr_map = torch.clamp(torch.fft.ifft2(m).real, min=0.0) return corr_map, m @@ -314,6 +371,8 @@ def cross_correlation_batch( dps: torch.Tensor, template_ft: torch.Tensor, background_sigma: float | None = None, + corr_power: float = 1.0, + sigma_cc: float | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: """Cross-correlate a stack of diffraction patterns with one template. @@ -336,9 +395,8 @@ def cross_correlation_batch( ``(B, H, W)`` Fourier-domain products. """ dps = torch.as_tensor(dps) - m = torch.fft.fft2(dps) * template_ft - if background_sigma is not None and background_sigma > 0: - m = m * _background_highpass(m.shape[-2:], background_sigma, m.device) + m = _apply_corr_power(torch.fft.fft2(dps) * template_ft, corr_power) + m = _fourier_filter(m, m.shape[-2:], background_sigma, sigma_cc) corr_map = torch.clamp(torch.fft.ifft2(m).real, min=0.0) return corr_map, m @@ -347,6 +405,8 @@ def _corr_map_rfft( dps: torch.Tensor, template_ft: torch.Tensor, background_sigma: float | None = None, + corr_power: float = 1.0, + sigma_cc: float | None = None, ) -> torch.Tensor: """Real-FFT correlation map(s), used when no Fourier product is needed downstream. @@ -369,9 +429,8 @@ def _corr_map_rfft( """ dps = torch.as_tensor(dps) H, W = dps.shape[-2], dps.shape[-1] - prod = torch.fft.rfft2(dps) * template_ft[..., : W // 2 + 1] - if background_sigma is not None and background_sigma > 0: - prod = prod * _background_highpass((H, W), background_sigma, prod.device, rfft=True) + prod = _apply_corr_power(torch.fft.rfft2(dps) * template_ft[..., : W // 2 + 1], corr_power) + prod = _fourier_filter(prod, (H, W), background_sigma, sigma_cc, rfft=True) corr_map = torch.fft.irfft2(prod, s=(H, W)) return torch.clamp(corr_map, min=0.0) @@ -387,6 +446,8 @@ def detect_disks( upsample_factor: int = 16, max_num_peaks: int = 1000, background_sigma: float | None = None, + corr_power: float = 1.0, + sigma_cc: float | None = None, ) -> np.ndarray: """Detect Bragg disks in one diffraction pattern by template matching. @@ -411,6 +472,18 @@ def detect_disks( Upsampling factor for the ``"upsample"`` subpixel refinement. max_num_peaks : int, default=1000 Maximum number of peaks to keep (after intensity sorting). + background_sigma : float | None + Width in pixels of the smoothed correlation background subtracted + before peak finding. + corr_power : float, default=1.0 + Correlation type: 1 the plain cross-correlation, 0 the phase + correlation, in between the hybrid correlation. Below 1 the weak disks + are amplified relative to the strong ones, which finds many more of + them on a bright background, at the cost of compressing the reported + intensities. + sigma_cc : float | None + Width in pixels of a Gaussian smoothing of the correlation map before + peak finding; merges the speckle of a noisy disk into one maximum. Returns ------- @@ -421,7 +494,7 @@ def detect_disks( if subpixel not in SUBPIXEL_MODES: raise ValueError(f"subpixel must be in {SUBPIXEL_MODES}, got {subpixel!r}") - corr_map, m = cross_correlation(dp, template_ft, background_sigma) + corr_map, m = cross_correlation(dp, template_ft, background_sigma, corr_power, sigma_cc) peaks = _local_maxima(corr_map, edge_boundary) peaks = _filter_maxima(peaks, min_abs_intensity, min_spacing, max_num_peaks) @@ -449,6 +522,8 @@ def detect_disks_batch( upsample_factor: int = 16, max_num_peaks: int = 1000, background_sigma: float | None = None, + corr_power: float = 1.0, + sigma_cc: float | None = None, ) -> list[np.ndarray]: """Detect Bragg disks across a stack of diffraction patterns (batched). @@ -482,6 +557,14 @@ def detect_disks_batch( Upsampling factor for the ``"upsample"`` subpixel refinement. max_num_peaks : int, default=1000 Maximum number of peaks to keep per pattern (after intensity sorting). + background_sigma : float | None + Width in pixels of the smoothed correlation background subtracted + before peak finding. + corr_power : float, default=1.0 + Correlation type: 1 cross-correlation, 0 phase correlation, in between + hybrid (see :func:`detect_disks`). + sigma_cc : float | None + Width in pixels of a Gaussian smoothing of the correlation map. Returns ------- @@ -493,9 +576,11 @@ def detect_disks_batch( raise ValueError(f"subpixel must be in {SUBPIXEL_MODES}, got {subpixel!r}") if subpixel == "upsample": - corr_map, m = cross_correlation_batch(dps, template_ft, background_sigma) + corr_map, m = cross_correlation_batch( + dps, template_ft, background_sigma, corr_power, sigma_cc + ) else: - corr_map = _corr_map_rfft(dps, template_ft, background_sigma) + corr_map = _corr_map_rfft(dps, template_ft, background_sigma, corr_power, sigma_cc) m = None peaks_all, bidx, counts = _detect_peaks_batched( diff --git a/src/quantem/diffraction/illumination.py b/src/quantem/diffraction/illumination.py index af6fa9d5b..e441541ce 100644 --- a/src/quantem/diffraction/illumination.py +++ b/src/quantem/diffraction/illumination.py @@ -227,9 +227,14 @@ def gaussian_envelope_ring_torch(c: torch.Tensor, a: torch.Tensor, sigma: float) v_safe = torch.where(smallv, torch.ones_like(v), v) i2v = torch.where(smallv, v * v / 8, i0v - 2 * i1v / v_safe) out = torch.exp(-0.5 * (c / sigma) ** 2 - v) * (i0u * i0v - 2 * i2u * i1v + 2 * i4u * i2v) - big = v > 0.3 + # v follows the shape of a, which may be narrower than the output: the + # fallback mask has to be taken in the broadcast shape or it indexes the + # wrong elements (a narrow sigma or a large sweep reaches this branch) + big = torch.broadcast_to(v > 0.3, out.shape) if bool(big.any()): out = out.clone() - ref = gaussian_envelope_ring_series(c[big], torch.broadcast_to(a, c.shape)[big], sigma) + c_b = torch.broadcast_to(c, out.shape) + a_b = torch.broadcast_to(a, out.shape) + ref = gaussian_envelope_ring_series(c_b[big], a_b[big], sigma) out[big] = ref.to(out.dtype) return out.clamp(0.0, 1.0) diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index f0738208a..f90b2f211 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -53,6 +53,33 @@ ) +def position_mask(positions, shape: tuple[int, int]) -> torch.Tensor: + """Normalize a `positions` argument into an (R, C) boolean mask. + + Accepts None (every position), a list of (row, col) scan positions, or + an (R, C) boolean array. Used by the staged workflow: run matching or + refinement on a handful of positions, look at the fits, then run the + whole scan with the same arguments. + """ + R, C = shape + if positions is None: + return torch.ones((R, C), dtype=torch.bool) + arr = np.asarray(positions) + if arr.dtype == bool: + if arr.shape != (R, C): + raise ValueError(f"boolean positions mask must have shape {(R, C)}, got {arr.shape}") + return torch.as_tensor(arr, dtype=torch.bool) + arr = np.atleast_2d(arr) + if arr.ndim != 2 or arr.shape[1] != 2: + raise ValueError("positions must be None, an (R, C) boolean mask, or a list of (row, col)") + mask = torch.zeros((R, C), dtype=torch.bool) + for r, c in arr.astype(int): + if not (0 <= r < R and 0 <= c < C): + raise ValueError(f"position ({r}, {c}) is outside the scan {(R, C)}") + mask[r, c] = True + return mask + + def fibonacci_hemisphere(n_points: int, dtype=torch.float64) -> torch.Tensor: """Spherical Fibonacci sampling of the upper hemisphere, (N, 3).""" i = torch.arange(n_points, dtype=dtype) + 0.5 @@ -180,6 +207,8 @@ def __init__( self.corr_second: torch.Tensor | None = None self.reliability: torch.Tensor | None = None self.mirror: torch.Tensor | None = None + # positions carrying a result; a subset after a staged test run + self.computed: torch.Tensor | None = None @classmethod def from_vectors( @@ -221,10 +250,14 @@ def build_plan( self, angle_step_zone_axis_deg: float = 1.0, angle_step_in_plane_deg: float = 5.0, + zone_axis_range="auto", + fiber_axis=None, + fiber_angle_deg: float = 0.0, corr_kernel_size: float = PAIR_DISTANCE, sigma_excitation: float = SIGMA_EXCITATION, power_radial: float = 1.0, power_intensity: float = POWER_INTENSITY, + power_intensity_experiment: float | None = None, tol_shell_distance: float = 0.01, detector_q_max: float | tuple[float, float] | str | None = "auto", device: str | torch.device = "cpu", @@ -245,6 +278,34 @@ def build_plan( continuously (parabolic sub-bin interpolation, then least squares on the paired peaks in refine_orientations), so a coarse step costs little accuracy and keeps the library small. + zone_axis_range : {"auto", "full", "fiber"} | array-like, default="auto" + Which zone axes the library covers. + + - "auto": the fundamental wedge of the matching point group + (the pseudo-symmetry group when one was detected), falling + back to the hemisphere for triclinic and monoclinic cells. + This is the right choice for an unknown texture. + - "full": the whole hemisphere, whatever the symmetry. Use when + the symmetry the cell reports is not the symmetry of its + diffraction, so the wedge would fold distinct orientations + onto each other. + - "fiber": a cap of half angle `fiber_angle_deg` about + `fiber_axis`, for a known texture (a 2D material or a + textured film). `fiber_angle_deg=0` samples the fiber axis + alone, so the match is over the in-plane angle only, which + makes the library tiny and the match far more robust. + - an array of 2 or 3 lattice directions [uvw] (or [uvtw] for a + hexagonal cell): the spherical triangle they span. With two + rows the wedge runs from [001] through both. + + The in-plane angle is always searched over the full 360 degrees; + the correlation is circular in it, so restricting it saves + nothing. + fiber_axis : array-like | None + Lattice direction [uvw] (or [uvtw]) of the fiber axis, required + by zone_axis_range="fiber". + fiber_angle_deg : float, default=0.0 + Half angle of the fiber cap, degrees. corr_kernel_size : float, default=0.05 Correlation kernel size delta (1/Angstroms): azimuthal extent of each reference peak and radial tolerance for shell assignment. @@ -261,6 +322,11 @@ def build_plan( Weighting prefactor q^power_radial * |V_g|^power_intensity for library peaks. power_intensity=0 matches on positions only (best for strongly dynamical data). + power_intensity_experiment : float | None + Exponent applied to the *measured* peak intensities; defaults to + `power_intensity`. Lower it than the library exponent when the + measured intensities are less trustworthy than the simulated + ones (saturation, a beam stop, strong dynamical transfer). tol_shell_distance : float, default=0.01 Reciprocal lattice radii closer than this merge into one shell. detector_q_max : float | tuple | "auto" | None, default="auto" @@ -293,17 +359,16 @@ def build_plan( self.sigma_excitation = float(sigma_excitation) self.power_radial = float(power_radial) self.power_intensity = float(power_intensity) + self.power_intensity_experiment = float( + power_intensity if power_intensity_experiment is None else power_intensity_experiment + ) - # zone axis sampling over the matching (pseudo-symmetry-reduced) wedge - msg = crystal.matching_symmetry_warning() - if msg is not None: - warnings.warn(msg, stacklevel=2) - wedge = crystal.zone_axis_wedge() - if wedge is None: - n_zones = int(np.ceil(2 * np.pi / np.deg2rad(angle_step_zone_axis_deg) ** 2)) - za = fibonacci_hemisphere(n_zones) - else: - za, _ = sample_zone_axes(wedge, angle_step_zone_axis_deg) + # zone axis sampling: the symmetry wedge, the hemisphere, a fiber + # cap, or an explicit spherical triangle of lattice directions + za = self._sample_zone_axes( + zone_axis_range, fiber_axis, fiber_angle_deg, angle_step_zone_axis_deg + ) + self.zone_axis_range = zone_axis_range self.zone_axes = za self.zone_quats = quat_from_zone_axis(za) self.zone_step_deg = float(angle_step_zone_axis_deg) @@ -414,11 +479,17 @@ def build_plan( semiconv_mrad=float(self.metadata.get("semiconv_mrad", 0.0) or 0.0), angle_step_zone_axis_deg=float(angle_step_zone_axis_deg), angle_step_in_plane_deg=float(angle_step_in_plane_deg), + zone_axis_range=zone_axis_range + if isinstance(zone_axis_range, str) + else np.asarray(zone_axis_range).tolist(), + fiber_axis=None if fiber_axis is None else np.asarray(fiber_axis).tolist(), + fiber_angle_deg=float(fiber_angle_deg), corr_kernel_size=self.corr_kernel_size, pair_distance=self.corr_kernel_size, sigma_excitation=self.sigma_excitation, power_radial=self.power_radial, power_intensity=self.power_intensity, + power_intensity_experiment=self.power_intensity_experiment, tol_shell_distance=float(tol_shell_distance), detector_q_max=None if detector_q_max is None @@ -437,6 +508,61 @@ def build_plan( ) return self + def _sample_zone_axes( + self, + zone_axis_range, + fiber_axis, + fiber_angle_deg: float, + step_deg: float, + ) -> torch.Tensor: + """Zone-axis sampling requested by build_plan, (Z, 3) Cartesian.""" + from quantem.diffraction.rotations import sample_zone_axis_cap + + crystal = self.crystal + + def cartesian(uvw) -> torch.Tensor: + v = np.asarray(uvw, dtype=float).reshape(-1) + if v.shape[0] == 4: # Miller-Bravais [uvtw] + from quantem.diffraction.crystal import miller_bravais_to_miller + + v = np.asarray(miller_bravais_to_miller(v)).reshape(-1) + if v.shape[0] != 3: + raise ValueError("a direction must have 3 indices [uvw] or 4 [uvtw]") + d = torch.as_tensor(v, dtype=torch.float64) @ crystal.lat_real + return d / torch.linalg.norm(d).clamp_min(1e-12) + + if isinstance(zone_axis_range, str): + mode = zone_axis_range.lower() + if mode == "fiber": + if fiber_axis is None: + raise ValueError('zone_axis_range="fiber" needs a fiber_axis') + return sample_zone_axis_cap(cartesian(fiber_axis), fiber_angle_deg, step_deg) + if mode in ("full", "hemisphere"): + n_zones = int(np.ceil(2 * np.pi / np.deg2rad(step_deg) ** 2)) + return fibonacci_hemisphere(n_zones) + if mode != "auto": + raise ValueError( + 'zone_axis_range must be "auto", "full", "fiber", or an array of directions' + ) + msg = crystal.matching_symmetry_warning() + if msg is not None: + warnings.warn(msg, stacklevel=3) + wedge = crystal.zone_axis_wedge() + if wedge is None: # triclinic / monoclinic: not a spherical triangle + n_zones = int(np.ceil(2 * np.pi / np.deg2rad(step_deg) ** 2)) + return fibonacci_hemisphere(n_zones) + za, _ = sample_zone_axes(wedge, step_deg) + return za + + rows = np.atleast_2d(np.asarray(zone_axis_range, dtype=float)) + dirs = [cartesian(r) for r in rows] + if len(dirs) == 2: + dirs = [cartesian([0, 0, 1]), *dirs] + if len(dirs) != 3: + raise ValueError("zone_axis_range as an array needs 2 or 3 directions") + za, _ = sample_zone_axes(torch.stack(dirs), step_deg) + return za + def _deposit_polar( self, qr: torch.Tensor, @@ -552,7 +678,7 @@ def _polar_image( """Sparse polar image (S, G) of one measured pattern.""" qr = torch.hypot(qx, qy) qphi = torch.atan2(qy, qx) - amp = intensity.clamp_min(0) ** (self.power_intensity) * qr**self.power_radial + amp = intensity.clamp_min(0) ** self.power_intensity_experiment * qr**self.power_radial out = torch.zeros((self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64) return self._deposit_polar(qr, qphi, amp, out) @@ -568,7 +694,7 @@ def _polar_images(self, arrays: list[np.ndarray], ix: list[int]) -> torch.Tensor intensity = torch.as_tensor(data[:, 2], dtype=torch.float64) qr = torch.hypot(qx, qy) qphi = torch.atan2(qy, qx) - amp = intensity.clamp_min(0) ** (self.power_intensity) * qr**self.power_radial + amp = intensity.clamp_min(0) ** self.power_intensity_experiment * qr**self.power_radial out = torch.zeros( (len(arrays), self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64 ) @@ -581,6 +707,7 @@ def _polar_images(self, arrays: list[np.ndarray], ix: list[int]) -> torch.Tensor def match_orientations( self, num_matches: int = 1, + positions=None, include_mirror: bool = True, min_number_peaks: int | None = None, min_angle_between_matches_deg: float = 15.0, @@ -590,7 +717,7 @@ def match_orientations( batch_size: int = 128, progress_bar: bool = True, ) -> "OrientationMap": - """Match all probe positions against the orientation plan. + """Match probe positions against the orientation plan. Patterns are processed in batches: the polar images are stacked, and the correlation over all zones and in-plane angles reduces to one @@ -610,6 +737,13 @@ def match_orientations( Number of orientations to return per probe position; matches after the first suppress zones within `min_angle_between_matches_deg` of earlier matches. + positions : list[tuple[int, int]] | np.ndarray | None + Scan positions to match: a list of (row, col), or an (R, C) + boolean mask. None (default) matches the whole scan. Pass a + handful of positions to check the plan and these parameters + with `plot_pattern_matches` before committing to the full + scan; the positions carrying a result are recorded in + `computed`, which the refinements and the phase fit follow. include_mirror : bool, default=True Also correlate against the in-plane mirrored pattern, testing inversion-related (opposite hemisphere) zone axes at no library @@ -634,6 +768,7 @@ def match_orientations( min_number_peaks = resolve(min_number_peaks, "min_number_peaks", default=MIN_NUMBER_PEAKS) self.metadata["match"] = dict( num_matches=int(num_matches), + positions=None if positions is None else "subset", include_mirror=bool(include_mirror), min_number_peaks=int(min_number_peaks), min_angle_between_matches_deg=float(min_angle_between_matches_deg), @@ -670,11 +805,21 @@ def match_orientations( ) # (Z, Z) plan_fft = self.plan_fft # (Z, S, G) complex + wanted = position_mask(positions, (R, C)) valid_rc = [ (rx, ry) for rx, ry in np.ndindex(R, C) - if peaks[rx, ry].array.shape[0] >= min_number_peaks + if wanted[rx, ry] and peaks[rx, ry].array.shape[0] >= min_number_peaks ] + if not valid_rc: + raise RuntimeError( + "no requested scan position has at least min_number_peaks = %d detected peaks" + % min_number_peaks + ) + computed = torch.zeros((R, C), dtype=torch.bool) + for rx, ry in valid_rc: + computed[rx, ry] = True + self.computed = computed batches = [valid_rc[i : i + batch_size] for i in range(0, len(valid_rc), batch_size)] if progress_bar: batches = tqdm(batches, desc=f"matching {self.crystal.name}") @@ -825,6 +970,7 @@ def match_orientations( def refine_orientations( self, num_iterations: int = 5, + positions=None, pair_distance: float | None = None, sigma_excitation: float | None = None, min_pairs: int | None = None, @@ -861,6 +1007,11 @@ def refine_orientations( ---------- num_iterations : int, default=5 Pairing + rotation solve rounds. + positions : list[tuple[int, int]] | np.ndarray | None + Scan positions to refine, as for `match_orientations`. None + (default) refines every position that carries a match, so a + staged test run on a few positions is refined without repeating + the position list. pair_distance : float | None Maximum pairing distance (1/Angstroms); defaults to the plan's corr_kernel_size. @@ -913,6 +1064,7 @@ def refine_orientations( min_pairs = resolve(min_pairs, "min_pairs", default=MIN_PAIRS) self.metadata["refine"] = dict( num_iterations=int(num_iterations), + positions=None if positions is None else "subset", pair_distance=float(delta), sigma_excitation=float(sigma), min_pairs=int(min_pairs), @@ -1094,9 +1246,14 @@ def get_exp(rx, ry): return q_exp, w_exp scores = torch.zeros((R, C), dtype=torch.float64) + # positions to refine: those requested, or everything matched + active = position_mask(positions, (R, C)) + if self.computed is not None: + active = active & self.computed if batched and not refine_tilt: self._refine_batched( scores, + active=active, delta=delta, sigma=sigma, sigma_env=sigma_env, @@ -1109,7 +1266,7 @@ def get_exp(rx, ry): progress_bar=progress_bar, ) else: - iterator = list(np.ndindex(R, C)) + iterator = [(rx, ry) for rx, ry in np.ndindex(R, C) if active[rx, ry]] if progress_bar: iterator = tqdm(iterator, desc="refining orientations") for rx, ry in iterator: @@ -1138,7 +1295,7 @@ def get_exp(rx, ry): ).reshape(R - dr, C - dc) miso_min[: R - dr, : C - dc] = torch.minimum(miso_min[: R - dr, : C - dc], mm) miso_min[dr:, dc:] = torch.minimum(miso_min[dr:, dc:], mm) - retry = torch.nonzero(miso_min > rescue_threshold_deg) + retry = torch.nonzero((miso_min > rescue_threshold_deg) & active) it2 = retry.tolist() if progress_bar and len(it2): it2 = tqdm(it2, desc="neighbor rescue") @@ -1155,6 +1312,8 @@ def get_exp(rx, ry): nr, nc = rx + dr, ry + dc if (dr == 0 and dc == 0) or not (0 <= nr < R and 0 <= nc < C): continue + if self.corr[nr, nc, 0] <= 0: + continue # neighbour carries no match (never run, or skipped) qn = self.quats[nr, nc, 0] if all( float(misorientation_angle_deg(qn, c, self.crystal.sym_quats)) > 0.5 @@ -1178,6 +1337,7 @@ def get_exp(rx, ry): def _refine_batched( self, scores: torch.Tensor, + active: torch.Tensor, delta: float, sigma: float, sigma_env: float, @@ -1190,7 +1350,7 @@ def _refine_batched( chunk: int = 64, power_env: float = POWER_INTENSITY, ) -> None: - """Chunk-vectorized in-plane + envelope refinement (all positions).""" + """Chunk-vectorized in-plane + envelope refinement of the active positions.""" from quantem.diffraction.rotations import quat_to_matrix peaks = self.peaks @@ -1243,9 +1403,9 @@ def envelope(S, g_rows): quats = self.quats.reshape(N, M, 4) corr = self.corr.reshape(N, M) - valid_pos = torch.as_tensor(counts >= min_pairs) + valid_pos = torch.as_tensor(counts >= min_pairs) & active.reshape(N) - chunks = range(0, N, chunk) + chunks = [i for i in range(0, N, chunk) if bool(valid_pos[i : i + chunk].any())] if progress_bar: chunks = tqdm(chunks, desc="refining orientations (batched)") for i0 in chunks: @@ -1665,13 +1825,24 @@ def calculate_strain( sm.num_pairs = num_pairs return sm - def in_plane_angle_deg(self, match: int = 0, mod_deg: float | None = None) -> torch.Tensor: + def in_plane_angle_deg( + self, match: int = 0, mod_deg: float | str | None = "auto" + ) -> torch.Tensor: """In-plane angle of the crystal a-axis at every position (degrees). The angle of the projected crystal [100] Cartesian axis, measured - from the scan column axis toward the row axis. `mod_deg` wraps the - angle by the crystal's in-plane symmetry (60 for hexagonal basal, 90 - for cubic <100> zones); None returns the full range. + from the scan column axis toward the row axis. + + `mod_deg` wraps the angle, which is what makes the map continuous + where the in-plane orientation is not uniquely indexable. The default + "auto" wraps each position by 360 / n with n the apparent rotational + symmetry of its own zero-layer pattern + (`Crystal.projected_rotation_order`), so the map folds by exactly the + ambiguity the data carry and no more. For a body-centered cubic + crystal near <111> that is 60 degrees, where the crystal itself + repeats only every 120, and folding removes the 60 degree jumps + between two variants no zero-layer pattern can separate. A float + wraps everywhere by that value, and None returns the full range. """ from quantem.diffraction.rotations import quat_to_matrix @@ -1679,21 +1850,41 @@ def in_plane_angle_deg(self, match: int = 0, mod_deg: float | None = None) -> to R = quat_to_matrix(self.quats[..., match, :]) a_lab = R[..., :, 0] # crystal x-axis in the lab frame ang = torch.rad2deg(torch.atan2(a_lab[..., 0], a_lab[..., 1])) + if isinstance(mod_deg, str): + if mod_deg != "auto": + raise ValueError('mod_deg must be a number, None, or "auto"') + # beam direction in crystal coordinates, deduplicated on a coarse + # grid: the projected order is piecewise constant in the zone axis + zone_c = R[..., 2, :] + key = torch.round(zone_c.reshape(-1, 3) * 200) / 200 + uniq, inv = torch.unique(key, dim=0, return_inverse=True) + order = self.crystal.projected_rotation_order(uniq.numpy()) + n = torch.as_tensor(np.asarray(order), dtype=torch.float64)[inv] + return ang % (360.0 / n.reshape(ang.shape)) if mod_deg is not None: ang = ang % mod_deg return ang + def _default_mask(self, kwargs: dict) -> dict: + """After a staged run on a subset of positions, plot only those.""" + if kwargs.get("mask") is None and self.computed is not None: + if not bool(self.computed.all()): + kwargs["mask"] = self.computed.numpy().astype(float) + return kwargs + def plot_orientation(self, direction: str = "z", match: int = 0, **kwargs): """IPF-colored orientation map; see orientation_visualization.""" from quantem.diffraction.orientation_visualization import plot_orientation_map - return plot_orientation_map(self, direction=direction, match=match, **kwargs) + return plot_orientation_map( + self, direction=direction, match=match, **self._default_mask(kwargs) + ) def plot_pole_figure(self, pole=(0, 0, 1), match: int = 0, **kwargs): """Stereographic pole figure; see orientation_visualization.""" from quantem.diffraction.orientation_visualization import plot_pole_figure - return plot_pole_figure(self, pole=pole, match=match, **kwargs) + return plot_pole_figure(self, pole=pole, match=match, **self._default_mask(kwargs)) def misorientation_map(self, reference: torch.Tensor | None = None) -> torch.Tensor: """Misorientation angle (deg) of match 0 to a reference orientation.""" diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py index 6edf3bdeb..b65da4c98 100644 --- a/src/quantem/diffraction/orientation_visualization.py +++ b/src/quantem/diffraction/orientation_visualization.py @@ -333,6 +333,8 @@ def plot_pattern_matches( scalebar: bool = True, show_measured: bool = True, marker_scale: float = 250.0, + marker: str | None = None, + transpose: bool = False, axsize: tuple[float, float] = (3.1, 3.1), ): """Candidate matches side by side, py4DSTEM style. @@ -362,6 +364,14 @@ def plot_pattern_matches( Match indices per crystal. colors : list | None One color per crystal; defaults to red, blue, green, purple. + marker : str | None + Matplotlib marker for the simulated peaks. The default is an open + circle over a diffraction pattern, which leaves the measured disk + visible inside it, and a plus over the gray measured peaks. + transpose : bool, default=False + By default rows are probe positions and columns are candidates. + True swaps them, giving one row per candidate across the positions, + which fits a few candidates and many positions on a page. """ import matplotlib.pyplot as plt @@ -382,13 +392,17 @@ def plot_pattern_matches( rot_back = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]]) panels = [(i, m) for i in range(len(oms)) for m in matches] - n_r, n_c = len(positions), len(panels) + n_pos, n_pan = len(positions), len(panels) + n_r, n_c = (n_pan, n_pos) if transpose else (n_pos, n_pan) fig, axs = plt.subplots( n_r, n_c, figsize=(axsize[0] * n_c, axsize[1] * n_r + 0.2), squeeze=False, ) + over_image = dataset is not None and pixel_size is not None + if marker is None: + marker = "o" if over_image else "+" ordinal = ["1st", "2nd", "3rd"] + [f"{k + 1}th" for k in range(3, 9)] for pi, (rx, ry) in enumerate(positions): data = peaks[rx, ry].array.copy() @@ -407,8 +421,8 @@ def plot_pattern_matches( q_lim = q_max_plot for ci, (i_om, m) in enumerate(panels): om = oms[i_om] - ax = axs[pi, ci] - if dataset is not None and pixel_size is not None: + ax = axs[ci, pi] if transpose else axs[pi, ci] + if over_image: H, W = dataset.shape[-2], dataset.shape[-1] if origins is not None: o_r, o_c = origins[rx, ry] @@ -438,24 +452,41 @@ def plot_pattern_matches( inten = sim["intensity"].numpy() sim_rc = np.stack([sim["qx"].numpy(), sim["qy"].numpy()], axis=1) @ rot_back.T if inten.size: - ax.scatter( - sim_rc[:, 1], - sim_rc[:, 0], - s=marker_scale * inten / inten.max(), - marker="+", - color=colors[i_om % len(colors)], - lw=1.8, - ) + size = marker_scale * inten / inten.max() + color = colors[i_om % len(colors)] + if marker == "o": + # open circles leave the measured disk visible inside + ax.scatter( + sim_rc[:, 1], + sim_rc[:, 0], + s=size, + marker="o", + facecolors="none", + edgecolors=color, + lw=1.4, + ) + else: + ax.scatter( + sim_rc[:, 1], + sim_rc[:, 0], + s=size, + marker=marker, + color=color, + lw=1.8, + ) ax.set_xlim(-q_lim, q_lim) ax.set_ylim(q_lim, -q_lim) ax.set_xticks([]) ax.set_yticks([]) ax.set_aspect("equal") ax.set_title( - "%s %s\ncorr = %.2f" % (om.crystal.name, ordinal[m], float(om.corr[rx, ry, m])), + "%s %s\n(%d, %d) corr = %.2f" + % (om.crystal.name, ordinal[m], rx, ry, float(om.corr[rx, ry, m])), fontsize=9, ) - if scalebar and pi == n_r - 1 and ci == 0: + last_row = (ci == n_r - 1) if transpose else (pi == n_r - 1) + first_col = (pi == 0) if transpose else (ci == 0) + if scalebar and last_row and first_col: add_scalebar_to_ax( ax, array_size=2 * q_lim, diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py index 604a29fe7..ea07ab2fb 100644 --- a/src/quantem/diffraction/phase.py +++ b/src/quantem/diffraction/phase.py @@ -34,7 +34,7 @@ POWER_INTENSITY, resolve, ) -from quantem.diffraction.orientation import OrientationMap +from quantem.diffraction.orientation import OrientationMap, position_mask class PhaseMap(AutoSerialize): @@ -86,6 +86,7 @@ def from_orientation_maps(cls, orientation_maps: list[OrientationMap]) -> "Phase def fit( self, + positions=None, pair_distance: float | None = None, power_intensity: float | None = None, max_patterns: int = 2, @@ -106,6 +107,11 @@ def fit( Parameters ---------- + positions : list[tuple[int, int]] | np.ndarray | None + Scan positions to fit: a list of (row, col) or an (R, C) + boolean mask. None (default) fits every position matched by + all of the orientation maps, so a staged test run on a few + positions carries through without repeating the list. pair_distance : float | None Pairing distance delta (1/Angstroms) between simulated and measured peaks; inherits the plan's correlation kernel. @@ -151,6 +157,7 @@ def fit( min_number_peaks, "min_number_peaks", match_md, default=MIN_NUMBER_PEAKS ) self.metadata["fit"] = dict( + positions=None if positions is None else "subset", pair_distance=float(pair_distance), power_intensity=float(power_intensity), max_patterns=int(max_patterns), @@ -178,7 +185,11 @@ def fit( reliability = torch.zeros((R, C), dtype=torch.float64) best_subset = torch.full((R, C), -1, dtype=torch.long) - iterator = list(np.ndindex(R, C)) + active = position_mask(positions, (R, C)) + for om in oms: + if om.computed is not None: + active = active & om.computed + iterator = [(rx, ry) for rx, ry in np.ndindex(R, C) if active[rx, ry]] if progress_bar: iterator = tqdm(iterator, desc="phase mapping") for rx, ry in iterator: diff --git a/src/quantem/diffraction/rotations.py b/src/quantem/diffraction/rotations.py index 2e0e6d841..08c396227 100644 --- a/src/quantem/diffraction/rotations.py +++ b/src/quantem/diffraction/rotations.py @@ -350,6 +350,47 @@ def _closest(cands: torch.Tensor, prefer: torch.Tensor) -> torch.Tensor: return signed[int(torch.argmax(key))] +def sample_zone_axis_cap( + axis: torch.Tensor, + half_angle_deg: float, + step_deg: float, +) -> torch.Tensor: + """Near-uniform sampling of a spherical cap of directions, (N, 3). + + The fiber-texture case: the zone axis is known to lie within + `half_angle_deg` of `axis` (a fiber axis normal to a 2D material, or a + textured film), and only that cap needs a library. A half angle of zero + returns the axis itself, so the match is over the in-plane angle alone. + + Points are placed on a Fibonacci spiral restricted to the cap, which + gives an equal-area covering; the count follows the cap area divided by + `step_deg` squared. + """ + axis = torch.as_tensor(axis, dtype=torch.float64) + axis = axis / torch.linalg.norm(axis).clamp_min(1e-12) + half = np.deg2rad(float(half_angle_deg)) + if half <= 0: + return axis[None, :] + step = np.deg2rad(float(step_deg)) + n = max(1, int(np.ceil(2 * np.pi * (1 - np.cos(half)) / step**2))) + i = torch.arange(n, dtype=torch.float64) + 0.5 + z = 1.0 - (1.0 - np.cos(half)) * i / n + r = torch.sqrt((1 - z**2).clamp_min(0)) + phi = i * (np.pi * (3 - np.sqrt(5))) + pts = torch.stack([r * torch.cos(phi), r * torch.sin(phi), z], dim=1) + # rotate the +z pole onto `axis` + zhat = torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64) + v = torch.linalg.cross(zhat, axis) + c = float(torch.dot(zhat, axis)) + if float(torch.linalg.norm(v)) < 1e-12: + return pts if c > 0 else -pts + vx = torch.tensor( + [[0.0, -v[2], v[1]], [v[2], 0.0, -v[0]], [-v[1], v[0], 0.0]], dtype=torch.float64 + ) + R = torch.eye(3, dtype=torch.float64) + vx + vx @ vx / (1 + c) + return pts @ R.T + + def fundamental_zone_axis_wedge(sym_quats: torch.Tensor) -> torch.Tensor | None: """Fundamental zone-axis wedge of a Laue group from its proper rotations. diff --git a/tests/diffraction/test_crystal.py b/tests/diffraction/test_crystal.py index a6183b8e4..aa78932d8 100644 --- a/tests/diffraction/test_crystal.py +++ b/tests/diffraction/test_crystal.py @@ -207,3 +207,142 @@ def test_pseudo_symmetry_dimensionless_and_intensity_check(): assert strict.pseudo_symmetry_report.get("rejected") is True assert strict.pointgroup_matching == strict.pointgroup assert "rejected" in strict.symmetry_summary() + + +def _l10(other: str, a: float = 3.58) -> Atoms: + """Two species ordered in alternating (001) layers of an fcc lattice. + + The lattice stays cubic and every atom sits exactly on its site, so no + relaxation of the positions recovers the cubic parent: only the + diffracted intensities can say whether the ordering is visible. + """ + at = Atoms( + "Ni4", + scaled_positions=[[0, 0, 0], [0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5]], + cell=[a, a, a], + pbc=True, + ) + at.symbols = ["Ni", "Ni", other, other] + return at + + +def test_pseudo_symmetry_from_weak_ordering(): + """Ordering of species that scatter alike is found through the lattice. + + Transition metals next to each other in the periodic table (the Ni, Co, + Mn of a cathode) give superlattice reflections far too weak to index, so + the orientation library must fold the variants together. The relaxed + position search cannot find this: the atoms are already where they + belong and only the species differ. + """ + weak = Crystal.from_ase(_l10("Co"), verbose=False) + assert weak.pointgroup == "4/mmm" + assert weak.pointgroup_matching == "m-3m" + assert weak.sym_quats_matching.shape[0] == 3 * weak.sym_quats.shape[0] + assert weak.pseudo_symmetry_report["route"] == "lattice" + assert weak.pseudo_symmetry_report["intensity_mismatch"] < 0.01 + + # a light partner makes the same ordering plainly visible, and the + # candidate is rejected + strong = Crystal.from_ase(_l10("Li"), verbose=False) + assert strong.pointgroup_matching == strong.pointgroup + assert strong.pseudo_symmetry_report["rejected"] is True + assert strong.pseudo_symmetry_report["intensity_mismatch"] > 0.1 + + # the intensity tolerance is the decision, and it is the user's + borderline = Crystal.from_ase(_l10("Al"), verbose=False) + assert borderline.pointgroup_matching == borderline.pointgroup + loose = Crystal.from_ase(_l10("Al"), pseudo_symmetry_intensity_tol=0.1, verbose=False) + assert loose.pointgroup_matching == "m-3m" + + +def test_true_symmetry_cells_are_unchanged(): + """The lattice route must not disturb cells that are already at their + lattice's symmetry, nor accept a lattice symmetry the structure breaks.""" + from ase.build import bulk + + for atoms, pg in ( + (bulk("Si", "diamond", a=5.43), "m-3m"), + (bulk("Ti", "hcp", a=2.95, c=4.686), "6/mmm"), + (bulk("Ti", "bcc", a=3.26, cubic=True), "m-3m"), + ): + xtl = Crystal.from_ase(atoms, verbose=False) + assert xtl.pointgroup_matching == pg + assert xtl.sym_quats_matching.shape[0] == xtl.sym_quats.shape[0] + + # corundum sits on a hexagonal lattice but its structure is only -3m; + # the lattice route proposes 6/mmm and the intensities reject it + from ase.spacegroup import crystal as ase_crystal + + al2o3 = ase_crystal( + ("Al", "O"), + basis=[(0, 0, 0.3522), (0.3064, 0, 0.25)], + spacegroup=167, + cellpar=[4.7607, 4.7607, 12.9947, 90, 90, 120], + ) + xtl = Crystal.from_ase(al2o3, verbose=False) + assert xtl.pointgroup_matching == "-3m" + assert xtl.pseudo_symmetry_report["candidate"] == "6/mmm" + assert xtl.pseudo_symmetry_report["rejected"] is True + + +def test_projected_rotation_order(): + """Apparent zero-layer symmetry, which limits in-plane indexing.""" + bcc = Crystal.from_ase( + bulk("Ti", "bcc", a=3.26, cubic=True), verbose=False + ).calculate_structure_factors(k_max=1.5) + hcp = Crystal.from_ase( + bulk("Ti", "hcp", a=2.95, c=4.686), verbose=False + ).calculate_structure_factors(k_max=1.5) + + def cartesian(xtl, uvw): + d = torch.as_tensor(np.asarray(uvw, dtype=float), dtype=torch.float64) @ xtl.lat_real + return (d / torch.linalg.norm(d)).numpy() + + # the zero-layer net of {110} along <111> is hexagonal, so the pattern + # repeats every 60 degrees while the crystal repeats every 120 + assert bcc.projected_rotation_order(cartesian(bcc, (1, 1, 1))) == 6 + assert bcc.projected_rotation_order(cartesian(bcc, (0, 0, 1))) == 4 + assert bcc.projected_rotation_order(cartesian(bcc, (0, 1, 1))) == 2 + # a general zone axis keeps the two-fold that Friedel's law provides + assert bcc.projected_rotation_order(cartesian(bcc, (1, 2, 3))) == 2 + assert hcp.projected_rotation_order(cartesian(hcp, (0, 0, 1))) == 6 + assert hcp.projected_rotation_order(cartesian(hcp, (1, 0, 0))) == 2 + + # vectorized over a stack + axes = np.stack([cartesian(bcc, u) for u in ((1, 1, 1), (0, 0, 1), (0, 1, 1))]) + assert list(bcc.projected_rotation_order(axes)) == [6, 4, 2] + + +def test_projected_order_matches_pattern_degeneracy(): + """The reported order is the rotation that leaves the pattern unchanged.""" + from quantem.diffraction.rotations import ( + qmult, + qnormalize, + quat_from_axis_angle, + quat_from_zone_axis, + ) + + bcc = Crystal.from_ase( + bulk("Ti", "bcc", a=3.26, cubic=True), verbose=False + ).calculate_structure_factors(k_max=1.5) + d = torch.tensor([1.0, 1.0, 1.0], dtype=torch.float64) @ bcc.lat_real + axis = d / torch.linalg.norm(d) + n = bcc.projected_rotation_order(axis.numpy()) + q = quat_from_zone_axis(axis) + beam = torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64) + spun = qnormalize( + qmult(quat_from_axis_angle(beam, torch.tensor(2 * np.pi / n)), q) + ) + + a = bcc.generate_pattern(q, energy_ev=200e3, sigma_excitation=0.02) + b = bcc.generate_pattern(spun, energy_ev=200e3, sigma_excitation=0.02) + pa = torch.stack([a["qx"], a["qy"]], dim=1) + pb = torch.stack([b["qx"], b["qy"]], dim=1) + assert pa.shape == pb.shape + # every peak of one pattern sits on a peak of the other, same intensity + dist = torch.cdist(pa, pb) + dmin, j = dist.min(dim=1) + assert float(dmin.max()) < 1e-6 + rel = (a["intensity"] - b["intensity"][j]).abs().max() / a["intensity"].max() + assert float(rel) < 1e-6 diff --git a/tests/diffraction/test_disk_detection.py b/tests/diffraction/test_disk_detection.py new file mode 100644 index 000000000..a9d207434 --- /dev/null +++ b/tests/diffraction/test_disk_detection.py @@ -0,0 +1,78 @@ +"""Correlation options of the Bragg disk detection.""" + +import numpy as np +import torch + +from quantem.diffraction.disk_detection import ( + detect_disks, + detect_disks_batch, + template_fourier, +) + +H = W = 64 +_YY, _XX = np.mgrid[0:H, 0:W] + + +def _disk(cy, cx, radius, amp): + return amp / (1 + np.exp((np.hypot(_YY - cy, _XX - cx) - radius) / 0.7)) + + +def _pattern(): + """Four disks spanning three decades of brightness on a bright halo.""" + planted = [(32, 32, 1000.0), (32, 44, 60.0), (20, 32, 12.0), (44, 20, 4.0)] + dp = sum(_disk(cy, cx, 3, a) for cy, cx, a in planted) + dp = dp + 30 * np.exp(-((_YY - 32) ** 2 + (_XX - 32) ** 2) / (2 * 25.0**2)) + dp = dp + np.random.default_rng(0).normal(0, 0.5, (H, W)) + template = torch.as_tensor(np.fft.ifftshift(_disk(32, 32, 3, 1.0)), dtype=torch.float) + return torch.as_tensor(dp, dtype=torch.float), template_fourier(template), planted + + +def _found(peaks, planted, tol=1.5): + """How many planted disks a peak list recovers.""" + if peaks.shape[0] == 0: + return 0 + return sum( + bool((np.hypot(peaks[:, 0] - cy, peaks[:, 1] - cx) < tol).any()) for cy, cx, _ in planted + ) + + +def test_corr_power_finds_weak_disks(): + """Hybrid correlation recovers disks the plain cross-correlation misses.""" + dp, tft, planted = _pattern() + common = dict(min_spacing=4.0, edge_boundary=2, max_num_peaks=50) + plain = detect_disks(dp, tft, corr_power=1.0, **common) + hybrid = detect_disks(dp, tft, corr_power=0.5, **common) + assert _found(plain, planted) < len(planted) + assert _found(hybrid, planted) == len(planted) + + +def test_batched_matches_single_with_correlation_options(): + """The batched path reproduces the per-pattern result for every option.""" + dp, tft, _ = _pattern() + common = dict(min_spacing=4.0, edge_boundary=2, max_num_peaks=50) + cases = [ + {}, + dict(corr_power=0.5), + dict(corr_power=0.0), + dict(sigma_cc=1.5), + dict(corr_power=0.7, sigma_cc=1.0, background_sigma=2.0), + ] + for kw in cases: + for subpixel in ("upsample", "parabolic"): + single = detect_disks(dp, tft, subpixel=subpixel, **common, **kw) + batch = detect_disks_batch( + torch.stack([dp, dp, dp]), tft, subpixel=subpixel, **common, **kw + )[1] + assert single.shape == batch.shape, (kw, subpixel) + # float32 round-off only: the half-spectrum and full-spectrum + # products differ in summation order once corr_power != 1 + assert np.allclose(single, batch, rtol=1e-4, atol=1e-5), (kw, subpixel) + + +def test_defaults_are_plain_cross_correlation(): + """corr_power=1 with no smoothing leaves the old behaviour untouched.""" + dp, tft, _ = _pattern() + common = dict(min_spacing=4.0, edge_boundary=2, max_num_peaks=50) + a = detect_disks(dp, tft, **common) + b = detect_disks(dp, tft, corr_power=1.0, sigma_cc=None, **common) + assert np.allclose(a, b) diff --git a/tests/diffraction/test_orientation.py b/tests/diffraction/test_orientation.py index 5f47e0902..d730e31ae 100644 --- a/tests/diffraction/test_orientation.py +++ b/tests/diffraction/test_orientation.py @@ -323,3 +323,162 @@ def test_roundtrip_with_precession(): assert np.median(err) < 0.3 assert (err < 1.5).mean() >= 0.75 assert om.metadata["precession_deg"] == 0.7 + + +def test_staged_positions_subset(): + """Matching a few positions leaves the rest untouched, and the later + stages follow the subset without repeating it.""" + torch.manual_seed(5) + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True)) + xtl.calculate_structure_factors(k_max=1.5) + N = 6 + q_true = qnormalize(torch.randn(N, 4, dtype=torch.float64)) + peaks = _make_peaks(xtl, q_true) + + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=2.0, angle_step_in_plane_deg=2.0, power_intensity=0.0) + + test_pos = [(0, 1), (0, 4)] + om.match_orientations(positions=test_pos, progress_bar=False) + assert om.computed.sum() == len(test_pos) + assert bool(om.computed[0, 1]) and bool(om.computed[0, 4]) + assert float(om.corr[0, 0, 0]) == 0.0 # not requested, untouched + assert float(om.corr[0, 1, 0]) > 0.5 + + # refinement follows `computed` with no position list of its own + before = om.quats.clone() + om.refine_orientations(progress_bar=False, zone_max_total_deg=1.5) + untouched = torch.allclose(before[0, 0], om.quats[0, 0]) + assert untouched + err = misorientation_angle_deg(q_true[[1, 4]], om.quats[0, [1, 4], 0], xtl.sym_quats).numpy() + assert np.all(err < 1.0) + + # the full run then covers everything + om.match_orientations(progress_bar=False) + assert bool(om.computed.all()) + assert float(om.corr[0, 0, 0]) > 0.5 + + +def test_fiber_zone_axis_range(): + """A fiber plan of zero half angle samples one zone axis and still + recovers the in-plane angle exactly.""" + torch.manual_seed(7) + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.9505, c=4.6855)) + xtl.calculate_structure_factors(k_max=1.5) + N = 6 + gam = torch.rand(N, dtype=torch.float64) * 2 * np.pi + q_true = qnormalize( + torch.stack( + [torch.cos(gam / 2), torch.zeros(N), torch.zeros(N), torch.sin(gam / 2)], dim=1 + ) + ) + peaks = _make_peaks(xtl, q_true) + + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan( + zone_axis_range="fiber", + fiber_axis=[0, 0, 0, 1], # Miller-Bravais [0001] + fiber_angle_deg=0.0, + angle_step_in_plane_deg=2.0, + power_intensity=0.0, + verbose=False, + ) + assert om.zone_axes.shape[0] == 1 + om.match_orientations(progress_bar=False) + om.refine_orientations(progress_bar=False, zone_max_total_deg=0.5) + err = misorientation_angle_deg(q_true, om.quats[0, :, 0], xtl.sym_quats).numpy() + assert np.all(err < 0.2) + + # a cap of a few degrees covers a spread of tilts, and the hemisphere + # fallback and the symmetry wedge both stay available + om2 = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om2.build_plan( + zone_axis_range="fiber", fiber_axis=[0, 0, 1], fiber_angle_deg=5.0, verbose=False + ) + assert om2.zone_axes.shape[0] > 1 + om3 = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om3.build_plan(zone_axis_range="full", angle_step_zone_axis_deg=4.0, verbose=False) + om4 = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om4.build_plan(angle_step_zone_axis_deg=4.0, verbose=False) + assert om3.zone_axes.shape[0] > om4.zone_axes.shape[0] + + +def test_power_intensity_experiment_is_separate(): + """The measured-intensity exponent defaults to the library one and can + be set independently.""" + torch.manual_seed(11) + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True)) + xtl.calculate_structure_factors(k_max=1.5) + q_true = qnormalize(torch.randn(3, 4, dtype=torch.float64)) + peaks = _make_peaks(xtl, q_true) + + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=3.0, power_intensity=0.25, verbose=False) + assert om.power_intensity_experiment == 0.25 + + om2 = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om2.build_plan( + angle_step_zone_axis_deg=3.0, + power_intensity=0.25, + power_intensity_experiment=0.0, + verbose=False, + ) + assert om2.power_intensity_experiment == 0.0 + assert om2.metadata["plan"]["power_intensity_experiment"] == 0.0 + om2.match_orientations(progress_bar=False) + assert float(om2.corr[0, 0, 0]) > 0.3 + + +def test_in_plane_angle_auto_fold(): + """The automatic fold removes the in-plane ambiguity of a <111> zone. + + Two orientations 60 degrees apart about a body-centered cubic <111> beam + give the same zero-layer pattern, so matching returns one or the other at + random. Folding by the projected order makes the reported angle the same + for both, which is what keeps an in-plane map continuous. + """ + from quantem.diffraction.rotations import ( + qmult, + quat_from_axis_angle, + quat_from_zone_axis, + ) + + torch.manual_seed(2) + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.26, cubic=True)) + xtl.calculate_structure_factors(k_max=1.5) + d = torch.tensor([1.0, 1.0, 1.0], dtype=torch.float64) @ xtl.lat_real + q0 = quat_from_zone_axis(d / torch.linalg.norm(d)) + beam = torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64) + + # N in-plane angles, each also present as its 60 degree twin + N = 5 + spin = torch.linspace(0.0, 1.0, N, dtype=torch.float64) + q_a = qnormalize(qmult(quat_from_axis_angle(beam, spin), q0)) + q_b = qnormalize(qmult(quat_from_axis_angle(beam, torch.tensor(np.deg2rad(60.0))), q_a)) + q_true = torch.stack([q_a, q_b], dim=0) # (2, N, 4) + + peaks = Vector.from_shape( + (2, N), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(2): + for j in range(N): + p = xtl.generate_pattern(q_true[i, j], energy_ev=200e3, sigma_excitation=0.02) + peaks[i, j] = np.stack( + [p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()], axis=1 + ) + + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=2.0, angle_step_in_plane_deg=2.0, power_intensity=0.0) + om.match_orientations(progress_bar=False) + om.refine_orientations(progress_bar=False, zone_max_total_deg=1.5) + + assert xtl.projected_rotation_order((d / torch.linalg.norm(d)).numpy()) == 6 + folded = om.in_plane_angle_deg(mod_deg="auto").numpy() + assert folded.max() <= 60.0 + 1e-6 + # the twin rows must agree once folded, to well under the library step + delta = np.abs(folded[0] - folded[1]) % 60.0 + delta = np.minimum(delta, 60.0 - delta) + assert np.all(delta < 1.0), delta + # explicit values and None still behave as before + assert om.in_plane_angle_deg(mod_deg=90.0).max() <= 90.0 + assert om.in_plane_angle_deg(mod_deg=None).max() > 60.0 From 28b4fd633d7bbcb3bec52f124b36d4e1bf71828c Mon Sep 17 00:00:00 2001 From: cophus Date: Sat, 19 Sep 2026 17:59:35 -0700 Subject: [PATCH 10/36] More fixes. --- src/quantem/diffraction/bragg_vectors.py | 15 ++ src/quantem/diffraction/calibration.py | 4 + src/quantem/diffraction/crystal.py | 28 +++ src/quantem/diffraction/orientation.py | 169 +++++++++++++++++- .../diffraction/orientation_visualization.py | 98 +++++++++- src/quantem/diffraction/phase.py | 11 +- src/quantem/diffraction/rotations.py | 22 +++ tests/diffraction/test_orientation.py | 126 +++++++++++++ 8 files changed, 468 insertions(+), 5 deletions(-) diff --git a/src/quantem/diffraction/bragg_vectors.py b/src/quantem/diffraction/bragg_vectors.py index 4a59c9f78..6b535b7fe 100644 --- a/src/quantem/diffraction/bragg_vectors.py +++ b/src/quantem/diffraction/bragg_vectors.py @@ -510,6 +510,7 @@ def detect_disks( # detection. from_data stacks all cells with one _replace_cells call. nested = [results[r * scan_c : (r + 1) * scan_c] for r in range(scan_r)] peaks = Vector.from_data(nested, fields=PEAK_FIELDS, name="bragg_peaks") + peaks.metadata.update(self._scan_calibration()) self.peaks = peaks self.metadata["detect"] = detect_kwargs @@ -1363,6 +1364,20 @@ def peak_histogram(self, *, returnfig: bool = False, **kwargs): # ---- helpers ---- + def _scan_calibration(self) -> dict: + """Scan step and units of the dataset, to travel with the peaks. + + Everything downstream draws its real-space scale bar from this, so + the step size is set once on the dataset rather than passed to every + plot. A dataset still in pixels records nothing. + """ + sampling = np.atleast_1d(np.asarray(self.dataset.sampling, dtype=float))[:2] + units = list(self.dataset.units)[:2] + unit = str(units[0]).strip("b'\"") if units else "" + if unit.lower() in ("pixels", "px", "pixel", ""): + return {} + return {"scan_sampling": tuple(float(v) for v in sampling), "scan_units": unit} + def _resolve_background_sigma(self, background_sigma: float | str | None) -> float | None: """Resolve the ``background_sigma`` argument to a value in pixels. diff --git a/src/quantem/diffraction/calibration.py b/src/quantem/diffraction/calibration.py index f72673171..2817a9f47 100644 --- a/src/quantem/diffraction/calibration.py +++ b/src/quantem/diffraction/calibration.py @@ -204,8 +204,12 @@ def peaks_to_calibrated( units=["A^-1", "A^-1", "counts"], name=name, ) + # carry the scan calibration through, so maps keep their scale bar, and # record the detector-to-scan rotation so pattern-overlay plots can put # peaks back into the raw detector frame + for key in ("scan_sampling", "scan_units"): + if key in (peaks_px.metadata or {}): + out.metadata[key] = peaks_px.metadata[key] out.metadata["rotation_ccw_deg"] = float(rotation_ccw_deg) return out diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py index 044f538ea..4d95aeafe 100644 --- a/src/quantem/diffraction/crystal.py +++ b/src/quantem/diffraction/crystal.py @@ -449,6 +449,8 @@ def projected_rotation_order( k_max: float | None = None, tol_zone: float = 0.02, intensity_tol: float = 0.05, + snap_deg: float = 4.0, + max_index: int = 3, ): """Apparent rotational symmetry of the zero-layer pattern, per zone axis. @@ -482,6 +484,17 @@ def projected_rotation_order( intensity_tol : float, default=0.05 A reflection and its image must agree in |F|^2 to within this fraction of the strongest zero-layer reflection. + snap_deg : float, default=4.0 + Zone axes within this angle of a low-index lattice direction are + evaluated at that direction. The extra symmetry is exact only on + the pole and decays away from it, but a beam a degree or two off + still produces a pattern whose positions carry it, which is + where a measured orientation normally sits; testing the exact + tilted axis would report no symmetry at all and miss the + ambiguity the indexing actually suffers. Set to 0 to test the + axis as given. + max_index : int, default=3 + Largest |u|, |v|, |w| considered when snapping. Returns ------- @@ -501,6 +514,21 @@ def projected_rotation_order( sel = self.g_len <= float(k_max) g, inten = g[sel], inten[sel] + if snap_deg > 0: + rng = torch.arange(-max_index, max_index + 1, dtype=torch.float64) + uvw = torch.cartesian_prod(rng, rng, rng) + uvw = uvw[uvw.abs().sum(dim=1) > 0] + cart = uvw @ self.lat_real + cart = cart / torch.linalg.norm(cart, dim=1, keepdim=True).clamp_min(1e-12) + dots = torch.abs(axes @ cart.T) + best = dots.max(dim=1) + near = best.values > np.cos(np.deg2rad(snap_deg)) + snapped = cart[best.indices] + # keep the original sense so the returned axis still points along + # the beam, and only replace the ones close enough to snap + sign = torch.sign(torch.einsum("ni,ni->n", snapped, axes)).unsqueeze(1) + axes = torch.where(near.unsqueeze(1), snapped * sign, axes) + out = np.ones(axes.shape[0], dtype=int) eye = torch.eye(3, dtype=torch.float64) for i, u in enumerate(axes): diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index f90b2f211..70099d37e 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -49,6 +49,7 @@ quat_from_axis_angle, quat_from_zone_axis, sample_zone_axes, + symmetry_aligned, symmetry_reduced_zone_angles, ) @@ -80,6 +81,75 @@ def position_mask(positions, shape: tuple[int, int]) -> torch.Tensor: return mask +def scan_scalebar(metadata: dict) -> dict | None: + """Scale bar arguments from the scan calibration recorded on the peaks. + + Returns {"sampling": step, "units": units} when the scan was calibrated, + or None when it is still in pixels, which is the signal that a plot + should draw no scale bar. + """ + step = (metadata or {}).get("scan_sampling") + units = (metadata or {}).get("scan_units") + if step is None or units is None: + return None + step = float(np.mean(np.atleast_1d(np.asarray(step, dtype=float)))) + units = str(units) + if not np.isfinite(step) or step <= 0 or units.lower() in ("pixels", "px", "pixel"): + return None + return {"sampling": step, "units": units} + + +def smooth_quaternions( + quats: torch.Tensor, + active: torch.Tensor, + sym_quats: torch.Tensor, + sigma_px: float = 1.0, + sigma_deg: float = 1.0, + max_angle_deg: float = 5.0, +) -> torch.Tensor: + """Bilateral average of an orientation field, (R, C, 4). + + Each position is replaced by the weighted mean of the orientations around + it, with weight exp(-r^2 / 2 sigma_px^2) * exp(-theta^2 / 2 sigma_deg^2) + for a neighbour r probe positions away and theta degrees misoriented, and + with neighbours beyond `max_angle_deg` dropped. The angular term is what + keeps a grain boundary or a second variant out of the average. + + This is an average, not a fit: it moves each orientation away from the one + that best explains its own pattern. Use it to display a map, not to + produce the orientations a later step will measure from. + """ + q = torch.as_tensor(quats, dtype=torch.float64) + R, C = q.shape[:2] + active = torch.as_tensor(active, dtype=torch.bool) + rad = max(1, int(np.ceil(3 * sigma_px))) + out = q.clone() + w_ang = 2.0 * sigma_deg**2 + for rx in range(R): + for ry in range(C): + if not bool(active[rx, ry]): + continue + r0, r1 = max(0, rx - rad), min(R, rx + rad + 1) + c0, c1 = max(0, ry - rad), min(C, ry + rad + 1) + sel = active[r0:r1, c0:c1] + if int(sel.sum()) < 2: + continue + rr, cc = torch.nonzero(sel, as_tuple=True) + qn = q[r0:r1, c0:c1][sel] + d2 = ((rr + r0 - rx) ** 2 + (cc + c0 - ry) ** 2).to(torch.float64) + ang = misorientation_angle_deg(q[rx, ry], qn, sym_quats) + keep = ang <= max_angle_deg + if int(keep.sum()) < 2: + continue + w = torch.exp(-d2[keep] / (2 * sigma_px**2)) * torch.exp(-(ang[keep] ** 2) / w_ang) + qk = symmetry_aligned(q[rx, ry], qn[keep], sym_quats) + qk = qk * torch.sign((qk @ q[rx, ry]).unsqueeze(-1)) + M = (w[:, None, None] * (qk[:, :, None] * qk[:, None, :])).sum(0) + _, evecs = torch.linalg.eigh(M) + out[rx, ry] = qnormalize(evecs[:, -1]) + return out + + def fibonacci_hemisphere(n_points: int, dtype=torch.float64) -> torch.Tensor: """Spherical Fibonacci sampling of the upper hemisphere, (N, 3).""" i = torch.arange(n_points, dtype=dtype) + 0.5 @@ -967,6 +1037,89 @@ def match_orientations( # sub-grid refinement # ------------------------------------------------------------------ + def smooth_orientations( + self, + match: int = 0, + sigma_px: float = 1.0, + sigma_deg: float = 1.0, + max_angle_deg: float = 5.0, + positions=None, + ) -> "OrientationMap": + """Average each orientation with its neighbours, keeping boundaries sharp. + + A bilateral filter on the orientation field: every position is + replaced by the weighted mean of the orientations around it, with + + w = exp(-r^2 / 2 sigma_px^2) * exp(-theta^2 / 2 sigma_deg^2) + + for a neighbour r probe positions away whose orientation differs by + theta, and with neighbours beyond `max_angle_deg` excluded outright. + The angular term is what keeps this from blurring across a grain + boundary or between two variants: those neighbours are tens of + degrees away and carry no weight. + + The point is the noise budget. Neighbouring probe positions inside a + grain measure the same orientation, so their scatter is measurement + error and averaging it down costs only spatial resolution, at the + scale of sigma_px probe steps. Running this before + `refine_orientations` starts the refinement from a cleaner field; + running it after smooths what the refinement leaves. + + This does not repair the ambiguities that make an orientation map + jump by tens of degrees, such as two variants with the same + zero-layer pattern: those differ by far more than `max_angle_deg` + and are excluded by design. Fold the in-plane angle by + `Crystal.projected_rotation_order` for those. + + Parameters + ---------- + match : int, default=0 + Which match index to smooth. + sigma_px : float, default=1.0 + Spatial width of the kernel in probe positions. The window is + three sigma wide. + sigma_deg : float, default=1.0 + Angular width: a neighbour misoriented by this much is weighted + down by 1/sqrt(e). + max_angle_deg : float, default=5.0 + Neighbours beyond this misorientation are excluded. + positions : list[tuple[int, int]] | np.ndarray | None + Positions to smooth; defaults to those carrying a match. + """ + assert self.quats is not None, "run match_orientations() first" + R, C = self.quats.shape[:2] + active = position_mask(positions, (R, C)) + if self.computed is not None: + active = active & self.computed + active = active & (self.corr[..., match] > 0) + self.quats[..., match, :] = smooth_quaternions( + self.quats[..., match, :], + active, + self.crystal.sym_quats, + sigma_px=sigma_px, + sigma_deg=sigma_deg, + max_angle_deg=max_angle_deg, + ) + self.metadata["smooth"] = dict( + match=int(match), + sigma_px=float(sigma_px), + sigma_deg=float(sigma_deg), + max_angle_deg=float(max_angle_deg), + ) + return self + + def smoothed_quats(self, match: int = 0, **kwargs) -> torch.Tensor: + """Smoothed copy of the orientations, leaving the stored ones alone.""" + assert self.quats is not None + R, C = self.quats.shape[:2] + active = torch.ones((R, C), dtype=torch.bool) + if self.computed is not None: + active = active & self.computed + active = active & (self.corr[..., match] > 0) + return smooth_quaternions( + self.quats[..., match, :], active, self.crystal.sym_quats, **kwargs + ) + def refine_orientations( self, num_iterations: int = 5, @@ -1407,7 +1560,7 @@ def envelope(S, g_rows): chunks = [i for i in range(0, N, chunk) if bool(valid_pos[i : i + chunk].any())] if progress_bar: - chunks = tqdm(chunks, desc="refining orientations (batched)") + chunks = tqdm(chunks, desc="refining orientations") for i0 in chunks: i1 = min(i0 + chunk, N) B = i1 - i0 @@ -1872,10 +2025,24 @@ def _default_mask(self, kwargs: dict) -> dict: kwargs["mask"] = self.computed.numpy().astype(float) return kwargs + @property + def scan_scalebar(self) -> dict | None: + """Real-space scale bar of the scan, carried from the dataset. + + `BraggVectors` stamps the scan sampling and units of the dataset onto + the detected peaks, and the calibration keeps them, so every map can + draw a scale bar without being told the step size. None when the + dataset was never calibrated, in which case set `dataset.sampling` + and `dataset.units` before detecting the disks. + """ + md = self.metadata.get("peaks", {}) or {} + return scan_scalebar(md) + def plot_orientation(self, direction: str = "z", match: int = 0, **kwargs): """IPF-colored orientation map; see orientation_visualization.""" from quantem.diffraction.orientation_visualization import plot_orientation_map + kwargs.setdefault("scalebar", self.scan_scalebar) return plot_orientation_map( self, direction=direction, match=match, **self._default_mask(kwargs) ) diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py index b65da4c98..91a0b7018 100644 --- a/src/quantem/diffraction/orientation_visualization.py +++ b/src/quantem/diffraction/orientation_visualization.py @@ -145,6 +145,69 @@ def ipf_color( return _bary_to_rgb(w.numpy()) +def fold_in_plane(quats: torch.Tensor, crystal: Crystal, strict: bool = False) -> torch.Tensor: + """Fold the in-plane angle of each orientation by its projected symmetry. + + The zero-layer pattern of a zone axis can repeat more often under + rotation about the beam than the crystal does, and where it does, two + orientations produce the same measured pattern and the match returns one + of them arbitrarily. Rotating each orientation about the beam into the + first such sector makes those two identical, so any map colored from the + result is continuous across the ambiguity. + + Positions whose pattern is no more symmetric than the crystal itself are + returned unchanged, unless `strict`, which folds by the projected order + everywhere. + """ + from quantem.diffraction.rotations import ( + qmult, + qnormalize, + quat_from_axis_angle, + quat_to_matrix, + ) + + q = torch.as_tensor(quats, dtype=torch.float64) + shape = q.shape[:-1] + flat = q.reshape(-1, 4) + R = quat_to_matrix(flat) + zone = R[:, 2, :] # beam direction in crystal coordinates + # the projected order is piecewise constant in the zone axis: evaluate it + # once per distinct axis on a coarse grid + key = torch.round(zone * 200) / 200 + uniq, inv = torch.unique(key, dim=0, return_inverse=True) + n_proj = torch.as_tensor( + np.asarray(crystal.projected_rotation_order(uniq.numpy())), dtype=torch.float64 + )[inv] + if not strict: + # only fold where the pattern is more symmetric than the crystal is + # about that same axis, which is where the indexing is degenerate + n_cryst = _crystal_rotation_order(uniq, crystal)[inv].to(torch.float64) + n_proj = torch.where(n_proj > n_cryst, n_proj, torch.ones_like(n_proj)) + if bool((n_proj <= 1).all()): + return q + a_lab = R[:, :, 0] + ang = torch.rad2deg(torch.atan2(a_lab[:, 0], a_lab[:, 1])) + sector = 360.0 / n_proj + delta = ang - (ang % sector) + # the in-plane angle is measured from the column axis toward the row + # axis, which runs opposite to a right-handed rotation about the beam + beam = torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64) + dq = quat_from_axis_angle(beam, torch.deg2rad(delta)) + return qnormalize(qmult(dq, flat)).reshape(*shape, 4) + + +def _crystal_rotation_order(axes: torch.Tensor, crystal: Crystal) -> torch.Tensor: + """Order of the crystal's own rotation axis along each direction, (N,).""" + from quantem.diffraction.rotations import quat_to_matrix + + Rs = quat_to_matrix(crystal.sym_quats) # (S, 3, 3) + u = axes / torch.linalg.norm(axes, dim=1, keepdim=True).clamp_min(1e-12) + # an operation is a rotation about u when it leaves u fixed + fixed = torch.einsum("sij,nj->nsi", Rs, u) + keeps = (fixed - u[:, None, :]).norm(dim=-1) < 1e-6 + return keeps.sum(dim=1) + + def wedge_legend( crystal: Crystal, ax, @@ -231,6 +294,8 @@ def plot_orientation_map( axsize: tuple[float, float] = (9.0, 4.5), crop: tuple[int, int, int, int] | None = None, title: str | None = None, + fold: bool | str = "auto", + smooth: dict | bool | None = None, ): """IPF-colored orientation map with the wedge legend in an adjacent panel. @@ -244,6 +309,26 @@ def plot_orientation_map( Which match index to plot. mask : np.ndarray | None Multiplied into the RGB image (e.g. a phase or reliability mask). + fold : bool | "auto", default="auto" + Fold the in-plane part of each orientation by the apparent + rotational symmetry of its own zero-layer pattern + (`Crystal.projected_rotation_order`) before coloring. Where that + symmetry exceeds the crystal's own, as for a cubic crystal near + <111>, two orientations give the same pattern and the match picks + between them at random; folding gives them the same color, which + removes jumps that no refinement can. It changes nothing for + direction="z", whose color depends only on the zone axis, and + nothing where the pattern is no more symmetric than the crystal. + "auto" folds only when some position needs it. + smooth : dict | bool | None + Smooth the orientations for display only, leaving the stored ones at + the fit to their own pattern. The average is bilateral and needs two + widths, not one: `sigma_px` over probe positions and `sigma_deg` over + misorientation, with `max_angle_deg` excluding anything further. The + angular pair is what stops the average at a grain boundary, so pass + a dict naming the values you want, such as + {"sigma_px": 1.0, "sigma_deg": 1.0, "max_angle_deg": 5.0}, which is + also what True uses. scalebar : dict | None Real-space scale bar, e.g. {"sampling": 30, "units": "A"}. figax : (fig, (ax_map, ax_legend)) | (fig, ax_map) | None @@ -256,7 +341,18 @@ def plot_orientation_map( import matplotlib.pyplot as plt assert om.quats is not None - rgb = ipf_color(om.quats[..., match, :], om.crystal, direction) + quats = om.quats[..., match, :] + if smooth is not None and smooth is not False: + if not isinstance(smooth, (dict, bool)): + raise TypeError( + "smooth must be a dict of widths or True; a bare number would set the " + "spatial width and leave the angular tolerance at its default, which " + "is the argument that keeps the average inside one grain" + ) + quats = om.smoothed_quats(match=match, **(smooth if isinstance(smooth, dict) else {})) + if fold: + quats = fold_in_plane(quats, om.crystal, strict=fold != "auto") + rgb = ipf_color(quats, om.crystal, direction) if mask is not None: rgb = rgb * np.asarray(mask, dtype=float)[..., None] if crop is not None: diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py index ea07ab2fb..2dbe71b2e 100644 --- a/src/quantem/diffraction/phase.py +++ b/src/quantem/diffraction/phase.py @@ -323,7 +323,7 @@ def plot_phase( self, phase_colors: np.ndarray | None = None, reliability_range: tuple[float, float] = (0.0, 0.1), - scalebar: dict | None = None, + scalebar: dict | str | None = "auto", figax=None, ): """Dominant-phase map, colored by phase and shaded by reliability. @@ -335,9 +335,14 @@ def plot_phase( the pattern overlay plots (gold, light blue, ...). reliability_range : tuple, default=(0.0, 0.1) Reliability values mapped to black ... full color. - scalebar : dict | None - Real-space scale bar, e.g. {"sampling": 30, "units": "A"}. + scalebar : dict | "auto" | None + Real-space scale bar. "auto" (the default) takes the scan step + and units carried from the dataset by the orientation maps; a + dict such as {"sampling": 30, "units": "A"} overrides it, and + None draws no bar. """ + if isinstance(scalebar, str): + scalebar = self.orientation_maps[0].scan_scalebar if scalebar == "auto" else None import matplotlib.pyplot as plt from quantem.core.visualization.visualization_utils import add_scalebar_to_ax diff --git a/src/quantem/diffraction/rotations.py b/src/quantem/diffraction/rotations.py index 08c396227..3d8e72c12 100644 --- a/src/quantem/diffraction/rotations.py +++ b/src/quantem/diffraction/rotations.py @@ -350,6 +350,28 @@ def _closest(cands: torch.Tensor, prefer: torch.Tensor) -> torch.Tensor: return signed[int(torch.argmax(key))] +def symmetry_aligned( + reference: torch.Tensor, + quats: torch.Tensor, + sym_quats: torch.Tensor, +) -> torch.Tensor: + """Symmetry images of `quats` that lie nearest to `reference`, (N, 4). + + Two quaternions can describe the same crystal orientation while being far + apart as quaternions, so any average over orientations has to bring them + into a common symmetry branch first. For each input this returns the + symmetry-equivalent quaternion whose misorientation to the reference is + smallest, which makes a weighted quaternion mean well defined. + """ + ref = torch.as_tensor(reference, dtype=torch.float64).reshape(4) + q = torch.as_tensor(quats, dtype=torch.float64).reshape(-1, 4) + sym = torch.as_tensor(sym_quats, dtype=torch.float64).reshape(-1, 4) + cand = qmult(q[:, None, :], sym[None, :, :]) # (N, S, 4) + dots = torch.abs(torch.einsum("nsi,i->ns", cand, ref)) + best = dots.argmax(dim=1) + return qnormalize(cand[torch.arange(q.shape[0]), best]) + + def sample_zone_axis_cap( axis: torch.Tensor, half_angle_deg: float, diff --git a/tests/diffraction/test_orientation.py b/tests/diffraction/test_orientation.py index d730e31ae..35c2622ce 100644 --- a/tests/diffraction/test_orientation.py +++ b/tests/diffraction/test_orientation.py @@ -482,3 +482,129 @@ def test_in_plane_angle_auto_fold(): # explicit values and None still behave as before assert om.in_plane_angle_deg(mod_deg=90.0).max() <= 90.0 assert om.in_plane_angle_deg(mod_deg=None).max() > 60.0 + + +def test_fold_in_plane_collapses_degenerate_variants(): + """Two orientations with the same zero-layer pattern get the same color.""" + from quantem.diffraction.orientation_visualization import fold_in_plane, ipf_color + from quantem.diffraction.rotations import ( + qmult, + quat_from_axis_angle, + quat_from_zone_axis, + ) + + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.26, cubic=True), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + d = torch.tensor([1.0, 1.0, 1.0], dtype=torch.float64) @ xtl.lat_real + q0 = quat_from_zone_axis(d / torch.linalg.norm(d)) + beam = torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64) + tilt_axis = torch.tensor([1.0, 0.0, 0.0], dtype=torch.float64) + + torch.manual_seed(4) + for tilt in (0.0, 1.8, 3.0): + base = qnormalize( + qmult(quat_from_axis_angle(tilt_axis, torch.tensor(np.deg2rad(tilt))), q0) + ) + spin = torch.rand(8, dtype=torch.float64) * 2 * np.pi + q_a = qnormalize(qmult(quat_from_axis_angle(beam, spin), base)) + q_b = qnormalize(qmult(quat_from_axis_angle(beam, torch.tensor(np.deg2rad(60.0))), q_a)) + folded_a = fold_in_plane(q_a, xtl) + folded_b = fold_in_plane(q_b, xtl) + assert torch.allclose(torch.abs(folded_a), torch.abs(folded_b), atol=1e-8) + c_a = ipf_color(folded_a, xtl, "r") + c_b = ipf_color(folded_b, xtl, "r") + assert np.abs(c_a - c_b).max() < 1e-6, tilt + # the out-of-plane color never depended on the in-plane angle + assert np.abs(ipf_color(q_a, xtl, "z") - ipf_color(q_b, xtl, "z")).max() < 1e-6 + + # far from the pole the ambiguity is gone and nothing is folded + far = qnormalize(qmult(quat_from_axis_angle(tilt_axis, torch.tensor(np.deg2rad(20.0))), q0)) + assert torch.allclose(fold_in_plane(far[None], xtl)[0], far, atol=1e-12) + + +def test_smooth_orientations(): + """Bilateral smoothing averages noise inside a grain, not across variants.""" + from quantem.diffraction.rotations import qmult, quat_from_axis_angle + + torch.manual_seed(6) + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True)) + xtl.calculate_structure_factors(k_max=1.5) + + # a 6 x 6 patch of one orientation with half a degree of scatter, plus a + # second grain 30 degrees away filling the right-hand columns + R, C = 6, 6 + base = qnormalize(torch.randn(4, dtype=torch.float64)) + axis = torch.randn(R, C, 3, dtype=torch.float64) + axis = axis / axis.norm(dim=-1, keepdim=True) + noise = quat_from_axis_angle(axis.reshape(-1, 3), torch.deg2rad(0.5 * torch.randn(R * C))) + q = qnormalize(qmult(noise, base)).reshape(R, C, 4) + other = qnormalize( + qmult( + quat_from_axis_angle(torch.tensor([0.0, 0.0, 1.0]), torch.tensor(np.deg2rad(30.0))), + base, + ) + ) + q[:, 4:] = other + + peaks = Vector.from_shape( + (R, C), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(R): + for j in range(C): + p = xtl.generate_pattern(q[i, j], energy_ev=200e3, sigma_excitation=0.02) + peaks[i, j] = np.stack( + [p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()], axis=1 + ) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=3.0, verbose=False) + om.quats = q.clone()[..., None, :] + om.corr = torch.ones((R, C, 1), dtype=torch.float64) + om.computed = torch.ones((R, C), dtype=torch.bool) + + before = misorientation_angle_deg(base, om.quats[:, :4, 0].reshape(-1, 4), xtl.sym_quats) + om.smooth_orientations(sigma_px=1.0, sigma_deg=1.0, max_angle_deg=5.0) + after = misorientation_angle_deg(base, om.quats[:, :4, 0].reshape(-1, 4), xtl.sym_quats) + assert float(after.mean()) < float(before.mean()), (float(before.mean()), float(after.mean())) + + # the second grain is 30 degrees away, beyond max_angle_deg, so it is + # neither pulled toward the first nor allowed to pull on it + kept = misorientation_angle_deg(other, om.quats[:, 5, 0], xtl.sym_quats) + assert float(kept.max()) < 1e-6 + + +def test_display_smoothing_needs_both_widths(): + """A bare number is refused: the angular tolerance must not default silently.""" + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + torch.manual_seed(8) + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True)) + xtl.calculate_structure_factors(k_max=1.5) + R, C = 4, 4 + q = qnormalize(torch.randn(4, dtype=torch.float64)).expand(R, C, 4).clone() + peaks = Vector.from_shape( + (R, C), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(R): + for j in range(C): + p = xtl.generate_pattern(q[i, j], energy_ev=200e3, sigma_excitation=0.02) + peaks[i, j] = np.stack( + [p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()], axis=1 + ) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=4.0, verbose=False) + om.quats = q.clone()[..., None, :] + om.corr = torch.ones((R, C, 1), dtype=torch.float64) + om.computed = torch.ones((R, C), dtype=torch.bool) + + with pytest.raises(TypeError, match="angular tolerance"): + om.plot_orientation(smooth=1.0) + + before = om.quats.clone() + om.plot_orientation(smooth={"sigma_px": 1.0, "sigma_deg": 1.0, "max_angle_deg": 5.0}) + om.plot_orientation(smooth=True) + plt.close("all") + # smoothing for display must never touch the stored orientations + assert torch.equal(before, om.quats) From 8f318b2c26dab0c4494e7c17fe93ee1b51b5246c Mon Sep 17 00:00:00 2001 From: cophus Date: Wed, 23 Sep 2026 09:48:27 -0700 Subject: [PATCH 11/36] fixes --- widget/js/diffsim-web/index.ts | 110 ++++++++++++++++++++------- widget/js/diffsim/index.tsx | 83 +++++++++++++------- widget/js/diffsim/physics.ts | 60 ++++++++++++--- widget/src/quantem/widget/diffsim.py | 24 ++++-- 4 files changed, 204 insertions(+), 73 deletions(-) diff --git a/widget/js/diffsim-web/index.ts b/widget/js/diffsim-web/index.ts index 8531e89ea..50a7604cc 100644 --- a/widget/js/diffsim-web/index.ts +++ b/widget/js/diffsim-web/index.ts @@ -39,6 +39,7 @@ const MAX_BEAMS_DRAG = 28; const SPIN_FPS = 20; // recompute rate while spinning (battery) const CBED_GRID = 7; const CBED_GRID_DRAG = 5; +const CBED_PREC_NODES = 8; // ring nodes per incident direction of the cone interface Model { get(key: string): unknown } @@ -210,7 +211,7 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) {
nanobeam
CBED
-
Kossel lines
+
Kikuchi pattern
@@ -298,7 +299,13 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { let nb: { beams: Reflection[]; nDyn: number } = { beams: [], nDyn: 0 }; let nbSolution: NanobeamSolution | null = null; let nbTilts: [number, number][] = [[0, 0]]; - let cbed: { grid: ReturnType; beams: Reflection[]; nDyn: number; sols: ReturnType[] | null } | null = null; + let cbed: { + grid: ReturnType; + beams: Reflection[]; + nDyn: number; + nodes: [number, number][]; // precession ring, one entry at the origin without it + sols: ReturnType[] | null; // grid tilt major, ring node minor + } | null = null; let lines: ReturnType = []; let geomKey = ""; @@ -326,15 +333,23 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { } else if (state.mode === "cbed") { const Rk = k0() * Math.sin(alpha); const grid = tiltGrid(Rk, (state.dragging || state.spinning) ? CBED_GRID_DRAG : CBED_GRID); + // every incident direction of the cone is itself precessed, so the cost + // is the grid times the ring: fewer ring nodes here than in nanobeam + const nRing = (state.dragging || state.spinning) ? Math.min(4, dragNodes) : CBED_PREC_NODES; + const nodes = precessionTilts(k0(), state.precession, nRing); if (state.dynamical) { const { beams, nDyn } = hybridBeams(crystal, q, crystal.k_max, SG_MAX, (state.dragging || state.spinning) ? 20 : 32, Math.sin(alpha)); const dyn = beams.slice(0, nDyn); - cbed = { grid, beams, nDyn, sols: grid.tilts.map((t) => blochSolve(crystal, dyn, t)) }; + const sols: ReturnType[] = []; + for (const t of grid.tilts) { + for (const nd of nodes) sols.push(blochSolve(crystal, dyn, [t[0] + nd[0], t[1] + nd[1]])); + } + cbed = { grid, beams, nDyn, nodes, sols }; } else { - cbed = { grid, beams: [DIRECT, ...labReflections(crystal, q, crystal.k_max)], nDyn: 0, sols: null }; + cbed = { grid, beams: [DIRECT, ...labReflections(crystal, q, crystal.k_max)], nDyn: 0, nodes, sols: null }; } } - if (state.mode === "kossel" || (state.mode === "nanobeam" && state.kikuchi)) { + if (state.mode === "kossel" || (state.mode !== "kossel" && state.kikuchi)) { const fov = state.mode === "kossel" ? state.fieldMrad * 1e-3 : state.patternRange / k0(); lines = kosselLines(crystal, q, Math.min(crystal.k_max, 2.5), fov); } @@ -350,6 +365,36 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { return out; }; + /** Intensity of every beam at each incident direction of the cone, averaged + * over the precession ring. */ + const intensitiesCbed = (): Float64Array[] => { + if (!cbed) return []; + const { grid, beams, nDyn, nodes, sols } = cbed; + return grid.tilts.map((t, i) => { + const out = new Float64Array(beams.length); + for (let k = 0; k < nodes.length; k++) { + const tilt: [number, number] = [t[0] + nodes[k][0], t[1] + nodes[k][1]]; + const acc = new Float64Array(beams.length); + if (sols) { + acc.set(blochIntensities(sols[i * nodes.length + k], state.thickness)); + slabIntensities(crystal, beams, nDyn, tilt, state.thickness, acc); + } else { + acc.set(kinematicalTilted(crystal, beams, tilt, 0.02)); + } + for (let b = 0; b < out.length; b++) out[b] += acc[b] / nodes.length; + } + return out; + }); + }; + + /** Disk-averaged intensity of every beam, for the double-click snap. */ + const intensitiesCbedMean = (): Float64Array => { + const per = intensitiesCbed(); + const out = new Float64Array(cbed ? cbed.beams.length : 0); + for (const arr of per) for (let b = 0; b < out.length; b++) out[b] += arr[b] / Math.max(per.length, 1); + return out; + }; + // ---------------------------------------------------------------- drawing const drawCellPanel = () => { const key = `${state.preset}|${state.nCells.join(",")}|${state.polyhedra}`; @@ -362,7 +407,7 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { if (ctx) { ctx.setTransform(dpr, 0, 0, dpr, 0, 0); const refl = state.mode === "nanobeam" && nb.beams.length ? nb.beams : labReflections(crystal, state.quat, crystal.k_max); - drawEwaldPanel(ctx, Sc, Se, refl, k0(), state.patternRange, SG_MAX, VIEW_X, dark, state.mode === "nanobeam" ? state.precession : 0); + drawEwaldPanel(ctx, Sc, Se, refl, k0(), state.patternRange, SG_MAX, VIEW_X, dark, state.mode !== "kossel" ? state.precession : 0); } } const R = quatToMatrix(state.quat); @@ -446,14 +491,7 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { ? `${nb.nDyn} Bloch beams (|s| < ${SG_MAX} Å⁻¹, absorptive), thin-slab intensities for the other ${nb.beams.length - nb.nDyn}${prec}` : `kinematical: |F|² with a Gaussian excitation envelope (σ = 0.02 Å⁻¹)`; } else if (state.mode === "cbed" && cbed) { - const inten = cbed.sols - ? cbed.sols.map((sol, i) => { - const out = new Float64Array(cbed!.beams.length); - out.set(blochIntensities(sol, state.thickness)); - slabIntensities(crystal, cbed!.beams, cbed!.nDyn, cbed!.grid.tilts[i], state.thickness, out); - return out; - }) - : cbed.grid.tilts.map((t) => kinematicalTilted(crystal, cbed!.beams, t, 0.02)); + const inten = intensitiesCbed(); const img = cbedImage(f, cbed.beams, cbed.grid, inten); // normalise to the brightest pixel outside the direct disk const Rpx = cbed.grid.R * (0.5 * S * 0.92) / f.qMax + 1.5; @@ -469,7 +507,11 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { for (let i = 0; i < img.length; i++) disp[i] = Math.min(1, Math.pow(Math.max(img[i], 0) / hi, state.power)); histogramOf(disp); drawImage(ctx, f, disp, dark ? "gray" : "gray_r", state.vmin, state.vmax, dark, "Å⁻¹", 1); - status = `${cbed.nDyn} Bloch beams × ${cbed.grid.tilts.length} incident tilts per disk; disks summed where they overlap`; + if (state.kikuchi) drawKikuchiOverlay(ctx, f, lines, k0(), dark); + const prec = state.precession > 0 + ? `; precession ${state.precession.toFixed(2)}° over ${cbed.nodes.length} ring nodes` + : ""; + status = `${cbed.nDyn} Bloch beams × ${cbed.grid.tilts.length} incident tilts per disk; disks summed where they overlap${prec}`; } else if (state.mode === "kossel") { drawKosselLines(ctx, f, lines, dark, state.showHkl, 0.02); status = `deficient line of every reflection: line width = two-beam rocking width |U_g| / (k₀|g|), darkness ∝ |U_g|`; @@ -574,17 +616,21 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { const x = e.clientX - rect.left, y = e.clientY - rect.top; const f = frame(); const sc = (0.5 * S * 0.92) / f.qMax; - if (state.mode === "nanobeam") { - const inten = intensitiesNanobeam(); + if (state.mode === "nanobeam" || state.mode === "cbed") { + // a convergent beam draws the same reflections as wide disks, so both + // modes snap to the same two-beam condition the same way + const beams = state.mode === "cbed" && cbed ? cbed.beams : nb.beams; + const inten = state.mode === "cbed" ? intensitiesCbedMean() : intensitiesNanobeam(); let iMax = 0; - for (let i = 1; i < nb.beams.length; i++) iMax = Math.max(iMax, inten[i]); - const snap = Math.max(8, 1.5 * k0() * Math.sin(state.semiconv * 1e-3) * sc); + for (let i = 1; i < beams.length; i++) iMax = Math.max(iMax, inten[i]); + const disk = k0() * Math.sin(state.semiconv * 1e-3) * sc; + const snap = Math.max(8, (state.mode === "cbed" ? 1.0 : 1.5) * disk); // several reflections of different g_z share one spot (in hcp the first // HOLZ layer is only 0.21 1/A up): take the candidate under the click // that needs the smallest tilt to reach Bragg, never more than 5 degrees let best: Reflection | null = null, bestTilt = (5 * Math.PI) / 180; - for (let i = 0; i < nb.beams.length; i++) { - const b = nb.beams[i]; + for (let i = 0; i < beams.length; i++) { + const b = beams[i]; if (b.index < 0 || !(inten[i] > 1e-4 * iMax)) continue; const [px, py] = toPx(f, b.g[0], b.g[1]); if (Math.hypot(px - x, py - y) > snap) continue; @@ -600,9 +646,12 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { recompute(); return; } + // empty space: the Laue circle centre moves to the click shiftPattern((VIEW_X * (x - rect.width / 2)) / sc, -(y - rect.height / 2) / sc, true); } else { - shiftPattern(-(VIEW_X * (x - rect.width / 2)) / sc, (y - rect.height / 2) / sc, state.mode !== "kossel"); + // the Kikuchi map is a map of beam directions, so the clicked direction + // moves onto the axis, which is the opposite sense + shiftPattern(-(VIEW_X * (x - rect.width / 2)) / sc, (y - rect.height / 2) / sc, false); } recompute(); }); @@ -705,13 +754,16 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { const updateModeUI = () => { modeButtons.forEach((b) => b.classList.toggle("active", b.dataset.mode === state.mode)); const show = (sel: string, on: boolean) => { $(sel).style.display = on ? "" : "none"; }; - show(`#${id}-thickwrap`, state.mode !== "kossel" && state.dynamical); - show(`#${id}-convwrap`, state.mode !== "kossel"); - show(`#${id}-rangewrap`, state.mode !== "kossel"); - show(`#${id}-fieldwrap`, state.mode === "kossel"); - show(`#${id}-dynwrap`, state.mode !== "kossel"); - show(`#${id}-kikwrap`, state.mode === "nanobeam"); - show(`#${id}-precwrap`, state.mode === "nanobeam"); + // nanobeam and CBED share every control; the Kikuchi pattern is a map of + // beam directions and takes the field of view instead + const pattern = state.mode !== "kossel"; + show(`#${id}-thickwrap`, pattern && state.dynamical); + show(`#${id}-convwrap`, pattern); + show(`#${id}-rangewrap`, pattern); + show(`#${id}-fieldwrap`, !pattern); + show(`#${id}-dynwrap`, pattern); + show(`#${id}-kikwrap`, pattern); + show(`#${id}-precwrap`, pattern); show(`#${id}-approw`, state.mode !== "kossel"); show(`#${id}-apppanel`, state.mode !== "kossel" && state.showAppearance); }; diff --git a/widget/js/diffsim/index.tsx b/widget/js/diffsim/index.tsx index aee8073a6..978eddfbe 100644 --- a/widget/js/diffsim/index.tsx +++ b/widget/js/diffsim/index.tsx @@ -36,6 +36,7 @@ import { const DIRECT: Reflection = { index: -1, hkl: [0, 0, 0], g: [0, 0, 0], gLen: 0, s: 0 }; const SPIN_FPS = 20; // orientation update rate while spinning +const CBED_PREC_NODES = 8; // precession ring nodes per incident direction of the cone const QUALITY: Record = { fast: { grid: 5, beams: 24, nanobeam: 40 }, medium: { grid: 7, beams: 36, nanobeam: 64 }, @@ -329,18 +330,24 @@ function DiffSim() { const onPatDoubleClick = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left, y = e.clientY - rect.top; - if (mode === "nanobeam" && crystal) { - const snapPx = Math.max(8, 1.2 * (render === "disks" ? k0 * Math.sin(alpha) * patScale.current : markerSize * (S / 420))); + if ((mode === "nanobeam" || mode === "cbed") && crystal) { + // a convergent beam draws the same reflections as wide disks, so both + // modes snap to the same two-beam condition the same way + const beamList = mode === "cbed" && cbed ? cbed.beams : nbBeams; + const intenList = mode === "cbed" ? cbedMean : nbInten; + const snapPx = mode === "cbed" + ? Math.max(8, k0 * Math.sin(alpha) * patScale.current) + : Math.max(8, 1.2 * (render === "disks" ? k0 * Math.sin(alpha) * patScale.current : markerSize * (S / 420))); // candidates under the click: several reflections of different g_z // share one spot (in hcp the first HOLZ layer is only 0.21 1/A up), so // take the one that needs the SMALLEST tilt to reach Bragg, and never // jump by more than 5 degrees let best: Reflection | null = null, bestTilt = (5 * Math.PI) / 180; let iMax = 0; - for (let i = 1; i < nbBeams.length; i++) iMax = Math.max(iMax, nbInten[i] || 0); - for (let i = 0; i < nbBeams.length; i++) { - const b = nbBeams[i]; - if (b.index < 0 || !(nbInten[i] > 1e-4 * iMax)) continue; + for (let i = 1; i < beamList.length; i++) iMax = Math.max(iMax, intenList[i] || 0); + for (let i = 0; i < beamList.length; i++) { + const b = beamList[i]; + if (b.index < 0 || !(intenList[i] > 1e-4 * iMax)) continue; const [px, py] = toPx(frame, b.g[0], b.g[1]); if (Math.hypot(px - x, py - y) > snapPx) continue; const gxy = Math.hypot(b.g[0], b.g[1]); @@ -404,29 +411,51 @@ function DiffSim() { if (!crystal || mode !== "cbed") return null; const Rk = k0 * Math.sin(alpha); const grid = tiltGrid(Rk, dragging ? 5 : qual.grid); - if (!dynamical) return { grid, beams: [DIRECT, ...labReflections(crystal, quat, crystal.k_max)], nDyn: 0, sols: null }; + // every incident direction of the cone is itself precessed, so the cost is + // the grid times the ring: fewer ring nodes here than in nanobeam + const nodes = precessionTilts(k0, precession || 0, dragging ? 4 : CBED_PREC_NODES); + if (!dynamical) { + return { grid, beams: [DIRECT, ...labReflections(crystal, quat, crystal.k_max)], nDyn: 0, nodes, sols: null }; + } const { beams, nDyn } = hybridBeams(crystal, quat, crystal.k_max, SG_MAX, dragging ? Math.min(qual.beams, 24) : qual.beams, Math.sin(alpha)); const dyn = beams.slice(0, nDyn); - const sols = grid.tilts.map((t) => blochSolve(crystal, dyn, t)); - return { grid, beams, nDyn, sols }; - }, [crystal, quat, qMaxDisp, mode, dynamical, alpha, k0, dragging, qual, SG_MAX]); + const sols: ReturnType[] = []; // grid tilt major, ring node minor + for (const t of grid.tilts) { + for (const nd of nodes) sols.push(blochSolve(crystal, dyn, [t[0] + nd[0], t[1] + nd[1]])); + } + return { grid, beams, nDyn, nodes, sols }; + }, [crystal, quat, qMaxDisp, mode, dynamical, alpha, k0, dragging, qual, SG_MAX, precession]); const cbedInten = React.useMemo(() => { if (!crystal || !cbed) return null; - if (cbed.sols) { - return cbed.sols.map((sol, i) => { - const out = new Float64Array(cbed.beams.length); - out.set(blochIntensities(sol, thickness)); - slabIntensities(crystal, cbed.beams, cbed.nDyn, cbed.grid.tilts[i], thickness, out); - return out; - }); - } - return cbed.grid.tilts.map((t) => kinematicalTilted(crystal, cbed.beams, t, sigma)); + const { grid, beams, nDyn, nodes, sols } = cbed; + return grid.tilts.map((t, i) => { + const out = new Float64Array(beams.length); + for (let k = 0; k < nodes.length; k++) { + const tilt: [number, number] = [t[0] + nodes[k][0], t[1] + nodes[k][1]]; + const acc = new Float64Array(beams.length); + if (sols) { + acc.set(blochIntensities(sols[i * nodes.length + k], thickness)); + slabIntensities(crystal, beams, nDyn, tilt, thickness, acc); + } else { + acc.set(kinematicalTilted(crystal, beams, tilt, sigma)); + } + for (let b = 0; b < out.length; b++) out[b] += acc[b] / nodes.length; + } + return out; + }); }, [crystal, cbed, thickness, sigma]); + // disk-averaged intensity of every beam, for the double-click snap + const cbedMean = React.useMemo(() => { + if (!cbed || !cbedInten) return new Float64Array(0); + const out = new Float64Array(cbed.beams.length); + for (const arr of cbedInten) for (let b = 0; b < out.length; b++) out[b] += arr[b] / cbedInten.length; + return out; + }, [cbed, cbedInten]); // ---- Kossel ------------------------------------------------------------- const fieldRad = fieldMrad * 1e-3; const lines = React.useMemo(() => { - if (!crystal || (mode !== "kossel" && !(mode === "nanobeam" && kikuchi))) return []; + if (!crystal || (mode !== "kossel" && !kikuchi)) return []; const fov = mode === "kossel" ? fieldRad : qMaxDisp / k0; return kosselLines(crystal, quat, Math.min(crystal.k_max, 2.5), fov); }, [crystal, quat, mode, fieldRad, kikuchi, qMaxDisp, k0]); @@ -479,7 +508,7 @@ function DiffSim() { if (!ctx) return; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); const refl = mode === "nanobeam" && nbBeams.length ? nbBeams : labReflections(crystal, quat, crystal.k_max); - drawEwaldPanel(ctx, Sc, Se, refl, k0, qMaxDisp, SG_MAX, viewX, dark, mode === "nanobeam" ? precession || 0 : 0); + drawEwaldPanel(ctx, Sc, Se, refl, k0, qMaxDisp, SG_MAX, viewX, dark, mode !== "kossel" ? precession || 0 : 0); }, [quat, Sc, Se, dark, viewX, showEwald, crystal, mode, nbBeams, qMaxDisp, k0, SG_MAX, precession]); const patRef = React.useRef(null); @@ -492,7 +521,7 @@ function DiffSim() { const vmin = display.lo + (vminPct / 100) * (display.hi - display.lo); const vmax = display.lo + (vmaxPct / 100) * (display.hi - display.lo); drawImage(ctx, frame, display.data, cmap, vmin, vmax, dark, mode === "kossel" ? "rad" : "Å⁻¹", mode === "kossel" ? 0.01 : 1); - if (mode === "nanobeam" && kikuchi) drawKikuchiOverlay(ctx, frame, lines, k0, true); + if (mode !== "kossel" && kikuchi) drawKikuchiOverlay(ctx, frame, lines, k0, true); if (mode === "nanobeam" && showHkl) labelBeams(ctx, frame, nbBeams, nbInten, dark, true); } else if (mode === "nanobeam" && render === "disks") { drawDisks(ctx, frame, nbBeams, nbInten, dark, showHkl, k0 * Math.sin(alpha), markerPower); @@ -688,7 +717,7 @@ function DiffSim() { v && setMode(v)} sx={tbg}> nanobeam CBED - Kossel / LACBED + Kikuchi pattern {mode !== "cbed" && ( v && setRender(v)} sx={tbg}> @@ -707,7 +736,7 @@ function DiffSim() { setShowHkl(e.target.checked)} /> hkl labels - {mode === "nanobeam" && ( + {mode !== "kossel" && ( setKikuchi(e.target.checked)} /> Kikuchi lines @@ -731,7 +760,7 @@ function DiffSim() { `${v.toFixed(0)} Å`} width={180} disabled={mode !== "kossel" ? !dynamical : render !== "pixels"} /> {(mode === "cbed" || (mode === "nanobeam" && render === "disks")) && `${v.toFixed(1)} mrad`} width={180} />} - {mode === "nanobeam" && (v > 0 ? `${v.toFixed(2)}°` : "off")} width={180} />} + {mode !== "kossel" && (v > 0 ? `${v.toFixed(2)}°` : "off")} width={180} />} {mode !== "kossel" && `${v.toFixed(2)} Å⁻¹`} width={180} />} {mode === "kossel" && `${v.toFixed(0)} mrad`} width={180} />} {mode !== "kossel" && !dynamical && `${v.toFixed(3)} Å⁻¹`} width={180} />} @@ -777,8 +806,10 @@ function DiffSim() { {(energy / 1e3).toFixed(0)} keV · λ = {(crystal.wavelength * 100).toFixed(3)} pm · {nBeams} {mode === "kossel" ? "lines" : "beams"} {mode !== "kossel" && dynamical ? ` · ${nDyn} Bloch beams (|s| < ${SG_MAX} Å⁻¹${crystal.absorptive ? ", absorptive" : ""}), thin-slab intensities for the rest` : ""} + {mode !== "kossel" && precession > 0 + ? ` · precession ${precession.toFixed(2)}° over ${mode === "cbed" ? (cbed ? cbed.nodes.length : 0) : precNodes.length} ring nodes` + : ""} {mode === "cbed" ? " · disks summed incoherently where they overlap" : ""} - {mode === "nanobeam" && precession > 0 ? ` · precession ${precession.toFixed(2)}°, ${precNodes.length} ring nodes` : ""} {status ? ` · ${status}` : ""} drag the cell (near face follows) or the pattern (tilt map follows) · shift-drag or two fingers twist about the beam · double-click a disk for its two-beam condition, or empty space to put the Laue circle centre there · buttons rotate about the screen axes diff --git a/widget/js/diffsim/physics.ts b/widget/js/diffsim/physics.ts index 3ec544202..2b64fb0d5 100644 --- a/widget/js/diffsim/physics.ts +++ b/widget/js/diffsim/physics.ts @@ -149,20 +149,59 @@ function tiltedExcitation(r: Reflection, kz: number, tilt: [number, number]): nu return num / (2 * (kz - r.g[2])); } +/** |U_h| for an index difference, zero when the crystal carries no such factor. */ +function couplingMag(c: CrystalData, h: number, k: number, l: number): number { + const key = `${h},${k},${l}`; + return Math.hypot(c.couplingRe.get(key) ?? 0, c.couplingIm.get(key) ?? 0); +} + +const N_STRONG = 24; // candidates treated as the intermediate beams of a two-step path + +/** + * Rank candidates for the Bloch set, strongest first. + * + * Ranking by |U_g| alone drops exactly the reflections a dynamical + * calculation exists to show: silicon 002 and 222 have no structure factor + * of their own, so they score zero, yet they are the textbook example of a + * beam that fills by double diffraction and grows with thickness. A beam is + * worth keeping when it is strongly coupled to something that is itself + * strongly excited, so each candidate is scored by the largest coupling + * joining it either to the transmitted beam (|U_g|) or to one of the + * strongest candidates (|U_(g-h)|), divided by its excitation error. + */ +function rankCandidates(c: CrystalData, cand: { ref: Reflection; s: number }[]): { ref: Reflection; s: number }[] { + const direct = cand.map((x) => ({ + x, + v: Math.hypot(c.U_re[x.ref.index], c.U_im[x.ref.index]) / (Math.abs(x.s) + 1e-4), + })); + direct.sort((a, b) => b.v - a.v); + const strong = direct.slice(0, Math.min(N_STRONG, direct.length)).map((d) => d.x.ref.hkl); + const scored = cand.map((x) => { + let best = Math.hypot(c.U_re[x.ref.index], c.U_im[x.ref.index]); + for (const h of strong) { + const u = couplingMag(c, x.ref.hkl[0] - h[0], x.ref.hkl[1] - h[1], x.ref.hkl[2] - h[2]); + if (u > best) best = u; + } + return { x, v: best / (Math.abs(x.s) + 1e-4) }; + }); + scored.sort((a, b) => b.v - a.v); + return scored.map((sc) => sc.x); +} + /** * Beams entering the Bloch calculation: the direct beam plus every * reflection within sgMax of the Ewald sphere at zero tilt (sgMax widened * by the tilt range when a convergent beam is sampled), capped at maxBeams - * by keeping the beams with the largest |U_g| / |s_g|. + * by the ranking of rankCandidates. */ export function selectBeams(c: CrystalData, q: Quat, kMax: number, sgMax: number, maxBeams: number, tiltRange = 0): Reflection[] { const all = labReflections(c, q, kMax); const widen = tiltRange * kMax; // max |delta s| = sin(alpha) |g| let beams = all.filter((r) => Math.abs(r.s) < sgMax + widen); if (beams.length > maxBeams) { - const score = (r: Reflection) => Math.hypot(c.U_re[r.index], c.U_im[r.index]) / (Math.abs(r.s) + 1e-4); - beams.sort((a, b) => score(b) - score(a)); - beams = beams.slice(0, maxBeams); + beams = rankCandidates(c, beams.map((r) => ({ ref: r, s: r.s }))) + .slice(0, maxBeams) + .map((x) => x.ref); } return [DIRECT, ...beams]; } @@ -334,16 +373,15 @@ export function nanobeamSolve(c: CrystalData, q: Quat, kMax: number, sgMax: numb let nSum = 0; for (const tilt of tilts) { const kz = Math.sqrt(Math.max(k0 * k0 - tilt[0] ** 2 - tilt[1] ** 2, 1e-12)); - const cand: { i: number; s: number; score: number }[] = []; + let cand: { ref: Reflection; s: number; i: number }[] = []; for (let i = 0; i < all.length; i++) { const st = tiltedExcitation(all[i], kz, tilt); - if (Math.abs(st) < sgMax) { - const r = all[i]; - cand.push({ i, s: st, score: Math.hypot(c.U_re[r.index], c.U_im[r.index]) / (Math.abs(st) + 1e-4) }); - } + if (Math.abs(st) < sgMax) cand.push({ ref: all[i], s: st, i }); + } + if (cand.length > maxBeams) { + cand = rankCandidates(c, cand).slice(0, maxBeams) as typeof cand; } - if (cand.length > maxBeams) { cand.sort((a, b) => b.score - a.score); cand.length = maxBeams; } - const dyn = [DIRECT, ...cand.map((x) => all[x.i])]; + const dyn = [DIRECT, ...cand.map((x) => x.ref)]; const pos = new Int32Array(dyn.length); pos[0] = 0; cand.forEach((x, j) => { pos[j + 1] = x.i + 1; }); diff --git a/widget/src/quantem/widget/diffsim.py b/widget/src/quantem/widget/diffsim.py index 393cf04d5..6c36b4321 100644 --- a/widget/src/quantem/widget/diffsim.py +++ b/widget/src/quantem/widget/diffsim.py @@ -4,7 +4,7 @@ Left panel: the unit cell in 3D, rotated by dragging (mouse or touch) or by buttons about the screen axes. Right panel: the diffraction pattern of the same orientation, updated live: nanobeam (kinematical markers, or Bloch wave -intensities that follow the thickness slider), CBED disks, or Kossel / +intensities that follow the thickness slider), CBED disks, or the Kikuchi LACBED lines. Every simulation runs in the browser, so the widget can be exported as a single HTML file and embedded in a web page without Python. """ @@ -185,8 +185,8 @@ def _coupling_vector(crystal, hkl: torch.Tensor, gamma_rel: float) -> tuple[torc def prepare_kossel_reference( crystal, energy_ev: float, thicknesses_A, angle_step_mrad=3.0, k_max=1.0 ): - """Bright field Kossel reference on the Lambert grid, for the pixel - rendering of the Kossel / LACBED mode (a lookup in the browser).""" + """Bright field reference on the Lambert grid, for the pixel rendering + of the Kikuchi pattern mode (a lookup in the browser).""" from quantem.diffraction import bloch master = bloch.calculate_kossel_reference( @@ -233,7 +233,7 @@ class DiffractionSim(anywidget.AnyWidget): Scattering vector at the edge of the nanobeam / CBED panel (1/Angstroms); None uses 3, or k_max when that is smaller. field_mrad : float, default=50 - Half angle of the Kossel / LACBED field of view. + Half angle of the Kikuchi pattern field of view. sg_max : float, default=0.05 Excitation error cutoff (1/Angstroms) selecting the Bloch beams; reflections outside it take thin-slab intensities. @@ -258,7 +258,9 @@ class DiffractionSim(anywidget.AnyWidget): operator's view down the column; there the entrance face is nearest and tilts opposite to the pattern (the Laue center marks where the zone axis exits toward the detector). - mode : {"nanobeam", "cbed", "kossel"} + mode : {"nanobeam", "cbed", "kikuchi"} + Nanobeam spots, convergent-beam disks, or the wide-angle Kikuchi + pattern. "kossel" is accepted as the old name of "kikuchi". render : {"markers", "disks", "pixels"} Nanobeam: markers sized by intensity, disks of the convergence semiangle with brightness by intensity, or a pixelated pattern. @@ -296,6 +298,14 @@ class DiffractionSim(anywidget.AnyWidget): sync=True ) mode = traitlets.Unicode("nanobeam").tag(sync=True) + + @traitlets.validate("mode") + def _accept_old_mode_name(self, proposal): + # the wide-angle mode was called "kossel" before it took the name the + # EBSD community uses for the same bands + value = str(proposal["value"]) + return "kossel" if value == "kikuchi" else value + render = traitlets.Unicode("markers").tag(sync=True) dynamical = traitlets.Bool(True).tag(sync=True) thickness_A = traitlets.Float(500.0).tag(sync=True) @@ -394,8 +404,8 @@ def set_zone_axis(self, zone_axis, in_plane_deg: float = 0.0) -> "DiffractionSim def compute_kossel_reference( self, thicknesses_A=(300.0, 600.0, 1000.0), angle_step_mrad=3.0, k_max=1.0 ): - """Precompute the Kossel reference pattern for the pixel rendering of - the Kossel / LACBED mode (about a minute for silicon at 3 mrad).""" + """Precompute the reference pattern for the pixel rendering of the + Kikuchi pattern mode (about a minute for silicon at 3 mrad).""" key = ( round(self.energy_ev), tuple(float(t) for t in thicknesses_A), From eb6a79c9cf64c593355be609e0746c2ee41d22db Mon Sep 17 00:00:00 2001 From: cophus Date: Wed, 23 Sep 2026 13:57:58 -0700 Subject: [PATCH 12/36] fixes --- .../core/datastructures/dataset4dstem.py | 129 +++++- src/quantem/core/io/serialize.py | 26 +- src/quantem/diffraction/calibration.py | 379 +++++++++++++++++- 3 files changed, 524 insertions(+), 10 deletions(-) diff --git a/src/quantem/core/datastructures/dataset4dstem.py b/src/quantem/core/datastructures/dataset4dstem.py index 4a628eb7e..d95d8cd1c 100644 --- a/src/quantem/core/datastructures/dataset4dstem.py +++ b/src/quantem/core/datastructures/dataset4dstem.py @@ -568,7 +568,15 @@ def _create_annular_mask( return (distance >= r_inner) & (distance <= r_outer) - def show_virtual_images(self, figsize: tuple[int, int] | None = None, **kwargs) -> tuple: + def show_virtual_images( + self, + figsize: tuple[int, int] | None = None, + *, + positions: list[tuple[int, int]] | None = None, + position_color: str = "red", + position_size: float = 60.0, + **kwargs, + ) -> tuple: """ Display all virtual images stored in the dataset using show_2d. @@ -576,6 +584,13 @@ def show_virtual_images(self, figsize: tuple[int, int] | None = None, **kwargs) ---------- figsize : tuple[int, int] | None, optional Figure size in inches. If None, automatically calculated based on number of images + positions : list of tuple of int, optional + ``(row, col)`` scan positions to mark on every image, e.g. the + positions used to tune Bragg disk detection. + position_color : str, default="red" + Color of the position markers. + position_size : float, default=60.0 + Area of the position markers in points squared. **kwargs Additional keyword arguments passed to show_2d (e.g., cmap, norm, cbar, etc.) @@ -625,8 +640,120 @@ def show_virtual_images(self, figsize: tuple[int, int] | None = None, **kwargs) kwargs.setdefault("scalebar", [scalebar] + [False] * (len(arrays) - 1)) fig, axs = show_2d(arrays_organized, title=titles_organized, figsize=figsize, **kwargs) + if positions is not None and len(positions) > 0: + pos = np.asarray(positions, dtype=float).reshape(-1, 2) + for ax in np.atleast_1d(np.asarray(axs, dtype=object)).ravel(): + ax.scatter( + pos[:, 1], + pos[:, 0], + s=position_size, + facecolors="none", + edgecolors=position_color, + linewidths=1.5, + ) + return fig, axs + def show_virtual_detectors( + self, + names: str | list[str] | None = None, + *, + colors: list[str] | None = None, + alpha: float = 0.2, + linewidth: float = 1.5, + legend: bool = True, + **kwargs, + ) -> tuple: + """Show the mean diffraction pattern with the virtual detectors drawn on it. + + Every detector attached by :meth:`get_virtual_image` is drawn on a single + mean pattern, so their placement relative to the direct beam and the + diffracted rings can be checked at a glance. + + Parameters + ---------- + names : str or list of str, optional + Detector(s) to draw. ``None`` (default) draws all attached detectors. + colors : list of str, optional + One color per detector; defaults to the matplotlib color cycle. + alpha : float, default=0.2 + Opacity of the filled detector area. Set to 0 for outlines only. + linewidth : float, default=1.5 + Width of the detector outline. + legend : bool, default=True + If ``True``, label the detectors in a legend. + **kwargs + Passed to :func:`~quantem.core.visualization.show_2d`; ``norm``, + ``scalebar`` and ``title`` have diffraction-pattern defaults. + + Returns + ------- + tuple + ``(fig, ax)`` from :func:`~quantem.core.visualization.show_2d`. + """ + if not self._virtual_detectors: + raise ValueError("No virtual detectors attached. Create one with get_virtual_image().") + if names is None: + names = list(self._virtual_detectors) + elif isinstance(names, str): + names = [names] + missing = [n for n in names if n not in self._virtual_detectors] + if missing: + raise ValueError( + f"Virtual detector(s) {missing} not found. " + f"Available detectors: {list(self._virtual_detectors)}" + ) + + if colors is None: + cycle = plt.rcParams["axes.prop_cycle"].by_key().get("color", ["red"]) + colors = [cycle[i % len(cycle)] for i in range(len(names))] + + dp_mean = self.dp_mean + kwargs.setdefault("norm", {"power": 0.4, "upper_quantile": 0.999}) + kwargs.setdefault("title", "mean DP with virtual detectors") + kwargs.setdefault( + "scalebar", ScalebarConfig(sampling=self.sampling[2], units=self.units[2]) + ) + fig, ax = show_2d(dp_mean.array, **kwargs) + + for name, color in zip(names, colors): + det = self._virtual_detectors[name] + mode, geometry, mask = det["mode"], det["geometry"], det["mask"] + if mask is not None: + ax.contour(mask, levels=[0.5], colors=[color], linewidths=linewidth) + ax.plot([], [], color=color, linewidth=linewidth, label=name) + continue + (cy, cx) = geometry[0] + if mode == "circle": + radius = geometry[1] + ax.add_patch( + Circle((cx, cy), radius, color=color, fill=True, alpha=alpha, label=name) + ) + ax.add_patch( + Circle((cx, cy), radius, color=color, fill=False, linewidth=linewidth) + ) + elif mode == "annular": + r_inner, r_outer = geometry[1] + ax.add_patch( + Wedge( + (cx, cy), + r_outer, + 0, + 360, + width=r_outer - r_inner, + color=color, + fill=True, + alpha=alpha, + label=name, + ) + ) + for r in (r_inner, r_outer): + ax.add_patch(Circle((cx, cy), r, color=color, fill=False, linewidth=linewidth)) + + if legend: + ax.legend(loc="upper right", framealpha=0.8) + return fig, ax + def regenerate_virtual_images(self) -> None: """ Regenerate virtual images from stored detector information. diff --git a/src/quantem/core/io/serialize.py b/src/quantem/core/io/serialize.py index 8aa0bea8c..9e9715624 100644 --- a/src/quantem/core/io/serialize.py +++ b/src/quantem/core/io/serialize.py @@ -171,6 +171,16 @@ def _convert_string_to_path_if_needed(val: Any, group: zarr.Group, key: str) -> return val return val + @staticmethod + def _convert_string_to_device_if_needed(val: Any, group: zarr.Group, key: str) -> Any: + """Convert string back to torch.device if it was originally a device.""" + if isinstance(val, str) and group.attrs.get(f"{key}.is_torch_device", False): + try: + return torch.device(val) + except (ValueError, RuntimeError): + return val + return val + @staticmethod def _is_autoserialize_instance(value: Any) -> bool: """Return True if value behaves like an AutoSerialize instance, even across autoreloads.""" @@ -406,6 +416,12 @@ def _serialize_value( group.attrs[name] = str(value) group.attrs[f"{name}.is_path"] = True + elif isinstance(value, torch.device): + # A device belongs to the machine, not to the data: store the string + # so the object reloads on a host that does not have that device. + group.attrs[name] = str(value) + group.attrs[f"{name}.is_torch_device"] = True + elif self._is_autoserialize_instance(value): # Nested AutoSerialize subtree subgroup = group.require_group(name) @@ -522,6 +538,7 @@ def _recursive_load( name == "_autoserialize" or name.endswith(".torch_save") or name.endswith(".is_path") + or name.endswith(".is_torch_device") ): continue # Skip metadata/flags if name in skip_names: @@ -531,6 +548,7 @@ def _recursive_load( # Convert string paths back to pathlib.Path objects if needed val = cls._convert_string_to_path_if_needed(val, group, name) + val = cls._convert_string_to_device_if_needed(val, group, name) setattr(obj, name, val) set_attrs.add(name) @@ -869,6 +887,7 @@ def maybe_tensor(group, key): val = group.attrs[key] # Convert string paths back to Path objects if needed val = cls._convert_string_to_path_if_needed(val, group, key) + val = cls._convert_string_to_device_if_needed(val, group, key) items.append(val) elif key in group.array_keys(): items.append(maybe_tensor(group, key)) @@ -984,6 +1003,7 @@ def maybe_tensor(group, key): val = group.attrs[key] # Convert string paths back to Path objects if needed val = cls._convert_string_to_path_if_needed(val, group, key) + val = cls._convert_string_to_device_if_needed(val, group, key) items.append(val) elif key in group.array_keys(): items.append(maybe_tensor(group, key)) @@ -1073,11 +1093,13 @@ def maybe_tensor(group, key): key == "_container_type" or key.endswith(".torch_save") or key.endswith(".is_path") + or key.endswith(".is_torch_device") ): continue val = group.attrs[key] # Convert string paths back to Path objects if needed val = cls._convert_string_to_path_if_needed(val, group, key) + val = cls._convert_string_to_device_if_needed(val, group, key) result[key] = val # Restore arrays (including torch tensors) for key in group.array_keys(): @@ -1514,8 +1536,6 @@ def __init__(self, **objects): def __repr__(self) -> str: items = ", ".join( - f"{k}: {type(v).__name__}" - for k, v in vars(self).items() - if not k.startswith("_") + f"{k}: {type(v).__name__}" for k, v in vars(self).items() if not k.startswith("_") ) return f"Bundle({items})" diff --git a/src/quantem/diffraction/calibration.py b/src/quantem/diffraction/calibration.py index 2817a9f47..00ac8d986 100644 --- a/src/quantem/diffraction/calibration.py +++ b/src/quantem/diffraction/calibration.py @@ -7,9 +7,12 @@ from __future__ import annotations +import warnings + import numpy as np from quantem.core.datastructures.vector import Vector +from quantem.core.io.serialize import AutoSerialize from quantem.diffraction.crystal import Crystal from quantem.diffraction.defaults import MIN_NUMBER_PEAKS @@ -452,7 +455,7 @@ def calibrate_pixel_size( label=f"{crystal.name} rings", ) ax.set_ylabel("intensity (norm.)") - ax.set_xlabel("scattering vector (1/$\\mathrm{\\AA}$)") + ax.set_xlabel(r"scattering vector (1/$\mathrm{\AA}$)") ax.set_title(f"1D radial fit, scale = {scale:.4f}", fontsize=10) ax.legend(loc="upper right", fontsize=9) if returnfig: @@ -637,7 +640,7 @@ def cost(e): fig, ax = plt.subplots(figsize=(10, 4)) ax.fill_between(k_bins, h0 / h0.max(), color="0.7", lw=0, label="measured") ax.plot(k_bins, h1 / h1.max(), "r-", lw=1.0, label="ellipse corrected") - ax.set_xlabel("scattering vector (1/$\\mathrm{\\AA}$)") + ax.set_xlabel(r"scattering vector (1/$\mathrm{\AA}$)") ax.set_ylabel("intensity (norm.)") mag = np.hypot(*ellipse) ax.set_title( @@ -674,6 +677,369 @@ def digit(v: int) -> str: return "(" + digit(h) + digit(k) + digit(ll) + ")" +def _crystal_rings(crystal: Crystal, k_min: float, k_max: float) -> np.ndarray: + """Distinct ring radii of a crystal with non-zero structure factor.""" + g = np.linalg.norm(np.asarray(crystal.g_vec), axis=1) + f = np.asarray(crystal.struct_factors_int) + keep = (g > k_min) & (g < k_max) & (f > 1e-6 * f.max()) + return np.array(sorted(set(np.round(g[keep], 4)))) + + +def _histogram_maxima(peaks, k_min: float, k_max: float, bragg_k_power: float) -> np.ndarray: + """Radii of the local maxima of the measured radial histogram.""" + from scipy.ndimage import gaussian_filter1d, maximum_filter1d + + k, hist = radial_histogram(peaks, k_min=k_min, k_max=k_max, bragg_k_power=bragg_k_power) + h = gaussian_filter1d(hist, 2.0) + loc = (h == maximum_filter1d(h, 15)) & (h > 0.05 * h.max()) + return k[loc] + + +class DiffractionCalibration(AutoSerialize): + """Reciprocal-space calibration of a detector, measured once and reused. + + Holds the reciprocal pixel size, the elliptic distortion and the + diffraction-to-scan rotation, with the evidence behind them. Strained + samples cannot calibrate themselves, so the normal route is to measure + this on a standard such as nanocrystalline gold, save it, and apply it + to the peaks of the sample of interest:: + + cal = calibrate(peaks_au, gold, 0.01) + cal.save("detector_300kV_80cm.zip", mode="o") + ... + cal = load("detector_300kV_80cm.zip") + peaks = cal.apply(bv_centered.peaks) + + A calibration is tied to the detector binning it was measured at, which + is recorded in `metadata`; `rebin` converts it to another binning. + """ + + def __init__( + self, + pixel_size: float, + ellipse=None, + rotation_ccw_deg: float = 0.0, + metadata: dict | None = None, + ): + self.pixel_size = float(pixel_size) + self.ellipse = None if ellipse is None else np.asarray(ellipse, dtype=float) + self.rotation_ccw_deg = float(rotation_ccw_deg) + self.metadata: dict = dict(metadata or {}) + + def apply(self, peaks_px, name: str = "bragg_peaks_calibrated"): + """Calibrated (qx, qy) peaks from origin-corrected pixel peaks.""" + return peaks_to_calibrated( + peaks_px, + self.pixel_size, + rotation_ccw_deg=self.rotation_ccw_deg, + ellipse=self.ellipse, + name=name, + ) + + def rebin(self, factor: float) -> "DiffractionCalibration": + """The same calibration for data binned by `factor` more than this one.""" + md = dict(self.metadata) + md["binning"] = md.get("binning", 1) * factor + return DiffractionCalibration( + self.pixel_size * factor, self.ellipse, self.rotation_ccw_deg, md + ) + + def __repr__(self) -> str: + e = "none" if self.ellipse is None else "e11 %+.5f e12 %+.5f" % tuple(self.ellipse) + rms = self.metadata.get("residual_rms") + q = ( + "" + if rms is None + else ", %d rings, rms %.2f%%" + % ( + self.metadata.get("n_rings", 0), + 100 * rms, + ) + ) + return ( + f"DiffractionCalibration(pixel_size={self.pixel_size:.6f} 1/A/px, " + f"ellipse: {e}, rotation {self.rotation_ccw_deg:g} deg{q})" + ) + + +def _ring_profile_scores( + peaks, + crystals, + scales: np.ndarray, + k_broadening: float, + k_min: float, + k_max: float, + bragg_k_power: float, +) -> np.ndarray: + """Normalized overlap of the measured rings with the reference rings, for + each trial scale. + + The score is the fraction of the total measured peak weight that lands on + a reference ring. Two normalizations matter and both are traps. Scaling + the reference rather than the data moves the comparison window with the + scale, and re-normalizing to the weight left inside the window rewards a + scale for pushing peaks out of it: either one lets a wrong scale beat the + truth. Dividing by the total weight, counted once and independent of the + scale, makes a lost peak a loss. + """ + flat = peaks.select_fields("qx", "qy", "intensity").flatten() + qr = np.hypot(flat[:, 0], flat[:, 1]) + weight = flat[:, 2] * qr**bragg_k_power + k_step = 0.002 + k = np.arange(k_min, k_max, k_step) + prof = np.zeros_like(k) + for xtl in crystals: + prof += simulated_ring_profile(xtl, k, k_broadening, bragg_k_power) + prof = prof / max(prof.max(), 1e-12) + total = float(weight.sum()) + scores = np.zeros_like(scales) + for i, sc in enumerate(scales): + frac = (qr * sc - k_min) / k_step + i0 = np.floor(frac).astype(int) + w1 = frac - i0 + ok = (i0 >= 0) & (i0 < k.size - 1) + h = np.bincount(i0[ok], weights=weight[ok] * (1 - w1[ok]), minlength=k.size) + h += np.bincount(i0[ok] + 1, weights=weight[ok] * w1[ok], minlength=k.size) + scores[i] = float((h[: k.size] * prof).sum() / total) if total > 0 else 0.0 + return scores + + +def _fit_scale(peaks, crystals, lo, hi, k_broadening, k_min, k_max, bragg_k_power, n=241): + """Best scale in [lo, hi] with parabolic refinement of the maximum.""" + scales = np.linspace(lo, hi, n) + scores = _ring_profile_scores( + peaks, crystals, scales, k_broadening, k_min, k_max, bragg_k_power + ) + i = int(np.argmax(scores)) + best = float(scales[i]) + if 0 < i < n - 1: + c0, c1, c2 = scores[i - 1 : i + 2] + denom = 4 * c1 - 2 * c0 - 2 * c2 + if abs(denom) > 1e-12: + best += (c2 - c0) / denom * (scales[1] - scales[0]) + return best, scales, scores + + +def calibrate( + peaks_px, + crystal, + pixel_size_guess: float, + rotation_ccw_deg: float = 0.0, + fit_ellipse: bool = True, + n_iter: int = 3, + scale_search: tuple[float, float] = (0.6, 1.7), + k_min: float = 0.05, + k_max: float = 1.3, + k_broadening: float = 0.01, + bragg_k_power: float = 2.0, + residual_tol: float = 0.01, + plot: bool = True, + figsize: tuple[float, float] = (13.0, 6.4), + marker_size: float = 8.0, + returnfig: bool = False, +): + """Measure the reciprocal pixel size and the elliptic distortion. + + Three stages, each one removing the reason the next could fail. A coarse + scan over `scale_search` with a deliberately broadened ring profile finds + the right ring assignment even when the starting pixel size is far out; + a broad profile has one maximum where a sharp one has many. The ellipse + and the scale are then refined in turn, the ellipse in log-radius bins + where it does not depend on the scale, and the scale against + progressively sharper rings. Finally every measured ring is matched to + its reference ring separately, which is the only check that can tell a + correct calibration from a plausible one: a single pixel size that + explains the pattern gives per-ring scale factors agreeing to a few + tenths of a percent with no trend in k. + + Parameters + ---------- + peaks_px : Vector + Origin-corrected peaks in detector pixels, (q_row, q_col, intensity). + crystal : Crystal | list[Crystal] + Reference phase or phases, structure factors calculated. + pixel_size_guess : float + Starting reciprocal pixel size (1/Angstroms per pixel). Only the + order of magnitude matters; the coarse scan covers `scale_search`. + rotation_ccw_deg : float, default=0.0 + Diffraction-to-scan rotation recorded on the calibration. + fit_ellipse : bool, default=True + Fit the elliptic distortion as well as the scale. + n_iter : int, default=3 + Ellipse and scale refinement rounds. + scale_search : tuple, default=(0.6, 1.7) + Capture range of the coarse scan, as a multiple of the guess. + residual_tol : float, default=0.01 + Per-ring residual rms above which the fit is reported as unreliable. + figsize : tuple, default=(13, 6.4) + Figure size. + marker_size : float, default=8.0 + Area of the brightest peak in the azimuth panels, where every peak is + drawn with area proportional to its intensity. Raise it to bring out + weak spots, lower it when strong ones hide the reference lines. + + Returns + ------- + DiffractionCalibration + """ + crystals = [crystal] if isinstance(crystal, Crystal) else list(crystal) + for xtl in crystals: + if xtl.g_vec is None: + raise RuntimeError(f"{xtl.name}: run calculate_structure_factors() first") + + peaks_0 = peaks_to_calibrated(peaks_px, pixel_size_guess) + # coarse: broad rings so the score has a single maximum over a wide range + scale, sc_coarse, score_coarse = _fit_scale( + peaks_0, + crystals, + scale_search[0], + scale_search[1], + 6 * k_broadening, + k_min, + k_max, + bragg_k_power, + ) + ellipse = None + for it in range(max(1, n_iter)): + if fit_ellipse: + pk = peaks_to_calibrated(peaks_px, pixel_size_guess * scale, ellipse=ellipse) + ellipse = calibrate_ellipse( + pk, k_min=max(k_min, 0.15), k_max=k_max, bragg_k_power=bragg_k_power + ) + pk = peaks_to_calibrated(peaks_px, pixel_size_guess, ellipse=ellipse) + half = 0.08 / (it + 1) + scale, sc_fine, score_fine = _fit_scale( + pk, + crystals, + scale * (1 - half), + scale * (1 + half), + k_broadening, + k_min, + k_max, + bragg_k_power, + ) + + pixel_size = pixel_size_guess * scale + peaks = peaks_to_calibrated( + peaks_px, pixel_size, rotation_ccw_deg=rotation_ccw_deg, ellipse=ellipse + ) + + rings = np.concatenate([_crystal_rings(x, k_min, k_max * 1.15) for x in crystals]) + rings = np.array(sorted(set(np.round(rings, 4)))) + k_meas = _histogram_maxima(peaks, k_min, k_max * 1.15, bragg_k_power) + table = [] + for km in k_meas: + j = int(np.argmin(np.abs(rings - km))) + if abs(rings[j] - km) < 4 * k_broadening: + table.append((float(km), float(rings[j]), float(rings[j] / km))) + per_ring = np.array([t[2] for t in table]) if table else np.array([np.nan]) + residual_rms = float(np.std(per_ring / np.median(per_ring))) if table else float("nan") + reliable = len(table) >= 3 and residual_rms <= residual_tol + + cal = DiffractionCalibration( + pixel_size, + ellipse, + rotation_ccw_deg, + metadata=dict( + reference=[x.name for x in crystals], + pixel_size_guess=float(pixel_size_guess), + scale=float(scale), + n_rings=len(table), + residual_rms=residual_rms, + rings=table, + reliable=bool(reliable), + binning=1, + ), + ) + if not reliable: + warnings.warn( + f"calibration looks unreliable: {len(table)} rings matched, residual rms " + f"{100 * residual_rms:.2f}% (tolerance {100 * residual_tol:.2f}%). Check the " + "reference phase and the peak detection before using this pixel size.", + stacklevel=2, + ) + if not plot: + return cal + + import matplotlib.pyplot as plt + + k_hi = k_max * 1.15 + fig, axs = plt.subplots( + 2, + 2, + figsize=figsize, + sharex="col", + gridspec_kw={"height_ratios": [1, 1.3]}, + ) + rings_ref = _crystal_rings(crystals[0], max(k_min, 0.12), k_hi) + + for col, (pk, ttl) in enumerate( + ( + (peaks_0, f"before: {pixel_size_guess:.5f} " + r"$\mathrm{\AA}^{-1}$/px"), + (peaks, f"after: {pixel_size:.5f} " + r"$\mathrm{\AA}^{-1}$/px"), + ) + ): + plot_ring_comparison( + pk, + crystals, + k_min=k_min, + k_max=k_hi, + k_broadening=None if col == 0 else k_broadening, + bragg_k_power=bragg_k_power, + figax=(fig, [axs[0, col]]), + ) + axs[0, col].set_title(ttl, fontsize=10) + axs[0, col].set_xlabel("") + + # azimuth against scattering vector: the elliptic distortion is the + # cos(2 phi) wobble of every ring, read off against the black lines + ax = axs[1, col] + flat = pk.select_fields("qx", "qy", "intensity").flatten() + r = np.hypot(flat[:, 0], flat[:, 1]) + phi = np.degrees(np.arctan2(flat[:, 1], flat[:, 0])) + sel = (r > k_min) & (r < k_hi) + w = flat[sel, 2] + hi = float(np.percentile(w, 99.5)) if w.size else 1.0 + ax.scatter( + r[sel], + phi[sel], + s=marker_size * np.clip(w / max(hi, 1e-12), 0.03, 1.0), + c="r", + alpha=0.5, + lw=0, + rasterized=True, + ) + for g0 in rings_ref: + ax.axvline(g0, color="k", lw=0.7, alpha=0.8) + ax.set_xlim(k_min, k_hi) + ax.set_ylim(-180, 180) + ax.set_yticks([-180, -90, 0, 90, 180]) + ax.set_xlabel(r"scattering vector (1/$\mathrm{\AA}$)") + axs[1, 0].set_ylabel("azimuth (deg)") + if ellipse is not None: + axs[1, 0].text( + 0.02, + 0.97, + "ellipse e11 %+.4f e12 %+.4f" % tuple(ellipse), + transform=axs[1, 0].transAxes, + va="top", + fontsize=9, + ) + axs[1, 1].text( + 0.02, + 0.97, + f"{len(table)} rings, rms {100 * residual_rms:.2f}%" + + ("" if reliable else " UNRELIABLE"), + transform=axs[1, 1].transAxes, + va="top", + fontsize=9, + color="k" if reliable else "tab:red", + ) + fig.tight_layout() + fig.subplots_adjust(hspace=0.08) + return (cal, fig, axs) if returnfig else cal + + def plot_ring_comparison( peaks, crystals, @@ -772,8 +1138,9 @@ def plot_ring_comparison( ax.set_ylabel("intensity (norm.)") ax.set_ylim(0, 1.32) ax.legend(loc="upper right", fontsize=9) - axs[-1].set_xlabel("scattering vector (1/$\\mathrm{\\AA}$)") - fig.tight_layout() + axs[-1].set_xlabel(r"scattering vector (1/$\mathrm{\AA}$)") + if figax is None: + fig.tight_layout() return fig, axs @@ -925,7 +1292,7 @@ def plot_bragg_rings( alpha=0.6, label=f"{xtl.name} rings" if k == 0 else None, ) - ax.set_xlabel("$q_c$ (1/$\\mathrm{\\AA}$)") - ax.set_ylabel("$q_r$ (1/$\\mathrm{\\AA}$)") + ax.set_xlabel(r"$q_c$ (1/$\mathrm{\AA}$)") + ax.set_ylabel(r"$q_r$ (1/$\mathrm{\AA}$)") ax.legend(loc="upper right", fontsize=9) return fig, ax From e356c319b0d3e5df8f066b9d5255d81f48f0fee3 Mon Sep 17 00:00:00 2001 From: cophus Date: Wed, 23 Sep 2026 17:50:24 -0700 Subject: [PATCH 13/36] many fixes --- src/quantem/core/io/serialize.py | 31 +- .../core/visualization/visualization_utils.py | 4 +- src/quantem/diffraction/__init__.py | 1 + src/quantem/diffraction/bragg_vectors.py | 169 +++++- src/quantem/diffraction/calibration.py | 26 +- src/quantem/diffraction/crystal.py | 3 +- src/quantem/diffraction/crystal_map.py | 567 ++++++++++++++++++ src/quantem/diffraction/orientation.py | 293 ++++++--- .../diffraction/orientation_visualization.py | 95 ++- src/quantem/diffraction/phase.py | 169 +++++- 10 files changed, 1240 insertions(+), 118 deletions(-) create mode 100644 src/quantem/diffraction/crystal_map.py diff --git a/src/quantem/core/io/serialize.py b/src/quantem/core/io/serialize.py index 9e9715624..0bd19d935 100644 --- a/src/quantem/core/io/serialize.py +++ b/src/quantem/core/io/serialize.py @@ -461,6 +461,20 @@ def _serialize_value( subgroup.attrs["_rng_type"] = "torch.Generator" # Don't try to save the state - it's not essential for core functionality + elif type(value).__module__.startswith("ase.") and type(value).__name__ == "Atoms": + # An ase.Atoms is fully defined by these four arrays; storing them + # keeps the file readable and avoids pickling an ase version in. + subgroup = group.require_group(name) + subgroup.attrs["_ase_atoms"] = True + self._write_ndarray( + subgroup, "numbers", np.asarray(value.get_atomic_numbers()), compressors + ) + self._write_ndarray( + subgroup, "positions", np.asarray(value.get_positions()), compressors + ) + self._write_ndarray(subgroup, "cell", np.asarray(value.get_cell()), compressors) + self._write_ndarray(subgroup, "pbc", np.asarray(value.get_pbc()), compressors) + else: # Fallback: dill-serialize + gzip-compress print(f"falling back in serialize for {name} of type {type(value)}") @@ -576,8 +590,23 @@ def _recursive_load( continue subgrp = AutoSerialize._get_group(group, name) + # ase.Atoms group + if subgrp.attrs.get("_ase_atoms"): + from ase import Atoms + + atoms = Atoms( + numbers=AutoSerialize._read_array_np(subgrp, "numbers"), + positions=AutoSerialize._read_array_np(subgrp, "positions"), + cell=AutoSerialize._read_array_np(subgrp, "cell"), + pbc=AutoSerialize._read_array_np(subgrp, "pbc"), + ) + if type(atoms) in skip_types: + continue + setattr(obj, name, atoms) + set_attrs.add(name) + # torch tensor group - if subgrp.attrs.get("_torch_tensor"): + elif subgrp.attrs.get("_torch_tensor"): data = AutoSerialize._read_array_np(subgrp, "tensor").tobytes() buf = io.BytesIO(data) tensor = torch.load(buf, map_location="cpu", weights_only=False) diff --git a/src/quantem/core/visualization/visualization_utils.py b/src/quantem/core/visualization/visualization_utils.py index 8e32bcc5f..c595eadd7 100644 --- a/src/quantem/core/visualization/visualization_utils.py +++ b/src/quantem/core/visualization/visualization_utils.py @@ -465,7 +465,9 @@ def add_cbar_to_ax( formatter = ticker.ScalarFormatter(useMathText=True) formatter.set_scientific(True) - formatter.set_powerlimits((-1, 1)) + # only fall back to a shared exponent for genuinely extreme ranges: a + # correlation running 0 to 0.8 should read 0.0, 0.2, ... not 0, 2 x 10^-1 + formatter.set_powerlimits((-3, 4)) sm = cm.ScalarMappable(norm=norm, cmap=cmap) cb = fig.colorbar(sm, cax=cax, ticks=ticks, format=formatter) diff --git a/src/quantem/diffraction/__init__.py b/src/quantem/diffraction/__init__.py index dce2f8608..6caed1891 100644 --- a/src/quantem/diffraction/__init__.py +++ b/src/quantem/diffraction/__init__.py @@ -1,5 +1,6 @@ from quantem.diffraction.bragg_vectors import BraggVectors as BraggVectors from quantem.diffraction.crystal import Crystal as Crystal +from quantem.diffraction.crystal_map import CrystalMap as CrystalMap from quantem.diffraction.orientation import OrientationMap as OrientationMap from quantem.diffraction.phase import PhaseMap as PhaseMap from quantem.diffraction.strain import StrainMap as StrainMap diff --git a/src/quantem/diffraction/bragg_vectors.py b/src/quantem/diffraction/bragg_vectors.py index 6b535b7fe..156a93d4b 100644 --- a/src/quantem/diffraction/bragg_vectors.py +++ b/src/quantem/diffraction/bragg_vectors.py @@ -93,6 +93,7 @@ def __init__( self.device = device self.peaks: Vector | None = None + self.calibration = None self.bvm: Dataset2d | None = None self.origin: np.ndarray | None = None @@ -586,6 +587,10 @@ def correct_peak_origins( bv.metadata["origin_correction"] = { "origin_ref": (float(origin_ref[0]), float(origin_ref[1])), } + # the fitted origins travel with the peaks: plots that put a pattern + # behind the peaks need them to line the two up + peaks.metadata["origins"] = origins + peaks.metadata["origin_ref"] = (float(origin_ref[0]), float(origin_ref[1])) bv.compute_bvm() return bv @@ -1364,19 +1369,163 @@ def peak_histogram(self, *, returnfig: bool = False, **kwargs): # ---- helpers ---- + # ------------------------------------------------------------------ + # detector calibration + # ------------------------------------------------------------------ + + def measure_origins(self, search_radius: float = 6.0, robust: bool = True, plot: bool = False): + """Fit the diffraction origin at every probe position. + + The direct beam wanders with the probe (descan). The brightest peak + within `search_radius` of the detector centre gives the origin at + each position, and a plane fit over the scan smooths the result. + + Parameters + ---------- + search_radius : float, default=6.0 + Radius in detector pixels searched around the centre. + robust : bool, default=True + Reject outliers before the plane fit. + plot : bool, default=False + Show the measured and fitted origins. + + Returns + ------- + np.ndarray + ``(scan_row, scan_col, 2)`` origins in detector pixels. + """ + from quantem.diffraction import calibration + + return calibration.measure_origins( + self, search_radius=search_radius, robust=robust, plot=plot + ) + + def plot_origin_fit(self, origins, search_radius: float = 6.0): + """Measured origins, the plane fit, and their residual. + + Parameters + ---------- + origins : np.ndarray + ``(scan_row, scan_col, 2)`` origins from :meth:`measure_origins`. + search_radius : float, default=6.0 + The radius used to measure them, drawn for reference. + + Returns + ------- + tuple + ``(fig, axs)``. + """ + from quantem.diffraction import calibration + + return calibration.plot_origin_fit(self, origins, search_radius=search_radius) + + def calibrate(self, crystal, pixel_size_guess: float, **kwargs): + """Measure the reciprocal pixel size and the elliptic distortion. + + Matches the radial distribution of the detected peaks against the + ring positions of a known crystal: a coarse scan over broadened + rings fixes the ring assignment even from a poor starting guess, + then the ellipse and the scale are refined in turn, and every ring is + checked separately. The result carries the evidence behind it and can + be saved and applied to another dataset, which is the route for a + strained sample: measure on a standard such as nanocrystalline gold, + save, and apply there. + + Call this on origin-corrected peaks -- the rings must be concentric + before their radii mean anything. + + Parameters + ---------- + crystal : Crystal or list of Crystal + Reference structure(s) with structure factors calculated. + pixel_size_guess : float + Starting reciprocal pixel size, 1/Angstroms per detector pixel. + Recovered from a factor of two out in either direction. + rotation_ccw_deg : float, default=0.0 + Diffraction-to-scan rotation, recorded for later use. + plot : bool, default=True + Show the ring comparison before and after. + **kwargs + Further arguments of + :func:`~quantem.diffraction.calibration.calibrate`, e.g. + `fit_ellipse`, `k_min`, `k_max`, `marker_size` and `figsize`. + + Returns + ------- + DiffractionCalibration + The measured calibration, also stored on :attr:`calibration`. + Save it with ``cal.save(path)`` and reload it with + :func:`~quantem.core.io.serialize.load`. + + Raises + ------ + ValueError + If no peaks have been detected. + """ + from quantem.diffraction import calibration as _cal + + if self.peaks is None: + raise ValueError("Run detect_disks() before calibrate().") + cal = _cal.calibrate(self.peaks, crystal, pixel_size_guess, **kwargs) + self.calibration = cal + return cal + + def apply_calibration(self, calibration=None, name: str = "bragg_peaks_calibrated"): + """Calibrated peaks in 1/Angstroms from the detected pixel peaks. + + Parameters + ---------- + calibration : DiffractionCalibration, optional + The calibration to apply. None (default) uses the one measured by + :meth:`calibrate`, so a calibration loaded from a standard is + passed here instead. + name : str, default="bragg_peaks_calibrated" + Name of the returned Vector. + + Returns + ------- + Vector + Peaks in 1/Angstroms, carrying the pixel size, the fitted origins + and the scan calibration, so plots downstream need none of them + passed. + + Raises + ------ + ValueError + If no peaks have been detected, or no calibration is available. + """ + if self.peaks is None: + raise ValueError("Run detect_disks() before apply_calibration().") + cal = calibration if calibration is not None else getattr(self, "calibration", None) + if cal is None: + raise ValueError( + "No calibration available: run calibrate() first, or pass one loaded from a standard." + ) + return cal.apply(self.peaks, name=name) + def _scan_calibration(self) -> dict: - """Scan step and units of the dataset, to travel with the peaks. + """Calibration of the dataset, to travel with the peaks. - Everything downstream draws its real-space scale bar from this, so - the step size is set once on the dataset rather than passed to every - plot. A dataset still in pixels records nothing. + The scan step and the detector pixel size are properties of the + measurement, so they are set once on the dataset and carried by the + peaks rather than passed to every plot. Axes still in pixels record + nothing for that half. """ - sampling = np.atleast_1d(np.asarray(self.dataset.sampling, dtype=float))[:2] - units = list(self.dataset.units)[:2] - unit = str(units[0]).strip("b'\"") if units else "" - if unit.lower() in ("pixels", "px", "pixel", ""): - return {} - return {"scan_sampling": tuple(float(v) for v in sampling), "scan_units": unit} + out: dict = {} + sampling = np.atleast_1d(np.asarray(self.dataset.sampling, dtype=float)) + units = [str(u).strip("b'\"") for u in self.dataset.units] + + scan_unit = units[0] if units else "" + if scan_unit.lower() not in ("pixels", "px", "pixel", ""): + out["scan_sampling"] = tuple(float(v) for v in sampling[:2]) + out["scan_units"] = scan_unit + + if sampling.size >= 4 and len(units) >= 4: + dp_unit = units[2] + if dp_unit.lower() not in ("pixels", "px", "pixel", ""): + out["pixel_size"] = float(sampling[2]) + out["pixel_size_units"] = dp_unit + return out def _resolve_background_sigma(self, background_sigma: float | str | None) -> float | None: """Resolve the ``background_sigma`` argument to a value in pixels. diff --git a/src/quantem/diffraction/calibration.py b/src/quantem/diffraction/calibration.py index 00ac8d986..ab7f770bb 100644 --- a/src/quantem/diffraction/calibration.py +++ b/src/quantem/diffraction/calibration.py @@ -210,7 +210,7 @@ def peaks_to_calibrated( # carry the scan calibration through, so maps keep their scale bar, and # record the detector-to-scan rotation so pattern-overlay plots can put # peaks back into the raw detector frame - for key in ("scan_sampling", "scan_units"): + for key in ("scan_sampling", "scan_units", "origins", "origin_ref"): if key in (peaks_px.metadata or {}): out.metadata[key] = peaks_px.metadata[key] out.metadata["rotation_ccw_deg"] = float(rotation_ccw_deg) @@ -727,14 +727,34 @@ def __init__( self.metadata: dict = dict(metadata or {}) def apply(self, peaks_px, name: str = "bragg_peaks_calibrated"): - """Calibrated (qx, qy) peaks from origin-corrected pixel peaks.""" - return peaks_to_calibrated( + """Calibrated (qx, qy) peaks from origin-corrected pixel peaks. + + Parameters + ---------- + peaks_px : Vector + Origin-corrected peaks in detector pixels, from + :meth:`~quantem.diffraction.BraggVectors.correct_peak_origins`. + name : str, default="bragg_peaks_calibrated" + Name of the returned Vector. + + Returns + ------- + Vector + Peaks in 1/Angstroms, carrying the measured pixel size and the + scan calibration forward so plots downstream need neither passed + to them. + """ + out = peaks_to_calibrated( peaks_px, self.pixel_size, rotation_ccw_deg=self.rotation_ccw_deg, ellipse=self.ellipse, name=name, ) + # the measured pixel size replaces whatever the dataset was carrying + out.metadata["pixel_size"] = float(self.pixel_size) + out.metadata["pixel_size_units"] = "A^-1" + return out def rebin(self, factor: float) -> "DiffractionCalibration": """The same calibration for data binned by `factor` more than this one.""" diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py index 4d95aeafe..d34bf6750 100644 --- a/src/quantem/diffraction/crystal.py +++ b/src/quantem/diffraction/crystal.py @@ -27,6 +27,7 @@ from ase import Atoms from ase.data import chemical_symbols +from quantem.core.io.serialize import AutoSerialize from quantem.diffraction.defaults import SIGMA_EXCITATION from quantem.diffraction.rotations import qrotate, symmetry_quaternions @@ -175,7 +176,7 @@ def electron_scattering_factor(numbers: torch.Tensor, g: torch.Tensor) -> torch. return (a * (2.0 + b * g2) / (1.0 + b * g2) ** 2).sum(dim=-1) -class Crystal: +class Crystal(AutoSerialize): """A crystal structure with kinematical diffraction methods. Build with `from_ase` or `from_cif`, then call diff --git a/src/quantem/diffraction/crystal_map.py b/src/quantem/diffraction/crystal_map.py new file mode 100644 index 000000000..6f5f0ccd1 --- /dev/null +++ b/src/quantem/diffraction/crystal_map.py @@ -0,0 +1,567 @@ +"""Orientation and phase mapping over one or more candidate crystals. + +CrystalMap is the standard entry point for ACOM. It owns one +:class:`~quantem.diffraction.orientation.OrientationMap` per candidate +crystal, fans the matching and refinement stages out over all of them, and +holds the :class:`~quantem.diffraction.phase.PhaseMap` that decides which +crystal sits at each probe position:: + + cm = CrystalMap.from_vectors(peaks, [Cu_metal, Cu2O], energy_ev=300e3) + cm.build_plan(**plan_params) + cm.match_orientations(**match_params) + cm.refine_orientations(**refine_params) + cm.fit() + cm.plot_phase() + cm.plot_orientation() + +The individual maps stay available for anything asymmetric or per-crystal -- +``cm["Cu metal"]``, ``cm[0]`` or ``cm.orientation_maps`` -- and every method +on OrientationMap works there exactly as before. +""" + +from __future__ import annotations + +import numpy as np + +from quantem.core.io.serialize import AutoSerialize +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.orientation import OrientationMap +from quantem.diffraction.phase import PhaseMap + + +class CrystalMap(AutoSerialize): + """Per-position crystal orientation and phase over a scan. + + Parameters + ---------- + orientation_maps : list of OrientationMap + One matched (or unmatched) map per candidate crystal. + + Attributes + ---------- + orientation_maps : list of OrientationMap + The per-crystal maps, in the order they were given. + names : list of str + Crystal names, used for indexing and plot labels. + phases : PhaseMap or None + The phase decision, populated by :meth:`fit`. + """ + + _token = object() + + def __init__(self, orientation_maps: list[OrientationMap], _token=None): + if _token is not self._token: + raise RuntimeError( + "Use CrystalMap.from_vectors() or CrystalMap.from_orientation_maps()." + ) + if len(orientation_maps) == 0: + raise ValueError("CrystalMap needs at least one crystal.") + names = [om.crystal.name for om in orientation_maps] + if len(set(names)) != len(names): + raise ValueError( + f"crystal names must be unique for indexing by name, got {names}. " + "Set Crystal(name=...) to tell them apart." + ) + shapes = {tuple(om.peaks.shape[:2]) for om in orientation_maps} + if len(shapes) != 1: + raise ValueError(f"all crystals must share one scan shape, got {shapes}") + self.orientation_maps = orientation_maps + self.phases: PhaseMap | None = None + self.metadata: dict = {} + + # ------------------------------------------------------------------ + # construction + # ------------------------------------------------------------------ + + @classmethod + def from_vectors( + cls, + peaks, + crystals: Crystal | list[Crystal], + energy_ev: float = 300e3, + precession_deg: float = 0.0, + semiconv_mrad: float = 0.0, + ) -> "CrystalMap": + """Build one OrientationMap per crystal from a shared peak table. + + Parameters + ---------- + peaks : Vector + Calibrated Bragg peaks, shared by every crystal. + crystals : Crystal or list of Crystal + Candidate phases. A single Crystal is accepted, so single-phase + work uses the same entry point. + energy_ev, precession_deg, semiconv_mrad + Passed to :meth:`OrientationMap.from_vectors`. + """ + xtls = list(crystals) if isinstance(crystals, (list, tuple)) else [crystals] + oms = [ + OrientationMap.from_vectors( + peaks, + xtl, + energy_ev=energy_ev, + precession_deg=precession_deg, + semiconv_mrad=semiconv_mrad, + ) + for xtl in xtls + ] + return cls(oms, _token=cls._token) + + @classmethod + def from_orientation_maps(cls, orientation_maps: list[OrientationMap]) -> "CrystalMap": + """Wrap maps that were built and matched by hand.""" + return cls(list(orientation_maps), _token=cls._token) + + # ------------------------------------------------------------------ + # access + # ------------------------------------------------------------------ + + @property + def names(self) -> list[str]: + return [om.crystal.name for om in self.orientation_maps] + + @property + def peaks(self): + return self.orientation_maps[0].peaks + + @property + def shape(self) -> tuple[int, int]: + return tuple(self.orientation_maps[0].peaks.shape[:2]) + + def __len__(self) -> int: + return len(self.orientation_maps) + + def __iter__(self): + return iter(self.orientation_maps) + + def __getitem__(self, key) -> OrientationMap: + """`cm[0]` or `cm["Cu metal"]` -> the OrientationMap of that crystal.""" + if isinstance(key, str): + try: + return self.orientation_maps[self.names.index(key)] + except ValueError: + raise KeyError(f"no crystal named {key!r}; have {self.names}") from None + return self.orientation_maps[key] + + def __repr__(self) -> str: + R, C = self.shape + stage = "unmatched" + if self.orientation_maps[0].quats is not None: + stage = "matched" + if "refine" in self.orientation_maps[0].metadata: + stage = "refined" + if self.phases is not None and self.phases.phase_index is not None: + stage += ", phase fit" + return "CrystalMap(%d x %d, %s, [%s])" % (R, C, stage, ", ".join(self.names)) + + # ------------------------------------------------------------------ + # staged workflow, fanned out over the crystals + # ------------------------------------------------------------------ + + def build_plan(self, overrides: dict | None = None, **kwargs) -> "CrystalMap": + """Build the correlation plan for every crystal. + + Parameters + ---------- + overrides : dict, optional + Per-crystal keyword overrides, keyed by crystal name, e.g. + ``overrides={"Cu2O": dict(angle_step_zone_axis_deg=2.0)}``. + **kwargs + Passed to :meth:`OrientationMap.build_plan` for every crystal. + """ + return self._fanout("build_plan", overrides, **kwargs) + + def match_orientations(self, overrides: dict | None = None, **kwargs) -> "CrystalMap": + """Match every crystal against the measured peaks. + + Each crystal is correlated against its own plan, so the scores are + comparable: the library slices are unit vectors and the measured + polar image is divided by its norm, making the correlation a cosine + similarity in [0, 1]. With `num_matches` above 1 each further match + is fitted to what the earlier ones leave unexplained, so a probe + straddling two grains indexes both. Every match is still scored + against the pattern as measured, so comparing the matches tells the + two cases apart: two grains score alike, while a spurious second + match on a single grain scores well below the first. + + Parameters + ---------- + overrides : dict, optional + Per-crystal keyword overrides, keyed by crystal name. + **kwargs + Passed to :meth:`OrientationMap.match_orientations` for every + crystal. The ones usually set are `num_matches`, + `min_number_peaks` and `positions`, the last restricting the + match to a few probe positions for a staged test run. + + Returns + ------- + CrystalMap + Self, so stages chain. + + Notes + ----- + The correlation saturates on sparse patterns: a position carrying + the direct beam and two noise peaks scores about as well as a real + grain, because some library orientation almost always has a + reflection at that radius and angle. Judge a match by + :meth:`signal_confidence` or the peak count, not by the correlation + alone. + """ + return self._fanout("match_orientations", overrides, **kwargs) + + def refine_orientations(self, overrides: dict | None = None, **kwargs) -> "CrystalMap": + """Refine every crystal off the library grid. + + Least squares on the paired peak positions removes the quantization + of the plan: the in-plane rotation comes from the pairing, the zone + axis tilt from the intensity envelope. A second pass rescues + positions whose answer disagrees with all of their neighbours. + + Parameters + ---------- + overrides : dict, optional + Per-crystal keyword overrides, keyed by crystal name. + **kwargs + Passed to :meth:`OrientationMap.refine_orientations` for every + crystal, commonly `num_iterations` and `zone_search_deg`. + + Returns + ------- + CrystalMap + Self, so stages chain. + """ + return self._fanout("refine_orientations", overrides, **kwargs) + + def _fanout(self, method: str, overrides: dict | None, **kwargs) -> "CrystalMap": + overrides = overrides or {} + unknown = set(overrides) - set(self.names) + if unknown: + raise KeyError(f"overrides name unknown crystals {sorted(unknown)}; have {self.names}") + for om in self.orientation_maps: + kw = {**kwargs, **overrides.get(om.crystal.name, {})} + getattr(om, method)(**kw) + return self + + def fit(self, **kwargs) -> "CrystalMap": + """Decide the phase at every position; see :meth:`PhaseMap.fit`. + + With a single crystal there is nothing to choose between, but the fit + still runs: it applies the null hypothesis, so positions with no + diffracted signal come out unindexed and the maps below fade them. + """ + self.phases = PhaseMap.from_orientation_maps(self.orientation_maps) + self.phases.fit(**kwargs) + return self + + # ------------------------------------------------------------------ + # derived quantities + # ------------------------------------------------------------------ + + def _require_fit(self, what: str) -> PhaseMap: + if self.phases is None or self.phases.phase_index is None: + raise ValueError(f"run fit() before {what}.") + return self.phases + + @property + def phase_index(self) -> np.ndarray: + """Winning crystal at each probe position. + + Returns + ------- + np.ndarray + ``(scan_row, scan_col)`` index into :attr:`names`, or -1 where + the null hypothesis in :meth:`fit` found too little diffracted + signal to name a crystal. + """ + return self._require_fit("phase_index").phase_index.numpy() + + def signal_confidence(self, signal_range="auto") -> np.ndarray: + """Confidence in [0, 1] that a crystal is present, from the data alone. + + The measured intensity beyond the direct beam, scaled to [0, 1]. + Vacuum and amorphous support diffract nothing, so they score zero + however well some orientation happens to correlate -- which the + correlation itself cannot tell you, since it saturates on sparse + patterns. + + Parameters + ---------- + signal_range : tuple or "auto", default="auto" + Diffracted intensity mapped to 0 ... 1. "auto" spans zero to the + 95th percentile over the indexed positions. + + Returns + ------- + np.ndarray + ``(scan_row, scan_col)`` confidence in [0, 1]. + """ + return self._require_fit("signal_confidence()").signal_confidence(signal_range) + + def mask(self, phase=None, signal_range="auto") -> np.ndarray: + """Display mask in [0, 1] for one crystal, or for all indexed positions. + + The phase decision times the diffracted-signal confidence: positions + of another crystal, and positions with nothing there, are zero. + + Parameters + ---------- + phase : int or str, optional + Crystal index or name. None (default) keeps every indexed + position, whichever crystal won. + signal_range : tuple or "auto" + Passed to :meth:`signal_confidence`. + """ + conf = self.signal_confidence(signal_range) + if phase is None: + return conf + i = self.names.index(phase) if isinstance(phase, str) else int(phase) + return (self.phase_index == i) * conf + + def phase_fractions(self) -> dict[str, float]: + """Fraction of the scan won by each crystal, plus the unindexed share.""" + ph = self.phase_index + out = {"unindexed": float((ph == -1).mean())} + for i, n in enumerate(self.names): + out[n] = float((ph == i).mean()) + return out + + # ------------------------------------------------------------------ + # plotting + # ------------------------------------------------------------------ + + def plot_phase(self, **kwargs): + """Map of which crystal won at each probe position. + + Color gives the crystal, brightness gives the evidence. Positions + the null hypothesis left unindexed in :meth:`fit` -- vacuum, + amorphous support, anything that diffracts nothing -- are black. + + Parameters + ---------- + shade_by : {"signal", "reliability", "none"}, default="signal" + What the brightness means. "signal" fades by the measured + diffracted intensity, so the map shows where crystals are. + "reliability" uses the cost gap to the best model without the + winning crystal, which answers which phase rather than whether + there is one. + shade_range : tuple or "auto", default="auto" + Values mapped to black ... full color. + phase_colors : np.ndarray, optional + One RGB color per crystal. + scalebar : dict, "auto" or None, default="auto" + Real-space scale bar; "auto" takes the scan step carried by the + peaks. + **kwargs + Further arguments of :meth:`PhaseMap.plot_phase`. + + Returns + ------- + tuple + ``(fig, ax)``. + + Raises + ------ + ValueError + If :meth:`fit` has not been run. + """ + return self._require_fit("plot_phase()").plot_phase(**kwargs) + + def plot_orientation(self, direction=("z", "r"), phase=None, mask=None, **kwargs): + """Inverse pole figure maps of every crystal, masked by the phase decision. + + Parameters + ---------- + direction : str or sequence of str, default=("z", "r") + Out-of-plane, in-plane, or both. + phase : int or str, optional + Restrict to one crystal. None (default) plots all of them. + mask : np.ndarray, optional + Overrides the automatic phase-and-signal mask. + **kwargs + Passed to :meth:`OrientationMap.plot_orientation`. + + Returns + ------- + list of tuple + One ``(fig, ax)`` per crystal and direction. + """ + dirs = [direction] if isinstance(direction, str) else list(direction) + out = [] + for i in self._phase_indices(phase): + m = mask if mask is not None else self.mask(i) + for d in dirs: + out.append( + self.orientation_maps[i].plot_orientation(direction=d, mask=m, **kwargs) + ) + return out + + def plot_pole_figure(self, pole=(0, 0, 1), phase=None, mask=None, **kwargs): + """Stereographic pole figure of each crystal, masked by the phase decision. + + Parameters + ---------- + pole : tuple of int, default=(0, 0, 1) + Crystal direction plotted, in Miller indices. + phase : int or str, optional + Restrict to one crystal. None (default) plots all of them. + mask : np.ndarray, optional + Overrides the automatic phase-and-signal mask. + **kwargs + Passed to :meth:`OrientationMap.plot_pole_figure`, e.g. + `color_by`, `int_range` and `overlay`. + + Returns + ------- + list of tuple + One ``(fig, ax)`` per crystal. + """ + out = [] + for i in self._phase_indices(phase): + m = mask if mask is not None else self.mask(i) + out.append(self.orientation_maps[i].plot_pole_figure(pole=pole, mask=m, **kwargs)) + return out + + def plot_matches(self, positions, phase=None, **kwargs): + """Matched patterns at a few probe positions, over the measured peaks. + + One panel per candidate, with the measured peaks as gray disks and + the simulated pattern as colored markers, both sized by intensity. + Passing a `dataset` puts the recorded pattern behind them instead; + the pixel size and the fitted origins then come from the peaks, which + carry them from the dataset through the calibration, so neither needs + passing. A position matched by nothing is drawn with its peaks alone + and labelled "no match". + + Parameters + ---------- + positions : list of tuple of int + ``(row, col)`` probe positions, one panel row each. + phase : int or str, optional + Restrict to one crystal. None (default) shows all of them. + matches : tuple of int, default=(0, 1) + Which matches of each crystal to draw. With `num_matches` of 2, + (0, 1) shows the best and the residual match side by side, which + is how a probe straddling two grains shows itself. + dataset : Dataset4dstem, optional + Show the recorded diffraction pattern behind the overlay. + measured_scale, measured_power : float, optional + Size and intensity compression of the gray measured peaks. + transpose_plots : bool, default=False + Panel layout only: rows are positions unless this is True. + **kwargs + Further arguments of + :func:`~quantem.diffraction.orientation_visualization.plot_pattern_matches`. + + Returns + ------- + tuple + ``(fig, axs)``. + """ + from quantem.diffraction.orientation_visualization import plot_pattern_matches + + oms = [self.orientation_maps[i] for i in self._phase_indices(phase)] + md = self.peaks.metadata or {} + if kwargs.get("dataset") is not None: + if md.get("pixel_size") is not None: + kwargs.setdefault("pixel_size", float(md["pixel_size"])) + if md.get("origins") is not None: + kwargs.setdefault("origins", np.asarray(md["origins"])) + return plot_pattern_matches(oms, positions=positions, **kwargs) + + def plot_correlation(self, **kwargs): + """Correlation and reliability of every crystal, one panel each. + + The top row is the best correlation of each crystal, the bottom row + its reliability. Read them with care: the correlation is a cosine + similarity, so it saturates on patterns carrying only a few peaks + and stays high on the substrate, and the reliability compares + crystals rather than testing whether one is there at all. Use + :meth:`plot_phase` or :meth:`signal_confidence` for that. + + Parameters + ---------- + mask : bool, default=False + If True, multiply every panel by :meth:`signal_confidence`, so + positions with no diffracted signal go to zero. + shared_scale : bool, default=True + Put every crystal on one scale, so the panels can be compared + directly. Each row keeps its own range, since correlation and + reliability are different quantities. False lets each panel + autoscale, which shows the structure within a weak crystal at + the cost of comparability. Passing `norm` overrides both. + **kwargs + Passed to :func:`~quantem.core.visualization.show_2d`. + + Returns + ------- + tuple + ``(fig, axs)``. + """ + from quantem.core.visualization import show_2d + + mask = kwargs.pop("mask", False) + shared_scale = kwargs.pop("shared_scale", True) + corr = [om.corr[..., 0].numpy() for om in self.orientation_maps] + rel = [om.reliability.numpy() for om in self.orientation_maps] + if mask: + conf = self.signal_confidence() + corr = [c * conf for c in corr] + rel = [r * conf for r in rel] + if shared_scale and "norm" not in kwargs: + # one scale per row, so the crystals are directly comparable; + # correlation and reliability keep their own ranges + kwargs["norm"] = [ + [ + { + "interval_type": "manual", + "vmin": float(min(np.nanmin(a) for a in row)), + "vmax": float(max(np.nanmax(a) for a in row)), + } + ] + * len(row) + for row in (corr, rel) + ] + kwargs.setdefault("cbar", True) + kwargs.setdefault( + "title", + [ + [f"{n} correlation" for n in self.names], + [f"{n} reliability" for n in self.names], + ], + ) + sb = self.orientation_maps[0].scan_scalebar + if sb is not None: + kwargs.setdefault("scalebar", [[sb] + [False] * (len(corr) - 1), [False] * len(corr)]) + return show_2d([corr, rel], **kwargs) + + def _phase_indices(self, phase) -> list[int]: + if phase is None: + return list(range(len(self.orientation_maps))) + i = self.names.index(phase) if isinstance(phase, str) else int(phase) + return [i] + + # ------------------------------------------------------------------ + # checkpointing + # ------------------------------------------------------------------ + + def save(self, path, mode: str = "w", include_plan: bool = False, **kwargs): + """Save the whole analysis to one file. + + Parameters + ---------- + include_plan : bool, default=False + The correlation plan dominates the file size and is rebuilt in + seconds by :meth:`build_plan`, so it is dropped by default. Pass + True to keep it and reload a map ready to match again. + """ + if include_plan: + return AutoSerialize.save(self, path, mode=mode, **kwargs) + stash = [(om, om.plan_fft) for om in self.orientation_maps] + try: + for om, _ in stash: + om.plan_fft = None + return AutoSerialize.save(self, path, mode=mode, **kwargs) + finally: + for om, plan in stash: + om.plan_fft = plan diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index 70099d37e..ae1773a7b 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -50,7 +50,6 @@ quat_from_zone_axis, sample_zone_axes, symmetry_aligned, - symmetry_reduced_zone_angles, ) @@ -274,6 +273,7 @@ def __init__( # results self.quats: torch.Tensor | None = None self.corr: torch.Tensor | None = None + self.corr_residual: torch.Tensor | None = None self.corr_second: torch.Tensor | None = None self.reliability: torch.Tensor | None = None self.mirror: torch.Tensor | None = None @@ -752,6 +752,39 @@ def _polar_image( out = torch.zeros((self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64) return self._deposit_polar(qr, qphi, amp, out) + def _grid_quats(self, flat_idx: torch.Tensor, Z: int, G: int, n_ch: int) -> torch.Tensor: + """Library orientations at flat (channel, zone, gamma) indices. + + No subpixel refinement: these are used only to test whether two + candidates are the same orientation, where the grid step is far + finer than the separation being tested. + + Parameters + ---------- + flat_idx : torch.Tensor + ``(..., )`` indices into the flattened ``(ch, Z, G)`` correlation. + + Returns + ------- + torch.Tensor + ``(..., 4)`` quaternions. + """ + idx = flat_idx.cpu() + ch = idx // (Z * G) + z = (idx // G) % Z + g = idx % G + q_zone = self.zone_quats[z] + gamma = self.gamma[g].clone() + is_mirror = ch == 1 + if n_ch > 1: + q_flip = torch.tensor([0.0, 1.0, 0.0, 0.0], dtype=torch.float64) + q_zone = torch.where(is_mirror[..., None], qmult(q_flip, q_zone), q_zone) + gamma = torch.where(is_mirror, -gamma - np.pi, gamma) + half = gamma / 2 + zeros = torch.zeros_like(half) + q_spin = torch.stack((torch.cos(half), zeros, zeros, torch.sin(half)), dim=-1) + return qmult(q_spin, q_zone) + def _polar_images(self, arrays: list[np.ndarray], ix: list[int]) -> torch.Tensor: """Sparse polar images (B, S, G) of a batch of measured patterns, deposited in one call.""" @@ -781,6 +814,8 @@ def match_orientations( include_mirror: bool = True, min_number_peaks: int | None = None, min_angle_between_matches_deg: float = 15.0, + suppress_matched: float = 1.0, + top_k_matches: int = 256, subpixel_gamma: bool = True, subpixel_zone: bool = True, min_detector_fraction: float = 0.3, @@ -821,7 +856,23 @@ def match_orientations( min_number_peaks : int | None Skip positions with fewer detected peaks (including the direct beam); defaults to MIN_NUMBER_PEAKS (5). + suppress_matched : float, default=1.0 + Fraction of each accepted match subtracted from the measured + polar image before the next match is sought, so later matches + fit the peaks earlier ones leave unexplained. 1.0 removes the + matched component exactly; 0 disables the deflation and leaves + `min_angle_between_matches_deg` as the only thing separating the + matches, which lets two of them index the same peaks. Only + meaningful with `num_matches` above 1. The deflation steers the + search only: `corr` scores every match against the pattern as + measured, so the matches stay comparable, and `corr_residual` + holds the score against the residual each one actually saw. min_angle_between_matches_deg : float, default=15.0 + Minimum separation between matches, applied to the zone axis and + the in-plane angle together: a candidate is rejected only when it + is within this angle of an earlier match in both. Two grains + sharing a zone axis but rotated in plane past this angle are + therefore kept as separate matches. Exclusion radius (degrees, zone-axis distance) around earlier matches, both for later matches and for the second-best score used in `reliability`. @@ -842,6 +893,8 @@ def match_orientations( include_mirror=bool(include_mirror), min_number_peaks=int(min_number_peaks), min_angle_between_matches_deg=float(min_angle_between_matches_deg), + suppress_matched=float(suppress_matched), + top_k_matches=int(top_k_matches), subpixel_gamma=bool(subpixel_gamma), subpixel_zone=bool(subpixel_zone), min_detector_fraction=float(min_detector_fraction), @@ -857,22 +910,14 @@ def match_orientations( quats = torch.zeros((R, C, M, 4), dtype=torch.float64) quats[..., 0] = 1.0 corr_out = torch.zeros((R, C, M), dtype=torch.float64) + corr_res = torch.zeros((R, C, M), dtype=torch.float64) corr_second = torch.zeros((R, C), dtype=torch.float64) mirror_out = torch.zeros((R, C, M), dtype=torch.bool) fields = peaks.fields ix = [fields.index(f) for f in ("qx", "qy", "intensity")] - # zone-pair angular distances for the exclusion ball around matches, - # minimized over the matching symmetry: a redundant library - # (hemisphere fallback, pseudo-symmetry) holds symmetry copies of - # every zone, and those must not count as the "second best" match dtype = getattr(self, "dtype", torch.float64) - zone_ang = ( - symmetry_reduced_zone_angles(self.zone_axes, self.crystal.sym_quats_matching) - .to(dtype) - .to(device) - ) # (Z, Z) plan_fft = self.plan_fft # (Z, S, G) complex wanted = position_mask(positions, (R, C)) @@ -901,51 +946,91 @@ def match_orientations( .to(dtype) .to(device) ) - norms = torch.linalg.norm(im_stack.reshape(len(batch), -1), dim=1).clamp_min(1e-12) + B = im_stack.shape[0] with warnings.catch_warnings(): # torch's MPS FFT emits an internal out-tensor resize notice warnings.simplefilter("ignore", UserWarning) im_fft = torch.fft.fft(im_stack, dim=-1) # (B, S, G) - - # contract shells: (B, Z, G) per channel - cc = torch.einsum("zsg,bsg->bzg", plan_fft, im_fft) - channels = [cc] - if include_mirror: - channels.append(torch.einsum("zsg,bsg->bzg", plan_fft, torch.conj(im_fft))) - corr = torch.fft.ifft(torch.stack(channels, dim=1), dim=-1).real - # normalize: library slices are unit vectors, so dividing by the - # experimental norm makes corr a cosine similarity in [0, 1] - corr = corr / norms[:, None, None, None] - if self.plan_norm_shift is not None: - # square-detector correction: renormalize by the on-detector - # template norm at each in-plane shift, and suppress - # rotations where most of the template is unmeasurable - n_ch = corr.shape[1] - corr = corr / self.plan_norm_shift[None, :n_ch].clamp_min(1e-3) - corr = corr.masked_fill( - self.plan_frac_shift[None, :n_ch] < min_detector_fraction, 0.0 - ) - # corr: (B, ch, Z, G) - B = corr.shape[0] + # frequency ramp used to roll a template to an in-plane angle + k_ramp = torch.fft.fftfreq(G, d=1.0 / G).to(im_fft.dtype).to(device) for m in range(M): - if m > 0: - # suppress zones near earlier matches, per pattern - for b in range(B): - for mm in range(m): - rx, ry = batch[b] - # zone index of previous match not stored; use angle - # to previous zone axis - zprev = self._zprev[b][mm] - corr[ - b, :, zone_ang[zprev] < min_angle_between_matches_deg, : - ] = -torch.inf - flat_idx = corr.reshape(B, -1).argmax(dim=1) + norms = torch.linalg.norm( + torch.fft.ifft(im_fft, dim=-1).real.reshape(B, -1), dim=1 + ).clamp_min(1e-12) + # contract shells: (B, Z, G) per channel + cc = torch.einsum("zsg,bsg->bzg", plan_fft, im_fft) + channels = [cc] + if include_mirror: + channels.append(torch.einsum("zsg,bsg->bzg", plan_fft, torch.conj(im_fft))) + corr_raw = torch.fft.ifft(torch.stack(channels, dim=1), dim=-1).real + # normalize: library slices are unit vectors, so dividing by the + # experimental norm makes corr a cosine similarity in [0, 1] + corr = corr_raw / norms[:, None, None, None] + if self.plan_norm_shift is not None: + # square-detector correction: renormalize by the on-detector + # template norm at each in-plane shift, and suppress + # rotations where most of the template is unmeasurable + n_ch = corr.shape[1] + corr = corr / self.plan_norm_shift[None, :n_ch].clamp_min(1e-3) + corr = corr.masked_fill( + self.plan_frac_shift[None, :n_ch] < min_detector_fraction, 0.0 + ) + # corr: (B, ch, Z, G) + if m == 0: + # the deflation below changes the image every match, so + # keep the correlation against the pattern as measured: + # selection uses the residual, the reported score does not + corr_full = corr n_ch = corr.shape[1] + flat = corr.reshape(B, -1) + if M > 1: + # Rank the candidates and walk down until one is a + # genuinely different orientation from every earlier + # match. The test is the full misorientation, reduced by + # crystal symmetry: neither the zone axis nor the in-plane + # angle alone can tell a symmetry copy (same orientation, + # different library entry) from two grains sharing a zone + # axis but rotated in plane, and those must be treated + # oppositely. + K = min(flat.shape[1], top_k_matches) + top_v, top_i = flat.topk(K, dim=1) + q_top = self._grid_quats(top_i, Z, G, n_ch) # (B, K, 4) + keep = torch.zeros((B, K), dtype=torch.bool) + if m == 0: + keep[:, 0] = True + else: + ok = torch.ones((B, K), dtype=torch.bool) + for mm in range(m): + q_prev = self._qprev[mm] # (B, 4) + ang = misorientation_angle_deg( + q_prev[:, None, :].expand(-1, K, -1).reshape(-1, 4), + q_top.reshape(-1, 4), + self.crystal.sym_quats_matching, + ).reshape(B, K) + ok &= ang >= min_angle_between_matches_deg + ok &= torch.isfinite(top_v.cpu()) + first = torch.where( + ok.any(dim=1), ok.double().argmax(dim=1), torch.full((B,), -1) + ) + for b in range(B): + if first[b] >= 0: + keep[b, int(first[b])] = True + sel = torch.where( + keep.any(dim=1), + keep.double().argmax(dim=1), + torch.zeros(B, dtype=torch.long), + ).to(flat.device) + flat_idx = top_i.gather(1, sel[:, None]).squeeze(1) + invalid = ~keep.any(dim=1).to(flat.device) + else: + flat_idx = flat.argmax(dim=1) + invalid = torch.zeros(B, dtype=torch.bool, device=flat.device) ch_i = flat_idx // (Z * G) z_i = (flat_idx // G) % Z g_i = flat_idx % G - c_val = corr.reshape(B, -1).gather(1, flat_idx[:, None]).squeeze(1) + c_val = flat.gather(1, flat_idx[:, None]).squeeze(1) + c_val = c_val.masked_fill(invalid, -torch.inf) gamma = gamma_grid[g_i.cpu()].clone() if subpixel_gamma: @@ -1001,33 +1086,86 @@ def match_orientations( q_spin = torch.stack((torch.cos(half), zeros, zeros, torch.sin(half)), dim=-1) q = qmult(q_spin, q_zone) + # score every match against the pattern as measured, so the + # matches are comparable with each other and across positions + c_report = corr_full.reshape(B, -1).gather(1, flat_idx[:, None]).squeeze(1) + c_report = c_report.masked_fill(invalid, -torch.inf) for b, (rx, ry) in enumerate(batch): if torch.isfinite(c_val[b]): quats[rx, ry, m] = q[b] - corr_out[rx, ry, m] = c_val[b].cpu().double() + corr_out[rx, ry, m] = c_report[b].cpu().double() + corr_res[rx, ry, m] = c_val[b].cpu().double() mirror_out[rx, ry, m] = bool(is_mirror[b]) if m == 0: - # second-best score outside the exclusion ball around the - # best zone axis -> reliability = corr - corr_second - far = zone_ang[z_i] >= min_angle_between_matches_deg # (B, Z) - c2 = ( - corr.masked_fill(~far[:, None, :, None], -torch.inf) - .reshape(B, -1) - .amax(dim=1) + # second-best score at an orientation genuinely different + # from the best one -> reliability = corr - corr_second. + # Same misorientation test, so a symmetry copy of the + # winner never counts as the runner-up, while a real + # in-plane degeneracy does. + K2 = min(corr.reshape(B, -1).shape[1], top_k_matches) + tv, ti = corr.reshape(B, -1).topk(K2, dim=1) + q_t2 = self._grid_quats(ti, Z, G, n_ch) + q_best = self._grid_quats(flat_idx[:, None], Z, G, n_ch)[:, 0] + ang2 = misorientation_angle_deg( + q_best[:, None, :].expand(-1, K2, -1).reshape(-1, 4), + q_t2.reshape(-1, 4), + self.crystal.sym_quats_matching, + ).reshape(B, K2) + far2 = (ang2 >= min_angle_between_matches_deg) & torch.isfinite(tv.cpu()) + c2 = torch.where( + far2.any(dim=1), + tv.cpu().double().masked_fill(~far2, -torch.inf).amax(dim=1), + torch.full((B,), -torch.inf, dtype=torch.float64), ) for b, (rx, ry) in enumerate(batch): if torch.isfinite(c2[b]): - corr_second[rx, ry] = c2[b].cpu().double() + corr_second[rx, ry] = c2[b] if M > 1: if m == 0: - self._zprev = [[] for _ in range(B)] - for b in range(B): - self._zprev[b].append(int(z_i[b])) + self._qprev = [] + self._qprev.append(q.clone()) + + if M > 1 and m < M - 1 and suppress_matched > 0: + # Deflate the matched template out of the measured polar + # image, so the next match sees only what this one leaves + # unexplained. Without this, the exclusion ball keeps the + # next zone axis far away in orientation but nothing stops + # it from being fitted to the same peaks -- two grains in + # one probe then index as one, and the second match is a + # different view of the first. + # + # The templates are unit vectors, so the amount of this + # template present in the image is its raw inner product, + # read off the un-normalized correlation at the winning + # (zone, in-plane angle). Subtracting that multiple is the + # matching-pursuit step and removes it exactly. + b_ar = torch.arange(B, device=device) + alpha = suppress_matched * corr_raw[b_ar, ch_i, z_i, g_i].clamp_min(0).to( + im_fft.dtype + ) + # roll the template to the matched in-plane angle: a shift + # of g samples is a linear phase on its transform + phase = torch.exp( + -2j * np.pi * k_ramp[None, :] * g_i[:, None].to(k_ramp.dtype) / G + ) + t_fft = torch.conj(plan_fft[z_i]) * phase[:, None, :] # (B, S, G) + if include_mirror: + # the mirrored template is gamma -> -gamma, a conjugate + # in the transform, and its own roll + t_mir = torch.conj(t_fft) + t_fft = torch.where((ch_i == 1)[:, None, None].to(device), t_mir, t_fft) + im_fft = im_fft - alpha[:, None, None] * t_fft + # measured intensity is non-negative; keep it that way + im_real = torch.fft.ifft(im_fft, dim=-1).real.clamp_min(0) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + im_fft = torch.fft.fft(im_real.to(dtype), dim=-1) self.quats = quats self.corr = corr_out + self.corr_residual = corr_res self.corr_second = corr_second self.reliability = corr_out[..., 0] - corr_second self.mirror = mirror_out @@ -1201,10 +1339,13 @@ def refine_orientations( grid robustness). neighbor_rescue : bool, default=True Second pass over positions whose best match disagrees with every - neighbor by more than rescue_threshold_deg: re-refine from each - distinct neighbor orientation and keep the highest-scoring - result (score = total paired measured intensity). Repairs - isolated wrong local optima such as near-degenerate variants. + neighbor by more than rescue_threshold_deg: re-refine from every + distinct candidate orientation -- all matches of all eight + neighbors, and this position's own remaining matches -- and keep + the highest-scoring result (score = total paired measured + intensity). Repairs isolated wrong local optima such as + near-degenerate variants, and probe positions straddling two + grains, where the correct orientation is often the second match. rescue_threshold_deg : float, default=2.0 Minimum-neighbor misorientation that triggers the rescue pass. """ @@ -1460,19 +1601,31 @@ def get_exp(rx, ry): best_q = self.quats[rx, ry, 0] best_s = float(scores[rx, ry]) cands = [] + + def _add(qn, cands=cands): + if all( + float(misorientation_angle_deg(qn, c, self.crystal.sym_quats)) > 0.5 + for c in cands + ): + cands.append(qn) + + # where two grains overlap in one probe, the right orientation + # is often this position's own second match rather than the + # first, so try every candidate here as well as every + # candidate of every neighbour + for m in range(1, M): + if self.corr[rx, ry, m] > 0: + _add(self.quats[rx, ry, m]) for dr in (-1, 0, 1): for dc in (-1, 0, 1): nr, nc = rx + dr, ry + dc if (dr == 0 and dc == 0) or not (0 <= nr < R and 0 <= nc < C): continue - if self.corr[nr, nc, 0] <= 0: - continue # neighbour carries no match (never run, or skipped) - qn = self.quats[nr, nc, 0] - if all( - float(misorientation_angle_deg(qn, c, self.crystal.sym_quats)) > 0.5 - for c in cands - ): - cands.append(qn) + for m in range(M): + # neighbour match absent (never run, or skipped) + if self.corr[nr, nc, m] <= 0: + continue + _add(self.quats[nr, nc, m]) for qc in cands: q, sc = refine_single(qc.clone(), q_exp, w_exp) if sc > best_s * 1.02: @@ -1481,6 +1634,8 @@ def get_exp(rx, ry): n_rescued += 1 self.quats[rx, ry, 0] = best_q scores[rx, ry] = best_s + self.metadata["refine"]["n_retried"] = len(retry) + self.metadata["refine"]["n_rescued"] = int(n_rescued) return self # ------------------------------------------------------------------ diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py index 91a0b7018..d88dd0383 100644 --- a/src/quantem/diffraction/orientation_visualization.py +++ b/src/quantem/diffraction/orientation_visualization.py @@ -429,8 +429,10 @@ def plot_pattern_matches( scalebar: bool = True, show_measured: bool = True, marker_scale: float = 250.0, + measured_scale: float | None = None, + measured_power: float = 0.5, marker: str | None = None, - transpose: bool = False, + transpose_plots: bool = False, axsize: tuple[float, float] = (3.1, 3.1), ): """Candidate matches side by side, py4DSTEM style. @@ -464,10 +466,18 @@ def plot_pattern_matches( Matplotlib marker for the simulated peaks. The default is an open circle over a diffraction pattern, which leaves the measured disk visible inside it, and a plus over the gray measured peaks. - transpose : bool, default=False - By default rows are probe positions and columns are candidates. - True swaps them, giving one row per candidate across the positions, - which fits a few candidates and many positions on a page. + measured_scale : float | None + Marker area of the gray measured peaks; defaults to + ``1.5 * marker_scale``. The direct beam is far brighter than the + disks, so the areas are compressed by `measured_power` and floored, + which keeps the weak spots visible. + measured_power : float, default=0.5 + Compression applied to the measured intensities before sizing. + transpose_plots : bool, default=False + Panel layout only, nothing in the data is transposed. By default + rows are probe positions and columns are candidates; True swaps + them, giving one row per candidate across the positions, which fits + a few candidates and many positions on a page. """ import matplotlib.pyplot as plt @@ -489,7 +499,7 @@ def plot_pattern_matches( panels = [(i, m) for i in range(len(oms)) for m in matches] n_pos, n_pan = len(positions), len(panels) - n_r, n_c = (n_pan, n_pos) if transpose else (n_pos, n_pan) + n_r, n_c = (n_pan, n_pos) if transpose_plots else (n_pos, n_pan) fig, axs = plt.subplots( n_r, n_c, @@ -500,24 +510,38 @@ def plot_pattern_matches( if marker is None: marker = "o" if over_image else "+" ordinal = ["1st", "2nd", "3rd"] + [f"{k + 1}th" for k in range(3, 9)] + + # one limit for every panel, so positions are directly comparable. A + # position holding only the direct beam has q_max of zero, which would + # collapse its axes, so the limit is taken over all of them together. + if q_max_plot is not None: + q_lim = float(q_max_plot) + elif dataset is not None and pixel_size is not None: + q_lim = dataset.shape[-1] / 2 * pixel_size + else: + q_all = [ + np.hypot(peaks[rx, ry].array[:, ix[0]], peaks[rx, ry].array[:, ix[1]]) + for rx, ry in positions + ] + q_max = max((float(q.max()) for q in q_all if q.size), default=0.0) + q_lim = 1.1 * q_max if q_max > 0 else 1.0 + for pi, (rx, ry) in enumerate(positions): data = peaks[rx, ry].array.copy() rc = data[:, [ix[0], ix[1]]] @ rot_back.T data[:, ix[0]] = rc[:, 0] data[:, ix[1]] = rc[:, 1] + # the direct beam outshines every disk, so scaling the areas by the + # brightest peak shrinks the real spots to nothing; normalize on the + # diffracted peaks instead, compress, and floor so none vanish w_meas = data[:, ix[2]].clip(min=0) - w_meas = w_meas / max(w_meas.max(), 1e-12) - if q_max_plot is None: - if dataset is not None and pixel_size is not None: - q_lim = dataset.shape[-1] / 2 * pixel_size - else: - qr = np.hypot(data[:, ix[0]], data[:, ix[1]]) - q_lim = 1.1 * qr.max() if qr.size else 1.0 - else: - q_lim = q_max_plot + q_meas = np.hypot(data[:, ix[0]], data[:, ix[1]]) + w_ref = w_meas[q_meas > 0.05] + hi = float(np.percentile(w_ref, 95)) if w_ref.size else float(w_meas.max(initial=0.0)) + w_meas = np.clip((w_meas / max(hi, 1e-12)) ** measured_power, 0.15, 1.0) for ci, (i_om, m) in enumerate(panels): om = oms[i_om] - ax = axs[ci, pi] if transpose else axs[pi, ci] + ax = axs[ci, pi] if transpose_plots else axs[pi, ci] if over_image: H, W = dataset.shape[-2], dataset.shape[-1] if origins is not None: @@ -526,9 +550,16 @@ def plot_pattern_matches( o_r, o_c = H / 2, W / 2 # pixel j has center (j - origin) * pixel_size; array edges # sit half a pixel beyond the first/last centers + img = np.asarray(dataset.array[rx, ry], dtype=float) + img = np.clip(img, 0, None) ** power + # the direct beam is orders of magnitude above the disks, so + # autoscaling to its peak flattens everything else + vmax = float(np.percentile(img, 99.9)) ax.imshow( - np.asarray(dataset.array[rx, ry]) ** power, + img, cmap="gray_r", + vmin=float(np.percentile(img, 2.0)), + vmax=vmax if vmax > 0 else None, extent=( (-0.5 - o_c) * pixel_size, (W - 0.5 - o_c) * pixel_size, @@ -540,13 +571,21 @@ def plot_pattern_matches( ax.scatter( data[:, ix[1]], data[:, ix[0]], - s=marker_scale * w_meas, + s=(1.5 * marker_scale if measured_scale is None else measured_scale) * w_meas, color="0.75", lw=0, ) - sim = om.generate_pattern(rx, ry, match=m) - inten = sim["intensity"].numpy() - sim_rc = np.stack([sim["qx"].numpy(), sim["qy"].numpy()], axis=1) @ rot_back.T + # a position with too few peaks was never matched, and its stored + # orientation is still the identity; drawing that [001] pattern + # would look like a fit where none was attempted + matched = float(om.corr[rx, ry, m]) > 0 + if om.computed is not None: + matched = matched and bool(om.computed[rx, ry]) + inten = np.zeros(0) + if matched: + sim = om.generate_pattern(rx, ry, match=m) + inten = sim["intensity"].numpy() + sim_rc = np.stack([sim["qx"].numpy(), sim["qy"].numpy()], axis=1) @ rot_back.T if inten.size: size = marker_scale * inten / inten.max() color = colors[i_om % len(colors)] @@ -576,12 +615,18 @@ def plot_pattern_matches( ax.set_yticks([]) ax.set_aspect("equal") ax.set_title( - "%s %s\n(%d, %d) corr = %.2f" - % (om.crystal.name, ordinal[m], rx, ry, float(om.corr[rx, ry, m])), + "%s %s\n(%d, %d) %s" + % ( + om.crystal.name, + ordinal[m], + rx, + ry, + ("corr = %.2f" % float(om.corr[rx, ry, m])) if matched else "no match", + ), fontsize=9, ) - last_row = (ci == n_r - 1) if transpose else (pi == n_r - 1) - first_col = (pi == 0) if transpose else (ci == 0) + last_row = (ci == n_r - 1) if transpose_plots else (pi == n_r - 1) + first_col = (pi == 0) if transpose_plots else (ci == 0) if scalebar and last_row and first_col: add_scalebar_to_ax( ax, diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py index 2dbe71b2e..cc4d2253b 100644 --- a/src/quantem/diffraction/phase.py +++ b/src/quantem/diffraction/phase.py @@ -37,6 +37,36 @@ from quantem.diffraction.orientation import OrientationMap, position_mask +def _majority_filter(phase: np.ndarray, radius: int) -> np.ndarray: + """Replace each position by the most common phase around it. + + Unindexed positions (-1) take part, so an isolated crystal pixel in + vacuum is removed rather than spreading. + + Parameters + ---------- + phase : np.ndarray + ``(scan_row, scan_col)`` phase indices, -1 where unindexed. + radius : int + Half-width of the square neighbourhood in probe positions. + + Returns + ------- + np.ndarray + Filtered phase indices, same shape and dtype. + """ + from scipy.ndimage import uniform_filter + + labels = np.unique(phase) + votes = np.stack( + [ + uniform_filter((phase == v).astype(float), size=2 * radius + 1, mode="nearest") + for v in labels + ] + ) + return labels[votes.argmax(axis=0)] + + class PhaseMap(AutoSerialize): """Assign best-fit phases to every probe position. @@ -72,6 +102,8 @@ def __init__(self, orientation_maps: list[OrientationMap], _token=None): self.cost_best: torch.Tensor | None = None self.phase_index: torch.Tensor | None = None self.reliability: torch.Tensor | None = None + self.diffracted_intensity: torch.Tensor | None = None + self.num_diffracted: torch.Tensor | None = None @classmethod def from_orientation_maps(cls, orientation_maps: list[OrientationMap]) -> "PhaseMap": @@ -96,6 +128,8 @@ def fit( min_sim_intensity_rel: float | None = None, k_max: float | None = None, min_number_peaks: int | None = None, + min_diffracted_peaks: int = 2, + null_k_min: float = 0.05, progress_bar: bool = True, ) -> "PhaseMap": """Score all candidate subsets at every probe position. @@ -140,6 +174,16 @@ def fit( structure factors happen to include many of them. k_max : float | None Restrict the comparison below this scattering vector. + min_diffracted_peaks : int, default=2 + Null hypothesis: a position needs at least this many measured + peaks beyond `null_k_min` before any phase is assigned. Vacuum + and amorphous support carry the direct beam and little else, and + a crystal fit to that is noise; those positions are left + unindexed (`phase_index` of -1) and plot black. + null_k_min : float, default=0.05 + Scattering vector (1/Angstroms) above which a measured peak + counts as diffracted. The default excludes the direct beam, + which sits at the origin after `correct_peak_origins`. """ from scipy.optimize import nnls @@ -167,6 +211,8 @@ def fit( min_sim_intensity_rel=float(min_sim_intensity_rel), k_max=k_max, min_number_peaks=int(min_number_peaks), + min_diffracted_peaks=int(min_diffracted_peaks), + null_k_min=float(null_k_min), ) peaks = oms[0].peaks R, C = peaks.shape[0], peaks.shape[1] @@ -184,6 +230,8 @@ def fit( weights_out = torch.zeros((R, C, F), dtype=torch.float64) reliability = torch.zeros((R, C), dtype=torch.float64) best_subset = torch.full((R, C), -1, dtype=torch.long) + diffracted = torch.zeros((R, C), dtype=torch.float64) + num_diffracted = torch.zeros((R, C), dtype=torch.long) active = position_mask(positions, (R, C)) for om in oms: @@ -196,6 +244,16 @@ def fit( data = peaks[rx, ry].array if data.shape[0] < min_number_peaks: continue + # null hypothesis: no diffracted signal, so no phase to decide. + # Vacuum and amorphous support carry the direct beam and nothing + # else, and the measured signal beyond it is the evidence that + # any crystal is present at all. + qr_meas = np.hypot(data[:, ix[0]], data[:, ix[1]]) + beyond = qr_meas > null_k_min + diffracted[rx, ry] = float(data[beyond, ix[2]].clip(min=0).sum()) + num_diffracted[rx, ry] = int(beyond.sum()) + if int(beyond.sum()) < min_diffracted_peaks: + continue qxy = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) im = torch.as_tensor(data[:, ix[2]], dtype=torch.float64).clamp_min(0) if k_max is not None: @@ -271,6 +329,8 @@ def fit( self.costs_single = costs_single self.cost_best = cost_best + self.diffracted_intensity = diffracted + self.num_diffracted = num_diffracted self.phase_weights = weights_out self.reliability = reliability self.best_subset = best_subset @@ -280,7 +340,10 @@ def fit( w_phase = torch.zeros((R, C, n_maps), dtype=torch.float64) for f, (i_om, _) in enumerate(cands): w_phase[..., i_om] += weights_out[..., f] + # argmax over all-zero weights returns 0, which would label every + # position that was never fit as the first phase; mark them instead self.phase_index = w_phase.argmax(dim=-1) + self.phase_index[torch.isnan(cost_best)] = -1 self.phase_fractions = w_phase / w_phase.sum(dim=-1, keepdim=True).clamp_min(1e-12) return self @@ -319,10 +382,53 @@ def apply_dynamical(self, result: dict) -> "PhaseMap": self.metadata["dynamical_applied"] = dict(result.get("metadata", {})) return self + def signal_confidence( + self, + signal_range: tuple[float, float] | str = "auto", + ) -> np.ndarray: + """Confidence in [0, 1] that a crystal is present, from the data alone. + + The measured intensity beyond the direct beam, scaled to [0, 1]. This + is the null-hypothesis test made visible: vacuum and amorphous support + diffract nothing, so they score zero however well some orientation + happens to correlate. Positions the null hypothesis left unindexed in + :meth:`fit` are forced to zero. + + Use it to fade the phase map and the orientation maps together:: + + conf = pm.signal_confidence() + mask_a = (pm.phase_index.numpy() == 0) * conf + + Parameters + ---------- + signal_range : tuple | "auto", default="auto" + Diffracted intensity mapped to 0 ... 1. "auto" spans zero to the + 95th percentile over the indexed positions. + + Returns + ------- + np.ndarray + ``(scan_row, scan_col)`` confidence in [0, 1]. + """ + if self.diffracted_intensity is None: + raise ValueError("Run fit() before signal_confidence().") + sig = np.nan_to_num(self.diffracted_intensity.numpy()) + indexed = self.phase_index.numpy() >= 0 + if isinstance(signal_range, str): + vals = sig[indexed] + hi = float(np.percentile(vals, 95)) if vals.size else 1.0 + lo, hi = 0.0, max(hi, 1e-12) + else: + lo, hi = signal_range + return ((sig - lo) / max(hi - lo, 1e-12)).clip(0, 1) * indexed + def plot_phase( self, phase_colors: np.ndarray | None = None, - reliability_range: tuple[float, float] = (0.0, 0.1), + shade_by: str = "signal", + shade_range: tuple[float, float] | str = "auto", + majority_filter: int = 0, + reliability_range: tuple[float, float] | None = None, scalebar: dict | str | None = "auto", figax=None, ): @@ -333,8 +439,30 @@ def plot_phase( phase_colors : np.ndarray | None One RGB color per phase; defaults to the shared palette used by the pattern overlay plots (gold, light blue, ...). - reliability_range : tuple, default=(0.0, 0.1) - Reliability values mapped to black ... full color. + shade_by : {"signal", "reliability", "none"}, default="signal" + What the brightness means. "signal" fades each position by the + measured diffracted intensity (see :meth:`signal_confidence`), so + vacuum and amorphous support go black and the map shows where + crystals actually are. "reliability" uses the cost gap to the best + model without the winning crystal, which answers a different + question -- which phase, given that there is one -- and carries no + information about whether anything is there. "none" draws every + indexed position at full color. + shade_range : tuple | "auto", default="auto" + Values mapped to black ... full color. "auto" takes a high + percentile over the indexed positions, since the absolute scale + depends on the data. + majority_filter : int, default=0 + Radius in probe positions of a majority filter applied to the + phase decision for display only; the stored decision is + untouched. 1 replaces each position by the most common phase in + its 3x3 neighbourhood, which removes isolated single-pixel + phases without moving a real boundary. Orientation smoothing + does not do this: it averages orientations within one phase and + leaves the phase assignment alone. + reliability_range : tuple | None + Backwards-compatible shortcut: setting it selects + ``shade_by="reliability"`` with this range. scalebar : dict | "auto" | None Real-space scale bar. "auto" (the default) takes the scan step and units carried from the dataset by the orientation maps; a @@ -351,10 +479,35 @@ def plot_phase( assert self.phase_index is not None and self.reliability is not None if phase_colors is None: phase_colors = DEFAULT_PHASE_COLORS[: len(self.names)] - lo, hi = reliability_range - rel = np.nan_to_num(self.reliability.numpy(), nan=0.0) - alpha = ((rel - lo) / (hi - lo)).clip(0, 1) - rgb = phase_colors[self.phase_index.numpy()] * alpha[..., None] + if reliability_range is not None: + shade_by, shade_range = "reliability", reliability_range + phase = self.phase_index.numpy() + indexed = phase >= 0 + if majority_filter > 0: + phase = _majority_filter(phase, int(majority_filter)) + if shade_by == "signal": + alpha = self.signal_confidence(shade_range) + lo, hi = 0.0, 1.0 + cbar_label = "diffracted signal" + elif shade_by == "reliability": + rel = np.nan_to_num(self.reliability.numpy(), nan=0.0) + if isinstance(shade_range, str): + vals = rel[indexed] + hi = float(np.percentile(vals, 98)) if vals.size else 1.0 + lo, hi = 0.0, max(hi, 1e-12) + else: + lo, hi = shade_range + alpha = ((rel - lo) / max(hi - lo, 1e-12)).clip(0, 1) * indexed + cbar_label = "reliability" + elif shade_by == "none": + alpha = indexed.astype(float) + lo, hi = 0.0, 1.0 + cbar_label = "indexed" + else: + raise ValueError( + f"shade_by must be 'signal', 'reliability' or 'none', got {shade_by!r}" + ) + rgb = phase_colors[np.where(indexed, phase, 0)] * alpha[..., None] if figax is None: fig, ax = plt.subplots(figsize=(9, 4.5)) @@ -393,5 +546,5 @@ def plot_phase( cb.set_ticks([]) else: cb.set_ticks([lo, hi]) - cb.set_label("reliability", fontsize=9) + cb.set_label(cbar_label, fontsize=9) return fig, ax From 4f128fdc43d99c0332612e26782f18480aa59757 Mon Sep 17 00:00:00 2001 From: cophus Date: Sun, 27 Sep 2026 15:36:41 -0700 Subject: [PATCH 14/36] Many updates including dynamical --- src/quantem/diffraction/bloch.py | 168 +++++-- src/quantem/diffraction/bragg_vectors.py | 72 ++- src/quantem/diffraction/calibration.py | 75 ++- src/quantem/diffraction/crystal.py | 339 ++++++++++++-- src/quantem/diffraction/crystal_map.py | 253 +++++++++- src/quantem/diffraction/illumination.py | 21 +- src/quantem/diffraction/orientation.py | 436 ++++++++++++++---- .../diffraction/orientation_visualization.py | 85 ++-- src/quantem/diffraction/phase.py | 27 +- tests/diffraction/test_bloch.py | 32 ++ tests/diffraction/test_crystal.py | 146 +++++- tests/diffraction/test_disk_detection.py | 27 ++ tests/diffraction/test_orientation.py | 87 ++++ tests/diffraction/test_two_phase_map.py | 94 +++- 14 files changed, 1621 insertions(+), 241 deletions(-) diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py index 89814d97f..b81e1755f 100644 --- a/src/quantem/diffraction/bloch.py +++ b/src/quantem/diffraction/bloch.py @@ -17,7 +17,10 @@ from __future__ import annotations +import os +import threading import warnings +from concurrent.futures import ThreadPoolExecutor import numpy as np import torch @@ -150,11 +153,23 @@ def _check_dynamical_factors(crystal: Crystal, energy_ev: float, g_max_beams: fl """Warn once per crystal when the absorptive factors were computed at another energy or do not cover every coupling g - h of the beam list (which needs factors out to twice the largest beam).""" - if getattr(crystal, "U_dyn", None) is None: - return key = id(crystal) if key in _coverage_warned: return + if getattr(crystal, "U_dyn", None) is None: + k_kin = getattr(crystal, "k_max", None) + msg = ( + "no absorptive structure factors (calculate_dynamical_structure_factors), " + "so the Bloch calculation uses the elastic kinematical factors" + ) + if k_kin is not None and 2 * g_max_beams > k_kin + 1e-9: + msg += ( + f", which stop at {k_kin:.2f} 1/A while the couplings of this beam list " + f"reach {2 * g_max_beams:.2f} 1/A" + ) + _coverage_warned.add(key) + warnings.warn(f"{crystal.name}: {msg}", stacklevel=3) + return e_dyn = getattr(crystal, "dyn_energy_ev", None) k_dyn = getattr(crystal, "dyn_k_max", None) msgs = [] @@ -321,6 +336,9 @@ def refine_thickness( A fitted PhaseMap (fit() has been run). thicknesses_A : np.ndarray | None Thickness grid in Angstroms; default 50 ... 1000 in 25 A steps. + min_number_peaks : int | None + Positions with fewer measured peaks, direct beam included, are + skipped; None inherits the phase fit's minimum. At least 3. Returns ------- @@ -337,9 +355,14 @@ def refine_thickness( fit_md = phase_map.metadata.get("fit") if hasattr(phase_map, "metadata") else None pair_distance = resolve(pair_distance, "pair_distance", fit_md, default=PAIR_DISTANCE) power_intensity = resolve(power_intensity, "power_intensity", fit_md, default=POWER_INTENSITY) - min_number_peaks = resolve( - min_number_peaks, "min_number_peaks", fit_md, default=MIN_NUMBER_PEAKS + min_number_peaks = int( + resolve(min_number_peaks, "min_number_peaks", fit_md, default=MIN_NUMBER_PEAKS) ) + if min_number_peaks < 3: + raise ValueError( + f"min_number_peaks={min_number_peaks}: a dynamical fit needs at least the " + "direct beam and two non-collinear reflections" + ) if hasattr(phase_map, "metadata"): phase_map.metadata["thickness"] = dict( thicknesses_A=np.asarray(thicknesses_A, dtype=float).tolist(), @@ -2155,6 +2178,7 @@ def refine_dynamical( rescue_thickness_A: float = 100.0, rescue_tilt_deg: float = 0.05, rescue_max_starts: int = 2, + num_workers: int | None = None, progress_bar: bool = True, ) -> dict: """Dynamical refinement on the Bragg vectors: orientation, thickness, @@ -2225,6 +2249,11 @@ def refine_dynamical( Unpaired simulated beams weaker than this fraction of the strongest simulated beam do not count against a candidate (the detector would not have seen them). + min_number_peaks : int | None + Positions with fewer measured peaks, direct beam included, are + skipped. None inherits the minimum of the phase fit, itself the + matching's (5 by default). At least 3: the direct beam and two + non-collinear reflections. refine_deformation : bool, default=True Solve the symmetric in-plane deformation and the in-plane rotation from the paired positions before the intensity search; the @@ -2259,10 +2288,18 @@ def refine_dynamical( skipping neighbors whose solution repeats one already tried. neighbor_rescue : bool, default=True Second pass: positions whose winning solution differs from a - 4-neighbor of the same crystal by more than rescue_thickness_A or - rescue_tilt_deg are refined again from that neighbor's solution, - and the lower cost is kept. Repairs isolated wrong basins + 4-neighbor of the same crystal (the nearest refined position within + two steps, so a mask of every second position works too) by more + than rescue_thickness_A or rescue_tilt_deg are refined again from + that neighbor's solution, and the lower cost is kept. Repairs isolated wrong basins (thickness aliases, tilt minima at a grid edge). + num_workers : int | None + Threads refining positions side by side; None uses every core. + The Bloch eigensolves are too small to spread over cores on their + own, so this is where the speed comes from. Positions are handed + out in contiguous raster-order blocks and warm starts stay inside a + block, so the result depends on the number of blocks, never on + which thread finishes first. Returns ------- @@ -2294,9 +2331,14 @@ def refine_dynamical( min_sim_intensity_rel = resolve( min_sim_intensity_rel, "min_sim_intensity_rel", fit_md, default=MIN_SIM_INTENSITY_REL ) - min_number_peaks = resolve( - min_number_peaks, "min_number_peaks", fit_md, default=MIN_NUMBER_PEAKS + min_number_peaks = int( + resolve(min_number_peaks, "min_number_peaks", fit_md, default=MIN_NUMBER_PEAKS) ) + if min_number_peaks < 3: + raise ValueError( + f"min_number_peaks={min_number_peaks}: a dynamical fit needs at least the " + "direct beam and two non-collinear reflections" + ) precession_deg = float(resolve(precession_deg, "precession_deg", om_md, default=0.0)) semiconv_mrad = float(resolve(semiconv_mrad, "semiconv_mrad", om_md, default=0.0)) if n_precession_search is None: @@ -2325,6 +2367,7 @@ def refine_dynamical( rescue_thickness_A=float(rescue_thickness_A), rescue_tilt_deg=float(rescue_tilt_deg), rescue_max_starts=int(rescue_max_starts), + num_workers=None if num_workers is None else int(num_workers), ) if hasattr(phase_map, "metadata"): phase_map.metadata["dynamical"] = used @@ -2557,13 +2600,12 @@ def peaks_at(rx, ry): for om in oms: if om.computed is not None: mask_rc = mask_rc & om.computed - iterator = [(r, c) for r, c in np.ndindex(R, C) if mask_rc[r, c]] - if progress_bar: - iterator = tqdm(iterator, desc="dynamical refinement") - for rx, ry in iterator: + positions = [(r, c) for r, c in np.ndindex(R, C) if mask_rc[r, c]] + + def refine_position(rx, ry, block): pk = peaks_at(rx, ry) if pk is None: - continue + return qxy, im, w_exp = pk for f, (i_om, m) in enumerate(cands): om = oms[i_om] @@ -2580,10 +2622,14 @@ def peaks_at(rx, ry): stages = tilt_stages if warm_start and len(tilt_stages) > 1: # an already refined neighbor of the same candidate (raster - # order: above or to the left) is a start inside the fine - # stages' reach; its solution costs one coarse stage less - for nr, nc in ((rx - 1, ry), (rx, ry - 1)): - if nr < 0 or nc < 0 or not torch.isfinite(cost_out[nr, nc, f]): + # order: above or to the left, in the same block, one or two + # steps away so a mask of every second position still warm + # starts) is a start inside the fine stages' reach; its + # solution costs one coarse stage less + for nr, nc in ((rx - 1, ry), (rx, ry - 1), (rx - 2, ry), (rx, ry - 2)): + if (nr, nc) not in block: + continue + if not torch.isfinite(cost_out[nr, nc, f]): continue # same grain: the kinematically matched orientations of # the two positions agree within the coarse stage @@ -2604,20 +2650,78 @@ def peaks_at(rx, ry): continue store(rx, ry, f, sol) + def run_parallel(jobs, work, desc): + """Run work(job) over jobs on num_workers threads. Each Bloch + eigensolve is too small to use more than one core, and torch + releases the GIL inside it, so positions run side by side; every + job writes only its own positions.""" + bar = tqdm(total=sum(len(j) for j in jobs), desc=desc) if progress_bar else None + lock = threading.Lock() + + def run(job): + for item in job: + work(item) + if bar is not None: + with lock: + bar.update(1) + + if n_workers == 1: + for job in jobs: + run(job) + else: + n_threads = torch.get_num_threads() + torch.set_num_threads(1) + try: + with ThreadPoolExecutor(n_workers) as pool: + for fut in [pool.submit(run, job) for job in jobs]: + fut.result() + finally: + torch.set_num_threads(n_threads) + if bar is not None: + bar.close() + + n_workers = max(1, int(num_workers if num_workers is not None else os.cpu_count() or 1)) + # fill the lazily cached lattice data once, before any thread reads it + for om in oms: + _beam_universe(om.crystal) + # contiguous runs of positions in raster order, several per worker so + # they balance but long enough that most positions still warm start; + # warm starts stay inside a run, so the result does not depend on which + # thread finished first + n_blocks = 1 if n_workers == 1 else max(1, min(4 * n_workers, len(positions) // 16)) + blocks = [] + for idx in np.array_split(np.arange(len(positions)), max(n_blocks, 1)): + block = {positions[i] for i in idx} + blocks.append([(*positions[i], block) for i in idx]) + run_parallel( + [b for b in blocks if b], lambda item: refine_position(*item), "dynamical refinement" + ) + if neighbor_rescue and len(tilt_stages) > 1: cost_f0 = torch.nan_to_num(cost_out, nan=torch.inf) f_win = cost_f0.argmin(dim=-1) done = torch.isfinite(cost_out).any(dim=-1) rescue_list = [] + + def nearest_done(rx, ry, dr, dc): + # the refined position one step away, or two on a sparse mask + for k in (1, 2): + nr, nc = rx + k * dr, ry + k * dc + if 0 <= nr < R and 0 <= nc < C and done[nr, nc]: + return nr, nc + return None + for rx, ry in np.ndindex(R, C): if not done[rx, ry]: continue f = int(f_win[rx, ry]) i_om = cands[f][0] starts = [] - for nr, nc in ((rx - 1, ry), (rx + 1, ry), (rx, ry - 1), (rx, ry + 1)): - if not (0 <= nr < R and 0 <= nc < C) or not done[nr, nc]: + for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)): + nb = nearest_done(rx, ry, dr, dc) + if nb is None: continue + nr, nc = nb fn = int(f_win[nr, nc]) if cands[fn][0] != i_om: continue @@ -2652,16 +2756,21 @@ def peaks_at(rx, ry): kept.append((c_n, nr, nc, fn)) if len(kept) >= max(1, rescue_max_starts): break - rescue_list.append((rx, ry, f, [(nr, nc, fn) for _, nr, nc, fn in kept])) - it = tqdm(rescue_list, desc="neighbor rescue") if progress_bar else rescue_list - for rx, ry, f, starts in it: + # the neighbors' solutions as they stand now: rescues run + # in parallel and must not start from each other's updates + rescue_list.append( + (rx, ry, f, [quat_out[nr, nc, fn].clone() for _, nr, nc, fn in kept]) + ) + + def rescue(item): + rx, ry, f, starts = item pk = peaks_at(rx, ry) if pk is None: - continue + return qxy, im, w_exp = pk crystal = oms[cands[f][0]].crystal - for nr, nc, fn in starts: - sol = refine_from(crystal, quat_out[nr, nc, fn], qxy, im, w_exp, tilt_stages[1:]) + for q_n in starts: + sol = refine_from(crystal, q_n, qxy, im, w_exp, tilt_stages[1:]) if sol is not None and sol["cost"] < float(cost_out[rx, ry, f]) - 1e-9: cost0_keep = float(cost0_out[rx, ry, f]) store(rx, ry, f, sol) @@ -2669,6 +2778,13 @@ def peaks_at(rx, ry): cost0_out[rx, ry, f] = cost0_keep rescued_out[rx, ry] = True + n_jobs = 1 if n_workers == 1 else 4 * n_workers + run_parallel( + [rescue_list[k::n_jobs] for k in range(n_jobs) if rescue_list[k::n_jobs]], + rescue, + "neighbor rescue", + ) + n_maps = len(oms) cost_f = torch.nan_to_num(cost_out, nan=torch.inf) cost_phase = torch.full((R, C, n_maps), torch.inf, dtype=torch.float64) diff --git a/src/quantem/diffraction/bragg_vectors.py b/src/quantem/diffraction/bragg_vectors.py index 156a93d4b..4116de842 100644 --- a/src/quantem/diffraction/bragg_vectors.py +++ b/src/quantem/diffraction/bragg_vectors.py @@ -1373,17 +1373,28 @@ def peak_histogram(self, *, returnfig: bool = False, **kwargs): # detector calibration # ------------------------------------------------------------------ - def measure_origins(self, search_radius: float = 6.0, robust: bool = True, plot: bool = False): + def measure_origins( + self, + search_radius: float = 6.0, + robust: bool = True, + plot: bool = False, + center=None, + ): """Fit the diffraction origin at every probe position. The direct beam wanders with the probe (descan). The brightest peak - within `search_radius` of the detector centre gives the origin at - each position, and a plane fit over the scan smooths the result. + within `search_radius` of `center` gives the origin at each + position, and a plane fit over the scan smooths the result. Parameters ---------- search_radius : float, default=6.0 - Radius in detector pixels searched around the centre. + Radius in detector pixels searched around `center`. + center : tuple of float, optional + ``(row, col)`` to search around; defaults to the detector + centre. Pass the beam position from + :func:`~quantem.diffraction.disk_detection.estimate_central_beam` + when the beam is not centred on the detector. robust : bool, default=True Reject outliers before the plane fit. plot : bool, default=False @@ -1397,10 +1408,10 @@ def measure_origins(self, search_radius: float = 6.0, robust: bool = True, plot: from quantem.diffraction import calibration return calibration.measure_origins( - self, search_radius=search_radius, robust=robust, plot=plot + self, search_radius=search_radius, robust=robust, plot=plot, center=center ) - def plot_origin_fit(self, origins, search_radius: float = 6.0): + def plot_origin_fit(self, origins, search_radius: float = 6.0, center=None): """Measured origins, the plane fit, and their residual. Parameters @@ -1408,7 +1419,9 @@ def plot_origin_fit(self, origins, search_radius: float = 6.0): origins : np.ndarray ``(scan_row, scan_col, 2)`` origins from :meth:`measure_origins`. search_radius : float, default=6.0 - The radius used to measure them, drawn for reference. + The radius used to measure them. + center : tuple of float, optional + The centre used to measure them. Returns ------- @@ -1417,7 +1430,50 @@ def plot_origin_fit(self, origins, search_radius: float = 6.0): """ from quantem.diffraction import calibration - return calibration.plot_origin_fit(self, origins, search_radius=search_radius) + return calibration.plot_origin_fit( + self, origins, search_radius=search_radius, center=center + ) + + def measure_scan_rotation( + self, + origins=None, + mask_radius: float | None = None, + plot: bool = False, + ) -> float: + """Rotation between the detector and the scan, from the direct beam. + + The center of mass of the direct beam traces the projected potential + gradient over the scan, which is a curl-free field in the scan frame. + Rotating the detector axes until the curl vanishes recovers the angle. + It sets the in-plane orientations and nothing else: zone axes, phases + and the out-of-plane maps do not depend on it. + + The curl is unchanged by a 180 degree rotation, so the answer is this + angle or this angle plus 180. + + Parameters + ---------- + origins : np.ndarray, optional + ``(scan_row, scan_col, 2)`` origins from :meth:`measure_origins`; + defaults to the detector centre. + mask_radius : float, optional + Radius in detector pixels around the origin used for the center + of mass, which keeps the Bragg disks out of it. Set it a little + beyond the direct beam. + plot : bool, default=False + Show the curl and divergence against the trial angle. + + Returns + ------- + float + Counter-clockwise rotation in degrees, in [0, 180). + """ + from quantem.diffraction import calibration + + out = calibration.measure_scan_rotation( + self.dataset, origins=origins, mask_radius=mask_radius, plot=plot + ) + return out[0] if isinstance(out, tuple) else out def calibrate(self, crystal, pixel_size_guess: float, **kwargs): """Measure the reciprocal pixel size and the elliptic distortion. diff --git a/src/quantem/diffraction/calibration.py b/src/quantem/diffraction/calibration.py index ab7f770bb..6b811b4a5 100644 --- a/src/quantem/diffraction/calibration.py +++ b/src/quantem/diffraction/calibration.py @@ -17,12 +17,12 @@ from quantem.diffraction.defaults import MIN_NUMBER_PEAKS -def _measure_raw_origins(bragg_vectors, search_radius: float) -> np.ndarray: +def _measure_raw_origins(bragg_vectors, search_radius: float, center=None) -> np.ndarray: """Brightest-peak origin per position, NaN where nothing is found.""" peaks = bragg_vectors.peaks scan_r, scan_c = peaks.shape[0], peaks.shape[1] H, W = int(bragg_vectors.dataset.shape[-2]), int(bragg_vectors.dataset.shape[-1]) - c0 = np.array([H / 2, W / 2]) + c0 = np.array([H / 2, W / 2]) if center is None else np.asarray(center, dtype=float) meas = np.full((scan_r, scan_c, 2), np.nan) for r in range(scan_r): for c in range(scan_c): @@ -38,11 +38,11 @@ def _measure_raw_origins(bragg_vectors, search_radius: float) -> np.ndarray: return meas -def plot_origin_fit(bragg_vectors, origins: np.ndarray, search_radius: float = 6.0): +def plot_origin_fit(bragg_vectors, origins: np.ndarray, search_radius: float = 6.0, center=None): """Diagnostic for measure_origins: measured vs fit vs residual, both axes.""" import matplotlib.pyplot as plt - meas = _measure_raw_origins(bragg_vectors, search_radius) + meas = _measure_raw_origins(bragg_vectors, search_radius, center) fig, axs = plt.subplots(2, 3, figsize=(13.5, 5.6)) names = ["row", "col"] for k in range(2): @@ -79,19 +79,34 @@ def measure_origins( search_radius: float = 6.0, robust: bool = True, plot: bool = False, + center=None, + min_coverage: float = 0.1, ): """Per-position diffraction origin from the brightest central peak. At each scan position the most intense detected peak within - `search_radius` pixels of the detector center is taken as the direct - beam; a plane is fit over the scan (least squares, optionally with one + `search_radius` pixels of `center` is taken as the direct beam; a plane + is fit over the scan (least squares, optionally with one outlier-rejection pass) to model the descan. Parameters ---------- + search_radius : float, default=6.0 + Radius in detector pixels searched around `center`. + robust : bool, default=True + Reject outliers before the plane fit. plot : bool, default=False Show the fitted origin planes and the residuals of the measured origins against the fit. + center : tuple of float, optional + ``(row, col)`` detector position to search around, e.g. from + :func:`~quantem.diffraction.disk_detection.estimate_central_beam`. + Defaults to the detector centre, which misses a beam that sits + further than `search_radius` from it. + min_coverage : float, default=0.1 + Fraction of scan positions that must yield an origin. Below it the + search has missed the beam, and the fit would be meaningless, so an + error is raised rather than returning a plane through nothing. Returns ------- @@ -99,9 +114,25 @@ def measure_origins( (scan_row, scan_col, 2) plane-fit origins, ready for BraggVectors.correct_peak_origins(). With plot=True, also returns (fig, axs). + + Raises + ------ + ValueError + If fewer than `min_coverage` of the positions have a peak within + `search_radius` of `center`. """ - meas = _measure_raw_origins(bragg_vectors, search_radius) + meas = _measure_raw_origins(bragg_vectors, search_radius, center) scan_r, scan_c = meas.shape[0], meas.shape[1] + coverage = float(np.isfinite(meas[..., 0]).mean()) + if coverage < min_coverage: + H, W = int(bragg_vectors.dataset.shape[-2]), int(bragg_vectors.dataset.shape[-1]) + c0 = (H / 2, W / 2) if center is None else tuple(float(v) for v in center) + raise ValueError( + f"only {coverage:.1%} of scan positions have a peak within " + f"{search_radius:g} px of ({c0[0]:.1f}, {c0[1]:.1f}); the direct beam is " + "elsewhere. Pass center= from estimate_central_beam(dataset.dp_mean), " + "or widen search_radius." + ) ry, rx = np.mgrid[0:scan_r, 0:scan_c] def plane(z, ok): @@ -505,22 +536,30 @@ def measure_scan_rotation( Curl-minimizing rotation in [0, 180); the physical answer is either this angle or this angle + 180. With returnfig=True, also (fig, ax). """ - arr = np.asarray(dataset.array, dtype=float) + arr = dataset.array scan_r, scan_c, H, W = arr.shape - rows = np.arange(H)[:, None] - cols = np.arange(W)[None, :] + rows = np.arange(H, dtype=float)[:, None] + cols = np.arange(W, dtype=float)[None, :] if origins is None: origins = np.zeros((scan_r, scan_c, 2)) origins[..., 0] = H / 2 origins[..., 1] = W / 2 - if mask_radius is not None: - rr = rows[None, None] - origins[..., 0][..., None, None] - cc = cols[None, None] - origins[..., 1][..., None, None] - arr = arr * (rr**2 + cc**2 <= mask_radius**2) - tot = arr.sum(axis=(-2, -1)) - tot[tot <= 0] = 1.0 - com_r = (arr * rows).sum(axis=(-2, -1)) / tot - origins[..., 0] - com_c = (arr * cols).sum(axis=(-2, -1)) / tot - origins[..., 1] + origins = np.asarray(origins, dtype=float) + # one scan row at a time: a float64 copy of a full scan is several times + # the size of the data (19 GB for 256 x 256 x 192 x 192), and the center + # of mass only ever needs one pattern + com_r = np.zeros((scan_r, scan_c)) + com_c = np.zeros((scan_r, scan_c)) + for i in range(scan_r): + block = np.asarray(arr[i], dtype=float) # (scan_c, H, W) + if mask_radius is not None: + rr = rows[None] - origins[i, :, 0][:, None, None] + cc = cols[None] - origins[i, :, 1][:, None, None] + block = block * (rr**2 + cc**2 <= mask_radius**2) + tot = block.sum(axis=(-2, -1)) + tot[tot <= 0] = 1.0 + com_r[i] = (block * rows).sum(axis=(-2, -1)) / tot - origins[i, :, 0] + com_c[i] = (block * cols).sum(axis=(-2, -1)) / tot - origins[i, :, 1] # spatial derivatives of both components over the scan d_rr = np.gradient(com_r, axis=0) diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py index d34bf6750..79a38bade 100644 --- a/src/quantem/diffraction/crystal.py +++ b/src/quantem/diffraction/crystal.py @@ -36,16 +36,20 @@ def direction_indices( - lat_real: torch.Tensor | np.ndarray, d, max_multiple: int = 12 + lat_real: torch.Tensor | np.ndarray, d, max_multiple: int = 12, atol: float = 2e-3 ) -> np.ndarray | None: """Smallest integer [uvw] along a Cartesian direction, or None if the - direction is not a lattice direction with indices up to max_multiple.""" + direction is not a lattice direction with indices up to max_multiple. + + `atol` is the allowed deviation of the normalized indices from integers; + loosen it to index the axes of a pseudo-symmetry, which are lattice + directions of an ideal parent but only nearly so in the real cell.""" A_T_inv = np.linalg.inv(np.asarray(lat_real, dtype=float).T) v = A_T_inv @ np.asarray(d, dtype=float) v = v / np.abs(v).max() for m in range(1, max_multiple + 1): w = v * m - if np.allclose(w, np.round(w), atol=2e-3): + if np.allclose(w, np.round(w), atol=atol): ints = np.round(w).astype(int) g = np.gcd.reduce(np.abs(ints)) return ints // max(g, 1) @@ -176,6 +180,53 @@ def electron_scattering_factor(numbers: torch.Tensor, g: torch.Tensor) -> torch. return (a * (2.0 + b * g2) / (1.0 + b * g2) ** 2).sum(dim=-1) +def _expand_partial_occupancy(atoms: Atoms) -> Atoms: + """One atom per species on every shared site, with its fractional occupancy. + + ASE's CIF reader keeps the majority species of a mixed site and records + the full composition in ``atoms.info['occupancy']``, keyed by the site + index held in ``atoms.arrays['spacegroup_kinds']``. Structures with only + fully occupied sites are returned unchanged. + + Parameters + ---------- + atoms : Atoms + As returned by ``ase.io.read`` on a CIF. + + Returns + ------- + Atoms + The expanded structure, with ``arrays['occupancy']`` set. + """ + occ = atoms.info.get("occupancy") + kinds = atoms.arrays.get("spacegroup_kinds") + if not occ or kinds is None: + return atoms + sites = [occ.get(str(int(k))) for k in kinds] + if all( + s is None or (len(s) == 1 and abs(float(next(iter(s.values()))) - 1.0) < 1e-9) + for s in sites + ): + return atoms + + frac = atoms.get_scaled_positions(wrap=False) + symbols, positions, occupancy = [], [], [] + for i, site in enumerate(sites): + if not site: + symbols.append(atoms[i].symbol) + positions.append(frac[i]) + occupancy.append(1.0) + continue + for element, fraction in site.items(): + symbols.append(element) + positions.append(frac[i]) + occupancy.append(float(fraction)) + out = Atoms(symbols=symbols, scaled_positions=positions, cell=atoms.cell, pbc=atoms.pbc) + out.set_array("occupancy", np.asarray(occupancy, dtype=float)) + out.info.update({k: v for k, v in atoms.info.items() if k != "occupancy"}) + return out + + class Crystal(AutoSerialize): """A crystal structure with kinematical diffraction methods. @@ -210,11 +261,22 @@ def __init__( matching group differs from the cell's own. None matches with the exact symmetry. pseudo_symmetry_intensity_tol : float, default=0.05 - Dimensionless intensity tolerance of the same decision: the - extra operations of the parent group must map the kinematical - intensities of the reflections they relate onto each other - within this fraction of the strongest reflection, otherwise the - patterns are distinguishable and the parent group is rejected. + Largest intensity difference allowed between reflections that a + candidate pseudo-symmetry would make equivalent, as a fraction of + the strongest reflection's kinematical intensity |F|^2. Each + extra rotation is applied to every reflection within 2.0 1/A and + each intensity compared with its image's; if any pair differs by + more than this fraction, the orientations the rotation relates + are distinguishable and it is rejected. 0.05 merges only + orientations whose patterns differ by reflections at 5% of the + strongest; 0.4 also merges orientations told apart only by a + reflection at 40%, appropriate when that reflection is known to + be weak or absent in the data (stacking disorder, cation mixing). + Candidates are the relaxed-position group, the lattice's own + holohedry, and the holohedries of the parent lattices generated + by the strong reflections, so superstructure twin variants are + tested as well. The printout names the reflection pair that + decides each candidate. """ self.atoms = atoms self.name = name if name is not None else atoms.get_chemical_formula() @@ -230,6 +292,9 @@ def __init__( self.occupancy = torch.as_tensor(np.asarray(occupancy, dtype=float)) self._setup_symmetry(symprec, pseudo_symmetry_tol, pseudo_symmetry_intensity_tol) + # the summary states any pseudo-symmetry adopted; when it has been + # shown, the orientation plan does not warn about it again + self._summary_shown = bool(verbose) if verbose: print(self.symmetry_summary()) @@ -247,11 +312,34 @@ def from_ase(cls, atoms: Atoms, name: str | None = None, **kwargs) -> "Crystal": @classmethod def from_cif(cls, file_path: str | Path, name: str | None = None, **kwargs) -> "Crystal": + """Build a Crystal from a CIF file, keeping fractional site occupancies. + + ASE reads a mixed-occupancy site as a single atom of the majority + species, which silently deletes every minority element: a layered + oxide with Sb sharing a site with Fe loads with no Sb at all, and Sb + is by far its strongest scatterer. ASE does record the occupancies it + discarded, so each shared site is expanded here back into one atom + per species, each carrying its fraction in + ``atoms.arrays['occupancy']``, which the structure-factor sum uses. + + Parameters + ---------- + file_path : str or Path + Path to the CIF file. + name : str, optional + Display name; defaults to the chemical formula. + **kwargs + Passed to the Crystal constructor, e.g. `pseudo_symmetry_tol`. + + Returns + ------- + Crystal + """ from ase.io import read atoms = read(file_path) assert isinstance(atoms, Atoms) - return cls(atoms, name=name, **kwargs) + return cls(_expand_partial_occupancy(atoms), name=name, **kwargs) @property def volume(self) -> float: @@ -262,7 +350,7 @@ def lat_recip(self) -> torch.Tensor: """Reciprocal lattice vectors as rows, no 2*pi factor.""" return torch.linalg.inv(self.lat_real).T - def _quick_intensities(self, k_max: float = 1.2) -> tuple[torch.Tensor, torch.Tensor]: + def _quick_intensities(self, k_max: float = 2.0) -> tuple[torch.Tensor, torch.Tensor]: """Kinematical |F|^2 of every reflection with |g| <= k_max (hkl, I), for the pseudo-symmetry intensity check; no thermal factors.""" recip = self.lat_recip @@ -334,47 +422,66 @@ def _setup_symmetry( # the heavy sublattice. Both candidate sets are filtered by the same # intensity test, so an operation is adopted only when it leaves the # kinematical pattern unchanged. - candidates: list[tuple[str, np.ndarray]] = [] + # + # "parent lattice": the lattice generated by the strong reflections + # alone. A superstructure (cation ordering on a rocksalt or layered + # frame) has a larger cell than its parent, and the parent's + # symmetries map the superstructure onto a twin of itself rather than + # onto itself, so neither route above can see them. When the + # superstructure reflections are weak the twins give the same + # pattern, and these are exactly the variants matching must merge. + lat = self.lat_real.numpy() + candidates: list[tuple[str, np.ndarray, np.ndarray]] = [] try: ds_relaxed = spglib.get_symmetry_dataset(cell, symprec=symprec_pseudo) except Exception: ds_relaxed = None if ds_relaxed is not None: - candidates.append(("relaxed positions", ds_relaxed.rotations)) - lattice_cell = ( - self.lat_real.numpy(), - np.zeros((1, 3)), - np.ones(1, dtype=int), - ) + candidates.append(("relaxed positions", ds_relaxed.rotations, lat)) + lattice_cell = (lat, np.zeros((1, 3)), np.ones(1, dtype=int)) try: ds_lattice = spglib.get_symmetry_dataset(lattice_cell, symprec=symprec_pseudo) except Exception: ds_lattice = None if ds_lattice is not None: - candidates.append(("lattice", ds_lattice.rotations)) + candidates.append(("lattice", ds_lattice.rotations, lat)) + candidates += self._parent_lattice_candidates(pseudo_symmetry_tol, intensity_tol) best = None - for route, rotations in candidates: - quats = symmetry_quaternions(rotations, self.lat_real.numpy()) + closest = None # the rejected candidate that came nearest to passing + for route, rotations, lattice in candidates: + quats = symmetry_quaternions(rotations, lattice) if quats.shape[0] <= self.sym_quats.shape[0]: continue pg_cand = spglib.get_pointgroup(rotations)[0].strip() accepted, worst = self._intensity_preserving_subgroup(quats, intensity_tol) - if self.pseudo_symmetry_report.get("candidate") is None or accepted is not None: - self.pseudo_symmetry_report.setdefault("candidate", pg_cand) - self.pseudo_symmetry_report.setdefault("intensity_mismatch", worst) if accepted is None: + if closest is None or worst < closest[0]: + closest = (worst, pg_cand, route, self._breaking_reflection) continue if best is None or accepted.shape[0] > best[1].shape[0]: - best = (route, accepted, pg_cand, worst, quats.shape[0]) + best = (route, accepted, pg_cand, quats.shape[0]) if best is None: - if "candidate" in self.pseudo_symmetry_report: - self.pseudo_symmetry_report["rejected"] = True + if closest is not None: + worst, pg_cand, route, pair = closest + self.pseudo_symmetry_report.update( + candidate=pg_cand, + intensity_mismatch=worst, + broken_by=pair, + route=route, + rejected=True, + ) return - route, accepted, pg_cand, worst, n_cand = best + route, accepted, pg_cand, n_cand = best + # the largest difference among the rotations actually adopted + _, worst = self._intensity_preserving_subgroup(accepted, 1.0) self.pseudo_symmetry_report.update( - candidate=pg_cand, intensity_mismatch=worst, route=route, rejected=False + candidate=pg_cand, + intensity_mismatch=worst, + broken_by=self._breaking_reflection, + route=route, + rejected=False, ) self.sym_quats_matching = accepted # name the accepted group by the candidate symbol when every one of @@ -386,6 +493,81 @@ def _setup_symmetry( self.pointgroup_matching = f"{accepted.shape[0]} rotations" self.laue_group_matching = self.laue_group + def _parent_lattice_candidates( + self, pseudo_symmetry_tol: float, intensity_tol: float + ) -> list[tuple[str, np.ndarray, np.ndarray]]: + """Holohedries of the lattices generated by the strong reflections. + + For each intensity cut, the parent translations are the fractions t + of the cell with h.t integer for every reflection h stronger than + the cut; they form the real-space lattice dual to the strong + reflections. Its holohedry is a candidate group. Cuts run from very + low (the cell's own lattice once centring is removed) up to the + intensity tolerance, since reflections weaker than that are allowed + to break the pseudo-symmetry anyway. + + Returns + ------- + list of tuple + ``(route, rotations, lattice)``: integer rotations in the basis of + ``lattice``, whose rows are the parent vectors in the crystal's + own Cartesian frame. + """ + import itertools + + import spglib + + hkl, inten = self._quick_intensities() + if inten.numel() == 0 or float(inten.max()) <= 0: + return [] + inten = inten / inten.max() + lat = self.lat_real.numpy() + vol = abs(float(np.linalg.det(lat))) + # denominators 1, 2, 3, 4, 6, 12 cover the supercells met in practice + n_grid = 12 + grid = np.array(list(itertools.product(range(n_grid), repeat=3)), dtype=float) / n_grid + + out: list[tuple[str, np.ndarray, np.ndarray]] = [] + seen: set[int] = set() + cuts = sorted({0.02, 0.05, 0.1, 0.2, 0.3, 0.5, float(intensity_tol)}) + for cut in cuts: + if cut > 1.0: + continue + strong = hkl[inten > cut].numpy().astype(float) + if strong.shape[0] < 3 or np.linalg.matrix_rank(strong) < 3: + continue + phase = strong @ grid.T + t = grid[np.all(np.abs(phase - np.round(phase)) < 1e-6, axis=0)] + if t.shape[0] < 2: + continue # the cell is its own parent: nothing new + try: + parent = spglib.standardize_cell( + (lat, t, np.ones(t.shape[0], dtype=int)), + to_primitive=True, + no_idealize=True, + symprec=1e-5, + ) + except Exception: + parent = None + if parent is None: + continue + L = np.asarray(parent[0], dtype=float) + ratio = int(round(vol / abs(float(np.linalg.det(L))))) + if ratio in seen: + continue + seen.add(ratio) + a_min = float(np.linalg.norm(L, axis=1).min()) + try: + ds = spglib.get_symmetry_dataset( + (L, np.zeros((1, 3)), np.ones(1, dtype=int)), + symprec=float(pseudo_symmetry_tol) * a_min, + ) + except Exception: + ds = None + if ds is not None: + out.append((f"parent lattice, {ratio}x smaller cell", ds.rotations, L)) + return out + def _intensity_preserving_subgroup( self, quats: torch.Tensor, intensity_tol: float ) -> tuple[torch.Tensor | None, float]: @@ -411,11 +593,25 @@ def _intensity_preserving_subgroup( Rs = quat_to_matrix(quats) Rs_true = quat_to_matrix(self.sym_quats) + # a pseudo-symmetry group comes from a lattice that is only nearly + # ideal, so its rotations and their products agree to the distortion + # (~1e-2 here, ~1e-6 for a hexagonal cell given to 5 decimals). + # Distinct crystallographic rotations are at least 60 degrees apart, + # with matrix entries differing by ~0.5, so 0.05 is unambiguous. + match_tol = 0.05 + def is_true(R): - return any(float((R - Rt).abs().max()) < 1e-6 for Rt in Rs_true) + return any(float((R - Rt).abs().max()) < match_tol for Rt in Rs_true) mismatch = torch.zeros(quats.shape[0], dtype=torch.float64) worst = 0.0 + self._breaking_reflection = None + # a pseudo-symmetry holds only approximately in the metric too, so an + # image lands near, not on, the reflection it maps onto: snap it to + # the nearest one within the same fractional tolerance allowed for + # the atom positions, and treat anything farther as absent + snap = max(float(self._pseudo_symmetry_tol or 0.0), 1e-3) + g_len = torch.linalg.norm(g, dim=1) for i, R in enumerate(Rs): if is_true(R): continue @@ -423,8 +619,24 @@ def is_true(R): hkl_img = torch.round(g_img @ self.lat_real.T).to(torch.long) idx = torch.tensor([lut.get(tuple(h), -1) for h in hkl_img.tolist()]) ok = idx >= 0 - m = float((inten[ok] - inten[idx[ok]]).abs().max()) / i_max if bool(ok.any()) else 0.0 + if not bool(ok.any()): + continue + near = torch.linalg.norm(g_img - g[idx.clamp(min=0)], dim=1) <= snap * g_len + 1e-9 + i_img = torch.where(near, inten[idx.clamp(min=0)], torch.zeros_like(inten)) + diff = (inten[ok] - i_img[ok]).abs() + m = float(diff.max()) / i_max mismatch[i] = m + if m > worst: + # the reflection pair responsible, reported so the user can + # judge whether the data actually resolve it + j = int(torch.argmax(diff)) + src = torch.nonzero(ok).squeeze(1)[j] + self._breaking_reflection = ( + tuple(int(v) for v in hkl[src].tolist()), + float(inten[src]) / i_max, + tuple(int(v) for v in hkl[idx[src]].tolist()), + float(i_img[src]) / i_max, + ) worst = max(worst, m) keep = mismatch <= intensity_tol @@ -437,7 +649,7 @@ def is_true(R): R_keep = Rs[idx] prod = torch.einsum("aij,bjk->abik", R_keep, R_keep).reshape(-1, 3, 3) d = (prod[:, None] - R_keep[None]).abs().amax(dim=(-1, -2)) - closed = bool((d.min(dim=1).values < 1e-6).all()) + closed = bool((d.min(dim=1).values < match_tol).all()) if closed: return quats[idx], worst drop = idx[int(torch.argmax(mismatch[idx]))] @@ -570,8 +782,14 @@ def zone_axis_wedge(self) -> torch.Tensor | None: @property def hexagonal_matching(self) -> bool: - """Whether the matching Laue class uses 4-index direction symbols.""" - return self.laue_group_matching in ("6/m", "6/mmm", "-3", "-3m") + """Whether directions are written with 4-index symbols. + + Directions are always indexed in the crystal's own cell, so this + follows that cell's Laue class, not the matching group's: a + monoclinic superstructure matched with a trigonal parent group still + has a monoclinic cell, and Miller-Bravais indices would be wrong. + """ + return self.laue_group in ("6/m", "6/mmm", "-3", "-3m") def zone_axis_wedge_labels(self, mathtext: bool = True) -> list[str] | None: """Direction labels of the wedge corners (4-index for hexagonal and @@ -579,14 +797,23 @@ def zone_axis_wedge_labels(self, mathtext: bool = True) -> list[str] | None: corners = self.zone_axis_wedge() if corners is None: return None - return [ - format_direction( - direction_indices(self.lat_real, c.numpy()), - hexagonal=self.hexagonal_matching, - mathtext=mathtext, + loose = max(2.0 * float(self._pseudo_symmetry_tol or 0.0), 0.02) + labels = [] + for c in corners: + uvw = direction_indices(self.lat_real, c.numpy()) + prefix = "" + if uvw is None: + # an axis of the pseudo-symmetry parent, a lattice direction + # only to within the distortion of the real cell + uvw = direction_indices(self.lat_real, c.numpy(), atol=loose) + prefix = "~" + labels.append( + prefix + + format_direction(uvw, hexagonal=self.hexagonal_matching, mathtext=mathtext) + if uvw is not None + else "(irrational)" ) - for c in corners - ] + return labels def matching_symmetry_warning(self) -> str | None: """Message when the matching (pseudo) symmetry differs from the @@ -599,7 +826,8 @@ def matching_symmetry_warning(self) -> str | None: f"pseudo-symmetry point group {self.pointgroup_matching} (Laue " f"class {self.laue_group_matching}, found at pseudo_symmetry_tol = " f"{self._pseudo_symmetry_tol:g} of the shortest lattice vector, " - f"intensities matching within {self.pseudo_symmetry_report.get('intensity_mismatch', 0.0):.1%}), " + f"intensities matching within {self.pseudo_symmetry_report.get('intensity_mismatch', 0.0):.2f} " + "of the strongest reflection), " f"while the cell's own symmetry " f"is {self.pointgroup} (Laue class {self.laue_group}). Orientations " f"related by the extra operations give the same library entry, so " @@ -627,14 +855,37 @@ def symmetry_summary(self) -> str: f"(Laue class {self.laue_group_matching}) " "-- used for orientation matching", ] + rep = self.pseudo_symmetry_report + if rep.get("route"): + lines += [f" from the {rep['route']}"] + if rep.get("intensity_mismatch", 0.0) > 0 and rep.get("broken_by") is not None: + h0, i0, h1, i1 = rep["broken_by"] + lines += [ + " accepted at intensity tol %.2f: largest difference " + "(%s) at %.2f against (%s) at %.2f" + % ( + self._pseudo_symmetry_intensity_tol, + " ".join(map(str, h0)), + i0, + " ".join(map(str, h1)), + i1, + ) + ] elif self._pseudo_symmetry_tol is not None: rep = self.pseudo_symmetry_report if rep.get("rejected"): lines += [ - f" pseudo-symmetry {rep['candidate']} within {self._pseudo_symmetry_tol:g} of " - f"the lattice, rejected: intensities differ by " - f"{rep['intensity_mismatch']:.1%} (tol {self._pseudo_symmetry_intensity_tol:.0%})", + f" pseudo-symmetry {rep['candidate']} from the {rep.get('route', 'lattice')}, " + f"rejected: intensities differ by " + f"{rep['intensity_mismatch']:.2f} (tol {self._pseudo_symmetry_intensity_tol:.2f})", ] + if rep.get("broken_by") is not None: + h0, i0, h1, i1 = rep["broken_by"] + lines += [ + " broken by (%s) at %.2f against (%s) at %.2f of the " + "strongest reflection" + % (" ".join(map(str, h0)), i0, " ".join(map(str, h1)), i1) + ] else: lines += [ f" pseudo-symmetry none found at tol = {self._pseudo_symmetry_tol:g} " diff --git a/src/quantem/diffraction/crystal_map.py b/src/quantem/diffraction/crystal_map.py index 6f5f0ccd1..32bc00f56 100644 --- a/src/quantem/diffraction/crystal_map.py +++ b/src/quantem/diffraction/crystal_map.py @@ -29,6 +29,25 @@ from quantem.diffraction.phase import PhaseMap +def _common_k_max(crystals, k_max: float | None) -> float: + """The one k_max every crystal is simulated to, or an error saying why not.""" + if k_max is not None: + return float(k_max) + have = {xtl.name: xtl.k_max for xtl in crystals} + missing = [n for n, k in have.items() if k is None] + if missing: + raise ValueError( + f"no structure factors for {missing}: pass k_max= to CrystalMap.from_vectors, " + "which computes them for every crystal" + ) + if len({round(float(k), 9) for k in have.values()}) > 1: + raise ValueError( + f"crystals are simulated to different k_max {have}; pass k_max= to " + "CrystalMap.from_vectors to set one range for all of them" + ) + return float(next(iter(have.values()))) + + class CrystalMap(AutoSerialize): """Per-position crystal orientation and phase over a scan. @@ -45,11 +64,15 @@ class CrystalMap(AutoSerialize): Crystal names, used for indexing and plot labels. phases : PhaseMap or None The phase decision, populated by :meth:`fit`. + dynamical : dict or None + The last :meth:`refine_dynamical` result. """ _token = object() - def __init__(self, orientation_maps: list[OrientationMap], _token=None): + def __init__( + self, orientation_maps: list[OrientationMap], _token=None, k_max: float | None = None + ): if _token is not self._token: raise RuntimeError( "Use CrystalMap.from_vectors() or CrystalMap.from_orientation_maps()." @@ -67,7 +90,9 @@ def __init__(self, orientation_maps: list[OrientationMap], _token=None): raise ValueError(f"all crystals must share one scan shape, got {shapes}") self.orientation_maps = orientation_maps self.phases: PhaseMap | None = None + self.dynamical: dict | None = None self.metadata: dict = {} + self.k_max = _common_k_max([om.crystal for om in orientation_maps], k_max) # ------------------------------------------------------------------ # construction @@ -81,6 +106,7 @@ def from_vectors( energy_ev: float = 300e3, precession_deg: float = 0.0, semiconv_mrad: float = 0.0, + k_max: float | None = None, ) -> "CrystalMap": """Build one OrientationMap per crystal from a shared peak table. @@ -93,8 +119,26 @@ def from_vectors( work uses the same entry point. energy_ev, precession_deg, semiconv_mrad Passed to :meth:`OrientationMap.from_vectors`. + k_max : float, optional + Largest scattering vector (1/Angstroms) in the simulated patterns, + applied to every crystal: their structure factors are computed + here, so it is set once. Match it to the detector; reflections + beyond it cannot be paired. None keeps the structure factors the + crystals already have, which must then share one k_max, since two + phases simulated to different ranges are not compared fairly. + + Raises + ------ + ValueError + If `k_max` is None and the crystals have no structure factors, or + have them to different ranges. """ xtls = list(crystals) if isinstance(crystals, (list, tuple)) else [crystals] + if k_max is not None: + for xtl in xtls: + xtl.calculate_structure_factors(k_max=float(k_max)) + else: + _common_k_max(xtls, None) # say what is wrong before anything is built oms = [ OrientationMap.from_vectors( peaks, @@ -105,7 +149,7 @@ def from_vectors( ) for xtl in xtls ] - return cls(oms, _token=cls._token) + return cls(oms, _token=cls._token, k_max=k_max) @classmethod def from_orientation_maps(cls, orientation_maps: list[OrientationMap]) -> "CrystalMap": @@ -215,8 +259,9 @@ def refine_orientations(self, overrides: dict | None = None, **kwargs) -> "Cryst Least squares on the paired peak positions removes the quantization of the plan: the in-plane rotation comes from the pairing, the zone - axis tilt from the intensity envelope. A second pass rescues - positions whose answer disagrees with all of their neighbours. + axis tilt from the intensity envelope. Positions that disagree with + a neighbour are then retried from the candidates around them, and + ties go to the orientation the neighbours share. Parameters ---------- @@ -254,6 +299,157 @@ def fit(self, **kwargs) -> "CrystalMap": self.phases.fit(**kwargs) return self + def refine_dynamical(self, mask=None, **kwargs) -> "CrystalMap": + """Dynamical refinement of orientation, thickness, strain and phase. + + Bloch-wave intensities, averaged over the precession ring, are fit to + the measured peaks of every candidate at every position of `mask`, + and the candidate with the lowest cost decides the phase there; the + rest of the scan keeps its decision. See + :func:`~quantem.diffraction.bloch.refine_dynamical` for the model and + every argument. The refined orientations are written back, so calls + can be staged: thickness and tilt first with + ``refine_deformation=False``, then the in-plane strain from those + orientations with a narrower tilt search. + + Every crystal is given absorptive structure factors out to twice + `k_max`, which the couplings between beams need; they are computed + here when missing or too short. + + Parameters + ---------- + mask : np.ndarray or list of tuple, optional + Positions to refine, an (R, C) boolean mask or (row, col) list. + None refines every matched position, which takes hours. + k_max : float, optional + Largest |g| (1/Angstroms) of the beams in the Bloch calculation. + Defaults to the k_max of the kinematical simulation. Cutting it + low saves time but drops beams that carry real dynamical + coupling. + **kwargs + Passed to :func:`~quantem.diffraction.bloch.refine_dynamical`. + `require_phase_weight` defaults to False here, so a crystal the + kinematical fit rejected still competes. + + Returns + ------- + CrystalMap + Self, with the result in :attr:`dynamical`. + """ + from quantem.diffraction import bloch + + pm = self._require_fit("refine_dynamical()") + k_max = float(kwargs.pop("k_max", None) or self.k_max) + energy_ev = self.orientation_maps[0].energy_ev + for om in self.orientation_maps: + xtl = om.crystal + if ( + getattr(xtl, "U_dyn", None) is None + or getattr(xtl, "dyn_k_max", 0.0) < 2 * k_max - 1e-9 + or abs(getattr(xtl, "dyn_energy_ev", energy_ev) - energy_ev) > 1.0 + ): + xtl.calculate_dynamical_structure_factors(energy_ev, k_max=2 * k_max) + kwargs["k_max"] = k_max + kwargs.setdefault("require_phase_weight", False) + self.dynamical = bloch.refine_dynamical(pm, mask=mask, **kwargs) + pm.apply_dynamical(self.dynamical) + return self + + def plot_dynamical(self, phase=None, strain: bool = False, crop: bool = True, **kwargs): + """Maps of the last :meth:`refine_dynamical`. + + Thickness, tilt correction, the cost gain of the tilt search and the + final cost, or with `strain` the six crystal-frame strain components. + + Parameters + ---------- + phase : int or str, optional + Show only positions this crystal won. None shows all of them. + strain : bool, default=False + Plot the strain components instead. + crop : bool, default=True + Crop to the refined positions. + **kwargs + Passed to :func:`~quantem.diffraction.bloch.plot_dynamical_maps` + or :func:`~quantem.diffraction.bloch.plot_strain_crystal_frame`. + + Returns + ------- + tuple + ``(fig, axs)``. + """ + from quantem.diffraction import bloch + + result = getattr(self, "dynamical", None) + if result is None: + raise ValueError("run refine_dynamical() before plot_dynamical().") + i = None if phase is None else self._phase_indices(phase)[0] + maps = bloch.dynamical_maps(result, self.phases, crystal_index=i) + m = maps["mask"].numpy() + sl = (slice(None), slice(None)) + if crop and m.any(): + rows, cols = np.nonzero(m) + sl = (slice(rows.min(), rows.max() + 1), slice(cols.min(), cols.max() + 1)) + kwargs.setdefault("scalebar", self.orientation_maps[0].scan_scalebar) + if strain: + comps = {k: v.numpy()[sl] for k, v in maps["strain"].items()} + return bloch.plot_strain_crystal_frame(comps, mask=m[sl], **kwargs) + cropped = dict(maps) + for k in ("thickness", "tilt_deg", "gain", "cost"): + cropped[k] = maps[k][sl] + return bloch.plot_dynamical_maps(cropped, **kwargs) + + def example_positions( + self, + phase=None, + num: int = 4, + ambiguous: bool = False, + min_distance: float = 16.0, + min_signal: float = 0.3, + ) -> list[tuple[int, int]]: + """Well-separated probe positions for inspecting the phase decision. + + The clearest examples of a crystal are where it won by the largest + margin (the phase reliability); the ambiguous ones are where the two + best crystals scored closest. Only positions that diffract are + considered, and each pick is at least `min_distance` from the others, + so the examples come from different parts of the scan. + + Parameters + ---------- + phase : int or str, optional + Crystal the positions must have been assigned to. None allows + any crystal. + num : int, default=4 + Number of positions. + ambiguous : bool, default=False + Pick the closest decisions instead of the clearest. + min_distance : float, default=16.0 + Smallest separation between picks, in probe positions. + min_signal : float, default=0.3 + Smallest :meth:`signal_confidence` a position needs. + + Returns + ------- + list of tuple of int + ``(row, col)`` positions, clearest (or closest) first. + """ + pm = self._require_fit("example_positions()") + ph = self.phase_index + rel = np.asarray(pm.reliability, dtype=float) + ok = (ph >= 0) & np.isfinite(rel) & (self.signal_confidence() >= min_signal) + if phase is not None: + ok &= ph == self._phase_indices(phase)[0] + rc = np.argwhere(ok) + order = np.argsort(rel[ok] if ambiguous else -rel[ok], kind="stable") + picks: list[tuple[int, int]] = [] + for r, c in rc[order]: + if all((r - a) ** 2 + (c - b) ** 2 >= min_distance**2 for a, b in picks): + picks.append((int(r), int(c))) + if len(picks) == num: + break + return picks + # ------------------------------------------------------------------ # derived quantities # ------------------------------------------------------------------ @@ -445,6 +641,9 @@ def plot_matches(self, positions, phase=None, **kwargs): is how a probe straddling two grains shows itself. dataset : Dataset4dstem, optional Show the recorded diffraction pattern behind the overlay. + norm : dict or str, optional + Passed to `show_2d`, which draws that pattern, e.g. + {"power": 0.5, "upper_quantile": 0.98}. measured_scale, measured_power : float, optional Size and intensity compression of the gray measured peaks. transpose_plots : bool, default=False @@ -469,6 +668,52 @@ def plot_matches(self, positions, phase=None, **kwargs): kwargs.setdefault("origins", np.asarray(md["origins"])) return plot_pattern_matches(oms, positions=positions, **kwargs) + def plot_ring_comparison(self, k_min: float = 0.1, k_max: float | None = None, **kwargs): + """Measured radial peak distribution against the rings of every crystal. + + One panel per candidate: the red fill is the histogram of every + calibrated peak, the black lines are that crystal's ring positions. + Run it before matching. With the scale fixed by a standard, a ring + that sits beside the measured peaks means either the reference + lattice parameter is wrong for this specimen or the calibration did + not transfer -- and matching cannot recover from either. + + Parameters + ---------- + k_min : float, default=0.1 + Smallest scattering vector shown, 1/Angstroms. + k_max : float, optional + Largest scattering vector shown; defaults to the map's own k_max. + k_broadening : float, optional + Broaden the rings into a simulated profile; None (default) draws + sharp lines. + **kwargs + Further arguments of + :func:`~quantem.diffraction.calibration.plot_ring_comparison`. + + Returns + ------- + tuple + ``(fig, axs)``. + """ + from quantem.diffraction import calibration + + return calibration.plot_ring_comparison( + self.peaks, + [om.crystal for om in self.orientation_maps], + k_min=k_min, + k_max=k_max if k_max is not None else self._k_max_or_crystals(), + **kwargs, + ) + + def _k_max_or_crystals(self) -> float: + # maps saved before k_max lived on the CrystalMap carry it only on + # their crystals + k = getattr(self, "k_max", None) + if k is None: + k = max(float(om.crystal.k_max or 1.5) for om in self.orientation_maps) + return float(k) + def plot_correlation(self, **kwargs): """Correlation and reliability of every crystal, one panel each. diff --git a/src/quantem/diffraction/illumination.py b/src/quantem/diffraction/illumination.py index e441541ce..2231d10ab 100644 --- a/src/quantem/diffraction/illumination.py +++ b/src/quantem/diffraction/illumination.py @@ -109,7 +109,26 @@ def averaged_gaussian_intensity_envelope( c, a, b = excitation_coefficients(g_lab, energy_ev, precession_deg, semiconv_mrad) if precession_deg <= 0 and semiconv_mrad <= 0: return np.exp(-0.5 * (c / sigma) ** 2), c, a, b - return gaussian_envelope(c, a, b, sigma), c, a, b + # A reflection farther from the Ewald sphere than the illumination sweeps + # it, plus six envelope widths, is never excited (the envelope there is + # below 1e-8). At k_max = 2 that is nearly all of them, and the quadrature + # below costs 400 Bessel evaluations per reflection. + env = np.zeros_like(c) + live = np.abs(c) < a + b + 6.0 * sigma + if live.any(): + if semiconv_mrad <= 0: + # a pure precession ring has the fast Bessel series the orientation + # plan uses; the disk quadrature is needed only with convergence + env[live] = ( + gaussian_envelope_ring_torch( + torch.as_tensor(c[live]), torch.as_tensor(a[live]), sigma + ) + .cpu() + .numpy() + ) + else: + env[live] = gaussian_envelope(c[live], a[live], b[live], sigma) + return env, c, a, b def ring_disk_quadrature(r: float, R: float, n_phi: int = 128, n_r: int = 8, n_psi: int = 32): diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index ae1773a7b..fda671e95 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -274,6 +274,7 @@ def __init__( self.quats: torch.Tensor | None = None self.corr: torch.Tensor | None = None self.corr_residual: torch.Tensor | None = None + self.score: torch.Tensor | None = None self.corr_second: torch.Tensor | None = None self.reliability: torch.Tensor | None = None self.mirror: torch.Tensor | None = None @@ -332,6 +333,7 @@ def build_plan( detector_q_max: float | tuple[float, float] | str | None = "auto", device: str | torch.device = "cpu", verbose: bool = True, + progress_bar: bool = True, ) -> "OrientationMap": """Build the polar correlation library over the fundamental wedge. @@ -415,8 +417,12 @@ def build_plan( silicon 'mps' runs the correlation in float32 (about 1.5x faster than the CPU); the refinements that follow stay on the CPU. verbose : bool, default=True - Print the symmetry actually used for matching (including any - pseudo-symmetry reduction) and the plan size. + Print the plan size and the group used for matching. The crystal + prints its full symmetry, pseudo-symmetry included, when built. + progress_bar : bool, default=True + Show a progress bar while the library is deposited, which is + most of the time: ~20 s per crystal at k_max = 2 over a trigonal + wedge, several times that over a full hemisphere. """ crystal = self.crystal self.device = torch.device(device) @@ -488,7 +494,7 @@ def build_plan( self.num_gamma = int(round(360 / angle_step_in_plane_deg)) self.gamma = torch.linspace(0, 2 * np.pi, self.num_gamma + 1, dtype=torch.float64)[:-1] - plan = self._build_reference(self.zone_quats) + plan = self._build_reference(self.zone_quats, progress_bar=progress_bar) # store conj(fft) along gamma so matching is a single complex matmul self.plan_fft = torch.conj(torch.fft.fft(plan, dim=-1)).to(self.cdtype).to(self.device) @@ -566,14 +572,17 @@ def build_plan( else tuple(np.atleast_1d(detector_q_max).tolist()), ) if verbose: - print(crystal.symmetry_summary()) + # the crystal printed its own symmetry when it was built; the plan + # adds only what it sampled print( - " orientation plan %d zone axes x %d in-plane angles, " - "%d radial shells" + "%s: orientation plan %d zone axes x %d in-plane angles, " + "%d radial shells, matching %s" % ( + crystal.name, self.zone_axes.shape[0], self.gamma.shape[0], self.shell_radii.shape[0], + crystal.pointgroup_matching, ) ) return self @@ -615,7 +624,8 @@ def cartesian(uvw) -> torch.Tensor: 'zone_axis_range must be "auto", "full", "fiber", or an array of directions' ) msg = crystal.matching_symmetry_warning() - if msg is not None: + # a crystal built with verbose=True already said this in its summary + if msg is not None and not getattr(crystal, "_summary_shown", False): warnings.warn(msg, stacklevel=3) wedge = crystal.zone_axis_wedge() if wedge is None: # triclinic / monoclinic: not a spherical triangle @@ -640,6 +650,7 @@ def _deposit_polar( amp: torch.Tensor, out: torch.Tensor, image: torch.Tensor | None = None, + progress: str | None = None, ) -> torch.Tensor: """Deposit peaks into polar images with the shared correlation kernel. @@ -658,6 +669,8 @@ def _deposit_polar( image : torch.Tensor | None (K,) image index of every peak, so a whole batch of patterns (or a whole library) is deposited in one call. + progress : str | None + Description for a progress bar over the chunks; None shows none. """ radii = self.shell_radii.to(qr.dtype) delta = self.corr_kernel_size @@ -671,7 +684,10 @@ def _deposit_polar( flat = out.view(-1, out.shape[-1]) # chunked so the (entries, G) weight array stays a few tens of MB chunk = max(1, 4_000_000 // gamma.shape[0]) - for c0 in range(0, k_all.numel(), chunk): + starts = range(0, k_all.numel(), chunk) + if progress is not None: + starts = tqdm(starts, desc=progress) + for c0 in starts: k_idx = k_all[c0 : c0 + chunk] s_idx = s_all[c0 : c0 + chunk] w_r = torch.exp(-(dr[k_idx, s_idx] ** 2) / (2 * delta**2)) * amp[k_idx] @@ -683,7 +699,9 @@ def _deposit_polar( flat.index_add_(0, rows, w) return out - def _build_reference(self, zone_quats: torch.Tensor) -> torch.Tensor: + def _build_reference( + self, zone_quats: torch.Tensor, progress_bar: bool = False + ) -> torch.Tensor: """Polar reference library (Z, S, G) for the given zone-axis quats.""" crystal = self.crystal lam = self.wavelength @@ -732,7 +750,12 @@ def _build_reference(self, zone_quats: torch.Tensor) -> torch.Tensor: plan = torch.zeros((Z, self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64) z_idx, n_idx = torch.nonzero(vals > 1e-8, as_tuple=True) self._deposit_polar( - qr[z_idx, n_idx], qphi[z_idx, n_idx], vals[z_idx, n_idx], plan, image=z_idx + qr[z_idx, n_idx], + qphi[z_idx, n_idx], + vals[z_idx, n_idx], + plan, + image=z_idx, + progress=f"orientation plan {crystal.name}" if progress_bar else None, ) norm = torch.linalg.norm(plan.reshape(Z, -1), dim=1).clamp_min(1e-12) @@ -872,10 +895,8 @@ def match_orientations( the in-plane angle together: a candidate is rejected only when it is within this angle of an earlier match in both. Two grains sharing a zone axis but rotated in plane past this angle are - therefore kept as separate matches. - Exclusion radius (degrees, zone-axis distance) around earlier - matches, both for later matches and for the second-best score - used in `reliability`. + therefore kept as separate matches. The same test picks the + second-best score used in `reliability`. subpixel_gamma : bool, default=True Parabolic sub-bin refinement of the in-plane angle. subpixel_zone : bool, default=True @@ -1233,7 +1254,7 @@ def smooth_orientations( self.quats[..., match, :] = smooth_quaternions( self.quats[..., match, :], active, - self.crystal.sym_quats, + self.crystal.sym_quats_matching, sigma_px=sigma_px, sigma_deg=sigma_deg, max_angle_deg=max_angle_deg, @@ -1255,7 +1276,7 @@ def smoothed_quats(self, match: int = 0, **kwargs) -> torch.Tensor: active = active & self.computed active = active & (self.corr[..., match] > 0) return smooth_quaternions( - self.quats[..., match, :], active, self.crystal.sym_quats, **kwargs + self.quats[..., match, :], active, self.crystal.sym_quats_matching, **kwargs ) def refine_orientations( @@ -1274,6 +1295,9 @@ def refine_orientations( batched: bool = True, neighbor_rescue: bool = True, rescue_threshold_deg: float = 2.0, + rescue_passes: int = 3, + score_tol: float = 0.002, + consensus_tol: float = 0.01, progress_bar: bool = True, ) -> "OrientationMap": """Refine matched orientations by least squares on paired peak positions. @@ -1324,30 +1348,63 @@ def refine_orientations( Half-range of the envelope tilt search, in degrees. zone_max_total_deg : float | None Trust region: cap on the cumulative envelope tilt applied to + each orientation, relative to its matched start. The coarse + match is grid-accurate to about half the zone-axis step, so tilt + corrections beyond that scale are noise walking the orientation + out of its basin. Defaults to 0.75 * the plan's zone step. power_intensity : float | None Power applied to the measured and predicted intensities in the tilt envelope fit, inherited from the plan (0.25 by default). Linear intensities let the strongest reflections dominate and, on dynamical data, drive the fit to the edge of the search range. - each orientation, relative to its matched start. The coarse - match is grid-accurate to about half the zone-axis step, so tilt - corrections beyond that scale are noise walking the orientation - out of its basin. Defaults to 0.375 * the plan's zone step. sigma_envelope : float | None Excitation-error width of the envelope objective; defaults to half the plan's sigma_excitation (the plan value is widened for grid robustness). neighbor_rescue : bool, default=True - Second pass over positions whose best match disagrees with every - neighbor by more than rescue_threshold_deg: re-refine from every - distinct candidate orientation -- all matches of all eight - neighbors, and this position's own remaining matches -- and keep - the highest-scoring result (score = total paired measured - intensity). Repairs isolated wrong local optima such as - near-degenerate variants, and probe positions straddling two - grains, where the correct orientation is often the second match. + Retry every position that disagrees with a matched neighbour by + more than `rescue_threshold_deg`, from every distinct candidate + around it: all matches of the eight neighbours, this position's + own other matches, and its Friedel twin (the orientation rotated + 180 degrees about the beam). The one that best explains the + measured peaks is kept, judged by the same correlation matching + maximizes (see below); among candidates within `consensus_tol` + of the best, the one most neighbours agree with. Repairs wrong + local optima, near-degenerate variants, and probe positions + straddling two grains. rescue_threshold_deg : float, default=2.0 - Minimum-neighbor misorientation that triggers the rescue pass. + Misorientation to a neighbour that triggers a retry. + rescue_passes : int, default=3 + Rescue passes; each after the first revisits only positions next + to a change, since a corrected neighbour can offer a better + candidate. + score_tol : float, default=0.002 + Correlation margin. A refinement that moves an orientation by + more than `rescue_threshold_deg` is undone where it lowers the + correlation below the library match's by more than this, and a + rescue candidate replaces the current orientation only when it + beats it by more than this. + consensus_tol : float, default=0.01 + Correlation within which two candidates count as equally good. + Sparse patterns often cannot tell a few orientations apart: + pseudo-symmetric variants whose distinguishing reflections were + not recorded, and always the Friedel twin, as kinematic spot + positions are centrosymmetric and only the Ewald curvature + separates the two. Such ties are broken by agreement with the + eight neighbours; the Friedel twin is adopted only this way, + never on its score alone. 0 judges every position by its own + pattern alone. + + Notes + ----- + Every orientation is judged by the correlation it gives with the + measured peaks -- the cosine similarity of the two patterns built + from the library's Gaussian pairing kernel -- and the result is + stored in :attr:`score`. Refinement itself works on paired peak + positions, a different objective; on sparse or ambiguous patterns it + can move an orientation downhill, and scoring every candidate the + same way is what keeps the stages consistent. The counts of reverted + refinements and rescued positions are in ``metadata['refine']``. """ assert self.quats is not None plan_md = self.metadata.get("plan") @@ -1370,6 +1427,9 @@ def refine_orientations( sigma_envelope=sigma_envelope, neighbor_rescue=bool(neighbor_rescue), rescue_threshold_deg=float(rescue_threshold_deg), + rescue_passes=int(rescue_passes), + score_tol=float(score_tol), + consensus_tol=float(consensus_tol), ) peaks = self.peaks R, C, M = self.quats.shape[:3] @@ -1544,6 +1604,12 @@ def get_exp(rx, ry): active = position_mask(positions, (R, C)) if self.computed is not None: active = active & self.computed + # the library matches, kept so refinement can be undone where it + # made the fit worse + q_start = self.quats.clone() + # one bar per crystal covers refinement and neighbour rescue; each + # stage adds its own work to the total as it starts + bar = tqdm(total=0, desc=f"refining {self.crystal.name}") if progress_bar else None if batched and not refine_tilt: self._refine_batched( scores, @@ -1557,13 +1623,16 @@ def get_exp(rx, ry): min_pairs=min_pairs, refine_zone=refine_zone, power_env=power_env, - progress_bar=progress_bar, + progress_bar=bar if bar is not None else False, ) else: iterator = [(rx, ry) for rx, ry in np.ndindex(R, C) if active[rx, ry]] - if progress_bar: - iterator = tqdm(iterator, desc="refining orientations") + if bar is not None: + bar.total = (bar.total or 0) + len(iterator) + bar.refresh() for rx, ry in iterator: + if bar is not None: + bar.update(1) q_exp, w_exp = get_exp(rx, ry) if q_exp is None: continue @@ -1575,73 +1644,245 @@ def get_exp(rx, ry): if m == 0: scores[rx, ry] = sc - if neighbor_rescue: - # a wrong local optimum (e.g. a near-degenerate variant) shows as - # a discontinuity: retry those positions from each distinct - # neighbor orientation and keep the best-scoring result - q0 = self.quats[..., 0, :] - miso_min = torch.full((R, C), torch.inf, dtype=torch.float64) - for dr, dc in ((0, 1), (1, 0)): - a = q0[: R - dr, : C - dc] - b = q0[dr:, dc:] - mm = misorientation_angle_deg( - a.reshape(-1, 4), b.reshape(-1, 4), self.crystal.sym_quats - ).reshape(R - dr, C - dc) - miso_min[: R - dr, : C - dc] = torch.minimum(miso_min[: R - dr, : C - dc], mm) - miso_min[dr:, dc:] = torch.minimum(miso_min[dr:, dc:], mm) - retry = torch.nonzero((miso_min > rescue_threshold_deg) & active) - it2 = retry.tolist() - if progress_bar and len(it2): - it2 = tqdm(it2, desc="neighbor rescue") - n_rescued = 0 - for rx, ry in it2: - q_exp, w_exp = get_exp(rx, ry) - if q_exp is None: + # Refinement polishes the orientation on paired peak positions, which + # is accurate for small corrections but can jump to another basin on + # sparse or ambiguous patterns. A polish within `rescue_threshold_deg` + # is trusted: the correlation depends on the excitation envelope, + # which is only approximately known, so it cannot referee sub-degree + # moves. A jump beyond that must explain the measured peaks better + # than the library match did, or it is undone. + act_list = [(rx, ry) for rx, ry in np.ndindex(R, C) if active[rx, ry]] + if bar is not None: + bar.set_description(f"{self.crystal.name} checking against the library match") + bar.total += len(act_list) + bar.refresh() + cscore = torch.zeros((R, C), dtype=torch.float64) + n_reverted = 0 + for rx, ry in act_list: + if bar is not None: + bar.update(1) + data = peaks[rx, ry].array + meas = self._measured_term(data, ix) + for m in range(M): + if self.corr[rx, ry, m] <= 0: continue - best_q = self.quats[rx, ry, 0] - best_s = float(scores[rx, ry]) - cands = [] - - def _add(qn, cands=cands): - if all( - float(misorientation_angle_deg(qn, c, self.crystal.sym_quats)) > 0.5 - for c in cands - ): - cands.append(qn) - - # where two grains overlap in one probe, the right orientation - # is often this position's own second match rather than the - # first, so try every candidate here as well as every - # candidate of every neighbour - for m in range(1, M): - if self.corr[rx, ry, m] > 0: - _add(self.quats[rx, ry, m]) + s_new = self._correlation_score(self.quats[rx, ry, m], data, ix, meas) + moved = float( + misorientation_angle_deg( + self.quats[rx, ry, m], q_start[rx, ry, m], self.crystal.sym_quats_matching + ) + ) + if moved > rescue_threshold_deg: + s_old = self._correlation_score(q_start[rx, ry, m], data, ix, meas) + if s_new < s_old - score_tol: + self.quats[rx, ry, m] = q_start[rx, ry, m] + s_new = s_old + n_reverted += int(m == 0) + if m == 0: + cscore[rx, ry] = s_new + self.metadata["refine"]["n_reverted"] = int(n_reverted) + + if neighbor_rescue: + # A wrong local optimum shows as a position disagreeing with a + # neighbour. Retry every such position from every distinct + # candidate around it -- all matches of the eight neighbours, + # this position's own other matches, and its Friedel twin -- and + # keep whichever explains the measured peaks best, by the same + # correlation; candidates within `consensus_tol` of the best are a + # tie, broken by how many neighbours agree. Only matched + # neighbours count, and the comparison is in the group the library + # was built with, where folded variants are one answer. Repeat + # while anything changes, up to `rescue_passes` times. + sym_m = self.crystal.sym_quats_matching + # 180 degrees about the beam: kinematic spot positions are + # centrosymmetric and the excitation errors nearly so, so only the + # Ewald curvature tells the two apart + q_twin = torch.tensor([0.0, 0.0, 0.0, 1.0], dtype=self.quats.dtype) + n_retried = n_rescued = 0 + changed = active.clone() + for _pass in range(max(int(rescue_passes), 0)): + q0 = self.quats[..., 0, :] + miso_max = torch.zeros((R, C), dtype=torch.float64) + for dr, dc in ((0, 1), (1, 0)): + both = active[: R - dr, : C - dc] & active[dr:, dc:] + mm = misorientation_angle_deg( + q0[: R - dr, : C - dc].reshape(-1, 4), q0[dr:, dc:].reshape(-1, 4), sym_m + ).reshape(R - dr, C - dc) + mm = torch.where(both, mm, torch.zeros_like(mm)) + miso_max[: R - dr, : C - dc] = torch.maximum(miso_max[: R - dr, : C - dc], mm) + miso_max[dr:, dc:] = torch.maximum(miso_max[dr:, dc:], mm) + # after the first pass, only where something nearby changed + near_change = changed.clone() for dr in (-1, 0, 1): for dc in (-1, 0, 1): - nr, nc = rx + dr, ry + dc - if (dr == 0 and dc == 0) or not (0 <= nr < R and 0 <= nc < C): - continue - for m in range(M): - # neighbour match absent (never run, or skipped) - if self.corr[nr, nc, m] <= 0: + near_change |= torch.roll(torch.roll(changed, dr, 0), dc, 1) + retry = torch.nonzero((miso_max > rescue_threshold_deg) & active & near_change) + it2 = retry.tolist() + if not it2: + break + if bar is not None: + bar.set_description( + f"{self.crystal.name} neighbor rescue {_pass + 1}/{rescue_passes}" + ) + bar.set_postfix_str(f"{len(it2)} positions") + bar.total += len(it2) + bar.refresh() + changed = torch.zeros((R, C), dtype=torch.bool) + for rx, ry in it2: + if bar is not None: + bar.update(1) + n_retried += 1 + q_exp, w_exp = get_exp(rx, ry) + if q_exp is None: + continue + data = peaks[rx, ry].array + cur_q = self.quats[rx, ry, 0].clone() + cur_s = float(cscore[rx, ry]) + cands = [cur_q] + + def _add(qn, cands=cands): + if all(float(misorientation_angle_deg(qn, c, sym_m)) > 0.5 for c in cands): + cands.append(qn) + + _add(qmult(q_twin, cur_q)) + # none where the twin is a symmetry copy of this orientation + twin_ix = 1 if len(cands) == 2 else None + nbrs = [] + for m in range(1, M): + if self.corr[rx, ry, m] > 0: + _add(self.quats[rx, ry, m]) + for dr in (-1, 0, 1): + for dc in (-1, 0, 1): + nr, nc = rx + dr, ry + dc + if (dr == 0 and dc == 0) or not (0 <= nr < R and 0 <= nc < C): continue - _add(self.quats[nr, nc, m]) - for qc in cands: - q, sc = refine_single(qc.clone(), q_exp, w_exp) - if sc > best_s * 1.02: - best_q, best_s = q, sc - if best_s > float(scores[rx, ry]): - n_rescued += 1 - self.quats[rx, ry, 0] = best_q - scores[rx, ry] = best_s - self.metadata["refine"]["n_retried"] = len(retry) + if not bool(active[nr, nc]): + continue + nbrs.append(self.quats[nr, nc, 0]) + for m in range(M): + if self.corr[nr, nc, m] > 0: + _add(self.quats[nr, nc, m]) + # score every candidate as it stands -- the neighbours' + # were refined on a neighbouring pattern already + cq = torch.stack(cands) + meas = self._measured_term(data, ix) + cs = [cur_s] + [ + self._correlation_score(qc, data, ix, meas) for qc in cands[1:] + ] + # polish the best on this pattern, keeping it if it helps + k = int(np.argmax(cs)) + if k > 0: + q_ref, _ = refine_single(cq[k].clone(), q_exp, w_exp) + s_ref = self._correlation_score(q_ref, data, ix, meas) + if s_ref > cs[k]: + cq[k], cs[k] = q_ref, s_ref + cs = np.asarray(cs) + # ties within consensus_tol go to the candidate most + # neighbours agree with, then to the higher correlation + support = np.zeros(len(cs), dtype=int) + if nbrs: + nq = torch.stack(nbrs) + miso = misorientation_angle_deg(cq[:, None, :], nq[None], sym_m) + support = (miso <= rescue_threshold_deg).sum(dim=1).numpy() + tied = cs >= cs.max() - consensus_tol + pick = max(np.flatnonzero(tied), key=lambda j: (support[j], cs[j])) + agreed = ( + consensus_tol > 0 + and support[pick] > support[0] + and cs[pick] >= cur_s - consensus_tol + ) + if not agreed: + # by the pattern alone; the twin never wins here, as + # its pattern differs only by the Ewald curvature and + # a higher score is the forward model's error + own = [j for j in range(len(cs)) if j != twin_ix] + pick = max(own, key=lambda j: cs[j]) + if pick > 0 and (agreed or cs[pick] > cur_s + score_tol): + n_rescued += 1 + changed[rx, ry] = True + self.quats[rx, ry, 0] = cq[pick] + cscore[rx, ry] = float(cs[pick]) + self.metadata["refine"]["n_retried"] = int(n_retried) self.metadata["refine"]["n_rescued"] = int(n_rescued) + if bar is not None: + bar.set_description(f"refined {self.crystal.name}") + bar.set_postfix_str( + f"kept the library match at {n_reverted}" + + ( + f", rescued {self.metadata['refine'].get('n_rescued', 0)}" + if neighbor_rescue + else "" + ) + ) + self.score = cscore + if bar is not None: + bar.close() return self # ------------------------------------------------------------------ # forward simulation of a match # ------------------------------------------------------------------ + def _measured_term(self, data: np.ndarray, ix: list[int]) -> tuple: + """Measured side of :meth:`_correlation_score`: positions, weights + and self-overlap, the same for every candidate at a position.""" + m_xy = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) + m_i = torch.as_tensor(data[:, ix[2]], dtype=torch.float64).clamp_min(0) + w_m = ( + m_i**self.power_intensity_experiment + * torch.linalg.norm(m_xy, dim=1) ** self.power_radial + ) + inv = 1.0 / (4.0 * self.corr_kernel_size**2) + mm = float(w_m @ torch.exp(-(torch.cdist(m_xy, m_xy) ** 2) * inv) @ w_m) + return m_xy, w_m, max(mm, 0.0) + + def _correlation_score( + self, q: torch.Tensor, data: np.ndarray, ix: list[int], measured: tuple | None = None + ) -> float: + """How well orientation `q` explains the measured peaks `data`. + + The cosine similarity between the measured and the simulated + patterns, each a set of Gaussian spots of the library's pairing + width: the continuous form of the correlation matching maximizes, + with the same intensity and radial weights. Unlike a sum of paired + intensity it charges for predicted spots that were not measured, so + a denser pattern does not win by pairing more peaks. Simulated + reflections outside the detector are left out, as in the library. + """ + pd = float(self.metadata.get("precession_deg", 0.0) or 0.0) + sc = float(self.metadata.get("semiconv_mrad", 0.0) or 0.0) + sim = self.crystal.generate_pattern( + q, + energy_ev=self.energy_ev, + sigma_excitation=self.sigma_excitation, + precession_deg=pd, + semiconv_mrad=sc, + ) + s_xy = torch.stack((sim["qx"], sim["qy"]), dim=1).to(torch.float64) + s_i = sim["intensity"].to(torch.float64).clamp_min(0) + det = (self.metadata.get("plan") or {}).get("detector_q_max") + if det is not None and s_xy.shape[0]: + det = np.atleast_1d(det).astype(float) + qx_max, qy_max = (det[0], det[0]) if det.size == 1 else (det[0], det[1]) + rot = np.deg2rad(-float(self.peaks.metadata.get("rotation_ccw_deg", 0.0) or 0.0)) + c, s_ = np.cos(rot), np.sin(rot) + r_det = s_xy[:, 0] * c - s_xy[:, 1] * s_ + c_det = s_xy[:, 0] * s_ + s_xy[:, 1] * c + on = (r_det.abs() <= qx_max) & (c_det.abs() <= qy_max) + s_xy, s_i = s_xy[on], s_i[on] + p_rad = float(self.power_radial) + inv = 1.0 / (4.0 * self.corr_kernel_size**2) + + def overlap(a, wa, b, wb): + return float(wa @ torch.exp(-(torch.cdist(a, b) ** 2) * inv) @ wb) + + m_xy, w_m, mm = measured if measured is not None else self._measured_term(data, ix) + if s_xy.shape[0] == 0 or m_xy.shape[0] == 0: + return 0.0 + w_s = s_i**self.power_intensity * torch.linalg.norm(s_xy, dim=1) ** p_rad + norm = np.sqrt(mm * max(overlap(s_xy, w_s, s_xy, w_s), 0.0)) + return overlap(m_xy, w_m, s_xy, w_s) / norm if norm > 0 else 0.0 + def _refine_batched( self, scores: torch.Tensor, @@ -1654,7 +1895,7 @@ def _refine_batched( num_iterations: int, min_pairs: int, refine_zone: bool, - progress_bar: bool, + progress_bar, chunk: int = 64, power_env: float = POWER_INTENSITY, ) -> None: @@ -1714,9 +1955,17 @@ def envelope(S, g_rows): valid_pos = torch.as_tensor(counts >= min_pairs) & active.reshape(N) chunks = [i for i in range(0, N, chunk) if bool(valid_pos[i : i + chunk].any())] - if progress_bar: - chunks = tqdm(chunks, desc="refining orientations") + # `progress_bar` is either a flag or the bar refine_orientations + # shares with the neighbour rescue, so one crystal shows one bar + bar = progress_bar if hasattr(progress_bar, "update") else None + if bar is not None: + bar.total = (bar.total or 0) + len(chunks) + bar.refresh() + elif progress_bar: + chunks = tqdm(chunks, desc=f"refining {self.crystal.name}") for i0 in chunks: + if bar is not None: + bar.update(1) i1 = min(i0 + chunk, N) B = i1 - i0 qe = q_exp[i0:i1] # (B, P, 2) @@ -1996,7 +2245,8 @@ def cluster_orientations( ok &= torch.as_tensor(np.asarray(mask, dtype=float).reshape(-1)) > 0.5 labels = torch.full((R * C,), -1, dtype=torch.long) - sym = self.crystal.sym_quats + # variants the library folds together belong to one grain + sym = self.crystal.sym_quats_matching unassigned = ok.clone() means, sizes = [], [] k = 0 @@ -2085,7 +2335,7 @@ def calculate_strain( iterator = list(np.ndindex(R, C)) if progress_bar: - iterator = tqdm(iterator, desc="strain mapping") + iterator = tqdm(iterator, desc=f"strain mapping {self.crystal.name}") for rx, ry in iterator: if mask is not None and not mask[rx, ry]: continue diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py index d88dd0383..d03550307 100644 --- a/src/quantem/diffraction/orientation_visualization.py +++ b/src/quantem/diffraction/orientation_visualization.py @@ -20,7 +20,7 @@ ) ORIGIN_COLOR = "#2ca02c" MEASURED_COLOR = "0.15" -IPF_GAMMA = 0.4 +IPF_SATURATION_POWER = 0.6 # <1 shrinks the white centre of the IPF wedge # cluster / grain label colors (tab10 cycle) CLUSTER_COLORS = [ (0.122, 0.467, 0.706), @@ -33,25 +33,27 @@ (0.498, 0.498, 0.498), (0.737, 0.741, 0.133), (0.090, 0.745, 0.812), -] # <1 expands the white / mixed-color regions of the wedge -# additive corner colors: full red, green capped to avoid the fluorescent -# look, blue lifted off pure dark blue; pairwise sums give near-max-chroma -# yellow / cyan / violet and the three together give white -IPF_CORNER_COLORS = np.array( - [ - [1.00, 0.00, 0.00], - [0.00, 0.70, 0.00], - [0.00, 0.30, 1.00], - ] -) +] def _bary_to_rgb(w: np.ndarray) -> np.ndarray: - """Barycentric wedge weights (..., 3) to RGB via the additive anchors.""" + """Barycentric wedge weights (..., 3) to RGB. + + Hue runs around the wedge centre, red, green and blue at the corners and + yellow, cyan and magenta midway along the edges; saturation is the + distance from the centre, full on every edge. Only the centre itself is + white, so the whole wedge, edges included, keeps its contrast. + """ + from matplotlib.colors import hsv_to_rgb + w = np.clip(w, 0, None) - w = w / np.clip(w.max(axis=-1, keepdims=True), 1e-12, None) - w = w**IPF_GAMMA - return np.clip(w @ IPF_CORNER_COLORS, 0, 1) + w = w / np.clip(w.sum(axis=-1, keepdims=True), 1e-12, None) + theta = np.deg2rad([90.0, 210.0, 330.0]) + x = w @ np.cos(theta) + y = w @ np.sin(theta) + hue = ((np.rad2deg(np.arctan2(y, x)) - 90.0) / 360.0) % 1.0 + sat = np.clip(1.0 - 3.0 * w.min(axis=-1), 0, 1) ** IPF_SATURATION_POWER + return hsv_to_rgb(np.stack((hue, sat, np.ones_like(hue)), axis=-1)) def _parse_direction(direction) -> torch.Tensor: @@ -424,7 +426,7 @@ def plot_pattern_matches( origins: np.ndarray | None = None, matches=(0, 1), colors=None, - power: float = 0.4, + norm=None, q_max_plot: float | None = None, scalebar: bool = True, show_measured: bool = True, @@ -460,6 +462,11 @@ def plot_pattern_matches( the background pattern with the origin-corrected peaks. matches : tuple[int, ...], default=(0, 1) Match indices per crystal. + norm : dict | str | None + `norm` of `show_2d`, which draws the recorded pattern, e.g. + {"power": 0.5, "upper_quantile": 0.98}. The default, + {"power": 0.4, "upper_quantile": 0.999}, keeps the direct beam from + flattening the disks. colors : list | None One color per crystal; defaults to red, blue, green, purple. marker : str | None @@ -467,7 +474,10 @@ def plot_pattern_matches( circle over a diffraction pattern, which leaves the measured disk visible inside it, and a plus over the gray measured peaks. measured_scale : float | None - Marker area of the gray measured peaks; defaults to + Largest marker area of the gray measured peaks, in points^2. The + default fits the markers to the patterns shown: the largest disk is + about 0.6 of the median spacing between neighbouring peaks, so dense + patterns get small markers and sparse ones large, up to ``1.5 * marker_scale``. The direct beam is far brighter than the disks, so the areas are compressed by `measured_power` and floored, which keeps the weak spots visible. @@ -481,6 +491,8 @@ def plot_pattern_matches( """ import matplotlib.pyplot as plt + from quantem.core.visualization import show_2d + oms = ( list(orientation_maps) if isinstance(orientation_maps, (list, tuple)) @@ -526,6 +538,23 @@ def plot_pattern_matches( q_max = max((float(q.max()) for q in q_all if q.size), default=0.0) q_lim = 1.1 * q_max if q_max > 0 else 1.0 + if measured_scale is None: + # size the measured disks to the spacing of the peaks on screen, so a + # dense pattern does not turn into overlapping blobs + spacings = [] + for rx, ry in positions: + xy = peaks[rx, ry].array[:, [ix[0], ix[1]]] + xy = xy[(np.abs(xy) <= q_lim).all(axis=1)] + if xy.shape[0] > 2: + d = np.hypot(xy[:, None, 0] - xy[None, :, 0], xy[:, None, 1] - xy[None, :, 1]) + np.fill_diagonal(d, np.inf) + spacings.append(np.median(d.min(axis=1))) + if spacings: + spacing_pt = float(np.median(spacings)) / (2 * q_lim) * min(axsize) * 72 + measured_scale = min(1.5 * marker_scale, np.pi / 4 * (0.6 * spacing_pt) ** 2) + else: + measured_scale = 1.5 * marker_scale + for pi, (rx, ry) in enumerate(positions): data = peaks[rx, ry].array.copy() rc = data[:, [ix[0], ix[1]]] @ rot_back.T @@ -550,28 +579,28 @@ def plot_pattern_matches( o_r, o_c = H / 2, W / 2 # pixel j has center (j - origin) * pixel_size; array edges # sit half a pixel beyond the first/last centers - img = np.asarray(dataset.array[rx, ry], dtype=float) - img = np.clip(img, 0, None) ** power # the direct beam is orders of magnitude above the disks, so # autoscaling to its peak flattens everything else - vmax = float(np.percentile(img, 99.9)) - ax.imshow( - img, + show_2d( + np.clip(np.asarray(dataset.array[rx, ry], dtype=float), 0, None), + norm=norm if norm is not None else {"power": 0.4, "upper_quantile": 0.999}, cmap="gray_r", - vmin=float(np.percentile(img, 2.0)), - vmax=vmax if vmax > 0 else None, - extent=( + figax=(fig, ax), + tight_layout=False, + ) + ax.images[-1].set_extent( + ( (-0.5 - o_c) * pixel_size, (W - 0.5 - o_c) * pixel_size, (H - 0.5 - o_r) * pixel_size, (-0.5 - o_r) * pixel_size, - ), + ) ) elif show_measured: ax.scatter( data[:, ix[1]], data[:, ix[0]], - s=(1.5 * marker_scale if measured_scale is None else measured_scale) * w_meas, + s=measured_scale * w_meas, color="0.75", lw=0, ) diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py index cc4d2253b..5cc846fee 100644 --- a/src/quantem/diffraction/phase.py +++ b/src/quantem/diffraction/phase.py @@ -356,7 +356,9 @@ def apply_dynamical(self, result: dict) -> "PhaseMap": the phase here. The reliability becomes the cost gap between the best candidates of the winning crystal and of the runner-up crystal, and the kinematical result is kept under - `metadata['kinematical']`. + `metadata['kinematical']`. Positions the refinement did not reach + (outside its `mask`) keep their current decision, so a refinement + of one region, or several in stages, updates only that region. """ cost = torch.nan_to_num(result["cost"], nan=torch.inf) n_maps = len(self.orientation_maps) @@ -372,13 +374,22 @@ def apply_dynamical(self, result: dict) -> "PhaseMap": else torch.zeros_like(order[..., 0]), torch.full_like(order[..., 0], torch.nan), ) - self.metadata["kinematical"] = { - "phase_index": self.phase_index, - "reliability": self.reliability, - } - self.phase_index = result["phase_index"] - self.reliability = reliability - self.cost_best = order[..., 0] + done = torch.isfinite(order[..., 0]) + if self.phase_index is None: + self.phase_index = torch.full((R, C), -1, dtype=torch.long) + self.reliability = torch.full((R, C), torch.nan, dtype=cost.dtype) + self.cost_best = torch.full((R, C), torch.nan, dtype=cost.dtype) + # keep the kinematical decision from before the first dynamical pass + self.metadata.setdefault( + "kinematical", + { + "phase_index": self.phase_index.clone(), + "reliability": self.reliability.clone(), + }, + ) + self.phase_index = torch.where(done, result["phase_index"], self.phase_index) + self.reliability = torch.where(done, reliability, self.reliability) + self.cost_best = torch.where(done, order[..., 0], self.cost_best) self.metadata["dynamical_applied"] = dict(result.get("metadata", {})) return self diff --git a/tests/diffraction/test_bloch.py b/tests/diffraction/test_bloch.py index e37f382b5..7be8e129f 100644 --- a/tests/diffraction/test_bloch.py +++ b/tests/diffraction/test_bloch.py @@ -987,3 +987,35 @@ def test_refine_dynamical_neighbor_rescue(): assert maps["mask"][0].all() assert set(maps["strain"]) == {"aa", "bb", "cc", "ab", "ac", "bc"} assert torch.isfinite(maps["gain"][0]).all() + + +def test_refine_dynamical_threads_match_one_worker(): + from quantem.diffraction.rotations import misorientation_angle_deg + + # 32 positions: two blocks of 16 on two threads, one warm-start chain + # broken at the block boundary + n = 32 + kw = dict( + thicknesses_A=np.arange(300, 700, 25.0), + tilt_stages=((0.25, 0.05), (0.04, 0.01)), + power_intensity=0.5, + sg_max=0.06, + k_max=1.0, + neighbor_rescue=False, + progress_bar=False, + ) + out = {} + for nw in (1, 2): + xtl, om, pm, q_true, _, _ = _smooth_map_setup(n, 0.1) + res = bloch.refine_dynamical(pm, num_workers=nw, **kw) + out[nw] = (res, om.quats[0, :, 0].clone()) + (res_1, q_1), (res_2, q_2) = out[1], out[2] + assert int(res_1["warm_started"].sum()) == n - 1 + assert int(res_2["warm_started"].sum()) == n - 2 + # the first block is the same computation on either route + assert torch.equal(q_1[:16], q_2[:16]) + assert torch.equal(res_1["thickness"][0, :16], res_2["thickness"][0, :16]) + # breaking the warm-start chain costs nothing in accuracy + e1 = misorientation_angle_deg(q_true, q_1, xtl.sym_quats).numpy() + e2 = misorientation_angle_deg(q_true, q_2, xtl.sym_quats).numpy() + assert e2.mean() <= e1.mean() + 0.01 diff --git a/tests/diffraction/test_crystal.py b/tests/diffraction/test_crystal.py index aa78932d8..9072cee07 100644 --- a/tests/diffraction/test_crystal.py +++ b/tests/diffraction/test_crystal.py @@ -331,9 +331,7 @@ def test_projected_order_matches_pattern_degeneracy(): n = bcc.projected_rotation_order(axis.numpy()) q = quat_from_zone_axis(axis) beam = torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64) - spun = qnormalize( - qmult(quat_from_axis_angle(beam, torch.tensor(2 * np.pi / n)), q) - ) + spun = qnormalize(qmult(quat_from_axis_angle(beam, torch.tensor(2 * np.pi / n)), q)) a = bcc.generate_pattern(q, energy_ev=200e3, sigma_excitation=0.02) b = bcc.generate_pattern(spun, energy_ev=200e3, sigma_excitation=0.02) @@ -346,3 +344,145 @@ def test_projected_order_matches_pattern_degeneracy(): assert float(dmin.max()) < 1e-6 rel = (a["intensity"] - b["intensity"][j]).abs().max() / a["intensity"].max() assert float(rel) < 1e-6 + + +_LFSO_PRISTINE_CIF = """data_ +_cell_length_a 5.1749 +_cell_length_b 8.9426 +_cell_length_c 5.1721 +_cell_angle_alpha 90 +_cell_angle_beta 109.697 +_cell_angle_gamma 90 +_symmetry_space_group_name_H-M C2/m +loop_ +_symmetry_equiv_pos_as_xyz + 'x, y, z' + '-x, y, -z' + 'x, -y, z' + '-x, -y, -z' + 'x+1/2, y+1/2, z' + '-x+1/2, y+1/2, -z' + 'x+1/2, -y+1/2, z' + '-x+1/2, -y+1/2, -z' +loop_ +_atom_site_label +_atom_site_type_symbol +_atom_site_fract_x +_atom_site_fract_y +_atom_site_fract_z +_atom_site_occupancy +Fe1 Fe 0 0.3380 0.5 0.389 +Li1 Li 0 0.3380 0.5 0.611 +Sb1 Sb 0 0 0.5 0.360 +Fe2 Fe 0 0 0.5 0.640 +Li2 Li 0 0.5 0 1 +Li3 Li 0 0.1662 0 1 +O1 O 0.7359 0.5 0.2706 1 +O2 O 0.7665 0.8415 0.2722 1 +""" + + +def test_from_cif_keeps_partial_occupancy(tmp_path): + # ASE reads a shared site as its majority species alone: this structure + # would load with no Sb at all, and Sb is its strongest scatterer + path = tmp_path / "lfso_pristine.cif" + path.write_text(_LFSO_PRISTINE_CIF) + xtl = Crystal.from_cif(path, verbose=False) + content: dict[str, float] = {} + for s, f in zip(xtl.atoms.get_chemical_symbols(), xtl.occupancy.numpy()): + content[s] = content.get(s, 0.0) + float(f) + assert content["Sb"] == pytest.approx(0.72, abs=1e-6) + assert content["Fe"] == pytest.approx(2.836, abs=1e-6) + assert content["Li"] == pytest.approx(8.444, abs=1e-6) + assert content["O"] == pytest.approx(12.0, abs=1e-6) + assert xtl.spacegroup.startswith("C2/m") + + +def test_pseudo_symmetry_names_the_breaking_reflection(tmp_path): + # the three 120 degree twin variants of the honeycomb-ordered cell differ + # only by the (020) superstructure reflection at ~0.07 of the strongest; + # at the default tolerance that rejects the layered parent, and the + # report must name the reflection responsible + path = tmp_path / "lfso_pristine.cif" + path.write_text(_LFSO_PRISTINE_CIF) + strict = Crystal.from_cif(path, verbose=False) + rep = strict.pseudo_symmetry_report + assert rep["rejected"] and rep["candidate"] == "-3m" + assert 0.05 < rep["intensity_mismatch"] < 0.1 + (h0, i0, h1, i1) = rep["broken_by"] + assert tuple(abs(v) for v in h0) == (0, 2, 0) + assert "broken by" in strict.symmetry_summary() + + +def test_pseudo_symmetry_from_parent_lattice(tmp_path): + # the honeycomb superstructure sits on a layered R-3m parent, itself on a + # rocksalt parent; neither parent's rotations map the monoclinic cell onto + # itself, so only the parent-lattice route can find them + path = tmp_path / "lfso_pristine.cif" + path.write_text(_LFSO_PRISTINE_CIF) + layered = Crystal.from_cif(path, pseudo_symmetry_intensity_tol=0.1, verbose=False) + assert layered.pointgroup_matching == "-3m" + assert layered.sym_quats_matching.shape[0] == 6 + assert "parent lattice" in layered.pseudo_symmetry_report["route"] + # the layer normal is ~[103] in the monoclinic cell + assert any("103" in lab for lab in layered.zone_axis_wedge_labels(mathtext=False)) + + rocksalt = Crystal.from_cif(path, pseudo_symmetry_intensity_tol=0.4, verbose=False) + assert rocksalt.pointgroup_matching == "m-3m" + assert rocksalt.sym_quats_matching.shape[0] == 24 + # the rocksalt parent is broken only by the (001) layer-ordering reflection + h0, i0, h1, i1 = rocksalt.pseudo_symmetry_report["broken_by"] + assert tuple(abs(v) for v in h0) == (0, 0, 1) + assert 0.3 < rocksalt.pseudo_symmetry_report["intensity_mismatch"] < 0.35 + + +_LFSO_CHARGED_CIF = """data_ +_cell_length_a 5.04848 +_cell_length_b 5.04848 +_cell_length_c 9.4279 +_cell_angle_alpha 90 +_cell_angle_beta 90 +_cell_angle_gamma 120 +_symmetry_space_group_name_H-M P-31c +loop_ +_symmetry_equiv_pos_as_xyz + 'x, y, z' + '-x, -y, -z' + '-x+y, -x, z' + '-x+y, y, -z+1/2' + '-y, -x, -z+1/2' + '-y, x-y, z' + 'y, -x+y, -z' + 'y, x, z+1/2' + 'x-y, -y, z+1/2' + 'x-y, x, -z' + '-x, -x+y, z+1/2' + 'x, x-y, -z+1/2' +loop_ +_atom_site_label +_atom_site_type_symbol +_atom_site_fract_x +_atom_site_fract_y +_atom_site_fract_z +_atom_site_occupancy +Me1 Li 0.3333333 0.6666667 0.75 0.673 +Me11 Sb 0.3333333 0.6666667 0.75 0.327 +Me2 Li 0.3333333 0.6666667 0.25 0.327 +Me22 Sb 0.3333333 0.6666667 0.25 0.673 +Me3 Fe 0 0 0.75 1 +O1 O 0.7409 0.7329 0.8660 1 +""" + + +def test_hexagonal_pseudo_symmetry_can_be_adopted(tmp_path): + # a hexagonal cell given to five decimals reproduces rotation products to + # only ~1e-6; a closure test that strict rejected every hexagonal parent + # group whatever the intensity tolerance + path = tmp_path / "lfso_charged.cif" + path.write_text(_LFSO_CHARGED_CIF) + strict = Crystal.from_cif(path, pseudo_symmetry_intensity_tol=0.4, verbose=False) + assert strict.pointgroup_matching == "-3m" + assert strict.pseudo_symmetry_report["candidate"] == "6/mmm" + loose = Crystal.from_cif(path, pseudo_symmetry_intensity_tol=1.0, verbose=False) + assert loose.pointgroup_matching == "6/mmm" + assert loose.sym_quats_matching.shape[0] == 12 diff --git a/tests/diffraction/test_disk_detection.py b/tests/diffraction/test_disk_detection.py index a9d207434..6e948de77 100644 --- a/tests/diffraction/test_disk_detection.py +++ b/tests/diffraction/test_disk_detection.py @@ -76,3 +76,30 @@ def test_defaults_are_plain_cross_correlation(): a = detect_disks(dp, tft, **common) b = detect_disks(dp, tft, corr_power=1.0, sigma_cc=None, **common) assert np.allclose(a, b) + + +def test_measure_origins_off_centre_beam(): + # a beam further than search_radius from the detector centre used to give + # an all-NaN measurement, whose plane fit silently returned zeros + import pytest + + from quantem.core.datastructures import Dataset4dstem + from quantem.diffraction import BraggVectors + + rows, cols = np.mgrid[0:48, 0:48] + arr = np.zeros((6, 6, 48, 48), dtype=np.float32) + for r in range(6): + for c in range(6): + cy, cx = 24.0 + 0.1 * r, 34.0 - 0.1 * c + arr[r, c] = 100 * np.exp(-((rows - cy) ** 2 + (cols - cx) ** 2) / 4.0) + arr[r, c] += 20 * np.exp(-((rows - cy - 12) ** 2 + (cols - cx) ** 2) / 4.0) + bv = BraggVectors.from_dataset(Dataset4dstem.from_array(arr)) + bv.make_template_synthetic(radius=1.5, edge=1.0) + bv.detect_disks(min_abs_intensity=1.0, min_spacing=4.0, progressbar=False) + + with pytest.raises(ValueError, match="direct beam is elsewhere"): + bv.measure_origins(search_radius=6.0) + + origins = bv.measure_origins(search_radius=6.0, center=(24.0, 34.0)) + assert np.abs(origins[..., 0] - (24.0 + 0.1 * np.arange(6)[:, None])).max() < 0.2 + assert np.abs(origins[..., 1] - (34.0 - 0.1 * np.arange(6)[None, :])).max() < 0.2 diff --git a/tests/diffraction/test_orientation.py b/tests/diffraction/test_orientation.py index 35c2622ce..f291bf9f3 100644 --- a/tests/diffraction/test_orientation.py +++ b/tests/diffraction/test_orientation.py @@ -608,3 +608,90 @@ def test_display_smoothing_needs_both_widths(): plt.close("all") # smoothing for display must never touch the stored orientations assert torch.equal(before, om.quats) + + +def test_rescue_breaks_friedel_ties_by_neighbours(): + """At a zone axis the pattern rotated 180 degrees about the beam is the + same pattern, so a scattered twin can only be undone by its neighbours.""" + from quantem.diffraction.rotations import qmult, quat_from_zone_axis + + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.95, c=4.686)) + xtl.calculate_structure_factors(k_max=1.5) + zone = torch.tensor([1.0, 0.0, 1.0], dtype=torch.float64) @ xtl.lat_real.to(torch.float64) + q = quat_from_zone_axis(zone, 20.0) + twin = qmult(torch.tensor([0.0, 0.0, 0.0, 1.0], dtype=torch.float64), q) + assert float(misorientation_angle_deg(q, twin, xtl.sym_quats_matching)) > 10.0 + + R, C = 5, 5 + peaks = Vector.from_shape( + (R, C), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + p = xtl.generate_pattern(q, energy_ev=200e3, sigma_excitation=0.02) + for i in range(R): + for j in range(C): + peaks[i, j] = np.stack( + [p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()], axis=1 + ) + flipped = torch.zeros((R, C), dtype=torch.bool) + flipped[1, 1] = flipped[2, 3] = flipped[3, 1] = True + + def run(consensus_tol): + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=2.0, verbose=False, progress_bar=False) + om.quats = torch.where(flipped[..., None], twin, q).clone()[..., None, :] + om.corr = torch.ones((R, C, 1), dtype=torch.float64) + om.computed = torch.ones((R, C), dtype=torch.bool) + om.refine_orientations(consensus_tol=consensus_tol, progress_bar=False) + return misorientation_angle_deg(q, om.quats[..., 0, :], xtl.sym_quats_matching) + + # each pattern alone cannot tell the twin apart ... + assert float(run(0.0)[flipped].min()) > 10.0 + # ... but it is a tie, and every neighbour holds the other variant + assert float(run(0.01).max()) < 2.0 + + +def test_ipf_key_saturates_edges_and_whitens_only_the_centre(): + from quantem.diffraction.orientation_visualization import _bary_to_rgb + + corners = _bary_to_rgb(np.eye(3)) + assert np.allclose(corners, [[1, 0, 0], [0, 1, 0], [0, 0, 1]], atol=1e-9) + # every point on an edge is fully saturated: some channel is zero + t = np.linspace(0, 1, 11)[:, None] + for i, j in ((0, 1), (1, 2), (2, 0)): + w = np.zeros((11, 3)) + w[:, i], w[:, j] = 1 - t[:, 0], t[:, 0] + assert np.allclose(_bary_to_rgb(w).min(axis=1), 0, atol=1e-9) + assert np.allclose(_bary_to_rgb(np.ones(3) / 3), 1) + # halfway from the centre to an edge is still clearly colored + assert _bary_to_rgb(np.array([0.5, 0.5, 0.0]) * 0.5 + 1 / 6).min() < 0.7 + + +def test_plot_matches_background_norm(): + import matplotlib + + matplotlib.use("Agg") + from types import SimpleNamespace + + from quantem.diffraction.orientation_visualization import plot_pattern_matches + + torch.manual_seed(0) + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True)) + xtl.calculate_structure_factors(k_max=1.5) + peaks = _make_peaks(xtl, qnormalize(torch.randn(2, 4, dtype=torch.float64))) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=3.0, angle_step_in_plane_deg=3.0) + om.match_orientations(progress_bar=False) + img = np.random.default_rng(0).random((1, 2, 32, 32)) ** 4 + dataset = SimpleNamespace(array=img, shape=img.shape) + shown = [] + for norm in (None, {"power": 0.5, "upper_quantile": 0.9}): + fig, axs = plot_pattern_matches( + om, [(0, 0)], dataset=dataset, pixel_size=0.05, matches=(0,), norm=norm + ) + # show_2d draws the pattern in the panel, extended to q units + im = axs[0, 0].images[0] + assert np.allclose(im.get_extent()[:2], (-0.5 * 0.05 - 16 * 0.05, 31.5 * 0.05 - 16 * 0.05)) + shown.append(np.asarray(im.get_array())[..., 0]) + matplotlib.pyplot.close(fig) + # gray_r: a lower upper quantile saturates more of the pattern to black + assert (shown[1] <= shown[1].min() + 1e-6).mean() > (shown[0] <= shown[0].min() + 1e-6).mean() diff --git a/tests/diffraction/test_two_phase_map.py b/tests/diffraction/test_two_phase_map.py index b2991df91..163fd2b34 100644 --- a/tests/diffraction/test_two_phase_map.py +++ b/tests/diffraction/test_two_phase_map.py @@ -22,9 +22,7 @@ def _pattern(xtl, q, rng): p = xtl.generate_pattern(q, energy_ev=200e3, sigma_excitation=0.02) - arr = np.column_stack( - [p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()] - ) + arr = np.column_stack([p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()]) arr[:, :2] += rng.normal(0, 0.003, (arr.shape[0], 2)) arr[:, 2] *= rng.lognormal(0, 0.3, arr.shape[0]) return arr @@ -43,8 +41,7 @@ def test_two_phase_map(): # beta along [111] zone; alpha along [0001]: the Burgers-related pair # shares the hexagonal net, the hard case for phase mapping q_beta = quat_from_axis_angle( - torch.tensor([1.0, -1.0, 0.0], dtype=torch.float64) - / np.sqrt(2), + torch.tensor([1.0, -1.0, 0.0], dtype=torch.float64) / np.sqrt(2), torch.tensor(np.arccos(1 / np.sqrt(3)), dtype=torch.float64), ) q_alpha = qmult( @@ -77,9 +74,7 @@ def test_two_phase_map(): truth[r, c] = 2 row.append(arr) cells.append(row) - peaks = Vector.from_data( - cells, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" - ) + peaks = Vector.from_data(cells, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t") oms = [] for xtl in (ti_a, ti_b): @@ -112,3 +107,86 @@ def test_two_phase_map(): # phases with a valid orientation (either is acceptable) band_pi = pi[:, band[0] : band[1] + 1] assert np.isin(band_pi, [0, 1]).all() + + +def test_crystal_map_sets_k_max_once(): + # two phases simulated to different ranges are not compared fairly, and + # setting k_max per crystal invites exactly that mistake + import numpy as np + import pytest + from ase.build import bulk + + from quantem.core.datastructures import Vector + from quantem.diffraction import Crystal, CrystalMap + + peaks = Vector.from_data( + [[np.array([[0.0, 0.0, 1.0], [0.3, 0.1, 0.5], [-0.2, 0.4, 0.3]])]], + fields=["qx", "qy", "intensity"], + name="p", + ) + au = Crystal.from_ase(bulk("Au", "fcc", a=4.08, cubic=True), verbose=False) + fe = Crystal.from_ase(bulk("Fe", "bcc", a=2.87, cubic=True), verbose=False) + + with pytest.raises(ValueError, match="pass k_max"): + CrystalMap.from_vectors(peaks, [au, fe]) + + au.calculate_structure_factors(k_max=1.4) + fe.calculate_structure_factors(k_max=2.0) + with pytest.raises(ValueError, match="different k_max"): + CrystalMap.from_vectors(peaks, [au, fe]) + + cm = CrystalMap.from_vectors(peaks, [au, fe], k_max=1.8) + assert cm.k_max == 1.8 + assert au.k_max == fe.k_max == 1.8 + assert float(au.g_len.max()) <= 1.8 and float(fe.g_len.max()) <= 1.8 + + +def test_dynamical_update_is_local_and_examples_are_spread(): + from quantem.diffraction.crystal_map import CrystalMap + + rng = np.random.default_rng(4) + ti_a = Crystal.from_ase( + bulk("Ti", "hcp", a=2.9505, c=4.6855), name="Ti alpha", verbose=False + ).calculate_structure_factors(k_max=1.5) + ti_b = Crystal.from_ase( + bulk("Ti", "bcc", a=3.26, cubic=True), name="Ti beta", verbose=False + ).calculate_structure_factors(k_max=1.5) + q_a = torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=torch.float64) + q_b = quat_from_axis_angle( + torch.tensor([1.0, 0.0, 0.0], dtype=torch.float64), + torch.tensor(np.deg2rad(20.0), dtype=torch.float64), + ) + R, C = 3, 8 + cells = [ + [_pattern(ti_a, q_a, rng) if c < 4 else _pattern(ti_b, q_b, rng) for c in range(C)] + for _ in range(R) + ] + peaks = Vector.from_data(cells, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t") + oms = [] + for xtl in (ti_a, ti_b): + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan() + om.match_orientations(progress_bar=False) + oms.append(om) + cm = CrystalMap.from_orientation_maps(oms) + cm.fit(progress_bar=False) + before = cm.phase_index.copy() + assert (before[:, :4] == 0).all() and (before[:, 4:] == 1).all() + + # a dynamical result that reached one position and flipped it + F = len(cm.phases.candidates) + cost = torch.full((R, C, F), torch.nan, dtype=torch.float64) + cost[0, 0] = torch.tensor([0.9, 0.1]) + cm.phases.apply_dynamical({"cost": cost, "phase_index": cost.nan_to_num(9).argmin(-1)}) + after = cm.phase_index + assert after[0, 0] == 1 + changed = after != before + changed[0, 0] = False + assert not changed.any() + assert (cm.phases.metadata["kinematical"]["phase_index"].numpy() == before).all() + + picks = cm.example_positions(phase="Ti beta", num=3, min_distance=3) + assert all(cm.phase_index[p] == 1 for p in picks) + assert all( + (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 >= 9 for i, a in enumerate(picks) for b in picks[:i] + ) From b9636669c58400cb995f2ceb3a81ddc92b6ac8ad Mon Sep 17 00:00:00 2001 From: cophus Date: Mon, 28 Sep 2026 12:00:43 -0700 Subject: [PATCH 15/36] various fixes --- src/quantem/diffraction/bloch.py | 20 +-- src/quantem/diffraction/crystal.py | 4 +- src/quantem/diffraction/crystal_map.py | 31 +++- .../diffraction/orientation_visualization.py | 132 +++++++++++------- src/quantem/diffraction/phase.py | 2 +- tests/diffraction/test_orientation.py | 25 ++-- tests/diffraction/test_two_phase_map.py | 24 ++++ 7 files changed, 163 insertions(+), 75 deletions(-) diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py index b81e1755f..45f3314a9 100644 --- a/src/quantem/diffraction/bloch.py +++ b/src/quantem/diffraction/bloch.py @@ -150,9 +150,10 @@ def _primitive_lattice_mask(crystal: Crystal, g: torch.Tensor) -> torch.Tensor: def _check_dynamical_factors(crystal: Crystal, energy_ev: float, g_max_beams: float) -> None: - """Warn once per crystal when the absorptive factors were computed at - another energy or do not cover every coupling g - h of the beam list - (which needs factors out to twice the largest beam).""" + """Warn once per crystal when the absorptive factors are missing, were + computed at another energy, or stop short of 1.5 times the largest beam + (the couplings g - h reach twice it, but the factors beyond 1.5 times + are negligible).""" key = id(crystal) if key in _coverage_warned: return @@ -162,10 +163,10 @@ def _check_dynamical_factors(crystal: Crystal, energy_ev: float, g_max_beams: fl "no absorptive structure factors (calculate_dynamical_structure_factors), " "so the Bloch calculation uses the elastic kinematical factors" ) - if k_kin is not None and 2 * g_max_beams > k_kin + 1e-9: + if k_kin is not None and 1.5 * g_max_beams > k_kin + 1e-9: msg += ( - f", which stop at {k_kin:.2f} 1/A while the couplings of this beam list " - f"reach {2 * g_max_beams:.2f} 1/A" + f", which stop at {k_kin:.2f} 1/A, short of the " + f"{1.5 * g_max_beams:.2f} 1/A the couplings of this beam list need" ) _coverage_warned.add(key) warnings.warn(f"{crystal.name}: {msg}", stacklevel=3) @@ -178,12 +179,15 @@ def _check_dynamical_factors(crystal: Crystal, energy_ev: float, g_max_beams: fl f"dynamical structure factors were computed at {e_dyn:.0f} eV, the " f"calculation runs at {energy_ev:.0f} eV" ) - if k_dyn is not None and 2 * g_max_beams > k_dyn + 1e-9: + # couplings g - h reach twice the beam radius, but the factors fall off + # fast: 1.5 times it keeps every coupling that matters (to 5%, since the + # fitted in-plane strain stretches the beams a little past k_max) + if k_dyn is not None and 1.5 * g_max_beams > 1.05 * k_dyn: msgs.append( f"dynamical structure factors extend to {k_dyn:.2f} 1/A but the beam " f"list reaches {g_max_beams:.2f} 1/A, so couplings beyond " f"{k_dyn:.2f} 1/A are missing (treated as zero); recompute with " - f"k_max >= {2 * g_max_beams:.2f}" + f"k_max >= {1.5 * g_max_beams:.2f}" ) if msgs: _coverage_warned.add(key) diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py index 79a38bade..c757d0a6f 100644 --- a/src/quantem/diffraction/crystal.py +++ b/src/quantem/diffraction/crystal.py @@ -995,8 +995,8 @@ def calculate_dynamical_structure_factors( RMS thermal displacement (Angstroms), scalar or per-element. k_max : float | None Maximum |g| of stored factors; defaults to the kinematical k_max. - For Bloch calculations with beams out to k, this should be 2k so - every coupling vector is covered. + For Bloch calculations with beams out to k, the couplings reach + 2k, but the factors fall off fast and 1.5k is enough. """ from quantem.diffraction.wk_scattering_factors import compute_WK_factor diff --git a/src/quantem/diffraction/crystal_map.py b/src/quantem/diffraction/crystal_map.py index 32bc00f56..ab9965654 100644 --- a/src/quantem/diffraction/crystal_map.py +++ b/src/quantem/diffraction/crystal_map.py @@ -94,6 +94,17 @@ def __init__( self.metadata: dict = {} self.k_max = _common_k_max([om.crystal for om in orientation_maps], k_max) + def __attrs_post_init__(self): + """After loading: the phase map shares this map's orientation maps. + + A saved file holds the phase map's copies of the orientation maps + separately, and without this they would load as independent objects: + anything computed on one (orientations written back by a refinement, + structure factors attached to a crystal) would be missed by the other. + """ + if self.phases is not None: + self.phases.orientation_maps = self.orientation_maps + # ------------------------------------------------------------------ # construction # ------------------------------------------------------------------ @@ -299,7 +310,9 @@ def fit(self, **kwargs) -> "CrystalMap": self.phases.fit(**kwargs) return self - def refine_dynamical(self, mask=None, **kwargs) -> "CrystalMap": + def refine_dynamical( + self, mask=None, k_max_coupling: float | None = None, **kwargs + ) -> "CrystalMap": """Dynamical refinement of orientation, thickness, strain and phase. Bloch-wave intensities, averaged over the precession ring, are fit to @@ -312,9 +325,9 @@ def refine_dynamical(self, mask=None, **kwargs) -> "CrystalMap": ``refine_deformation=False``, then the in-plane strain from those orientations with a narrower tilt search. - Every crystal is given absorptive structure factors out to twice - `k_max`, which the couplings between beams need; they are computed - here when missing or too short. + Every crystal is given absorptive structure factors out to + `k_max_coupling`, which the couplings between beams need; they are + computed here when missing or too short. Parameters ---------- @@ -326,6 +339,11 @@ def refine_dynamical(self, mask=None, **kwargs) -> "CrystalMap": Defaults to the k_max of the kinematical simulation. Cutting it low saves time but drops beams that carry real dynamical coupling. + k_max_coupling : float, optional + Largest |g| (1/Angstroms) of the structure factors coupling the + beams. The couplings g - h reach twice `k_max`, but the factors + fall off fast; None uses 1.5 `k_max`. It sets accuracy, not run + time, which the number of beams sets. **kwargs Passed to :func:`~quantem.diffraction.bloch.refine_dynamical`. `require_phase_weight` defaults to False here, so a crystal the @@ -340,15 +358,16 @@ def refine_dynamical(self, mask=None, **kwargs) -> "CrystalMap": pm = self._require_fit("refine_dynamical()") k_max = float(kwargs.pop("k_max", None) or self.k_max) + k_c = float(k_max_coupling if k_max_coupling is not None else 1.5 * k_max) energy_ev = self.orientation_maps[0].energy_ev for om in self.orientation_maps: xtl = om.crystal if ( getattr(xtl, "U_dyn", None) is None - or getattr(xtl, "dyn_k_max", 0.0) < 2 * k_max - 1e-9 + or getattr(xtl, "dyn_k_max", 0.0) < k_c - 1e-9 or abs(getattr(xtl, "dyn_energy_ev", energy_ev) - energy_ev) > 1.0 ): - xtl.calculate_dynamical_structure_factors(energy_ev, k_max=2 * k_max) + xtl.calculate_dynamical_structure_factors(energy_ev, k_max=k_c) kwargs["k_max"] = k_max kwargs.setdefault("require_phase_weight", False) self.dynamical = bloch.refine_dynamical(pm, mask=mask, **kwargs) diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py index d03550307..8956d1c5c 100644 --- a/src/quantem/diffraction/orientation_visualization.py +++ b/src/quantem/diffraction/orientation_visualization.py @@ -21,6 +21,16 @@ ORIGIN_COLOR = "#2ca02c" MEASURED_COLOR = "0.15" IPF_SATURATION_POWER = 0.6 # <1 shrinks the white centre of the IPF wedge +# corner colors: full red, green capped to avoid the fluorescent look, blue +# lifted off pure dark blue; pairwise blends give near-max-chroma orange, +# cyan and violet along the edges +IPF_CORNER_COLORS = np.array( + [ + [1.00, 0.00, 0.00], + [0.00, 0.70, 0.00], + [0.00, 0.30, 1.00], + ] +) # cluster / grain label colors (tab10 cycle) CLUSTER_COLORS = [ (0.122, 0.467, 0.706), @@ -39,21 +49,20 @@ def _bary_to_rgb(w: np.ndarray) -> np.ndarray: """Barycentric wedge weights (..., 3) to RGB. - Hue runs around the wedge centre, red, green and blue at the corners and - yellow, cyan and magenta midway along the edges; saturation is the - distance from the centre, full on every edge. Only the centre itself is - white, so the whole wedge, edges included, keeps its contrast. + Each direction takes the color of the edge point straight out from the + wedge centre, a blend of the two nearest corner colors, and fades toward + white with its distance from that edge. Every edge is fully colored and + only the centre itself is white, so the whole wedge keeps its contrast. """ - from matplotlib.colors import hsv_to_rgb - w = np.clip(w, 0, None) w = w / np.clip(w.sum(axis=-1, keepdims=True), 1e-12, None) - theta = np.deg2rad([90.0, 210.0, 330.0]) - x = w @ np.cos(theta) - y = w @ np.sin(theta) - hue = ((np.rad2deg(np.arctan2(y, x)) - 90.0) / 360.0) % 1.0 - sat = np.clip(1.0 - 3.0 * w.min(axis=-1), 0, 1) ** IPF_SATURATION_POWER - return hsv_to_rgb(np.stack((hue, sat, np.ones_like(hue)), axis=-1)) + m = w.min(axis=-1, keepdims=True) + # the edge point on the ray from the centre (1/3, 1/3, 1/3) through w + edge = (w - m) / np.clip(1.0 - 3.0 * m, 1e-12, None) + edge = edge / np.clip(edge.max(axis=-1, keepdims=True), 1e-12, None) + rgb = np.clip(edge @ IPF_CORNER_COLORS, 0, 1) + sat = np.clip(1.0 - 3.0 * m, 0, 1) ** IPF_SATURATION_POWER + return 1.0 - sat * (1.0 - rgb) def _parse_direction(direction) -> torch.Tensor: @@ -233,20 +242,32 @@ def wedge_legend( # tall panel with the wedge hanging straight down (the rotation aligns # the wedge's angular bisector with the downward direction) if orientation == "vertical": - az = [np.arctan2(c[k, 1] / (1 + c[k, 2]), c[k, 0] / (1 + c[k, 2])) for k in (1, 2)] - th = -np.pi / 2 - (az[0] + az[1]) / 2 + # bisector from the summed directions, not the mean of two angles, + # which jumps by pi when a corner sits at +-180 degrees (-0.0 in y) + d = sum(c[k, :2] / (1 + c[k, 2]) / np.linalg.norm(c[k, :2]) for k in (1, 2)) + th = -np.pi / 2 - np.arctan2(d[1], d[0]) else: th = 0.0 rot = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]]) cxy = np.stack([c[:, 0] / (1 + c[:, 2]), c[:, 1] / (1 + c[:, 2])], axis=1) @ rot.T cx, cy = cxy[:, 0], cxy[:, 1] + # wedge edges: stereographic great-circle arcs, which can bulge past the + # corners (the equator arc does once the wedge is rotated upright) + tt = np.linspace(0, 1, 60)[:, None] + edges = [] + for i0, i1 in ((0, 1), (1, 2), (2, 0)): + e = c[i0][None, :] * (1 - tt) + c[i1][None, :] * tt + e = e / np.linalg.norm(e, axis=1, keepdims=True) + edges.append(np.stack([e[:, 0] / (1 + e[:, 2]), e[:, 1] / (1 + e[:, 2])], axis=1) @ rot.T) + outline = np.concatenate(edges) + # rasterize the wedge interior: invert the stereographic projection on a - # pixel grid and alpha-mask outside the wedge, so no color spills past - # the outline + # pixel grid covering the whole outline and alpha-mask outside the + # wedge, so no color spills past it and none is missing inside it m = 8 - x0, x1 = cx.min() - 0.02, cx.max() + 0.02 - y0, y1 = cy.min() - 0.02, cy.max() + 0.02 + x0, x1 = outline[:, 0].min() - 0.02, outline[:, 0].max() + 0.02 + y0, y1 = outline[:, 1].min() - 0.02, outline[:, 1].max() + 0.02 X, Y = np.meshgrid(np.linspace(x0, x1, n * m), np.linspace(y0, y1, n * m), indexing="xy") Xu = np.cos(th) * X + np.sin(th) * Y Yu = -np.sin(th) * X + np.cos(th) * Y @@ -259,12 +280,7 @@ def wedge_legend( rgba[..., :3] = _bary_to_rgb(W) rgba[..., 3] = inside ax.imshow(rgba, extent=(x0, x1, y0, y1), origin="lower", interpolation="nearest") - # black outline along the wedge edges (stereographic great-circle arcs) - tt = np.linspace(0, 1, 60)[:, None] - for i0, i1 in ((0, 1), (1, 2), (2, 0)): - e = c[i0][None, :] * (1 - tt) + c[i1][None, :] * tt - e = e / np.linalg.norm(e, axis=1, keepdims=True) - exy = np.stack([e[:, 0] / (1 + e[:, 2]), e[:, 1] / (1 + e[:, 2])], axis=1) @ rot.T + for exy in edges: ax.plot(exy[:, 0], exy[:, 1], color="k", lw=1.2) if labels: names = crystal.zone_axis_wedge_labels() or ["", "", ""] @@ -276,11 +292,10 @@ def wedge_legend( ha = "left" if off[0] > 0.02 else ("right" if off[0] < -0.02 else "center") va = "bottom" if off[1] > 0.02 else ("top" if off[1] < -0.02 else "center") ax.text(xi + off[0], yi + off[1], name, fontsize=fontsize, ha=ha, va=va) - span_x = cx.max() - cx.min() - span_y = cy.max() - cy.min() - pad = 0.45 * max(span_x, span_y, 0.2) - ax.set_xlim(cx.min() - pad, cx.max() + pad) - ax.set_ylim(cy.min() - pad, cy.max() + pad) + ox, oy = outline[:, 0], outline[:, 1] + pad = 0.45 * max(ox.max() - ox.min(), oy.max() - oy.min(), 0.2) + ax.set_xlim(ox.min() - pad, ox.max() + pad) + ax.set_ylim(oy.min() - pad, oy.max() + pad) ax.set_aspect("equal") ax.axis("off") @@ -427,6 +442,7 @@ def plot_pattern_matches( matches=(0, 1), colors=None, norm=None, + sigma_plot: float | None = 1.0, q_max_plot: float | None = None, scalebar: bool = True, show_measured: bool = True, @@ -467,6 +483,9 @@ def plot_pattern_matches( {"power": 0.5, "upper_quantile": 0.98}. The default, {"power": 0.4, "upper_quantile": 0.999}, keeps the direct beam from flattening the disks. + sigma_plot : float | None, default=1.0 + Gaussian blur (pixels) of the displayed pattern only, which makes + the disks easier to see in low-dose data; None shows it raw. colors : list | None One color per crystal; defaults to red, blue, green, purple. marker : str | None @@ -492,6 +511,7 @@ def plot_pattern_matches( import matplotlib.pyplot as plt from quantem.core.visualization import show_2d + from quantem.diffraction.bragg_vectors_visualization import _blur oms = ( list(orientation_maps) @@ -577,33 +597,38 @@ def plot_pattern_matches( o_r, o_c = origins[rx, ry] else: o_r, o_c = H / 2, W / 2 - # pixel j has center (j - origin) * pixel_size; array edges - # sit half a pixel beyond the first/last centers # the direct beam is orders of magnitude above the disks, so # autoscaling to its peak flattens everything else + img = np.clip(np.asarray(dataset.array[rx, ry], dtype=float), 0, None) show_2d( - np.clip(np.asarray(dataset.array[rx, ry], dtype=float), 0, None), + _blur(img, sigma_plot), norm=norm if norm is not None else {"power": 0.4, "upper_quantile": 0.999}, cmap="gray_r", figax=(fig, ax), tight_layout=False, ) - ax.images[-1].set_extent( - ( - (-0.5 - o_c) * pixel_size, - (W - 0.5 - o_c) * pixel_size, - (H - 0.5 - o_r) * pixel_size, - (-0.5 - o_r) * pixel_size, - ) - ) - elif show_measured: - ax.scatter( - data[:, ix[1]], - data[:, ix[0]], - s=measured_scale * w_meas, - color="0.75", - lw=0, + # pixel j has center (j - origin) * pixel_size; array edges + # sit half a pixel beyond the first/last centers + extent = ( + (-0.5 - o_c) * pixel_size, + (W - 0.5 - o_c) * pixel_size, + (H - 0.5 - o_r) * pixel_size, + (-0.5 - o_r) * pixel_size, ) + ax.images[-1].set_extent(extent) + # the pattern is off-centre by the origin: show exactly the + # recorded area, so nothing is drawn beyond its edges + x_lim, y_lim = extent[:2], extent[2:] + else: + x_lim, y_lim = (-q_lim, q_lim), (q_lim, -q_lim) + if show_measured: + ax.scatter( + data[:, ix[1]], + data[:, ix[0]], + s=measured_scale * w_meas, + color="0.75", + lw=0, + ) # a position with too few peaks was never matched, and its stored # orientation is still the identity; drawing that [001] pattern # would look like a fit where none was attempted @@ -616,7 +641,14 @@ def plot_pattern_matches( inten = sim["intensity"].numpy() sim_rc = np.stack([sim["qx"].numpy(), sim["qy"].numpy()], axis=1) @ rot_back.T if inten.size: + inside = ( + (sim_rc[:, 1] >= min(x_lim)) + & (sim_rc[:, 1] <= max(x_lim)) + & (sim_rc[:, 0] >= min(y_lim)) + & (sim_rc[:, 0] <= max(y_lim)) + ) size = marker_scale * inten / inten.max() + sim_rc, size = sim_rc[inside], size[inside] color = colors[i_om % len(colors)] if marker == "o": # open circles leave the measured disk visible inside @@ -638,8 +670,8 @@ def plot_pattern_matches( color=color, lw=1.8, ) - ax.set_xlim(-q_lim, q_lim) - ax.set_ylim(q_lim, -q_lim) + ax.set_xlim(*x_lim) + ax.set_ylim(*y_lim) ax.set_xticks([]) ax.set_yticks([]) ax.set_aspect("equal") @@ -659,7 +691,7 @@ def plot_pattern_matches( if scalebar and last_row and first_col: add_scalebar_to_ax( ax, - array_size=2 * q_lim, + array_size=abs(x_lim[1] - x_lim[0]), sampling=1.0, length_units=0.5, units="A^-1", diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py index 5cc846fee..a31c7da16 100644 --- a/src/quantem/diffraction/phase.py +++ b/src/quantem/diffraction/phase.py @@ -543,7 +543,7 @@ def plot_phase( plt.Line2D([0], [0], marker="s", ls="", color=c, label=n) for c, n in zip(phase_colors, self.names) ] - ax.legend(handles=handles, loc="upper right", fontsize=8) + ax.legend(handles=handles, loc="upper left", fontsize=8) # stacked reliability colorbars, black -> phase color from matplotlib.cm import ScalarMappable from matplotlib.colors import LinearSegmentedColormap, Normalize diff --git a/tests/diffraction/test_orientation.py b/tests/diffraction/test_orientation.py index f291bf9f3..edae8d120 100644 --- a/tests/diffraction/test_orientation.py +++ b/tests/diffraction/test_orientation.py @@ -651,16 +651,16 @@ def run(consensus_tol): def test_ipf_key_saturates_edges_and_whitens_only_the_centre(): - from quantem.diffraction.orientation_visualization import _bary_to_rgb + from quantem.diffraction.orientation_visualization import IPF_CORNER_COLORS, _bary_to_rgb - corners = _bary_to_rgb(np.eye(3)) - assert np.allclose(corners, [[1, 0, 0], [0, 1, 0], [0, 0, 1]], atol=1e-9) - # every point on an edge is fully saturated: some channel is zero - t = np.linspace(0, 1, 11)[:, None] + assert np.allclose(_bary_to_rgb(np.eye(3)), IPF_CORNER_COLORS) + # every edge point is the full blend of its two corner colors, no white + t = np.linspace(0, 1, 11) for i, j in ((0, 1), (1, 2), (2, 0)): w = np.zeros((11, 3)) - w[:, i], w[:, j] = 1 - t[:, 0], t[:, 0] - assert np.allclose(_bary_to_rgb(w).min(axis=1), 0, atol=1e-9) + w[:, i], w[:, j] = 1 - t, t + blend = w / w.max(axis=1, keepdims=True) + assert np.allclose(_bary_to_rgb(w), np.clip(blend @ IPF_CORNER_COLORS, 0, 1)) assert np.allclose(_bary_to_rgb(np.ones(3) / 3), 1) # halfway from the centre to an edge is still clearly colored assert _bary_to_rgb(np.array([0.5, 0.5, 0.0]) * 0.5 + 1 / 6).min() < 0.7 @@ -689,8 +689,17 @@ def test_plot_matches_background_norm(): om, [(0, 0)], dataset=dataset, pixel_size=0.05, matches=(0,), norm=norm ) # show_2d draws the pattern in the panel, extended to q units - im = axs[0, 0].images[0] + ax = axs[0, 0] + im = ax.images[0] assert np.allclose(im.get_extent()[:2], (-0.5 * 0.05 - 16 * 0.05, 31.5 * 0.05 - 16 * 0.05)) + # the panel shows the recorded area and no marker outside it + assert np.allclose(ax.get_xlim(), im.get_extent()[:2]) + x0, x1, y1, y0 = im.get_extent() + for coll in ax.collections: + xy = coll.get_offsets() + assert ( + (xy[:, 0] >= x0) & (xy[:, 0] <= x1) & (xy[:, 1] >= y0) & (xy[:, 1] <= y1) + ).all() shown.append(np.asarray(im.get_array())[..., 0]) matplotlib.pyplot.close(fig) # gray_r: a lower upper quantile saturates more of the pattern to black diff --git a/tests/diffraction/test_two_phase_map.py b/tests/diffraction/test_two_phase_map.py index 163fd2b34..dad9deb9f 100644 --- a/tests/diffraction/test_two_phase_map.py +++ b/tests/diffraction/test_two_phase_map.py @@ -190,3 +190,27 @@ def test_dynamical_update_is_local_and_examples_are_spread(): assert all( (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 >= 9 for i, a in enumerate(picks) for b in picks[:i] ) + + +def test_loaded_crystal_map_shares_orientation_maps(tmp_path): + from quantem.core.io.serialize import load + from quantem.diffraction.crystal_map import CrystalMap + + rng = np.random.default_rng(5) + ti_a = Crystal.from_ase( + bulk("Ti", "hcp", a=2.9505, c=4.6855), name="Ti alpha", verbose=False + ).calculate_structure_factors(k_max=1.5) + q = torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=torch.float64) + cells = [[_pattern(ti_a, q, rng) for _ in range(3)] for _ in range(2)] + peaks = Vector.from_data(cells, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t") + om = OrientationMap.from_vectors(peaks, ti_a, energy_ev=200e3) + om.build_plan() + om.match_orientations(progress_bar=False) + cm = CrystalMap.from_orientation_maps([om]) + cm.fit(progress_bar=False) + cm.save(tmp_path / "cm.zip", mode="o") + cm2 = load(tmp_path / "cm.zip") + # one set of maps: what a refinement writes through the phase map is + # what the crystal map shows + assert cm2.phases.orientation_maps[0] is cm2.orientation_maps[0] + assert cm2.phases.orientation_maps[0].crystal is cm2[0].crystal From 531f3aca9e4b14f0b07a827f4074a104c463fc14 Mon Sep 17 00:00:00 2001 From: cophus Date: Mon, 28 Sep 2026 12:16:39 -0700 Subject: [PATCH 16/36] fixes to vis --- src/quantem/diffraction/crystal_map.py | 102 +++++++++++++++++- .../diffraction/orientation_visualization.py | 18 +++- src/quantem/diffraction/phase.py | 37 ++++++- 3 files changed, 153 insertions(+), 4 deletions(-) diff --git a/src/quantem/diffraction/crystal_map.py b/src/quantem/diffraction/crystal_map.py index ab9965654..088d61299 100644 --- a/src/quantem/diffraction/crystal_map.py +++ b/src/quantem/diffraction/crystal_map.py @@ -73,6 +73,26 @@ class CrystalMap(AutoSerialize): def __init__( self, orientation_maps: list[OrientationMap], _token=None, k_max: float | None = None ): + """Private constructor; use :meth:`from_vectors` or :meth:`from_orientation_maps`. + + Parameters + ---------- + orientation_maps : list of OrientationMap + One per crystal, all sharing a scan shape. Names must be unique, + because crystals are indexed by name. + _token : object + Guard against direct construction. + k_max : float, optional + Scattering-vector limit shared by the crystals; taken from them + when None. + + Raises + ------ + RuntimeError + If called without the class token. + ValueError + If no crystals are given, names repeat, or scan shapes differ. + """ if _token is not self._token: raise RuntimeError( "Use CrystalMap.from_vectors() or CrystalMap.from_orientation_maps()." @@ -173,20 +193,25 @@ def from_orientation_maps(cls, orientation_maps: list[OrientationMap]) -> "Cryst @property def names(self) -> list[str]: + """Crystal names, in the order the maps were given.""" return [om.crystal.name for om in self.orientation_maps] @property def peaks(self): + """The shared peak list; every crystal was matched against these.""" return self.orientation_maps[0].peaks @property def shape(self) -> tuple[int, int]: + """Scan shape ``(rows, cols)`` in probe positions.""" return tuple(self.orientation_maps[0].peaks.shape[:2]) def __len__(self) -> int: + """Number of crystals.""" return len(self.orientation_maps) def __iter__(self): + """Iterate over the per-crystal OrientationMaps.""" return iter(self.orientation_maps) def __getitem__(self, key) -> OrientationMap: @@ -199,6 +224,7 @@ def __getitem__(self, key) -> OrientationMap: return self.orientation_maps[key] def __repr__(self) -> str: + """Scan shape, how far the analysis has run, and the crystal names.""" R, C = self.shape stage = "unmatched" if self.orientation_maps[0].quats is not None: @@ -290,6 +316,27 @@ def refine_orientations(self, overrides: dict | None = None, **kwargs) -> "Cryst return self._fanout("refine_orientations", overrides, **kwargs) def _fanout(self, method: str, overrides: dict | None, **kwargs) -> "CrystalMap": + """Call ``method`` on every OrientationMap, with per-crystal overrides. + + Parameters + ---------- + method : str + Name of the OrientationMap method to call. + overrides : dict or None + Keyword arguments per crystal name, merged over ``kwargs``. + **kwargs + Arguments common to every crystal. + + Returns + ------- + CrystalMap + Self, so stages chain. + + Raises + ------ + KeyError + If an override names a crystal not in this map. + """ overrides = overrides or {} unknown = set(overrides) - set(self.names) if unknown: @@ -474,6 +521,22 @@ def example_positions( # ------------------------------------------------------------------ def _require_fit(self, what: str) -> PhaseMap: + """Return the fitted PhaseMap, or explain which call needs it first. + + Parameters + ---------- + what : str + Name of the caller, used in the error message. + + Returns + ------- + PhaseMap + + Raises + ------ + ValueError + If :meth:`fit` has not been run. + """ if self.phases is None or self.phases.phase_index is None: raise ValueError(f"run fit() before {what}.") return self.phases @@ -562,6 +625,15 @@ def plot_phase(self, **kwargs): there is one. shade_range : tuple or "auto", default="auto" Values mapped to black ... full color. + shade_gamma : float, default=0.5 + Exponent applied to the brightness. Diffracted intensity is + strongly skewed, so below 1 lifts the faint positions and 1.0 is + the linear scale. Vacuum stays black at any value. + majority_filter : int, default=0 + Radius in probe positions of a majority filter on the phase + decision, for display only. 1 replaces each position by the most + common phase in its 3x3 neighbourhood, dropping isolated single + positions without moving a real boundary. phase_colors : np.ndarray, optional One RGB color per crystal. scalebar : dict, "auto" or None, default="auto" @@ -665,6 +737,14 @@ def plot_matches(self, positions, phase=None, **kwargs): {"power": 0.5, "upper_quantile": 0.98}. measured_scale, measured_power : float, optional Size and intensity compression of the gray measured peaks. + q_max_plot : float, optional + Half-width of every panel, 1/Angstroms. The default fits it to + the peaks plotted. + q_max_quantile : float, default=0.98 + Quantile of the measured peak radii setting that automatic limit. + A few stray high-angle detections would otherwise set the scale + for every panel and leave the pattern surrounded by empty space. + Lower it to crop in further; 1.0 encloses every peak. transpose_plots : bool, default=False Panel layout only: rows are positions unless this is True. **kwargs @@ -726,8 +806,15 @@ def plot_ring_comparison(self, k_min: float = 0.1, k_max: float | None = None, * ) def _k_max_or_crystals(self) -> float: - # maps saved before k_max lived on the CrystalMap carry it only on - # their crystals + """Scattering-vector limit, falling back to the crystals\' own values. + + Maps saved before ``k_max`` lived on the CrystalMap carry it only on + their crystals, so this keeps those files loadable. + + Returns + ------- + float + """ k = getattr(self, "k_max", None) if k is None: k = max(float(om.crystal.k_max or 1.5) for om in self.orientation_maps) @@ -800,6 +887,17 @@ def plot_correlation(self, **kwargs): return show_2d([corr, rel], **kwargs) def _phase_indices(self, phase) -> list[int]: + """Resolve a crystal selector to a list of indices. + + Parameters + ---------- + phase : int, str or None + One crystal by index or name, or None for all of them. + + Returns + ------- + list of int + """ if phase is None: return list(range(len(self.orientation_maps))) i = self.names.index(phase) if isinstance(phase, str) else int(phase) diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py index 8956d1c5c..5b6e3decb 100644 --- a/src/quantem/diffraction/orientation_visualization.py +++ b/src/quantem/diffraction/orientation_visualization.py @@ -444,6 +444,7 @@ def plot_pattern_matches( norm=None, sigma_plot: float | None = 1.0, q_max_plot: float | None = None, + q_max_quantile: float = 0.98, scalebar: bool = True, show_measured: bool = True, marker_scale: float = 250.0, @@ -486,6 +487,15 @@ def plot_pattern_matches( sigma_plot : float | None, default=1.0 Gaussian blur (pixels) of the displayed pattern only, which makes the disks easier to see in low-dose data; None shows it raw. + q_max_plot : float | None + Half-width of every panel, 1/Angstroms. None fits it to the peaks + actually plotted, using `q_max_quantile`. + q_max_quantile : float, default=0.98 + Quantile of the measured peak radii that sets the automatic limit, + used only when `q_max_plot` is None and no `dataset` is given. A few + stray high-angle detections would otherwise set the scale for every + panel and leave the pattern in the middle of empty space, so the + default trims the furthest 2%. Pass 1.0 to enclose every peak. colors : list | None One color per crystal; defaults to red, blue, green, purple. marker : str | None @@ -551,11 +561,17 @@ def plot_pattern_matches( elif dataset is not None and pixel_size is not None: q_lim = dataset.shape[-1] / 2 * pixel_size else: + if not 0.0 < q_max_quantile <= 1.0: + raise ValueError(f"q_max_quantile must be in (0, 1], got {q_max_quantile}") q_all = [ np.hypot(peaks[rx, ry].array[:, ix[0]], peaks[rx, ry].array[:, ix[1]]) for rx, ry in positions ] - q_max = max((float(q.max()) for q in q_all if q.size), default=0.0) + # the direct beam is at zero and every position has one, so it is + # dropped before taking the quantile over the diffracted peaks + q_flat = np.concatenate([q for q in q_all if q.size]) if q_all else np.empty(0) + q_flat = q_flat[q_flat > 0.05] + q_max = float(np.quantile(q_flat, q_max_quantile)) if q_flat.size else 0.0 q_lim = 1.1 * q_max if q_max > 0 else 1.0 if measured_scale is None: diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py index a31c7da16..990e90cf2 100644 --- a/src/quantem/diffraction/phase.py +++ b/src/quantem/diffraction/phase.py @@ -84,6 +84,25 @@ class PhaseMap(AutoSerialize): _token = object() def __init__(self, orientation_maps: list[OrientationMap], _token=None): + """Private constructor; use :meth:`from_orientation_maps`. + + Enumerates the candidate list, one entry per (orientation map, match) + pair, so that two matched orientations of one crystal compete on equal + footing with one orientation of each of two crystals. The fit results + are left as None until :meth:`fit` runs. + + Parameters + ---------- + orientation_maps : list of OrientationMap + Per-crystal maps sharing one set of peaks. + _token : object + Guard against direct construction. + + Raises + ------ + RuntimeError + If called without the class token. + """ if _token is not self._token: raise RuntimeError("Use PhaseMap.from_orientation_maps().") self.orientation_maps = orientation_maps @@ -438,6 +457,7 @@ def plot_phase( phase_colors: np.ndarray | None = None, shade_by: str = "signal", shade_range: tuple[float, float] | str = "auto", + shade_gamma: float = 0.5, majority_filter: int = 0, reliability_range: tuple[float, float] | None = None, scalebar: dict | str | None = "auto", @@ -463,6 +483,14 @@ def plot_phase( Values mapped to black ... full color. "auto" takes a high percentile over the indexed positions, since the absolute scale depends on the data. + shade_gamma : float, default=0.5 + Exponent applied to the brightness, ``alpha ** shade_gamma``. + Diffracted intensity is strongly skewed, so a linear scale leaves + most indexed positions dark and only the brightest grains + readable. Values below 1 lift the faint ones: 0.5 is the default + and 1.0 restores the linear scale. Zero brightness is a fixed + point, so vacuum and unindexed positions stay black however low + this is set, and the colorbars carry the same curve. majority_filter : int, default=0 Radius in probe positions of a majority filter applied to the phase decision for display only; the stored decision is @@ -518,6 +546,11 @@ def plot_phase( raise ValueError( f"shade_by must be 'signal', 'reliability' or 'none', got {shade_by!r}" ) + if shade_gamma <= 0: + raise ValueError(f"shade_gamma must be positive, got {shade_gamma}") + # zero maps to zero under any positive exponent, so unindexed positions + # stay black and only the faint indexed ones are lifted + alpha = np.power(alpha, shade_gamma) rgb = phase_colors[np.where(indexed, phase, 0)] * alpha[..., None] if figax is None: @@ -550,7 +583,9 @@ def plot_phase( n_ph = len(phase_colors) for k, color in enumerate(phase_colors): - cmap_k = LinearSegmentedColormap.from_list(f"rel{k}", [(0, 0, 0), tuple(color)]) + cmap_k = LinearSegmentedColormap.from_list( + f"rel{k}", [(0, 0, 0), tuple(color)], gamma=shade_gamma + ) cax = ax.inset_axes([1.02 + 0.025 * k, 0.05, 0.025, 0.9]) cb = fig.colorbar(ScalarMappable(norm=Normalize(lo, hi), cmap=cmap_k), cax=cax) if k < n_ph - 1: From c2b72366c2bb66e8beaa1a65a3a734860fa5a92c Mon Sep 17 00:00:00 2001 From: cophus Date: Mon, 28 Sep 2026 12:30:44 -0700 Subject: [PATCH 17/36] plotting fixes --- src/quantem/diffraction/crystal.py | 15 +++ src/quantem/diffraction/crystal_map.py | 69 ++++++++-- src/quantem/diffraction/orientation.py | 59 +++++---- .../diffraction/orientation_visualization.py | 120 ++++++++++++++---- src/quantem/diffraction/phase.py | 20 ++- 5 files changed, 222 insertions(+), 61 deletions(-) diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py index c757d0a6f..dd4c443b8 100644 --- a/src/quantem/diffraction/crystal.py +++ b/src/quantem/diffraction/crystal.py @@ -807,6 +807,21 @@ def zone_axis_wedge_labels(self, mathtext: bool = True) -> list[str] | None: # only to within the distortion of the real cell uvw = direction_indices(self.lat_real, c.numpy(), atol=loose) prefix = "~" + if uvw is not None: + # v and -v are the same zone axis: name it with the first + # nonzero index of the printed symbol positive, [100] rather + # than [-100] (for 4-index symbols, [U V T W] with U = 2u - v, + # V = 2v - u, T = -(u + v), up to a common factor) + uvw = np.asarray(uvw) + u, v, w = uvw + shown = ( + np.array([2 * u - v, 2 * v - u, -(u + v), w]) + if self.hexagonal_matching + else uvw + ) + nz = np.flatnonzero(shown) + if nz.size and shown[nz[0]] < 0: + uvw = -uvw labels.append( prefix + format_direction(uvw, hexagonal=self.hexagonal_matching, mathtext=mathtext) diff --git a/src/quantem/diffraction/crystal_map.py b/src/quantem/diffraction/crystal_map.py index 088d61299..a764eabb3 100644 --- a/src/quantem/diffraction/crystal_map.py +++ b/src/quantem/diffraction/crystal_map.py @@ -26,7 +26,7 @@ from quantem.core.io.serialize import AutoSerialize from quantem.diffraction.crystal import Crystal from quantem.diffraction.orientation import OrientationMap -from quantem.diffraction.phase import PhaseMap +from quantem.diffraction.phase import SHADE_GAMMA, PhaseMap def _common_k_max(crystals, k_max: float | None) -> float: @@ -554,7 +554,7 @@ def phase_index(self) -> np.ndarray: """ return self._require_fit("phase_index").phase_index.numpy() - def signal_confidence(self, signal_range="auto") -> np.ndarray: + def signal_confidence(self, signal_range="auto", gamma: float = 1.0) -> np.ndarray: """Confidence in [0, 1] that a crystal is present, from the data alone. The measured intensity beyond the direct beam, scaled to [0, 1]. @@ -568,15 +568,18 @@ def signal_confidence(self, signal_range="auto") -> np.ndarray: signal_range : tuple or "auto", default="auto" Diffracted intensity mapped to 0 ... 1. "auto" spans zero to the 95th percentile over the indexed positions. + gamma : float, default=1.0 + Exponent applied to the result. The default is the raw confidence, + which is what a threshold should be taken on. Returns ------- np.ndarray ``(scan_row, scan_col)`` confidence in [0, 1]. """ - return self._require_fit("signal_confidence()").signal_confidence(signal_range) + return self._require_fit("signal_confidence()").signal_confidence(signal_range, gamma) - def mask(self, phase=None, signal_range="auto") -> np.ndarray: + def mask(self, phase=None, signal_range="auto", gamma: float = SHADE_GAMMA) -> np.ndarray: """Display mask in [0, 1] for one crystal, or for all indexed positions. The phase decision times the diffracted-signal confidence: positions @@ -588,9 +591,21 @@ def mask(self, phase=None, signal_range="auto") -> np.ndarray: Crystal index or name. None (default) keeps every indexed position, whichever crystal won. signal_range : tuple or "auto" - Passed to :meth:`signal_confidence`. + Passed to :meth:`signal_confidence`. Set it to override the + automatic brightness range, e.g. (0, 500). + gamma : float, default=:data:`~quantem.diffraction.phase.SHADE_GAMMA` + Brightness exponent, the same one :meth:`plot_phase` shades with, + so a masked orientation map and the phase map agree. Diffracted + intensity is strongly skewed, so the default lifts the faint + positions; 1.0 gives the linear scale. Zero stays zero, so vacuum + is black at any value. + + Returns + ------- + np.ndarray + ``(scan_row, scan_col)`` mask in [0, 1]. """ - conf = self.signal_confidence(signal_range) + conf = self.signal_confidence(signal_range, gamma) if phase is None: return conf i = self.names.index(phase) if isinstance(phase, str) else int(phase) @@ -654,7 +669,15 @@ def plot_phase(self, **kwargs): """ return self._require_fit("plot_phase()").plot_phase(**kwargs) - def plot_orientation(self, direction=("z", "r"), phase=None, mask=None, **kwargs): + def plot_orientation( + self, + direction=("z", "r"), + phase=None, + mask=None, + signal_range="auto", + shade_gamma: float = SHADE_GAMMA, + **kwargs, + ): """Inverse pole figure maps of every crystal, masked by the phase decision. Parameters @@ -664,9 +687,17 @@ def plot_orientation(self, direction=("z", "r"), phase=None, mask=None, **kwargs phase : int or str, optional Restrict to one crystal. None (default) plots all of them. mask : np.ndarray, optional - Overrides the automatic phase-and-signal mask. + Overrides the automatic phase-and-signal mask entirely. + signal_range : tuple or "auto", default="auto" + Diffracted intensity mapped to black ... full color. Set it to + override the automatic range, e.g. (0, 500). + shade_gamma : float, default=:data:`~quantem.diffraction.phase.SHADE_GAMMA` + Brightness exponent of that mask, the same one :meth:`plot_phase` + shades with. Below 1 lifts the faint positions, 1.0 is the linear + scale. Both are ignored when `mask` is given. **kwargs - Passed to :meth:`OrientationMap.plot_orientation`. + Passed to :meth:`OrientationMap.plot_orientation`, e.g. `smooth`, + and `saturation_power` and `chroma` for the color wedge. Returns ------- @@ -676,14 +707,22 @@ def plot_orientation(self, direction=("z", "r"), phase=None, mask=None, **kwargs dirs = [direction] if isinstance(direction, str) else list(direction) out = [] for i in self._phase_indices(phase): - m = mask if mask is not None else self.mask(i) + m = mask if mask is not None else self.mask(i, signal_range, shade_gamma) for d in dirs: out.append( self.orientation_maps[i].plot_orientation(direction=d, mask=m, **kwargs) ) return out - def plot_pole_figure(self, pole=(0, 0, 1), phase=None, mask=None, **kwargs): + def plot_pole_figure( + self, + pole=(0, 0, 1), + phase=None, + mask=None, + signal_range="auto", + shade_gamma: float = SHADE_GAMMA, + **kwargs, + ): """Stereographic pole figure of each crystal, masked by the phase decision. Parameters @@ -693,7 +732,11 @@ def plot_pole_figure(self, pole=(0, 0, 1), phase=None, mask=None, **kwargs): phase : int or str, optional Restrict to one crystal. None (default) plots all of them. mask : np.ndarray, optional - Overrides the automatic phase-and-signal mask. + Overrides the automatic phase-and-signal mask entirely. + signal_range : tuple or "auto", default="auto" + Diffracted intensity mapped to black ... full color. + shade_gamma : float, default=:data:`~quantem.diffraction.phase.SHADE_GAMMA` + Brightness exponent of that mask. Ignored when `mask` is given. **kwargs Passed to :meth:`OrientationMap.plot_pole_figure`, e.g. `color_by`, `int_range` and `overlay`. @@ -705,7 +748,7 @@ def plot_pole_figure(self, pole=(0, 0, 1), phase=None, mask=None, **kwargs): """ out = [] for i in self._phase_indices(phase): - m = mask if mask is not None else self.mask(i) + m = mask if mask is not None else self.mask(i, signal_range, shade_gamma) out.append(self.orientation_maps[i].plot_pole_figure(pole=pole, mask=m, **kwargs)) return out diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index fda671e95..b68ab1ae9 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -121,31 +121,44 @@ def smooth_quaternions( q = torch.as_tensor(quats, dtype=torch.float64) R, C = q.shape[:2] active = torch.as_tensor(active, dtype=torch.bool) + sym = torch.as_tensor(sym_quats, dtype=torch.float64).reshape(-1, 4) rad = max(1, int(np.ceil(3 * sigma_px))) - out = q.clone() - w_ang = 2.0 * sigma_deg**2 - for rx in range(R): - for ry in range(C): - if not bool(active[rx, ry]): - continue - r0, r1 = max(0, rx - rad), min(R, rx + rad + 1) - c0, c1 = max(0, ry - rad), min(C, ry + rad + 1) - sel = active[r0:r1, c0:c1] - if int(sel.sum()) < 2: - continue - rr, cc = torch.nonzero(sel, as_tuple=True) - qn = q[r0:r1, c0:c1][sel] - d2 = ((rr + r0 - rx) ** 2 + (cc + c0 - ry) ** 2).to(torch.float64) - ang = misorientation_angle_deg(q[rx, ry], qn, sym_quats) - keep = ang <= max_angle_deg - if int(keep.sum()) < 2: + cos_max = float(np.cos(np.deg2rad(max_angle_deg) / 2)) + # accumulate the weighted outer products one neighbour offset at a time, + # over the whole map at once + M = torch.zeros((R, C, 4, 4), dtype=torch.float64) + count = torch.zeros((R, C), dtype=torch.long) + for dr in range(-rad, rad + 1): + for dc in range(-rad, rad + 1): + r0, r1 = max(0, -dr), min(R, R - dr) + c0, c1 = max(0, -dc), min(C, C - dc) + if r1 <= r0 or c1 <= c0: continue - w = torch.exp(-d2[keep] / (2 * sigma_px**2)) * torch.exp(-(ang[keep] ** 2) / w_ang) - qk = symmetry_aligned(q[rx, ry], qn[keep], sym_quats) - qk = qk * torch.sign((qk @ q[rx, ry]).unsqueeze(-1)) - M = (w[:, None, None] * (qk[:, :, None] * qk[:, None, :])).sum(0) - _, evecs = torch.linalg.eigh(M) - out[rx, ry] = qnormalize(evecs[:, -1]) + q0 = q[r0:r1, c0:c1] + qn = q[r0 + dr : r1 + dr, c0 + dc : c1 + dc] + ok = active[r0:r1, c0:c1] & active[r0 + dr : r1 + dr, c0 + dc : c1 + dc] + # the symmetry image of the neighbour nearest each centre + cand = qmult(qn[..., None, :], sym) # (r, c, S, 4) + dots = torch.einsum("rcsi,rci->rcs", cand, q0) + best = dots.abs().argmax(dim=-1) + qk = torch.gather(cand, 2, best[..., None, None].expand(*best.shape, 1, 4))[..., 0, :] + dot = (qk * q0).sum(-1) + qk = qk * torch.sign(dot)[..., None] + cos_half = dot.abs().clamp(max=1.0) + ok &= cos_half >= cos_max + ang = torch.rad2deg(2 * torch.acos(cos_half)) + w = np.exp(-(dr * dr + dc * dc) / (2 * sigma_px**2)) * torch.exp( + -(ang**2) / (2.0 * sigma_deg**2) + ) + w = torch.where(ok, w, torch.zeros_like(w)) + M[r0:r1, c0:c1] += w[..., None, None] * qk[..., :, None] * qk[..., None, :] + count[r0:r1, c0:c1] += ok.to(torch.long) + out = q.clone() + # a position needs itself and at least one neighbour inside the angle + upd = active & (count >= 2) + if bool(upd.any()): + _, evecs = torch.linalg.eigh(M[upd]) + out[upd] = qnormalize(evecs[..., -1]) return out diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py index 5b6e3decb..99e6df4b4 100644 --- a/src/quantem/diffraction/orientation_visualization.py +++ b/src/quantem/diffraction/orientation_visualization.py @@ -20,7 +20,13 @@ ) ORIGIN_COLOR = "#2ca02c" MEASURED_COLOR = "0.15" -IPF_SATURATION_POWER = 0.6 # <1 shrinks the white centre of the IPF wedge +# exponent on the distance from the wedge centre: >1 widens the white centre +# and softens the transition into it, <1 shrinks it (much below 0.5 leaves a +# bright point at the centre) +IPF_SATURATION_POWER = 0.5 +# colorfulness relative to the corner colors: 1 keeps them exact, lower values +# wash the whole wedge toward white +IPF_CHROMA = 1.0 # corner colors: full red, green capped to avoid the fluorescent look, blue # lifted off pure dark blue; pairwise blends give near-max-chroma orange, # cyan and violet along the edges @@ -46,23 +52,49 @@ ] -def _bary_to_rgb(w: np.ndarray) -> np.ndarray: +def _bary_to_rgb( + w: np.ndarray, + saturation_power: float | None = None, + chroma: float | None = None, +) -> np.ndarray: """Barycentric wedge weights (..., 3) to RGB. - Each direction takes the color of the edge point straight out from the - wedge centre, a blend of the two nearest corner colors, and fades toward - white with its distance from that edge. Every edge is fully colored and - only the centre itself is white, so the whole wedge keeps its contrast. + The corner colors are mixed additively with the weights scaled by their + 4-norm, a smooth stand-in for dividing by the largest weight: the mix + stays bright between corners (orange, cyan and violet midway along the + edges) and the corners keep their own colors. The mix is then faded + toward white by 1 - 27 w0 w1 w2, which is zero at the wedge centre and + one on every edge. Both terms are smooth in the weights, so the colors + change gradually across the whole wedge, with no creases where one + corner takes over from another, and the mix never leaves the sRGB gamut. + + Parameters + ---------- + w : np.ndarray + Barycentric coordinates in the fundamental wedge, (..., 3). + saturation_power : float | None + Exponent on the distance from the wedge centre. Above 1 the color + builds up more slowly away from the centre, widening the white region + and softening the transition into it; below 1 shrinks it. None takes + `IPF_SATURATION_POWER`. + chroma : float | None + Colorfulness relative to the corner colors: 1 keeps them exact, lower + washes the wedge toward white. None takes `IPF_CHROMA`. + + Returns + ------- + np.ndarray + RGB array (..., 3) in [0, 1]. """ - w = np.clip(w, 0, None) + saturation_power = IPF_SATURATION_POWER if saturation_power is None else saturation_power + chroma = IPF_CHROMA if chroma is None else chroma + w = np.clip(np.asarray(w, dtype=float), 0, None) w = w / np.clip(w.sum(axis=-1, keepdims=True), 1e-12, None) - m = w.min(axis=-1, keepdims=True) - # the edge point on the ray from the centre (1/3, 1/3, 1/3) through w - edge = (w - m) / np.clip(1.0 - 3.0 * m, 1e-12, None) - edge = edge / np.clip(edge.max(axis=-1, keepdims=True), 1e-12, None) - rgb = np.clip(edge @ IPF_CORNER_COLORS, 0, 1) - sat = np.clip(1.0 - 3.0 * m, 0, 1) ** IPF_SATURATION_POWER - return 1.0 - sat * (1.0 - rgb) + u = w / np.clip((w**4).sum(axis=-1, keepdims=True) ** 0.25, 1e-12, None) + rgb = u @ IPF_CORNER_COLORS + # distance from the centre: 0 there, 1 on the edges, smooth in w + r = np.clip(1.0 - 27.0 * w[..., 0] * w[..., 1] * w[..., 2], 0, 1) ** saturation_power + return np.clip(1.0 - np.clip(chroma * r, 0, 1)[..., None] * (1.0 - rgb), 0, 1) def _parse_direction(direction) -> torch.Tensor: @@ -118,6 +150,8 @@ def ipf_color( orientations: torch.Tensor, crystal: Crystal, direction: str | torch.Tensor = "z", + saturation_power: float | None = None, + chroma: float | None = None, ) -> np.ndarray: """Inverse pole figure RGB colors for orientations. @@ -130,6 +164,10 @@ def ipf_color( direction : {"x", "y", "z"} | torch.Tensor, default="z" Lab direction whose crystal-frame coordinates are colored; "z" is the beam direction (zone-axis map). + saturation_power : float | None + Width of the white centre; see :func:`_bary_to_rgb`. + chroma : float | None + Colorfulness relative to the corner colors; see :func:`_bary_to_rgb`. Returns ------- @@ -148,12 +186,15 @@ def ipf_color( az = (torch.atan2(v[..., 1], v[..., 0]) / (2 * np.pi)) % 1.0 pol = torch.acos(v[..., 2].clamp(-1, 1)) / (np.pi / 2) - hsv = torch.stack((az, pol.clamp(0, 1), torch.ones_like(az)), dim=-1) + sat = pol.clamp(0, 1) ** ( + IPF_SATURATION_POWER if saturation_power is None else saturation_power + ) + hsv = torch.stack((az, sat, torch.ones_like(az)), dim=-1) return hsv_to_rgb(hsv.numpy()) A_inv = torch.linalg.inv(corners.to(v.dtype).T) w = torch.einsum("ij,...j->...i", A_inv, v) - return _bary_to_rgb(w.numpy()) + return _bary_to_rgb(w.numpy(), saturation_power, chroma) def fold_in_plane(quats: torch.Tensor, crystal: Crystal, strict: bool = False) -> torch.Tensor: @@ -226,12 +267,17 @@ def wedge_legend( labels: bool = True, orientation: str = "horizontal", fontsize: int = 11, + saturation_power: float | None = None, + chroma: float | None = None, ) -> None: """Draw the labeled IPF color triangle for the crystal's fundamental wedge. Corner direction labels use 4-index Miller-Bravais symbols for hexagonal and trigonal crystals. orientation="vertical" rotates the wedge 90 degrees to fill a tall side panel. + + `saturation_power` and `chroma` go to :func:`_bary_to_rgb` and must match + the map being labelled, which the plotting functions ensure. """ corners = crystal.zone_axis_wedge() if corners is None: @@ -277,7 +323,7 @@ def wedge_legend( W = V @ A_inv.T inside = (W > -1e-9).all(axis=-1) rgba = np.zeros(X.shape + (4,)) - rgba[..., :3] = _bary_to_rgb(W) + rgba[..., :3] = _bary_to_rgb(W, saturation_power, chroma) rgba[..., 3] = inside ax.imshow(rgba, extent=(x0, x1, y0, y1), origin="lower", interpolation="nearest") for exy in edges: @@ -313,6 +359,8 @@ def plot_orientation_map( title: str | None = None, fold: bool | str = "auto", smooth: dict | bool | None = None, + saturation_power: float | None = None, + chroma: float | None = None, ): """IPF-colored orientation map with the wedge legend in an adjacent panel. @@ -346,6 +394,14 @@ def plot_orientation_map( a dict naming the values you want, such as {"sigma_px": 1.0, "sigma_deg": 1.0, "max_angle_deg": 5.0}, which is also what True uses. + saturation_power : float | None + Widens or narrows the white centre of the color wedge. Above 1 covers + a wider range of orientations near the wedge centre and softens the + transition into it. None takes `IPF_SATURATION_POWER`. + chroma : float | None + Colorfulness relative to the corner colors: 1 keeps them exact, lower + washes the map toward white. None takes `IPF_CHROMA`. + The legend is drawn with the same values. scalebar : dict | None Real-space scale bar, e.g. {"sampling": 30, "units": "A"}. figax : (fig, (ax_map, ax_legend)) | (fig, ax_map) | None @@ -367,9 +423,10 @@ def plot_orientation_map( "is the argument that keeps the average inside one grain" ) quats = om.smoothed_quats(match=match, **(smooth if isinstance(smooth, dict) else {})) - if fold: + if fold and not (isinstance(direction, str) and direction == "z"): + # the zone-axis color does not depend on the in-plane angle quats = fold_in_plane(quats, om.crystal, strict=fold != "auto") - rgb = ipf_color(quats, om.crystal, direction) + rgb = ipf_color(quats, om.crystal, direction, saturation_power, chroma) if mask is not None: rgb = rgb * np.asarray(mask, dtype=float)[..., None] if crop is not None: @@ -429,7 +486,13 @@ def plot_orientation_map( loc="lower right", ) if legend and ax_leg is not None: - wedge_legend(om.crystal, ax_leg, orientation="vertical") + wedge_legend( + om.crystal, + ax_leg, + orientation="vertical", + saturation_power=saturation_power, + chroma=chroma, + ) return fig, ax @@ -907,6 +970,8 @@ def plot_pole_figure( label: str | None = None, grid: bool = True, overlay: dict | None = None, + saturation_power: float | None = None, + chroma: float | None = None, figax=None, ): """Stereographic pole figure of a crystal direction family over the map. @@ -937,6 +1002,9 @@ def plot_pole_figure( Annotation for the pole family, e.g. "(0001)" or "{110}". grid : bool, default=True Draw polar-angle circles and azimuth spokes every 30 degrees. + saturation_power, chroma : float | None + Color wedge shape, used when `color_by` is "ipf"; see + :func:`plot_orientation_map`. """ import matplotlib.pyplot as plt @@ -960,7 +1028,9 @@ def plot_pole_figure( if color_by == "ipf": # white background: blend from white toward the per-position IPF # color as the histogram density rises - rgb_pos = ipf_color(om.quats[..., match, :], om.crystal, "z").reshape(-1, 3) + rgb_pos = ipf_color( + om.quats[..., match, :], om.crystal, "z", saturation_power, chroma + ).reshape(-1, 3) rgb_all = rgb_pos[src] img = np.zeros((bins, bins, 3)) cnt = np.zeros((bins, bins)) @@ -1115,7 +1185,13 @@ def plot_pole_figure( ax.set_title(title) if ax_leg is not None: if color_by == "ipf": - wedge_legend(om.crystal, ax_leg, orientation="vertical") + wedge_legend( + om.crystal, + ax_leg, + orientation="vertical", + saturation_power=saturation_power, + chroma=chroma, + ) else: ax_leg.axis("off") return fig, ax diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py index 990e90cf2..08acb245f 100644 --- a/src/quantem/diffraction/phase.py +++ b/src/quantem/diffraction/phase.py @@ -36,6 +36,11 @@ ) from quantem.diffraction.orientation import OrientationMap, position_mask +# Diffracted intensity is strongly skewed, so a linear brightness leaves most +# indexed positions nearly black. This exponent is applied wherever a map is +# shaded by signal, so the phase map and the orientation maps agree. +SHADE_GAMMA = 0.5 + def _majority_filter(phase: np.ndarray, radius: int) -> np.ndarray: """Replace each position by the most common phase around it. @@ -415,6 +420,7 @@ def apply_dynamical(self, result: dict) -> "PhaseMap": def signal_confidence( self, signal_range: tuple[float, float] | str = "auto", + gamma: float = 1.0, ) -> np.ndarray: """Confidence in [0, 1] that a crystal is present, from the data alone. @@ -434,12 +440,19 @@ def signal_confidence( signal_range : tuple | "auto", default="auto" Diffracted intensity mapped to 0 ... 1. "auto" spans zero to the 95th percentile over the indexed positions. + gamma : float, default=1.0 + Exponent applied to the result. The default returns the raw + confidence, which is what a threshold should be taken on; + :attr:`SHADE_GAMMA` is the value used when shading a display, and + zero stays zero either way. Returns ------- np.ndarray ``(scan_row, scan_col)`` confidence in [0, 1]. """ + if gamma <= 0: + raise ValueError(f"gamma must be positive, got {gamma}") if self.diffracted_intensity is None: raise ValueError("Run fit() before signal_confidence().") sig = np.nan_to_num(self.diffracted_intensity.numpy()) @@ -450,14 +463,15 @@ def signal_confidence( lo, hi = 0.0, max(hi, 1e-12) else: lo, hi = signal_range - return ((sig - lo) / max(hi - lo, 1e-12)).clip(0, 1) * indexed + conf = ((sig - lo) / max(hi - lo, 1e-12)).clip(0, 1) * indexed + return conf if gamma == 1.0 else np.power(conf, gamma) def plot_phase( self, phase_colors: np.ndarray | None = None, shade_by: str = "signal", shade_range: tuple[float, float] | str = "auto", - shade_gamma: float = 0.5, + shade_gamma: float = SHADE_GAMMA, majority_filter: int = 0, reliability_range: tuple[float, float] | None = None, scalebar: dict | str | None = "auto", @@ -483,7 +497,7 @@ def plot_phase( Values mapped to black ... full color. "auto" takes a high percentile over the indexed positions, since the absolute scale depends on the data. - shade_gamma : float, default=0.5 + shade_gamma : float, default=:attr:`SHADE_GAMMA` (0.5) Exponent applied to the brightness, ``alpha ** shade_gamma``. Diffracted intensity is strongly skewed, so a linear scale leaves most indexed positions dark and only the brightest grains From 2fcdf516f4d9ae41be00e94c12bd72f674cd9e13 Mon Sep 17 00:00:00 2001 From: cophus Date: Mon, 28 Sep 2026 12:31:34 -0700 Subject: [PATCH 18/36] test update --- tests/diffraction/test_orientation.py | 41 ++++++++++++++++++++------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/tests/diffraction/test_orientation.py b/tests/diffraction/test_orientation.py index edae8d120..7c24b0c5e 100644 --- a/tests/diffraction/test_orientation.py +++ b/tests/diffraction/test_orientation.py @@ -650,20 +650,41 @@ def run(consensus_tol): assert float(run(0.01).max()) < 2.0 -def test_ipf_key_saturates_edges_and_whitens_only_the_centre(): +def test_ipf_key_is_smooth_with_exact_corners_and_white_centre(): from quantem.diffraction.orientation_visualization import IPF_CORNER_COLORS, _bary_to_rgb assert np.allclose(_bary_to_rgb(np.eye(3)), IPF_CORNER_COLORS) - # every edge point is the full blend of its two corner colors, no white - t = np.linspace(0, 1, 11) - for i, j in ((0, 1), (1, 2), (2, 0)): - w = np.zeros((11, 3)) - w[:, i], w[:, j] = 1 - t, t - blend = w / w.max(axis=1, keepdims=True) - assert np.allclose(_bary_to_rgb(w), np.clip(blend @ IPF_CORNER_COLORS, 0, 1)) assert np.allclose(_bary_to_rgb(np.ones(3) / 3), 1) - # halfway from the centre to an edge is still clearly colored - assert _bary_to_rgb(np.array([0.5, 0.5, 0.0]) * 0.5 + 1 / 6).min() < 0.7 + # no creases: along lines across the wedge, including across the lines + # where one corner takes over from another, the color turns gently + t = np.linspace(0, 1, 801)[:, None] + for p0, p1 in ( + ((0.55, 0.40, 0.05), (0.20, 0.10, 0.70)), + ((0.90, 0.05, 0.05), (0.05, 0.90, 0.05)), + ((0.70, 0.30, 0.00), (0.00, 0.30, 0.70)), + ): + c = _bary_to_rgb(np.array(p0) * (1 - t) + np.array(p1) * t) + assert np.abs(np.diff(c, 2, axis=0)).max() < 1e-4 + # the edges stay colored all along: midway between two corners is vivid + for i, j in ((0, 1), (1, 2), (2, 0)): + w = np.zeros(3) + w[i] = w[j] = 0.5 + rgb = _bary_to_rgb(w) + assert rgb.max() - rgb.min() > 0.5 + + +def test_wedge_labels_name_zone_axes_with_positive_leading_index(): + import re + + for xtl in ( + Crystal.from_ase(bulk("Cu", "fcc", a=3.6, cubic=True), verbose=False), + Crystal.from_ase(bulk("Ti", "hcp", a=2.95, c=4.68), verbose=False), + ): + for label in xtl.zone_axis_wedge_labels(mathtext=False): + # the first nonzero digit carries no overbar + m = re.search("[1-9]", label) + assert m is not None + assert label[m.end() : m.end() + 1] != "\u0305", label def test_plot_matches_background_norm(): From 285042580729bd2515f13f7d6f1cc2f4ee6c7134 Mon Sep 17 00:00:00 2001 From: cophus Date: Thu, 1 Oct 2026 09:22:12 -0700 Subject: [PATCH 19/36] one more arg --- src/quantem/diffraction/orientation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index b68ab1ae9..8e6c45e15 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -49,7 +49,6 @@ quat_from_axis_angle, quat_from_zone_axis, sample_zone_axes, - symmetry_aligned, ) From 92862d412c08ffd57ec5a8cd891ac1f08479cb40 Mon Sep 17 00:00:00 2001 From: cophus Date: Fri, 2 Oct 2026 08:15:40 -0700 Subject: [PATCH 20/36] ACOM updates --- src/quantem/diffraction/bragg_vectors.py | 4 +- src/quantem/diffraction/calibration.py | 227 ++++++++++++++++++----- src/quantem/diffraction/crystal_map.py | 66 ++++++- tests/diffraction/test_two_phase_map.py | 33 +++- 4 files changed, 281 insertions(+), 49 deletions(-) diff --git a/src/quantem/diffraction/bragg_vectors.py b/src/quantem/diffraction/bragg_vectors.py index 4116de842..280408aa7 100644 --- a/src/quantem/diffraction/bragg_vectors.py +++ b/src/quantem/diffraction/bragg_vectors.py @@ -1500,7 +1500,9 @@ def calibrate(self, crystal, pixel_size_guess: float, **kwargs): rotation_ccw_deg : float, default=0.0 Diffraction-to-scan rotation, recorded for later use. plot : bool, default=True - Show the ring comparison before and after. + Show the ring comparison before and after, for the first + reference crystal. :meth:`CrystalMap.plot_calibration` checks + every candidate phase afterwards. **kwargs Further arguments of :func:`~quantem.diffraction.calibration.calibrate`, e.g. diff --git a/src/quantem/diffraction/calibration.py b/src/quantem/diffraction/calibration.py index 6b811b4a5..f2271ca04 100644 --- a/src/quantem/diffraction/calibration.py +++ b/src/quantem/diffraction/calibration.py @@ -940,6 +940,13 @@ def calibrate( Returns ------- DiffractionCalibration + + Notes + ----- + The figure shows the first reference crystal before and after the fit. + To check every candidate phase against the result, including phases the + calibration was not fit to, plot :func:`plot_calibration` on the + calibrated peaks, or :meth:`CrystalMap.plot_calibration`. """ crystals = [crystal] if isinstance(crystal, Crystal) else list(crystal) for xtl in crystals: @@ -1030,51 +1037,25 @@ def calibrate( sharex="col", gridspec_kw={"height_ratios": [1, 1.3]}, ) - rings_ref = _crystal_rings(crystals[0], max(k_min, 0.12), k_hi) - - for col, (pk, ttl) in enumerate( + for col, (pk, ttl, kb) in enumerate( ( - (peaks_0, f"before: {pixel_size_guess:.5f} " + r"$\mathrm{\AA}^{-1}$/px"), - (peaks, f"after: {pixel_size:.5f} " + r"$\mathrm{\AA}^{-1}$/px"), + (peaks_0, f"before: {pixel_size_guess:.5f} " + r"$\mathrm{\AA}^{-1}$/px", None), + (peaks, f"after: {pixel_size:.5f} " + r"$\mathrm{\AA}^{-1}$/px", k_broadening), ) ): - plot_ring_comparison( + _calibration_panels( + axs[0, col], + axs[1, col], pk, - crystals, + crystals[0], k_min=k_min, k_max=k_hi, - k_broadening=None if col == 0 else k_broadening, + k_broadening=kb, bragg_k_power=bragg_k_power, - figax=(fig, [axs[0, col]]), + marker_size=marker_size, ) axs[0, col].set_title(ttl, fontsize=10) - axs[0, col].set_xlabel("") - - # azimuth against scattering vector: the elliptic distortion is the - # cos(2 phi) wobble of every ring, read off against the black lines - ax = axs[1, col] - flat = pk.select_fields("qx", "qy", "intensity").flatten() - r = np.hypot(flat[:, 0], flat[:, 1]) - phi = np.degrees(np.arctan2(flat[:, 1], flat[:, 0])) - sel = (r > k_min) & (r < k_hi) - w = flat[sel, 2] - hi = float(np.percentile(w, 99.5)) if w.size else 1.0 - ax.scatter( - r[sel], - phi[sel], - s=marker_size * np.clip(w / max(hi, 1e-12), 0.03, 1.0), - c="r", - alpha=0.5, - lw=0, - rasterized=True, - ) - for g0 in rings_ref: - ax.axvline(g0, color="k", lw=0.7, alpha=0.8) - ax.set_xlim(k_min, k_hi) - ax.set_ylim(-180, 180) - ax.set_yticks([-180, -90, 0, 90, 180]) - ax.set_xlabel(r"scattering vector (1/$\mathrm{\AA}$)") - axs[1, 0].set_ylabel("azimuth (deg)") + axs[1, 1].set_ylabel("") if ellipse is not None: axs[1, 0].text( 0.02, @@ -1099,6 +1080,156 @@ def calibrate( return (cal, fig, axs) if returnfig else cal +def _ring_shells(crystal: Crystal, k_min: float, k_max: float, bragg_k_power: float): + """Ring radii of a crystal in (k_min, k_max) and their summed intensity, + relative to the strongest ring in that range.""" + g = crystal.g_len.numpy() + ints = crystal.struct_factors_int.numpy() * g**bragg_k_power + shells = np.round(g / 0.005) * 0.005 + keep = (shells > k_min) & (shells < k_max) + uniq = np.unique(shells[keep]) + tot = np.array([ints[keep][shells[keep] == u].sum() for u in uniq]) + return uniq, tot / max(float(tot.max()), 1e-30) if tot.size else tot + + +def _calibration_panels( + ax_hist, + ax_az, + peaks, + crystal: Crystal, + k_min: float, + k_max: float, + k_broadening: float | None = None, + bragg_k_power: float = 2.0, + marker_size: float = 8.0, + n_rings: int = 12, +) -> None: + """The radial histogram against one crystal's rings (top) and every peak + as azimuth against scattering vector with its strongest rings (bottom).""" + plot_ring_comparison( + peaks, + [crystal], + k_min=k_min, + k_max=k_max, + k_broadening=k_broadening, + bragg_k_power=bragg_k_power, + n_labels=n_rings, + figax=(ax_hist.figure, [ax_hist]), + ) + ax_hist.set_xlabel("") + # azimuth against scattering vector: a pixel size error shifts every + # ring, the elliptic distortion is a cos(2 phi) wobble of each one + flat = peaks.select_fields("qx", "qy", "intensity").flatten() + r = np.hypot(flat[:, 0], flat[:, 1]) + phi = np.degrees(np.arctan2(flat[:, 1], flat[:, 0])) + sel = (r > k_min) & (r < k_max) + w = flat[sel, 2] + hi = float(np.percentile(w, 99.5)) if w.size else 1.0 + ax_az.scatter( + r[sel], + phi[sel], + s=marker_size * np.clip(w / max(hi, 1e-12), 0.03, 1.0), + c="r", + alpha=0.5, + lw=0, + rasterized=True, + ) + radii, rel = _ring_shells(crystal, k_min, k_max, bragg_k_power) + for g0 in radii[np.argsort(-rel)[:n_rings]]: + ax_az.axvline(g0, color="k", lw=0.7, alpha=0.8) + ax_az.set_xlim(k_min, k_max) + ax_az.set_ylim(-180, 180) + ax_az.set_yticks([-180, -90, 0, 90, 180]) + ax_az.set_xlabel(r"scattering vector (1/$\mathrm{\AA}$)") + ax_az.set_ylabel("azimuth (deg)") + + +def plot_calibration( + peaks, + crystals, + k_min: float = 0.05, + k_max: float = 1.5, + k_broadening: float | None = None, + bragg_k_power: float = 2.0, + marker_size: float = 8.0, + n_rings: int = 12, + axsize: tuple[float, float] = (6.0, 6.4), + figax=None, +): + """Calibrated peaks against the rings of every crystal, one column each. + + The calibration check for all candidate phases, however many of them the + calibration itself was fit to. Each column holds the radial histogram of + every peak against that crystal's rings (top) and every peak as azimuth + against scattering vector with the same rings (bottom). A ring beside the + measured peaks in every direction is a lattice parameter or pixel size + error; a ring that wobbles with azimuth is elliptic distortion. + + Parameters + ---------- + peaks : Vector + Calibrated peaks (qx, qy, intensity) in 1/Angstroms. + crystals : Crystal | list[Crystal] + Candidate phases, structure factors calculated. + k_min, k_max : float + Range of scattering vectors shown, 1/Angstroms. + k_broadening : float | None + None draws sharp ring lines in the histograms; a width (1/Angstroms) + draws the broadened ring profile instead. + marker_size : float, default=8.0 + Area of the brightest peak in the azimuth panels. + n_rings : int, default=12 + Rings drawn in the azimuth panels and labeled in the histograms, + strongest first; a large cell would otherwise fill both with its + weak superstructure rings. + axsize : tuple, default=(6.0, 6.4) + Size of one column. + figax : (fig, axs) | None + Existing figure and a (2, n_crystals) array of axes. + + Returns + ------- + tuple + ``(fig, axs)``, axs of shape (2, n_crystals). + """ + import matplotlib.pyplot as plt + + xtls = [crystals] if isinstance(crystals, Crystal) else list(crystals) + if figax is None: + fig, axs = plt.subplots( + 2, + len(xtls), + figsize=(axsize[0] * len(xtls), axsize[1]), + sharex=True, + squeeze=False, + gridspec_kw={"height_ratios": [1, 1.3]}, + ) + else: + fig, axs = figax + axs = np.asarray(axs).reshape(2, len(xtls)) + for col, xtl in enumerate(xtls): + _calibration_panels( + axs[0, col], + axs[1, col], + peaks, + xtl, + k_min=k_min, + k_max=k_max, + k_broadening=k_broadening, + bragg_k_power=bragg_k_power, + marker_size=marker_size, + n_rings=n_rings, + ) + axs[0, col].set_title(xtl.name, fontsize=10) + if col: + axs[0, col].set_ylabel("") + axs[1, col].set_ylabel("") + if figax is None: + fig.tight_layout() + fig.subplots_adjust(hspace=0.08) + return fig, axs + + def plot_ring_comparison( peaks, crystals, @@ -1108,6 +1239,7 @@ def plot_ring_comparison( bragg_k_power: float = 2.0, label_hkl: bool = True, label_min_intensity: float = 0.05, + n_labels: int = 12, figax=None, ): """Measured radial peak histogram against crystal ring positions. @@ -1131,6 +1263,9 @@ def plot_ring_comparison( label_min_intensity : float, default=0.05 Label rings whose summed intensity exceeds this fraction of the strongest ring. + n_labels : int, default=12 + Label at most this many rings, strongest first, which keeps a large + cell with many rings readable. """ import matplotlib.pyplot as plt @@ -1171,21 +1306,26 @@ def plot_ring_comparison( ) if label_hkl: - rows = 0 + # the strongest rings first, each at least 0.03 1/A from the + # last, then drawn left to right labeled_g: list[float] = [] - for u, si in zip(uniq, shell_int): + for j in np.argsort(-shell_int): + u, si = uniq[j], shell_int[j] + if len(labeled_g) >= n_labels: + break if u < k_min or u > k_max or si < label_min_intensity: continue if any(abs(u - g0) < 0.03 for g0 in labeled_g): continue + labeled_g.append(u) + for rows, u in enumerate(sorted(labeled_g)): in_shell = shells == u idx = np.nonzero(in_shell)[0] idx = idx[ints[idx] > 0.99 * ints[idx].max()] key = [tuple(-hkl_np[i]) for i in idx] best = idx[int(np.lexsort(np.array(key).T[::-1])[0])] - labeled_g.append(u) - y = 1.05 + 0.11 * (rows % 2) - rows += 1 + # three staggered rows keep neighbouring labels apart + y = 1.04 + 0.1 * (rows % 3) ax.text( u, y, @@ -1195,8 +1335,9 @@ def plot_ring_comparison( va="bottom", ) ax.set_ylabel("intensity (norm.)") - ax.set_ylim(0, 1.32) - ax.legend(loc="upper right", fontsize=9) + ax.set_ylim(0, 1.42) + # below the band of ring labels at the top + ax.legend(loc="upper right", bbox_to_anchor=(1.0, 0.72), fontsize=8) axs[-1].set_xlabel(r"scattering vector (1/$\mathrm{\AA}$)") if figax is None: fig.tight_layout() diff --git a/src/quantem/diffraction/crystal_map.py b/src/quantem/diffraction/crystal_map.py index a764eabb3..6667b78f4 100644 --- a/src/quantem/diffraction/crystal_map.py +++ b/src/quantem/diffraction/crystal_map.py @@ -404,7 +404,7 @@ def refine_dynamical( from quantem.diffraction import bloch pm = self._require_fit("refine_dynamical()") - k_max = float(kwargs.pop("k_max", None) or self.k_max) + k_max = float(kwargs.pop("k_max", None) or self._k_max_or_crystals()) k_c = float(k_max_coupling if k_max_coupling is not None else 1.5 * k_max) energy_ev = self.orientation_maps[0].energy_ev for om in self.orientation_maps: @@ -743,14 +743,15 @@ def plot_pole_figure( Returns ------- - list of tuple - One ``(fig, ax)`` per crystal. + tuple or list of tuple + ``(fig, ax)`` when `phase` names one crystal, otherwise one + ``(fig, ax)`` per crystal. """ out = [] for i in self._phase_indices(phase): m = mask if mask is not None else self.mask(i, signal_range, shade_gamma) out.append(self.orientation_maps[i].plot_pole_figure(pole=pole, mask=m, **kwargs)) - return out + return out[0] if phase is not None else out def plot_matches(self, positions, phase=None, **kwargs): """Matched patterns at a few probe positions, over the measured peaks. @@ -848,6 +849,63 @@ def plot_ring_comparison(self, k_min: float = 0.1, k_max: float | None = None, * **kwargs, ) + def plot_calibration(self, k_min: float = 0.05, k_max: float | None = None, **kwargs): + """Calibrated peaks against the rings of every crystal in the map. + + One column per crystal: the radial histogram of every peak against + that crystal's rings, and every peak as azimuth against scattering + vector with the same rings. Run it after calibrating, whichever + phase the calibration was fit to: every candidate should line up, + and one that does not has the wrong lattice parameter for this + specimen, which matching cannot recover from. + + Parameters + ---------- + k_min : float, default=0.05 + Smallest scattering vector shown, 1/Angstroms. + k_max : float, optional + Largest scattering vector shown; defaults to the map's own k_max. + **kwargs + Further arguments of + :func:`~quantem.diffraction.calibration.plot_calibration`, e.g. + `k_broadening` and `marker_size`. + + Returns + ------- + tuple + ``(fig, axs)``, axs of shape (2, number of crystals). + """ + from quantem.diffraction import calibration + + return calibration.plot_calibration( + self.peaks, + [om.crystal for om in self.orientation_maps], + k_min=k_min, + k_max=k_max if k_max is not None else self._k_max_or_crystals(), + **kwargs, + ) + + def plot_bragg_rings(self, **kwargs): + """Bragg vector map of every peak with the rings of every crystal. + + Parameters + ---------- + **kwargs + Arguments of + :func:`~quantem.diffraction.calibration.plot_bragg_rings`, e.g. + `n_rings` and `q_max`. + + Returns + ------- + tuple + ``(fig, ax)``. + """ + from quantem.diffraction import calibration + + return calibration.plot_bragg_rings( + self.peaks, [om.crystal for om in self.orientation_maps], **kwargs + ) + def _k_max_or_crystals(self) -> float: """Scattering-vector limit, falling back to the crystals\' own values. diff --git a/tests/diffraction/test_two_phase_map.py b/tests/diffraction/test_two_phase_map.py index dad9deb9f..56b7cd42f 100644 --- a/tests/diffraction/test_two_phase_map.py +++ b/tests/diffraction/test_two_phase_map.py @@ -188,7 +188,9 @@ def test_dynamical_update_is_local_and_examples_are_spread(): picks = cm.example_positions(phase="Ti beta", num=3, min_distance=3) assert all(cm.phase_index[p] == 1 for p in picks) assert all( - (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 >= 9 for i, a in enumerate(picks) for b in picks[:i] + (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 >= 9 + for i, a in enumerate(picks) + for b in picks[:i] ) @@ -214,3 +216,32 @@ def test_loaded_crystal_map_shares_orientation_maps(tmp_path): # what the crystal map shows assert cm2.phases.orientation_maps[0] is cm2.orientation_maps[0] assert cm2.phases.orientation_maps[0].crystal is cm2[0].crystal + + +def test_plot_calibration_shows_every_crystal(): + import matplotlib + + matplotlib.use("Agg") + from quantem.diffraction.crystal_map import CrystalMap + + rng = np.random.default_rng(6) + ti_a = Crystal.from_ase( + bulk("Ti", "hcp", a=2.9505, c=4.6855), name="Ti alpha", verbose=False + ).calculate_structure_factors(k_max=1.5) + ti_b = Crystal.from_ase( + bulk("Ti", "bcc", a=3.26, cubic=True), name="Ti beta", verbose=False + ).calculate_structure_factors(k_max=1.5) + q = torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=torch.float64) + cells = [[_pattern(ti_a, q, rng) for _ in range(3)] for _ in range(2)] + peaks = Vector.from_data(cells, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t") + cm = CrystalMap.from_vectors(peaks, [ti_a, ti_b], energy_ev=200e3) + fig, axs = cm.plot_calibration() + # one column per crystal, the histogram above the azimuth panel + assert axs.shape == (2, 2) + assert [ax.get_title() for ax in axs[0]] == ["Ti alpha", "Ti beta"] + assert all(len(ax.collections) > 0 for ax in axs[1]) + matplotlib.pyplot.close(fig) + fig, ax = cm.plot_bragg_rings() + labels = [t.get_text() for t in ax.get_legend().get_texts()] + assert labels == ["Ti alpha rings", "Ti beta rings"] + matplotlib.pyplot.close(fig) From 727e62537be1744eba55fb91debbaff8cf358b1a Mon Sep 17 00:00:00 2001 From: Colin Ophus Date: Fri, 2 Oct 2026 08:32:10 -0700 Subject: [PATCH 21/36] Migrate ACOM code to the torch-backed Vector - Read Vector data with .numpy().astype(np.float64) instead of .array and flatten(), which now return tensors. This keeps the old writable float64 arrays, so consumers mixing peaks with float64 tensors also work for float32 Vectors, and torch.as_tensor no longer warns about read-only input. - Detected peaks stay float64 (dtype=torch.float64); Vectors derived from other Vectors (calibrated peaks, residual peaks, masked copies) keep the source dtype. Co-Authored-By: Claude Opus 5.5 --- src/quantem/core/utils/clustering.py | 10 ++++---- src/quantem/diffraction/bloch.py | 4 ++-- src/quantem/diffraction/bragg_vectors.py | 23 +++++++++++-------- src/quantem/diffraction/calibration.py | 21 +++++++++-------- src/quantem/diffraction/digital_dark_field.py | 6 ++--- src/quantem/diffraction/orientation.py | 22 ++++++++++-------- .../diffraction/orientation_visualization.py | 9 +++++--- src/quantem/diffraction/phase.py | 2 +- tests/core/test_clustering.py | 2 +- tests/diffraction/test_bloch.py | 10 ++++---- 10 files changed, 60 insertions(+), 49 deletions(-) diff --git a/src/quantem/core/utils/clustering.py b/src/quantem/core/utils/clustering.py index c2989fc09..686e13f7d 100644 --- a/src/quantem/core/utils/clustering.py +++ b/src/quantem/core/utils/clustering.py @@ -182,9 +182,9 @@ def cluster_vector( Copy of `vector` with the integer labels appended as a new field (-1 = noise). labels : np.ndarray - The flat label array, aligned with vector.flatten(). + The flat label array, aligned with vector.numpy().astype(np.float64). """ - flat = vector.select_fields(*fields).flatten().astype(float) + flat = vector.select_fields(*fields).numpy().astype(float) if field_scales is not None: flat = flat * np.asarray(field_scales, dtype=float)[None, :] dims = [flat] @@ -201,7 +201,7 @@ def cluster_vector( labeled = vector.copy() labeled.add_fields([label_field], units=["index"]) - full = labeled.flatten() + full = labeled.numpy().astype(np.float64) full[:, -1] = labels labeled.set_flattened(full) return labeled, labels @@ -214,7 +214,7 @@ def filter_rows(vector, mask): """ mask = np.asarray(mask, dtype=bool) counts = np.asarray(vector.row_counts(), dtype=int) - flat = vector.flatten() + flat = vector.numpy().astype(np.float64) starts = np.concatenate([[0], np.cumsum(counts)]) shape = vector.shape[:2] nested = [] @@ -229,7 +229,7 @@ def filter_rows(vector, mask): out = Vector.from_data( nested, fields=list(vector.fields), units=list(vector.units), - name=vector.name, + name=vector.name, dtype=vector.dtype, ) out.metadata.update(vector.metadata) return out diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py index 45f3314a9..408aa38d3 100644 --- a/src/quantem/diffraction/bloch.py +++ b/src/quantem/diffraction/bloch.py @@ -392,7 +392,7 @@ def refine_thickness( if progress_bar: iterator = tqdm(iterator, desc="dynamical refinement") for rx, ry in iterator: - data = peaks[rx, ry].array + data = peaks[rx, ry].numpy().astype(np.float64) if data.shape[0] < min_number_peaks: continue qxy = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) @@ -2591,7 +2591,7 @@ def store(rx, ry, f, sol): cost0_out[rx, ry, f] = sol["cost0"] def peaks_at(rx, ry): - data = peaks[rx, ry].array + data = peaks[rx, ry].numpy().astype(np.float64) if data.shape[0] < min_number_peaks: return None qxy = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) diff --git a/src/quantem/diffraction/bragg_vectors.py b/src/quantem/diffraction/bragg_vectors.py index 280408aa7..af3b08e4a 100644 --- a/src/quantem/diffraction/bragg_vectors.py +++ b/src/quantem/diffraction/bragg_vectors.py @@ -498,19 +498,22 @@ def detect_disks( raise ValueError("positions must contain at least one (row, col) to test on.") coords = [(int(r), int(c)) for r, c in positions] results = self._detect_positions(coords, detect_kwargs, batch_size, progressbar=False) - return Vector.from_data(results, fields=PEAK_FIELDS, name="bragg_peaks_test") + return Vector.from_data( + results, fields=PEAK_FIELDS, name="bragg_peaks_test", dtype=torch.float64 + ) scan_r, scan_c = int(self.dataset.shape[0]), int(self.dataset.shape[1]) coords = list(np.ndindex(scan_r, scan_c)) results = self._detect_positions( coords, detect_kwargs, batch_size, progressbar=progressbar ) - # Store every cell in a single bulk pass. Per-cell assignment - # (peaks[r, c] = arr) re-concatenates the entire backing buffer on every - # write -- O(N^2) over the scan, which is the stall at the end of - # detection. from_data stacks all cells with one _replace_cells call. + # Store every cell in one pass. Per-cell assignment (peaks[r, c] = arr) + # appends to the backing buffer on every write, while from_data joins all + # cells with a single concatenation. nested = [results[r * scan_c : (r + 1) * scan_c] for r in range(scan_r)] - peaks = Vector.from_data(nested, fields=PEAK_FIELDS, name="bragg_peaks") + peaks = Vector.from_data( + nested, fields=PEAK_FIELDS, name="bragg_peaks", dtype=torch.float64 + ) peaks.metadata.update(self._scan_calibration()) self.peaks = peaks @@ -578,7 +581,7 @@ def correct_peak_origins( peaks = bv.peaks # rowwise transform on the flat peak table: one shift per scan cell, # repeated per detected peak (cells and shifts share raster order) - flat = peaks.flatten() + flat = peaks.numpy().astype(np.float64) counts = np.asarray(peaks.row_counts(), dtype=int) shifts = np.repeat(origin_ref[None, :] - origins.reshape(-1, 2), counts, axis=0) flat[:, :2] += shifts @@ -610,7 +613,7 @@ def compute_bvm(self, sampling: float = 1.0) -> Dataset2d: if self.peaks is None: raise ValueError("Run detect_disks() before compute_bvm().") H, W = (int(self.dataset.shape[-2]), int(self.dataset.shape[-1])) - flat = self.peaks.select_fields("q_row", "q_col", "intensity").flatten() + flat = self.peaks.select_fields("q_row", "q_col", "intensity").numpy().astype(np.float64) bvm = np.zeros((H, W), dtype=float) if flat.shape[0] > 0: @@ -952,7 +955,7 @@ def fit_lattice( pass for r, c in coords: - cell = self.peaks[r, c].array + cell = self.peaks[r, c].numpy().astype(np.float64) if cell.shape[0] == 0: continue qpos = cell[:, [i_qr, i_qc]] @@ -1320,7 +1323,7 @@ def show_detection( image_title = getattr(image, "name", None) or "virtual image" image_arr = np.asarray(image.array if hasattr(image, "array") else image) dps = [np.asarray(self.dataset.array[r, c], dtype=float) for r, c in positions] - peaks = [sub[i].array for i in range(len(positions))] + peaks = [sub[i].numpy().astype(np.float64) for i in range(len(positions))] fig, ax = plot_detection( image_arr, dps, diff --git a/src/quantem/diffraction/calibration.py b/src/quantem/diffraction/calibration.py index f2271ca04..a67517eaa 100644 --- a/src/quantem/diffraction/calibration.py +++ b/src/quantem/diffraction/calibration.py @@ -26,7 +26,7 @@ def _measure_raw_origins(bragg_vectors, search_radius: float, center=None) -> np meas = np.full((scan_r, scan_c, 2), np.nan) for r in range(scan_r): for c in range(scan_c): - arr = peaks[r, c].array + arr = peaks[r, c].numpy().astype(np.float64) if arr.shape[0] == 0: continue d = np.hypot(arr[:, 0] - c0[0], arr[:, 1] - c0[1]) @@ -218,7 +218,7 @@ def peaks_to_calibrated( Fields (qx, qy, intensity) in 1/Angstroms. """ scan_r, scan_c = peaks_px.shape[0], peaks_px.shape[1] - flat = peaks_px.select_fields("q_row", "q_col", "intensity").flatten() + flat = peaks_px.select_fields("q_row", "q_col", "intensity").numpy().astype(np.float64) row_counts = np.asarray(peaks_px.row_counts(), dtype=int) qrc = flat[:, :2] * pixel_size_inv_A if ellipse is not None: @@ -237,6 +237,7 @@ def peaks_to_calibrated( fields=["qx", "qy", "intensity"], units=["A^-1", "A^-1", "counts"], name=name, + dtype=peaks_px.dtype, ) # carry the scan calibration through, so maps keep their scale bar, and # record the detector-to-scan rotation so pattern-overlay plots can put @@ -251,7 +252,7 @@ def peaks_to_calibrated( def scale_peaks(peaks, scale: float): """Return a copy of a (qx, qy, intensity) Vector with q scaled.""" out = peaks.copy() - flat = out.flatten() + flat = out.numpy().astype(np.float64) flat[:, :2] *= scale out.set_flattened(flat) return out @@ -274,7 +275,7 @@ def radial_histogram( hist : np.ndarray Weighted counts, with linear interpolation between adjacent bins. """ - flat = peaks.select_fields("qx", "qy", "intensity").flatten() + flat = peaks.select_fields("qx", "qy", "intensity").numpy().astype(np.float64) qr = np.hypot(flat[:, 0], flat[:, 1]) weight = flat[:, 2] ** bragg_intensity_power * qr**bragg_k_power @@ -636,7 +637,7 @@ def calibrate_ellipse( """ from scipy.optimize import minimize - flat = peaks.select_fields("qx", "qy", "intensity").flatten() + flat = peaks.select_fields("qx", "qy", "intensity").numpy().astype(np.float64) q = flat[:, :2] log_lo, log_hi = np.log(k_min), np.log(k_max) bin_w = (log_hi - log_lo) / n_bins @@ -694,7 +695,7 @@ def cost(e): def apply_ellipse(peaks, ellipse): """Return a copy of (qx, qy, intensity) peaks with the ellipse applied.""" out = peaks.copy() - flat = out.flatten() + flat = out.numpy().astype(np.float64) e11, e12 = float(ellipse[0]), float(ellipse[1]) A = np.array([[1 + e11, e12], [e12, 1 - e11]]) flat[:, :2] = flat[:, :2] @ A.T @@ -841,7 +842,7 @@ def _ring_profile_scores( truth. Dividing by the total weight, counted once and independent of the scale, makes a lost peak a loss. """ - flat = peaks.select_fields("qx", "qy", "intensity").flatten() + flat = peaks.select_fields("qx", "qy", "intensity").numpy().astype(np.float64) qr = np.hypot(flat[:, 0], flat[:, 1]) weight = flat[:, 2] * qr**bragg_k_power k_step = 0.002 @@ -1119,7 +1120,7 @@ def _calibration_panels( ax_hist.set_xlabel("") # azimuth against scattering vector: a pixel size error shifts every # ring, the elliptic distortion is a cos(2 phi) wobble of each one - flat = peaks.select_fields("qx", "qy", "intensity").flatten() + flat = peaks.select_fields("qx", "qy", "intensity").numpy().astype(np.float64) r = np.hypot(flat[:, 0], flat[:, 1]) phi = np.degrees(np.arctan2(flat[:, 1], flat[:, 0])) sel = (r > k_min) & (r < k_max) @@ -1347,7 +1348,7 @@ def plot_ring_comparison( def transform_peaks(peaks, M: np.ndarray): """Return a copy of a (qx, qy, intensity) Vector with q mapped by M (2x2).""" out = peaks.copy() - flat = out.flatten() + flat = out.numpy().astype(np.float64) flat[:, :2] = flat[:, :2] @ np.asarray(M, dtype=float).T out.set_flattened(flat) return out @@ -1450,7 +1451,7 @@ def plot_bragg_rings( import matplotlib.pyplot as plt xtls = crystals if isinstance(crystals, (list, tuple)) else [crystals] - flat = peaks.select_fields("qx", "qy", "intensity").flatten() + flat = peaks.select_fields("qx", "qy", "intensity").numpy().astype(np.float64) if q_max is None: q_max = float(np.hypot(flat[:, 0], flat[:, 1]).max()) * 1.02 H, xe, ye = np.histogram2d( diff --git a/src/quantem/diffraction/digital_dark_field.py b/src/quantem/diffraction/digital_dark_field.py index a09746cb5..f5d0a38bf 100644 --- a/src/quantem/diffraction/digital_dark_field.py +++ b/src/quantem/diffraction/digital_dark_field.py @@ -53,7 +53,7 @@ def cluster_coms( (K,) number of peaks per cluster. """ fields = labeled.fields - flat = labeled.flatten() + flat = labeled.numpy().astype(np.float64) labels = flat[:, fields.index(label_field)].astype(int) w = flat[:, fields.index(intensity_field)].clip(min=0) if weighted else None rc = _scan_cells(labeled).astype(float) @@ -87,7 +87,7 @@ def ddf_images( (len(cluster_ids), scan_row, scan_col) images. """ fields = labeled.fields - flat = labeled.flatten() + flat = labeled.numpy().astype(np.float64) labels = flat[:, fields.index(label_field)].astype(int) inten = flat[:, fields.index(intensity_field)].clip(min=0) rc = _scan_cells(labeled) @@ -172,7 +172,7 @@ def plot_cluster_scatter( import matplotlib.pyplot as plt fields = labeled.fields - flat = labeled.flatten() + flat = labeled.numpy().astype(np.float64) labels = flat[:, fields.index(label_field)].astype(int) qx = flat[:, fields.index(q_fields[0])] qy = flat[:, fields.index(q_fields[1])] diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index 8e6c45e15..d49e05d02 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -517,7 +517,7 @@ def build_plan( # the square aperture in the calibrated (qx, qy) frame. rot_deg = float(self.peaks.metadata.get("rotation_ccw_deg", 0.0) or 0.0) if isinstance(detector_q_max, str) and detector_q_max == "auto": - flat = self.peaks.select_fields("qx", "qy", "intensity").flatten() + flat = self.peaks.select_fields("qx", "qy", "intensity").numpy().astype(np.float64) if flat.shape[0] == 0: detector_q_max = None else: @@ -957,7 +957,8 @@ def match_orientations( valid_rc = [ (rx, ry) for rx, ry in np.ndindex(R, C) - if wanted[rx, ry] and peaks[rx, ry].array.shape[0] >= min_number_peaks + if wanted[rx, ry] + and peaks[rx, ry].numpy().astype(np.float64).shape[0] >= min_number_peaks ] if not valid_rc: raise RuntimeError( @@ -975,7 +976,9 @@ def match_orientations( gamma_grid = self.gamma for batch in batches: im_stack = ( - self._polar_images([peaks[rx, ry].array for rx, ry in batch], ix) + self._polar_images( + [peaks[rx, ry].numpy().astype(np.float64) for rx, ry in batch], ix + ) .to(dtype) .to(device) ) @@ -1603,7 +1606,7 @@ def refine_single(q, q_exp, w_exp): return q, score def get_exp(rx, ry): - data = peaks[rx, ry].array + data = peaks[rx, ry].numpy().astype(np.float64) if data.shape[0] < min_pairs: return None, None q_exp = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) @@ -1673,7 +1676,7 @@ def get_exp(rx, ry): for rx, ry in act_list: if bar is not None: bar.update(1) - data = peaks[rx, ry].array + data = peaks[rx, ry].numpy().astype(np.float64) meas = self._measured_term(data, ix) for m in range(M): if self.corr[rx, ry, m] <= 0: @@ -1747,7 +1750,7 @@ def get_exp(rx, ry): q_exp, w_exp = get_exp(rx, ry) if q_exp is None: continue - data = peaks[rx, ry].array + data = peaks[rx, ry].numpy().astype(np.float64) cur_q = self.quats[rx, ry, 0].clone() cur_s = float(cscore[rx, ry]) cands = [cur_q] @@ -1948,7 +1951,7 @@ def envelope(S, g_rows): ) # flatten measured peaks once, padded per position - cells = [peaks[r, c].array for r, c in np.ndindex(R, C)] + cells = [peaks[r, c].numpy().astype(np.float64) for r, c in np.ndindex(R, C)] counts = np.array([c.shape[0] for c in cells]) Pmax = max(1, counts.max()) N = R * C @@ -2154,7 +2157,7 @@ def match_residual( ) cells = [] for rx, ry in np.ndindex(R, C): - data = peaks[rx, ry].array + data = peaks[rx, ry].numpy().astype(np.float64) if data.shape[0] < min_number_peaks or other.corr[rx, ry, 0] <= min_corr_other: cells.append(np.zeros((0, 3))) continue @@ -2173,6 +2176,7 @@ def match_residual( fields=["qx", "qy", "intensity"], units=["A^-1", "A^-1", "counts"], name="residual_peaks", + dtype=peaks.dtype, ) om_res = OrientationMap.from_vectors(residual, self.crystal, self.energy_ev) @@ -2353,7 +2357,7 @@ def calculate_strain( continue if self.corr[rx, ry, match] <= 0: continue - data = peaks[rx, ry].array + data = peaks[rx, ry].numpy().astype(np.float64) if data.shape[0] < min_pairs: continue q_exp = torch.as_tensor(data[:, ix[:2]], dtype=torch.float64) diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py index 99e6df4b4..2842db95c 100644 --- a/src/quantem/diffraction/orientation_visualization.py +++ b/src/quantem/diffraction/orientation_visualization.py @@ -627,7 +627,10 @@ def plot_pattern_matches( if not 0.0 < q_max_quantile <= 1.0: raise ValueError(f"q_max_quantile must be in (0, 1], got {q_max_quantile}") q_all = [ - np.hypot(peaks[rx, ry].array[:, ix[0]], peaks[rx, ry].array[:, ix[1]]) + np.hypot( + peaks[rx, ry].numpy().astype(np.float64)[:, ix[0]], + peaks[rx, ry].numpy().astype(np.float64)[:, ix[1]], + ) for rx, ry in positions ] # the direct beam is at zero and every position has one, so it is @@ -642,7 +645,7 @@ def plot_pattern_matches( # dense pattern does not turn into overlapping blobs spacings = [] for rx, ry in positions: - xy = peaks[rx, ry].array[:, [ix[0], ix[1]]] + xy = peaks[rx, ry].numpy().astype(np.float64)[:, [ix[0], ix[1]]] xy = xy[(np.abs(xy) <= q_lim).all(axis=1)] if xy.shape[0] > 2: d = np.hypot(xy[:, None, 0] - xy[None, :, 0], xy[:, None, 1] - xy[None, :, 1]) @@ -655,7 +658,7 @@ def plot_pattern_matches( measured_scale = 1.5 * marker_scale for pi, (rx, ry) in enumerate(positions): - data = peaks[rx, ry].array.copy() + data = peaks[rx, ry].numpy().astype(np.float64) rc = data[:, [ix[0], ix[1]]] @ rot_back.T data[:, ix[0]] = rc[:, 0] data[:, ix[1]] = rc[:, 1] diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py index 08acb245f..e8b904868 100644 --- a/src/quantem/diffraction/phase.py +++ b/src/quantem/diffraction/phase.py @@ -265,7 +265,7 @@ def fit( if progress_bar: iterator = tqdm(iterator, desc="phase mapping") for rx, ry in iterator: - data = peaks[rx, ry].array + data = peaks[rx, ry].numpy().astype(np.float64) if data.shape[0] < min_number_peaks: continue # null hypothesis: no diffracted signal, so no phase to decide. diff --git a/tests/core/test_clustering.py b/tests/core/test_clustering.py index 318ceced8..37cd47544 100644 --- a/tests/core/test_clustering.py +++ b/tests/core/test_clustering.py @@ -49,7 +49,7 @@ def test_cluster_vector_and_filter(): ) assert "cluster" in labeled.fields assert labels.max() == 0 # exactly one cluster found - got = labeled[1, 2].array + got = labeled[1, 2].numpy() assert (got[:, -1] == 0).sum() >= 28 kept = filter_rows(vec, labels == 0) diff --git a/tests/diffraction/test_bloch.py b/tests/diffraction/test_bloch.py index 7be8e129f..c30724c6b 100644 --- a/tests/diffraction/test_bloch.py +++ b/tests/diffraction/test_bloch.py @@ -443,7 +443,7 @@ def test_refine_dynamical_recovery(): progress_bar=False, ) # positions with too few beams cannot constrain a 2x2 deformation - valid = np.array([peaks[0, i].array.shape[0] >= 6 for i in range(N)]) + valid = np.array([peaks[0, i].numpy().shape[0] >= 6 for i in range(N)]) assert valid.sum() >= 4 err = misorientation_angle_deg(q_expect, om.quats[0, :, 0], xtl.sym_quats).numpy()[valid] t_err = np.abs(res["thickness"][0].numpy() - t_true.numpy())[valid] @@ -534,7 +534,7 @@ def test_image_refinement_round_trip(): k_max=1.0, progress_bar=False, ) - valid = [i for i in range(N) if peaks[0, i].array.shape[0] >= 6] + valid = [i for i in range(N) if peaks[0, i].numpy().shape[0] >= 6] assert len(valid) >= 2 shape_fit = bloch.fit_disk_shape( dataset, @@ -707,7 +707,7 @@ def test_refine_dynamical_reported_cost_reproducible(): progress_bar=False, ) for i in range(3): - if not torch.isfinite(res["cost"][0, i, 0]) or peaks[0, i].array.shape[0] < 5: + if not torch.isfinite(res["cost"][0, i, 0]) or peaks[0, i].numpy().shape[0] < 5: continue q = res["quats"][0, i, 0] d3 = torch.eye(3, dtype=torch.float64) @@ -727,7 +727,7 @@ def test_refine_dynamical_reported_cost_reproducible(): deform=d3, beams=beams, ) - data = peaks[0, i].array + data = peaks[0, i].numpy().astype(np.float64) qxy = torch.as_tensor(data[:, :2]) im = torch.as_tensor(data[:, 2]).clamp_min(0) ** 0.25 cost, _, _, _ = bloch._dynamical_cost(inten[:, :, 1:], g_xy[1:], qxy, im, 0.05, 0.25, 0.02) @@ -840,7 +840,7 @@ def test_refine_dynamical_with_precession_and_convergence(): progress_bar=False, ) assert res["metadata"]["precession_deg"] == 0.4 and res["metadata"]["semiconv_mrad"] == 1.5 - valid = np.array([peaks[0, i].array.shape[0] >= 6 for i in range(N)]) + valid = np.array([peaks[0, i].numpy().shape[0] >= 6 for i in range(N)]) assert valid.sum() >= 2 err = misorientation_angle_deg(q_true, om.quats[0, :, 0], xtl.sym_quats).numpy()[valid] t_err = np.abs(res["thickness"][0].numpy() - t_true.numpy())[valid] From 6ccbbcf1f7a355c1d12122ebaf42179f8b856347 Mon Sep 17 00:00:00 2001 From: Colin Ophus Date: Fri, 2 Oct 2026 08:39:09 -0700 Subject: [PATCH 22/36] Store detected peaks in float32 float32 holds peak positions to ~3e-5 px, well below detection precision, and every read casts to float64 before computing, so the float64 storage only doubled memory and file size. Co-Authored-By: Claude Opus 5.5 --- src/quantem/diffraction/bragg_vectors.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/quantem/diffraction/bragg_vectors.py b/src/quantem/diffraction/bragg_vectors.py index af3b08e4a..fb4f2fe23 100644 --- a/src/quantem/diffraction/bragg_vectors.py +++ b/src/quantem/diffraction/bragg_vectors.py @@ -498,9 +498,7 @@ def detect_disks( raise ValueError("positions must contain at least one (row, col) to test on.") coords = [(int(r), int(c)) for r, c in positions] results = self._detect_positions(coords, detect_kwargs, batch_size, progressbar=False) - return Vector.from_data( - results, fields=PEAK_FIELDS, name="bragg_peaks_test", dtype=torch.float64 - ) + return Vector.from_data(results, fields=PEAK_FIELDS, name="bragg_peaks_test") scan_r, scan_c = int(self.dataset.shape[0]), int(self.dataset.shape[1]) coords = list(np.ndindex(scan_r, scan_c)) @@ -511,9 +509,7 @@ def detect_disks( # appends to the backing buffer on every write, while from_data joins all # cells with a single concatenation. nested = [results[r * scan_c : (r + 1) * scan_c] for r in range(scan_r)] - peaks = Vector.from_data( - nested, fields=PEAK_FIELDS, name="bragg_peaks", dtype=torch.float64 - ) + peaks = Vector.from_data(nested, fields=PEAK_FIELDS, name="bragg_peaks") peaks.metadata.update(self._scan_calibration()) self.peaks = peaks From 00ae26e50fedadf0b570e41e9762dc9ac6b4328e Mon Sep 17 00:00:00 2001 From: cophus Date: Fri, 2 Oct 2026 10:42:12 -0700 Subject: [PATCH 23/36] adding foil normals and other fixes --- src/quantem/diffraction/bragg_vectors.py | 3 +- src/quantem/diffraction/calibration.py | 77 ++++++++++++- src/quantem/diffraction/crystal.py | 52 ++++++++- src/quantem/diffraction/crystal_map.py | 66 +++++++++-- src/quantem/diffraction/illumination.py | 78 +++++++++++-- src/quantem/diffraction/orientation.py | 140 ++++++++++++++++++----- src/quantem/diffraction/phase.py | 10 +- tests/diffraction/test_two_phase_map.py | 66 +++++++++++ 8 files changed, 435 insertions(+), 57 deletions(-) diff --git a/src/quantem/diffraction/bragg_vectors.py b/src/quantem/diffraction/bragg_vectors.py index fb4f2fe23..ce3f59bba 100644 --- a/src/quantem/diffraction/bragg_vectors.py +++ b/src/quantem/diffraction/bragg_vectors.py @@ -1505,7 +1505,8 @@ def calibrate(self, crystal, pixel_size_guess: float, **kwargs): **kwargs Further arguments of :func:`~quantem.diffraction.calibration.calibrate`, e.g. - `fit_ellipse`, `k_min`, `k_max`, `marker_size` and `figsize`. + `zone_axis` to fit only the rings of one zone, `fit_ellipse`, + `scale_search`, `k_min`, `k_max`, `marker_size` and `figsize`. Returns ------- diff --git a/src/quantem/diffraction/calibration.py b/src/quantem/diffraction/calibration.py index a67517eaa..f007c5916 100644 --- a/src/quantem/diffraction/calibration.py +++ b/src/quantem/diffraction/calibration.py @@ -10,6 +10,7 @@ import warnings import numpy as np +import torch from quantem.core.datastructures.vector import Vector from quantem.core.io.serialize import AutoSerialize @@ -703,6 +704,58 @@ def apply_ellipse(peaks, ellipse): return out +def zone_reflections(crystal: Crystal, zone_axis) -> Crystal: + """The crystal restricted to the reflections of one zone. + + A specimen that sits near one zone axis everywhere -- a flake lying flat, + a textured film -- only ever shows the reflections of that zone, so its + radial histogram holds those rings and no others. Calibrating or + plotting against every ring of the crystal then compares the peaks with + rings that cannot appear. This keeps the reflections hkl with + h u + k v + l w = 0 for the zone axis [uvw] and drops the rest. + + Parameters + ---------- + crystal : Crystal + With structure factors calculated. + zone_axis : sequence of int + Zone axis direction in the crystal's own cell, [uvw], or [UVTW] for a + hexagonal or trigonal cell, e.g. (0, 0, 0, 1) for the basal plane. + + Returns + ------- + Crystal + A shallow copy whose reflection list holds only that zone, named + after it; the original is unchanged. + """ + import copy + + if crystal.g_vec is None: + raise RuntimeError(f"{crystal.name}: run calculate_structure_factors() first") + z = np.asarray(zone_axis, dtype=float).ravel() + if z.size == 4: + U, V, T, W = z + uvw = np.array([U - T, V - T, W]) + label = "[" + "".join(str(int(round(v))) for v in z) + "]" + elif z.size == 3: + uvw = z + label = "[" + "".join(str(int(round(v))) for v in z) + "]" + else: + raise ValueError(f"zone_axis must have 3 or 4 indices, got {zone_axis}") + keep = torch.as_tensor(np.abs(crystal.hkl.numpy() @ uvw) < 1e-6) + out = copy.copy(crystal) + for name in ("hkl", "g_vec", "g_len", "struct_factors", "struct_factors_int"): + setattr(out, name, getattr(crystal, name)[keep]) + out.name = f"{crystal.name} {label}" + return out + + +def _restrict(crystals, zone_axis): + """Crystal list, each restricted to `zone_axis` when one is given.""" + xtls = [crystals] if isinstance(crystals, Crystal) else list(crystals) + return xtls if zone_axis is None else [zone_reflections(x, zone_axis) for x in xtls] + + def _hkl_label(hkl: np.ndarray, hexagonal: bool) -> str: """Compact (hkl) / (hkil) plane label with unicode overbars.""" @@ -896,6 +949,7 @@ def calibrate( plot: bool = True, figsize: tuple[float, float] = (13.0, 6.4), marker_size: float = 8.0, + zone_axis=None, returnfig: bool = False, ): """Measure the reciprocal pixel size and the elliptic distortion. @@ -937,6 +991,10 @@ def calibrate( Area of the brightest peak in the azimuth panels, where every peak is drawn with area proportional to its intensity. Raise it to bring out weak spots, lower it when strong ones hide the reference lines. + zone_axis : sequence of int, optional + Fit only the rings of this zone, [uvw] or [UVTW] (see + :func:`zone_reflections`). For a specimen near one zone axis + everywhere, whose peaks hold no other rings. Returns ------- @@ -953,6 +1011,7 @@ def calibrate( for xtl in crystals: if xtl.g_vec is None: raise RuntimeError(f"{xtl.name}: run calculate_structure_factors() first") + crystals = _restrict(crystals, zone_axis) peaks_0 = peaks_to_calibrated(peaks_px, pixel_size_guess) # coarse: broad rings so the score has a single maximum over a wide range @@ -1154,6 +1213,7 @@ def plot_calibration( bragg_k_power: float = 2.0, marker_size: float = 8.0, n_rings: int = 12, + zone_axis=None, axsize: tuple[float, float] = (6.0, 6.4), figax=None, ): @@ -1183,6 +1243,9 @@ def plot_calibration( Rings drawn in the azimuth panels and labeled in the histograms, strongest first; a large cell would otherwise fill both with its weak superstructure rings. + zone_axis : sequence of int, optional + Show only the rings of this zone, [uvw] or [UVTW] (see + :func:`zone_reflections`). axsize : tuple, default=(6.0, 6.4) Size of one column. figax : (fig, axs) | None @@ -1195,7 +1258,7 @@ def plot_calibration( """ import matplotlib.pyplot as plt - xtls = [crystals] if isinstance(crystals, Crystal) else list(crystals) + xtls = _restrict(crystals, zone_axis) if figax is None: fig, axs = plt.subplots( 2, @@ -1241,6 +1304,7 @@ def plot_ring_comparison( label_hkl: bool = True, label_min_intensity: float = 0.05, n_labels: int = 12, + zone_axis=None, figax=None, ): """Measured radial peak histogram against crystal ring positions. @@ -1267,10 +1331,13 @@ def plot_ring_comparison( n_labels : int, default=12 Label at most this many rings, strongest first, which keeps a large cell with many rings readable. + zone_axis : sequence of int, optional + Show only the rings of this zone, [uvw] or [UVTW] (see + :func:`zone_reflections`). """ import matplotlib.pyplot as plt - xtls = crystals if isinstance(crystals, (list, tuple)) else [crystals] + xtls = _restrict(crystals, zone_axis) k, hist = radial_histogram(peaks, k_min=k_min, k_max=k_max) n = len(xtls) @@ -1428,6 +1495,7 @@ def plot_bragg_rings( q_max: float | None = None, bins: int = 400, power: float = 0.25, + zone_axis=None, figax=None, ): """2D histogram of all Bragg peaks with crystal rings overlaid. @@ -1447,10 +1515,13 @@ def plot_bragg_rings( (solid, then dashed line styles). n_rings : int, default=8 Number of rings per crystal, strongest first. + zone_axis : sequence of int, optional + Draw only the rings of this zone, [uvw] or [UVTW] (see + :func:`zone_reflections`). """ import matplotlib.pyplot as plt - xtls = crystals if isinstance(crystals, (list, tuple)) else [crystals] + xtls = _restrict(crystals, zone_axis) flat = peaks.select_fields("qx", "qy", "intensity").numpy().astype(np.float64) if q_max is None: q_max = float(np.hypot(flat[:, 0], flat[:, 1]).max()) * 1.02 diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py index dd4c443b8..5e4913dec 100644 --- a/src/quantem/diffraction/crystal.py +++ b/src/quantem/diffraction/crystal.py @@ -1064,6 +1064,23 @@ def calculate_dynamical_structure_factors( self.dyn_k_max = float(k_max) return self + def direction_vector(self, direction) -> torch.Tensor: + """Unit Cartesian vector (3,) of a lattice direction in the crystal frame. + + Parameters + ---------- + direction : sequence of float + [uvw] in this cell, or [UVTW] for a hexagonal or trigonal cell. + """ + d = np.asarray(direction, dtype=float).ravel() + if d.size == 4: + U, V, T, W = d + d = np.array([U - T, V - T, W]) + elif d.size != 3: + raise ValueError(f"a direction has 3 or 4 indices, got {direction}") + v = d @ self.lat_real.numpy() + return torch.as_tensor(v / np.linalg.norm(v), dtype=torch.float64) + def generate_pattern( self, orientation: torch.Tensor, @@ -1075,6 +1092,7 @@ def generate_pattern( semiconv_mrad: float = 0.0, excitation_model: str = "gaussian", thickness_A: float | None = None, + foil_normal=None, ) -> dict[str, torch.Tensor]: """Kinematical diffraction pattern for one orientation. @@ -1111,25 +1129,42 @@ def generate_pattern( of the Bloch wave calculation for thin crystals. thickness_A : float | None Thickness for the slab model (Angstroms). + foil_normal : sequence of float, optional + Plate normal of the specimen as a direction in this crystal, + [uvw] or [UVTW], e.g. (0, 0, 0, 1) for a 2D material lying in its + basal plane. Every reflection is then a rod along that normal: it + is excited by its distance along the rod to the Ewald sphere, and + its spot sits where the rod meets the sphere rather than at the + projection of g. For a tilted flake, whose rods are long, the + spots shift by up to s_g tan(tilt). None (default) takes the + normal along the beam, the usual geometry. Returns ------- dict with 'qx', 'qy', 'intensity', 'hkl', 's_g' (the central - excitation error), 'a' and 'b' (ring and disk sweep amplitudes). + excitation error, along the rod when `foil_normal` is given), 'a' + and 'b' (ring and disk sweep amplitudes). """ if self.g_vec is None: raise RuntimeError("Run calculate_structure_factors first.") from quantem.diffraction.illumination import ( averaged_gaussian_intensity_envelope, excitation_coefficients, + relrod_factor, slab_envelope, ) g = qrotate(orientation, self.g_vec) + n_lab = None + if foil_normal is not None: + n_lab = qrotate(orientation, self.direction_vector(foil_normal)[None])[0] if excitation_model == "slab": if thickness_A is None: raise ValueError("the slab excitation model needs thickness_A") c, a, b = excitation_coefficients(g, energy_ev, precession_deg, semiconv_mrad) + if n_lab is not None: + f = relrod_factor(g.numpy(), n_lab.numpy(), energy_ev, precession_deg) + c, a, b = c * f, a * np.abs(f), b * np.abs(f) # the sinc^2 tails are algebraic: keep everything whose main # lobe (width 1/z) plus illumination sweep is within the tolerance width = tol_excitation_mult / float(thickness_A) @@ -1152,7 +1187,12 @@ def generate_pattern( ) else: env, c, a, b = averaged_gaussian_intensity_envelope( - g, energy_ev, sigma_excitation, precession_deg, semiconv_mrad + g, + energy_ev, + sigma_excitation, + precession_deg, + semiconv_mrad, + foil_normal_lab=None if n_lab is None else n_lab.numpy(), ) c_t = torch.as_tensor(c, dtype=torch.float64) a_t = torch.as_tensor(a, dtype=torch.float64) @@ -1165,9 +1205,13 @@ def generate_pattern( intensity = ( self.struct_factors_int[keep] * torch.as_tensor(env, dtype=torch.float64)[keep] ) + qxy = g[keep, :2] + if n_lab is not None: + # the spot is where the rod meets the sphere: g + t n, t = -c + qxy = qxy - c_t[keep, None] * n_lab[None, :2] return { - "qx": g[keep, 0], - "qy": g[keep, 1], + "qx": qxy[:, 0], + "qy": qxy[:, 1], "intensity": intensity, "hkl": self.hkl[keep], "s_g": c_t[keep], diff --git a/src/quantem/diffraction/crystal_map.py b/src/quantem/diffraction/crystal_map.py index 6667b78f4..3c8bcd068 100644 --- a/src/quantem/diffraction/crystal_map.py +++ b/src/quantem/diffraction/crystal_map.py @@ -138,6 +138,7 @@ def from_vectors( precession_deg: float = 0.0, semiconv_mrad: float = 0.0, k_max: float | None = None, + foil_normal=None, ) -> "CrystalMap": """Build one OrientationMap per crystal from a shared peak table. @@ -157,6 +158,13 @@ def from_vectors( beyond it cannot be paired. None keeps the structure factors the crystals already have, which must then share one k_max, since two phases simulated to different ranges are not compared fairly. + foil_normal : sequence or dict, optional + Plate normal of a 2D material or thin flake, as a direction in each + crystal, [uvw] or [UVTW]; a dict keyed by crystal name sets it per + crystal. Reflections are then rods along it, which places the + spots of a tilted flake where the rods meet the Ewald sphere (see + :meth:`OrientationMap.from_vectors`). None (default) is the usual + geometry. Raises ------ @@ -177,6 +185,9 @@ def from_vectors( energy_ev=energy_ev, precession_deg=precession_deg, semiconv_mrad=semiconv_mrad, + foil_normal=( + foil_normal.get(xtl.name) if isinstance(foil_normal, dict) else foil_normal + ), ) for xtl in xtls ] @@ -291,7 +302,12 @@ def match_orientations(self, overrides: dict | None = None, **kwargs) -> "Crysta """ return self._fanout("match_orientations", overrides, **kwargs) - def refine_orientations(self, overrides: dict | None = None, **kwargs) -> "CrystalMap": + def refine_orientations( + self, + overrides: dict | None = None, + competitive_margin: float | None = 0.1, + **kwargs, + ) -> "CrystalMap": """Refine every crystal off the library grid. Least squares on the paired peak positions removes the quantization @@ -300,20 +316,42 @@ def refine_orientations(self, overrides: dict | None = None, **kwargs) -> "Cryst a neighbour are then retried from the candidates around them, and ties go to the orientation the neighbours share. + With several crystals, each is refined only where it is in the + running: where its library correlation is within + `competitive_margin` of the best crystal's. Elsewhere its + orientations are noise -- the crystal is not there -- and refining + them, and retrying them against their equally random neighbours, is + most of the work in a two-phase map for no change in the phase + decision. + Parameters ---------- overrides : dict, optional Per-crystal keyword overrides, keyed by crystal name. + competitive_margin : float or None, default=0.1 + Correlation margin behind the best crystal within which a crystal + is still refined. None refines every crystal everywhere. **kwargs Passed to :meth:`OrientationMap.refine_orientations` for every - crystal, commonly `num_iterations` and `zone_search_deg`. + crystal, commonly `num_iterations` and `zone_search_deg`. An + explicit `positions` is used as given. Returns ------- CrystalMap Self, so stages chain. """ - return self._fanout("refine_orientations", overrides, **kwargs) + oms = self.orientation_maps + if competitive_margin is None or len(oms) < 2 or "positions" in kwargs: + return self._fanout("refine_orientations", overrides, **kwargs) + corr = np.stack([om.corr[..., 0].numpy() for om in oms]) + best = corr.max(axis=0) + for i, om in enumerate(oms): + kw = dict(kwargs) + kw.update((overrides or {}).get(om.crystal.name, {})) + kw.setdefault("positions", corr[i] >= best - competitive_margin) + om.refine_orientations(**kw) + return self def _fanout(self, method: str, overrides: dict | None, **kwargs) -> "CrystalMap": """Call ``method`` on every OrientationMap, with per-crystal overrides. @@ -477,7 +515,8 @@ def example_positions( The clearest examples of a crystal are where it won by the largest margin (the phase reliability); the ambiguous ones are where the two - best crystals scored closest. Only positions that diffract are + best crystals scored closest. With a single crystal there is no + margin, and positions are ranked by its correlation instead. Only positions that diffract are considered, and each pick is at least `min_distance` from the others, so the examples come from different parts of the scan. @@ -503,6 +542,13 @@ def example_positions( pm = self._require_fit("example_positions()") ph = self.phase_index rel = np.asarray(pm.reliability, dtype=float) + if not np.isfinite(rel[ph >= 0]).any(): + # one crystal: no runner-up to compare against, so rank by how + # well the crystal itself matches + corr = np.stack([om.corr[..., 0].numpy() for om in self.orientation_maps]) + rel = np.where( + ph >= 0, np.take_along_axis(corr, np.clip(ph, 0, None)[None], 0)[0], np.nan + ) ok = (ph >= 0) & np.isfinite(rel) & (self.signal_confidence() >= min_signal) if phase is not None: ok &= ph == self._phase_indices(phase)[0] @@ -566,8 +612,8 @@ def signal_confidence(self, signal_range="auto", gamma: float = 1.0) -> np.ndarr Parameters ---------- signal_range : tuple or "auto", default="auto" - Diffracted intensity mapped to 0 ... 1. "auto" spans zero to the - 95th percentile over the indexed positions. + Diffracted intensity mapped to 0 ... 1. "auto" spans zero to half + the median over the indexed positions. gamma : float, default=1.0 Exponent applied to the result. The default is the raw confidence, which is what a threshold should be taken on. @@ -868,7 +914,8 @@ def plot_calibration(self, k_min: float = 0.05, k_max: float | None = None, **kw **kwargs Further arguments of :func:`~quantem.diffraction.calibration.plot_calibration`, e.g. - `k_broadening` and `marker_size`. + `zone_axis` to show only the rings of one zone, `k_broadening` + and `marker_size`. Returns ------- @@ -893,7 +940,7 @@ def plot_bragg_rings(self, **kwargs): **kwargs Arguments of :func:`~quantem.diffraction.calibration.plot_bragg_rings`, e.g. - `n_rings` and `q_max`. + `n_rings`, `q_max` and `zone_axis`. Returns ------- @@ -975,6 +1022,9 @@ def plot_correlation(self, **kwargs): for row in (corr, rel) ] kwargs.setdefault("cbar", True) + # panels shaped like the scan, so a wide map leaves no gap between rows + R, C = self.shape + kwargs.setdefault("axsize", (4.5, 4.5 * R / C)) kwargs.setdefault( "title", [ diff --git a/src/quantem/diffraction/illumination.py b/src/quantem/diffraction/illumination.py index 2231d10ab..35c24e6e9 100644 --- a/src/quantem/diffraction/illumination.py +++ b/src/quantem/diffraction/illumination.py @@ -101,12 +101,70 @@ def excitation_coefficients( return c, a, b +def relrod_factor(g_lab, n_lab, energy_ev: float, precession_deg: float = 0.0): + """How far along a relrod the Ewald sphere is, per unit excitation error. + + A plate-shaped crystal spreads every reciprocal lattice point into a rod + along the plate normal n, a long one for a 2D material. The sphere meets + the rod through g at g + t n, with, to first order, + + t = -f s_g, f = (K - g_z) / (K n_z - n . g), + + where s_g is the excitation error measured along the beam and + K = sqrt(k0^2 - r^2) as in :func:`excitation_coefficients`. For n along + the beam f = 1. A rod nearly tangent to the sphere (an edge-on plate) is + never excited; f is set huge there so the reflection drops out. + + Parameters + ---------- + g_lab : torch.Tensor | np.ndarray + Lab-frame reciprocal vectors (..., 3). + n_lab : torch.Tensor | np.ndarray + Lab-frame unit plate normal, broadcastable to `g_lab`. + + Returns + ------- + torch.Tensor | np.ndarray + f (...,), the same type as `g_lab`. + """ + k0 = 1.0 / electron_wavelength_angstrom(energy_ev) + r = k0 * np.sin(np.deg2rad(precession_deg)) + K = float(np.sqrt(k0**2 - r**2)) + if isinstance(g_lab, torch.Tensor): + n = torch.as_tensor(n_lab, dtype=g_lab.dtype, device=g_lab.device) + den_n = K * n[..., 2] - (g_lab * n).sum(-1) + tangent = den_n.abs() < 0.05 * K + f = (K - g_lab[..., 2]) / torch.where(tangent, torch.ones_like(den_n), den_n) + return torch.where(tangent, torch.full_like(f, 1e6), f) + g = np.asarray(g_lab, dtype=float) + n = np.asarray(n_lab, dtype=float) + den_n = K * n[..., 2] - (g * n).sum(-1) + tangent = np.abs(den_n) < 0.05 * K + f = (K - g[..., 2]) / np.where(tangent, 1.0, den_n) + return np.where(tangent, 1e6, f) + + def averaged_gaussian_intensity_envelope( - g_lab, energy_ev: float, sigma: float, precession_deg: float = 0.0, semiconv_mrad: float = 0.0 + g_lab, + energy_ev: float, + sigma: float, + precession_deg: float = 0.0, + semiconv_mrad: float = 0.0, + foil_normal_lab=None, ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Envelope, central excitation error and support half-width of every - reflection under the illumination: returns (envelope, c, a, b).""" + reflection under the illumination: returns (envelope, c, a, b). + + With `foil_normal_lab` (a lab-frame unit plate normal) the excitation is + measured along the relrod instead of along the beam: c, a and b are the + distances along the rod (see :func:`relrod_factor`).""" c, a, b = excitation_coefficients(g_lab, energy_ev, precession_deg, semiconv_mrad) + if foil_normal_lab is not None: + g_np = g_lab.detach().cpu().numpy() if isinstance(g_lab, torch.Tensor) else g_lab + f = relrod_factor( + np.asarray(g_np, dtype=float), foil_normal_lab, energy_ev, precession_deg + ) + c, a, b = c * f, a * np.abs(f), b * np.abs(f) if precession_deg <= 0 and semiconv_mrad <= 0: return np.exp(-0.5 * (c / sigma) ** 2), c, a, b # A reflection farther from the Ewald sphere than the illumination sweeps @@ -227,7 +285,7 @@ def gaussian_envelope_ring_torch(c: torch.Tensor, a: torch.Tensor, sigma: float) after the I_4 term, with I_n(u) from the I_0 / I_1 recurrences (and their small-argument series where the recurrence would cancel). Below v = a^2 / 4 sigma^2 = 0.3 the truncation error is under 1e-5; larger - sweeps fall back to the reference series.""" + sweeps are averaged over the ring directly by quadrature.""" c = c.to(torch.float64) a = torch.as_tensor(a, dtype=torch.float64) u = c * a / sigma**2 @@ -252,8 +310,14 @@ def gaussian_envelope_ring_torch(c: torch.Tensor, a: torch.Tensor, sigma: float) big = torch.broadcast_to(v > 0.3, out.shape) if bool(big.any()): out = out.clone() - c_b = torch.broadcast_to(c, out.shape) - a_b = torch.broadcast_to(a, out.shape) - ref = gaussian_envelope_ring_series(c_b[big], a_b[big], sigma) - out[big] = ref.to(out.dtype) + c_b = torch.broadcast_to(c, out.shape)[big] + a_b = torch.broadcast_to(a, out.shape)[big] + # the ring average itself, (1/pi) int_0^pi exp(-(c - a cos phi)^2 / + # 2 sigma^2) dphi, by the midpoint rule: exact to rounding for this + # periodic integrand once the nodes resolve a / sigma, and it stays + # in torch on the input's device + n = int(min(256, max(16, np.ceil(8 + 4 * float(a_b.max()) / sigma)))) + phi = (torch.arange(n, dtype=torch.float64, device=c_b.device) + 0.5) * (np.pi / n) + d = c_b[:, None] - a_b[:, None] * torch.cos(phi) + out[big] = torch.exp(-0.5 * (d / sigma) ** 2).mean(dim=-1).to(out.dtype) return out.clamp(0.0, 1.0) diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index d49e05d02..19030bc02 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -301,6 +301,7 @@ def from_vectors( energy_ev: float = 300e3, precession_deg: float = 0.0, semiconv_mrad: float = 0.0, + foil_normal=None, ) -> "OrientationMap": """Create from detected Bragg peaks. @@ -317,14 +318,31 @@ def from_vectors( average the intensities over them) and inherited by them. energy_ev : float, default=300e3 Beam energy in eV. + foil_normal : sequence of float, optional + Plate normal of the specimen as a direction in the crystal, [uvw] + or [UVTW], e.g. (0, 0, 0, 1) for a 2D material lying in its basal + plane. Reflections are then rods along it, which moves the + simulated spots of a tilted flake to where the rods meet the + Ewald sphere (see :meth:`Crystal.generate_pattern`); the library, + the refinement and every simulated pattern use it. None + (default) is the usual geometry. """ if crystal.g_vec is None: raise RuntimeError("Run crystal.calculate_structure_factors() first.") om = cls(peaks, crystal, energy_ev, _token=cls._token) om.metadata["precession_deg"] = float(precession_deg) om.metadata["semiconv_mrad"] = float(semiconv_mrad) + om.metadata["foil_normal"] = ( + None if foil_normal is None else [float(v) for v in np.ravel(foil_normal)] + ) return om + def _foil_normal_crystal(self) -> torch.Tensor | None: + """Unit plate normal in the crystal frame, or None for the usual + geometry (normal along the beam).""" + fn = self.metadata.get("foil_normal") + return None if fn is None else self.crystal.direction_vector(fn) + # ------------------------------------------------------------------ # orientation plan # ------------------------------------------------------------------ @@ -726,6 +744,19 @@ def _build_reference( s_g = (2 * gz - lam * g2) / (2 - 2 * lam * gz) prec = float(self.metadata.get("precession_deg", 0.0) or 0.0) conv = float(self.metadata.get("semiconv_mrad", 0.0) or 0.0) + spot_xy = gr[..., :2] + n_c = self._foil_normal_crystal() + f_rod = None + if n_c is not None: + # plate geometry: excitation along the rod, spot where the rod + # meets the sphere; the normal turns with the crystal, so the + # template still rolls in gamma + from quantem.diffraction.illumination import relrod_factor + + n_lab = qrotate(zone_quats, n_c[None].expand(zone_quats.shape[0], 3)) # (Z, 3) + f_rod = relrod_factor(gr, n_lab[:, None, :], self.energy_ev, 0.0) + s_g = s_g * f_rod + spot_xy = spot_xy - s_g[..., None] * n_lab[:, None, :2] if prec > 0 or conv > 0: # excitation envelope averaged over the illumination # (quantem.diffraction.illumination); the peak weighting below @@ -737,6 +768,8 @@ def _build_reference( ) a_r, b_r = excitation_amplitudes(gr, self.energy_ev, prec, conv) + if f_rod is not None: + a_r, b_r = a_r * f_rod.abs(), b_r * f_rod.abs() if conv <= 0: amp = gaussian_envelope_ring_torch(s_g, a_r, self.sigma_excitation) else: @@ -755,8 +788,8 @@ def _build_reference( crystal.g_len**self.power_radial * crystal.struct_factors_int**self.power_intensity ) vals = amp * weight[None, :] # (Z, N) - qr = torch.hypot(gr[..., 0], gr[..., 1]) - qphi = torch.atan2(gr[..., 1], gr[..., 0]) + qr = torch.hypot(spot_xy[..., 0], spot_xy[..., 1]) + qphi = torch.atan2(spot_xy[..., 1], spot_xy[..., 0]) Z = zone_quats.shape[0] plan = torch.zeros((Z, self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64) @@ -1465,11 +1498,14 @@ def refine_orientations( power_env = POWER_INTENSITY prec_ill = float(self.metadata.get("precession_deg", 0.0) or 0.0) conv_ill = float(self.metadata.get("semiconv_mrad", 0.0) or 0.0) + n_rod = self._foil_normal_crystal() + from quantem.diffraction.illumination import relrod_factor - def envelope(S, g_rows): + def envelope(S, g_rows, f_rows=None): # Laue-circle envelope of the paired reflections at shifted # excitation errors S (P, T, T), averaged over the illumination - # recorded on this map (ring: Bessel series; disk: transform) + # recorded on this map (ring: Bessel series; disk: transform); + # f_rows scales the sweep onto the relrod for a foil normal if prec_ill <= 0 and conv_ill <= 0: return torch.exp(-(S**2) / (2 * sigma_env**2)) from quantem.diffraction.illumination import ( @@ -1479,6 +1515,8 @@ def envelope(S, g_rows): ) a_r, b_r = excitation_amplitudes(g_rows, self.energy_ev, prec_ill, conv_ill) + if f_rows is not None: + a_r, b_r = a_r * f_rows.abs(), b_r * f_rows.abs() if conv_ill <= 0: return gaussian_envelope_ring_torch(S, a_r[:, None, None], sigma_env) return torch.as_tensor( @@ -1505,11 +1543,20 @@ def refine_single(q, q_exp, w_exp): g = qrotate(q, g_all) gz, g2 = g[:, 2], (g**2).sum(dim=1) s_g = (2 * gz - lam * g2) / (2 - 2 * lam * gz) + spot = g[:, :2] + f_rod = torch.ones_like(s_g) + if n_rod is not None: + # plate geometry: excitation along the rod, the spot + # where the rod meets the sphere + n_lab = qrotate(q, n_rod[None])[0] + f_rod = relrod_factor(g, n_lab, self.energy_ev, 0.0) + s_g = s_g * f_rod + spot = spot - s_g[:, None] * n_lab[None, :2] sel = torch.abs(s_g) < 2 * sigma g_sel = g[sel] if g_sel.shape[0] == 0: return q, score - d = torch.cdist(g_sel[:, :2], q_exp) + d = torch.cdist(spot[sel], q_exp) d_min, j_min = d.min(dim=1) pair = d_min < delta if int(pair.sum()) < min_pairs: @@ -1519,7 +1566,7 @@ def refine_single(q, q_exp, w_exp): w = w_exp[j_min[pair]] * (1 - d_min[pair] / delta) score = float(w.sum()) # solve min sum w | tgt - (g + omega x g)_xy |^2 for omega - r = tgt - gp[:, :2] # (P, 2) + r = tgt - spot[sel][pair] # (P, 2) if refine_tilt: A = torch.zeros((gp.shape[0], 2, 3), dtype=torch.float64) A[:, 0, 1] = gp[:, 2] @@ -1551,17 +1598,19 @@ def refine_single(q, q_exp, w_exp): # positions carry no tilt information, the excitation # pattern does) s0 = s_g[sel][pair] - a1 = gp[:, 1] - a2 = -gp[:, 0] + fr = f_rod[sel][pair] + a1 = gp[:, 1] * fr + a2 = -gp[:, 0] * fr f_p = f_all[sel][pair] S = ( s0[:, None, None] + tg[None, :, None] * a1[:, None, None] + tg[None, None, :] * a2[:, None, None] ) - pred = (f_p[:, None, None] * envelope(S, g_sel[pair])).clamp_min( - 0 - ) ** power_env + pred = ( + f_p[:, None, None] + * envelope(S, g_sel[pair], None if n_rod is None else fr) + ).clamp_min(0) ** power_env w_env = w**power_env E = (w_env[:, None, None] * pred).sum(dim=0) / ( (pred**2).sum(dim=0).sqrt().clamp_min(1e-12) @@ -1753,19 +1802,13 @@ def get_exp(rx, ry): data = peaks[rx, ry].numpy().astype(np.float64) cur_q = self.quats[rx, ry, 0].clone() cur_s = float(cscore[rx, ry]) - cands = [cur_q] - - def _add(qn, cands=cands): - if all(float(misorientation_angle_deg(qn, c, sym_m)) > 0.5 for c in cands): - cands.append(qn) - - _add(qmult(q_twin, cur_q)) - # none where the twin is a symmetry copy of this orientation - twin_ix = 1 if len(cands) == 2 else None + # every candidate in order: this orientation, its Friedel + # twin, its own other matches, the neighbours' matches + raw = [cur_q, qmult(q_twin, cur_q)] nbrs = [] for m in range(1, M): if self.corr[rx, ry, m] > 0: - _add(self.quats[rx, ry, m]) + raw.append(self.quats[rx, ry, m]) for dr in (-1, 0, 1): for dc in (-1, 0, 1): nr, nc = rx + dr, ry + dc @@ -1776,7 +1819,18 @@ def _add(qn, cands=cands): nbrs.append(self.quats[nr, nc, 0]) for m in range(M): if self.corr[nr, nc, m] > 0: - _add(self.quats[nr, nc, m]) + raw.append(self.quats[nr, nc, m]) + # drop repeats within 0.5 degrees, keeping the first, from + # one batched misorientation matrix + qr = torch.stack(raw) + dup = (misorientation_angle_deg(qr[:, None], qr[None], sym_m) <= 0.5).numpy() + keep: list[int] = [] + for i in range(len(raw)): + if not any(dup[i, j] for j in keep): + keep.append(i) + cands = [raw[i] for i in keep] + # none where the twin is a symmetry copy of this orientation + twin_ix = 1 if 1 in keep else None # score every candidate as it stands -- the neighbours' # were refined on a neighbouring pattern already cq = torch.stack(cands) @@ -1872,6 +1926,7 @@ def _correlation_score( sigma_excitation=self.sigma_excitation, precession_deg=pd, semiconv_mrad=sc, + foil_normal=self.metadata.get("foil_normal"), ) s_xy = torch.stack((sim["qx"], sim["qy"]), dim=1).to(torch.float64) s_i = sim["intensity"].to(torch.float64).clamp_min(0) @@ -1927,11 +1982,14 @@ def _refine_batched( n_tg = tg.shape[0] prec_ill = float(self.metadata.get("precession_deg", 0.0) or 0.0) conv_ill = float(self.metadata.get("semiconv_mrad", 0.0) or 0.0) + n_rod = self._foil_normal_crystal() + from quantem.diffraction.illumination import relrod_factor - def envelope(S, g_rows): + def envelope(S, g_rows, f_rows=None): # Laue-circle envelope of the paired reflections at shifted # excitation errors S (P, T, T), averaged over the illumination - # recorded on this map (ring: Bessel series; disk: transform) + # recorded on this map (ring: Bessel series; disk: transform); + # f_rows scales the sweep onto the relrod for a foil normal if prec_ill <= 0 and conv_ill <= 0: return torch.exp(-(S**2) / (2 * sigma_env**2)) from quantem.diffraction.illumination import ( @@ -1941,6 +1999,8 @@ def envelope(S, g_rows): ) a_r, b_r = excitation_amplitudes(g_rows, self.energy_ev, prec_ill, conv_ill) + if f_rows is not None: + a_r, b_r = a_r * f_rows.abs(), b_r * f_rows.abs() if conv_ill <= 0: return gaussian_envelope_ring_torch(S, a_r[:, None, None], sigma_env) return torch.as_tensor( @@ -1997,8 +2057,17 @@ def envelope(S, g_rows): g = torch.einsum("bij,gj->bgi", Rm, g_all) # (B, G, 3) gz, g2 = g[..., 2], (g**2).sum(dim=-1) s_g = (2 * gz - lam * g2) / (2 - 2 * lam * gz) + spot = g[..., :2] + f_rod = None + if n_rod is not None: + # plate geometry: excitation along the rod, the spot + # where the rod meets the sphere + n_lab = torch.einsum("bij,j->bi", Rm, n_rod) # (B, 3) + f_rod = relrod_factor(g, n_lab[:, None, :], self.energy_ev, 0.0) + s_g = s_g * f_rod + spot = spot - s_g[..., None] * n_lab[:, None, :2] sel = torch.abs(s_g) < 2 * sigma # (B, G) - d = torch.cdist(g[..., :2], qe) # (B, G, P) + d = torch.cdist(spot, qe) # (B, G, P) d_min, j_min = d.min(dim=-1) # (B, G) pair = sel & (d_min < delta) w_g = torch.gather(we, 1, j_min) * (1 - d_min / delta).clamp_min(0) @@ -2009,9 +2078,9 @@ def envelope(S, g_rows): break sc = torch.where(ok, w_g.sum(dim=1), sc) tgt = torch.gather(qe, 1, j_min[..., None].expand(-1, -1, 2)) # (B, G, 2) - r_vec = tgt - g[..., :2] + r_vec = tgt - spot # in-plane closed form - a_vec = torch.stack((-g[..., 1], g[..., 0]), dim=-1) + a_vec = torch.stack((-spot[..., 1], spot[..., 0]), dim=-1) num = (w_g[..., None] * a_vec * r_vec).sum(dim=(1, 2)) den = (w_g[..., None] * a_vec * a_vec).sum(dim=(1, 2)) wz = torch.where(ok, num / den.clamp_min(1e-12), torch.zeros_like(num)) @@ -2031,8 +2100,9 @@ def envelope(S, g_rows): # sparse over paired reflections only idx_b, idx_g = torch.nonzero(pair, as_tuple=True) s0f = s_g[idx_b, idx_g] - gyf = g[idx_b, idx_g, 1] - gxf = g[idx_b, idx_g, 0] + fr = None if f_rod is None else f_rod[idx_b, idx_g] + gyf = g[idx_b, idx_g, 1] * (1.0 if fr is None else fr) + gxf = g[idx_b, idx_g, 0] * (1.0 if fr is None else fr) ff = f_all[idx_g] wf = w_g[idx_b, idx_g] S = ( @@ -2040,7 +2110,7 @@ def envelope(S, g_rows): + tg[None, :, None] * gyf[:, None, None] - tg[None, None, :] * gxf[:, None, None] ) # (Np, T, T) - pred = (ff[:, None, None] * envelope(S, g[idx_b, idx_g])).clamp_min( + pred = (ff[:, None, None] * envelope(S, g[idx_b, idx_g], fr)).clamp_min( 0 ) ** power_env E_num = torch.zeros((B, n_tg, n_tg), dtype=torch.float64).index_add_( @@ -2107,6 +2177,7 @@ def generate_pattern(self, rx: int, ry: int, match: int = 0, **kwargs): assert self.quats is not None kwargs.setdefault("precession_deg", self.metadata.get("precession_deg", 0.0)) kwargs.setdefault("semiconv_mrad", self.metadata.get("semiconv_mrad", 0.0)) + kwargs.setdefault("foil_normal", self.metadata.get("foil_normal")) return self.crystal.generate_pattern( self.quats[rx, ry, match], energy_ev=self.energy_ev, @@ -2179,7 +2250,14 @@ def match_residual( dtype=peaks.dtype, ) - om_res = OrientationMap.from_vectors(residual, self.crystal, self.energy_ev) + om_res = OrientationMap.from_vectors( + residual, + self.crystal, + self.energy_ev, + precession_deg=self.metadata.get("precession_deg", 0.0) or 0.0, + semiconv_mrad=self.metadata.get("semiconv_mrad", 0.0) or 0.0, + foil_normal=self.metadata.get("foil_normal"), + ) for attr in ( "device", "corr_kernel_size", diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py index e8b904868..2237439c4 100644 --- a/src/quantem/diffraction/phase.py +++ b/src/quantem/diffraction/phase.py @@ -438,8 +438,9 @@ def signal_confidence( Parameters ---------- signal_range : tuple | "auto", default="auto" - Diffracted intensity mapped to 0 ... 1. "auto" spans zero to the - 95th percentile over the indexed positions. + Diffracted intensity mapped to 0 ... 1. "auto" spans zero to half + the median over the indexed positions, so a crystal shows at full + strength and only weakly diffracting positions fade. gamma : float, default=1.0 Exponent applied to the result. The default returns the raw confidence, which is what a threshold should be taken on; @@ -458,8 +459,11 @@ def signal_confidence( sig = np.nan_to_num(self.diffracted_intensity.numpy()) indexed = self.phase_index.numpy() >= 0 if isinstance(signal_range, str): + # full brightness from half the median signal of the indexed + # positions: crystals show at full strength and only positions + # that diffract well below typical fade vals = sig[indexed] - hi = float(np.percentile(vals, 95)) if vals.size else 1.0 + hi = 0.5 * float(np.median(vals)) if vals.size else 1.0 lo, hi = 0.0, max(hi, 1e-12) else: lo, hi = signal_range diff --git a/tests/diffraction/test_two_phase_map.py b/tests/diffraction/test_two_phase_map.py index 56b7cd42f..f774f50da 100644 --- a/tests/diffraction/test_two_phase_map.py +++ b/tests/diffraction/test_two_phase_map.py @@ -245,3 +245,69 @@ def test_plot_calibration_shows_every_crystal(): labels = [t.get_text() for t in ax.get_legend().get_texts()] assert labels == ["Ti alpha rings", "Ti beta rings"] matplotlib.pyplot.close(fig) + + +def test_refine_skips_crystals_out_of_the_running(): + from quantem.diffraction.crystal_map import CrystalMap + + rng = np.random.default_rng(7) + ti_a = Crystal.from_ase( + bulk("Ti", "hcp", a=2.9505, c=4.6855), name="Ti alpha", verbose=False + ).calculate_structure_factors(k_max=1.5) + ti_b = Crystal.from_ase( + bulk("Ti", "bcc", a=3.26, cubic=True), name="Ti beta", verbose=False + ).calculate_structure_factors(k_max=1.5) + q_a = torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=torch.float64) + q_b = quat_from_axis_angle( + torch.tensor([1.0, 0.0, 0.0], dtype=torch.float64), + torch.tensor(np.deg2rad(20.0), dtype=torch.float64), + ) + cells = [ + [_pattern(ti_a, q_a, rng) if c < 3 else _pattern(ti_b, q_b, rng) for c in range(6)] + for _ in range(2) + ] + peaks = Vector.from_data(cells, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t") + fractions = [] + for margin in (None, 0.1): + cm = CrystalMap.from_vectors(peaks, [ti_a, ti_b], energy_ev=200e3) + cm.build_plan() + cm.match_orientations(progress_bar=False) + corr = np.stack([om.corr[..., 0].numpy() for om in cm]) + q_lib = [om.quats.clone() for om in cm] + cm.refine_orientations(competitive_margin=margin, progress_bar=False) + cm.fit(progress_bar=False) + fractions.append(cm.phase_index.copy()) + if margin is not None: + out = corr < corr.max(axis=0) - margin + for i, om in enumerate(cm): + # where a crystal is out of the running its library match stays + assert torch.equal(om.quats[out[i]], q_lib[i][out[i]]) + assert out.any() + assert (fractions[0] == fractions[1]).all() + + +def test_zone_reflections_keep_one_zone(): + from quantem.diffraction.calibration import plot_calibration, zone_reflections + + ti_a = Crystal.from_ase( + bulk("Ti", "hcp", a=2.9505, c=4.6855), name="Ti alpha", verbose=False + ).calculate_structure_factors(k_max=1.5) + basal = zone_reflections(ti_a, (0, 0, 0, 1)) + # [0001] keeps exactly the hk0 reflections, and 3- and 4-index agree + assert (basal.hkl[:, 2] == 0).all() + assert len(basal.hkl) == int((ti_a.hkl[:, 2] == 0).sum()) + assert torch.equal(zone_reflections(ti_a, (0, 0, 1)).hkl, basal.hkl) + assert basal.name == "Ti alpha [0001]" and ti_a.name == "Ti alpha" + assert len(ti_a.hkl) > len(basal.hkl) + # the plots accept it and title the column by the zone + import matplotlib + + matplotlib.use("Agg") + rng = np.random.default_rng(8) + q = torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=torch.float64) + peaks = Vector.from_data( + [[_pattern(ti_a, q, rng)]], fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + fig, axs = plot_calibration(peaks, ti_a, zone_axis=(0, 0, 0, 1)) + assert axs[0, 0].get_title() == "Ti alpha [0001]" + matplotlib.pyplot.close(fig) From e96a3146507e53c819aa901cc63c2daffcd3ba1c Mon Sep 17 00:00:00 2001 From: cophus Date: Fri, 2 Oct 2026 16:05:38 -0700 Subject: [PATCH 24/36] initial RMC build --- src/quantem/core/io/file_readers.py | 27 +- src/quantem/diffraction/__init__.py | 1 + .../diffraction/reverse_monte_carlo.py | 1335 +++++++++++++++++ tests/diffraction/test_reverse_monte_carlo.py | 109 ++ 4 files changed, 1463 insertions(+), 9 deletions(-) create mode 100644 src/quantem/diffraction/reverse_monte_carlo.py create mode 100644 tests/diffraction/test_reverse_monte_carlo.py diff --git a/src/quantem/core/io/file_readers.py b/src/quantem/core/io/file_readers.py index db9e71d9f..ba35d1fc3 100644 --- a/src/quantem/core/io/file_readers.py +++ b/src/quantem/core/io/file_readers.py @@ -19,6 +19,21 @@ ) +def _rsciio_reader(file_path: str | PathLike, file_type: str | None = None): + """rosettasciio file_reader from a plugin name ("digitalmicrograph") or extension ("dm3").""" + if file_type is None: + file_type = Path(file_path).suffix.lstrip(".") + try: + return importlib.import_module(f"rsciio.{file_type.lower()}").file_reader + except ModuleNotFoundError: + import rsciio + + for plugin in rsciio.IO_PLUGINS: + if file_type.lower() in (ext.lower() for ext in plugin["file_extensions"]): + return importlib.import_module(plugin["api"]).file_reader + raise ValueError(f"No rosettasciio reader for file type '{file_type}'") + + def _print_available_datasets(data_list): print("Available datasets:") for index, entry in enumerate(data_list): @@ -196,15 +211,12 @@ def _reshape_3d_to_4d( return imported_data_4d - if file_type is None: - file_type = Path(file_path).suffix.lower().lstrip(".") - sampling_override = kwargs.pop("sampling", None) origin_override = kwargs.pop("origin", None) units_override = kwargs.pop("units", None) name_override = kwargs.pop("name", None) - file_reader = importlib.import_module(f"rsciio.{file_type}").file_reader + file_reader = _rsciio_reader(file_path, file_type) data_list = file_reader(file_path, **kwargs) if not data_list: @@ -354,7 +366,7 @@ def read_3d_spectroscopy( """ data_type_normalized = str(data_type).upper() - file_reader = importlib.import_module(f"rsciio.{file_type}").file_reader # type: ignore + file_reader = _rsciio_reader(file_path, file_type) data_list = file_reader(file_path) # If specific index provided, use it @@ -440,10 +452,7 @@ def read_2d( -------- Dataset """ - if file_type is None: - file_type = Path(file_path).suffix.lower().lstrip(".") - - file_reader = importlib.import_module(f"rsciio.{file_type}").file_reader + file_reader = _rsciio_reader(file_path, file_type) imported_data = file_reader(file_path)[0] dataset = Dataset2d.from_array( diff --git a/src/quantem/diffraction/__init__.py b/src/quantem/diffraction/__init__.py index 6caed1891..2ccc6d9e2 100644 --- a/src/quantem/diffraction/__init__.py +++ b/src/quantem/diffraction/__init__.py @@ -3,6 +3,7 @@ from quantem.diffraction.crystal_map import CrystalMap as CrystalMap from quantem.diffraction.orientation import OrientationMap as OrientationMap from quantem.diffraction.phase import PhaseMap as PhaseMap +from quantem.diffraction.reverse_monte_carlo import ReverseMonteCarlo as ReverseMonteCarlo from quantem.diffraction.strain import StrainMap as StrainMap from quantem.diffraction import bloch as bloch from quantem.diffraction import calibration as calibration diff --git a/src/quantem/diffraction/reverse_monte_carlo.py b/src/quantem/diffraction/reverse_monte_carlo.py new file mode 100644 index 000000000..b2724cb07 --- /dev/null +++ b/src/quantem/diffraction/reverse_monte_carlo.py @@ -0,0 +1,1335 @@ +"""Reverse Monte Carlo fitting of diffuse electron scattering from several zone axes. + +One periodic supercell of a disordered crystal is fitted to every pattern at +once. The supercell's diffuse amplitude ``G(h) = sum_j (sigma_j - c) +exp(-2 pi i h.x_j / N)`` lives on the FFT grid of its sites; the diffuse +intensity of each pattern is ``|f_B - f_A|^2 |G|^2`` read off where the +pattern's Ewald sphere (with that pattern's fitted tilt) cuts the grid, +averaged over the cubic rotations so the model is as symmetric as the +(statistically cubic) foil. Swapping two atoms changes ``G`` by two phase +factors, so every move is scored exactly without recomputing the supercell. +Bragg peaks are excluded by a sigmoid weight that is 0 on each reflection and +1 away from it; a smooth background (constant, two Gaussians about the direct +beam, an Einstein thermal diffuse term and the Bragg cores blurred by the +detector's point spread) and one scale per pattern are solved in closed form. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from itertools import permutations, product + +import numpy as np +import torch +from scipy import ndimage, optimize, sparse +from scipy.spatial import cKDTree +from tqdm.auto import tqdm + +from quantem.core.io.serialize import AutoSerialize +from quantem.diffraction.crystal import Crystal, electron_scattering_factor + + +def electron_wavelength(energy_ev: float) -> float: + """Relativistic electron wavelength in Angstroms.""" + return 12.2642598 / np.sqrt(energy_ev * (1.0 + 0.97847573e-6 * energy_ev)) + + +def cubic_rotations() -> np.ndarray: + """The 24 proper rotations of the cube as signed permutation matrices (24, 3, 3).""" + ops = [] + for perm in permutations(range(3)): + for signs in product((1, -1), repeat=3): + m = np.zeros((3, 3), dtype=int) + m[range(3), perm] = signs + if round(np.linalg.det(m)) == 1: + ops.append(m) + return np.stack(ops) + + +def _zone_frame(zone_axis) -> np.ndarray: + """Orthonormal crystal-frame basis (e1, e2, z) with z along the zone axis.""" + z = np.asarray(zone_axis, dtype=float) + z /= np.linalg.norm(z) + trial = np.eye(3)[np.argmin(np.abs(z))] + e1 = trial - (trial @ z) * z + e1 /= np.linalg.norm(e1) + return np.stack([e1, np.cross(z, e1), z]) + + +def _rot2(theta: float) -> np.ndarray: + c, s = np.cos(theta), np.sin(theta) + return np.array([[c, -s], [s, c]]) + + +def _sigmoid(x): + return 0.5 * (1.0 + np.tanh(0.5 * x)) + + +def _default_device() -> str: + if torch.cuda.is_available(): + return "cuda" + if torch.backends.mps.is_available(): + return "mps" + return "cpu" + + +class ReverseMonteCarlo(AutoSerialize): + """Reverse Monte Carlo fit of one supercell to diffraction patterns along several zone axes. + + Build with :meth:`from_images`, then ``set_crystal`` -> ``fit_geometry`` -> + ``set_mask`` -> ``build_supercell`` -> ``fit_background`` -> ``run``. + """ + + _token = object() + + def __init__(self, images, zone_axes, sampling, energy, names, bin_factor, _token=None): + if _token is not self._token: + raise RuntimeError("Use ReverseMonteCarlo.from_images().") + self.images = [np.asarray(im, dtype=np.float32) for im in images] + self.zone_axes = [tuple(int(v) for v in z) for z in zone_axes] + self.sampling = float(sampling) + self.energy = float(energy) + self.wavelength = electron_wavelength(self.energy) + self.names = list(names) + self.bin_factor = int(bin_factor) + self.crystal: Crystal | None = None + self.geometry: dict | None = None + self.mask: dict | None = None + self.loss_history: list[float] = [] + + @classmethod + def from_images( + cls, + images: Sequence, + zone_axes: Sequence[Sequence[int]], + energy: float = 200e3, + bin_factor: int = 8, + sampling: float | None = None, + names: Sequence[str] | None = None, + ) -> "ReverseMonteCarlo": + """Patterns (Dataset2d or arrays) and the zone axis of each. + + Parameters + ---------- + images : sequence of Dataset2d or ndarray + One diffraction pattern per zone axis. + zone_axes : sequence of (u, v, w) + Nominal zone axis of each pattern, in the crystal's lattice + indices; the tilt off it is fitted. + energy : float + Beam energy in eV. + bin_factor : int + Detector binning for the diffuse fit (geometry uses full resolution). + sampling : float, optional + Detector pixel size in 1/Angstrom. Read from the first Dataset2d + (1/nm is converted) when not given. + names : sequence of str, optional + Panel titles; default the zone axes. + """ + arrays = [] + for im in images: + if hasattr(im, "array"): + if sampling is None: + s = float(np.asarray(im.sampling)[0]) + units = str(im.units[0]).lower() + sampling = s * 0.1 if "nm" in units else s + arrays.append(np.asarray(im.array)) + else: + arrays.append(np.asarray(im)) + if sampling is None: + raise ValueError("sampling (1/Angstrom per pixel) is required for plain arrays.") + if len(arrays) != len(zone_axes): + raise ValueError("one zone axis per image") + if names is None: + names = ["[" + "".join(str(v) for v in z) + "]" for z in zone_axes] + return cls(arrays, zone_axes, sampling, energy, names, bin_factor, _token=cls._token) + + def _binned(self, a: np.ndarray, reduce: str = "mean") -> np.ndarray: + b = self.bin_factor + ny, nx = (a.shape[0] // b) * b, (a.shape[1] // b) * b + out = a[:ny, :nx].reshape(ny // b, b, nx // b, b).sum(axis=(1, 3)) + return out / b**2 if reduce == "mean" else out + + # ------------------------------------------------------------------ crystal + + def set_crystal( + self, + crystal: Crystal | None = None, + cif_file: str | None = None, + merge: dict[str, str] | None = None, + ) -> "ReverseMonteCarlo": + """Crystal whose mixed-occupancy sites are fitted. + + Parameters + ---------- + crystal, cif_file : Crystal or path + The average structure, with fractional occupancies on shared sites. + merge : dict, optional + Species to relabel before fitting, e.g. ``{"Zr": "Nb"}`` folds Zr + into Nb so the shared site becomes a binary V-Nb site. + """ + from ase.data import atomic_numbers, chemical_symbols + + if crystal is None: + if cif_file is None: + raise ValueError("give crystal or cif_file") + crystal = Crystal.from_cif(cif_file, verbose=False) + self.crystal = crystal + cell = crystal.lat_real.numpy() + a = float(np.linalg.norm(cell[0])) + if not np.allclose(cell, a * np.eye(3), atol=1e-3 * a): + raise NotImplementedError("Only cubic cells are supported so far.") + merge = merge or {} + frac = np.mod(crystal.positions_frac.numpy(), 1.0) + symbols = [chemical_symbols[int(z)] for z in crystal.numbers] + symbols = [merge.get(s, s) for s in symbols] + occ = crystal.occupancy.numpy() + + # group species by site + sites: list[tuple[np.ndarray, dict[str, float]]] = [] + for f, s, o in zip(frac, symbols, occ): + for site in sites: + if np.allclose(site[0], f, atol=1e-4): + site[1][s] = site[1].get(s, 0.0) + float(o) + break + else: + sites.append((f, {s: float(o)})) + mixed = [(f, comp) for f, comp in sites if len(comp) > 1] + if not mixed: + raise ValueError("The crystal has no mixed-occupancy site to fit.") + species = sorted({s for _, comp in mixed for s in comp}) + if len(species) != 2: + raise NotImplementedError(f"Binary sites only so far; got {species}. Use merge=.") + comps = {tuple(sorted(comp.items())) for _, comp in mixed} + if len(comps) != 1: + raise NotImplementedError("All mixed sites must share one composition.") + comp = dict(next(iter(comps))) + total = sum(comp.values()) + + # smallest grid divisor that puts every mixed site on an integer grid + fr = np.stack([f for f, _ in mixed]) + for d in range(1, 13): + if np.allclose(fr * d, np.round(fr * d), atol=1e-4): + break + else: + raise ValueError("Mixed sites are not on a rational grid with denominator <= 12.") + + self._a_crystal = a + self.lattice_parameter = a + self.species = species # [A, B]; sigma = 1 marks B + self.numbers = [atomic_numbers[s] for s in species] + self.concentration = comp[species[1]] / total + self.site_grid = np.round(fr * d).astype(int) + self.grid_divisor = d + print( + f"{len(mixed)} mixed site(s) per cell, " + + ", ".join(f"{s} {comp[s] / total:.3f}" for s in species) + + f", a = {a:.4f} A" + ) + return self + + def _zone_reflections(self, zone_axis, k_max: float): + """Allowed reflections in a zone: hkl (n, 3), zone-frame coords at the CIF a (n, 2), |F|^2.""" + crystal = self.crystal + crystal.calculate_structure_factors(k_max) + hkl = crystal.hkl.numpy() + inten = crystal.struct_factors_int.numpy() + keep = (hkl @ np.asarray(zone_axis) == 0) & (inten > 1e-4 * inten.max()) + hkl, inten = hkl[keep], inten[keep] + frame = _zone_frame(zone_axis) + g = hkl / self._a_crystal + return hkl, g @ frame[:2].T, inten + + # ----------------------------------------------------------------- geometry + + @staticmethod + def _find_peaks(im, n_peaks: int = 150): + smooth = ndimage.gaussian_filter(im, 2.0) + prom = smooth - ndimage.gaussian_filter(im, 25.0) + local = (prom == ndimage.maximum_filter(prom, 15)) & (prom > 0) + r, c = np.nonzero(local) + order = np.argsort(prom[r, c])[::-1][:n_peaks] + r, c = r[order], c[order] + pts = [] + for ri, ci in zip(r, c): + r0, r1 = max(ri - 4, 0), min(ri + 5, im.shape[0]) + c0, c1 = max(ci - 4, 0), min(ci + 5, im.shape[1]) + w = np.clip(prom[r0:r1, c0:c1] - 0.3 * prom[ri, ci], 0, None) + rr, cc = np.mgrid[r0:r1, c0:c1] + pts.append([(w * rr).sum() / w.sum(), (w * cc).sum() / w.sum()]) + return np.asarray(pts), prom[r, c], prom + + @staticmethod + def _halo_center(im) -> np.ndarray: + """Center of the broad inelastic halo, which sits on the direct beam.""" + small = ndimage.median_filter(im[::4, ::4].astype(np.float64), size=9) + b = ndimage.gaussian_filter(small, 10) + return np.asarray(np.unravel_index(np.argmax(b), b.shape), dtype=float) * 4 + 1.5 + + def fit_geometry( + self, + scale_range: tuple[float, float] = (0.85, 1.2), + k_max: float = 1.6, + centers: Sequence | None = None, + fit_tilt: bool = True, + verbose: bool = True, + ) -> "ReverseMonteCarlo": + """Index every pattern, fit its detector distortion and its tilt off the zone axis. + + The direct beam is the detected peak nearest the center of the broad + inelastic halo (a tilted pattern can have diffracted beams brighter + than the direct beam), or nearest ``centers[i]`` (row, col). Each + pattern then gets its own center and 2x2 detector matrix (rotation, + scale, ellipticity), fitted to the matched peaks. The lattice + parameter is the mean over patterns at the nominal pixel size. + + The tilt (beam direction off the zone axis, small-angle vector in the + zone frame) is fitted to the Bragg intensities: each reflection's + excitation error is ``s = -(|g|^2 / 2K + tilt . g)`` and its intensity + ``|F|^2 exp(-s^2 / 2 sigma^2)``. Zero tilt puts the Laue circle on the + direct beam. + """ + if self.crystal is None: + raise RuntimeError("set_crystal first") + pix = self.sampling + k_wave = 1.0 / self.wavelength + geo = dict(centers=[], matrices=[], tilts=[], a=[], rms_px=[], n_matched=[], peaks=[]) + geo.update(bragg_hkl=[], bragg_g=[], bragg_intensity=[], bragg_px=[], excitation_width=[]) + for i, (im, zone) in enumerate(zip(self.images, self.zone_axes)): + pts, heights, prom = self._find_peaks(im) + guess = ( + np.asarray(centers[i], dtype=float) + if centers is not None and centers[i] is not None + else self._halo_center(im) + ) + center = pts[np.argmin(np.linalg.norm(pts - guess, axis=1))] + hkl, g2, inten = self._zone_reflections(zone, k_max) + nz = np.linalg.norm(hkl, axis=1) > 0 + g2_nz, inten_nz = g2[nz], inten[nz] + + # coarse search: in-plane rotation and scale + score_img = ndimage.gaussian_filter(np.clip(prom, 0, None), 3.0) + wts = np.sqrt(inten_nz) + best = (-np.inf, 0.0, 1.0) + for scale in np.arange(scale_range[0], scale_range[1] + 1e-9, 0.004): + for th in np.deg2rad(np.arange(0.0, 360.0, 0.5)): + p = center + (g2_nz @ _rot2(th).T) / (pix * scale) + ok = ( + (p[:, 0] >= 0) + & (p[:, 0] < im.shape[0] - 1) + & (p[:, 1] >= 0) + & (p[:, 1] < im.shape[1] - 1) + ) + if ok.sum() < 4: + continue + pi = np.round(p[ok]).astype(int) + s = (wts[ok] * score_img[pi[:, 0], pi[:, 1]]).sum() / wts[ok].sum() + if s > best[0]: + best = (s, th, scale) + A = _rot2(best[1]) / (pix * best[2]) + c = center.copy() + + # refine center + 2x2 matrix on matched peaks, tightening the match + for tol in (12.0, 8.0, 5.0, 5.0): + p = c + g2_nz @ A.T + dist, j = cKDTree(pts).query(p) + ok = dist < tol + obs = pts[j[ok]] + X = np.column_stack([np.ones(ok.sum()), g2_nz[ok]]) + coef, *_ = np.linalg.lstsq(X, obs, rcond=None) + c, A = coef[0], coef[1:].T + p = c + g2_nz @ A.T + dist, j = cKDTree(pts).query(p) + ok = dist < 5.0 + rms = float(np.sqrt(np.mean(dist[ok] ** 2))) + a_i = self._a_crystal / (pix * np.sqrt(abs(np.linalg.det(A)))) + sv = np.linalg.svd(A, compute_uv=False) + + # Bragg intensities at the fitted positions + p_all = c + g2 @ A.T + r_core = 0.03 / pix + inten_meas = _integrate_spots(im, p_all, r_core) + + geo["centers"].append(c) + geo["matrices"].append(A) + geo["a"].append(float(a_i)) + geo["rms_px"].append(rms) + geo["n_matched"].append(int(ok.sum())) + geo["peaks"].append(pts) + geo["bragg_hkl"].append(hkl) + geo["bragg_g"].append(g2) # zone frame, at the CIF lattice parameter + geo["bragg_px"].append(p_all) + geo["bragg_intensity"].append(inten_meas) + geo["_inten_kin"] = geo.get("_inten_kin", []) + [inten] + if verbose: + print( + f"{self.names[i]}: center ({c[0]:.1f}, {c[1]:.1f}), a = {a_i:.4f} A, " + f"anisotropy {100 * (sv[0] / sv[1] - 1):.2f}%, " + f"{int(ok.sum())} peaks, rms {rms:.2f} px" + ) + + self.lattice_parameter = float(np.mean(geo["a"])) + self.geometry = geo + for i in range(len(self.images)): + tilt, width = (np.zeros(2), np.nan) + if fit_tilt: + tilt, width = self._fit_tilt(i, k_wave) + geo["tilts"].append(tilt) + geo["excitation_width"].append(width) + if verbose and fit_tilt: + ang = np.rad2deg(np.linalg.norm(tilt)) + print(f"{self.names[i]}: tilt {ang:.2f} deg off the zone axis") + if verbose: + print(f"lattice parameter {self.lattice_parameter:.4f} A at {pix:.6f} 1/A per pixel") + return self + + def _fit_tilt(self, i: int, k_wave: float, max_tilt_deg: float = 3.5, prior_deg: float = 2.0): + """Tilt vector (zone frame, radians) from the Bragg intensities of pattern i, with a + Gaussian prior of ``prior_deg`` so patterns whose intensities barely constrain it stay + near the zone axis.""" + geo = self.geometry + hkl = geo["bragg_hkl"][i] + nz = np.linalg.norm(hkl, axis=1) > 0 + g = geo["bragg_g"][i][nz] * self._a_crystal / self.lattice_parameter + p_px = geo["bragg_px"][i][nz] + ny, nx = self.images[i].shape + inside = ( + (p_px[:, 0] > 20) & (p_px[:, 0] < ny - 20) & (p_px[:, 1] > 20) & (p_px[:, 1] < nx - 20) + ) + g = g[inside] + meas = geo["bragg_intensity"][i][nz][inside] + kin = geo["_inten_kin"][i][nz][inside] + ok = np.isfinite(meas) + g, meas, kin = g[ok], np.clip(meas[ok], 0, None), kin[ok] + y = np.sqrt(meas / meas.max()) + g2 = (g**2).sum(1) / (2 * k_wave) + + def model(x): + tilt, log_w, log_a = x[:2], x[2], x[3] + s = -(g2 + g @ tilt) + return ( + np.exp(log_a) * np.sqrt(kin / kin.max()) * np.exp(-0.25 * (s / np.exp(log_w)) ** 2) + ) + + best = None + for tx in np.linspace(-0.06, 0.06, 25): + for ty in np.linspace(-0.06, 0.06, 25): + for lw in (np.log(0.01), np.log(0.03)): + x = np.array([tx, ty, lw, 0.0]) + r = ((model(x) - y) ** 2).sum() + ((x[:2] / np.deg2rad(prior_deg)) ** 2).sum() + if best is None or r < best[0]: + best = (r, x) + lim = np.deg2rad(max_tilt_deg) + lo = np.array([-lim, -lim, np.log(0.003), -5.0]) + hi = np.array([lim, lim, np.log(0.05), 5.0]) + prior = np.deg2rad(prior_deg) + + def resid(x): + return np.concatenate([model(x) - y, x[:2] / prior]) + + sol = optimize.least_squares( + resid, np.clip(best[1], lo + 1e-9, hi - 1e-9), bounds=(lo, hi) + ) + return sol.x[:2], float(np.exp(sol.x[2])) + + def bragg_positions(self, i: int, k_max: float = 3.0) -> np.ndarray: + """Detector positions (row, col) of every zone reflection, direct beam included.""" + _, g2, _ = self._zone_reflections(self.zone_axes[i], k_max) + return self.geometry["centers"][i] + g2 @ self.geometry["matrices"][i].T + + def _q_zone(self, i: int, rows, cols) -> np.ndarray: + """Pixel coordinates -> in-plane scattering vector (..., 2) in the zone frame, 1/A.""" + p = np.stack([rows, cols], axis=-1) - self.geometry["centers"][i] + g = p @ np.linalg.inv(self.geometry["matrices"][i]).T + return g * self._a_crystal / self.lattice_parameter + + def _q_crystal(self, i: int, q2: np.ndarray) -> np.ndarray: + """In-plane zone-frame q (n, 2) -> crystal-frame q (n, 3) on the tilted Ewald sphere.""" + k_wave = 1.0 / self.wavelength + tilt = self.geometry["tilts"][i] + qz = -((q2**2).sum(1) / (2 * k_wave) + q2 @ tilt) + return np.column_stack([q2, qz]) @ _zone_frame(self.zone_axes[i]) + + # --------------------------------------------------------------------- mask + + def set_mask( + self, + bragg_radius: float = 0.08, + softness: float = 0.01, + q_max: float = 1.2, + center_radius: float = 0.25, + edge_px: int = 8, + tail_widths: tuple[float, ...] = (0.02, 0.06, 0.15), + ) -> "ReverseMonteCarlo": + """Diffuse-scattering weight, binned data, Bragg intensities and PSF tails. + + The weight is ``sigmoid((d - bragg_radius) / softness)``, with ``d`` + the distance (1/A) to the nearest reflection, the direct beam + included: 0 on every Bragg peak, 1 between them. Pixels beyond + ``q_max`` or within ``edge_px`` of the detector edge are dropped, and + a second sigmoid removes the direct beam's bloom out to + ``center_radius``. Each ``bin_factor`` square is reduced to its + weighted mean. + + Each reflection's integrated intensity weights the diffuse envelope. + The Bragg cores blurred by ``(1 + (r / width)^2)^-1.5`` kernels, one + per ``tail_widths`` (1/A), are background terms for the detector's + point-spread tails. + """ + from scipy.signal import fftconvolve + + b = self.bin_factor + out = dict( + bragg_radius=bragg_radius, softness=softness, q_max=q_max, center_radius=center_radius + ) + out.update(y=[], w=[], k=[], data=[], scale=[], bragg_k=[], bragg_intensity=[], tails=[]) + for i, im in enumerate(self.images): + ny, nx = (im.shape[0] // b) * b, (im.shape[1] // b) * b + rows, cols = np.mgrid[0:ny, 0:nx].astype(np.float64) + k = self._q_zone(i, rows, cols) + bragg = self.bragg_positions(i) + g_q = self._q_zone(i, bragg[:, 0], bragg[:, 1]) + d, j = cKDTree(g_q).query(k.reshape(-1, 2)) + d = d.reshape(ny, nx) + j = j.reshape(ny, nx) + q = np.linalg.norm(k, axis=-1) + w = ( + _sigmoid((d - bragg_radius) / softness) + * _sigmoid((q - center_radius) / softness) + * (q < q_max) + ) + w[:edge_px] = w[-edge_px:] = 0 + w[:, :edge_px] = w[:, -edge_px:] = 0 + y = im[:ny, :nx].astype(np.float64) + + # integrated Bragg intensities over the local ring median + n_g = len(g_q) + core = d < bragg_radius + ring = (d >= bragg_radius) & (d < 1.6 * bragg_radius) + ring_med = np.zeros(n_g) + jr, yr = j[ring], y[ring] + order = np.argsort(jr, kind="stable") + jr, yr = jr[order], yr[order] + starts = np.searchsorted(jr, np.arange(n_g)) + ends = np.searchsorted(jr, np.arange(n_g), side="right") + for g in np.nonzero(ends > starts)[0]: + ring_med[g] = np.median(yr[starts[g] : ends[g]]) + core_sig = np.where(core, np.clip(y - ring_med[j], 0, None), 0.0) + inten = np.bincount(j[core], weights=core_sig[core], minlength=n_g) + n_core = np.bincount(j[core], minlength=n_g) + seen = n_core > 0.5 * n_core.max() # whole spot on the detector + + wb = self._binned(w, "sum") + yb = np.where(wb > 0, self._binned(w * y, "sum") / np.maximum(wb, 1e-12), 0.0) + rb, cb = np.mgrid[0 : ny // b, 0 : nx // b].astype(np.float64) * b + (b - 1) / 2 + norm = (wb * yb).sum() / wb.sum() + tails = [] + for width in tail_widths: + gpx = width / self.sampling + half = int(min(6 * gpx, 200)) + rr = np.hypot(*np.mgrid[-half : half + 1, -half : half + 1]) + kern = (1 + (rr / gpx) ** 2) ** -1.5 + t = fftconvolve(core_sig, kern / kern.sum(), mode="same") + tails.append( + np.where(wb > 0, self._binned(w * t, "sum") / np.maximum(wb, 1e-12), 0.0) + / norm + ) + out["y"].append(yb / norm) + out["w"].append(wb / b**2) + out["k"].append(self._q_zone(i, rb, cb)) + out["data"].append(self._binned(y) / norm) + out["scale"].append(norm) + out["bragg_k"].append(g_q[seen]) + out["bragg_intensity"].append(inten[seen] / norm) + out["tails"].append(np.stack(tails)) + self.mask = out + return self + + # ---------------------------------------------------------------- supercell + + def build_supercell( + self, + cells: int = 16, + seed: int | None = 0, + symmetrize: bool = True, + debye_waller: float = 0.5, + envelope: str = "measured", + resolution: float = 0.75, + shared_scale: bool = True, + device: str | None = None, + ) -> "ReverseMonteCarlo": + """Random supercell of ``cells^3`` unit cells at the crystal's composition. + + Parameters + ---------- + cells : int + Unit cells along each cube edge. The diffuse model is sampled + every ``1 / (cells a)`` in reciprocal space. + symmetrize : bool + Average every pattern over the 24 cubic rotations of the supercell. + debye_waller : float + Isotropic B (A^2) damping the diffuse intensity. + envelope : {"measured", "kinematic"} + Chemical diffuse intensity is periodic in the reciprocal lattice, + so diffuse scattering out of every Bragg beam g lands on the same + |G|^2 and only its form factor changes: the intensity is + ``|G(q)|^2 sum_g P_g |f_B - f_A|^2(q - g)``. "measured" takes P_g + from the integrated Bragg intensities of each pattern (dynamical + redistribution, tilt and thickness included); "kinematic" keeps + only the direct beam. + resolution : float + Gaussian sigma, in supercell reciprocal-grid steps, with which + each pixel reads the diffuse grid (27 nearest points). A finite + supercell's |G|^2 is speckle; reading it through a kernel of about + one step damps the speckle and stops the fit chasing single grid + points (most visibly on mirror planes, where fewer symmetry images + average). + shared_scale : bool + One diffuse scale for every pattern. The envelope carries each + pattern's absolute Bragg intensities, so diffuse scattering out of + a beam is proportional to that beam; a per-pattern scale lets a + pattern's smooth background swallow its diffuse intensity. + device : str, optional + torch device; default cuda, then mps, then cpu. + """ + if self.mask is None: + raise RuntimeError("set_mask first") + rng = np.random.default_rng(seed) + self.rng = rng + d = self.grid_divisor + n = cells * d + m = np.stack(np.meshgrid(*(np.arange(cells),) * 3, indexing="ij"), -1).reshape(-1, 3) + x = (m[:, None, :] * d + self.site_grid[None]).reshape(-1, 3) + n_sites = len(x) + n_b = int(round(self.concentration * n_sites)) + sigma = np.zeros(n_sites, dtype=bool) + sigma[rng.choice(n_sites, n_b, replace=False)] = True + self.cells, self.grid_size = cells, n + self.site_x = x + self.sigma = sigma + self.debye_waller = float(debye_waller) + self.symmetrize = bool(symmetrize) + self.envelope = envelope + self.resolution = float(resolution) + self.shared_scale = bool(shared_scale) + self.device = torch.device(device or _default_device()) + self._setup_forward() + print(f"{n_sites} sites, {n}^3 grid, {int(self._fit.sum())} fitted pixels, {self.device}") + return self + + def _setup_forward(self): + """Ewald-sphere sampling of the supercell grid for every binned pixel.""" + n = self.grid_size + dev = self.device + cols_w, vals_w, u_all, basis_all, pix_image = [], [], [], [], [] + stencil = np.array(list(product((-1, 0, 1), repeat=3))) + for i, kk in enumerate(self.mask["k"]): + q2 = kk.reshape(-1, 2) + h = self._q_crystal(i, q2) * self.lattice_parameter * self.cells + h0 = np.round(h).astype(np.int64) + f = h - h0 + wts = np.exp( + -0.5 + * ((f[:, None, :] - stencil[None]) ** 2).sum(-1) + / max(self.resolution, 0.3) ** 2 + ) + wts /= wts.sum(1, keepdims=True) + idx = np.mod(h0[:, None, :] + stencil[None], n) + cols_w.append((idx[..., 0] * n + idx[..., 1]) * n + idx[..., 2]) + vals_w.append(wts) + u, tds = self._envelope(i, q2) + u_all.append(u) + tails = self.mask["tails"][i].reshape(len(self.mask["tails"][i]), -1).T + qm = np.linalg.norm(q2, axis=1) + halos = self._phonon_halos(i, q2) + rings = self._powder_rings(qm) + basis_all.append(np.column_stack([np.ones_like(qm), qm, tds, halos, rings, tails])) + pix_image.append(np.full(len(h), i)) + cols = np.concatenate(cols_w) + vals = np.concatenate(vals_w) + used, inv = np.unique(cols, return_inverse=True) + cols_used = inv.reshape(cols.shape) + n_pix = len(cols) + self._W_all = sparse.csr_matrix( + (vals.ravel(), (np.repeat(np.arange(n_pix), cols.shape[1]), cols_used.ravel())), + shape=(n_pix, len(used)), + ) + # symmetry: model reads mean_k I(S_k h) at each used h + ops = cubic_rotations() if self.symmetrize else np.eye(3, dtype=int)[None] + hh = np.stack(np.unravel_index(used, (n,) * 3), -1) + sym_flat = np.stack( + [(lambda s: (s[:, 0] * n + s[:, 1]) * n + s[:, 2])(np.mod(hh @ op.T, n)) for op in ops] + ) + needed, inv_s = np.unique(sym_flat, return_inverse=True) + self._needed = needed + self._sym_index = torch.as_tensor(inv_s.reshape(sym_flat.shape), device=dev) + self._u_all = np.concatenate(u_all) + self._basis_all = np.concatenate(basis_all) + self._pix_image = np.concatenate(pix_image) + y = np.concatenate([a.ravel() for a in self.mask["y"]]) + w = np.concatenate([a.ravel() for a in self.mask["w"]]) + self._y_all, self._w_all = y, w + fit = w > 1e-3 + self._fit = fit + self._Wc = torch.as_tensor(cols_used[fit], device=dev) + self._Wv = torch.as_tensor(vals[fit], dtype=torch.float32, device=dev) + self._y = torch.as_tensor(y[fit], dtype=torch.float32, device=dev) + self._w = torch.as_tensor(w[fit], dtype=torch.float32, device=dev) + self._img = torch.as_tensor(self._pix_image[fit], device=dev) + self._u = torch.as_tensor(self._u_all[fit], dtype=torch.float32, device=dev) + hn = np.stack(np.unravel_index(needed, (n,) * 3), -1) + self._h_needed = torch.as_tensor(hn, device=dev) + ang = 2 * np.pi * np.arange(n) / n + self._cos = torch.as_tensor(np.cos(ang), dtype=torch.float32, device=dev) + self._sin = torch.as_tensor(np.sin(ang), dtype=torch.float32, device=dev) + self._x = torch.as_tensor(self.site_x, device=dev) + self._recompute_G() + # direct-beam Lorentzian half width and wide Gaussian sigma, 1/A + self.background_sigmas = [(0.1, 0.8) for _ in self.images] + self._sigma_bounds = (np.array([0.01, 0.3]), np.array([1.0, 3.0])) + self.coefficients = None + + def _read(self, i_used: torch.Tensor) -> torch.Tensor: + """Grid values on the used points (..., n_used) -> kernel-weighted values on fitted pixels.""" + return (i_used[..., self._Wc] * self._Wv).sum(-1) + + def _envelope(self, i: int, q2: np.ndarray, max_beams: int = 40): + """Diffuse and thermal-diffuse envelopes summed over the Bragg beams of pattern i.""" + if self.envelope == "measured": + g = self.mask["bragg_k"][i] + p = self.mask["bragg_intensity"][i] + order = np.argsort(p)[::-1][:max_beams] + g, p = g[order], p[order] + keep = p > 0.002 * p.max() + g, p = g[keep], p[keep] + elif self.envelope == "kinematic": + g, p = np.zeros((1, 2)), np.array([self.mask["bragg_intensity"][i].sum()]) + else: + raise ValueError(f"unknown envelope {self.envelope!r}") + z = torch.tensor(self.numbers) + c = self.concentration + u = np.zeros(len(q2)) + tds = np.zeros(len(q2)) + for gi, pi in zip(g, p): + qm = np.linalg.norm(q2 - gi, axis=1) + fe = electron_scattering_factor(z, torch.as_tensor(qm, dtype=torch.float64)).numpy() + dw = np.exp(-0.5 * self.debye_waller * qm**2) + u += pi * (fe[1] - fe[0]) ** 2 * dw + tds += pi * ((1 - c) * fe[0] ** 2 + c * fe[1] ** 2) * (1 - dw) + return u, tds + + def _powder_rings(self, q: np.ndarray, width: float = 0.025, k_max: float = 1.5) -> np.ndarray: + """Powder rings of the average crystal about the direct beam, ``sum_g |F_g|^2 / g^2`` + broadened by ``width`` (1/A): misoriented grains or a damaged surface layer.""" + self.crystal.calculate_structure_factors(k_max) + g = self.crystal.g_len.numpy() * self._a_crystal / self.lattice_parameter + f2 = self.crystal.struct_factors_int.numpy() + keep = g > 1e-6 + g, f2 = g[keep], f2[keep] + out = np.zeros_like(q) + for gi, fi in zip(g, f2): + out += fi / gi**2 * np.exp(-0.5 * ((q - gi) / width) ** 2) + return out / out.max() + + def _phonon_halos(self, i: int, q2: np.ndarray, widths=(0.04, 0.12)) -> np.ndarray: + """Thermal diffuse halos about every Bragg spot: ``sum_g P_g k^2 / (|q - g|^2 + k^2)`` + per width ``k`` (1/A), acoustic phonons concentrating TDS next to each reflection.""" + g = self.mask["bragg_k"][i] + p = self.mask["bragg_intensity"][i] + nz = np.linalg.norm(g, axis=1) > 1e-6 + g, p = g[nz], p[nz] / max(p[nz].sum(), 1e-12) + out = np.zeros((len(q2), len(widths))) + for gi, pi in zip(g, p): + d2 = ((q2 - gi) ** 2).sum(1) + for n, k in enumerate(widths): + out[:, n] += pi * k**2 / (d2 + k**2) + return out + + def _phases(self, sites: torch.Tensor): + """cos and sin of 2 pi h.x / N for the given sites on every needed grid point.""" + idx = (self._x[sites] @ self._h_needed.T) % self.grid_size # (B, n_needed) + return self._cos[idx], self._sin[idx] + + def _recompute_G(self): + n = self.grid_size + a = np.zeros((n,) * 3) + xs = self.site_x + a[xs[:, 0], xs[:, 1], xs[:, 2]] = self.sigma - self.concentration + G = np.fft.fftn(a).ravel()[self._needed] + self._Gr = torch.as_tensor(G.real, dtype=torch.float32, device=self.device) + self._Gi = torch.as_tensor(G.imag, dtype=torch.float32, device=self.device) + + def _diffuse_used(self, Gr=None, Gi=None) -> torch.Tensor: + """Symmetrized |G|^2 per site on the used grid points.""" + Gr = self._Gr if Gr is None else Gr + Gi = self._Gi if Gi is None else Gi + inten = (Gr**2 + Gi**2) / len(self.sigma) + return inten[..., self._sym_index].mean(dim=-2) + + # --------------------------------------------------------------- background + + def _bg_basis(self, i: int, sel: np.ndarray) -> np.ndarray: + q = self._basis_all[sel, 1] + s1, s2 = self.background_sigmas[i] + return np.column_stack( + [ + np.ones_like(q), + 1.0 / (1.0 + (q / s1) ** 2), + np.exp(-0.5 * (q / s2) ** 2), + self._basis_all[sel, 2:], + ] + ) + + def _solve_linear(self, diffuse_fit: np.ndarray, refit_sigmas: bool = False): + """Scale, background amplitudes (and optionally widths) per image, weighted least squares.""" + y = self._y_all[self._fit] + w = self._w_all[self._fit] + img = self._pix_image[self._fit] + sel_all = np.nonzero(self._fit)[0] + coefs, loss = [], 0.0 + for i in range(len(self.images)): + m = img == i + sel = sel_all[m] + sw = np.sqrt(w[m]) + + def solve(sig): + sig = np.clip(sig, *self._sigma_bounds) + self.background_sigmas[i] = tuple(float(v) for v in sig) + X = np.column_stack([diffuse_fit[m], self._bg_basis(i, sel)]) + lo = np.zeros(X.shape[1]) + lo[1] = -np.inf + x = _lsq_bounded(X, y[m], sw, lo) + return x, float(((X @ x - y[m]) ** 2 * w[m]).sum()) + + if refit_sigmas: + r = optimize.minimize( + lambda ls: solve(np.exp(ls))[1], + np.log(self.background_sigmas[i]), + method="Nelder-Mead", + options=dict(xatol=1e-3, fatol=1e-6, maxiter=200), + ) + solve(np.exp(r.x)) + c, l_i = solve(self.background_sigmas[i]) + coefs.append(c) + loss += l_i + self.coefficients = np.stack(coefs) + if not getattr(self, "shared_scale", False): + return loss + + # one diffuse scale for all patterns, backgrounds per pattern + blocks = [self._bg_basis(i, sel_all[img == i]) for i in range(len(self.images))] + nb = blocks[0].shape[1] + X = np.zeros((len(y), 1 + nb * len(blocks))) + X[:, 0] = diffuse_fit + lo = np.zeros(X.shape[1]) + for i, bl in enumerate(blocks): + X[img == i, 1 + nb * i : 1 + nb * (i + 1)] = bl + lo[1 + nb * i] = -np.inf + sw = np.sqrt(w) + x = _lsq_bounded(X, y, sw, lo) + if x[0] <= 0: + # a random supercell explains nothing yet: start the scale from the background residual + r = y - X[:, 1:] @ x[1:] + x[0] = max((w * r * diffuse_fit).sum() / max((w * diffuse_fit**2).sum(), 1e-30), 0) + for i in range(len(blocks)): + self.coefficients[i, 0] = x[0] + self.coefficients[i, 1:] = x[1 + nb * i : 1 + nb * (i + 1)] + return float(((X @ x - y) ** 2 * w).sum()) + + def fit_background(self) -> float: + """Fit the diffuse scale and, per pattern, a constant, a direct-beam Lorentzian, a wide + Gaussian, Einstein thermal diffuse, phonon halos about each reflection, powder rings of + the average crystal and Bragg tails.""" + loss = self._solve_linear(self._model_diffuse(), refit_sigmas=True) + self._update_residual() + print( + "background widths (1/A): " + + ", ".join( + f"{n} {s[0]:.3f}/{s[1]:.3f}" for n, s in zip(self.names, self.background_sigmas) + ) + ) + return loss + + def _model_diffuse(self) -> np.ndarray: + """u(q) * (W I_sym) on the fitted pixels (before scale).""" + return (self._u * self._read(self._diffuse_used())).cpu().numpy().astype(np.float64) + + def _update_residual(self): + dev = self.device + img = self._pix_image[self._fit] + sel = np.nonzero(self._fit)[0] + bg = np.zeros(len(sel)) + for i in range(len(self.images)): + m = img == i + bg[m] = self._bg_basis(i, sel[m]) @ self.coefficients[i, 1:] + self._bg = torch.as_tensor(bg, dtype=torch.float32, device=dev) + c = torch.as_tensor(self.coefficients[:, 0], dtype=torch.float32, device=dev) + self._scale = c[self._img] + model = self._scale * self._u * self._read(self._diffuse_used()) + self._bg + self._r = self._y - model + return float((self._w * self._r**2).sum()) + + # ---------------------------------------------------------------------- RMC + + def run( + self, + n_sweeps: int = 20, + batch: int = 64, + temperature: float = 0.05, + refit_every: int = 2, + progress: bool = True, + ) -> "ReverseMonteCarlo": + """Composition-conserving swaps until the diffuse fit converges. + + Each batch proposes ``batch`` disjoint swaps against the current + supercell and scores each exactly. Metropolis acceptance at + ``temperature`` (a fraction of the median score change, falling + linearly to 0); the accepted swaps are applied together, capped at a + number that adapts so the joint step never raises the loss. A sweep is + one proposal per site. Scale and background are refit every + ``refit_every`` sweeps. + """ + if self.coefficients is None: + self.fit_background() + n_sites = len(self.sigma) + loss = self._update_residual() + if not self.loss_history: + self.loss_history.append(loss) + cap = max(batch // 8, 1) + t0 = None + n_batches = max(n_sites // (2 * batch), 1) + sweeps = tqdm(range(n_sweeps), desc="RMC sweeps", disable=not progress) + su = None + for sweep in sweeps: + accepted = 0 + for _ in range(n_batches): + if su is None: + su = self._scale * self._u + on = np.nonzero(~self.sigma)[0] + off = np.nonzero(self.sigma)[0] + j_on = self.rng.choice(on, batch, replace=False) + j_off = self.rng.choice(off, batch, replace=False) + c1, s1 = self._phases(torch.as_tensor(j_on, device=self.device)) + c2, s2 = self._phases(torch.as_tensor(j_off, device=self.device)) + dGr = c1 - c2 + dGi = s2 - s1 + d_int = (2 * (self._Gr * dGr + self._Gi * dGi) + dGr**2 + dGi**2) / n_sites + d_used = d_int[:, self._sym_index].mean(dim=1) # (B, n_used) + dm = su * self._read(d_used) # (B, n_fit) + dL = (self._w * (dm**2 - 2 * self._r * dm)).sum(-1) + dl = dL.cpu().numpy() + if not t0: + t0 = float(np.median(np.abs(dl))) + temp = temperature * t0 * (1 - sweep / n_sweeps) + if temp > 0: + ok = (dl < 0) | (self.rng.random(batch) < np.exp(-np.clip(dl / temp, 0, 50))) + else: + ok = dl < 0 + pick = np.nonzero(ok)[0] + pick = pick[np.argsort(dl[pick])][:cap] + if len(pick) == 0: + continue + pk = torch.as_tensor(pick, device=self.device) + Gr_new = self._Gr + dGr[pk].sum(0) + Gi_new = self._Gi + dGi[pk].sum(0) + r_new = self._y - su * self._read(self._diffuse_used(Gr_new, Gi_new)) - self._bg + loss_new = float((self._w * r_new**2).sum()) + if loss_new > loss + max(temp, 0.0) * len(pick) and len(pick) > 1: + cap = max(cap // 2, 1) + continue + self._Gr, self._Gi, self._r = Gr_new, Gi_new, r_new + self.sigma[j_on[pick]] = True + self.sigma[j_off[pick]] = False + loss = loss_new + accepted += len(pick) + cap = min(int(cap * 1.25) + 1, batch) + if (sweep + 1) % refit_every == 0: + self._recompute_G() + self._solve_linear(self._model_diffuse()) + loss = self._update_residual() + su = None + self.loss_history.append(loss) + sweeps.set_postfix(loss=f"{loss:.4g}", accepted=accepted) + self._recompute_G() + self._solve_linear(self._model_diffuse()) + self.loss_history[-1] = self._update_residual() + return self + + # ---------------------------------------------------------------- analysis + + def model_images(self, diffuse_only: bool = False) -> list[np.ndarray]: + """Model on the binned grid of every pattern (data units): scaled diffuse + background, + or the scaled diffuse term alone.""" + i_used = self._diffuse_used().cpu().numpy().astype(np.float64) + diffuse = self._u_all * (self._W_all @ i_used) + out = [] + for i, k in enumerate(self.mask["k"]): + m = self._pix_image == i + sel = np.nonzero(m)[0] + c = self.coefficients[i] + img = c[0] * diffuse[m] + if not diffuse_only: + img = img + self._bg_basis(i, sel) @ c[1:] + out.append(img.reshape(k.shape[:2])) + return out + + def background_images(self) -> list[np.ndarray]: + """Fitted background (constant, Gaussians, thermal diffuse, Bragg tails) per pattern.""" + full = self.model_images() + diffuse = self.model_images(diffuse_only=True) + return [f - d for f, d in zip(full, diffuse)] + + def warren_cowley(self, n_shells: int = 6) -> dict: + """Warren-Cowley alpha of the B species about B for the first neighbour shells.""" + n = self.grid_size + d = self.grid_divisor + occ = np.zeros((n,) * 3) + xs = self.site_x + occ[xs[:, 0], xs[:, 1], xs[:, 2]] = self.sigma + site = np.zeros((n,) * 3, dtype=bool) + site[xs[:, 0], xs[:, 1], xs[:, 2]] = True + fo = np.fft.fftn(occ) + fs = np.fft.fftn(site) + bb = np.real(np.fft.ifftn(fo * np.conj(fo))) + ss = np.real(np.fft.ifftn(fs * np.conj(fs))) + v = np.stack(np.meshgrid(*(np.fft.fftfreq(n, 1 / n),) * 3, indexing="ij"), -1) + r = np.linalg.norm(v, axis=-1) / d * self.lattice_parameter + valid = ss > 0.5 + radii = np.unique(np.round(r[valid], 4))[1 : n_shells + 1] + c = self.concentration + alpha = [] + for rad in radii: + m = valid & (np.abs(r - rad) < 1e-3) + p_bb = bb[m].sum() / ss[m].sum() / c # P(B neighbour | B) + alpha.append((p_bb - c) / (1 - c)) + return dict( + radius=radii, alpha=np.asarray(alpha), pair=f"{self.species[1]}-{self.species[1]}" + ) + + def diffuse_section(self, normal=(0, 0, 1), extent: float = 2.0, smooth: bool = True): + """Symmetrized supercell diffuse intensity on a reciprocal-lattice plane, in Laue units. + + Parameters + ---------- + normal : (h, k, l) + Plane normal; the plane passes through the origin. + extent : float + Half width in units of the cubic reciprocal lattice vector 1/a. + smooth : bool + Blur by the fit's ``resolution`` kernel. + + Returns + ------- + image, (u, v) in-plane axes (crystal frame, unit vectors), distance of each pixel from + the nearest reciprocal-lattice node (1/a units) + """ + n = self.grid_size + a = np.zeros((n,) * 3) + xs = self.site_x + a[xs[:, 0], xs[:, 1], xs[:, 2]] = self.sigma - self.concentration + inten = np.abs(np.fft.fftn(a)) ** 2 / len(self.sigma) + ops = cubic_rotations() if self.symmetrize else np.eye(3, dtype=int)[None] + idx = np.stack(np.meshgrid(*(np.arange(n),) * 3, indexing="ij"), -1).reshape(-1, 3) + sym = np.zeros(n**3) + for op in ops: + s = np.mod(idx @ op.T, n) + sym += inten[s[:, 0], s[:, 1], s[:, 2]] + sym = (sym / len(ops)).reshape((n,) * 3) + if smooth and self.resolution > 0: + sym = ndimage.gaussian_filter(sym, self.resolution, mode="wrap") + laue = self.concentration * (1 - self.concentration) + frame = _zone_frame(normal) + u, v = frame[0], frame[1] + steps = np.arange(-extent * self.cells, extent * self.cells + 1) + su, sv = np.meshgrid(steps, steps, indexing="ij") + pts = su[..., None] * u + sv[..., None] * v # grid units (cells per 1/a) + img = ndimage.map_coordinates( + sym, np.moveaxis(pts, -1, 0).reshape(3, -1), order=1, mode="grid-wrap" + ).reshape(su.shape) + frac = pts / self.cells + node_dist = np.linalg.norm(frac - np.round(frac), axis=-1) # 1/a units + return img / laue, (u, v), node_dist + + # ----------------------------------------------------------------- plotting + + def plot_images( + self, quantiles: tuple[float, float] = (0.86, 0.98), cmap: str = "turbo_black", **kwargs + ): + """Binned patterns on a linear scale. Once a mask is set, the scale spans min to max of the + diffuse region of each pattern; before that, ``quantiles`` of the whole pattern.""" + from quantem.core.visualization import show_2d + + arrays, norms = [], [] + for i, im in enumerate(self.images): + b = self._binned(im) + if self.mask is not None: + q = np.linalg.norm(self.mask["k"][i], axis=-1) + sel = (self.mask["w"][i] > 0.5) & (q < self.mask["q_max"]) + vals = b[sel] + norms.append(dict(interval_type="manual", vmin=vals.min(), vmax=vals.max())) + else: + lo, hi = np.quantile(b, quantiles) + norms.append(dict(interval_type="manual", vmin=lo, vmax=hi)) + arrays.append(b) + return show_2d( + arrays, + title=self.names, + norm=norms, + cmap=cmap, + axsize=kwargs.pop("axsize", (6, 4)), + **kwargs, + ) + + def plot_geometry(self, power: float = 0.3, q_view: float = 1.4, **kwargs): + """Each pattern with its fitted reflections (red), direct beam (cyan) and Laue circle (yellow).""" + import matplotlib.patches as mpatches + + from quantem.core.visualization import show_2d + + fig, axs = show_2d( + self.images, + title=self.names, + norm={ + "stretch_type": "power", + "power": power, + "lower_quantile": 0.05, + "upper_quantile": 0.999, + }, + axsize=kwargs.pop("axsize", (5, 5)), + **kwargs, + ) + axs = np.atleast_1d(axs).ravel() + r = 0.03 / self.sampling + k_wave = 1.0 / self.wavelength + for i, ax in enumerate(axs): + p = self.bragg_positions(i) + c = self.geometry["centers"][i] + for pr, pc in p: + ax.add_patch(mpatches.Circle((pc, pr), r, fill=False, color="tab:red", lw=1.0)) + ax.add_patch(mpatches.Circle((c[1], c[0]), 1.5 * r, fill=False, color="cyan", lw=1.5)) + tilt = self.geometry["tilts"][i] + if np.linalg.norm(tilt) > 0: + th = np.linspace(0, 2 * np.pi, 361) + circ = -k_wave * tilt + k_wave * np.linalg.norm(tilt) * np.column_stack( + [np.cos(th), np.sin(th)] + ) + px = ( + c + + (circ * self.lattice_parameter / self._a_crystal) + @ self.geometry["matrices"][i].T + ) + ax.plot(px[:, 1], px[:, 0], "--", color="yellow", lw=1.0) + half = q_view / self.sampling + ax.set_xlim(c[1] - half, c[1] + half) + ax.set_ylim(c[0] + half, c[0] - half) + return fig, axs + + def plot_mask(self, **kwargs): + """Binned diffuse weight of every pattern.""" + from quantem.core.visualization import show_2d + + return show_2d( + self.mask["w"], + title=self.names, + cmap="gray", + axsize=kwargs.pop("axsize", (4, 2.7)), + **kwargs, + ) + + def plot_fit( + self, + diffuse_only: bool = False, + sigma: float = 0.7, + quantiles: tuple[float, float] = (0.01, 0.99), + cmap: str = "turbo_black", + **kwargs, + ): + """Experiment (left) and model (right) for every zone on one linear scale per row. + + Default: the binned pattern (blurred by ``sigma`` binned pixels) and + the full model. ``diffuse_only``: the experiment minus the fitted + background next to the supercell's diffuse term, then their + difference on a diverging map (white = no difference). The scale + spans ``quantiles`` of the experiment inside the diffuse mask (weight + > 0.5), which excludes the Bragg peaks and the direct-beam bloom; + masked pixels are black in the diffuse view. Panels are cropped to + ``q_max``. + """ + import matplotlib + + from quantem.core.visualization import show_2d + + model = self.model_images(diffuse_only=diffuse_only) + background = self.background_images() if diffuse_only else None + cmap_obj = matplotlib.colormaps[cmap].with_extremes(bad="black") + diverging = matplotlib.colormaps["RdBu_r"].with_extremes(bad="black") + rows, titles, norms, cmaps = [], [], [], [] + for i in range(len(self.images)): + q = np.linalg.norm(self.mask["k"][i], axis=-1) + inside = q < self.mask["q_max"] + w = self.mask["w"][i] + sel = (w > 0.5) & inside + rr, cc = np.nonzero(inside) + crop = (slice(rr.min(), rr.max() + 1), slice(cc.min(), cc.max() + 1)) + if diffuse_only: + exp = self.mask["y"][i] - background[i] + if sigma: + exp = _nan_blur(np.where(w > 0.2, exp, np.nan), sigma) + exp = np.where(sel, exp, np.nan) + mod = np.where(sel, model[i], np.nan) + lo, hi = np.nanquantile(exp, quantiles) + h = 0.5 * (hi - lo) + rows.append([exp[crop], mod[crop], (exp - mod)[crop]]) + titles.append( + [ + f"{self.names[i]} experiment - background", + f"{self.names[i]} model", + "experiment - background - model", + ] + ) + norms.append( + [dict(interval_type="manual", vmin=lo, vmax=hi)] * 2 + + [dict(interval_type="manual", vmin=-h, vmax=h)] + ) + cmaps.append([cmap_obj, cmap_obj, diverging]) + else: + exp = self.mask["data"][i] + if sigma: + exp = ndimage.gaussian_filter(exp, sigma) + lo, hi = np.quantile(exp[sel], quantiles) + rows.append( + [np.where(inside, exp, np.nan)[crop], np.where(inside, model[i], np.nan)[crop]] + ) + titles.append([f"{self.names[i]} experiment", f"{self.names[i]} model"]) + norms.append([dict(interval_type="manual", vmin=lo, vmax=hi)] * 2) + cmaps.append([cmap_obj, cmap_obj]) + return show_2d( + rows, + title=titles, + norm=norms, + cmap=cmaps, + axsize=kwargs.pop("axsize", (4, 4)), + **kwargs, + ) + + def plot_sro(self, n_shells: int = 8, extent: float = 2.0, layer: int = 0): + """Short-range order three ways. + + Left: Warren-Cowley alpha against neighbour distance (alpha < 0 + prefers unlike neighbours, > 0 like). Middle: the symmetrized diffuse + intensity of the supercell in Laue units (1 = random alloy) on the + (001) and (1-10) reciprocal planes, where ordering shows as diffuse + maxima at special points, e.g. 100 (B2-type) or 1/2 1/2 1/2 (D0_3); the color + scale is set away from the reciprocal-lattice nodes, so clustering (small-q + intensity on the nodes) saturates. + Right: one (001) layer of the supercell, B atoms dark, and its local + B concentration over the first two shells. + """ + import matplotlib.pyplot as plt + + sro = self.warren_cowley(n_shells) + sec_001, _, d_001 = self.diffuse_section((0, 0, 1), extent) + sec_110, _, d_110 = self.diffuse_section((1, -1, 0), extent) + n = self.grid_size + d = self.grid_divisor + occ = np.full((n,) * 3, np.nan) + xs = self.site_x + occ[xs[:, 0], xs[:, 1], xs[:, 2]] = self.sigma + site = ~np.isnan(occ) + local = ndimage.gaussian_filter(np.nan_to_num(occ), d * 0.6, mode="wrap") / np.maximum( + ndimage.gaussian_filter(site.astype(float), d * 0.6, mode="wrap"), 1e-9 + ) + z = layer * d + sl = occ[:, :, z] + sl_sites = site[:, :, z] + + fig, axs = plt.subplots(1, 5, figsize=(22, 4.4)) + ax = axs[0] + ax.axhline(0, color="0.6", lw=0.8) + ax.stem(sro["radius"], sro["alpha"], basefmt=" ") + ax.set_xlabel("neighbour distance (A)") + ax.set_ylabel(f"Warren-Cowley alpha ({sro['pair']})") + lim = max(0.1, 1.2 * np.abs(sro["alpha"]).max()) + ax.set_ylim(-lim, lim) + ext = [-extent, extent, -extent, extent] + # scale to the diffuse between the nodes, not the small-q peaks on them + between = np.concatenate([sec_001[d_001 > 0.2], sec_110[d_110 > 0.2]]) + vmax = np.quantile(between, 0.995) + for ax, sec, title, xl, yl in ( + (axs[1], sec_001, "(001) section", "h", "k"), + (axs[2], sec_110, "(1-10) section", "[001]", "[110]/sqrt2"), + ): + im = ax.imshow(sec.T, origin="lower", extent=ext, cmap="magma", vmin=0, vmax=vmax) + ax.set_title(f"{title}, Laue units") + ax.set_xlabel(xl) + ax.set_ylabel(yl) + fig.colorbar(im, ax=ax, fraction=0.046) + rr, cc = np.nonzero(sl_sites) + axs[3].scatter( + cc / d, rr / d, c=sl[rr, cc], cmap="gray_r", s=6, vmin=-0.2, vmax=1.2, marker="s" + ) + axs[3].set_aspect("equal") + axs[3].set_title(f"(001) layer, {self.species[1]} dark") + axs[3].set_xlabel("cells") + im = axs[4].imshow( + local[:, :, z], + cmap="RdBu_r", + vmin=self.concentration - 0.3, + vmax=self.concentration + 0.3, + origin="lower", + extent=[0, self.cells, 0, self.cells], + ) + axs[4].set_title(f"local {self.species[1]} fraction") + fig.colorbar(im, ax=axs[4], fraction=0.046) + fig.tight_layout() + return fig, axs + + def plot_loss(self): + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(5, 3)) + ax.plot(self.loss_history, "k.-") + ax.set_xlabel("sweep") + ax.set_ylabel("weighted loss") + ax.set_yscale("log") + fig.tight_layout() + return fig, ax + + +def _lsq_bounded(X: np.ndarray, y: np.ndarray, sw: np.ndarray, lo: np.ndarray) -> np.ndarray: + """Weighted least squares with lower bounds, columns normalized for conditioning.""" + Xw = X * sw[:, None] + norm = np.linalg.norm(Xw, axis=0) + norm[norm == 0] = 1.0 + res = optimize.lsq_linear(Xw / norm, y * sw, bounds=(lo * norm, np.inf)) + return res.x / norm + + +def _integrate_spots(im: np.ndarray, positions: np.ndarray, radius: float) -> np.ndarray: + """Integrated intensity inside ``radius`` px of each position, over the median of the ring + out to 1.6 radius; NaN for spots off the detector.""" + out = np.full(len(positions), np.nan) + r_out = int(np.ceil(1.6 * radius)) + 1 + yy, xx = np.mgrid[-r_out : r_out + 1, -r_out : r_out + 1] + for n, (pr, pc) in enumerate(positions): + r0, c0 = int(round(pr)), int(round(pc)) + if ( + r0 - r_out < 0 + or c0 - r_out < 0 + or r0 + r_out >= im.shape[0] + or c0 + r_out >= im.shape[1] + ): + continue + win = im[r0 - r_out : r0 + r_out + 1, c0 - r_out : c0 + r_out + 1] + dist = np.hypot(yy + r0 - pr, xx + c0 - pc) + bg = np.median(win[(dist >= radius) & (dist < 1.6 * radius)]) + out[n] = (win[dist < radius] - bg).sum() + return out + + +def _nan_blur(a: np.ndarray, sigma: float) -> np.ndarray: + """Gaussian blur that ignores NaNs.""" + ok = np.isfinite(a) + num = ndimage.gaussian_filter(np.where(ok, a, 0.0), sigma) + den = ndimage.gaussian_filter(ok.astype(float), sigma) + return np.where(ok, num / np.maximum(den, 1e-12), np.nan) diff --git a/tests/diffraction/test_reverse_monte_carlo.py b/tests/diffraction/test_reverse_monte_carlo.py new file mode 100644 index 000000000..abc523f67 --- /dev/null +++ b/tests/diffraction/test_reverse_monte_carlo.py @@ -0,0 +1,109 @@ +import numpy as np +import pytest +import torch + +from quantem.diffraction import Crystal, ReverseMonteCarlo +from quantem.diffraction.reverse_monte_carlo import cubic_rotations + +CIF = """data_VNb +_cell_length_a 3.2 +_cell_length_b 3.2 +_cell_length_c 3.2 +_cell_angle_alpha 90 +_cell_angle_beta 90 +_cell_angle_gamma 90 +_symmetry_space_group_name_H-M 'I m -3 m' +_symmetry_Int_Tables_number 229 +loop_ +_atom_site_label +_atom_site_type_symbol +_atom_site_fract_x +_atom_site_fract_y +_atom_site_fract_z +_atom_site_occupancy +Nb1 Nb 0 0 0 0.7 +V1 V 0 0 0 0.3 +""" + + +@pytest.fixture +def rmc(tmp_path): + path = tmp_path / "vnb.cif" + path.write_text(CIF) + rng = np.random.default_rng(1) + images = [rng.random((64, 64)) + 1.0 for _ in range(2)] + out = ReverseMonteCarlo.from_images( + images, zone_axes=[(0, 0, 1), (0, 1, 1)], sampling=0.02, bin_factor=2 + ) + out.set_crystal(Crystal.from_cif(path, verbose=False)) + out.geometry = dict( + centers=[np.array([32.0, 32.0])] * 2, + matrices=[np.eye(2) / 0.02, np.array([[0.0, -1.0], [1.0, 0.0]]) / 0.02], + tilts=[np.zeros(2), np.array([0.01, 0.0])], + ) + out.set_mask(bragg_radius=0.06, q_max=0.6, center_radius=0.1, edge_px=2) + out.build_supercell(cells=4, seed=0, device="cpu") + out.fit_background() + return out + + +def test_cubic_rotations(): + ops = cubic_rotations() + assert ops.shape == (24, 3, 3) + assert np.allclose([np.linalg.det(o) for o in ops], 1) + assert len({o.tobytes() for o in ops}) == 24 + + +def test_binary_site_and_composition(rmc): + assert rmc.species == ["Nb", "V"] + assert rmc.concentration == pytest.approx(0.3) + assert len(rmc.sigma) == 2 * 4**3 + assert rmc.sigma.sum() == round(0.3 * len(rmc.sigma)) + + +def test_swap_score_matches_recompute(rmc): + """The incremental loss change of one swap equals the loss after recomputing G from scratch.""" + loss0 = rmc._update_residual() + j_on = int(np.nonzero(~rmc.sigma)[0][0]) + j_off = int(np.nonzero(rmc.sigma)[0][0]) + n_sites = len(rmc.sigma) + c1, s1 = rmc._phases(torch.tensor([j_on])) + c2, s2 = rmc._phases(torch.tensor([j_off])) + dGr, dGi = c1 - c2, -s1 + s2 + d_int = (2 * (rmc._Gr * dGr + rmc._Gi * dGi) + dGr**2 + dGi**2) / n_sites + d_used = d_int[:, rmc._sym_index].mean(dim=1) + dm = rmc._scale * rmc._u * rmc._read(d_used) + dL = float((rmc._w * (dm**2 - 2 * rmc._r * dm)).sum()) + + rmc.sigma[j_on], rmc.sigma[j_off] = True, False + rmc._recompute_G() + loss1 = rmc._update_residual() + assert loss1 - loss0 == pytest.approx(dL, rel=1e-3, abs=1e-4 * loss0) + + +def test_run_lowers_loss_and_keeps_composition(rmc): + n_b = rmc.sigma.sum() + rmc.run(n_sweeps=3, batch=8, progress=False) + assert rmc.sigma.sum() == n_b + assert rmc.loss_history[-1] <= rmc.loss_history[0] + 1e-6 + + +def test_warren_cowley_random_is_near_zero(rmc): + sro = rmc.warren_cowley(n_shells=2) + assert np.allclose(sro["radius"], [3.2 * np.sqrt(3) / 2, 3.2], atol=1e-3) + assert np.all(np.abs(sro["alpha"]) < 0.15) + + +def test_mask_is_zero_on_bragg_peaks(rmc): + w = rmc.mask["w"][0] + b = rmc.bin_factor + for r, c in rmc.bragg_positions(0): + r, c = int(r // b), int(c // b) + if 0 <= r < w.shape[0] and 0 <= c < w.shape[1]: + assert w[r, c] < 0.3 + + +def test_sro_section_random_is_near_laue(rmc): + img, _, node_dist = rmc.diffuse_section((0, 0, 1), extent=1.0, smooth=True) + between = img[node_dist > 0.2] + assert 0.5 < between.mean() < 1.5 From d18b8a145aba8efba5fcdf6fbef109d062aec9a4 Mon Sep 17 00:00:00 2001 From: cophus Date: Sat, 3 Oct 2026 16:29:07 -0700 Subject: [PATCH 25/36] more RMC updates --- .../diffraction/reverse_monte_carlo.py | 1133 +++++++++++++---- tests/diffraction/test_reverse_monte_carlo.py | 146 ++- 2 files changed, 1010 insertions(+), 269 deletions(-) diff --git a/src/quantem/diffraction/reverse_monte_carlo.py b/src/quantem/diffraction/reverse_monte_carlo.py index b2724cb07..42a46f002 100644 --- a/src/quantem/diffraction/reverse_monte_carlo.py +++ b/src/quantem/diffraction/reverse_monte_carlo.py @@ -1,17 +1,29 @@ """Reverse Monte Carlo fitting of diffuse electron scattering from several zone axes. One periodic supercell of a disordered crystal is fitted to every pattern at -once. The supercell's diffuse amplitude ``G(h) = sum_j (sigma_j - c) -exp(-2 pi i h.x_j / N)`` lives on the FFT grid of its sites; the diffuse -intensity of each pattern is ``|f_B - f_A|^2 |G|^2`` read off where the -pattern's Ewald sphere (with that pattern's fitted tilt) cuts the grid, -averaged over the cubic rotations so the model is as symmetric as the -(statistically cubic) foil. Swapping two atoms changes ``G`` by two phase +once. Its amplitude ``F(q) = sum_j f_s(j)(q) exp(-2 pi i q.(r_j + u_j))`` +lives on the FFT grid of the (displaced) atom positions, with the Bragg +nodes of the average lattice removed; the diffuse intensity of each pattern +is ``|F|^2`` read where the pattern's Ewald sphere (with its fitted tilt) +cuts the grid, averaged over the cubic rotations so the model is as +symmetric as the (statistically cubic) foil. Moves are swaps of unlike atoms +and omega embryos (three consecutive atoms of a <111> row, the last two +collapsed toward each other by a/12); each changes ``F`` by a few phase factors, so every move is scored exactly without recomputing the supercell. -Bragg peaks are excluded by a sigmoid weight that is 0 on each reflection and -1 away from it; a smooth background (constant, two Gaussians about the direct -beam, an Einstein thermal diffuse term and the Bragg cores blurred by the -detector's point spread) and one scale per pattern are solved in closed form. + +Bragg peaks and the direct-beam bloom are excluded by sigmoid weights; a +smooth background (constant, direct-beam Lorentzian, a wide Gaussian about +the zone-axis pole, Einstein thermal diffuse, phonon halos about each +reflection, powder rings and the Bragg cores blurred by the detector's point +spread) and the diffuse scale are solved in closed form. + +Electrons barely separate species of neighbouring atomic number (Nb and Zr +differ by a few percent in scattering factor), so their relative arrangement +is set by the moves' randomness unless something else tells them apart. +``set_size_effect`` / ``fit_size_effect`` add the linear size effect: each +species pushes its neighbours by its misfit through harmonic springs, and the +resulting displacement field (Huang and size-effect scattering, odd about +every Bragg peak) depends on which species sits where. """ from __future__ import annotations @@ -66,11 +78,8 @@ def _sigmoid(x): def _default_device() -> str: - if torch.cuda.is_available(): - return "cuda" - if torch.backends.mps.is_available(): - return "mps" - return "cpu" + """cuda when present, otherwise cpu (mps only on request: long runs heat a laptop).""" + return "cuda" if torch.cuda.is_available() else "cpu" class ReverseMonteCarlo(AutoSerialize): @@ -166,7 +175,8 @@ def set_crystal( The average structure, with fractional occupancies on shared sites. merge : dict, optional Species to relabel before fitting, e.g. ``{"Zr": "Nb"}`` folds Zr - into Nb so the shared site becomes a binary V-Nb site. + into Nb (their electron scattering factors differ by a few percent, + so the patterns barely tell them apart). """ from ase.data import atomic_numbers, chemical_symbols @@ -198,8 +208,6 @@ def set_crystal( if not mixed: raise ValueError("The crystal has no mixed-occupancy site to fit.") species = sorted({s for _, comp in mixed for s in comp}) - if len(species) != 2: - raise NotImplementedError(f"Binary sites only so far; got {species}. Use merge=.") comps = {tuple(sorted(comp.items())) for _, comp in mixed} if len(comps) != 1: raise NotImplementedError("All mixed sites must share one composition.") @@ -216,9 +224,9 @@ def set_crystal( self._a_crystal = a self.lattice_parameter = a - self.species = species # [A, B]; sigma = 1 marks B + self.species = species self.numbers = [atomic_numbers[s] for s in species] - self.concentration = comp[species[1]] / total + self.concentrations = np.array([comp[s] / total for s in species]) self.site_grid = np.round(fr * d).astype(int) self.grid_divisor = d print( @@ -551,44 +559,70 @@ def build_supercell( self, cells: int = 16, seed: int | None = 0, + displacements: bool = True, + omega_amplitudes: Sequence[int] = (1, 2), symmetrize: bool = True, debye_waller: float = 0.5, envelope: str = "measured", resolution: float = 0.75, shared_scale: bool = True, + kikuchi: bool | int = False, + max_beams: int = 40, device: str | None = None, ) -> "ReverseMonteCarlo": """Random supercell of ``cells^3`` unit cells at the crystal's composition. + Every species on the mixed sites is kept (use ``merge`` in + ``set_crystal`` to fold any together). The supercell amplitude is + ``F(q) = sum_j f_s(j)(q) exp(-2 pi i q.(r_j + u_j))`` with the + Bragg nodes of the average lattice removed. + Parameters ---------- cells : int Unit cells along each cube edge. The diffuse model is sampled every ``1 / (cells a)`` in reciprocal space. + displacements : bool + Allow omega displacements: atoms move along a <111> sense, two of + every three {111} planes collapsing toward each other. Positions + live on a grid a/24 fine, so moves are still scored exactly. + omega_amplitudes : sequence of int + Allowed displacements in units of a/24 along each axis: 2 is the + ideal omega collapse (a/12, 0.53 A along <111> for a = 3.66 A), 1 + a half collapse. symmetrize : bool Average every pattern over the 24 cubic rotations of the supercell. debye_waller : float Isotropic B (A^2) damping the diffuse intensity. - envelope : {"measured", "kinematic"} - Chemical diffuse intensity is periodic in the reciprocal lattice, - so diffuse scattering out of every Bragg beam g lands on the same - |G|^2 and only its form factor changes: the intensity is - ``|G(q)|^2 sum_g P_g |f_B - f_A|^2(q - g)``. "measured" takes P_g - from the integrated Bragg intensities of each pattern (dynamical - redistribution, tilt and thickness included); "kinematic" keeps - only the direct beam. + envelope : {"measured", "fitted", "kinematic"} + "measured" redistributes the diffuse intensity over the Bragg + beams of each pattern, ``sum_g P_g fbar^2(q - g) / fbar^2(q)`` + with P_g the integrated Bragg intensities (exact for occupational + disorder, whose diffuse intensity is periodic in the reciprocal + lattice; approximate for displacements). "fitted" starts there + and re-solves the non-negative P_g of each pattern with its + background: diffuse scattering is generated by the beams' depth + averaged intensities, which dynamical diffraction makes differ + from their exit intensities. "kinematic" keeps only the direct + beam. resolution : float Gaussian sigma, in supercell reciprocal-grid steps, with which each pixel reads the diffuse grid (27 nearest points). A finite - supercell's |G|^2 is speckle; reading it through a kernel of about - one step damps the speckle and stops the fit chasing single grid - points (most visibly on mirror planes, where fewer symmetry images - average). + supercell's intensity is speckle; reading it through a kernel of + about one step damps the speckle. 0 reads the 8 nearest points + trilinearly, about half as many grid points in total (faster). shared_scale : bool - One diffuse scale for every pattern. The envelope carries each - pattern's absolute Bragg intensities, so diffuse scattering out of - a beam is proportional to that beam; a per-pattern scale lets a - pattern's smooth background swallow its diffuse intensity. + One diffuse scale for every pattern; the envelope carries each + pattern's absolute Bragg intensities. + kikuchi : bool + Add Kikuchi bands to the background: for the three lowest-order + reflection families of each zone, a band interior and its edge + lines, ``|(q - pole) . g_hat| = |g| / 2`` about the zone-axis pole + ``-K tilt``, each with a free-signed amplitude (excess or + deficit). They follow the fitted tilt, so ``refine_tilts`` feels them. + An integer sets the number of families (True: 3). + max_beams : int + Strongest Bragg beams of each pattern in the diffuse envelope. device : str, optional torch device; default cuda, then mps, then cpu. """ @@ -597,45 +631,107 @@ def build_supercell( rng = np.random.default_rng(seed) self.rng = rng d = self.grid_divisor - n = cells * d - m = np.stack(np.meshgrid(*(np.arange(cells),) * 3, indexing="ij"), -1).reshape(-1, 3) - x = (m[:, None, :] * d + self.site_grid[None]).reshape(-1, 3) - n_sites = len(x) - n_b = int(round(self.concentration * n_sites)) - sigma = np.zeros(n_sites, dtype=bool) - sigma[rng.choice(n_sites, n_b, replace=False)] = True - self.cells, self.grid_size = cells, n - self.site_x = x - self.sigma = sigma + if displacements and not ( + d == 2 + and len(self.site_grid) == 2 + and np.array_equal(np.sort(self.site_grid.sum(1)), [0, 3]) + ): + raise NotImplementedError("Omega displacements are implemented for BCC sites only.") + m = 24 // d if displacements else 1 + unit = m * d // 24 # fine-grid steps per a/24 + self._omega_vectors = np.array( + [k * unit * np.array(v) for k in omega_amplitudes for v in product((1, -1), repeat=3)], + dtype=np.int64, + ) + + n = cells * d * m + idx = np.stack(np.meshgrid(*(np.arange(cells),) * 3, indexing="ij"), -1).reshape(-1, 3) + x0 = ((idx[:, None, :] * d + self.site_grid[None]) * m).reshape(-1, 3) + n_sites = len(x0) + counts = np.round(self.concentrations * n_sites).astype(int) + counts[-1] = n_sites - counts[:-1].sum() + spec = np.repeat(np.arange(len(counts)), counts) + rng.shuffle(spec) + self.cells, self.refine, self.grid_size = cells, m, n + self.site_x = x0 + self.species_index = spec + self.displacement = np.zeros((n_sites, 3), dtype=np.int64) # fine-grid steps + nc = cells * d + self._site_lookup = np.full((nc,) * 3, -1, dtype=np.int64) + xc = x0 // m + self._site_lookup[xc[:, 0], xc[:, 1], xc[:, 2]] = np.arange(n_sites) + self.displacements = bool(displacements) + self.size_eta = np.zeros(len(self.species)) self.debye_waller = float(debye_waller) self.symmetrize = bool(symmetrize) self.envelope = envelope self.resolution = float(resolution) self.shared_scale = bool(shared_scale) + self.kikuchi = bool(kikuchi) + self._kikuchi_families = 3 if kikuchi is True else int(kikuchi) + self.max_beams = int(max_beams) self.device = torch.device(device or _default_device()) self._setup_forward() - print(f"{n_sites} sites, {n}^3 grid, {int(self._fit.sum())} fitted pixels, {self.device}") + print( + f"{n_sites} sites (" + + ", ".join(f"{s} {c}" for s, c in zip(self.species, counts)) + + f"), {n}^3 grid, {len(self._needed)} grid points read, " + f"{int(self._fit.sum())} fitted pixels, {self.device}" + ) return self + def _positions(self, sites: np.ndarray, disp: np.ndarray | None = None) -> np.ndarray: + disp = self.displacement[sites] if disp is None else disp + return np.mod(self.site_x[sites] + disp, self.grid_size) + + # metallic (12-fold coordination) radii, A + _RADII = { + "V": 1.34, + "Nb": 1.46, + "Zr": 1.60, + "Ti": 1.47, + "Mo": 1.39, + "Ta": 1.46, + "Hf": 1.59, + "W": 1.39, + "Cr": 1.28, + "Fe": 1.26, + "Al": 1.43, + } + + def _pixel_grid(self, i: int, q2: np.ndarray, n: int | None = None): + """Grid indices and weights (n, 8) trilinear, or (n, 27) Gaussian of ``resolution`` + steps, for in-plane q of pattern i, on a grid of ``n`` points per edge (default the + supercell grid; the reciprocal sampling is the same on any coarser grid).""" + n = self.grid_size if n is None else n + h = self._q_crystal(i, q2) * self.lattice_parameter * self.cells + if self.resolution > 0: + stencil = np.array(list(product((-1, 0, 1), repeat=3))) + h0 = np.round(h).astype(np.int64) + f = h - h0 + wts = np.exp( + -0.5 * ((f[:, None, :] - stencil[None]) ** 2).sum(-1) / self.resolution**2 + ) + wts /= wts.sum(1, keepdims=True) + else: + stencil = np.array(list(product((0, 1), repeat=3))) + h0 = np.floor(h).astype(np.int64) + f = h - h0 + wts = np.prod(np.where(stencil[None], f[:, None, :], 1 - f[:, None, :]), axis=-1) + ijk = np.mod(h0[:, None, :] + stencil[None], n) + return (ijk[..., 0] * n + ijk[..., 1]) * n + ijk[..., 2], wts + def _setup_forward(self): """Ewald-sphere sampling of the supercell grid for every binned pixel.""" n = self.grid_size dev = self.device cols_w, vals_w, u_all, basis_all, pix_image = [], [], [], [], [] - stencil = np.array(list(product((-1, 0, 1), repeat=3))) + self._env_cols = [None] * len(self.images) + self._env_p = [None] * len(self.images) for i, kk in enumerate(self.mask["k"]): q2 = kk.reshape(-1, 2) - h = self._q_crystal(i, q2) * self.lattice_parameter * self.cells - h0 = np.round(h).astype(np.int64) - f = h - h0 - wts = np.exp( - -0.5 - * ((f[:, None, :] - stencil[None]) ** 2).sum(-1) - / max(self.resolution, 0.3) ** 2 - ) - wts /= wts.sum(1, keepdims=True) - idx = np.mod(h0[:, None, :] + stencil[None], n) - cols_w.append((idx[..., 0] * n + idx[..., 1]) * n + idx[..., 2]) + cols, wts = self._pixel_grid(i, q2) + cols_w.append(cols) vals_w.append(wts) u, tds = self._envelope(i, q2) u_all.append(u) @@ -643,17 +739,31 @@ def _setup_forward(self): qm = np.linalg.norm(q2, axis=1) halos = self._phonon_halos(i, q2) rings = self._powder_rings(qm) - basis_all.append(np.column_stack([np.ones_like(qm), qm, tds, halos, rings, tails])) - pix_image.append(np.full(len(h), i)) + extra = [self._kikuchi(i, q2)] if getattr(self, "kikuchi", False) else [] + basis_all.append( + np.column_stack([np.ones_like(qm), qm, q2, tds, halos, rings, tails, *extra]) + ) + pix_image.append(np.full(len(q2), i)) cols = np.concatenate(cols_w) vals = np.concatenate(vals_w) - used, inv = np.unique(cols, return_inverse=True) - cols_used = inv.reshape(cols.shape) + # only pixels inside q_max are modelled: the fit never reads the rest, and every grid + # point read costs time in each move + inside = np.concatenate( + [ + np.linalg.norm(kk.reshape(-1, 2), axis=1) < self.mask["q_max"] + for kk in self.mask["k"] + ] + ) + used, inv = np.unique(cols[inside], return_inverse=True) + cols_used = np.zeros(cols.shape, dtype=np.int64) + cols_used[inside] = inv.reshape(-1, cols.shape[1]) + vals = np.where(inside[:, None], vals, 0.0) n_pix = len(cols) self._W_all = sparse.csr_matrix( (vals.ravel(), (np.repeat(np.arange(n_pix), cols.shape[1]), cols_used.ravel())), shape=(n_pix, len(used)), ) + self._used = used # symmetry: model reads mean_k I(S_k h) at each used h ops = cubic_rotations() if self.symmetrize else np.eye(3, dtype=int)[None] hh = np.stack(np.unravel_index(used, (n,) * 3), -1) @@ -663,6 +773,10 @@ def _setup_forward(self): needed, inv_s = np.unique(sym_flat, return_inverse=True) self._needed = needed self._sym_index = torch.as_tensor(inv_s.reshape(sym_flat.shape), device=dev) + hn = np.stack(np.unravel_index(needed, (n,) * 3), -1) + fs, keep = self._grid_factors(hn) + self._fs = torch.as_tensor(fs, dtype=torch.float32, device=dev) + self._keep = torch.as_tensor(keep, dtype=torch.float32, device=dev) self._u_all = np.concatenate(u_all) self._basis_all = np.concatenate(basis_all) self._pix_image = np.concatenate(pix_image) @@ -677,28 +791,200 @@ def _setup_forward(self): self._w = torch.as_tensor(w[fit], dtype=torch.float32, device=dev) self._img = torch.as_tensor(self._pix_image[fit], device=dev) self._u = torch.as_tensor(self._u_all[fit], dtype=torch.float32, device=dev) - hn = np.stack(np.unravel_index(needed, (n,) * 3), -1) - self._h_needed = torch.as_tensor(hn, device=dev) + self._h_needed = torch.as_tensor(hn.T.copy(), dtype=torch.int32, device=dev) # (3, n) ang = 2 * np.pi * np.arange(n) / n self._cos = torch.as_tensor(np.cos(ang), dtype=torch.float32, device=dev) self._sin = torch.as_tensor(np.sin(ang), dtype=torch.float32, device=dev) - self._x = torch.as_tensor(self.site_x, device=dev) - self._recompute_G() - # direct-beam Lorentzian half width and wide Gaussian sigma, 1/A - self.background_sigmas = [(0.1, 0.8) for _ in self.images] - self._sigma_bounds = (np.array([0.01, 0.3]), np.array([1.0, 3.0])) - self.coefficients = None + self._recompute_F() + if getattr(self, "background_sigmas", None) is None or len(self.background_sigmas) != len( + self.images + ): + # direct-beam Lorentzian half width, wide Gaussian sigma and its center, 1/A; the + # wide Gaussian floats because a tilted crystal centers its diffuse and Kikuchi + # background on the zone-axis pole rather than the direct beam + self.background_sigmas = [(0.1, 0.8, 0.0, 0.0) for _ in self.images] + self._sigma_bounds = (np.array([0.01, 0.3, -1.0, -1.0]), np.array([1.0, 3.0, 1.0, 1.0])) + self.coefficients = getattr(self, "coefficients", None) + if self.coefficients is not None: + n_coef = 1 + self._bg_basis(0, np.arange(1)).shape[1] + if self.coefficients.shape[1] != n_coef: # background terms changed: pad with zeros + c = np.zeros((len(self.images), n_coef)) + k = min(n_coef, self.coefficients.shape[1]) + c[:, :k] = self.coefficients[:, :k] + self.coefficients = c + self._update_residual() + + def _grid_factors(self, h: np.ndarray, n: int | None = None): + """Scattering factors (K, n) of every species at grid points h (n, 3), and a 0/1 weight + removing the Bragg nodes of the average lattice.""" + n = self.grid_size if n is None else n + hm = np.where(h > n // 2, h - n, h) + q = np.linalg.norm(hm, axis=1) / (self.lattice_parameter * self.cells) + fs = electron_scattering_factor( + torch.tensor(self.numbers), torch.as_tensor(q, dtype=torch.float64) + ).numpy() + eta = getattr(self, "size_eta", None) + if eta is not None and np.any(eta != 0): + fs = ( + fs + + np.asarray(eta)[:, None] + * self._size_chi(hm / (self.lattice_parameter * self.cells))[None] + ) + on_node = np.all(np.mod(hm, self.cells) == 0, axis=1) + hkl = hm[on_node] // self.cells + frac = self.site_grid / self.grid_divisor + f_avg = np.exp(-2j * np.pi * hkl @ frac.T).sum(1) + keep = np.ones(len(h)) + keep[np.nonzero(on_node)[0][np.abs(f_avg) > 1e-6]] = 0.0 + return fs, keep + + def _size_chi(self, q: np.ndarray, k_ratio: float = 0.5, chunk: int = 200_000) -> np.ndarray: + """First-order size-effect factor chi(q) (A) at crystal-frame q (n, 3), 1/A. + + Each atom of species s pushes its 8 nearest and 6 next-nearest neighbours with Kanzaki + forces ``k_n eta_s |r_n| / 2`` along the bond; the lattice relaxes harmonically with the + same springs, u(q) = D(q)^-1 Phi(q) sum_s eta_s A_s(q). To first order in u the + amplitude gains ``-2 pi i fbar q.u``, i.e. each species' scattering factor becomes + ``f_s + eta_s chi(q)`` with ``chi = -2 pi i fbar q.D^-1 Phi`` (real). It is odd about + every Bragg node and grows as 1/|q - g| toward it: Huang and size-effect scattering. + """ + a = self.lattice_parameter + r_n = ( + np.array( + [list(v) for v in product((0.5, -0.5), repeat=3)] + + [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]] + ) + * a + ) # A + length = np.linalg.norm(r_n, axis=1) + e_n = r_n / length[:, None] + k_n = np.where(np.arange(14) < 8, 1.0, k_ratio) + ee = e_n[:, :, None] * e_n[:, None, :] + out = np.zeros(len(q)) + for c0 in range(0, len(q), chunk): + qc = q[c0 : c0 + chunk] + theta = 2 * np.pi * qc @ r_n.T # (n, 14) + D = np.einsum("nk,kab->nab", k_n * (1 - np.cos(theta)), ee) + # Phi = sum_n k |r| / 2 e_n exp(-i theta) = -i sum_n k |r| / 2 e_n sin(theta) + psi = np.einsum("nk,ka->na", (k_n * length / 2) * np.sin(theta), e_n) + on_node = np.abs(np.linalg.det(D)) < 1e-12 + D[on_node] = np.eye(3) + x = np.linalg.solve(D, psi[..., None])[..., 0] # Phi = -i psi -> D^-1 Phi = -i x + fbar, _ = self._fbar(np.linalg.norm(qc, axis=1)) + chi = -2 * np.pi * fbar * (qc * x).sum(1) # -2 pi i fbar q.(-i x) + chi[on_node] = 0.0 + out[c0 : c0 + chunk] = chi + return out + + def set_size_effect(self, eta: dict[str, float] | str | None = "radii") -> "ReverseMonteCarlo": + """Linear size effect: species mismatch ``eta_s`` (dimensionless, relative to the mean). + + ``"radii"`` takes ``(r_s - r_mean) / r_mean`` from metallic radii (V 1.34, Nb 1.46, + Zr 1.60 A); a dict sets them; None switches the size effect off. Only differences + between species matter (a common shift only moves the Bragg peaks). + """ + if eta is None: + self.size_eta = np.zeros(len(self.species)) + elif isinstance(eta, str): + from ase.data import atomic_numbers, covalent_radii + + r = np.array( + [self._RADII.get(sp, covalent_radii[atomic_numbers[sp]]) for sp in self.species] + ) + r_mean = float((self.concentrations * r).sum()) + self.size_eta = (r - r_mean) / r_mean + else: + self.size_eta = np.array([float(eta.get(sp, 0.0)) for sp in self.species]) + self._setup_forward() + return self + + def _species_amplitudes(self) -> np.ndarray: + """Lattice sums A_s(h) = sum_{j in s} exp(-2 pi i h.x_j / N) on the needed points (K, n).""" + n = self.grid_size + hn = np.stack(np.unravel_index(self._needed, (n,) * 3), -1) + upper = hn[:, 2] > n // 2 + hr = np.where(upper[:, None], np.mod(-hn, n), hn) + flat = (hr[:, 0] * n + hr[:, 1]) * (n // 2 + 1) + hr[:, 2] + out = np.zeros((len(self.species), len(self._needed)), dtype=np.complex64) + for s_, A in self._species_fft(real=True): + v = A.ravel()[flat] + out[s_] = np.where(upper, np.conj(v), v) + return out + + def fit_size_effect(self, step: float = 0.005, verbose: bool = True) -> dict: + """Fit the species size mismatches eta_s to the diffuse scattering of the current + supercell (background and scale re-solved at each step; one eta fixed by + ``sum c_s eta_s = 0``).""" + A = torch.as_tensor(self._species_amplitudes()) + n = self.grid_size + hn = np.stack(np.unravel_index(self._needed, (n,) * 3), -1) + hm = np.where(hn > n // 2, hn - n, hn) + chi = torch.as_tensor( + self._size_chi(hm / (self.lattice_parameter * self.cells)), dtype=torch.float32 + ) + self.size_eta = np.zeros(len(self.species)) + fs0, _ = self._grid_factors(hn) + fs0 = torch.as_tensor(fs0, dtype=torch.float32) + c = self.concentrations + + def full_eta(x): + e = np.concatenate([x, [0.0]]) + return e - (c * e).sum() + + def loss(x): + eta = torch.as_tensor(full_eta(x), dtype=torch.float32) + F = ((fs0 + eta[:, None] * chi[None]) * A).sum(0) + self._Fr, self._Fi = F.real.contiguous(), F.imag.contiguous() + self._solve_linear(self._model_diffuse()) + return self._update_residual() + + # start from no size effect with a small simplex: at radius-sized mismatches the + # first-order displacements are no longer small and the loss is far from quadratic + x0 = np.zeros(len(c) - 1) + l0 = loss(x0) + simplex = np.vstack([x0, x0 + step * np.eye(len(x0))]) + sol = optimize.minimize( + loss, + x0, + method="Nelder-Mead", + options=dict(xatol=1e-5, fatol=1e-4, initial_simplex=simplex), + ) + if sol.fun > l0: + sol.x = x0 + self.size_eta = full_eta(sol.x) + self._setup_forward() + self._solve_linear(self._model_diffuse(), refit_sigmas=True) + l1 = self._update_residual() + out = dict(eta={sp: float(e) for sp, e in zip(self.species, self.size_eta)}, loss=(l0, l1)) + if verbose: + print( + "size mismatch eta: " + + ", ".join(f"{k} {v:+.4f}" for k, v in out["eta"].items()) + + f"; loss {l0:.2f} -> {l1:.2f}" + ) + return out def _read(self, i_used: torch.Tensor) -> torch.Tensor: """Grid values on the used points (..., n_used) -> kernel-weighted values on fitted pixels.""" return (i_used[..., self._Wc] * self._Wv).sum(-1) - def _envelope(self, i: int, q2: np.ndarray, max_beams: int = 40): - """Diffuse and thermal-diffuse envelopes summed over the Bragg beams of pattern i.""" - if self.envelope == "measured": + def _fbar(self, q: np.ndarray): + fe = electron_scattering_factor( + torch.tensor(self.numbers), torch.as_tensor(q, dtype=torch.float64) + ).numpy() + c = self.concentrations[:, None] + return (c * fe).sum(0), (c * fe**2).sum(0) + + def _envelope(self, i: int, q2: np.ndarray): + """Diffuse envelope (beam redistribution x Debye-Waller) and Einstein thermal diffuse. + + Also stores the per-beam envelope columns ``fbar^2(q - g) / fbar^2(q) x DW(q)`` and the + beam weights, which ``envelope="fitted"`` re-solves. + """ + if self.envelope in ("measured", "fitted"): g = self.mask["bragg_k"][i] p = self.mask["bragg_intensity"][i] - order = np.argsort(p)[::-1][:max_beams] + order = np.argsort(p)[::-1][: getattr(self, "max_beams", 40)] g, p = g[order], p[order] keep = p > 0.002 * p.max() g, p = g[keep], p[keep] @@ -706,17 +992,50 @@ def _envelope(self, i: int, q2: np.ndarray, max_beams: int = 40): g, p = np.zeros((1, 2)), np.array([self.mask["bragg_intensity"][i].sum()]) else: raise ValueError(f"unknown envelope {self.envelope!r}") - z = torch.tensor(self.numbers) - c = self.concentration - u = np.zeros(len(q2)) + q0 = np.linalg.norm(q2, axis=1) + fbar0, _ = self._fbar(q0) + dw0 = np.exp(-0.5 * self.debye_waller * q0**2) + cols = np.zeros((len(q2), len(g))) tds = np.zeros(len(q2)) - for gi, pi in zip(g, p): + for k, (gi, pi) in enumerate(zip(g, p)): qm = np.linalg.norm(q2 - gi, axis=1) - fe = electron_scattering_factor(z, torch.as_tensor(qm, dtype=torch.float64)).numpy() + fbar, f2 = self._fbar(qm) dw = np.exp(-0.5 * self.debye_waller * qm**2) - u += pi * (fe[1] - fe[0]) ** 2 * dw - tds += pi * ((1 - c) * fe[0] ** 2 + c * fe[1] ** 2) * (1 - dw) - return u, tds + cols[:, k] = fbar**2 / fbar0**2 * dw0 + tds += pi * f2 * (1 - dw) + self._env_cols[i] = cols + self._env_p[i] = p.astype(float) + return cols @ p, tds + + def _kikuchi(self, i: int, q2: np.ndarray, tilt: np.ndarray | None = None) -> np.ndarray: + """Kikuchi band interiors and edge lines (n, 2 * families) about the zone-axis pole.""" + n_families = self._kikuchi_families + tilt = self.geometry["tilts"][i] if tilt is None else tilt + pole = -np.asarray(tilt) / self.wavelength + hkl, g2, _ = self._zone_reflections(self.zone_axes[i], 1.2) + g2 = g2 * self._a_crystal / self.lattice_parameter + gl = np.linalg.norm(g2, axis=1) + nz = gl > 1e-6 + g2, gl = g2[nz], gl[nz] + radii = np.unique(np.round(gl, 3))[:n_families] + out = np.zeros((len(q2), 2 * len(radii))) + width = 0.015 + for f, rad in enumerate(radii): + for g, length in zip(g2[np.abs(gl - rad) < 2e-3], gl[np.abs(gl - rad) < 2e-3]): + d = (q2 - pole) @ (g / length) + out[:, 2 * f] += _sigmoid((0.5 * length - np.abs(d)) / 0.01) + out[:, 2 * f + 1] += np.exp(-0.5 * ((np.abs(d) - 0.5 * length) / width) ** 2) + if len(radii) < n_families: + out = np.column_stack([out, np.zeros((len(q2), 2 * (n_families - len(radii))))]) + return out + + def _bg_lower(self, n: int) -> np.ndarray: + """Lower bounds of the background amplitudes: free sign for the constant and Kikuchi.""" + lo = np.zeros(n) + lo[0] = -np.inf + if getattr(self, "kikuchi", False): + lo[-2 * self._kikuchi_families :] = -np.inf + return lo def _powder_rings(self, q: np.ndarray, width: float = 0.025, k_max: float = 1.5) -> np.ndarray: """Powder rings of the average crystal about the direct beam, ``sum_g |F_g|^2 / g^2`` @@ -741,42 +1060,92 @@ def _phonon_halos(self, i: int, q2: np.ndarray, widths=(0.04, 0.12)) -> np.ndarr out = np.zeros((len(q2), len(widths))) for gi, pi in zip(g, p): d2 = ((q2 - gi) ** 2).sum(1) - for n, k in enumerate(widths): - out[:, n] += pi * k**2 / (d2 + k**2) + for k, width in enumerate(widths): + out[:, k] += pi * width**2 / (d2 + width**2) return out - def _phases(self, sites: torch.Tensor): - """cos and sin of 2 pi h.x / N for the given sites on every needed grid point.""" - idx = (self._x[sites] @ self._h_needed.T) % self.grid_size # (B, n_needed) + def _phases(self, pos: np.ndarray): + """cos and sin of 2 pi h.x / N for fine-grid positions (B, 3) on every needed point.""" + p = torch.as_tensor(pos, dtype=torch.int32, device=self.device) + h = self._h_needed + idx = (p[:, 0:1] * h[0] + p[:, 1:2] * h[1] + p[:, 2:3] * h[2]) % self.grid_size + idx = idx.long() return self._cos[idx], self._sin[idx] - def _recompute_G(self): + def _species_fft(self, coarsen: int = 1, real: bool = False): + """Per-species FFT of the occupancy, one at a time. ``coarsen`` rounds positions onto a + grid that many times coarser; ``real`` returns the half spectrum (rfftn).""" + from scipy import fft as sfft + + n = self.grid_size // coarsen + pos = self._positions(np.arange(len(self.site_x))) + pos = np.mod((pos + coarsen // 2) // coarsen, n) + for s in range(len(self.species)): + occ = np.zeros((n,) * 3, dtype=np.float32) + sel = self.species_index == s + np.add.at(occ, (pos[sel, 0], pos[sel, 1], pos[sel, 2]), 1.0) + yield s, (sfft.rfftn if real else sfft.fftn)(occ, workers=-1) + + def _recompute_F(self): n = self.grid_size - a = np.zeros((n,) * 3) - xs = self.site_x - a[xs[:, 0], xs[:, 1], xs[:, 2]] = self.sigma - self.concentration - G = np.fft.fftn(a).ravel()[self._needed] - self._Gr = torch.as_tensor(G.real, dtype=torch.float32, device=self.device) - self._Gi = torch.as_tensor(G.imag, dtype=torch.float32, device=self.device) - - def _diffuse_used(self, Gr=None, Gi=None) -> torch.Tensor: - """Symmetrized |G|^2 per site on the used grid points.""" - Gr = self._Gr if Gr is None else Gr - Gi = self._Gi if Gi is None else Gi - inten = (Gr**2 + Gi**2) / len(self.sigma) + hn = np.stack(np.unravel_index(self._needed, (n,) * 3), -1) + upper = hn[:, 2] > n // 2 # read from the conjugate half + hr = np.where(upper[:, None], np.mod(-hn, n), hn) + flat = (hr[:, 0] * n + hr[:, 1]) * (n // 2 + 1) + hr[:, 2] + F = np.zeros(len(self._needed), dtype=np.complex128) + fs = self._fs.cpu().numpy() + for s, A in self._species_fft(real=True): + v = A.ravel()[flat] + F += fs[s] * np.where(upper, np.conj(v), v) + self._Fr = torch.as_tensor(F.real, dtype=torch.float32, device=self.device) + self._Fi = torch.as_tensor(F.imag, dtype=torch.float32, device=self.device) + + def _diffuse_used(self, Fr=None, Fi=None) -> torch.Tensor: + """Symmetrized diffuse intensity per site on the used grid points.""" + Fr = self._Fr if Fr is None else Fr + Fi = self._Fi if Fi is None else Fi + inten = (Fr**2 + Fi**2) * self._keep / len(self.site_x) return inten[..., self._sym_index].mean(dim=-2) + def diffuse_grid(self, max_size: int = 400) -> np.ndarray: + """Symmetrized diffuse intensity per site on the whole grid (Bragg nodes removed). + + Grids above ``max_size`` per edge are evaluated with positions rounded onto a coarser + grid (the same reciprocal sampling over a smaller q range), which slightly blurs the + displacement scattering.""" + coarsen = 1 + while self.grid_size // coarsen > max_size and self.refine % (2 * coarsen) == 0: + coarsen *= 2 + n = self.grid_size // coarsen + hh = np.stack(np.meshgrid(*(np.arange(n),) * 3, indexing="ij"), -1).reshape(-1, 3) + fs, keep = self._grid_factors(hh, n) + F = np.zeros(n**3, dtype=np.complex64) + for s, A in self._species_fft(coarsen): + F += (fs[s] * A.ravel()).astype(np.complex64) + S = (np.abs(F) ** 2 * keep).reshape((n,) * 3).astype(np.float32) / len(self.site_x) + if not self.symmetrize: + return S + og = np.ogrid[0:n, 0:n, 0:n] + out = np.zeros_like(S) + ops = cubic_rotations() + for op in ops: + perm = np.argmax(np.abs(op), axis=1) + sign = op[np.arange(3), perm] + out += S[tuple(np.mod(sign[r] * og[perm[r]], n) for r in range(3))] + return out / len(ops) + # --------------------------------------------------------------- background def _bg_basis(self, i: int, sel: np.ndarray) -> np.ndarray: q = self._basis_all[sel, 1] - s1, s2 = self.background_sigmas[i] + q2 = self._basis_all[sel, 2:4] + s1, s2, cy, cx = self.background_sigmas[i] return np.column_stack( [ np.ones_like(q), 1.0 / (1.0 + (q / s1) ** 2), - np.exp(-0.5 * (q / s2) ** 2), - self._basis_all[sel, 2:], + np.exp(-0.5 * ((q2 - [cy, cx]) ** 2).sum(1) / s2**2), + self._basis_all[sel, 4:], ] ) @@ -786,34 +1155,59 @@ def _solve_linear(self, diffuse_fit: np.ndarray, refit_sigmas: bool = False): w = self._w_all[self._fit] img = self._pix_image[self._fit] sel_all = np.nonzero(self._fit)[0] + fitted = self.envelope == "fitted" + if fitted: + read = self._read(self._diffuse_used()).cpu().numpy().astype(np.float64) coefs, loss = [], 0.0 for i in range(len(self.images)): m = img == i sel = sel_all[m] sw = np.sqrt(w[m]) - - def solve(sig): - sig = np.clip(sig, *self._sigma_bounds) - self.background_sigmas[i] = tuple(float(v) for v in sig) - X = np.column_stack([diffuse_fit[m], self._bg_basis(i, sel)]) - lo = np.zeros(X.shape[1]) - lo[1] = -np.inf + if fitted: + rows = sel - np.nonzero(self._pix_image == i)[0][0] + Xd = read[m][:, None] * self._env_cols[i][rows] + else: + Xd = diffuse_fit[m][:, None] + nd = Xd.shape[1] + + def solve(par): + par = np.clip(par, *self._sigma_bounds) + self.background_sigmas[i] = tuple(float(v) for v in par) + X_bg = self._bg_basis(i, sel) + X = np.column_stack([Xd, X_bg]) + lo = np.concatenate([np.zeros(nd), self._bg_lower(X_bg.shape[1])]) x = _lsq_bounded(X, y[m], sw, lo) return x, float(((X @ x - y[m]) ** 2 * w[m]).sum()) if refit_sigmas: + p0 = np.asarray(self.background_sigmas[i], dtype=float) + + def unpack(x): + return np.concatenate([np.exp(x[:2]), x[2:]]) + r = optimize.minimize( - lambda ls: solve(np.exp(ls))[1], - np.log(self.background_sigmas[i]), + lambda x: solve(unpack(x))[1], + np.concatenate([np.log(p0[:2]), p0[2:]]), method="Nelder-Mead", - options=dict(xatol=1e-3, fatol=1e-6, maxiter=200), + options=dict(xatol=1e-3, fatol=1e-6, maxiter=300), ) - solve(np.exp(r.x)) + solve(unpack(r.x)) c, l_i = solve(self.background_sigmas[i]) + if fitted: + self._env_p[i] = c[:nd] + c = np.concatenate([[1.0], c[nd:]]) coefs.append(c) loss += l_i self.coefficients = np.stack(coefs) - if not getattr(self, "shared_scale", False): + if fitted: + self._u_all = np.concatenate( + [self._env_cols[i] @ self._env_p[i] for i in range(len(self.images))] + ) + self._u = torch.as_tensor( + self._u_all[self._fit], dtype=torch.float32, device=self.device + ) + return loss + if not self.shared_scale: return loss # one diffuse scale for all patterns, backgrounds per pattern @@ -824,7 +1218,7 @@ def solve(sig): lo = np.zeros(X.shape[1]) for i, bl in enumerate(blocks): X[img == i, 1 + nb * i : 1 + nb * (i + 1)] = bl - lo[1 + nb * i] = -np.inf + lo[1 + nb * i : 1 + nb * (i + 1)] = self._bg_lower(nb) sw = np.sqrt(w) x = _lsq_bounded(X, y, sw, lo) if x[0] <= 0: @@ -838,14 +1232,15 @@ def solve(sig): def fit_background(self) -> float: """Fit the diffuse scale and, per pattern, a constant, a direct-beam Lorentzian, a wide - Gaussian, Einstein thermal diffuse, phonon halos about each reflection, powder rings of - the average crystal and Bragg tails.""" + Gaussian with a free center, Einstein thermal diffuse, phonon halos about each + reflection, powder rings of the average crystal and Bragg tails.""" loss = self._solve_linear(self._model_diffuse(), refit_sigmas=True) self._update_residual() print( "background widths (1/A): " + ", ".join( - f"{n} {s[0]:.3f}/{s[1]:.3f}" for n, s in zip(self.names, self.background_sigmas) + f"{n} {s[0]:.3f}/{s[1]:.3f} at ({s[2]:+.2f}, {s[3]:+.2f})" + for n, s in zip(self.names, self.background_sigmas) ) ) return loss @@ -874,92 +1269,260 @@ def _update_residual(self): def run( self, n_sweeps: int = 20, - batch: int = 64, + batch: int = 32, temperature: float = 0.05, + omega_fraction: float = 0.5, + omega_repeats: Sequence[int] = (1,), + max_static_b: float | None = 0.5, refit_every: int = 2, progress: bool = True, ) -> "ReverseMonteCarlo": - """Composition-conserving swaps until the diffuse fit converges. - - Each batch proposes ``batch`` disjoint swaps against the current - supercell and scores each exactly. Metropolis acceptance at - ``temperature`` (a fraction of the median score change, falling - linearly to 0); the accepted swaps are applied together, capped at a - number that adapts so the joint step never raises the loss. A sweep is - one proposal per site. Scale and background are refit every + """Species swaps and omega embryos until the diffuse fit converges. + + Each batch proposes ``batch`` moves on distinct sites against the + current supercell and scores each exactly: either swaps of two + unlike atoms (composition conserved) or, with probability + ``omega_fraction``, an omega embryo on three consecutive atoms of a + <111> row: the first stays and the next two collapse toward each + other by a/12 each, (0, +v, -v), repeated along the row a number of + times drawn from ``omega_repeats`` (one triple makes a compact + embryo; longer chains make the diffuse sheets normal to <111> that + cut a zone as streaks). Proposing a chain where it already stands + clears it instead. ``max_static_b`` (A^2) caps the + static Debye-Waller factor of all displacements, 8 pi^2 / 3, so + they stay consistent with how slowly the Bragg intensities fall off + (a displacement field this strong would damp them; the diffuse scale + alone does not fix how many atoms are displaced). Metropolis acceptance + at ``temperature`` (a fraction of the median score change, falling + linearly to 0); the accepted moves are applied together, capped at a + number that adapts so the joint step never raises the loss. A sweep + is one proposal per site. Scale and background are refit every ``refit_every`` sweeps. """ if self.coefficients is None: self.fit_background() - n_sites = len(self.sigma) + n_sites = len(self.site_x) loss = self._update_residual() if not self.loss_history: self.loss_history.append(loss) cap = max(batch // 8, 1) t0 = None - n_batches = max(n_sites // (2 * batch), 1) + n_batches = max(n_sites // batch, 1) + p_omega = omega_fraction if self.displacements else 0.0 + budget = ( + 3 * max_static_b / (8 * np.pi**2) * n_sites if max_static_b is not None else np.inf + ) + keep = self._keep / n_sites sweeps = tqdm(range(n_sweeps), desc="RMC sweeps", disable=not progress) su = None for sweep in sweeps: - accepted = 0 + accepted = {"swap": 0, "omega": 0} for _ in range(n_batches): if su is None: su = self._scale * self._u - on = np.nonzero(~self.sigma)[0] - off = np.nonzero(self.sigma)[0] - j_on = self.rng.choice(on, batch, replace=False) - j_off = self.rng.choice(off, batch, replace=False) - c1, s1 = self._phases(torch.as_tensor(j_on, device=self.device)) - c2, s2 = self._phases(torch.as_tensor(j_off, device=self.device)) - dGr = c1 - c2 - dGi = s2 - s1 - d_int = (2 * (self._Gr * dGr + self._Gi * dGi) + dGr**2 + dGi**2) / n_sites - d_used = d_int[:, self._sym_index].mean(dim=1) # (B, n_used) - dm = su * self._read(d_used) # (B, n_fit) + spec = self.species_index + roll = self.rng.random() + if roll < p_omega: + kind = "omega" + sites, new = self._omega_proposals(batch, int(self.rng.choice(omega_repeats))) + else: + kind = "swap" + j = self.rng.choice(n_sites, 2 * batch, replace=False) + j1, j2 = j[:batch], j[batch:] + ok = spec[j1] != spec[j2] + sites = np.stack([j1[ok], j2[ok]], 1) + new = None + if len(sites) == 0: + continue + if kind == "swap": + c1, s1 = self._phases(self._positions(sites[:, 0])) + c2, s2 = self._phases(self._positions(sites[:, 1])) + df = ( + self._fs[torch.as_tensor(spec[sites[:, 1]], device=self.device)] + - self._fs[torch.as_tensor(spec[sites[:, 0]], device=self.device)] + ) + dFr = df * (c1 - c2) + dFi = df * (s2 - s1) + else: + dFr = torch.zeros((len(sites), len(self._needed)), device=self.device) + dFi = torch.zeros_like(dFr) + for k in range(sites.shape[1]): + j = sites[:, k] + changed = np.any(new[:, k] != self.displacement[j], axis=1) + if not changed.any(): + continue + co, so = self._phases(self._positions(j)) + cn, sn = self._phases(self._positions(j, new[:, k])) + f = self._fs[torch.as_tensor(spec[j], device=self.device)] + f = ( + f + * torch.as_tensor(changed, dtype=torch.float32, device=self.device)[ + :, None + ] + ) + dFr += f * (cn - co) + dFi += f * (so - sn) + d_int = (2 * (self._Fr * dFr + self._Fi * dFi) + dFr**2 + dFi**2) * keep + d_used = d_int[:, self._sym_index].mean(dim=1) + dm = su * self._read(d_used) dL = (self._w * (dm**2 - 2 * self._r * dm)).sum(-1) dl = dL.cpu().numpy() + nb = len(dl) if not t0: t0 = float(np.median(np.abs(dl))) temp = temperature * t0 * (1 - sweep / n_sweeps) if temp > 0: - ok = (dl < 0) | (self.rng.random(batch) < np.exp(-np.clip(dl / temp, 0, 50))) + acc = (dl < 0) | (self.rng.random(nb) < np.exp(-np.clip(dl / temp, 0, 50))) else: - ok = dl < 0 - pick = np.nonzero(ok)[0] + acc = dl < 0 + pick = np.nonzero(acc)[0] pick = pick[np.argsort(dl[pick])][:cap] + if kind != "swap" and max_static_b is not None: + du2 = (self._u2(new) - self._u2(self.displacement[sites])).sum(1) + total = self._u2(self.displacement).sum() + chosen = [] + for b in pick: + if du2[b] <= 0 or total + du2[b] <= budget: + chosen.append(b) + total += du2[b] + pick = np.asarray(chosen, dtype=int) if len(pick) == 0: continue pk = torch.as_tensor(pick, device=self.device) - Gr_new = self._Gr + dGr[pk].sum(0) - Gi_new = self._Gi + dGi[pk].sum(0) - r_new = self._y - su * self._read(self._diffuse_used(Gr_new, Gi_new)) - self._bg + Fr_new = self._Fr + dFr[pk].sum(0) + Fi_new = self._Fi + dFi[pk].sum(0) + r_new = self._y - su * self._read(self._diffuse_used(Fr_new, Fi_new)) - self._bg loss_new = float((self._w * r_new**2).sum()) + spec_new, disp_new = spec.copy(), self.displacement.copy() + if kind == "swap": + a, b = sites[pick, 0], sites[pick, 1] + spec_new[a], spec_new[b] = spec[b], spec[a] + else: + disp_new[sites[pick]] = new[pick] if loss_new > loss + max(temp, 0.0) * len(pick) and len(pick) > 1: cap = max(cap // 2, 1) continue - self._Gr, self._Gi, self._r = Gr_new, Gi_new, r_new - self.sigma[j_on[pick]] = True - self.sigma[j_off[pick]] = False + self._Fr, self._Fi, self._r = Fr_new, Fi_new, r_new + self.species_index[:] = spec_new + self.displacement[:] = disp_new loss = loss_new - accepted += len(pick) + accepted[kind] += len(pick) cap = min(int(cap * 1.25) + 1, batch) if (sweep + 1) % refit_every == 0: - self._recompute_G() + self._recompute_F() self._solve_linear(self._model_diffuse()) loss = self._update_residual() su = None self.loss_history.append(loss) - sweeps.set_postfix(loss=f"{loss:.4g}", accepted=accepted) - self._recompute_G() + sweeps.set_postfix(loss=f"{loss:.4g}", swaps=accepted["swap"], omega=accepted["omega"]) + self._recompute_F() self._solve_linear(self._model_diffuse()) self.loss_history[-1] = self._update_residual() return self + def _omega_proposals(self, batch: int, repeats: int = 1): + """Disjoint omega chains along <111> rows: sites (B, 3 * repeats) and their new + displacement vectors (B, 3 * repeats, 3). + + Each chain repeats the (0, +v, -v) collapse ``repeats`` times along one row.""" + n_sites = len(self.site_x) + nc = self._site_lookup.shape[0] + j0 = self.rng.choice(n_sites, batch, replace=False) + vec = self._omega_vectors[self.rng.integers(0, len(self._omega_vectors), batch)] + step = np.sign(vec) # nearest neighbour along that <111>, in site units + xc = self.site_x[j0] // self.refine + length = 3 * repeats + sites = np.stack( + [self._site_lookup[tuple(np.mod(xc + t * step, nc).T)] for t in range(length)], 1 + ) + triple = np.stack([np.zeros_like(vec), vec, -vec], 1) # (B, 3, 3) + pattern = np.tile(triple, (1, repeats, 1)) + current = self.displacement[sites] + standing = np.all(current == pattern, axis=(1, 2)) + new = np.where(standing[:, None, None], 0, pattern) + keep = _disjoint_rows(sites) + return sites[keep], new[keep] + + def refine_tilts( + self, + max_tilt_deg: float = 2.5, + step_deg: float = 0.25, + patterns: Sequence[int] | None = None, + max_grid: int = 400, + verbose: bool = True, + ) -> "ReverseMonteCarlo": + """Refine each pattern's tilt against the diffuse fit of the current supercell. + + The tilt moves where the Ewald sphere cuts the 3D diffuse intensity. + For each pattern, a grid of tilts within ``max_tilt_deg`` of the + zone axis is scored by reading the full symmetrized supercell + intensity and re-solving that pattern's scale and background, then + the best is polished with Nelder-Mead. Run it after some RMC sweeps, + once the supercell has structure, and alternate the two. + """ + grid = self.diffuse_grid(max_grid) + n_grid = grid.shape[0] + grid = grid.ravel() + y = self._y_all + w = self._w_all + patterns = range(len(self.images)) if patterns is None else patterns + for i in patterns: + sel = np.nonzero((self._pix_image == i) & self._fit)[0] + q2 = self.mask["k"][i].reshape(-1, 2)[sel - np.nonzero(self._pix_image == i)[0][0]] + u = self._u_all[sel] + X_bg = self._bg_basis(i, sel) + sw = np.sqrt(w[sel]) + lo = np.concatenate([[0.0], self._bg_lower(X_bg.shape[1])]) + tilt0 = np.array(self.geometry["tilts"][i], dtype=float) + kik = getattr(self, "kikuchi", False) + + def loss(t): + self.geometry["tilts"][i] = np.asarray(t) + cols, wts = self._pixel_grid(i, q2, n_grid) + if kik: + X_bg[:, -2 * self._kikuchi_families :] = self._kikuchi(i, q2, t) + X = np.column_stack([u * (grid[cols] * wts).sum(1), X_bg]) + x = _lsq_bounded(X, y[sel], sw, lo) + return float(((X @ x - y[sel]) ** 2 * w[sel]).sum()) + + l0 = loss(tilt0) + lim = np.deg2rad(max_tilt_deg) + steps = np.arange(-lim, lim + 1e-12, np.deg2rad(step_deg)) + best = (l0, tilt0) + for tx in steps: + for ty in steps: + t = np.array([tx, ty]) + if np.hypot(tx, ty) > lim: + continue + lt = loss(t) + if lt < best[0]: + best = (lt, t) + sol = optimize.minimize( + loss, + best[1], + method="Nelder-Mead", + bounds=[(-lim, lim)] * 2, + options=dict(xatol=1e-4, fatol=1e-6), + ) + t = sol.x if sol.fun < best[0] and np.linalg.norm(sol.x) <= lim else best[1] + self.geometry["tilts"][i] = np.asarray(t) + if verbose: + print( + f"{self.names[i]}: tilt {np.rad2deg(np.linalg.norm(tilt0)):.2f} -> " + f"{np.rad2deg(np.linalg.norm(t)):.2f} deg, pattern loss {l0:.2f} -> " + f"{min(sol.fun, best[0]):.2f}" + ) + self._setup_forward() + self._solve_linear(self._model_diffuse()) + loss_all = self._update_residual() + self.loss_history.append(loss_all) + return self + # ---------------------------------------------------------------- analysis def model_images(self, diffuse_only: bool = False) -> list[np.ndarray]: """Model on the binned grid of every pattern (data units): scaled diffuse + background, - or the scaled diffuse term alone.""" + or the scaled diffuse term alone. The diffuse term is zero beyond ``q_max``.""" i_used = self._diffuse_used().cpu().numpy().astype(np.float64) diffuse = self._u_all * (self._W_all @ i_used) out = [] @@ -973,6 +1536,20 @@ def model_images(self, diffuse_only: bool = False) -> list[np.ndarray]: out.append(img.reshape(k.shape[:2])) return out + def r_factors(self) -> dict: + """Weighted R of the diffuse fit per pattern, ``sqrt(sum w (y - model)^2 / sum w (y - + background)^2)``: the fraction of the diffuse signal (experiment minus fitted + background) the supercell leaves unexplained.""" + full = self.model_images() + bg = self.background_images() + out = {} + for i, name in enumerate(self.names): + w, y = self.mask["w"][i], self.mask["y"][i] + out[name] = float( + np.sqrt((w * (y - full[i]) ** 2).sum() / max((w * (y - bg[i]) ** 2).sum(), 1e-30)) + ) + return out + def background_images(self) -> list[np.ndarray]: """Fitted background (constant, Gaussians, thermal diffuse, Bragg tails) per pattern.""" full = self.model_images() @@ -980,33 +1557,81 @@ def background_images(self) -> list[np.ndarray]: return [f - d for f, d in zip(full, diffuse)] def warren_cowley(self, n_shells: int = 6) -> dict: - """Warren-Cowley alpha of the B species about B for the first neighbour shells.""" - n = self.grid_size + """Warren-Cowley alpha for every species pair over the first neighbour shells. + + ``alpha[shell, s, t] = 1 - P(t | s) / c_t`` for unlike pairs and + ``(P(s | s) - c_s) / (1 - c_s)`` for like pairs, with ``P(t | s)`` the + fraction of the shell around an ``s`` atom occupied by ``t``. + Negative: unlike neighbours preferred; positive: like. + """ d = self.grid_divisor - occ = np.zeros((n,) * 3) - xs = self.site_x - occ[xs[:, 0], xs[:, 1], xs[:, 2]] = self.sigma - site = np.zeros((n,) * 3, dtype=bool) - site[xs[:, 0], xs[:, 1], xs[:, 2]] = True - fo = np.fft.fftn(occ) - fs = np.fft.fftn(site) - bb = np.real(np.fft.ifftn(fo * np.conj(fo))) - ss = np.real(np.fft.ifftn(fs * np.conj(fs))) + n = self.cells * d + xs = self.site_x // self.refine + K = len(self.species) + site = np.zeros((n,) * 3) + site[xs[:, 0], xs[:, 1], xs[:, 2]] = 1 + fsite = np.fft.fftn(site) + focc = [] + for s in range(K): + occ = np.zeros((n,) * 3) + sel = self.species_index == s + occ[xs[sel, 0], xs[sel, 1], xs[sel, 2]] = 1 + focc.append(np.fft.fftn(occ)) + ss = np.real(np.fft.ifftn(fsite * np.conj(fsite))) v = np.stack(np.meshgrid(*(np.fft.fftfreq(n, 1 / n),) * 3, indexing="ij"), -1) r = np.linalg.norm(v, axis=-1) / d * self.lattice_parameter valid = ss > 0.5 radii = np.unique(np.round(r[valid], 4))[1 : n_shells + 1] - c = self.concentration - alpha = [] - for rad in radii: - m = valid & (np.abs(r - rad) < 1e-3) - p_bb = bb[m].sum() / ss[m].sum() / c # P(B neighbour | B) - alpha.append((p_bb - c) / (1 - c)) - return dict( - radius=radii, alpha=np.asarray(alpha), pair=f"{self.species[1]}-{self.species[1]}" + shells = [valid & (np.abs(r - rad) < 1e-3) for rad in radii] + c = self.concentrations + alpha = np.zeros((len(radii), K, K)) + for s in range(K): + cs_site = np.real(np.fft.ifftn(focc[s] * np.conj(fsite))) + for t in range(K): + cst = np.real(np.fft.ifftn(focc[s] * np.conj(focc[t]))) + for k, m in enumerate(shells): + p = cst[m].sum() / cs_site[m].sum() + alpha[k, s, t] = (p - c[t]) / (1 - c[t]) if s == t else 1 - p / c[t] + return dict(radius=radii, alpha=alpha, species=list(self.species)) + + def _u2(self, disp: np.ndarray) -> np.ndarray: + """Squared displacement (A^2) of each displacement vector (..., 3).""" + step = self.lattice_parameter / (self.grid_divisor * self.refine) + return (disp**2).sum(-1) * step**2 + + def static_b(self) -> float: + """Static Debye-Waller B (A^2) of the displacements, 8 pi^2 / 3.""" + return float(8 * np.pi**2 * self._u2(self.displacement).mean() / 3) + + def _omega_like(self) -> np.ndarray: + """Atoms displaced along a <111> by at least a half omega collapse.""" + a = np.abs(self.displacement) + unit = self.refine * self.grid_divisor // 24 + return (a[:, 0] >= unit) & (a[:, 0] == a[:, 1]) & (a[:, 1] == a[:, 2]) + + def displacement_summary(self) -> dict: + """Static Debye-Waller B, mean displacement and omega fraction per species.""" + step = self.lattice_parameter / (self.grid_divisor * self.refine) + mag = np.linalg.norm(self.displacement, axis=1) * step + omega = self._omega_like() + out = dict( + static_b=self.static_b(), + mean_displacement={ + s: float(mag[self.species_index == k].mean()) for k, s in enumerate(self.species) + }, + omega_fraction={ + s: float(omega[self.species_index == k].mean()) for k, s in enumerate(self.species) + }, ) + return out - def diffuse_section(self, normal=(0, 0, 1), extent: float = 2.0, smooth: bool = True): + def diffuse_section( + self, + normal=(0, 0, 1), + extent: float = 2.0, + smooth: bool = True, + grid: np.ndarray | None = None, + ): """Symmetrized supercell diffuse intensity on a reciprocal-lattice plane, in Laue units. Parameters @@ -1017,35 +1642,28 @@ def diffuse_section(self, normal=(0, 0, 1), extent: float = 2.0, smooth: bool = Half width in units of the cubic reciprocal lattice vector 1/a. smooth : bool Blur by the fit's ``resolution`` kernel. + grid : ndarray, optional + Output of ``diffuse_grid()``, to reuse. Returns ------- image, (u, v) in-plane axes (crystal frame, unit vectors), distance of each pixel from the nearest reciprocal-lattice node (1/a units) """ - n = self.grid_size - a = np.zeros((n,) * 3) - xs = self.site_x - a[xs[:, 0], xs[:, 1], xs[:, 2]] = self.sigma - self.concentration - inten = np.abs(np.fft.fftn(a)) ** 2 / len(self.sigma) - ops = cubic_rotations() if self.symmetrize else np.eye(3, dtype=int)[None] - idx = np.stack(np.meshgrid(*(np.arange(n),) * 3, indexing="ij"), -1).reshape(-1, 3) - sym = np.zeros(n**3) - for op in ops: - s = np.mod(idx @ op.T, n) - sym += inten[s[:, 0], s[:, 1], s[:, 2]] - sym = (sym / len(ops)).reshape((n,) * 3) + grid = self.diffuse_grid() if grid is None else grid if smooth and self.resolution > 0: - sym = ndimage.gaussian_filter(sym, self.resolution, mode="wrap") - laue = self.concentration * (1 - self.concentration) + grid = ndimage.gaussian_filter(grid, self.resolution, mode="wrap") frame = _zone_frame(normal) u, v = frame[0], frame[1] steps = np.arange(-extent * self.cells, extent * self.cells + 1) su, sv = np.meshgrid(steps, steps, indexing="ij") pts = su[..., None] * u + sv[..., None] * v # grid units (cells per 1/a) img = ndimage.map_coordinates( - sym, np.moveaxis(pts, -1, 0).reshape(3, -1), order=1, mode="grid-wrap" + grid, np.moveaxis(pts, -1, 0).reshape(3, -1), order=1, mode="grid-wrap" ).reshape(su.shape) + q = np.linalg.norm(pts, axis=-1) / (self.cells * self.lattice_parameter) + fbar, f2 = self._fbar(q.ravel()) + laue = (f2 - fbar**2).reshape(q.shape) frac = pts / self.cells node_dist = np.linalg.norm(frac - np.round(frac), axis=-1) # 1/a units return img / laue, (u, v), node_dist @@ -1213,74 +1831,90 @@ def plot_fit( ) def plot_sro(self, n_shells: int = 8, extent: float = 2.0, layer: int = 0): - """Short-range order three ways. - - Left: Warren-Cowley alpha against neighbour distance (alpha < 0 - prefers unlike neighbours, > 0 like). Middle: the symmetrized diffuse - intensity of the supercell in Laue units (1 = random alloy) on the - (001) and (1-10) reciprocal planes, where ordering shows as diffuse - maxima at special points, e.g. 100 (B2-type) or 1/2 1/2 1/2 (D0_3); the color - scale is set away from the reciprocal-lattice nodes, so clustering (small-q - intensity on the nodes) saturates. - Right: one (001) layer of the supercell, B atoms dark, and its local - B concentration over the first two shells. + """Short-range order and displacements. + + 1: Warren-Cowley alpha of every species pair against neighbour + distance (< 0 unlike neighbours preferred, > 0 like). 2-3: the + symmetrized diffuse intensity of the supercell in Laue units (1 = + random alloy) on the (001) and (1-10) reciprocal planes; maxima at + special points name the order (100: B2-type, 1/2 1/2 1/2: D0_3, + 2/3 2/3 2/3: omega). The color scale is set away from the + reciprocal-lattice nodes. 4: one (001) layer of the supercell colored + by species, displaced atoms ringed. 5: fraction of each species + displaced along <111>. """ import matplotlib.pyplot as plt sro = self.warren_cowley(n_shells) - sec_001, _, d_001 = self.diffuse_section((0, 0, 1), extent) - sec_110, _, d_110 = self.diffuse_section((1, -1, 0), extent) - n = self.grid_size - d = self.grid_divisor - occ = np.full((n,) * 3, np.nan) - xs = self.site_x - occ[xs[:, 0], xs[:, 1], xs[:, 2]] = self.sigma - site = ~np.isnan(occ) - local = ndimage.gaussian_filter(np.nan_to_num(occ), d * 0.6, mode="wrap") / np.maximum( - ndimage.gaussian_filter(site.astype(float), d * 0.6, mode="wrap"), 1e-9 - ) - z = layer * d - sl = occ[:, :, z] - sl_sites = site[:, :, z] + grid = self.diffuse_grid() + sec_001, _, d_001 = self.diffuse_section((0, 0, 1), extent, grid=grid) + sec_110, _, d_110 = self.diffuse_section((1, -1, 0), extent, grid=grid) + K = len(self.species) + colors = plt.get_cmap("tab10")(np.arange(K)) - fig, axs = plt.subplots(1, 5, figsize=(22, 4.4)) + fig, axs = plt.subplots(1, 5, figsize=(24, 4.6)) ax = axs[0] ax.axhline(0, color="0.6", lw=0.8) - ax.stem(sro["radius"], sro["alpha"], basefmt=" ") + for s in range(K): + for t in range(s, K): + ax.plot( + sro["radius"], + sro["alpha"][:, s, t], + "o-" if s == t else "s--", + ms=4, + label=f"{self.species[s]}-{self.species[t]}", + ) ax.set_xlabel("neighbour distance (A)") - ax.set_ylabel(f"Warren-Cowley alpha ({sro['pair']})") - lim = max(0.1, 1.2 * np.abs(sro["alpha"]).max()) - ax.set_ylim(-lim, lim) - ext = [-extent, extent, -extent, extent] - # scale to the diffuse between the nodes, not the small-q peaks on them + ax.set_ylabel("Warren-Cowley alpha") + ax.legend(fontsize=8, ncol=2) between = np.concatenate([sec_001[d_001 > 0.2], sec_110[d_110 > 0.2]]) vmax = np.quantile(between, 0.995) + ext = [-extent, extent, -extent, extent] for ax, sec, title, xl, yl in ( - (axs[1], sec_001, "(001) section", "h", "k"), - (axs[2], sec_110, "(1-10) section", "[001]", "[110]/sqrt2"), + (axs[1], sec_001, "(001) section, Laue units", "h", "k"), + (axs[2], sec_110, "(1-10) section, Laue units", "[001]", "[110]/sqrt2"), ): - im = ax.imshow(sec.T, origin="lower", extent=ext, cmap="magma", vmin=0, vmax=vmax) - ax.set_title(f"{title}, Laue units") + im = ax.imshow( + sec.T, origin="lower", extent=ext, cmap="turbo_black", vmin=0, vmax=vmax + ) + ax.set_title(title) ax.set_xlabel(xl) ax.set_ylabel(yl) fig.colorbar(im, ax=ax, fraction=0.046) - rr, cc = np.nonzero(sl_sites) - axs[3].scatter( - cc / d, rr / d, c=sl[rr, cc], cmap="gray_r", s=6, vmin=-0.2, vmax=1.2, marker="s" - ) - axs[3].set_aspect("equal") - axs[3].set_title(f"(001) layer, {self.species[1]} dark") - axs[3].set_xlabel("cells") - im = axs[4].imshow( - local[:, :, z], - cmap="RdBu_r", - vmin=self.concentration - 0.3, - vmax=self.concentration + 0.3, - origin="lower", - extent=[0, self.cells, 0, self.cells], + + ax = axs[3] + z = layer * self.grid_divisor * self.refine + in_layer = self.site_x[:, 2] == z + xy = self.site_x[in_layer, :2] / (self.grid_divisor * self.refine) + sp = self.species_index[in_layer] + for s in range(K): + m = sp == s + ax.scatter(xy[m, 1], xy[m, 0], s=10, color=colors[s], label=self.species[s]) + moved = self._omega_like()[in_layer] + step = self.lattice_parameter / (self.grid_divisor * self.refine) + u = self.displacement[in_layer, :2] * step / self.lattice_parameter + ax.quiver( + xy[:, 1], + xy[:, 0], + u[:, 1], + u[:, 0], + angles="xy", + scale_units="xy", + scale=0.1, + width=0.003, + color="0.3", ) - axs[4].set_title(f"local {self.species[1]} fraction") - fig.colorbar(im, ax=axs[4], fraction=0.046) + ax.scatter(xy[moved, 1], xy[moved, 0], s=40, facecolors="none", edgecolors="k", lw=0.6) + ax.set_aspect("equal") + ax.set_title("(001) layer, displacements x10, omega ringed") + ax.set_xlabel("cells") + ax.legend(fontsize=8, loc="upper right") + + ax = axs[4] + summary = self.displacement_summary() + ax.bar(self.species, [summary["mean_displacement"][s] for s in self.species], color=colors) + ax.set_ylabel("mean static displacement (A)") + ax.set_title(f"static B = {summary['static_b']:.3f} A^2") fig.tight_layout() return fig, axs @@ -1305,6 +1939,17 @@ def _lsq_bounded(X: np.ndarray, y: np.ndarray, sw: np.ndarray, lo: np.ndarray) - return res.x / norm +def _disjoint_rows(sites: np.ndarray) -> list[int]: + """Rows of ``sites`` sharing no site with an earlier kept row.""" + seen, keep = set(), [] + for b, row in enumerate(sites): + r = row.tolist() + if seen.isdisjoint(r): + seen.update(r) + keep.append(b) + return keep + + def _integrate_spots(im: np.ndarray, positions: np.ndarray, radius: float) -> np.ndarray: """Integrated intensity inside ``radius`` px of each position, over the median of the ring out to 1.6 radius; NaN for spots off the detector.""" diff --git a/tests/diffraction/test_reverse_monte_carlo.py b/tests/diffraction/test_reverse_monte_carlo.py index abc523f67..0b5e6bdef 100644 --- a/tests/diffraction/test_reverse_monte_carlo.py +++ b/tests/diffraction/test_reverse_monte_carlo.py @@ -21,8 +21,9 @@ _atom_site_fract_y _atom_site_fract_z _atom_site_occupancy -Nb1 Nb 0 0 0 0.7 +Nb1 Nb 0 0 0 0.4 V1 V 0 0 0 0.3 +Zr1 Zr 0 0 0 0.3 """ @@ -54,44 +55,68 @@ def test_cubic_rotations(): assert len({o.tobytes() for o in ops}) == 24 -def test_binary_site_and_composition(rmc): - assert rmc.species == ["Nb", "V"] - assert rmc.concentration == pytest.approx(0.3) - assert len(rmc.sigma) == 2 * 4**3 - assert rmc.sigma.sum() == round(0.3 * len(rmc.sigma)) +def test_ternary_site_and_composition(rmc): + assert rmc.species == ["Nb", "V", "Zr"] + assert np.allclose(rmc.concentrations, [0.4, 0.3, 0.3]) + assert len(rmc.site_x) == 2 * 4**3 + counts = np.bincount(rmc.species_index, minlength=3) + assert counts.sum() == 128 + assert np.all(np.abs(counts - 128 * rmc.concentrations) <= 1) + + +def _score(rmc, d_fr, d_fi): + keep = rmc._keep / len(rmc.site_x) + d_int = (2 * (rmc._Fr * d_fr + rmc._Fi * d_fi) + d_fr**2 + d_fi**2) * keep + dm = rmc._scale * rmc._u * rmc._read(d_int[:, rmc._sym_index].mean(dim=1)) + return float((rmc._w * (dm**2 - 2 * rmc._r * dm)).sum()) def test_swap_score_matches_recompute(rmc): - """The incremental loss change of one swap equals the loss after recomputing G from scratch.""" + """The incremental loss change of one swap equals the loss after recomputing F from scratch.""" + loss0 = rmc._update_residual() + spec = rmc.species_index + j1 = int(np.nonzero(spec == 0)[0][0]) + j2 = int(np.nonzero(spec == 1)[0][0]) + c1, s1 = rmc._phases(rmc._positions(np.array([j1]))) + c2, s2 = rmc._phases(rmc._positions(np.array([j2]))) + df = rmc._fs[1] - rmc._fs[0] + dL = _score(rmc, df * (c1 - c2), df * (s2 - s1)) + spec[j1], spec[j2] = 1, 0 + rmc._recompute_F() + assert rmc._update_residual() - loss0 == pytest.approx(dL, rel=1e-3, abs=1e-4 * loss0) + + +def test_displacement_score_matches_recompute(rmc): loss0 = rmc._update_residual() - j_on = int(np.nonzero(~rmc.sigma)[0][0]) - j_off = int(np.nonzero(rmc.sigma)[0][0]) - n_sites = len(rmc.sigma) - c1, s1 = rmc._phases(torch.tensor([j_on])) - c2, s2 = rmc._phases(torch.tensor([j_off])) - dGr, dGi = c1 - c2, -s1 + s2 - d_int = (2 * (rmc._Gr * dGr + rmc._Gi * dGi) + dGr**2 + dGi**2) / n_sites - d_used = d_int[:, rmc._sym_index].mean(dim=1) - dm = rmc._scale * rmc._u * rmc._read(d_used) - dL = float((rmc._w * (dm**2 - 2 * rmc._r * dm)).sum()) - - rmc.sigma[j_on], rmc.sigma[j_off] = True, False - rmc._recompute_G() - loss1 = rmc._update_residual() - assert loss1 - loss0 == pytest.approx(dL, rel=1e-3, abs=1e-4 * loss0) + j = np.array([5]) + new = np.array([[1, -1, 1]]) + co, so = rmc._phases(rmc._positions(j)) + cn, sn = rmc._phases(rmc._positions(j, new)) + f = rmc._fs[int(rmc.species_index[5])] + dL = _score(rmc, f * (cn - co), f * (so - sn)) + rmc.displacement[5] = new[0] + rmc._recompute_F() + assert rmc._update_residual() - loss0 == pytest.approx(dL, rel=1e-3, abs=1e-4 * loss0) def test_run_lowers_loss_and_keeps_composition(rmc): - n_b = rmc.sigma.sum() + counts = np.bincount(rmc.species_index) rmc.run(n_sweeps=3, batch=8, progress=False) - assert rmc.sigma.sum() == n_b + assert np.array_equal(np.bincount(rmc.species_index), counts) assert rmc.loss_history[-1] <= rmc.loss_history[0] + 1e-6 def test_warren_cowley_random_is_near_zero(rmc): sro = rmc.warren_cowley(n_shells=2) assert np.allclose(sro["radius"], [3.2 * np.sqrt(3) / 2, 3.2], atol=1e-3) - assert np.all(np.abs(sro["alpha"]) < 0.15) + assert sro["alpha"].shape == (2, 3, 3) + assert np.all(np.abs(sro["alpha"]) < 0.35) + + +def test_refine_tilts_runs(rmc): + rmc.refine_tilts(max_tilt_deg=0.5, step_deg=0.25, verbose=False) + rmc.refine_tilts(max_tilt_deg=0.5, step_deg=0.25, max_grid=rmc.grid_size // 2, verbose=False) + assert all(np.rad2deg(np.linalg.norm(t)) <= 0.6 for t in rmc.geometry["tilts"]) def test_mask_is_zero_on_bragg_peaks(rmc): @@ -107,3 +132,74 @@ def test_sro_section_random_is_near_laue(rmc): img, _, node_dist = rmc.diffuse_section((0, 0, 1), extent=1.0, smooth=True) between = img[node_dist > 0.2] assert 0.5 < between.mean() < 1.5 + + +def test_omega_embryo_is_a_collapsed_row_and_scores_exactly(rmc): + sites, new = rmc._omega_proposals(4) + assert sites.shape[1] == 3 and len(set(sites.ravel())) == sites.size + xc = rmc.site_x[sites] // rmc.refine + step = np.mod(xc[:, 1] - xc[:, 0], rmc.cells * 2) + step = np.where(step > rmc.cells, step - 2 * rmc.cells, step) + assert np.all(np.abs(step) == 1) # nearest neighbours along <111> + vec = new + assert np.all(vec[:, 0] == 0) and np.all(vec[:, 1] == -vec[:, 2]) + + loss0 = rmc._update_residual() + d_fr = torch.zeros(len(rmc._needed)) + d_fi = torch.zeros(len(rmc._needed)) + for k in range(3): + j = sites[:1, k] + co, so = rmc._phases(rmc._positions(j)) + cn, sn = rmc._phases(rmc._positions(j, new[:1, k])) + f = rmc._fs[int(rmc.species_index[j[0]])] + d_fr += (f * (cn - co))[0] + d_fi += (f * (so - sn))[0] + dL = _score(rmc, d_fr[None], d_fi[None]) + rmc.displacement[sites[0]] = new[0] + rmc._recompute_F() + assert rmc._update_residual() - loss0 == pytest.approx(dL, rel=1e-3, abs=1e-4 * loss0) + + +def test_fitted_envelope_does_not_raise_loss(rmc): + loss_measured = rmc._update_residual() + rmc.envelope = "fitted" + rmc._setup_forward() + rmc._solve_linear(rmc._model_diffuse()) + assert rmc._update_residual() <= loss_measured * (1 + 1e-6) + assert all(np.all(p >= 0) for p in rmc._env_p) + + +def test_static_b_cap_holds(rmc): + rmc.run(n_sweeps=3, batch=8, omega_fraction=1.0, max_static_b=0.2, progress=False) + assert rmc.static_b() <= 0.2 + 1e-9 + assert len(rmc._omega_vectors) == 16 # two amplitudes x eight <111> senses + + +def test_omega_chain_repeats_along_one_row(rmc): + sites, new = rmc._omega_proposals(2, repeats=2) + assert sites.shape[1] == 6 + assert np.array_equal(new[:, :3], new[:, 3:]) + xc = rmc.site_x[sites] // rmc.refine + n = rmc.cells * 2 + steps = np.mod(np.diff(xc, axis=1), n) + steps = np.where(steps > n // 2, steps - n, steps) + assert np.all(steps == steps[:, :1]) # one straight <111> row + + +def test_size_effect_chi_is_odd_about_bragg_nodes(rmc): + a = rmc.lattice_parameter + g = np.array([1.0, 1.0, 0.0]) / a # allowed BCC reflection + kappa = np.array([[0.02, 0.005, -0.01], [0.0, 0.03, 0.01]]) + plus = rmc._size_chi(g + kappa) + minus = rmc._size_chi(g - kappa) + assert np.all(np.abs(plus) > 0) + assert np.allclose(plus, -minus, rtol=0.2) + far = rmc._size_chi(g + 4 * kappa) + assert np.all(np.abs(far) < np.abs(plus)) # grows toward the node + + +def test_size_effect_radii_orders_species(rmc): + rmc.set_size_effect("radii") + eta = dict(zip(rmc.species, rmc.size_eta)) + assert eta["V"] < eta["Nb"] < eta["Zr"] + assert abs((rmc.concentrations * rmc.size_eta).sum()) < 1e-12 From f424e1564048a27ff188d0237e474bc5ea0cd6db Mon Sep 17 00:00:00 2001 From: cophus Date: Sun, 4 Oct 2026 09:36:19 -0700 Subject: [PATCH 26/36] updates --- src/quantem/diffraction/bloch.py | 4 +- .../diffraction/reverse_monte_carlo.py | 740 +++++++++++------- tests/diffraction/test_reverse_monte_carlo.py | 48 +- 3 files changed, 480 insertions(+), 312 deletions(-) diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py index 408aa38d3..e50218f37 100644 --- a/src/quantem/diffraction/bloch.py +++ b/src/quantem/diffraction/bloch.py @@ -252,7 +252,8 @@ def dynamical_pattern( ------- dict 'qx', 'qy' (N,), 'hkl' (N, 3), 'intensity' (T, N) diffracted - intensities per thickness, 's_g' (N,). + intensities per thickness, 'intensity_000' (T,) the direct beam, + 's_g' (N,). """ if crystal.g_vec is None: raise RuntimeError("Run crystal.calculate_structure_factors() first.") @@ -308,6 +309,7 @@ def dynamical_pattern( "hkl": hkl_sel, "s_g": s_sel, "intensity": intensity, + "intensity_000": torch.abs(psi[:, 0]) ** 2, "thicknesses": t, } diff --git a/src/quantem/diffraction/reverse_monte_carlo.py b/src/quantem/diffraction/reverse_monte_carlo.py index 42a46f002..8e0417bf0 100644 --- a/src/quantem/diffraction/reverse_monte_carlo.py +++ b/src/quantem/diffraction/reverse_monte_carlo.py @@ -11,11 +11,10 @@ collapsed toward each other by a/12); each changes ``F`` by a few phase factors, so every move is scored exactly without recomputing the supercell. -Bragg peaks and the direct-beam bloom are excluded by sigmoid weights; a -smooth background (constant, direct-beam Lorentzian, a wide Gaussian about -the zone-axis pole, Einstein thermal diffuse, phonon halos about each -reflection, powder rings and the Bragg cores blurred by the detector's point -spread) and the diffuse scale are solved in closed form. +Bragg peaks with their tails and the direct-beam bloom are masked by sigmoid +weights; a smooth background (constant, direct-beam Lorentzian, a wide +Gaussian about the zone-axis pole, Einstein thermal diffuse and powder rings) +and the diffuse envelope are solved in closed form. Electrons barely separate species of neighbouring atomic number (Nb and Zr differ by a few percent in scattering factor), so their relative arrangement @@ -159,6 +158,34 @@ def _binned(self, a: np.ndarray, reduce: str = "mean") -> np.ndarray: out = a[:ny, :nx].reshape(ny // b, b, nx // b, b).sum(axis=(1, 3)) return out / b**2 if reduce == "mean" else out + # rebuilt from the saved state by _post_load: pass as ``skip`` to ``save`` to keep files small + DERIVED_ATTRIBUTES = ( + "_W_all", "_Wc", "_Wv", "_sym_index", "_needed", "_used", "_h_needed", "_cos", "_sin", + "_Fr", "_Fi", "_fs", "_keep", "_u", "_u_all", "_y", "_w", "_y_all", "_w_all", "_img", + "_fit", "_r", "_bg", "_scale", "_basis_all", "_pix_image", "_env_cols", "_site_lookup", + ) # fmt: skip + + def _post_load(self) -> None: + """AutoSerialize hook: rebuild the derived arrays skipped at save time.""" + if getattr(self, "site_x", None) is None or hasattr(self, "_W_all"): + return + self.device = torch.device(_default_device()) + nc = self.cells * self.grid_divisor + self._site_lookup = np.full((nc,) * 3, -1, dtype=np.int64) + xc = self.site_x // self.refine + self._site_lookup[xc[:, 0], xc[:, 1], xc[:, 2]] = np.arange(len(self.site_x)) + env_p = getattr(self, "_env_p", None) + self._setup_forward() + if self.envelope == "fitted" and env_p is not None: + self._env_p = list(env_p) + self._u_all = np.concatenate( + [self._env_cols[i] @ np.asarray(self._env_p[i]) for i in range(len(self.images))] + ) + self._u = torch.as_tensor( + self._u_all[self._fit], dtype=torch.float32, device=self.device + ) + self._update_residual() + # ------------------------------------------------------------------ crystal def set_crystal( @@ -440,6 +467,173 @@ def resid(x): ) return sol.x[:2], float(np.exp(sol.x[2])) + def _orientation_quat(self, i: int, tilt: np.ndarray) -> torch.Tensor: + """Quaternion rotating crystal vectors into the lab frame of pattern i at a tilt. + + The zone axis is turned toward ``(tilt_x, tilt_y, 1)``, which puts the Bloch excitation + errors on the same Laue circle as the diffuse model's Ewald sphere.""" + from quantem.diffraction.rotations import quat_from_matrix + + n = np.array([tilt[0], tilt[1], 1.0]) + n /= np.linalg.norm(n) + z = np.array([0.0, 0.0, 1.0]) + axis = np.cross(z, n) + s_ang, c_ang = np.linalg.norm(axis), n[2] + if s_ang < 1e-12: + rot = np.eye(3) + else: + k = axis / s_ang + kx = np.array([[0, -k[2], k[1]], [k[2], 0, -k[0]], [-k[1], k[0], 0]]) + rot = np.eye(3) + s_ang * kx + (1 - c_ang) * kx @ kx + u = rot @ _zone_frame(self.zone_axes[i]) + return quat_from_matrix(torch.as_tensor(u, dtype=torch.float64)) + + def _measured_bragg(self, i: int): + """Measured integrated intensities of pattern i: hkl (n, 3) with the direct beam first.""" + hkl = np.asarray(self.geometry["bragg_hkl"][i]) + meas = np.asarray(self.geometry["bragg_intensity"][i], dtype=float) + nz = np.linalg.norm(hkl, axis=1) > 0 + hkl, meas = hkl[nz], meas[nz] + i000 = _integrate_spots( + self.images[i], self.geometry["centers"][i][None], 0.03 / self.sampling + )[0] + hkl = np.vstack([[0, 0, 0], hkl]) + meas = np.concatenate([[i000], meas]) + ok = np.isfinite(meas) + return hkl[ok].astype(int), np.clip(meas[ok], 0, None) + + def fit_thickness( + self, + thickness: tuple[float, float] = (20.0, 1000.0), + step: float = 10.0, + tilt_range_deg: float = 0.6, + tilt_step_deg: float = 0.1, + k_max: float = 1.6, + depth_samples: int = 24, + verbose: bool = True, + ) -> dict: + """Bloch-wave thickness and tilt of every pattern from its Bragg intensities. + + For each pattern, a grid of tilts about the current one and of thicknesses is scored by + comparing the Bloch exit intensities of the zone reflections with the measured + integrated intensities (each set normalized to unit sum, compared as square roots). + Absorptive (Weickenmeier-Kohl) structure factors of the average crystal are used. The + best thickness and tilt are stored, with each beam's intensity averaged over depth, + ``(1/t) int_0^t |phi_g(z)|^2 dz``: the beams that generate diffuse scattering inside + the foil, used by ``envelope="bloch"``. + """ + from quantem.diffraction import bloch + + crystal = self.crystal + crystal.calculate_structure_factors(2 * k_max) + crystal.calculate_dynamical_structure_factors(self.energy, k_max=2 * k_max) + t_grid = np.arange(thickness[0], thickness[1] + 1e-9, step) + geo = self.geometry + geo.setdefault("thickness", [np.nan] * len(self.images)) + geo.setdefault("bloch_g", [None] * len(self.images)) + geo.setdefault("bloch_p", [None] * len(self.images)) + geo.setdefault("bloch_score", [None] * len(self.images)) + results = {} + for i in range(len(self.images)): + hkl_m, meas = self._measured_bragg(i) + a_meas = np.sqrt(meas / meas.sum()) + keys = {tuple(h): k for k, h in enumerate(hkl_m)} + tilt0 = np.asarray(geo["tilts"][i], dtype=float) + offs = np.deg2rad(np.arange(-tilt_range_deg, tilt_range_deg + 1e-9, tilt_step_deg)) + best = (np.inf, None, None, None) + curves = {} + for dx in offs: + for dy in offs: + tilt = tilt0 + np.array([dx, dy]) + out = bloch.dynamical_pattern( + crystal, self._orientation_quat(i, tilt), t_grid, self.energy, k_max=k_max + ) + calc = np.zeros((len(t_grid), len(hkl_m))) + calc[:, keys[(0, 0, 0)]] = out["intensity_000"].numpy() + for col, h in enumerate(out["hkl"].numpy().astype(int)): + k = keys.get(tuple(h)) + if k is not None: + calc[:, k] = out["intensity"][:, col].numpy() + a_calc = np.sqrt(calc / calc.sum(1, keepdims=True)) + score = ((a_calc - a_meas[None]) ** 2).sum(1) + curves[(dx, dy)] = score + j = int(np.argmin(score)) + if score[j] < best[0]: + best = (score[j], tilt, t_grid[j], (dx, dy)) + score, tilt, t_best, key = best + # depth-averaged beam intensities at the best thickness and tilt + z = (np.arange(depth_samples) + 0.5) / depth_samples * t_best + out = bloch.dynamical_pattern( + crystal, self._orientation_quat(i, tilt), z, self.energy, k_max=k_max + ) + frame = _zone_frame(self.zone_axes[i]) + hkl_b = np.vstack([[0, 0, 0], out["hkl"].numpy()]) + g_b = ( + (hkl_b / self._a_crystal) @ frame[:2].T * self._a_crystal / self.lattice_parameter + ) + p_b = np.concatenate( + [[out["intensity_000"].mean().item()], out["intensity"].mean(0).numpy()] + ) + geo["tilts"][i] = tilt + geo["thickness"][i] = float(t_best) + geo["bloch_g"][i] = g_b + geo["bloch_p"][i] = p_b + geo["bloch_score"][i] = dict(thickness=t_grid, score=curves[key], best=float(score)) + results[self.names[i]] = dict( + thickness=float(t_best), tilt_deg=float(np.rad2deg(np.linalg.norm(tilt))) + ) + if verbose: + print( + f"{self.names[i]}: thickness {t_best / 10:.0f} nm, tilt " + f"{np.rad2deg(np.linalg.norm(tilt)):.2f} deg, misfit {score:.3f}" + ) + return results + + def plot_thickness(self, **kwargs): + """Bloch thickness fit per pattern: misfit against thickness (top) and measured against + calculated Bragg intensities at the best fit, square-root scale (bottom).""" + import matplotlib.pyplot as plt + + from quantem.diffraction import bloch + + n = len(self.images) + fig, axs = plt.subplots(2, n, figsize=kwargs.pop("figsize", (4.2 * n, 7.5))) + for i in range(n): + sc = self.geometry["bloch_score"][i] + ax = axs[0, i] + ax.plot(sc["thickness"] / 10, sc["score"], "k-") + t_best = self.geometry["thickness"][i] + ax.axvline(t_best / 10, color="tab:red", lw=1) + ax.set_xlabel("thickness (nm)") + ax.set_ylabel("misfit") + ax.set_title(f"{self.names[i]}: {t_best / 10:.0f} nm") + hkl_m, meas = self._measured_bragg(i) + out = bloch.dynamical_pattern( + self.crystal, + self._orientation_quat(i, self.geometry["tilts"][i]), + [t_best], + self.energy, + k_max=1.6, + ) + lookup = { + tuple(h): v + for h, v in zip(out["hkl"].numpy().astype(int), out["intensity"][0].numpy()) + } + lookup[(0, 0, 0)] = float(out["intensity_000"][0]) + calc = np.array([lookup.get(tuple(h), 0.0) for h in hkl_m]) + ax = axs[1, i] + x, y = np.sqrt(calc / calc.sum()), np.sqrt(meas / meas.sum()) + ax.plot(x, y, "o", ms=4, color="tab:blue") + lim = 1.05 * max(x.max(), y.max()) + ax.plot([0, lim], [0, lim], "k--", lw=0.8) + ax.set_xlim(0, lim) + ax.set_ylim(0, lim) + ax.set_aspect("equal") + ax.set_xlabel("Bloch sqrt(I)") + ax.set_ylabel("measured sqrt(I)") + fig.tight_layout() + return fig, axs + def bragg_positions(self, i: int, k_max: float = 3.0) -> np.ndarray: """Detector positions (row, col) of every zone reflection, direct beam included.""" _, g2, _ = self._zone_reflections(self.zone_axes[i], k_max) @@ -462,14 +656,13 @@ def _q_crystal(self, i: int, q2: np.ndarray) -> np.ndarray: def set_mask( self, - bragg_radius: float = 0.08, + bragg_radius: float = 0.12, softness: float = 0.01, q_max: float = 1.2, center_radius: float = 0.25, edge_px: int = 8, - tail_widths: tuple[float, ...] = (0.02, 0.06, 0.15), ) -> "ReverseMonteCarlo": - """Diffuse-scattering weight, binned data, Bragg intensities and PSF tails. + """Diffuse-scattering weight, binned data and Bragg intensities. The weight is ``sigmoid((d - bragg_radius) / softness)``, with ``d`` the distance (1/A) to the nearest reflection, the direct beam @@ -479,18 +672,17 @@ def set_mask( ``center_radius``. Each ``bin_factor`` square is reduced to its weighted mean. + The mask has to cover the peaks' tails (detector point spread and + near-peak scattering), which are not modelled: here they fall to a few + percent of the local diffuse level by 0.12 1/A. + Each reflection's integrated intensity weights the diffuse envelope. - The Bragg cores blurred by ``(1 + (r / width)^2)^-1.5`` kernels, one - per ``tail_widths`` (1/A), are background terms for the detector's - point-spread tails. """ - from scipy.signal import fftconvolve - b = self.bin_factor out = dict( bragg_radius=bragg_radius, softness=softness, q_max=q_max, center_radius=center_radius ) - out.update(y=[], w=[], k=[], data=[], scale=[], bragg_k=[], bragg_intensity=[], tails=[]) + out.update(y=[], w=[], k=[], data=[], scale=[], bragg_k=[], bragg_intensity=[]) for i, im in enumerate(self.images): ny, nx = (im.shape[0] // b) * b, (im.shape[1] // b) * b rows, cols = np.mgrid[0:ny, 0:nx].astype(np.float64) @@ -531,17 +723,6 @@ def set_mask( yb = np.where(wb > 0, self._binned(w * y, "sum") / np.maximum(wb, 1e-12), 0.0) rb, cb = np.mgrid[0 : ny // b, 0 : nx // b].astype(np.float64) * b + (b - 1) / 2 norm = (wb * yb).sum() / wb.sum() - tails = [] - for width in tail_widths: - gpx = width / self.sampling - half = int(min(6 * gpx, 200)) - rr = np.hypot(*np.mgrid[-half : half + 1, -half : half + 1]) - kern = (1 + (rr / gpx) ** 2) ** -1.5 - t = fftconvolve(core_sig, kern / kern.sum(), mode="same") - tails.append( - np.where(wb > 0, self._binned(w * t, "sum") / np.maximum(wb, 1e-12), 0.0) - / norm - ) out["y"].append(yb / norm) out["w"].append(wb / b**2) out["k"].append(self._q_zone(i, rb, cb)) @@ -549,7 +730,6 @@ def set_mask( out["scale"].append(norm) out["bragg_k"].append(g_q[seen]) out["bragg_intensity"].append(inten[seen] / norm) - out["tails"].append(np.stack(tails)) self.mask = out return self @@ -566,7 +746,6 @@ def build_supercell( envelope: str = "measured", resolution: float = 0.75, shared_scale: bool = True, - kikuchi: bool | int = False, max_beams: int = 40, device: str | None = None, ) -> "ReverseMonteCarlo": @@ -594,7 +773,7 @@ def build_supercell( Average every pattern over the 24 cubic rotations of the supercell. debye_waller : float Isotropic B (A^2) damping the diffuse intensity. - envelope : {"measured", "fitted", "kinematic"} + envelope : {"measured", "fitted", "bloch", "kinematic"} "measured" redistributes the diffuse intensity over the Bragg beams of each pattern, ``sum_g P_g fbar^2(q - g) / fbar^2(q)`` with P_g the integrated Bragg intensities (exact for occupational @@ -603,8 +782,10 @@ def build_supercell( and re-solves the non-negative P_g of each pattern with its background: diffuse scattering is generated by the beams' depth averaged intensities, which dynamical diffraction makes differ - from their exit intensities. "kinematic" keeps only the direct - beam. + from their exit intensities. "bloch" takes the depth-averaged + Bloch-wave beam intensities at each pattern's fitted thickness and + tilt (``fit_thickness``), with one diffuse scale for all patterns. + "kinematic" keeps only the direct beam. resolution : float Gaussian sigma, in supercell reciprocal-grid steps, with which each pixel reads the diffuse grid (27 nearest points). A finite @@ -614,13 +795,6 @@ def build_supercell( shared_scale : bool One diffuse scale for every pattern; the envelope carries each pattern's absolute Bragg intensities. - kikuchi : bool - Add Kikuchi bands to the background: for the three lowest-order - reflection families of each zone, a band interior and its edge - lines, ``|(q - pole) . g_hat| = |g| / 2`` about the zone-axis pole - ``-K tilt``, each with a free-signed amplitude (excess or - deficit). They follow the fitted tilt, so ``refine_tilts`` feels them. - An integer sets the number of families (True: 3). max_beams : int Strongest Bragg beams of each pattern in the diffuse envelope. device : str, optional @@ -667,8 +841,6 @@ def build_supercell( self.envelope = envelope self.resolution = float(resolution) self.shared_scale = bool(shared_scale) - self.kikuchi = bool(kikuchi) - self._kikuchi_families = 3 if kikuchi is True else int(kikuchi) self.max_beams = int(max_beams) self.device = torch.device(device or _default_device()) self._setup_forward() @@ -699,11 +871,10 @@ def _positions(self, sites: np.ndarray, disp: np.ndarray | None = None) -> np.nd "Al": 1.43, } - def _pixel_grid(self, i: int, q2: np.ndarray, n: int | None = None): + def _pixel_grid(self, i: int, q2: np.ndarray): """Grid indices and weights (n, 8) trilinear, or (n, 27) Gaussian of ``resolution`` - steps, for in-plane q of pattern i, on a grid of ``n`` points per edge (default the - supercell grid; the reciprocal sampling is the same on any coarser grid).""" - n = self.grid_size if n is None else n + steps, for in-plane q of pattern i.""" + n = self.grid_size h = self._q_crystal(i, q2) * self.lattice_parameter * self.cells if self.resolution > 0: stencil = np.array(list(product((-1, 0, 1), repeat=3))) @@ -735,14 +906,9 @@ def _setup_forward(self): vals_w.append(wts) u, tds = self._envelope(i, q2) u_all.append(u) - tails = self.mask["tails"][i].reshape(len(self.mask["tails"][i]), -1).T qm = np.linalg.norm(q2, axis=1) - halos = self._phonon_halos(i, q2) rings = self._powder_rings(qm) - extra = [self._kikuchi(i, q2)] if getattr(self, "kikuchi", False) else [] - basis_all.append( - np.column_stack([np.ones_like(qm), qm, q2, tds, halos, rings, tails, *extra]) - ) + basis_all.append(np.column_stack([np.ones_like(qm), qm, q2, tds, rings])) pix_image.append(np.full(len(q2), i)) cols = np.concatenate(cols_w) vals = np.concatenate(vals_w) @@ -814,6 +980,15 @@ def _setup_forward(self): self.coefficients = c self._update_residual() + def set_envelope(self, envelope: str) -> "ReverseMonteCarlo": + """Switch the diffuse envelope ("fitted", "measured", "bloch", "kinematic") and refit the + scale and background for the current supercell.""" + self.envelope = envelope + self._setup_forward() + self._solve_linear(self._model_diffuse(), refit_sigmas=True) + self._update_residual() + return self + def _grid_factors(self, h: np.ndarray, n: int | None = None): """Scattering factors (K, n) of every species at grid points h (n, 3), and a 0/1 weight removing the Bragg nodes of the average lattice.""" @@ -981,7 +1156,14 @@ def _envelope(self, i: int, q2: np.ndarray): Also stores the per-beam envelope columns ``fbar^2(q - g) / fbar^2(q) x DW(q)`` and the beam weights, which ``envelope="fitted"`` re-solves. """ - if self.envelope in ("measured", "fitted"): + if self.envelope == "bloch": + g = self.geometry["bloch_g"][i] + p = self.geometry["bloch_p"][i] + if g is None: + raise RuntimeError("fit_thickness before envelope='bloch'") + order = np.argsort(p)[::-1][: getattr(self, "max_beams", 40)] + g, p = g[order], p[order] + elif self.envelope in ("measured", "fitted"): g = self.mask["bragg_k"][i] p = self.mask["bragg_intensity"][i] order = np.argsort(p)[::-1][: getattr(self, "max_beams", 40)] @@ -1007,34 +1189,10 @@ def _envelope(self, i: int, q2: np.ndarray): self._env_p[i] = p.astype(float) return cols @ p, tds - def _kikuchi(self, i: int, q2: np.ndarray, tilt: np.ndarray | None = None) -> np.ndarray: - """Kikuchi band interiors and edge lines (n, 2 * families) about the zone-axis pole.""" - n_families = self._kikuchi_families - tilt = self.geometry["tilts"][i] if tilt is None else tilt - pole = -np.asarray(tilt) / self.wavelength - hkl, g2, _ = self._zone_reflections(self.zone_axes[i], 1.2) - g2 = g2 * self._a_crystal / self.lattice_parameter - gl = np.linalg.norm(g2, axis=1) - nz = gl > 1e-6 - g2, gl = g2[nz], gl[nz] - radii = np.unique(np.round(gl, 3))[:n_families] - out = np.zeros((len(q2), 2 * len(radii))) - width = 0.015 - for f, rad in enumerate(radii): - for g, length in zip(g2[np.abs(gl - rad) < 2e-3], gl[np.abs(gl - rad) < 2e-3]): - d = (q2 - pole) @ (g / length) - out[:, 2 * f] += _sigmoid((0.5 * length - np.abs(d)) / 0.01) - out[:, 2 * f + 1] += np.exp(-0.5 * ((np.abs(d) - 0.5 * length) / width) ** 2) - if len(radii) < n_families: - out = np.column_stack([out, np.zeros((len(q2), 2 * (n_families - len(radii))))]) - return out - def _bg_lower(self, n: int) -> np.ndarray: """Lower bounds of the background amplitudes: free sign for the constant and Kikuchi.""" lo = np.zeros(n) lo[0] = -np.inf - if getattr(self, "kikuchi", False): - lo[-2 * self._kikuchi_families :] = -np.inf return lo def _powder_rings(self, q: np.ndarray, width: float = 0.025, k_max: float = 1.5) -> np.ndarray: @@ -1050,20 +1208,6 @@ def _powder_rings(self, q: np.ndarray, width: float = 0.025, k_max: float = 1.5) out += fi / gi**2 * np.exp(-0.5 * ((q - gi) / width) ** 2) return out / out.max() - def _phonon_halos(self, i: int, q2: np.ndarray, widths=(0.04, 0.12)) -> np.ndarray: - """Thermal diffuse halos about every Bragg spot: ``sum_g P_g k^2 / (|q - g|^2 + k^2)`` - per width ``k`` (1/A), acoustic phonons concentrating TDS next to each reflection.""" - g = self.mask["bragg_k"][i] - p = self.mask["bragg_intensity"][i] - nz = np.linalg.norm(g, axis=1) > 1e-6 - g, p = g[nz], p[nz] / max(p[nz].sum(), 1e-12) - out = np.zeros((len(q2), len(widths))) - for gi, pi in zip(g, p): - d2 = ((q2 - gi) ** 2).sum(1) - for k, width in enumerate(widths): - out[:, k] += pi * width**2 / (d2 + width**2) - return out - def _phases(self, pos: np.ndarray): """cos and sin of 2 pi h.x / N for fine-grid positions (B, 3) on every needed point.""" p = torch.as_tensor(pos, dtype=torch.int32, device=self.device) @@ -1232,8 +1376,8 @@ def unpack(x): def fit_background(self) -> float: """Fit the diffuse scale and, per pattern, a constant, a direct-beam Lorentzian, a wide - Gaussian with a free center, Einstein thermal diffuse, phonon halos about each - reflection, powder rings of the average crystal and Bragg tails.""" + Gaussian with a free center, Einstein thermal diffuse and powder rings of the + average crystal.""" loss = self._solve_linear(self._model_diffuse(), refit_sigmas=True) self._update_residual() print( @@ -1272,7 +1416,6 @@ def run( batch: int = 32, temperature: float = 0.05, omega_fraction: float = 0.5, - omega_repeats: Sequence[int] = (1,), max_static_b: float | None = 0.5, refit_every: int = 2, progress: bool = True, @@ -1284,11 +1427,8 @@ def run( unlike atoms (composition conserved) or, with probability ``omega_fraction``, an omega embryo on three consecutive atoms of a <111> row: the first stays and the next two collapse toward each - other by a/12 each, (0, +v, -v), repeated along the row a number of - times drawn from ``omega_repeats`` (one triple makes a compact - embryo; longer chains make the diffuse sheets normal to <111> that - cut a zone as streaks). Proposing a chain where it already stands - clears it instead. ``max_static_b`` (A^2) caps the + other by a/24 or a/12 each, (0, +v, -v). Proposing an embryo where it + already stands clears it instead. ``max_static_b`` (A^2) caps the static Debye-Waller factor of all displacements, 8 pi^2 / 3, so they stay consistent with how slowly the Bragg intensities fall off (a displacement field this strong would damp them; the diffuse scale @@ -1324,7 +1464,7 @@ def run( roll = self.rng.random() if roll < p_omega: kind = "omega" - sites, new = self._omega_proposals(batch, int(self.rng.choice(omega_repeats))) + sites, new = self._omega_proposals(batch) else: kind = "swap" j = self.rng.choice(n_sites, 2 * batch, replace=False) @@ -1420,104 +1560,25 @@ def run( self.loss_history[-1] = self._update_residual() return self - def _omega_proposals(self, batch: int, repeats: int = 1): - """Disjoint omega chains along <111> rows: sites (B, 3 * repeats) and their new - displacement vectors (B, 3 * repeats, 3). - - Each chain repeats the (0, +v, -v) collapse ``repeats`` times along one row.""" + def _omega_proposals(self, batch: int): + """Disjoint omega embryos along <111> rows: sites (B, 3) and their new displacement + vectors (B, 3, 3), the (0, +v, -v) collapse.""" n_sites = len(self.site_x) nc = self._site_lookup.shape[0] j0 = self.rng.choice(n_sites, batch, replace=False) vec = self._omega_vectors[self.rng.integers(0, len(self._omega_vectors), batch)] step = np.sign(vec) # nearest neighbour along that <111>, in site units xc = self.site_x[j0] // self.refine - length = 3 * repeats sites = np.stack( - [self._site_lookup[tuple(np.mod(xc + t * step, nc).T)] for t in range(length)], 1 + [self._site_lookup[tuple(np.mod(xc + t * step, nc).T)] for t in range(3)], 1 ) - triple = np.stack([np.zeros_like(vec), vec, -vec], 1) # (B, 3, 3) - pattern = np.tile(triple, (1, repeats, 1)) + pattern = np.stack([np.zeros_like(vec), vec, -vec], 1) # (B, 3, 3) current = self.displacement[sites] standing = np.all(current == pattern, axis=(1, 2)) new = np.where(standing[:, None, None], 0, pattern) keep = _disjoint_rows(sites) return sites[keep], new[keep] - def refine_tilts( - self, - max_tilt_deg: float = 2.5, - step_deg: float = 0.25, - patterns: Sequence[int] | None = None, - max_grid: int = 400, - verbose: bool = True, - ) -> "ReverseMonteCarlo": - """Refine each pattern's tilt against the diffuse fit of the current supercell. - - The tilt moves where the Ewald sphere cuts the 3D diffuse intensity. - For each pattern, a grid of tilts within ``max_tilt_deg`` of the - zone axis is scored by reading the full symmetrized supercell - intensity and re-solving that pattern's scale and background, then - the best is polished with Nelder-Mead. Run it after some RMC sweeps, - once the supercell has structure, and alternate the two. - """ - grid = self.diffuse_grid(max_grid) - n_grid = grid.shape[0] - grid = grid.ravel() - y = self._y_all - w = self._w_all - patterns = range(len(self.images)) if patterns is None else patterns - for i in patterns: - sel = np.nonzero((self._pix_image == i) & self._fit)[0] - q2 = self.mask["k"][i].reshape(-1, 2)[sel - np.nonzero(self._pix_image == i)[0][0]] - u = self._u_all[sel] - X_bg = self._bg_basis(i, sel) - sw = np.sqrt(w[sel]) - lo = np.concatenate([[0.0], self._bg_lower(X_bg.shape[1])]) - tilt0 = np.array(self.geometry["tilts"][i], dtype=float) - kik = getattr(self, "kikuchi", False) - - def loss(t): - self.geometry["tilts"][i] = np.asarray(t) - cols, wts = self._pixel_grid(i, q2, n_grid) - if kik: - X_bg[:, -2 * self._kikuchi_families :] = self._kikuchi(i, q2, t) - X = np.column_stack([u * (grid[cols] * wts).sum(1), X_bg]) - x = _lsq_bounded(X, y[sel], sw, lo) - return float(((X @ x - y[sel]) ** 2 * w[sel]).sum()) - - l0 = loss(tilt0) - lim = np.deg2rad(max_tilt_deg) - steps = np.arange(-lim, lim + 1e-12, np.deg2rad(step_deg)) - best = (l0, tilt0) - for tx in steps: - for ty in steps: - t = np.array([tx, ty]) - if np.hypot(tx, ty) > lim: - continue - lt = loss(t) - if lt < best[0]: - best = (lt, t) - sol = optimize.minimize( - loss, - best[1], - method="Nelder-Mead", - bounds=[(-lim, lim)] * 2, - options=dict(xatol=1e-4, fatol=1e-6), - ) - t = sol.x if sol.fun < best[0] and np.linalg.norm(sol.x) <= lim else best[1] - self.geometry["tilts"][i] = np.asarray(t) - if verbose: - print( - f"{self.names[i]}: tilt {np.rad2deg(np.linalg.norm(tilt0)):.2f} -> " - f"{np.rad2deg(np.linalg.norm(t)):.2f} deg, pattern loss {l0:.2f} -> " - f"{min(sol.fun, best[0]):.2f}" - ) - self._setup_forward() - self._solve_linear(self._model_diffuse()) - loss_all = self._update_residual() - self.loss_history.append(loss_all) - return self - # ---------------------------------------------------------------- analysis def model_images(self, diffuse_only: bool = False) -> list[np.ndarray]: @@ -1551,7 +1612,7 @@ def r_factors(self) -> dict: return out def background_images(self) -> list[np.ndarray]: - """Fitted background (constant, Gaussians, thermal diffuse, Bragg tails) per pattern.""" + """Fitted smooth background per pattern.""" full = self.model_images() diffuse = self.model_images(diffuse_only=True) return [f - d for f, d in zip(full, diffuse)] @@ -1756,31 +1817,40 @@ def plot_mask(self, **kwargs): def plot_fit( self, + columns: Sequence[str] = ("experiment", "background", "difference", "model", "residual"), diffuse_only: bool = False, sigma: float = 0.7, quantiles: tuple[float, float] = (0.01, 0.99), cmap: str = "turbo_black", **kwargs, ): - """Experiment (left) and model (right) for every zone on one linear scale per row. - - Default: the binned pattern (blurred by ``sigma`` binned pixels) and - the full model. ``diffuse_only``: the experiment minus the fitted - background next to the supercell's diffuse term, then their - difference on a diverging map (white = no difference). The scale - spans ``quantiles`` of the experiment inside the diffuse mask (weight - > 0.5), which excludes the Bragg peaks and the direct-beam bloom; - masked pixels are black in the diffuse view. Panels are cropped to - ``q_max``. + """Experiment, fitted background, experiment - background, supercell diffuse model and + residual, one row per zone, cropped to ``q_max``. + + Experiment and background share a linear scale spanning ``quantiles`` of the + experiment inside the diffuse mask (weight > 0.5); experiment - background and the + model share one spanning ``quantiles`` of the difference; the residual uses a diverging + map at half that range (white = no difference). Masked pixels are black in the last + three columns. The experiment is blurred by ``sigma`` binned pixels. ``diffuse_only`` + keeps the last three columns. """ import matplotlib from quantem.core.visualization import show_2d - model = self.model_images(diffuse_only=diffuse_only) - background = self.background_images() if diffuse_only else None + if diffuse_only: + columns = ("difference", "model", "residual") + full = self.model_images() + diffuse = self.model_images(diffuse_only=True) cmap_obj = matplotlib.colormaps[cmap].with_extremes(bad="black") diverging = matplotlib.colormaps["RdBu_r"].with_extremes(bad="black") + titles_of = { + "experiment": "experiment", + "background": "background", + "difference": "experiment - background", + "model": "model", + "residual": "residual", + } rows, titles, norms, cmaps = [], [], [], [] for i in range(len(self.images)): q = np.linalg.norm(self.mask["k"][i], axis=-1) @@ -1789,132 +1859,210 @@ def plot_fit( sel = (w > 0.5) & inside rr, cc = np.nonzero(inside) crop = (slice(rr.min(), rr.max() + 1), slice(cc.min(), cc.max() + 1)) - if diffuse_only: - exp = self.mask["y"][i] - background[i] - if sigma: - exp = _nan_blur(np.where(w > 0.2, exp, np.nan), sigma) - exp = np.where(sel, exp, np.nan) - mod = np.where(sel, model[i], np.nan) - lo, hi = np.nanquantile(exp, quantiles) - h = 0.5 * (hi - lo) - rows.append([exp[crop], mod[crop], (exp - mod)[crop]]) - titles.append( - [ - f"{self.names[i]} experiment - background", - f"{self.names[i]} model", - "experiment - background - model", - ] - ) - norms.append( - [dict(interval_type="manual", vmin=lo, vmax=hi)] * 2 - + [dict(interval_type="manual", vmin=-h, vmax=h)] - ) - cmaps.append([cmap_obj, cmap_obj, diverging]) - else: - exp = self.mask["data"][i] - if sigma: - exp = ndimage.gaussian_filter(exp, sigma) - lo, hi = np.quantile(exp[sel], quantiles) - rows.append( - [np.where(inside, exp, np.nan)[crop], np.where(inside, model[i], np.nan)[crop]] - ) - titles.append([f"{self.names[i]} experiment", f"{self.names[i]} model"]) - norms.append([dict(interval_type="manual", vmin=lo, vmax=hi)] * 2) - cmaps.append([cmap_obj, cmap_obj]) + background = full[i] - diffuse[i] + exp = self.mask["data"][i] + if sigma: + exp = ndimage.gaussian_filter(exp, sigma) + diff = self.mask["y"][i] - background + if sigma: + diff = _nan_blur(np.where(w > 0.2, diff, np.nan), sigma) + diff = np.where(sel, diff, np.nan) + panels = { + "experiment": np.where(inside, exp, np.nan), + "background": np.where(inside, background, np.nan), + "difference": diff, + "model": np.where(sel, diffuse[i], np.nan), + "residual": diff - np.where(sel, diffuse[i], np.nan), + } + lo, hi = np.quantile(exp[sel], quantiles) + dlo, dhi = np.nanquantile(diff, quantiles) + half = 0.5 * (dhi - dlo) + scales = { + "experiment": (lo, hi), + "background": (lo, hi), + "difference": (dlo, dhi), + "model": (dlo, dhi), + "residual": (-half, half), + } + rows.append([panels[c][crop] for c in columns]) + titles.append([f"{self.names[i]} {titles_of[c]}" for c in columns]) + norms.append( + [ + dict(interval_type="manual", vmin=scales[c][0], vmax=scales[c][1]) + for c in columns + ] + ) + cmaps.append([diverging if c == "residual" else cmap_obj for c in columns]) return show_2d( rows, title=titles, norm=norms, cmap=cmaps, - axsize=kwargs.pop("axsize", (4, 4)), + axsize=kwargs.pop("axsize", (3.4, 3.4)), **kwargs, ) - def plot_sro(self, n_shells: int = 8, extent: float = 2.0, layer: int = 0): - """Short-range order and displacements. - - 1: Warren-Cowley alpha of every species pair against neighbour - distance (< 0 unlike neighbours preferred, > 0 like). 2-3: the - symmetrized diffuse intensity of the supercell in Laue units (1 = - random alloy) on the (001) and (1-10) reciprocal planes; maxima at - special points name the order (100: B2-type, 1/2 1/2 1/2: D0_3, - 2/3 2/3 2/3: omega). The color scale is set away from the - reciprocal-lattice nodes. 4: one (001) layer of the supercell colored - by species, displaced atoms ringed. 5: fraction of each species - displaced along <111>. - """ + def plot_warren_cowley(self, n_shells: int = 8): + """Warren-Cowley alpha against neighbour distance (< 0 unlike neighbours preferred, > 0 + like); one curve for a binary site, one per species pair otherwise.""" import matplotlib.pyplot as plt sro = self.warren_cowley(n_shells) - grid = self.diffuse_grid() - sec_001, _, d_001 = self.diffuse_section((0, 0, 1), extent, grid=grid) - sec_110, _, d_110 = self.diffuse_section((1, -1, 0), extent, grid=grid) K = len(self.species) - colors = plt.get_cmap("tab10")(np.arange(K)) - - fig, axs = plt.subplots(1, 5, figsize=(24, 4.6)) - ax = axs[0] + fig, ax = plt.subplots(figsize=(5.5, 4)) ax.axhline(0, color="0.6", lw=0.8) - for s in range(K): - for t in range(s, K): - ax.plot( - sro["radius"], - sro["alpha"][:, s, t], - "o-" if s == t else "s--", - ms=4, - label=f"{self.species[s]}-{self.species[t]}", - ) + if K == 2: # a binary site has one alpha for every pair + ax.plot(sro["radius"], sro["alpha"][:, 0, 0], "o-", ms=4, color="k") + ax.set_title(f"{self.species[0]}-{self.species[1]}") + else: + for s_ in range(K): + for t in range(s_, K): + ax.plot( + sro["radius"], + sro["alpha"][:, s_, t], + "o-" if s_ == t else "s--", + ms=4, + label=f"{self.species[s_]}-{self.species[t]}", + ) + ax.legend(fontsize=8) ax.set_xlabel("neighbour distance (A)") ax.set_ylabel("Warren-Cowley alpha") - ax.legend(fontsize=8, ncol=2) - between = np.concatenate([sec_001[d_001 > 0.2], sec_110[d_110 > 0.2]]) + fig.tight_layout() + return fig, ax + + def plot_diffuse_sections(self, extent: float = 2.0, normals=((0, 0, 1), (1, -1, 0))): + """Symmetrized diffuse intensity of the supercell in Laue units (1 = random alloy) on + reciprocal-lattice planes through the origin. Maxima at special points name the order + (100: B2-type, 1/2 1/2 1/2: D0_3, 2/3 2/3 2/3: omega). The color scale is set away + from the reciprocal-lattice nodes.""" + import matplotlib.pyplot as plt + + grid = self.diffuse_grid() + secs = [self.diffuse_section(n, extent, grid=grid) for n in normals] + between = np.concatenate([sec[d > 0.2] for sec, _, d in secs]) vmax = np.quantile(between, 0.995) + fig, axs = plt.subplots(1, len(normals), figsize=(5.2 * len(normals), 4.4)) + axs = np.atleast_1d(axs) ext = [-extent, extent, -extent, extent] - for ax, sec, title, xl, yl in ( - (axs[1], sec_001, "(001) section, Laue units", "h", "k"), - (axs[2], sec_110, "(1-10) section, Laue units", "[001]", "[110]/sqrt2"), - ): + for ax, n, (sec, (u, v), _) in zip(axs, normals, secs): im = ax.imshow( sec.T, origin="lower", extent=ext, cmap="turbo_black", vmin=0, vmax=vmax ) - ax.set_title(title) - ax.set_xlabel(xl) - ax.set_ylabel(yl) + ax.set_title("(" + "".join(f"{int(x)}" for x in n) + ") section, Laue units") + ax.set_xlabel("[" + " ".join(f"{x:.2f}" for x in u) + "] (1/a)") + ax.set_ylabel("[" + " ".join(f"{x:.2f}" for x in v) + "] (1/a)") fig.colorbar(im, ax=ax, fraction=0.046) + fig.tight_layout() + return fig, axs - ax = axs[3] - z = layer * self.grid_divisor * self.refine - in_layer = self.site_x[:, 2] == z - xy = self.site_x[in_layer, :2] / (self.grid_divisor * self.refine) - sp = self.species_index[in_layer] - for s in range(K): - m = sp == s - ax.scatter(xy[m, 1], xy[m, 0], s=10, color=colors[s], label=self.species[s]) - moved = self._omega_like()[in_layer] + def _shells(self, n_shells: int): + """Neighbour offsets of the BCC site lattice (site units, a / 2) grouped by distance.""" + r = np.arange(-4, 5) + v = np.stack(np.meshgrid(r, r, r, indexing="ij"), -1).reshape(-1, 3) + bcc = np.all(v % 2 == 0, axis=1) | np.all(v % 2 == 1, axis=1) + v = v[bcc & np.any(v != 0, axis=1)] + d2 = (v**2).sum(1) + return [v[d2 == d] for d in np.unique(d2)[:n_shells]] + + def displacement_correlations(self, n_shells: int = 6) -> dict: + """Displacement short-range order per neighbour shell. + + ``longitudinal``: <(u_i.r)(u_j.r)> / <(u.r)^2> with r the bond direction; + ``transverse``: the same for the components normal to the bond. Omega embryos give a + strong negative longitudinal correlation on the nearest-neighbour <111> bond (the + collapsing pair moves together). + """ + if self.grid_divisor != 2: + raise NotImplementedError("Displacement correlations are implemented for BCC sites.") + step = self.lattice_parameter / (self.grid_divisor * self.refine) + u = self.displacement * step + nc = self._site_lookup.shape[0] + xc = self.site_x // self.refine + out = dict(radius=[], longitudinal=[], transverse=[]) + for offs in self._shells(n_shells): + rhat = offs / np.linalg.norm(offs, axis=1, keepdims=True) + nb = np.stack([self._site_lookup[tuple(np.mod(xc + o, nc).T)] for o in offs], 1) + ui = u[:, None, :] + uj = u[nb] + li = (ui * rhat[None]).sum(-1) + lj = (uj * rhat[None]).sum(-1) + ti = ui - li[..., None] * rhat[None] + tj = uj - lj[..., None] * rhat[None] + ll = (li**2).mean() + tt = (ti**2).sum(-1).mean() + out["radius"].append(np.linalg.norm(offs[0]) * self.lattice_parameter / 2) + out["longitudinal"].append(float((li * lj).mean() / ll) if ll > 0 else 0.0) + out["transverse"].append(float((ti * tj).sum(-1).mean() / tt) if tt > 0 else 0.0) + out["radius"] = np.asarray(out["radius"]) + return out + + def plot_displacement_correlations(self, n_shells: int = 6): + """Displacement short-range order: longitudinal and transverse displacement + correlations by neighbour shell.""" + import matplotlib.pyplot as plt + + c = self.displacement_correlations(n_shells) + fig, ax = plt.subplots(figsize=(5.5, 4)) + ax.axhline(0, color="0.6", lw=0.8) + ax.plot(c["radius"], c["longitudinal"], "o-", label="longitudinal (along bond)") + ax.plot(c["radius"], c["transverse"], "s--", label="transverse") + ax.set_xlabel("neighbour distance (A)") + ax.set_ylabel("displacement correlation") + ax.legend(fontsize=8) + fig.tight_layout() + return fig, ax + + def displacement_distributions(self) -> dict: + """Probability of each projected displacement u.n (A) per species, pooled over the + symmetry-equivalent directions n of <100>, <110> and <111> (both senses).""" step = self.lattice_parameter / (self.grid_divisor * self.refine) - u = self.displacement[in_layer, :2] * step / self.lattice_parameter - ax.quiver( - xy[:, 1], - xy[:, 0], - u[:, 1], - u[:, 0], - angles="xy", - scale_units="xy", - scale=0.1, - width=0.003, - color="0.3", + families = { + "<100>": np.eye(3), + "<110>": np.array( + [[1, 1, 0], [1, -1, 0], [1, 0, 1], [1, 0, -1], [0, 1, 1], [0, 1, -1]] + ), + "<111>": np.array([[1, 1, 1], [1, 1, -1], [1, -1, 1], [-1, 1, 1]]), + } + out = {} + for name, dirs in families.items(): + n = dirs / np.linalg.norm(dirs, axis=1, keepdims=True) + proj = self.displacement @ n.T * step # (n_sites, n_dirs) + proj = np.concatenate([proj, -proj], axis=1) + out[name] = {} + for k, sp in enumerate(self.species): + v = np.round(proj[self.species_index == k].ravel(), 4) + vals, counts = np.unique(v, return_counts=True) + out[name][sp] = (vals, counts / counts.sum()) + return out + + def plot_displacements(self, **kwargs): + """Probability distribution of the static displacement of each species projected on + <100>, <110> and <111> (pooled over equivalent directions and both senses). Omega + displacements are discrete, so the distributions are a central peak at 0 with side + peaks at the half and full collapse.""" + import matplotlib.pyplot as plt + + dist = self.displacement_distributions() + fig, axs = plt.subplots(1, 3, figsize=kwargs.pop("figsize", (14, 3.8)), sharey=True) + colors = plt.get_cmap("tab10")(np.arange(len(self.species))) + width = ( + 0.8 * self.lattice_parameter / (self.grid_divisor * self.refine) / len(self.species) + ) + for ax, (name, per_species) in zip(axs, dist.items()): + for k, sp in enumerate(self.species): + vals, prob = per_species[sp] + offset = (k - (len(self.species) - 1) / 2) * width + ax.bar(vals + offset, prob, width=width, color=colors[k], label=sp) + ax.set_yscale("log") + ax.set_xlabel(f"u . n, n along {name} (A)") + ax.set_title(name) + axs[0].set_ylabel("probability") + axs[0].legend() + s = self.displacement_summary() + fig.suptitle( + f"static B = {s['static_b']:.3f} A^2; omega fraction " + + ", ".join(f"{k} {100 * v:.1f}%" for k, v in s["omega_fraction"].items()) ) - ax.scatter(xy[moved, 1], xy[moved, 0], s=40, facecolors="none", edgecolors="k", lw=0.6) - ax.set_aspect("equal") - ax.set_title("(001) layer, displacements x10, omega ringed") - ax.set_xlabel("cells") - ax.legend(fontsize=8, loc="upper right") - - ax = axs[4] - summary = self.displacement_summary() - ax.bar(self.species, [summary["mean_displacement"][s] for s in self.species], color=colors) - ax.set_ylabel("mean static displacement (A)") - ax.set_title(f"static B = {summary['static_b']:.3f} A^2") fig.tight_layout() return fig, axs diff --git a/tests/diffraction/test_reverse_monte_carlo.py b/tests/diffraction/test_reverse_monte_carlo.py index 0b5e6bdef..29947091a 100644 --- a/tests/diffraction/test_reverse_monte_carlo.py +++ b/tests/diffraction/test_reverse_monte_carlo.py @@ -113,10 +113,13 @@ def test_warren_cowley_random_is_near_zero(rmc): assert np.all(np.abs(sro["alpha"]) < 0.35) -def test_refine_tilts_runs(rmc): - rmc.refine_tilts(max_tilt_deg=0.5, step_deg=0.25, verbose=False) - rmc.refine_tilts(max_tilt_deg=0.5, step_deg=0.25, max_grid=rmc.grid_size // 2, verbose=False) - assert all(np.rad2deg(np.linalg.norm(t)) <= 0.6 for t in rmc.geometry["tilts"]) +def test_coarse_diffuse_grid_matches_sections(rmc): + fine = rmc.diffuse_grid() + coarse = rmc.diffuse_grid(max_size=rmc.grid_size // 2) + assert coarse.shape[0] == fine.shape[0] // 2 + a, _, d = rmc.diffuse_section((0, 0, 1), extent=1.0, grid=fine) + b, _, _ = rmc.diffuse_section((0, 0, 1), extent=1.0, grid=coarse) + assert np.allclose(a[d > 0.2], b[d > 0.2], rtol=1e-4) # no displacements: identical def test_mask_is_zero_on_bragg_peaks(rmc): @@ -175,17 +178,6 @@ def test_static_b_cap_holds(rmc): assert len(rmc._omega_vectors) == 16 # two amplitudes x eight <111> senses -def test_omega_chain_repeats_along_one_row(rmc): - sites, new = rmc._omega_proposals(2, repeats=2) - assert sites.shape[1] == 6 - assert np.array_equal(new[:, :3], new[:, 3:]) - xc = rmc.site_x[sites] // rmc.refine - n = rmc.cells * 2 - steps = np.mod(np.diff(xc, axis=1), n) - steps = np.where(steps > n // 2, steps - n, steps) - assert np.all(steps == steps[:, :1]) # one straight <111> row - - def test_size_effect_chi_is_odd_about_bragg_nodes(rmc): a = rmc.lattice_parameter g = np.array([1.0, 1.0, 0.0]) / a # allowed BCC reflection @@ -203,3 +195,29 @@ def test_size_effect_radii_orders_species(rmc): eta = dict(zip(rmc.species, rmc.size_eta)) assert eta["V"] < eta["Nb"] < eta["Zr"] assert abs((rmc.concentrations * rmc.size_eta).sum()) < 1e-12 + + +def test_autoserialize_round_trip_rebuilds_model(rmc, tmp_path): + from quantem.core.io import load + + rmc.envelope = "fitted" + rmc._setup_forward() + rmc._solve_linear(rmc._model_diffuse()) + loss = rmc._update_residual() + model = rmc.model_images() + path = tmp_path / "rmc.zip" + rmc.save(path, mode="o", skip=rmc.DERIVED_ATTRIBUTES) + back = load(path) + assert back._update_residual() == pytest.approx(loss, rel=1e-5) + for a, b in zip(model, back.model_images()): + assert np.allclose(a, b, rtol=1e-4, atol=1e-6) + assert np.array_equal(back.species_index, rmc.species_index) + + +def test_displacement_correlations_see_omega(rmc): + sites, new = rmc._omega_proposals(8) + rmc.displacement[sites.reshape(-1)] = new.reshape(-1, 3) + c = rmc.displacement_correlations(n_shells=2) + assert c["longitudinal"][0] < 0 # collapsing nearest-neighbour pairs move toward each other + dist = rmc.displacement_distributions() + assert set(dist) == {"<100>", "<110>", "<111>"} From 0888aae97791ce4e49c5d942a86ee88f7ae51851 Mon Sep 17 00:00:00 2001 From: cophus Date: Sun, 4 Oct 2026 14:45:53 -0700 Subject: [PATCH 27/36] updates to displacements --- .../diffraction/reverse_monte_carlo.py | 106 ++++++++++++------ tests/diffraction/test_reverse_monte_carlo.py | 20 ++++ 2 files changed, 92 insertions(+), 34 deletions(-) diff --git a/src/quantem/diffraction/reverse_monte_carlo.py b/src/quantem/diffraction/reverse_monte_carlo.py index 8e0417bf0..1ec45026b 100644 --- a/src/quantem/diffraction/reverse_monte_carlo.py +++ b/src/quantem/diffraction/reverse_monte_carlo.py @@ -740,6 +740,8 @@ def build_supercell( cells: int = 16, seed: int | None = 0, displacements: bool = True, + displacement_grid: int = 24, + max_displacement: float = 0.3, omega_amplitudes: Sequence[int] = (1, 2), symmetrize: bool = True, debye_waller: float = 0.5, @@ -762,9 +764,14 @@ def build_supercell( Unit cells along each cube edge. The diffuse model is sampled every ``1 / (cells a)`` in reciprocal space. displacements : bool - Allow omega displacements: atoms move along a <111> sense, two of - every three {111} planes collapsing toward each other. Positions - live on a grid a/24 fine, so moves are still scored exactly. + Allow static displacements. Positions live on a grid + ``a / displacement_grid`` fine, so every move is still scored + exactly. + displacement_grid : int + Steps per lattice parameter of the displacement grid (a multiple + of 24): 24 gives 0.15 A steps, 48 gives 0.076 A for a = 3.66 A. + max_displacement : float + Largest displacement component (A) reached by random moves. omega_amplitudes : sequence of int Allowed displacements in units of a/24 along each axis: 2 is the ideal omega collapse (a/12, 0.53 A along <111> for a = 3.66 A), 1 @@ -810,8 +817,10 @@ def build_supercell( and len(self.site_grid) == 2 and np.array_equal(np.sort(self.site_grid.sum(1)), [0, 3]) ): - raise NotImplementedError("Omega displacements are implemented for BCC sites only.") - m = 24 // d if displacements else 1 + raise NotImplementedError("Displacements are implemented for BCC sites only.") + if displacement_grid % 24: + raise ValueError("displacement_grid must be a multiple of 24.") + m = displacement_grid // d if displacements else 1 unit = m * d // 24 # fine-grid steps per a/24 self._omega_vectors = np.array( [k * unit * np.array(v) for k in omega_amplitudes for v in product((1, -1), repeat=3)], @@ -835,6 +844,7 @@ def build_supercell( xc = x0 // m self._site_lookup[xc[:, 0], xc[:, 1], xc[:, 2]] = np.arange(n_sites) self.displacements = bool(displacements) + self._max_steps = int(np.floor(max_displacement / (self._a_crystal / (d * m)) + 1e-9)) self.size_eta = np.zeros(len(self.species)) self.debye_waller = float(debye_waller) self.symmetrize = bool(symmetrize) @@ -1415,20 +1425,24 @@ def run( n_sweeps: int = 20, batch: int = 32, temperature: float = 0.05, - omega_fraction: float = 0.5, + random_fraction: float = 0.5, + omega_fraction: float = 0.0, max_static_b: float | None = 0.5, refit_every: int = 2, progress: bool = True, ) -> "ReverseMonteCarlo": - """Species swaps and omega embryos until the diffuse fit converges. + """Species swaps and displacements until the diffuse fit converges. Each batch proposes ``batch`` moves on distinct sites against the - current supercell and scores each exactly: either swaps of two - unlike atoms (composition conserved) or, with probability - ``omega_fraction``, an omega embryo on three consecutive atoms of a - <111> row: the first stays and the next two collapse toward each - other by a/24 or a/12 each, (0, +v, -v). Proposing an embryo where it - already stands clears it instead. ``max_static_b`` (A^2) caps the + current supercell and scores each exactly. With probability + ``random_fraction`` the batch moves single atoms by one grid step + (-1, 0 or +1 along each axis, up to ``max_displacement``), with no + assumed pattern, so any displacement correlation has to come from the + data. With probability ``omega_fraction`` it proposes omega embryos + instead: three consecutive atoms of a <111> row, the first fixed and + the next two collapsed toward each other, (0, +v, -v), or cleared + where one already stands. Otherwise it swaps two unlike atoms + (composition conserved). ``max_static_b`` (A^2) caps the static Debye-Waller factor of all displacements, 8 pi^2 / 3, so they stay consistent with how slowly the Bragg intensities fall off (a displacement field this strong would damp them; the diffuse scale @@ -1449,6 +1463,7 @@ def run( t0 = None n_batches = max(n_sites // batch, 1) p_omega = omega_fraction if self.displacements else 0.0 + p_random = random_fraction if self.displacements else 0.0 budget = ( 3 * max_static_b / (8 * np.pi**2) * n_sites if max_static_b is not None else np.inf ) @@ -1456,13 +1471,16 @@ def run( sweeps = tqdm(range(n_sweeps), desc="RMC sweeps", disable=not progress) su = None for sweep in sweeps: - accepted = {"swap": 0, "omega": 0} + accepted = {"swap": 0, "random": 0, "omega": 0} for _ in range(n_batches): if su is None: su = self._scale * self._u spec = self.species_index roll = self.rng.random() - if roll < p_omega: + if roll < p_random: + kind = "random" + sites, new = self._random_proposals(batch) + elif roll < p_random + p_omega: kind = "omega" sites, new = self._omega_proposals(batch) else: @@ -1554,12 +1572,24 @@ def run( loss = self._update_residual() su = None self.loss_history.append(loss) - sweeps.set_postfix(loss=f"{loss:.4g}", swaps=accepted["swap"], omega=accepted["omega"]) + sweeps.set_postfix(loss=f"{loss:.4g}", **accepted) self._recompute_F() self._solve_linear(self._model_diffuse()) self.loss_history[-1] = self._update_residual() return self + def _random_proposals(self, batch: int): + """Single atoms moved by -1, 0 or +1 grid steps along each axis (not all zero), within + ``max_displacement``: sites (B, 1) and new displacement vectors (B, 1, 3).""" + n_sites = len(self.site_x) + j = self.rng.choice(n_sites, batch, replace=False) + delta = self.rng.integers(-1, 2, (batch, 3)) + zero = ~np.any(delta, axis=1) + delta[zero, self.rng.integers(0, 3, zero.sum())] = self.rng.choice((-1, 1), zero.sum()) + new = self.displacement[j] + delta + ok = np.all(np.abs(new) <= self._max_steps, axis=1) + return j[ok][:, None], new[ok][:, None, :] + def _omega_proposals(self, batch: int): """Disjoint omega embryos along <111> rows: sites (B, 3) and their new displacement vectors (B, 3, 3), the (0, +v, -v) collapse.""" @@ -1978,8 +2008,14 @@ def displacement_correlations(self, n_shells: int = 6) -> dict: u = self.displacement * step nc = self._site_lookup.shape[0] xc = self.site_x // self.refine - out = dict(radius=[], longitudinal=[], transverse=[]) + out = dict(radius=[], shell=[], longitudinal=[], transverse=[]) for offs in self._shells(n_shells): + v = np.sort(np.abs(offs[0]))[::-1] + out["shell"].append( + "<" + "".join(str(int(x)) for x in v // 2) + ">" + if np.all(v % 2 == 0) + else "1/2<" + "".join(str(int(x)) for x in v) + ">" + ) rhat = offs / np.linalg.norm(offs, axis=1, keepdims=True) nb = np.stack([self._site_lookup[tuple(np.mod(xc + o, nc).T)] for o in offs], 1) ui = u[:, None, :] @@ -1996,18 +2032,25 @@ def displacement_correlations(self, n_shells: int = 6) -> dict: out["radius"] = np.asarray(out["radius"]) return out - def plot_displacement_correlations(self, n_shells: int = 6): + def plot_displacement_correlations(self, n_shells: int = 8): """Displacement short-range order: longitudinal and transverse displacement - correlations by neighbour shell.""" + correlations for each neighbour shell, labelled by the shell's bond vector in units of + a. BCC has no shell between a (second neighbours) and a sqrt(2) (third).""" import matplotlib.pyplot as plt c = self.displacement_correlations(n_shells) - fig, ax = plt.subplots(figsize=(5.5, 4)) + fig, ax = plt.subplots(figsize=(9, 4.5)) ax.axhline(0, color="0.6", lw=0.8) - ax.plot(c["radius"], c["longitudinal"], "o-", label="longitudinal (along bond)") - ax.plot(c["radius"], c["transverse"], "s--", label="transverse") - ax.set_xlabel("neighbour distance (A)") + ax.plot(c["radius"], c["longitudinal"], "o-", ms=5, label="longitudinal (along bond)") + ax.plot(c["radius"], c["transverse"], "s--", ms=5, label="transverse") + ax.set_xticks(c["radius"]) + ax.set_xticklabels(c["shell"], rotation=45, fontsize=8) + ax.set_xlabel("neighbour shell (bond vector / a)") ax.set_ylabel("displacement correlation") + top = ax.secondary_xaxis("top") + top.set_xticks(c["radius"]) + top.set_xticklabels([f"{r:.2f}" for r in c["radius"]], fontsize=7) + top.set_xlabel("distance (A)") ax.legend(fontsize=8) fig.tight_layout() return fig, ax @@ -2037,31 +2080,26 @@ def displacement_distributions(self) -> dict: def plot_displacements(self, **kwargs): """Probability distribution of the static displacement of each species projected on - <100>, <110> and <111> (pooled over equivalent directions and both senses). Omega - displacements are discrete, so the distributions are a central peak at 0 with side - peaks at the half and full collapse.""" + <100>, <110> and <111> (pooled over equivalent directions and both senses), on a log + scale. Displacements live on a grid, so each curve connects the grid values.""" import matplotlib.pyplot as plt dist = self.displacement_distributions() fig, axs = plt.subplots(1, 3, figsize=kwargs.pop("figsize", (14, 3.8)), sharey=True) colors = plt.get_cmap("tab10")(np.arange(len(self.species))) - width = ( - 0.8 * self.lattice_parameter / (self.grid_divisor * self.refine) / len(self.species) - ) for ax, (name, per_species) in zip(axs, dist.items()): for k, sp in enumerate(self.species): vals, prob = per_species[sp] - offset = (k - (len(self.species) - 1) / 2) * width - ax.bar(vals + offset, prob, width=width, color=colors[k], label=sp) + ax.plot(vals, prob, "o-", ms=4, lw=1.2, color=colors[k], label=sp) ax.set_yscale("log") ax.set_xlabel(f"u . n, n along {name} (A)") ax.set_title(name) axs[0].set_ylabel("probability") axs[0].legend() - s = self.displacement_summary() + summary = self.displacement_summary() fig.suptitle( - f"static B = {s['static_b']:.3f} A^2; omega fraction " - + ", ".join(f"{k} {100 * v:.1f}%" for k, v in s["omega_fraction"].items()) + f"static B = {summary['static_b']:.3f} A^2; mean |u| " + + ", ".join(f"{k} {v:.3f} A" for k, v in summary["mean_displacement"].items()) ) fig.tight_layout() return fig, axs diff --git a/tests/diffraction/test_reverse_monte_carlo.py b/tests/diffraction/test_reverse_monte_carlo.py index 29947091a..158e1ad50 100644 --- a/tests/diffraction/test_reverse_monte_carlo.py +++ b/tests/diffraction/test_reverse_monte_carlo.py @@ -221,3 +221,23 @@ def test_displacement_correlations_see_omega(rmc): assert c["longitudinal"][0] < 0 # collapsing nearest-neighbour pairs move toward each other dist = rmc.displacement_distributions() assert set(dist) == {"<100>", "<110>", "<111>"} + + +def test_random_displacement_scores_exactly_and_stays_bounded(rmc): + loss0 = rmc._update_residual() + sites, new = rmc._random_proposals(16) + assert np.all(np.abs(new) <= rmc._max_steps) + assert np.all(np.any(new[:, 0] != rmc.displacement[sites[:, 0]], axis=1)) + j = sites[:1, 0] + co, so = rmc._phases(rmc._positions(j)) + cn, sn = rmc._phases(rmc._positions(j, new[:1, 0])) + f = rmc._fs[int(rmc.species_index[j[0]])] + dL = _score(rmc, f * (cn - co), f * (so - sn)) + rmc.displacement[j[0]] = new[0, 0] + rmc._recompute_F() + assert rmc._update_residual() - loss0 == pytest.approx(dL, rel=1e-3, abs=1e-4 * loss0) + + +def test_shell_labels(rmc): + c = rmc.displacement_correlations(n_shells=4) + assert c["shell"] == ["1/2<111>", "<100>", "<110>", "1/2<311>"] From 61f0c38c1cffc2d879a3eb05320576cb93933fcf Mon Sep 17 00:00:00 2001 From: Colin Ophus Date: Sun, 4 Oct 2026 15:15:28 -0700 Subject: [PATCH 28/36] DDF: port Ian's virtual aperture and polar functions to Vector Port aperture_array_generator, aperture_array_subtract, DDFimage, pointlist_to_array (rphi), DDF_radial_image and DDFradialazimuthimage from py4DSTEM into digital_dark_field.py as aperture_array, aperture_array_subtract, aperture_ddf_image, add_polar_fields, polar_mask and radial_ddf_image, all reading peaks from a Vector. Add ddf_image(peaks, mask) and plot_apertures. Fix assign_grain_labels and plot_cluster_scatter from PR #286 for the torch-backed Vector. Co-Authored-By: Claude Opus 5.5 --- src/quantem/diffraction/digital_dark_field.py | 684 +++++++++++++++--- tests/diffraction/test_digital_dark_field.py | 86 +++ 2 files changed, 655 insertions(+), 115 deletions(-) create mode 100644 tests/diffraction/test_digital_dark_field.py diff --git a/src/quantem/diffraction/digital_dark_field.py b/src/quantem/diffraction/digital_dark_field.py index 98f3c1507..0bbfe476f 100644 --- a/src/quantem/diffraction/digital_dark_field.py +++ b/src/quantem/diffraction/digital_dark_field.py @@ -1,26 +1,40 @@ -"""Clustering-based digital dark field imaging. - -Implements the workflow of MacLaren and co-workers: DBSCAN in the joint -(diffraction, scan) space groups the detected Bragg peaks into single-spot, -single-crystallite clusters (L1); clustering the real-space centers of mass -of those clusters groups the g-vectors of each grain (L2); clustering the -remaining unindexed peaks in diffraction space alone isolates ring-like -nanocrystalline or amorphous components (L3). Each cluster's summed -intensity per probe position is a digital dark field image. - -The clustering itself is the generic quantem.core.utils.clustering.dbscan / -cluster_vector; this module holds the diffraction-specific pieces: centers -of mass, DDF image formation, and composite color rendering. +"""Digital dark field imaging from detected Bragg peaks. + +A digital dark field (DDF) image is the summed intensity of a selected subset +of the detected Bragg peaks at each probe position. These functions select +the peaks in three ways, following MacLaren and co-workers +(https://doi.org/10.1093/mam/ozae104): + +- Virtual apertures: peaks within a radius of a set of aperture positions, + usually a lattice built from two reciprocal lattice vectors + (aperture_array, aperture_array_subtract, aperture_ddf_image). +- Polar selection: peaks within a ring of radius q, optionally restricted to + a range of azimuthal angles (add_polar_fields, polar_mask, + radial_ddf_image). +- Clustering: DBSCAN in the joint (diffraction, scan) space groups the peaks + into single-spot, single-crystallite clusters (L1), clustering the + real-space centers of mass of those clusters groups the spots of each grain + (L2), and clustering the remaining peaks in diffraction space alone + isolates ring-like nanocrystalline or amorphous components (L3) + (cluster_coms, ddf_images, assign_grain_labels). + +All functions read the peaks from a Vector with one cell per probe position. +The diffraction coordinates are given by `q_fields`, which defaults to +("qx", "qy") for calibrated peaks or ("q_row", "q_col") for peaks in detector +pixels. Boolean masks are aligned with the flattened rows of the Vector, so +they can be combined with & and | and passed to ddf_image or to +quantem.core.utils.clustering.filter_rows. """ from __future__ import annotations -import numpy as np +from collections.abc import Sequence import matplotlib.pyplot as plt -from matplotlib.colors import hsv_to_rgb +import numpy as np +from matplotlib.collections import EllipseCollection +from matplotlib.colors import hsv_to_rgb -from quantem.core.datastructures.vector import Vector from quantem.core.utils.clustering import cluster_vector, dbscan # noqa: F401 @@ -29,9 +43,454 @@ def _scan_cells(vector) -> np.ndarray: counts = np.asarray(vector.row_counts(), dtype=int) shape = vector.shape[:2] cell_r, cell_c = np.divmod(np.arange(counts.size), shape[1]) - return np.stack( - [np.repeat(cell_r, counts), np.repeat(cell_c, counts)], axis=1 + return np.stack([np.repeat(cell_r, counts), np.repeat(cell_c, counts)], axis=1) + + +def _resolve_q_fields(vector, q_fields) -> tuple[str, str]: + """The two diffraction coordinate fields of a peak Vector.""" + if q_fields is not None: + return tuple(q_fields) + for candidate in (("qx", "qy"), ("q_row", "q_col")): + if all(f in vector.fields for f in candidate): + return candidate + raise KeyError( + f"No diffraction coordinate fields found in {vector.fields}; pass q_fields explicitly." + ) + + +def _q_coordinates(vector, q_fields=None, center=(0.0, 0.0)) -> np.ndarray: + """(N, 2) diffraction coordinates of every flattened row, relative to center.""" + q_fields = _resolve_q_fields(vector, q_fields) + q = vector.select_fields(*q_fields).numpy().astype(np.float64) + return q - np.asarray(center, dtype=np.float64)[None, :] + + +# --------------------------------------------------------------------------- # +# DDF images +# --------------------------------------------------------------------------- # + + +def ddf_image( + peaks, + mask=None, + intensity_field: str = "intensity", +) -> np.ndarray: + """Digital dark field image from a subset of the peaks. + + Parameters + ---------- + peaks : Vector + Peaks with one cell per probe position. + mask : array-like of bool, optional + (N,) selection aligned with the flattened rows of `peaks`, for + example from aperture_mask or polar_mask. None uses every peak. + intensity_field : str, default="intensity" + Field summed at each probe position. Negative values are clipped to 0. + + Returns + ------- + np.ndarray + (scan_row, scan_col) image. + """ + inten = peaks.select_fields(intensity_field).numpy()[:, 0].astype(np.float64).clip(min=0) + rc = _scan_cells(peaks) + if mask is not None: + mask = np.asarray(mask, dtype=bool) + inten, rc = inten[mask], rc[mask] + R, C = peaks.shape[:2] + image = np.zeros((R, C)) + np.add.at(image, (rc[:, 0], rc[:, 1]), inten) + return image + + +# --------------------------------------------------------------------------- # +# Virtual apertures +# --------------------------------------------------------------------------- # + + +def aperture_array( + g1, + g2=None, + mode: str = "array", + center=(0.0, 0.0), + shift=(0, 0), + n1_range: tuple[int, int] = (-5, 5), + n2_range: tuple[int, int] = (-5, 5), + radius_range: tuple[float, float] = (0.0, np.inf), + shape=None, + edge: float = 0.0, +) -> np.ndarray: + """Virtual aperture positions on a lattice of diffraction vectors. + + Each aperture sits at center + (n1 + s1) g1 + (n2 + s2) g2, where (s1, s2) + is `shift`. We keep the positions whose distance from `center` falls + inside `radius_range`, which is how the direct beam is usually excluded. + When the peaks were shifted to a common origin with + BraggVectors.correct_peak_origins, setting `center` to that origin puts + the apertures in detector pixels, so they can be drawn over the mean + pattern or the Bragg vector map. + + Parameters + ---------- + g1, g2 : array-like of float + (2,) lattice vectors in the same coordinates as the peaks. g2 is not + needed for mode="line". + mode : {"array", "line", "single"}, default="array" + "array" places a 2D lattice of apertures over n1_range and n2_range, + "line" places a row of apertures along g1 over n1_range (a systematic + row, such as a two-beam condition), and "single" places one aperture + at (s1 g1 + s2 g2). + center : array-like of float, default=(0, 0) + (2,) origin of the lattice, normally the direct beam. + shift : (int, int), default=(0, 0) + Lattice offset (s1, s2) in multiples of g1 and g2. + n1_range, n2_range : (int, int), default=(-5, 5) + Inclusive range of lattice multiples of g1 and g2. + radius_range : (float, float), default=(0, inf) + Inner and outer distance from `center` of the apertures kept. + shape : (int, int), optional + Detector shape. When given, apertures closer than `edge` to the + detector boundary are removed, which requires `center` to be in + detector pixels. + edge : float, default=0 + Boundary width in pixels, used only with `shape`. + + Returns + ------- + np.ndarray + (N, 2) aperture positions. + """ + g1 = np.asarray(g1, dtype=np.float64) + g2 = np.zeros(2) if g2 is None else np.asarray(g2, dtype=np.float64) + s1, s2 = shift + if mode == "single": + n1 = np.array([0]) + n2 = np.array([0]) + elif mode in ("line", "2-beam"): + n1 = np.arange(n1_range[0], n1_range[1] + 1) + n2 = np.zeros_like(n1) + elif mode == "array": + n1, n2 = np.meshgrid( + np.arange(n1_range[0], n1_range[1] + 1), + np.arange(n2_range[0], n2_range[1] + 1), + indexing="ij", + ) + n1, n2 = n1.ravel(), n2.ravel() + else: + raise ValueError(f"mode must be 'array', 'line' or 'single', got {mode!r}.") + + offsets = (n1[:, None] + s1) * g1[None, :] + (n2[:, None] + s2) * g2[None, :] + r = np.hypot(offsets[:, 0], offsets[:, 1]) + keep = (r >= radius_range[0]) & (r <= radius_range[1]) + positions = offsets + np.asarray(center, dtype=np.float64)[None, :] + if shape is not None: + keep &= ( + (positions[:, 0] > edge) + & (positions[:, 0] < shape[0] - edge) + & (positions[:, 1] > edge) + & (positions[:, 1] < shape[1] - edge) + ) + return positions[keep] + + +def aperture_array_subtract( + positions, + positions_remove, + tol: float = 1.0, +) -> np.ndarray: + """Remove the apertures that coincide with a second set. + + Subtracting the fundamental lattice from a finer lattice leaves only the + superlattice positions, for example. + + Parameters + ---------- + positions : array-like of float + (N, 2) aperture positions. + positions_remove : array-like of float + (M, 2) aperture positions to remove from `positions`. + tol : float, default=1.0 + Apertures within this distance of any position in + `positions_remove` are removed. + + Returns + ------- + np.ndarray + (N', 2) remaining aperture positions. + """ + positions = np.atleast_2d(np.asarray(positions, dtype=np.float64)) + positions_remove = np.atleast_2d(np.asarray(positions_remove, dtype=np.float64)) + if positions_remove.size == 0: + return positions + d2 = ((positions[:, None, :] - positions_remove[None, :, :]) ** 2).sum(axis=-1) + return positions[d2.min(axis=1) > tol**2] + + +def aperture_mask( + peaks, + positions, + radius: float = 1.0, + q_fields=None, +) -> np.ndarray: + """Peaks that fall inside any of a set of virtual apertures. + + Parameters + ---------- + peaks : Vector + Peaks with one cell per probe position. + positions : array-like of float + (M, 2) aperture positions, in the coordinates of `q_fields`. + radius : float, default=1.0 + Aperture radius. + q_fields : (str, str), optional + Diffraction coordinate fields. Defaults to ("qx", "qy") or + ("q_row", "q_col"), whichever are present. + + Returns + ------- + np.ndarray + (N,) bool mask aligned with the flattened rows of `peaks`. A peak + inside two overlapping apertures is selected once. + """ + q = _q_coordinates(peaks, q_fields) + positions = np.atleast_2d(np.asarray(positions, dtype=np.float64)) + mask = np.zeros(q.shape[0], dtype=bool) + r2 = radius**2 + for p in positions: + mask |= ((q - p[None, :]) ** 2).sum(axis=1) <= r2 + return mask + + +def aperture_ddf_image( + peaks, + positions, + radius: float = 1.0, + q_fields=None, + intensity_field: str = "intensity", +) -> np.ndarray: + """Digital dark field image through a set of virtual apertures. + + Equivalent to ddf_image(peaks, aperture_mask(peaks, positions, radius)). + + Parameters + ---------- + peaks : Vector + Peaks with one cell per probe position. + positions : array-like of float + (M, 2) aperture positions, from aperture_array for example. + radius : float, default=1.0 + Aperture radius, in the units of the peak coordinates. + q_fields : (str, str), optional + Diffraction coordinate fields. + intensity_field : str, default="intensity" + Field summed at each probe position. + + Returns + ------- + np.ndarray + (scan_row, scan_col) image. + """ + mask = aperture_mask(peaks, positions, radius=radius, q_fields=q_fields) + return ddf_image(peaks, mask, intensity_field=intensity_field) + + +def plot_apertures( + positions, + image=None, + radius: float | None = None, + positions_removed=None, + color="tab:green", + color_removed="tab:red", + marker_size: float = 60.0, + figax=None, + **show_kwargs, +): + """Aperture positions drawn over a diffraction image. + + Parameters + ---------- + positions : array-like of float + (N, 2) aperture positions in (row, col) pixels of `image`. + image : array-like, optional + Background image, such as the mean pattern or the Bragg vector map. + radius : float, optional + Draw each aperture as a circle of this radius in pixels. None draws + markers of size `marker_size`. + positions_removed : array-like of float, optional + (M, 2) positions drawn in `color_removed`, for example the apertures + removed by aperture_array_subtract. + color, color_removed : matplotlib color + Colors of the kept and removed apertures. + marker_size : float, default=60 + Marker size in points squared, used when radius is None. + figax : (Figure, Axes), optional + Axes to draw into. + **show_kwargs : + Passed to quantem.core.visualization.show_2d, for example + norm={"power": 0.5}. + + Returns + ------- + fig, ax + """ + from quantem.core.visualization import show_2d + + if image is not None: + show_kwargs.setdefault("axsize", (6, 6)) + fig, ax = show_2d(np.asarray(image), figax=figax, **show_kwargs) + elif figax is not None: + fig, ax = figax + else: + fig, ax = plt.subplots(figsize=(6, 6)) + ax.set_aspect("equal") + ax.invert_yaxis() + + def draw(p, c): + p = np.atleast_2d(np.asarray(p, dtype=np.float64)) + if p.size == 0: + return + if radius is None: + ax.scatter(p[:, 1], p[:, 0], s=marker_size, color=c, alpha=0.5, lw=0) + else: + ax.add_collection( + EllipseCollection( + widths=2.0 * radius, + heights=2.0 * radius, + angles=0, + units="xy", + facecolors=c, + alpha=0.4, + offsets=p[:, ::-1], + offset_transform=ax.transData, + ) + ) + + if positions_removed is not None: + draw(positions_removed, color_removed) + draw(positions, color) + return fig, ax + + +# --------------------------------------------------------------------------- # +# Polar selection +# --------------------------------------------------------------------------- # + + +def _polar_coordinates(peaks, q_fields=None, center=(0.0, 0.0)): + """(qr, qphi) of every flattened row, with qphi in degrees.""" + q = _q_coordinates(peaks, q_fields, center) + qr = np.hypot(q[:, 0], q[:, 1]) + qphi = np.degrees(np.arctan2(-q[:, 0], q[:, 1])) + return qr, qphi + + +def add_polar_fields( + peaks, + q_fields=None, + center=(0.0, 0.0), + names: tuple[str, str] = ("qr", "qphi"), +): + """Copy of the peaks with polar coordinate fields added. + + The radius qr has the units of the diffraction coordinates. The angle + qphi is in degrees, measured anticlockwise from the +col (right) direction + as the pattern is displayed with rows increasing downward, over the range + (-180, 180]. + + Parameters + ---------- + peaks : Vector + Peaks with one cell per probe position. + q_fields : (str, str), optional + Diffraction coordinate fields. + center : array-like of float, default=(0, 0) + (2,) origin of the polar coordinates, normally the direct beam. + names : (str, str), default=("qr", "qphi") + Names of the new fields. + + Returns + ------- + Vector + """ + q_fields = _resolve_q_fields(peaks, q_fields) + qr, qphi = _polar_coordinates(peaks, q_fields, center) + q_unit = peaks.units[peaks.fields.index(q_fields[0])] + out = peaks.copy() + out.add_fields(list(names), values=np.stack([qr, qphi], axis=1), units=[q_unit, "deg"]) + return out + + +def polar_mask( + peaks, + q_radius: float, + tol: float = 1.0, + phi_range: tuple[float, float] | None = None, + q_fields=None, + center=(0.0, 0.0), +) -> np.ndarray: + """Peaks inside a ring, optionally restricted to a range of angles. + + Parameters + ---------- + peaks : Vector + Peaks with one cell per probe position. + q_radius : float + Ring radius, in the units of the diffraction coordinates. + tol : float, default=1.0 + Half width of the ring: peaks with |qr - q_radius| <= tol are kept. + phi_range : (float, float), optional + Angular range (phi_0, phi_1) in degrees, using the qphi convention of + add_polar_fields. Peaks with phi_0 <= qphi < phi_1 are kept. When + phi_0 > phi_1 the range wraps through 180 degrees. + q_fields : (str, str), optional + Diffraction coordinate fields. + center : array-like of float, default=(0, 0) + (2,) origin of the polar coordinates. + + Returns + ------- + np.ndarray + (N,) bool mask aligned with the flattened rows of `peaks`. + """ + qr, qphi = _polar_coordinates(peaks, q_fields, center) + mask = np.abs(qr - q_radius) <= tol + if phi_range is not None: + phi_0, phi_1 = phi_range + if phi_0 <= phi_1: + mask &= (qphi >= phi_0) & (qphi < phi_1) + else: + mask &= (qphi >= phi_0) | (qphi < phi_1) + return mask + + +def radial_ddf_image( + peaks, + q_radius: float, + tol: float = 1.0, + phi_range: tuple[float, float] | None = None, + q_fields=None, + center=(0.0, 0.0), + intensity_field: str = "intensity", +) -> np.ndarray: + """Digital dark field image from a ring of diffraction space. + + Equivalent to ddf_image(peaks, polar_mask(peaks, q_radius, tol, phi_range)). + See polar_mask for the parameters. + + Returns + ------- + np.ndarray + (scan_row, scan_col) image. + """ + mask = polar_mask( + peaks, q_radius, tol=tol, phi_range=phi_range, q_fields=q_fields, center=center ) + return ddf_image(peaks, mask, intensity_field=intensity_field) + + +# --------------------------------------------------------------------------- # +# Clustering +# --------------------------------------------------------------------------- # def cluster_coms( @@ -97,13 +556,54 @@ def ddf_images( rc = _scan_cells(labeled) R, C = labeled.shape[:2] + cluster_ids = np.atleast_1d(cluster_ids) out = np.zeros((len(cluster_ids), R, C)) - for i, k in enumerate(np.atleast_1d(cluster_ids)): + for i, k in enumerate(cluster_ids): m = labels == k np.add.at(out[i], (rc[m, 0], rc[m, 1]), inten[m]) return out +def assign_grain_labels( + labeled, + grain_labels, + label_field: str = "cluster", + grain_field: str = "grain_label", +): + """Copy of the L1-labeled peaks with the L2 grain of every peak added. + + Parameters + ---------- + labeled : Vector + Peaks carrying L1 cluster labels (from cluster_vector). + grain_labels : array-like of int + (K,) L2 grain label of each L1 cluster, for example + dbscan(cluster_coms(labeled)[0], ...). + label_field : str, default="cluster" + Field holding the L1 cluster labels. + grain_field : str, default="grain_label" + Name of the new field. + + Returns + ------- + Vector + Copy of `labeled` with `grain_field` added. Peaks outside every L1 + cluster get -2, and peaks whose L1 cluster joined no grain get -1. + """ + l1 = labeled.select_fields(label_field).numpy()[:, 0].astype(int) + grain_labels = np.asarray(grain_labels, dtype=int) + grains = np.where(l1 >= 0, grain_labels[l1.clip(min=0)], -2) + + out = labeled.copy() + out.add_fields(grain_field, values=grains[:, None], units="index") + return out + + +# --------------------------------------------------------------------------- # +# Display +# --------------------------------------------------------------------------- # + + def composite_ddf( images: np.ndarray, colors=None, @@ -128,13 +628,10 @@ def composite_ddf( np.ndarray (R, C, 3) RGB image in [0, 1]. """ - K = images.shape[0] if colors is None: hues = np.linspace(0, 1, K, endpoint=False) - colors = hsv_to_rgb( - np.stack([hues, np.ones(K), np.ones(K)], axis=1) - ) + colors = hsv_to_rgb(np.stack([hues, np.ones(K), np.ones(K)], axis=1)) colors = np.asarray(colors, dtype=float) if normalize == "global": @@ -147,142 +644,99 @@ def composite_ddf( def color_wheel(n: int = 256, saturation: float = 1.0) -> np.ndarray: - """ - (n, n, 4) RGBA hue wheel for labeling composite images. + """Hue wheel for labeling composite images. Parameters - n: int - Size of the output image. - saturation: float - Saturation of the hue wheel (0=gray, 1=full color). + ---------- + n : int, default=256 + Size of the output image in pixels. + saturation : float, default=1.0 + Saturation of the hues, from 0 (gray) to 1 (full color). Returns ------- np.ndarray - (n, n, 4) RGBA image in [0, 1]. + (n, n, 4) RGBA image in [0, 1], transparent outside the wheel. """ - - y, x = np.mgrid[-1 : 1 : n * 1j, -1 : 1 : n * 1j] r = np.hypot(x, y) hue = (np.arctan2(y, x) / (2 * np.pi)) % 1.0 - hsv = np.stack( - [hue, np.full_like(hue, saturation), np.clip(r, 0, 1)], axis=-1 - ) - rgba = np.concatenate( - [hsv_to_rgb(hsv), (r <= 1.0)[..., None].astype(float)], axis=-1 - ) + hsv = np.stack([hue, np.full_like(hue, saturation), np.clip(r, 0, 1)], axis=-1) + rgba = np.concatenate([hsv_to_rgb(hsv), (r <= 1.0)[..., None].astype(float)], axis=-1) return rgba def plot_cluster_scatter( labeled, - q_fields=("qx", "qy"), + q_fields=None, label_field: str = "cluster", - specific_cluster: int | None = None, + specific_cluster: int | Sequence[int] | None = None, max_clusters: int | None = None, show_unclustered: bool = True, point_size: float = 2.0, alpha: float = 0.2, figax=None, ): - """ - All peaks in diffraction space, colored by cluster (unclustered in gray). + """All peaks in diffraction space, colored by cluster. Parameters ---------- labeled : Vector - Labeled diffraction data. - q_fields: tuple of str - Field names for the diffraction-space coordinates to plot. - label_field: str - Field name for the cluster label. - specific_cluster: int | None - If given, plot only this cluster (unclustered points are not shown). - max_clusters: int | None - Maximum number of clusters to plot (for large datasets). - show_unclustered: bool - Whether to show unclustered points (label < 0) in gray. - point_size: float - Size of the scatter points. - alpha: float - Transparency of the scatter points. Recommended to be << 1.0 for large datasets so only dense - regions are visible. - figax: tuple of (matplotlib.figure.Figure, matplotlib.axes.Axes) | None - If provided, plot into this figure and axes instead of creating a new one. + Peaks carrying a label field. + q_fields : (str, str), optional + Diffraction coordinate fields, drawn as (vertical, horizontal). + label_field : str, default="cluster" + Field holding the labels, for example "grain_label" from + assign_grain_labels. + specific_cluster : int or sequence of int, optional + Draw only these clusters, in one color, without the unclustered peaks. + max_clusters : int, optional + Draw only the first max_clusters clusters (the largest, since dbscan + sorts clusters by size). + show_unclustered : bool, default=True + Draw the peaks with a negative label in gray. + point_size : float, default=2.0 + Marker size. + alpha : float, default=0.2 + Marker opacity. Values well below 1 show the dense regions of large + datasets. + figax : (Figure, Axes), optional + Axes to draw into. Returns ------- - fig: matplotlib.figure.Figure - The figure containing the scatter plot. - ax: matplotlib.axes.Axes - The axes containing the scatter plot. + fig, ax """ - - fields = labeled.fields - flat = labeled.numpy().astype(np.float64) - labels = flat[:, fields.index(label_field)].astype(int) - qx = flat[:, fields.index(q_fields[0])] - qy = flat[:, fields.index(q_fields[1])] + q_fields = _resolve_q_fields(labeled, q_fields) + q = labeled.select_fields(*q_fields).numpy().astype(np.float64) + labels = labeled.select_fields(label_field).numpy()[:, 0].astype(int) + q0, q1 = q[:, 0], q[:, 1] if figax is None: fig, ax = plt.subplots(figsize=(6.5, 6.5)) else: fig, ax = figax - q_max = np.max(np.abs([qx, qy]))*1.05 + q_max = 1.05 * float(np.abs(q).max()) if q.size else 1.0 ax.set_xlim(-q_max, q_max) - ax.set_ylim(-q_max, q_max) + ax.set_ylim(q_max, -q_max) ax.set_aspect("equal") - ax.invert_yaxis() - ax.set_xlabel("$q_y$") - ax.set_ylabel("$q_x$") + ax.set_xlabel(q_fields[1]) + ax.set_ylabel(q_fields[0]) if specific_cluster is not None: - m = labels == specific_cluster - ax.scatter(qy[m], qx[m], s=point_size, color="C0", lw=0, alpha=alpha) + m = np.isin(labels, np.atleast_1d(specific_cluster)) + ax.scatter(q1[m], q0[m], s=point_size, color="C0", lw=0, alpha=alpha) return fig, ax - + if show_unclustered: m = labels < 0 - ax.scatter(qy[m], qx[m], s=point_size, color="0.85", lw=0) + ax.scatter(q1[m], q0[m], s=point_size, color="0.85", lw=0) n = labels.max() + 1 - ids = range(n if max_clusters is None else min(n, max_clusters)) + n_show = n if max_clusters is None else min(n, max_clusters) cmap = plt.get_cmap("hsv") rng = np.random.default_rng(0) - hues = rng.permutation(np.linspace(0, 1, len(list(ids)), endpoint=False)) - for k in ids: + hues = rng.permutation(np.linspace(0, 1, n_show, endpoint=False)) + for k in range(n_show): m = labels == k - ax.scatter(qy[m], qx[m], s=point_size, color=cmap(hues[k]), lw=0, alpha=alpha) + ax.scatter(q1[m], q0[m], s=point_size, color=cmap(hues[k]), lw=0, alpha=alpha) return fig, ax - -def assign_grain_labels(L1, L2_labels,label_field: str = "cluster"): - """Assign L2 grain labels to L1 clusters based on their centers of mass. - - Parameters - ---------- - L1 : Vector - Labeled diffraction data with L1 cluster labels. - L2_labels : np.ndarray - (K,) array of L2 grain labels corresponding to each L1 cluster. - - Returns - ------- - L2 : Vector - Labeled diffraction data with L2 grain labels assigned. - L2_labels : np.ndarray - Updated array of L2 grain labels. - """ - fields = L1.fields - flat = L1.flatten() - L1_labels = flat[:, fields.index(label_field)].astype(int) - L1_unique = np.unique(L1_labels) - - L1_to_L2 = np.insert(L2_labels,0,-2) - mapper = dict(zip(L1_unique, L1_to_L2)) - L2_labels_full = np.array([mapper[label] for label in L1_labels]) - - L2 = L1.copy() - L2.add_fields("grain_label", values = L2_labels_full) - - # Return the new Vector with L2 labels - return L2 \ No newline at end of file diff --git a/tests/diffraction/test_digital_dark_field.py b/tests/diffraction/test_digital_dark_field.py new file mode 100644 index 000000000..0e1bdc50a --- /dev/null +++ b/tests/diffraction/test_digital_dark_field.py @@ -0,0 +1,86 @@ +"""Digital dark field: apertures, polar selection and grain labels on synthetic peaks.""" + +import numpy as np +import pytest + +from quantem.core.datastructures.vector import Vector +from quantem.diffraction import digital_dark_field as ddf + + +def _lattice_peaks(R=4, C=5, fields=("q_row", "q_col", "intensity")): + """Square lattice g1=(10,0), g2=(0,10); cells with c >= 3 also carry (5,5).""" + nested = [] + for r in range(R): + row = [] + for c in range(C): + pts = [[10 * i, 10 * j, 1.0 + r] for i in (-1, 0, 1) for j in (-1, 0, 1)] + if c >= 3: + pts.append([5.0, 5.0, 2.0]) + row.append(np.asarray(pts, dtype=float)) + nested.append(row) + return Vector.from_data(nested, fields=list(fields)) + + +def test_aperture_array_modes(): + g1, g2 = (10.0, 0.0), (0.0, 10.0) + arr = ddf.aperture_array(g1, g2, n1_range=(-1, 1), n2_range=(-1, 1)) + assert arr.shape == (9, 2) + no_center = ddf.aperture_array( + g1, g2, n1_range=(-1, 1), n2_range=(-1, 1), radius_range=(1, np.inf) + ) + assert no_center.shape == (8, 2) + line = ddf.aperture_array(g1, mode="line", n1_range=(-2, 2), center=(50, 50)) + np.testing.assert_allclose(line[:, 1], 50.0) + single = ddf.aperture_array(g1, g2, mode="single", shift=(1, 2)) + np.testing.assert_allclose(single, [[10.0, 20.0]]) + clipped = ddf.aperture_array(g1, g2, center=(15, 15), shape=(30, 30), edge=6) + np.testing.assert_allclose(clipped, [[15.0, 15.0]]) # 5 and 25 lie within the edge + with pytest.raises(ValueError): + ddf.aperture_array(g1, g2, mode="bad") + + +def test_aperture_subtract_and_image(): + fine = ddf.aperture_array((5.0, 0.0), (0.0, 5.0), n1_range=(-2, 2), n2_range=(-2, 2)) + coarse = ddf.aperture_array((10.0, 0.0), (0.0, 10.0), n1_range=(-1, 1), n2_range=(-1, 1)) + super_only = ddf.aperture_array_subtract(fine, coarse, tol=1.0) + assert super_only.shape == (25 - 9, 2) + + peaks = _lattice_peaks() + image = ddf.aperture_ddf_image(peaks, super_only, radius=1.0) + assert image.shape == (4, 5) + assert np.all(image[:, :3] == 0) + np.testing.assert_allclose(image[:, 3:], 2.0) + + # overlapping apertures count each peak once + image_full = ddf.aperture_ddf_image(peaks, np.vstack([coarse, coarse]), radius=1.0) + np.testing.assert_allclose(image_full[2, 0], 9 * 3.0) + + +def test_polar_fields_and_mask(): + peaks = _lattice_peaks(fields=("qx", "qy", "intensity")) + polar = ddf.add_polar_fields(peaks) + assert polar.fields[-2:] == ["qr", "qphi"] + flat = polar.select_fields("qx", "qy", "qr", "qphi").numpy() + np.testing.assert_allclose(flat[:, 2], np.hypot(flat[:, 0], flat[:, 1]), atol=1e-5) + # (qx, qy) = (-10, 0) is straight up on screen: +90 degrees + up = (flat[:, 0] == -10) & (flat[:, 1] == 0) + np.testing.assert_allclose(flat[up, 3], 90.0) + + ring = ddf.polar_mask(peaks, 10.0, tol=0.5) + assert ring.sum() == 4 * 20 + upper = ddf.polar_mask(peaks, 10.0, tol=0.5, phi_range=(45, 135)) + assert upper.sum() == 20 + wrapped = ddf.polar_mask(peaks, 10.0, tol=0.5, phi_range=(135, -135)) # 180 degrees + assert wrapped.sum() == 20 + image = ddf.radial_ddf_image(peaks, 10.0, tol=0.5) + np.testing.assert_allclose(image[1], 4 * 2.0) + + +def test_assign_grain_labels(): + peaks = _lattice_peaks(R=1, C=1, fields=("qx", "qy", "intensity")) + labels = np.array([0, 0, 1, 1, -1, 2, 2, 2, -1]) + labeled = peaks.copy() + labeled.add_fields("cluster", values=labels[:, None]) + out = ddf.assign_grain_labels(labeled, grain_labels=np.array([3, -1, 4])) + grains = out.select_fields("grain_label").numpy()[:, 0] + np.testing.assert_array_equal(grains, [3, 3, -1, -1, -2, 4, 4, 4, -2]) From cdc35798e6fd84c1afc828679a863b28545181be Mon Sep 17 00:00:00 2001 From: cophus Date: Sun, 4 Oct 2026 15:45:15 -0700 Subject: [PATCH 29/36] Updating DDF functions --- src/quantem/diffraction/digital_dark_field.py | 145 ++++++++++++++++-- tests/diffraction/test_digital_dark_field.py | 29 ++++ 2 files changed, 165 insertions(+), 9 deletions(-) diff --git a/src/quantem/diffraction/digital_dark_field.py b/src/quantem/diffraction/digital_dark_field.py index 0bbfe476f..af04610e0 100644 --- a/src/quantem/diffraction/digital_dark_field.py +++ b/src/quantem/diffraction/digital_dark_field.py @@ -7,16 +7,17 @@ - Virtual apertures: peaks within a radius of a set of aperture positions, usually a lattice built from two reciprocal lattice vectors - (aperture_array, aperture_array_subtract, aperture_ddf_image). + (fit_lattice, aperture_array, aperture_array_subtract, aperture_ddf_image). - Polar selection: peaks within a ring of radius q, optionally restricted to a range of azimuthal angles (add_polar_fields, polar_mask, radial_ddf_image). - Clustering: DBSCAN in the joint (diffraction, scan) space groups the peaks - into single-spot, single-crystallite clusters (L1), clustering the - real-space centers of mass of those clusters groups the spots of each grain - (L2), and clustering the remaining peaks in diffraction space alone - isolates ring-like nanocrystalline or amorphous components (L3) - (cluster_coms, ddf_images, assign_grain_labels). + into single-spot, single-crystallite clusters (L1), clustering either the + real-space centers of mass or the dark field images of those clusters + groups the spots of each grain (L2), and clustering the remaining peaks in + diffraction space alone isolates ring-like nanocrystalline or amorphous + components (L3) (ddf_images, cluster_coms, group_ddf_images, + assign_grain_labels). All functions read the peaks from a Vector with one cell per probe position. The diffraction coordinates are given by `q_fields`, which defaults to @@ -193,6 +194,85 @@ def aperture_array( return positions[keep] +def fit_lattice( + peaks, + g1, + g2, + center=(0.0, 0.0), + radius: float = 6.0, + n1_range: tuple[int, int] = (-5, 5), + n2_range: tuple[int, int] = (-5, 5), + num_iterations: int = 3, + q_fields=None, + intensity_field: str = "intensity", +): + """Refine two lattice vectors against the peaks of every probe position. + + For each lattice point n1 g1 + n2 g2 (excluding the origin) we take the + intensity-weighted mean position of all peaks within `radius` of it, + summed over the scan, then solve for g1 and g2 by weighted least squares + with each point weighted by its summed intensity. Repeating this a few + times lets the fit follow lattice points that start near the edge of + `radius`. The lattice with the most total intensity dominates, so the + starting vectors should be close to the orientation of interest. + + Parameters + ---------- + peaks : Vector + Peaks with one cell per probe position. + g1, g2 : array-like of float + (2,) starting lattice vectors. + center : array-like of float, default=(0, 0) + (2,) lattice origin, held fixed. + radius : float, default=6.0 + Search radius around each lattice point. + n1_range, n2_range : (int, int), default=(-5, 5) + Inclusive range of lattice multiples used in the fit. + num_iterations : int, default=3 + Number of search and fit passes. + q_fields : (str, str), optional + Diffraction coordinate fields. + intensity_field : str, default="intensity" + Field used as the weight of each peak. + + Returns + ------- + g1, g2 : np.ndarray + (2,) refined lattice vectors. + """ + q = _q_coordinates(peaks, q_fields, center) + w = peaks.select_fields(intensity_field).numpy()[:, 0].astype(np.float64).clip(min=0) + n1, n2 = np.meshgrid( + np.arange(n1_range[0], n1_range[1] + 1), + np.arange(n2_range[0], n2_range[1] + 1), + indexing="ij", + ) + n = np.stack([n1.ravel(), n2.ravel()], axis=1) + n = n[np.any(n != 0, axis=1)].astype(np.float64) + + g = np.stack([np.asarray(g1, dtype=np.float64), np.asarray(g2, dtype=np.float64)]) + order = np.argsort(q[:, 0]) + q_sorted, w_sorted = q[order], w[order] + for _ in range(num_iterations): + targets = n @ g + means = np.full_like(targets, np.nan) + weights = np.zeros(targets.shape[0]) + for k, t in enumerate(targets): + # peaks sorted along the first coordinate, so each search is a slice + i0, i1 = np.searchsorted(q_sorted[:, 0], [t[0] - radius, t[0] + radius]) + qs, ws = q_sorted[i0:i1], w_sorted[i0:i1] + m = ((qs - t[None, :]) ** 2).sum(axis=1) <= radius**2 + if ws[m].sum() > 0: + weights[k] = ws[m].sum() + means[k] = (qs[m] * ws[m, None]).sum(axis=0) / weights[k] + ok = weights > 0 + if ok.sum() < 2: + raise ValueError("Fewer than two lattice points have peaks within radius.") + sw = np.sqrt(weights[ok])[:, None] + g, *_ = np.linalg.lstsq(n[ok] * sw, means[ok] * sw, rcond=None) + return g[0], g[1] + + def aperture_array_subtract( positions, positions_remove, @@ -599,6 +679,46 @@ def assign_grain_labels( return out +def group_ddf_images( + images: np.ndarray, + min_correlation: float = 0.7, + min_samples: int = 2, + device: str = "cpu", +) -> np.ndarray: + """Group DDF images that show the same region of the sample. + + The spots of one grain or lath share the same dark field image, so we + cluster the images by their cosine similarity. Each image is normalized + to unit length, and DBSCAN runs with eps = sqrt(2 (1 - min_correlation)), + so that neighbors have a cosine similarity of at least min_correlation. + Unlike clustering the centers of mass (cluster_coms), this separates + grains that extend across the whole field of view, such as a matrix + phase. + + Parameters + ---------- + images : np.ndarray + (K, R, C) DDF images, for example ddf_images(labeled, range(K)). + min_correlation : float, default=0.7 + Cosine similarity between neighboring images, from 0 to 1. + min_samples : int, default=2 + DBSCAN min_samples, counting the image itself. + device : str, default="cpu" + Torch device for the distance computations. + + Returns + ------- + np.ndarray + (K,) group label of each image, -1 for images that joined no group. + Groups are numbered largest first. + """ + K = images.shape[0] + v = images.reshape(K, -1).astype(np.float64) + v = v / np.maximum(np.linalg.norm(v, axis=1, keepdims=True), 1e-12) + eps = float(np.sqrt(2.0 * (1.0 - min_correlation))) + return dbscan(v, eps=eps, min_samples=min_samples, device=device) + + # --------------------------------------------------------------------------- # # Display # --------------------------------------------------------------------------- # @@ -675,6 +795,7 @@ def plot_cluster_scatter( show_unclustered: bool = True, point_size: float = 2.0, alpha: float = 0.2, + center=None, figax=None, ): """All peaks in diffraction space, colored by cluster. @@ -700,6 +821,9 @@ def plot_cluster_scatter( alpha : float, default=0.2 Marker opacity. Values well below 1 show the dense regions of large datasets. + center : array-like of float, optional + (2,) diffraction origin at the middle of the plot. Defaults to the + "origin_ref" stored by BraggVectors.correct_peak_origins, or (0, 0). figax : (Figure, Axes), optional Axes to draw into. @@ -708,6 +832,9 @@ def plot_cluster_scatter( fig, ax """ q_fields = _resolve_q_fields(labeled, q_fields) + if center is None: + center = labeled.metadata.get("origin_ref", (0.0, 0.0)) + center = np.asarray(center, dtype=np.float64) q = labeled.select_fields(*q_fields).numpy().astype(np.float64) labels = labeled.select_fields(label_field).numpy()[:, 0].astype(int) q0, q1 = q[:, 0], q[:, 1] @@ -716,9 +843,9 @@ def plot_cluster_scatter( fig, ax = plt.subplots(figsize=(6.5, 6.5)) else: fig, ax = figax - q_max = 1.05 * float(np.abs(q).max()) if q.size else 1.0 - ax.set_xlim(-q_max, q_max) - ax.set_ylim(q_max, -q_max) + q_max = 1.05 * float(np.abs(q - center[None, :]).max()) if q.size else 1.0 + ax.set_xlim(center[1] - q_max, center[1] + q_max) + ax.set_ylim(center[0] + q_max, center[0] - q_max) ax.set_aspect("equal") ax.set_xlabel(q_fields[1]) ax.set_ylabel(q_fields[0]) diff --git a/tests/diffraction/test_digital_dark_field.py b/tests/diffraction/test_digital_dark_field.py index 0e1bdc50a..501832791 100644 --- a/tests/diffraction/test_digital_dark_field.py +++ b/tests/diffraction/test_digital_dark_field.py @@ -84,3 +84,32 @@ def test_assign_grain_labels(): out = ddf.assign_grain_labels(labeled, grain_labels=np.array([3, -1, 4])) grains = out.select_fields("grain_label").numpy()[:, 0] np.testing.assert_array_equal(grains, [3, 3, -1, -1, -2, 4, 4, 4, -2]) + + +def test_fit_lattice_and_group_images(): + rng = np.random.default_rng(0) + g1, g2 = np.array([20.0, 3.0]), np.array([4.0, 21.0]) + nested = [] + for r in range(3): + row = [] + for c in range(3): + n = np.array([[i, j] for i in (-2, -1, 0, 1, 2) for j in (-2, -1, 0, 1, 2)], float) + q = n @ np.stack([g1, g2]) + rng.normal(0, 0.2, (len(n), 2)) + row.append(np.concatenate([q, np.ones((len(n), 1))], axis=1)) + nested.append(row) + peaks = Vector.from_data(nested, fields=["q_row", "q_col", "intensity"]) + f1, f2 = ddf.fit_lattice( + peaks, (19.0, 2.0), (5.0, 20.0), radius=4.0, n1_range=(-2, 2), n2_range=(-2, 2) + ) + np.testing.assert_allclose(f1, g1, atol=0.1) + np.testing.assert_allclose(f2, g2, atol=0.1) + + a = np.zeros((4, 4)) + a[:2] = 1 + b = np.zeros((4, 4)) + b[2:] = 1 + images = np.stack([a, 2 * a, a + 0.05 * b, b, 3 * b, np.eye(4)]) + labels = ddf.group_ddf_images(images, min_correlation=0.9) + assert labels[0] == labels[1] == labels[2] >= 0 + assert labels[3] == labels[4] >= 0 and labels[3] != labels[0] + assert labels[5] == -1 From 48b5ad2f9016a83d437bae8e83a10345f9476df8 Mon Sep 17 00:00:00 2001 From: cophus Date: Sun, 4 Oct 2026 17:25:20 -0700 Subject: [PATCH 30/36] few more DDF updates --- src/quantem/diffraction/digital_dark_field.py | 71 ++++++++++++++++++- tests/diffraction/test_digital_dark_field.py | 14 ++++ 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/quantem/diffraction/digital_dark_field.py b/src/quantem/diffraction/digital_dark_field.py index af04610e0..bd57d1707 100644 --- a/src/quantem/diffraction/digital_dark_field.py +++ b/src/quantem/diffraction/digital_dark_field.py @@ -7,7 +7,8 @@ - Virtual apertures: peaks within a radius of a set of aperture positions, usually a lattice built from two reciprocal lattice vectors - (fit_lattice, aperture_array, aperture_array_subtract, aperture_ddf_image). + (fit_lattice, lattice_distance, aperture_array, aperture_array_subtract, + aperture_ddf_image). - Polar selection: peaks within a ring of radius q, optionally restricted to a range of azimuthal angles (add_polar_fields, polar_mask, radial_ddf_image). @@ -16,8 +17,8 @@ real-space centers of mass or the dark field images of those clusters groups the spots of each grain (L2), and clustering the remaining peaks in diffraction space alone isolates ring-like nanocrystalline or amorphous - components (L3) (ddf_images, cluster_coms, group_ddf_images, - assign_grain_labels). + components (L3) (ddf_images, cluster_coms, cluster_centers, + group_ddf_images, assign_grain_labels). All functions read the peaks from a Vector with one cell per probe position. The diffraction coordinates are given by `q_fields`, which defaults to @@ -273,6 +274,34 @@ def fit_lattice( return g[0], g[1] +def lattice_distance( + positions, + g1, + g2, + center=(0.0, 0.0), +) -> np.ndarray: + """Distance from each position to the nearest point of a 2D lattice. + + Parameters + ---------- + positions : array-like of float + (N, 2) diffraction positions, for example from cluster_centers. + g1, g2 : array-like of float + (2,) lattice vectors. + center : array-like of float, default=(0, 0) + (2,) lattice origin. + + Returns + ------- + np.ndarray + (N,) distances, in the units of the positions. + """ + basis = np.stack([np.asarray(g1, dtype=np.float64), np.asarray(g2, dtype=np.float64)]) + q = np.atleast_2d(np.asarray(positions, dtype=np.float64)) - np.asarray(center)[None, :] + frac = q @ np.linalg.inv(basis) + return np.linalg.norm((frac - np.round(frac)) @ basis, axis=1) + + def aperture_array_subtract( positions, positions_remove, @@ -616,6 +645,42 @@ def cluster_coms( return coms, sizes +def cluster_centers( + labeled, + q_fields=None, + label_field: str = "cluster", + intensity_field: str = "intensity", +) -> np.ndarray: + """Intensity-weighted diffraction position of every cluster. + + Parameters + ---------- + labeled : Vector + Vector carrying a cluster label field (from cluster_vector). + q_fields : (str, str), optional + Diffraction coordinate fields. + label_field : str, default="cluster" + Field holding the cluster labels. + intensity_field : str, default="intensity" + Field used as the weight of each peak. + + Returns + ------- + np.ndarray + (K, 2) mean diffraction positions, ordered by cluster id. + """ + q = _q_coordinates(labeled, q_fields) + labels = labeled.select_fields(label_field).numpy()[:, 0].astype(int) + w = labeled.select_fields(intensity_field).numpy()[:, 0].astype(np.float64).clip(min=0) + m = labels >= 0 + n = labels.max() + 1 + wsum = np.maximum(np.bincount(labels[m], weights=w[m], minlength=n), 1e-12) + return np.stack( + [np.bincount(labels[m], weights=w[m] * q[m, k], minlength=n) / wsum for k in range(2)], + axis=1, + ) + + def ddf_images( labeled, cluster_ids, diff --git a/tests/diffraction/test_digital_dark_field.py b/tests/diffraction/test_digital_dark_field.py index 501832791..de4a4b7b3 100644 --- a/tests/diffraction/test_digital_dark_field.py +++ b/tests/diffraction/test_digital_dark_field.py @@ -113,3 +113,17 @@ def test_fit_lattice_and_group_images(): assert labels[0] == labels[1] == labels[2] >= 0 assert labels[3] == labels[4] >= 0 and labels[3] != labels[0] assert labels[5] == -1 + + +def test_cluster_centers_and_lattice_distance(): + peaks = _lattice_peaks(R=1, C=2) + labeled = peaks.copy() + n = labeled.total_rows + labels = np.full(n, -1) + labels[:9] = 0 # the 3x3 lattice in cell (0, 0) + labeled.add_fields("cluster", values=labels[:, None]) + centers = ddf.cluster_centers(labeled) + np.testing.assert_allclose(centers, [[0.0, 0.0]], atol=1e-6) + + d = ddf.lattice_distance([[10.0, 10.0], [5.0, 5.0], [11.0, 0.0]], (10.0, 0.0), (0.0, 10.0)) + np.testing.assert_allclose(d, [0.0, np.hypot(5, 5), 1.0]) From b638dbd14771215345b589b727791e6952f39c62 Mon Sep 17 00:00:00 2001 From: cophus Date: Mon, 5 Oct 2026 06:12:53 -0700 Subject: [PATCH 31/36] changing layout of the diffraction demo --- widget/js/diffsim-web/index.ts | 49 ++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/widget/js/diffsim-web/index.ts b/widget/js/diffsim-web/index.ts index 50a7604cc..b444120b2 100644 --- a/widget/js/diffsim-web/index.ts +++ b/widget/js/diffsim-web/index.ts @@ -138,6 +138,14 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { .${id}-top { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; justify-content: center; margin-bottom: 8px; } .${id}-panels { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; } .${id}-panel { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 0 0 auto; } + .${id}-ctl { display: flex; flex-direction: column; gap: 6px; } + .${id}-panels.stacked { flex-direction: column; flex-wrap: nowrap; align-items: center; } + .${id}-panels.stacked .${id}-panel { display: contents; } + .${id}-panels.stacked #${id}-cell { order: 1; } + .${id}-panels.stacked #${id}-pat { order: 2; } + .${id}-panels.stacked #${id}-patctl { order: 3; } + .${id}-panels.stacked #${id}-cellctl { order: 4; } + .${id}-panels.stacked #${id}-ewaldc { order: 5; } .${id}-canvas { display: block; border-radius: 4px; touch-action: none; cursor: grab; background: #000; } .${id}-canvas.cell { background: var(--${id}-cellbg, #161616); border: 1px solid var(--${id}-border, #444); } .${id}-row { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; } @@ -185,6 +193,7 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) {
+
x −
x +
y −
y +
@@ -204,9 +213,11 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) {
Drag to tilt the crystal (the near face follows the pointer). Shift-drag, or two fingers, twist about the beam. The beam comes toward you.
+
+
nanobeam
@@ -243,6 +254,7 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) {
Drag the pattern to move the tilt map. Double-click a disk to tilt to its two-beam condition, or empty space to put the Laue circle centre there.
+
`; @@ -271,21 +283,44 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { // ---------------------------------------------------------------- size // Layout: left column = cell (Sc square) above the Ewald panel (Sc x Se); // right column = pattern square whose side S matches the left column's - // height, so S = Sc + gap + Se with Se = Sc / 2. Stacked on narrow screens. + // height, so S = Sc + gap + Se with Se = Sc / 2. On narrow (portrait) + // screens everything stacks in one column with the cell and the pattern + // adjacent, then the controls, then the Ewald panel. Both layouts are + // capped so the canvases fit in the visible height below a fixed page + // header (the MyST top bar). let S = 400, Sc = 260, Se = 130; const GAP = 8; + const panelsEl = $(`.${id}-panels`); + // small viewport height (browser toolbars shown): stable while the mobile toolbars collapse on scroll + const probe = document.createElement("div"); + probe.style.cssText = "position:fixed;top:0;left:0;width:0;height:100vh;height:100svh;visibility:hidden;pointer-events:none"; + el.appendChild(probe); + const availHeight = () => { + const h = probe.offsetHeight || window.innerHeight; + let nav = 0; + for (const e of document.querySelectorAll(".myst-top-nav, header")) { + const p = getComputedStyle(e).position; + const r = e.getBoundingClientRect(); + if ((p === "fixed" || p === "sticky") && r.top <= 0 && r.height < h / 3) nav = Math.max(nav, r.bottom); + } + return h - nav - 16; + }; const computeSize = () => { const w = wrap.clientWidth - 30; + const h = availHeight(); const sideBySide = w >= 2.5 * 180 + 2 * GAP + 12; if (sideBySide) { - Sc = Math.floor(Math.min((w - 12 - GAP) / 2.5, state.sizePref / 1.5)); + const hCap = state.showEwald ? (h - GAP) / 1.5 : h; + Sc = Math.floor(Math.max(140, Math.min((w - 12 - GAP) / 2.5, state.sizePref / 1.5, hCap))); Se = state.showEwald ? Math.round(Sc / 2) : 0; S = Sc + (Se ? GAP + Se : 0); } else { - Sc = Math.max(180, Math.min(state.sizePref, w)); + // cell and pattern stacked: keep both on screen at once + Sc = Math.floor(Math.max(180, Math.min(state.sizePref, w, (h - 12) / 2))); Se = state.showEwald ? Math.round(Sc / 2) : 0; S = Sc; } + panelsEl.classList.toggle("stacked", !sideBySide); cellCanvas.style.width = `${Sc}px`; cellCanvas.style.height = `${Sc}px`; ewaldCanvas.style.display = Se ? "block" : "none"; ewaldCanvas.style.width = `${Sc}px`; ewaldCanvas.style.height = `${Se}px`; @@ -293,6 +328,8 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { const panels = wrap.querySelectorAll(`.${id}-panel`); if (panels[0]) panels[0].style.width = `${Sc}px`; if (panels[1]) panels[1].style.width = `${S}px`; + $(`#${id}-cellctl`).style.width = sideBySide ? "" : `${Math.max(Sc, Math.min(w, 420))}px`; + $(`#${id}-patctl`).style.width = sideBySide ? "" : `${Math.max(Sc, Math.min(w, 420))}px`; }; // ---------------------------------------------------------------- physics cache @@ -810,9 +847,11 @@ export function render({ model, el }: { model: Model; el: HTMLElement }) { computeSize(); applyTheme(); recompute(); - const ro = new ResizeObserver(() => { const old = S + Sc; computeSize(); if (S + Sc !== old) drawAll(); }); + const relayout = () => { const old = S + Sc; computeSize(); if (S + Sc !== old) drawAll(); }; + const ro = new ResizeObserver(relayout); ro.observe(wrap); - return () => { mo.disconnect(); ro.disconnect(); }; + ro.observe(probe); // viewport height: rotation, window resize + return () => { mo.disconnect(); ro.disconnect(); probe.remove(); }; } export default { render }; From 68d0ba94f04903ed1a9722d75da34ca8694d130e Mon Sep 17 00:00:00 2001 From: cophus Date: Tue, 6 Oct 2026 16:50:14 -0700 Subject: [PATCH 32/36] various fixes --- .gitignore | 14 - pyproject.toml | 4 +- src/quantem/core/datastructures/dataset.py | 34 - .../core/datastructures/dataset4dstem.py | 14 +- .../core/datastructures/polar4dstem.py | 94 +- src/quantem/core/io/file_readers.py | 127 +- src/quantem/core/io/serialize.py | 90 +- src/quantem/core/utils/clustering.py | 57 +- .../core/visualization/visualization_utils.py | 13 +- src/quantem/diffraction/__init__.py | 2 + src/quantem/diffraction/bloch.py | 1388 +++++++++++--- src/quantem/diffraction/bragg_vectors.py | 259 ++- .../bragg_vectors_visualization.py | 49 +- src/quantem/diffraction/calibration.py | 465 ++++- src/quantem/diffraction/crystal.py | 288 ++- src/quantem/diffraction/crystal_map.py | 132 +- src/quantem/diffraction/defaults.py | 24 +- src/quantem/diffraction/digital_dark_field.py | 182 +- src/quantem/diffraction/disk_detection.py | 56 +- src/quantem/diffraction/illumination.py | 172 +- src/quantem/diffraction/orientation.py | 440 ++++- .../diffraction/orientation_visualization.py | 265 ++- src/quantem/diffraction/phase.py | 136 +- .../diffraction/reverse_monte_carlo.py | 354 +++- src/quantem/diffraction/rotations.py | 306 ++- src/quantem/diffraction/strain.py | 481 +++-- .../diffraction/strain_visualization.py | 303 ++- .../diffraction/wk_scattering_factors.py | 176 +- tests/core/test_clustering.py | 20 +- tests/core/test_file_readers.py | 93 + tests/core/test_polar4dstem.py | 53 + tests/core/test_serialize_bundle.py | 74 + tests/diffraction/test_bloch.py | 469 +++-- tests/diffraction/test_calibrate.py | 157 ++ tests/diffraction/test_calibration_refine.py | 4 +- tests/diffraction/test_crystal.py | 110 ++ tests/diffraction/test_crystal_map.py | 229 +++ tests/diffraction/test_digital_dark_field.py | 117 +- tests/diffraction/test_illumination.py | 25 + tests/diffraction/test_orientation.py | 165 +- tests/diffraction/test_reverse_monte_carlo.py | 75 + tests/diffraction/test_rotation_convention.py | 8 +- tests/diffraction/test_rotations.py | 94 + .../diffraction/test_strain_bragg_vectors.py | 174 ++ uv.lock | 4 +- widget/js/colormaps.ts | 1105 ----------- widget/js/diffsim-web/cif.ts | 341 ---- widget/js/diffsim-web/index.ts | 857 --------- widget/js/diffsim-web/lobato.ts | 107 -- widget/js/diffsim-web/presets.ts | 14 - widget/js/diffsim/crystal3d.ts | 328 ---- widget/js/diffsim/index.tsx | 881 --------- widget/js/diffsim/math.ts | 234 --- widget/js/diffsim/pattern.ts | 445 ----- widget/js/diffsim/physics.ts | 537 ------ widget/js/format.ts | 40 - widget/js/theme.ts | 149 -- widget/package-lock.json | 1637 ----------------- widget/package.json | 26 - widget/pyproject.toml | 22 - widget/scripts/build.mjs | 47 - widget/src/quantem/widget/__init__.py | 11 - widget/src/quantem/widget/diffsim.py | 559 ------ widget/tsconfig.json | 25 - 64 files changed, 6314 insertions(+), 8847 deletions(-) create mode 100644 tests/core/test_file_readers.py create mode 100644 tests/core/test_polar4dstem.py create mode 100644 tests/core/test_serialize_bundle.py create mode 100644 tests/diffraction/test_calibrate.py create mode 100644 tests/diffraction/test_crystal_map.py create mode 100644 tests/diffraction/test_strain_bragg_vectors.py delete mode 100644 widget/js/colormaps.ts delete mode 100644 widget/js/diffsim-web/cif.ts delete mode 100644 widget/js/diffsim-web/index.ts delete mode 100644 widget/js/diffsim-web/lobato.ts delete mode 100644 widget/js/diffsim-web/presets.ts delete mode 100644 widget/js/diffsim/crystal3d.ts delete mode 100644 widget/js/diffsim/index.tsx delete mode 100644 widget/js/diffsim/math.ts delete mode 100644 widget/js/diffsim/pattern.ts delete mode 100644 widget/js/diffsim/physics.ts delete mode 100644 widget/js/format.ts delete mode 100644 widget/js/theme.ts delete mode 100644 widget/package-lock.json delete mode 100644 widget/package.json delete mode 100644 widget/pyproject.toml delete mode 100644 widget/scripts/build.mjs delete mode 100644 widget/src/quantem/widget/__init__.py delete mode 100644 widget/src/quantem/widget/diffsim.py delete mode 100644 widget/tsconfig.json diff --git a/.gitignore b/.gitignore index 3fec799c6..c5bc9776b 100644 --- a/.gitignore +++ b/.gitignore @@ -194,17 +194,3 @@ ipynb-playground/ CLAUDE.md AGENTS.md AGENT.md - -# widget (JS build artifacts); acom keeps the diffraction widget under widget/ -node_modules/ -widget/src/quantem/widget/static/ -widget/dist/ - -# widget, local-only (per-developer notebooks, docs scratch, build/test scripts) -widget/.gitignore -widget/docs/ -widget/notebooks/ -widget/scripts/ -!widget/scripts/build.mjs -widget/tests/integration/ -widget/tests/snapshots/ diff --git a/pyproject.toml b/pyproject.toml index a949cd638..e463bb581 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,8 +48,8 @@ dependencies = [ "optuna>=4.5.0", "hdf5plugin>=6.0.0", "torchinfo>=1.8.0", - "ase", - "spglib", + "ase>=3.23", + "spglib>=2.5", "em-database>=0.5", ] diff --git a/src/quantem/core/datastructures/dataset.py b/src/quantem/core/datastructures/dataset.py index 1ef22b74e..947449784 100644 --- a/src/quantem/core/datastructures/dataset.py +++ b/src/quantem/core/datastructures/dataset.py @@ -191,11 +191,6 @@ def sampling(self) -> NDArray: def sampling(self, value: NDArray | tuple | list | float | int) -> None: self._sampling = validate_ndinfo(value, self.ndim, "sampling") - @property - def origin_units(self) -> NDArray: - # Origin expressed in physical units: origin * sampling - return np.asarray(self.origin) * np.asarray(self.sampling) - @property def units(self) -> list[str]: return self._units @@ -373,35 +368,6 @@ def _copy_custom_attributes(self, new_dataset: Self) -> None: # Skip attributes that can't be copied pass - def coords(self, axis: int) -> Any: - """ - Coordinate array for a given axis in pixel units. - - coords(d) = arange(shape[d]) - origin[d] - """ - axis = int(axis) - if axis < 0 or axis >= self.ndim: - raise ValueError(f"axis {axis} out of bounds for ndim={self.ndim}") - - xp = self._xp - n = int(self.shape[axis]) - origin_d = float(np.asarray(self.origin)[axis]) - - return xp.arange(n, dtype=float) - origin_d - - def coords_units(self, axis: int) -> Any: - """ - Coordinate array for a given axis in physical units. - - coords_units(d) = (arange(shape[d]) - origin[d]) * sampling[d] - """ - axis = int(axis) - if axis < 0 or axis >= self.ndim: - raise ValueError(f"axis {axis} out of bounds for ndim={self.ndim}") - - sampling_d = float(np.asarray(self.sampling)[axis]) - return self.coords(axis) * sampling_d - def mean(self, axes: int | tuple[int, ...] | None = None) -> Any: """ Computes and returns mean of the data array. diff --git a/src/quantem/core/datastructures/dataset4dstem.py b/src/quantem/core/datastructures/dataset4dstem.py index d95d8cd1c..4b2c80560 100644 --- a/src/quantem/core/datastructures/dataset4dstem.py +++ b/src/quantem/core/datastructures/dataset4dstem.py @@ -74,8 +74,18 @@ def __init__( signal_units : str, optional Units for the array values, by default "arb. units" metadata : dict - "r_to_q_rotation_cw_deg": rotation r to q clockwise in degrees - "ellipticity": 3 parameters (a, b, theta (degrees)) + Missing keys below are set to None. + + "q_to_r_rotation_ccw_deg" : float + Rotation in degrees that maps detector (q) vectors onto the + scan (r) frame. A detector vector (dr, dc) is first swapped to + (dc, dr) if "q_transpose" is True, then rotated as + dr' = cos(t) dr - sin(t) dc, dc' = sin(t) dr + cos(t) dc. + "q_transpose" : bool + If True, swap the detector row and column axes before the + rotation. + "ellipticity" : tuple + 3 parameters (a, b, theta in degrees). _token : object | None, optional Token to prevent direct instantiation, by default None """ diff --git a/src/quantem/core/datastructures/polar4dstem.py b/src/quantem/core/datastructures/polar4dstem.py index 6619af5c9..81ae5d037 100644 --- a/src/quantem/core/datastructures/polar4dstem.py +++ b/src/quantem/core/datastructures/polar4dstem.py @@ -1,13 +1,14 @@ +from typing import TYPE_CHECKING, Any + import numpy as np from numpy.typing import NDArray -from typing import Any, TYPE_CHECKING from scipy.ndimage import map_coordinates +from quantem.core.datastructures.dataset4d import Dataset4d + if TYPE_CHECKING: from .dataset4dstem import Dataset4dstem -from quantem.core.datastructures.dataset4d import Dataset4d - class Polar4dstem(Dataset4d): """4D-STEM dataset in polar coordinates (scan_y, scan_x, phi, r).""" @@ -60,6 +61,30 @@ def from_array( signal_units: str = "arb. units", metadata: dict | None = None, ) -> "Polar4dstem": + """ + Create a Polar4dstem from a 4D array shaped (scan_y, scan_x, phi, r). + + Parameters + ---------- + array : NDArray + Polar data, shape (scan_y, scan_x, n_phi, n_r). + name : str, optional + Dataset name, by default "Polar 4D-STEM dataset". + origin : NDArray | tuple | list | float | int, optional + Origin of each axis in calibrated units, by default zeros. + sampling : NDArray | tuple | list | float | int, optional + Sampling of each axis, by default ones. + units : list[str] | tuple | list, optional + Units of each axis, by default ["pixels", "pixels", "deg", "pixels"]. + signal_units : str, optional + Units of the array values, by default "arb. units". + metadata : dict, optional + Metadata. Missing "polar_*" keys are set to None. + + Returns + ------- + Polar4dstem + """ array = np.asarray(array) if array.ndim != 4: raise ValueError("Polar4dstem.from_array expects a 4D array.") @@ -84,11 +109,13 @@ def from_array( @property def n_phi(self) -> int: - return int(self.array.shape[2]) + """Number of azimuthal bins (axis 2).""" + return int(self.shape[2]) @property def n_r(self) -> int: - return int(self.array.shape[3]) + """Number of radial bins (axis 3).""" + return int(self.shape[3]) def _precompute_polar_coords( @@ -151,8 +178,8 @@ def _precompute_polar_coords( def dataset4dstem_polar_transform( self: "Dataset4dstem", - origin_row: float | int | NDArray, - origin_col: float | int | NDArray, + origin_row: float, + origin_col: float, ellipse_params: tuple[float, float, float] | None = None, num_annular_bins: int = 180, radial_min: float = 0.0, @@ -162,9 +189,54 @@ def dataset4dstem_polar_transform( name: str | None = None, signal_units: str | None = None, ) -> Polar4dstem: - if self.array.ndim != 4: + """ + Resample every diffraction pattern onto a polar (phi, r) grid. + + Bound to `Dataset4dstem.polar_transform`. Uses bilinear interpolation + (`scipy.ndimage.map_coordinates`, order 1); samples outside the detector + are set to 0. + + Parameters + ---------- + origin_row, origin_col : float + Center of the polar grid on the detector, in detector pixels. The same + center is used for all scan positions. + ellipse_params : tuple[float, float, float], optional + Elliptical distortion (a, b, theta_deg): the radius along the + direction theta_deg (degrees) is scaled by a / b. None for circular + sampling. + num_annular_bins : int, optional + Number of azimuthal bins, by default 180. + radial_min : float, optional + First radial bin in detector pixels, by default 0. + radial_max : float, optional + Radial upper limit (exclusive) in detector pixels. If None, the + distance from the origin to the nearest detector edge. + radial_step : float, optional + Radial bin width in detector pixels, by default 1. + two_fold_rotation_symmetry : bool, optional + If True, phi covers [0, 180) degrees instead of [0, 360). + name : str, optional + Name of the output, by default "_polar". + signal_units : str, optional + Signal units of the output, by default those of this dataset. + + Returns + ------- + Polar4dstem + Array shaped (scan_y, scan_x, n_phi, n_r). Phi is in degrees, 0 along + +column and increasing toward +row. The radial axis uses the sampling + and units of the last detector axis. The polar parameters are stored + in metadata under "polar_*" keys. + + Notes + ----- + Tensor-backed datasets are copied to a CPU numpy array first. + """ + array = self.numpy() + if array.ndim != 4: raise ValueError("polar_transform requires a 4D-STEM dataset (ndim=4).") - scan_y, scan_x, ny, nx = self.array.shape + scan_y, scan_x, ny, nx = array.shape origin_row_f = float(origin_row) origin_col_f = float(origin_col) coords, phi_bins, radial_bins, radial_max_eff = _precompute_polar_coords( @@ -181,11 +253,11 @@ def dataset4dstem_polar_transform( ) n_phi = phi_bins.size n_r = radial_bins.size - result_dtype = np.result_type(self.array.dtype, np.float32) + result_dtype = np.result_type(array.dtype, np.float32) out = np.empty((scan_y, scan_x, n_phi, n_r), dtype=result_dtype) for iy in range(scan_y): for ix in range(scan_x): - dp = self.array[iy, ix] + dp = array[iy, ix] out[iy, ix] = map_coordinates( dp, coords, diff --git a/src/quantem/core/io/file_readers.py b/src/quantem/core/io/file_readers.py index ba35d1fc3..395d828e0 100644 --- a/src/quantem/core/io/file_readers.py +++ b/src/quantem/core/io/file_readers.py @@ -19,19 +19,67 @@ ) +def _resolve_rsciio_plugin(file_path: str | PathLike, file_type: str | None = None) -> str: + """ + Resolve the RosettaSciIO plugin module used to read a file. + + Parameters + ---------- + file_path : str | PathLike + Path to the file. Its extension is used when ``file_type`` is None. + file_type : str, optional + RosettaSciIO plugin name (e.g. "digitalmicrograph", "quantumdetector") + or a file extension (e.g. "dm4", "mib"). Case-insensitive. + + Returns + ------- + str + Module name of the plugin, e.g. "rsciio.digitalmicrograph". + + Raises + ------ + ValueError + If no plugin matches, or if an extension is listed by more than one + plugin (e.g. ".h5"); pass the plugin name as ``file_type`` in that case. + """ + import rsciio + + key = file_type if file_type is not None else Path(file_path).suffix.lstrip(".") + key = str(key).lower().lstrip(".") + if not key: + raise ValueError( + f"Cannot infer the file type of '{file_path}'; pass file_type= " + "(a RosettaSciIO plugin name such as 'digitalmicrograph')." + ) + + plugins = rsciio.IO_PLUGINS + by_name = sorted({p["api"] for p in plugins if p["api"].lower() == f"rsciio.{key}"}) + if by_name: + return by_name[0] + + by_ext = sorted( + {p["api"] for p in plugins if key in (ext.lower() for ext in p["file_extensions"])} + ) + if len(by_ext) == 1: + return by_ext[0] + if len(by_ext) > 1: + names = ", ".join(f"'{api.removeprefix('rsciio.')}'" for api in by_ext) + raise ValueError( + f"File extension '{key}' is used by several RosettaSciIO plugins ({names}). " + "Pass one of them as file_type=." + ) + raise ValueError(f"No RosettaSciIO reader for file type '{key}'.") + + def _rsciio_reader(file_path: str | PathLike, file_type: str | None = None): - """rosettasciio file_reader from a plugin name ("digitalmicrograph") or extension ("dm3").""" - if file_type is None: - file_type = Path(file_path).suffix.lstrip(".") - try: - return importlib.import_module(f"rsciio.{file_type.lower()}").file_reader - except ModuleNotFoundError: - import rsciio + """ + Return ``(plugin, file_reader)`` for a file, see `_resolve_rsciio_plugin`. - for plugin in rsciio.IO_PLUGINS: - if file_type.lower() in (ext.lower() for ext in plugin["file_extensions"]): - return importlib.import_module(plugin["api"]).file_reader - raise ValueError(f"No rosettasciio reader for file type '{file_type}'") + An ImportError raised here means the plugin exists but one of its optional + dependencies is missing. + """ + plugin = _resolve_rsciio_plugin(file_path, file_type) + return plugin, importlib.import_module(plugin).file_reader def _print_available_datasets(data_list): @@ -59,7 +107,9 @@ def read_4dstem( file_path : str | PathLike Path to data. file_type : str, optional - The type of file reader needed. See RosettaSciIO for supported formats: + RosettaSciIO plugin name (e.g. "arina", "digitalmicrograph") or file + extension. If None, the extension of `file_path` is used. Extensions + shared by several plugins (e.g. "h5") require the plugin name. See https://hyperspy.org/rosettasciio/supported_formats/index.html dataset_index : int, optional Index of the dataset to load if file contains multiple datasets. @@ -129,7 +179,7 @@ def read_4dstem( def _reshape_3d_to_4d( imported_data: dict, *, - dataset_index_local: int | None, + dataset_index_local: int, scan_length_local: int, scan_axis_local: int, transpose_scan_axes_local: bool, @@ -140,9 +190,6 @@ def _reshape_3d_to_4d( f"Expected 3D data to reshape, got ndim={data.ndim} with shape {data.shape}" ) - if scan_axis_local not in (0, 1): - raise ValueError(f"scan_axis must be 0 or 1, got {scan_axis_local}") - # Move scan axis to front so it becomes the frame axis if scan_axis_local != 0: data = np.moveaxis(data, scan_axis_local, 0) @@ -185,8 +232,8 @@ def _reshape_3d_to_4d( "name": "scan_x", } - ax_qy = dict(old_axes[1]) - ax_qx = dict(old_axes[2]) + # Detector calibrations come from the two axes that are not the scan axis. + ax_qy, ax_qx = (dict(ax) for i, ax in enumerate(old_axes) if i != scan_axis_local) imported_data_4d = imported_data.copy() imported_data_4d["data"] = data_4d @@ -194,33 +241,28 @@ def _reshape_3d_to_4d( original_shape = imported_data["data"].shape new_shape = data_4d.shape - if dataset_index_local is not None: - print( - f"Using 3D dataset {dataset_index_local} with shape {original_shape} " - f"interpreted as 4D with shape={new_shape} " - f"(scan_axis={scan_axis_local}, scan_length={scan_length_local}, " - f"transpose_scan_axes={transpose_scan_axes_local})." - ) - else: - print( - f"Using 3D dataset with shape {original_shape} " - f"interpreted as 4D with shape={new_shape} " - f"(scan_axis={scan_axis_local}, scan_length={scan_length_local}, " - f"transpose_scan_axes={transpose_scan_axes_local})." - ) + print( + f"Using 3D dataset {dataset_index_local} with shape {original_shape} " + f"interpreted as 4D with shape={new_shape} " + f"(scan_axis={scan_axis_local}, scan_length={scan_length_local}, " + f"transpose_scan_axes={transpose_scan_axes_local})." + ) return imported_data_4d + if scan_axis not in (0, 1): + raise ValueError(f"scan_axis must be 0 or 1, got {scan_axis}") + sampling_override = kwargs.pop("sampling", None) origin_override = kwargs.pop("origin", None) units_override = kwargs.pop("units", None) name_override = kwargs.pop("name", None) - file_reader = _rsciio_reader(file_path, file_type) + plugin, file_reader = _rsciio_reader(file_path, file_type) data_list = file_reader(file_path, **kwargs) if not data_list: - raise ValueError(f"No datasets returned by rsciio.{file_type} for '{file_path}'") + raise ValueError(f"No datasets returned by {plugin} for '{file_path}'") # Case 1: dataset_index specified explicitly if dataset_index is not None: @@ -279,8 +321,6 @@ def _reshape_3d_to_4d( candidates: list[tuple[int, dict]] = [] for i, d in three_d_datasets: shape = d["data"].shape - if scan_axis < 0 or scan_axis > 2: - raise ValueError(f"scan_axis must be in [0, 2] for 3D data, got {scan_axis}") n_frames_axis = shape[scan_axis] if n_frames_axis % scan_length == 0: candidates.append((i, d)) @@ -366,7 +406,7 @@ def read_3d_spectroscopy( """ data_type_normalized = str(data_type).upper() - file_reader = _rsciio_reader(file_path, file_type) + plugin, file_reader = _rsciio_reader(file_path, file_type) data_list = file_reader(file_path) # If specific index provided, use it @@ -395,13 +435,10 @@ def read_3d_spectroscopy( ) imported_axes = imported_data["axes"] - # axis_order = (0, 1, 2) if file_type == "digitalmicrograph" else (2, 0, 1) - axis_order = (1, 2, 0) if file_type == "digitalmicrograph" else (0, 1, 2) - array = ( - imported_data["data"].transpose(axis_order) - if file_type == "digitalmicrograph" - else imported_data["data"] - ) + # DigitalMicrograph spectrum images are reordered so that axis 0 moves last. + is_dm = plugin == "rsciio.digitalmicrograph" + axis_order = (1, 2, 0) if is_dm else (0, 1, 2) + array = imported_data["data"].transpose(axis_order) if is_dm else imported_data["data"] ordered_axes = [imported_axes[idx] for idx in axis_order] sampling = [ax.get("scale", 1) for ax in ordered_axes] origin = [ax.get("offset", 0) for ax in ordered_axes] @@ -452,7 +489,7 @@ def read_2d( -------- Dataset """ - file_reader = _rsciio_reader(file_path, file_type) + _, file_reader = _rsciio_reader(file_path, file_type) imported_data = file_reader(file_path)[0] dataset = Dataset2d.from_array( diff --git a/src/quantem/core/io/serialize.py b/src/quantem/core/io/serialize.py index 87aec0919..d1f959a86 100644 --- a/src/quantem/core/io/serialize.py +++ b/src/quantem/core/io/serialize.py @@ -1,5 +1,6 @@ import gzip import io +import json import os import shutil import tempfile @@ -181,6 +182,32 @@ def _convert_string_to_device_if_needed(val: Any, group: zarr.Group, key: str) - return val return val + @staticmethod + def _read_ase_atoms(group: zarr.Group) -> Any: + """Rebuild an ase.Atoms written by `_serialize_value`.""" + from ase import Atoms + + if "arrays" in group.group_keys(): + arrays_group = AutoSerialize._get_group(group, "arrays") + arrays = { + k: AutoSerialize._read_array_np(arrays_group, k) for k in arrays_group.array_keys() + } + arrays.update( + {k: np.asarray(v) for k, v in dict(group.attrs.get("text_arrays", {})).items()} + ) + else: # files written before per-atom arrays were stored + arrays = {k: AutoSerialize._read_array_np(group, k) for k in ("numbers", "positions")} + atoms = Atoms( + numbers=arrays.pop("numbers"), + positions=arrays.pop("positions"), + cell=AutoSerialize._read_array_np(group, "cell"), + pbc=AutoSerialize._read_array_np(group, "pbc"), + ) + for key, arr in arrays.items(): + atoms.set_array(key, arr) + atoms.info.update(dict(group.attrs.get("info", {}))) + return atoms + @staticmethod def _is_autoserialize_instance(value: Any) -> bool: """Return True if value behaves like an AutoSerialize instance, even across autoreloads.""" @@ -462,18 +489,25 @@ def _serialize_value( # Don't try to save the state - it's not essential for core functionality elif type(value).__module__.startswith("ase.") and type(value).__name__ == "Atoms": - # An ase.Atoms is fully defined by these four arrays; storing them - # keeps the file readable and avoids pickling an ase version in. + # Stored as plain arrays so the file stays readable without pickling + # an ase version in: cell, pbc, every per-atom array in atoms.arrays + # (numbers, positions, occupancy, tags, masses, ...) and the + # JSON-serializable entries of atoms.info. Constraints and attached + # calculators are not saved. subgroup = group.require_group(name) subgroup.attrs["_ase_atoms"] = True - self._write_ndarray( - subgroup, "numbers", np.asarray(value.get_atomic_numbers()), compressors - ) - self._write_ndarray( - subgroup, "positions", np.asarray(value.get_positions()), compressors - ) self._write_ndarray(subgroup, "cell", np.asarray(value.get_cell()), compressors) self._write_ndarray(subgroup, "pbc", np.asarray(value.get_pbc()), compressors) + arrays_group = subgroup.require_group("arrays") + text_arrays = {} + for key, arr in value.arrays.items(): + arr = np.asarray(arr) + if arr.dtype.kind in "biufc": + self._write_ndarray(arrays_group, key, arr, compressors) + else: + text_arrays[key] = arr.tolist() + subgroup.attrs["text_arrays"] = _json_entries(text_arrays, f"{name}.arrays") + subgroup.attrs["info"] = _json_entries(dict(value.info), f"{name}.info") else: # Fallback: dill-serialize + gzip-compress @@ -592,14 +626,7 @@ def _recursive_load( # ase.Atoms group if subgrp.attrs.get("_ase_atoms"): - from ase import Atoms - - atoms = Atoms( - numbers=AutoSerialize._read_array_np(subgrp, "numbers"), - positions=AutoSerialize._read_array_np(subgrp, "positions"), - cell=AutoSerialize._read_array_np(subgrp, "cell"), - pbc=AutoSerialize._read_array_np(subgrp, "pbc"), - ) + atoms = AutoSerialize._read_ase_atoms(subgrp) if type(atoms) in skip_types: continue setattr(obj, name, atoms) @@ -1557,6 +1584,21 @@ def _recurse(obj: Any, prefix: str = "", current_depth: int = 0, is_last: bool = _recurse(root) +def _json_entries(entries: dict, label: str) -> dict: + """Return the JSON-serializable entries of a dict, warning about the rest.""" + kept, dropped = {}, [] + for key, val in entries.items(): + try: + json.dumps({str(key): val}) + except (TypeError, ValueError): + dropped.append(str(key)) + else: + kept[str(key)] = val + if dropped: + print(f"Not saving non-JSON-serializable entries of {label}: {dropped}") + return kept + + class Bundle(AutoSerialize): """A named collection of serializable objects, saved as one file. @@ -1567,9 +1609,25 @@ class Bundle(AutoSerialize): bundle.save("data.zip") b = load("data.zip"); b.adf, b.peaks + Parameters + ---------- + **objects + Objects to store, keyed by attribute name. Names must not shadow an + existing attribute or method of the class (e.g. ``save``). + + Raises + ------ + ValueError + If a name shadows a class attribute or method. """ def __init__(self, **objects): + reserved = sorted(name for name in objects if hasattr(type(self), name)) + if reserved: + raise ValueError( + f"Bundle names {reserved} shadow Bundle/AutoSerialize attributes; " + "choose different names." + ) for name, obj in objects.items(): setattr(self, name, obj) diff --git a/src/quantem/core/utils/clustering.py b/src/quantem/core/utils/clustering.py index cf3c18c8d..ff3d0abb3 100644 --- a/src/quantem/core/utils/clustering.py +++ b/src/quantem/core/utils/clustering.py @@ -101,14 +101,10 @@ def block_candidates(i0: int, i1: int) -> tuple[int, int]: if edge_rows: rows = torch.cat(edge_rows).cpu().numpy() cols = torch.cat(edge_cols).cpu().numpy() - graph = sp.coo_matrix( - (np.ones(rows.shape[0], dtype=np.int8), (rows, cols)), shape=(N, N) - ) + graph = sp.coo_matrix((np.ones(rows.shape[0], dtype=np.int8), (rows, cols)), shape=(N, N)) _, comp = connected_components(graph, directed=False) core_np = core.cpu().numpy() - labels = torch.as_tensor( - np.where(core_np, comp, -1), dtype=torch.long, device=device - ) + labels = torch.as_tensor(np.where(core_np, comp, -1), dtype=torch.long, device=device) # pass 3: border points join the nearest core cluster within eps for i0 in range(0, N, block): @@ -118,9 +114,7 @@ def block_candidates(i0: int, i1: int) -> tuple[int, int]: continue j0, j1 = block_candidates(i0, i1) d = torch.cdist(ps[i0:i1], ps[j0:j1]) - d = torch.where( - core[None, j0:j1], d, torch.full_like(d, torch.inf) - ) + d = torch.where(core[None, j0:j1], d, torch.full_like(d, torch.inf)) d_min, j_min = d.min(dim=1) near = bmask & (d_min <= eps) if bool(near.any()): @@ -168,12 +162,17 @@ def cluster_vector( Ragged vector over a scan grid. fields : sequence of str Field names contributing dimensions. - eps, min_samples : - DBSCAN parameters (Euclidean metric in the scaled space). + eps : float + DBSCAN neighborhood radius, in the scaled clustering space + (Euclidean metric). + min_samples : int + Neighbors (including the point itself) required for a core point. field_scales : sequence of float | None Multiplier per field; default 1. scan_scales : (float, float) | None If given, append (row * s0, col * s1) of each row's scan cell. + device : str | torch.device, default="cpu" + Torch device for the DBSCAN distance computations. label_field : str, default="cluster" Name of the label field on the returned Vector. @@ -212,8 +211,31 @@ def filter_rows(vector, mask): """Copy of a ragged Vector keeping only the flattened rows where mask. The scan-grid shape is unchanged; rows are dropped from their cells. + + Parameters + ---------- + vector : Vector + Ragged vector over a 2D scan grid. + mask : array-like of bool + (vector.total_rows,) keep flag per row, in the flattened order of + vector.numpy(). + + Returns + ------- + Vector + New Vector with the same fields, units, name, dtype and metadata, + holding only the kept rows. + + Raises + ------ + ValueError + If len(mask) differs from vector.total_rows. """ - mask = np.asarray(mask, dtype=bool) + mask = np.asarray(mask, dtype=bool).ravel() + if mask.size != vector.total_rows: + raise ValueError( + f"mask has {mask.size} entries but the Vector has {vector.total_rows} rows." + ) counts = np.asarray(vector.row_counts(), dtype=int) flat = vector.numpy().astype(np.float64) starts = np.concatenate([[0], np.cumsum(counts)]) @@ -223,14 +245,17 @@ def filter_rows(vector, mask): row = [] for c in range(shape[1]): k = r * shape[1] + c - sel = mask[starts[k]:starts[k + 1]] - row.append(flat[starts[k]:starts[k + 1]][sel]) + sel = mask[starts[k] : starts[k + 1]] + row.append(flat[starts[k] : starts[k + 1]][sel]) nested.append(row) from quantem.core.datastructures.vector import Vector out = Vector.from_data( - nested, fields=list(vector.fields), units=list(vector.units), - name=vector.name, dtype=vector.dtype, + nested, + fields=list(vector.fields), + units=list(vector.units), + name=vector.name, + dtype=vector.dtype, ) out.metadata.update(vector.metadata) return out diff --git a/src/quantem/core/visualization/visualization_utils.py b/src/quantem/core/visualization/visualization_utils.py index c595eadd7..5bac8029e 100644 --- a/src/quantem/core/visualization/visualization_utils.py +++ b/src/quantem/core/visualization/visualization_utils.py @@ -335,6 +335,11 @@ def _normalize_length_units(length_units: float, units: str) -> tuple[float, str return length_units, units +# Minimum AnchoredSizeBar padding (fraction of the font size) when a box is +# drawn behind the scale bar, so the box does not clip the label. +_SCALEBAR_BOX_MIN_PAD = 0.35 + + def add_scalebar_to_ax( ax: Axes, array_size: float, @@ -381,7 +386,11 @@ def add_scalebar_to_ax( box : bool, default=False Draw a translucent box behind the bar and label so it stays readable on any image (e.g. white bar on a black box, or black bar - on a white box). + on a white box). The padding is raised to at least + ``_SCALEBAR_BOX_MIN_PAD`` (fraction of the font size) so the box clears the + label. ``box``, ``box_color`` and ``box_alpha`` are only available when + calling this function directly; ScalebarConfig and show_2d do not + pass them. box_color : str, default="black" Fill color of the box. box_alpha : float, default=0.5 @@ -414,7 +423,7 @@ def add_scalebar_to_ax( length_px, label, loc, - pad=pad_px if not box else max(pad_px, 0.35), + pad=pad_px if not box else max(pad_px, _SCALEBAR_BOX_MIN_PAD), color=color, frameon=box, label_top=label_top, diff --git a/src/quantem/diffraction/__init__.py b/src/quantem/diffraction/__init__.py index 2ccc6d9e2..f167408e5 100644 --- a/src/quantem/diffraction/__init__.py +++ b/src/quantem/diffraction/__init__.py @@ -8,3 +8,5 @@ from quantem.diffraction import bloch as bloch from quantem.diffraction import calibration as calibration from quantem.diffraction import rotations as rotations +from quantem.diffraction import digital_dark_field as digital_dark_field +from quantem.diffraction import illumination as illumination diff --git a/src/quantem/diffraction/bloch.py b/src/quantem/diffraction/bloch.py index e50218f37..1b469ef6d 100644 --- a/src/quantem/diffraction/bloch.py +++ b/src/quantem/diffraction/bloch.py @@ -1,16 +1,34 @@ -"""Dynamical (Bloch wave) diffraction for orientation and phase refinement. - -Second-pass refinement: kinematical matching fixes orientations from peak -positions (which dynamical scattering does not move), then this module -recomputes peak intensities with multiple scattering to refine specimen -thickness and phase assignment for the top candidates. - -Follows the Bloch wave formulation of De Graef (2003), ch. 5. The structure -matrix uses U_g = gamma_rel * F_g / pi with F_g the kinematical structure -factors (scattering amplitude per volume, 1/Angstrom^2), off-diagonals -U_(g-h) and diagonal 2 k0 s_g. Without absorption the matrix is Hermitian, -so one eigendecomposition per orientation gives the diffracted intensities -at every thickness essentially for free: +"""Dynamical (Bloch wave) electron diffraction. + +Simulation and refinement with the Bloch wave formulation of De Graef +(2003), ch. 5. The module has five families of functions: + +- Spot patterns: dynamical_pattern() gives the Bloch intensities of one + orientation at every thickness; refine_thickness() refits thickness and + phase of a fitted PhaseMap with them. +- Convergent beam patterns: calculate_cbed() and calculate_cbed_library() + (disk patterns), calculate_lacbed() (one reflection's rocking surface) + and calculate_kossel() (wide-angle Kossel patterns). +- Kossel reference patterns: calculate_kossel_reference() computes the + bright field over all beam directions once; kossel_from_reference() and + kossel_polar_from_reference() look patterns up from it. The line model + (kossel_lines(), render_kossel_lines(), kossel_line_segments()) describes + the same patterns as one profile per systematic row, with + kossel_reference_residual() adding the many-beam correction near zone + axes. +- Bragg-vector refinement: refine_dynamical() refines orientation, + thickness, in-plane deformation and phase per position against the + measured peak intensities; dynamical_maps(), plot_dynamical_maps(), + strain_crystal_frame() and plot_strain_crystal_frame() present the result. +- Image refinement: fit_disk_shape() and refine_dynamical_image() refine + against the diffraction pattern pixels. + +The structure matrix uses U_g = gamma_rel * F_g / pi with F_g the +kinematical structure factors (scattering amplitude per volume, +1/Angstrom^2), or the absorptive Weickenmeier-Kohl factors when the crystal +carries them (Crystal.calculate_dynamical_structure_factors), off-diagonals +U_(g-h) and diagonal 2 k0 s_g. One eigendecomposition per incident +direction gives the intensities at every thickness: psi(t) = C exp(2 pi i gamma t) C^-1 psi_0, A C = 2 k0 gamma C """ @@ -40,7 +58,7 @@ def relativistic_gamma(energy_ev: float) -> float: - """Relativistic mass factor 1 + eV / (m0 c^2).""" + """Relativistic mass factor 1 + eV / (m0 c^2) at beam energy energy_ev (eV).""" return 1.0 + float(energy_ev) / 510998.95 @@ -83,25 +101,33 @@ def _coupling_matrix( def keys(h): return ((h + m) * key_mult).sum(dim=-1) - lut = {int(k): i for i, k in enumerate(keys(hkl_all))} - diff_keys = keys(diff) + # vectorized lookup: binary search of the queried keys in the sorted + # stored keys + keys_all = keys(hkl_all) + order = torch.argsort(keys_all) + keys_sorted = keys_all[order] + + def lookup(k): + pos = torch.searchsorted(keys_sorted, k).clamp(max=keys_sorted.shape[0] - 1) + return torch.where(keys_sorted[pos] == k, order[pos], -1) + U = torch.zeros((nb, nb), dtype=torch.complex128) - idx = torch.tensor( - [lut.get(int(k), -1) for k in diff_keys.reshape(-1)], dtype=torch.long - ).reshape(nb, nb) + idx = lookup(keys(diff).reshape(-1)).reshape(nb, nb) has = idx >= 0 U[has] = U_all[idx[has]] U.fill_diagonal_(0) u0_imag = 0.0 if absorptive: - i0 = lut.get(int(keys(torch.zeros(3, dtype=torch.long))), -1) + i0 = int(lookup(keys(torch.zeros((1, 3), dtype=torch.long)))[0]) if i0 >= 0: u0_imag = float(U_all[i0].imag) return U, u0_imag, absorptive -_coverage_warned: set = set() +# guards the per-crystal record of issued coverage warnings, which the +# threads of refine_dynamical() check concurrently +_coverage_lock = threading.Lock() def _beam_universe(crystal: Crystal) -> tuple[torch.Tensor, torch.Tensor]: @@ -137,12 +163,19 @@ def _primitive_lattice_mask(crystal: Crystal, g: torch.Tensor) -> torch.Tensor: if lat_p is None: import spglib + from quantem.diffraction.crystal import _spglib_raises + cell = ( crystal.lat_real.numpy(), crystal.positions_frac.numpy(), crystal.numbers.numpy(), ) - prim = spglib.standardize_cell(cell, to_primitive=True, no_idealize=True) + try: + with _spglib_raises(): + prim = spglib.standardize_cell(cell, to_primitive=True, no_idealize=True) + except Exception: + # no primitive cell found: keep every point of the stored box + prim = None lat_p = np.asarray(crystal.lat_real.numpy() if prim is None else prim[0], dtype=float) crystal._primitive_lattice = lat_p m = g.to(torch.float64) @ torch.as_tensor(lat_p, dtype=torch.float64).T @@ -150,13 +183,25 @@ def _primitive_lattice_mask(crystal: Crystal, g: torch.Tensor) -> torch.Tensor: def _check_dynamical_factors(crystal: Crystal, energy_ev: float, g_max_beams: float) -> None: - """Warn once per crystal when the absorptive factors are missing, were - computed at another energy, or stop short of 1.5 times the largest beam - (the couplings g - h reach twice it, but the factors beyond 1.5 times - are negligible).""" - key = id(crystal) - if key in _coverage_warned: + """Warn when the absorptive factors are missing, were computed at + another energy, or stop short of 1.5 times the largest beam (the + couplings g - h reach twice it, but the factors beyond 1.5 times are + negligible). + + A warning is issued once per crystal and factor set: the record is + kept on the crystal, keyed by the energy and extent of its dynamical + factors and the energy of the calculation, so recomputing the factors + or running at another energy is checked again. + """ + key = ( + getattr(crystal, "dyn_energy_ev", None), + getattr(crystal, "dyn_k_max", None), + round(float(energy_ev)), + ) + warned = getattr(crystal, "_bloch_coverage_warned", None) + if warned is not None and key in warned: return + msgs = [] if getattr(crystal, "U_dyn", None) is None: k_kin = getattr(crystal, "k_max", None) msg = ( @@ -168,30 +213,37 @@ def _check_dynamical_factors(crystal: Crystal, energy_ev: float, g_max_beams: fl f", which stop at {k_kin:.2f} 1/A, short of the " f"{1.5 * g_max_beams:.2f} 1/A the couplings of this beam list need" ) - _coverage_warned.add(key) - warnings.warn(f"{crystal.name}: {msg}", stacklevel=3) + msgs.append(msg) + else: + e_dyn = getattr(crystal, "dyn_energy_ev", None) + k_dyn = getattr(crystal, "dyn_k_max", None) + if e_dyn is not None and abs(e_dyn - energy_ev) > 1.0: + msgs.append( + f"dynamical structure factors were computed at {e_dyn:.0f} eV, the " + f"calculation runs at {energy_ev:.0f} eV" + ) + # couplings g - h reach twice the beam radius, but the factors fall + # off fast: 1.5 times it keeps every coupling that matters (to 5%, + # since the fitted in-plane strain stretches the beams a little past + # k_max) + if k_dyn is not None and 1.5 * g_max_beams > 1.05 * k_dyn: + msgs.append( + f"dynamical structure factors extend to {k_dyn:.2f} 1/A but the beam " + f"list reaches {g_max_beams:.2f} 1/A, so couplings beyond " + f"{k_dyn:.2f} 1/A are missing (treated as zero); recompute with " + f"k_max >= {1.5 * g_max_beams:.2f}" + ) + if not msgs: return - e_dyn = getattr(crystal, "dyn_energy_ev", None) - k_dyn = getattr(crystal, "dyn_k_max", None) - msgs = [] - if e_dyn is not None and abs(e_dyn - energy_ev) > 1.0: - msgs.append( - f"dynamical structure factors were computed at {e_dyn:.0f} eV, the " - f"calculation runs at {energy_ev:.0f} eV" - ) - # couplings g - h reach twice the beam radius, but the factors fall off - # fast: 1.5 times it keeps every coupling that matters (to 5%, since the - # fitted in-plane strain stretches the beams a little past k_max) - if k_dyn is not None and 1.5 * g_max_beams > 1.05 * k_dyn: - msgs.append( - f"dynamical structure factors extend to {k_dyn:.2f} 1/A but the beam " - f"list reaches {g_max_beams:.2f} 1/A, so couplings beyond " - f"{k_dyn:.2f} 1/A are missing (treated as zero); recompute with " - f"k_max >= {1.5 * g_max_beams:.2f}" - ) - if msgs: - _coverage_warned.add(key) - warnings.warn(f"{crystal.name}: " + "; ".join(msgs), stacklevel=3) + with _coverage_lock: + warned = getattr(crystal, "_bloch_coverage_warned", None) + if warned is None: + warned = set() + crystal._bloch_coverage_warned = warned + if key in warned: + return + warned.add(key) + warnings.warn(f"{crystal.name}: " + "; ".join(msgs), stacklevel=3) def select_dynamical_beams( @@ -203,12 +255,37 @@ def select_dynamical_beams( k_max: float | None = None, deform: torch.Tensor | None = None, ) -> torch.Tensor: - """Beam list (nb, 3) hkl, 000 first, for a Bloch calculation at an - orientation and every incident direction within alpha_max_rad of the - optic axis: reflections with |s_g| < sg_max + alpha_max |g| (a tilt t - shifts s_g by at most |t| |g| / k0 to leading order). One list computed - with the largest tilt of a refinement search keeps every stage of that - search in the same truncated system.""" + """Beam list for a Bloch calculation over a range of incident directions. + + Selects the reflections with |s_g| < sg_max + alpha_max_rad |g| at the + given orientation, which covers every incident direction within + alpha_max_rad of the optic axis (a tilt t shifts s_g by at most + |t| |g| / k0 to leading order). One list computed with the largest tilt + of a refinement search keeps every stage of that search in the same + truncated system. + + Parameters + ---------- + crystal : Crystal + With structure factors calculated. + orientation : torch.Tensor + Unit quaternion (4,), crystal to lab. + energy_ev : float + Beam energy in eV. + alpha_max_rad : float, default=0.0 + Largest incident tilt from the optic axis, in radians. + sg_max : float, default=SG_MAX + Excitation error cutoff in 1/Angstroms at zero tilt. + k_max : float | None + Largest |g| in 1/Angstroms; None keeps every candidate reflection. + deform : torch.Tensor | None + (3, 3) deformation applied to the lab-frame reciprocal vectors. + + Returns + ------- + torch.Tensor + (nb, 3) Miller indices, with the 000 beam first. + """ lam = electron_wavelength_angstrom(energy_ev) hkl_u, g_u = _beam_universe(crystal) g_lab = qrotate(orientation, g_u) @@ -233,27 +310,35 @@ def dynamical_pattern( ) -> dict[str, torch.Tensor]: """Bloch-wave diffraction intensities for one orientation, all thicknesses. + The beams are the reflections with |s_g| < sg_max (and |g| <= k_max); + one eigendecomposition gives the intensities at every thickness. + Parameters ---------- crystal : Crystal - With structure factors calculated. For accurate couplings, - calculate_structure_factors should cover 2x the k_max used here so - every difference vector g - h has a structure factor. + With structure factors calculated. Preferably also with + calculate_dynamical_structure_factors (absorptive factors, at this + energy, covering at least 1.5 times k_max so the couplings g - h + that matter have a factor); a warning is issued otherwise. orientation : torch.Tensor Unit quaternion (4,) rotating crystal vectors into the lab frame. thicknesses_A : array-like or float Specimen thicknesses in Angstroms. - sg_max : float, default=0.1 + energy_ev : float, default=300e3 + Beam energy in eV. + sg_max : float, default=SG_MAX Excitation error cutoff (1/Angstroms) for including a beam. k_max : float | None - In-plane scattering vector cutoff for included beams. + Largest |g| (1/Angstroms) of an included beam; None keeps every + reflection within sg_max. Returns ------- dict - 'qx', 'qy' (N,), 'hkl' (N, 3), 'intensity' (T, N) diffracted + 'qx', 'qy' (N,) lab-frame positions (1/Angstroms), 'hkl' (N, 3), + 's_g' (N,) excitation errors, 'intensity' (T, N) diffracted intensities per thickness, 'intensity_000' (T,) the direct beam, - 's_g' (N,). + 'thicknesses' (T,) in Angstroms. """ if crystal.g_vec is None: raise RuntimeError("Run crystal.calculate_structure_factors() first.") @@ -324,37 +409,53 @@ def refine_thickness( min_number_peaks: int | None = None, progress_bar: bool = True, ): - """Second-pass thickness and phase refinement with dynamical intensities. + """Thickness and phase refinement with dynamical intensities. + + For every probe position, the winning candidates of a fitted PhaseMap are + re-simulated with Bloch waves over a thickness grid at their matched + orientations. The peak pairing is fixed (positions are kinematic); the + intensity cost is evaluated for all thicknesses from a single + eigendecomposition per candidate, and the best (thickness, candidate) + combination updates the phase decision. refine_dynamical() also refines + the orientation and the in-plane deformation. Parameters left as None inherit the phase fit's values (see PhaseMap.fit); the resolved values are recorded in phase_map.metadata['thickness']. - For every probe position, the winning candidates of a fitted PhaseMap are - re-simulated with Bloch waves over a thickness grid. The peak pairing is - fixed (positions are kinematic); the intensity cost is evaluated for all - thicknesses from a single eigendecomposition per candidate, and the best - (thickness, candidate) combination updates the phase decision. - Parameters ---------- phase_map : PhaseMap A fitted PhaseMap (fit() has been run). thicknesses_A : np.ndarray | None - Thickness grid in Angstroms; default 50 ... 1000 in 25 A steps. + Thickness grid in Angstroms; default 50 to 1000 in 25 A steps. + pair_distance : float | None + Largest distance (1/Angstroms) at which a simulated and a measured + peak are paired. + power_intensity : float | None + Intensities are compared as I ** power_intensity. + sg_max : float, default=SG_MAX + Excitation error cutoff (1/Angstroms) of the Bloch beam list. + k_max : float | None + Largest |g| (1/Angstroms) of a beam; None keeps every reflection + within sg_max. min_number_peaks : int | None Positions with fewer measured peaks, direct beam included, are skipped; None inherits the phase fit's minimum. At least 3. + progress_bar : bool, default=True + Show a progress bar over positions. Returns ------- dict - 'thickness' (R, C) best-fit thickness map, 'cost' (R, C, F) dynamical - costs per candidate at its best thickness, 'phase_index' (R, C) - updated phase assignment. + 'thickness' (R, C) best-fit thickness of the winning candidate, + 'cost' (R, C, F) dynamical cost per candidate at its best + thickness, 'phase_index' (R, C) updated phase assignment (-1 where + no candidate was refined), 'thickness_per_candidate' (R, C, F). + NaN where a candidate was not refined. """ if thicknesses_A is None: - thicknesses_A = np.arange(50.0, 1000.0, 25.0) + thicknesses_A = np.arange(50.0, 1000.0 + 1e-6, 25.0) t_grid = torch.as_tensor(thicknesses_A, dtype=torch.float64) oms = phase_map.orientation_maps @@ -453,7 +554,8 @@ def refine_thickness( for f, (i_om, _) in enumerate(cands): c = torch.nan_to_num(cost_out[..., f], nan=torch.inf) cost_phase[..., i_om] = torch.minimum(cost_phase[..., i_om], c) - phase_index = cost_phase.argmin(dim=-1) + done = torch.isfinite(cost_out).any(dim=-1) + phase_index = torch.where(done, cost_phase.argmin(dim=-1), -1) f_best = torch.nan_to_num(cost_out, nan=torch.inf).argmin(dim=-1) thickness = torch.gather(thick_out, 2, f_best[..., None]).squeeze(-1) @@ -574,8 +676,9 @@ def _bloch_solve( With fast_absorption=True the Hermitian part is diagonalized (eigh, much faster and better batched than the general complex eig) and the weak absorption enters first order: gamma_imag = diag(C^dagger U'' C)/(2 k0). - Standard for master-pattern computations; the absorptive parts of U are - a few percent of the elastic parts, so the first-order error is small. + Standard for reference (master) pattern computations; the absorptive + parts of U are a few percent of the elastic parts, so the first-order + error is small. """ nb = U.shape[0] if absorptive and fast_absorption: @@ -612,8 +715,20 @@ def _bloch_solve( def tilt_grid(semiconv_mrad: float, energy_ev: float, n_rings: int = 8): """Concentric-ring sampling of the illumination aperture. - Returns (M, 2) in-plane incident wavevectors (1/Angstroms) covering the - disk of semiangle `semiconv_mrad`, with approximately uniform density. + Parameters + ---------- + semiconv_mrad : float + Convergence semiangle in mrad. + energy_ev : float + Beam energy in eV. + n_rings : int, default=8 + Rings outside the center point; ring r has ceil(2 pi r) points. + + Returns + ------- + torch.Tensor + (M, 2) in-plane incident wavevectors (1/Angstroms) covering the + disk with approximately uniform density, the center first. """ lam = electron_wavelength_angstrom(energy_ev) alpha_k = semiconv_mrad * 1e-3 / lam @@ -633,7 +748,7 @@ def calculate_cbed( energy_ev: float = 300e3, semiconv_mrad: float = 3.0, n_rings: int = 8, - sg_max: float = 0.1, + sg_max: float = SG_MAX, k_max: float | None = None, pixel_size: float | None = None, q_max_plot: float | None = None, @@ -650,32 +765,43 @@ def calculate_cbed( Parameters ---------- crystal : Crystal - With structure factors calculated (cover 2x k_max so every - difference vector g - h has a coupling). + With structure factors calculated, and preferably + calculate_dynamical_structure_factors at this energy (absorption). orientation : torch.Tensor - Unit quaternion (4,). + Unit quaternion (4,), crystal to lab. thicknesses_A : float | array-like One or more specimen thicknesses in Angstroms. + energy_ev : float, default=300e3 + Beam energy in eV. semiconv_mrad : float, default=3.0 - Convergence semiangle. Disks overlap when it exceeds half the - smallest g spacing times the wavelength. + Convergence semiangle in mrad. Disks overlap when it exceeds half + the smallest g spacing times the wavelength. n_rings : int, default=8 Radial sampling rings across the aperture (~200 tilts at 8). - sg_max : float, default=0.1 - Excitation error cutoff for beam selection (widened automatically - by the aperture tilt range). + sg_max : float, default=SG_MAX + Excitation error cutoff (1/Angstroms) for beam selection, widened + automatically by the aperture tilt range. k_max : float | None - In-plane cutoff for included beams. + Largest |g| (1/Angstroms) of an included beam. pixel_size : float | None Detector sampling (1/Angstroms per pixel); default disk radius / 12. q_max_plot : float | None - Half-width of the detector; default covers all beams plus a disk. + Half-width of the detector (1/Angstroms); default covers all beams + plus a disk. Disk samples beyond it are dropped. + tilt_batch : int, default=64 + Incident tilts per batched eigendecomposition (memory versus + speed). Returns ------- - dict with 'pattern' ((T, H, W), squeezed to (H, W) for one thickness), - 'sampling' (1/Angstroms per pixel), 'disk_radius' (1/Angstroms), - 'thicknesses', 'hkl', 'g_xy'. + dict + 'pattern' ((T, H, H), squeezed to (H, H) for one thickness; rows + are qx, columns qy, the direct beam at the center pixel; the + intensity is averaged over the incident tilts, so it sums to the + transmitted fraction when every disk is on the detector), + 'sampling' (1/Angstroms per pixel), 'disk_radius' (1/Angstroms), + 'thicknesses', 'hkl' (nb, 3) and 'g_xy' (nb, 2) of the beams, 000 + first. """ lam = electron_wavelength_angstrom(energy_ev) alpha_k = semiconv_mrad * 1e-3 / lam @@ -709,10 +835,12 @@ def calculate_cbed( for dx in (0, 1): for dy in (0, 1): w = (wx if dx else 1 - wx) * (wy if dy else 1 - wy) - jx = np.clip(ix0 + dx, 0, H - 1) - jy = np.clip(iy0 + dy, 0, H - 1) + jx = ix0 + dx + jy = iy0 + dy + # samples beyond the detector are dropped, not piled on its edge + ok = (jx >= 0) & (jx < H) & (jy >= 0) & (jy < H) for ti in range(T): - np.add.at(pattern[ti], (jx, jy), w * inten_np[:, ti, :]) + np.add.at(pattern[ti], (jx[ok], jy[ok]), (w * inten_np[:, ti, :])[ok]) pattern /= tilts.shape[0] return { @@ -733,7 +861,7 @@ def calculate_lacbed( energy_ev: float = 300e3, semiconv_mrad: float = 10.0, n_pixels: int = 48, - sg_max: float = 0.1, + sg_max: float = SG_MAX, k_max: float | None = None, tilt_batch: int = 64, ) -> dict: @@ -743,10 +871,42 @@ def calculate_lacbed( disk on a square grid (parallax / LACBED view of a single disk, without the geometric overlap of neighboring disks). + Parameters + ---------- + crystal : Crystal + With structure factors calculated, and preferably + calculate_dynamical_structure_factors at this energy (absorption). + orientation : torch.Tensor + Unit quaternion (4,), crystal to lab. + thicknesses_A : float | array-like + One or more specimen thicknesses in Angstroms. + hkl : sequence of int + The reflection to map; (0, 0, 0) gives the bright field disk. + energy_ev : float, default=300e3 + Beam energy in eV. + semiconv_mrad : float, default=10.0 + Convergence semiangle in mrad. + n_pixels : int, default=48 + Pixels across the disk (the incident-tilt sampling). + sg_max : float, default=SG_MAX + Excitation error cutoff (1/Angstroms), widened automatically by + the aperture tilt range. + k_max : float | None + Largest |g| (1/Angstroms) of an included beam. + tilt_batch : int, default=64 + Incident tilts per batched eigendecomposition. + Returns ------- - dict with 'disk' ((T, n, n) squeezed), 'tilt_max' (1/Angstroms), - 'thicknesses'. Pixels outside the aperture are NaN. + dict + 'disk' ((T, n, n), squeezed for one thickness; rows are the y + tilt, columns the x tilt; NaN outside the aperture), 'tilt_max' + (aperture radius, 1/Angstroms), 'thicknesses'. + + Raises + ------ + ValueError + If the reflection is not among the excited beams. """ lam = electron_wavelength_angstrom(energy_ev) alpha_k = semiconv_mrad * 1e-3 / lam @@ -794,12 +954,37 @@ def calculate_cbed_library( The starting point for CBED orientation matching: all patterns share the same sampling and extent, ready for polar transformation and - correlation. One entry per orientation. + correlation. One entry per orientation, all at one thickness. + + Parameters + ---------- + crystal : Crystal + With structure factors calculated. + orientations : torch.Tensor + (N, 4) unit quaternions, crystal to lab. + thickness_A : float + Specimen thickness in Angstroms. + energy_ev : float, default=300e3 + Beam energy in eV. + semiconv_mrad : float, default=3.0 + Convergence semiangle in mrad. + k_max : float | None + Largest |g| (1/Angstroms) of an included beam. + q_max_plot : float | None + Half-width of the detector (1/Angstroms); default k_max (or half + the crystal's structure factor range) plus two disk radii. + pixel_size : float | None + Detector sampling (1/Angstroms per pixel); default disk radius / 12. + progress_bar : bool, default=True + Show a progress bar over orientations. + **kwargs + Passed to calculate_cbed() (n_rings, sg_max, tilt_batch). Returns ------- - dict with 'patterns' (N, H, W), 'quats' (N, 4), 'sampling', - 'disk_radius', 'thickness_A'. + dict + 'patterns' (N, H, H), 'quats' (N, 4), 'sampling' (1/Angstroms per + pixel), 'disk_radius' (1/Angstroms), 'thickness_A'. """ lam = electron_wavelength_angstrom(energy_ev) alpha_k = semiconv_mrad * 1e-3 / lam @@ -857,10 +1042,10 @@ def calculate_kossel( field disk alone is the LACBED view). One Bloch computation over the incident-tilt grid yields both: - - 'bright_field': the (000) beam intensity at each incident tilt -- - the deficiency (dark) line system, every line at a Bragg condition. + - 'bright_field': the (000) beam intensity at each incident tilt, the + deficiency (dark) line system, every line at a Bragg condition. - 'pattern': the full detector intensity, the incoherent sum of every - diffracted cone shifted by its g -- deficiency lines from the direct + diffracted cone shifted by its g: deficiency lines from the direct beam plus the excess (bright) lines of the diffracted beams. Line positions are exact; line profiles carry the many-beam dynamical @@ -870,28 +1055,43 @@ def calculate_kossel( Parameters ---------- crystal : Crystal - With structure factors calculated (cover 2x k_max for couplings), - and ideally calculate_dynamical_structure_factors for absorption. + With structure factors calculated, and preferably + calculate_dynamical_structure_factors at this energy (absorption). orientation : torch.Tensor - Unit quaternion (4,). + Unit quaternion (4,), crystal to lab. thicknesses_A : float | array-like One or more thicknesses in Angstroms. + energy_ev : float, default=300e3 + Beam energy in eV. semiconv_mrad : float, default=40.0 - Convergence semiangle; the pattern covers this angular radius. + Convergence semiangle in mrad; the pattern covers this angular + radius. n_pixels : int, default=192 Detector pixels across the pattern (also the tilt sampling; the 1-2 mrad dynamical line widths need ~0.5 mrad per pixel). sg_max : float, default=0.05 - Excitation error cutoff; the beam list is widened by the aperture - automatically. + Excitation error cutoff (1/Angstroms). Smaller than the SG_MAX of + the spot pattern functions: the beam list is widened by the + aperture (alpha |g|, already 0.04 1/A for |g| = 1 at 40 mrad), so + the base cutoff can be tighter without losing lines, and the + eigensolves over tens of thousands of tilts stay affordable. k_max : float | None - In-plane cutoff for included reflections. + Largest |g| (1/Angstroms) of an included reflection. + tilt_batch : int, default=64 + Incident tilts per batched eigendecomposition. + fast_absorption : bool, default=False + First-order absorption (Hermitian eigensolver, faster); see + _bloch_solve. + progress_bar : bool, default=True + Show a progress bar over tilt batches. Returns ------- - dict with 'bright_field' and 'pattern' ((T, n, n), squeezed for one - thickness; NaN / 0 outside the aperture), 'sampling' (1/Angstroms per - pixel), 'mrad_per_pixel', 'thicknesses', 'hkl'. + dict + 'bright_field' and 'pattern' ((T, n, n), squeezed for one + thickness; rows are theta_y, columns theta_x, as in + render_kossel_lines; NaN / 0 outside the aperture), 'sampling' + (1/Angstroms per pixel), 'mrad_per_pixel', 'thicknesses', 'hkl'. """ lam = electron_wavelength_angstrom(energy_ev) alpha_k = semiconv_mrad * 1e-3 / lam @@ -943,8 +1143,9 @@ def calculate_kossel( jy = iy0 + dy ok = (jx >= 0) & (jx < n_pixels) & (jy >= 0) & (jy < n_pixels) w = (wx if dx else 1 - wx) * (wy if dy else 1 - wy) + # (row, col) = (theta_y, theta_x), as the bright field for ti in range(T): - np.add.at(pattern[ti], (jx[ok], jy[ok]), (w * inten_np[:, ti, b])[ok]) + np.add.at(pattern[ti], (jy[ok], jx[ok]), (w * inten_np[:, ti, b])[ok]) pattern[:, ~m] = 0.0 return { @@ -1048,7 +1249,7 @@ def calculate_kossel_reference( pattern for every specimen orientation at once (called a master pattern in parts of the EBSD literature). Patterns for arbitrary orientations, convergence angles, and all precomputed thicknesses are then - interpolation lookups via kossel_from_reference(), microseconds instead + interpolation lookups via kossel_from_reference(), milliseconds instead of a fresh dynamical calculation. The wedge samples are expanded by the crystal's proper rotations plus @@ -1060,24 +1261,46 @@ def calculate_kossel_reference( Parameters ---------- crystal : Crystal - With structure factors calculated (cover 2x k_max), and ideally - calculate_dynamical_structure_factors for absorption. + With structure factors calculated, and preferably + calculate_dynamical_structure_factors at this energy (absorption). thicknesses_A : float | array-like - Thickness grid; all thicknesses share the eigendecompositions, so a - thickness AXIS is nearly free -- precompute the matching range here. + Thickness grid in Angstroms; all thicknesses share the + eigendecompositions, so a thickness axis is nearly free. + energy_ev : float, default=300e3 + Beam energy in eV. angle_step_mrad : float, default=1.0 - Angular sampling of the wedge. The dynamical line widths are - 1-2 mrad; 0.5 for production masters, 1-2 for quick looks. + Angular sampling of the wedge, and the pixel size of the Lambert + grid (in Lambert radius units of 1e-3). The dynamical line widths + are 1-2 mrad; 0.5 for production references, 1-2 for quick looks. + sg_max : float, default=0.05 + Excitation error cutoff (1/Angstroms) at the center of each chunk + of directions, widened by the chunk's angular radius; as in + calculate_kossel, tighter than SG_MAX because of that widening. + k_max : float | None + Largest |g| (1/Angstroms) of an included beam; None keeps every + reflection of the factor set, which is slow for large sets. The + cost grows steeply with it. theta_max_deg : float, default=90.0 - Polar cutoff of the WEDGE samples. Keep at 90 unless the wedge's + Polar cutoff of the wedge samples. Keep at 90 unless the wedge's far corners are never observed: cutting the wedge leaves coverage holes at all their symmetry equivalents. + chunk : int, default=256 + Directions per batch; each batch shares one beam list and one + coupling matrix. + fast_absorption : bool, default=True + First-order absorption (Hermitian eigensolver, several times + faster); see _bloch_solve. + progress_bar : bool, default=True + Show a progress bar over chunks. Returns ------- - dict with 'lambert' (T, n, n) master on the equal-area grid (NaN where - unsampled), 'rho_max', 'thicknesses', 'energy_ev', and the raw wedge - 'directions' / 'intensity'. + dict + 'lambert' (T, n, n) bright field on the equal-area grid of the + upper hemisphere (NaN where unsampled), 'rho_max' (Lambert radius + of the equator, sqrt(2)), 'step' (Lambert grid spacing), + 'thicknesses', 'energy_ev', 'k_max' (as given, possibly None), and + the raw wedge samples 'directions' (N, 3) and 'intensity' (N, T). """ lam = electron_wavelength_angstrom(energy_ev) k0 = 1.0 / lam @@ -1108,7 +1331,7 @@ def calculate_kossel_reference( out = torch.zeros((N, T), dtype=torch.float64) chunks = range(0, N, chunk) if progress_bar: - chunks = tqdm(chunks, desc="Kossel master") + chunks = tqdm(chunks, desc="Kossel reference") for c0 in chunks: c1 = min(c0 + chunk, N) d = dirs[c0:c1] # (B, 3) beam directions in the crystal frame @@ -1188,39 +1411,53 @@ def _lambert_lookup(lambert: np.ndarray, step: float, d_c: torch.Tensor) -> np.n def kossel_from_reference( - master: dict, + reference: dict, orientation: torch.Tensor, semiconv_mrad: float = 40.0, n_pixels: int = 192, ) -> dict: """Extract a bright field Kossel pattern from a reference pattern. - Interpolation only -- microseconds per pattern per thickness. The - detector tilt grid is mapped into the crystal frame by the orientation - and looked up on the master's Lambert grid. + Interpolation only, no Bloch calculation. The detector tilt grid is + mapped into the crystal frame by the orientation and looked up + bilinearly on the reference's Lambert grid, so the pattern has the + reference's angular resolution (angle_step_mrad), whatever n_pixels. + + Parameters + ---------- + reference : dict + From calculate_kossel_reference(). + orientation : torch.Tensor + Unit quaternion (4,), crystal to lab. + semiconv_mrad : float, default=40.0 + Convergence semiangle in mrad; the pattern covers this radius. + n_pixels : int, default=192 + Pixels across the pattern. Returns ------- - dict with 'bright_field' ((T, n, n), squeezed), 'mrad_per_pixel', - 'thicknesses'. + dict + 'bright_field' ((T, n, n), squeezed for one thickness; rows are + theta_y, columns theta_x; NaN outside the aperture), + 'mrad_per_pixel', 'thicknesses'. """ - lam = electron_wavelength_angstrom(master["energy_ev"]) + lam = electron_wavelength_angstrom(reference["energy_ev"]) d_c, inside, _, _ = _detector_directions( lam, orientation, semiconv_mrad, False, n_pixels, 1, 1 ) - bf = _lambert_lookup(master["lambert"], master["step"], d_c) + bf = _lambert_lookup(reference["lambert"], reference["step"], d_c) bf[:, ~inside.numpy()] = np.nan T = bf.shape[0] return { "bright_field": bf[0] if T == 1 else bf, - "mrad_per_pixel": 2 * semiconv_mrad / n_pixels, - "thicknesses": master["thicknesses"], + "mrad_per_pixel": 2 * semiconv_mrad / (n_pixels - 1), + "thicknesses": reference["thicknesses"], } def plot_kossel_reference( - master: dict, + reference: dict, crystal: Crystal, thickness_index: int = 0, max_index: int = 2, @@ -1250,10 +1487,12 @@ def plot_kossel_reference( Parameters ---------- - master : dict + reference : dict From calculate_kossel_reference(). crystal : Crystal The crystal the reference was computed for. + thickness_index : int, default=0 + Which thickness of the reference to show. max_index : int, default=2 Largest direction index to label. theta_max_label_deg : float, default=75.0 @@ -1282,8 +1521,9 @@ def plot_kossel_reference( beyond this contribute little, since their band edges are too far from the zone axis to read as a crossing. lines : dict | None - Line set from kossel_lines() for the crossing strength; computed - from the crystal and the reference's thickness and k_max if + Line set from kossel_lines() for the crossing strength, on the + reference's thickness grid; computed from the crystal, the chosen + thickness and the reference's k_max (1.2 1/A when it has none) if omitted. theta_circles : sequence, default=() Polar angles (degrees) at which to draw dashed circles; off by @@ -1293,8 +1533,19 @@ def plot_kossel_reference( drawn behind each label. upsample : int, default=2 Bilinear upsampling factor of the displayed pattern. + cmap : str, default="gray" + Colormap of the pattern. + axsize : tuple[float, float], default=(9.0, 9.0) + Figure size in inches when a new figure is made. filename : str | None If given, save the figure (PDF recommended). + figax : tuple | None + (fig, ax) to draw into; a new figure if None. + + Returns + ------- + fig, ax + The matplotlib figure and axes. """ import matplotlib.pyplot as plt from matplotlib import patheffects @@ -1302,8 +1553,8 @@ def plot_kossel_reference( from quantem.diffraction.crystal import miller_to_miller_bravais from quantem.diffraction.rotations import quat_to_matrix - L = master["lambert"][thickness_index] - step = master["step"] + L = reference["lambert"][thickness_index] + step = reference["step"] half = (L.shape[-1] - 1) // 2 if upsample > 1: from scipy.ndimage import zoom @@ -1348,9 +1599,11 @@ def to_px(v): if lines is None: lines = kossel_lines( crystal, - master["thicknesses"][thickness_index], - energy_ev=master["energy_ev"], - k_max=master.get("k_max", 1.2), + reference["thicknesses"][thickness_index], + energy_ev=reference["energy_ev"], + # a reference computed without a cutoff stores k_max=None; the + # line set needs a finite one + k_max=reference.get("k_max") or 1.2, ) ti = 0 else: @@ -1440,7 +1693,7 @@ def crossing_strength(dc): def kossel_polar_from_reference( - master: dict, + reference: dict, orientation: torch.Tensor, semiconv_mrad: float = 40.0, n_radial: int = 64, @@ -1450,26 +1703,40 @@ def kossel_polar_from_reference( Dictionary matching correlates over the in-plane rotation, which is a cyclic shift of the azimuthal axis in polar coordinates: sampling the - master directly at the polar detector positions avoids the intermediate + reference directly at the polar detector positions avoids the intermediate Cartesian raster and its interpolation. + Parameters + ---------- + reference : dict + From calculate_kossel_reference(). + orientation : torch.Tensor + Unit quaternion (4,), crystal to lab. + semiconv_mrad : float, default=40.0 + Outer radius of the polar grid in mrad. + n_radial : int, default=64 + Radial samples, at radii semiconv_mrad * (1 ... n_radial) / n_radial. + n_azimuthal : int, default=180 + Azimuthal samples, at 2 pi (0 ... n_azimuthal - 1) / n_azimuthal. + Returns ------- - dict with 'polar' ((T, n_azimuthal, n_radial), squeezed; rows are - azimuth, columns radius, matching the quantem polar transform - convention), 'radii_mrad', 'azimuth_rad', 'thicknesses'. + dict + 'polar' ((T, n_azimuthal, n_radial), squeezed for one thickness; + rows are azimuth, columns radius, matching the quantem polar + transform convention), 'radii_mrad', 'azimuth_rad', 'thicknesses'. """ - lam = electron_wavelength_angstrom(master["energy_ev"]) + lam = electron_wavelength_angstrom(reference["energy_ev"]) d_c, _, axes, _ = _detector_directions( lam, orientation, semiconv_mrad, True, 1, n_radial, n_azimuthal ) - out = _lambert_lookup(master["lambert"], master["step"], d_c) + out = _lambert_lookup(reference["lambert"], reference["step"], d_c) T = out.shape[0] return { "polar": out[0] if T == 1 else out, "radii_mrad": axes["radii_mrad"], "azimuth_rad": axes["azimuth_rad"], - "thicknesses": master["thicknesses"], + "thicknesses": reference["thicknesses"], } @@ -1504,9 +1771,16 @@ def kossel_lines( Parameters ---------- + crystal : Crystal + With structure factors calculated, and preferably + calculate_dynamical_structure_factors at this energy (absorption). + thicknesses_A : float | array-like + Thickness grid in Angstroms. + energy_ev : float, default=300e3 + Beam energy in eV. k_max : float, default=1.2 - Reflections with |g| up to this are included; a row keeps every - order |n| |g| <= k_max. + Reflections with |g| up to this (1/Angstroms) are included; a row + keeps every order |n| |g| <= k_max. Must be a number. u_step_mrad : float, default=0.05 Profile sampling; the line widths are 1-2 mrad. u_tail_mrad : float, default=150.0 @@ -1517,6 +1791,8 @@ def kossel_lines( min_depth : float, default=0.005 Lines (and rows) whose deepest deficit at any thickness is below this fraction of the background are dropped. + fast_absorption : bool, default=False + First-order absorption in the row calculations; see _bloch_solve. Returns ------- @@ -1527,10 +1803,21 @@ def kossel_lines( 'line_order' (K,) the order n, 'line_hkl' (K, 3), 'line_u' (K,) the cone position u = n lambda |g| / 2, 'line_depth' (K, T) the deepest deficit fraction and 'line_width_mrad' (K, T) the equivalent width - (integrated deficit over depth). + (integrated deficit over depth); also 'energy_ev' and 'thicknesses'. + + Raises + ------ + ValueError + If k_max is None, or no line reaches min_depth. """ if crystal.g_vec is None: raise RuntimeError("Run crystal.calculate_structure_factors() first.") + if k_max is None: + raise ValueError( + "kossel_lines needs a numeric k_max: every reflection up to it gets a " + "row profile, so the full factor set would be very slow" + ) + k_max = float(k_max) lam = electron_wavelength_angstrom(energy_ev) k0 = 1.0 / lam gamma_rel = relativistic_gamma(energy_ev) @@ -1609,6 +1896,11 @@ def kossel_lines( l_u.append(u_n) l_depth.append(dep) l_width.append(width) + if not g_hat_out: + raise ValueError( + f"no Kossel line reaches min_depth={min_depth} with k_max={k_max} 1/A: " + "raise k_max or lower min_depth" + ) return { "g_hat": torch.stack(g_hat_out), @@ -1655,7 +1947,7 @@ def _detector_directions( ax = torch.linspace(-alpha_k, alpha_k, n_pixels, dtype=torch.float64) ty, tx = torch.meshgrid(ax, ax, indexing="ij") inside = (tx**2 + ty**2) <= alpha_k**2 - axes = {"mrad_per_pixel": 2 * semiconv_mrad / n_pixels} + axes = {"mrad_per_pixel": 2 * semiconv_mrad / (n_pixels - 1)} tz = torch.sqrt((k0**2 - tx**2 - ty**2).clamp_min(0)) # the beam landing at detector tilt +t propagates along (t, -tz); the # line set and the reference parameterize the anti-propagation direction @@ -1684,23 +1976,42 @@ def _lines_bright_field(lines: dict, d_c: torch.Tensor) -> torch.Tensor: return lines["background"] * torch.exp(v.sum(dim=-2)) -def kossel_reference_residual(master: dict, lines: dict, crystal: Crystal) -> dict: +def kossel_reference_residual(reference: dict, lines: dict, crystal: Crystal) -> dict: """Add the many-beam residual of the line model to a reference pattern. The line model is evaluated at the reference's own wedge samples and rasterized onto the same Lambert grid, and the difference (reference - minus line model) is stored as master['residual']. It is zero away + minus line model) is stored as reference['residual']. It is zero away from the zone axes, where the rows are independent, and carries the many-beam correction of the zone axis rosettes. render_kossel_lines() adds it by lookup when given the reference. + + Parameters + ---------- + reference : dict + From calculate_kossel_reference(); modified in place. + lines : dict + From kossel_lines(), on the same thickness grid and energy. + crystal : Crystal + The crystal both were computed for. + + Returns + ------- + dict + The reference, with 'residual' (T, n, n) added. + + Raises + ------ + ValueError + If the thickness grids differ. """ - if not np.allclose(master["thicknesses"], lines["thicknesses"]): + if not np.allclose(reference["thicknesses"], lines["thicknesses"]): raise ValueError("reference and line set must share the thickness grid") - dirs = torch.as_tensor(master["directions"], dtype=torch.float64) + dirs = torch.as_tensor(reference["directions"], dtype=torch.float64) I_lines = _lines_bright_field(lines, dirs) # (N, T) - lambert_lines = _lambert_raster(crystal, dirs, I_lines, master["step"]) - master["residual"] = np.nan_to_num(master["lambert"] - lambert_lines, nan=0.0) - return master + lambert_lines = _lambert_raster(crystal, dirs, I_lines, reference["step"]) + reference["residual"] = np.nan_to_num(reference["lambert"] - lambert_lines, nan=0.0) + return reference def render_kossel_lines( @@ -1723,6 +2034,19 @@ def render_kossel_lines( Parameters ---------- + lines : dict + From kossel_lines(). + orientation : torch.Tensor + Unit quaternion (4,), crystal to lab. + semiconv_mrad : float, default=40.0 + Convergence semiangle in mrad; the pattern covers this radius. + n_pixels : int, default=256 + Pixels across a Cartesian pattern. + polar : bool, default=False + Sample on a polar grid instead (see kossel_polar_from_reference + for the grid). + n_radial, n_azimuthal : int, default=64, 180 + Polar grid size. reference : dict | None A reference pattern carrying the many-beam residual from kossel_reference_residual(). If given, the residual is added to @@ -1733,8 +2057,8 @@ def render_kossel_lines( Returns ------- - dict with 'bright_field' ((T, n, n), squeezed; NaN outside the - aperture) or, with polar=True, 'polar' ((T, n_azimuthal, n_radial), + dict with 'bright_field' ((T, n, n), squeezed; rows are theta_y, + columns theta_x; NaN outside the aperture) or, with polar=True, 'polar' ((T, n_azimuthal, n_radial), squeezed; rows are azimuth, columns radius), plus the grid axes and 'thicknesses'. """ @@ -1772,6 +2096,17 @@ def kossel_line_segments( curvature term is |g_z| alpha^2 / 2, below 0.1 mrad at 40 mrad). The end points on the aperture edge are computed exactly from the cone. + Parameters + ---------- + lines : dict + From kossel_lines(). + orientation : torch.Tensor + Unit quaternion (4,), crystal to lab. + semiconv_mrad : float, default=40.0 + Aperture radius in mrad. + thickness_index : int, default=0 + Thickness of the line set for 'depth' and 'width_mrad'. + Returns ------- dict of arrays over the K visible lines. Cartesian positions are @@ -1856,7 +2191,37 @@ def overlay_kossel_segments( segments run between their aperture-edge end points; on a polar axis (rows azimuth, columns radius) each straight line becomes the curve radius = distance / cos(azimuth - azimuth_normal), drawn from end - point to end point and split at the azimuth wrap. + point to end point and split at the azimuth wrap. The pixel registration + is that of render_kossel_lines and the reference lookups: Cartesian + pixel i at angle -semiconv + 2 semiconv i / (n_pixels - 1), polar + column j at radius semiconv (j + 1) / n_radial and row i at azimuth + 2 pi i / n_azimuthal. + + Parameters + ---------- + ax : matplotlib.axes.Axes + Axes showing the rendered pattern (imshow pixel coordinates). + segments : dict + From kossel_line_segments(). + semiconv_mrad : float + Aperture radius of the pattern in mrad. + n_pixels : int | None + Pixels across a Cartesian pattern. + polar : bool, default=False + Draw on a polar pattern instead. + n_radial, n_azimuthal : int | None + Polar grid size. + color : color, default=(0.9, 0.0, 0.0) + Line color. + width_scale : float, default=1.0 + Multiplier of the drawn line width. + min_depth : float, default=0.05 + Lines shallower than this deficit fraction are not drawn. + + Returns + ------- + matplotlib.axes.Axes + The axes. """ sel = segments["depth"] >= min_depth n_lines = int(sel.sum()) @@ -1867,6 +2232,7 @@ def overlay_kossel_segments( dep = segments["depth"][sel] wid = segments["width_mrad"][sel] if polar: + # radius r_j = semiconv (j + 1) / n_radial, azimuth phi_i = 2 pi i / n_az px_r = n_radial / semiconv_mrad px_phi = n_azimuthal / (2 * np.pi) tang = np.stack([-nrm[:, 1], nrm[:, 0]], axis=1) @@ -1879,24 +2245,25 @@ def overlay_kossel_segments( jumps = np.abs(np.diff(phi)) > np.pi phi = np.ma.array(phi, mask=np.r_[False, jumps]) ax.plot( - r * px_r - 0.5, - phi * px_phi - 0.5, + r * px_r - 1.0, + phi * px_phi, color=color, lw=wid[k] * px_r * width_scale, alpha=float(dep[k]), solid_capstyle="butt", ) else: - px = n_pixels / (2 * semiconv_mrad) + # linspace(-semiconv, semiconv, n_pixels): pixel centers at the ends + px = (n_pixels - 1) / (2 * semiconv_mrad) for k in range(n_lines): ax.plot( [ - (start[k, 1] + semiconv_mrad) * px - 0.5, - (stop[k, 1] + semiconv_mrad) * px - 0.5, + (start[k, 1] + semiconv_mrad) * px, + (stop[k, 1] + semiconv_mrad) * px, ], [ - (start[k, 0] + semiconv_mrad) * px - 0.5, - (stop[k, 0] + semiconv_mrad) * px - 0.5, + (start[k, 0] + semiconv_mrad) * px, + (stop[k, 0] + semiconv_mrad) * px, ], color=color, lw=wid[k] * px * width_scale, @@ -1949,12 +2316,50 @@ def average_bloch_fourier( The absorption is the full complex matrix (there is no first-order variant here). Cost: a sparse block matrix of size (2 n_harmonics + 1) x nb per trial tilt and one Krylov exponential action over the - thickness grid; see the benchmark in the tests for how it compares - with the batched eigensolves of the quadrature, which reuse one - eigendecomposition for every thickness. + thickness grid. + + Status: an alternative to the azimuthal quadrature of + illumination_nodes(), verified against it in the tests but not used by + the refinement functions of this module, which average batched + eigensolves instead (one eigendecomposition serves every thickness). + + Parameters + ---------- + crystal : Crystal + With structure factors calculated. + orientation : torch.Tensor + Unit quaternion (4,), crystal to lab. + trial_tilts : torch.Tensor + (M, 2) ring centers as in-plane incident wavevectors (1/Angstroms). + thicknesses_A : float | array-like + Thickness grid in Angstroms; a uniform grid is propagated in one + pass. + energy_ev : float + Beam energy in eV. + precession_deg : float + Precession semi-angle in degrees. + sg_max : float, default=SG_MAX + Excitation error cutoff (1/Angstroms) of the beam list. + k_max : float | None + Largest |g| (1/Angstroms) of a beam. + deform : torch.Tensor | None + (3, 3) deformation of the lab-frame reciprocal vectors. + beams : torch.Tensor | None + Explicit beam list (nb, 3), 000 first; selected here if None. + n_harmonics : int, default=48 + Azimuthal modes kept, |n| <= n_harmonics. + n_geometry : int, default=128 + Azimuths sampled for the coefficients of a displaced ring. + n_matrix_harmonics : int | None + Fourier coefficients of the displaced ring kept; default + n_harmonics // 3. - Returns (intensities (M_trial, T, nb) with the direct beam first, - g_xy (nb, 2)). + Returns + ------- + intensities : torch.Tensor + (M, T, nb) ring-averaged intensities, direct beam first. + g_xy : torch.Tensor + (nb, 2) in-plane positions of the beams (1/Angstroms). """ from scipy.sparse import csr_matrix, diags, kron from scipy.sparse.linalg import expm_multiply @@ -2042,8 +2447,7 @@ def illumination_nodes( maped_tilts_deg=None, maped_weights=None, ) -> tuple[torch.Tensor, torch.Tensor]: - """Incident beam tilts (M, 2) in 1/Angstroms and their weights (M,) - whose Bloch intensities are averaged to model one measured pattern. + """Incident beam tilts and weights that model one measured pattern. Precession is a ring of radius k0 sin(theta_p) sampled uniformly in azimuth (the Gauss-Chebyshev quadrature of the ring integral); the @@ -2054,6 +2458,30 @@ def illumination_nodes( MAPED is an explicit tilt list with exposure weights. Ring and disk combine as a product measure. Zero tilt with unit weight when none apply. + + Parameters + ---------- + energy_ev : float + Beam energy in eV. + precession_deg : float, default=0.0 + Precession semi-angle in degrees; 0 for none. + n_precession : int, default=32 + Azimuthal samples on the precession ring. + semiconv_mrad : float, default=0.0 + Convergence semiangle in mrad; 0 for a parallel beam. + n_disk_radial, n_disk_azimuthal : int, default=4, 16 + Gauss-Legendre radii and azimuths of the convergence disk. + maped_tilts_deg : array-like | None + (M, 2) explicit beam tilts in degrees (MAPED), replacing the ring. + maped_weights : array-like | None + (M,) exposure weights of the MAPED tilts; equal if None. + + Returns + ------- + tilts : torch.Tensor + (M, 2) in-plane incident wavevectors (1/Angstroms). + weights : torch.Tensor + (M,) weights summing to one. """ lam = electron_wavelength_angstrom(energy_ev) k0 = 1.0 / lam @@ -2157,6 +2585,46 @@ def _fit_deformation(sq, qxy, w_exp, delta): return S, wz, pair +# matched orientations of two positions closer than this (degrees) belong to +# one grain for the neighbor rescue: above the error of kinematical matching +# (a few tenths of a degree), below typical grain boundary angles +_RESCUE_SAME_GRAIN_DEG = 2.0 + + +def _closest_symmetry_variant(q: torch.Tensor, ref: torch.Tensor, sym_quats) -> torch.Tensor: + """The symmetry equivalent q * s of orientation q closest to ref.""" + if sym_quats is None: + return q + from quantem.diffraction.rotations import qmult + + variants = qmult(q[None], torch.as_tensor(sym_quats, dtype=q.dtype)) # (S, 4) + return variants[int((variants @ ref).abs().argmax())] + + +def _tilt_twist(dq: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """Split a lab-frame rotation into dq = tilt * twist: the twist is a + rotation about the beam (z), the tilt one about an in-plane axis. + + Returns the tilt as its rotation vector (wx, wy) in radians, and the + twist quaternion.""" + from quantem.diffraction.rotations import qconj, qmult + + dq = dq / torch.linalg.norm(dq) + n = float(torch.hypot(dq[0], dq[3])) + if n < 1e-12: + twist = torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=dq.dtype) + else: + twist = torch.stack([dq[0], torch.zeros_like(dq[0]), torch.zeros_like(dq[0]), dq[3]]) / n + swing = qmult(dq, qconj(twist)) + if swing[0] < 0: + swing = -swing + sin_half = float(torch.linalg.norm(swing[1:3])) + if sin_half < 1e-15: + return torch.zeros(2, dtype=dq.dtype), twist + angle = 2 * np.arctan2(sin_half, float(swing[0])) + return swing[1:3] / sin_half * angle, twist + + def refine_dynamical( phase_map, thicknesses_A: np.ndarray | None = None, @@ -2228,8 +2696,8 @@ def refine_dynamical( phase_map : PhaseMap A fitted PhaseMap (fit() has been run). thicknesses_A : np.ndarray | None - Thickness grid in Angstroms; default 50 ... 2000 in 25 A steps (the - thickness axis is free: all thicknesses come from one + Thickness grid in Angstroms; default 50 to 2000 in 25 A steps (the + thickness axis is nearly free: all thicknesses come from one eigendecomposition). tilt_stages : sequence of (half_range_deg, step_deg) Successive tilt grids, each centered on the previous optimum. @@ -2248,13 +2716,29 @@ def refine_dynamical( Convergence semiangle (inherited); the intensities are averaged over the disk with n_disk_radial Gauss-Legendre radii times n_disk_azimuthal azimuths (64 nodes by default, times the ring). + n_disk_radial, n_disk_azimuthal : int, default=4, 16 + Convergence disk sampling, see semiconv_mrad. maped_tilts_deg : array-like | None Explicit (M, 2) beam tilt list (degrees) for MAPED, overriding precession. - min_sim_intensity_rel : float, default=0.02 + pair_distance : float | None + Largest distance (1/Angstroms) at which a simulated and a measured + peak are paired; inherited from the phase fit. + power_intensity : float | None + Intensities are compared as I ** power_intensity; inherited from + the phase fit. + min_sim_intensity_rel : float | None Unpaired simulated beams weaker than this fraction of the strongest simulated beam do not count against a candidate (the - detector would not have seen them). + detector would not have seen them). Inherited from the phase fit, + else MIN_SIM_INTENSITY_REL (0.02). + sg_max : float, default=SG_MAX + Excitation error cutoff (1/Angstroms) of the Bloch beam list, + widened by the tilt search range and the illumination. + k_max : float | None + Largest |g| (1/Angstroms) of a beam; None keeps every reflection + within the cutoff. Recorded in the metadata, so the image + refinement uses the same beam set. min_number_peaks : int | None Positions with fewer measured peaks, direct beam included, are skipped. None inherits the minimum of the phase fit, itself the @@ -2288,17 +2772,28 @@ def refine_dynamical( orientation to well inside the fine stages, so this removes about half of the eigensolves; the in-plane deformation and rotation are still fit from the position's own peaks, and the final evaluation - is unchanged. + is unchanged. The reported tilt and zero-tilt cost still refer to + the position's own matched orientation. + neighbor_rescue : bool, default=True + Second pass: positions whose winning solution differs from a + 4-neighbor in the same grain (same crystal, matched orientations + within 2 degrees; the nearest refined position + within two steps, so a mask of every second position works too) by + more than rescue_thickness_A in thickness or rescue_tilt_deg in + orientation are refined again from that neighbor's solution, and + the lower cost is kept. Repairs isolated wrong basins (thickness + aliases, tilt minima at a grid edge). + rescue_thickness_A : float, default=100.0 + Thickness difference (Angstroms) to a neighbor that triggers a + rescue. + rescue_tilt_deg : float, default=0.05 + Misorientation (degrees) between the refined orientations of a + position and a neighbor that triggers a rescue. Neighbors in one + grain differ by the true orientation gradient, so keep it above + that. rescue_max_starts : int, default=2 Neighbor solutions tried per rescued position, lowest cost first, skipping neighbors whose solution repeats one already tried. - neighbor_rescue : bool, default=True - Second pass: positions whose winning solution differs from a - 4-neighbor of the same crystal (the nearest refined position within - two steps, so a mask of every second position works too) by more - than rescue_thickness_A or rescue_tilt_deg are refined again from - that neighbor's solution, and the lower cost is kept. Repairs isolated wrong basins - (thickness aliases, tilt minima at a grid edge). num_workers : int | None Threads refining positions side by side; None uses every core. The Bloch eigensolves are too small to spread over cores on their @@ -2306,26 +2801,42 @@ def refine_dynamical( out in contiguous raster-order blocks and warm starts stay inside a block, so the result depends on the number of blocks, never on which thread finishes first. + progress_bar : bool, default=True + Show progress bars over positions and rescues. Returns ------- - dict with 'thickness' (R, C) at the winning candidate, 'tilt_deg' - (R, C, 2) its tilt correction, 'quats' (R, C, F, 4) refined - orientations, 'deformation' (R, C, F, 2, 2) symmetric in-plane - deformation A of the tilted cell in the calibrated frame (measured - reciprocal positions = A x ideal), 'cost' (R, C, F), 'cost_zero_tilt' - (R, C, F) the cost at the matched orientation (its difference to - 'cost' is the gain of the tilt search; a small gain means the - intensities do not constrain the tilt), 'quats_base' (R, C, F, 4) the - matched orientation with the in-plane rotation folded in, from which - 'tilt_deg' leads to 'quats', 'warm_started' (R, C, F) and 'rescued' - (R, C) flags, 'phase_index' (R, C), - 'candidate' (R, C), 'thickness_per_candidate' (R, C, F). + dict + Per position (R, C) at the winning candidate: 'thickness', + 'thickness_contrast' (range of the final cost over the thickness + grid; a small value means the thickness is not determined) and + 'tilt_deg' (R, C, 2), the tilt about the lab x and y axes (degrees) + from 'quats_base' to 'quats'. Per candidate (R, C, F): 'quats' + (..., 4) refined orientations; 'quats_base' (..., 4) the matched + orientation with the in-plane rotation of the refinement folded in + (a symmetry equivalent of it closest to the solution), so quats = + tilt x quats_base whether or not the position was warm started or + rescued; 'deformation' (..., 2, 2) symmetric in-plane deformation + A of the tilted cell in the calibrated frame (measured reciprocal + positions = A x ideal); 'cost'; 'cost_zero_tilt', the best cost + over thickness at 'quats_base', evaluated like the final cost (its + difference to 'cost' is the gain of the tilt search; a small gain + means the intensities do not constrain the tilt); + 'thickness_per_candidate'; 'warm_started' flags. Also 'rescued' + (R, C) flags, 'phase_index' (R, C) the winning crystal and + 'candidate' (R, C) the winning candidate (both -1 where nothing + was refined), and 'metadata', the resolved parameters. Values are + NaN where a candidate was not refined. """ - from quantem.diffraction.rotations import misorientation_angle_deg, qmult, quat_from_axis_angle + from quantem.diffraction.rotations import ( + misorientation_angle_deg, + qconj, + qmult, + quat_from_axis_angle, + ) if thicknesses_A is None: - thicknesses_A = np.arange(50.0, 2000.0, 25.0) + thicknesses_A = np.arange(50.0, 2000.0 + 1e-6, 25.0) t_grid = torch.as_tensor(thicknesses_A, dtype=torch.float64) T = t_grid.shape[0] @@ -2431,10 +2942,13 @@ def stage_grid(center, half, step): warm_out = torch.zeros((R, C, F), dtype=torch.bool) rescued_out = torch.zeros((R, C), dtype=torch.bool) - def refine_from(crystal, q_start, qxy, im, w_exp, stages): + def refine_from(crystal, q_start, q_match, qxy, im, w_exp, stages): """Search from q_start: in-plane deformation and rotation from the positions, one beam list, the tilt stages, and the exact final - evaluation. Returns None or a dict with the solution.""" + evaluation. q_match is the kinematically matched orientation of the + position, the reference of the reported tilt and of the zero-tilt + cost (q_start differs from it on a warm start or a rescue). Returns + None or a dict with the solution.""" q0 = q_start S = None deform3 = None @@ -2458,23 +2972,57 @@ def refine_from(crystal, q_start, qxy, im, w_exp, stages): q0 = qmult(dqz, q0) deform3 = torch.eye(3, dtype=torch.float64) deform3[:2, :2] = S + # the matched orientation with the in-plane rotation of q0: the + # base the reported tilt is measured from (equal to q0 on a cold + # start), and its tilt away from q0 + q_m = _closest_symmetry_variant(q_match, q0, crystal.sym_quats) + base_tilt, twist = _tilt_twist(qmult(q0, qconj(q_m))) + q_base = qmult(twist, q_m) + offset = float(torch.linalg.norm(base_tilt)) # one beam list for the whole search of this candidate: every - # trial center, the illumination and the deformation are inside - # its selection, so all stages compare the same truncated system + # trial center, the base, the illumination and the deformation are + # inside its selection, so all stages compare the same truncated + # system beam_list = select_dynamical_beams( crystal, q0, energy_ev, - np.deg2rad(stages[0][0]) * np.sqrt(2) + alpha_ill, + np.deg2rad(stages[0][0]) * np.sqrt(2) + alpha_ill + offset, sg_max, k_max, deform3, ) if beam_list.shape[0] < 2: return None + + def exact_cost(q): + # full illumination and exact absorption at one orientation + inten, g_xy, _ = _cbed_amplitudes( + crystal, + q, + ring, + t_grid, + energy_ev, + sg_max, + k_max, + tilt_batch=max(64, Mr * 8), + progress_bar=False, + fast_absorption=False, + deform=deform3, + beams=beam_list, + ) + inten = (inten * w_ring[:, None, None]).sum(dim=0, keepdim=True) + cost, _, _, _ = _dynamical_cost( + inten[:, :, 1:], g_xy[1:], qxy, im, delta, power_intensity, min_sim_intensity_rel + ) + return cost + + # untilted reference: the best thickness at the matched orientation, + # for the gain the tilt search achieves + cost_base = exact_cost(q_base) + cost0 = float("nan") if cost_base is None else float(cost_base[0].min()) center = torch.zeros(2, dtype=torch.float64) best = None - cost0 = float("nan") n_stages = len(stages) for i_stage, (half, step) in enumerate(stages): half = np.deg2rad(half) @@ -2516,11 +3064,6 @@ def refine_from(crystal, q_start, qxy, im, w_exp, stages): flat = int(cost.argmin()) m_best, t_best = flat // T, flat % T i_b, j_b = m_best // n, m_best % n - if i_stage == 0: - # untilted reference: the best thickness at the start - # orientation, for the gain the tilt search achieves - m0 = int(((w_grid**2).sum(1)).argmin()) - cost0 = float(cost[m0].min()) cost_t = cost[:, t_best].reshape(n, n) wx, wy = float(w_grid[m_best, 0]), float(w_grid[m_best, 1]) if 0 < i_b < n - 1: @@ -2549,24 +3092,7 @@ def refine_from(crystal, q_start, qxy, im, w_exp, stages): # stored cost and thickness belong to the stored orientation at full # accuracy (the beam-offset search geometry is paraxially, not # exactly, equivalent to it) - inten, g_xy, _ = _cbed_amplitudes( - crystal, - q, - ring, - t_grid, - energy_ev, - sg_max, - k_max, - tilt_batch=max(64, Mr * 8), - progress_bar=False, - fast_absorption=False, - deform=deform3, - beams=beam_list, - ) - inten = (inten * w_ring[:, None, None]).sum(dim=0, keepdim=True) - cost, _, _, _ = _dynamical_cost( - inten[:, :, 1:], g_xy[1:], qxy, im, delta, power_intensity, min_sim_intensity_rel - ) + cost = exact_cost(q) t_contrast = float("nan") if cost is not None: t_best = int(cost[0].argmin()) @@ -2575,8 +3101,22 @@ def refine_from(crystal, q_start, qxy, im, w_exp, stages): # orientation: a flat curve means the thickness is not # determined by these intensities (precession, few beams) t_contrast = float(cost[0].max() - cost[0].min()) + # the reported tilt and base split the rotation from the matched + # orientation exactly, q = tilt x base; on a cold start they are the + # search's own (wx, wy) and q0, on a warm start the base differs + # from the zero-tilt one above only at second order in the tilt + tilt, twist = _tilt_twist(qmult(q, qconj(q_m))) + q_base = qmult(twist, q_m) return dict( - cost=c_best, t=t_fit, wx=wx, wy=wy, q=q, q0=q0, S=S, cost0=cost0, t_contrast=t_contrast + cost=c_best, + t=t_fit, + wx=float(tilt[0]), + wy=float(tilt[1]), + q=q, + q_base=q_base, + S=S, + cost0=cost0, + t_contrast=t_contrast, ) def store(rx, ry, f, sol): @@ -2585,7 +3125,7 @@ def store(rx, ry, f, sol): tcontrast_out[rx, ry, f] = sol.get("t_contrast", float("nan")) tilt_out[rx, ry, f, 0] = sol["wx"] tilt_out[rx, ry, f, 1] = sol["wy"] - quat_base[rx, ry, f] = sol["q0"] + quat_base[rx, ry, f] = sol["q_base"] quat_out[rx, ry, f] = sol["q"] if sol["S"] is not None: deform_out[rx, ry, f] = sol["S"] @@ -2651,7 +3191,7 @@ def refine_position(rx, ry, block): stages = tilt_stages[1:] warm_out[rx, ry, f] = True break - sol = refine_from(om.crystal, q_start, qxy, im, w_exp, stages) + sol = refine_from(om.crystal, q_start, om.quats[rx, ry, m], qxy, im, w_exp, stages) if sol is None: continue store(rx, ry, f, sol) @@ -2675,6 +3215,10 @@ def run(job): for job in jobs: run(job) else: + # one intra-op thread per worker: the worker threads already + # fill the cores, and torch's own pool on top of them would + # oversubscribe. The setting is process-global, so the previous + # value is restored afterwards. n_threads = torch.get_num_threads() torch.set_num_threads(1) try: @@ -2709,6 +3253,18 @@ def run(job): done = torch.isfinite(cost_out).any(dim=-1) rescue_list = [] + def miso(r0, c0, f0, r1, c1, f1): + # misorientation (degrees) of two refined solutions of the same + # crystal: the tilt corrections have different bases (the + # matched orientations of the two positions), the solutions not + return float( + misorientation_angle_deg( + quat_out[r0, c0, f0][None], + quat_out[r1, c1, f1][None], + oms[cands[f0][0]].crystal.sym_quats, + )[0] + ) + def nearest_done(rx, ry, dr, dc): # the refined position one step away, or two on a sparse mask for k in (1, 2): @@ -2731,11 +3287,20 @@ def nearest_done(rx, ry, dr, dc): fn = int(f_win[nr, nc]) if cands[fn][0] != i_om: continue + # same grain only: a start from another grain is no rescue, + # and its tilt from the matched orientation would widen the + # beam list without bound + if ( + misorientation_angle_deg( + oms[i_om].quats[rx, ry, cands[f][1]][None], + oms[i_om].quats[nr, nc, cands[fn][1]][None], + oms[i_om].crystal.sym_quats, + )[0] + >= _RESCUE_SAME_GRAIN_DEG + ): + continue dt = abs(float(thick_out[nr, nc, fn]) - float(thick_out[rx, ry, f])) - dtilt = float( - torch.rad2deg(torch.linalg.norm(tilt_out[nr, nc, fn] - tilt_out[rx, ry, f])) - ) - if dt > rescue_thickness_A or dtilt > rescue_tilt_deg: + if dt > rescue_thickness_A or miso(nr, nc, fn, rx, ry, f) > rescue_tilt_deg: starts.append((float(cost_out[nr, nc, fn]), nr, nc, fn)) if starts: # lowest-cost neighbors first, one start per distinct @@ -2749,12 +3314,7 @@ def nearest_done(rx, ry, dr, dc): if ( abs(float(thick_out[nr, nc, fn]) - float(thick_out[kr, kc, kf])) <= rescue_thickness_A - and float( - torch.rad2deg( - torch.linalg.norm(tilt_out[nr, nc, fn] - tilt_out[kr, kc, kf]) - ) - ) - <= rescue_tilt_deg + and miso(nr, nc, fn, kr, kc, kf) <= rescue_tilt_deg ): dup = True break @@ -2774,14 +3334,13 @@ def rescue(item): if pk is None: return qxy, im, w_exp = pk - crystal = oms[cands[f][0]].crystal + i_om, m = cands[f] + crystal = oms[i_om].crystal + q_match = oms[i_om].quats[rx, ry, m] for q_n in starts: - sol = refine_from(crystal, q_n, qxy, im, w_exp, tilt_stages[1:]) + sol = refine_from(crystal, q_n, q_match, qxy, im, w_exp, tilt_stages[1:]) if sol is not None and sol["cost"] < float(cost_out[rx, ry, f]) - 1e-9: - cost0_keep = float(cost0_out[rx, ry, f]) store(rx, ry, f, sol) - if np.isfinite(cost0_keep): - cost0_out[rx, ry, f] = cost0_keep rescued_out[rx, ry] = True n_jobs = 1 if n_workers == 1 else 4 * n_workers @@ -2796,13 +3355,15 @@ def rescue(item): cost_phase = torch.full((R, C, n_maps), torch.inf, dtype=torch.float64) for f, (i_om, _) in enumerate(cands): cost_phase[..., i_om] = torch.minimum(cost_phase[..., i_om], cost_f[..., f]) - phase_index = cost_phase.argmin(dim=-1) + done = torch.isfinite(cost_out).any(dim=-1) + phase_index = torch.where(done, cost_phase.argmin(dim=-1), -1) f_best = cost_f.argmin(dim=-1) thickness = torch.gather(thick_out, 2, f_best[..., None]).squeeze(-1) thickness_contrast = torch.gather(tcontrast_out, 2, f_best[..., None]).squeeze(-1) tilt_deg = torch.rad2deg( torch.gather(tilt_out, 2, f_best[..., None, None].expand(R, C, 1, 2)).squeeze(2) ) + tilt_deg[~done] = torch.nan if update_orientations: for f, (i_om, m) in enumerate(cands): @@ -2821,7 +3382,7 @@ def rescue(item): "warm_started": warm_out, "rescued": rescued_out, "phase_index": phase_index, - "candidate": f_best, + "candidate": torch.where(done, f_best, -1), "thickness_per_candidate": thick_out, "metadata": used, } @@ -2835,19 +3396,36 @@ def dynamical_maps( ) -> dict: """Maps of the winning candidate of a refine_dynamical() result. - Returns 'thickness' (A), 'tilt_deg' (magnitude of the tilt correction), - 'gain' (cost at the start orientation minus the final cost), 'cost', - 'phase_index', 'mask' (positions refined, and of the given crystal when - crystal_index is set), 'quats' (R, C, 4), 'deformation' (R, C, 2, 2) - 'thickness_contrast' (range of the cost over the thickness grid at the - refined orientation) and 'strain', the crystal-frame strain components - of strain_crystal_frame(); unrefined or masked positions are NaN. The - thickness is NaN where the contrast is below min_thickness_contrast: - a flat cost curve, typical of precessed data with few beams, does not - determine the thickness and the grid minimum there is not a measurement. + Parameters + ---------- + result : dict + From refine_dynamical(). + phase_map : PhaseMap + The PhaseMap that was refined (for the candidate list). + crystal_index : int | None + Keep only positions won by this crystal; None keeps all. + min_thickness_contrast : float, default=0.02 + The thickness is NaN where the thickness contrast is below this: a + flat cost curve, typical of precessed data with few beams, does not + determine the thickness and the grid minimum there is not a + measurement. + + Returns + ------- + dict + (R, C) maps 'thickness' (Angstroms), 'tilt_deg' (magnitude of the + tilt from the matched orientation, degrees), 'gain' (cost at the + matched orientation minus the final cost), 'cost', + 'thickness_contrast', 'phase_index' (-1 outside the mask), 'mask' + (positions refined, and of the given crystal when crystal_index is + set), 'quats' (R, C, 4), 'deformation' (R, C, 2, 2) and 'strain', + the crystal-frame strain components of strain_crystal_frame(). + Positions outside the mask are NaN. """ cand = result["candidate"] R, C = cand.shape + refined = cand >= 0 + cand = cand.clamp_min(0) idx4 = cand[..., None, None] quats = torch.gather(result["quats"], 2, idx4.expand(R, C, 1, 4)).squeeze(2) deform = torch.gather( @@ -2858,7 +3436,7 @@ def dynamical_maps( tcon = result.get("thickness_contrast") if tcon is None: tcon = torch.full_like(cost, torch.nan) - mask = torch.isfinite(cost) + mask = refined & torch.isfinite(cost) if crystal_index is not None: i_om = torch.tensor([c[0] for c in phase_map.candidates]) mask &= i_om[cand] == crystal_index @@ -2872,10 +3450,10 @@ def dynamical_maps( "gain": torch.where(mask, cost0 - cost, nan), "thickness_contrast": torch.where(mask, tcon, nan), "cost": torch.where(mask, cost, nan), - "phase_index": result["phase_index"], + "phase_index": torch.where(mask, result["phase_index"], -1), "mask": mask, - "quats": quats, - "deformation": deform, + "quats": torch.where(mask[..., None], quats, torch.nan), + "deformation": torch.where(mask[..., None, None], deform, torch.nan), "strain": {k: torch.where(mask, v, nan) for k, v in strain.items() if k != "eps_crystal"}, } return out @@ -2890,7 +3468,28 @@ def plot_dynamical_maps( axsize: tuple[float, float] = (4.0, 4.0), ): """Thickness, tilt correction, gain of the tilt search and final cost - of a dynamical refinement, from dynamical_maps().""" + of a dynamical refinement. + + Parameters + ---------- + maps : dict + From dynamical_maps(). + scalebar : dict | None + Passed to show_2d. + thickness_range_A : tuple[float, float], default=(0.0, 2000.0) + Color range of the thickness map (Angstroms). + tilt_range_deg : tuple[float, float], default=(0.0, 0.3) + Color range of the tilt map (degrees). + gain_range : tuple[float, float], default=(0.0, 0.05) + Color range of the gain map. + axsize : tuple[float, float], default=(4.0, 4.0) + Size of each panel in inches. + + Returns + ------- + fig, axs + From show_2d. NaN positions are shown as zero. + """ from quantem.core.visualization import show_2d imgs = [ @@ -2984,6 +3583,27 @@ def plot_strain_crystal_frame( the ab, ac and bc shears below, in percent, masked where the fit is not trusted. Components with a c (beam-direction) index are the rotated in-plane measurement only; see strain_crystal_frame(). + + Parameters + ---------- + strain : dict + (R, C) arrays 'aa', 'bb', 'cc', 'ab', 'ac', 'bc', as returned by + strain_crystal_frame() or dynamical_maps()['strain']. + mask : np.ndarray | None + (R, C) weights (0 hides a position); None shows all. + strain_range_percent : tuple[float, float], default=(-2.0, 2.0) + Color range in percent. + scalebar : dict | None + Passed to show_2d. + axsize : tuple[float, float], default=(4.0, 4.0) + Size of each panel in inches. + cmap : str, default="RdBu_r" + Colormap. + + Returns + ------- + fig, axs + From show_2d. """ from quantem.core.visualization import show_2d @@ -3035,9 +3655,29 @@ def render_disks( disk_radius_px: float, edge_px: float, ) -> torch.Tensor: - """Sum of soft-edged disks: (..., ny, nx) images for intensities (..., N) - at centers (N, 2) [row, col]. The edge is a logistic of width edge_px - (the disk profile of a defocused or blurred aperture).""" + """Sum of soft-edged disks on a pixel grid. + + The edge is a logistic of width edge_px (the disk profile of a + defocused or blurred aperture). + + Parameters + ---------- + centers_px : torch.Tensor + (N, 2) disk centers in pixels, (row, col). + intensities : torch.Tensor + (..., N) disk intensities; leading dimensions give a stack. + shape : tuple[int, int] + Image shape (ny, nx). + disk_radius_px : float + Disk radius in pixels (the logistic's half point). + edge_px : float + Edge width in pixels. + + Returns + ------- + torch.Tensor + (..., ny, nx) images. + """ ny, nx = shape rows = torch.arange(ny, dtype=torch.float64) cols = torch.arange(nx, dtype=torch.float64) @@ -3070,12 +3710,62 @@ def render_pattern_image( tilt_weights: torch.Tensor | None = None, beams: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: - """Dynamical diffraction pattern images: Bloch intensities (averaged over - the precession / convergence tilt set `tilts`) rendered as disks on the - detector grid, for every trial orientation tilt and every thickness. + """Dynamical diffraction pattern images on the detector grid. + + Bloch intensities, averaged over the precession / convergence tilt set + `tilts`, rendered as disks for every trial orientation tilt and every + thickness. + + Parameters + ---------- + crystal : Crystal + With structure factors calculated. + orientation : torch.Tensor + Unit quaternion (4,), crystal to lab. + thicknesses_A : float | array-like + Thicknesses in Angstroms. + energy_ev : float + Beam energy in eV. + shape : tuple[int, int] + Detector shape (ny, nx). + origin_rc : array-like + Direct beam position (row, col) in pixels. + pixel_size : float + Detector sampling (1/Angstroms per pixel). + rotation_ccw_deg : float, default=0.0 + Scan-to-detector rotation of the calibration, undone here. + ellipse : sequence | None + Elliptic distortion (e11, e12) of the calibration, undone here. + deform : torch.Tensor | None + (3, 3) deformation of the lab-frame reciprocal vectors. + disk_radius_px, edge_px : float, default=3.0, 1.0 + Disk shape, see render_disks. + tilts : torch.Tensor | None + (Mr, 2) illumination tilts (1/Angstroms), e.g. from + illumination_nodes(); a single untilted beam if None. + trial_tilts : torch.Tensor | None + (M, 2) crystal tilts about the lab x and y axes (radians); none if + None. + sg_max : float, default=SG_MAX + Excitation error cutoff (1/Angstroms) of the beam list. + k_max : float | None + Largest |g| (1/Angstroms) of a beam. + fast_absorption : bool, default=True + First-order absorption; see _bloch_solve. + tilt_weights : torch.Tensor | None + (Mr,) weights of the illumination tilts; equal if None. + beams : torch.Tensor | None + Explicit beam list (nb, 3), 000 first. - Returns (images (M, T, ny, nx), centers_px (N, 2), intensities - (M, T, N)); M is the number of trial tilts (1 when None).""" + Returns + ------- + images : torch.Tensor + (M, T, ny, nx), M the number of trial tilts (1 when None). + centers_px : torch.Tensor + (nb, 2) disk centers (row, col), direct beam first. + intensities : torch.Tensor + (M, T, nb) illumination-averaged intensities. + """ t_grid = torch.atleast_1d(torch.as_tensor(thicknesses_A, dtype=torch.float64)) lam = electron_wavelength_angstrom(energy_ev) k0 = 1.0 / lam @@ -3190,9 +3880,9 @@ def fit_disk_shape( r_max_px: float | None = None, exclude_direct_px: float | None = None, background: str = "constant", - sg_max: float = SG_MAX, + sg_max: float | None = None, k_max: float | None = None, - fast_absorption: bool = True, + fast_absorption: bool | None = None, progress_bar: bool = True, ) -> dict: """Global disk radius and edge width from the best-fit patterns. @@ -3204,6 +3894,59 @@ def fit_disk_shape( compared with the measured image over a grid of (radius, edge), and the pair minimizing the summed image cost is returned for refine_dynamical_image() to use. + + The illumination, intensity power and beam set (sg_max, k_max, + fast_absorption) default to those of the refine_dynamical() result. + + Parameters + ---------- + dataset : Dataset4dstem + Measured patterns; anything with `.array` (R, C, ny, nx) and + `.shape`. + phase_map : PhaseMap + The refined PhaseMap. + result : dict + From refine_dynamical(). + origins : np.ndarray + (R, C, 2) direct beam positions (row, col) in pixels. + pixel_size : float + Detector sampling (1/Angstroms per pixel). + rotation_ccw_deg : float, default=0.0 + Scan-to-detector rotation of the calibration. + ellipse : sequence | None + Elliptic distortion (e11, e12) of the calibration. + positions : list of (int, int) | None + Positions to fit; None takes the n_positions with the lowest + dynamical cost. + n_positions : int, default=20 + Number of positions when positions is None. + radii_px : array-like | None + Disk radii to try (pixels); default 1.5 to 6 in steps of 0.5. + edges_px : array-like | None + Edge widths to try (pixels); default 0.5, 0.75, 1, 1.5, 2. + power_intensity : float | None + Images are compared as I ** power_intensity; inherited. + r_max_px : float | None + Ignore pixels farther than this from the origin; None uses all. + exclude_direct_px : float | None + Ignore pixels within this distance of the origin; default 1.5 + times the largest radius tried. + background : {"constant", "radial"}, default="constant" + Background model of the image cost: a constant, or a quadratic in + the distance from the direct beam. + sg_max, k_max : float | None + Beam set; inherited from the result's metadata (SG_MAX if absent). + fast_absorption : bool | None + First-order absorption; inherited (True if absent). + progress_bar : bool, default=True + Show a progress bar over positions. + + Returns + ------- + dict + 'disk_radius_px', 'edge_px' the best pair, 'cost' (n_radii, + n_edges) the summed image cost, 'radii_px', 'edges_px', + 'positions'. """ oms = phase_map.orientation_maps cands = phase_map.candidates @@ -3212,6 +3955,10 @@ def fit_disk_shape( power_intensity = float( resolve(power_intensity, "power_intensity", md, default=POWER_INTENSITY) ) + # the beam set of the Bragg-vector refinement, unless overridden + sg_max = float(resolve(sg_max, "sg_max", md, default=SG_MAX)) + k_max = resolve(k_max, "k_max", md) + fast_absorption = bool(resolve(fast_absorption, "fast_absorption", md, default=True)) tilts, tilt_w = illumination_nodes( energy_ev, md.get("precession_deg", 0.0), @@ -3308,9 +4055,9 @@ def refine_dynamical_image( exclude_direct_px: float | None = None, background: str = "constant", mask: np.ndarray | None = None, - sg_max: float = SG_MAX, + sg_max: float | None = None, k_max: float | None = None, - fast_absorption: bool = True, + fast_absorption: bool | None = None, update_orientations: bool = True, progress_bar: bool = True, ) -> dict: @@ -3321,8 +4068,8 @@ def refine_dynamical_image( of the measured pattern is compared with a rendered pattern: Bloch intensities averaged over the precession / convergence tilt set, drawn as disks of the global radius and edge width from - fit_disk_shape(), with a free intensity scale and constant - background. The thickness and the orientation tilt are re-searched + fit_disk_shape(), with a free intensity scale and a constant or + radial background. The thickness and the orientation tilt are re-searched on a local grid (thickness +- thickness_half_range_A, tilt +- the stage half-range), the deformation and in-plane rotation are kept from the position fit. The image cost is the residual after the @@ -3335,11 +4082,62 @@ def refine_dynamical_image( refinement is not accurate enough; it costs one rendered image per trial (tilt, thickness) on top of the Bloch solves. + The illumination, intensity power and beam set (sg_max, k_max, + fast_absorption) default to those of the refine_dynamical() result. + + Parameters + ---------- + dataset : Dataset4dstem + Measured patterns; anything with `.array` (R, C, ny, nx) and + `.shape`. + phase_map : PhaseMap + The refined PhaseMap. + result : dict + From refine_dynamical(). + origins : np.ndarray + (R, C, 2) direct beam positions (row, col) in pixels. + pixel_size : float + Detector sampling (1/Angstroms per pixel). + disk_radius_px, edge_px : float + Disk shape, from fit_disk_shape(). + rotation_ccw_deg : float, default=0.0 + Scan-to-detector rotation of the calibration. + ellipse : sequence | None + Elliptic distortion (e11, e12) of the calibration. + thickness_half_range_A : float, default=100.0 + Half-width (Angstroms) of the thickness search around the + Bragg-vector thickness. + thickness_step_A : float, default=10.0 + Thickness step in Angstroms. + tilt_stage : (float, float), default=(0.03, 0.01) + Tilt search half-range and step in degrees. + power_intensity : float | None + Images are compared as I ** power_intensity; inherited. + r_max_px : float | None + Ignore pixels farther than this from the origin; None uses all. + exclude_direct_px : float | None + Ignore pixels within this distance of the origin; default 1.5 + disk radii. + background : {"constant", "radial"}, default="constant" + Background model: a constant, or a quadratic in the distance from + the direct beam (the diffuse scattering floor). + mask : np.ndarray | None + Positions to refine, as in refine_dynamical; None refines all. + sg_max, k_max : float | None + Beam set; inherited from the result's metadata (SG_MAX if absent). + fast_absorption : bool | None + First-order absorption; inherited (True if absent). + update_orientations : bool, default=True + Write the refined quaternions back into the OrientationMaps. + progress_bar : bool, default=True + Show a progress bar over positions. + Returns ------- - dict with 'thickness' (R, C), 'tilt_deg' (R, C, 2) the additional - tilt over the Bragg-vector result, 'quats' (R, C, 4), 'cost' (R, C) - the normalized image residual, and 'metadata'. + dict + 'thickness' (R, C), 'tilt_deg' (R, C, 2) the additional tilt over + the Bragg-vector result, 'quats' (R, C, 4), 'cost' (R, C) the + normalized image residual (NaN where not refined), and 'metadata'. """ from quantem.diffraction.rotations import qmult, quat_from_axis_angle @@ -3350,6 +4148,10 @@ def refine_dynamical_image( power_intensity = float( resolve(power_intensity, "power_intensity", md, default=POWER_INTENSITY) ) + # the beam set of the Bragg-vector refinement, unless overridden + sg_max = float(resolve(sg_max, "sg_max", md, default=SG_MAX)) + k_max = resolve(k_max, "k_max", md) + fast_absorption = bool(resolve(fast_absorption, "fast_absorption", md, default=True)) if exclude_direct_px is None: exclude_direct_px = 1.5 * disk_radius_px tilts, tilt_w = illumination_nodes( @@ -3383,7 +4185,7 @@ def refine_dynamical_image( iterator = tqdm(iterator, desc="image refinement") for rx, ry in iterator: f = int(result["candidate"][rx, ry]) - if not torch.isfinite(result["cost"][rx, ry, f]): + if f < 0 or not torch.isfinite(result["cost"][rx, ry, f]): continue i_om, m = cands[f] om = oms[i_om] diff --git a/src/quantem/diffraction/bragg_vectors.py b/src/quantem/diffraction/bragg_vectors.py index ce3f59bba..a733d3d20 100644 --- a/src/quantem/diffraction/bragg_vectors.py +++ b/src/quantem/diffraction/bragg_vectors.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy as _copy +import warnings from pathlib import Path from typing import Any, Literal, Sequence, Union @@ -46,7 +47,7 @@ class BraggVectors(AutoSerialize): image (:meth:`make_template_from_probe`). 2. :meth:`detect_disks` – template-match every scan position; detected peaks are stored in :attr:`peaks` (a :class:`Vector` of ``[q_row, q_col, - intensity]``, numpy-backed) and accumulated into the Bragg vector map + intensity]`` in detector pixels) and accumulated into the Bragg vector map :attr:`bvm`. 3. :meth:`choose_basis_vectors` – pick the lattice basis ``(origin, g1, g2)`` from the BVM, automatically or by hand; also stores the numbered candidate @@ -55,15 +56,16 @@ class BraggVectors(AutoSerialize): peaks into a reference lattice (``reference_ab``/``reference_qpos``). 5. :meth:`fit_lattice` – the heavy step: at every scan position, match the detections to the reference within ``max_peak_shift``, intensity-weighted - least-squares fit the lattice vectors into ``u_array``/``v_array`` of shape + least-squares fit the lattice vectors into ``g1_array``/``g2_array`` of shape ``(scan_row, scan_col, 2)``, and compute the per-position ``mask_weight``. 6. :meth:`calculate_strain_map` – hand the lattice vectors (and ``mask_weight``) to a :class:`~quantem.diffraction.strain.StrainMap`. - Detection runs in torch (CPU now, CUDA later); the ragged peak table is held - in a numpy-backed :class:`Vector`. The detector→scan rotation is read from - the parent dataset metadata (``q_to_r_rotation_ccw_deg`` + ``q_transpose``), - the single source of truth shared with the DPC/CoM workflow. + Detection runs in torch on :attr:`device` (CPU or GPU); the ragged peak table + is held in a torch-backed :class:`Vector`. The detector-to-scan rotation is + read from the parent dataset metadata (``q_to_r_rotation_ccw_deg`` and + ``q_transpose``), the same keys used by the DPC/CoM workflow, and applied in + :meth:`calculate_strain_map`. Use :meth:`from_dataset` to construct an instance. @@ -108,14 +110,16 @@ def __init__( self.reference_qpos: np.ndarray | None = None self.reference_intensity: np.ndarray | None = None - self.u_array: np.ndarray | None = None - self.v_array: np.ndarray | None = None + self.g1_array: np.ndarray | None = None + self.g2_array: np.ndarray | None = None # per-position diagnostics from fit_lattice() self.mask_weight: np.ndarray | None = None self.fit_error: np.ndarray | None = None self._template: torch.Tensor | None = None self._template_ft: torch.Tensor | None = None + # full dataset resident on self.device, set by detect_disks(save_to_gpu=True) + self._gpu_cache: torch.Tensor | None = None self.metadata: dict[str, Any] = {} @classmethod @@ -159,9 +163,10 @@ def save( Overrides :meth:`~quantem.core.io.serialize.AutoSerialize.save` to drop :attr:`dataset` — the raw 4D-STEM cube, which dominates the file size — from serialization by default. The detected :attr:`peaks`, lattice fit - (:attr:`u_array`/:attr:`v_array`), Bragg vector map and all diagnostics are + (:attr:`g1_array`/:attr:`g2_array`), Bragg vector map and all diagnostics are kept, so the file holds the *results* of the workflow (orders of magnitude - smaller than the data) rather than the data itself. + smaller than the data) rather than the data itself. The device copy of the + dataset made by ``detect_disks(save_to_gpu=True)`` is never saved. ``"dataset"`` is recorded in the file's skip metadata, so a reloaded workflow simply has no ``dataset`` attribute. Re-attach one (``bv.dataset = ds``) before @@ -194,6 +199,8 @@ def save( skip = list(skip) if not include_dataset and "dataset" not in skip: skip.append("dataset") + if "_gpu_cache" not in skip: + skip.append("_gpu_cache") # Explicit (two-arg) super() rather than the bare super(): the zero-arg form # needs a compiler-created __class__ closure cell that is absent when this # method's source is re-exec'd from a string (Jupyter autoreload), which @@ -285,16 +292,29 @@ def make_template_from_data( BraggVectors ``self``, for method chaining. """ - data = torch.as_tensor( - np.asarray(self.dataset.array), dtype=torch.float, device=self.device - ) - if roi is None: - probe = data.mean(dim=(0, 1)) + gpu_cache = getattr(self, "_gpu_cache", None) + if gpu_cache is not None: + # Dataset is already resident on self.device (e.g. from a prior + # detect_disks(save_to_gpu=True)) -- reuse it instead of transferring again. + if roi is None: + probe = gpu_cache.mean(dim=(0, 1)) + else: + m = torch.as_tensor(np.asarray(roi) > 0, device=self.device) + if not bool(m.any()): + raise ValueError("roi selects no scan positions.") + probe = gpu_cache[m].mean(dim=0) else: - m = torch.as_tensor(np.asarray(roi) > 0, device=self.device) - if not bool(m.any()): - raise ValueError("roi selects no scan positions.") - probe = data[m].mean(dim=0) + # Select the (small) ROI on the CPU first so we never have to put the + # full dataset on the device just to average a handful of positions. + array = np.asarray(self.dataset.array) + if roi is None: + probe_np = array.mean(axis=(0, 1)) + else: + m = np.asarray(roi) > 0 + if not m.any(): + raise ValueError("roi selects no scan positions.") + probe_np = array[m].mean(axis=0) + probe = torch.as_tensor(probe_np, dtype=torch.float, device=self.device) if center is None: center = probe_centroid(probe) self._set_template(probe, center=center, subtract_mean=subtract_mean) @@ -415,6 +435,7 @@ def detect_disks( sigma_cc: float | None = None, batch_size: int | None = None, progressbar: bool = True, + save_to_gpu: bool = True, ) -> Vector: """Detect Bragg disks at every scan position (or a subset for testing). @@ -471,6 +492,12 @@ def detect_disks( detector dimensions. progressbar : bool, default=True If ``True``, show a tqdm progress bar over the full-scan detection. + save_to_gpu : bool, default=True + If ``True`` and :attr:`device` is not the CPU, copy the whole dataset + to the device once and read the batches from that copy, which is + faster and lowers CPU load. If the copy does not fit in device memory, + batches are read from the dataset instead. The copy is kept for later + calls (including :meth:`make_template_from_data`) and is not saved. Returns ------- @@ -493,6 +520,19 @@ def detect_disks( sigma_cc=sigma_cc, ) + if save_to_gpu and str(self.device) != "cpu": + if getattr(self, "_gpu_cache", None) is None: + try: + print(f"Loading dataset to {self.device}...", end=" ", flush=True) + self._gpu_cache = torch.as_tensor( + np.asarray(self.dataset.array), + dtype=torch.float32, + device=self.device, + ) + except (RuntimeError, torch.cuda.OutOfMemoryError): + print("out of memory, reading per batch instead.") + self._gpu_cache = None + if positions is not None: if len(positions) == 0: raise ValueError("positions must contain at least one (row, col) to test on.") @@ -861,8 +901,8 @@ def fit_lattice( ``origin + a*g1 + b*g2`` for each reference ``(a, b)`` — keeping a peak only when it lands within ``max_peak_shift`` of its nearest ideal site (not the measured candidate position), then ``q = x0 + a*g1 + b*g2`` is fit by - intensity-weighted least squares over the matched peaks. The fitted ``g1``/``g2`` go into :attr:`u_array`/ - :attr:`v_array` (shape ``(scan_row, scan_col, 2)``, row/col components); + intensity-weighted least squares over the matched peaks. The fitted ``g1``/``g2`` go into :attr:`g1_array`/ + :attr:`g2_array` (shape ``(scan_row, scan_col, 2)``, row/col components); positions with fewer than ``min_num_peaks`` matched peaks are left ``nan``. Two diagnostics are stored per position. :attr:`fit_error` is the RMS fit @@ -932,8 +972,8 @@ def fit_lattice( rms_rand = float(np.sqrt(cell_area / (2.0 * np.pi))) if cell_area > 0 else 1.0 scan_r, scan_c = int(self.dataset.shape[0]), int(self.dataset.shape[1]) - u_array = np.full((scan_r, scan_c, 2), np.nan, dtype=float) - v_array = np.full((scan_r, scan_c, 2), np.nan, dtype=float) + g1_array = np.full((scan_r, scan_c, 2), np.nan, dtype=float) + g2_array = np.full((scan_r, scan_c, 2), np.nan, dtype=float) mask_weight = np.zeros((scan_r, scan_c), dtype=float) fit_error = np.full((scan_r, scan_c), np.nan, dtype=float) @@ -974,8 +1014,8 @@ def fit_lattice( ) if beta is None: continue - u_array[r, c] = beta[1] - v_array[r, c] = beta[2] + g1_array[r, c] = beta[1] + g2_array[r, c] = beta[2] fit_error[r, c] = rms # mask weight = lattice "order parameter": snap EVERY detected peak to the @@ -998,8 +1038,8 @@ def fit_lattice( rms_all = float(np.sqrt(np.sum(w * disp[nonzero] ** 2) / wsum)) mask_weight[r, c] = float(np.clip(1.0 - rms_all / rms_rand, 0.0, 1.0)) - self.u_array = u_array - self.v_array = v_array + self.g1_array = g1_array + self.g2_array = g2_array self.mask_weight = mask_weight self.fit_error = fit_error self.metadata["fit"] = { @@ -1017,52 +1057,116 @@ def fit_lattice( def calculate_strain_map( self, - u_ref: np.ndarray | None = None, - v_ref: np.ndarray | None = None, + g1_ref: np.ndarray | None = None, + g2_ref: np.ndarray | None = None, mask: np.ndarray | None = None, + q_to_r_rotation_ccw_deg: float | None = None, + q_transpose: bool | None = None, + calculation_metric: str = "median", ) -> StrainMap: """Build a :class:`StrainMap` from the fitted per-position lattice vectors. Parameters ---------- - u_ref : np.ndarray, optional - ``(2,)`` reference for the first lattice vector. Defaults to the median - over the scan inside :class:`StrainMap`. - v_ref : np.ndarray, optional - ``(2,)`` reference for the second lattice vector. Defaults to the median - over the scan inside :class:`StrainMap`. + g1_ref : np.ndarray, optional + ``(2,)`` reference for the first lattice vector, in detector + ``(row, col)`` pixels (the frame of :attr:`g1_array`). Defaults to the + ``calculation_metric`` over the scan inside :class:`StrainMap`. + g2_ref : np.ndarray, optional + ``(2,)`` reference for the second lattice vector, as ``g1_ref``. mask : np.ndarray, optional ``(scan_row, scan_col)`` per-position weighting used when computing the reference lattice. Defaults to :attr:`mask_weight` from - :meth:`fit_lattice` (the lattice order parameter — how well all detected + :meth:`fit_lattice` (the lattice order parameter: how well all detected intensity snaps to the fitted lattice), so clean single-crystal positions dominate the reference and positions with off-lattice intensity are down-weighted. + q_to_r_rotation_ccw_deg : float, optional + Counter-clockwise rotation in degrees from the detector frame to the + scan frame, applied to the lattice vectors before the strain is + computed, so ``e_rr``/``e_cc`` refer to the scan rows and columns. + ``None`` (default) reads ``q_to_r_rotation_ccw_deg`` from the dataset + metadata, else uses 0. + q_transpose : bool, optional + If ``True``, swap the detector row/col axes before the rotation. + ``None`` (default) reads ``q_transpose`` from the dataset metadata, + else uses ``False``. + calculation_metric : {"median", "mean"}, default="median" + Statistic for the automatic reference lattice (weighted by ``mask``). Returns ------- StrainMap - A strain map initialized from the fitted lattice vectors. + A strain map initialized from the fitted lattice vectors. The rotation + and transpose used are also stored in :attr:`metadata`. """ - if self.u_array is None or self.v_array is None: + if self.g1_array is None or self.g2_array is None: raise ValueError("Run fit_lattice() before calculate_strain_map().") if mask is None: mask = self.mask_weight - ds_sampling = float(self.dataset.sampling[0]) - ds_units = str(self.dataset.units[0]) + ds_units = None + ds_sampling = None + if hasattr(self.dataset, "units"): + if isinstance(self.dataset.units, (tuple, list)): + ds_units = str(self.dataset.units[0]) + else: + ds_units = str(self.dataset.units) + if hasattr(self.dataset, "sampling"): + if isinstance(self.dataset.sampling, (tuple, list, np.ndarray)): + ds_sampling = float(self.dataset.sampling[0]) + else: + ds_sampling = float(self.dataset.sampling) + + metadata = getattr(self.dataset, "metadata", None) or {} + parent_rot = metadata.get("q_to_r_rotation_ccw_deg", None) + parent_tr = metadata.get("q_transpose", None) + + used_parent = False + if q_to_r_rotation_ccw_deg is None and parent_rot is not None: + q_to_r_rotation_ccw_deg = parent_rot + used_parent = True + if q_transpose is None and parent_tr is not None: + q_transpose = parent_tr + used_parent = True + + if used_parent: + warnings.warn( + "BraggVectors.calculate_strain_map: using Dataset4dstem metadata " + f"(q_to_r_rotation_ccw_deg={q_to_r_rotation_ccw_deg or 0.0}, " + f"q_transpose={q_transpose or False}).", + UserWarning, + ) + + if q_to_r_rotation_ccw_deg is None or q_transpose is None: + q_to_r_rotation_ccw_deg = ( + 0.0 if q_to_r_rotation_ccw_deg is None else q_to_r_rotation_ccw_deg + ) + q_transpose = False if q_transpose is None else q_transpose + warnings.warn( + "BraggVectors.calculate_strain_map: no detector rotation given or in " + f"the dataset metadata; using q_to_r_rotation_ccw_deg=" + f"{q_to_r_rotation_ccw_deg} and q_transpose={q_transpose}.", + UserWarning, + ) + + self.metadata["q_to_r_rotation_ccw_deg"] = float(q_to_r_rotation_ccw_deg) + self.metadata["q_transpose"] = bool(q_transpose) return StrainMap( - u_array=self.u_array, - v_array=self.v_array, + g1_array=self.g1_array, + g2_array=self.g2_array, ds_shape=tuple(self.dataset.shape), real_space=self.real_space, - u_ref=u_ref, - v_ref=v_ref, + g1_ref=g1_ref, + g2_ref=g2_ref, mask=mask, ds_sampling=ds_sampling, ds_units=ds_units, + q_to_r_rotation_ccw_deg=float(q_to_r_rotation_ccw_deg), + q_transpose=bool(q_transpose), + calculation_metric=calculation_metric, ) # ---- visualization ---- @@ -1080,7 +1184,8 @@ def show_template( Parameters ---------- position : tuple of int, default=(0, 0) - ``(row, col)`` scan position whose correlation map is shown. + ``(row, col)`` scan position (scan pixels) whose correlation map is + shown. crop_factor : float, optional If given, zoom to a square window of half-width ``crop_factor * radius`` about the central-beam center, where ``radius`` is the central-beam @@ -1474,7 +1579,14 @@ def measure_scan_rotation( ) return out[0] if isinstance(out, tuple) else out - def calibrate(self, crystal, pixel_size_guess: float, **kwargs): + def calibrate( + self, + crystal, + pixel_size_guess: float, + rotation_ccw_deg: float = 0.0, + plot: bool = False, + **kwargs, + ): """Measure the reciprocal pixel size and the elliptic distortion. Matches the radial distribution of the detected peaks against the @@ -1497,8 +1609,9 @@ def calibrate(self, crystal, pixel_size_guess: float, **kwargs): Starting reciprocal pixel size, 1/Angstroms per detector pixel. Recovered from a factor of two out in either direction. rotation_ccw_deg : float, default=0.0 - Diffraction-to-scan rotation, recorded for later use. - plot : bool, default=True + Counter-clockwise diffraction-to-scan rotation in degrees, recorded + on the calibration for later use. + plot : bool, default=False Show the ring comparison before and after, for the first reference crystal. :meth:`CrystalMap.plot_calibration` checks every candidate phase afterwards. @@ -1524,7 +1637,14 @@ def calibrate(self, crystal, pixel_size_guess: float, **kwargs): if self.peaks is None: raise ValueError("Run detect_disks() before calibrate().") - cal = _cal.calibrate(self.peaks, crystal, pixel_size_guess, **kwargs) + cal = _cal.calibrate( + self.peaks, + crystal, + pixel_size_guess, + rotation_ccw_deg=rotation_ccw_deg, + plot=plot, + **kwargs, + ) self.calibration = cal return cal @@ -1588,13 +1708,22 @@ def _scan_calibration(self) -> dict: def _resolve_background_sigma(self, background_sigma: float | str | None) -> float | None: """Resolve the ``background_sigma`` argument to a value in pixels. - ``"auto"`` (the default everywhere) maps to twice the central-beam - radius: wide enough that the disk-scale correlation peaks pass + ``None`` (the default everywhere) disables the background subtraction. + ``"auto"`` maps to twice the central-beam radius: wide enough that the disk-scale correlation peaks pass untouched, narrow enough to remove the zero-sum template's negative moat around a bright unscattered beam -- which otherwise pushes weak disk peaks below zero, where the correlation clamp erases them before - peak finding. Pass ``None`` to disable the background subtraction or a - float to set the scale explicitly. + peak finding. A float sets the width explicitly. + + Parameters + ---------- + background_sigma : float, "auto" or None + The value passed by the caller. + + Returns + ------- + float or None + Width in pixels, or ``None`` for no background subtraction. """ if background_sigma is None: return None @@ -1685,6 +1814,8 @@ def _detect_positions( if batch_size is None: batch_size = int(min(1024, max(1, 16_000_000 // (H * W)))) + use_cache = getattr(self, "_gpu_cache", None) is not None + it = range(0, len(coords), batch_size) if progressbar: try: @@ -1699,17 +1830,15 @@ def _detect_positions( results: list[NDArray] = [] for start in it: chunk = coords[start : start + batch_size] - dps = torch.stack( - [ - torch.as_tensor( - np.asarray(self.dataset.array[r, c]), - dtype=torch.float, - device=self.device, - ) - for r, c in chunk - ], - dim=0, - ) + rows = [r for r, c in chunk] + cols = [c for r, c in chunk] + if use_cache: + dps = self._gpu_cache[rows, cols] + else: + # one fancy-indexed read per batch rather than one per pattern + dps_np = np.asarray(self.dataset.array[np.asarray(rows), np.asarray(cols)]) + dps = torch.as_tensor(dps_np, dtype=torch.float32, device=self.device) + out = detect_disks_batch(dps, self._template_ft, **detect_kwargs) results.extend( arr if arr.shape[0] else np.empty((0, len(PEAK_FIELDS)), dtype=float) @@ -1813,7 +1942,7 @@ def _fit_lattice_vectors( a: NDArray, b: NDArray, intensity: NDArray, -) -> tuple[NDArray | None, NDArray | None]: +) -> tuple[NDArray | None, float]: """Intensity-weighted lattice fit ``q = x0 + a*g1 + b*g2`` for one pattern. Parameters diff --git a/src/quantem/diffraction/bragg_vectors_visualization.py b/src/quantem/diffraction/bragg_vectors_visualization.py index f2cb4af26..a105909ac 100644 --- a/src/quantem/diffraction/bragg_vectors_visualization.py +++ b/src/quantem/diffraction/bragg_vectors_visualization.py @@ -47,7 +47,7 @@ def plot_template( ax[1].imshow(template, cmap="gray") ax[1].set_title("template (centered)") ax[2].imshow(corr_map, cmap="viridis") - ax[2].set_title(f"correlation @ {tuple(position)}") + ax[2].set_title(f"correlation @ {tuple(position)} (real space pixels)") for a in ax: a.set_xticks([]) a.set_yticks([]) @@ -531,32 +531,57 @@ def plot_bvm( counts: np.ndarray, *, figsize: tuple[float, float] = (10, 4), + norm: str | dict = "log_auto", + cmap: str = "inferno", + counts_kwargs: dict | None = None, + **plotting_kwargs, ): - """The Bragg vector map (log-scaled) beside the per-position peak count. + """The Bragg vector map beside the per-position peak count. Parameters ---------- bvm : np.ndarray - Bragg vector map; displayed log-scaled (``log1p``). + ``(H, W)`` Bragg vector map in detector pixels. counts : np.ndarray Per-position peak count, shape ``(scan_row, scan_col)``. figsize : tuple of float, default=(10, 4) Figure size in inches. + norm : str or dict, default="log_auto" + Intensity normalization of the Bragg vector map, passed to + :func:`~quantem.core.visualization.show_2d`. + cmap : str, default="inferno" + Colormap of the Bragg vector map. + counts_kwargs : dict, optional + Keyword arguments for :func:`~quantem.core.visualization.show_2d` on the + peak-count panel (defaults: ``cmap="viridis"``, ``cbar=True``). + **plotting_kwargs + Further keyword arguments for :func:`~quantem.core.visualization.show_2d` + on the Bragg vector map panel. Returns ------- tuple ``(fig, ax)`` with ``ax`` a length-2 array of axes. """ + from quantem.core.visualization import show_2d + fig, ax = plt.subplots(1, 2, figsize=figsize) - ax[0].imshow(np.log1p(bvm), cmap="inferno") - ax[0].set_title("Bragg vector map (log)") - im = ax[1].imshow(counts, cmap="viridis") - ax[1].set_title("peaks per position") - for a in ax: - a.set_xticks([]) - a.set_yticks([]) - fig.colorbar(im, ax=ax[1], fraction=0.046, pad=0.04) + + show_2d( + np.asarray(bvm), + figax=(fig, ax[0]), + cmap=cmap, + norm=norm, + title="Bragg vector map", + **plotting_kwargs, + ) + counts_plot_kwargs = { + "cmap": "viridis", + "cbar": True, + "title": "peaks per position", + **(counts_kwargs or {}), + } + show_2d(np.asarray(counts), figax=(fig, ax[1]), **counts_plot_kwargs) fig.tight_layout() return fig, ax @@ -773,4 +798,4 @@ def plot_lattice_fit( a.set_xticks([]) a.set_yticks([]) fig.tight_layout() - return fig, ax \ No newline at end of file + return fig, ax diff --git a/src/quantem/diffraction/calibration.py b/src/quantem/diffraction/calibration.py index f007c5916..8ce1857c0 100644 --- a/src/quantem/diffraction/calibration.py +++ b/src/quantem/diffraction/calibration.py @@ -1,8 +1,27 @@ -"""Diffraction-space calibration against known crystal structures. - -Functions here operate on detected Bragg peaks (a quantem Vector) and refine -the reciprocal-space pixel size by comparing the radial peak histogram with -the ring positions of a reference crystal. +"""Diffraction-space calibration of 4D-STEM Bragg peaks. + +Each step works on detected Bragg peaks (a quantem Vector): + +- Origins: :func:`measure_origins` fits a plane to the direct-beam position + over the scan, for ``BraggVectors.correct_peak_origins``. +- Scan rotation: :func:`measure_scan_rotation` finds the detector-to-scan + rotation from the curl of the center-of-mass field. +- Pixel size and elliptic distortion: :func:`calibrate` matches the radial + peak histogram against the rings of one or more reference crystals and + returns a :class:`DiffractionCalibration`. +- :class:`DiffractionCalibration` holds the pixel size (1/Angstroms per + pixel), the ellipse and the rotation, converts pixel peaks to calibrated + (qx, qy) peaks with :meth:`DiffractionCalibration.apply`, and can be saved + from a standard and reused on another dataset. + +:func:`calibrate` is the main entry point. The other functions are +lower-level steps that act on peaks directly: :func:`peaks_to_calibrated` +and :func:`calibrate_ellipse` (both used by :func:`calibrate`), +:func:`calibrate_pixel_size` (radial histogram fit of the scale), +:func:`calibrate_pixel_size_matching` (scale fit by full orientation +matching), :func:`scale_peaks` and :func:`apply_ellipse`. +:func:`refine_calibration` measures the remaining calibration error from +strain maps after orientation matching. """ from __future__ import annotations @@ -39,17 +58,16 @@ def _measure_raw_origins(bragg_vectors, search_radius: float, center=None) -> np return meas -def plot_origin_fit(bragg_vectors, origins: np.ndarray, search_radius: float = 6.0, center=None): - """Diagnostic for measure_origins: measured vs fit vs residual, both axes.""" +def _plot_origin_panels(meas: np.ndarray, origins: np.ndarray): + """Measured origins, plane fit and residual for both detector axes.""" import matplotlib.pyplot as plt - meas = _measure_raw_origins(bragg_vectors, search_radius, center) fig, axs = plt.subplots(2, 3, figsize=(13.5, 5.6)) names = ["row", "col"] for k in range(2): m, f = meas[..., k], origins[..., k] resid = m - f - center = np.nanmean(m) + mean_m = np.nanmean(m) span = max(np.nanstd(m) * 3, 1e-3) for j, (img, title) in enumerate( [ @@ -58,7 +76,7 @@ def plot_origin_fit(bragg_vectors, origins: np.ndarray, search_radius: float = 6 (resid, f"residual {names[k]} (px)"), ] ): - c0 = 0.0 if j == 2 else center + c0 = 0.0 if j == 2 else mean_m sp = max(np.nanstd(resid) * 3, 1e-3) if j == 2 else span im = axs[k, j].imshow( img, @@ -75,6 +93,32 @@ def plot_origin_fit(bragg_vectors, origins: np.ndarray, search_radius: float = 6 return fig, axs +def plot_origin_fit(bragg_vectors, origins: np.ndarray, search_radius: float = 6.0, center=None): + """Measured origins against the plane fit of :func:`measure_origins`. + + Parameters + ---------- + bragg_vectors : BraggVectors + With detected peaks. + origins : np.ndarray + (scan_row, scan_col, 2) fitted origins from :func:`measure_origins`. + search_radius : float, default=6.0 + Radius in detector pixels searched around `center`, as passed to + :func:`measure_origins`. + center : tuple of float, optional + ``(row, col)`` detector position searched around, as passed to + :func:`measure_origins`. Defaults to the detector center. + + Returns + ------- + tuple + ``(fig, axs)``, axs of shape (2, 3): rows are the detector row and + column, columns are measured, fit and residual, all in pixels. + """ + meas = _measure_raw_origins(bragg_vectors, search_radius, center) + return _plot_origin_panels(meas, origins) + + def measure_origins( bragg_vectors, search_radius: float = 6.0, @@ -92,6 +136,9 @@ def measure_origins( Parameters ---------- + bragg_vectors : BraggVectors + With detected peaks, fields (q_row, q_col, intensity) in detector + pixels, not yet origin-corrected. search_radius : float, default=6.0 Radius in detector pixels searched around `center`. robust : bool, default=True @@ -154,36 +201,7 @@ def plane(z, ok): out[..., k] = fit if plot: - import matplotlib.pyplot as plt - - fig, axs = plt.subplots(2, 3, figsize=(13.5, 5.6)) - names = ["row", "col"] - for k in range(2): - m, f = meas[..., k], out[..., k] - resid = m - f - center = np.nanmean(m) - span = max(np.nanstd(m) * 3, 1e-3) - for j, (img, title) in enumerate( - [ - (m, f"measured origin {names[k]} (px)"), - (f, f"plane fit {names[k]} (px)"), - (resid, f"residual {names[k]} (px)"), - ] - ): - c0 = 0.0 if j == 2 else center - s = max(np.nanstd(resid) * 3, 1e-3) if j == 2 else span - im = axs[k, j].imshow( - img, - cmap="RdBu_r", - vmin=c0 - s, - vmax=c0 + s, - interpolation="nearest", - ) - axs[k, j].set_title(title, fontsize=10) - axs[k, j].set_xticks([]) - axs[k, j].set_yticks([]) - fig.colorbar(im, ax=axs[k, j], shrink=0.85) - fig.tight_layout() + fig, axs = _plot_origin_panels(meas, out) return out, fig, axs return out @@ -212,6 +230,8 @@ def peaks_to_calibrated( ellipse : array-like | None [e11, e12] elliptic distortion correction from calibrate_ellipse(), applied in the detector frame before the rotation. + name : str, default="bragg_peaks_calibrated" + Name of the returned Vector. Returns ------- @@ -223,9 +243,7 @@ def peaks_to_calibrated( row_counts = np.asarray(peaks_px.row_counts(), dtype=int) qrc = flat[:, :2] * pixel_size_inv_A if ellipse is not None: - e11, e12 = float(ellipse[0]), float(ellipse[1]) - A = np.array([[1 + e11, e12], [e12, 1 - e11]]) - qrc = qrc @ A.T + qrc = qrc @ _ellipse_matrix(ellipse).T if rotation_ccw_deg != 0.0: th = np.deg2rad(rotation_ccw_deg) rot = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]]) @@ -251,7 +269,20 @@ def peaks_to_calibrated( def scale_peaks(peaks, scale: float): - """Return a copy of a (qx, qy, intensity) Vector with q scaled.""" + """Return a copy of a (qx, qy, intensity) Vector with q scaled. + + Parameters + ---------- + peaks : Vector + Peaks with fields (qx, qy, intensity). + scale : float + Factor applied to qx and qy, e.g. from :func:`calibrate_pixel_size`. + + Returns + ------- + Vector + Scaled copy of `peaks`. + """ out = peaks.copy() flat = out.numpy().astype(np.float64) flat[:, :2] *= scale @@ -269,6 +300,21 @@ def radial_histogram( ) -> tuple[np.ndarray, np.ndarray]: """Intensity-weighted histogram of Bragg peak radii over all positions. + Parameters + ---------- + peaks : Vector + Calibrated peaks with fields (qx, qy, intensity) in 1/Angstroms. + k_min, k_max : float, default=0.05, 1.5 + Range of the bins, 1/Angstroms. + k_step : float, default=0.002 + Bin width, 1/Angstroms. + bragg_k_power : float, default=2.0 + Each peak is weighted by ``|q| ** bragg_k_power``, which offsets the + fall-off of the scattering factors with q. + bragg_intensity_power : float, default=1.0 + Each peak is weighted by ``intensity ** bragg_intensity_power``; 0 + counts every peak equally. + Returns ------- k : np.ndarray @@ -296,7 +342,24 @@ def simulated_ring_profile( k_broadening: float = 0.01, bragg_k_power: float = 2.0, ) -> np.ndarray: - """1D ring profile of a crystal: Gaussians at |g| weighted by intensity.""" + """1D ring profile of a crystal: Gaussians at |g| weighted by intensity. + + Parameters + ---------- + crystal : Crystal + With structure factors calculated. + k : np.ndarray + Scattering vectors to evaluate at, 1/Angstroms. + k_broadening : float, default=0.01 + Gaussian standard deviation of each ring, 1/Angstroms. + bragg_k_power : float, default=2.0 + Each ring is weighted by ``|g| ** bragg_k_power`` times its intensity. + + Returns + ------- + np.ndarray + Profile, same shape as `k`. + """ g = crystal.g_len.numpy() w = crystal.struct_factors_int.numpy() * g**bragg_k_power prof = (w[None, :] * np.exp(-((k[:, None] - g[None, :]) ** 2) / (2 * k_broadening**2))).sum( @@ -345,13 +408,32 @@ def calibrate_pixel_size_matching( Candidate scale factors; defaults to 0.90 ... 1.10 in 2% steps. subsample : int, default=8 Stride of the probe-position grid used for scoring. + angle_step_deg : float, default=3.0 + Zone-axis and in-plane angular step of the orientation plan, degrees. + corr_kernel_size : float, default=0.02 + Matching kernel width, 1/Angstroms. Keep it near the peak position + noise: a wide kernel gives partial credit to near-coincident rings + at a wrong scale. + min_number_peaks : int, default=MIN_NUMBER_PEAKS + Positions with fewer peaks are not matched. + plot : bool, default=False + Plot the score against the scale factor. + return_scores : bool, default=False + Also return the scale factors and their scores. + returnfig : bool, default=False + With `plot`, also return the figure and axes. Returns ------- scale : float Best scale factor (parabolic refinement over the score maximum). - With return_scores=True, also (scales, scores); with returnfig=True, - also (fig, ax). + Multiply the pixel size by it. + scales, scores : np.ndarray + Only with `return_scores`. The score is the mean over phases of the + median correlation of the matched positions, 0 for a phase where + nothing matched. + fig, ax + Only with `plot` and `returnfig`. """ from quantem.diffraction.orientation import OrientationMap @@ -382,10 +464,12 @@ def calibrate_pixel_size_matching( ) om.match_orientations(progress_bar=False, min_number_peaks=min_number_peaks) corr = om.corr[..., 0] - per_phase.append(float(corr[corr > 0].median())) + matched = corr[corr > 0] + # a phase that indexes nothing at this scale scores zero + per_phase.append(float(matched.median()) if matched.numel() > 0 else 0.0) scores[i] = float(np.mean(per_phase)) - i_best = int(np.argmax(scores)) + i_best = int(np.nanargmax(scores)) if np.isfinite(scores).any() else 0 scale = float(scales[i_best]) if 0 < i_best < len(scales) - 1: c0, c1, c2 = scores[i_best - 1 : i_best + 2] @@ -438,15 +522,27 @@ def calibrate_pixel_size( majority phase of the scan. scale_range : tuple, default=(0.8, 1.25) Search range of the scale factor. + scale_step : float, default=5e-4 + Step of the scale factor scan. + k_min, k_max : float, default=0.05, 1.3 + Range of scattering vectors compared, 1/Angstroms, after scaling. + k_broadening : float, default=0.01 + Gaussian standard deviation of the simulated rings, 1/Angstroms. + bragg_k_power : float, default=2.0 + Rings are weighted by ``|g| ** bragg_k_power``; see + :func:`simulated_ring_profile`. plot : bool, default=False - Show the measured histogram against the crystal ring positions, - before and after applying the scale. + Show the scaled histogram against the crystal ring profile. + returnfig : bool, default=False + With `plot`, also return the figure and axes. Returns ------- scale : float Multiply existing q values (and the pixel size) by this factor, - e.g. with scale_peaks(). With returnfig=True, also (fig, axs). + e.g. with scale_peaks(). + fig, ax + Only with `plot` and `returnfig`. """ k, hist = radial_histogram(peaks, k_min=k_min * scale_range[0], k_max=k_max / scale_range[0]) scales = np.arange(scale_range[0], scale_range[1], scale_step) @@ -512,11 +608,11 @@ def measure_scan_rotation( squared curl recovers the angle. The curl is invariant under 180-degree rotation, so the sign of the - measured field cannot distinguish theta from theta + 180. Both candidates - are returned; pick the one consistent with a known feature (e.g. a - Burgers orientation relationship, or the divergence sign convention of - DPC). The returned angle is ready to pass to peaks_to_calibrated() as - rotation_ccw_deg. + measured field cannot distinguish theta from theta + 180. Only the + candidate in [0, 180) is returned; the other is that angle + 180. Pick + the one consistent with a known feature (e.g. a Burgers orientation + relationship, or the divergence sign convention of DPC). The returned + angle is ready to pass to peaks_to_calibrated() as rotation_ccw_deg. Parameters ---------- @@ -531,12 +627,16 @@ def measure_scan_rotation( Bragg disks. Recommended for crystalline data. plot : bool, default=False Plot the curl and divergence measures against the rotation angle. + returnfig : bool, default=False + With `plot`, also return the figure and axes. Returns ------- rotation_ccw_deg : float - Curl-minimizing rotation in [0, 180); the physical answer is either - this angle or this angle + 180. With returnfig=True, also (fig, ax). + Curl-minimizing rotation in [0, 180), degrees, on a 0.25 degree + grid; the physical answer is either this angle or this angle + 180. + fig, ax + Only with `plot` and `returnfig`. """ arr = dataset.array scan_r, scan_c, H, W = arr.shape @@ -625,16 +725,27 @@ def calibrate_ellipse( ---------- peaks : Vector Calibrated peaks (qx, qy, intensity), approximate scale is fine. - k_min, k_max : float + k_min, k_max : float, default=0.15, 1.4 Radial range (1/Angstroms) included in the sharpness measure. + n_bins : int, default=800 + Number of log-radius bins between `k_min` and `k_max`. + bragg_k_power : float, default=2.0 + Each peak is weighted by ``|q| ** bragg_k_power``. + bragg_intensity_power : float, default=1.0 + Each peak is weighted by ``intensity ** bragg_intensity_power``. plot : bool, default=False Show the radial histogram before and after the correction. + returnfig : bool, default=False + With `plot`, also return the figure and axes. Returns ------- ellipse : np.ndarray - [e11, e12]; pass to peaks_to_calibrated(ellipse=...) or - apply_ellipse(). With returnfig=True, also (fig, ax). + [e11, e12], the correction matrix [[1 + e11, e12], [e12, 1 - e11]] + that undoes the distortion; pass to peaks_to_calibrated(ellipse=...) + or apply_ellipse(). + fig, ax + Only with `plot` and `returnfig`. """ from scipy.optimize import minimize @@ -693,13 +804,58 @@ def cost(e): return tuple(out) if len(out) > 1 else out[0] +def _ellipse_matrix(ellipse) -> np.ndarray: + """The correction matrix [[1 + e11, e12], [e12, 1 - e11]] of an ellipse.""" + e11, e12 = float(ellipse[0]), float(ellipse[1]) + return np.array([[1 + e11, e12], [e12, 1 - e11]]) + + +def _compose_ellipse(ellipse_new, ellipse_prev) -> np.ndarray: + """One ellipse equivalent to applying `ellipse_prev`, then `ellipse_new`. + + The product of the two correction matrices is reduced to the traceless + symmetric form [[1 + e11, e12], [e12, 1 - e11]]: its isotropic part is + a pixel size change, fit separately, and its antisymmetric part is a + rotation of second order in the ellipse components. + + Parameters + ---------- + ellipse_new : array-like + [e11, e12] fit on peaks that already have `ellipse_prev` applied. + ellipse_prev : array-like | None + [e11, e12] already applied, or None. + + Returns + ------- + np.ndarray + Combined [e11, e12]. + """ + if ellipse_prev is None: + return np.asarray(ellipse_new, dtype=float) + A = _ellipse_matrix(ellipse_new) @ _ellipse_matrix(ellipse_prev) + sym = 0.5 * (A + A.T) / (0.5 * np.trace(A)) + return np.array([0.5 * (sym[0, 0] - sym[1, 1]), sym[0, 1]]) + + def apply_ellipse(peaks, ellipse): - """Return a copy of (qx, qy, intensity) peaks with the ellipse applied.""" + """Return a copy of (qx, qy, intensity) peaks with the ellipse applied. + + Parameters + ---------- + peaks : Vector + Peaks with fields (qx, qy, intensity). + ellipse : array-like + [e11, e12] from :func:`calibrate_ellipse`. Each q is mapped by the + correction matrix [[1 + e11, e12], [e12, 1 - e11]]. + + Returns + ------- + Vector + Corrected copy of `peaks`. + """ out = peaks.copy() flat = out.numpy().astype(np.float64) - e11, e12 = float(ellipse[0]), float(ellipse[1]) - A = np.array([[1 + e11, e12], [e12, 1 - e11]]) - flat[:, :2] = flat[:, :2] @ A.T + flat[:, :2] = flat[:, :2] @ _ellipse_matrix(ellipse).T out.set_flattened(flat) return out @@ -805,6 +961,20 @@ class DiffractionCalibration(AutoSerialize): A calibration is tied to the detector binning it was measured at, which is recorded in `metadata`; `rebin` converts it to another binning. + + Parameters + ---------- + pixel_size : float + Reciprocal pixel size, 1/Angstroms per detector pixel. + ellipse : array-like | None + [e11, e12] elliptic distortion correction, see + :func:`calibrate_ellipse`. None applies no correction. + rotation_ccw_deg : float, default=0.0 + Diffraction-to-scan rotation in degrees, see + :func:`measure_scan_rotation`. + metadata : dict | None + Evidence and provenance, e.g. the reference phases, the matched + rings and their residual, and the detector binning. """ def __init__( @@ -850,7 +1020,19 @@ def apply(self, peaks_px, name: str = "bragg_peaks_calibrated"): return out def rebin(self, factor: float) -> "DiffractionCalibration": - """The same calibration for data binned by `factor` more than this one.""" + """The same calibration for data binned by `factor` more than this one. + + Parameters + ---------- + factor : float + Additional detector binning. The pixel size is multiplied by it; + the ellipse and rotation do not depend on binning. + + Returns + ------- + DiffractionCalibration + New calibration with ``metadata["binning"]`` updated. + """ md = dict(self.metadata) md["binning"] = md.get("binning", 1) * factor return DiffractionCalibration( @@ -946,7 +1128,7 @@ def calibrate( k_broadening: float = 0.01, bragg_k_power: float = 2.0, residual_tol: float = 0.01, - plot: bool = True, + plot: bool = False, figsize: tuple[float, float] = (13.0, 6.4), marker_size: float = 8.0, zone_axis=None, @@ -980,11 +1162,25 @@ def calibrate( fit_ellipse : bool, default=True Fit the elliptic distortion as well as the scale. n_iter : int, default=3 - Ellipse and scale refinement rounds. + Ellipse and scale refinement rounds. Each round fits the ellipse + left over after the current correction and composes the two. scale_search : tuple, default=(0.6, 1.7) Capture range of the coarse scan, as a multiple of the guess. + k_min, k_max : float, default=0.05, 1.3 + Range of scattering vectors fit, 1/Angstroms. The ellipse fit uses + at least 0.15 as its lower limit. + k_broadening : float, default=0.01 + Gaussian standard deviation of the reference rings in the fine scale + fit, 1/Angstroms; the coarse scan uses six times this. Measured rings + further than four times this from any reference ring are not counted + in the per-ring check. + bragg_k_power : float, default=2.0 + Peaks and rings are weighted by ``|q| ** bragg_k_power``. residual_tol : float, default=0.01 Per-ring residual rms above which the fit is reported as unreliable. + plot : bool, default=False + Show the ring comparison before and after the fit, for the first + reference crystal. figsize : tuple, default=(13, 6.4) Figure size. marker_size : float, default=8.0 @@ -995,10 +1191,22 @@ def calibrate( Fit only the rings of this zone, [uvw] or [UVTW] (see :func:`zone_reflections`). For a specimen near one zone axis everywhere, whose peaks hold no other rings. + returnfig : bool, default=False + With `plot`, also return the figure and axes. Returns ------- DiffractionCalibration + The pixel size, ellipse and rotation. ``metadata`` holds the matched + rings as (measured k, reference k, ratio), their residual rms and + whether the fit passed `residual_tol`. With `plot` and `returnfig`, + ``(cal, fig, axs)`` instead, axs of shape (2, 2). + + Warns + ----- + UserWarning + If fewer than three rings match or their residual rms exceeds + `residual_tol`. Notes ----- @@ -1015,7 +1223,7 @@ def calibrate( peaks_0 = peaks_to_calibrated(peaks_px, pixel_size_guess) # coarse: broad rings so the score has a single maximum over a wide range - scale, sc_coarse, score_coarse = _fit_scale( + scale, _, _ = _fit_scale( peaks_0, crystals, scale_search[0], @@ -1029,12 +1237,16 @@ def calibrate( for it in range(max(1, n_iter)): if fit_ellipse: pk = peaks_to_calibrated(peaks_px, pixel_size_guess * scale, ellipse=ellipse) - ellipse = calibrate_ellipse( + # the fit sees peaks with the current ellipse already applied, so + # it returns the residual distortion: compose it with the current + # correction instead of replacing it + residual = calibrate_ellipse( pk, k_min=max(k_min, 0.15), k_max=k_max, bragg_k_power=bragg_k_power ) + ellipse = _compose_ellipse(residual, ellipse) pk = peaks_to_calibrated(peaks_px, pixel_size_guess, ellipse=ellipse) half = 0.08 / (it + 1) - scale, sc_fine, score_fine = _fit_scale( + scale, _, _ = _fit_scale( pk, crystals, scale * (1 - half), @@ -1140,15 +1352,38 @@ def calibrate( return (cal, fig, axs) if returnfig else cal +# reflections closer than this in |g| (1/Angstroms) are drawn as one ring +_SHELL_STEP = 0.005 + + +def _shells(crystal: Crystal, bragg_k_power: float): + """Group a crystal's reflections into rings. + + Returns + ------- + radius : np.ndarray + Mean |g| of each ring, 1/Angstroms, ascending. + intensity : np.ndarray + Summed intensity of each ring, weighted by ``|g| ** bragg_k_power``. + index : np.ndarray + Ring index of every reflection. + """ + g = crystal.g_len.numpy() + ints = crystal.struct_factors_int.numpy() * g**bragg_k_power + _, index = np.unique(np.round(g / _SHELL_STEP).astype(np.int64), return_inverse=True) + index = index.ravel() + count = np.bincount(index) + radius = np.bincount(index, weights=g) / count + intensity = np.bincount(index, weights=ints) + return radius, intensity, index + + def _ring_shells(crystal: Crystal, k_min: float, k_max: float, bragg_k_power: float): """Ring radii of a crystal in (k_min, k_max) and their summed intensity, relative to the strongest ring in that range.""" - g = crystal.g_len.numpy() - ints = crystal.struct_factors_int.numpy() * g**bragg_k_power - shells = np.round(g / 0.005) * 0.005 - keep = (shells > k_min) & (shells < k_max) - uniq = np.unique(shells[keep]) - tot = np.array([ints[keep][shells[keep] == u].sum() for u in uniq]) + radius, intensity, _ = _shells(crystal, bragg_k_power) + keep = (radius > k_min) & (radius < k_max) + uniq, tot = radius[keep], intensity[keep] return uniq, tot / max(float(tot.max()), 1e-30) if tot.size else tot @@ -1322,9 +1557,16 @@ def plot_ring_comparison( Calibrated peaks (qx, qy, intensity) in 1/Angstroms. crystals : Crystal | list[Crystal] Reference crystal(s) with structure factors calculated. + k_min, k_max : float, default=0.1, 1.5 + Range of scattering vectors shown, 1/Angstroms. k_broadening : float | None None draws sharp lines at the ring positions; a value (1/Angstroms) draws the broadened ring profile instead. + bragg_k_power : float, default=2.0 + Measured peaks and reference rings are weighted by + ``|q| ** bragg_k_power``. + label_hkl : bool, default=True + Label the strongest rings by their Miller indices. label_min_intensity : float, default=0.05 Label rings whose summed intensity exceeds this fraction of the strongest ring. @@ -1334,11 +1576,18 @@ def plot_ring_comparison( zone_axis : sequence of int, optional Show only the rings of this zone, [uvw] or [UVTW] (see :func:`zone_reflections`). + figax : (fig, axs) | None + Existing figure and one axis per crystal. + + Returns + ------- + tuple + ``(fig, axs)``, axs a 1D array with one axis per crystal. """ import matplotlib.pyplot as plt xtls = _restrict(crystals, zone_axis) - k, hist = radial_histogram(peaks, k_min=k_min, k_max=k_max) + k, hist = radial_histogram(peaks, k_min=k_min, k_max=k_max, bragg_k_power=bragg_k_power) n = len(xtls) if figax is None: @@ -1348,15 +1597,13 @@ def plot_ring_comparison( fig, axs = figax axs = np.atleast_1d(axs) - for ci, (ax, xtl) in enumerate(zip(axs, xtls)): + for ax, xtl in zip(axs, xtls): ax.fill_between(k, hist / hist.max(), color="r", alpha=0.75, lw=0, label="measured") - hexagonal = xtl.laue_group in ("6/m", "6/mmm", "-3", "-3m") + hexagonal = xtl.hexagonal_matching g_len = xtl.g_len.numpy() ints = xtl.struct_factors_int.numpy() * g_len**bragg_k_power hkl_np = xtl.hkl.numpy() - shells = np.round(g_len / 0.01) * 0.01 - uniq = np.unique(shells) - shell_int = np.array([ints[shells == u].sum() for u in uniq]) + uniq, shell_int, shells = _shells(xtl, bragg_k_power) shell_int = shell_int / shell_int.max() if k_broadening is not None: @@ -1376,19 +1623,19 @@ def plot_ring_comparison( if label_hkl: # the strongest rings first, each at least 0.03 1/A from the # last, then drawn left to right - labeled_g: list[float] = [] + labeled: list[int] = [] for j in np.argsort(-shell_int): u, si = uniq[j], shell_int[j] - if len(labeled_g) >= n_labels: + if len(labeled) >= n_labels: break if u < k_min or u > k_max or si < label_min_intensity: continue - if any(abs(u - g0) < 0.03 for g0 in labeled_g): + if any(abs(u - uniq[j0]) < 0.03 for j0 in labeled): continue - labeled_g.append(u) - for rows, u in enumerate(sorted(labeled_g)): - in_shell = shells == u - idx = np.nonzero(in_shell)[0] + labeled.append(int(j)) + for rows, j in enumerate(sorted(labeled)): + u = uniq[j] + idx = np.nonzero(shells == j)[0] idx = idx[ints[idx] > 0.99 * ints[idx].max()] key = [tuple(-hkl_np[i]) for i in idx] best = idx[int(np.lexsort(np.array(key).T[::-1])[0])] @@ -1460,17 +1707,30 @@ def refine_calibration( 'rotation_deg' : residual detector rotation, 'ellipse' : (e11, e12) traceless ellipticity components, 'num_positions' : positions used. + + Raises + ------ + ValueError + If `strain_maps` is empty or no position passes the filters. """ As = [] for i, sm in enumerate(strain_maps): - A = np.stack([sm.u_array, sm.v_array], axis=-1) # (R, C, 2, 2) + A = np.stack([sm.g1_array, sm.g2_array], axis=-1) # (R, C, 2, 2) ok = np.isfinite(A).all(axis=(-2, -1)) dev = np.abs(A - np.eye(2)).max(axis=(-2, -1)) ok &= dev < max_strain if masks is not None and masks[i] is not None: ok &= np.asarray(masks[i]) > 0 As.append(A[ok]) + if not As: + raise ValueError("strain_maps is empty: pass at least one StrainMap.") A_all = np.concatenate(As, axis=0) + if A_all.shape[0] == 0: + raise ValueError( + "no positions left for the calibration residual: every strain fit " + f"failed, was masked out, or deviates from the identity by more than " + f"max_strain={max_strain:g}." + ) M = np.median(A_all, axis=0) scale = float(np.sqrt(np.abs(np.linalg.det(M)))) @@ -1515,9 +1775,24 @@ def plot_bragg_rings( (solid, then dashed line styles). n_rings : int, default=8 Number of rings per crystal, strongest first. + q_max : float | None + Half-width of the histogram, 1/Angstroms. Defaults to just beyond + the largest peak radius. + bins : int, default=400 + Number of histogram bins along each axis. + power : float, default=0.25 + The histogram is shown raised to this power, which brings out weak + rings next to the direct beam. zone_axis : sequence of int, optional Draw only the rings of this zone, [uvw] or [UVTW] (see :func:`zone_reflections`). + figax : (fig, ax) | None + Existing figure and axis. + + Returns + ------- + tuple + ``(fig, ax)``. """ import matplotlib.pyplot as plt @@ -1546,11 +1821,7 @@ def plot_bragg_rings( colors = ["r", "b", "g"] th = np.linspace(0, 2 * np.pi, 361) for ci, xtl in enumerate(xtls): - g_len = xtl.g_len.numpy() - ints = xtl.struct_factors_int.numpy() * g_len**2 - shells = np.round(g_len / 0.01) * 0.01 - uniq = np.unique(shells) - shell_int = np.array([ints[shells == u].sum() for u in uniq]) + uniq, shell_int, _ = _shells(xtl, 2.0) keep = uniq < q_max uniq, shell_int = uniq[keep], shell_int[keep] order = np.argsort(shell_int)[::-1][:n_rings] diff --git a/src/quantem/diffraction/crystal.py b/src/quantem/diffraction/crystal.py index 5e4913dec..51f938365 100644 --- a/src/quantem/diffraction/crystal.py +++ b/src/quantem/diffraction/crystal.py @@ -19,6 +19,8 @@ from __future__ import annotations import json +import warnings +from contextlib import contextmanager from importlib import resources from pathlib import Path @@ -34,16 +36,60 @@ # unicode combining overline, applies to the preceding character _B = "\u0305" +_EXCITATION_MODELS = ("gaussian", "slab") + + +@contextmanager +def _spglib_raises(): + """Within the block, spglib raises SpglibError on failure. + + spglib 2.7 reports failures by returning None and emitting a + DeprecationWarning on every call, success or not, unless + ``spglib.error.OLD_ERROR_HANDLING`` is False, which spglib 2.8 makes the + default. Every spglib call here is wrapped in try/except, so the flag is + switched for the block only and restored afterwards, leaving other + spglib users in the process unaffected. Older spglib versions without + the flag are left as they are. + """ + import spglib + + err = getattr(spglib, "error", None) + if err is None or not hasattr(err, "OLD_ERROR_HANDLING"): + yield + return + old = err.OLD_ERROR_HANDLING + err.OLD_ERROR_HANDLING = False + try: + yield + finally: + err.OLD_ERROR_HANDLING = old + def direction_indices( lat_real: torch.Tensor | np.ndarray, d, max_multiple: int = 12, atol: float = 2e-3 ) -> np.ndarray | None: - """Smallest integer [uvw] along a Cartesian direction, or None if the - direction is not a lattice direction with indices up to max_multiple. + """Smallest integer [uvw] along a Cartesian direction. + + Parameters + ---------- + lat_real : torch.Tensor | np.ndarray + Real-space lattice vectors as rows (3, 3), Angstroms. + d : array-like + Cartesian direction (3,) in the crystal frame; need not be + normalized. + max_multiple : int, default=12 + Largest multiplier tried to make the indices integer. + atol : float, default=2e-3 + Allowed deviation of the scaled indices from integers. Loosen it to + index the axes of a pseudo-symmetry, which are lattice directions of + an ideal parent but only nearly so in the real cell. - `atol` is the allowed deviation of the normalized indices from integers; - loosen it to index the axes of a pseudo-symmetry, which are lattice - directions of an ideal parent but only nearly so in the real cell.""" + Returns + ------- + np.ndarray | None + Integer [uvw] (3,) with no common factor, or None if `d` is not a + lattice direction with indices up to `max_multiple`. + """ A_T_inv = np.linalg.inv(np.asarray(lat_real, dtype=float).T) v = A_T_inv @ np.asarray(d, dtype=float) v = v / np.abs(v).max() @@ -57,8 +103,24 @@ def direction_indices( def format_direction(uvw, hexagonal: bool = False, mathtext: bool = True) -> str: - """Direction label such as [011] or [10-10], with overlines on negative - indices (mathtext for figures, combining overlines for text).""" + """Direction label such as [011] or [10-10], with overlines on negatives. + + Parameters + ---------- + uvw : array-like | None + Integer 3-index direction [uvw]. + hexagonal : bool, default=False + Write the 4-index [UVTW] symbol instead, see + :func:`miller_to_miller_bravais`. + mathtext : bool, default=True + Overlines as matplotlib mathtext (``$\\bar{1}$``) for figures; False + uses unicode combining overlines for plain text. + + Returns + ------- + str + The label, or an empty string for None. + """ if uvw is None: return "" ks = miller_to_miller_bravais(uvw) if hexagonal else np.asarray(uvw) @@ -75,6 +137,16 @@ def miller_to_miller_bravais(uvw: np.ndarray) -> np.ndarray: u = (2u' - v') / 3, v = (2v' - u') / 3, t = -(u + v), w = w', cleared to the smallest integer form. + + Parameters + ---------- + uvw : array-like + Integer 3-index directions, (3,) or (N, 3). + + Returns + ------- + np.ndarray + Integer [u v t w], (4,) or (N, 4). """ uvw = np.atleast_2d(np.asarray(uvw, dtype=float)) u = (2 * uvw[:, 0] - uvw[:, 1]) / 3 @@ -85,19 +157,30 @@ def miller_to_miller_bravais(uvw: np.ndarray) -> np.ndarray: gcd = np.gcd.reduce(np.abs(np.round(out)).astype(int), axis=1) gcd[gcd == 0] = 1 out = out / gcd[:, None] - return out.astype(int).squeeze() + return np.rint(out).astype(int).squeeze() def miller_bravais_to_miller(uvtw: np.ndarray) -> np.ndarray: """Convert 4-index [u v t w] direction indices to 3-index [u'v'w']. - u' = 2u + v, v' = 2v + u, w' = w (t is redundant: t = -(u + v)). + u' = 2u + v, v' = 2v + u, w' = w (t is redundant: t = -(u + v)), + cleared to the smallest integer form. + + Parameters + ---------- + uvtw : array-like + Integer 4-index directions, (4,) or (N, 4). + + Returns + ------- + np.ndarray + Integer [u'v'w'], (3,) or (N, 3). """ uvtw = np.atleast_2d(np.asarray(uvtw, dtype=float)) out = np.stack([2 * uvtw[:, 0] + uvtw[:, 1], 2 * uvtw[:, 1] + uvtw[:, 0], uvtw[:, 3]], axis=1) gcd = np.gcd.reduce(np.abs(np.round(out)).astype(int), axis=1) gcd[gcd == 0] = 1 - return (out / gcd[:, None]).astype(int).squeeze() + return np.rint(out / gcd[:, None]).astype(int).squeeze() # point group -> Laue class @@ -233,6 +316,44 @@ class Crystal(AutoSerialize): Build with `from_ase` or `from_cif`, then call `calculate_structure_factors` before generating patterns or orientation plans. + + Parameters + ---------- + atoms : ase.Atoms + The structure. Fractional site occupancies are read from + ``atoms.arrays['occupancy']`` when present (see :meth:`from_cif`). + name : str | None + Display name; defaults to the chemical formula. + symprec : float, default=1e-4 + spglib tolerance (Angstroms) for the cell's own symmetry. + pseudo_symmetry_tol : float | None, default=0.01 + Dimensionless distance tolerance for the symmetry used in + orientation matching: a fraction of the shortest lattice vector + within which atoms and lattice vectors are allowed to deviate from a + higher-symmetry parent (a 4 A cell with an atom at + (0.5, 0.5, 0.50001) is body centered at any tolerance above 1e-5). + Cells within it are matched with the parent group, so variants no + experiment can separate are never sampled as distinct orientations; + the library builders warn when the matching group differs from the + cell's own. None matches with the exact symmetry. + pseudo_symmetry_intensity_tol : float, default=0.05 + Largest intensity difference allowed between reflections that a + candidate pseudo-symmetry would make equivalent, as a fraction of + the strongest reflection's kinematical intensity |F|^2. Each extra + rotation is applied to every reflection within 2.0 1/A and each + intensity compared with its image's; if any pair differs by more + than this fraction, the orientations the rotation relates are + distinguishable and it is rejected. 0.05 merges only orientations + whose patterns differ by reflections at 5% of the strongest; 0.4 + also merges orientations told apart only by a reflection at 40%, + appropriate when that reflection is known to be weak or absent in + the data (stacking disorder, cation mixing). Candidates are the + relaxed-position group, the lattice's own holohedry, and the + holohedries of the parent lattices generated by the strong + reflections, so superstructure twin variants are tested as well. + The printout names the reflection pair that decides each candidate. + verbose : bool, default=True + Print :meth:`symmetry_summary` after the symmetry analysis. """ def __init__( @@ -244,40 +365,6 @@ def __init__( pseudo_symmetry_intensity_tol: float = 0.05, verbose: bool = True, ): - """ - Parameters - ---------- - symprec : float, default=1e-4 - spglib tolerance (Angstroms) for the cell's own symmetry. - pseudo_symmetry_tol : float | None, default=0.01 - Dimensionless distance tolerance for the symmetry used in - orientation matching: a fraction of the shortest lattice vector - within which atoms and lattice vectors are allowed to deviate - from a higher-symmetry parent (a 4 A cell with an atom at - (0.5, 0.5, 0.50001) is body centered at any tolerance above - 1e-5). Cells within it are matched with the parent group, so - variants no experiment can separate are never sampled as - distinct orientations; the library builders warn when the - matching group differs from the cell's own. None matches with - the exact symmetry. - pseudo_symmetry_intensity_tol : float, default=0.05 - Largest intensity difference allowed between reflections that a - candidate pseudo-symmetry would make equivalent, as a fraction of - the strongest reflection's kinematical intensity |F|^2. Each - extra rotation is applied to every reflection within 2.0 1/A and - each intensity compared with its image's; if any pair differs by - more than this fraction, the orientations the rotation relates - are distinguishable and it is rejected. 0.05 merges only - orientations whose patterns differ by reflections at 5% of the - strongest; 0.4 also merges orientations told apart only by a - reflection at 40%, appropriate when that reflection is known to - be weak or absent in the data (stacking disorder, cation mixing). - Candidates are the relaxed-position group, the lattice's own - holohedry, and the holohedries of the parent lattices generated - by the strong reflections, so superstructure twin variants are - tested as well. The printout names the reflection pair that - decides each candidate. - """ self.atoms = atoms self.name = name if name is not None else atoms.get_chemical_formula() self._pseudo_symmetry_tol = pseudo_symmetry_tol @@ -291,7 +378,8 @@ def __init__( occupancy = atoms.arrays.get("occupancy", np.ones(len(atoms))) self.occupancy = torch.as_tensor(np.asarray(occupancy, dtype=float)) - self._setup_symmetry(symprec, pseudo_symmetry_tol, pseudo_symmetry_intensity_tol) + with _spglib_raises(): + self._setup_symmetry(symprec, pseudo_symmetry_tol, pseudo_symmetry_intensity_tol) # the summary states any pseudo-symmetry adopted; when it has been # shown, the orientation plan does not warn about it again self._summary_shown = bool(verbose) @@ -306,8 +394,31 @@ def __init__( self.struct_factors: torch.Tensor | None = None self.struct_factors_int: torch.Tensor | None = None + # populated by calculate_dynamical_structure_factors + self.hkl_dyn: torch.Tensor | None = None + self.g_len_dyn: torch.Tensor | None = None + self.U_dyn: torch.Tensor | None = None + self.dyn_energy_ev: float | None = None + self.dyn_k_max: float | None = None + @classmethod def from_ase(cls, atoms: Atoms, name: str | None = None, **kwargs) -> "Crystal": + """Build a Crystal from an ase.Atoms object. + + Parameters + ---------- + atoms : ase.Atoms + The structure, e.g. from ``ase.build.bulk``. + name : str, optional + Display name; defaults to the chemical formula. + **kwargs + Passed to the Crystal constructor, e.g. `pseudo_symmetry_tol` or + `verbose`. + + Returns + ------- + Crystal + """ return cls(atoms, name=name, **kwargs) @classmethod @@ -343,6 +454,7 @@ def from_cif(cls, file_path: str | Path, name: str | None = None, **kwargs) -> " @property def volume(self) -> float: + """Unit cell volume, cubic Angstroms.""" return float(torch.abs(torch.linalg.det(self.lat_real))) @property @@ -389,12 +501,26 @@ def _setup_symmetry( self.positions_frac.numpy(), self.numbers.numpy(), ) - dataset = spglib.get_symmetry_dataset(cell, symprec=symprec) - self.spacegroup: str = f"{dataset.international} ({dataset.number})" - pg = spglib.get_pointgroup(dataset.rotations)[0].strip() + try: + dataset = spglib.get_symmetry_dataset(cell, symprec=symprec) + except Exception: + dataset = None + if dataset is None: + # no symmetry found at all (e.g. overlapping atoms): carry on in P1 + warnings.warn( + f"{self.name}: spglib found no symmetry at symprec={symprec:g}; " + "using P1. Check the structure for overlapping atoms.", + stacklevel=3, + ) + rotations = np.eye(3, dtype=np.intc)[None] + self.spacegroup: str = "P1 (1)" + else: + rotations = dataset.rotations + self.spacegroup = f"{dataset.international} ({dataset.number})" + pg = spglib.get_pointgroup(rotations)[0].strip() self.pointgroup: str = pg self.laue_group: str = _LAUE_CLASS.get(pg, "-1") - self.sym_quats = symmetry_quaternions(dataset.rotations, self.lat_real.numpy()) + self.sym_quats = symmetry_quaternions(rotations, self.lat_real.numpy()) self.pointgroup_matching = pg self.laue_group_matching = self.laue_group @@ -749,7 +875,7 @@ def projected_rotation_order( gz, iz = g[zol], inten[zol] if gz.shape[0] < 3: continue - i_max = float(iz.max()).__abs__() or 1.0 + i_max = abs(float(iz.max())) or 1.0 ux = torch.tensor( [[0.0, -u[2], u[1]], [u[2], 0.0, -u[0]], [-u[1], u[0], 0.0]], dtype=torch.float64, @@ -836,10 +962,17 @@ def matching_symmetry_warning(self) -> str | None: if self.pointgroup_matching == self.pointgroup: return None n_extra = self.sym_quats_matching.shape[0] // max(self.sym_quats.shape[0], 1) + # a partially accepted group has no Laue class of its own, and keeps + # the cell's: name it only when it differs + laue = ( + f"Laue class {self.laue_group_matching}, " + if self.laue_group_matching != self.laue_group + else "" + ) return ( f"{self.name}: orientation libraries are built with the " - f"pseudo-symmetry point group {self.pointgroup_matching} (Laue " - f"class {self.laue_group_matching}, found at pseudo_symmetry_tol = " + f"pseudo-symmetry point group {self.pointgroup_matching} ({laue}" + f"found at pseudo_symmetry_tol = " f"{self._pseudo_symmetry_tol:g} of the shortest lattice vector, " f"intensities matching within {self.pseudo_symmetry_report.get('intensity_mismatch', 0.0):.2f} " "of the strongest reflection), " @@ -865,9 +998,13 @@ def symmetry_summary(self) -> str: f" point group {self.pointgroup} (Laue class {self.laue_group})", ] if self.pointgroup_matching != self.pointgroup: - lines += [ - f" pseudo-symmetry {self.pointgroup_matching} " + laue = ( f"(Laue class {self.laue_group_matching}) " + if self.laue_group_matching != self.laue_group + else "" + ) + lines += [ + f" pseudo-symmetry {self.pointgroup_matching} {laue}" "-- used for orientation matching", ] rep = self.pseudo_symmetry_report @@ -1012,6 +1149,24 @@ def calculate_dynamical_structure_factors( Maximum |g| of stored factors; defaults to the kinematical k_max. For Bloch calculations with beams out to k, the couplings reach 2k, but the factors fall off fast and 1.5k is enough. + include_core : bool, default=True + Include the core-loss (inner-shell ionization) absorptive part. + include_phonon : bool, default=True + Include the phonon (thermal diffuse scattering) absorptive part. + + Returns + ------- + Crystal + self, for chaining. Sets ``hkl_dyn`` (N, 3) and ``g_len_dyn`` (N,) + for every reflection with |g| <= k_max including (000), + ``U_dyn`` (N,) complex128 in 1/Angstroms^2, and the + ``dyn_energy_ev`` and ``dyn_k_max`` they were computed for. + + Raises + ------ + RuntimeError + If `k_max` is None and :meth:`calculate_structure_factors` has + not been run. """ from quantem.diffraction.wk_scattering_factors import compute_WK_factor @@ -1110,9 +1265,10 @@ def generate_pattern( Unit quaternion (4,) rotating crystal vectors into the lab frame. energy_ev : float, default=300e3 Beam energy in eV. - sigma_excitation : float, default=0.02 + sigma_excitation : float, default=SIGMA_EXCITATION Excitation error tolerance (1/Angstroms) in the shape-factor - envelope exp(-s_g^2 / 2 sigma^2). + envelope exp(-s_g^2 / 2 sigma^2); the default is + :data:`quantem.diffraction.defaults.SIGMA_EXCITATION`. tol_excitation_mult : float, default=3.0 Include reflections with |s_g| below this multiple of sigma. k_max : float | None @@ -1141,12 +1297,26 @@ def generate_pattern( Returns ------- - dict with 'qx', 'qy', 'intensity', 'hkl', 's_g' (the central - excitation error, along the rod when `foil_normal` is given), 'a' - and 'b' (ring and disk sweep amplitudes). + dict + 'qx', 'qy' (1/Angstroms), 'intensity', 'hkl', 's_g' (the central + excitation error, along the rod when `foil_normal` is given), 'a' + and 'b' (ring and disk sweep amplitudes), one entry per excited + reflection. + + Raises + ------ + RuntimeError + If :meth:`calculate_structure_factors` has not been run. + ValueError + If `excitation_model` is not "gaussian" or "slab", or the slab + model is asked for without `thickness_A`. """ if self.g_vec is None: raise RuntimeError("Run calculate_structure_factors first.") + if excitation_model not in _EXCITATION_MODELS: + raise ValueError( + f"excitation_model must be one of {_EXCITATION_MODELS}, got {excitation_model!r}" + ) from quantem.diffraction.illumination import ( averaged_gaussian_intensity_envelope, excitation_coefficients, diff --git a/src/quantem/diffraction/crystal_map.py b/src/quantem/diffraction/crystal_map.py index 3c8bcd068..2452a5204 100644 --- a/src/quantem/diffraction/crystal_map.py +++ b/src/quantem/diffraction/crystal_map.py @@ -195,7 +195,25 @@ def from_vectors( @classmethod def from_orientation_maps(cls, orientation_maps: list[OrientationMap]) -> "CrystalMap": - """Wrap maps that were built and matched by hand.""" + """Wrap maps that were built and matched by hand. + + Parameters + ---------- + orientation_maps : list of OrientationMap + One per crystal, all sharing a scan shape and with unique + crystal names. The scattering-vector limit is taken from the + crystals, which must share one. + + Returns + ------- + CrystalMap + + Raises + ------ + ValueError + If the list is empty, names repeat, scan shapes differ, or the + crystals have no common k_max. + """ return cls(list(orientation_maps), _token=cls._token) # ------------------------------------------------------------------ @@ -344,15 +362,39 @@ def refine_orientations( oms = self.orientation_maps if competitive_margin is None or len(oms) < 2 or "positions" in kwargs: return self._fanout("refine_orientations", overrides, **kwargs) + overrides = self._check_overrides(overrides) corr = np.stack([om.corr[..., 0].numpy() for om in oms]) best = corr.max(axis=0) for i, om in enumerate(oms): - kw = dict(kwargs) - kw.update((overrides or {}).get(om.crystal.name, {})) + kw = {**kwargs, **overrides.get(om.crystal.name, {})} kw.setdefault("positions", corr[i] >= best - competitive_margin) om.refine_orientations(**kw) return self + def _check_overrides(self, overrides: dict | None) -> dict: + """Per-crystal overrides, checked against the crystal names. + + Parameters + ---------- + overrides : dict or None + Keyword arguments per crystal name. + + Returns + ------- + dict + `overrides`, or an empty dict for None. + + Raises + ------ + KeyError + If an override names a crystal not in this map. + """ + overrides = overrides or {} + unknown = set(overrides) - set(self.names) + if unknown: + raise KeyError(f"overrides name unknown crystals {sorted(unknown)}; have {self.names}") + return overrides + def _fanout(self, method: str, overrides: dict | None, **kwargs) -> "CrystalMap": """Call ``method`` on every OrientationMap, with per-crystal overrides. @@ -375,10 +417,7 @@ def _fanout(self, method: str, overrides: dict | None, **kwargs) -> "CrystalMap" KeyError If an override names a crystal not in this map. """ - overrides = overrides or {} - unknown = set(overrides) - set(self.names) - if unknown: - raise KeyError(f"overrides name unknown crystals {sorted(unknown)}; have {self.names}") + overrides = self._check_overrides(overrides) for om in self.orientation_maps: kw = {**kwargs, **overrides.get(om.crystal.name, {})} getattr(om, method)(**kw) @@ -396,7 +435,11 @@ def fit(self, **kwargs) -> "CrystalMap": return self def refine_dynamical( - self, mask=None, k_max_coupling: float | None = None, **kwargs + self, + mask=None, + k_max: float | None = None, + k_max_coupling: float | None = None, + **kwargs, ) -> "CrystalMap": """Dynamical refinement of orientation, thickness, strain and phase. @@ -421,7 +464,7 @@ def refine_dynamical( None refines every matched position, which takes hours. k_max : float, optional Largest |g| (1/Angstroms) of the beams in the Bloch calculation. - Defaults to the k_max of the kinematical simulation. Cutting it + None takes the k_max of the kinematical simulation. Cutting it low saves time but drops beams that carry real dynamical coupling. k_max_coupling : float, optional @@ -442,7 +485,7 @@ def refine_dynamical( from quantem.diffraction import bloch pm = self._require_fit("refine_dynamical()") - k_max = float(kwargs.pop("k_max", None) or self._k_max_or_crystals()) + k_max = float(k_max if k_max is not None else self._k_max_or_crystals()) k_c = float(k_max_coupling if k_max_coupling is not None else 1.5 * k_max) energy_ev = self.orientation_maps[0].energy_ev for om in self.orientation_maps: @@ -650,15 +693,30 @@ def mask(self, phase=None, signal_range="auto", gamma: float = SHADE_GAMMA) -> n ------- np.ndarray ``(scan_row, scan_col)`` mask in [0, 1]. + + Raises + ------ + KeyError + If `phase` names no crystal in this map. """ conf = self.signal_confidence(signal_range, gamma) if phase is None: return conf - i = self.names.index(phase) if isinstance(phase, str) else int(phase) + i = self._phase_indices(phase)[0] return (self.phase_index == i) * conf def phase_fractions(self) -> dict[str, float]: - """Fraction of the scan won by each crystal, plus the unindexed share.""" + """Fraction of the scan won by each crystal, plus the unindexed share. + + These are area fractions of the phase decision, counted over every + probe position. The per-position model weights are + `phases.crystal_weights`. + + Returns + ------- + dict[str, float] + "unindexed" and one entry per crystal name; the values sum to 1. + """ ph = self.phase_index out = {"unindexed": float((ph == -1).mean())} for i, n in enumerate(self.names): @@ -747,8 +805,10 @@ def plot_orientation( Returns ------- - list of tuple - One ``(fig, ax)`` per crystal and direction. + tuple or list of tuple + A single ``(fig, ax)`` when `phase` names one crystal and one + direction is given; otherwise one ``(fig, ax)`` per crystal and + direction, the same rule as :meth:`plot_pole_figure`. """ dirs = [direction] if isinstance(direction, str) else list(direction) out = [] @@ -758,7 +818,7 @@ def plot_orientation( out.append( self.orientation_maps[i].plot_orientation(direction=d, mask=m, **kwargs) ) - return out + return out[0] if phase is not None and len(out) == 1 else out def plot_pole_figure( self, @@ -774,7 +834,8 @@ def plot_pole_figure( Parameters ---------- pole : tuple of int, default=(0, 0, 1) - Crystal direction plotted, in Miller indices. + Crystal direction plotted, in Miller indices [uvw] or [uvtw] of + each crystal. phase : int or str, optional Restrict to one crystal. None (default) plots all of them. mask : np.ndarray, optional @@ -790,14 +851,15 @@ def plot_pole_figure( Returns ------- tuple or list of tuple - ``(fig, ax)`` when `phase` names one crystal, otherwise one - ``(fig, ax)`` per crystal. + A single ``(fig, ax)`` when `phase` names one crystal; otherwise + one ``(fig, ax)`` per crystal, the same rule as + :meth:`plot_orientation`. """ out = [] for i in self._phase_indices(phase): m = mask if mask is not None else self.mask(i, signal_range, shade_gamma) out.append(self.orientation_maps[i].plot_pole_figure(pole=pole, mask=m, **kwargs)) - return out[0] if phase is not None else out + return out[0] if phase is not None and len(out) == 1 else out def plot_matches(self, positions, phase=None, **kwargs): """Matched patterns at a few probe positions, over the measured peaks. @@ -817,9 +879,11 @@ def plot_matches(self, positions, phase=None, **kwargs): phase : int or str, optional Restrict to one crystal. None (default) shows all of them. matches : tuple of int, default=(0, 1) - Which matches of each crystal to draw. With `num_matches` of 2, - (0, 1) shows the best and the residual match side by side, which - is how a probe straddling two grains shows itself. + Which matches of each crystal to draw; indices a crystal does not + hold are skipped, so the default draws one panel per crystal after + `num_matches=1`. With `num_matches` of 2, (0, 1) shows the best + and the residual match side by side, which is how a probe + straddling two grains shows itself. dataset : Dataset4dstem, optional Show the recorded diffraction pattern behind the overlay. norm : dict or str, optional @@ -1048,11 +1112,22 @@ def _phase_indices(self, phase) -> list[int]: Returns ------- list of int + + Raises + ------ + KeyError + If `phase` names no crystal in this map, by name or index. """ if phase is None: return list(range(len(self.orientation_maps))) - i = self.names.index(phase) if isinstance(phase, str) else int(phase) - return [i] + if isinstance(phase, str): + if phase not in self.names: + raise KeyError(f"no crystal named {phase!r}; have {self.names}") + return [self.names.index(phase)] + i = int(phase) + if not -len(self.names) <= i < len(self.names): + raise KeyError(f"no crystal {i}; have {len(self.names)}") + return [i % len(self.names)] # ------------------------------------------------------------------ # checkpointing @@ -1061,12 +1136,21 @@ def _phase_indices(self, phase) -> list[int]: def save(self, path, mode: str = "w", include_plan: bool = False, **kwargs): """Save the whole analysis to one file. + Load it back with :func:`quantem.core.io.serialize.load`. + Parameters ---------- + path : str or Path + Target path; a ".zip" extension writes one zip file, anything + else a directory. + mode : {"w", "o"}, default="w" + "w" refuses to replace an existing file, "o" overwrites it. include_plan : bool, default=False The correlation plan dominates the file size and is rebuilt in seconds by :meth:`build_plan`, so it is dropped by default. Pass True to keep it and reload a map ready to match again. + **kwargs + Passed to :meth:`AutoSerialize.save`, e.g. `compression_level`. """ if include_plan: return AutoSerialize.save(self, path, mode=mode, **kwargs) diff --git a/src/quantem/diffraction/defaults.py b/src/quantem/diffraction/defaults.py index 3c14197ee..df94766fa 100644 --- a/src/quantem/diffraction/defaults.py +++ b/src/quantem/diffraction/defaults.py @@ -33,10 +33,28 @@ def resolve(value, key: str, *sources: dict | None, default=None): - """First non-None of: the explicit `value`, `key` in each metadata - source (dicts, searched in order, None sources skipped), the default. + """First non-None of an explicit value, metadata entries and a default. + The refinement stages call this so a parameter left as None inherits - the value the previous stage used.""" + the value the previous stage used. + + Parameters + ---------- + value : object + Explicit value; returned whenever it is not None. + key : str + Key looked up in each source. + *sources : dict | None + Metadata dicts, searched in order; None sources are skipped, as are + entries that are None. + default : object, optional + Returned when nothing else is found. + + Returns + ------- + object + The resolved value. + """ if value is not None: return value for src in sources: diff --git a/src/quantem/diffraction/digital_dark_field.py b/src/quantem/diffraction/digital_dark_field.py index bd57d1707..3297056e6 100644 --- a/src/quantem/diffraction/digital_dark_field.py +++ b/src/quantem/diffraction/digital_dark_field.py @@ -7,8 +7,8 @@ - Virtual apertures: peaks within a radius of a set of aperture positions, usually a lattice built from two reciprocal lattice vectors - (fit_lattice, lattice_distance, aperture_array, aperture_array_subtract, - aperture_ddf_image). + (refine_lattice_vectors, lattice_distance, aperture_array, + aperture_array_subtract, aperture_ddf_image). - Polar selection: peaks within a ring of radius q, optionally restricted to a range of azimuthal angles (add_polar_fields, polar_mask, radial_ddf_image). @@ -20,12 +20,31 @@ components (L3) (ddf_images, cluster_coms, cluster_centers, group_ddf_images, assign_grain_labels). +The aperture, polar and DDF image functions are ports of Ian MacLaren's +digital dark field functions in py4DSTEM (aperture_array_generator, +aperture_array_subtract, DDFimage, pointlist_to_array with rphi=True, +DDF_radial_image and DDFradialazimuthimage), rewritten to read the peaks +from a Vector. + All functions read the peaks from a Vector with one cell per probe position. The diffraction coordinates are given by `q_fields`, which defaults to ("qx", "qy") for calibrated peaks or ("q_row", "q_col") for peaks in detector pixels. Boolean masks are aligned with the flattened rows of the Vector, so they can be combined with & and | and passed to ddf_image or to quantem.core.utils.clustering.filter_rows. + +Where a function needs the diffraction origin (`center=None`), calibrated +("qx", "qy") peaks are taken to be relative to the direct beam, so the origin +is (0, 0). Peaks in detector pixels ("q_row", "q_col") use the "origin_ref" +stored in the Vector metadata by BraggVectors.correct_peak_origins; without +it, pass `center` explicitly. + +The azimuth qphi of the polar functions follows py4DSTEM's DDF functions: +qphi = atan2(-q0, q1) in degrees, measured anticlockwise from the +col +(right) direction as the pattern is displayed with rows increasing +downward, so phi ranges written for py4DSTEM carry over unchanged. This is +not the azimuth used by quantem.diffraction.calibration, atan2(q1, q0), +which is measured from the +row axis toward +col. """ from __future__ import annotations @@ -60,6 +79,27 @@ def _resolve_q_fields(vector, q_fields) -> tuple[str, str]: ) +def _resolve_center(vector, q_fields, center) -> np.ndarray: + """Diffraction origin: `center` if given, else (0, 0) or the stored origin_ref. + + Calibrated fields are relative to the direct beam already. Detector-pixel + fields ("q_row", "q_col") need the common origin that + BraggVectors.correct_peak_origins stores as "origin_ref". + """ + if center is not None: + return np.asarray(center, dtype=np.float64).reshape(2) + if tuple(q_fields) == ("q_row", "q_col"): + origin_ref = vector.metadata.get("origin_ref") + if origin_ref is None: + raise ValueError( + "Peaks are in detector pixels and carry no 'origin_ref'; pass " + "center=(row, col) of the direct beam, or correct the origins with " + "BraggVectors.correct_peak_origins first." + ) + return np.asarray(origin_ref, dtype=np.float64).reshape(2) + return np.zeros(2) + + def _q_coordinates(vector, q_fields=None, center=(0.0, 0.0)) -> np.ndarray: """(N, 2) diffraction coordinates of every flattened row, relative to center.""" q_fields = _resolve_q_fields(vector, q_fields) @@ -115,7 +155,7 @@ def aperture_array( g2=None, mode: str = "array", center=(0.0, 0.0), - shift=(0, 0), + shift=(0.0, 0.0), n1_range: tuple[int, int] = (-5, 5), n2_range: tuple[int, int] = (-5, 5), radius_range: tuple[float, float] = (0.0, np.inf), @@ -135,8 +175,9 @@ def aperture_array( Parameters ---------- g1, g2 : array-like of float - (2,) lattice vectors in the same coordinates as the peaks. g2 is not - needed for mode="line". + (2,) lattice vectors in the same coordinates as the peaks. g2 is + required for mode="array", ignored for mode="line", and optional for + mode="single" (where it is only used with a nonzero s2). mode : {"array", "line", "single"}, default="array" "array" places a 2D lattice of apertures over n1_range and n2_range, "line" places a row of apertures along g1 over n1_range (a systematic @@ -144,8 +185,10 @@ def aperture_array( at (s1 g1 + s2 g2). center : array-like of float, default=(0, 0) (2,) origin of the lattice, normally the direct beam. - shift : (int, int), default=(0, 0) - Lattice offset (s1, s2) in multiples of g1 and g2. + shift : (float, float), default=(0, 0) + Lattice offset (s1, s2) in multiples of g1 and g2; fractional values + place apertures between lattice points, for example (0.5, 0.5) for a + centered superlattice. n1_range, n2_range : (int, int), default=(-5, 5) Inclusive range of lattice multiples of g1 and g2. radius_range : (float, float), default=(0, inf) @@ -162,13 +205,15 @@ def aperture_array( np.ndarray (N, 2) aperture positions. """ + if mode == "array" and g2 is None: + raise ValueError('mode="array" needs both g1 and g2.') g1 = np.asarray(g1, dtype=np.float64) g2 = np.zeros(2) if g2 is None else np.asarray(g2, dtype=np.float64) - s1, s2 = shift + s1, s2 = (float(v) for v in shift) if mode == "single": n1 = np.array([0]) n2 = np.array([0]) - elif mode in ("line", "2-beam"): + elif mode == "line": n1 = np.arange(n1_range[0], n1_range[1] + 1) n2 = np.zeros_like(n1) elif mode == "array": @@ -195,11 +240,11 @@ def aperture_array( return positions[keep] -def fit_lattice( +def refine_lattice_vectors( peaks, g1, g2, - center=(0.0, 0.0), + center=None, radius: float = 6.0, n1_range: tuple[int, int] = (-5, 5), n2_range: tuple[int, int] = (-5, 5), @@ -207,9 +252,12 @@ def fit_lattice( q_fields=None, intensity_field: str = "intensity", ): - """Refine two lattice vectors against the peaks of every probe position. + """Refine one pair of lattice vectors against the peaks of the whole scan. - For each lattice point n1 g1 + n2 g2 (excluding the origin) we take the + This is a single global refinement, used to place virtual apertures; it is + not the per-position lattice fit of BraggVectors.fit_lattice used for + strain mapping. For each lattice point n1 g1 + n2 g2 (excluding the + origin) we take the intensity-weighted mean position of all peaks within `radius` of it, summed over the scan, then solve for g1 and g2 by weighted least squares with each point weighted by its summed intensity. Repeating this a few @@ -222,11 +270,13 @@ def fit_lattice( peaks : Vector Peaks with one cell per probe position. g1, g2 : array-like of float - (2,) starting lattice vectors. - center : array-like of float, default=(0, 0) - (2,) lattice origin, held fixed. + (2,) starting lattice vectors, in the units of the peak coordinates. + center : array-like of float, optional + (2,) lattice origin, held fixed. None uses (0, 0) for calibrated peaks + and the stored "origin_ref" for peaks in detector pixels. radius : float, default=6.0 - Search radius around each lattice point. + Search radius around each lattice point, in the units of the peak + coordinates. n1_range, n2_range : (int, int), default=(-5, 5) Inclusive range of lattice multiples used in the fit. num_iterations : int, default=3 @@ -241,6 +291,8 @@ def fit_lattice( g1, g2 : np.ndarray (2,) refined lattice vectors. """ + q_fields = _resolve_q_fields(peaks, q_fields) + center = _resolve_center(peaks, q_fields, center) q = _q_coordinates(peaks, q_fields, center) w = peaks.select_fields(intensity_field).numpy()[:, 0].astype(np.float64).clip(min=0) n1, n2 = np.meshgrid( @@ -485,8 +537,10 @@ def draw(p, c): # --------------------------------------------------------------------------- # -def _polar_coordinates(peaks, q_fields=None, center=(0.0, 0.0)): - """(qr, qphi) of every flattened row, with qphi in degrees.""" +def _polar_coordinates(peaks, q_fields=None, center=None): + """(qr, qphi) of every flattened row, with qphi = atan2(-q0, q1) in degrees.""" + q_fields = _resolve_q_fields(peaks, q_fields) + center = _resolve_center(peaks, q_fields, center) q = _q_coordinates(peaks, q_fields, center) qr = np.hypot(q[:, 0], q[:, 1]) qphi = np.degrees(np.arctan2(-q[:, 0], q[:, 1])) @@ -496,15 +550,17 @@ def _polar_coordinates(peaks, q_fields=None, center=(0.0, 0.0)): def add_polar_fields( peaks, q_fields=None, - center=(0.0, 0.0), + center=None, names: tuple[str, str] = ("qr", "qphi"), ): """Copy of the peaks with polar coordinate fields added. The radius qr has the units of the diffraction coordinates. The angle - qphi is in degrees, measured anticlockwise from the +col (right) direction - as the pattern is displayed with rows increasing downward, over the range - (-180, 180]. + qphi = atan2(-q0, q1) is in degrees, measured anticlockwise from the +col + (right) direction as the pattern is displayed with rows increasing + downward, over the range (-180, 180]. This is py4DSTEM's DDF convention + (see the module docstring); it differs from the azimuth used in + quantem.diffraction.calibration. Parameters ---------- @@ -512,8 +568,10 @@ def add_polar_fields( Peaks with one cell per probe position. q_fields : (str, str), optional Diffraction coordinate fields. - center : array-like of float, default=(0, 0) - (2,) origin of the polar coordinates, normally the direct beam. + center : array-like of float, optional + (2,) origin of the polar coordinates, normally the direct beam. None + uses (0, 0) for calibrated peaks and the stored "origin_ref" for peaks + in detector pixels. names : (str, str), default=("qr", "qphi") Names of the new fields. @@ -535,7 +593,7 @@ def polar_mask( tol: float = 1.0, phi_range: tuple[float, float] | None = None, q_fields=None, - center=(0.0, 0.0), + center=None, ) -> np.ndarray: """Peaks inside a ring, optionally restricted to a range of angles. @@ -553,8 +611,9 @@ def polar_mask( phi_0 > phi_1 the range wraps through 180 degrees. q_fields : (str, str), optional Diffraction coordinate fields. - center : array-like of float, default=(0, 0) - (2,) origin of the polar coordinates. + center : array-like of float, optional + (2,) origin of the polar coordinates. None uses (0, 0) for calibrated + peaks and the stored "origin_ref" for peaks in detector pixels. Returns ------- @@ -578,13 +637,31 @@ def radial_ddf_image( tol: float = 1.0, phi_range: tuple[float, float] | None = None, q_fields=None, - center=(0.0, 0.0), + center=None, intensity_field: str = "intensity", ) -> np.ndarray: """Digital dark field image from a ring of diffraction space. - Equivalent to ddf_image(peaks, polar_mask(peaks, q_radius, tol, phi_range)). - See polar_mask for the parameters. + Equivalent to ddf_image(peaks, polar_mask(peaks, q_radius, tol, + phi_range, q_fields, center)). + + Parameters + ---------- + peaks : Vector + Peaks with one cell per probe position. + q_radius : float + Ring radius, in the units of the diffraction coordinates. + tol : float, default=1.0 + Half width of the ring, in the same units. + phi_range : (float, float), optional + Angular range (phi_0, phi_1) in degrees, as in polar_mask. + q_fields : (str, str), optional + Diffraction coordinate fields. + center : array-like of float, optional + (2,) origin of the polar coordinates. None uses (0, 0) for calibrated + peaks and the stored "origin_ref" for peaks in detector pixels. + intensity_field : str, default="intensity" + Field summed at each probe position. Returns ------- @@ -614,13 +691,18 @@ def cluster_coms( ---------- labeled : Vector Vector carrying a cluster label field (from cluster_vector). + label_field : str, default="cluster" + Field holding the cluster labels; negative labels are ignored. + intensity_field : str, default="intensity" + Field used as the weight of each peak when `weighted` is True. weighted : bool, default=True Weight the center of mass by peak intensity. Returns ------- coms : np.ndarray - (K, 2) scan-coordinate centers of mass, ordered by cluster id. + (K, 2) centers of mass in scan (row, col) pixels, ordered by cluster + id. K is the largest label plus one; (0, 2) when no peak is labeled. sizes : np.ndarray (K,) number of peaks per cluster. """ @@ -630,7 +712,7 @@ def cluster_coms( w = flat[:, fields.index(intensity_field)].clip(min=0) if weighted else None rc = _scan_cells(labeled).astype(float) - n = labels.max() + 1 + n = max(int(labels.max()) + 1, 0) if labels.size else 0 coms = np.zeros((n, 2)) sizes = np.zeros(n, dtype=int) for k in range(n): @@ -667,13 +749,16 @@ def cluster_centers( Returns ------- np.ndarray - (K, 2) mean diffraction positions, ordered by cluster id. + (K, 2) mean diffraction positions, ordered by cluster id; (0, 2) when + no peak is labeled. """ q = _q_coordinates(labeled, q_fields) labels = labeled.select_fields(label_field).numpy()[:, 0].astype(int) w = labeled.select_fields(intensity_field).numpy()[:, 0].astype(np.float64).clip(min=0) m = labels >= 0 - n = labels.max() + 1 + n = int(labels[m].max()) + 1 if m.any() else 0 + if n == 0: + return np.zeros((0, 2)) wsum = np.maximum(np.bincount(labels[m], weights=w[m], minlength=n), 1e-12) return np.stack( [np.bincount(labels[m], weights=w[m] * q[m, k], minlength=n) / wsum for k in range(2)], @@ -689,6 +774,17 @@ def ddf_images( ) -> np.ndarray: """Digital dark field images: per-cluster summed intensity per position. + Parameters + ---------- + labeled : Vector + Peaks carrying a cluster label field (from cluster_vector). + cluster_ids : int or array-like of int + Cluster labels to image, one image each, in this order. + label_field : str, default="cluster" + Field holding the cluster labels. + intensity_field : str, default="intensity" + Field summed at each probe position. Negative values are clipped to 0. + Returns ------- np.ndarray @@ -887,8 +983,10 @@ def plot_cluster_scatter( Marker opacity. Values well below 1 show the dense regions of large datasets. center : array-like of float, optional - (2,) diffraction origin at the middle of the plot. Defaults to the - "origin_ref" stored by BraggVectors.correct_peak_origins, or (0, 0). + (2,) diffraction origin at the middle of the plot. None uses (0, 0) + for calibrated peaks and the "origin_ref" stored by + BraggVectors.correct_peak_origins for peaks in detector pixels (or the + middle of the peak positions when there is none). figax : (Figure, Axes), optional Axes to draw into. @@ -897,10 +995,12 @@ def plot_cluster_scatter( fig, ax """ q_fields = _resolve_q_fields(labeled, q_fields) - if center is None: - center = labeled.metadata.get("origin_ref", (0.0, 0.0)) - center = np.asarray(center, dtype=np.float64) q = labeled.select_fields(*q_fields).numpy().astype(np.float64) + try: + center = _resolve_center(labeled, q_fields, center) + except ValueError: + # pixel peaks without a stored origin: frame the peaks themselves + center = 0.5 * (q.min(axis=0) + q.max(axis=0)) if q.size else np.zeros(2) labels = labeled.select_fields(label_field).numpy()[:, 0].astype(int) q0, q1 = q[:, 0], q[:, 1] @@ -923,7 +1023,7 @@ def plot_cluster_scatter( if show_unclustered: m = labels < 0 ax.scatter(q1[m], q0[m], s=point_size, color="0.85", lw=0) - n = labels.max() + 1 + n = max(int(labels.max()) + 1, 0) if labels.size else 0 n_show = n if max_clusters is None else min(n, max_clusters) cmap = plt.get_cmap("hsv") rng = np.random.default_rng(0) diff --git a/src/quantem/diffraction/disk_detection.py b/src/quantem/diffraction/disk_detection.py index 7459756d9..14bcf3599 100644 --- a/src/quantem/diffraction/disk_detection.py +++ b/src/quantem/diffraction/disk_detection.py @@ -1,3 +1,17 @@ +"""Template-matching Bragg disk detection for 4D-STEM, in torch. + +Each diffraction pattern is cross-correlated with a probe template in Fourier +space (optionally as a hybrid or phase correlation, with a Fourier high-pass +background removal and low-pass smoothing), local maxima of the correlation map +are kept as candidate disks, and each one is refined to subpixel precision by a +parabolic fit and then by DFT upsampling of the Fourier product. The approach +follows the multicorr / ``find_Bragg_disks`` routines of py4DSTEM, which use the +single-step DFT upsampling of Guizar-Sicairos, Thurman and Fienup, "Efficient +subpixel image registration algorithms", Optics Letters 33, 156 (2008). +Functions here are used by :class:`~quantem.diffraction.bragg_vectors.BraggVectors`. +Peak coordinates are ``(row, col)`` in detector pixels. +""" + from __future__ import annotations import numpy as np @@ -350,6 +364,16 @@ def cross_correlation( ``(H, W)`` diffraction pattern. template_ft : torch.Tensor ``(H, W)`` pre-computed template FT from :func:`template_fourier`. + background_sigma : float, optional + Width in pixels of the Gaussian whose smoothed copy of the correlation is + subtracted (a Fourier high-pass). ``None`` or ``0`` disables it. + corr_power : float, default=1.0 + Exponent applied to the magnitude of the Fourier product: 1 is the plain + cross-correlation, 0 the phase correlation, and values in between the + hybrid correlation. + sigma_cc : float, optional + Width in pixels of a Gaussian smoothing of the correlation map (a Fourier + low-pass). ``None`` or ``0`` disables it. Returns ------- @@ -357,8 +381,8 @@ def cross_correlation( ``(H, W)`` real-space correlation map ``relu(real(ifft2(m)))`` (used for peak finding). m : torch.Tensor - ``(H, W)`` Fourier-domain product ``fft2(dp) * template_ft`` (used for DFT - subpixel refinement). + ``(H, W)`` Fourier-domain product ``fft2(dp) * template_ft`` after the + ``corr_power`` and the filters (used for DFT subpixel refinement). """ dp = torch.as_tensor(dp) m = _apply_corr_power(torch.fft.fft2(dp) * template_ft, corr_power) @@ -386,6 +410,16 @@ def cross_correlation_batch( ``(B, H, W)`` stack of diffraction patterns. template_ft : torch.Tensor ``(H, W)`` pre-computed template FT from :func:`template_fourier`. + background_sigma : float, optional + Width in pixels of the Gaussian whose smoothed copy of the correlation is + subtracted (a Fourier high-pass). ``None`` or ``0`` disables it. + corr_power : float, default=1.0 + Exponent applied to the magnitude of the Fourier product: 1 is the plain + cross-correlation, 0 the phase correlation, and values in between the + hybrid correlation. + sigma_cc : float, optional + Width in pixels of a Gaussian smoothing of the correlation map (a Fourier + low-pass). ``None`` or ``0`` disables it. Returns ------- @@ -421,6 +455,16 @@ def _corr_map_rfft( ``(H, W)`` or ``(B, H, W)`` diffraction pattern(s). template_ft : torch.Tensor ``(H, W)`` pre-computed template FT from :func:`template_fourier`. + background_sigma : float, optional + Width in pixels of the Gaussian whose smoothed copy of the correlation is + subtracted (a Fourier high-pass). ``None`` or ``0`` disables it. + corr_power : float, default=1.0 + Exponent applied to the magnitude of the Fourier product: 1 is the plain + cross-correlation, 0 the phase correlation, and values in between the + hybrid correlation. + sigma_cc : float, optional + Width in pixels of a Gaussian smoothing of the correlation map (a Fourier + low-pass). ``None`` or ``0`` disables it. Returns ------- @@ -472,9 +516,9 @@ def detect_disks( Upsampling factor for the ``"upsample"`` subpixel refinement. max_num_peaks : int, default=1000 Maximum number of peaks to keep (after intensity sorting). - background_sigma : float | None + background_sigma : float, optional Width in pixels of the smoothed correlation background subtracted - before peak finding. + before peak finding. ``None`` (default) disables it. corr_power : float, default=1.0 Correlation type: 1 the plain cross-correlation, 0 the phase correlation, in between the hybrid correlation. Below 1 the weak disks @@ -557,9 +601,9 @@ def detect_disks_batch( Upsampling factor for the ``"upsample"`` subpixel refinement. max_num_peaks : int, default=1000 Maximum number of peaks to keep per pattern (after intensity sorting). - background_sigma : float | None + background_sigma : float, optional Width in pixels of the smoothed correlation background subtracted - before peak finding. + before peak finding. ``None`` (default) disables it. corr_power : float, default=1.0 Correlation type: 1 cross-correlation, 0 phase correlation, in between hybrid (see :func:`detect_disks`). diff --git a/src/quantem/diffraction/illumination.py b/src/quantem/diffraction/illumination.py index 35c24e6e9..54f60aeff 100644 --- a/src/quantem/diffraction/illumination.py +++ b/src/quantem/diffraction/illumination.py @@ -82,7 +82,23 @@ def excitation_coefficients( the same affine model. Without illumination, c_g is the static excitation error and a_g = b_g = 0. - Returns (c, a, b) as float arrays (N,). + Parameters + ---------- + g_lab : torch.Tensor | np.ndarray + Lab-frame reciprocal vectors (N, 3), 1/Angstroms, with the beam + along -z. + energy_ev : float + Beam energy, eV. + precession_deg : float, default=0.0 + Precession semi-angle, degrees. + semiconv_mrad : float, default=0.0 + Convergence semi-angle, mrad. + + Returns + ------- + c, a, b : np.ndarray + Central excitation error, ring amplitude and disk amplitude (N,), + 1/Angstroms. """ g = np.asarray( g_lab.detach().cpu().numpy() if isinstance(g_lab, torch.Tensor) else g_lab, dtype=float @@ -118,14 +134,19 @@ def relrod_factor(g_lab, n_lab, energy_ev: float, precession_deg: float = 0.0): Parameters ---------- g_lab : torch.Tensor | np.ndarray - Lab-frame reciprocal vectors (..., 3). + Lab-frame reciprocal vectors (..., 3), 1/Angstroms. n_lab : torch.Tensor | np.ndarray Lab-frame unit plate normal, broadcastable to `g_lab`. + energy_ev : float + Beam energy, eV. + precession_deg : float, default=0.0 + Precession semi-angle, degrees; sets K as above. Returns ------- torch.Tensor | np.ndarray - f (...,), the same type as `g_lab`. + f (...,), dimensionless, the same type as `g_lab`. 1e6 where the rod + is within 0.05 K of tangent to the sphere. """ k0 = 1.0 / electron_wavelength_angstrom(energy_ev) r = k0 * np.sin(np.deg2rad(precession_deg)) @@ -152,12 +173,35 @@ def averaged_gaussian_intensity_envelope( semiconv_mrad: float = 0.0, foil_normal_lab=None, ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - """Envelope, central excitation error and support half-width of every - reflection under the illumination: returns (envelope, c, a, b). + """Illumination-averaged Gaussian envelope of every reflection. - With `foil_normal_lab` (a lab-frame unit plate normal) the excitation is - measured along the relrod instead of along the beam: c, a and b are the - distances along the rod (see :func:`relrod_factor`).""" + With `foil_normal_lab` the excitation is measured along the relrod + instead of along the beam: c, a and b are the distances along the rod + (see :func:`relrod_factor`). + + Parameters + ---------- + g_lab : torch.Tensor | np.ndarray + Lab-frame reciprocal vectors (N, 3), 1/Angstroms. + energy_ev : float + Beam energy, eV. + sigma : float + Width of the excitation envelope, 1/Angstroms. + precession_deg : float, default=0.0 + Precession semi-angle, degrees. + semiconv_mrad : float, default=0.0 + Convergence semi-angle, mrad. + foil_normal_lab : array-like, optional + Lab-frame unit plate normal (3,). + + Returns + ------- + envelope : np.ndarray + Averaged envelope (N,) in [0, 1]. + c, a, b : np.ndarray + Central excitation error and ring and disk amplitudes (N,), + 1/Angstroms; a + b is the half-width of the swept range about c. + """ c, a, b = excitation_coefficients(g_lab, energy_ev, precession_deg, semiconv_mrad) if foil_normal_lab is not None: g_np = g_lab.detach().cpu().numpy() if isinstance(g_lab, torch.Tensor) else g_lab @@ -190,9 +234,30 @@ def averaged_gaussian_intensity_envelope( def ring_disk_quadrature(r: float, R: float, n_phi: int = 128, n_r: int = 8, n_psi: int = 32): - """Positive angular quadrature of the ring x disk illumination, the - reference against which the analytic envelope is checked: returns - in-plane tilts (M, 2) and normalized weights (M,).""" + """Positive quadrature of the ring x disk illumination. + + The reference against which the analytic envelopes are checked. + + Parameters + ---------- + r : float + Ring radius, in the units the tilts are wanted in. + R : float + Disk radius, same units. + n_phi : int, default=128 + Equally spaced points on the ring. + n_r : int, default=8 + Gauss-Legendre radial nodes of the disk (in r^2). + n_psi : int, default=32 + Equally spaced azimuths of the disk. + + Returns + ------- + tilts : np.ndarray + In-plane tilts (M, 2), ring point plus disk point. + weights : np.ndarray + Positive weights (M,) summing to 1. + """ phi = 2 * np.pi * np.arange(n_phi) / n_phi if r > 0 else np.zeros(1) ring = r * np.column_stack((np.cos(phi), np.sin(phi))) if R > 0: @@ -218,8 +283,25 @@ def gaussian_envelope_ring_series(c, a, sigma: float, n_terms: int = 6) -> np.nd which converges in a few terms for v < 1 (the precession sweep of the excitation error smaller than the envelope width, the electron - diffraction regime); larger v falls back to the transform. Accepts - numpy arrays or torch tensors of any shape (a broadcast to c).""" + diffraction regime); larger v falls back to the transform. + + Parameters + ---------- + c : np.ndarray | torch.Tensor + Central excitation errors, any shape, 1/Angstroms. + a : np.ndarray | torch.Tensor | float + Ring amplitudes, broadcast to `c`, 1/Angstroms. + sigma : float + Width of the excitation envelope, 1/Angstroms. + n_terms : int, default=6 + Terms of the sum over n. + + Returns + ------- + np.ndarray | torch.Tensor + Envelope in [0, 1], shape of `c`; a float64 tensor when `c` is a + tensor. + """ from scipy.special import iv is_torch = isinstance(c, torch.Tensor) @@ -244,8 +326,28 @@ def gaussian_envelope_ring_series(c, a, sigma: float, n_terms: int = 6) -> np.nd def excitation_amplitudes( g_lab: torch.Tensor, energy_ev: float, precession_deg: float, semiconv_mrad: float ): - """Ring and disk amplitudes (a, b) as torch tensors for lab-frame g of - any leading shape (..., 3); zero tensors when no illumination.""" + """Ring and disk amplitudes of lab-frame reflections, in torch. + + The a and b of :func:`excitation_coefficients`, without c, for any + leading shape and differentiable in `g_lab`. + + Parameters + ---------- + g_lab : torch.Tensor + Lab-frame reciprocal vectors (..., 3), 1/Angstroms. + energy_ev : float + Beam energy, eV. + precession_deg : float + Precession semi-angle, degrees. + semiconv_mrad : float + Convergence semi-angle, mrad. + + Returns + ------- + a, b : torch.Tensor + Ring and disk amplitudes (...,), 1/Angstroms; zero without + precession or convergence. + """ lam = electron_wavelength_angstrom(energy_ev) k0 = 1.0 / lam r = k0 * np.sin(np.deg2rad(precession_deg)) @@ -270,7 +372,22 @@ def slab_envelope(c, a, b, thickness_A: float) -> np.ndarray: which reduces to sinc(c z)^2 without illumination; the Born intensity of reflection g is (pi |U_g| z / k0)^2 S. Vectorized Gauss-Legendre quadrature over v, exact to ~1e-10 for the phase ranges of electron - diffraction (c z below ~20).""" + diffraction (c z below ~20). + + Parameters + ---------- + c, a, b : array-like + Central excitation error, ring amplitude and disk amplitude + (1/Angstroms), from :func:`excitation_coefficients`; a and b are + broadcast to the shape of c. + thickness_A : float + Specimen thickness z, Angstroms. + + Returns + ------- + np.ndarray + S in [0, 1], same shape as c. + """ c = np.asarray(c, dtype=float) a = np.broadcast_to(np.asarray(a, dtype=float), c.shape) b = np.broadcast_to(np.asarray(b, dtype=float), c.shape) @@ -280,12 +397,29 @@ def slab_envelope(c, a, b, thickness_A: float) -> np.ndarray: def gaussian_envelope_ring_torch(c: torch.Tensor, a: torch.Tensor, sigma: float) -> torch.Tensor: - """Ring-averaged Gaussian envelope (b = 0) in torch, for the refinement - loops: the Bessel series of gaussian_envelope_ring_series truncated + """Ring-averaged Gaussian envelope (b = 0) in torch, for the refinements. + + The Bessel series of :func:`gaussian_envelope_ring_series` truncated after the I_4 term, with I_n(u) from the I_0 / I_1 recurrences (and their small-argument series where the recurrence would cancel). Below v = a^2 / 4 sigma^2 = 0.3 the truncation error is under 1e-5; larger - sweeps are averaged over the ring directly by quadrature.""" + sweeps are averaged over the ring directly by quadrature. Stays on the + device of `c` and is differentiable. + + Parameters + ---------- + c : torch.Tensor + Central excitation errors, any shape, 1/Angstroms. + a : torch.Tensor + Ring amplitudes, broadcastable to `c`, 1/Angstroms. + sigma : float + Width of the excitation envelope, 1/Angstroms. + + Returns + ------- + torch.Tensor + float64 envelope in [0, 1], the broadcast shape of `c` and `a`. + """ c = c.to(torch.float64) a = torch.as_tensor(a, dtype=torch.float64) u = c * a / sigma**2 diff --git a/src/quantem/diffraction/orientation.py b/src/quantem/diffraction/orientation.py index 19030bc02..e4d4010a1 100644 --- a/src/quantem/diffraction/orientation.py +++ b/src/quantem/diffraction/orientation.py @@ -55,10 +55,28 @@ def position_mask(positions, shape: tuple[int, int]) -> torch.Tensor: """Normalize a `positions` argument into an (R, C) boolean mask. - Accepts None (every position), a list of (row, col) scan positions, or - an (R, C) boolean array. Used by the staged workflow: run matching or - refinement on a handful of positions, look at the fits, then run the - whole scan with the same arguments. + Used by the staged workflow: run matching or refinement on a handful of + positions, look at the fits, then run the whole scan with the same + arguments. + + Parameters + ---------- + positions : None | list[tuple[int, int]] | np.ndarray + None for every position, a list of (row, col) scan positions, or an + (R, C) boolean array. + shape : tuple[int, int] + Scan shape (R, C) in probe positions. + + Returns + ------- + torch.Tensor + (R, C) boolean mask. + + Raises + ------ + ValueError + If a boolean mask has the wrong shape, the input is neither a mask + nor a list of (row, col), or a position lies outside the scan. """ R, C = shape if positions is None: @@ -82,9 +100,17 @@ def position_mask(positions, shape: tuple[int, int]) -> torch.Tensor: def scan_scalebar(metadata: dict) -> dict | None: """Scale bar arguments from the scan calibration recorded on the peaks. - Returns {"sampling": step, "units": units} when the scan was calibrated, - or None when it is still in pixels, which is the signal that a plot - should draw no scale bar. + Parameters + ---------- + metadata : dict + Peak metadata carrying "scan_sampling" and "scan_units". + + Returns + ------- + dict | None + {"sampling": step, "units": units} when the scan was calibrated, or + None when it is still in pixels, which tells a plot to draw no + scale bar. """ step = (metadata or {}).get("scan_sampling") units = (metadata or {}).get("scan_units") @@ -116,6 +142,27 @@ def smooth_quaternions( This is an average, not a fit: it moves each orientation away from the one that best explains its own pattern. Use it to display a map, not to produce the orientations a later step will measure from. + + Parameters + ---------- + quats : torch.Tensor + (R, C, 4) orientation quaternions. + active : torch.Tensor + (R, C) boolean mask of positions to smooth and to average over. + sym_quats : torch.Tensor + (S, 4) symmetry rotations used to reduce the misorientations. + sigma_px : float, default=1.0 + Spatial width of the kernel in probe positions. + sigma_deg : float, default=1.0 + Angular width of the kernel in degrees. + max_angle_deg : float, default=5.0 + Neighbours misoriented by more than this many degrees are dropped. + + Returns + ------- + torch.Tensor + (R, C, 4) smoothed quaternions; inactive positions, and positions + with no neighbour inside `max_angle_deg`, are returned unchanged. """ q = torch.as_tensor(quats, dtype=torch.float64) R, C = q.shape[:2] @@ -249,9 +296,10 @@ class OrientationMap(AutoSerialize): om.match_orientations(num_matches=1) om.plot_orientation() - The object is both the engine and the result: after `match`, `quats` - holds (R, C, M, 4) orientation quaternions, `corr` the correlation - scores, and `mirror` the inversion flags. + The object is both the engine and the result: after + `match_orientations()`, `quats` holds (R, C, M, 4) orientation + quaternions, `corr` the correlation scores, and `mirror` the inversion + flags. """ _token = object() @@ -263,6 +311,24 @@ def __init__( energy_ev: float, _token: object | None = None, ): + """Private constructor; use :meth:`from_vectors`. + + Parameters + ---------- + peaks : Vector + Calibrated Bragg peaks over the scan. + crystal : Crystal + Candidate crystal with structure factors already calculated. + energy_ev : float + Beam energy in eV. + _token : object + Guard against direct construction. + + Raises + ------ + RuntimeError + If called without the class token. + """ if _token is not self._token: raise RuntimeError("Use OrientationMap.from_vectors() to construct.") self.peaks = peaks @@ -810,16 +876,6 @@ def _build_reference( # experimental polar images # ------------------------------------------------------------------ - def _polar_image( - self, qx: torch.Tensor, qy: torch.Tensor, intensity: torch.Tensor - ) -> torch.Tensor: - """Sparse polar image (S, G) of one measured pattern.""" - qr = torch.hypot(qx, qy) - qphi = torch.atan2(qy, qx) - amp = intensity.clamp_min(0) ** self.power_intensity_experiment * qr**self.power_radial - out = torch.zeros((self.shell_radii.shape[0], self.num_gamma), dtype=torch.float64) - return self._deposit_polar(qr, qphi, amp, out) - def _grid_quats(self, flat_idx: torch.Tensor, Z: int, G: int, n_ch: int) -> torch.Tensor: """Library orientations at flat (channel, zone, gamma) indices. @@ -942,13 +998,40 @@ def match_orientations( sharing a zone axis but rotated in plane past this angle are therefore kept as separate matches. The same test picks the second-best score used in `reliability`. + top_k_matches : int, default=256 + Number of highest-scoring library entries searched, in order, + for a candidate that passes the separation test, both for the + matches after the first and for `corr_second`. Most of the top + entries are symmetry copies or near neighbours of the best one, + so too small a value leaves later matches empty. subpixel_gamma : bool, default=True Parabolic sub-bin refinement of the in-plane angle. subpixel_zone : bool, default=True Sub-grid zone axis from a quadratic fit of the correlation over the best zone and its grid neighbors (centroid fallback). + min_detector_fraction : float, default=0.3 + With a detector footprint in the plan (`detector_q_max`), library + orientations that put less than this fraction of their template + weight on the detector at a given in-plane angle score zero + there. Ignored when the plan has no detector correction. batch_size : int, default=128 Number of patterns correlated at once. + progress_bar : bool, default=True + Show a progress bar over the batches. + + Returns + ------- + OrientationMap + Self, with `quats` (R, C, M, 4), `corr` and `corr_residual` + (R, C, M), `corr_second` and `reliability` (R, C), `mirror` + (R, C, M) and `computed` (R, C) filled in. Positions not matched + keep the identity orientation and a correlation of zero. + + Raises + ------ + RuntimeError + If the plan has not been built, or no requested position has + `min_number_peaks` peaks. """ if self.plan_fft is None: raise RuntimeError("Run build_plan() first.") @@ -1022,6 +1105,8 @@ def match_orientations( im_fft = torch.fft.fft(im_stack, dim=-1) # (B, S, G) # frequency ramp used to roll a template to an in-plane angle k_ramp = torch.fft.fftfreq(G, d=1.0 / G).to(im_fft.dtype).to(device) + # orientations accepted so far in this batch, one (B, 4) per match + q_prev: list[torch.Tensor] = [] for m in range(M): norms = torch.linalg.norm( @@ -1070,10 +1155,9 @@ def match_orientations( keep[:, 0] = True else: ok = torch.ones((B, K), dtype=torch.bool) - for mm in range(m): - q_prev = self._qprev[mm] # (B, 4) + for q_mm in q_prev: # (B, 4) each ang = misorientation_angle_deg( - q_prev[:, None, :].expand(-1, K, -1).reshape(-1, 4), + q_mm[:, None, :].expand(-1, K, -1).reshape(-1, 4), q_top.reshape(-1, 4), self.crystal.sym_quats_matching, ).reshape(B, K) @@ -1192,9 +1276,7 @@ def match_orientations( corr_second[rx, ry] = c2[b] if M > 1: - if m == 0: - self._qprev = [] - self._qprev.append(q.clone()) + q_prev.append(q.clone()) if M > 1 and m < M - 1 and suppress_matched > 0: # Deflate the matched template out of the measured polar @@ -1316,7 +1398,21 @@ def smooth_orientations( return self def smoothed_quats(self, match: int = 0, **kwargs) -> torch.Tensor: - """Smoothed copy of the orientations, leaving the stored ones alone.""" + """Smoothed copy of the orientations, leaving the stored ones alone. + + Parameters + ---------- + match : int, default=0 + Which match index to smooth. + **kwargs + `sigma_px`, `sigma_deg` and `max_angle_deg` of + :func:`smooth_quaternions`. + + Returns + ------- + torch.Tensor + (R, C, 4) smoothed quaternions. + """ assert self.quats is not None R, C = self.quats.shape[:2] active = torch.ones((R, C), dtype=torch.bool) @@ -1409,6 +1505,10 @@ def refine_orientations( Excitation-error width of the envelope objective; defaults to half the plan's sigma_excitation (the plan value is widened for grid robustness). + batched : bool, default=True + Refine positions in vectorized chunks instead of one at a time, + which is several times faster. `refine_tilt=True` disables it, + since only the per-position path solves the tilt from positions. neighbor_rescue : bool, default=True Retry every position that disagrees with a matched neighbour by more than `rescue_threshold_deg`, from every distinct candidate @@ -1442,6 +1542,14 @@ def refine_orientations( eight neighbours; the Friedel twin is adopted only this way, never on its score alone. 0 judges every position by its own pattern alone. + progress_bar : bool, default=True + Show one progress bar covering refinement and neighbour rescue. + + Returns + ------- + OrientationMap + Self, with `quats` refined in place and `score` (R, C) holding + the correlation of match 0 with the measured peaks. Notes ----- @@ -1663,7 +1771,6 @@ def get_exp(rx, ry): w_exp = w_exp / w_exp.max().clamp_min(1e-12) return q_exp, w_exp - scores = torch.zeros((R, C), dtype=torch.float64) # positions to refine: those requested, or everything matched active = position_mask(positions, (R, C)) if self.computed is not None: @@ -1676,7 +1783,6 @@ def get_exp(rx, ry): bar = tqdm(total=0, desc=f"refining {self.crystal.name}") if progress_bar else None if batched and not refine_tilt: self._refine_batched( - scores, active=active, delta=delta, sigma=sigma, @@ -1703,10 +1809,8 @@ def get_exp(rx, ry): for m in range(M): if self.corr[rx, ry, m] <= 0: continue - q, sc = refine_single(self.quats[rx, ry, m], q_exp, w_exp) + q, _ = refine_single(self.quats[rx, ry, m], q_exp, w_exp) self.quats[rx, ry, m] = q - if m == 0: - scores[rx, ry] = sc # Refinement polishes the orientation on paired peak positions, which # is accurate for small corrections but can jump to another basin on @@ -1955,7 +2059,6 @@ def overlap(a, wa, b, wb): def _refine_batched( self, - scores: torch.Tensor, active: torch.Tensor, delta: float, sigma: float, @@ -2051,7 +2154,6 @@ def envelope(S, g_rows, f_rows=None): continue q = quats[i0:i1, m].clone() # (B, 4) tilt_total = torch.zeros((B, 2), dtype=torch.float64) - sc = torch.zeros(B, dtype=torch.float64) for _ in range(num_iterations): Rm = quat_to_matrix(q) # (B, 3, 3) g = torch.einsum("bij,gj->bgi", Rm, g_all) # (B, G, 3) @@ -2076,7 +2178,6 @@ def envelope(S, g_rows, f_rows=None): ok = act & (n_pair >= min_pairs) if not bool(ok.any()): break - sc = torch.where(ok, w_g.sum(dim=1), sc) tgt = torch.gather(qe, 1, j_min[..., None].expand(-1, -1, 2)) # (B, G, 2) r_vec = tgt - spot # in-plane closed form @@ -2168,12 +2269,29 @@ def envelope(S, g_rows, f_rows=None): q_nz = q[nz] q[nz] = qmult(dq_t, q_nz) quats[i0:i1, m] = torch.where(act[:, None], q, quats[i0:i1, m]) - if m == 0: - scores.reshape(-1)[i0:i1] = torch.where(act, sc, scores.reshape(-1)[i0:i1]) self.quats = quats.reshape(R, C, M, 4) def generate_pattern(self, rx: int, ry: int, match: int = 0, **kwargs): - """Simulated pattern for the matched orientation at (rx, ry).""" + """Simulated pattern for the matched orientation at one probe position. + + The illumination (precession, convergence) and foil normal recorded + on this map are used unless overridden. + + Parameters + ---------- + rx, ry : int + Scan row and column. + match : int, default=0 + Which match index to simulate. + **kwargs + Passed to :meth:`Crystal.generate_pattern`, e.g. `k_max`. + + Returns + ------- + dict[str, torch.Tensor] + The simulated reflections, as returned by + :meth:`Crystal.generate_pattern` ("qx", "qy", "intensity", ...). + """ assert self.quats is not None kwargs.setdefault("precession_deg", self.metadata.get("precession_deg", 0.0)) kwargs.setdefault("semiconv_mrad", self.metadata.get("semiconv_mrad", 0.0)) @@ -2197,11 +2315,13 @@ def match_residual( For overlapping patterns (e.g. a thin lath on a matrix), the direct match of the minority phase is poisoned by the majority phase's - peaks. Here the majority candidate's simulated pattern is used to - delete its measured peaks at each position, and this crystal is - re-matched against the residual peaks only. Where the residual match - beats this map's stored second match, it replaces it (match index 1), - so the joint phase fit sees one clean candidate per phase. + peaks. Here the other crystal's simulated pattern is used to delete + its measured peaks at each position, and this crystal is matched and + refined against the remaining peaks only, with this map's plan. + Where the residual match scores above this map's stored second + match, it replaces it (match index 1), so the joint phase fit sees + one clean candidate per phase. A map with a single match is first + extended to two, the second empty (correlation zero). Parameters ---------- @@ -2210,22 +2330,39 @@ def match_residual( delete_radius : float, default=0.04 Measured peaks within this distance (1/Angstroms) of one of the other crystal's simulated peaks are removed. + min_number_peaks : int, default=MIN_NUMBER_PEAKS (5) + Positions with fewer measured peaks, or fewer residual peaks, + are not re-matched. min_corr_other : float, default=0.0 - Skip positions where the other crystal's correlation is below - this (nothing trustworthy to delete). + Positions where the other crystal's correlation is at or below + this are not re-matched (nothing trustworthy to delete). + progress_bar : bool, default=True + Show progress bars for the residual matching and refinement. + + Returns + ------- + OrientationMap + Self, with `quats`, `corr`, `corr_residual` and `mirror` holding + at least two matches. Where the residual match was taken, both + `corr[..., 1]` and `corr_residual[..., 1]` hold its correlation + with the residual peaks. When no position has enough residual + peaks, nothing is replaced. """ - assert self.quats is not None and other.quats is not None + if self.quats is None or other.quats is None: + raise RuntimeError("Run match_orientations() on both maps first.") + if self.plan_fft is None: + raise RuntimeError("Run build_plan() first.") peaks = self.peaks R, C = peaks.shape[0], peaks.shape[1] fields = peaks.fields ix = [fields.index(f) for f in ("qx", "qy", "intensity")] - - residual = Vector.from_shape( - (R, C), - fields=["qx", "qy", "intensity"], - units=["A^-1", "A^-1", "counts"], - name="residual_peaks", + self.metadata["match_residual"] = dict( + other=other.crystal.name, + delete_radius=float(delete_radius), + min_number_peaks=int(min_number_peaks), + min_corr_other=float(min_corr_other), ) + cells = [] for rx, ry in np.ndindex(R, C): data = peaks[rx, ry].numpy().astype(np.float64) @@ -2233,7 +2370,7 @@ def match_residual( cells.append(np.zeros((0, 3))) continue sim = other.generate_pattern(rx, ry) - sq = torch.stack((sim["qx"], sim["qy"]), dim=1) + sq = torch.stack((sim["qx"], sim["qy"]), dim=1).to(torch.float64) if sq.shape[0] == 0: cells.append(data[:, ix]) continue @@ -2241,15 +2378,38 @@ def match_residual( d_min = torch.cdist(qxy, sq).min(dim=1).values keep = (d_min > delete_radius).numpy() cells.append(data[keep][:, ix]) - nested = [cells[r * C : (r + 1) * C] for r in range(R)] + + # extend to two matches first, so the result has the same layout + # whether or not anything is replaced + if self.quats.shape[2] < 2: + pad_q = torch.zeros((R, C, 1, 4), dtype=self.quats.dtype) + pad_q[..., 0] = 1.0 + self.quats = torch.cat([self.quats, pad_q], dim=2) + pad = torch.zeros((R, C, 1), dtype=self.corr.dtype) + self.corr = torch.cat([self.corr, pad], dim=2) + if self.corr_residual is not None: + self.corr_residual = torch.cat([self.corr_residual, pad.clone()], dim=2) + self.mirror = torch.cat([self.mirror, torch.zeros((R, C, 1), dtype=torch.bool)], dim=2) + if self.corr_residual is None: + self.corr_residual = self.corr.clone() + + # positions with enough residual peaks, among those this map covers + wanted = torch.as_tensor( + np.array([c.shape[0] >= min_number_peaks for c in cells]).reshape(R, C) + ) + if self.computed is not None: + wanted &= self.computed + if not bool(wanted.any()): + return self + residual = Vector.from_data( - nested, + [cells[r * C : (r + 1) * C] for r in range(R)], fields=["qx", "qy", "intensity"], units=["A^-1", "A^-1", "counts"], name="residual_peaks", + metadata=dict(peaks.metadata or {}), dtype=peaks.dtype, ) - om_res = OrientationMap.from_vectors( residual, self.crystal, @@ -2258,12 +2418,17 @@ def match_residual( semiconv_mrad=self.metadata.get("semiconv_mrad", 0.0) or 0.0, foil_normal=self.metadata.get("foil_normal"), ) + # share this map's plan rather than rebuilding it for attr in ( "device", + "dtype", + "cdtype", "corr_kernel_size", "sigma_excitation", "power_radial", "power_intensity", + "power_intensity_experiment", + "zone_axis_range", "zone_axes", "zone_quats", "zone_step_deg", @@ -2279,25 +2444,24 @@ def match_residual( "plan_frac_shift", ): setattr(om_res, attr, getattr(self, attr)) + om_res.metadata["plan"] = dict(self.metadata.get("plan") or {}) om_res.match_orientations( num_matches=1, + positions=wanted.numpy(), min_number_peaks=min_number_peaks, progress_bar=progress_bar, ) om_res.refine_orientations(progress_bar=progress_bar) # replace the stored second match where the residual match is better - if self.quats.shape[2] < 2: - pad_q = torch.zeros((R, C, 1, 4), dtype=torch.float64) - pad_q[..., 0] = 1.0 - self.quats = torch.cat([self.quats, pad_q], dim=2) - self.corr = torch.cat([self.corr, torch.zeros((R, C, 1), dtype=torch.float64)], dim=2) - self.mirror = torch.cat([self.mirror, torch.zeros((R, C, 1), dtype=torch.bool)], dim=2) - better = om_res.corr[..., 0] > self.corr[..., 1] + better = om_res.computed & (om_res.corr[..., 0] > self.corr[..., 1]) self.quats[..., 1, :] = torch.where( better[..., None], om_res.quats[..., 0, :], self.quats[..., 1, :] ) self.corr[..., 1] = torch.where(better, om_res.corr[..., 0], self.corr[..., 1]) + self.corr_residual[..., 1] = torch.where( + better, om_res.corr[..., 0], self.corr_residual[..., 1] + ) self.mirror[..., 1] = torch.where(better, om_res.mirror[..., 0], self.mirror[..., 1]) return self @@ -2318,11 +2482,15 @@ def cluster_orientations( Parameters ---------- mask : np.ndarray | None - Boolean or weight mask of positions to include (e.g. phase mask). + (R, C) boolean or weight mask of positions to include, e.g. a + phase mask. A position is included where the mask is above 0.5, + the same rule as :meth:`calculate_strain`. threshold_deg : float, default=5.0 - Misorientation radius of a cluster. + Misorientation radius of a cluster, degrees. min_cluster_size : int, default=10 Smaller clusters are discarded (labels stay -1). + match : int, default=0 + Which match index to cluster. Returns ------- @@ -2396,6 +2564,28 @@ def calculate_strain( lattice-vector strain mapping it is absolute, not relative to a reference region. + Parameters + ---------- + match : int, default=0 + Which match index to measure. + pair_distance : float | None + Largest distance (1/Angstroms) between a simulated and a measured + peak that are paired; inherits the refinement's, then the plan's. + min_pairs : int | None + Positions with fewer paired peaks are left as NaN; inherits the + refinement's value, else MIN_PAIRS (4). + mask : np.ndarray | None + (R, C) boolean or weight mask of positions to measure. A position + is measured where the mask is above 0.5, the same rule as + :meth:`cluster_orientations`. None measures every matched + position. + ds_sampling : float | None + Scan step, passed to StrainMap for scale bars. + ds_units : str | None + Units of `ds_sampling`. + progress_bar : bool, default=True + Show a progress bar over the positions. + Returns ------- StrainMap @@ -2426,12 +2616,13 @@ def calculate_strain( A_map = np.full((R, C, 2, 2), np.nan) num_pairs = np.zeros((R, C), dtype=int) + include = None if mask is None else np.asarray(mask, dtype=float) > 0.5 iterator = list(np.ndindex(R, C)) if progress_bar: iterator = tqdm(iterator, desc=f"strain mapping {self.crystal.name}") for rx, ry in iterator: - if mask is not None and not mask[rx, ry]: + if include is not None and not include[rx, ry]: continue if self.corr[rx, ry, match] <= 0: continue @@ -2464,12 +2655,12 @@ def calculate_strain( # StrainMap's reciprocal-space branch (U_ref @ inv(U) = F^T) then # yields the real-space strain with the shared sign conventions sm = StrainMap( - u_array=A_map[..., :, 0], - v_array=A_map[..., :, 1], + g1_array=A_map[..., :, 0], + g2_array=A_map[..., :, 1], ds_shape=(R, C), real_space=False, - u_ref=np.array([1.0, 0.0]), - v_ref=np.array([0.0, 1.0]), + g1_ref=np.array([1.0, 0.0]), + g2_ref=np.array([0.0, 1.0]), mask=None if mask is None else np.asarray(mask, dtype=float), ds_sampling=ds_sampling, ds_units=ds_units, @@ -2538,7 +2729,27 @@ def scan_scalebar(self) -> dict | None: return scan_scalebar(md) def plot_orientation(self, direction: str = "z", match: int = 0, **kwargs): - """IPF-colored orientation map; see orientation_visualization.""" + """IPF-colored orientation map with the color wedge beside it. + + Parameters + ---------- + direction : {"z", "r", "c"} | float | array-like, default="z" + Lab direction whose crystal-frame coordinates are colored; see + :func:`~quantem.diffraction.orientation_visualization.plot_orientation_map`. + match : int, default=0 + Which match index to plot. + **kwargs + Passed to + :func:`~quantem.diffraction.orientation_visualization.plot_orientation_map`. + The scale bar defaults to the scan calibration, and after a + staged run on a subset of positions the mask defaults to those + positions. + + Returns + ------- + tuple + ``(fig, ax)``. + """ from quantem.diffraction.orientation_visualization import plot_orientation_map kwargs.setdefault("scalebar", self.scan_scalebar) @@ -2547,15 +2758,94 @@ def plot_orientation(self, direction: str = "z", match: int = 0, **kwargs): ) def plot_pole_figure(self, pole=(0, 0, 1), match: int = 0, **kwargs): - """Stereographic pole figure; see orientation_visualization.""" + """Stereographic pole figure of a crystal direction family over the map. + + Parameters + ---------- + pole : array-like, default=(0, 0, 1) + Crystal direction in Miller indices, [uvw] or [uvtw]. + match : int, default=0 + Which match index to plot. + **kwargs + Passed to + :func:`~quantem.diffraction.orientation_visualization.plot_pole_figure`. + After a staged run on a subset of positions the mask defaults + to those positions. + + Returns + ------- + tuple + ``(fig, ax)``. + """ from quantem.diffraction.orientation_visualization import plot_pole_figure return plot_pole_figure(self, pole=pole, match=match, **self._default_mask(kwargs)) - def misorientation_map(self, reference: torch.Tensor | None = None) -> torch.Tensor: - """Misorientation angle (deg) of match 0 to a reference orientation.""" + def plot_cluster_map(self, clusters: dict, **kwargs): + """Map of the orientation clusters, one color per cluster. + + Parameters + ---------- + clusters : dict + Output of :meth:`cluster_orientations`. + **kwargs + Passed to + :func:`~quantem.diffraction.orientation_visualization.plot_cluster_map`. + The scale bar defaults to the scan calibration. + + Returns + ------- + tuple + ``(fig, ax)``. + """ + from quantem.diffraction.orientation_visualization import plot_cluster_map + + kwargs.setdefault("scalebar", self.scan_scalebar) + return plot_cluster_map(self, clusters, **kwargs) + + def plot_cluster_pole_figure(self, clusters: dict, pole=(0, 0, 1), **kwargs): + """Pole figure of the cluster mean orientations, one color per cluster. + + Parameters + ---------- + clusters : dict + Output of :meth:`cluster_orientations`. + pole : array-like, default=(0, 0, 1) + Crystal direction in Miller indices, [uvw] or [uvtw]. + **kwargs + Passed to + :func:`~quantem.diffraction.orientation_visualization.plot_cluster_pole_figure`, + e.g. `pole_label` and `overlay`. + + Returns + ------- + tuple + ``(fig, ax)``. + """ + from quantem.diffraction.orientation_visualization import plot_cluster_pole_figure + + return plot_cluster_pole_figure(self, clusters, pole=pole, **kwargs) + + def misorientation_map( + self, reference: torch.Tensor | None = None, match: int = 0 + ) -> torch.Tensor: + """Misorientation angle of every position to a reference orientation. + + Parameters + ---------- + reference : torch.Tensor | None + (4,) reference quaternion; None is the identity. + match : int, default=0 + Which match index to compare. + + Returns + ------- + torch.Tensor + (R, C) misorientation angles in degrees, reduced by the matching + symmetry of the crystal. + """ assert self.quats is not None - q = self.quats[..., 0, :] + q = self.quats[..., match, :] if reference is None: reference = torch.tensor([1.0, 0, 0, 0], dtype=torch.float64) return misorientation_angle_deg(reference, q, self.crystal.sym_quats_matching) diff --git a/src/quantem/diffraction/orientation_visualization.py b/src/quantem/diffraction/orientation_visualization.py index 2842db95c..6fcdf29bc 100644 --- a/src/quantem/diffraction/orientation_visualization.py +++ b/src/quantem/diffraction/orientation_visualization.py @@ -9,7 +9,8 @@ from quantem.diffraction.crystal import Crystal from quantem.diffraction.rotations import quat_to_matrix -# one color per candidate phase, used consistently across every plot +# one color per candidate phase, used consistently across every plot; index +# with phase_color_cycle() so more crystals than colors cycle through them DEFAULT_PHASE_COLORS = np.array( [ [1.00, 0.80, 0.25], # gold @@ -18,8 +19,6 @@ [0.85, 0.50, 0.80], # purple ] ) -ORIGIN_COLOR = "#2ca02c" -MEASURED_COLOR = "0.15" # exponent on the distance from the wedge centre: >1 widens the white centre # and softens the transition into it, <1 shrinks it (much below 0.5 leaves a # bright point at the centre) @@ -52,6 +51,29 @@ ] +def phase_color_cycle(n: int, colors=None) -> np.ndarray: + """One RGB color per phase, cycling through the palette. + + Parameters + ---------- + n : int + Number of phases. + colors : sequence | None + Palette of K matplotlib colors (RGB rows or names); None takes + `DEFAULT_PHASE_COLORS`. + + Returns + ------- + np.ndarray + (n, 3) RGB colors in [0, 1]; phase k takes palette entry k modulo K. + """ + from matplotlib.colors import to_rgb + + palette = DEFAULT_PHASE_COLORS if colors is None else colors + palette = np.array([to_rgb(c) for c in palette], dtype=float) + return palette[np.arange(n) % palette.shape[0]] + + def _bary_to_rgb( w: np.ndarray, saturation_power: float | None = None, @@ -161,9 +183,13 @@ def ipf_color( Quaternions (..., 4). crystal : Crystal Provides symmetry and the fundamental wedge. - direction : {"x", "y", "z"} | torch.Tensor, default="z" - Lab direction whose crystal-frame coordinates are colored; "z" is the - beam direction (zone-axis map). + direction : {"z", "r", "c"} | float | array-like, default="z" + Lab direction whose crystal-frame coordinates are colored: "z" is + the beam direction (zone-axis map), "r" the scan row axis and "c" + the scan column axis ("x" and "y" are accepted as aliases of "r" + and "c"). A number is an in-plane angle in degrees from the column + axis toward the row axis; a 2 or 3 element vector is an explicit + (row, col[, z]) direction. saturation_power : float | None Width of the white centre; see :func:`_bary_to_rgb`. chroma : float | None @@ -368,12 +394,22 @@ def plot_orientation_map( ---------- om : OrientationMap Matched orientation map. - direction : {"x", "y", "z"}, default="z" - Lab direction to color ("z" = zone axis). + direction : {"z", "r", "c"} | float | array-like, default="z" + Lab direction to color: "z" the beam (zone axis), "r" the scan row + axis, "c" the scan column axis ("x" and "y" are aliases of "r" and + "c"), a number for an in-plane angle in degrees, or an explicit + vector; see :func:`ipf_color`. match : int, default=0 Which match index to plot. mask : np.ndarray | None Multiplied into the RGB image (e.g. a phase or reliability mask). + figax : (fig, (ax_map, ax_legend)) | (fig, ax_map) | None + Existing axes; with a single axis the legend is skipped. + legend : bool, default=True + Draw the IPF color wedge in a panel beside the map. + axsize : tuple[float, float], default=(9.0, 4.5) + Figure size in inches of the map panel; the legend panel widens the + figure by 30%. Ignored when `figax` is given. fold : bool | "auto", default="auto" Fold the in-plane part of each orientation by the apparent rotational symmetry of its own zero-layer pattern @@ -404,12 +440,15 @@ def plot_orientation_map( The legend is drawn with the same values. scalebar : dict | None Real-space scale bar, e.g. {"sampling": 30, "units": "A"}. - figax : (fig, (ax_map, ax_legend)) | (fig, ax_map) | None - Existing axes; with a single axis the legend is skipped. crop : (r0, r1, c0, c1) | None Show only this window of the map (rows r0:r1, columns c0:c1). title : str | None Replaces the default title (crystal name and colored direction). + + Returns + ------- + tuple + ``(fig, ax)`` with `ax` the map panel. """ import matplotlib.pyplot as plt @@ -522,9 +561,9 @@ def plot_pattern_matches( One row per probe position; one column per (crystal, match) candidate, so alpha and beta fits sit next to each other for direct comparison. Measured peaks are solid gray disks with area proportional to intensity; - each candidate's simulation is drawn as colored crosses (red, then blue - by default) also sized by intensity. With `dataset` given, the raw - pattern is shown behind the crosses instead of the gray disks. + each candidate's simulation is drawn as colored markers in its phase + color, also sized by intensity. With `dataset` given, the raw pattern is + shown behind the markers instead of the gray disks. Parameters ---------- @@ -536,12 +575,14 @@ def plot_pattern_matches( If given, the diffraction pattern is shown behind the overlay and the gray measured disks are omitted. pixel_size : float | None - Reciprocal pixel size (1/Angstroms per pixel); required with dataset. + Reciprocal pixel size (1/Angstroms per pixel); required with + `dataset`. origins : np.ndarray | None (scan_r, scan_c, 2) fitted origins from measure_origins(); aligns the background pattern with the origin-corrected peaks. matches : tuple[int, ...], default=(0, 1) - Match indices per crystal. + Match indices per crystal. Indices a map does not hold (the second + match of a map matched with `num_matches=1`) are skipped. norm : dict | str | None `norm` of `show_2d`, which draws the recorded pattern, e.g. {"power": 0.5, "upper_quantile": 0.98}. The default, @@ -560,7 +601,8 @@ def plot_pattern_matches( panel and leave the pattern in the middle of empty space, so the default trims the furthest 2%. Pass 1.0 to enclose every peak. colors : list | None - One color per crystal; defaults to red, blue, green, purple. + One color per crystal; defaults to `DEFAULT_PHASE_COLORS`, the + palette of the phase map, cycled when there are more crystals. marker : str | None Matplotlib marker for the simulated peaks. The default is an open circle over a diffraction pattern, which leaves the measured disk @@ -575,11 +617,32 @@ def plot_pattern_matches( which keeps the weak spots visible. measured_power : float, default=0.5 Compression applied to the measured intensities before sizing. + scalebar : bool, default=True + Draw a 0.5 1/Angstrom scale bar in the bottom-left panel. + show_measured : bool, default=True + Draw the measured peaks as gray disks. Ignored with `dataset`, + where the recorded pattern is shown instead. + marker_scale : float, default=250.0 + Marker area, in points^2, of the strongest simulated reflection; + the others scale with intensity. transpose_plots : bool, default=False Panel layout only, nothing in the data is transposed. By default rows are probe positions and columns are candidates; True swaps them, giving one row per candidate across the positions, which fits a few candidates and many positions on a page. + axsize : tuple[float, float], default=(3.1, 3.1) + Size of one panel in inches. + + Returns + ------- + tuple + ``(fig, axs)`` with `axs` a 2D array of panels. + + Raises + ------ + ValueError + If `dataset` is given without `pixel_size`, or none of `matches` + exists in any map. """ import matplotlib.pyplot as plt @@ -591,8 +654,12 @@ def plot_pattern_matches( if isinstance(orientation_maps, (list, tuple)) else [orientation_maps] ) - if colors is None: - colors = ["#d62728", "#1f77b4", "#2ca02c", "#9467bd"] + if dataset is not None and pixel_size is None: + raise ValueError( + "plot_pattern_matches needs pixel_size (1/Angstroms per pixel) to place the " + "dataset behind the peaks" + ) + colors = [tuple(c) for c in phase_color_cycle(len(oms), colors)] peaks = oms[0].peaks fields = peaks.fields ix = [fields.index(f) for f in ("qx", "qy", "intensity")] @@ -602,7 +669,10 @@ def plot_pattern_matches( th = np.deg2rad(-rot_deg) rot_back = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]]) - panels = [(i, m) for i in range(len(oms)) for m in matches] + # a map matched with fewer matches than requested has no panel for them + panels = [(i, m) for i, om in enumerate(oms) for m in matches if 0 <= m < om.quats.shape[2]] + if not panels: + raise ValueError(f"none of matches={tuple(matches)} exists in the orientation maps") n_pos, n_pan = len(positions), len(panels) n_r, n_c = (n_pan, n_pos) if transpose_plots else (n_pos, n_pan) fig, axs = plt.subplots( @@ -611,7 +681,7 @@ def plot_pattern_matches( figsize=(axsize[0] * n_c, axsize[1] * n_r + 0.2), squeeze=False, ) - over_image = dataset is not None and pixel_size is not None + over_image = dataset is not None if marker is None: marker = "o" if over_image else "+" ordinal = ["1st", "2nd", "3rd"] + [f"{k + 1}th" for k in range(3, 9)] @@ -621,7 +691,7 @@ def plot_pattern_matches( # collapse its axes, so the limit is taken over all of them together. if q_max_plot is not None: q_lim = float(q_max_plot) - elif dataset is not None and pixel_size is not None: + elif over_image: q_lim = dataset.shape[-1] / 2 * pixel_size else: if not 0.0 < q_max_quantile <= 1.0: @@ -794,7 +864,28 @@ def plot_cluster_map( scalebar: dict | None = None, figax=None, ): - """Map of orientation clusters (variants), one color per cluster.""" + """Map of orientation clusters (variants), one color per cluster. + + Also available as :meth:`OrientationMap.plot_cluster_map`. + + Parameters + ---------- + om : OrientationMap + The clustered map; gives the crystal name for the title. + clusters : dict + Output of :meth:`OrientationMap.cluster_orientations`. + colors : sequence | None + One color per cluster, cycled; defaults to `CLUSTER_COLORS`. + scalebar : dict | None + Real-space scale bar, e.g. {"sampling": 30, "units": "A"}. + figax : (fig, ax) | None + Existing axes to draw into. + + Returns + ------- + tuple + ``(fig, ax)``. Unassigned positions are black. + """ import matplotlib.pyplot as plt labels = clusters["labels"].numpy() @@ -840,17 +931,52 @@ def plot_cluster_map( return fig, ax -def _pole_points( - quats: torch.Tensor, crystal: Crystal, pole, mask=None -) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """Stereographic (x, y, weight) of all symmetry-equivalent poles.""" - q = quats.reshape(-1, 4) - p = torch.as_tensor(pole, dtype=torch.float64) - p = p / torch.linalg.norm(p) +def _pole_family(crystal: Crystal, pole) -> torch.Tensor: + """Unit Cartesian vectors (F, 3) of every symmetry equivalent of a pole. + + Parameters + ---------- + crystal : Crystal + Gives the lattice and the symmetry rotations. + pole : array-like + Crystal direction in Miller indices, [uvw] or [uvtw]. + + Returns + ------- + torch.Tensor + The distinct symmetry images of the pole and their inverses. + """ + p = crystal.direction_vector(pole).to(torch.float64) Rs = quat_to_matrix(crystal.sym_quats) fam = torch.einsum("sij,j->si", Rs, p) fam = torch.unique(torch.round(fam / 1e-6) * 1e-6, dim=0) - fam = torch.cat([fam, -fam]) + return torch.cat([fam, -fam]) + + +def _pole_points( + quats: torch.Tensor, crystal: Crystal, pole, mask=None +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Stereographic projection of every symmetry-equivalent pole of a map. + + Parameters + ---------- + quats : torch.Tensor + (..., 4) orientations. + crystal : Crystal + Gives the lattice and the symmetry rotations. + pole : array-like + Crystal direction in Miller indices, [uvw] or [uvtw]. + mask : np.ndarray | None + Per-orientation weights; zero-weight orientations are dropped. + + Returns + ------- + tuple of np.ndarray + (x, y, weight, source) of the upper-hemisphere poles, with `source` + the flat index of the orientation each pole came from. + """ + q = quats.reshape(-1, 4) + fam = _pole_family(crystal, pole) n_fam = fam.shape[0] R = quat_to_matrix(q) poles_lab = torch.einsum("nij,sj->nsi", R, fam) @@ -869,13 +995,23 @@ def _pole_points( def _pole_scatter_xy(quats: torch.Tensor, crystal: Crystal, pole) -> np.ndarray: - """Stereographic (x, y) of all symmetry-equivalent poles for orientations.""" - p = torch.as_tensor(pole, dtype=torch.float64) - p = p / torch.linalg.norm(p) - Rs = quat_to_matrix(crystal.sym_quats) - fam = torch.einsum("sij,j->si", Rs, p) - fam = torch.unique(torch.round(fam / 1e-6) * 1e-6, dim=0) - fam = torch.cat([fam, -fam]) + """Stereographic (x, y) of all symmetry-equivalent poles for orientations. + + Parameters + ---------- + quats : torch.Tensor + (4,) or (N, 4) orientations. + crystal : Crystal + Gives the lattice and the symmetry rotations. + pole : array-like + Crystal direction in Miller indices, [uvw] or [uvtw]. + + Returns + ------- + np.ndarray + (P, 2) projected upper-hemisphere poles. + """ + fam = _pole_family(crystal, pole) R = quat_to_matrix(torch.atleast_2d(quats)) v = torch.einsum("nij,sj->nsi", R, fam).reshape(-1, 3) v = v[v[:, 2] > -1e-8] @@ -895,18 +1031,33 @@ def plot_cluster_pole_figure( ): """Pole figure of the cluster mean orientations, one color per cluster. + Also available as :meth:`OrientationMap.plot_cluster_pole_figure`. + Parameters ---------- om : OrientationMap Provides the crystal symmetry of the clustered phase. clusters : dict - Output of OrientationMap.cluster_orientations(). + Output of :meth:`OrientationMap.cluster_orientations`. pole : array-like - Crystal-Cartesian pole direction of the plotted family. + Crystal direction of the plotted family in Miller indices, [uvw] or + [uvtw]. + pole_label : str, default="" + Legend label prefix of the pole family, e.g. "[0001]". overlay : dict | None Second pole family drawn as open markers, e.g. {"quats": q_beta_mean, "crystal": ti_beta, "pole": (1, 1, 0), - "label": "<110> beta"} -- the standard Burgers relationship check. + "label": "<110> beta"}, the standard Burgers relationship check. + Its "pole" is in Miller indices of its own crystal. + colors : sequence | None + One color per cluster, cycled; defaults to `CLUSTER_COLORS`. + figax : (fig, ax) | None + Existing axes to draw into. + + Returns + ------- + tuple + ``(fig, ax)``. """ import matplotlib.pyplot as plt @@ -988,26 +1139,46 @@ def plot_pole_figure( om : OrientationMap Matched orientation map. pole : array-like, default=(0, 0, 1) - Crystal direction (Cartesian) of the pole family. + Crystal direction of the pole family in Miller indices, [uvw] or + [uvtw]; converted to Cartesian with the crystal's lattice. + match : int, default=0 + Which match index to plot. mask : np.ndarray | None Per-position weights (e.g. phase mask). bins : int, default=181 Histogram bins across the stereographic disk. color_by : {"density", "ipf"}, default="density" - "density": grayscale-to-color histogram. "ipf": each contribution is - colored by the IPF (zone axis) color of its probe position, and the - histogram density sets the brightness, black background. + "density": white through yellow and red to black with increasing + density. "ipf": each contribution is colored by the IPF (zone axis) + color of its probe position, blended from a white background as the + density rises, with the color wedge in a panel beside it. int_range : tuple, default=(0.0, 1.0) - Density display range as fractions of the maximum bin: values below - the lower limit saturate to black, above the upper limit to full - brightness. + Density display range as fractions of the 98th percentile of the + occupied bins: values below the lower limit show as background, + above the upper limit at full strength. + smooth_sigma : float, default=1.5 + Gaussian blur of the histogram, in bins; 0 disables it. label : str | None Annotation for the pole family, e.g. "(0001)" or "{110}". grid : bool, default=True Draw polar-angle circles and azimuth spokes every 30 degrees. + overlay : dict | None + A second pole family drawn on top. With an "om" key, the density + of that map's poles is drawn as contours: + {"om": om_beta, "pole": (1, 1, 0), "match": 0, "mask": mask_beta, + "label": "<110> beta"}. Otherwise fixed orientations are drawn as + open markers: {"quats": q, "crystal": xtl, "pole": (1, 1, 0), + "label": ...}. Poles are Miller indices of the overlay's crystal. saturation_power, chroma : float | None Color wedge shape, used when `color_by` is "ipf"; see :func:`plot_orientation_map`. + figax : (fig, ax) | (fig, (ax, ax_legend)) | None + Existing axes; the legend panel is used only with `color_by="ipf"`. + + Returns + ------- + tuple + ``(fig, ax)`` with `ax` the pole figure panel. """ import matplotlib.pyplot as plt diff --git a/src/quantem/diffraction/phase.py b/src/quantem/diffraction/phase.py index 2237439c4..b683dd1c2 100644 --- a/src/quantem/diffraction/phase.py +++ b/src/quantem/diffraction/phase.py @@ -121,8 +121,9 @@ def __init__(self, orientation_maps: list[OrientationMap], _token=None): # hyperparameters inherited from the maps and recorded per stage self.metadata: dict = {"orientation_maps": [dict(om.metadata) for om in orientation_maps]} - self.phase_weights: torch.Tensor | None = None - self.costs_single: torch.Tensor | None = None + # fit results, (R, C, ...) over the scan; see fit() + self.phase_weights: torch.Tensor | None = None # (R, C, F) per candidate + self.crystal_weights: torch.Tensor | None = None # (R, C, n_crystals) self.cost_best: torch.Tensor | None = None self.phase_index: torch.Tensor | None = None self.reliability: torch.Tensor | None = None @@ -131,13 +132,32 @@ def __init__(self, orientation_maps: list[OrientationMap], _token=None): @classmethod def from_orientation_maps(cls, orientation_maps: list[OrientationMap]) -> "PhaseMap": - """Create from OrientationMaps that share the same peaks.""" + """Create from OrientationMaps that share the same peaks. + + Parameters + ---------- + orientation_maps : list of OrientationMap + Matched maps, one per candidate crystal. + + Returns + ------- + PhaseMap + + Raises + ------ + ValueError + If the maps have different scan shapes. + RuntimeError + If a map has not been matched. + """ p0 = orientation_maps[0].peaks for om in orientation_maps: if om.peaks.shape != p0.shape: raise ValueError("All OrientationMaps must share the same scan shape.") if om.quats is None: - raise RuntimeError(f"OrientationMap for {om.crystal.name}: run match() first.") + raise RuntimeError( + f"OrientationMap for {om.crystal.name}: run match_orientations() first." + ) return cls(orientation_maps, _token=cls._token) def fit( @@ -191,13 +211,18 @@ def fit( non-precession data with strong dynamical scattering, lowering this weight makes the decision coverage-driven and removes the bias toward sparse templates. - min_sim_intensity_rel : float, default=0.02 + min_sim_intensity_rel : float | None Simulated reflections weaker than this fraction of the pattern maximum are dropped before comparison: kinematically weak spots are frequently unobservable and should not penalize a phase whose - structure factors happen to include many of them. + structure factors happen to include many of them. None takes + MIN_SIM_INTENSITY_REL (0.02). k_max : float | None - Restrict the comparison below this scattering vector. + Restrict the comparison below this scattering vector + (1/Angstroms). + min_number_peaks : int | None + Positions with fewer measured peaks (direct beam included) are + not fit and stay unindexed; inherits the matching's value. min_diffracted_peaks : int, default=2 Null hypothesis: a position needs at least this many measured peaks beyond `null_k_min` before any phase is assigned. Vacuum @@ -208,6 +233,25 @@ def fit( Scattering vector (1/Angstroms) above which a measured peak counts as diffracted. The default excludes the direct beam, which sits at the origin after `correct_peak_origins`. + progress_bar : bool, default=True + Show a progress bar over the positions. + + Returns + ------- + PhaseMap + Self, with these (R, C, ...) results: + + - `phase_index`: winning crystal, -1 where unindexed (not fit, + rejected by the null hypothesis, or no candidate weight). + - `phase_weights`: (R, C, F) non-negative weight of every + candidate in the best model, F = len(`candidates`). + - `crystal_weights`: (R, C, n_crystals) those weights summed per + crystal and normalized to sum to one, zero where unindexed. + - `cost_best`: cost of the best model, NaN where not fit. + - `reliability`: cost gap to the best model without the winning + crystal, NaN where not fit or no such model exists. + - `diffracted_intensity`, `num_diffracted`: measured intensity + and number of peaks beyond `null_k_min`. """ from scipy.optimize import nnls @@ -249,11 +293,9 @@ def fit( subsets = [s for n in range(1, max_patterns + 1) for s in combinations(range(F), n)] - costs_single = torch.full((R, C, F), torch.nan, dtype=torch.float64) cost_best = torch.full((R, C), torch.nan, dtype=torch.float64) weights_out = torch.zeros((R, C, F), dtype=torch.float64) - reliability = torch.zeros((R, C), dtype=torch.float64) - best_subset = torch.full((R, C), -1, dtype=torch.long) + reliability = torch.full((R, C), torch.nan, dtype=torch.float64) diffracted = torch.zeros((R, C), dtype=torch.float64) num_diffracted = torch.zeros((R, C), dtype=torch.long) @@ -334,14 +376,10 @@ def fit( if not results: continue results.sort(key=lambda r: r[0]) - c_best, s_best, cols_best, w_best = results[0] + c_best, _, cols_best, w_best = results[0] cost_best[rx, ry] = c_best - best_subset[rx, ry] = subsets.index(s_best) for f, w in zip(cols_best, w_best): weights_out[rx, ry, f] = w - for cost, s, _, _ in results: - if len(s) == 1: - costs_single[rx, ry, s[0]] = cost # reliability: cost gap to the best model containing NO candidate # of the dominant crystal (candidates of one crystal can be @@ -351,13 +389,11 @@ def fit( others = [c for c, s, _, _ in results if all(cands[f][0] != i_dom for f in s)] reliability[rx, ry] = (min(others) - c_best) if others else torch.nan - self.costs_single = costs_single self.cost_best = cost_best self.diffracted_intensity = diffracted self.num_diffracted = num_diffracted self.phase_weights = weights_out self.reliability = reliability - self.best_subset = best_subset # dominant phase: candidate weights summed per crystal n_maps = len(oms) @@ -365,10 +401,15 @@ def fit( for f, (i_om, _) in enumerate(cands): w_phase[..., i_om] += weights_out[..., f] # argmax over all-zero weights returns 0, which would label every - # position that was never fit as the first phase; mark them instead + # position that was never fit, or whose best model has no weight + # (NNLS returns all zeros when no candidate overlaps the peaks), as + # the first phase; mark them instead + w_sum = w_phase.sum(dim=-1) self.phase_index = w_phase.argmax(dim=-1) - self.phase_index[torch.isnan(cost_best)] = -1 - self.phase_fractions = w_phase / w_phase.sum(dim=-1, keepdim=True).clamp_min(1e-12) + unindexed = torch.isnan(cost_best) | (w_sum <= 0) + self.phase_index[unindexed] = -1 + self.reliability[unindexed] = torch.nan + self.crystal_weights = w_phase / w_sum[..., None].clamp_min(1e-12) return self def apply_dynamical(self, result: dict) -> "PhaseMap": @@ -383,6 +424,21 @@ def apply_dynamical(self, result: dict) -> "PhaseMap": `metadata['kinematical']`. Positions the refinement did not reach (outside its `mask`) keep their current decision, so a refinement of one region, or several in stages, updates only that region. + + Parameters + ---------- + result : dict + Output of :func:`~quantem.diffraction.bloch.refine_dynamical`: + "cost" (R, C, F) per-candidate cost, NaN where not refined, + "phase_index" (R, C) the winning crystal, and optionally + "metadata". + + Returns + ------- + PhaseMap + Self, with `phase_index`, `reliability` and `cost_best` updated + at the refined positions. With one crystal there is no + runner-up and the reliability is NaN, as in :meth:`fit`. """ cost = torch.nan_to_num(result["cost"], nan=torch.inf) n_maps = len(self.orientation_maps) @@ -395,7 +451,7 @@ def apply_dynamical(self, result: dict) -> "PhaseMap": torch.isfinite(order[..., 0]), (order[..., 1] - order[..., 0]).clamp_min(0) if n_maps > 1 - else torch.zeros_like(order[..., 0]), + else torch.full_like(order[..., 0], torch.nan), torch.full_like(order[..., 0], torch.nan), ) done = torch.isfinite(order[..., 0]) @@ -481,13 +537,17 @@ def plot_phase( scalebar: dict | str | None = "auto", figax=None, ): - """Dominant-phase map, colored by phase and shaded by reliability. + """Dominant-phase map: color gives the crystal, brightness the evidence. + + By default the brightness is the diffracted signal, so vacuum and + unindexed positions are black; see `shade_by`. Parameters ---------- phase_colors : np.ndarray | None - One RGB color per phase; defaults to the shared palette used by - the pattern overlay plots (gold, light blue, ...). + One RGB color per phase, cycled when there are more phases; + defaults to `DEFAULT_PHASE_COLORS`, the palette shared with the + pattern overlay plots (gold, cyan, green, purple). shade_by : {"signal", "reliability", "none"}, default="signal" What the brightness means. "signal" fades each position by the measured diffracted intensity (see :meth:`signal_confidence`), so @@ -525,23 +585,32 @@ def plot_phase( and units carried from the dataset by the orientation maps; a dict such as {"sampling": 30, "units": "A"} overrides it, and None draws no bar. + figax : (fig, ax) | None + Existing axes to draw into. + + Returns + ------- + tuple + ``(fig, ax)``. + + Raises + ------ + ValueError + If `shade_by` is unknown or `shade_gamma` is not positive. """ if isinstance(scalebar, str): scalebar = self.orientation_maps[0].scan_scalebar if scalebar == "auto" else None import matplotlib.pyplot as plt from quantem.core.visualization.visualization_utils import add_scalebar_to_ax - from quantem.diffraction.orientation_visualization import DEFAULT_PHASE_COLORS + from quantem.diffraction.orientation_visualization import phase_color_cycle assert self.phase_index is not None and self.reliability is not None - if phase_colors is None: - phase_colors = DEFAULT_PHASE_COLORS[: len(self.names)] + phase_colors = phase_color_cycle(len(self.names), phase_colors) if reliability_range is not None: shade_by, shade_range = "reliability", reliability_range phase = self.phase_index.numpy() indexed = phase >= 0 - if majority_filter > 0: - phase = _majority_filter(phase, int(majority_filter)) if shade_by == "signal": alpha = self.signal_confidence(shade_range) lo, hi = 0.0, 1.0 @@ -569,7 +638,14 @@ def plot_phase( # zero maps to zero under any positive exponent, so unindexed positions # stay black and only the faint indexed ones are lifted alpha = np.power(alpha, shade_gamma) - rgb = phase_colors[np.where(indexed, phase, 0)] * alpha[..., None] + if majority_filter > 0: + # the filter can turn an indexed position unindexed (-1) and the + # reverse, so the colors and the black mask follow the filtered + # decision; a position it newly indexes has no brightness of its + # own and stays black + phase = _majority_filter(phase, int(majority_filter)) + alpha = alpha * (phase >= 0) + rgb = phase_colors[np.where(phase >= 0, phase, 0)] * alpha[..., None] if figax is None: fig, ax = plt.subplots(figsize=(9, 4.5)) diff --git a/src/quantem/diffraction/reverse_monte_carlo.py b/src/quantem/diffraction/reverse_monte_carlo.py index 1ec45026b..67ee3bf2d 100644 --- a/src/quantem/diffraction/reverse_monte_carlo.py +++ b/src/quantem/diffraction/reverse_monte_carlo.py @@ -6,10 +6,11 @@ nodes of the average lattice removed; the diffuse intensity of each pattern is ``|F|^2`` read where the pattern's Ewald sphere (with its fitted tilt) cuts the grid, averaged over the cubic rotations so the model is as -symmetric as the (statistically cubic) foil. Moves are swaps of unlike atoms -and omega embryos (three consecutive atoms of a <111> row, the last two -collapsed toward each other by a/12); each changes ``F`` by a few phase -factors, so every move is scored exactly without recomputing the supercell. +symmetric as the (statistically cubic) foil. Each Monte Carlo move changes +``F`` by a few phase factors, so every move is scored exactly without +recomputing the supercell. The move types are species swaps, single-atom +displacements on the fine position grid and, optionally, omega embryos (see +`ReverseMonteCarlo.run`). Bragg peaks with their tails and the direct-beam bloom are masked by sigmoid weights; a smooth background (constant, direct-beam Lorentzian, a wide @@ -23,6 +24,15 @@ species pushes its neighbours by its misfit through harmonic springs, and the resulting displacement field (Huang and size-effect scattering, odd about every Bragg peak) depends on which species sits where. + +Workflow: ``from_images`` -> ``set_crystal`` -> ``fit_geometry`` -> +``fit_thickness`` -> ``set_mask`` -> ``build_supercell`` -> ``fit_background`` +-> ``run`` -> analysis and plots. ``set_envelope``, ``set_size_effect`` and +``fit_size_effect`` are optional. See `ReverseMonteCarlo` for details. + +Limitations: cubic unit cells only; every mixed-occupancy site must share one +composition; static displacements (and ``displacement_correlations``) are +implemented for BCC site lattices only. """ from __future__ import annotations @@ -84,8 +94,45 @@ def _default_device() -> str: class ReverseMonteCarlo(AutoSerialize): """Reverse Monte Carlo fit of one supercell to diffraction patterns along several zone axes. - Build with :meth:`from_images`, then ``set_crystal`` -> ``fit_geometry`` -> - ``set_mask`` -> ``build_supercell`` -> ``fit_background`` -> ``run``. + Notes + ----- + Workflow: + + 1. ``from_images``: patterns, their zone axes, beam energy and pixel size. + 2. ``set_crystal``: average structure with the mixed-occupancy sites. + 3. ``fit_geometry``: center, detector distortion and tilt of each pattern. + 4. ``fit_thickness``: Bloch-wave thickness and tilt from the Bragg + intensities (required for ``envelope="bloch"``; recommended otherwise + since it refines the tilts). + 5. ``set_mask``: diffuse weights, binned data and Bragg intensities. + 6. ``build_supercell``: random supercell at the crystal's composition. + 7. ``fit_background``: diffuse scale and smooth background per pattern. + 8. ``run``: Monte Carlo sweeps. + 9. Analysis (``r_factors``, ``warren_cowley``, ``displacement_correlations``, + ``diffuse_section``, ...) and ``plot_*`` methods. + + Optional steps after ``build_supercell``: ``set_envelope`` switches the + diffuse envelope; ``set_size_effect`` / ``fit_size_effect`` add the linear + size effect. + + Moves: ``run`` mixes three move types on the current supercell: + + - species swaps of two unlike atoms (composition conserved); + - single-atom random displacements by one step of the fine position grid + along each axis (``random_fraction``, default 0.5, when the supercell + was built with ``displacements=True``); + - omega embryos, three consecutive atoms of a <111> row with the last two + collapsed toward each other (``omega_fraction``, default 0). + + Limitations: cubic unit cells only. Every mixed-occupancy site must share one + composition. Static displacements and ``displacement_correlations`` are + implemented for BCC site lattices only. + + Saving and loading: save without the arrays that can be rebuilt, then load with + `quantem.core.io.load`, which calls ``_post_load`` to rebuild them:: + + rmc.save(path, mode="o", skip=rmc.DERIVED_ATTRIBUTES) + rmc = quantem.core.io.load(path) """ _token = object() @@ -323,6 +370,35 @@ def fit_geometry( excitation error is ``s = -(|g|^2 / 2K + tilt . g)`` and its intensity ``|F|^2 exp(-s^2 / 2 sigma^2)``. Zero tilt puts the Laue circle on the direct beam. + + Parameters + ---------- + scale_range : (float, float), optional + Range of the pixel-size scale factor searched in the coarse + indexing step, relative to ``sampling``. Default (0.85, 1.2). + k_max : float, optional + Largest scattering vector (1/A) of the reflections used for + indexing and tilt fitting. Default 1.6. + centers : sequence of (row, col) or None, optional + Approximate direct-beam position of each pattern in pixels; None + entries (or ``centers=None``) use the halo center. + fit_tilt : bool, optional + Fit the tilt of each pattern. If False, the tilts are zero. + verbose : bool, optional + Print the fitted geometry of each pattern. + + Returns + ------- + ReverseMonteCarlo + self. The results are stored in ``self.geometry``, a dict of + per-pattern lists: "centers" (row, col) px, "matrices" (2x2, px + per 1/A at the CIF lattice parameter), "tilts" (zone-frame vector, + radians), "a" (lattice parameter, A), "rms_px", "n_matched", + "peaks", "bragg_hkl", "bragg_g", "bragg_px", "bragg_intensity" and + "excitation_width" (1/A). "excitation_width" is the width of the + Gaussian excitation-error profile fitted with the tilt; it is a + diagnostic only and is not used later. ``self.lattice_parameter`` + is set to the mean of "a". """ if self.crystal is None: raise RuntimeError("set_crystal first") @@ -330,6 +406,7 @@ def fit_geometry( k_wave = 1.0 / self.wavelength geo = dict(centers=[], matrices=[], tilts=[], a=[], rms_px=[], n_matched=[], peaks=[]) geo.update(bragg_hkl=[], bragg_g=[], bragg_intensity=[], bragg_px=[], excitation_width=[]) + geo["_inten_kin"] = [] # kinematic |F|^2 of each pattern's reflections, for _fit_tilt for i, (im, zone) in enumerate(zip(self.images, self.zone_axes)): pts, heights, prom = self._find_peaks(im) guess = ( @@ -395,7 +472,7 @@ def fit_geometry( geo["bragg_g"].append(g2) # zone frame, at the CIF lattice parameter geo["bragg_px"].append(p_all) geo["bragg_intensity"].append(inten_meas) - geo["_inten_kin"] = geo.get("_inten_kin", []) + [inten] + geo["_inten_kin"].append(inten) if verbose: print( f"{self.names[i]}: center ({c[0]:.1f}, {c[1]:.1f}), a = {a_i:.4f} A, " @@ -521,7 +598,37 @@ def fit_thickness( best thickness and tilt are stored, with each beam's intensity averaged over depth, ``(1/t) int_0^t |phi_g(z)|^2 dz``: the beams that generate diffuse scattering inside the foil, used by ``envelope="bloch"``. + + Parameters + ---------- + thickness : (float, float), optional + Thickness range searched, in A. Default (20, 1000). + step : float, optional + Thickness step, in A. Default 10. + tilt_range_deg : float, optional + Half width of the tilt search about the current tilt, along each + zone-frame axis, in degrees. Default 0.6. + tilt_step_deg : float, optional + Tilt search step, in degrees. Default 0.1. + k_max : float, optional + Largest scattering vector (1/A) of the Bloch-wave beams. Default + 1.6. Stored in ``geometry["thickness_k_max"]`` for + ``plot_thickness``. + depth_samples : int, optional + Depths at which the beam intensities are averaged. Default 24. + verbose : bool, optional + Print the result for each pattern. + + Returns + ------- + dict + ``{name: {"thickness": A, "tilt_deg": degrees off the zone axis}}`` + per pattern. ``self.geometry`` is updated in place: "tilts", + "thickness", "bloch_g", "bloch_p", "bloch_score" and + "thickness_k_max". """ + if self.geometry is None: + raise RuntimeError("fit_geometry first") from quantem.diffraction import bloch crystal = self.crystal @@ -533,6 +640,7 @@ def fit_thickness( geo.setdefault("bloch_g", [None] * len(self.images)) geo.setdefault("bloch_p", [None] * len(self.images)) geo.setdefault("bloch_score", [None] * len(self.images)) + geo["thickness_k_max"] = float(k_max) results = {} for i in range(len(self.images)): hkl_m, meas = self._measured_bragg(i) @@ -591,7 +699,8 @@ def fit_thickness( def plot_thickness(self, **kwargs): """Bloch thickness fit per pattern: misfit against thickness (top) and measured against - calculated Bragg intensities at the best fit, square-root scale (bottom).""" + calculated Bragg intensities at the best fit, square-root scale (bottom). Uses the + ``k_max`` given to ``fit_thickness``.""" import matplotlib.pyplot as plt from quantem.diffraction import bloch @@ -613,7 +722,7 @@ def plot_thickness(self, **kwargs): self._orientation_quat(i, self.geometry["tilts"][i]), [t_best], self.energy, - k_max=1.6, + k_max=self.geometry.get("thickness_k_max", 1.6), ) lookup = { tuple(h): v @@ -677,7 +786,34 @@ def set_mask( percent of the local diffuse level by 0.12 1/A. Each reflection's integrated intensity weights the diffuse envelope. + + Parameters + ---------- + bragg_radius : float, optional + Distance (1/A) from each reflection at which the weight reaches + 0.5. Default 0.12. + softness : float, optional + Width (1/A) of the sigmoid edges. Default 0.01. + q_max : float, optional + Largest scattering vector (1/A) fitted. Default 1.2. + center_radius : float, optional + Radius (1/A) of the direct-beam bloom removed. Default 0.25. + edge_px : int, optional + Detector-edge border (unbinned pixels) given zero weight. 0 keeps + the whole detector. Default 8. + + Returns + ------- + ReverseMonteCarlo + self. The results are stored in ``self.mask``: per-pattern lists + "y" (weighted mean of each binned pixel, normalized), "w" (binned + weight, 0 to 1), "k" (zone-frame q of each binned pixel, 1/A), + "data" (binned pattern, normalized), "scale" (normalization), + "bragg_k" and "bragg_intensity" (reflections fully on the + detector), plus the parameters above. """ + if self.geometry is None: + raise RuntimeError("fit_geometry first") b = self.bin_factor out = dict( bragg_radius=bragg_radius, softness=softness, q_max=q_max, center_radius=center_radius @@ -698,8 +834,9 @@ def set_mask( * _sigmoid((q - center_radius) / softness) * (q < q_max) ) - w[:edge_px] = w[-edge_px:] = 0 - w[:, :edge_px] = w[:, -edge_px:] = 0 + if edge_px > 0: + w[:edge_px] = w[-edge_px:] = 0 + w[:, :edge_px] = w[:, -edge_px:] = 0 y = im[:ny, :nx].astype(np.float64) # integrated Bragg intensities over the local ring median @@ -763,10 +900,16 @@ def build_supercell( cells : int Unit cells along each cube edge. The diffuse model is sampled every ``1 / (cells a)`` in reciprocal space. + seed : int or None + Seed of the random generator (stored as ``self.rng``) used for the + initial species arrangement and for every later Monte Carlo move. + None seeds from fresh OS entropy. Default 0. displacements : bool Allow static displacements. Positions live on a grid ``a / displacement_grid`` fine, so every move is still scored - exactly. + exactly. Implemented for BCC site lattices only: the default + True raises NotImplementedError for any other site lattice, so + pass False there. displacement_grid : int Steps per lattice parameter of the displacement grid (a multiple of 24): 24 gives 0.15 A steps, 48 gives 0.076 A for a = 3.66 A. @@ -791,8 +934,9 @@ def build_supercell( averaged intensities, which dynamical diffraction makes differ from their exit intensities. "bloch" takes the depth-averaged Bloch-wave beam intensities at each pattern's fitted thickness and - tilt (``fit_thickness``), with one diffuse scale for all patterns. - "kinematic" keeps only the direct beam. + tilt (requires ``fit_thickness``); use it with ``shared_scale`` + so one diffuse scale covers all patterns. "kinematic" keeps only + the direct beam. resolution : float Gaussian sigma, in supercell reciprocal-grid steps, with which each pixel reads the diffuse grid (27 nearest points). A finite @@ -801,14 +945,22 @@ def build_supercell( trilinearly, about half as many grid points in total (faster). shared_scale : bool One diffuse scale for every pattern; the envelope carries each - pattern's absolute Bragg intensities. + pattern's absolute Bragg intensities. Ignored with + ``envelope="fitted"``, whose beam weights are solved per pattern. max_beams : int Strongest Bragg beams of each pattern in the diffuse envelope. device : str, optional - torch device; default cuda, then mps, then cpu. + torch device. Default cuda when available, otherwise cpu; mps is + used only when requested explicitly. + + Returns + ------- + ReverseMonteCarlo + self. """ if self.mask is None: raise RuntimeError("set_mask first") + self._check_envelope(envelope) rng = np.random.default_rng(seed) self.rng = rng d = self.grid_divisor @@ -976,7 +1128,7 @@ def _setup_forward(self): self.images ): # direct-beam Lorentzian half width, wide Gaussian sigma and its center, 1/A; the - # wide Gaussian floats because a tilted crystal centers its diffuse and Kikuchi + # wide Gaussian's center floats because a tilted crystal centers its smooth # background on the zone-axis pole rather than the direct beam self.background_sigmas = [(0.1, 0.8, 0.0, 0.0) for _ in self.images] self._sigma_bounds = (np.array([0.01, 0.3, -1.0, -1.0]), np.array([1.0, 3.0, 1.0, 1.0])) @@ -990,9 +1142,31 @@ def _setup_forward(self): self.coefficients = c self._update_residual() + _ENVELOPES = ("measured", "fitted", "bloch", "kinematic") + + def _check_envelope(self, envelope: str) -> None: + """Raise if ``envelope`` is unknown, or is "bloch" before ``fit_thickness``.""" + if envelope not in self._ENVELOPES: + raise ValueError(f"unknown envelope {envelope!r}; use one of {self._ENVELOPES}") + if envelope == "bloch": + bloch_g = (self.geometry or {}).get("bloch_g") + if bloch_g is None or any(g is None for g in bloch_g): + raise RuntimeError("fit_thickness before envelope='bloch'") + def set_envelope(self, envelope: str) -> "ReverseMonteCarlo": - """Switch the diffuse envelope ("fitted", "measured", "bloch", "kinematic") and refit the - scale and background for the current supercell.""" + """Switch the diffuse envelope and refit the scale and background. + + Parameters + ---------- + envelope : {"measured", "fitted", "bloch", "kinematic"} + See ``build_supercell``. "bloch" requires ``fit_thickness``. + + Returns + ------- + ReverseMonteCarlo + self. + """ + self._check_envelope(envelope) self.envelope = envelope self._setup_forward() self._solve_linear(self._model_diffuse(), refit_sigmas=True) @@ -1062,11 +1236,27 @@ def _size_chi(self, q: np.ndarray, k_ratio: float = 0.5, chunk: int = 200_000) - return out def set_size_effect(self, eta: dict[str, float] | str | None = "radii") -> "ReverseMonteCarlo": - """Linear size effect: species mismatch ``eta_s`` (dimensionless, relative to the mean). + """Set the linear size effect: species mismatch ``eta_s`` (dimensionless). + + Only differences between species matter (a common shift only moves the + Bragg peaks). The forward model is rebuilt; call ``fit_background`` + afterwards to refit the scale and background. - ``"radii"`` takes ``(r_s - r_mean) / r_mean`` from metallic radii (V 1.34, Nb 1.46, - Zr 1.60 A); a dict sets them; None switches the size effect off. Only differences - between species matter (a common shift only moves the Bragg peaks). + Parameters + ---------- + eta : "radii", dict or None, optional + "radii" (default) takes ``(r_s - r_mean) / r_mean`` from metallic + radii (e.g. V 1.34, Nb 1.46, Zr 1.60 A; ASE covalent radii for + species without a tabulated metallic radius), with ``r_mean`` + the composition-weighted mean. A dict ``{species: eta}`` sets + them directly (missing species get 0). None switches the size + effect off. + + Returns + ------- + ReverseMonteCarlo + self. The mismatches are stored in ``self.size_eta``, in the + order of ``self.species``. """ if eta is None: self.size_eta = np.zeros(len(self.species)) @@ -1097,19 +1287,38 @@ def _species_amplitudes(self) -> np.ndarray: return out def fit_size_effect(self, step: float = 0.005, verbose: bool = True) -> dict: - """Fit the species size mismatches eta_s to the diffuse scattering of the current - supercell (background and scale re-solved at each step; one eta fixed by - ``sum c_s eta_s = 0``).""" - A = torch.as_tensor(self._species_amplitudes()) + """Fit the species size mismatches eta_s to the diffuse scattering of the current supercell. + + Nelder-Mead from no size effect; the background and scale are + re-solved at each step, and one eta is fixed by ``sum c_s eta_s = 0``. + If the fit does not lower the loss, the size effect stays off. + + Parameters + ---------- + step : float, optional + Size of the initial Nelder-Mead simplex in eta. Default 0.005. + verbose : bool, optional + Print the fitted mismatches and the loss change. + + Returns + ------- + dict + "eta": ``{species: eta_s}``; "loss": (loss without size effect, + loss after the fit and a background refit). + """ + dev = self.device + A = torch.as_tensor(self._species_amplitudes(), device=dev) n = self.grid_size hn = np.stack(np.unravel_index(self._needed, (n,) * 3), -1) hm = np.where(hn > n // 2, hn - n, hn) chi = torch.as_tensor( - self._size_chi(hm / (self.lattice_parameter * self.cells)), dtype=torch.float32 + self._size_chi(hm / (self.lattice_parameter * self.cells)), + dtype=torch.float32, + device=dev, ) self.size_eta = np.zeros(len(self.species)) fs0, _ = self._grid_factors(hn) - fs0 = torch.as_tensor(fs0, dtype=torch.float32) + fs0 = torch.as_tensor(fs0, dtype=torch.float32, device=dev) c = self.concentrations def full_eta(x): @@ -1117,7 +1326,7 @@ def full_eta(x): return e - (c * e).sum() def loss(x): - eta = torch.as_tensor(full_eta(x), dtype=torch.float32) + eta = torch.as_tensor(full_eta(x), dtype=torch.float32, device=dev) F = ((fs0 + eta[:, None] * chi[None]) * A).sum(0) self._Fr, self._Fi = F.real.contiguous(), F.imag.contiguous() self._solve_linear(self._model_diffuse()) @@ -1167,10 +1376,10 @@ def _envelope(self, i: int, q2: np.ndarray): beam weights, which ``envelope="fitted"`` re-solves. """ if self.envelope == "bloch": - g = self.geometry["bloch_g"][i] - p = self.geometry["bloch_p"][i] + g = (self.geometry.get("bloch_g") or [None] * len(self.images))[i] if g is None: raise RuntimeError("fit_thickness before envelope='bloch'") + p = self.geometry["bloch_p"][i] order = np.argsort(p)[::-1][: getattr(self, "max_beams", 40)] g, p = g[order], p[order] elif self.envelope in ("measured", "fitted"): @@ -1200,7 +1409,8 @@ def _envelope(self, i: int, q2: np.ndarray): return cols @ p, tds def _bg_lower(self, n: int) -> np.ndarray: - """Lower bounds of the background amplitudes: free sign for the constant and Kikuchi.""" + """Lower bounds of the background amplitudes: the constant may be negative, the others + are non-negative.""" lo = np.zeros(n) lo[0] = -np.inf return lo @@ -1452,6 +1662,35 @@ def run( number that adapts so the joint step never raises the loss. A sweep is one proposal per site. Scale and background are refit every ``refit_every`` sweeps. + + Parameters + ---------- + n_sweeps : int, optional + Number of sweeps. Default 20. + batch : int, optional + Moves proposed per batch. Default 32. + temperature : float, optional + Initial Metropolis temperature as a fraction of the median + absolute score change of the first batch. Default 0.05. + random_fraction : float, optional + Probability that a batch proposes single-atom displacements. + Ignored (0) if the supercell has no displacements. Default 0.5. + omega_fraction : float, optional + Probability that a batch proposes omega embryos. Ignored (0) if + the supercell has no displacements. Default 0. + max_static_b : float or None, optional + Cap on the static Debye-Waller B (A^2) of all displacements; None + for no cap. Default 0.5. + refit_every : int, optional + Sweeps between refits of the scale and background. Default 2. + progress : bool, optional + Show a progress bar. + + Returns + ------- + ReverseMonteCarlo + self. The weighted loss after each sweep is appended to + ``self.loss_history``. """ if self.coefficients is None: self.fit_background() @@ -1628,9 +1867,17 @@ def model_images(self, diffuse_only: bool = False) -> list[np.ndarray]: return out def r_factors(self) -> dict: - """Weighted R of the diffuse fit per pattern, ``sqrt(sum w (y - model)^2 / sum w (y - - background)^2)``: the fraction of the diffuse signal (experiment minus fitted - background) the supercell leaves unexplained.""" + """Weighted R of the diffuse fit per pattern. + + ``R = sqrt(sum w (y - model)^2 / sum w (y - background)^2)``: the + fraction of the diffuse signal (experiment minus fitted background) + the supercell leaves unexplained. + + Returns + ------- + dict + ``{pattern name: R}``. + """ full = self.model_images() bg = self.background_images() out = {} @@ -1654,6 +1901,17 @@ def warren_cowley(self, n_shells: int = 6) -> dict: ``(P(s | s) - c_s) / (1 - c_s)`` for like pairs, with ``P(t | s)`` the fraction of the shell around an ``s`` atom occupied by ``t``. Negative: unlike neighbours preferred; positive: like. + + Parameters + ---------- + n_shells : int, optional + Number of neighbour shells. Default 6. + + Returns + ------- + dict + "radius": (n_shells,) shell radii in A; "alpha": (n_shells, K, K) + Warren-Cowley parameters; "species": the K species in index order. """ d = self.grid_divisor n = self.cells * d @@ -2001,6 +2259,23 @@ def displacement_correlations(self, n_shells: int = 6) -> dict: ``transverse``: the same for the components normal to the bond. Omega embryos give a strong negative longitudinal correlation on the nearest-neighbour <111> bond (the collapsing pair moves together). + + Parameters + ---------- + n_shells : int, optional + Number of neighbour shells. Default 6. + + Returns + ------- + dict + "radius": (n_shells,) shell radii in A; "shell": bond-vector + labels (e.g. "1/2<111>", "<100>"); "longitudinal" and + "transverse": lists of correlations, dimensionless. + + Raises + ------ + NotImplementedError + For site lattices other than BCC. """ if self.grid_divisor != 2: raise NotImplementedError("Displacement correlations are implemented for BCC sites.") @@ -2105,6 +2380,13 @@ def plot_displacements(self, **kwargs): return fig, axs def plot_loss(self): + """Weighted loss after each sweep (``loss_history``) on a log scale. + + Returns + ------- + tuple + ``(fig, ax)``. + """ import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(5, 3)) diff --git a/src/quantem/diffraction/rotations.py b/src/quantem/diffraction/rotations.py index 3d8e72c12..60fd668b1 100644 --- a/src/quantem/diffraction/rotations.py +++ b/src/quantem/diffraction/rotations.py @@ -12,9 +12,10 @@ v_lab = R(q) @ v_crystal -The electron beam travels along -z in the lab frame. The zone axis --- the -beam direction expressed in crystal Cartesian coordinates --- is therefore -the third row of R(q):: +The electron beam travels along -z in the lab frame. The zone axis is the +crystal direction that points from the specimen back toward the source, +lab +z, expressed in crystal Cartesian coordinates. It is therefore the +third row of R(q):: zone_axis = R(q).T @ [0, 0, 1] @@ -28,7 +29,20 @@ def qmult(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: - """Hamilton product of quaternions, broadcasting over leading dims.""" + """Hamilton product a * b of quaternions, broadcasting over leading dims. + + The product applies `b` first, then `a`: R(a * b) = R(a) @ R(b). + + Parameters + ---------- + a, b : torch.Tensor + Scalar-first quaternions (..., 4), broadcastable. + + Returns + ------- + torch.Tensor + Product quaternions (..., 4), not renormalized. + """ aw, ax, ay, az = a.unbind(-1) bw, bx, by, bz = b.unbind(-1) return torch.stack( @@ -43,25 +57,75 @@ def qmult(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: def qconj(q: torch.Tensor) -> torch.Tensor: - """Quaternion conjugate (inverse for unit quaternions).""" + """Quaternion conjugate (inverse for unit quaternions). + + Parameters + ---------- + q : torch.Tensor + Scalar-first quaternions (..., 4). + + Returns + ------- + torch.Tensor + (w, -x, -y, -z), shape (..., 4). For a crystal-to-lab orientation + this is the lab-to-crystal rotation. + """ w, x, y, z = q.unbind(-1) return torch.stack((w, -x, -y, -z), dim=-1) def qnormalize(q: torch.Tensor) -> torch.Tensor: - """Normalize to unit length, with w >= 0 canonicalization.""" + """Normalize to unit length, with w >= 0 canonicalization. + + Parameters + ---------- + q : torch.Tensor + Scalar-first quaternions (..., 4), nonzero. + + Returns + ------- + torch.Tensor + Unit quaternions (..., 4) with w >= 0; q and -q describe the same + rotation, so this picks one of the two. + """ q = q / torch.linalg.norm(q, dim=-1, keepdim=True) return torch.where(q[..., :1] < 0, -q, q) def qrotate(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor: - """Rotate vectors v (..., 3) by quaternions q (..., 4).""" + """Rotate vectors by quaternions, v' = R(q) @ v. + + Parameters + ---------- + q : torch.Tensor + Unit scalar-first quaternions (..., 4). For an orientation, crystal + frame vectors are rotated into the lab frame. + v : torch.Tensor + Vectors (..., 3), broadcastable against `q`. + + Returns + ------- + torch.Tensor + Rotated vectors (..., 3). + """ qv = torch.cat((torch.zeros_like(v[..., :1]), v), dim=-1) return qmult(qmult(q, qv), qconj(q))[..., 1:] def quat_to_matrix(q: torch.Tensor) -> torch.Tensor: - """Convert quaternions (..., 4) to rotation matrices (..., 3, 3).""" + """Convert quaternions to rotation matrices. + + Parameters + ---------- + q : torch.Tensor + Unit scalar-first quaternions (..., 4). + + Returns + ------- + torch.Tensor + Rotation matrices (..., 3, 3) with v_lab = R @ v_crystal for an + orientation. + """ w, x, y, z = q.unbind(-1) two = 2.0 R = torch.stack( @@ -82,10 +146,21 @@ def quat_to_matrix(q: torch.Tensor) -> torch.Tensor: def quat_from_matrix(R: torch.Tensor) -> torch.Tensor: - """Convert rotation matrices (..., 3, 3) to unit quaternions (..., 4). + """Convert rotation matrices to unit quaternions. Uses the numerically stable branch selection of Shepperd's method, vectorized over leading dimensions. + + Parameters + ---------- + R : torch.Tensor + Proper rotation matrices (..., 3, 3). + + Returns + ------- + torch.Tensor + Unit scalar-first quaternions (..., 4) with w >= 0, the inverse of + :func:`quat_to_matrix`. """ batch_shape = R.shape[:-2] R = R.reshape(-1, 3, 3) @@ -110,7 +185,20 @@ def quat_from_matrix(R: torch.Tensor) -> torch.Tensor: def quat_from_axis_angle(axis: torch.Tensor, angle: torch.Tensor) -> torch.Tensor: - """Quaternion for rotation of `angle` (radians) about `axis` (..., 3).""" + """Quaternion for a right-handed rotation about an axis. + + Parameters + ---------- + axis : torch.Tensor + Rotation axes (..., 3); need not be normalized. + angle : torch.Tensor + Rotation angles (...,), radians. + + Returns + ------- + torch.Tensor + Unit scalar-first quaternions (..., 4). + """ axis = axis / torch.linalg.norm(axis, dim=-1, keepdim=True) angle = torch.as_tensor(angle, dtype=axis.dtype, device=axis.device) half = angle[..., None] / 2 @@ -118,7 +206,21 @@ def quat_from_axis_angle(axis: torch.Tensor, angle: torch.Tensor) -> torch.Tenso def quat_to_axis_angle(q: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: - """Return (axis (..., 3), angle (...,)) of unit quaternions.""" + """Rotation axis and angle of unit quaternions. + + Parameters + ---------- + q : torch.Tensor + Scalar-first quaternions (..., 4). + + Returns + ------- + axis : torch.Tensor + Unit rotation axes (..., 3). Undefined (near zero) for the identity. + angle : torch.Tensor + Rotation angles (...,) in [0, pi], radians, after w >= 0 + canonicalization. + """ q = qnormalize(q) angle = 2 * torch.acos(q[..., 0].clamp(-1, 1)) sin_half = torch.sqrt((1 - q[..., 0] ** 2).clamp_min(1e-24)) @@ -127,7 +229,19 @@ def quat_to_axis_angle(q: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: def quat_from_euler_zxz(angles: torch.Tensor) -> torch.Tensor: - """Quaternion from Z-X-Z Euler angles (..., 3) in radians.""" + """Quaternion from Z-X-Z Euler angles. + + Parameters + ---------- + angles : torch.Tensor + (phi1, Phi, phi2) Euler angles (..., 3), radians, applied as + R = Rz(phi1) @ Rx(Phi) @ Rz(phi2). + + Returns + ------- + torch.Tensor + Scalar-first quaternions (..., 4). + """ a, b, c = angles.unbind(-1) z = torch.zeros_like(a) qa = torch.stack((torch.cos(a / 2), z, z, torch.sin(a / 2)), dim=-1) @@ -137,7 +251,20 @@ def quat_from_euler_zxz(angles: torch.Tensor) -> torch.Tensor: def quat_to_euler_zxz(q: torch.Tensor) -> torch.Tensor: - """Z-X-Z Euler angles (..., 3) in radians from unit quaternions.""" + """Z-X-Z Euler angles from unit quaternions. + + Parameters + ---------- + q : torch.Tensor + Unit scalar-first quaternions (..., 4). + + Returns + ------- + torch.Tensor + (phi1, Phi, phi2) Euler angles (..., 3), radians, the inverse of + :func:`quat_from_euler_zxz`. In the gimbal-locked case (Phi = 0 or + pi) the whole rotation about z is put in phi1 and phi2 is 0. + """ R = quat_to_matrix(q) beta = torch.acos(R[..., 2, 2].clamp(-1, 1)) alpha = torch.atan2(R[..., 0, 2], -R[..., 1, 2]) @@ -159,14 +286,16 @@ def quat_from_zone_axis( Parameters ---------- zone_axis : torch.Tensor - Crystal-frame Cartesian direction(s) (..., 3) to place along the beam. + Crystal-frame Cartesian direction(s) (..., 3) to place along lab +z, + pointing toward the source; need not be normalized. in_plane_deg : torch.Tensor | float, default=0.0 - Additional in-plane rotation of the pattern, degrees. + Additional rotation about lab z, degrees. Returns ------- torch.Tensor - Quaternions (..., 4) such that quat_to_matrix(q).T @ [0,0,1] == zone_axis. + Unit quaternions (..., 4), crystal to lab, such that + quat_to_matrix(q).T @ [0, 0, 1] == zone_axis. """ v = zone_axis / torch.linalg.norm(zone_axis, dim=-1, keepdim=True) zhat = torch.zeros_like(v) @@ -190,7 +319,19 @@ def quat_from_zone_axis( def zone_axis_from_quat(q: torch.Tensor) -> torch.Tensor: - """Beam direction in crystal Cartesian coordinates (third row of R).""" + """Zone axis of orientations, in crystal Cartesian coordinates. + + Parameters + ---------- + q : torch.Tensor + Unit scalar-first quaternions (..., 4), crystal to lab. + + Returns + ------- + torch.Tensor + Unit crystal directions (..., 3) along lab +z, toward the source: + the third row of R(q). + """ return quat_to_matrix(q)[..., 2, :] @@ -228,10 +369,30 @@ def misorientation_axis_angle( qb: torch.Tensor, sym_ops: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: - """Symmetry-reduced misorientation axis (crystal frame) and angle. + """Symmetry-reduced misorientation axis and angle. - Returns the rotation axis (..., 3) in crystal Cartesian coordinates and - the angle (...,) in degrees, minimized over the symmetry operators. + The misorientation dq = conj(qa) * qb takes the crystal frame of `qa` + onto that of `qb`, R(qb) = R(qa) @ R(dq). Among the equivalent + dq * s over the symmetry operators s, the one with the smallest angle is + returned. + + Parameters + ---------- + qa, qb : torch.Tensor + Unit scalar-first quaternions (..., 4), crystal to lab, + broadcastable against each other. + sym_ops : torch.Tensor | None + Proper rotation symmetry quaternions (S, 4) of the crystal. If None, + the raw misorientation is returned. + + Returns + ------- + axis : torch.Tensor + Unit rotation axes (..., 3) in the crystal Cartesian frame of `qa`. + Undefined (near zero) when the angle is zero. + angle : torch.Tensor + Misorientation angles (...,) in degrees, in [0, 180]; equal to + :func:`misorientation_angle_deg` for the same inputs. """ dq = qmult(qconj(qa), qb) if sym_ops is not None: @@ -244,7 +405,20 @@ def misorientation_axis_angle( def slerp(v0: torch.Tensor, v1: torch.Tensor, t: torch.Tensor) -> torch.Tensor: - """Spherical linear interpolation between unit vectors v0 and v1.""" + """Spherical linear interpolation between directions. + + Parameters + ---------- + v0, v1 : torch.Tensor + End directions (..., 3); normalized internally. + t : torch.Tensor + Interpolation fractions (...,), 0 at `v0` and 1 at `v1`. + + Returns + ------- + torch.Tensor + Unit vectors (..., 3) along the great circle from `v0` to `v1`. + """ v0 = v0 / torch.linalg.norm(v0, dim=-1, keepdim=True) v1 = v1 / torch.linalg.norm(v1, dim=-1, keepdim=True) omega = torch.acos((v0 * v1).sum(-1, keepdim=True).clamp(-1, 1)) @@ -311,9 +485,20 @@ def sample_zone_axes( def symmetry_axes(sym_quats: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Distinct rotation axes of a proper point group and their orders. - Returns (axes (A, 3) unit vectors, orders (A,) long): each axis once, - with the highest rotation order about it (a 4-fold axis is listed as - order 4, not also as 2). + Parameters + ---------- + sym_quats : torch.Tensor + Proper rotation quaternions (S, 4) of the group, crystal Cartesian + frame. + + Returns + ------- + axes : torch.Tensor + Unit axes (A, 3), each listed once with the sign that makes it + point into the upper hemisphere (+z, then +y, then +x on ties). + orders : torch.Tensor + Long (A,), the highest rotation order about each axis (a 4-fold + axis is listed as order 4, not also as 2). """ axis, angle = quat_to_axis_angle(sym_quats) keep = angle > 1e-6 @@ -362,6 +547,23 @@ def symmetry_aligned( into a common symmetry branch first. For each input this returns the symmetry-equivalent quaternion whose misorientation to the reference is smallest, which makes a weighted quaternion mean well defined. + + Parameters + ---------- + reference : torch.Tensor + Unit scalar-first quaternion (4,), crystal to lab. + quats : torch.Tensor + Unit quaternions, reshaped to (N, 4). + sym_quats : torch.Tensor + Proper rotation symmetry quaternions (S, 4) of the crystal, crystal + Cartesian frame. Each candidate is q * s, the same lab orientation + of the symmetric crystal. + + Returns + ------- + torch.Tensor + (N, 4) float64 quaternions with w >= 0, each describing the same + orientation as the corresponding input. """ ref = torch.as_tensor(reference, dtype=torch.float64).reshape(4) q = torch.as_tensor(quats, dtype=torch.float64).reshape(-1, 4) @@ -387,6 +589,21 @@ def sample_zone_axis_cap( Points are placed on a Fibonacci spiral restricted to the cap, which gives an equal-area covering; the count follows the cap area divided by `step_deg` squared. + + Parameters + ---------- + axis : torch.Tensor + Center of the cap (3,), crystal Cartesian; need not be normalized. + half_angle_deg : float + Angular radius of the cap, degrees. + step_deg : float + Approximate spacing between neighboring directions, degrees. + + Returns + ------- + torch.Tensor + Unit directions (N, 3), float64, all within `half_angle_deg` of + `axis`; (1, 3) holding `axis` itself when `half_angle_deg` is zero. """ axis = torch.as_tensor(axis, dtype=torch.float64) axis = axis / torch.linalg.norm(axis).clamp_min(1e-12) @@ -426,9 +643,18 @@ def fundamental_zone_axis_wedge(sym_quats: torch.Tensor) -> torch.Tensor | None: non-standard Cartesian setting the corners follow the axes wherever they point. - Returns (3, 3) corner directions, or None for Laue classes -1 and 2/m - whose fundamental domain is not a spherical triangle (sample the - hemisphere instead). + Parameters + ---------- + sym_quats : torch.Tensor + Proper rotation quaternions (S, 4) of the group, crystal Cartesian + frame. + + Returns + ------- + torch.Tensor | None + Corner directions as rows (3, 3), or None for Laue classes -1 and + 2/m whose fundamental domain is not a spherical triangle (sample the + hemisphere instead). """ axes, orders = symmetry_axes(sym_quats) n_ops = sym_quats.shape[0] @@ -485,10 +711,28 @@ def fundamental_zone_axis_wedge(sym_quats: torch.Tensor) -> torch.Tensor | None: def symmetry_reduced_zone_angles( zone_axes: torch.Tensor, sym_quats: torch.Tensor, chunk: int = 8 ) -> torch.Tensor: - """(Z, Z) angular distances between zone axes, minimized over the - symmetry operations and the inversion (zone axes are directions modulo - sign). Symmetry-equivalent zones are at distance zero, so an exclusion - ball around a match also excludes its symmetry copies.""" + """Angular distances between zone axes, minimized over symmetry. + + The minimum is over the symmetry operations and the inversion (zone axes + are directions modulo sign). Symmetry-equivalent zones are at distance + zero, so an exclusion ball around a match also excludes its symmetry + copies. + + Parameters + ---------- + zone_axes : torch.Tensor + Unit directions (Z, 3), crystal Cartesian. + sym_quats : torch.Tensor + Proper rotation quaternions (S, 4) of the crystal. + chunk : int, default=8 + Symmetry operations processed at once, which bounds the memory to + chunk * Z * Z. + + Returns + ------- + torch.Tensor + (Z, Z) angles in degrees. + """ Rs = quat_to_matrix(sym_quats).to(zone_axes.dtype) best = torch.full((zone_axes.shape[0],) * 2, -1.0, dtype=zone_axes.dtype) for s0 in range(0, Rs.shape[0], chunk): diff --git a/src/quantem/diffraction/strain.py b/src/quantem/diffraction/strain.py index 0c000e364..cde29b50b 100644 --- a/src/quantem/diffraction/strain.py +++ b/src/quantem/diffraction/strain.py @@ -18,9 +18,15 @@ class StrainMap(AutoSerialize): """Strain tensor maps fit from per-position lattice vectors. Stores the reference-frame strain components ``e_rr`` (row), ``e_cc`` (col), - ``e_rc`` (shear), and ``phi`` (infinitesimal rotation). The reference lattice - is the median of the fitted ``g_u``/``g_v`` over a mask/ROI; the strain tensor - is recomputed by :meth:`update_reference`. + ``e_rc`` (shear), and ``phi`` (infinitesimal rotation, radians). The reference + lattice is the weighted median (or mean) of the fitted ``g1``/``g2`` over a + mask/ROI; the strain tensor is recomputed by :meth:`update_reference`. + + The lattice vectors are first mapped from the detector frame into the scan + frame (optional row/col transpose, then a counter-clockwise rotation by + ``q_to_r_rotation_ccw_deg``), so ``e_rr``/``e_cc`` refer to the scan rows and + columns. :attr:`g1_array`, :attr:`g2_array`, :attr:`g1_ref` and :attr:`g2_ref` + are stored in that rotated frame. Two measurement modalities are supported and give identical strain for the same deformation, so correlation and cepstral maps can be compared directly: @@ -29,9 +35,9 @@ class StrainMap(AutoSerialize): Parameters ---------- - u_array : np.ndarray + g1_array : np.ndarray Per-position first lattice vector, shape ``(scan_row, scan_col, 2)``. - v_array : np.ndarray + g2_array : np.ndarray Per-position second lattice vector, shape ``(scan_row, scan_col, 2)``. ds_shape : tuple of int Shape of the parent scan grid, used to size the strain maps. @@ -39,12 +45,12 @@ class StrainMap(AutoSerialize): ``False`` for reciprocal-space (Bragg/correlation) lattice vectors; ``True`` for real-space (cepstral autocorrelation / DPC) vectors. Both modalities are arranged to yield matching strain (see :func:`_strain_tensor`). - u_ref : np.ndarray, optional - Fixed reference for ``u``; if omitted the median over the mask/ROI is used. - A value supplied here persists across re-fits. - v_ref : np.ndarray, optional - Fixed reference for ``v``; if omitted the median over the mask/ROI is used. - A value supplied here persists across re-fits. + g1_ref : np.ndarray, optional + Fixed reference for ``g1``, in the same (unrotated) frame as ``g1_array``; + if omitted the ``calculation_metric`` over the mask/ROI is used. A value + supplied here persists across re-fits. + g2_ref : np.ndarray, optional + Fixed reference for ``g2``, as ``g1_ref``. mask : np.ndarray, optional ``(scan_row, scan_col)`` weighting/ROI mask; defaults to all ones (the full scan). Normalized to ``[0, 1]`` on assignment. @@ -52,6 +58,15 @@ class StrainMap(AutoSerialize): Real-space scan sampling (step size); defaults to ``1.0``. ds_units : str, optional Units for ``ds_sampling``; defaults to ``"pixels"``. + q_to_r_rotation_ccw_deg : float, default=0.0 + Counter-clockwise rotation in degrees from the detector (``q``) frame to + the scan (``r``) frame, applied to the lattice vectors and to ``g1_ref`` / + ``g2_ref``. + q_transpose : bool, default=False + If ``True``, swap the detector row/col axes before the rotation. + calculation_metric : {"median", "mean"}, default="median" + Statistic used for the automatic reference lattice. Stored and reused by + :meth:`update_reference` unless overridden there. """ mask: np.ndarray | None = None @@ -62,38 +77,45 @@ class StrainMap(AutoSerialize): e_rc: Dataset2d phi: Dataset2d - u_ref: np.ndarray | None = None - v_ref: np.ndarray | None = None - u_array: np.ndarray - v_array: np.ndarray + g1_ref: np.ndarray | None = None + g2_ref: np.ndarray | None = None + g1_array: np.ndarray + g2_array: np.ndarray ds_sampling: float = 1.0 ds_units: str = "pixels" ds_shape: tuple[int, ...] + calculation_metric: str = "median" + def __init__( self, - u_array: np.ndarray, - v_array: np.ndarray, + g1_array: np.ndarray, + g2_array: np.ndarray, ds_shape: tuple[int, ...], real_space: bool, - u_ref: np.ndarray | None = None, - v_ref: np.ndarray | None = None, + g1_ref: np.ndarray | None = None, + g2_ref: np.ndarray | None = None, mask: np.ndarray | None = None, ds_sampling: float | None = None, ds_units: str | None = None, q_to_r_rotation_ccw_deg: float = 0.0, q_transpose: bool = False, + calculation_metric: str = "median", ): super().__init__() - self.u_array = u_array - self.v_array = v_array - + self.g1_array = g1_array + self.g2_array = g2_array + self.q_to_r_rotation_ccw_deg = q_to_r_rotation_ccw_deg self.q_transpose = q_transpose - self.u_array = _raw_vec_to_display(self.u_array, rotation_ccw_deg = q_to_r_rotation_ccw_deg, transpose=q_transpose) - self.v_array = _raw_vec_to_display(self.v_array, rotation_ccw_deg = q_to_r_rotation_ccw_deg, transpose=q_transpose) + self.g1_array = _raw_vec_to_display( + self.g1_array, rotation_ccw_deg=q_to_r_rotation_ccw_deg, transpose=q_transpose + ) + self.g2_array = _raw_vec_to_display( + self.g2_array, rotation_ccw_deg=q_to_r_rotation_ccw_deg, transpose=q_transpose + ) self.ds_shape = ds_shape self.real_space = real_space @@ -110,53 +132,72 @@ def __init__( m = (m - m_lo) / (m_hi - m_lo) self.mask = m - # user-supplied reference vectors persist across re-fits (None = use median) - self._u_ref_fixed = None if u_ref is None else _raw_vec_to_display(np.asarray(u_ref, dtype=float), - rotation_ccw_deg=q_to_r_rotation_ccw_deg, - transpose=q_transpose) - self._v_ref_fixed = None if v_ref is None else _raw_vec_to_display(np.asarray(v_ref, dtype=float), - rotation_ccw_deg=q_to_r_rotation_ccw_deg, - transpose=q_transpose) - self.u_ref = None - self.v_ref = None - - self.update_reference() + # user-supplied reference vectors persist across re-fits (None = automatic) + self.g1_ref_fixed = ( + None + if g1_ref is None + else _raw_vec_to_display( + np.asarray(g1_ref, dtype=float), + rotation_ccw_deg=q_to_r_rotation_ccw_deg, + transpose=q_transpose, + ) + ) + self.g2_ref_fixed = ( + None + if g2_ref is None + else _raw_vec_to_display( + np.asarray(g2_ref, dtype=float), + rotation_ccw_deg=q_to_r_rotation_ccw_deg, + transpose=q_transpose, + ) + ) + self.g1_ref = None + self.g2_ref = None + self.calculation_metric = calculation_metric + self.update_reference(calculation_metric=calculation_metric) # ---- main methods ---- def update_reference( self, strain_mask: np.ndarray | None = None, - u_ref: np.ndarray | None = None, - v_ref: np.ndarray | None = None, + g1_ref: np.ndarray | None = None, + g2_ref: np.ndarray | None = None, plot_strain_roi: bool = False, define_in_rotated_frame: bool = False, + calculation_metric: str | None = None, **plot_kwargs, ) -> "StrainMap": """(Re)compute the reference lattice and strain tensor maps. - Reference precedence: explicit ``u_ref``/``v_ref`` argument > vectors fixed at - construction > median over ``strain_mask`` (if given) else over ``self.mask`` - else the global median. + Reference precedence: explicit ``g1_ref``/``g2_ref`` argument > vectors fixed at + construction > ``calculation_metric`` (weighted median or mean) over + ``strain_mask`` (if given) else weighted by ``self.mask``. Parameters ---------- strain_mask : np.ndarray, optional - ``(scan_row, scan_col)`` ROI selecting the positions used to compute the - median reference lattice. If omitted, ``self.mask`` (else the global - median) is used. - u_ref : np.ndarray, optional - Explicit reference for ``u``; overrides both the construction-time fixed + ``(scan_row, scan_col)`` ROI or weights selecting the positions used to + compute the automatic reference lattice. If omitted, ``self.mask`` is + used. + g1_ref : np.ndarray, optional + Explicit reference for ``g1``; overrides both the construction-time fixed value and the median. - v_ref : np.ndarray, optional - Explicit reference for ``v``; overrides both the construction-time fixed + g2_ref : np.ndarray, optional + Explicit reference for ``g2``; overrides both the construction-time fixed value and the median. plot_strain_roi : bool, default=False If ``True``, show the recomputed strain via :meth:`plot_strain_roi` (color-scaled to the ROI) so the chosen reference region can be checked for flatness. - define_in_rotated_frame: bool, default = False - If ''True'' means the u_ref and v_ref passed into the function is defined in the rotated detector frame + define_in_rotated_frame : bool, default=False + If ``True``, the ``g1_ref`` and ``g2_ref`` passed here are already in + the rotated (scan) frame; otherwise they are in the detector frame and + are rotated like the lattice vectors. + calculation_metric : {"median", "mean"}, optional + Statistic for the automatic reference lattice. If given, it is stored on the + object and used by later calls; if ``None`` (default), the stored value is + used. **plot_kwargs Forwarded to :meth:`plot_strain_roi` when ``plot_strain_roi=True``. @@ -165,36 +206,46 @@ def update_reference( StrainMap ``self``, with the reference lattice and strain maps recomputed. """ - u_med, v_med = _reference_lattice(self.u_array, self.v_array, self.mask, strain_mask) + if calculation_metric is not None: + self.calculation_metric = calculation_metric + g1_med, g2_med = _reference_lattice( + self.g1_array, + self.g2_array, + self.mask, + strain_mask, + calculation_metric=self.calculation_metric, + ) - if u_ref is not None: + if g1_ref is not None: if define_in_rotated_frame: - self.u_ref = np.asarray(u_ref, dtype=float) + self.g1_ref = np.asarray(g1_ref, dtype=float) else: - self.u_ref = _raw_vec_to_display( - np.asarray(u_ref, dtype=float), - rotation_ccw_deg=self.q_to_r_rotation_ccw_deg, - transpose=self.q_transpose) - elif self._u_ref_fixed is not None: - self.u_ref = self._u_ref_fixed + self.g1_ref = _raw_vec_to_display( + np.asarray(g1_ref, dtype=float), + rotation_ccw_deg=self.q_to_r_rotation_ccw_deg, + transpose=self.q_transpose, + ) + elif self.g1_ref_fixed is not None: + self.g1_ref = self.g1_ref_fixed else: - self.u_ref = u_med + self.g1_ref = g1_med - if v_ref is not None: + if g2_ref is not None: if define_in_rotated_frame: - self.v_ref = np.asarray(v_ref, dtype=float) + self.g2_ref = np.asarray(g2_ref, dtype=float) else: - self.v_ref = _raw_vec_to_display( - np.asarray(v_ref, dtype=float), - rotation_ccw_deg=self.q_to_r_rotation_ccw_deg, - transpose=self.q_transpose) - elif self._v_ref_fixed is not None: - self.v_ref = self._v_ref_fixed + self.g2_ref = _raw_vec_to_display( + np.asarray(g2_ref, dtype=float), + rotation_ccw_deg=self.q_to_r_rotation_ccw_deg, + transpose=self.q_transpose, + ) + elif self.g2_ref_fixed is not None: + self.g2_ref = self.g2_ref_fixed else: - self.v_ref = v_med + self.g2_ref = g2_med e_rr, e_cc, e_rc, phi = _strain_tensor( - self.u_array, self.v_array, self.u_ref, self.v_ref, self.real_space + self.g1_array, self.g2_array, self.g1_ref, self.g2_ref, self.real_space ) self.e_rr = Dataset2d.from_array(e_rr, name="strain e_rr", signal_units="fractional") self.e_cc = Dataset2d.from_array(e_cc, name="strain e_cc", signal_units="fractional") @@ -265,21 +316,24 @@ def plot_strain_roi( Colormap for the strain panels. cmap_rotation : str, default="PiYG" Colormap for the rotation panel. - strain_range_percent : tuple of float, default=(-3.0, 3.0) - Symmetric color range for the strain panels, in percent. - rotation_range_degrees : tuple of float, default=(-2.0, 2.0) - Symmetric color range for the rotation panel, in degrees. - transpose_image: bool, default = False - If ''True'' transpose the real space image before plotting strain - rotate_title: bool, default = False - If ''True'', rotates panel titles by 90 degrees. - plot_dilation: bool, default = False - If ''True'' plots euu + evv, and euv instead of euu, evv, euv + strain_range_percent : tuple of float, optional + Color range for the strain panels, in percent. ``None`` (default) uses + a symmetric range set by the largest absolute strain inside the ROI. + rotation_range_degrees : tuple of float, optional + Color range for the rotation panel, in degrees. ``None`` (default) uses + a symmetric range set by the largest absolute rotation inside the ROI. + transpose_image : bool, default=False + If ``True``, transpose the real-space images before plotting. + rotate_title : bool, default=False + If ``True``, rotate the panel titles by 90 degrees. + plot_dilation : bool, default=False + If ``True``, plot ``e_rr + e_cc`` and ``e_rc`` instead of ``e_rr``, + ``e_cc`` and ``e_rc``. layout : {"horizontal", "vertical"}, default="horizontal" Panel arrangement. - arrow_style: str, default="title" - Plots the directional arrows along with the strain titles. - Alternatively can be "legend" where it plots it on the side + arrow_style : {"title", "legend"}, default="title" + Accepted for symmetry with :meth:`plot_strain`; the row/col titles + carry their own arrows, so no extra arrows are drawn here. figsize : tuple of float, optional Figure size in inches; if omitted it is derived from the layout. **kwargs @@ -295,6 +349,18 @@ def plot_strain_roi( if arrow_style not in ("title", "legend"): raise ValueError("arrow_style must be 'title' or 'legend'") + if plot_dilation: + panel_titles = ( + r"$\epsilon_{rr} + \epsilon_{cc}$", + r"$\epsilon_{rc}$ $\nwarrow\!\!\!\!\!\!\!\!\!\:\searrow$", + ) + else: + panel_titles = ( + r"$\epsilon_{rr}$ $\updownarrow$", + r"$\epsilon_{cc}$ $\leftrightarrow$", + r"$\epsilon_{rc}$ $\nwarrow\!\!\!\!\!\!\!\!\!\:\searrow$", + ) + roi_src = self.mask if strain_mask is None else strain_mask e_rr, e_cc, e_rc, phi = ( self.e_rr.array, @@ -319,28 +385,28 @@ def plot_strain_roi( e_rc, phi, self.mask, - self.u_ref, - self.v_ref, + self.g1_ref, + self.g2_ref, self.ds_shape, ds_sampling=self.ds_sampling, ds_units=self.ds_units, - strain_range_percent=(-smax, smax) if strain_range_percent is None else strain_range_percent, - rotation_range_degrees=(-rmax, rmax) if rotation_range_degrees is None else rotation_range_degrees, + strain_range_percent=(-smax, smax) + if strain_range_percent is None + else strain_range_percent, + rotation_range_degrees=(-rmax, rmax) + if rotation_range_degrees is None + else rotation_range_degrees, roi=inside, plot_rotation=plot_rotation, cmap_strain=cmap_strain, cmap_rotation=cmap_rotation, layout=layout, - transpose_image = transpose_image, - rotate_title = rotate_title, - plot_dilation = plot_dilation, + transpose_image=transpose_image, + rotate_title=rotate_title, + plot_dilation=plot_dilation, figsize=figsize, - panel_titles=( - r"$\epsilon_{rr}$ $\updownarrow$", - r"$\epsilon_{cc}$ $\leftrightarrow$", - r"$\epsilon_{rc}$ $\nwarrow\!\!\!\!\!\!\!\!\!\:\searrow$", - ), - arrow_style = arrow_style, + panel_titles=panel_titles, + arrow_style=arrow_style, **kwargs, ) @@ -390,25 +456,26 @@ def plot_strain( Colormap for the strain panels. cmap_rotation : str, default="PiYG" Colormap for the rotation panel. - transpose_image: bool, default = False - If ''True'' transpose the real space image before plotting strain + transpose_image : bool, default=False + If ``True``, transpose the real-space images before plotting. transpose_strain : bool, default=False - If ``True``, transpose the detector (row/col) axes before rotating, - matching the DPC convention (see - :func:`~quantem.diffraction.strain_autocorrelation._raw_vec_to_display`): - transpose first, then rotate. This swaps the normal strain components, - leaves the shear unchanged, and reverses the sign of the rotation field. - rotate_title: bool, default = False - If ''True'', rotates panel titles by 90 degrees. - plot_dilation: bool, default = False - If ''True'' plots euu + evv, and euv instead of euu, evv, euv + If ``True``, transpose the (row/col) axes of the strain tensor before + rotating, matching the DPC convention of :func:`_raw_vec_to_display` + (transpose first, then rotate). This swaps the normal strain + components, leaves the shear unchanged, and reverses the sign of the + rotation field. + rotate_title : bool, default=False + If ``True``, rotate the panel titles by 90 degrees. + plot_dilation : bool, default=False + If ``True``, plot ``e_uu + e_vv`` and ``e_uv`` instead of ``e_uu``, + ``e_vv`` and ``e_uv``. layout : {"horizontal", "vertical"}, default="horizontal" Panel arrangement. + arrow_style : {"title", "legend"}, default="title" + Draw the strain direction arrows next to the panel titles, or in a + legend at the side of the figure. figsize : tuple of float, optional Figure size in inches; if omitted it is derived from the layout. - arrow_style: str, default="title" - Plots the directional arrows along with the strain titles. - Alternatively can be "legend" where it plots it on the side **kwargs Forwarded to :func:`~quantem.diffraction.strain_visualization.plot_strain_panels`. @@ -420,7 +487,7 @@ def plot_strain( """ if arrow_style not in ("title", "legend"): raise ValueError("arrow_style must be 'title' or 'legend'") - + e_rr = self.e_rr.array e_cc = self.e_cc.array e_rc = self.e_rc.array @@ -439,8 +506,8 @@ def plot_strain( e_uv, phi, self.mask, - self.u_ref, - self.v_ref, + self.g1_ref, + self.g2_ref, self.ds_shape, ds_sampling=self.ds_sampling, ds_units=self.ds_units, @@ -453,12 +520,12 @@ def plot_strain( cmap_strain=cmap_strain, cmap_rotation=cmap_rotation, layout=layout, - transpose_image = transpose_image, - rotate_title = rotate_title, - plot_dilation = plot_dilation, + transpose_image=transpose_image, + rotate_title=rotate_title, + plot_dilation=plot_dilation, figsize=figsize, strain_rotation_angle=rotation_angle, - arrow_style = arrow_style, + arrow_style=arrow_style, **kwargs, ) @@ -469,11 +536,13 @@ def estimate_strain_precision( window: int = 5, mask_threshold: float = 0.5, min_neighbors: int = 3, + require_full_neighborhood: bool = True, component: str = "combined", bins: int = 50, bounds: tuple[float, float] | None = None, plot: bool = True, returnfig: bool = False, + verbose: bool = False, ): """Estimate strain *precision* (random scatter) from local median deviations. @@ -530,7 +599,12 @@ def estimate_strain_precision( pixel cannot leak its scatter into the precision. min_neighbors : int, default=3 Minimum number of valid neighbors required; positions with fewer get no - precision estimate (``nan``, dropped from the statistics). + precision estimate (``nan``, dropped from the statistics). Ignored when + ``require_full_neighborhood=True``. + require_full_neighborhood : bool, default=True + If ``True``, require every neighbor in the footprint to be valid (sets + ``min_neighbors`` to the footprint size), so positions next to masked + or missing data get no estimate. component : {"combined","e_uu","e_vv","e_uv","rotation"}, default="combined" Which error distribution to histogram. bins : int, default=50 @@ -549,6 +623,8 @@ def estimate_strain_precision( If ``True``, draw the weighted precision histogram. returnfig : bool, default=False If ``True``, return ``(fig, ax)`` instead of the results dict. + verbose : bool, default=False + If ``True``, print a summary of the precision per component. Returns ------- @@ -570,7 +646,9 @@ def estimate_strain_precision( # number of neighbors in the circular footprint (matches _local_masked_median) p = window // 2 oy, ox = np.ogrid[-p : p + 1, -p : p + 1] - n_neighbors = int(np.sum((oy ** 2 + ox ** 2) <= (window / 2.0) ** 2) - 1) + n_neighbors = int(np.sum((oy**2 + ox**2) <= (window / 2.0) ** 2) - 1) + if require_full_neighborhood: + min_neighbors = n_neighbors # per-component fields in the (optionally rotated) display frame; phi is # rotation-invariant and is carried through unchanged @@ -597,9 +675,7 @@ def estimate_strain_precision( # deviations). Rotation is reported separately, not folded in: in nanobeam # data it is partly a systematic (tilt/descan) and would mix radians into a # percent figure. - dev["combined"] = np.sqrt( - dev["e_uu"] ** 2 + dev["e_vv"] ** 2 + 2.0 * dev["e_uv"] ** 2 - ) + dev["combined"] = np.sqrt(dev["e_uu"] ** 2 + dev["e_vv"] ** 2 + 2.0 * dev["e_uv"] ** 2) # display-unit scaling: strain -> percent, rotation -> degrees scale = { @@ -669,21 +745,24 @@ def _weighted_median(err_native: np.ndarray, factor: float) -> float: "mask_threshold": float(mask_threshold), "mask_range": (low, high), "rotation_angle": float(rotation_angle), + "min_neighbors": int(min_neighbors), + "require_full_neighborhood": bool(require_full_neighborhood), } - print("Strain precision (median local deviation, mask-weighted)") - print( - f" reference={n_neighbors} neighbors (disk, window={window}) " - f"mask>{mask_threshold:g} min_neighbors={min_neighbors} " - f"rotation_angle={rotation_angle:g} deg" - ) - for name in ("e_uu", "e_vv", "e_uv"): - print(f" {name:<9}: {precision[name]:7.4f} %") - print(f" {'rotation':<9}: {precision['rotation']:7.4f} deg") - print( - f" {'combined':<9}: {precision['combined']:7.4f} % " - "(strain-only Frobenius norm; rotation excluded)" - ) + if verbose: + print("Strain precision (median local deviation, mask-weighted)") + print( + f" reference={n_neighbors} neighbors (disk, window={window}) " + f"mask>{mask_threshold:g} min_neighbors={min_neighbors} " + f"rotation_angle={rotation_angle:g} deg" + ) + for name in ("e_uu", "e_vv", "e_uv"): + print(f" {name:<9}: {precision[name]:7.4f} %") + print(f" {'rotation':<9}: {precision['rotation']:7.4f} deg") + print( + f" {'combined':<9}: {precision['combined']:7.4f} % " + "(strain-only Frobenius norm; rotation excluded)" + ) if not (plot or returnfig): return result @@ -759,7 +838,7 @@ def _local_masked_median( fw[:, :, p, p] = np.nan # exclude the center position from its own median # restrict the square box to a circular footprint of radius window/2 oy, ox = np.ogrid[-p : p + 1, -p : p + 1] - outside = (oy ** 2 + ox ** 2) > (window / 2.0) ** 2 + outside = (oy**2 + ox**2) > (window / 2.0) ** 2 fw[:, :, outside] = np.nan flat = fw.reshape(fw.shape[0], fw.shape[1], -1) @@ -772,41 +851,52 @@ def _local_masked_median( def _reference_lattice( - u_array: np.ndarray, - v_array: np.ndarray, + g1_array: np.ndarray, + g2_array: np.ndarray, mask: np.ndarray | None = None, strain_mask: np.ndarray | None = None, + calculation_metric: str = "median", ) -> tuple[np.ndarray, np.ndarray]: - """Weighted-median reference lattice vectors, else the global median. - - The reference is the per-component **weighted median** of the lattice vectors. - Weights come from ``strain_mask`` if given, else the continuous ``mask`` - (the ``[0, 1]`` per-position weight from :meth:`create_mask` / ``fit_lattice``): - strong, well-indexed positions dominate the reference and weak / vacuum / bad-fit - positions are down-weighted. A boolean ROI (weights in ``{0, 1}``) reduces to the - plain median over the selected positions, so an explicit ``strain_mask`` behaves - as before. The weighted median (not ``mask == 1``) is used because a continuous - weight rarely hits *exactly* 1 -- the old exact-equality test collapsed a min-max - normalized mask to its single global-max position and made the reference one - arbitrary pixel. + """Reference lattice vectors: weighted median or mean over a mask/ROI. + + Each component of the reference is the weighted median + (``calculation_metric="median"``) or weighted mean (``"mean"``) of the lattice + vectors over finite positions with positive weight. Weights come from + ``strain_mask`` if given, else from the continuous ``mask`` (the ``[0, 1]`` + per-position weight, e.g. ``BraggVectors.mask_weight`` from + ``BraggVectors.fit_lattice``). A boolean ROI + (weights in ``{0, 1}``) reduces to the plain median/mean over the selected + positions. If no position has positive weight, the unweighted statistic over all + finite positions is used. A continuous weight (rather than ``mask == 1``) is used + because a min-max normalized mask rarely hits exactly 1. Parameters ---------- - u_array : np.ndarray + g1_array : np.ndarray Per-position first lattice vector, shape ``(scan_row, scan_col, 2)``. - v_array : np.ndarray + g2_array : np.ndarray Per-position second lattice vector, shape ``(scan_row, scan_col, 2)``. mask : np.ndarray, optional - ``(scan_row, scan_col)`` per-position weight in ``[0, 1]``. Used as the median + ``(scan_row, scan_col)`` per-position weight in ``[0, 1]``. Used as the weights when ``strain_mask`` is not given. strain_mask : np.ndarray, optional ``(scan_row, scan_col)`` ROI / weight taking precedence over ``mask``. + calculation_metric : {"median", "mean"}, default="median" + Statistic used for the reference: weighted median or weighted mean. Returns ------- tuple of np.ndarray - ``(u_ref, v_ref)``, each a length-2 reference vector. + ``(g1_ref, g2_ref)``, each a length-2 reference vector. """ + if calculation_metric not in ("mean", "median"): + raise ValueError("calculation metric must be mean or median") + + def reduce(v: np.ndarray, ww: np.ndarray) -> float: + if calculation_metric == "mean": + return float(np.average(v, weights=ww)) + return _weighted_quantile(v, ww, 0.5) + if strain_mask is not None: w = np.asarray(strain_mask, dtype=float).reshape(-1) elif mask is not None: @@ -814,31 +904,31 @@ def _reference_lattice( else: w = None - u_flat = u_array.reshape(-1, 2) - v_flat = v_array.reshape(-1, 2) + g1_flat = g1_array.reshape(-1, 2) + g2_flat = g2_array.reshape(-1, 2) def _wmed(vals: np.ndarray) -> float: - # weighted median over finite, positively-weighted positions; positions + # weighted statistic over finite, positively-weighted positions; positions # fit_lattice could not fit are NaN and must be dropped, else the reference # (and the whole strain map) collapses to NaN. Falls back to the unweighted - # nan-median when no weight is given or none survives. + # statistic over finite positions when no weight survives. finite = np.isfinite(vals) ww = np.ones_like(vals) if w is None else w use = finite & (ww > 0) if not use.any(): - return float(np.nanmedian(vals)) if finite.any() else float("nan") - return _weighted_quantile(vals[use], ww[use], 0.5) + return reduce(vals[finite], np.ones(finite.sum())) if finite.any() else float("nan") + return reduce(vals[use], ww[use]) - u_ref = np.array((_wmed(u_flat[:, 0]), _wmed(u_flat[:, 1])), dtype=float) - v_ref = np.array((_wmed(v_flat[:, 0]), _wmed(v_flat[:, 1])), dtype=float) - return u_ref, v_ref + g1_ref = np.array((_wmed(g1_flat[:, 0]), _wmed(g1_flat[:, 1])), dtype=float) + g2_ref = np.array((_wmed(g2_flat[:, 0]), _wmed(g2_flat[:, 1])), dtype=float) + return g1_ref, g2_ref def _strain_tensor( - u_array: np.ndarray, - v_array: np.ndarray, - u_ref: np.ndarray, - v_ref: np.ndarray, + g1_array: np.ndarray, + g2_array: np.ndarray, + g1_ref: np.ndarray, + g2_ref: np.ndarray, real_space: bool, ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Per-position strain tensor from lattice vectors relative to a reference. @@ -855,19 +945,21 @@ def _strain_tensor( ``strain_trans = (U @ inv(U_ref)).T``. Both expressions evaluate to ``F.T`` (the transpose of the real-space deformation - gradient), so the normal strains, shear, and rotation come out the same - regardless of modality, and the reciprocal-space sign convention (``const = -1``, - which sets the shear/rotation handedness) is shared. + gradient ``F``), so the normal strains, shear, and rotation come out the same + regardless of modality: ``e_rr = F[0, 0] - 1``, ``e_cc = F[1, 1] - 1``, + ``e_rc = (F[0, 1] + F[1, 0]) / 2`` and ``phi = (F[1, 0] - F[0, 1]) / 2``, so a + lattice rotated counter-clockwise in the (row, col) frame by a small angle + ``theta`` gives ``phi = theta``. Parameters ---------- - u_array : np.ndarray + g1_array : np.ndarray Per-position first lattice vector, shape ``(scan_row, scan_col, 2)``. - v_array : np.ndarray + g2_array : np.ndarray Per-position second lattice vector, shape ``(scan_row, scan_col, 2)``. - u_ref : np.ndarray + g1_ref : np.ndarray Reference first lattice vector (length 2). - v_ref : np.ndarray + g2_ref : np.ndarray Reference second lattice vector (length 2). real_space : bool ``False`` for reciprocal-space (Bragg/correlation) vectors; ``True`` for @@ -879,8 +971,8 @@ def _strain_tensor( tuple of np.ndarray ``(e_rr, e_cc, e_rc, phi)``, each of shape ``(scan_row, scan_col)``. """ - scan_r, scan_c = u_array.shape[0], u_array.shape[1] - Uref = np.stack((u_ref, v_ref), axis=1).astype(float) + scan_r, scan_c = g1_array.shape[0], g1_array.shape[1] + Uref = np.stack((g1_ref, g2_ref), axis=1).astype(float) strain_trans = np.zeros((scan_r, scan_c, 2, 2)) # For real-space vectors the reference is inverted once (it is shared by every @@ -891,7 +983,7 @@ def _strain_tensor( for r in range(scan_r): for c in range(scan_c): - U = np.stack((u_array[r, c, :], v_array[r, c, :]), axis=1) + U = np.stack((g1_array[r, c, :], g2_array[r, c, :]), axis=1) # Positions fit_lattice could not fit are NaN; a degenerate (collinear) # fit is singular. Either way there is no meaningful inverse -- leave the # strain NaN (masked out downstream) rather than feeding NaN into pinv, @@ -909,13 +1001,10 @@ def _strain_tensor( # reciprocal-space vectors contract under tension: U_ref @ U^-1 == F.T strain_trans[r, c, :, :] = Uref @ np.linalg.inv(U) - # const = -1 is the reciprocal-space (nanobeam) shear/rotation convention. Both - # modalities reduce strain_trans to F.T above, so the convention is shared. - const = 1 if real_space else -1 e_rr = strain_trans[:, :, 0, 0] - 1 e_cc = strain_trans[:, :, 1, 1] - 1 - e_rc = strain_trans[:, :, 1, 0] * 0.5 * const + strain_trans[:, :, 0, 1] * 0.5 * const - phi = strain_trans[:, :, 1, 0] * -0.5 * const + strain_trans[:, :, 0, 1] * 0.5 * const + e_rc = strain_trans[:, :, 1, 0] * 0.5 + strain_trans[:, :, 0, 1] * 0.5 + phi = strain_trans[:, :, 1, 0] * -0.5 + strain_trans[:, :, 0, 1] * 0.5 return e_rr, e_cc, e_rc, phi @@ -924,7 +1013,6 @@ def _rotate_strain_tensor( e_cc: np.ndarray, e_rc: np.ndarray, rotation_angle: float, - real_space=False ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Rotate a 2D strain tensor by ``rotation_angle`` (degrees). @@ -938,8 +1026,7 @@ def _rotate_strain_tensor( Row-column (shear) strain component. rotation_angle : float Frame rotation angle, in degrees. - real_space: bool - Tells whether vector is defined in real or reciprocal space + Returns ------- tuple of np.ndarray @@ -948,17 +1035,31 @@ def _rotate_strain_tensor( angle = np.deg2rad(rotation_angle) c = np.cos(angle) s = np.sin(angle) - sign = -1.0 if real_space else 1.0 - e_uu = e_rr * (c * c) + sign * 2.0 * e_rc * (c * s) + e_cc * (s * s) - e_vv = e_rr * (s * s) - sign * 2.0 * e_rc * (c * s) + e_cc * (c * c) - e_uv = sign * (e_cc - e_rr) * (c * s) + e_rc * (c * c - s * s) + e_uu = e_rr * (c * c) + 2.0 * e_rc * (c * s) + e_cc * (s * s) + e_vv = e_rr * (s * s) - 2.0 * e_rc * (c * s) + e_cc * (c * c) + e_uv = (e_cc - e_rr) * (c * s) + e_rc * (c * c - s * s) return e_uu, e_vv, e_uv + def _raw_vec_to_display(vec_rc: NDArray, *, rotation_ccw_deg: float, transpose: bool) -> NDArray: """Map a raw-detector ``(row, col)`` vector into the rotated display frame. Applies the optional axis transpose, then a counter-clockwise rotation of - ``rotation_ccw_deg``. Inverse of :func:`_display_vec_to_raw`. + ``rotation_ccw_deg``. + + Parameters + ---------- + vec_rc : np.ndarray + Vector(s) with ``(row, col)`` in the last axis, shape ``(..., 2)``. + rotation_ccw_deg : float + Counter-clockwise rotation in degrees. + transpose : bool + If ``True``, swap row and col before rotating. + + Returns + ------- + np.ndarray + Rotated vector(s), same shape as ``vec_rc``. """ v = np.asarray(vec_rc, dtype=float) dr, dc = v[..., 0], v[..., 1] @@ -972,4 +1073,4 @@ def _raw_vec_to_display(vec_rc: NDArray, *, rotation_ccw_deg: float, transpose: dr2 = ct * dr - st * dc dc2 = st * dr + ct * dc - return np.stack((dr2, dc2), axis=-1) \ No newline at end of file + return np.stack((dr2, dc2), axis=-1) diff --git a/src/quantem/diffraction/strain_visualization.py b/src/quantem/diffraction/strain_visualization.py index 17fa5adcd..f899e1685 100644 --- a/src/quantem/diffraction/strain_visualization.py +++ b/src/quantem/diffraction/strain_visualization.py @@ -1,5 +1,7 @@ from __future__ import annotations +import warnings + import matplotlib.pyplot as plt import numpy as np from matplotlib.cm import ScalarMappable @@ -16,8 +18,8 @@ def plot_strain_panels( e_uv: np.ndarray, rotation: np.ndarray, mask: np.ndarray | None, - u_ref: np.ndarray | None, - v_ref: np.ndarray | None, + g1_ref: np.ndarray | None, + g2_ref: np.ndarray | None, ds_shape: tuple[int, ...], ds_sampling: float = 1.0, ds_units: str = "pixels", @@ -54,6 +56,73 @@ def plot_strain_panels( When ``roi`` (a boolean ``(scan_row, scan_col)`` array) is given, positions inside it are drawn in color and positions outside it in greyscale (the same field, desaturated), so a chosen reference region stands out from its context. + + Parameters + ---------- + e_uu, e_vv, e_uv : np.ndarray + ``(scan_row, scan_col)`` fractional strain components in the display frame. + rotation : np.ndarray + ``(scan_row, scan_col)`` infinitesimal rotation in radians. + mask : np.ndarray or None + ``(scan_row, scan_col)`` brightness weights in ``[0, 1]``; ``None`` shows + every position at full brightness. + g1_ref, g2_ref : np.ndarray or None + Reference lattice vectors ``(row, col)``; only their directions are drawn, + and only when ``plot_gvecs=True``. + ds_shape : tuple of int + Scan shape; the first two entries size the default mask and the scale bar. + ds_sampling : float, default=1.0 + Scan step size used by the scale bar, in ``ds_units``. + ds_units : str, default="pixels" + Units of ``ds_sampling``. + strain_range_percent : tuple of float, default=(-3.0, 3.0) + Color range of the strain panels, in percent. + rotation_range_degrees : tuple of float, default=(-2.0, 2.0) + Color range of the rotation panel, in degrees. + mask_range : tuple of float, default=(0.0, 1.0) + ``(low, high)`` window remapping ``mask`` before display (see above). + roi : np.ndarray, optional + Boolean ``(scan_row, scan_col)`` region drawn in color; the rest is grey. + plot_rotation : bool, default=True + Whether to add the rotation panel. + plot_gvecs : bool, default=False + Whether to draw the directions of ``g1_ref`` and ``g2_ref`` beside the panels. + plot_scalebar : bool, default=False + Whether to draw a scale bar on the first panel. + cmap_strain : str, default="RdBu_r" + Colormap of the strain panels. + cmap_rotation : str, default="PiYG" + Colormap of the rotation panel; ``None`` uses ``cmap_strain``. + layout : {"horizontal", "vertical"}, default="horizontal" + Panel arrangement. + transpose_image : bool, default=False + If ``True``, transpose every panel (swap scan rows and columns) for display. + rotate_title : bool, default=False + If ``True``, draw the panel titles vertically. + plot_dilation : bool, default=False + If ``True``, show ``e_uu + e_vv`` and ``e_uv`` (plus rotation) instead of + the three strain components. Forces the rotation panel on. + figsize : tuple of float, optional + Figure size in inches; derived from ``layout`` if omitted. + panel_titles : tuple of str, optional + Titles of the strain panels (three, or two with ``plot_dilation``). When + given, no direction arrows are drawn. + strain_rotation_angle : float, default=0.0 + Angle in degrees by which the default direction arrows are rotated, to + match a strain tensor rotated by this angle. + arrow_style : {"title", "legend"}, default="title" + Draw the direction arrows next to the panel titles, or in a legend at the + side of the figure. + **kwargs + Keys starting with ``scalebar_`` are passed to the scale bar with the prefix + removed (for example ``scalebar_length``, ``scalebar_color``, + ``scalebar_box``, ``scalebar_box_color``, ``scalebar_box_alpha``). Other + keys are ignored. + + Returns + ------- + tuple + ``(fig, ax)`` with ``ax`` the array of panel axes. """ if mask is None: mask = np.ones(ds_shape[:2]) @@ -75,7 +144,7 @@ def plot_strain_panels( ncols = 4 if plot_rotation else 3 is_horizontal = layout == "horizontal" if plot_dilation: - ncols=3 + ncols = 3 plot_rotation = True n_strain = 2 if plot_dilation else 3 @@ -116,16 +185,16 @@ def _roi_compose(norm_vals, color_cm): euv_disp = _roi_compose(norm_strain(euv_pct), cm_strain) if transpose_image: - euu_disp = euu_disp.transpose(1,0,2) - evv_disp = evv_disp.transpose(1,0,2) - euv_disp = euv_disp.transpose(1,0,2) + euu_disp = euu_disp.transpose(1, 0, 2) + evv_disp = evv_disp.transpose(1, 0, 2) + euv_disp = euv_disp.transpose(1, 0, 2) mask = mask.T - + if plot_dilation: etot_pct = (e_uu + e_vv) * 100 etot_disp = _roi_compose(norm_strain(etot_pct), cm_strain) if transpose_image: - etot_disp = etot_disp.transpose(1,0,2) + etot_disp = etot_disp.transpose(1, 0, 2) ax[0].imshow(etot_disp * mask[:, :, np.newaxis]) ax[1].imshow(euv_disp * mask[:, :, np.newaxis]) else: @@ -133,13 +202,12 @@ def _roi_compose(norm_vals, color_cm): ax[1].imshow(evv_disp * mask[:, :, np.newaxis]) ax[2].imshow(euv_disp * mask[:, :, np.newaxis]) - ref_dim = figsize[1] if is_horizontal else figsize[0] fs_threshold = 3.0 fs_scale = min(1.0, max(0.5, ref_dim / fs_threshold)) title_fs = 16 * fs_scale tick_fs = 12 * fs_scale - title_val = 'vertical' if rotate_title else 'horizontal' + title_val = "vertical" if rotate_title else "horizontal" if panel_titles is None: if plot_dilation: panel_titles = ( @@ -154,22 +222,31 @@ def _roi_compose(norm_vals, color_cm): r"$\epsilon_{vv}$", r"$\epsilon_{uv}$", ) - title_arrow_angles = (0 + strain_rotation_angle, 90 + strain_rotation_angle, -45 + strain_rotation_angle) + title_arrow_angles = ( + 90 + strain_rotation_angle, + 0 + strain_rotation_angle, + -45 + strain_rotation_angle, + ) + if transpose_image: + title_arrow_angles = ( + 0 + strain_rotation_angle, + 90 + strain_rotation_angle, + 45 + strain_rotation_angle, + ) else: - title_arrow_angles = (None, None, None) - + title_arrow_angles = (None, None, None) if plot_rotation: norm_rot = Normalize(vmin=rotation_range_degrees[0], vmax=rotation_range_degrees[1]) rot_disp = _roi_compose(norm_rot(rot_deg), cm_rot) - if transpose_image: rot_disp = rot_disp.transpose(1,0,2) + if transpose_image: + rot_disp = rot_disp.transpose(1, 0, 2) ax[-1].imshow(rot_disp * mask[:, :, np.newaxis]) if arrow_style == "title": ax[-1].set_title(r"$\phi$ $\circlearrowleft$", fontsize=title_fs, rotation=title_val) else: ax[-1].set_title(r"$\phi$", fontsize=title_fs, rotation=title_val) - for a in ax: a.set_xticks([]) a.set_yticks([]) @@ -181,7 +258,7 @@ def _roi_compose(norm_vals, color_cm): scalebar_kwargs = {} for key, value in kwargs.items(): if key.startswith("scalebar_"): - scalebar_key = key[len("scalebar_"):] + scalebar_key = key[len("scalebar_") :] scalebar_kwargs[scalebar_key] = value # default: white bar on a translucent black box, readable on the @@ -308,26 +385,32 @@ def _finalize_layout(): cbar2.update_ticks() cbar2.ax.tick_params(labelsize=tick_fs) - def _add_title_arrow(ax, angle_deg, gap_pt=4.0, color="black", fontsize=None): - fs = fontsize if fontsize is not None else title_fs - try: - ax.figure.draw_without_rendering() - except AttributeError: # matplotlib < 3.5 - ax.figure.canvas.draw() - renderer = ax.figure.canvas.get_renderer() - bbox_ax = ax.title.get_window_extent(renderer=renderer).transformed(ax.transAxes.inverted()) - y = 0.5 * (bbox_ax.y0 + bbox_ax.y1) - ax.annotate( - "\u2194", - xy=(bbox_ax.x1, y), xycoords=ax.transAxes, - xytext=(gap_pt + fs / 2.0, 0), textcoords="offset points", - ha="center", va="center", - rotation=angle_deg, rotation_mode="anchor", - fontsize=fs, color=color, - annotation_clip=False, - ) - + fs = fontsize if fontsize is not None else title_fs + try: + ax.figure.draw_without_rendering() + except AttributeError: # matplotlib < 3.5 + ax.figure.canvas.draw() + renderer = ax.figure.canvas.get_renderer() + bbox_ax = ax.title.get_window_extent(renderer=renderer).transformed( + ax.transAxes.inverted() + ) + y = 0.5 * (bbox_ax.y0 + bbox_ax.y1) + ax.annotate( + "\u2194", + xy=(bbox_ax.x1, y), + xycoords=ax.transAxes, + xytext=(gap_pt + fs / 2.0, 0), + textcoords="offset points", + ha="center", + va="center", + rotation=angle_deg, + rotation_mode="anchor", + fontsize=fs, + color=color, + annotation_clip=False, + ) + def _add_arrow_legend(fig, x0, y_top, entries, plot_rotation, fontsize, color="black"): fig_w_in, fig_h_in = figsize row_h_in = fontsize * 1.6 / 72.0 @@ -346,13 +429,24 @@ def _add_arrow_legend(fig, x0, y_top, entries, plot_rotation, fontsize, color="b leg_ax.text(0.0, y, "Strain", fontsize=fontsize, fontweight="bold", ha="left", va="center") for label, angle_deg in entries: y -= dy - leg_ax.text(0.15, y, "\u2194", rotation=angle_deg, rotation_mode="anchor", - ha="center", va="center", fontsize=fontsize, color=color) + leg_ax.text( + 0.15, + y, + "\u2194", + rotation=angle_deg, + rotation_mode="anchor", + ha="center", + va="center", + fontsize=fontsize, + color=color, + ) leg_ax.text(0.32, y, label, fontsize=fontsize, ha="left", va="center") if plot_rotation: y -= dy - leg_ax.text(0.0, y, "Rotation", fontsize=fontsize, fontweight="bold", ha="left", va="center") + leg_ax.text( + 0.0, y, "Rotation", fontsize=fontsize, fontweight="bold", ha="left", va="center" + ) y -= dy leg_ax.text(0.15, y, "\u21ba", fontsize=fontsize, ha="center", va="center") leg_ax.text(0.32, y, r"$\phi$", fontsize=fontsize, ha="left", va="center") @@ -369,7 +463,12 @@ def _add_arrow_legend(fig, x0, y_top, entries, plot_rotation, fontsize, color="b renderer = fig.canvas.get_renderer() panel_edge = last_pos.x1 if plot_rotation: - title_edge = ax[-1].title.get_window_extent(renderer=renderer).transformed(fig.transFigure.inverted()).x1 + title_edge = ( + ax[-1] + .title.get_window_extent(renderer=renderer) + .transformed(fig.transFigure.inverted()) + .x1 + ) panel_edge = max(panel_edge, title_edge) margin_x0 = panel_edge + 0.03 else: @@ -386,14 +485,21 @@ def _add_arrow_legend(fig, x0, y_top, entries, plot_rotation, fontsize, color="b entries = [] leg_h = 0.0 if arrow_style == "legend": - entries = [(panel_titles[i], title_arrow_angles[i]) for i in range(n_strain) - if title_arrow_angles[i] is not None] + entries = [ + (panel_titles[i], title_arrow_angles[i]) + for i in range(n_strain) + if title_arrow_angles[i] is not None + ] n_rows = len(entries) + 1 + (2 if plot_rotation else 0) leg_h = (title_fs * 1.6 / 72.0 / figsize[1]) * n_rows - show_gvecs = plot_gvecs and u_ref is not None and v_ref is not None + show_gvecs = plot_gvecs and g1_ref is not None and g2_ref is not None if plot_gvecs and not show_gvecs: - print("Warning: u_ref and v_ref not found. Call fit_strain() first.") + warnings.warn( + "plot_gvecs=True but g1_ref and g2_ref are not set; run " + "StrainMap.update_reference() first.", + UserWarning, + ) fig_aspect = figsize[0] / figsize[1] gvec_w = min(0.99 - margin_x0, 0.15) if show_gvecs else 0.0 gvec_h = gvec_w * fig_aspect if show_gvecs else 0.0 @@ -410,8 +516,15 @@ def _add_arrow_legend(fig, x0, y_top, entries, plot_rotation, fontsize, color="b y_top = center_y + (leg_h + gap * side_scale + gvec_h) / 2.0 if leg_h > 0: - _add_arrow_legend(fig, margin_x0, y_top, entries, plot_rotation=plot_rotation, - fontsize=legend_fontsize, color="black") + _add_arrow_legend( + fig, + margin_x0, + y_top, + entries, + plot_rotation=plot_rotation, + fontsize=legend_fontsize, + color="black", + ) y_top -= leg_h + gap * side_scale if show_gvecs: @@ -420,17 +533,37 @@ def _add_arrow_legend(fig, x0, y_top, entries, plot_rotation, fontsize, color="b ref_ax.set_ylim(-1.5, 1.5) ref_ax.set_aspect("equal") ref_ax.axis("off") - u_norm = u_ref / np.linalg.norm(u_ref) - v_norm = v_ref / np.linalg.norm(v_ref) - u_row, u_col = u_norm - v_row, v_col = v_norm + g1_norm = g1_ref / np.linalg.norm(g1_ref) + g2_norm = g2_ref / np.linalg.norm(g2_ref) + g1_row, g1_col = g1_norm + g2_row, g2_col = g2_norm arrow_props_ref = dict(arrowstyle="->", lw=3, mutation_scale=25) - ref_ax.add_patch(FancyArrowPatch((0, 0), (u_col, -u_row), color="darkred", **arrow_props_ref)) - ref_ax.add_patch(FancyArrowPatch((0, 0), (v_col, -v_row), color="darkblue", **arrow_props_ref)) - ref_ax.text(u_col * 1.3, -u_row * 1.3, r"$\mathbf{g}_{1}$", fontsize=14, fontweight="bold", - color="darkred", ha="center", va="center") - ref_ax.text(v_col * 1.3, -v_row * 1.3, r"$\mathbf{g}_{2}$", fontsize=14, fontweight="bold", - color="darkblue", ha="center", va="center") + ref_ax.add_patch( + FancyArrowPatch((0, 0), (g1_col, -g1_row), color="darkred", **arrow_props_ref) + ) + ref_ax.add_patch( + FancyArrowPatch((0, 0), (g2_col, -g2_row), color="darkblue", **arrow_props_ref) + ) + ref_ax.text( + g1_col * 1.3, + -g1_row * 1.3, + r"$\mathbf{g}_{1}$", + fontsize=14, + fontweight="bold", + color="darkred", + ha="center", + va="center", + ) + ref_ax.text( + g2_col * 1.3, + -g2_row * 1.3, + r"$\mathbf{g}_{2}$", + fontsize=14, + fontweight="bold", + color="darkblue", + ha="center", + va="center", + ) return fig, ax @@ -450,6 +583,27 @@ def plot_strain_precision_histogram( chosen ``component`` deviation in display units (``unit``). ``precision`` is the weighted-median local deviation per component (used for the annotation box); the plotted component's median is marked with a solid line. + + Parameters + ---------- + edges : np.ndarray + ``(bins + 1,)`` histogram bin edges, in ``unit``. + counts : np.ndarray + ``(bins,)`` weighted fraction of positions in each bin. + precision : dict of str to float + Median local deviation per component; must contain ``"e_uu"``, ``"e_vv"``, + ``"e_uv"`` (percent), ``"rotation"`` (degrees) and ``"combined"`` (percent). + component : str + Key of ``precision`` that was histogrammed; its median is marked. + unit : str + Display unit of ``component`` (``"%"`` or ``"°"``). + figsize : tuple of float, default=(6.0, 4.0) + Figure size in inches. + + Returns + ------- + tuple + ``(fig, ax)``. """ fig, ax = plt.subplots(figsize=figsize) edges = np.asarray(edges, dtype=float) @@ -457,8 +611,15 @@ def plot_strain_precision_histogram( centers = 0.5 * (edges[:-1] + edges[1:]) widths = np.diff(edges) - ax.bar(centers, counts, width=widths, align="center", - color="#4C72B0", edgecolor="white", linewidth=0.3) + ax.bar( + centers, + counts, + width=widths, + align="center", + color="#4C72B0", + edgecolor="white", + linewidth=0.3, + ) median_value = precision[component] if np.isfinite(median_value): @@ -469,10 +630,14 @@ def plot_strain_precision_histogram( on_right = span > 0 and (median_value - edges[0]) / span > 0.5 ax.annotate( f"median = {median_value:.3g} {unit}", - xy=(median_value, 0.96), xycoords=("data", "axes fraction"), - xytext=(-6 if on_right else 6, 0), textcoords="offset points", - ha="right" if on_right else "left", va="top", - color="crimson", fontsize=9, + xy=(median_value, 0.96), + xycoords=("data", "axes fraction"), + xytext=(-6 if on_right else 6, 0), + textcoords="offset points", + ha="right" if on_right else "left", + va="top", + color="crimson", + fontsize=9, ) label = "combined" if component == "combined" else component @@ -491,9 +656,17 @@ def plot_strain_precision_histogram( rf" combined: {precision['combined']:.3g} %", ] ) - ax.text(0.97, 0.97, annotation, transform=ax.transAxes, ha="right", va="top", - fontsize=9, family="monospace", - bbox=dict(boxstyle="round", fc="white", ec="0.7", alpha=0.9)) + ax.text( + 0.97, + 0.97, + annotation, + transform=ax.transAxes, + ha="right", + va="top", + fontsize=9, + family="monospace", + bbox=dict(boxstyle="round", fc="white", ec="0.7", alpha=0.9), + ) fig.tight_layout() - return fig, ax \ No newline at end of file + return fig, ax diff --git a/src/quantem/diffraction/wk_scattering_factors.py b/src/quantem/diffraction/wk_scattering_factors.py index 09419e127..744acefd8 100644 --- a/src/quantem/diffraction/wk_scattering_factors.py +++ b/src/quantem/diffraction/wk_scattering_factors.py @@ -1,90 +1,83 @@ -import numpy as np -from scipy.special import expi - -# from functools import lru_cache - -from quantem.core.utils.utils import electron_wavelength_angstrom - -""" -Weickenmeier-Kohl absorptive electron scattering factors. +"""Weickenmeier-Kohl absorptive electron scattering factors. Elastic form factors use the 8-parameter fit of Weickenmeier & Kohl, Acta Cryst. A47, 590 (1991); the absorptive (core-loss and phonon/TDS) -parts are computed analytically from the same fit. This implementation -was adapted by SE Zeltmann for py4DSTEM from EMsoftLib/others.f90 by -Marc De Graef, who adapted it from Weickenmeier's original F77 code; -vendored here from py4DSTEM with only the import adjusted. +parts are computed analytically from the same fit. The implementation +follows Weickenmeier's original F77 code as adapted by Marc De Graef in +EMsoftLib/others.f90, translated to Python by SE Zeltmann for py4DSTEM and +vectorized over g by Colin Ophus. It was vendored from py4DSTEM with one +correction: the middle branch of WEKO now starts at argu >= 0.1, as in +others.f90 (py4DSTEM used argu >= 1.0, which dropped every term with argu +in [0.1, 1) and made the form factors non-monotonic at small g). Comments +in quotation marks are carried over from the original code. """ +import numpy as np +from scipy.special import expi + +from quantem.core.utils.utils import electron_wavelength_angstrom + def compute_WK_factor( g: np.ndarray, Z: int, accelerating_voltage: float, - thermal_sigma: float = None, + thermal_sigma: float | None = None, include_core: bool = True, include_phonon: bool = True, - verbose=False, -) -> np.complex128: - """ - Compute the Weickenmeier-Kohl atomic scattering factors, using the parameterization - of the elastic part and computation of the inelastic part found in EMsoftLib/others.f90. - Return value should be in Å. - - This implementation always returns the absorptive, relativistically corrected factors. - - Currently this is mostly a direct translation of the Fortran code, along with - the accompanying comments from the original in quotation marks. Colin Ophus - vectorized it around v0.13.17. Currently it is only vectorized over `g` (i.e. - `Z` and all other args must be a single value.) - - This method uses an 8-parameter fit to the elastic form factors, and then computes the - absorptive form factors using an analytic solution based on that fitting function. - - Args: (note that these values cannot be arrays: the code is not vectorized) - g (float/ndarray): Scattering vector magnitude in the crystallographic/py4DSTEM - convention, 1/d_hkl in units of 1/Å - Z (int): Atomic number. Data are available for H thru Cf (1 thru 98) - accelerating_voltage (float): Accelerating voltage in eV. - thermal_sigma (float): RMS atomic displacement for TDS, in Å - (This is often written as 〈u〉in papers) - include_core (bool): If True, include the core loss contribution to the absorptive - form factors. - include_phonon (bool): If True, include the phonon/TDS contribution to the - absorptive form factors. - Returns: - Fscatt (np.complex128): The computed atomic form factor +) -> np.ndarray: + """Absorptive, relativistically corrected Weickenmeier-Kohl scattering factors. + + The elastic part uses the 8-parameter fit of the elastic form factors; + the absorptive part is computed analytically from the same fitting + function, following EMsoftLib/others.f90. Vectorized over `g` only. + + Parameters + ---------- + g : array-like + Scattering vector magnitudes 1/d_hkl (crystallographic convention, + no 2 pi), 1/Angstroms. + Z : int + Atomic number, 1 (H) to 98 (Cf). + accelerating_voltage : float + Beam energy, eV. + thermal_sigma : float | None + RMS atomic displacement for the Debye-Waller factor and the TDS + absorption, Angstroms (often written in papers). None or 0 + means no thermal motion: no Debye-Waller damping and no phonon + absorption. + include_core : bool, default=True + Include the core-loss contribution to the absorptive form factor. + include_phonon : bool, default=True + Include the phonon/TDS contribution to the absorptive form factor. + + Returns + ------- + np.ndarray + Complex128 form factors, same shape as `g`, Angstroms. The real part + is elastic, the imaginary part absorptive. """ + g = np.atleast_1d(np.asarray(g, dtype=float)) - # the WK Fortran code works in weird units: - # lowercase "g", our input, is the standard crystallographic quantity, in Å^-1 - # uppercase "G" is the "G" in others.f90:FSCATT, g * 2π - # uppercase "S" is the "S" in others.f90:FSCATT, G / 4π = g / 2 + # the WK Fortran code works in its own units: + # lowercase "g", our input, is the standard crystallographic quantity, in A^-1 + # uppercase "G" is the "G" in others.f90:FSCATT, g * 2 pi + # uppercase "S" is the "S" in others.f90:FSCATT, G / 4 pi = g / 2 G = g * 2.0 * np.pi S = g / 2.0 - if verbose: - print(f"S:{S}") - accelerating_voltage_kV = accelerating_voltage / 1.0e3 if thermal_sigma is not None: - UL = thermal_sigma + UL = float(thermal_sigma) DWF = np.exp(-0.5 * UL**2 * G**2) else: UL = 0.0 DWF = 1.0 - if verbose: - print(f"DWF:{DWF}") - A = WK_A_param[int(Z) - 1] B = WK_B_param[int(Z) - 1] - if verbose: - print(f"A:{A}") - print(f"B:{B}") - # WEKO(A,B,S) # NOTE: the py4DSTEM version this was vendored from used `argu >= 1.0` # for the middle branch, silently dropping every term with argu in @@ -102,9 +95,6 @@ def compute_WK_factor( Freal = 4.0 * np.pi * DWF * WK - if verbose: - print(f"Freal:{Freal}") - ################################################# # calculate "core" contribution, following FCORE: k0 = ( @@ -157,8 +147,7 @@ def compute_WK_factor( 1.0 / (OMEGA[sub] * np.sqrt(O2[sub] + 4.0 * (1.0 + K2))) * np.log( - (OMEGA[sub] + np.sqrt(O2[sub] + 4.0 * (1.0 + K2))) - / (2.0 * np.sqrt(1.0 + K2)) + (OMEGA[sub] + np.sqrt(O2[sub] + 4.0 * (1.0 + K2))) / (2.0 * np.sqrt(1.0 + K2)) ) ) sub = np.logical_not(sub) @@ -168,18 +157,15 @@ def compute_WK_factor( A0 = 0.5289 Fcore = 4.0 / (A0 * A0) * 2.0 * np.pi / (k0 * k0) * HI - - if verbose: - print(f"Fcore:{Fcore}") else: Fcore = 0.0 ########################################################## # calculate phonon contribution, following FPHON(G,UL,A,B) + # without thermal motion RI2 equals RI1 and the phonon term vanishes; + # evaluating it at U = 0 instead gives inf - inf in the asymptotic branch Fphon = 0.0 - if include_phonon: - U2 = UL**2 - + if include_phonon and UL > 0.0: A1 = A * (4.0 * np.pi) ** 2 B1 = B / (4.0 * np.pi) ** 2 @@ -191,25 +177,17 @@ def compute_WK_factor( * A1[ii] * (DWF * RI1(B1[ii], B1[jj], G) - RI2(B1[ii], B1[jj], G, UL)) ) - if verbose: - print(f"Fphon:{Fphon}") Fimag = (Fcore * DWF) + Fphon # perform relativistic correction gamma = (accelerating_voltage_kV + 511.0) / (511.0) - if verbose: - print(f"gamma:{gamma}") + Fscatt = np.asarray((Freal * gamma) + (1.0j * (Fimag * gamma**2 / k0)), dtype=np.complex128) - Fscatt = np.complex128((Freal * gamma) + (1.0j * (Fimag * gamma**2 / k0))) - - if verbose: - print(f"Fscatt:{Fscatt}") - - return ( - Fscatt * 0.4787801 * 0.664840340614319 / (4.0 * np.pi) - ) # convert to Å, and remove extra physicist factors, as performed in diffraction.f90:427,576,630 + # convert to Angstroms and remove the extra physicist factors, as in + # diffraction.f90:427,576,630 + return Fscatt * 0.4787801 * 0.664840340614319 / (4.0 * np.pi) ############################################## @@ -239,13 +217,9 @@ def RI1(BI, BJ, G): - 2.0 * expi(-BI * BJ * G[sub] ** 2 / (BI + BJ)) ) - ri1[sub] += RIH1( - BI * G[sub] ** 2, BI * G[sub] ** 2 * BI / (BI + BJ), BI * G[sub] ** 2 - ) + ri1[sub] += RIH1(BI * G[sub] ** 2, BI * G[sub] ** 2 * BI / (BI + BJ), BI * G[sub] ** 2) - ri1[sub] += RIH1( - BJ * G[sub] ** 2, BJ * G[sub] ** 2 * BJ / (BI + BJ), BJ * G[sub] ** 2 - ) + ri1[sub] += RIH1(BJ * G[sub] ** 2, BJ * G[sub] ** 2 * BJ / (BI + BJ), BJ * G[sub] ** 2) ri1[sub] *= np.pi / G[sub] ** 2 return ri1 @@ -289,12 +263,8 @@ def RI2(BI, BJ, G, U): ri2[sub] += np.pi * G2[sub] * TEMP sub = EPS > 0.1 - ri2[sub] = expi(-0.5 * U2 * G2[sub] * BIUH / BIU) + expi( - -0.5 * U2 * G2[sub] * BJUH / BJU - ) - ri2[sub] -= expi(-BIUH * BJUH * G2[sub] / (BIUH + BJUH)) + expi( - -0.25 * U2 * G2[sub] - ) + ri2[sub] = expi(-0.5 * U2 * G2[sub] * BIUH / BIU) + expi(-0.5 * U2 * G2[sub] * BJUH / BJU) + ri2[sub] -= expi(-BIUH * BJUH * G2[sub] / (BIUH + BJUH)) + expi(-0.25 * U2 * G2[sub]) ri2[sub] *= 2.0 X1 = 0.5 * U2 * G2[sub] X2 = 0.25 * U2 * G2[sub] @@ -329,14 +299,13 @@ def RIH1(X1, X2, X3): rih1[sub] = np.exp(-X1[sub]) * (expi(X2[sub]) - expi(X3[sub])) sub = np.logical_and(X2 > 20.0, X3 <= 20.0) - rih1[sub] = np.exp(X2[sub] - X1[sub]) * RIH2(X2[sub]) / X2[sub] - np.exp( - -X1[sub] - ) * expi(X3[sub]) + rih1[sub] = np.exp(X2[sub] - X1[sub]) * RIH2(X2[sub]) / X2[sub] - np.exp(-X1[sub]) * expi( + X3[sub] + ) sub = np.logical_and(X2 <= 20.0, X3 > 20.0) rih1[sub] = ( - np.exp(-X1[sub]) * expi(X2[sub]) - - np.exp(X3[sub] - X1[sub]) * RIH2(X3[sub]) / X3[sub] + np.exp(-X1[sub]) * expi(X2[sub]) - np.exp(X3[sub] - X1[sub]) * RIH2(X3[sub]) / X3[sub] ) sub = np.logical_and(X2 > 20.0, X3 > 20.0) @@ -362,15 +331,6 @@ def RIH2(X): return sig -# NOTE - This function is present in EMSoftLib but apparently not used. -def RIH3(X): - # "WERTET DEN AUSDRUCK EXP(-X) * EI(X) AUS" - if X <= 20.0: - return np.exp(-X) * expi(X) - else: - return RIH2(X) / X - - ################## # TABULATED DATA # ################## diff --git a/tests/core/test_clustering.py b/tests/core/test_clustering.py index 37cd47544..d2bfa6ca2 100644 --- a/tests/core/test_clustering.py +++ b/tests/core/test_clustering.py @@ -41,12 +41,8 @@ def test_cluster_vector_and_filter(): inten = rng.uniform(1, 2, (n, 1)) row.append(np.concatenate([q, inten], axis=1)) nested.append(row) - vec = Vector.from_data( - nested, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" - ) - labeled, labels = cluster_vector( - vec, fields=("qx", "qy"), eps=0.05, min_samples=10 - ) + vec = Vector.from_data(nested, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t") + labeled, labels = cluster_vector(vec, fields=("qx", "qy"), eps=0.05, min_samples=10) assert "cluster" in labeled.fields assert labels.max() == 0 # exactly one cluster found got = labeled[1, 2].numpy() @@ -55,3 +51,15 @@ def test_cluster_vector_and_filter(): kept = filter_rows(vec, labels == 0) assert kept.total_rows == int((labels == 0).sum()) assert kept.shape[:2] == vec.shape[:2] + + +def test_filter_rows_checks_mask_length(): + import pytest + + from quantem.core.datastructures.vector import Vector + + nested = [[np.ones((2, 1)), np.ones((3, 1))]] + vec = Vector.from_data(nested, fields=["intensity"], units=["counts"], name="t") + assert filter_rows(vec, [True, False, True, True, False]).total_rows == 3 + with pytest.raises(ValueError, match="rows"): + filter_rows(vec, [True, False]) diff --git a/tests/core/test_file_readers.py b/tests/core/test_file_readers.py new file mode 100644 index 000000000..3d2c5123e --- /dev/null +++ b/tests/core/test_file_readers.py @@ -0,0 +1,93 @@ +import numpy as np +import pytest + +from quantem.core.io import file_readers +from quantem.core.io.file_readers import _resolve_rsciio_plugin, read_4dstem + + +class TestResolveRsciioPlugin: + def test_plugin_name(self): + assert _resolve_rsciio_plugin("x.dat", "digitalmicrograph") == "rsciio.digitalmicrograph" + assert _resolve_rsciio_plugin("x.dat", "DigitalMicrograph") == "rsciio.digitalmicrograph" + + def test_unique_extension(self): + assert _resolve_rsciio_plugin("scan.dm4") == "rsciio.digitalmicrograph" + assert _resolve_rsciio_plugin("scan.DM3") == "rsciio.digitalmicrograph" + assert _resolve_rsciio_plugin("scan.mib") == "rsciio.quantumdetector" + assert _resolve_rsciio_plugin("x.dat", file_type="mib") == "rsciio.quantumdetector" + + def test_extension_equal_to_plugin_name_wins(self): + # "mrc" is listed by both the MRC and MRCZ plugins + assert _resolve_rsciio_plugin("stack.mrc") == "rsciio.mrc" + + def test_ambiguous_extension_raises(self): + with pytest.raises(ValueError, match="file_type") as err: + _resolve_rsciio_plugin("data.h5") + assert "arina" in str(err.value) + + def test_unknown_or_missing_type_raises(self): + with pytest.raises(ValueError, match="No RosettaSciIO reader"): + _resolve_rsciio_plugin("data.notaformat") + with pytest.raises(ValueError, match="Cannot infer"): + _resolve_rsciio_plugin("data_without_extension") + + +def _fake_reader(monkeypatch, entries): + def file_reader(path, **kwargs): + return entries + + monkeypatch.setattr( + file_readers, "_rsciio_reader", lambda path, file_type=None: ("rsciio.fake", file_reader) + ) + + +def _axis(scale, offset, units, name): + return {"scale": scale, "offset": offset, "units": units, "name": name} + + +@pytest.mark.parametrize("scan_axis", [0, 1]) +def test_read_4dstem_reshapes_3d_stack(monkeypatch, scan_axis): + n_frames, ny, nx, scan_length = 12, 5, 7, 4 + frames = np.arange(n_frames * ny * nx, dtype=np.float32).reshape(n_frames, ny, nx) + det_y = _axis(0.1, -0.25, "1/nm", "ky") + det_x = _axis(0.2, -0.7, "1/nm", "kx") + scan = _axis(1.0, 0.0, "1", "frame") + if scan_axis == 0: + data, axes = frames, [scan, det_y, det_x] + else: + data, axes = np.moveaxis(frames, 0, 1), [det_y, scan, det_x] + _fake_reader(monkeypatch, [{"data": data, "axes": axes}]) + + ds = read_4dstem("stack.fake", scan_length=scan_length, scan_axis=scan_axis) + + assert ds.shape == (n_frames // scan_length, scan_length, ny, nx) + assert np.array_equal(ds.array[1, 2], frames[1 * scan_length + 2]) + assert np.allclose(ds.sampling, [1.0, 1.0, 0.1, 0.2]) + assert np.allclose(ds.origin, [0.0, 0.0, -0.25, -0.7]) + assert list(ds.units[2:]) == ["1/nm", "1/nm"] + + +def test_read_4dstem_transpose_scan_axes(monkeypatch): + frames = np.random.default_rng(0).random((6, 3, 3)) + axes = [_axis(1.0, 0.0, "1", "f"), _axis(1.0, 0.0, "1", "y"), _axis(1.0, 0.0, "1", "x")] + _fake_reader(monkeypatch, [{"data": frames, "axes": axes}]) + ds = read_4dstem("stack.fake", scan_length=3, transpose_scan_axes=True) + assert ds.shape == (3, 2, 3, 3) + assert np.array_equal(ds.array[2, 1], frames[1 * 3 + 2]) + + +def test_read_4dstem_3d_needs_scan_length(monkeypatch): + axes = [_axis(1.0, 0.0, "1", n) for n in "fyx"] + _fake_reader(monkeypatch, [{"data": np.zeros((4, 3, 3)), "axes": axes}]) + with pytest.raises(ValueError, match="scan_length"): + read_4dstem("stack.fake") + with pytest.raises(ValueError, match="divisible"): + read_4dstem("stack.fake", scan_length=3) + + +def test_read_4dstem_rejects_bad_scan_axis(monkeypatch): + axes = [_axis(1.0, 0.0, "1", n) for n in "fyx"] + _fake_reader(monkeypatch, [{"data": np.zeros((4, 3, 3)), "axes": axes}]) + for scan_axis in (2, -1): + with pytest.raises(ValueError, match="scan_axis must be 0 or 1"): + read_4dstem("stack.fake", scan_length=2, scan_axis=scan_axis) diff --git a/tests/core/test_polar4dstem.py b/tests/core/test_polar4dstem.py new file mode 100644 index 000000000..0c1311a6f --- /dev/null +++ b/tests/core/test_polar4dstem.py @@ -0,0 +1,53 @@ +import numpy as np +import pytest +import torch + +from quantem.core.datastructures import Dataset4dstem +from quantem.core.datastructures.polar4dstem import Polar4dstem + + +def _ring_dataset(radius=9.0, center=(16.0, 15.0), shape=(2, 3, 33, 33)): + rr, cc = np.mgrid[0 : shape[2], 0 : shape[3]].astype(float) + r = np.hypot(rr - center[0], cc - center[1]) + ring = np.exp(-0.5 * ((r - radius) / 1.0) ** 2) + array = np.broadcast_to(ring, shape).astype(np.float32).copy() + return array, Dataset4dstem.from_array( + array, sampling=[1.0, 1.0, 0.05, 0.05], units=["A", "A", "1/A", "1/A"] + ) + + +def test_polar_transform_ring_peaks_at_radius(): + _, ds = _ring_dataset() + polar = ds.polar_transform(origin_row=16.0, origin_col=15.0, num_annular_bins=36) + assert isinstance(polar, Polar4dstem) + assert polar.shape[:2] == (2, 3) + assert polar.n_phi == 36 + profile = polar.array.mean(axis=(0, 1, 2)) + assert int(np.argmax(profile)) == 9 # radial_step 1 px from radial_min 0 + # the ring is isotropic: every azimuthal bin peaks at the same radius + assert np.all(np.argmax(polar.array[0, 0], axis=1) == 9) + assert polar.sampling[3] == pytest.approx(0.05) + assert polar.sampling[2] == pytest.approx(10.0) + assert polar.units[2] == "deg" + assert polar.metadata["polar_origin_row"] == 16.0 + assert polar.n_r == len(profile) + + +def test_polar_transform_two_fold_and_radial_range(): + _, ds = _ring_dataset() + polar = ds.polar_transform( + 16.0, 15.0, num_annular_bins=18, radial_min=4.0, radial_max=12.0, + radial_step=0.5, two_fold_rotation_symmetry=True, + ) # fmt: skip + assert polar.n_r == 16 + assert polar.sampling[2] == pytest.approx(10.0) # 180 deg over 18 bins + profile = polar.array.mean(axis=(0, 1, 2)) + assert 4.0 + 0.5 * int(np.argmax(profile)) == pytest.approx(9.0) + + +def test_polar_transform_tensor_backed(): + array, ds = _ring_dataset() + ds_t = Dataset4dstem.from_tensor(torch.as_tensor(array)) + a = ds.polar_transform(16.0, 15.0, num_annular_bins=12).array + b = ds_t.polar_transform(16.0, 15.0, num_annular_bins=12).array + assert np.allclose(a, b) diff --git a/tests/core/test_serialize_bundle.py b/tests/core/test_serialize_bundle.py new file mode 100644 index 000000000..8f3af7e0f --- /dev/null +++ b/tests/core/test_serialize_bundle.py @@ -0,0 +1,74 @@ +import numpy as np +import pytest +import torch +from ase import Atoms + +from quantem.core.datastructures import Dataset2d +from quantem.core.io import load +from quantem.core.io.serialize import AutoSerialize, Bundle + + +class _Holder(AutoSerialize): + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + +def _partial_atoms() -> Atoms: + atoms = Atoms( + "NbVZr", + scaled_positions=[[0, 0, 0], [0.5, 0.5, 0.5], [0, 0, 0]], + cell=[3.2, 3.2, 3.2], + pbc=[True, True, False], + ) + atoms.set_array("occupancy", np.array([0.4, 1.0, 0.6])) + atoms.set_tags([1, 2, 1]) + atoms.set_masses([92.9, 50.9, 91.2]) + atoms.info["spacegroup_number"] = 229 + atoms.info["not_json"] = object() + return atoms + + +def test_ase_atoms_round_trip_keeps_occupancy(tmp_path): + atoms = _partial_atoms() + path = tmp_path / "atoms.zip" + _Holder(atoms=atoms).save(path, mode="o") + back = load(path).atoms + + assert isinstance(back, Atoms) + assert back.get_chemical_symbols() == ["Nb", "V", "Zr"] + assert np.allclose(back.get_positions(), atoms.get_positions()) + assert np.allclose(back.get_cell(), atoms.get_cell()) + assert np.array_equal(back.get_pbc(), atoms.get_pbc()) + assert np.allclose(back.arrays["occupancy"], [0.4, 1.0, 0.6]) + assert np.array_equal(back.get_tags(), [1, 2, 1]) + assert np.allclose(back.get_masses(), [92.9, 50.9, 91.2]) + assert back.info == {"spacegroup_number": 229} + + +def test_torch_device_round_trip(tmp_path): + path = tmp_path / "device.zip" + _Holder(device=torch.device("cpu"), name="x").save(path, mode="o") + back = load(path) + assert isinstance(back.device, torch.device) + assert back.device == torch.device("cpu") + assert back.name == "x" + + +def test_bundle_round_trip(tmp_path): + image = Dataset2d.from_array(np.arange(12.0).reshape(3, 4), name="adf") + bundle = Bundle(adf=image, atoms=_partial_atoms(), note="IM689") + assert "adf: Dataset2d" in repr(bundle) + path = tmp_path / "bundle.zip" + bundle.save(path, mode="o") + back = load(path) + assert isinstance(back, Bundle) + assert np.array_equal(back.adf.array, image.array) + assert back.note == "IM689" + assert np.allclose(back.atoms.arrays["occupancy"], [0.4, 1.0, 0.6]) + + +@pytest.mark.parametrize("name", ["save", "print_tree", "_recursive_save"]) +def test_bundle_rejects_reserved_names(name): + with pytest.raises(ValueError, match="shadow"): + Bundle(**{name: 1}) diff --git a/tests/diffraction/test_bloch.py b/tests/diffraction/test_bloch.py index c30724c6b..5650f0375 100644 --- a/tests/diffraction/test_bloch.py +++ b/tests/diffraction/test_bloch.py @@ -1,5 +1,7 @@ """Tests for quantem.diffraction.bloch.""" +from functools import lru_cache + import numpy as np import pytest import torch @@ -67,8 +69,8 @@ def test_thickness_recovery(ti_beta): om.match_orientations(progress_bar=False) # thickness oscillations are sensitive to ~1 degree tilt errors, beyond # what kinematical matching provides for dynamical patterns; test the - # thickness scan itself with the true orientations (dynamical tilt - # refinement is the future joint pass) + # thickness scan itself with the true orientations (the joint tilt and + # thickness search is refine_dynamical, tested below) om.quats[0, :, 0] = q_true pm = PhaseMap.from_orientation_maps([om]) @@ -84,11 +86,14 @@ def test_thickness_recovery(ti_beta): # ---------------------------------------------------------------------- -# CBED / LACBED / Kossel / master pattern +# CBED / LACBED / Kossel / Kossel reference pattern # ---------------------------------------------------------------------- +@lru_cache(maxsize=None) def _si(absorptive: bool) -> Crystal: + """Silicon, built once per session; the tests only read it (the Bloch + code caches lattice data on it, which every test shares safely).""" si = Crystal.from_ase(bulk("Si", "diamond", a=5.431, cubic=True), name="Si", verbose=False) si.calculate_structure_factors(k_max=3.0) if absorptive: @@ -168,10 +173,64 @@ def test_cbed_library_common_grid(): assert lib["patterns"][0].max() > 0 +def test_cbed_geometry_and_normalization(): + """Without absorption the evolution is unitary, so a detector holding + every disk carries the full intensity (the pattern is the tilt + average). In a thin crystal the direct beam disk is uniform and centered + on the center pixel; diffracted disks sit at their g (rows qx, columns + qy). Checked on and off a zone axis.""" + si = _si(absorptive=False) + for q in (_zone_110(), _tilted_110()): + res = bloch.calculate_cbed( + si, + q, + [10.0, 500.0], + energy_ev=200e3, + semiconv_mrad=2.0, + n_rings=3, + sg_max=0.06, + k_max=0.8, + ) + thin, thick = res["pattern"] + H = thin.shape[0] + c = (H - 1) // 2 + assert np.allclose(res["pattern"].sum(axis=(1, 2)), 1.0, rtol=1e-9) + assert np.allclose(res["g_xy"][0], 0.0) + r_px = res["disk_radius"] / res["sampling"] + yy, xx = np.mgrid[0:H, 0:H] + w = thin * (np.hypot(yy - c, xx - c) <= r_px + 1.5) + assert w.sum() > 0.95 + assert abs((w * yy).sum() / w.sum() - c) < 0.05 + assert abs((w * xx).sum() / w.sum() - c) < 0.05 + # every disk sits at its g: the pattern is nonzero only inside the + # disks centered at (row, col) = (qx, qy) / sampling + center + centers = res["g_xy"] / res["sampling"] + c + d = np.hypot(yy[..., None] - centers[:, 0], xx[..., None] - centers[:, 1]).min(-1) + assert np.all(thick[d > r_px + 1.5] == 0) + + +def test_cbed_detector_crop_drops_outside_samples(): + """A smaller detector is a crop of the larger one: samples beyond its + edge are dropped, not piled onto the border pixels.""" + si = _si(absorptive=True) + q = _tilted_110() + kw = dict(energy_ev=200e3, semiconv_mrad=2.0, n_rings=3, sg_max=0.06, k_max=0.8) + full = bloch.calculate_cbed(si, q, 500.0, **kw) + s = full["sampling"] + small = bloch.calculate_cbed(si, q, 500.0, pixel_size=s, q_max_plot=0.3, **kw) + h_full = (full["pattern"].shape[0] - 1) // 2 + h_small = (small["pattern"].shape[0] - 1) // 2 + crop = full["pattern"][ + h_full - h_small : h_full + h_small + 1, h_full - h_small : h_full + h_small + 1 + ] + assert small["pattern"].sum() < 0.99 * full["pattern"].sum() # disks were cut + assert np.allclose(small["pattern"], crop, rtol=1e-12, atol=1e-15) + + def test_kossel_bright_field_matches_lacbed(): si = _si(absorptive=True) q = _zone_110() - kw = dict(energy_ev=200e3, semiconv_mrad=15.0, sg_max=0.06, k_max=1.2) + kw = dict(energy_ev=200e3, semiconv_mrad=15.0, sg_max=0.06, k_max=0.8) kos = bloch.calculate_kossel(si, q, 900.0, n_pixels=32, progress_bar=False, **kw) lac = bloch.calculate_lacbed(si, q, 900.0, hkl=(0, 0, 0), n_pixels=32, **kw) a, b = kos["bright_field"], lac["disk"] @@ -186,98 +245,165 @@ def test_kossel_bright_field_matches_lacbed(): assert np.all(pat[~np.isfinite(a)] == 0) -def test_reference_pattern_lookup(): - from scipy.ndimage import gaussian_filter - +def test_kossel_pattern_orientation_matches_bright_field(): + """The full Kossel pattern is stored on the bright field's axes, (row, + col) = (theta_y, theta_x): off a zone axis, where no symmetry hides a + transposition, the deficiency lines of the direct beam make the two + correlate, and the transposed pattern does not.""" si = _si(absorptive=True) - q = _zone_110() - # the master stores the pattern at its own angular resolution - # (angle_step_mrad); compare against the direct calculation blurred to - # the same resolution - master = bloch.calculate_kossel_reference( + kos = bloch.calculate_kossel( si, - [800.0], + _tilted_110(), + 800.0, energy_ev=200e3, - angle_step_mrad=2.0, + semiconv_mrad=15.0, + n_pixels=32, sg_max=0.06, - k_max=1.0, + k_max=0.7, progress_bar=False, ) - fast = bloch.kossel_from_reference(master, q, semiconv_mrad=25.0, n_pixels=48) - direct = bloch.calculate_kossel( - si, + a, p = kos["bright_field"], kos["pattern"] + m = np.isfinite(a) + cc = np.corrcoef(a[m], p[m])[0, 1] + cc_t = np.corrcoef(a[m], p.T[m])[0, 1] + assert cc > 0.4 + assert cc - cc_t > 0.3 + assert np.isclose(kos["mrad_per_pixel"], 2 * 15.0 / 31) + + +# The reference-pattern tests share one coarse reference: 3 mrad sampling and +# beams to 0.7 1/A cost ~3 s, against ~30 s for 2 mrad and 1.0 1/A, and keep +# the comparisons meaningful (the lookup and the line model are compared at +# the reference's resolution). +REF_KW = dict(energy_ev=200e3, sg_max=0.06, k_max=0.7) +REF_STEP_MRAD = 3.0 + + +def _tilted_110(): + from quantem.diffraction.rotations import qmult, quat_from_axis_angle + + tilt = quat_from_axis_angle( + torch.tensor([1.0, 0.3, 0.0], dtype=torch.float64) / np.hypot(1, 0.3), + torch.tensor(np.deg2rad(5.0), dtype=torch.float64), + ) + return qmult(tilt, _zone_110()) + + +@pytest.fixture(scope="module") +def si_reference(): + return bloch.calculate_kossel_reference( + _si(absorptive=True), + [800.0], + angle_step_mrad=REF_STEP_MRAD, + progress_bar=False, + **REF_KW, + ) + + +@pytest.fixture(scope="module") +def si_lines(): + return bloch.kossel_lines(_si(absorptive=True), 800.0, energy_ev=200e3, k_max=REF_KW["k_max"]) + + +def _direct_bright_field(q, semiconv_mrad=25.0, n_pixels=48): + return bloch.calculate_kossel( + _si(absorptive=True), q, 800.0, - energy_ev=200e3, - semiconv_mrad=25.0, - n_pixels=48, - sg_max=0.06, - k_max=1.0, + semiconv_mrad=semiconv_mrad, + n_pixels=n_pixels, progress_bar=False, - ) - a, b = fast["bright_field"], direct["bright_field"] + **REF_KW, + )["bright_field"] + + +def _blurred_cc(a, b, n_pixels=48, semiconv_mrad=25.0): + """Correlation of two bright fields inside the aperture after blurring + both to the reference's resolution. Bilinear splatting onto the Lambert + grid and bilinear lookup are two triangle kernels of one grid step, + together a blur of standard deviation step / sqrt(3).""" + from scipy.ndimage import gaussian_filter + + px_mrad = 2 * semiconv_mrad / (n_pixels - 1) + sigma = REF_STEP_MRAD / np.sqrt(3) / px_mrad m = np.isfinite(a) & np.isfinite(b) assert m.sum() > 1000 - # blur both to the master's angular resolution before comparing - px_mrad = 2 * 25.0 / 48 - sigma = 2.0 / px_mrad / 2.355 af = gaussian_filter(np.nan_to_num(a), sigma) bf = gaussian_filter(np.nan_to_num(b), sigma) - cc = np.corrcoef(af[m], bf[m])[0, 1] - assert cc > 0.9 + return np.corrcoef(af[m], bf[m])[0, 1] + + +def test_reference_pattern_lookup(si_reference): + q = _zone_110() + assert si_reference["k_max"] == REF_KW["k_max"] + fast = bloch.kossel_from_reference(si_reference, q, semiconv_mrad=25.0, n_pixels=48) + assert np.isclose(fast["mrad_per_pixel"], 2 * 25.0 / 47) + assert _blurred_cc(fast["bright_field"], _direct_bright_field(q)) > 0.9 # off-zone orientation: catches in-plane sign errors that zone-axis # symmetry hides (the reference stores the ANTI-propagation direction) - from quantem.diffraction.rotations import qmult, quat_from_axis_angle + q2 = _tilted_110() + fast2 = bloch.kossel_from_reference(si_reference, q2, semiconv_mrad=25.0, n_pixels=48) + assert _blurred_cc(fast2["bright_field"], _direct_bright_field(q2)) > 0.9 - tilt = quat_from_axis_angle( - torch.tensor([1.0, 0.3, 0.0], dtype=torch.float64) / np.hypot(1, 0.3), - torch.tensor(np.deg2rad(5.0), dtype=torch.float64), - ) - q2 = qmult(tilt, q) - fast2 = bloch.kossel_from_reference(master, q2, semiconv_mrad=25.0, n_pixels=48) - direct2 = bloch.calculate_kossel( - si, - q2, - 800.0, - energy_ev=200e3, - semiconv_mrad=25.0, - n_pixels=48, - sg_max=0.06, - k_max=1.0, - progress_bar=False, + +def test_kossel_polar_from_reference_matches_cartesian(si_reference): + """The polar lookup samples the same function: on a Cartesian grid with + pixels at the polar radii, the azimuth 0 and 90 degree rows coincide + with the center row and column.""" + q = _tilted_110() + n_r = 8 + pol = bloch.kossel_polar_from_reference( + si_reference, q, semiconv_mrad=20.0, n_radial=n_r, n_azimuthal=16 ) - a2, b2 = fast2["bright_field"], direct2["bright_field"] - m2 = np.isfinite(a2) & np.isfinite(b2) - af2 = gaussian_filter(np.nan_to_num(a2), sigma) - bf2 = gaussian_filter(np.nan_to_num(b2), sigma) - assert np.corrcoef(af2[m2], bf2[m2])[0, 1] > 0.9 + assert pol["polar"].shape == (16, n_r) + assert np.allclose(pol["radii_mrad"], 20.0 * np.arange(1, n_r + 1) / n_r) + cart = bloch.kossel_from_reference(si_reference, q, semiconv_mrad=20.0, n_pixels=2 * n_r + 1) + bf = cart["bright_field"] + # (row, col) = (theta_y, theta_x): azimuth 0 runs along +col, 90 along +row + assert np.allclose(pol["polar"][0], bf[n_r, n_r + 1 :]) + assert np.allclose(pol["polar"][4], bf[n_r + 1 :, n_r]) -def _tilted_110(): - from quantem.diffraction.rotations import qmult, quat_from_axis_angle +def test_plot_kossel_reference(si_reference, si_lines, monkeypatch): + import matplotlib.pyplot as plt - tilt = quat_from_axis_angle( - torch.tensor([1.0, 0.3, 0.0], dtype=torch.float64) / np.hypot(1, 0.3), - torch.tensor(np.deg2rad(5.0), dtype=torch.float64), - ) - return qmult(tilt, _zone_110()) + fig, ax = bloch.plot_kossel_reference(si_reference, _si(True), lines=si_lines, upsample=1) + assert len(ax.images) == 1 and len(ax.texts) > 0 + plt.close(fig) + + # a reference computed without a beam cutoff stores k_max=None; the + # default line set then falls back to 1.2 1/A instead of failing + seen = {} + + def fake_lines(crystal, thicknesses_A, energy_ev, k_max): + seen["k_max"] = k_max + return si_lines + monkeypatch.setattr(bloch, "kossel_lines", fake_lines) + fig, ax = bloch.plot_kossel_reference({**si_reference, "k_max": None}, _si(True), upsample=1) + assert seen["k_max"] == 1.2 + plt.close(fig) -def test_kossel_lines_render_matches_direct(): + +def test_kossel_lines_rejects_missing_cutoff_and_empty_set(): si = _si(absorptive=True) + with pytest.raises(ValueError, match="k_max"): + bloch.kossel_lines(si, 800.0, energy_ev=200e3, k_max=None) + with pytest.raises(ValueError, match="min_depth"): + bloch.kossel_lines(si, 800.0, energy_ev=200e3, k_max=0.4, min_depth=2.0) + + +def test_kossel_lines_render_matches_direct(si_lines): q = _tilted_110() - kw = dict(energy_ev=200e3, semiconv_mrad=25.0, sg_max=0.06, k_max=1.0) - direct = bloch.calculate_kossel(si, q, 800.0, n_pixels=48, progress_bar=False, **kw)[ - "bright_field" - ] - lines = bloch.kossel_lines(si, 800.0, energy_ev=200e3, k_max=1.0) + lines = si_lines + direct = _direct_bright_field(q, semiconv_mrad=40.0) # every line is a band edge: the +g and -g cones of a row sit at # +-theta_B, never on the zone plane first = lines["line_order"] == 1 assert torch.all(lines["line_u"][first] > 0) assert torch.all(lines["line_u"][lines["line_order"] == -1] < 0) - r = bloch.render_kossel_lines(lines, q, semiconv_mrad=25.0, n_pixels=48) + r = bloch.render_kossel_lines(lines, q, semiconv_mrad=40.0, n_pixels=48) a = r["bright_field"] m = np.isfinite(a) & np.isfinite(direct) assert m.sum() > 1000 @@ -286,20 +412,19 @@ def test_kossel_lines_render_matches_direct(): # polar rendering samples the same function: its first ring must # agree with the Cartesian pattern evaluated at those angles pol = bloch.render_kossel_lines( - lines, q, semiconv_mrad=25.0, polar=True, n_radial=10, n_azimuthal=12 + lines, q, semiconv_mrad=40.0, polar=True, n_radial=10, n_azimuthal=12 )["polar"] assert pol.shape == (12, 10) assert np.all(np.isfinite(pol)) assert pol.min() > 0 and pol.max() < 1.5 * float(lines["background"][0]) -def test_kossel_line_segments_on_cones(): +def test_kossel_line_segments_on_cones(si_lines): from quantem.diffraction.rotations import quat_to_matrix - si = _si(absorptive=True) + lines = si_lines q = _tilted_110() - lines = bloch.kossel_lines(si, 800.0, energy_ev=200e3, k_max=1.0) - alpha = 25.0 + alpha = 40.0 seg = bloch.kossel_line_segments(lines, q, semiconv_mrad=alpha) n = seg["depth"].shape[0] assert n >= 3 @@ -325,48 +450,58 @@ def test_kossel_line_segments_on_cones(): assert seg["width_mrad"][k] > 0 and 0 < seg["depth"][k] <= 1 -def test_reference_residual_hybrid(): - from scipy.ndimage import gaussian_filter +def test_overlay_kossel_segments_registration(si_lines): + """Segment end points lie on the aperture edge, which is the pixel + circle of radius (n - 1) / 2 about the center pixel (Cartesian) and the + last column (polar).""" + import matplotlib.pyplot as plt - si = _si(absorptive=True) - q = _zone_110() - master = bloch.calculate_kossel_reference( - si, - [800.0], - energy_ev=200e3, - angle_step_mrad=2.0, - sg_max=0.06, - k_max=1.0, - progress_bar=False, + q = _tilted_110() + alpha, n = 40.0, 33 + seg = bloch.kossel_line_segments(si_lines, q, semiconv_mrad=alpha) + n_draw = int((seg["depth"] >= 0.01).sum()) + assert n_draw >= 3 + fig, ax = plt.subplots() + bloch.overlay_kossel_segments(ax, seg, alpha, n_pixels=n, min_depth=0.01) + assert len(ax.lines) == n_draw + c = (n - 1) / 2 + for ln in ax.lines: + x, y = ln.get_xdata(), ln.get_ydata() + assert np.allclose(np.hypot(np.asarray(x) - c, np.asarray(y) - c), c) + plt.close(fig) + + n_r, n_az = 16, 90 + fig, ax = plt.subplots() + bloch.overlay_kossel_segments( + ax, seg, alpha, polar=True, n_radial=n_r, n_azimuthal=n_az, min_depth=0.01 ) - assert master["k_max"] == 1.0 - lines = bloch.kossel_lines(si, 800.0, energy_ev=200e3, k_max=1.0) - bloch.kossel_reference_residual(master, lines, si) - assert master["residual"].shape == master["lambert"].shape - assert np.all(np.isfinite(master["residual"])) - direct = bloch.calculate_kossel( - si, - q, - 800.0, - energy_ev=200e3, - semiconv_mrad=25.0, - n_pixels=48, - sg_max=0.06, - k_max=1.0, - progress_bar=False, - )["bright_field"] - plain = bloch.render_kossel_lines(lines, q, semiconv_mrad=25.0, n_pixels=48) - hybrid = bloch.render_kossel_lines(lines, q, semiconv_mrad=25.0, n_pixels=48, reference=master) - m = np.isfinite(direct) - sigma = 2.0 / (2 * 25.0 / 48) / 2.355 - bf = gaussian_filter(np.nan_to_num(direct), sigma) + assert len(ax.lines) == n_draw + for ln in ax.lines: + cols = np.asarray(ln.get_xdata()) + # radius semiconv sits in the last column, n_radial - 1 + assert np.isclose(cols.max(), n_r - 1) + assert np.all(cols <= n_r - 1 + 1e-9) + plt.close(fig) - def cc(x): - return np.corrcoef(gaussian_filter(np.nan_to_num(x), sigma)[m], bf[m])[0, 1] +def test_reference_residual_hybrid(si_reference, si_lines): + q = _zone_110() + si = _si(absorptive=True) + reference = dict(si_reference) # the residual is added in place + bloch.kossel_reference_residual(reference, si_lines, si) + assert reference["residual"].shape == reference["lambert"].shape + assert np.all(np.isfinite(reference["residual"])) + assert "residual" not in si_reference + direct = _direct_bright_field(q) + plain = bloch.render_kossel_lines(si_lines, q, semiconv_mrad=25.0, n_pixels=48) + hybrid = bloch.render_kossel_lines( + si_lines, q, semiconv_mrad=25.0, n_pixels=48, reference=reference + ) + cc_plain = _blurred_cc(plain["bright_field"], direct) + cc_hybrid = _blurred_cc(hybrid["bright_field"], direct) # on the zone axis the many-beam residual must improve the line model - assert cc(hybrid["bright_field"]) > cc(plain["bright_field"]) - assert cc(hybrid["bright_field"]) > 0.9 + assert cc_hybrid > cc_plain + 0.05 + assert cc_hybrid > 0.9 def test_refine_dynamical_recovery(): @@ -808,7 +943,9 @@ def test_refine_dynamical_with_precession_and_convergence(): om = OrientationMap.from_vectors( peaks, xtl, energy_ev=energy_ev, precession_deg=0.4, semiconv_mrad=1.5 ) - om.build_plan(angle_step_zone_axis_deg=3.0, verbose=False) + # the matched orientations are replaced below, so a coarse plan will do + # (the precession-integrated library is the costly part) + om.build_plan(angle_step_zone_axis_deg=10.0, angle_step_in_plane_deg=10.0, verbose=False) om.match_orientations(progress_bar=False) phis = rng.uniform(0, 2 * np.pi, N) om.quats[0, :, 0] = torch.stack( @@ -927,7 +1064,12 @@ def _smooth_map_setup(n, tilt_start_deg, corrupt=None): def test_refine_dynamical_warm_start_matches_cold(): - from quantem.diffraction.rotations import misorientation_angle_deg + from quantem.diffraction.rotations import ( + misorientation_angle_deg, + qconj, + qmult, + quat_from_axis_angle, + ) n = 5 kw = dict( @@ -942,6 +1084,7 @@ def test_refine_dynamical_warm_start_matches_cold(): out = {} for warm in (False, True): xtl, om, pm, q_true, t_true, A_true = _smooth_map_setup(n, 0.1) + q_match = om.quats[0, :, 0].clone() res = bloch.refine_dynamical(pm, warm_start=warm, **kw) out[warm] = (res, om.quats[0, :, 0].clone(), q_true, t_true, xtl) res_c, q_c, q_true, t_true, xtl = out[False] @@ -956,6 +1099,36 @@ def test_refine_dynamical_warm_start_matches_cold(): assert err.max() < 0.03 assert np.abs(res_w["thickness"][0].numpy() - t_true.numpy()).max() <= 25 + # the reported tilt is measured from the position's own matched + # orientation on either route: every start was 0.1 degrees off the truth + for res in (res_c, res_w): + tilt = res["tilt_deg"][0].numpy() # (n, 2) + assert np.all(np.abs(np.hypot(tilt[:, 0], tilt[:, 1]) - 0.1) < 0.03) + base = res["quats_base"][0, :, 0] + # the base is the matched orientation turned about the beam only + # (the in-plane rotation of the deformation fit) + dq = qmult(base, qconj(q_match)) + assert torch.all(dq[:, 1:3].abs() < 1e-9) + assert misorientation_angle_deg(base, q_match).max() < 0.05 + # and tilt x base gives the solution + for i in range(n): + wx, wy = np.deg2rad(tilt[i]) + ang = np.hypot(wx, wy) + tq = quat_from_axis_angle( + torch.tensor([wx / ang, wy / ang, 0.0], dtype=torch.float64), + torch.tensor(ang, dtype=torch.float64), + ) + q_rebuilt = qmult(tq, base[i]) + assert torch.allclose( + q_rebuilt * torch.sign(q_rebuilt @ res["quats"][0, i, 0]), + res["quats"][0, i, 0], + atol=1e-9, + ) + # the zero-tilt cost is at the matched orientation, above the final + assert torch.all(res["cost_zero_tilt"][0, :, 0] > res["cost"][0, :, 0]) + assert np.allclose(res_c["tilt_deg"].numpy(), res_w["tilt_deg"].numpy(), atol=0.01) + assert np.allclose(res_c["cost_zero_tilt"].numpy(), res_w["cost_zero_tilt"].numpy(), rtol=0.05) + def test_refine_dynamical_neighbor_rescue(): from quantem.diffraction.rotations import misorientation_angle_deg @@ -993,7 +1166,8 @@ def test_refine_dynamical_threads_match_one_worker(): from quantem.diffraction.rotations import misorientation_angle_deg # 32 positions: two blocks of 16 on two threads, one warm-start chain - # broken at the block boundary + # broken at the block boundary. Both runs start from the same matched + # orientations (update_orientations=False leaves them in place) n = 32 kw = dict( thicknesses_A=np.arange(300, 700, 25.0), @@ -1002,14 +1176,13 @@ def test_refine_dynamical_threads_match_one_worker(): sg_max=0.06, k_max=1.0, neighbor_rescue=False, + update_orientations=False, progress_bar=False, ) - out = {} - for nw in (1, 2): - xtl, om, pm, q_true, _, _ = _smooth_map_setup(n, 0.1) - res = bloch.refine_dynamical(pm, num_workers=nw, **kw) - out[nw] = (res, om.quats[0, :, 0].clone()) - (res_1, q_1), (res_2, q_2) = out[1], out[2] + xtl, om, pm, q_true, _, _ = _smooth_map_setup(n, 0.1) + res_1 = bloch.refine_dynamical(pm, num_workers=1, **kw) + res_2 = bloch.refine_dynamical(pm, num_workers=2, **kw) + q_1, q_2 = res_1["quats"][0, :, 0], res_2["quats"][0, :, 0] assert int(res_1["warm_started"].sum()) == n - 1 assert int(res_2["warm_started"].sum()) == n - 2 # the first block is the same computation on either route @@ -1019,3 +1192,65 @@ def test_refine_dynamical_threads_match_one_worker(): e1 = misorientation_angle_deg(q_true, q_1, xtl.sym_quats).numpy() e2 = misorientation_angle_deg(q_true, q_2, xtl.sym_quats).numpy() assert e2.mean() <= e1.mean() + 0.01 + + +def _fake_dynamical_result(): + """A 2 x 2 refine_dynamical result with one candidate; position (1, 1) + was not refined.""" + from types import SimpleNamespace + + from quantem.diffraction.rotations import quat_from_axis_angle + + R, C, F = 2, 2, 1 + q = quat_from_axis_angle( + torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64), torch.tensor(0.3, dtype=torch.float64) + ) + deform = torch.eye(2, dtype=torch.float64).repeat(R, C, F, 1, 1) + deform[0, 0, 0] = torch.tensor([[1.01, 0.0], [0.0, 0.99]], dtype=torch.float64) + cost = torch.tensor([[0.10, 0.20], [0.15, torch.nan]], dtype=torch.float64)[..., None] + done = torch.isfinite(cost[..., 0]) + result = { + "candidate": torch.where(done, 0, -1), + "phase_index": torch.where(done, 0, -1), + "quats": q.repeat(R, C, F, 1), + "deformation": deform, + "cost": cost, + "cost_zero_tilt": cost + 0.01, + "thickness": torch.where(done, 400.0, torch.nan).to(torch.float64), + "thickness_contrast": torch.full((R, C), 0.1, dtype=torch.float64), + "tilt_deg": torch.full((R, C, 2), 0.05, dtype=torch.float64), + } + return result, SimpleNamespace(candidates=[(0, 0)]) + + +def test_dynamical_maps_mask_unrefined(): + result, pm = _fake_dynamical_result() + maps = bloch.dynamical_maps(result, pm) + assert maps["mask"].tolist() == [[True, True], [True, False]] + assert int(maps["phase_index"][1, 1]) == -1 and int(maps["phase_index"][0, 0]) == 0 + assert torch.isnan(maps["quats"][1, 1]).all() and torch.isfinite(maps["quats"][0, 0]).all() + assert torch.isnan(maps["deformation"][1, 1]).all() + assert torch.isnan(maps["thickness"][1, 1]) and float(maps["thickness"][0, 0]) == 400.0 + assert np.isclose(float(maps["gain"][0, 0]), 0.01) + assert np.isclose(float(maps["tilt_deg"][0, 0]), 0.05 * np.sqrt(2)) + # the strain of the strained position, in the crystal frame (a pure + # rotation about the beam keeps the normal strains on the diagonal) + eps = maps["strain"] + assert float(eps["aa"][0, 0]) < 0 < float(eps["bb"][0, 0]) + assert torch.isnan(eps["cc"][1, 1]) + # restricted to a crystal that won nowhere: empty mask + assert not bloch.dynamical_maps(result, pm, crystal_index=1)["mask"].any() + + +def test_plot_dynamical_and_strain_maps(): + import matplotlib.pyplot as plt + + result, pm = _fake_dynamical_result() + maps = bloch.dynamical_maps(result, pm) + fig, axs = bloch.plot_dynamical_maps(maps) + assert np.asarray(axs).size >= 4 + plt.close(fig) + strain = {k: v.numpy() for k, v in maps["strain"].items()} + fig, axs = bloch.plot_strain_crystal_frame(strain, mask=maps["mask"].numpy()) + assert np.asarray(axs).size >= 6 + plt.close(fig) diff --git a/tests/diffraction/test_calibrate.py b/tests/diffraction/test_calibrate.py new file mode 100644 index 000000000..a2d6f79ee --- /dev/null +++ b/tests/diffraction/test_calibrate.py @@ -0,0 +1,157 @@ +"""calibrate(), DiffractionCalibration and the scan rotation measurement.""" + +from types import SimpleNamespace + +import numpy as np +import pytest +import torch +from ase.build import bulk + +from quantem.core.datastructures.vector import Vector +from quantem.core.io.serialize import load +from quantem.diffraction import calibration +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.rotations import qnormalize + +PIXEL_SIZE = 0.0123 +E_TRUE = np.array([0.012, -0.008]) + + +@pytest.fixture(scope="module") +def ti(): + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.9505, c=4.6855), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + return xtl + + +@pytest.fixture(scope="module") +def peaks_px(ti): + """Ti patterns in detector pixels with a known ellipse and pixel size.""" + torch.manual_seed(0) + rng = np.random.default_rng(0) + A_inv = np.linalg.inv(calibration._ellipse_matrix(E_TRUE)) + cells = [] + for _ in range(60): + q = qnormalize(torch.randn(4, dtype=torch.float64)) + pat = ti.generate_pattern(q, energy_ev=200e3, sigma_excitation=0.02) + qxy = np.stack([pat["qx"].numpy(), pat["qy"].numpy()], axis=1) + qxy = qxy @ A_inv.T + rng.normal(0, 0.002, qxy.shape) + cells.append(np.column_stack([qxy / PIXEL_SIZE, pat["intensity"].numpy()])) + return Vector.from_data( + [cells[:30], cells[30:]], + fields=["q_row", "q_col", "intensity"], + units=["px", "px", "counts"], + name="synthetic", + ) + + +@pytest.mark.parametrize("n_iter", [1, 2, 3]) +def test_calibrate_stable_in_n_iter(ti, peaks_px, n_iter): + # each round must refine the ellipse, not replace it with the residual + cal = calibration.calibrate(peaks_px, ti, 0.011, n_iter=n_iter) + assert abs(cal.pixel_size / PIXEL_SIZE - 1) < 2e-3 + assert np.allclose(cal.ellipse, E_TRUE, atol=1.5e-3), cal.ellipse + assert cal.metadata["reliable"] + assert cal.metadata["n_rings"] >= 3 + + +def test_calibrate_returnfig(ti, peaks_px): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + cal, fig, axs = calibration.calibrate(peaks_px, ti, 0.011, plot=True, returnfig=True) + assert isinstance(cal, calibration.DiffractionCalibration) + assert axs.shape == (2, 2) + plt.close(fig) + + +def test_compose_ellipse(): + a, b = np.array([0.01, -0.004]), np.array([0.003, 0.002]) + c = calibration._compose_ellipse(a, b) + # to first order the components add + assert np.allclose(c, a + b, atol=1e-4) + assert np.allclose(calibration._compose_ellipse(a, None), a) + assert np.allclose(calibration._compose_ellipse(np.zeros(2), b), b) + + +def test_diffraction_calibration_apply_rebin_save(tmp_path): + cells = [[np.array([[10.0, 0.0, 1.0], [0.0, -20.0, 2.0]]), np.zeros((0, 3))]] + peaks_px = Vector.from_data( + cells, fields=["q_row", "q_col", "intensity"], units=["px", "px", "counts"], name="p" + ) + ellipse = np.array([0.01, -0.02]) + cal = calibration.DiffractionCalibration( + 0.01, ellipse, rotation_ccw_deg=90.0, metadata={"binning": 1, "n_rings": 4} + ) + + out = cal.apply(peaks_px) + assert out.fields == ["qx", "qy", "intensity"] + assert out.metadata["pixel_size"] == pytest.approx(0.01) + flat = out.numpy().astype(np.float64) + q = np.array([[0.1, 0.0], [0.0, -0.2]]) @ calibration._ellipse_matrix(ellipse).T + rot = np.array([[0.0, -1.0], [1.0, 0.0]]) + assert np.allclose(flat[:, :2], q @ rot.T, atol=1e-6) + assert np.allclose(flat[:, 2], [1.0, 2.0]) + + binned = cal.rebin(2) + assert binned.pixel_size == pytest.approx(0.02) + assert binned.metadata["binning"] == 2 + assert np.allclose(binned.ellipse, ellipse) + assert binned.rotation_ccw_deg == cal.rotation_ccw_deg + assert cal.metadata["binning"] == 1 + + path = tmp_path / "cal.zip" + cal.save(path, mode="o") + cal2 = load(path) + assert isinstance(cal2, calibration.DiffractionCalibration) + assert cal2.pixel_size == pytest.approx(cal.pixel_size) + assert np.allclose(cal2.ellipse, cal.ellipse) + assert cal2.rotation_ccw_deg == pytest.approx(90.0) + assert cal2.metadata["n_rings"] == 4 + assert np.allclose(cal2.apply(peaks_px).numpy(), out.numpy()) + + +def test_refine_calibration_empty_raises(): + with pytest.raises(ValueError, match="empty"): + calibration.refine_calibration([]) + sm = SimpleNamespace( + u_array=np.full((2, 2, 2), np.nan), + v_array=np.full((2, 2, 2), np.nan), + g1_array=np.full((2, 2, 2), np.nan), + g2_array=np.full((2, 2, 2), np.nan), + ) + with pytest.raises(ValueError, match="no positions"): + calibration.refine_calibration([sm]) + + +@pytest.mark.parametrize("theta_deg", [30.0, 210.0, 125.0]) +def test_measure_scan_rotation_sign(theta_deg): + """A gradient field seen on a detector rotated by -theta returns theta mod 180. + + The returned angle uses the convention of peaks_to_calibrated: rotating + the detector field by +theta brings it back into the scan frame. + """ + R = C = 16 + H = W = 32 + ry, rx = np.mgrid[0:R, 0:C] / R + # CoM shift in the scan frame: gradient of an asymmetric potential + phi = np.sin(2 * np.pi * ry) * np.cos(np.pi * rx) + 0.5 * rx**2 + g_r, g_c = np.gradient(phi) + g = np.stack([g_r, g_c], axis=-1) + g *= 1.5 / np.abs(g).max() + th = np.deg2rad(theta_deg) + rot = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]]) + d = g @ rot # detector frame: rot.T applied to each scan-frame vector + rows = np.arange(H)[:, None] + cols = np.arange(W)[None, :] + arr = np.zeros((R, C, H, W)) + for i in range(R): + for j in range(C): + r0, c0 = H / 2 + d[i, j, 0], W / 2 + d[i, j, 1] + arr[i, j] = np.exp(-((rows - r0) ** 2 + (cols - c0) ** 2) / (2 * 1.5**2)) + angle = calibration.measure_scan_rotation(SimpleNamespace(array=arr)) + expected = theta_deg % 180 + diff = (angle - expected + 90) % 180 - 90 + assert abs(diff) < 1.0, (angle, expected) diff --git a/tests/diffraction/test_calibration_refine.py b/tests/diffraction/test_calibration_refine.py index 971470543..01f035d57 100644 --- a/tests/diffraction/test_calibration_refine.py +++ b/tests/diffraction/test_calibration_refine.py @@ -14,9 +14,7 @@ def test_refine_calibration_recovers_distortion(): torch.manual_seed(1) rng = np.random.default_rng(1) - xtl = Crystal.from_ase( - bulk("Ti", "hcp", a=2.9505, c=4.6855), name="Ti alpha", verbose=False - ) + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.9505, c=4.6855), name="Ti alpha", verbose=False) xtl.calculate_structure_factors(k_max=1.5) # global calibration error: 1.5% scale, 0.8% ellipticity, 0.3 deg rotation diff --git a/tests/diffraction/test_crystal.py b/tests/diffraction/test_crystal.py index 9072cee07..e9e8214f7 100644 --- a/tests/diffraction/test_crystal.py +++ b/tests/diffraction/test_crystal.py @@ -486,3 +486,113 @@ def test_hexagonal_pseudo_symmetry_can_be_adopted(tmp_path): loose = Crystal.from_cif(path, pseudo_symmetry_intensity_tol=1.0, verbose=False) assert loose.pointgroup_matching == "6/mmm" assert loose.sym_quats_matching.shape[0] == 12 + + +def test_direction_vector(): + cubic = Crystal.from_ase(bulk("Au", "fcc", a=4.08, cubic=True), verbose=False) + v = cubic.direction_vector([1, 1, 0]) + assert torch.allclose(v, torch.tensor([1.0, 1.0, 0.0], dtype=torch.float64) / np.sqrt(2)) + hcp = Crystal.from_ase(bulk("Ti", "hcp", a=2.95, c=4.686), verbose=False) + a1 = hcp.lat_real[0] / torch.linalg.norm(hcp.lat_real[0]) + c = hcp.lat_real[2] / torch.linalg.norm(hcp.lat_real[2]) + assert torch.allclose(hcp.direction_vector([2, -1, -1, 0]), a1) + assert torch.allclose(hcp.direction_vector([0, 0, 0, 1]), c) + # 3- and 4-index forms of the same direction agree + assert torch.allclose(hcp.direction_vector([1, 0, 0]), hcp.direction_vector([2, -1, -1, 0])) + with pytest.raises(ValueError): + hcp.direction_vector([1, 0]) + + +def test_miller_bravais_round_trip(): + from quantem.diffraction.crystal import miller_bravais_to_miller, miller_to_miller_bravais + + assert miller_to_miller_bravais([1, 0, 0]).tolist() == [2, -1, -1, 0] + assert miller_to_miller_bravais([1, 1, 0]).tolist() == [1, 1, -2, 0] + assert miller_to_miller_bravais([0, 0, 1]).tolist() == [0, 0, 0, 1] + assert miller_bravais_to_miller([2, -1, -1, 0]).tolist() == [1, 0, 0] + rng = np.random.default_rng(0) + uvw = rng.integers(-4, 5, size=(200, 3)) + uvw = uvw[np.abs(uvw).sum(axis=1) > 0] + uvtw = miller_to_miller_bravais(uvw) + assert np.all(uvtw[:, 2] == -(uvtw[:, 0] + uvtw[:, 1])) + back = miller_bravais_to_miller(uvtw) + reduced = uvw // np.gcd.reduce(np.abs(uvw), axis=1)[:, None] + assert np.array_equal(back, reduced) + + +def test_format_direction(): + from quantem.diffraction.crystal import format_direction + + bar = "̅" + assert format_direction(None) == "" + assert format_direction([1, -1, 0], mathtext=False) == "[11" + bar + "0]" + assert format_direction([1, -1, 0]) == "[1$\\bar{1}$0]" + assert format_direction([1, 0, 0], hexagonal=True, mathtext=False) == ( + "[21" + bar + "1" + bar + "0]" + ) + + +def test_spglib_no_deprecation_warnings(): + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.95, c=4.686), verbose=False) + assert xtl.pointgroup == "6/mmm" + + +def test_generate_pattern_validates_excitation_model(ti_beta): + q = quat_from_zone_axis(torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64)) + with pytest.raises(ValueError, match="excitation_model"): + ti_beta.generate_pattern(q, excitation_model="slabb") + with pytest.raises(ValueError, match="thickness_A"): + ti_beta.generate_pattern(q, excitation_model="slab") + + +def test_generate_pattern_foil_normal(): + from quantem.diffraction.illumination import excitation_coefficients + from quantem.diffraction.rotations import qrotate + + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.95, c=4.686), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + c_axis = xtl.direction_vector([0, 0, 0, 1]) + + # foil normal along the beam: identical to the default geometry + q0 = quat_from_zone_axis(c_axis, in_plane_deg=10.0) + p0 = xtl.generate_pattern(q0, energy_ev=200e3) + p1 = xtl.generate_pattern(q0, energy_ev=200e3, foil_normal=(0, 0, 0, 1)) + for key in ("qx", "qy", "intensity", "s_g"): + assert torch.allclose(p0[key], p1[key], atol=1e-12) + + # a tilted flake: each spot sits where its rod meets the Ewald sphere + tilt = xtl.direction_vector([0, 1, -1, 6]) + q = quat_from_zone_axis(tilt, in_plane_deg=10.0) + p = xtl.generate_pattern(q, energy_ev=200e3, foil_normal=(0, 0, 0, 1)) + assert p["qx"].shape[0] > 5 + n_lab = qrotate(q, c_axis[None])[0] + assert float(n_lab[2]) < 0.999 # really tilted + g_lab = qrotate(q, p["hkl"].to(torch.float64) @ xtl.lat_recip) + spot = g_lab - p["s_g"][:, None] * n_lab[None] + assert torch.allclose(spot[:, :2], torch.stack([p["qx"], p["qy"]], dim=1), atol=1e-12) + s_spot, _, _ = excitation_coefficients(spot, 200e3) + # first order in s_g: the residual is far below the excitation error + s_g = p["s_g"].numpy() + big = np.abs(s_g) > 1e-3 + assert np.all(np.abs(s_spot[big]) < 0.05 * np.abs(s_g[big])) + # and moves off the projection of g, which is where it sits by default + assert float((spot[:, :2] - g_lab[:, :2]).abs().max()) > 1e-4 + + +def test_wk_factor_without_thermal_motion(): + from quantem.diffraction.wk_scattering_factors import compute_WK_factor + + g = np.linspace(0.0, 3.0, 61) + f0 = compute_WK_factor(g, 29, 200e3, thermal_sigma=None) + assert f0.dtype == np.complex128 and f0.shape == g.shape + assert np.all(np.isfinite(f0)) + # the phonon absorption vanishes continuously as the displacement -> 0 + f_small = compute_WK_factor(g, 29, 200e3, thermal_sigma=1e-3) + assert np.allclose(f_small, f0, rtol=1e-3, atol=1e-7) + # elastic part is monotonic in g, imaginary part positive + assert np.all(np.diff(f0.real) < 0) + assert np.all(compute_WK_factor(g, 29, 200e3, thermal_sigma=0.08).imag > 0) diff --git a/tests/diffraction/test_crystal_map.py b/tests/diffraction/test_crystal_map.py new file mode 100644 index 000000000..61919272f --- /dev/null +++ b/tests/diffraction/test_crystal_map.py @@ -0,0 +1,229 @@ +"""CrystalMap and PhaseMap behaviour on small synthetic alpha/beta titanium scans.""" + +import matplotlib + +matplotlib.use("Agg") + +import matplotlib.pyplot as plt +import numpy as np +import pytest +import torch +from ase.build import bulk + +from quantem.core.datastructures.vector import Vector +from quantem.diffraction.crystal import Crystal +from quantem.diffraction.crystal_map import CrystalMap +from quantem.diffraction.orientation import OrientationMap +from quantem.diffraction.rotations import misorientation_angle_deg, quat_from_axis_angle + +Q_ALPHA = torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=torch.float64) +Q_BETA = quat_from_axis_angle( + torch.tensor([1.0, 0.0, 0.0], dtype=torch.float64), + torch.tensor(np.deg2rad(20.0), dtype=torch.float64), +) + + +def _crystals(): + ti_a = Crystal.from_ase( + bulk("Ti", "hcp", a=2.9505, c=4.6855), name="Ti alpha", verbose=False + ).calculate_structure_factors(k_max=1.5) + ti_b = Crystal.from_ase( + bulk("Ti", "bcc", a=3.26, cubic=True), name="Ti beta", verbose=False + ).calculate_structure_factors(k_max=1.5) + return ti_a, ti_b + + +def _pattern(xtl, q, rng): + p = xtl.generate_pattern(q, energy_ev=200e3, sigma_excitation=0.02) + arr = np.column_stack([p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()]) + arr[:, :2] += rng.normal(0, 0.003, (arr.shape[0], 2)) + arr[:, 2] *= rng.lognormal(0, 0.3, arr.shape[0]) + return arr + + +def _vacuum(rng): + """Direct beam and a few detections around it: enough peaks to be + matched, but nothing diffracted.""" + arr = np.zeros((5, 3)) + arr[0, 2] = 100.0 + arr[1:, :2] = rng.uniform(-0.03, 0.03, (4, 2)) + arr[1:, 2] = 1.0 + return arr + + +def _peaks(cells): + return Vector.from_data(cells, fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t") + + +@pytest.fixture(scope="module") +def two_phase(): + """Alpha on the left, beta on the right, one column of vacuum.""" + rng = np.random.default_rng(11) + ti_a, ti_b = _crystals() + R, C = 3, 7 + cells = [] + for _ in range(R): + row = [] + for c in range(C): + if c == C - 1: + row.append(_vacuum(rng)) + elif c < 3: + row.append(_pattern(ti_a, Q_ALPHA, rng)) + else: + row.append(_pattern(ti_b, Q_BETA, rng)) + cells.append(row) + cm = CrystalMap.from_vectors(_peaks(cells), [ti_a, ti_b], energy_ev=200e3) + cm.build_plan(angle_step_zone_axis_deg=3.0, verbose=False, progress_bar=False) + cm.match_orientations(progress_bar=False) + cm.fit(progress_bar=False) + return cm + + +def test_null_hypothesis_leaves_vacuum_unindexed(two_phase): + cm = two_phase + ph = cm.phase_index + # the vacuum column was matched (enough peaks) but diffracts nothing + assert bool(cm[0].computed[:, -1].all()) + assert (ph[:, -1] == -1).all() + assert (ph[:, :3] == 0).all() and (ph[:, 3:-1] == 1).all() + # unfit and unindexed positions carry NaN reliability, not zero + assert np.isnan(cm.phases.reliability.numpy()[:, -1]).all() + # per-crystal weights are normalized where indexed, zero elsewhere + w = cm.phases.crystal_weights.numpy() + assert np.allclose(w[ph >= 0].sum(-1), 1.0) + assert np.allclose(w[ph < 0], 0.0) + + +def test_crystal_map_mask_and_phase_fractions(two_phase): + cm = two_phase + ph = cm.phase_index + m_a = cm.mask("Ti alpha") + assert np.array_equal(m_a, cm.mask(0)) + assert (m_a[ph != 0] == 0).all() and (m_a[ph == 0] > 0).all() + m_all = cm.mask() + assert (m_all[ph == -1] == 0).all() and (m_all[ph >= 0] > 0).all() + with pytest.raises(KeyError): + cm.mask("Ti gamma") + with pytest.raises(KeyError): + cm.mask(5) + + frac = cm.phase_fractions() + assert set(frac) == {"unindexed", "Ti alpha", "Ti beta"} + assert np.isclose(sum(frac.values()), 1.0) + assert np.isclose(frac["unindexed"], (ph == -1).mean()) + assert np.isclose(frac["Ti beta"], (ph == 1).mean()) + + +def test_plot_phase_majority_filter_blacks_out_removed_positions(two_phase): + cm = two_phase + pm = cm.phases + saved = pm.phase_index.clone() + try: + # one isolated indexed position in an unindexed field: the filter + # removes it, and it must then be black, not the last phase color + lone = torch.full_like(saved, -1) + lone[1, 1] = 0 + pm.phase_index = lone + for radius, lit in ((0, True), (1, False)): + fig, ax = pm.plot_phase(majority_filter=radius, shade_by="none", scalebar=None) + rgb = np.asarray(ax.images[0].get_array()) + assert (rgb[1, 1].sum() > 0) == lit + plt.close(fig) + # and on the real decision the filter runs and draws vacuum black + pm.phase_index = saved + fig, ax = cm.plot_phase(majority_filter=1, scalebar=None) + rgb = np.asarray(ax.images[0].get_array()) + assert np.allclose(rgb[:, -1], 0.0) + plt.close(fig) + finally: + pm.phase_index = saved + + +def test_plot_phase_cycles_colors_past_the_palette(two_phase): + from quantem.diffraction.orientation_visualization import ( + DEFAULT_PHASE_COLORS, + phase_color_cycle, + ) + + n = len(DEFAULT_PHASE_COLORS) + 2 + colors = phase_color_cycle(n) + assert colors.shape == (n, 3) + assert np.allclose(colors[len(DEFAULT_PHASE_COLORS)], DEFAULT_PHASE_COLORS[0]) + # a short palette of names cycles as well + assert np.allclose(phase_color_cycle(3, ["red", "blue"])[2], (1.0, 0.0, 0.0)) + fig, ax = two_phase.plot_phase(phase_colors=np.array([[1.0, 0.0, 0.0]]), shade_by="none") + plt.close(fig) + + +def test_crystal_map_plots_smoke(two_phase): + cm = two_phase + figs = cm.plot_orientation() + assert isinstance(figs, list) and len(figs) == 4 + fig, ax = cm.plot_orientation(phase="Ti beta", direction="z") + assert len(cm.plot_orientation(phase=0)) == 2 + poles = cm.plot_pole_figure() + assert isinstance(poles, list) and len(poles) == 2 + fig, ax = cm.plot_pole_figure(pole=(0, 0, 0, 1), phase="Ti alpha", color_by="ipf") + fig, axs = cm.plot_correlation() + assert np.asarray(axs).shape == (2, 2) + fig, axs = cm.plot_correlation(mask=True, shared_scale=False) + # a single match per crystal: the default matches=(0, 1) skips the + # second, so there is one panel per crystal + fig, axs = cm.plot_matches([(0, 0), (0, 4)]) + assert axs.shape == (2, 2) + fig, axs = cm.plot_matches([(0, 0)], phase="Ti alpha") + assert axs.shape == (1, 1) + plt.close("all") + + +def test_refine_overrides_name_known_crystals(two_phase): + with pytest.raises(KeyError, match="unknown crystals"): + two_phase.refine_orientations(overrides={"Ti gamma": {}}, progress_bar=False) + + +def test_fit_without_any_paired_peak_is_unindexed(): + # every measured peak sits between the crystal's rings, so the best model + # has zero weight; argmax of zero weights must not name the first phase + ti_a, _ = _crystals() + rng = np.random.default_rng(3) + ring = np.zeros((7, 3)) + ring[0, 2] = 100.0 + phi = np.linspace(0, 2 * np.pi, 6, endpoint=False) + ring[1:, 0], ring[1:, 1], ring[1:, 2] = 0.2 * np.cos(phi), 0.2 * np.sin(phi), 5.0 + cells = [[_pattern(ti_a, Q_ALPHA, rng), ring]] + om = OrientationMap.from_vectors(_peaks(cells), ti_a, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=4.0, verbose=False, progress_bar=False) + om.match_orientations(progress_bar=False) + om.corr[0, 1, 0] = 0.5 # force the candidate into the fit + cm = CrystalMap.from_orientation_maps([om]) + cm.fit(progress_bar=False, k_max=1.0) + assert cm.phase_index[0, 0] == 0 + assert cm.phase_index[0, 1] == -1 + + +def test_single_crystal_map_end_to_end(tmp_path): + rng = np.random.default_rng(5) + ti_a, _ = _crystals() + cells = [[_pattern(ti_a, Q_ALPHA, rng) for _ in range(4)] + [_vacuum(rng)] for _ in range(2)] + cm = CrystalMap.from_vectors(_peaks(cells), ti_a, energy_ev=200e3) + assert len(cm) == 1 and cm.names == ["Ti alpha"] + cm.build_plan(angle_step_zone_axis_deg=3.0, verbose=False, progress_bar=False) + cm.match_orientations(progress_bar=False) + cm.refine_orientations(progress_bar=False) + cm.fit(progress_bar=False) + assert "refined" in repr(cm) and "phase fit" in repr(cm) + ph = cm.phase_index + assert (ph[:, :4] == 0).all() and (ph[:, 4] == -1).all() + err = misorientation_angle_deg(Q_ALPHA, cm[0].quats[:, :4, 0].reshape(-1, 4), ti_a.sym_quats) + assert float(err.max()) < 1.5 + # no runner-up crystal: reliability is NaN, and examples fall back to + # the correlation + assert np.isnan(cm.phases.reliability.numpy()).all() + picks = cm.example_positions(num=2, min_distance=1) + assert len(picks) == 2 and all(ph[p] == 0 for p in picks) + assert np.isclose(cm.phase_fractions()["Ti alpha"], 0.8) + fig, ax = cm.plot_phase() + fig, ax = cm.plot_orientation(phase=0, direction="z") + fig, ax = cm.plot_pole_figure(phase="Ti alpha") + plt.close("all") + cm.save(tmp_path / "cm.zip", mode="o") diff --git a/tests/diffraction/test_digital_dark_field.py b/tests/diffraction/test_digital_dark_field.py index de4a4b7b3..b569f82bf 100644 --- a/tests/diffraction/test_digital_dark_field.py +++ b/tests/diffraction/test_digital_dark_field.py @@ -86,7 +86,7 @@ def test_assign_grain_labels(): np.testing.assert_array_equal(grains, [3, 3, -1, -1, -2, 4, 4, 4, -2]) -def test_fit_lattice_and_group_images(): +def test_refine_lattice_vectors_and_group_images(): rng = np.random.default_rng(0) g1, g2 = np.array([20.0, 3.0]), np.array([4.0, 21.0]) nested = [] @@ -98,8 +98,14 @@ def test_fit_lattice_and_group_images(): row.append(np.concatenate([q, np.ones((len(n), 1))], axis=1)) nested.append(row) peaks = Vector.from_data(nested, fields=["q_row", "q_col", "intensity"]) - f1, f2 = ddf.fit_lattice( - peaks, (19.0, 2.0), (5.0, 20.0), radius=4.0, n1_range=(-2, 2), n2_range=(-2, 2) + f1, f2 = ddf.refine_lattice_vectors( + peaks, + (19.0, 2.0), + (5.0, 20.0), + center=(0.0, 0.0), + radius=4.0, + n1_range=(-2, 2), + n2_range=(-2, 2), ) np.testing.assert_allclose(f1, g1, atol=0.1) np.testing.assert_allclose(f2, g2, atol=0.1) @@ -127,3 +133,108 @@ def test_cluster_centers_and_lattice_distance(): d = ddf.lattice_distance([[10.0, 10.0], [5.0, 5.0], [11.0, 0.0]], (10.0, 0.0), (0.0, 10.0)) np.testing.assert_allclose(d, [0.0, np.hypot(5, 5), 1.0]) + + +def _pixel_lattice_peaks(origin=(32.0, 32.0)): + """3 x 3 scan of a pixel lattice g1=(20, 3), g2=(4, 21) around origin.""" + g = np.array([[20.0, 3.0], [4.0, 21.0]]) + n = np.array([[i, j] for i in (-1, 0, 1) for j in (-1, 0, 1)], float) + q = np.asarray(origin) + n @ g + pts = np.concatenate([q, np.ones((len(n), 1))], axis=1) + peaks = Vector.from_data([[pts] * 3 for _ in range(3)], fields=["q_row", "q_col", "intensity"]) + return peaks, g + + +def test_refine_lattice_vectors_uses_stored_origin_for_pixel_peaks(): + peaks, g = _pixel_lattice_peaks() + with pytest.raises(ValueError, match="origin_ref"): + ddf.refine_lattice_vectors(peaks, g[0] + 1, g[1] - 1, radius=4.0) + peaks.metadata["origin_ref"] = (32.0, 32.0) + f1, f2 = ddf.refine_lattice_vectors( + peaks, g[0] + 1, g[1] - 1, radius=4.0, n1_range=(-1, 1), n2_range=(-1, 1) + ) + np.testing.assert_allclose(f1, g[0], atol=1e-4) + np.testing.assert_allclose(f2, g[1], atol=1e-4) + + # the polar functions resolve the same origin + ring = ddf.polar_mask(peaks, np.hypot(20.0, 3.0), tol=0.5) + assert ring.sum() == 9 * 2 + np.testing.assert_array_equal( + ring, ddf.polar_mask(peaks, np.hypot(20, 3), 0.5, center=(32, 32)) + ) + + +def test_aperture_array_needs_g2_for_array_mode(): + with pytest.raises(ValueError, match="g2"): + ddf.aperture_array((10.0, 0.0)) + with pytest.raises(ValueError): + ddf.aperture_array((10.0, 0.0), mode="2-beam") + half = ddf.aperture_array((10.0, 0.0), (0.0, 10.0), mode="single", shift=(0.5, 0.5)) + np.testing.assert_allclose(half, [[5.0, 5.0]]) + + +def test_ddf_images_and_cluster_coms(): + # 2 x 3 scan; cluster 0 lives in column 0, cluster 1 in row 1 + nested = [] + for r in range(2): + row = [] + for c in range(3): + pts = [[0.0, 0.0, 1.0, -1]] + if c == 0: + pts.append([5.0, 0.0, 2.0, 0]) + if r == 1: + pts.append([0.0, 5.0, 1.0 + c, 1]) + row.append(np.asarray(pts, dtype=float)) + nested.append(row) + labeled = Vector.from_data(nested, fields=["qx", "qy", "intensity", "cluster"]) + + images = ddf.ddf_images(labeled, [0, 1]) + assert images.shape == (2, 2, 3) + np.testing.assert_allclose(images[0], [[2, 0, 0], [2, 0, 0]]) + np.testing.assert_allclose(images[1], [[0, 0, 0], [1, 2, 3]]) + + coms, sizes = ddf.cluster_coms(labeled) + np.testing.assert_array_equal(sizes, [2, 3]) + np.testing.assert_allclose(coms[0], [0.5, 0.0]) + np.testing.assert_allclose(coms[1], [1.0, (0 * 1 + 1 * 2 + 2 * 3) / 6]) + coms_u, _ = ddf.cluster_coms(labeled, weighted=False) + np.testing.assert_allclose(coms_u[1], [1.0, 1.0]) + + centers = ddf.cluster_centers(labeled) + np.testing.assert_allclose(centers, [[5.0, 0.0], [0.0, 5.0]]) + + +def test_cluster_functions_accept_empty_vector(): + empty = Vector.from_data( + [[np.empty((0, 4)), np.empty((0, 4))]], fields=["qx", "qy", "intensity", "cluster"] + ) + coms, sizes = ddf.cluster_coms(empty) + assert coms.shape == (0, 2) and sizes.shape == (0,) + assert ddf.cluster_centers(empty).shape == (0, 2) + assert ddf.ddf_images(empty, [0]).shape == (1, 1, 2) + fig, ax = ddf.plot_cluster_scatter(empty) + import matplotlib.pyplot as plt + + plt.close(fig) + + +def test_plot_cluster_scatter_center(): + import matplotlib.pyplot as plt + + peaks, _ = _pixel_lattice_peaks() + labeled = peaks.copy() + labeled.add_fields("cluster", values=np.zeros((labeled.total_rows, 1))) + labeled.metadata["origin_ref"] = (32.0, 32.0) + fig, ax = ddf.plot_cluster_scatter(labeled) + assert np.isclose(np.mean(ax.get_xlim()), 32.0) + plt.close(fig) + + calibrated = Vector.from_data( + [[np.array([[0.5, 0.1, 1.0, 0], [-0.5, -0.1, 1.0, 0]])]], + fields=["qx", "qy", "intensity", "cluster"], + ) + calibrated.metadata["origin_ref"] = (128.0, 128.0) # detector pixels: ignored + fig, ax = ddf.plot_cluster_scatter(calibrated) + assert np.isclose(np.mean(ax.get_xlim()), 0.0) + assert np.isclose(np.mean(ax.get_ylim()), 0.0) + plt.close(fig) diff --git a/tests/diffraction/test_illumination.py b/tests/diffraction/test_illumination.py index b83bc0991..cefb0d364 100644 --- a/tests/diffraction/test_illumination.py +++ b/tests/diffraction/test_illumination.py @@ -139,3 +139,28 @@ def test_refine_batched_matches_loop(): out.append(om.quats[0, :, 0].clone()) d = misorientation_angle_deg(out[0], out[1], xtl.sym_quats).numpy() assert d.max() < 1e-4 + + +def test_relrod_factor(): + from quantem.diffraction.illumination import relrod_factor + + rng = np.random.default_rng(0) + g = rng.normal(0, 0.5, (40, 3)) + g[:, 2] *= 0.05 + # normal along the beam: the excitation error is already along the rod + assert np.allclose(relrod_factor(g, np.array([0.0, 0.0, 1.0]), 200e3), 1.0) + # tilted normal: g + t n with t = -f s_g lies on the Ewald sphere to + # first order in s_g + n = np.array([np.sin(0.3), 0.0, np.cos(0.3)]) + f = relrod_factor(g, n, 200e3, precession_deg=0.0) + s, _, _ = excitation_coefficients(g, 200e3) + spot = g - (f * s)[:, None] * n[None] + s_spot, _, _ = excitation_coefficients(spot, 200e3) + big = np.abs(s) > 1e-3 + assert np.all(np.abs(s_spot[big]) < 0.05 * np.abs(s[big])) + # torch in, torch out, same values + f_t = relrod_factor(torch.as_tensor(g), torch.as_tensor(n), 200e3) + assert isinstance(f_t, torch.Tensor) and np.allclose(f_t.numpy(), f) + # an edge-on plate is never excited + edge = relrod_factor(np.array([[0.0, 0.0, 0.0]]), np.array([1.0, 0.0, 0.0]), 200e3) + assert edge[0] >= 1e6 diff --git a/tests/diffraction/test_orientation.py b/tests/diffraction/test_orientation.py index 7c24b0c5e..d6e6c3228 100644 --- a/tests/diffraction/test_orientation.py +++ b/tests/diffraction/test_orientation.py @@ -138,7 +138,7 @@ def _ase(spacegroup, symbols, basis, cellpar): 2.0, ), # ilmenite's projections are nearly mirror symmetric, so the flipped - # orientation is a close rival and needs the finer zone grid + # orientation is a close rival and needs a finer zone grid than Bi ( "ilmenite -3", lambda: _ase( @@ -147,7 +147,7 @@ def _ase(spacegroup, symbols, basis, cellpar): [(0, 0, 0.355), (0, 0, 0.146), (0.317, 0.023, 0.245)], [5.09, 5.09, 14.09, 90, 90, 120], ), - 1.0, + 1.5, ), ], ) @@ -725,3 +725,164 @@ def test_plot_matches_background_norm(): matplotlib.pyplot.close(fig) # gray_r: a lower upper quantile saturates more of the pattern to black assert (shown[1] <= shown[1].min() + 1e-6).mean() > (shown[0] <= shown[0].min() + 1e-6).mean() + + +def _quat_deg(axis, angle_deg): + from quantem.diffraction.rotations import quat_from_axis_angle + + a = torch.tensor(axis, dtype=torch.float64) + return quat_from_axis_angle(a / a.norm(), torch.tensor(np.deg2rad(angle_deg))) + + +def _two_grain_peaks(xtl, q1, q2, n=3): + """n positions, each the sum of two grains' patterns.""" + peaks = Vector.from_shape( + (1, n), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(n): + rows = [] + for q in (q1, q2): + p = xtl.generate_pattern(q, energy_ev=200e3, sigma_excitation=0.02) + rows.append(np.stack([p["qx"].numpy(), p["qy"].numpy(), p["intensity"].numpy()], 1)) + peaks[0, i] = np.concatenate(rows) + return peaks + + +def test_second_match_indexes_second_grain(): + """With deflation, the second match fits the peaks the first leaves.""" + from quantem.diffraction.rotations import quat_from_zone_axis + + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + q1 = quat_from_zone_axis(xtl.direction_vector((0, 0, 1)), 10.0) + q2 = quat_from_zone_axis(xtl.direction_vector((1, 1, 1)), 35.0) + peaks = _two_grain_peaks(xtl, q1, q2) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=2.0, angle_step_in_plane_deg=2.0, verbose=False) + om.match_orientations(num_matches=2, suppress_matched=1.0, progress_bar=False) + assert om.quats.shape[2] == 2 and om.corr_residual.shape == om.corr.shape + sym = xtl.sym_quats + for i in range(peaks.shape[1]): + e = [ + [float(misorientation_angle_deg(q, om.quats[0, i, m], sym)) for q in (q1, q2)] + for m in range(2) + ] + # the two matches are the two grains, one each + assert min(e[0][0] + e[1][1], e[0][1] + e[1][0]) < 6.0, e + + +def test_match_residual(): + """The residual of one crystal is re-matched by the other.""" + torch.manual_seed(1) + ti_a = Crystal.from_ase(bulk("Ti", "hcp", a=2.9505, c=4.6855), name="a", verbose=False) + ti_b = Crystal.from_ase(bulk("Ti", "bcc", a=3.26, cubic=True), name="b", verbose=False) + for x in (ti_a, ti_b): + x.calculate_structure_factors(k_max=1.5) + q_a = torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=torch.float64) + q_b = _quat_deg((1.0, 0.0, 0.0), 20.0) + N = 3 + peaks = Vector.from_shape( + (1, N), fields=["qx", "qy", "intensity"], units=["A^-1"] * 3, name="t" + ) + for i in range(N): + rows = [] + for x, q, s in ((ti_a, q_a, 1.0), (ti_b, q_b, 0.5)): + p = x.generate_pattern(q, energy_ev=200e3, sigma_excitation=0.02) + rows.append( + np.stack([p["qx"].numpy(), p["qy"].numpy(), s * p["intensity"].numpy()], 1) + ) + peaks[0, i] = np.concatenate(rows) + oms = {} + for x in (ti_a, ti_b): + om = OrientationMap.from_vectors(peaks, x, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=3.0, verbose=False, progress_bar=False) + om.match_orientations(progress_bar=False) + oms[x.name] = om + om_b = oms["b"] + om_b.match_residual(oms["a"], progress_bar=False) + assert om_b.quats.shape[2] == 2 + for name in ("corr", "corr_residual", "mirror"): + assert getattr(om_b, name).shape == (1, N, 2), name + # with alpha's peaks removed, beta is found by one of its two matches + err = misorientation_angle_deg(q_b, om_b.quats[0], ti_b.sym_quats).amin(dim=-1) + assert float(err.max()) < 2.0, err + assert float(om_b.corr[0, :, 1].min()) > 0 + assert "match_residual" in om_b.metadata + + # nothing left once every peak is deleted: no error, an empty second match + om_a = oms["a"] + om_a.match_residual(om_a, delete_radius=10.0, progress_bar=False) + assert om_a.quats.shape[2] == 2 + assert float(om_a.corr[..., 1].abs().max()) == 0.0 + assert om_a.corr_residual.shape == om_a.corr.shape + + +def test_plot_pattern_matches_defaults_with_one_match(): + import matplotlib + + matplotlib.use("Agg") + from types import SimpleNamespace + + from quantem.diffraction.orientation_visualization import plot_pattern_matches + + torch.manual_seed(0) + xtl = Crystal.from_ase(bulk("Ti", "bcc", a=3.31, cubic=True), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + peaks = _make_peaks(xtl, qnormalize(torch.randn(2, 4, dtype=torch.float64))) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.build_plan(angle_step_zone_axis_deg=3.0, verbose=False, progress_bar=False) + om.match_orientations(progress_bar=False) + # default matches=(0, 1) with a single match: one panel, no IndexError + fig, axs = plot_pattern_matches(om, [(0, 0), (0, 1)]) + assert axs.shape == (2, 1) + with pytest.raises(ValueError, match="none of matches"): + plot_pattern_matches(om, [(0, 0)], matches=(3,)) + img = np.ones((1, 2, 16, 16)) + with pytest.raises(ValueError, match="pixel_size"): + plot_pattern_matches(om, [(0, 0)], dataset=SimpleNamespace(array=img, shape=img.shape)) + matplotlib.pyplot.close("all") + + +def test_misorientation_map_and_cluster_plots(): + import matplotlib + + matplotlib.use("Agg") + from quantem.diffraction.orientation_visualization import _pole_family + + xtl = Crystal.from_ase(bulk("Ti", "hcp", a=2.95, c=4.686), verbose=False) + xtl.calculate_structure_factors(k_max=1.5) + q0 = torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=torch.float64) + q1 = _quat_deg((1.0, 0.0, 0.0), 25.0) + R, C = 4, 6 + q = torch.where((torch.arange(C) < 3)[None, :, None], q0, q1).expand(R, C, 4).clone() + peaks = _make_peaks(xtl, q0[None]) + om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) + om.quats = q[..., None, :] + om.corr = torch.ones((R, C, 1), dtype=torch.float64) + + mis = om.misorientation_map() + assert mis.shape == (R, C) + assert float(mis[:, :3].max()) < 1e-6 and np.allclose(mis[:, 3:].numpy(), 25.0, atol=1e-6) + assert np.allclose(om.misorientation_map(reference=q1)[:, 3:].numpy(), 0.0, atol=1e-6) + + clusters = om.cluster_orientations(min_cluster_size=2) + assert clusters["sizes"].tolist() == [12, 12] + fig, ax = om.plot_cluster_map(clusters) + assert len(ax.get_legend().get_texts()) == 2 + fig, ax = om.plot_cluster_pole_figure(clusters, pole=(0, 0, 0, 1), pole_label="[0001]") + assert len(ax.collections) == 2 + # a mask selects positions above one half + half = np.zeros((R, C)) + half[:, :3] = 0.9 + half[:, 3:] = 0.4 + assert om.cluster_orientations(mask=half, min_cluster_size=2)["sizes"].tolist() == [12] + matplotlib.pyplot.close("all") + + # poles are Miller indices: hexagonal [110] is 60 degrees from [100], + # not the 45 degrees of the Cartesian (1, 1, 0) + fam = _pole_family(xtl, (1, 1, 0)) + d = xtl.direction_vector((1, 1, 0)) + assert float((fam @ d).max()) > 1 - 1e-5 + assert torch.allclose(_pole_family(xtl, (0, 0, 1)), _pole_family(xtl, (0, 0, 0, 1))) + a = xtl.direction_vector((1, 0, 0)) + assert np.isclose(float(a @ d), 0.5) diff --git a/tests/diffraction/test_reverse_monte_carlo.py b/tests/diffraction/test_reverse_monte_carlo.py index 158e1ad50..54c928829 100644 --- a/tests/diffraction/test_reverse_monte_carlo.py +++ b/tests/diffraction/test_reverse_monte_carlo.py @@ -241,3 +241,78 @@ def test_random_displacement_scores_exactly_and_stays_bounded(rmc): def test_shell_labels(rmc): c = rmc.displacement_correlations(n_shells=4) assert c["shell"] == ["1/2<111>", "<100>", "<110>", "1/2<311>"] + + +def test_set_mask_edge_px_zero_keeps_detector(rmc): + rmc.set_mask(bragg_radius=0.06, q_max=0.6, center_radius=0.1, edge_px=0) + assert all(np.any(w > 0.5) for w in rmc.mask["w"]) + + +def test_bloch_envelope_needs_fit_thickness(rmc): + with pytest.raises(RuntimeError, match="fit_thickness"): + rmc.set_envelope("bloch") + assert rmc.envelope == "measured" + with pytest.raises(ValueError, match="unknown envelope"): + rmc.set_envelope("nope") + assert rmc.envelope == "measured" + + +def test_fit_size_effect_runs_on_model_device(rmc): + out = rmc.fit_size_effect(verbose=False) + assert set(out["eta"]) == set(rmc.species) + assert out["loss"][1] <= out["loss"][0] * (1 + 1e-6) + assert abs((rmc.concentrations * rmc.size_eta).sum()) < 1e-12 + + +def _synthetic_pattern(rmc, zone, center, theta, shape=(160, 160)): + """Gaussian spots at the kinematic intensities on a broad halo about the direct beam.""" + from quantem.diffraction.reverse_monte_carlo import _rot2 + + _, g2, inten = rmc._zone_reflections(zone, 1.6) + A = _rot2(theta) / rmc.sampling + pos = np.vstack([center, np.asarray(center) + g2 @ A.T]) + inten = np.concatenate([[2 * inten.max()], inten]) # direct beam first + rr, cc = np.mgrid[0 : shape[0], 0 : shape[1]].astype(float) + im = 50.0 * np.exp(-((rr - center[0]) ** 2 + (cc - center[1]) ** 2) / (2 * 40.0**2)) + for (pr, pc), i in zip(pos, inten / inten.max()): + im += 1e3 * i * np.exp(-((rr - pr) ** 2 + (cc - pc) ** 2) / (2 * 1.2**2)) + return im + 1.0 + + +def test_fit_geometry_on_synthetic_lattice(tmp_path): + path = tmp_path / "vnb.cif" + path.write_text(CIF) + zones = [(0, 0, 1), (0, 1, 1)] + centers = [np.array([78.3, 81.6]), np.array([80.5, 79.2])] + out = ReverseMonteCarlo.from_images( + [np.zeros((160, 160))] * 2, zone_axes=zones, sampling=0.03, bin_factor=4 + ) + out.set_crystal(Crystal.from_cif(path, verbose=False)) + out.images = [ + _synthetic_pattern(out, z, c, th).astype(np.float32) + for z, c, th in zip(zones, centers, (0.3, -0.7)) + ] + out.fit_geometry(scale_range=(0.97, 1.03), verbose=False) + geo = out.geometry + for c, c_fit in zip(centers, geo["centers"]): + assert np.allclose(c_fit, c, atol=0.3) + # the halo biases the peak centroids slightly on so small a detector + assert out.lattice_parameter == pytest.approx(3.2, rel=1e-2) + assert all(np.rad2deg(np.linalg.norm(t)) < 0.5 for t in geo["tilts"]) + assert all(rms < 0.3 for rms in geo["rms_px"]) + assert len(geo["_inten_kin"]) == len(geo["excitation_width"]) == 2 + + out.fit_thickness( + thickness=(100.0, 200.0), step=50.0, tilt_range_deg=0.0, k_max=1.0, depth_samples=4, + verbose=False, + ) # fmt: skip + assert out.geometry["thickness_k_max"] == 1.0 + assert all(g is not None for g in out.geometry["bloch_g"]) + + out.set_mask(bragg_radius=0.08, q_max=1.0, center_radius=0.15, edge_px=4) + out.build_supercell(cells=3, seed=0, device="cpu") + out.fit_background() + for envelope in ("kinematic", "bloch"): + out.set_envelope(envelope) + assert out.envelope == envelope + assert np.isfinite(out._update_residual()) diff --git a/tests/diffraction/test_rotation_convention.py b/tests/diffraction/test_rotation_convention.py index 29cd69c9c..8ff8e6ddd 100644 --- a/tests/diffraction/test_rotation_convention.py +++ b/tests/diffraction/test_rotation_convention.py @@ -46,9 +46,7 @@ def test_rotation_roundtrip(rot_scan_deg): units=["px", "px", "counts"], name="synthetic", ) - peaks = calibration.peaks_to_calibrated( - peaks_px, pixel_size, rotation_ccw_deg=rot_scan_deg - ) + peaks = calibration.peaks_to_calibrated(peaks_px, pixel_size, rotation_ccw_deg=rot_scan_deg) om = OrientationMap.from_vectors(peaks, xtl, energy_ev=200e3) om.build_plan(power_intensity=0.0) @@ -59,7 +57,5 @@ def test_rotation_roundtrip(rot_scan_deg): # (modulo crystal symmetry) -- independent of the detector rotation from quantem.diffraction.rotations import misorientation_angle_deg - err = float( - misorientation_angle_deg(q_true, om.quats[0, 0, 0], xtl.sym_quats) - ) + err = float(misorientation_angle_deg(q_true, om.quats[0, 0, 0], xtl.sym_quats)) assert err < 1.0, f"misorientation {err:.2f} deg at rot {rot_scan_deg}" diff --git a/tests/diffraction/test_rotations.py b/tests/diffraction/test_rotations.py index 1941a986a..2dbe27243 100644 --- a/tests/diffraction/test_rotations.py +++ b/tests/diffraction/test_rotations.py @@ -130,3 +130,97 @@ def test_symmetry_reduced_zone_angles(): assert float(ang[0, 1]) < 1e-6 and float(ang[0, 2]) < 1e-6 assert float(ang[3, 4]) < 1e-6 assert abs(float(ang[0, 3]) - 54.7356) < 1e-3 + + +def test_misorientation_axis_angle(): + from quantem.diffraction.rotations import misorientation_axis_angle + + axis = torch.tensor([1.0, 2.0, 2.0], dtype=torch.float64) / 3 + qa = qnormalize(torch.tensor([0.9, 0.1, -0.3, 0.2], dtype=torch.float64)) + dq = quat_from_axis_angle(axis, torch.tensor(np.deg2rad(35.0), dtype=torch.float64)) + qb = qmult(qa, dq) # R(qb) = R(qa) R(dq): dq is in the crystal frame of qa + ax, ang = misorientation_axis_angle(qa, qb) + assert abs(float(ang) - 35.0) < 1e-8 + assert torch.allclose(ax, axis, atol=1e-8) + + # with cubic symmetry, a 90 degree turn about [001] plus 10 degrees + # about the same axis reduces to 10 degrees, axis unchanged up to sign + sym = _cubic_sym_quats() + z = torch.tensor([0.0, 0.0, 1.0], dtype=torch.float64) + qb = qmult(qa, quat_from_axis_angle(z, torch.tensor(np.deg2rad(100.0), dtype=torch.float64))) + ax, ang = misorientation_axis_angle(qa, qb, sym) + assert abs(float(ang) - 10.0) < 1e-6 + assert abs(abs(float(ax @ z)) - 1.0) < 1e-6 + assert abs(float(ang) - float(misorientation_angle_deg(qa, qb, sym))) < 1e-6 + + # broadcasting over a batch + qs = qnormalize(torch.randn(7, 4, dtype=torch.float64)) + ax, ang = misorientation_axis_angle(qs, qs[:1], sym) + assert ax.shape == (7, 3) and ang.shape == (7,) + assert torch.allclose(ang, misorientation_angle_deg(qs, qs[:1], sym), atol=1e-4) + + +def _cubic_sym_quats(): + import itertools + + from quantem.diffraction.rotations import symmetry_quaternions + + # the 24 proper rotations of m-3m as signed permutation matrices + mats = [] + for perm in itertools.permutations(range(3)): + for signs in itertools.product((1, -1), repeat=3): + M = np.zeros((3, 3), dtype=int) + for i, (j, s) in enumerate(zip(perm, signs)): + M[i, j] = s + if round(np.linalg.det(M)) == 1: + mats.append(M) + return symmetry_quaternions(np.array(mats), np.eye(3)) + + +def test_symmetry_aligned(): + from quantem.diffraction.rotations import symmetry_aligned + + torch.manual_seed(3) + sym = _cubic_sym_quats() + assert sym.shape == (24, 4) + ref = qnormalize(torch.randn(4, dtype=torch.float64)) + # small perturbations of the reference, each moved to a random symmetry + # branch: alignment must bring every one back next to the reference + eps = quat_from_axis_angle( + torch.randn(10, 3, dtype=torch.float64), torch.full((10,), 0.05, dtype=torch.float64) + ) + near = qmult(ref.expand(10, 4), eps) + far = qmult(near, sym[torch.randint(1, 24, (10,))]) + out = symmetry_aligned(ref, far, sym) + assert out.shape == (10, 4) + assert torch.allclose(out, qnormalize(near), atol=1e-10) + # the same orientations as the inputs + assert torch.allclose( + misorientation_angle_deg(out, far, sym), torch.zeros(10, dtype=torch.float64), atol=1e-4 + ) + + +def test_sample_zone_axis_cap(): + from quantem.diffraction.rotations import sample_zone_axis_cap + + axis = torch.tensor([1.0, -1.0, 2.0], dtype=torch.float64) + unit = axis / torch.linalg.norm(axis) + pts = sample_zone_axis_cap(axis, 10.0, 2.0) + assert pts.ndim == 2 and pts.shape[1] == 3 + assert torch.allclose( + torch.linalg.norm(pts, dim=1), torch.ones(pts.shape[0], dtype=torch.float64), atol=1e-12 + ) + ang = torch.rad2deg(torch.acos((pts @ unit).clamp(-1, 1))) + assert float(ang.max()) <= 10.0 + 1e-9 + # equal-area count: cap area / step^2 + n_expect = 2 * np.pi * (1 - np.cos(np.deg2rad(10))) / np.deg2rad(2) ** 2 + assert abs(pts.shape[0] - np.ceil(n_expect)) <= 1 + # covers the cap: every direction in it has a sample within ~step + rng = np.random.default_rng(0) + probe = sample_zone_axis_cap(axis, 9.0, 0.5)[rng.choice(300, 50)] + d = torch.rad2deg(torch.acos((probe @ pts.T).clamp(-1, 1))).min(dim=1).values + assert float(d.max()) < 2.0 + # zero half angle: the axis alone; the poles take the short path + assert torch.allclose(sample_zone_axis_cap(axis, 0.0, 1.0), unit[None]) + down = sample_zone_axis_cap(torch.tensor([0.0, 0.0, -1.0]), 5.0, 1.0) + assert float(down[:, 2].max()) < -np.cos(np.deg2rad(5.0)) + 1e-9 diff --git a/tests/diffraction/test_strain_bragg_vectors.py b/tests/diffraction/test_strain_bragg_vectors.py new file mode 100644 index 000000000..c3d3cefdb --- /dev/null +++ b/tests/diffraction/test_strain_bragg_vectors.py @@ -0,0 +1,174 @@ +"""BraggVectors lattice fit and StrainMap on synthetic peaks (no disk detection).""" + +import numpy as np +import pytest + +from quantem.core.datastructures import Dataset4dstem +from quantem.core.datastructures.vector import Vector +from quantem.diffraction import BraggVectors +from quantem.diffraction.strain import StrainMap + +ORIGIN = np.array([32.0, 32.0]) +G1 = np.array([9.0, 1.0]) +G2 = np.array([-1.5, 11.0]) + + +def _synthetic_bragg_vectors(deform, R=8, C=6, metadata=None): + """BraggVectors whose peaks sit on origin + a M g1 + b M g2, M = deform(r, c). + + The peaks are written straight into ``bv.peaks``; the dataset is only a + zero cube that sets the scan and detector shapes. Peaks are stored as + float32, so fitted vectors match to about 1e-6 pixels. + """ + nested = [] + for r in range(R): + row = [] + for c in range(C): + M = np.asarray(deform(r, c), dtype=float) + g1, g2 = M @ G1, M @ G2 + pts = [] + for a in range(-2, 3): + for b in range(-2, 3): + q = ORIGIN + a * g1 + b * g2 + pts.append([q[0], q[1], 10.0 if a == b == 0 else 1.0]) + row.append(np.asarray(pts)) + nested.append(row) + + ds = Dataset4dstem.from_array(np.zeros((R, C, 64, 64), dtype=np.float32)) + if metadata: + ds.metadata.update(metadata) + bv = BraggVectors.from_dataset(ds) + bv.peaks = Vector.from_data(nested, fields=["q_row", "q_col", "intensity"]) + bv.compute_bvm() + bv.choose_basis_vectors(origin=ORIGIN, g1=G1, g2=G2, plot=False) + bv.index_peaks(plot=False) + bv.fit_lattice(min_num_peaks=5, progressbar=False, plot=False) + return bv + + +def test_reciprocal_scaling_ramp_gives_compressive_strain(): + # reciprocal vectors grow with row, so the real-space lattice shrinks: + # e = 1 / s - 1, negative and decreasing down the scan + s = 1.0 + 0.002 * np.arange(8) + bv = _synthetic_bragg_vectors(lambda r, c: s[r] * np.eye(2)) + np.testing.assert_allclose(bv.g1_array[:, 0], s[:, None] * G1[None, :], atol=2e-5) + assert np.all(bv.mask_weight > 0.99) + + with pytest.warns(UserWarning, match="no detector rotation"): + sm = bv.calculate_strain_map(g1_ref=G1, g2_ref=G2) + expected = (1.0 / s - 1.0)[:, None] * np.ones((1, 6)) + np.testing.assert_allclose(sm.e_rr.array, expected, atol=2e-5) + np.testing.assert_allclose(sm.e_cc.array, expected, atol=2e-5) + np.testing.assert_allclose(sm.e_rc.array, 0.0, atol=2e-5) + np.testing.assert_allclose(sm.phi.array, 0.0, atol=2e-5) + assert sm.e_rr.array[-1, 0] < 0 + assert np.all(np.diff(sm.e_rr.array[:, 0]) < 0) + + # automatic reference (weighted median over the scan): same ramp, offset + with pytest.warns(UserWarning): + sm_auto = bv.calculate_strain_map() + ramp = sm_auto.e_rr.array[:, 0] + assert np.all(np.diff(ramp) < 0) + assert ramp.min() < 0 < ramp.max() + + +def test_real_and_reciprocal_vectors_give_same_strain(): + rng = np.random.default_rng(0) + R, C = 4, 5 + F = np.eye(2)[None, None] + 0.01 * rng.normal(size=(R, C, 2, 2)) + A0 = np.array([[3.0, 0.5], [-0.4, 2.5]]) # real-space basis, columns a1, a2 + G0 = np.linalg.inv(A0).T # reciprocal basis, columns g1, g2 (G0.T @ A0 = I) + A = F @ A0 + G = np.linalg.inv(F).transpose(0, 1, 3, 2) @ G0 + + common = dict(ds_shape=(R, C), q_to_r_rotation_ccw_deg=0.0, q_transpose=False) + sm_real = StrainMap( + g1_array=A[..., :, 0], + g2_array=A[..., :, 1], + real_space=True, + g1_ref=A0[:, 0], + g2_ref=A0[:, 1], + **common, + ) + sm_recip = StrainMap( + g1_array=G[..., :, 0], + g2_array=G[..., :, 1], + real_space=False, + g1_ref=G0[:, 0], + g2_ref=G0[:, 1], + **common, + ) + expected = { + "e_rr": F[..., 0, 0] - 1, + "e_cc": F[..., 1, 1] - 1, + "e_rc": 0.5 * (F[..., 0, 1] + F[..., 1, 0]), + "phi": 0.5 * (F[..., 1, 0] - F[..., 0, 1]), + } + for name, value in expected.items(): + np.testing.assert_allclose(getattr(sm_real, name).array, value, atol=1e-12) + np.testing.assert_allclose(getattr(sm_recip, name).array, value, atol=1e-12) + + +def test_counterclockwise_lattice_rotation_gives_positive_phi(): + theta = np.deg2rad(0.5) + rot = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]) + g = rot @ np.stack([G1, G2], axis=1) + sm = StrainMap( + g1_array=np.broadcast_to(g[:, 0], (2, 2, 2)).copy(), + g2_array=np.broadcast_to(g[:, 1], (2, 2, 2)).copy(), + ds_shape=(2, 2), + real_space=False, + g1_ref=G1, + g2_ref=G2, + ) + np.testing.assert_allclose(sm.phi.array, np.sin(theta), atol=1e-12) + np.testing.assert_allclose(sm.e_rc.array, 0.0, atol=1e-12) + + +def test_q_to_r_rotation_is_applied(): + # uniaxial real-space stretch of 1 % along the detector row axis + stretch = np.diag([1.0 / 1.01, 1.0]) # reciprocal vectors shrink along rows + + bv = _synthetic_bragg_vectors(lambda r, c: stretch, R=3, C=3) + with pytest.warns(UserWarning): + sm0 = bv.calculate_strain_map(g1_ref=G1, g2_ref=G2) + np.testing.assert_allclose(sm0.e_rr.array, 0.01, atol=2e-5) + np.testing.assert_allclose(sm0.e_cc.array, 0.0, atol=2e-5) + + # a 90 degree detector-to-scan rotation moves the stretch onto the scan columns + sm90 = bv.calculate_strain_map( + g1_ref=G1, g2_ref=G2, q_to_r_rotation_ccw_deg=90.0, q_transpose=False + ) + np.testing.assert_allclose(sm90.e_rr.array, 0.0, atol=2e-5) + np.testing.assert_allclose(sm90.e_cc.array, 0.01, atol=2e-5) + assert bv.metadata["q_to_r_rotation_ccw_deg"] == 90.0 + + # the same rotation read from the dataset metadata + bv_md = _synthetic_bragg_vectors( + lambda r, c: stretch, + R=3, + C=3, + metadata={"q_to_r_rotation_ccw_deg": 90.0, "q_transpose": False}, + ) + with pytest.warns(UserWarning, match="using Dataset4dstem metadata"): + sm_md = bv_md.calculate_strain_map(g1_ref=G1, g2_ref=G2) + np.testing.assert_allclose(sm_md.e_cc.array, 0.01, atol=2e-5) + np.testing.assert_allclose(sm_md.e_rr.array, 0.0, atol=2e-5) + + # a transpose alone also swaps rows and columns + sm_t = bv.calculate_strain_map( + g1_ref=G1, g2_ref=G2, q_to_r_rotation_ccw_deg=0.0, q_transpose=True + ) + np.testing.assert_allclose(sm_t.e_cc.array, 0.01, atol=2e-5) + + +def test_estimate_strain_precision_is_quiet_by_default(capsys): + rng = np.random.default_rng(1) + g1 = G1[None, None] + 0.01 * rng.normal(size=(12, 12, 2)) + g2 = G2[None, None] + 0.01 * rng.normal(size=(12, 12, 2)) + sm = StrainMap(g1_array=g1, g2_array=g2, ds_shape=(12, 12), real_space=False) + out = sm.estimate_strain_precision(plot=False) + assert capsys.readouterr().out == "" + assert np.isfinite(out["precision"]["combined"]) + sm.estimate_strain_precision(plot=False, verbose=True) + assert "Strain precision" in capsys.readouterr().out diff --git a/uv.lock b/uv.lock index d0ce32be8..d6dc44c36 100644 --- a/uv.lock +++ b/uv.lock @@ -3687,7 +3687,7 @@ test = [ [package.metadata] requires-dist = [ - { name = "ase" }, + { name = "ase", specifier = ">=3.23" }, { name = "cmasher", specifier = ">=1.9.2" }, { name = "colorspacious" }, { name = "dill" }, @@ -3701,7 +3701,7 @@ requires-dist = [ { name = "rosettasciio", specifier = ">=0.8.0" }, { name = "scikit-image", specifier = ">=0.25.2" }, { name = "scipy" }, - { name = "spglib" }, + { name = "spglib", specifier = ">=2.5" }, { name = "tensorboard", specifier = ">=2.19.0" }, { name = "torch", specifier = ">=2.7.0" }, { name = "torchinfo", specifier = ">=1.8.0" }, diff --git a/widget/js/colormaps.ts b/widget/js/colormaps.ts deleted file mode 100644 index 18a796a9f..000000000 --- a/widget/js/colormaps.ts +++ /dev/null @@ -1,1105 +0,0 @@ -// ============================================================================ -// Color palettes (LUT control points) -// ============================================================================ - -const COLORMAP_POINTS: Record = { - inferno: [ - [0, 0, 4], [40, 11, 84], [101, 21, 110], [159, 42, 99], - [212, 72, 66], [245, 125, 21], [252, 193, 57], [252, 255, 164], - ], - viridis: [ - [68, 1, 84], [72, 36, 117], [65, 68, 135], [53, 95, 141], - [42, 120, 142], [33, 145, 140], [34, 168, 132], [68, 191, 112], - [122, 209, 81], [189, 223, 38], [253, 231, 37], - ], - plasma: [ - [13, 8, 135], [75, 3, 161], [126, 3, 168], [168, 34, 150], - [203, 70, 121], [229, 107, 93], [248, 148, 65], [253, 195, 40], [240, 249, 33], - ], - magma: [ - [0, 0, 4], [28, 16, 68], [79, 18, 123], [129, 37, 129], - [181, 54, 122], [229, 80, 100], [251, 135, 97], [254, 194, 135], [252, 253, 191], - ], - hot: [ - [0, 0, 0], [87, 0, 0], [173, 0, 0], [255, 0, 0], - [255, 87, 0], [255, 173, 0], [255, 255, 0], [255, 255, 128], [255, 255, 255], - ], - gray: [[0, 0, 0], [255, 255, 255]], - gray_r: [[255, 255, 255], [0, 0, 0]], - hsv: [ - [255, 0, 0], [255, 255, 0], [0, 255, 0], [0, 255, 255], - [0, 0, 255], [255, 0, 255], [255, 0, 0], - ], - turbo_black: [ - [0, 0, 0], [21, 18, 47], [48, 57, 135], [71, 110, 230], [69, 138, 252], [56, 165, 251], - [37, 192, 231], [24, 215, 202], [32, 234, 172], [63, 246, 138], [105, 253, 102], [146, 255, 71], - [177, 249, 54], [205, 236, 52], [229, 217, 56], [246, 195, 58], [254, 167, 50], [252, 135, 37], - [244, 102, 23], [231, 73, 12], [212, 51, 5], [188, 32, 2], [158, 16, 1], [122, 4, 3], - ], - turbo: [ - [48, 18, 59], [69, 55, 161], [66, 107, 230], [30, 162, 230], - [29, 212, 169], [79, 241, 89], [175, 240, 32], [244, 195, 12], - [248, 118, 11], [207, 46, 3], [122, 4, 2], - ], - RdBu: [ - [103, 0, 31], [178, 24, 43], [214, 96, 77], [244, 165, 130], - [253, 219, 199], [247, 247, 247], [209, 229, 240], [146, 197, 222], - [67, 147, 195], [33, 102, 172], [5, 48, 97], - ], -}; - -export const COLORMAP_NAMES = Object.keys(COLORMAP_POINTS); - -function createColormapLUT(points: number[][]): Uint8Array { - const lut = new Uint8Array(256 * 3); - for (let i = 0; i < 256; i++) { - const t = (i / 255) * (points.length - 1); - const idx = Math.floor(t); - const frac = t - idx; - const p0 = points[Math.min(idx, points.length - 1)]; - const p1 = points[Math.min(idx + 1, points.length - 1)]; - lut[i * 3] = Math.round(p0[0] + frac * (p1[0] - p0[0])); - lut[i * 3 + 1] = Math.round(p0[1] + frac * (p1[1] - p0[1])); - lut[i * 3 + 2] = Math.round(p0[2] + frac * (p1[2] - p0[2])); - } - return lut; -} - -export const COLORMAPS: Record = Object.fromEntries( - Object.entries(COLORMAP_POINTS).map(([name, points]) => [name, createColormapLUT(points)]) -); - -// ============================================================================ -// CPU colormap (Float32 -> RGBA via 256-entry LUT) -// ============================================================================ - -/** Apply colormap LUT to float data, writing into an RGBA Uint8ClampedArray. */ -export function applyColormap( - data: Float32Array, - rgba: Uint8ClampedArray, - lut: Uint8Array, - vmin: number, - vmax: number, -): void { - const range = vmax > vmin ? vmax - vmin : 1; - const uniformData = !(vmax > vmin); - for (let i = 0; i < data.length; i++) { - const clipped = Math.max(vmin, Math.min(vmax, data[i])); - const v = uniformData ? 128 : Math.min(255, Math.floor(((clipped - vmin) / range) * 255)); - const j = i * 4; - const lutIdx = v * 3; - rgba[j] = lut[lutIdx]; - rgba[j + 1] = lut[lutIdx + 1]; - rgba[j + 2] = lut[lutIdx + 2]; - rgba[j + 3] = 255; - } -} - -/** Create an offscreen canvas with colormapped data. Returns null if context unavailable. */ -export function renderToOffscreen( - data: Float32Array, - width: number, - height: number, - lut: Uint8Array, - vmin: number, - vmax: number, -): HTMLCanvasElement | null { - const offscreen = document.createElement("canvas"); - offscreen.width = width; - offscreen.height = height; - const ctx = offscreen.getContext("2d"); - if (!ctx) return null; - const imgData = ctx.createImageData(width, height); - applyColormap(data, imgData.data, lut, vmin, vmax); - ctx.putImageData(imgData, 0, 0); - return offscreen; -} - -/** Render colormapped data to a reusable offscreen canvas + ImageData (avoids per-frame allocation). */ -export function renderToOffscreenReuse( - data: Float32Array, - lut: Uint8Array, - vmin: number, - vmax: number, - offscreen: HTMLCanvasElement, - imgData: ImageData, -): void { - applyColormap(data, imgData.data, lut, vmin, vmax); - offscreen.getContext("2d")!.putImageData(imgData, 0, 0); -} - -// ============================================================================ -// WebGPU-accelerated colormap engine -// ============================================================================ - -// 2D dispatch (16×16 workgroups) to stay within WebGPU's 65535 workgroup limit. -// 1D dispatch with wg=256 needs ceil(4096*4096/256)=65536 — exceeds the limit by 1. -// ============================================================================ -// WebGPU colormap engine (compute shader, ~300x faster than CPU loop on 4K data) -// ============================================================================ - -const COLORMAP_SHADER = /* wgsl */ ` -struct Params { - width: u32, - height: u32, - vmin: f32, - vmax: f32, - log_scale: u32, - _pad: u32, -}; - -@group(0) @binding(0) var params: Params; -@group(0) @binding(1) var data: array; -@group(0) @binding(2) var lut: array; -@group(0) @binding(3) var rgba: array; - -@compute @workgroup_size(16, 16) -fn main(@builtin(global_invocation_id) gid: vec3u) { - if (gid.x >= params.width || gid.y >= params.height) { return; } - let idx = gid.y * params.width + gid.x; - var val = data[idx]; - if (params.log_scale == 1u) { - val = log(1.0 + max(val, 0.0)); - } - let range = max(params.vmax - params.vmin, 1e-30); - let clipped = clamp(val, params.vmin, params.vmax); - let t = (clipped - params.vmin) / range; - let lutIdx = min(u32(t * 255.0), 255u); - let rgb = lut[lutIdx]; - // Simplified: LUT is already packed as R|(G<<8)|(B<<16), just add alpha - rgba[idx] = rgb | 0xFF000000u; -} -`; - -// Fullscreen-quad blit shader: reads RGBA u32 buffer, renders to canvas texture -const BLIT_SHADER = /* wgsl */ ` -struct BlitParams { width: u32, height: u32 }; -@group(0) @binding(0) var params: BlitParams; -@group(0) @binding(1) var rgba: array; - -struct VSOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f }; - -@vertex fn vs(@builtin(vertex_index) vi: u32) -> VSOut { - // Fullscreen triangle (3 vertices, covers entire clip space) - var out: VSOut; - let x = f32(i32(vi & 1u)) * 4.0 - 1.0; - let y = f32(i32(vi >> 1u)) * 4.0 - 1.0; - out.pos = vec4f(x, y, 0.0, 1.0); - out.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5); - return out; -} - -@fragment fn fs(in: VSOut) -> @location(0) vec4f { - let px = u32(in.uv.x * f32(params.width)); - let py = u32(in.uv.y * f32(params.height)); - let idx = py * params.width + px; - let packed = rgba[idx]; - let r = f32(packed & 0xFFu) / 255.0; - let g = f32((packed >> 8u) & 0xFFu) / 255.0; - let b = f32((packed >> 16u) & 0xFFu) / 255.0; - return vec4f(r, g, b, 1.0); -} -`; - -/** - * GPU-accelerated colormap engine. Holds persistent data buffers on GPU; - * histogram slider changes only update a small uniform — no data re-upload. - */ -type GPUSlot = { - dataBuffer: GPUBuffer; - rgbaBuffer: GPUBuffer; - readBuffer: GPUBuffer; - paramsBuffer: GPUBuffer; - histBinsBuffer: GPUBuffer; - histReadBuffer: GPUBuffer; - count: number; - width: number; - height: number; -}; - -export class GPUColormapEngine { - private device: GPUDevice; - private pipeline: GPUComputePipeline | null = null; - private blitPipeline: GPURenderPipeline | null = null; - // Per-image GPU state: persistent buffers (data, rgba, read, params, histogram) - private slots: GPUSlot[] = []; - private lutBuffer: GPUBuffer | null = null; - private currentLutName: string = ""; - - constructor(device: GPUDevice) { this.device = device; } - - private ensurePipeline(): void { - if (this.pipeline) return; - const module = this.device.createShaderModule({ code: COLORMAP_SHADER }); - this.pipeline = this.device.createComputePipeline({ - layout: "auto", - compute: { module, entryPoint: "main" }, - }); - } - - /** Upload LUT to GPU (only when colormap name changes). */ - uploadLUT(lutName: string, lut: Uint8Array): void { - if (this.currentLutName === lutName && this.lutBuffer) return; - this.ensurePipeline(); - if (this.lutBuffer) this.lutBuffer.destroy(); - // Pack RGB triplets into u32 for GPU (R in low bits) - const packed = new Uint32Array(256); - for (let i = 0; i < 256; i++) { - packed[i] = lut[i * 3] | (lut[i * 3 + 1] << 8) | (lut[i * 3 + 2] << 16); - } - this.lutBuffer = this.device.createBuffer({ - size: packed.byteLength, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - }); - this.device.queue.writeBuffer(this.lutBuffer, 0, packed); - this.currentLutName = lutName; - } - - - /** Upload float32 image data for slot `idx`. Only call when data changes. */ - uploadData(idx: number, data: Float32Array, width?: number, height?: number): void { - this.ensurePipeline(); - while (this.slots.length <= idx) this.slots.push(null as never); - if (this.slots[idx]) { - this.slots[idx].dataBuffer.destroy(); - this.slots[idx].rgbaBuffer.destroy(); - this.slots[idx].readBuffer.destroy(); - this.slots[idx].paramsBuffer.destroy(); - this.slots[idx].histBinsBuffer.destroy(); - this.slots[idx].histReadBuffer.destroy(); - } - // Validate dimensions — if width*height doesn't match data length, derive from sqrt - // (catches stale closure values like width=1 from mount effects) - const validDims = width && height && width > 1 && height > 1 && width * height === data.length; - const w = validDims ? width : Math.round(Math.sqrt(data.length)); - const h = validDims ? height : Math.round(data.length / w); - const byteSize = data.byteLength; - const rgbaSize = data.length * 4; - const dataBuffer = this.device.createBuffer({ - size: byteSize, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - }); - this.device.queue.writeBuffer(dataBuffer, 0, data.buffer as ArrayBuffer, data.byteOffset, data.byteLength); - const rgbaBuffer = this.device.createBuffer({ - size: rgbaSize, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, - }); - // Persistent read buffer — reused on every applySlots call (no create/destroy overhead) - const readBuffer = this.device.createBuffer({ - size: rgbaSize, - usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, - }); - // Persistent params buffer — reused (just writeBuffer on each call) - const paramsBuffer = this.device.createBuffer({ - size: 24, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - // Persistent histogram buffers (256 bins × 4 bytes = 1KB each) - const histBinsBuffer = this.device.createBuffer({ - size: 256 * 4, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, - }); - const histReadBuffer = this.device.createBuffer({ - size: 256 * 4, - usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, - }); - this.slots[idx] = { dataBuffer, rgbaBuffer, readBuffer, paramsBuffer, histBinsBuffer, histReadBuffer, count: data.length, width: w, height: h }; - } - - // Params buffer: 24 bytes = { width: u32, height: u32, vmin: f32, vmax: f32, log_scale: u32, _pad: u32 } - private _writeParams(buf: ArrayBuffer, width: number, height: number, vmin: number, vmax: number, logScale: boolean): void { - const u = new Uint32Array(buf); - const f = new Float32Array(buf); - u[0] = width; - u[1] = height; - f[2] = vmin; - f[3] = vmax; - u[4] = logScale ? 1 : 0; - u[5] = 0; // pad - } - - /** - * Apply colormap to specific slot indices with per-image vmin/vmax. - * Uses persistent per-slot read buffers (no create/destroy overhead). - * Log scale is applied on GPU per pixel. - */ - async applySlots( - indices: number[], - ranges: { vmin: number; vmax: number }[], - logScale: boolean = false, - ): Promise<{ idx: number; rgba: Uint8ClampedArray }[]> { - if (!this.pipeline || !this.lutBuffer || indices.length === 0) return []; - - const activeSlots: { idx: number; slot: GPUSlot; count: number }[] = []; - const encoder = this.device.createCommandEncoder(); - const params = new ArrayBuffer(24); - - for (let k = 0; k < indices.length; k++) { - const i = indices[k]; - const slot = this.slots[i]; - if (!slot) continue; - const range = ranges[k] || { vmin: 0, vmax: 1 }; - - // Reuse persistent paramsBuffer — just write new values - this._writeParams(params, slot.width, slot.height, range.vmin, range.vmax, logScale); - this.device.queue.writeBuffer(slot.paramsBuffer, 0, params); - - const bindGroup = this.device.createBindGroup({ - layout: this.pipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: slot.paramsBuffer } }, - { binding: 1, resource: { buffer: slot.dataBuffer } }, - { binding: 2, resource: { buffer: this.lutBuffer } }, - { binding: 3, resource: { buffer: slot.rgbaBuffer } }, - ], - }); - - const pass = encoder.beginComputePass(); - pass.setPipeline(this.pipeline); - pass.setBindGroup(0, bindGroup); - pass.dispatchWorkgroups(Math.ceil(slot.width / 16), Math.ceil(slot.height / 16)); - pass.end(); - - // Copy to persistent read buffer - encoder.copyBufferToBuffer(slot.rgbaBuffer, 0, slot.readBuffer, 0, slot.count * 4); - activeSlots.push({ idx: i, slot, count: slot.count }); - } - this.device.queue.submit([encoder.finish()]); - await Promise.all(activeSlots.map(s => s.slot.readBuffer.mapAsync(GPUMapMode.READ))); - - const results: { idx: number; rgba: Uint8ClampedArray }[] = []; - for (const s of activeSlots) { - const mapped = s.slot.readBuffer.getMappedRange(); - const rgba = new Uint8ClampedArray(s.count * 4); - rgba.set(new Uint8ClampedArray(mapped)); - s.slot.readBuffer.unmap(); - results.push({ idx: s.idx, rgba }); - } - - // applySlots is for callers that need raw RGBA arrays (not rendering to canvas) - // For rendering, use renderSlots which avoids the intermediate copy - return results; - } - - /** Apply colormap to ALL slots with shared vmin/vmax. */ - async apply(vmin: number, vmax: number, logScale: boolean = false): Promise { - const indices = this.slots.map((_, i) => i).filter(i => this.slots[i]); - const ranges = indices.map(() => ({ vmin, vmax })); - const results = await this.applySlots(indices, ranges, logScale); - // Return in slot order - const out: Uint8ClampedArray[] = []; - for (const r of results) out[r.idx] = r.rgba; - return out.filter(x => x); - } - - /** Apply colormap with per-image vmin/vmax. */ - async applyPerImage(ranges: { vmin: number; vmax: number }[], logScale: boolean = false): Promise { - const indices = this.slots.map((_, i) => i).filter(i => this.slots[i]); - const perSlotRanges = indices.map(i => ranges[i] || { vmin: 0, vmax: 1 }); - const results = await this.applySlots(indices, perSlotRanges, logScale); - const out: Uint8ClampedArray[] = []; - for (const r of results) out[r.idx] = r.rgba; - return out.filter(x => x); - } - - /** Apply colormap to a SINGLE slot (fast path for slider drag). */ - async applySingle(idx: number, vmin: number, vmax: number, logScale: boolean = false): Promise { - const results = await this.applySlots([idx], [{ vmin, vmax }], logScale); - return results.length > 0 ? results[0].rgba : null; - } - - /** - * GPU colormap → offscreen canvas in one pass (zero intermediate allocation). - * Writes from GPU mapped memory directly into ImageData, then putImageData. - * Eliminates the 768MB temp Uint8ClampedArray that applySlots allocates. - */ - async renderSlots( - indices: number[], - ranges: { vmin: number; vmax: number }[], - offscreens: (HTMLCanvasElement | null)[], - imgDatas: (ImageData | null)[], - logScale: boolean = false, - ): Promise { - if (!this.pipeline || !this.lutBuffer || indices.length === 0) return 0; - - const activeSlots: { k: number; idx: number; slot: GPUSlot }[] = []; - const encoder = this.device.createCommandEncoder(); - const params = new ArrayBuffer(24); - - for (let k = 0; k < indices.length; k++) { - const i = indices[k]; - const slot = this.slots[i]; - if (!slot || !offscreens[k] || !imgDatas[k]) continue; - const range = ranges[k] || { vmin: 0, vmax: 1 }; - - this._writeParams(params, slot.width, slot.height, range.vmin, range.vmax, logScale); - this.device.queue.writeBuffer(slot.paramsBuffer, 0, params); - - const bindGroup = this.device.createBindGroup({ - layout: this.pipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: slot.paramsBuffer } }, - { binding: 1, resource: { buffer: slot.dataBuffer } }, - { binding: 2, resource: { buffer: this.lutBuffer } }, - { binding: 3, resource: { buffer: slot.rgbaBuffer } }, - ], - }); - - const pass = encoder.beginComputePass(); - pass.setPipeline(this.pipeline); - pass.setBindGroup(0, bindGroup); - pass.dispatchWorkgroups(Math.ceil(slot.width / 16), Math.ceil(slot.height / 16)); - pass.end(); - encoder.copyBufferToBuffer(slot.rgbaBuffer, 0, slot.readBuffer, 0, slot.count * 4); - activeSlots.push({ k, idx: i, slot }); - } - this.device.queue.submit([encoder.finish()]); - await Promise.all(activeSlots.map(s => s.slot.readBuffer.mapAsync(GPUMapMode.READ))); - - // Write directly from GPU mapped memory → ImageData → offscreen canvas - let rendered = 0; - for (const s of activeSlots) { - const mapped = s.slot.readBuffer.getMappedRange(); - const imgData = imgDatas[s.k]!; - imgData.data.set(new Uint8ClampedArray(mapped)); - s.slot.readBuffer.unmap(); - offscreens[s.k]!.getContext("2d")!.putImageData(imgData, 0, 0); - rendered++; - } - return rendered; - } - - private ensureBlitPipeline(format: GPUTextureFormat): void { - if (this.blitPipeline) return; - const module = this.device.createShaderModule({ code: BLIT_SHADER }); - this.blitPipeline = this.device.createRenderPipeline({ - layout: "auto", - vertex: { module, entryPoint: "vs" }, - fragment: { - module, entryPoint: "fs", - targets: [{ format }], - }, - primitive: { topology: "triangle-list" }, - }); - } - - /** - * Zero-copy GPU render: compute colormap + blit directly to WebGPU canvas textures. - * No mapAsync, no CPU copy, no putImageData. Target: <16ms for 60fps. - * - * Each canvas must have a 'webgpu' context (not '2d'). Call configureCanvas() first. - * Returns the number of images rendered. - */ - renderSlotsZeroCopy( - indices: number[], - ranges: { vmin: number; vmax: number }[], - contexts: (GPUCanvasContext | null)[], - logScale: boolean = false, - ): number { - if (!this.pipeline || !this.lutBuffer || indices.length === 0) return 0; - - // Get texture format from first valid context - const fmt = navigator.gpu.getPreferredCanvasFormat(); - this.ensureBlitPipeline(fmt); - if (!this.blitPipeline) return 0; - - const encoder = this.device.createCommandEncoder(); - const params = new ArrayBuffer(24); - let rendered = 0; - - for (let k = 0; k < indices.length; k++) { - const i = indices[k]; - const slot = this.slots[i]; - const ctx = contexts[k]; - if (!slot || !ctx) continue; - const range = ranges[k] || { vmin: 0, vmax: 1 }; - - // 1. Compute colormap (same as renderSlots) - this._writeParams(params, slot.width, slot.height, range.vmin, range.vmax, logScale); - this.device.queue.writeBuffer(slot.paramsBuffer, 0, params); - - const computeGroup = this.device.createBindGroup({ - layout: this.pipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: slot.paramsBuffer } }, - { binding: 1, resource: { buffer: slot.dataBuffer } }, - { binding: 2, resource: { buffer: this.lutBuffer } }, - { binding: 3, resource: { buffer: slot.rgbaBuffer } }, - ], - }); - const computePass = encoder.beginComputePass(); - computePass.setPipeline(this.pipeline); - computePass.setBindGroup(0, computeGroup); - computePass.dispatchWorkgroups(Math.ceil(slot.width / 16), Math.ceil(slot.height / 16)); - computePass.end(); - - // 2. Blit RGBA buffer → canvas texture (zero-copy render pass) - const blitParamsBuffer = this.device.createBuffer({ - size: 8, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - this.device.queue.writeBuffer(blitParamsBuffer, 0, new Uint32Array([slot.width, slot.height])); - - const blitGroup = this.device.createBindGroup({ - layout: this.blitPipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: blitParamsBuffer } }, - { binding: 1, resource: { buffer: slot.rgbaBuffer } }, - ], - }); - - const texture = ctx.getCurrentTexture(); - const renderPass = encoder.beginRenderPass({ - colorAttachments: [{ - view: texture.createView(), - loadOp: "clear" as GPULoadOp, - storeOp: "store" as GPUStoreOp, - clearValue: { r: 0, g: 0, b: 0, a: 1 }, - }], - }); - renderPass.setPipeline(this.blitPipeline); - renderPass.setBindGroup(0, blitGroup); - renderPass.draw(3); // fullscreen triangle - renderPass.end(); - rendered++; - - // Note: blitParamsBuffer is a temporary — ideally per-slot persistent - // For now, acceptable overhead (8 bytes per image) - } - - this.device.queue.submit([encoder.finish()]); - if (rendered > 0) { - } - return rendered; - } - - /** - * GPU colormap → OffscreenCanvas → ImageBitmap (zero mapAsync). - * Compute shader writes RGBA, render pass blits to OffscreenCanvas texture, - * transferToImageBitmap() returns ImageBitmap for drawImage on 2D canvas. - * Eliminates the 35ms JS memcpy for 12×4K images. - */ - renderSlotsToImageBitmap( - indices: number[], - ranges: { vmin: number; vmax: number }[], - logScale: boolean = false, - ): ImageBitmap[] | null { - if (!this.pipeline || !this.lutBuffer || indices.length === 0) return null; - const fmt = navigator.gpu.getPreferredCanvasFormat(); - this.ensureBlitPipeline(fmt); - if (!this.blitPipeline) return null; - - const encoder = this.device.createCommandEncoder(); - const params = new ArrayBuffer(24); - const canvases: OffscreenCanvas[] = []; - - for (let k = 0; k < indices.length; k++) { - const i = indices[k]; - const slot = this.slots[i]; - if (!slot) { canvases.push(null as never); continue; } - const range = ranges[k] || { vmin: 0, vmax: 1 }; - - // Compute colormap - this._writeParams(params, slot.width, slot.height, range.vmin, range.vmax, logScale); - this.device.queue.writeBuffer(slot.paramsBuffer, 0, params); - - const computeGroup = this.device.createBindGroup({ - layout: this.pipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: slot.paramsBuffer } }, - { binding: 1, resource: { buffer: slot.dataBuffer } }, - { binding: 2, resource: { buffer: this.lutBuffer } }, - { binding: 3, resource: { buffer: slot.rgbaBuffer } }, - ], - }); - const computePass = encoder.beginComputePass(); - computePass.setPipeline(this.pipeline); - computePass.setBindGroup(0, computeGroup); - computePass.dispatchWorkgroups(Math.ceil(slot.width / 16), Math.ceil(slot.height / 16)); - computePass.end(); - - // Blit to OffscreenCanvas - const oc = new OffscreenCanvas(slot.width, slot.height); - const ctx = oc.getContext("webgpu") as GPUCanvasContext; - ctx.configure({ device: this.device, format: fmt, alphaMode: "opaque" }); - - const blitParamsBuffer = this.device.createBuffer({ - size: 8, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - this.device.queue.writeBuffer(blitParamsBuffer, 0, new Uint32Array([slot.width, slot.height])); - - const blitGroup = this.device.createBindGroup({ - layout: this.blitPipeline!.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: blitParamsBuffer } }, - { binding: 1, resource: { buffer: slot.rgbaBuffer } }, - ], - }); - - const texture = ctx.getCurrentTexture(); - const renderPass = encoder.beginRenderPass({ - colorAttachments: [{ - view: texture.createView(), - loadOp: "clear" as GPULoadOp, - storeOp: "store" as GPUStoreOp, - clearValue: { r: 0, g: 0, b: 0, a: 1 }, - }], - }); - renderPass.setPipeline(this.blitPipeline!); - renderPass.setBindGroup(0, blitGroup); - renderPass.draw(3); - renderPass.end(); - canvases.push(oc); - } - - this.device.queue.submit([encoder.finish()]); - - // transferToImageBitmap after GPU finishes (synchronous, no mapAsync) - const bitmaps: ImageBitmap[] = []; - for (const oc of canvases) { - if (oc) bitmaps.push(oc.transferToImageBitmap()); - else bitmaps.push(null as never); - } - return bitmaps; - } - - /** - * Configure a canvas for WebGPU zero-copy rendering. - * Returns the GPUCanvasContext, or null if WebGPU canvas is not supported. - */ - configureCanvas(canvas: HTMLCanvasElement, width: number, height: number): GPUCanvasContext | null { - try { - const ctx = canvas.getContext("webgpu") as GPUCanvasContext | null; - if (!ctx) return null; - ctx.configure({ - device: this.device, - format: navigator.gpu.getPreferredCanvasFormat(), - alphaMode: "opaque", - }); - canvas.width = width; - canvas.height = height; - return ctx; - } catch { - return null; - } - } - - /** Release all GPU resources. */ - destroy(): void { - for (const slot of this.slots) { - if (slot) { - slot.dataBuffer.destroy(); - slot.rgbaBuffer.destroy(); - slot.readBuffer.destroy(); - slot.paramsBuffer.destroy(); - slot.histBinsBuffer.destroy(); - slot.histReadBuffer.destroy(); - } - } - this.slots = []; - this.lutBuffer?.destroy(); - this.lutBuffer = null; - this.currentLutName = ""; - } - - /** Number of uploaded image slots. */ - get slotCount(): number { return this.slots.filter(s => s).length; } - - // ── GPU min/max reduction ── - - private rangePipeline: GPUComputePipeline | null = null; - private RANGE_WG_SIZE = 256; - - private ensureRangePipeline(): void { - if (this.rangePipeline) return; - // Two-pass parallel reduction: each workgroup reduces a chunk to one min/max pair. - // Output: array of [min, max] pairs (one per workgroup). JS reduces the partials. - const code = /* wgsl */ ` -@group(0) @binding(0) var data: array; -@group(0) @binding(1) var out: array; -@group(0) @binding(2) var count: u32; - -var sMin: array; -var sMax: array; - -@compute @workgroup_size(256) -fn reduce(@builtin(global_invocation_id) gid: vec3u, @builtin(local_invocation_id) lid: vec3u, @builtin(workgroup_id) wid: vec3u) { - let i = gid.x; - if (i < count) { - sMin[lid.x] = data[i]; - sMax[lid.x] = data[i]; - } else { - sMin[lid.x] = 3.4028235e+38; - sMax[lid.x] = -3.4028235e+38; - } - workgroupBarrier(); - - // Tree reduction in shared memory - for (var s = 128u; s > 0u; s >>= 1u) { - if (lid.x < s) { - sMin[lid.x] = min(sMin[lid.x], sMin[lid.x + s]); - sMax[lid.x] = max(sMax[lid.x], sMax[lid.x + s]); - } - workgroupBarrier(); - } - - if (lid.x == 0u) { - out[wid.x * 2u] = sMin[0]; - out[wid.x * 2u + 1u] = sMax[0]; - } -} -`; - const module = this.device.createShaderModule({ code }); - this.rangePipeline = this.device.createComputePipeline({ - layout: "auto", - compute: { module, entryPoint: "reduce" }, - }); - } - - /** - * Batch-compute min/max for multiple slots on GPU. - * Returns { min, max } per slot. One GPU submission for all slots. - */ - async computeRangeBatch(indices: number[]): Promise<{ min: number; max: number }[]> { - this.ensureRangePipeline(); - if (!this.rangePipeline || indices.length === 0) return []; - const WG = this.RANGE_WG_SIZE; - - const encoder = this.device.createCommandEncoder(); - const jobs: { idx: number; nGroups: number; outBuf: GPUBuffer; readBuf: GPUBuffer; countBuf: GPUBuffer }[] = []; - - for (const i of indices) { - const slot = this.slots[i]; - if (!slot) continue; - const N = slot.count; - const nGroups = Math.ceil(N / WG); - const outSize = nGroups * 2 * 4; // 2 floats (min, max) per workgroup - const outBuf = this.device.createBuffer({ size: outSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC }); - const readBuf = this.device.createBuffer({ size: outSize, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST }); - const countBuf = this.device.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); - this.device.queue.writeBuffer(countBuf, 0, new Uint32Array([N])); - - const bg = this.device.createBindGroup({ - layout: this.rangePipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: slot.dataBuffer } }, - { binding: 1, resource: { buffer: outBuf } }, - { binding: 2, resource: { buffer: countBuf } }, - ], - }); - const pass = encoder.beginComputePass(); - pass.setPipeline(this.rangePipeline); - pass.setBindGroup(0, bg); - pass.dispatchWorkgroups(nGroups); - pass.end(); - encoder.copyBufferToBuffer(outBuf, 0, readBuf, 0, outSize); - jobs.push({ idx: i, nGroups, outBuf, readBuf, countBuf }); - } - - this.device.queue.submit([encoder.finish()]); - await Promise.all(jobs.map(j => j.readBuf.mapAsync(GPUMapMode.READ))); - - const results: { min: number; max: number }[] = []; - for (const j of jobs) { - const partials = new Float32Array(j.readBuf.getMappedRange().slice(0)); - j.readBuf.unmap(); - j.outBuf.destroy(); j.readBuf.destroy(); j.countBuf.destroy(); - // JS reduces partials: ~65K elements for 16M data = trivial - let dmin = Infinity, dmax = -Infinity; - for (let k = 0; k < j.nGroups; k++) { - if (partials[k * 2] < dmin) dmin = partials[k * 2]; - if (partials[k * 2 + 1] > dmax) dmax = partials[k * 2 + 1]; - } - results.push({ min: dmin, max: dmax }); - } - return results; - } - - // ── GPU histogram ── - - private histPipeline: GPUComputePipeline | null = null; - private histClearPipeline: GPUComputePipeline | null = null; - - private ensureHistPipeline(): void { - if (this.histPipeline) return; - const code = /* wgsl */ ` -struct HistParams { - width: u32, - height: u32, - dmin: f32, - dmax: f32, - log_scale: u32, - _pad: u32, -}; -@group(0) @binding(0) var params: HistParams; -@group(0) @binding(1) var data: array; -@group(0) @binding(2) var bins: array>; - -@compute @workgroup_size(16, 16) -fn histogram(@builtin(global_invocation_id) gid: vec3u) { - if (gid.x >= params.width || gid.y >= params.height) { return; } - let idx = gid.y * params.width + gid.x; - var val = data[idx]; - if (params.log_scale == 1u) { val = log(1.0 + max(val, 0.0)); } - let range = max(params.dmax - params.dmin, 1e-30); - let t = clamp((val - params.dmin) / range, 0.0, 1.0); - let bin = min(u32(t * 256.0), 255u); - atomicAdd(&bins[bin], 1u); -} - -@compute @workgroup_size(256) -fn clear_bins(@builtin(global_invocation_id) gid: vec3u) { - if (gid.x < 256u) { atomicStore(&bins[gid.x], 0u); } -} -`; - const module = this.device.createShaderModule({ code }); - this.histPipeline = this.device.createComputePipeline({ - layout: "auto", - compute: { module, entryPoint: "histogram" }, - }); - this.histClearPipeline = this.device.createComputePipeline({ - layout: "auto", - compute: { module, entryPoint: "clear_bins" }, - }); - } - - /** - * Compute a 256-bin histogram for slot `idx` on GPU. - * Returns normalized bins (0–1) matching `computeHistogramFromBytes`. - */ - async computeHistogram(idx: number, _logScale: boolean = false): Promise { - this.ensureHistPipeline(); - const slot = this.slots[idx]; - if (!slot || !this.histPipeline || !this.histClearPipeline) return new Array(256).fill(0); - - // Find data range (we need min/max for binning) - // For GPU efficiency, do a quick CPU scan — findDataRange is fast (<5ms for 16M) - // A full GPU min/max reduction would add complexity for minimal gain here. - // Note: when logScale is true, we need the log-transformed range. - - const binsBuffer = this.device.createBuffer({ - size: 256 * 4, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, - }); - const readBuffer = this.device.createBuffer({ - size: 256 * 4, - usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, - }); - const paramsBuf = this.device.createBuffer({ - size: 16, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - - // We need min/max from the (possibly log-transformed) data for proper binning. - // Pass raw min/max = 0; the shader will use the actual data range. - // Actually, we need to know the range to bin correctly. Read it back from - // the data we already uploaded. For now, accept min/max as parameters. - // The caller (Show2D data effect) already computes findDataRange. - // So let's accept dmin/dmax as params. - - // This method needs dmin/dmax — return a version that takes them: - binsBuffer.destroy(); - readBuffer.destroy(); - paramsBuf.destroy(); - return new Array(256).fill(0); - } - - /** - * Batch-compute 256-bin histograms for multiple slots in ONE GPU submission. - * Uses persistent per-slot histogram buffers (zero create/destroy overhead). - * Returns normalized bins per image. - */ - async computeHistogramBatch( - indices: number[], - ranges: { min: number; max: number }[], - logScale: boolean = false, - ): Promise { - this.ensureHistPipeline(); - if (!this.histPipeline || !this.histClearPipeline || indices.length === 0) return []; - - const encoder = this.device.createCommandEncoder(); - const activeSlots: { k: number; slot: GPUSlot }[] = []; - const params = new ArrayBuffer(24); - - for (let k = 0; k < indices.length; k++) { - const i = indices[k]; - const slot = this.slots[i]; - if (!slot) continue; - const r = ranges[k] || { min: 0, max: 1 }; - if (r.min === r.max) continue; - - // Reuse persistent paramsBuffer for histogram (same layout as colormap params) - const pu = new Uint32Array(params); - const pf = new Float32Array(params); - pu[0] = slot.width; pu[1] = slot.height; - pf[2] = r.min; pf[3] = r.max; - pu[4] = logScale ? 1 : 0; pu[5] = 0; - this.device.queue.writeBuffer(slot.paramsBuffer, 0, params); - - // Clear bins (persistent buffer) - const clearGroup = this.device.createBindGroup({ - layout: this.histClearPipeline!.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: slot.paramsBuffer } }, - { binding: 1, resource: { buffer: slot.dataBuffer } }, - { binding: 2, resource: { buffer: slot.histBinsBuffer } }, - ], - }); - const clearPass = encoder.beginComputePass(); - clearPass.setPipeline(this.histClearPipeline!); - clearPass.setBindGroup(0, clearGroup); - clearPass.dispatchWorkgroups(1); - clearPass.end(); - - // Histogram (persistent buffer) - const histGroup = this.device.createBindGroup({ - layout: this.histPipeline!.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: slot.paramsBuffer } }, - { binding: 1, resource: { buffer: slot.dataBuffer } }, - { binding: 2, resource: { buffer: slot.histBinsBuffer } }, - ], - }); - const histPass = encoder.beginComputePass(); - histPass.setPipeline(this.histPipeline!); - histPass.setBindGroup(0, histGroup); - histPass.dispatchWorkgroups(Math.ceil(slot.width / 16), Math.ceil(slot.height / 16)); - histPass.end(); - - encoder.copyBufferToBuffer(slot.histBinsBuffer, 0, slot.histReadBuffer, 0, 256 * 4); - activeSlots.push({ k, slot }); - } - - this.device.queue.submit([encoder.finish()]); - await Promise.all(activeSlots.map(s => s.slot.histReadBuffer.mapAsync(GPUMapMode.READ))); - - const results: number[][] = []; - for (const s of activeSlots) { - const rawBins = new Uint32Array(s.slot.histReadBuffer.getMappedRange().slice(0)); - s.slot.histReadBuffer.unmap(); - - let maxCount = 0; - for (let j = 0; j < 256; j++) if (rawBins[j] > maxCount) maxCount = rawBins[j]; - const norm = new Array(256); - for (let j = 0; j < 256; j++) norm[j] = maxCount > 0 ? rawBins[j] / maxCount : 0; - results.push(norm); - } - return results; - } - - /** - * Compute a 256-bin histogram for slot `idx` on GPU, given known data range. - * Returns normalized bins (0–1) matching `computeHistogramFromBytes`. - */ - async computeHistogramWithRange( - idx: number, dmin: number, dmax: number, logScale: boolean = false, - ): Promise { - this.ensureHistPipeline(); - const slot = this.slots[idx]; - if (!slot || !this.histPipeline || !this.histClearPipeline) return new Array(256).fill(0); - if (dmin === dmax) return new Array(256).fill(0); - - const binsBuffer = this.device.createBuffer({ - size: 256 * 4, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, - }); - const readBuffer = this.device.createBuffer({ - size: 256 * 4, - usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, - }); - const paramsBuf = this.device.createBuffer({ - size: 24, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - - const params = new ArrayBuffer(24); - const pu = new Uint32Array(params); - const pf = new Float32Array(params); - pu[0] = slot.width; pu[1] = slot.height; - pf[2] = dmin; pf[3] = dmax; - pu[4] = logScale ? 1 : 0; pu[5] = 0; - this.device.queue.writeBuffer(paramsBuf, 0, params); - - const encoder = this.device.createCommandEncoder(); - - // Clear bins - const clearGroup = this.device.createBindGroup({ - layout: this.histClearPipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: paramsBuf } }, - { binding: 1, resource: { buffer: slot.dataBuffer } }, - { binding: 2, resource: { buffer: binsBuffer } }, - ], - }); - const clearPass = encoder.beginComputePass(); - clearPass.setPipeline(this.histClearPipeline); - clearPass.setBindGroup(0, clearGroup); - clearPass.dispatchWorkgroups(1); - clearPass.end(); - - // Histogram - const histGroup = this.device.createBindGroup({ - layout: this.histPipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: paramsBuf } }, - { binding: 1, resource: { buffer: slot.dataBuffer } }, - { binding: 2, resource: { buffer: binsBuffer } }, - ], - }); - const histPass = encoder.beginComputePass(); - histPass.setPipeline(this.histPipeline); - histPass.setBindGroup(0, histGroup); - histPass.dispatchWorkgroups(Math.ceil(slot.width / 16), Math.ceil(slot.height / 16)); - histPass.end(); - - encoder.copyBufferToBuffer(binsBuffer, 0, readBuffer, 0, 256 * 4); - this.device.queue.submit([encoder.finish()]); - - await readBuffer.mapAsync(GPUMapMode.READ); - const rawBins = new Uint32Array(readBuffer.getMappedRange().slice(0)); - readBuffer.unmap(); - binsBuffer.destroy(); - readBuffer.destroy(); - paramsBuf.destroy(); - - // Normalize (match CPU: divide by max count) - let maxCount = 0; - for (let i = 0; i < 256; i++) if (rawBins[i] > maxCount) maxCount = rawBins[i]; - const result = new Array(256); - if (maxCount > 0) { - for (let i = 0; i < 256; i++) result[i] = rawBins[i] / maxCount; - } else { - for (let i = 0; i < 256; i++) result[i] = 0; - } - return result; - } -} - -let gpuColormapEngine: GPUColormapEngine | null = null; - -/** Get or create the singleton GPU colormap engine. Returns null if WebGPU unavailable. */ -export async function getGPUColormapEngine(): Promise { - if (gpuColormapEngine) return gpuColormapEngine; - // Reuse the GPU device from fft - try { - const { getGPUDevice } = await import("./fft"); - const device = await getGPUDevice(); - if (!device) return null; - gpuColormapEngine = new GPUColormapEngine(device); - return gpuColormapEngine; - } catch { - return null; - } -} - -/** Query the GPU's max buffer size in bytes. Returns 0 if WebGPU unavailable. */ -export async function getGPUMaxBufferSize(): Promise { - try { - if (!navigator.gpu) return 0; - const adapter = await navigator.gpu.requestAdapter(); - if (!adapter) return 0; - return adapter.limits.maxStorageBufferBindingSize || adapter.limits.maxBufferSize || 0; - } catch { - return 0; - } -} diff --git a/widget/js/diffsim-web/cif.ts b/widget/js/diffsim-web/cif.ts deleted file mode 100644 index 8b2da47d8..000000000 --- a/widget/js/diffsim-web/cif.ts +++ /dev/null @@ -1,341 +0,0 @@ -/** - * CIF reader for the browser simulator: cell, symmetry operations and atom - * sites from a CIF text, expanded to the full cell, with kinematical - * structure factors (Lobato & Van Dyck electron scattering factors) and - * Bloch couplings on the reciprocal lattice out to k_max. The absorptive - * part of the potential is not computed here: it is approximated as a fixed - * fraction of the elastic coupling (ABSORPTION_FRACTION), which damps the - * thickness fringes at a plausible rate; the preset structures carry the - * Weickenmeier-Kohl factors from quantem instead. - */ - -import type { CrystalData } from "../diffsim/physics"; -import { relativisticGamma, electronWavelength } from "../diffsim/physics"; -import { LOBATO } from "./lobato"; - -const ABSORPTION_FRACTION = 0.08; - -const SYMBOLS = ["", "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr"]; - -// jmol colors (fraction of 255) and covalent radii (A) for the drawn atoms -const JMOL: Record = { - H: [1, 1, 1], He: [0.85, 1, 1], Li: [0.8, 0.5, 1], Be: [0.76, 1, 0], B: [1, 0.71, 0.71], C: [0.56, 0.56, 0.56], N: [0.19, 0.31, 0.97], O: [1, 0.05, 0.05], - F: [0.56, 0.88, 0.31], Ne: [0.7, 0.89, 0.96], Na: [0.67, 0.36, 0.95], Mg: [0.54, 1, 0], Al: [0.75, 0.65, 0.65], Si: [0.94, 0.78, 0.63], P: [1, 0.5, 0], S: [1, 1, 0.19], - Cl: [0.12, 0.94, 0.12], Ar: [0.5, 0.82, 0.89], K: [0.56, 0.25, 0.83], Ca: [0.24, 1, 0], Sc: [0.9, 0.9, 0.9], Ti: [0.75, 0.76, 0.78], V: [0.65, 0.65, 0.67], Cr: [0.54, 0.6, 0.78], - Mn: [0.61, 0.48, 0.78], Fe: [0.88, 0.4, 0.2], Co: [0.94, 0.56, 0.63], Ni: [0.31, 0.82, 0.31], Cu: [0.78, 0.5, 0.2], Zn: [0.49, 0.5, 0.69], Ga: [0.76, 0.56, 0.56], Ge: [0.4, 0.56, 0.56], - As: [0.74, 0.5, 0.89], Se: [1, 0.63, 0], Br: [0.65, 0.16, 0.16], Kr: [0.36, 0.72, 0.82], Rb: [0.44, 0.18, 0.69], Sr: [0, 1, 0], Y: [0.58, 1, 1], Zr: [0.58, 0.88, 0.88], - Nb: [0.45, 0.76, 0.79], Mo: [0.33, 0.71, 0.71], Tc: [0.23, 0.62, 0.62], Ru: [0.14, 0.56, 0.56], Rh: [0.04, 0.49, 0.55], Pd: [0, 0.41, 0.52], Ag: [0.75, 0.75, 0.75], Cd: [1, 0.85, 0.56], - In: [0.65, 0.46, 0.45], Sn: [0.4, 0.5, 0.5], Sb: [0.62, 0.39, 0.71], Te: [0.83, 0.48, 0], I: [0.58, 0, 0.58], Xe: [0.26, 0.62, 0.69], Cs: [0.34, 0.09, 0.56], Ba: [0, 0.79, 0], - La: [0.44, 0.83, 1], Ce: [1, 1, 0.78], Pr: [0.85, 1, 0.78], Nd: [0.78, 1, 0.78], Sm: [0.56, 1, 0.78], Eu: [0.38, 1, 0.78], Gd: [0.27, 1, 0.78], Tb: [0.19, 1, 0.78], Dy: [0.12, 1, 0.78], - Ho: [0, 1, 0.61], Er: [0, 0.9, 0.46], Tm: [0, 0.83, 0.32], Yb: [0, 0.75, 0.22], Lu: [0, 0.67, 0.14], Hf: [0.3, 0.76, 1], Ta: [0.3, 0.65, 1], W: [0.13, 0.58, 0.84], Re: [0.15, 0.49, 0.67], - Os: [0.15, 0.4, 0.59], Ir: [0.09, 0.33, 0.53], Pt: [0.82, 0.82, 0.88], Au: [1, 0.82, 0.14], Hg: [0.72, 0.72, 0.82], Tl: [0.65, 0.33, 0.3], Pb: [0.34, 0.35, 0.38], Bi: [0.62, 0.31, 0.71], - Th: [0, 0.73, 1], U: [0, 0.56, 1], -}; -const RADII: Record = { - H: 0.31, He: 0.28, Li: 1.28, Be: 0.96, B: 0.84, C: 0.76, N: 0.71, O: 0.66, F: 0.57, Ne: 0.58, Na: 1.66, Mg: 1.41, Al: 1.21, Si: 1.11, P: 1.07, S: 1.05, Cl: 1.02, Ar: 1.06, - K: 2.03, Ca: 1.76, Sc: 1.7, Ti: 1.6, V: 1.53, Cr: 1.39, Mn: 1.39, Fe: 1.32, Co: 1.26, Ni: 1.24, Cu: 1.32, Zn: 1.22, Ga: 1.22, Ge: 1.2, As: 1.19, Se: 1.2, Br: 1.2, Kr: 1.16, - Rb: 2.2, Sr: 1.95, Y: 1.9, Zr: 1.75, Nb: 1.64, Mo: 1.54, Tc: 1.47, Ru: 1.46, Rh: 1.42, Pd: 1.39, Ag: 1.45, Cd: 1.44, In: 1.42, Sn: 1.39, Sb: 1.39, Te: 1.38, I: 1.39, Xe: 1.4, - Cs: 2.44, Ba: 2.15, La: 2.07, Ce: 2.04, Pr: 2.03, Nd: 2.01, Sm: 1.98, Eu: 1.98, Gd: 1.96, Tb: 1.94, Dy: 1.92, Ho: 1.92, Er: 1.89, Tm: 1.9, Yb: 1.87, Lu: 1.87, Hf: 1.75, - Ta: 1.7, W: 1.62, Re: 1.51, Os: 1.44, Ir: 1.41, Pt: 1.36, Au: 1.36, Hg: 1.32, Tl: 1.45, Pb: 1.46, Bi: 1.48, Th: 2.06, U: 1.96, -}; - -// --------------------------------------------------------------------------- -// CIF text -> tokens, data items, loops -// --------------------------------------------------------------------------- -function tokenize(text: string): string[] { - const out: string[] = []; - const lines = text.replace(/\r\n?/g, "\n").split("\n"); - let i = 0; - while (i < lines.length) { - let line = lines[i]; - if (line.startsWith(";")) { - // multi-line text field - const parts = [line.slice(1)]; - i++; - while (i < lines.length && !lines[i].startsWith(";")) { parts.push(lines[i]); i++; } - out.push(parts.join("\n")); - i++; - continue; - } - const hash = line.indexOf("#"); - if (hash >= 0 && !/['"]/.test(line.slice(0, hash))) line = line.slice(0, hash); - const re = /'([^']*)'|"([^"]*)"|(\S+)/g; - let m: RegExpExecArray | null; - while ((m = re.exec(line))) out.push(m[1] ?? m[2] ?? m[3]); - i++; - } - return out; -} - -interface CifBlock { items: Record; loops: Record[] } - -function parseBlocks(text: string): CifBlock { - const tok = tokenize(text); - const block: CifBlock = { items: {}, loops: [] }; - let i = 0; - // use the first data block that has atom sites - while (i < tok.length) { - const t = tok[i]; - if (t.toLowerCase().startsWith("data_") || t.toLowerCase() === "stop_" || t.toLowerCase().startsWith("save_")) { i++; continue; } - if (t.toLowerCase() === "loop_") { - i++; - const names: string[] = []; - while (i < tok.length && tok[i].startsWith("_")) { names.push(tok[i].toLowerCase()); i++; } - const loop: Record = {}; - for (const n of names) loop[n] = []; - let k = 0; - while (i < tok.length && !tok[i].startsWith("_") && !/^(loop_|data_|stop_|save_)/i.test(tok[i])) { - loop[names[k % names.length]].push(tok[i]); - k++; i++; - } - block.loops.push(loop); - continue; - } - if (t.startsWith("_")) { - block.items[t.toLowerCase()] = tok[i + 1] ?? ""; - i += 2; - continue; - } - i++; - } - return block; -} - -const num = (s: string | undefined, fallback = NaN): number => { - if (s === undefined) return fallback; - const v = parseFloat(s.replace(/\(.*\)/, "")); - return isFinite(v) ? v : fallback; -}; - -// --------------------------------------------------------------------------- -// symmetry operations "x, y+1/2, -z" -> rotation matrix + translation -// --------------------------------------------------------------------------- -interface SymOp { R: number[][]; t: number[] } - -function parseSymop(s: string): SymOp | null { - const parts = s.toLowerCase().replace(/\s+/g, "").split(","); - if (parts.length !== 3) return null; - const R: number[][] = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; - const t = [0, 0, 0]; - for (let r = 0; r < 3; r++) { - // split into signed terms - const terms = parts[r].replace(/-/g, "+-").split("+").filter(Boolean); - for (const term of terms) { - const m = term.match(/^([+-]?\d*\.?\d*(?:\/\d+)?)\*?([xyz])?$/); - if (!m) return null; - let coef = 1; - const cs = m[1]; - if (cs && cs !== "+" && cs !== "-") { - coef = cs.includes("/") ? parseFloat(cs.split("/")[0]) / parseFloat(cs.split("/")[1]) : parseFloat(cs); - } else if (cs === "-") coef = -1; - if (m[2]) R[r]["xyz".indexOf(m[2])] += coef; - else t[r] += coef; - } - } - return { R, t }; -} - -// --------------------------------------------------------------------------- -export interface ParsedCif { - name: string; - cellpar: [number, number, number, number, number, number]; - cell: number[][]; // rows a, b, c (A), a along x, b in the xy plane - symbols: string[]; - positions: number[][]; // fractional, full cell - occupancy: number[]; - spacegroup: string; - centering: number[][]; // pure translations (centering vectors) -} - -export function parseCif(text: string, name = "CIF"): ParsedCif { - const blk = parseBlocks(text); - const it = blk.items; - const cellpar = [it["_cell_length_a"], it["_cell_length_b"], it["_cell_length_c"], it["_cell_angle_alpha"], it["_cell_angle_beta"], it["_cell_angle_gamma"]].map((v) => num(v)); - if (cellpar.some((v) => !isFinite(v))) throw new Error("CIF: missing cell parameters"); - const [a, b, c, al, be, ga] = cellpar; - const d2r = Math.PI / 180; - const cosA = Math.cos(al * d2r), cosB = Math.cos(be * d2r), cosG = Math.cos(ga * d2r), sinG = Math.sin(ga * d2r); - const cx = c * cosB, cy = (c * (cosA - cosB * cosG)) / sinG; - const cz = Math.sqrt(Math.max(c * c - cx * cx - cy * cy, 0)); - const cell = [[a, 0, 0], [b * cosG, b * sinG, 0], [cx, cy, cz]]; - - // atom sites - const atomLoop = blk.loops.find((l) => "_atom_site_fract_x" in l); - if (!atomLoop) throw new Error("CIF: no _atom_site_fract_x loop"); - const n = atomLoop["_atom_site_fract_x"].length; - const typeCol = atomLoop["_atom_site_type_symbol"] || atomLoop["_atom_site_label"]; - const occCol = atomLoop["_atom_site_occupancy"]; - const base: { sym: string; p: number[]; occ: number }[] = []; - for (let i = 0; i < n; i++) { - const raw = (typeCol?.[i] || "").replace(/[^A-Za-z]/g, ""); - let sym = raw.slice(0, 2); - if (!(sym in LOBATO)) sym = raw.slice(0, 1); - if (!(sym in LOBATO)) throw new Error(`CIF: unknown element "${raw}"`); - const p = [num(atomLoop["_atom_site_fract_x"][i]), num(atomLoop["_atom_site_fract_y"][i]), num(atomLoop["_atom_site_fract_z"][i])]; - if (p.some((v) => !isFinite(v))) continue; - base.push({ sym, p, occ: occCol ? num(occCol[i], 1) : 1 }); - } - - // symmetry operations - const symLoop = blk.loops.find((l) => "_symmetry_equiv_pos_as_xyz" in l || "_space_group_symop_operation_xyz" in l); - const opStrings = symLoop ? symLoop["_symmetry_equiv_pos_as_xyz"] || symLoop["_space_group_symop_operation_xyz"] : ["x,y,z"]; - const ops = opStrings.map(parseSymop).filter((o): o is SymOp => !!o); - if (!ops.length) ops.push({ R: [[1, 0, 0], [0, 1, 0], [0, 0, 1]], t: [0, 0, 0] }); - const spacegroup = it["_symmetry_space_group_name_h-m"] || it["_space_group_name_h-m_alt"] || (it["_symmetry_int_tables_number"] ? `#${it["_symmetry_int_tables_number"]}` : symLoop ? "" : "P1 (no symmetry operations in file)"); - - // expand to the full cell - const symbols: string[] = []; - const positions: number[][] = []; - const occupancy: number[] = []; - const wrap = (v: number) => ((v % 1) + 1) % 1; - for (const at of base) { - for (const op of ops) { - const q = [0, 1, 2].map((r) => wrap(op.R[r][0] * at.p[0] + op.R[r][1] * at.p[1] + op.R[r][2] * at.p[2] + op.t[r])); - let dup = false; - for (let j = 0; j < positions.length; j++) { - if (symbols[j] !== at.sym) continue; - const d = positions[j].map((v, k) => Math.abs(wrap(v - q[k] + 0.5) - 0.5)); - if (d.every((v) => v < 1e-3)) { dup = true; break; } - } - if (!dup) { symbols.push(at.sym); positions.push(q); occupancy.push(at.occ); } - } - } - // centering translations: from the symmetry list, and also any of the - // standard centerings that map the expanded atom list onto itself (a P1 - // listing of a conventional cell carries them only implicitly); the - // reflections they extinguish never carry intensity and are dropped - const centering = ops.filter((o) => o.R.every((row, r) => row.every((v, cc) => v === (r === cc ? 1 : 0))) && o.t.some((v) => Math.abs(wrap(v)) > 1e-6)).map((o) => o.t.map(wrap)); - const mapsOntoItself = (t: number[]) => positions.every((pos, j) => { - const q = pos.map((v, k) => wrap(v + t[k])); - return positions.some((p2, j2) => symbols[j2] === symbols[j] && p2.every((v, k) => Math.abs(wrap(v - q[k] + 0.5) - 0.5) < 1e-3)); - }); - for (const t of [[0, 0.5, 0.5], [0.5, 0, 0.5], [0.5, 0.5, 0], [0.5, 0.5, 0.5], [2 / 3, 1 / 3, 1 / 3], [1 / 3, 2 / 3, 2 / 3]]) { - if (!centering.some((c) => c.every((v, k) => Math.abs(v - t[k]) < 1e-6)) && mapsOntoItself(t)) centering.push(t); - } - return { name, cellpar: cellpar as ParsedCif["cellpar"], cell, symbols, positions, occupancy, spacegroup, centering }; -} - -// --------------------------------------------------------------------------- -// structure factors -> CrystalData -// --------------------------------------------------------------------------- -function scatteringFactor(sym: string, g: number): number { - const [a, b] = LOBATO[sym]; - const g2 = g * g; - let f = 0; - for (let i = 0; i < 5; i++) { - const d = 1 + b[i] * g2; - f += (a[i] * (2 + b[i] * g2)) / (d * d); - } - return f; -} - -function inv3(m: number[][]): number[][] { - const [[a, b, c], [d, e, f], [g, h, i]] = m; - const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g; - const det = a * A + b * B + c * C; - return [ - [A / det, -(b * i - c * h) / det, (b * f - c * e) / det], - [B / det, (a * i - c * g) / det, -(a * f - c * d) / det], - [C / det, -(a * h - b * g) / det, (a * e - b * d) / det], - ]; -} - -const MAX_REFLECTIONS = 30000; // keeps the live solve responsive for large cells - -/** k_max (1/A) at which a cell of this volume (primitive lattice points per - * centering) reaches the reflection budget, capped at the requested value. */ -export function kMaxForCell(p: ParsedCif, kMaxRequested: number): number { - const cell = p.cell; - const volume = Math.abs(cell[0][0] * (cell[1][1] * cell[2][2] - cell[1][2] * cell[2][1]) - cell[0][1] * (cell[1][0] * cell[2][2] - cell[1][2] * cell[2][0]) + cell[0][2] * (cell[1][0] * cell[2][1] - cell[1][1] * cell[2][0])); - const vPrim = volume / (p.centering.length + 1); - // N(k) = 4/3 pi k^3 V_prim - const kBudget = Math.cbrt((3 * MAX_REFLECTIONS) / (4 * Math.PI * vPrim)); - return Math.min(kMaxRequested, Math.floor(kBudget * 20) / 20); -} - -/** Build the simulator's crystal data from a parsed CIF at an energy and k_max (1/A). */ -export function crystalFromCif(p: ParsedCif, energyEv: number, kMaxRequested: number): CrystalData { - const kMax = kMaxForCell(p, kMaxRequested); - const cell = p.cell; - const inv = inv3(cell); // columns of inv = reciprocal vectors; recip rows b_i = inv^T rows - const recip = [[inv[0][0], inv[1][0], inv[2][0]], [inv[0][1], inv[1][1], inv[2][1]], [inv[0][2], inv[1][2], inv[2][2]]]; - const volume = Math.abs(cell[0][0] * (cell[1][1] * cell[2][2] - cell[1][2] * cell[2][1]) - cell[0][1] * (cell[1][0] * cell[2][2] - cell[1][2] * cell[2][0]) + cell[0][2] * (cell[1][0] * cell[2][1] - cell[1][1] * cell[2][0])); - const gamma = relativisticGamma(energyEv); - const species = [...new Set(p.symbols)]; - const nmax = recip.map((b) => Math.ceil(kMax / Math.hypot(b[0], b[1], b[2])) + 1); - const hkl: number[][] = []; - const g = new Float32Array(0); - const gList: number[] = []; - const F2: number[] = []; - const Ure: number[] = []; - const Uim: number[] = []; - const fCache = new Map(); - // extinguished by a centering translation when g . t is not an integer - const isCentered = (h: number, k: number, l: number) => p.centering.some((t) => { const x = h * t[0] + k * t[1] + l * t[2]; return Math.abs(x - Math.round(x)) > 1e-6; }); - for (let h = -nmax[0]; h <= nmax[0]; h++) for (let k = -nmax[1]; k <= nmax[1]; k++) for (let l = -nmax[2]; l <= nmax[2]; l++) { - if (h === 0 && k === 0 && l === 0) continue; - const gx = h * recip[0][0] + k * recip[1][0] + l * recip[2][0]; - const gy = h * recip[0][1] + k * recip[1][1] + l * recip[2][1]; - const gz = h * recip[0][2] + k * recip[1][2] + l * recip[2][2]; - const gl = Math.hypot(gx, gy, gz); - if (gl > kMax || isCentered(h, k, l)) continue; - // structure factor F = sum f_j occ_j exp(-2 pi i g.r_j) / V (quantem convention) - let re = 0, im = 0; - const gk = gl.toFixed(5); - for (const s of species) { - const key = s + gk; - let f = fCache.get(key); - if (f === undefined) { f = scatteringFactor(s, gl); fCache.set(key, f); } - for (let j = 0; j < p.symbols.length; j++) { - if (p.symbols[j] !== s) continue; - const ph = -2 * Math.PI * (h * p.positions[j][0] + k * p.positions[j][1] + l * p.positions[j][2]); - re += f * p.occupancy[j] * Math.cos(ph); - im += f * p.occupancy[j] * Math.sin(ph); - } - } - re /= volume; im /= volume; - hkl.push([h, k, l]); - gList.push(gx, gy, gz); - F2.push(re * re + im * im); - // coupling U = gamma F / pi, plus an absorptive part i * fraction * U (same phase) - const ur = (gamma * re) / Math.PI, ui = (gamma * im) / Math.PI; - Ure.push(ur - ABSORPTION_FRACTION * ui); - Uim.push(ui + ABSORPTION_FRACTION * ur); - } - void g; - let f0 = 0; - for (let j = 0; j < p.symbols.length; j++) f0 += scatteringFactor(p.symbols[j], 0) * p.occupancy[j]; - const u0 = (gamma * f0) / (Math.PI * volume); - const couplingRe = new Map(); - const couplingIm = new Map(); - for (let i = 0; i < hkl.length; i++) { - couplingRe.set(`${hkl[i][0]},${hkl[i][1]},${hkl[i][2]}`, Ure[i]); - couplingIm.set(`${hkl[i][0]},${hkl[i][1]},${hkl[i][2]}`, Uim[i]); - } - const [a, b, , , , ga] = p.cellpar; - return { - name: p.name, - spacegroup: p.spacegroup, - pointgroup: "", - cell, - recip, - positions_frac: p.positions, - numbers: p.symbols.map((s) => Math.max(1, SYMBOLS.indexOf(s))), - symbols: p.symbols, - colors: p.symbols.map((s) => JMOL[s] || [0.7, 0.7, 0.7]), - radii: p.symbols.map((s) => RADII[s] || 1.4), - hkl, - g: Float32Array.from(gList), - F2: Float32Array.from(F2), - U_re: Float32Array.from(Ure), - U_im: Float32Array.from(Uim), - couplingRe, - couplingIm, - u0_imag: ABSORPTION_FRACTION * u0, - absorptive: true, - energy_ev: energyEv, - wavelength: electronWavelength(energyEv), - k_max: kMax, - hexagonal: Math.abs(a - b) < 1e-3 * a && Math.abs(ga - 120) < 0.05, - }; -} diff --git a/widget/js/diffsim-web/index.ts b/widget/js/diffsim-web/index.ts deleted file mode 100644 index b444120b2..000000000 --- a/widget/js/diffsim-web/index.ts +++ /dev/null @@ -1,857 +0,0 @@ -// diffraction-sim.js — interactive electron diffraction simulator for the -// website (MyST anywidget directive, no framework). Built from the quantEM -// widget sources: widget/js/diffsim-web/index.ts in the quantem repository -// (`npm run build` writes dist/diffraction-sim.js). -// -// Left panel: the unit cell, drawn as seen from the detector side (the beam -// comes toward you). Drag to tilt, two fingers to twist, buttons for 15 deg -// steps. Right panel: the diffraction pattern of the same orientation, -// computed live in the browser: nanobeam disks (Bloch wave intensities for -// the beams near the Ewald sphere, thin-slab intensities for the rest), -// CBED disks, or the Kossel / Kikuchi line pattern. Drag the pattern to move -// the tilt map with the pointer; double-click a point to tilt the crystal by -// that angle (the clicked direction moves onto the optic axis). -// -// Directive options (all optional): -// preset (structure name), zone_axis [u,v,w] or [u,v,t,w], thickness_A, semiconv_mrad, precession_deg, -// pattern_range (1/A), mode ("nanobeam" | "cbed" | "kossel"), dynamical, -// size (px per panel), show_labels (cell axes), show_hkl, show_kikuchi, polyhedra, n_cells, power -// (brightness exponent, default 0.5 = square root of the intensity), vmax -// (upper end of the contrast window, default 0.5 of the strongest diffracted beam). - -import { Quat, Vec3, directionIndices, fourToThree, matTVec, parseDirection, qmult, qnormalize, quatFromAxisAngle, quatFromZoneAxis, quatToMatrix, threeToFour } from "../diffsim/math"; -import { - CrystalData, NanobeamSolution, Reflection, blochIntensities, blochSolve, hybridBeams, kinematicalTilted, kosselLines, - labReflections, nanobeamIntensities, nanobeamSolve, parseCrystal, precessionTilts, slabIntensities, -} from "../diffsim/physics"; -import { cellGeometry, drawCell } from "../diffsim/crystal3d"; -import { - Frame, cbedImage, drawDisks, drawEwaldPanel, drawImage, drawKikuchiOverlay, drawKosselLines, setupCanvas, tiltGrid, toPx, -} from "../diffsim/pattern"; -import { ENERGY_EV, PRESETS } from "./presets"; -import { ParsedCif, crystalFromCif, parseCif } from "./cif"; - -const DIRECT: Reflection = { index: -1, hkl: [0, 0, 0], g: [0, 0, 0], gLen: 0, s: 0 }; -const SG_MAX = 0.05; -const VIEW_X = -1; // detector-side view: cell and pattern move together -const MAX_BEAMS = 48; -const MAX_BEAMS_DRAG = 28; -const SPIN_FPS = 20; // recompute rate while spinning (battery) -const CBED_GRID = 7; -const CBED_GRID_DRAG = 5; -const CBED_PREC_NODES = 8; // ring nodes per incident direction of the cone - -interface Model { get(key: string): unknown } - -function fmtIndices(v: [number, number, number] | null, hexagonal = false): string { - if (!v) return "—"; - const idx: number[] = hexagonal ? threeToFour(v) : v; - return "[" + idx.map((h) => (h < 0 ? `${-h}̅` : `${h}`)).join("") + "]"; -} - -function detectDark(): boolean { - const de = document.documentElement; - if (de.classList.contains("dark")) return true; - if (de.classList.contains("light")) return false; - try { - const m = getComputedStyle(document.body).backgroundColor.match(/\d+/g); - if (m && m.length >= 3) return (0.299 * +m[0] + 0.587 * +m[1] + 0.114 * +m[2]) / 255 < 0.5; - } catch (e) { /* ignore */ } - return !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches); -} - -export function render({ model, el }: { model: Model; el: HTMLElement }) { - const opt = (key: string, fallback: T): T => { - const v = model && typeof model.get === "function" ? (model.get(key) as T | undefined) : undefined; - return v === undefined || v === null ? fallback : v; - }; - const id = "dsim-" + Math.random().toString(36).slice(2, 8); - const names = Object.keys(PRESETS); - - // ---------------------------------------------------------------- state - const state = { - preset: names.includes(opt("preset", "")) ? opt("preset", "") : names[0], - mode: opt("mode", "nanobeam") as string, - dynamical: opt("dynamical", true) as boolean, - thickness: opt("thickness_A", 400) as number, - semiconv: opt("semiconv_mrad", 3) as number, - precession: opt("precession_deg", 0) as number, - fieldMrad: opt("field_mrad", 50) as number, - patternRange: opt("pattern_range", 3.0) as number, - stepDeg: 15, - spinSpeed: opt("rotation_speed_deg", 6) as number, // deg/s for the continuous rotation buttons - spinX: false, // continuous slow rotation about the screen x axis (vertical motion) - spinY: false, // ... about the screen y axis (horizontal motion) - showLabels: opt("show_labels", true) as boolean, - showHkl: opt("show_hkl", true) as boolean, - showAppearance: false, - kikuchi: opt("show_kikuchi", false) as boolean, - polyhedra: opt("polyhedra", false) as boolean, - nCells: (opt("n_cells", [1, 1, 1]) as number[]).slice(0, 3) as [number, number, number], - sizePref: opt("size", 400) as number, - power: opt("power", 0.5) as number, // brightness ~ intensity^power - vmin: 0, // contrast window on the scaled intensities (1 = strongest diffracted beam) - vmax: opt("vmax", 0.5) as number, - energy: opt("energy_ev", ENERGY_EV) as number, - showEwald: opt("show_ewald", true) as boolean, - quat: [1, 0, 0, 0] as Quat, - dragging: false, - spinning: false, - }; - const ENERGIES = [60e3, 80e3, 100e3, 120e3, 200e3, 300e3]; - if (!ENERGIES.includes(state.energy)) ENERGIES.push(state.energy); - ENERGIES.sort((a, b) => a - b); - const cifs = new Map(); // structures loaded from CIF files, by menu name - const loadCrystal = (name: string, energy: number): CrystalData => { - const cif = cifs.get(name); - if (cif) return crystalFromCif(cif, energy, 3.0); - return parseCrystal(PRESETS[name], energy)!; - }; - let crystal: CrystalData = loadCrystal(state.preset, state.energy); - let geom = cellGeometry(crystal, state.nCells, state.polyhedra); - const k0 = () => 1 / crystal.wavelength; - - const setZoneAxis = (uvw: Vec3) => { - const c = crystal.cell; - const d: Vec3 = [ - uvw[0] * c[0][0] + uvw[1] * c[1][0] + uvw[2] * c[2][0], - uvw[0] * c[0][1] + uvw[1] * c[1][1] + uvw[2] * c[2][1], - uvw[0] * c[0][2] + uvw[1] * c[1][2] + uvw[2] * c[2][2], - ]; - state.quat = quatFromZoneAxis(d); - }; - const za = opt("zone_axis", null) as number[] | null; - if (za && za.length === 4) setZoneAxis(fourToThree(za)); // Miller-Bravais [u v t w] - else if (za && za.length === 3) setZoneAxis(za as Vec3); - else setZoneAxis(crystal.hexagonal ? [0, 0, 1] : [1, 1, 0]); - - // ---------------------------------------------------------------- DOM - const style = document.createElement("style"); - style.textContent = ` - .${id}-wrap { background: var(--${id}-bg, #111); color: var(--${id}-fg, #ccc); border: 1px solid var(--${id}-border, #444); - border-radius: 8px; padding: 10px 14px 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - font-size: 13px; max-width: 100%; box-sizing: border-box; } - .${id}-titlerow { position: relative; display: flex; align-items: center; justify-content: center; margin-bottom: 6px; min-height: 28px; } - .${id}-title { font-size: 16px; font-weight: 600; color: var(--${id}-title, #ddd); text-align: center; } - .${id}-titlerow .${id}-btn { position: absolute; right: 0; top: 50%; transform: translateY(-50%); } - .${id}-file { display: none; } - .${id}-top { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; justify-content: center; margin-bottom: 8px; } - .${id}-panels { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; } - .${id}-panel { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 0 0 auto; } - .${id}-ctl { display: flex; flex-direction: column; gap: 6px; } - .${id}-panels.stacked { flex-direction: column; flex-wrap: nowrap; align-items: center; } - .${id}-panels.stacked .${id}-panel { display: contents; } - .${id}-panels.stacked #${id}-cell { order: 1; } - .${id}-panels.stacked #${id}-pat { order: 2; } - .${id}-panels.stacked #${id}-patctl { order: 3; } - .${id}-panels.stacked #${id}-cellctl { order: 4; } - .${id}-panels.stacked #${id}-ewaldc { order: 5; } - .${id}-canvas { display: block; border-radius: 4px; touch-action: none; cursor: grab; background: #000; } - .${id}-canvas.cell { background: var(--${id}-cellbg, #161616); border: 1px solid var(--${id}-border, #444); } - .${id}-row { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; } - .${id}-hint { font-size: 11px; color: var(--${id}-dim, #777); line-height: 1.35; } - .${id}-mono { font-family: ui-monospace, Menlo, monospace; font-size: 12px; } - .${id}-btn { padding: 4px 9px; border: 1px solid var(--${id}-border, #444); background: var(--${id}-btnbg, #222); - color: var(--${id}-fg, #ccc); border-radius: 4px; cursor: pointer; font-size: 12px; line-height: 1.2; } - .${id}-btn:hover { background: var(--${id}-btnhover, #333); } - .${id}-btn.active { background: #1a4d2e; border-color: #00cc66; color: #00ff88; } - .${id}-group { display: inline-flex; } - .${id}-group .${id}-btn { border-radius: 0; margin-left: -1px; } - .${id}-group .${id}-btn:first-child { border-radius: 4px 0 0 4px; margin-left: 0; } - .${id}-group .${id}-btn:last-child { border-radius: 0 4px 4px 0; } - .${id}-wrap select, .${id}-wrap input[type=text], .${id}-wrap input[type=number] { font-size: 12px; padding: 3px 6px; border-radius: 4px; - border: 1px solid var(--${id}-border, #444); background: var(--${id}-btnbg, #222); color: var(--${id}-fg, #ccc); } - .${id}-slider { flex: 1; min-width: 130px; max-width: 200px; } - .${id}-slider label { display: flex; justify-content: space-between; font-size: 11px; color: var(--${id}-label, #aaa); } - .${id}-slider input[type=range] { width: 100%; accent-color: #00cc66; margin: 2px 0 0; } - .${id}-check { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; } - .${id}-check input { accent-color: #00cc66; } - .${id}-appearance { padding: 6px 8px; border: 1px solid var(--${id}-border, #444); border-radius: 6px; } - .${id}-hist { display: block; border: 1px solid var(--${id}-border, #444); border-radius: 3px; cursor: ew-resize; touch-action: none; } - .${id}-histwrap { display: flex; flex-direction: column; gap: 2px; } - .${id}-histwrap .${id}-hint { display: flex; justify-content: space-between; font-family: ui-monospace, Menlo, monospace; } - `; - el.appendChild(style); - - const wrap = document.createElement("div"); - wrap.className = `${id}-wrap`; - const presetOptions = names.map((n) => ``).join(""); - wrap.innerHTML = ` -
-
Electron diffraction simulator
-
load CIF…
- -
-
- - -
go
-
reset
- -
-
-
- - -
-
-
x −
x +
-
y −
y +
-
z −
z +
- ° -
↔
↕
- ${state.spinSpeed}°/s -
-
- - -
-
- - - - -
-
Drag to tilt the crystal (the near face follows the pointer). Shift-drag, or two fingers, twist about the beam. The beam comes toward you.
-
-
-
- -
-
-
-
nanobeam
-
CBED
-
Kikuchi pattern
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
appearance ▾
-
- -
-
Drag the pattern to move the tilt map. Double-click a disk to tilt to its two-beam condition, or empty space to put the Laue circle centre there.
-
-
-
- `; - el.appendChild(wrap); - const $ = (sel: string) => wrap.querySelector(sel) as T; - const cellCanvas = $(`#${id}-cell`); - const ewaldCanvas = $(`#${id}-ewaldc`); - const patCanvas = $(`#${id}-pat`); - - // ---------------------------------------------------------------- theme - const palettes = { - dark: { bg: "#111", fg: "#ccc", title: "#ddd", label: "#aaa", dim: "#777", btnbg: "#222", btnhover: "#333", border: "#444", cellbg: "#161616" }, - light: { bg: "#f4f6f3", fg: "#333", title: "#1f1f1f", label: "#556", dim: "#777", btnbg: "#ffffff", btnhover: "#e9ebe6", border: "#d3d6d0", cellbg: "#fbfbfa" }, - }; - let dark = detectDark(); - const applyTheme = () => { - dark = detectDark(); - const p = palettes[dark ? "dark" : "light"]; - for (const k in p) wrap.style.setProperty(`--${id}-${k}`, (p as Record)[k]); - drawAll(); - }; - const mo = new MutationObserver(() => applyTheme()); - mo.observe(document.documentElement, { attributes: true }); - if (window.matchMedia) window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyTheme); - - // ---------------------------------------------------------------- size - // Layout: left column = cell (Sc square) above the Ewald panel (Sc x Se); - // right column = pattern square whose side S matches the left column's - // height, so S = Sc + gap + Se with Se = Sc / 2. On narrow (portrait) - // screens everything stacks in one column with the cell and the pattern - // adjacent, then the controls, then the Ewald panel. Both layouts are - // capped so the canvases fit in the visible height below a fixed page - // header (the MyST top bar). - let S = 400, Sc = 260, Se = 130; - const GAP = 8; - const panelsEl = $(`.${id}-panels`); - // small viewport height (browser toolbars shown): stable while the mobile toolbars collapse on scroll - const probe = document.createElement("div"); - probe.style.cssText = "position:fixed;top:0;left:0;width:0;height:100vh;height:100svh;visibility:hidden;pointer-events:none"; - el.appendChild(probe); - const availHeight = () => { - const h = probe.offsetHeight || window.innerHeight; - let nav = 0; - for (const e of document.querySelectorAll(".myst-top-nav, header")) { - const p = getComputedStyle(e).position; - const r = e.getBoundingClientRect(); - if ((p === "fixed" || p === "sticky") && r.top <= 0 && r.height < h / 3) nav = Math.max(nav, r.bottom); - } - return h - nav - 16; - }; - const computeSize = () => { - const w = wrap.clientWidth - 30; - const h = availHeight(); - const sideBySide = w >= 2.5 * 180 + 2 * GAP + 12; - if (sideBySide) { - const hCap = state.showEwald ? (h - GAP) / 1.5 : h; - Sc = Math.floor(Math.max(140, Math.min((w - 12 - GAP) / 2.5, state.sizePref / 1.5, hCap))); - Se = state.showEwald ? Math.round(Sc / 2) : 0; - S = Sc + (Se ? GAP + Se : 0); - } else { - // cell and pattern stacked: keep both on screen at once - Sc = Math.floor(Math.max(180, Math.min(state.sizePref, w, (h - 12) / 2))); - Se = state.showEwald ? Math.round(Sc / 2) : 0; - S = Sc; - } - panelsEl.classList.toggle("stacked", !sideBySide); - cellCanvas.style.width = `${Sc}px`; cellCanvas.style.height = `${Sc}px`; - ewaldCanvas.style.display = Se ? "block" : "none"; - ewaldCanvas.style.width = `${Sc}px`; ewaldCanvas.style.height = `${Se}px`; - patCanvas.style.width = `${S}px`; patCanvas.style.height = `${S}px`; - const panels = wrap.querySelectorAll(`.${id}-panel`); - if (panels[0]) panels[0].style.width = `${Sc}px`; - if (panels[1]) panels[1].style.width = `${S}px`; - $(`#${id}-cellctl`).style.width = sideBySide ? "" : `${Math.max(Sc, Math.min(w, 420))}px`; - $(`#${id}-patctl`).style.width = sideBySide ? "" : `${Math.max(Sc, Math.min(w, 420))}px`; - }; - - // ---------------------------------------------------------------- physics cache - let nb: { beams: Reflection[]; nDyn: number } = { beams: [], nDyn: 0 }; - let nbSolution: NanobeamSolution | null = null; - let nbTilts: [number, number][] = [[0, 0]]; - let cbed: { - grid: ReturnType; - beams: Reflection[]; - nDyn: number; - nodes: [number, number][]; // precession ring, one entry at the origin without it - sols: ReturnType[] | null; // grid tilt major, ring node minor - } | null = null; - let lines: ReturnType = []; - let geomKey = ""; - - const frame = (): Frame => ({ size: S, qMax: state.mode === "kossel" ? state.fieldMrad * 1e-3 : state.patternRange, viewX: VIEW_X }); - - // quality while dragging adapts to the device: the beam cap and the - // precession node count shrink when a recompute takes too long - let dragBeams = MAX_BEAMS_DRAG; - let dragNodes = 8; - const solveOrientation = () => { - // everything that depends on the orientation (not on thickness) - const q = state.quat; - const alpha = state.semiconv * 1e-3; - const maxBeams = (state.dragging || state.spinning) ? dragBeams : MAX_BEAMS; - if (state.mode === "nanobeam") { - nbTilts = precessionTilts(k0(), state.precession, (state.dragging || state.spinning) ? dragNodes : 16); - if (state.dynamical) { - // the physics always uses every reflection the crystal carries; the pattern range only crops the drawing - nbSolution = nanobeamSolve(crystal, q, crystal.k_max, SG_MAX, maxBeams, nbTilts); - nb = { beams: nbSolution.beams, nDyn: Math.round(nbSolution.nDynMean) }; - } else { - nb = { beams: [DIRECT, ...labReflections(crystal, q, crystal.k_max)], nDyn: 0 }; - nbSolution = null; - } - } else if (state.mode === "cbed") { - const Rk = k0() * Math.sin(alpha); - const grid = tiltGrid(Rk, (state.dragging || state.spinning) ? CBED_GRID_DRAG : CBED_GRID); - // every incident direction of the cone is itself precessed, so the cost - // is the grid times the ring: fewer ring nodes here than in nanobeam - const nRing = (state.dragging || state.spinning) ? Math.min(4, dragNodes) : CBED_PREC_NODES; - const nodes = precessionTilts(k0(), state.precession, nRing); - if (state.dynamical) { - const { beams, nDyn } = hybridBeams(crystal, q, crystal.k_max, SG_MAX, (state.dragging || state.spinning) ? 20 : 32, Math.sin(alpha)); - const dyn = beams.slice(0, nDyn); - const sols: ReturnType[] = []; - for (const t of grid.tilts) { - for (const nd of nodes) sols.push(blochSolve(crystal, dyn, [t[0] + nd[0], t[1] + nd[1]])); - } - cbed = { grid, beams, nDyn, nodes, sols }; - } else { - cbed = { grid, beams: [DIRECT, ...labReflections(crystal, q, crystal.k_max)], nDyn: 0, nodes, sols: null }; - } - } - if (state.mode === "kossel" || (state.mode !== "kossel" && state.kikuchi)) { - const fov = state.mode === "kossel" ? state.fieldMrad * 1e-3 : state.patternRange / k0(); - lines = kosselLines(crystal, q, Math.min(crystal.k_max, 2.5), fov); - } - }; - - const intensitiesNanobeam = (): Float64Array => { - if (state.dynamical && nbSolution) return nanobeamIntensities(crystal, nbSolution, state.thickness); - const out = new Float64Array(nb.beams.length); - for (const t of nbTilts) { - const v = kinematicalTilted(crystal, nb.beams, t, 0.02); - for (let i = 0; i < out.length; i++) out[i] += v[i] / nbTilts.length; - } - return out; - }; - - /** Intensity of every beam at each incident direction of the cone, averaged - * over the precession ring. */ - const intensitiesCbed = (): Float64Array[] => { - if (!cbed) return []; - const { grid, beams, nDyn, nodes, sols } = cbed; - return grid.tilts.map((t, i) => { - const out = new Float64Array(beams.length); - for (let k = 0; k < nodes.length; k++) { - const tilt: [number, number] = [t[0] + nodes[k][0], t[1] + nodes[k][1]]; - const acc = new Float64Array(beams.length); - if (sols) { - acc.set(blochIntensities(sols[i * nodes.length + k], state.thickness)); - slabIntensities(crystal, beams, nDyn, tilt, state.thickness, acc); - } else { - acc.set(kinematicalTilted(crystal, beams, tilt, 0.02)); - } - for (let b = 0; b < out.length; b++) out[b] += acc[b] / nodes.length; - } - return out; - }); - }; - - /** Disk-averaged intensity of every beam, for the double-click snap. */ - const intensitiesCbedMean = (): Float64Array => { - const per = intensitiesCbed(); - const out = new Float64Array(cbed ? cbed.beams.length : 0); - for (const arr of per) for (let b = 0; b < out.length; b++) out[b] += arr[b] / Math.max(per.length, 1); - return out; - }; - - // ---------------------------------------------------------------- drawing - const drawCellPanel = () => { - const key = `${state.preset}|${state.nCells.join(",")}|${state.polyhedra}`; - if (key !== geomKey) { geom = cellGeometry(crystal, state.nCells, state.polyhedra); geomKey = key; } - drawCell(cellCanvas, geom, state.quat, Sc, { dark, showAxes: true, showLabels: state.showLabels, atomScale: 0.45, viewX: VIEW_X }); - if (state.showEwald && Se > 0) { - const dpr = window.devicePixelRatio || 1; - if (ewaldCanvas.width !== Sc * dpr || ewaldCanvas.height !== Se * dpr) { ewaldCanvas.width = Sc * dpr; ewaldCanvas.height = Se * dpr; } - const ctx = ewaldCanvas.getContext("2d"); - if (ctx) { - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - const refl = state.mode === "nanobeam" && nb.beams.length ? nb.beams : labReflections(crystal, state.quat, crystal.k_max); - drawEwaldPanel(ctx, Sc, Se, refl, k0(), state.patternRange, SG_MAX, VIEW_X, dark, state.mode !== "kossel" ? state.precession : 0); - } - } - const R = quatToMatrix(state.quat); - $(`#${id}-za`).textContent = "zone axis " + fmtIndices(directionIndices(crystal.cell, matTVec(R, [0, 0, 1])), crystal.hexagonal); - $(`#${id}-zone`).placeholder = crystal.hexagonal ? "0 0 0 1" : "1 1 0"; - $(`#${id}-info`).textContent = `${crystal.name} · ${crystal.spacegroup || crystal.pointgroup}`; - }; - - // ---- histogram of the scaled intensities with a draggable contrast window - const histCanvas = $(`#${id}-hist`); - let histBins = new Array(64).fill(0); - const histogramOf = (vals: ArrayLike) => { - const bins = new Array(64).fill(0); - for (let i = 0; i < vals.length; i++) { - const v = vals[i]; - if (!(v >= 0)) continue; - bins[Math.min(63, Math.floor(v * 63.999))]++; - } - histBins = bins; - drawHistogram(); - }; - const drawHistogram = () => { - const dpr = window.devicePixelRatio || 1; - const W = 160, H = 40; - if (histCanvas.width !== W * dpr) { histCanvas.width = W * dpr; histCanvas.height = H * dpr; } - histCanvas.style.width = `${W}px`; histCanvas.style.height = `${H}px`; - const c = histCanvas.getContext("2d"); - if (!c) return; - c.setTransform(dpr, 0, 0, dpr, 0, 0); - c.fillStyle = dark ? "#1a1a1a" : "#f0f0f0"; - c.fillRect(0, 0, W, H); - const mx = Math.max(1e-3, ...histBins.map((v) => Math.log1p(v))); - const bw = W / 64; - for (let i = 0; i < 64; i++) { - const h = (Math.log1p(histBins[i]) / mx) * (H - 2); - const x = (i + 0.5) / 64; - c.fillStyle = x >= state.vmin && x <= state.vmax ? (dark ? "#9a9a9a" : "#666") : (dark ? "#444" : "#c4c4c4"); - c.fillRect(i * bw + 0.5, H - h, Math.max(1, bw - 1), h); - } - c.strokeStyle = "#00cc66"; c.lineWidth = 2; - for (const v of [state.vmin, state.vmax]) { c.beginPath(); c.moveTo(v * W, 0); c.lineTo(v * W, H); c.stroke(); } - $(`#${id}-hist-lo`).textContent = state.vmin.toFixed(2); - $(`#${id}-hist-hi`).textContent = state.vmax.toFixed(2); - }; - let histHandle: "lo" | "hi" | null = null; - histCanvas.addEventListener("pointerdown", (e) => { - const rect = histCanvas.getBoundingClientRect(); - const x = (e.clientX - rect.left) / rect.width; - histHandle = Math.abs(x - state.vmin) <= Math.abs(x - state.vmax) ? "lo" : "hi"; - histCanvas.setPointerCapture?.(e.pointerId); - e.preventDefault(); - }); - histCanvas.addEventListener("pointermove", (e) => { - if (!histHandle) return; - const rect = histCanvas.getBoundingClientRect(); - const x = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width)); - if (histHandle === "lo") state.vmin = Math.min(x, state.vmax - 0.02); - else state.vmax = Math.max(x, state.vmin + 0.02); - drawPattern(); - }); - const histUp = () => { histHandle = null; }; - histCanvas.addEventListener("pointerup", histUp); - histCanvas.addEventListener("pointercancel", histUp); - - const drawPattern = () => { - const ctx = setupCanvas(patCanvas, S); - if (!ctx) return; - const f = frame(); - let status = ""; - if (state.mode === "nanobeam") { - const inten = intensitiesNanobeam(); - drawDisks(ctx, f, nb.beams, inten, dark, state.showHkl, k0() * Math.sin(state.semiconv * 1e-3), state.power, state.vmin, state.vmax); - if (state.kikuchi) drawKikuchiOverlay(ctx, f, lines, k0(), dark); - let iMax = 0; - for (let i = 0; i < inten.length; i++) if (nb.beams[i].index >= 0) iMax = Math.max(iMax, inten[i]); - const scaled = new Float32Array(inten.length); - for (let i = 0; i < inten.length; i++) scaled[i] = inten[i] > 1e-6 * iMax ? Math.pow(inten[i] / (iMax || 1), state.power) : -1; - histogramOf(scaled); - const prec = state.precession > 0 ? `; precession ${state.precession.toFixed(2)}° averaged over ${nbTilts.length} ring nodes` : ""; - status = state.dynamical - ? `${nb.nDyn} Bloch beams (|s| < ${SG_MAX} Å⁻¹, absorptive), thin-slab intensities for the other ${nb.beams.length - nb.nDyn}${prec}` - : `kinematical: |F|² with a Gaussian excitation envelope (σ = 0.02 Å⁻¹)`; - } else if (state.mode === "cbed" && cbed) { - const inten = intensitiesCbed(); - const img = cbedImage(f, cbed.beams, cbed.grid, inten); - // normalise to the brightest pixel outside the direct disk - const Rpx = cbed.grid.R * (0.5 * S * 0.92) / f.qMax + 1.5; - let hi = 0; - for (let py = 0; py < S; py++) for (let px = 0; px < S; px++) { - if (Math.hypot(px + 0.5 - S / 2, py + 0.5 - S / 2) <= Rpx) continue; - const v = img[py * S + px]; - if (v > hi) hi = v; - } - if (!(hi > 0)) hi = 1; - // power-law scaling and the contrast window, like the disks - const disp = new Float32Array(img.length); - for (let i = 0; i < img.length; i++) disp[i] = Math.min(1, Math.pow(Math.max(img[i], 0) / hi, state.power)); - histogramOf(disp); - drawImage(ctx, f, disp, dark ? "gray" : "gray_r", state.vmin, state.vmax, dark, "Å⁻¹", 1); - if (state.kikuchi) drawKikuchiOverlay(ctx, f, lines, k0(), dark); - const prec = state.precession > 0 - ? `; precession ${state.precession.toFixed(2)}° over ${cbed.nodes.length} ring nodes` - : ""; - status = `${cbed.nDyn} Bloch beams × ${cbed.grid.tilts.length} incident tilts per disk; disks summed where they overlap${prec}`; - } else if (state.mode === "kossel") { - drawKosselLines(ctx, f, lines, dark, state.showHkl, 0.02); - status = `deficient line of every reflection: line width = two-beam rocking width |U_g| / (k₀|g|), darkness ∝ |U_g|`; - } - $(`#${id}-status`).textContent = status; - }; - - const drawAll = () => { drawCellPanel(); drawPattern(); }; - const recompute = () => { - const t0 = performance.now(); - solveOrientation(); - drawAll(); - if (state.dragging || state.spinning) { - // keep dragging responsive: aim for well under 100 ms per recompute - const dt = performance.now() - t0; - if (dt > 90) { dragBeams = Math.max(12, Math.round(dragBeams * 0.7)); dragNodes = Math.max(4, dragNodes - 2); } - else if (dt < 30) { dragBeams = Math.min(MAX_BEAMS_DRAG, dragBeams + 4); dragNodes = Math.min(8, dragNodes + 1); } - } - }; - - // ---------------------------------------------------------------- interaction - const rotateScreen = (axis: Vec3, deg: number) => { - // axis in screen coordinates (x right, y up, z toward the viewer) -> lab - const dq = quatFromAxisAngle([VIEW_X * axis[0], axis[1], VIEW_X * axis[2]], (deg * Math.PI) / 180); - state.quat = qnormalize(qmult(dq, state.quat)); - }; - const shiftPattern = (dqx: number, dqy: number, inverseAngstrom: boolean) => { - const ax = inverseAngstrom ? dqx / k0() : dqx, ay = inverseAngstrom ? dqy / k0() : dqy; - const ang = Math.hypot(ax, ay); - if (ang <= 0) return; - state.quat = qnormalize(qmult(quatFromAxisAngle([ay, -ax, 0], ang), state.quat)); - }; - let raf = 0; - const scheduleRecompute = () => { - if (raf) return; - raf = requestAnimationFrame(() => { raf = 0; recompute(); }); - }; - const endDrag = () => { - if (!state.dragging) return; - state.dragging = false; - cellCanvas.style.cursor = patCanvas.style.cursor = "grab"; - recompute(); // full quality - }; - - const attachDrag = (canvas: HTMLCanvasElement, onMove: (dx: number, dy: number) => void) => { - const pointers = new Map(); - canvas.addEventListener("pointerdown", (e) => { - canvas.setPointerCapture?.(e.pointerId); - pointers.set(e.pointerId, [e.clientX, e.clientY]); - state.dragging = true; - canvas.style.cursor = "grabbing"; - e.preventDefault(); - }); - canvas.addEventListener("pointermove", (e) => { - const prev = pointers.get(e.pointerId); - if (!prev) return; - const cur: [number, number] = [e.clientX, e.clientY]; - if (pointers.size >= 2) { - const other = [...pointers.entries()].find(([pid]) => pid !== e.pointerId); - if (other) { - const [ox, oy] = other[1]; - let da = Math.atan2(cur[1] - oy, cur[0] - ox) - Math.atan2(prev[1] - oy, prev[0] - ox); - if (da > Math.PI) da -= 2 * Math.PI; - if (da < -Math.PI) da += 2 * Math.PI; - rotateScreen([0, 0, 1], (-da * 180) / Math.PI); - } - } else if (e.shiftKey) { - // shift-drag twists about the beam: the desktop version of the two-finger gesture - const rect = canvas.getBoundingClientRect(); - const cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2; - let da = Math.atan2(cur[1] - cy, cur[0] - cx) - Math.atan2(prev[1] - cy, prev[0] - cx); - if (da > Math.PI) da -= 2 * Math.PI; - if (da < -Math.PI) da += 2 * Math.PI; - rotateScreen([0, 0, 1], (-da * 180) / Math.PI); - } else { - onMove(cur[0] - prev[0], cur[1] - prev[1]); - } - pointers.set(e.pointerId, cur); - scheduleRecompute(); - }); - const up = (e: PointerEvent) => { pointers.delete(e.pointerId); if (pointers.size === 0) endDrag(); }; - canvas.addEventListener("pointerup", up); - canvas.addEventListener("pointercancel", up); - canvas.addEventListener("pointerleave", up); - }; - attachDrag(cellCanvas, (dx, dy) => { - const ang = Math.hypot(dx, dy) * (180 / Sc); - if (ang > 0) rotateScreen([dy, dx, 0], ang); // trackball: the near face follows the pointer - }); - attachDrag(patCanvas, (dx, dy) => { - const f = frame(); - const sc = (0.5 * S * 0.92) / f.qMax; - shiftPattern((VIEW_X * dx) / sc, -dy / sc, state.mode !== "kossel"); - }); - // Double-click on a visible disk: tilt the crystal to the exact Bragg - // condition of that reflection (two-beam: Laue circle through 000 and g; - // w = s_g (-g_y, g_x) / |g_xy|^2 zeroes s_g). On empty space the - // Laue-circle centre moves to the clicked point (same sense); in Kossel - // mode the clicked direction of the tilt map moves onto the axis. - patCanvas.addEventListener("dblclick", (e) => { - const rect = patCanvas.getBoundingClientRect(); - const x = e.clientX - rect.left, y = e.clientY - rect.top; - const f = frame(); - const sc = (0.5 * S * 0.92) / f.qMax; - if (state.mode === "nanobeam" || state.mode === "cbed") { - // a convergent beam draws the same reflections as wide disks, so both - // modes snap to the same two-beam condition the same way - const beams = state.mode === "cbed" && cbed ? cbed.beams : nb.beams; - const inten = state.mode === "cbed" ? intensitiesCbedMean() : intensitiesNanobeam(); - let iMax = 0; - for (let i = 1; i < beams.length; i++) iMax = Math.max(iMax, inten[i]); - const disk = k0() * Math.sin(state.semiconv * 1e-3) * sc; - const snap = Math.max(8, (state.mode === "cbed" ? 1.0 : 1.5) * disk); - // several reflections of different g_z share one spot (in hcp the first - // HOLZ layer is only 0.21 1/A up): take the candidate under the click - // that needs the smallest tilt to reach Bragg, never more than 5 degrees - let best: Reflection | null = null, bestTilt = (5 * Math.PI) / 180; - for (let i = 0; i < beams.length; i++) { - const b = beams[i]; - if (b.index < 0 || !(inten[i] > 1e-4 * iMax)) continue; - const [px, py] = toPx(f, b.g[0], b.g[1]); - if (Math.hypot(px - x, py - y) > snap) continue; - const gxy = Math.hypot(b.g[0], b.g[1]); - if (gxy < 1e-6) continue; - const tilt = Math.abs(b.s) / gxy; - if (tilt < bestTilt) { bestTilt = tilt; best = b; } - } - if (best) { - const gxy2 = best.g[0] ** 2 + best.g[1] ** 2; - const wx = (-best.s * best.g[1]) / gxy2, wy = (best.s * best.g[0]) / gxy2; - state.quat = qnormalize(qmult(quatFromAxisAngle([wx, wy, 0], Math.hypot(wx, wy)), state.quat)); - recompute(); - return; - } - // empty space: the Laue circle centre moves to the click - shiftPattern((VIEW_X * (x - rect.width / 2)) / sc, -(y - rect.height / 2) / sc, true); - } else { - // the Kikuchi map is a map of beam directions, so the clicked direction - // moves onto the axis, which is the opposite sense - shiftPattern(-(VIEW_X * (x - rect.width / 2)) / sc, (y - rect.height / 2) / sc, false); - } - recompute(); - }); - - // ---------------------------------------------------------------- controls - wrap.querySelectorAll(`[data-rot]`).forEach((b) => { - b.addEventListener("click", () => { - const key = b.dataset.rot!; - const axis: Vec3 = [+(key[0] === "x"), +(key[0] === "y"), +(key[0] === "z")]; - rotateScreen(axis, key[1] === "+" ? state.stepDeg : -state.stepDeg); - recompute(); - }); - }); - $(`#${id}-step`).addEventListener("change", (e) => { state.stepDeg = Math.max(0.1, +(e.target as HTMLInputElement).value || 15); }); - // continuous slow rotation (toggle buttons): state.spinSpeed deg/s about the screen - // axes, recomputed at drag quality no more than SPIN_FPS times a second - let spinRaf = 0, spinLast = 0, spinDue = 0, spinPendX = 0, spinPendY = 0; - const spinTick = (t: number) => { - spinRaf = 0; - if (!state.spinX && !state.spinY) { - if (state.spinning) { state.spinning = false; recompute(); } // back to full quality - return; - } - const dt = spinLast ? Math.min(0.1, (t - spinLast) / 1000) : 0; - spinLast = t; - if (state.spinY) spinPendY += state.spinSpeed * dt; - if (state.spinX) spinPendX += state.spinSpeed * dt; - if (t >= spinDue) { - spinDue = t + 1000 / SPIN_FPS; - if (spinPendY) rotateScreen([0, 1, 0], spinPendY); - if (spinPendX) rotateScreen([1, 0, 0], spinPendX); - spinPendX = spinPendY = 0; - recompute(); - } - spinRaf = requestAnimationFrame(spinTick); - }; - $(`#${id}-spin`).addEventListener("input", (e) => { - state.spinSpeed = Math.max(1, +(e.target as HTMLInputElement).value || 6); - $(`#${id}-spinv`).textContent = `${state.spinSpeed}°/s`; - }); - wrap.querySelectorAll(`[data-spin]`).forEach((b) => { - b.addEventListener("click", () => { - if (b.dataset.spin === "x") state.spinX = !state.spinX; else state.spinY = !state.spinY; - b.classList.toggle("active", b.dataset.spin === "x" ? state.spinX : state.spinY); - if ((state.spinX || state.spinY) && !spinRaf) { state.spinning = true; spinLast = 0; spinRaf = requestAnimationFrame(spinTick); } - }); - }); - const goZone = () => { - const v = parseDirection($(`#${id}-zone`).value); // 3 or 4 (Miller-Bravais) indices - if (!v) return; - setZoneAxis(v); - recompute(); - }; - $(`#${id}-go`).addEventListener("click", goZone); - $(`#${id}-zone`).addEventListener("keydown", (e) => { if (e.key === "Enter") goZone(); }); - $(`#${id}-reset`).addEventListener("click", () => { setZoneAxis(crystal.hexagonal ? [0, 0, 1] : [1, 1, 0]); recompute(); }); - // CIF upload: parse in the browser, add to the structure menu, select it - const fileInput = $(`#${id}-ciffile`); - $(`#${id}-cifbtn`).addEventListener("click", () => fileInput.click()); - fileInput.addEventListener("change", async () => { - const file = fileInput.files?.[0]; - if (!file) return; - try { - const text = await file.text(); - const parsed = parseCif(text, file.name.replace(/\.cif$/i, "")); - const menuName = `${parsed.name} (CIF)`; - cifs.set(menuName, parsed); - const sel = $(`#${id}-preset`); - if (![...sel.options].some((o) => o.value === menuName)) { - const opt = document.createElement("option"); - opt.value = menuName; opt.textContent = menuName; - sel.appendChild(opt); - } - sel.value = menuName; - state.preset = menuName; - crystal = loadCrystal(menuName, state.energy); - const range = $(`#${id}-range`); - range.max = String(crystal.k_max); - if (state.patternRange > crystal.k_max) { state.patternRange = crystal.k_max; range.value = String(crystal.k_max); } - range.dispatchEvent(new Event("input")); // refresh the slider label - geomKey = ""; - setZoneAxis(crystal.hexagonal ? [0, 0, 1] : [1, 1, 0]); - recompute(); - $(`#${id}-status`).textContent = `${menuName}: ${parsed.symbols.length} atoms in the cell, ${parsed.spacegroup || "symmetry from the file"}, ${crystal.hkl.length} reflections out to ${crystal.k_max.toFixed(2)} Å⁻¹${crystal.k_max < 3 ? " (reduced for this cell size)" : ""}; absorption approximated as 8 % of the potential`; - } catch (err) { - $(`#${id}-status`).textContent = `could not read ${file.name}: ${(err as Error).message}`; - } - fileInput.value = ""; - }); - $(`#${id}-preset`).addEventListener("change", (e) => { - state.preset = (e.target as HTMLSelectElement).value; - crystal = loadCrystal(state.preset, state.energy); - const range = $(`#${id}-range`); - range.max = String(crystal.k_max); - if (state.patternRange > crystal.k_max) { state.patternRange = crystal.k_max; range.value = String(crystal.k_max); } - setZoneAxis(crystal.hexagonal ? [0, 0, 1] : [1, 1, 0]); - recompute(); - }); - const modeButtons = wrap.querySelectorAll(`[data-mode]`); - const updateModeUI = () => { - modeButtons.forEach((b) => b.classList.toggle("active", b.dataset.mode === state.mode)); - const show = (sel: string, on: boolean) => { $(sel).style.display = on ? "" : "none"; }; - // nanobeam and CBED share every control; the Kikuchi pattern is a map of - // beam directions and takes the field of view instead - const pattern = state.mode !== "kossel"; - show(`#${id}-thickwrap`, pattern && state.dynamical); - show(`#${id}-convwrap`, pattern); - show(`#${id}-rangewrap`, pattern); - show(`#${id}-fieldwrap`, !pattern); - show(`#${id}-dynwrap`, pattern); - show(`#${id}-kikwrap`, pattern); - show(`#${id}-precwrap`, pattern); - show(`#${id}-approw`, state.mode !== "kossel"); - show(`#${id}-apppanel`, state.mode !== "kossel" && state.showAppearance); - }; - modeButtons.forEach((b) => b.addEventListener("click", () => { state.mode = b.dataset.mode!; updateModeUI(); recompute(); })); - $(`#${id}-dyn`).addEventListener("change", (e) => { state.dynamical = (e.target as HTMLInputElement).checked; updateModeUI(); recompute(); }); - $(`#${id}-kik`).addEventListener("change", (e) => { state.kikuchi = (e.target as HTMLInputElement).checked; recompute(); }); - $(`#${id}-hkl`).addEventListener("change", (e) => { state.showHkl = (e.target as HTMLInputElement).checked; drawPattern(); }); - $(`#${id}-apptoggle`).addEventListener("click", () => { - state.showAppearance = !state.showAppearance; - $(`#${id}-apptoggle`).textContent = state.showAppearance ? "appearance ▴" : "appearance ▾"; - updateModeUI(); - if (state.showAppearance) drawPattern(); - }); - $(`#${id}-labels`).addEventListener("change", (e) => { state.showLabels = (e.target as HTMLInputElement).checked; drawAll(); }); - $(`#${id}-poly`).addEventListener("change", (e) => { state.polyhedra = (e.target as HTMLInputElement).checked; drawAll(); }); - $(`#${id}-ewald`).addEventListener("change", (e) => { state.showEwald = (e.target as HTMLInputElement).checked; computeSize(); drawAll(); }); - $(`#${id}-energy`).addEventListener("change", (e) => { - state.energy = +(e.target as HTMLSelectElement).value; - crystal = loadCrystal(state.preset, state.energy); - recompute(); - }); - $(`#${id}-ncell`).addEventListener("change", (e) => { - const n = Math.max(1, Math.min(3, Math.round(+(e.target as HTMLInputElement).value || 1))); - state.nCells = [n, n, n]; drawAll(); - }); - const slider = (sel: string, valSel: string, fmt: (v: number) => string, apply: (v: number) => void, heavy: boolean) => { - const inp = $(sel); - const out = $(valSel); - const update = () => { const v = +inp.value; out.textContent = fmt(v); apply(v); }; - inp.addEventListener("input", () => { update(); if (heavy) scheduleRecompute(); else drawPattern(); }); - update(); - }; - slider(`#${id}-thick`, `#${id}-thick-val`, (v) => `${v.toFixed(0)} Å`, (v) => { state.thickness = v; }, false); - slider(`#${id}-pow`, `#${id}-pow-val`, (v) => `p = ${v.toFixed(2)}`, (v) => { state.power = v; }, false); - slider(`#${id}-conv`, `#${id}-conv-val`, (v) => `${v.toFixed(1)} mrad`, (v) => { state.semiconv = v; }, true); - slider(`#${id}-prec`, `#${id}-prec-val`, (v) => (v > 0 ? `${v.toFixed(2)}°` : "off"), (v) => { state.precession = v; }, true); - slider(`#${id}-range`, `#${id}-range-val`, (v) => `${v.toFixed(2)} Å⁻¹`, (v) => { state.patternRange = v; }, true); - slider(`#${id}-field`, `#${id}-field-val`, (v) => `${v.toFixed(0)} mrad`, (v) => { state.fieldMrad = v; }, true); - // the convergence slider only changes the disk radius in nanobeam mode: no re-solve needed there - $(`#${id}-conv`).addEventListener("input", () => { if (state.mode === "nanobeam") drawPattern(); }); - - // ---------------------------------------------------------------- go - updateModeUI(); - computeSize(); - applyTheme(); - recompute(); - const relayout = () => { const old = S + Sc; computeSize(); if (S + Sc !== old) drawAll(); }; - const ro = new ResizeObserver(relayout); - ro.observe(wrap); - ro.observe(probe); // viewport height: rotation, window resize - return () => { mo.disconnect(); ro.disconnect(); probe.remove(); }; -} - -export default { render }; diff --git a/widget/js/diffsim-web/lobato.ts b/widget/js/diffsim-web/lobato.ts deleted file mode 100644 index d237d03c4..000000000 --- a/widget/js/diffsim-web/lobato.ts +++ /dev/null @@ -1,107 +0,0 @@ -// Generated from quantem/diffraction/data/lobato.json: Lobato & Van Dyck (2014) -// electron scattering factor parameters, 5 (a_i, b_i) pairs per element. -export const LOBATO: Record = { - H: [[0.006473848488, -0.4901925768, 0.5732841604, -0.3794033015, 0.5544264748], [2.785198854, 2.776204283, 2.775385911, 2.767593029, 2.765118976]], - He: [[3.057451161, -62.00447791, 64.00555371, -5.001325785, 0.1517988287], [1.089672487, 0.9398387981, 0.9252890344, 0.8229474987, 0.5773931107]], - Li: [[3.926222729, -4.548619626, 2.193353129, 0.0699451265, 0.002098642249], [8.142760135, 4.98941077, 4.144289992, 0.4019223151, 0.1564790347]], - Be: [[3.398249706, -1.908668861, 0.03907021175, -0.01116310102, 0.009462044654], [4.442701786, 3.324515425, 0.1897728803, 0.08719186146, 0.082780906]], - B: [[1.472792486, -0.4019330422, 0.305998957, 0.01961442172, 0.0009771771061], [3.749740483, 0.5880665361, 0.5156396131, 0.1213775701, 0.06809824122]], - C: [[124.4660886, -220.3528571, 195.2353523, -98.10793613, 0.01420230412], [2.421208493, 2.305379438, 2.048519321, 1.933525529, 0.07689768185]], - N: [[58.13271507, -147.5424091, 130.1430656, -39.61956741, 0.01059577633], [1.700448564, 1.559038526, 1.415768275, 1.278418182, 0.05655877985]], - O: [[29.94740452, -77.61012663, 99.88177646, -51.21270055, 0.008196189544], [1.302839879, 1.157941053, 1.009885493, 0.9433279714, 0.04331976113]], - F: [[0.9489848945, -30.1333923, 52.79650781, -22.70627038, 0.006569976645], [1.458829332, 0.6887799932, 0.6542398693, 0.6148361308, 0.03428374195]], - Ne: [[0.5827411922, 0.3706765618, -0.5467449674, 0.4140526825, 0.005199030809], [1.281185731, 0.4445208972, 0.1986508755, 0.1854772467, 0.0275738382]], - Na: [[23.67006039, -21.85317862, 0.5924994481, -0.02446522903, 0.004839502217], [8.451487735, 8.040966005, 0.6249960005, 0.1324503949, 0.0233994362]], - Mg: [[4.855010477, -2.662209065, 0.4780012361, -0.07023070647, 0.003989058281], [5.946392738, 4.171303125, 0.3982698082, 0.1618861858, 0.01953450564]], - Al: [[2.834095616, -4.280041334, 4.421916805, -0.03457744719, 0.003523859414], [6.66235024, 0.5512947222, 0.5093289634, 0.1117848374, 0.01676023518]], - Si: [[2.871891426, -2.061735012, 2.171140242, -0.06630736331, 0.003010707097], [5.084871036, 0.4291781853, 0.3664854342, 0.1197106113, 0.01439945361]], - P: [[2.7915184, -4.365068378, 4.435584555, -0.08096357734, 0.00267900018], [3.900659619, 0.3298259684, 0.3060899565, 0.1080832325, 0.01258944953]], - S: [[2.679714156, -0.4742528222, 0.514835949, -0.0958360025, 0.002488719638], [3.068891212, 0.3782167022, 0.1887218119, 0.092337059, 0.01119208772]], - Cl: [[2.5662484, -0.3388763508, 1.145845588, -0.9231093165, 0.00229168002], [2.415949204, 0.4214142393, 0.109592405, 0.09909554582, 0.009996659489]], - Ar: [[2.459817464, -0.3641981771, 0.2505844772, -0.05774370295, 0.002301438668], [1.94004632, 0.3992410679, 0.1174724063, 0.0567803726, 0.009155798329]], - K: [[5.811078786, -50.25370965, 48.86094121, 0.0740628592, 0.0007278027386], [12.66914834, 3.956410397, 3.683850596, 0.1074585176, 0.006655767894]], - Ca: [[21.17811615, -339.0438243, 322.7569585, 0.06500776739, 0.0006558743666], [6.396086194, 3.740247139, 3.648884499, 0.09450906345, 0.005985206199]], - Sc: [[12.60351866, -276.8753821, 268.8716039, 0.05568241789, 0.0005770712551], [6.156256154, 3.088735543, 3.027276633, 0.08188747484, 0.005382898321]], - Ti: [[8.575957752, -210.3315635, 206.0971726, 0.0477773949, 0.0005057164845], [6.007806689, 2.602858567, 2.553523451, 0.0711429484, 0.004856284394]], - V: [[6.527684332, -200.4305768, 198.0150539, 0.04139115181, 0.0004474550243], [5.835524794, 2.232559524, 2.197860186, 0.06239738768, 0.004403836491]], - Cr: [[3.028317848, -95.53939331, 96.17615624, 0.0359777316, 0.0003914928907], [8.359115043, 1.802637903, 1.77509489, 0.05481444121, 0.003998289689]], - Mn: [[4.374175506, -160.9255109, 160.2733081, 0.0312303861, 0.000346966021], [5.510317055, 1.687982164, 1.666140478, 0.04833703903, 0.003647469601]], - Fe: [[3.798100908, -91.68935494, 91.44542522, 0.0272754344, 0.0003033798544], [5.317126459, 1.497130949, 1.468092418, 0.04272478501, 0.003327918552]], - Co: [[3.330378745, -77.00175965, 77.07252218, 0.0239904669, 0.0002682566655], [5.181359646, 1.329151223, 1.30284929, 0.03806454868, 0.00305010008]], - Ni: [[2.969080787, -75.74770691, 76.03982876, 0.02101621137, 0.0002311517511], [5.041809491, 1.182755079, 1.162165458, 0.03374790879, 0.002786808621]], - Cu: [[1.752071452, -43.04105235, 44.07059155, 0.01868761541, 0.0002017273248], [6.18750498, 1.002662636, 0.9853843114, 0.03029847039, 0.002558555987]], - Zn: [[2.466371105, -61.46785413, 62.01769452, 0.01641601739, 0.0001724871176], [4.910280785, 0.9678985203, 0.9512838348, 0.02696009677, 0.00234109611]], - Ga: [[2.760102031, -34.44526142, 35.22622672, 0.0132067197, 0.0001259455609], [6.101282245, 0.7651433136, 0.7513286234, 0.02248796343, 0.002067373743]], - Ge: [[3.182416353, -52.45140378, 52.96908272, 0.01140961686, 9.509543581e-05], [5.017190409, 0.7123957644, 0.7022801925, 0.01967472957, 0.001841466145]], - As: [[3.456429691, -33.31760444, 33.57121939, 0.009790022956, 6.534348623e-05], [4.01358016, 0.6623557781, 0.6457719411, 0.01709193532, 0.001603016028]], - Se: [[3.649050478, -43.68516622, 43.69202886, 0.008449022842, 3.786114741e-05], [3.250432671, 0.6096662017, 0.5969713008, 0.01485545127, 0.001336255874]], - Br: [[3.838463122, -52.2723471, 51.98612795, 0.007339559893, 1.64694208e-05], [2.611894706, 0.5661950629, 0.5552793267, 0.01297464694, 0.001029865369]], - Kr: [[4.025410303, -46.30423321, 45.72136819, 0.006353379596, 1.334778453e-06], [2.136483814, 0.5265911665, 0.5141367845, 0.01128072422, 0.0004880898579]], - Rb: [[3.389753516, 2.143483487, 0.3543226035, 0.003740093401, 3.003425023e-07], [20.57448144, 1.910799452, 0.1974105894, 0.008134594653, 0.0002926857911]], - Sr: [[4.770925093, 1.475978502, 0.3044513555, 0.003594749819, 3.000855492e-07], [13.36688813, 1.337383796, 0.1775323694, 0.00779105033, 0.0002822551396]], - Y: [[4.607210199, 1.42801851, 0.2955810456, 0.003389978409, 2.66862975e-07], [10.86869056, 1.311374559, 0.1680228708, 0.007359645454, 0.000262343281]], - Zr: [[4.311754534, 1.49331578, 0.2812360501, 0.003093377385, 2.580244485e-07], [9.458965805, 1.330636228, 0.1565068714, 0.006824105238, 0.0002498188791]], - Nb: [[3.111790391, 2.202590609, 0.2703307749, 0.002687991948, 2.325494834e-07], [10.69031413, 1.653163562, 0.1451151857, 0.006139563516, 0.0002322402359]], - Mo: [[2.831059684, 2.348581375, 0.2451058884, 0.002352821082, 2.312708383e-07], [10.43571958, 1.604828687, 0.1316969348, 0.005549779014, 0.0002227474638]], - Tc: [[2.571798593, 2.45633742, 0.2206584409, 0.00205531418, 2.321329478e-07], [10.16431171, 1.534419192, 0.1191386198, 0.005018532409, 0.0002145903791]], - Ru: [[2.332300304, 2.535780255, 0.1982080421, 0.001761201305, 1.981294116e-07], [9.921674602, 1.455856688, 0.1076821879, 0.004472435457, 0.0001965747162]], - Rh: [[2.113525349, 2.586363162, 0.1770639139, 0.00149737051, 2.054814456e-07], [9.659137259, 1.371066569, 0.09703530276, 0.003971285091, 0.0001913551907]], - Pd: [[0.6421597962, 2.979148144, 0.168154426, 0.001337442139, 1.914109683e-07], [5.974797503, 1.433594325, 0.09098684012, 0.003624101371, 0.0001806889144]], - Ag: [[1.553172167, 2.639303647, 0.142015487, 0.001008504601, 1.9463843e-07], [8.156202358, 1.216008875, 0.07900988649, 0.002965479014, 0.0001745930959]], - Cd: [[61.5307852, -78.60167412, 21.55012926, 0.1376850157, 0.0003246449309], [3.114681025, 2.760169834, 1.935513123, 0.07224683473, 0.001170016296]], - In: [[4.222321779, -26.41213184, 27.28528527, 0.1216179019, 0.0003068835464], [6.072655104, 1.64550179, 1.522570749, 0.06565079147, 0.001120938515]], - Sn: [[5.142220746, -25.49454138, 25.74144875, 0.1117782276, 0.0002936473847], [5.272726365, 1.531949592, 1.40257525, 0.06116853873, 0.001075608154]], - Sb: [[6.241640318, -93.38687244, 92.63328758, 0.1034126922, 0.0002818484269], [4.269841081, 1.394077461, 1.355668549, 0.05726679497, 0.001033079543]], - Te: [[7.377433018, -126.0251069, 124.128405, 0.09599783718, 0.0002710722164], [3.469177578, 1.297598109, 1.267710676, 0.05377717502, 0.000993084577]], - I: [[9.644006663, -122.9244354, 118.6825648, 0.0895025947, 0.0002612761215], [2.726455454, 1.237234259, 1.200620369, 0.05066786823, 0.0009554378834]], - Xe: [[15.54517497, -118.2410279, 108.009525, 0.0836259342, 0.0002519918624], [2.106373409, 1.208603761, 1.153952706, 0.04781893913, 0.0009198862576]], - Cs: [[4.287087392, 3.232506654, 0.6740295336, 0.06189080834, 0.000235612053], [22.65878708, 2.237973865, 0.3689955687, 0.04022665753, 0.0008837618909]], - Ba: [[6.244751874, 2.351722714, 0.4742793732, 0.06381138743, 0.0002346512806], [15.14313542, 1.453790006, 0.3208356464, 0.04043545321, 0.0008540811313]], - La: [[6.097881796, 2.194951647, 0.548172792, 0.06166695732, 0.0002268073559], [12.42885443, 1.505359924, 0.3337380397, 0.03877445535, 0.0008240498841]], - Ce: [[5.795268796, 2.370226641, 0.4713987569, 0.05743682606, 0.0002189794893], [14.28010551, 1.359690157, 0.3020173497, 0.03664367981, 0.0007954345265]], - Pr: [[5.604062554, 2.357962596, 0.4760010986, 0.05441233743, 0.0002114146022], [13.95174902, 1.312397549, 0.2949337012, 0.03486015425, 0.0007682616827]], - Nd: [[5.42908392, 2.336873254, 0.4833735541, 0.05146549646, 0.0002037761329], [13.65036494, 1.267598414, 0.2886106816, 0.03312918074, 0.0007423484838]], - Pm: [[5.267744451, 2.308558133, 0.493265479, 0.04863562797, 0.0001963088423], [13.36020968, 1.225858566, 0.2829196321, 0.03147353971, 0.0007176580251]], - Sm: [[5.126804285, 2.26925534, 0.5042093005, 0.04569239924, 0.0001886750505], [13.1581501, 1.181295082, 0.2771070382, 0.02979576045, 0.0006940110992]], - Eu: [[4.979623597, 2.241830875, 0.5129339614, 0.04298018485, 0.0001813816925], [12.83926631, 1.147054464, 0.2703871609, 0.0282418715, 0.0006714866032]], - Gd: [[5.0783583, 1.957440272, 0.5928259832, 0.04195020341, 0.0001752410915], [10.51327255, 1.117649413, 0.2843867418, 0.02726633276, 0.0006503108607]], - Tb: [[4.711616367, 2.172619508, 0.5397176013, 0.03797960045, 0.0001669237636], [12.31094159, 1.087437968, 0.2598049655, 0.02532899239, 0.000629278567]], - Dy: [[4.590755045, 2.13572373, 0.5513555601, 0.03560584067, 0.0001598240169], [12.06567406, 1.058117371, 0.2539944389, 0.02395087396, 0.0006094827484]], - Ho: [[4.484001106, 2.089043471, 0.5659326183, 0.03327035902, 0.0001524456103], [11.87492507, 1.028284937, 0.2489078079, 0.02258440655, 0.0005903744452]], - Er: [[4.376651408, 2.046451504, 0.5806628606, 0.03123885283, 0.0001453748697], [11.66530807, 1.003147697, 0.2441532927, 0.021361858, 0.0005721103385]], - Tm: [[4.283083182, 1.995380015, 0.5970535713, 0.02909516689, 0.000138064769], [11.4961906, 0.977039102, 0.239429133, 0.02009122333, 0.0005543771385]], - Yb: [[4.195638407, 1.94333286, 0.6124466173, 0.02715152077, 0.0001305947874], [11.41077505, 0.9490119245, 0.2349503265, 0.01890515763, 0.0005372336492]], - Lu: [[4.356925933, 1.695892048, 0.6639045201, 0.02630200188, 0.0001254973185], [9.294345147, 0.910500046, 0.2387465959, 0.01820985425, 0.000521559372]], - Hf: [[4.331384057, 1.527648647, 0.7357959229, 0.02495263232, 0.0001187408526], [7.876844338, 0.9425156426, 0.24169948, 0.01728998944, 0.0005058346313]], - Ta: [[4.197260013, 1.468548068, 0.7839312482, 0.02324940949, 0.0001112613586], [6.936740249, 1.017252766, 0.2389489397, 0.01623324331, 0.000490239004]], - W: [[3.976297158, 1.522926843, 0.7977062464, 0.02136667416, 0.0001030786894], [6.296857792, 1.112899512, 0.2310570407, 0.01510135456, 0.0004746824771]], - Re: [[3.751443814, 1.62768803, 0.7817567555, 0.01951708039, 9.431998043e-05], [5.797546361, 1.182236311, 0.219913586, 0.01398104555, 0.0004591271258]], - Os: [[3.484015173, 1.793779204, 0.7448783567, 0.01754277852, 8.448723516e-05], [5.439988461, 1.227921341, 0.206208857, 0.01278994017, 0.0004430322268]], - Ir: [[1.599565782, 2.975344522, 0.6950926784, 0.01487796275, 6.905495791e-05], [5.792444474, 1.55300983, 0.1886263359, 0.01117634707, 0.0004227723617]], - Pt: [[2.04021564, 2.899226346, 0.6363440838, 0.01320719196, 5.673821925e-05], [6.658194296, 1.413379238, 0.1740001045, 0.01006878045, 0.0004030771057]], - Au: [[1.675934671, 3.00486603, 0.5953400132, 0.01171631866, 4.296782976e-05], [5.522310932, 1.38007223, 0.1622292377, 0.009018148904, 0.0003792776675]], - Hg: [[2.235228504, 2.682766387, 0.5551949262, 0.01072733544, 3.28473992e-05], [5.02030989, 1.230775906, 0.152248123, 0.008283991169, 0.0003562419389]], - Tl: [[2.803427374, 2.71882788, 0.5224759154, 0.00984537837, 2.345245265e-05], [6.558768728, 1.169724225, 0.1435566918, 0.007619765262, 0.0003296276739]], - Pb: [[3.60861021, 2.450567747, 0.4786395001, 0.008872142352, 1.040019329e-05], [6.581625946, 1.027728527, 0.1335336806, 0.006848612389, 0.0002763888755]], - Bi: [[4.242099011, 2.099943421, 0.4328366314, 0.008020215704, 7.217850309e-07], [5.752801155, 0.8739014892, 0.1235999562, 0.006176003331, 0.0001414951776]], - Po: [[4.636200957, 1.780633114, 0.403730444, 0.007685395547, 8.954159028e-08], [4.888252998, 0.7563105269, 0.1173023645, 0.005930343942, 7.666686639e-05]], - At: [[4.965922506, 1.438155615, 0.3712992006, 0.007472564285, 1.141152842e-07], [4.091293788, 0.6292289966, 0.1110966787, 0.005772350103, 8.085118939e-05]], - Rn: [[5.306156145, 1.117331602, 0.3158587231, 0.007203422423, 1.073553306e-07], [3.488007355, 0.4811907411, 0.1018744679, 0.005592453744, 7.795066735e-05]], - Fr: [[4.52053399, 4.106953979, 0.7139468785, 0.01692940277, 8.574921292e-05], [19.44822342, 1.898246732, 0.1695535636, 0.01148195675, 0.0003461220383]], - Ra: [[6.52401072, 3.207870807, 0.5404787743, 0.008782788069, 6.910106029e-06], [14.00925543, 1.32635036, 0.1314085684, 0.006286474345, 0.0002278399815]], - Ac: [[6.896028536, 2.835141545, 0.5035068819, 0.00802294511, 9.204009548e-08], [11.07638257, 1.171326163, 0.1234946514, 0.005735155958, 7.197075418e-05]], - Th: [[7.093749002, 2.529123739, 0.4821078199, 0.007719366741, 7.271141994e-08], [9.094737952, 1.06391667, 0.1186194466, 0.005539457089, 6.584947305e-05]], - Pa: [[6.434013248, 2.970999705, 0.4607966518, 0.007240331258, 6.362366643e-08], [10.25968513, 1.131774523, 0.1127759354, 0.005274595834, 6.192638241e-05]], - U: [[6.210708268, 3.039344538, 0.4373399844, 0.006807147641, 6.182293277e-08], [10.02141059, 1.103998802, 0.1072189732, 0.005024321309, 6.005069942e-05]], - Np: [[6.004315983, 3.094316545, 0.4128084877, 0.006408916579, 6.730073762e-08], [9.817930467, 1.069787051, 0.1016352914, 0.004795248469, 6.028065987e-05]], - Pu: [[5.200617168, 3.498494404, 0.4054149311, 0.006223437008, 6.008593295e-08], [11.20383102, 1.128463695, 0.09893334727, 0.00465917432, 5.72590639e-05]], - Am: [[5.0253386, 3.518439883, 0.3819503494, 0.005821102081, 6.526093388e-08], [10.97721907, 1.084772148, 0.09367468749, 0.004426823469, 5.738568371e-05]], - Cm: [[5.346561003, 3.224684666, 0.3494617526, 0.005292517567, 6.159176302e-08], [9.231183798, 0.9728362292, 0.0870596221, 0.004130119645, 5.494638944e-05]], - Bk: [[5.225823503, 3.22818874, 0.3270988788, 0.004888825756, 5.212722694e-08], [9.071357066, 0.9311242775, 0.08207206021, 0.00389029656, 5.103679766e-05]], - Cf: [[4.586412478, 3.518695957, 0.3192617142, 0.00472979648, 5.513244808e-08], [10.31861021, 0.9572788378, 0.07968074314, 0.003771932, 5.097508604e-05]], - Es: [[4.457994807, 3.508672126, 0.3013753805, 0.004407637462, 4.887879032e-08], [10.08906934, 0.9194464474, 0.07564191778, 0.003570264651, 4.804951744e-05]], - Fm: [[4.338975764, 3.491850989, 0.281471099, 0.004002092715, 5.529298082e-08], [9.963309372, 0.878083957, 0.07116371158, 0.003321524042, 4.851031795e-05]], - Md: [[4.227294704, 3.472492275, 0.2648222295, 0.003690728778, 6.258714262e-08], [9.72400602, 0.842873776, 0.0673534744, 0.003123646063, 4.91217097e-05]], - No: [[4.109517024, 3.457991325, 0.2470873512, 0.003304239209, 5.991049262e-08], [9.677359945, 0.8069400425, 0.0632815437, 0.002875446396, 4.706791537e-05]], - Lr: [[4.521474212, 3.202129856, 0.223028727, 0.002817164539, 4.06427954e-08], [8.283099069, 0.7319189581, 0.0580942773, 0.00256168016, 4.038165155e-05]], -}; diff --git a/widget/js/diffsim-web/presets.ts b/widget/js/diffsim-web/presets.ts deleted file mode 100644 index 5527bfff4..000000000 --- a/widget/js/diffsim-web/presets.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Generated by scripts/diffsim_presets.py: crystal data for the website -// simulator (200 keV, k_max 3.0 1/A). Do not edit by hand. -export const ENERGY_EV = 200000; -export const PRESETS: Record = { - "Si (diamond cubic)": "{\"name\":\"Si\",\"spacegroup\":\"Fd-3m (227)\",\"pointgroup\":\"m-3m\",\"cell\":[[5.431,0.0,0.0],[0.0,5.431,0.0],[0.0,0.0,5.431]],\"recip\":[[0.18412815319462345,0.0,0.0],[0.0,0.18412815319462345,0.0],[0.0,0.0,0.18412815319462345]],\"positions_frac\":[[0.0,0.0,0.0],[0.24999999999999997,0.24999999999999997,0.24999999999999997],[0.0,0.49999999999999994,0.49999999999999994],[0.24999999999999997,0.7499999999999999,0.7499999999999999],[0.49999999999999994,0.0,0.49999999999999994],[0.7499999999999999,0.24999999999999997,0.7499999999999999],[0.49999999999999994,0.49999999999999994,0.0],[0.7499999999999999,0.7499999999999999,0.24999999999999997]],\"numbers\":[14,14,14,14,14,14,14,14],\"symbols\":[\"Si\",\"Si\",\"Si\",\"Si\",\"Si\",\"Si\",\"Si\",\"Si\"],\"colors\":[[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627],[0.941,0.784,0.627]],\"radii\":[1.11,1.11,1.11,1.11,1.11,1.11,1.11,1.11],\"hkl_i16\":\"8P/+//7/8P/+/wAA8P/+/wIA8P8AAP7/8P8AAAAA8P8AAAIA8P8CAP7/8P8CAAAA8P8CAAIA8f/7//3/8f/7////8f/7/wEA8f/7/wMA8f/9//v/8f/9//3/8f/9////8f/9/wEA8f/9/wMA8f/9/wUA8f////v/8f////3/8f//////8f///wEA8f///wMA8f///wUA8f8BAPv/8f8BAP3/8f8BAP//8f8BAAEA8f8BAAMA8f8BAAUA8f8DAPv/8f8DAP3/8f8DAP//8f8DAAEA8f8DAAMA8f8DAAUA8f8FAP3/8f8FAP//8f8FAAEA8f8FAAMA8v/4//7/8v/4/wAA8v/4/wIA8v/6//z/8v/6//7/8v/6/wAA8v/6/wIA8v/6/wQA8v/8//r/8v/8//z/8v/8//7/8v/8/wAA8v/8/wIA8v/8/wQA8v/8/wYA8v/+//j/8v/+//r/8v/+//z/8v/+//7/8v/+/wAA8v/+/wIA8v/+/wQA8v/+/wYA8v/+/wgA8v8AAPj/8v8AAPr/8v8AAPz/8v8AAP7/8v8AAAAA8v8AAAIA8v8AAAQA8v8AAAYA8v8AAAgA8v8CAPj/8v8CAPr/8v8CAPz/8v8CAP7/8v8CAAAA8v8CAAIA8v8CAAQA8v8CAAYA8v8CAAgA8v8EAPr/8v8EAPz/8v8EAP7/8v8EAAAA8v8EAAIA8v8EAAQA8v8EAAYA8v8GAPz/8v8GAP7/8v8GAAAA8v8GAAIA8v8GAAQA8v8IAP7/8v8IAAAA8v8IAAIA8//3//3/8//3////8//3/wEA8//3/wMA8//5//v/8//5//3/8//5////8//5/wEA8//5/wMA8//5/wUA8//7//n/8//7//v/8//7//3/8//7////8//7/wEA8//7/wMA8//7/wUA8//7/wcA8//9//f/8//9//n/8//9//v/8//9//3/8//9////8//9/wEA8//9/wMA8//9/wUA8//9/wcA8//9/wkA8/////f/8/////n/8/////v/8/////3/8///////8////wEA8////wMA8////wUA8////wcA8////wkA8/8BAPf/8/8BAPn/8/8BAPv/8/8BAP3/8/8BAP//8/8BAAEA8/8BAAMA8/8BAAUA8/8BAAcA8/8BAAkA8/8DAPf/8/8DAPn/8/8DAPv/8/8DAP3/8/8DAP//8/8DAAEA8/8DAAMA8/8DAAUA8/8DAAcA8/8DAAkA8/8FAPn/8/8FAPv/8/8FAP3/8/8FAP//8/8FAAEA8/8FAAMA8/8FAAUA8/8FAAcA8/8HAPv/8/8HAP3/8/8HAP//8/8HAAEA8/8HAAMA8/8HAAUA8/8JAP3/8/8JAP//8/8JAAEA8/8JAAMA9P/2//z/9P/2//7/9P/2/wAA9P/2/wIA9P/2/wQA9P/4//r/9P/4//z/9P/4//7/9P/4/wAA9P/4/wIA9P/4/wQA9P/4/wYA9P/6//j/9P/6//r/9P/6//z/9P/6//7/9P/6/wAA9P/6/wIA9P/6/wQA9P/6/wYA9P/6/wgA9P/8//b/9P/8//j/9P/8//r/9P/8//z/9P/8//7/9P/8/wAA9P/8/wIA9P/8/wQA9P/8/wYA9P/8/wgA9P/8/woA9P/+//b/9P/+//j/9P/+//r/9P/+//z/9P/+//7/9P/+/wAA9P/+/wIA9P/+/wQA9P/+/wYA9P/+/wgA9P/+/woA9P8AAPb/9P8AAPj/9P8AAPr/9P8AAPz/9P8AAP7/9P8AAAAA9P8AAAIA9P8AAAQA9P8AAAYA9P8AAAgA9P8AAAoA9P8CAPb/9P8CAPj/9P8CAPr/9P8CAPz/9P8CAP7/9P8CAAAA9P8CAAIA9P8CAAQA9P8CAAYA9P8CAAgA9P8CAAoA9P8EAPb/9P8EAPj/9P8EAPr/9P8EAPz/9P8EAP7/9P8EAAAA9P8EAAIA9P8EAAQA9P8EAAYA9P8EAAgA9P8EAAoA9P8GAPj/9P8GAPr/9P8GAPz/9P8GAP7/9P8GAAAA9P8GAAIA9P8GAAQA9P8GAAYA9P8GAAgA9P8IAPr/9P8IAPz/9P8IAP7/9P8IAAAA9P8IAAIA9P8IAAQA9P8IAAYA9P8KAPz/9P8KAP7/9P8KAAAA9P8KAAIA9P8KAAQA9f/1//3/9f/1////9f/1/wEA9f/1/wMA9f/3//n/9f/3//v/9f/3//3/9f/3////9f/3/wEA9f/3/wMA9f/3/wUA9f/3/wcA9f/5//f/9f/5//n/9f/5//v/9f/5//3/9f/5////9f/5/wEA9f/5/wMA9f/5/wUA9f/5/wcA9f/5/wkA9f/7//f/9f/7//n/9f/7//v/9f/7//3/9f/7////9f/7/wEA9f/7/wMA9f/7/wUA9f/7/wcA9f/7/wkA9f/9//X/9f/9//f/9f/9//n/9f/9//v/9f/9//3/9f/9////9f/9/wEA9f/9/wMA9f/9/wUA9f/9/wcA9f/9/wkA9f/9/wsA9f////X/9f////f/9f////n/9f////v/9f////3/9f//////9f///wEA9f///wMA9f///wUA9f///wcA9f///wkA9f///wsA9f8BAPX/9f8BAPf/9f8BAPn/9f8BAPv/9f8BAP3/9f8BAP//9f8BAAEA9f8BAAMA9f8BAAUA9f8BAAcA9f8BAAkA9f8BAAsA9f8DAPX/9f8DAPf/9f8DAPn/9f8DAPv/9f8DAP3/9f8DAP//9f8DAAEA9f8DAAMA9f8DAAUA9f8DAAcA9f8DAAkA9f8DAAsA9f8FAPf/9f8FAPn/9f8FAPv/9f8FAP3/9f8FAP//9f8FAAEA9f8FAAMA9f8FAAUA9f8FAAcA9f8FAAkA9f8HAPf/9f8HAPn/9f8HAPv/9f8HAP3/9f8HAP//9f8HAAEA9f8HAAMA9f8HAAUA9f8HAAcA9f8HAAkA9f8JAPn/9f8JAPv/9f8JAP3/9f8JAP//9f8JAAEA9f8JAAMA9f8JAAUA9f8JAAcA9f8LAP3/9f8LAP//9f8LAAEA9f8LAAMA9v/0//z/9v/0//7/9v/0/wAA9v/0/wIA9v/0/wQA9v/2//j/9v/2//r/9v/2//z/9v/2//7/9v/2/wAA9v/2/wIA9v/2/wQA9v/2/wYA9v/2/wgA9v/4//b/9v/4//j/9v/4//r/9v/4//z/9v/4//7/9v/4/wAA9v/4/wIA9v/4/wQA9v/4/wYA9v/4/wgA9v/4/woA9v/6//b/9v/6//j/9v/6//r/9v/6//z/9v/6//7/9v/6/wAA9v/6/wIA9v/6/wQA9v/6/wYA9v/6/wgA9v/6/woA9v/8//T/9v/8//b/9v/8//j/9v/8//r/9v/8//z/9v/8//7/9v/8/wAA9v/8/wIA9v/8/wQA9v/8/wYA9v/8/wgA9v/8/woA9v/8/wwA9v/+//T/9v/+//b/9v/+//j/9v/+//r/9v/+//z/9v/+//7/9v/+/wAA9v/+/wIA9v/+/wQA9v/+/wYA9v/+/wgA9v/+/woA9v/+/wwA9v8AAPT/9v8AAPb/9v8AAPj/9v8AAPr/9v8AAPz/9v8AAP7/9v8AAAAA9v8AAAIA9v8AAAQA9v8AAAYA9v8AAAgA9v8AAAoA9v8AAAwA9v8CAPT/9v8CAPb/9v8CAPj/9v8CAPr/9v8CAPz/9v8CAP7/9v8CAAAA9v8CAAIA9v8CAAQA9v8CAAYA9v8CAAgA9v8CAAoA9v8CAAwA9v8EAPT/9v8EAPb/9v8EAPj/9v8EAPr/9v8EAPz/9v8EAP7/9v8EAAAA9v8EAAIA9v8EAAQA9v8EAAYA9v8EAAgA9v8EAAoA9v8EAAwA9v8GAPb/9v8GAPj/9v8GAPr/9v8GAPz/9v8GAP7/9v8GAAAA9v8GAAIA9v8GAAQA9v8GAAYA9v8GAAgA9v8GAAoA9v8IAPb/9v8IAPj/9v8IAPr/9v8IAPz/9v8IAP7/9v8IAAAA9v8IAAIA9v8IAAQA9v8IAAYA9v8IAAgA9v8IAAoA9v8KAPj/9v8KAPr/9v8KAPz/9v8KAP7/9v8KAAAA9v8KAAIA9v8KAAQA9v8KAAYA9v8KAAgA9v8MAPz/9v8MAP7/9v8MAAAA9v8MAAIA9v8MAAQA9//z//3/9//z////9//z/wEA9//z/wMA9//1//n/9//1//v/9//1//3/9//1////9//1/wEA9//1/wMA9//1/wUA9//1/wcA9//3//f/9//3//n/9//3//v/9//3//3/9//3////9//3/wEA9//3/wMA9//3/wUA9//3/wcA9//3/wkA9//5//X/9//5//f/9//5//n/9//5//v/9//5//3/9//5////9//5/wEA9//5/wMA9//5/wUA9//5/wcA9//5/wkA9//5/wsA9//7//X/9//7//f/9//7//n/9//7//v/9//7//3/9//7////9//7/wEA9//7/wMA9//7/wUA9//7/wcA9//7/wkA9//7/wsA9//9//P/9//9//X/9//9//f/9//9//n/9//9//v/9//9//3/9//9////9//9/wEA9//9/wMA9//9/wUA9//9/wcA9//9/wkA9//9/wsA9//9/w0A9/////P/9/////X/9/////f/9/////n/9/////v/9/////3/9///////9////wEA9////wMA9////wUA9////wcA9////wkA9////wsA9////w0A9/8BAPP/9/8BAPX/9/8BAPf/9/8BAPn/9/8BAPv/9/8BAP3/9/8BAP//9/8BAAEA9/8BAAMA9/8BAAUA9/8BAAcA9/8BAAkA9/8BAAsA9/8BAA0A9/8DAPP/9/8DAPX/9/8DAPf/9/8DAPn/9/8DAPv/9/8DAP3/9/8DAP//9/8DAAEA9/8DAAMA9/8DAAUA9/8DAAcA9/8DAAkA9/8DAAsA9/8DAA0A9/8FAPX/9/8FAPf/9/8FAPn/9/8FAPv/9/8FAP3/9/8FAP//9/8FAAEA9/8FAAMA9/8FAAUA9/8FAAcA9/8FAAkA9/8FAAsA9/8HAPX/9/8HAPf/9/8HAPn/9/8HAPv/9/8HAP3/9/8HAP//9/8HAAEA9/8HAAMA9/8HAAUA9/8HAAcA9/8HAAkA9/8HAAsA9/8JAPf/9/8JAPn/9/8JAPv/9/8JAP3/9/8JAP//9/8JAAEA9/8JAAMA9/8JAAUA9/8JAAcA9/8JAAkA9/8LAPn/9/8LAPv/9/8LAP3/9/8LAP//9/8LAAEA9/8LAAMA9/8LAAUA9/8LAAcA9/8NAP3/9/8NAP//9/8NAAEA9/8NAAMA+P/y//7/+P/y/wAA+P/y/wIA+P/0//r/+P/0//z/+P/0//7/+P/0/wAA+P/0/wIA+P/0/wQA+P/0/wYA+P/2//b/+P/2//j/+P/2//r/+P/2//z/+P/2//7/+P/2/wAA+P/2/wIA+P/2/wQA+P/2/wYA+P/2/wgA+P/2/woA+P/4//b/+P/4//j/+P/4//r/+P/4//z/+P/4//7/+P/4/wAA+P/4/wIA+P/4/wQA+P/4/wYA+P/4/wgA+P/4/woA+P/6//T/+P/6//b/+P/6//j/+P/6//r/+P/6//z/+P/6//7/+P/6/wAA+P/6/wIA+P/6/wQA+P/6/wYA+P/6/wgA+P/6/woA+P/6/wwA+P/8//T/+P/8//b/+P/8//j/+P/8//r/+P/8//z/+P/8//7/+P/8/wAA+P/8/wIA+P/8/wQA+P/8/wYA+P/8/wgA+P/8/woA+P/8/wwA+P/+//L/+P/+//T/+P/+//b/+P/+//j/+P/+//r/+P/+//z/+P/+//7/+P/+/wAA+P/+/wIA+P/+/wQA+P/+/wYA+P/+/wgA+P/+/woA+P/+/wwA+P/+/w4A+P8AAPL/+P8AAPT/+P8AAPb/+P8AAPj/+P8AAPr/+P8AAPz/+P8AAP7/+P8AAAAA+P8AAAIA+P8AAAQA+P8AAAYA+P8AAAgA+P8AAAoA+P8AAAwA+P8AAA4A+P8CAPL/+P8CAPT/+P8CAPb/+P8CAPj/+P8CAPr/+P8CAPz/+P8CAP7/+P8CAAAA+P8CAAIA+P8CAAQA+P8CAAYA+P8CAAgA+P8CAAoA+P8CAAwA+P8CAA4A+P8EAPT/+P8EAPb/+P8EAPj/+P8EAPr/+P8EAPz/+P8EAP7/+P8EAAAA+P8EAAIA+P8EAAQA+P8EAAYA+P8EAAgA+P8EAAoA+P8EAAwA+P8GAPT/+P8GAPb/+P8GAPj/+P8GAPr/+P8GAPz/+P8GAP7/+P8GAAAA+P8GAAIA+P8GAAQA+P8GAAYA+P8GAAgA+P8GAAoA+P8GAAwA+P8IAPb/+P8IAPj/+P8IAPr/+P8IAPz/+P8IAP7/+P8IAAAA+P8IAAIA+P8IAAQA+P8IAAYA+P8IAAgA+P8IAAoA+P8KAPb/+P8KAPj/+P8KAPr/+P8KAPz/+P8KAP7/+P8KAAAA+P8KAAIA+P8KAAQA+P8KAAYA+P8KAAgA+P8KAAoA+P8MAPr/+P8MAPz/+P8MAP7/+P8MAAAA+P8MAAIA+P8MAAQA+P8MAAYA+P8OAP7/+P8OAAAA+P8OAAIA+f/z//v/+f/z//3/+f/z////+f/z/wEA+f/z/wMA+f/z/wUA+f/1//f/+f/1//n/+f/1//v/+f/1//3/+f/1////+f/1/wEA+f/1/wMA+f/1/wUA+f/1/wcA+f/1/wkA+f/3//X/+f/3//f/+f/3//n/+f/3//v/+f/3//3/+f/3////+f/3/wEA+f/3/wMA+f/3/wUA+f/3/wcA+f/3/wkA+f/3/wsA+f/5//X/+f/5//f/+f/5//n/+f/5//v/+f/5//3/+f/5////+f/5/wEA+f/5/wMA+f/5/wUA+f/5/wcA+f/5/wkA+f/5/wsA+f/7//P/+f/7//X/+f/7//f/+f/7//n/+f/7//v/+f/7//3/+f/7////+f/7/wEA+f/7/wMA+f/7/wUA+f/7/wcA+f/7/wkA+f/7/wsA+f/7/w0A+f/9//P/+f/9//X/+f/9//f/+f/9//n/+f/9//v/+f/9//3/+f/9////+f/9/wEA+f/9/wMA+f/9/wUA+f/9/wcA+f/9/wkA+f/9/wsA+f/9/w0A+f////P/+f////X/+f////f/+f////n/+f////v/+f////3/+f//////+f///wEA+f///wMA+f///wUA+f///wcA+f///wkA+f///wsA+f///w0A+f8BAPP/+f8BAPX/+f8BAPf/+f8BAPn/+f8BAPv/+f8BAP3/+f8BAP//+f8BAAEA+f8BAAMA+f8BAAUA+f8BAAcA+f8BAAkA+f8BAAsA+f8BAA0A+f8DAPP/+f8DAPX/+f8DAPf/+f8DAPn/+f8DAPv/+f8DAP3/+f8DAP//+f8DAAEA+f8DAAMA+f8DAAUA+f8DAAcA+f8DAAkA+f8DAAsA+f8DAA0A+f8FAPP/+f8FAPX/+f8FAPf/+f8FAPn/+f8FAPv/+f8FAP3/+f8FAP//+f8FAAEA+f8FAAMA+f8FAAUA+f8FAAcA+f8FAAkA+f8FAAsA+f8FAA0A+f8HAPX/+f8HAPf/+f8HAPn/+f8HAPv/+f8HAP3/+f8HAP//+f8HAAEA+f8HAAMA+f8HAAUA+f8HAAcA+f8HAAkA+f8HAAsA+f8JAPX/+f8JAPf/+f8JAPn/+f8JAPv/+f8JAP3/+f8JAP//+f8JAAEA+f8JAAMA+f8JAAUA+f8JAAcA+f8JAAkA+f8JAAsA+f8LAPf/+f8LAPn/+f8LAPv/+f8LAP3/+f8LAP//+f8LAAEA+f8LAAMA+f8LAAUA+f8LAAcA+f8LAAkA+f8NAPv/+f8NAP3/+f8NAP//+f8NAAEA+f8NAAMA+f8NAAUA+v/y//z/+v/y//7/+v/y/wAA+v/y/wIA+v/y/wQA+v/0//j/+v/0//r/+v/0//z/+v/0//7/+v/0/wAA+v/0/wIA+v/0/wQA+v/0/wYA+v/0/wgA+v/2//b/+v/2//j/+v/2//r/+v/2//z/+v/2//7/+v/2/wAA+v/2/wIA+v/2/wQA+v/2/wYA+v/2/wgA+v/2/woA+v/4//T/+v/4//b/+v/4//j/+v/4//r/+v/4//z/+v/4//7/+v/4/wAA+v/4/wIA+v/4/wQA+v/4/wYA+v/4/wgA+v/4/woA+v/4/wwA+v/6//T/+v/6//b/+v/6//j/+v/6//r/+v/6//z/+v/6//7/+v/6/wAA+v/6/wIA+v/6/wQA+v/6/wYA+v/6/wgA+v/6/woA+v/6/wwA+v/8//L/+v/8//T/+v/8//b/+v/8//j/+v/8//r/+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/8/wYA+v/8/wgA+v/8/woA+v/8/wwA+v/8/w4A+v/+//L/+v/+//T/+v/+//b/+v/+//j/+v/+//r/+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v/+/wYA+v/+/wgA+v/+/woA+v/+/wwA+v/+/w4A+v8AAPL/+v8AAPT/+v8AAPb/+v8AAPj/+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8AAAgA+v8AAAoA+v8AAAwA+v8AAA4A+v8CAPL/+v8CAPT/+v8CAPb/+v8CAPj/+v8CAPr/+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8CAAYA+v8CAAgA+v8CAAoA+v8CAAwA+v8CAA4A+v8EAPL/+v8EAPT/+v8EAPb/+v8EAPj/+v8EAPr/+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8EAAYA+v8EAAgA+v8EAAoA+v8EAAwA+v8EAA4A+v8GAPT/+v8GAPb/+v8GAPj/+v8GAPr/+v8GAPz/+v8GAP7/+v8GAAAA+v8GAAIA+v8GAAQA+v8GAAYA+v8GAAgA+v8GAAoA+v8GAAwA+v8IAPT/+v8IAPb/+v8IAPj/+v8IAPr/+v8IAPz/+v8IAP7/+v8IAAAA+v8IAAIA+v8IAAQA+v8IAAYA+v8IAAgA+v8IAAoA+v8IAAwA+v8KAPb/+v8KAPj/+v8KAPr/+v8KAPz/+v8KAP7/+v8KAAAA+v8KAAIA+v8KAAQA+v8KAAYA+v8KAAgA+v8KAAoA+v8MAPj/+v8MAPr/+v8MAPz/+v8MAP7/+v8MAAAA+v8MAAIA+v8MAAQA+v8MAAYA+v8MAAgA+v8OAPz/+v8OAP7/+v8OAAAA+v8OAAIA+v8OAAQA+//x//3/+//x////+//x/wEA+//x/wMA+//z//n/+//z//v/+//z//3/+//z////+//z/wEA+//z/wMA+//z/wUA+//z/wcA+//1//f/+//1//n/+//1//v/+//1//3/+//1////+//1/wEA+//1/wMA+//1/wUA+//1/wcA+//1/wkA+//3//X/+//3//f/+//3//n/+//3//v/+//3//3/+//3////+//3/wEA+//3/wMA+//3/wUA+//3/wcA+//3/wkA+//3/wsA+//5//P/+//5//X/+//5//f/+//5//n/+//5//v/+//5//3/+//5////+//5/wEA+//5/wMA+//5/wUA+//5/wcA+//5/wkA+//5/wsA+//5/w0A+//7//P/+//7//X/+//7//f/+//7//n/+//7//v/+//7//3/+//7////+//7/wEA+//7/wMA+//7/wUA+//7/wcA+//7/wkA+//7/wsA+//7/w0A+//9//H/+//9//P/+//9//X/+//9//f/+//9//n/+//9//v/+//9//3/+//9////+//9/wEA+//9/wMA+//9/wUA+//9/wcA+//9/wkA+//9/wsA+//9/w0A+//9/w8A+/////H/+/////P/+/////X/+/////f/+/////n/+/////v/+/////3/+///////+////wEA+////wMA+////wUA+////wcA+////wkA+////wsA+////w0A+////w8A+/8BAPH/+/8BAPP/+/8BAPX/+/8BAPf/+/8BAPn/+/8BAPv/+/8BAP3/+/8BAP//+/8BAAEA+/8BAAMA+/8BAAUA+/8BAAcA+/8BAAkA+/8BAAsA+/8BAA0A+/8BAA8A+/8DAPH/+/8DAPP/+/8DAPX/+/8DAPf/+/8DAPn/+/8DAPv/+/8DAP3/+/8DAP//+/8DAAEA+/8DAAMA+/8DAAUA+/8DAAcA+/8DAAkA+/8DAAsA+/8DAA0A+/8DAA8A+/8FAPP/+/8FAPX/+/8FAPf/+/8FAPn/+/8FAPv/+/8FAP3/+/8FAP//+/8FAAEA+/8FAAMA+/8FAAUA+/8FAAcA+/8FAAkA+/8FAAsA+/8FAA0A+/8HAPP/+/8HAPX/+/8HAPf/+/8HAPn/+/8HAPv/+/8HAP3/+/8HAP//+/8HAAEA+/8HAAMA+/8HAAUA+/8HAAcA+/8HAAkA+/8HAAsA+/8HAA0A+/8JAPX/+/8JAPf/+/8JAPn/+/8JAPv/+/8JAP3/+/8JAP//+/8JAAEA+/8JAAMA+/8JAAUA+/8JAAcA+/8JAAkA+/8JAAsA+/8LAPf/+/8LAPn/+/8LAPv/+/8LAP3/+/8LAP//+/8LAAEA+/8LAAMA+/8LAAUA+/8LAAcA+/8LAAkA+/8NAPn/+/8NAPv/+/8NAP3/+/8NAP//+/8NAAEA+/8NAAMA+/8NAAUA+/8NAAcA+/8PAP3/+/8PAP//+/8PAAEA+/8PAAMA/P/y//r//P/y//z//P/y//7//P/y/wAA/P/y/wIA/P/y/wQA/P/y/wYA/P/0//b//P/0//j//P/0//r//P/0//z//P/0//7//P/0/wAA/P/0/wIA/P/0/wQA/P/0/wYA/P/0/wgA/P/0/woA/P/2//T//P/2//b//P/2//j//P/2//r//P/2//z//P/2//7//P/2/wAA/P/2/wIA/P/2/wQA/P/2/wYA/P/2/wgA/P/2/woA/P/2/wwA/P/4//T//P/4//b//P/4//j//P/4//r//P/4//z//P/4//7//P/4/wAA/P/4/wIA/P/4/wQA/P/4/wYA/P/4/wgA/P/4/woA/P/4/wwA/P/6//L//P/6//T//P/6//b//P/6//j//P/6//r//P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/6/wYA/P/6/wgA/P/6/woA/P/6/wwA/P/6/w4A/P/8//L//P/8//T//P/8//b//P/8//j//P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/8/wgA/P/8/woA/P/8/wwA/P/8/w4A/P/+//L//P/+//T//P/+//b//P/+//j//P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P/+/wgA/P/+/woA/P/+/wwA/P/+/w4A/P8AAPL//P8AAPT//P8AAPb//P8AAPj//P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8AAAgA/P8AAAoA/P8AAAwA/P8AAA4A/P8CAPL//P8CAPT//P8CAPb//P8CAPj//P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8CAAgA/P8CAAoA/P8CAAwA/P8CAA4A/P8EAPL//P8EAPT//P8EAPb//P8EAPj//P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8EAAgA/P8EAAoA/P8EAAwA/P8EAA4A/P8GAPL//P8GAPT//P8GAPb//P8GAPj//P8GAPr//P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8GAAYA/P8GAAgA/P8GAAoA/P8GAAwA/P8GAA4A/P8IAPT//P8IAPb//P8IAPj//P8IAPr//P8IAPz//P8IAP7//P8IAAAA/P8IAAIA/P8IAAQA/P8IAAYA/P8IAAgA/P8IAAoA/P8IAAwA/P8KAPT//P8KAPb//P8KAPj//P8KAPr//P8KAPz//P8KAP7//P8KAAAA/P8KAAIA/P8KAAQA/P8KAAYA/P8KAAgA/P8KAAoA/P8KAAwA/P8MAPb//P8MAPj//P8MAPr//P8MAPz//P8MAP7//P8MAAAA/P8MAAIA/P8MAAQA/P8MAAYA/P8MAAgA/P8MAAoA/P8OAPr//P8OAPz//P8OAP7//P8OAAAA/P8OAAIA/P8OAAQA/P8OAAYA/f/x//v//f/x//3//f/x/////f/x/wEA/f/x/wMA/f/x/wUA/f/z//f//f/z//n//f/z//v//f/z//3//f/z/////f/z/wEA/f/z/wMA/f/z/wUA/f/z/wcA/f/z/wkA/f/1//X//f/1//f//f/1//n//f/1//v//f/1//3//f/1/////f/1/wEA/f/1/wMA/f/1/wUA/f/1/wcA/f/1/wkA/f/1/wsA/f/3//P//f/3//X//f/3//f//f/3//n//f/3//v//f/3//3//f/3/////f/3/wEA/f/3/wMA/f/3/wUA/f/3/wcA/f/3/wkA/f/3/wsA/f/3/w0A/f/5//P//f/5//X//f/5//f//f/5//n//f/5//v//f/5//3//f/5/////f/5/wEA/f/5/wMA/f/5/wUA/f/5/wcA/f/5/wkA/f/5/wsA/f/5/w0A/f/7//H//f/7//P//f/7//X//f/7//f//f/7//n//f/7//v//f/7//3//f/7/////f/7/wEA/f/7/wMA/f/7/wUA/f/7/wcA/f/7/wkA/f/7/wsA/f/7/w0A/f/7/w8A/f/9//H//f/9//P//f/9//X//f/9//f//f/9//n//f/9//v//f/9//3//f/9/////f/9/wEA/f/9/wMA/f/9/wUA/f/9/wcA/f/9/wkA/f/9/wsA/f/9/w0A/f/9/w8A/f////H//f////P//f////X//f////f//f////n//f////v//f////3//f///////f///wEA/f///wMA/f///wUA/f///wcA/f///wkA/f///wsA/f///w0A/f///w8A/f8BAPH//f8BAPP//f8BAPX//f8BAPf//f8BAPn//f8BAPv//f8BAP3//f8BAP///f8BAAEA/f8BAAMA/f8BAAUA/f8BAAcA/f8BAAkA/f8BAAsA/f8BAA0A/f8BAA8A/f8DAPH//f8DAPP//f8DAPX//f8DAPf//f8DAPn//f8DAPv//f8DAP3//f8DAP///f8DAAEA/f8DAAMA/f8DAAUA/f8DAAcA/f8DAAkA/f8DAAsA/f8DAA0A/f8DAA8A/f8FAPH//f8FAPP//f8FAPX//f8FAPf//f8FAPn//f8FAPv//f8FAP3//f8FAP///f8FAAEA/f8FAAMA/f8FAAUA/f8FAAcA/f8FAAkA/f8FAAsA/f8FAA0A/f8FAA8A/f8HAPP//f8HAPX//f8HAPf//f8HAPn//f8HAPv//f8HAP3//f8HAP///f8HAAEA/f8HAAMA/f8HAAUA/f8HAAcA/f8HAAkA/f8HAAsA/f8HAA0A/f8JAPP//f8JAPX//f8JAPf//f8JAPn//f8JAPv//f8JAP3//f8JAP///f8JAAEA/f8JAAMA/f8JAAUA/f8JAAcA/f8JAAkA/f8JAAsA/f8JAA0A/f8LAPX//f8LAPf//f8LAPn//f8LAPv//f8LAP3//f8LAP///f8LAAEA/f8LAAMA/f8LAAUA/f8LAAcA/f8LAAkA/f8LAAsA/f8NAPf//f8NAPn//f8NAPv//f8NAP3//f8NAP///f8NAAEA/f8NAAMA/f8NAAUA/f8NAAcA/f8NAAkA/f8PAPv//f8PAP3//f8PAP///f8PAAEA/f8PAAMA/f8PAAUA/v/w//7//v/w/wAA/v/w/wIA/v/y//j//v/y//r//v/y//z//v/y//7//v/y/wAA/v/y/wIA/v/y/wQA/v/y/wYA/v/y/wgA/v/0//b//v/0//j//v/0//r//v/0//z//v/0//7//v/0/wAA/v/0/wIA/v/0/wQA/v/0/wYA/v/0/wgA/v/0/woA/v/2//T//v/2//b//v/2//j//v/2//r//v/2//z//v/2//7//v/2/wAA/v/2/wIA/v/2/wQA/v/2/wYA/v/2/wgA/v/2/woA/v/2/wwA/v/4//L//v/4//T//v/4//b//v/4//j//v/4//r//v/4//z//v/4//7//v/4/wAA/v/4/wIA/v/4/wQA/v/4/wYA/v/4/wgA/v/4/woA/v/4/wwA/v/4/w4A/v/6//L//v/6//T//v/6//b//v/6//j//v/6//r//v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/6/wYA/v/6/wgA/v/6/woA/v/6/wwA/v/6/w4A/v/8//L//v/8//T//v/8//b//v/8//j//v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/8/wgA/v/8/woA/v/8/wwA/v/8/w4A/v/+//D//v/+//L//v/+//T//v/+//b//v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v/+/woA/v/+/wwA/v/+/w4A/v/+/xAA/v8AAPD//v8AAPL//v8AAPT//v8AAPb//v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8AAAoA/v8AAAwA/v8AAA4A/v8AABAA/v8CAPD//v8CAPL//v8CAPT//v8CAPb//v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8CAAoA/v8CAAwA/v8CAA4A/v8CABAA/v8EAPL//v8EAPT//v8EAPb//v8EAPj//v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8EAAgA/v8EAAoA/v8EAAwA/v8EAA4A/v8GAPL//v8GAPT//v8GAPb//v8GAPj//v8GAPr//v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8GAAYA/v8GAAgA/v8GAAoA/v8GAAwA/v8GAA4A/v8IAPL//v8IAPT//v8IAPb//v8IAPj//v8IAPr//v8IAPz//v8IAP7//v8IAAAA/v8IAAIA/v8IAAQA/v8IAAYA/v8IAAgA/v8IAAoA/v8IAAwA/v8IAA4A/v8KAPT//v8KAPb//v8KAPj//v8KAPr//v8KAPz//v8KAP7//v8KAAAA/v8KAAIA/v8KAAQA/v8KAAYA/v8KAAgA/v8KAAoA/v8KAAwA/v8MAPb//v8MAPj//v8MAPr//v8MAPz//v8MAP7//v8MAAAA/v8MAAIA/v8MAAQA/v8MAAYA/v8MAAgA/v8MAAoA/v8OAPj//v8OAPr//v8OAPz//v8OAP7//v8OAAAA/v8OAAIA/v8OAAQA/v8OAAYA/v8OAAgA/v8QAP7//v8QAAAA/v8QAAIA///x//v////x//3////x///////x/wEA///x/wMA///x/wUA///z//f////z//n////z//v////z//3////z///////z/wEA///z/wMA///z/wUA///z/wcA///z/wkA///1//X////1//f////1//n////1//v////1//3////1///////1/wEA///1/wMA///1/wUA///1/wcA///1/wkA///1/wsA///3//P////3//X////3//f////3//n////3//v////3//3////3///////3/wEA///3/wMA///3/wUA///3/wcA///3/wkA///3/wsA///3/w0A///5//P////5//X////5//f////5//n////5//v////5//3////5///////5/wEA///5/wMA///5/wUA///5/wcA///5/wkA///5/wsA///5/w0A///7//H////7//P////7//X////7//f////7//n////7//v////7//3////7///////7/wEA///7/wMA///7/wUA///7/wcA///7/wkA///7/wsA///7/w0A///7/w8A///9//H////9//P////9//X////9//f////9//n////9//v////9//3////9///////9/wEA///9/wMA///9/wUA///9/wcA///9/wkA///9/wsA///9/w0A///9/w8A//////H///////P///////X///////f///////n///////v///////3//////////////wEA/////wMA/////wUA/////wcA/////wkA/////wsA/////w0A/////w8A//8BAPH///8BAPP///8BAPX///8BAPf///8BAPn///8BAPv///8BAP3///8BAP////8BAAEA//8BAAMA//8BAAUA//8BAAcA//8BAAkA//8BAAsA//8BAA0A//8BAA8A//8DAPH///8DAPP///8DAPX///8DAPf///8DAPn///8DAPv///8DAP3///8DAP////8DAAEA//8DAAMA//8DAAUA//8DAAcA//8DAAkA//8DAAsA//8DAA0A//8DAA8A//8FAPH///8FAPP///8FAPX///8FAPf///8FAPn///8FAPv///8FAP3///8FAP////8FAAEA//8FAAMA//8FAAUA//8FAAcA//8FAAkA//8FAAsA//8FAA0A//8FAA8A//8HAPP///8HAPX///8HAPf///8HAPn///8HAPv///8HAP3///8HAP////8HAAEA//8HAAMA//8HAAUA//8HAAcA//8HAAkA//8HAAsA//8HAA0A//8JAPP///8JAPX///8JAPf///8JAPn///8JAPv///8JAP3///8JAP////8JAAEA//8JAAMA//8JAAUA//8JAAcA//8JAAkA//8JAAsA//8JAA0A//8LAPX///8LAPf///8LAPn///8LAPv///8LAP3///8LAP////8LAAEA//8LAAMA//8LAAUA//8LAAcA//8LAAkA//8LAAsA//8NAPf///8NAPn///8NAPv///8NAP3///8NAP////8NAAEA//8NAAMA//8NAAUA//8NAAcA//8NAAkA//8PAPv///8PAP3///8PAP////8PAAEA//8PAAMA//8PAAUAAADw//7/AADw/wAAAADw/wIAAADy//j/AADy//r/AADy//z/AADy//7/AADy/wAAAADy/wIAAADy/wQAAADy/wYAAADy/wgAAAD0//b/AAD0//j/AAD0//r/AAD0//z/AAD0//7/AAD0/wAAAAD0/wIAAAD0/wQAAAD0/wYAAAD0/wgAAAD0/woAAAD2//T/AAD2//b/AAD2//j/AAD2//r/AAD2//z/AAD2//7/AAD2/wAAAAD2/wIAAAD2/wQAAAD2/wYAAAD2/wgAAAD2/woAAAD2/wwAAAD4//L/AAD4//T/AAD4//b/AAD4//j/AAD4//r/AAD4//z/AAD4//7/AAD4/wAAAAD4/wIAAAD4/wQAAAD4/wYAAAD4/wgAAAD4/woAAAD4/wwAAAD4/w4AAAD6//L/AAD6//T/AAD6//b/AAD6//j/AAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD6/wgAAAD6/woAAAD6/wwAAAD6/w4AAAD8//L/AAD8//T/AAD8//b/AAD8//j/AAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD8/wgAAAD8/woAAAD8/wwAAAD8/w4AAAD+//D/AAD+//L/AAD+//T/AAD+//b/AAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD+/woAAAD+/wwAAAD+/w4AAAD+/xAAAAAAAPD/AAAAAPL/AAAAAPT/AAAAAPb/AAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAAAAAoAAAAAAAwAAAAAAA4AAAAAABAAAAACAPD/AAACAPL/AAACAPT/AAACAPb/AAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAACAAoAAAACAAwAAAACAA4AAAACABAAAAAEAPL/AAAEAPT/AAAEAPb/AAAEAPj/AAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAEAAgAAAAEAAoAAAAEAAwAAAAEAA4AAAAGAPL/AAAGAPT/AAAGAPb/AAAGAPj/AAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAGAAgAAAAGAAoAAAAGAAwAAAAGAA4AAAAIAPL/AAAIAPT/AAAIAPb/AAAIAPj/AAAIAPr/AAAIAPz/AAAIAP7/AAAIAAAAAAAIAAIAAAAIAAQAAAAIAAYAAAAIAAgAAAAIAAoAAAAIAAwAAAAIAA4AAAAKAPT/AAAKAPb/AAAKAPj/AAAKAPr/AAAKAPz/AAAKAP7/AAAKAAAAAAAKAAIAAAAKAAQAAAAKAAYAAAAKAAgAAAAKAAoAAAAKAAwAAAAMAPb/AAAMAPj/AAAMAPr/AAAMAPz/AAAMAP7/AAAMAAAAAAAMAAIAAAAMAAQAAAAMAAYAAAAMAAgAAAAMAAoAAAAOAPj/AAAOAPr/AAAOAPz/AAAOAP7/AAAOAAAAAAAOAAIAAAAOAAQAAAAOAAYAAAAOAAgAAAAQAP7/AAAQAAAAAAAQAAIAAQDx//v/AQDx//3/AQDx////AQDx/wEAAQDx/wMAAQDx/wUAAQDz//f/AQDz//n/AQDz//v/AQDz//3/AQDz////AQDz/wEAAQDz/wMAAQDz/wUAAQDz/wcAAQDz/wkAAQD1//X/AQD1//f/AQD1//n/AQD1//v/AQD1//3/AQD1////AQD1/wEAAQD1/wMAAQD1/wUAAQD1/wcAAQD1/wkAAQD1/wsAAQD3//P/AQD3//X/AQD3//f/AQD3//n/AQD3//v/AQD3//3/AQD3////AQD3/wEAAQD3/wMAAQD3/wUAAQD3/wcAAQD3/wkAAQD3/wsAAQD3/w0AAQD5//P/AQD5//X/AQD5//f/AQD5//n/AQD5//v/AQD5//3/AQD5////AQD5/wEAAQD5/wMAAQD5/wUAAQD5/wcAAQD5/wkAAQD5/wsAAQD5/w0AAQD7//H/AQD7//P/AQD7//X/AQD7//f/AQD7//n/AQD7//v/AQD7//3/AQD7////AQD7/wEAAQD7/wMAAQD7/wUAAQD7/wcAAQD7/wkAAQD7/wsAAQD7/w0AAQD7/w8AAQD9//H/AQD9//P/AQD9//X/AQD9//f/AQD9//n/AQD9//v/AQD9//3/AQD9////AQD9/wEAAQD9/wMAAQD9/wUAAQD9/wcAAQD9/wkAAQD9/wsAAQD9/w0AAQD9/w8AAQD///H/AQD///P/AQD///X/AQD///f/AQD///n/AQD///v/AQD///3/AQD/////AQD//wEAAQD//wMAAQD//wUAAQD//wcAAQD//wkAAQD//wsAAQD//w0AAQD//w8AAQABAPH/AQABAPP/AQABAPX/AQABAPf/AQABAPn/AQABAPv/AQABAP3/AQABAP//AQABAAEAAQABAAMAAQABAAUAAQABAAcAAQABAAkAAQABAAsAAQABAA0AAQABAA8AAQADAPH/AQADAPP/AQADAPX/AQADAPf/AQADAPn/AQADAPv/AQADAP3/AQADAP//AQADAAEAAQADAAMAAQADAAUAAQADAAcAAQADAAkAAQADAAsAAQADAA0AAQADAA8AAQAFAPH/AQAFAPP/AQAFAPX/AQAFAPf/AQAFAPn/AQAFAPv/AQAFAP3/AQAFAP//AQAFAAEAAQAFAAMAAQAFAAUAAQAFAAcAAQAFAAkAAQAFAAsAAQAFAA0AAQAFAA8AAQAHAPP/AQAHAPX/AQAHAPf/AQAHAPn/AQAHAPv/AQAHAP3/AQAHAP//AQAHAAEAAQAHAAMAAQAHAAUAAQAHAAcAAQAHAAkAAQAHAAsAAQAHAA0AAQAJAPP/AQAJAPX/AQAJAPf/AQAJAPn/AQAJAPv/AQAJAP3/AQAJAP//AQAJAAEAAQAJAAMAAQAJAAUAAQAJAAcAAQAJAAkAAQAJAAsAAQAJAA0AAQALAPX/AQALAPf/AQALAPn/AQALAPv/AQALAP3/AQALAP//AQALAAEAAQALAAMAAQALAAUAAQALAAcAAQALAAkAAQALAAsAAQANAPf/AQANAPn/AQANAPv/AQANAP3/AQANAP//AQANAAEAAQANAAMAAQANAAUAAQANAAcAAQANAAkAAQAPAPv/AQAPAP3/AQAPAP//AQAPAAEAAQAPAAMAAQAPAAUAAgDw//7/AgDw/wAAAgDw/wIAAgDy//j/AgDy//r/AgDy//z/AgDy//7/AgDy/wAAAgDy/wIAAgDy/wQAAgDy/wYAAgDy/wgAAgD0//b/AgD0//j/AgD0//r/AgD0//z/AgD0//7/AgD0/wAAAgD0/wIAAgD0/wQAAgD0/wYAAgD0/wgAAgD0/woAAgD2//T/AgD2//b/AgD2//j/AgD2//r/AgD2//z/AgD2//7/AgD2/wAAAgD2/wIAAgD2/wQAAgD2/wYAAgD2/wgAAgD2/woAAgD2/wwAAgD4//L/AgD4//T/AgD4//b/AgD4//j/AgD4//r/AgD4//z/AgD4//7/AgD4/wAAAgD4/wIAAgD4/wQAAgD4/wYAAgD4/wgAAgD4/woAAgD4/wwAAgD4/w4AAgD6//L/AgD6//T/AgD6//b/AgD6//j/AgD6//r/AgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD6/wYAAgD6/wgAAgD6/woAAgD6/wwAAgD6/w4AAgD8//L/AgD8//T/AgD8//b/AgD8//j/AgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD8/wgAAgD8/woAAgD8/wwAAgD8/w4AAgD+//D/AgD+//L/AgD+//T/AgD+//b/AgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD+/woAAgD+/wwAAgD+/w4AAgD+/xAAAgAAAPD/AgAAAPL/AgAAAPT/AgAAAPb/AgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgAAAAoAAgAAAAwAAgAAAA4AAgAAABAAAgACAPD/AgACAPL/AgACAPT/AgACAPb/AgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgACAAoAAgACAAwAAgACAA4AAgACABAAAgAEAPL/AgAEAPT/AgAEAPb/AgAEAPj/AgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAEAAgAAgAEAAoAAgAEAAwAAgAEAA4AAgAGAPL/AgAGAPT/AgAGAPb/AgAGAPj/AgAGAPr/AgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAGAAYAAgAGAAgAAgAGAAoAAgAGAAwAAgAGAA4AAgAIAPL/AgAIAPT/AgAIAPb/AgAIAPj/AgAIAPr/AgAIAPz/AgAIAP7/AgAIAAAAAgAIAAIAAgAIAAQAAgAIAAYAAgAIAAgAAgAIAAoAAgAIAAwAAgAIAA4AAgAKAPT/AgAKAPb/AgAKAPj/AgAKAPr/AgAKAPz/AgAKAP7/AgAKAAAAAgAKAAIAAgAKAAQAAgAKAAYAAgAKAAgAAgAKAAoAAgAKAAwAAgAMAPb/AgAMAPj/AgAMAPr/AgAMAPz/AgAMAP7/AgAMAAAAAgAMAAIAAgAMAAQAAgAMAAYAAgAMAAgAAgAMAAoAAgAOAPj/AgAOAPr/AgAOAPz/AgAOAP7/AgAOAAAAAgAOAAIAAgAOAAQAAgAOAAYAAgAOAAgAAgAQAP7/AgAQAAAAAgAQAAIAAwDx//v/AwDx//3/AwDx////AwDx/wEAAwDx/wMAAwDx/wUAAwDz//f/AwDz//n/AwDz//v/AwDz//3/AwDz////AwDz/wEAAwDz/wMAAwDz/wUAAwDz/wcAAwDz/wkAAwD1//X/AwD1//f/AwD1//n/AwD1//v/AwD1//3/AwD1////AwD1/wEAAwD1/wMAAwD1/wUAAwD1/wcAAwD1/wkAAwD1/wsAAwD3//P/AwD3//X/AwD3//f/AwD3//n/AwD3//v/AwD3//3/AwD3////AwD3/wEAAwD3/wMAAwD3/wUAAwD3/wcAAwD3/wkAAwD3/wsAAwD3/w0AAwD5//P/AwD5//X/AwD5//f/AwD5//n/AwD5//v/AwD5//3/AwD5////AwD5/wEAAwD5/wMAAwD5/wUAAwD5/wcAAwD5/wkAAwD5/wsAAwD5/w0AAwD7//H/AwD7//P/AwD7//X/AwD7//f/AwD7//n/AwD7//v/AwD7//3/AwD7////AwD7/wEAAwD7/wMAAwD7/wUAAwD7/wcAAwD7/wkAAwD7/wsAAwD7/w0AAwD7/w8AAwD9//H/AwD9//P/AwD9//X/AwD9//f/AwD9//n/AwD9//v/AwD9//3/AwD9////AwD9/wEAAwD9/wMAAwD9/wUAAwD9/wcAAwD9/wkAAwD9/wsAAwD9/w0AAwD9/w8AAwD///H/AwD///P/AwD///X/AwD///f/AwD///n/AwD///v/AwD///3/AwD/////AwD//wEAAwD//wMAAwD//wUAAwD//wcAAwD//wkAAwD//wsAAwD//w0AAwD//w8AAwABAPH/AwABAPP/AwABAPX/AwABAPf/AwABAPn/AwABAPv/AwABAP3/AwABAP//AwABAAEAAwABAAMAAwABAAUAAwABAAcAAwABAAkAAwABAAsAAwABAA0AAwABAA8AAwADAPH/AwADAPP/AwADAPX/AwADAPf/AwADAPn/AwADAPv/AwADAP3/AwADAP//AwADAAEAAwADAAMAAwADAAUAAwADAAcAAwADAAkAAwADAAsAAwADAA0AAwADAA8AAwAFAPH/AwAFAPP/AwAFAPX/AwAFAPf/AwAFAPn/AwAFAPv/AwAFAP3/AwAFAP//AwAFAAEAAwAFAAMAAwAFAAUAAwAFAAcAAwAFAAkAAwAFAAsAAwAFAA0AAwAFAA8AAwAHAPP/AwAHAPX/AwAHAPf/AwAHAPn/AwAHAPv/AwAHAP3/AwAHAP//AwAHAAEAAwAHAAMAAwAHAAUAAwAHAAcAAwAHAAkAAwAHAAsAAwAHAA0AAwAJAPP/AwAJAPX/AwAJAPf/AwAJAPn/AwAJAPv/AwAJAP3/AwAJAP//AwAJAAEAAwAJAAMAAwAJAAUAAwAJAAcAAwAJAAkAAwAJAAsAAwAJAA0AAwALAPX/AwALAPf/AwALAPn/AwALAPv/AwALAP3/AwALAP//AwALAAEAAwALAAMAAwALAAUAAwALAAcAAwALAAkAAwALAAsAAwANAPf/AwANAPn/AwANAPv/AwANAP3/AwANAP//AwANAAEAAwANAAMAAwANAAUAAwANAAcAAwANAAkAAwAPAPv/AwAPAP3/AwAPAP//AwAPAAEAAwAPAAMAAwAPAAUABADy//r/BADy//z/BADy//7/BADy/wAABADy/wIABADy/wQABADy/wYABAD0//b/BAD0//j/BAD0//r/BAD0//z/BAD0//7/BAD0/wAABAD0/wIABAD0/wQABAD0/wYABAD0/wgABAD0/woABAD2//T/BAD2//b/BAD2//j/BAD2//r/BAD2//z/BAD2//7/BAD2/wAABAD2/wIABAD2/wQABAD2/wYABAD2/wgABAD2/woABAD2/wwABAD4//T/BAD4//b/BAD4//j/BAD4//r/BAD4//z/BAD4//7/BAD4/wAABAD4/wIABAD4/wQABAD4/wYABAD4/wgABAD4/woABAD4/wwABAD6//L/BAD6//T/BAD6//b/BAD6//j/BAD6//r/BAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD6/wYABAD6/wgABAD6/woABAD6/wwABAD6/w4ABAD8//L/BAD8//T/BAD8//b/BAD8//j/BAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD8/wgABAD8/woABAD8/wwABAD8/w4ABAD+//L/BAD+//T/BAD+//b/BAD+//j/BAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD+/wgABAD+/woABAD+/wwABAD+/w4ABAAAAPL/BAAAAPT/BAAAAPb/BAAAAPj/BAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAAAAAgABAAAAAoABAAAAAwABAAAAA4ABAACAPL/BAACAPT/BAACAPb/BAACAPj/BAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAACAAgABAACAAoABAACAAwABAACAA4ABAAEAPL/BAAEAPT/BAAEAPb/BAAEAPj/BAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAEAAgABAAEAAoABAAEAAwABAAEAA4ABAAGAPL/BAAGAPT/BAAGAPb/BAAGAPj/BAAGAPr/BAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAGAAYABAAGAAgABAAGAAoABAAGAAwABAAGAA4ABAAIAPT/BAAIAPb/BAAIAPj/BAAIAPr/BAAIAPz/BAAIAP7/BAAIAAAABAAIAAIABAAIAAQABAAIAAYABAAIAAgABAAIAAoABAAIAAwABAAKAPT/BAAKAPb/BAAKAPj/BAAKAPr/BAAKAPz/BAAKAP7/BAAKAAAABAAKAAIABAAKAAQABAAKAAYABAAKAAgABAAKAAoABAAKAAwABAAMAPb/BAAMAPj/BAAMAPr/BAAMAPz/BAAMAP7/BAAMAAAABAAMAAIABAAMAAQABAAMAAYABAAMAAgABAAMAAoABAAOAPr/BAAOAPz/BAAOAP7/BAAOAAAABAAOAAIABAAOAAQABAAOAAYABQDx//3/BQDx////BQDx/wEABQDx/wMABQDz//n/BQDz//v/BQDz//3/BQDz////BQDz/wEABQDz/wMABQDz/wUABQDz/wcABQD1//f/BQD1//n/BQD1//v/BQD1//3/BQD1////BQD1/wEABQD1/wMABQD1/wUABQD1/wcABQD1/wkABQD3//X/BQD3//f/BQD3//n/BQD3//v/BQD3//3/BQD3////BQD3/wEABQD3/wMABQD3/wUABQD3/wcABQD3/wkABQD3/wsABQD5//P/BQD5//X/BQD5//f/BQD5//n/BQD5//v/BQD5//3/BQD5////BQD5/wEABQD5/wMABQD5/wUABQD5/wcABQD5/wkABQD5/wsABQD5/w0ABQD7//P/BQD7//X/BQD7//f/BQD7//n/BQD7//v/BQD7//3/BQD7////BQD7/wEABQD7/wMABQD7/wUABQD7/wcABQD7/wkABQD7/wsABQD7/w0ABQD9//H/BQD9//P/BQD9//X/BQD9//f/BQD9//n/BQD9//v/BQD9//3/BQD9////BQD9/wEABQD9/wMABQD9/wUABQD9/wcABQD9/wkABQD9/wsABQD9/w0ABQD9/w8ABQD///H/BQD///P/BQD///X/BQD///f/BQD///n/BQD///v/BQD///3/BQD/////BQD//wEABQD//wMABQD//wUABQD//wcABQD//wkABQD//wsABQD//w0ABQD//w8ABQABAPH/BQABAPP/BQABAPX/BQABAPf/BQABAPn/BQABAPv/BQABAP3/BQABAP//BQABAAEABQABAAMABQABAAUABQABAAcABQABAAkABQABAAsABQABAA0ABQABAA8ABQADAPH/BQADAPP/BQADAPX/BQADAPf/BQADAPn/BQADAPv/BQADAP3/BQADAP//BQADAAEABQADAAMABQADAAUABQADAAcABQADAAkABQADAAsABQADAA0ABQADAA8ABQAFAPP/BQAFAPX/BQAFAPf/BQAFAPn/BQAFAPv/BQAFAP3/BQAFAP//BQAFAAEABQAFAAMABQAFAAUABQAFAAcABQAFAAkABQAFAAsABQAFAA0ABQAHAPP/BQAHAPX/BQAHAPf/BQAHAPn/BQAHAPv/BQAHAP3/BQAHAP//BQAHAAEABQAHAAMABQAHAAUABQAHAAcABQAHAAkABQAHAAsABQAHAA0ABQAJAPX/BQAJAPf/BQAJAPn/BQAJAPv/BQAJAP3/BQAJAP//BQAJAAEABQAJAAMABQAJAAUABQAJAAcABQAJAAkABQAJAAsABQALAPf/BQALAPn/BQALAPv/BQALAP3/BQALAP//BQALAAEABQALAAMABQALAAUABQALAAcABQALAAkABQANAPn/BQANAPv/BQANAP3/BQANAP//BQANAAEABQANAAMABQANAAUABQANAAcABQAPAP3/BQAPAP//BQAPAAEABQAPAAMABgDy//z/BgDy//7/BgDy/wAABgDy/wIABgDy/wQABgD0//j/BgD0//r/BgD0//z/BgD0//7/BgD0/wAABgD0/wIABgD0/wQABgD0/wYABgD0/wgABgD2//b/BgD2//j/BgD2//r/BgD2//z/BgD2//7/BgD2/wAABgD2/wIABgD2/wQABgD2/wYABgD2/wgABgD2/woABgD4//T/BgD4//b/BgD4//j/BgD4//r/BgD4//z/BgD4//7/BgD4/wAABgD4/wIABgD4/wQABgD4/wYABgD4/wgABgD4/woABgD4/wwABgD6//T/BgD6//b/BgD6//j/BgD6//r/BgD6//z/BgD6//7/BgD6/wAABgD6/wIABgD6/wQABgD6/wYABgD6/wgABgD6/woABgD6/wwABgD8//L/BgD8//T/BgD8//b/BgD8//j/BgD8//r/BgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD8/wYABgD8/wgABgD8/woABgD8/wwABgD8/w4ABgD+//L/BgD+//T/BgD+//b/BgD+//j/BgD+//r/BgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD+/wYABgD+/wgABgD+/woABgD+/wwABgD+/w4ABgAAAPL/BgAAAPT/BgAAAPb/BgAAAPj/BgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgAAAAgABgAAAAoABgAAAAwABgAAAA4ABgACAPL/BgACAPT/BgACAPb/BgACAPj/BgACAPr/BgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgACAAYABgACAAgABgACAAoABgACAAwABgACAA4ABgAEAPL/BgAEAPT/BgAEAPb/BgAEAPj/BgAEAPr/BgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAEAAYABgAEAAgABgAEAAoABgAEAAwABgAEAA4ABgAGAPT/BgAGAPb/BgAGAPj/BgAGAPr/BgAGAPz/BgAGAP7/BgAGAAAABgAGAAIABgAGAAQABgAGAAYABgAGAAgABgAGAAoABgAGAAwABgAIAPT/BgAIAPb/BgAIAPj/BgAIAPr/BgAIAPz/BgAIAP7/BgAIAAAABgAIAAIABgAIAAQABgAIAAYABgAIAAgABgAIAAoABgAIAAwABgAKAPb/BgAKAPj/BgAKAPr/BgAKAPz/BgAKAP7/BgAKAAAABgAKAAIABgAKAAQABgAKAAYABgAKAAgABgAKAAoABgAMAPj/BgAMAPr/BgAMAPz/BgAMAP7/BgAMAAAABgAMAAIABgAMAAQABgAMAAYABgAMAAgABgAOAPz/BgAOAP7/BgAOAAAABgAOAAIABgAOAAQABwDz//v/BwDz//3/BwDz////BwDz/wEABwDz/wMABwDz/wUABwD1//f/BwD1//n/BwD1//v/BwD1//3/BwD1////BwD1/wEABwD1/wMABwD1/wUABwD1/wcABwD1/wkABwD3//X/BwD3//f/BwD3//n/BwD3//v/BwD3//3/BwD3////BwD3/wEABwD3/wMABwD3/wUABwD3/wcABwD3/wkABwD3/wsABwD5//X/BwD5//f/BwD5//n/BwD5//v/BwD5//3/BwD5////BwD5/wEABwD5/wMABwD5/wUABwD5/wcABwD5/wkABwD5/wsABwD7//P/BwD7//X/BwD7//f/BwD7//n/BwD7//v/BwD7//3/BwD7////BwD7/wEABwD7/wMABwD7/wUABwD7/wcABwD7/wkABwD7/wsABwD7/w0ABwD9//P/BwD9//X/BwD9//f/BwD9//n/BwD9//v/BwD9//3/BwD9////BwD9/wEABwD9/wMABwD9/wUABwD9/wcABwD9/wkABwD9/wsABwD9/w0ABwD///P/BwD///X/BwD///f/BwD///n/BwD///v/BwD///3/BwD/////BwD//wEABwD//wMABwD//wUABwD//wcABwD//wkABwD//wsABwD//w0ABwABAPP/BwABAPX/BwABAPf/BwABAPn/BwABAPv/BwABAP3/BwABAP//BwABAAEABwABAAMABwABAAUABwABAAcABwABAAkABwABAAsABwABAA0ABwADAPP/BwADAPX/BwADAPf/BwADAPn/BwADAPv/BwADAP3/BwADAP//BwADAAEABwADAAMABwADAAUABwADAAcABwADAAkABwADAAsABwADAA0ABwAFAPP/BwAFAPX/BwAFAPf/BwAFAPn/BwAFAPv/BwAFAP3/BwAFAP//BwAFAAEABwAFAAMABwAFAAUABwAFAAcABwAFAAkABwAFAAsABwAFAA0ABwAHAPX/BwAHAPf/BwAHAPn/BwAHAPv/BwAHAP3/BwAHAP//BwAHAAEABwAHAAMABwAHAAUABwAHAAcABwAHAAkABwAHAAsABwAJAPX/BwAJAPf/BwAJAPn/BwAJAPv/BwAJAP3/BwAJAP//BwAJAAEABwAJAAMABwAJAAUABwAJAAcABwAJAAkABwAJAAsABwALAPf/BwALAPn/BwALAPv/BwALAP3/BwALAP//BwALAAEABwALAAMABwALAAUABwALAAcABwALAAkABwANAPv/BwANAP3/BwANAP//BwANAAEABwANAAMABwANAAUACADy//7/CADy/wAACADy/wIACAD0//r/CAD0//z/CAD0//7/CAD0/wAACAD0/wIACAD0/wQACAD0/wYACAD2//b/CAD2//j/CAD2//r/CAD2//z/CAD2//7/CAD2/wAACAD2/wIACAD2/wQACAD2/wYACAD2/wgACAD2/woACAD4//b/CAD4//j/CAD4//r/CAD4//z/CAD4//7/CAD4/wAACAD4/wIACAD4/wQACAD4/wYACAD4/wgACAD4/woACAD6//T/CAD6//b/CAD6//j/CAD6//r/CAD6//z/CAD6//7/CAD6/wAACAD6/wIACAD6/wQACAD6/wYACAD6/wgACAD6/woACAD6/wwACAD8//T/CAD8//b/CAD8//j/CAD8//r/CAD8//z/CAD8//7/CAD8/wAACAD8/wIACAD8/wQACAD8/wYACAD8/wgACAD8/woACAD8/wwACAD+//L/CAD+//T/CAD+//b/CAD+//j/CAD+//r/CAD+//z/CAD+//7/CAD+/wAACAD+/wIACAD+/wQACAD+/wYACAD+/wgACAD+/woACAD+/wwACAD+/w4ACAAAAPL/CAAAAPT/CAAAAPb/CAAAAPj/CAAAAPr/CAAAAPz/CAAAAP7/CAAAAAAACAAAAAIACAAAAAQACAAAAAYACAAAAAgACAAAAAoACAAAAAwACAAAAA4ACAACAPL/CAACAPT/CAACAPb/CAACAPj/CAACAPr/CAACAPz/CAACAP7/CAACAAAACAACAAIACAACAAQACAACAAYACAACAAgACAACAAoACAACAAwACAACAA4ACAAEAPT/CAAEAPb/CAAEAPj/CAAEAPr/CAAEAPz/CAAEAP7/CAAEAAAACAAEAAIACAAEAAQACAAEAAYACAAEAAgACAAEAAoACAAEAAwACAAGAPT/CAAGAPb/CAAGAPj/CAAGAPr/CAAGAPz/CAAGAP7/CAAGAAAACAAGAAIACAAGAAQACAAGAAYACAAGAAgACAAGAAoACAAGAAwACAAIAPb/CAAIAPj/CAAIAPr/CAAIAPz/CAAIAP7/CAAIAAAACAAIAAIACAAIAAQACAAIAAYACAAIAAgACAAIAAoACAAKAPb/CAAKAPj/CAAKAPr/CAAKAPz/CAAKAP7/CAAKAAAACAAKAAIACAAKAAQACAAKAAYACAAKAAgACAAKAAoACAAMAPr/CAAMAPz/CAAMAP7/CAAMAAAACAAMAAIACAAMAAQACAAMAAYACAAOAP7/CAAOAAAACAAOAAIACQDz//3/CQDz////CQDz/wEACQDz/wMACQD1//n/CQD1//v/CQD1//3/CQD1////CQD1/wEACQD1/wMACQD1/wUACQD1/wcACQD3//f/CQD3//n/CQD3//v/CQD3//3/CQD3////CQD3/wEACQD3/wMACQD3/wUACQD3/wcACQD3/wkACQD5//X/CQD5//f/CQD5//n/CQD5//v/CQD5//3/CQD5////CQD5/wEACQD5/wMACQD5/wUACQD5/wcACQD5/wkACQD5/wsACQD7//X/CQD7//f/CQD7//n/CQD7//v/CQD7//3/CQD7////CQD7/wEACQD7/wMACQD7/wUACQD7/wcACQD7/wkACQD7/wsACQD9//P/CQD9//X/CQD9//f/CQD9//n/CQD9//v/CQD9//3/CQD9////CQD9/wEACQD9/wMACQD9/wUACQD9/wcACQD9/wkACQD9/wsACQD9/w0ACQD///P/CQD///X/CQD///f/CQD///n/CQD///v/CQD///3/CQD/////CQD//wEACQD//wMACQD//wUACQD//wcACQD//wkACQD//wsACQD//w0ACQABAPP/CQABAPX/CQABAPf/CQABAPn/CQABAPv/CQABAP3/CQABAP//CQABAAEACQABAAMACQABAAUACQABAAcACQABAAkACQABAAsACQABAA0ACQADAPP/CQADAPX/CQADAPf/CQADAPn/CQADAPv/CQADAP3/CQADAP//CQADAAEACQADAAMACQADAAUACQADAAcACQADAAkACQADAAsACQADAA0ACQAFAPX/CQAFAPf/CQAFAPn/CQAFAPv/CQAFAP3/CQAFAP//CQAFAAEACQAFAAMACQAFAAUACQAFAAcACQAFAAkACQAFAAsACQAHAPX/CQAHAPf/CQAHAPn/CQAHAPv/CQAHAP3/CQAHAP//CQAHAAEACQAHAAMACQAHAAUACQAHAAcACQAHAAkACQAHAAsACQAJAPf/CQAJAPn/CQAJAPv/CQAJAP3/CQAJAP//CQAJAAEACQAJAAMACQAJAAUACQAJAAcACQAJAAkACQALAPn/CQALAPv/CQALAP3/CQALAP//CQALAAEACQALAAMACQALAAUACQALAAcACQANAP3/CQANAP//CQANAAEACQANAAMACgD0//z/CgD0//7/CgD0/wAACgD0/wIACgD0/wQACgD2//j/CgD2//r/CgD2//z/CgD2//7/CgD2/wAACgD2/wIACgD2/wQACgD2/wYACgD2/wgACgD4//b/CgD4//j/CgD4//r/CgD4//z/CgD4//7/CgD4/wAACgD4/wIACgD4/wQACgD4/wYACgD4/wgACgD4/woACgD6//b/CgD6//j/CgD6//r/CgD6//z/CgD6//7/CgD6/wAACgD6/wIACgD6/wQACgD6/wYACgD6/wgACgD6/woACgD8//T/CgD8//b/CgD8//j/CgD8//r/CgD8//z/CgD8//7/CgD8/wAACgD8/wIACgD8/wQACgD8/wYACgD8/wgACgD8/woACgD8/wwACgD+//T/CgD+//b/CgD+//j/CgD+//r/CgD+//z/CgD+//7/CgD+/wAACgD+/wIACgD+/wQACgD+/wYACgD+/wgACgD+/woACgD+/wwACgAAAPT/CgAAAPb/CgAAAPj/CgAAAPr/CgAAAPz/CgAAAP7/CgAAAAAACgAAAAIACgAAAAQACgAAAAYACgAAAAgACgAAAAoACgAAAAwACgACAPT/CgACAPb/CgACAPj/CgACAPr/CgACAPz/CgACAP7/CgACAAAACgACAAIACgACAAQACgACAAYACgACAAgACgACAAoACgACAAwACgAEAPT/CgAEAPb/CgAEAPj/CgAEAPr/CgAEAPz/CgAEAP7/CgAEAAAACgAEAAIACgAEAAQACgAEAAYACgAEAAgACgAEAAoACgAEAAwACgAGAPb/CgAGAPj/CgAGAPr/CgAGAPz/CgAGAP7/CgAGAAAACgAGAAIACgAGAAQACgAGAAYACgAGAAgACgAGAAoACgAIAPb/CgAIAPj/CgAIAPr/CgAIAPz/CgAIAP7/CgAIAAAACgAIAAIACgAIAAQACgAIAAYACgAIAAgACgAIAAoACgAKAPj/CgAKAPr/CgAKAPz/CgAKAP7/CgAKAAAACgAKAAIACgAKAAQACgAKAAYACgAKAAgACgAMAPz/CgAMAP7/CgAMAAAACgAMAAIACgAMAAQACwD1//3/CwD1////CwD1/wEACwD1/wMACwD3//n/CwD3//v/CwD3//3/CwD3////CwD3/wEACwD3/wMACwD3/wUACwD3/wcACwD5//f/CwD5//n/CwD5//v/CwD5//3/CwD5////CwD5/wEACwD5/wMACwD5/wUACwD5/wcACwD5/wkACwD7//f/CwD7//n/CwD7//v/CwD7//3/CwD7////CwD7/wEACwD7/wMACwD7/wUACwD7/wcACwD7/wkACwD9//X/CwD9//f/CwD9//n/CwD9//v/CwD9//3/CwD9////CwD9/wEACwD9/wMACwD9/wUACwD9/wcACwD9/wkACwD9/wsACwD///X/CwD///f/CwD///n/CwD///v/CwD///3/CwD/////CwD//wEACwD//wMACwD//wUACwD//wcACwD//wkACwD//wsACwABAPX/CwABAPf/CwABAPn/CwABAPv/CwABAP3/CwABAP//CwABAAEACwABAAMACwABAAUACwABAAcACwABAAkACwABAAsACwADAPX/CwADAPf/CwADAPn/CwADAPv/CwADAP3/CwADAP//CwADAAEACwADAAMACwADAAUACwADAAcACwADAAkACwADAAsACwAFAPf/CwAFAPn/CwAFAPv/CwAFAP3/CwAFAP//CwAFAAEACwAFAAMACwAFAAUACwAFAAcACwAFAAkACwAHAPf/CwAHAPn/CwAHAPv/CwAHAP3/CwAHAP//CwAHAAEACwAHAAMACwAHAAUACwAHAAcACwAHAAkACwAJAPn/CwAJAPv/CwAJAP3/CwAJAP//CwAJAAEACwAJAAMACwAJAAUACwAJAAcACwALAP3/CwALAP//CwALAAEACwALAAMADAD2//z/DAD2//7/DAD2/wAADAD2/wIADAD2/wQADAD4//r/DAD4//z/DAD4//7/DAD4/wAADAD4/wIADAD4/wQADAD4/wYADAD6//j/DAD6//r/DAD6//z/DAD6//7/DAD6/wAADAD6/wIADAD6/wQADAD6/wYADAD6/wgADAD8//b/DAD8//j/DAD8//r/DAD8//z/DAD8//7/DAD8/wAADAD8/wIADAD8/wQADAD8/wYADAD8/wgADAD8/woADAD+//b/DAD+//j/DAD+//r/DAD+//z/DAD+//7/DAD+/wAADAD+/wIADAD+/wQADAD+/wYADAD+/wgADAD+/woADAAAAPb/DAAAAPj/DAAAAPr/DAAAAPz/DAAAAP7/DAAAAAAADAAAAAIADAAAAAQADAAAAAYADAAAAAgADAAAAAoADAACAPb/DAACAPj/DAACAPr/DAACAPz/DAACAP7/DAACAAAADAACAAIADAACAAQADAACAAYADAACAAgADAACAAoADAAEAPb/DAAEAPj/DAAEAPr/DAAEAPz/DAAEAP7/DAAEAAAADAAEAAIADAAEAAQADAAEAAYADAAEAAgADAAEAAoADAAGAPj/DAAGAPr/DAAGAPz/DAAGAP7/DAAGAAAADAAGAAIADAAGAAQADAAGAAYADAAGAAgADAAIAPr/DAAIAPz/DAAIAP7/DAAIAAAADAAIAAIADAAIAAQADAAIAAYADAAKAPz/DAAKAP7/DAAKAAAADAAKAAIADAAKAAQADQD3//3/DQD3////DQD3/wEADQD3/wMADQD5//v/DQD5//3/DQD5////DQD5/wEADQD5/wMADQD5/wUADQD7//n/DQD7//v/DQD7//3/DQD7////DQD7/wEADQD7/wMADQD7/wUADQD7/wcADQD9//f/DQD9//n/DQD9//v/DQD9//3/DQD9////DQD9/wEADQD9/wMADQD9/wUADQD9/wcADQD9/wkADQD///f/DQD///n/DQD///v/DQD///3/DQD/////DQD//wEADQD//wMADQD//wUADQD//wcADQD//wkADQABAPf/DQABAPn/DQABAPv/DQABAP3/DQABAP//DQABAAEADQABAAMADQABAAUADQABAAcADQABAAkADQADAPf/DQADAPn/DQADAPv/DQADAP3/DQADAP//DQADAAEADQADAAMADQADAAUADQADAAcADQADAAkADQAFAPn/DQAFAPv/DQAFAP3/DQAFAP//DQAFAAEADQAFAAMADQAFAAUADQAFAAcADQAHAPv/DQAHAP3/DQAHAP//DQAHAAEADQAHAAMADQAHAAUADQAJAP3/DQAJAP//DQAJAAEADQAJAAMADgD4//7/DgD4/wAADgD4/wIADgD6//z/DgD6//7/DgD6/wAADgD6/wIADgD6/wQADgD8//r/DgD8//z/DgD8//7/DgD8/wAADgD8/wIADgD8/wQADgD8/wYADgD+//j/DgD+//r/DgD+//z/DgD+//7/DgD+/wAADgD+/wIADgD+/wQADgD+/wYADgD+/wgADgAAAPj/DgAAAPr/DgAAAPz/DgAAAP7/DgAAAAAADgAAAAIADgAAAAQADgAAAAYADgAAAAgADgACAPj/DgACAPr/DgACAPz/DgACAP7/DgACAAAADgACAAIADgACAAQADgACAAYADgACAAgADgAEAPr/DgAEAPz/DgAEAP7/DgAEAAAADgAEAAIADgAEAAQADgAEAAYADgAGAPz/DgAGAP7/DgAGAAAADgAGAAIADgAGAAQADgAIAP7/DgAIAAAADgAIAAIADwD7//3/DwD7////DwD7/wEADwD7/wMADwD9//v/DwD9//3/DwD9////DwD9/wEADwD9/wMADwD9/wUADwD///v/DwD///3/DwD/////DwD//wEADwD//wMADwD//wUADwABAPv/DwABAP3/DwABAP//DwABAAEADwABAAMADwABAAUADwADAPv/DwADAP3/DwADAP//DwADAAEADwADAAMADwADAAUADwAFAP3/DwAFAP//DwAFAAEADwAFAAMAEAD+//7/EAD+/wAAEAD+/wIAEAAAAP7/EAAAAAAAEAAAAAIAEAACAP7/EAACAAAAEAACAAIA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.004360592167337278,\"absorptive\":true,\"n_reflections\":4572,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", - "GaAs (zincblende)": "{\"name\":\"GaAs\",\"spacegroup\":\"F-43m (216)\",\"pointgroup\":\"-43m\",\"cell\":[[5.653,0.0,0.0],[0.0,5.653,0.0],[0.0,0.0,5.653]],\"recip\":[[0.17689722271360342,0.0,0.0],[0.0,0.17689722271360342,0.0],[0.0,0.0,0.17689722271360342]],\"positions_frac\":[[0.0,0.0,0.0],[0.25,0.25,0.25],[0.0,0.5,0.5],[0.25,0.7500000000000001,0.7500000000000001],[0.5,0.0,0.5],[0.7500000000000001,0.25,0.7500000000000001],[0.5,0.5,0.0],[0.7500000000000001,0.7500000000000001,0.25]],\"numbers\":[31,33,31,33,31,33,31,33],\"symbols\":[\"Ga\",\"As\",\"Ga\",\"As\",\"Ga\",\"As\",\"Ga\",\"As\"],\"colors\":[[0.761,0.561,0.561],[0.741,0.502,0.89],[0.761,0.561,0.561],[0.741,0.502,0.89],[0.761,0.561,0.561],[0.741,0.502,0.89],[0.761,0.561,0.561],[0.741,0.502,0.89]],\"radii\":[1.22,1.19,1.22,1.19,1.22,1.19,1.22,1.19],\"hkl_i16\":\"8P/8//7/8P/8/wAA8P/8/wIA8P/+//z/8P/+//7/8P/+/wAA8P/+/wIA8P/+/wQA8P8AAPz/8P8AAP7/8P8AAAAA8P8AAAIA8P8AAAQA8P8CAPz/8P8CAP7/8P8CAAAA8P8CAAIA8P8CAAQA8P8EAP7/8P8EAAAA8P8EAAIA8f/5//3/8f/5////8f/5/wEA8f/5/wMA8f/7//v/8f/7//3/8f/7////8f/7/wEA8f/7/wMA8f/7/wUA8f/9//n/8f/9//v/8f/9//3/8f/9////8f/9/wEA8f/9/wMA8f/9/wUA8f/9/wcA8f////n/8f////v/8f////3/8f//////8f///wEA8f///wMA8f///wUA8f///wcA8f8BAPn/8f8BAPv/8f8BAP3/8f8BAP//8f8BAAEA8f8BAAMA8f8BAAUA8f8BAAcA8f8DAPn/8f8DAPv/8f8DAP3/8f8DAP//8f8DAAEA8f8DAAMA8f8DAAUA8f8DAAcA8f8FAPv/8f8FAP3/8f8FAP//8f8FAAEA8f8FAAMA8f8FAAUA8f8HAP3/8f8HAP//8f8HAAEA8f8HAAMA8v/4//z/8v/4//7/8v/4/wAA8v/4/wIA8v/4/wQA8v/6//r/8v/6//z/8v/6//7/8v/6/wAA8v/6/wIA8v/6/wQA8v/6/wYA8v/8//j/8v/8//r/8v/8//z/8v/8//7/8v/8/wAA8v/8/wIA8v/8/wQA8v/8/wYA8v/8/wgA8v/+//j/8v/+//r/8v/+//z/8v/+//7/8v/+/wAA8v/+/wIA8v/+/wQA8v/+/wYA8v/+/wgA8v8AAPj/8v8AAPr/8v8AAPz/8v8AAP7/8v8AAAAA8v8AAAIA8v8AAAQA8v8AAAYA8v8AAAgA8v8CAPj/8v8CAPr/8v8CAPz/8v8CAP7/8v8CAAAA8v8CAAIA8v8CAAQA8v8CAAYA8v8CAAgA8v8EAPj/8v8EAPr/8v8EAPz/8v8EAP7/8v8EAAAA8v8EAAIA8v8EAAQA8v8EAAYA8v8EAAgA8v8GAPr/8v8GAPz/8v8GAP7/8v8GAAAA8v8GAAIA8v8GAAQA8v8GAAYA8v8IAPz/8v8IAP7/8v8IAAAA8v8IAAIA8v8IAAQA8//3//v/8//3//3/8//3////8//3/wEA8//3/wMA8//3/wUA8//5//n/8//5//v/8//5//3/8//5////8//5/wEA8//5/wMA8//5/wUA8//5/wcA8//7//f/8//7//n/8//7//v/8//7//3/8//7////8//7/wEA8//7/wMA8//7/wUA8//7/wcA8//7/wkA8//9//f/8//9//n/8//9//v/8//9//3/8//9////8//9/wEA8//9/wMA8//9/wUA8//9/wcA8//9/wkA8/////f/8/////n/8/////v/8/////3/8///////8////wEA8////wMA8////wUA8////wcA8////wkA8/8BAPf/8/8BAPn/8/8BAPv/8/8BAP3/8/8BAP//8/8BAAEA8/8BAAMA8/8BAAUA8/8BAAcA8/8BAAkA8/8DAPf/8/8DAPn/8/8DAPv/8/8DAP3/8/8DAP//8/8DAAEA8/8DAAMA8/8DAAUA8/8DAAcA8/8DAAkA8/8FAPf/8/8FAPn/8/8FAPv/8/8FAP3/8/8FAP//8/8FAAEA8/8FAAMA8/8FAAUA8/8FAAcA8/8FAAkA8/8HAPn/8/8HAPv/8/8HAP3/8/8HAP//8/8HAAEA8/8HAAMA8/8HAAUA8/8HAAcA8/8JAPv/8/8JAP3/8/8JAP//8/8JAAEA8/8JAAMA8/8JAAUA9P/2//r/9P/2//z/9P/2//7/9P/2/wAA9P/2/wIA9P/2/wQA9P/2/wYA9P/4//j/9P/4//r/9P/4//z/9P/4//7/9P/4/wAA9P/4/wIA9P/4/wQA9P/4/wYA9P/4/wgA9P/6//b/9P/6//j/9P/6//r/9P/6//z/9P/6//7/9P/6/wAA9P/6/wIA9P/6/wQA9P/6/wYA9P/6/wgA9P/6/woA9P/8//b/9P/8//j/9P/8//r/9P/8//z/9P/8//7/9P/8/wAA9P/8/wIA9P/8/wQA9P/8/wYA9P/8/wgA9P/8/woA9P/+//b/9P/+//j/9P/+//r/9P/+//z/9P/+//7/9P/+/wAA9P/+/wIA9P/+/wQA9P/+/wYA9P/+/wgA9P/+/woA9P8AAPb/9P8AAPj/9P8AAPr/9P8AAPz/9P8AAP7/9P8AAAAA9P8AAAIA9P8AAAQA9P8AAAYA9P8AAAgA9P8AAAoA9P8CAPb/9P8CAPj/9P8CAPr/9P8CAPz/9P8CAP7/9P8CAAAA9P8CAAIA9P8CAAQA9P8CAAYA9P8CAAgA9P8CAAoA9P8EAPb/9P8EAPj/9P8EAPr/9P8EAPz/9P8EAP7/9P8EAAAA9P8EAAIA9P8EAAQA9P8EAAYA9P8EAAgA9P8EAAoA9P8GAPb/9P8GAPj/9P8GAPr/9P8GAPz/9P8GAP7/9P8GAAAA9P8GAAIA9P8GAAQA9P8GAAYA9P8GAAgA9P8GAAoA9P8IAPj/9P8IAPr/9P8IAPz/9P8IAP7/9P8IAAAA9P8IAAIA9P8IAAQA9P8IAAYA9P8IAAgA9P8KAPr/9P8KAPz/9P8KAP7/9P8KAAAA9P8KAAIA9P8KAAQA9P8KAAYA9f/1//v/9f/1//3/9f/1////9f/1/wEA9f/1/wMA9f/1/wUA9f/3//f/9f/3//n/9f/3//v/9f/3//3/9f/3////9f/3/wEA9f/3/wMA9f/3/wUA9f/3/wcA9f/3/wkA9f/5//f/9f/5//n/9f/5//v/9f/5//3/9f/5////9f/5/wEA9f/5/wMA9f/5/wUA9f/5/wcA9f/5/wkA9f/7//X/9f/7//f/9f/7//n/9f/7//v/9f/7//3/9f/7////9f/7/wEA9f/7/wMA9f/7/wUA9f/7/wcA9f/7/wkA9f/7/wsA9f/9//X/9f/9//f/9f/9//n/9f/9//v/9f/9//3/9f/9////9f/9/wEA9f/9/wMA9f/9/wUA9f/9/wcA9f/9/wkA9f/9/wsA9f////X/9f////f/9f////n/9f////v/9f////3/9f//////9f///wEA9f///wMA9f///wUA9f///wcA9f///wkA9f///wsA9f8BAPX/9f8BAPf/9f8BAPn/9f8BAPv/9f8BAP3/9f8BAP//9f8BAAEA9f8BAAMA9f8BAAUA9f8BAAcA9f8BAAkA9f8BAAsA9f8DAPX/9f8DAPf/9f8DAPn/9f8DAPv/9f8DAP3/9f8DAP//9f8DAAEA9f8DAAMA9f8DAAUA9f8DAAcA9f8DAAkA9f8DAAsA9f8FAPX/9f8FAPf/9f8FAPn/9f8FAPv/9f8FAP3/9f8FAP//9f8FAAEA9f8FAAMA9f8FAAUA9f8FAAcA9f8FAAkA9f8FAAsA9f8HAPf/9f8HAPn/9f8HAPv/9f8HAP3/9f8HAP//9f8HAAEA9f8HAAMA9f8HAAUA9f8HAAcA9f8HAAkA9f8JAPf/9f8JAPn/9f8JAPv/9f8JAP3/9f8JAP//9f8JAAEA9f8JAAMA9f8JAAUA9f8JAAcA9f8JAAkA9f8LAPv/9f8LAP3/9f8LAP//9f8LAAEA9f8LAAMA9f8LAAUA9v/0//r/9v/0//z/9v/0//7/9v/0/wAA9v/0/wIA9v/0/wQA9v/0/wYA9v/2//j/9v/2//r/9v/2//z/9v/2//7/9v/2/wAA9v/2/wIA9v/2/wQA9v/2/wYA9v/2/wgA9v/4//b/9v/4//j/9v/4//r/9v/4//z/9v/4//7/9v/4/wAA9v/4/wIA9v/4/wQA9v/4/wYA9v/4/wgA9v/4/woA9v/6//T/9v/6//b/9v/6//j/9v/6//r/9v/6//z/9v/6//7/9v/6/wAA9v/6/wIA9v/6/wQA9v/6/wYA9v/6/wgA9v/6/woA9v/6/wwA9v/8//T/9v/8//b/9v/8//j/9v/8//r/9v/8//z/9v/8//7/9v/8/wAA9v/8/wIA9v/8/wQA9v/8/wYA9v/8/wgA9v/8/woA9v/8/wwA9v/+//T/9v/+//b/9v/+//j/9v/+//r/9v/+//z/9v/+//7/9v/+/wAA9v/+/wIA9v/+/wQA9v/+/wYA9v/+/wgA9v/+/woA9v/+/wwA9v8AAPT/9v8AAPb/9v8AAPj/9v8AAPr/9v8AAPz/9v8AAP7/9v8AAAAA9v8AAAIA9v8AAAQA9v8AAAYA9v8AAAgA9v8AAAoA9v8AAAwA9v8CAPT/9v8CAPb/9v8CAPj/9v8CAPr/9v8CAPz/9v8CAP7/9v8CAAAA9v8CAAIA9v8CAAQA9v8CAAYA9v8CAAgA9v8CAAoA9v8CAAwA9v8EAPT/9v8EAPb/9v8EAPj/9v8EAPr/9v8EAPz/9v8EAP7/9v8EAAAA9v8EAAIA9v8EAAQA9v8EAAYA9v8EAAgA9v8EAAoA9v8EAAwA9v8GAPT/9v8GAPb/9v8GAPj/9v8GAPr/9v8GAPz/9v8GAP7/9v8GAAAA9v8GAAIA9v8GAAQA9v8GAAYA9v8GAAgA9v8GAAoA9v8GAAwA9v8IAPb/9v8IAPj/9v8IAPr/9v8IAPz/9v8IAP7/9v8IAAAA9v8IAAIA9v8IAAQA9v8IAAYA9v8IAAgA9v8IAAoA9v8KAPj/9v8KAPr/9v8KAPz/9v8KAP7/9v8KAAAA9v8KAAIA9v8KAAQA9v8KAAYA9v8KAAgA9v8MAPr/9v8MAPz/9v8MAP7/9v8MAAAA9v8MAAIA9v8MAAQA9v8MAAYA9//z//v/9//z//3/9//z////9//z/wEA9//z/wMA9//z/wUA9//1//f/9//1//n/9//1//v/9//1//3/9//1////9//1/wEA9//1/wMA9//1/wUA9//1/wcA9//1/wkA9//3//X/9//3//f/9//3//n/9//3//v/9//3//3/9//3////9//3/wEA9//3/wMA9//3/wUA9//3/wcA9//3/wkA9//3/wsA9//5//X/9//5//f/9//5//n/9//5//v/9//5//3/9//5////9//5/wEA9//5/wMA9//5/wUA9//5/wcA9//5/wkA9//5/wsA9//7//P/9//7//X/9//7//f/9//7//n/9//7//v/9//7//3/9//7////9//7/wEA9//7/wMA9//7/wUA9//7/wcA9//7/wkA9//7/wsA9//7/w0A9//9//P/9//9//X/9//9//f/9//9//n/9//9//v/9//9//3/9//9////9//9/wEA9//9/wMA9//9/wUA9//9/wcA9//9/wkA9//9/wsA9//9/w0A9/////P/9/////X/9/////f/9/////n/9/////v/9/////3/9///////9////wEA9////wMA9////wUA9////wcA9////wkA9////wsA9////w0A9/8BAPP/9/8BAPX/9/8BAPf/9/8BAPn/9/8BAPv/9/8BAP3/9/8BAP//9/8BAAEA9/8BAAMA9/8BAAUA9/8BAAcA9/8BAAkA9/8BAAsA9/8BAA0A9/8DAPP/9/8DAPX/9/8DAPf/9/8DAPn/9/8DAPv/9/8DAP3/9/8DAP//9/8DAAEA9/8DAAMA9/8DAAUA9/8DAAcA9/8DAAkA9/8DAAsA9/8DAA0A9/8FAPP/9/8FAPX/9/8FAPf/9/8FAPn/9/8FAPv/9/8FAP3/9/8FAP//9/8FAAEA9/8FAAMA9/8FAAUA9/8FAAcA9/8FAAkA9/8FAAsA9/8FAA0A9/8HAPX/9/8HAPf/9/8HAPn/9/8HAPv/9/8HAP3/9/8HAP//9/8HAAEA9/8HAAMA9/8HAAUA9/8HAAcA9/8HAAkA9/8HAAsA9/8JAPX/9/8JAPf/9/8JAPn/9/8JAPv/9/8JAP3/9/8JAP//9/8JAAEA9/8JAAMA9/8JAAUA9/8JAAcA9/8JAAkA9/8JAAsA9/8LAPf/9/8LAPn/9/8LAPv/9/8LAP3/9/8LAP//9/8LAAEA9/8LAAMA9/8LAAUA9/8LAAcA9/8LAAkA9/8NAPv/9/8NAP3/9/8NAP//9/8NAAEA9/8NAAMA9/8NAAUA+P/y//z/+P/y//7/+P/y/wAA+P/y/wIA+P/y/wQA+P/0//j/+P/0//r/+P/0//z/+P/0//7/+P/0/wAA+P/0/wIA+P/0/wQA+P/0/wYA+P/0/wgA+P/2//b/+P/2//j/+P/2//r/+P/2//z/+P/2//7/+P/2/wAA+P/2/wIA+P/2/wQA+P/2/wYA+P/2/wgA+P/2/woA+P/4//T/+P/4//b/+P/4//j/+P/4//r/+P/4//z/+P/4//7/+P/4/wAA+P/4/wIA+P/4/wQA+P/4/wYA+P/4/wgA+P/4/woA+P/4/wwA+P/6//T/+P/6//b/+P/6//j/+P/6//r/+P/6//z/+P/6//7/+P/6/wAA+P/6/wIA+P/6/wQA+P/6/wYA+P/6/wgA+P/6/woA+P/6/wwA+P/8//L/+P/8//T/+P/8//b/+P/8//j/+P/8//r/+P/8//z/+P/8//7/+P/8/wAA+P/8/wIA+P/8/wQA+P/8/wYA+P/8/wgA+P/8/woA+P/8/wwA+P/8/w4A+P/+//L/+P/+//T/+P/+//b/+P/+//j/+P/+//r/+P/+//z/+P/+//7/+P/+/wAA+P/+/wIA+P/+/wQA+P/+/wYA+P/+/wgA+P/+/woA+P/+/wwA+P/+/w4A+P8AAPL/+P8AAPT/+P8AAPb/+P8AAPj/+P8AAPr/+P8AAPz/+P8AAP7/+P8AAAAA+P8AAAIA+P8AAAQA+P8AAAYA+P8AAAgA+P8AAAoA+P8AAAwA+P8AAA4A+P8CAPL/+P8CAPT/+P8CAPb/+P8CAPj/+P8CAPr/+P8CAPz/+P8CAP7/+P8CAAAA+P8CAAIA+P8CAAQA+P8CAAYA+P8CAAgA+P8CAAoA+P8CAAwA+P8CAA4A+P8EAPL/+P8EAPT/+P8EAPb/+P8EAPj/+P8EAPr/+P8EAPz/+P8EAP7/+P8EAAAA+P8EAAIA+P8EAAQA+P8EAAYA+P8EAAgA+P8EAAoA+P8EAAwA+P8EAA4A+P8GAPT/+P8GAPb/+P8GAPj/+P8GAPr/+P8GAPz/+P8GAP7/+P8GAAAA+P8GAAIA+P8GAAQA+P8GAAYA+P8GAAgA+P8GAAoA+P8GAAwA+P8IAPT/+P8IAPb/+P8IAPj/+P8IAPr/+P8IAPz/+P8IAP7/+P8IAAAA+P8IAAIA+P8IAAQA+P8IAAYA+P8IAAgA+P8IAAoA+P8IAAwA+P8KAPb/+P8KAPj/+P8KAPr/+P8KAPz/+P8KAP7/+P8KAAAA+P8KAAIA+P8KAAQA+P8KAAYA+P8KAAgA+P8KAAoA+P8MAPj/+P8MAPr/+P8MAPz/+P8MAP7/+P8MAAAA+P8MAAIA+P8MAAQA+P8MAAYA+P8MAAgA+P8OAPz/+P8OAP7/+P8OAAAA+P8OAAIA+P8OAAQA+f/x//3/+f/x////+f/x/wEA+f/x/wMA+f/z//n/+f/z//v/+f/z//3/+f/z////+f/z/wEA+f/z/wMA+f/z/wUA+f/z/wcA+f/1//f/+f/1//n/+f/1//v/+f/1//3/+f/1////+f/1/wEA+f/1/wMA+f/1/wUA+f/1/wcA+f/1/wkA+f/3//X/+f/3//f/+f/3//n/+f/3//v/+f/3//3/+f/3////+f/3/wEA+f/3/wMA+f/3/wUA+f/3/wcA+f/3/wkA+f/3/wsA+f/5//P/+f/5//X/+f/5//f/+f/5//n/+f/5//v/+f/5//3/+f/5////+f/5/wEA+f/5/wMA+f/5/wUA+f/5/wcA+f/5/wkA+f/5/wsA+f/5/w0A+f/7//P/+f/7//X/+f/7//f/+f/7//n/+f/7//v/+f/7//3/+f/7////+f/7/wEA+f/7/wMA+f/7/wUA+f/7/wcA+f/7/wkA+f/7/wsA+f/7/w0A+f/9//H/+f/9//P/+f/9//X/+f/9//f/+f/9//n/+f/9//v/+f/9//3/+f/9////+f/9/wEA+f/9/wMA+f/9/wUA+f/9/wcA+f/9/wkA+f/9/wsA+f/9/w0A+f/9/w8A+f////H/+f////P/+f////X/+f////f/+f////n/+f////v/+f////3/+f//////+f///wEA+f///wMA+f///wUA+f///wcA+f///wkA+f///wsA+f///w0A+f///w8A+f8BAPH/+f8BAPP/+f8BAPX/+f8BAPf/+f8BAPn/+f8BAPv/+f8BAP3/+f8BAP//+f8BAAEA+f8BAAMA+f8BAAUA+f8BAAcA+f8BAAkA+f8BAAsA+f8BAA0A+f8BAA8A+f8DAPH/+f8DAPP/+f8DAPX/+f8DAPf/+f8DAPn/+f8DAPv/+f8DAP3/+f8DAP//+f8DAAEA+f8DAAMA+f8DAAUA+f8DAAcA+f8DAAkA+f8DAAsA+f8DAA0A+f8DAA8A+f8FAPP/+f8FAPX/+f8FAPf/+f8FAPn/+f8FAPv/+f8FAP3/+f8FAP//+f8FAAEA+f8FAAMA+f8FAAUA+f8FAAcA+f8FAAkA+f8FAAsA+f8FAA0A+f8HAPP/+f8HAPX/+f8HAPf/+f8HAPn/+f8HAPv/+f8HAP3/+f8HAP//+f8HAAEA+f8HAAMA+f8HAAUA+f8HAAcA+f8HAAkA+f8HAAsA+f8HAA0A+f8JAPX/+f8JAPf/+f8JAPn/+f8JAPv/+f8JAP3/+f8JAP//+f8JAAEA+f8JAAMA+f8JAAUA+f8JAAcA+f8JAAkA+f8JAAsA+f8LAPf/+f8LAPn/+f8LAPv/+f8LAP3/+f8LAP//+f8LAAEA+f8LAAMA+f8LAAUA+f8LAAcA+f8LAAkA+f8NAPn/+f8NAPv/+f8NAP3/+f8NAP//+f8NAAEA+f8NAAMA+f8NAAUA+f8NAAcA+f8PAP3/+f8PAP//+f8PAAEA+f8PAAMA+v/y//r/+v/y//z/+v/y//7/+v/y/wAA+v/y/wIA+v/y/wQA+v/y/wYA+v/0//b/+v/0//j/+v/0//r/+v/0//z/+v/0//7/+v/0/wAA+v/0/wIA+v/0/wQA+v/0/wYA+v/0/wgA+v/0/woA+v/2//T/+v/2//b/+v/2//j/+v/2//r/+v/2//z/+v/2//7/+v/2/wAA+v/2/wIA+v/2/wQA+v/2/wYA+v/2/wgA+v/2/woA+v/2/wwA+v/4//T/+v/4//b/+v/4//j/+v/4//r/+v/4//z/+v/4//7/+v/4/wAA+v/4/wIA+v/4/wQA+v/4/wYA+v/4/wgA+v/4/woA+v/4/wwA+v/6//L/+v/6//T/+v/6//b/+v/6//j/+v/6//r/+v/6//z/+v/6//7/+v/6/wAA+v/6/wIA+v/6/wQA+v/6/wYA+v/6/wgA+v/6/woA+v/6/wwA+v/6/w4A+v/8//L/+v/8//T/+v/8//b/+v/8//j/+v/8//r/+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/8/wYA+v/8/wgA+v/8/woA+v/8/wwA+v/8/w4A+v/+//L/+v/+//T/+v/+//b/+v/+//j/+v/+//r/+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v/+/wYA+v/+/wgA+v/+/woA+v/+/wwA+v/+/w4A+v8AAPL/+v8AAPT/+v8AAPb/+v8AAPj/+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8AAAgA+v8AAAoA+v8AAAwA+v8AAA4A+v8CAPL/+v8CAPT/+v8CAPb/+v8CAPj/+v8CAPr/+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8CAAYA+v8CAAgA+v8CAAoA+v8CAAwA+v8CAA4A+v8EAPL/+v8EAPT/+v8EAPb/+v8EAPj/+v8EAPr/+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8EAAYA+v8EAAgA+v8EAAoA+v8EAAwA+v8EAA4A+v8GAPL/+v8GAPT/+v8GAPb/+v8GAPj/+v8GAPr/+v8GAPz/+v8GAP7/+v8GAAAA+v8GAAIA+v8GAAQA+v8GAAYA+v8GAAgA+v8GAAoA+v8GAAwA+v8GAA4A+v8IAPT/+v8IAPb/+v8IAPj/+v8IAPr/+v8IAPz/+v8IAP7/+v8IAAAA+v8IAAIA+v8IAAQA+v8IAAYA+v8IAAgA+v8IAAoA+v8IAAwA+v8KAPT/+v8KAPb/+v8KAPj/+v8KAPr/+v8KAPz/+v8KAP7/+v8KAAAA+v8KAAIA+v8KAAQA+v8KAAYA+v8KAAgA+v8KAAoA+v8KAAwA+v8MAPb/+v8MAPj/+v8MAPr/+v8MAPz/+v8MAP7/+v8MAAAA+v8MAAIA+v8MAAQA+v8MAAYA+v8MAAgA+v8MAAoA+v8OAPr/+v8OAPz/+v8OAP7/+v8OAAAA+v8OAAIA+v8OAAQA+v8OAAYA+//x//v/+//x//3/+//x////+//x/wEA+//x/wMA+//x/wUA+//z//f/+//z//n/+//z//v/+//z//3/+//z////+//z/wEA+//z/wMA+//z/wUA+//z/wcA+//z/wkA+//1//X/+//1//f/+//1//n/+//1//v/+//1//3/+//1////+//1/wEA+//1/wMA+//1/wUA+//1/wcA+//1/wkA+//1/wsA+//3//P/+//3//X/+//3//f/+//3//n/+//3//v/+//3//3/+//3////+//3/wEA+//3/wMA+//3/wUA+//3/wcA+//3/wkA+//3/wsA+//3/w0A+//5//P/+//5//X/+//5//f/+//5//n/+//5//v/+//5//3/+//5////+//5/wEA+//5/wMA+//5/wUA+//5/wcA+//5/wkA+//5/wsA+//5/w0A+//7//H/+//7//P/+//7//X/+//7//f/+//7//n/+//7//v/+//7//3/+//7////+//7/wEA+//7/wMA+//7/wUA+//7/wcA+//7/wkA+//7/wsA+//7/w0A+//7/w8A+//9//H/+//9//P/+//9//X/+//9//f/+//9//n/+//9//v/+//9//3/+//9////+//9/wEA+//9/wMA+//9/wUA+//9/wcA+//9/wkA+//9/wsA+//9/w0A+//9/w8A+/////H/+/////P/+/////X/+/////f/+/////n/+/////v/+/////3/+///////+////wEA+////wMA+////wUA+////wcA+////wkA+////wsA+////w0A+////w8A+/8BAPH/+/8BAPP/+/8BAPX/+/8BAPf/+/8BAPn/+/8BAPv/+/8BAP3/+/8BAP//+/8BAAEA+/8BAAMA+/8BAAUA+/8BAAcA+/8BAAkA+/8BAAsA+/8BAA0A+/8BAA8A+/8DAPH/+/8DAPP/+/8DAPX/+/8DAPf/+/8DAPn/+/8DAPv/+/8DAP3/+/8DAP//+/8DAAEA+/8DAAMA+/8DAAUA+/8DAAcA+/8DAAkA+/8DAAsA+/8DAA0A+/8DAA8A+/8FAPH/+/8FAPP/+/8FAPX/+/8FAPf/+/8FAPn/+/8FAPv/+/8FAP3/+/8FAP//+/8FAAEA+/8FAAMA+/8FAAUA+/8FAAcA+/8FAAkA+/8FAAsA+/8FAA0A+/8FAA8A+/8HAPP/+/8HAPX/+/8HAPf/+/8HAPn/+/8HAPv/+/8HAP3/+/8HAP//+/8HAAEA+/8HAAMA+/8HAAUA+/8HAAcA+/8HAAkA+/8HAAsA+/8HAA0A+/8JAPP/+/8JAPX/+/8JAPf/+/8JAPn/+/8JAPv/+/8JAP3/+/8JAP//+/8JAAEA+/8JAAMA+/8JAAUA+/8JAAcA+/8JAAkA+/8JAAsA+/8JAA0A+/8LAPX/+/8LAPf/+/8LAPn/+/8LAPv/+/8LAP3/+/8LAP//+/8LAAEA+/8LAAMA+/8LAAUA+/8LAAcA+/8LAAkA+/8LAAsA+/8NAPf/+/8NAPn/+/8NAPv/+/8NAP3/+/8NAP//+/8NAAEA+/8NAAMA+/8NAAUA+/8NAAcA+/8NAAkA+/8PAPv/+/8PAP3/+/8PAP//+/8PAAEA+/8PAAMA+/8PAAUA/P/w//7//P/w/wAA/P/w/wIA/P/y//j//P/y//r//P/y//z//P/y//7//P/y/wAA/P/y/wIA/P/y/wQA/P/y/wYA/P/y/wgA/P/0//b//P/0//j//P/0//r//P/0//z//P/0//7//P/0/wAA/P/0/wIA/P/0/wQA/P/0/wYA/P/0/wgA/P/0/woA/P/2//T//P/2//b//P/2//j//P/2//r//P/2//z//P/2//7//P/2/wAA/P/2/wIA/P/2/wQA/P/2/wYA/P/2/wgA/P/2/woA/P/2/wwA/P/4//L//P/4//T//P/4//b//P/4//j//P/4//r//P/4//z//P/4//7//P/4/wAA/P/4/wIA/P/4/wQA/P/4/wYA/P/4/wgA/P/4/woA/P/4/wwA/P/4/w4A/P/6//L//P/6//T//P/6//b//P/6//j//P/6//r//P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/6/wYA/P/6/wgA/P/6/woA/P/6/wwA/P/6/w4A/P/8//L//P/8//T//P/8//b//P/8//j//P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/8/wgA/P/8/woA/P/8/wwA/P/8/w4A/P/+//D//P/+//L//P/+//T//P/+//b//P/+//j//P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P/+/wgA/P/+/woA/P/+/wwA/P/+/w4A/P/+/xAA/P8AAPD//P8AAPL//P8AAPT//P8AAPb//P8AAPj//P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8AAAgA/P8AAAoA/P8AAAwA/P8AAA4A/P8AABAA/P8CAPD//P8CAPL//P8CAPT//P8CAPb//P8CAPj//P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8CAAgA/P8CAAoA/P8CAAwA/P8CAA4A/P8CABAA/P8EAPL//P8EAPT//P8EAPb//P8EAPj//P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8EAAgA/P8EAAoA/P8EAAwA/P8EAA4A/P8GAPL//P8GAPT//P8GAPb//P8GAPj//P8GAPr//P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8GAAYA/P8GAAgA/P8GAAoA/P8GAAwA/P8GAA4A/P8IAPL//P8IAPT//P8IAPb//P8IAPj//P8IAPr//P8IAPz//P8IAP7//P8IAAAA/P8IAAIA/P8IAAQA/P8IAAYA/P8IAAgA/P8IAAoA/P8IAAwA/P8IAA4A/P8KAPT//P8KAPb//P8KAPj//P8KAPr//P8KAPz//P8KAP7//P8KAAAA/P8KAAIA/P8KAAQA/P8KAAYA/P8KAAgA/P8KAAoA/P8KAAwA/P8MAPb//P8MAPj//P8MAPr//P8MAPz//P8MAP7//P8MAAAA/P8MAAIA/P8MAAQA/P8MAAYA/P8MAAgA/P8MAAoA/P8OAPj//P8OAPr//P8OAPz//P8OAP7//P8OAAAA/P8OAAIA/P8OAAQA/P8OAAYA/P8OAAgA/P8QAP7//P8QAAAA/P8QAAIA/f/x//n//f/x//v//f/x//3//f/x/////f/x/wEA/f/x/wMA/f/x/wUA/f/x/wcA/f/z//f//f/z//n//f/z//v//f/z//3//f/z/////f/z/wEA/f/z/wMA/f/z/wUA/f/z/wcA/f/z/wkA/f/1//X//f/1//f//f/1//n//f/1//v//f/1//3//f/1/////f/1/wEA/f/1/wMA/f/1/wUA/f/1/wcA/f/1/wkA/f/1/wsA/f/3//P//f/3//X//f/3//f//f/3//n//f/3//v//f/3//3//f/3/////f/3/wEA/f/3/wMA/f/3/wUA/f/3/wcA/f/3/wkA/f/3/wsA/f/3/w0A/f/5//H//f/5//P//f/5//X//f/5//f//f/5//n//f/5//v//f/5//3//f/5/////f/5/wEA/f/5/wMA/f/5/wUA/f/5/wcA/f/5/wkA/f/5/wsA/f/5/w0A/f/5/w8A/f/7//H//f/7//P//f/7//X//f/7//f//f/7//n//f/7//v//f/7//3//f/7/////f/7/wEA/f/7/wMA/f/7/wUA/f/7/wcA/f/7/wkA/f/7/wsA/f/7/w0A/f/7/w8A/f/9//H//f/9//P//f/9//X//f/9//f//f/9//n//f/9//v//f/9//3//f/9/////f/9/wEA/f/9/wMA/f/9/wUA/f/9/wcA/f/9/wkA/f/9/wsA/f/9/w0A/f/9/w8A/f////H//f////P//f////X//f////f//f////n//f////v//f////3//f///////f///wEA/f///wMA/f///wUA/f///wcA/f///wkA/f///wsA/f///w0A/f///w8A/f8BAPH//f8BAPP//f8BAPX//f8BAPf//f8BAPn//f8BAPv//f8BAP3//f8BAP///f8BAAEA/f8BAAMA/f8BAAUA/f8BAAcA/f8BAAkA/f8BAAsA/f8BAA0A/f8BAA8A/f8DAPH//f8DAPP//f8DAPX//f8DAPf//f8DAPn//f8DAPv//f8DAP3//f8DAP///f8DAAEA/f8DAAMA/f8DAAUA/f8DAAcA/f8DAAkA/f8DAAsA/f8DAA0A/f8DAA8A/f8FAPH//f8FAPP//f8FAPX//f8FAPf//f8FAPn//f8FAPv//f8FAP3//f8FAP///f8FAAEA/f8FAAMA/f8FAAUA/f8FAAcA/f8FAAkA/f8FAAsA/f8FAA0A/f8FAA8A/f8HAPH//f8HAPP//f8HAPX//f8HAPf//f8HAPn//f8HAPv//f8HAP3//f8HAP///f8HAAEA/f8HAAMA/f8HAAUA/f8HAAcA/f8HAAkA/f8HAAsA/f8HAA0A/f8HAA8A/f8JAPP//f8JAPX//f8JAPf//f8JAPn//f8JAPv//f8JAP3//f8JAP///f8JAAEA/f8JAAMA/f8JAAUA/f8JAAcA/f8JAAkA/f8JAAsA/f8JAA0A/f8LAPX//f8LAPf//f8LAPn//f8LAPv//f8LAP3//f8LAP///f8LAAEA/f8LAAMA/f8LAAUA/f8LAAcA/f8LAAkA/f8LAAsA/f8NAPf//f8NAPn//f8NAPv//f8NAP3//f8NAP///f8NAAEA/f8NAAMA/f8NAAUA/f8NAAcA/f8NAAkA/f8PAPn//f8PAPv//f8PAP3//f8PAP///f8PAAEA/f8PAAMA/f8PAAUA/f8PAAcA/v/w//z//v/w//7//v/w/wAA/v/w/wIA/v/w/wQA/v/y//j//v/y//r//v/y//z//v/y//7//v/y/wAA/v/y/wIA/v/y/wQA/v/y/wYA/v/y/wgA/v/0//b//v/0//j//v/0//r//v/0//z//v/0//7//v/0/wAA/v/0/wIA/v/0/wQA/v/0/wYA/v/0/wgA/v/0/woA/v/2//T//v/2//b//v/2//j//v/2//r//v/2//z//v/2//7//v/2/wAA/v/2/wIA/v/2/wQA/v/2/wYA/v/2/wgA/v/2/woA/v/2/wwA/v/4//L//v/4//T//v/4//b//v/4//j//v/4//r//v/4//z//v/4//7//v/4/wAA/v/4/wIA/v/4/wQA/v/4/wYA/v/4/wgA/v/4/woA/v/4/wwA/v/4/w4A/v/6//L//v/6//T//v/6//b//v/6//j//v/6//r//v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/6/wYA/v/6/wgA/v/6/woA/v/6/wwA/v/6/w4A/v/8//D//v/8//L//v/8//T//v/8//b//v/8//j//v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/8/wgA/v/8/woA/v/8/wwA/v/8/w4A/v/8/xAA/v/+//D//v/+//L//v/+//T//v/+//b//v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v/+/woA/v/+/wwA/v/+/w4A/v/+/xAA/v8AAPD//v8AAPL//v8AAPT//v8AAPb//v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8AAAoA/v8AAAwA/v8AAA4A/v8AABAA/v8CAPD//v8CAPL//v8CAPT//v8CAPb//v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8CAAoA/v8CAAwA/v8CAA4A/v8CABAA/v8EAPD//v8EAPL//v8EAPT//v8EAPb//v8EAPj//v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8EAAgA/v8EAAoA/v8EAAwA/v8EAA4A/v8EABAA/v8GAPL//v8GAPT//v8GAPb//v8GAPj//v8GAPr//v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8GAAYA/v8GAAgA/v8GAAoA/v8GAAwA/v8GAA4A/v8IAPL//v8IAPT//v8IAPb//v8IAPj//v8IAPr//v8IAPz//v8IAP7//v8IAAAA/v8IAAIA/v8IAAQA/v8IAAYA/v8IAAgA/v8IAAoA/v8IAAwA/v8IAA4A/v8KAPT//v8KAPb//v8KAPj//v8KAPr//v8KAPz//v8KAP7//v8KAAAA/v8KAAIA/v8KAAQA/v8KAAYA/v8KAAgA/v8KAAoA/v8KAAwA/v8MAPb//v8MAPj//v8MAPr//v8MAPz//v8MAP7//v8MAAAA/v8MAAIA/v8MAAQA/v8MAAYA/v8MAAgA/v8MAAoA/v8OAPj//v8OAPr//v8OAPz//v8OAP7//v8OAAAA/v8OAAIA/v8OAAQA/v8OAAYA/v8OAAgA/v8QAPz//v8QAP7//v8QAAAA/v8QAAIA/v8QAAQA///x//n////x//v////x//3////x///////x/wEA///x/wMA///x/wUA///x/wcA///z//f////z//n////z//v////z//3////z///////z/wEA///z/wMA///z/wUA///z/wcA///z/wkA///1//X////1//f////1//n////1//v////1//3////1///////1/wEA///1/wMA///1/wUA///1/wcA///1/wkA///1/wsA///3//P////3//X////3//f////3//n////3//v////3//3////3///////3/wEA///3/wMA///3/wUA///3/wcA///3/wkA///3/wsA///3/w0A///5//H////5//P////5//X////5//f////5//n////5//v////5//3////5///////5/wEA///5/wMA///5/wUA///5/wcA///5/wkA///5/wsA///5/w0A///5/w8A///7//H////7//P////7//X////7//f////7//n////7//v////7//3////7///////7/wEA///7/wMA///7/wUA///7/wcA///7/wkA///7/wsA///7/w0A///7/w8A///9//H////9//P////9//X////9//f////9//n////9//v////9//3////9///////9/wEA///9/wMA///9/wUA///9/wcA///9/wkA///9/wsA///9/w0A///9/w8A//////H///////P///////X///////f///////n///////v///////3//////////////wEA/////wMA/////wUA/////wcA/////wkA/////wsA/////w0A/////w8A//8BAPH///8BAPP///8BAPX///8BAPf///8BAPn///8BAPv///8BAP3///8BAP////8BAAEA//8BAAMA//8BAAUA//8BAAcA//8BAAkA//8BAAsA//8BAA0A//8BAA8A//8DAPH///8DAPP///8DAPX///8DAPf///8DAPn///8DAPv///8DAP3///8DAP////8DAAEA//8DAAMA//8DAAUA//8DAAcA//8DAAkA//8DAAsA//8DAA0A//8DAA8A//8FAPH///8FAPP///8FAPX///8FAPf///8FAPn///8FAPv///8FAP3///8FAP////8FAAEA//8FAAMA//8FAAUA//8FAAcA//8FAAkA//8FAAsA//8FAA0A//8FAA8A//8HAPH///8HAPP///8HAPX///8HAPf///8HAPn///8HAPv///8HAP3///8HAP////8HAAEA//8HAAMA//8HAAUA//8HAAcA//8HAAkA//8HAAsA//8HAA0A//8HAA8A//8JAPP///8JAPX///8JAPf///8JAPn///8JAPv///8JAP3///8JAP////8JAAEA//8JAAMA//8JAAUA//8JAAcA//8JAAkA//8JAAsA//8JAA0A//8LAPX///8LAPf///8LAPn///8LAPv///8LAP3///8LAP////8LAAEA//8LAAMA//8LAAUA//8LAAcA//8LAAkA//8LAAsA//8NAPf///8NAPn///8NAPv///8NAP3///8NAP////8NAAEA//8NAAMA//8NAAUA//8NAAcA//8NAAkA//8PAPn///8PAPv///8PAP3///8PAP////8PAAEA//8PAAMA//8PAAUA//8PAAcAAADw//z/AADw//7/AADw/wAAAADw/wIAAADw/wQAAADy//j/AADy//r/AADy//z/AADy//7/AADy/wAAAADy/wIAAADy/wQAAADy/wYAAADy/wgAAAD0//b/AAD0//j/AAD0//r/AAD0//z/AAD0//7/AAD0/wAAAAD0/wIAAAD0/wQAAAD0/wYAAAD0/wgAAAD0/woAAAD2//T/AAD2//b/AAD2//j/AAD2//r/AAD2//z/AAD2//7/AAD2/wAAAAD2/wIAAAD2/wQAAAD2/wYAAAD2/wgAAAD2/woAAAD2/wwAAAD4//L/AAD4//T/AAD4//b/AAD4//j/AAD4//r/AAD4//z/AAD4//7/AAD4/wAAAAD4/wIAAAD4/wQAAAD4/wYAAAD4/wgAAAD4/woAAAD4/wwAAAD4/w4AAAD6//L/AAD6//T/AAD6//b/AAD6//j/AAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD6/wgAAAD6/woAAAD6/wwAAAD6/w4AAAD8//D/AAD8//L/AAD8//T/AAD8//b/AAD8//j/AAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD8/wgAAAD8/woAAAD8/wwAAAD8/w4AAAD8/xAAAAD+//D/AAD+//L/AAD+//T/AAD+//b/AAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD+/woAAAD+/wwAAAD+/w4AAAD+/xAAAAAAAPD/AAAAAPL/AAAAAPT/AAAAAPb/AAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAAAAAoAAAAAAAwAAAAAAA4AAAAAABAAAAACAPD/AAACAPL/AAACAPT/AAACAPb/AAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAACAAoAAAACAAwAAAACAA4AAAACABAAAAAEAPD/AAAEAPL/AAAEAPT/AAAEAPb/AAAEAPj/AAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAEAAgAAAAEAAoAAAAEAAwAAAAEAA4AAAAEABAAAAAGAPL/AAAGAPT/AAAGAPb/AAAGAPj/AAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAGAAgAAAAGAAoAAAAGAAwAAAAGAA4AAAAIAPL/AAAIAPT/AAAIAPb/AAAIAPj/AAAIAPr/AAAIAPz/AAAIAP7/AAAIAAAAAAAIAAIAAAAIAAQAAAAIAAYAAAAIAAgAAAAIAAoAAAAIAAwAAAAIAA4AAAAKAPT/AAAKAPb/AAAKAPj/AAAKAPr/AAAKAPz/AAAKAP7/AAAKAAAAAAAKAAIAAAAKAAQAAAAKAAYAAAAKAAgAAAAKAAoAAAAKAAwAAAAMAPb/AAAMAPj/AAAMAPr/AAAMAPz/AAAMAP7/AAAMAAAAAAAMAAIAAAAMAAQAAAAMAAYAAAAMAAgAAAAMAAoAAAAOAPj/AAAOAPr/AAAOAPz/AAAOAP7/AAAOAAAAAAAOAAIAAAAOAAQAAAAOAAYAAAAOAAgAAAAQAPz/AAAQAP7/AAAQAAAAAAAQAAIAAAAQAAQAAQDx//n/AQDx//v/AQDx//3/AQDx////AQDx/wEAAQDx/wMAAQDx/wUAAQDx/wcAAQDz//f/AQDz//n/AQDz//v/AQDz//3/AQDz////AQDz/wEAAQDz/wMAAQDz/wUAAQDz/wcAAQDz/wkAAQD1//X/AQD1//f/AQD1//n/AQD1//v/AQD1//3/AQD1////AQD1/wEAAQD1/wMAAQD1/wUAAQD1/wcAAQD1/wkAAQD1/wsAAQD3//P/AQD3//X/AQD3//f/AQD3//n/AQD3//v/AQD3//3/AQD3////AQD3/wEAAQD3/wMAAQD3/wUAAQD3/wcAAQD3/wkAAQD3/wsAAQD3/w0AAQD5//H/AQD5//P/AQD5//X/AQD5//f/AQD5//n/AQD5//v/AQD5//3/AQD5////AQD5/wEAAQD5/wMAAQD5/wUAAQD5/wcAAQD5/wkAAQD5/wsAAQD5/w0AAQD5/w8AAQD7//H/AQD7//P/AQD7//X/AQD7//f/AQD7//n/AQD7//v/AQD7//3/AQD7////AQD7/wEAAQD7/wMAAQD7/wUAAQD7/wcAAQD7/wkAAQD7/wsAAQD7/w0AAQD7/w8AAQD9//H/AQD9//P/AQD9//X/AQD9//f/AQD9//n/AQD9//v/AQD9//3/AQD9////AQD9/wEAAQD9/wMAAQD9/wUAAQD9/wcAAQD9/wkAAQD9/wsAAQD9/w0AAQD9/w8AAQD///H/AQD///P/AQD///X/AQD///f/AQD///n/AQD///v/AQD///3/AQD/////AQD//wEAAQD//wMAAQD//wUAAQD//wcAAQD//wkAAQD//wsAAQD//w0AAQD//w8AAQABAPH/AQABAPP/AQABAPX/AQABAPf/AQABAPn/AQABAPv/AQABAP3/AQABAP//AQABAAEAAQABAAMAAQABAAUAAQABAAcAAQABAAkAAQABAAsAAQABAA0AAQABAA8AAQADAPH/AQADAPP/AQADAPX/AQADAPf/AQADAPn/AQADAPv/AQADAP3/AQADAP//AQADAAEAAQADAAMAAQADAAUAAQADAAcAAQADAAkAAQADAAsAAQADAA0AAQADAA8AAQAFAPH/AQAFAPP/AQAFAPX/AQAFAPf/AQAFAPn/AQAFAPv/AQAFAP3/AQAFAP//AQAFAAEAAQAFAAMAAQAFAAUAAQAFAAcAAQAFAAkAAQAFAAsAAQAFAA0AAQAFAA8AAQAHAPH/AQAHAPP/AQAHAPX/AQAHAPf/AQAHAPn/AQAHAPv/AQAHAP3/AQAHAP//AQAHAAEAAQAHAAMAAQAHAAUAAQAHAAcAAQAHAAkAAQAHAAsAAQAHAA0AAQAHAA8AAQAJAPP/AQAJAPX/AQAJAPf/AQAJAPn/AQAJAPv/AQAJAP3/AQAJAP//AQAJAAEAAQAJAAMAAQAJAAUAAQAJAAcAAQAJAAkAAQAJAAsAAQAJAA0AAQALAPX/AQALAPf/AQALAPn/AQALAPv/AQALAP3/AQALAP//AQALAAEAAQALAAMAAQALAAUAAQALAAcAAQALAAkAAQALAAsAAQANAPf/AQANAPn/AQANAPv/AQANAP3/AQANAP//AQANAAEAAQANAAMAAQANAAUAAQANAAcAAQANAAkAAQAPAPn/AQAPAPv/AQAPAP3/AQAPAP//AQAPAAEAAQAPAAMAAQAPAAUAAQAPAAcAAgDw//z/AgDw//7/AgDw/wAAAgDw/wIAAgDw/wQAAgDy//j/AgDy//r/AgDy//z/AgDy//7/AgDy/wAAAgDy/wIAAgDy/wQAAgDy/wYAAgDy/wgAAgD0//b/AgD0//j/AgD0//r/AgD0//z/AgD0//7/AgD0/wAAAgD0/wIAAgD0/wQAAgD0/wYAAgD0/wgAAgD0/woAAgD2//T/AgD2//b/AgD2//j/AgD2//r/AgD2//z/AgD2//7/AgD2/wAAAgD2/wIAAgD2/wQAAgD2/wYAAgD2/wgAAgD2/woAAgD2/wwAAgD4//L/AgD4//T/AgD4//b/AgD4//j/AgD4//r/AgD4//z/AgD4//7/AgD4/wAAAgD4/wIAAgD4/wQAAgD4/wYAAgD4/wgAAgD4/woAAgD4/wwAAgD4/w4AAgD6//L/AgD6//T/AgD6//b/AgD6//j/AgD6//r/AgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD6/wYAAgD6/wgAAgD6/woAAgD6/wwAAgD6/w4AAgD8//D/AgD8//L/AgD8//T/AgD8//b/AgD8//j/AgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD8/wgAAgD8/woAAgD8/wwAAgD8/w4AAgD8/xAAAgD+//D/AgD+//L/AgD+//T/AgD+//b/AgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD+/woAAgD+/wwAAgD+/w4AAgD+/xAAAgAAAPD/AgAAAPL/AgAAAPT/AgAAAPb/AgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgAAAAoAAgAAAAwAAgAAAA4AAgAAABAAAgACAPD/AgACAPL/AgACAPT/AgACAPb/AgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgACAAoAAgACAAwAAgACAA4AAgACABAAAgAEAPD/AgAEAPL/AgAEAPT/AgAEAPb/AgAEAPj/AgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAEAAgAAgAEAAoAAgAEAAwAAgAEAA4AAgAEABAAAgAGAPL/AgAGAPT/AgAGAPb/AgAGAPj/AgAGAPr/AgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAGAAYAAgAGAAgAAgAGAAoAAgAGAAwAAgAGAA4AAgAIAPL/AgAIAPT/AgAIAPb/AgAIAPj/AgAIAPr/AgAIAPz/AgAIAP7/AgAIAAAAAgAIAAIAAgAIAAQAAgAIAAYAAgAIAAgAAgAIAAoAAgAIAAwAAgAIAA4AAgAKAPT/AgAKAPb/AgAKAPj/AgAKAPr/AgAKAPz/AgAKAP7/AgAKAAAAAgAKAAIAAgAKAAQAAgAKAAYAAgAKAAgAAgAKAAoAAgAKAAwAAgAMAPb/AgAMAPj/AgAMAPr/AgAMAPz/AgAMAP7/AgAMAAAAAgAMAAIAAgAMAAQAAgAMAAYAAgAMAAgAAgAMAAoAAgAOAPj/AgAOAPr/AgAOAPz/AgAOAP7/AgAOAAAAAgAOAAIAAgAOAAQAAgAOAAYAAgAOAAgAAgAQAPz/AgAQAP7/AgAQAAAAAgAQAAIAAgAQAAQAAwDx//n/AwDx//v/AwDx//3/AwDx////AwDx/wEAAwDx/wMAAwDx/wUAAwDx/wcAAwDz//f/AwDz//n/AwDz//v/AwDz//3/AwDz////AwDz/wEAAwDz/wMAAwDz/wUAAwDz/wcAAwDz/wkAAwD1//X/AwD1//f/AwD1//n/AwD1//v/AwD1//3/AwD1////AwD1/wEAAwD1/wMAAwD1/wUAAwD1/wcAAwD1/wkAAwD1/wsAAwD3//P/AwD3//X/AwD3//f/AwD3//n/AwD3//v/AwD3//3/AwD3////AwD3/wEAAwD3/wMAAwD3/wUAAwD3/wcAAwD3/wkAAwD3/wsAAwD3/w0AAwD5//H/AwD5//P/AwD5//X/AwD5//f/AwD5//n/AwD5//v/AwD5//3/AwD5////AwD5/wEAAwD5/wMAAwD5/wUAAwD5/wcAAwD5/wkAAwD5/wsAAwD5/w0AAwD5/w8AAwD7//H/AwD7//P/AwD7//X/AwD7//f/AwD7//n/AwD7//v/AwD7//3/AwD7////AwD7/wEAAwD7/wMAAwD7/wUAAwD7/wcAAwD7/wkAAwD7/wsAAwD7/w0AAwD7/w8AAwD9//H/AwD9//P/AwD9//X/AwD9//f/AwD9//n/AwD9//v/AwD9//3/AwD9////AwD9/wEAAwD9/wMAAwD9/wUAAwD9/wcAAwD9/wkAAwD9/wsAAwD9/w0AAwD9/w8AAwD///H/AwD///P/AwD///X/AwD///f/AwD///n/AwD///v/AwD///3/AwD/////AwD//wEAAwD//wMAAwD//wUAAwD//wcAAwD//wkAAwD//wsAAwD//w0AAwD//w8AAwABAPH/AwABAPP/AwABAPX/AwABAPf/AwABAPn/AwABAPv/AwABAP3/AwABAP//AwABAAEAAwABAAMAAwABAAUAAwABAAcAAwABAAkAAwABAAsAAwABAA0AAwABAA8AAwADAPH/AwADAPP/AwADAPX/AwADAPf/AwADAPn/AwADAPv/AwADAP3/AwADAP//AwADAAEAAwADAAMAAwADAAUAAwADAAcAAwADAAkAAwADAAsAAwADAA0AAwADAA8AAwAFAPH/AwAFAPP/AwAFAPX/AwAFAPf/AwAFAPn/AwAFAPv/AwAFAP3/AwAFAP//AwAFAAEAAwAFAAMAAwAFAAUAAwAFAAcAAwAFAAkAAwAFAAsAAwAFAA0AAwAFAA8AAwAHAPH/AwAHAPP/AwAHAPX/AwAHAPf/AwAHAPn/AwAHAPv/AwAHAP3/AwAHAP//AwAHAAEAAwAHAAMAAwAHAAUAAwAHAAcAAwAHAAkAAwAHAAsAAwAHAA0AAwAHAA8AAwAJAPP/AwAJAPX/AwAJAPf/AwAJAPn/AwAJAPv/AwAJAP3/AwAJAP//AwAJAAEAAwAJAAMAAwAJAAUAAwAJAAcAAwAJAAkAAwAJAAsAAwAJAA0AAwALAPX/AwALAPf/AwALAPn/AwALAPv/AwALAP3/AwALAP//AwALAAEAAwALAAMAAwALAAUAAwALAAcAAwALAAkAAwALAAsAAwANAPf/AwANAPn/AwANAPv/AwANAP3/AwANAP//AwANAAEAAwANAAMAAwANAAUAAwANAAcAAwANAAkAAwAPAPn/AwAPAPv/AwAPAP3/AwAPAP//AwAPAAEAAwAPAAMAAwAPAAUAAwAPAAcABADw//7/BADw/wAABADw/wIABADy//j/BADy//r/BADy//z/BADy//7/BADy/wAABADy/wIABADy/wQABADy/wYABADy/wgABAD0//b/BAD0//j/BAD0//r/BAD0//z/BAD0//7/BAD0/wAABAD0/wIABAD0/wQABAD0/wYABAD0/wgABAD0/woABAD2//T/BAD2//b/BAD2//j/BAD2//r/BAD2//z/BAD2//7/BAD2/wAABAD2/wIABAD2/wQABAD2/wYABAD2/wgABAD2/woABAD2/wwABAD4//L/BAD4//T/BAD4//b/BAD4//j/BAD4//r/BAD4//z/BAD4//7/BAD4/wAABAD4/wIABAD4/wQABAD4/wYABAD4/wgABAD4/woABAD4/wwABAD4/w4ABAD6//L/BAD6//T/BAD6//b/BAD6//j/BAD6//r/BAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD6/wYABAD6/wgABAD6/woABAD6/wwABAD6/w4ABAD8//L/BAD8//T/BAD8//b/BAD8//j/BAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD8/wgABAD8/woABAD8/wwABAD8/w4ABAD+//D/BAD+//L/BAD+//T/BAD+//b/BAD+//j/BAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD+/wgABAD+/woABAD+/wwABAD+/w4ABAD+/xAABAAAAPD/BAAAAPL/BAAAAPT/BAAAAPb/BAAAAPj/BAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAAAAAgABAAAAAoABAAAAAwABAAAAA4ABAAAABAABAACAPD/BAACAPL/BAACAPT/BAACAPb/BAACAPj/BAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAACAAgABAACAAoABAACAAwABAACAA4ABAACABAABAAEAPL/BAAEAPT/BAAEAPb/BAAEAPj/BAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAEAAgABAAEAAoABAAEAAwABAAEAA4ABAAGAPL/BAAGAPT/BAAGAPb/BAAGAPj/BAAGAPr/BAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAGAAYABAAGAAgABAAGAAoABAAGAAwABAAGAA4ABAAIAPL/BAAIAPT/BAAIAPb/BAAIAPj/BAAIAPr/BAAIAPz/BAAIAP7/BAAIAAAABAAIAAIABAAIAAQABAAIAAYABAAIAAgABAAIAAoABAAIAAwABAAIAA4ABAAKAPT/BAAKAPb/BAAKAPj/BAAKAPr/BAAKAPz/BAAKAP7/BAAKAAAABAAKAAIABAAKAAQABAAKAAYABAAKAAgABAAKAAoABAAKAAwABAAMAPb/BAAMAPj/BAAMAPr/BAAMAPz/BAAMAP7/BAAMAAAABAAMAAIABAAMAAQABAAMAAYABAAMAAgABAAMAAoABAAOAPj/BAAOAPr/BAAOAPz/BAAOAP7/BAAOAAAABAAOAAIABAAOAAQABAAOAAYABAAOAAgABAAQAP7/BAAQAAAABAAQAAIABQDx//v/BQDx//3/BQDx////BQDx/wEABQDx/wMABQDx/wUABQDz//f/BQDz//n/BQDz//v/BQDz//3/BQDz////BQDz/wEABQDz/wMABQDz/wUABQDz/wcABQDz/wkABQD1//X/BQD1//f/BQD1//n/BQD1//v/BQD1//3/BQD1////BQD1/wEABQD1/wMABQD1/wUABQD1/wcABQD1/wkABQD1/wsABQD3//P/BQD3//X/BQD3//f/BQD3//n/BQD3//v/BQD3//3/BQD3////BQD3/wEABQD3/wMABQD3/wUABQD3/wcABQD3/wkABQD3/wsABQD3/w0ABQD5//P/BQD5//X/BQD5//f/BQD5//n/BQD5//v/BQD5//3/BQD5////BQD5/wEABQD5/wMABQD5/wUABQD5/wcABQD5/wkABQD5/wsABQD5/w0ABQD7//H/BQD7//P/BQD7//X/BQD7//f/BQD7//n/BQD7//v/BQD7//3/BQD7////BQD7/wEABQD7/wMABQD7/wUABQD7/wcABQD7/wkABQD7/wsABQD7/w0ABQD7/w8ABQD9//H/BQD9//P/BQD9//X/BQD9//f/BQD9//n/BQD9//v/BQD9//3/BQD9////BQD9/wEABQD9/wMABQD9/wUABQD9/wcABQD9/wkABQD9/wsABQD9/w0ABQD9/w8ABQD///H/BQD///P/BQD///X/BQD///f/BQD///n/BQD///v/BQD///3/BQD/////BQD//wEABQD//wMABQD//wUABQD//wcABQD//wkABQD//wsABQD//w0ABQD//w8ABQABAPH/BQABAPP/BQABAPX/BQABAPf/BQABAPn/BQABAPv/BQABAP3/BQABAP//BQABAAEABQABAAMABQABAAUABQABAAcABQABAAkABQABAAsABQABAA0ABQABAA8ABQADAPH/BQADAPP/BQADAPX/BQADAPf/BQADAPn/BQADAPv/BQADAP3/BQADAP//BQADAAEABQADAAMABQADAAUABQADAAcABQADAAkABQADAAsABQADAA0ABQADAA8ABQAFAPH/BQAFAPP/BQAFAPX/BQAFAPf/BQAFAPn/BQAFAPv/BQAFAP3/BQAFAP//BQAFAAEABQAFAAMABQAFAAUABQAFAAcABQAFAAkABQAFAAsABQAFAA0ABQAFAA8ABQAHAPP/BQAHAPX/BQAHAPf/BQAHAPn/BQAHAPv/BQAHAP3/BQAHAP//BQAHAAEABQAHAAMABQAHAAUABQAHAAcABQAHAAkABQAHAAsABQAHAA0ABQAJAPP/BQAJAPX/BQAJAPf/BQAJAPn/BQAJAPv/BQAJAP3/BQAJAP//BQAJAAEABQAJAAMABQAJAAUABQAJAAcABQAJAAkABQAJAAsABQAJAA0ABQALAPX/BQALAPf/BQALAPn/BQALAPv/BQALAP3/BQALAP//BQALAAEABQALAAMABQALAAUABQALAAcABQALAAkABQALAAsABQANAPf/BQANAPn/BQANAPv/BQANAP3/BQANAP//BQANAAEABQANAAMABQANAAUABQANAAcABQANAAkABQAPAPv/BQAPAP3/BQAPAP//BQAPAAEABQAPAAMABQAPAAUABgDy//r/BgDy//z/BgDy//7/BgDy/wAABgDy/wIABgDy/wQABgDy/wYABgD0//b/BgD0//j/BgD0//r/BgD0//z/BgD0//7/BgD0/wAABgD0/wIABgD0/wQABgD0/wYABgD0/wgABgD0/woABgD2//T/BgD2//b/BgD2//j/BgD2//r/BgD2//z/BgD2//7/BgD2/wAABgD2/wIABgD2/wQABgD2/wYABgD2/wgABgD2/woABgD2/wwABgD4//T/BgD4//b/BgD4//j/BgD4//r/BgD4//z/BgD4//7/BgD4/wAABgD4/wIABgD4/wQABgD4/wYABgD4/wgABgD4/woABgD4/wwABgD6//L/BgD6//T/BgD6//b/BgD6//j/BgD6//r/BgD6//z/BgD6//7/BgD6/wAABgD6/wIABgD6/wQABgD6/wYABgD6/wgABgD6/woABgD6/wwABgD6/w4ABgD8//L/BgD8//T/BgD8//b/BgD8//j/BgD8//r/BgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD8/wYABgD8/wgABgD8/woABgD8/wwABgD8/w4ABgD+//L/BgD+//T/BgD+//b/BgD+//j/BgD+//r/BgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD+/wYABgD+/wgABgD+/woABgD+/wwABgD+/w4ABgAAAPL/BgAAAPT/BgAAAPb/BgAAAPj/BgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgAAAAgABgAAAAoABgAAAAwABgAAAA4ABgACAPL/BgACAPT/BgACAPb/BgACAPj/BgACAPr/BgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgACAAYABgACAAgABgACAAoABgACAAwABgACAA4ABgAEAPL/BgAEAPT/BgAEAPb/BgAEAPj/BgAEAPr/BgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAEAAYABgAEAAgABgAEAAoABgAEAAwABgAEAA4ABgAGAPL/BgAGAPT/BgAGAPb/BgAGAPj/BgAGAPr/BgAGAPz/BgAGAP7/BgAGAAAABgAGAAIABgAGAAQABgAGAAYABgAGAAgABgAGAAoABgAGAAwABgAGAA4ABgAIAPT/BgAIAPb/BgAIAPj/BgAIAPr/BgAIAPz/BgAIAP7/BgAIAAAABgAIAAIABgAIAAQABgAIAAYABgAIAAgABgAIAAoABgAIAAwABgAKAPT/BgAKAPb/BgAKAPj/BgAKAPr/BgAKAPz/BgAKAP7/BgAKAAAABgAKAAIABgAKAAQABgAKAAYABgAKAAgABgAKAAoABgAKAAwABgAMAPb/BgAMAPj/BgAMAPr/BgAMAPz/BgAMAP7/BgAMAAAABgAMAAIABgAMAAQABgAMAAYABgAMAAgABgAMAAoABgAOAPr/BgAOAPz/BgAOAP7/BgAOAAAABgAOAAIABgAOAAQABgAOAAYABwDx//3/BwDx////BwDx/wEABwDx/wMABwDz//n/BwDz//v/BwDz//3/BwDz////BwDz/wEABwDz/wMABwDz/wUABwDz/wcABwD1//f/BwD1//n/BwD1//v/BwD1//3/BwD1////BwD1/wEABwD1/wMABwD1/wUABwD1/wcABwD1/wkABwD3//X/BwD3//f/BwD3//n/BwD3//v/BwD3//3/BwD3////BwD3/wEABwD3/wMABwD3/wUABwD3/wcABwD3/wkABwD3/wsABwD5//P/BwD5//X/BwD5//f/BwD5//n/BwD5//v/BwD5//3/BwD5////BwD5/wEABwD5/wMABwD5/wUABwD5/wcABwD5/wkABwD5/wsABwD5/w0ABwD7//P/BwD7//X/BwD7//f/BwD7//n/BwD7//v/BwD7//3/BwD7////BwD7/wEABwD7/wMABwD7/wUABwD7/wcABwD7/wkABwD7/wsABwD7/w0ABwD9//H/BwD9//P/BwD9//X/BwD9//f/BwD9//n/BwD9//v/BwD9//3/BwD9////BwD9/wEABwD9/wMABwD9/wUABwD9/wcABwD9/wkABwD9/wsABwD9/w0ABwD9/w8ABwD///H/BwD///P/BwD///X/BwD///f/BwD///n/BwD///v/BwD///3/BwD/////BwD//wEABwD//wMABwD//wUABwD//wcABwD//wkABwD//wsABwD//w0ABwD//w8ABwABAPH/BwABAPP/BwABAPX/BwABAPf/BwABAPn/BwABAPv/BwABAP3/BwABAP//BwABAAEABwABAAMABwABAAUABwABAAcABwABAAkABwABAAsABwABAA0ABwABAA8ABwADAPH/BwADAPP/BwADAPX/BwADAPf/BwADAPn/BwADAPv/BwADAP3/BwADAP//BwADAAEABwADAAMABwADAAUABwADAAcABwADAAkABwADAAsABwADAA0ABwADAA8ABwAFAPP/BwAFAPX/BwAFAPf/BwAFAPn/BwAFAPv/BwAFAP3/BwAFAP//BwAFAAEABwAFAAMABwAFAAUABwAFAAcABwAFAAkABwAFAAsABwAFAA0ABwAHAPP/BwAHAPX/BwAHAPf/BwAHAPn/BwAHAPv/BwAHAP3/BwAHAP//BwAHAAEABwAHAAMABwAHAAUABwAHAAcABwAHAAkABwAHAAsABwAHAA0ABwAJAPX/BwAJAPf/BwAJAPn/BwAJAPv/BwAJAP3/BwAJAP//BwAJAAEABwAJAAMABwAJAAUABwAJAAcABwAJAAkABwAJAAsABwALAPf/BwALAPn/BwALAPv/BwALAP3/BwALAP//BwALAAEABwALAAMABwALAAUABwALAAcABwALAAkABwANAPn/BwANAPv/BwANAP3/BwANAP//BwANAAEABwANAAMABwANAAUABwANAAcABwAPAP3/BwAPAP//BwAPAAEABwAPAAMACADy//z/CADy//7/CADy/wAACADy/wIACADy/wQACAD0//j/CAD0//r/CAD0//z/CAD0//7/CAD0/wAACAD0/wIACAD0/wQACAD0/wYACAD0/wgACAD2//b/CAD2//j/CAD2//r/CAD2//z/CAD2//7/CAD2/wAACAD2/wIACAD2/wQACAD2/wYACAD2/wgACAD2/woACAD4//T/CAD4//b/CAD4//j/CAD4//r/CAD4//z/CAD4//7/CAD4/wAACAD4/wIACAD4/wQACAD4/wYACAD4/wgACAD4/woACAD4/wwACAD6//T/CAD6//b/CAD6//j/CAD6//r/CAD6//z/CAD6//7/CAD6/wAACAD6/wIACAD6/wQACAD6/wYACAD6/wgACAD6/woACAD6/wwACAD8//L/CAD8//T/CAD8//b/CAD8//j/CAD8//r/CAD8//z/CAD8//7/CAD8/wAACAD8/wIACAD8/wQACAD8/wYACAD8/wgACAD8/woACAD8/wwACAD8/w4ACAD+//L/CAD+//T/CAD+//b/CAD+//j/CAD+//r/CAD+//z/CAD+//7/CAD+/wAACAD+/wIACAD+/wQACAD+/wYACAD+/wgACAD+/woACAD+/wwACAD+/w4ACAAAAPL/CAAAAPT/CAAAAPb/CAAAAPj/CAAAAPr/CAAAAPz/CAAAAP7/CAAAAAAACAAAAAIACAAAAAQACAAAAAYACAAAAAgACAAAAAoACAAAAAwACAAAAA4ACAACAPL/CAACAPT/CAACAPb/CAACAPj/CAACAPr/CAACAPz/CAACAP7/CAACAAAACAACAAIACAACAAQACAACAAYACAACAAgACAACAAoACAACAAwACAACAA4ACAAEAPL/CAAEAPT/CAAEAPb/CAAEAPj/CAAEAPr/CAAEAPz/CAAEAP7/CAAEAAAACAAEAAIACAAEAAQACAAEAAYACAAEAAgACAAEAAoACAAEAAwACAAEAA4ACAAGAPT/CAAGAPb/CAAGAPj/CAAGAPr/CAAGAPz/CAAGAP7/CAAGAAAACAAGAAIACAAGAAQACAAGAAYACAAGAAgACAAGAAoACAAGAAwACAAIAPT/CAAIAPb/CAAIAPj/CAAIAPr/CAAIAPz/CAAIAP7/CAAIAAAACAAIAAIACAAIAAQACAAIAAYACAAIAAgACAAIAAoACAAIAAwACAAKAPb/CAAKAPj/CAAKAPr/CAAKAPz/CAAKAP7/CAAKAAAACAAKAAIACAAKAAQACAAKAAYACAAKAAgACAAKAAoACAAMAPj/CAAMAPr/CAAMAPz/CAAMAP7/CAAMAAAACAAMAAIACAAMAAQACAAMAAYACAAMAAgACAAOAPz/CAAOAP7/CAAOAAAACAAOAAIACAAOAAQACQDz//v/CQDz//3/CQDz////CQDz/wEACQDz/wMACQDz/wUACQD1//f/CQD1//n/CQD1//v/CQD1//3/CQD1////CQD1/wEACQD1/wMACQD1/wUACQD1/wcACQD1/wkACQD3//X/CQD3//f/CQD3//n/CQD3//v/CQD3//3/CQD3////CQD3/wEACQD3/wMACQD3/wUACQD3/wcACQD3/wkACQD3/wsACQD5//X/CQD5//f/CQD5//n/CQD5//v/CQD5//3/CQD5////CQD5/wEACQD5/wMACQD5/wUACQD5/wcACQD5/wkACQD5/wsACQD7//P/CQD7//X/CQD7//f/CQD7//n/CQD7//v/CQD7//3/CQD7////CQD7/wEACQD7/wMACQD7/wUACQD7/wcACQD7/wkACQD7/wsACQD7/w0ACQD9//P/CQD9//X/CQD9//f/CQD9//n/CQD9//v/CQD9//3/CQD9////CQD9/wEACQD9/wMACQD9/wUACQD9/wcACQD9/wkACQD9/wsACQD9/w0ACQD///P/CQD///X/CQD///f/CQD///n/CQD///v/CQD///3/CQD/////CQD//wEACQD//wMACQD//wUACQD//wcACQD//wkACQD//wsACQD//w0ACQABAPP/CQABAPX/CQABAPf/CQABAPn/CQABAPv/CQABAP3/CQABAP//CQABAAEACQABAAMACQABAAUACQABAAcACQABAAkACQABAAsACQABAA0ACQADAPP/CQADAPX/CQADAPf/CQADAPn/CQADAPv/CQADAP3/CQADAP//CQADAAEACQADAAMACQADAAUACQADAAcACQADAAkACQADAAsACQADAA0ACQAFAPP/CQAFAPX/CQAFAPf/CQAFAPn/CQAFAPv/CQAFAP3/CQAFAP//CQAFAAEACQAFAAMACQAFAAUACQAFAAcACQAFAAkACQAFAAsACQAFAA0ACQAHAPX/CQAHAPf/CQAHAPn/CQAHAPv/CQAHAP3/CQAHAP//CQAHAAEACQAHAAMACQAHAAUACQAHAAcACQAHAAkACQAHAAsACQAJAPX/CQAJAPf/CQAJAPn/CQAJAPv/CQAJAP3/CQAJAP//CQAJAAEACQAJAAMACQAJAAUACQAJAAcACQAJAAkACQAJAAsACQALAPf/CQALAPn/CQALAPv/CQALAP3/CQALAP//CQALAAEACQALAAMACQALAAUACQALAAcACQALAAkACQANAPv/CQANAP3/CQANAP//CQANAAEACQANAAMACQANAAUACgD0//r/CgD0//z/CgD0//7/CgD0/wAACgD0/wIACgD0/wQACgD0/wYACgD2//j/CgD2//r/CgD2//z/CgD2//7/CgD2/wAACgD2/wIACgD2/wQACgD2/wYACgD2/wgACgD4//b/CgD4//j/CgD4//r/CgD4//z/CgD4//7/CgD4/wAACgD4/wIACgD4/wQACgD4/wYACgD4/wgACgD4/woACgD6//T/CgD6//b/CgD6//j/CgD6//r/CgD6//z/CgD6//7/CgD6/wAACgD6/wIACgD6/wQACgD6/wYACgD6/wgACgD6/woACgD6/wwACgD8//T/CgD8//b/CgD8//j/CgD8//r/CgD8//z/CgD8//7/CgD8/wAACgD8/wIACgD8/wQACgD8/wYACgD8/wgACgD8/woACgD8/wwACgD+//T/CgD+//b/CgD+//j/CgD+//r/CgD+//z/CgD+//7/CgD+/wAACgD+/wIACgD+/wQACgD+/wYACgD+/wgACgD+/woACgD+/wwACgAAAPT/CgAAAPb/CgAAAPj/CgAAAPr/CgAAAPz/CgAAAP7/CgAAAAAACgAAAAIACgAAAAQACgAAAAYACgAAAAgACgAAAAoACgAAAAwACgACAPT/CgACAPb/CgACAPj/CgACAPr/CgACAPz/CgACAP7/CgACAAAACgACAAIACgACAAQACgACAAYACgACAAgACgACAAoACgACAAwACgAEAPT/CgAEAPb/CgAEAPj/CgAEAPr/CgAEAPz/CgAEAP7/CgAEAAAACgAEAAIACgAEAAQACgAEAAYACgAEAAgACgAEAAoACgAEAAwACgAGAPT/CgAGAPb/CgAGAPj/CgAGAPr/CgAGAPz/CgAGAP7/CgAGAAAACgAGAAIACgAGAAQACgAGAAYACgAGAAgACgAGAAoACgAGAAwACgAIAPb/CgAIAPj/CgAIAPr/CgAIAPz/CgAIAP7/CgAIAAAACgAIAAIACgAIAAQACgAIAAYACgAIAAgACgAIAAoACgAKAPj/CgAKAPr/CgAKAPz/CgAKAP7/CgAKAAAACgAKAAIACgAKAAQACgAKAAYACgAKAAgACgAMAPr/CgAMAPz/CgAMAP7/CgAMAAAACgAMAAIACgAMAAQACgAMAAYACwD1//v/CwD1//3/CwD1////CwD1/wEACwD1/wMACwD1/wUACwD3//f/CwD3//n/CwD3//v/CwD3//3/CwD3////CwD3/wEACwD3/wMACwD3/wUACwD3/wcACwD3/wkACwD5//f/CwD5//n/CwD5//v/CwD5//3/CwD5////CwD5/wEACwD5/wMACwD5/wUACwD5/wcACwD5/wkACwD7//X/CwD7//f/CwD7//n/CwD7//v/CwD7//3/CwD7////CwD7/wEACwD7/wMACwD7/wUACwD7/wcACwD7/wkACwD7/wsACwD9//X/CwD9//f/CwD9//n/CwD9//v/CwD9//3/CwD9////CwD9/wEACwD9/wMACwD9/wUACwD9/wcACwD9/wkACwD9/wsACwD///X/CwD///f/CwD///n/CwD///v/CwD///3/CwD/////CwD//wEACwD//wMACwD//wUACwD//wcACwD//wkACwD//wsACwABAPX/CwABAPf/CwABAPn/CwABAPv/CwABAP3/CwABAP//CwABAAEACwABAAMACwABAAUACwABAAcACwABAAkACwABAAsACwADAPX/CwADAPf/CwADAPn/CwADAPv/CwADAP3/CwADAP//CwADAAEACwADAAMACwADAAUACwADAAcACwADAAkACwADAAsACwAFAPX/CwAFAPf/CwAFAPn/CwAFAPv/CwAFAP3/CwAFAP//CwAFAAEACwAFAAMACwAFAAUACwAFAAcACwAFAAkACwAFAAsACwAHAPf/CwAHAPn/CwAHAPv/CwAHAP3/CwAHAP//CwAHAAEACwAHAAMACwAHAAUACwAHAAcACwAHAAkACwAJAPf/CwAJAPn/CwAJAPv/CwAJAP3/CwAJAP//CwAJAAEACwAJAAMACwAJAAUACwAJAAcACwAJAAkACwALAPv/CwALAP3/CwALAP//CwALAAEACwALAAMACwALAAUADAD2//r/DAD2//z/DAD2//7/DAD2/wAADAD2/wIADAD2/wQADAD2/wYADAD4//j/DAD4//r/DAD4//z/DAD4//7/DAD4/wAADAD4/wIADAD4/wQADAD4/wYADAD4/wgADAD6//b/DAD6//j/DAD6//r/DAD6//z/DAD6//7/DAD6/wAADAD6/wIADAD6/wQADAD6/wYADAD6/wgADAD6/woADAD8//b/DAD8//j/DAD8//r/DAD8//z/DAD8//7/DAD8/wAADAD8/wIADAD8/wQADAD8/wYADAD8/wgADAD8/woADAD+//b/DAD+//j/DAD+//r/DAD+//z/DAD+//7/DAD+/wAADAD+/wIADAD+/wQADAD+/wYADAD+/wgADAD+/woADAAAAPb/DAAAAPj/DAAAAPr/DAAAAPz/DAAAAP7/DAAAAAAADAAAAAIADAAAAAQADAAAAAYADAAAAAgADAAAAAoADAACAPb/DAACAPj/DAACAPr/DAACAPz/DAACAP7/DAACAAAADAACAAIADAACAAQADAACAAYADAACAAgADAACAAoADAAEAPb/DAAEAPj/DAAEAPr/DAAEAPz/DAAEAP7/DAAEAAAADAAEAAIADAAEAAQADAAEAAYADAAEAAgADAAEAAoADAAGAPb/DAAGAPj/DAAGAPr/DAAGAPz/DAAGAP7/DAAGAAAADAAGAAIADAAGAAQADAAGAAYADAAGAAgADAAGAAoADAAIAPj/DAAIAPr/DAAIAPz/DAAIAP7/DAAIAAAADAAIAAIADAAIAAQADAAIAAYADAAIAAgADAAKAPr/DAAKAPz/DAAKAP7/DAAKAAAADAAKAAIADAAKAAQADAAKAAYADQD3//v/DQD3//3/DQD3////DQD3/wEADQD3/wMADQD3/wUADQD5//n/DQD5//v/DQD5//3/DQD5////DQD5/wEADQD5/wMADQD5/wUADQD5/wcADQD7//f/DQD7//n/DQD7//v/DQD7//3/DQD7////DQD7/wEADQD7/wMADQD7/wUADQD7/wcADQD7/wkADQD9//f/DQD9//n/DQD9//v/DQD9//3/DQD9////DQD9/wEADQD9/wMADQD9/wUADQD9/wcADQD9/wkADQD///f/DQD///n/DQD///v/DQD///3/DQD/////DQD//wEADQD//wMADQD//wUADQD//wcADQD//wkADQABAPf/DQABAPn/DQABAPv/DQABAP3/DQABAP//DQABAAEADQABAAMADQABAAUADQABAAcADQABAAkADQADAPf/DQADAPn/DQADAPv/DQADAP3/DQADAP//DQADAAEADQADAAMADQADAAUADQADAAcADQADAAkADQAFAPf/DQAFAPn/DQAFAPv/DQAFAP3/DQAFAP//DQAFAAEADQAFAAMADQAFAAUADQAFAAcADQAFAAkADQAHAPn/DQAHAPv/DQAHAP3/DQAHAP//DQAHAAEADQAHAAMADQAHAAUADQAHAAcADQAJAPv/DQAJAP3/DQAJAP//DQAJAAEADQAJAAMADQAJAAUADgD4//z/DgD4//7/DgD4/wAADgD4/wIADgD4/wQADgD6//r/DgD6//z/DgD6//7/DgD6/wAADgD6/wIADgD6/wQADgD6/wYADgD8//j/DgD8//r/DgD8//z/DgD8//7/DgD8/wAADgD8/wIADgD8/wQADgD8/wYADgD8/wgADgD+//j/DgD+//r/DgD+//z/DgD+//7/DgD+/wAADgD+/wIADgD+/wQADgD+/wYADgD+/wgADgAAAPj/DgAAAPr/DgAAAPz/DgAAAP7/DgAAAAAADgAAAAIADgAAAAQADgAAAAYADgAAAAgADgACAPj/DgACAPr/DgACAPz/DgACAP7/DgACAAAADgACAAIADgACAAQADgACAAYADgACAAgADgAEAPj/DgAEAPr/DgAEAPz/DgAEAP7/DgAEAAAADgAEAAIADgAEAAQADgAEAAYADgAEAAgADgAGAPr/DgAGAPz/DgAGAP7/DgAGAAAADgAGAAIADgAGAAQADgAGAAYADgAIAPz/DgAIAP7/DgAIAAAADgAIAAIADgAIAAQADwD5//3/DwD5////DwD5/wEADwD5/wMADwD7//v/DwD7//3/DwD7////DwD7/wEADwD7/wMADwD7/wUADwD9//n/DwD9//v/DwD9//3/DwD9////DwD9/wEADwD9/wMADwD9/wUADwD9/wcADwD///n/DwD///v/DwD///3/DwD/////DwD//wEADwD//wMADwD//wUADwD//wcADwABAPn/DwABAPv/DwABAP3/DwABAP//DwABAAEADwABAAMADwABAAUADwABAAcADwADAPn/DwADAPv/DwADAP3/DwADAP//DwADAAEADwADAAMADwADAAUADwADAAcADwAFAPv/DwAFAP3/DwAFAP//DwAFAAEADwAFAAMADwAFAAUADwAHAP3/DwAHAP//DwAHAAEADwAHAAMAEAD8//7/EAD8/wAAEAD8/wIAEAD+//z/EAD+//7/EAD+/wAAEAD+/wIAEAD+/wQAEAAAAPz/EAAAAP7/EAAAAAAAEAAAAAIAEAAAAAQAEAACAPz/EAACAP7/EAACAAAAEAACAAIAEAACAAQAEAAEAP7/EAAEAAAAEAAEAAIA\",\"F2\":\"kZAuNHFeMzmRkC40kZAuNHBIPTlVJ0E0cEg9OZGQLjRxXjM5VSdBNB8OSDlVJ0E0cV4zOZGQLjRwSD05VSdBNHBIPTmRkC40kZAuNHFeMzmRkC404iOnOIEGsDiBBrA44iOnOIEGsDhEGcQ44nfPOOJ3zzhEGcQ4gQawOOIjpzhEGcQ4197bOCxv6Tgsb+k4197bOEQZxDjiI6c4gQawOOJ3zzgsb+k4Nk/4ODZP+Dgsb+k44nfPOIEGsDiBBrA44nfPOCxv6Tg2T/g4Nk/4OCxv6Tjid884gQawOOIjpzhEGcQ4197bOCxv6Tgsb+k4197bOEQZxDjiI6c4gQawOEQZxDjid8844nfPOEQZxDiBBrA44iOnOIEGsDiBBrA44iOnOJGQLjRwSD05VSdBNHBIPTmRkC40mIw3NKPJUznqLmI0gZ9uOeouYjSjyVM5mIw3NJGQLjSjyVM5XMduNPp8hzkjWYU0+nyHOVzHbjSjyVM5kZAuNHBIPTnqLmI0+nyHOaIRjTQVPJs5ohGNNPp8hznqLmI0cEg9OVUnQTSBn245I1mFNBU8mzlzUpU0FTybOSNZhTSBn245VSdBNHBIPTnqLmI0+nyHOaIRjTQVPJs5ohGNNPp8hznqLmI0cEg9OZGQLjSjyVM5XMduNPp8hzkjWYU0+nyHOVzHbjSjyVM5kZAuNJiMNzSjyVM56i5iNIGfbjnqLmI0o8lTOZiMNzSRkC40cEg9OVUnQTRwSD05kZAuNIEGsDhEGcQ44nfPOOJ3zzhEGcQ4gQawOGqmuTjX3ts4Nk/4ONNVBDnTVQQ5Nk/4ONfe2zhqprk4gQawONfe2zjTVQQ5OFoXObNzIjmzcyI5OFoXOdNVBDnX3ts4gQawOEQZxDg2T/g4OFoXOU3ULjkQsDw5ELA8OU3ULjk4Whc5Nk/4OEQZxDjid88401UEObNzIjkQsDw5vUVMOb1FTDkQsDw5s3MiOdNVBDnid8844nfPONNVBDmzcyI5ELA8Ob1FTDm9RUw5ELA8ObNzIjnTVQQ54nfPOEQZxDg2T/g4OFoXOU3ULjkQsDw5ELA8OU3ULjk4Whc5Nk/4OEQZxDiBBrA4197bONNVBDk4Whc5s3MiObNzIjk4Whc501UEOdfe2ziBBrA4aqa5ONfe2zg2T/g401UEOdNVBDk2T/g4197bOGqmuTiBBrA4RBnEOOJ3zzjid884RBnEOIEGsDheOSo5VSdBNKPJUzkCa1Y0o8lTOVUnQTReOSo5cV4zOQJrVjRRBX45I1mFNIXakDkjWYU0UQV+OQJrVjRxXjM5XjkqOQJrVjT6fIc5c1KVNHuuszkThqc0e66zOXNSlTT6fIc5AmtWNF45KjlVJ0E0UQV+OXNSlTSHJcI5fhS8NJTj5Dl+FLw0hyXCOXNSlTRRBX45VSdBNKPJUzkjWYU0e66zOX4UvDSa4fk5penSNJrh+Tl+FLw0e66zOSNZhTSjyVM5AmtWNIXakDkThqc0lOPkOaXp0jQY9Ag6penSNJTj5DkThqc0hdqQOQJrVjSjyVM5I1mFNHuuszl+FLw0muH5OaXp0jSa4fk5fhS8NHuuszkjWYU0o8lTOVUnQTRRBX45c1KVNIclwjl+FLw0lOPkOX4UvDSHJcI5c1KVNFEFfjlVJ0E0XjkqOQJrVjT6fIc5c1KVNHuuszkThqc0e66zOXNSlTT6fIc5AmtWNF45KjlxXjM5AmtWNFEFfjkjWYU0hdqQOSNZhTRRBX45AmtWNHFeMzleOSo5VSdBNKPJUzkCa1Y0o8lTOVUnQTReOSo5aqa5OOJ3zzjX3ts4197bOOJ3zzhqprk44iOnOOJ3zzg2T/g4bFwNOThaFzk4Whc5bFwNOTZP+Djid8844iOnOOJ3zzjTVQQ5s3MiORCwPDm9RUw5vUVMORCwPDmzcyI501UEOeJ3zzhqprk4Nk/4OLNzIjm9RUw50OBxOVdbhDlXW4Q50OBxOb1FTDmzcyI5Nk/4OGqmuTjid884bFwNORCwPDnQ4HE5UXmROZukoDmbpKA5UXmROdDgcTkQsDw5bFwNOeJ3zzjX3ts4OFoXOb1FTDlXW4Q5m6SgOQ9QsjkPULI5m6SgOVdbhDm9RUw5OFoXOdfe2zjX3ts4OFoXOb1FTDlXW4Q5m6SgOQ9QsjkPULI5m6SgOVdbhDm9RUw5OFoXOdfe2zjid884bFwNORCwPDnQ4HE5UXmROZukoDmbpKA5UXmROdDgcTkQsDw5bFwNOeJ3zzhqprk4Nk/4OLNzIjm9RUw50OBxOVdbhDlXW4Q50OBxOb1FTDmzcyI5Nk/4OGqmuTjid88401UEObNzIjkQsDw5vUVMOb1FTDkQsDw5s3MiOdNVBDnid8844iOnOOJ3zzg2T/g4bFwNOThaFzk4Whc5bFwNOTZP+Djid8844iOnOGqmuTjid884197bONfe2zjid884aqa5OF45KjlVJ0E0o8lTOQJrVjSjyVM5VSdBNF45KjlwSD056i5iNPp8hzmiEY00FTybOaIRjTT6fIc56i5iNHBIPTlwSD05XMduNBU8mzkThqc0LXHSOX4UvDQtcdI5E4anNBU8mzlcx240cEg9OV45KjnqLmI0FTybOXWBsTSa4fk5Bg3fNMfJFjoGDd80muH5OXWBsTQVPJs56i5iNF45KjlVJ0E0+nyHOROGpzSa4fk5n4TrNIGSOTo7SwI1gZI5Op+E6zSa4fk5E4anNPp8hzlVJ0E0o8lTOaIRjTQtcdI5Bg3fNIGSOTqvQwg1behpOq9DCDWBkjk6Bg3fNC1x0jmiEY00o8lTOQJrVjQVPJs5fhS8NMfJFjo7SwI1behpOoW3DTVt6Gk6O0sCNcfJFjp+FLw0FTybOQJrVjSjyVM5ohGNNC1x0jkGDd80gZI5Oq9DCDVt6Gk6r0MINYGSOToGDd80LXHSOaIRjTSjyVM5VSdBNPp8hzkThqc0muH5OZ+E6zSBkjk6O0sCNYGSOTqfhOs0muH5OROGpzT6fIc5VSdBNF45KjnqLmI0FTybOXWBsTSa4fk5Bg3fNMfJFjoGDd80muH5OXWBsTQVPJs56i5iNF45KjlwSD05XMduNBU8mzkThqc0LXHSOX4UvDQtcdI5E4anNBU8mzlcx240cEg9OXBIPTnqLmI0+nyHOaIRjTQVPJs5ohGNNPp8hznqLmI0cEg9OV45KjlVJ0E0o8lTOQJrVjSjyVM5VSdBNF45KjmBBrA4RBnEOOJ3zzjid884RBnEOIEGsDjiI6c44nfPODZP+DhsXA05OFoXOThaFzlsXA05Nk/4OOJ3zzjiI6c44iOnONfe2zhsXA05TdQuOb1FTDmO4V05juFdOb1FTDlN1C45bFwNOdfe2zjiI6c44nfPOGxcDTkQsDw50OBxOVF5kTmbpKA5m6SgOVF5kTnQ4HE5ELA8OWxcDTnid884gQawODZP+DhN1C450OBxOZukoDmhD8c5jKTfOYyk3zmhD8c5m6SgOdDgcTlN1C45Nk/4OIEGsDhEGcQ4bFwNOb1FTDlReZE5oQ/HOSgP/TnZVBA62VQQOigP/TmhD8c5UXmROb1FTDlsXA05RBnEOOJ3zzg4Whc5juFdOZukoDmMpN852VQQOtIoJjrSKCY62VQQOoyk3zmbpKA5juFdOThaFznid8844nfPODhaFzmO4V05m6SgOYyk3znZVBA60igmOtIoJjrZVBA6jKTfOZukoDmO4V05OFoXOeJ3zzhEGcQ4bFwNOb1FTDlReZE5oQ/HOSgP/TnZVBA62VQQOigP/TmhD8c5UXmROb1FTDlsXA05RBnEOIEGsDg2T/g4TdQuOdDgcTmbpKA5oQ/HOYyk3zmMpN85oQ/HOZukoDnQ4HE5TdQuOTZP+DiBBrA44nfPOGxcDTkQsDw50OBxOVF5kTmbpKA5m6SgOVF5kTnQ4HE5ELA8OWxcDTnid8844iOnONfe2zhsXA05TdQuOb1FTDmO4V05juFdOb1FTDlN1C45bFwNOdfe2zjiI6c44iOnOOJ3zzg2T/g4bFwNOThaFzk4Whc5bFwNOTZP+Djid8844iOnOIEGsDhEGcQ44nfPOOJ3zzhEGcQ4gQawOJGQLjRwSD05VSdBNHBIPTmRkC40cV4zOQJrVjRRBX45I1mFNIXakDkjWYU0UQV+OQJrVjRxXjM5cEg9OVzHbjQVPJs5E4anNC1x0jl+FLw0LXHSOROGpzQVPJs5XMduNHBIPTlxXjM5XMduNJfIpjl+FLw0GPQIOp+E6zSJ1CY6n4TrNBj0CDp+FLw0l8imOVzHbjRxXjM5AmtWNBU8mzl+FLw0x8kWOjtLAjVt6Gk6hbcNNW3oaTo7SwI1x8kWOn4UvDQVPJs5AmtWNJGQLjRRBX45E4anNBj0CDo7SwI1nLqEOru4FTXpj686u7gVNZy6hDo7SwI1GPQIOhOGpzRRBX45kZAuNHBIPTkjWYU0LXHSOZ+E6zRt6Gk6u7gVNQ0xzTrtjRc1DTHNOru4FTVt6Gk6n4TrNC1x0jkjWYU0cEg9OVUnQTSF2pA5fhS8NInUJjqFtw016Y+vOu2NFzVcCPM67Y0XNemPrzqFtw01idQmOn4UvDSF2pA5VSdBNHBIPTkjWYU0LXHSOZ+E6zRt6Gk6u7gVNQ0xzTrtjRc1DTHNOru4FTVt6Gk6n4TrNC1x0jkjWYU0cEg9OZGQLjRRBX45E4anNBj0CDo7SwI1nLqEOru4FTXpj686u7gVNZy6hDo7SwI1GPQIOhOGpzRRBX45kZAuNAJrVjQVPJs5fhS8NMfJFjo7SwI1behpOoW3DTVt6Gk6O0sCNcfJFjp+FLw0FTybOQJrVjRxXjM5XMduNJfIpjl+FLw0GPQIOp+E6zSJ1CY6n4TrNBj0CDp+FLw0l8imOVzHbjRxXjM5cEg9OVzHbjQVPJs5E4anNC1x0jl+FLw0LXHSOROGpzQVPJs5XMduNHBIPTlxXjM5AmtWNFEFfjkjWYU0hdqQOSNZhTRRBX45AmtWNHFeMzmRkC40cEg9OVUnQTRwSD05kZAuNOIjpziBBrA4gQawOOIjpzhqprk4197bODZP+DjTVQQ501UEOTZP+DjX3ts4aqa5OOJ3zzjTVQQ5s3MiORCwPDm9RUw5vUVMORCwPDmzcyI501UEOeJ3zzjid884bFwNORCwPDnQ4HE5UXmROZukoDmbpKA5UXmROdDgcTkQsDw5bFwNOeJ3zzhqprk401UEORCwPDlXW4Q5D1CyOYyk3zkoD/05KA/9OYyk3zkPULI5V1uEORCwPDnTVQQ5aqa5ONfe2zizcyI50OBxOQ9QsjkoD/050igmOgJVQToCVUE60igmOigP/TkPULI50OBxObNzIjnX3ts44iOnODZP+DgQsDw5UXmROYyk3znSKCY6HsVjOngwiDp4MIg6HsVjOtIoJjqMpN85UXmRORCwPDk2T/g44iOnOIEGsDjTVQQ5vUVMOZukoDkoD/05AlVBOngwiDra4qU62uKlOngwiDoCVUE6KA/9OZukoDm9RUw501UEOYEGsDiBBrA401UEOb1FTDmbpKA5KA/9OQJVQTp4MIg62uKlOtripTp4MIg6AlVBOigP/TmbpKA5vUVMOdNVBDmBBrA44iOnODZP+DgQsDw5UXmROYyk3znSKCY6HsVjOngwiDp4MIg6HsVjOtIoJjqMpN85UXmRORCwPDk2T/g44iOnONfe2zizcyI50OBxOQ9QsjkoD/050igmOgJVQToCVUE60igmOigP/TkPULI50OBxObNzIjnX3ts4aqa5ONNVBDkQsDw5V1uEOQ9QsjmMpN85KA/9OSgP/TmMpN85D1CyOVdbhDkQsDw501UEOWqmuTjid884bFwNORCwPDnQ4HE5UXmROZukoDmbpKA5UXmROdDgcTkQsDw5bFwNOeJ3zzjid88401UEObNzIjkQsDw5vUVMOb1FTDkQsDw5s3MiOdNVBDnid884aqa5ONfe2zg2T/g401UEOdNVBDk2T/g4197bOGqmuTjiI6c4gQawOIEGsDjiI6c4mIw3NKPJUznqLmI0gZ9uOeouYjSjyVM5mIw3NF45KjkCa1Y0+nyHOXNSlTR7rrM5E4anNHuuszlzUpU0+nyHOQJrVjReOSo5XjkqOeouYjQVPJs5dYGxNJrh+TkGDd80x8kWOgYN3zSa4fk5dYGxNBU8mznqLmI0XjkqOQJrVjQVPJs5fhS8NMfJFjo7SwI1behpOoW3DTVt6Gk6O0sCNcfJFjp+FLw0FTybOQJrVjSYjDc0+nyHOXWBsTTHyRY6r0MINX/plzqrhxc1DTHNOquHFzV/6Zc6r0MINcfJFjp1gbE0+nyHOZiMNzSjyVM5c1KVNJrh+Tk7SwI1f+mXOu2NFzVaQhI7pDAVNVpCEjvtjRc1f+mXOjtLAjWa4fk5c1KVNKPJUznqLmI0e66zOQYN3zRt6Gk6q4cXNVpCEjvwcRo1E3JiO/BxGjVaQhI7q4cXNW3oaToGDd80e66zOeouYjSBn245E4anNMfJFjqFtw01DTHNOqQwFTUTcmI7o7c3NRNyYjukMBU1DTHNOoW3DTXHyRY6E4anNIGfbjnqLmI0e66zOQYN3zRt6Gk6q4cXNVpCEjvwcRo1E3JiO/BxGjVaQhI7q4cXNW3oaToGDd80e66zOeouYjSjyVM5c1KVNJrh+Tk7SwI1f+mXOu2NFzVaQhI7pDAVNVpCEjvtjRc1f+mXOjtLAjWa4fk5c1KVNKPJUzmYjDc0+nyHOXWBsTTHyRY6r0MINX/plzqrhxc1DTHNOquHFzV/6Zc6r0MINcfJFjp1gbE0+nyHOZiMNzQCa1Y0FTybOX4UvDTHyRY6O0sCNW3oaTqFtw01behpOjtLAjXHyRY6fhS8NBU8mzkCa1Y0XjkqOeouYjQVPJs5dYGxNJrh+TkGDd80x8kWOgYN3zSa4fk5dYGxNBU8mznqLmI0XjkqOV45KjkCa1Y0+nyHOXNSlTR7rrM5E4anNHuuszlzUpU0+nyHOQJrVjReOSo5mIw3NKPJUznqLmI0gZ9uOeouYjSjyVM5mIw3NIEGsDhEGcQ44nfPOOJ3zzhEGcQ4gQawOIEGsDjX3ts401UEOThaFzmzcyI5s3MiOThaFznTVQQ5197bOIEGsDhqprk4Nk/4OLNzIjm9RUw50OBxOVdbhDlXW4Q50OBxOb1FTDmzcyI5Nk/4OGqmuTiBBrA4Nk/4OE3ULjnQ4HE5m6SgOaEPxzmMpN85jKTfOaEPxzmbpKA50OBxOU3ULjk2T/g4gQawONfe2zizcyI50OBxOQ9QsjkoD/050igmOgJVQToCVUE60igmOigP/TkPULI50OBxObNzIjnX3ts4gQawONNVBDm9RUw5m6SgOSgP/TkCVUE6eDCIOtripTra4qU6eDCIOgJVQTooD/05m6SgOb1FTDnTVQQ5gQawOEQZxDg4Whc50OBxOaEPxznSKCY6eDCIOlPjzjqlPAU7pTwFO1Pjzjp4MIg60igmOqEPxznQ4HE5OFoXOUQZxDjid884s3MiOVdbhDmMpN85AlVBOtripTqlPAU7oRY0O6EWNDulPAU72uKlOgJVQTqMpN85V1uEObNzIjnid8844nfPOLNzIjlXW4Q5jKTfOQJVQTra4qU6pTwFO6EWNDuhFjQ7pTwFO9ripToCVUE6jKTfOVdbhDmzcyI54nfPOEQZxDg4Whc50OBxOaEPxznSKCY6eDCIOlPjzjqlPAU7pTwFO1Pjzjp4MIg60igmOqEPxznQ4HE5OFoXOUQZxDiBBrA401UEOb1FTDmbpKA5KA/9OQJVQTp4MIg62uKlOtripTp4MIg6AlVBOigP/TmbpKA5vUVMOdNVBDmBBrA4197bOLNzIjnQ4HE5D1CyOSgP/TnSKCY6AlVBOgJVQTrSKCY6KA/9OQ9QsjnQ4HE5s3MiOdfe2ziBBrA4Nk/4OE3ULjnQ4HE5m6SgOaEPxzmMpN85jKTfOaEPxzmbpKA50OBxOU3ULjk2T/g4gQawOGqmuTg2T/g4s3MiOb1FTDnQ4HE5V1uEOVdbhDnQ4HE5vUVMObNzIjk2T/g4aqa5OIEGsDjX3ts401UEOThaFzmzcyI5s3MiOThaFznTVQQ5197bOIEGsDiBBrA4RBnEOOJ3zzjid884RBnEOIEGsDiRkC40cV4zOZGQLjSRkC40o8lTOVzHbjT6fIc5I1mFNPp8hzlcx240o8lTOZGQLjRVJ0E0UQV+OXNSlTSHJcI5fhS8NJTj5Dl+FLw0hyXCOXNSlTRRBX45VSdBNFUnQTT6fIc5E4anNJrh+TmfhOs0gZI5OjtLAjWBkjk6n4TrNJrh+TkThqc0+nyHOVUnQTSRkC40UQV+OROGpzQY9Ag6O0sCNZy6hDq7uBU16Y+vOru4FTWcuoQ6O0sCNRj0CDoThqc0UQV+OZGQLjSjyVM5c1KVNJrh+Tk7SwI1f+mXOu2NFzVaQhI7pDAVNVpCEjvtjRc1f+mXOjtLAjWa4fk5c1KVNKPJUzlcx240hyXCOZ+E6zScuoQ67Y0XNXucMzujtzc14x6UO6O3NzV7nDM77Y0XNZy6hDqfhOs0hyXCOVzHbjSRkC40+nyHOX4UvDSBkjk6u7gVNVpCEjujtzc1ZlPNO+0FWTZmU807o7c3NVpCEju7uBU1gZI5On4UvDT6fIc5kZAuNHFeMzkjWYU0lOPkOTtLAjXpj686pDAVNeMelDvtBVk2cGoePO0FWTbjHpQ7pDAVNemPrzo7SwI1lOPkOSNZhTRxXjM5kZAuNPp8hzl+FLw0gZI5Oru4FTVaQhI7o7c3NWZTzTvtBVk2ZlPNO6O3NzVaQhI7u7gVNYGSOTp+FLw0+nyHOZGQLjRcx240hyXCOZ+E6zScuoQ67Y0XNXucMzujtzc14x6UO6O3NzV7nDM77Y0XNZy6hDqfhOs0hyXCOVzHbjSjyVM5c1KVNJrh+Tk7SwI1f+mXOu2NFzVaQhI7pDAVNVpCEjvtjRc1f+mXOjtLAjWa4fk5c1KVNKPJUzmRkC40UQV+OROGpzQY9Ag6O0sCNZy6hDq7uBU16Y+vOru4FTWcuoQ6O0sCNRj0CDoThqc0UQV+OZGQLjRVJ0E0+nyHOROGpzSa4fk5n4TrNIGSOTo7SwI1gZI5Op+E6zSa4fk5E4anNPp8hzlVJ0E0VSdBNFEFfjlzUpU0hyXCOX4UvDSU4+Q5fhS8NIclwjlzUpU0UQV+OVUnQTSRkC40o8lTOVzHbjT6fIc5I1mFNPp8hzlcx240o8lTOZGQLjSRkC40cV4zOZGQLjTiI6c4RBnEONfe2zgsb+k4LG/pONfe2zhEGcQ44iOnOEQZxDg2T/g4OFoXOU3ULjkQsDw5ELA8OU3ULjk4Whc5Nk/4OEQZxDjid884bFwNORCwPDnQ4HE5UXmROZukoDmbpKA5UXmROdDgcTkQsDw5bFwNOeJ3zzhEGcQ4bFwNOb1FTDlReZE5oQ/HOSgP/TnZVBA62VQQOigP/TmhD8c5UXmROb1FTDlsXA05RBnEOOIjpzg2T/g4ELA8OVF5kTmMpN850igmOh7FYzp4MIg6eDCIOh7FYzrSKCY6jKTfOVF5kTkQsDw5Nk/4OOIjpzhEGcQ4OFoXOdDgcTmhD8c50igmOngwiDpT4846pTwFO6U8BTtT4846eDCIOtIoJjqhD8c50OBxOThaFzlEGcQ4197bOE3ULjlReZE5KA/9OR7FYzpT4846oRY0O1pFhDtaRYQ7oRY0O1PjzjoexWM6KA/9OVF5kTlN1C45197bOCxv6TgQsDw5m6SgOdlUEDp4MIg6pTwFO1pFhDv6Zug7+mboO1pFhDulPAU7eDCIOtlUEDqbpKA5ELA8OSxv6Tgsb+k4ELA8OZukoDnZVBA6eDCIOqU8BTtaRYQ7+mboO/pm6DtaRYQ7pTwFO3gwiDrZVBA6m6SgORCwPDksb+k4197bOE3ULjlReZE5KA/9OR7FYzpT4846oRY0O1pFhDtaRYQ7oRY0O1PjzjoexWM6KA/9OVF5kTlN1C45197bOEQZxDg4Whc50OBxOaEPxznSKCY6eDCIOlPjzjqlPAU7pTwFO1Pjzjp4MIg60igmOqEPxznQ4HE5OFoXOUQZxDjiI6c4Nk/4OBCwPDlReZE5jKTfOdIoJjoexWM6eDCIOngwiDoexWM60igmOoyk3zlReZE5ELA8OTZP+DjiI6c4RBnEOGxcDTm9RUw5UXmROaEPxzkoD/052VQQOtlUEDooD/05oQ/HOVF5kTm9RUw5bFwNOUQZxDjid884bFwNORCwPDnQ4HE5UXmROZukoDmbpKA5UXmROdDgcTkQsDw5bFwNOeJ3zzhEGcQ4Nk/4ODhaFzlN1C45ELA8ORCwPDlN1C45OFoXOTZP+DhEGcQ44iOnOEQZxDjX3ts4LG/pOCxv6TjX3ts4RBnEOOIjpziRkC40cEg9OVUnQTRwSD05kZAuNHBIPTnqLmI0+nyHOaIRjTQVPJs5ohGNNPp8hznqLmI0cEg9OaPJUzkjWYU0e66zOX4UvDSa4fk5penSNJrh+Tl+FLw0e66zOSNZhTSjyVM5o8lTOaIRjTQtcdI5Bg3fNIGSOTqvQwg1behpOq9DCDWBkjk6Bg3fNC1x0jmiEY00o8lTOXBIPTkjWYU0LXHSOZ+E6zRt6Gk6u7gVNQ0xzTrtjRc1DTHNOru4FTVt6Gk6n4TrNC1x0jkjWYU0cEg9OeouYjR7rrM5Bg3fNG3oaTqrhxc1WkISO/BxGjUTcmI78HEaNVpCEjurhxc1behpOgYN3zR7rrM56i5iNJGQLjT6fIc5fhS8NIGSOTq7uBU1WkISO6O3NzVmU8077QVZNmZTzTujtzc1WkISO7u4FTWBkjk6fhS8NPp8hzmRkC40cEg9OaIRjTSa4fk5r0MINQ0xzTrwcRo1ZlPNO9xaizdxHp483FqLN2ZTzTvwcRo1DTHNOq9DCDWa4fk5ohGNNHBIPTlVJ0E0FTybOaXp0jRt6Gk67Y0XNRNyYjvtBVk2cR6ePPb58jhxHp487QVZNhNyYjvtjRc1behpOqXp0jQVPJs5VSdBNHBIPTmiEY00muH5Oa9DCDUNMc068HEaNWZTzTvcWos3cR6ePNxaizdmU8078HEaNQ0xzTqvQwg1muH5OaIRjTRwSD05kZAuNPp8hzl+FLw0gZI5Oru4FTVaQhI7o7c3NWZTzTvtBVk2ZlPNO6O3NzVaQhI7u7gVNYGSOTp+FLw0+nyHOZGQLjTqLmI0e66zOQYN3zRt6Gk6q4cXNVpCEjvwcRo1E3JiO/BxGjVaQhI7q4cXNW3oaToGDd80e66zOeouYjRwSD05I1mFNC1x0jmfhOs0behpOru4FTUNMc067Y0XNQ0xzTq7uBU1behpOp+E6zQtcdI5I1mFNHBIPTmjyVM5ohGNNC1x0jkGDd80gZI5Oq9DCDVt6Gk6r0MINYGSOToGDd80LXHSOaIRjTSjyVM5o8lTOSNZhTR7rrM5fhS8NJrh+Tml6dI0muH5OX4UvDR7rrM5I1mFNKPJUzlwSD056i5iNPp8hzmiEY00FTybOaIRjTT6fIc56i5iNHBIPTmRkC40cEg9OVUnQTRwSD05kZAuNIEGsDjid884LG/pODZP+Dg2T/g4LG/pOOJ3zziBBrA44nfPONNVBDmzcyI5ELA8Ob1FTDm9RUw5ELA8ObNzIjnTVQQ54nfPONfe2zg4Whc5vUVMOVdbhDmbpKA5D1CyOQ9QsjmbpKA5V1uEOb1FTDk4Whc5197bOOJ3zzg4Whc5juFdOZukoDmMpN852VQQOtIoJjrSKCY62VQQOoyk3zmbpKA5juFdOThaFznid884gQawONNVBDm9RUw5m6SgOSgP/TkCVUE6eDCIOtripTra4qU6eDCIOgJVQTooD/05m6SgOb1FTDnTVQQ5gQawOOJ3zzizcyI5V1uEOYyk3zkCVUE62uKlOqU8BTuhFjQ7oRY0O6U8BTva4qU6AlVBOoyk3zlXW4Q5s3MiOeJ3zzgsb+k4ELA8OZukoDnZVBA6eDCIOqU8BTtaRYQ7+mboO/pm6DtaRYQ7pTwFO3gwiDrZVBA6m6SgORCwPDksb+k4Nk/4OL1FTDkPULI50igmOtripTqhFjQ7+mboO9LpsDzS6bA8+mboO6EWNDva4qU60igmOg9Qsjm9RUw5Nk/4ODZP+Di9RUw5D1CyOdIoJjra4qU6oRY0O/pm6DvS6bA80umwPPpm6DuhFjQ72uKlOtIoJjoPULI5vUVMOTZP+Dgsb+k4ELA8OZukoDnZVBA6eDCIOqU8BTtaRYQ7+mboO/pm6DtaRYQ7pTwFO3gwiDrZVBA6m6SgORCwPDksb+k44nfPOLNzIjlXW4Q5jKTfOQJVQTra4qU6pTwFO6EWNDuhFjQ7pTwFO9ripToCVUE6jKTfOVdbhDmzcyI54nfPOIEGsDjTVQQ5vUVMOZukoDkoD/05AlVBOngwiDra4qU62uKlOngwiDoCVUE6KA/9OZukoDm9RUw501UEOYEGsDjid884OFoXOY7hXTmbpKA5jKTfOdlUEDrSKCY60igmOtlUEDqMpN85m6SgOY7hXTk4Whc54nfPONfe2zg4Whc5vUVMOVdbhDmbpKA5D1CyOQ9QsjmbpKA5V1uEOb1FTDk4Whc5197bOOJ3zzjTVQQ5s3MiORCwPDm9RUw5vUVMORCwPDmzcyI501UEOeJ3zziBBrA44nfPOCxv6Tg2T/g4Nk/4OCxv6Tjid884gQawOHFeMzlVJ0E0Hw5IOVUnQTRxXjM5VSdBNIGfbjkjWYU0FTybOXNSlTQVPJs5I1mFNIGfbjlVJ0E0AmtWNIXakDkThqc0lOPkOaXp0jQY9Ag6penSNJTj5DkThqc0hdqQOQJrVjQCa1Y0FTybOX4UvDTHyRY6O0sCNW3oaTqFtw01behpOjtLAjXHyRY6fhS8NBU8mzkCa1Y0VSdBNIXakDl+FLw0idQmOoW3DTXpj6867Y0XNVwI8zrtjRc16Y+vOoW3DTWJ1CY6fhS8NIXakDlVJ0E0gZ9uOROGpzTHyRY6hbcNNQ0xzTqkMBU1E3JiO6O3NzUTcmI7pDAVNQ0xzTqFtw01x8kWOhOGpzSBn245cV4zOSNZhTSU4+Q5O0sCNemPrzqkMBU14x6UO+0FWTZwah487QVZNuMelDukMBU16Y+vOjtLAjWU4+Q5I1mFNHFeMzlVJ0E0FTybOaXp0jRt6Gk67Y0XNRNyYjvtBVk2cR6ePPb58jhxHp487QVZNhNyYjvtjRc1behpOqXp0jQVPJs5VSdBNB8OSDlzUpU0GPQIOoW3DTVcCPM6o7c3NXBqHjz2+fI49vnyOHBqHjyjtzc1XAjzOoW3DTUY9Ag6c1KVNB8OSDlVJ0E0FTybOaXp0jRt6Gk67Y0XNRNyYjvtBVk2cR6ePPb58jhxHp487QVZNhNyYjvtjRc1behpOqXp0jQVPJs5VSdBNHFeMzkjWYU0lOPkOTtLAjXpj686pDAVNeMelDvtBVk2cGoePO0FWTbjHpQ7pDAVNemPrzo7SwI1lOPkOSNZhTRxXjM5gZ9uOROGpzTHyRY6hbcNNQ0xzTqkMBU1E3JiO6O3NzUTcmI7pDAVNQ0xzTqFtw01x8kWOhOGpzSBn245VSdBNIXakDl+FLw0idQmOoW3DTXpj6867Y0XNVwI8zrtjRc16Y+vOoW3DTWJ1CY6fhS8NIXakDlVJ0E0AmtWNBU8mzl+FLw0x8kWOjtLAjVt6Gk6hbcNNW3oaTo7SwI1x8kWOn4UvDQVPJs5AmtWNAJrVjSF2pA5E4anNJTj5Dml6dI0GPQIOqXp0jSU4+Q5E4anNIXakDkCa1Y0VSdBNIGfbjkjWYU0FTybOXNSlTQVPJs5I1mFNIGfbjlVJ0E0cV4zOVUnQTQfDkg5VSdBNHFeMzmBBrA44nfPOCxv6Tg2T/g4Nk/4OCxv6Tjid884gQawOOJ3zzjTVQQ5s3MiORCwPDm9RUw5vUVMORCwPDmzcyI501UEOeJ3zzjX3ts4OFoXOb1FTDlXW4Q5m6SgOQ9QsjkPULI5m6SgOVdbhDm9RUw5OFoXOdfe2zjid884OFoXOY7hXTmbpKA5jKTfOdlUEDrSKCY60igmOtlUEDqMpN85m6SgOY7hXTk4Whc54nfPOIEGsDjTVQQ5vUVMOZukoDkoD/05AlVBOngwiDra4qU62uKlOngwiDoCVUE6KA/9OZukoDm9RUw501UEOYEGsDjid884s3MiOVdbhDmMpN85AlVBOtripTqlPAU7oRY0O6EWNDulPAU72uKlOgJVQTqMpN85V1uEObNzIjnid884LG/pOBCwPDmbpKA52VQQOngwiDqlPAU7WkWEO/pm6Dv6Zug7WkWEO6U8BTt4MIg62VQQOpukoDkQsDw5LG/pODZP+Di9RUw5D1CyOdIoJjra4qU6oRY0O/pm6DvS6bA80umwPPpm6DuhFjQ72uKlOtIoJjoPULI5vUVMOTZP+Dg2T/g4vUVMOQ9QsjnSKCY62uKlOqEWNDv6Zug70umwPNLpsDz6Zug7oRY0O9ripTrSKCY6D1CyOb1FTDk2T/g4LG/pOBCwPDmbpKA52VQQOngwiDqlPAU7WkWEO/pm6Dv6Zug7WkWEO6U8BTt4MIg62VQQOpukoDkQsDw5LG/pOOJ3zzizcyI5V1uEOYyk3zkCVUE62uKlOqU8BTuhFjQ7oRY0O6U8BTva4qU6AlVBOoyk3zlXW4Q5s3MiOeJ3zziBBrA401UEOb1FTDmbpKA5KA/9OQJVQTp4MIg62uKlOtripTp4MIg6AlVBOigP/TmbpKA5vUVMOdNVBDmBBrA44nfPODhaFzmO4V05m6SgOYyk3znZVBA60igmOtIoJjrZVBA6jKTfOZukoDmO4V05OFoXOeJ3zzjX3ts4OFoXOb1FTDlXW4Q5m6SgOQ9QsjkPULI5m6SgOVdbhDm9RUw5OFoXOdfe2zjid88401UEObNzIjkQsDw5vUVMOb1FTDkQsDw5s3MiOdNVBDnid884gQawOOJ3zzgsb+k4Nk/4ODZP+Dgsb+k44nfPOIEGsDiRkC40cEg9OVUnQTRwSD05kZAuNHBIPTnqLmI0+nyHOaIRjTQVPJs5ohGNNPp8hznqLmI0cEg9OaPJUzkjWYU0e66zOX4UvDSa4fk5penSNJrh+Tl+FLw0e66zOSNZhTSjyVM5o8lTOaIRjTQtcdI5Bg3fNIGSOTqvQwg1behpOq9DCDWBkjk6Bg3fNC1x0jmiEY00o8lTOXBIPTkjWYU0LXHSOZ+E6zRt6Gk6u7gVNQ0xzTrtjRc1DTHNOru4FTVt6Gk6n4TrNC1x0jkjWYU0cEg9OeouYjR7rrM5Bg3fNG3oaTqrhxc1WkISO/BxGjUTcmI78HEaNVpCEjurhxc1behpOgYN3zR7rrM56i5iNJGQLjT6fIc5fhS8NIGSOTq7uBU1WkISO6O3NzVmU8077QVZNmZTzTujtzc1WkISO7u4FTWBkjk6fhS8NPp8hzmRkC40cEg9OaIRjTSa4fk5r0MINQ0xzTrwcRo1ZlPNO9xaizdxHp483FqLN2ZTzTvwcRo1DTHNOq9DCDWa4fk5ohGNNHBIPTlVJ0E0FTybOaXp0jRt6Gk67Y0XNRNyYjvtBVk2cR6ePPb58jhxHp487QVZNhNyYjvtjRc1behpOqXp0jQVPJs5VSdBNHBIPTmiEY00muH5Oa9DCDUNMc068HEaNWZTzTvcWos3cR6ePNxaizdmU8078HEaNQ0xzTqvQwg1muH5OaIRjTRwSD05kZAuNPp8hzl+FLw0gZI5Oru4FTVaQhI7o7c3NWZTzTvtBVk2ZlPNO6O3NzVaQhI7u7gVNYGSOTp+FLw0+nyHOZGQLjTqLmI0e66zOQYN3zRt6Gk6q4cXNVpCEjvwcRo1E3JiO/BxGjVaQhI7q4cXNW3oaToGDd80e66zOeouYjRwSD05I1mFNC1x0jmfhOs0behpOru4FTUNMc067Y0XNQ0xzTq7uBU1behpOp+E6zQtcdI5I1mFNHBIPTmjyVM5ohGNNC1x0jkGDd80gZI5Oq9DCDVt6Gk6r0MINYGSOToGDd80LXHSOaIRjTSjyVM5o8lTOSNZhTR7rrM5fhS8NJrh+Tml6dI0muH5OX4UvDR7rrM5I1mFNKPJUzlwSD056i5iNPp8hzmiEY00FTybOaIRjTT6fIc56i5iNHBIPTmRkC40cEg9OVUnQTRwSD05kZAuNOIjpzhEGcQ4197bOCxv6Tgsb+k4197bOEQZxDjiI6c4RBnEODZP+Dg4Whc5TdQuORCwPDkQsDw5TdQuOThaFzk2T/g4RBnEOOJ3zzhsXA05ELA8OdDgcTlReZE5m6SgOZukoDlReZE50OBxORCwPDlsXA054nfPOEQZxDhsXA05vUVMOVF5kTmhD8c5KA/9OdlUEDrZVBA6KA/9OaEPxzlReZE5vUVMOWxcDTlEGcQ44iOnODZP+DgQsDw5UXmROYyk3znSKCY6HsVjOngwiDp4MIg6HsVjOtIoJjqMpN85UXmRORCwPDk2T/g44iOnOEQZxDg4Whc50OBxOaEPxznSKCY6eDCIOlPjzjqlPAU7pTwFO1Pjzjp4MIg60igmOqEPxznQ4HE5OFoXOUQZxDjX3ts4TdQuOVF5kTkoD/05HsVjOlPjzjqhFjQ7WkWEO1pFhDuhFjQ7U+POOh7FYzooD/05UXmROU3ULjnX3ts4LG/pOBCwPDmbpKA52VQQOngwiDqlPAU7WkWEO/pm6Dv6Zug7WkWEO6U8BTt4MIg62VQQOpukoDkQsDw5LG/pOCxv6TgQsDw5m6SgOdlUEDp4MIg6pTwFO1pFhDv6Zug7+mboO1pFhDulPAU7eDCIOtlUEDqbpKA5ELA8OSxv6TjX3ts4TdQuOVF5kTkoD/05HsVjOlPjzjqhFjQ7WkWEO1pFhDuhFjQ7U+POOh7FYzooD/05UXmROU3ULjnX3ts4RBnEODhaFznQ4HE5oQ/HOdIoJjp4MIg6U+POOqU8BTulPAU7U+POOngwiDrSKCY6oQ/HOdDgcTk4Whc5RBnEOOIjpzg2T/g4ELA8OVF5kTmMpN850igmOh7FYzp4MIg6eDCIOh7FYzrSKCY6jKTfOVF5kTkQsDw5Nk/4OOIjpzhEGcQ4bFwNOb1FTDlReZE5oQ/HOSgP/TnZVBA62VQQOigP/TmhD8c5UXmROb1FTDlsXA05RBnEOOJ3zzhsXA05ELA8OdDgcTlReZE5m6SgOZukoDlReZE50OBxORCwPDlsXA054nfPOEQZxDg2T/g4OFoXOU3ULjkQsDw5ELA8OU3ULjk4Whc5Nk/4OEQZxDjiI6c4RBnEONfe2zgsb+k4LG/pONfe2zhEGcQ44iOnOJGQLjRxXjM5kZAuNJGQLjSjyVM5XMduNPp8hzkjWYU0+nyHOVzHbjSjyVM5kZAuNFUnQTRRBX45c1KVNIclwjl+FLw0lOPkOX4UvDSHJcI5c1KVNFEFfjlVJ0E0VSdBNPp8hzkThqc0muH5OZ+E6zSBkjk6O0sCNYGSOTqfhOs0muH5OROGpzT6fIc5VSdBNJGQLjRRBX45E4anNBj0CDo7SwI1nLqEOru4FTXpj686u7gVNZy6hDo7SwI1GPQIOhOGpzRRBX45kZAuNKPJUzlzUpU0muH5OTtLAjV/6Zc67Y0XNVpCEjukMBU1WkISO+2NFzV/6Zc6O0sCNZrh+TlzUpU0o8lTOVzHbjSHJcI5n4TrNJy6hDrtjRc1e5wzO6O3NzXjHpQ7o7c3NXucMzvtjRc1nLqEOp+E6zSHJcI5XMduNJGQLjT6fIc5fhS8NIGSOTq7uBU1WkISO6O3NzVmU8077QVZNmZTzTujtzc1WkISO7u4FTWBkjk6fhS8NPp8hzmRkC40cV4zOSNZhTSU4+Q5O0sCNemPrzqkMBU14x6UO+0FWTZwah487QVZNuMelDukMBU16Y+vOjtLAjWU4+Q5I1mFNHFeMzmRkC40+nyHOX4UvDSBkjk6u7gVNVpCEjujtzc1ZlPNO+0FWTZmU807o7c3NVpCEju7uBU1gZI5On4UvDT6fIc5kZAuNFzHbjSHJcI5n4TrNJy6hDrtjRc1e5wzO6O3NzXjHpQ7o7c3NXucMzvtjRc1nLqEOp+E6zSHJcI5XMduNKPJUzlzUpU0muH5OTtLAjV/6Zc67Y0XNVpCEjukMBU1WkISO+2NFzV/6Zc6O0sCNZrh+TlzUpU0o8lTOZGQLjRRBX45E4anNBj0CDo7SwI1nLqEOru4FTXpj686u7gVNZy6hDo7SwI1GPQIOhOGpzRRBX45kZAuNFUnQTT6fIc5E4anNJrh+TmfhOs0gZI5OjtLAjWBkjk6n4TrNJrh+TkThqc0+nyHOVUnQTRVJ0E0UQV+OXNSlTSHJcI5fhS8NJTj5Dl+FLw0hyXCOXNSlTRRBX45VSdBNJGQLjSjyVM5XMduNPp8hzkjWYU0+nyHOVzHbjSjyVM5kZAuNJGQLjRxXjM5kZAuNIEGsDhEGcQ44nfPOOJ3zzhEGcQ4gQawOIEGsDjX3ts401UEOThaFzmzcyI5s3MiOThaFznTVQQ5197bOIEGsDhqprk4Nk/4OLNzIjm9RUw50OBxOVdbhDlXW4Q50OBxOb1FTDmzcyI5Nk/4OGqmuTiBBrA4Nk/4OE3ULjnQ4HE5m6SgOaEPxzmMpN85jKTfOaEPxzmbpKA50OBxOU3ULjk2T/g4gQawONfe2zizcyI50OBxOQ9QsjkoD/050igmOgJVQToCVUE60igmOigP/TkPULI50OBxObNzIjnX3ts4gQawONNVBDm9RUw5m6SgOSgP/TkCVUE6eDCIOtripTra4qU6eDCIOgJVQTooD/05m6SgOb1FTDnTVQQ5gQawOEQZxDg4Whc50OBxOaEPxznSKCY6eDCIOlPjzjqlPAU7pTwFO1Pjzjp4MIg60igmOqEPxznQ4HE5OFoXOUQZxDjid884s3MiOVdbhDmMpN85AlVBOtripTqlPAU7oRY0O6EWNDulPAU72uKlOgJVQTqMpN85V1uEObNzIjnid8844nfPOLNzIjlXW4Q5jKTfOQJVQTra4qU6pTwFO6EWNDuhFjQ7pTwFO9ripToCVUE6jKTfOVdbhDmzcyI54nfPOEQZxDg4Whc50OBxOaEPxznSKCY6eDCIOlPjzjqlPAU7pTwFO1Pjzjp4MIg60igmOqEPxznQ4HE5OFoXOUQZxDiBBrA401UEOb1FTDmbpKA5KA/9OQJVQTp4MIg62uKlOtripTp4MIg6AlVBOigP/TmbpKA5vUVMOdNVBDmBBrA4197bOLNzIjnQ4HE5D1CyOSgP/TnSKCY6AlVBOgJVQTrSKCY6KA/9OQ9QsjnQ4HE5s3MiOdfe2ziBBrA4Nk/4OE3ULjnQ4HE5m6SgOaEPxzmMpN85jKTfOaEPxzmbpKA50OBxOU3ULjk2T/g4gQawOGqmuTg2T/g4s3MiOb1FTDnQ4HE5V1uEOVdbhDnQ4HE5vUVMObNzIjk2T/g4aqa5OIEGsDjX3ts401UEOThaFzmzcyI5s3MiOThaFznTVQQ5197bOIEGsDiBBrA4RBnEOOJ3zzjid884RBnEOIEGsDiYjDc0o8lTOeouYjSBn2456i5iNKPJUzmYjDc0XjkqOQJrVjT6fIc5c1KVNHuuszkThqc0e66zOXNSlTT6fIc5AmtWNF45KjleOSo56i5iNBU8mzl1gbE0muH5OQYN3zTHyRY6Bg3fNJrh+Tl1gbE0FTybOeouYjReOSo5AmtWNBU8mzl+FLw0x8kWOjtLAjVt6Gk6hbcNNW3oaTo7SwI1x8kWOn4UvDQVPJs5AmtWNJiMNzT6fIc5dYGxNMfJFjqvQwg1f+mXOquHFzUNMc06q4cXNX/plzqvQwg1x8kWOnWBsTT6fIc5mIw3NKPJUzlzUpU0muH5OTtLAjV/6Zc67Y0XNVpCEjukMBU1WkISO+2NFzV/6Zc6O0sCNZrh+TlzUpU0o8lTOeouYjR7rrM5Bg3fNG3oaTqrhxc1WkISO/BxGjUTcmI78HEaNVpCEjurhxc1behpOgYN3zR7rrM56i5iNIGfbjkThqc0x8kWOoW3DTUNMc06pDAVNRNyYjujtzc1E3JiO6QwFTUNMc06hbcNNcfJFjoThqc0gZ9uOeouYjR7rrM5Bg3fNG3oaTqrhxc1WkISO/BxGjUTcmI78HEaNVpCEjurhxc1behpOgYN3zR7rrM56i5iNKPJUzlzUpU0muH5OTtLAjV/6Zc67Y0XNVpCEjukMBU1WkISO+2NFzV/6Zc6O0sCNZrh+TlzUpU0o8lTOZiMNzT6fIc5dYGxNMfJFjqvQwg1f+mXOquHFzUNMc06q4cXNX/plzqvQwg1x8kWOnWBsTT6fIc5mIw3NAJrVjQVPJs5fhS8NMfJFjo7SwI1behpOoW3DTVt6Gk6O0sCNcfJFjp+FLw0FTybOQJrVjReOSo56i5iNBU8mzl1gbE0muH5OQYN3zTHyRY6Bg3fNJrh+Tl1gbE0FTybOeouYjReOSo5XjkqOQJrVjT6fIc5c1KVNHuuszkThqc0e66zOXNSlTT6fIc5AmtWNF45KjmYjDc0o8lTOeouYjSBn2456i5iNKPJUzmYjDc04iOnOIEGsDiBBrA44iOnOGqmuTjX3ts4Nk/4ONNVBDnTVQQ5Nk/4ONfe2zhqprk44nfPONNVBDmzcyI5ELA8Ob1FTDm9RUw5ELA8ObNzIjnTVQQ54nfPOOJ3zzhsXA05ELA8OdDgcTlReZE5m6SgOZukoDlReZE50OBxORCwPDlsXA054nfPOGqmuTjTVQQ5ELA8OVdbhDkPULI5jKTfOSgP/TkoD/05jKTfOQ9QsjlXW4Q5ELA8OdNVBDlqprk4197bOLNzIjnQ4HE5D1CyOSgP/TnSKCY6AlVBOgJVQTrSKCY6KA/9OQ9QsjnQ4HE5s3MiOdfe2zjiI6c4Nk/4OBCwPDlReZE5jKTfOdIoJjoexWM6eDCIOngwiDoexWM60igmOoyk3zlReZE5ELA8OTZP+DjiI6c4gQawONNVBDm9RUw5m6SgOSgP/TkCVUE6eDCIOtripTra4qU6eDCIOgJVQTooD/05m6SgOb1FTDnTVQQ5gQawOIEGsDjTVQQ5vUVMOZukoDkoD/05AlVBOngwiDra4qU62uKlOngwiDoCVUE6KA/9OZukoDm9RUw501UEOYEGsDjiI6c4Nk/4OBCwPDlReZE5jKTfOdIoJjoexWM6eDCIOngwiDoexWM60igmOoyk3zlReZE5ELA8OTZP+DjiI6c4197bOLNzIjnQ4HE5D1CyOSgP/TnSKCY6AlVBOgJVQTrSKCY6KA/9OQ9QsjnQ4HE5s3MiOdfe2zhqprk401UEORCwPDlXW4Q5D1CyOYyk3zkoD/05KA/9OYyk3zkPULI5V1uEORCwPDnTVQQ5aqa5OOJ3zzhsXA05ELA8OdDgcTlReZE5m6SgOZukoDlReZE50OBxORCwPDlsXA054nfPOOJ3zzjTVQQ5s3MiORCwPDm9RUw5vUVMORCwPDmzcyI501UEOeJ3zzhqprk4197bODZP+DjTVQQ501UEOTZP+DjX3ts4aqa5OOIjpziBBrA4gQawOOIjpziRkC40cEg9OVUnQTRwSD05kZAuNHFeMzkCa1Y0UQV+OSNZhTSF2pA5I1mFNFEFfjkCa1Y0cV4zOXBIPTlcx240FTybOROGpzQtcdI5fhS8NC1x0jkThqc0FTybOVzHbjRwSD05cV4zOVzHbjSXyKY5fhS8NBj0CDqfhOs0idQmOp+E6zQY9Ag6fhS8NJfIpjlcx240cV4zOQJrVjQVPJs5fhS8NMfJFjo7SwI1behpOoW3DTVt6Gk6O0sCNcfJFjp+FLw0FTybOQJrVjSRkC40UQV+OROGpzQY9Ag6O0sCNZy6hDq7uBU16Y+vOru4FTWcuoQ6O0sCNRj0CDoThqc0UQV+OZGQLjRwSD05I1mFNC1x0jmfhOs0behpOru4FTUNMc067Y0XNQ0xzTq7uBU1behpOp+E6zQtcdI5I1mFNHBIPTlVJ0E0hdqQOX4UvDSJ1CY6hbcNNemPrzrtjRc1XAjzOu2NFzXpj686hbcNNYnUJjp+FLw0hdqQOVUnQTRwSD05I1mFNC1x0jmfhOs0behpOru4FTUNMc067Y0XNQ0xzTq7uBU1behpOp+E6zQtcdI5I1mFNHBIPTmRkC40UQV+OROGpzQY9Ag6O0sCNZy6hDq7uBU16Y+vOru4FTWcuoQ6O0sCNRj0CDoThqc0UQV+OZGQLjQCa1Y0FTybOX4UvDTHyRY6O0sCNW3oaTqFtw01behpOjtLAjXHyRY6fhS8NBU8mzkCa1Y0cV4zOVzHbjSXyKY5fhS8NBj0CDqfhOs0idQmOp+E6zQY9Ag6fhS8NJfIpjlcx240cV4zOXBIPTlcx240FTybOROGpzQtcdI5fhS8NC1x0jkThqc0FTybOVzHbjRwSD05cV4zOQJrVjRRBX45I1mFNIXakDkjWYU0UQV+OQJrVjRxXjM5kZAuNHBIPTlVJ0E0cEg9OZGQLjSBBrA4RBnEOOJ3zzjid884RBnEOIEGsDjiI6c44nfPODZP+DhsXA05OFoXOThaFzlsXA05Nk/4OOJ3zzjiI6c44iOnONfe2zhsXA05TdQuOb1FTDmO4V05juFdOb1FTDlN1C45bFwNOdfe2zjiI6c44nfPOGxcDTkQsDw50OBxOVF5kTmbpKA5m6SgOVF5kTnQ4HE5ELA8OWxcDTnid884gQawODZP+DhN1C450OBxOZukoDmhD8c5jKTfOYyk3zmhD8c5m6SgOdDgcTlN1C45Nk/4OIEGsDhEGcQ4bFwNOb1FTDlReZE5oQ/HOSgP/TnZVBA62VQQOigP/TmhD8c5UXmROb1FTDlsXA05RBnEOOJ3zzg4Whc5juFdOZukoDmMpN852VQQOtIoJjrSKCY62VQQOoyk3zmbpKA5juFdOThaFznid8844nfPODhaFzmO4V05m6SgOYyk3znZVBA60igmOtIoJjrZVBA6jKTfOZukoDmO4V05OFoXOeJ3zzhEGcQ4bFwNOb1FTDlReZE5oQ/HOSgP/TnZVBA62VQQOigP/TmhD8c5UXmROb1FTDlsXA05RBnEOIEGsDg2T/g4TdQuOdDgcTmbpKA5oQ/HOYyk3zmMpN85oQ/HOZukoDnQ4HE5TdQuOTZP+DiBBrA44nfPOGxcDTkQsDw50OBxOVF5kTmbpKA5m6SgOVF5kTnQ4HE5ELA8OWxcDTnid8844iOnONfe2zhsXA05TdQuOb1FTDmO4V05juFdOb1FTDlN1C45bFwNOdfe2zjiI6c44iOnOOJ3zzg2T/g4bFwNOThaFzk4Whc5bFwNOTZP+Djid8844iOnOIEGsDhEGcQ44nfPOOJ3zzhEGcQ4gQawOF45KjlVJ0E0o8lTOQJrVjSjyVM5VSdBNF45KjlwSD056i5iNPp8hzmiEY00FTybOaIRjTT6fIc56i5iNHBIPTlwSD05XMduNBU8mzkThqc0LXHSOX4UvDQtcdI5E4anNBU8mzlcx240cEg9OV45KjnqLmI0FTybOXWBsTSa4fk5Bg3fNMfJFjoGDd80muH5OXWBsTQVPJs56i5iNF45KjlVJ0E0+nyHOROGpzSa4fk5n4TrNIGSOTo7SwI1gZI5Op+E6zSa4fk5E4anNPp8hzlVJ0E0o8lTOaIRjTQtcdI5Bg3fNIGSOTqvQwg1behpOq9DCDWBkjk6Bg3fNC1x0jmiEY00o8lTOQJrVjQVPJs5fhS8NMfJFjo7SwI1behpOoW3DTVt6Gk6O0sCNcfJFjp+FLw0FTybOQJrVjSjyVM5ohGNNC1x0jkGDd80gZI5Oq9DCDVt6Gk6r0MINYGSOToGDd80LXHSOaIRjTSjyVM5VSdBNPp8hzkThqc0muH5OZ+E6zSBkjk6O0sCNYGSOTqfhOs0muH5OROGpzT6fIc5VSdBNF45KjnqLmI0FTybOXWBsTSa4fk5Bg3fNMfJFjoGDd80muH5OXWBsTQVPJs56i5iNF45KjlwSD05XMduNBU8mzkThqc0LXHSOX4UvDQtcdI5E4anNBU8mzlcx240cEg9OXBIPTnqLmI0+nyHOaIRjTQVPJs5ohGNNPp8hznqLmI0cEg9OV45KjlVJ0E0o8lTOQJrVjSjyVM5VSdBNF45Kjlqprk44nfPONfe2zjX3ts44nfPOGqmuTjiI6c44nfPODZP+DhsXA05OFoXOThaFzlsXA05Nk/4OOJ3zzjiI6c44nfPONNVBDmzcyI5ELA8Ob1FTDm9RUw5ELA8ObNzIjnTVQQ54nfPOGqmuTg2T/g4s3MiOb1FTDnQ4HE5V1uEOVdbhDnQ4HE5vUVMObNzIjk2T/g4aqa5OOJ3zzhsXA05ELA8OdDgcTlReZE5m6SgOZukoDlReZE50OBxORCwPDlsXA054nfPONfe2zg4Whc5vUVMOVdbhDmbpKA5D1CyOQ9QsjmbpKA5V1uEOb1FTDk4Whc5197bONfe2zg4Whc5vUVMOVdbhDmbpKA5D1CyOQ9QsjmbpKA5V1uEOb1FTDk4Whc5197bOOJ3zzhsXA05ELA8OdDgcTlReZE5m6SgOZukoDlReZE50OBxORCwPDlsXA054nfPOGqmuTg2T/g4s3MiOb1FTDnQ4HE5V1uEOVdbhDnQ4HE5vUVMObNzIjk2T/g4aqa5OOJ3zzjTVQQ5s3MiORCwPDm9RUw5vUVMORCwPDmzcyI501UEOeJ3zzjiI6c44nfPODZP+DhsXA05OFoXOThaFzlsXA05Nk/4OOJ3zzjiI6c4aqa5OOJ3zzjX3ts4197bOOJ3zzhqprk4XjkqOVUnQTSjyVM5AmtWNKPJUzlVJ0E0XjkqOXFeMzkCa1Y0UQV+OSNZhTSF2pA5I1mFNFEFfjkCa1Y0cV4zOV45KjkCa1Y0+nyHOXNSlTR7rrM5E4anNHuuszlzUpU0+nyHOQJrVjReOSo5VSdBNFEFfjlzUpU0hyXCOX4UvDSU4+Q5fhS8NIclwjlzUpU0UQV+OVUnQTSjyVM5I1mFNHuuszl+FLw0muH5OaXp0jSa4fk5fhS8NHuuszkjWYU0o8lTOQJrVjSF2pA5E4anNJTj5Dml6dI0GPQIOqXp0jSU4+Q5E4anNIXakDkCa1Y0o8lTOSNZhTR7rrM5fhS8NJrh+Tml6dI0muH5OX4UvDR7rrM5I1mFNKPJUzlVJ0E0UQV+OXNSlTSHJcI5fhS8NJTj5Dl+FLw0hyXCOXNSlTRRBX45VSdBNF45KjkCa1Y0+nyHOXNSlTR7rrM5E4anNHuuszlzUpU0+nyHOQJrVjReOSo5cV4zOQJrVjRRBX45I1mFNIXakDkjWYU0UQV+OQJrVjRxXjM5XjkqOVUnQTSjyVM5AmtWNKPJUzlVJ0E0XjkqOYEGsDhEGcQ44nfPOOJ3zzhEGcQ4gQawOGqmuTjX3ts4Nk/4ONNVBDnTVQQ5Nk/4ONfe2zhqprk4gQawONfe2zjTVQQ5OFoXObNzIjmzcyI5OFoXOdNVBDnX3ts4gQawOEQZxDg2T/g4OFoXOU3ULjkQsDw5ELA8OU3ULjk4Whc5Nk/4OEQZxDjid88401UEObNzIjkQsDw5vUVMOb1FTDkQsDw5s3MiOdNVBDnid8844nfPONNVBDmzcyI5ELA8Ob1FTDm9RUw5ELA8ObNzIjnTVQQ54nfPOEQZxDg2T/g4OFoXOU3ULjkQsDw5ELA8OU3ULjk4Whc5Nk/4OEQZxDiBBrA4197bONNVBDk4Whc5s3MiObNzIjk4Whc501UEOdfe2ziBBrA4aqa5ONfe2zg2T/g401UEOdNVBDk2T/g4197bOGqmuTiBBrA4RBnEOOJ3zzjid884RBnEOIEGsDiRkC40cEg9OVUnQTRwSD05kZAuNJiMNzSjyVM56i5iNIGfbjnqLmI0o8lTOZiMNzSRkC40o8lTOVzHbjT6fIc5I1mFNPp8hzlcx240o8lTOZGQLjRwSD056i5iNPp8hzmiEY00FTybOaIRjTT6fIc56i5iNHBIPTlVJ0E0gZ9uOSNZhTQVPJs5c1KVNBU8mzkjWYU0gZ9uOVUnQTRwSD056i5iNPp8hzmiEY00FTybOaIRjTT6fIc56i5iNHBIPTmRkC40o8lTOVzHbjT6fIc5I1mFNPp8hzlcx240o8lTOZGQLjSYjDc0o8lTOeouYjSBn2456i5iNKPJUzmYjDc0kZAuNHBIPTlVJ0E0cEg9OZGQLjTiI6c4gQawOIEGsDjiI6c4gQawOEQZxDjid8844nfPOEQZxDiBBrA44iOnOEQZxDjX3ts4LG/pOCxv6TjX3ts4RBnEOOIjpziBBrA44nfPOCxv6Tg2T/g4Nk/4OCxv6Tjid884gQawOIEGsDjid884LG/pODZP+Dg2T/g4LG/pOOJ3zziBBrA44iOnOEQZxDjX3ts4LG/pOCxv6TjX3ts4RBnEOOIjpziBBrA4RBnEOOJ3zzjid884RBnEOIEGsDjiI6c4gQawOIEGsDjiI6c4kZAuNHFeMzmRkC40kZAuNHBIPTlVJ0E0cEg9OZGQLjRxXjM5VSdBNB8OSDlVJ0E0cV4zOZGQLjRwSD05VSdBNHBIPTmRkC40kZAuNHFeMzmRkC40\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.005782605554528505,\"absorptive\":true,\"n_reflections\":5028,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", - "Al (fcc)": "{\"name\":\"Al\",\"spacegroup\":\"Fm-3m (225)\",\"pointgroup\":\"m-3m\",\"cell\":[[4.05,0.0,0.0],[0.0,4.05,0.0],[0.0,0.0,4.05]],\"recip\":[[0.2469135802469136,0.0,0.0],[0.0,0.2469135802469136,0.0],[0.0,0.0,0.2469135802469136]],\"positions_frac\":[[0.0,0.0,0.0],[0.0,0.5,0.5],[0.5,0.0,0.5],[0.5,0.5,0.0]],\"numbers\":[13,13,13,13],\"symbols\":[\"Al\",\"Al\",\"Al\",\"Al\"],\"colors\":[[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651]],\"radii\":[1.21,1.21,1.21,1.21],\"hkl_i16\":\"9P8AAAAA9f/7////9f/7/wEA9f/9//3/9f/9////9f/9/wEA9f/9/wMA9f////v/9f////3/9f//////9f///wEA9f///wMA9f///wUA9f8BAPv/9f8BAP3/9f8BAP//9f8BAAEA9f8BAAMA9f8BAAUA9f8DAP3/9f8DAP//9f8DAAEA9f8DAAMA9f8FAP//9f8FAAEA9v/6//7/9v/6/wAA9v/6/wIA9v/8//z/9v/8//7/9v/8/wAA9v/8/wIA9v/8/wQA9v/+//r/9v/+//z/9v/+//7/9v/+/wAA9v/+/wIA9v/+/wQA9v/+/wYA9v8AAPr/9v8AAPz/9v8AAP7/9v8AAAAA9v8AAAIA9v8AAAQA9v8AAAYA9v8CAPr/9v8CAPz/9v8CAP7/9v8CAAAA9v8CAAIA9v8CAAQA9v8CAAYA9v8EAPz/9v8EAP7/9v8EAAAA9v8EAAIA9v8EAAQA9v8GAP7/9v8GAAAA9v8GAAIA9//5//3/9//5////9//5/wEA9//5/wMA9//7//v/9//7//3/9//7////9//7/wEA9//7/wMA9//7/wUA9//9//n/9//9//v/9//9//3/9//9////9//9/wEA9//9/wMA9//9/wUA9//9/wcA9/////n/9/////v/9/////3/9///////9////wEA9////wMA9////wUA9////wcA9/8BAPn/9/8BAPv/9/8BAP3/9/8BAP//9/8BAAEA9/8BAAMA9/8BAAUA9/8BAAcA9/8DAPn/9/8DAPv/9/8DAP3/9/8DAP//9/8DAAEA9/8DAAMA9/8DAAUA9/8DAAcA9/8FAPv/9/8FAP3/9/8FAP//9/8FAAEA9/8FAAMA9/8FAAUA9/8HAP3/9/8HAP//9/8HAAEA9/8HAAMA+P/4//z/+P/4//7/+P/4/wAA+P/4/wIA+P/4/wQA+P/6//r/+P/6//z/+P/6//7/+P/6/wAA+P/6/wIA+P/6/wQA+P/6/wYA+P/8//j/+P/8//r/+P/8//z/+P/8//7/+P/8/wAA+P/8/wIA+P/8/wQA+P/8/wYA+P/8/wgA+P/+//j/+P/+//r/+P/+//z/+P/+//7/+P/+/wAA+P/+/wIA+P/+/wQA+P/+/wYA+P/+/wgA+P8AAPj/+P8AAPr/+P8AAPz/+P8AAP7/+P8AAAAA+P8AAAIA+P8AAAQA+P8AAAYA+P8AAAgA+P8CAPj/+P8CAPr/+P8CAPz/+P8CAP7/+P8CAAAA+P8CAAIA+P8CAAQA+P8CAAYA+P8CAAgA+P8EAPj/+P8EAPr/+P8EAPz/+P8EAP7/+P8EAAAA+P8EAAIA+P8EAAQA+P8EAAYA+P8EAAgA+P8GAPr/+P8GAPz/+P8GAP7/+P8GAAAA+P8GAAIA+P8GAAQA+P8GAAYA+P8IAPz/+P8IAP7/+P8IAAAA+P8IAAIA+P8IAAQA+f/3//3/+f/3////+f/3/wEA+f/3/wMA+f/5//n/+f/5//v/+f/5//3/+f/5////+f/5/wEA+f/5/wMA+f/5/wUA+f/5/wcA+f/7//n/+f/7//v/+f/7//3/+f/7////+f/7/wEA+f/7/wMA+f/7/wUA+f/7/wcA+f/9//f/+f/9//n/+f/9//v/+f/9//3/+f/9////+f/9/wEA+f/9/wMA+f/9/wUA+f/9/wcA+f/9/wkA+f////f/+f////n/+f////v/+f////3/+f//////+f///wEA+f///wMA+f///wUA+f///wcA+f///wkA+f8BAPf/+f8BAPn/+f8BAPv/+f8BAP3/+f8BAP//+f8BAAEA+f8BAAMA+f8BAAUA+f8BAAcA+f8BAAkA+f8DAPf/+f8DAPn/+f8DAPv/+f8DAP3/+f8DAP//+f8DAAEA+f8DAAMA+f8DAAUA+f8DAAcA+f8DAAkA+f8FAPn/+f8FAPv/+f8FAP3/+f8FAP//+f8FAAEA+f8FAAMA+f8FAAUA+f8FAAcA+f8HAPn/+f8HAPv/+f8HAP3/+f8HAP//+f8HAAEA+f8HAAMA+f8HAAUA+f8HAAcA+f8JAP3/+f8JAP//+f8JAAEA+f8JAAMA+v/2//7/+v/2/wAA+v/2/wIA+v/4//r/+v/4//z/+v/4//7/+v/4/wAA+v/4/wIA+v/4/wQA+v/4/wYA+v/6//j/+v/6//r/+v/6//z/+v/6//7/+v/6/wAA+v/6/wIA+v/6/wQA+v/6/wYA+v/6/wgA+v/8//j/+v/8//r/+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/8/wYA+v/8/wgA+v/+//b/+v/+//j/+v/+//r/+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v/+/wYA+v/+/wgA+v/+/woA+v8AAPb/+v8AAPj/+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8AAAgA+v8AAAoA+v8CAPb/+v8CAPj/+v8CAPr/+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8CAAYA+v8CAAgA+v8CAAoA+v8EAPj/+v8EAPr/+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8EAAYA+v8EAAgA+v8GAPj/+v8GAPr/+v8GAPz/+v8GAP7/+v8GAAAA+v8GAAIA+v8GAAQA+v8GAAYA+v8GAAgA+v8IAPr/+v8IAPz/+v8IAP7/+v8IAAAA+v8IAAIA+v8IAAQA+v8IAAYA+v8KAP7/+v8KAAAA+v8KAAIA+//1////+//1/wEA+//3//v/+//3//3/+//3////+//3/wEA+//3/wMA+//3/wUA+//5//n/+//5//v/+//5//3/+//5////+//5/wEA+//5/wMA+//5/wUA+//5/wcA+//7//f/+//7//n/+//7//v/+//7//3/+//7////+//7/wEA+//7/wMA+//7/wUA+//7/wcA+//7/wkA+//9//f/+//9//n/+//9//v/+//9//3/+//9////+//9/wEA+//9/wMA+//9/wUA+//9/wcA+//9/wkA+/////X/+/////f/+/////n/+/////v/+/////3/+///////+////wEA+////wMA+////wUA+////wcA+////wkA+////wsA+/8BAPX/+/8BAPf/+/8BAPn/+/8BAPv/+/8BAP3/+/8BAP//+/8BAAEA+/8BAAMA+/8BAAUA+/8BAAcA+/8BAAkA+/8BAAsA+/8DAPf/+/8DAPn/+/8DAPv/+/8DAP3/+/8DAP//+/8DAAEA+/8DAAMA+/8DAAUA+/8DAAcA+/8DAAkA+/8FAPf/+/8FAPn/+/8FAPv/+/8FAP3/+/8FAP//+/8FAAEA+/8FAAMA+/8FAAUA+/8FAAcA+/8FAAkA+/8HAPn/+/8HAPv/+/8HAP3/+/8HAP//+/8HAAEA+/8HAAMA+/8HAAUA+/8HAAcA+/8JAPv/+/8JAP3/+/8JAP//+/8JAAEA+/8JAAMA+/8JAAUA+/8LAP//+/8LAAEA/P/2//z//P/2//7//P/2/wAA/P/2/wIA/P/2/wQA/P/4//j//P/4//r//P/4//z//P/4//7//P/4/wAA/P/4/wIA/P/4/wQA/P/4/wYA/P/4/wgA/P/6//j//P/6//r//P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/6/wYA/P/6/wgA/P/8//b//P/8//j//P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/8/wgA/P/8/woA/P/+//b//P/+//j//P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P/+/wgA/P/+/woA/P8AAPb//P8AAPj//P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8AAAgA/P8AAAoA/P8CAPb//P8CAPj//P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8CAAgA/P8CAAoA/P8EAPb//P8EAPj//P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8EAAgA/P8EAAoA/P8GAPj//P8GAPr//P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8GAAYA/P8GAAgA/P8IAPj//P8IAPr//P8IAPz//P8IAP7//P8IAAAA/P8IAAIA/P8IAAQA/P8IAAYA/P8IAAgA/P8KAPz//P8KAP7//P8KAAAA/P8KAAIA/P8KAAQA/f/1//3//f/1/////f/1/wEA/f/1/wMA/f/3//n//f/3//v//f/3//3//f/3/////f/3/wEA/f/3/wMA/f/3/wUA/f/3/wcA/f/5//f//f/5//n//f/5//v//f/5//3//f/5/////f/5/wEA/f/5/wMA/f/5/wUA/f/5/wcA/f/5/wkA/f/7//f//f/7//n//f/7//v//f/7//3//f/7/////f/7/wEA/f/7/wMA/f/7/wUA/f/7/wcA/f/7/wkA/f/9//X//f/9//f//f/9//n//f/9//v//f/9//3//f/9/////f/9/wEA/f/9/wMA/f/9/wUA/f/9/wcA/f/9/wkA/f/9/wsA/f////X//f////f//f////n//f////v//f////3//f///////f///wEA/f///wMA/f///wUA/f///wcA/f///wkA/f///wsA/f8BAPX//f8BAPf//f8BAPn//f8BAPv//f8BAP3//f8BAP///f8BAAEA/f8BAAMA/f8BAAUA/f8BAAcA/f8BAAkA/f8BAAsA/f8DAPX//f8DAPf//f8DAPn//f8DAPv//f8DAP3//f8DAP///f8DAAEA/f8DAAMA/f8DAAUA/f8DAAcA/f8DAAkA/f8DAAsA/f8FAPf//f8FAPn//f8FAPv//f8FAP3//f8FAP///f8FAAEA/f8FAAMA/f8FAAUA/f8FAAcA/f8FAAkA/f8HAPf//f8HAPn//f8HAPv//f8HAP3//f8HAP///f8HAAEA/f8HAAMA/f8HAAUA/f8HAAcA/f8HAAkA/f8JAPn//f8JAPv//f8JAP3//f8JAP///f8JAAEA/f8JAAMA/f8JAAUA/f8JAAcA/f8LAP3//f8LAP///f8LAAEA/f8LAAMA/v/2//r//v/2//z//v/2//7//v/2/wAA/v/2/wIA/v/2/wQA/v/2/wYA/v/4//j//v/4//r//v/4//z//v/4//7//v/4/wAA/v/4/wIA/v/4/wQA/v/4/wYA/v/4/wgA/v/6//b//v/6//j//v/6//r//v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/6/wYA/v/6/wgA/v/6/woA/v/8//b//v/8//j//v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/8/wgA/v/8/woA/v/+//b//v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v/+/woA/v8AAPb//v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8AAAoA/v8CAPb//v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8CAAoA/v8EAPb//v8EAPj//v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8EAAgA/v8EAAoA/v8GAPb//v8GAPj//v8GAPr//v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8GAAYA/v8GAAgA/v8GAAoA/v8IAPj//v8IAPr//v8IAPz//v8IAP7//v8IAAAA/v8IAAIA/v8IAAQA/v8IAAYA/v8IAAgA/v8KAPr//v8KAPz//v8KAP7//v8KAAAA/v8KAAIA/v8KAAQA/v8KAAYA///1//v////1//3////1///////1/wEA///1/wMA///1/wUA///3//n////3//v////3//3////3///////3/wEA///3/wMA///3/wUA///3/wcA///5//f////5//n////5//v////5//3////5///////5/wEA///5/wMA///5/wUA///5/wcA///5/wkA///7//X////7//f////7//n////7//v////7//3////7///////7/wEA///7/wMA///7/wUA///7/wcA///7/wkA///7/wsA///9//X////9//f////9//n////9//v////9//3////9///////9/wEA///9/wMA///9/wUA///9/wcA///9/wkA///9/wsA//////X///////f///////n///////v///////3//////////////wEA/////wMA/////wUA/////wcA/////wkA/////wsA//8BAPX///8BAPf///8BAPn///8BAPv///8BAP3///8BAP////8BAAEA//8BAAMA//8BAAUA//8BAAcA//8BAAkA//8BAAsA//8DAPX///8DAPf///8DAPn///8DAPv///8DAP3///8DAP////8DAAEA//8DAAMA//8DAAUA//8DAAcA//8DAAkA//8DAAsA//8FAPX///8FAPf///8FAPn///8FAPv///8FAP3///8FAP////8FAAEA//8FAAMA//8FAAUA//8FAAcA//8FAAkA//8FAAsA//8HAPf///8HAPn///8HAPv///8HAP3///8HAP////8HAAEA//8HAAMA//8HAAUA//8HAAcA//8HAAkA//8JAPn///8JAPv///8JAP3///8JAP////8JAAEA//8JAAMA//8JAAUA//8JAAcA//8LAPv///8LAP3///8LAP////8LAAEA//8LAAMA//8LAAUAAAD0/wAAAAD2//r/AAD2//z/AAD2//7/AAD2/wAAAAD2/wIAAAD2/wQAAAD2/wYAAAD4//j/AAD4//r/AAD4//z/AAD4//7/AAD4/wAAAAD4/wIAAAD4/wQAAAD4/wYAAAD4/wgAAAD6//b/AAD6//j/AAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD6/wgAAAD6/woAAAD8//b/AAD8//j/AAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD8/wgAAAD8/woAAAD+//b/AAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD+/woAAAAAAPT/AAAAAPb/AAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAAAAAoAAAAAAAwAAAACAPb/AAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAACAAoAAAAEAPb/AAAEAPj/AAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAEAAgAAAAEAAoAAAAGAPb/AAAGAPj/AAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAGAAgAAAAGAAoAAAAIAPj/AAAIAPr/AAAIAPz/AAAIAP7/AAAIAAAAAAAIAAIAAAAIAAQAAAAIAAYAAAAIAAgAAAAKAPr/AAAKAPz/AAAKAP7/AAAKAAAAAAAKAAIAAAAKAAQAAAAKAAYAAAAMAAAAAQD1//v/AQD1//3/AQD1////AQD1/wEAAQD1/wMAAQD1/wUAAQD3//n/AQD3//v/AQD3//3/AQD3////AQD3/wEAAQD3/wMAAQD3/wUAAQD3/wcAAQD5//f/AQD5//n/AQD5//v/AQD5//3/AQD5////AQD5/wEAAQD5/wMAAQD5/wUAAQD5/wcAAQD5/wkAAQD7//X/AQD7//f/AQD7//n/AQD7//v/AQD7//3/AQD7////AQD7/wEAAQD7/wMAAQD7/wUAAQD7/wcAAQD7/wkAAQD7/wsAAQD9//X/AQD9//f/AQD9//n/AQD9//v/AQD9//3/AQD9////AQD9/wEAAQD9/wMAAQD9/wUAAQD9/wcAAQD9/wkAAQD9/wsAAQD///X/AQD///f/AQD///n/AQD///v/AQD///3/AQD/////AQD//wEAAQD//wMAAQD//wUAAQD//wcAAQD//wkAAQD//wsAAQABAPX/AQABAPf/AQABAPn/AQABAPv/AQABAP3/AQABAP//AQABAAEAAQABAAMAAQABAAUAAQABAAcAAQABAAkAAQABAAsAAQADAPX/AQADAPf/AQADAPn/AQADAPv/AQADAP3/AQADAP//AQADAAEAAQADAAMAAQADAAUAAQADAAcAAQADAAkAAQADAAsAAQAFAPX/AQAFAPf/AQAFAPn/AQAFAPv/AQAFAP3/AQAFAP//AQAFAAEAAQAFAAMAAQAFAAUAAQAFAAcAAQAFAAkAAQAFAAsAAQAHAPf/AQAHAPn/AQAHAPv/AQAHAP3/AQAHAP//AQAHAAEAAQAHAAMAAQAHAAUAAQAHAAcAAQAHAAkAAQAJAPn/AQAJAPv/AQAJAP3/AQAJAP//AQAJAAEAAQAJAAMAAQAJAAUAAQAJAAcAAQALAPv/AQALAP3/AQALAP//AQALAAEAAQALAAMAAQALAAUAAgD2//r/AgD2//z/AgD2//7/AgD2/wAAAgD2/wIAAgD2/wQAAgD2/wYAAgD4//j/AgD4//r/AgD4//z/AgD4//7/AgD4/wAAAgD4/wIAAgD4/wQAAgD4/wYAAgD4/wgAAgD6//b/AgD6//j/AgD6//r/AgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD6/wYAAgD6/wgAAgD6/woAAgD8//b/AgD8//j/AgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD8/wgAAgD8/woAAgD+//b/AgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD+/woAAgAAAPb/AgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgAAAAoAAgACAPb/AgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgACAAoAAgAEAPb/AgAEAPj/AgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAEAAgAAgAEAAoAAgAGAPb/AgAGAPj/AgAGAPr/AgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAGAAYAAgAGAAgAAgAGAAoAAgAIAPj/AgAIAPr/AgAIAPz/AgAIAP7/AgAIAAAAAgAIAAIAAgAIAAQAAgAIAAYAAgAIAAgAAgAKAPr/AgAKAPz/AgAKAP7/AgAKAAAAAgAKAAIAAgAKAAQAAgAKAAYAAwD1//3/AwD1////AwD1/wEAAwD1/wMAAwD3//n/AwD3//v/AwD3//3/AwD3////AwD3/wEAAwD3/wMAAwD3/wUAAwD3/wcAAwD5//f/AwD5//n/AwD5//v/AwD5//3/AwD5////AwD5/wEAAwD5/wMAAwD5/wUAAwD5/wcAAwD5/wkAAwD7//f/AwD7//n/AwD7//v/AwD7//3/AwD7////AwD7/wEAAwD7/wMAAwD7/wUAAwD7/wcAAwD7/wkAAwD9//X/AwD9//f/AwD9//n/AwD9//v/AwD9//3/AwD9////AwD9/wEAAwD9/wMAAwD9/wUAAwD9/wcAAwD9/wkAAwD9/wsAAwD///X/AwD///f/AwD///n/AwD///v/AwD///3/AwD/////AwD//wEAAwD//wMAAwD//wUAAwD//wcAAwD//wkAAwD//wsAAwABAPX/AwABAPf/AwABAPn/AwABAPv/AwABAP3/AwABAP//AwABAAEAAwABAAMAAwABAAUAAwABAAcAAwABAAkAAwABAAsAAwADAPX/AwADAPf/AwADAPn/AwADAPv/AwADAP3/AwADAP//AwADAAEAAwADAAMAAwADAAUAAwADAAcAAwADAAkAAwADAAsAAwAFAPf/AwAFAPn/AwAFAPv/AwAFAP3/AwAFAP//AwAFAAEAAwAFAAMAAwAFAAUAAwAFAAcAAwAFAAkAAwAHAPf/AwAHAPn/AwAHAPv/AwAHAP3/AwAHAP//AwAHAAEAAwAHAAMAAwAHAAUAAwAHAAcAAwAHAAkAAwAJAPn/AwAJAPv/AwAJAP3/AwAJAP//AwAJAAEAAwAJAAMAAwAJAAUAAwAJAAcAAwALAP3/AwALAP//AwALAAEAAwALAAMABAD2//z/BAD2//7/BAD2/wAABAD2/wIABAD2/wQABAD4//j/BAD4//r/BAD4//z/BAD4//7/BAD4/wAABAD4/wIABAD4/wQABAD4/wYABAD4/wgABAD6//j/BAD6//r/BAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD6/wYABAD6/wgABAD8//b/BAD8//j/BAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD8/wgABAD8/woABAD+//b/BAD+//j/BAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD+/wgABAD+/woABAAAAPb/BAAAAPj/BAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAAAAAgABAAAAAoABAACAPb/BAACAPj/BAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAACAAgABAACAAoABAAEAPb/BAAEAPj/BAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAEAAgABAAEAAoABAAGAPj/BAAGAPr/BAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAGAAYABAAGAAgABAAIAPj/BAAIAPr/BAAIAPz/BAAIAP7/BAAIAAAABAAIAAIABAAIAAQABAAIAAYABAAIAAgABAAKAPz/BAAKAP7/BAAKAAAABAAKAAIABAAKAAQABQD1////BQD1/wEABQD3//v/BQD3//3/BQD3////BQD3/wEABQD3/wMABQD3/wUABQD5//n/BQD5//v/BQD5//3/BQD5////BQD5/wEABQD5/wMABQD5/wUABQD5/wcABQD7//f/BQD7//n/BQD7//v/BQD7//3/BQD7////BQD7/wEABQD7/wMABQD7/wUABQD7/wcABQD7/wkABQD9//f/BQD9//n/BQD9//v/BQD9//3/BQD9////BQD9/wEABQD9/wMABQD9/wUABQD9/wcABQD9/wkABQD///X/BQD///f/BQD///n/BQD///v/BQD///3/BQD/////BQD//wEABQD//wMABQD//wUABQD//wcABQD//wkABQD//wsABQABAPX/BQABAPf/BQABAPn/BQABAPv/BQABAP3/BQABAP//BQABAAEABQABAAMABQABAAUABQABAAcABQABAAkABQABAAsABQADAPf/BQADAPn/BQADAPv/BQADAP3/BQADAP//BQADAAEABQADAAMABQADAAUABQADAAcABQADAAkABQAFAPf/BQAFAPn/BQAFAPv/BQAFAP3/BQAFAP//BQAFAAEABQAFAAMABQAFAAUABQAFAAcABQAFAAkABQAHAPn/BQAHAPv/BQAHAP3/BQAHAP//BQAHAAEABQAHAAMABQAHAAUABQAHAAcABQAJAPv/BQAJAP3/BQAJAP//BQAJAAEABQAJAAMABQAJAAUABQALAP//BQALAAEABgD2//7/BgD2/wAABgD2/wIABgD4//r/BgD4//z/BgD4//7/BgD4/wAABgD4/wIABgD4/wQABgD4/wYABgD6//j/BgD6//r/BgD6//z/BgD6//7/BgD6/wAABgD6/wIABgD6/wQABgD6/wYABgD6/wgABgD8//j/BgD8//r/BgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD8/wYABgD8/wgABgD+//b/BgD+//j/BgD+//r/BgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD+/wYABgD+/wgABgD+/woABgAAAPb/BgAAAPj/BgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgAAAAgABgAAAAoABgACAPb/BgACAPj/BgACAPr/BgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgACAAYABgACAAgABgACAAoABgAEAPj/BgAEAPr/BgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAEAAYABgAEAAgABgAGAPj/BgAGAPr/BgAGAPz/BgAGAP7/BgAGAAAABgAGAAIABgAGAAQABgAGAAYABgAGAAgABgAIAPr/BgAIAPz/BgAIAP7/BgAIAAAABgAIAAIABgAIAAQABgAIAAYABgAKAP7/BgAKAAAABgAKAAIABwD3//3/BwD3////BwD3/wEABwD3/wMABwD5//n/BwD5//v/BwD5//3/BwD5////BwD5/wEABwD5/wMABwD5/wUABwD5/wcABwD7//n/BwD7//v/BwD7//3/BwD7////BwD7/wEABwD7/wMABwD7/wUABwD7/wcABwD9//f/BwD9//n/BwD9//v/BwD9//3/BwD9////BwD9/wEABwD9/wMABwD9/wUABwD9/wcABwD9/wkABwD///f/BwD///n/BwD///v/BwD///3/BwD/////BwD//wEABwD//wMABwD//wUABwD//wcABwD//wkABwABAPf/BwABAPn/BwABAPv/BwABAP3/BwABAP//BwABAAEABwABAAMABwABAAUABwABAAcABwABAAkABwADAPf/BwADAPn/BwADAPv/BwADAP3/BwADAP//BwADAAEABwADAAMABwADAAUABwADAAcABwADAAkABwAFAPn/BwAFAPv/BwAFAP3/BwAFAP//BwAFAAEABwAFAAMABwAFAAUABwAFAAcABwAHAPn/BwAHAPv/BwAHAP3/BwAHAP//BwAHAAEABwAHAAMABwAHAAUABwAHAAcABwAJAP3/BwAJAP//BwAJAAEABwAJAAMACAD4//z/CAD4//7/CAD4/wAACAD4/wIACAD4/wQACAD6//r/CAD6//z/CAD6//7/CAD6/wAACAD6/wIACAD6/wQACAD6/wYACAD8//j/CAD8//r/CAD8//z/CAD8//7/CAD8/wAACAD8/wIACAD8/wQACAD8/wYACAD8/wgACAD+//j/CAD+//r/CAD+//z/CAD+//7/CAD+/wAACAD+/wIACAD+/wQACAD+/wYACAD+/wgACAAAAPj/CAAAAPr/CAAAAPz/CAAAAP7/CAAAAAAACAAAAAIACAAAAAQACAAAAAYACAAAAAgACAACAPj/CAACAPr/CAACAPz/CAACAP7/CAACAAAACAACAAIACAACAAQACAACAAYACAACAAgACAAEAPj/CAAEAPr/CAAEAPz/CAAEAP7/CAAEAAAACAAEAAIACAAEAAQACAAEAAYACAAEAAgACAAGAPr/CAAGAPz/CAAGAP7/CAAGAAAACAAGAAIACAAGAAQACAAGAAYACAAIAPz/CAAIAP7/CAAIAAAACAAIAAIACAAIAAQACQD5//3/CQD5////CQD5/wEACQD5/wMACQD7//v/CQD7//3/CQD7////CQD7/wEACQD7/wMACQD7/wUACQD9//n/CQD9//v/CQD9//3/CQD9////CQD9/wEACQD9/wMACQD9/wUACQD9/wcACQD///n/CQD///v/CQD///3/CQD/////CQD//wEACQD//wMACQD//wUACQD//wcACQABAPn/CQABAPv/CQABAP3/CQABAP//CQABAAEACQABAAMACQABAAUACQABAAcACQADAPn/CQADAPv/CQADAP3/CQADAP//CQADAAEACQADAAMACQADAAUACQADAAcACQAFAPv/CQAFAP3/CQAFAP//CQAFAAEACQAFAAMACQAFAAUACQAHAP3/CQAHAP//CQAHAAEACQAHAAMACgD6//7/CgD6/wAACgD6/wIACgD8//z/CgD8//7/CgD8/wAACgD8/wIACgD8/wQACgD+//r/CgD+//z/CgD+//7/CgD+/wAACgD+/wIACgD+/wQACgD+/wYACgAAAPr/CgAAAPz/CgAAAP7/CgAAAAAACgAAAAIACgAAAAQACgAAAAYACgACAPr/CgACAPz/CgACAP7/CgACAAAACgACAAIACgACAAQACgACAAYACgAEAPz/CgAEAP7/CgAEAAAACgAEAAIACgAEAAQACgAGAP7/CgAGAAAACgAGAAIACwD7////CwD7/wEACwD9//3/CwD9////CwD9/wEACwD9/wMACwD///v/CwD///3/CwD/////CwD//wEACwD//wMACwD//wUACwABAPv/CwABAP3/CwABAP//CwABAAEACwABAAMACwABAAUACwADAP3/CwADAP//CwADAAEACwADAAMACwAFAP//CwAFAAEADAAAAAAA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.005121869242937061,\"absorptive\":true,\"n_reflections\":1892,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", - "Au (fcc)": "{\"name\":\"Au\",\"spacegroup\":\"Fm-3m (225)\",\"pointgroup\":\"m-3m\",\"cell\":[[4.078,0.0,0.0],[0.0,4.078,0.0],[0.0,0.0,4.078]],\"recip\":[[0.24521824423737124,0.0,0.0],[0.0,0.24521824423737124,0.0],[0.0,0.0,0.24521824423737124]],\"positions_frac\":[[0.0,0.0,0.0],[0.0,0.5,0.5],[0.5,0.0,0.5],[0.5,0.5,0.0]],\"numbers\":[79,79,79,79],\"symbols\":[\"Au\",\"Au\",\"Au\",\"Au\"],\"colors\":[[1.0,0.82,0.137],[1.0,0.82,0.137],[1.0,0.82,0.137],[1.0,0.82,0.137]],\"radii\":[1.36,1.36,1.36,1.36],\"hkl_i16\":\"9P/+/wAA9P8AAP7/9P8AAAAA9P8AAAIA9P8CAAAA9f/7////9f/7/wEA9f/9//3/9f/9////9f/9/wEA9f/9/wMA9f////v/9f////3/9f//////9f///wEA9f///wMA9f///wUA9f8BAPv/9f8BAP3/9f8BAP//9f8BAAEA9f8BAAMA9f8BAAUA9f8DAP3/9f8DAP//9f8DAAEA9f8DAAMA9f8FAP//9f8FAAEA9v/6//7/9v/6/wAA9v/6/wIA9v/8//z/9v/8//7/9v/8/wAA9v/8/wIA9v/8/wQA9v/+//r/9v/+//z/9v/+//7/9v/+/wAA9v/+/wIA9v/+/wQA9v/+/wYA9v8AAPr/9v8AAPz/9v8AAP7/9v8AAAAA9v8AAAIA9v8AAAQA9v8AAAYA9v8CAPr/9v8CAPz/9v8CAP7/9v8CAAAA9v8CAAIA9v8CAAQA9v8CAAYA9v8EAPz/9v8EAP7/9v8EAAAA9v8EAAIA9v8EAAQA9v8GAP7/9v8GAAAA9v8GAAIA9//5//3/9//5////9//5/wEA9//5/wMA9//7//v/9//7//3/9//7////9//7/wEA9//7/wMA9//7/wUA9//9//n/9//9//v/9//9//3/9//9////9//9/wEA9//9/wMA9//9/wUA9//9/wcA9/////n/9/////v/9/////3/9///////9////wEA9////wMA9////wUA9////wcA9/8BAPn/9/8BAPv/9/8BAP3/9/8BAP//9/8BAAEA9/8BAAMA9/8BAAUA9/8BAAcA9/8DAPn/9/8DAPv/9/8DAP3/9/8DAP//9/8DAAEA9/8DAAMA9/8DAAUA9/8DAAcA9/8FAPv/9/8FAP3/9/8FAP//9/8FAAEA9/8FAAMA9/8FAAUA9/8HAP3/9/8HAP//9/8HAAEA9/8HAAMA+P/4//z/+P/4//7/+P/4/wAA+P/4/wIA+P/4/wQA+P/6//r/+P/6//z/+P/6//7/+P/6/wAA+P/6/wIA+P/6/wQA+P/6/wYA+P/8//j/+P/8//r/+P/8//z/+P/8//7/+P/8/wAA+P/8/wIA+P/8/wQA+P/8/wYA+P/8/wgA+P/+//j/+P/+//r/+P/+//z/+P/+//7/+P/+/wAA+P/+/wIA+P/+/wQA+P/+/wYA+P/+/wgA+P8AAPj/+P8AAPr/+P8AAPz/+P8AAP7/+P8AAAAA+P8AAAIA+P8AAAQA+P8AAAYA+P8AAAgA+P8CAPj/+P8CAPr/+P8CAPz/+P8CAP7/+P8CAAAA+P8CAAIA+P8CAAQA+P8CAAYA+P8CAAgA+P8EAPj/+P8EAPr/+P8EAPz/+P8EAP7/+P8EAAAA+P8EAAIA+P8EAAQA+P8EAAYA+P8EAAgA+P8GAPr/+P8GAPz/+P8GAP7/+P8GAAAA+P8GAAIA+P8GAAQA+P8GAAYA+P8IAPz/+P8IAP7/+P8IAAAA+P8IAAIA+P8IAAQA+f/3//3/+f/3////+f/3/wEA+f/3/wMA+f/5//n/+f/5//v/+f/5//3/+f/5////+f/5/wEA+f/5/wMA+f/5/wUA+f/5/wcA+f/7//n/+f/7//v/+f/7//3/+f/7////+f/7/wEA+f/7/wMA+f/7/wUA+f/7/wcA+f/9//f/+f/9//n/+f/9//v/+f/9//3/+f/9////+f/9/wEA+f/9/wMA+f/9/wUA+f/9/wcA+f/9/wkA+f////f/+f////n/+f////v/+f////3/+f//////+f///wEA+f///wMA+f///wUA+f///wcA+f///wkA+f8BAPf/+f8BAPn/+f8BAPv/+f8BAP3/+f8BAP//+f8BAAEA+f8BAAMA+f8BAAUA+f8BAAcA+f8BAAkA+f8DAPf/+f8DAPn/+f8DAPv/+f8DAP3/+f8DAP//+f8DAAEA+f8DAAMA+f8DAAUA+f8DAAcA+f8DAAkA+f8FAPn/+f8FAPv/+f8FAP3/+f8FAP//+f8FAAEA+f8FAAMA+f8FAAUA+f8FAAcA+f8HAPn/+f8HAPv/+f8HAP3/+f8HAP//+f8HAAEA+f8HAAMA+f8HAAUA+f8HAAcA+f8JAP3/+f8JAP//+f8JAAEA+f8JAAMA+v/2//7/+v/2/wAA+v/2/wIA+v/4//r/+v/4//z/+v/4//7/+v/4/wAA+v/4/wIA+v/4/wQA+v/4/wYA+v/6//j/+v/6//r/+v/6//z/+v/6//7/+v/6/wAA+v/6/wIA+v/6/wQA+v/6/wYA+v/6/wgA+v/8//j/+v/8//r/+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/8/wYA+v/8/wgA+v/+//b/+v/+//j/+v/+//r/+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v/+/wYA+v/+/wgA+v/+/woA+v8AAPb/+v8AAPj/+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8AAAgA+v8AAAoA+v8CAPb/+v8CAPj/+v8CAPr/+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8CAAYA+v8CAAgA+v8CAAoA+v8EAPj/+v8EAPr/+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8EAAYA+v8EAAgA+v8GAPj/+v8GAPr/+v8GAPz/+v8GAP7/+v8GAAAA+v8GAAIA+v8GAAQA+v8GAAYA+v8GAAgA+v8IAPr/+v8IAPz/+v8IAP7/+v8IAAAA+v8IAAIA+v8IAAQA+v8IAAYA+v8KAP7/+v8KAAAA+v8KAAIA+//1////+//1/wEA+//3//v/+//3//3/+//3////+//3/wEA+//3/wMA+//3/wUA+//5//n/+//5//v/+//5//3/+//5////+//5/wEA+//5/wMA+//5/wUA+//5/wcA+//7//f/+//7//n/+//7//v/+//7//3/+//7////+//7/wEA+//7/wMA+//7/wUA+//7/wcA+//7/wkA+//9//f/+//9//n/+//9//v/+//9//3/+//9////+//9/wEA+//9/wMA+//9/wUA+//9/wcA+//9/wkA+/////X/+/////f/+/////n/+/////v/+/////3/+///////+////wEA+////wMA+////wUA+////wcA+////wkA+////wsA+/8BAPX/+/8BAPf/+/8BAPn/+/8BAPv/+/8BAP3/+/8BAP//+/8BAAEA+/8BAAMA+/8BAAUA+/8BAAcA+/8BAAkA+/8BAAsA+/8DAPf/+/8DAPn/+/8DAPv/+/8DAP3/+/8DAP//+/8DAAEA+/8DAAMA+/8DAAUA+/8DAAcA+/8DAAkA+/8FAPf/+/8FAPn/+/8FAPv/+/8FAP3/+/8FAP//+/8FAAEA+/8FAAMA+/8FAAUA+/8FAAcA+/8FAAkA+/8HAPn/+/8HAPv/+/8HAP3/+/8HAP//+/8HAAEA+/8HAAMA+/8HAAUA+/8HAAcA+/8JAPv/+/8JAP3/+/8JAP//+/8JAAEA+/8JAAMA+/8JAAUA+/8LAP//+/8LAAEA/P/2//z//P/2//7//P/2/wAA/P/2/wIA/P/2/wQA/P/4//j//P/4//r//P/4//z//P/4//7//P/4/wAA/P/4/wIA/P/4/wQA/P/4/wYA/P/4/wgA/P/6//j//P/6//r//P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/6/wYA/P/6/wgA/P/8//b//P/8//j//P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/8/wgA/P/8/woA/P/+//b//P/+//j//P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P/+/wgA/P/+/woA/P8AAPb//P8AAPj//P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8AAAgA/P8AAAoA/P8CAPb//P8CAPj//P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8CAAgA/P8CAAoA/P8EAPb//P8EAPj//P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8EAAgA/P8EAAoA/P8GAPj//P8GAPr//P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8GAAYA/P8GAAgA/P8IAPj//P8IAPr//P8IAPz//P8IAP7//P8IAAAA/P8IAAIA/P8IAAQA/P8IAAYA/P8IAAgA/P8KAPz//P8KAP7//P8KAAAA/P8KAAIA/P8KAAQA/f/1//3//f/1/////f/1/wEA/f/1/wMA/f/3//n//f/3//v//f/3//3//f/3/////f/3/wEA/f/3/wMA/f/3/wUA/f/3/wcA/f/5//f//f/5//n//f/5//v//f/5//3//f/5/////f/5/wEA/f/5/wMA/f/5/wUA/f/5/wcA/f/5/wkA/f/7//f//f/7//n//f/7//v//f/7//3//f/7/////f/7/wEA/f/7/wMA/f/7/wUA/f/7/wcA/f/7/wkA/f/9//X//f/9//f//f/9//n//f/9//v//f/9//3//f/9/////f/9/wEA/f/9/wMA/f/9/wUA/f/9/wcA/f/9/wkA/f/9/wsA/f////X//f////f//f////n//f////v//f////3//f///////f///wEA/f///wMA/f///wUA/f///wcA/f///wkA/f///wsA/f8BAPX//f8BAPf//f8BAPn//f8BAPv//f8BAP3//f8BAP///f8BAAEA/f8BAAMA/f8BAAUA/f8BAAcA/f8BAAkA/f8BAAsA/f8DAPX//f8DAPf//f8DAPn//f8DAPv//f8DAP3//f8DAP///f8DAAEA/f8DAAMA/f8DAAUA/f8DAAcA/f8DAAkA/f8DAAsA/f8FAPf//f8FAPn//f8FAPv//f8FAP3//f8FAP///f8FAAEA/f8FAAMA/f8FAAUA/f8FAAcA/f8FAAkA/f8HAPf//f8HAPn//f8HAPv//f8HAP3//f8HAP///f8HAAEA/f8HAAMA/f8HAAUA/f8HAAcA/f8HAAkA/f8JAPn//f8JAPv//f8JAP3//f8JAP///f8JAAEA/f8JAAMA/f8JAAUA/f8JAAcA/f8LAP3//f8LAP///f8LAAEA/f8LAAMA/v/0/wAA/v/2//r//v/2//z//v/2//7//v/2/wAA/v/2/wIA/v/2/wQA/v/2/wYA/v/4//j//v/4//r//v/4//z//v/4//7//v/4/wAA/v/4/wIA/v/4/wQA/v/4/wYA/v/4/wgA/v/6//b//v/6//j//v/6//r//v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/6/wYA/v/6/wgA/v/6/woA/v/8//b//v/8//j//v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/8/wgA/v/8/woA/v/+//b//v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v/+/woA/v8AAPT//v8AAPb//v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8AAAoA/v8AAAwA/v8CAPb//v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8CAAoA/v8EAPb//v8EAPj//v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8EAAgA/v8EAAoA/v8GAPb//v8GAPj//v8GAPr//v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8GAAYA/v8GAAgA/v8GAAoA/v8IAPj//v8IAPr//v8IAPz//v8IAP7//v8IAAAA/v8IAAIA/v8IAAQA/v8IAAYA/v8IAAgA/v8KAPr//v8KAPz//v8KAP7//v8KAAAA/v8KAAIA/v8KAAQA/v8KAAYA/v8MAAAA///1//v////1//3////1///////1/wEA///1/wMA///1/wUA///3//n////3//v////3//3////3///////3/wEA///3/wMA///3/wUA///3/wcA///5//f////5//n////5//v////5//3////5///////5/wEA///5/wMA///5/wUA///5/wcA///5/wkA///7//X////7//f////7//n////7//v////7//3////7///////7/wEA///7/wMA///7/wUA///7/wcA///7/wkA///7/wsA///9//X////9//f////9//n////9//v////9//3////9///////9/wEA///9/wMA///9/wUA///9/wcA///9/wkA///9/wsA//////X///////f///////n///////v///////3//////////////wEA/////wMA/////wUA/////wcA/////wkA/////wsA//8BAPX///8BAPf///8BAPn///8BAPv///8BAP3///8BAP////8BAAEA//8BAAMA//8BAAUA//8BAAcA//8BAAkA//8BAAsA//8DAPX///8DAPf///8DAPn///8DAPv///8DAP3///8DAP////8DAAEA//8DAAMA//8DAAUA//8DAAcA//8DAAkA//8DAAsA//8FAPX///8FAPf///8FAPn///8FAPv///8FAP3///8FAP////8FAAEA//8FAAMA//8FAAUA//8FAAcA//8FAAkA//8FAAsA//8HAPf///8HAPn///8HAPv///8HAP3///8HAP////8HAAEA//8HAAMA//8HAAUA//8HAAcA//8HAAkA//8JAPn///8JAPv///8JAP3///8JAP////8JAAEA//8JAAMA//8JAAUA//8JAAcA//8LAPv///8LAP3///8LAP////8LAAEA//8LAAMA//8LAAUAAAD0//7/AAD0/wAAAAD0/wIAAAD2//r/AAD2//z/AAD2//7/AAD2/wAAAAD2/wIAAAD2/wQAAAD2/wYAAAD4//j/AAD4//r/AAD4//z/AAD4//7/AAD4/wAAAAD4/wIAAAD4/wQAAAD4/wYAAAD4/wgAAAD6//b/AAD6//j/AAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD6/wgAAAD6/woAAAD8//b/AAD8//j/AAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD8/wgAAAD8/woAAAD+//T/AAD+//b/AAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD+/woAAAD+/wwAAAAAAPT/AAAAAPb/AAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAAAAAoAAAAAAAwAAAACAPT/AAACAPb/AAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAACAAoAAAACAAwAAAAEAPb/AAAEAPj/AAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAEAAgAAAAEAAoAAAAGAPb/AAAGAPj/AAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAGAAgAAAAGAAoAAAAIAPj/AAAIAPr/AAAIAPz/AAAIAP7/AAAIAAAAAAAIAAIAAAAIAAQAAAAIAAYAAAAIAAgAAAAKAPr/AAAKAPz/AAAKAP7/AAAKAAAAAAAKAAIAAAAKAAQAAAAKAAYAAAAMAP7/AAAMAAAAAAAMAAIAAQD1//v/AQD1//3/AQD1////AQD1/wEAAQD1/wMAAQD1/wUAAQD3//n/AQD3//v/AQD3//3/AQD3////AQD3/wEAAQD3/wMAAQD3/wUAAQD3/wcAAQD5//f/AQD5//n/AQD5//v/AQD5//3/AQD5////AQD5/wEAAQD5/wMAAQD5/wUAAQD5/wcAAQD5/wkAAQD7//X/AQD7//f/AQD7//n/AQD7//v/AQD7//3/AQD7////AQD7/wEAAQD7/wMAAQD7/wUAAQD7/wcAAQD7/wkAAQD7/wsAAQD9//X/AQD9//f/AQD9//n/AQD9//v/AQD9//3/AQD9////AQD9/wEAAQD9/wMAAQD9/wUAAQD9/wcAAQD9/wkAAQD9/wsAAQD///X/AQD///f/AQD///n/AQD///v/AQD///3/AQD/////AQD//wEAAQD//wMAAQD//wUAAQD//wcAAQD//wkAAQD//wsAAQABAPX/AQABAPf/AQABAPn/AQABAPv/AQABAP3/AQABAP//AQABAAEAAQABAAMAAQABAAUAAQABAAcAAQABAAkAAQABAAsAAQADAPX/AQADAPf/AQADAPn/AQADAPv/AQADAP3/AQADAP//AQADAAEAAQADAAMAAQADAAUAAQADAAcAAQADAAkAAQADAAsAAQAFAPX/AQAFAPf/AQAFAPn/AQAFAPv/AQAFAP3/AQAFAP//AQAFAAEAAQAFAAMAAQAFAAUAAQAFAAcAAQAFAAkAAQAFAAsAAQAHAPf/AQAHAPn/AQAHAPv/AQAHAP3/AQAHAP//AQAHAAEAAQAHAAMAAQAHAAUAAQAHAAcAAQAHAAkAAQAJAPn/AQAJAPv/AQAJAP3/AQAJAP//AQAJAAEAAQAJAAMAAQAJAAUAAQAJAAcAAQALAPv/AQALAP3/AQALAP//AQALAAEAAQALAAMAAQALAAUAAgD0/wAAAgD2//r/AgD2//z/AgD2//7/AgD2/wAAAgD2/wIAAgD2/wQAAgD2/wYAAgD4//j/AgD4//r/AgD4//z/AgD4//7/AgD4/wAAAgD4/wIAAgD4/wQAAgD4/wYAAgD4/wgAAgD6//b/AgD6//j/AgD6//r/AgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD6/wYAAgD6/wgAAgD6/woAAgD8//b/AgD8//j/AgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD8/wgAAgD8/woAAgD+//b/AgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD+/woAAgAAAPT/AgAAAPb/AgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgAAAAoAAgAAAAwAAgACAPb/AgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgACAAoAAgAEAPb/AgAEAPj/AgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAEAAgAAgAEAAoAAgAGAPb/AgAGAPj/AgAGAPr/AgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAGAAYAAgAGAAgAAgAGAAoAAgAIAPj/AgAIAPr/AgAIAPz/AgAIAP7/AgAIAAAAAgAIAAIAAgAIAAQAAgAIAAYAAgAIAAgAAgAKAPr/AgAKAPz/AgAKAP7/AgAKAAAAAgAKAAIAAgAKAAQAAgAKAAYAAgAMAAAAAwD1//3/AwD1////AwD1/wEAAwD1/wMAAwD3//n/AwD3//v/AwD3//3/AwD3////AwD3/wEAAwD3/wMAAwD3/wUAAwD3/wcAAwD5//f/AwD5//n/AwD5//v/AwD5//3/AwD5////AwD5/wEAAwD5/wMAAwD5/wUAAwD5/wcAAwD5/wkAAwD7//f/AwD7//n/AwD7//v/AwD7//3/AwD7////AwD7/wEAAwD7/wMAAwD7/wUAAwD7/wcAAwD7/wkAAwD9//X/AwD9//f/AwD9//n/AwD9//v/AwD9//3/AwD9////AwD9/wEAAwD9/wMAAwD9/wUAAwD9/wcAAwD9/wkAAwD9/wsAAwD///X/AwD///f/AwD///n/AwD///v/AwD///3/AwD/////AwD//wEAAwD//wMAAwD//wUAAwD//wcAAwD//wkAAwD//wsAAwABAPX/AwABAPf/AwABAPn/AwABAPv/AwABAP3/AwABAP//AwABAAEAAwABAAMAAwABAAUAAwABAAcAAwABAAkAAwABAAsAAwADAPX/AwADAPf/AwADAPn/AwADAPv/AwADAP3/AwADAP//AwADAAEAAwADAAMAAwADAAUAAwADAAcAAwADAAkAAwADAAsAAwAFAPf/AwAFAPn/AwAFAPv/AwAFAP3/AwAFAP//AwAFAAEAAwAFAAMAAwAFAAUAAwAFAAcAAwAFAAkAAwAHAPf/AwAHAPn/AwAHAPv/AwAHAP3/AwAHAP//AwAHAAEAAwAHAAMAAwAHAAUAAwAHAAcAAwAHAAkAAwAJAPn/AwAJAPv/AwAJAP3/AwAJAP//AwAJAAEAAwAJAAMAAwAJAAUAAwAJAAcAAwALAP3/AwALAP//AwALAAEAAwALAAMABAD2//z/BAD2//7/BAD2/wAABAD2/wIABAD2/wQABAD4//j/BAD4//r/BAD4//z/BAD4//7/BAD4/wAABAD4/wIABAD4/wQABAD4/wYABAD4/wgABAD6//j/BAD6//r/BAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD6/wYABAD6/wgABAD8//b/BAD8//j/BAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD8/wgABAD8/woABAD+//b/BAD+//j/BAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD+/wgABAD+/woABAAAAPb/BAAAAPj/BAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAAAAAgABAAAAAoABAACAPb/BAACAPj/BAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAACAAgABAACAAoABAAEAPb/BAAEAPj/BAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAEAAgABAAEAAoABAAGAPj/BAAGAPr/BAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAGAAYABAAGAAgABAAIAPj/BAAIAPr/BAAIAPz/BAAIAP7/BAAIAAAABAAIAAIABAAIAAQABAAIAAYABAAIAAgABAAKAPz/BAAKAP7/BAAKAAAABAAKAAIABAAKAAQABQD1////BQD1/wEABQD3//v/BQD3//3/BQD3////BQD3/wEABQD3/wMABQD3/wUABQD5//n/BQD5//v/BQD5//3/BQD5////BQD5/wEABQD5/wMABQD5/wUABQD5/wcABQD7//f/BQD7//n/BQD7//v/BQD7//3/BQD7////BQD7/wEABQD7/wMABQD7/wUABQD7/wcABQD7/wkABQD9//f/BQD9//n/BQD9//v/BQD9//3/BQD9////BQD9/wEABQD9/wMABQD9/wUABQD9/wcABQD9/wkABQD///X/BQD///f/BQD///n/BQD///v/BQD///3/BQD/////BQD//wEABQD//wMABQD//wUABQD//wcABQD//wkABQD//wsABQABAPX/BQABAPf/BQABAPn/BQABAPv/BQABAP3/BQABAP//BQABAAEABQABAAMABQABAAUABQABAAcABQABAAkABQABAAsABQADAPf/BQADAPn/BQADAPv/BQADAP3/BQADAP//BQADAAEABQADAAMABQADAAUABQADAAcABQADAAkABQAFAPf/BQAFAPn/BQAFAPv/BQAFAP3/BQAFAP//BQAFAAEABQAFAAMABQAFAAUABQAFAAcABQAFAAkABQAHAPn/BQAHAPv/BQAHAP3/BQAHAP//BQAHAAEABQAHAAMABQAHAAUABQAHAAcABQAJAPv/BQAJAP3/BQAJAP//BQAJAAEABQAJAAMABQAJAAUABQALAP//BQALAAEABgD2//7/BgD2/wAABgD2/wIABgD4//r/BgD4//z/BgD4//7/BgD4/wAABgD4/wIABgD4/wQABgD4/wYABgD6//j/BgD6//r/BgD6//z/BgD6//7/BgD6/wAABgD6/wIABgD6/wQABgD6/wYABgD6/wgABgD8//j/BgD8//r/BgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD8/wYABgD8/wgABgD+//b/BgD+//j/BgD+//r/BgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD+/wYABgD+/wgABgD+/woABgAAAPb/BgAAAPj/BgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgAAAAgABgAAAAoABgACAPb/BgACAPj/BgACAPr/BgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgACAAYABgACAAgABgACAAoABgAEAPj/BgAEAPr/BgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAEAAYABgAEAAgABgAGAPj/BgAGAPr/BgAGAPz/BgAGAP7/BgAGAAAABgAGAAIABgAGAAQABgAGAAYABgAGAAgABgAIAPr/BgAIAPz/BgAIAP7/BgAIAAAABgAIAAIABgAIAAQABgAIAAYABgAKAP7/BgAKAAAABgAKAAIABwD3//3/BwD3////BwD3/wEABwD3/wMABwD5//n/BwD5//v/BwD5//3/BwD5////BwD5/wEABwD5/wMABwD5/wUABwD5/wcABwD7//n/BwD7//v/BwD7//3/BwD7////BwD7/wEABwD7/wMABwD7/wUABwD7/wcABwD9//f/BwD9//n/BwD9//v/BwD9//3/BwD9////BwD9/wEABwD9/wMABwD9/wUABwD9/wcABwD9/wkABwD///f/BwD///n/BwD///v/BwD///3/BwD/////BwD//wEABwD//wMABwD//wUABwD//wcABwD//wkABwABAPf/BwABAPn/BwABAPv/BwABAP3/BwABAP//BwABAAEABwABAAMABwABAAUABwABAAcABwABAAkABwADAPf/BwADAPn/BwADAPv/BwADAP3/BwADAP//BwADAAEABwADAAMABwADAAUABwADAAcABwADAAkABwAFAPn/BwAFAPv/BwAFAP3/BwAFAP//BwAFAAEABwAFAAMABwAFAAUABwAFAAcABwAHAPn/BwAHAPv/BwAHAP3/BwAHAP//BwAHAAEABwAHAAMABwAHAAUABwAHAAcABwAJAP3/BwAJAP//BwAJAAEABwAJAAMACAD4//z/CAD4//7/CAD4/wAACAD4/wIACAD4/wQACAD6//r/CAD6//z/CAD6//7/CAD6/wAACAD6/wIACAD6/wQACAD6/wYACAD8//j/CAD8//r/CAD8//z/CAD8//7/CAD8/wAACAD8/wIACAD8/wQACAD8/wYACAD8/wgACAD+//j/CAD+//r/CAD+//z/CAD+//7/CAD+/wAACAD+/wIACAD+/wQACAD+/wYACAD+/wgACAAAAPj/CAAAAPr/CAAAAPz/CAAAAP7/CAAAAAAACAAAAAIACAAAAAQACAAAAAYACAAAAAgACAACAPj/CAACAPr/CAACAPz/CAACAP7/CAACAAAACAACAAIACAACAAQACAACAAYACAACAAgACAAEAPj/CAAEAPr/CAAEAPz/CAAEAP7/CAAEAAAACAAEAAIACAAEAAQACAAEAAYACAAEAAgACAAGAPr/CAAGAPz/CAAGAP7/CAAGAAAACAAGAAIACAAGAAQACAAGAAYACAAIAPz/CAAIAP7/CAAIAAAACAAIAAIACAAIAAQACQD5//3/CQD5////CQD5/wEACQD5/wMACQD7//v/CQD7//3/CQD7////CQD7/wEACQD7/wMACQD7/wUACQD9//n/CQD9//v/CQD9//3/CQD9////CQD9/wEACQD9/wMACQD9/wUACQD9/wcACQD///n/CQD///v/CQD///3/CQD/////CQD//wEACQD//wMACQD//wUACQD//wcACQABAPn/CQABAPv/CQABAP3/CQABAP//CQABAAEACQABAAMACQABAAUACQABAAcACQADAPn/CQADAPv/CQADAP3/CQADAP//CQADAAEACQADAAMACQADAAUACQADAAcACQAFAPv/CQAFAP3/CQAFAP//CQAFAAEACQAFAAMACQAFAAUACQAHAP3/CQAHAP//CQAHAAEACQAHAAMACgD6//7/CgD6/wAACgD6/wIACgD8//z/CgD8//7/CgD8/wAACgD8/wIACgD8/wQACgD+//r/CgD+//z/CgD+//7/CgD+/wAACgD+/wIACgD+/wQACgD+/wYACgAAAPr/CgAAAPz/CgAAAP7/CgAAAAAACgAAAAIACgAAAAQACgAAAAYACgACAPr/CgACAPz/CgACAP7/CgACAAAACgACAAIACgACAAQACgACAAYACgAEAPz/CgAEAP7/CgAEAAAACgAEAAIACgAEAAQACgAGAP7/CgAGAAAACgAGAAIACwD7////CwD7/wEACwD9//3/CwD9////CwD9/wEACwD9/wMACwD///v/CwD///3/CwD/////CwD//wEACwD//wMACwD//wUACwABAPv/CwABAP3/CwABAP//CwABAAEACwABAAMACwABAAUACwADAP3/CwADAP//CwADAAEACwADAAMACwAFAP//CwAFAAEADAD+/wAADAAAAP7/DAAAAAAADAAAAAIADAACAAAA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.01772783003829353,\"absorptive\":true,\"n_reflections\":1916,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", - "Fe (bcc)": "{\"name\":\"Fe\",\"spacegroup\":\"Im-3m (229)\",\"pointgroup\":\"m-3m\",\"cell\":[[2.866,0.0,0.0],[0.0,2.866,0.0],[0.0,0.0,2.866]],\"recip\":[[0.34891835310537334,0.0,0.0],[0.0,0.34891835310537334,0.0],[0.0,0.0,0.34891835310537334]],\"positions_frac\":[[0.0,0.0,0.0],[0.5,0.5,0.5]],\"numbers\":[26,26],\"symbols\":[\"Fe\",\"Fe\"],\"colors\":[[0.878,0.4,0.2],[0.878,0.4,0.2]],\"radii\":[1.32,1.32],\"hkl_i16\":\"+P/+//7/+P/+/wAA+P/+/wIA+P//////+P///wEA+P8AAP7/+P8AAAAA+P8AAAIA+P8BAP//+P8BAAEA+P8CAP7/+P8CAAAA+P8CAAIA+f/8////+f/8/wEA+f/9//7/+f/9/wAA+f/9/wIA+f/+//3/+f/+////+f/+/wEA+f/+/wMA+f////z/+f////7/+f///wAA+f///wIA+f///wQA+f8AAP3/+f8AAP//+f8AAAEA+f8AAAMA+f8BAPz/+f8BAP7/+f8BAAAA+f8BAAIA+f8BAAQA+f8CAP3/+f8CAP//+f8CAAEA+f8CAAMA+f8DAP7/+f8DAAAA+f8DAAIA+f8EAP//+f8EAAEA+v/6/wAA+v/7//3/+v/7////+v/7/wEA+v/7/wMA+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/9//v/+v/9//3/+v/9////+v/9/wEA+v/9/wMA+v/9/wUA+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v////v/+v////3/+v//////+v///wEA+v///wMA+v///wUA+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8BAPv/+v8BAP3/+v8BAP//+v8BAAEA+v8BAAMA+v8BAAUA+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8DAPv/+v8DAP3/+v8DAP//+v8DAAEA+v8DAAMA+v8DAAUA+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8FAP3/+v8FAP//+v8FAAEA+v8FAAMA+v8GAAAA+//6//3/+//6////+//6/wEA+//6/wMA+//7//z/+//7//7/+//7/wAA+//7/wIA+//7/wQA+//8//v/+//8//3/+//8////+//8/wEA+//8/wMA+//8/wUA+//9//r/+//9//z/+//9//7/+//9/wAA+//9/wIA+//9/wQA+//9/wYA+//+//v/+//+//3/+//+////+//+/wEA+//+/wMA+//+/wUA+/////r/+/////z/+/////7/+////wAA+////wIA+////wQA+////wYA+/8AAPv/+/8AAP3/+/8AAP//+/8AAAEA+/8AAAMA+/8AAAUA+/8BAPr/+/8BAPz/+/8BAP7/+/8BAAAA+/8BAAIA+/8BAAQA+/8BAAYA+/8CAPv/+/8CAP3/+/8CAP//+/8CAAEA+/8CAAMA+/8CAAUA+/8DAPr/+/8DAPz/+/8DAP7/+/8DAAAA+/8DAAIA+/8DAAQA+/8DAAYA+/8EAPv/+/8EAP3/+/8EAP//+/8EAAEA+/8EAAMA+/8EAAUA+/8FAPz/+/8FAP7/+/8FAAAA+/8FAAIA+/8FAAQA+/8GAP3/+/8GAP//+/8GAAEA+/8GAAMA/P/5/////P/5/wEA/P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/7//v//P/7//3//P/7/////P/7/wEA/P/7/wMA/P/7/wUA/P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/9//v//P/9//3//P/9/////P/9/wEA/P/9/wMA/P/9/wUA/P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P////n//P////v//P////3//P///////P///wEA/P///wMA/P///wUA/P///wcA/P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8BAPn//P8BAPv//P8BAP3//P8BAP///P8BAAEA/P8BAAMA/P8BAAUA/P8BAAcA/P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8DAPv//P8DAP3//P8DAP///P8DAAEA/P8DAAMA/P8DAAUA/P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8FAPv//P8FAP3//P8FAP///P8FAAEA/P8FAAMA/P8FAAUA/P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8HAP///P8HAAEA/f/5//7//f/5/wAA/f/5/wIA/f/6//v//f/6//3//f/6/////f/6/wEA/f/6/wMA/f/6/wUA/f/7//r//f/7//z//f/7//7//f/7/wAA/f/7/wIA/f/7/wQA/f/7/wYA/f/8//v//f/8//3//f/8/////f/8/wEA/f/8/wMA/f/8/wUA/f/9//r//f/9//z//f/9//7//f/9/wAA/f/9/wIA/f/9/wQA/f/9/wYA/f/+//n//f/+//v//f/+//3//f/+/////f/+/wEA/f/+/wMA/f/+/wUA/f/+/wcA/f////r//f////z//f////7//f///wAA/f///wIA/f///wQA/f///wYA/f8AAPn//f8AAPv//f8AAP3//f8AAP///f8AAAEA/f8AAAMA/f8AAAUA/f8AAAcA/f8BAPr//f8BAPz//f8BAP7//f8BAAAA/f8BAAIA/f8BAAQA/f8BAAYA/f8CAPn//f8CAPv//f8CAP3//f8CAP///f8CAAEA/f8CAAMA/f8CAAUA/f8CAAcA/f8DAPr//f8DAPz//f8DAP7//f8DAAAA/f8DAAIA/f8DAAQA/f8DAAYA/f8EAPv//f8EAP3//f8EAP///f8EAAEA/f8EAAMA/f8EAAUA/f8FAPr//f8FAPz//f8FAP7//f8FAAAA/f8FAAIA/f8FAAQA/f8FAAYA/f8GAPv//f8GAP3//f8GAP///f8GAAEA/f8GAAMA/f8GAAUA/f8HAP7//f8HAAAA/f8HAAIA/v/4//7//v/4/wAA/v/4/wIA/v/5//3//v/5/////v/5/wEA/v/5/wMA/v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/7//v//v/7//3//v/7/////v/7/wEA/v/7/wMA/v/7/wUA/v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/9//n//v/9//v//v/9//3//v/9/////v/9/wEA/v/9/wMA/v/9/wUA/v/9/wcA/v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v////n//v////v//v////3//v///////v///wEA/v///wMA/v///wUA/v///wcA/v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8BAPn//v8BAPv//v8BAP3//v8BAP///v8BAAEA/v8BAAMA/v8BAAUA/v8BAAcA/v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8DAPn//v8DAPv//v8DAP3//v8DAP///v8DAAEA/v8DAAMA/v8DAAUA/v8DAAcA/v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8FAPv//v8FAP3//v8FAP///v8FAAEA/v8FAAMA/v8FAAUA/v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8HAP3//v8HAP///v8HAAEA/v8HAAMA/v8IAP7//v8IAAAA/v8IAAIA///4///////4/wEA///5//z////5//7////5/wAA///5/wIA///5/wQA///6//v////6//3////6///////6/wEA///6/wMA///6/wUA///7//r////7//z////7//7////7/wAA///7/wIA///7/wQA///7/wYA///8//n////8//v////8//3////8///////8/wEA///8/wMA///8/wUA///8/wcA///9//r////9//z////9//7////9/wAA///9/wIA///9/wQA///9/wYA///+//n////+//v////+//3////+///////+/wEA///+/wMA///+/wUA///+/wcA//////j///////r///////z///////7//////wAA/////wIA/////wQA/////wYA/////wgA//8AAPn///8AAPv///8AAP3///8AAP////8AAAEA//8AAAMA//8AAAUA//8AAAcA//8BAPj///8BAPr///8BAPz///8BAP7///8BAAAA//8BAAIA//8BAAQA//8BAAYA//8BAAgA//8CAPn///8CAPv///8CAP3///8CAP////8CAAEA//8CAAMA//8CAAUA//8CAAcA//8DAPr///8DAPz///8DAP7///8DAAAA//8DAAIA//8DAAQA//8DAAYA//8EAPn///8EAPv///8EAP3///8EAP////8EAAEA//8EAAMA//8EAAUA//8EAAcA//8FAPr///8FAPz///8FAP7///8FAAAA//8FAAIA//8FAAQA//8FAAYA//8GAPv///8GAP3///8GAP////8GAAEA//8GAAMA//8GAAUA//8HAPz///8HAP7///8HAAAA//8HAAIA//8HAAQA//8IAP////8IAAEAAAD4//7/AAD4/wAAAAD4/wIAAAD5//3/AAD5////AAD5/wEAAAD5/wMAAAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD7//v/AAD7//3/AAD7////AAD7/wEAAAD7/wMAAAD7/wUAAAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD9//n/AAD9//v/AAD9//3/AAD9////AAD9/wEAAAD9/wMAAAD9/wUAAAD9/wcAAAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD///n/AAD///v/AAD///3/AAD/////AAD//wEAAAD//wMAAAD//wUAAAD//wcAAAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAABAPn/AAABAPv/AAABAP3/AAABAP//AAABAAEAAAABAAMAAAABAAUAAAABAAcAAAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAADAPn/AAADAPv/AAADAP3/AAADAP//AAADAAEAAAADAAMAAAADAAUAAAADAAcAAAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAFAPv/AAAFAP3/AAAFAP//AAAFAAEAAAAFAAMAAAAFAAUAAAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAHAP3/AAAHAP//AAAHAAEAAAAHAAMAAAAIAP7/AAAIAAAAAAAIAAIAAQD4////AQD4/wEAAQD5//z/AQD5//7/AQD5/wAAAQD5/wIAAQD5/wQAAQD6//v/AQD6//3/AQD6////AQD6/wEAAQD6/wMAAQD6/wUAAQD7//r/AQD7//z/AQD7//7/AQD7/wAAAQD7/wIAAQD7/wQAAQD7/wYAAQD8//n/AQD8//v/AQD8//3/AQD8////AQD8/wEAAQD8/wMAAQD8/wUAAQD8/wcAAQD9//r/AQD9//z/AQD9//7/AQD9/wAAAQD9/wIAAQD9/wQAAQD9/wYAAQD+//n/AQD+//v/AQD+//3/AQD+////AQD+/wEAAQD+/wMAAQD+/wUAAQD+/wcAAQD///j/AQD///r/AQD///z/AQD///7/AQD//wAAAQD//wIAAQD//wQAAQD//wYAAQD//wgAAQAAAPn/AQAAAPv/AQAAAP3/AQAAAP//AQAAAAEAAQAAAAMAAQAAAAUAAQAAAAcAAQABAPj/AQABAPr/AQABAPz/AQABAP7/AQABAAAAAQABAAIAAQABAAQAAQABAAYAAQABAAgAAQACAPn/AQACAPv/AQACAP3/AQACAP//AQACAAEAAQACAAMAAQACAAUAAQACAAcAAQADAPr/AQADAPz/AQADAP7/AQADAAAAAQADAAIAAQADAAQAAQADAAYAAQAEAPn/AQAEAPv/AQAEAP3/AQAEAP//AQAEAAEAAQAEAAMAAQAEAAUAAQAEAAcAAQAFAPr/AQAFAPz/AQAFAP7/AQAFAAAAAQAFAAIAAQAFAAQAAQAFAAYAAQAGAPv/AQAGAP3/AQAGAP//AQAGAAEAAQAGAAMAAQAGAAUAAQAHAPz/AQAHAP7/AQAHAAAAAQAHAAIAAQAHAAQAAQAIAP//AQAIAAEAAgD4//7/AgD4/wAAAgD4/wIAAgD5//3/AgD5////AgD5/wEAAgD5/wMAAgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD7//v/AgD7//3/AgD7////AgD7/wEAAgD7/wMAAgD7/wUAAgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD9//n/AgD9//v/AgD9//3/AgD9////AgD9/wEAAgD9/wMAAgD9/wUAAgD9/wcAAgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD///n/AgD///v/AgD///3/AgD/////AgD//wEAAgD//wMAAgD//wUAAgD//wcAAgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgABAPn/AgABAPv/AgABAP3/AgABAP//AgABAAEAAgABAAMAAgABAAUAAgABAAcAAgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgADAPn/AgADAPv/AgADAP3/AgADAP//AgADAAEAAgADAAMAAgADAAUAAgADAAcAAgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAFAPv/AgAFAP3/AgAFAP//AgAFAAEAAgAFAAMAAgAFAAUAAgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAHAP3/AgAHAP//AgAHAAEAAgAHAAMAAgAIAP7/AgAIAAAAAgAIAAIAAwD5//7/AwD5/wAAAwD5/wIAAwD6//v/AwD6//3/AwD6////AwD6/wEAAwD6/wMAAwD6/wUAAwD7//r/AwD7//z/AwD7//7/AwD7/wAAAwD7/wIAAwD7/wQAAwD7/wYAAwD8//v/AwD8//3/AwD8////AwD8/wEAAwD8/wMAAwD8/wUAAwD9//r/AwD9//z/AwD9//7/AwD9/wAAAwD9/wIAAwD9/wQAAwD9/wYAAwD+//n/AwD+//v/AwD+//3/AwD+////AwD+/wEAAwD+/wMAAwD+/wUAAwD+/wcAAwD///r/AwD///z/AwD///7/AwD//wAAAwD//wIAAwD//wQAAwD//wYAAwAAAPn/AwAAAPv/AwAAAP3/AwAAAP//AwAAAAEAAwAAAAMAAwAAAAUAAwAAAAcAAwABAPr/AwABAPz/AwABAP7/AwABAAAAAwABAAIAAwABAAQAAwABAAYAAwACAPn/AwACAPv/AwACAP3/AwACAP//AwACAAEAAwACAAMAAwACAAUAAwACAAcAAwADAPr/AwADAPz/AwADAP7/AwADAAAAAwADAAIAAwADAAQAAwADAAYAAwAEAPv/AwAEAP3/AwAEAP//AwAEAAEAAwAEAAMAAwAEAAUAAwAFAPr/AwAFAPz/AwAFAP7/AwAFAAAAAwAFAAIAAwAFAAQAAwAFAAYAAwAGAPv/AwAGAP3/AwAGAP//AwAGAAEAAwAGAAMAAwAGAAUAAwAHAP7/AwAHAAAAAwAHAAIABAD5////BAD5/wEABAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD7//v/BAD7//3/BAD7////BAD7/wEABAD7/wMABAD7/wUABAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD9//v/BAD9//3/BAD9////BAD9/wEABAD9/wMABAD9/wUABAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD///n/BAD///v/BAD///3/BAD/////BAD//wEABAD//wMABAD//wUABAD//wcABAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAABAPn/BAABAPv/BAABAP3/BAABAP//BAABAAEABAABAAMABAABAAUABAABAAcABAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAADAPv/BAADAP3/BAADAP//BAADAAEABAADAAMABAADAAUABAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAFAPv/BAAFAP3/BAAFAP//BAAFAAEABAAFAAMABAAFAAUABAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAHAP//BAAHAAEABQD6//3/BQD6////BQD6/wEABQD6/wMABQD7//z/BQD7//7/BQD7/wAABQD7/wIABQD7/wQABQD8//v/BQD8//3/BQD8////BQD8/wEABQD8/wMABQD8/wUABQD9//r/BQD9//z/BQD9//7/BQD9/wAABQD9/wIABQD9/wQABQD9/wYABQD+//v/BQD+//3/BQD+////BQD+/wEABQD+/wMABQD+/wUABQD///r/BQD///z/BQD///7/BQD//wAABQD//wIABQD//wQABQD//wYABQAAAPv/BQAAAP3/BQAAAP//BQAAAAEABQAAAAMABQAAAAUABQABAPr/BQABAPz/BQABAP7/BQABAAAABQABAAIABQABAAQABQABAAYABQACAPv/BQACAP3/BQACAP//BQACAAEABQACAAMABQACAAUABQADAPr/BQADAPz/BQADAP7/BQADAAAABQADAAIABQADAAQABQADAAYABQAEAPv/BQAEAP3/BQAEAP//BQAEAAEABQAEAAMABQAEAAUABQAFAPz/BQAFAP7/BQAFAAAABQAFAAIABQAFAAQABQAGAP3/BQAGAP//BQAGAAEABQAGAAMABgD6/wAABgD7//3/BgD7////BgD7/wEABgD7/wMABgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD9//v/BgD9//3/BgD9////BgD9/wEABgD9/wMABgD9/wUABgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD///v/BgD///3/BgD/////BgD//wEABgD//wMABgD//wUABgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgABAPv/BgABAP3/BgABAP//BgABAAEABgABAAMABgABAAUABgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgADAPv/BgADAP3/BgADAP//BgADAAEABgADAAMABgADAAUABgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAFAP3/BgAFAP//BgAFAAEABgAFAAMABgAGAAAABwD8////BwD8/wEABwD9//7/BwD9/wAABwD9/wIABwD+//3/BwD+////BwD+/wEABwD+/wMABwD///z/BwD///7/BwD//wAABwD//wIABwD//wQABwAAAP3/BwAAAP//BwAAAAEABwAAAAMABwABAPz/BwABAP7/BwABAAAABwABAAIABwABAAQABwACAP3/BwACAP//BwACAAEABwACAAMABwADAP7/BwADAAAABwADAAIABwAEAP//BwAEAAEACAD+//7/CAD+/wAACAD+/wIACAD/////CAD//wEACAAAAP7/CAAAAAAACAAAAAIACAABAP//CAABAAEACAACAP7/CAACAAAACAACAAIA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.009831241654189844,\"absorptive\":true,\"n_reflections\":1288,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", - "Ti (hcp)": "{\"name\":\"Ti\",\"spacegroup\":\"P6_3/mmc (194)\",\"pointgroup\":\"6/mmm\",\"cell\":[[2.9505,0.0,0.0],[-1.47525,2.555207953865986,0.0],[0.0,0.0,4.6855]],\"recip\":[[0.3389256058295204,0.19567878976093062,0.0],[0.0,0.39135757952186123,0.0],[0.0,0.0,0.21342439440828084]],\"positions_frac\":[[0.0,0.0,0.0],[0.3333333333333333,0.6666666666666666,0.49999999999999994]],\"numbers\":[22,22],\"symbols\":[\"Ti\",\"Ti\"],\"colors\":[[0.749,0.761,0.78],[0.749,0.761,0.78]],\"radii\":[1.6,1.6],\"hkl_i16\":\"+P8BAP7/+P8BAP//+P8BAAAA+P8BAAEA+P8BAAIA+P8CAPz/+P8CAP3/+P8CAP7/+P8CAP//+P8CAAAA+P8CAAEA+P8CAAIA+P8CAAMA+P8CAAQA+P8DAPv/+P8DAPz/+P8DAP3/+P8DAP7/+P8DAP//+P8DAAAA+P8DAAEA+P8DAAIA+P8DAAMA+P8DAAQA+P8DAAUA+P8EAPr/+P8EAPv/+P8EAPz/+P8EAP3/+P8EAP7/+P8EAP//+P8EAAAA+P8EAAEA+P8EAAIA+P8EAAMA+P8EAAQA+P8EAAUA+P8EAAYA+P8FAPv/+P8FAPz/+P8FAP3/+P8FAP7/+P8FAP//+P8FAAAA+P8FAAEA+P8FAAIA+P8FAAMA+P8FAAQA+P8FAAUA+P8GAPz/+P8GAP3/+P8GAP7/+P8GAP//+P8GAAAA+P8GAAEA+P8GAAIA+P8GAAMA+P8GAAQA+P8HAP7/+P8HAP//+P8HAAAA+P8HAAEA+P8HAAIA+f////7/+f//////+f///wAA+f///wEA+f///wIA+f8AAPv/+f8AAPz/+f8AAP3/+f8AAP7/+f8AAP//+f8AAAAA+f8AAAEA+f8AAAIA+f8AAAMA+f8AAAQA+f8AAAUA+f8BAPn/+f8BAPr/+f8BAPv/+f8BAPz/+f8BAP3/+f8BAP7/+f8BAP//+f8BAAAA+f8BAAEA+f8BAAIA+f8BAAMA+f8BAAQA+f8BAAUA+f8BAAYA+f8BAAcA+f8CAPj/+f8CAPn/+f8CAPr/+f8CAPv/+f8CAPz/+f8CAP3/+f8CAP7/+f8CAP//+f8CAAAA+f8CAAEA+f8CAAIA+f8CAAMA+f8CAAQA+f8CAAUA+f8CAAYA+f8CAAcA+f8CAAgA+f8DAPj/+f8DAPn/+f8DAPr/+f8DAPv/+f8DAPz/+f8DAP3/+f8DAP7/+f8DAP//+f8DAAAA+f8DAAEA+f8DAAIA+f8DAAMA+f8DAAQA+f8DAAUA+f8DAAYA+f8DAAcA+f8DAAgA+f8EAPj/+f8EAPn/+f8EAPr/+f8EAPv/+f8EAPz/+f8EAP3/+f8EAP7/+f8EAP//+f8EAAAA+f8EAAEA+f8EAAIA+f8EAAMA+f8EAAQA+f8EAAUA+f8EAAYA+f8EAAcA+f8EAAgA+f8FAPj/+f8FAPn/+f8FAPr/+f8FAPv/+f8FAPz/+f8FAP3/+f8FAP7/+f8FAP//+f8FAAAA+f8FAAEA+f8FAAIA+f8FAAMA+f8FAAQA+f8FAAUA+f8FAAYA+f8FAAcA+f8FAAgA+f8GAPn/+f8GAPr/+f8GAPv/+f8GAPz/+f8GAP3/+f8GAP7/+f8GAP//+f8GAAAA+f8GAAEA+f8GAAIA+f8GAAMA+f8GAAQA+f8GAAUA+f8GAAYA+f8GAAcA+f8HAPv/+f8HAPz/+f8HAP3/+f8HAP7/+f8HAP//+f8HAAAA+f8HAAEA+f8HAAIA+f8HAAMA+f8HAAQA+f8HAAUA+f8IAP7/+f8IAP//+f8IAAAA+f8IAAEA+f8IAAIA+v/+//z/+v/+//3/+v/+//7/+v/+////+v/+/wAA+v/+/wEA+v/+/wIA+v/+/wMA+v/+/wQA+v////n/+v////r/+v////v/+v////z/+v////3/+v////7/+v//////+v///wAA+v///wEA+v///wIA+v///wMA+v///wQA+v///wUA+v///wYA+v///wcA+v8AAPj/+v8AAPn/+v8AAPr/+v8AAPv/+v8AAPz/+v8AAP3/+v8AAP7/+v8AAP//+v8AAAAA+v8AAAEA+v8AAAIA+v8AAAMA+v8AAAQA+v8AAAUA+v8AAAYA+v8AAAcA+v8AAAgA+v8BAPf/+v8BAPj/+v8BAPn/+v8BAPr/+v8BAPv/+v8BAPz/+v8BAP3/+v8BAP7/+v8BAP//+v8BAAAA+v8BAAEA+v8BAAIA+v8BAAMA+v8BAAQA+v8BAAUA+v8BAAYA+v8BAAcA+v8BAAgA+v8BAAkA+v8CAPb/+v8CAPf/+v8CAPj/+v8CAPn/+v8CAPr/+v8CAPv/+v8CAPz/+v8CAP3/+v8CAP7/+v8CAP//+v8CAAAA+v8CAAEA+v8CAAIA+v8CAAMA+v8CAAQA+v8CAAUA+v8CAAYA+v8CAAcA+v8CAAgA+v8CAAkA+v8CAAoA+v8DAPb/+v8DAPf/+v8DAPj/+v8DAPn/+v8DAPr/+v8DAPv/+v8DAPz/+v8DAP3/+v8DAP7/+v8DAP//+v8DAAAA+v8DAAEA+v8DAAIA+v8DAAMA+v8DAAQA+v8DAAUA+v8DAAYA+v8DAAcA+v8DAAgA+v8DAAkA+v8DAAoA+v8EAPb/+v8EAPf/+v8EAPj/+v8EAPn/+v8EAPr/+v8EAPv/+v8EAPz/+v8EAP3/+v8EAP7/+v8EAP//+v8EAAAA+v8EAAEA+v8EAAIA+v8EAAMA+v8EAAQA+v8EAAUA+v8EAAYA+v8EAAcA+v8EAAgA+v8EAAkA+v8EAAoA+v8FAPf/+v8FAPj/+v8FAPn/+v8FAPr/+v8FAPv/+v8FAPz/+v8FAP3/+v8FAP7/+v8FAP//+v8FAAAA+v8FAAEA+v8FAAIA+v8FAAMA+v8FAAQA+v8FAAUA+v8FAAYA+v8FAAcA+v8FAAgA+v8FAAkA+v8GAPj/+v8GAPn/+v8GAPr/+v8GAPv/+v8GAPz/+v8GAP3/+v8GAP7/+v8GAP//+v8GAAAA+v8GAAEA+v8GAAIA+v8GAAMA+v8GAAQA+v8GAAUA+v8GAAYA+v8GAAcA+v8GAAgA+v8HAPn/+v8HAPr/+v8HAPv/+v8HAPz/+v8HAP3/+v8HAP7/+v8HAP//+v8HAAAA+v8HAAEA+v8HAAIA+v8HAAMA+v8HAAQA+v8HAAUA+v8HAAYA+v8HAAcA+v8IAPz/+v8IAP3/+v8IAP7/+v8IAP//+v8IAAAA+v8IAAEA+v8IAAIA+v8IAAMA+v8IAAQA+//9//v/+//9//z/+//9//3/+//9//7/+//9////+//9/wAA+//9/wEA+//9/wIA+//9/wMA+//9/wQA+//9/wUA+//+//j/+//+//n/+//+//r/+//+//v/+//+//z/+//+//3/+//+//7/+//+////+//+/wAA+//+/wEA+//+/wIA+//+/wMA+//+/wQA+//+/wUA+//+/wYA+//+/wcA+//+/wgA+/////f/+/////j/+/////n/+/////r/+/////v/+/////z/+/////3/+/////7/+///////+////wAA+////wEA+////wIA+////wMA+////wQA+////wUA+////wYA+////wcA+////wgA+////wkA+/8AAPb/+/8AAPf/+/8AAPj/+/8AAPn/+/8AAPr/+/8AAPv/+/8AAPz/+/8AAP3/+/8AAP7/+/8AAP//+/8AAAAA+/8AAAEA+/8AAAIA+/8AAAMA+/8AAAQA+/8AAAUA+/8AAAYA+/8AAAcA+/8AAAgA+/8AAAkA+/8AAAoA+/8BAPX/+/8BAPb/+/8BAPf/+/8BAPj/+/8BAPn/+/8BAPr/+/8BAPv/+/8BAPz/+/8BAP3/+/8BAP7/+/8BAP//+/8BAAAA+/8BAAEA+/8BAAIA+/8BAAMA+/8BAAQA+/8BAAUA+/8BAAYA+/8BAAcA+/8BAAgA+/8BAAkA+/8BAAoA+/8BAAsA+/8CAPX/+/8CAPb/+/8CAPf/+/8CAPj/+/8CAPn/+/8CAPr/+/8CAPv/+/8CAPz/+/8CAP3/+/8CAP7/+/8CAP//+/8CAAAA+/8CAAEA+/8CAAIA+/8CAAMA+/8CAAQA+/8CAAUA+/8CAAYA+/8CAAcA+/8CAAgA+/8CAAkA+/8CAAoA+/8CAAsA+/8DAPX/+/8DAPb/+/8DAPf/+/8DAPj/+/8DAPn/+/8DAPr/+/8DAPv/+/8DAPz/+/8DAP3/+/8DAP7/+/8DAP//+/8DAAAA+/8DAAEA+/8DAAIA+/8DAAMA+/8DAAQA+/8DAAUA+/8DAAYA+/8DAAcA+/8DAAgA+/8DAAkA+/8DAAoA+/8DAAsA+/8EAPX/+/8EAPb/+/8EAPf/+/8EAPj/+/8EAPn/+/8EAPr/+/8EAPv/+/8EAPz/+/8EAP3/+/8EAP7/+/8EAP//+/8EAAAA+/8EAAEA+/8EAAIA+/8EAAMA+/8EAAQA+/8EAAUA+/8EAAYA+/8EAAcA+/8EAAgA+/8EAAkA+/8EAAoA+/8EAAsA+/8FAPb/+/8FAPf/+/8FAPj/+/8FAPn/+/8FAPr/+/8FAPv/+/8FAPz/+/8FAP3/+/8FAP7/+/8FAP//+/8FAAAA+/8FAAEA+/8FAAIA+/8FAAMA+/8FAAQA+/8FAAUA+/8FAAYA+/8FAAcA+/8FAAgA+/8FAAkA+/8FAAoA+/8GAPf/+/8GAPj/+/8GAPn/+/8GAPr/+/8GAPv/+/8GAPz/+/8GAP3/+/8GAP7/+/8GAP//+/8GAAAA+/8GAAEA+/8GAAIA+/8GAAMA+/8GAAQA+/8GAAUA+/8GAAYA+/8GAAcA+/8GAAgA+/8GAAkA+/8HAPj/+/8HAPn/+/8HAPr/+/8HAPv/+/8HAPz/+/8HAP3/+/8HAP7/+/8HAP//+/8HAAAA+/8HAAEA+/8HAAIA+/8HAAMA+/8HAAQA+/8HAAUA+/8HAAYA+/8HAAcA+/8HAAgA+/8IAPv/+/8IAPz/+/8IAP3/+/8IAP7/+/8IAP//+/8IAAAA+/8IAAEA+/8IAAIA+/8IAAMA+/8IAAQA+/8IAAUA/P/8//r//P/8//v//P/8//z//P/8//3//P/8//7//P/8/////P/8/wAA/P/8/wEA/P/8/wIA/P/8/wMA/P/8/wQA/P/8/wUA/P/8/wYA/P/9//j//P/9//n//P/9//r//P/9//v//P/9//z//P/9//3//P/9//7//P/9/////P/9/wAA/P/9/wEA/P/9/wIA/P/9/wMA/P/9/wQA/P/9/wUA/P/9/wYA/P/9/wcA/P/9/wgA/P/+//b//P/+//f//P/+//j//P/+//n//P/+//r//P/+//v//P/+//z//P/+//3//P/+//7//P/+/////P/+/wAA/P/+/wEA/P/+/wIA/P/+/wMA/P/+/wQA/P/+/wUA/P/+/wYA/P/+/wcA/P/+/wgA/P/+/wkA/P/+/woA/P////X//P////b//P////f//P////j//P////n//P////r//P////v//P////z//P////3//P////7//P///////P///wAA/P///wEA/P///wIA/P///wMA/P///wQA/P///wUA/P///wYA/P///wcA/P///wgA/P///wkA/P///woA/P///wsA/P8AAPX//P8AAPb//P8AAPf//P8AAPj//P8AAPn//P8AAPr//P8AAPv//P8AAPz//P8AAP3//P8AAP7//P8AAP///P8AAAAA/P8AAAEA/P8AAAIA/P8AAAMA/P8AAAQA/P8AAAUA/P8AAAYA/P8AAAcA/P8AAAgA/P8AAAkA/P8AAAoA/P8AAAsA/P8BAPT//P8BAPX//P8BAPb//P8BAPf//P8BAPj//P8BAPn//P8BAPr//P8BAPv//P8BAPz//P8BAP3//P8BAP7//P8BAP///P8BAAAA/P8BAAEA/P8BAAIA/P8BAAMA/P8BAAQA/P8BAAUA/P8BAAYA/P8BAAcA/P8BAAgA/P8BAAkA/P8BAAoA/P8BAAsA/P8BAAwA/P8CAPT//P8CAPX//P8CAPb//P8CAPf//P8CAPj//P8CAPn//P8CAPr//P8CAPv//P8CAPz//P8CAP3//P8CAP7//P8CAP///P8CAAAA/P8CAAEA/P8CAAIA/P8CAAMA/P8CAAQA/P8CAAUA/P8CAAYA/P8CAAcA/P8CAAgA/P8CAAkA/P8CAAoA/P8CAAsA/P8CAAwA/P8DAPT//P8DAPX//P8DAPb//P8DAPf//P8DAPj//P8DAPn//P8DAPr//P8DAPv//P8DAPz//P8DAP3//P8DAP7//P8DAP///P8DAAAA/P8DAAEA/P8DAAIA/P8DAAMA/P8DAAQA/P8DAAUA/P8DAAYA/P8DAAcA/P8DAAgA/P8DAAkA/P8DAAoA/P8DAAsA/P8DAAwA/P8EAPX//P8EAPb//P8EAPf//P8EAPj//P8EAPn//P8EAPr//P8EAPv//P8EAPz//P8EAP3//P8EAP7//P8EAP///P8EAAAA/P8EAAEA/P8EAAIA/P8EAAMA/P8EAAQA/P8EAAUA/P8EAAYA/P8EAAcA/P8EAAgA/P8EAAkA/P8EAAoA/P8EAAsA/P8FAPX//P8FAPb//P8FAPf//P8FAPj//P8FAPn//P8FAPr//P8FAPv//P8FAPz//P8FAP3//P8FAP7//P8FAP///P8FAAAA/P8FAAEA/P8FAAIA/P8FAAMA/P8FAAQA/P8FAAUA/P8FAAYA/P8FAAcA/P8FAAgA/P8FAAkA/P8FAAoA/P8FAAsA/P8GAPb//P8GAPf//P8GAPj//P8GAPn//P8GAPr//P8GAPv//P8GAPz//P8GAP3//P8GAP7//P8GAP///P8GAAAA/P8GAAEA/P8GAAIA/P8GAAMA/P8GAAQA/P8GAAUA/P8GAAYA/P8GAAcA/P8GAAgA/P8GAAkA/P8GAAoA/P8HAPj//P8HAPn//P8HAPr//P8HAPv//P8HAPz//P8HAP3//P8HAP7//P8HAP///P8HAAAA/P8HAAEA/P8HAAIA/P8HAAMA/P8HAAQA/P8HAAUA/P8HAAYA/P8HAAcA/P8HAAgA/P8IAPr//P8IAPv//P8IAPz//P8IAP3//P8IAP7//P8IAP///P8IAAAA/P8IAAEA/P8IAAIA/P8IAAMA/P8IAAQA/P8IAAUA/P8IAAYA/f/7//v//f/7//z//f/7//3//f/7//7//f/7/////f/7/wAA/f/7/wEA/f/7/wIA/f/7/wMA/f/7/wQA/f/7/wUA/f/8//j//f/8//n//f/8//r//f/8//v//f/8//z//f/8//3//f/8//7//f/8/////f/8/wAA/f/8/wEA/f/8/wIA/f/8/wMA/f/8/wQA/f/8/wUA/f/8/wYA/f/8/wcA/f/8/wgA/f/9//b//f/9//f//f/9//j//f/9//n//f/9//r//f/9//v//f/9//z//f/9//3//f/9//7//f/9/////f/9/wAA/f/9/wEA/f/9/wIA/f/9/wMA/f/9/wQA/f/9/wUA/f/9/wYA/f/9/wcA/f/9/wgA/f/9/wkA/f/9/woA/f/+//X//f/+//b//f/+//f//f/+//j//f/+//n//f/+//r//f/+//v//f/+//z//f/+//3//f/+//7//f/+/////f/+/wAA/f/+/wEA/f/+/wIA/f/+/wMA/f/+/wQA/f/+/wUA/f/+/wYA/f/+/wcA/f/+/wgA/f/+/wkA/f/+/woA/f/+/wsA/f////T//f////X//f////b//f////f//f////j//f////n//f////r//f////v//f////z//f////3//f////7//f///////f///wAA/f///wEA/f///wIA/f///wMA/f///wQA/f///wUA/f///wYA/f///wcA/f///wgA/f///wkA/f///woA/f///wsA/f///wwA/f8AAPT//f8AAPX//f8AAPb//f8AAPf//f8AAPj//f8AAPn//f8AAPr//f8AAPv//f8AAPz//f8AAP3//f8AAP7//f8AAP///f8AAAAA/f8AAAEA/f8AAAIA/f8AAAMA/f8AAAQA/f8AAAUA/f8AAAYA/f8AAAcA/f8AAAgA/f8AAAkA/f8AAAoA/f8AAAsA/f8AAAwA/f8BAPP//f8BAPT//f8BAPX//f8BAPb//f8BAPf//f8BAPj//f8BAPn//f8BAPr//f8BAPv//f8BAPz//f8BAP3//f8BAP7//f8BAP///f8BAAAA/f8BAAEA/f8BAAIA/f8BAAMA/f8BAAQA/f8BAAUA/f8BAAYA/f8BAAcA/f8BAAgA/f8BAAkA/f8BAAoA/f8BAAsA/f8BAAwA/f8BAA0A/f8CAPP//f8CAPT//f8CAPX//f8CAPb//f8CAPf//f8CAPj//f8CAPn//f8CAPr//f8CAPv//f8CAPz//f8CAP3//f8CAP7//f8CAP///f8CAAAA/f8CAAEA/f8CAAIA/f8CAAMA/f8CAAQA/f8CAAUA/f8CAAYA/f8CAAcA/f8CAAgA/f8CAAkA/f8CAAoA/f8CAAsA/f8CAAwA/f8CAA0A/f8DAPT//f8DAPX//f8DAPb//f8DAPf//f8DAPj//f8DAPn//f8DAPr//f8DAPv//f8DAPz//f8DAP3//f8DAP7//f8DAP///f8DAAAA/f8DAAEA/f8DAAIA/f8DAAMA/f8DAAQA/f8DAAUA/f8DAAYA/f8DAAcA/f8DAAgA/f8DAAkA/f8DAAoA/f8DAAsA/f8DAAwA/f8EAPT//f8EAPX//f8EAPb//f8EAPf//f8EAPj//f8EAPn//f8EAPr//f8EAPv//f8EAPz//f8EAP3//f8EAP7//f8EAP///f8EAAAA/f8EAAEA/f8EAAIA/f8EAAMA/f8EAAQA/f8EAAUA/f8EAAYA/f8EAAcA/f8EAAgA/f8EAAkA/f8EAAoA/f8EAAsA/f8EAAwA/f8FAPX//f8FAPb//f8FAPf//f8FAPj//f8FAPn//f8FAPr//f8FAPv//f8FAPz//f8FAP3//f8FAP7//f8FAP///f8FAAAA/f8FAAEA/f8FAAIA/f8FAAMA/f8FAAQA/f8FAAUA/f8FAAYA/f8FAAcA/f8FAAgA/f8FAAkA/f8FAAoA/f8FAAsA/f8GAPb//f8GAPf//f8GAPj//f8GAPn//f8GAPr//f8GAPv//f8GAPz//f8GAP3//f8GAP7//f8GAP///f8GAAAA/f8GAAEA/f8GAAIA/f8GAAMA/f8GAAQA/f8GAAUA/f8GAAYA/f8GAAcA/f8GAAgA/f8GAAkA/f8GAAoA/f8HAPj//f8HAPn//f8HAPr//f8HAPv//f8HAPz//f8HAP3//f8HAP7//f8HAP///f8HAAAA/f8HAAEA/f8HAAIA/f8HAAMA/f8HAAQA/f8HAAUA/f8HAAYA/f8HAAcA/f8HAAgA/f8IAPv//f8IAPz//f8IAP3//f8IAP7//f8IAP///f8IAAAA/f8IAAEA/f8IAAIA/f8IAAMA/f8IAAQA/f8IAAUA/v/6//z//v/6//3//v/6//7//v/6/////v/6/wAA/v/6/wEA/v/6/wIA/v/6/wMA/v/6/wQA/v/7//j//v/7//n//v/7//r//v/7//v//v/7//z//v/7//3//v/7//7//v/7/////v/7/wAA/v/7/wEA/v/7/wIA/v/7/wMA/v/7/wQA/v/7/wUA/v/7/wYA/v/7/wcA/v/7/wgA/v/8//b//v/8//f//v/8//j//v/8//n//v/8//r//v/8//v//v/8//z//v/8//3//v/8//7//v/8/////v/8/wAA/v/8/wEA/v/8/wIA/v/8/wMA/v/8/wQA/v/8/wUA/v/8/wYA/v/8/wcA/v/8/wgA/v/8/wkA/v/8/woA/v/9//X//v/9//b//v/9//f//v/9//j//v/9//n//v/9//r//v/9//v//v/9//z//v/9//3//v/9//7//v/9/////v/9/wAA/v/9/wEA/v/9/wIA/v/9/wMA/v/9/wQA/v/9/wUA/v/9/wYA/v/9/wcA/v/9/wgA/v/9/wkA/v/9/woA/v/9/wsA/v/+//T//v/+//X//v/+//b//v/+//f//v/+//j//v/+//n//v/+//r//v/+//v//v/+//z//v/+//3//v/+//7//v/+/////v/+/wAA/v/+/wEA/v/+/wIA/v/+/wMA/v/+/wQA/v/+/wUA/v/+/wYA/v/+/wcA/v/+/wgA/v/+/wkA/v/+/woA/v/+/wsA/v/+/wwA/v////P//v////T//v////X//v////b//v////f//v////j//v////n//v////r//v////v//v////z//v////3//v////7//v///////v///wAA/v///wEA/v///wIA/v///wMA/v///wQA/v///wUA/v///wYA/v///wcA/v///wgA/v///wkA/v///woA/v///wsA/v///wwA/v///w0A/v8AAPP//v8AAPT//v8AAPX//v8AAPb//v8AAPf//v8AAPj//v8AAPn//v8AAPr//v8AAPv//v8AAPz//v8AAP3//v8AAP7//v8AAP///v8AAAAA/v8AAAEA/v8AAAIA/v8AAAMA/v8AAAQA/v8AAAUA/v8AAAYA/v8AAAcA/v8AAAgA/v8AAAkA/v8AAAoA/v8AAAsA/v8AAAwA/v8AAA0A/v8BAPP//v8BAPT//v8BAPX//v8BAPb//v8BAPf//v8BAPj//v8BAPn//v8BAPr//v8BAPv//v8BAPz//v8BAP3//v8BAP7//v8BAP///v8BAAAA/v8BAAEA/v8BAAIA/v8BAAMA/v8BAAQA/v8BAAUA/v8BAAYA/v8BAAcA/v8BAAgA/v8BAAkA/v8BAAoA/v8BAAsA/v8BAAwA/v8BAA0A/v8CAPP//v8CAPT//v8CAPX//v8CAPb//v8CAPf//v8CAPj//v8CAPn//v8CAPr//v8CAPv//v8CAPz//v8CAP3//v8CAP7//v8CAP///v8CAAAA/v8CAAEA/v8CAAIA/v8CAAMA/v8CAAQA/v8CAAUA/v8CAAYA/v8CAAcA/v8CAAgA/v8CAAkA/v8CAAoA/v8CAAsA/v8CAAwA/v8CAA0A/v8DAPP//v8DAPT//v8DAPX//v8DAPb//v8DAPf//v8DAPj//v8DAPn//v8DAPr//v8DAPv//v8DAPz//v8DAP3//v8DAP7//v8DAP///v8DAAAA/v8DAAEA/v8DAAIA/v8DAAMA/v8DAAQA/v8DAAUA/v8DAAYA/v8DAAcA/v8DAAgA/v8DAAkA/v8DAAoA/v8DAAsA/v8DAAwA/v8DAA0A/v8EAPT//v8EAPX//v8EAPb//v8EAPf//v8EAPj//v8EAPn//v8EAPr//v8EAPv//v8EAPz//v8EAP3//v8EAP7//v8EAP///v8EAAAA/v8EAAEA/v8EAAIA/v8EAAMA/v8EAAQA/v8EAAUA/v8EAAYA/v8EAAcA/v8EAAgA/v8EAAkA/v8EAAoA/v8EAAsA/v8EAAwA/v8FAPX//v8FAPb//v8FAPf//v8FAPj//v8FAPn//v8FAPr//v8FAPv//v8FAPz//v8FAP3//v8FAP7//v8FAP///v8FAAAA/v8FAAEA/v8FAAIA/v8FAAMA/v8FAAQA/v8FAAUA/v8FAAYA/v8FAAcA/v8FAAgA/v8FAAkA/v8FAAoA/v8FAAsA/v8GAPb//v8GAPf//v8GAPj//v8GAPn//v8GAPr//v8GAPv//v8GAPz//v8GAP3//v8GAP7//v8GAP///v8GAAAA/v8GAAEA/v8GAAIA/v8GAAMA/v8GAAQA/v8GAAUA/v8GAAYA/v8GAAcA/v8GAAgA/v8GAAkA/v8GAAoA/v8HAPj//v8HAPn//v8HAPr//v8HAPv//v8HAPz//v8HAP3//v8HAP7//v8HAP///v8HAAAA/v8HAAEA/v8HAAIA/v8HAAMA/v8HAAQA/v8HAAUA/v8HAAYA/v8HAAcA/v8HAAgA/v8IAPz//v8IAP3//v8IAP7//v8IAP///v8IAAAA/v8IAAEA/v8IAAIA/v8IAAMA/v8IAAQA///5//7////5///////5/wAA///5/wEA///5/wIA///6//n////6//r////6//v////6//z////6//3////6//7////6///////6/wAA///6/wEA///6/wIA///6/wMA///6/wQA///6/wUA///6/wYA///6/wcA///7//f////7//j////7//n////7//r////7//v////7//z////7//3////7//7////7///////7/wAA///7/wEA///7/wIA///7/wMA///7/wQA///7/wUA///7/wYA///7/wcA///7/wgA///7/wkA///8//X////8//b////8//f////8//j////8//n////8//r////8//v////8//z////8//3////8//7////8///////8/wAA///8/wEA///8/wIA///8/wMA///8/wQA///8/wUA///8/wYA///8/wcA///8/wgA///8/wkA///8/woA///8/wsA///9//T////9//X////9//b////9//f////9//j////9//n////9//r////9//v////9//z////9//3////9//7////9///////9/wAA///9/wEA///9/wIA///9/wMA///9/wQA///9/wUA///9/wYA///9/wcA///9/wgA///9/wkA///9/woA///9/wsA///9/wwA///+//P////+//T////+//X////+//b////+//f////+//j////+//n////+//r////+//v////+//z////+//3////+//7////+///////+/wAA///+/wEA///+/wIA///+/wMA///+/wQA///+/wUA///+/wYA///+/wcA///+/wgA///+/wkA///+/woA///+/wsA///+/wwA///+/w0A//////P///////T///////X///////b///////f///////j///////n///////r///////v///////z///////3///////7//////////////wAA/////wEA/////wIA/////wMA/////wQA/////wUA/////wYA/////wcA/////wgA/////wkA/////woA/////wsA/////wwA/////w0A//8AAPP///8AAPT///8AAPX///8AAPb///8AAPf///8AAPj///8AAPn///8AAPr///8AAPv///8AAPz///8AAP3///8AAP7///8AAP////8AAAAA//8AAAEA//8AAAIA//8AAAMA//8AAAQA//8AAAUA//8AAAYA//8AAAcA//8AAAgA//8AAAkA//8AAAoA//8AAAsA//8AAAwA//8AAA0A//8BAPP///8BAPT///8BAPX///8BAPb///8BAPf///8BAPj///8BAPn///8BAPr///8BAPv///8BAPz///8BAP3///8BAP7///8BAP////8BAAAA//8BAAEA//8BAAIA//8BAAMA//8BAAQA//8BAAUA//8BAAYA//8BAAcA//8BAAgA//8BAAkA//8BAAoA//8BAAsA//8BAAwA//8BAA0A//8CAPP///8CAPT///8CAPX///8CAPb///8CAPf///8CAPj///8CAPn///8CAPr///8CAPv///8CAPz///8CAP3///8CAP7///8CAP////8CAAAA//8CAAEA//8CAAIA//8CAAMA//8CAAQA//8CAAUA//8CAAYA//8CAAcA//8CAAgA//8CAAkA//8CAAoA//8CAAsA//8CAAwA//8CAA0A//8DAPP///8DAPT///8DAPX///8DAPb///8DAPf///8DAPj///8DAPn///8DAPr///8DAPv///8DAPz///8DAP3///8DAP7///8DAP////8DAAAA//8DAAEA//8DAAIA//8DAAMA//8DAAQA//8DAAUA//8DAAYA//8DAAcA//8DAAgA//8DAAkA//8DAAoA//8DAAsA//8DAAwA//8DAA0A//8EAPT///8EAPX///8EAPb///8EAPf///8EAPj///8EAPn///8EAPr///8EAPv///8EAPz///8EAP3///8EAP7///8EAP////8EAAAA//8EAAEA//8EAAIA//8EAAMA//8EAAQA//8EAAUA//8EAAYA//8EAAcA//8EAAgA//8EAAkA//8EAAoA//8EAAsA//8EAAwA//8FAPX///8FAPb///8FAPf///8FAPj///8FAPn///8FAPr///8FAPv///8FAPz///8FAP3///8FAP7///8FAP////8FAAAA//8FAAEA//8FAAIA//8FAAMA//8FAAQA//8FAAUA//8FAAYA//8FAAcA//8FAAgA//8FAAkA//8FAAoA//8FAAsA//8GAPf///8GAPj///8GAPn///8GAPr///8GAPv///8GAPz///8GAP3///8GAP7///8GAP////8GAAAA//8GAAEA//8GAAIA//8GAAMA//8GAAQA//8GAAUA//8GAAYA//8GAAcA//8GAAgA//8GAAkA//8HAPn///8HAPr///8HAPv///8HAPz///8HAP3///8HAP7///8HAP////8HAAAA//8HAAEA//8HAAIA//8HAAMA//8HAAQA//8HAAUA//8HAAYA//8HAAcA//8IAP7///8IAP////8IAAAA//8IAAEA//8IAAIAAAD5//v/AAD5//z/AAD5//3/AAD5//7/AAD5////AAD5/wAAAAD5/wEAAAD5/wIAAAD5/wMAAAD5/wQAAAD5/wUAAAD6//j/AAD6//n/AAD6//r/AAD6//v/AAD6//z/AAD6//3/AAD6//7/AAD6////AAD6/wAAAAD6/wEAAAD6/wIAAAD6/wMAAAD6/wQAAAD6/wUAAAD6/wYAAAD6/wcAAAD6/wgAAAD7//b/AAD7//f/AAD7//j/AAD7//n/AAD7//r/AAD7//v/AAD7//z/AAD7//3/AAD7//7/AAD7////AAD7/wAAAAD7/wEAAAD7/wIAAAD7/wMAAAD7/wQAAAD7/wUAAAD7/wYAAAD7/wcAAAD7/wgAAAD7/wkAAAD7/woAAAD8//X/AAD8//b/AAD8//f/AAD8//j/AAD8//n/AAD8//r/AAD8//v/AAD8//z/AAD8//3/AAD8//7/AAD8////AAD8/wAAAAD8/wEAAAD8/wIAAAD8/wMAAAD8/wQAAAD8/wUAAAD8/wYAAAD8/wcAAAD8/wgAAAD8/wkAAAD8/woAAAD8/wsAAAD9//T/AAD9//X/AAD9//b/AAD9//f/AAD9//j/AAD9//n/AAD9//r/AAD9//v/AAD9//z/AAD9//3/AAD9//7/AAD9////AAD9/wAAAAD9/wEAAAD9/wIAAAD9/wMAAAD9/wQAAAD9/wUAAAD9/wYAAAD9/wcAAAD9/wgAAAD9/wkAAAD9/woAAAD9/wsAAAD9/wwAAAD+//P/AAD+//T/AAD+//X/AAD+//b/AAD+//f/AAD+//j/AAD+//n/AAD+//r/AAD+//v/AAD+//z/AAD+//3/AAD+//7/AAD+////AAD+/wAAAAD+/wEAAAD+/wIAAAD+/wMAAAD+/wQAAAD+/wUAAAD+/wYAAAD+/wcAAAD+/wgAAAD+/wkAAAD+/woAAAD+/wsAAAD+/wwAAAD+/w0AAAD///P/AAD///T/AAD///X/AAD///b/AAD///f/AAD///j/AAD///n/AAD///r/AAD///v/AAD///z/AAD///3/AAD///7/AAD/////AAD//wAAAAD//wEAAAD//wIAAAD//wMAAAD//wQAAAD//wUAAAD//wYAAAD//wcAAAD//wgAAAD//wkAAAD//woAAAD//wsAAAD//wwAAAD//w0AAAAAAPL/AAAAAPP/AAAAAPT/AAAAAPX/AAAAAPb/AAAAAPf/AAAAAPj/AAAAAPn/AAAAAPr/AAAAAPv/AAAAAPz/AAAAAP3/AAAAAP7/AAAAAP//AAAAAAEAAAAAAAIAAAAAAAMAAAAAAAQAAAAAAAUAAAAAAAYAAAAAAAcAAAAAAAgAAAAAAAkAAAAAAAoAAAAAAAsAAAAAAAwAAAAAAA0AAAAAAA4AAAABAPP/AAABAPT/AAABAPX/AAABAPb/AAABAPf/AAABAPj/AAABAPn/AAABAPr/AAABAPv/AAABAPz/AAABAP3/AAABAP7/AAABAP//AAABAAAAAAABAAEAAAABAAIAAAABAAMAAAABAAQAAAABAAUAAAABAAYAAAABAAcAAAABAAgAAAABAAkAAAABAAoAAAABAAsAAAABAAwAAAABAA0AAAACAPP/AAACAPT/AAACAPX/AAACAPb/AAACAPf/AAACAPj/AAACAPn/AAACAPr/AAACAPv/AAACAPz/AAACAP3/AAACAP7/AAACAP//AAACAAAAAAACAAEAAAACAAIAAAACAAMAAAACAAQAAAACAAUAAAACAAYAAAACAAcAAAACAAgAAAACAAkAAAACAAoAAAACAAsAAAACAAwAAAACAA0AAAADAPT/AAADAPX/AAADAPb/AAADAPf/AAADAPj/AAADAPn/AAADAPr/AAADAPv/AAADAPz/AAADAP3/AAADAP7/AAADAP//AAADAAAAAAADAAEAAAADAAIAAAADAAMAAAADAAQAAAADAAUAAAADAAYAAAADAAcAAAADAAgAAAADAAkAAAADAAoAAAADAAsAAAADAAwAAAAEAPX/AAAEAPb/AAAEAPf/AAAEAPj/AAAEAPn/AAAEAPr/AAAEAPv/AAAEAPz/AAAEAP3/AAAEAP7/AAAEAP//AAAEAAAAAAAEAAEAAAAEAAIAAAAEAAMAAAAEAAQAAAAEAAUAAAAEAAYAAAAEAAcAAAAEAAgAAAAEAAkAAAAEAAoAAAAEAAsAAAAFAPb/AAAFAPf/AAAFAPj/AAAFAPn/AAAFAPr/AAAFAPv/AAAFAPz/AAAFAP3/AAAFAP7/AAAFAP//AAAFAAAAAAAFAAEAAAAFAAIAAAAFAAMAAAAFAAQAAAAFAAUAAAAFAAYAAAAFAAcAAAAFAAgAAAAFAAkAAAAFAAoAAAAGAPj/AAAGAPn/AAAGAPr/AAAGAPv/AAAGAPz/AAAGAP3/AAAGAP7/AAAGAP//AAAGAAAAAAAGAAEAAAAGAAIAAAAGAAMAAAAGAAQAAAAGAAUAAAAGAAYAAAAGAAcAAAAGAAgAAAAHAPv/AAAHAPz/AAAHAP3/AAAHAP7/AAAHAP//AAAHAAAAAAAHAAEAAAAHAAIAAAAHAAMAAAAHAAQAAAAHAAUAAQD4//7/AQD4////AQD4/wAAAQD4/wEAAQD4/wIAAQD5//n/AQD5//r/AQD5//v/AQD5//z/AQD5//3/AQD5//7/AQD5////AQD5/wAAAQD5/wEAAQD5/wIAAQD5/wMAAQD5/wQAAQD5/wUAAQD5/wYAAQD5/wcAAQD6//f/AQD6//j/AQD6//n/AQD6//r/AQD6//v/AQD6//z/AQD6//3/AQD6//7/AQD6////AQD6/wAAAQD6/wEAAQD6/wIAAQD6/wMAAQD6/wQAAQD6/wUAAQD6/wYAAQD6/wcAAQD6/wgAAQD6/wkAAQD7//X/AQD7//b/AQD7//f/AQD7//j/AQD7//n/AQD7//r/AQD7//v/AQD7//z/AQD7//3/AQD7//7/AQD7////AQD7/wAAAQD7/wEAAQD7/wIAAQD7/wMAAQD7/wQAAQD7/wUAAQD7/wYAAQD7/wcAAQD7/wgAAQD7/wkAAQD7/woAAQD7/wsAAQD8//T/AQD8//X/AQD8//b/AQD8//f/AQD8//j/AQD8//n/AQD8//r/AQD8//v/AQD8//z/AQD8//3/AQD8//7/AQD8////AQD8/wAAAQD8/wEAAQD8/wIAAQD8/wMAAQD8/wQAAQD8/wUAAQD8/wYAAQD8/wcAAQD8/wgAAQD8/wkAAQD8/woAAQD8/wsAAQD8/wwAAQD9//P/AQD9//T/AQD9//X/AQD9//b/AQD9//f/AQD9//j/AQD9//n/AQD9//r/AQD9//v/AQD9//z/AQD9//3/AQD9//7/AQD9////AQD9/wAAAQD9/wEAAQD9/wIAAQD9/wMAAQD9/wQAAQD9/wUAAQD9/wYAAQD9/wcAAQD9/wgAAQD9/wkAAQD9/woAAQD9/wsAAQD9/wwAAQD9/w0AAQD+//P/AQD+//T/AQD+//X/AQD+//b/AQD+//f/AQD+//j/AQD+//n/AQD+//r/AQD+//v/AQD+//z/AQD+//3/AQD+//7/AQD+////AQD+/wAAAQD+/wEAAQD+/wIAAQD+/wMAAQD+/wQAAQD+/wUAAQD+/wYAAQD+/wcAAQD+/wgAAQD+/wkAAQD+/woAAQD+/wsAAQD+/wwAAQD+/w0AAQD///P/AQD///T/AQD///X/AQD///b/AQD///f/AQD///j/AQD///n/AQD///r/AQD///v/AQD///z/AQD///3/AQD///7/AQD/////AQD//wAAAQD//wEAAQD//wIAAQD//wMAAQD//wQAAQD//wUAAQD//wYAAQD//wcAAQD//wgAAQD//wkAAQD//woAAQD//wsAAQD//wwAAQD//w0AAQAAAPP/AQAAAPT/AQAAAPX/AQAAAPb/AQAAAPf/AQAAAPj/AQAAAPn/AQAAAPr/AQAAAPv/AQAAAPz/AQAAAP3/AQAAAP7/AQAAAP//AQAAAAAAAQAAAAEAAQAAAAIAAQAAAAMAAQAAAAQAAQAAAAUAAQAAAAYAAQAAAAcAAQAAAAgAAQAAAAkAAQAAAAoAAQAAAAsAAQAAAAwAAQAAAA0AAQABAPP/AQABAPT/AQABAPX/AQABAPb/AQABAPf/AQABAPj/AQABAPn/AQABAPr/AQABAPv/AQABAPz/AQABAP3/AQABAP7/AQABAP//AQABAAAAAQABAAEAAQABAAIAAQABAAMAAQABAAQAAQABAAUAAQABAAYAAQABAAcAAQABAAgAAQABAAkAAQABAAoAAQABAAsAAQABAAwAAQABAA0AAQACAPP/AQACAPT/AQACAPX/AQACAPb/AQACAPf/AQACAPj/AQACAPn/AQACAPr/AQACAPv/AQACAPz/AQACAP3/AQACAP7/AQACAP//AQACAAAAAQACAAEAAQACAAIAAQACAAMAAQACAAQAAQACAAUAAQACAAYAAQACAAcAAQACAAgAAQACAAkAAQACAAoAAQACAAsAAQACAAwAAQACAA0AAQADAPT/AQADAPX/AQADAPb/AQADAPf/AQADAPj/AQADAPn/AQADAPr/AQADAPv/AQADAPz/AQADAP3/AQADAP7/AQADAP//AQADAAAAAQADAAEAAQADAAIAAQADAAMAAQADAAQAAQADAAUAAQADAAYAAQADAAcAAQADAAgAAQADAAkAAQADAAoAAQADAAsAAQADAAwAAQAEAPX/AQAEAPb/AQAEAPf/AQAEAPj/AQAEAPn/AQAEAPr/AQAEAPv/AQAEAPz/AQAEAP3/AQAEAP7/AQAEAP//AQAEAAAAAQAEAAEAAQAEAAIAAQAEAAMAAQAEAAQAAQAEAAUAAQAEAAYAAQAEAAcAAQAEAAgAAQAEAAkAAQAEAAoAAQAEAAsAAQAFAPf/AQAFAPj/AQAFAPn/AQAFAPr/AQAFAPv/AQAFAPz/AQAFAP3/AQAFAP7/AQAFAP//AQAFAAAAAQAFAAEAAQAFAAIAAQAFAAMAAQAFAAQAAQAFAAUAAQAFAAYAAQAFAAcAAQAFAAgAAQAFAAkAAQAGAPn/AQAGAPr/AQAGAPv/AQAGAPz/AQAGAP3/AQAGAP7/AQAGAP//AQAGAAAAAQAGAAEAAQAGAAIAAQAGAAMAAQAGAAQAAQAGAAUAAQAGAAYAAQAGAAcAAQAHAP7/AQAHAP//AQAHAAAAAQAHAAEAAQAHAAIAAgD4//z/AgD4//3/AgD4//7/AgD4////AgD4/wAAAgD4/wEAAgD4/wIAAgD4/wMAAgD4/wQAAgD5//j/AgD5//n/AgD5//r/AgD5//v/AgD5//z/AgD5//3/AgD5//7/AgD5////AgD5/wAAAgD5/wEAAgD5/wIAAgD5/wMAAgD5/wQAAgD5/wUAAgD5/wYAAgD5/wcAAgD5/wgAAgD6//b/AgD6//f/AgD6//j/AgD6//n/AgD6//r/AgD6//v/AgD6//z/AgD6//3/AgD6//7/AgD6////AgD6/wAAAgD6/wEAAgD6/wIAAgD6/wMAAgD6/wQAAgD6/wUAAgD6/wYAAgD6/wcAAgD6/wgAAgD6/wkAAgD6/woAAgD7//X/AgD7//b/AgD7//f/AgD7//j/AgD7//n/AgD7//r/AgD7//v/AgD7//z/AgD7//3/AgD7//7/AgD7////AgD7/wAAAgD7/wEAAgD7/wIAAgD7/wMAAgD7/wQAAgD7/wUAAgD7/wYAAgD7/wcAAgD7/wgAAgD7/wkAAgD7/woAAgD7/wsAAgD8//T/AgD8//X/AgD8//b/AgD8//f/AgD8//j/AgD8//n/AgD8//r/AgD8//v/AgD8//z/AgD8//3/AgD8//7/AgD8////AgD8/wAAAgD8/wEAAgD8/wIAAgD8/wMAAgD8/wQAAgD8/wUAAgD8/wYAAgD8/wcAAgD8/wgAAgD8/wkAAgD8/woAAgD8/wsAAgD8/wwAAgD9//P/AgD9//T/AgD9//X/AgD9//b/AgD9//f/AgD9//j/AgD9//n/AgD9//r/AgD9//v/AgD9//z/AgD9//3/AgD9//7/AgD9////AgD9/wAAAgD9/wEAAgD9/wIAAgD9/wMAAgD9/wQAAgD9/wUAAgD9/wYAAgD9/wcAAgD9/wgAAgD9/wkAAgD9/woAAgD9/wsAAgD9/wwAAgD9/w0AAgD+//P/AgD+//T/AgD+//X/AgD+//b/AgD+//f/AgD+//j/AgD+//n/AgD+//r/AgD+//v/AgD+//z/AgD+//3/AgD+//7/AgD+////AgD+/wAAAgD+/wEAAgD+/wIAAgD+/wMAAgD+/wQAAgD+/wUAAgD+/wYAAgD+/wcAAgD+/wgAAgD+/wkAAgD+/woAAgD+/wsAAgD+/wwAAgD+/w0AAgD///P/AgD///T/AgD///X/AgD///b/AgD///f/AgD///j/AgD///n/AgD///r/AgD///v/AgD///z/AgD///3/AgD///7/AgD/////AgD//wAAAgD//wEAAgD//wIAAgD//wMAAgD//wQAAgD//wUAAgD//wYAAgD//wcAAgD//wgAAgD//wkAAgD//woAAgD//wsAAgD//wwAAgD//w0AAgAAAPP/AgAAAPT/AgAAAPX/AgAAAPb/AgAAAPf/AgAAAPj/AgAAAPn/AgAAAPr/AgAAAPv/AgAAAPz/AgAAAP3/AgAAAP7/AgAAAP//AgAAAAAAAgAAAAEAAgAAAAIAAgAAAAMAAgAAAAQAAgAAAAUAAgAAAAYAAgAAAAcAAgAAAAgAAgAAAAkAAgAAAAoAAgAAAAsAAgAAAAwAAgAAAA0AAgABAPP/AgABAPT/AgABAPX/AgABAPb/AgABAPf/AgABAPj/AgABAPn/AgABAPr/AgABAPv/AgABAPz/AgABAP3/AgABAP7/AgABAP//AgABAAAAAgABAAEAAgABAAIAAgABAAMAAgABAAQAAgABAAUAAgABAAYAAgABAAcAAgABAAgAAgABAAkAAgABAAoAAgABAAsAAgABAAwAAgABAA0AAgACAPT/AgACAPX/AgACAPb/AgACAPf/AgACAPj/AgACAPn/AgACAPr/AgACAPv/AgACAPz/AgACAP3/AgACAP7/AgACAP//AgACAAAAAgACAAEAAgACAAIAAgACAAMAAgACAAQAAgACAAUAAgACAAYAAgACAAcAAgACAAgAAgACAAkAAgACAAoAAgACAAsAAgACAAwAAgADAPX/AgADAPb/AgADAPf/AgADAPj/AgADAPn/AgADAPr/AgADAPv/AgADAPz/AgADAP3/AgADAP7/AgADAP//AgADAAAAAgADAAEAAgADAAIAAgADAAMAAgADAAQAAgADAAUAAgADAAYAAgADAAcAAgADAAgAAgADAAkAAgADAAoAAgADAAsAAgAEAPb/AgAEAPf/AgAEAPj/AgAEAPn/AgAEAPr/AgAEAPv/AgAEAPz/AgAEAP3/AgAEAP7/AgAEAP//AgAEAAAAAgAEAAEAAgAEAAIAAgAEAAMAAgAEAAQAAgAEAAUAAgAEAAYAAgAEAAcAAgAEAAgAAgAEAAkAAgAEAAoAAgAFAPj/AgAFAPn/AgAFAPr/AgAFAPv/AgAFAPz/AgAFAP3/AgAFAP7/AgAFAP//AgAFAAAAAgAFAAEAAgAFAAIAAgAFAAMAAgAFAAQAAgAFAAUAAgAFAAYAAgAFAAcAAgAFAAgAAgAGAPz/AgAGAP3/AgAGAP7/AgAGAP//AgAGAAAAAgAGAAEAAgAGAAIAAgAGAAMAAgAGAAQAAwD4//v/AwD4//z/AwD4//3/AwD4//7/AwD4////AwD4/wAAAwD4/wEAAwD4/wIAAwD4/wMAAwD4/wQAAwD4/wUAAwD5//j/AwD5//n/AwD5//r/AwD5//v/AwD5//z/AwD5//3/AwD5//7/AwD5////AwD5/wAAAwD5/wEAAwD5/wIAAwD5/wMAAwD5/wQAAwD5/wUAAwD5/wYAAwD5/wcAAwD5/wgAAwD6//b/AwD6//f/AwD6//j/AwD6//n/AwD6//r/AwD6//v/AwD6//z/AwD6//3/AwD6//7/AwD6////AwD6/wAAAwD6/wEAAwD6/wIAAwD6/wMAAwD6/wQAAwD6/wUAAwD6/wYAAwD6/wcAAwD6/wgAAwD6/wkAAwD6/woAAwD7//X/AwD7//b/AwD7//f/AwD7//j/AwD7//n/AwD7//r/AwD7//v/AwD7//z/AwD7//3/AwD7//7/AwD7////AwD7/wAAAwD7/wEAAwD7/wIAAwD7/wMAAwD7/wQAAwD7/wUAAwD7/wYAAwD7/wcAAwD7/wgAAwD7/wkAAwD7/woAAwD7/wsAAwD8//T/AwD8//X/AwD8//b/AwD8//f/AwD8//j/AwD8//n/AwD8//r/AwD8//v/AwD8//z/AwD8//3/AwD8//7/AwD8////AwD8/wAAAwD8/wEAAwD8/wIAAwD8/wMAAwD8/wQAAwD8/wUAAwD8/wYAAwD8/wcAAwD8/wgAAwD8/wkAAwD8/woAAwD8/wsAAwD8/wwAAwD9//T/AwD9//X/AwD9//b/AwD9//f/AwD9//j/AwD9//n/AwD9//r/AwD9//v/AwD9//z/AwD9//3/AwD9//7/AwD9////AwD9/wAAAwD9/wEAAwD9/wIAAwD9/wMAAwD9/wQAAwD9/wUAAwD9/wYAAwD9/wcAAwD9/wgAAwD9/wkAAwD9/woAAwD9/wsAAwD9/wwAAwD+//P/AwD+//T/AwD+//X/AwD+//b/AwD+//f/AwD+//j/AwD+//n/AwD+//r/AwD+//v/AwD+//z/AwD+//3/AwD+//7/AwD+////AwD+/wAAAwD+/wEAAwD+/wIAAwD+/wMAAwD+/wQAAwD+/wUAAwD+/wYAAwD+/wcAAwD+/wgAAwD+/wkAAwD+/woAAwD+/wsAAwD+/wwAAwD+/w0AAwD///P/AwD///T/AwD///X/AwD///b/AwD///f/AwD///j/AwD///n/AwD///r/AwD///v/AwD///z/AwD///3/AwD///7/AwD/////AwD//wAAAwD//wEAAwD//wIAAwD//wMAAwD//wQAAwD//wUAAwD//wYAAwD//wcAAwD//wgAAwD//wkAAwD//woAAwD//wsAAwD//wwAAwD//w0AAwAAAPT/AwAAAPX/AwAAAPb/AwAAAPf/AwAAAPj/AwAAAPn/AwAAAPr/AwAAAPv/AwAAAPz/AwAAAP3/AwAAAP7/AwAAAP//AwAAAAAAAwAAAAEAAwAAAAIAAwAAAAMAAwAAAAQAAwAAAAUAAwAAAAYAAwAAAAcAAwAAAAgAAwAAAAkAAwAAAAoAAwAAAAsAAwAAAAwAAwABAPT/AwABAPX/AwABAPb/AwABAPf/AwABAPj/AwABAPn/AwABAPr/AwABAPv/AwABAPz/AwABAP3/AwABAP7/AwABAP//AwABAAAAAwABAAEAAwABAAIAAwABAAMAAwABAAQAAwABAAUAAwABAAYAAwABAAcAAwABAAgAAwABAAkAAwABAAoAAwABAAsAAwABAAwAAwACAPX/AwACAPb/AwACAPf/AwACAPj/AwACAPn/AwACAPr/AwACAPv/AwACAPz/AwACAP3/AwACAP7/AwACAP//AwACAAAAAwACAAEAAwACAAIAAwACAAMAAwACAAQAAwACAAUAAwACAAYAAwACAAcAAwACAAgAAwACAAkAAwACAAoAAwACAAsAAwADAPb/AwADAPf/AwADAPj/AwADAPn/AwADAPr/AwADAPv/AwADAPz/AwADAP3/AwADAP7/AwADAP//AwADAAAAAwADAAEAAwADAAIAAwADAAMAAwADAAQAAwADAAUAAwADAAYAAwADAAcAAwADAAgAAwADAAkAAwADAAoAAwAEAPj/AwAEAPn/AwAEAPr/AwAEAPv/AwAEAPz/AwAEAP3/AwAEAP7/AwAEAP//AwAEAAAAAwAEAAEAAwAEAAIAAwAEAAMAAwAEAAQAAwAEAAUAAwAEAAYAAwAEAAcAAwAEAAgAAwAFAPv/AwAFAPz/AwAFAP3/AwAFAP7/AwAFAP//AwAFAAAAAwAFAAEAAwAFAAIAAwAFAAMAAwAFAAQAAwAFAAUABAD4//r/BAD4//v/BAD4//z/BAD4//3/BAD4//7/BAD4////BAD4/wAABAD4/wEABAD4/wIABAD4/wMABAD4/wQABAD4/wUABAD4/wYABAD5//j/BAD5//n/BAD5//r/BAD5//v/BAD5//z/BAD5//3/BAD5//7/BAD5////BAD5/wAABAD5/wEABAD5/wIABAD5/wMABAD5/wQABAD5/wUABAD5/wYABAD5/wcABAD5/wgABAD6//b/BAD6//f/BAD6//j/BAD6//n/BAD6//r/BAD6//v/BAD6//z/BAD6//3/BAD6//7/BAD6////BAD6/wAABAD6/wEABAD6/wIABAD6/wMABAD6/wQABAD6/wUABAD6/wYABAD6/wcABAD6/wgABAD6/wkABAD6/woABAD7//X/BAD7//b/BAD7//f/BAD7//j/BAD7//n/BAD7//r/BAD7//v/BAD7//z/BAD7//3/BAD7//7/BAD7////BAD7/wAABAD7/wEABAD7/wIABAD7/wMABAD7/wQABAD7/wUABAD7/wYABAD7/wcABAD7/wgABAD7/wkABAD7/woABAD7/wsABAD8//X/BAD8//b/BAD8//f/BAD8//j/BAD8//n/BAD8//r/BAD8//v/BAD8//z/BAD8//3/BAD8//7/BAD8////BAD8/wAABAD8/wEABAD8/wIABAD8/wMABAD8/wQABAD8/wUABAD8/wYABAD8/wcABAD8/wgABAD8/wkABAD8/woABAD8/wsABAD9//T/BAD9//X/BAD9//b/BAD9//f/BAD9//j/BAD9//n/BAD9//r/BAD9//v/BAD9//z/BAD9//3/BAD9//7/BAD9////BAD9/wAABAD9/wEABAD9/wIABAD9/wMABAD9/wQABAD9/wUABAD9/wYABAD9/wcABAD9/wgABAD9/wkABAD9/woABAD9/wsABAD9/wwABAD+//T/BAD+//X/BAD+//b/BAD+//f/BAD+//j/BAD+//n/BAD+//r/BAD+//v/BAD+//z/BAD+//3/BAD+//7/BAD+////BAD+/wAABAD+/wEABAD+/wIABAD+/wMABAD+/wQABAD+/wUABAD+/wYABAD+/wcABAD+/wgABAD+/wkABAD+/woABAD+/wsABAD+/wwABAD///T/BAD///X/BAD///b/BAD///f/BAD///j/BAD///n/BAD///r/BAD///v/BAD///z/BAD///3/BAD///7/BAD/////BAD//wAABAD//wEABAD//wIABAD//wMABAD//wQABAD//wUABAD//wYABAD//wcABAD//wgABAD//wkABAD//woABAD//wsABAD//wwABAAAAPX/BAAAAPb/BAAAAPf/BAAAAPj/BAAAAPn/BAAAAPr/BAAAAPv/BAAAAPz/BAAAAP3/BAAAAP7/BAAAAP//BAAAAAAABAAAAAEABAAAAAIABAAAAAMABAAAAAQABAAAAAUABAAAAAYABAAAAAcABAAAAAgABAAAAAkABAAAAAoABAAAAAsABAABAPX/BAABAPb/BAABAPf/BAABAPj/BAABAPn/BAABAPr/BAABAPv/BAABAPz/BAABAP3/BAABAP7/BAABAP//BAABAAAABAABAAEABAABAAIABAABAAMABAABAAQABAABAAUABAABAAYABAABAAcABAABAAgABAABAAkABAABAAoABAABAAsABAACAPb/BAACAPf/BAACAPj/BAACAPn/BAACAPr/BAACAPv/BAACAPz/BAACAP3/BAACAP7/BAACAP//BAACAAAABAACAAEABAACAAIABAACAAMABAACAAQABAACAAUABAACAAYABAACAAcABAACAAgABAACAAkABAACAAoABAADAPj/BAADAPn/BAADAPr/BAADAPv/BAADAPz/BAADAP3/BAADAP7/BAADAP//BAADAAAABAADAAEABAADAAIABAADAAMABAADAAQABAADAAUABAADAAYABAADAAcABAADAAgABAAEAPr/BAAEAPv/BAAEAPz/BAAEAP3/BAAEAP7/BAAEAP//BAAEAAAABAAEAAEABAAEAAIABAAEAAMABAAEAAQABAAEAAUABAAEAAYABQD4//v/BQD4//z/BQD4//3/BQD4//7/BQD4////BQD4/wAABQD4/wEABQD4/wIABQD4/wMABQD4/wQABQD4/wUABQD5//j/BQD5//n/BQD5//r/BQD5//v/BQD5//z/BQD5//3/BQD5//7/BQD5////BQD5/wAABQD5/wEABQD5/wIABQD5/wMABQD5/wQABQD5/wUABQD5/wYABQD5/wcABQD5/wgABQD6//f/BQD6//j/BQD6//n/BQD6//r/BQD6//v/BQD6//z/BQD6//3/BQD6//7/BQD6////BQD6/wAABQD6/wEABQD6/wIABQD6/wMABQD6/wQABQD6/wUABQD6/wYABQD6/wcABQD6/wgABQD6/wkABQD7//b/BQD7//f/BQD7//j/BQD7//n/BQD7//r/BQD7//v/BQD7//z/BQD7//3/BQD7//7/BQD7////BQD7/wAABQD7/wEABQD7/wIABQD7/wMABQD7/wQABQD7/wUABQD7/wYABQD7/wcABQD7/wgABQD7/wkABQD7/woABQD8//X/BQD8//b/BQD8//f/BQD8//j/BQD8//n/BQD8//r/BQD8//v/BQD8//z/BQD8//3/BQD8//7/BQD8////BQD8/wAABQD8/wEABQD8/wIABQD8/wMABQD8/wQABQD8/wUABQD8/wYABQD8/wcABQD8/wgABQD8/wkABQD8/woABQD8/wsABQD9//X/BQD9//b/BQD9//f/BQD9//j/BQD9//n/BQD9//r/BQD9//v/BQD9//z/BQD9//3/BQD9//7/BQD9////BQD9/wAABQD9/wEABQD9/wIABQD9/wMABQD9/wQABQD9/wUABQD9/wYABQD9/wcABQD9/wgABQD9/wkABQD9/woABQD9/wsABQD+//X/BQD+//b/BQD+//f/BQD+//j/BQD+//n/BQD+//r/BQD+//v/BQD+//z/BQD+//3/BQD+//7/BQD+////BQD+/wAABQD+/wEABQD+/wIABQD+/wMABQD+/wQABQD+/wUABQD+/wYABQD+/wcABQD+/wgABQD+/wkABQD+/woABQD+/wsABQD///X/BQD///b/BQD///f/BQD///j/BQD///n/BQD///r/BQD///v/BQD///z/BQD///3/BQD///7/BQD/////BQD//wAABQD//wEABQD//wIABQD//wMABQD//wQABQD//wUABQD//wYABQD//wcABQD//wgABQD//wkABQD//woABQD//wsABQAAAPb/BQAAAPf/BQAAAPj/BQAAAPn/BQAAAPr/BQAAAPv/BQAAAPz/BQAAAP3/BQAAAP7/BQAAAP//BQAAAAAABQAAAAEABQAAAAIABQAAAAMABQAAAAQABQAAAAUABQAAAAYABQAAAAcABQAAAAgABQAAAAkABQAAAAoABQABAPf/BQABAPj/BQABAPn/BQABAPr/BQABAPv/BQABAPz/BQABAP3/BQABAP7/BQABAP//BQABAAAABQABAAEABQABAAIABQABAAMABQABAAQABQABAAUABQABAAYABQABAAcABQABAAgABQABAAkABQACAPj/BQACAPn/BQACAPr/BQACAPv/BQACAPz/BQACAP3/BQACAP7/BQACAP//BQACAAAABQACAAEABQACAAIABQACAAMABQACAAQABQACAAUABQACAAYABQACAAcABQACAAgABQADAPv/BQADAPz/BQADAP3/BQADAP7/BQADAP//BQADAAAABQADAAEABQADAAIABQADAAMABQADAAQABQADAAUABgD4//z/BgD4//3/BgD4//7/BgD4////BgD4/wAABgD4/wEABgD4/wIABgD4/wMABgD4/wQABgD5//n/BgD5//r/BgD5//v/BgD5//z/BgD5//3/BgD5//7/BgD5////BgD5/wAABgD5/wEABgD5/wIABgD5/wMABgD5/wQABgD5/wUABgD5/wYABgD5/wcABgD6//j/BgD6//n/BgD6//r/BgD6//v/BgD6//z/BgD6//3/BgD6//7/BgD6////BgD6/wAABgD6/wEABgD6/wIABgD6/wMABgD6/wQABgD6/wUABgD6/wYABgD6/wcABgD6/wgABgD7//f/BgD7//j/BgD7//n/BgD7//r/BgD7//v/BgD7//z/BgD7//3/BgD7//7/BgD7////BgD7/wAABgD7/wEABgD7/wIABgD7/wMABgD7/wQABgD7/wUABgD7/wYABgD7/wcABgD7/wgABgD7/wkABgD8//b/BgD8//f/BgD8//j/BgD8//n/BgD8//r/BgD8//v/BgD8//z/BgD8//3/BgD8//7/BgD8////BgD8/wAABgD8/wEABgD8/wIABgD8/wMABgD8/wQABgD8/wUABgD8/wYABgD8/wcABgD8/wgABgD8/wkABgD8/woABgD9//b/BgD9//f/BgD9//j/BgD9//n/BgD9//r/BgD9//v/BgD9//z/BgD9//3/BgD9//7/BgD9////BgD9/wAABgD9/wEABgD9/wIABgD9/wMABgD9/wQABgD9/wUABgD9/wYABgD9/wcABgD9/wgABgD9/wkABgD9/woABgD+//b/BgD+//f/BgD+//j/BgD+//n/BgD+//r/BgD+//v/BgD+//z/BgD+//3/BgD+//7/BgD+////BgD+/wAABgD+/wEABgD+/wIABgD+/wMABgD+/wQABgD+/wUABgD+/wYABgD+/wcABgD+/wgABgD+/wkABgD+/woABgD///f/BgD///j/BgD///n/BgD///r/BgD///v/BgD///z/BgD///3/BgD///7/BgD/////BgD//wAABgD//wEABgD//wIABgD//wMABgD//wQABgD//wUABgD//wYABgD//wcABgD//wgABgD//wkABgAAAPj/BgAAAPn/BgAAAPr/BgAAAPv/BgAAAPz/BgAAAP3/BgAAAP7/BgAAAP//BgAAAAAABgAAAAEABgAAAAIABgAAAAMABgAAAAQABgAAAAUABgAAAAYABgAAAAcABgAAAAgABgABAPn/BgABAPr/BgABAPv/BgABAPz/BgABAP3/BgABAP7/BgABAP//BgABAAAABgABAAEABgABAAIABgABAAMABgABAAQABgABAAUABgABAAYABgABAAcABgACAPz/BgACAP3/BgACAP7/BgACAP//BgACAAAABgACAAEABgACAAIABgACAAMABgACAAQABwD4//7/BwD4////BwD4/wAABwD4/wEABwD4/wIABwD5//v/BwD5//z/BwD5//3/BwD5//7/BwD5////BwD5/wAABwD5/wEABwD5/wIABwD5/wMABwD5/wQABwD5/wUABwD6//n/BwD6//r/BwD6//v/BwD6//z/BwD6//3/BwD6//7/BwD6////BwD6/wAABwD6/wEABwD6/wIABwD6/wMABwD6/wQABwD6/wUABwD6/wYABwD6/wcABwD7//j/BwD7//n/BwD7//r/BwD7//v/BwD7//z/BwD7//3/BwD7//7/BwD7////BwD7/wAABwD7/wEABwD7/wIABwD7/wMABwD7/wQABwD7/wUABwD7/wYABwD7/wcABwD7/wgABwD8//j/BwD8//n/BwD8//r/BwD8//v/BwD8//z/BwD8//3/BwD8//7/BwD8////BwD8/wAABwD8/wEABwD8/wIABwD8/wMABwD8/wQABwD8/wUABwD8/wYABwD8/wcABwD8/wgABwD9//j/BwD9//n/BwD9//r/BwD9//v/BwD9//z/BwD9//3/BwD9//7/BwD9////BwD9/wAABwD9/wEABwD9/wIABwD9/wMABwD9/wQABwD9/wUABwD9/wYABwD9/wcABwD9/wgABwD+//j/BwD+//n/BwD+//r/BwD+//v/BwD+//z/BwD+//3/BwD+//7/BwD+////BwD+/wAABwD+/wEABwD+/wIABwD+/wMABwD+/wQABwD+/wUABwD+/wYABwD+/wcABwD+/wgABwD///n/BwD///r/BwD///v/BwD///z/BwD///3/BwD///7/BwD/////BwD//wAABwD//wEABwD//wIABwD//wMABwD//wQABwD//wUABwD//wYABwD//wcABwAAAPv/BwAAAPz/BwAAAP3/BwAAAP7/BwAAAP//BwAAAAAABwAAAAEABwAAAAIABwAAAAMABwAAAAQABwAAAAUABwABAP7/BwABAP//BwABAAAABwABAAEABwABAAIACAD5//7/CAD5////CAD5/wAACAD5/wEACAD5/wIACAD6//z/CAD6//3/CAD6//7/CAD6////CAD6/wAACAD6/wEACAD6/wIACAD6/wMACAD6/wQACAD7//v/CAD7//z/CAD7//3/CAD7//7/CAD7////CAD7/wAACAD7/wEACAD7/wIACAD7/wMACAD7/wQACAD7/wUACAD8//r/CAD8//v/CAD8//z/CAD8//3/CAD8//7/CAD8////CAD8/wAACAD8/wEACAD8/wIACAD8/wMACAD8/wQACAD8/wUACAD8/wYACAD9//v/CAD9//z/CAD9//3/CAD9//7/CAD9////CAD9/wAACAD9/wEACAD9/wIACAD9/wMACAD9/wQACAD9/wUACAD+//z/CAD+//3/CAD+//7/CAD+////CAD+/wAACAD+/wEACAD+/wIACAD+/wMACAD+/wQACAD///7/CAD/////CAD//wAACAD//wEACAD//wIA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.006001374253468719,\"absorptive\":true,\"n_reflections\":3994,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":true}", - "NaCl (rocksalt)": "{\"name\":\"NaCl\",\"spacegroup\":\"Fm-3m (225)\",\"pointgroup\":\"m-3m\",\"cell\":[[5.64,0.0,0.0],[0.0,5.64,0.0],[0.0,0.0,5.64]],\"recip\":[[0.1773049645390071,0.0,0.0],[0.0,0.1773049645390071,0.0],[0.0,0.0,0.1773049645390071]],\"positions_frac\":[[0.0,0.0,0.0],[0.5,0.0,0.0],[0.0,0.5,0.5],[0.5,0.5,0.5],[0.5,0.0,0.5],[0.0,0.0,0.5],[0.5,0.5,0.0],[0.0,0.5,0.0]],\"numbers\":[11,17,11,17,11,17,11,17],\"symbols\":[\"Na\",\"Cl\",\"Na\",\"Cl\",\"Na\",\"Cl\",\"Na\",\"Cl\"],\"colors\":[[0.671,0.361,0.949],[0.122,0.941,0.122],[0.671,0.361,0.949],[0.122,0.941,0.122],[0.671,0.361,0.949],[0.122,0.941,0.122],[0.671,0.361,0.949],[0.122,0.941,0.122]],\"radii\":[1.66,1.02,1.66,1.02,1.66,1.02,1.66,1.02],\"hkl_i16\":\"8P/8//7/8P/8/wAA8P/8/wIA8P/+//z/8P/+//7/8P/+/wAA8P/+/wIA8P/+/wQA8P8AAPz/8P8AAP7/8P8AAAAA8P8AAAIA8P8AAAQA8P8CAPz/8P8CAP7/8P8CAAAA8P8CAAIA8P8CAAQA8P8EAP7/8P8EAAAA8P8EAAIA8f/5//3/8f/5////8f/5/wEA8f/5/wMA8f/7//v/8f/7//3/8f/7////8f/7/wEA8f/7/wMA8f/7/wUA8f/9//n/8f/9//v/8f/9//3/8f/9////8f/9/wEA8f/9/wMA8f/9/wUA8f/9/wcA8f////n/8f////v/8f////3/8f//////8f///wEA8f///wMA8f///wUA8f///wcA8f8BAPn/8f8BAPv/8f8BAP3/8f8BAP//8f8BAAEA8f8BAAMA8f8BAAUA8f8BAAcA8f8DAPn/8f8DAPv/8f8DAP3/8f8DAP//8f8DAAEA8f8DAAMA8f8DAAUA8f8DAAcA8f8FAPv/8f8FAP3/8f8FAP//8f8FAAEA8f8FAAMA8f8FAAUA8f8HAP3/8f8HAP//8f8HAAEA8f8HAAMA8v/4//z/8v/4//7/8v/4/wAA8v/4/wIA8v/4/wQA8v/6//r/8v/6//z/8v/6//7/8v/6/wAA8v/6/wIA8v/6/wQA8v/6/wYA8v/8//j/8v/8//r/8v/8//z/8v/8//7/8v/8/wAA8v/8/wIA8v/8/wQA8v/8/wYA8v/8/wgA8v/+//j/8v/+//r/8v/+//z/8v/+//7/8v/+/wAA8v/+/wIA8v/+/wQA8v/+/wYA8v/+/wgA8v8AAPj/8v8AAPr/8v8AAPz/8v8AAP7/8v8AAAAA8v8AAAIA8v8AAAQA8v8AAAYA8v8AAAgA8v8CAPj/8v8CAPr/8v8CAPz/8v8CAP7/8v8CAAAA8v8CAAIA8v8CAAQA8v8CAAYA8v8CAAgA8v8EAPj/8v8EAPr/8v8EAPz/8v8EAP7/8v8EAAAA8v8EAAIA8v8EAAQA8v8EAAYA8v8EAAgA8v8GAPr/8v8GAPz/8v8GAP7/8v8GAAAA8v8GAAIA8v8GAAQA8v8GAAYA8v8IAPz/8v8IAP7/8v8IAAAA8v8IAAIA8v8IAAQA8//3//v/8//3//3/8//3////8//3/wEA8//3/wMA8//3/wUA8//5//n/8//5//v/8//5//3/8//5////8//5/wEA8//5/wMA8//5/wUA8//5/wcA8//7//f/8//7//n/8//7//v/8//7//3/8//7////8//7/wEA8//7/wMA8//7/wUA8//7/wcA8//7/wkA8//9//f/8//9//n/8//9//v/8//9//3/8//9////8//9/wEA8//9/wMA8//9/wUA8//9/wcA8//9/wkA8/////f/8/////n/8/////v/8/////3/8///////8////wEA8////wMA8////wUA8////wcA8////wkA8/8BAPf/8/8BAPn/8/8BAPv/8/8BAP3/8/8BAP//8/8BAAEA8/8BAAMA8/8BAAUA8/8BAAcA8/8BAAkA8/8DAPf/8/8DAPn/8/8DAPv/8/8DAP3/8/8DAP//8/8DAAEA8/8DAAMA8/8DAAUA8/8DAAcA8/8DAAkA8/8FAPf/8/8FAPn/8/8FAPv/8/8FAP3/8/8FAP//8/8FAAEA8/8FAAMA8/8FAAUA8/8FAAcA8/8FAAkA8/8HAPn/8/8HAPv/8/8HAP3/8/8HAP//8/8HAAEA8/8HAAMA8/8HAAUA8/8HAAcA8/8JAPv/8/8JAP3/8/8JAP//8/8JAAEA8/8JAAMA8/8JAAUA9P/2//r/9P/2//z/9P/2//7/9P/2/wAA9P/2/wIA9P/2/wQA9P/2/wYA9P/4//j/9P/4//r/9P/4//z/9P/4//7/9P/4/wAA9P/4/wIA9P/4/wQA9P/4/wYA9P/4/wgA9P/6//b/9P/6//j/9P/6//r/9P/6//z/9P/6//7/9P/6/wAA9P/6/wIA9P/6/wQA9P/6/wYA9P/6/wgA9P/6/woA9P/8//b/9P/8//j/9P/8//r/9P/8//z/9P/8//7/9P/8/wAA9P/8/wIA9P/8/wQA9P/8/wYA9P/8/wgA9P/8/woA9P/+//b/9P/+//j/9P/+//r/9P/+//z/9P/+//7/9P/+/wAA9P/+/wIA9P/+/wQA9P/+/wYA9P/+/wgA9P/+/woA9P8AAPb/9P8AAPj/9P8AAPr/9P8AAPz/9P8AAP7/9P8AAAAA9P8AAAIA9P8AAAQA9P8AAAYA9P8AAAgA9P8AAAoA9P8CAPb/9P8CAPj/9P8CAPr/9P8CAPz/9P8CAP7/9P8CAAAA9P8CAAIA9P8CAAQA9P8CAAYA9P8CAAgA9P8CAAoA9P8EAPb/9P8EAPj/9P8EAPr/9P8EAPz/9P8EAP7/9P8EAAAA9P8EAAIA9P8EAAQA9P8EAAYA9P8EAAgA9P8EAAoA9P8GAPb/9P8GAPj/9P8GAPr/9P8GAPz/9P8GAP7/9P8GAAAA9P8GAAIA9P8GAAQA9P8GAAYA9P8GAAgA9P8GAAoA9P8IAPj/9P8IAPr/9P8IAPz/9P8IAP7/9P8IAAAA9P8IAAIA9P8IAAQA9P8IAAYA9P8IAAgA9P8KAPr/9P8KAPz/9P8KAP7/9P8KAAAA9P8KAAIA9P8KAAQA9P8KAAYA9f/1//v/9f/1//3/9f/1////9f/1/wEA9f/1/wMA9f/1/wUA9f/3//f/9f/3//n/9f/3//v/9f/3//3/9f/3////9f/3/wEA9f/3/wMA9f/3/wUA9f/3/wcA9f/3/wkA9f/5//f/9f/5//n/9f/5//v/9f/5//3/9f/5////9f/5/wEA9f/5/wMA9f/5/wUA9f/5/wcA9f/5/wkA9f/7//X/9f/7//f/9f/7//n/9f/7//v/9f/7//3/9f/7////9f/7/wEA9f/7/wMA9f/7/wUA9f/7/wcA9f/7/wkA9f/7/wsA9f/9//X/9f/9//f/9f/9//n/9f/9//v/9f/9//3/9f/9////9f/9/wEA9f/9/wMA9f/9/wUA9f/9/wcA9f/9/wkA9f/9/wsA9f////X/9f////f/9f////n/9f////v/9f////3/9f//////9f///wEA9f///wMA9f///wUA9f///wcA9f///wkA9f///wsA9f8BAPX/9f8BAPf/9f8BAPn/9f8BAPv/9f8BAP3/9f8BAP//9f8BAAEA9f8BAAMA9f8BAAUA9f8BAAcA9f8BAAkA9f8BAAsA9f8DAPX/9f8DAPf/9f8DAPn/9f8DAPv/9f8DAP3/9f8DAP//9f8DAAEA9f8DAAMA9f8DAAUA9f8DAAcA9f8DAAkA9f8DAAsA9f8FAPX/9f8FAPf/9f8FAPn/9f8FAPv/9f8FAP3/9f8FAP//9f8FAAEA9f8FAAMA9f8FAAUA9f8FAAcA9f8FAAkA9f8FAAsA9f8HAPf/9f8HAPn/9f8HAPv/9f8HAP3/9f8HAP//9f8HAAEA9f8HAAMA9f8HAAUA9f8HAAcA9f8HAAkA9f8JAPf/9f8JAPn/9f8JAPv/9f8JAP3/9f8JAP//9f8JAAEA9f8JAAMA9f8JAAUA9f8JAAcA9f8JAAkA9f8LAPv/9f8LAP3/9f8LAP//9f8LAAEA9f8LAAMA9f8LAAUA9v/0//r/9v/0//z/9v/0//7/9v/0/wAA9v/0/wIA9v/0/wQA9v/0/wYA9v/2//j/9v/2//r/9v/2//z/9v/2//7/9v/2/wAA9v/2/wIA9v/2/wQA9v/2/wYA9v/2/wgA9v/4//b/9v/4//j/9v/4//r/9v/4//z/9v/4//7/9v/4/wAA9v/4/wIA9v/4/wQA9v/4/wYA9v/4/wgA9v/4/woA9v/6//T/9v/6//b/9v/6//j/9v/6//r/9v/6//z/9v/6//7/9v/6/wAA9v/6/wIA9v/6/wQA9v/6/wYA9v/6/wgA9v/6/woA9v/6/wwA9v/8//T/9v/8//b/9v/8//j/9v/8//r/9v/8//z/9v/8//7/9v/8/wAA9v/8/wIA9v/8/wQA9v/8/wYA9v/8/wgA9v/8/woA9v/8/wwA9v/+//T/9v/+//b/9v/+//j/9v/+//r/9v/+//z/9v/+//7/9v/+/wAA9v/+/wIA9v/+/wQA9v/+/wYA9v/+/wgA9v/+/woA9v/+/wwA9v8AAPT/9v8AAPb/9v8AAPj/9v8AAPr/9v8AAPz/9v8AAP7/9v8AAAAA9v8AAAIA9v8AAAQA9v8AAAYA9v8AAAgA9v8AAAoA9v8AAAwA9v8CAPT/9v8CAPb/9v8CAPj/9v8CAPr/9v8CAPz/9v8CAP7/9v8CAAAA9v8CAAIA9v8CAAQA9v8CAAYA9v8CAAgA9v8CAAoA9v8CAAwA9v8EAPT/9v8EAPb/9v8EAPj/9v8EAPr/9v8EAPz/9v8EAP7/9v8EAAAA9v8EAAIA9v8EAAQA9v8EAAYA9v8EAAgA9v8EAAoA9v8EAAwA9v8GAPT/9v8GAPb/9v8GAPj/9v8GAPr/9v8GAPz/9v8GAP7/9v8GAAAA9v8GAAIA9v8GAAQA9v8GAAYA9v8GAAgA9v8GAAoA9v8GAAwA9v8IAPb/9v8IAPj/9v8IAPr/9v8IAPz/9v8IAP7/9v8IAAAA9v8IAAIA9v8IAAQA9v8IAAYA9v8IAAgA9v8IAAoA9v8KAPj/9v8KAPr/9v8KAPz/9v8KAP7/9v8KAAAA9v8KAAIA9v8KAAQA9v8KAAYA9v8KAAgA9v8MAPr/9v8MAPz/9v8MAP7/9v8MAAAA9v8MAAIA9v8MAAQA9v8MAAYA9//z//v/9//z//3/9//z////9//z/wEA9//z/wMA9//z/wUA9//1//f/9//1//n/9//1//v/9//1//3/9//1////9//1/wEA9//1/wMA9//1/wUA9//1/wcA9//1/wkA9//3//X/9//3//f/9//3//n/9//3//v/9//3//3/9//3////9//3/wEA9//3/wMA9//3/wUA9//3/wcA9//3/wkA9//3/wsA9//5//X/9//5//f/9//5//n/9//5//v/9//5//3/9//5////9//5/wEA9//5/wMA9//5/wUA9//5/wcA9//5/wkA9//5/wsA9//7//P/9//7//X/9//7//f/9//7//n/9//7//v/9//7//3/9//7////9//7/wEA9//7/wMA9//7/wUA9//7/wcA9//7/wkA9//7/wsA9//7/w0A9//9//P/9//9//X/9//9//f/9//9//n/9//9//v/9//9//3/9//9////9//9/wEA9//9/wMA9//9/wUA9//9/wcA9//9/wkA9//9/wsA9//9/w0A9/////P/9/////X/9/////f/9/////n/9/////v/9/////3/9///////9////wEA9////wMA9////wUA9////wcA9////wkA9////wsA9////w0A9/8BAPP/9/8BAPX/9/8BAPf/9/8BAPn/9/8BAPv/9/8BAP3/9/8BAP//9/8BAAEA9/8BAAMA9/8BAAUA9/8BAAcA9/8BAAkA9/8BAAsA9/8BAA0A9/8DAPP/9/8DAPX/9/8DAPf/9/8DAPn/9/8DAPv/9/8DAP3/9/8DAP//9/8DAAEA9/8DAAMA9/8DAAUA9/8DAAcA9/8DAAkA9/8DAAsA9/8DAA0A9/8FAPP/9/8FAPX/9/8FAPf/9/8FAPn/9/8FAPv/9/8FAP3/9/8FAP//9/8FAAEA9/8FAAMA9/8FAAUA9/8FAAcA9/8FAAkA9/8FAAsA9/8FAA0A9/8HAPX/9/8HAPf/9/8HAPn/9/8HAPv/9/8HAP3/9/8HAP//9/8HAAEA9/8HAAMA9/8HAAUA9/8HAAcA9/8HAAkA9/8HAAsA9/8JAPX/9/8JAPf/9/8JAPn/9/8JAPv/9/8JAP3/9/8JAP//9/8JAAEA9/8JAAMA9/8JAAUA9/8JAAcA9/8JAAkA9/8JAAsA9/8LAPf/9/8LAPn/9/8LAPv/9/8LAP3/9/8LAP//9/8LAAEA9/8LAAMA9/8LAAUA9/8LAAcA9/8LAAkA9/8NAPv/9/8NAP3/9/8NAP//9/8NAAEA9/8NAAMA9/8NAAUA+P/y//z/+P/y//7/+P/y/wAA+P/y/wIA+P/y/wQA+P/0//j/+P/0//r/+P/0//z/+P/0//7/+P/0/wAA+P/0/wIA+P/0/wQA+P/0/wYA+P/0/wgA+P/2//b/+P/2//j/+P/2//r/+P/2//z/+P/2//7/+P/2/wAA+P/2/wIA+P/2/wQA+P/2/wYA+P/2/wgA+P/2/woA+P/4//T/+P/4//b/+P/4//j/+P/4//r/+P/4//z/+P/4//7/+P/4/wAA+P/4/wIA+P/4/wQA+P/4/wYA+P/4/wgA+P/4/woA+P/4/wwA+P/6//T/+P/6//b/+P/6//j/+P/6//r/+P/6//z/+P/6//7/+P/6/wAA+P/6/wIA+P/6/wQA+P/6/wYA+P/6/wgA+P/6/woA+P/6/wwA+P/8//L/+P/8//T/+P/8//b/+P/8//j/+P/8//r/+P/8//z/+P/8//7/+P/8/wAA+P/8/wIA+P/8/wQA+P/8/wYA+P/8/wgA+P/8/woA+P/8/wwA+P/8/w4A+P/+//L/+P/+//T/+P/+//b/+P/+//j/+P/+//r/+P/+//z/+P/+//7/+P/+/wAA+P/+/wIA+P/+/wQA+P/+/wYA+P/+/wgA+P/+/woA+P/+/wwA+P/+/w4A+P8AAPL/+P8AAPT/+P8AAPb/+P8AAPj/+P8AAPr/+P8AAPz/+P8AAP7/+P8AAAAA+P8AAAIA+P8AAAQA+P8AAAYA+P8AAAgA+P8AAAoA+P8AAAwA+P8AAA4A+P8CAPL/+P8CAPT/+P8CAPb/+P8CAPj/+P8CAPr/+P8CAPz/+P8CAP7/+P8CAAAA+P8CAAIA+P8CAAQA+P8CAAYA+P8CAAgA+P8CAAoA+P8CAAwA+P8CAA4A+P8EAPL/+P8EAPT/+P8EAPb/+P8EAPj/+P8EAPr/+P8EAPz/+P8EAP7/+P8EAAAA+P8EAAIA+P8EAAQA+P8EAAYA+P8EAAgA+P8EAAoA+P8EAAwA+P8EAA4A+P8GAPT/+P8GAPb/+P8GAPj/+P8GAPr/+P8GAPz/+P8GAP7/+P8GAAAA+P8GAAIA+P8GAAQA+P8GAAYA+P8GAAgA+P8GAAoA+P8GAAwA+P8IAPT/+P8IAPb/+P8IAPj/+P8IAPr/+P8IAPz/+P8IAP7/+P8IAAAA+P8IAAIA+P8IAAQA+P8IAAYA+P8IAAgA+P8IAAoA+P8IAAwA+P8KAPb/+P8KAPj/+P8KAPr/+P8KAPz/+P8KAP7/+P8KAAAA+P8KAAIA+P8KAAQA+P8KAAYA+P8KAAgA+P8KAAoA+P8MAPj/+P8MAPr/+P8MAPz/+P8MAP7/+P8MAAAA+P8MAAIA+P8MAAQA+P8MAAYA+P8MAAgA+P8OAPz/+P8OAP7/+P8OAAAA+P8OAAIA+P8OAAQA+f/x//3/+f/x////+f/x/wEA+f/x/wMA+f/z//n/+f/z//v/+f/z//3/+f/z////+f/z/wEA+f/z/wMA+f/z/wUA+f/z/wcA+f/1//f/+f/1//n/+f/1//v/+f/1//3/+f/1////+f/1/wEA+f/1/wMA+f/1/wUA+f/1/wcA+f/1/wkA+f/3//X/+f/3//f/+f/3//n/+f/3//v/+f/3//3/+f/3////+f/3/wEA+f/3/wMA+f/3/wUA+f/3/wcA+f/3/wkA+f/3/wsA+f/5//P/+f/5//X/+f/5//f/+f/5//n/+f/5//v/+f/5//3/+f/5////+f/5/wEA+f/5/wMA+f/5/wUA+f/5/wcA+f/5/wkA+f/5/wsA+f/5/w0A+f/7//P/+f/7//X/+f/7//f/+f/7//n/+f/7//v/+f/7//3/+f/7////+f/7/wEA+f/7/wMA+f/7/wUA+f/7/wcA+f/7/wkA+f/7/wsA+f/7/w0A+f/9//H/+f/9//P/+f/9//X/+f/9//f/+f/9//n/+f/9//v/+f/9//3/+f/9////+f/9/wEA+f/9/wMA+f/9/wUA+f/9/wcA+f/9/wkA+f/9/wsA+f/9/w0A+f/9/w8A+f////H/+f////P/+f////X/+f////f/+f////n/+f////v/+f////3/+f//////+f///wEA+f///wMA+f///wUA+f///wcA+f///wkA+f///wsA+f///w0A+f///w8A+f8BAPH/+f8BAPP/+f8BAPX/+f8BAPf/+f8BAPn/+f8BAPv/+f8BAP3/+f8BAP//+f8BAAEA+f8BAAMA+f8BAAUA+f8BAAcA+f8BAAkA+f8BAAsA+f8BAA0A+f8BAA8A+f8DAPH/+f8DAPP/+f8DAPX/+f8DAPf/+f8DAPn/+f8DAPv/+f8DAP3/+f8DAP//+f8DAAEA+f8DAAMA+f8DAAUA+f8DAAcA+f8DAAkA+f8DAAsA+f8DAA0A+f8DAA8A+f8FAPP/+f8FAPX/+f8FAPf/+f8FAPn/+f8FAPv/+f8FAP3/+f8FAP//+f8FAAEA+f8FAAMA+f8FAAUA+f8FAAcA+f8FAAkA+f8FAAsA+f8FAA0A+f8HAPP/+f8HAPX/+f8HAPf/+f8HAPn/+f8HAPv/+f8HAP3/+f8HAP//+f8HAAEA+f8HAAMA+f8HAAUA+f8HAAcA+f8HAAkA+f8HAAsA+f8HAA0A+f8JAPX/+f8JAPf/+f8JAPn/+f8JAPv/+f8JAP3/+f8JAP//+f8JAAEA+f8JAAMA+f8JAAUA+f8JAAcA+f8JAAkA+f8JAAsA+f8LAPf/+f8LAPn/+f8LAPv/+f8LAP3/+f8LAP//+f8LAAEA+f8LAAMA+f8LAAUA+f8LAAcA+f8LAAkA+f8NAPn/+f8NAPv/+f8NAP3/+f8NAP//+f8NAAEA+f8NAAMA+f8NAAUA+f8NAAcA+f8PAP3/+f8PAP//+f8PAAEA+f8PAAMA+v/y//r/+v/y//z/+v/y//7/+v/y/wAA+v/y/wIA+v/y/wQA+v/y/wYA+v/0//b/+v/0//j/+v/0//r/+v/0//z/+v/0//7/+v/0/wAA+v/0/wIA+v/0/wQA+v/0/wYA+v/0/wgA+v/0/woA+v/2//T/+v/2//b/+v/2//j/+v/2//r/+v/2//z/+v/2//7/+v/2/wAA+v/2/wIA+v/2/wQA+v/2/wYA+v/2/wgA+v/2/woA+v/2/wwA+v/4//T/+v/4//b/+v/4//j/+v/4//r/+v/4//z/+v/4//7/+v/4/wAA+v/4/wIA+v/4/wQA+v/4/wYA+v/4/wgA+v/4/woA+v/4/wwA+v/6//L/+v/6//T/+v/6//b/+v/6//j/+v/6//r/+v/6//z/+v/6//7/+v/6/wAA+v/6/wIA+v/6/wQA+v/6/wYA+v/6/wgA+v/6/woA+v/6/wwA+v/6/w4A+v/8//L/+v/8//T/+v/8//b/+v/8//j/+v/8//r/+v/8//z/+v/8//7/+v/8/wAA+v/8/wIA+v/8/wQA+v/8/wYA+v/8/wgA+v/8/woA+v/8/wwA+v/8/w4A+v/+//L/+v/+//T/+v/+//b/+v/+//j/+v/+//r/+v/+//z/+v/+//7/+v/+/wAA+v/+/wIA+v/+/wQA+v/+/wYA+v/+/wgA+v/+/woA+v/+/wwA+v/+/w4A+v8AAPL/+v8AAPT/+v8AAPb/+v8AAPj/+v8AAPr/+v8AAPz/+v8AAP7/+v8AAAAA+v8AAAIA+v8AAAQA+v8AAAYA+v8AAAgA+v8AAAoA+v8AAAwA+v8AAA4A+v8CAPL/+v8CAPT/+v8CAPb/+v8CAPj/+v8CAPr/+v8CAPz/+v8CAP7/+v8CAAAA+v8CAAIA+v8CAAQA+v8CAAYA+v8CAAgA+v8CAAoA+v8CAAwA+v8CAA4A+v8EAPL/+v8EAPT/+v8EAPb/+v8EAPj/+v8EAPr/+v8EAPz/+v8EAP7/+v8EAAAA+v8EAAIA+v8EAAQA+v8EAAYA+v8EAAgA+v8EAAoA+v8EAAwA+v8EAA4A+v8GAPL/+v8GAPT/+v8GAPb/+v8GAPj/+v8GAPr/+v8GAPz/+v8GAP7/+v8GAAAA+v8GAAIA+v8GAAQA+v8GAAYA+v8GAAgA+v8GAAoA+v8GAAwA+v8GAA4A+v8IAPT/+v8IAPb/+v8IAPj/+v8IAPr/+v8IAPz/+v8IAP7/+v8IAAAA+v8IAAIA+v8IAAQA+v8IAAYA+v8IAAgA+v8IAAoA+v8IAAwA+v8KAPT/+v8KAPb/+v8KAPj/+v8KAPr/+v8KAPz/+v8KAP7/+v8KAAAA+v8KAAIA+v8KAAQA+v8KAAYA+v8KAAgA+v8KAAoA+v8KAAwA+v8MAPb/+v8MAPj/+v8MAPr/+v8MAPz/+v8MAP7/+v8MAAAA+v8MAAIA+v8MAAQA+v8MAAYA+v8MAAgA+v8MAAoA+v8OAPr/+v8OAPz/+v8OAP7/+v8OAAAA+v8OAAIA+v8OAAQA+v8OAAYA+//x//v/+//x//3/+//x////+//x/wEA+//x/wMA+//x/wUA+//z//f/+//z//n/+//z//v/+//z//3/+//z////+//z/wEA+//z/wMA+//z/wUA+//z/wcA+//z/wkA+//1//X/+//1//f/+//1//n/+//1//v/+//1//3/+//1////+//1/wEA+//1/wMA+//1/wUA+//1/wcA+//1/wkA+//1/wsA+//3//P/+//3//X/+//3//f/+//3//n/+//3//v/+//3//3/+//3////+//3/wEA+//3/wMA+//3/wUA+//3/wcA+//3/wkA+//3/wsA+//3/w0A+//5//P/+//5//X/+//5//f/+//5//n/+//5//v/+//5//3/+//5////+//5/wEA+//5/wMA+//5/wUA+//5/wcA+//5/wkA+//5/wsA+//5/w0A+//7//H/+//7//P/+//7//X/+//7//f/+//7//n/+//7//v/+//7//3/+//7////+//7/wEA+//7/wMA+//7/wUA+//7/wcA+//7/wkA+//7/wsA+//7/w0A+//7/w8A+//9//H/+//9//P/+//9//X/+//9//f/+//9//n/+//9//v/+//9//3/+//9////+//9/wEA+//9/wMA+//9/wUA+//9/wcA+//9/wkA+//9/wsA+//9/w0A+//9/w8A+/////H/+/////P/+/////X/+/////f/+/////n/+/////v/+/////3/+///////+////wEA+////wMA+////wUA+////wcA+////wkA+////wsA+////w0A+////w8A+/8BAPH/+/8BAPP/+/8BAPX/+/8BAPf/+/8BAPn/+/8BAPv/+/8BAP3/+/8BAP//+/8BAAEA+/8BAAMA+/8BAAUA+/8BAAcA+/8BAAkA+/8BAAsA+/8BAA0A+/8BAA8A+/8DAPH/+/8DAPP/+/8DAPX/+/8DAPf/+/8DAPn/+/8DAPv/+/8DAP3/+/8DAP//+/8DAAEA+/8DAAMA+/8DAAUA+/8DAAcA+/8DAAkA+/8DAAsA+/8DAA0A+/8DAA8A+/8FAPH/+/8FAPP/+/8FAPX/+/8FAPf/+/8FAPn/+/8FAPv/+/8FAP3/+/8FAP//+/8FAAEA+/8FAAMA+/8FAAUA+/8FAAcA+/8FAAkA+/8FAAsA+/8FAA0A+/8FAA8A+/8HAPP/+/8HAPX/+/8HAPf/+/8HAPn/+/8HAPv/+/8HAP3/+/8HAP//+/8HAAEA+/8HAAMA+/8HAAUA+/8HAAcA+/8HAAkA+/8HAAsA+/8HAA0A+/8JAPP/+/8JAPX/+/8JAPf/+/8JAPn/+/8JAPv/+/8JAP3/+/8JAP//+/8JAAEA+/8JAAMA+/8JAAUA+/8JAAcA+/8JAAkA+/8JAAsA+/8JAA0A+/8LAPX/+/8LAPf/+/8LAPn/+/8LAPv/+/8LAP3/+/8LAP//+/8LAAEA+/8LAAMA+/8LAAUA+/8LAAcA+/8LAAkA+/8LAAsA+/8NAPf/+/8NAPn/+/8NAPv/+/8NAP3/+/8NAP//+/8NAAEA+/8NAAMA+/8NAAUA+/8NAAcA+/8NAAkA+/8PAPv/+/8PAP3/+/8PAP//+/8PAAEA+/8PAAMA+/8PAAUA/P/w//7//P/w/wAA/P/w/wIA/P/y//j//P/y//r//P/y//z//P/y//7//P/y/wAA/P/y/wIA/P/y/wQA/P/y/wYA/P/y/wgA/P/0//b//P/0//j//P/0//r//P/0//z//P/0//7//P/0/wAA/P/0/wIA/P/0/wQA/P/0/wYA/P/0/wgA/P/0/woA/P/2//T//P/2//b//P/2//j//P/2//r//P/2//z//P/2//7//P/2/wAA/P/2/wIA/P/2/wQA/P/2/wYA/P/2/wgA/P/2/woA/P/2/wwA/P/4//L//P/4//T//P/4//b//P/4//j//P/4//r//P/4//z//P/4//7//P/4/wAA/P/4/wIA/P/4/wQA/P/4/wYA/P/4/wgA/P/4/woA/P/4/wwA/P/4/w4A/P/6//L//P/6//T//P/6//b//P/6//j//P/6//r//P/6//z//P/6//7//P/6/wAA/P/6/wIA/P/6/wQA/P/6/wYA/P/6/wgA/P/6/woA/P/6/wwA/P/6/w4A/P/8//L//P/8//T//P/8//b//P/8//j//P/8//r//P/8//z//P/8//7//P/8/wAA/P/8/wIA/P/8/wQA/P/8/wYA/P/8/wgA/P/8/woA/P/8/wwA/P/8/w4A/P/+//D//P/+//L//P/+//T//P/+//b//P/+//j//P/+//r//P/+//z//P/+//7//P/+/wAA/P/+/wIA/P/+/wQA/P/+/wYA/P/+/wgA/P/+/woA/P/+/wwA/P/+/w4A/P/+/xAA/P8AAPD//P8AAPL//P8AAPT//P8AAPb//P8AAPj//P8AAPr//P8AAPz//P8AAP7//P8AAAAA/P8AAAIA/P8AAAQA/P8AAAYA/P8AAAgA/P8AAAoA/P8AAAwA/P8AAA4A/P8AABAA/P8CAPD//P8CAPL//P8CAPT//P8CAPb//P8CAPj//P8CAPr//P8CAPz//P8CAP7//P8CAAAA/P8CAAIA/P8CAAQA/P8CAAYA/P8CAAgA/P8CAAoA/P8CAAwA/P8CAA4A/P8CABAA/P8EAPL//P8EAPT//P8EAPb//P8EAPj//P8EAPr//P8EAPz//P8EAP7//P8EAAAA/P8EAAIA/P8EAAQA/P8EAAYA/P8EAAgA/P8EAAoA/P8EAAwA/P8EAA4A/P8GAPL//P8GAPT//P8GAPb//P8GAPj//P8GAPr//P8GAPz//P8GAP7//P8GAAAA/P8GAAIA/P8GAAQA/P8GAAYA/P8GAAgA/P8GAAoA/P8GAAwA/P8GAA4A/P8IAPL//P8IAPT//P8IAPb//P8IAPj//P8IAPr//P8IAPz//P8IAP7//P8IAAAA/P8IAAIA/P8IAAQA/P8IAAYA/P8IAAgA/P8IAAoA/P8IAAwA/P8IAA4A/P8KAPT//P8KAPb//P8KAPj//P8KAPr//P8KAPz//P8KAP7//P8KAAAA/P8KAAIA/P8KAAQA/P8KAAYA/P8KAAgA/P8KAAoA/P8KAAwA/P8MAPb//P8MAPj//P8MAPr//P8MAPz//P8MAP7//P8MAAAA/P8MAAIA/P8MAAQA/P8MAAYA/P8MAAgA/P8MAAoA/P8OAPj//P8OAPr//P8OAPz//P8OAP7//P8OAAAA/P8OAAIA/P8OAAQA/P8OAAYA/P8OAAgA/P8QAP7//P8QAAAA/P8QAAIA/f/x//n//f/x//v//f/x//3//f/x/////f/x/wEA/f/x/wMA/f/x/wUA/f/x/wcA/f/z//f//f/z//n//f/z//v//f/z//3//f/z/////f/z/wEA/f/z/wMA/f/z/wUA/f/z/wcA/f/z/wkA/f/1//X//f/1//f//f/1//n//f/1//v//f/1//3//f/1/////f/1/wEA/f/1/wMA/f/1/wUA/f/1/wcA/f/1/wkA/f/1/wsA/f/3//P//f/3//X//f/3//f//f/3//n//f/3//v//f/3//3//f/3/////f/3/wEA/f/3/wMA/f/3/wUA/f/3/wcA/f/3/wkA/f/3/wsA/f/3/w0A/f/5//H//f/5//P//f/5//X//f/5//f//f/5//n//f/5//v//f/5//3//f/5/////f/5/wEA/f/5/wMA/f/5/wUA/f/5/wcA/f/5/wkA/f/5/wsA/f/5/w0A/f/5/w8A/f/7//H//f/7//P//f/7//X//f/7//f//f/7//n//f/7//v//f/7//3//f/7/////f/7/wEA/f/7/wMA/f/7/wUA/f/7/wcA/f/7/wkA/f/7/wsA/f/7/w0A/f/7/w8A/f/9//H//f/9//P//f/9//X//f/9//f//f/9//n//f/9//v//f/9//3//f/9/////f/9/wEA/f/9/wMA/f/9/wUA/f/9/wcA/f/9/wkA/f/9/wsA/f/9/w0A/f/9/w8A/f////H//f////P//f////X//f////f//f////n//f////v//f////3//f///////f///wEA/f///wMA/f///wUA/f///wcA/f///wkA/f///wsA/f///w0A/f///w8A/f8BAPH//f8BAPP//f8BAPX//f8BAPf//f8BAPn//f8BAPv//f8BAP3//f8BAP///f8BAAEA/f8BAAMA/f8BAAUA/f8BAAcA/f8BAAkA/f8BAAsA/f8BAA0A/f8BAA8A/f8DAPH//f8DAPP//f8DAPX//f8DAPf//f8DAPn//f8DAPv//f8DAP3//f8DAP///f8DAAEA/f8DAAMA/f8DAAUA/f8DAAcA/f8DAAkA/f8DAAsA/f8DAA0A/f8DAA8A/f8FAPH//f8FAPP//f8FAPX//f8FAPf//f8FAPn//f8FAPv//f8FAP3//f8FAP///f8FAAEA/f8FAAMA/f8FAAUA/f8FAAcA/f8FAAkA/f8FAAsA/f8FAA0A/f8FAA8A/f8HAPH//f8HAPP//f8HAPX//f8HAPf//f8HAPn//f8HAPv//f8HAP3//f8HAP///f8HAAEA/f8HAAMA/f8HAAUA/f8HAAcA/f8HAAkA/f8HAAsA/f8HAA0A/f8HAA8A/f8JAPP//f8JAPX//f8JAPf//f8JAPn//f8JAPv//f8JAP3//f8JAP///f8JAAEA/f8JAAMA/f8JAAUA/f8JAAcA/f8JAAkA/f8JAAsA/f8JAA0A/f8LAPX//f8LAPf//f8LAPn//f8LAPv//f8LAP3//f8LAP///f8LAAEA/f8LAAMA/f8LAAUA/f8LAAcA/f8LAAkA/f8LAAsA/f8NAPf//f8NAPn//f8NAPv//f8NAP3//f8NAP///f8NAAEA/f8NAAMA/f8NAAUA/f8NAAcA/f8NAAkA/f8PAPn//f8PAPv//f8PAP3//f8PAP///f8PAAEA/f8PAAMA/f8PAAUA/f8PAAcA/v/w//z//v/w//7//v/w/wAA/v/w/wIA/v/w/wQA/v/y//j//v/y//r//v/y//z//v/y//7//v/y/wAA/v/y/wIA/v/y/wQA/v/y/wYA/v/y/wgA/v/0//b//v/0//j//v/0//r//v/0//z//v/0//7//v/0/wAA/v/0/wIA/v/0/wQA/v/0/wYA/v/0/wgA/v/0/woA/v/2//T//v/2//b//v/2//j//v/2//r//v/2//z//v/2//7//v/2/wAA/v/2/wIA/v/2/wQA/v/2/wYA/v/2/wgA/v/2/woA/v/2/wwA/v/4//L//v/4//T//v/4//b//v/4//j//v/4//r//v/4//z//v/4//7//v/4/wAA/v/4/wIA/v/4/wQA/v/4/wYA/v/4/wgA/v/4/woA/v/4/wwA/v/4/w4A/v/6//L//v/6//T//v/6//b//v/6//j//v/6//r//v/6//z//v/6//7//v/6/wAA/v/6/wIA/v/6/wQA/v/6/wYA/v/6/wgA/v/6/woA/v/6/wwA/v/6/w4A/v/8//D//v/8//L//v/8//T//v/8//b//v/8//j//v/8//r//v/8//z//v/8//7//v/8/wAA/v/8/wIA/v/8/wQA/v/8/wYA/v/8/wgA/v/8/woA/v/8/wwA/v/8/w4A/v/8/xAA/v/+//D//v/+//L//v/+//T//v/+//b//v/+//j//v/+//r//v/+//z//v/+//7//v/+/wAA/v/+/wIA/v/+/wQA/v/+/wYA/v/+/wgA/v/+/woA/v/+/wwA/v/+/w4A/v/+/xAA/v8AAPD//v8AAPL//v8AAPT//v8AAPb//v8AAPj//v8AAPr//v8AAPz//v8AAP7//v8AAAAA/v8AAAIA/v8AAAQA/v8AAAYA/v8AAAgA/v8AAAoA/v8AAAwA/v8AAA4A/v8AABAA/v8CAPD//v8CAPL//v8CAPT//v8CAPb//v8CAPj//v8CAPr//v8CAPz//v8CAP7//v8CAAAA/v8CAAIA/v8CAAQA/v8CAAYA/v8CAAgA/v8CAAoA/v8CAAwA/v8CAA4A/v8CABAA/v8EAPD//v8EAPL//v8EAPT//v8EAPb//v8EAPj//v8EAPr//v8EAPz//v8EAP7//v8EAAAA/v8EAAIA/v8EAAQA/v8EAAYA/v8EAAgA/v8EAAoA/v8EAAwA/v8EAA4A/v8EABAA/v8GAPL//v8GAPT//v8GAPb//v8GAPj//v8GAPr//v8GAPz//v8GAP7//v8GAAAA/v8GAAIA/v8GAAQA/v8GAAYA/v8GAAgA/v8GAAoA/v8GAAwA/v8GAA4A/v8IAPL//v8IAPT//v8IAPb//v8IAPj//v8IAPr//v8IAPz//v8IAP7//v8IAAAA/v8IAAIA/v8IAAQA/v8IAAYA/v8IAAgA/v8IAAoA/v8IAAwA/v8IAA4A/v8KAPT//v8KAPb//v8KAPj//v8KAPr//v8KAPz//v8KAP7//v8KAAAA/v8KAAIA/v8KAAQA/v8KAAYA/v8KAAgA/v8KAAoA/v8KAAwA/v8MAPb//v8MAPj//v8MAPr//v8MAPz//v8MAP7//v8MAAAA/v8MAAIA/v8MAAQA/v8MAAYA/v8MAAgA/v8MAAoA/v8OAPj//v8OAPr//v8OAPz//v8OAP7//v8OAAAA/v8OAAIA/v8OAAQA/v8OAAYA/v8OAAgA/v8QAPz//v8QAP7//v8QAAAA/v8QAAIA/v8QAAQA///x//n////x//v////x//3////x///////x/wEA///x/wMA///x/wUA///x/wcA///z//f////z//n////z//v////z//3////z///////z/wEA///z/wMA///z/wUA///z/wcA///z/wkA///1//X////1//f////1//n////1//v////1//3////1///////1/wEA///1/wMA///1/wUA///1/wcA///1/wkA///1/wsA///3//P////3//X////3//f////3//n////3//v////3//3////3///////3/wEA///3/wMA///3/wUA///3/wcA///3/wkA///3/wsA///3/w0A///5//H////5//P////5//X////5//f////5//n////5//v////5//3////5///////5/wEA///5/wMA///5/wUA///5/wcA///5/wkA///5/wsA///5/w0A///5/w8A///7//H////7//P////7//X////7//f////7//n////7//v////7//3////7///////7/wEA///7/wMA///7/wUA///7/wcA///7/wkA///7/wsA///7/w0A///7/w8A///9//H////9//P////9//X////9//f////9//n////9//v////9//3////9///////9/wEA///9/wMA///9/wUA///9/wcA///9/wkA///9/wsA///9/w0A///9/w8A//////H///////P///////X///////f///////n///////v///////3//////////////wEA/////wMA/////wUA/////wcA/////wkA/////wsA/////w0A/////w8A//8BAPH///8BAPP///8BAPX///8BAPf///8BAPn///8BAPv///8BAP3///8BAP////8BAAEA//8BAAMA//8BAAUA//8BAAcA//8BAAkA//8BAAsA//8BAA0A//8BAA8A//8DAPH///8DAPP///8DAPX///8DAPf///8DAPn///8DAPv///8DAP3///8DAP////8DAAEA//8DAAMA//8DAAUA//8DAAcA//8DAAkA//8DAAsA//8DAA0A//8DAA8A//8FAPH///8FAPP///8FAPX///8FAPf///8FAPn///8FAPv///8FAP3///8FAP////8FAAEA//8FAAMA//8FAAUA//8FAAcA//8FAAkA//8FAAsA//8FAA0A//8FAA8A//8HAPH///8HAPP///8HAPX///8HAPf///8HAPn///8HAPv///8HAP3///8HAP////8HAAEA//8HAAMA//8HAAUA//8HAAcA//8HAAkA//8HAAsA//8HAA0A//8HAA8A//8JAPP///8JAPX///8JAPf///8JAPn///8JAPv///8JAP3///8JAP////8JAAEA//8JAAMA//8JAAUA//8JAAcA//8JAAkA//8JAAsA//8JAA0A//8LAPX///8LAPf///8LAPn///8LAPv///8LAP3///8LAP////8LAAEA//8LAAMA//8LAAUA//8LAAcA//8LAAkA//8LAAsA//8NAPf///8NAPn///8NAPv///8NAP3///8NAP////8NAAEA//8NAAMA//8NAAUA//8NAAcA//8NAAkA//8PAPn///8PAPv///8PAP3///8PAP////8PAAEA//8PAAMA//8PAAUA//8PAAcAAADw//z/AADw//7/AADw/wAAAADw/wIAAADw/wQAAADy//j/AADy//r/AADy//z/AADy//7/AADy/wAAAADy/wIAAADy/wQAAADy/wYAAADy/wgAAAD0//b/AAD0//j/AAD0//r/AAD0//z/AAD0//7/AAD0/wAAAAD0/wIAAAD0/wQAAAD0/wYAAAD0/wgAAAD0/woAAAD2//T/AAD2//b/AAD2//j/AAD2//r/AAD2//z/AAD2//7/AAD2/wAAAAD2/wIAAAD2/wQAAAD2/wYAAAD2/wgAAAD2/woAAAD2/wwAAAD4//L/AAD4//T/AAD4//b/AAD4//j/AAD4//r/AAD4//z/AAD4//7/AAD4/wAAAAD4/wIAAAD4/wQAAAD4/wYAAAD4/wgAAAD4/woAAAD4/wwAAAD4/w4AAAD6//L/AAD6//T/AAD6//b/AAD6//j/AAD6//r/AAD6//z/AAD6//7/AAD6/wAAAAD6/wIAAAD6/wQAAAD6/wYAAAD6/wgAAAD6/woAAAD6/wwAAAD6/w4AAAD8//D/AAD8//L/AAD8//T/AAD8//b/AAD8//j/AAD8//r/AAD8//z/AAD8//7/AAD8/wAAAAD8/wIAAAD8/wQAAAD8/wYAAAD8/wgAAAD8/woAAAD8/wwAAAD8/w4AAAD8/xAAAAD+//D/AAD+//L/AAD+//T/AAD+//b/AAD+//j/AAD+//r/AAD+//z/AAD+//7/AAD+/wAAAAD+/wIAAAD+/wQAAAD+/wYAAAD+/wgAAAD+/woAAAD+/wwAAAD+/w4AAAD+/xAAAAAAAPD/AAAAAPL/AAAAAPT/AAAAAPb/AAAAAPj/AAAAAPr/AAAAAPz/AAAAAP7/AAAAAAIAAAAAAAQAAAAAAAYAAAAAAAgAAAAAAAoAAAAAAAwAAAAAAA4AAAAAABAAAAACAPD/AAACAPL/AAACAPT/AAACAPb/AAACAPj/AAACAPr/AAACAPz/AAACAP7/AAACAAAAAAACAAIAAAACAAQAAAACAAYAAAACAAgAAAACAAoAAAACAAwAAAACAA4AAAACABAAAAAEAPD/AAAEAPL/AAAEAPT/AAAEAPb/AAAEAPj/AAAEAPr/AAAEAPz/AAAEAP7/AAAEAAAAAAAEAAIAAAAEAAQAAAAEAAYAAAAEAAgAAAAEAAoAAAAEAAwAAAAEAA4AAAAEABAAAAAGAPL/AAAGAPT/AAAGAPb/AAAGAPj/AAAGAPr/AAAGAPz/AAAGAP7/AAAGAAAAAAAGAAIAAAAGAAQAAAAGAAYAAAAGAAgAAAAGAAoAAAAGAAwAAAAGAA4AAAAIAPL/AAAIAPT/AAAIAPb/AAAIAPj/AAAIAPr/AAAIAPz/AAAIAP7/AAAIAAAAAAAIAAIAAAAIAAQAAAAIAAYAAAAIAAgAAAAIAAoAAAAIAAwAAAAIAA4AAAAKAPT/AAAKAPb/AAAKAPj/AAAKAPr/AAAKAPz/AAAKAP7/AAAKAAAAAAAKAAIAAAAKAAQAAAAKAAYAAAAKAAgAAAAKAAoAAAAKAAwAAAAMAPb/AAAMAPj/AAAMAPr/AAAMAPz/AAAMAP7/AAAMAAAAAAAMAAIAAAAMAAQAAAAMAAYAAAAMAAgAAAAMAAoAAAAOAPj/AAAOAPr/AAAOAPz/AAAOAP7/AAAOAAAAAAAOAAIAAAAOAAQAAAAOAAYAAAAOAAgAAAAQAPz/AAAQAP7/AAAQAAAAAAAQAAIAAAAQAAQAAQDx//n/AQDx//v/AQDx//3/AQDx////AQDx/wEAAQDx/wMAAQDx/wUAAQDx/wcAAQDz//f/AQDz//n/AQDz//v/AQDz//3/AQDz////AQDz/wEAAQDz/wMAAQDz/wUAAQDz/wcAAQDz/wkAAQD1//X/AQD1//f/AQD1//n/AQD1//v/AQD1//3/AQD1////AQD1/wEAAQD1/wMAAQD1/wUAAQD1/wcAAQD1/wkAAQD1/wsAAQD3//P/AQD3//X/AQD3//f/AQD3//n/AQD3//v/AQD3//3/AQD3////AQD3/wEAAQD3/wMAAQD3/wUAAQD3/wcAAQD3/wkAAQD3/wsAAQD3/w0AAQD5//H/AQD5//P/AQD5//X/AQD5//f/AQD5//n/AQD5//v/AQD5//3/AQD5////AQD5/wEAAQD5/wMAAQD5/wUAAQD5/wcAAQD5/wkAAQD5/wsAAQD5/w0AAQD5/w8AAQD7//H/AQD7//P/AQD7//X/AQD7//f/AQD7//n/AQD7//v/AQD7//3/AQD7////AQD7/wEAAQD7/wMAAQD7/wUAAQD7/wcAAQD7/wkAAQD7/wsAAQD7/w0AAQD7/w8AAQD9//H/AQD9//P/AQD9//X/AQD9//f/AQD9//n/AQD9//v/AQD9//3/AQD9////AQD9/wEAAQD9/wMAAQD9/wUAAQD9/wcAAQD9/wkAAQD9/wsAAQD9/w0AAQD9/w8AAQD///H/AQD///P/AQD///X/AQD///f/AQD///n/AQD///v/AQD///3/AQD/////AQD//wEAAQD//wMAAQD//wUAAQD//wcAAQD//wkAAQD//wsAAQD//w0AAQD//w8AAQABAPH/AQABAPP/AQABAPX/AQABAPf/AQABAPn/AQABAPv/AQABAP3/AQABAP//AQABAAEAAQABAAMAAQABAAUAAQABAAcAAQABAAkAAQABAAsAAQABAA0AAQABAA8AAQADAPH/AQADAPP/AQADAPX/AQADAPf/AQADAPn/AQADAPv/AQADAP3/AQADAP//AQADAAEAAQADAAMAAQADAAUAAQADAAcAAQADAAkAAQADAAsAAQADAA0AAQADAA8AAQAFAPH/AQAFAPP/AQAFAPX/AQAFAPf/AQAFAPn/AQAFAPv/AQAFAP3/AQAFAP//AQAFAAEAAQAFAAMAAQAFAAUAAQAFAAcAAQAFAAkAAQAFAAsAAQAFAA0AAQAFAA8AAQAHAPH/AQAHAPP/AQAHAPX/AQAHAPf/AQAHAPn/AQAHAPv/AQAHAP3/AQAHAP//AQAHAAEAAQAHAAMAAQAHAAUAAQAHAAcAAQAHAAkAAQAHAAsAAQAHAA0AAQAHAA8AAQAJAPP/AQAJAPX/AQAJAPf/AQAJAPn/AQAJAPv/AQAJAP3/AQAJAP//AQAJAAEAAQAJAAMAAQAJAAUAAQAJAAcAAQAJAAkAAQAJAAsAAQAJAA0AAQALAPX/AQALAPf/AQALAPn/AQALAPv/AQALAP3/AQALAP//AQALAAEAAQALAAMAAQALAAUAAQALAAcAAQALAAkAAQALAAsAAQANAPf/AQANAPn/AQANAPv/AQANAP3/AQANAP//AQANAAEAAQANAAMAAQANAAUAAQANAAcAAQANAAkAAQAPAPn/AQAPAPv/AQAPAP3/AQAPAP//AQAPAAEAAQAPAAMAAQAPAAUAAQAPAAcAAgDw//z/AgDw//7/AgDw/wAAAgDw/wIAAgDw/wQAAgDy//j/AgDy//r/AgDy//z/AgDy//7/AgDy/wAAAgDy/wIAAgDy/wQAAgDy/wYAAgDy/wgAAgD0//b/AgD0//j/AgD0//r/AgD0//z/AgD0//7/AgD0/wAAAgD0/wIAAgD0/wQAAgD0/wYAAgD0/wgAAgD0/woAAgD2//T/AgD2//b/AgD2//j/AgD2//r/AgD2//z/AgD2//7/AgD2/wAAAgD2/wIAAgD2/wQAAgD2/wYAAgD2/wgAAgD2/woAAgD2/wwAAgD4//L/AgD4//T/AgD4//b/AgD4//j/AgD4//r/AgD4//z/AgD4//7/AgD4/wAAAgD4/wIAAgD4/wQAAgD4/wYAAgD4/wgAAgD4/woAAgD4/wwAAgD4/w4AAgD6//L/AgD6//T/AgD6//b/AgD6//j/AgD6//r/AgD6//z/AgD6//7/AgD6/wAAAgD6/wIAAgD6/wQAAgD6/wYAAgD6/wgAAgD6/woAAgD6/wwAAgD6/w4AAgD8//D/AgD8//L/AgD8//T/AgD8//b/AgD8//j/AgD8//r/AgD8//z/AgD8//7/AgD8/wAAAgD8/wIAAgD8/wQAAgD8/wYAAgD8/wgAAgD8/woAAgD8/wwAAgD8/w4AAgD8/xAAAgD+//D/AgD+//L/AgD+//T/AgD+//b/AgD+//j/AgD+//r/AgD+//z/AgD+//7/AgD+/wAAAgD+/wIAAgD+/wQAAgD+/wYAAgD+/wgAAgD+/woAAgD+/wwAAgD+/w4AAgD+/xAAAgAAAPD/AgAAAPL/AgAAAPT/AgAAAPb/AgAAAPj/AgAAAPr/AgAAAPz/AgAAAP7/AgAAAAAAAgAAAAIAAgAAAAQAAgAAAAYAAgAAAAgAAgAAAAoAAgAAAAwAAgAAAA4AAgAAABAAAgACAPD/AgACAPL/AgACAPT/AgACAPb/AgACAPj/AgACAPr/AgACAPz/AgACAP7/AgACAAAAAgACAAIAAgACAAQAAgACAAYAAgACAAgAAgACAAoAAgACAAwAAgACAA4AAgACABAAAgAEAPD/AgAEAPL/AgAEAPT/AgAEAPb/AgAEAPj/AgAEAPr/AgAEAPz/AgAEAP7/AgAEAAAAAgAEAAIAAgAEAAQAAgAEAAYAAgAEAAgAAgAEAAoAAgAEAAwAAgAEAA4AAgAEABAAAgAGAPL/AgAGAPT/AgAGAPb/AgAGAPj/AgAGAPr/AgAGAPz/AgAGAP7/AgAGAAAAAgAGAAIAAgAGAAQAAgAGAAYAAgAGAAgAAgAGAAoAAgAGAAwAAgAGAA4AAgAIAPL/AgAIAPT/AgAIAPb/AgAIAPj/AgAIAPr/AgAIAPz/AgAIAP7/AgAIAAAAAgAIAAIAAgAIAAQAAgAIAAYAAgAIAAgAAgAIAAoAAgAIAAwAAgAIAA4AAgAKAPT/AgAKAPb/AgAKAPj/AgAKAPr/AgAKAPz/AgAKAP7/AgAKAAAAAgAKAAIAAgAKAAQAAgAKAAYAAgAKAAgAAgAKAAoAAgAKAAwAAgAMAPb/AgAMAPj/AgAMAPr/AgAMAPz/AgAMAP7/AgAMAAAAAgAMAAIAAgAMAAQAAgAMAAYAAgAMAAgAAgAMAAoAAgAOAPj/AgAOAPr/AgAOAPz/AgAOAP7/AgAOAAAAAgAOAAIAAgAOAAQAAgAOAAYAAgAOAAgAAgAQAPz/AgAQAP7/AgAQAAAAAgAQAAIAAgAQAAQAAwDx//n/AwDx//v/AwDx//3/AwDx////AwDx/wEAAwDx/wMAAwDx/wUAAwDx/wcAAwDz//f/AwDz//n/AwDz//v/AwDz//3/AwDz////AwDz/wEAAwDz/wMAAwDz/wUAAwDz/wcAAwDz/wkAAwD1//X/AwD1//f/AwD1//n/AwD1//v/AwD1//3/AwD1////AwD1/wEAAwD1/wMAAwD1/wUAAwD1/wcAAwD1/wkAAwD1/wsAAwD3//P/AwD3//X/AwD3//f/AwD3//n/AwD3//v/AwD3//3/AwD3////AwD3/wEAAwD3/wMAAwD3/wUAAwD3/wcAAwD3/wkAAwD3/wsAAwD3/w0AAwD5//H/AwD5//P/AwD5//X/AwD5//f/AwD5//n/AwD5//v/AwD5//3/AwD5////AwD5/wEAAwD5/wMAAwD5/wUAAwD5/wcAAwD5/wkAAwD5/wsAAwD5/w0AAwD5/w8AAwD7//H/AwD7//P/AwD7//X/AwD7//f/AwD7//n/AwD7//v/AwD7//3/AwD7////AwD7/wEAAwD7/wMAAwD7/wUAAwD7/wcAAwD7/wkAAwD7/wsAAwD7/w0AAwD7/w8AAwD9//H/AwD9//P/AwD9//X/AwD9//f/AwD9//n/AwD9//v/AwD9//3/AwD9////AwD9/wEAAwD9/wMAAwD9/wUAAwD9/wcAAwD9/wkAAwD9/wsAAwD9/w0AAwD9/w8AAwD///H/AwD///P/AwD///X/AwD///f/AwD///n/AwD///v/AwD///3/AwD/////AwD//wEAAwD//wMAAwD//wUAAwD//wcAAwD//wkAAwD//wsAAwD//w0AAwD//w8AAwABAPH/AwABAPP/AwABAPX/AwABAPf/AwABAPn/AwABAPv/AwABAP3/AwABAP//AwABAAEAAwABAAMAAwABAAUAAwABAAcAAwABAAkAAwABAAsAAwABAA0AAwABAA8AAwADAPH/AwADAPP/AwADAPX/AwADAPf/AwADAPn/AwADAPv/AwADAP3/AwADAP//AwADAAEAAwADAAMAAwADAAUAAwADAAcAAwADAAkAAwADAAsAAwADAA0AAwADAA8AAwAFAPH/AwAFAPP/AwAFAPX/AwAFAPf/AwAFAPn/AwAFAPv/AwAFAP3/AwAFAP//AwAFAAEAAwAFAAMAAwAFAAUAAwAFAAcAAwAFAAkAAwAFAAsAAwAFAA0AAwAFAA8AAwAHAPH/AwAHAPP/AwAHAPX/AwAHAPf/AwAHAPn/AwAHAPv/AwAHAP3/AwAHAP//AwAHAAEAAwAHAAMAAwAHAAUAAwAHAAcAAwAHAAkAAwAHAAsAAwAHAA0AAwAHAA8AAwAJAPP/AwAJAPX/AwAJAPf/AwAJAPn/AwAJAPv/AwAJAP3/AwAJAP//AwAJAAEAAwAJAAMAAwAJAAUAAwAJAAcAAwAJAAkAAwAJAAsAAwAJAA0AAwALAPX/AwALAPf/AwALAPn/AwALAPv/AwALAP3/AwALAP//AwALAAEAAwALAAMAAwALAAUAAwALAAcAAwALAAkAAwALAAsAAwANAPf/AwANAPn/AwANAPv/AwANAP3/AwANAP//AwANAAEAAwANAAMAAwANAAUAAwANAAcAAwANAAkAAwAPAPn/AwAPAPv/AwAPAP3/AwAPAP//AwAPAAEAAwAPAAMAAwAPAAUAAwAPAAcABADw//7/BADw/wAABADw/wIABADy//j/BADy//r/BADy//z/BADy//7/BADy/wAABADy/wIABADy/wQABADy/wYABADy/wgABAD0//b/BAD0//j/BAD0//r/BAD0//z/BAD0//7/BAD0/wAABAD0/wIABAD0/wQABAD0/wYABAD0/wgABAD0/woABAD2//T/BAD2//b/BAD2//j/BAD2//r/BAD2//z/BAD2//7/BAD2/wAABAD2/wIABAD2/wQABAD2/wYABAD2/wgABAD2/woABAD2/wwABAD4//L/BAD4//T/BAD4//b/BAD4//j/BAD4//r/BAD4//z/BAD4//7/BAD4/wAABAD4/wIABAD4/wQABAD4/wYABAD4/wgABAD4/woABAD4/wwABAD4/w4ABAD6//L/BAD6//T/BAD6//b/BAD6//j/BAD6//r/BAD6//z/BAD6//7/BAD6/wAABAD6/wIABAD6/wQABAD6/wYABAD6/wgABAD6/woABAD6/wwABAD6/w4ABAD8//L/BAD8//T/BAD8//b/BAD8//j/BAD8//r/BAD8//z/BAD8//7/BAD8/wAABAD8/wIABAD8/wQABAD8/wYABAD8/wgABAD8/woABAD8/wwABAD8/w4ABAD+//D/BAD+//L/BAD+//T/BAD+//b/BAD+//j/BAD+//r/BAD+//z/BAD+//7/BAD+/wAABAD+/wIABAD+/wQABAD+/wYABAD+/wgABAD+/woABAD+/wwABAD+/w4ABAD+/xAABAAAAPD/BAAAAPL/BAAAAPT/BAAAAPb/BAAAAPj/BAAAAPr/BAAAAPz/BAAAAP7/BAAAAAAABAAAAAIABAAAAAQABAAAAAYABAAAAAgABAAAAAoABAAAAAwABAAAAA4ABAAAABAABAACAPD/BAACAPL/BAACAPT/BAACAPb/BAACAPj/BAACAPr/BAACAPz/BAACAP7/BAACAAAABAACAAIABAACAAQABAACAAYABAACAAgABAACAAoABAACAAwABAACAA4ABAACABAABAAEAPL/BAAEAPT/BAAEAPb/BAAEAPj/BAAEAPr/BAAEAPz/BAAEAP7/BAAEAAAABAAEAAIABAAEAAQABAAEAAYABAAEAAgABAAEAAoABAAEAAwABAAEAA4ABAAGAPL/BAAGAPT/BAAGAPb/BAAGAPj/BAAGAPr/BAAGAPz/BAAGAP7/BAAGAAAABAAGAAIABAAGAAQABAAGAAYABAAGAAgABAAGAAoABAAGAAwABAAGAA4ABAAIAPL/BAAIAPT/BAAIAPb/BAAIAPj/BAAIAPr/BAAIAPz/BAAIAP7/BAAIAAAABAAIAAIABAAIAAQABAAIAAYABAAIAAgABAAIAAoABAAIAAwABAAIAA4ABAAKAPT/BAAKAPb/BAAKAPj/BAAKAPr/BAAKAPz/BAAKAP7/BAAKAAAABAAKAAIABAAKAAQABAAKAAYABAAKAAgABAAKAAoABAAKAAwABAAMAPb/BAAMAPj/BAAMAPr/BAAMAPz/BAAMAP7/BAAMAAAABAAMAAIABAAMAAQABAAMAAYABAAMAAgABAAMAAoABAAOAPj/BAAOAPr/BAAOAPz/BAAOAP7/BAAOAAAABAAOAAIABAAOAAQABAAOAAYABAAOAAgABAAQAP7/BAAQAAAABAAQAAIABQDx//v/BQDx//3/BQDx////BQDx/wEABQDx/wMABQDx/wUABQDz//f/BQDz//n/BQDz//v/BQDz//3/BQDz////BQDz/wEABQDz/wMABQDz/wUABQDz/wcABQDz/wkABQD1//X/BQD1//f/BQD1//n/BQD1//v/BQD1//3/BQD1////BQD1/wEABQD1/wMABQD1/wUABQD1/wcABQD1/wkABQD1/wsABQD3//P/BQD3//X/BQD3//f/BQD3//n/BQD3//v/BQD3//3/BQD3////BQD3/wEABQD3/wMABQD3/wUABQD3/wcABQD3/wkABQD3/wsABQD3/w0ABQD5//P/BQD5//X/BQD5//f/BQD5//n/BQD5//v/BQD5//3/BQD5////BQD5/wEABQD5/wMABQD5/wUABQD5/wcABQD5/wkABQD5/wsABQD5/w0ABQD7//H/BQD7//P/BQD7//X/BQD7//f/BQD7//n/BQD7//v/BQD7//3/BQD7////BQD7/wEABQD7/wMABQD7/wUABQD7/wcABQD7/wkABQD7/wsABQD7/w0ABQD7/w8ABQD9//H/BQD9//P/BQD9//X/BQD9//f/BQD9//n/BQD9//v/BQD9//3/BQD9////BQD9/wEABQD9/wMABQD9/wUABQD9/wcABQD9/wkABQD9/wsABQD9/w0ABQD9/w8ABQD///H/BQD///P/BQD///X/BQD///f/BQD///n/BQD///v/BQD///3/BQD/////BQD//wEABQD//wMABQD//wUABQD//wcABQD//wkABQD//wsABQD//w0ABQD//w8ABQABAPH/BQABAPP/BQABAPX/BQABAPf/BQABAPn/BQABAPv/BQABAP3/BQABAP//BQABAAEABQABAAMABQABAAUABQABAAcABQABAAkABQABAAsABQABAA0ABQABAA8ABQADAPH/BQADAPP/BQADAPX/BQADAPf/BQADAPn/BQADAPv/BQADAP3/BQADAP//BQADAAEABQADAAMABQADAAUABQADAAcABQADAAkABQADAAsABQADAA0ABQADAA8ABQAFAPH/BQAFAPP/BQAFAPX/BQAFAPf/BQAFAPn/BQAFAPv/BQAFAP3/BQAFAP//BQAFAAEABQAFAAMABQAFAAUABQAFAAcABQAFAAkABQAFAAsABQAFAA0ABQAFAA8ABQAHAPP/BQAHAPX/BQAHAPf/BQAHAPn/BQAHAPv/BQAHAP3/BQAHAP//BQAHAAEABQAHAAMABQAHAAUABQAHAAcABQAHAAkABQAHAAsABQAHAA0ABQAJAPP/BQAJAPX/BQAJAPf/BQAJAPn/BQAJAPv/BQAJAP3/BQAJAP//BQAJAAEABQAJAAMABQAJAAUABQAJAAcABQAJAAkABQAJAAsABQAJAA0ABQALAPX/BQALAPf/BQALAPn/BQALAPv/BQALAP3/BQALAP//BQALAAEABQALAAMABQALAAUABQALAAcABQALAAkABQALAAsABQANAPf/BQANAPn/BQANAPv/BQANAP3/BQANAP//BQANAAEABQANAAMABQANAAUABQANAAcABQANAAkABQAPAPv/BQAPAP3/BQAPAP//BQAPAAEABQAPAAMABQAPAAUABgDy//r/BgDy//z/BgDy//7/BgDy/wAABgDy/wIABgDy/wQABgDy/wYABgD0//b/BgD0//j/BgD0//r/BgD0//z/BgD0//7/BgD0/wAABgD0/wIABgD0/wQABgD0/wYABgD0/wgABgD0/woABgD2//T/BgD2//b/BgD2//j/BgD2//r/BgD2//z/BgD2//7/BgD2/wAABgD2/wIABgD2/wQABgD2/wYABgD2/wgABgD2/woABgD2/wwABgD4//T/BgD4//b/BgD4//j/BgD4//r/BgD4//z/BgD4//7/BgD4/wAABgD4/wIABgD4/wQABgD4/wYABgD4/wgABgD4/woABgD4/wwABgD6//L/BgD6//T/BgD6//b/BgD6//j/BgD6//r/BgD6//z/BgD6//7/BgD6/wAABgD6/wIABgD6/wQABgD6/wYABgD6/wgABgD6/woABgD6/wwABgD6/w4ABgD8//L/BgD8//T/BgD8//b/BgD8//j/BgD8//r/BgD8//z/BgD8//7/BgD8/wAABgD8/wIABgD8/wQABgD8/wYABgD8/wgABgD8/woABgD8/wwABgD8/w4ABgD+//L/BgD+//T/BgD+//b/BgD+//j/BgD+//r/BgD+//z/BgD+//7/BgD+/wAABgD+/wIABgD+/wQABgD+/wYABgD+/wgABgD+/woABgD+/wwABgD+/w4ABgAAAPL/BgAAAPT/BgAAAPb/BgAAAPj/BgAAAPr/BgAAAPz/BgAAAP7/BgAAAAAABgAAAAIABgAAAAQABgAAAAYABgAAAAgABgAAAAoABgAAAAwABgAAAA4ABgACAPL/BgACAPT/BgACAPb/BgACAPj/BgACAPr/BgACAPz/BgACAP7/BgACAAAABgACAAIABgACAAQABgACAAYABgACAAgABgACAAoABgACAAwABgACAA4ABgAEAPL/BgAEAPT/BgAEAPb/BgAEAPj/BgAEAPr/BgAEAPz/BgAEAP7/BgAEAAAABgAEAAIABgAEAAQABgAEAAYABgAEAAgABgAEAAoABgAEAAwABgAEAA4ABgAGAPL/BgAGAPT/BgAGAPb/BgAGAPj/BgAGAPr/BgAGAPz/BgAGAP7/BgAGAAAABgAGAAIABgAGAAQABgAGAAYABgAGAAgABgAGAAoABgAGAAwABgAGAA4ABgAIAPT/BgAIAPb/BgAIAPj/BgAIAPr/BgAIAPz/BgAIAP7/BgAIAAAABgAIAAIABgAIAAQABgAIAAYABgAIAAgABgAIAAoABgAIAAwABgAKAPT/BgAKAPb/BgAKAPj/BgAKAPr/BgAKAPz/BgAKAP7/BgAKAAAABgAKAAIABgAKAAQABgAKAAYABgAKAAgABgAKAAoABgAKAAwABgAMAPb/BgAMAPj/BgAMAPr/BgAMAPz/BgAMAP7/BgAMAAAABgAMAAIABgAMAAQABgAMAAYABgAMAAgABgAMAAoABgAOAPr/BgAOAPz/BgAOAP7/BgAOAAAABgAOAAIABgAOAAQABgAOAAYABwDx//3/BwDx////BwDx/wEABwDx/wMABwDz//n/BwDz//v/BwDz//3/BwDz////BwDz/wEABwDz/wMABwDz/wUABwDz/wcABwD1//f/BwD1//n/BwD1//v/BwD1//3/BwD1////BwD1/wEABwD1/wMABwD1/wUABwD1/wcABwD1/wkABwD3//X/BwD3//f/BwD3//n/BwD3//v/BwD3//3/BwD3////BwD3/wEABwD3/wMABwD3/wUABwD3/wcABwD3/wkABwD3/wsABwD5//P/BwD5//X/BwD5//f/BwD5//n/BwD5//v/BwD5//3/BwD5////BwD5/wEABwD5/wMABwD5/wUABwD5/wcABwD5/wkABwD5/wsABwD5/w0ABwD7//P/BwD7//X/BwD7//f/BwD7//n/BwD7//v/BwD7//3/BwD7////BwD7/wEABwD7/wMABwD7/wUABwD7/wcABwD7/wkABwD7/wsABwD7/w0ABwD9//H/BwD9//P/BwD9//X/BwD9//f/BwD9//n/BwD9//v/BwD9//3/BwD9////BwD9/wEABwD9/wMABwD9/wUABwD9/wcABwD9/wkABwD9/wsABwD9/w0ABwD9/w8ABwD///H/BwD///P/BwD///X/BwD///f/BwD///n/BwD///v/BwD///3/BwD/////BwD//wEABwD//wMABwD//wUABwD//wcABwD//wkABwD//wsABwD//w0ABwD//w8ABwABAPH/BwABAPP/BwABAPX/BwABAPf/BwABAPn/BwABAPv/BwABAP3/BwABAP//BwABAAEABwABAAMABwABAAUABwABAAcABwABAAkABwABAAsABwABAA0ABwABAA8ABwADAPH/BwADAPP/BwADAPX/BwADAPf/BwADAPn/BwADAPv/BwADAP3/BwADAP//BwADAAEABwADAAMABwADAAUABwADAAcABwADAAkABwADAAsABwADAA0ABwADAA8ABwAFAPP/BwAFAPX/BwAFAPf/BwAFAPn/BwAFAPv/BwAFAP3/BwAFAP//BwAFAAEABwAFAAMABwAFAAUABwAFAAcABwAFAAkABwAFAAsABwAFAA0ABwAHAPP/BwAHAPX/BwAHAPf/BwAHAPn/BwAHAPv/BwAHAP3/BwAHAP//BwAHAAEABwAHAAMABwAHAAUABwAHAAcABwAHAAkABwAHAAsABwAHAA0ABwAJAPX/BwAJAPf/BwAJAPn/BwAJAPv/BwAJAP3/BwAJAP//BwAJAAEABwAJAAMABwAJAAUABwAJAAcABwAJAAkABwAJAAsABwALAPf/BwALAPn/BwALAPv/BwALAP3/BwALAP//BwALAAEABwALAAMABwALAAUABwALAAcABwALAAkABwANAPn/BwANAPv/BwANAP3/BwANAP//BwANAAEABwANAAMABwANAAUABwANAAcABwAPAP3/BwAPAP//BwAPAAEABwAPAAMACADy//z/CADy//7/CADy/wAACADy/wIACADy/wQACAD0//j/CAD0//r/CAD0//z/CAD0//7/CAD0/wAACAD0/wIACAD0/wQACAD0/wYACAD0/wgACAD2//b/CAD2//j/CAD2//r/CAD2//z/CAD2//7/CAD2/wAACAD2/wIACAD2/wQACAD2/wYACAD2/wgACAD2/woACAD4//T/CAD4//b/CAD4//j/CAD4//r/CAD4//z/CAD4//7/CAD4/wAACAD4/wIACAD4/wQACAD4/wYACAD4/wgACAD4/woACAD4/wwACAD6//T/CAD6//b/CAD6//j/CAD6//r/CAD6//z/CAD6//7/CAD6/wAACAD6/wIACAD6/wQACAD6/wYACAD6/wgACAD6/woACAD6/wwACAD8//L/CAD8//T/CAD8//b/CAD8//j/CAD8//r/CAD8//z/CAD8//7/CAD8/wAACAD8/wIACAD8/wQACAD8/wYACAD8/wgACAD8/woACAD8/wwACAD8/w4ACAD+//L/CAD+//T/CAD+//b/CAD+//j/CAD+//r/CAD+//z/CAD+//7/CAD+/wAACAD+/wIACAD+/wQACAD+/wYACAD+/wgACAD+/woACAD+/wwACAD+/w4ACAAAAPL/CAAAAPT/CAAAAPb/CAAAAPj/CAAAAPr/CAAAAPz/CAAAAP7/CAAAAAAACAAAAAIACAAAAAQACAAAAAYACAAAAAgACAAAAAoACAAAAAwACAAAAA4ACAACAPL/CAACAPT/CAACAPb/CAACAPj/CAACAPr/CAACAPz/CAACAP7/CAACAAAACAACAAIACAACAAQACAACAAYACAACAAgACAACAAoACAACAAwACAACAA4ACAAEAPL/CAAEAPT/CAAEAPb/CAAEAPj/CAAEAPr/CAAEAPz/CAAEAP7/CAAEAAAACAAEAAIACAAEAAQACAAEAAYACAAEAAgACAAEAAoACAAEAAwACAAEAA4ACAAGAPT/CAAGAPb/CAAGAPj/CAAGAPr/CAAGAPz/CAAGAP7/CAAGAAAACAAGAAIACAAGAAQACAAGAAYACAAGAAgACAAGAAoACAAGAAwACAAIAPT/CAAIAPb/CAAIAPj/CAAIAPr/CAAIAPz/CAAIAP7/CAAIAAAACAAIAAIACAAIAAQACAAIAAYACAAIAAgACAAIAAoACAAIAAwACAAKAPb/CAAKAPj/CAAKAPr/CAAKAPz/CAAKAP7/CAAKAAAACAAKAAIACAAKAAQACAAKAAYACAAKAAgACAAKAAoACAAMAPj/CAAMAPr/CAAMAPz/CAAMAP7/CAAMAAAACAAMAAIACAAMAAQACAAMAAYACAAMAAgACAAOAPz/CAAOAP7/CAAOAAAACAAOAAIACAAOAAQACQDz//v/CQDz//3/CQDz////CQDz/wEACQDz/wMACQDz/wUACQD1//f/CQD1//n/CQD1//v/CQD1//3/CQD1////CQD1/wEACQD1/wMACQD1/wUACQD1/wcACQD1/wkACQD3//X/CQD3//f/CQD3//n/CQD3//v/CQD3//3/CQD3////CQD3/wEACQD3/wMACQD3/wUACQD3/wcACQD3/wkACQD3/wsACQD5//X/CQD5//f/CQD5//n/CQD5//v/CQD5//3/CQD5////CQD5/wEACQD5/wMACQD5/wUACQD5/wcACQD5/wkACQD5/wsACQD7//P/CQD7//X/CQD7//f/CQD7//n/CQD7//v/CQD7//3/CQD7////CQD7/wEACQD7/wMACQD7/wUACQD7/wcACQD7/wkACQD7/wsACQD7/w0ACQD9//P/CQD9//X/CQD9//f/CQD9//n/CQD9//v/CQD9//3/CQD9////CQD9/wEACQD9/wMACQD9/wUACQD9/wcACQD9/wkACQD9/wsACQD9/w0ACQD///P/CQD///X/CQD///f/CQD///n/CQD///v/CQD///3/CQD/////CQD//wEACQD//wMACQD//wUACQD//wcACQD//wkACQD//wsACQD//w0ACQABAPP/CQABAPX/CQABAPf/CQABAPn/CQABAPv/CQABAP3/CQABAP//CQABAAEACQABAAMACQABAAUACQABAAcACQABAAkACQABAAsACQABAA0ACQADAPP/CQADAPX/CQADAPf/CQADAPn/CQADAPv/CQADAP3/CQADAP//CQADAAEACQADAAMACQADAAUACQADAAcACQADAAkACQADAAsACQADAA0ACQAFAPP/CQAFAPX/CQAFAPf/CQAFAPn/CQAFAPv/CQAFAP3/CQAFAP//CQAFAAEACQAFAAMACQAFAAUACQAFAAcACQAFAAkACQAFAAsACQAFAA0ACQAHAPX/CQAHAPf/CQAHAPn/CQAHAPv/CQAHAP3/CQAHAP//CQAHAAEACQAHAAMACQAHAAUACQAHAAcACQAHAAkACQAHAAsACQAJAPX/CQAJAPf/CQAJAPn/CQAJAPv/CQAJAP3/CQAJAP//CQAJAAEACQAJAAMACQAJAAUACQAJAAcACQAJAAkACQAJAAsACQALAPf/CQALAPn/CQALAPv/CQALAP3/CQALAP//CQALAAEACQALAAMACQALAAUACQALAAcACQALAAkACQANAPv/CQANAP3/CQANAP//CQANAAEACQANAAMACQANAAUACgD0//r/CgD0//z/CgD0//7/CgD0/wAACgD0/wIACgD0/wQACgD0/wYACgD2//j/CgD2//r/CgD2//z/CgD2//7/CgD2/wAACgD2/wIACgD2/wQACgD2/wYACgD2/wgACgD4//b/CgD4//j/CgD4//r/CgD4//z/CgD4//7/CgD4/wAACgD4/wIACgD4/wQACgD4/wYACgD4/wgACgD4/woACgD6//T/CgD6//b/CgD6//j/CgD6//r/CgD6//z/CgD6//7/CgD6/wAACgD6/wIACgD6/wQACgD6/wYACgD6/wgACgD6/woACgD6/wwACgD8//T/CgD8//b/CgD8//j/CgD8//r/CgD8//z/CgD8//7/CgD8/wAACgD8/wIACgD8/wQACgD8/wYACgD8/wgACgD8/woACgD8/wwACgD+//T/CgD+//b/CgD+//j/CgD+//r/CgD+//z/CgD+//7/CgD+/wAACgD+/wIACgD+/wQACgD+/wYACgD+/wgACgD+/woACgD+/wwACgAAAPT/CgAAAPb/CgAAAPj/CgAAAPr/CgAAAPz/CgAAAP7/CgAAAAAACgAAAAIACgAAAAQACgAAAAYACgAAAAgACgAAAAoACgAAAAwACgACAPT/CgACAPb/CgACAPj/CgACAPr/CgACAPz/CgACAP7/CgACAAAACgACAAIACgACAAQACgACAAYACgACAAgACgACAAoACgACAAwACgAEAPT/CgAEAPb/CgAEAPj/CgAEAPr/CgAEAPz/CgAEAP7/CgAEAAAACgAEAAIACgAEAAQACgAEAAYACgAEAAgACgAEAAoACgAEAAwACgAGAPT/CgAGAPb/CgAGAPj/CgAGAPr/CgAGAPz/CgAGAP7/CgAGAAAACgAGAAIACgAGAAQACgAGAAYACgAGAAgACgAGAAoACgAGAAwACgAIAPb/CgAIAPj/CgAIAPr/CgAIAPz/CgAIAP7/CgAIAAAACgAIAAIACgAIAAQACgAIAAYACgAIAAgACgAIAAoACgAKAPj/CgAKAPr/CgAKAPz/CgAKAP7/CgAKAAAACgAKAAIACgAKAAQACgAKAAYACgAKAAgACgAMAPr/CgAMAPz/CgAMAP7/CgAMAAAACgAMAAIACgAMAAQACgAMAAYACwD1//v/CwD1//3/CwD1////CwD1/wEACwD1/wMACwD1/wUACwD3//f/CwD3//n/CwD3//v/CwD3//3/CwD3////CwD3/wEACwD3/wMACwD3/wUACwD3/wcACwD3/wkACwD5//f/CwD5//n/CwD5//v/CwD5//3/CwD5////CwD5/wEACwD5/wMACwD5/wUACwD5/wcACwD5/wkACwD7//X/CwD7//f/CwD7//n/CwD7//v/CwD7//3/CwD7////CwD7/wEACwD7/wMACwD7/wUACwD7/wcACwD7/wkACwD7/wsACwD9//X/CwD9//f/CwD9//n/CwD9//v/CwD9//3/CwD9////CwD9/wEACwD9/wMACwD9/wUACwD9/wcACwD9/wkACwD9/wsACwD///X/CwD///f/CwD///n/CwD///v/CwD///3/CwD/////CwD//wEACwD//wMACwD//wUACwD//wcACwD//wkACwD//wsACwABAPX/CwABAPf/CwABAPn/CwABAPv/CwABAP3/CwABAP//CwABAAEACwABAAMACwABAAUACwABAAcACwABAAkACwABAAsACwADAPX/CwADAPf/CwADAPn/CwADAPv/CwADAP3/CwADAP//CwADAAEACwADAAMACwADAAUACwADAAcACwADAAkACwADAAsACwAFAPX/CwAFAPf/CwAFAPn/CwAFAPv/CwAFAP3/CwAFAP//CwAFAAEACwAFAAMACwAFAAUACwAFAAcACwAFAAkACwAFAAsACwAHAPf/CwAHAPn/CwAHAPv/CwAHAP3/CwAHAP//CwAHAAEACwAHAAMACwAHAAUACwAHAAcACwAHAAkACwAJAPf/CwAJAPn/CwAJAPv/CwAJAP3/CwAJAP//CwAJAAEACwAJAAMACwAJAAUACwAJAAcACwAJAAkACwALAPv/CwALAP3/CwALAP//CwALAAEACwALAAMACwALAAUADAD2//r/DAD2//z/DAD2//7/DAD2/wAADAD2/wIADAD2/wQADAD2/wYADAD4//j/DAD4//r/DAD4//z/DAD4//7/DAD4/wAADAD4/wIADAD4/wQADAD4/wYADAD4/wgADAD6//b/DAD6//j/DAD6//r/DAD6//z/DAD6//7/DAD6/wAADAD6/wIADAD6/wQADAD6/wYADAD6/wgADAD6/woADAD8//b/DAD8//j/DAD8//r/DAD8//z/DAD8//7/DAD8/wAADAD8/wIADAD8/wQADAD8/wYADAD8/wgADAD8/woADAD+//b/DAD+//j/DAD+//r/DAD+//z/DAD+//7/DAD+/wAADAD+/wIADAD+/wQADAD+/wYADAD+/wgADAD+/woADAAAAPb/DAAAAPj/DAAAAPr/DAAAAPz/DAAAAP7/DAAAAAAADAAAAAIADAAAAAQADAAAAAYADAAAAAgADAAAAAoADAACAPb/DAACAPj/DAACAPr/DAACAPz/DAACAP7/DAACAAAADAACAAIADAACAAQADAACAAYADAACAAgADAACAAoADAAEAPb/DAAEAPj/DAAEAPr/DAAEAPz/DAAEAP7/DAAEAAAADAAEAAIADAAEAAQADAAEAAYADAAEAAgADAAEAAoADAAGAPb/DAAGAPj/DAAGAPr/DAAGAPz/DAAGAP7/DAAGAAAADAAGAAIADAAGAAQADAAGAAYADAAGAAgADAAGAAoADAAIAPj/DAAIAPr/DAAIAPz/DAAIAP7/DAAIAAAADAAIAAIADAAIAAQADAAIAAYADAAIAAgADAAKAPr/DAAKAPz/DAAKAP7/DAAKAAAADAAKAAIADAAKAAQADAAKAAYADQD3//v/DQD3//3/DQD3////DQD3/wEADQD3/wMADQD3/wUADQD5//n/DQD5//v/DQD5//3/DQD5////DQD5/wEADQD5/wMADQD5/wUADQD5/wcADQD7//f/DQD7//n/DQD7//v/DQD7//3/DQD7////DQD7/wEADQD7/wMADQD7/wUADQD7/wcADQD7/wkADQD9//f/DQD9//n/DQD9//v/DQD9//3/DQD9////DQD9/wEADQD9/wMADQD9/wUADQD9/wcADQD9/wkADQD///f/DQD///n/DQD///v/DQD///3/DQD/////DQD//wEADQD//wMADQD//wUADQD//wcADQD//wkADQABAPf/DQABAPn/DQABAPv/DQABAP3/DQABAP//DQABAAEADQABAAMADQABAAUADQABAAcADQABAAkADQADAPf/DQADAPn/DQADAPv/DQADAP3/DQADAP//DQADAAEADQADAAMADQADAAUADQADAAcADQADAAkADQAFAPf/DQAFAPn/DQAFAPv/DQAFAP3/DQAFAP//DQAFAAEADQAFAAMADQAFAAUADQAFAAcADQAFAAkADQAHAPn/DQAHAPv/DQAHAP3/DQAHAP//DQAHAAEADQAHAAMADQAHAAUADQAHAAcADQAJAPv/DQAJAP3/DQAJAP//DQAJAAEADQAJAAMADQAJAAUADgD4//z/DgD4//7/DgD4/wAADgD4/wIADgD4/wQADgD6//r/DgD6//z/DgD6//7/DgD6/wAADgD6/wIADgD6/wQADgD6/wYADgD8//j/DgD8//r/DgD8//z/DgD8//7/DgD8/wAADgD8/wIADgD8/wQADgD8/wYADgD8/wgADgD+//j/DgD+//r/DgD+//z/DgD+//7/DgD+/wAADgD+/wIADgD+/wQADgD+/wYADgD+/wgADgAAAPj/DgAAAPr/DgAAAPz/DgAAAP7/DgAAAAAADgAAAAIADgAAAAQADgAAAAYADgAAAAgADgACAPj/DgACAPr/DgACAPz/DgACAP7/DgACAAAADgACAAIADgACAAQADgACAAYADgACAAgADgAEAPj/DgAEAPr/DgAEAPz/DgAEAP7/DgAEAAAADgAEAAIADgAEAAQADgAEAAYADgAEAAgADgAGAPr/DgAGAPz/DgAGAP7/DgAGAAAADgAGAAIADgAGAAQADgAGAAYADgAIAPz/DgAIAP7/DgAIAAAADgAIAAIADgAIAAQADwD5//3/DwD5////DwD5/wEADwD5/wMADwD7//v/DwD7//3/DwD7////DwD7/wEADwD7/wMADwD7/wUADwD9//n/DwD9//v/DwD9//3/DwD9////DwD9/wEADwD9/wMADwD9/wUADwD9/wcADwD///n/DwD///v/DwD///3/DwD/////DwD//wEADwD//wMADwD//wUADwD//wcADwABAPn/DwABAPv/DwABAP3/DwABAP//DwABAAEADwABAAMADwABAAUADwABAAcADwADAPn/DwADAPv/DwADAP3/DwADAP//DwADAAEADwADAAMADwADAAUADwADAAcADwAFAPv/DwAFAP3/DwAFAP//DwAFAAEADwAFAAMADwAFAAUADwAHAP3/DwAHAP//DwAHAAEADwAHAAMAEAD8//7/EAD8/wAAEAD8/wIAEAD+//z/EAD+//7/EAD+/wAAEAD+/wIAEAD+/wQAEAAAAPz/EAAAAP7/EAAAAAAAEAAAAAIAEAAAAAQAEAACAPz/EAACAP7/EAACAAAAEAACAAIAEAACAAQAEAAEAP7/EAAEAAAAEAAEAAIA\",\"F2\":\"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0nkENvnXOjZC14I2BSDANv3oMTekgQc4tw4TOdq/1jnav9Y5tw4TOaSBBzj96DE3BSDANkLXgjb51zo20nkENjDaCjZt50Q2/oaLNlxx1jaozmk3wrt/ONq/1jk0Ypo6NGKaOtq/1jnCu384qM5pN1xx1jb+hos2bedENjDaCjYw2go2bedENv6GizZccdY2qM5pN8K7fzjav9Y5NGKaOjRimjrav9Y5wrt/OKjOaTdccdY2/oaLNm3nRDYw2go20nkENvnXOjZC14I2BSDANv3oMTekgQc4tw4TOdq/1jnav9Y5tw4TOaSBBzj96DE3BSDANkLXgjb51zo20nkENh+h8TWftyg2GDdoNooCoTYXzfQ2qM5pN6SBBzjCu384wrt/OKSBBziozmk3F830NooCoTYYN2g2n7coNh+h8TWDY9M1VKERNm3nRDZC14I2j+OuNhfN9Db96DE3qM5pN6jOaTf96DE3F830No/jrjZC14I2bedENlShETaDY9M1H6HxNQuGIDZRvU82QteCNooCoTYFIMA2XHHWNlxx1jYFIMA2igKhNkLXgjZRvU82C4YgNh+h8TWs8fw1C4YgNm3nRDYYN2g2QteCNv6Gizb+hos2QteCNhg3aDZt50Q2C4YgNqzx/DUfofE1VKERNp+3KDb51zo2bedENm3nRDb51zo2n7coNlShETYfofE1g2PTNR+h8TXSeQQ2MNoKNjDaCjbSeQQ2H6HxNYNj0zWA1xc4V54kOIg8KThXniQ4gNcXOFeeJDjRf0k4aftrONiMgjiSL4c42IyCOGn7azjRf0k4V54kONVQODiM73M4bHKcOHD0vjhugtk4vZ7jOG6C2Thw9L44bHKcOIzvczjVUDg41VA4ONiMgjgKMLc4pR36OJLyITlPyUA5uB1NOU/JQDmS8iE5pR36OAowtzjYjII41VA4OFeeJDiM73M4CjC3OI8YCjm4HU05KQ2ROcyDuTkFEMs5zIO5OSkNkTm4HU05jxgKOQowtziM73M4V54kONF/SThscpw4pR36OLgdTTnsPao5TooJOmn3RjqznmU6afdGOk6KCTrsPao5uB1NOaUd+jhscpw40X9JOIDXFzhp+2s4cPS+OJLyITkpDZE5TooJOqFBhjpvye86/oMZO2/J7zqhQYY6TooJOikNkTmS8iE5cPS+OGn7aziA1xc4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pIEHOMK7fzjCu384pIEHOKjOaTcXzfQ2igKhNhg3aDaftyg2H6HxNdX15jULhiA2GXZbNhpxlTZccdY2/egxN4O/pzekgQc4pIEHOIO/pzf96DE3XHHWNhpxlTYZdls2C4YgNtX15jWDY9M1VKERNm3nRDZC14I2j+OuNhfN9Db96DE3qM5pN6jOaTf96DE3F830No/jrjZC14I2bedENlShETaDY9M1rPH8NZ+3KDYZdls2/oaLNo/jrjZccdY2F830NhfN9DZccdY2j+OuNv6GizYZdls2n7coNqzx/DWDY9M1MNoKNq14MTYZdls2QteCNhpxlTaKAqE2igKhNhpxlTZC14I2GXZbNq14MTYw2go2g2PTNbPk3DUw2go2n7coNm3nRDYZdls2GDdoNhg3aDYZdls2bedENp+3KDYw2go2s+TcNYNj0zWs8fw1VKERNguGIDaftyg2n7coNguGIDZUoRE2rPH8NYNj0zWDY9M11fXmNR+h8TUfofE11fXmNYNj0zXDLyA41VA4ONF/SThhw0840X9JONVQODjDLyA4QekTOJTKPThp+2s41RKMOGxynDj6jKI4bHKcONUSjDhp+2s4lMo9OEHpEzhB6RM40X9JOJIvhziR4684boLZOKUd+jhzVAM5pR36OG6C2TiR4684ki+HONF/SThB6RM4lMo9OJIvhzhw9L44c1QDOTtTKzm4HU05irZaObgdTTk7Uys5c1QDOXD0vjiSL4c4lMo9OMMvIDhp+2s4keOvOHNUAzlPyUA5qpSGOew9qjnMg7k57D2qOaqUhjlPyUA5c1QDOZHjrzhp+2s4wy8gONVQODjVEow4boLZODtTKzmqlIY5BRDLOU6KCTqWNho6TooJOgUQyzmqlIY5O1MrOW6C2TjVEow41VA4ONF/SThscpw4pR36OLgdTTnsPao5TooJOmn3RjqznmU6afdGOk6KCTrsPao5uB1NOaUd+jhscpw40X9JOGHDTzj6jKI4c1QDOYq2WjnMg7k5ljYaOrOeZTqhQYY6s55lOpY2GjrMg7k5irZaOXNUAzn6jKI4YcNPONF/SThscpw4pR36OLgdTTnsPao5TooJOmn3RjqznmU6afdGOk6KCTrsPao5uB1NOaUd+jhscpw40X9JONVQODjVEow4boLZODtTKzmqlIY5BRDLOU6KCTqWNho6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\",\"U_re\":\"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dL0VO9qT9zqSpQM7gTYgOwF4PTtA0Fc7rIxqO0FjcTusjGo7QNBXOwF4PTuBNiA7kqUDO+RtBjsZ8yM7RFFCO0HIXTtBY3E7EY94O0FjcTtByF07RFFCOxnzIzvkbQY7kqUDO4E2IDsBeD07QNBXO6yMajtBY3E7rIxqO0DQVzsBeD07gTYgO5KlAzvak/c6dL0VO8n/LzsRXUc7QNBXO0HIXTtA0Fc7EV1HO8n/Lzt0vRU72pP3OjZF4DrkbQY7OJocO8n/LzsBeD07RFFCOwF4PTvJ/y87OJocO+RtBjs2ReA6izHpOuRtBjt0vRU7gTYgOxnzIzuBNiA7dL0VO+RtBjuLMek6NkXgOtqT9zqSpQM75G0GO5KlAzvak/c6NkXgOtmmvLln+Mm5CxXRuQsV0bln+Mm52aa8uX8pw7kahNi5sm7ouWL28Lli9vC5sm7ouRqE2Ll/KcO52aa8uRqE2Lli9vC56agBuiuaBrormga66agBumL28LkahNi52aa8uWf4ybmybui56agBul/PC7oUTxG6FE8Rul/PC7rpqAG6sm7ouWf4ybkLFdG5YvbwuSuaBroUTxG6IiEXuiIhF7oUTxG6K5oGumL28LkLFdG5CxXRuWL28Lkrmga6FE8RuiIhF7oiIRe6FE8RuiuaBrpi9vC5CxXRuWf4ybmybui56agBul/PC7oUTxG6FE8Rul/PC7rpqAG6sm7ouWf4ybnZpry5GoTYuWL28LnpqAG6K5oGuiuaBrrpqAG6YvbwuRqE2LnZpry5fynDuRqE2Lmybui5YvbwuWL28Lmybui5GoTYuX8pw7nZpry5Z/jJuQsV0bkLFdG5Z/jJudmmvLmhq+Q6maTyOtqT9zqZpPI6oavkOqTZ7TqSpQM74EIMO6NRDzvgQgw7kqUDO6TZ7Tqhq+Q6kqUDOxZ6Ejs4mhw7gTYgOziaHDsWehI7kqUDO6Gr5DqZpPI64EIMOziaHDt20ic7CdYrO3bSJzs4mhw74EIMO5mk8jrak/c6o1EPO4E2IDsJ1is7yf8vOwnWKzuBNiA7o1EPO9qT9zqZpPI64EIMOziaHDt20ic7CdYrO3bSJzs4mhw74EIMO5mk8jqhq+Q6kqUDOxZ6Ejs4mhw7gTYgOziaHDsWehI7kqUDO6Gr5Dqk2e06kqUDO+BCDDujUQ874EIMO5KlAzuk2e06oavkOpmk8jrak/c6maTyOqGr5DoTZ7a52aa8udmmvLkTZ7a52aa8uWf4ybkLFdG5CxXRuWf4ybnZpry5E2e2uWf4ybkahNi5vkrgub5K4LkahNi5Z/jJuRNntrnZpry5CxXRub5K4Lmybui5sm7oub5K4LkLFdG52aa8udmmvLkLFdG5vkrgubJu6Lmybui5vkrguQsV0bnZpry5E2e2uWf4ybkahNi5vkrgub5K4LkahNi5Z/jJuRNntrnZpry5Z/jJuQsV0bkLFdG5Z/jJudmmvLkTZ7a52aa8udmmvLkTZ7a5oavkOosx6Tqhq+Q6oavkOpmk8jrak/c6maTyOqGr5DqLMek62pP3Ou+o/Drak/c6izHpOqGr5DqZpPI62pP3Opmk8jqhq+Q6oavkOosx6Tqhq+Q6\",\"U_im\":\"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\",\"u0_imag\":0.0038940976084273944,\"absorptive\":true,\"n_reflections\":5028,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", - "SrTiO3 (perovskite)": "{\"name\":\"SrTiO3\",\"spacegroup\":\"Pm-3m (221)\",\"pointgroup\":\"m-3m\",\"cell\":[[3.905,0.0,0.0],[0.0,3.905,0.0],[0.0,0.0,3.905]],\"recip\":[[0.2560819462227913,0.0,0.0],[0.0,0.2560819462227913,0.0],[0.0,0.0,0.2560819462227913]],\"positions_frac\":[[0.0,0.0,0.0],[0.49999999999999994,0.49999999999999994,0.49999999999999994],[0.49999999999999994,0.49999999999999994,0.0],[0.0,0.49999999999999994,0.49999999999999994],[0.49999999999999994,0.0,0.49999999999999994]],\"numbers\":[38,22,8,8,8],\"symbols\":[\"Sr\",\"Ti\",\"O\",\"O\",\"O\"],\"colors\":[[0.0,1.0,0.0],[0.749,0.761,0.78],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051]],\"radii\":[1.95,1.6,0.66,0.66,0.66],\"hkl_i16\":\"9f/8/wAA9f/9//7/9f/9////9f/9/wAA9f/9/wEA9f/9/wIA9f/+//3/9f/+//7/9f/+////9f/+/wAA9f/+/wEA9f/+/wIA9f/+/wMA9f////3/9f////7/9f//////9f///wAA9f///wEA9f///wIA9f///wMA9f8AAPz/9f8AAP3/9f8AAP7/9f8AAP//9f8AAAAA9f8AAAEA9f8AAAIA9f8AAAMA9f8AAAQA9f8BAP3/9f8BAP7/9f8BAP//9f8BAAAA9f8BAAEA9f8BAAIA9f8BAAMA9f8CAP3/9f8CAP7/9f8CAP//9f8CAAAA9f8CAAEA9f8CAAIA9f8CAAMA9f8DAP7/9f8DAP//9f8DAAAA9f8DAAEA9f8DAAIA9f8EAAAA9v/6////9v/6/wAA9v/6/wEA9v/7//3/9v/7//7/9v/7////9v/7/wAA9v/7/wEA9v/7/wIA9v/7/wMA9v/8//z/9v/8//3/9v/8//7/9v/8////9v/8/wAA9v/8/wEA9v/8/wIA9v/8/wMA9v/8/wQA9v/9//v/9v/9//z/9v/9//3/9v/9//7/9v/9////9v/9/wAA9v/9/wEA9v/9/wIA9v/9/wMA9v/9/wQA9v/9/wUA9v/+//v/9v/+//z/9v/+//3/9v/+//7/9v/+////9v/+/wAA9v/+/wEA9v/+/wIA9v/+/wMA9v/+/wQA9v/+/wUA9v////r/9v////v/9v////z/9v////3/9v////7/9v//////9v///wAA9v///wEA9v///wIA9v///wMA9v///wQA9v///wUA9v///wYA9v8AAPr/9v8AAPv/9v8AAPz/9v8AAP3/9v8AAP7/9v8AAP//9v8AAAAA9v8AAAEA9v8AAAIA9v8AAAMA9v8AAAQA9v8AAAUA9v8AAAYA9v8BAPr/9v8BAPv/9v8BAPz/9v8BAP3/9v8BAP7/9v8BAP//9v8BAAAA9v8BAAEA9v8BAAIA9v8BAAMA9v8BAAQA9v8BAAUA9v8BAAYA9v8CAPv/9v8CAPz/9v8CAP3/9v8CAP7/9v8CAP//9v8CAAAA9v8CAAEA9v8CAAIA9v8CAAMA9v8CAAQA9v8CAAUA9v8DAPv/9v8DAPz/9v8DAP3/9v8DAP7/9v8DAP//9v8DAAAA9v8DAAEA9v8DAAIA9v8DAAMA9v8DAAQA9v8DAAUA9v8EAPz/9v8EAP3/9v8EAP7/9v8EAP//9v8EAAAA9v8EAAEA9v8EAAIA9v8EAAMA9v8EAAQA9v8FAP3/9v8FAP7/9v8FAP//9v8FAAAA9v8FAAEA9v8FAAIA9v8FAAMA9v8GAP//9v8GAAAA9v8GAAEA9//5//7/9//5////9//5/wAA9//5/wEA9//5/wIA9//6//z/9//6//3/9//6//7/9//6////9//6/wAA9//6/wEA9//6/wIA9//6/wMA9//6/wQA9//7//v/9//7//z/9//7//3/9//7//7/9//7////9//7/wAA9//7/wEA9//7/wIA9//7/wMA9//7/wQA9//7/wUA9//8//r/9//8//v/9//8//z/9//8//3/9//8//7/9//8////9//8/wAA9//8/wEA9//8/wIA9//8/wMA9//8/wQA9//8/wUA9//8/wYA9//9//r/9//9//v/9//9//z/9//9//3/9//9//7/9//9////9//9/wAA9//9/wEA9//9/wIA9//9/wMA9//9/wQA9//9/wUA9//9/wYA9//+//n/9//+//r/9//+//v/9//+//z/9//+//3/9//+//7/9//+////9//+/wAA9//+/wEA9//+/wIA9//+/wMA9//+/wQA9//+/wUA9//+/wYA9//+/wcA9/////n/9/////r/9/////v/9/////z/9/////3/9/////7/9///////9////wAA9////wEA9////wIA9////wMA9////wQA9////wUA9////wYA9////wcA9/8AAPn/9/8AAPr/9/8AAPv/9/8AAPz/9/8AAP3/9/8AAP7/9/8AAP//9/8AAAAA9/8AAAEA9/8AAAIA9/8AAAMA9/8AAAQA9/8AAAUA9/8AAAYA9/8AAAcA9/8BAPn/9/8BAPr/9/8BAPv/9/8BAPz/9/8BAP3/9/8BAP7/9/8BAP//9/8BAAAA9/8BAAEA9/8BAAIA9/8BAAMA9/8BAAQA9/8BAAUA9/8BAAYA9/8BAAcA9/8CAPn/9/8CAPr/9/8CAPv/9/8CAPz/9/8CAP3/9/8CAP7/9/8CAP//9/8CAAAA9/8CAAEA9/8CAAIA9/8CAAMA9/8CAAQA9/8CAAUA9/8CAAYA9/8CAAcA9/8DAPr/9/8DAPv/9/8DAPz/9/8DAP3/9/8DAP7/9/8DAP//9/8DAAAA9/8DAAEA9/8DAAIA9/8DAAMA9/8DAAQA9/8DAAUA9/8DAAYA9/8EAPr/9/8EAPv/9/8EAPz/9/8EAP3/9/8EAP7/9/8EAP//9/8EAAAA9/8EAAEA9/8EAAIA9/8EAAMA9/8EAAQA9/8EAAUA9/8EAAYA9/8FAPv/9/8FAPz/9/8FAP3/9/8FAP7/9/8FAP//9/8FAAAA9/8FAAEA9/8FAAIA9/8FAAMA9/8FAAQA9/8FAAUA9/8GAPz/9/8GAP3/9/8GAP7/9/8GAP//9/8GAAAA9/8GAAEA9/8GAAIA9/8GAAMA9/8GAAQA9/8HAP7/9/8HAP//9/8HAAAA9/8HAAEA9/8HAAIA+P/4//3/+P/4//7/+P/4////+P/4/wAA+P/4/wEA+P/4/wIA+P/4/wMA+P/5//z/+P/5//3/+P/5//7/+P/5////+P/5/wAA+P/5/wEA+P/5/wIA+P/5/wMA+P/5/wQA+P/6//r/+P/6//v/+P/6//z/+P/6//3/+P/6//7/+P/6////+P/6/wAA+P/6/wEA+P/6/wIA+P/6/wMA+P/6/wQA+P/6/wUA+P/6/wYA+P/7//r/+P/7//v/+P/7//z/+P/7//3/+P/7//7/+P/7////+P/7/wAA+P/7/wEA+P/7/wIA+P/7/wMA+P/7/wQA+P/7/wUA+P/7/wYA+P/8//n/+P/8//r/+P/8//v/+P/8//z/+P/8//3/+P/8//7/+P/8////+P/8/wAA+P/8/wEA+P/8/wIA+P/8/wMA+P/8/wQA+P/8/wUA+P/8/wYA+P/8/wcA+P/9//j/+P/9//n/+P/9//r/+P/9//v/+P/9//z/+P/9//3/+P/9//7/+P/9////+P/9/wAA+P/9/wEA+P/9/wIA+P/9/wMA+P/9/wQA+P/9/wUA+P/9/wYA+P/9/wcA+P/9/wgA+P/+//j/+P/+//n/+P/+//r/+P/+//v/+P/+//z/+P/+//3/+P/+//7/+P/+////+P/+/wAA+P/+/wEA+P/+/wIA+P/+/wMA+P/+/wQA+P/+/wUA+P/+/wYA+P/+/wcA+P/+/wgA+P////j/+P////n/+P////r/+P////v/+P////z/+P////3/+P////7/+P//////+P///wAA+P///wEA+P///wIA+P///wMA+P///wQA+P///wUA+P///wYA+P///wcA+P///wgA+P8AAPj/+P8AAPn/+P8AAPr/+P8AAPv/+P8AAPz/+P8AAP3/+P8AAP7/+P8AAP//+P8AAAAA+P8AAAEA+P8AAAIA+P8AAAMA+P8AAAQA+P8AAAUA+P8AAAYA+P8AAAcA+P8AAAgA+P8BAPj/+P8BAPn/+P8BAPr/+P8BAPv/+P8BAPz/+P8BAP3/+P8BAP7/+P8BAP//+P8BAAAA+P8BAAEA+P8BAAIA+P8BAAMA+P8BAAQA+P8BAAUA+P8BAAYA+P8BAAcA+P8BAAgA+P8CAPj/+P8CAPn/+P8CAPr/+P8CAPv/+P8CAPz/+P8CAP3/+P8CAP7/+P8CAP//+P8CAAAA+P8CAAEA+P8CAAIA+P8CAAMA+P8CAAQA+P8CAAUA+P8CAAYA+P8CAAcA+P8CAAgA+P8DAPj/+P8DAPn/+P8DAPr/+P8DAPv/+P8DAPz/+P8DAP3/+P8DAP7/+P8DAP//+P8DAAAA+P8DAAEA+P8DAAIA+P8DAAMA+P8DAAQA+P8DAAUA+P8DAAYA+P8DAAcA+P8DAAgA+P8EAPn/+P8EAPr/+P8EAPv/+P8EAPz/+P8EAP3/+P8EAP7/+P8EAP//+P8EAAAA+P8EAAEA+P8EAAIA+P8EAAMA+P8EAAQA+P8EAAUA+P8EAAYA+P8EAAcA+P8FAPr/+P8FAPv/+P8FAPz/+P8FAP3/+P8FAP7/+P8FAP//+P8FAAAA+P8FAAEA+P8FAAIA+P8FAAMA+P8FAAQA+P8FAAUA+P8FAAYA+P8GAPr/+P8GAPv/+P8GAPz/+P8GAP3/+P8GAP7/+P8GAP//+P8GAAAA+P8GAAEA+P8GAAIA+P8GAAMA+P8GAAQA+P8GAAUA+P8GAAYA+P8HAPz/+P8HAP3/+P8HAP7/+P8HAP//+P8HAAAA+P8HAAEA+P8HAAIA+P8HAAMA+P8HAAQA+P8IAP3/+P8IAP7/+P8IAP//+P8IAAAA+P8IAAEA+P8IAAIA+P8IAAMA+f/3//7/+f/3////+f/3/wAA+f/3/wEA+f/3/wIA+f/4//z/+f/4//3/+f/4//7/+f/4////+f/4/wAA+f/4/wEA+f/4/wIA+f/4/wMA+f/4/wQA+f/5//r/+f/5//v/+f/5//z/+f/5//3/+f/5//7/+f/5////+f/5/wAA+f/5/wEA+f/5/wIA+f/5/wMA+f/5/wQA+f/5/wUA+f/5/wYA+f/6//n/+f/6//r/+f/6//v/+f/6//z/+f/6//3/+f/6//7/+f/6////+f/6/wAA+f/6/wEA+f/6/wIA+f/6/wMA+f/6/wQA+f/6/wUA+f/6/wYA+f/6/wcA+f/7//n/+f/7//r/+f/7//v/+f/7//z/+f/7//3/+f/7//7/+f/7////+f/7/wAA+f/7/wEA+f/7/wIA+f/7/wMA+f/7/wQA+f/7/wUA+f/7/wYA+f/7/wcA+f/8//j/+f/8//n/+f/8//r/+f/8//v/+f/8//z/+f/8//3/+f/8//7/+f/8////+f/8/wAA+f/8/wEA+f/8/wIA+f/8/wMA+f/8/wQA+f/8/wUA+f/8/wYA+f/8/wcA+f/8/wgA+f/9//j/+f/9//n/+f/9//r/+f/9//v/+f/9//z/+f/9//3/+f/9//7/+f/9////+f/9/wAA+f/9/wEA+f/9/wIA+f/9/wMA+f/9/wQA+f/9/wUA+f/9/wYA+f/9/wcA+f/9/wgA+f/+//f/+f/+//j/+f/+//n/+f/+//r/+f/+//v/+f/+//z/+f/+//3/+f/+//7/+f/+////+f/+/wAA+f/+/wEA+f/+/wIA+f/+/wMA+f/+/wQA+f/+/wUA+f/+/wYA+f/+/wcA+f/+/wgA+f/+/wkA+f////f/+f////j/+f////n/+f////r/+f////v/+f////z/+f////3/+f////7/+f//////+f///wAA+f///wEA+f///wIA+f///wMA+f///wQA+f///wUA+f///wYA+f///wcA+f///wgA+f///wkA+f8AAPf/+f8AAPj/+f8AAPn/+f8AAPr/+f8AAPv/+f8AAPz/+f8AAP3/+f8AAP7/+f8AAP//+f8AAAAA+f8AAAEA+f8AAAIA+f8AAAMA+f8AAAQA+f8AAAUA+f8AAAYA+f8AAAcA+f8AAAgA+f8AAAkA+f8BAPf/+f8BAPj/+f8BAPn/+f8BAPr/+f8BAPv/+f8BAPz/+f8BAP3/+f8BAP7/+f8BAP//+f8BAAAA+f8BAAEA+f8BAAIA+f8BAAMA+f8BAAQA+f8BAAUA+f8BAAYA+f8BAAcA+f8BAAgA+f8BAAkA+f8CAPf/+f8CAPj/+f8CAPn/+f8CAPr/+f8CAPv/+f8CAPz/+f8CAP3/+f8CAP7/+f8CAP//+f8CAAAA+f8CAAEA+f8CAAIA+f8CAAMA+f8CAAQA+f8CAAUA+f8CAAYA+f8CAAcA+f8CAAgA+f8CAAkA+f8DAPj/+f8DAPn/+f8DAPr/+f8DAPv/+f8DAPz/+f8DAP3/+f8DAP7/+f8DAP//+f8DAAAA+f8DAAEA+f8DAAIA+f8DAAMA+f8DAAQA+f8DAAUA+f8DAAYA+f8DAAcA+f8DAAgA+f8EAPj/+f8EAPn/+f8EAPr/+f8EAPv/+f8EAPz/+f8EAP3/+f8EAP7/+f8EAP//+f8EAAAA+f8EAAEA+f8EAAIA+f8EAAMA+f8EAAQA+f8EAAUA+f8EAAYA+f8EAAcA+f8EAAgA+f8FAPn/+f8FAPr/+f8FAPv/+f8FAPz/+f8FAP3/+f8FAP7/+f8FAP//+f8FAAAA+f8FAAEA+f8FAAIA+f8FAAMA+f8FAAQA+f8FAAUA+f8FAAYA+f8FAAcA+f8GAPn/+f8GAPr/+f8GAPv/+f8GAPz/+f8GAP3/+f8GAP7/+f8GAP//+f8GAAAA+f8GAAEA+f8GAAIA+f8GAAMA+f8GAAQA+f8GAAUA+f8GAAYA+f8GAAcA+f8HAPr/+f8HAPv/+f8HAPz/+f8HAP3/+f8HAP7/+f8HAP//+f8HAAAA+f8HAAEA+f8HAAIA+f8HAAMA+f8HAAQA+f8HAAUA+f8HAAYA+f8IAPz/+f8IAP3/+f8IAP7/+f8IAP//+f8IAAAA+f8IAAEA+f8IAAIA+f8IAAMA+f8IAAQA+f8JAP7/+f8JAP//+f8JAAAA+f8JAAEA+f8JAAIA+v/2////+v/2/wAA+v/2/wEA+v/3//z/+v/3//3/+v/3//7/+v/3////+v/3/wAA+v/3/wEA+v/3/wIA+v/3/wMA+v/3/wQA+v/4//r/+v/4//v/+v/4//z/+v/4//3/+v/4//7/+v/4////+v/4/wAA+v/4/wEA+v/4/wIA+v/4/wMA+v/4/wQA+v/4/wUA+v/4/wYA+v/5//n/+v/5//r/+v/5//v/+v/5//z/+v/5//3/+v/5//7/+v/5////+v/5/wAA+v/5/wEA+v/5/wIA+v/5/wMA+v/5/wQA+v/5/wUA+v/5/wYA+v/5/wcA+v/6//j/+v/6//n/+v/6//r/+v/6//v/+v/6//z/+v/6//3/+v/6//7/+v/6////+v/6/wAA+v/6/wEA+v/6/wIA+v/6/wMA+v/6/wQA+v/6/wUA+v/6/wYA+v/6/wcA+v/6/wgA+v/7//j/+v/7//n/+v/7//r/+v/7//v/+v/7//z/+v/7//3/+v/7//7/+v/7////+v/7/wAA+v/7/wEA+v/7/wIA+v/7/wMA+v/7/wQA+v/7/wUA+v/7/wYA+v/7/wcA+v/7/wgA+v/8//f/+v/8//j/+v/8//n/+v/8//r/+v/8//v/+v/8//z/+v/8//3/+v/8//7/+v/8////+v/8/wAA+v/8/wEA+v/8/wIA+v/8/wMA+v/8/wQA+v/8/wUA+v/8/wYA+v/8/wcA+v/8/wgA+v/8/wkA+v/9//f/+v/9//j/+v/9//n/+v/9//r/+v/9//v/+v/9//z/+v/9//3/+v/9//7/+v/9////+v/9/wAA+v/9/wEA+v/9/wIA+v/9/wMA+v/9/wQA+v/9/wUA+v/9/wYA+v/9/wcA+v/9/wgA+v/9/wkA+v/+//f/+v/+//j/+v/+//n/+v/+//r/+v/+//v/+v/+//z/+v/+//3/+v/+//7/+v/+////+v/+/wAA+v/+/wEA+v/+/wIA+v/+/wMA+v/+/wQA+v/+/wUA+v/+/wYA+v/+/wcA+v/+/wgA+v/+/wkA+v////b/+v////f/+v////j/+v////n/+v////r/+v////v/+v////z/+v////3/+v////7/+v//////+v///wAA+v///wEA+v///wIA+v///wMA+v///wQA+v///wUA+v///wYA+v///wcA+v///wgA+v///wkA+v///woA+v8AAPb/+v8AAPf/+v8AAPj/+v8AAPn/+v8AAPr/+v8AAPv/+v8AAPz/+v8AAP3/+v8AAP7/+v8AAP//+v8AAAAA+v8AAAEA+v8AAAIA+v8AAAMA+v8AAAQA+v8AAAUA+v8AAAYA+v8AAAcA+v8AAAgA+v8AAAkA+v8AAAoA+v8BAPb/+v8BAPf/+v8BAPj/+v8BAPn/+v8BAPr/+v8BAPv/+v8BAPz/+v8BAP3/+v8BAP7/+v8BAP//+v8BAAAA+v8BAAEA+v8BAAIA+v8BAAMA+v8BAAQA+v8BAAUA+v8BAAYA+v8BAAcA+v8BAAgA+v8BAAkA+v8BAAoA+v8CAPf/+v8CAPj/+v8CAPn/+v8CAPr/+v8CAPv/+v8CAPz/+v8CAP3/+v8CAP7/+v8CAP//+v8CAAAA+v8CAAEA+v8CAAIA+v8CAAMA+v8CAAQA+v8CAAUA+v8CAAYA+v8CAAcA+v8CAAgA+v8CAAkA+v8DAPf/+v8DAPj/+v8DAPn/+v8DAPr/+v8DAPv/+v8DAPz/+v8DAP3/+v8DAP7/+v8DAP//+v8DAAAA+v8DAAEA+v8DAAIA+v8DAAMA+v8DAAQA+v8DAAUA+v8DAAYA+v8DAAcA+v8DAAgA+v8DAAkA+v8EAPf/+v8EAPj/+v8EAPn/+v8EAPr/+v8EAPv/+v8EAPz/+v8EAP3/+v8EAP7/+v8EAP//+v8EAAAA+v8EAAEA+v8EAAIA+v8EAAMA+v8EAAQA+v8EAAUA+v8EAAYA+v8EAAcA+v8EAAgA+v8EAAkA+v8FAPj/+v8FAPn/+v8FAPr/+v8FAPv/+v8FAPz/+v8FAP3/+v8FAP7/+v8FAP//+v8FAAAA+v8FAAEA+v8FAAIA+v8FAAMA+v8FAAQA+v8FAAUA+v8FAAYA+v8FAAcA+v8FAAgA+v8GAPj/+v8GAPn/+v8GAPr/+v8GAPv/+v8GAPz/+v8GAP3/+v8GAP7/+v8GAP//+v8GAAAA+v8GAAEA+v8GAAIA+v8GAAMA+v8GAAQA+v8GAAUA+v8GAAYA+v8GAAcA+v8GAAgA+v8HAPn/+v8HAPr/+v8HAPv/+v8HAPz/+v8HAP3/+v8HAP7/+v8HAP//+v8HAAAA+v8HAAEA+v8HAAIA+v8HAAMA+v8HAAQA+v8HAAUA+v8HAAYA+v8HAAcA+v8IAPr/+v8IAPv/+v8IAPz/+v8IAP3/+v8IAP7/+v8IAP//+v8IAAAA+v8IAAEA+v8IAAIA+v8IAAMA+v8IAAQA+v8IAAUA+v8IAAYA+v8JAPz/+v8JAP3/+v8JAP7/+v8JAP//+v8JAAAA+v8JAAEA+v8JAAIA+v8JAAMA+v8JAAQA+v8KAP//+v8KAAAA+v8KAAEA+//2//3/+//2//7/+//2////+//2/wAA+//2/wEA+//2/wIA+//2/wMA+//3//v/+//3//z/+//3//3/+//3//7/+//3////+//3/wAA+//3/wEA+//3/wIA+//3/wMA+//3/wQA+//3/wUA+//4//r/+//4//v/+//4//z/+//4//3/+//4//7/+//4////+//4/wAA+//4/wEA+//4/wIA+//4/wMA+//4/wQA+//4/wUA+//4/wYA+//5//n/+//5//r/+//5//v/+//5//z/+//5//3/+//5//7/+//5////+//5/wAA+//5/wEA+//5/wIA+//5/wMA+//5/wQA+//5/wUA+//5/wYA+//5/wcA+//6//j/+//6//n/+//6//r/+//6//v/+//6//z/+//6//3/+//6//7/+//6////+//6/wAA+//6/wEA+//6/wIA+//6/wMA+//6/wQA+//6/wUA+//6/wYA+//6/wcA+//6/wgA+//7//f/+//7//j/+//7//n/+//7//r/+//7//v/+//7//z/+//7//3/+//7//7/+//7////+//7/wAA+//7/wEA+//7/wIA+//7/wMA+//7/wQA+//7/wUA+//7/wYA+//7/wcA+//7/wgA+//7/wkA+//8//f/+//8//j/+//8//n/+//8//r/+//8//v/+//8//z/+//8//3/+//8//7/+//8////+//8/wAA+//8/wEA+//8/wIA+//8/wMA+//8/wQA+//8/wUA+//8/wYA+//8/wcA+//8/wgA+//8/wkA+//9//b/+//9//f/+//9//j/+//9//n/+//9//r/+//9//v/+//9//z/+//9//3/+//9//7/+//9////+//9/wAA+//9/wEA+//9/wIA+//9/wMA+//9/wQA+//9/wUA+//9/wYA+//9/wcA+//9/wgA+//9/wkA+//9/woA+//+//b/+//+//f/+//+//j/+//+//n/+//+//r/+//+//v/+//+//z/+//+//3/+//+//7/+//+////+//+/wAA+//+/wEA+//+/wIA+//+/wMA+//+/wQA+//+/wUA+//+/wYA+//+/wcA+//+/wgA+//+/wkA+//+/woA+/////b/+/////f/+/////j/+/////n/+/////r/+/////v/+/////z/+/////3/+/////7/+///////+////wAA+////wEA+////wIA+////wMA+////wQA+////wUA+////wYA+////wcA+////wgA+////wkA+////woA+/8AAPb/+/8AAPf/+/8AAPj/+/8AAPn/+/8AAPr/+/8AAPv/+/8AAPz/+/8AAP3/+/8AAP7/+/8AAP//+/8AAAAA+/8AAAEA+/8AAAIA+/8AAAMA+/8AAAQA+/8AAAUA+/8AAAYA+/8AAAcA+/8AAAgA+/8AAAkA+/8AAAoA+/8BAPb/+/8BAPf/+/8BAPj/+/8BAPn/+/8BAPr/+/8BAPv/+/8BAPz/+/8BAP3/+/8BAP7/+/8BAP//+/8BAAAA+/8BAAEA+/8BAAIA+/8BAAMA+/8BAAQA+/8BAAUA+/8BAAYA+/8BAAcA+/8BAAgA+/8BAAkA+/8BAAoA+/8CAPb/+/8CAPf/+/8CAPj/+/8CAPn/+/8CAPr/+/8CAPv/+/8CAPz/+/8CAP3/+/8CAP7/+/8CAP//+/8CAAAA+/8CAAEA+/8CAAIA+/8CAAMA+/8CAAQA+/8CAAUA+/8CAAYA+/8CAAcA+/8CAAgA+/8CAAkA+/8CAAoA+/8DAPb/+/8DAPf/+/8DAPj/+/8DAPn/+/8DAPr/+/8DAPv/+/8DAPz/+/8DAP3/+/8DAP7/+/8DAP//+/8DAAAA+/8DAAEA+/8DAAIA+/8DAAMA+/8DAAQA+/8DAAUA+/8DAAYA+/8DAAcA+/8DAAgA+/8DAAkA+/8DAAoA+/8EAPf/+/8EAPj/+/8EAPn/+/8EAPr/+/8EAPv/+/8EAPz/+/8EAP3/+/8EAP7/+/8EAP//+/8EAAAA+/8EAAEA+/8EAAIA+/8EAAMA+/8EAAQA+/8EAAUA+/8EAAYA+/8EAAcA+/8EAAgA+/8EAAkA+/8FAPf/+/8FAPj/+/8FAPn/+/8FAPr/+/8FAPv/+/8FAPz/+/8FAP3/+/8FAP7/+/8FAP//+/8FAAAA+/8FAAEA+/8FAAIA+/8FAAMA+/8FAAQA+/8FAAUA+/8FAAYA+/8FAAcA+/8FAAgA+/8FAAkA+/8GAPj/+/8GAPn/+/8GAPr/+/8GAPv/+/8GAPz/+/8GAP3/+/8GAP7/+/8GAP//+/8GAAAA+/8GAAEA+/8GAAIA+/8GAAMA+/8GAAQA+/8GAAUA+/8GAAYA+/8GAAcA+/8GAAgA+/8HAPn/+/8HAPr/+/8HAPv/+/8HAPz/+/8HAP3/+/8HAP7/+/8HAP//+/8HAAAA+/8HAAEA+/8HAAIA+/8HAAMA+/8HAAQA+/8HAAUA+/8HAAYA+/8HAAcA+/8IAPr/+/8IAPv/+/8IAPz/+/8IAP3/+/8IAP7/+/8IAP//+/8IAAAA+/8IAAEA+/8IAAIA+/8IAAMA+/8IAAQA+/8IAAUA+/8IAAYA+/8JAPv/+/8JAPz/+/8JAP3/+/8JAP7/+/8JAP//+/8JAAAA+/8JAAEA+/8JAAIA+/8JAAMA+/8JAAQA+/8JAAUA+/8KAP3/+/8KAP7/+/8KAP//+/8KAAAA+/8KAAEA+/8KAAIA+/8KAAMA/P/1/wAA/P/2//z//P/2//3//P/2//7//P/2/////P/2/wAA/P/2/wEA/P/2/wIA/P/2/wMA/P/2/wQA/P/3//r//P/3//v//P/3//z//P/3//3//P/3//7//P/3/////P/3/wAA/P/3/wEA/P/3/wIA/P/3/wMA/P/3/wQA/P/3/wUA/P/3/wYA/P/4//n//P/4//r//P/4//v//P/4//z//P/4//3//P/4//7//P/4/////P/4/wAA/P/4/wEA/P/4/wIA/P/4/wMA/P/4/wQA/P/4/wUA/P/4/wYA/P/4/wcA/P/5//j//P/5//n//P/5//r//P/5//v//P/5//z//P/5//3//P/5//7//P/5/////P/5/wAA/P/5/wEA/P/5/wIA/P/5/wMA/P/5/wQA/P/5/wUA/P/5/wYA/P/5/wcA/P/5/wgA/P/6//f//P/6//j//P/6//n//P/6//r//P/6//v//P/6//z//P/6//3//P/6//7//P/6/////P/6/wAA/P/6/wEA/P/6/wIA/P/6/wMA/P/6/wQA/P/6/wUA/P/6/wYA/P/6/wcA/P/6/wgA/P/6/wkA/P/7//f//P/7//j//P/7//n//P/7//r//P/7//v//P/7//z//P/7//3//P/7//7//P/7/////P/7/wAA/P/7/wEA/P/7/wIA/P/7/wMA/P/7/wQA/P/7/wUA/P/7/wYA/P/7/wcA/P/7/wgA/P/7/wkA/P/8//b//P/8//f//P/8//j//P/8//n//P/8//r//P/8//v//P/8//z//P/8//3//P/8//7//P/8/////P/8/wAA/P/8/wEA/P/8/wIA/P/8/wMA/P/8/wQA/P/8/wUA/P/8/wYA/P/8/wcA/P/8/wgA/P/8/wkA/P/8/woA/P/9//b//P/9//f//P/9//j//P/9//n//P/9//r//P/9//v//P/9//z//P/9//3//P/9//7//P/9/////P/9/wAA/P/9/wEA/P/9/wIA/P/9/wMA/P/9/wQA/P/9/wUA/P/9/wYA/P/9/wcA/P/9/wgA/P/9/wkA/P/9/woA/P/+//b//P/+//f//P/+//j//P/+//n//P/+//r//P/+//v//P/+//z//P/+//3//P/+//7//P/+/////P/+/wAA/P/+/wEA/P/+/wIA/P/+/wMA/P/+/wQA/P/+/wUA/P/+/wYA/P/+/wcA/P/+/wgA/P/+/wkA/P/+/woA/P////b//P////f//P////j//P////n//P////r//P////v//P////z//P////3//P////7//P///////P///wAA/P///wEA/P///wIA/P///wMA/P///wQA/P///wUA/P///wYA/P///wcA/P///wgA/P///wkA/P///woA/P8AAPX//P8AAPb//P8AAPf//P8AAPj//P8AAPn//P8AAPr//P8AAPv//P8AAPz//P8AAP3//P8AAP7//P8AAP///P8AAAAA/P8AAAEA/P8AAAIA/P8AAAMA/P8AAAQA/P8AAAUA/P8AAAYA/P8AAAcA/P8AAAgA/P8AAAkA/P8AAAoA/P8AAAsA/P8BAPb//P8BAPf//P8BAPj//P8BAPn//P8BAPr//P8BAPv//P8BAPz//P8BAP3//P8BAP7//P8BAP///P8BAAAA/P8BAAEA/P8BAAIA/P8BAAMA/P8BAAQA/P8BAAUA/P8BAAYA/P8BAAcA/P8BAAgA/P8BAAkA/P8BAAoA/P8CAPb//P8CAPf//P8CAPj//P8CAPn//P8CAPr//P8CAPv//P8CAPz//P8CAP3//P8CAP7//P8CAP///P8CAAAA/P8CAAEA/P8CAAIA/P8CAAMA/P8CAAQA/P8CAAUA/P8CAAYA/P8CAAcA/P8CAAgA/P8CAAkA/P8CAAoA/P8DAPb//P8DAPf//P8DAPj//P8DAPn//P8DAPr//P8DAPv//P8DAPz//P8DAP3//P8DAP7//P8DAP///P8DAAAA/P8DAAEA/P8DAAIA/P8DAAMA/P8DAAQA/P8DAAUA/P8DAAYA/P8DAAcA/P8DAAgA/P8DAAkA/P8DAAoA/P8EAPb//P8EAPf//P8EAPj//P8EAPn//P8EAPr//P8EAPv//P8EAPz//P8EAP3//P8EAP7//P8EAP///P8EAAAA/P8EAAEA/P8EAAIA/P8EAAMA/P8EAAQA/P8EAAUA/P8EAAYA/P8EAAcA/P8EAAgA/P8EAAkA/P8EAAoA/P8FAPf//P8FAPj//P8FAPn//P8FAPr//P8FAPv//P8FAPz//P8FAP3//P8FAP7//P8FAP///P8FAAAA/P8FAAEA/P8FAAIA/P8FAAMA/P8FAAQA/P8FAAUA/P8FAAYA/P8FAAcA/P8FAAgA/P8FAAkA/P8GAPf//P8GAPj//P8GAPn//P8GAPr//P8GAPv//P8GAPz//P8GAP3//P8GAP7//P8GAP///P8GAAAA/P8GAAEA/P8GAAIA/P8GAAMA/P8GAAQA/P8GAAUA/P8GAAYA/P8GAAcA/P8GAAgA/P8GAAkA/P8HAPj//P8HAPn//P8HAPr//P8HAPv//P8HAPz//P8HAP3//P8HAP7//P8HAP///P8HAAAA/P8HAAEA/P8HAAIA/P8HAAMA/P8HAAQA/P8HAAUA/P8HAAYA/P8HAAcA/P8HAAgA/P8IAPn//P8IAPr//P8IAPv//P8IAPz//P8IAP3//P8IAP7//P8IAP///P8IAAAA/P8IAAEA/P8IAAIA/P8IAAMA/P8IAAQA/P8IAAUA/P8IAAYA/P8IAAcA/P8JAPr//P8JAPv//P8JAPz//P8JAP3//P8JAP7//P8JAP///P8JAAAA/P8JAAEA/P8JAAIA/P8JAAMA/P8JAAQA/P8JAAUA/P8JAAYA/P8KAPz//P8KAP3//P8KAP7//P8KAP///P8KAAAA/P8KAAEA/P8KAAIA/P8KAAMA/P8KAAQA/P8LAAAA/f/1//7//f/1/////f/1/wAA/f/1/wEA/f/1/wIA/f/2//v//f/2//z//f/2//3//f/2//7//f/2/////f/2/wAA/f/2/wEA/f/2/wIA/f/2/wMA/f/2/wQA/f/2/wUA/f/3//r//f/3//v//f/3//z//f/3//3//f/3//7//f/3/////f/3/wAA/f/3/wEA/f/3/wIA/f/3/wMA/f/3/wQA/f/3/wUA/f/3/wYA/f/4//j//f/4//n//f/4//r//f/4//v//f/4//z//f/4//3//f/4//7//f/4/////f/4/wAA/f/4/wEA/f/4/wIA/f/4/wMA/f/4/wQA/f/4/wUA/f/4/wYA/f/4/wcA/f/4/wgA/f/5//j//f/5//n//f/5//r//f/5//v//f/5//z//f/5//3//f/5//7//f/5/////f/5/wAA/f/5/wEA/f/5/wIA/f/5/wMA/f/5/wQA/f/5/wUA/f/5/wYA/f/5/wcA/f/5/wgA/f/6//f//f/6//j//f/6//n//f/6//r//f/6//v//f/6//z//f/6//3//f/6//7//f/6/////f/6/wAA/f/6/wEA/f/6/wIA/f/6/wMA/f/6/wQA/f/6/wUA/f/6/wYA/f/6/wcA/f/6/wgA/f/6/wkA/f/7//b//f/7//f//f/7//j//f/7//n//f/7//r//f/7//v//f/7//z//f/7//3//f/7//7//f/7/////f/7/wAA/f/7/wEA/f/7/wIA/f/7/wMA/f/7/wQA/f/7/wUA/f/7/wYA/f/7/wcA/f/7/wgA/f/7/wkA/f/7/woA/f/8//b//f/8//f//f/8//j//f/8//n//f/8//r//f/8//v//f/8//z//f/8//3//f/8//7//f/8/////f/8/wAA/f/8/wEA/f/8/wIA/f/8/wMA/f/8/wQA/f/8/wUA/f/8/wYA/f/8/wcA/f/8/wgA/f/8/wkA/f/8/woA/f/9//b//f/9//f//f/9//j//f/9//n//f/9//r//f/9//v//f/9//z//f/9//3//f/9//7//f/9/////f/9/wAA/f/9/wEA/f/9/wIA/f/9/wMA/f/9/wQA/f/9/wUA/f/9/wYA/f/9/wcA/f/9/wgA/f/9/wkA/f/9/woA/f/+//X//f/+//b//f/+//f//f/+//j//f/+//n//f/+//r//f/+//v//f/+//z//f/+//3//f/+//7//f/+/////f/+/wAA/f/+/wEA/f/+/wIA/f/+/wMA/f/+/wQA/f/+/wUA/f/+/wYA/f/+/wcA/f/+/wgA/f/+/wkA/f/+/woA/f/+/wsA/f////X//f////b//f////f//f////j//f////n//f////r//f////v//f////z//f////3//f////7//f///////f///wAA/f///wEA/f///wIA/f///wMA/f///wQA/f///wUA/f///wYA/f///wcA/f///wgA/f///wkA/f///woA/f///wsA/f8AAPX//f8AAPb//f8AAPf//f8AAPj//f8AAPn//f8AAPr//f8AAPv//f8AAPz//f8AAP3//f8AAP7//f8AAP///f8AAAAA/f8AAAEA/f8AAAIA/f8AAAMA/f8AAAQA/f8AAAUA/f8AAAYA/f8AAAcA/f8AAAgA/f8AAAkA/f8AAAoA/f8AAAsA/f8BAPX//f8BAPb//f8BAPf//f8BAPj//f8BAPn//f8BAPr//f8BAPv//f8BAPz//f8BAP3//f8BAP7//f8BAP///f8BAAAA/f8BAAEA/f8BAAIA/f8BAAMA/f8BAAQA/f8BAAUA/f8BAAYA/f8BAAcA/f8BAAgA/f8BAAkA/f8BAAoA/f8BAAsA/f8CAPX//f8CAPb//f8CAPf//f8CAPj//f8CAPn//f8CAPr//f8CAPv//f8CAPz//f8CAP3//f8CAP7//f8CAP///f8CAAAA/f8CAAEA/f8CAAIA/f8CAAMA/f8CAAQA/f8CAAUA/f8CAAYA/f8CAAcA/f8CAAgA/f8CAAkA/f8CAAoA/f8CAAsA/f8DAPb//f8DAPf//f8DAPj//f8DAPn//f8DAPr//f8DAPv//f8DAPz//f8DAP3//f8DAP7//f8DAP///f8DAAAA/f8DAAEA/f8DAAIA/f8DAAMA/f8DAAQA/f8DAAUA/f8DAAYA/f8DAAcA/f8DAAgA/f8DAAkA/f8DAAoA/f8EAPb//f8EAPf//f8EAPj//f8EAPn//f8EAPr//f8EAPv//f8EAPz//f8EAP3//f8EAP7//f8EAP///f8EAAAA/f8EAAEA/f8EAAIA/f8EAAMA/f8EAAQA/f8EAAUA/f8EAAYA/f8EAAcA/f8EAAgA/f8EAAkA/f8EAAoA/f8FAPb//f8FAPf//f8FAPj//f8FAPn//f8FAPr//f8FAPv//f8FAPz//f8FAP3//f8FAP7//f8FAP///f8FAAAA/f8FAAEA/f8FAAIA/f8FAAMA/f8FAAQA/f8FAAUA/f8FAAYA/f8FAAcA/f8FAAgA/f8FAAkA/f8FAAoA/f8GAPf//f8GAPj//f8GAPn//f8GAPr//f8GAPv//f8GAPz//f8GAP3//f8GAP7//f8GAP///f8GAAAA/f8GAAEA/f8GAAIA/f8GAAMA/f8GAAQA/f8GAAUA/f8GAAYA/f8GAAcA/f8GAAgA/f8GAAkA/f8HAPj//f8HAPn//f8HAPr//f8HAPv//f8HAPz//f8HAP3//f8HAP7//f8HAP///f8HAAAA/f8HAAEA/f8HAAIA/f8HAAMA/f8HAAQA/f8HAAUA/f8HAAYA/f8HAAcA/f8HAAgA/f8IAPj//f8IAPn//f8IAPr//f8IAPv//f8IAPz//f8IAP3//f8IAP7//f8IAP///f8IAAAA/f8IAAEA/f8IAAIA/f8IAAMA/f8IAAQA/f8IAAUA/f8IAAYA/f8IAAcA/f8IAAgA/f8JAPr//f8JAPv//f8JAPz//f8JAP3//f8JAP7//f8JAP///f8JAAAA/f8JAAEA/f8JAAIA/f8JAAMA/f8JAAQA/f8JAAUA/f8JAAYA/f8KAPv//f8KAPz//f8KAP3//f8KAP7//f8KAP///f8KAAAA/f8KAAEA/f8KAAIA/f8KAAMA/f8KAAQA/f8KAAUA/f8LAP7//f8LAP///f8LAAAA/f8LAAEA/f8LAAIA/v/1//3//v/1//7//v/1/////v/1/wAA/v/1/wEA/v/1/wIA/v/1/wMA/v/2//v//v/2//z//v/2//3//v/2//7//v/2/////v/2/wAA/v/2/wEA/v/2/wIA/v/2/wMA/v/2/wQA/v/2/wUA/v/3//n//v/3//r//v/3//v//v/3//z//v/3//3//v/3//7//v/3/////v/3/wAA/v/3/wEA/v/3/wIA/v/3/wMA/v/3/wQA/v/3/wUA/v/3/wYA/v/3/wcA/v/4//j//v/4//n//v/4//r//v/4//v//v/4//z//v/4//3//v/4//7//v/4/////v/4/wAA/v/4/wEA/v/4/wIA/v/4/wMA/v/4/wQA/v/4/wUA/v/4/wYA/v/4/wcA/v/4/wgA/v/5//f//v/5//j//v/5//n//v/5//r//v/5//v//v/5//z//v/5//3//v/5//7//v/5/////v/5/wAA/v/5/wEA/v/5/wIA/v/5/wMA/v/5/wQA/v/5/wUA/v/5/wYA/v/5/wcA/v/5/wgA/v/5/wkA/v/6//f//v/6//j//v/6//n//v/6//r//v/6//v//v/6//z//v/6//3//v/6//7//v/6/////v/6/wAA/v/6/wEA/v/6/wIA/v/6/wMA/v/6/wQA/v/6/wUA/v/6/wYA/v/6/wcA/v/6/wgA/v/6/wkA/v/7//b//v/7//f//v/7//j//v/7//n//v/7//r//v/7//v//v/7//z//v/7//3//v/7//7//v/7/////v/7/wAA/v/7/wEA/v/7/wIA/v/7/wMA/v/7/wQA/v/7/wUA/v/7/wYA/v/7/wcA/v/7/wgA/v/7/wkA/v/7/woA/v/8//b//v/8//f//v/8//j//v/8//n//v/8//r//v/8//v//v/8//z//v/8//3//v/8//7//v/8/////v/8/wAA/v/8/wEA/v/8/wIA/v/8/wMA/v/8/wQA/v/8/wUA/v/8/wYA/v/8/wcA/v/8/wgA/v/8/wkA/v/8/woA/v/9//X//v/9//b//v/9//f//v/9//j//v/9//n//v/9//r//v/9//v//v/9//z//v/9//3//v/9//7//v/9/////v/9/wAA/v/9/wEA/v/9/wIA/v/9/wMA/v/9/wQA/v/9/wUA/v/9/wYA/v/9/wcA/v/9/wgA/v/9/wkA/v/9/woA/v/9/wsA/v/+//X//v/+//b//v/+//f//v/+//j//v/+//n//v/+//r//v/+//v//v/+//z//v/+//3//v/+//7//v/+/////v/+/wAA/v/+/wEA/v/+/wIA/v/+/wMA/v/+/wQA/v/+/wUA/v/+/wYA/v/+/wcA/v/+/wgA/v/+/wkA/v/+/woA/v/+/wsA/v////X//v////b//v////f//v////j//v////n//v////r//v////v//v////z//v////3//v////7//v///////v///wAA/v///wEA/v///wIA/v///wMA/v///wQA/v///wUA/v///wYA/v///wcA/v///wgA/v///wkA/v///woA/v///wsA/v8AAPX//v8AAPb//v8AAPf//v8AAPj//v8AAPn//v8AAPr//v8AAPv//v8AAPz//v8AAP3//v8AAP7//v8AAP///v8AAAAA/v8AAAEA/v8AAAIA/v8AAAMA/v8AAAQA/v8AAAUA/v8AAAYA/v8AAAcA/v8AAAgA/v8AAAkA/v8AAAoA/v8AAAsA/v8BAPX//v8BAPb//v8BAPf//v8BAPj//v8BAPn//v8BAPr//v8BAPv//v8BAPz//v8BAP3//v8BAP7//v8BAP///v8BAAAA/v8BAAEA/v8BAAIA/v8BAAMA/v8BAAQA/v8BAAUA/v8BAAYA/v8BAAcA/v8BAAgA/v8BAAkA/v8BAAoA/v8BAAsA/v8CAPX//v8CAPb//v8CAPf//v8CAPj//v8CAPn//v8CAPr//v8CAPv//v8CAPz//v8CAP3//v8CAP7//v8CAP///v8CAAAA/v8CAAEA/v8CAAIA/v8CAAMA/v8CAAQA/v8CAAUA/v8CAAYA/v8CAAcA/v8CAAgA/v8CAAkA/v8CAAoA/v8CAAsA/v8DAPX//v8DAPb//v8DAPf//v8DAPj//v8DAPn//v8DAPr//v8DAPv//v8DAPz//v8DAP3//v8DAP7//v8DAP///v8DAAAA/v8DAAEA/v8DAAIA/v8DAAMA/v8DAAQA/v8DAAUA/v8DAAYA/v8DAAcA/v8DAAgA/v8DAAkA/v8DAAoA/v8DAAsA/v8EAPb//v8EAPf//v8EAPj//v8EAPn//v8EAPr//v8EAPv//v8EAPz//v8EAP3//v8EAP7//v8EAP///v8EAAAA/v8EAAEA/v8EAAIA/v8EAAMA/v8EAAQA/v8EAAUA/v8EAAYA/v8EAAcA/v8EAAgA/v8EAAkA/v8EAAoA/v8FAPb//v8FAPf//v8FAPj//v8FAPn//v8FAPr//v8FAPv//v8FAPz//v8FAP3//v8FAP7//v8FAP///v8FAAAA/v8FAAEA/v8FAAIA/v8FAAMA/v8FAAQA/v8FAAUA/v8FAAYA/v8FAAcA/v8FAAgA/v8FAAkA/v8FAAoA/v8GAPf//v8GAPj//v8GAPn//v8GAPr//v8GAPv//v8GAPz//v8GAP3//v8GAP7//v8GAP///v8GAAAA/v8GAAEA/v8GAAIA/v8GAAMA/v8GAAQA/v8GAAUA/v8GAAYA/v8GAAcA/v8GAAgA/v8GAAkA/v8HAPf//v8HAPj//v8HAPn//v8HAPr//v8HAPv//v8HAPz//v8HAP3//v8HAP7//v8HAP///v8HAAAA/v8HAAEA/v8HAAIA/v8HAAMA/v8HAAQA/v8HAAUA/v8HAAYA/v8HAAcA/v8HAAgA/v8HAAkA/v8IAPj//v8IAPn//v8IAPr//v8IAPv//v8IAPz//v8IAP3//v8IAP7//v8IAP///v8IAAAA/v8IAAEA/v8IAAIA/v8IAAMA/v8IAAQA/v8IAAUA/v8IAAYA/v8IAAcA/v8IAAgA/v8JAPn//v8JAPr//v8JAPv//v8JAPz//v8JAP3//v8JAP7//v8JAP///v8JAAAA/v8JAAEA/v8JAAIA/v8JAAMA/v8JAAQA/v8JAAUA/v8JAAYA/v8JAAcA/v8KAPv//v8KAPz//v8KAP3//v8KAP7//v8KAP///v8KAAAA/v8KAAEA/v8KAAIA/v8KAAMA/v8KAAQA/v8KAAUA/v8LAP3//v8LAP7//v8LAP///v8LAAAA/v8LAAEA/v8LAAIA/v8LAAMA///1//3////1//7////1///////1/wAA///1/wEA///1/wIA///1/wMA///2//r////2//v////2//z////2//3////2//7////2///////2/wAA///2/wEA///2/wIA///2/wMA///2/wQA///2/wUA///2/wYA///3//n////3//r////3//v////3//z////3//3////3//7////3///////3/wAA///3/wEA///3/wIA///3/wMA///3/wQA///3/wUA///3/wYA///3/wcA///4//j////4//n////4//r////4//v////4//z////4//3////4//7////4///////4/wAA///4/wEA///4/wIA///4/wMA///4/wQA///4/wUA///4/wYA///4/wcA///4/wgA///5//f////5//j////5//n////5//r////5//v////5//z////5//3////5//7////5///////5/wAA///5/wEA///5/wIA///5/wMA///5/wQA///5/wUA///5/wYA///5/wcA///5/wgA///5/wkA///6//b////6//f////6//j////6//n////6//r////6//v////6//z////6//3////6//7////6///////6/wAA///6/wEA///6/wIA///6/wMA///6/wQA///6/wUA///6/wYA///6/wcA///6/wgA///6/wkA///6/woA///7//b////7//f////7//j////7//n////7//r////7//v////7//z////7//3////7//7////7///////7/wAA///7/wEA///7/wIA///7/wMA///7/wQA///7/wUA///7/wYA///7/wcA///7/wgA///7/wkA///7/woA///8//b////8//f////8//j////8//n////8//r////8//v////8//z////8//3////8//7////8///////8/wAA///8/wEA///8/wIA///8/wMA///8/wQA///8/wUA///8/wYA///8/wcA///8/wgA///8/wkA///8/woA///9//X////9//b////9//f////9//j////9//n////9//r////9//v////9//z////9//3////9//7////9///////9/wAA///9/wEA///9/wIA///9/wMA///9/wQA///9/wUA///9/wYA///9/wcA///9/wgA///9/wkA///9/woA///9/wsA///+//X////+//b////+//f////+//j////+//n////+//r////+//v////+//z////+//3////+//7////+///////+/wAA///+/wEA///+/wIA///+/wMA///+/wQA///+/wUA///+/wYA///+/wcA///+/wgA///+/wkA///+/woA///+/wsA//////X///////b///////f///////j///////n///////r///////v///////z///////3///////7//////////////wAA/////wEA/////wIA/////wMA/////wQA/////wUA/////wYA/////wcA/////wgA/////wkA/////woA/////wsA//8AAPX///8AAPb///8AAPf///8AAPj///8AAPn///8AAPr///8AAPv///8AAPz///8AAP3///8AAP7///8AAP////8AAAAA//8AAAEA//8AAAIA//8AAAMA//8AAAQA//8AAAUA//8AAAYA//8AAAcA//8AAAgA//8AAAkA//8AAAoA//8AAAsA//8BAPX///8BAPb///8BAPf///8BAPj///8BAPn///8BAPr///8BAPv///8BAPz///8BAP3///8BAP7///8BAP////8BAAAA//8BAAEA//8BAAIA//8BAAMA//8BAAQA//8BAAUA//8BAAYA//8BAAcA//8BAAgA//8BAAkA//8BAAoA//8BAAsA//8CAPX///8CAPb///8CAPf///8CAPj///8CAPn///8CAPr///8CAPv///8CAPz///8CAP3///8CAP7///8CAP////8CAAAA//8CAAEA//8CAAIA//8CAAMA//8CAAQA//8CAAUA//8CAAYA//8CAAcA//8CAAgA//8CAAkA//8CAAoA//8CAAsA//8DAPX///8DAPb///8DAPf///8DAPj///8DAPn///8DAPr///8DAPv///8DAPz///8DAP3///8DAP7///8DAP////8DAAAA//8DAAEA//8DAAIA//8DAAMA//8DAAQA//8DAAUA//8DAAYA//8DAAcA//8DAAgA//8DAAkA//8DAAoA//8DAAsA//8EAPb///8EAPf///8EAPj///8EAPn///8EAPr///8EAPv///8EAPz///8EAP3///8EAP7///8EAP////8EAAAA//8EAAEA//8EAAIA//8EAAMA//8EAAQA//8EAAUA//8EAAYA//8EAAcA//8EAAgA//8EAAkA//8EAAoA//8FAPb///8FAPf///8FAPj///8FAPn///8FAPr///8FAPv///8FAPz///8FAP3///8FAP7///8FAP////8FAAAA//8FAAEA//8FAAIA//8FAAMA//8FAAQA//8FAAUA//8FAAYA//8FAAcA//8FAAgA//8FAAkA//8FAAoA//8GAPb///8GAPf///8GAPj///8GAPn///8GAPr///8GAPv///8GAPz///8GAP3///8GAP7///8GAP////8GAAAA//8GAAEA//8GAAIA//8GAAMA//8GAAQA//8GAAUA//8GAAYA//8GAAcA//8GAAgA//8GAAkA//8GAAoA//8HAPf///8HAPj///8HAPn///8HAPr///8HAPv///8HAPz///8HAP3///8HAP7///8HAP////8HAAAA//8HAAEA//8HAAIA//8HAAMA//8HAAQA//8HAAUA//8HAAYA//8HAAcA//8HAAgA//8HAAkA//8IAPj///8IAPn///8IAPr///8IAPv///8IAPz///8IAP3///8IAP7///8IAP////8IAAAA//8IAAEA//8IAAIA//8IAAMA//8IAAQA//8IAAUA//8IAAYA//8IAAcA//8IAAgA//8JAPn///8JAPr///8JAPv///8JAPz///8JAP3///8JAP7///8JAP////8JAAAA//8JAAEA//8JAAIA//8JAAMA//8JAAQA//8JAAUA//8JAAYA//8JAAcA//8KAPr///8KAPv///8KAPz///8KAP3///8KAP7///8KAP////8KAAAA//8KAAEA//8KAAIA//8KAAMA//8KAAQA//8KAAUA//8KAAYA//8LAP3///8LAP7///8LAP////8LAAAA//8LAAEA//8LAAIA//8LAAMAAAD1//z/AAD1//3/AAD1//7/AAD1////AAD1/wAAAAD1/wEAAAD1/wIAAAD1/wMAAAD1/wQAAAD2//r/AAD2//v/AAD2//z/AAD2//3/AAD2//7/AAD2////AAD2/wAAAAD2/wEAAAD2/wIAAAD2/wMAAAD2/wQAAAD2/wUAAAD2/wYAAAD3//n/AAD3//r/AAD3//v/AAD3//z/AAD3//3/AAD3//7/AAD3////AAD3/wAAAAD3/wEAAAD3/wIAAAD3/wMAAAD3/wQAAAD3/wUAAAD3/wYAAAD3/wcAAAD4//j/AAD4//n/AAD4//r/AAD4//v/AAD4//z/AAD4//3/AAD4//7/AAD4////AAD4/wAAAAD4/wEAAAD4/wIAAAD4/wMAAAD4/wQAAAD4/wUAAAD4/wYAAAD4/wcAAAD4/wgAAAD5//f/AAD5//j/AAD5//n/AAD5//r/AAD5//v/AAD5//z/AAD5//3/AAD5//7/AAD5////AAD5/wAAAAD5/wEAAAD5/wIAAAD5/wMAAAD5/wQAAAD5/wUAAAD5/wYAAAD5/wcAAAD5/wgAAAD5/wkAAAD6//b/AAD6//f/AAD6//j/AAD6//n/AAD6//r/AAD6//v/AAD6//z/AAD6//3/AAD6//7/AAD6////AAD6/wAAAAD6/wEAAAD6/wIAAAD6/wMAAAD6/wQAAAD6/wUAAAD6/wYAAAD6/wcAAAD6/wgAAAD6/wkAAAD6/woAAAD7//b/AAD7//f/AAD7//j/AAD7//n/AAD7//r/AAD7//v/AAD7//z/AAD7//3/AAD7//7/AAD7////AAD7/wAAAAD7/wEAAAD7/wIAAAD7/wMAAAD7/wQAAAD7/wUAAAD7/wYAAAD7/wcAAAD7/wgAAAD7/wkAAAD7/woAAAD8//X/AAD8//b/AAD8//f/AAD8//j/AAD8//n/AAD8//r/AAD8//v/AAD8//z/AAD8//3/AAD8//7/AAD8////AAD8/wAAAAD8/wEAAAD8/wIAAAD8/wMAAAD8/wQAAAD8/wUAAAD8/wYAAAD8/wcAAAD8/wgAAAD8/wkAAAD8/woAAAD8/wsAAAD9//X/AAD9//b/AAD9//f/AAD9//j/AAD9//n/AAD9//r/AAD9//v/AAD9//z/AAD9//3/AAD9//7/AAD9////AAD9/wAAAAD9/wEAAAD9/wIAAAD9/wMAAAD9/wQAAAD9/wUAAAD9/wYAAAD9/wcAAAD9/wgAAAD9/wkAAAD9/woAAAD9/wsAAAD+//X/AAD+//b/AAD+//f/AAD+//j/AAD+//n/AAD+//r/AAD+//v/AAD+//z/AAD+//3/AAD+//7/AAD+////AAD+/wAAAAD+/wEAAAD+/wIAAAD+/wMAAAD+/wQAAAD+/wUAAAD+/wYAAAD+/wcAAAD+/wgAAAD+/wkAAAD+/woAAAD+/wsAAAD///X/AAD///b/AAD///f/AAD///j/AAD///n/AAD///r/AAD///v/AAD///z/AAD///3/AAD///7/AAD/////AAD//wAAAAD//wEAAAD//wIAAAD//wMAAAD//wQAAAD//wUAAAD//wYAAAD//wcAAAD//wgAAAD//wkAAAD//woAAAD//wsAAAAAAPX/AAAAAPb/AAAAAPf/AAAAAPj/AAAAAPn/AAAAAPr/AAAAAPv/AAAAAPz/AAAAAP3/AAAAAP7/AAAAAP//AAAAAAEAAAAAAAIAAAAAAAMAAAAAAAQAAAAAAAUAAAAAAAYAAAAAAAcAAAAAAAgAAAAAAAkAAAAAAAoAAAAAAAsAAAABAPX/AAABAPb/AAABAPf/AAABAPj/AAABAPn/AAABAPr/AAABAPv/AAABAPz/AAABAP3/AAABAP7/AAABAP//AAABAAAAAAABAAEAAAABAAIAAAABAAMAAAABAAQAAAABAAUAAAABAAYAAAABAAcAAAABAAgAAAABAAkAAAABAAoAAAABAAsAAAACAPX/AAACAPb/AAACAPf/AAACAPj/AAACAPn/AAACAPr/AAACAPv/AAACAPz/AAACAP3/AAACAP7/AAACAP//AAACAAAAAAACAAEAAAACAAIAAAACAAMAAAACAAQAAAACAAUAAAACAAYAAAACAAcAAAACAAgAAAACAAkAAAACAAoAAAACAAsAAAADAPX/AAADAPb/AAADAPf/AAADAPj/AAADAPn/AAADAPr/AAADAPv/AAADAPz/AAADAP3/AAADAP7/AAADAP//AAADAAAAAAADAAEAAAADAAIAAAADAAMAAAADAAQAAAADAAUAAAADAAYAAAADAAcAAAADAAgAAAADAAkAAAADAAoAAAADAAsAAAAEAPX/AAAEAPb/AAAEAPf/AAAEAPj/AAAEAPn/AAAEAPr/AAAEAPv/AAAEAPz/AAAEAP3/AAAEAP7/AAAEAP//AAAEAAAAAAAEAAEAAAAEAAIAAAAEAAMAAAAEAAQAAAAEAAUAAAAEAAYAAAAEAAcAAAAEAAgAAAAEAAkAAAAEAAoAAAAEAAsAAAAFAPb/AAAFAPf/AAAFAPj/AAAFAPn/AAAFAPr/AAAFAPv/AAAFAPz/AAAFAP3/AAAFAP7/AAAFAP//AAAFAAAAAAAFAAEAAAAFAAIAAAAFAAMAAAAFAAQAAAAFAAUAAAAFAAYAAAAFAAcAAAAFAAgAAAAFAAkAAAAFAAoAAAAGAPb/AAAGAPf/AAAGAPj/AAAGAPn/AAAGAPr/AAAGAPv/AAAGAPz/AAAGAP3/AAAGAP7/AAAGAP//AAAGAAAAAAAGAAEAAAAGAAIAAAAGAAMAAAAGAAQAAAAGAAUAAAAGAAYAAAAGAAcAAAAGAAgAAAAGAAkAAAAGAAoAAAAHAPf/AAAHAPj/AAAHAPn/AAAHAPr/AAAHAPv/AAAHAPz/AAAHAP3/AAAHAP7/AAAHAP//AAAHAAAAAAAHAAEAAAAHAAIAAAAHAAMAAAAHAAQAAAAHAAUAAAAHAAYAAAAHAAcAAAAHAAgAAAAHAAkAAAAIAPj/AAAIAPn/AAAIAPr/AAAIAPv/AAAIAPz/AAAIAP3/AAAIAP7/AAAIAP//AAAIAAAAAAAIAAEAAAAIAAIAAAAIAAMAAAAIAAQAAAAIAAUAAAAIAAYAAAAIAAcAAAAIAAgAAAAJAPn/AAAJAPr/AAAJAPv/AAAJAPz/AAAJAP3/AAAJAP7/AAAJAP//AAAJAAAAAAAJAAEAAAAJAAIAAAAJAAMAAAAJAAQAAAAJAAUAAAAJAAYAAAAJAAcAAAAKAPr/AAAKAPv/AAAKAPz/AAAKAP3/AAAKAP7/AAAKAP//AAAKAAAAAAAKAAEAAAAKAAIAAAAKAAMAAAAKAAQAAAAKAAUAAAAKAAYAAAALAPz/AAALAP3/AAALAP7/AAALAP//AAALAAAAAAALAAEAAAALAAIAAAALAAMAAAALAAQAAQD1//3/AQD1//7/AQD1////AQD1/wAAAQD1/wEAAQD1/wIAAQD1/wMAAQD2//r/AQD2//v/AQD2//z/AQD2//3/AQD2//7/AQD2////AQD2/wAAAQD2/wEAAQD2/wIAAQD2/wMAAQD2/wQAAQD2/wUAAQD2/wYAAQD3//n/AQD3//r/AQD3//v/AQD3//z/AQD3//3/AQD3//7/AQD3////AQD3/wAAAQD3/wEAAQD3/wIAAQD3/wMAAQD3/wQAAQD3/wUAAQD3/wYAAQD3/wcAAQD4//j/AQD4//n/AQD4//r/AQD4//v/AQD4//z/AQD4//3/AQD4//7/AQD4////AQD4/wAAAQD4/wEAAQD4/wIAAQD4/wMAAQD4/wQAAQD4/wUAAQD4/wYAAQD4/wcAAQD4/wgAAQD5//f/AQD5//j/AQD5//n/AQD5//r/AQD5//v/AQD5//z/AQD5//3/AQD5//7/AQD5////AQD5/wAAAQD5/wEAAQD5/wIAAQD5/wMAAQD5/wQAAQD5/wUAAQD5/wYAAQD5/wcAAQD5/wgAAQD5/wkAAQD6//b/AQD6//f/AQD6//j/AQD6//n/AQD6//r/AQD6//v/AQD6//z/AQD6//3/AQD6//7/AQD6////AQD6/wAAAQD6/wEAAQD6/wIAAQD6/wMAAQD6/wQAAQD6/wUAAQD6/wYAAQD6/wcAAQD6/wgAAQD6/wkAAQD6/woAAQD7//b/AQD7//f/AQD7//j/AQD7//n/AQD7//r/AQD7//v/AQD7//z/AQD7//3/AQD7//7/AQD7////AQD7/wAAAQD7/wEAAQD7/wIAAQD7/wMAAQD7/wQAAQD7/wUAAQD7/wYAAQD7/wcAAQD7/wgAAQD7/wkAAQD7/woAAQD8//b/AQD8//f/AQD8//j/AQD8//n/AQD8//r/AQD8//v/AQD8//z/AQD8//3/AQD8//7/AQD8////AQD8/wAAAQD8/wEAAQD8/wIAAQD8/wMAAQD8/wQAAQD8/wUAAQD8/wYAAQD8/wcAAQD8/wgAAQD8/wkAAQD8/woAAQD9//X/AQD9//b/AQD9//f/AQD9//j/AQD9//n/AQD9//r/AQD9//v/AQD9//z/AQD9//3/AQD9//7/AQD9////AQD9/wAAAQD9/wEAAQD9/wIAAQD9/wMAAQD9/wQAAQD9/wUAAQD9/wYAAQD9/wcAAQD9/wgAAQD9/wkAAQD9/woAAQD9/wsAAQD+//X/AQD+//b/AQD+//f/AQD+//j/AQD+//n/AQD+//r/AQD+//v/AQD+//z/AQD+//3/AQD+//7/AQD+////AQD+/wAAAQD+/wEAAQD+/wIAAQD+/wMAAQD+/wQAAQD+/wUAAQD+/wYAAQD+/wcAAQD+/wgAAQD+/wkAAQD+/woAAQD+/wsAAQD///X/AQD///b/AQD///f/AQD///j/AQD///n/AQD///r/AQD///v/AQD///z/AQD///3/AQD///7/AQD/////AQD//wAAAQD//wEAAQD//wIAAQD//wMAAQD//wQAAQD//wUAAQD//wYAAQD//wcAAQD//wgAAQD//wkAAQD//woAAQD//wsAAQAAAPX/AQAAAPb/AQAAAPf/AQAAAPj/AQAAAPn/AQAAAPr/AQAAAPv/AQAAAPz/AQAAAP3/AQAAAP7/AQAAAP//AQAAAAAAAQAAAAEAAQAAAAIAAQAAAAMAAQAAAAQAAQAAAAUAAQAAAAYAAQAAAAcAAQAAAAgAAQAAAAkAAQAAAAoAAQAAAAsAAQABAPX/AQABAPb/AQABAPf/AQABAPj/AQABAPn/AQABAPr/AQABAPv/AQABAPz/AQABAP3/AQABAP7/AQABAP//AQABAAAAAQABAAEAAQABAAIAAQABAAMAAQABAAQAAQABAAUAAQABAAYAAQABAAcAAQABAAgAAQABAAkAAQABAAoAAQABAAsAAQACAPX/AQACAPb/AQACAPf/AQACAPj/AQACAPn/AQACAPr/AQACAPv/AQACAPz/AQACAP3/AQACAP7/AQACAP//AQACAAAAAQACAAEAAQACAAIAAQACAAMAAQACAAQAAQACAAUAAQACAAYAAQACAAcAAQACAAgAAQACAAkAAQACAAoAAQACAAsAAQADAPX/AQADAPb/AQADAPf/AQADAPj/AQADAPn/AQADAPr/AQADAPv/AQADAPz/AQADAP3/AQADAP7/AQADAP//AQADAAAAAQADAAEAAQADAAIAAQADAAMAAQADAAQAAQADAAUAAQADAAYAAQADAAcAAQADAAgAAQADAAkAAQADAAoAAQADAAsAAQAEAPb/AQAEAPf/AQAEAPj/AQAEAPn/AQAEAPr/AQAEAPv/AQAEAPz/AQAEAP3/AQAEAP7/AQAEAP//AQAEAAAAAQAEAAEAAQAEAAIAAQAEAAMAAQAEAAQAAQAEAAUAAQAEAAYAAQAEAAcAAQAEAAgAAQAEAAkAAQAEAAoAAQAFAPb/AQAFAPf/AQAFAPj/AQAFAPn/AQAFAPr/AQAFAPv/AQAFAPz/AQAFAP3/AQAFAP7/AQAFAP//AQAFAAAAAQAFAAEAAQAFAAIAAQAFAAMAAQAFAAQAAQAFAAUAAQAFAAYAAQAFAAcAAQAFAAgAAQAFAAkAAQAFAAoAAQAGAPb/AQAGAPf/AQAGAPj/AQAGAPn/AQAGAPr/AQAGAPv/AQAGAPz/AQAGAP3/AQAGAP7/AQAGAP//AQAGAAAAAQAGAAEAAQAGAAIAAQAGAAMAAQAGAAQAAQAGAAUAAQAGAAYAAQAGAAcAAQAGAAgAAQAGAAkAAQAGAAoAAQAHAPf/AQAHAPj/AQAHAPn/AQAHAPr/AQAHAPv/AQAHAPz/AQAHAP3/AQAHAP7/AQAHAP//AQAHAAAAAQAHAAEAAQAHAAIAAQAHAAMAAQAHAAQAAQAHAAUAAQAHAAYAAQAHAAcAAQAHAAgAAQAHAAkAAQAIAPj/AQAIAPn/AQAIAPr/AQAIAPv/AQAIAPz/AQAIAP3/AQAIAP7/AQAIAP//AQAIAAAAAQAIAAEAAQAIAAIAAQAIAAMAAQAIAAQAAQAIAAUAAQAIAAYAAQAIAAcAAQAIAAgAAQAJAPn/AQAJAPr/AQAJAPv/AQAJAPz/AQAJAP3/AQAJAP7/AQAJAP//AQAJAAAAAQAJAAEAAQAJAAIAAQAJAAMAAQAJAAQAAQAJAAUAAQAJAAYAAQAJAAcAAQAKAPr/AQAKAPv/AQAKAPz/AQAKAP3/AQAKAP7/AQAKAP//AQAKAAAAAQAKAAEAAQAKAAIAAQAKAAMAAQAKAAQAAQAKAAUAAQAKAAYAAQALAP3/AQALAP7/AQALAP//AQALAAAAAQALAAEAAQALAAIAAQALAAMAAgD1//3/AgD1//7/AgD1////AgD1/wAAAgD1/wEAAgD1/wIAAgD1/wMAAgD2//v/AgD2//z/AgD2//3/AgD2//7/AgD2////AgD2/wAAAgD2/wEAAgD2/wIAAgD2/wMAAgD2/wQAAgD2/wUAAgD3//n/AgD3//r/AgD3//v/AgD3//z/AgD3//3/AgD3//7/AgD3////AgD3/wAAAgD3/wEAAgD3/wIAAgD3/wMAAgD3/wQAAgD3/wUAAgD3/wYAAgD3/wcAAgD4//j/AgD4//n/AgD4//r/AgD4//v/AgD4//z/AgD4//3/AgD4//7/AgD4////AgD4/wAAAgD4/wEAAgD4/wIAAgD4/wMAAgD4/wQAAgD4/wUAAgD4/wYAAgD4/wcAAgD4/wgAAgD5//f/AgD5//j/AgD5//n/AgD5//r/AgD5//v/AgD5//z/AgD5//3/AgD5//7/AgD5////AgD5/wAAAgD5/wEAAgD5/wIAAgD5/wMAAgD5/wQAAgD5/wUAAgD5/wYAAgD5/wcAAgD5/wgAAgD5/wkAAgD6//f/AgD6//j/AgD6//n/AgD6//r/AgD6//v/AgD6//z/AgD6//3/AgD6//7/AgD6////AgD6/wAAAgD6/wEAAgD6/wIAAgD6/wMAAgD6/wQAAgD6/wUAAgD6/wYAAgD6/wcAAgD6/wgAAgD6/wkAAgD7//b/AgD7//f/AgD7//j/AgD7//n/AgD7//r/AgD7//v/AgD7//z/AgD7//3/AgD7//7/AgD7////AgD7/wAAAgD7/wEAAgD7/wIAAgD7/wMAAgD7/wQAAgD7/wUAAgD7/wYAAgD7/wcAAgD7/wgAAgD7/wkAAgD7/woAAgD8//b/AgD8//f/AgD8//j/AgD8//n/AgD8//r/AgD8//v/AgD8//z/AgD8//3/AgD8//7/AgD8////AgD8/wAAAgD8/wEAAgD8/wIAAgD8/wMAAgD8/wQAAgD8/wUAAgD8/wYAAgD8/wcAAgD8/wgAAgD8/wkAAgD8/woAAgD9//X/AgD9//b/AgD9//f/AgD9//j/AgD9//n/AgD9//r/AgD9//v/AgD9//z/AgD9//3/AgD9//7/AgD9////AgD9/wAAAgD9/wEAAgD9/wIAAgD9/wMAAgD9/wQAAgD9/wUAAgD9/wYAAgD9/wcAAgD9/wgAAgD9/wkAAgD9/woAAgD9/wsAAgD+//X/AgD+//b/AgD+//f/AgD+//j/AgD+//n/AgD+//r/AgD+//v/AgD+//z/AgD+//3/AgD+//7/AgD+////AgD+/wAAAgD+/wEAAgD+/wIAAgD+/wMAAgD+/wQAAgD+/wUAAgD+/wYAAgD+/wcAAgD+/wgAAgD+/wkAAgD+/woAAgD+/wsAAgD///X/AgD///b/AgD///f/AgD///j/AgD///n/AgD///r/AgD///v/AgD///z/AgD///3/AgD///7/AgD/////AgD//wAAAgD//wEAAgD//wIAAgD//wMAAgD//wQAAgD//wUAAgD//wYAAgD//wcAAgD//wgAAgD//wkAAgD//woAAgD//wsAAgAAAPX/AgAAAPb/AgAAAPf/AgAAAPj/AgAAAPn/AgAAAPr/AgAAAPv/AgAAAPz/AgAAAP3/AgAAAP7/AgAAAP//AgAAAAAAAgAAAAEAAgAAAAIAAgAAAAMAAgAAAAQAAgAAAAUAAgAAAAYAAgAAAAcAAgAAAAgAAgAAAAkAAgAAAAoAAgAAAAsAAgABAPX/AgABAPb/AgABAPf/AgABAPj/AgABAPn/AgABAPr/AgABAPv/AgABAPz/AgABAP3/AgABAP7/AgABAP//AgABAAAAAgABAAEAAgABAAIAAgABAAMAAgABAAQAAgABAAUAAgABAAYAAgABAAcAAgABAAgAAgABAAkAAgABAAoAAgABAAsAAgACAPX/AgACAPb/AgACAPf/AgACAPj/AgACAPn/AgACAPr/AgACAPv/AgACAPz/AgACAP3/AgACAP7/AgACAP//AgACAAAAAgACAAEAAgACAAIAAgACAAMAAgACAAQAAgACAAUAAgACAAYAAgACAAcAAgACAAgAAgACAAkAAgACAAoAAgACAAsAAgADAPX/AgADAPb/AgADAPf/AgADAPj/AgADAPn/AgADAPr/AgADAPv/AgADAPz/AgADAP3/AgADAP7/AgADAP//AgADAAAAAgADAAEAAgADAAIAAgADAAMAAgADAAQAAgADAAUAAgADAAYAAgADAAcAAgADAAgAAgADAAkAAgADAAoAAgADAAsAAgAEAPb/AgAEAPf/AgAEAPj/AgAEAPn/AgAEAPr/AgAEAPv/AgAEAPz/AgAEAP3/AgAEAP7/AgAEAP//AgAEAAAAAgAEAAEAAgAEAAIAAgAEAAMAAgAEAAQAAgAEAAUAAgAEAAYAAgAEAAcAAgAEAAgAAgAEAAkAAgAEAAoAAgAFAPb/AgAFAPf/AgAFAPj/AgAFAPn/AgAFAPr/AgAFAPv/AgAFAPz/AgAFAP3/AgAFAP7/AgAFAP//AgAFAAAAAgAFAAEAAgAFAAIAAgAFAAMAAgAFAAQAAgAFAAUAAgAFAAYAAgAFAAcAAgAFAAgAAgAFAAkAAgAFAAoAAgAGAPf/AgAGAPj/AgAGAPn/AgAGAPr/AgAGAPv/AgAGAPz/AgAGAP3/AgAGAP7/AgAGAP//AgAGAAAAAgAGAAEAAgAGAAIAAgAGAAMAAgAGAAQAAgAGAAUAAgAGAAYAAgAGAAcAAgAGAAgAAgAGAAkAAgAHAPf/AgAHAPj/AgAHAPn/AgAHAPr/AgAHAPv/AgAHAPz/AgAHAP3/AgAHAP7/AgAHAP//AgAHAAAAAgAHAAEAAgAHAAIAAgAHAAMAAgAHAAQAAgAHAAUAAgAHAAYAAgAHAAcAAgAHAAgAAgAHAAkAAgAIAPj/AgAIAPn/AgAIAPr/AgAIAPv/AgAIAPz/AgAIAP3/AgAIAP7/AgAIAP//AgAIAAAAAgAIAAEAAgAIAAIAAgAIAAMAAgAIAAQAAgAIAAUAAgAIAAYAAgAIAAcAAgAIAAgAAgAJAPn/AgAJAPr/AgAJAPv/AgAJAPz/AgAJAP3/AgAJAP7/AgAJAP//AgAJAAAAAgAJAAEAAgAJAAIAAgAJAAMAAgAJAAQAAgAJAAUAAgAJAAYAAgAJAAcAAgAKAPv/AgAKAPz/AgAKAP3/AgAKAP7/AgAKAP//AgAKAAAAAgAKAAEAAgAKAAIAAgAKAAMAAgAKAAQAAgAKAAUAAgALAP3/AgALAP7/AgALAP//AgALAAAAAgALAAEAAgALAAIAAgALAAMAAwD1//7/AwD1////AwD1/wAAAwD1/wEAAwD1/wIAAwD2//v/AwD2//z/AwD2//3/AwD2//7/AwD2////AwD2/wAAAwD2/wEAAwD2/wIAAwD2/wMAAwD2/wQAAwD2/wUAAwD3//r/AwD3//v/AwD3//z/AwD3//3/AwD3//7/AwD3////AwD3/wAAAwD3/wEAAwD3/wIAAwD3/wMAAwD3/wQAAwD3/wUAAwD3/wYAAwD4//j/AwD4//n/AwD4//r/AwD4//v/AwD4//z/AwD4//3/AwD4//7/AwD4////AwD4/wAAAwD4/wEAAwD4/wIAAwD4/wMAAwD4/wQAAwD4/wUAAwD4/wYAAwD4/wcAAwD4/wgAAwD5//j/AwD5//n/AwD5//r/AwD5//v/AwD5//z/AwD5//3/AwD5//7/AwD5////AwD5/wAAAwD5/wEAAwD5/wIAAwD5/wMAAwD5/wQAAwD5/wUAAwD5/wYAAwD5/wcAAwD5/wgAAwD6//f/AwD6//j/AwD6//n/AwD6//r/AwD6//v/AwD6//z/AwD6//3/AwD6//7/AwD6////AwD6/wAAAwD6/wEAAwD6/wIAAwD6/wMAAwD6/wQAAwD6/wUAAwD6/wYAAwD6/wcAAwD6/wgAAwD6/wkAAwD7//b/AwD7//f/AwD7//j/AwD7//n/AwD7//r/AwD7//v/AwD7//z/AwD7//3/AwD7//7/AwD7////AwD7/wAAAwD7/wEAAwD7/wIAAwD7/wMAAwD7/wQAAwD7/wUAAwD7/wYAAwD7/wcAAwD7/wgAAwD7/wkAAwD7/woAAwD8//b/AwD8//f/AwD8//j/AwD8//n/AwD8//r/AwD8//v/AwD8//z/AwD8//3/AwD8//7/AwD8////AwD8/wAAAwD8/wEAAwD8/wIAAwD8/wMAAwD8/wQAAwD8/wUAAwD8/wYAAwD8/wcAAwD8/wgAAwD8/wkAAwD8/woAAwD9//b/AwD9//f/AwD9//j/AwD9//n/AwD9//r/AwD9//v/AwD9//z/AwD9//3/AwD9//7/AwD9////AwD9/wAAAwD9/wEAAwD9/wIAAwD9/wMAAwD9/wQAAwD9/wUAAwD9/wYAAwD9/wcAAwD9/wgAAwD9/wkAAwD9/woAAwD+//X/AwD+//b/AwD+//f/AwD+//j/AwD+//n/AwD+//r/AwD+//v/AwD+//z/AwD+//3/AwD+//7/AwD+////AwD+/wAAAwD+/wEAAwD+/wIAAwD+/wMAAwD+/wQAAwD+/wUAAwD+/wYAAwD+/wcAAwD+/wgAAwD+/wkAAwD+/woAAwD+/wsAAwD///X/AwD///b/AwD///f/AwD///j/AwD///n/AwD///r/AwD///v/AwD///z/AwD///3/AwD///7/AwD/////AwD//wAAAwD//wEAAwD//wIAAwD//wMAAwD//wQAAwD//wUAAwD//wYAAwD//wcAAwD//wgAAwD//wkAAwD//woAAwD//wsAAwAAAPX/AwAAAPb/AwAAAPf/AwAAAPj/AwAAAPn/AwAAAPr/AwAAAPv/AwAAAPz/AwAAAP3/AwAAAP7/AwAAAP//AwAAAAAAAwAAAAEAAwAAAAIAAwAAAAMAAwAAAAQAAwAAAAUAAwAAAAYAAwAAAAcAAwAAAAgAAwAAAAkAAwAAAAoAAwAAAAsAAwABAPX/AwABAPb/AwABAPf/AwABAPj/AwABAPn/AwABAPr/AwABAPv/AwABAPz/AwABAP3/AwABAP7/AwABAP//AwABAAAAAwABAAEAAwABAAIAAwABAAMAAwABAAQAAwABAAUAAwABAAYAAwABAAcAAwABAAgAAwABAAkAAwABAAoAAwABAAsAAwACAPX/AwACAPb/AwACAPf/AwACAPj/AwACAPn/AwACAPr/AwACAPv/AwACAPz/AwACAP3/AwACAP7/AwACAP//AwACAAAAAwACAAEAAwACAAIAAwACAAMAAwACAAQAAwACAAUAAwACAAYAAwACAAcAAwACAAgAAwACAAkAAwACAAoAAwACAAsAAwADAPb/AwADAPf/AwADAPj/AwADAPn/AwADAPr/AwADAPv/AwADAPz/AwADAP3/AwADAP7/AwADAP//AwADAAAAAwADAAEAAwADAAIAAwADAAMAAwADAAQAAwADAAUAAwADAAYAAwADAAcAAwADAAgAAwADAAkAAwADAAoAAwAEAPb/AwAEAPf/AwAEAPj/AwAEAPn/AwAEAPr/AwAEAPv/AwAEAPz/AwAEAP3/AwAEAP7/AwAEAP//AwAEAAAAAwAEAAEAAwAEAAIAAwAEAAMAAwAEAAQAAwAEAAUAAwAEAAYAAwAEAAcAAwAEAAgAAwAEAAkAAwAEAAoAAwAFAPb/AwAFAPf/AwAFAPj/AwAFAPn/AwAFAPr/AwAFAPv/AwAFAPz/AwAFAP3/AwAFAP7/AwAFAP//AwAFAAAAAwAFAAEAAwAFAAIAAwAFAAMAAwAFAAQAAwAFAAUAAwAFAAYAAwAFAAcAAwAFAAgAAwAFAAkAAwAFAAoAAwAGAPf/AwAGAPj/AwAGAPn/AwAGAPr/AwAGAPv/AwAGAPz/AwAGAP3/AwAGAP7/AwAGAP//AwAGAAAAAwAGAAEAAwAGAAIAAwAGAAMAAwAGAAQAAwAGAAUAAwAGAAYAAwAGAAcAAwAGAAgAAwAGAAkAAwAHAPj/AwAHAPn/AwAHAPr/AwAHAPv/AwAHAPz/AwAHAP3/AwAHAP7/AwAHAP//AwAHAAAAAwAHAAEAAwAHAAIAAwAHAAMAAwAHAAQAAwAHAAUAAwAHAAYAAwAHAAcAAwAHAAgAAwAIAPj/AwAIAPn/AwAIAPr/AwAIAPv/AwAIAPz/AwAIAP3/AwAIAP7/AwAIAP//AwAIAAAAAwAIAAEAAwAIAAIAAwAIAAMAAwAIAAQAAwAIAAUAAwAIAAYAAwAIAAcAAwAIAAgAAwAJAPr/AwAJAPv/AwAJAPz/AwAJAP3/AwAJAP7/AwAJAP//AwAJAAAAAwAJAAEAAwAJAAIAAwAJAAMAAwAJAAQAAwAJAAUAAwAJAAYAAwAKAPv/AwAKAPz/AwAKAP3/AwAKAP7/AwAKAP//AwAKAAAAAwAKAAEAAwAKAAIAAwAKAAMAAwAKAAQAAwAKAAUAAwALAP7/AwALAP//AwALAAAAAwALAAEAAwALAAIABAD1/wAABAD2//z/BAD2//3/BAD2//7/BAD2////BAD2/wAABAD2/wEABAD2/wIABAD2/wMABAD2/wQABAD3//r/BAD3//v/BAD3//z/BAD3//3/BAD3//7/BAD3////BAD3/wAABAD3/wEABAD3/wIABAD3/wMABAD3/wQABAD3/wUABAD3/wYABAD4//n/BAD4//r/BAD4//v/BAD4//z/BAD4//3/BAD4//7/BAD4////BAD4/wAABAD4/wEABAD4/wIABAD4/wMABAD4/wQABAD4/wUABAD4/wYABAD4/wcABAD5//j/BAD5//n/BAD5//r/BAD5//v/BAD5//z/BAD5//3/BAD5//7/BAD5////BAD5/wAABAD5/wEABAD5/wIABAD5/wMABAD5/wQABAD5/wUABAD5/wYABAD5/wcABAD5/wgABAD6//f/BAD6//j/BAD6//n/BAD6//r/BAD6//v/BAD6//z/BAD6//3/BAD6//7/BAD6////BAD6/wAABAD6/wEABAD6/wIABAD6/wMABAD6/wQABAD6/wUABAD6/wYABAD6/wcABAD6/wgABAD6/wkABAD7//f/BAD7//j/BAD7//n/BAD7//r/BAD7//v/BAD7//z/BAD7//3/BAD7//7/BAD7////BAD7/wAABAD7/wEABAD7/wIABAD7/wMABAD7/wQABAD7/wUABAD7/wYABAD7/wcABAD7/wgABAD7/wkABAD8//b/BAD8//f/BAD8//j/BAD8//n/BAD8//r/BAD8//v/BAD8//z/BAD8//3/BAD8//7/BAD8////BAD8/wAABAD8/wEABAD8/wIABAD8/wMABAD8/wQABAD8/wUABAD8/wYABAD8/wcABAD8/wgABAD8/wkABAD8/woABAD9//b/BAD9//f/BAD9//j/BAD9//n/BAD9//r/BAD9//v/BAD9//z/BAD9//3/BAD9//7/BAD9////BAD9/wAABAD9/wEABAD9/wIABAD9/wMABAD9/wQABAD9/wUABAD9/wYABAD9/wcABAD9/wgABAD9/wkABAD9/woABAD+//b/BAD+//f/BAD+//j/BAD+//n/BAD+//r/BAD+//v/BAD+//z/BAD+//3/BAD+//7/BAD+////BAD+/wAABAD+/wEABAD+/wIABAD+/wMABAD+/wQABAD+/wUABAD+/wYABAD+/wcABAD+/wgABAD+/wkABAD+/woABAD///b/BAD///f/BAD///j/BAD///n/BAD///r/BAD///v/BAD///z/BAD///3/BAD///7/BAD/////BAD//wAABAD//wEABAD//wIABAD//wMABAD//wQABAD//wUABAD//wYABAD//wcABAD//wgABAD//wkABAD//woABAAAAPX/BAAAAPb/BAAAAPf/BAAAAPj/BAAAAPn/BAAAAPr/BAAAAPv/BAAAAPz/BAAAAP3/BAAAAP7/BAAAAP//BAAAAAAABAAAAAEABAAAAAIABAAAAAMABAAAAAQABAAAAAUABAAAAAYABAAAAAcABAAAAAgABAAAAAkABAAAAAoABAAAAAsABAABAPb/BAABAPf/BAABAPj/BAABAPn/BAABAPr/BAABAPv/BAABAPz/BAABAP3/BAABAP7/BAABAP//BAABAAAABAABAAEABAABAAIABAABAAMABAABAAQABAABAAUABAABAAYABAABAAcABAABAAgABAABAAkABAABAAoABAACAPb/BAACAPf/BAACAPj/BAACAPn/BAACAPr/BAACAPv/BAACAPz/BAACAP3/BAACAP7/BAACAP//BAACAAAABAACAAEABAACAAIABAACAAMABAACAAQABAACAAUABAACAAYABAACAAcABAACAAgABAACAAkABAACAAoABAADAPb/BAADAPf/BAADAPj/BAADAPn/BAADAPr/BAADAPv/BAADAPz/BAADAP3/BAADAP7/BAADAP//BAADAAAABAADAAEABAADAAIABAADAAMABAADAAQABAADAAUABAADAAYABAADAAcABAADAAgABAADAAkABAADAAoABAAEAPb/BAAEAPf/BAAEAPj/BAAEAPn/BAAEAPr/BAAEAPv/BAAEAPz/BAAEAP3/BAAEAP7/BAAEAP//BAAEAAAABAAEAAEABAAEAAIABAAEAAMABAAEAAQABAAEAAUABAAEAAYABAAEAAcABAAEAAgABAAEAAkABAAEAAoABAAFAPf/BAAFAPj/BAAFAPn/BAAFAPr/BAAFAPv/BAAFAPz/BAAFAP3/BAAFAP7/BAAFAP//BAAFAAAABAAFAAEABAAFAAIABAAFAAMABAAFAAQABAAFAAUABAAFAAYABAAFAAcABAAFAAgABAAFAAkABAAGAPf/BAAGAPj/BAAGAPn/BAAGAPr/BAAGAPv/BAAGAPz/BAAGAP3/BAAGAP7/BAAGAP//BAAGAAAABAAGAAEABAAGAAIABAAGAAMABAAGAAQABAAGAAUABAAGAAYABAAGAAcABAAGAAgABAAGAAkABAAHAPj/BAAHAPn/BAAHAPr/BAAHAPv/BAAHAPz/BAAHAP3/BAAHAP7/BAAHAP//BAAHAAAABAAHAAEABAAHAAIABAAHAAMABAAHAAQABAAHAAUABAAHAAYABAAHAAcABAAHAAgABAAIAPn/BAAIAPr/BAAIAPv/BAAIAPz/BAAIAP3/BAAIAP7/BAAIAP//BAAIAAAABAAIAAEABAAIAAIABAAIAAMABAAIAAQABAAIAAUABAAIAAYABAAIAAcABAAJAPr/BAAJAPv/BAAJAPz/BAAJAP3/BAAJAP7/BAAJAP//BAAJAAAABAAJAAEABAAJAAIABAAJAAMABAAJAAQABAAJAAUABAAJAAYABAAKAPz/BAAKAP3/BAAKAP7/BAAKAP//BAAKAAAABAAKAAEABAAKAAIABAAKAAMABAAKAAQABAALAAAABQD2//3/BQD2//7/BQD2////BQD2/wAABQD2/wEABQD2/wIABQD2/wMABQD3//v/BQD3//z/BQD3//3/BQD3//7/BQD3////BQD3/wAABQD3/wEABQD3/wIABQD3/wMABQD3/wQABQD3/wUABQD4//r/BQD4//v/BQD4//z/BQD4//3/BQD4//7/BQD4////BQD4/wAABQD4/wEABQD4/wIABQD4/wMABQD4/wQABQD4/wUABQD4/wYABQD5//n/BQD5//r/BQD5//v/BQD5//z/BQD5//3/BQD5//7/BQD5////BQD5/wAABQD5/wEABQD5/wIABQD5/wMABQD5/wQABQD5/wUABQD5/wYABQD5/wcABQD6//j/BQD6//n/BQD6//r/BQD6//v/BQD6//z/BQD6//3/BQD6//7/BQD6////BQD6/wAABQD6/wEABQD6/wIABQD6/wMABQD6/wQABQD6/wUABQD6/wYABQD6/wcABQD6/wgABQD7//f/BQD7//j/BQD7//n/BQD7//r/BQD7//v/BQD7//z/BQD7//3/BQD7//7/BQD7////BQD7/wAABQD7/wEABQD7/wIABQD7/wMABQD7/wQABQD7/wUABQD7/wYABQD7/wcABQD7/wgABQD7/wkABQD8//f/BQD8//j/BQD8//n/BQD8//r/BQD8//v/BQD8//z/BQD8//3/BQD8//7/BQD8////BQD8/wAABQD8/wEABQD8/wIABQD8/wMABQD8/wQABQD8/wUABQD8/wYABQD8/wcABQD8/wgABQD8/wkABQD9//b/BQD9//f/BQD9//j/BQD9//n/BQD9//r/BQD9//v/BQD9//z/BQD9//3/BQD9//7/BQD9////BQD9/wAABQD9/wEABQD9/wIABQD9/wMABQD9/wQABQD9/wUABQD9/wYABQD9/wcABQD9/wgABQD9/wkABQD9/woABQD+//b/BQD+//f/BQD+//j/BQD+//n/BQD+//r/BQD+//v/BQD+//z/BQD+//3/BQD+//7/BQD+////BQD+/wAABQD+/wEABQD+/wIABQD+/wMABQD+/wQABQD+/wUABQD+/wYABQD+/wcABQD+/wgABQD+/wkABQD+/woABQD///b/BQD///f/BQD///j/BQD///n/BQD///r/BQD///v/BQD///z/BQD///3/BQD///7/BQD/////BQD//wAABQD//wEABQD//wIABQD//wMABQD//wQABQD//wUABQD//wYABQD//wcABQD//wgABQD//wkABQD//woABQAAAPb/BQAAAPf/BQAAAPj/BQAAAPn/BQAAAPr/BQAAAPv/BQAAAPz/BQAAAP3/BQAAAP7/BQAAAP//BQAAAAAABQAAAAEABQAAAAIABQAAAAMABQAAAAQABQAAAAUABQAAAAYABQAAAAcABQAAAAgABQAAAAkABQAAAAoABQABAPb/BQABAPf/BQABAPj/BQABAPn/BQABAPr/BQABAPv/BQABAPz/BQABAP3/BQABAP7/BQABAP//BQABAAAABQABAAEABQABAAIABQABAAMABQABAAQABQABAAUABQABAAYABQABAAcABQABAAgABQABAAkABQABAAoABQACAPb/BQACAPf/BQACAPj/BQACAPn/BQACAPr/BQACAPv/BQACAPz/BQACAP3/BQACAP7/BQACAP//BQACAAAABQACAAEABQACAAIABQACAAMABQACAAQABQACAAUABQACAAYABQACAAcABQACAAgABQACAAkABQACAAoABQADAPb/BQADAPf/BQADAPj/BQADAPn/BQADAPr/BQADAPv/BQADAPz/BQADAP3/BQADAP7/BQADAP//BQADAAAABQADAAEABQADAAIABQADAAMABQADAAQABQADAAUABQADAAYABQADAAcABQADAAgABQADAAkABQADAAoABQAEAPf/BQAEAPj/BQAEAPn/BQAEAPr/BQAEAPv/BQAEAPz/BQAEAP3/BQAEAP7/BQAEAP//BQAEAAAABQAEAAEABQAEAAIABQAEAAMABQAEAAQABQAEAAUABQAEAAYABQAEAAcABQAEAAgABQAEAAkABQAFAPf/BQAFAPj/BQAFAPn/BQAFAPr/BQAFAPv/BQAFAPz/BQAFAP3/BQAFAP7/BQAFAP//BQAFAAAABQAFAAEABQAFAAIABQAFAAMABQAFAAQABQAFAAUABQAFAAYABQAFAAcABQAFAAgABQAFAAkABQAGAPj/BQAGAPn/BQAGAPr/BQAGAPv/BQAGAPz/BQAGAP3/BQAGAP7/BQAGAP//BQAGAAAABQAGAAEABQAGAAIABQAGAAMABQAGAAQABQAGAAUABQAGAAYABQAGAAcABQAGAAgABQAHAPn/BQAHAPr/BQAHAPv/BQAHAPz/BQAHAP3/BQAHAP7/BQAHAP//BQAHAAAABQAHAAEABQAHAAIABQAHAAMABQAHAAQABQAHAAUABQAHAAYABQAHAAcABQAIAPr/BQAIAPv/BQAIAPz/BQAIAP3/BQAIAP7/BQAIAP//BQAIAAAABQAIAAEABQAIAAIABQAIAAMABQAIAAQABQAIAAUABQAIAAYABQAJAPv/BQAJAPz/BQAJAP3/BQAJAP7/BQAJAP//BQAJAAAABQAJAAEABQAJAAIABQAJAAMABQAJAAQABQAJAAUABQAKAP3/BQAKAP7/BQAKAP//BQAKAAAABQAKAAEABQAKAAIABQAKAAMABgD2////BgD2/wAABgD2/wEABgD3//z/BgD3//3/BgD3//7/BgD3////BgD3/wAABgD3/wEABgD3/wIABgD3/wMABgD3/wQABgD4//r/BgD4//v/BgD4//z/BgD4//3/BgD4//7/BgD4////BgD4/wAABgD4/wEABgD4/wIABgD4/wMABgD4/wQABgD4/wUABgD4/wYABgD5//n/BgD5//r/BgD5//v/BgD5//z/BgD5//3/BgD5//7/BgD5////BgD5/wAABgD5/wEABgD5/wIABgD5/wMABgD5/wQABgD5/wUABgD5/wYABgD5/wcABgD6//j/BgD6//n/BgD6//r/BgD6//v/BgD6//z/BgD6//3/BgD6//7/BgD6////BgD6/wAABgD6/wEABgD6/wIABgD6/wMABgD6/wQABgD6/wUABgD6/wYABgD6/wcABgD6/wgABgD7//j/BgD7//n/BgD7//r/BgD7//v/BgD7//z/BgD7//3/BgD7//7/BgD7////BgD7/wAABgD7/wEABgD7/wIABgD7/wMABgD7/wQABgD7/wUABgD7/wYABgD7/wcABgD7/wgABgD8//f/BgD8//j/BgD8//n/BgD8//r/BgD8//v/BgD8//z/BgD8//3/BgD8//7/BgD8////BgD8/wAABgD8/wEABgD8/wIABgD8/wMABgD8/wQABgD8/wUABgD8/wYABgD8/wcABgD8/wgABgD8/wkABgD9//f/BgD9//j/BgD9//n/BgD9//r/BgD9//v/BgD9//z/BgD9//3/BgD9//7/BgD9////BgD9/wAABgD9/wEABgD9/wIABgD9/wMABgD9/wQABgD9/wUABgD9/wYABgD9/wcABgD9/wgABgD9/wkABgD+//f/BgD+//j/BgD+//n/BgD+//r/BgD+//v/BgD+//z/BgD+//3/BgD+//7/BgD+////BgD+/wAABgD+/wEABgD+/wIABgD+/wMABgD+/wQABgD+/wUABgD+/wYABgD+/wcABgD+/wgABgD+/wkABgD///b/BgD///f/BgD///j/BgD///n/BgD///r/BgD///v/BgD///z/BgD///3/BgD///7/BgD/////BgD//wAABgD//wEABgD//wIABgD//wMABgD//wQABgD//wUABgD//wYABgD//wcABgD//wgABgD//wkABgD//woABgAAAPb/BgAAAPf/BgAAAPj/BgAAAPn/BgAAAPr/BgAAAPv/BgAAAPz/BgAAAP3/BgAAAP7/BgAAAP//BgAAAAAABgAAAAEABgAAAAIABgAAAAMABgAAAAQABgAAAAUABgAAAAYABgAAAAcABgAAAAgABgAAAAkABgAAAAoABgABAPb/BgABAPf/BgABAPj/BgABAPn/BgABAPr/BgABAPv/BgABAPz/BgABAP3/BgABAP7/BgABAP//BgABAAAABgABAAEABgABAAIABgABAAMABgABAAQABgABAAUABgABAAYABgABAAcABgABAAgABgABAAkABgABAAoABgACAPf/BgACAPj/BgACAPn/BgACAPr/BgACAPv/BgACAPz/BgACAP3/BgACAP7/BgACAP//BgACAAAABgACAAEABgACAAIABgACAAMABgACAAQABgACAAUABgACAAYABgACAAcABgACAAgABgACAAkABgADAPf/BgADAPj/BgADAPn/BgADAPr/BgADAPv/BgADAPz/BgADAP3/BgADAP7/BgADAP//BgADAAAABgADAAEABgADAAIABgADAAMABgADAAQABgADAAUABgADAAYABgADAAcABgADAAgABgADAAkABgAEAPf/BgAEAPj/BgAEAPn/BgAEAPr/BgAEAPv/BgAEAPz/BgAEAP3/BgAEAP7/BgAEAP//BgAEAAAABgAEAAEABgAEAAIABgAEAAMABgAEAAQABgAEAAUABgAEAAYABgAEAAcABgAEAAgABgAEAAkABgAFAPj/BgAFAPn/BgAFAPr/BgAFAPv/BgAFAPz/BgAFAP3/BgAFAP7/BgAFAP//BgAFAAAABgAFAAEABgAFAAIABgAFAAMABgAFAAQABgAFAAUABgAFAAYABgAFAAcABgAFAAgABgAGAPj/BgAGAPn/BgAGAPr/BgAGAPv/BgAGAPz/BgAGAP3/BgAGAP7/BgAGAP//BgAGAAAABgAGAAEABgAGAAIABgAGAAMABgAGAAQABgAGAAUABgAGAAYABgAGAAcABgAGAAgABgAHAPn/BgAHAPr/BgAHAPv/BgAHAPz/BgAHAP3/BgAHAP7/BgAHAP//BgAHAAAABgAHAAEABgAHAAIABgAHAAMABgAHAAQABgAHAAUABgAHAAYABgAHAAcABgAIAPr/BgAIAPv/BgAIAPz/BgAIAP3/BgAIAP7/BgAIAP//BgAIAAAABgAIAAEABgAIAAIABgAIAAMABgAIAAQABgAIAAUABgAIAAYABgAJAPz/BgAJAP3/BgAJAP7/BgAJAP//BgAJAAAABgAJAAEABgAJAAIABgAJAAMABgAJAAQABgAKAP//BgAKAAAABgAKAAEABwD3//7/BwD3////BwD3/wAABwD3/wEABwD3/wIABwD4//z/BwD4//3/BwD4//7/BwD4////BwD4/wAABwD4/wEABwD4/wIABwD4/wMABwD4/wQABwD5//r/BwD5//v/BwD5//z/BwD5//3/BwD5//7/BwD5////BwD5/wAABwD5/wEABwD5/wIABwD5/wMABwD5/wQABwD5/wUABwD5/wYABwD6//n/BwD6//r/BwD6//v/BwD6//z/BwD6//3/BwD6//7/BwD6////BwD6/wAABwD6/wEABwD6/wIABwD6/wMABwD6/wQABwD6/wUABwD6/wYABwD6/wcABwD7//n/BwD7//r/BwD7//v/BwD7//z/BwD7//3/BwD7//7/BwD7////BwD7/wAABwD7/wEABwD7/wIABwD7/wMABwD7/wQABwD7/wUABwD7/wYABwD7/wcABwD8//j/BwD8//n/BwD8//r/BwD8//v/BwD8//z/BwD8//3/BwD8//7/BwD8////BwD8/wAABwD8/wEABwD8/wIABwD8/wMABwD8/wQABwD8/wUABwD8/wYABwD8/wcABwD8/wgABwD9//j/BwD9//n/BwD9//r/BwD9//v/BwD9//z/BwD9//3/BwD9//7/BwD9////BwD9/wAABwD9/wEABwD9/wIABwD9/wMABwD9/wQABwD9/wUABwD9/wYABwD9/wcABwD9/wgABwD+//f/BwD+//j/BwD+//n/BwD+//r/BwD+//v/BwD+//z/BwD+//3/BwD+//7/BwD+////BwD+/wAABwD+/wEABwD+/wIABwD+/wMABwD+/wQABwD+/wUABwD+/wYABwD+/wcABwD+/wgABwD+/wkABwD///f/BwD///j/BwD///n/BwD///r/BwD///v/BwD///z/BwD///3/BwD///7/BwD/////BwD//wAABwD//wEABwD//wIABwD//wMABwD//wQABwD//wUABwD//wYABwD//wcABwD//wgABwD//wkABwAAAPf/BwAAAPj/BwAAAPn/BwAAAPr/BwAAAPv/BwAAAPz/BwAAAP3/BwAAAP7/BwAAAP//BwAAAAAABwAAAAEABwAAAAIABwAAAAMABwAAAAQABwAAAAUABwAAAAYABwAAAAcABwAAAAgABwAAAAkABwABAPf/BwABAPj/BwABAPn/BwABAPr/BwABAPv/BwABAPz/BwABAP3/BwABAP7/BwABAP//BwABAAAABwABAAEABwABAAIABwABAAMABwABAAQABwABAAUABwABAAYABwABAAcABwABAAgABwABAAkABwACAPf/BwACAPj/BwACAPn/BwACAPr/BwACAPv/BwACAPz/BwACAP3/BwACAP7/BwACAP//BwACAAAABwACAAEABwACAAIABwACAAMABwACAAQABwACAAUABwACAAYABwACAAcABwACAAgABwACAAkABwADAPj/BwADAPn/BwADAPr/BwADAPv/BwADAPz/BwADAP3/BwADAP7/BwADAP//BwADAAAABwADAAEABwADAAIABwADAAMABwADAAQABwADAAUABwADAAYABwADAAcABwADAAgABwAEAPj/BwAEAPn/BwAEAPr/BwAEAPv/BwAEAPz/BwAEAP3/BwAEAP7/BwAEAP//BwAEAAAABwAEAAEABwAEAAIABwAEAAMABwAEAAQABwAEAAUABwAEAAYABwAEAAcABwAEAAgABwAFAPn/BwAFAPr/BwAFAPv/BwAFAPz/BwAFAP3/BwAFAP7/BwAFAP//BwAFAAAABwAFAAEABwAFAAIABwAFAAMABwAFAAQABwAFAAUABwAFAAYABwAFAAcABwAGAPn/BwAGAPr/BwAGAPv/BwAGAPz/BwAGAP3/BwAGAP7/BwAGAP//BwAGAAAABwAGAAEABwAGAAIABwAGAAMABwAGAAQABwAGAAUABwAGAAYABwAGAAcABwAHAPr/BwAHAPv/BwAHAPz/BwAHAP3/BwAHAP7/BwAHAP//BwAHAAAABwAHAAEABwAHAAIABwAHAAMABwAHAAQABwAHAAUABwAHAAYABwAIAPz/BwAIAP3/BwAIAP7/BwAIAP//BwAIAAAABwAIAAEABwAIAAIABwAIAAMABwAIAAQABwAJAP7/BwAJAP//BwAJAAAABwAJAAEABwAJAAIACAD4//3/CAD4//7/CAD4////CAD4/wAACAD4/wEACAD4/wIACAD4/wMACAD5//z/CAD5//3/CAD5//7/CAD5////CAD5/wAACAD5/wEACAD5/wIACAD5/wMACAD5/wQACAD6//r/CAD6//v/CAD6//z/CAD6//3/CAD6//7/CAD6////CAD6/wAACAD6/wEACAD6/wIACAD6/wMACAD6/wQACAD6/wUACAD6/wYACAD7//r/CAD7//v/CAD7//z/CAD7//3/CAD7//7/CAD7////CAD7/wAACAD7/wEACAD7/wIACAD7/wMACAD7/wQACAD7/wUACAD7/wYACAD8//n/CAD8//r/CAD8//v/CAD8//z/CAD8//3/CAD8//7/CAD8////CAD8/wAACAD8/wEACAD8/wIACAD8/wMACAD8/wQACAD8/wUACAD8/wYACAD8/wcACAD9//j/CAD9//n/CAD9//r/CAD9//v/CAD9//z/CAD9//3/CAD9//7/CAD9////CAD9/wAACAD9/wEACAD9/wIACAD9/wMACAD9/wQACAD9/wUACAD9/wYACAD9/wcACAD9/wgACAD+//j/CAD+//n/CAD+//r/CAD+//v/CAD+//z/CAD+//3/CAD+//7/CAD+////CAD+/wAACAD+/wEACAD+/wIACAD+/wMACAD+/wQACAD+/wUACAD+/wYACAD+/wcACAD+/wgACAD///j/CAD///n/CAD///r/CAD///v/CAD///z/CAD///3/CAD///7/CAD/////CAD//wAACAD//wEACAD//wIACAD//wMACAD//wQACAD//wUACAD//wYACAD//wcACAD//wgACAAAAPj/CAAAAPn/CAAAAPr/CAAAAPv/CAAAAPz/CAAAAP3/CAAAAP7/CAAAAP//CAAAAAAACAAAAAEACAAAAAIACAAAAAMACAAAAAQACAAAAAUACAAAAAYACAAAAAcACAAAAAgACAABAPj/CAABAPn/CAABAPr/CAABAPv/CAABAPz/CAABAP3/CAABAP7/CAABAP//CAABAAAACAABAAEACAABAAIACAABAAMACAABAAQACAABAAUACAABAAYACAABAAcACAABAAgACAACAPj/CAACAPn/CAACAPr/CAACAPv/CAACAPz/CAACAP3/CAACAP7/CAACAP//CAACAAAACAACAAEACAACAAIACAACAAMACAACAAQACAACAAUACAACAAYACAACAAcACAACAAgACAADAPj/CAADAPn/CAADAPr/CAADAPv/CAADAPz/CAADAP3/CAADAP7/CAADAP//CAADAAAACAADAAEACAADAAIACAADAAMACAADAAQACAADAAUACAADAAYACAADAAcACAADAAgACAAEAPn/CAAEAPr/CAAEAPv/CAAEAPz/CAAEAP3/CAAEAP7/CAAEAP//CAAEAAAACAAEAAEACAAEAAIACAAEAAMACAAEAAQACAAEAAUACAAEAAYACAAEAAcACAAFAPr/CAAFAPv/CAAFAPz/CAAFAP3/CAAFAP7/CAAFAP//CAAFAAAACAAFAAEACAAFAAIACAAFAAMACAAFAAQACAAFAAUACAAFAAYACAAGAPr/CAAGAPv/CAAGAPz/CAAGAP3/CAAGAP7/CAAGAP//CAAGAAAACAAGAAEACAAGAAIACAAGAAMACAAGAAQACAAGAAUACAAGAAYACAAHAPz/CAAHAP3/CAAHAP7/CAAHAP//CAAHAAAACAAHAAEACAAHAAIACAAHAAMACAAHAAQACAAIAP3/CAAIAP7/CAAIAP//CAAIAAAACAAIAAEACAAIAAIACAAIAAMACQD5//7/CQD5////CQD5/wAACQD5/wEACQD5/wIACQD6//z/CQD6//3/CQD6//7/CQD6////CQD6/wAACQD6/wEACQD6/wIACQD6/wMACQD6/wQACQD7//v/CQD7//z/CQD7//3/CQD7//7/CQD7////CQD7/wAACQD7/wEACQD7/wIACQD7/wMACQD7/wQACQD7/wUACQD8//r/CQD8//v/CQD8//z/CQD8//3/CQD8//7/CQD8////CQD8/wAACQD8/wEACQD8/wIACQD8/wMACQD8/wQACQD8/wUACQD8/wYACQD9//r/CQD9//v/CQD9//z/CQD9//3/CQD9//7/CQD9////CQD9/wAACQD9/wEACQD9/wIACQD9/wMACQD9/wQACQD9/wUACQD9/wYACQD+//n/CQD+//r/CQD+//v/CQD+//z/CQD+//3/CQD+//7/CQD+////CQD+/wAACQD+/wEACQD+/wIACQD+/wMACQD+/wQACQD+/wUACQD+/wYACQD+/wcACQD///n/CQD///r/CQD///v/CQD///z/CQD///3/CQD///7/CQD/////CQD//wAACQD//wEACQD//wIACQD//wMACQD//wQACQD//wUACQD//wYACQD//wcACQAAAPn/CQAAAPr/CQAAAPv/CQAAAPz/CQAAAP3/CQAAAP7/CQAAAP//CQAAAAAACQAAAAEACQAAAAIACQAAAAMACQAAAAQACQAAAAUACQAAAAYACQAAAAcACQABAPn/CQABAPr/CQABAPv/CQABAPz/CQABAP3/CQABAP7/CQABAP//CQABAAAACQABAAEACQABAAIACQABAAMACQABAAQACQABAAUACQABAAYACQABAAcACQACAPn/CQACAPr/CQACAPv/CQACAPz/CQACAP3/CQACAP7/CQACAP//CQACAAAACQACAAEACQACAAIACQACAAMACQACAAQACQACAAUACQACAAYACQACAAcACQADAPr/CQADAPv/CQADAPz/CQADAP3/CQADAP7/CQADAP//CQADAAAACQADAAEACQADAAIACQADAAMACQADAAQACQADAAUACQADAAYACQAEAPr/CQAEAPv/CQAEAPz/CQAEAP3/CQAEAP7/CQAEAP//CQAEAAAACQAEAAEACQAEAAIACQAEAAMACQAEAAQACQAEAAUACQAEAAYACQAFAPv/CQAFAPz/CQAFAP3/CQAFAP7/CQAFAP//CQAFAAAACQAFAAEACQAFAAIACQAFAAMACQAFAAQACQAFAAUACQAGAPz/CQAGAP3/CQAGAP7/CQAGAP//CQAGAAAACQAGAAEACQAGAAIACQAGAAMACQAGAAQACQAHAP7/CQAHAP//CQAHAAAACQAHAAEACQAHAAIACgD6////CgD6/wAACgD6/wEACgD7//3/CgD7//7/CgD7////CgD7/wAACgD7/wEACgD7/wIACgD7/wMACgD8//z/CgD8//3/CgD8//7/CgD8////CgD8/wAACgD8/wEACgD8/wIACgD8/wMACgD8/wQACgD9//v/CgD9//z/CgD9//3/CgD9//7/CgD9////CgD9/wAACgD9/wEACgD9/wIACgD9/wMACgD9/wQACgD9/wUACgD+//v/CgD+//z/CgD+//3/CgD+//7/CgD+////CgD+/wAACgD+/wEACgD+/wIACgD+/wMACgD+/wQACgD+/wUACgD///r/CgD///v/CgD///z/CgD///3/CgD///7/CgD/////CgD//wAACgD//wEACgD//wIACgD//wMACgD//wQACgD//wUACgD//wYACgAAAPr/CgAAAPv/CgAAAPz/CgAAAP3/CgAAAP7/CgAAAP//CgAAAAAACgAAAAEACgAAAAIACgAAAAMACgAAAAQACgAAAAUACgAAAAYACgABAPr/CgABAPv/CgABAPz/CgABAP3/CgABAP7/CgABAP//CgABAAAACgABAAEACgABAAIACgABAAMACgABAAQACgABAAUACgABAAYACgACAPv/CgACAPz/CgACAP3/CgACAP7/CgACAP//CgACAAAACgACAAEACgACAAIACgACAAMACgACAAQACgACAAUACgADAPv/CgADAPz/CgADAP3/CgADAP7/CgADAP//CgADAAAACgADAAEACgADAAIACgADAAMACgADAAQACgADAAUACgAEAPz/CgAEAP3/CgAEAP7/CgAEAP//CgAEAAAACgAEAAEACgAEAAIACgAEAAMACgAEAAQACgAFAP3/CgAFAP7/CgAFAP//CgAFAAAACgAFAAEACgAFAAIACgAFAAMACgAGAP//CgAGAAAACgAGAAEACwD8/wAACwD9//7/CwD9////CwD9/wAACwD9/wEACwD9/wIACwD+//3/CwD+//7/CwD+////CwD+/wAACwD+/wEACwD+/wIACwD+/wMACwD///3/CwD///7/CwD/////CwD//wAACwD//wEACwD//wIACwD//wMACwAAAPz/CwAAAP3/CwAAAP7/CwAAAP//CwAAAAAACwAAAAEACwAAAAIACwAAAAMACwAAAAQACwABAP3/CwABAP7/CwABAP//CwABAAAACwABAAEACwABAAIACwABAAMACwACAP3/CwACAP7/CwACAP//CwACAAAACwACAAEACwACAAIACwACAAMACwADAP7/CwADAP//CwADAAAACwADAAEACwADAAIACwAEAAAA\",\"F2\":\"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\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.007982493960435978,\"absorptive\":true,\"n_reflections\":6762,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":false}", - "Al2O3 (corundum)": "{\"name\":\"Al2O3\",\"spacegroup\":\"R-3c (167)\",\"pointgroup\":\"-3m\",\"cell\":[[4.7602,0.0,0.0],[-2.380099999999999,4.122454127094685,0.0],[0.0,0.0,12.9933]],\"recip\":[[0.2100752069240788,0.12128697726768317,0.0],[0.0,0.24257395453536645,0.0],[0.0,0.0,0.07696274233643494]],\"positions_frac\":[[0.0,0.0,0.3523],[0.0,0.0,0.1477],[0.6666666666666665,0.3333333333333333,0.6856333333333333],[0.6666666666666665,0.3333333333333333,0.48103333333333326],[0.3333333333333333,0.6666666666666666,0.018966666666666576],[0.3333333333333333,0.6666666666666666,0.8143666666666665],[0.0,0.0,0.6476999999999999],[0.0,0.0,0.8523000000000001],[0.6666666666666665,0.3333333333333333,0.9810333333333334],[0.6666666666666665,0.3333333333333333,0.18563333333333332],[0.3333333333333333,0.6666666666666666,0.3143666666666667],[0.3333333333333333,0.6666666666666666,0.5189666666666666],[0.3064,0.0,0.25],[0.0,0.3064,0.25],[0.6936,0.6936,0.25],[0.9730666666666666,0.3333333333333333,0.5833333333333333],[0.6666666666666666,0.6397333333333333,0.5833333333333333],[0.3602666666666666,0.02693333333333331,0.5833333333333333],[0.6397333333333332,0.6666666666666666,0.9166666666666667],[0.33333333333333337,0.9730666666666666,0.9166666666666667],[0.0269333333333333,0.3602666666666666,0.9166666666666667],[0.6936,0.0,0.75],[0.0,0.6936,0.75],[0.3064,0.3064,0.75],[0.3602666666666666,0.3333333333333333,0.08333333333333333],[0.6666666666666666,0.02693333333333331,0.08333333333333333],[0.9730666666666666,0.6397333333333333,0.08333333333333333],[0.0269333333333333,0.6666666666666666,0.41666666666666663],[0.3333333333333333,0.3602666666666666,0.41666666666666663],[0.6397333333333332,0.9730666666666666,0.41666666666666663]],\"numbers\":[13,13,13,13,13,13,13,13,13,13,13,13,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8],\"symbols\":[\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"Al\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\",\"O\"],\"colors\":[[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[0.749,0.651,0.651],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051],[1.0,0.051,0.051]],\"radii\":[1.21,1.21,1.21,1.21,1.21,1.21,1.21,1.21,1.21,1.21,1.21,1.21,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66,0.66],\"hkl_i16\":\"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+P/8/wIA+P/8/wUA+P/8/wgA+P/8/wsA+P/8/w4A+P/8/xEA+P/8/xQA+P/9/+n/+P/9/+z/+P/9/+//+P/9//L/+P/9//X/+P/9//j/+P/9//v/+P/9//7/+P/9/wEA+P/9/wQA+P/9/wcA+P/9/woA+P/9/w0A+P/9/xAA+P/9/xMA+P/9/xYA+P/+/+j/+P/+/+v/+P/+/+7/+P/+//H/+P/+//T/+P/+//f/+P/+//r/+P/+//3/+P/+/wAA+P/+/wMA+P/+/wYA+P/+/wkA+P/+/wwA+P/+/w8A+P/+/xIA+P/+/xUA+P/+/xgA+P///+T/+P///+f/+P///+r/+P///+3/+P////D/+P////P/+P////b/+P////n/+P////z/+P//////+P///wIA+P///wUA+P///wgA+P///wsA+P///w4A+P///xEA+P///xQA+P///xcA+P///xoA+P8AAOP/+P8AAOb/+P8AAOn/+P8AAOz/+P8AAO//+P8AAPL/+P8AAPX/+P8AAPj/+P8AAPv/+P8AAP7/+P8AAAEA+P8AAAQA+P8AAAcA+P8AAAoA+P8AAA0A+P8AABAA+P8AABMA+P8AABYA+P8AABkA+P8AABwA+P8BAOL/+P8BAOX/+P8BAOj/+P8BAOv/+P8BAO7/+P8BAPH/+P8BAPT/+P8BAPf/+P8BAPr/+P8BAP3/+P8BAAAA+P8BAAMA+P8BAAYA+P8BAAkA+P8BAAwA+P8BAA8A+P8BABIA+P8BABUA+P8BABgA+P8BABsA+P8BAB4A+P8CAOH/+P8CAOT/+P8CAOf/+P8CAOr/+P8CAO3/+P8CAPD/+P8CAPP/+P8CAPb/+P8CAPn/+P8CAPz/+P8CAP//+P8CAAIA+P8CAAUA+P8CAAgA+P8CAAsA+P8CAA4A+P8CABEA+P8CABQA+P8CABcA+P8CABoA+P8CAB0A+P8DAOD/+P8DAOP/+P8DAOb/+P8DAOn/+P8DAOz/+P8DAO//+P8DAPL/+P8DAPX/+P8DAPj/+P8DAPv/+P8DAP7/+P8DAAEA+P8DAAQA+P8DAAcA+P8DAAoA+P8DAA0A+P8DABAA+P8DABMA+P8DABYA+P8DABkA+P8DABwA+P8DAB8A+P8EAOL/+P8EAOX/+P8EAOj/+P8EAOv/+P8EAO7/+P8EAPH/+P8EAPT/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+f8AAAsA+f8AAA4A+f8AABEA+f8AABQA+f8AABcA+f8AABoA+f8AAB0A+f8AACAA+f8BAOD/+f8BAOP/+f8BAOb/+f8BAOn/+f8BAOz/+f8BAO//+f8BAPL/+f8BAPX/+f8BAPj/+f8BAPv/+f8BAP7/+f8BAAEA+f8BAAQA+f8BAAcA+f8BAAoA+f8BAA0A+f8BABAA+f8BABMA+f8BABYA+f8BABkA+f8BABwA+f8BAB8A+f8CAN//+f8CAOL/+f8CAOX/+f8CAOj/+f8CAOv/+f8CAO7/+f8CAPH/+f8CAPT/+f8CAPf/+f8CAPr/+f8CAP3/+f8CAAAA+f8CAAMA+f8CAAYA+f8CAAkA+f8CAAwA+f8CAA8A+f8CABIA+f8CABUA+f8CABgA+f8CABsA+f8CAB4A+f8CACEA+f8DAOH/+f8DAOT/+f8DAOf/+f8DAOr/+f8DAO3/+f8DAPD/+f8DAPP/+f8DAPb/+f8DAPn/+f8DAPz/+f8DAP//+f8DAAIA+f8DAAUA+f8DAAgA+f8DAAsA+f8DAA4A+f8DABEA+f8DABQA+f8DABcA+f8DABoA+f8DAB0A+f8DACAA+f8EAOD/+f8EAOP/+f8EAOb/+f8EAOn/+f8EAOz/+f8EAO//+f8EAPL/+f8EAPX/+f8EAPj/+f8EAPv/+f8EAP7/+f8EAAEA+f8EAAQA+f8EAAcA+f8EAAoA+f8EAA0A+f8EABAA+f8EABMA+f8EABYA+f8EABkA+f8EABwA+f8EAB8A+f8FAN//+f8FAOL/+f8FAOX/+f8FAOj/+f8FAOv/+f8FAO7/+f8FAPH/+f8FAPT/+f8FAPf/+f8FAPr/+f8FAP3/+f8FAAAA+f8FAAMA+f8FAAYA+f8FAAkA+f8FAAwA+f8FAA8A+f8FABIA+f8FABUA+f8FABgA+f8FABsA+f8FAB4A+f8FACEA+f8GAOH/+f8GAOT/+f8GAOf/+f8GAOr/+f8GAO3/+f8GAPD/+f8GAPP/+f8GAPb/+f8GAPn/+f8GAPz/+f8GAP//+f8GAAIA+f8GAAUA+f8GAAgA+f8GAAsA+f8GAA4A+f8GABEA+f8GABQA+f8GABcA+f8GABoA+f8GAB0A+f8GACAA+f8HAOD/+f8HAOP/+f8HAOb/+f8HAOn/+f8HAOz/+f8HAO//+f8HAPL/+f8HAPX/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+v8CAPL/+v8CAPX/+v8CAPj/+v8CAPv/+v8CAP7/+v8CAAEA+v8CAAQA+v8CAAcA+v8CAAoA+v8CAA0A+v8CABAA+v8CABMA+v8CABYA+v8CABkA+v8CABwA+v8CAB8A+v8CACIA+v8DAN//+v8DAOL/+v8DAOX/+v8DAOj/+v8DAOv/+v8DAO7/+v8DAPH/+v8DAPT/+v8DAPf/+v8DAPr/+v8DAP3/+v8DAAAA+v8DAAMA+v8DAAYA+v8DAAkA+v8DAAwA+v8DAA8A+v8DABIA+v8DABUA+v8DABgA+v8DABsA+v8DAB4A+v8DACEA+v8EAN7/+v8EAOH/+v8EAOT/+v8EAOf/+v8EAOr/+v8EAO3/+v8EAPD/+v8EAPP/+v8EAPb/+v8EAPn/+v8EAPz/+v8EAP//+v8EAAIA+v8EAAUA+v8EAAgA+v8EAAsA+v8EAA4A+v8EABEA+v8EABQA+v8EABcA+v8EABoA+v8EAB0A+v8EACAA+v8EACMA+v8FAOD/+v8FAOP/+v8FAOb/+v8FAOn/+v8FAOz/+v8FAO//+v8FAPL/+v8FAPX/+v8FAPj/+v8FAPv/+v8FAP7/+v8FAAEA+v8FAAQA+v8FAAcA+v8FAAoA+v8FAA0A+v8FABAA+v8FABMA+v8FABYA+v8FABkA+v8FABwA+v8FAB8A+v8FACIA+v8GAN//+v8GAOL/+v8GAOX/+v8GAOj/+v8GAOv/+v8GAO7/+v8GAPH/+v8GAPT/+v8GAPf/+v8GAPr/+v8GAP3/+v8GAAAA+v8GAAMA+v8GAAYA+v8GAAkA+v8GAAwA+v8GAA8A+v8GABIA+v8GABUA+v8GABgA+v8GABsA+v8GAB4A+v8GACEA+v8HAOH/+v8HAOT/+v8HAOf/+v8HAOr/+v8HAO3/+v8HAPD/+v8HAPP/+v8HAPb/+v8HAPn/+v8HAPz/+v8HAP//+v8HAAIA+v8HAAUA+v8HAAgA+v8HAAsA+v8HAA4A+v8HABEA+v8HABQA+v8HABcA+v8HABoA+v8HAB0A+v8HACAA+v8IAOP/+v8IAOb/+v8IAOn/+v8IAOz/+v8IAO//+v8IAPL/+v8IAPX/+v8IAPj/+v8IAPv/+v8IAP7/+v8IAAEA+v8IAAQA+v8IAAcA+v8IAAoA+v8IAA0A+v8IABAA+v8IABMA+v8IABYA+v8IABkA+v8IABwA+v8IAB8A+v8JAOX/+v8JAOj/+v8JAOv/+v8JAO7/+v8JAPH/+v8JAPT/+v8JAPf/+v8JAPr/+v8JAP3/+v8JAAAA+v8JAAMA+v8JAAYA+v8JAAkA+v8JAAwA+v8JAA8A+v8JABIA+v8JABUA+v8JABgA+v8JABsA+v8KAOf/+v8KAOr/+v8KAO3/+v8KAPD/+v8KAPP/+v8KAPb/+v8KAPn/+v8KAPz/+v8KAP//+v8KAAIA+v8KAAUA+v8KAAgA+v8KAAsA+v8KAA4A+v8KABEA+v8KABQA+v8KABcA+v8KABoA+v8LAOn/+v8LAOz/+v8LAO//+v8LAPL/+v8LAPX/+v8LAPj/+v8LAPv/+v8LAP7/+v8LAAEA+v8LAAQA+v8LAAcA+v8LAAoA+v8LAA0A+v8LABAA+v8LABMA+v8LABYA+v8MAOv/+v8MAO7/+v8MAPH/+v8MAPT/+v8MAPf/+v8MAPr/+v8MAP3/+v8MAAAA+v8MAAMA+v8MAAYA+v8MAAkA+v8MAAwA+v8MAA8A+v8MABIA+v8MABUA+v8NAPD/+v8NAPP/+v8NAPb/+v8NAPn/+v8NAPz/+v8NAP//+v8NAAIA+v8NAAUA+v8NAAgA+v8NAAsA+v8NAA4A+v8OAPv/+v8OAP7/+v8OAAEA+v8OAAQA+v8OAAcA+//3//7/+//3/wEA+//3/wQA+//4//H/+//4//T/+//4//f/+//4//r/+//4//3/+//4/wAA+//4/wMA+//4/wYA+//4/wkA+//4/wwA+//4/w8A+//5/+3/+//5//D/+//5//P/+//5//b/+//5//n/+//5//z/+//5////+//5/wIA+//5/wUA+//5/wgA+//5/wsA+//5/w4A+//5/xEA+//5/xQA+//6/+n/+//6/+z/+//6/+//+//6//L/+//6//X/+//6//j/+//6//v/+//6//7/+//6/wEA+//6/wQA+//6/wcA+//6/woA+//6/w0A+//6/xAA+//6/xMA+//6/xYA+//7/+X/+//7/+j/+//7/+v/+//7/+7/+//7//H/+//7//T/+//7//f/+//7//r/+//7//3/+//7/wAA+//7/wMA+//7/wYA+//7/wkA+//7/wwA+//7/w8A+//7/xIA+//7/xUA+//7/xgA+//7/xsA+//8/+T/+//8/+f/+//8/+r/+//8/+3/+//8//D/+//8//P/+//8//b/+//8//n/+//8//z/+//8////+//8/wIA+//8/wUA+//8/wgA+//8/wsA+//8/w4A+//8/xEA+//8/xQA+//8/xcA+//8/xoA+//8/x0A+//9/+D/+//9/+P/+//9/+b/+//9/+n/+//9/+z/+//9/+//+//9//L/+//9//X/+//9//j/+//9//v/+//9//7/+//9/wEA+//9/wQA+//9/wcA+//9/woA+//9/w0A+//9/xAA+//9/xMA+//9/xYA+//9/xkA+//9/xwA+//9/x8A+//+/9//+//+/+L/+//+/+X/+//+/+j/+//+/+v/+//+/+7/+//+//H/+//+//T/+//+//f/+//+//r/+//+//3/+//+/wAA+//+/wMA+//+/wYA+//+/wkA+//+/wwA+//+/w8A+//+/xIA+//+/xUA+//+/xgA+//+/xsA+//+/x4A+//+/yEA+////97/+////+H/+////+T/+////+f/+////+r/+////+3/+/////D/+/////P/+/////b/+/////n/+/////z/+///////+////wIA+////wUA+////wgA+////wsA+////w4A+////xEA+////xQA+////xcA+////xoA+////x0A+////yAA+/8AAN3/+/8AAOD/+/8AAOP/+/8AAOb/+/8AAOn/+/8AAOz/+/8AAO//+/8AAPL/+/8AAPX/+/8AAPj/+/8AAPv/+/8AAP7/+/8AAAEA+/8AAAQA+/8AAAcA+/8AAAoA+/8AAA0A+/8AABAA+/8AABMA+/8AABYA+/8AABkA+/8AABwA+/8AAB8A+/8AACIA+/8BANz/+/8BAN//+/8BAOL/+/8BAOX/+/8BAOj/+/8BAOv/+/8BAO7/+/8BAPH/+/8BAPT/+/8BAPf/+/8BAPr/+/8BAP3/+/8BAAAA+/8BAAMA+/8BAAYA+/8BAAkA+/8BAAwA+/8BAA8A+/8BABIA+/8BABUA+/8BABgA+/8BABsA+/8BAB4A+/8BACEA+/8BACQA+/8CAN7/+/8CAOH/+/8CAOT/+/8CAOf/+/8CAOr/+/8CAO3/+/8CAPD/+/8CAPP/+/8CAPb/+/8CAPn/+/8CAPz/+/8CAP//+/8CAAIA+/8CAAUA+/8CAAgA+/8CAAsA+/8CAA4A+/8CABEA+/8CABQA+/8CABcA+/8CABoA+/8CAB0A+/8CACAA+/8CACMA+/8DAN3/+/8DAOD/+/8DAOP/+/8DAOb/+/8DAOn/+/8DAOz/+/8DAO//+/8DAPL/+/8DAPX/+/8DAPj/+/8DAPv/+/8DAP7/+/8DAAEA+/8DAAQA+/8DAAcA+/8DAAoA+/8DAA0A+/8DABAA+/8DABMA+/8DABYA+/8DABkA+/8DABwA+/8DAB8A+/8DACIA+/8EANz/+/8EAN//+/8EAOL/+/8EAOX/+/8EAOj/+/8EAOv/+/8EAO7/+/8EAPH/+/8EAPT/+/8EAPf/+/8EAPr/+/8EAP3/+/8EAAAA+/8EAAMA+/8EAAYA+/8EAAkA+/8EAAwA+/8EAA8A+/8EABIA+/8EABUA+/8EABgA+/8EABsA+/8EAB4A+/8EACEA+/8EACQA+/8FAN7/+/8FAOH/+/8FAOT/+/8FAOf/+/8FAOr/+/8FAO3/+/8FAPD/+/8FAPP/+/8FAPb/+/8FAPn/+/8FAPz/+/8FAP//+/8FAAIA+/8FAAUA+/8FAAgA+/8FAAsA+/8FAA4A+/8FABEA+/8FABQA+/8FABcA+/8FABoA+/8FAB0A+/8FACAA+/8FACMA+/8GAOD/+/8GAOP/+/8GAOb/+/8GAOn/+/8GAOz/+/8GAO//+/8GAPL/+/8GAPX/+/8GAPj/+/8GAPv/+/8GAP7/+/8GAAEA+/8GAAQA+/8GAAcA+/8GAAoA+/8GAA0A+/8GABAA+/8GABMA+/8GABYA+/8GABkA+/8GABwA+/8GAB8A+/8GACIA+/8HAN//+/8HAOL/+/8HAOX/+/8HAOj/+/8HAOv/+/8HAO7/+/8HAPH/+/8HAPT/+/8HAPf/+/8HAPr/+/8HAP3/+/8HAAAA+/8HAAMA+/8HAAYA+/8HAAkA+/8HAAwA+/8HAA8A+/8HABIA+/8HABUA+/8HABgA+/8HABsA+/8HAB4A+/8HACEA+/8IAOH/+/8IAOT/+/8IAOf/+/8IAOr/+/8IAO3/+/8IAPD/+/8IAPP/+/8IAPb/+/8IAPn/+/8IAPz/+/8IAP//+/8IAAIA+/8IAAUA+/8IAAgA+/8IAAsA+/8IAA4A+/8IABEA+/8IABQA+/8IABcA+/8IABoA+/8IAB0A+/8IACAA+/8JAOP/+/8JAOb/+/8JAOn/+/8JAOz/+/8JAO//+/8JAPL/+/8JAPX/+/8JAPj/+/8JAPv/+/8JAP7/+/8JAAEA+/8JAAQA+/8JAAcA+/8JAAoA+/8JAA0A+/8JABAA+/8JABMA+/8JABYA+/8JABkA+/8JABwA+/8KAOX/+/8KAOj/+/8KAOv/+/8KAO7/+/8KAPH/+/8KAPT/+/8KAPf/+/8KAPr/+/8KAP3/+/8KAAAA+/8KAAMA+/8KAAYA+/8KAAkA+/8KAAwA+/8KAA8A+/8KABIA+/8KABUA+/8KABgA+/8KABsA+/8LAOr/+/8LAO3/+/8LAPD/+/8LAPP/+/8LAPb/+/8LAPn/+/8LAPz/+/8LAP//+/8LAAIA+/8LAAUA+/8LAAgA+/8LAAsA+/8LAA4A+/8LABEA+/8LABQA+/8LABcA+/8MAOz/+/8MAO//+/8MAPL/+/8MAPX/+/8MAPj/+/8MAPv/+/8MAP7/+/8MAAEA+/8MAAQA+/8MAAcA+/8MAAoA+/8MAA0A+/8MABAA+/8MABMA+/8NAPH/+/8NAPT/+/8NAPf/+/8NAPr/+/8NAP3/+/8NAAAA+/8NAAMA+/8NAAYA+/8NAAkA+/8NAAwA+/8NAA8A+/8OAPz/+/8OAP//+/8OAAIA/P/3//P//P/3//b//P/3//n//P/3//z//P/3/////P/3/wIA/P/3/wUA/P/3/wgA/P/3/wsA/P/3/w4A/P/4/+z//P/4/+///P/4//L//P/4//X//P/4//j//P/4//v//P/4//7//P/4/wEA/P/4/wQA/P/4/wcA/P/4/woA/P/4/w0A/P/4/xAA/P/4/xMA/P/5/+j//P/5/+v//P/5/+7//P/5//H//P/5//T//P/5//f//P/5//r//P/5//3//P/5/wAA/P/5/wMA/P/5/wYA/P/5/wkA/P/5/wwA/P/5/w8A/P/5/xIA/P/5/xUA/P/5/xgA/P/6/+f//P/6/+r//P/6/+3//P/6//D//P/6//P//P/6//b//P/6//n//P/6//z//P/6/////P/6/wIA/P/6/wUA/P/6/wgA/P/6/wsA/P/6/w4A/P/6/xEA/P/6/xQA/P/6/xcA/P/6/xoA/P/7/+P//P/7/+b//P/7/+n//P/7/+z//P/7/+///P/7//L//P/7//X//P/7//j//P/7//v//P/7//7//P/7/wEA/P/7/wQA/P/7/wcA/P/7/woA/P/7/w0A/P/7/xAA/P/7/xMA/P/7/xYA/P/7/xkA/P/7/xwA/P/8/+L//P/8/+X//P/8/+j//P/8/+v//P/8/+7//P/8//H//P/8//T//P/8//f//P/8//r//P/8//3//P/8/wAA/P/8/wMA/P/8/wYA/P/8/wkA/P/8/wwA/P/8/w8A/P/8/xIA/P/8/xUA/P/8/xgA/P/8/xsA/P/8/x4A/P/9/+H//P/9/+T//P/9/+f//P/9/+r//P/9/+3//P/9//D//P/9//P//P/9//b//P/9//n//P/9//z//P/9/////P/9/wIA/P/9/wUA/P/9/wgA/P/9/wsA/P/9/w4A/P/9/xEA/P/9/xQA/P/9/xcA/P/9/xoA/P/9/x0A/P/9/yAA/P/+/93//P/+/+D//P/+/+P//P/+/+b//P/+/+n//P/+/+z//P/+/+///P/+//L//P/+//X//P/+//j//P/+//v//P/+//7//P/+/wEA/P/+/wQA/P/+/wcA/P/+/woA/P/+/w0A/P/+/xAA/P/+/xMA/P/+/xYA/P/+/xkA/P/+/xwA/P/+/x8A/P/+/yIA/P///9z//P///9///P///+L//P///+X//P///+j//P///+v//P///+7//P////H//P////T//P////f//P////r//P////3//P///wAA/P///wMA/P///wYA/P///wkA/P///wwA/P///w8A/P///xIA/P///xUA/P///xgA/P///xsA/P///x4A/P///yEA/P///yQA/P8AAN7//P8AAOH//P8AAOT//P8AAOf//P8AAOr//P8AAO3//P8AAPD//P8AAPP//P8AAPb//P8AAPn//P8AAPz//P8AAP///P8AAAIA/P8AAAUA/P8AAAgA/P8AAAsA/P8AAA4A/P8AABEA/P8AABQA/P8AABcA/P8AABoA/P8AAB0A/P8AACAA/P8AACMA/P8BAN3//P8BAOD//P8BAOP//P8BAOb//P8BAOn//P8BAOz//P8BAO///P8BAPL//P8BAPX//P8BAPj//P8BAPv//P8BAP7//P8BAAEA/P8BAAQA/P8BAAcA/P8BAAoA/P8BAA0A/P8BABAA/P8BABMA/P8BABYA/P8BABkA/P8BABwA/P8BAB8A/P8BACIA/P8BACUA/P8CANz//P8CAN///P8CAOL//P8CAOX//P8CAOj//P8CAOv//P8CAO7//P8CAPH//P8CAPT//P8CAPf//P8CAPr//P8CAP3//P8CAAAA/P8CAAMA/P8CAAYA/P8CAAkA/P8CAAwA/P8CAA8A/P8CABIA/P8CABUA/P8CABgA/P8CABsA/P8CAB4A/P8CACEA/P8CACQA/P8DANv//P8DAN7//P8DAOH//P8DAOT//P8DAOf//P8DAOr//P8DAO3//P8DAPD//P8DAPP//P8DAPb//P8DAPn//P8DAPz//P8DAP///P8DAAIA/P8DAAUA/P8DAAgA/P8DAAsA/P8DAA4A/P8DABEA/P8DABQA/P8DABcA/P8DABoA/P8DAB0A/P8DACAA/P8DACMA/P8EAN3//P8EAOD//P8EAOP//P8EAOb//P8EAOn//P8EAOz//P8EAO///P8EAPL//P8EAPX//P8EAPj//P8EAPv//P8EAP7//P8EAAEA/P8EAAQA/P8EAAcA/P8EAAoA/P8EAA0A/P8EABAA/P8EABMA/P8EABYA/P8EABkA/P8EABwA/P8EAB8A/P8EACIA/P8FANz//P8FAN///P8FAOL//P8FAOX//P8FAOj//P8FAOv//P8FAO7//P8FAPH//P8FAPT//P8FAPf//P8FAPr//P8FAP3//P8FAAAA/P8FAAMA/P8FAAYA/P8FAAkA/P8FAAwA/P8FAA8A/P8FABIA/P8FABUA/P8FABgA/P8FABsA/P8FAB4A/P8FACEA/P8FACQA/P8GAN7//P8GAOH//P8GAOT//P8GAOf//P8GAOr//P8GAO3//P8GAPD//P8GAPP//P8GAPb//P8GAPn//P8GAPz//P8GAP///P8GAAIA/P8GAAUA/P8GAAgA/P8GAAsA/P8GAA4A/P8GABEA/P8GABQA/P8GABcA/P8GABoA/P8GAB0A/P8GACAA/P8GACMA/P8HAOD//P8HAOP//P8HAOb//P8HAOn//P8HAOz//P8HAO///P8HAPL//P8HAPX//P8HAPj//P8HAPv//P8HAP7//P8HAAEA/P8HAAQA/P8HAAcA/P8HAAoA/P8HAA0A/P8HABAA/P8HABMA/P8HABYA/P8HABkA/P8HABwA/P8HAB8A/P8IAOL//P8IAOX//P8IAOj//P8IAOv//P8IAO7//P8IAPH//P8IAPT//P8IAPf//P8IAPr//P8IAP3//P8IAAAA/P8IAAMA/P8IAAYA/P8IAAkA/P8IAAwA/P8IAA8A/P8IABIA/P8IABUA/P8IABgA/P8IABsA/P8IAB4A/P8JAOT//P8JAOf//P8JAOr//P8JAO3//P8JAPD//P8JAPP//P8JAPb//P8JAPn//P8JAPz//P8JAP///P8JAAIA/P8JAAUA/P8JAAgA/P8JAAsA/P8JAA4A/P8JABEA/P8JABQA/P8JABcA/P8JABoA/P8JAB0A/P8KAOb//P8KAOn//P8KAOz//P8KAO///P8KAPL//P8KAPX//P8KAPj//P8KAPv//P8KAP7//P8KAAEA/P8KAAQA/P8KAAcA/P8KAAoA/P8KAA0A/P8KABAA/P8KABMA/P8KABYA/P8KABkA/P8LAOj//P8LAOv//P8LAO7//P8LAPH//P8LAPT//P8LAPf//P8LAPr//P8LAP3//P8LAAAA/P8LAAMA/P8LAAYA/P8LAAkA/P8LAAwA/P8LAA8A/P8LABIA/P8LABUA/P8LABgA/P8MAO3//P8MAPD//P8MAPP//P8MAPb//P8MAPn//P8MAPz//P8MAP///P8MAAIA/P8MAAUA/P8MAAgA/P8MAAsA/P8MAA4A/P8MABEA/P8MABQA/P8NAPL//P8NAPX//P8NAPj//P8NAPv//P8NAP7//P8NAAEA/P8NAAQA/P8NAAcA/P8NAAoA/P8NAA0A/f/2//X//f/2//j//f/2//v//f/2//7//f/2/wEA/f/2/wQA/f/2/wcA/f/2/woA/f/3/+7//f/3//H//f/3//T//f/3//f//f/3//r//f/3//3//f/3/wAA/f/3/wMA/f/3/wYA/f/3/wkA/f/3/wwA/f/3/w8A/f/3/xIA/f/4/+r//f/4/+3//f/4//D//f/4//P//f/4//b//f/4//n//f/4//z//f/4/////f/4/wIA/f/4/wUA/f/4/wgA/f/4/wsA/f/4/w4A/f/4/xEA/f/4/xQA/f/4/xcA/f/5/+b//f/5/+n//f/5/+z//f/5/+///f/5//L//f/5//X//f/5//j//f/5//v//f/5//7//f/5/wEA/f/5/wQA/f/5/wcA/f/5/woA/f/5/w0A/f/5/xAA/f/5/xMA/f/5/xYA/f/5/xkA/f/6/+X//f/6/+j//f/6/+v//f/6/+7//f/6//H//f/6//T//f/6//f//f/6//r//f/6//3//f/6/wAA/f/6/wMA/f/6/wYA/f/6/wkA/f/6/wwA/f/6/w8A/f/6/xIA/f/6/xUA/f/6/xgA/f/6/xsA/f/7/+H//f/7/+T//f/7/+f//f/7/+r//f/7/+3//f/7//D//f/7//P//f/7//b//f/7//n//f/7//z//f/7/////f/7/wIA/f/7/wUA/f/7/wgA/f/7/wsA/f/7/w4A/f/7/xEA/f/7/xQA/f/7/xcA/f/7/xoA/f/7/x0A/f/7/yAA/f/8/+D//f/8/+P//f/8/+b//f/8/+n//f/8/+z//f/8/+///f/8//L//f/8//X//f/8//j//f/8//v//f/8//7//f/8/wEA/f/8/wQA/f/8/wcA/f/8/woA/f/8/w0A/f/8/xAA/f/8/xMA/f/8/xYA/f/8/xkA/f/8/xwA/f/8/x8A/f/9/9///f/9/+L//f/9/+X//f/9/+j//f/9/+v//f/9/+7//f/9//H//f/9//T//f/9//f//f/9//r//f/9//3//f/9/wAA/f/9/wMA/f/9/wYA/f/9/wkA/f/9/wwA/f/9/w8A/f/9/xIA/f/9/xUA/f/9/xgA/f/9/xsA/f/9/x4A/f/9/yEA/f/+/97//f/+/+H//f/+/+T//f/+/+f//f/+/+r//f/+/+3//f/+//D//f/+//P//f/+//b//f/+//n//f/+//z//f/+/////f/+/wIA/f/+/wUA/f/+/wgA/f/+/wsA/f/+/w4A/f/+/xEA/f/+/xQA/f/+/xcA/f/+/xoA/f/+/x0A/f/+/yAA/f/+/yMA/f///93//f///+D//f///+P//f///+b//f///+n//f///+z//f///+///f////L//f////X//f////j//f////v//f////7//f///wEA/f///wQA/f///wcA/f///woA/f///w0A/f///xAA/f///xMA/f///xYA/f///xkA/f///xwA/f///x8A/f///yIA/f///yUA/f8AANz//f8AAN///f8AAOL//f8AAOX//f8AAOj//f8AAOv//f8AAO7//f8AAPH//f8AAPT//f8AAPf//f8AAPr//f8AAP3//f8AAAAA/f8AAAMA/f8AAAYA/f8AAAkA/f8AAAwA/f8AAA8A/f8AABIA/f8AABUA/f8AABgA/f8AABsA/f8AAB4A/f8AACEA/f8AACQA/f8BANv//f8BAN7//f8BAOH//f8BAOT//f8BAOf//f8BAOr//f8BAO3//f8BAPD//f8BAPP//f8BAPb//f8BAPn//f8BAPz//f8BAP///f8BAAIA/f8BAAUA/f8BAAgA/f8BAAsA/f8BAA4A/f8BABEA/f8BABQA/f8BABcA/f8BABoA/f8BAB0A/f8BACAA/f8BACMA/f8BACYA/f8CANr//f8CAN3//f8CAOD//f8CAOP//f8CAOb//f8CAOn//f8CAOz//f8CAO///f8CAPL//f8CAPX//f8CAPj//f8CAPv//f8CAP7//f8CAAEA/f8CAAQA/f8CAAcA/f8CAAoA/f8CAA0A/f8CABAA/f8CABMA/f8CABYA/f8CABkA/f8CABwA/f8CAB8A/f8CACIA/f8CACUA/f8DANz//f8DAN///f8DAOL//f8DAOX//f8DAOj//f8DAOv//f8DAO7//f8DAPH//f8DAPT//f8DAPf//f8DAPr//f8DAP3//f8DAAAA/f8DAAMA/f8DAAYA/f8DAAkA/f8DAAwA/f8DAA8A/f8DABIA/f8DABUA/f8DABgA/f8DABsA/f8DAB4A/f8DACEA/f8DACQA/f8EANv//f8EAN7//f8EAOH//f8EAOT//f8EAOf//f8EAOr//f8EAO3//f8EAPD//f8EAPP//f8EAPb//f8EAPn//f8EAPz//f8EAP///f8EAAIA/f8EAAUA/f8EAAgA/f8EAAsA/f8EAA4A/f8EABEA/f8EABQA/f8EABcA/f8EABoA/f8EAB0A/f8EACAA/f8EACMA/f8FAN3//f8FAOD//f8FAOP//f8FAOb//f8FAOn//f8FAOz//f8FAO///f8FAPL//f8FAPX//f8FAPj//f8FAPv//f8FAP7//f8FAAEA/f8FAAQA/f8FAAcA/f8FAAoA/f8FAA0A/f8FABAA/f8FABMA/f8FABYA/f8FABkA/f8FABwA/f8FAB8A/f8FACIA/f8GAN///f8GAOL//f8GAOX//f8GAOj//f8GAOv//f8GAO7//f8GAPH//f8GAPT//f8GAPf//f8GAPr//f8GAP3//f8GAAAA/f8GAAMA/f8GAAYA/f8GAAkA/f8GAAwA/f8GAA8A/f8GABIA/f8GABUA/f8GABgA/f8GABsA/f8GAB4A/f8GACEA/f8HAOH//f8HAOT//f8HAOf//f8HAOr//f8HAO3//f8HAPD//f8HAPP//f8HAPb//f8HAPn//f8HAPz//f8HAP///f8HAAIA/f8HAAUA/f8HAAgA/f8HAAsA/f8HAA4A/f8HABEA/f8HABQA/f8HABcA/f8HABoA/f8HAB0A/f8HACAA/f8IAOD//f8IAOP//f8IAOb//f8IAOn//f8IAOz//f8IAO///f8IAPL//f8IAPX//f8IAPj//f8IAPv//f8IAP7//f8IAAEA/f8IAAQA/f8IAAcA/f8IAAoA/f8IAA0A/f8IABAA/f8IABMA/f8IABYA/f8IABkA/f8IABwA/f8IAB8A/f8JAOX//f8JAOj//f8JAOv//f8JAO7//f8JAPH//f8JAPT//f8JAPf//f8JAPr//f8JAP3//f8JAAAA/f8JAAMA/f8JAAYA/f8JAAkA/f8JAAwA/f8JAA8A/f8JABIA/f8JABUA/f8JABgA/f8JABsA/f8KAOf//f8KAOr//f8KAO3//f8KAPD//f8KAPP//f8KAPb//f8KAPn//f8KAPz//f8KAP///f8KAAIA/f8KAAUA/f8KAAgA/f8KAAsA/f8KAA4A/f8KABEA/f8KABQA/f8KABcA/f8KABoA/f8LAOn//f8LAOz//f8LAO///f8LAPL//f8LAPX//f8LAPj//f8LAPv//f8LAP7//f8LAAEA/f8LAAQA/f8LAAcA/f8LAAoA/f8LAA0A/f8LABAA/f8LABMA/f8LABYA/f8MAO7//f8MAPH//f8MAPT//f8MAPf//f8MAPr//f8MAP3//f8MAAAA/f8MAAMA/f8MAAYA/f8MAAkA/f8MAAwA/f8MAA8A/f8MABIA/f8NAPb//f8NAPn//f8NAPz//f8NAP///f8NAAIA/f8NAAUA/f8NAAgA/f8NAAsA/v/1//r//v/1//3//v/1/wAA/v/1/wMA/v/1/wYA/v/2//D//v/2//P//v/2//b//v/2//n//v/2//z//v/2/////v/2/wIA/v/2/wUA/v/2/wgA/v/2/wsA/v/2/w4A/v/3/+z//v/3/+///v/3//L//v/3//X//v/3//j//v/3//v//v/3//7//v/3/wEA/v/3/wQA/v/3/wcA/v/3/woA/v/3/w0A/v/3/xAA/v/3/xMA/v/3/xYA/v/4/+j//v/4/+v//v/4/+7//v/4//H//v/4//T//v/4//f//v/4//r//v/4//3//v/4/wAA/v/4/wMA/v/4/wYA/v/4/wkA/v/4/wwA/v/4/w8A/v/4/xIA/v/4/xUA/v/4/xgA/v/5/+T//v/5/+f//v/5/+r//v/5/+3//v/5//D//v/5//P//v/5//b//v/5//n//v/5//z//v/5/////v/5/wIA/v/5/wUA/v/5/wgA/v/5/wsA/v/5/w4A/v/5/xEA/v/5/xQA/v/5/xcA/v/5/xoA/v/5/x0A/v/6/+P//v/6/+b//v/6/+n//v/6/+z//v/6/+///v/6//L//v/6//X//v/6//j//v/6//v//v/6//7//v/6/wEA/v/6/wQA/v/6/wcA/v/6/woA/v/6/w0A/v/6/xAA/v/6/xMA/v/6/xYA/v/6/xkA/v/6/xwA/v/6/x8A/v/7/9///v/7/+L//v/7/+X//v/7/+j//v/7/+v//v/7/+7//v/7//H//v/7//T//v/7//f//v/7//r//v/7//3//v/7/wAA/v/7/wMA/v/7/wYA/v/7/wkA/v/7/wwA/v/7/w8A/v/7/xIA/v/7/xUA/v/7/xgA/v/7/xsA/v/7/x4A/v/7/yEA/v/8/97//v/8/+H//v/8/+T//v/8/+f//v/8/+r//v/8/+3//v/8//D//v/8//P//v/8//b//v/8//n//v/8//z//v/8/////v/8/wIA/v/8/wUA/v/8/wgA/v/8/wsA/v/8/w4A/v/8/xEA/v/8/xQA/v/8/xcA/v/8/xoA/v/8/x0A/v/8/yAA/v/8/yMA/v/9/93//v/9/+D//v/9/+P//v/9/+b//v/9/+n//v/9/+z//v/9/+///v/9//L//v/9//X//v/9//j//v/9//v//v/9//7//v/9/wEA/v/9/wQA/v/9/wcA/v/9/woA/v/9/w0A/v/9/xAA/v/9/xMA/v/9/xYA/v/9/xkA/v/9/xwA/v/9/x8A/v/9/yIA/v/+/9z//v/+/9///v/+/+L//v/+/+X//v/+/+j//v/+/+v//v/+/+7//v/+//H//v/+//T//v/+//f//v/+//r//v/+//3//v/+/wAA/v/+/wMA/v/+/wYA/v/+/wkA/v/+/wwA/v/+/w8A/v/+/xIA/v/+/xUA/v/+/xgA/v/+/xsA/v/+/x4A/v/+/yEA/v/+/yQA/v///9v//v///97//v///+H//v///+T//v///+f//v///+r//v///+3//v////D//v////P//v////b//v////n//v////z//v///////v///wIA/v///wUA/v///wgA/v///wsA/v///w4A/v///xEA/v///xQA/v///xcA/v///xoA/v///x0A/v///yAA/v///yMA/v///yYA/v8AANr//v8AAN3//v8AAOD//v8AAOP//v8AAOb//v8AAOn//v8AAOz//v8AAO///v8AAPL//v8AAPX//v8AAPj//v8AAPv//v8AAP7//v8AAAEA/v8AAAQA/v8AAAcA/v8AAAoA/v8AAA0A/v8AABAA/v8AABMA/v8AABYA/v8AABkA/v8AABwA/v8AAB8A/v8AACIA/v8AACUA/v8BANz//v8BAN///v8BAOL//v8BAOX//v8BAOj//v8BAOv//v8BAO7//v8BAPH//v8BAPT//v8BAPf//v8BAPr//v8BAP3//v8BAAAA/v8BAAMA/v8BAAYA/v8BAAkA/v8BAAwA/v8BAA8A/v8BABIA/v8BABUA/v8BABgA/v8BABsA/v8BAB4A/v8BACEA/v8BACQA/v8CANv//v8CAN7//v8CAOH//v8CAOT//v8CAOf//v8CAOr//v8CAO3//v8CAPD//v8CAPP//v8CAPb//v8CAPn//v8CAPz//v8CAP///v8CAAIA/v8CAAUA/v8CAAgA/v8CAAsA/v8CAA4A/v8CABEA/v8CABQA/v8CABcA/v8CABoA/v8CAB0A/v8CACAA/v8CACMA/v8CACYA/v8DANr//v8DAN3//v8DAOD//v8DAOP//v8DAOb//v8DAOn//v8DAOz//v8DAO///v8DAPL//v8DAPX//v8DAPj//v8DAPv//v8DAP7//v8DAAEA/v8DAAQA/v8DAAcA/v8DAAoA/v8DAA0A/v8DABAA/v8DABMA/v8DABYA/v8DABkA/v8DABwA/v8DAB8A/v8DACIA/v8DACUA/v8EANz//v8EAN///v8EAOL//v8EAOX//v8EAOj//v8EAOv//v8EAO7//v8EAPH//v8EAPT//v8EAPf//v8EAPr//v8EAP3//v8EAAAA/v8EAAMA/v8EAAYA/v8EAAkA/v8EAAwA/v8EAA8A/v8EABIA/v8EABUA/v8EABgA/v8EABsA/v8EAB4A/v8EACEA/v8EACQA/v8FAN7//v8FAOH//v8FAOT//v8FAOf//v8FAOr//v8FAO3//v8FAPD//v8FAPP//v8FAPb//v8FAPn//v8FAPz//v8FAP///v8FAAIA/v8FAAUA/v8FAAgA/v8FAAsA/v8FAA4A/v8FABEA/v8FABQA/v8FABcA/v8FABoA/v8FAB0A/v8FACAA/v8FACMA/v8GAN3//v8GAOD//v8GAOP//v8GAOb//v8GAOn//v8GAOz//v8GAO///v8GAPL//v8GAPX//v8GAPj//v8GAPv//v8GAP7//v8GAAEA/v8GAAQA/v8GAAcA/v8GAAoA/v8GAA0A/v8GABAA/v8GABMA/v8GABYA/v8GABkA/v8GABwA/v8GAB8A/v8GACIA/v8HAN///v8HAOL//v8HAOX//v8HAOj//v8HAOv//v8HAO7//v8HAPH//v8HAPT//v8HAPf//v8HAPr//v8HAP3//v8HAAAA/v8HAAMA/v8HAAYA/v8HAAkA/v8HAAwA/v8HAA8A/v8HABIA/v8HABUA/v8HABgA/v8HABsA/v8HAB4A/v8HACEA/v8IAOH//v8IAOT//v8IAOf//v8IAOr//v8IAO3//v8IAPD//v8IAPP//v8IAPb//v8IAPn//v8IAPz//v8IAP///v8IAAIA/v8IAAUA/v8IAAgA/v8IAAsA/v8IAA4A/v8IABEA/v8IABQA/v8IABcA/v8IABoA/v8IAB0A/v8JAOP//v8JAOb//v8JAOn//v8JAOz//v8JAO///v8JAPL//v8JAPX//v8JAPj//v8JAPv//v8JAP7//v8JAAEA/v8JAAQA/v8JAAcA/v8JAAoA/v8JAA0A/v8JABAA/v8JABMA/v8JABYA/v8JABkA/v8JABwA/v8KAOj//v8KAOv//v8KAO7//v8KAPH//v8KAPT//v8KAPf//v8KAPr//v8KAP3//v8KAAAA/v8KAAMA/v8KAAYA/v8KAAkA/v8KAAwA/v8KAA8A/v8KABIA/v8KABUA/v8KABgA/v8LAOr//v8LAO3//v8LAPD//v8LAPP//v8LAPb//v8LAPn//v8LAPz//v8LAP///v8LAAIA/v8LAAUA/v8LAAgA/v8LAAsA/v8LAA4A/v8LABEA/v8LABQA/v8MAPL//v8MAPX//v8MAPj//v8MAPv//v8MAP7//v8MAAEA/v8MAAQA/v8MAAcA/v8MAAoA/v8MAA0A/v8MABAA/v8NAPr//v8NAP3//v8NAAAA/v8NAAMA/v8NAAYA///1//L////1//X////1//j////1//v////1//7////1/wEA///1/wQA///1/wcA///1/woA///1/w0A///2/+7////2//H////2//T////2//f////2//r////2//3////2/wAA///2/wMA///2/wYA///2/wkA///2/wwA///2/w8A///2/xIA///3/+r////3/+3////3//D////3//P////3//b////3//n////3//z////3///////3/wIA///3/wUA///3/wgA///3/wsA///3/w4A///3/xEA///3/xQA///3/xcA///4/+b////4/+n////4/+z////4/+/////4//L////4//X////4//j////4//v////4//7////4/wEA///4/wQA///4/wcA///4/woA///4/w0A///4/xAA///4/xMA///4/xYA///4/xkA///4/xwA///5/+L////5/+X////5/+j////5/+v////5/+7////5//H////5//T////5//f////5//r////5//3////5/wAA///5/wMA///5/wYA///5/wkA///5/wwA///5/w8A///5/xIA///5/xUA///5/xgA///5/xsA///5/x4A///6/+H////6/+T////6/+f////6/+r////6/+3////6//D////6//P////6//b////6//n////6//z////6///////6/wIA///6/wUA///6/wgA///6/wsA///6/w4A///6/xEA///6/xQA///6/xcA///6/xoA///6/x0A///6/yAA///7/+D////7/+P////7/+b////7/+n////7/+z////7/+/////7//L////7//X////7//j////7//v////7//7////7/wEA///7/wQA///7/wcA///7/woA///7/w0A///7/xAA///7/xMA///7/xYA///7/xkA///7/xwA///7/x8A///7/yIA///8/9z////8/9/////8/+L////8/+X////8/+j////8/+v////8/+7////8//H////8//T////8//f////8//r////8//3////8/wAA///8/wMA///8/wYA///8/wkA///8/wwA///8/w8A///8/xIA///8/xUA///8/xgA///8/xsA///8/x4A///8/yEA///8/yQA///9/9v////9/97////9/+H////9/+T////9/+f////9/+r////9/+3////9//D////9//P////9//b////9//n////9//z////9///////9/wIA///9/wUA///9/wgA///9/wsA///9/w4A///9/xEA///9/xQA///9/xcA///9/xoA///9/x0A///9/yAA///9/yMA///+/9r////+/93////+/+D////+/+P////+/+b////+/+n////+/+z////+/+/////+//L////+//X////+//j////+//v////+//7////+/wEA///+/wQA///+/wcA///+/woA///+/w0A///+/xAA///+/xMA///+/xYA///+/xkA///+/xwA///+/x8A///+/yIA///+/yUA/////9z//////9///////+L//////+X//////+j//////+v//////+7///////H///////T///////f///////r///////3//////wAA/////wMA/////wYA/////wkA/////wwA/////w8A/////xIA/////xUA/////xgA/////xsA/////x4A/////yEA/////yQA//8AANv///8AAN7///8AAOH///8AAOT///8AAOf///8AAOr///8AAO3///8AAPD///8AAPP///8AAPb///8AAPn///8AAPz///8AAP////8AAAIA//8AAAUA//8AAAgA//8AAAsA//8AAA4A//8AABEA//8AABQA//8AABcA//8AABoA//8AAB0A//8AACAA//8AACMA//8AACYA//8BANr///8BAN3///8BAOD///8BAOP///8BAOb///8BAOn///8BAOz///8BAO////8BAPL///8BAPX///8BAPj///8BAPv///8BAP7///8BAAEA//8BAAQA//8BAAcA//8BAAoA//8BAA0A//8BABAA//8BABMA//8BABYA//8BABkA//8BABwA//8BAB8A//8BACIA//8BACUA//8CANz///8CAN////8CAOL///8CAOX///8CAOj///8CAOv///8CAO7///8CAPH///8CAPT///8CAPf///8CAPr///8CAP3///8CAAAA//8CAAMA//8CAAYA//8CAAkA//8CAAwA//8CAA8A//8CABIA//8CABUA//8CABgA//8CABsA//8CAB4A//8CACEA//8CACQA//8DANv///8DAN7///8DAOH///8DAOT///8DAOf///8DAOr///8DAO3///8DAPD///8DAPP///8DAPb///8DAPn///8DAPz///8DAP////8DAAIA//8DAAUA//8DAAgA//8DAAsA//8DAA4A//8DABEA//8DABQA//8DABcA//8DABoA//8DAB0A//8DACAA//8DACMA//8DACYA//8EAN3///8EAOD///8EAOP///8EAOb///8EAOn///8EAOz///8EAO////8EAPL///8EAPX///8EAPj///8EAPv///8EAP7///8EAAEA//8EAAQA//8EAAcA//8EAAoA//8EAA0A//8EABAA//8EABMA//8EABYA//8EABkA//8EABwA//8EAB8A//8EACIA//8EACUA//8FANz///8FAN////8FAOL///8FAOX///8FAOj///8FAOv///8FAO7///8FAPH///8FAPT///8FAPf///8FAPr///8FAP3///8FAAAA//8FAAMA//8FAAYA//8FAAkA//8FAAwA//8FAA8A//8FABIA//8FABUA//8FABgA//8FABsA//8FAB4A//8FACEA//8FACQA//8GAN7///8GAOH///8GAOT///8GAOf///8GAOr///8GAO3///8GAPD///8GAPP///8GAPb///8GAPn///8GAPz///8GAP////8GAAIA//8GAAUA//8GAAgA//8GAAsA//8GAA4A//8GABEA//8GABQA//8GABcA//8GABoA//8GAB0A//8GACAA//8HAOD///8HAOP///8HAOb///8HAOn///8HAOz///8HAO////8HAPL///8HAPX///8HAPj///8HAPv///8HAP7///8HAAEA//8HAAQA//8HAAcA//8HAAoA//8HAA0A//8HABAA//8HABMA//8HABYA//8HABkA//8HABwA//8HAB8A//8IAOL///8IAOX///8IAOj///8IAOv///8IAO7///8IAPH///8IAPT///8IAPf///8IAPr///8IAP3///8IAAAA//8IAAMA//8IAAYA//8IAAkA//8IAAwA//8IAA8A//8IABIA//8IABUA//8IABgA//8IABsA//8IAB4A//8JAOT///8JAOf///8JAOr///8JAO3///8JAPD///8JAPP///8JAPb///8JAPn///8JAPz///8JAP////8JAAIA//8JAAUA//8JAAgA//8JAAsA//8JAA4A//8JABEA//8JABQA//8JABcA//8JABoA//8KAOn///8KAOz///8KAO////8KAPL///8KAPX///8KAPj///8KAPv///8KAP7///8KAAEA//8KAAQA//8KAAcA//8KAAoA//8KAA0A//8KABAA//8KABMA//8KABYA//8LAO7///8LAPH///8LAPT///8LAPf///8LAPr///8LAP3///8LAAAA//8LAAMA//8LAAYA//8LAAkA//8LAAwA//8LAA8A//8LABIA//8MAPP///8MAPb///8MAPn///8MAPz///8MAP////8MAAIA//8MAAUA//8MAAgA//8MAAsA//8MAA4AAAD0//f/AAD0//r/AAD0//3/AAD0/wAAAAD0/wMAAAD0/wYAAAD0/wkAAAD1//D/AAD1//P/AAD1//b/AAD1//n/AAD1//z/AAD1////AAD1/wIAAAD1/wUAAAD1/wgAAAD1/wsAAAD1/w4AAAD1/xEAAAD2/+z/AAD2/+//AAD2//L/AAD2//X/AAD2//j/AAD2//v/AAD2//7/AAD2/wEAAAD2/wQAAAD2/wcAAAD2/woAAAD2/w0AAAD2/xAAAAD2/xMAAAD2/xYAAAD3/+j/AAD3/+v/AAD3/+7/AAD3//H/AAD3//T/AAD3//f/AAD3//r/AAD3//3/AAD3/wAAAAD3/wMAAAD3/wYAAAD3/wkAAAD3/wwAAAD3/w8AAAD3/xIAAAD3/xUAAAD3/xgAAAD4/+T/AAD4/+f/AAD4/+r/AAD4/+3/AAD4//D/AAD4//P/AAD4//b/AAD4//n/AAD4//z/AAD4////AAD4/wIAAAD4/wUAAAD4/wgAAAD4/wsAAAD4/w4AAAD4/xEAAAD4/xQAAAD4/xcAAAD4/xoAAAD4/x0AAAD5/+D/AAD5/+P/AAD5/+b/AAD5/+n/AAD5/+z/AAD5/+//AAD5//L/AAD5//X/AAD5//j/AAD5//v/AAD5//7/AAD5/wEAAAD5/wQAAAD5/wcAAAD5/woAAAD5/w0AAAD5/xAAAAD5/xMAAAD5/xYAAAD5/xkAAAD5/xwAAAD5/x8AAAD6/9//AAD6/+L/AAD6/+X/AAD6/+j/AAD6/+v/AAD6/+7/AAD6//H/AAD6//T/AAD6//f/AAD6//r/AAD6//3/AAD6/wAAAAD6/wMAAAD6/wYAAAD6/wkAAAD6/wwAAAD6/w8AAAD6/xIAAAD6/xUAAAD6/xgAAAD6/xsAAAD6/x4AAAD6/yEAAAD7/97/AAD7/+H/AAD7/+T/AAD7/+f/AAD7/+r/AAD7/+3/AAD7//D/AAD7//P/AAD7//b/AAD7//n/AAD7//z/AAD7////AAD7/wIAAAD7/wUAAAD7/wgAAAD7/wsAAAD7/w4AAAD7/xEAAAD7/xQAAAD7/xcAAAD7/xoAAAD7/x0AAAD7/yAAAAD7/yMAAAD8/93/AAD8/+D/AAD8/+P/AAD8/+b/AAD8/+n/AAD8/+z/AAD8/+//AAD8//L/AAD8//X/AAD8//j/AAD8//v/AAD8//7/AAD8/wEAAAD8/wQAAAD8/wcAAAD8/woAAAD8/w0AAAD8/xAAAAD8/xMAAAD8/xYAAAD8/xkAAAD8/xwAAAD8/x8AAAD8/yIAAAD9/9z/AAD9/9//AAD9/+L/AAD9/+X/AAD9/+j/AAD9/+v/AAD9/+7/AAD9//H/AAD9//T/AAD9//f/AAD9//r/AAD9//3/AAD9/wAAAAD9/wMAAAD9/wYAAAD9/wkAAAD9/wwAAAD9/w8AAAD9/xIAAAD9/xUAAAD9/xgAAAD9/xsAAAD9/x4AAAD9/yEAAAD9/yQAAAD+/9v/AAD+/97/AAD+/+H/AAD+/+T/AAD+/+f/AAD+/+r/AAD+/+3/AAD+//D/AAD+//P/AAD+//b/AAD+//n/AAD+//z/AAD+////AAD+/wIAAAD+/wUAAAD+/wgAAAD+/wsAAAD+/w4AAAD+/xEAAAD+/xQAAAD+/xcAAAD+/xoAAAD+/x0AAAD+/yAAAAD+/yMAAAD+/yYAAAD//9r/AAD//93/AAD//+D/AAD//+P/AAD//+b/AAD//+n/AAD//+z/AAD//+//AAD///L/AAD///X/AAD///j/AAD///v/AAD///7/AAD//wEAAAD//wQAAAD//wcAAAD//woAAAD//w0AAAD//xAAAAD//xMAAAD//xYAAAD//xkAAAD//xwAAAD//x8AAAD//yIAAAD//yUAAAAAANz/AAAAAN//AAAAAOL/AAAAAOX/AAAAAOj/AAAAAOv/AAAAAO7/AAAAAPH/AAAAAPT/AAAAAPf/AAAAAPr/AAAAAP3/AAAAAAMAAAAAAAYAAAAAAAkAAAAAAAwAAAAAAA8AAAAAABIAAAAAABUAAAAAABgAAAAAABsAAAAAAB4AAAAAACEAAAAAACQAAAABANv/AAABAN7/AAABAOH/AAABAOT/AAABAOf/AAABAOr/AAABAO3/AAABAPD/AAABAPP/AAABAPb/AAABAPn/AAABAPz/AAABAP//AAABAAIAAAABAAUAAAABAAgAAAABAAsAAAABAA4AAAABABEAAAABABQAAAABABcAAAABABoAAAABAB0AAAABACAAAAABACMAAAABACYAAAACANr/AAACAN3/AAACAOD/AAACAOP/AAACAOb/AAACAOn/AAACAOz/AAACAO//AAACAPL/AAACAPX/AAACAPj/AAACAPv/AAACAP7/AAACAAEAAAACAAQAAAACAAcAAAACAAoAAAACAA0AAAACABAAAAACABMAAAACABYAAAACABkAAAACABwAAAACAB8AAAACACIAAAACACUAAAADANz/AAADAN//AAADAOL/AAADAOX/AAADAOj/AAADAOv/AAADAO7/AAADAPH/AAADAPT/AAADAPf/AAADAPr/AAADAP3/AAADAAAAAAADAAMAAAADAAYAAAADAAkAAAADAAwAAAADAA8AAAADABIAAAADABUAAAADABgAAAADABsAAAADAB4AAAADACEAAAADACQAAAAEAN7/AAAEAOH/AAAEAOT/AAAEAOf/AAAEAOr/AAAEAO3/AAAEAPD/AAAEAPP/AAAEAPb/AAAEAPn/AAAEAPz/AAAEAP//AAAEAAIAAAAEAAUAAAAEAAgAAAAEAAsAAAAEAA4AAAAEABEAAAAEABQAAAAEABcAAAAEABoAAAAEAB0AAAAEACAAAAAEACMAAAAFAN3/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AQACAPb/AQACAPn/AQACAPz/AQACAP//AQACAAIAAQACAAUAAQACAAgAAQACAAsAAQACAA4AAQACABEAAQACABQAAQACABcAAQACABoAAQACAB0AAQACACAAAQACACMAAQACACYAAQADAN3/AQADAOD/AQADAOP/AQADAOb/AQADAOn/AQADAOz/AQADAO//AQADAPL/AQADAPX/AQADAPj/AQADAPv/AQADAP7/AQADAAEAAQADAAQAAQADAAcAAQADAAoAAQADAA0AAQADABAAAQADABMAAQADABYAAQADABkAAQADABwAAQADAB8AAQADACIAAQADACUAAQAEANz/AQAEAN//AQAEAOL/AQAEAOX/AQAEAOj/AQAEAOv/AQAEAO7/AQAEAPH/AQAEAPT/AQAEAPf/AQAEAPr/AQAEAP3/AQAEAAAAAQAEAAMAAQAEAAYAAQAEAAkAAQAEAAwAAQAEAA8AAQAEABIAAQAEABUAAQAEABgAAQAEABsAAQAEAB4AAQAEACEAAQAEACQAAQAFAN7/AQAFAOH/AQAFAOT/AQAFAOf/AQAFAOr/AQAFAO3/AQAFAPD/AQAFAPP/AQAFAPb/AQAFAPn/AQAFAPz/AQAFAP//AQAFAAIAAQAFAAUAAQAFAAgAAQAFAAsAAQAFAA4AAQAFABEAAQAFABQAAQAFABcAAQAFABoAAQAFAB0AAQAFACAAAQAGAOD/AQAGAOP/AQAGAOb/AQAGAOn/AQAGAOz/AQAGAO//AQAGAPL/AQAGAPX/AQAGAPj/AQAGAPv/AQAGAP7/AQAGAAEAAQAGAAQAAQAGAAcAAQAGAAoAAQAGAA0AAQAGABAAAQAGABMAAQAGABYAAQAGABkAAQAGABwAAQAGAB8AAQAHAOL/AQAHAOX/AQAHAOj/AQAHAOv/AQAHAO7/AQAHAPH/AQAHAPT/AQAHAPf/AQAHAPr/AQAHAP3/AQAHAAAAAQAHAAMAAQAHAAYAAQAHAAkAAQAHAAwAAQAHAA8AAQAHABIAAQAHABUAAQAHABgAAQAHABsAAQAHAB4AAQAIAOT/AQAIAOf/AQAIAOr/AQAIAO3/AQAIAPD/AQAIAPP/AQAIAPb/AQAIAPn/AQAIAPz/AQAIAP//AQAIAAIAAQAIAAUAAQAIAAgAAQAIAAsAAQAIAA4AAQAIABEAAQAIABQAAQAIABcAAQAIABoAAQAJAOn/AQAJAOz/AQAJAO//AQAJAPL/AQAJAPX/AQAJAPj/AQAJAPv/AQAJAP7/AQAJAAEAAQAJAAQAAQAJAAcAAQAJAAoAAQAJAA0AAQAJABAAAQAJABMAAQAJABYAAQAKAO7/AQAKAPH/AQAKAPT/AQAKAPf/AQAKAPr/AQAKAP3/AQAKAAAAAQAKAAMAAQAKAAYAAQAKAAkAAQAKAAwAAQAKAA8AAQAKABIAAQALAPP/AQALAPb/AQALAPn/AQALAPz/AQALAP//AQALAAIAAQALAAUAAQALAAgAAQALAAsAAQALAA4AAgDz//r/AgDz//3/AgDz/wAAAgDz/wMAAgDz/wYAAgD0//D/AgD0//P/AgD0//b/AgD0//n/AgD0//z/AgD0////AgD0/wIAAgD0/wUAAgD0/wgAAgD0/wsAAgD0/w4AAgD1/+z/AgD1/+//AgD1//L/AgD1//X/AgD1//j/AgD1//v/AgD1//7/AgD1/wEAAgD1/wQAAgD1/wcAAgD1/woAAgD1/w0AAgD1/xAAAgD1/xMAAgD1/xYAAgD2/+j/AgD2/+v/AgD2/+7/AgD2//H/AgD2//T/AgD2//f/AgD2//r/AgD2//3/AgD2/wAAAgD2/wMAAgD2/wYAAgD2/wkAAgD2/wwAAgD2/w8AAgD2/xIAAgD2/xUAAgD2/xgAAgD3/+T/AgD3/+f/AgD3/+r/AgD3/+3/AgD3//D/AgD3//P/AgD3//b/AgD3//n/AgD3//z/AgD3////AgD3/wIAAgD3/wUAAgD3/wgAAgD3/wsAAgD3/w4AAgD3/xEAAgD3/xQAAgD3/xcAAgD3/xoAAgD3/x0AAgD4/+P/AgD4/+b/AgD4/+n/AgD4/+z/AgD4/+//AgD4//L/AgD4//X/AgD4//j/AgD4//v/AgD4//7/AgD4/wEAAgD4/wQAAgD4/wcAAgD4/woAAgD4/w0AAgD4/xAAAgD4/xMAAgD4/xYAAgD4/xkAAgD4/xwAAgD4/x8AAgD5/9//AgD5/+L/AgD5/+X/AgD5/+j/AgD5/+v/AgD5/+7/AgD5//H/AgD5//T/AgD5//f/AgD5//r/AgD5//3/AgD5/wAAAgD5/wMAAgD5/wYAAgD5/wkAAgD5/wwAAgD5/w8AAgD5/xIAAgD5/xUAAgD5/xgAAgD5/xsAAgD5/x4AAgD5/yEAAgD6/97/AgD6/+H/AgD6/+T/AgD6/+f/AgD6/+r/AgD6/+3/AgD6//D/AgD6//P/AgD6//b/AgD6//n/AgD6//z/AgD6////AgD6/wIAAgD6/wUAAgD6/wgAAgD6/wsAAgD6/w4AAgD6/xEAAgD6/xQAAgD6/xcAAgD6/xoAAgD6/x0AAgD6/yAAAgD6/yMAAgD7/93/AgD7/+D/AgD7/+P/AgD7/+b/AgD7/+n/AgD7/+z/AgD7/+//AgD7//L/AgD7//X/AgD7//j/AgD7//v/AgD7//7/AgD7/wEAAgD7/wQAAgD7/wcAAgD7/woAAgD7/w0AAgD7/xAAAgD7/xMAAgD7/xYAAgD7/xkAAgD7/xwAAgD7/x8AAgD7/yIAAgD8/9z/AgD8/9//AgD8/+L/AgD8/+X/AgD8/+j/AgD8/+v/AgD8/+7/AgD8//H/AgD8//T/AgD8//f/AgD8//r/AgD8//3/AgD8/wAAAgD8/wMAAgD8/wYAAgD8/wkAAgD8/wwAAgD8/w8AAgD8/xIAAgD8/xUAAgD8/xgAAgD8/xsAAgD8/x4AAgD8/yEAAgD8/yQAAgD9/9v/AgD9/97/AgD9/+H/AgD9/+T/AgD9/+f/AgD9/+r/AgD9/+3/AgD9//D/AgD9//P/AgD9//b/AgD9//n/AgD9//z/AgD9////AgD9/wIAAgD9/wUAAgD9/wgAAgD9/wsAAgD9/w4AAgD9/xEAAgD9/xQAAgD9/xcAAgD9/xoAAgD9/x0AAgD9/yAAAgD9/yMAAgD9/yYAAgD+/9r/AgD+/93/AgD+/+D/AgD+/+P/AgD+/+b/AgD+/+n/AgD+/+z/AgD+/+//AgD+//L/AgD+//X/AgD+//j/AgD+//v/AgD+//7/AgD+/wEAAgD+/wQAAgD+/wcAAgD+/woAAgD+/w0AAgD+/xAAAgD+/xMAAgD+/xYAAgD+/xkAAgD+/xwAAgD+/x8AAgD+/yIAAgD+/yUAAgD//9z/AgD//9//AgD//+L/AgD//+X/AgD//+j/AgD//+v/AgD//+7/AgD///H/AgD///T/AgD///f/AgD///r/AgD///3/AgD//wAAAgD//wMAAgD//wYAAgD//wkAAgD//wwAAgD//w8AAgD//xIAAgD//xUAAgD//xgAAgD//xsAAgD//x4AAgD//yEAAgD//yQAAgAAANv/AgAAAN7/AgAAAOH/AgAAAOT/AgAAAOf/AgAAAOr/AgAAAO3/AgAAAPD/AgAAAPP/AgAAAPb/AgAAAPn/AgAAAPz/AgAAAP//AgAAAAIAAgAAAAUAAgAAAAgAAgAAAAsAAgAAAA4AAgAAABEAAgAAABQAAgAAABcAAgAAABoAAgAAAB0AAgAAACAAAgAAACMAAgAAACYAAgABANr/AgABAN3/AgABAOD/AgABAOP/AgABAOb/AgABAOn/AgABAOz/AgABAO//AgABAPL/AgABAPX/AgABAPj/AgABAPv/AgABAP7/AgABAAEAAgABAAQAAgABAAcAAgABAAoAAgABAA0AAgABABAAAgABABMAAgABABYAAgABABkAAgABABwAAgABAB8AAgABACIAAgABACUAAgACANz/AgACAN//AgACAOL/AgACAOX/AgACAOj/AgACAOv/AgACAO7/AgACAPH/AgACAPT/AgACAPf/AgACAPr/AgACAP3/AgACAAAAAgACAAMAAgACAAYAAgACAAkAAgACAAwAAgACAA8AAgACABIAAgACABUAAgACABgAAgACABsAAgACAB4AAgACACEAAgACACQAAgADAN7/AgADAOH/AgADAOT/AgADAOf/AgADAOr/AgADAO3/AgADAPD/AgADAPP/AgADAPb/AgADAPn/AgADAPz/AgADAP//AgADAAIAAgADAAUAAgADAAgAAgADAAsAAgADAA4AAgADABEAAgADABQAAgADABcAAgADABoAAgADAB0AAgADACAAAgADACMAAgAEAN3/AgAEAOD/AgAEAOP/AgAEAOb/AgAEAOn/AgAEAOz/AgAEAO//AgAEAPL/AgAEAPX/AgAEAPj/AgAEAPv/AgAEAP7/AgAEAAEAAgAEAAQAAgAEAAcAAgAEAAoAAgAEAA0AAgAEABAAAgAEABMAAgAEABYAAgAEABkAAgAEABwAAgAEAB8AAgAEACIAAgAFAN//AgAFAOL/AgAFAOX/AgAFAOj/AgAFAOv/AgAFAO7/AgAFAPH/AgAFAPT/AgAFAPf/AgAFAPr/AgAFAP3/AgAFAAAAAgAFAAMAAgAFAAYAAgAFAAkAAgAFAAwAAgAFAA8AAgAFABIAAgAFABUAAgAFABgAAgAFABsAAgAFAB4AAgAFACEAAgAGAOH/AgAGAOT/AgAGAOf/AgAGAOr/AgAGAO3/AgAGAPD/AgAGAPP/AgAGAPb/AgAGAPn/AgAGAPz/AgAGAP//AgAGAAIAAgAGAAUAAgAGAAgAAgAGAAsAAgAGAA4AAgAGABEAAgAGABQAAgAGABcAAgAGABoAAgAGAB0AAgAHAOP/AgAHAOb/AgAHAOn/AgAHAOz/AgAHAO//AgAHAPL/AgAHAPX/AgAHAPj/AgAHAPv/AgAHAP7/AgAHAAEAAgAHAAQAAgAHAAcAAgAHAAoAAgAHAA0AAgAHABAAAgAHABMAAgAHABYAAgAHABkAAgAHABwAAgAIAOj/AgAIAOv/AgAIAO7/AgAIAPH/AgAIAPT/AgAIAPf/AgAIAPr/AgAIAP3/AgAIAAAAAgAIAAMAAgAIAAYAAgAIAAkAAgAIAAwAAgAIAA8AAgAIABIAAgAIABUAAgAIABgAAgAJAOr/AgAJAO3/AgAJAPD/AgAJAPP/AgAJAPb/AgAJAPn/AgAJAPz/AgAJAP//AgAJAAIAAgAJAAUAAgAJAAgAAgAJAAsAAgAJAA4AAgAJABEAAgAJABQAAgAKAPL/AgAKAPX/AgAKAPj/AgAKAPv/AgAKAP7/AgAKAAEAAgAKAAQAAgAKAAcAAgAKAAoAAgAKAA0AAgAKABAAAgALAPr/AgALAP3/AgALAAAAAgALAAMAAgALAAYAAwDz//X/AwDz//j/AwDz//v/AwDz//7/AwDz/wEAAwDz/wQAAwDz/wcAAwDz/woAAwD0/+7/AwD0//H/AwD0//T/AwD0//f/AwD0//r/AwD0//3/AwD0/wAAAwD0/wMAAwD0/wYAAwD0/wkAAwD0/wwAAwD0/w8AAwD0/xIAAwD1/+r/AwD1/+3/AwD1//D/AwD1//P/AwD1//b/AwD1//n/AwD1//z/AwD1////AwD1/wIAAwD1/wUAAwD1/wgAAwD1/wsAAwD1/w4AAwD1/xEAAwD1/xQAAwD1/xcAAwD2/+b/AwD2/+n/AwD2/+z/AwD2/+//AwD2//L/AwD2//X/AwD2//j/AwD2//v/AwD2//7/AwD2/wEAAwD2/wQAAwD2/wcAAwD2/woAAwD2/w0AAwD2/xAAAwD2/xMAAwD2/xYAAwD2/xkAAwD3/+X/AwD3/+j/AwD3/+v/AwD3/+7/AwD3//H/AwD3//T/AwD3//f/AwD3//r/AwD3//3/AwD3/wAAAwD3/wMAAwD3/wYAAwD3/wkAAwD3/wwAAwD3/w8AAwD3/xIAAwD3/xUAAwD3/xgAAwD3/xsAAwD4/+H/AwD4/+T/AwD4/+f/AwD4/+r/AwD4/+3/AwD4//D/AwD4//P/AwD4//b/AwD4//n/AwD4//z/AwD4////AwD4/wIAAwD4/wUAAwD4/wgAAwD4/wsAAwD4/w4AAwD4/xEAAwD4/xQAAwD4/xcAAwD4/xoAAwD4/x0AAwD4/yAAAwD5/+D/AwD5/+P/AwD5/+b/AwD5/+n/AwD5/+z/AwD5/+//AwD5//L/AwD5//X/AwD5//j/AwD5//v/AwD5//7/AwD5/wEAAwD5/wQAAwD5/wcAAwD5/woAAwD5/w0AAwD5/xAAAwD5/xMAAwD5/xYAAwD5/xkAAwD5/xwAAwD5/x8AAwD6/9//AwD6/+L/AwD6/+X/AwD6/+j/AwD6/+v/AwD6/+7/AwD6//H/AwD6//T/AwD6//f/AwD6//r/AwD6//3/AwD6/wAAAwD6/wMAAwD6/wYAAwD6/wkAAwD6/wwAAwD6/w8AAwD6/xIAAwD6/xUAAwD6/xgAAwD6/xsAAwD6/x4AAwD6/yEAAwD7/97/AwD7/+H/AwD7/+T/AwD7/+f/AwD7/+r/AwD7/+3/AwD7//D/AwD7//P/AwD7//b/AwD7//n/AwD7//z/AwD7////AwD7/wIAAwD7/wUAAwD7/wgAAwD7/wsAAwD7/w4AAwD7/xEAAwD7/xQAAwD7/xcAAwD7/xoAAwD7/x0AAwD7/yAAAwD7/yMAAwD8/93/AwD8/+D/AwD8/+P/AwD8/+b/AwD8/+n/AwD8/+z/AwD8/+//AwD8//L/AwD8//X/AwD8//j/AwD8//v/AwD8//7/AwD8/wEAAwD8/wQAAwD8/wcAAwD8/woAAwD8/w0AAwD8/xAAAwD8/xMAAwD8/xYAAwD8/xkAAwD8/xwAAwD8/x8AAwD8/yIAAwD8/yUAAwD9/9z/AwD9/9//AwD9/+L/AwD9/+X/AwD9/+j/AwD9/+v/AwD9/+7/AwD9//H/AwD9//T/AwD9//f/AwD9//r/AwD9//3/AwD9/wAAAwD9/wMAAwD9/wYAAwD9/wkAAwD9/wwAAwD9/w8AAwD9/xIAAwD9/xUAAwD9/xgAAwD9/xsAAwD9/x4AAwD9/yEAAwD9/yQAAwD+/9v/AwD+/97/AwD+/+H/AwD+/+T/AwD+/+f/AwD+/+r/AwD+/+3/AwD+//D/AwD+//P/AwD+//b/AwD+//n/AwD+//z/AwD+////AwD+/wIAAwD+/wUAAwD+/wgAAwD+/wsAAwD+/w4AAwD+/xEAAwD+/xQAAwD+/xcAAwD+/xoAAwD+/x0AAwD+/yAAAwD+/yMAAwD+/yYAAwD//9r/AwD//93/AwD//+D/AwD//+P/AwD//+b/AwD//+n/AwD//+z/AwD//+//AwD///L/AwD///X/AwD///j/AwD///v/AwD///7/AwD//wEAAwD//wQAAwD//wcAAwD//woAAwD//w0AAwD//xAAAwD//xMAAwD//xYAAwD//xkAAwD//xwAAwD//x8AAwD//yIAAwD//yUAAwAAANz/AwAAAN//AwAAAOL/AwAAAOX/AwAAAOj/AwAAAOv/AwAAAO7/AwAAAPH/AwAAAPT/AwAAAPf/AwAAAPr/AwAAAP3/AwAAAAAAAwAAAAMAAwAAAAYAAwAAAAkAAwAAAAwAAwAAAA8AAwAAABIAAwAAABUAAwAAABgAAwAAABsAAwAAAB4AAwAAACEAAwAAACQAAwABANv/AwABAN7/AwABAOH/AwABAOT/AwABAOf/AwABAOr/AwABAO3/AwABAPD/AwABAPP/AwABAPb/AwABAPn/AwABAPz/AwABAP//AwABAAIAAwABAAUAAwABAAgAAwABAAsAAwABAA4AAwABABEAAwABABQAAwABABcAAwABABoAAwABAB0AAwABACAAAwABACMAAwACAN3/AwACAOD/AwACAOP/AwACAOb/AwACAOn/AwACAOz/AwACAO//AwACAPL/AwACAPX/AwACAPj/AwACAPv/AwACAP7/AwACAAEAAwACAAQAAwACAAcAAwACAAoAAwACAA0AAwACABAAAwACABMAAwACABYAAwACABkAAwACABwAAwACAB8AAwACACIAAwADAN//AwADAOL/AwADAOX/AwADAOj/AwADAOv/AwADAO7/AwADAPH/AwADAPT/AwADAPf/AwADAPr/AwADAP3/AwADAAAAAwADAAMAAwADAAYAAwADAAkAAwADAAwAAwADAA8AAwADABIAAwADABUAAwADABgAAwADABsAAwADAB4AAwADACEAAwAEAOH/AwAEAOT/AwAEAOf/AwAEAOr/AwAEAO3/AwAEAPD/AwAEAPP/AwAEAPb/AwAEAPn/AwAEAPz/AwAEAP//AwAEAAIAAwAEAAUAAwAEAAgAAwAEAAsAAwAEAA4AAwAEABEAAwAEABQAAwAEABcAAwAEABoAAwAEAB0AAwAEACAAAwAFAOD/AwAFAOP/AwAFAOb/AwAFAOn/AwAFAOz/AwAFAO//AwAFAPL/AwAFAPX/AwAFAPj/AwAFAPv/AwAFAP7/AwAFAAEAAwAFAAQAAwAFAAcAAwAFAAoAAwAFAA0AAwAFABAAAwAFABMAAwAFABYAAwAFABkAAwAFABwAAwAFAB8AAwAGAOX/AwAGAOj/AwAGAOv/AwAGAO7/AwAGAPH/AwAGAPT/AwAGAPf/AwAGAPr/AwAGAP3/AwAGAAAAAwAGAAMAAwAGAAYAAwAGAAkAAwAGAAwAAwAGAA8AAwAGABIAAwAGABUAAwAGABgAAwAGABsAAwAHAOf/AwAHAOr/AwAHAO3/AwAHAPD/AwAHAPP/AwAHAPb/AwAHAPn/AwAHAPz/AwAHAP//AwAHAAIAAwAHAAUAAwAHAAgAAwAHAAsAAwAHAA4AAwAHABEAAwAHABQAAwAHABcAAwAHABoAAwAIAOn/AwAIAOz/AwAIAO//AwAIAPL/AwAIAPX/AwAIAPj/AwAIAPv/AwAIAP7/AwAIAAEAAwAIAAQAAwAIAAcAAwAIAAoAAwAIAA0AAwAIABAAAwAIABMAAwAIABYAAwAJAO7/AwAJAPH/AwAJAPT/AwAJAPf/AwAJAPr/AwAJAP3/AwAJAAAAAwAJAAMAAwAJAAYAAwAJAAkAAwAJAAwAAwAJAA8AAwAJABIAAwAKAPb/AwAKAPn/AwAKAPz/AwAKAP//AwAKAAIAAwAKAAUAAwAKAAgAAwAKAAsABADz//P/BADz//b/BADz//n/BADz//z/BADz////BADz/wIABADz/wUABADz/wgABADz/wsABADz/w4ABAD0/+z/BAD0/+//BAD0//L/BAD0//X/BAD0//j/BAD0//v/BAD0//7/BAD0/wEABAD0/wQABAD0/wcABAD0/woABAD0/w0ABAD0/xAABAD0/xMABAD1/+j/BAD1/+v/BAD1/+7/BAD1//H/BAD1//T/BAD1//f/BAD1//r/BAD1//3/BAD1/wAABAD1/wMABAD1/wYABAD1/wkABAD1/wwABAD1/w8ABAD1/xIABAD1/xUABAD1/xgABAD2/+f/BAD2/+r/BAD2/+3/BAD2//D/BAD2//P/BAD2//b/BAD2//n/BAD2//z/BAD2////BAD2/wIABAD2/wUABAD2/wgABAD2/wsABAD2/w4ABAD2/xEABAD2/xQABAD2/xcABAD2/xoABAD3/+P/BAD3/+b/BAD3/+n/BAD3/+z/BAD3/+//BAD3//L/BAD3//X/BAD3//j/BAD3//v/BAD3//7/BAD3/wEABAD3/wQABAD3/wcABAD3/woABAD3/w0ABAD3/xAABAD3/xMABAD3/xYABAD3/xkABAD3/xwABAD4/+L/BAD4/+X/BAD4/+j/BAD4/+v/BAD4/+7/BAD4//H/BAD4//T/BAD4//f/BAD4//r/BAD4//3/BAD4/wAABAD4/wMABAD4/wYABAD4/wkABAD4/wwABAD4/w8ABAD4/xIABAD4/xUABAD4/xgABAD4/xsABAD4/x4ABAD5/+H/BAD5/+T/BAD5/+f/BAD5/+r/BAD5/+3/BAD5//D/BAD5//P/BAD5//b/BAD5//n/BAD5//z/BAD5////BAD5/wIABAD5/wUABAD5/wgABAD5/wsABAD5/w4ABAD5/xEABAD5/xQABAD5/xcABAD5/xoABAD5/x0ABAD5/yAABAD6/93/BAD6/+D/BAD6/+P/BAD6/+b/BAD6/+n/BAD6/+z/BAD6/+//BAD6//L/BAD6//X/BAD6//j/BAD6//v/BAD6//7/BAD6/wEABAD6/wQABAD6/wcABAD6/woABAD6/w0ABAD6/xAABAD6/xMABAD6/xYABAD6/xkABAD6/xwABAD6/x8ABAD6/yIABAD7/9z/BAD7/9//BAD7/+L/BAD7/+X/BAD7/+j/BAD7/+v/BAD7/+7/BAD7//H/BAD7//T/BAD7//f/BAD7//r/BAD7//3/BAD7/wAABAD7/wMABAD7/wYABAD7/wkABAD7/wwABAD7/w8ABAD7/xIABAD7/xUABAD7/xgABAD7/xsABAD7/x4ABAD7/yEABAD7/yQABAD8/97/BAD8/+H/BAD8/+T/BAD8/+f/BAD8/+r/BAD8/+3/BAD8//D/BAD8//P/BAD8//b/BAD8//n/BAD8//z/BAD8////BAD8/wIABAD8/wUABAD8/wgABAD8/wsABAD8/w4ABAD8/xEABAD8/xQABAD8/xcABAD8/xoABAD8/x0ABAD8/yAABAD8/yMABAD9/93/BAD9/+D/BAD9/+P/BAD9/+b/BAD9/+n/BAD9/+z/BAD9/+//BAD9//L/BAD9//X/BAD9//j/BAD9//v/BAD9//7/BAD9/wEABAD9/wQABAD9/wcABAD9/woABAD9/w0ABAD9/xAABAD9/xMABAD9/xYABAD9/xkABAD9/xwABAD9/x8ABAD9/yIABAD9/yUABAD+/9z/BAD+/9//BAD+/+L/BAD+/+X/BAD+/+j/BAD+/+v/BAD+/+7/BAD+//H/BAD+//T/BAD+//f/BAD+//r/BAD+//3/BAD+/wAABAD+/wMABAD+/wYABAD+/wkABAD+/wwABAD+/w8ABAD+/xIABAD+/xUABAD+/xgABAD+/xsABAD+/x4ABAD+/yEABAD+/yQABAD//9v/BAD//97/BAD//+H/BAD//+T/BAD//+f/BAD//+r/BAD//+3/BAD///D/BAD///P/BAD///b/BAD///n/BAD///z/BAD/////BAD//wIABAD//wUABAD//wgABAD//wsABAD//w4ABAD//xEABAD//xQABAD//xcABAD//xoABAD//x0ABAD//yAABAD//yMABAAAAN3/BAAAAOD/BAAAAOP/BAAAAOb/BAAAAOn/BAAAAOz/BAAAAO//BAAAAPL/BAAAAPX/BAAAAPj/BAAAAPv/BAAAAP7/BAAAAAEABAAAAAQABAAAAAcABAAAAAoABAAAAA0ABAAAABAABAAAABMABAAAABYABAAAABkABAAAABwABAAAAB8ABAAAACIABAABANz/BAABAN//BAABAOL/BAABAOX/BAABAOj/BAABAOv/BAABAO7/BAABAPH/BAABAPT/BAABAPf/BAABAPr/BAABAP3/BAABAAAABAABAAMABAABAAYABAABAAkABAABAAwABAABAA8ABAABABIABAABABUABAABABgABAABABsABAABAB4ABAABACEABAABACQABAACAN7/BAACAOH/BAACAOT/BAACAOf/BAACAOr/BAACAO3/BAACAPD/BAACAPP/BAACAPb/BAACAPn/BAACAPz/BAACAP//BAACAAIABAACAAUABAACAAgABAACAAsABAACAA4ABAACABEABAACABQABAACABcABAACABoABAACAB0ABAACACAABAACACMABAADAOD/BAADAOP/BAADAOb/BAADAOn/BAADAOz/BAADAO//BAADAPL/BAADAPX/BAADAPj/BAADAPv/BAADAP7/BAADAAEABAADAAQABAADAAcABAADAAoABAADAA0ABAADABAABAADABMABAADABYABAADABkABAADABwABAADAB8ABAAEAOL/BAAEAOX/BAAEAOj/BAAEAOv/BAAEAO7/BAAEAPH/BAAEAPT/BAAEAPf/BAAEAPr/BAAEAP3/BAAEAAAABAAEAAMABAAEAAYABAAEAAkABAAEAAwABAAEAA8ABAAEABIABAAEABUABAAEABgABAAEABsABAAEAB4ABAAFAOT/BAAFAOf/BAAFAOr/BAAFAO3/BAAFAPD/BAAFAPP/BAAFAPb/BAAFAPn/BAAFAPz/BAAFAP//BAAFAAIABAAFAAUABAAFAAgABAAFAAsABAAFAA4ABAAFABEABAAFABQABAAFABcABAAFABoABAAFAB0ABAAGAOb/BAAGAOn/BAAGAOz/BAAGAO//BAAGAPL/BAAGAPX/BAAGAPj/BAAGAPv/BAAGAP7/BAAGAAEABAAGAAQABAAGAAcABAAGAAoABAAGAA0ABAAGABAABAAGABMABAAGABYABAAGABkABAAHAOj/BAAHAOv/BAAHAO7/BAAHAPH/BAAHAPT/BAAHAPf/BAAHAPr/BAAHAP3/BAAHAAAABAAHAAMABAAHAAYABAAHAAkABAAHAAwABAAHAA8ABAAHABIABAAHABUABAAHABgABAAIAO3/BAAIAPD/BAAIAPP/BAAIAPb/BAAIAPn/BAAIAPz/BAAIAP//BAAIAAIABAAIAAUABAAIAAgABAAIAAsABAAIAA4ABAAIABEABAAIABQABAAJAPL/BAAJAPX/BAAJAPj/BAAJAPv/BAAJAP7/BAAJAAEABAAJAAQABAAJAAcABAAJAAoABAAJAA0ABQDy//7/BQDy/wEABQDy/wQABQDz//H/BQDz//T/BQDz//f/BQDz//r/BQDz//3/BQDz/wAABQDz/wMABQDz/wYABQDz/wkABQDz/wwABQDz/w8ABQD0/+3/BQD0//D/BQD0//P/BQD0//b/BQD0//n/BQD0//z/BQD0////BQD0/wIABQD0/wUABQD0/wgABQD0/wsABQD0/w4ABQD0/xEABQD0/xQABQD1/+n/BQD1/+z/BQD1/+//BQD1//L/BQD1//X/BQD1//j/BQD1//v/BQD1//7/BQD1/wEABQD1/wQABQD1/wcABQD1/woABQD1/w0ABQD1/xAABQD1/xMABQD1/xYABQD2/+X/BQD2/+j/BQD2/+v/BQD2/+7/BQD2//H/BQD2//T/BQD2//f/BQD2//r/BQD2//3/BQD2/wAABQD2/wMABQD2/wYABQD2/wkABQD2/wwABQD2/w8ABQD2/xIABQD2/xUABQD2/xgABQD2/xsABQD3/+T/BQD3/+f/BQD3/+r/BQD3/+3/BQD3//D/BQD3//P/BQD3//b/BQD3//n/BQD3//z/BQD3////BQD3/wIABQD3/wUABQD3/wgABQD3/wsABQD3/w4ABQD3/xEABQD3/xQABQD3/xcABQD3/xoABQD3/x0ABQD4/+D/BQD4/+P/BQD4/+b/BQD4/+n/BQD4/+z/BQD4/+//BQD4//L/BQD4//X/BQD4//j/BQD4//v/BQD4//7/BQD4/wEABQD4/wQABQD4/wcABQD4/woABQD4/w0ABQD4/xAABQD4/xMABQD4/xYABQD4/xkABQD4/xwABQD4/x8ABQD5/9//BQD5/+L/BQD5/+X/BQD5/+j/BQD5/+v/BQD5/+7/BQD5//H/BQD5//T/BQD5//f/BQD5//r/BQD5//3/BQD5/wAABQD5/wMABQD5/wYABQD5/wkABQD5/wwABQD5/w8ABQD5/xIABQD5/xUABQD5/xgABQD5/xsABQD5/x4ABQD5/yEABQD6/97/BQD6/+H/BQD6/+T/BQD6/+f/BQD6/+r/BQD6/+3/BQD6//D/BQD6//P/BQD6//b/BQD6//n/BQD6//z/BQD6////BQD6/wIABQD6/wUABQD6/wgABQD6/wsABQD6/w4ABQD6/xEABQD6/xQABQD6/xcABQD6/xoABQD6/x0ABQD6/yAABQD7/93/BQD7/+D/BQD7/+P/BQD7/+b/BQD7/+n/BQD7/+z/BQD7/+//BQD7//L/BQD7//X/BQD7//j/BQD7//v/BQD7//7/BQD7/wEABQD7/wQABQD7/wcABQD7/woABQD7/w0ABQD7/xAABQD7/xMABQD7/xYABQD7/xkABQD7/xwABQD7/x8ABQD7/yIABQD8/9z/BQD8/9//BQD8/+L/BQD8/+X/BQD8/+j/BQD8/+v/BQD8/+7/BQD8//H/BQD8//T/BQD8//f/BQD8//r/BQD8//3/BQD8/wAABQD8/wMABQD8/wYABQD8/wkABQD8/wwABQD8/w8ABQD8/xIABQD8/xUABQD8/xgABQD8/xsABQD8/x4ABQD8/yEABQD8/yQABQD9/97/BQD9/+H/BQD9/+T/BQD9/+f/BQD9/+r/BQD9/+3/BQD9//D/BQD9//P/BQD9//b/BQD9//n/BQD9//z/BQD9////BQD9/wIABQD9/wUABQD9/wgABQD9/wsABQD9/w4ABQD9/xEABQD9/xQABQD9/xcABQD9/xoABQD9/x0ABQD9/yAABQD9/yMABQD+/93/BQD+/+D/BQD+/+P/BQD+/+b/BQD+/+n/BQD+/+z/BQD+/+//BQD+//L/BQD+//X/BQD+//j/BQD+//v/BQD+//7/BQD+/wEABQD+/wQABQD+/wcABQD+/woABQD+/w0ABQD+/xAABQD+/xMABQD+/xYABQD+/xkABQD+/xwABQD+/x8ABQD+/yIABQD//9z/BQD//9//BQD//+L/BQD//+X/BQD//+j/BQD//+v/BQD//+7/BQD///H/BQD///T/BQD///f/BQD///r/BQD///3/BQD//wAABQD//wMABQD//wYABQD//wkABQD//wwABQD//w8ABQD//xIABQD//xUABQD//xgABQD//xsABQD//x4ABQD//yEABQD//yQABQAAAN7/BQAAAOH/BQAAAOT/BQAAAOf/BQAAAOr/BQAAAO3/BQAAAPD/BQAAAPP/BQAAAPb/BQAAAPn/BQAAAPz/BQAAAP//BQAAAAIABQAAAAUABQAAAAgABQAAAAsABQAAAA4ABQAAABEABQAAABQABQAAABcABQAAABoABQAAAB0ABQAAACAABQAAACMABQABAOD/BQABAOP/BQABAOb/BQABAOn/BQABAOz/BQABAO//BQABAPL/BQABAPX/BQABAPj/BQABAPv/BQABAP7/BQABAAEABQABAAQABQABAAcABQABAAoABQABAA0ABQABABAABQABABMABQABABYABQABABkABQABABwABQABAB8ABQABACIABQACAN//BQACAOL/BQACAOX/BQACAOj/BQACAOv/BQACAO7/BQACAPH/BQACAPT/BQACAPf/BQACAPr/BQACAP3/BQACAAAABQACAAMABQACAAYABQACAAkABQACAAwABQACAA8ABQACABIABQACABUABQACABgABQACABsABQACAB4ABQACACEABQADAOH/BQADAOT/BQADAOf/BQADAOr/BQADAO3/BQADAPD/BQADAPP/BQADAPb/BQADAPn/BQADAPz/BQADAP//BQADAAIABQADAAUABQADAAgABQADAAsABQADAA4ABQADABEABQADABQABQADABcABQADABoABQADAB0ABQADACAABQAEAOP/BQAEAOb/BQAEAOn/BQAEAOz/BQAEAO//BQAEAPL/BQAEAPX/BQAEAPj/BQAEAPv/BQAEAP7/BQAEAAEABQAEAAQABQAEAAcABQAEAAoABQAEAA0ABQAEABAABQAEABMABQAEABYABQAEABkABQAEABwABQAFAOX/BQAFAOj/BQAFAOv/BQAFAO7/BQAFAPH/BQAFAPT/BQAFAPf/BQAFAPr/BQAFAP3/BQAFAAAABQAFAAMABQAFAAYABQAFAAkABQAFAAwABQAFAA8ABQAFABIABQAFABUABQAFABgABQAFABsABQAGAOr/BQAGAO3/BQAGAPD/BQAGAPP/BQAGAPb/BQAGAPn/BQAGAPz/BQAGAP//BQAGAAIABQAGAAUABQAGAAgABQAGAAsABQAGAA4ABQAGABEABQAGABQABQAGABcABQAHAOz/BQAHAO//BQAHAPL/BQAHAPX/BQAHAPj/BQAHAPv/BQAHAP7/BQAHAAEABQAHAAQABQAHAAcABQAHAAoABQAHAA0ABQAHABAABQAHABMABQAIAPH/BQAIAPT/BQAIAPf/BQAIAPr/BQAIAP3/BQAIAAAABQAIAAMABQAIAAYABQAIAAkABQAIAAwABQAIAA8ABQAJAPz/BQAJAP//BQAJAAIABgDy//n/BgDy//z/BgDy////BgDy/wIABgDy/wUABgDz//L/BgDz//X/BgDz//j/BgDz//v/BgDz//7/BgDz/wEABgDz/wQABgDz/wcABgDz/woABgDz/w0ABgDz/xAABgD0/+v/BgD0/+7/BgD0//H/BgD0//T/BgD0//f/BgD0//r/BgD0//3/BgD0/wAABgD0/wMABgD0/wYABgD0/wkABgD0/wwABgD0/w8ABgD0/xIABgD0/xUABgD1/+r/BgD1/+3/BgD1//D/BgD1//P/BgD1//b/BgD1//n/BgD1//z/BgD1////BgD1/wIABgD1/wUABgD1/wgABgD1/wsABgD1/w4ABgD1/xEABgD1/xQABgD1/xcABgD2/+b/BgD2/+n/BgD2/+z/BgD2/+//BgD2//L/BgD2//X/BgD2//j/BgD2//v/BgD2//7/BgD2/wEABgD2/wQABgD2/wcABgD2/woABgD2/w0ABgD2/xAABgD2/xMABgD2/xYABgD2/xkABgD3/+X/BgD3/+j/BgD3/+v/BgD3/+7/BgD3//H/BgD3//T/BgD3//f/BgD3//r/BgD3//3/BgD3/wAABgD3/wMABgD3/wYABgD3/wkABgD3/wwABgD3/w8ABgD3/xIABgD3/xUABgD3/xgABgD3/xsABgD4/+H/BgD4/+T/BgD4/+f/BgD4/+r/BgD4/+3/BgD4//D/BgD4//P/BgD4//b/BgD4//n/BgD4//z/BgD4////BgD4/wIABgD4/wUABgD4/wgABgD4/wsABgD4/w4ABgD4/xEABgD4/xQABgD4/xcABgD4/xoABgD4/x0ABgD5/+D/BgD5/+P/BgD5/+b/BgD5/+n/BgD5/+z/BgD5/+//BgD5//L/BgD5//X/BgD5//j/BgD5//v/BgD5//7/BgD5/wEABgD5/wQABgD5/wcABgD5/woABgD5/w0ABgD5/xAABgD5/xMABgD5/xYABgD5/xkABgD5/xwABgD5/x8ABgD6/9//BgD6/+L/BgD6/+X/BgD6/+j/BgD6/+v/BgD6/+7/BgD6//H/BgD6//T/BgD6//f/BgD6//r/BgD6//3/BgD6/wAABgD6/wMABgD6/wYABgD6/wkABgD6/wwABgD6/w8ABgD6/xIABgD6/xUABgD6/xgABgD6/xsABgD6/x4ABgD6/yEABgD7/97/BgD7/+H/BgD7/+T/BgD7/+f/BgD7/+r/BgD7/+3/BgD7//D/BgD7//P/BgD7//b/BgD7//n/BgD7//z/BgD7////BgD7/wIABgD7/wUABgD7/wgABgD7/wsABgD7/w4ABgD7/xEABgD7/xQABgD7/xcABgD7/xoABgD7/x0ABgD7/yAABgD8/93/BgD8/+D/BgD8/+P/BgD8/+b/BgD8/+n/BgD8/+z/BgD8/+//BgD8//L/BgD8//X/BgD8//j/BgD8//v/BgD8//7/BgD8/wEABgD8/wQABgD8/wcABgD8/woABgD8/w0ABgD8/xAABgD8/xMABgD8/xYABgD8/xkABgD8/xwABgD8/x8ABgD8/yIABgD9/9//BgD9/+L/BgD9/+X/BgD9/+j/BgD9/+v/BgD9/+7/BgD9//H/BgD9//T/BgD9//f/BgD9//r/BgD9//3/BgD9/wAABgD9/wMABgD9/wYABgD9/wkABgD9/wwABgD9/w8ABgD9/xIABgD9/xUABgD9/xgABgD9/xsABgD9/x4ABgD9/yEABgD+/97/BgD+/+H/BgD+/+T/BgD+/+f/BgD+/+r/BgD+/+3/BgD+//D/BgD+//P/BgD+//b/BgD+//n/BgD+//z/BgD+////BgD+/wIABgD+/wUABgD+/wgABgD+/wsABgD+/w4ABgD+/xEABgD+/xQABgD+/xcABgD+/xoABgD+/x0ABgD+/yAABgD+/yMABgD//+D/BgD//+P/BgD//+b/BgD//+n/BgD//+z/BgD//+//BgD///L/BgD///X/BgD///j/BgD///v/BgD///7/BgD//wEABgD//wQABgD//wcABgD//woABgD//w0ABgD//xAABgD//xMABgD//xYABgD//xkABgD//xwABgD//x8ABgD//yIABgAAAN//BgAAAOL/BgAAAOX/BgAAAOj/BgAAAOv/BgAAAO7/BgAAAPH/BgAAAPT/BgAAAPf/BgAAAPr/BgAAAP3/BgAAAAAABgAAAAMABgAAAAYABgAAAAkABgAAAAwABgAAAA8ABgAAABIABgAAABUABgAAABgABgAAABsABgAAAB4ABgAAACEABgABAOH/BgABAOT/BgABAOf/BgABAOr/BgABAO3/BgABAPD/BgABAPP/BgABAPb/BgABAPn/BgABAPz/BgABAP//BgABAAIABgABAAUABgABAAgABgABAAsABgABAA4ABgABABEABgABABQABgABABcABgABABoABgABAB0ABgABACAABgACAOP/BgACAOb/BgACAOn/BgACAOz/BgACAO//BgACAPL/BgACAPX/BgACAPj/BgACAPv/BgACAP7/BgACAAEABgACAAQABgACAAcABgACAAoABgACAA0ABgACABAABgACABMABgACABYABgACABkABgACABwABgACAB8ABgADAOX/BgADAOj/BgADAOv/BgADAO7/BgADAPH/BgADAPT/BgADAPf/BgADAPr/BgADAP3/BgADAAAABgADAAMABgADAAYABgADAAkABgADAAwABgADAA8ABgADABIABgADABUABgADABgABgADABsABgAEAOf/BgAEAOr/BgAEAO3/BgAEAPD/BgAEAPP/BgAEAPb/BgAEAPn/BgAEAPz/BgAEAP//BgAEAAIABgAEAAUABgAEAAgABgAEAAsABgAEAA4ABgAEABEABgAEABQABgAEABcABgAEABoABgAFAOn/BgAFAOz/BgAFAO//BgAFAPL/BgAFAPX/BgAFAPj/BgAFAPv/BgAFAP7/BgAFAAEABgAFAAQABgAFAAcABgAFAAoABgAFAA0ABgAFABAABgAFABMABgAFABYABgAGAOv/BgAGAO7/BgAGAPH/BgAGAPT/BgAGAPf/BgAGAPr/BgAGAP3/BgAGAAAABgAGAAMABgAGAAYABgAGAAkABgAGAAwABgAGAA8ABgAGABIABgAGABUABgAHAPD/BgAHAPP/BgAHAPb/BgAHAPn/BgAHAPz/BgAHAP//BgAHAAIABgAHAAUABgAHAAgABgAHAAsABgAHAA4ABgAIAPv/BgAIAP7/BgAIAAEABgAIAAQABgAIAAcABwDy//r/BwDy//3/BwDy/wAABwDy/wMABwDy/wYABwDz//D/BwDz//P/BwDz//b/BwDz//n/BwDz//z/BwDz////BwDz/wIABwDz/wUABwDz/wgABwDz/wsABwDz/w4ABwD0/+z/BwD0/+//BwD0//L/BwD0//X/BwD0//j/BwD0//v/BwD0//7/BwD0/wEABwD0/wQABwD0/wcABwD0/woABwD0/w0ABwD0/xAABwD0/xMABwD1/+j/BwD1/+v/BwD1/+7/BwD1//H/BwD1//T/BwD1//f/BwD1//r/BwD1//3/BwD1/wAABwD1/wMABwD1/wYABwD1/wkABwD1/wwABwD1/w8ABwD1/xIABwD1/xUABwD1/xgABwD2/+f/BwD2/+r/BwD2/+3/BwD2//D/BwD2//P/BwD2//b/BwD2//n/BwD2//z/BwD2////BwD2/wIABwD2/wUABwD2/wgABwD2/wsABwD2/w4ABwD2/xEABwD2/xQABwD2/xcABwD2/xoABwD3/+P/BwD3/+b/BwD3/+n/BwD3/+z/BwD3/+//BwD3//L/BwD3//X/BwD3//j/BwD3//v/BwD3//7/BwD3/wEABwD3/wQABwD3/wcABwD3/woABwD3/w0ABwD3/xAABwD3/xMABwD3/xYABwD3/xkABwD3/xwABwD4/+L/BwD4/+X/BwD4/+j/BwD4/+v/BwD4/+7/BwD4//H/BwD4//T/BwD4//f/BwD4//r/BwD4//3/BwD4/wAABwD4/wMABwD4/wYABwD4/wkABwD4/wwABwD4/w8ABwD4/xIABwD4/xUABwD4/xgABwD4/xsABwD4/x4ABwD5/+H/BwD5/+T/BwD5/+f/BwD5/+r/BwD5/+3/BwD5//D/BwD5//P/BwD5//b/BwD5//n/BwD5//z/BwD5////BwD5/wIABwD5/wUABwD5/wgABwD5/wsABwD5/w4ABwD5/xEABwD5/xQABwD5/xcABwD5/xoABwD5/x0ABwD5/yAABwD6/+D/BwD6/+P/BwD6/+b/BwD6/+n/BwD6/+z/BwD6/+//BwD6//L/BwD6//X/BwD6//j/BwD6//v/BwD6//7/BwD6/wEABwD6/wQABwD6/wcABwD6/woABwD6/w0ABwD6/xAABwD6/xMABwD6/xYABwD6/xkABwD6/xwABwD6/x8ABwD7/9//BwD7/+L/BwD7/+X/BwD7/+j/BwD7/+v/BwD7/+7/BwD7//H/BwD7//T/BwD7//f/BwD7//r/BwD7//3/BwD7/wAABwD7/wMABwD7/wYABwD7/wkABwD7/wwABwD7/w8ABwD7/xIABwD7/xUABwD7/xgABwD7/xsABwD7/x4ABwD7/yEABwD8/+H/BwD8/+T/BwD8/+f/BwD8/+r/BwD8/+3/BwD8//D/BwD8//P/BwD8//b/BwD8//n/BwD8//z/BwD8////BwD8/wIABwD8/wUABwD8/wgABwD8/wsABwD8/w4ABwD8/xEABwD8/xQABwD8/xcABwD8/xoABwD8/x0ABwD8/yAABwD9/+D/BwD9/+P/BwD9/+b/BwD9/+n/BwD9/+z/BwD9/+//BwD9//L/BwD9//X/BwD9//j/BwD9//v/BwD9//7/BwD9/wEABwD9/wQABwD9/wcABwD9/woABwD9/w0ABwD9/xAABwD9/xMABwD9/xYABwD9/xkABwD9/xwABwD9/x8ABwD+/9//BwD+/+L/BwD+/+X/BwD+/+j/BwD+/+v/BwD+/+7/BwD+//H/BwD+//T/BwD+//f/BwD+//r/BwD+//3/BwD+/wAABwD+/wMABwD+/wYABwD+/wkABwD+/wwABwD+/w8ABwD+/xIABwD+/xUABwD+/xgABwD+/xsABwD+/x4ABwD+/yEABwD//+H/BwD//+T/BwD//+f/BwD//+r/BwD//+3/BwD///D/BwD///P/BwD///b/BwD///n/BwD///z/BwD/////BwD//wIABwD//wUABwD//wgABwD//wsABwD//w4ABwD//xEABwD//xQABwD//xcABwD//xoABwD//x0ABwD//yAABwAAAOD/BwAAAOP/BwAAAOb/BwAAAOn/BwAAAOz/BwAAAO//BwAAAPL/BwAAAPX/BwAAAPj/BwAAAPv/BwAAAP7/BwAAAAEABwAAAAQABwAAAAcABwAAAAoABwAAAA0ABwAAABAABwAAABMABwAAABYABwAAABkABwAAABwABwAAAB8ABwABAOL/BwABAOX/BwABAOj/BwABAOv/BwABAO7/BwABAPH/BwABAPT/BwABAPf/BwABAPr/BwABAP3/BwABAAAABwABAAMABwABAAYABwABAAkABwABAAwABwABAA8ABwABABIABwABABUABwABABgABwABABsABwABAB4ABwACAOT/BwACAOf/BwACAOr/BwACAO3/BwACAPD/BwACAPP/BwACAPb/BwACAPn/BwACAPz/BwACAP//BwACAAIABwACAAUABwACAAgABwACAAsABwACAA4ABwACABEABwACABQABwACABcABwACABoABwACAB0ABwADAOb/BwADAOn/BwADAOz/BwADAO//BwADAPL/BwADAPX/BwADAPj/BwADAPv/BwADAP7/BwADAAEABwADAAQABwADAAcABwADAAoABwADAA0ABwADABAABwADABMABwADABYABwADABkABwAEAOj/BwAEAOv/BwAEAO7/BwAEAPH/BwAEAPT/BwAEAPf/BwAEAPr/BwAEAP3/BwAEAAAABwAEAAMABwAEAAYABwAEAAkABwAEAAwABwAEAA8ABwAEABIABwAEABUABwAEABgABwAFAO3/BwAFAPD/BwAFAPP/BwAFAPb/BwAFAPn/BwAFAPz/BwAFAP//BwAFAAIABwAFAAUABwAFAAgABwAFAAsABwAFAA4ABwAFABEABwAFABQABwAGAPL/BwAGAPX/BwAGAPj/BwAGAPv/BwAGAP7/BwAGAAEABwAGAAQABwAGAAcABwAGAAoABwAGAA0ABwAGABAABwAHAPr/BwAHAP3/BwAHAAAABwAHAAMABwAHAAYACADy//v/CADy//7/CADy/wEACADy/wQACADy/wcACADz//H/CADz//T/CADz//f/CADz//r/CADz//3/CADz/wAACADz/wMACADz/wYACADz/wkACADz/wwACADz/w8ACAD0/+3/CAD0//D/CAD0//P/CAD0//b/CAD0//n/CAD0//z/CAD0////CAD0/wIACAD0/wUACAD0/wgACAD0/wsACAD0/w4ACAD0/xEACAD0/xQACAD1/+n/CAD1/+z/CAD1/+//CAD1//L/CAD1//X/CAD1//j/CAD1//v/CAD1//7/CAD1/wEACAD1/wQACAD1/wcACAD1/woACAD1/w0ACAD1/xAACAD1/xMACAD1/xYACAD2/+j/CAD2/+v/CAD2/+7/CAD2//H/CAD2//T/CAD2//f/CAD2//r/CAD2//3/CAD2/wAACAD2/wMACAD2/wYACAD2/wkACAD2/wwACAD2/w8ACAD2/xIACAD2/xUACAD2/xgACAD3/+T/CAD3/+f/CAD3/+r/CAD3/+3/CAD3//D/CAD3//P/CAD3//b/CAD3//n/CAD3//z/CAD3////CAD3/wIACAD3/wUACAD3/wgACAD3/wsACAD3/w4ACAD3/xEACAD3/xQACAD3/xcACAD3/xoACAD4/+P/CAD4/+b/CAD4/+n/CAD4/+z/CAD4/+//CAD4//L/CAD4//X/CAD4//j/CAD4//v/CAD4//7/CAD4/wEACAD4/wQACAD4/wcACAD4/woACAD4/w0ACAD4/xAACAD4/xMACAD4/xYACAD4/xkACAD4/xwACAD5/+L/CAD5/+X/CAD5/+j/CAD5/+v/CAD5/+7/CAD5//H/CAD5//T/CAD5//f/CAD5//r/CAD5//3/CAD5/wAACAD5/wMACAD5/wYACAD5/wkACAD5/wwACAD5/w8ACAD5/xIACAD5/xUACAD5/xgACAD5/xsACAD5/x4ACAD6/+H/CAD6/+T/CAD6/+f/CAD6/+r/CAD6/+3/CAD6//D/CAD6//P/CAD6//b/CAD6//n/CAD6//z/CAD6////CAD6/wIACAD6/wUACAD6/wgACAD6/wsACAD6/w4ACAD6/xEACAD6/xQACAD6/xcACAD6/xoACAD6/x0ACAD7/+D/CAD7/+P/CAD7/+b/CAD7/+n/CAD7/+z/CAD7/+//CAD7//L/CAD7//X/CAD7//j/CAD7//v/CAD7//7/CAD7/wEACAD7/wQACAD7/wcACAD7/woACAD7/w0ACAD7/xAACAD7/xMACAD7/xYACAD7/xkACAD7/xwACAD7/x8ACAD8/+L/CAD8/+X/CAD8/+j/CAD8/+v/CAD8/+7/CAD8//H/CAD8//T/CAD8//f/CAD8//r/CAD8//3/CAD8/wAACAD8/wMACAD8/wYACAD8/wkACAD8/wwACAD8/w8ACAD8/xIACAD8/xUACAD8/xgACAD8/xsACAD8/x4ACAD9/+H/CAD9/+T/CAD9/+f/CAD9/+r/CAD9/+3/CAD9//D/CAD9//P/CAD9//b/CAD9//n/CAD9//z/CAD9////CAD9/wIACAD9/wUACAD9/wgACAD9/wsACAD9/w4ACAD9/xEACAD9/xQACAD9/xcACAD9/xoACAD9/x0ACAD9/yAACAD+/+P/CAD+/+b/CAD+/+n/CAD+/+z/CAD+/+//CAD+//L/CAD+//X/CAD+//j/CAD+//v/CAD+//7/CAD+/wEACAD+/wQACAD+/wcACAD+/woACAD+/w0ACAD+/xAACAD+/xMACAD+/xYACAD+/xkACAD+/xwACAD+/x8ACAD//+L/CAD//+X/CAD//+j/CAD//+v/CAD//+7/CAD///H/CAD///T/CAD///f/CAD///r/CAD///3/CAD//wAACAD//wMACAD//wYACAD//wkACAD//wwACAD//w8ACAD//xIACAD//xUACAD//xgACAD//xsACAD//x4ACAAAAOT/CAAAAOf/CAAAAOr/CAAAAO3/CAAAAPD/CAAAAPP/CAAAAPb/CAAAAPn/CAAAAPz/CAAAAP//CAAAAAIACAAAAAUACAAAAAgACAAAAAsACAAAAA4ACAAAABEACAAAABQACAAAABcACAAAABoACAAAAB0ACAABAOb/CAABAOn/CAABAOz/CAABAO//CAABAPL/CAABAPX/CAABAPj/CAABAPv/CAABAP7/CAABAAEACAABAAQACAABAAcACAABAAoACAABAA0ACAABABAACAABABMACAABABYACAABABkACAABABwACAACAOj/CAACAOv/CAACAO7/CAACAPH/CAACAPT/CAACAPf/CAACAPr/CAACAP3/CAACAAAACAACAAMACAACAAYACAACAAkACAACAAwACAACAA8ACAACABIACAACABUACAACABgACAADAOr/CAADAO3/CAADAPD/CAADAPP/CAADAPb/CAADAPn/CAADAPz/CAADAP//CAADAAIACAADAAUACAADAAgACAADAAsACAADAA4ACAADABEACAADABQACAADABcACAAEAOz/CAAEAO//CAAEAPL/CAAEAPX/CAAEAPj/CAAEAPv/CAAEAP7/CAAEAAEACAAEAAQACAAEAAcACAAEAAoACAAEAA0ACAAEABAACAAEABMACAAFAPH/CAAFAPT/CAAFAPf/CAAFAPr/CAAFAP3/CAAFAAAACAAFAAMACAAFAAYACAAFAAkACAAFAAwACAAFAA8ACAAGAPn/CAAGAPz/CAAGAP//CAAGAAIACAAGAAUACQDy//z/CQDy////CQDy/wIACQDz//L/CQDz//X/CQDz//j/CQDz//v/CQDz//7/CQDz/wEACQDz/wQACQDz/wcACQDz/woACQDz/w0ACQD0/+7/CQD0//H/CQD0//T/CQD0//f/CQD0//r/CQD0//3/CQD0/wAACQD0/wMACQD0/wYACQD0/wkACQD0/wwACQD0/w8ACQD0/xIACQD1/+r/CQD1/+3/CQD1//D/CQD1//P/CQD1//b/CQD1//n/CQD1//z/CQD1////CQD1/wIACQD1/wUACQD1/wgACQD1/wsACQD1/w4ACQD1/xEACQD1/xQACQD2/+n/CQD2/+z/CQD2/+//CQD2//L/CQD2//X/CQD2//j/CQD2//v/CQD2//7/CQD2/wEACQD2/wQACQD2/wcACQD2/woACQD2/w0ACQD2/xAACQD2/xMACQD2/xYACQD3/+j/CQD3/+v/CQD3/+7/CQD3//H/CQD3//T/CQD3//f/CQD3//r/CQD3//3/CQD3/wAACQD3/wMACQD3/wYACQD3/wkACQD3/wwACQD3/w8ACQD3/xIACQD3/xUACQD3/xgACQD4/+T/CQD4/+f/CQD4/+r/CQD4/+3/CQD4//D/CQD4//P/CQD4//b/CQD4//n/CQD4//z/CQD4////CQD4/wIACQD4/wUACQD4/wgACQD4/wsACQD4/w4ACQD4/xEACQD4/xQACQD4/xcACQD4/xoACQD5/+P/CQD5/+b/CQD5/+n/CQD5/+z/CQD5/+//CQD5//L/CQD5//X/CQD5//j/CQD5//v/CQD5//7/CQD5/wEACQD5/wQACQD5/wcACQD5/woACQD5/w0ACQD5/xAACQD5/xMACQD5/xYACQD5/xkACQD5/xwACQD6/+X/CQD6/+j/CQD6/+v/CQD6/+7/CQD6//H/CQD6//T/CQD6//f/CQD6//r/CQD6//3/CQD6/wAACQD6/wMACQD6/wYACQD6/wkACQD6/wwACQD6/w8ACQD6/xIACQD6/xUACQD6/xgACQD6/xsACQD7/+T/CQD7/+f/CQD7/+r/CQD7/+3/CQD7//D/CQD7//P/CQD7//b/CQD7//n/CQD7//z/CQD7////CQD7/wIACQD7/wUACQD7/wgACQD7/wsACQD7/w4ACQD7/xEACQD7/xQACQD7/xcACQD7/xoACQD7/x0ACQD8/+P/CQD8/+b/CQD8/+n/CQD8/+z/CQD8/+//CQD8//L/CQD8//X/CQD8//j/CQD8//v/CQD8//7/CQD8/wEACQD8/wQACQD8/wcACQD8/woACQD8/w0ACQD8/xAACQD8/xMACQD8/xYACQD8/xkACQD8/xwACQD9/+X/CQD9/+j/CQD9/+v/CQD9/+7/CQD9//H/CQD9//T/CQD9//f/CQD9//r/CQD9//3/CQD9/wAACQD9/wMACQD9/wYACQD9/wkACQD9/wwACQD9/w8ACQD9/xIACQD9/xUACQD9/xgACQD9/xsACQD+/+T/CQD+/+f/CQD+/+r/CQD+/+3/CQD+//D/CQD+//P/CQD+//b/CQD+//n/CQD+//z/CQD+////CQD+/wIACQD+/wUACQD+/wgACQD+/wsACQD+/w4ACQD+/xEACQD+/xQACQD+/xcACQD+/xoACQD+/x0ACQD//+b/CQD//+n/CQD//+z/CQD//+//CQD///L/CQD///X/CQD///j/CQD///v/CQD///7/CQD//wEACQD//wQACQD//wcACQD//woACQD//w0ACQD//xAACQD//xMACQD//xYACQD//xkACQD//xwACQAAAOj/CQAAAOv/CQAAAO7/CQAAAPH/CQAAAPT/CQAAAPf/CQAAAPr/CQAAAP3/CQAAAAAACQAAAAMACQAAAAYACQAAAAkACQAAAAwACQAAAA8ACQAAABIACQAAABUACQAAABgACQABAOr/CQABAO3/CQABAPD/CQABAPP/CQABAPb/CQABAPn/CQABAPz/CQABAP//CQABAAIACQABAAUACQABAAgACQABAAsACQABAA4ACQABABEACQABABQACQABABcACQACAOz/CQACAO//CQACAPL/CQACAPX/CQACAPj/CQACAPv/CQACAP7/CQACAAEACQACAAQACQACAAcACQACAAoACQACAA0ACQACABAACQACABMACQACABYACQADAO7/CQADAPH/CQADAPT/CQADAPf/CQADAPr/CQADAP3/CQADAAAACQADAAMACQADAAYACQADAAkACQADAAwACQADAA8ACQADABIACQAEAPP/CQAEAPb/CQAEAPn/CQAEAPz/CQAEAP//CQAEAAIACQAEAAUACQAEAAgACQAEAAsACQAEAA4ACQAFAP7/CQAFAAEACQAFAAQACgDz//b/CgDz//n/CgDz//z/CgDz////CgDz/wIACgDz/wUACgDz/wgACgDz/wsACgD0//L/CgD0//X/CgD0//j/CgD0//v/CgD0//7/CgD0/wEACgD0/wQACgD0/wcACgD0/woACgD0/w0ACgD0/xAACgD1/+7/CgD1//H/CgD1//T/CgD1//f/CgD1//r/CgD1//3/CgD1/wAACgD1/wMACgD1/wYACgD1/wkACgD1/wwACgD1/w8ACgD1/xIACgD2/+r/CgD2/+3/CgD2//D/CgD2//P/CgD2//b/CgD2//n/CgD2//z/CgD2////CgD2/wIACgD2/wUACgD2/wgACgD2/wsACgD2/w4ACgD2/xEACgD2/xQACgD3/+n/CgD3/+z/CgD3/+//CgD3//L/CgD3//X/CgD3//j/CgD3//v/CgD3//7/CgD3/wEACgD3/wQACgD3/wcACgD3/woACgD3/w0ACgD3/xAACgD3/xMACgD3/xYACgD4/+j/CgD4/+v/CgD4/+7/CgD4//H/CgD4//T/CgD4//f/CgD4//r/CgD4//3/CgD4/wAACgD4/wMACgD4/wYACgD4/wkACgD4/wwACgD4/w8ACgD4/xIACgD4/xUACgD4/xgACgD5/+f/CgD5/+r/CgD5/+3/CgD5//D/CgD5//P/CgD5//b/CgD5//n/CgD5//z/CgD5////CgD5/wIACgD5/wUACgD5/wgACgD5/wsACgD5/w4ACgD5/xEACgD5/xQACgD5/xcACgD5/xoACgD6/+b/CgD6/+n/CgD6/+z/CgD6/+//CgD6//L/CgD6//X/CgD6//j/CgD6//v/CgD6//7/CgD6/wEACgD6/wQACgD6/wcACgD6/woACgD6/w0ACgD6/xAACgD6/xMACgD6/xYACgD6/xkACgD7/+X/CgD7/+j/CgD7/+v/CgD7/+7/CgD7//H/CgD7//T/CgD7//f/CgD7//r/CgD7//3/CgD7/wAACgD7/wMACgD7/wYACgD7/wkACgD7/wwACgD7/w8ACgD7/xIACgD7/xUACgD7/xgACgD7/xsACgD8/+f/CgD8/+r/CgD8/+3/CgD8//D/CgD8//P/CgD8//b/CgD8//n/CgD8//z/CgD8////CgD8/wIACgD8/wUACgD8/wgACgD8/wsACgD8/w4ACgD8/xEACgD8/xQACgD8/xcACgD8/xoACgD9/+b/CgD9/+n/CgD9/+z/CgD9/+//CgD9//L/CgD9//X/CgD9//j/CgD9//v/CgD9//7/CgD9/wEACgD9/wQACgD9/wcACgD9/woACgD9/w0ACgD9/xAACgD9/xMACgD9/xYACgD9/xkACgD+/+j/CgD+/+v/CgD+/+7/CgD+//H/CgD+//T/CgD+//f/CgD+//r/CgD+//3/CgD+/wAACgD+/wMACgD+/wYACgD+/wkACgD+/wwACgD+/w8ACgD+/xIACgD+/xUACgD+/xgACgD//+r/CgD//+3/CgD///D/CgD///P/CgD///b/CgD///n/CgD///z/CgD/////CgD//wIACgD//wUACgD//wgACgD//wsACgD//w4ACgD//xEACgD//xQACgD//xcACgAAAOz/CgAAAO//CgAAAPL/CgAAAPX/CgAAAPj/CgAAAPv/CgAAAP7/CgAAAAEACgAAAAQACgAAAAcACgAAAAoACgAAAA0ACgAAABAACgAAABMACgAAABYACgABAO7/CgABAPH/CgABAPT/CgABAPf/CgABAPr/CgABAP3/CgABAAAACgABAAMACgABAAYACgABAAkACgABAAwACgABAA8ACgABABIACgACAPD/CgACAPP/CgACAPb/CgACAPn/CgACAPz/CgACAP//CgACAAIACgACAAUACgACAAgACgACAAsACgACAA4ACgADAPX/CgADAPj/CgADAPv/CgADAP7/CgADAAEACgADAAQACgADAAcACgADAAoACwDz//r/CwDz//3/CwDz/wAACwDz/wMACwDz/wYACwD0//P/CwD0//b/CwD0//n/CwD0//z/CwD0////CwD0/wIACwD0/wUACwD0/wgACwD0/wsACwD0/w4ACwD1/+//CwD1//L/CwD1//X/CwD1//j/CwD1//v/CwD1//7/CwD1/wEACwD1/wQACwD1/wcACwD1/woACwD1/w0ACwD1/xAACwD2/+7/CwD2//H/CwD2//T/CwD2//f/CwD2//r/CwD2//3/CwD2/wAACwD2/wMACwD2/wYACwD2/wkACwD2/wwACwD2/w8ACwD2/xIACwD3/+r/CwD3/+3/CwD3//D/CwD3//P/CwD3//b/CwD3//n/CwD3//z/CwD3////CwD3/wIACwD3/wUACwD3/wgACwD3/wsACwD3/w4ACwD3/xEACwD3/xQACwD4/+n/CwD4/+z/CwD4/+//CwD4//L/CwD4//X/CwD4//j/CwD4//v/CwD4//7/CwD4/wEACwD4/wQACwD4/wcACwD4/woACwD4/w0ACwD4/xAACwD4/xMACwD4/xYACwD5/+j/CwD5/+v/CwD5/+7/CwD5//H/CwD5//T/CwD5//f/CwD5//r/CwD5//3/CwD5/wAACwD5/wMACwD5/wYACwD5/wkACwD5/wwACwD5/w8ACwD5/xIACwD5/xUACwD5/xgACwD6/+r/CwD6/+3/CwD6//D/CwD6//P/CwD6//b/CwD6//n/CwD6//z/CwD6////CwD6/wIACwD6/wUACwD6/wgACwD6/wsACwD6/w4ACwD6/xEACwD6/xQACwD6/xcACwD7/+n/CwD7/+z/CwD7/+//CwD7//L/CwD7//X/CwD7//j/CwD7//v/CwD7//7/CwD7/wEACwD7/wQACwD7/wcACwD7/woACwD7/w0ACwD7/xAACwD7/xMACwD7/xYACwD8/+j/CwD8/+v/CwD8/+7/CwD8//H/CwD8//T/CwD8//f/CwD8//r/CwD8//3/CwD8/wAACwD8/wMACwD8/wYACwD8/wkACwD8/wwACwD8/w8ACwD8/xIACwD8/xUACwD8/xgACwD9/+r/CwD9/+3/CwD9//D/CwD9//P/CwD9//b/CwD9//n/CwD9//z/CwD9////CwD9/wIACwD9/wUACwD9/wgACwD9/wsACwD9/w4ACwD9/xEACwD9/xQACwD9/xcACwD+/+z/CwD+/+//CwD+//L/CwD+//X/CwD+//j/CwD+//v/CwD+//7/CwD+/wEACwD+/wQACwD+/wcACwD+/woACwD+/w0ACwD+/xAACwD+/xMACwD+/xYACwD//+7/CwD///H/CwD///T/CwD///f/CwD///r/CwD///3/CwD//wAACwD//wMACwD//wYACwD//wkACwD//wwACwD//w8ACwD//xIACwAAAPD/CwAAAPP/CwAAAPb/CwAAAPn/CwAAAPz/CwAAAP//CwAAAAIACwAAAAUACwAAAAgACwAAAAsACwAAAA4ACwAAABEACwABAPL/CwABAPX/CwABAPj/CwABAPv/CwABAP7/CwABAAEACwABAAQACwABAAcACwABAAoACwABAA0ACwACAPr/CwACAP3/CwACAAAACwACAAMACwACAAYADAD0//f/DAD0//r/DAD0//3/DAD0/wAADAD0/wMADAD0/wYADAD0/wkADAD1//P/DAD1//b/DAD1//n/DAD1//z/DAD1////DAD1/wIADAD1/wUADAD1/wgADAD1/wsADAD1/w4ADAD2//L/DAD2//X/DAD2//j/DAD2//v/DAD2//7/DAD2/wEADAD2/wQADAD2/wcADAD2/woADAD2/w0ADAD2/xAADAD3/+7/DAD3//H/DAD3//T/DAD3//f/DAD3//r/DAD3//3/DAD3/wAADAD3/wMADAD3/wYADAD3/wkADAD3/wwADAD3/w8ADAD3/xIADAD4/+3/DAD4//D/DAD4//P/DAD4//b/DAD4//n/DAD4//z/DAD4////DAD4/wIADAD4/wUADAD4/wgADAD4/wsADAD4/w4ADAD4/xEADAD4/xQADAD5/+z/DAD5/+//DAD5//L/DAD5//X/DAD5//j/DAD5//v/DAD5//7/DAD5/wEADAD5/wQADAD5/wcADAD5/woADAD5/w0ADAD5/xAADAD5/xMADAD6/+v/DAD6/+7/DAD6//H/DAD6//T/DAD6//f/DAD6//r/DAD6//3/DAD6/wAADAD6/wMADAD6/wYADAD6/wkADAD6/wwADAD6/w8ADAD6/xIADAD6/xUADAD7/+3/DAD7//D/DAD7//P/DAD7//b/DAD7//n/DAD7//z/DAD7////DAD7/wIADAD7/wUADAD7/wgADAD7/wsADAD7/w4ADAD7/xEADAD7/xQADAD8/+z/DAD8/+//DAD8//L/DAD8//X/DAD8//j/DAD8//v/DAD8//7/DAD8/wEADAD8/wQADAD8/wcADAD8/woADAD8/w0ADAD8/xAADAD8/xMADAD9/+7/DAD9//H/DAD9//T/DAD9//f/DAD9//r/DAD9//3/DAD9/wAADAD9/wMADAD9/wYADAD9/wkADAD9/wwADAD9/w8ADAD9/xIADAD+//D/DAD+//P/DAD+//b/DAD+//n/DAD+//z/DAD+////DAD+/wIADAD+/wUADAD+/wgADAD+/wsADAD+/w4ADAD///L/DAD///X/DAD///j/DAD///v/DAD///7/DAD//wEADAD//wQADAD//wcADAD//woADAD//w0ADAAAAPf/DAAAAPr/DAAAAP3/DAAAAAAADAAAAAMADAAAAAYADAAAAAkADQD1//r/DQD1//3/DQD1/wAADQD1/wMADQD1/wYADQD2//b/DQD2//n/DQD2//z/DQD2////DQD2/wIADQD2/wUADQD2/wgADQD2/wsADQD3//L/DQD3//X/DQD3//j/DQD3//v/DQD3//7/DQD3/wEADQD3/wQADQD3/wcADQD3/woADQD3/w0ADQD4//H/DQD4//T/DQD4//f/DQD4//r/DQD4//3/DQD4/wAADQD4/wMADQD4/wYADQD4/wkADQD4/wwADQD4/w8ADQD5//D/DQD5//P/DQD5//b/DQD5//n/DQD5//z/DQD5////DQD5/wIADQD5/wUADQD5/wgADQD5/wsADQD5/w4ADQD6//L/DQD6//X/DQD6//j/DQD6//v/DQD6//7/DQD6/wEADQD6/wQADQD6/wcADQD6/woADQD6/w0ADQD6/xAADQD7//H/DQD7//T/DQD7//f/DQD7//r/DQD7//3/DQD7/wAADQD7/wMADQD7/wYADQD7/wkADQD7/wwADQD7/w8ADQD8//P/DQD8//b/DQD8//n/DQD8//z/DQD8////DQD8/wIADQD8/wUADQD8/wgADQD8/wsADQD8/w4ADQD9//X/DQD9//j/DQD9//v/DQD9//7/DQD9/wEADQD9/wQADQD9/wcADQD9/woADQD+//r/DQD+//3/DQD+/wAADQD+/wMADQD+/wYADgD3//z/DgD3////DgD3/wIADgD4//v/DgD4//7/DgD4/wEADgD4/wQADgD4/wcADgD5//r/DgD5//3/DgD5/wAADgD5/wMADgD5/wYADgD6//n/DgD6//z/DgD6////DgD6/wIADgD6/wUADgD7//7/DgD7/wEADgD7/wQA\",\"F2\":\"3qpJONcieDfVAFg0RMr0Np35XzTlOPw2ARUfOA2x7TZaUu82q0ijNbJefTirSKM1WlLvNg2x7TYBFR845Tj8Np35XzREyvQ21QBYNNcieDfeqkk4do3kN68z6DNAzN83rzPoM3aN5DeSIfc4AAAAAAAAAAAAAAAAUXlTOAAAAADqFHU4AAAAAHFgJzdwuQIzCsrJNwN+FDPlsZU3YoUZM/HBIjfTtg8zoraQOOwX9jKwhBA0S5dTNc0GLjQTTkU4Y25ANC+JxDdjbkA0E05FOM0GLjRLl1M1sIQQNAAAAAAAAAAAwwzmOAAAAAAW4uc1AAAAAE3YKTgAAAAAE3xMOAAAAACJ6CQ2iegkNgAAAAATfEw4AAAAAE3YKTgAAAAAFuLnNQAAAADDDOY4AAAAAAAAAACwhBA0S5dTNc0GLjQTTkU4Y25ANC+JxDdjbkA0E05FOM0GLjRLl1M1sIQQNOwX9jKitpA407YPM/HBIjdihRkz5bGVNwN+FDMKysk3cLkCM3FgJzcAAAAA6hR1OAAAAABReVM4AAAAAAAAAAAAAAAAkiH3OHaN5DevM+gzQMzfN68z6DN2jeQ3AAAAAJ/+mjcAAAAALBonOAAAAACf/po3AAAAAGuErzeEAjk3c//MN+OekziV/No3GvmgNT3Q0zcuBC4zBni6N0BPgTj2vRs4RHnPNRDGpDZmx+01xr37NX1u9jXgri04kpblNcX+ETifgsI1j2GeN+PSiTfgf6k1rFRxNlikzzVrJKI3hvDnNRMFlziG8Oc1aySiN1ikzzWsVHE24H+pNePSiTeHj4I3J2w0OP0LqTeVl583ZtvKNzbPtDhfZds3utQSNR7g0jfGDbEzw3C1N05RmTipO483WCU9N0QllDdurQk3CfWAOGRhLzetD901PaRMN4rOqTMKJVU3iRiXOB+nRDfY6fY3Xx0jN2DwDjhjfvo2JCcGNlaVrTcCWzI2MWOzNkHSXTZRMqM3vgd6Nnueuzi+B3o2UTKjN0HSXTYxY7M2AlsyNlaVrTckJwY2Y376NmDwDjhfHSM32On2Nx+nRDeJGJc4CiVVN4rOqTM9pEw3rQ/dNWRhLzcJ9YA4bq0JN0QllDdYJT03qTuPN05RmTjDcLU3xg2xMx7g0je61BI1X2XbNzbPtDhm28o3lZefN/0LqTcnbDQ4h4+CN+PSiTfgf6k1rFRxNlikzzVrJKI3hvDnNRMFlziG8Oc1aySiN1ikzzWsVHE24H+pNePSiTePYZ43n4LCNcX+ETiSluU14K4tOH1u9jXGvfs1ZsftNRDGpDZEec819r0bOEBPgTgGeLo3LgQuMz3Q0zca+aA1lfzaN+Oekzhz/8w3hAI5N2uErzcAAAAAn/6aNwAAAAAsGic4AAAAAJ/+mjcAAAAAdo3kN68z6DNAzN83rzPoM3aN5DdrhK83hAI5N3P/zDfjnpM4lfzaNxr5oDU90NM3LgQuMwZ4ujdAT4E4AAAAAMINKzgAAAAAoueQNgAAAADoHcM1AAAAAEaGQDgAAAAA/UQPOAAAAAA48bA34akiN4EZwDUEVhE1oqftNZMkFTiVgwU2fc9sOJWDBTaTJBU4oqftNQRWETWBGcA14akiN69v0Db0SK03t5BfOJqD5Dc5Ap033SQLONHm4jhShRc4kYa9NdQgETgAAAAAoqD2N9nIuzixS783sdMzN0ZYMTaqQ7Q3/E91NuMklzhFP6A2NKocNm9pvjbRFkI0kVXHNvP3tTjJF7Y27YobOFvbkzbpEyU40C5dNlbMCDZdfvo32u2KNicXgjc2M782vZ7pNaBE9TZIni44D9wMN3PdtzgP3Aw3SJ4uOKBE9Ta9nuk1NjO/NicXgjfa7Yo2XX76NyTMoTYlv2Q31nNmOJ1Amze2s/k3vZ3BN11b+TiJNdU3rXzHNDEMyzfJz8s0CQ2pN7FDyTgrAX83xriLN/+XNTf/lzU3xriLNysBfzexQ8k4CQ2pN8nPyzQxDMs3rXzHNIk11TddW/k4vZ3BN7az+TedQJs31nNmOCW/ZDckzKE2XX76N9rtijYnF4I3NjO/Nr2e6TWgRPU2SJ4uOA/cDDdz3bc4D9wMN0ieLjigRPU2vZ7pNTYzvzYnF4I32u2KNl1++jdWzAg20C5dNukTJThb25M27YobOMkXtjbz97U4kVXHNtEWQjRvab42NKocNkU/oDbjJJc4/E91NqpDtDdGWDE2sdMzN7FLvzfZyLs4oqD2NwAAAADUIBE4kYa9NVKFFzjR5uI43SQLODkCnTeag+Q3t5BfOPRIrTevb9A24akiN4EZwDUEVhE1oqftNZMkFTiVgwU2fc9sOJWDBTaTJBU4oqftNQRWETWBGcA14akiNzjxsDcAAAAA/UQPOAAAAABGhkA4AAAAAOgdwzUAAAAAoueQNgAAAADCDSs4AAAAAEBPgTgGeLo3LgQuMz3Q0zca+aA1lfzaN+Oekzhz/8w3hAI5N2uErzd2jeQ3rzPoM0DM3zevM+gzdo3kN5Ih9zgAAAAAAAAAAAAAAABReVM4AAAAAOoUdTgAAAAA9r0bOER5zzUQxqQ2ZsftNca9+zV9bvY14K4tOJKW5TXF/hE4n4LCNY9hnjfhqSI3gRnANQRWETWip+01kyQVOJWDBTZ9z2w4lYMFNpMkFTiip+01BFYRNYEZwDXhqSI3N3LZNwAAAAASpgw2AAAAAHAIXTkAAAAAPd4KMwAAAAD02cc4AAAAAE1K4DgAAAAA9C0INAAAAABKKQg58F48NYKndDi9QoQ1rw0FOCVcrzWGM+w3G6DSNZMfoDeIKt01MU4WOIvXyDXaQ9E45QuhNZmJRzeVSG01rs+0NaWaZzjighw2OC2qNqHfXDbPnUw1HvuQNoKwwTgE8ag2CyVxOATxqDaCsME4HvuQNs+dTDWh31w2OC2qNuKCHDalmmc4stzrMhPMzjceLy0zX9XdNHeeczOOrIw574+cMxChmjXY6q4zz1/xOP9YpTPXXgc58B+GM7aCIDbIYUMz65IeOd5fBjO4Crc1wMHMMyEkkDRBBfM4dKjTNGngbjezxRI1Zm+yOFJVNjWoapQ4KFBBNcpcbDcuSSw1u+NbOdrcBDVeRQQ2P/m6NNfiPTf3QHw0hpYINuTJkzgoSkw2+RVfNt5ylDaEmzs2El/INj4dCDnuou02ebdsOO6i7TY+HQg5El/INoSbOzbecpQ2+RVfNihKTDbkyZM4hpYINvdAfDTX4j03P/m6NF5FBDba3AQ1u+NbOS5JLDXKXGw3KFBBNahqlDhSVTY1Zm+yOLPFEjVp4G43dKjTNEEF8zghJJA0wMHMM7gKtzXeXwYz65IeOchhQzO2giA28B+GM9deBzn/WKUzz1/xONjqrjMQoZo174+cM46sjDl3nnMzX9XdNB4vLTMTzM43stzrMqWaZzjighw2OC2qNqHfXDbPnUw1HvuQNoKwwTgE8ag2CyVxOATxqDaCsME4HvuQNs+dTDWh31w2OC2qNuKCHDalmmc4rs+0NZVIbTWZiUc35QuhNdpD0TiL18g1MU4WOIgq3TWTH6A3G6DSNYYz7DclXK81rw0FOL1ChDWCp3Q48F48NUopCDkAAAAA9C0INAAAAABNSuA4AAAAAPTZxzgAAAAAPd4KMwAAAABwCF05AAAAABKmDDYAAAAAN3LZN+GpIjeBGcA1BFYRNaKn7TWTJBU4lYMFNn3PbDiVgwU2kyQVOKKn7TUEVhE1gRnANeGpIjePYZ43n4LCNcX+ETiSluU14K4tOH1u9jXGvfs1ZsftNRDGpDZEec819r0bOAAAAADqFHU4AAAAAFF5UzgAAAAAAAAAAAAAAACSIfc43qpJONcieDfVAFg0cWAnN3C5AjMKysk3A34UM+WxlTdihRkz8cEiN9O2DzOitpA47Bf2MuPSiTfgf6k1rFRxNlikzzVrJKI3hvDnNRMFlziG8Oc1aySiN1ikzzWsVHE24H+pNePSiTevb9A29EitN7eQXziag+Q3OQKdN90kCzjR5uI4UoUXOJGGvTXUIBE4AAAAAKKg9jfZyLs4sUu/N7HTMzfwXjw1gqd0OL1ChDWvDQU4JVyvNYYz7DcboNI1kx+gN4gq3TUxThY4i9fINdpD0TjlC6E1mYlHN5VIbTWuz7Q1lMGDNwAAAADOfTo4AAAAAFzglTcAAAAAnl6SNwAAAABOxEc5AAAAAJ5ekjcAAAAAXOCVNwAAAADOfTo4AAAAAJTBgze84y02He+gN1Qz/DZG8vA3cuWnOEkWLTgEdB041pRiONuuQjk5an84j3+7NXxVcDjF7vczm98/OF8GEzmr3gg4oMajN0R+uDc4vwg4lq+HNqpYYjc6gdA2HVhpONI1Hje7ha84rxxjN123njfE4ZA3NRIbN5nNmjfBw+I4lueHNxHw4DhaTUs3gvEjOPIqCjf2+oUzzqq0NpUOcTec5Oc0U0ZrN0GjNDV5aKE4j7OJNcnkJTgMy8I1yI9oN5ky7jVnxLU5mTLuNciPaDcMy8I1yeQlOI+ziTV5aKE4QaM0NVNGazec5Oc0XL3qNnOJczd7n282d7q+N3BdnTh8LhA4x2udOPCNRTgg/Ew58s5jOMaMfDR221M4jrmVNjaCIjhY0xQ5AlfcN594HDhFko03uqvlN3eJNDd3iTQ3uqvlN0WSjTefeBw4AlfcN1jTFDk2giI4jrmVNnbbUzjGjHw08s5jOCD8TDnwjUU4x2udOHwuEDhwXZ04d7q+N3ufbzZziXM3XL3qNpzk5zRTRms3QaM0NXlooTiPs4k1yeQlOAzLwjXIj2g3mTLuNWfEtTmZMu41yI9oNwzLwjXJ5CU4j7OJNXlooThBozQ1U0ZrN5zk5zSVDnE3zqq0Nvb6hTPyKgo3gvEjOFpNSzcR8OA4lueHN8HD4jiZzZo3NRIbN8ThkDddt543rxxjN7uFrzjSNR43HVhpODqB0DaqWGI3lq+HNji/CDhEfrg3oMajN6veCDhfBhM5m98/OMXu9zN8VXA4j3+7NTlqfzjbrkI51pRiOAR0HThJFi04cuWnOEby8DdUM/w2He+gN7zjLTaUwYM3AAAAAM59OjgAAAAAXOCVNwAAAACeXpI3AAAAAE7ERzkAAAAAnl6SNwAAAABc4JU3AAAAAM59OjgAAAAAlMGDN67PtDWVSG01mYlHN+ULoTXaQ9E4i9fINTFOFjiIKt01kx+gNxug0jWGM+w3JVyvNa8NBTi9QoQ1gqd0OPBePDWx0zM3sUu/N9nIuziioPY3AAAAANQgETiRhr01UoUXONHm4jjdJAs4OQKdN5qD5De3kF849EitN69v0Dbj0ok34H+pNaxUcTZYpM81aySiN4bw5zUTBZc4hvDnNWskojdYpM81rFRxNuB/qTXj0ok37Bf2MqK2kDjTtg8z8cEiN2KFGTPlsZU3A34UMwrKyTdwuQIzcWAnN9UAWDTXIng33qpJOETK9Dad+V805Tj8NgEVHzgNse02sIQQNEuXUzXNBi40E05FOGNuQDQvicQ3Y25ANBNORTjNBi40S5dTNbCEEDSHj4I3J2w0OP0LqTeVl583ZtvKNzbPtDhfZds3utQSNR7g0jfGDbEzw3C1N05RmTipO483WCU9N0ZYMTaqQ7Q3/E91NuMklzhFP6A2NKocNm9pvjbRFkI0kVXHNvP3tTjJF7Y27YobOFvbkzbpEyU40C5dNlbMCDalmmc44oIcNjgtqjah31w2z51MNR77kDaCsME4BPGoNgslcTgE8ag2grDBOB77kDbPnUw1od9cNjgtqjbighw2pZpnOLzjLTYd76A3VDP8Nkby8Ddy5ac4SRYtOAR0HTjWlGI4265COTlqfziPf7s1fFVwOMXu9zOb3z84XwYTOaveCDigxqM3RH64Nzi/CDgAAAAA14/yNwAAAABL8gQ4AAAAAPIQFTkAAAAAj/coNgAAAAAAAAAAAAAAAOE6SzkAAAAAuu+DOAAAAAByMKE4AAAAAKaKmTYAAAAAEPy5Nm+okTdo8Aw31I6iOBbWYDeu+iY38WWwN8UCODVgdgA4yuQsOV0wIDib9vg4XTAgOMrkLDlgdgA4xQI4NfFlsDeu+iY3FtZgN9SOojho8Aw3b6iRN66bMDergIU2fmmON4GyIDfcaeg3K6j4ODBrODg0A4c4xi+EOJ2qpjnE/5s4gycNNv1XjzjNvLg0rERTOI5pYTmEXQg4ab/9NyispzcthTA4GJBON04W+TbdQ4E2RpQpOD6L1DapPhg4T8QvN+VXZDmDyIo3NIewNb9dvzfA7mo1EkfRN4OhqzmXrq83ceWkOEm/cDepzPY4SOQUNx8lCDc2rrM2QmqxNsJIXDYNqKk3lCFeN/okuTjMiLg3GIZWN2FgGDiaZi41AjJqOParXDnPh5c4bdYmOc+Hlzj2q1w5AjJqOJpmLjVhYBg4GIZWN8yIuDf6JLk4lCFeNw2oqTfCSFw2QmqxNjauszYfJQg3SOQUN6nM9jhJv3A3ceWkOJeurzeDoas5EkfRN8DuajW/Xb83NIewNYPIijflV2Q5T8QvN6k+GDg+i9Q2RpQpON1DgTZOFvk2GJBONy2FMDgorKc3ab/9N4RdCDiOaWE5rERTOM28uDT9V484gycNNsT/mzidqqY5xi+EODQDhzgwazg4K6j4ONxp6DeBsiA3fmmON6uAhTaumzA3b6iRN2jwDDfUjqI4FtZgN676JjfxZbA3xQI4NWB2ADjK5Cw5XTAgOJv2+DhdMCA4yuQsOWB2ADjFAjg18WWwN676JjcW1mA31I6iOGjwDDdvqJE3EPy5NgAAAACmipk2AAAAAHIwoTgAAAAAuu+DOAAAAADhOks5AAAAAAAAAAAAAAAAj/coNgAAAADyEBU5AAAAAEvyBDgAAAAA14/yNwAAAAA4vwg4RH64N6DGozer3gg4XwYTOZvfPzjF7vczfFVwOI9/uzU5an84265COdaUYjgEdB04SRYtOHLlpzhG8vA3VDP8Nh3voDe84y02pZpnOOKCHDY4Lao2od9cNs+dTDUe+5A2grDBOATxqDYLJXE4BPGoNoKwwTge+5A2z51MNaHfXDY4Lao24oIcNqWaZzhWzAg20C5dNukTJThb25M27YobOMkXtjbz97U4kVXHNtEWQjRvab42NKocNkU/oDbjJJc4/E91NqpDtDdGWDE2WCU9N6k7jzdOUZk4w3C1N8YNsTMe4NI3utQSNV9l2zc2z7Q4ZtvKN5WXnzf9C6k3J2w0OIePgjewhBA0S5dTNc0GLjQTTkU4Y25ANC+JxDdjbkA0E05FOM0GLjRLl1M1sIQQNA2x7TYBFR845Tj8Np35XzREyvQ2WlLvNqtIozWyXn04q0ijNVpS7zYAAAAAAAAAAMMM5jgAAAAAFuLnNQAAAABN2Ck4AAAAABN8TDgAAAAAiegkNkQllDdurQk3CfWAOGRhLzetD901PaRMN4rOqTMKJVU3iRiXOB+nRDfY6fY3Xx0jN2DwDjhjfvo2XX76N9rtijYnF4I3NjO/Nr2e6TWgRPU2SJ4uOA/cDDdz3bc4D9wMN0ieLjigRPU2vZ7pNTYzvzYnF4I32u2KNl1++jey3OsyE8zONx4vLTNf1d00d55zM46sjDnvj5wzEKGaNdjqrjPPX/E4/1ilM9deBznwH4YztoIgNshhQzPrkh453l8GM7gKtzWWr4c2qlhiNzqB0DYdWGk40jUeN7uFrzivHGM3XbeeN8ThkDc1Ehs3mc2aN8HD4jiW54c3EfDgOFpNSzeC8SM48ioKN/b6hTPOqrQ2lQ5xN2+okTdo8Aw31I6iOBbWYDeu+iY38WWwN8UCODVgdgA4yuQsOV0wIDib9vg4XTAgOMrkLDlgdgA4xQI4NfFlsDeu+iY3FtZgN9SOojho8Aw3b6iRNwAAAAAyw604AAAAAGZvETgAAAAAqd0TNAAAAAAi9Ag6AAAAALSTejYAAAAADvN+OQAAAAC0CoY5AAAAABLW5TYAAAAAhfF8OQAAAABiBo41AAAAADddHjcIzao0Ld4NNs7CsTbJyW82gv0ZOafDzTZ8Fpg4u60pN8Ou5jiV13I3+pq3OEhRhjcVcMo4QpxcN/IQsDnCBxE3JWfEN1H+qzbPFKM2eqdINqaqLDh7/+81PGwSN88pdDfjw3U3rd4JOU1J1jcHkak2jpc8ONyVKzddgps4IEjLOdKv0zjDUA450q/TOCBIyzldgps43JUrN46XPDgHkak2TUnWN63eCTnjw3U3zyl0NzxsEjdFwq400KWcOH8oFjWpnMg3KsaFNalBWDajHu016n0fOpu7PTYXpvw3A4RtNkqzhzmEnlM2Ck+OOfcxDTbUGvs33oyiNRkVgjkZhTU19LnYM65F0DT2y6E29suhNq5F0DT0udgzGYU1NRkVgjnejKI11Br7N/cxDTYKT445hJ5TNkqzhzkDhG02F6b8N5u7PTbqfR86ox7tNalBWDYqxoU1qZzIN38oFjXQpZw4RcKuNDxsEjfPKXQ348N1N63eCTlNSdY3B5GpNo6XPDjclSs3XYKbOCBIyznSr9M4w1AOOdKv0zggSMs5XYKbONyVKzeOlzw4B5GpNk1J1jet3gk548N1N88pdDc8bBI3e//vNaaqLDh6p0g2zxSjNlH+qzYlZ8Q3wgcRN/IQsDlCnFw3FXDKOEhRhjf6mrc4lddyN8Ou5ji7rSk3fBaYOKfDzTaC/Rk5yclvNs7CsTYt3g02CM2qNDddHjcAAAAAYgaONQAAAACF8Xw5AAAAABLW5TYAAAAAtAqGOQAAAAAO8345AAAAALSTejYAAAAAIvQIOgAAAACp3RM0AAAAAGZvETgAAAAAMsOtOAAAAABvqJE3aPAMN9SOojgW1mA3rvomN/FlsDfFAjg1YHYAOMrkLDldMCA4m/b4OF0wIDjK5Cw5YHYAOMUCODXxZbA3rvomNxbWYDfUjqI4aPAMN2+okTeVDnE3zqq0Nvb6hTPyKgo3gvEjOFpNSzcR8OA4lueHN8HD4jiZzZo3NRIbN8ThkDddt543rxxjN7uFrzjSNR43HVhpODqB0DaqWGI3lq+HNrgKtzXeXwYz65IeOchhQzO2giA28B+GM9deBzn/WKUzz1/xONjqrjMQoZo174+cM46sjDl3nnMzX9XdNB4vLTMTzM43stzrMl1++jfa7Yo2JxeCNzYzvza9nuk1oET1NkieLjgP3Aw3c923OA/cDDdIni44oET1Nr2e6TU2M782JxeCN9rtijZdfvo3Y376NmDwDjhfHSM32On2Nx+nRDeJGJc4CiVVN4rOqTM9pEw3rQ/dNWRhLzcJ9YA4bq0JN0QllDeJ6CQ2AAAAABN8TDgAAAAATdgpOAAAAAAW4uc1AAAAAMMM5jgAAAAAAAAAAFpS7zarSKM1sl59OKtIozVaUu82DbHtNgEVHzjlOPw2nflfNETK9DaJ6CQ2AAAAABN8TDgAAAAATdgpOAAAAAAW4uc1AAAAAMMM5jgAAAAAAAAAACQnBjZWla03AlsyNjFjszZB0l02UTKjN74HejZ7nrs4vgd6NlEyozdB0l02MWOzNgJbMjZWla03JCcGNiTMoTYlv2Q31nNmOJ1Amze2s/k3vZ3BN11b+TiJNdU3rXzHNDEMyzfJz8s0CQ2pN7FDyTgrAX83xriLN/+XNTfAwcwzISSQNEEF8zh0qNM0aeBuN7PFEjVmb7I4UlU2NahqlDgoUEE1ylxsNy5JLDW741s52twENV5FBDY/+bo01+I9N/dAfDSc5Oc0U0ZrN0GjNDV5aKE4j7OJNcnkJTgMy8I1yI9oN5ky7jVnxLU5mTLuNciPaDcMy8I1yeQlOI+ziTV5aKE4QaM0NVNGazec5Oc0rpswN6uAhTZ+aY43gbIgN9xp6DcrqPg4MGs4ODQDhzjGL4Q4naqmOcT/mziDJw02/VePOM28uDSsRFM4jmlhOYRdCDhpv/03KKynNy2FMDgYkE43CM2qNC3eDTbOwrE2yclvNoL9GTmnw802fBaYOLutKTfDruY4lddyN/qatzhIUYY3FXDKOEKcXDfyELA5wgcRNyVnxDdR/qs2zxSjNnqnSDamqiw4e//vNQAAAACSKuk4AAAAAHp6GjcAAAAAINRPOQAAAAB3+R05AAAAANQN2zUAAAAA6GeUOgAAAADUDds1AAAAAHf5HTkAAAAAINRPOQAAAAB6eho3AAAAAJIq6TgAAAAAwK1jOISoNTcS3jI3quegN4h+5DZy2hU44KwnOXz4jDjS6jE53V/wOM5gFDr17Bs5AAAAAJZVCDl7jwY3T5qrOIzRpjktdjk4aXmNODIRxTc1Oj44oWpaN1702TY2bZQ2KwqwNYkuADehFdA3pLtrN+44AzmKJ+Q3nTVsOSQLWzjnP4U4CEq0OB20EziknNA4/G3NOQ1NnThPAbA5a6AxOLIpzThXbrY3pDrHM+YXPzdD/9o3xTTUNoYdIDhz6nQzT6MROdEE2TPeguc21RtNNGB+lTleP8o0jFyFOeb6PjWD3mQ1oD2QNWlY/jqgPZA1g95kNeb6PjWMXIU5Xj/KNGB+lTnVG0003oLnNtEE2TNPoxE5c+p0M4YdIDjFNNQ2Q//aN+YXPzekOsczV262N7IpzThroDE4TwGwOQ1NnTj8bc05pJzQOB20EzgISrQ45z+FOCQLWzidNWw5iifkN+44Azmku2s3oRXQN4kuADcrCrA1Nm2UNl702Tahalo3NTo+ODIRxTdpeY04LXY5OIzRpjlPmqs4e48GN5ZVCDkAAAAA9ewbOc5gFDrdX/A40uoxOXz4jDjgrCc5ctoVOIh+5Daq56A3Et4yN4SoNTfArWM4AAAAAJIq6TgAAAAAenoaNwAAAAAg1E85AAAAAHf5HTkAAAAA1A3bNQAAAADoZ5Q6AAAAANQN2zUAAAAAd/kdOQAAAAAg1E85AAAAAHp6GjcAAAAAkirpOAAAAAB7/+81pqosOHqnSDbPFKM2Uf6rNiVnxDfCBxE38hCwOUKcXDcVcMo4SFGGN/qatziV13I3w67mOLutKTd8Fpg4p8PNNoL9GTnJyW82zsKxNi3eDTYIzao0GJBONy2FMDgorKc3ab/9N4RdCDiOaWE5rERTOM28uDT9V484gycNNsT/mzidqqY5xi+EODQDhzgwazg4K6j4ONxp6DeBsiA3fmmON6uAhTaumzA3nOTnNFNGazdBozQ1eWihOI+ziTXJ5CU4DMvCNciPaDeZMu41Z8S1OZky7jXIj2g3DMvCNcnkJTiPs4k1eWihOEGjNDVTRms3nOTnNPdAfDTX4j03P/m6NF5FBDba3AQ1u+NbOS5JLDXKXGw3KFBBNahqlDhSVTY1Zm+yOLPFEjVp4G43dKjTNEEF8zghJJA0wMHMM/+XNTfGuIs3KwF/N7FDyTgJDak3yc/LNDEMyzetfMc0iTXVN11b+Ti9ncE3trP5N51AmzfWc2Y4Jb9kNyTMoTYkJwY2VpWtNwJbMjYxY7M2QdJdNlEyoze+B3o2e567OL4HejZRMqM3QdJdNjFjszYCWzI2VpWtNyQnBjYAAAAAAAAAAMMM5jgAAAAAFuLnNQAAAABN2Ck4AAAAABN8TDgAAAAAiegkNkTK9Dad+V805Tj8NgEVHzgNse021QBYNNcieDfeqkk4sIQQNEuXUzXNBi40E05FOGNuQDQvicQ3Y25ANBNORTjNBi40S5dTNbCEEDRjfvo2YPAOOF8dIzfY6fY3H6dEN4kYlzgKJVU3is6pMz2kTDetD901ZGEvNwn1gDhurQk3RCWUN/+XNTfGuIs3KwF/N7FDyTgJDak3yc/LNDEMyzetfMc0iTXVN11b+Ti9ncE3trP5N51AmzfWc2Y4Jb9kNyTMoTaGlgg25MmTOChKTDb5FV823nKUNoSbOzYSX8g2Ph0IOe6i7TZ5t2w47qLtNj4dCDkSX8g2hJs7Nt5ylDb5FV82KEpMNuTJkziGlgg2XL3qNnOJczd7n282d7q+N3BdnTh8LhA4x2udOPCNRTgg/Ew58s5jOMaMfDR221M4jrmVNjaCIjhY0xQ5AlfcN594HDhFko03uqvlN3eJNDdOFvk23UOBNkaUKTg+i9Q2qT4YOE/ELzflV2Q5g8iKNzSHsDW/Xb83wO5qNRJH0TeDoas5l66vN3HlpDhJv3A3qcz2OEjkFDcfJQg3Nq6zNkJqsTbCSFw2PGwSN88pdDfjw3U3rd4JOU1J1jcHkak2jpc8ONyVKzddgps4IEjLOdKv0zjDUA450q/TOCBIyzldgps43JUrN46XPDgHkak2TUnWN63eCTnjw3U3zyl0NzxsEjfArWM4hKg1NxLeMjeq56A3iH7kNnLaFTjgrCc5fPiMONLqMTndX/A4zmAUOvXsGzkAAAAAllUIOXuPBjdPmqs4jNGmOS12OThpeY04MhHFNzU6Pjihalo3XvTZNgAAAACqDBY3AAAAABqscDgAAAAApC2NOAAAAACRYN85AAAAAHa5WDYAAAAA3ts/NgAAAACDlV06AAAAANmFOzkAAAAALcZhOQAAAAAFkj03AAAAAJJsGjcAAAAA6v2EOGIZpzcGNpw3KdOYN3jgDjjnN0w55uONONgzzTbpwRY564XiNznCnDm2uHQ6JjUAOhW9hjkmNQA6trh0OjnCnDnrheI36cEWOdgzzTbm44045zdMOXjgDjgp05g3BjacN2IZpzei3Yk49geYNhWuRzdEwhA3ZQ81N27JljeLQoI5I9smOIRFgjmdv6s4qqiWOiqx+TjvSgw2GXLNOE/+4TbCWlg4yVoFOgblwzdTbbM4hxU3Nz0PfThim7o2/ksPN0BOTzZATk82/ksPN2KbujY9D304hxU3N1NtszgG5cM3yVoFOsJaWDhP/uE2GXLNOO9KDDYqsfk4qqiWOp2/qziERYI5I9smOItCgjluyZY3ZQ81N0TCEDcVrkc39geYNqLdiThiGac3BjacNynTmDd44A445zdMOebjjTjYM8026cEWOeuF4jc5wpw5trh0OiY1ADoVvYY5JjUAOra4dDo5wpw564XiN+nBFjnYM8025uONOOc3TDl44A44KdOYNwY2nDdiGac36v2EOAAAAACSbBo3AAAAAAWSPTcAAAAALcZhOQAAAADZhTs5AAAAAIOVXToAAAAA3ts/NgAAAAB2uVg2AAAAAJFg3zkAAAAApC2NOAAAAAAarHA4AAAAAKoMFjcAAAAAXvTZNqFqWjc1Oj44MhHFN2l5jTgtdjk4jNGmOU+aqzh7jwY3llUIOQAAAAD17Bs5zmAUOt1f8DjS6jE5fPiMOOCsJzly2hU4iH7kNqrnoDcS3jI3hKg1N8CtYzg8bBI3zyl0N+PDdTet3gk5TUnWNweRqTaOlzw43JUrN12CmzggSMs50q/TOMNQDjnSr9M4IEjLOV2CmzjclSs3jpc8OAeRqTZNSdY3rd4JOePDdTfPKXQ3PGwSN8JIXDZCarE2Nq6zNh8lCDdI5BQ3qcz2OEm/cDdx5aQ4l66vN4OhqzkSR9E3wO5qNb9dvzc0h7A1g8iKN+VXZDlPxC83qT4YOD6L1DZGlCk43UOBNk4W+TZ3iTQ3uqvlN0WSjTefeBw4AlfcN1jTFDk2giI4jrmVNnbbUzjGjHw08s5jOCD8TDnwjUU4x2udOHwuEDhwXZ04d7q+N3ufbzZziXM3XL3qNoaWCDbkyZM4KEpMNvkVXzbecpQ2hJs7NhJfyDY+HQg57qLtNnm3bDjuou02Ph0IORJfyDaEmzs23nKUNvkVXzYoSkw25MmTOIaWCDYkzKE2Jb9kN9ZzZjidQJs3trP5N72dwTddW/k4iTXVN618xzQxDMs3yc/LNAkNqTexQ8k4KwF/N8a4izf/lzU3RCWUN26tCTcJ9YA4ZGEvN60P3TU9pEw3is6pMwolVTeJGJc4H6dEN9jp9jdfHSM3YPAOOGN++jawhBA0S5dTNc0GLjQTTkU4Y25ANC+JxDdjbkA0E05FOM0GLjRLl1M1sIQQNN6qSTjXIng31QBYNOwX9jKitpA407YPM/HBIjdihRkz5bGVNwN+FDMKysk3cLkCM3FgJzdYJT03qTuPN05RmTjDcLU3xg2xMx7g0je61BI1X2XbNzbPtDhm28o3lZefN/0LqTcnbDQ4h4+CN11++jfa7Yo2JxeCNzYzvza9nuk1oET1NkieLjgP3Aw3c923OA/cDDdIni44oET1Nr2e6TU2M782JxeCN9rtijZdfvo390B8NNfiPTc/+bo0XkUENtrcBDW741s5LkksNcpcbDcoUEE1qGqUOFJVNjVmb7I4s8USNWngbjd0qNM0QQXzOCEkkDTAwcwzd4k0N7qr5TdFko03n3gcOAJX3DdY0xQ5NoIiOI65lTZ221M4xox8NPLOYzgg/Ew58I1FOMdrnTh8LhA4cF2dOHe6vjd7n282c4lzN1y96jYNqKk3lCFeN/okuTjMiLg3GIZWN2FgGDiaZi41AjJqOParXDnPh5c4bdYmOc+Hlzj2q1w5AjJqOJpmLjVhYBg4GIZWN8yIuDf6JLk4lCFeNw2oqTdFwq400KWcOH8oFjWpnMg3KsaFNalBWDajHu016n0fOpu7PTYXpvw3A4RtNkqzhzmEnlM2Ck+OOfcxDTbUGvs33oyiNRkVgjkZhTU19LnYM65F0DT2y6E2Nm2UNisKsDWJLgA3oRXQN6S7azfuOAM5iifkN501bDkkC1s45z+FOAhKtDgdtBM4pJzQOPxtzTkNTZ04TwGwOWugMTiyKc04V262N6Q6xzPmFz83Q//aN8U01DaGHSA4YhmnNwY2nDcp05g3eOAOOOc3TDnm44042DPNNunBFjnrheI3OcKcOba4dDomNQA6Fb2GOSY1ADq2uHQ6OcKcOeuF4jfpwRY52DPNNubjjTjnN0w5eOAOOCnTmDcGNpw3YhmnN91kgTcAAAAA5ju8OAAAAADSK9Y3AAAAAC8RgTcAAAAA7qisOgAAAACaCfc4AAAAAPGcOToAAAAAWYsgOgAAAACmWrU4AAAAAFAswzkAAAAA0E3+NQAAAACb1Fs2AAAAAD0ikzXeF0w0+KgINnhCQTfQIo02dHWcOSdUJDfDkVc5Z1nON4d02TlldWI4abfqOVcykThspOI5vv80ONankjp3xZc38IGFOHjR9Dbvb/k2IsFaNkAHhjhD/ds1UTPrN8sZdDWr6dg3cevON55njjee50c4uAOKOWU52TgAezE2gIaFOVhMnDh7wSo6xL0DO9TbqDrgl7Y51NuoOsS9Azt7wSo6WEycOICGhTkAezE2ZTnZOLgDijme50c4nmeON3Hrzjer6dg3yxl0NVEz6zdD/ds1QAeGOCLBWjbvb/k2eNH0NvCBhTh3xZc31qeSOr7/NDhspOI5VzKROGm36jlldWI4h3TZOWdZzjfDkVc5J1QkN3R1nDnQIo02eEJBN/ioCDbeF0w0PSKTNQAAAACb1Fs2AAAAANBN/jUAAAAAUCzDOQAAAACmWrU4AAAAAFmLIDoAAAAA8Zw5OgAAAACaCfc4AAAAAO6orDoAAAAALxGBNwAAAADSK9Y3AAAAAOY7vDgAAAAA3WSBN2IZpzcGNpw3KdOYN3jgDjjnN0w55uONONgzzTbpwRY564XiNznCnDm2uHQ6JjUAOhW9hjkmNQA6trh0OjnCnDnrheI36cEWOdgzzTbm44045zdMOXjgDjgp05g3BjacN2IZpzeGHSA4xTTUNkP/2jfmFz83pDrHM1dutjeyKc04a6AxOE8BsDkNTZ04/G3NOaSc0DgdtBM4CEq0OOc/hTgkC1s4nTVsOYon5DfuOAM5pLtrN6EV0DeJLgA3KwqwNTZtlDb2y6E2rkXQNPS52DMZhTU1GRWCOd6MojXUGvs39zENNgpPjjmEnlM2SrOHOQOEbTYXpvw3m7s9Nup9HzqjHu01qUFYNirGhTWpnMg3fygWNdClnDhFwq40DaipN5QhXjf6JLk4zIi4NxiGVjdhYBg4mmYuNQIyajj2q1w5z4eXOG3WJjnPh5c49qtcOQIyajiaZi41YWAYOBiGVjfMiLg3+iS5OJQhXjcNqKk3XL3qNnOJczd7n282d7q+N3BdnTh8LhA4x2udOPCNRTgg/Ew58s5jOMaMfDR221M4jrmVNjaCIjhY0xQ5AlfcN594HDhFko03uqvlN3eJNDfAwcwzISSQNEEF8zh0qNM0aeBuN7PFEjVmb7I4UlU2NahqlDgoUEE1ylxsNy5JLDW741s52twENV5FBDY/+bo01+I9N/dAfDRdfvo32u2KNicXgjc2M782vZ7pNaBE9TZIni44D9wMN3PdtzgP3Aw3SJ4uOKBE9Ta9nuk1NjO/NicXgjfa7Yo2XX76N4ePgjcnbDQ4/QupN5WXnzdm28o3Ns+0OF9l2ze61BI1HuDSN8YNsTPDcLU3TlGZOKk7jzdYJT03cWAnN3C5AjMKysk3A34UM+WxlTdihRkz8cEiN9O2DzOitpA47Bf2MgAAAADqFHU4AAAAAFF5UzgAAAAAAAAAAAAAAACSIfc449KJN+B/qTWsVHE2WKTPNWskojeG8Oc1EwWXOIbw5zVrJKI3WKTPNaxUcTbgf6k149KJN1bMCDbQLl026RMlOFvbkzbtihs4yRe2NvP3tTiRVcc20RZCNG9pvjY0qhw2RT+gNuMklzj8T3U2qkO0N0ZYMTa4Crc13l8GM+uSHjnIYUMztoIgNvAfhjPXXgc5/1ilM89f8TjY6q4zEKGaNe+PnDOOrIw5d55zM1/V3TQeLy0zE8zON7Lc6zKc5Oc0U0ZrN0GjNDV5aKE4j7OJNcnkJTgMy8I1yI9oN5ky7jVnxLU5mTLuNciPaDcMy8I1yeQlOI+ziTV5aKE4QaM0NVNGazec5Oc0wkhcNkJqsTY2rrM2HyUIN0jkFDepzPY4Sb9wN3HlpDiXrq83g6GrORJH0TfA7mo1v12/NzSHsDWDyIo35VdkOU/ELzepPhg4PovUNkaUKTjdQ4E2Thb5NvbLoTauRdA09LnYMxmFNTUZFYI53oyiNdQa+zf3MQ02Ck+OOYSeUzZKs4c5A4RtNhem/Debuz026n0fOqMe7TWpQVg2KsaFNamcyDd/KBY10KWcOEXCrjRz6nQzT6MROdEE2TPeguc21RtNNGB+lTleP8o0jFyFOeb6PjWD3mQ1oD2QNWlY/jqgPZA1g95kNeb6PjWMXIU5Xj/KNGB+lTnVG0003oLnNtEE2TNPoxE5c+p0M6LdiTj2B5g2Fa5HN0TCEDdlDzU3bsmWN4tCgjkj2yY4hEWCOZ2/qziqqJY6KrH5OO9KDDYZcs04T/7hNsJaWDjJWgU6BuXDN1NtsziHFTc3PQ99OGKbujb+Sw83QE5PNj0ikzXeF0w0+KgINnhCQTfQIo02dHWcOSdUJDfDkVc5Z1nON4d02TlldWI4abfqOVcykThspOI5vv80ONankjp3xZc38IGFOHjR9Dbvb/k2IsFaNkAHhjhD/ds1UTPrN8sZdDWKh+82AAAAAF0MazkAAAAAGejhNQAAAACvbS86AAAAANvOdjoAAAAAg19DOQAAAACzOTI8AAAAAINfQzkAAAAA2852OgAAAACvbS86AAAAABno4TUAAAAAXQxrOQAAAACKh+82JJQINlcgazgAjn02oU4LOPh3BDfMqaE1mKKhNz9RfTlAYWg4TWtkOn7HKzkWvhU7OX+iObkT8jgcGGc5hHkhOSC+qDiZhBo6syHiN/5nVTk7Ay83AGE9ODqtnzZwfUQ2KgUmNi56hTcueoU3KgUmNnB9RDY6rZ82AGE9ODsDLzf+Z1U5syHiN5mEGjogvqg4hHkhORwYZzm5E/I4OX+iORa+FTt+xys5TWtkOkBhaDg/UX05mKKhN8ypoTX4dwQ3oU4LOACOfTZXIGs4JJQINoqH7zYAAAAAXQxrOQAAAAAZ6OE1AAAAAK9tLzoAAAAA2852OgAAAACDX0M5AAAAALM5MjwAAAAAg19DOQAAAADbznY6AAAAAK9tLzoAAAAAGejhNQAAAABdDGs5AAAAAIqH7zbLGXQ1UTPrN0P92zVAB4Y4IsFaNu9v+TZ40fQ28IGFOHfFlzfWp5I6vv80OGyk4jlXMpE4abfqOWV1YjiHdNk5Z1nON8ORVzknVCQ3dHWcOdAijTZ4QkE3+KgINt4XTDQ9IpM1QE5PNv5LDzdim7o2PQ99OIcVNzdTbbM4BuXDN8laBTrCWlg4T/7hNhlyzTjvSgw2KrH5OKqoljqdv6s4hEWCOSPbJjiLQoI5bsmWN2UPNTdEwhA3Fa5HN/YHmDai3Yk4c+p0M0+jETnRBNkz3oLnNtUbTTRgfpU5Xj/KNIxchTnm+j41g95kNaA9kDVpWP46oD2QNYPeZDXm+j41jFyFOV4/yjRgfpU51RtNNN6C5zbRBNkzT6MROXPqdDNFwq400KWcOH8oFjWpnMg3KsaFNalBWDajHu016n0fOpu7PTYXpvw3A4RtNkqzhzmEnlM2Ck+OOfcxDTbUGvs33oyiNRkVgjkZhTU19LnYM65F0DT2y6E2Thb5Nt1DgTZGlCk4PovUNqk+GDhPxC835VdkOYPIijc0h7A1v12/N8DuajUSR9E3g6GrOZeurzdx5aQ4Sb9wN6nM9jhI5BQ3HyUINzauszZCarE2wkhcNpzk5zRTRms3QaM0NXlooTiPs4k1yeQlOAzLwjXIj2g3mTLuNWfEtTmZMu41yI9oNwzLwjXJ5CU4j7OJNXlooThBozQ1U0ZrN5zk5zSy3OsyE8zONx4vLTNf1d00d55zM46sjDnvj5wzEKGaNdjqrjPPX/E4/1ilM9deBznwH4YztoIgNshhQzPrkh453l8GM7gKtzVGWDE2qkO0N/xPdTbjJJc4RT+gNjSqHDZvab420RZCNJFVxzbz97U4yRe2Nu2KGzhb25M26RMlONAuXTZWzAg249KJN+B/qTWsVHE2WKTPNWskojeG8Oc1EwWXOIbw5zVrJKI3WKTPNaxUcTbgf6k149KJN5Ih9zgAAAAAAAAAAAAAAABReVM4AAAAAOoUdTgAAAAAdo3kN68z6DNAzN83rzPoM3aN5DePYZ43n4LCNcX+ETiSluU14K4tOH1u9jXGvfs1ZsftNRDGpDZEec819r0bOLHTMzexS7832ci7OKKg9jcAAAAA1CAROJGGvTVShRc40ebiON0kCzg5Ap03moPkN7eQXzj0SK03r2/QNqWaZzjighw2OC2qNqHfXDbPnUw1HvuQNoKwwTgE8ag2CyVxOATxqDaCsME4HvuQNs+dTDWh31w2OC2qNuKCHDalmmc4lQ5xN86qtDb2+oUz8ioKN4LxIzhaTUs3EfDgOJbnhzfBw+I4mc2aNzUSGzfE4ZA3XbeeN68cYze7ha840jUeNx1YaTg6gdA2qlhiN5avhzYYkE43LYUwOCispzdpv/03hF0IOI5pYTmsRFM4zby4NP1XjziDJw02xP+bOJ2qpjnGL4Q4NAOHODBrODgrqPg43GnoN4GyIDd+aY43q4CFNq6bMDc8bBI3zyl0N+PDdTet3gk5TUnWNweRqTaOlzw43JUrN12CmzggSMs50q/TOMNQDjnSr9M4IEjLOV2CmzjclSs3jpc8OAeRqTZNSdY3rd4JOePDdTfPKXQ3PGwSN4YdIDjFNNQ2Q//aN+YXPzekOsczV262N7IpzThroDE4TwGwOQ1NnTj8bc05pJzQOB20EzgISrQ45z+FOCQLWzidNWw5iifkN+44Azmku2s3oRXQN4kuADcrCrA1Nm2UNkBOTzb+Sw83Ypu6Nj0PfTiHFTc3U22zOAblwzfJWgU6wlpYOE/+4TYZcs0470oMNiqx+TiqqJY6nb+rOIRFgjkj2yY4i0KCOW7JljdlDzU3RMIQNxWuRzf2B5g2ot2JOKvp2Ddx6843nmeON57nRzi4A4o5ZTnZOAB7MTaAhoU5WEycOHvBKjrEvQM71NuoOuCXtjnU26g6xL0DO3vBKjpYTJw4gIaFOQB7MTZlOdk4uAOKOZ7nRzieZ443cevON6vp2DcklAg2VyBrOACOfTahTgs4+HcEN8ypoTWYoqE3P1F9OUBhaDhNa2Q6fscrORa+FTs5f6I5uRPyOBwYZzmEeSE5IL6oOJmEGjqzIeI3/mdVOTsDLzcAYT04Oq2fNnB9RDYqBSY2LnqFN1arUzcAAAAA7P3JNgAAAABoUYU4AAAAAPxIPDkAAAAAfIFeOgAAAADqTbk4AAAAAOSFdjgAAAAAvyajOwAAAADOrF46AAAAACcpvDkAAAAAbz3TNgAAAAC2vNY3AAAAAOQEkDgAAAAAYoUCOM9CETg4z5U3YtiVOJBbtjnY1DY5Syd+NZiBCjrgPkI5CEr7OjBl0zvvrbs7OXZAO++tuzswZdM7CEr7OuA+QjmYgQo6Syd+NdjUNjmQW7Y5YtiVODjPlTfPQhE4YoUCOAAAAADkBJA4AAAAALa81jcAAAAAbz3TNgAAAAAnKbw5AAAAAM6sXjoAAAAAvyajOwAAAADkhXY4AAAAAOpNuTgAAAAAfIFeOgAAAAD8SDw5AAAAAGhRhTgAAAAA7P3JNgAAAABWq1M3LnqFNyoFJjZwfUQ2Oq2fNgBhPTg7Ay83/mdVObMh4jeZhBo6IL6oOIR5ITkcGGc5uRPyODl/ojkWvhU7fscrOU1rZDpAYWg4P1F9OZiioTfMqaE1+HcEN6FOCzgAjn02VyBrOCSUCDar6dg3cevON55njjee50c4uAOKOWU52TgAezE2gIaFOVhMnDh7wSo6xL0DO9TbqDrgl7Y51NuoOsS9Azt7wSo6WEycOICGhTkAezE2ZTnZOLgDijme50c4nmeON3Hrzjer6dg3ot2JOPYHmDYVrkc3RMIQN2UPNTduyZY3i0KCOSPbJjiERYI5nb+rOKqoljoqsfk470oMNhlyzThP/uE2wlpYOMlaBToG5cM3U22zOIcVNzc9D304Ypu6Nv5LDzdATk82Nm2UNisKsDWJLgA3oRXQN6S7azfuOAM5iifkN501bDkkC1s45z+FOAhKtDgdtBM4pJzQOPxtzTkNTZ04TwGwOWugMTiyKc04V262N6Q6xzPmFz83Q//aN8U01DaGHSA4PGwSN88pdDfjw3U3rd4JOU1J1jcHkak2jpc8ONyVKzddgps4IEjLOdKv0zjDUA450q/TOCBIyzldgps43JUrN46XPDgHkak2TUnWN63eCTnjw3U3zyl0NzxsEjeumzA3q4CFNn5pjjeBsiA33GnoNyuo+Dgwazg4NAOHOMYvhDidqqY5xP+bOIMnDTb9V484zby4NKxEUziOaWE5hF0IOGm//TcorKc3LYUwOBiQTjeWr4c2qlhiNzqB0DYdWGk40jUeN7uFrzivHGM3XbeeN8ThkDc1Ehs3mc2aN8HD4jiW54c3EfDgOFpNSzeC8SM48ioKN/b6hTPOqrQ2lQ5xN6WaZzjighw2OC2qNqHfXDbPnUw1HvuQNoKwwTgE8ag2CyVxOATxqDaCsME4HvuQNs+dTDWh31w2OC2qNuKCHDalmmc4r2/QNvRIrTe3kF84moPkNzkCnTfdJAs40ebiOFKFFziRhr011CAROAAAAACioPY32ci7OLFLvzex0zM39r0bOER5zzUQxqQ2ZsftNca9+zV9bvY14K4tOJKW5TXF/hE4n4LCNY9hnjd2jeQ3rzPoM0DM3zevM+gzdo3kN0BPgTgGeLo3LgQuMz3Q0zca+aA1lfzaN+Oekzhz/8w3hAI5N2uErzfhqSI3gRnANQRWETWip+01kyQVOJWDBTZ9z2w4lYMFNpMkFTiip+01BFYRNYEZwDXhqSI3rs+0NZVIbTWZiUc35QuhNdpD0TiL18g1MU4WOIgq3TWTH6A3G6DSNYYz7DclXK81rw0FOL1ChDWCp3Q48F48NTi/CDhEfrg3oMajN6veCDhfBhM5m98/OMXu9zN8VXA4j3+7NTlqfzjbrkI51pRiOAR0HThJFi04cuWnOEby8DdUM/w2He+gN7zjLTZvqJE3aPAMN9SOojgW1mA3rvomN/FlsDfFAjg1YHYAOMrkLDldMCA4m/b4OF0wIDjK5Cw5YHYAOMUCODXxZbA3rvomNxbWYDfUjqI4aPAMN2+okTd7/+81pqosOHqnSDbPFKM2Uf6rNiVnxDfCBxE38hCwOUKcXDcVcMo4SFGGN/qatziV13I3w67mOLutKTd8Fpg4p8PNNoL9GTnJyW82zsKxNi3eDTYIzao0XvTZNqFqWjc1Oj44MhHFN2l5jTgtdjk4jNGmOU+aqzh7jwY3llUIOQAAAAD17Bs5zmAUOt1f8DjS6jE5fPiMOOCsJzly2hU4iH7kNqrnoDcS3jI3hKg1N8CtYzhiGac3BjacNynTmDd44A445zdMOebjjTjYM8026cEWOeuF4jc5wpw5trh0OiY1ADoVvYY5JjUAOra4dDo5wpw564XiN+nBFjnYM8025uONOOc3TDl44A44KdOYNwY2nDdiGac3yxl0NVEz6zdD/ds1QAeGOCLBWjbvb/k2eNH0NvCBhTh3xZc31qeSOr7/NDhspOI5VzKROGm36jlldWI4h3TZOWdZzjfDkVc5J1QkN3R1nDnQIo02eEJBN/ioCDbeF0w0PSKTNS56hTcqBSY2cH1ENjqtnzYAYT04OwMvN/5nVTmzIeI3mYQaOiC+qDiEeSE5HBhnObkT8jg5f6I5Fr4VO37HKzlNa2Q6QGFoOD9RfTmYoqE3zKmhNfh3BDehTgs4AI59NlcgazgklAg2YoUCOM9CETg4z5U3YtiVOJBbtjnY1DY5Syd+NZiBCjrgPkI5CEr7OjBl0zvvrbs7OXZAO++tuzswZdM7CEr7OuA+QjmYgQo6Syd+NdjUNjmQW7Y5YtiVODjPlTfPQhE4YoUCOAAAAABvqjg4AAAAAH3DgTgAAAAAGzIRNgAAAACZ6jc5AAAAAFbIGDsAAAAAbUvgOwAAAAA7a0c7AAAAAIc2cDoAAAAA9sUKOgAAAACjI705AAAAALuj5jcAAAAAb9pbNAAAAADBiNw3wYjcNwAAAABv2ls0AAAAALuj5jcAAAAAoyO9OQAAAAD2xQo6AAAAAIc2cDoAAAAAO2tHOwAAAABtS+A7AAAAAFbIGDsAAAAAmeo3OQAAAAAbMhE2AAAAAH3DgTgAAAAAb6o4OAAAAABihQI4z0IRODjPlTdi2JU4kFu2OdjUNjlLJ341mIEKOuA+QjkISvs6MGXTO++tuzs5dkA77627OzBl0zsISvs64D5COZiBCjpLJ3412NQ2OZBbtjli2JU4OM+VN89CEThihQI4JJQINlcgazgAjn02oU4LOPh3BDfMqaE1mKKhNz9RfTlAYWg4TWtkOn7HKzkWvhU7OX+iObkT8jgcGGc5hHkhOSC+qDiZhBo6syHiN/5nVTk7Ay83AGE9ODqtnzZwfUQ2KgUmNi56hTc9IpM13hdMNPioCDZ4QkE30CKNNnR1nDknVCQ3w5FXOWdZzjeHdNk5ZXViOGm36jlXMpE4bKTiOb7/NDjWp5I6d8WXN/CBhTh40fQ272/5NiLBWjZAB4Y4Q/3bNVEz6zfLGXQ1YhmnNwY2nDcp05g3eOAOOOc3TDnm44042DPNNunBFjnrheI3OcKcOba4dDomNQA6Fb2GOSY1ADq2uHQ6OcKcOeuF4jfpwRY52DPNNubjjTjnN0w5eOAOOCnTmDcGNpw3YhmnN8CtYziEqDU3Et4yN6rnoDeIfuQ2ctoVOOCsJzl8+Iw40uoxOd1f8DjOYBQ69ewbOQAAAACWVQg5e48GN0+aqziM0aY5LXY5OGl5jTgyEcU3NTo+OKFqWjde9Nk2CM2qNC3eDTbOwrE2yclvNoL9GTmnw802fBaYOLutKTfDruY4lddyN/qatzhIUYY3FXDKOEKcXDfyELA5wgcRNyVnxDdR/qs2zxSjNnqnSDamqiw4e//vNW+okTdo8Aw31I6iOBbWYDeu+iY38WWwN8UCODVgdgA4yuQsOV0wIDib9vg4XTAgOMrkLDlgdgA4xQI4NfFlsDeu+iY3FtZgN9SOojho8Aw3b6iRN7zjLTYd76A3VDP8Nkby8Ddy5ac4SRYtOAR0HTjWlGI4265COTlqfziPf7s1fFVwOMXu9zOb3z84XwYTOaveCDigxqM3RH64Nzi/CDjwXjw1gqd0OL1ChDWvDQU4JVyvNYYz7DcboNI1kx+gN4gq3TUxThY4i9fINdpD0TjlC6E1mYlHN5VIbTWuz7Q14akiN4EZwDUEVhE1oqftNZMkFTiVgwU2fc9sOJWDBTaTJBU4oqftNQRWETWBGcA14akiN2uErzeEAjk3c//MN+OekziV/No3GvmgNT3Q0zcuBC4zBni6N0BPgTgAAAAAn/6aNwAAAAAsGic4AAAAAJ/+mjcAAAAAOPGwNwAAAAD9RA84AAAAAEaGQDgAAAAA6B3DNQAAAACi55A2AAAAAMINKzgAAAAASikIOQAAAAD0LQg0AAAAAE1K4DgAAAAA9NnHOAAAAAA93gozAAAAAHAIXTkAAAAAEqYMNgAAAAA3ctk3lMGDNwAAAADOfTo4AAAAAFzglTcAAAAAnl6SNwAAAABOxEc5AAAAAJ5ekjcAAAAAXOCVNwAAAADOfTo4AAAAAJTBgzcQ/Lk2AAAAAKaKmTYAAAAAcjChOAAAAAC674M4AAAAAOE6SzkAAAAAAAAAAAAAAACP9yg2AAAAAPIQFTkAAAAAS/IEOAAAAADXj/I3AAAAADddHjcAAAAAYgaONQAAAACF8Xw5AAAAABLW5TYAAAAAtAqGOQAAAAAO8345AAAAALSTejYAAAAAIvQIOgAAAACp3RM0AAAAAGZvETgAAAAAMsOtOAAAAAAAAAAAkirpOAAAAAB6eho3AAAAACDUTzkAAAAAd/kdOQAAAADUDds1AAAAAOhnlDoAAAAA1A3bNQAAAAB3+R05AAAAACDUTzkAAAAAenoaNwAAAACSKuk4AAAAAOr9hDgAAAAAkmwaNwAAAAAFkj03AAAAAC3GYTkAAAAA2YU7OQAAAACDlV06AAAAAN7bPzYAAAAAdrlYNgAAAACRYN85AAAAAKQtjTgAAAAAGqxwOAAAAACqDBY3AAAAAAAAAACb1Fs2AAAAANBN/jUAAAAAUCzDOQAAAACmWrU4AAAAAFmLIDoAAAAA8Zw5OgAAAACaCfc4AAAAAO6orDoAAAAALxGBNwAAAADSK9Y3AAAAAOY7vDgAAAAA3WSBN4qH7zYAAAAAXQxrOQAAAAAZ6OE1AAAAAK9tLzoAAAAA2852OgAAAACDX0M5AAAAALM5MjwAAAAAg19DOQAAAADbznY6AAAAAK9tLzoAAAAAGejhNQAAAABdDGs5AAAAAIqH7zYAAAAA5ASQOAAAAAC2vNY3AAAAAG890zYAAAAAJym8OQAAAADOrF46AAAAAL8mozsAAAAA5IV2OAAAAADqTbk4AAAAAHyBXjoAAAAA/Eg8OQAAAABoUYU4AAAAAOz9yTYAAAAAVqtTN8GI3DcAAAAAb9pbNAAAAAC7o+Y3AAAAAKMjvTkAAAAA9sUKOgAAAACHNnA6AAAAADtrRzsAAAAAbUvgOwAAAABWyBg7AAAAAJnqNzkAAAAAGzIRNgAAAAB9w4E4AAAAAG+qODgAAAAAnYk8NwAAAADZYpg5AAAAACY6HzMAAAAA4UmUOgAAAAAZKRk7AAAAAHj+FjoAAAAAAAAAAHj+FjoAAAAAGSkZOwAAAADhSZQ6AAAAACY6HzMAAAAA2WKYOQAAAACdiTw3AAAAAG+qODgAAAAAfcOBOAAAAAAbMhE2AAAAAJnqNzkAAAAAVsgYOwAAAABtS+A7AAAAADtrRzsAAAAAhzZwOgAAAAD2xQo6AAAAAKMjvTkAAAAAu6PmNwAAAABv2ls0AAAAAMGI3DdWq1M3AAAAAOz9yTYAAAAAaFGFOAAAAAD8SDw5AAAAAHyBXjoAAAAA6k25OAAAAADkhXY4AAAAAL8mozsAAAAAzqxeOgAAAAAnKbw5AAAAAG890zYAAAAAtrzWNwAAAADkBJA4AAAAAIqH7zYAAAAAXQxrOQAAAAAZ6OE1AAAAAK9tLzoAAAAA2852OgAAAACDX0M5AAAAALM5MjwAAAAAg19DOQAAAADbznY6AAAAAK9tLzoAAAAAGejhNQAAAABdDGs5AAAAAIqH7zbdZIE3AAAAAOY7vDgAAAAA0ivWNwAAAAAvEYE3AAAAAO6orDoAAAAAmgn3OAAAAADxnDk6AAAAAFmLIDoAAAAAplq1OAAAAABQLMM5AAAAANBN/jUAAAAAm9RbNgAAAAAAAAAAqgwWNwAAAAAarHA4AAAAAKQtjTgAAAAAkWDfOQAAAAB2uVg2AAAAAN7bPzYAAAAAg5VdOgAAAADZhTs5AAAAAC3GYTkAAAAABZI9NwAAAACSbBo3AAAAAOr9hDgAAAAAkirpOAAAAAB6eho3AAAAACDUTzkAAAAAd/kdOQAAAADUDds1AAAAAOhnlDoAAAAA1A3bNQAAAAB3+R05AAAAACDUTzkAAAAAenoaNwAAAACSKuk4AAAAAAAAAAAyw604AAAAAGZvETgAAAAAqd0TNAAAAAAi9Ag6AAAAALSTejYAAAAADvN+OQAAAAC0CoY5AAAAABLW5TYAAAAAhfF8OQAAAABiBo41AAAAADddHjcAAAAA14/yNwAAAABL8gQ4AAAAAPIQFTkAAAAAj/coNgAAAAAAAAAAAAAAAOE6SzkAAAAAuu+DOAAAAAByMKE4AAAAAKaKmTYAAAAAEPy5NpTBgzcAAAAAzn06OAAAAABc4JU3AAAAAJ5ekjcAAAAATsRHOQAAAACeXpI3AAAAAFzglTcAAAAAzn06OAAAAACUwYM3N3LZNwAAAAASpgw2AAAAAHAIXTkAAAAAPd4KMwAAAAD02cc4AAAAAE1K4DgAAAAA9C0INAAAAABKKQg5AAAAAMINKzgAAAAAoueQNgAAAADoHcM1AAAAAEaGQDgAAAAA/UQPOAAAAAA48bA3AAAAAJ/+mjcAAAAALBonOAAAAACf/po3AAAAAEBPgTgGeLo3LgQuMz3Q0zca+aA1lfzaN+Oekzhz/8w3hAI5N2uErzfhqSI3gRnANQRWETWip+01kyQVOJWDBTZ9z2w4lYMFNpMkFTiip+01BFYRNYEZwDXhqSI3rs+0NZVIbTWZiUc35QuhNdpD0TiL18g1MU4WOIgq3TWTH6A3G6DSNYYz7DclXK81rw0FOL1ChDWCp3Q48F48NTi/CDhEfrg3oMajN6veCDhfBhM5m98/OMXu9zN8VXA4j3+7NTlqfzjbrkI51pRiOAR0HThJFi04cuWnOEby8DdUM/w2He+gN7zjLTZvqJE3aPAMN9SOojgW1mA3rvomN/FlsDfFAjg1YHYAOMrkLDldMCA4m/b4OF0wIDjK5Cw5YHYAOMUCODXxZbA3rvomNxbWYDfUjqI4aPAMN2+okTd7/+81pqosOHqnSDbPFKM2Uf6rNiVnxDfCBxE38hCwOUKcXDcVcMo4SFGGN/qatziV13I3w67mOLutKTd8Fpg4p8PNNoL9GTnJyW82zsKxNi3eDTYIzao0XvTZNqFqWjc1Oj44MhHFN2l5jTgtdjk4jNGmOU+aqzh7jwY3llUIOQAAAAD17Bs5zmAUOt1f8DjS6jE5fPiMOOCsJzly2hU4iH7kNqrnoDcS3jI3hKg1N8CtYzhiGac3BjacNynTmDd44A445zdMOebjjTjYM8026cEWOeuF4jc5wpw5trh0OiY1ADoVvYY5JjUAOra4dDo5wpw564XiN+nBFjnYM8025uONOOc3TDl44A44KdOYNwY2nDdiGac3yxl0NVEz6zdD/ds1QAeGOCLBWjbvb/k2eNH0NvCBhTh3xZc31qeSOr7/NDhspOI5VzKROGm36jlldWI4h3TZOWdZzjfDkVc5J1QkN3R1nDnQIo02eEJBN/ioCDbeF0w0PSKTNS56hTcqBSY2cH1ENjqtnzYAYT04OwMvN/5nVTmzIeI3mYQaOiC+qDiEeSE5HBhnObkT8jg5f6I5Fr4VO37HKzlNa2Q6QGFoOD9RfTmYoqE3zKmhNfh3BDehTgs4AI59NlcgazgklAg2YoUCOM9CETg4z5U3YtiVOJBbtjnY1DY5Syd+NZiBCjrgPkI5CEr7OjBl0zvvrbs7OXZAO++tuzswZdM7CEr7OuA+QjmYgQo6Syd+NdjUNjmQW7Y5YtiVODjPlTfPQhE4YoUCOAAAAABvqjg4AAAAAH3DgTgAAAAAGzIRNgAAAACZ6jc5AAAAAFbIGDsAAAAAbUvgOwAAAAA7a0c7AAAAAIc2cDoAAAAA9sUKOgAAAACjI705AAAAALuj5jcAAAAAb9pbNAAAAADBiNw3wYjcNwAAAABv2ls0AAAAALuj5jcAAAAAoyO9OQAAAAD2xQo6AAAAAIc2cDoAAAAAO2tHOwAAAABtS+A7AAAAAFbIGDsAAAAAmeo3OQAAAAAbMhE2AAAAAH3DgTgAAAAAb6o4OAAAAABihQI4z0IRODjPlTdi2JU4kFu2OdjUNjlLJ341mIEKOuA+QjkISvs6MGXTO++tuzs5dkA77627OzBl0zsISvs64D5COZiBCjpLJ3412NQ2OZBbtjli2JU4OM+VN89CEThihQI4JJQINlcgazgAjn02oU4LOPh3BDfMqaE1mKKhNz9RfTlAYWg4TWtkOn7HKzkWvhU7OX+iObkT8jgcGGc5hHkhOSC+qDiZhBo6syHiN/5nVTk7Ay83AGE9ODqtnzZwfUQ2KgUmNi56hTc9IpM13hdMNPioCDZ4QkE30CKNNnR1nDknVCQ3w5FXOWdZzjeHdNk5ZXViOGm36jlXMpE4bKTiOb7/NDjWp5I6d8WXN/CBhTh40fQ272/5NiLBWjZAB4Y4Q/3bNVEz6zfLGXQ1YhmnNwY2nDcp05g3eOAOOOc3TDnm44042DPNNunBFjnrheI3OcKcOba4dDomNQA6Fb2GOSY1ADq2uHQ6OcKcOeuF4jfpwRY52DPNNubjjTjnN0w5eOAOOCnTmDcGNpw3YhmnN8CtYziEqDU3Et4yN6rnoDeIfuQ2ctoVOOCsJzl8+Iw40uoxOd1f8DjOYBQ69ewbOQAAAACWVQg5e48GN0+aqziM0aY5LXY5OGl5jTgyEcU3NTo+OKFqWjde9Nk2CM2qNC3eDTbOwrE2yclvNoL9GTmnw802fBaYOLutKTfDruY4lddyN/qatzhIUYY3FXDKOEKcXDfyELA5wgcRNyVnxDdR/qs2zxSjNnqnSDamqiw4e//vNW+okTdo8Aw31I6iOBbWYDeu+iY38WWwN8UCODVgdgA4yuQsOV0wIDib9vg4XTAgOMrkLDlgdgA4xQI4NfFlsDeu+iY3FtZgN9SOojho8Aw3b6iRN7zjLTYd76A3VDP8Nkby8Ddy5ac4SRYtOAR0HTjWlGI4265COTlqfziPf7s1fFVwOMXu9zOb3z84XwYTOaveCDigxqM3RH64Nzi/CDjwXjw1gqd0OL1ChDWvDQU4JVyvNYYz7DcboNI1kx+gN4gq3TUxThY4i9fINdpD0TjlC6E1mYlHN5VIbTWuz7Q14akiN4EZwDUEVhE1oqftNZMkFTiVgwU2fc9sOJWDBTaTJBU4oqftNQRWETWBGcA14akiN2uErzeEAjk3c//MN+OekziV/No3GvmgNT3Q0zcuBC4zBni6N0BPgTh2jeQ3rzPoM0DM3zevM+gzdo3kN49hnjefgsI1xf4ROJKW5TXgri04fW72Nca9+zVmx+01EMakNkR5zzX2vRs4sdMzN7FLvzfZyLs4oqD2NwAAAADUIBE4kYa9NVKFFzjR5uI43SQLODkCnTeag+Q3t5BfOPRIrTevb9A2pZpnOOKCHDY4Lao2od9cNs+dTDUe+5A2grDBOATxqDYLJXE4BPGoNoKwwTge+5A2z51MNaHfXDY4Lao24oIcNqWaZziVDnE3zqq0Nvb6hTPyKgo3gvEjOFpNSzcR8OA4lueHN8HD4jiZzZo3NRIbN8ThkDddt543rxxjN7uFrzjSNR43HVhpODqB0DaqWGI3lq+HNhiQTjcthTA4KKynN2m//TeEXQg4jmlhOaxEUzjNvLg0/VePOIMnDTbE/5s4naqmOcYvhDg0A4c4MGs4OCuo+Djcaeg3gbIgN35pjjergIU2rpswNzxsEjfPKXQ348N1N63eCTlNSdY3B5GpNo6XPDjclSs3XYKbOCBIyznSr9M4w1AOOdKv0zggSMs5XYKbONyVKzeOlzw4B5GpNk1J1jet3gk548N1N88pdDc8bBI3hh0gOMU01DZD/9o35hc/N6Q6xzNXbrY3sinNOGugMThPAbA5DU2dOPxtzTmknNA4HbQTOAhKtDjnP4U4JAtbOJ01bDmKJ+Q37jgDOaS7azehFdA3iS4ANysKsDU2bZQ2QE5PNv5LDzdim7o2PQ99OIcVNzdTbbM4BuXDN8laBTrCWlg4T/7hNhlyzTjvSgw2KrH5OKqoljqdv6s4hEWCOSPbJjiLQoI5bsmWN2UPNTdEwhA3Fa5HN/YHmDai3Yk4q+nYN3HrzjeeZ443nudHOLgDijllOdk4AHsxNoCGhTlYTJw4e8EqOsS9AzvU26g64Je2OdTbqDrEvQM7e8EqOlhMnDiAhoU5AHsxNmU52Ti4A4o5nudHOJ5njjdx6843q+nYNySUCDZXIGs4AI59NqFOCzj4dwQ3zKmhNZiioTc/UX05QGFoOE1rZDp+xys5Fr4VOzl/ojm5E/I4HBhnOYR5ITkgvqg4mYQaOrMh4jf+Z1U5OwMvNwBhPTg6rZ82cH1ENioFJjYueoU3VqtTNwAAAADs/ck2AAAAAGhRhTgAAAAA/Eg8OQAAAAB8gV46AAAAAOpNuTgAAAAA5IV2OAAAAAC/JqM7AAAAAM6sXjoAAAAAJym8OQAAAABvPdM2AAAAALa81jcAAAAA5ASQOAAAAABihQI4z0IRODjPlTdi2JU4kFu2OdjUNjlLJ341mIEKOuA+QjkISvs6MGXTO++tuzs5dkA77627OzBl0zsISvs64D5COZiBCjpLJ3412NQ2OZBbtjli2JU4OM+VN89CEThihQI4AAAAAOQEkDgAAAAAtrzWNwAAAABvPdM2AAAAACcpvDkAAAAAzqxeOgAAAAC/JqM7AAAAAOSFdjgAAAAA6k25OAAAAAB8gV46AAAAAPxIPDkAAAAAaFGFOAAAAADs/ck2AAAAAFarUzcueoU3KgUmNnB9RDY6rZ82AGE9ODsDLzf+Z1U5syHiN5mEGjogvqg4hHkhORwYZzm5E/I4OX+iORa+FTt+xys5TWtkOkBhaDg/UX05mKKhN8ypoTX4dwQ3oU4LOACOfTZXIGs4JJQINqvp2Ddx6843nmeON57nRzi4A4o5ZTnZOAB7MTaAhoU5WEycOHvBKjrEvQM71NuoOuCXtjnU26g6xL0DO3vBKjpYTJw4gIaFOQB7MTZlOdk4uAOKOZ7nRzieZ443cevON6vp2Dei3Yk49geYNhWuRzdEwhA3ZQ81N27JljeLQoI5I9smOIRFgjmdv6s4qqiWOiqx+TjvSgw2GXLNOE/+4TbCWlg4yVoFOgblwzdTbbM4hxU3Nz0PfThim7o2/ksPN0BOTzY2bZQ2KwqwNYkuADehFdA3pLtrN+44AzmKJ+Q3nTVsOSQLWzjnP4U4CEq0OB20EziknNA4/G3NOQ1NnThPAbA5a6AxOLIpzThXbrY3pDrHM+YXPzdD/9o3xTTUNoYdIDg8bBI3zyl0N+PDdTet3gk5TUnWNweRqTaOlzw43JUrN12CmzggSMs50q/TOMNQDjnSr9M4IEjLOV2CmzjclSs3jpc8OAeRqTZNSdY3rd4JOePDdTfPKXQ3PGwSN66bMDergIU2fmmON4GyIDfcaeg3K6j4ODBrODg0A4c4xi+EOJ2qpjnE/5s4gycNNv1XjzjNvLg0rERTOI5pYTmEXQg4ab/9NyispzcthTA4GJBON5avhzaqWGI3OoHQNh1YaTjSNR43u4WvOK8cYzddt543xOGQNzUSGzeZzZo3wcPiOJbnhzcR8OA4Wk1LN4LxIzjyKgo39vqFM86qtDaVDnE3pZpnOOKCHDY4Lao2od9cNs+dTDUe+5A2grDBOATxqDYLJXE4BPGoNoKwwTge+5A2z51MNaHfXDY4Lao24oIcNqWaZzivb9A29EitN7eQXziag+Q3OQKdN90kCzjR5uI4UoUXOJGGvTXUIBE4AAAAAKKg9jfZyLs4sUu/N7HTMzf2vRs4RHnPNRDGpDZmx+01xr37NX1u9jXgri04kpblNcX+ETifgsI1j2GeN3aN5DevM+gzQMzfN68z6DN2jeQ3AAAAAOoUdTgAAAAAUXlTOAAAAAAAAAAAAAAAAJIh9zjj0ok34H+pNaxUcTZYpM81aySiN4bw5zUTBZc4hvDnNWskojdYpM81rFRxNuB/qTXj0ok3VswINtAuXTbpEyU4W9uTNu2KGzjJF7Y28/e1OJFVxzbRFkI0b2m+NjSqHDZFP6A24ySXOPxPdTaqQ7Q3RlgxNrgKtzXeXwYz65IeOchhQzO2giA28B+GM9deBzn/WKUzz1/xONjqrjMQoZo174+cM46sjDl3nnMzX9XdNB4vLTMTzM43stzrMpzk5zRTRms3QaM0NXlooTiPs4k1yeQlOAzLwjXIj2g3mTLuNWfEtTmZMu41yI9oNwzLwjXJ5CU4j7OJNXlooThBozQ1U0ZrN5zk5zTCSFw2QmqxNjauszYfJQg3SOQUN6nM9jhJv3A3ceWkOJeurzeDoas5EkfRN8DuajW/Xb83NIewNYPIijflV2Q5T8QvN6k+GDg+i9Q2RpQpON1DgTZOFvk29suhNq5F0DT0udgzGYU1NRkVgjnejKI11Br7N/cxDTYKT445hJ5TNkqzhzkDhG02F6b8N5u7PTbqfR86ox7tNalBWDYqxoU1qZzIN38oFjXQpZw4RcKuNHPqdDNPoxE50QTZM96C5zbVG000YH6VOV4/yjSMXIU55vo+NYPeZDWgPZA1aVj+OqA9kDWD3mQ15vo+NYxchTleP8o0YH6VOdUbTTTeguc20QTZM0+jETlz6nQzot2JOPYHmDYVrkc3RMIQN2UPNTduyZY3i0KCOSPbJjiERYI5nb+rOKqoljoqsfk470oMNhlyzThP/uE2wlpYOMlaBToG5cM3U22zOIcVNzc9D304Ypu6Nv5LDzdATk82PSKTNd4XTDT4qAg2eEJBN9AijTZ0dZw5J1QkN8ORVzlnWc43h3TZOWV1Yjhpt+o5VzKROGyk4jm+/zQ41qeSOnfFlzfwgYU4eNH0Nu9v+TYiwVo2QAeGOEP92zVRM+s3yxl0NYqH7zYAAAAAXQxrOQAAAAAZ6OE1AAAAAK9tLzoAAAAA2852OgAAAACDX0M5AAAAALM5MjwAAAAAg19DOQAAAADbznY6AAAAAK9tLzoAAAAAGejhNQAAAABdDGs5AAAAAIqH7zYklAg2VyBrOACOfTahTgs4+HcEN8ypoTWYoqE3P1F9OUBhaDhNa2Q6fscrORa+FTs5f6I5uRPyOBwYZzmEeSE5IL6oOJmEGjqzIeI3/mdVOTsDLzcAYT04Oq2fNnB9RDYqBSY2LnqFNy56hTcqBSY2cH1ENjqtnzYAYT04OwMvN/5nVTmzIeI3mYQaOiC+qDiEeSE5HBhnObkT8jg5f6I5Fr4VO37HKzlNa2Q6QGFoOD9RfTmYoqE3zKmhNfh3BDehTgs4AI59NlcgazgklAg2iofvNgAAAABdDGs5AAAAABno4TUAAAAAr20vOgAAAADbznY6AAAAAINfQzkAAAAAszkyPAAAAACDX0M5AAAAANvOdjoAAAAAr20vOgAAAAAZ6OE1AAAAAF0MazkAAAAAiofvNssZdDVRM+s3Q/3bNUAHhjgiwVo272/5NnjR9DbwgYU4d8WXN9ankjq+/zQ4bKTiOVcykThpt+o5ZXViOId02TlnWc43w5FXOSdUJDd0dZw50CKNNnhCQTf4qAg23hdMND0ikzVATk82/ksPN2KbujY9D304hxU3N1NtszgG5cM3yVoFOsJaWDhP/uE2GXLNOO9KDDYqsfk4qqiWOp2/qziERYI5I9smOItCgjluyZY3ZQ81N0TCEDcVrkc39geYNqLdiThz6nQzT6MROdEE2TPeguc21RtNNGB+lTleP8o0jFyFOeb6PjWD3mQ1oD2QNWlY/jqgPZA1g95kNeb6PjWMXIU5Xj/KNGB+lTnVG0003oLnNtEE2TNPoxE5c+p0M0XCrjTQpZw4fygWNamcyDcqxoU1qUFYNqMe7TXqfR86m7s9Nhem/DcDhG02SrOHOYSeUzYKT4459zENNtQa+zfejKI1GRWCORmFNTX0udgzrkXQNPbLoTZOFvk23UOBNkaUKTg+i9Q2qT4YOE/ELzflV2Q5g8iKNzSHsDW/Xb83wO5qNRJH0TeDoas5l66vN3HlpDhJv3A3qcz2OEjkFDcfJQg3Nq6zNkJqsTbCSFw2nOTnNFNGazdBozQ1eWihOI+ziTXJ5CU4DMvCNciPaDeZMu41Z8S1OZky7jXIj2g3DMvCNcnkJTiPs4k1eWihOEGjNDVTRms3nOTnNLLc6zITzM43Hi8tM1/V3TR3nnMzjqyMOe+PnDMQoZo12OquM89f8Tj/WKUz114HOfAfhjO2giA2yGFDM+uSHjneXwYzuAq3NUZYMTaqQ7Q3/E91NuMklzhFP6A2NKocNm9pvjbRFkI0kVXHNvP3tTjJF7Y27YobOFvbkzbpEyU40C5dNlbMCDbj0ok34H+pNaxUcTZYpM81aySiN4bw5zUTBZc4hvDnNWskojdYpM81rFRxNuB/qTXj0ok3kiH3OAAAAAAAAAAAAAAAAFF5UzgAAAAA6hR1OAAAAADsF/YyoraQONO2DzPxwSI3YoUZM+WxlTcDfhQzCsrJN3C5AjNxYCc3WCU9N6k7jzdOUZk4w3C1N8YNsTMe4NI3utQSNV9l2zc2z7Q4ZtvKN5WXnzf9C6k3J2w0OIePgjddfvo32u2KNicXgjc2M782vZ7pNaBE9TZIni44D9wMN3PdtzgP3Aw3SJ4uOKBE9Ta9nuk1NjO/NicXgjfa7Yo2XX76N/dAfDTX4j03P/m6NF5FBDba3AQ1u+NbOS5JLDXKXGw3KFBBNahqlDhSVTY1Zm+yOLPFEjVp4G43dKjTNEEF8zghJJA0wMHMM3eJNDe6q+U3RZKNN594HDgCV9w3WNMUOTaCIjiOuZU2dttTOMaMfDTyzmM4IPxMOfCNRTjHa504fC4QOHBdnTh3ur43e59vNnOJczdcveo2DaipN5QhXjf6JLk4zIi4NxiGVjdhYBg4mmYuNQIyajj2q1w5z4eXOG3WJjnPh5c49qtcOQIyajiaZi41YWAYOBiGVjfMiLg3+iS5OJQhXjcNqKk3RcKuNNClnDh/KBY1qZzINyrGhTWpQVg2ox7tNep9Hzqbuz02F6b8NwOEbTZKs4c5hJ5TNgpPjjn3MQ021Br7N96MojUZFYI5GYU1NfS52DOuRdA09suhNjZtlDYrCrA1iS4AN6EV0Deku2s37jgDOYon5DedNWw5JAtbOOc/hTgISrQ4HbQTOKSc0Dj8bc05DU2dOE8BsDlroDE4sinNOFdutjekOscz5hc/N0P/2jfFNNQ2hh0gOGIZpzcGNpw3KdOYN3jgDjjnN0w55uONONgzzTbpwRY564XiNznCnDm2uHQ6JjUAOhW9hjkmNQA6trh0OjnCnDnrheI36cEWOdgzzTbm44045zdMOXjgDjgp05g3BjacN2IZpzfdZIE3AAAAAOY7vDgAAAAA0ivWNwAAAAAvEYE3AAAAAO6orDoAAAAAmgn3OAAAAADxnDk6AAAAAFmLIDoAAAAAplq1OAAAAABQLMM5AAAAANBN/jUAAAAAm9RbNgAAAAA9IpM13hdMNPioCDZ4QkE30CKNNnR1nDknVCQ3w5FXOWdZzjeHdNk5ZXViOGm36jlXMpE4bKTiOb7/NDjWp5I6d8WXN/CBhTh40fQ272/5NiLBWjZAB4Y4Q/3bNVEz6zfLGXQ1q+nYN3HrzjeeZ443nudHOLgDijllOdk4AHsxNoCGhTlYTJw4e8EqOsS9AzvU26g64Je2OdTbqDrEvQM7e8EqOlhMnDiAhoU5AHsxNmU52Ti4A4o5nudHOJ5njjdx6843q+nYN8sZdDVRM+s3Q/3bNUAHhjgiwVo272/5NnjR9DbwgYU4d8WXN9ankjq+/zQ4bKTiOVcykThpt+o5ZXViOId02TlnWc43w5FXOSdUJDd0dZw50CKNNnhCQTf4qAg23hdMND0ikzUAAAAAm9RbNgAAAADQTf41AAAAAFAswzkAAAAAplq1OAAAAABZiyA6AAAAAPGcOToAAAAAmgn3OAAAAADuqKw6AAAAAC8RgTcAAAAA0ivWNwAAAADmO7w4AAAAAN1kgTdiGac3BjacNynTmDd44A445zdMOebjjTjYM8026cEWOeuF4jc5wpw5trh0OiY1ADoVvYY5JjUAOra4dDo5wpw564XiN+nBFjnYM8025uONOOc3TDl44A44KdOYNwY2nDdiGac3hh0gOMU01DZD/9o35hc/N6Q6xzNXbrY3sinNOGugMThPAbA5DU2dOPxtzTmknNA4HbQTOAhKtDjnP4U4JAtbOJ01bDmKJ+Q37jgDOaS7azehFdA3iS4ANysKsDU2bZQ29suhNq5F0DT0udgzGYU1NRkVgjnejKI11Br7N/cxDTYKT445hJ5TNkqzhzkDhG02F6b8N5u7PTbqfR86ox7tNalBWDYqxoU1qZzIN38oFjXQpZw4RcKuNA2oqTeUIV43+iS5OMyIuDcYhlY3YWAYOJpmLjUCMmo49qtcOc+Hlzht1iY5z4eXOParXDkCMmo4mmYuNWFgGDgYhlY3zIi4N/okuTiUIV43DaipN1y96jZziXM3e59vNne6vjdwXZ04fC4QOMdrnTjwjUU4IPxMOfLOYzjGjHw0dttTOI65lTY2giI4WNMUOQJX3DefeBw4RZKNN7qr5Td3iTQ3wMHMMyEkkDRBBfM4dKjTNGngbjezxRI1Zm+yOFJVNjWoapQ4KFBBNcpcbDcuSSw1u+NbOdrcBDVeRQQ2P/m6NNfiPTf3QHw0XX76N9rtijYnF4I3NjO/Nr2e6TWgRPU2SJ4uOA/cDDdz3bc4D9wMN0ieLjigRPU2vZ7pNTYzvzYnF4I32u2KNl1++jeHj4I3J2w0OP0LqTeVl583ZtvKNzbPtDhfZds3utQSNR7g0jfGDbEzw3C1N05RmTipO483WCU9N3FgJzdwuQIzCsrJNwN+FDPlsZU3YoUZM/HBIjfTtg8zoraQOOwX9jLVAFg01yJ4N96qSTiwhBA0S5dTNc0GLjQTTkU4Y25ANC+JxDdjbkA0E05FOM0GLjRLl1M1sIQQNGN++jZg8A44Xx0jN9jp9jcfp0Q3iRiXOAolVTeKzqkzPaRMN60P3TVkYS83CfWAOG6tCTdEJZQ3/5c1N8a4izcrAX83sUPJOAkNqTfJz8s0MQzLN618xzSJNdU3XVv5OL2dwTe2s/k3nUCbN9ZzZjglv2Q3JMyhNoaWCDbkyZM4KEpMNvkVXzbecpQ2hJs7NhJfyDY+HQg57qLtNnm3bDjuou02Ph0IORJfyDaEmzs23nKUNvkVXzYoSkw25MmTOIaWCDZcveo2c4lzN3ufbzZ3ur43cF2dOHwuEDjHa5048I1FOCD8TDnyzmM4xox8NHbbUziOuZU2NoIiOFjTFDkCV9w3n3gcOEWSjTe6q+U3d4k0N04W+TbdQ4E2RpQpOD6L1DapPhg4T8QvN+VXZDmDyIo3NIewNb9dvzfA7mo1EkfRN4OhqzmXrq83ceWkOEm/cDepzPY4SOQUNx8lCDc2rrM2QmqxNsJIXDY8bBI3zyl0N+PDdTet3gk5TUnWNweRqTaOlzw43JUrN12CmzggSMs50q/TOMNQDjnSr9M4IEjLOV2CmzjclSs3jpc8OAeRqTZNSdY3rd4JOePDdTfPKXQ3PGwSN8CtYziEqDU3Et4yN6rnoDeIfuQ2ctoVOOCsJzl8+Iw40uoxOd1f8DjOYBQ69ewbOQAAAACWVQg5e48GN0+aqziM0aY5LXY5OGl5jTgyEcU3NTo+OKFqWjde9Nk2AAAAAKoMFjcAAAAAGqxwOAAAAACkLY04AAAAAJFg3zkAAAAAdrlYNgAAAADe2z82AAAAAIOVXToAAAAA2YU7OQAAAAAtxmE5AAAAAAWSPTcAAAAAkmwaNwAAAADq/YQ4YhmnNwY2nDcp05g3eOAOOOc3TDnm44042DPNNunBFjnrheI3OcKcOba4dDomNQA6Fb2GOSY1ADq2uHQ6OcKcOeuF4jfpwRY52DPNNubjjTjnN0w5eOAOOCnTmDcGNpw3YhmnN6LdiTj2B5g2Fa5HN0TCEDdlDzU3bsmWN4tCgjkj2yY4hEWCOZ2/qziqqJY6KrH5OO9KDDYZcs04T/7hNsJaWDjJWgU6BuXDN1NtsziHFTc3PQ99OGKbujb+Sw83QE5PNkBOTzb+Sw83Ypu6Nj0PfTiHFTc3U22zOAblwzfJWgU6wlpYOE/+4TYZcs0470oMNiqx+TiqqJY6nb+rOIRFgjkj2yY4i0KCOW7JljdlDzU3RMIQNxWuRzf2B5g2ot2JOGIZpzcGNpw3KdOYN3jgDjjnN0w55uONONgzzTbpwRY564XiNznCnDm2uHQ6JjUAOhW9hjkmNQA6trh0OjnCnDnrheI36cEWOdgzzTbm44045zdMOXjgDjgp05g3BjacN2IZpzfq/YQ4AAAAAJJsGjcAAAAABZI9NwAAAAAtxmE5AAAAANmFOzkAAAAAg5VdOgAAAADe2z82AAAAAHa5WDYAAAAAkWDfOQAAAACkLY04AAAAABqscDgAAAAAqgwWNwAAAABe9Nk2oWpaNzU6PjgyEcU3aXmNOC12OTiM0aY5T5qrOHuPBjeWVQg5AAAAAPXsGznOYBQ63V/wONLqMTl8+Iw44KwnOXLaFTiIfuQ2quegNxLeMjeEqDU3wK1jODxsEjfPKXQ348N1N63eCTlNSdY3B5GpNo6XPDjclSs3XYKbOCBIyznSr9M4w1AOOdKv0zggSMs5XYKbONyVKzeOlzw4B5GpNk1J1jet3gk548N1N88pdDc8bBI3wkhcNkJqsTY2rrM2HyUIN0jkFDepzPY4Sb9wN3HlpDiXrq83g6GrORJH0TfA7mo1v12/NzSHsDWDyIo35VdkOU/ELzepPhg4PovUNkaUKTjdQ4E2Thb5NneJNDe6q+U3RZKNN594HDgCV9w3WNMUOTaCIjiOuZU2dttTOMaMfDTyzmM4IPxMOfCNRTjHa504fC4QOHBdnTh3ur43e59vNnOJczdcveo2hpYINuTJkzgoSkw2+RVfNt5ylDaEmzs2El/INj4dCDnuou02ebdsOO6i7TY+HQg5El/INoSbOzbecpQ2+RVfNihKTDbkyZM4hpYINiTMoTYlv2Q31nNmOJ1Amze2s/k3vZ3BN11b+TiJNdU3rXzHNDEMyzfJz8s0CQ2pN7FDyTgrAX83xriLN/+XNTdEJZQ3bq0JNwn1gDhkYS83rQ/dNT2kTDeKzqkzCiVVN4kYlzgfp0Q32On2N18dIzdg8A44Y376NrCEEDRLl1M1zQYuNBNORThjbkA0L4nEN2NuQDQTTkU4zQYuNEuXUzWwhBA03qpJONcieDfVAFg0DbHtNgEVHzjlOPw2nflfNETK9DaJ6CQ2AAAAABN8TDgAAAAATdgpOAAAAAAW4uc1AAAAAMMM5jgAAAAAAAAAACQnBjZWla03AlsyNjFjszZB0l02UTKjN74HejZ7nrs4vgd6NlEyozdB0l02MWOzNgJbMjZWla03JCcGNiTMoTYlv2Q31nNmOJ1Amze2s/k3vZ3BN11b+TiJNdU3rXzHNDEMyzfJz8s0CQ2pN7FDyTgrAX83xriLN/+XNTfAwcwzISSQNEEF8zh0qNM0aeBuN7PFEjVmb7I4UlU2NahqlDgoUEE1ylxsNy5JLDW741s52twENV5FBDY/+bo01+I9N/dAfDSc5Oc0U0ZrN0GjNDV5aKE4j7OJNcnkJTgMy8I1yI9oN5ky7jVnxLU5mTLuNciPaDcMy8I1yeQlOI+ziTV5aKE4QaM0NVNGazec5Oc0rpswN6uAhTZ+aY43gbIgN9xp6DcrqPg4MGs4ODQDhzjGL4Q4naqmOcT/mziDJw02/VePOM28uDSsRFM4jmlhOYRdCDhpv/03KKynNy2FMDgYkE43CM2qNC3eDTbOwrE2yclvNoL9GTmnw802fBaYOLutKTfDruY4lddyN/qatzhIUYY3FXDKOEKcXDfyELA5wgcRNyVnxDdR/qs2zxSjNnqnSDamqiw4e//vNQAAAACSKuk4AAAAAHp6GjcAAAAAINRPOQAAAAB3+R05AAAAANQN2zUAAAAA6GeUOgAAAADUDds1AAAAAHf5HTkAAAAAINRPOQAAAAB6eho3AAAAAJIq6TgAAAAAwK1jOISoNTcS3jI3quegN4h+5DZy2hU44KwnOXz4jDjS6jE53V/wOM5gFDr17Bs5AAAAAJZVCDl7jwY3T5qrOIzRpjktdjk4aXmNODIRxTc1Oj44oWpaN1702TY2bZQ2KwqwNYkuADehFdA3pLtrN+44AzmKJ+Q3nTVsOSQLWzjnP4U4CEq0OB20EziknNA4/G3NOQ1NnThPAbA5a6AxOLIpzThXbrY3pDrHM+YXPzdD/9o3xTTUNoYdIDhz6nQzT6MROdEE2TPeguc21RtNNGB+lTleP8o0jFyFOeb6PjWD3mQ1oD2QNWlY/jqgPZA1g95kNeb6PjWMXIU5Xj/KNGB+lTnVG0003oLnNtEE2TNPoxE5c+p0M4YdIDjFNNQ2Q//aN+YXPzekOsczV262N7IpzThroDE4TwGwOQ1NnTj8bc05pJzQOB20EzgISrQ45z+FOCQLWzidNWw5iifkN+44Azmku2s3oRXQN4kuADcrCrA1Nm2UNl702Tahalo3NTo+ODIRxTdpeY04LXY5OIzRpjlPmqs4e48GN5ZVCDkAAAAA9ewbOc5gFDrdX/A40uoxOXz4jDjgrCc5ctoVOIh+5Daq56A3Et4yN4SoNTfArWM4AAAAAJIq6TgAAAAAenoaNwAAAAAg1E85AAAAAHf5HTkAAAAA1A3bNQAAAADoZ5Q6AAAAANQN2zUAAAAAd/kdOQAAAAAg1E85AAAAAHp6GjcAAAAAkirpOAAAAAB7/+81pqosOHqnSDbPFKM2Uf6rNiVnxDfCBxE38hCwOUKcXDcVcMo4SFGGN/qatziV13I3w67mOLutKTd8Fpg4p8PNNoL9GTnJyW82zsKxNi3eDTYIzao0GJBONy2FMDgorKc3ab/9N4RdCDiOaWE5rERTOM28uDT9V484gycNNsT/mzidqqY5xi+EODQDhzgwazg4K6j4ONxp6DeBsiA3fmmON6uAhTaumzA3nOTnNFNGazdBozQ1eWihOI+ziTXJ5CU4DMvCNciPaDeZMu41Z8S1OZky7jXIj2g3DMvCNcnkJTiPs4k1eWihOEGjNDVTRms3nOTnNPdAfDTX4j03P/m6NF5FBDba3AQ1u+NbOS5JLDXKXGw3KFBBNahqlDhSVTY1Zm+yOLPFEjVp4G43dKjTNEEF8zghJJA0wMHMM/+XNTfGuIs3KwF/N7FDyTgJDak3yc/LNDEMyzetfMc0iTXVN11b+Ti9ncE3trP5N51AmzfWc2Y4Jb9kNyTMoTYkJwY2VpWtNwJbMjYxY7M2QdJdNlEyoze+B3o2e567OL4HejZRMqM3QdJdNjFjszYCWzI2VpWtNyQnBjYAAAAAAAAAAMMM5jgAAAAAFuLnNQAAAABN2Ck4AAAAABN8TDgAAAAAiegkNkTK9Dad+V805Tj8NgEVHzgNse02WlLvNqtIozWyXn04q0ijNVpS7zYAAAAAAAAAAMMM5jgAAAAAFuLnNQAAAABN2Ck4AAAAABN8TDgAAAAAiegkNkQllDdurQk3CfWAOGRhLzetD901PaRMN4rOqTMKJVU3iRiXOB+nRDfY6fY3Xx0jN2DwDjhjfvo2XX76N9rtijYnF4I3NjO/Nr2e6TWgRPU2SJ4uOA/cDDdz3bc4D9wMN0ieLjigRPU2vZ7pNTYzvzYnF4I32u2KNl1++jey3OsyE8zONx4vLTNf1d00d55zM46sjDnvj5wzEKGaNdjqrjPPX/E4/1ilM9deBznwH4YztoIgNshhQzPrkh453l8GM7gKtzWWr4c2qlhiNzqB0DYdWGk40jUeN7uFrzivHGM3XbeeN8ThkDc1Ehs3mc2aN8HD4jiW54c3EfDgOFpNSzeC8SM48ioKN/b6hTPOqrQ2lQ5xN2+okTdo8Aw31I6iOBbWYDeu+iY38WWwN8UCODVgdgA4yuQsOV0wIDib9vg4XTAgOMrkLDlgdgA4xQI4NfFlsDeu+iY3FtZgN9SOojho8Aw3b6iRNwAAAAAyw604AAAAAGZvETgAAAAAqd0TNAAAAAAi9Ag6AAAAALSTejYAAAAADvN+OQAAAAC0CoY5AAAAABLW5TYAAAAAhfF8OQAAAABiBo41AAAAADddHjcIzao0Ld4NNs7CsTbJyW82gv0ZOafDzTZ8Fpg4u60pN8Ou5jiV13I3+pq3OEhRhjcVcMo4QpxcN/IQsDnCBxE3JWfEN1H+qzbPFKM2eqdINqaqLDh7/+81PGwSN88pdDfjw3U3rd4JOU1J1jcHkak2jpc8ONyVKzddgps4IEjLOdKv0zjDUA450q/TOCBIyzldgps43JUrN46XPDgHkak2TUnWN63eCTnjw3U3zyl0NzxsEjdFwq400KWcOH8oFjWpnMg3KsaFNalBWDajHu016n0fOpu7PTYXpvw3A4RtNkqzhzmEnlM2Ck+OOfcxDTbUGvs33oyiNRkVgjkZhTU19LnYM65F0DT2y6E29suhNq5F0DT0udgzGYU1NRkVgjnejKI11Br7N/cxDTYKT445hJ5TNkqzhzkDhG02F6b8N5u7PTbqfR86ox7tNalBWDYqxoU1qZzIN38oFjXQpZw4RcKuNDxsEjfPKXQ348N1N63eCTlNSdY3B5GpNo6XPDjclSs3XYKbOCBIyznSr9M4w1AOOdKv0zggSMs5XYKbONyVKzeOlzw4B5GpNk1J1jet3gk548N1N88pdDc8bBI3e//vNaaqLDh6p0g2zxSjNlH+qzYlZ8Q3wgcRN/IQsDlCnFw3FXDKOEhRhjf6mrc4lddyN8Ou5ji7rSk3fBaYOKfDzTaC/Rk5yclvNs7CsTYt3g02CM2qNDddHjcAAAAAYgaONQAAAACF8Xw5AAAAABLW5TYAAAAAtAqGOQAAAAAO8345AAAAALSTejYAAAAAIvQIOgAAAACp3RM0AAAAAGZvETgAAAAAMsOtOAAAAABvqJE3aPAMN9SOojgW1mA3rvomN/FlsDfFAjg1YHYAOMrkLDldMCA4m/b4OF0wIDjK5Cw5YHYAOMUCODXxZbA3rvomNxbWYDfUjqI4aPAMN2+okTeVDnE3zqq0Nvb6hTPyKgo3gvEjOFpNSzcR8OA4lueHN8HD4jiZzZo3NRIbN8ThkDddt543rxxjN7uFrzjSNR43HVhpODqB0DaqWGI3lq+HNrgKtzXeXwYz65IeOchhQzO2giA28B+GM9deBzn/WKUzz1/xONjqrjMQoZo174+cM46sjDl3nnMzX9XdNB4vLTMTzM43stzrMl1++jfa7Yo2JxeCNzYzvza9nuk1oET1NkieLjgP3Aw3c923OA/cDDdIni44oET1Nr2e6TU2M782JxeCN9rtijZdfvo3Y376NmDwDjhfHSM32On2Nx+nRDeJGJc4CiVVN4rOqTM9pEw3rQ/dNWRhLzcJ9YA4bq0JN0QllDeJ6CQ2AAAAABN8TDgAAAAATdgpOAAAAAAW4uc1AAAAAMMM5jgAAAAAAAAAAFpS7zarSKM1sl59OKtIozVaUu82RMr0Np35XzTlOPw2ARUfOA2x7TawhBA0S5dTNc0GLjQTTkU4Y25ANC+JxDdjbkA0E05FOM0GLjRLl1M1sIQQNIePgjcnbDQ4/QupN5WXnzdm28o3Ns+0OF9l2ze61BI1HuDSN8YNsTPDcLU3TlGZOKk7jzdYJT03RlgxNqpDtDf8T3U24ySXOEU/oDY0qhw2b2m+NtEWQjSRVcc28/e1OMkXtjbtihs4W9uTNukTJTjQLl02VswINqWaZzjighw2OC2qNqHfXDbPnUw1HvuQNoKwwTgE8ag2CyVxOATxqDaCsME4HvuQNs+dTDWh31w2OC2qNuKCHDalmmc4vOMtNh3voDdUM/w2RvLwN3LlpzhJFi04BHQdONaUYjjbrkI5OWp/OI9/uzV8VXA4xe73M5vfPzhfBhM5q94IOKDGozdEfrg3OL8IOAAAAADXj/I3AAAAAEvyBDgAAAAA8hAVOQAAAACP9yg2AAAAAAAAAAAAAAAA4TpLOQAAAAC674M4AAAAAHIwoTgAAAAApoqZNgAAAAAQ/Lk2b6iRN2jwDDfUjqI4FtZgN676JjfxZbA3xQI4NWB2ADjK5Cw5XTAgOJv2+DhdMCA4yuQsOWB2ADjFAjg18WWwN676JjcW1mA31I6iOGjwDDdvqJE3rpswN6uAhTZ+aY43gbIgN9xp6DcrqPg4MGs4ODQDhzjGL4Q4naqmOcT/mziDJw02/VePOM28uDSsRFM4jmlhOYRdCDhpv/03KKynNy2FMDgYkE43Thb5Nt1DgTZGlCk4PovUNqk+GDhPxC835VdkOYPIijc0h7A1v12/N8DuajUSR9E3g6GrOZeurzdx5aQ4Sb9wN6nM9jhI5BQ3HyUINzauszZCarE2wkhcNg2oqTeUIV43+iS5OMyIuDcYhlY3YWAYOJpmLjUCMmo49qtcOc+Hlzht1iY5z4eXOParXDkCMmo4mmYuNWFgGDgYhlY3zIi4N/okuTiUIV43DaipN8JIXDZCarE2Nq6zNh8lCDdI5BQ3qcz2OEm/cDdx5aQ4l66vN4OhqzkSR9E3wO5qNb9dvzc0h7A1g8iKN+VXZDlPxC83qT4YOD6L1DZGlCk43UOBNk4W+TYYkE43LYUwOCispzdpv/03hF0IOI5pYTmsRFM4zby4NP1XjziDJw02xP+bOJ2qpjnGL4Q4NAOHODBrODgrqPg43GnoN4GyIDd+aY43q4CFNq6bMDdvqJE3aPAMN9SOojgW1mA3rvomN/FlsDfFAjg1YHYAOMrkLDldMCA4m/b4OF0wIDjK5Cw5YHYAOMUCODXxZbA3rvomNxbWYDfUjqI4aPAMN2+okTcQ/Lk2AAAAAKaKmTYAAAAAcjChOAAAAAC674M4AAAAAOE6SzkAAAAAAAAAAAAAAACP9yg2AAAAAPIQFTkAAAAAS/IEOAAAAADXj/I3AAAAADi/CDhEfrg3oMajN6veCDhfBhM5m98/OMXu9zN8VXA4j3+7NTlqfzjbrkI51pRiOAR0HThJFi04cuWnOEby8DdUM/w2He+gN7zjLTalmmc44oIcNjgtqjah31w2z51MNR77kDaCsME4BPGoNgslcTgE8ag2grDBOB77kDbPnUw1od9cNjgtqjbighw2pZpnOFbMCDbQLl026RMlOFvbkzbtihs4yRe2NvP3tTiRVcc20RZCNG9pvjY0qhw2RT+gNuMklzj8T3U2qkO0N0ZYMTZYJT03qTuPN05RmTjDcLU3xg2xMx7g0je61BI1X2XbNzbPtDhm28o3lZefN/0LqTcnbDQ4h4+CN7CEEDRLl1M1zQYuNBNORThjbkA0L4nEN2NuQDQTTkU4zQYuNEuXUzWwhBA0DbHtNgEVHzjlOPw2nflfNETK9Dbeqkk41yJ4N9UAWDRxYCc3cLkCMwrKyTcDfhQz5bGVN2KFGTPxwSI307YPM6K2kDjsF/Yy49KJN+B/qTWsVHE2WKTPNWskojeG8Oc1EwWXOIbw5zVrJKI3WKTPNaxUcTbgf6k149KJN69v0Db0SK03t5BfOJqD5Dc5Ap033SQLONHm4jhShRc4kYa9NdQgETgAAAAAoqD2N9nIuzixS783sdMzN/BePDWCp3Q4vUKENa8NBTglXK81hjPsNxug0jWTH6A3iCrdNTFOFjiL18g12kPROOULoTWZiUc3lUhtNa7PtDWUwYM3AAAAAM59OjgAAAAAXOCVNwAAAACeXpI3AAAAAE7ERzkAAAAAnl6SNwAAAABc4JU3AAAAAM59OjgAAAAAlMGDN7zjLTYd76A3VDP8Nkby8Ddy5ac4SRYtOAR0HTjWlGI4265COTlqfziPf7s1fFVwOMXu9zOb3z84XwYTOaveCDigxqM3RH64Nzi/CDiWr4c2qlhiNzqB0DYdWGk40jUeN7uFrzivHGM3XbeeN8ThkDc1Ehs3mc2aN8HD4jiW54c3EfDgOFpNSzeC8SM48ioKN/b6hTPOqrQ2lQ5xN5zk5zRTRms3QaM0NXlooTiPs4k1yeQlOAzLwjXIj2g3mTLuNWfEtTmZMu41yI9oNwzLwjXJ5CU4j7OJNXlooThBozQ1U0ZrN5zk5zRcveo2c4lzN3ufbzZ3ur43cF2dOHwuEDjHa5048I1FOCD8TDnyzmM4xox8NHbbUziOuZU2NoIiOFjTFDkCV9w3n3gcOEWSjTe6q+U3d4k0N3eJNDe6q+U3RZKNN594HDgCV9w3WNMUOTaCIjiOuZU2dttTOMaMfDTyzmM4IPxMOfCNRTjHa504fC4QOHBdnTh3ur43e59vNnOJczdcveo2nOTnNFNGazdBozQ1eWihOI+ziTXJ5CU4DMvCNciPaDeZMu41Z8S1OZky7jXIj2g3DMvCNcnkJTiPs4k1eWihOEGjNDVTRms3nOTnNJUOcTfOqrQ29vqFM/IqCjeC8SM4Wk1LNxHw4DiW54c3wcPiOJnNmjc1Ehs3xOGQN123njevHGM3u4WvONI1HjcdWGk4OoHQNqpYYjeWr4c2OL8IOER+uDegxqM3q94IOF8GEzmb3z84xe73M3xVcDiPf7s1OWp/ONuuQjnWlGI4BHQdOEkWLThy5ac4RvLwN1Qz/DYd76A3vOMtNpTBgzcAAAAAzn06OAAAAABc4JU3AAAAAJ5ekjcAAAAATsRHOQAAAACeXpI3AAAAAFzglTcAAAAAzn06OAAAAACUwYM3rs+0NZVIbTWZiUc35QuhNdpD0TiL18g1MU4WOIgq3TWTH6A3G6DSNYYz7DclXK81rw0FOL1ChDWCp3Q48F48NbHTMzexS7832ci7OKKg9jcAAAAA1CAROJGGvTVShRc40ebiON0kCzg5Ap03moPkN7eQXzj0SK03r2/QNuPSiTfgf6k1rFRxNlikzzVrJKI3hvDnNRMFlziG8Oc1aySiN1ikzzWsVHE24H+pNePSiTfsF/YyoraQONO2DzPxwSI3YoUZM+WxlTcDfhQzCsrJN3C5AjNxYCc31QBYNNcieDfeqkk4kiH3OAAAAAAAAAAAAAAAAFF5UzgAAAAA6hR1OAAAAAD2vRs4RHnPNRDGpDZmx+01xr37NX1u9jXgri04kpblNcX+ETifgsI1j2GeN+GpIjeBGcA1BFYRNaKn7TWTJBU4lYMFNn3PbDiVgwU2kyQVOKKn7TUEVhE1gRnANeGpIjc3ctk3AAAAABKmDDYAAAAAcAhdOQAAAAA93gozAAAAAPTZxzgAAAAATUrgOAAAAAD0LQg0AAAAAEopCDnwXjw1gqd0OL1ChDWvDQU4JVyvNYYz7DcboNI1kx+gN4gq3TUxThY4i9fINdpD0TjlC6E1mYlHN5VIbTWuz7Q1pZpnOOKCHDY4Lao2od9cNs+dTDUe+5A2grDBOATxqDYLJXE4BPGoNoKwwTge+5A2z51MNaHfXDY4Lao24oIcNqWaZziy3OsyE8zONx4vLTNf1d00d55zM46sjDnvj5wzEKGaNdjqrjPPX/E4/1ilM9deBznwH4YztoIgNshhQzPrkh453l8GM7gKtzXAwcwzISSQNEEF8zh0qNM0aeBuN7PFEjVmb7I4UlU2NahqlDgoUEE1ylxsNy5JLDW741s52twENV5FBDY/+bo01+I9N/dAfDSGlgg25MmTOChKTDb5FV823nKUNoSbOzYSX8g2Ph0IOe6i7TZ5t2w47qLtNj4dCDkSX8g2hJs7Nt5ylDb5FV82KEpMNuTJkziGlgg290B8NNfiPTc/+bo0XkUENtrcBDW741s5LkksNcpcbDcoUEE1qGqUOFJVNjVmb7I4s8USNWngbjd0qNM0QQXzOCEkkDTAwcwzuAq3Nd5fBjPrkh45yGFDM7aCIDbwH4Yz114HOf9YpTPPX/E42OquMxChmjXvj5wzjqyMOXeeczNf1d00Hi8tMxPMzjey3OsypZpnOOKCHDY4Lao2od9cNs+dTDUe+5A2grDBOATxqDYLJXE4BPGoNoKwwTge+5A2z51MNaHfXDY4Lao24oIcNqWaZziuz7Q1lUhtNZmJRzflC6E12kPROIvXyDUxThY4iCrdNZMfoDcboNI1hjPsNyVcrzWvDQU4vUKENYKndDjwXjw1SikIOQAAAAD0LQg0AAAAAE1K4DgAAAAA9NnHOAAAAAA93gozAAAAAHAIXTkAAAAAEqYMNgAAAAA3ctk34akiN4EZwDUEVhE1oqftNZMkFTiVgwU2fc9sOJWDBTaTJBU4oqftNQRWETWBGcA14akiN49hnjefgsI1xf4ROJKW5TXgri04fW72Nca9+zVmx+01EMakNkR5zzX2vRs4AAAAAOoUdTgAAAAAUXlTOAAAAAAAAAAAAAAAAJIh9zh2jeQ3rzPoM0DM3zevM+gzdo3kN2uErzeEAjk3c//MN+OekziV/No3GvmgNT3Q0zcuBC4zBni6N0BPgTgAAAAAwg0rOAAAAACi55A2AAAAAOgdwzUAAAAARoZAOAAAAAD9RA84AAAAADjxsDfhqSI3gRnANQRWETWip+01kyQVOJWDBTZ9z2w4lYMFNpMkFTiip+01BFYRNYEZwDXhqSI3r2/QNvRIrTe3kF84moPkNzkCnTfdJAs40ebiOFKFFziRhr011CAROAAAAACioPY32ci7OLFLvzex0zM3RlgxNqpDtDf8T3U24ySXOEU/oDY0qhw2b2m+NtEWQjSRVcc28/e1OMkXtjbtihs4W9uTNukTJTjQLl02VswINl1++jfa7Yo2JxeCNzYzvza9nuk1oET1NkieLjgP3Aw3c923OA/cDDdIni44oET1Nr2e6TU2M782JxeCN9rtijZdfvo3JMyhNiW/ZDfWc2Y4nUCbN7az+Te9ncE3XVv5OIk11TetfMc0MQzLN8nPyzQJDak3sUPJOCsBfzfGuIs3/5c1N/+XNTfGuIs3KwF/N7FDyTgJDak3yc/LNDEMyzetfMc0iTXVN11b+Ti9ncE3trP5N51AmzfWc2Y4Jb9kNyTMoTZdfvo32u2KNicXgjc2M782vZ7pNaBE9TZIni44D9wMN3PdtzgP3Aw3SJ4uOKBE9Ta9nuk1NjO/NicXgjfa7Yo2XX76N1bMCDbQLl026RMlOFvbkzbtihs4yRe2NvP3tTiRVcc20RZCNG9pvjY0qhw2RT+gNuMklzj8T3U2qkO0N0ZYMTax0zM3sUu/N9nIuziioPY3AAAAANQgETiRhr01UoUXONHm4jjdJAs4OQKdN5qD5De3kF849EitN69v0DbhqSI3gRnANQRWETWip+01kyQVOJWDBTZ9z2w4lYMFNpMkFTiip+01BFYRNYEZwDXhqSI3OPGwNwAAAAD9RA84AAAAAEaGQDgAAAAA6B3DNQAAAACi55A2AAAAAMINKzgAAAAAQE+BOAZ4ujcuBC4zPdDTNxr5oDWV/No3456TOHP/zDeEAjk3a4SvN3aN5DevM+gzQMzfN68z6DN2jeQ3AAAAAJ/+mjcAAAAALBonOAAAAACf/po3AAAAAGuErzeEAjk3c//MN+OekziV/No3GvmgNT3Q0zcuBC4zBni6N0BPgTj2vRs4RHnPNRDGpDZmx+01xr37NX1u9jXgri04kpblNcX+ETifgsI1j2GeN+PSiTfgf6k1rFRxNlikzzVrJKI3hvDnNRMFlziG8Oc1aySiN1ikzzWsVHE24H+pNePSiTeHj4I3J2w0OP0LqTeVl583ZtvKNzbPtDhfZds3utQSNR7g0jfGDbEzw3C1N05RmTipO483WCU9N0QllDdurQk3CfWAOGRhLzetD901PaRMN4rOqTMKJVU3iRiXOB+nRDfY6fY3Xx0jN2DwDjhjfvo2JCcGNlaVrTcCWzI2MWOzNkHSXTZRMqM3vgd6Nnueuzi+B3o2UTKjN0HSXTYxY7M2AlsyNlaVrTckJwY2Y376NmDwDjhfHSM32On2Nx+nRDeJGJc4CiVVN4rOqTM9pEw3rQ/dNWRhLzcJ9YA4bq0JN0QllDdYJT03qTuPN05RmTjDcLU3xg2xMx7g0je61BI1X2XbNzbPtDhm28o3lZefN/0LqTcnbDQ4h4+CN+PSiTfgf6k1rFRxNlikzzVrJKI3hvDnNRMFlziG8Oc1aySiN1ikzzWsVHE24H+pNePSiTePYZ43n4LCNcX+ETiSluU14K4tOH1u9jXGvfs1ZsftNRDGpDZEec819r0bOEBPgTgGeLo3LgQuMz3Q0zca+aA1lfzaN+Oekzhz/8w3hAI5N2uErzcAAAAAn/6aNwAAAAAsGic4AAAAAJ/+mjcAAAAAdo3kN68z6DNAzN83rzPoM3aN5DeSIfc4AAAAAAAAAAAAAAAAUXlTOAAAAADqFHU4AAAAAHFgJzdwuQIzCsrJNwN+FDPlsZU3YoUZM/HBIjfTtg8zoraQOOwX9jKwhBA0S5dTNc0GLjQTTkU4Y25ANC+JxDdjbkA0E05FOM0GLjRLl1M1sIQQNAAAAAAAAAAAwwzmOAAAAAAW4uc1AAAAAE3YKTgAAAAAE3xMOAAAAACJ6CQ2iegkNgAAAAATfEw4AAAAAE3YKTgAAAAAFuLnNQAAAADDDOY4AAAAAAAAAACwhBA0S5dTNc0GLjQTTkU4Y25ANC+JxDdjbkA0E05FOM0GLjRLl1M1sIQQNOwX9jKitpA407YPM/HBIjdihRkz5bGVNwN+FDMKysk3cLkCM3FgJzcAAAAA6hR1OAAAAABReVM4AAAAAAAAAAAAAAAAkiH3OHaN5DevM+gzQMzfN68z6DN2jeQ33qpJONcieDfVAFg0RMr0Np35XzTlOPw2ARUfOA2x7TZaUu82q0ijNbJefTirSKM1WlLvNg2x7TYBFR845Tj8Np35XzREyvQ21QBYNNcieDfeqkk4\",\"U_re\":\"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\",\"U_im\":\"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\",\"u0_imag\":0.009214336277859584,\"absorptive\":true,\"n_reflections\":9672,\"energy_ev\":200000.0,\"wavelength\":0.025079337357037376,\"k_max\":3.0,\"hexagonal\":true}", -}; diff --git a/widget/js/diffsim/crystal3d.ts b/widget/js/diffsim/crystal3d.ts deleted file mode 100644 index 2c4800c4d..000000000 --- a/widget/js/diffsim/crystal3d.ts +++ /dev/null @@ -1,328 +0,0 @@ -/** - * Orthographic renderer of the unit cell (or a block of cells) on a 2D - * canvas. The view looks along the lab z axis (the beam), so the drawing - * shares its axes with the diffraction pattern; viewX = -1 mirrors x for the - * view from the detector side. Optional coordination polyhedra are convex - * hulls of the nearest neighbours of each centre atom. - */ - -import { Quat, Vec3, matVec, quatToMatrix } from "./math"; -import type { CrystalData } from "./physics"; - -export interface CellStyle { - dark: boolean; - showAxes: boolean; - showLabels: boolean; - atomScale: number; // covalent radius multiplier - viewX: number; // +1 gun side (beam into the screen), -1 detector side (beam toward the viewer) -} - -interface Atom { - pos: Vec3; // Cartesian, crystal frame, relative to the block centre - color: string; - radius: number; - symbol: string; -} - -interface Face { - verts: Vec3[]; // polygon, crystal frame relative to the block centre - color: string; -} - -function rgb(c: number[]): string { - return `rgb(${Math.round(c[0] * 255)},${Math.round(c[1] * 255)},${Math.round(c[2] * 255)})`; -} - -export interface CellGeometry { - atoms: Atom[]; - corners: Vec3[]; // 8 corners of the block relative to the centre - edges: [number, number][]; - innerEdges: [Vec3, Vec3][]; // cell boundaries inside the block - faces: Face[]; - axes: Vec3[]; // a, b, c (one cell) from the origin corner - origin: Vec3; - radius: number; // bounding radius, A -} - -/** - * Geometry of an na x nb x nc block of cells. Atoms on the block boundary - * are repeated (the corner atoms of fcc all appear). Polyhedra are drawn - * around every species except the most numerous one (cations in an oxide; - * every atom of an elemental crystal), using neighbours within - * 1.2 (r_i + r_j) of the covalent radii, including atoms outside the block. - */ -export function cellGeometry(c: CrystalData, nCells: [number, number, number] = [1, 1, 1], polyhedra = false): CellGeometry { - const cell = c.cell; - const [na, nb, nc] = nCells.map((n) => Math.max(1, Math.min(6, Math.round(n)))) as [number, number, number]; - const cart = (f: number[]): Vec3 => [ - f[0] * cell[0][0] + f[1] * cell[1][0] + f[2] * cell[2][0], - f[0] * cell[0][1] + f[1] * cell[1][1] + f[2] * cell[2][1], - f[0] * cell[0][2] + f[1] * cell[1][2] + f[2] * cell[2][2], - ]; - const center = cart([na / 2, nb / 2, nc / 2]); - const rel = (v: Vec3): Vec3 => [v[0] - center[0], v[1] - center[1], v[2] - center[2]]; - const corners: Vec3[] = []; - for (let i = 0; i < 8; i++) corners.push(rel(cart([(i & 1) * na, ((i >> 1) & 1) * nb, ((i >> 2) & 1) * nc]))); - const edges: [number, number][] = []; - for (let i = 0; i < 8; i++) for (let j = i + 1; j < 8; j++) { - const d = i ^ j; - if (d === 1 || d === 2 || d === 4) edges.push([i, j]); - } - const innerEdges: [Vec3, Vec3][] = []; - const n3 = [na, nb, nc]; - for (let ax = 0; ax < 3; ax++) { - const [u, v] = [(ax + 1) % 3, (ax + 2) % 3]; - for (let iu = 0; iu <= n3[u]; iu++) for (let iv = 0; iv <= n3[v]; iv++) { - const onBoundary = (iu === 0 || iu === n3[u]) && (iv === 0 || iv === n3[v]); - if (onBoundary) continue; - const f0 = [0, 0, 0], f1 = [0, 0, 0]; - f0[u] = iu; f0[v] = iv; f1[u] = iu; f1[v] = iv; f1[ax] = n3[ax]; - innerEdges.push([rel(cart(f0)), rel(cart(f1))]); - } - } - - // atoms inside the block (boundary included) and a halo of images for neighbour search - const eps = 1e-4; - const atoms: Atom[] = []; - const halo: { pos: Vec3; species: number }[] = []; - const inBlock: boolean[] = []; - const nSpec = c.positions_frac.length; - for (let n = 0; n < nSpec; n++) { - const f = c.positions_frac[n].map((x) => x - Math.floor(x + eps)); - for (let sx = -1; sx <= na + 1; sx++) for (let sy = -1; sy <= nb + 1; sy++) for (let sz = -1; sz <= nc + 1; sz++) { - const g = [f[0] + sx, f[1] + sy, f[2] + sz]; - const inside = g[0] <= na + eps && g[1] <= nb + eps && g[2] <= nc + eps && g[0] >= -eps && g[1] >= -eps && g[2] >= -eps; - const p = rel(cart(g)); - halo.push({ pos: p, species: n }); - inBlock.push(inside); - if (inside) atoms.push({ pos: p, color: rgb(c.colors[n]), radius: c.radii[n], symbol: c.symbols[n] }); - } - } - - const faces: Face[] = []; - if (polyhedra) { - const counts = new Map(); - for (const s of c.symbols) counts.set(s, (counts.get(s) || 0) + 1); - const species = [...counts.keys()]; - let centres = new Set(species); - if (species.length > 1) { - const most = species.reduce((a, b) => ((counts.get(a) || 0) >= (counts.get(b) || 0) ? a : b)); - centres = new Set(species.filter((s) => s !== most)); - } - const cutoffMax = 1.2 * 2 * Math.max(...c.radii); - for (let i = 0; i < halo.length; i++) { - if (!inBlock[i]) continue; - const si = halo[i].species; - if (!centres.has(c.symbols[si])) continue; - const pi = halo[i].pos; - const nbr: Vec3[] = []; - for (let j = 0; j < halo.length; j++) { - if (j === i) continue; - const sj = halo[j].species; - if (species.length > 1 && c.symbols[sj] === c.symbols[si]) continue; - const pj = halo[j].pos; - const d = Math.hypot(pj[0] - pi[0], pj[1] - pi[1], pj[2] - pi[2]); - if (d > cutoffMax) continue; - if (d <= 1.2 * (c.radii[si] + c.radii[sj])) nbr.push(pj); - } - if (nbr.length >= 4 && nbr.length <= 14) { - for (const poly of convexHullFaces(nbr, pi)) faces.push({ verts: poly, color: rgb(c.colors[si]) }); - } - } - } - - let radius = 0; - for (const p of corners) radius = Math.max(radius, Math.hypot(p[0], p[1], p[2])); - return { - atoms, corners, edges, innerEdges, faces, - axes: [cell[0] as Vec3, cell[1] as Vec3, cell[2] as Vec3], origin: corners[0], radius: radius + 0.5, - }; -} - -/** Faces of the convex hull of a few points (brute force, n <= 14), as outward-ordered polygons. */ -function convexHullFaces(pts: Vec3[], inside: Vec3): Vec3[][] { - const n = pts.length; - const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; - const cross = (a: Vec3, b: Vec3): Vec3 => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; - const dot = (a: Vec3, b: Vec3) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; - const planes = new Map }>(); - let scale = 0; - for (const p of pts) scale = Math.max(scale, Math.hypot(...sub(p, inside))); - const tol = 1e-3 * scale; - for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) for (let k = j + 1; k < n; k++) { - let nrm = cross(sub(pts[j], pts[i]), sub(pts[k], pts[i])); - const L = Math.hypot(...nrm); - if (L < 1e-9) continue; - nrm = [nrm[0] / L, nrm[1] / L, nrm[2] / L]; - if (dot(nrm, sub(pts[i], inside)) < 0) nrm = [-nrm[0], -nrm[1], -nrm[2]]; - const off = dot(nrm, pts[i]); - let ok = true; - const on: number[] = []; - for (let m = 0; m < n; m++) { - const d = dot(nrm, pts[m]) - off; - if (d > tol) { ok = false; break; } - if (Math.abs(d) <= tol) on.push(m); - } - if (!ok) continue; - const key = `${nrm.map((v) => v.toFixed(2)).join(",")}|${off.toFixed(2)}`; - const entry = planes.get(key) || { normal: nrm, verts: new Set() }; - for (const m of on) entry.verts.add(m); - planes.set(key, entry); - } - const out: Vec3[][] = []; - for (const { normal, verts } of planes.values()) { - const vs = [...verts].map((m) => pts[m]); - if (vs.length < 3) continue; - const cen: Vec3 = [0, 0, 0]; - for (const v of vs) { cen[0] += v[0] / vs.length; cen[1] += v[1] / vs.length; cen[2] += v[2] / vs.length; } - const e1 = sub(vs[0], cen); - const e2 = cross(normal, e1); - vs.sort((a, b) => Math.atan2(dot(sub(a, cen), e2), dot(sub(a, cen), e1)) - Math.atan2(dot(sub(b, cen), e2), dot(sub(b, cen), e1))); - out.push(vs); - } - return out; -} - -export function drawCell( - canvas: HTMLCanvasElement, geom: CellGeometry, q: Quat, size: number, style: CellStyle, -) { - const dpr = window.devicePixelRatio || 1; - if (canvas.width !== size * dpr || canvas.height !== size * dpr) { - canvas.width = size * dpr; - canvas.height = size * dpr; - } - const ctx = canvas.getContext("2d"); - if (!ctx) return; - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - ctx.clearRect(0, 0, size, size); - const R = quatToMatrix(q); - const scale = (0.5 * size * 0.82) / geom.radius; // px per A - const cx = size / 2, cy = size / 2; - const proj = (v: Vec3): [number, number, number] => { - const w = matVec(R, v); - return [cx + style.viewX * w[0] * scale, cy - w[1] * scale, style.viewX * w[2]]; - }; - const edgeColor = style.dark ? "rgba(220,220,220," : "rgba(40,40,40,"; - const depthFrac = (z: number) => Math.min(1, Math.max(0, 0.5 + z / (2 * geom.radius))); - const pc = geom.corners.map(proj); - type Prim = { z: number; draw: () => void }; - const prims: Prim[] = []; - for (const [i, j] of geom.edges) { - const a = pc[i], b = pc[j]; - const z = 0.5 * (a[2] + b[2]); - prims.push({ - z, - draw: () => { - ctx.strokeStyle = edgeColor + (0.35 + 0.5 * depthFrac(z)) + ")"; - ctx.lineWidth = 1.2; - ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke(); - }, - }); - } - for (const [p0, p1] of geom.innerEdges) { - const a = proj(p0), b = proj(p1); - const z = 0.5 * (a[2] + b[2]); - prims.push({ - z, - draw: () => { - ctx.strokeStyle = edgeColor + (0.12 + 0.2 * depthFrac(z)) + ")"; - ctx.lineWidth = 0.8; - ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke(); - }, - }); - } - for (const face of geom.faces) { - const pv = face.verts.map(proj); - let z = 0; - for (const p of pv) z += p[2] / pv.length; - prims.push({ - z: z - 1e-3, - draw: () => { - ctx.beginPath(); - ctx.moveTo(pv[0][0], pv[0][1]); - for (let i = 1; i < pv.length; i++) ctx.lineTo(pv[i][0], pv[i][1]); - ctx.closePath(); - ctx.fillStyle = face.color; - ctx.globalAlpha = 0.18 + 0.17 * depthFrac(z); - ctx.fill(); - ctx.globalAlpha = 0.6; - ctx.strokeStyle = face.color; - ctx.lineWidth = 0.8; - ctx.stroke(); - ctx.globalAlpha = 1; - }, - }); - } - const atomScale = style.atomScale * (geom.faces.length ? 0.6 : 1); - for (const at of geom.atoms) { - const p = proj(at.pos); - const r = Math.max(1.5, at.radius * atomScale * scale); - prims.push({ - z: p[2], - draw: () => { - const grad = ctx.createRadialGradient(p[0] - 0.35 * r, p[1] - 0.35 * r, 0.1 * r, p[0], p[1], r); - grad.addColorStop(0, lighten(at.color, 0.55)); - grad.addColorStop(0.7, at.color); - grad.addColorStop(1, lighten(at.color, -0.45)); - ctx.fillStyle = grad; - ctx.globalAlpha = 0.55 + 0.45 * depthFrac(p[2]); - ctx.beginPath(); ctx.arc(p[0], p[1], r, 0, 2 * Math.PI); ctx.fill(); - ctx.globalAlpha = 1; - ctx.strokeStyle = style.dark ? "rgba(0,0,0,0.6)" : "rgba(0,0,0,0.35)"; - ctx.lineWidth = 0.8; - ctx.stroke(); - }, - }); - } - prims.sort((a, b) => a.z - b.z); - for (const p of prims) p.draw(); - - if (style.showAxes) { - const o = proj(geom.origin); - const names = ["a", "b", "c"]; - const cols = ["#e53935", "#43a047", "#1e88e5"]; - for (let i = 0; i < 3; i++) { - const ax = geom.axes[i]; - const tip = proj([geom.origin[0] + ax[0], geom.origin[1] + ax[1], geom.origin[2] + ax[2]]); - ctx.strokeStyle = cols[i]; - ctx.lineWidth = 2.5; - ctx.beginPath(); ctx.moveTo(o[0], o[1]); ctx.lineTo(tip[0], tip[1]); ctx.stroke(); - if (style.showLabels) { - const dx = tip[0] - o[0], dy = tip[1] - o[1]; - const L = Math.hypot(dx, dy) || 1; - ctx.fillStyle = cols[i]; - ctx.font = "bold 14px sans-serif"; - ctx.textAlign = "center"; ctx.textBaseline = "middle"; - ctx.fillText(names[i], tip[0] + (dx / L) * 11, tip[1] + (dy / L) * 11); - } - } - } - // lab axes in the corner - const ox = 22, oy = size - 22, L = 18; - ctx.strokeStyle = style.dark ? "#aaa" : "#555"; - ctx.fillStyle = style.dark ? "#aaa" : "#555"; - ctx.lineWidth = 1.2; - ctx.font = "10px sans-serif"; - ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(ox + L, oy); ctx.stroke(); - ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(ox, oy - L); ctx.stroke(); - ctx.textAlign = "left"; ctx.textBaseline = "middle"; - ctx.fillText("x", ox + L + 3, oy); - ctx.textAlign = "center"; ctx.textBaseline = "bottom"; - ctx.fillText("y", ox, oy - L - 2); - ctx.beginPath(); ctx.arc(ox, oy, 3.5, 0, 2 * Math.PI); ctx.stroke(); - ctx.textAlign = "left"; ctx.textBaseline = "top"; - if (style.viewX > 0) { - ctx.beginPath(); ctx.moveTo(ox - 2.5, oy - 2.5); ctx.lineTo(ox + 2.5, oy + 2.5); ctx.moveTo(ox - 2.5, oy + 2.5); ctx.lineTo(ox + 2.5, oy - 2.5); ctx.stroke(); - ctx.fillText("beam into screen (view from gun)", ox + 6, oy + 4); - } else { - ctx.beginPath(); ctx.arc(ox, oy, 1.2, 0, 2 * Math.PI); ctx.fill(); - ctx.fillText("beam toward you (view from detector)", ox + 6, oy + 4); - } -} - -function lighten(color: string, amount: number): string { - const m = color.match(/rgb\((\d+),(\d+),(\d+)\)/); - if (!m) return color; - const f = (v: number) => Math.max(0, Math.min(255, Math.round(amount >= 0 ? v + (255 - v) * amount : v * (1 + amount)))); - return `rgb(${f(+m[1])},${f(+m[2])},${f(+m[3])})`; -} diff --git a/widget/js/diffsim/index.tsx b/widget/js/diffsim/index.tsx deleted file mode 100644 index 978eddfbe..000000000 --- a/widget/js/diffsim/index.tsx +++ /dev/null @@ -1,881 +0,0 @@ -/** - * DiffractionSim: unit cell (left) and its diffraction pattern (right), both - * following the same orientation. Drag the cell to tilt the crystal and the - * pattern follows live. Every calculation (kinematical and Bloch wave - * intensities, CBED disks, Kossel lines) runs here in the browser, so the - * widget also works as a standalone HTML page. - */ - -import * as React from "react"; -import { createRender, useModel, useModelState } from "@anywidget/react"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; -import Stack from "@mui/material/Stack"; -import Select from "@mui/material/Select"; -import MenuItem from "@mui/material/MenuItem"; -import Switch from "@mui/material/Switch"; -import Slider from "@mui/material/Slider"; -import Button from "@mui/material/Button"; -import TextField from "@mui/material/TextField"; -import ToggleButton from "@mui/material/ToggleButton"; -import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; -import Tooltip from "@mui/material/Tooltip"; -import { useTheme } from "../theme"; -import { COLORMAP_NAMES } from "../colormaps"; -import { downloadBlob } from "../format"; -import { Quat, Vec3, directionIndices, matTVec, parseDirection, qmult, qnormalize, quatFromAxisAngle, quatFromZoneAxis, quatToMatrix, threeToFour } from "./math"; -import { - Reflection, blochIntensities, blochSolve, kinematicalTilted, kosselLines, kosselLookup, labReflections, - nanobeamIntensities, nanobeamSolve, parseCrystal, parseKossel, hybridBeams, precessionTilts, slabIntensities, -} from "./physics"; -import { cellGeometry, drawCell } from "./crystal3d"; -import { - Frame, cbedImage, drawDisks, drawEwaldPanel, drawImage, drawKikuchiOverlay, drawKosselLines, drawMarkers, histogramBins, - nanobeamImage, setupCanvas, tiltGrid, toPx, -} from "./pattern"; - -const DIRECT: Reflection = { index: -1, hkl: [0, 0, 0], g: [0, 0, 0], gLen: 0, s: 0 }; -const SPIN_FPS = 20; // orientation update rate while spinning -const CBED_PREC_NODES = 8; // precession ring nodes per incident direction of the cone -const QUALITY: Record = { - fast: { grid: 5, beams: 24, nanobeam: 40 }, - medium: { grid: 7, beams: 36, nanobeam: 64 }, - fine: { grid: 9, beams: 56, nanobeam: 96 }, -}; - -// --------------------------------------------------------------------------- -function Histogram({ bins, vminPct, vmaxPct, onRangeChange, width = 130, height = 40, dark, lo, hi }: { - bins: number[]; vminPct: number; vmaxPct: number; onRangeChange: (a: number, b: number) => void; - width?: number; height?: number; dark: boolean; lo: number; hi: number; -}) { - const canvasRef = React.useRef(null); - const c = dark ? { bg: "#1a1a1a", on: "#888", off: "#444", border: "#333" } : { bg: "#f0f0f0", on: "#666", off: "#bbb", border: "#ccc" }; - React.useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext("2d"); - if (!ctx) return; - const dpr = window.devicePixelRatio || 1; - canvas.width = width * dpr; canvas.height = height * dpr; - ctx.scale(dpr, dpr); - ctx.fillStyle = c.bg; ctx.fillRect(0, 0, width, height); - const nb = 64, ratio = Math.floor(bins.length / nb); - const red: number[] = []; - for (let i = 0; i < nb; i++) { let s = 0; for (let j = 0; j < ratio; j++) s += bins[i * ratio + j] || 0; red.push(s); } - const mx = Math.max(...red.map((v) => Math.log1p(v)), 1e-3); - const bw = width / nb; - const b0 = Math.floor((vminPct / 100) * nb), b1 = Math.floor((vmaxPct / 100) * nb); - for (let i = 0; i < nb; i++) { - const h = (Math.log1p(red[i]) / mx) * (height - 2); - ctx.fillStyle = i >= b0 && i <= b1 ? c.on : c.off; - ctx.fillRect(i * bw + 0.5, height - h, Math.max(1, bw - 1), h); - } - }, [bins, vminPct, vmaxPct, width, height, dark]); - const fmt = (pct: number) => { const v = lo + (pct / 100) * (hi - lo); return Math.abs(v) >= 1000 || (Math.abs(v) < 0.01 && v !== 0) ? v.toExponential(1) : v.toFixed(2); }; - return ( - - - { const [a, b] = v as number[]; onRangeChange(Math.min(a, b - 1), Math.max(b, a + 1)); }} - min={0} max={100} size="small" valueLabelDisplay="auto" valueLabelFormat={fmt} - sx={{ width, py: 0, "& .MuiSlider-thumb": { width: 8, height: 8 }, "& .MuiSlider-rail": { height: 2 }, "& .MuiSlider-track": { height: 2 }, "& .MuiSlider-valueLabel": { fontSize: 10, padding: "2px 4px" } }} - /> - - {fmt(vminPct)} - {fmt(vmaxPct)} - - - ); -} - -function LabeledSlider({ label, value, onChange, min, max, step, fmt, width = 200, disabled }: { - label: string; value: number; onChange: (v: number) => void; min: number; max: number; step: number; - fmt: (v: number) => string; width?: number; disabled?: boolean; -}) { - return ( - - - {label} - {fmt(value)} - - onChange(v as number)} - sx={{ py: 0.5, "& .MuiSlider-thumb": { width: 12, height: 12 } }} /> - - ); -} - -function fmtIndices(v: [number, number, number] | null, hexagonal = false): string { - if (!v) return "—"; - const idx: number[] = hexagonal ? threeToFour(v) : v; - return "[" + idx.map((h) => (h < 0 ? `${-h}̅` : `${h}`)).join("") + "]"; -} - -// --------------------------------------------------------------------------- -function DiffSim() { - const model = useModel(); - const { themeInfo, colors } = useTheme(); - const dark = themeInfo.theme === "dark"; - const standalone = !!model.get("standalone"); - const embedded: Record = (model.get("embedded_presets") as Record) || {}; - - const [crystalJson, setCrystalJson] = useModelState("crystal_json"); - const [kosselJson] = useModelState("kossel_json"); - const [presets] = useModelState("presets"); - const [preset, setPreset] = useModelState("preset"); - const [energy, setEnergy] = useModelState("energy_ev"); - const [orientation, setOrientation] = useModelState("orientation"); - const [mode, setMode] = useModelState("mode"); - const [render, setRender] = useModelState("render"); - const [dynamical, setDynamical] = useModelState("dynamical"); - const [thickness, setThickness] = useModelState("thickness_A"); - const [semiconv, setSemiconv] = useModelState("semiconv_mrad"); - const [precession, setPrecession] = useModelState("precession_deg"); - const [nPrecession] = useModelState("n_precession"); - const [sigma, setSigma] = useModelState("sigma_excitation"); - const [stepDeg, setStepDeg] = useModelState("rotation_step_deg"); - const [spinSpeed, setSpinSpeed] = useModelState("rotation_speed_deg"); // deg/s for the continuous rotation buttons - const [scaling, setScaling] = useModelState("scaling"); - const [power, setPower] = useModelState("power"); - const [cmap, setCmap] = useModelState("cmap"); - const [markerPower, setMarkerPower] = useModelState("marker_power"); - const [markerSize, setMarkerSize] = useModelState("marker_size"); - const [vminPct, setVminPct] = useModelState("vmin_pct"); - const [vmaxPct, setVmaxPct] = useModelState("vmax_pct"); - const [showLabels, setShowLabels] = useModelState("show_labels"); - const [showHkl, setShowHkl] = useModelState("show_hkl"); - const [showCellAxes, setShowCellAxes] = useModelState("show_cell_axes"); - const [nCells, setNCells] = useModelState("n_cells"); - const [polyhedra, setPolyhedra] = useModelState("polyhedra"); - const [showEwald, setShowEwald] = useModelState("show_ewald"); - const [sizePref] = useModelState("size"); - const [status] = useModelState("status"); - - const crystal = React.useMemo(() => parseCrystal(crystalJson), [crystalJson]); - const kossel = React.useMemo(() => parseKossel(kosselJson), [kosselJson]); - const nCellsSafe: [number, number, number] = [nCells?.[0] || 1, nCells?.[1] || 1, nCells?.[2] || 1]; - const geom = React.useMemo(() => (crystal ? cellGeometry(crystal, nCellsSafe, polyhedra) : null), [crystal, nCellsSafe.join(","), polyhedra]); - - // local view state - const [patternRange, setPatternRange] = useModelState("pattern_range"); - const [fieldMrad, setFieldMrad] = useModelState("field_mrad"); - const [SG_MAX] = useModelState("sg_max"); - const [quality, setQuality] = useModelState("quality"); - const [kikuchi, setKikuchi] = useModelState("show_kikuchi"); - const [viewFrom] = useModelState("view_from"); - const viewX = viewFrom === "gun" ? 1 : -1; - const qMaxDisp = Math.min(Math.max(patternRange || 0, 0.2), crystal?.k_max ?? 4); - const setQMaxDisp = setPatternRange; - const [zoneText, setZoneText] = React.useState(""); - const [ptrDrag, setDragging] = React.useState(false); - // continuous slow rotation about the screen axes (toggle buttons next to the step box) - const [spin, setSpin] = React.useState<{ x: boolean; y: boolean }>({ x: false, y: false }); - const spinning = spin.x || spin.y; - const dragging = ptrDrag || spinning; // reduced quality while the orientation is changing - const [winW, setWinW] = React.useState(typeof window !== "undefined" ? window.innerWidth : 1200); - React.useEffect(() => { - const f = () => setWinW(window.innerWidth); - window.addEventListener("resize", f); - return () => window.removeEventListener("resize", f); - }, []); - // pattern square S on the right; on the left the cell square Sc above the Ewald panel (Sc x Se), S = Sc + gap + Se - const S = Math.max(220, Math.min(sizePref, winW - 40)); - const GAP = 8; - const Sc = showEwald ? Math.round((S - GAP) / 1.5) : S; - const Se = showEwald ? S - GAP - Sc : 0; - - // orientation: local quaternion for smooth dragging, pushed to the model with a throttle - const [quat, setQuatLocal] = React.useState(orientation as Quat); - const quatRef = React.useRef(quat); - React.useEffect(() => { const q = orientation as Quat; quatRef.current = q; setQuatLocal(q); }, [orientation.join(",")]); - const pushTimer = React.useRef(null); - const setQuat = React.useCallback((q: Quat, immediate = false) => { - quatRef.current = q; - setQuatLocal(q); - const push = () => { pushTimer.current = null; setOrientation([...quatRef.current]); }; - if (immediate) { if (pushTimer.current) window.clearTimeout(pushTimer.current); push(); } - else if (!pushTimer.current) pushTimer.current = window.setTimeout(push, 200); - }, [setOrientation]); - - // axis given in SCREEN coordinates (x right, y up, z toward the viewer); mapped to the lab frame by the view - const rotateLab = React.useCallback((axis: Vec3, deg: number, immediate = true) => { - const dq = quatFromAxisAngle([viewX * axis[0], axis[1], viewX * axis[2]], (deg * Math.PI) / 180); - setQuat(qnormalize(qmult(dq, quatRef.current)), immediate); - }, [setQuat, viewX]); - - const spinRef = React.useRef({ ...spin, speed: spinSpeed || 6 }); - spinRef.current = { ...spin, speed: spinSpeed || 6 }; - React.useEffect(() => { - if (!spinning) return; - let raf = 0, last = 0, due = 0, pendX = 0, pendY = 0; - const tick = (t: number) => { - const dt = last ? Math.min(0.1, (t - last) / 1000) : 0; - last = t; - if (spinRef.current.y) pendY += spinRef.current.speed * dt; - if (spinRef.current.x) pendX += spinRef.current.speed * dt; - if (t >= due) { - due = t + 1000 / SPIN_FPS; - if (pendY) rotateLab([0, 1, 0], pendY, false); - if (pendX) rotateLab([1, 0, 0], pendX, false); - pendX = pendY = 0; - } - raf = requestAnimationFrame(tick); - }; - raf = requestAnimationFrame(tick); - return () => cancelAnimationFrame(raf); - }, [spinning, rotateLab]); - - // ---- pointer handling on the cell canvas (mouse and touch) ------------- - const cellRef = React.useRef(null); - const pointers = React.useRef>(new Map()); - const onPointerDown = (e: React.PointerEvent) => { - (e.target as HTMLElement).setPointerCapture?.(e.pointerId); - pointers.current.set(e.pointerId, [e.clientX, e.clientY]); - setDragging(true); - }; - const onPointerMove = (e: React.PointerEvent) => { - const prev = pointers.current.get(e.pointerId); - if (!prev) return; - const cur: [number, number] = [e.clientX, e.clientY]; - if (pointers.current.size >= 2) { - // two fingers: twist about the beam axis - const other = [...pointers.current.entries()].find(([id]) => id !== e.pointerId); - if (other) { - const [ox, oy] = other[1]; - const a0 = Math.atan2(prev[1] - oy, prev[0] - ox); - const a1 = Math.atan2(cur[1] - oy, cur[0] - ox); - let da = a1 - a0; - if (da > Math.PI) da -= 2 * Math.PI; - if (da < -Math.PI) da += 2 * Math.PI; - rotateLab([0, 0, 1], (-da * 180) / Math.PI, false); - } - } else if (e.shiftKey) { - // shift-drag: twist about the beam (the desktop version of the two-finger gesture) - const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); - const cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2; - let da = Math.atan2(cur[1] - cy, cur[0] - cx) - Math.atan2(prev[1] - cy, prev[0] - cx); - if (da > Math.PI) da -= 2 * Math.PI; - if (da < -Math.PI) da += 2 * Math.PI; - rotateLab([0, 0, 1], (-da * 180) / Math.PI, false); - } else { - const dx = cur[0] - prev[0], dy = cur[1] - prev[1]; - const degPerPx = 180 / Sc; - const ang = Math.hypot(dx, dy) * degPerPx; - if (ang > 0) rotateLab([dy, dx, 0], ang, false); // trackball: the face nearest the viewer follows the pointer - } - pointers.current.set(e.pointerId, cur); - }; - const onPointerUp = (e: React.PointerEvent) => { - pointers.current.delete(e.pointerId); - if (pointers.current.size === 0) { setDragging(false); setQuat(quatRef.current, true); } - }; - - // ---- pointer handling on the pattern canvas --------------------------------- - // Dragging the pattern by dq (1/A, or rad in Kossel mode) moves the zone - // axis so the pattern follows: the Laue circle centre sits at q = -k0 delta - // for a zone axis tilted by delta, so the crystal tilts by -dq/k0. - const shiftPattern = React.useCallback((dqx: number, dqy: number, inverseAngstrom: boolean, immediate: boolean) => { - const k0 = crystal ? 1 / crystal.wavelength : 1; - const ax = inverseAngstrom ? dqx / k0 : dqx, ay = inverseAngstrom ? dqy / k0 : dqy; - const ang = Math.hypot(ax, ay); - if (ang <= 0) return; - const dq = quatFromAxisAngle([ay, -ax, 0], ang); - setQuat(qnormalize(qmult(dq, quatRef.current)), immediate); - }, [crystal, setQuat]); - const patPointers = React.useRef>(new Map()); - const patScale = React.useRef(1); // px per unit of the current frame - const onPatDown = (e: React.PointerEvent) => { - (e.target as HTMLElement).setPointerCapture?.(e.pointerId); - patPointers.current.set(e.pointerId, [e.clientX, e.clientY]); - setDragging(true); - }; - const onPatMove = (e: React.PointerEvent) => { - const prev = patPointers.current.get(e.pointerId); - if (!prev) return; - const cur: [number, number] = [e.clientX, e.clientY]; - if (patPointers.current.size >= 2) { - const other = [...patPointers.current.entries()].find(([id]) => id !== e.pointerId); - if (other) { - const [ox, oy] = other[1]; - let da = Math.atan2(cur[1] - oy, cur[0] - ox) - Math.atan2(prev[1] - oy, prev[0] - ox); - if (da > Math.PI) da -= 2 * Math.PI; - if (da < -Math.PI) da += 2 * Math.PI; - rotateLab([0, 0, 1], (-da * 180) / Math.PI, false); - } - } else if (e.shiftKey) { - const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); - const cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2; - let da = Math.atan2(cur[1] - cy, cur[0] - cx) - Math.atan2(prev[1] - cy, prev[0] - cx); - if (da > Math.PI) da -= 2 * Math.PI; - if (da < -Math.PI) da += 2 * Math.PI; - rotateLab([0, 0, 1], (-da * 180) / Math.PI, false); - } else { - const dx = (viewX * (cur[0] - prev[0])) / patScale.current, dy = -(cur[1] - prev[1]) / patScale.current; - shiftPattern(dx, dy, mode !== "kossel", false); - } - patPointers.current.set(e.pointerId, cur); - }; - const onPatUp = (e: React.PointerEvent) => { - patPointers.current.delete(e.pointerId); - if (patPointers.current.size === 0) { setDragging(false); setQuat(quatRef.current, true); } - }; - // Double-click on a visible disk: tilt the crystal to the exact Bragg - // condition of that reflection (two-beam: the Laue circle through 000 and - // g). A crystal tilt (wx, wy) about the lab axes changes s_g by - // wx g_y - wy g_x, so w = s_g (-g_y, g_x) / |g_xy|^2 zeroes it. On empty - // space the Laue-circle centre moves to the clicked point (same sense: - // the zone axis tilts away from the click by q / k0); in Kossel mode the - // clicked direction of the tilt map moves onto the axis. - const onPatDoubleClick = (e: React.MouseEvent) => { - const rect = e.currentTarget.getBoundingClientRect(); - const x = e.clientX - rect.left, y = e.clientY - rect.top; - if ((mode === "nanobeam" || mode === "cbed") && crystal) { - // a convergent beam draws the same reflections as wide disks, so both - // modes snap to the same two-beam condition the same way - const beamList = mode === "cbed" && cbed ? cbed.beams : nbBeams; - const intenList = mode === "cbed" ? cbedMean : nbInten; - const snapPx = mode === "cbed" - ? Math.max(8, k0 * Math.sin(alpha) * patScale.current) - : Math.max(8, 1.2 * (render === "disks" ? k0 * Math.sin(alpha) * patScale.current : markerSize * (S / 420))); - // candidates under the click: several reflections of different g_z - // share one spot (in hcp the first HOLZ layer is only 0.21 1/A up), so - // take the one that needs the SMALLEST tilt to reach Bragg, and never - // jump by more than 5 degrees - let best: Reflection | null = null, bestTilt = (5 * Math.PI) / 180; - let iMax = 0; - for (let i = 1; i < beamList.length; i++) iMax = Math.max(iMax, intenList[i] || 0); - for (let i = 0; i < beamList.length; i++) { - const b = beamList[i]; - if (b.index < 0 || !(intenList[i] > 1e-4 * iMax)) continue; - const [px, py] = toPx(frame, b.g[0], b.g[1]); - if (Math.hypot(px - x, py - y) > snapPx) continue; - const gxy = Math.hypot(b.g[0], b.g[1]); - if (gxy < 1e-6) continue; - const tilt = Math.abs(b.s) / gxy; - if (tilt < bestTilt) { bestTilt = tilt; best = b; } - } - if (best) { - const gxy2 = best.g[0] ** 2 + best.g[1] ** 2; - const wx = (-best.s * best.g[1]) / gxy2, wy = (best.s * best.g[0]) / gxy2; - setQuat(qnormalize(qmult(quatFromAxisAngle([wx, wy, 0], Math.hypot(wx, wy)), quatRef.current)), true); - return; - } - } - const qx = (viewX * (x - rect.width / 2)) / patScale.current, qy = -(y - rect.height / 2) / patScale.current; - if (mode === "kossel") shiftPattern(-qx, -qy, false, true); - else shiftPattern(qx, qy, true, true); - }; - - // ---- derived geometry --------------------------------------------------- - const R = React.useMemo(() => quatToMatrix(quat), [quat]); - const zoneAxis = React.useMemo(() => { - if (!crystal) return null; - const dc = matTVec(R, [0, 0, 1]); - return directionIndices(crystal.cell, dc); - }, [crystal, R]); - const k0 = crystal ? 1 / crystal.wavelength : 0; - const qual = QUALITY[quality] || QUALITY.medium; - - // ---- nanobeam ----------------------------------------------------------- - // one Bloch solution per precession node (a single untilted node without precession) - const precNodes = React.useMemo(() => { - const n = dragging ? Math.max(6, Math.round((nPrecession || 24) / 2)) : nPrecession || 24; - return precessionTilts(k0, precession || 0, n); - }, [k0, precession, nPrecession, dragging]); - const nbSolution = React.useMemo(() => { - if (!crystal || mode !== "nanobeam" || !dynamical) return null; - // the physics uses every reflection the crystal carries; the pattern range only crops the drawing - return nanobeamSolve(crystal, quat, crystal.k_max, SG_MAX, dragging ? Math.min(qual.nanobeam, 40) : qual.nanobeam, precNodes); - }, [crystal, quat, mode, dynamical, dragging, qual, SG_MAX, precNodes]); - const nbBeams = React.useMemo(() => { - if (!crystal || mode !== "nanobeam") return []; - if (nbSolution) return nbSolution.beams; - return [DIRECT, ...labReflections(crystal, quat, crystal.k_max)]; - }, [crystal, quat, mode, nbSolution]); - const nb = { beams: nbBeams, nDyn: nbSolution ? Math.round(nbSolution.nDynMean) : 0 }; - const nbInten = React.useMemo(() => { - if (!crystal || mode !== "nanobeam") return new Float64Array(0); - if (nbSolution) return nanobeamIntensities(crystal, nbSolution, thickness); - const out = new Float64Array(nbBeams.length); - for (const t of precNodes) { - const v = kinematicalTilted(crystal, nbBeams, t, sigma); - for (let i = 0; i < out.length; i++) out[i] += v[i] / precNodes.length; - } - return out; - }, [crystal, nbBeams, nbSolution, precNodes, mode, thickness, sigma]); - - // ---- CBED --------------------------------------------------------------- - const alpha = semiconv * 1e-3; - const cbed = React.useMemo(() => { - if (!crystal || mode !== "cbed") return null; - const Rk = k0 * Math.sin(alpha); - const grid = tiltGrid(Rk, dragging ? 5 : qual.grid); - // every incident direction of the cone is itself precessed, so the cost is - // the grid times the ring: fewer ring nodes here than in nanobeam - const nodes = precessionTilts(k0, precession || 0, dragging ? 4 : CBED_PREC_NODES); - if (!dynamical) { - return { grid, beams: [DIRECT, ...labReflections(crystal, quat, crystal.k_max)], nDyn: 0, nodes, sols: null }; - } - const { beams, nDyn } = hybridBeams(crystal, quat, crystal.k_max, SG_MAX, dragging ? Math.min(qual.beams, 24) : qual.beams, Math.sin(alpha)); - const dyn = beams.slice(0, nDyn); - const sols: ReturnType[] = []; // grid tilt major, ring node minor - for (const t of grid.tilts) { - for (const nd of nodes) sols.push(blochSolve(crystal, dyn, [t[0] + nd[0], t[1] + nd[1]])); - } - return { grid, beams, nDyn, nodes, sols }; - }, [crystal, quat, qMaxDisp, mode, dynamical, alpha, k0, dragging, qual, SG_MAX, precession]); - const cbedInten = React.useMemo(() => { - if (!crystal || !cbed) return null; - const { grid, beams, nDyn, nodes, sols } = cbed; - return grid.tilts.map((t, i) => { - const out = new Float64Array(beams.length); - for (let k = 0; k < nodes.length; k++) { - const tilt: [number, number] = [t[0] + nodes[k][0], t[1] + nodes[k][1]]; - const acc = new Float64Array(beams.length); - if (sols) { - acc.set(blochIntensities(sols[i * nodes.length + k], thickness)); - slabIntensities(crystal, beams, nDyn, tilt, thickness, acc); - } else { - acc.set(kinematicalTilted(crystal, beams, tilt, sigma)); - } - for (let b = 0; b < out.length; b++) out[b] += acc[b] / nodes.length; - } - return out; - }); - }, [crystal, cbed, thickness, sigma]); - // disk-averaged intensity of every beam, for the double-click snap - const cbedMean = React.useMemo(() => { - if (!cbed || !cbedInten) return new Float64Array(0); - const out = new Float64Array(cbed.beams.length); - for (const arr of cbedInten) for (let b = 0; b < out.length; b++) out[b] += arr[b] / cbedInten.length; - return out; - }, [cbed, cbedInten]); - - // ---- Kossel ------------------------------------------------------------- - const fieldRad = fieldMrad * 1e-3; - const lines = React.useMemo(() => { - if (!crystal || (mode !== "kossel" && !kikuchi)) return []; - const fov = mode === "kossel" ? fieldRad : qMaxDisp / k0; - return kosselLines(crystal, quat, Math.min(crystal.k_max, 2.5), fov); - }, [crystal, quat, mode, fieldRad, kikuchi, qMaxDisp, k0]); - - // ---- pixel image of the current mode -------------------------------------- - const frame: Frame = React.useMemo(() => ({ size: S, qMax: mode === "kossel" ? fieldRad : qMaxDisp, viewX }), [S, mode, fieldRad, qMaxDisp, viewX]); - patScale.current = (0.5 * S * 0.92) / frame.qMax; - const pixelMode = (mode === "nanobeam" && render === "pixels") || mode === "cbed" || (mode === "kossel" && render === "pixels"); - const image = React.useMemo(() => { - if (!crystal || !pixelMode) return null; - if (mode === "nanobeam") return nanobeamImage(frame, nbBeams, nbInten, Math.max(1.5, S / 200)); - if (mode === "cbed" && cbed && cbedInten) return cbedImage(frame, cbed.beams, cbed.grid, cbedInten); - if (mode === "kossel" && kossel) return kosselLookup(kossel, quat, fieldRad, S, thickness, viewX); - return null; - }, [crystal, pixelMode, mode, frame, nbBeams, nbInten, cbed, cbedInten, kossel, quat, fieldRad, S, thickness, viewX]); - const display = React.useMemo(() => { - if (!image) return null; - let data = image; - if (scaling === "log") { - let mx = 0; - for (let i = 0; i < image.length; i++) if (isFinite(image[i])) mx = Math.max(mx, image[i]); - const eps = 1e-4 * (mx || 1); - data = new Float32Array(image.length); - for (let i = 0; i < image.length; i++) data[i] = isFinite(image[i]) ? Math.log10(Math.max(image[i], 0) + eps) : NaN; - } else if (scaling === "power") { - const pw = Math.min(Math.max(power || 0.5, 0.05), 1); - data = new Float32Array(image.length); - for (let i = 0; i < image.length; i++) data[i] = isFinite(image[i]) ? Math.pow(Math.max(image[i], 0), pw) : NaN; - } - let lo = Infinity, hi = -Infinity; - for (let i = 0; i < data.length; i++) { const v = data[i]; if (isFinite(v)) { if (v < lo) lo = v; if (v > hi) hi = v; } } - if (!(hi > lo)) { lo = 0; hi = 1; } - return { data, lo, hi, bins: histogramBins(data, lo, hi) }; - }, [image, scaling, power]); - - // ---- drawing -------------------------------------------------------------- - React.useEffect(() => { - const canvas = cellRef.current; - if (!canvas || !geom || !crystal) return; - drawCell(canvas, geom, quat, Sc, { dark, showAxes: showCellAxes, showLabels: showLabels, atomScale: 0.45, viewX }); - }, [geom, quat, Sc, dark, showCellAxes, showLabels, viewX]); - - const ewaldRef = React.useRef(null); - React.useEffect(() => { - const canvas = ewaldRef.current; - if (!canvas || !crystal || !showEwald || Se <= 0) return; - const dpr = window.devicePixelRatio || 1; - if (canvas.width !== Sc * dpr || canvas.height !== Se * dpr) { canvas.width = Sc * dpr; canvas.height = Se * dpr; } - const ctx = canvas.getContext("2d"); - if (!ctx) return; - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - const refl = mode === "nanobeam" && nbBeams.length ? nbBeams : labReflections(crystal, quat, crystal.k_max); - drawEwaldPanel(ctx, Sc, Se, refl, k0, qMaxDisp, SG_MAX, viewX, dark, mode !== "kossel" ? precession || 0 : 0); - }, [quat, Sc, Se, dark, viewX, showEwald, crystal, mode, nbBeams, qMaxDisp, k0, SG_MAX, precession]); - - const patRef = React.useRef(null); - React.useEffect(() => { - const canvas = patRef.current; - if (!canvas || !crystal) return; - const ctx = setupCanvas(canvas, S); - if (!ctx) return; - if (display) { - const vmin = display.lo + (vminPct / 100) * (display.hi - display.lo); - const vmax = display.lo + (vmaxPct / 100) * (display.hi - display.lo); - drawImage(ctx, frame, display.data, cmap, vmin, vmax, dark, mode === "kossel" ? "rad" : "Å⁻¹", mode === "kossel" ? 0.01 : 1); - if (mode !== "kossel" && kikuchi) drawKikuchiOverlay(ctx, frame, lines, k0, true); - if (mode === "nanobeam" && showHkl) labelBeams(ctx, frame, nbBeams, nbInten, dark, true); - } else if (mode === "nanobeam" && render === "disks") { - drawDisks(ctx, frame, nbBeams, nbInten, dark, showHkl, k0 * Math.sin(alpha), markerPower); - if (kikuchi) drawKikuchiOverlay(ctx, frame, lines, k0, dark); - } else if (mode === "nanobeam") { - drawMarkers(ctx, frame, nbBeams, nbInten, dark, showHkl, !dynamical, markerPower, markerSize); - if (kikuchi) drawKikuchiOverlay(ctx, frame, lines, k0, dark); - } else if (mode === "kossel") { - if (render === "pixels" && !kossel) { - ctx.fillStyle = dark ? "#000" : "#fff"; ctx.fillRect(0, 0, S, S); - ctx.fillStyle = dark ? "#ccc" : "#333"; ctx.font = "13px sans-serif"; ctx.textAlign = "center"; - ctx.fillText("no Kossel reference pattern loaded", S / 2, S / 2 - 10); - ctx.fillText(standalone ? "(export the page after compute_kossel_reference)" : "press “compute reference” below", S / 2, S / 2 + 10); - } else { - drawKosselLines(ctx, frame, lines, dark, showHkl, 0.02); - } - } else if (mode === "cbed") { - ctx.fillStyle = dark ? "#000" : "#fff"; ctx.fillRect(0, 0, S, S); - } - }, [crystal, display, frame, mode, render, dark, cmap, vminPct, vmaxPct, nbBeams, nbInten, showHkl, dynamical, kikuchi, lines, k0, kossel, S, standalone, markerPower, markerSize, alpha]); - - // ---- actions --------------------------------------------------------------- - const goZoneAxis = () => { - const uvw = parseDirection(zoneText); - if (!uvw || !crystal) return; - const c = crystal.cell; - const d: Vec3 = [ - uvw[0] * c[0][0] + uvw[1] * c[1][0] + uvw[2] * c[2][0], - uvw[0] * c[0][1] + uvw[1] * c[1][1] + uvw[2] * c[2][1], - uvw[0] * c[0][2] + uvw[1] * c[1][2] + uvw[2] * c[2][2], - ]; - setQuat(quatFromZoneAxis(d), true); - }; - const choosePreset = (name: string) => { - if (standalone) { - const data = embedded[name]; - if (data) { setCrystalJson(JSON.stringify(data)); setPreset(name); } - } else { - setPreset(name); - } - }; - const savePng = () => { - const a = cellRef.current, b = patRef.current; - if (!a || !b) return; - const off = document.createElement("canvas"); - off.width = a.width + b.width + 8; off.height = Math.max(a.height, b.height); - const ctx = off.getContext("2d"); - if (!ctx) return; - ctx.fillStyle = dark ? "#1e1e1e" : "#fff"; ctx.fillRect(0, 0, off.width, off.height); - ctx.drawImage(a, 0, 0); ctx.drawImage(b, a.width + 8, 0); - off.toBlob((blob) => { if (blob) downloadBlob(blob, `${crystal?.name || "crystal"}_${mode}.png`); }); - }; - const exportHtml = async () => { - const res = await fetch(import.meta.url); - const bundle = await res.text(); - const keys = ["crystal_json", "presets", "preset", "energy_ev", "k_max", "orientation", "mode", "render", "dynamical", "thickness_A", - "semiconv_mrad", "precession_deg", "n_precession", "sigma_excitation", "rotation_step_deg", "rotation_speed_deg", "pattern_range", "field_mrad", "sg_max", "quality", "show_kikuchi", "view_from", - "scaling", "power", "cmap", "marker_power", "marker_size", "vmin_pct", "vmax_pct", "show_labels", - "show_cell_axes", "show_hkl", "n_cells", "polyhedra", "show_ewald", "size", "kossel_json", "status", "widget_version"]; - const state: Record = {}; - for (const k of keys) state[k] = model.get(k); - state.orientation = [...quatRef.current]; - const emb: Record = { ...embedded }; - if (crystal && crystalJson) emb[preset || crystal.name] = JSON.parse(crystalJson); - state.embedded_presets = emb; - state.presets = Object.keys(emb); - if (!state.preset) state.preset = crystal?.name || ""; - downloadBlob(new Blob([standaloneHtml(bundle, state, `quantEM diffraction simulator: ${crystal?.name || ""}`)], { type: "text/html" }), - `${crystal?.name || "crystal"}_diffsim.html`); - }; - - const presetNames = standalone ? Object.keys(embedded) : presets; - const ctl = { - fontSize: 12, height: 30, bgcolor: colors.controlBg, color: colors.text, - "& .MuiSelect-select": { py: 0.4, fontSize: 12 }, - "& .MuiSvgIcon-root": { color: colors.textMuted }, - "& .MuiOutlinedInput-notchedOutline": { borderColor: colors.border }, - "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: colors.accent }, - "&.Mui-disabled": { color: colors.textMuted, "& .MuiSelect-select": { WebkitTextFillColor: colors.textMuted } }, - }; - const menuProps = { PaperProps: { sx: { bgcolor: colors.controlBg, color: colors.text, border: `1px solid ${colors.border}` } }, sx: { zIndex: 9999 } }; - const tbg = { - "& .MuiToggleButton-root": { - px: 1, py: 0.3, fontSize: 11, textTransform: "none", color: colors.textMuted, borderColor: colors.border, bgcolor: colors.controlBg, - "&.Mui-selected": { color: colors.accent, bgcolor: dark ? "#2e3a48" : "#e3eefc" }, - "&:hover": { bgcolor: dark ? "#333" : "#e8e8e8" }, - }, - }; - const tf = { - "& input": { fontSize: 12, py: 0.6, color: colors.text }, - "& input::placeholder": { color: colors.textMuted, opacity: 1 }, - "& .MuiOutlinedInput-notchedOutline": { borderColor: colors.border }, - "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: colors.accent }, - bgcolor: colors.controlBg, - }; - const btn = { fontSize: 11, height: 30, color: colors.accent, borderColor: colors.border, textTransform: "none" as const, "&:hover": { borderColor: colors.accent } }; - const sw = { "& .MuiSwitch-track": { bgcolor: dark ? "#777" : undefined } }; - const nDyn = mode === "nanobeam" ? nb.nDyn : mode === "cbed" && cbed ? cbed.nDyn : 0; - const nBeams = mode === "nanobeam" ? nbBeams.length : mode === "cbed" && cbed ? cbed.beams.length : lines.length; - - if (!crystal || !geom) { - return loading crystal…; - } - - return ( - - {/* top bar */} - - Diffraction simulator - - - - - setZoneText(e.target.value)} - onKeyDown={(e) => { if (e.key === "Enter") goZoneAxis(); }} - sx={{ width: 150, ...tf }} /> - - - - - - - {/* panels: cell (with the Ewald view below it) and the pattern */} - - - - {showEwald && Se > 0 && ( - - )} - - - - - - - {/* controls, full width under the panels */} - - {/* crystal row */} - - crystal - {(["x", "y", "z"] as const).map((ax, i) => ( - - rotateLab([+(i === 0), +(i === 1), +(i === 2)], -stepDeg)}>{ax} − - rotateLab([+(i === 0), +(i === 1), +(i === 2)], stepDeg)}>{ax} + - - ))} - setStepDeg(Math.max(0.01, Number(e.target.value) || 0.01))} - inputProps={{ step: 1, min: 0.01, max: 180, style: { fontSize: 11, padding: "4px 6px", width: 42 } }} sx={tf} /> - ° - - setSpin((s) => ({ ...s, y: !s.y }))}>↔ - setSpin((s) => ({ ...s, x: !s.x }))}>↕ - - - setSpinSpeed(v as number)} - sx={{ py: 0.5, "& .MuiSlider-thumb": { width: 12, height: 12 } }} /> - - {Math.round(spinSpeed || 6)}°/s - - zone axis {fmtIndices(zoneAxis, crystal.hexagonal)} - {crystal.name} · {crystal.spacegroup || crystal.pointgroup} - - - setShowCellAxes(e.target.checked)} /> - cell axes - setShowLabels(e.target.checked)} /> - axis labels - setPolyhedra(e.target.checked)} /> - polyhedra - setShowEwald(e.target.checked)} /> - Ewald sphere - cells - {[0, 1, 2].map((i) => ( - { const v = [...nCellsSafe]; v[i] = Math.max(1, Math.min(6, Math.round(Number(e.target.value) || 1))); setNCells(v); }} - inputProps={{ min: 1, max: 6, step: 1, style: { fontSize: 11, padding: "3px 4px", width: 26 } }} sx={tf} /> - ))} - along a, b, c - - - {/* pattern row */} - - pattern - v && setMode(v)} sx={tbg}> - nanobeam - CBED - Kikuchi pattern - - {mode !== "cbed" && ( - v && setRender(v)} sx={tbg}> - {mode === "kossel" ? "lines" : "markers"} - {mode === "nanobeam" && disks} - pixels - - )} - {mode !== "kossel" && ( - - setDynamical(e.target.checked)} /> - dynamical - - )} - - setShowHkl(e.target.checked)} /> - hkl labels - - {mode !== "kossel" && ( - - setKikuchi(e.target.checked)} /> - Kikuchi lines - - )} - {mode !== "kossel" && dynamical && ( - - quality - - - )} - {mode === "kossel" && render === "pixels" && !kossel && !standalone && ( - - )} - - - {/* physics sliders */} - - `${v.toFixed(0)} Å`} width={180} - disabled={mode !== "kossel" ? !dynamical : render !== "pixels"} /> - {(mode === "cbed" || (mode === "nanobeam" && render === "disks")) && `${v.toFixed(1)} mrad`} width={180} />} - {mode !== "kossel" && (v > 0 ? `${v.toFixed(2)}°` : "off")} width={180} />} - {mode !== "kossel" && `${v.toFixed(2)} Å⁻¹`} width={180} />} - {mode === "kossel" && `${v.toFixed(0)} mrad`} width={180} />} - {mode !== "kossel" && !dynamical && `${v.toFixed(3)} Å⁻¹`} width={180} />} - - - {/* display row */} - - {mode === "nanobeam" && render === "markers" && ( - <> - `p = ${v.toFixed(2)}`} width={180} /> - `${v.toFixed(0)} px`} width={140} /> - - )} - {mode === "nanobeam" && render === "disks" && ( - `p = ${v.toFixed(2)}`} width={180} /> - )} - {pixelMode && ( - <> - - intensity scaling - - - {scaling === "power" && ( - v.toFixed(2)} width={110} /> - )} - - colormap - - - - )} - {display && ( - { setVminPct(a); setVmaxPct(b); }} dark={dark} lo={display.lo} hi={display.hi} /> - )} - - - - {(energy / 1e3).toFixed(0)} keV · λ = {(crystal.wavelength * 100).toFixed(3)} pm · {nBeams} {mode === "kossel" ? "lines" : "beams"} - {mode !== "kossel" && dynamical ? ` · ${nDyn} Bloch beams (|s| < ${SG_MAX} Å⁻¹${crystal.absorptive ? ", absorptive" : ""}), thin-slab intensities for the rest` : ""} - {mode !== "kossel" && precession > 0 - ? ` · precession ${precession.toFixed(2)}° over ${mode === "cbed" ? (cbed ? cbed.nodes.length : 0) : precNodes.length} ring nodes` - : ""} - {mode === "cbed" ? " · disks summed incoherently where they overlap" : ""} - {status ? ` · ${status}` : ""} - - drag the cell (near face follows) or the pattern (tilt map follows) · shift-drag or two fingers twist about the beam · double-click a disk for its two-beam condition, or empty space to put the Laue circle centre there · buttons rotate about the screen axes - - - ); -} - -function labelBeams(ctx: CanvasRenderingContext2D, f: Frame, beams: Reflection[], inten: Float64Array, dark: boolean, onImage: boolean) { - let iMax = 0; - for (let i = 0; i < beams.length; i++) iMax = Math.max(iMax, inten[i]); - if (iMax <= 0) return; - ctx.font = `${Math.max(9, Math.round(f.size / 38))}px sans-serif`; - ctx.textAlign = "center"; ctx.textBaseline = "bottom"; - ctx.fillStyle = onImage ? "#ffd54f" : dark ? "#ffd54f" : "#c62828"; - let count = 0; - for (let i = 0; i < beams.length && count < 40; i++) { - if (inten[i] / iMax < 0.08) continue; - const [x, y] = toPx(f, beams[i].g[0], beams[i].g[1]); - ctx.fillText(beams[i].hkl.map((h) => (h < 0 ? `${-h}̅` : `${h}`)).join(""), x, y - 6); - count++; - } -} - -function standaloneHtml(bundle: string, state: Record, title: string): string { - const bytes = new TextEncoder().encode(bundle); - let bin = ""; - for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); - const b64 = btoa(bin); - const stateJson = JSON.stringify(state).replace(/<\//g, "<\\/"); - const safeTitle = title.replace(/[<>&]/g, ""); - return ` - - - - -${safeTitle} - - - -
- - - -`; -} - -export const render = createRender(DiffSim); diff --git a/widget/js/diffsim/math.ts b/widget/js/diffsim/math.ts deleted file mode 100644 index 82105f7cd..000000000 --- a/widget/js/diffsim/math.ts +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Small numerical kit for the diffraction simulator: quaternions (scalar - * first, the quantem convention: v_lab = R(q) v_crystal), base64 float32 - * decoding, and a complex Hermitian eigensolver (cyclic Jacobi) for the - * Bloch wave calculation in the browser. - */ - -export type Quat = [number, number, number, number]; -export type Vec3 = [number, number, number]; - -export function qmult(a: Quat, b: Quat): Quat { - const [aw, ax, ay, az] = a; - const [bw, bx, by, bz] = b; - return [ - aw * bw - ax * bx - ay * by - az * bz, - aw * bx + ax * bw + ay * bz - az * by, - aw * by - ax * bz + ay * bw + az * bx, - aw * bz + ax * by - ay * bx + az * bw, - ]; -} - -export function qnormalize(q: Quat): Quat { - const n = Math.hypot(q[0], q[1], q[2], q[3]) || 1; - const s = q[0] < 0 ? -1 / n : 1 / n; - return [q[0] * s, q[1] * s, q[2] * s, q[3] * s]; -} - -export function quatFromAxisAngle(axis: Vec3, angle: number): Quat { - const n = Math.hypot(axis[0], axis[1], axis[2]) || 1; - const s = Math.sin(angle / 2) / n; - return [Math.cos(angle / 2), axis[0] * s, axis[1] * s, axis[2] * s]; -} - -/** Row-major 3x3 rotation matrix R with v_lab = R v_crystal. */ -export function quatToMatrix(q: Quat): number[] { - const [w, x, y, z] = q; - return [ - 1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w), - 2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w), - 2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y), - ]; -} - -export function matVec(R: number[], v: Vec3): Vec3 { - return [ - R[0] * v[0] + R[1] * v[1] + R[2] * v[2], - R[3] * v[0] + R[4] * v[1] + R[5] * v[2], - R[6] * v[0] + R[7] * v[1] + R[8] * v[2], - ]; -} - -/** R^T v: lab to crystal frame. */ -export function matTVec(R: number[], v: Vec3): Vec3 { - return [ - R[0] * v[0] + R[3] * v[1] + R[6] * v[2], - R[1] * v[0] + R[4] * v[1] + R[7] * v[2], - R[2] * v[0] + R[5] * v[1] + R[8] * v[2], - ]; -} - -/** Quaternion putting the crystal-frame unit direction d along +z (lab). */ -export function quatFromZoneAxis(d: Vec3, inPlaneDeg = 0): Quat { - const n = Math.hypot(d[0], d[1], d[2]) || 1; - const v: Vec3 = [d[0] / n, d[1] / n, d[2] / n]; - const axis: Vec3 = [v[1], -v[0], 0]; // v x z - const sinT = Math.hypot(axis[0], axis[1]); - const angle = Math.atan2(sinT, v[2]); - const qTilt = sinT < 1e-12 ? quatFromAxisAngle([1, 0, 0], v[2] > 0 ? 0 : Math.PI) : quatFromAxisAngle(axis, angle); - const qSpin = quatFromAxisAngle([0, 0, 1], (inPlaneDeg * Math.PI) / 180); - return qnormalize(qmult(qSpin, qTilt)); -} - -export function decodeF32(b64: string): Float32Array { - if (!b64) return new Float32Array(0); - const bin = atob(b64); - const bytes = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); - return new Float32Array(bytes.buffer); -} - -/** Smallest integer direction indices [uvw] with u a + v b + w c along d (crystal Cartesian), or null. */ -export function directionIndices(cell: number[][], d: Vec3, maxMult = 8): [number, number, number] | null { - // fractional coordinates of d: solve d = cell^T uvw -> uvw = inv(cell^T) d - const m = invert3(transpose3(cell)); - if (!m) return null; - const f = matVec(m, d); - const fm = Math.max(Math.abs(f[0]), Math.abs(f[1]), Math.abs(f[2])) || 1; - const v = f.map((x) => x / fm); - for (let mult = 1; mult <= maxMult; mult++) { - const w = v.map((x) => x * mult); - if (w.every((x) => Math.abs(x - Math.round(x)) < 0.02)) { - const ints = w.map((x) => Math.round(x)) as [number, number, number]; - // accept only if the integer direction is within 0.3 degrees of d - const c = cell; - const v: Vec3 = [ - ints[0] * c[0][0] + ints[1] * c[1][0] + ints[2] * c[2][0], - ints[0] * c[0][1] + ints[1] * c[1][1] + ints[2] * c[2][1], - ints[0] * c[0][2] + ints[1] * c[1][2] + ints[2] * c[2][2], - ]; - const cosang = (v[0] * d[0] + v[1] * d[1] + v[2] * d[2]) / ((Math.hypot(...v) * Math.hypot(...d)) || 1); - if (Math.abs(cosang) < Math.cos((0.3 * Math.PI) / 180)) return null; - const g = gcd3(ints); - return ints.map((x) => x / g) as [number, number, number]; - } - } - return null; -} - -/** Miller-Bravais direction [u v t w] -> three-index [U V W] = [2u+v, u+2v, w]. */ -export function fourToThree(v: number[]): [number, number, number] { - const [u, vv, , w] = v; - return [2 * u + vv, u + 2 * vv, w]; -} - -/** Three-index direction [U V W] of a hexagonal cell -> smallest integer [u v t w]. */ -export function threeToFour(d: [number, number, number]): [number, number, number, number] { - const [U, V, W] = d; - // u = (2U - V)/3, v = (2V - U)/3, t = -(u + v): scale by 3 and reduce - const ints = [2 * U - V, 2 * V - U, -(U + V), 3 * W]; - const g = Math.max(1, ints.reduce((a, b) => gcd(a, b), 0)); - return ints.map((x) => x / g) as [number, number, number, number]; -} - -/** Parse "1 1 0", "110", "1,-1,0", "[1-10]" or a 4-index "0001" / "1 0 -1 0"; 4 indices are Miller-Bravais. */ -export function parseDirection(text: string): [number, number, number] | null { - const t = text.trim().replace(/[\[\]()]/g, ""); - let parts: string[]; - if (/[\s,]/.test(t)) parts = t.split(/[\s,]+/).filter(Boolean); - else parts = t.match(/-?\d/g) || []; - if (parts.length !== 3 && parts.length !== 4) return null; - const v = parts.map(Number); - if (v.some((x) => !isFinite(x)) || v.every((x) => x === 0)) return null; - if (v.length === 4) { - if (Math.abs(v[0] + v[1] + v[2]) > 1e-9) return null; // u + v + t must vanish - return fourToThree(v); - } - return v as [number, number, number]; -} - -function gcd(a: number, b: number): number { - a = Math.abs(a); b = Math.abs(b); - while (b) [a, b] = [b, a % b]; - return a; -} -function gcd3(v: [number, number, number]): number { - return Math.max(1, gcd(gcd(v[0], v[1]), v[2])); -} - -export function transpose3(a: number[][]): number[] { - return [a[0][0], a[1][0], a[2][0], a[0][1], a[1][1], a[2][1], a[0][2], a[1][2], a[2][2]]; -} - -export function invert3(m: number[]): number[] | null { - const [a, b, c, d, e, f, g, h, i] = m; - const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g; - const det = a * A + b * B + c * C; - if (Math.abs(det) < 1e-14) return null; - const inv = [ - A, -(b * i - c * h), b * f - c * e, - B, a * i - c * g, -(a * f - c * d), - C, -(a * h - b * g), a * e - b * d, - ]; - return inv.map((x) => x / det); -} - -/** - * Eigendecomposition of a complex Hermitian matrix by cyclic Jacobi - * rotations. re/im are row-major n x n; returns eigenvalues and the - * eigenvectors as columns (vecRe[i*n + j] = component i of eigenvector j). - * O(n^3) per sweep, a handful of sweeps: milliseconds for n ~ 100. - */ -export function eighComplex(re: Float64Array, im: Float64Array, n: number) { - const a = Float64Array.from(re); - const b = Float64Array.from(im); - const vr = new Float64Array(n * n); - const vi = new Float64Array(n * n); - for (let i = 0; i < n; i++) vr[i * n + i] = 1; - const idx = (i: number, j: number) => i * n + j; - for (let sweep = 0; sweep < 60; sweep++) { - let off = 0; - for (let p = 0; p < n; p++) for (let q = p + 1; q < n; q++) off += a[idx(p, q)] ** 2 + b[idx(p, q)] ** 2; - if (off < 1e-24) break; - for (let p = 0; p < n - 1; p++) { - for (let q = p + 1; q < n; q++) { - const apq_r = a[idx(p, q)], apq_i = b[idx(p, q)]; - const mag = Math.hypot(apq_r, apq_i); - if (mag < 1e-300) continue; - // phase rotation of column q (and its row) makes a_pq real positive - const cph = apq_r / mag, sph = apq_i / mag; // e^{i phi} = (cph, sph) - // column q *= e^{-i phi}; row q *= e^{i phi} - for (let k = 0; k < n; k++) { - const kr = a[idx(k, q)], ki = b[idx(k, q)]; - a[idx(k, q)] = kr * cph + ki * sph; - b[idx(k, q)] = ki * cph - kr * sph; - } - for (let k = 0; k < n; k++) { - const kr = a[idx(q, k)], ki = b[idx(q, k)]; - a[idx(q, k)] = kr * cph - ki * sph; - b[idx(q, k)] = ki * cph + kr * sph; - } - for (let k = 0; k < n; k++) { - const kr = vr[idx(k, q)], ki = vi[idx(k, q)]; - vr[idx(k, q)] = kr * cph + ki * sph; - vi[idx(k, q)] = ki * cph - kr * sph; - } - // real Jacobi rotation in the (p, q) plane - const app = a[idx(p, p)], aqq = a[idx(q, q)], apq = a[idx(p, q)]; - const theta = 0.5 * Math.atan2(2 * apq, aqq - app); - const c = Math.cos(theta), s = Math.sin(theta); - for (let k = 0; k < n; k++) { - // columns - const pr = a[idx(k, p)], pi = b[idx(k, p)], qr = a[idx(k, q)], qi = b[idx(k, q)]; - a[idx(k, p)] = c * pr - s * qr; b[idx(k, p)] = c * pi - s * qi; - a[idx(k, q)] = s * pr + c * qr; b[idx(k, q)] = s * pi + c * qi; - } - for (let k = 0; k < n; k++) { - // rows - const pr = a[idx(p, k)], pi = b[idx(p, k)], qr = a[idx(q, k)], qi = b[idx(q, k)]; - a[idx(p, k)] = c * pr - s * qr; b[idx(p, k)] = c * pi - s * qi; - a[idx(q, k)] = s * pr + c * qr; b[idx(q, k)] = s * pi + c * qi; - } - for (let k = 0; k < n; k++) { - const pr = vr[idx(k, p)], pi = vi[idx(k, p)], qr = vr[idx(k, q)], qi = vi[idx(k, q)]; - vr[idx(k, p)] = c * pr - s * qr; vi[idx(k, p)] = c * pi - s * qi; - vr[idx(k, q)] = s * pr + c * qr; vi[idx(k, q)] = s * pi + c * qi; - } - a[idx(p, q)] = 0; b[idx(p, q)] = 0; a[idx(q, p)] = 0; b[idx(q, p)] = 0; - } - } - } - const vals = new Float64Array(n); - for (let i = 0; i < n; i++) vals[i] = a[idx(i, i)]; - return { vals, vecRe: vr, vecIm: vi }; -} diff --git a/widget/js/diffsim/pattern.ts b/widget/js/diffsim/pattern.ts deleted file mode 100644 index d70c94eee..000000000 --- a/widget/js/diffsim/pattern.ts +++ /dev/null @@ -1,445 +0,0 @@ -/** - * Pattern renderers for the simulator: nanobeam markers or pixels, CBED - * disks, Kossel lines and the Kossel reference lookup. Every renderer works - * in canvas coordinates with q_x to the right and q_y up. - */ - -import { COLORMAPS, applyColormap } from "../colormaps"; -import type { Reflection } from "./physics"; -import type { KosselLine } from "./physics"; - -const MAX_LABELS = 20; // strongest reflections labelled - -export interface Frame { - size: number; // canvas CSS px (square) - qMax: number; // 1/A at the edge (nanobeam / CBED) or rad (Kossel) - viewX: number; // +1: seen from the gun side (lab x to the right); -1: from the detector side (mirrored) -} - -export function scaleOf(f: Frame): number { - return (0.5 * f.size * 0.92) / f.qMax; // px per unit -} - -/** Lab (x, y) in pattern units to canvas px. */ -export function toPx(f: Frame, x: number, y: number): [number, number] { - const s = scaleOf(f); - return [f.size / 2 + f.viewX * x * s, f.size / 2 - y * s]; -} - -/** Canvas px to lab (x, y) in pattern units. */ -export function fromPx(f: Frame, px: number, py: number): [number, number] { - const s = scaleOf(f); - return [(f.viewX * (px - f.size / 2)) / s, -(py - f.size / 2) / s]; -} - -export function setupCanvas(canvas: HTMLCanvasElement, size: number): CanvasRenderingContext2D | null { - const dpr = window.devicePixelRatio || 1; - if (canvas.width !== size * dpr || canvas.height !== size * dpr) { - canvas.width = size * dpr; - canvas.height = size * dpr; - } - const ctx = canvas.getContext("2d"); - if (!ctx) return null; - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - return ctx; -} - -/** - * Nanobeam pattern as markers. Marker AREA scales as intensity^markerPower - * (0.5 = sqrt intensity, the default); markerSize is the radius (px) of - * the strongest beam, capped so the densest net does not merge. - */ -export function drawMarkers( - ctx: CanvasRenderingContext2D, f: Frame, beams: Reflection[], inten: Float64Array, dark: boolean, - labels: boolean, kinematic: boolean, markerPower = 0.5, markerSize = 20, -) { - const s = scaleOf(f); - ctx.fillStyle = dark ? "#000" : "#fff"; - ctx.fillRect(0, 0, f.size, f.size); - // normalise to the strongest diffracted beam: the direct beam saturates and the weak spots stay visible - let iMax = 0; - for (let i = 0; i < beams.length; i++) if (beams[i].index >= 0) iMax = Math.max(iMax, inten[i]); - if (iMax <= 0) iMax = 1; - // marker radius capped so neighbouring spots of the densest net do not merge - let gMin = Infinity; - for (const b of beams) if (b.index >= 0 && b.gLen > 1e-6) gMin = Math.min(gMin, b.gLen); - const rMax = Math.min(markerSize * (f.size / 420), isFinite(gMin) ? 0.42 * gMin * s : Infinity); - const fg = dark ? "#fff" : "#000"; - const strong: { x: number; y: number; r: number; hkl: number[]; rel: number }[] = []; - for (let i = 0; i < beams.length; i++) { - const b = beams[i]; - const rel = Math.min(1, inten[i] / iMax); - const [x, y] = toPx(f, b.g[0], b.g[1]); - if (b.index < 0 && kinematic) { - ctx.strokeStyle = fg; ctx.lineWidth = 1.5; - ctx.beginPath(); ctx.arc(x, y, rMax * 0.9, 0, 2 * Math.PI); ctx.stroke(); - strong.push({ x, y, r: rMax * 0.9, hkl: b.hkl, rel: 2 }); - continue; - } - if (rel < 1e-6) continue; - const r = rMax * Math.pow(rel, 0.5 * markerPower); // area ~ I^markerPower - ctx.fillStyle = fg; - ctx.globalAlpha = 0.9; - ctx.beginPath(); ctx.arc(x, y, Math.max(r, 0.6), 0, 2 * Math.PI); ctx.fill(); - ctx.globalAlpha = 1; - if (rel > 0.08) strong.push({ x, y, r, hkl: b.hkl, rel: b.index < 0 ? 2 : rel }); - } - if (labels) { - ctx.font = `${Math.max(9, Math.round(f.size / 38))}px sans-serif`; - ctx.textAlign = "center"; ctx.textBaseline = "bottom"; - ctx.fillStyle = dark ? "#ffd54f" : "#c62828"; - for (const p of strong.sort((a, b) => b.rel - a.rel).slice(0, MAX_LABELS)) { - ctx.fillText(hklText(p.hkl), p.x, p.y - p.r - 2); - } - } - // scale bar of 1 1/A - drawScaleBar(ctx, f, s, "Å⁻¹", dark, 1); -} - -/** - * Nanobeam pattern as disks of the physical convergence angle: filled - * circles of radius k0 sin(alpha) at every beam, brightness (I/Imax)^power. - * Overlapping disks add; the direct beam is drawn like the others. - */ -export function drawDisks( - ctx: CanvasRenderingContext2D, f: Frame, beams: Reflection[], inten: Float64Array, dark: boolean, - labels: boolean, radiusQ: number, power = 0.5, vmin = 0, vmax = 1, -) { - const s = scaleOf(f); - // dark theme: bright disks on black, adding where they overlap; light - // theme: the inverted greyscale, dark disks on white, multiplying - ctx.fillStyle = dark ? "#000" : "#fff"; - ctx.fillRect(0, 0, f.size, f.size); - // normalise to the strongest diffracted beam (the direct beam saturates) - let iMax = 0; - for (let i = 0; i < beams.length; i++) if (beams[i].index >= 0) iMax = Math.max(iMax, inten[i]); - if (iMax <= 0) iMax = 1; - const r = Math.max(1.2, radiusQ * s); - const strong: { x: number; y: number; hkl: number[]; rel: number }[] = []; - ctx.globalCompositeOperation = dark ? "lighter" : "multiply"; - for (let i = 0; i < beams.length; i++) { - const rel = Math.min(1, inten[i] / iMax); - if (rel < 1e-6) continue; - const [x, y] = toPx(f, beams[i].g[0], beams[i].g[1]); - if (x < -r || y < -r || x > f.size + r || y > f.size + r) continue; - const v = Math.min(1, Math.max(0, (Math.pow(rel, power) - vmin) / Math.max(vmax - vmin, 1e-6))); // contrast window on I^power - const c = Math.round(255 * (dark ? v : 1 - v)); - ctx.fillStyle = `rgb(${c},${c},${c})`; - ctx.beginPath(); ctx.arc(x, y, r, 0, 2 * Math.PI); ctx.fill(); - if (rel > 0.08) strong.push({ x, y, hkl: beams[i].hkl, rel: beams[i].index < 0 ? 2 : rel }); - } - ctx.globalCompositeOperation = "source-over"; - if (labels) { - ctx.font = `${Math.max(9, Math.round(f.size / 38))}px sans-serif`; - ctx.textAlign = "center"; ctx.textBaseline = "bottom"; - ctx.fillStyle = dark ? "#ffd54f" : "#c62828"; - for (const p of strong.sort((a, b) => b.rel - a.rel).slice(0, MAX_LABELS)) ctx.fillText(hklText(p.hkl), p.x, p.y - r - 2); - } - drawScaleBar(ctx, f, s, "Å⁻¹", dark, 1); -} - -export function hklText(hkl: number[]): string { - return hkl.map((h) => (h < 0 ? `${-h}̅` : `${h}`)).join(""); -} - -/** - * Side view of the Ewald sphere, filling a W x H canvas: the lab x-z - * plane (x mirrored by viewX like the pattern, +z = upstream at the top), - * the reciprocal lattice points with |g_y| below a slab width as dots, the - * sphere z = k0 - sqrt(k0^2 - x^2) through the origin, and the excited - * reflections (|s| < sgMax) highlighted. The z axis is stretched so the - * sphere's sagitta over the pattern range fills the inset. - */ -export function drawEwaldPanel( - ctx: CanvasRenderingContext2D, W: number, H: number, refl: Reflection[], k0: number, qMax: number, sgMax: number, - viewX: number, dark: boolean, precDeg = 0, -) { - const size = Math.max(W, 2 * H); // reference for the font size - const x0 = 0, y0 = 0; - const fg = dark ? "#d8d8d8" : "#222"; - ctx.save(); - ctx.fillStyle = dark ? "#141414" : "#fafafa"; - ctx.fillRect(0, 0, W, H); - ctx.beginPath(); ctx.rect(x0, y0, W, H); ctx.clip(); - const sinP = Math.sin((precDeg * Math.PI) / 180); - const sag = (qMax * qMax) / (2 * k0) + qMax * sinP; // sphere height over the pattern range, incl. the precession tilt - const zHalf = Math.max(sag * 1.15, 4 * sgMax); - const sx = (W * 0.46) / qMax; // px per 1/A along x - const sz = (H * 0.4) / zHalf; // px per 1/A along z (stretched) - const cx = x0 + W / 2, cy = y0 + H * 0.64; // origin: lower middle - const X = (x: number) => cx + viewX * x * sx; - const Z = (z: number) => cy - z * sz; - // sphere for an incident beam with in-plane wavevector tx: centre (-tx, kz), through the origin - const sphereZ = (x: number, tx: number) => { - const kz = Math.sqrt(Math.max(k0 * k0 - tx * tx, 0)); - return kz - Math.sqrt(Math.max(k0 * k0 - (x + tx) * (x + tx), 0)); - }; - const xs: number[] = []; - for (let i = 0; i <= 60; i++) xs.push(-qMax * 1.08 + (2.16 * qMax * i) / 60); - const orange = dark ? "#e0b060" : "#c07a00"; - if (sinP > 0) { - // precession: the sphere sweeps the band between its two extreme tilts in this plane - const tA = k0 * sinP, tB = -k0 * sinP; - ctx.fillStyle = dark ? "rgba(224,176,96,0.22)" : "rgba(192,122,0,0.18)"; - ctx.beginPath(); - xs.forEach((x, i) => { const z = sphereZ(x, tA); if (i === 0) ctx.moveTo(X(x), Z(z)); else ctx.lineTo(X(x), Z(z)); }); - for (let i = xs.length - 1; i >= 0; i--) ctx.lineTo(X(xs[i]), Z(sphereZ(xs[i], tB))); - ctx.closePath(); ctx.fill(); - ctx.strokeStyle = orange; ctx.lineWidth = 0.9; - for (const t of [tA, tB]) { - ctx.beginPath(); - xs.forEach((x, i) => { const z = sphereZ(x, t); if (i === 0) ctx.moveTo(X(x), Z(z)); else ctx.lineTo(X(x), Z(z)); }); - ctx.stroke(); - } - // the beam cone: the two extreme incident directions, from the top to the origin - ctx.strokeStyle = dark ? "#9ad" : "#37c"; ctx.lineWidth = 1; - const zTop = (cy - y0 - 4) / sz; - for (const t of [tA, tB]) { - const kz = Math.sqrt(Math.max(k0 * k0 - t * t, 0)); - ctx.beginPath(); ctx.moveTo(X((-t / kz) * zTop), Z(zTop)); ctx.lineTo(cx, cy); ctx.stroke(); - } - } - // untilted beam arrow (travels -z: from the top toward the origin) and sphere - ctx.strokeStyle = dark ? "#9ad" : "#37c"; ctx.fillStyle = ctx.strokeStyle; ctx.lineWidth = 1.3; - ctx.beginPath(); ctx.moveTo(cx, y0 + 4); ctx.lineTo(cx, cy - 3); ctx.stroke(); - ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx - 3.5, cy - 7); ctx.lineTo(cx + 3.5, cy - 7); ctx.closePath(); ctx.fill(); - ctx.strokeStyle = orange; ctx.lineWidth = 1.4; - ctx.beginPath(); - xs.forEach((x, i) => { const z = sphereZ(x, 0); if (i === 0) ctx.moveTo(X(x), Z(z)); else ctx.lineTo(X(x), Z(z)); }); - ctx.stroke(); - // reciprocal lattice points in a slab about the x-z plane - const slab = 0.12 * qMax; - for (const r of refl) { - if (Math.abs(r.g[1]) > slab || Math.abs(r.g[0]) > qMax * 1.08 || Math.abs(r.g[2]) > zHalf * 1.3) continue; - const excited = Math.abs(r.s) < sgMax + Math.abs(r.g[0]) * sinP; // within the swept band - ctx.fillStyle = excited ? "#00cc66" : (dark ? "#8a8a8a" : "#777"); - ctx.beginPath(); ctx.arc(X(r.g[0]), Z(r.g[2]), excited ? 2.6 : 1.6, 0, 2 * Math.PI); ctx.fill(); - } - ctx.fillStyle = dark ? "#eee" : "#111"; - ctx.beginPath(); ctx.arc(cx, cy, 2.4, 0, 2 * Math.PI); ctx.fill(); - // labels on an opaque strip so the cone lines do not run through them - const fontPx = Math.max(10, Math.round(size / 36)); - ctx.font = `${fontPx}px sans-serif`; ctx.textAlign = "left"; ctx.textBaseline = "top"; - const line1 = `Ewald sphere, side view, z ×${Math.round(sz / sx)}`; - const line2 = sinP > 0 ? `precession ±${precDeg.toFixed(2)}°` : ""; - const tw = Math.max(ctx.measureText(line1).width, line2 ? ctx.measureText(line2).width : 0); - ctx.fillStyle = dark ? "rgba(20,20,20,0.9)" : "rgba(250,250,250,0.92)"; - ctx.fillRect(x0 + 1, y0 + 1, tw + 9, (line2 ? 2.5 : 1.2) * fontPx + 6); - ctx.fillStyle = fg; - ctx.fillText(line1, x0 + 5, y0 + 4); - if (line2) ctx.fillText(line2, x0 + 5, y0 + 5 + 1.4 * fontPx); - ctx.restore(); -} - -function drawScaleBar(ctx: CanvasRenderingContext2D, f: Frame, s: number, unit: string, dark: boolean, value: number) { - let v = value; - while (v * s > 0.4 * f.size) v /= 2; - while (v * s < 0.12 * f.size) v *= 2; - const L = v * s; - const x0 = f.size - L - 14, y0 = f.size - 14; - ctx.strokeStyle = dark ? "#eee" : "#222"; - ctx.fillStyle = dark ? "#eee" : "#222"; - ctx.lineWidth = 3; - ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x0 + L, y0); ctx.stroke(); - ctx.font = "11px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "bottom"; - const label = v >= 1 ? `${+v.toFixed(2)} ${unit}` : `${+(v * 1000).toFixed(0)} m${unit}`; - ctx.fillText(label, x0 + L / 2, y0 - 4); -} - -/** Splat beams as Gaussian spots into a float image (canvas order). */ -export function nanobeamImage(f: Frame, beams: Reflection[], inten: Float64Array, spotPx: number): Float32Array { - const n = f.size; - const img = new Float32Array(n * n); - const sig = spotPx; - const w = Math.ceil(3.5 * sig); - for (let i = 0; i < beams.length; i++) { - if (inten[i] <= 0) continue; - const [x, y] = toPx(f, beams[i].g[0], beams[i].g[1]); - const amp = inten[i] / (2 * Math.PI * sig * sig); - const x0 = Math.max(0, Math.floor(x - w)), x1 = Math.min(n - 1, Math.ceil(x + w)); - const y0 = Math.max(0, Math.floor(y - w)), y1 = Math.min(n - 1, Math.ceil(y + w)); - for (let py = y0; py <= y1; py++) { - const dy = py + 0.5 - y; - for (let px = x0; px <= x1; px++) { - const dx = px + 0.5 - x; - img[py * n + px] += amp * Math.exp(-(dx * dx + dy * dy) / (2 * sig * sig)); - } - } - } - return img; -} - -export interface TiltGrid { - n: number; // grid points per side - h: number; // spacing, 1/A - R: number; // disk radius, 1/A - tilts: [number, number][]; // grid points within R + h (row-major over the n x n grid, NaN-free) - index: Int32Array; // n*n -> position in tilts or -1 -} - -export function tiltGrid(R: number, n: number): TiltGrid { - const h = (2 * R) / (n - 1); - const tilts: [number, number][] = []; - const index = new Int32Array(n * n).fill(-1); - for (let j = 0; j < n; j++) { - for (let i = 0; i < n; i++) { - const tx = -R + i * h, ty = -R + j * h; - if (Math.hypot(tx, ty) <= R + 1.01 * h) { - index[j * n + i] = tilts.length; - tilts.push([tx, ty]); - } - } - } - return { n, h, R, tilts, index }; -} - -/** - * CBED image: for every beam a disk of radius R centered on g, with the - * intensity at each pixel interpolated bilinearly from the tilt grid. - * intensities[t][b] is the intensity of beam b at tilt t. - */ -export function cbedImage(f: Frame, beams: Reflection[], grid: TiltGrid, intensities: Float64Array[]): Float32Array { - const n = f.size; - const img = new Float32Array(n * n); - const s = scaleOf(f); - const Rpx = grid.R * s; - const lookup = (b: number, tx: number, ty: number): number => { - const fx = (tx + grid.R) / grid.h, fy = (ty + grid.R) / grid.h; - const i0 = Math.min(Math.max(Math.floor(fx), 0), grid.n - 2); - const j0 = Math.min(Math.max(Math.floor(fy), 0), grid.n - 2); - const wx = Math.min(Math.max(fx - i0, 0), 1), wy = Math.min(Math.max(fy - j0, 0), 1); - const v = (i: number, j: number) => { - const k = grid.index[j * grid.n + i]; - return k < 0 ? 0 : intensities[k][b]; - }; - return v(i0, j0) * (1 - wx) * (1 - wy) + v(i0 + 1, j0) * wx * (1 - wy) + v(i0, j0 + 1) * (1 - wx) * wy + v(i0 + 1, j0 + 1) * wx * wy; - }; - for (let b = 0; b < beams.length; b++) { - const [cx, cy] = toPx(f, beams[b].g[0], beams[b].g[1]); - if (cx < -Rpx || cy < -Rpx || cx > n + Rpx || cy > n + Rpx) continue; - let anyInt = 0; - for (const arr of intensities) if (arr[b] > 1e-7) { anyInt = 1; break; } - if (!anyInt) continue; - const x0 = Math.max(0, Math.floor(cx - Rpx - 1)), x1 = Math.min(n - 1, Math.ceil(cx + Rpx + 1)); - const y0 = Math.max(0, Math.floor(cy - Rpx - 1)), y1 = Math.min(n - 1, Math.ceil(cy + Rpx + 1)); - for (let py = y0; py <= y1; py++) { - const dy = (py + 0.5 - cy); - for (let px = x0; px <= x1; px++) { - const dx = (px + 0.5 - cx); - const rr = Math.hypot(dx, dy); - if (rr > Rpx + 0.5) continue; - const edge = Math.min(1, Rpx + 0.5 - rr); // anti-aliased rim - const tx = (f.viewX * dx) / s, ty = -dy / s; - img[py * n + px] += edge * lookup(b, tx, ty); - } - } - } - return img; -} - -/** Colormapped float image onto the canvas, with a percentile contrast window. */ -export function drawImage( - ctx: CanvasRenderingContext2D, f: Frame, img: Float32Array, cmap: string, vmin: number, vmax: number, - dark: boolean, unit: string, barValue: number, -) { - const n = f.size; - const lut = COLORMAPS[cmap] || COLORMAPS[Object.keys(COLORMAPS)[0]]; - const rgba = new Uint8ClampedArray(n * n * 4); - const clean = new Float32Array(n * n); - for (let i = 0; i < n * n; i++) clean[i] = isFinite(img[i]) ? img[i] : vmin; - applyColormap(clean, rgba, lut, vmin, vmax); - for (let i = 0; i < n * n; i++) if (!isFinite(img[i])) rgba[4 * i + 3] = 0; - const off = document.createElement("canvas"); - off.width = n; off.height = n; - const octx = off.getContext("2d"); - if (!octx) return; - octx.putImageData(new ImageData(rgba, n, n), 0, 0); - ctx.fillStyle = dark ? "#000" : "#fff"; - ctx.fillRect(0, 0, n, n); - ctx.imageSmoothingEnabled = false; - ctx.drawImage(off, 0, 0, n, n); - drawScaleBar(ctx, f, scaleOf(f), unit, dark, barValue); -} - -/** Bright field Kossel pattern as vector lines (deficient lines dark). */ -export function drawKosselLines( - ctx: CanvasRenderingContext2D, f: Frame, lines: KosselLine[], dark: boolean, labels: boolean, minStrength: number, -) { - const n = f.size, s = scaleOf(f); - const cx = n / 2, cy = n / 2, Rpx = f.qMax * s; - ctx.fillStyle = dark ? "#000" : "#fff"; - ctx.fillRect(0, 0, n, n); - ctx.save(); - ctx.beginPath(); ctx.arc(cx, cy, Rpx, 0, 2 * Math.PI); ctx.clip(); - ctx.fillStyle = dark ? "#bdbdbd" : "#e0e0e0"; - ctx.fill(); - const sorted = [...lines].filter((l) => l.strength >= minStrength).sort((a, b) => a.strength - b.strength); - const L = 2 * f.qMax; - for (const l of sorted) { - const [nx, ny] = l.normal; - // line p . n = distance, direction t = (-ny, nx); drawn between two lab points - const [x0, y0] = toPx(f, nx * l.distance - ny * L, ny * l.distance + nx * L); - const [x1, y1] = toPx(f, nx * l.distance + ny * L, ny * l.distance - nx * L); - ctx.strokeStyle = dark ? `rgba(20,20,20,${Math.min(1, 0.1 + 0.9 * l.strength)})` : `rgba(30,30,30,${Math.min(1, 0.1 + 0.9 * l.strength)})`; - ctx.lineWidth = Math.max(0.6, l.width * s); - ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); - } - if (labels) { - ctx.font = `${Math.max(9, Math.round(n / 40))}px sans-serif`; - ctx.fillStyle = dark ? "#ffd54f" : "#c62828"; - ctx.textAlign = "center"; ctx.textBaseline = "middle"; - const strong = sorted.filter((l) => l.strength > 0.3 && Math.abs(l.distance) < f.qMax * 0.95).slice(-24); - for (const l of strong) { - const [nx, ny] = l.normal; - // label near the rim along the line - const t = Math.sqrt(Math.max(f.qMax * f.qMax * 0.8 - l.distance * l.distance, 0)); - const [x, y] = toPx(f, nx * l.distance - ny * t, ny * l.distance + nx * t); - ctx.fillText(hklText(l.hkl), x, y); - } - } - ctx.restore(); - drawScaleBar(ctx, f, s, "rad", dark, 0.01); -} - -/** - * Kikuchi line pairs overlaid on a nanobeam pattern (deficient blue, excess - * red). Opacity and width grow with the cube of the strength relative to the - * strongest line of the cell, so only the few major lines read as heavy and - * the rest fade into the background; at most the MAX_KIKUCHI strongest pairs - * are drawn, weakest first. - */ -const MAX_KIKUCHI = 40; -export function drawKikuchiOverlay(ctx: CanvasRenderingContext2D, f: Frame, lines: KosselLine[], k0: number, dark: boolean) { - const L = 3 * f.qMax; - const shown = lines.filter((l) => l.strength > 0).sort((a, b) => b.strength - a.strength).slice(0, MAX_KIKUCHI).reverse(); - const sMax = shown.length ? shown[shown.length - 1].strength : 1; - for (const l of shown) { - const d = l.distance * k0; // 1/A - const [nx, ny] = l.normal; - const r3 = (l.strength / sMax) ** 3; - const alpha = (0.05 + 0.5 * r3).toFixed(3); - const width = 0.5 + 1.3 * r3; - const draw = (dist: number, color: string) => { - const [x0, y0] = toPx(f, nx * dist - ny * L, ny * dist + nx * L); - const [x1, y1] = toPx(f, nx * dist + ny * L, ny * dist - nx * L); - ctx.strokeStyle = color; - ctx.lineWidth = width; - ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); - }; - draw(d, dark ? `rgba(120,170,255,${alpha})` : `rgba(30,90,200,${alpha})`); - draw(d + l.gxy, dark ? `rgba(255,140,120,${alpha})` : `rgba(200,60,40,${alpha})`); - } -} - -/** 256-bin histogram of the finite values. */ -export function histogramBins(img: Float32Array, lo: number, hi: number): number[] { - const bins = new Array(256).fill(0); - const range = hi > lo ? hi - lo : 1; - for (let i = 0; i < img.length; i++) { - const v = img[i]; - if (!isFinite(v)) continue; - const b = Math.min(255, Math.max(0, Math.floor(((v - lo) / range) * 255))); - bins[b]++; - } - return bins; -} diff --git a/widget/js/diffsim/physics.ts b/widget/js/diffsim/physics.ts deleted file mode 100644 index 2b64fb0d5..000000000 --- a/widget/js/diffsim/physics.ts +++ /dev/null @@ -1,537 +0,0 @@ -/** - * Diffraction physics for the simulator, all in the browser: reciprocal - * lattice geometry, kinematical intensities, the Bloch wave calculation - * (structure matrix, eigendecomposition, thickness dependence, first-order - * absorption), CBED tilt sampling, Kossel line geometry and the reference - * pattern lookup. - */ - -import { Quat, Vec3, decodeF32, eighComplex, matVec, quatToMatrix } from "./math"; - -export interface CrystalData { - name: string; - spacegroup: string; - pointgroup: string; - cell: number[][]; - recip: number[][]; - positions_frac: number[][]; - numbers: number[]; - symbols: string[]; - colors: number[][]; - radii: number[]; - hkl: number[][]; - g: Float32Array; // (N, 3) crystal frame - F2: Float32Array; - U_re: Float32Array; - U_im: Float32Array; - couplingRe: Map; - couplingIm: Map; - u0_imag: number; - absorptive: boolean; - energy_ev: number; - wavelength: number; - k_max: number; - hexagonal: boolean; -} - -/** Relativistic electron wavelength (A) for a beam energy in eV. */ -export function electronWavelength(energyEv: number): number { - return 12.2643 / Math.sqrt(energyEv * (1 + 0.97845e-6 * energyEv)); -} - -/** Relativistic mass factor 1 + E / (m0 c^2). */ -export function relativisticGamma(energyEv: number): number { - return 1 + energyEv / 510998.95; -} - -/** - * Parse the crystal data. With energyEv the stored couplings (computed at - * the data's energy) are rescaled by the ratio of relativistic mass factors - * and the wavelength is recomputed: exact for the elastic potential, an - * approximation for the absorptive part. Kinematical |F|^2 is unchanged. - */ -export function parseCrystal(json: string, energyEv?: number): CrystalData | null { - if (!json || json === "{}") return null; - const o = JSON.parse(json); - const scaleU = energyEv && Math.abs(energyEv - o.energy_ev) > 1 ? relativisticGamma(energyEv) / relativisticGamma(o.energy_ev) : 1; - if (scaleU !== 1) { - o.energy_ev = energyEv; - o.wavelength = electronWavelength(energyEv!); - o.u0_imag = o.u0_imag * scaleU; - } - // reflection indices packed as int16 triplets; g rebuilt from the reciprocal cell - const bin = atob(o.hkl_i16 || ""); - const bytes = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); - const hi16 = new Int16Array(bytes.buffer); - const n = hi16.length / 3; - const hkl: number[][] = new Array(n); - const g = new Float32Array(3 * n); - const B: number[][] = o.recip; - for (let i = 0; i < n; i++) { - const h = hi16[3 * i], k = hi16[3 * i + 1], l = hi16[3 * i + 2]; - hkl[i] = [h, k, l]; - g[3 * i] = h * B[0][0] + k * B[1][0] + l * B[2][0]; - g[3 * i + 1] = h * B[0][1] + k * B[1][1] + l * B[2][1]; - g[3 * i + 2] = h * B[0][2] + k * B[1][2] + l * B[2][2]; - } - const U_re = decodeF32(o.U_re), U_im = decodeF32(o.U_im); - if (scaleU !== 1) for (let i = 0; i < U_re.length; i++) { U_re[i] *= scaleU; U_im[i] *= scaleU; } - const couplingRe = new Map(); - const couplingIm = new Map(); - for (let i = 0; i < n; i++) { - const key = `${hkl[i][0]},${hkl[i][1]},${hkl[i][2]}`; - couplingRe.set(key, U_re[i]); - couplingIm.set(key, U_im[i]); - } - return { - name: o.name, spacegroup: o.spacegroup, pointgroup: o.pointgroup, - cell: o.cell, recip: o.recip, positions_frac: o.positions_frac, numbers: o.numbers, - symbols: o.symbols, colors: o.colors, radii: o.radii, hkl, - g, F2: decodeF32(o.F2), U_re, U_im, - couplingRe, couplingIm, u0_imag: o.u0_imag, absorptive: o.absorptive, - energy_ev: o.energy_ev, wavelength: o.wavelength, k_max: o.k_max, hexagonal: o.hexagonal, - }; -} - -export interface Reflection { - index: number; - hkl: number[]; - g: Vec3; // lab frame - gLen: number; - s: number; // excitation error at zero tilt -} - -/** Lab-frame reflections within kMax, with excitation errors for the beam along -z. */ -export function labReflections(c: CrystalData, q: Quat, kMax: number): Reflection[] { - const R = quatToMatrix(q); - const lam = c.wavelength; - const out: Reflection[] = []; - const n = c.hkl.length; - for (let i = 0; i < n; i++) { - const gc: Vec3 = [c.g[3 * i], c.g[3 * i + 1], c.g[3 * i + 2]]; - const gLen = Math.hypot(gc[0], gc[1], gc[2]); - if (gLen > kMax) continue; - const g = matVec(R, gc); - const g2 = gLen * gLen; - const s = (2 * g[2] - lam * g2) / (2 - 2 * lam * g[2]); - out.push({ index: i, hkl: c.hkl[i], g, gLen, s }); - } - return out; -} - -/** Kinematical intensities |F|^2 exp(-s^2 / 2 sigma^2). */ -export function kinematicalIntensities(c: CrystalData, refl: Reflection[], sigma: number): Float64Array { - const out = new Float64Array(refl.length); - for (let i = 0; i < refl.length; i++) { - const r = refl[i]; - out[i] = c.F2[r.index] * Math.exp(-(r.s * r.s) / (2 * sigma * sigma)); - } - return out; -} - -export interface BlochSolution { - beams: Reflection[]; // beam 0 is the direct beam (hkl 000) - n: number; - gammaRe: Float64Array; // 1/A - gammaIm: Float64Array; - vecRe: Float64Array; // columns = eigenvectors - vecIm: Float64Array; - psi0Re: Float64Array; // conj(C_0j) - psi0Im: Float64Array; -} - -const DIRECT: Reflection = { index: -1, hkl: [0, 0, 0], g: [0, 0, 0], gLen: 0, s: 0 }; - -function tiltedExcitation(r: Reflection, kz: number, tilt: [number, number]): number { - const g2 = r.gLen * r.gLen; - const num = 2 * kz * r.g[2] - 2 * (tilt[0] * r.g[0] + tilt[1] * r.g[1]) - g2; - return num / (2 * (kz - r.g[2])); -} - -/** |U_h| for an index difference, zero when the crystal carries no such factor. */ -function couplingMag(c: CrystalData, h: number, k: number, l: number): number { - const key = `${h},${k},${l}`; - return Math.hypot(c.couplingRe.get(key) ?? 0, c.couplingIm.get(key) ?? 0); -} - -const N_STRONG = 24; // candidates treated as the intermediate beams of a two-step path - -/** - * Rank candidates for the Bloch set, strongest first. - * - * Ranking by |U_g| alone drops exactly the reflections a dynamical - * calculation exists to show: silicon 002 and 222 have no structure factor - * of their own, so they score zero, yet they are the textbook example of a - * beam that fills by double diffraction and grows with thickness. A beam is - * worth keeping when it is strongly coupled to something that is itself - * strongly excited, so each candidate is scored by the largest coupling - * joining it either to the transmitted beam (|U_g|) or to one of the - * strongest candidates (|U_(g-h)|), divided by its excitation error. - */ -function rankCandidates(c: CrystalData, cand: { ref: Reflection; s: number }[]): { ref: Reflection; s: number }[] { - const direct = cand.map((x) => ({ - x, - v: Math.hypot(c.U_re[x.ref.index], c.U_im[x.ref.index]) / (Math.abs(x.s) + 1e-4), - })); - direct.sort((a, b) => b.v - a.v); - const strong = direct.slice(0, Math.min(N_STRONG, direct.length)).map((d) => d.x.ref.hkl); - const scored = cand.map((x) => { - let best = Math.hypot(c.U_re[x.ref.index], c.U_im[x.ref.index]); - for (const h of strong) { - const u = couplingMag(c, x.ref.hkl[0] - h[0], x.ref.hkl[1] - h[1], x.ref.hkl[2] - h[2]); - if (u > best) best = u; - } - return { x, v: best / (Math.abs(x.s) + 1e-4) }; - }); - scored.sort((a, b) => b.v - a.v); - return scored.map((sc) => sc.x); -} - -/** - * Beams entering the Bloch calculation: the direct beam plus every - * reflection within sgMax of the Ewald sphere at zero tilt (sgMax widened - * by the tilt range when a convergent beam is sampled), capped at maxBeams - * by the ranking of rankCandidates. - */ -export function selectBeams(c: CrystalData, q: Quat, kMax: number, sgMax: number, maxBeams: number, tiltRange = 0): Reflection[] { - const all = labReflections(c, q, kMax); - const widen = tiltRange * kMax; // max |delta s| = sin(alpha) |g| - let beams = all.filter((r) => Math.abs(r.s) < sgMax + widen); - if (beams.length > maxBeams) { - beams = rankCandidates(c, beams.map((r) => ({ ref: r, s: r.s }))) - .slice(0, maxBeams) - .map((x) => x.ref); - } - return [DIRECT, ...beams]; -} - -/** - * Bloch wave eigenproblem for a fixed beam list and an incident beam with - * in-plane wavevector tilt (1/A). The structure matrix has 2 k0 s_g on the - * diagonal and the couplings U_(g-h) off it; the Hermitian part is - * diagonalized and the absorption enters to first order (the - * fast_absorption path of quantem). - */ -export function blochSolve(c: CrystalData, beams: Reflection[], tilt: [number, number] = [0, 0]): BlochSolution { - const k0 = 1 / c.wavelength; - const kz = Math.sqrt(Math.max(k0 * k0 - tilt[0] ** 2 - tilt[1] ** 2, 1e-12)); - const n = beams.length; - const sg = beams.map((r) => (r.index < 0 ? 0 : tiltedExcitation(r, kz, tilt))); - const hRe = new Float64Array(n * n); - const hIm = new Float64Array(n * n); - const wRe = new Float64Array(n * n); - const wIm = new Float64Array(n * n); - for (let i = 0; i < n; i++) { - hRe[i * n + i] = 2 * k0 * sg[i]; - for (let j = 0; j < n; j++) { - if (i === j) continue; - const d0 = beams[i].hkl[0] - beams[j].hkl[0], d1 = beams[i].hkl[1] - beams[j].hkl[1], d2 = beams[i].hkl[2] - beams[j].hkl[2]; - const ur = c.couplingRe.get(`${d0},${d1},${d2}`) ?? 0; - const ui = c.couplingIm.get(`${d0},${d1},${d2}`) ?? 0; - const tr = c.couplingRe.get(`${-d0},${-d1},${-d2}`) ?? 0; - const ti = c.couplingIm.get(`${-d0},${-d1},${-d2}`) ?? 0; - // A_ij = U_(gi - gj) = (ur, ui); conj(A_ji) = (tr, -ti) - // H = (A + A^dagger) / 2, W = (A - H) / i - const hr = 0.5 * (ur + tr), hi = 0.5 * (ui - ti); - hRe[i * n + j] = hr; - hIm[i * n + j] = hi; - wRe[i * n + j] = ui - hi; - wIm[i * n + j] = -(ur - hr); - } - } - const { vals, vecRe, vecIm } = eighComplex(hRe, hIm, n); - const gammaRe = new Float64Array(n); - const gammaIm = new Float64Array(n); - for (let j = 0; j < n; j++) { - gammaRe[j] = vals[j] / (2 * k0); - let acc = 0; - for (let i = 0; i < n; i++) { - let sr = 0, si = 0; - for (let k = 0; k < n; k++) { - const wr = wRe[i * n + k], wi = wIm[i * n + k]; - if (wr === 0 && wi === 0) continue; - const cr = vecRe[k * n + j], ci = vecIm[k * n + j]; - sr += wr * cr - wi * ci; - si += wr * ci + wi * cr; - } - acc += vecRe[i * n + j] * sr + vecIm[i * n + j] * si; - } - gammaIm[j] = (acc + c.u0_imag) / (2 * k0); - } - const psi0Re = new Float64Array(n); - const psi0Im = new Float64Array(n); - for (let j = 0; j < n; j++) { - psi0Re[j] = vecRe[j]; - psi0Im[j] = -vecIm[j]; - } - return { beams, n, gammaRe, gammaIm, vecRe, vecIm, psi0Re, psi0Im }; -} - -/** - * Beam list for the hybrid pattern: the direct beam, then the Bloch set - * (within sgMax of the Ewald sphere, capped), then every other reflection - * within kMax. The first nDyn beams enter the Bloch calculation; the rest - * take thin-slab intensities (slabIntensities). - */ -export function hybridBeams(c: CrystalData, q: Quat, kMax: number, sgMax: number, maxBeams: number, tiltRange = 0): { beams: Reflection[]; nDyn: number } { - const dyn = selectBeams(c, q, kMax, sgMax, maxBeams, tiltRange); - const inDyn = new Set(); - for (const b of dyn) inDyn.add(b.index); - const rest = labReflections(c, q, kMax).filter((r) => !inDyn.has(r.index)); - return { beams: [...dyn, ...rest], nDyn: dyn.length }; -} - -/** - * Thin-slab (first Born) intensities I_g = (pi |U_g| t / k0)^2 sinc^2(pi s_g t) - * for beams[from..] at an incident tilt: the weak-beam limit of the Bloch - * result, in the same normalization (fraction of the incident intensity). - */ -export function slabIntensities(c: CrystalData, beams: Reflection[], from: number, tilt: [number, number], thickness: number, out: Float64Array) { - const k0 = 1 / c.wavelength; - const kz = Math.sqrt(Math.max(k0 * k0 - tilt[0] ** 2 - tilt[1] ** 2, 1e-12)); - for (let i = from; i < beams.length; i++) { - const r = beams[i]; - const s = tilt[0] === 0 && tilt[1] === 0 ? r.s : tiltedExcitation(r, kz, tilt); - const u = Math.hypot(c.U_re[r.index], c.U_im[r.index]); - const x = Math.PI * s * thickness; - const sinc = Math.abs(x) < 1e-8 ? 1 : Math.sin(x) / x; - const amp = (Math.PI * u * thickness) / k0; - out[i] = amp * amp * sinc * sinc; - } -} - -/** Kinematical intensities of a beam list at an incident tilt. */ -export function kinematicalTilted(c: CrystalData, beams: Reflection[], tilt: [number, number], sigma: number): Float64Array { - const k0 = 1 / c.wavelength; - const kz = Math.sqrt(Math.max(k0 * k0 - tilt[0] ** 2 - tilt[1] ** 2, 1e-12)); - const out = new Float64Array(beams.length); - for (let i = 0; i < beams.length; i++) { - const r = beams[i]; - if (r.index < 0) { out[i] = 1; continue; } - const s = tiltedExcitation(r, kz, tilt); - out[i] = c.F2[r.index] * Math.exp(-(s * s) / (2 * sigma * sigma)); - } - return out; -} - -/** - * Incident-beam tilts (1/A) sampling a precession cone of half angle - * precDeg: n points evenly spaced on the ring of radius k0 sin(phi), the - * Gauss-Chebyshev quadrature of the azimuthal average. No precession - * returns the single untilted beam. - */ -export function precessionTilts(k0: number, precDeg: number, n: number): [number, number][] { - if (!(precDeg > 0) || n < 1) return [[0, 0]]; - const r = k0 * Math.sin((precDeg * Math.PI) / 180); - const out: [number, number][] = []; - for (let i = 0; i < n; i++) { - const th = (2 * Math.PI * (i + 0.5)) / n; - out.push([r * Math.cos(th), r * Math.sin(th)]); - } - return out; -} - -/** - * Hybrid nanobeam intensities averaged over incident tilts: Bloch - * intensities of the first nDyn beams from one solution per tilt, thin-slab - * intensities for the rest, mean over the tilts. - */ -export function averagedIntensities( - c: CrystalData, beams: Reflection[], nDyn: number, sols: (BlochSolution | null)[], tilts: [number, number][], thickness: number, -): Float64Array { - const out = new Float64Array(beams.length); - const tmp = new Float64Array(beams.length); - for (let t = 0; t < tilts.length; t++) { - tmp.fill(0); - const sol = sols[t]; - if (sol) tmp.set(blochIntensities(sol, thickness)); - slabIntensities(c, beams, sol ? nDyn : 0, tilts[t], thickness, tmp); - for (let i = 0; i < beams.length; i++) out[i] += tmp[i] / tilts.length; - } - return out; -} - -export interface NanobeamSolution { - beams: Reflection[]; // DIRECT + every reflection within kMax (draw list) - nodes: { tilt: [number, number]; sol: BlochSolution; pos: Int32Array }[]; // pos: position of each Bloch beam in beams - nDynMean: number; -} - -/** - * Nanobeam pattern averaged over incident tilts (precession ring, or the - * single untilted beam). Each node selects its own Bloch set from the - * reflections within sgMax of ITS Ewald sphere (capped at maxBeams by - * |U_g| / |s_g|), so a 3 degree precession cone excites the right beams at - * every azimuth; every other reflection takes the thin-slab intensity. - */ -export function nanobeamSolve(c: CrystalData, q: Quat, kMax: number, sgMax: number, maxBeams: number, tilts: [number, number][]): NanobeamSolution { - const k0 = 1 / c.wavelength; - const all = labReflections(c, q, kMax); - const beams = [DIRECT, ...all]; - const nodes: NanobeamSolution["nodes"] = []; - let nSum = 0; - for (const tilt of tilts) { - const kz = Math.sqrt(Math.max(k0 * k0 - tilt[0] ** 2 - tilt[1] ** 2, 1e-12)); - let cand: { ref: Reflection; s: number; i: number }[] = []; - for (let i = 0; i < all.length; i++) { - const st = tiltedExcitation(all[i], kz, tilt); - if (Math.abs(st) < sgMax) cand.push({ ref: all[i], s: st, i }); - } - if (cand.length > maxBeams) { - cand = rankCandidates(c, cand).slice(0, maxBeams) as typeof cand; - } - const dyn = [DIRECT, ...cand.map((x) => x.ref)]; - const pos = new Int32Array(dyn.length); - pos[0] = 0; - cand.forEach((x, j) => { pos[j + 1] = x.i + 1; }); - nodes.push({ tilt, sol: blochSolve(c, dyn, tilt), pos }); - nSum += cand.length; - } - return { beams, nodes, nDynMean: nSum / Math.max(1, tilts.length) }; -} - -/** Intensities of a NanobeamSolution at a thickness (A): mean over the nodes. */ -export function nanobeamIntensities(c: CrystalData, ns: NanobeamSolution, thickness: number): Float64Array { - const out = new Float64Array(ns.beams.length); - const tmp = new Float64Array(ns.beams.length); - for (const node of ns.nodes) { - slabIntensities(c, ns.beams, 1, node.tilt, thickness, tmp); // every diffracted beam, then overwrite the Bloch ones - tmp[0] = 0; - const bi = blochIntensities(node.sol, thickness); - for (let j = 0; j < node.pos.length; j++) tmp[node.pos[j]] = bi[j]; - for (let i = 0; i < out.length; i++) out[i] += tmp[i] / ns.nodes.length; - } - return out; -} - -/** Beam intensities at a thickness (A) from a Bloch solution. */ -export function blochIntensities(sol: BlochSolution, thickness: number): Float64Array { - const { n, gammaRe, gammaIm, vecRe, vecIm, psi0Re, psi0Im } = sol; - const phRe = new Float64Array(n); - const phIm = new Float64Array(n); - for (let j = 0; j < n; j++) { - const amp = Math.exp(-2 * Math.PI * gammaIm[j] * thickness); - const ph = 2 * Math.PI * gammaRe[j] * thickness; - const er = amp * Math.cos(ph), ei = amp * Math.sin(ph); - // e^{2 pi i gamma t} * conj(C_0j) - phRe[j] = er * psi0Re[j] - ei * psi0Im[j]; - phIm[j] = er * psi0Im[j] + ei * psi0Re[j]; - } - const out = new Float64Array(n); - for (let g = 0; g < n; g++) { - let sr = 0, si = 0; - for (let j = 0; j < n; j++) { - const cr = vecRe[g * n + j], ci = vecIm[g * n + j]; - sr += cr * phRe[j] - ci * phIm[j]; - si += cr * phIm[j] + ci * phRe[j]; - } - out[g] = sr * sr + si * si; - } - return out; -} - -export interface KosselLine { - hkl: number[]; - normal: [number, number]; // unit, in the tilt plane (rad) - distance: number; // rad, line is {theta : theta . normal = distance} - width: number; // rad - strength: number; // relative 0..1 - gxy: number; // |g_xy|, 1/A (excess Kikuchi line offset) -} - -/** - * Kossel (deficiency) lines of every reflection in the tilt plane: - * g_xy . theta = g_z - lambda |g|^2 / 2, a straight line at small angles. - * The width is the two-beam rocking width |U_g| / (k0 |g_xy|). - */ -/** Coupling of a strong reflection (Si 111 at 200 keV is 0.05 1/A^2): line darkness is measured against it. */ -export const U_REF_LINES = 0.04; -const MAX_LINES = 200; // strongest lines kept: enough for the rosettes of small cells, readable for large ones - -export function kosselLines(c: CrystalData, q: Quat, kMax: number, fieldRad: number): KosselLine[] { - const R = quatToMatrix(q); - const lam = c.wavelength; - const k0 = 1 / lam; - const lines: KosselLine[] = []; - // strength is ABSOLUTE (|U_g| / U_REF_LINES, capped at 1): a weakly - // scattering cell gives faint lines, and only the strongest MAX_LINES are - // kept so a large cell does not draw a hundred thousand of them - const uMin = 0.02 * U_REF_LINES; - for (let i = 0; i < c.hkl.length; i++) { - const gc: Vec3 = [c.g[3 * i], c.g[3 * i + 1], c.g[3 * i + 2]]; - const gLen = Math.hypot(gc[0], gc[1], gc[2]); - if (gLen > kMax) continue; - const g = matVec(R, gc); - const gxy = Math.hypot(g[0], g[1]); - if (gxy < 1e-6) continue; - const dist = (g[2] - (lam * gLen * gLen) / 2) / gxy; - if (Math.abs(dist) > fieldRad * 1.5) continue; - const u = Math.hypot(c.U_re[i], c.U_im[i]); - if (u < uMin) continue; - lines.push({ hkl: c.hkl[i], normal: [g[0] / gxy, g[1] / gxy], distance: dist, width: u / (k0 * gxy), strength: Math.min(1, u / U_REF_LINES), gxy }); - } - if (lines.length > MAX_LINES) { - lines.sort((a, b) => b.strength - a.strength); - lines.length = MAX_LINES; - } - return lines; -} - -export interface KosselReference { - shape: number[]; // (T, n, n) - step: number; - thicknesses: number[]; - data: Float32Array; -} - -export function parseKossel(json: string): KosselReference | null { - if (!json || json === "{}") return null; - const o = JSON.parse(json); - return { shape: o.shape, step: o.step, thicknesses: o.thicknesses, data: decodeF32(o.data) }; -} - -/** Bright field Kossel image (size x size) over +-fieldRad by lookup in the reference. */ -export function kosselLookup(ref: KosselReference, q: Quat, fieldRad: number, size: number, thickness: number, viewX = 1): Float32Array { - const R = quatToMatrix(q); - const [T, n] = ref.shape; - const half = (n - 1) / 2; - // nearest thickness with linear blend - let ti = 0; - for (let k = 0; k < T; k++) if (Math.abs(ref.thicknesses[k] - thickness) < Math.abs(ref.thicknesses[ti] - thickness)) ti = k; - const plane = ti * n * n; - const out = new Float32Array(size * size); - for (let r = 0; r < size; r++) { - const ty = -((r + 0.5) / size - 0.5) * 2 * fieldRad; // canvas rows run downward - for (let col = 0; col < size; col++) { - const tx = viewX * ((col + 0.5) / size - 0.5) * 2 * fieldRad; - if (tx * tx + ty * ty > fieldRad * fieldRad) { out[r * size + col] = NaN; continue; } - // incident wavevector is (t, -kz) in quantem (beam travels along -z); the - // reference is indexed by the anti-propagation direction (-t, +kz)/k0 - const dl: Vec3 = [-tx, -ty, Math.sqrt(Math.max(1 - tx * tx - ty * ty, 0))]; - let dc: Vec3 = [R[0] * dl[0] + R[3] * dl[1] + R[6] * dl[2], R[1] * dl[0] + R[4] * dl[1] + R[7] * dl[2], R[2] * dl[0] + R[5] * dl[1] + R[8] * dl[2]]; - if (dc[2] < 0) dc = [-dc[0], -dc[1], -dc[2]]; - const rho = Math.sqrt(Math.max(2 * (1 - dc[2]), 0)); - const dxy = Math.max(Math.hypot(dc[0], dc[1]), 1e-12); - const fx = (dc[0] / dxy) * rho / ref.step + half; - const fy = (dc[1] / dxy) * rho / ref.step + half; - const ix = Math.min(Math.max(Math.floor(fx), 0), n - 2), iy = Math.min(Math.max(Math.floor(fy), 0), n - 2); - const wx = Math.min(Math.max(fx - ix, 0), 1), wy = Math.min(Math.max(fy - iy, 0), 1); - const v = ref.data[plane + ix * n + iy] * (1 - wx) * (1 - wy) + ref.data[plane + (ix + 1) * n + iy] * wx * (1 - wy) - + ref.data[plane + ix * n + iy + 1] * (1 - wx) * wy + ref.data[plane + (ix + 1) * n + iy + 1] * wx * wy; - out[r * size + col] = v; - } - } - return out; -} - -/** Percentile-based display range of finite values. */ -export function percentiles(data: Float32Array | Float64Array, pLow: number, pHigh: number): [number, number] { - const vals: number[] = []; - for (let i = 0; i < data.length; i++) if (isFinite(data[i])) vals.push(data[i]); - if (!vals.length) return [0, 1]; - vals.sort((a, b) => a - b); - const lo = vals[Math.min(vals.length - 1, Math.floor((pLow / 100) * (vals.length - 1)))]; - const hi = vals[Math.min(vals.length - 1, Math.floor((pHigh / 100) * (vals.length - 1)))]; - return hi > lo ? [lo, hi] : [lo, lo + 1e-12]; -} diff --git a/widget/js/format.ts b/widget/js/format.ts deleted file mode 100644 index 31f2c4ca3..000000000 --- a/widget/js/format.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** Convert anywidget DataView/ArrayBuffer to Uint8Array. */ -export function extractBytes(dataView: DataView | ArrayBuffer | Uint8Array): Uint8Array { - if (dataView instanceof Uint8Array) return dataView; - if (dataView instanceof ArrayBuffer) return new Uint8Array(dataView); - if (dataView && "buffer" in dataView) { - return new Uint8Array(dataView.buffer, dataView.byteOffset, dataView.byteLength); - } - return new Uint8Array(0); -} - -/** Extract Float32Array from anywidget DataView. Returns null if empty. */ -export function extractFloat32(dataView: DataView | ArrayBuffer | Uint8Array): Float32Array | null { - const bytes = extractBytes(dataView); - if (bytes.length === 0) return null; - return new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4); -} - -/** Download a Blob as a file. */ -export function downloadBlob(blob: Blob, filename: string): void { - const link = document.createElement("a"); - link.download = filename; - const url = URL.createObjectURL(blob); - link.href = url; - link.click(); - // Defer revocation to ensure browser has time to start the download - setTimeout(() => URL.revokeObjectURL(url), 60000); -} - -/** Download a DataView as a file (e.g. GIF/ZIP from Python). */ -export function downloadDataView(dataView: DataView, filename: string, mimeType: string): void { - const buf = new Uint8Array(dataView.buffer as ArrayBuffer, dataView.byteOffset, dataView.byteLength); - downloadBlob(new Blob([buf as BlobPart], { type: mimeType }), filename); -} - -/** Format number with exponential notation for large/small values. */ -export function formatNumber(val: number, decimals: number = 2): string { - if (val === 0) return "0"; - if (Math.abs(val) >= 1000 || Math.abs(val) < 0.01) return val.toExponential(decimals); - return val.toFixed(decimals); -} diff --git a/widget/js/theme.ts b/widget/js/theme.ts deleted file mode 100644 index f13123d5c..000000000 --- a/widget/js/theme.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Shared theme detection and color system for all widgets. - * Detects JupyterLab, VS Code, Colab, Classic Jupyter, and OS preferences. - */ - -import { useState, useEffect, useMemo } from "react"; - -// ============================================================================ -// Types -// ============================================================================ -export type Environment = "jupyterlab" | "vscode" | "colab" | "jupyter-classic" | "unknown"; -export type Theme = "light" | "dark"; - -export interface ThemeInfo { - environment: Environment; - theme: Theme; -} - -export interface ThemeColors { - bg: string; - bgAlt: string; - text: string; - textMuted: string; - border: string; - controlBg: string; - accent: string; -} - -// ============================================================================ -// Color palettes -// ============================================================================ -export const DARK_COLORS: ThemeColors = { - bg: "#1e1e1e", - bgAlt: "#1a1a1a", - text: "#e0e0e0", - textMuted: "#888", - border: "#3a3a3a", - controlBg: "#252525", - accent: "#5af", -}; - -export const LIGHT_COLORS: ThemeColors = { - bg: "#ffffff", - bgAlt: "#f5f5f5", - text: "#1e1e1e", - textMuted: "#666", - border: "#ccc", - controlBg: "#f0f0f0", - accent: "#0066cc", -}; - -export function getThemeColors(theme: Theme): ThemeColors { - return theme === "dark" ? DARK_COLORS : LIGHT_COLORS; -} - -// ============================================================================ -// Theme detection -// ============================================================================ - -/** Check if a CSS color string is dark (luminance < 0.5) */ -export function isColorDark(color: string): boolean { - const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - if (!match) return true; - const [, r, g, b] = match.map(Number); - const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; - return luminance < 0.5; -} - -export function detectTheme(): ThemeInfo { - // 1. JupyterLab - has data-jp-theme-light attribute - const jpThemeLight = document.body.dataset.jpThemeLight; - if (jpThemeLight !== undefined) { - return { - environment: "jupyterlab", - theme: jpThemeLight === "true" ? "light" : "dark", - }; - } - - // 2. VS Code - has vscode-* classes on body or html - const bodyClasses = document.body.className; - const htmlClasses = document.documentElement.className; - if (bodyClasses.includes("vscode-") || htmlClasses.includes("vscode-")) { - const isDark = bodyClasses.includes("vscode-dark") || htmlClasses.includes("vscode-dark"); - return { - environment: "vscode", - theme: isDark ? "dark" : "light", - }; - } - - // 3. Google Colab - has specific markers - if (document.querySelector('colab-shaded-scroller') || document.body.classList.contains('colaboratory')) { - const bg = getComputedStyle(document.body).backgroundColor; - return { - environment: "colab", - theme: isColorDark(bg) ? "dark" : "light", - }; - } - - // 4. Classic Jupyter Notebook - has #notebook element - if (document.getElementById('notebook')) { - const bodyBg = getComputedStyle(document.body).backgroundColor; - return { - environment: "jupyter-classic", - theme: isColorDark(bodyBg) ? "dark" : "light", - }; - } - - // 5. Fallback: check OS preference, then computed background - const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)')?.matches; - if (prefersDark !== undefined) { - return { - environment: "unknown", - theme: prefersDark ? "dark" : "light", - }; - } - - // Final fallback: check body background luminance - const bg = getComputedStyle(document.body).backgroundColor; - return { - environment: "unknown", - theme: isColorDark(bg) ? "dark" : "light", - }; -} - -// ============================================================================ -// React hook -// ============================================================================ -export function useTheme(): { themeInfo: ThemeInfo; colors: ThemeColors } { - const [themeInfo, setThemeInfo] = useState(() => detectTheme()); - - useEffect(() => { - const mediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)'); - const handleChange = () => setThemeInfo(detectTheme()); - mediaQuery?.addEventListener?.('change', handleChange); - - const observer = new MutationObserver(() => setThemeInfo(detectTheme())); - observer.observe(document.body, { attributes: true, attributeFilter: ['data-jp-theme-light', 'class'] }); - - return () => { - mediaQuery?.removeEventListener?.('change', handleChange); - observer.disconnect(); - }; - }, []); - - // Memoize by theme string so `colors` is referentially stable across renders — - // effects/components that depend on `colors` only re-run when the theme flips. - const colors = useMemo(() => getThemeColors(themeInfo.theme), [themeInfo.theme]); - return { themeInfo, colors }; -} diff --git a/widget/package-lock.json b/widget/package-lock.json deleted file mode 100644 index ff1510fd5..000000000 --- a/widget/package-lock.json +++ /dev/null @@ -1,1637 +0,0 @@ -{ - "name": "quantem-widget-frontend", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "quantem-widget-frontend", - "dependencies": { - "@anywidget/react": "^0.2.0", - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^7.3.7", - "@mui/material": "^7.3.6", - "jszip": "^3.10.1", - "react": "^19.1.0", - "react-dom": "^19.1.0" - }, - "devDependencies": { - "@types/react": "^19.1.3", - "@types/react-dom": "^19.1.4", - "@webgpu/types": "^0.1.68", - "esbuild": "^0.21.3", - "typescript": "^5.8.3" - } - }, - "node_modules/@anywidget/react": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@anywidget/react/-/react-0.2.2.tgz", - "integrity": "sha512-MjrbbUimaX72ATL9IrrZeMZL+IHVzeW5lMlgZywDAMj4/zUVQ7MeBSaijq7V9genze7sM3Y0WnUyB+Mc7KnWWw==", - "license": "MIT", - "dependencies": { - "@anywidget/types": "^0.4.0" - }, - "peerDependencies": { - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@anywidget/types": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@anywidget/types/-/types-0.4.0.tgz", - "integrity": "sha512-Qno/7V0lKHCMq3DJuSKKHMwilFKPSe8wFftL5xWmgaMCc938mNTtv+i19UrvDfpj9cQTnlPqyXy8t3JOgQ8laA==", - "license": "MIT" - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/cache": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", - "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/react": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", - "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", - "license": "MIT" - }, - "node_modules/@emotion/styled": { - "version": "11.14.1", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", - "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "license": "MIT" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mui/core-downloads-tracker": { - "version": "7.3.10", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.10.tgz", - "integrity": "sha512-vrOpWRmPJSuwLo23J62wggEm/jvGdzqctej+UOCtgDUz6nZJQuj3ByPccVyaa7eQmwAzUwKN56FQPMKkqbj1GA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/icons-material": { - "version": "7.3.10", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.10.tgz", - "integrity": "sha512-Au0ma4NSKGKNiimukj8UT/W1x2Qx6Qwn2RvFGykiSqVLYBNlIOPbjnIMvrwLGLu89EEpTVdu/ys/OduZR+tWqw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/material": "^7.3.10", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material": { - "version": "7.3.10", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.10.tgz", - "integrity": "sha512-cHvGOk2ZEfbQt3LnGe0ZKd/ETs9gsUpkW66DCO+GSjMZhpdKU4XsuIr7zJ/B/2XaN8ihxuzHfYAR4zPtCN4RYg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@mui/core-downloads-tracker": "^7.3.10", - "@mui/system": "^7.3.10", - "@mui/types": "^7.4.12", - "@mui/utils": "^7.3.10", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.12", - "clsx": "^2.1.1", - "csstype": "^3.2.3", - "prop-types": "^15.8.1", - "react-is": "^19.2.3", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^7.3.10", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming": { - "version": "7.3.10", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.10.tgz", - "integrity": "sha512-j3EZN+zOctxUISvJSmsEPo5o2F8zse4l5vRkBY+ps6UtnL6J7o14kUaI4w7gwo73id9e3cDNMVQK/9BVaMHVBw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@mui/utils": "^7.3.10", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "7.3.10", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.10.tgz", - "integrity": "sha512-WxE9SiF8xskAQqGjsp0poXCkCqsoXFEsSr0HBXfApmGHR+DBnXRp+z46Vsltg4gpPM4Z96DeAQRpeAOnhNg7Ng==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.2.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "7.3.10", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.10.tgz", - "integrity": "sha512-/sfPpdpJaQn7BSF+avjIdHSYmxHp0UOBYNxSG9QGKfMOD6sLANCpRPCnanq1Pe0lFf0NHkO2iUk0TNzdWC1USQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@mui/private-theming": "^7.3.10", - "@mui/styled-engine": "^7.3.10", - "@mui/types": "^7.4.12", - "@mui/utils": "^7.3.10", - "clsx": "^2.1.1", - "csstype": "^3.2.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "7.4.12", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", - "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "7.3.10", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.10.tgz", - "integrity": "sha512-7y2eIfy0h7JPz+Yy4pS+wgV68d46PuuxDqKBN4Q8VlPQSsCAGwroMCV6xWyc7g9dvEp8ZNFsknc59GHWO+r6Ow==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@mui/types": "^7.4.12", - "@types/prop-types": "^15.7.15", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.2.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@webgpu/types": { - "version": "0.1.69", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", - "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", - "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.5" - } - }, - "node_modules/react-is": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", - "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", - "license": "MIT" - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "license": "MIT" - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - } - } -} diff --git a/widget/package.json b/widget/package.json deleted file mode 100644 index 8ceb653b2..000000000 --- a/widget/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "quantem-widget-frontend", - "type": "module", - "scripts": { - "dev": "npm run build -- --watch", - "build": "node scripts/build.mjs", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@anywidget/react": "^0.2.0", - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^7.3.7", - "@mui/material": "^7.3.6", - "jszip": "^3.10.1", - "react": "^19.1.0", - "react-dom": "^19.1.0" - }, - "devDependencies": { - "@types/react": "^19.1.3", - "@types/react-dom": "^19.1.4", - "@webgpu/types": "^0.1.68", - "esbuild": "^0.21.3", - "typescript": "^5.8.3" - } -} diff --git a/widget/pyproject.toml b/widget/pyproject.toml deleted file mode 100644 index 738c7bc98..000000000 --- a/widget/pyproject.toml +++ /dev/null @@ -1,22 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "quantem.widget" -version = "0.0.1" -description = "Interactive custom Python widgets for electron microscopy" -license = "MIT" -requires-python = ">=3.11" -dependencies = [ - "anywidget>=0.9.0", - "numpy>=2.0.0", - "traitlets>=5.0.0", - "torch>=2.0.0", - "matplotlib>=3.7.0", - "Pillow>=10.0.0", -] - -[tool.hatch.build.targets.wheel] -packages = ["src/quantem"] -artifacts = ["src/quantem/widget/static/**"] diff --git a/widget/scripts/build.mjs b/widget/scripts/build.mjs deleted file mode 100644 index 21dc88851..000000000 --- a/widget/scripts/build.mjs +++ /dev/null @@ -1,47 +0,0 @@ -// Bundle each widget as a self-contained ESM file. -// anywidget loads bundles via Blob URL; relative imports break in that context. -// esbuild flattens everything into one file per widget. - -import { build, context } from "esbuild"; -import { rmSync, copyFileSync, mkdirSync, existsSync } from "fs"; - -const watch = process.argv.includes("--watch"); -const widgets = [ - { name: "diffsim" }, - // framework-free build of the diffraction simulator for web pages (MyST anywidget directive) - { name: "diffsim-web", entry: "js/diffsim-web/index.ts", outfile: "dist/diffraction-sim.js" }, -]; - -rmSync("src/quantem/widget/static", { recursive: true, force: true }); -mkdirSync("src/quantem/widget/static", { recursive: true }); - -const baseOpts = { - bundle: true, - format: "esm", - jsx: "automatic", - target: "es2022", - define: { "process.env.NODE_ENV": '"production"' }, - loader: { ".css": "text" }, - minify: true, - sourcemap: false, - legalComments: "none", -}; - -for (const w of widgets) { - const opts = { - ...baseOpts, - entryPoints: [w.entry || `js/${w.name}/index.tsx`], - outfile: w.outfile || `src/quantem/widget/static/${w.name}.js`, - }; - if (watch) { - const ctx = await context(opts); - await ctx.watch(); - console.log(`watching ${w.name}...`); - } else { - const start = Date.now(); - await build(opts); - console.log(`built ${w.name}.js (${Date.now() - start}ms)`); - } -} - -if (!watch) console.log("done."); diff --git a/widget/src/quantem/widget/__init__.py b/widget/src/quantem/widget/__init__.py deleted file mode 100644 index 3a4b63f5d..000000000 --- a/widget/src/quantem/widget/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from importlib.metadata import PackageNotFoundError, version - -from quantem.widget.diffsim import DiffractionSim - -try: - __version__ = version("quantem.widget") -except PackageNotFoundError: - # Source-tree imports (e.g. `PYTHONPATH=src pytest`) skip pip install. - __version__ = "0.0.0+local" - -__all__ = ["DiffractionSim"] diff --git a/widget/src/quantem/widget/diffsim.py b/widget/src/quantem/widget/diffsim.py deleted file mode 100644 index 6c36b4321..000000000 --- a/widget/src/quantem/widget/diffsim.py +++ /dev/null @@ -1,559 +0,0 @@ -""" -DiffractionSim: interactive crystal and diffraction simulation for teaching. - -Left panel: the unit cell in 3D, rotated by dragging (mouse or touch) or by -buttons about the screen axes. Right panel: the diffraction pattern of the -same orientation, updated live: nanobeam (kinematical markers, or Bloch wave -intensities that follow the thickness slider), CBED disks, or the Kikuchi -LACBED lines. Every simulation runs in the browser, so the widget can be -exported as a single HTML file and embedded in a web page without Python. -""" - -from __future__ import annotations - -import base64 -import json -import pathlib -import re - -import anywidget -import numpy as np -import torch -import traitlets - -_STATIC = pathlib.Path(__file__).parent / "static" / "diffsim.js" - -# ASE bulk structures offered in the crystal menu -PRESETS: dict[str, dict] = { - "Si (diamond cubic)": dict(name="Si", crystalstructure="diamond", a=5.431, cubic=True), - "Ge (diamond cubic)": dict(name="Ge", crystalstructure="diamond", a=5.658, cubic=True), - "Al (fcc)": dict(name="Al", crystalstructure="fcc", a=4.05, cubic=True), - "Cu (fcc)": dict(name="Cu", crystalstructure="fcc", a=3.615, cubic=True), - "Au (fcc)": dict(name="Au", crystalstructure="fcc", a=4.078, cubic=True), - "Fe (bcc)": dict(name="Fe", crystalstructure="bcc", a=2.866, cubic=True), - "W (bcc)": dict(name="W", crystalstructure="bcc", a=3.165, cubic=True), - "Ti (hcp)": dict(name="Ti", crystalstructure="hcp", a=2.9505, c=4.6855), - "Mg (hcp)": dict(name="Mg", crystalstructure="hcp", a=3.209, c=5.211), - "GaAs (zincblende)": dict(name="GaAs", crystalstructure="zincblende", a=5.653, cubic=True), - "NaCl (rocksalt)": dict(name="NaCl", crystalstructure="rocksalt", a=5.64, cubic=True), - "SrTiO3 (perovskite)": dict( - symbols=["Sr", "Ti", "O"], - basis=[(0, 0, 0), (0.5, 0.5, 0.5), (0.5, 0.5, 0)], - spacegroup=221, - cellpar=[3.905, 3.905, 3.905, 90, 90, 90], - ), - "Al2O3 (corundum)": dict( - symbols=["Al", "O"], - basis=[(0, 0, 0.3523), (0.3064, 0, 0.25)], - spacegroup=167, - cellpar=[4.7602, 4.7602, 12.9933, 90, 90, 120], - ), - "SiO2 (alpha quartz)": dict( - symbols=["Si", "O"], - basis=[(0.4697, 0, 0), (0.4135, 0.2669, 0.1191)], - spacegroup=154, - cellpar=[4.9134, 4.9134, 5.4052, 90, 90, 120], - ), - "alpha-Mn (58 atoms)": dict( - symbols=["Mn", "Mn", "Mn", "Mn"], - basis=[ - (0, 0, 0), - (0.3175, 0.3175, 0.3175), - (0.3570, 0.3570, 0.0348), - (0.0896, 0.0896, 0.2820), - ], - spacegroup=217, - cellpar=[8.911, 8.911, 8.911, 90, 90, 90], - ), -} - - -def _preset_atoms(key: str): - from ase.build import bulk - - spec = PRESETS[key] - if "spacegroup" in spec: - from ase.spacegroup import crystal as ase_crystal - - return ase_crystal( - spec["symbols"], - basis=spec["basis"], - spacegroup=spec["spacegroup"], - cellpar=spec["cellpar"], - ) - return bulk(**spec) - - -def _f32_b64(a) -> str: - return base64.b64encode( - np.ascontiguousarray(np.asarray(a, dtype=np.float32)).tobytes() - ).decode() - - -def prepare_crystal(crystal, energy_ev: float, k_max: float) -> dict: - """Everything the browser needs to draw the cell and simulate patterns. - - One reflection list: the points of the primitive reciprocal lattice - within k_max (glide-forbidden reflections such as Si 200 and 222 are - present with zero kinematical intensity and fill by multiple scattering - in the Bloch calculation). Per reflection the kinematical |F_g|^2 - (Lobato) and the Bloch coupling U_g (absorptive Weickenmeier-Kohl - factors at this energy when available, else gamma F_g / pi). The browser - builds the structure matrix from the same list, so couplings between - beams further apart than k_max are taken as zero; at the default - k_max = 4 1/A those factors are below 2% of U_000 for every element. - Indices travel as int16, g is rebuilt from the reciprocal cell. - """ - from ase.data import chemical_symbols, covalent_radii - from ase.data.colors import jmol_colors - - from quantem.core.utils.utils import electron_wavelength_angstrom - from quantem.diffraction import bloch - - if crystal.g_vec is None or crystal.k_max is None or crystal.k_max < k_max: - crystal.calculate_structure_factors(k_max=k_max) - have_dyn = ( - getattr(crystal, "U_dyn", None) is not None - and abs(getattr(crystal, "dyn_energy_ev", -1) - energy_ev) < 1 - and getattr(crystal, "dyn_k_max", 0) >= k_max - ) - if not have_dyn: - try: - crystal.calculate_dynamical_structure_factors(energy_ev=energy_ev, k_max=k_max) - have_dyn = True - except Exception: - have_dyn = False - hkl_u, g_u = bloch._beam_universe(crystal) - keep = torch.linalg.norm(g_u, dim=1) <= k_max - hkl = hkl_u[keep] - gamma_rel = bloch.relativistic_gamma(energy_ev) - U_g, u0_imag = _coupling_vector(crystal, hkl, gamma_rel) - # kinematical |F|^2 (Lobato) on the same list - lut = {tuple(h): i for i, h in enumerate(crystal.hkl.tolist())} - F2 = torch.zeros(hkl.shape[0], dtype=torch.float64) - for i, h in enumerate(hkl.tolist()): - j = lut.get(tuple(h)) - if j is not None: - F2[i] = crystal.struct_factors_int[j] - numbers = crystal.numbers.numpy() - hkl_i16 = np.ascontiguousarray(hkl.numpy().astype(np.int16)) - return { - "name": crystal.name, - "spacegroup": getattr(crystal, "spacegroup", ""), - "pointgroup": getattr(crystal, "pointgroup", ""), - "cell": crystal.lat_real.numpy().tolist(), - "recip": crystal.lat_recip.numpy().tolist(), - "positions_frac": crystal.positions_frac.numpy().tolist(), - "numbers": numbers.tolist(), - "symbols": [chemical_symbols[int(z)] for z in numbers], - "colors": [jmol_colors[int(z)].tolist() for z in numbers], - "radii": [float(covalent_radii[int(z)]) for z in numbers], - "hkl_i16": base64.b64encode(hkl_i16.tobytes()).decode(), - "F2": _f32_b64(F2.numpy()), - "U_re": _f32_b64(U_g.real.numpy()), - "U_im": _f32_b64(U_g.imag.numpy()), - "u0_imag": float(u0_imag), - "absorptive": bool(have_dyn), - "n_reflections": int(hkl.shape[0]), - "energy_ev": float(energy_ev), - "wavelength": float(electron_wavelength_angstrom(energy_ev)), - "k_max": float(k_max), - "hexagonal": bool(getattr(crystal, "hexagonal_matching", False)), - } - - -def _coupling_vector(crystal, hkl: torch.Tensor, gamma_rel: float) -> tuple[torch.Tensor, float]: - """U_g for a list of hkl (zero where no factor is stored) and the mean - absorption U_000''. Same factor choice as bloch._coupling_matrix, but a - vector lookup instead of the (N, N) difference matrix.""" - if getattr(crystal, "U_dyn", None) is not None: - hkl_all, U_all = crystal.hkl_dyn, crystal.U_dyn - else: - hkl_all, U_all = crystal.hkl, crystal.struct_factors * (gamma_rel / np.pi) - lut = {tuple(h): i for i, h in enumerate(hkl_all.tolist())} - idx = torch.tensor([lut.get(tuple(h), -1) for h in hkl.tolist()], dtype=torch.long) - U = torch.zeros(hkl.shape[0], dtype=torch.complex128) - has = idx >= 0 - U[has] = U_all[idx[has]] - i0 = lut.get((0, 0, 0), -1) - u0_imag = ( - float(U_all[i0].imag) if (i0 >= 0 and getattr(crystal, "U_dyn", None) is not None) else 0.0 - ) - return U, u0_imag - - -def prepare_kossel_reference( - crystal, energy_ev: float, thicknesses_A, angle_step_mrad=3.0, k_max=1.0 -): - """Bright field reference on the Lambert grid, for the pixel rendering - of the Kikuchi pattern mode (a lookup in the browser).""" - from quantem.diffraction import bloch - - master = bloch.calculate_kossel_reference( - crystal, - list(thicknesses_A), - energy_ev=energy_ev, - angle_step_mrad=angle_step_mrad, - sg_max=0.05, - k_max=k_max, - progress_bar=False, - ) - lam = np.nan_to_num(master["lambert"], nan=float(np.nanmax(master["lambert"]))) - return { - "shape": list(lam.shape), - "step": float(master["step"]), - "thicknesses": [float(t) for t in master["thicknesses"]], - "data": _f32_b64(lam), - } - - -class DiffractionSim(anywidget.AnyWidget): - """Interactive unit cell and diffraction pattern simulator. - - Parameters - ---------- - crystal : Crystal | ase.Atoms | str | None - A quantem Crystal, an ASE Atoms object, a CIF path, or the name of a - preset (see DiffractionSim.presets_available()). None starts with silicon. - energy_ev : float, default=200e3 - Beam energy. - k_max : float, default=4.0 - Largest scattering vector in the pattern (1/Angstroms). - zone_axis : sequence of 3 | None - Initial zone axis along the beam; None keeps the identity - orientation (c axis along the beam). - thickness_A, semiconv_mrad, sigma_excitation : float - Initial values of the thickness, convergence semiangle and - excitation envelope sliders. - precession_deg : float, default=0 - Precession half angle for the nanobeam pattern; intensities are - averaged over n_precession incident tilts on the precession ring - (24 by default, half of that while dragging). - pattern_range : float | None - Scattering vector at the edge of the nanobeam / CBED panel - (1/Angstroms); None uses 3, or k_max when that is smaller. - field_mrad : float, default=50 - Half angle of the Kikuchi pattern field of view. - sg_max : float, default=0.05 - Excitation error cutoff (1/Angstroms) selecting the Bloch beams; - reflections outside it take thin-slab intensities. - quality : {"fast", "medium", "fine"} - Bloch beam cap and CBED tilt sampling. - show_kikuchi : bool - Overlay the Kikuchi line pairs on the nanobeam pattern. - scaling : {"linear", "power", "log"} - Intensity scaling of the pixel renderings; "power" raises the - intensities to `power` (default 0.5). - cmap : str - Colormap of the pixel renderings, e.g. "inferno", "turbo_black", "gray". - marker_power : float, default=0.5 - Marker area scales as intensity**marker_power (0.5: sqrt intensity). - marker_size : float, default=20 - Radius in pixels of the strongest marker (at size 420), capped so - the densest net of spots does not merge. - view_from : {"detector", "gun"} - Viewpoint shared by both panels (a launch argument, no UI control). "detector" looks up the column from - the detector side: the exit face of the cell is nearest you and tilts - together with the Laue circle and Kikuchi pattern. "gun" is the - operator's view down the column; there the entrance face is nearest - and tilts opposite to the pattern (the Laue center marks where the - zone axis exits toward the detector). - mode : {"nanobeam", "cbed", "kikuchi"} - Nanobeam spots, convergent-beam disks, or the wide-angle Kikuchi - pattern. "kossel" is accepted as the old name of "kikuchi". - render : {"markers", "disks", "pixels"} - Nanobeam: markers sized by intensity, disks of the convergence - semiangle with brightness by intensity, or a pixelated pattern. - Kossel: vector lines, or the pixel lookup of the reference pattern - (compute_kossel_reference()). - n_cells : sequence of 3 int, default=(1, 1, 1) - Block of cells drawn in the left panel (up to 6 per axis). - polyhedra : bool - Draw coordination polyhedra (convex hull of the nearest neighbours) - around every species except the most numerous one; around every - atom of an elemental crystal. - show_ewald : bool, default=True - Side-view inset of the Ewald sphere on the cell panel: the reciprocal - lattice points near the x-z plane, the sphere through the origin - (z stretched), and the excited reflections in green. - size : int - Height of the panels in CSS pixels. - - Examples - -------- - >>> from quantem.widget import DiffractionSim - >>> w = DiffractionSim("Si (diamond cubic)", zone_axis=[1, 1, 0]) - >>> w - >>> w.export_html("si_110.html") # standalone page, no Python needed - """ - - _esm = _STATIC - - crystal_json = traitlets.Unicode("{}").tag(sync=True) - presets = traitlets.List(trait=traitlets.Unicode(), default_value=list(PRESETS)).tag(sync=True) - preset = traitlets.Unicode("").tag(sync=True) - energy_ev = traitlets.Float(200e3).tag(sync=True) - k_max = traitlets.Float(4.0).tag(sync=True) - orientation = traitlets.List(trait=traitlets.Float(), default_value=[1.0, 0.0, 0.0, 0.0]).tag( - sync=True - ) - mode = traitlets.Unicode("nanobeam").tag(sync=True) - - @traitlets.validate("mode") - def _accept_old_mode_name(self, proposal): - # the wide-angle mode was called "kossel" before it took the name the - # EBSD community uses for the same bands - value = str(proposal["value"]) - return "kossel" if value == "kikuchi" else value - - render = traitlets.Unicode("markers").tag(sync=True) - dynamical = traitlets.Bool(True).tag(sync=True) - thickness_A = traitlets.Float(500.0).tag(sync=True) - semiconv_mrad = traitlets.Float(2.0).tag(sync=True) - precession_deg = traitlets.Float(0.0).tag(sync=True) - n_precession = traitlets.Int(24).tag(sync=True) - sigma_excitation = traitlets.Float(0.02).tag(sync=True) - rotation_step_deg = traitlets.Float(15.0).tag(sync=True) - rotation_speed_deg = traitlets.Float(6.0).tag(sync=True) # continuous rotation buttons, deg/s - pattern_range = traitlets.Float(3.0).tag(sync=True) - field_mrad = traitlets.Float(50.0).tag(sync=True) - sg_max = traitlets.Float(0.05).tag(sync=True) - quality = traitlets.Unicode("medium").tag(sync=True) - show_kikuchi = traitlets.Bool(False).tag(sync=True) - view_from = traitlets.Unicode("detector").tag(sync=True) - scaling = traitlets.Unicode("linear").tag(sync=True) - power = traitlets.Float(0.5).tag(sync=True) - marker_power = traitlets.Float(0.5).tag(sync=True) - marker_size = traitlets.Float(20.0).tag(sync=True) - cmap = traitlets.Unicode("inferno").tag(sync=True) - vmin_pct = traitlets.Float(0.0).tag(sync=True) - vmax_pct = traitlets.Float(100.0).tag(sync=True) - show_labels = traitlets.Bool(True).tag(sync=True) - show_hkl = traitlets.Bool(True).tag(sync=True) - show_cell_axes = traitlets.Bool(True).tag(sync=True) - n_cells = traitlets.List(trait=traitlets.Int(), default_value=[1, 1, 1]).tag(sync=True) - polyhedra = traitlets.Bool(False).tag(sync=True) - show_ewald = traitlets.Bool(True).tag(sync=True) - size = traitlets.Int(420).tag(sync=True) - kossel_json = traitlets.Unicode("{}").tag(sync=True) - status = traitlets.Unicode("").tag(sync=True) - widget_version = traitlets.Unicode("0.1").tag(sync=True) - - def __init__(self, crystal=None, zone_axis=None, pattern_range=None, **kwargs): - if "n_cells" in kwargs: - kwargs["n_cells"] = [int(n) for n in kwargs["n_cells"]] - super().__init__(**kwargs) - self.pattern_range = ( - float(pattern_range) if pattern_range is not None else min(3.0, self.k_max) - ) - self._crystal = None - self._kossel_cache: dict = {} - if crystal is None: - crystal = "Si (diamond cubic)" - self.set_crystal(crystal) - if zone_axis is not None: - self.set_zone_axis(zone_axis) - self.observe(self._on_preset, names="preset") - self.observe(self._on_physics, names=["energy_ev", "k_max"]) - self.on_msg(self._on_message) - - # ------------------------------------------------------------------ - @staticmethod - def presets_available() -> list[str]: - return list(PRESETS) - - @property - def crystal(self): - return self._crystal - - def set_crystal(self, crystal) -> "DiffractionSim": - """Load a Crystal, ASE Atoms, CIF path or preset name.""" - from ase import Atoms - - from quantem.diffraction.crystal import Crystal - - preset_name = "" - if isinstance(crystal, str): - if crystal in PRESETS: - preset_name = crystal - xtl = Crystal.from_ase( - _preset_atoms(crystal), name=crystal.split(" (")[0], verbose=False - ) - else: - xtl = Crystal.from_cif(crystal, verbose=False) - elif isinstance(crystal, Atoms): - xtl = Crystal.from_ase(crystal, verbose=False) - else: - xtl = crystal - self._crystal = xtl - self.crystal_json = json.dumps(prepare_crystal(xtl, self.energy_ev, self.k_max)) - self.kossel_json = "{}" - if preset_name and self.preset != preset_name: - self.preset = preset_name - return self - - def set_zone_axis(self, zone_axis, in_plane_deg: float = 0.0) -> "DiffractionSim": - """Put a crystal direction [uvw] along the beam.""" - from quantem.diffraction.rotations import quat_from_zone_axis - - d = torch.as_tensor(zone_axis, dtype=torch.float64) @ self._crystal.lat_real - q = quat_from_zone_axis(d[None], in_plane_deg)[0] - self.orientation = [float(v) for v in q] - return self - - def compute_kossel_reference( - self, thicknesses_A=(300.0, 600.0, 1000.0), angle_step_mrad=3.0, k_max=1.0 - ): - """Precompute the reference pattern for the pixel rendering of the - Kikuchi pattern mode (about a minute for silicon at 3 mrad).""" - key = ( - round(self.energy_ev), - tuple(float(t) for t in thicknesses_A), - angle_step_mrad, - k_max, - ) - if key not in self._kossel_cache: - self.status = "computing Kossel reference pattern..." - self._kossel_cache[key] = prepare_kossel_reference( - self._crystal, self.energy_ev, thicknesses_A, angle_step_mrad, k_max - ) - self.status = "" - self.kossel_json = json.dumps(self._kossel_cache[key]) - return self - - # ------------------------------------------------------------------ - def _on_preset(self, change): - name = change["new"] - if ( - name in PRESETS - and self._crystal is not None - and self._crystal.name != name.split(" (")[0] - ): - self.set_crystal(name) - - def _on_physics(self, change): - if change["name"] == "k_max" and self.pattern_range > self.k_max: - self.pattern_range = self.k_max - if self._crystal is not None: - self.crystal_json = json.dumps( - prepare_crystal(self._crystal, self.energy_ev, self.k_max) - ) - self.kossel_json = "{}" - - def _on_message(self, widget, content, buffers): - if isinstance(content, dict) and content.get("type") == "kossel_reference": - self.compute_kossel_reference() - - # ------------------------------------------------------------------ - def state_dict(self) -> dict: - keys = [ - "crystal_json", - "presets", - "preset", - "energy_ev", - "k_max", - "orientation", - "mode", - "render", - "dynamical", - "thickness_A", - "semiconv_mrad", - "precession_deg", - "n_precession", - "sigma_excitation", - "rotation_step_deg", - "rotation_speed_deg", - "pattern_range", - "field_mrad", - "sg_max", - "quality", - "show_kikuchi", - "view_from", - "scaling", - "power", - "marker_power", - "marker_size", - "cmap", - "vmin_pct", - "vmax_pct", - "show_labels", - "show_hkl", - "show_cell_axes", - "n_cells", - "polyhedra", - "show_ewald", - "size", - "kossel_json", - "status", - "widget_version", - ] - return {k: getattr(self, k) for k in keys} - - def export_html( - self, path, title: str | None = None, presets: list[str] | None = None - ) -> pathlib.Path: - """Write a standalone HTML page of the widget with its current state. - - The page carries the compiled widget, the crystal data and (when - computed) the Kossel reference, and runs entirely in the browser: - rotating the cell, changing thickness or mode needs no Python. - `presets` lists additional crystals to embed so the crystal menu - works offline (each adds its reflection list to the file). - """ - state = self.state_dict() - embedded = {} - for name in presets or []: - from quantem.diffraction.crystal import Crystal - - xtl = Crystal.from_ase(_preset_atoms(name), name=name.split(" (")[0], verbose=False) - embedded[name] = prepare_crystal(xtl, self.energy_ev, self.k_max) - if self.preset and self.preset not in embedded: - embedded[self.preset] = json.loads(self.crystal_json) - state["embedded_presets"] = embedded - bundle = _STATIC.read_text() - title = title or f"quantEM diffraction simulator: {self._crystal.name}" - html = _standalone_html(bundle, state, title) - out = pathlib.Path(path) - out.write_text(html) - return out - - -def _standalone_html(bundle_js: str, state: dict, title: str) -> str: - state_json = json.dumps(state).replace("&]", "", title) - return f""" - - - - -{safe_title} - - - -
- - - -""" diff --git a/widget/tsconfig.json b/widget/tsconfig.json deleted file mode 100644 index 8b4afe790..000000000 --- a/widget/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "include": [ - "js" - ], - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "lib": [ - "ES2020", - "DOM", - "DOM.Iterable" - ], - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - } -} \ No newline at end of file From e6f2d7e8b30da612d74e30c8184826d4cc554a68 Mon Sep 17 00:00:00 2001 From: cophus Date: Tue, 6 Oct 2026 17:24:26 -0700 Subject: [PATCH 33/36] test fix --- tests/diffraction/test_two_phase_map.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/diffraction/test_two_phase_map.py b/tests/diffraction/test_two_phase_map.py index f774f50da..efa4b90f9 100644 --- a/tests/diffraction/test_two_phase_map.py +++ b/tests/diffraction/test_two_phase_map.py @@ -88,9 +88,21 @@ def test_two_phase_map(): err_a = misorientation_angle_deg( q_alpha, oms[0].quats[:, band[1] + 1 :, 0].reshape(-1, 4), ti_a.sym_quats ).numpy() - err_b = misorientation_angle_deg( - q_beta, oms[1].quats[:, : band[0], 0].reshape(-1, 4), ti_b.sym_quats - ).numpy() + # along [111], beta and its 60 degree twin about [111] give identical + # kinematical patterns, so either is a correct match; which one wins is + # decided by round-off and differs between platforms + q_beta_twin = qmult( + q_beta, + quat_from_axis_angle( + torch.tensor([1.0, 1.0, 1.0], dtype=torch.float64) / np.sqrt(3), + torch.tensor(np.pi / 3, dtype=torch.float64), + ), + ) + q_found = oms[1].quats[:, : band[0], 0].reshape(-1, 4) + err_b = np.minimum( + misorientation_angle_deg(q_beta, q_found, ti_b.sym_quats).numpy(), + misorientation_angle_deg(q_beta_twin, q_found, ti_b.sym_quats).numpy(), + ) assert np.median(err_a) < 1.0 assert np.median(err_b) < 1.0 From a6c306d5d35b4f4f7f9c46de900c394f23ec9302 Mon Sep 17 00:00:00 2001 From: Colin Ophus Date: Wed, 7 Oct 2026 15:20:22 -0700 Subject: [PATCH 34/36] RMC: shell pair correlations and longer Warren-Cowley range Add ReverseMonteCarlo.shell_correlations and plot_shell_correlations: the pair count of every species pair per neighbour shell divided by the count of a random alloy at the supercell composition (1 = random), stacked panels on shared distance and ratio axes. plot_warren_cowley now runs to the same default radius (five lattice parameters) so both plots reach their random limit. Shells along <100>, <110> and <111> are labelled above the plots. warren_cowley and shell_correlations share one pair probability calculation, now limited by radius as well as shell count. Co-Authored-By: Claude Opus 5.5 --- .../diffraction/reverse_monte_carlo.py | 256 +++++++++++++++--- tests/diffraction/test_reverse_monte_carlo.py | 29 ++ 2 files changed, 253 insertions(+), 32 deletions(-) diff --git a/src/quantem/diffraction/reverse_monte_carlo.py b/src/quantem/diffraction/reverse_monte_carlo.py index 67ee3bf2d..539862b4d 100644 --- a/src/quantem/diffraction/reverse_monte_carlo.py +++ b/src/quantem/diffraction/reverse_monte_carlo.py @@ -1894,7 +1894,7 @@ def background_images(self) -> list[np.ndarray]: diffuse = self.model_images(diffuse_only=True) return [f - d for f, d in zip(full, diffuse)] - def warren_cowley(self, n_shells: int = 6) -> dict: + def warren_cowley(self, n_shells: int | None = 6, max_radius: float | None = None) -> dict: """Warren-Cowley alpha for every species pair over the first neighbour shells. ``alpha[shell, s, t] = 1 - P(t | s) / c_t`` for unlike pairs and @@ -1905,7 +1905,11 @@ def warren_cowley(self, n_shells: int = 6) -> dict: Parameters ---------- n_shells : int, optional - Number of neighbour shells. Default 6. + Largest number of neighbour shells. Default 6; None keeps every shell within + `max_radius`. + max_radius : float, optional + Largest neighbour distance in A. Default None (no limit beyond half the + supercell). Returns ------- @@ -1913,6 +1917,23 @@ def warren_cowley(self, n_shells: int = 6) -> dict: "radius": (n_shells,) shell radii in A; "alpha": (n_shells, K, K) Warren-Cowley parameters; "species": the K species in index order. """ + radii, prob = self._pair_probabilities(n_shells=n_shells, max_radius=max_radius) + c = self.concentrations + K = len(self.species) + alpha = np.zeros_like(prob) + for s in range(K): + for t in range(K): + p = prob[:, s, t] + alpha[:, s, t] = (p - c[t]) / (1 - c[t]) if s == t else 1 - p / c[t] + return dict(radius=radii, alpha=alpha, species=list(self.species)) + + def _pair_probabilities( + self, n_shells: int | None = None, max_radius: float | None = None + ) -> tuple[np.ndarray, np.ndarray]: + """Shell radii (A) and ``P(t | s)`` (n_shells, K, K): the fraction of the sites in each + neighbour shell of an ``s`` atom that hold a ``t`` atom, averaged over every ``s`` atom + of the supercell (periodic boundaries). Shells are kept up to `n_shells` and up to + `max_radius` (A), whichever ends first, and never beyond half the supercell.""" d = self.grid_divisor n = self.cells * d xs = self.site_x // self.refine @@ -1929,19 +1950,73 @@ def warren_cowley(self, n_shells: int = 6) -> dict: ss = np.real(np.fft.ifftn(fsite * np.conj(fsite))) v = np.stack(np.meshgrid(*(np.fft.fftfreq(n, 1 / n),) * 3, indexing="ij"), -1) r = np.linalg.norm(v, axis=-1) / d * self.lattice_parameter - valid = ss > 0.5 - radii = np.unique(np.round(r[valid], 4))[1 : n_shells + 1] - shells = [valid & (np.abs(r - rad) < 1e-3) for rad in radii] - c = self.concentrations - alpha = np.zeros((len(radii), K, K)) + half_box = self.cells * self.lattice_parameter / 2 + r_max = half_box if max_radius is None else min(max_radius, half_box) + valid = (ss > 0.5) & (r <= r_max + 1e-6) + radii = np.unique(np.round(r[valid], 4))[1:] + if n_shells is not None: + radii = radii[:n_shells] + shell = np.searchsorted(radii, np.round(r, 4)) + inside = valid & (shell < len(radii)) + inside[inside] = radii[shell[inside]] == np.round(r[inside], 4) + index = shell[inside] + + def shell_sums(a): + return np.bincount(index, weights=a[inside], minlength=len(radii)) + + prob = np.zeros((len(radii), K, K)) for s in range(K): - cs_site = np.real(np.fft.ifftn(focc[s] * np.conj(fsite))) + cs_site = shell_sums(np.real(np.fft.ifftn(focc[s] * np.conj(fsite)))) for t in range(K): - cst = np.real(np.fft.ifftn(focc[s] * np.conj(focc[t]))) - for k, m in enumerate(shells): - p = cst[m].sum() / cs_site[m].sum() - alpha[k, s, t] = (p - c[t]) / (1 - c[t]) if s == t else 1 - p / c[t] - return dict(radius=radii, alpha=alpha, species=list(self.species)) + cst = shell_sums(np.real(np.fft.ifftn(focc[s] * np.conj(focc[t])))) + prob[:, s, t] = cst / cs_site + return radii, prob + + def shell_correlations( + self, n_shells: int | None = None, max_radius: float | None = None + ) -> dict: + """Pair correlation of every species pair per neighbour shell, relative to a random + alloy. + + ``ratio[shell, s, t] = P(t | s) / c_t``, the number of ``s``-``t`` pairs in the shell + divided by the number expected for a random arrangement at the same composition. It + is symmetric in ``s`` and ``t``, equals 1 for a random alloy, is above 1 for pairs that + are favoured and below 1 for pairs that are avoided. ``c_t`` is the composition of + the supercell itself. For an unlike pair, ``ratio = 1 - alpha`` with the Warren-Cowley + parameter of :meth:`warren_cowley`, up to the rounding of the composition to whole + atoms. + + Parameters + ---------- + n_shells : int, optional + Largest number of neighbour shells. Default None (every shell within + `max_radius`). + max_radius : float, optional + Largest neighbour distance in A. Default None: five lattice parameters, far + enough for the short-range order to decay. Never beyond half the supercell. + + Returns + ------- + dict + "radius": (n_shells,) shell radii in A; "shell": bond-vector labels in units of a + (e.g. "1/2<111>"), or None for site lattices other than BCC; "ratio": + (n_shells, K, K) pair correlations relative to random; "species": the K species + in index order. + """ + if max_radius is None: + max_radius = 5 * self.lattice_parameter + radii, prob = self._pair_probabilities(n_shells=n_shells, max_radius=max_radius) + # the supercell's own composition, so that the ratio is exactly symmetric + counts = np.bincount(self.species_index, minlength=len(self.species)) + ratio = prob / (counts / counts.sum())[None, None, :] + shell = None + if self.grid_divisor == 2: + reach = int(np.ceil(radii[-1] / (self.lattice_parameter / 2))) + 1 + offsets = self._shells(len(radii), reach=reach) + dist = [np.linalg.norm(o[0]) * self.lattice_parameter / 2 for o in offsets] + if len(offsets) == len(radii) and np.allclose(dist, radii, atol=1e-3): + shell = [self._shell_label(o[0]) for o in offsets] + return dict(radius=radii, shell=shell, ratio=ratio, species=list(self.species)) def _u2(self, disp: np.ndarray) -> np.ndarray: """Squared displacement (A^2) of each displacement vector (..., 3).""" @@ -2190,18 +2265,33 @@ def plot_fit( **kwargs, ) - def plot_warren_cowley(self, n_shells: int = 8): - """Warren-Cowley alpha against neighbour distance (< 0 unlike neighbours preferred, > 0 - like); one curve for a binary site, one per species pair otherwise.""" + def plot_warren_cowley(self, max_radius: float | None = None): + """Warren-Cowley alpha against neighbour distance (0 = random, < 0 unlike neighbours + preferred, > 0 like); one curve for a binary site, one per species pair otherwise. + + Parameters + ---------- + max_radius : float, optional + Largest neighbour distance in A. Default None: five lattice parameters. + + Returns + ------- + fig, ax + """ import matplotlib.pyplot as plt - sro = self.warren_cowley(n_shells) + max_radius = self._default_shell_radius(max_radius) + sro = self.warren_cowley(n_shells=None, max_radius=max_radius) + sc = self.shell_correlations(max_radius=max_radius) + labelled = self._direction_shells(sc["radius"], sc["shell"]) K = len(self.species) - fig, ax = plt.subplots(figsize=(5.5, 4)) + fig, ax = plt.subplots(figsize=(8.0, 3.6)) ax.axhline(0, color="0.6", lw=0.8) + for radius, _ in labelled: + ax.axvline(radius, color="0.9", lw=0.6, zorder=0) if K == 2: # a binary site has one alpha for every pair - ax.plot(sro["radius"], sro["alpha"][:, 0, 0], "o-", ms=4, color="k") - ax.set_title(f"{self.species[0]}-{self.species[1]}") + ax.plot(sro["radius"], sro["alpha"][:, 0, 0], "o-", ms=3, lw=1.2, color="tab:blue") + ax.set_ylabel(f"Warren-Cowley alpha, {self.species[0]}-{self.species[1]}") else: for s_ in range(K): for t in range(s_, K): @@ -2209,15 +2299,112 @@ def plot_warren_cowley(self, n_shells: int = 8): sro["radius"], sro["alpha"][:, s_, t], "o-" if s_ == t else "s--", - ms=4, + ms=3, + lw=1.2, label=f"{self.species[s_]}-{self.species[t]}", ) ax.legend(fontsize=8) + ax.set_ylabel("Warren-Cowley alpha") ax.set_xlabel("neighbour distance (A)") - ax.set_ylabel("Warren-Cowley alpha") + ax.set_xlim(0, sro["radius"][-1] * 1.02) fig.tight_layout() + self._label_direction_shells(fig, ax, labelled, sro["radius"][-1] * 1.02) return fig, ax + def _default_shell_radius(self, max_radius: float | None) -> float: + """Largest neighbour distance of the shell plots: `max_radius`, or five lattice parameters.""" + return 5 * self.lattice_parameter if max_radius is None else max_radius + + @staticmethod + def _direction_shells(radii, labels) -> list[tuple[float, str]]: + """(radius, label) of the shells along <100>, <110> and <111>, from the bond-vector + labels of :meth:`shell_correlations`; empty when there are no labels.""" + if labels is None: + return [] + out = [] + for radius, label in zip(radii, labels): + digits = [int(c) for c in label.split("<")[1].rstrip(">")] + if ( + digits[1:] == [0, 0] + or (digits[0] == digits[1] and digits[2] == 0) + or digits[0] == digits[1] == digits[2] + ): + out.append((radius, label)) + return out + + @staticmethod + def _label_direction_shells(fig, ax, labelled, span: float) -> None: + """Write shell labels above `ax`, stacked in rows so that none overlap, and make room + for them at the top of the figure. Call after ``tight_layout``.""" + last: list[float] = [] + for radius, label in labelled: + width = 0.012 * span * len(label) + row = next((k for k, x in enumerate(last) if radius - width / 2 > x), len(last)) + if row == len(last): + last.append(0.0) + last[row] = radius + width / 2 + ax.annotate( + label, + (radius, 1.0), + xycoords=("data", "axes fraction"), + xytext=(0, 3 + 11 * row), + textcoords="offset points", + ha="center", + va="bottom", + fontsize=8, + annotation_clip=False, + ) + if last: + height = fig.get_size_inches()[1] + fig.subplots_adjust(top=fig.subplotpars.top - (0.03 + 0.15 * len(last)) / height) + + def plot_shell_correlations( + self, + max_radius: float | None = None, + panel_size: tuple[float, float] = (8.0, 2.2), + ): + """Pair correlation of every species pair against neighbour distance, relative to a + random alloy (1 = random), one panel per pair stacked on shared distance and ratio axes. The + shells along <100>, <110> and <111> are labelled above the top panel (BCC only). + + Parameters + ---------- + max_radius : float, optional + Largest neighbour distance in A. Default None: five lattice parameters. + panel_size : tuple of float, optional + Width and height of each panel in inches. Default (8.0, 2.2). + + Returns + ------- + fig, axs + The figure and the array of panels, top to bottom. + """ + import matplotlib.pyplot as plt + + sc = self.shell_correlations(max_radius=self._default_shell_radius(max_radius)) + K = len(self.species) + pairs = [(s, t) for s in range(K) for t in range(s, K)] + fig, axs = plt.subplots( + len(pairs), + 1, + sharex=True, + sharey=True, + figsize=(panel_size[0], panel_size[1] * len(pairs)), + ) + axs = np.atleast_1d(axs) + labelled = self._direction_shells(sc["radius"], sc["shell"]) + for ax, (s, t) in zip(axs, pairs): + ax.axhline(1, color="0.6", lw=0.8) + for radius, _ in labelled: + ax.axvline(radius, color="0.9", lw=0.6, zorder=0) + ax.plot(sc["radius"], sc["ratio"][:, s, t], "o-", ms=3, lw=1.2, color="tab:blue") + ax.set_ylabel(f"{self.species[s]}-{self.species[t]}\n/ random") + axs[-1].set_xlabel("neighbour distance (A)") + axs[-1].set_xlim(0, sc["radius"][-1] * 1.02) + fig.tight_layout() + self._label_direction_shells(fig, axs[0], labelled, sc["radius"][-1] * 1.02) + return fig, axs + def plot_diffuse_sections(self, extent: float = 2.0, normals=((0, 0, 1), (1, -1, 0))): """Symmetrized diffuse intensity of the supercell in Laue units (1 = random alloy) on reciprocal-lattice planes through the origin. Maxima at special points name the order @@ -2243,14 +2430,24 @@ def plot_diffuse_sections(self, extent: float = 2.0, normals=((0, 0, 1), (1, -1, fig.tight_layout() return fig, axs - def _shells(self, n_shells: int): - """Neighbour offsets of the BCC site lattice (site units, a / 2) grouped by distance.""" - r = np.arange(-4, 5) + def _shells(self, n_shells: int, reach: int = 4): + """Neighbour offsets of the BCC site lattice (site units, a / 2) grouped by distance, + complete for every shell within `reach` site units.""" + r = np.arange(-reach, reach + 1) v = np.stack(np.meshgrid(r, r, r, indexing="ij"), -1).reshape(-1, 3) bcc = np.all(v % 2 == 0, axis=1) | np.all(v % 2 == 1, axis=1) v = v[bcc & np.any(v != 0, axis=1)] d2 = (v**2).sum(1) - return [v[d2 == d] for d in np.unique(d2)[:n_shells]] + return [v[d2 == d] for d in np.unique(d2[d2 <= reach**2])[:n_shells]] + + @staticmethod + def _shell_label(offset) -> str: + """Bond vector of a BCC neighbour shell in units of a, e.g. "1/2<111>" or "<100>", from + one offset in site units (a / 2).""" + v = np.sort(np.abs(np.asarray(offset)))[::-1] + if np.all(v % 2 == 0): + return "<" + "".join(str(int(x)) for x in v // 2) + ">" + return "1/2<" + "".join(str(int(x)) for x in v) + ">" def displacement_correlations(self, n_shells: int = 6) -> dict: """Displacement short-range order per neighbour shell. @@ -2285,12 +2482,7 @@ def displacement_correlations(self, n_shells: int = 6) -> dict: xc = self.site_x // self.refine out = dict(radius=[], shell=[], longitudinal=[], transverse=[]) for offs in self._shells(n_shells): - v = np.sort(np.abs(offs[0]))[::-1] - out["shell"].append( - "<" + "".join(str(int(x)) for x in v // 2) + ">" - if np.all(v % 2 == 0) - else "1/2<" + "".join(str(int(x)) for x in v) + ">" - ) + out["shell"].append(self._shell_label(offs[0])) rhat = offs / np.linalg.norm(offs, axis=1, keepdims=True) nb = np.stack([self._site_lookup[tuple(np.mod(xc + o, nc).T)] for o in offs], 1) ui = u[:, None, :] diff --git a/tests/diffraction/test_reverse_monte_carlo.py b/tests/diffraction/test_reverse_monte_carlo.py index 54c928829..c29769193 100644 --- a/tests/diffraction/test_reverse_monte_carlo.py +++ b/tests/diffraction/test_reverse_monte_carlo.py @@ -316,3 +316,32 @@ def test_fit_geometry_on_synthetic_lattice(tmp_path): out.set_envelope(envelope) assert out.envelope == envelope assert np.isfinite(out._update_residual()) + + +def test_shell_correlations_match_warren_cowley(rmc): + sc = rmc.shell_correlations(n_shells=4) + sro = rmc.warren_cowley(n_shells=4) + ratio = sc["ratio"] + np.testing.assert_allclose(sc["radius"], sro["radius"]) + # pair counts are symmetric, and an unlike pair has ratio = 1 - alpha + np.testing.assert_allclose(ratio, np.swapaxes(ratio, 1, 2), atol=1e-9) + np.testing.assert_allclose(ratio[:, 0, 1], 1 - sro["alpha"][:, 0, 1], atol=0.02) + # a random arrangement sits near 1 in every shell + assert np.abs(ratio - 1).max() < 0.2 + assert sc["shell"][:3] == ["1/2<111>", "<100>", "<110>"] + # the default radius reaches five lattice parameters, or half the supercell + far = rmc.shell_correlations() + assert far["radius"][-1] <= min(5, rmc.cells / 2) * rmc.lattice_parameter + 1e-6 + assert len(far["shell"]) == len(far["radius"]) + + +def test_plot_shell_correlations(rmc): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + fig, axs = rmc.plot_shell_correlations() + K = len(rmc.species) + assert len(axs) == K * (K + 1) // 2 + plt.close(fig) From 56997f5e24c2d26c483e1b954e4d18b39f6b5c36 Mon Sep 17 00:00:00 2001 From: smribet Date: Thu, 8 Oct 2026 09:16:27 -0700 Subject: [PATCH 35/36] fit probe circle option --- .../core/datastructures/dataset4dstem.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/quantem/core/datastructures/dataset4dstem.py b/src/quantem/core/datastructures/dataset4dstem.py index 4b2c80560..d61584446 100644 --- a/src/quantem/core/datastructures/dataset4dstem.py +++ b/src/quantem/core/datastructures/dataset4dstem.py @@ -10,6 +10,9 @@ from quantem.core.datastructures.dataset2d import Dataset2d from quantem.core.datastructures.dataset4d import Dataset4d from quantem.core.datastructures.polar4dstem import dataset4dstem_polar_transform +from quantem.core.utils.diffractive_imaging_utils import ( + fit_probe_circle as _fit_probe_circle, +) from quantem.core.utils.validators import ensure_valid_array from quantem.core.visualization import show_2d from quantem.core.visualization.visualization_utils import ScalebarConfig @@ -286,6 +289,42 @@ def get_dp_mean(self, attach: bool = True) -> Dataset2d: return dp_mean_dataset + def fit_probe_circle( + self, + array: NDArray | None = None, + threshold: float | None = None, + show: bool = True, + ) -> tuple[float, float, float]: + """Fit a circle to the probe in a diffraction pattern. + + Parameters + ---------- + array : NDArray | None, optional + 2D diffraction pattern to fit. If None, uses this dataset's mean + diffraction pattern, computing it if needed. + threshold : float | None, optional + Threshold for binarizing the diffraction pattern. If None, Otsu's method + is used. + show : bool, optional + Whether to display the fitted circle, by default True. + + Returns + ------- + tuple[float, float, float] + Probe center in diffraction-pattern row and column coordinates + (probe_qy0, probe_qx0), followed by the fitted radius. + """ + if array is None: + dp_mean = ( + self._dp_mean if hasattr(self, "_dp_mean") else self.get_dp_mean(attach=False) + ) + array = dp_mean.array + + if array.ndim != 2: + raise ValueError(f"Expected a 2D diffraction pattern, got shape {array.shape}.") + + return _fit_probe_circle(array, threshold=threshold, show=show) + @property def dp_max(self) -> Dataset2d: """ From 44d8ec8e8c403be8254c5ab328b777844cdaed79 Mon Sep 17 00:00:00 2001 From: smribet Date: Thu, 8 Oct 2026 09:25:13 -0700 Subject: [PATCH 36/36] bug fix --- src/quantem/core/datastructures/dataset4dstem.py | 11 ++++------- .../core/utils/diffractive_imaging_utils.py | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/quantem/core/datastructures/dataset4dstem.py b/src/quantem/core/datastructures/dataset4dstem.py index d61584446..32ef023c0 100644 --- a/src/quantem/core/datastructures/dataset4dstem.py +++ b/src/quantem/core/datastructures/dataset4dstem.py @@ -291,7 +291,7 @@ def get_dp_mean(self, attach: bool = True) -> Dataset2d: def fit_probe_circle( self, - array: NDArray | None = None, + array: NDArray | Dataset2d | None = None, threshold: float | None = None, show: bool = True, ) -> tuple[float, float, float]: @@ -299,9 +299,9 @@ def fit_probe_circle( Parameters ---------- - array : NDArray | None, optional - 2D diffraction pattern to fit. If None, uses this dataset's mean - diffraction pattern, computing it if needed. + array : NDArray | Dataset2d | None, optional + 2D diffraction pattern to fit, either as an array or a Dataset2d. If + None, uses this dataset's mean diffraction pattern, computing it if needed. threshold : float | None, optional Threshold for binarizing the diffraction pattern. If None, Otsu's method is used. @@ -320,9 +320,6 @@ def fit_probe_circle( ) array = dp_mean.array - if array.ndim != 2: - raise ValueError(f"Expected a 2D diffraction pattern, got shape {array.shape}.") - return _fit_probe_circle(array, threshold=threshold, show=show) @property diff --git a/src/quantem/core/utils/diffractive_imaging_utils.py b/src/quantem/core/utils/diffractive_imaging_utils.py index 0959c1b6c..ffb6d75fb 100644 --- a/src/quantem/core/utils/diffractive_imaging_utils.py +++ b/src/quantem/core/utils/diffractive_imaging_utils.py @@ -1,4 +1,6 @@ -from typing import Optional, Tuple +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional, Tuple import matplotlib.pyplot as plt import numpy as np @@ -8,21 +10,29 @@ from quantem.core.utils.filter import otsu_threshold +if TYPE_CHECKING: + from quantem.core.datastructures.dataset2d import Dataset2d + def fit_probe_circle( - img: np.ndarray, threshold: Optional[float] = None, show: bool = True + img: np.ndarray | Dataset2d, threshold: Optional[float] = None, show: bool = True ) -> Tuple[float, float, float]: """ Fit a circle to the probe shape in an image. Args: - img (np.ndarray): Input image containing the probe. + img (np.ndarray | Dataset2d): Input diffraction pattern containing the probe. threshold (Optional[float]): Threshold for binarization. If None, Otsu's method is used. show (bool): Whether to display the fitted circle. Default is True. Returns: Tuple[float, float, float]: Center coordinates (xc, yc) and radius R of the fitted circle. """ + if not isinstance(img, np.ndarray) and hasattr(img, "array"): + img = img.array + if img.ndim != 2: + raise ValueError(f"Expected a 2D diffraction pattern, got shape {img.shape}.") + if threshold is None: threshold = otsu_threshold(img) binary = ndi.binary_closing(img > threshold, iterations=2)