From c4073eb48ee51b9f817f86ea78eec32ac7a3742b Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Sun, 12 Jul 2026 15:56:20 -0400 Subject: [PATCH 01/37] feat: first version of delta/beta profile --- orgui/datautils/xrayutils/CTRcalc.py | 39 +++- orgui/datautils/xrayutils/CTRfilm.py | 41 +++++ orgui/datautils/xrayutils/CTRoptics.py | 245 +++++++++++++++++++++++++ orgui/datautils/xrayutils/CTRuc.py | 134 +++++++++++++- orgui/datautils/xrayutils/__init__.py | 1 + 5 files changed, 449 insertions(+), 11 deletions(-) create mode 100644 orgui/datautils/xrayutils/CTRoptics.py diff --git a/orgui/datautils/xrayutils/CTRcalc.py b/orgui/datautils/xrayutils/CTRcalc.py index e453877..9f621d7 100644 --- a/orgui/datautils/xrayutils/CTRcalc.py +++ b/orgui/datautils/xrayutils/CTRcalc.py @@ -40,6 +40,7 @@ from .CTRuc import WaterModel, UnitCell from .CTRfilm import EpitaxyInterface, Film, PoissonSurface +from .CTRoptics import add_structural_to_sampled_profile, combine_profiles from .CTRdistributions import PoissonProfile, SkellamProfile # noqa: F401 from .CTRstacking import ( # noqa: F401 LayerCycle, @@ -84,7 +85,8 @@ def __init__(self, uc_bulk, *uc_surface, **keyargs): self.enable_uc_stacking = keyargs.get("enable_stacking", False) if not self.enable_uc_stacking: for uc in self.uc_surface_list: - if isinstance(uc, Film | PoissonSurface | EpitaxyInterface): + stackable = Film | PoissonSurface | EpitaxyInterface | WaterModel + if isinstance(uc, stackable): self.enable_uc_stacking = True break @@ -1079,6 +1081,41 @@ def zDensity_G(self, z, h, k): rho += uc.zDensity_G(z, h, k) * w return rho + def optical_profile(self): + """Return the combined homogeneous optical profile of this crystal. + + Crystal surface weights are applied directly to ``delta`` and ``beta``. + Water and other continuum-only components are not yet supported. + + :returns: + C-contiguous ``(N, 3)`` array with columns ``z`` in Angstrom, + ``delta``, and ``beta``. + :rtype: numpy.ndarray + :raises NotImplementedError: + If a component does not expose an atomistic optical profile. + """ + if self.enable_uc_stacking: + self.apply_stacking() + profiles = [self.uc_bulk.optical_profile_asbulk()] + water_profiles = [] + for component, weight in zip(self.uc_surface_list, self.weights): + if not hasattr(component, "optical_profile"): + raise NotImplementedError( + f"{type(component).__name__} has no atomistic optical profile." + ) + profile = component.optical_profile().copy() + profile[:, 1:] *= weight + if isinstance(component, WaterModel): + water_profiles.append(profile) + else: + profiles.append(profile) + structural_profile = combine_profiles(*profiles) + if not water_profiles: + return structural_profile + return add_structural_to_sampled_profile( + structural_profile, *water_profiles + ) + def toRODStr(self): s = f"E = {self.uc_bulk._E * 1e-3:.5f} keV\n" for i, w, uc in zip(self.uc_stacking, self.weights, self.uc_surface_list): diff --git a/orgui/datautils/xrayutils/CTRfilm.py b/orgui/datautils/xrayutils/CTRfilm.py index 4806cd9..701657f 100644 --- a/orgui/datautils/xrayutils/CTRfilm.py +++ b/orgui/datautils/xrayutils/CTRfilm.py @@ -35,6 +35,7 @@ from .CTRutil import _ensure_contiguous, next_skip_comment, LinearFitFunctions +from .CTRoptics import combine_profiles from .CTRuc import UnitCell, ctr_accel_enabled from .CTRdistributions import ( @@ -759,6 +760,20 @@ def zDensity_G(self, z, h, k): rho += uc_b.zDensity_G(z, h, k) return rho + def optical_profile(self): + """Return the combined homogeneous optical profile of the interface. + + :returns: + C-contiguous ``(N, 3)`` array with columns ``z``, ``delta``, and + ``beta``. + :rtype: numpy.ndarray + """ + if np.any(self._basis_created != self.basis): + self.createInterfaceCells() + profiles = [uc.optical_profile() for uc in self.top_layers] + profiles.extend(uc.optical_profile() for uc in self.bottom_layers) + return combine_profiles(*profiles) + def addFitParameter(self, indexarray, limits=(-np.inf, np.inf), **kwarg): """to assign multiple unitcells with the same fitparameter, provide list of unitcell names as kwarg `unitcell` @@ -1274,6 +1289,18 @@ def zDensity_G(self, z, h, k): rho += uc.zDensity_G(z, h, k) return rho + def optical_profile(self): + """Return the combined homogeneous optical profile of the Film. + + :returns: + C-contiguous ``(N, 3)`` array with columns ``z``, ``delta``, and + ``beta``. + :rtype: numpy.ndarray + """ + if np.any(self._basis_created != self.basis): + self.createLayers() + return combine_profiles(*(uc.optical_profile() for uc in self.layer_ucs)) + def addFitParameter(self, indexarray, limits=(-np.inf, np.inf), **kwarg): """to assign multiple unitcells with the same fitparameter, provide list of unitcell names as kwarg `unitcell` @@ -1754,6 +1781,20 @@ def zDensity_G(self, z, h, k): rho += uc.zDensity_G(z, h, k) return rho + def optical_profile(self): + """Return the homogeneous optical profile of the Poisson surface. + + :returns: + C-contiguous ``(N, 3)`` array with columns ``z``, ``delta``, and + ``beta``. + :rtype: numpy.ndarray + """ + if np.any(self._basis_created != self.basis): + self.createLayers() + return combine_profiles( + *(uc.optical_profile() for uc in self.layer_ucs if uc.coherentDomainMatrix) + ) + def addFitParameter(self, indexarray, limits=(-np.inf, np.inf), **kwarg): """to assign multiple unitcells with the same fitparameter, provide list of unitcell names as kwarg `unitcell` diff --git a/orgui/datautils/xrayutils/CTRoptics.py b/orgui/datautils/xrayutils/CTRoptics.py new file mode 100644 index 0000000..f643178 --- /dev/null +++ b/orgui/datautils/xrayutils/CTRoptics.py @@ -0,0 +1,245 @@ +"""Optical constants derived from CTR structural models.""" + +import numpy as np + +from .CTRutil import atomic_number + + +def optical_profile(unit_cell, include_coherent_domains=True): + """Return homogeneous optical constants for every layer and domain. + + The returned C-contiguous ``(N, 3)`` float64 array has columns ``z`` in + Angstrom, ``delta``, and ``beta``. Each row is a domain-transformed layer + contribution; coherent-domain occupancy is already applied to ``delta`` + and ``beta`` in the convention ``n = 1-delta-i*beta``. + + :param UnitCell unit_cell: + Unit cell with energy-dependent scattering factors populated by + :meth:`UnitCell.setEnergy`. + :param bool include_coherent_domains: + Apply the unit cell's coherent-domain transforms and occupancies. + :returns: + One row for every structural layer and coherent domain. + :rtype: numpy.ndarray + :raises ValueError: + If the unit-cell energy has not populated anomalous scattering factors. + """ + if not hasattr(unit_cell, "_E") or not hasattr(unit_cell, "f"): + raise ValueError( + "Set the UnitCell energy before calculating optical constants." + ) + + layers = unit_cell.split_in_layers(ordered=True) + layer_ids = tuple(layers) + if not layer_ids: + return np.empty((0, 3), dtype=np.float64) + + if hasattr(unit_cell, "_optical_layer_origin"): + if len(layer_ids) != 1: + raise ValueError("Layer optical metadata requires exactly one layer.") + origins = np.asarray([unit_cell._optical_layer_origin], dtype=np.float64) + thickness_fraction = np.asarray( + [unit_cell._optical_layer_thickness_fraction], dtype=np.float64 + ) + else: + origins = [] + for layer_id in layer_ids: + origin = unit_cell.layerpos.get(float(layer_id)) + if origin is None: + atom_mask = unit_cell.basis[:, 7] == layer_id + origin = float(np.mean(unit_cell.basis[atom_mask, 3])) + origins.append(float(origin)) + origins = np.asarray(origins, dtype=np.float64) + thickness_fraction = np.empty(len(layer_ids), dtype=np.float64) + for index in range(len(layer_ids)): + spacing = origins[(index + 1) % len(layer_ids)] - origins[index] + while spacing <= 0.0: + spacing += 1.0 + thickness_fraction[index] = spacing + + wavelength = 12.398419843320026 / (unit_cell._E * 1e-3) + scale = 2.8179403262e-5 * wavelength**2 / (2.0 * np.pi) + if include_coherent_domains: + domain_matrices = unit_cell.coherentDomainMatrix + domain_occupancies = unit_cell.coherentDomainOccupancy + else: + domain_matrices = [np.hstack((np.identity(3), np.zeros((3, 1))))] + domain_occupancies = [1.0] + profile = np.empty((len(layer_ids) * len(domain_matrices), 3), dtype=np.float64) + row = 0 + for layer_index, (layer_id, layer_cell) in enumerate(layers.items()): + atomic_numbers = np.asarray( + [atomic_number(name) for name in layer_cell.names], dtype=np.float64 + ) + forward_factor = np.sum( + layer_cell.basis[:, 6] + * (atomic_numbers + layer_cell.f[:, 11] + 1j * layer_cell.f[:, 12]) + ) + layer_volume = unit_cell.volume * thickness_fraction[layer_index] + optical_density = scale * forward_factor / layer_volume + origin_vector = np.array([0.0, 0.0, origins[layer_index]]) + for matrix, occupancy in zip( + domain_matrices, domain_occupancies + ): + domain_matrix = unit_cell.R_mat_inv @ matrix[:, :-1] @ unit_cell.R_mat + domain_origin = domain_matrix @ origin_vector + matrix[:, -1] + contribution = float(occupancy) * optical_density + profile[row] = [ + domain_origin[2] * unit_cell.a[2], + contribution.real, + contribution.imag, + ] + row += 1 + return profile + + +def optical_profile_asbulk(unit_cell, noUC=30): + """Return the semi-infinite bulk approximation used by CTR densities. + + This mirrors :meth:`UnitCell.zDensity_G_asbulk`: the native bulk unit + cell is repeated towards negative z, with ``noUC`` cells explicitly + represented. Coherent-domain transforms are intentionally not used for + this bulk baseline. + + :param UnitCell unit_cell: + Bulk unit cell with energy-dependent scattering factors populated. + :param int noUC: + Number of unit cells to represent below the termination. + :returns: + C-contiguous ``(N, 3)`` array with columns ``z`` in Angstrom, + ``delta``, and ``beta``. + :rtype: numpy.ndarray + :raises ValueError: + If ``noUC`` is not a positive integer. + """ + if not isinstance(noUC, int | np.integer) or noUC <= 0: + raise ValueError("noUC must be a positive integer.") + + unit_profile = optical_profile(unit_cell, include_coherent_domains=False) + + profiles = [] + for number in range(noUC): + profile = unit_profile.copy() + profile[:, 0] -= number * unit_cell.a[2] + profiles.append(profile) + return np.ascontiguousarray(np.concatenate(profiles, axis=0)) + + +def water_optical_profile(water_model, noUC=30, z_step=0.6): + """Sample continuous water optical constants towards positive z. + + The sampled density is calculated directly from + :meth:`WaterModel.zDensity_G` at ``h = k = 0``. This is the continuous + analogue of :func:`optical_profile_asbulk`, whose repeated bulk cells + extend towards negative z. + + :param WaterModel water_model: + Water model with energy-dependent scattering factors populated. + :param int noUC: + Length of the sampled water region in water-model unit cells. + :param float z_step: + Uniform z sampling interval in Angstrom. + :returns: + C-contiguous ``(N, 3)`` array with columns ``z`` in Angstrom, + ``delta``, and ``beta``. + :rtype: numpy.ndarray + :raises ValueError: + If energy, sample length, or sample spacing is invalid. + """ + if not hasattr(water_model, "_E") or not hasattr(water_model, "f"): + raise ValueError( + "Set the WaterModel energy before calculating optical constants." + ) + if not isinstance(noUC, int | np.integer) or noUC <= 0: + raise ValueError("noUC must be a positive integer.") + if z_step <= 0.0: + raise ValueError("z_step must be positive.") + + z_start = water_model.pos_absolute + water_model.basis[0] * water_model.a[2] + z_stop = z_start + noUC * water_model.a[2] + z = np.arange(z_start, z_stop + 0.5 * z_step, z_step, dtype=np.float64) + rho_f = water_model.zDensity_G(z, 0.0, 0.0) + wavelength = 12.398419843320026 / (water_model._E * 1e-3) + scale = 2.8179403262e-5 * wavelength**2 / (2.0 * np.pi) + profile = np.empty((z.size, 3), dtype=np.float64) + profile[:, 0] = z + profile[:, 1] = scale * rho_f.real + profile[:, 2] = scale * rho_f.imag + return np.ascontiguousarray(profile) + + +def add_structural_to_sampled_profile( + structural_profile, *sampled_profiles, z_tolerance=0.6 +): + """Add discrete structural layers to a continuous sampled profile. + + A continuous profile must retain each of its sampling points. Therefore + it cannot use :func:`combine_profiles`, which coalesces nearby origins. + Instead, each structural contribution within ``z_tolerance`` Angstrom is + added to the nearest sampled point; structural contributions outside the + sampled region keep their original position. + + :param numpy.ndarray structural_profile: + Discrete layer contributions with columns z, delta, and beta. + :param sampled_profiles: + Continuous sampled profiles with the same columns. + :param float z_tolerance: + Maximum structural-to-sample separation in Angstrom. + :returns: + C-contiguous combined profile sorted by z. + :rtype: numpy.ndarray + """ + if z_tolerance < 0.0: + raise ValueError("z_tolerance must be non-negative.") + samples = [profile for profile in sampled_profiles if profile.size] + if not samples: + return np.ascontiguousarray(structural_profile) + sampled = np.ascontiguousarray(np.concatenate(samples, axis=0), dtype=np.float64) + sampled = sampled[np.argsort(sampled[:, 0], kind="stable")] + remainder = [] + for row in structural_profile: + right = np.searchsorted(sampled[:, 0], row[0]) + candidates = [ + index for index in (right - 1, right) if 0 <= index < len(sampled) + ] + nearest = min(candidates, key=lambda index: abs(sampled[index, 0] - row[0])) + if abs(sampled[nearest, 0] - row[0]) <= z_tolerance: + sampled[nearest, 1:] += row[1:] + else: + remainder.append(row) + if remainder: + profile = np.concatenate((np.asarray(remainder), sampled), axis=0) + else: + profile = sampled + return np.ascontiguousarray(profile[np.argsort(profile[:, 0], kind="stable")]) + + +def combine_profiles(*profiles, z_tolerance=0.6): + """Combine and coalesce homogeneous optical-profile contributions. + + :param profiles: + Arrays returned by :func:`optical_profile`, optionally empty. + :param float z_tolerance: + Maximum separation in Angstrom between layer origins that represent + one homogeneous optical layer. The lowest origin in each merged + group is retained as its coordinate. + :returns: + C-contiguous ``(N, 3)`` array sorted by z, with delta and beta summed + for origins separated by at most ``z_tolerance``. + :rtype: numpy.ndarray + """ + if z_tolerance < 0.0: + raise ValueError("z_tolerance must be non-negative.") + populated = [profile for profile in profiles if profile.size] + if not populated: + return np.empty((0, 3), dtype=np.float64) + stacked = np.ascontiguousarray(np.concatenate(populated, axis=0), dtype=np.float64) + order = np.argsort(stacked[:, 0], kind="stable") + stacked = stacked[order] + group_starts = np.r_[0, np.flatnonzero(np.diff(stacked[:, 0]) > z_tolerance) + 1] + group_stops = np.r_[group_starts[1:], len(stacked)] + combined = np.empty((len(group_starts), 3), dtype=np.float64) + for index, (start, stop) in enumerate(zip(group_starts, group_stops)): + group = stacked[start:stop] + combined[index] = [group[0, 0], group[:, 1].sum(), group[:, 2].sum()] + return np.ascontiguousarray(combined) diff --git a/orgui/datautils/xrayutils/CTRuc.py b/orgui/datautils/xrayutils/CTRuc.py index 964e941..a53d58f 100644 --- a/orgui/datautils/xrayutils/CTRuc.py +++ b/orgui/datautils/xrayutils/CTRuc.py @@ -273,6 +273,50 @@ def pos_absolute(self): def pos_absolute(self, pos): self._pos_absolute = pos + @property + def height_absolute(self): + """Return the water onset height in Angstrom. + + Water is continuous towards positive z and therefore does not advance + the structural stacking cursor beyond its onset. + """ + return self.pos_absolute + + @property + def loc_absolute(self): + """Return the water onset location in Angstrom.""" + return self.pos_absolute + + @property + def stacking_height_absolute(self): + """Return the height passed to components above the water model.""" + return self.pos_absolute + + @property + def stacking_loc_absolute(self): + """Return the location passed to components above the water model.""" + return self.pos_absolute + + @property + def end_layer_number(self): + """Preserve the structural layer number below continuous water.""" + return getattr(self, "start_layer_number", -1) + + def stack_on(self, below_loc, below_height, below_layer=-1, below_state=None): + """Place the water onset at the top of the object below it. + + :param float below_loc: + Reference location of the object below in Angstrom. + :param float below_height: + Top height of the object below in Angstrom. + :param float below_layer: + Top structural layer identifier of the object below. + :param below_state: + Accepted for compatibility with other stackable components. + """ + self.start_layer_number = below_layer + self.pos_absolute = below_height + def F_uc(self, h, k, l): # noqa: E741 """Return the water-model structure factor in electrons. @@ -443,6 +487,27 @@ def lookupScatteringFactors(self, E): for i, name in enumerate(["O", "H", "O2-"]): self.f[i, :11] = readWaasmaier(name) self.f[i, 11:] = readDispersion(name, E) + self.wat_dispersion = ( + self.f[0, 11] + + 2.0 * self.f[1, 11] + + 1j * (self.f[0, 12] + 2.0 * self.f[1, 12]) + ) + + def optical_profile(self, noUC=30, z_step=0.6): + """Return continuous water optical constants sampled towards positive z. + + :param int noUC: + Length of the sampled water region in water-model unit cells. + :param float z_step: + Uniform z sampling interval in Angstrom. + :returns: + ``(N, 3)`` float64 array containing z in Angstrom, delta, and + beta. + :rtype: numpy.ndarray + """ + from .CTRoptics import water_optical_profile + + return water_optical_profile(self, noUC=noUC, z_step=z_step) # f1,f2 = UnitCell.special_formfactors['H2O'][1](E) # self.wat_dispersion = f1 + 1j*f2 @@ -511,9 +576,9 @@ def zDensity_G(self, z, h, k): + erf((z - zpos * self._a[2]) / (np.sqrt(2) * sigma_0 * self._a[2])) ) ) - rho = gaussian_filter1d(np.abs(rho), 0.2547 / zstep_mean).astype( - np.complex128 - ) # estimation of water molecular form factor + rho_real = gaussian_filter1d(rho.real, 0.2547 / zstep_mean) + rho_imag = gaussian_filter1d(rho.imag, 0.2547 / zstep_mean) + rho = rho_real + 1j * rho_imag # estimation of water molecular form factor elif self.type == "layered": zrange_waterstructure = np.amax(z) / self._a[2] - zpos # lattice units nogausseans_inrange = np.ceil(zrange_waterstructure / d_layering) @@ -582,9 +647,9 @@ def zDensity_G(self, z, h, k): + erf((z - zpos * self._a[2]) / (np.sqrt(2) * sigma_0 * self._a[2])) ) ) - rho = gaussian_filter1d(np.abs(rho), 0.2547 / zstep_mean).astype( - np.complex128 - ) # estimation of water molecular form factor + rho_real = gaussian_filter1d(rho.real, 0.2547 / zstep_mean) + rho_imag = gaussian_filter1d(rho.imag, 0.2547 / zstep_mean) + rho = rho_real + 1j * rho_imag # estimation of water molecular form factor rho_layer, mu_offset = self._1layer_firstGauss() rho += rho_layer * np.exp( -((z / self._a[2] - zpos - mu_offset) ** 2) / (2 * sigma2_rel) @@ -1097,12 +1162,11 @@ def split_in_layers(self, ordered=True): uc.dw_increase_constraint = self.dw_increase_constraint[idx_low:idx_high] uc._test_special_formfactors() uc.f = self.f[idx_low:idx_high] + if hasattr(self, "_E"): + uc._E = self._E uc.refRealTransform = self.refRealTransform uc.refHKLTransform = self.refHKLTransform layers[l] = uc - if len(layer_numbers) == 1: - return layers - if self._explicit_layer_cycle is not None: cycle = tuple(self._explicit_layer_cycle) missing = set(layers) - set(cycle) @@ -1111,7 +1175,7 @@ def split_in_layers(self, ordered=True): f"layer_cycle omits unit-cell layers {sorted(missing)!r}" ) layers = OrderedDict((n, layers[n]) for n in cycle) - elif ordered: + elif ordered and len(layer_numbers) > 1: avg_height = [] uc_nms = [] for uc_l in layers: @@ -1124,8 +1188,58 @@ def split_in_layers(self, ordered=True): layers = OrderedDict([(n, layers[n]) for n in uc_nms]) + ordered_layers = tuple(layers) + origins = [] + for layer in ordered_layers: + origin = self.layerpos.get(float(layer)) + if origin is None: + atom_mask = self.basis[:, 7] == layer + origin = float(np.mean(self.basis[atom_mask, 3])) + origins.append(float(origin)) + for index, layer in enumerate(ordered_layers): + spacing = origins[(index + 1) % len(ordered_layers)] - origins[index] + while spacing <= 0.0: + spacing += 1.0 + layers[layer]._optical_layer_origin = origins[index] + layers[layer]._optical_layer_thickness_fraction = spacing + return layers + def optical_profile(self): + """Return homogeneous optical constants for every layer and domain. + + The returned ``(N, 3)`` float64 array has columns ``z`` in Angstrom, + ``delta``, and ``beta``. Each row is a domain-transformed layer + contribution with coherent-domain occupancy already applied. + + :returns: + One row for every structural layer and coherent domain. + :rtype: numpy.ndarray + :raises ValueError: + If :meth:`setEnergy` has not populated the anomalous scattering + factors. + """ + from .CTRoptics import optical_profile + + return optical_profile(self) + + def optical_profile_asbulk(self, noUC=30): + """Return a finite representation of the semi-infinite bulk profile. + + The unit cell is repeated towards negative z exactly as in + :meth:`zDensity_G_asbulk`. + + :param int noUC: + Number of unit cells to represent below the termination. + :returns: + ``(N, 3)`` float64 array containing z in Angstrom, delta, and + beta. + :rtype: numpy.ndarray + """ + from .CTRoptics import optical_profile_asbulk + + return optical_profile_asbulk(self, noUC=noUC) + @property def layers(self): return np.sort(np.unique(self.basis[:, 7])) diff --git a/orgui/datautils/xrayutils/__init__.py b/orgui/datautils/xrayutils/__init__.py index 5a5560f..ec855a6 100644 --- a/orgui/datautils/xrayutils/__init__.py +++ b/orgui/datautils/xrayutils/__init__.py @@ -30,6 +30,7 @@ __all__ = [ "CTRcalc", + "CTRoptics", "CTRplotutil", "CTRsymmetry", "DetectorCalibration", From beee738779ed8cd553ef18e93866f78f1b652f54 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Sun, 12 Jul 2026 18:27:46 -0400 Subject: [PATCH 02/37] fix: EpitaxyInterface now is again consuming Film so thatinterfacial strain field is correct --- orgui/datautils/xrayutils/CTRfilm.py | 124 +++++++++--------- .../datautils/xrayutils/test/test_CTRcalc.py | 124 +++++++++++++----- 2 files changed, 150 insertions(+), 98 deletions(-) diff --git a/orgui/datautils/xrayutils/CTRfilm.py b/orgui/datautils/xrayutils/CTRfilm.py index 4806cd9..21cc015 100644 --- a/orgui/datautils/xrayutils/CTRfilm.py +++ b/orgui/datautils/xrayutils/CTRfilm.py @@ -381,15 +381,6 @@ def _parse_float_metadata(string, name, default): return default -def _parse_text_metadata(string, name, default=None): - prefix = name.lower() + ":" - for line in string.splitlines(): - normalized = line.strip() - if normalized.lower().startswith(prefix): - return normalized.split(":", 1)[1].strip() - return default - - class EpitaxyInterface(_LayerStackingMixin, LinearFitFunctions): parameterOrder = "Width/cells Skew/cells" @@ -419,9 +410,6 @@ def __init__(self, uc_top, uc_bottom, type="skellam", **kwargs): raise TypeError("EpitaxyInterface profile must be SkellamProfile") self.type = type self.profile = profile - self._legacy_support_cursor = kwargs.pop( - "legacy_support_cursor", profile is None - ) self.sigma_calc = kwargs.get("sigma_calc", 3) self.fixed_ucs = kwargs.get("fixed_ucs", False) self.set_ucs(uc_top, uc_bottom, **kwargs) @@ -577,26 +565,27 @@ def end_layer_number(self): @property def stacking_height_absolute(self): - """Return the nominal interface boundary height in Angstrom.""" - if self._legacy_support_cursor: - return self.height_absolute - return self.below_H + """Return the physical upper support height in Angstrom. + + Components stacked above this interface start at this height, so the + interface alone owns every layer in its generated support. + """ + return self.height_absolute @property def stacking_loc_absolute(self): - """Return the nominal interface boundary location in Angstrom.""" - if self._legacy_support_cursor: - return self.loc_absolute - return self.below_H + """Return the nominal interface boundary location in Angstrom. + + A Film uses this location as the origin of its requested total width, + while :attr:`stacking_height_absolute` supplies the physical support + it must not duplicate. + """ + return self.loc_absolute @property def layer_state(self): - """Return the nominal layer state at the upper side of the interface.""" - if self._legacy_support_cursor: - return super().layer_state - layers = self.layer_cycle.layers - start = layers.index(self.start_layer_number) - return LayerState(self.layer_cycle, layers[start - 1]) + """Return the terminal layer state of the physical upper support.""" + return super().layer_state def createInterfaceCells(self): n_layers = len(self.uc_top.layers) @@ -634,14 +623,11 @@ def createInterfaceCells(self): (-1, n_layers) ) probability_bottom = 1.0 - probability_top - occupancy_top = probability_top - occupancy_bottom = probability_bottom - if not self._legacy_support_cursor: - sharp_top = ( - (unitcells >= loc).astype(np.float64).reshape((-1, n_layers)) - ) - occupancy_top = probability_top - sharp_top - occupancy_bottom = probability_bottom - (1.0 - sharp_top) + sharp_top = ( + (unitcells >= loc).astype(np.float64).reshape((-1, n_layers)) + ) + occupancy_top = probability_top - sharp_top + occupancy_bottom = probability_bottom - (1.0 - sharp_top) a3_top = self.top_layers[0].a[2] a3_bottom = self.bottom_layers[0].a[2] @@ -707,12 +693,8 @@ def createInterfaceCells(self): self._loc_absolute_ref = ( loc_mat[2, 3] * a3_top + loc_remainder * loc_mat[2, 2] * a3_top ) - if self._legacy_support_cursor: - translation = self.below_H - self._loc_absolute = self._loc_absolute_ref + self.below_H - else: - translation = self.below_H - self._loc_absolute_ref - self._loc_absolute = self.below_H + translation = self.below_H - self._loc_absolute_ref + self._loc_absolute = self.below_H _translate_domains(self.top_layers, translation) _translate_domains(self.bottom_layers, translation) self._end_layer_number = self.layer_order[-1] @@ -1005,20 +987,16 @@ def fromStr(cls, string): uc_top.name = top_name uc_bottom.name = bottom_name - support_cursor = _parse_text_metadata(string, "support_cursor", "legacy") tail_probability = _parse_float_metadata( string, "tail_probability", DEFAULT_TAIL_PROBABILITY ) - profile = None - if support_cursor == "nominal": - profile = SkellamProfile(basis[0], basis[1], tail_probability) + profile = SkellamProfile(basis[0], basis[1], tail_probability) epit = cls( uc_top, uc_bottom, ep_type, profile=profile, layer_transition=_parse_layer_transition(string), - legacy_support_cursor=(support_cursor != "nominal"), ) epit.statistics = statistics epit.basis = basis @@ -1042,13 +1020,11 @@ def toStr(self, showErrors=True): + "\n" + self.epitToStr(showErrors=showErrors) ) - if not self._legacy_support_cursor: - s += "\nsupport_cursor: nominal" - if ( - self.profile is not None - and self.profile.tail_probability != DEFAULT_TAIL_PROBABILITY - ): - s += f"\ntail_probability: {self.profile.tail_probability:.12g}" + if ( + self.profile is not None + and self.profile.tail_probability != DEFAULT_TAIL_PROBABILITY + ): + s += f"\ntail_probability: {self.profile.tail_probability:.12g}" metadata = self._stacking_metadata_to_str() if metadata: s += "\n" + metadata.rstrip() @@ -1158,6 +1134,15 @@ def loc_absolute(self): return self.pos_absolute def set_below(self, loc, height): + """Place Film above support while retaining its nominal width origin. + + :param float loc: + Nominal lower boundary in Angstrom, used as the origin of + :attr:`basis` width. + :param float height: + Physical upper support height in Angstrom. Generated unstrained + Film layers begin here. + """ self.below_loc = loc self.below_H = height self.createLayers() @@ -1166,6 +1151,8 @@ def set_below(self, loc, height): def height_absolute(self): if np.any(self._basis_created != self.basis): self.createLayers() + if self._layers_to_create == 0: + return self.below_H upper_layer_id = self.end_layer_number upper_layer = self.uc_layers[upper_layer_id] idx = self.layer_ucs.index(upper_layer) @@ -1180,6 +1167,8 @@ def height_absolute(self): def pos_absolute(self): if np.any(self._basis_created != self.basis): self.createLayers() + if self._layers_to_create == 0: + return self.below_H lower_layer = self.layer_ucs[0] matrix = lower_layer.coherentDomainMatrix[0] layer_id = self.layer_order[0] @@ -1203,17 +1192,35 @@ def end_layer_number(self): return self._end_layer_number def createLayers(self): + """Create unstrained Film layers above lower-component support. + + The Film width in :attr:`basis` is measured from ``below_loc``. When + the component below has already generated support up to ``below_H``, + only the remaining width is represented by Film layers. + """ n_layers_in_uc = len(self.unitcell.layers) scaled_width = self.basis[0] - n_layers_in_uc * ( (self.below_H - self.below_loc) / self.unitcell.a[2] ) + if scaled_width < 0 and not np.isclose(scaled_width, 0.0): + raise ValueError( + "Effective film width is shorter than lower-component support" + ) layers_to_create = int(round(scaled_width, 0)) - if layers_to_create <= 0: - raise ValueError("Effective film width <= 0. Cannot create layers") + if layers_to_create < 0: + raise ValueError( + "Effective film width is shorter than lower-component support" + ) for i, uc in enumerate(self.layer_ucs): uc.coherentDomainMatrix = [] uc.coherentDomainOccupancy = [] + self._layers_to_create = layers_to_create + if layers_to_create == 0: + self._end_layer_number = self.layer_order[-1] + self._basis_created = np.copy(self.basis) + return + mat_0 = np.vstack((np.identity(3).T, np.array([0, 0, 0]))).T strain = self.unitcell.coherentDomainMatrix[0][2, 2] occup = self.unitcell.coherentDomainOccupancy[0] @@ -1932,12 +1939,7 @@ def fromStr(cls, string): string, "tail_probability", DEFAULT_TAIL_PROBABILITY ) legacy_parameter_names = any("deltaW" in line for line in string.splitlines()) - support_cursor = _parse_text_metadata(string, "support_cursor", None) - legacy_absolute_width = ( - legacy_parameter_names - if support_cursor is None - else support_cursor != "nominal" - ) + legacy_absolute_width = legacy_parameter_names if legacy_absolute_width: profile = PoissonProfile( mean_change=basis[1], @@ -1985,8 +1987,6 @@ def toStr(self, showErrors=True): else: parameter_order = PoissonSurface.parameterOrder s = "\n" + parameter_order + "\n" + self.filmToStr(showErrors=showErrors) - if not self._legacy_absolute_width: - s += "\nsupport_cursor: nominal" if ( self.profile is not None and self.profile.tail_probability != DEFAULT_TAIL_PROBABILITY diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index d3ea957..a9ca67b 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -301,7 +301,7 @@ def test_zero_width_poisson_surface_stacks_without_domains(self): 0.0, ) - def test_nominal_delta_width_serialization_migrates_to_offset(self): + def test_delta_width_serialization_uses_legacy_absolute_width(self): transitional = CTRfilm.PoissonSurface( self.unitcell, profile=PoissonProfile(mean_change=1.0), @@ -316,9 +316,9 @@ def test_nominal_delta_width_serialization_migrates_to_offset(self): restored = CTRfilm.PoissonSurface.fromStr(transitional) - np.testing.assert_allclose(restored.basis, [1.0, -1.0]) - self.assertAlmostEqual(restored.profile.expected_height_change, 0.0) - self.assertIn("W/layers offset/layers", restored.toStr()) + np.testing.assert_allclose(restored.basis, [0.0, 1.0]) + self.assertAlmostEqual(restored.profile.expected_height_change, 1.0) + self.assertIn("Width/layers deltaW/layers", restored.toStr()) class TestLayerStacking(unittest.TestCase): @@ -1063,7 +1063,8 @@ def test_epitaxy_interface_rotates_cyclic_layer_order(self): self.assertEqual(interface.start_layer_number, 2) self.assertEqual(interface.end_layer_number, 1) np.testing.assert_array_equal(interface.layer_order, [2, 3, 1]) - self.assertAlmostEqual(interface.pos_absolute, 12.0) + self.assertAlmostEqual(interface.loc_absolute, 12.0) + self.assertLess(interface.pos_absolute, interface.loc_absolute) self.assertGreater(interface.height_absolute, 12.0) self.assertGreater( interface.top_layers[-1].coherentDomainMatrix[0][2, 3], @@ -1179,7 +1180,7 @@ def test_rotated_cycle_is_compatible_without_transition(self): self.assertEqual(lower.end_layer_number, 3) self.assertEqual(upper.start_layer_number, 1) - def test_profile_interface_does_not_advance_nominal_cursor(self): + def test_profile_interface_support_is_not_owned_by_film(self): lower = self.make_layered_unitcell("lower") interface = CTRfilm.EpitaxyInterface( self.make_layered_unitcell("top"), @@ -1187,7 +1188,7 @@ def test_profile_interface_does_not_advance_nominal_cursor(self): profile=SkellamProfile(0.5), ) film = CTRfilm.Film(self.make_layered_unitcell("film")) - film.basis[0] = 3 + film.basis[0] = 18 crystal = CTRcalc.SXRDCrystal( lower, interface, @@ -1197,14 +1198,80 @@ def test_profile_interface_does_not_advance_nominal_cursor(self): crystal.apply_stacking() - self.assertEqual(interface.stacking_height_absolute, 0.0) - self.assertEqual(film.pos_absolute, 0.0) - self.assertAlmostEqual(film.height_absolute, 6.0) + self.assertAlmostEqual( + interface.stacking_height_absolute, interface.height_absolute + ) + self.assertAlmostEqual( + interface.stacking_loc_absolute, interface.loc_absolute + ) + self.assertAlmostEqual(film.pos_absolute, interface.height_absolute) + self.assertGreater(film.pos_absolute, interface.loc_absolute) + self.assertAlmostEqual( + film.height_absolute - interface.loc_absolute, + film.basis[0] * film.unitcell.a[2] / len(film.layers), + ) + layer_cycle = interface.layer_cycle.layers + next_index = (layer_cycle.index(interface.end_layer_number) + 1) % len( + layer_cycle + ) + self.assertEqual(film.start_layer_number, layer_cycle[next_index]) restored = CTRfilm.EpitaxyInterface.fromStr(interface.toStr()) - self.assertFalse(restored._legacy_support_cursor) self.assertIsInstance(restored.profile, SkellamProfile) np.testing.assert_allclose(restored.basis, interface.basis) + self.assertNotIn("support_cursor", interface.toStr()) + + def test_profile_interface_can_consume_entire_film_width(self): + lower = self.make_layered_unitcell("lower") + interface = CTRfilm.EpitaxyInterface( + self.make_layered_unitcell("top"), + self.make_layered_unitcell("bottom"), + profile=SkellamProfile(0.5), + ) + interface.stack_on( + 0.0, + 0.0, + lower.end_layer_number, + below_state=lower.layer_state, + ) + film = CTRfilm.Film(self.make_layered_unitcell("film")) + film.basis[0] = ( + (interface.height_absolute - interface.loc_absolute) + * len(film.layers) / film.unitcell.a[2] + ) + crystal = CTRcalc.SXRDCrystal( + lower, interface, film, stacking=np.array([1, 2]) + ) + + crystal.apply_stacking() + + self.assertAlmostEqual(film.pos_absolute, interface.height_absolute) + self.assertAlmostEqual(film.height_absolute, interface.height_absolute) + self.assertEqual( + sum(len(layer.coherentDomainMatrix) for layer in film.layer_ucs), 0 + ) + np.testing.assert_allclose( + film.F_uc( + np.array([0.0]), np.array([0.0]), np.array([1.0]) + ), + 0.0, + ) + + def test_profile_interface_rejects_film_narrower_than_support(self): + lower = self.make_layered_unitcell("lower") + interface = CTRfilm.EpitaxyInterface( + self.make_layered_unitcell("top"), + self.make_layered_unitcell("bottom"), + profile=SkellamProfile(0.5), + ) + film = CTRfilm.Film(self.make_layered_unitcell("film")) + film.basis[0] = 3 + crystal = CTRcalc.SXRDCrystal( + lower, interface, film, stacking=np.array([1, 2]) + ) + + with self.assertRaisesRegex(ValueError, "shorter than lower-component"): + crystal.apply_stacking() def test_profile_interface_is_a_correction_not_extra_material(self): unitcell = self.make_layered_unitcell("material") @@ -1237,7 +1304,7 @@ def test_profile_interface_is_a_correction_not_extra_material(self): class TestLegacyLayeredCTR(unittest.TestCase): - def test_legacy_xtal_reconstructs_reference_interface(self): + def test_legacy_xtal_uses_corrected_interface_support(self): repository_root = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "..", "..", "..") ) @@ -1247,8 +1314,7 @@ def test_legacy_xtal_reconstructs_reference_interface(self): xtal_path = os.path.join( fixture_root, "0001_fit_2V036_reference.xtal" ) - reference_path = os.path.join(fixture_root, "CTRs_reference.dat") - for path in (xtal_path, reference_path): + for path in (xtal_path,): if not os.path.exists(path): self.skipTest(f"Missing optional CTR fixture: {path}") xtal = CTRcalc.SXRDCrystal.fromFile( @@ -1258,30 +1324,16 @@ def test_legacy_xtal_reconstructs_reference_interface(self): self.assertIsInstance(xtal["RuO2"], CTRfilm.Film) self.assertIsInstance(xtal["TiO2toRuO2"], CTRfilm.EpitaxyInterface) np.testing.assert_allclose(xtal["TiO2toRuO2"].basis, [0.35, 0.0]) - self.assertTrue(xtal["TiO2toRuO2"]._legacy_support_cursor) - - xtal["RuO2"].basis[0] = 17.0 - xtal["RuO2"].basis_0[0] = 17.0 - xtal.atten = 0.01 - reference = np.loadtxt( - reference_path, - skiprows=1, - max_rows=8, - ) - l_values = np.linspace(0.0, 7.25, 2000)[:8] - calculated = np.abs( - xtal.F( - np.zeros(8, dtype=np.float64), - np.zeros(8, dtype=np.float64), - l_values, - ) + interface = xtal["TiO2toRuO2"] + xtal.apply_stacking() + self.assertAlmostEqual( + interface.stacking_height_absolute, interface.height_absolute ) - np.testing.assert_allclose( - calculated, - reference[:, 3] * xtal.reference_area, - rtol=2e-5, - atol=2e-5, + self.assertAlmostEqual( + interface.stacking_loc_absolute, interface.loc_absolute ) + self.assertIsInstance(interface.profile, SkellamProfile) + self.assertNotIn("support_cursor", interface.toStr()) class TestStructureFactorNormalization(unittest.TestCase): From 1e54f7d8a0db1be1f1721524b9de277e68108a9a Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Mon, 13 Jul 2026 01:43:58 -0400 Subject: [PATCH 03/37] fix: gap in zDensity --- orgui/datautils/xrayutils/CTRfilm.py | 6 +++++- orgui/datautils/xrayutils/test/test_CTRcalc.py | 14 +++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/orgui/datautils/xrayutils/CTRfilm.py b/orgui/datautils/xrayutils/CTRfilm.py index 0caa8a5..1cd8289 100644 --- a/orgui/datautils/xrayutils/CTRfilm.py +++ b/orgui/datautils/xrayutils/CTRfilm.py @@ -627,7 +627,11 @@ def createInterfaceCells(self): sharp_top = ( (unitcells >= loc).astype(np.float64).reshape((-1, n_layers)) ) - occupancy_top = probability_top - sharp_top + # The upper material owns the complete generated support because + # Film starts only above ``stacking_height_absolute``. The lower + # material remains a signed correction to the semi-infinite bulk + # that already occupies the lower side of the nominal boundary. + occupancy_top = probability_top occupancy_bottom = probability_bottom - (1.0 - sharp_top) a3_top = self.top_layers[0].a[2] diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index a9ca67b..e6fa183 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -1273,7 +1273,7 @@ def test_profile_interface_rejects_film_narrower_than_support(self): with self.assertRaisesRegex(ValueError, "shorter than lower-component"): crystal.apply_stacking() - def test_profile_interface_is_a_correction_not_extra_material(self): + def test_profile_interface_owns_upper_support_material(self): unitcell = self.make_layered_unitcell("material") interface = CTRfilm.EpitaxyInterface( self.make_layered_unitcell("top"), @@ -1294,13 +1294,13 @@ def test_profile_interface_is_a_correction_not_extra_material(self): [layer.coherentDomainOccupancy for layer in interface.bottom_layers] ) - self.assertTrue(np.any(top_occupancy < 0.0)) + self.assertTrue(np.all(top_occupancy >= 0.0)) + self.assertTrue(np.any(top_occupancy > 0.9)) self.assertTrue(np.any(bottom_occupancy < 0.0)) - np.testing.assert_allclose( - np.sort(top_occupancy), - np.sort(-bottom_occupancy), - atol=1e-15, - ) + self.assertTrue(np.any(bottom_occupancy > 0.0)) + total_occupancy = top_occupancy + bottom_occupancy + self.assertTrue(np.any(np.isclose(total_occupancy, 0.0))) + self.assertTrue(np.any(np.isclose(total_occupancy, 1.0))) class TestLegacyLayeredCTR(unittest.TestCase): From 5e1faf399e33a17bb71fd189e053a37ad9f46c4e Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Tue, 14 Jul 2026 23:16:55 -0400 Subject: [PATCH 04/37] feat: make optical profile based on interfaces, reduce profile numerical complexity --- orgui/datautils/xrayutils/CTRcalc.py | 180 +++++++- orgui/datautils/xrayutils/CTRoptics.py | 400 ++++++++++++++++- orgui/datautils/xrayutils/CTRuc.py | 11 +- .../xrayutils/test/test_CTRoptical_profile.py | 403 ++++++++++++++++++ 4 files changed, 979 insertions(+), 15 deletions(-) create mode 100644 orgui/datautils/xrayutils/test/test_CTRoptical_profile.py diff --git a/orgui/datautils/xrayutils/CTRcalc.py b/orgui/datautils/xrayutils/CTRcalc.py index 9f621d7..a3bf799 100644 --- a/orgui/datautils/xrayutils/CTRcalc.py +++ b/orgui/datautils/xrayutils/CTRcalc.py @@ -40,7 +40,14 @@ from .CTRuc import WaterModel, UnitCell from .CTRfilm import EpitaxyInterface, Film, PoissonSurface -from .CTRoptics import add_structural_to_sampled_profile, combine_profiles +from .CTRoptics import ( + add_structural_to_sampled_profile, + combine_profiles, + simplify_profile, + solve_wavefield, + stratify_profile, + top_layer_spacing, +) from .CTRdistributions import PoissonProfile, SkellamProfile # noqa: F401 from .CTRstacking import ( # noqa: F401 LayerCycle, @@ -1085,7 +1092,9 @@ def optical_profile(self): """Return the combined homogeneous optical profile of this crystal. Crystal surface weights are applied directly to ``delta`` and ``beta``. - Water and other continuum-only components are not yet supported. + Water is sampled on the top atomistic layer grid. Without water, one + vacuum row is appended one top-layer spacing above the highest atomic + layer. :returns: C-contiguous ``(N, 3)`` array with columns ``z`` in Angstrom, @@ -1097,25 +1106,180 @@ def optical_profile(self): if self.enable_uc_stacking: self.apply_stacking() profiles = [self.uc_bulk.optical_profile_asbulk()] - water_profiles = [] + water_components = [] for component, weight in zip(self.uc_surface_list, self.weights): if not hasattr(component, "optical_profile"): raise NotImplementedError( f"{type(component).__name__} has no atomistic optical profile." ) - profile = component.optical_profile().copy() - profile[:, 1:] *= weight if isinstance(component, WaterModel): - water_profiles.append(profile) + water_components.append((component, weight)) else: + profile = component.optical_profile().copy() + profile[:, 1:] *= weight profiles.append(profile) structural_profile = combine_profiles(*profiles) - if not water_profiles: - return structural_profile + dz = top_layer_spacing(structural_profile) + if not water_components: + vacuum = np.array( + [[structural_profile[-1, 0] + dz, 0.0, 0.0]], dtype=np.float64 + ) + return np.ascontiguousarray( + np.concatenate((structural_profile, vacuum), axis=0) + ) + water_profiles = [] + z_origin = structural_profile[-1, 0] + for component, weight in water_components: + profile = component.optical_profile( + z_step=dz, z_origin=z_origin + ).copy() + profile[:, 1:] *= weight + water_profiles.append(profile) return add_structural_to_sampled_profile( structural_profile, *water_profiles ) + def simplified_optical_profile( + self, delta_tolerance=1e-9, beta_tolerance=None + ): + """Return a thickness-conserving simplified optical profile. + + :param float delta_tolerance: + Maximum delta range within a merged finite layer. + :param float beta_tolerance: + Maximum beta range within a merged finite layer. Defaults to the + delta tolerance. + :returns: + Simplified ``(N, 3)`` optical profile. + :rtype: numpy.ndarray + """ + return simplify_profile( + self.optical_profile(), delta_tolerance, beta_tolerance + ) + + def stratified_optical_profile( + self, delta_tolerance=1e-9, beta_tolerance=None + ): + """Return optical media and their physical interface positions. + + Profile z coordinates are centers of homogeneous media. Interfaces + are centered between adjacent samples before optional simplification; + merged media retain those original outer boundaries. + + :param float delta_tolerance: + Maximum delta range within a merged finite layer. Pass ``None`` + to retain every sampled medium. + :param float beta_tolerance: + Maximum beta range within a merged finite layer. Defaults to the + delta tolerance when simplification is requested. + :returns: + Layer-center optical constants and substrate-to-ambient edges. + :rtype: CTRoptics.StratifiedProfile + """ + return stratify_profile( + self.optical_profile(), delta_tolerance, beta_tolerance + ) + + def wavefield( + self, + alpha, + polarization="s", + delta_tolerance=1e-9, + beta_tolerance=None, + ): + """Return the unperturbed layered electric field. + + The field follows the Renaud convention with downward and upward + amplitudes ``A_plus`` and ``A_minus``. Slabs are ordered from the + incident medium towards the substrate. + + :param float alpha: + Glancing incidence angle in degrees inside the incident medium. + :param str polarization: + ``"s"`` or ``"p"``. + :param float delta_tolerance: + Maximum delta range used to simplify adjacent layers. Pass + ``None`` to disable simplification. + :param float beta_tolerance: + Optional beta simplification tolerance. Defaults to the delta + tolerance when simplification is requested. + :returns: + Layer amplitudes, normal wavevectors, and sampled electric field. + :rtype: CTRoptics.Wavefield + """ + profile = self.stratified_optical_profile( + delta_tolerance, beta_tolerance + ) + return solve_wavefield( + profile.values, + self.uc_bulk._E, + alpha, + polarization, + boundaries=profile.boundaries, + ) + + def specular_reflectivity( + self, + alpha, + polarization="s", + delta_tolerance=1e-9, + beta_tolerance=None, + ): + """Return scalar optical specular reflectivity. + + :param float or numpy.ndarray alpha: + Glancing incidence angle or angles in degrees inside the incident + medium. + :param str polarization: + ``"s"``, ``"p"``, or ``"unpolarized"``. Unpolarized intensity + is the equal incoherent average of s and p reflectivity. + :param float delta_tolerance: + Maximum delta range used to simplify adjacent layers. Pass + ``None`` to disable simplification. + :param float beta_tolerance: + Optional beta simplification tolerance. Defaults to the delta + tolerance when simplification is requested. + :returns: + Reflectivity with the same scalar/array shape as ``alpha``. + :rtype: float or numpy.ndarray + """ + if polarization not in {"s", "p", "unpolarized"}: + raise ValueError( + "polarization must be 's', 'p', or 'unpolarized'." + ) + alpha_array = np.asarray(alpha, dtype=np.float64) + scalar = alpha_array.ndim == 0 + angles = np.atleast_1d(alpha_array) + profile = self.stratified_optical_profile( + delta_tolerance, beta_tolerance + ) + + def reflectivity(pol): + return np.asarray( + [ + abs( + solve_wavefield( + profile.values, + self.uc_bulk._E, + angle, + pol, + boundaries=profile.boundaries, + ).r_S + ) + ** 2 + for angle in angles + ], + dtype=np.float64, + ) + + if polarization == "unpolarized": + result = 0.5 * (reflectivity("s") + reflectivity("p")) + else: + result = reflectivity(polarization) + if scalar: + return float(result[0]) + return result.reshape(alpha_array.shape) + def toRODStr(self): s = f"E = {self.uc_bulk._E * 1e-3:.5f} keV\n" for i, w, uc in zip(self.uc_stacking, self.weights, self.uc_surface_list): diff --git a/orgui/datautils/xrayutils/CTRoptics.py b/orgui/datautils/xrayutils/CTRoptics.py index f643178..465dfb4 100644 --- a/orgui/datautils/xrayutils/CTRoptics.py +++ b/orgui/datautils/xrayutils/CTRoptics.py @@ -1,10 +1,89 @@ """Optical constants derived from CTR structural models.""" +from dataclasses import dataclass + import numpy as np from .CTRutil import atomic_number +HC_KEV_ANGSTROM = 12.398419843320026 + + +@dataclass(frozen=True) +class Wavefield: + """Unperturbed one-dimensional wavefield in Renaud notation. + + Slabs are ordered from the incident medium towards the substrate. + ``A_plus`` and ``A_minus`` are downward- and upward-propagating electric + field amplitudes referenced to ``z_reference`` for each slab. + + :param numpy.ndarray z: + Sample positions in Angstrom, ordered from substrate to ambient. + :param numpy.ndarray psi: + Complex electric field sampled at ``z``. + :param numpy.ndarray z_interfaces: + Interface positions in Angstrom, ordered from ambient to substrate. + :param numpy.ndarray z_reference: + Local amplitude-reference position for each slab in Angstrom. + :param numpy.ndarray n: + Complex slab refractive indices, ambient first. + :param numpy.ndarray kz: + Renaud normal wavevectors in inverse Angstrom. + :param numpy.ndarray A_plus: + Downward-propagating slab amplitudes. + :param numpy.ndarray A_minus: + Upward-propagating slab amplitudes. + :param complex r_S: + Specular reflection amplitude in the incident medium. + :param complex t_S: + Transmission amplitude at the substrate boundary. + :param str polarization: + ``"s"`` or ``"p"``. + """ + + z: np.ndarray + psi: np.ndarray + z_interfaces: np.ndarray + z_reference: np.ndarray + n: np.ndarray + kz: np.ndarray + A_plus: np.ndarray + A_minus: np.ndarray + r_S: complex + t_S: complex + polarization: str + + +@dataclass(frozen=True) +class StratifiedProfile: + """Optical media centers and their physical interface positions. + + :param numpy.ndarray values: + ``(N, 3)`` array containing layer-center z, delta, and beta. + :param numpy.ndarray boundaries: + ``N - 1`` interfaces in Angstrom, ordered from substrate to ambient. + """ + + values: np.ndarray + boundaries: np.ndarray + + @property + def z(self): + """Return layer-center z coordinates in Angstrom.""" + return self.values[:, 0] + + @property + def delta(self): + """Return layer delta values.""" + return self.values[:, 1] + + @property + def beta(self): + """Return layer beta values.""" + return self.values[:, 2] + + def optical_profile(unit_cell, include_coherent_domains=True): """Return homogeneous optical constants for every layer and domain. @@ -125,7 +204,7 @@ def optical_profile_asbulk(unit_cell, noUC=30): return np.ascontiguousarray(np.concatenate(profiles, axis=0)) -def water_optical_profile(water_model, noUC=30, z_step=0.6): +def water_optical_profile(water_model, noUC=30, z_step=None, z_origin=None): """Sample continuous water optical constants towards positive z. The sampled density is calculated directly from @@ -138,7 +217,12 @@ def water_optical_profile(water_model, noUC=30, z_step=0.6): :param int noUC: Length of the sampled water region in water-model unit cells. :param float z_step: - Uniform z sampling interval in Angstrom. + Uniform z sampling interval in Angstrom. Defaults to the water-model + lattice height when no atomistic profile supplies a spacing. + :param float z_origin: + Optional origin of the atomistic z grid in Angstrom. When supplied, + the first water sample is aligned to that grid at or above the water + onset. :returns: C-contiguous ``(N, 3)`` array with columns ``z`` in Angstrom, ``delta``, and ``beta``. @@ -152,11 +236,17 @@ def water_optical_profile(water_model, noUC=30, z_step=0.6): ) if not isinstance(noUC, int | np.integer) or noUC <= 0: raise ValueError("noUC must be a positive integer.") + if z_step is None: + z_step = water_model.a[2] if z_step <= 0.0: raise ValueError("z_step must be positive.") - z_start = water_model.pos_absolute + water_model.basis[0] * water_model.a[2] - z_stop = z_start + noUC * water_model.a[2] + water_onset = water_model.pos_absolute + water_model.basis[0] * water_model.a[2] + z_start = water_onset + if z_origin is not None: + grid_index = np.ceil((water_onset - z_origin) / z_step - 1e-12) + z_start = z_origin + grid_index * z_step + z_stop = water_onset + noUC * water_model.a[2] z = np.arange(z_start, z_stop + 0.5 * z_step, z_step, dtype=np.float64) rho_f = water_model.zDensity_G(z, 0.0, 0.0) wavelength = 12.398419843320026 / (water_model._E * 1e-3) @@ -168,6 +258,26 @@ def water_optical_profile(water_model, noUC=30, z_step=0.6): return np.ascontiguousarray(profile) +def top_layer_spacing(profile): + """Return the spacing between the two highest optical-layer origins. + + :param numpy.ndarray profile: + Atomistic optical profile with z in the first column. + :returns: + Positive top-layer spacing in Angstrom. + :rtype: float + :raises ValueError: + If fewer than two distinct layer origins are available. + """ + z = np.unique(np.asarray(profile[:, 0], dtype=np.float64)) + if z.size < 2: + raise ValueError("At least two atomic layer origins are required for dz.") + dz = float(z[-1] - z[-2]) + if dz <= 0.0: + raise ValueError("Atomic layer spacing must be positive.") + return dz + + def add_structural_to_sampled_profile( structural_profile, *sampled_profiles, z_tolerance=0.6 ): @@ -243,3 +353,285 @@ def combine_profiles(*profiles, z_tolerance=0.6): group = stacked[start:stop] combined[index] = [group[0, 0], group[:, 1].sum(), group[:, 2].sum()] return np.ascontiguousarray(combined) + + +def profile_boundaries(profile): + """Return interfaces centered between adjacent optical-profile samples. + + The profile coordinates identify the centers at which ``delta`` and + ``beta`` apply. Each interface is therefore the midpoint of two adjacent + center coordinates. The first and last media remain semi-infinite. + + :param numpy.ndarray profile: + ``(N, 3)`` profile with strictly increasing layer-center z positions + in Angstrom, delta, and beta. + :returns: + ``N - 1`` interfaces in Angstrom, ordered from substrate to ambient. + :rtype: numpy.ndarray + :raises ValueError: + If the profile is not a valid ordered ``(N, 3)`` array. + """ + profile = np.asarray(profile, dtype=np.float64) + if profile.ndim != 2 or profile.shape[1] != 3 or len(profile) < 2: + raise ValueError("profile must be an (N, 3) array with at least two rows.") + if not np.all(np.diff(profile[:, 0]) > 0.0): + raise ValueError("profile z positions must be strictly increasing.") + return np.ascontiguousarray(0.5 * (profile[:-1, 0] + profile[1:, 0])) + + +def stratify_profile(profile, delta_tolerance=None, beta_tolerance=None): + """Create finite optical layers and their midpoint interfaces. + + When simplification is requested, adjacent finite media are merged without + moving their outer interfaces. Their optical constants are weighted by + the physical widths between those inherited interfaces. The substrate + and incident medium are never merged. + + :param numpy.ndarray profile: + ``(N, 3)`` profile with layer-center z in Angstrom, delta, and beta. + :param float delta_tolerance: + Maximum delta range within one merged layer. Pass ``None`` to retain + every sampled medium. + :param float beta_tolerance: + Maximum beta range within one merged layer. Defaults to + ``delta_tolerance`` when simplification is requested. + :returns: + Optical media and their explicit physical boundaries. + :rtype: StratifiedProfile + :raises ValueError: + If the profile or tolerances are invalid. + """ + profile = np.asarray(profile, dtype=np.float64) + boundaries = profile_boundaries(profile) + if delta_tolerance is None: + return StratifiedProfile( + np.ascontiguousarray(profile.copy()), boundaries + ) + if beta_tolerance is None: + beta_tolerance = delta_tolerance + if delta_tolerance < 0.0 or beta_tolerance < 0.0: + raise ValueError("delta and beta tolerances must be non-negative.") + if len(profile) <= 3: + return StratifiedProfile( + np.ascontiguousarray(profile.copy()), boundaries + ) + + values = [profile[0].copy()] + inherited_boundaries = [boundaries[0]] + start = 1 + last_finite = len(profile) - 2 + while start <= last_finite: + stop = start + delta_min = delta_max = profile[start, 1] + beta_min = beta_max = profile[start, 2] + while stop < last_finite: + candidate = stop + 1 + next_delta_min = min(delta_min, profile[candidate, 1]) + next_delta_max = max(delta_max, profile[candidate, 1]) + next_beta_min = min(beta_min, profile[candidate, 2]) + next_beta_max = max(beta_max, profile[candidate, 2]) + if ( + next_delta_max - next_delta_min > delta_tolerance + or next_beta_max - next_beta_min > beta_tolerance + ): + break + stop = candidate + delta_min, delta_max = next_delta_min, next_delta_max + beta_min, beta_max = next_beta_min, next_beta_max + + lower_boundary = boundaries[start - 1] + upper_boundary = boundaries[stop] + layer_boundaries = boundaries[start - 1 : stop + 1] + thicknesses = np.diff(layer_boundaries) + merged = profile[start].copy() + merged[0] = 0.5 * (lower_boundary + upper_boundary) + merged[1:] = np.average( + profile[start : stop + 1, 1:], axis=0, weights=thicknesses + ) + values.append(merged) + inherited_boundaries.append(upper_boundary) + start = stop + 1 + + values.append(profile[-1].copy()) + return StratifiedProfile( + np.ascontiguousarray(np.asarray(values, dtype=np.float64)), + np.ascontiguousarray(np.asarray(inherited_boundaries, dtype=np.float64)), + ) + + +def simplify_profile(profile, delta_tolerance=1e-9, beta_tolerance=None): + """Merge adjacent optically similar finite layers. + + A merged layer receives thickness-weighted delta and beta, preserving the + integrated optical constants through the combined finite thickness. The + semi-infinite substrate and incident-medium rows are always retained. + Grouping is bounded by the full delta/beta range in each group, preventing + a long sequence of small changes from drifting beyond the tolerances. + + :param numpy.ndarray profile: + ``(N, 3)`` profile with strictly increasing z, delta, and beta. + :param float delta_tolerance: + Maximum delta range within one merged layer. + :param float beta_tolerance: + Maximum beta range within one merged layer. Defaults to + ``delta_tolerance``. + :returns: + Simplified C-contiguous optical profile. + :rtype: numpy.ndarray + :raises ValueError: + If the profile or tolerances are invalid. + """ + return stratify_profile( + profile, delta_tolerance, beta_tolerance + ).values + + +def _normal_wavevector(n, k0, alpha_rad): + """Return normal propagation constants for incidence from medium zero.""" + k_parallel_over_k0 = n[0] * np.cos(alpha_rad) + argument = n**2 - k_parallel_over_k0**2 + q = k0 * np.sqrt(argument) + q = np.where(q.imag > 0.0, -q, q) + q = np.where((q.imag == 0.0) & (q.real < 0.0), -q, q) + # Avoid cancellation in n_0**2 - (n_0*cos(alpha))**2 at tiny angles. + q[0] = k0 * n[0] * np.sin(alpha_rad) + return q + + +def _admittance(n, q, polarization): + """Return scalar optical admittance for s or p polarization.""" + if polarization == "s": + return q + if polarization == "p": + return n**2 / q + raise ValueError("polarization must be 's' or 'p'.") + + +def solve_wavefield( + profile, energy_eV, alpha, polarization="s", boundaries=None +): + """Solve the layered unperturbed wavefield by stable Parratt recursion. + + The profile rows define optical media at layer centers. By default, their + interfaces are placed at adjacent-center midpoints. Explicit boundaries + preserve the original outer interfaces of a simplified profile. The first + and last rows represent the semi-infinite substrate and incident media. + Angles are glancing angles measured from the surface inside the incident + medium. The conserved tangential wavevector is therefore + ``k_x = n_0 k_0 cos(alpha)``, where ``n_0`` is the last profile row. + + :param numpy.ndarray profile: + ``(N, 3)`` optical profile with z in Angstrom, delta, and beta. + :param float energy_eV: + X-ray photon energy in eV. + :param float alpha: + Glancing angle in degrees inside the incident medium. + :param str polarization: + ``"s"`` or ``"p"``. + :param numpy.ndarray boundaries: + Optional ``N - 1`` interfaces in Angstrom, ordered from substrate to + ambient. Defaults to adjacent-center midpoints. + :returns: + Slab amplitudes and sampled electric field. + :rtype: Wavefield + :raises ValueError: + If the profile, energy, angle, or polarization is invalid. + """ + profile = np.asarray(profile, dtype=np.float64) + if profile.ndim != 2 or profile.shape[1] != 3 or len(profile) < 2: + raise ValueError("profile must be an (N, 3) array with at least two rows.") + default_boundaries = profile_boundaries(profile) + if boundaries is None: + boundaries = default_boundaries + else: + boundaries = np.asarray(boundaries, dtype=np.float64) + if boundaries.ndim != 1 or len(boundaries) != len(profile) - 1: + raise ValueError("boundaries must be a one-dimensional (N - 1) array.") + if not np.all(np.diff(boundaries) > 0.0): + raise ValueError("boundaries must be strictly increasing.") + if not np.all(profile[:-1, 0] < boundaries) or not np.all( + boundaries < profile[1:, 0] + ): + raise ValueError("each boundary must lie between adjacent layer centers.") + if energy_eV <= 0.0: + raise ValueError("energy_eV must be positive.") + if alpha <= 0.0 or alpha > 90.0: + raise ValueError("alpha must be in the interval (0, 90] degrees.") + if polarization not in {"s", "p"}: + raise ValueError("polarization must be 's' or 'p'.") + + wavelength = HC_KEV_ANGSTROM / (energy_eV * 1e-3) + k0 = 2.0 * np.pi / wavelength + alpha_rad = np.deg2rad(alpha) + + z_ascending = profile[:, 0] + n_ascending = 1.0 - profile[:, 1] - 1j * profile[:, 2] + n = np.ascontiguousarray(n_ascending[::-1]) + z_interfaces = np.ascontiguousarray(boundaries[::-1]) + q = _normal_wavevector(n, k0, alpha_rad) + kz = -q + admittance = _admittance(n, q, polarization) + + interface_r = (admittance[:-1] - admittance[1:]) / ( + admittance[:-1] + admittance[1:] + ) + reflection = np.empty(len(interface_r), dtype=np.complex128) + reflection[-1] = interface_r[-1] + for interface in range(len(interface_r) - 2, -1, -1): + thickness = z_interfaces[interface] - z_interfaces[interface + 1] + phase = np.exp(-1j * q[interface + 1] * thickness) + reflected_below = reflection[interface + 1] * phase**2 + reflection[interface] = ( + interface_r[interface] + reflected_below + ) / (1.0 + interface_r[interface] * reflected_below) + + A_plus = np.empty(len(n), dtype=np.complex128) + A_minus = np.empty(len(n), dtype=np.complex128) + z_reference = np.empty(len(n), dtype=np.float64) + A_plus[0] = 1.0 + A_minus[0] = reflection[0] + z_reference[0] = z_interfaces[0] + + down_at_interface = A_plus[0] + up_at_interface = A_minus[0] + for interface in range(len(interface_r)): + field = down_at_interface + up_at_interface + derivative = admittance[interface] * ( + down_at_interface - up_at_interface + ) + A_plus[interface + 1] = 0.5 * ( + field + derivative / admittance[interface + 1] + ) + A_minus[interface + 1] = 0.5 * ( + field - derivative / admittance[interface + 1] + ) + z_reference[interface + 1] = z_interfaces[interface] + if interface + 1 < len(interface_r): + thickness = z_interfaces[interface] - z_interfaces[interface + 1] + phase = np.exp(-1j * q[interface + 1] * thickness) + down_at_interface = A_plus[interface + 1] * phase + up_at_interface = A_minus[interface + 1] / phase + + psi = np.empty(len(n), dtype=np.complex128) + for ascending_index, z_value in enumerate(z_ascending): + medium = len(n) - 1 - ascending_index + depth = z_reference[medium] - z_value + psi[ascending_index] = ( + A_plus[medium] * np.exp(-1j * q[medium] * depth) + + A_minus[medium] * np.exp(1j * q[medium] * depth) + ) + psi = np.ascontiguousarray(psi) + + return Wavefield( + z=np.ascontiguousarray(z_ascending), + psi=psi, + z_interfaces=z_interfaces, + z_reference=np.ascontiguousarray(z_reference), + n=n, + kz=np.ascontiguousarray(kz), + A_plus=np.ascontiguousarray(A_plus), + A_minus=np.ascontiguousarray(A_minus), + r_S=complex(A_minus[0]), + t_S=complex(A_plus[-1]), + polarization=polarization, + ) diff --git a/orgui/datautils/xrayutils/CTRuc.py b/orgui/datautils/xrayutils/CTRuc.py index a53d58f..5fa0ae3 100644 --- a/orgui/datautils/xrayutils/CTRuc.py +++ b/orgui/datautils/xrayutils/CTRuc.py @@ -493,13 +493,16 @@ def lookupScatteringFactors(self, E): + 1j * (self.f[0, 12] + 2.0 * self.f[1, 12]) ) - def optical_profile(self, noUC=30, z_step=0.6): + def optical_profile(self, noUC=30, z_step=None, z_origin=None): """Return continuous water optical constants sampled towards positive z. :param int noUC: Length of the sampled water region in water-model unit cells. :param float z_step: - Uniform z sampling interval in Angstrom. + Uniform z sampling interval in Angstrom. Defaults to this model's + lattice height. + :param float z_origin: + Optional atomistic-grid origin in Angstrom used to align samples. :returns: ``(N, 3)`` float64 array containing z in Angstrom, delta, and beta. @@ -507,7 +510,9 @@ def optical_profile(self, noUC=30, z_step=0.6): """ from .CTRoptics import water_optical_profile - return water_optical_profile(self, noUC=noUC, z_step=z_step) + return water_optical_profile( + self, noUC=noUC, z_step=z_step, z_origin=z_origin + ) # f1,f2 = UnitCell.special_formfactors['H2O'][1](E) # self.wat_dispersion = f1 + 1j*f2 diff --git a/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py b/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py new file mode 100644 index 0000000..48e4c18 --- /dev/null +++ b/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py @@ -0,0 +1,403 @@ +import unittest + +import numpy as np +import xraydb + +from .. import CTRcalc, CTRfilm, CTRoptics, unitcells + + +AVOGADRO = 6.02214076e23 + + +def density_from_cell(formula_mass, formula_units, volume): + """Return density in g / cm**3 from an Angstrom**3 cell volume.""" + return formula_mass * formula_units / (volume * 1e-24 * AVOGADRO) + + +class TestUnitCellOpticalProfile(unittest.TestCase): + energy_eV = 10000.0 + + def test_requires_energy(self): + cell = CTRcalc.UnitCell([3.0, 3.0, 4.0], [90.0, 90.0, 90.0]) + cell.addAtom("C", [0.0, 0.0, 0.0], 0.1, 0.1, 1.0) + + with self.assertRaisesRegex(ValueError, "Set the UnitCell energy"): + cell.optical_profile() + + def test_pt100_matches_xraydb(self): + cell = unitcells.unitcell("Pt100") + cell.setEnergy(self.energy_eV) + profile = cell.optical_profile() + np.testing.assert_array_equal(profile, CTRoptics.optical_profile(cell)) + density = density_from_cell( + xraydb.atomic_mass("Pt"), 4, cell.volume + ) + delta, beta, _ = xraydb.xray_delta_beta("Pt", density, self.energy_eV) + + self.assertEqual(profile.shape, (1, 3)) + self.assertEqual(profile.dtype, np.float64) + self.assertTrue(profile.flags.c_contiguous) + self.assertEqual(profile[0, 0], 0.0) + np.testing.assert_allclose(profile[:, 1], delta, rtol=1e-7) + np.testing.assert_allclose(profile[:, 2], beta, rtol=1e-7) + + def test_tio2_100_has_two_homogeneous_layers(self): + cell = unitcells.crystal("TiO2(100)").uc_bulk + cell.setEnergy(self.energy_eV) + profile = cell.optical_profile() + formula_mass = xraydb.atomic_mass("Ti") + 2.0 * xraydb.atomic_mass("O") + density = density_from_cell(formula_mass, 4, cell.volume) + delta, beta, _ = xraydb.xray_delta_beta("TiO2", density, self.energy_eV) + + self.assertEqual(profile.shape, (2, 3)) + np.testing.assert_allclose(profile[:, 0], [-6.5807, -3.29035]) + np.testing.assert_allclose(profile[:, 1], delta, rtol=1e-7) + np.testing.assert_allclose(profile[:, 2], beta, rtol=1e-7) + + def test_domain_translation_and_occupancy_weight_optical_contribution(self): + cell = unitcells.unitcell("Pt100") + cell.setEnergy(self.energy_eV) + translated = np.eye(3, 4) + translated[2, 3] = 0.25 + cell.coherentDomainMatrix = [np.eye(3, 4), translated] + cell.coherentDomainOccupancy = [0.75, 0.25] + + profile = cell.optical_profile() + + np.testing.assert_allclose(profile[:, 0], [0.0, 0.25 * cell.a[2]]) + np.testing.assert_allclose(profile[1, 1], profile[0, 1] / 3.0) + np.testing.assert_allclose(profile[1, 2], profile[0, 2] / 3.0) + + def test_film_and_crystal_combine_layer_profiles(self): + bulk = unitcells.crystal("TiO2(100)").uc_bulk + bulk.setEnergy(self.energy_eV) + film = CTRfilm.Film(bulk) + film.basis[0] = 2.0 + + film_profile = film.optical_profile() + crystal_profile = CTRcalc.SXRDCrystal(bulk, film).optical_profile() + + self.assertEqual(film_profile.shape, (2, 3)) + np.testing.assert_allclose(film_profile[:, 0], [0.0, 3.29035]) + self.assertEqual(crystal_profile.shape, (63, 3)) + np.testing.assert_allclose( + crystal_profile[-5:, 0], + [-6.5807, -3.29035, 0.0, 3.29035, 6.5807], + ) + np.testing.assert_allclose( + crystal_profile[:-1, 1:], + np.tile(film_profile[0, 1:], (62, 1)), + ) + np.testing.assert_array_equal(crystal_profile[-1, 1:], [0.0, 0.0]) + + def test_combine_profiles_sums_coincident_positions(self): + combined = CTRoptics.combine_profiles( + np.array([[1.0, 2.0, 3.0]]), + np.array([[1.0, 4.0, 5.0], [2.0, 6.0, 7.0]]), + ) + + np.testing.assert_allclose(combined, [[1.0, 6.0, 8.0], [2.0, 6.0, 7.0]]) + self.assertTrue(combined.flags.c_contiguous) + + def test_combine_profiles_merges_nearby_layer_origins(self): + combined = CTRoptics.combine_profiles( + np.array([[3.2728, 2.0, 3.0]]), + np.array([[3.2731576542, 4.0, 5.0], [4.0, 6.0, 7.0]]), + ) + + np.testing.assert_allclose(combined, [[3.2728, 6.0, 8.0], [4.0, 6.0, 7.0]]) + + def test_structural_layers_are_added_to_continuous_samples(self): + combined = CTRoptics.add_structural_to_sampled_profile( + np.array([[1.1, 2.0, 3.0], [4.0, 5.0, 6.0]]), + np.array([[1.0, 10.0, 20.0], [1.6, 10.0, 20.0], [2.2, 10.0, 20.0]]), + ) + + np.testing.assert_allclose( + combined, + [[1.0, 12.0, 23.0], [1.6, 10.0, 20.0], [2.2, 10.0, 20.0], [4.0, 5.0, 6.0]], + ) + + def test_bulk_profile_repeats_towards_negative_z(self): + bulk = unitcells.unitcell("Pt100") + bulk.setEnergy(self.energy_eV) + + profile = bulk.optical_profile_asbulk(noUC=3) + + self.assertEqual(profile.shape, (3, 3)) + np.testing.assert_allclose(profile[:, 0], [0.0, -bulk.a[2], -2 * bulk.a[2]]) + np.testing.assert_allclose(profile[:, 1:], np.tile(profile[0, 1:], (3, 1))) + + def test_simplify_profile_conserves_finite_optical_thickness(self): + profile = np.array( + [ + [0.0, 2.0e-6, 2.0e-8], + [1.0, 1.0e-6, 1.0e-8], + [3.0, 1.1e-6, 1.1e-8], + [6.0, 1.2e-6, 1.2e-8], + [10.0, 5.0e-6, 5.0e-8], + [12.0, 0.0, 0.0], + ] + ) + + stratified = CTRoptics.stratify_profile( + profile, delta_tolerance=0.25e-6, beta_tolerance=0.25e-8 + ) + simplified = stratified.values + + np.testing.assert_allclose( + simplified[:, 0], [0.0, 4.25, 9.5, 12.0] + ) + np.testing.assert_allclose(stratified.boundaries, [0.5, 8.0, 11.0]) + original_boundaries = CTRoptics.profile_boundaries(profile) + original_integral = np.sum( + profile[1:-1, 1:] + * np.diff(original_boundaries)[:, None], + axis=0, + ) + simplified_integral = np.sum( + simplified[1:-1, 1:] + * np.diff(stratified.boundaries)[:, None], + axis=0, + ) + np.testing.assert_allclose(simplified_integral, original_integral) + np.testing.assert_array_equal(simplified[[0, -1]], profile[[0, -1]]) + + def test_simplify_profile_respects_beta_and_group_range(self): + profile = np.array( + [ + [0.0, 2.0e-6, 2.0e-8], + [1.0, 1.00e-6, 1.0e-8], + [2.0, 1.09e-6, 1.0e-8], + [3.0, 1.18e-6, 4.0e-8], + [4.0, 0.0, 0.0], + ] + ) + + simplified = CTRoptics.simplify_profile( + profile, delta_tolerance=0.15e-6, beta_tolerance=0.5e-8 + ) + + np.testing.assert_allclose(simplified[:, 0], [0.0, 1.5, 3.0, 4.0]) + + def test_profile_boundaries_are_centered_between_samples(self): + profile = np.array( + [ + [-4.0, 2.0e-6, 0.0], + [2.0, 1.0e-6, 0.0], + [10.0, 0.0, 0.0], + ] + ) + + stratified = CTRoptics.stratify_profile(profile) + + np.testing.assert_array_equal(stratified.values, profile) + np.testing.assert_allclose(stratified.boundaries, [-1.0, 6.0]) + + def test_water_profile_matches_bulk_water_and_stacks_on_film(self): + bulk = unitcells.crystal("TiO2(100)").uc_bulk + bulk.setEnergy(self.energy_eV) + film = CTRfilm.Film(bulk) + film.basis[0] = 2.0 + water = CTRcalc.WaterModel(bulk.a, bulk.alpha, "step") + water.setEnergy(self.energy_eV) + crystal = CTRcalc.SXRDCrystal( + bulk, film, water, stacking=np.array([1, 2]) + ) + + crystal.apply_stacking() + self.assertAlmostEqual(water.pos_absolute, film.stacking_height_absolute) + initial_water_position = water.pos_absolute + dz = bulk.a[2] / 2.0 + profile = water.optical_profile(noUC=10, z_step=dz) + self.assertTrue(np.allclose(np.diff(profile[:, 0]), dz)) + density = water.pw * 18.01528 * 1.66053906660 + expected = xraydb.xray_delta_beta("H2O", density, self.energy_eV) + np.testing.assert_allclose(profile[-1, 1:], expected[:2], rtol=5e-5) + + crystal_profile = crystal.optical_profile() + np.testing.assert_allclose(crystal_profile[-1, 1:], expected[:2], rtol=5e-5) + water_region = crystal_profile[crystal_profile[:, 0] >= profile[0, 0]] + np.testing.assert_allclose(np.diff(water_region[:, 0]), dz) + + film.basis[0] = 4.0 + crystal.apply_stacking() + self.assertAlmostEqual(water.pos_absolute, film.stacking_height_absolute) + self.assertGreater(water.pos_absolute, initial_water_position) + + +class TestLayeredWavefield(unittest.TestCase): + energy_eV = 10000.0 + + @staticmethod + def _q(n, energy_eV, alpha): + wavelength = CTRoptics.HC_KEV_ANGSTROM / (energy_eV * 1e-3) + k0 = 2.0 * np.pi / wavelength + if n == 1.0: + return k0 * np.sin(np.deg2rad(alpha)) + q = k0 * np.sqrt( + n**2 - np.cos(np.deg2rad(alpha)) ** 2 + ) + if q.imag > 0.0: + q = -q + return q + + def test_single_interface_matches_fresnel_and_conserves_flux(self): + profile = np.array([[-10.0, 1.0e-5, 0.0], [0.0, 0.0, 0.0]]) + alpha = 1.0 + + for polarization in ("s", "p"): + field = CTRoptics.solve_wavefield( + profile, self.energy_eV, alpha, polarization + ) + n = np.array([1.0 + 0.0j, 1.0 - 1.0e-5 + 0.0j]) + q = np.array([self._q(value, self.energy_eV, alpha) for value in n]) + admittance = q if polarization == "s" else n**2 / q + expected_r = (admittance[0] - admittance[1]) / ( + admittance[0] + admittance[1] + ) + expected_t = 2.0 * admittance[0] / ( + admittance[0] + admittance[1] + ) + + np.testing.assert_allclose(field.r_S, expected_r, rtol=1e-13) + np.testing.assert_allclose(field.t_S, expected_t, rtol=1e-13) + flux = abs(field.r_S) ** 2 + ( + admittance[1].real / admittance[0].real + ) * abs(field.t_S) ** 2 + np.testing.assert_allclose(flux, 1.0, rtol=1e-12) + np.testing.assert_allclose(field.A_minus[-1], 0.0, atol=1e-14) + + def test_non_vacuum_incident_medium_uses_its_tangential_wavevector(self): + incident_delta = 4.6e-7 + substrate_delta = 2.05e-6 + profile = np.array( + [ + [-10.0, substrate_delta, 0.0], + [0.0, incident_delta, 0.0], + ] + ) + alpha = 0.02 + + field = CTRoptics.solve_wavefield( + profile, self.energy_eV, alpha, "s" + ) + wavelength = CTRoptics.HC_KEV_ANGSTROM / (self.energy_eV * 1e-3) + k0 = 2.0 * np.pi / wavelength + n_incident = 1.0 - incident_delta + expected_q_incident = ( + k0 * n_incident * np.sin(np.deg2rad(alpha)) + ) + + np.testing.assert_allclose(-field.kz[0], expected_q_incident, rtol=1e-13) + np.testing.assert_allclose(abs(field.r_S) ** 2, 1.0, rtol=1e-12) + + def test_amplitudes_satisfy_interface_continuity(self): + profile = np.array( + [ + [-12.0, 1.2e-5, 0.0], + [0.0, 6.0e-6, 0.0], + [20.0, 0.0, 0.0], + ] + ) + field = CTRoptics.solve_wavefield(profile, self.energy_eV, 1.2, "p") + q = -field.kz + admittance = field.n**2 / q + + for interface, z_value in enumerate(field.z_interfaces): + depth = field.z_reference[interface] - z_value + down = field.A_plus[interface] * np.exp( + -1j * q[interface] * depth + ) + up = field.A_minus[interface] * np.exp( + 1j * q[interface] * depth + ) + lower_down = field.A_plus[interface + 1] + lower_up = field.A_minus[interface + 1] + np.testing.assert_allclose( + down + up, lower_down + lower_up, rtol=1e-12, atol=1e-12 + ) + np.testing.assert_allclose( + admittance[interface] * (down - up), + admittance[interface + 1] * (lower_down - lower_up), + rtol=1e-12, + atol=1e-12, + ) + + def test_absorbing_substrate_field_decays_with_depth(self): + profile = np.array([[-40.0, 1.0e-5, 2.0e-6], [0.0, 0.0, 0.0]]) + field = CTRoptics.solve_wavefield(profile, self.energy_eV, 1.0, "s") + + self.assertLess(abs(field.psi[0]), abs(field.t_S)) + q_substrate = -field.kz[-1] + depth = field.z_interfaces[-1] - field.z[0] + expected = abs(field.t_S) * np.exp(q_substrate.imag * depth) + np.testing.assert_allclose(abs(field.psi[0]), expected, rtol=1e-12) + + def test_single_interface_critical_angle(self): + delta = 1.0e-5 + profile = np.array([[-10.0, delta, 0.0], [0.0, 0.0, 0.0]]) + alpha_c = np.rad2deg(np.sqrt(2.0 * delta)) + + below = CTRoptics.solve_wavefield( + profile, self.energy_eV, 0.5 * alpha_c, "s" + ) + above = CTRoptics.solve_wavefield( + profile, self.energy_eV, 2.0 * alpha_c, "s" + ) + + np.testing.assert_allclose(abs(below.r_S) ** 2, 1.0, rtol=1e-12) + self.assertLess(abs(above.r_S) ** 2, 1.0) + self.assertLess((-below.kz[-1]).imag, 0.0) + + def test_one_film_matches_analytic_slab_expression(self): + thickness = 35.0 + profile = np.array( + [ + [-0.5 * thickness, 1.4e-5, 0.0], + [0.5 * thickness, 5.0e-6, 0.0], + [1.5 * thickness, 0.0, 0.0], + ] + ) + alpha = 1.5 + field = CTRoptics.solve_wavefield(profile, self.energy_eV, alpha, "s") + n = np.array([1.0, 1.0 - 5.0e-6, 1.0 - 1.4e-5]) + q = np.array([self._q(value, self.energy_eV, alpha) for value in n]) + r01 = (q[0] - q[1]) / (q[0] + q[1]) + r12 = (q[1] - q[2]) / (q[1] + q[2]) + phase = np.exp(-1j * q[1] * thickness) + expected = (r01 + r12 * phase**2) / ( + 1.0 + r01 * r12 * phase**2 + ) + + np.testing.assert_allclose(field.r_S, expected, rtol=1e-12) + + def test_crystal_reflectivity_polarizations(self): + bulk = unitcells.unitcell("Pt100") + bulk.setEnergy(self.energy_eV) + crystal = CTRcalc.SXRDCrystal(bulk) + angles = np.array([0.5, 1.0, 2.0]) + + s = crystal.specular_reflectivity(angles, "s") + p = crystal.specular_reflectivity(angles, "p") + unpolarized = crystal.specular_reflectivity(angles, "unpolarized") + + np.testing.assert_allclose(unpolarized, 0.5 * (s + p)) + self.assertIsInstance(crystal.specular_reflectivity(1.0), float) + self.assertEqual(crystal.wavefield(1.0).polarization, "s") + + def test_crystal_simplification_removes_redundant_bulk_layers(self): + bulk = unitcells.unitcell("Pt100") + bulk.setEnergy(self.energy_eV) + crystal = CTRcalc.SXRDCrystal(bulk) + full = crystal.optical_profile() + simplified = crystal.simplified_optical_profile() + angles = np.array([0.5, 1.0, 2.0]) + + self.assertEqual(len(full), 31) + self.assertEqual(len(simplified), 3) + exact = crystal.specular_reflectivity(angles, delta_tolerance=None) + reduced = crystal.specular_reflectivity( + angles, delta_tolerance=1e-9 + ) + np.testing.assert_allclose(reduced, exact, rtol=1e-13, atol=1e-15) From 393aec6c682f27052887ab8d8c0c51d61aa4b773 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Thu, 16 Jul 2026 00:23:09 -0400 Subject: [PATCH 05/37] feat(phys): add Wyckoff setters and compact symmetry metadata --- doc/source/wyckoff_fitting.rst | 35 ++- orgui/datautils/xrayutils/CTRsymmetry.py | 179 +++++++++++--- orgui/datautils/xrayutils/CTRuc.py | 220 ++++++++++++++++++ .../xrayutils/test/test_CTRsymmetry.py | 159 +++++++++++++ 4 files changed, 555 insertions(+), 38 deletions(-) diff --git a/doc/source/wyckoff_fitting.rst b/doc/source/wyckoff_fitting.rst index 5abf3bb..b4d6c42 100644 --- a/doc/source/wyckoff_fitting.rst +++ b/doc/source/wyckoff_fitting.rst @@ -59,12 +59,34 @@ Querying Wyckoff Metadata .. code-block:: python unitcell.wyckoff_sites() + unitcell.wyckoff("O_4f") unitcell.wyckoff_couplings("O_4f") unitcell.wyckoff_site_couplings("O_4f") unitcell.atom_wyckoff_metadata(0) ``wyckoff_sites()`` returns site dictionaries with the site id, element, -Wyckoff label, free variables, generated atom indices, space group, and status. +Wyckoff label, free variables, generated atom indices, site occupancy and +Debye-Waller parameters, space group, and status. ``wyckoff(site_id)`` returns +the same dictionary for one site. + +Direct convenience setters use symmetric signatures for atom and site data: + +.. code-block:: python + + # Surface-cell atom coordinates or per-atom physical parameters. + unitcell.set_wyckoff_atom_parameter("O_4f", "occ", 0.5) + unitcell.set_wyckoff_atom_parameter("O_4f", "z", oxygen_z_values) + + # Representative coordinate in the parent conventional cell. + unitcell.set_wyckoff_site_parameter("O_4f", "x", 0.31) + unitcell.set_wyckoff_site_parameter("O_4f", "iDW", 0.6) + +The atom setter accepts ``x``, ``y``, ``z``, ``iDW``, ``oDW``, and ``occ``. +Coordinate values are in surface-cell fractional units and may be supplied per +atom; ``iDW``, ``oDW``, and ``occ`` are scalar site-wide values. The site setter +accepts ``x``, ``y``, and ``z`` in parent conventional fractional units and +propagates the displacement through the stored symmetry couplings. It also +accepts scalar site-wide ``iDW``, ``oDW``, and ``occ`` values. The status is one of: ``metadata_only`` @@ -225,8 +247,12 @@ after the layer metadata. The persisted information includes: * surface transform and origin; * Wyckoff site ids, labels, variables, and representative parent coordinates; * generated atom metadata; -* Wyckoff variable couplings; -* representative-site shift couplings. +* deduplicated matrices for Wyckoff-variable and representative-site + couplings, referenced by the generated atoms. + +The matrix representation avoids repeating one text row for every nonzero +atom/coordinate coupling. The reader also accepts the older expanded +``wyckoff_couplings`` and ``wyckoff_site_couplings`` tables. Reloaded files can be queried and fitted with ``addWyckoffParameter`` and ``addWyckoffShift`` without PyXtal because the resolved couplings are already @@ -242,7 +268,8 @@ API Reference :no-index: .. autoclass:: orgui.datautils.xrayutils.CTRuc.UnitCell - :members: wyckoff_sites, wyckoff_couplings, wyckoff_site_couplings, + :members: wyckoff_sites, wyckoff, set_wyckoff_atom_parameter, + set_wyckoff_site_parameter, wyckoff_couplings, wyckoff_site_couplings, atom_wyckoff_metadata, addWyckoffParameter, addWyckoffParameters, addWyckoffShift, addWyckoffShifts :member-order: bysource diff --git a/orgui/datautils/xrayutils/CTRsymmetry.py b/orgui/datautils/xrayutils/CTRsymmetry.py index e11b570..69fb344 100644 --- a/orgui/datautils/xrayutils/CTRsymmetry.py +++ b/orgui/datautils/xrayutils/CTRsymmetry.py @@ -46,6 +46,8 @@ "surface_transform:", "wyckoff_sites:", "wyckoff_atoms:", + "wyckoff_coupling_matrices:", + "wyckoff_site_coupling_matrices:", "wyckoff_couplings:", "wyckoff_site_couplings:", } @@ -392,6 +394,9 @@ def wyckoff_sites(self, parameters=None): "element": site.element, "wyckoff_label": site.wyckoff_label, "variables": variables, + "occ": site.occ, + "iDW": site.iDW, + "oDW": site.oDW, "representative_parent_fractional": ( None if site.representative_parent_fractional is None @@ -965,10 +970,35 @@ def symmetry_metadata_to_lines(model): ] ) ) + coordinate_matrices, coordinate_matrix_ids = _compact_coupling_matrices(model) + site_matrices, site_matrix_ids = _compact_site_coupling_matrices(model) + lines.append("wyckoff_coupling_matrices:") + lines.append(" matrix_id variables factors_by_surface_coordinate") + for matrix_id, variables, matrix in coordinate_matrices: + lines.append( + " " + + " ".join( + [ + matrix_id, + ",".join(variables), + *_format_values(matrix.ravel()).split(), + ] + ) + ) + lines.append("wyckoff_site_coupling_matrices:") + lines.append(" matrix_id parent_to_surface_jacobian") + for matrix_id, matrix in site_matrices: + lines.append( + " " + + " ".join( + [matrix_id, *_format_values(matrix.ravel()).split()] + ) + ) lines.append("wyckoff_atoms:") lines.append( " atom_index element site_id wyckoff_label " - "parent_x parent_y parent_z surface_x surface_y surface_z layer" + "parent_x parent_y parent_z surface_x surface_y surface_z layer " + "coupling_matrix site_coupling_matrix" ) for atom in model.atoms: values = [ @@ -979,42 +1009,64 @@ def symmetry_metadata_to_lines(model): *_format_values(atom.parent_fractional).split(), *_format_values(atom.surface_fractional).split(), _format_float(atom.layer), + coordinate_matrix_ids.get(atom.atom_index, "-"), + site_matrix_ids.get(atom.atom_index, "-"), ] lines.append(" " + " ".join(values)) - lines.append("wyckoff_couplings:") - lines.append(" atom_index coordinate site_id variable constant factor") - for coupling in model.wyckoff_couplings(): - lines.append( - " " - + " ".join( - [ - str(coupling.atom_index), - coupling.coordinate, - coupling.site_id, - coupling.variable, - _format_float(coupling.constant), - _format_float(coupling.factor), - ] - ) - ) - lines.append("wyckoff_site_couplings:") - lines.append(" atom_index coordinate site_id axis factor") - for coupling in model.wyckoff_site_couplings(): - lines.append( - " " - + " ".join( - [ - str(coupling.atom_index), - coupling.coordinate, - coupling.site_id, - coupling.axis, - _format_float(coupling.factor), - ] - ) - ) return lines +def _compact_coupling_matrices(model): + matrices = [] + matrix_ids = {} + known = {} + sites = {site.site_id: site for site in model.sites} + coordinate_index = {name: index for index, name in enumerate(COORDINATE_NAMES)} + for atom in model.atoms: + variables = tuple(sites[atom.site_id].variables) + if not atom.couplings: + continue + variable_index = {name: index for index, name in enumerate(variables)} + matrix = np.zeros((3, len(variables)), dtype=np.float64) + for coupling in atom.couplings: + matrix[ + coordinate_index[coupling.coordinate], + variable_index[coupling.variable], + ] = coupling.factor + key = (variables, tuple(_format_float(value) for value in matrix.ravel())) + matrix_id = known.get(key) + if matrix_id is None: + matrix_id = f"c{len(matrices)}" + known[key] = matrix_id + matrices.append((matrix_id, variables, matrix)) + matrix_ids[atom.atom_index] = matrix_id + return matrices, matrix_ids + + +def _compact_site_coupling_matrices(model): + matrices = [] + matrix_ids = {} + known = {} + coordinate_index = {name: index for index, name in enumerate(COORDINATE_NAMES)} + for atom in model.atoms: + if not atom.site_couplings: + continue + matrix = np.zeros((3, 3), dtype=np.float64) + for coupling in atom.site_couplings: + matrix[ + coordinate_index[coupling.coordinate], + coordinate_index[coupling.axis], + ] = coupling.factor + key = tuple(_format_float(value) for value in matrix.ravel()) + matrix_id = known.get(key) + if matrix_id is None: + matrix_id = f"s{len(matrices)}" + known[key] = matrix_id + matrices.append((matrix_id, matrix)) + matrix_ids[atom.atom_index] = matrix_id + return matrices, matrix_ids + + def symmetry_metadata_from_lines(lines, unitcell=None): """Deserialize plain-text table lines into a symmetry metadata model. @@ -1042,6 +1094,8 @@ def symmetry_metadata_from_lines(lines, unitcell=None): atoms = [] couplings_by_atom = {} site_couplings_by_atom = {} + coupling_matrices = {} + site_coupling_matrices = {} sections = _collect_sections(cleaned) if "spacegroup" in sections: @@ -1102,6 +1156,21 @@ def symmetry_metadata_from_lines(lines, unitcell=None): ) ) + for row in _data_rows(sections.get("wyckoff_coupling_matrices", [])): + parts = row.split() + variables = tuple(parts[1].split(",")) + coupling_matrices[parts[0]] = ( + variables, + np.asarray(parts[2:], dtype=np.float64).reshape(3, len(variables)), + ) + + for row in _data_rows(sections.get("wyckoff_site_coupling_matrices", [])): + parts = row.split() + site_coupling_matrices[parts[0]] = np.asarray( + parts[1:], + dtype=np.float64, + ).reshape(3, 3) + for row in _data_rows(sections.get("wyckoff_couplings", [])): parts = row.split() coupling = WyckoffCoupling( @@ -1128,14 +1197,55 @@ def symmetry_metadata_from_lines(lines, unitcell=None): for row in _data_rows(sections.get("wyckoff_atoms", [])): parts = row.split() atom_index = int(parts[0]) + site_id = parts[2] + surface_fractional = np.asarray(parts[7:10], dtype=np.float64) + if len(parts) >= 13 and parts[11] != "-": + variables, matrix = coupling_matrices[parts[11]] + site = next(site for site in site_specs if site.site_id == site_id) + variable_values = np.asarray( + [site.variables[variable] for variable in variables], + dtype=np.float64, + ) + for coordinate_index, coordinate in enumerate(COORDINATE_NAMES): + constant = surface_fractional[coordinate_index] - ( + matrix[coordinate_index] @ variable_values + ) + for variable_index, variable in enumerate(variables): + factor = matrix[coordinate_index, variable_index] + if not math.isclose(factor, 0.0, abs_tol=1e-12): + couplings_by_atom.setdefault(atom_index, []).append( + WyckoffCoupling( + atom_index=atom_index, + coordinate=coordinate, + variable=variable, + constant=float(constant), + factor=float(factor), + site_id=site_id, + ) + ) + if len(parts) >= 13 and parts[12] != "-": + matrix = site_coupling_matrices[parts[12]] + for coordinate_index, coordinate in enumerate(COORDINATE_NAMES): + for axis_index, axis in enumerate(COORDINATE_NAMES): + factor = matrix[coordinate_index, axis_index] + if not math.isclose(factor, 0.0, abs_tol=1e-12): + site_couplings_by_atom.setdefault(atom_index, []).append( + WyckoffSiteCoupling( + atom_index=atom_index, + coordinate=coordinate, + axis=axis, + factor=float(factor), + site_id=site_id, + ) + ) atoms.append( GeneratedWyckoffAtom( atom_index=atom_index, element=parts[1], - site_id=parts[2], + site_id=site_id, wyckoff_label=parts[3], parent_fractional=np.asarray(parts[4:7], dtype=np.float64), - surface_fractional=np.asarray(parts[7:10], dtype=np.float64), + surface_fractional=surface_fractional, layer=int(float(parts[10])), couplings=tuple(couplings_by_atom.get(atom_index, ())), site_couplings=tuple(site_couplings_by_atom.get(atom_index, ())), @@ -1423,6 +1533,7 @@ def _data_rows(rows): if stripped.split()[0] in { "site_id", "atom_index", + "matrix_id", }: continue yield stripped diff --git a/orgui/datautils/xrayutils/CTRuc.py b/orgui/datautils/xrayutils/CTRuc.py index 5fa0ae3..c571c82 100644 --- a/orgui/datautils/xrayutils/CTRuc.py +++ b/orgui/datautils/xrayutils/CTRuc.py @@ -2280,6 +2280,226 @@ def wyckoff_sites(self): return [] return self.symmetry_metadata.wyckoff_sites(self.parameters) + def wyckoff(self, site_id): + """Return metadata for one Wyckoff site. + + :param str site_id: + Site identifier returned by :meth:`wyckoff_sites`. + :returns: + The matching site metadata dictionary. + :rtype: + dict + :raises ValueError: + If this unit cell has no symmetry metadata or the site is unknown. + """ + return self._wyckoff_site(site_id) + + def set_wyckoff_atom_parameter(self, site_id, parameter, value): + """Set one field on every generated atom in a Wyckoff site. + + Coordinates are fractional coordinates in the surface unit cell. + Coordinate values may be a scalar, applied to every atom, or one value + per atom in the order reported by + ``wyckoff(site_id)["atom_indices"]``. The site-wide ``iDW``, ``oDW``, + and ``occ`` fields require a scalar. Both the active basis and its + unfitted baseline are updated. + + :param str site_id: + Site identifier returned by :meth:`wyckoff_sites`. + :param str parameter: + One of ``"x"``, ``"y"``, ``"z"``, ``"iDW"``, ``"oDW"``, + or ``"occ"``. + :param value: + Scalar value or one value per generated atom. + :raises ValueError: + If the parameter is invalid or the value cannot be broadcast to + the atoms in the site. + """ + allowed = {"x", "y", "z", "iDW", "oDW", "occ"} + if parameter not in allowed: + raise ValueError( + "parameter must be one of 'x', 'y', 'z', 'iDW', 'oDW', or 'occ'." + ) + site = self.wyckoff(site_id) + atoms = np.asarray(site["atom_indices"], dtype=np.intp) + if not len(atoms): + raise ValueError(f"Wyckoff site {site_id} has no generated atoms.") + value_array = np.asarray(value, dtype=np.float64) + if parameter in {"iDW", "oDW", "occ"} and value_array.ndim: + raise ValueError(f"{parameter} must be a scalar site-wide value.") + try: + values = np.broadcast_to( + value_array, + atoms.shape, + ) + except ValueError as error: + raise ValueError( + f"value must be scalar or contain one value for each of the " + f"{len(atoms)} atoms in Wyckoff site {site_id}." + ) from error + + column = self.parameterLookup[parameter] + self.basis_0[atoms, column] = values + self.basis[atoms, column] = values + self._update_wyckoff_metadata_from_atom_values( + site_id, + parameter, + atoms, + values, + ) + self._refresh_after_baseline_change() + + def set_wyckoff_site_parameter(self, site_id, parameter, value): + """Set one representative coordinate or site-wide physical parameter. + + An absolute parent conventional fractional coordinate is propagated to + all generated surface-cell atoms through their stored space-group and + surface-transform couplings. ``iDW``, ``oDW``, and ``occ`` are applied + as scalar values to every atom in the site. Both the active basis and + its unfitted baseline are updated. + + :param str site_id: + Site identifier returned by :meth:`wyckoff_sites`. + :param str parameter: + Parent conventional fractional axis ``"x"``, ``"y"``, or + ``"z"``; or site-wide ``"iDW"``, ``"oDW"``, or ``"occ"``. + :param float value: + New absolute coordinate in parent fractional units or scalar + physical parameter value. + :raises ValueError: + If the parameter is invalid or required symmetry metadata is absent. + """ + allowed = {"x", "y", "z", "iDW", "oDW", "occ"} + if parameter not in allowed: + raise ValueError( + "parameter must be one of 'x', 'y', 'z', 'iDW', 'oDW', or 'occ'." + ) + if parameter in {"iDW", "oDW", "occ"}: + self.set_wyckoff_atom_parameter(site_id, parameter, value) + return + + site = self.wyckoff(site_id) + representative = site.get("representative_parent_fractional") + if representative is None: + raise ValueError( + f"Wyckoff site {site_id} has no representative parent coordinate." + ) + + axis_index = {"x": 0, "y": 1, "z": 2}[parameter] + value = float(value) + delta = value - representative[axis_index] + couplings = [ + coupling + for coupling in self.wyckoff_site_couplings(site_id) + if coupling.axis == parameter + ] + if not couplings: + raise ValueError( + f"No site-displacement couplings found for Wyckoff site " + f"{site_id} and parent axis {parameter}." + ) + + surface_deltas = {} + for coupling in couplings: + column = self.parameterLookup[coupling.coordinate] + change = delta * coupling.factor + self.basis_0[coupling.atom_index, column] += change + self.basis[coupling.atom_index, column] += change + surface_deltas.setdefault( + coupling.atom_index, + np.zeros(3, dtype=np.float64), + )[{"x": 0, "y": 1, "z": 2}[coupling.coordinate]] += change + + self._update_wyckoff_metadata_from_site_value( + site_id, + axis_index, + value, + surface_deltas, + ) + self._refresh_after_baseline_change() + + def _refresh_after_baseline_change(self): + self.errors = None + self._basis_parvalues = None + self._errors_parvalues = None + if any(self.parameters.values()): + self.updateFromParameters() + + def _update_wyckoff_metadata_from_atom_values( + self, + site_id, + parameter, + atoms, + values, + ): + model = self.symmetry_metadata + if parameter in {"iDW", "oDW", "occ"}: + model.sites = tuple( + dataclasses.replace(site, **{parameter: float(values[0])}) + if site.site_id == site_id + else site + for site in model.sites + ) + return + + coordinate = {"x": 0, "y": 1, "z": 2}[parameter] + values_by_atom = dict(zip(atoms.tolist(), values.tolist())) + transform = model.surface_spec.transform + updated_atoms = [] + for atom in model.atoms: + if atom.atom_index not in values_by_atom: + updated_atoms.append(atom) + continue + surface = np.array(atom.surface_fractional, copy=True) + surface_delta = values_by_atom[atom.atom_index] - surface[coordinate] + surface[coordinate] = values_by_atom[atom.atom_index] + parent = np.array(atom.parent_fractional, copy=True) + parent += transform[:, coordinate] * surface_delta + updated_atoms.append( + dataclasses.replace( + atom, + surface_fractional=surface, + parent_fractional=parent, + ) + ) + model.atoms = updated_atoms + + def _update_wyckoff_metadata_from_site_value( + self, + site_id, + axis_index, + value, + surface_deltas, + ): + model = self.symmetry_metadata + model.sites = tuple( + dataclasses.replace( + site, + representative_parent_fractional=tuple( + value if index == axis_index else coordinate + for index, coordinate in enumerate( + site.representative_parent_fractional + ) + ), + ) + if site.site_id == site_id + else site + for site in model.sites + ) + transform = model.surface_spec.transform + model.atoms = [ + dataclasses.replace( + atom, + surface_fractional=np.asarray(atom.surface_fractional) + + surface_deltas[atom.atom_index], + parent_fractional=np.asarray(atom.parent_fractional) + + transform @ surface_deltas[atom.atom_index], + ) + if atom.atom_index in surface_deltas + else atom + for atom in model.atoms + ] + def wyckoff_couplings(self, site_id=None): """Return symmetry couplings for generated Wyckoff atom coordinates. diff --git a/orgui/datautils/xrayutils/test/test_CTRsymmetry.py b/orgui/datautils/xrayutils/test/test_CTRsymmetry.py index de863b1..7b1cef7 100644 --- a/orgui/datautils/xrayutils/test/test_CTRsymmetry.py +++ b/orgui/datautils/xrayutils/test/test_CTRsymmetry.py @@ -38,6 +38,139 @@ class TestSymmetryUtilities(unittest.TestCase): + @staticmethod + def make_metadata_unitcell(): + site = CTRsymmetry.WyckoffSiteSpec( + site_id="O_2a", + element="O", + wyckoff_label="2a", + coordinates=(), + representative_parent_fractional=(0.2, 0.3, 0.4), + occ=0.9, + iDW=0.5, + oDW=0.6, + ) + atoms = [] + for index, (position, x_factor) in enumerate( + (((0.2, 0.3, 0.4), 1.0), ((0.8, 0.7, 0.6), -1.0)) + ): + atoms.append( + CTRsymmetry.GeneratedWyckoffAtom( + atom_index=index, + element="O", + site_id="O_2a", + wyckoff_label="2a", + parent_fractional=np.asarray(position), + surface_fractional=np.asarray(position), + layer=0, + site_couplings=( + CTRsymmetry.WyckoffSiteCoupling( + atom_index=index, + coordinate="x", + axis="x", + factor=x_factor, + site_id="O_2a", + ), + ), + ) + ) + model = CTRsymmetry.SurfaceSymmetryModel( + CTRsymmetry.SurfaceCellSpec( + (4.0, 4.0, 4.0), + (90.0, 90.0, 90.0), + np.identity(3), + translation_range=0, + ), + (site,), + atoms, + ) + return model.build_unitcell("test") + + def test_wyckoff_returns_one_site_and_reports_site_parameters(self): + unitcell = self.make_metadata_unitcell() + + self.assertEqual(unitcell.wyckoff("O_2a"), unitcell.wyckoff_sites()[0]) + self.assertEqual(unitcell.wyckoff("O_2a")["occ"], 0.9) + self.assertEqual(unitcell.wyckoff("O_2a")["iDW"], 0.5) + self.assertEqual(unitcell.wyckoff("O_2a")["oDW"], 0.6) + with self.assertRaisesRegex(ValueError, "Unknown Wyckoff site"): + unitcell.wyckoff("missing") + + def test_set_wyckoff_atom_parameter_updates_all_site_atoms(self): + unitcell = self.make_metadata_unitcell() + + unitcell.set_wyckoff_atom_parameter("O_2a", "occ", 0.75) + unitcell.set_wyckoff_atom_parameter("O_2a", "z", [0.1, 0.9]) + + np.testing.assert_allclose(unitcell.basis[:, 6], 0.75) + np.testing.assert_allclose(unitcell.basis_0[:, 6], 0.75) + np.testing.assert_allclose(unitcell.basis[:, 3], [0.1, 0.9]) + np.testing.assert_allclose(unitcell.basis_0[:, 3], [0.1, 0.9]) + self.assertEqual(unitcell.wyckoff("O_2a")["occ"], 0.75) + np.testing.assert_allclose( + [atom.surface_fractional[2] for atom in unitcell.symmetry_metadata.atoms], + [0.1, 0.9], + ) + + def test_set_wyckoff_site_parameter_propagates_parent_coordinate(self): + unitcell = self.make_metadata_unitcell() + + unitcell.set_wyckoff_site_parameter("O_2a", "x", 0.25) + + np.testing.assert_allclose(unitcell.basis[:, 1], [0.25, 0.75]) + np.testing.assert_allclose(unitcell.basis_0[:, 1], [0.25, 0.75]) + self.assertEqual( + unitcell.wyckoff("O_2a")["representative_parent_fractional"], + (0.25, 0.3, 0.4), + ) + np.testing.assert_allclose( + [atom.parent_fractional[0] for atom in unitcell.symmetry_metadata.atoms], + [0.25, 0.75], + ) + + def test_set_wyckoff_site_parameter_sets_site_wide_physical_values(self): + unitcell = self.make_metadata_unitcell() + + unitcell.set_wyckoff_site_parameter("O_2a", "iDW", 0.7) + unitcell.set_wyckoff_site_parameter("O_2a", "oDW", 0.8) + unitcell.set_wyckoff_site_parameter("O_2a", "occ", 0.65) + + np.testing.assert_allclose(unitcell.basis[:, 4], 0.7) + np.testing.assert_allclose(unitcell.basis[:, 5], 0.8) + np.testing.assert_allclose(unitcell.basis[:, 6], 0.65) + self.assertEqual(unitcell.wyckoff("O_2a")["iDW"], 0.7) + self.assertEqual(unitcell.wyckoff("O_2a")["oDW"], 0.8) + self.assertEqual(unitcell.wyckoff("O_2a")["occ"], 0.65) + + def test_legacy_expanded_couplings_remain_readable(self): + lines = [ + "parent_a: 4 4 4", + "parent_alpha: 90 90 90", + "surface_transform:", + "1 0 0", + "0 1 0", + "0 0 1", + "wyckoff_sites:", + "site_id element wyckoff_label variables representative_x " + "representative_y representative_z occ iDW oDW", + "O_1a O 1a u=0.2 0.2 0 0 1 0.5 0.5", + "wyckoff_atoms:", + "atom_index element site_id wyckoff_label parent_x parent_y " + "parent_z surface_x surface_y surface_z layer", + "0 O O_1a 1a 0.2 0 0 0.2 0 0 0", + "wyckoff_couplings:", + "atom_index coordinate site_id variable constant factor", + "0 x O_1a u 0 1", + "wyckoff_site_couplings:", + "atom_index coordinate site_id axis factor", + "0 x O_1a x 1", + ] + + model = CTRsymmetry.symmetry_metadata_from_lines(lines) + + self.assertEqual(len(model.wyckoff_couplings("O_1a")), 1) + self.assertEqual(len(model.wyckoff_site_couplings("O_1a")), 1) + def test_duplicate_wyckoff_site_ids_are_disambiguated(self): sites = CTRsymmetry._with_unique_site_ids( ( @@ -447,11 +580,37 @@ def test_symmetry_metadata_round_trips_without_pyxtal_construction(self): unitcell = self.make_rutile_110_unitcell() text = unitcell.toStr() + self.assertIn("wyckoff_coupling_matrices:", text) + self.assertIn("wyckoff_site_coupling_matrices:", text) + self.assertNotIn("\nwyckoff_couplings:\n", text) + self.assertNotIn("\nwyckoff_site_couplings:\n", text) + restored = UnitCell.fromStr(text) self.assertEqual(restored.wyckoff_sites()[1]["variables"], {"u": 0.30569}) self.assertEqual(len(restored.wyckoff_couplings("O_4f")), 8) self.assertGreater(len(restored.wyckoff_site_couplings("Ru_2a")), 0) + original_couplings = sorted( + ( + coupling.atom_index, + coupling.coordinate, + coupling.variable, + round(coupling.constant, 10), + round(coupling.factor, 10), + ) + for coupling in unitcell.wyckoff_couplings() + ) + restored_couplings = sorted( + ( + coupling.atom_index, + coupling.coordinate, + coupling.variable, + round(coupling.constant, 10), + round(coupling.factor, 10), + ) + for coupling in restored.wyckoff_couplings() + ) + self.assertEqual(restored_couplings, original_couplings) restored.addWyckoffParameter("O_4f", "u", absolute_limits=(0.2, 0.4)) self.assertEqual(restored.wyckoff_sites()[1]["status"], "symmetry_preserving") From 2fa9e43eb71b34a9d23d4081f72534162c6c50f4 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Thu, 16 Jul 2026 01:43:01 -0400 Subject: [PATCH 06/37] fix: Wyckoff parameters are now fitted as absolute parameter (user facing, internal still relative) --- doc/source/wyckoff_fitting.rst | 36 ++-- orgui/datautils/xrayutils/CTRcalc.py | 85 +++++---- orgui/datautils/xrayutils/CTRfilm.py | 29 +-- orgui/datautils/xrayutils/CTRuc.py | 168 ++++++++++++------ .../datautils/xrayutils/test/test_CTRcalc.py | 113 ++++++++++-- .../xrayutils/test/test_CTRsymmetry.py | 29 ++- 6 files changed, 319 insertions(+), 141 deletions(-) diff --git a/doc/source/wyckoff_fitting.rst b/doc/source/wyckoff_fitting.rst index b4d6c42..de628b6 100644 --- a/doc/source/wyckoff_fitting.rst +++ b/doc/source/wyckoff_fitting.rst @@ -116,15 +116,17 @@ atom coordinates are updated through the stored affine Wyckoff couplings: unitcell.addWyckoffParameter( "O_4f", "u", - absolute_limits=(0.28, 0.34), + limits=(0.28, 0.34), ) -The fitted value is a delta from the stored variable value. For rutile oxygen, -coordinates such as ``u``, ``1-u``, ``0.5+u``, and ``0.5-u`` therefore move -with the correct signs. +The fitted value and limits are absolute Wyckoff-variable coordinates. The +implementation converts the absolute value to an internal delta from the +site's reference value and applies that delta through the affine couplings. +For rutile oxygen, coordinates such as ``u``, ``1-u``, ``0.5+u``, and +``0.5-u`` therefore move with the correct signs. -``addWyckoffShift`` fits a parent conventional fractional displacement of the -representative site coordinate: +``addWyckoffShift`` fits a relative parent conventional fractional displacement +from the representative symmetry-site coordinate: .. code-block:: python @@ -135,10 +137,11 @@ representative site coordinate: ) Here ``"x"``, ``"y"``, and ``"z"`` are parent conventional fractional axes, -not generated surface-cell axes. The shift is propagated through the stored +not generated surface-cell axes, and the parameter is a relative shift rather +than an absolute coordinate. The shift is propagated through the stored space-group operation for each generated atom and then through the surface-cell -transform. This intentionally keeps the original atom assignment and atom count -while allowing the site to move away from its exact Wyckoff constraint. +transform. This intentionally keeps the original atom assignment and atom +count while allowing the site to move away from its exact Wyckoff constraint. Plural helpers are available for fitting several variables or shifts: @@ -155,7 +158,7 @@ template unit cell: .. code-block:: python - film.addWyckoffParameter("O_4f", "u", limits=(-0.05, 0.05)) + film.addWyckoffParameter("O_4f", "u", limits=(0.25, 0.35)) poisson.addWyckoffShift("O_4f", "x", limits=(-0.02, 0.02)) ``EpitaxyInterface`` follows the same selector convention as its existing @@ -166,7 +169,7 @@ template unit cell: interface.addWyckoffParameter( "O_4f", "u", - limits=(-0.05, 0.05), + limits=(0.25, 0.35), unitcell="top", ) @@ -185,8 +188,11 @@ Linking Across Unit Cells ------------------------- ``SXRDCrystal`` provides the same two Wyckoff helper modes at the crystal -level. These helpers create one coupled crystal parameter and ordinary -relative fit parameters inside each selected unit cell. +level. ``addWyckoffParameter`` distributes one absolute value to the selected +unit cells, while ``addWyckoffShift`` distributes one relative displacement. +If selected sites begin with different values, the coupled starting value is +their mean; setting the coupled parameter assigns the same absolute Wyckoff +variable to every selected site. For a symmetry-preserving Wyckoff variable: @@ -198,7 +204,7 @@ For a symmetry-preserving Wyckoff variable: "TiO2": ("O_4f", "u"), }, name="shared_rutile_oxygen_u", - limits=(-0.05, 0.05), + limits=(0.25, 0.35), ) For a representative-site shift: @@ -228,7 +234,7 @@ value dictionary, or use a ``(component, unitcell)`` key: } }, name="shared_interface_u", - limits=(-0.05, 0.05), + limits=(0.25, 0.35), ) crystal.addWyckoffShift( diff --git a/orgui/datautils/xrayutils/CTRcalc.py b/orgui/datautils/xrayutils/CTRcalc.py index a3bf799..2edb04b 100644 --- a/orgui/datautils/xrayutils/CTRcalc.py +++ b/orgui/datautils/xrayutils/CTRcalc.py @@ -521,10 +521,14 @@ def addWyckoffParameter(self, parameter, limits=(-np.inf, np.inf), **keyargs): ``unitcell="top"``, ``unitcell="bottom"``, or a list in the selection dictionary. + The coupled value and limits are absolute. This method distributes the + request to :meth:`UnitCell.addWyckoffParameter` on each selected unit + cell and links the resulting absolute parameters by name. + :param dict parameter: Component-specific Wyckoff variable selections. :param tuple limits: - Shared delta limits in fractional units. + Shared absolute limits in fractional units. :param keyargs: Additional coupled-parameter settings such as ``name`` or ``prior``. @@ -537,13 +541,12 @@ def addWyckoffParameter(self, parameter, limits=(-np.inf, np.inf), **keyargs): parameter, kind="coordinate", value_key="variable", - coupling_getter="wyckoff_couplings", limits=limits, **keyargs, ) def addWyckoffShift(self, parameter, limits=(-np.inf, np.inf), **keyargs): - """Add a coupled representative Wyckoff-site shift parameter. + """Add coupled relative shifts along parent-cell directions. ``parameter`` maps crystal component names to Wyckoff shift selections. A selection can be ``(site_id, axis)`` or a dictionary with @@ -555,7 +558,7 @@ def addWyckoffShift(self, parameter, limits=(-np.inf, np.inf), **keyargs): :param dict parameter: Component-specific Wyckoff shift selections. :param tuple limits: - Shared parent-coordinate delta limits in fractional units. + Shared relative-shift limits in parent fractional units. :param keyargs: Additional coupled-parameter settings such as ``name`` or ``prior``. @@ -568,7 +571,6 @@ def addWyckoffShift(self, parameter, limits=(-np.inf, np.inf), **keyargs): parameter, kind="site_displacement", value_key="axis", - coupling_getter="wyckoff_site_couplings", limits=limits, **keyargs, ) @@ -578,7 +580,6 @@ def _add_wyckoff_coupled_parameter( parameter, kind, value_key, - coupling_getter, limits=(-np.inf, np.inf), **keyargs, ): @@ -590,47 +591,41 @@ def _add_wyckoff_coupled_parameter( keyargs["name"] = name keyargs["prior"] = prior - component_indices = [] + targets = [] for component_key, selection in parameter.items(): - for component_index, unitcell, site_id, value in self._wyckoff_targets( - component_key, - selection, - value_key, - ): - couplings = [ - coupling - for coupling in getattr(unitcell, coupling_getter)(site_id) - if getattr(coupling, value_key) == value - ] - if not couplings: - raise ValueError( - f"No Wyckoff {value_key} couplings found for " - f"{component_key!r}, site {site_id!r}, {value_key} " - f"{value!r}." - ) - atoms = np.asarray( - [coupling.atom_index for coupling in couplings], - dtype=np.intp, + targets.extend( + (component_key, *target) + for target in self._wyckoff_targets( + component_key, + selection, + value_key, ) - coordinates = tuple(coupling.coordinate for coupling in couplings) - factors = np.asarray( - [coupling.factor for coupling in couplings], - dtype=np.float64, + ) + + component_indices = [] + for component_key, component_index, unitcell, site_id, value in targets: + settings = dict(keyargs) + if kind == "coordinate": + unitcell.addWyckoffParameter( + site_id, + value, + limits=limits, + **settings, ) - settings = dict(keyargs) - settings["wyckoff"] = { - "site_id": site_id, - "kind": kind, - value_key: value, - "value_kind": "delta", - } - unitcell.addRelParameter( - (atoms, coordinates), - factors, - limits, + else: + unitcell.addWyckoffShift( + site_id, + value, + limits=limits, **settings, ) - component_indices.append(component_index) + component_indices.append(component_index) + + coupled_settings = dict(keyargs) + coupled_settings["wyckoff"] = { + "kind": kind, + "value_kind": "absolute" if kind == "coordinate" else "delta", + } par = Parameter( name, @@ -638,7 +633,7 @@ def _add_wyckoff_coupled_parameter( ParameterType.RELATIVE, limits, prior, - keyargs, + coupled_settings, ) self.parameters["coupled"].append(par) self._parIdNo += 1 @@ -935,7 +930,7 @@ def setParameters(self, x): x = np.asarray(x) x_r = x[ self.fit_metadata_cache["par_idx_sortarray"] - ] # reorder fitpars, this handles coupled parameters + ] # reorder fitpars, including coupled absolute Wyckoff parameters x_ucs = x_r[self.fit_metadata_cache["number_xtalpar"] :] uc_numberpars = self.fit_metadata_cache["uc_numberpars"] @@ -967,7 +962,7 @@ def setLimits(self, lim): lim = np.asarray(lim) x_r = lim[ self.fit_metadata_cache["par_idx_sortarray"] - ] # reorder fitpars, this handles coupled parameters + ] # reorder limits, including coupled absolute Wyckoff parameters x_ucs = x_r[self.fit_metadata_cache["number_xtalpar"] :] uc_numberpars = self.fit_metadata_cache["uc_numberpars"] diff --git a/orgui/datautils/xrayutils/CTRfilm.py b/orgui/datautils/xrayutils/CTRfilm.py index 1cd8289..b0fed5e 100644 --- a/orgui/datautils/xrayutils/CTRfilm.py +++ b/orgui/datautils/xrayutils/CTRfilm.py @@ -154,16 +154,16 @@ def addWyckoffParameter( absolute_limits=None, **kwargs, ): - """Add a Wyckoff variable parameter on contained unit cells. + """Add an absolute Wyckoff variable parameter on contained unit cells. :param str site_id: Wyckoff site identifier. :param str variable: Wyckoff variable name, for example ``"u"``. :param tuple limits: - Delta fit limits in fractional units. + Absolute variable limits in parent fractional units. :param tuple absolute_limits: - Optional absolute variable limits. + Deprecated alias for ``limits`` retained for compatibility. :param kwargs: For ``EpitaxyInterface``, provide ``unitcell="top"``, ``unitcell="bottom"``, or a list of these names. @@ -187,16 +187,17 @@ def addWyckoffParameters( absolute_limits=None, **kwargs, ): - """Add several Wyckoff variable parameters on contained unit cells. + """Add absolute Wyckoff variable parameters on contained unit cells. :param str site_id: Wyckoff site identifier. :param variables: Iterable of Wyckoff variable names. If ``None``, fit all variables. :param tuple limits: - Delta fit limits in fractional units. + Absolute variable limits in parent fractional units, or a mapping + from variable names to absolute limits. :param absolute_limits: - Optional absolute variable limits. + Deprecated alias for ``limits`` retained for compatibility. :param kwargs: For ``EpitaxyInterface``, provide ``unitcell="top"``, ``unitcell="bottom"``, or a list of these names. @@ -221,7 +222,10 @@ def addWyckoffShift( absolute_limits=None, **kwargs, ): - """Add a representative Wyckoff-site shift on contained unit cells. + """Add a relative symmetry-site shift along a parent direction. + + The parameter is a displacement from the representative symmetry-site + coordinate, not an absolute parent coordinate. :param str site_id: Wyckoff site identifier. @@ -231,7 +235,8 @@ def addWyckoffShift( :param tuple limits: Delta fit limits in parent fractional units. :param tuple absolute_limits: - Optional absolute parent-coordinate limits. + Optional absolute parent-coordinate bounds converted to relative + shift bounds. The fitted value remains a relative displacement. :param kwargs: For ``EpitaxyInterface``, provide ``unitcell="top"``, ``unitcell="bottom"``, or a list of these names. @@ -255,7 +260,10 @@ def addWyckoffShifts( absolute_limits=None, **kwargs, ): - """Add representative Wyckoff-site shifts on contained unit cells. + """Add relative symmetry-site shifts along parent directions. + + Every parameter is a displacement from the representative + symmetry-site coordinate, not an absolute parent coordinate. :param str site_id: Wyckoff site identifier. @@ -264,7 +272,8 @@ def addWyckoffShifts( :param tuple limits: Delta fit limits in parent fractional units. :param absolute_limits: - Optional absolute parent-coordinate limits. + Optional absolute parent-coordinate bounds converted to relative + shift bounds. The fitted values remain relative displacements. :param kwargs: For ``EpitaxyInterface``, provide ``unitcell="top"``, ``unitcell="bottom"``, or a list of these names. diff --git a/orgui/datautils/xrayutils/CTRuc.py b/orgui/datautils/xrayutils/CTRuc.py index c571c82..d1ffc98 100644 --- a/orgui/datautils/xrayutils/CTRuc.py +++ b/orgui/datautils/xrayutils/CTRuc.py @@ -871,9 +871,24 @@ def fromStr(string): class UnitCell(Lattice): - parameterOrder = ( - "Name x/frac y/frac z/frac iDW oDW occup layerIdx" + _atom_column_names = ( + "Name", + "x/frac", + "y/frac", + "z/frac", + "iDW", + "oDW", + "occup", + "layerIdx", ) + _atom_column_widths = (6, 12, 12, 12, 10, 10, 10, 10) + _atom_error_column_widths = (6, 24, 24, 24, 22, 22, 22, 14) + parameterOrder = "".join( + f"{name:<{width}}" if index == 0 else f"{name:>{width}}" + for index, (name, width) in enumerate( + zip(_atom_column_names, _atom_column_widths) + ) + ).rstrip() parameterLookup = {"x": 1, "y": 2, "z": 3, "iDW": 4, "oDW": 5, "occ": 6, "layer": 7} @@ -998,7 +1013,8 @@ def updateFromParameters(self): no_errors = True for par in self.parameters["relative"]: if par.value is not None: - self.basis[par.indices] += par.factors * par.value + value = self._relative_internal_value(par, par.value) + self.basis[par.indices] += par.factors * value else: raise ValueError( f"Can not set basis values from parameters. Value of Parameter {par.name} is None." # noqa: E501 @@ -1015,6 +1031,14 @@ def updateFromParameters(self): if not no_errors: self._errors_parvalues = np.copy(self.errors) + @staticmethod + def _relative_internal_value(parameter, value): + """Convert an exposed parameter value to its internal relative delta.""" + wyckoff = parameter.settings.get("wyckoff", {}) + if wyckoff.get("value_kind") == "absolute": + return value - wyckoff["reference_value"] + return value + def clearParameters(self): for p in self.parameters: self.parameters[p] = [] @@ -2422,8 +2446,6 @@ def _refresh_after_baseline_change(self): self.errors = None self._basis_parvalues = None self._errors_parvalues = None - if any(self.parameters.values()): - self.updateFromParameters() def _update_wyckoff_metadata_from_atom_values( self, @@ -2627,25 +2649,22 @@ def addWyckoffParameter( absolute_limits=None, **keyargs, ): - """Add a symmetry-preserving fit parameter for a Wyckoff variable. + """Add an absolute symmetry-preserving Wyckoff variable parameter. - The stored fit value is the change in the Wyckoff variable from the - generated coordinates. For example, fitting rutile oxygen ``u`` adds - ``factor * delta_u`` to every generated coordinate that depends on - ``u``. Multi-variable Wyckoff sites are fitted by adding one parameter - per independent coordinate variable. + The exposed fit value is the absolute Wyckoff variable. Internally, + its difference from the site's reference value is propagated through + every affine coupling, preserving symmetry-equivalent coordinates. :param str site_id: Site identifier returned by :meth:`wyckoff_sites`. :param str variable: Wyckoff variable name, for example ``"u"``. :param tuple limits: - Fit limits for the variable change in fractional units. + Absolute variable limits in parent fractional units. :param tuple absolute_limits: - Optional absolute variable limits. These are converted to change - limits around the metadata variable value. + Deprecated alias for ``limits`` retained for compatibility. :returns: - Created relative fit parameter. + Parameter exposing the absolute Wyckoff variable value. :rtype: CTRutil.Parameter :raises ValueError: @@ -2659,17 +2678,17 @@ def addWyckoffParameter( if variable not in site["variables"]: raise ValueError(f"Wyckoff site {site_id} has no variable {variable}.") - limits = self._delta_limits( - limits, - absolute_limits, - site["variables"][variable], - ) + if absolute_limits is not None: + if limits != (-np.inf, np.inf): + raise ValueError("Use either limits or absolute_limits, not both.") + limits = absolute_limits + reference_value = site["variables"][variable] couplings = [ coupling for coupling in self.wyckoff_couplings(site_id) if coupling.variable == variable ] - return self._add_wyckoff_relative_parameter( + parameter = self._add_wyckoff_relative_parameter( site_id, "variable", variable, @@ -2683,6 +2702,13 @@ def addWyckoffParameter( ), keyargs, ) + parameter.settings["wyckoff"].update( + { + "value_kind": "absolute", + "reference_value": reference_value, + } + ) + return parameter def addWyckoffParameters( self, @@ -2692,7 +2718,7 @@ def addWyckoffParameters( absolute_limits=None, **keyargs, ): - """Add symmetry-preserving fit parameters for Wyckoff variables. + """Add absolute symmetry-preserving Wyckoff variable parameters. :param str site_id: Site identifier returned by :meth:`wyckoff_sites`. @@ -2702,12 +2728,12 @@ def addWyckoffParameters( :type variables: iterable or None :param limits: - Either one ``(lower, upper)`` tuple applied to every variable or a - dictionary mapping variable names to delta limits. + One absolute ``(lower, upper)`` tuple for every variable or a + dictionary mapping variable names to absolute limits. :param absolute_limits: - Optional dictionary mapping variable names to absolute limits. + Deprecated alias for absolute ``limits`` retained for compatibility. :returns: - Created relative fit parameters in variable order. + Created absolute variable parameters in variable order. :rtype: list :raises ValueError: @@ -2750,12 +2776,12 @@ def addWyckoffShift( absolute_limits=None, **keyargs, ): - """Add a symmetry-lowering shift for representative site motion. + """Add a relative shift from a symmetry site along a parent direction. - ``axis`` is a parent conventional fractional coordinate of the - representative atom. The stored fit value is a delta from the - representative coordinate; generated atoms move through the stored - space-group operation and surface-cell transform factors. + ``axis`` is a parent conventional-cell direction. The exposed value is + a relative displacement from the representative symmetry-site + coordinate, not an absolute parent coordinate. Generated atoms move + through the stored space-group operation and surface-cell transform. :param str site_id: Site identifier returned by :meth:`wyckoff_sites`. @@ -2765,8 +2791,8 @@ def addWyckoffShift( :param tuple limits: Fit limits for the coordinate change in parent fractional units. :param tuple absolute_limits: - Optional absolute parent-coordinate limits, converted to changes - around the stored representative coordinate. + Optional absolute parent-coordinate bounds converted to relative + shift bounds. The fitted value remains a relative displacement. :returns: Created relative fit parameter. :rtype: @@ -2817,7 +2843,11 @@ def addWyckoffShifts( absolute_limits=None, **keyargs, ): - """Add representative site-displacement shifts for several axes. + """Add relative symmetry-site shifts along parent-cell directions. + + Every exposed value is a displacement from the representative + symmetry-site coordinate along the selected parent conventional-cell + direction; it is not an absolute coordinate. :param str site_id: Site identifier returned by :meth:`wyckoff_sites`. @@ -2829,7 +2859,8 @@ def addWyckoffShifts( Either one ``(lower, upper)`` tuple applied to every axis or a dictionary mapping axis names to delta limits. :param absolute_limits: - Optional dictionary mapping axis names to absolute limits. + Optional absolute parent-coordinate bounds converted to relative + shift bounds. The fitted values remain relative displacements. :returns: Created relative fit parameters in axis order. :rtype: @@ -2938,6 +2969,9 @@ def fitparToStr(self, par): val = np.mean( (self.basis[par.indices] - basis_0[par.indices]) / par.factors ) + wyckoff = par.settings.get("wyckoff", {}) + if wyckoff.get("value_kind") == "absolute": + val += wyckoff["reference_value"] # if isinstance(par.indices[1], (np.integer,int)): # parameternames = (UnitCell.parameterLookup_inv[par.indices[1]] + '_r',) # atoms = (f"{par.indices[0]}_{self.names[par.indices[0]]}",) @@ -2984,12 +3018,14 @@ def getStartParamAndLimits(self, force_recalculate=False): for par in relpar: if recalculate or par.value is None: - x0.append( - np.mean( - (self.basis[par.indices] - self.basis_0[par.indices]) - / par.factors - ) + value = np.mean( + (self.basis[par.indices] - self.basis_0[par.indices]) + / par.factors ) + wyckoff = par.settings.get("wyckoff", {}) + if wyckoff.get("value_kind") == "absolute": + value += wyckoff["reference_value"] + x0.append(value) else: x0.append(par.value) lower.append(par.limits[0]) @@ -3005,7 +3041,8 @@ def setFitParameters(self, x): self.basis[par.indices] = val par.value = val for val, par in zip(x_r, self.parameters["relative"]): - self.basis[par.indices] += par.factors * val + internal_value = self._relative_internal_value(par, val) + self.basis[par.indices] += par.factors * internal_value par.value = val self._basis_parvalues = np.copy(self.basis) @@ -3704,19 +3741,42 @@ def atomToStr(self, no, showErrors=True): name = self.names[no] if (self.errors is not None) and showErrors: err = self.errors[no][1:] - l = [] # noqa: E741 - for t in zip(param, err): - [l.append(ti) for ti in t] - return ( - "{} ({:.5f} +- {:.5f}) ({:.5f} +- {:.5f}) ({:.5f} +- {:.5f}) ({:.4f} +- {:.4f}) ({:.4f} +- {:.4f}) ({:.4f} +- {:.4f}) ({:.0f} +- {:.0f})".format( # noqa: E501 - name, *l - ) # noqa: E501 + values = [ + f"({param[index]:.5f} +- {err[index]:.5f})" + for index in range(3) + ] + values.extend( + f"({param[index]:.4f} +- {err[index]:.4f})" + for index in range(3, 6) ) - else: - return "{} {:.5f} {:.5f} {:.5f} {:.4f} {:.4f} {:.4f} {:.0f}".format( # noqa: E501 - name, - *param, + values.append(f"({param[6]:.0f} +- {err[6]:.0f})") + return f"{name:<{self._atom_error_column_widths[0]}}" + "".join( + f"{value:>{width}}" + for value, width in zip( + values, + self._atom_error_column_widths[1:], + ) + ) + + formats = (".5f", ".5f", ".5f", ".4f", ".4f", ".4f", ".0f") + values = [format(value, spec) for value, spec in zip(param, formats)] + return f"{name:<{self._atom_column_widths[0]}}" + "".join( + f"{value:>{width}}" + for value, width in zip(values, self._atom_column_widths[1:]) + ) + + def _parameter_header(self, showErrors=True): + widths = ( + self._atom_error_column_widths + if self.errors is not None and showErrors + else self._atom_column_widths + ) + return "".join( + f"{name:<{width}}" if index == 0 else f"{name:>{width}}" + for index, (name, width) in enumerate( + zip(self._atom_column_names, widths) ) + ).rstrip() def domainsToStr(self): s = "" @@ -3772,7 +3832,7 @@ def parameterStrRod(self): def __str__(self): st = repr(self) + "\n" - st += "id " + UnitCell.parameterOrder + "\n" + st += "id " + self._parameter_header() + "\n" return st + self.parameterStr() def writeSURfile(self, filename): @@ -3809,7 +3869,7 @@ def toStr(self, showErrors=True): + self.domainsToStr() + self.latticeRODStr() + "\n" - + UnitCell.parameterOrder + + self._parameter_header(showErrors) + "\n" + self.parameterStr(showErrors=showErrors) ) diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index e6fa183..a075ca1 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -30,6 +30,7 @@ __maintainer__ = "Timo Fuchs" __email__ = "fuchs@physik.uni-kiel.de" +import dataclasses import unittest from unittest import mock import os @@ -822,8 +823,9 @@ def test_unitcell_supercell_preserves_shared_wyckoff_site(self): [0.5, 0.5], ) - supercell.addWyckoffParameter("C_1", "u", limits=(-0.2, 0.2)) - supercell.setFitParameters([0.1]) + supercell.addWyckoffParameter("C_1", "u", limits=(0.1, 0.4)) + np.testing.assert_allclose(supercell.getInitialParameters(), [0.25]) + supercell.setFitParameters([0.35]) np.testing.assert_allclose(supercell.basis[:, 1], [0.175, 0.675]) @@ -845,9 +847,9 @@ def test_unitcell_supercell_independent_wyckoff_sites_fit_separately(self): supercell.addWyckoffParameter( "C_1_copy0", "u", - limits=(-0.2, 0.2), + limits=(0.1, 0.4), ) - supercell.setFitParameters([0.1]) + supercell.setFitParameters([0.35]) np.testing.assert_allclose(supercell.basis[:, 1], [0.175, 0.625]) @@ -860,12 +862,29 @@ def test_unitcell_supercell_independent_wyckoff_sites_fit_separately(self): def test_film_forwards_wyckoff_parameters_to_template_unitcell(self): film = CTRfilm.Film(self.make_single_wyckoff_cell()) - film.addWyckoffParameter("C_1", "u", limits=(-0.2, 0.2)) - film.setFitParameters([0.1]) + film.addWyckoffParameter("C_1", "u", limits=(0.1, 0.4)) + np.testing.assert_allclose(film.getInitialParameters(), [0.25]) + film.setFitParameters([0.35]) self.assertEqual(film.unitcell.wyckoff_sites()[0]["status"], "symmetry_preserving") np.testing.assert_allclose(film.unitcell.basis[:, 1], [0.35]) + def test_film_forwards_absolute_plural_wyckoff_parameters(self): + film = CTRfilm.Film(self.make_single_wyckoff_cell()) + + parameters = film.addWyckoffParameters( + "C_1", + limits={"u": (0.1, 0.4)}, + ) + + self.assertEqual(len(parameters), 1) + np.testing.assert_allclose( + film.getStartParamAndLimits(), + ([0.25], [0.1], [0.4]), + ) + film.setFitParameters([0.3]) + np.testing.assert_allclose(film.unitcell.basis[:, 1], [0.3]) + def test_poisson_surface_forwards_wyckoff_shifts_to_template_unitcell(self): surface = CTRfilm.PoissonSurface(self.make_single_wyckoff_cell()) @@ -906,10 +925,11 @@ def test_epitaxy_interface_accepts_unitcell_list_for_wyckoff_parameters(self): parameters = interface.addWyckoffParameter( "C_1", "u", - limits=(-0.2, 0.2), + limits=(0.1, 0.4), unitcell=["top", "bottom"], ) - interface.setFitParameters([0.1, 0.1]) + np.testing.assert_allclose(interface.getInitialParameters(), [0.25, 0.25]) + interface.setFitParameters([0.35, 0.35]) self.assertEqual(len(parameters), 2) self.assertEqual(interface.uc_top.wyckoff_sites()[0]["status"], "symmetry_preserving") @@ -932,9 +952,22 @@ def test_sxrdcrystal_links_wyckoff_parameters_across_unitcells(self): "film2": ("C_1", "u"), }, name="shared_u", - limits=(-0.2, 0.2), + limits=(0.1, 0.4), ) - crystal.setParameters([0.1]) + np.testing.assert_allclose( + crystal.getStartParamAndLimits(), + ([0.25], [0.1], [0.4]), + ) + np.testing.assert_allclose( + film1.getStartParamAndLimits()[1:], + ([0.1], [0.4]), + ) + crystal.setLimits([(0.2, 0.3)]) + np.testing.assert_allclose( + film1.getStartParamAndLimits()[1:], + ([0.2], [0.3]), + ) + crystal.setParameters([0.35]) self.assertEqual(parameter.name, "shared_u") self.assertEqual(crystal.fitparnames, ["shared_u"]) @@ -988,9 +1021,10 @@ def test_sxrdcrystal_links_wyckoff_parameters_inside_interface(self): } }, name="shared_interface_u", - limits=(-0.2, 0.2), + limits=(0.1, 0.4), ) - crystal.setParameters([0.1]) + np.testing.assert_allclose(crystal.getInitialParameters(), [0.25]) + crystal.setParameters([0.35]) self.assertEqual(crystal.fitparnames, ["shared_interface_u"]) self.assertEqual(interface.uc_top.wyckoff_sites()[0]["status"], "symmetry_preserving") @@ -998,6 +1032,34 @@ def test_sxrdcrystal_links_wyckoff_parameters_inside_interface(self): np.testing.assert_allclose(interface.uc_top.basis[:, 1], [0.35]) np.testing.assert_allclose(interface.uc_bottom.basis[:, 1], [0.35]) + def test_sxrdcrystal_distributes_absolute_wyckoff_parameter(self): + bulk = self.make_single_wyckoff_cell() + film1 = self.make_single_wyckoff_cell() + film2 = self.make_single_wyckoff_cell() + bulk.name = "bulk_template" + film1.name = "film1" + film2.name = "film2" + film2.basis[:, 1] = 0.27 + film2.basis_0[:, 1] = 0.27 + film2.symmetry_metadata.sites = ( + dataclasses.replace( + film2.symmetry_metadata.sites[0], + variables={"u": 0.27}, + ), + ) + crystal = CTRcalc.SXRDCrystal(bulk, film1, film2) + + crystal.addWyckoffParameter( + {"film1": ("C_1", "u"), "film2": ("C_1", "u")}, + name="mean_u", + limits=(0.2, 0.35), + ) + + np.testing.assert_allclose(crystal.getInitialParameters(), [0.26]) + crystal.setParameters([0.28]) + np.testing.assert_allclose(film1.basis[:, 1], [0.28]) + np.testing.assert_allclose(film2.basis[:, 1], [0.28]) + def test_sxrdcrystal_links_wyckoff_shift_to_interface_unitcell_key(self): bulk = self.make_single_wyckoff_cell() top = self.make_single_wyckoff_cell() @@ -1420,6 +1482,33 @@ def test_interface_uses_lower_unitcell_area(self): class TestCTRTextFilesAndFitParameters(unittest.TestCase): + def test_unitcell_atom_table_uses_fixed_width_columns(self): + cell = CTRcalc.UnitCell([1.0, 1.0, 1.0], [90.0, 90.0, 90.0]) + cell.addAtom("O", [0.5, 0.0, 0.80569], 0.4119, 0.4119, 1.0, layer=2) + + lines = cell.toStr(showErrors=False).splitlines() + + self.assertIn( + "Name x/frac y/frac z/frac iDW oDW" + " occup layerIdx", + lines, + ) + self.assertIn( + "00 O 0.50000 0.00000 0.80569 0.4119" + " 0.4119 1.0000 2", + lines, + ) + + cell.errors = np.full_like(cell.basis, np.nan) + cell.errors[0, 3] = 0.00007 + error_lines = cell.toStr(showErrors=True).splitlines() + header = next(line for line in error_lines if line.startswith("Name")) + atom = next(line for line in error_lines if line.startswith("00")) + for column in ("x/frac", "y/frac", "z/frac", "iDW", "oDW", "occup"): + self.assertGreater(header.index(column), 0) + self.assertIn("(0.80569 +- 0.00007)", atom) + self.assertEqual(len(header), len(atom) - 4) + @staticmethod def make_layered_cell(name): cell = CTRcalc.UnitCell( diff --git a/orgui/datautils/xrayutils/test/test_CTRsymmetry.py b/orgui/datautils/xrayutils/test/test_CTRsymmetry.py index 7b1cef7..c41347a 100644 --- a/orgui/datautils/xrayutils/test/test_CTRsymmetry.py +++ b/orgui/datautils/xrayutils/test/test_CTRsymmetry.py @@ -112,6 +112,21 @@ def test_set_wyckoff_atom_parameter_updates_all_site_atoms(self): [0.1, 0.9], ) + def test_set_wyckoff_atom_parameter_allows_uninitialized_fit_parameters(self): + unitcell = self.make_metadata_unitcell() + unitcell.addFitParameter( + ([0], ["z"]), + name="manual_top_oxygen_z", + ) + original_z = unitcell.basis[:, 3].copy() + + unitcell.set_wyckoff_atom_parameter("O_2a", "iDW", 0.35) + + np.testing.assert_allclose(unitcell.basis[:, 4], 0.35) + np.testing.assert_allclose(unitcell.basis_0[:, 4], 0.35) + np.testing.assert_allclose(unitcell.basis[:, 3], original_z) + self.assertIsNone(unitcell.parameters["absolute"][0].value) + def test_set_wyckoff_site_parameter_propagates_parent_coordinate(self): unitcell = self.make_metadata_unitcell() @@ -375,15 +390,15 @@ def test_wyckoff_coordinate_parameter_preserves_rutile_u_symmetry(self): ) self.assertEqual(parameter.settings["wyckoff"]["kind"], "coordinate") - self.assertEqual(parameter.settings["wyckoff"]["value_kind"], "delta") - np.testing.assert_allclose(unitcell.getInitialParameters(), [0.0]) + self.assertEqual(parameter.settings["wyckoff"]["value_kind"], "absolute") + np.testing.assert_allclose(unitcell.getInitialParameters(), [0.30569]) np.testing.assert_allclose( unitcell.getStartParamAndLimits()[1:], - ([-0.10569], [0.09431]), + ([0.2], [0.4]), ) self.assertEqual(unitcell.wyckoff_sites()[1]["status"], "symmetry_preserving") - unitcell.setFitParameters([0.01]) + unitcell.setFitParameters([0.31569]) for coupling in couplings: column = unitcell.parameterLookup[coupling.coordinate] @@ -454,9 +469,13 @@ def test_wyckoff_coordinate_parameters_fit_all_site_variables(self): [par.settings["wyckoff"]["variable"] for par in parameters], ["u", "v", "w"], ) + np.testing.assert_allclose( + unitcell.getInitialParameters(), + [0.12, 0.23, 0.34], + ) self.assertEqual(unitcell.wyckoff_sites()[0]["status"], "symmetry_preserving") - unitcell.setFitParameters([0.01, -0.02, 0.03]) + unitcell.setFitParameters([0.13, 0.21, 0.37]) deltas = {"u": 0.01, "v": -0.02, "w": 0.03} for coordinate_name in ("x", "y", "z"): From 5b151bee289eed75f18e2449322a1ee4f3dd5d5d Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Sat, 18 Jul 2026 01:25:05 -0400 Subject: [PATCH 07/37] feat: add Poisson surface unit cell handling --- .../_static/poisson_surface_occupancies.svg | 69 ++ doc/source/ctr_structure_factors.rst | 142 +++- examples/CTR/RuO2_TiO2_Poisson_etching.xpr | 294 ++++---- examples/CTR/RuO2_TiO2_Poisson_etching.xtal | 294 ++++---- orgui/datautils/xrayutils/CTRcalc.py | 8 +- orgui/datautils/xrayutils/CTRdistributions.py | 169 ++++- orgui/datautils/xrayutils/CTRfilm.py | 677 ++++++++++++------ orgui/datautils/xrayutils/CTRuc.py | 53 ++ orgui/datautils/xrayutils/CTRutil.py | 90 +++ .../datautils/xrayutils/test/test_CTRcalc.py | 464 +++++++++--- 10 files changed, 1679 insertions(+), 581 deletions(-) create mode 100644 doc/source/_static/poisson_surface_occupancies.svg diff --git a/doc/source/_static/poisson_surface_occupancies.svg b/doc/source/_static/poisson_surface_occupancies.svg new file mode 100644 index 0000000..f75b6cb --- /dev/null +++ b/doc/source/_static/poisson_surface_occupancies.svg @@ -0,0 +1,69 @@ + + Poisson surface occupancy decomposition + Stacked bars show covered Film occupancy and exposed surface occupancy for structural-layer offsets minus one through five. + + + + + + + + + + + 0 + 0.25 + 0.50 + 0.75 + 1.00 + occupancy + z / structural layers + + + + + + + + + + + + + + + + + + + + + + −1 + 0 + 1 + 2 + 3 + 4 + 5 + + + covered Film, cᵢ + + exposed surface, sᵢ + diff --git a/doc/source/ctr_structure_factors.rst b/doc/source/ctr_structure_factors.rst index cd6e1fe..70c6187 100644 --- a/doc/source/ctr_structure_factors.rst +++ b/doc/source/ctr_structure_factors.rst @@ -30,6 +30,136 @@ area or volume normalization is applied. and return electrons for one lateral unit cell of their source ``UnitCell``. +Surface structure on a rough Film +--------------------------------- + +A ``PoissonSurface`` is stacked immediately above a ``Film`` and replaces the +Film structure only on the fraction that is truly exposed. Let +:math:`\theta_i` be the cumulative material occupancy of structural layer +:math:`i`, ordered from bottom to top. Its exposed surface occupancy is + +.. math:: + + s_i = \theta_i - \theta_{i+1}, + +where the occupancy above the highest represented layer is set to zero. The +part of layer :math:`i` that is covered by another layer remains Film material +with occupancy + +.. math:: + + c_i = \theta_i - s_i. + +The sharp Film already supplies occupancy :math:`\chi_{i<0}` below its nominal +boundary. Before applying a reconstructed surface structure, +``PoissonSurface`` adds the rough-Film correction + +.. math:: + + \Delta F_{\mathrm{rough}} + = \sum_i \left(\theta_i-\chi_{i<0}\right)F_{\mathrm{Film},i}. + +The exposed fraction is subsequently replaced by a termination-specific +surface slab as described below. Film and surface structures can consequently +occur at the same terrace height on complementary lateral fractions. + +.. figure:: _static/poisson_surface_occupancies.svg + :alt: Stacked bars of covered Film and exposed surface occupancy versus structural-layer offset. + :align: center + :width: 92% + + Occupancy decomposition for ``PoissonProfile(mean_change=2, alpha=0.5)``. + Each bar has total height :math:`\theta_i`; its orange segment is the true + surface fraction :math:`s_i`, and its blue segment is covered Film + :math:`c_i`. + +Termination-specific relaxed surface slabs +------------------------------------------- + +A reconstructed surface generally does not repeat the bulk layer cycle. The +``layer`` column therefore has a narrower meaning for a surface slab: it +selects the Film termination to which the *complete slab* belongs. Internal +planes of that slab are distinguished by their z coordinates, not by cycling +layer identifiers. ``UnitCell.as_surface_termination`` makes this explicit by +assigning one termination identifier to every atom and setting +``layer_behavior="select"``. Selection masks an inactive cell during the +calculation; it does not overwrite atomic occupancy fit parameters. + +A ``PoissonSurface`` owns exactly one complete unit cell for every member of +the underlying Film's primitive stacking cycle. For a two-layer RuO2 Film, +this means two termination cells regardless of how many bulk cells deep each +surface slab is. The surface-normal length of every termination cell may be +an integer multiple of the Film c axis, while its lateral lattice constants and +lattice angles must match the Film. + +Surface-supercell generation example +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``CTRutil.generate_surface_termination_cells`` automates the affine cycling, +top-plane selection, naming, and whole-slab relabeling. For example, a +two-cell-deep RuO2 slab and both of its surface terminations can be built as + +.. code-block:: python + + from orgui.datautils.xrayutils.CTRdistributions import PoissonProfile + from orgui.datautils.xrayutils.CTRfilm import PoissonSurface + from orgui.datautils.xrayutils.CTRutil import ( + generate_surface_termination_cells, + ) + + slab = ruo2_surface.supercell((1, 1, 2), symmetry="independent") + terminations = generate_surface_termination_cells( + slab, + ruo2_film, + name_template="RuO2_termination_{layer:g}", + ) + + surface = PoissonSurface( + terminations, + profile=PoissonProfile(mean_change=-0.55, alpha=0.5), + ) + +Here ``ruo2_film`` may be the underlying ``Film``, its ``UnitCell``, its +``LayerCycle``, or simply the ordered tuple ``(1, 2)``. The number of internal +layers in ``slab`` must be an integer multiple of this primitive Film-cycle +length. ``symmetry="independent"`` gives repeated copies independent Wyckoff +sites; use ``symmetry="preserve"`` when all repeats should retain shared +symmetry parameters. + +The helper chooses which original plane is at the top of each generated slab +and then assigns the complete slab to that Film-cycle state. Atoms at the +selected top plane can consequently be relaxed independently of their former +internal layer numbers. The returned termination cells are separate objects +and can carry independent coordinate, displacement-factor, occupancy, and +Wyckoff fit parameters. The same mapping contract can be consumed by other +surface-roughness models; it is not specific to ``PoissonSurface``. + +At exposed terrace :math:`i`, let :math:`t_i` be the corresponding Film-cycle +termination. CTRcalc generates a bulk-reference slab with the same depth and +alignment as the selected surface slab and adds + +.. math:: + + \Delta F_{\mathrm{termination}} + = \sum_i s_i + \left( + F_{\mathrm{surface\ slab},t_i} + - F_{\mathrm{Film\ slab},t_i} + \right). + +Thus a multi-layer relaxed slab replaces, rather than duplicates, the same +depth of Film material. If every termination slab is identical to its Film +reference, this term cancels numerically in ``F_uc``, ``zDensity_G``, and the +optical profile. A zero-width distribution selects and replaces only the +single sharp termination. + +The immediately underlying ``Film`` and its current stacking phase are inferred +when ``SXRDCrystal`` applies stacking; no Film reference is serialized. +Termination banks are stored in ``.xtal`` and ``.xpr`` files as +``TerminationUnitCell `` sections. A legacy single ``UnitCell`` +surface remains readable and is expanded automatically into one termination +variant per Film-cycle state. + An ``EpitaxyInterface`` can join materials with different lateral areas. Its canonical lateral cell is the lower unit cell. Internally it combines the upper and lower amplitudes as @@ -79,12 +209,20 @@ included. Calculated intensity is proportional to API reference ------------- +.. autoclass:: orgui.datautils.xrayutils.CTRdistributions.SurfaceProfile + :members: support, occupancy, correction, surface_occupancy + :member-order: bysource + +.. autoclass:: orgui.datautils.xrayutils.CTRdistributions.PoissonProfile + :members: + :member-order: bysource + .. autoclass:: orgui.datautils.xrayutils.CTRcalc.SXRDCrystal :members: F, F_surf, setGlobalReferenceUnitCell :member-order: bysource .. autoclass:: orgui.datautils.xrayutils.CTRuc.UnitCell - :members: F_uc, F_bulk, setReferenceUnitCell + :members: F_uc, F_bulk, setReferenceUnitCell, supercell, affine_layer_transform, as_surface_termination :member-order: bysource .. autoclass:: orgui.datautils.xrayutils.CTRfilm.Film @@ -98,3 +236,5 @@ API reference .. autoclass:: orgui.datautils.xrayutils.CTRfilm.PoissonSurface :members: F_uc, uc_area, setReferenceUnitCell :member-order: bysource + +.. autofunction:: orgui.datautils.xrayutils.CTRutil.generate_surface_termination_cells diff --git a/examples/CTR/RuO2_TiO2_Poisson_etching.xpr b/examples/CTR/RuO2_TiO2_Poisson_etching.xpr index 6cd3373..684cac7 100644 --- a/examples/CTR/RuO2_TiO2_Poisson_etching.xpr +++ b/examples/CTR/RuO2_TiO2_Poisson_etching.xpr @@ -1,124 +1,170 @@ -E = 20.00000 keV -# PoissonSurface RuO2surface -0003 occupancy = 1.00000 - -W/layers offset/layers --6.00000 0.00000 -support_cursor: nominal - -UnitCell RuO2 -return -Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 -6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 -Name x/frac y/frac z/frac iDW oDW occup layerIdx -00 O (0.50000 +- nan) (0.00000 +- nan) (0.80569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -01 O (0.00000 +- nan) (0.00000 +- nan) (0.69431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -02 Ru (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -03 Ru (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -04 O (0.30569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -05 O (0.69431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -06 O (0.00000 +- nan) (0.00000 +- nan) (0.30569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -07 O (0.50000 +- nan) (0.00000 +- nan) (0.19431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -08 Ru (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -09 Ru (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -10 O (0.19431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -11 O (0.80569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -layerpos: 1.0 = 0.0, 2.0 = 0.5 -layer_behaviour: ignore - - -# Film RuO2 -0002 occupancy = 1.00000 - -Width/layers -(17.00000 +- nan) - -UnitCell RuO2 -return -Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 -6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 -Name x/frac y/frac z/frac iDW oDW occup layerIdx -00 O (0.50000 +- nan) (0.00000 +- nan) (0.80569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -01 O (0.00000 +- nan) (0.00000 +- nan) (0.69431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -02 Ru (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -03 Ru (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -04 O (0.30569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -05 O (0.69431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -06 O (0.00000 +- nan) (0.00000 +- nan) (0.30569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -07 O (0.50000 +- nan) (0.00000 +- nan) (0.19431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -08 Ru (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -09 Ru (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -10 O (0.19431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -11 O (0.80569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -layerpos: 1.0 = 0.0, 2.0 = 0.5 -layer_behaviour: ignore - - -# EpitaxyInterface TiO2toRuO2 -0001 occupancy = 1.00000 -type skellam -Width/cells Skew/cells -0.35000 0.00000 -support_cursor: nominal - -TopUnitCell RuO2interf -return -Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 -6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 -Name x/frac y/frac z/frac iDW oDW occup layerIdx -00 O (0.50000 +- nan) (0.00000 +- nan) (0.80569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -01 O (0.00000 +- nan) (0.00000 +- nan) (0.69431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -02 Ru (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -03 Ru (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -04 O (0.30569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -05 O (0.69431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) -06 O (0.00000 +- nan) (0.00000 +- nan) (0.30569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -07 O (0.50000 +- nan) (0.00000 +- nan) (0.19431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -08 Ru (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -09 Ru (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -10 O (0.19431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -11 O (0.80569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) -layerpos: 1.0 = 0.0, 2.0 = 0.5 -layer_behaviour: ignore - - -BottomUnitCell TiO2interf -return -Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 -6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 -Name x/frac y/frac z/frac iDW oDW occup layerIdx -00 O (0.50000 +- nan) (0.00000 +- nan) (0.80458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -01 O (0.00000 +- nan) (0.00000 +- nan) (0.69542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -02 Ti (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -03 Ti (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -04 O (0.30458 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -05 O (0.69542 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -06 O (0.00000 +- nan) (0.00000 +- nan) (0.30458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -07 O (0.50000 +- nan) (0.00000 +- nan) (0.19542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -08 Ti (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -09 Ti (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -10 O (0.19542 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -11 O (0.80458 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -layerpos: 1.0 = 0.0, 2.0 = 0.5 -layer_behaviour: ignore - - -# UnitCell bulk -return -Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 -6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 -Name x/frac y/frac z/frac iDW oDW occup layerIdx -00 O (0.50000 +- nan) (0.00000 +- nan) (-0.19542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -01 O (0.00000 +- nan) (0.00000 +- nan) (-0.30458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -02 Ti (0.50000 +- nan) (0.00000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -03 Ti (0.00000 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -04 O (0.30458 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -05 O (0.69542 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) -06 O (0.00000 +- nan) (0.00000 +- nan) (-0.69542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -07 O (0.50000 +- nan) (0.00000 +- nan) (-0.80458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -08 Ti (0.00000 +- nan) (0.00000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -09 Ti (0.50000 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -10 O (0.19542 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -11 O (0.80458 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) -layerpos: 1.0 = -1.0, 2.0 = -0.5 -layer_behaviour: ignore +E = 20.00000 keV +# PoissonSurface RuO2surface +0003 occupancy = 1.00000 + +W/layers alpha offset/layers +-6.00000 1.00000 0.00000 + +TerminationUnitCell 1 RuO2_termination_1 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 13.0912 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (0.15285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (0.09715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +02 Ru (0.50000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +03 Ru (0.00000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +04 O (0.30569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +05 O (0.69431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (0.40285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (0.34715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ru (0.00000 +- nan) (0.00000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ru (0.50000 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19431 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80569 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +12 O (0.50000 +- nan) (0.00000 +- nan) (0.65285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +13 O (0.00000 +- nan) (0.00000 +- nan) (0.59715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +14 Ru (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +15 Ru (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +16 O (0.30569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +17 O (0.69431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +18 O (0.00000 +- nan) (0.00000 +- nan) (0.90285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +19 O (0.50000 +- nan) (0.00000 +- nan) (0.84715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +20 Ru (0.00000 +- nan) (0.00000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +21 Ru (0.50000 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +22 O (0.19431 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +23 O (0.80569 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = 0.75 +layer_behaviour: select +layer_cycle: 1.0 + + +TerminationUnitCell 2 RuO2_termination_2 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 13.0912 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.00000 +- nan) (0.00000 +- nan) (0.15285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.50000 +- nan) (0.00000 +- nan) (0.09716 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ru (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ru (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.19431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.80569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.50000 +- nan) (0.00000 +- nan) (0.40285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +07 O (0.00000 +- nan) (0.00000 +- nan) (0.34715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +08 Ru (0.50000 +- nan) (0.00000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +09 Ru (0.00000 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +10 O (0.30569 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +11 O (0.69431 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +12 O (0.00000 +- nan) (0.00000 +- nan) (0.65285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +13 O (0.50000 +- nan) (0.00000 +- nan) (0.59715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +14 Ru (0.00000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +15 Ru (0.50000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +16 O (0.19431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +17 O (0.80569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +18 O (0.50000 +- nan) (0.00000 +- nan) (0.90285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +19 O (0.00000 +- nan) (0.00000 +- nan) (0.84715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +20 Ru (0.50000 +- nan) (0.00000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +21 Ru (0.00000 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +22 O (0.30569 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +23 O (0.69431 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +layerpos: 2.0 = 0.75 +layer_behaviour: select +layer_cycle: 2.0 + + + +# Film RuO2 +0002 occupancy = 1.00000 + +Width/layers +(17.00000 +- nan) + +UnitCell RuO2 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (0.80569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (0.69431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ru (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ru (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (0.30569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (0.19431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ru (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ru (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +# EpitaxyInterface TiO2toRuO2 +0001 occupancy = 1.00000 +type skellam +Width/cells Skew/cells +0.35000 0.00000 + +TopUnitCell RuO2interf +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (0.80569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (0.69431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ru (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ru (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (0.30569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (0.19431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ru (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ru (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +BottomUnitCell TiO2interf +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (0.80458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (0.69542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ti (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ti (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30458 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69542 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (0.30458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (0.19542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ti (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ti (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19542 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80458 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +# UnitCell bulk +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (-0.19542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (-0.30458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ti (0.50000 +- nan) (0.00000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ti (0.00000 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30458 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69542 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (-0.69542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (-0.80458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ti (0.00000 +- nan) (0.00000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ti (0.50000 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19542 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80458 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = -1.0, 2.0 = -0.5 +layer_behaviour: ignore diff --git a/examples/CTR/RuO2_TiO2_Poisson_etching.xtal b/examples/CTR/RuO2_TiO2_Poisson_etching.xtal index fc7d326..4e310a9 100644 --- a/examples/CTR/RuO2_TiO2_Poisson_etching.xtal +++ b/examples/CTR/RuO2_TiO2_Poisson_etching.xtal @@ -1,124 +1,170 @@ -E = 20.00000 keV -# PoissonSurface RuO2surface -0003 occupancy = 1.00000 - -W/layers offset/layers --6.00000 0.00000 -support_cursor: nominal - -UnitCell RuO2 -return -Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 -6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 -Name x/frac y/frac z/frac iDW oDW occup layerIdx -00 O 0.50000 0.00000 0.80569 0.5033 0.5033 1.0000 2 -01 O 0.00000 0.00000 0.69431 0.5033 0.5033 1.0000 2 -02 Ru 0.50000 0.00000 0.50000 0.5033 0.5033 1.0000 2 -03 Ru 0.00000 0.50000 0.50000 0.5033 0.5033 1.0000 2 -04 O 0.30569 0.50000 0.50000 0.5033 0.5033 1.0000 2 -05 O 0.69431 0.50000 0.50000 0.5033 0.5033 1.0000 2 -06 O 0.00000 0.00000 0.30569 0.5033 0.5033 1.0000 1 -07 O 0.50000 0.00000 0.19431 0.5033 0.5033 1.0000 1 -08 Ru 0.00000 0.00000 0.00000 0.5033 0.5033 1.0000 1 -09 Ru 0.50000 0.50000 0.00000 0.5033 0.5033 1.0000 1 -10 O 0.19431 0.50000 0.00000 0.5033 0.5033 1.0000 1 -11 O 0.80569 0.50000 0.00000 0.5033 0.5033 1.0000 1 -layerpos: 1.0 = 0.0, 2.0 = 0.5 -layer_behaviour: ignore - - -# Film RuO2 -0002 occupancy = 1.00000 - -Width/layers -17.00000 - -UnitCell RuO2 -return -Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 -6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 -Name x/frac y/frac z/frac iDW oDW occup layerIdx -00 O 0.50000 0.00000 0.80569 0.5033 0.5033 1.0000 2 -01 O 0.00000 0.00000 0.69431 0.5033 0.5033 1.0000 2 -02 Ru 0.50000 0.00000 0.50000 0.5033 0.5033 1.0000 2 -03 Ru 0.00000 0.50000 0.50000 0.5033 0.5033 1.0000 2 -04 O 0.30569 0.50000 0.50000 0.5033 0.5033 1.0000 2 -05 O 0.69431 0.50000 0.50000 0.5033 0.5033 1.0000 2 -06 O 0.00000 0.00000 0.30569 0.5033 0.5033 1.0000 1 -07 O 0.50000 0.00000 0.19431 0.5033 0.5033 1.0000 1 -08 Ru 0.00000 0.00000 0.00000 0.5033 0.5033 1.0000 1 -09 Ru 0.50000 0.50000 0.00000 0.5033 0.5033 1.0000 1 -10 O 0.19431 0.50000 0.00000 0.5033 0.5033 1.0000 1 -11 O 0.80569 0.50000 0.00000 0.5033 0.5033 1.0000 1 -layerpos: 1.0 = 0.0, 2.0 = 0.5 -layer_behaviour: ignore - - -# EpitaxyInterface TiO2toRuO2 -0001 occupancy = 1.00000 -type skellam -Width/cells Skew/cells -0.35000 0.00000 -support_cursor: nominal - -TopUnitCell RuO2interf -return -Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 -6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 -Name x/frac y/frac z/frac iDW oDW occup layerIdx -00 O 0.50000 0.00000 0.80569 0.5033 0.5033 1.0000 2 -01 O 0.00000 0.00000 0.69431 0.5033 0.5033 1.0000 2 -02 Ru 0.50000 0.00000 0.50000 0.5033 0.5033 1.0000 2 -03 Ru 0.00000 0.50000 0.50000 0.5033 0.5033 1.0000 2 -04 O 0.30569 0.50000 0.50000 0.5033 0.5033 1.0000 2 -05 O 0.69431 0.50000 0.50000 0.5033 0.5033 1.0000 2 -06 O 0.00000 0.00000 0.30569 0.5033 0.5033 1.0000 1 -07 O 0.50000 0.00000 0.19431 0.5033 0.5033 1.0000 1 -08 Ru 0.00000 0.00000 0.00000 0.5033 0.5033 1.0000 1 -09 Ru 0.50000 0.50000 0.00000 0.5033 0.5033 1.0000 1 -10 O 0.19431 0.50000 0.00000 0.5033 0.5033 1.0000 1 -11 O 0.80569 0.50000 0.00000 0.5033 0.5033 1.0000 1 -layerpos: 1.0 = 0.0, 2.0 = 0.5 -layer_behaviour: ignore - - -BottomUnitCell TiO2interf -return -Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 -6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 -Name x/frac y/frac z/frac iDW oDW occup layerIdx -00 O 0.50000 0.00000 0.80458 0.3719 0.3719 1.0000 2 -01 O 0.00000 0.00000 0.69542 0.3719 0.3719 1.0000 2 -02 Ti 0.50000 0.00000 0.50000 0.3719 0.3719 1.0000 2 -03 Ti 0.00000 0.50000 0.50000 0.3719 0.3719 1.0000 2 -04 O 0.30458 0.50000 0.50000 0.3719 0.3719 1.0000 2 -05 O 0.69542 0.50000 0.50000 0.3719 0.3719 1.0000 2 -06 O 0.00000 0.00000 0.30458 0.3719 0.3719 1.0000 1 -07 O 0.50000 0.00000 0.19542 0.3719 0.3719 1.0000 1 -08 Ti 0.00000 0.00000 0.00000 0.3719 0.3719 1.0000 1 -09 Ti 0.50000 0.50000 0.00000 0.3719 0.3719 1.0000 1 -10 O 0.19542 0.50000 0.00000 0.3719 0.3719 1.0000 1 -11 O 0.80458 0.50000 0.00000 0.3719 0.3719 1.0000 1 -layerpos: 1.0 = 0.0, 2.0 = 0.5 -layer_behaviour: ignore - - -# UnitCell bulk -return -Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 -6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 -Name x/frac y/frac z/frac iDW oDW occup layerIdx -00 O 0.50000 0.00000 -0.19542 0.3719 0.3719 1.0000 2 -01 O 0.00000 0.00000 -0.30458 0.3719 0.3719 1.0000 2 -02 Ti 0.50000 0.00000 -0.50000 0.3719 0.3719 1.0000 2 -03 Ti 0.00000 0.50000 -0.50000 0.3719 0.3719 1.0000 2 -04 O 0.30458 0.50000 -0.50000 0.3719 0.3719 1.0000 2 -05 O 0.69542 0.50000 -0.50000 0.3719 0.3719 1.0000 2 -06 O 0.00000 0.00000 -0.69542 0.3719 0.3719 1.0000 1 -07 O 0.50000 0.00000 -0.80458 0.3719 0.3719 1.0000 1 -08 Ti 0.00000 0.00000 -1.00000 0.3719 0.3719 1.0000 1 -09 Ti 0.50000 0.50000 -1.00000 0.3719 0.3719 1.0000 1 -10 O 0.19542 0.50000 -1.00000 0.3719 0.3719 1.0000 1 -11 O 0.80458 0.50000 -1.00000 0.3719 0.3719 1.0000 1 -layerpos: 1.0 = -1.0, 2.0 = -0.5 -layer_behaviour: ignore +E = 20.00000 keV +# PoissonSurface RuO2surface +0003 occupancy = 1.00000 + +W/layers alpha offset/layers +-6.00000 1.00000 0.00000 + +TerminationUnitCell 1 RuO2_termination_1 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 13.0912 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.50000 0.00000 0.15285 0.5033 0.5033 1.0000 1 +01 O 0.00000 0.00000 0.09715 0.5033 0.5033 1.0000 1 +02 Ru 0.50000 0.00000 0.00000 0.5033 0.5033 1.0000 1 +03 Ru 0.00000 0.50000 0.00000 0.5033 0.5033 1.0000 1 +04 O 0.30569 0.50000 0.00000 0.5033 0.5033 1.0000 1 +05 O 0.69431 0.50000 0.00000 0.5033 0.5033 1.0000 1 +06 O 0.00000 0.00000 0.40285 0.5033 0.5033 1.0000 1 +07 O 0.50000 0.00000 0.34715 0.5033 0.5033 1.0000 1 +08 Ru 0.00000 0.00000 0.25000 0.5033 0.5033 1.0000 1 +09 Ru 0.50000 0.50000 0.25000 0.5033 0.5033 1.0000 1 +10 O 0.19431 0.50000 0.25000 0.5033 0.5033 1.0000 1 +11 O 0.80569 0.50000 0.25000 0.5033 0.5033 1.0000 1 +12 O 0.50000 0.00000 0.65285 0.5033 0.5033 1.0000 1 +13 O 0.00000 0.00000 0.59715 0.5033 0.5033 1.0000 1 +14 Ru 0.50000 0.00000 0.50000 0.5033 0.5033 1.0000 1 +15 Ru 0.00000 0.50000 0.50000 0.5033 0.5033 1.0000 1 +16 O 0.30569 0.50000 0.50000 0.5033 0.5033 1.0000 1 +17 O 0.69431 0.50000 0.50000 0.5033 0.5033 1.0000 1 +18 O 0.00000 0.00000 0.90285 0.5033 0.5033 1.0000 1 +19 O 0.50000 0.00000 0.84715 0.5033 0.5033 1.0000 1 +20 Ru 0.00000 0.00000 0.75000 0.5033 0.5033 1.0000 1 +21 Ru 0.50000 0.50000 0.75000 0.5033 0.5033 1.0000 1 +22 O 0.19431 0.50000 0.75000 0.5033 0.5033 1.0000 1 +23 O 0.80569 0.50000 0.75000 0.5033 0.5033 1.0000 1 +layerpos: 1.0 = 0.75 +layer_behaviour: select +layer_cycle: 1.0 + + +TerminationUnitCell 2 RuO2_termination_2 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 13.0912 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.00000 0.00000 0.15285 0.5033 0.5033 1.0000 2 +01 O 0.50000 0.00000 0.09716 0.5033 0.5033 1.0000 2 +02 Ru 0.00000 0.00000 0.00000 0.5033 0.5033 1.0000 2 +03 Ru 0.50000 0.50000 0.00000 0.5033 0.5033 1.0000 2 +04 O 0.19431 0.50000 0.00000 0.5033 0.5033 1.0000 2 +05 O 0.80569 0.50000 0.00000 0.5033 0.5033 1.0000 2 +06 O 0.50000 0.00000 0.40285 0.5033 0.5033 1.0000 2 +07 O 0.00000 0.00000 0.34715 0.5033 0.5033 1.0000 2 +08 Ru 0.50000 0.00000 0.25000 0.5033 0.5033 1.0000 2 +09 Ru 0.00000 0.50000 0.25000 0.5033 0.5033 1.0000 2 +10 O 0.30569 0.50000 0.25000 0.5033 0.5033 1.0000 2 +11 O 0.69431 0.50000 0.25000 0.5033 0.5033 1.0000 2 +12 O 0.00000 0.00000 0.65285 0.5033 0.5033 1.0000 2 +13 O 0.50000 0.00000 0.59715 0.5033 0.5033 1.0000 2 +14 Ru 0.00000 0.00000 0.50000 0.5033 0.5033 1.0000 2 +15 Ru 0.50000 0.50000 0.50000 0.5033 0.5033 1.0000 2 +16 O 0.19431 0.50000 0.50000 0.5033 0.5033 1.0000 2 +17 O 0.80569 0.50000 0.50000 0.5033 0.5033 1.0000 2 +18 O 0.50000 0.00000 0.90285 0.5033 0.5033 1.0000 2 +19 O 0.00000 0.00000 0.84715 0.5033 0.5033 1.0000 2 +20 Ru 0.50000 0.00000 0.75000 0.5033 0.5033 1.0000 2 +21 Ru 0.00000 0.50000 0.75000 0.5033 0.5033 1.0000 2 +22 O 0.30569 0.50000 0.75000 0.5033 0.5033 1.0000 2 +23 O 0.69431 0.50000 0.75000 0.5033 0.5033 1.0000 2 +layerpos: 2.0 = 0.75 +layer_behaviour: select +layer_cycle: 2.0 + + + +# Film RuO2 +0002 occupancy = 1.00000 + +Width/layers +17.00000 + +UnitCell RuO2 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.50000 0.00000 0.80569 0.5033 0.5033 1.0000 2 +01 O 0.00000 0.00000 0.69431 0.5033 0.5033 1.0000 2 +02 Ru 0.50000 0.00000 0.50000 0.5033 0.5033 1.0000 2 +03 Ru 0.00000 0.50000 0.50000 0.5033 0.5033 1.0000 2 +04 O 0.30569 0.50000 0.50000 0.5033 0.5033 1.0000 2 +05 O 0.69431 0.50000 0.50000 0.5033 0.5033 1.0000 2 +06 O 0.00000 0.00000 0.30569 0.5033 0.5033 1.0000 1 +07 O 0.50000 0.00000 0.19431 0.5033 0.5033 1.0000 1 +08 Ru 0.00000 0.00000 0.00000 0.5033 0.5033 1.0000 1 +09 Ru 0.50000 0.50000 0.00000 0.5033 0.5033 1.0000 1 +10 O 0.19431 0.50000 0.00000 0.5033 0.5033 1.0000 1 +11 O 0.80569 0.50000 0.00000 0.5033 0.5033 1.0000 1 +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +# EpitaxyInterface TiO2toRuO2 +0001 occupancy = 1.00000 +type skellam +Width/cells Skew/cells +0.35000 0.00000 + +TopUnitCell RuO2interf +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.50000 0.00000 0.80569 0.5033 0.5033 1.0000 2 +01 O 0.00000 0.00000 0.69431 0.5033 0.5033 1.0000 2 +02 Ru 0.50000 0.00000 0.50000 0.5033 0.5033 1.0000 2 +03 Ru 0.00000 0.50000 0.50000 0.5033 0.5033 1.0000 2 +04 O 0.30569 0.50000 0.50000 0.5033 0.5033 1.0000 2 +05 O 0.69431 0.50000 0.50000 0.5033 0.5033 1.0000 2 +06 O 0.00000 0.00000 0.30569 0.5033 0.5033 1.0000 1 +07 O 0.50000 0.00000 0.19431 0.5033 0.5033 1.0000 1 +08 Ru 0.00000 0.00000 0.00000 0.5033 0.5033 1.0000 1 +09 Ru 0.50000 0.50000 0.00000 0.5033 0.5033 1.0000 1 +10 O 0.19431 0.50000 0.00000 0.5033 0.5033 1.0000 1 +11 O 0.80569 0.50000 0.00000 0.5033 0.5033 1.0000 1 +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +BottomUnitCell TiO2interf +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.50000 0.00000 0.80458 0.3719 0.3719 1.0000 2 +01 O 0.00000 0.00000 0.69542 0.3719 0.3719 1.0000 2 +02 Ti 0.50000 0.00000 0.50000 0.3719 0.3719 1.0000 2 +03 Ti 0.00000 0.50000 0.50000 0.3719 0.3719 1.0000 2 +04 O 0.30458 0.50000 0.50000 0.3719 0.3719 1.0000 2 +05 O 0.69542 0.50000 0.50000 0.3719 0.3719 1.0000 2 +06 O 0.00000 0.00000 0.30458 0.3719 0.3719 1.0000 1 +07 O 0.50000 0.00000 0.19542 0.3719 0.3719 1.0000 1 +08 Ti 0.00000 0.00000 0.00000 0.3719 0.3719 1.0000 1 +09 Ti 0.50000 0.50000 0.00000 0.3719 0.3719 1.0000 1 +10 O 0.19542 0.50000 0.00000 0.3719 0.3719 1.0000 1 +11 O 0.80458 0.50000 0.00000 0.3719 0.3719 1.0000 1 +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +# UnitCell bulk +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.50000 0.00000 -0.19542 0.3719 0.3719 1.0000 2 +01 O 0.00000 0.00000 -0.30458 0.3719 0.3719 1.0000 2 +02 Ti 0.50000 0.00000 -0.50000 0.3719 0.3719 1.0000 2 +03 Ti 0.00000 0.50000 -0.50000 0.3719 0.3719 1.0000 2 +04 O 0.30458 0.50000 -0.50000 0.3719 0.3719 1.0000 2 +05 O 0.69542 0.50000 -0.50000 0.3719 0.3719 1.0000 2 +06 O 0.00000 0.00000 -0.69542 0.3719 0.3719 1.0000 1 +07 O 0.50000 0.00000 -0.80458 0.3719 0.3719 1.0000 1 +08 Ti 0.00000 0.00000 -1.00000 0.3719 0.3719 1.0000 1 +09 Ti 0.50000 0.50000 -1.00000 0.3719 0.3719 1.0000 1 +10 O 0.19542 0.50000 -1.00000 0.3719 0.3719 1.0000 1 +11 O 0.80458 0.50000 -1.00000 0.3719 0.3719 1.0000 1 +layerpos: 1.0 = -1.0, 2.0 = -0.5 +layer_behaviour: ignore diff --git a/orgui/datautils/xrayutils/CTRcalc.py b/orgui/datautils/xrayutils/CTRcalc.py index 2edb04b..c46af2c 100644 --- a/orgui/datautils/xrayutils/CTRcalc.py +++ b/orgui/datautils/xrayutils/CTRcalc.py @@ -262,18 +262,23 @@ def apply_stacking(self): layer_state_new = getattr(self.uc_bulk, "layer_state", None) layer_number_new = layer_state_new.layer if layer_state_new is not None else -1 loc_new = 0.0 + component_new = self.uc_bulk for stacking_level in np.unique(self.uc_stacking_ordered): below_height = height_new below_layer = layer_number_new below_loc = loc_new + below_component = component_new while self.uc_stacking_ordered[i] == stacking_level: uc = self.uc_surface_list_ordered[i] if hasattr(uc, "stack_on"): + stack_kwargs = {"below_state": layer_state_new} + if isinstance(uc, PoissonSurface): + stack_kwargs["below_component"] = below_component uc.stack_on( below_loc, below_height, below_layer, - below_state=layer_state_new, + **stack_kwargs, ) else: uc.start_layer_number = below_layer @@ -288,6 +293,7 @@ def apply_stacking(self): loc_new = uc.loc_absolute layer_number_new = uc.end_layer_number layer_state_new = getattr(uc, "layer_state", None) + component_new = uc i += 1 if i == len(self.uc_stacking_ordered): return diff --git a/orgui/datautils/xrayutils/CTRdistributions.py b/orgui/datautils/xrayutils/CTRdistributions.py index d686caa..7c2b408 100644 --- a/orgui/datautils/xrayutils/CTRdistributions.py +++ b/orgui/datautils/xrayutils/CTRdistributions.py @@ -1,5 +1,6 @@ """Statistical occupancy profiles for CTR interfaces and surfaces.""" +from abc import ABC, abstractmethod from dataclasses import dataclass import numpy as np @@ -7,6 +8,7 @@ DEFAULT_TAIL_PROBABILITY = 1e-10 +SURFACE_OCCUPANCY_TOLERANCE = 1e-12 def _validate_tail_probability(tail_probability): @@ -16,18 +18,95 @@ def _validate_tail_probability(tail_probability): return tail_probability -class PoissonProfile: - """Describe signed Poisson growth or etching of a surface. +class SurfaceProfile(ABC): + """Contract for discrete surface-height occupancy profiles. - The random surface-height change in structural layers is - ``offset + sign(mean_change) * Poisson(abs(mean_change))``. + Offsets passed to :meth:`surface_occupancy` are consecutive structural + layers ordered from bottom to top. :meth:`occupancy` returns the total + material fraction at each offset. The exposed fraction is the decrease + in material occupancy toward the next layer, with the highest represented + layer treated as completely exposed. + """ + + @abstractmethod + def support(self): + """Return the inclusive structural-layer support offsets.""" + + @abstractmethod + def occupancy(self, offsets): + """Return cumulative material occupancy at structural-layer offsets.""" + + def correction(self, offsets): + """Return material occupancy relative to a sharp surface at zero.""" + offsets = np.asarray(offsets, dtype=np.float64) + sharp = (offsets < 0).astype(np.float64) + return self.occupancy(offsets) - sharp + + def surface_occupancy(self, offsets): + """Return the truly exposed fraction of each represented layer. + + :param numpy.ndarray offsets: + Consecutive integer structural-layer offsets ordered bottom to + top. + :returns: + Exposed surface occupancy for every supplied offset. + :rtype: numpy.ndarray + :raises ValueError: + If offsets are not consecutive or the cumulative occupancy is + non-finite, outside zero to one, or increases toward the surface. + """ + offsets = np.asarray(offsets, dtype=np.float64) + if offsets.ndim != 1: + raise ValueError("surface offsets must be a one-dimensional array") + if offsets.size == 0: + return np.array([], dtype=np.float64) + if not np.all(np.isclose(offsets, np.rint(offsets))): + raise ValueError("surface offsets must be integer structural layers") + if offsets.size > 1 and not np.all(np.diff(offsets) == 1): + raise ValueError("surface offsets must be consecutive and bottom-to-top") + + occupancy = np.asarray(self.occupancy(offsets), dtype=np.float64) + if occupancy.shape != offsets.shape: + raise ValueError("surface occupancy must match the offsets shape") + if not np.all(np.isfinite(occupancy)): + raise ValueError("surface occupancy must be finite") + tolerance = SURFACE_OCCUPANCY_TOLERANCE + if np.any(occupancy < -tolerance) or np.any(occupancy > 1.0 + tolerance): + raise ValueError("surface occupancy must be between zero and one") + occupancy = np.clip(occupancy, 0.0, 1.0) + + exposed = np.empty_like(occupancy) + exposed[:-1] = occupancy[:-1] - occupancy[1:] + exposed[-1] = occupancy[-1] + if np.any(exposed < -tolerance): + raise ValueError("surface occupancy must not increase toward the surface") + return np.clip(exposed, 0.0, 1.0) + + +class PoissonProfile(SurfaceProfile): + """Describe signed step--Poisson growth or etching of a surface. + + The random surface-height change is the convolution + + ``Step(offset + (1 - alpha) * mean_change)`` + + ``+ sign(mean_change) * Poisson(alpha * abs(mean_change))``. + + For ``s = sign(s) * (floor(abs(s)) + f)``, ``Step(s)`` is + ``sign(s) * (floor(abs(s)) + Bernoulli(f))``. This two-layer + interpolation keeps a fractional deterministic offset continuous on the + structural-layer grid. Consequently, ``alpha = 0`` gives ideal + layer-by-layer growth or dissolution, and ``alpha = 1`` combines the + deterministic structural cursor with a Poisson process. :param float mean_change: Signed mean process width in structural layers. Positive values model growth and negative values model dissolution or etching. + :param float alpha: + Fraction of the absolute mean assigned to the Poisson component. + Must lie between zero and one. :param float offset: - Deterministic surface-height offset in structural layers. Set this to - ``-mean_change`` for mean-preserving roughness. + Deterministic surface-height offset in structural layers. :param float tail_probability: Maximum omitted probability mass in the unrepresented Poisson tail. :param float mean: @@ -37,6 +116,7 @@ class PoissonProfile: def __init__( self, mean_change=None, + alpha=1.0, offset=0.0, tail_probability=DEFAULT_TAIL_PROBABILITY, *, @@ -49,6 +129,9 @@ def __init__( elif mean is not None: raise TypeError("provide mean_change or mean, not both") self.mean_change = float(mean_change) + self.alpha = float(alpha) + if not 0.0 <= self.alpha <= 1.0: + raise ValueError("alpha must be between zero and one") self.offset = float(offset) self.tail_probability = _validate_tail_probability(tail_probability) @@ -59,26 +142,72 @@ def mean(self): @property def rate(self): - """Return the non-negative Poisson rate in structural layers.""" - return abs(self.mean_change) + """Return the Poisson component mean in structural layers.""" + return self.alpha * abs(self.mean_change) + + @property + def step_mean(self): + """Return the unsigned process step mean in structural layers.""" + return (1.0 - self.alpha) * abs(self.mean_change) + + @property + def structural_mean(self): + """Return the signed deterministic cursor mean in structural layers.""" + return self.offset + (1.0 - self.alpha) * self.mean_change @property def expected_height_change(self): """Return the expected surface-height change in structural layers.""" return self.offset + self.mean_change + def _step_parameters(self): + step_sign = -1 if self.structural_mean < 0 else 1 + magnitude = abs(self.structural_mean) + step_floor = int(np.floor(magnitude)) + step_fraction = magnitude - step_floor + step_low = step_sign * step_floor + step_high = step_low + step_sign + return step_low, step_high, step_fraction + + def _height_cdf(self, values): + """Return the CDF of the signed convolved surface-height change.""" + values = np.asarray(values, dtype=np.float64) + step_low, step_high, step_fraction = self._step_parameters() + if self.rate == 0: + lower = (values >= step_low).astype(np.float64) + upper = (values >= step_high).astype(np.float64) + elif self.mean_change > 0: + lower = poisson.cdf(np.floor(values - step_low), self.rate) + upper = poisson.cdf(np.floor(values - step_high), self.rate) + else: + lower = poisson.sf(np.ceil(step_low - values) - 1, self.rate) + upper = poisson.sf(np.ceil(step_high - values) - 1, self.rate) + return (1.0 - step_fraction) * lower + step_fraction * upper + + def probability(self, changes): + """Return probability masses for signed integer height changes.""" + changes = np.asarray(changes, dtype=np.float64) + integer = np.isclose(changes, np.rint(changes)) + changes = np.rint(changes) + probability = self._height_cdf(changes) - self._height_cdf(changes - 1) + return np.where(integer, probability, 0.0) + def support(self): """Return the inclusive structural-layer support offsets.""" + step_low, step_high, step_fraction = self._step_parameters() + if step_fraction == 0.0: + step_high = step_low if self.rate == 0: - process_low = process_high = self.offset + process_low = min(step_low, step_high) + process_high = max(step_low, step_high) else: quantile = int(poisson.ppf(1.0 - self.tail_probability, self.rate)) if self.mean_change > 0: - process_low = self.offset - process_high = self.offset + quantile + process_low = min(step_low, step_high) + process_high = max(step_low, step_high) + quantile else: - process_low = self.offset - quantile - process_high = self.offset + process_low = min(step_low, step_high) - quantile + process_high = max(step_low, step_high) lower = int(np.floor(min(0.0, process_low))) - 1 upper = int(np.ceil(max(0.0, process_high))) return lower, upper @@ -86,19 +215,7 @@ def support(self): def occupancy(self, offsets): """Return material occupancy at structural-layer offsets.""" offsets = np.asarray(offsets, dtype=np.float64) - if self.rate == 0: - return (offsets < self.offset).astype(np.float64) - if self.mean_change > 0: - return poisson.sf(offsets - self.offset, self.rate) - threshold = self.offset - offsets - return poisson.cdf(np.ceil(threshold) - 1, self.rate) - - def correction(self, offsets): - """Return occupancy relative to a sharp surface at zero.""" - offsets = np.asarray(offsets, dtype=np.float64) - sharp = (offsets < 0).astype(np.float64) - return self.occupancy(offsets) - sharp - + return 1.0 - self._height_cdf(offsets) @dataclass(frozen=True) class SkellamProfile: diff --git a/orgui/datautils/xrayutils/CTRfilm.py b/orgui/datautils/xrayutils/CTRfilm.py index b0fed5e..0ea94b0 100644 --- a/orgui/datautils/xrayutils/CTRfilm.py +++ b/orgui/datautils/xrayutils/CTRfilm.py @@ -31,10 +31,16 @@ import numpy as np from .. import util import re +from collections.abc import Mapping # random.seed(45) -from .CTRutil import _ensure_contiguous, next_skip_comment, LinearFitFunctions +from .CTRutil import ( + _ensure_contiguous, + generate_surface_termination_cells, + next_skip_comment, + LinearFitFunctions, +) from .CTRoptics import combine_profiles from .CTRuc import UnitCell, ctr_accel_enabled @@ -337,7 +343,14 @@ def _set_start_layer(self, start_layer): self._set_layer_order(layer_order, indices) self._basis_created = np.full_like(self.basis, np.nan) - def stack_on(self, below_loc, below_height, below_layer=-1, below_state=None): + def stack_on( + self, + below_loc, + below_height, + below_layer=-1, + below_state=None, + below_component=None, + ): """Generate this object's layer order and height from the object below. :param float below_loc: @@ -347,6 +360,9 @@ def stack_on(self, below_loc, below_height, below_layer=-1, below_state=None): :param float below_layer: Top cyclic layer identifier of the object below. """ + bind_underlying = getattr(self, "_bind_underlying_component", None) + if bind_underlying is not None: + bind_underlying(below_component) if below_state is None: below_state = LayerState(self.layer_cycle, below_layer) if self.layer_behavior == "ignore": @@ -1455,28 +1471,18 @@ def fromStr(cls, string): basis = np.array(sline, dtype=np.float64) errors = None - # very explicit searching for the lines containing TopUnitCell and BottomUnitCell: # noqa: E501 sp_str = string.splitlines() uc_pos = -1 - for i, l in enumerate(sp_str): # noqa: E741 - if uc_pos == -1: - if "UnitCell" in l: - uc_pos = i # found it, and save line number - if uc_pos != -1: + for i, line in enumerate(sp_str): + if "UnitCell" in line: + uc_pos = i break - else: - msg = "Cannot create Film. " - if uc_pos < 0: - msg += "No UnitCell provided. " - raise ValueError(msg) + if uc_pos < 0: + raise ValueError("Cannot create Film. No UnitCell provided.") uc_classname, uc_name = sp_str[uc_pos].split(maxsplit=1) - assert uc_classname == "UnitCell" - uc_str = "\n".join(sp_str[uc_pos + 1 :]) - - uc = UnitCell.fromStr(uc_str) - + uc = UnitCell.fromStr("\n".join(sp_str[uc_pos + 1 :])) uc.name = uc_name film = cls(uc, layer_transition=_parse_layer_transition(string)) @@ -1524,22 +1530,20 @@ def filmToStr(self, showErrors=True): class PoissonSurface(_LayerStackingMixin, LinearFitFunctions): - """Signed Poisson growth or etching at a nominal Film boundary. - - For the current API, ``W`` is the signed Poisson mean in structural layers - and ``offset`` is a deterministic height offset. Positive ``W`` models - growth, negative ``W`` models dissolution or etching, and - ``offset = -W`` preserves the original mean Film height. + """Signed step--Poisson growth or etching at a Film boundary. - Legacy files using ``Width/layers deltaW/layers`` remain readable with - their historical absolute-width semantics. + ``W`` is the signed process mean in structural layers. ``alpha`` assigns + a fraction of its magnitude to Poisson roughening and the remainder to + ideal layer-by-layer progression. ``offset`` is an independent, + deterministic structural height shift. Positive ``W`` models growth and + negative ``W`` models dissolution or etching. """ - parameterOrder = "W/layers offset/layers" + parameterOrder = "W/layers alpha offset/layers" - parameterLookup = {"W": 0, "offset": 1, "deltaW": 1} + parameterLookup = {"W": 0, "alpha": 1, "offset": 2} - parameterLookup_inv = {0: "W", 1: "offset"} + parameterLookup_inv = {0: "W", 1: "alpha", 2: "offset"} def __init__(self, unitcell, **kwargs): super().__init__() @@ -1547,17 +1551,16 @@ def __init__(self, unitcell, **kwargs): if profile is not None and not isinstance(profile, PoissonProfile): raise TypeError("PoissonSurface profile must be PoissonProfile") self.profile = profile - self._legacy_absolute_width = kwargs.pop( - "legacy_absolute_width", profile is None - ) self.type = type self.set_ucs(unitcell, **kwargs) if profile is None: - self.basis = np.array([0.0, 0.0]) + self.basis = np.array([0.0, 1.0, 0.0]) else: - self.basis = np.array([profile.mean_change, profile.offset]) - self._basis_created = np.array([np.nan, np.nan]) - self.basis_0 = np.array([0.0, 0.0]) + self.basis = np.array( + [profile.mean_change, profile.alpha, profile.offset] + ) + self._basis_created = np.array([np.nan, np.nan, np.nan]) + self.basis_0 = np.array([0.0, 0.0, 0.0]) self.errors = None if "name" in kwargs: self.name = kwargs["name"] @@ -1567,17 +1570,41 @@ def __init__(self, unitcell, **kwargs): self.below_loc = 0.0 self.below_H = 0.0 self.below_layer = -1.0 + self.underlying_film = None + self._film_layer_ucs_base = [] + self.film_layer_ucs = [] self._initialize_layer_stacking(kwargs) def set_ucs(self, unitcell, **kwargs): - self.unitcell = unitcell - self.uc_layers = self.unitcell.split_in_layers() - self._layer_ids = np.array(list(self.uc_layers)) - self._layer_ucs_base = [self.uc_layers[uc] for uc in self._layer_ids] - self._layerpos_base = np.array( - [self.unitcell.layerpos[i] for i in self._layer_ids] - ) - self.layer_ucs = list(self._layer_ucs_base) + """Set a legacy source cell or explicit termination-cell mapping.""" + self._source_unitcell = None + self.termination_cells = {} + if isinstance(unitcell, Mapping): + if not unitcell: + raise ValueError("PoissonSurface termination mapping cannot be empty") + self.termination_cells = { + float(layer): cell for layer, cell in unitcell.items() + } + self.unitcell = next(iter(self.termination_cells.values())) + self._layer_ids = np.asarray(list(self.termination_cells)) + else: + self._source_unitcell = unitcell + self.unitcell = unitcell + self._layer_ids = np.asarray(unitcell.layer_cycle.layers) + + self._layer_ucs_base = [] + self.layer_ucs = [] + if self.termination_cells: + self._layerpos_base = np.asarray( + [ + self.termination_cells[float(layer)].layerpos[float(layer)] + for layer in self._layer_ids + ] + ) + else: + self._layerpos_base = np.asarray( + [self.unitcell.layerpos[layer] for layer in self._layer_ids] + ) self.layerpos = np.copy(self._layerpos_base) @property @@ -1593,8 +1620,126 @@ def uc_area(self): def _set_layer_order(self, layer_order, order): self.layer_order = np.asarray(layer_order) self._layer_order_indices = np.asarray(order) - self.layer_ucs = [self._layer_ucs_base[i] for i in order] - self.layerpos = _unwrapped_layer_positions(self._layerpos_base, order) + if hasattr(self, "_termination_views"): + self.layer_ucs = [self._termination_views[float(i)] for i in layer_order] + elif self.termination_cells: + self.layer_ucs = [self.termination_cells[float(i)] for i in layer_order] + else: + self.layer_ucs = [] + if self._film_layer_ucs_base: + self.film_layer_ucs = [self._film_layer_ucs_base[i] for i in order] + if self.underlying_film is not None: + self.layerpos = _unwrapped_layer_positions(self._layerpos_base, order) + + def _owned_unitcells(self): + if self._source_unitcell is not None: + return [self._source_unitcell] + return list(self.termination_cells.values()) + + def _refresh_legacy_terminations(self): + if self._source_unitcell is None or self.underlying_film is None: + return + underlying_film = self.underlying_film + self.underlying_film = None + self._bind_underlying_component(underlying_film) + + def _wyckoff_target_unitcells(self, kwargs): + target = kwargs.pop("unitcell", None) + if target is None: + return [self.unitcell] + if isinstance(target, list | tuple): + return [self[item] for item in target] + return [self[target]] + + def _bind_underlying_component(self, component): + """Bind the Film whose exposed structural layers are replaced.""" + if not isinstance(component, Film): + raise ValueError( + "PoissonSurface must be stacked immediately above a Film" + ) + if self.underlying_film is component and hasattr( + self, "_film_termination_ucs" + ): + return + film_uc = component.unitcell + film_layers = film_uc.split_in_layers() + film_layer_ids = np.asarray(list(film_layers)) + + if self._source_unitcell is not None: + source = self._source_unitcell + self.termination_cells = generate_surface_termination_cells( + source, + film_layer_ids, + ) + if set(self.termination_cells) != set(film_layer_ids): + raise ValueError( + "PoissonSurface requires exactly one surface unit cell for " + "each layer in the underlying Film cycle" + ) + + self._layer_ids = film_layer_ids + self._layerpos_base = np.asarray( + [film_uc.layerpos[layer] for layer in film_layer_ids] + ) + self._layer_ucs_base = [ + self.termination_cells[float(layer)] for layer in film_layer_ids + ] + self._termination_views = {} + for layer, cell in self.termination_cells.items(): + view = cell.split_in_layers()[float(layer)] + view.name = cell.name + view.layerpos = dict(cell.layerpos) + view.layer_behavior = "select" + view._start_layer = float(layer) + self._termination_views[layer] = view + self._termination_domain_strain = { + id(self._termination_views[layer]): cell.coherentDomainMatrix[0][2, 2] + for layer, cell in self.termination_cells.items() + } + self._termination_domain_occupancy = { + id(self._termination_views[layer]): cell.coherentDomainOccupancy[0] + for layer, cell in self.termination_cells.items() + } + self._film_termination_ucs = {} + for layer, surface_uc in self.termination_cells.items(): + atom_layers = set(np.asarray(surface_uc.basis[:, 7], dtype=float)) + if atom_layers != {float(layer)}: + raise ValueError( + "Every atom in a surface termination cell must have its " + "termination key as the layer identifier; use " + "UnitCell.as_surface_termination" + ) + if surface_uc.layer_behavior != "select": + raise ValueError( + "Surface termination unit cells must use " + "layer_behavior='select'" + ) + if not np.allclose(surface_uc.a[:2], film_uc.a[:2]) or not np.allclose( + surface_uc.alpha, film_uc.alpha + ): + raise ValueError( + "PoissonSurface and underlying Film must have matching " + "lateral lattices and lattice angles" + ) + repeats_z = surface_uc.a[2] / film_uc.a[2] + if not np.isclose(repeats_z, np.rint(repeats_z)) or repeats_z < 1: + raise ValueError( + "Each surface termination c axis must be a positive integer " + "multiple of the underlying Film c axis" + ) + film_slab = film_uc.supercell((1, 1, int(np.rint(repeats_z)))) + film_terminations = generate_surface_termination_cells( + film_slab, + film_layer_ids, + ) + reference_uc = film_terminations[layer] + reference_uc.name = f"{film_uc.name}_reference_termination_{layer:g}" + self._film_termination_ucs[layer] = reference_uc + + self.underlying_film = component + self._film_layer_ucs_base = [film_layers[layer] for layer in film_layer_ids] + self._set_layer_order(self.layer_order, self._layer_order_indices) + self._basis_created = np.full_like(self.basis, np.nan) def setReferenceUnitCell(self, uc, rotMatrix=np.identity(3)): """Set the reference frame on the surface and generated layers. @@ -1605,9 +1750,12 @@ def setReferenceUnitCell(self, uc, rotMatrix=np.identity(3)): Optional 3-by-3 rotation from the reference frame into the surface crystal frame. """ - self.unitcell.setReferenceUnitCell(uc, rotMatrix) - for layer in self._layer_ucs_base: - layer.setReferenceUnitCell(uc, rotMatrix) + for cell in self._owned_unitcells(): + cell.setReferenceUnitCell(uc, rotMatrix) + for cell in getattr(self, "_termination_views", {}).values(): + cell.setReferenceUnitCell(uc, rotMatrix) + for cell in getattr(self, "_film_termination_ucs", {}).values(): + cell.setReferenceUnitCell(uc, rotMatrix) def setEnergy(self, E): """Set X-ray energy for the surface and generated layers. @@ -1616,9 +1764,12 @@ def setEnergy(self, E): X-ray energy in eV. """ self.E = E - self.unitcell.setEnergy(E) - for layer in self._layer_ucs_base: - layer.setEnergy(E) + for cell in self._owned_unitcells(): + cell.setEnergy(E) + for cell in getattr(self, "_termination_views", {}).values(): + cell.setEnergy(E) + for cell in getattr(self, "_film_termination_ucs", {}).values(): + cell.setEnergy(E) @property def loc_absolute(self): @@ -1633,27 +1784,13 @@ def set_below(self, loc, height): def height_absolute(self): if np.any(self._basis_created != self.basis): self.createLayers() - upper_layer_id = self.end_layer_number - upper_layer = self.uc_layers[upper_layer_id] - idx = self.layer_ucs.index(upper_layer) - pos = upper_layer.coherentDomainMatrix[-1][2, 3] * upper_layer.a[2] - strain = upper_layer.coherentDomainMatrix[-1][2, 2] - layerpos = self.unitcell.layerpos[upper_layer_id] - layer_space = np.diff(self.layerpos, append=self.layerpos[0] + 1) - H = pos + strain * (layerpos + layer_space[idx]) * upper_layer.a[2] - return H + return self._height_absolute @property def pos_absolute(self): if np.any(self._basis_created != self.basis): self.createLayers() - lower_layer = self.layer_ucs[0] - matrix = lower_layer.coherentDomainMatrix[0] - layer_id = self.layer_order[0] - H = ( - matrix[2, 3] + matrix[2, 2] * self.unitcell.layerpos[layer_id] - ) * lower_layer.a[2] - return H + return self.below_H @pos_absolute.setter def pos_absolute(self, pos): @@ -1661,6 +1798,8 @@ def pos_absolute(self, pos): self.createLayers() delta_height = pos - self.pos_absolute _move_domains(self.layer_ucs, delta_height) + _move_domains(self.film_layer_ucs, delta_height) + _move_domains(list(self._film_termination_ucs.values()), delta_height) self.below_H = pos @property @@ -1672,8 +1811,6 @@ def end_layer_number(self): @property def stacking_height_absolute(self): """Return the expected surface height in Angstrom.""" - if self._legacy_absolute_width: - return self.height_absolute return self.mean_height_absolute @property @@ -1684,89 +1821,148 @@ def stacking_loc_absolute(self): @property def mean_height_absolute(self): """Return the expected surface height in Angstrom.""" - layer_height = self.unitcell.a[2] / len(self.unitcell.layers) - return self.below_H + layer_height * (self.basis[0] + self.basis[1]) + film_uc = ( + self.underlying_film.unitcell + if self.underlying_film + else self.unitcell + ) + layer_height = film_uc.a[2] / len(self._layer_ids) + return self.below_H + layer_height * (self.basis[0] + self.basis[2]) def createLayers(self): - """Create layer domains and their Poisson surface occupancies.""" - n_layers_in_uc = len(self.unitcell.layers) + """Create co-located surface and covered-Film layer domains.""" + if self.underlying_film is None: + raise ValueError( + "PoissonSurface must be stacked immediately above a Film " + "before layers can be created" + ) + n_layers_in_uc = len(self._layer_ids) tail_probability = ( self.profile.tail_probability if self.profile is not None else DEFAULT_TAIL_PROBABILITY ) - if self._legacy_absolute_width: - delta_width = self.basis[1] - if delta_width < 0: - raise ValueError("legacy deltaW must be greater than or equal to zero") - profile = PoissonProfile( - mean_change=delta_width, - tail_probability=tail_probability, - ) - mean_width = self.basis[0] - n_layers_in_uc * ( - (self.below_H - self.below_loc) / self.unitcell.a[2] - ) - scaled_width = mean_width + delta_width - layers_to_create = int(round(scaled_width, 0)) - if layers_to_create <= 0: - raise ValueError("Effective film width <= 0. Cannot create layers") - layer_numbers = np.arange(layers_to_create) - if delta_width == 0: - layer_occupancy = np.ones(layers_to_create) - else: - minimum_width = mean_width - delta_width - layer_occupancy = profile.occupancy(layer_numbers - minimum_width) - else: - profile = PoissonProfile( - mean_change=self.basis[0], - offset=self.basis[1], - tail_probability=tail_probability, - ) - support_low, support_high = profile.support() - layer_numbers = np.arange(support_low, support_high + 1) - layer_occupancy = profile.correction(layer_numbers) - represented = np.abs(layer_occupancy) > tail_probability - layer_numbers = layer_numbers[represented] - layer_occupancy = layer_occupancy[represented] - layers_to_create = len(layer_numbers) - for i, uc in enumerate(self.layer_ucs): + profile = PoissonProfile( + mean_change=self.basis[0], + alpha=self.basis[1], + offset=self.basis[2], + tail_probability=tail_probability, + ) + support_low, support_high = profile.support() + layer_numbers = np.arange(support_low, support_high + 1) + material_occupancy = profile.occupancy(layer_numbers) + represented_material = np.flatnonzero( + material_occupancy > tail_probability + ) + if represented_material.size == 0: + raise ValueError("Poisson surface profile has no represented material") + top_stop = represented_material[-1] + 1 + layer_numbers = layer_numbers[:top_stop] + material_occupancy = material_occupancy[:top_stop] + surface_occupancy = profile.surface_occupancy(layer_numbers) + sharp_film_occupancy = (layer_numbers < 0).astype(np.float64) + film_correction_occupancy = material_occupancy - sharp_film_occupancy + represented = (surface_occupancy > tail_probability) | ( + np.abs(film_correction_occupancy) > tail_probability + ) + layer_numbers = layer_numbers[represented] + surface_occupancy = surface_occupancy[represented] + film_correction_occupancy = film_correction_occupancy[represented] + layers_to_create = len(layer_numbers) + for uc in self.layer_ucs: + uc.coherentDomainMatrix = [] + uc.coherentDomainOccupancy = [] + for uc in self.film_layer_ucs: + uc.coherentDomainMatrix = [] + uc.coherentDomainOccupancy = [] + for uc in self._film_termination_ucs.values(): uc.coherentDomainMatrix = [] uc.coherentDomainOccupancy = [] mat_0 = np.vstack((np.identity(3).T, np.array([0, 0, 0]))).T - strain = self.unitcell.coherentDomainMatrix[0][2, 2] - occup = self.unitcell.coherentDomainOccupancy[0] - - layer_occupancy *= occup + film_domain_occupancy = ( + self.underlying_film.unitcell.coherentDomainOccupancy[0] + ) for layer_index, layer_number in enumerate(layer_numbers): order_index = layer_number % n_layers_in_uc cycle_index = layer_number // n_layers_in_uc uc = self.layer_ucs[order_index] + film_uc = self.film_layer_ucs[order_index] + reference_uc = self._film_termination_ucs[ + float(self.layer_order[order_index]) + ] mat_i = np.copy(mat_0) layer_id = self.layer_order[order_index] relative_layer_position = ( cycle_index + self.layerpos[order_index] - self.layerpos[0] ) - layer_offset = relative_layer_position - self.unitcell.layerpos[layer_id] + layer_offset = ( + relative_layer_position + - self.underlying_film.unitcell.layerpos[layer_id] + ) - mat_i[2, 2] = strain - mat_i[2, 3] = layer_offset * strain + film_strain = self.underlying_film.unitcell.coherentDomainMatrix[0][2, 2] + mat_i[2, 2] = film_strain + mat_i[2, 3] = layer_offset * film_strain - uc.coherentDomainMatrix.append(mat_i) - uc.coherentDomainOccupancy.append(layer_occupancy[layer_index]) + film_uc.coherentDomainMatrix.append(np.copy(mat_i)) + film_uc.coherentDomainOccupancy.append( + film_domain_occupancy * film_correction_occupancy[layer_index] + ) + + terrace_height = ( + relative_layer_position * self.underlying_film.unitcell.a[2] + ) + surface_matrix = np.copy(mat_0) + surface_strain = self._termination_domain_strain[id(uc)] + surface_origin = uc.layerpos[float(layer_id)] + surface_matrix[2, 2] = surface_strain + surface_matrix[2, 3] = ( + terrace_height / uc.a[2] - surface_strain * surface_origin + ) + uc.coherentDomainMatrix.append(surface_matrix) + uc.coherentDomainOccupancy.append( + self._termination_domain_occupancy[id(uc)] + * surface_occupancy[layer_index] + ) + + reference_matrix = np.copy(mat_0) + reference_origin = reference_uc.layerpos[float(layer_id)] + reference_matrix[2, 2] = film_strain + reference_matrix[2, 3] = ( + terrace_height / reference_uc.a[2] + - film_strain * reference_origin + ) + reference_uc.coherentDomainMatrix.append(reference_matrix) + reference_uc.coherentDomainOccupancy.append( + -film_domain_occupancy * surface_occupancy[layer_index] + ) if layers_to_create: - upper_layer = uc.basis[0, 7] - self._end_layer_number = upper_layer + self._end_layer_number = self.layer_order[ + int(layer_numbers[-1] % n_layers_in_uc) + ] + top_relative = ( + layer_numbers[-1] // n_layers_in_uc + + self.layerpos[int(layer_numbers[-1] % n_layers_in_uc)] + - self.layerpos[0] + ) + layer_spacing = self.underlying_film.unitcell.a[2] / n_layers_in_uc + self._height_absolute = self.below_H + ( + top_relative * self.underlying_film.unitcell.a[2] + layer_spacing + ) else: self._end_layer_number = self.start_layer_number + self._height_absolute = self.below_H _translate_domains(self.layer_ucs, self.below_H) + _translate_domains(self.film_layer_ucs, self.below_H) + _translate_domains(list(self._film_termination_ucs.values()), self.below_H) self._basis_created = np.copy(self.basis) def F_uc(self, h, k, l): # noqa: E741 - """Return the Poisson surface correction in electrons. + """Return the step--Poisson surface correction in electrons. Positive occupancies add grown material and negative occupancies remove etched material relative to the sharp Film boundary. The amplitude @@ -1787,18 +1983,28 @@ def F_uc(self, h, k, l): # noqa: E741 if ctr_accel_enabled(): h, k, l = _ensure_contiguous(h, k, l, testOnly=False, astype=np.float64) # noqa: E741 F = np.zeros_like(l, dtype=np.complex128) - for uc in self.layer_ucs: - if uc.coherentDomainMatrix: - F += uc.F_uc(h, k, l) + for surface_uc, film_uc in zip(self.layer_ucs, self.film_layer_ucs): + if surface_uc.coherentDomainMatrix: + F += surface_uc.F_uc(h, k, l) + if film_uc.coherentDomainMatrix: + F += film_uc.F_uc(h, k, l) + for film_uc in self._film_termination_ucs.values(): + if film_uc.coherentDomainMatrix: + F += film_uc.F_uc(h, k, l) return F def zDensity_G(self, z, h, k): if np.any(self._basis_created != self.basis): self.createLayers() rho = np.zeros_like(z, dtype=np.complex128) - for uc in self.layer_ucs: - if uc.coherentDomainMatrix: - rho += uc.zDensity_G(z, h, k) + for surface_uc, film_uc in zip(self.layer_ucs, self.film_layer_ucs): + if surface_uc.coherentDomainMatrix: + rho += surface_uc.zDensity_G(z, h, k) + if film_uc.coherentDomainMatrix: + rho += film_uc.zDensity_G(z, h, k) + for film_uc in self._film_termination_ucs.values(): + if film_uc.coherentDomainMatrix: + rho += film_uc.zDensity_G(z, h, k) return rho def optical_profile(self): @@ -1811,9 +2017,16 @@ def optical_profile(self): """ if np.any(self._basis_created != self.basis): self.createLayers() - return combine_profiles( - *(uc.optical_profile() for uc in self.layer_ucs if uc.coherentDomainMatrix) - ) + profiles = [] + for surface_uc, film_uc in zip(self.layer_ucs, self.film_layer_ucs): + if surface_uc.coherentDomainMatrix: + profiles.append(surface_uc.optical_profile()) + if film_uc.coherentDomainMatrix: + profiles.append(film_uc.optical_profile()) + for film_uc in self._film_termination_ucs.values(): + if film_uc.coherentDomainMatrix: + profiles.append(film_uc.optical_profile()) + return combine_profiles(*profiles) def addFitParameter(self, indexarray, limits=(-np.inf, np.inf), **kwarg): """to assign multiple unitcells with the same fitparameter, provide list of @@ -1822,7 +2035,9 @@ def addFitParameter(self, indexarray, limits=(-np.inf, np.inf), **kwarg): if len(np.array(indexarray).shape) < 2: return super().addFitParameter(indexarray, limits, **kwarg) - return self.unitcell.addFitParameter(indexarray, limits, **kwarg) + target = kwarg.pop("unitcell", None) + cell = self[target] if target is not None else self.unitcell + return cell.addFitParameter(indexarray, limits, **kwarg) def addRelParameter(self, indexarray, factors, limits=(-np.inf, np.inf), **kwarg): """to assign multiple unitcells with the same fitparameter, provide list of @@ -1830,7 +2045,9 @@ def addRelParameter(self, indexarray, factors, limits=(-np.inf, np.inf), **kwarg """ if len(np.array(indexarray).shape) < 2: return super().addRelParameter(indexarray, factors, limits, **kwarg) - return self.unitcell.addRelParameter(indexarray, factors, limits, **kwarg) + target = kwarg.pop("unitcell", None) + cell = self[target] if target is not None else self.unitcell + return cell.addRelParameter(indexarray, factors, limits, **kwarg) def getStartParamAndLimits(self, force_recalculate=False): # if self.basis_0 is None: @@ -1838,9 +2055,13 @@ def getStartParamAndLimits(self, force_recalculate=False): x0, lower, upper = super().getStartParamAndLimits( force_recalculate ) # absolute and relative - uc_x0, uc_lower, uc_upper = self.unitcell.getStartParamAndLimits( - force_recalculate - ) + uc_values = [ + cell.getStartParamAndLimits(force_recalculate) + for cell in self._owned_unitcells() + ] + uc_x0 = np.concatenate([value[0] for value in uc_values]) + uc_lower = np.concatenate([value[1] for value in uc_values]) + uc_upper = np.concatenate([value[2] for value in uc_values]) return ( np.concatenate([x0, uc_x0]), np.concatenate([lower, uc_lower]), @@ -1849,68 +2070,109 @@ def getStartParamAndLimits(self, force_recalculate=False): def setFitParameters(self, x): abs_rel_no = len(self.parameters["absolute"]) + len(self.parameters["relative"]) - fp_no = len(self.unitcell.fitparnames) super().setFitParameters(x[:abs_rel_no]) if self.profile is not None: self.profile = PoissonProfile( mean_change=self.basis[0], - offset=self.basis[1], + alpha=self.basis[1], + offset=self.basis[2], tail_probability=self.profile.tail_probability, ) - self.unitcell.setFitParameters(x[abs_rel_no : abs_rel_no + fp_no]) + cursor = abs_rel_no + for cell in self._owned_unitcells(): + fp_no = len(cell.fitparnames) + cell.setFitParameters(x[cursor : cursor + fp_no]) + cursor += fp_no + self._refresh_legacy_terminations() def setLimits(self, lim): abs_rel_no = len(self.parameters["absolute"]) + len(self.parameters["relative"]) - fp_no = len(self.unitcell.fitparnames) super().setLimits(lim[:abs_rel_no]) - self.unitcell.setLimits(lim[abs_rel_no : abs_rel_no + fp_no]) + cursor = abs_rel_no + for cell in self._owned_unitcells(): + fp_no = len(cell.fitparnames) + cell.setLimits(lim[cursor : cursor + fp_no]) + cursor += fp_no def setFitErrors(self, errors): abs_rel_no = len(self.parameters["absolute"]) + len(self.parameters["relative"]) - fp_no = len(self.unitcell.fitparnames) super().setFitErrors(errors[:abs_rel_no]) - self.unitcell.setFitErrors(errors[abs_rel_no : abs_rel_no + fp_no]) + cursor = abs_rel_no + for cell in self._owned_unitcells(): + fp_no = len(cell.fitparnames) + cell.setFitErrors(errors[cursor : cursor + fp_no]) + cursor += fp_no def getFitErrors(self): err = super().getFitErrors() - err_uc = self.unitcell.getFitErrors() + err_uc = np.concatenate( + [cell.getFitErrors() for cell in self._owned_unitcells()] + ) return np.concatenate([err, err_uc]) @property def fitparnames(self): - return super().fitparnames + self.unitcell.fitparnames + cells = self._owned_unitcells() + if len(cells) == 1: + names = cells[0].fitparnames + else: + names = [ + f"{cell.name}:{name}" + for cell in cells + for name in cell.fitparnames + ] + return super().fitparnames + names @property def priors(self): - return super().priors + self.unitcell.priors + return super().priors + [ + prior for cell in self._owned_unitcells() for prior in cell.priors + ] def parametersToDict(self): d = super().parametersToDict() - d["unitcells"] = {} - d["unitcells"]["unitcell"] = self.unitcell.parametersToDict() + d["unitcells"] = { + cell.name: cell.parametersToDict() for cell in self._owned_unitcells() + } return d def clearParameters(self): super().clearParameters() - self.unitcell.clearParameters() + for cell in self._owned_unitcells(): + cell.clearParameters() def parametersFromDict(self, d, override_values=True): - self.unitcell.parametersFromDict(d["unitcells"]["unitcell"], override_values) + cells = self._owned_unitcells() + stored = d["unitcells"] + if "unitcell" in stored and len(cells) == 1: + cells[0].parametersFromDict(stored["unitcell"], override_values) + else: + for cell in cells: + cell.parametersFromDict(stored[cell.name], override_values) super().parametersFromDict(d, override_values) def updateFromParameters(self): """Update basis from the values stored in the Parameters""" - self.unitcell.updateFromParameters() + for cell in self._owned_unitcells(): + cell.updateFromParameters() super().updateFromParameters() if self.profile is not None: self.profile = PoissonProfile( mean_change=self.basis[0], - offset=self.basis[1], + alpha=self.basis[1], + offset=self.basis[2], tail_probability=self.profile.tail_probability, ) + self._refresh_legacy_terminations() def __getitem__(self, uc_name_or_index): if isinstance(uc_name_or_index, str): + for layer, cell in self.termination_cells.items(): + if uc_name_or_index.lower() in { + cell.name.lower(), + f"termination_{layer:g}", + }: + return cell if uc_name_or_index.lower() in [ "uc", "unitcell", @@ -1921,11 +2183,22 @@ def __getitem__(self, uc_name_or_index): raise KeyError( f"No unit cell {uc_name_or_index} in PoissonSurface {self.name}" ) + elif isinstance(uc_name_or_index, int | float | np.integer | np.floating): + try: + return self.termination_cells[float(uc_name_or_index)] + except KeyError as exc: + raise KeyError( + f"No termination {uc_name_or_index} in PoissonSurface {self.name}" + ) from exc else: raise ValueError(f"must be str, not {type(uc_name_or_index)}") def parameter_list(self): - return super().parameter_list() + self.unitcell.parameter_list() + return super().parameter_list() + [ + parameter + for cell in self._owned_unitcells() + for parameter in cell.parameter_list() + ] @classmethod def fromStr(cls, string): @@ -1965,60 +2238,53 @@ def fromStr(cls, string): basis = np.array(sline, dtype=np.float64) errors = None - # very explicit searching for the lines containing TopUnitCell and BottomUnitCell: # noqa: E501 sp_str = string.splitlines() - uc_pos = -1 - for i, l in enumerate(sp_str): # noqa: E741 - if uc_pos == -1: - if "UnitCell" in l: - uc_pos = i # found it, and save line number - if uc_pos != -1: - break - else: - msg = "Cannot create Film. " - if uc_pos < 0: - msg += "No UnitCell provided. " - raise ValueError(msg) - - uc_classname, uc_name = sp_str[uc_pos].split(maxsplit=1) - - assert uc_classname == "UnitCell" - uc_str = "\n".join(sp_str[uc_pos + 1 :]) - - uc = UnitCell.fromStr(uc_str) - - uc.name = uc_name + cell_headers = [ + i + for i, line in enumerate(sp_str) + if line.strip().startswith(("UnitCell ", "TerminationUnitCell ")) + ] + if not cell_headers: + raise ValueError("Cannot create PoissonSurface. No UnitCell provided.") + cells = {} + legacy_uc = None + for header_index, end_index in zip( + cell_headers, cell_headers[1:] + [len(sp_str)] + ): + header = sp_str[header_index].split(maxsplit=2) + cell = UnitCell.fromStr( + "\n".join(sp_str[header_index + 1 : end_index]) + ) + if header[0] == "TerminationUnitCell": + if len(header) != 3: + raise ValueError( + "TerminationUnitCell requires a layer identifier and name" + ) + layer = float(header[1]) + cell.name = header[2] + cells[layer] = cell + else: + cell.name = " ".join(header[1:]) + legacy_uc = cell + if cells and legacy_uc is not None: + raise ValueError( + "PoissonSurface cannot mix UnitCell and TerminationUnitCell sections" + ) + unitcells = cells if cells else legacy_uc tail_probability = _parse_float_metadata( string, "tail_probability", DEFAULT_TAIL_PROBABILITY ) - legacy_parameter_names = any("deltaW" in line for line in string.splitlines()) - legacy_absolute_width = legacy_parameter_names - if legacy_absolute_width: - profile = PoissonProfile( - mean_change=basis[1], - tail_probability=tail_probability, - ) - elif legacy_parameter_names: - # Migrate the short-lived nominal-cursor format where W was zero - # and deltaW was a positive, mean-preserving roughness parameter. - profile = PoissonProfile( - mean_change=basis[1], - offset=-basis[1], - tail_probability=tail_probability, - ) - basis = np.array([profile.mean_change, profile.offset]) - else: - profile = PoissonProfile( - mean_change=basis[0], - offset=basis[1], - tail_probability=tail_probability, - ) + profile = PoissonProfile( + mean_change=basis[0], + alpha=basis[1], + offset=basis[2], + tail_probability=tail_probability, + ) film = cls( - uc, + unitcells, profile=profile, layer_transition=_parse_layer_transition(string), - legacy_absolute_width=legacy_absolute_width, ) film.statistics = statistics film.basis = basis @@ -2036,11 +2302,7 @@ def toStr(self, showErrors=True): :rtype: str """ # s = "type %s" % self.type - if self._legacy_absolute_width: - parameter_order = "Width/layers deltaW/layers" - else: - parameter_order = PoissonSurface.parameterOrder - s = "\n" + parameter_order + "\n" + self.filmToStr(showErrors=showErrors) + s = "\n" + self.parameterOrder + "\n" + self.filmToStr(showErrors=showErrors) if ( self.profile is not None and self.profile.tail_probability != DEFAULT_TAIL_PROBABILITY @@ -2050,8 +2312,13 @@ def toStr(self, showErrors=True): if metadata: s += "\n" + metadata.rstrip() s += "\n\n" - s += f"UnitCell {self.unitcell.name}\n" - s += self.unitcell.toStr(showErrors=showErrors) + "\n" + if self._source_unitcell is not None: + s += f"UnitCell {self._source_unitcell.name}\n" + s += self._source_unitcell.toStr(showErrors=showErrors) + "\n" + else: + for layer, cell in self.termination_cells.items(): + s += f"TerminationUnitCell {layer:g} {cell.name}\n" + s += cell.toStr(showErrors=showErrors) + "\n\n" return s def __repr__(self): diff --git a/orgui/datautils/xrayutils/CTRuc.py b/orgui/datautils/xrayutils/CTRuc.py index d1ffc98..66fb61a 100644 --- a/orgui/datautils/xrayutils/CTRuc.py +++ b/orgui/datautils/xrayutils/CTRuc.py @@ -1755,6 +1755,59 @@ def affine_layer_transform(self, translation, name=None): transformed._explicit_layer_cycle = tuple(cycle) return transformed + def as_surface_termination(self, layer, name=None, origin=None): + """Return a copy whose complete structure is one surface termination. + + The atomic ``layer`` column is used only as a stacking/termination + selector in the returned cell. All atoms therefore receive the same + layer identifier, while their fractional coordinates and fit + parameters remain unchanged. This permits a multi-layer relaxed slab + to be selected as one termination with ``layer_behavior='select'``. + + :param float layer: + Layer identifier in the primitive Film stacking cycle. + :param str name: + Optional name for the returned unit cell. + :param float origin: + Fractional z coordinate in this cell that is placed at the exposed + terrace height. By default the highest structural-layer origin is + used. + :returns: + Independent termination-cell copy. + :rtype: UnitCell + """ + transformed = copy.deepcopy(self) + if name is not None: + transformed.name = name + layer = float(layer) + if origin is None: + origins = list(self.layerpos.values()) + if origins: + origin = max(origins) + elif self.basis.size: + origin = float(np.max(self.basis[:, 3])) + else: + origin = 0.0 + origin = float(origin) + + for values in ( + transformed.basis, + transformed.basis_0, + transformed._basis_parvalues, + ): + if values is not None and values.size: + values[:, 7] = layer + if transformed.symmetry_metadata is not None: + transformed.symmetry_metadata.atoms = tuple( + dataclasses.replace(atom, layer=layer) + for atom in transformed.symmetry_metadata.atoms + ) + transformed.layerpos = {layer: origin} + transformed._explicit_layer_cycle = (layer,) + transformed.layer_behavior = "select" + transformed._start_layer = layer + return transformed + def supercell(self, repeats, symmetry="preserve", name=None): """Return a repeated unit-cell copy. diff --git a/orgui/datautils/xrayutils/CTRutil.py b/orgui/datautils/xrayutils/CTRutil.py index cd982b8..ca72b22 100644 --- a/orgui/datautils/xrayutils/CTRutil.py +++ b/orgui/datautils/xrayutils/CTRutil.py @@ -525,6 +525,96 @@ def next_skip_comment(it, comment=("//", "return")): return line +def generate_surface_termination_cells( + surface_supercell, + film_cycle, + name_template=None, +): + """Generate one complete surface-slab cell per Film termination. + + The returned cells are suitable for any roughness model that selects a + surface structure from the primitive Film stacking cycle. Each variant is + first shifted with ``UnitCell.affine_layer_transform`` so the requested + primitive-cycle member is the uppermost internal layer. All atoms are + then assigned that one termination identifier with + ``UnitCell.as_surface_termination``. Internal slab planes remain encoded + by their fractional z coordinates and may be fitted independently. + + :param UnitCell surface_supercell: + Surface slab produced by ``UnitCell.supercell``. Its number of + structural layers must be an integer multiple of the Film-cycle + length. + :param film_cycle: + A Film, UnitCell, LayerCycle, or iterable of primitive Film layer + identifiers, ordered from bottom to top. + :param str name_template: + Optional ``str.format`` template for generated names. The fields + ``name`` (the source-cell name) and ``layer`` are available. The + default is ``"{name}_termination_"``. + :returns: + Mapping from primitive Film layer identifier to an independent, + complete termination unit cell. + :rtype: dict + :raises ValueError: + If either cycle is empty, contains duplicate identifiers, or the slab + layer count is not an integer multiple of the Film cycle. + :raises TypeError: + If the supplied surface cell does not provide the required layered + transformation methods. + """ + required_methods = ("affine_layer_transform", "as_surface_termination") + if any(not hasattr(surface_supercell, method) for method in required_methods): + raise TypeError( + "surface_supercell must be a layered UnitCell with affine and " + "surface-termination transforms" + ) + + cycle = getattr(film_cycle, "layer_cycle", film_cycle) + cycle = getattr(cycle, "layers", cycle) + film_layers = tuple(cycle) + if not film_layers: + raise ValueError("Film termination cycle cannot be empty") + if len(set(film_layers)) != len(film_layers): + raise ValueError("Film termination cycle identifiers must be unique") + + internal_layers = tuple(surface_supercell.layer_cycle.layers) + if not internal_layers: + raise ValueError("Surface supercell layer cycle cannot be empty") + if len(internal_layers) % len(film_layers): + raise ValueError( + "Surface slab layer count must be an integer multiple of the " + "Film layer-cycle length" + ) + + terminations = {} + for termination_index, termination in enumerate(film_layers): + source_index = list( + range(termination_index, len(internal_layers), len(film_layers)) + )[-1] + shift = len(internal_layers) - 1 - source_index + if name_template is None: + layer_label = f"{termination:g}" if isinstance( + termination, int | float | np.integer | np.floating + ) else str(termination) + name = f"{surface_supercell.name}_termination_{layer_label}" + else: + name = name_template.format( + name=surface_supercell.name, + layer=termination, + ) + transformed = surface_supercell.affine_layer_transform( + [0, 0, shift], + name=name, + ) + origin = max(transformed.layerpos.values()) + terminations[termination] = transformed.as_surface_termination( + termination, + name=name, + origin=origin, + ) + return terminations + + SQRT2pi = np.sqrt(2 * np.pi) diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index a075ca1..52c51ac 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -31,6 +31,7 @@ __email__ = "fuchs@physik.uni-kiel.de" import dataclasses +import copy import unittest from unittest import mock import os @@ -40,7 +41,8 @@ from ... import util from .. import CTRcalc, CTRfilm, CTRplotutil, CTRsymmetry, CTRuc -from ..CTRdistributions import PoissonProfile, SkellamProfile +from ..CTRdistributions import PoissonProfile, SkellamProfile, SurfaceProfile +from ..CTRutil import generate_surface_termination_cells class TestReadSXRDCrystal(unittest.TestCase): @@ -143,11 +145,24 @@ def setUp(self): self.unitcell.addAtom("C", [0.0, 0.0, 0.5], 0.1, 0.1, 1.0, layer=1) self.unitcell.coherentDomainOccupancy = [0.75] + def bind_surface(self, surface, loc=0.0, height=4.0): + film = CTRfilm.Film(copy.deepcopy(self.unitcell), name="underlying_film") + film.basis[0] = 2.0 + film.createLayers() + surface.stack_on( + loc, + height, + film.end_layer_number, + below_state=film.layer_state, + below_component=film, + ) + return film + def test_serialization_round_trip(self): surface = CTRfilm.PoissonSurface(self.unitcell) - surface.basis = np.array([4.0, 1.0]) + surface.basis = np.array([4.0, 1.0, 0.25]) surface.basis_0 = np.copy(surface.basis) - surface.errors = np.array([0.2, 0.1]) + surface.errors = np.array([0.2, 0.1, 0.05]) restored = CTRfilm.PoissonSurface.fromStr(surface.toStr()) @@ -161,57 +176,157 @@ def test_serialization_round_trip(self): self.assertEqual(restored.unitcell.name, self.unitcell.name) self.assertEqual(restored.toStr(), surface.toStr()) - def test_create_layers_assigns_poisson_occupancies(self): - surface = CTRfilm.PoissonSurface(self.unitcell) - surface.basis = np.array([4.0, 1.0]) + def test_create_layers_assigns_convolved_occupancies(self): + surface = CTRfilm.PoissonSurface( + self.unitcell, + profile=PoissonProfile(mean_change=2.0, alpha=0.5), + ) - surface.set_below(0.0, self.unitcell.a[2]) + self.bind_surface(surface) - expected = 0.75 * np.array( + lower, upper = surface.profile.support() + offsets = np.arange(lower, upper + 1) + material = surface.profile.occupancy(offsets) + top_stop = np.flatnonzero( + material > surface.profile.tail_probability + )[-1] + 1 + offsets = offsets[:top_stop] + material = material[:top_stop] + expected_surface = 0.75 * surface.profile.surface_occupancy(offsets) + expected_film = 0.75 * (material - (offsets < 0)) + expected_reference = -expected_surface + represented = (expected_surface > surface.profile.tail_probability) | ( + np.abs(expected_film) > surface.profile.tail_probability + ) + actual_surface = np.concatenate( + [uc.coherentDomainOccupancy for uc in surface.layer_ucs] + ) + actual_film = np.concatenate( + [uc.coherentDomainOccupancy for uc in surface.film_layer_ucs] + ) + actual_reference = np.concatenate( [ - 1.0, - 1.0 - np.exp(-1.0), - 1.0 - 2.0 * np.exp(-1.0), + uc.coherentDomainOccupancy + for uc in surface._film_termination_ucs.values() ] ) np.testing.assert_allclose( - surface.layer_ucs[0].coherentDomainOccupancy, - expected[[0, 2]], + np.sort(actual_surface), np.sort(expected_surface[represented]) ) np.testing.assert_allclose( - surface.layer_ucs[1].coherentDomainOccupancy, - expected[[1]], + np.sort(actual_film), np.sort(expected_film[represented]) ) - self.assertEqual( - [len(uc.coherentDomainMatrix) for uc in surface.layer_ucs], - [2, 1], + np.testing.assert_allclose( + np.sort(actual_reference), np.sort(expected_reference[represented]) ) def test_profile_support_and_serialization_are_numerical_only(self): profile = PoissonProfile( mean_change=1.0, - offset=-1.0, + alpha=0.25, + offset=0.25, tail_probability=1e-8, ) surface = CTRfilm.PoissonSurface(self.unitcell, profile=profile) surface.basis_0[:] = surface.basis - surface.set_below(7.0, 11.0) + self.bind_surface(surface, loc=7.0, height=11.0) self.assertEqual(surface.pos_absolute, 11.0) - self.assertEqual(surface.stacking_height_absolute, 11.0) - self.assertGreater(surface.height_absolute, 11.0) + self.assertEqual(surface.stacking_height_absolute, 13.5) + self.assertGreater(surface.height_absolute, 13.5) self.assertLessEqual( surface.layer_ucs[-1].coherentDomainOccupancy[-1], self.unitcell.coherentDomainOccupancy[0], ) restored = CTRfilm.PoissonSurface.fromStr(surface.toStr()) - self.assertFalse(restored._legacy_absolute_width) self.assertAlmostEqual(restored.profile.mean_change, 1.0) - self.assertAlmostEqual(restored.profile.offset, -1.0) + self.assertAlmostEqual(restored.profile.alpha, 0.25) + self.assertAlmostEqual(restored.profile.offset, 0.25) self.assertAlmostEqual(restored.profile.tail_probability, 1e-8) self.assertEqual(restored.toStr(), surface.toStr()) + def test_supercell_termination_bank_selects_complete_relaxed_slabs(self): + slab = self.unitcell.supercell((1, 1, 2), symmetry="independent") + termination_1 = slab.affine_layer_transform([0, 0, 0]).as_surface_termination( + 1, + name="surface_termination_1", + ) + termination_0 = slab.affine_layer_transform([0, 0, 1]).as_surface_termination( + 0, + name="surface_termination_0", + ) + top_atom = np.argmax(termination_0.basis[:, 3]) + termination_0.basis[top_atom, 3] += 0.025 + termination_0.basis_0[top_atom, 3] += 0.025 + + surface = CTRfilm.PoissonSurface( + {0: termination_0, 1: termination_1}, + profile=PoissonProfile(1.0, alpha=1.0), + name="surface", + ) + serialized = surface.toStr() + restored = CTRfilm.PoissonSurface.fromStr(serialized) + self.assertEqual(set(restored.termination_cells), {0.0, 1.0}) + self.assertIn("TerminationUnitCell 0", restored.toStr()) + + self.bind_surface(surface) + self.assertEqual( + {cell.basis.shape[0] for cell in surface.termination_cells.values()}, + {4}, + ) + for layer, cell in surface.termination_cells.items(): + np.testing.assert_array_equal(cell.basis[:, 7], layer) + self.assertEqual(cell.layer_behavior, "select") + self.assertTrue( + all( + cell.coherentDomainMatrix + for cell in surface.layer_ucs + ) + ) + self.assertEqual(surface.toStr(), serialized) + + def test_surface_termination_helper_accepts_film_cycle_owner(self): + film = CTRfilm.Film(copy.deepcopy(self.unitcell)) + slab = self.unitcell.supercell((1, 1, 2), symmetry="independent") + + terminations = generate_surface_termination_cells( + slab, + film, + name_template="relaxed_{layer:g}", + ) + + self.assertEqual(set(terminations), {0.0, 1.0}) + self.assertEqual( + [terminations[layer].name for layer in (0.0, 1.0)], + ["relaxed_0", "relaxed_1"], + ) + for layer, cell in terminations.items(): + self.assertEqual(cell.basis.shape[0], 4) + np.testing.assert_array_equal(cell.basis[:, 7], layer) + self.assertEqual(cell.layer_behavior, "select") + self.assertEqual(tuple(cell.layer_cycle.layers), (layer,)) + + with self.assertRaisesRegex(ValueError, "integer multiple"): + generate_surface_termination_cells(slab, (0, 1, 2)) + + def test_termination_bank_requires_one_cell_per_film_cycle(self): + termination = self.unitcell.as_surface_termination(0) + surface = CTRfilm.PoissonSurface( + {0: termination}, + profile=PoissonProfile(0.0), + ) + film = CTRfilm.Film(copy.deepcopy(self.unitcell)) + film.basis[0] = 2.0 + crystal = CTRcalc.SXRDCrystal( + self.unitcell, + film, + surface, + stacking=np.array([1, 2]), + ) + with self.assertRaisesRegex(ValueError, "exactly one surface unit cell"): + crystal.apply_stacking() + def test_distribution_support_bounds_omitted_tail(self): poisson_profile = PoissonProfile(2.5, tail_probability=1e-9) _, upper = poisson_profile.support() @@ -222,32 +337,92 @@ def test_distribution_support_bounds_omitted_tail(self): self.assertLess(lower, 0) self.assertGreater(upper, 0) - def test_signed_poisson_process_changes_mean_surface_height(self): - for width, offset in ( - (2.0, 0.0), - (-2.0, 0.0), - (2.0, -2.0), - (-2.0, 2.0), + def test_step_poisson_convolution_preserves_signed_mean(self): + for width in (2.3, -2.3): + for alpha in (0.0, 0.25, 1.0): + offset = 0.4 + profile = PoissonProfile( + mean_change=width, + alpha=alpha, + offset=offset, + tail_probability=1e-12, + ) + lower, upper = profile.support() + layers = np.arange(lower, upper + 1) + self.assertAlmostEqual( + np.sum(profile.correction(layers)), + width + offset, + places=10, + ) + + def test_surface_occupancy_is_neighbor_difference(self): + for profile in ( + PoissonProfile(2.5, alpha=0.4, tail_probability=1e-12), + PoissonProfile(-2.5, alpha=0.4, tail_probability=1e-12), ): - profile = PoissonProfile( - mean_change=width, - offset=offset, - tail_probability=1e-12, - ) lower, upper = profile.support() - layers = np.arange(lower, upper + 1) - self.assertAlmostEqual( - np.sum(profile.correction(layers)), - width + offset, - places=10, + offsets = np.arange(lower, upper + 1) + material = profile.occupancy(offsets) + exposed = profile.surface_occupancy(offsets) + + np.testing.assert_allclose(exposed[:-1], material[:-1] - material[1:]) + self.assertEqual(exposed[-1], material[-1]) + self.assertTrue(np.all(exposed >= 0.0)) + self.assertAlmostEqual(np.sum(exposed), material[0]) + + def test_surface_profile_rejects_nonmonotonic_occupancy(self): + class InvalidProfile(SurfaceProfile): + def support(self): + return 0, 2 + + def occupancy(self, offsets): + return np.array([0.5, 0.6, 0.1]) + + with self.assertRaisesRegex(ValueError, "must not increase"): + InvalidProfile().surface_occupancy(np.arange(3)) + + def test_probability_is_step_poisson_convolution(self): + profile = PoissonProfile(mean_change=2.0, alpha=0.25) + changes = np.arange(0, 8) + poisson_probability = np.exp(-0.5) * 0.5 ** np.arange(7) / np.array( + [1, 1, 2, 6, 24, 120, 720] + ) + expected = np.zeros_like(changes, dtype=np.float64) + expected[1:] += 0.5 * poisson_probability + expected[2:] += 0.5 * poisson_probability[:-1] + np.testing.assert_allclose(profile.probability(changes), expected) + + dissolution = PoissonProfile(mean_change=-2.0, alpha=0.25) + np.testing.assert_allclose( + dissolution.probability(-changes), + expected, + ) + + def test_alpha_selects_layer_by_layer_and_poisson_limits(self): + layer_by_layer = PoissonProfile(mean_change=-1.25, alpha=0.0) + np.testing.assert_allclose( + layer_by_layer.probability([-1, -2]), + [0.75, 0.25], + ) + + poisson_only = PoissonProfile(mean_change=1.25, alpha=1.0) + np.testing.assert_allclose( + poisson_only.probability([0, 1, 2]), + np.exp(-1.25) * np.array([1.0, 1.25, 1.25**2 / 2.0]), + ) + + with self.assertRaisesRegex(ValueError, "alpha"): + PoissonProfile( + mean_change=1.0, + alpha=1.01, ) def test_poisson_surface_growth_and_etching_are_signed(self): growth = CTRfilm.PoissonSurface( self.unitcell, - profile=PoissonProfile(mean_change=1.0), + profile=PoissonProfile(mean_change=1.0, alpha=0.4), ) - growth.set_below(0.0, 8.0) + self.bind_surface(growth, height=8.0) growth_occupancy = np.concatenate( [layer.coherentDomainOccupancy for layer in growth.layer_ucs] ) @@ -256,39 +431,51 @@ def test_poisson_surface_growth_and_etching_are_signed(self): etching = CTRfilm.PoissonSurface( self.unitcell, - profile=PoissonProfile(mean_change=-1.0), + profile=PoissonProfile(mean_change=-1.0, alpha=0.4), ) - etching.set_below(0.0, 8.0) - etching_occupancy = np.concatenate( + self.bind_surface(etching, height=8.0) + etching_surface_occupancy = np.concatenate( [layer.coherentDomainOccupancy for layer in etching.layer_ucs] ) - self.assertTrue(np.all(etching_occupancy < 0)) + etching_film_occupancy = np.concatenate( + [layer.coherentDomainOccupancy for layer in etching.film_layer_ucs] + ) + self.assertTrue(np.all(etching_surface_occupancy > 0)) + self.assertTrue(np.all(etching_film_occupancy < 0)) self.assertAlmostEqual(etching.mean_height_absolute, 6.0) - compensated = CTRfilm.PoissonSurface( + def test_offset_is_an_independent_fit_parameter(self): + surface = CTRfilm.PoissonSurface( self.unitcell, - profile=PoissonProfile(mean_change=1.0, offset=-1.0), + profile=PoissonProfile(mean_change=1.0, alpha=0.4, offset=0.25), ) - compensated.set_below(0.0, 8.0) - compensated_occupancy = np.concatenate( - [layer.coherentDomainOccupancy for layer in compensated.layer_ucs] - ) - self.assertTrue(np.any(compensated_occupancy < 0)) - self.assertTrue(np.any(compensated_occupancy > 0)) - self.assertAlmostEqual(compensated.mean_height_absolute, 8.0) + surface.basis_0[:] = surface.basis + surface.addFitParameter("offset", limits=(-2.0, 2.0)) + + np.testing.assert_allclose(surface.getStartParamAndLimits()[0], [0.25]) + surface.setFitParameters([-0.5]) - def test_zero_width_poisson_surface_stacks_without_domains(self): + self.assertAlmostEqual(surface.profile.mean_change, 1.0) + self.assertAlmostEqual(surface.profile.alpha, 0.4) + self.assertAlmostEqual(surface.profile.offset, -0.5) + self.assertAlmostEqual(surface.profile.expected_height_change, 0.5) + + def test_zero_width_identical_surface_is_zero_net_correction(self): + film = CTRfilm.Film(copy.deepcopy(self.unitcell), name="film") + film.basis[0] = 2.0 surface = CTRfilm.PoissonSurface( - self.unitcell, + copy.deepcopy(self.unitcell), profile=PoissonProfile(mean_change=0.0), ) - crystal = CTRcalc.SXRDCrystal(self.unitcell, surface, stacking=np.array([1])) + crystal = CTRcalc.SXRDCrystal( + self.unitcell, film, surface, stacking=np.array([1, 2]) + ) crystal.apply_stacking() - self.assertEqual(surface.stacking_height_absolute, 0.0) + self.assertEqual(surface.stacking_height_absolute, film.height_absolute) self.assertTrue( - all(not layer.coherentDomainMatrix for layer in surface.layer_ucs) + any(layer.coherentDomainMatrix for layer in surface.layer_ucs) ) h = np.zeros(3) ell = np.linspace(0.1, 0.3, 3) @@ -301,26 +488,98 @@ def test_zero_width_poisson_surface_stacks_without_domains(self): surface.zDensity_G(np.linspace(-1.0, 1.0, 5), 0, 0), 0.0, ) + np.testing.assert_allclose(surface.optical_profile()[:, 1:], 0.0) - def test_delta_width_serialization_uses_legacy_absolute_width(self): - transitional = CTRfilm.PoissonSurface( - self.unitcell, - profile=PoissonProfile(mean_change=1.0), - ).toStr() - transitional = transitional.replace( - "W/layers offset/layers", - "Width/layers deltaW/layers", - ).replace( - "1.00000 0.00000", - "0.00000 1.00000", + def test_zero_width_replaces_top_film_layer_with_surface_structure(self): + film_cell = copy.deepcopy(self.unitcell) + surface_cell = copy.deepcopy(self.unitcell) + surface_cell.names[:] = ["O", "O"] + surface_cell.basis[:, 0] = 8 + surface_cell.setEnergy(10000.0) + film = CTRfilm.Film(film_cell, name="film") + film.basis[0] = 2.0 + surface = CTRfilm.PoissonSurface( + surface_cell, + profile=PoissonProfile(mean_change=0.0), + name="surface", + ) + crystal = CTRcalc.SXRDCrystal( + self.unitcell, film, surface, stacking=np.array([1, 2]) ) + crystal.apply_stacking() - restored = CTRfilm.PoissonSurface.fromStr(transitional) + surface_occupancies = np.concatenate( + [layer.coherentDomainOccupancy for layer in surface.layer_ucs] + ) + film_corrections = np.concatenate( + [layer.coherentDomainOccupancy for layer in surface.film_layer_ucs] + ) + reference_corrections = np.concatenate( + [ + cell.coherentDomainOccupancy + for cell in surface._film_termination_ucs.values() + ] + ) + np.testing.assert_allclose(surface_occupancies, [0.75]) + np.testing.assert_allclose(film_corrections, [0.0]) + np.testing.assert_allclose(reference_corrections, [-0.75]) + h = np.zeros(3) + ell = np.linspace(0.1, 0.3, 3) + self.assertTrue(np.any(np.abs(surface.F_uc(h, h, ell)) > 0.0)) - np.testing.assert_allclose(restored.basis, [0.0, 1.0]) - self.assertAlmostEqual(restored.profile.expected_height_change, 1.0) - self.assertIn("Width/layers deltaW/layers", restored.toStr()) + def test_surface_requires_compatible_underlying_film(self): + surface = CTRfilm.PoissonSurface( + copy.deepcopy(self.unitcell), profile=PoissonProfile(1.0) + ) + crystal = CTRcalc.SXRDCrystal( + self.unitcell, surface, stacking=np.array([1]) + ) + with self.assertRaisesRegex(ValueError, "immediately above a Film"): + crystal.apply_stacking() + + mismatched = copy.deepcopy(self.unitcell) + mismatched.a[2] *= 1.1 + film = CTRfilm.Film(mismatched) + film.basis[0] = 2.0 + crystal = CTRcalc.SXRDCrystal( + self.unitcell, film, surface, stacking=np.array([1, 2]) + ) + with self.assertRaisesRegex(ValueError, "integer multiple"): + crystal.apply_stacking() + + def test_repeated_stacking_refreshes_underlying_film_views(self): + film = CTRfilm.Film(copy.deepcopy(self.unitcell), name="film") + film.basis[0] = 2.0 + surface = CTRfilm.PoissonSurface( + copy.deepcopy(self.unitcell), + profile=PoissonProfile(1.0, alpha=0.5), + name="surface", + ) + crystal = CTRcalc.SXRDCrystal( + self.unitcell, film, surface, stacking=np.array([1, 2]) + ) + crystal.apply_stacking() + first_counts = [ + len(layer.coherentDomainMatrix) for layer in surface.film_layer_ucs + ] + film.unitcell.basis[:, 6] *= 0.5 + crystal.apply_stacking() + + self.assertEqual( + [len(layer.coherentDomainMatrix) for layer in surface.film_layer_ucs], + first_counts, + ) + self.assertTrue( + all( + np.shares_memory(layer.basis, film.unitcell.basis) + for layer in surface.film_layer_ucs + ) + ) + np.testing.assert_allclose( + np.concatenate([layer.basis[:, 6] for layer in surface.film_layer_ucs]), + 0.5, + ) class TestLayerStacking(unittest.TestCase): @staticmethod @@ -1097,7 +1356,8 @@ def test_film_and_poisson_rotate_cyclic_layer_order(self): self.assertAlmostEqual(film.height_absolute, 4.0) surface = CTRfilm.PoissonSurface(self.make_layered_unitcell("surface")) - surface.basis[:] = [2, 0] + surface.basis[:] = [2, 0, 0] + surface._bind_underlying_component(film) surface.start_layer_number = 2 surface.createLayers() @@ -1110,7 +1370,7 @@ def test_film_and_poisson_rotate_cyclic_layer_order(self): ) self.assertAlmostEqual( surface.layer_ucs[1].coherentDomainMatrix[0][2, 3], - 1 / 3, + -1 / 3, ) def test_epitaxy_interface_rotates_cyclic_layer_order(self): @@ -1145,7 +1405,7 @@ def test_sxrdcrystal_passes_top_layer_to_object_above(self): surface = CTRfilm.PoissonSurface(self.make_layered_unitcell("surface")) surface.name = "surface" - surface.basis[:] = [5, 0] + surface.basis[:] = [5, 0, 0] crystal = CTRcalc.SXRDCrystal( self.make_layered_unitcell("bulk"), @@ -1535,14 +1795,14 @@ def make_crystal_with_errors(self): film.basis_0[:] = film.basis surface = CTRfilm.PoissonSurface( self.make_layered_cell("surface_cell"), - profile=PoissonProfile(mean_change=1.5, offset=-1.5), + profile=PoissonProfile(mean_change=1.5, alpha=0.5), name="surface", ) surface.basis_0[:] = surface.basis film.errors = np.array([0.2]) interface.errors = np.array([0.03, 0.04]) - surface.errors = np.array([0.1, 0.15]) + surface.errors = np.array([0.1, 0.15, 0.2]) film.unitcell.errors = np.full_like(film.unitcell.basis, np.nan) film.unitcell.errors[:, 1:7] = 0.01 @@ -1582,7 +1842,10 @@ def test_xtal_and_xpr_plain_text_round_trip(self): np.testing.assert_allclose(restored_xpr.werrors, crystal.werrors) np.testing.assert_allclose(restored_xpr["film"].errors, [0.2]) np.testing.assert_allclose(restored_xpr["interface"].errors, [0.03, 0.04]) - np.testing.assert_allclose(restored_xpr["surface"].errors, [0.1, 0.15]) + np.testing.assert_allclose( + restored_xpr["surface"].errors, + [0.1, 0.15, 0.2], + ) np.testing.assert_allclose( restored_xpr["film"].unitcell.errors[:, 1:7], crystal["film"].unitcell.errors[:, 1:7], @@ -1616,42 +1879,43 @@ def test_ctrfilm_fit_parameters_and_errors(self): surface = CTRfilm.PoissonSurface( self.make_layered_cell("surface_cell"), - profile=PoissonProfile(mean_change=2.0, offset=-2.0), + profile=PoissonProfile(mean_change=2.0, alpha=0.4), name="surface", ) surface.basis_0[:] = surface.basis surface.addRelParameter( - ("W", "offset"), - (1.0, -1.0), - limits=(-1.0, 1.0), + ("W", "alpha"), + (1.0, 0.1), + limits=(-0.4, 0.4), ) - surface.setFitParameters([0.5]) + surface.setFitParameters([0.25]) surface.setFitErrors([0.1]) - np.testing.assert_allclose(surface.basis, [2.5, -2.5]) - np.testing.assert_allclose(surface.errors, [0.1, 0.1]) - self.assertAlmostEqual(surface.profile.mean_change, 2.5) - self.assertAlmostEqual(surface.profile.offset, -2.5) - self.assertAlmostEqual(surface.profile.expected_height_change, 0.0) + np.testing.assert_allclose(surface.basis, [2.25, 0.425, 0.0]) + np.testing.assert_allclose(surface.errors[:2], [0.1, 0.01]) + self.assertTrue(np.isnan(surface.errors[2])) + self.assertAlmostEqual(surface.profile.mean_change, 2.25) + self.assertAlmostEqual(surface.profile.alpha, 0.425) + self.assertAlmostEqual(surface.profile.expected_height_change, 2.25) def test_linear_fit_parameter_dictionary_round_trip(self): original = CTRfilm.PoissonSurface( self.make_layered_cell("surface_cell"), - profile=PoissonProfile(mean_change=2.0, offset=-2.0), + profile=PoissonProfile(mean_change=2.0, alpha=0.4), name="surface", ) original.basis_0[:] = original.basis original.addRelParameter( - ("W", "offset"), - (1.0, -1.0), - limits=(-1.0, 1.0), - name="mean_preserving_width", + ("W", "alpha"), + (1.0, 0.1), + limits=(-0.4, 0.4), + name="coupled_roughness", ) original.setFitParameters([0.4]) original.setFitErrors([0.05]) restored = CTRfilm.PoissonSurface( self.make_layered_cell("surface_cell"), - profile=PoissonProfile(mean_change=2.0, offset=-2.0), + profile=PoissonProfile(mean_change=2.0, alpha=0.4), name="surface", ) restored.parametersFromDict(original.parametersToDict()) @@ -1661,7 +1925,7 @@ def test_linear_fit_parameter_dictionary_round_trip(self): np.testing.assert_allclose(restored.getFitErrors(), original.getFitErrors()) self.assertEqual(restored.fitparnames, original.fitparnames) self.assertAlmostEqual(restored.profile.mean_change, 2.4) - self.assertAlmostEqual(restored.profile.offset, -2.4) + self.assertAlmostEqual(restored.profile.alpha, 0.44) def test_crystal_fit_vector_updates_ctrfilm_components(self): crystal = self.make_crystal_with_errors() @@ -1678,7 +1942,7 @@ def test_crystal_fit_vector_updates_ctrfilm_components(self): np.testing.assert_allclose(crystal["interface"].basis, [0.4, 0.0]) np.testing.assert_allclose(crystal["film"].basis, [7.0]) - np.testing.assert_allclose(crystal["surface"].basis, [2.0, -1.5]) + np.testing.assert_allclose(crystal["surface"].basis, [2.0, 0.5, 0.0]) np.testing.assert_allclose(crystal.getFitErrors(), [0.01, 0.2, 0.1]) self.assertAlmostEqual(crystal["interface"].profile.width, 0.4) self.assertAlmostEqual(crystal["surface"].profile.mean_change, 2.0) @@ -1713,7 +1977,7 @@ def test_example_xtal_and_xpr_describe_same_poisson_stack(self): np.testing.assert_array_equal(xpr.uc_stacking, [3, 2, 1]) np.testing.assert_allclose(xtal["TiO2toRuO2"].basis, [0.35, 0.0]) np.testing.assert_allclose(xtal["RuO2"].basis, [17.0]) - np.testing.assert_allclose(xtal["RuO2surface"].basis, [-6.0, 0.0]) + np.testing.assert_allclose(xtal["RuO2surface"].basis, [-6.0, 1.0, 0.0]) for name in ("TiO2toRuO2", "RuO2", "RuO2surface"): np.testing.assert_allclose(xtal[name].basis, xpr[name].basis) From eef423a3d378d694836a760dd718944dde5173b0 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Thu, 23 Jul 2026 10:43:37 -0500 Subject: [PATCH 08/37] feat: add simple resolution correction --- CHANGELOG.md | 10 + doc/source/api.rst | 1 + doc/source/ctr_resolution.rst | 128 ++++++ doc/source/release_notes.rst | 7 + orgui/datautils/xrayutils/CTRplotutil.py | 58 ++- orgui/datautils/xrayutils/CTRresolution.py | 387 ++++++++++++++++++ orgui/datautils/xrayutils/__init__.py | 1 + .../xrayutils/test/test_CTRresolution.py | 274 +++++++++++++ .../xrayutils/unitcells/TiO2(100).xtal | 21 + 9 files changed, 886 insertions(+), 1 deletion(-) create mode 100644 doc/source/ctr_resolution.rst create mode 100644 orgui/datautils/xrayutils/CTRresolution.py create mode 100644 orgui/datautils/xrayutils/test/test_CTRresolution.py create mode 100644 orgui/datautils/xrayutils/unitcells/TiO2(100).xtal diff --git a/CHANGELOG.md b/CHANGELOG.md index b64bfa1..b5af9c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ This is the changelog for the software orGUI, written by Timo Fuchs +## [Unreleased] (2026-07-19) + +Scientific and analysis additions: + +- Added optional CTR intensity-resolution modeling with constant or + gamma-dependent box and Gaussian functions. Calculations can convolve + irregular existing L points or sample the crystal structure factor with + deterministic quadrature. + + ## [1.5.0] (2026-06-07) [532b60b](https://github.com/tifuchs/orGUI/commit/532b60bab3073ae9f0ff063a0e119aa9e9957857)...[c574bdf](https://github.com/tifuchs/orGUI/commit/c574bdf0bd5af6fa8258d108e7355c579bfef857) diff --git a/doc/source/api.rst b/doc/source/api.rst index ff93af9..124716f 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -5,6 +5,7 @@ API :maxdepth: 1 ctr_structure_factors + ctr_resolution wyckoff_fitting GUI API diff --git a/doc/source/ctr_resolution.rst b/doc/source/ctr_resolution.rst new file mode 100644 index 0000000..d9b5f8a --- /dev/null +++ b/doc/source/ctr_resolution.rst @@ -0,0 +1,128 @@ +CTR Resolution Modeling +======================= + +``CTRresolution`` supplies optional instrumental-resolution broadening for +calculated crystal truncation rods. It does not change ``SXRDCrystal.F`` or +enable broadening implicitly in fitting code. + +Intensity and output convention +------------------------------- + +Resolution acts on calculated intensity rather than on the complex amplitude: + +.. math:: + + I_{\mathrm{res}}(L_i) = + \frac{\int R_i(L-L_i)\lvert F(H_i,K_i,L)\rvert^2\,\mathrm{d}L} + {\int R_i(L-L_i)\,\mathrm{d}L}. + +The returned ``CTRCollection`` follows the existing ``sfI`` convention and +stores the effective amplitude + +.. math:: + + F_{\mathrm{eff}}(L_i) = \sqrt{I_{\mathrm{res}}(L_i)}. + +Phase and experimental-error information cannot be retained by this operation +and is therefore absent from the result. Input collections are never modified. + +Resolution functions +-------------------- + +The built-in functions are ``BoxResolution`` and ``GaussianResolution``. Their +effective width is in reciprocal lattice units: + +.. math:: + + \Delta L = \Delta L_0 + \Delta L_1\lvert\sin(\gamma)\rvert. + +Here ``gamma`` is the Vlieg out-of-plane detector angle in radians. For a box, +``DeltaL`` is the complete support width. For a Gaussian it is the full width +at half maximum (FWHM). Setting ``delta_l_1=0`` creates a constant function; +setting both contributions to zero disables broadening. + +Gamma-dependent models require angle records on their input collection. They +can be calculated for all rods at once: + +.. code-block:: python + + ub = HKLVlieg.UBCalculator(crystal.reference_uc, energy=70.0) + ub.defaultU_GID() + angle_calculator = HKLVlieg.VliegAngles(ub) + ctrs.calcAnglesZmode( + angle_calculator, + fixedangle=np.deg2rad(0.1), + fixed="in", + ) + +Fast convolution on existing points +----------------------------------- + +``fast_convolve`` uses only the L coordinates and amplitudes already present +in each rod. It does not interpolate and does not require an equidistant grid. +Local composite-trapezoidal weights account for unequal point spacing, so a +dense part of a scan is not overrepresented merely because it has more points. + +At the first and last points the kernel is truncated to the available data and +renormalized. This preserves constant intensity without assuming either zero +intensity or an extrapolation outside the measured range. L coordinates must +be finite and unique within a rod, but they may be in any order. + +.. code-block:: python + + from orgui.datautils.xrayutils import CTRresolution + + model = CTRresolution.GaussianResolution( + delta_l_0=0.015, + delta_l_1=0.08, + ) + broadened = CTRresolution.fast_convolve(calculated_ctrs, model) + +The fast method approximates the convolution represented by the existing +sampling. If the kernel is narrower than the local point spacing, it may only +contain the central point and consequently cannot reproduce unresolved detail. + +Sampled structure factors +------------------------- + +``sample_structure_factor`` reevaluates ``crystal.F`` around every requested +point and is slower but does not depend on the density of the input L samples. +It uses Gauss-Legendre quadrature for a box and Gauss-Hermite quadrature for an +unbounded Gaussian. The quadrature order must be a positive odd integer and +defaults to 25. + +.. code-block:: python + + sampled = CTRresolution.sample_structure_factor( + data_positions, + crystal, + model, + quadrature_order=25, + ) + +The central point's gamma determines the width of all quadrature samples for +that point. H and K remain fixed. This first implementation therefore models +only out-of-plane L resolution; a future in-plane model can use the H, K, L, +and complete angle context accepted by ``ResolutionFunction.width``. + +A complete runnable comparison using the bundled CTR reference data is in +``examples/CTR/ctr_resolution_example.ipynb``. + +API reference +------------- + +.. autoclass:: orgui.datautils.xrayutils.CTRresolution.ResolutionFunction + :members: width, weights, quadrature + :member-order: bysource + +.. autoclass:: orgui.datautils.xrayutils.CTRresolution.BoxResolution + :members: + :member-order: bysource + +.. autoclass:: orgui.datautils.xrayutils.CTRresolution.GaussianResolution + :members: + :member-order: bysource + +.. autofunction:: orgui.datautils.xrayutils.CTRresolution.fast_convolve + +.. autofunction:: orgui.datautils.xrayutils.CTRresolution.sample_structure_factor diff --git a/doc/source/release_notes.rst b/doc/source/release_notes.rst index dec89b3..498dd3d 100644 --- a/doc/source/release_notes.rst +++ b/doc/source/release_notes.rst @@ -4,6 +4,13 @@ Release Notes .. This file is generated by doc/generate_release_notes.py from CHANGELOG.md. Do not edit it by hand. +Unreleased (2026-07-19) +----------------------- + +Scientific and analysis additions: + +- Added optional CTR intensity-resolution modeling with constant or gamma-dependent box and Gaussian functions. Calculations can convolve irregular existing L points or sample the crystal structure factor with deterministic quadrature. + 1.5.0 (2026-06-07) ------------------ diff --git a/orgui/datautils/xrayutils/CTRplotutil.py b/orgui/datautils/xrayutils/CTRplotutil.py index 03d6265..e640618 100644 --- a/orgui/datautils/xrayutils/CTRplotutil.py +++ b/orgui/datautils/xrayutils/CTRplotutil.py @@ -580,13 +580,30 @@ def calcAnglesZmode( phi=0.0, **keyargs, ): + """Calculate and store Vlieg z-mode angles in radians. + + :param HKLVlieg.VliegAngles vliegangles: + Angle calculator configured for the CTR reference lattice. + :param float fixedangle: + Fixed incidence or exit angle in rad. + :param str fixed: + Angle constraint: ``"in"``, ``"out"``, or ``"eq"``. + :param float chi: + Fixed chi angle in rad. + :param float phi: + Fixed phi angle in rad. + :returns: + Structured records containing alpha, delta, gamma, omega, chi, + and phi in rad. + :rtype: numpy.recarray + """ l = self.l # noqa: E741 h = self.harr k = self.karr hkl = np.vstack((h, k, l)) pos = vliegangles.anglesZmode( - hkl, fixedangle, fixed="in", chi=0, phi=0, **keyargs + hkl, fixedangle, fixed=fixed, chi=chi, phi=phi, **keyargs ) dt = np.dtype( [ @@ -806,6 +823,45 @@ def convertToF(self): for ctr in self: ctr.convertToF() + def calcAnglesZmode( + self, + vliegangles, + fixedangle=np.deg2rad(0.1), + fixed="in", + chi=0.0, + phi=0.0, + **keyargs, + ): + """Calculate and store Vlieg z-mode angles for every CTR. + + All angle arguments and returned angle records are in rad. + + :param HKLVlieg.VliegAngles vliegangles: + Angle calculator configured for the CTR reference lattice. + :param float fixedangle: + Fixed incidence or exit angle in rad. + :param str fixed: + Angle constraint: ``"in"``, ``"out"``, or ``"eq"``. + :param float chi: + Fixed chi angle in rad. + :param float phi: + Fixed phi angle in rad. + :returns: + One structured angle-record array per CTR. + :rtype: list[numpy.recarray] + """ + return [ + ctr.calcAnglesZmode( + vliegangles, + fixedangle=fixedangle, + fixed=fixed, + chi=chi, + phi=phi, + **keyargs, + ) + for ctr in self + ] + def generateAverage(self, step_size=None, **kwargs): """Creates a new CTRCollection with CTRs, which were individually averaged. diff --git a/orgui/datautils/xrayutils/CTRresolution.py b/orgui/datautils/xrayutils/CTRresolution.py new file mode 100644 index 0000000..0293ebd --- /dev/null +++ b/orgui/datautils/xrayutils/CTRresolution.py @@ -0,0 +1,387 @@ +# /*########################################################################## +# +# Copyright (c) 2026 Timo Fuchs +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Optional one-dimensional resolution modeling for calculated CTRs. + +Resolution functions in this module act on intensity, ``abs(F)**2``, along +the CTR L direction. Results are converted back to effective amplitudes so +they remain compatible with :class:`~.CTRplotutil.CTRCollection`. +""" + +from abc import ABC, abstractmethod +import copy +from dataclasses import dataclass + +import numpy as np +from numpy.polynomial.hermite import hermgauss +from numpy.polynomial.legendre import leggauss + +from .CTRplotutil import CTR, CTRCollection + + +@dataclass(frozen=True) +class ResolutionFunction(ABC): + """Base interface for an L-direction CTR resolution function. + + The effective full width is + + ``delta_l_0 + delta_l_1 * abs(sin(gamma))``. + + The full central HKL coordinates and angle record are accepted by + :meth:`width` so future resolution models can depend on more than gamma. + + :param float delta_l_0: + Constant L-width contribution in r.l.u. + :param float delta_l_1: + Gamma-dependent L-width contribution in r.l.u. + """ + + delta_l_0: float + delta_l_1: float = 0.0 + + def __post_init__(self): + for name, value in ( + ("delta_l_0", self.delta_l_0), + ("delta_l_1", self.delta_l_1), + ): + if not np.isscalar(value) or not np.isfinite(value) or value < 0.0: + raise ValueError(f"{name} must be a finite, nonnegative scalar") + + def width(self, h, k, l, angles=None): # noqa: E741 + """Return the effective L width at the supplied CTR points. + + :param numpy.ndarray h: + H coordinates in r.l.u. + :param numpy.ndarray k: + K coordinates in r.l.u. + :param numpy.ndarray l: + L coordinates in r.l.u. + :param angles: + Optional structured angle array containing ``gamma`` in rad. + :returns: + Effective L widths in r.l.u., broadcast to the HKL shape. + :rtype: numpy.ndarray + :raises ValueError: + If HKL shapes are incompatible or gamma is required but missing. + """ + try: + h_array, k_array, l_array = np.broadcast_arrays( + np.asarray(h, dtype=np.float64), + np.asarray(k, dtype=np.float64), + np.asarray(l, dtype=np.float64), + ) + except ValueError as exc: + raise ValueError( + "H, K, and L coordinates must be broadcast-compatible" + ) from exc + + widths = np.full(l_array.shape, float(self.delta_l_0), dtype=np.float64) + if self.delta_l_1 == 0.0: + return widths + + if angles is None: + raise ValueError( + "Gamma-dependent resolution requires CTR angle records; " + "call CTRCollection.calcAnglesZmode first" + ) + try: + gamma = np.asarray(angles["gamma"], dtype=np.float64) + except (KeyError, TypeError, ValueError, IndexError) as exc: + raise ValueError( + "Gamma-dependent resolution requires an angle field named 'gamma'" + ) from exc + try: + gamma = np.broadcast_to(gamma, l_array.shape) + except ValueError as exc: + raise ValueError( + "Gamma angles must have the same shape as the CTR points" + ) from exc + if not np.all(np.isfinite(gamma)): + raise ValueError("Gamma angles must be finite and expressed in radians") + widths += self.delta_l_1 * np.abs(np.sin(gamma)) + return widths + + @abstractmethod + def weights(self, offsets, width): + """Return unnormalized kernel weights for L offsets in r.l.u.""" + + @abstractmethod + def quadrature(self, width, order): + """Return normalized L quadrature offsets and weights. + + :param numpy.ndarray width: + Effective widths in r.l.u. + :param int order: + Quadrature order. + :returns: + Tuple of offsets shaped ``width.shape + (order,)`` and normalized + one-dimensional quadrature weights. + :rtype: tuple[numpy.ndarray, numpy.ndarray] + """ + + +class BoxResolution(ResolutionFunction): + """Box resolution whose ``DeltaL`` is the full support width.""" + + def weights(self, offsets, width): + """Return one inside ``[-DeltaL / 2, DeltaL / 2]`` and zero outside.""" + offsets = np.asarray(offsets, dtype=np.float64) + width = np.asarray(width, dtype=np.float64) + return (np.abs(offsets) <= width / 2.0).astype(np.float64) + + def quadrature(self, width, order): + """Return Gauss-Legendre quadrature for the normalized box.""" + nodes, weights = leggauss(order) + width = np.asarray(width, dtype=np.float64) + offsets = width[..., np.newaxis] * nodes / 2.0 + return offsets, weights / 2.0 + + +class GaussianResolution(ResolutionFunction): + """Gaussian resolution whose ``DeltaL`` is its FWHM.""" + + def weights(self, offsets, width): + """Return Gaussian weights with the configured full width at half maximum.""" + offsets, width = np.broadcast_arrays( + np.asarray(offsets, dtype=np.float64), + np.asarray(width, dtype=np.float64), + ) + result = np.zeros(offsets.shape, dtype=np.float64) + nonzero = width > 0.0 + result[nonzero] = np.exp( + -4.0 + * np.log(2.0) + * (offsets[nonzero] / width[nonzero]) ** 2 + ) + result[~nonzero] = offsets[~nonzero] == 0.0 + return result + + def quadrature(self, width, order): + """Return Gauss-Hermite quadrature for the normalized Gaussian.""" + nodes, weights = hermgauss(order) + width = np.asarray(width, dtype=np.float64) + sigma = width / (2.0 * np.sqrt(2.0 * np.log(2.0))) + offsets = np.sqrt(2.0) * sigma[..., np.newaxis] * nodes + return offsets, weights / np.sqrt(np.pi) + + +def _validate_ctr_arrays(ctr, require_amplitudes): + l_array = np.asarray(ctr.l, dtype=np.float64) + h_array = np.asarray(ctr.harr, dtype=np.float64) + k_array = np.asarray(ctr.karr, dtype=np.float64) + if ( + l_array.ndim != 1 + or h_array.shape != l_array.shape + or k_array.shape != l_array.shape + ): + raise ValueError( + f"{ctr!r} must contain one-dimensional, equal-length HKL arrays" + ) + if ( + not np.all(np.isfinite(h_array)) + or not np.all(np.isfinite(k_array)) + or not np.all(np.isfinite(l_array)) + ): + raise ValueError(f"{ctr!r} contains non-finite HKL coordinates") + if np.unique(l_array).size != l_array.size: + raise ValueError(f"{ctr!r} contains duplicate L coordinates") + + if not require_amplitudes: + return h_array, k_array, l_array, None + + if not np.isrealobj(ctr.sfI): + raise ValueError(f"{ctr!r} amplitudes must be real") + amplitude = np.asarray(ctr.sfI, dtype=np.float64) + if amplitude.shape != l_array.shape: + raise ValueError(f"{ctr!r} amplitudes must have the same shape as L") + if not np.all(np.isfinite(amplitude)) or np.any(amplitude < 0.0): + raise ValueError(f"{ctr!r} amplitudes must be finite and nonnegative") + return h_array, k_array, l_array, amplitude + + +def _validate_widths(widths, shape): + widths = np.asarray(widths, dtype=np.float64) + try: + widths = np.broadcast_to(widths, shape) + except ValueError as exc: + raise ValueError("Resolution widths must match the CTR point shape") from exc + if not np.all(np.isfinite(widths)) or np.any(widths < 0.0): + raise ValueError("Effective resolution widths must be finite and nonnegative") + return widths + + +def _new_collection(ctrs, amplitudes): + result = CTRCollection(name=ctrs.name) + result.plotsett = copy.deepcopy(ctrs.plotsett) + result.plotkeyargs = copy.deepcopy(ctrs.plotkeyargs) + for source, amplitude in zip(ctrs, amplitudes): + target = CTR( + source.hk, + np.copy(source.l), + np.ascontiguousarray(amplitude), + err=None, + phi=None, + name=source.name, + ) + target.harr = np.copy(source.harr) + target.karr = np.copy(source.karr) + target.weight = source.weight + if hasattr(source, "angles"): + target.angles = copy.deepcopy(source.angles) + result.append(target) + return result + + +def fast_convolve(ctrs, resolution): + """Convolve CTR intensity using only the existing irregular L points. + + No interpolation or equidistant resampling is performed. Local composite + trapezoidal weights account for unequal L spacing, and each kernel is + renormalized over the samples available at the rod boundaries. + + :param CTRCollection ctrs: + CTR amplitudes to convolve. Amplitudes must be finite and nonnegative. + :param ResolutionFunction resolution: + Box or Gaussian L-resolution function. + :returns: + A new collection containing effective amplitudes + ``sqrt(convolved(abs(F)**2))``. + :rtype: CTRCollection + """ + if not isinstance(ctrs, CTRCollection): + raise TypeError("ctrs must be a CTRCollection") + if not isinstance(resolution, ResolutionFunction): + raise TypeError("resolution must be a ResolutionFunction") + + convolved = [] + for ctr in ctrs: + h_array, k_array, l_array, amplitude = _validate_ctr_arrays( + ctr, require_amplitudes=True + ) + angles = getattr(ctr, "angles", None) + widths = _validate_widths( + resolution.width(h_array, k_array, l_array, angles), l_array.shape + ) + + if l_array.size < 2: + convolved.append(np.copy(amplitude)) + continue + + order = np.argsort(l_array) + l_sorted = l_array[order] + intensity = amplitude[order] ** 2 + widths_sorted = widths[order] + + integration_weights = np.empty_like(l_sorted) + integration_weights[0] = (l_sorted[1] - l_sorted[0]) / 2.0 + integration_weights[-1] = (l_sorted[-1] - l_sorted[-2]) / 2.0 + integration_weights[1:-1] = (l_sorted[2:] - l_sorted[:-2]) / 2.0 + + convolved_sorted = np.empty_like(intensity) + for index, (center, width) in enumerate(zip(l_sorted, widths_sorted)): + if width == 0.0: + convolved_sorted[index] = intensity[index] + continue + kernel_weights = resolution.weights(l_sorted - center, width) + combined_weights = kernel_weights * integration_weights + normalization = np.sum(combined_weights) + if not np.isfinite(normalization) or normalization <= 0.0: + raise ValueError(f"Resolution kernel has no support for {ctr!r}") + convolved_sorted[index] = ( + np.sum(combined_weights * intensity) / normalization + ) + + convolved_rod = np.empty_like(convolved_sorted) + convolved_rod[order] = np.sqrt(np.maximum(convolved_sorted, 0.0)) + convolved.append(convolved_rod) + + return _new_collection(ctrs, convolved) + + +def sample_structure_factor(ctrs, crystal, resolution, quadrature_order=25): + """Sample crystal intensity around every CTR point along L. + + H and K remain fixed. Box functions use Gauss-Legendre quadrature and + Gaussian functions use Gauss-Hermite quadrature. The effective width is + evaluated from each central point and its central angle record. + + :param CTRCollection ctrs: + Collection supplying the requested HKL points and optional angles. + :param crystal: + Crystal-like object providing ``F(h, k, l)``. + :param ResolutionFunction resolution: + Box or Gaussian L-resolution function. + :param int quadrature_order: + Positive odd number of deterministic quadrature points. Defaults to + 25. + :returns: + A new collection containing effective amplitudes + ``sqrt(integrated(abs(F)**2))``. + :rtype: CTRCollection + """ + if not isinstance(ctrs, CTRCollection): + raise TypeError("ctrs must be a CTRCollection") + if not isinstance(resolution, ResolutionFunction): + raise TypeError("resolution must be a ResolutionFunction") + if not hasattr(crystal, "F") or not callable(crystal.F): + raise TypeError("crystal must provide a callable F(h, k, l) method") + if ( + not isinstance(quadrature_order, int | np.integer) + or isinstance(quadrature_order, bool | np.bool_) + or quadrature_order <= 0 + or quadrature_order % 2 == 0 + ): + raise ValueError("quadrature_order must be a positive odd integer") + + sampled = [] + for ctr in ctrs: + h_array, k_array, l_array, _ = _validate_ctr_arrays( + ctr, require_amplitudes=False + ) + angles = getattr(ctr, "angles", None) + widths = _validate_widths( + resolution.width(h_array, k_array, l_array, angles), l_array.shape + ) + offsets, integration_weights = resolution.quadrature( + widths, int(quadrature_order) + ) + sample_shape = offsets.shape + h_samples = np.broadcast_to(h_array[..., np.newaxis], sample_shape) + k_samples = np.broadcast_to(k_array[..., np.newaxis], sample_shape) + l_samples = l_array[..., np.newaxis] + offsets + structure_factor = np.asarray( + crystal.F(h_samples.ravel(), k_samples.ravel(), l_samples.ravel()) + ) + try: + structure_factor = np.broadcast_to( + structure_factor, (l_samples.size,) + ).reshape(sample_shape) + except ValueError as exc: + raise ValueError("crystal.F returned an incompatible array shape") from exc + intensity = np.abs(structure_factor) ** 2 + if not np.all(np.isfinite(intensity)): + raise ValueError("crystal.F returned non-finite structure factors") + integrated = np.sum(intensity * integration_weights, axis=-1) + sampled.append(np.sqrt(np.maximum(integrated, 0.0))) + + return _new_collection(ctrs, sampled) diff --git a/orgui/datautils/xrayutils/__init__.py b/orgui/datautils/xrayutils/__init__.py index ec855a6..7ebc780 100644 --- a/orgui/datautils/xrayutils/__init__.py +++ b/orgui/datautils/xrayutils/__init__.py @@ -32,6 +32,7 @@ "CTRcalc", "CTRoptics", "CTRplotutil", + "CTRresolution", "CTRsymmetry", "DetectorCalibration", "HKLVlieg", diff --git a/orgui/datautils/xrayutils/test/test_CTRresolution.py b/orgui/datautils/xrayutils/test/test_CTRresolution.py new file mode 100644 index 0000000..b74c9d1 --- /dev/null +++ b/orgui/datautils/xrayutils/test/test_CTRresolution.py @@ -0,0 +1,274 @@ +"""Regression tests for optional CTR resolution modeling.""" + +import unittest + +import numpy as np + +from .. import CTRplotutil, CTRresolution + + +def _angles(gamma): + gamma = np.asarray(gamma, dtype=np.float64) + zeros = np.zeros_like(gamma) + return np.rec.fromarrays( + [zeros, zeros, gamma, zeros, zeros, zeros], + names="alpha,delta,gamma,omega,chi,phi", + ) + + +class QuadraticCrystal: + """Crystal-like test double with intensity equal to L squared.""" + + @staticmethod + def F(h, k, l): # noqa: N802,E741 + return np.asarray(l, dtype=np.complex128) + + +class TestResolutionFunctions(unittest.TestCase): + def test_constant_and_gamma_dependent_widths(self): + h = np.array([0.0, 1.0, 2.0]) + l = np.array([0.1, 0.2, 0.3]) # noqa: E741 + gamma = np.array([0.0, np.pi / 6.0, np.pi / 2.0]) + + constant = CTRresolution.BoxResolution(0.2) + varying = CTRresolution.GaussianResolution(0.2, 0.4) + + np.testing.assert_allclose(constant.width(h, h, l), 0.2) + np.testing.assert_allclose( + varying.width(h, h, l, _angles(gamma)), + [0.2, 0.4, 0.6], + ) + + def test_box_full_width_and_gaussian_fwhm(self): + box = CTRresolution.BoxResolution(0.4) + gaussian = CTRresolution.GaussianResolution(0.4) + + np.testing.assert_array_equal( + box.weights(np.array([-0.21, -0.2, 0.0, 0.2, 0.21]), 0.4), + [0.0, 1.0, 1.0, 1.0, 0.0], + ) + np.testing.assert_allclose( + gaussian.weights(np.array([-0.2, 0.0, 0.2]), 0.4), + [0.5, 1.0, 0.5], + ) + + def test_invalid_width_parameters_are_rejected(self): + for value in (-0.1, np.inf, np.nan): + with self.subTest(value=value): + with self.assertRaises(ValueError): + CTRresolution.BoxResolution(value) + with self.assertRaises(ValueError): + CTRresolution.GaussianResolution(0.1, -0.2) + + def test_gamma_dependent_width_requires_angle_records(self): + resolution = CTRresolution.BoxResolution(0.1, 0.2) + with self.assertRaisesRegex(ValueError, "calcAnglesZmode"): + resolution.width([0.0], [0.0], [1.0]) + + +class TestFastConvolution(unittest.TestCase): + def test_constant_intensity_is_preserved_on_unsorted_irregular_grid(self): + l = np.array([3.0, 0.0, 1.0, 1.8]) # noqa: E741 + ctr = CTRplotutil.CTR((1.0, 0.0), l, np.full(l.shape, 4.0)) + ctrs = CTRplotutil.CTRCollection([ctr]) + + result = CTRresolution.fast_convolve( + ctrs, CTRresolution.GaussianResolution(1.2) + ) + + np.testing.assert_array_equal(result[0].l, l) + np.testing.assert_allclose(result[0].sfI, 4.0) + + def test_box_uses_trapezoidal_weights_and_boundary_normalization(self): + l = np.array([0.0, 1.0, 3.0]) # noqa: E741 + ctr = CTRplotutil.CTR((0.0, 0.0), l, np.array([1.0, 2.0, 4.0])) + result = CTRresolution.fast_convolve( + CTRplotutil.CTRCollection([ctr]), + CTRresolution.BoxResolution(4.0), + )[0] + + expected_intensity = np.array( + [ + (0.5 * 1.0 + 1.5 * 4.0) / 2.0, + (0.5 * 1.0 + 1.5 * 4.0 + 1.0 * 16.0) / 3.0, + (1.5 * 4.0 + 1.0 * 16.0) / 2.5, + ] + ) + np.testing.assert_allclose(result.sfI, np.sqrt(expected_intensity)) + + def test_zero_width_is_identity_and_output_metadata_is_clean(self): + l = np.array([0.0, 0.4, 1.0]) # noqa: E741 + ctr = CTRplotutil.CTR( + (1.0, 2.0), + l, + np.array([2.0, 3.0, 5.0]), + err=np.ones(3), + phi=np.array([10.0, 20.0, 30.0]), + name="source rod", + ) + ctr.angles = _angles([0.0, 0.1, 0.2]) + ctr.bgI = np.ones(3) + ctrs = CTRplotutil.CTRCollection([ctr], name="source collection") + ctrs.setPlotSettings("plot argument", color="red") + + result = CTRresolution.fast_convolve( + ctrs, CTRresolution.GaussianResolution(0.0) + ) + + self.assertIsNot(result, ctrs) + self.assertIsNot(result[0], ctr) + self.assertEqual(result.name, ctrs.name) + self.assertEqual(result.plotsett, ctrs.plotsett) + self.assertEqual(result.plotkeyargs, ctrs.plotkeyargs) + self.assertEqual(result[0].name, ctr.name) + np.testing.assert_array_equal(result[0].sfI, ctr.sfI) + np.testing.assert_array_equal(result[0].angles, ctr.angles) + self.assertIsNone(result[0].err) + self.assertIsNone(result[0].phi) + self.assertFalse(hasattr(result[0], "bgI")) + np.testing.assert_array_equal(ctr.err, np.ones(3)) + np.testing.assert_array_equal(ctr.phi, [10.0, 20.0, 30.0]) + + def test_gamma_dependent_fast_convolution_requires_angles(self): + ctr = CTRplotutil.CTR((0.0, 0.0), [0.0, 1.0], [1.0, 2.0]) + with self.assertRaisesRegex(ValueError, "calcAnglesZmode"): + CTRresolution.fast_convolve( + CTRplotutil.CTRCollection([ctr]), + CTRresolution.BoxResolution(0.1, 0.2), + ) + + def test_invalid_ctr_data_are_rejected(self): + cases = [ + CTRplotutil.CTR((0.0, 0.0), [0.0, 0.0], [1.0, 2.0]), + CTRplotutil.CTR((0.0, 0.0), [0.0, np.nan], [1.0, 2.0]), + CTRplotutil.CTR((0.0, 0.0), [0.0, 1.0], [1.0, -2.0]), + ] + for ctr in cases: + with self.subTest(ctr=ctr): + with self.assertRaises(ValueError): + CTRresolution.fast_convolve( + CTRplotutil.CTRCollection([ctr]), + CTRresolution.BoxResolution(0.2), + ) + + +class TestStructureFactorSampling(unittest.TestCase): + def test_box_quadrature_matches_quadratic_intensity_average(self): + center = 2.0 + width = 0.6 + ctr = CTRplotutil.CTR((0.0, 0.0), [center], [0.0]) + result = CTRresolution.sample_structure_factor( + CTRplotutil.CTRCollection([ctr]), + QuadraticCrystal(), + CTRresolution.BoxResolution(width), + quadrature_order=5, + )[0] + + expected_intensity = center**2 + width**2 / 12.0 + np.testing.assert_allclose(result.sfI, np.sqrt(expected_intensity)) + + def test_gaussian_quadrature_matches_quadratic_intensity_average(self): + center = 2.0 + fwhm = 0.6 + ctr = CTRplotutil.CTR((0.0, 0.0), [center], [0.0]) + result = CTRresolution.sample_structure_factor( + CTRplotutil.CTRCollection([ctr]), + QuadraticCrystal(), + CTRresolution.GaussianResolution(fwhm), + quadrature_order=5, + )[0] + + sigma = fwhm / (2.0 * np.sqrt(2.0 * np.log(2.0))) + expected_intensity = center**2 + sigma**2 + np.testing.assert_allclose(result.sfI, np.sqrt(expected_intensity)) + + def test_zero_width_samples_the_central_structure_factor(self): + ctr = CTRplotutil.CTR((1.0, 2.0), [0.5, 1.5], [99.0, 99.0]) + result = CTRresolution.sample_structure_factor( + CTRplotutil.CTRCollection([ctr]), + QuadraticCrystal(), + CTRresolution.BoxResolution(0.0), + )[0] + np.testing.assert_allclose(result.sfI, [0.5, 1.5]) + + def test_dense_irregular_fast_result_converges_to_slow_result(self): + left = np.linspace(0.2, 1.0, 401) ** 1.3 + right = 1.0 + np.linspace(0.0, 0.8, 401)[1:] ** 1.7 + l = np.concatenate((left, right)) # noqa: E741 + ctr = CTRplotutil.CTR((0.0, 0.0), l, l) + ctrs = CTRplotutil.CTRCollection([ctr]) + resolution = CTRresolution.GaussianResolution(0.3) + + fast = CTRresolution.fast_convolve(ctrs, resolution)[0] + center_index = np.flatnonzero(l == 1.0)[0] + template = CTRplotutil.CTR((0.0, 0.0), [1.0], [0.0]) + slow = CTRresolution.sample_structure_factor( + CTRplotutil.CTRCollection([template]), + QuadraticCrystal(), + resolution, + )[0] + + np.testing.assert_allclose( + fast.sfI[center_index], slow.sfI[0], rtol=2e-4 + ) + + def test_invalid_quadrature_order_is_rejected(self): + ctrs = CTRplotutil.CTRCollection( + [CTRplotutil.CTR((0.0, 0.0), [1.0], [0.0])] + ) + for order in (0, 2, 2.5, True): + with self.subTest(order=order): + with self.assertRaises(ValueError): + CTRresolution.sample_structure_factor( + ctrs, + QuadraticCrystal(), + CTRresolution.BoxResolution(0.1), + quadrature_order=order, + ) + + +class RecordingAngles: + def __init__(self): + self.calls = [] + + def anglesZmode(self, hkl, fixedangle, **kwargs): + self.calls.append((np.copy(hkl), fixedangle, kwargs)) + count = hkl.shape[1] + values = np.zeros((count, 6), dtype=np.float64) + values[:, 2] = np.arange(count) + 0.25 + return values + + +class TestCollectionAngleAPI(unittest.TestCase): + def test_collection_forwards_all_z_mode_constraints(self): + ctrs = CTRplotutil.CTRCollection( + [ + CTRplotutil.CTR((0.0, 0.0), [0.1, 0.2], [1.0, 1.0]), + CTRplotutil.CTR((1.0, 0.0), [0.3], [1.0]), + ] + ) + calculator = RecordingAngles() + + result = ctrs.calcAnglesZmode( + calculator, + fixedangle=0.12, + fixed="out", + chi=0.03, + phi=-0.04, + mirrorx=True, + ) + + self.assertEqual(len(result), 2) + self.assertEqual(len(calculator.calls), 2) + for call in calculator.calls: + self.assertEqual(call[1], 0.12) + self.assertEqual( + call[2], + {"fixed": "out", "chi": 0.03, "phi": -0.04, "mirrorx": True}, + ) + np.testing.assert_allclose(ctrs[0].angles["gamma"], [0.25, 1.25]) + np.testing.assert_allclose(ctrs[1].angles["gamma"], [0.25]) + + +if __name__ == "__main__": + unittest.main() diff --git a/orgui/datautils/xrayutils/unitcells/TiO2(100).xtal b/orgui/datautils/xrayutils/unitcells/TiO2(100).xtal new file mode 100644 index 0000000..0b93b76 --- /dev/null +++ b/orgui/datautils/xrayutils/unitcells/TiO2(100).xtal @@ -0,0 +1,21 @@ +E = 20.00000 keV + +# UnitCell bulk +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.50000 0.00000 -0.19542 0.3719 0.3719 1.0000 2 +01 O 0.00000 0.00000 -0.30458 0.3719 0.3719 1.0000 2 +02 Ti 0.50000 0.00000 -0.50000 0.3719 0.3719 1.0000 2 +03 Ti 0.00000 0.50000 -0.50000 0.3719 0.3719 1.0000 2 +04 O 0.30458 0.50000 -0.50000 0.3719 0.3719 1.0000 2 +05 O 0.69542 0.50000 -0.50000 0.3719 0.3719 1.0000 2 +06 O 0.00000 0.00000 -0.69542 0.3719 0.3719 1.0000 1 +07 O 0.50000 0.00000 -0.80458 0.3719 0.3719 1.0000 1 +08 Ti 0.00000 0.00000 -1.00000 0.3719 0.3719 1.0000 1 +09 Ti 0.50000 0.50000 -1.00000 0.3719 0.3719 1.0000 1 +10 O 0.19542 0.50000 -1.00000 0.3719 0.3719 1.0000 1 +11 O 0.80458 0.50000 -1.00000 0.3719 0.3719 1.0000 1 +layerpos: 1.0 = -1.0, 2.0 = -0.5 +layer_behaviour: ignore \ No newline at end of file From 3d1783396ceb9417abc31e6348e79eee89dc930e Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Fri, 24 Jul 2026 00:13:27 -0400 Subject: [PATCH 09/37] feat: visualization of atomistic models in jupyter notebook using py3dmol --- CHANGELOG.md | 5 + orgui/datautils/xrayutils/CTRcalc.py | 66 +++++- orgui/datautils/xrayutils/CTRuc.py | 202 ++++++++++++++++-- .../datautils/xrayutils/test/test_CTRcalc.py | 201 +++++++++++++++++ pyproject.toml | 4 +- requirements.txt | 1 + 6 files changed, 452 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5af9c3..23ebb22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ Scientific and analysis additions: gamma-dependent box and Gaussian functions. Calculations can convolve irregular existing L points or sample the crystal structure factor with deterministic quadrature. +- Added py3Dmol atom-sphere rendering for Jupyter notebooks. ``plot3d`` now + selects py3Dmol automatically in a notebook, can be directed to either + py3Dmol or Mayavi explicitly, and can incrementally add unit cells to a + shared viewer. Covalent radii are interpreted consistently by both backends + and can be adjusted with the dimensionless ``radius_scale`` parameter. ## [1.5.0] (2026-06-07) diff --git a/orgui/datautils/xrayutils/CTRcalc.py b/orgui/datautils/xrayutils/CTRcalc.py index c46af2c..732ac04 100644 --- a/orgui/datautils/xrayutils/CTRcalc.py +++ b/orgui/datautils/xrayutils/CTRcalc.py @@ -1417,23 +1417,69 @@ def plot3d( occuon=False, figure=None, translate=np.array([0.0, 0.0, 0.0]), + backend="auto", + radius_scale=1.0, **keyargs, ): - try: - from mayavi import mlab - except ImportError: - warnings.warn("can not import mayavi: 3D plotting not supported") - return - - if figure is None: - figure = mlab.figure() + """Plot the bulk and surface unit cells in one 3D viewer. + + Coordinates and radii passed to either backend are in Angstrom. + Assign the result or terminate an incremental call with a semicolon to + prevent Jupyter from also rendering the returned view as a new output. + + :param int ucx: Number of cells along the first lattice direction. + :param int ucy: Number of cells along the second lattice direction. + :param int ucz: Number of bulk cells along the third lattice direction. + :param bool dwon: + Sample atom positions using the stored Debye-Waller disorder. + :param bool occuon: + Randomly omit atoms according to their occupancies. + :param figure: + Existing Mayavi figure or py3Dmol view to extend. + :param numpy.ndarray translate: + Fractional translation retained for API compatibility. + :param str backend: + ``"auto"``, ``"mayavi"``, or ``"py3dmol"``. + :param float radius_scale: + Positive dimensionless multiplier applied to covalent radii. + :returns: The selected backend's shared figure or view. + """ + keyargs["_defer_update"] = True for mat in self.uc_bulk.coherentDomainMatrix: - self.uc_bulk.plot3d(ucx, ucy, ucz, dwon, occuon, figure, mat, **keyargs) + figure = self.uc_bulk.plot3d( + ucx, + ucy, + ucz, + dwon, + occuon, + figure, + mat, + backend=backend, + radius_scale=radius_scale, + **keyargs, + ) for uc in self.uc_surface_list: for mat in uc.coherentDomainMatrix: - uc.plot3d(ucx, ucy, 1, dwon, occuon, figure, mat, **keyargs) + figure = uc.plot3d( + ucx, + ucy, + 1, + dwon, + occuon, + figure, + mat, + backend=backend, + radius_scale=radius_scale, + **keyargs, + ) + + if getattr(figure, "_orgui_plot3d_backend", None) == "py3dmol": + figure.zoomTo() + if getattr(figure, "uniqueid", None) is not None: + figure.update() + return figure def getUcNames(self): return [uc.name for uc in self.uc_surface_list] + ["bulk"] diff --git a/orgui/datautils/xrayutils/CTRuc.py b/orgui/datautils/xrayutils/CTRuc.py index 66fb61a..8d29eb3 100644 --- a/orgui/datautils/xrayutils/CTRuc.py +++ b/orgui/datautils/xrayutils/CTRuc.py @@ -73,6 +73,112 @@ resolve_upper_start, ) + +_PLOT3D_BACKENDS = ("mayavi", "py3dmol") + + +def _in_jupyter_kernel(): + """Return whether the current process is attached to an IPython kernel.""" + try: + from IPython import get_ipython + except ImportError: + return False + + shell = get_ipython() + return shell is not None and getattr(shell, "kernel", None) is not None + + +def _figure_plot3d_backend(figure): + """Identify an existing plot figure without importing optional backends.""" + backend = getattr(figure, "_orgui_plot3d_backend", None) + if backend in _PLOT3D_BACKENDS: + return backend + + module = type(figure).__module__.lower() + if "py3dmol" in module: + return "py3dmol" + if "mayavi" in module or "tvtk" in module: + return "mayavi" + return None + + +def _prepare_plot3d_figure(backend, figure): + """Resolve a 3D plotting backend and return its figure and renderer.""" + backend = backend.lower() + if backend not in (*_PLOT3D_BACKENDS, "auto"): + raise ValueError( + f"Unknown 3D plotting backend {backend!r}; expected 'auto', " + "'mayavi', or 'py3dmol'." + ) + + figure_backend = None + if figure is not None: + figure_backend = _figure_plot3d_backend(figure) + if backend != "auto" and figure_backend not in (None, backend): + raise TypeError( + f"The supplied figure uses {figure_backend}, not the requested " + f"{backend} backend." + ) + + if backend == "auto": + if figure_backend is not None: + candidates = (figure_backend,) + elif figure is not None: + # Existing callers historically pass Mayavi scene objects. + candidates = ("mayavi",) + elif _in_jupyter_kernel(): + candidates = ("py3dmol", "mayavi") + else: + candidates = ("mayavi", "py3dmol") + else: + candidates = (backend,) + + errors = {} + for candidate in candidates: + try: + if candidate == "py3dmol": + if figure is None: + py3Dmol = importlib.import_module("py3Dmol") + figure = py3Dmol.view() + renderer = None + else: + renderer = importlib.import_module("mayavi.mlab") + if figure is None: + figure = renderer.figure() + except ImportError as error: + errors[candidate] = error + continue + + try: + figure._orgui_plot3d_backend = candidate + except AttributeError: + pass + return candidate, figure, renderer + + if backend == "auto": + message = ( + "No supported 3D plotting backend is installed. Install py3Dmol " + "(included in orGUI[full]) for notebooks or Mayavi for desktop use." + ) + elif backend == "py3dmol": + message = ( + "The py3dmol backend requires py3Dmol. Install orGUI[full] or " + "install py3Dmol separately." + ) + else: + message = "The mayavi backend requires Mayavi." + raise ImportError(message) from next(iter(errors.values()), None) + + +def _rgb_to_hex(color): + """Convert an RGB triple in the Mayavi range to a CSS hexadecimal color.""" + rgb = np.asarray(color, dtype=np.float64) + if rgb.shape != (3,): + raise ValueError("Atom colors must be RGB triples.") + rgb = np.clip(np.rint(rgb * 255), 0, 255).astype(np.uint8) + return "#" + "".join(f"{channel:02x}" for channel in rgb) + + def _import_cpp_accel(): try: return importlib.import_module("orgui.datautils.xrayutils._CTRcalc_cpp") @@ -3583,18 +3689,50 @@ def plot3d( figure=None, translate=np.array([0.0, 0.0, 0.0]), domain=0, + backend="auto", + radius_scale=1.0, **keyargs, ): + """Plot atoms as covalent-radius spheres in a 3D viewer. + + Coordinates and radii passed to either backend are in Angstrom. + Reusing the returned figure adds atoms to the existing scene. In a + notebook, an already displayed py3Dmol view is updated in place. + Assign the result or terminate an incremental call with a semicolon to + prevent Jupyter from also rendering the returned view as a new output. + The ``resolution`` keyword controls Mayavi spheres only; py3Dmol uses + its native sphere rendering. + + :param int ucx: Number of cells along the first lattice direction. + :param int ucy: Number of cells along the second lattice direction. + :param int ucz: Number of cells along the third lattice direction. + :param bool dwon: + Sample atom positions using the stored Debye-Waller disorder. + :param bool occuon: + Randomly omit atoms according to their occupancies. + :param figure: + Existing Mayavi figure or py3Dmol view to extend. + :param numpy.ndarray translate: + Fractional translation vector or coherent-domain transform. + :param int domain: Index of the coherent-domain matrix. + :param str backend: + ``"auto"``, ``"mayavi"``, or ``"py3dmol"``. Automatic selection + prefers py3Dmol in an IPython kernel and Mayavi otherwise. + :param float radius_scale: + Positive dimensionless multiplier applied to covalent radii. + :returns: The selected backend's figure or view. + :raises ValueError: If ``radius_scale`` is not positive and finite. + """ try: - from mayavi import mlab - except ImportError: - warnings.warn("can not import mayavi: 3D plotting not supported") - return + radius_scale = float(radius_scale) + except (TypeError, ValueError) as error: + raise ValueError("radius_scale must be positive and finite.") from error + if not np.isfinite(radius_scale) or radius_scale <= 0: + raise ValueError("radius_scale must be positive and finite.") - if figure is None: - figure = mlab.figure() + backend, figure, mlab = _prepare_plot3d_figure(backend, figure) if keyargs.get("useSelected", True): - basis, formf, names = self.build_selected_basis() + basis, _formf, names = self.build_selected_basis() else: basis, _formf, names = self.basis, self.f, self.names if ucx == 0 or ucy == 0 or ucz == 0: @@ -3620,7 +3758,10 @@ def color_generator(name): mat = self.coherentDomainMatrix[domain] domainmatrix = self.R_mat_inv @ mat[:, :-1] @ self.R_mat for i, params in enumerate(basis): - radius = cov_radii_array[atomic_number(names[i]) - 1][2] * 2 + covalent_radius = float( + cov_radii_array[atomic_number(names[i]) - 1][2] + ) + sphere_radius = covalent_radius * radius_scale # elcolor_c = keyargs.get('color') # if elcolor_c is None: # elcolor_c = elements.rgb(int(params[0])) @@ -3676,18 +3817,49 @@ def color_generator(name): if dwon: position_cart = np.random.default_rng().normal(position_cart, sigmas) - mlab.points3d( - *position_cart, - scale_factor=radius, - color=elcolors[i], - resolution=resolution, - figure=figure, - ) + if backend == "mayavi": + mlab.points3d( + *position_cart, + scale_factor=2 * sphere_radius, + color=elcolors[i], + resolution=resolution, + figure=figure, + ) + else: + valid = np.all(np.isfinite(position_cart), axis=0) + positions_valid = position_cart[:, valid].T + if positions_valid.size == 0: + continue + symbol = cov_radii_array[atomic_number(names[i]) - 1][1] + xyz_lines = [ + str(len(positions_valid)), + f"orGUI UnitCell {self.name}", + ] + xyz_lines.extend( + f"{symbol} {position[0]:.16g} {position[1]:.16g} " + f"{position[2]:.16g}" + for position in positions_valid + ) + figure.addModel("\n".join(xyz_lines) + "\n", "xyz") + figure.getModel().setStyle( + {}, + { + "sphere": { + "color": _rgb_to_hex(elcolors[i]), + "radius": sphere_radius, + } + }, + ) atomlist = keyargs.get("atomlist") if atomlist is not None: atomlist.append(self.pos_cart_all(ucx, ucy, ucz, translate)) + if backend == "py3dmol" and not keyargs.get("_defer_update", False): + figure.zoomTo() + if getattr(figure, "uniqueid", None) is not None: + figure.update() + return figure def pos_cart_all( diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index 52c51ac..dad0483 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -45,6 +45,207 @@ from ..CTRutil import generate_surface_termination_cells +class _FakePy3DmolModel: + def __init__(self): + self.styles = [] + + def setStyle(self, selection, style): + self.styles.append((selection, style)) + + +class _FakePy3DmolView: + def __init__(self, displayed=False): + self._orgui_plot3d_backend = "py3dmol" + self.uniqueid = "displayed" if displayed else None + self.models = [] + self.zoom_count = 0 + self.update_count = 0 + + def addModel(self, data, file_format): + self.models.append((data, file_format, _FakePy3DmolModel())) + + def getModel(self): + return self.models[-1][2] + + def zoomTo(self): + self.zoom_count += 1 + + def update(self): + self.update_count += 1 + + +class TestPlot3d(unittest.TestCase): + def setUp(self): + self.cell = CTRuc.UnitCell( + [2.0, 3.0, 4.0], + [90.0, 90.0, 90.0], + name="plot-test", + ) + self.cell.addAtom("C", [0.5, 0.0, 0.0], 0.1, 0.1, 1.0) + self.cell.addAtom("O", [0.0, 0.5, 0.0], 0.1, 0.1, 1.0) + self.cell.f = np.empty((2, 13), dtype=np.float64) + + @staticmethod + def _xyz_positions(model_data): + lines = model_data.splitlines() + return np.array( + [[float(value) for value in line.split()[1:]] for line in lines[2:]] + ) + + def test_py3dmol_models_preserve_positions_styles_and_increment(self): + view = _FakePy3DmolView(displayed=True) + + result = self.cell.plot3d( + 2, + 1, + 1, + figure=view, + backend="py3dmol", + ) + + self.assertIs(result, view) + self.assertEqual(len(view.models), 2) + self.assertEqual(view.update_count, 1) + self.assertEqual(view.zoom_count, 1) + carbon_xyz, file_format, carbon_model = view.models[0] + self.assertEqual(file_format, "xyz") + self.assertEqual(carbon_xyz.splitlines()[0], "2") + expected = self.cell.pos_cart_all(2, 1, 1) + expected_carbon = np.column_stack( + ( + expected["x"][expected["name"] == "C"], + expected["y"][expected["name"] == "C"], + expected["z"][expected["name"] == "C"], + ) + ) + np.testing.assert_allclose( + self._xyz_positions(carbon_xyz), + expected_carbon, + atol=1e-14, + ) + selection, style = carbon_model.styles[0] + self.assertEqual(selection, {}) + self.assertEqual(style["sphere"]["radius"], 0.76) + self.assertRegex(style["sphere"]["color"], r"^#[0-9a-f]{6}$") + + second_result = self.cell.plot3d(figure=view) + self.assertIs(second_result, view) + self.assertEqual(len(view.models), 4) + self.assertEqual(view.update_count, 2) + + def test_radius_scale_applies_to_py3dmol_radius(self): + view = _FakePy3DmolView() + + self.cell.plot3d( + figure=view, + backend="py3dmol", + radius_scale=0.5, + ) + + radii = [ + model.styles[0][1]["sphere"]["radius"] + for _, _, model in view.models + ] + self.assertEqual(radii, [0.38, 0.33]) + + def test_py3dmol_omits_unoccupied_atoms(self): + self.cell.basis[0, 6] = 0.0 + view = _FakePy3DmolView() + + self.cell.plot3d( + figure=view, + backend="py3dmol", + occuon=True, + ) + + self.assertEqual(len(view.models), 1) + self.assertTrue(view.models[0][0].splitlines()[2].startswith("O ")) + + def test_mayavi_backend_keeps_points3d_contract(self): + figure = mock.Mock() + figure._orgui_plot3d_backend = "mayavi" + mlab = mock.Mock() + + with mock.patch.object(CTRuc.importlib, "import_module", return_value=mlab): + result = self.cell.plot3d( + figure=figure, + backend="mayavi", + color=(0.1, 0.2, 0.3), + resolution=17, + ) + + self.assertIs(result, figure) + self.assertEqual(mlab.points3d.call_count, 2) + diameters = [ + call.kwargs["scale_factor"] for call in mlab.points3d.call_args_list + ] + self.assertEqual(diameters, [1.52, 1.32]) + kwargs = mlab.points3d.call_args.kwargs + self.assertEqual(kwargs["color"], (0.1, 0.2, 0.3)) + self.assertEqual(kwargs["resolution"], 17) + self.assertIs(kwargs["figure"], figure) + + def test_backend_validation_and_missing_dependency(self): + for radius_scale in (0, -1, np.nan, np.inf, None, "invalid"): + with self.subTest(radius_scale=radius_scale): + with self.assertRaisesRegex(ValueError, "positive and finite"): + self.cell.plot3d(radius_scale=radius_scale) + + with self.assertRaisesRegex(ValueError, "Unknown 3D plotting backend"): + self.cell.plot3d(backend="unknown") + + mayavi_figure = mock.Mock() + mayavi_figure._orgui_plot3d_backend = "mayavi" + with self.assertRaisesRegex(TypeError, "uses mayavi"): + self.cell.plot3d(figure=mayavi_figure, backend="py3dmol") + + with mock.patch.object( + CTRuc.importlib, + "import_module", + side_effect=ImportError("simulated missing dependency"), + ): + with self.assertRaisesRegex(ImportError, r"orGUI\[full\]"): + self.cell.plot3d(backend="py3dmol") + + def test_auto_prefers_py3dmol_in_kernel(self): + view = _FakePy3DmolView() + py3dmol = mock.Mock() + py3dmol.view.return_value = view + + with ( + mock.patch.object(CTRuc, "_in_jupyter_kernel", return_value=True), + mock.patch.object( + CTRuc.importlib, + "import_module", + return_value=py3dmol, + ) as import_module, + ): + result = self.cell.plot3d(backend="auto") + + self.assertIs(result, view) + import_module.assert_called_once_with("py3Dmol") + self.assertEqual(len(view.models), 2) + + def test_crystal_updates_one_shared_view_once(self): + surface = copy.deepcopy(self.cell) + crystal = CTRcalc.SXRDCrystal(self.cell, surface) + view = _FakePy3DmolView(displayed=True) + + result = crystal.plot3d( + 1, + 1, + 1, + figure=view, + backend="py3dmol", + radius_scale=0.5, + ) + + self.assertIs(result, view) + self.assertEqual(len(view.models), 4) + self.assertEqual(view.update_count, 1) + self.assertEqual(view.models[0][2].styles[0][1]["sphere"]["radius"], 0.38) + + class TestReadSXRDCrystal(unittest.TestCase): xpr_file = """E = 68.00000 keV # UnitCell relaxations diff --git a/pyproject.toml b/pyproject.toml index 19ad146..b75701d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,8 +52,8 @@ console = ['qtconsole'] speedup = ['numba'] extendedfilesupport = ['ase', 'hdf5plugin'] symmetry = ['pyxtal'] -full = ['qtconsole', 'ase', 'numba' , 'PyOpenGL', 'hdf5plugin', 'pyxtal'] -full-devel = ['qtconsole', 'ase', 'numba' , 'PyOpenGL', 'hdf5plugin', 'pyxtal', 'ruff', 'pyupgrade'] +full = ['qtconsole', 'ase', 'numba' , 'PyOpenGL', 'hdf5plugin', 'pyxtal', 'py3Dmol'] +full-devel = ['qtconsole', 'ase', 'numba' , 'PyOpenGL', 'hdf5plugin', 'pyxtal', 'py3Dmol', 'ruff', 'pyupgrade'] [build-system] requires = ["meson-python", "pybind11", "setuptools-scm>=8", "numpy"] diff --git a/requirements.txt b/requirements.txt index 5b65809..cdd56bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ qtconsole ase hdf5plugin PyOpenGL +py3Dmol From af90fcda988e1d33c84bfd0c797457a4b5383edf Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Fri, 24 Jul 2026 00:18:46 -0400 Subject: [PATCH 10/37] fix: force atomic coordinates to be stacked for split_in_layers() --- orgui/datautils/xrayutils/CTRcalc.py | 3 +- orgui/datautils/xrayutils/CTRuc.py | 99 +++++++++++-- orgui/datautils/xrayutils/_CTRcalc_accel.py | 2 +- orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp | 2 +- .../datautils/xrayutils/test/test_CTRcalc.py | 130 ++++++++++++++++++ 5 files changed, 225 insertions(+), 11 deletions(-) diff --git a/orgui/datautils/xrayutils/CTRcalc.py b/orgui/datautils/xrayutils/CTRcalc.py index 732ac04..735d2f1 100644 --- a/orgui/datautils/xrayutils/CTRcalc.py +++ b/orgui/datautils/xrayutils/CTRcalc.py @@ -85,7 +85,8 @@ def __init__(self, uc_bulk, *uc_surface, **keyargs): :param numpy.ndarray stacking: Integer stacking levels for the surface components. :param float atten: - Dimensionless bulk attenuation exponent per unit cell. + Dimensionless bulk attenuation exponent per reference-cell + out-of-plane repeat. """ self.uc_bulk = uc_bulk self.uc_surface_list = list(uc_surface) diff --git a/orgui/datautils/xrayutils/CTRuc.py b/orgui/datautils/xrayutils/CTRuc.py index 8d29eb3..5c3f8cf 100644 --- a/orgui/datautils/xrayutils/CTRuc.py +++ b/orgui/datautils/xrayutils/CTRuc.py @@ -1277,7 +1277,30 @@ def insertAtom( """ def split_in_layers(self, ordered=True): + """Return structural layers as views into a contiguously ordered basis. + + Layered ``UnitCell`` objects require atoms in each layer to occupy one + contiguous basis block. Native CTR text models must already satisfy + this invariant. CIF files imported through ASE are the sole exception: + their atom-site order is not crystallographically significant, so their + basis may be normalized here when necessary. + """ layer_numbers = np.sort(np.unique(self.basis[:, 7])) + noncontiguous_layers = [] + for layer in layer_numbers: + indices = np.flatnonzero(self.basis[:, 7] == layer) + if indices.size > 1 and np.any(np.diff(indices) != 1): + noncontiguous_layers.append(layer) + if noncontiguous_layers: + if not getattr(self, "_allow_layer_order_normalization", False): + raise ValueError( + "UnitCell layers must occupy contiguous basis rows; " + f"noncontiguous layers: {noncontiguous_layers!r}. " + "Native .xtal/.xpr and manually constructed UnitCells " + "must be written in layer order." + ) + self._reorder_atoms(np.argsort(self.basis[:, 7], kind="stable")) + layers = OrderedDict() if not hasattr(self, "f"): self.setEnergy( @@ -1427,6 +1450,47 @@ def _remap_parameter_atom_indices(parameter, old_to_new): atoms = np.asarray(atoms, dtype=np.intp) parameter.indices = (old_to_new[atoms], parindexes) + def _reorder_atoms(self, order): + """Reorder atom-aligned state and remap atom-indexed metadata. + + :param numpy.ndarray order: + Permutation mapping new basis rows to their old row indices. + """ + order = np.asarray(order, dtype=np.intp) + atom_count = self.basis.shape[0] + if order.shape != (atom_count,) or not np.array_equal( + np.sort(order), np.arange(atom_count, dtype=np.intp) + ): + raise ValueError("order must be a permutation of UnitCell atom indices.") + + old_to_new = np.empty(atom_count, dtype=np.intp) + old_to_new[order] = np.arange(atom_count, dtype=np.intp) + for attribute in ( + "basis", + "basis_0", + "_basis_parvalues", + "errors", + "_errors_parvalues", + "f", + "dw_increase_constraint", + ): + values = getattr(self, attribute, None) + if values is not None and np.size(values): + setattr(self, attribute, np.asarray(values)[order]) + self.names = [self.names[index] for index in order] + + for parameters in self.parameters.values(): + for parameter in parameters: + self._remap_parameter_atom_indices(parameter, old_to_new) + layer_map = {layer: layer for layer in np.unique(self.basis[:, 7])} + self.symmetry_metadata = self._transform_symmetry_metadata( + self.symmetry_metadata, + old_to_new, + layer_map, + np.zeros(2, dtype=np.float64), + {layer: 0.0 for layer in layer_map}, + ) + @staticmethod def _update_absolute_parameter_values_after_basis_transform( transformed, @@ -3493,7 +3557,9 @@ def F_bulk(self, h, k, l, atten=0): # noqa: E741 """Return the semi-infinite bulk structure factor in electrons. The amplitude represents one lateral bulk unit cell. The geometric - lattice sum is applied only along the out-of-plane direction. + lattice sum is applied only along the out-of-plane direction. Its + phase and attenuation follow the bulk-cell repeat after conversion + from the configured reference unit cell. :param numpy.ndarray h: Reference-frame reciprocal coordinate in r.l.u. @@ -3502,12 +3568,16 @@ def F_bulk(self, h, k, l, atten=0): # noqa: E741 :param numpy.ndarray l: Reference-frame reciprocal coordinate in r.l.u. :param float atten: - Dimensionless attenuation exponent per bulk unit cell. + Dimensionless attenuation exponent per reference-cell + out-of-plane repeat. :returns: Complex bulk amplitude in electrons per lateral bulk cell. :rtype: numpy.ndarray """ basis, formf, names = self.build_selected_basis() + # The reciprocal transform maps reference r.l.u. to bulk r.l.u.; + # (2, 2) is the bulk/reference out-of-plane repeat ratio. + repeat_atten = atten * abs(self.refHKLTransform[2, 2]) if ctr_accel_enabled(): h, k, l = _ensure_contiguous(h, k, l, testOnly=False, astype=np.float64) # noqa: E741 accel = _ctr_accel_module() @@ -3515,7 +3585,7 @@ def F_bulk(self, h, k, l, atten=0): # noqa: E741 h, k, l, - atten, + repeat_atten, basis, formf, self.refHKLTransform, @@ -3529,8 +3599,8 @@ def F_bulk(self, h, k, l, atten=0): # noqa: E741 return F else: hkl = self.refHKLTransform @ np.vstack((h, k, l)) - Fuc = self.F_uc_bulk_direct(*hkl, atten) - return Fuc / (1 - np.exp(-2j * np.pi * l - atten)) + Fuc = self.F_uc_bulk_direct(*hkl, repeat_atten) + return Fuc / (1 - np.exp(-2j * np.pi * hkl[2] - repeat_atten)) SQRT2pi = np.sqrt(2 * np.pi) @@ -4165,10 +4235,23 @@ def fromFile(filename): f"File {filename} contains no valid crystal lattice parameters" ) uc = UnitCell(atoms.cell.lengths(), atoms.cell.angles()) - coord = atoms.get_scaled_positions() + coord = np.mod(atoms.get_scaled_positions(), 1.0) symb = atoms.get_chemical_symbols() - for sym, xyz in zip(symb, coord): - uc.addAtom(sym, xyz, 0.0, 0.0, 1.0) + if ext.lower() == ".cif": + order = sorted( + range(len(symb)), + key=lambda index: ( + coord[index, 2], + coord[index, 1], + coord[index, 0], + symb[index], + ), + ) + uc._allow_layer_order_normalization = True + else: + order = range(len(symb)) + for index in order: + uc.addAtom(symb[index], coord[index], 0.0, 0.0, 1.0) return uc @classmethod diff --git a/orgui/datautils/xrayutils/_CTRcalc_accel.py b/orgui/datautils/xrayutils/_CTRcalc_accel.py index ebbc87e..01e7d5f 100644 --- a/orgui/datautils/xrayutils/_CTRcalc_accel.py +++ b/orgui/datautils/xrayutils/_CTRcalc_accel.py @@ -142,7 +142,7 @@ def _unitcell_F_core( domain_factor *= math.exp(atten * z_rel) amplitude += domain_factor * form_factor if apply_bulk_lattice_sum: - denominator_phase = -two_pi * l[p] + denominator_phase = -two_pi * ll denominator = 1.0 - math.exp(-atten) * ( math.cos(denominator_phase) + 1j * math.sin(denominator_phase) ) diff --git a/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp b/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp index a1634f5..5afd49c 100644 --- a/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp +++ b/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp @@ -320,7 +320,7 @@ py::array_t unitcell_core( } } if (apply_bulk_lattice_sum) { - const double denominator_phase = -two_pi * l_data[p]; + const double denominator_phase = -two_pi * ll; const double lattice_factor = std::exp(-atten); const double denominator_real = ( 1.0 - lattice_factor * std::cos(denominator_phase) diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index dad0483..423f0f6 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -335,6 +335,40 @@ def testFromFile(self): self.assertEqual(str(restored), serialized) +class TestUnitCellCifImport(unittest.TestCase): + def test_cif_import_orders_atoms_by_fractional_coordinate(self): + cif = """data_ordering +_cell_length_a 5 +_cell_length_b 5 +_cell_length_c 5 +_cell_angle_alpha 90 +_cell_angle_beta 90 +_cell_angle_gamma 90 +_space_group_name_H-M_alt P1 +loop_ +_atom_site_label +_atom_site_type_symbol +_atom_site_fract_x +_atom_site_fract_y +_atom_site_fract_z +O1 O 0.3 0.4 0.8 +O2 O 0.1 0.8 0.2 +C1 C 0.4 0.2 0.2 +Fe1 Fe 0.9 0.1 0.2 +""" + with tempfile.NamedTemporaryFile(suffix=".cif", mode="w") as handle: + handle.write(cif) + handle.flush() + unitcell = CTRcalc.UnitCell.fromFile(handle.name) + + self.assertEqual(unitcell.names, ["Fe", "C", "O", "O"]) + np.testing.assert_allclose( + unitcell.basis[:, 1:4], + [[0.9, 0.1, 0.2], [0.4, 0.2, 0.2], [0.1, 0.8, 0.2], [0.3, 0.4, 0.8]], + ) + self.assertTrue(unitcell._allow_layer_order_normalization) + + class TestPoissonSurface(unittest.TestCase): def setUp(self): self.unitcell = CTRcalc.UnitCell( @@ -795,6 +829,70 @@ def make_layered_unitcell(name="layered"): unitcell.layerpos[float(layer)] = z return unitcell + def test_split_in_layers_rejects_interleaved_native_layers(self): + unitcell = CTRcalc.UnitCell([3.0, 3.0, 6.0], [90.0, 90.0, 90.0]) + for name, z, layer in (("Fe", 0.1, 1), ("Re", 0.6, 2), ("O", 0.2, 1)): + unitcell.addAtom(name, [0.0, 0.0, z], 0.1, 0.1, 1.0, layer=layer) + + with self.assertRaisesRegex(ValueError, "must be written in layer order"): + unitcell.split_in_layers() + + def test_split_in_layers_normalizes_cif_metadata_and_couplings(self): + unitcell = CTRcalc.UnitCell([3.0, 3.0, 6.0], [90.0, 90.0, 90.0]) + for name, z, layer in (("Fe", 0.1, 1), ("Re", 0.6, 2), ("O", 0.2, 1), ("O", 0.7, 2)): + unitcell.addAtom(name, [0.0, 0.0, z], 0.1, 0.1, 1.0, layer=layer) + unitcell._allow_layer_order_normalization = True + unitcell.symmetry_metadata = CTRsymmetry.SurfaceSymmetryModel( + CTRsymmetry.SurfaceCellSpec( + (3.0, 3.0, 6.0), + (90.0, 90.0, 90.0), + np.identity(3), + layer_origins=(0.0, 0.5), + ), + (), + atoms=[ + CTRsymmetry.GeneratedWyckoffAtom( + atom_index=2, + element="O", + site_id="O_1", + wyckoff_label="1a", + parent_fractional=np.array([0.0, 0.0, 0.2]), + surface_fractional=np.array([0.0, 0.0, 0.2]), + layer=1, + couplings=( + CTRsymmetry.WyckoffCoupling( + atom_index=2, + coordinate="z", + variable="u", + constant=0.2, + factor=1.0, + site_id="O_1", + ), + ), + site_couplings=( + CTRsymmetry.WyckoffSiteCoupling( + atom_index=2, + coordinate="z", + axis="z", + factor=1.0, + site_id="O_1", + ), + ), + ), + ], + ) + + layers = unitcell.split_in_layers() + + self.assertEqual(unitcell.names, ["Fe", "O", "Re", "O"]) + self.assertEqual(layers[1.0].names, ["Fe", "O"]) + self.assertEqual(layers[2.0].names, ["Re", "O"]) + self.assertTrue(np.shares_memory(layers[1.0].basis, unitcell.basis)) + self.assertTrue(np.shares_memory(layers[2.0].basis, unitcell.basis)) + atom = unitcell.atom_wyckoff_metadata(1) + self.assertEqual(atom.couplings[0].atom_index, 1) + self.assertEqual(atom.site_couplings[0].atom_index, 1) + def test_unitcell_selects_layer_after_below_layer(self): unitcell = self.make_layered_unitcell() unitcell.layer_behavior = "select" @@ -1897,6 +1995,38 @@ def test_crystal_factor_is_invariant_to_lateral_supercell(self): rtol=1e-2, ) + def test_bulk_repeat_follows_reference_unit_cell(self): + reference = CTRcalc.UnitCell( + [3.0, 3.0, 4.0], + [90.0, 90.0, 90.0], + name="reference", + ) + bulk = CTRcalc.UnitCell( + [3.0, 3.0, 8.0], + [90.0, 90.0, 90.0], + name="bulk", + ) + bulk.addAtom("C", [0.0, 0.0, 0.25], 0.1, 0.1, 1.0) + bulk.setEnergy(10000.0) + bulk.setReferenceUnitCell(reference) + h = np.zeros(2) + k = np.zeros(2) + l = np.array([0.17, 0.41]) + atten = 0.07 + + hkl_bulk = bulk.refHKLTransform @ np.vstack((h, k, l)) + repeat_atten = 2.0 * atten + expected = bulk.F_uc_bulk_direct(*hkl_bulk, repeat_atten) + expected /= 1 - np.exp( + -2j * np.pi * hkl_bulk[2] - repeat_atten + ) + + with mock.patch.object(CTRuc, "CTR_ACCEL_BACKEND", "numpy"): + actual = bulk.F_bulk(h, k, l, atten) + + np.testing.assert_allclose(actual, expected) + np.testing.assert_allclose(hkl_bulk[2], 2.0 * l) + def test_constructor_propagates_bulk_reference_to_layers(self): bulk = self.make_carbon_cell(3.0, [0.0], "bulk") film_cell = self.make_carbon_cell(6.0, [0.0, 0.5], "film") From d4a2eed7727561d9a1ba2f8833809f63a99dd0e4 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Fri, 24 Jul 2026 12:30:11 -0500 Subject: [PATCH 11/37] perf: caching of atomic form factors --- .gitignore | 2 + benchmarks/verify_ctr_numba_host.py | 45 ++ .../benchmarks/ctr_accel_backends.ipynb | 149 ++++++- meson.build | 3 +- orgui/datautils/xrayutils/CTRuc.py | 66 +++ orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp | 386 +++++++++++++++++- .../datautils/xrayutils/test/test_CTRcalc.py | 30 +- subprojects/packagefiles/xxhash/meson.build | 19 + subprojects/xxhash.wrap | 9 + 9 files changed, 670 insertions(+), 39 deletions(-) create mode 100644 subprojects/packagefiles/xxhash/meson.build create mode 100644 subprojects/xxhash.wrap diff --git a/.gitignore b/.gitignore index 93b0e18..15a3129 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ __pycache__/ dist/ build/ +subprojects/xxHash-0.8.2/ +subprojects/.wraplock orgui.egg-info/ orgui/_version.py benchmarks/roi_sum_results.json diff --git a/benchmarks/verify_ctr_numba_host.py b/benchmarks/verify_ctr_numba_host.py index b029107..8aa2e81 100644 --- a/benchmarks/verify_ctr_numba_host.py +++ b/benchmarks/verify_ctr_numba_host.py @@ -69,6 +69,17 @@ def median_time(func, repeats: int) -> float: return statistics.median(timings) +def median_cold_cache_time(func, repeats: int) -> float: + """Return median C++ timing with the cache cleared before each call.""" + timings = [] + for _ in range(repeats): + CTRuc.clear_form_factor_cache() + start = time.perf_counter() + func() + timings.append(time.perf_counter() - start) + return statistics.median(timings) + + def load_crystal(): """Load the bundled CTR regression crystal.""" xpr_path = FIXTURE_DIR / "0V12_calculated.xpr" @@ -178,6 +189,40 @@ def run_benchmark(repeats: int = 5) -> None: ) print(text) + if CTRuc.HAS_CPP_ACCEL: + set_backend("cpp") + + def cache_func(): + """Evaluate the cacheable canonical unit-cell amplitude.""" + return xtal.uc_bulk.F_uc(h, k, l_values) + + old_budget = CTRuc.form_factor_cache_stats()["budget_bytes"] + try: + CTRuc.clear_form_factor_cache() + CTRuc.reset_form_factor_cache_stats() + CTRuc.set_form_factor_cache_budget(0) + uncached = median_time(cache_func, repeats) + CTRuc.set_form_factor_cache_budget(old_budget) + cold = median_cold_cache_time(cache_func, repeats) + CTRuc.clear_form_factor_cache() + cache_func() + warm = median_time(cache_func, repeats) + stats = CTRuc.form_factor_cache_stats() + finally: + CTRuc.clear_form_factor_cache() + CTRuc.set_form_factor_cache_budget(old_budget) + expected = CTRuc.form_factor_cache_expected_bytes( + n_points, + stats["species_entries"], + ) + print( + "F_uc cache " + f"uncached={uncached:.6g}s cold={cold:.6g}s warm={warm:.6g}s " + f"hits={stats['hits']} misses={stats['misses']} " + f"evictions={stats['evictions']} resident={stats['resident_bytes']}B " + f"expected={expected}B" + ) + def main() -> None: """Print environment information and run the benchmark.""" diff --git a/doc/source/benchmarks/ctr_accel_backends.ipynb b/doc/source/benchmarks/ctr_accel_backends.ipynb index bfb1d77..e7de2e5 100644 --- a/doc/source/benchmarks/ctr_accel_backends.ipynb +++ b/doc/source/benchmarks/ctr_accel_backends.ipynb @@ -62,7 +62,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Benchmark directory: C:\\Users\\timof\\Documents\\repos\\orGUI\\benchmarks\n", + "Benchmark directory: C:\\Users\\timof\\Documents\\repos\\orGUI\\doc\\source\\benchmarks\n", "Fixture: C:\\Users\\timof\\Documents\\repos\\orGUI\\benchmarks\\fixtures\\0V12_calculated.xpr\n" ] } @@ -76,7 +76,16 @@ "\n", "\n", "BENCHMARK_DIR = Path.cwd().resolve()\n", - "FIXTURE_DIR = BENCHMARK_DIR / \"fixtures\"\n", + "FIXTURE_CANDIDATES = (\n", + " BENCHMARK_DIR / \"fixtures\",\n", + " BENCHMARK_DIR.parents[2] / \"benchmarks\" / \"fixtures\",\n", + ")\n", + "FIXTURE_DIR = next((path for path in FIXTURE_CANDIDATES if path.is_dir()), None)\n", + "if FIXTURE_DIR is None:\n", + " raise FileNotFoundError(\n", + " \"Could not find benchmark fixtures. Run from benchmarks/ or execute \"\n", + " \"this notebook from doc/source/benchmarks/.\"\n", + " )\n", "XPR_PATH = FIXTURE_DIR / \"0V12_calculated.xpr\"\n", "\n", "if (BENCHMARK_DIR / \"orgui\").is_dir():\n", @@ -84,11 +93,6 @@ " \"Notebook is running from the repository root. Start Jupyter from \"\n", " \"benchmarks/ to benchmark the installed package.\"\n", " )\n", - "if not XPR_PATH.exists():\n", - " raise FileNotFoundError(\n", - " f\"Could not find {XPR_PATH}. Start Jupyter from the benchmarks/ \"\n", - " \"directory so benchmark fixtures are available.\"\n", - " )\n", "\n", "print(f\"Benchmark directory: {BENCHMARK_DIR}\")\n", "print(f\"Fixture: {XPR_PATH}\")\n" @@ -122,8 +126,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "orGUI version 1.5.1.dev39+g72e3872c2.d20260704\n", - "{'native_optimization': True, 'target_cpu': 'x86_64', 'allowed_instruction_set': ['AVX512'], 'native_arguments': '/arch:AVX512', 'cpp_compiler_id': 'msvc', 'cpp_compiler_version': '19.43.34810', 'host_system': 'windows', 'host_cpu_family': 'x86_64', 'host_cpu': 'x86_64', 'available': True}\n" + "orGUI version 1.5.1.dev69+gaf90fcda9.d20260724\n", + "{'native_optimization': False, 'target_cpu': 'generic', 'allowed_instruction_set': [], 'native_arguments': '', 'cpp_compiler_id': 'msvc', 'cpp_compiler_version': '19.43.34810', 'host_system': 'windows', 'host_cpu_family': 'x86_64', 'host_cpu': 'x86_64', 'available': True}\n" ] } ], @@ -229,7 +233,19 @@ " return f\"{value * 1e6:8.1f} us\"\n", " if value < 1:\n", " return f\"{value * 1e3:8.2f} ms\"\n", - " return f\"{value:8.3f} s\"\n" + " return f\"{value:8.3f} s\"\n", + "\n", + "\n", + "def cold_cache_time(func, repeats=7):\n", + " \"\"\"Return a median with the global cache cleared before every call.\"\"\"\n", + " timings = []\n", + " for _ in range(repeats):\n", + " CTRuc.clear_form_factor_cache()\n", + " start = time.perf_counter()\n", + " func()\n", + " timings.append(time.perf_counter() - start)\n", + " return statistics.median(timings)\n", + "\n" ] }, { @@ -281,9 +297,11 @@ "id": "b7762abd", "metadata": {}, "source": [ - "## Benchmark Structure-Factor Evaluation\n", + "## Structure-Factor and C++ Form-Factor Cache Benchmarks\n", "\n", - "This measures direct repeated evaluations for increasing CTR rod lengths. Use the median time for stable comparisons; the minimum time is also shown as a lower bound on a quiet machine.\n" + "The first table measures direct repeated structure-factor evaluations for increasing CTR rod lengths. The cache table then measures canonical `UnitCell.F_uc` with no resident cache, with a cleared cache before each call, and with a pre-populated cache.\n", + "\n", + "Exact cache reuse requires bitwise-identical computed float64 Q-squared vectors and complete 13-value Waasmaier rows. The cache is process-global; its 256 MiB LRU budget counts retained Q-squared and real form-factor vectors, not call-local arrays or output arrays.\n" ] }, { @@ -297,12 +315,12 @@ "output_type": "stream", "text": [ "benchmark points numpy median cpp median numba median cpp speedup numba speedup\n", - "UnitCell.F_uc 200 130.9 us 29.3 us 27.0 us 4.47x 4.85x\n", - "SXRDCrystal.F 200 370.5 us 99.5 us 97.9 us 3.72x 3.78x\n", - "UnitCell.F_uc 2000 551.5 us 207.7 us 216.1 us 2.66x 2.55x\n", - "SXRDCrystal.F 2000 1.10 ms 767.0 us 981.5 us 1.44x 1.12x\n", - "UnitCell.F_uc 20000 3.02 ms 2.04 ms 2.20 ms 1.48x 1.37x\n", - "SXRDCrystal.F 20000 11.02 ms 7.37 ms 9.38 ms 1.50x 1.18x\n" + "UnitCell.F_uc 200 169.7 us 57.3 us 25.8 us 2.96x 6.58x\n", + "SXRDCrystal.F 200 496.3 us 114.0 us 121.8 us 4.35x 4.07x\n", + "UnitCell.F_uc 2000 407.2 us 150.2 us 301.5 us 2.71x 1.35x\n", + "SXRDCrystal.F 2000 1.56 ms 674.8 us 950.7 us 2.30x 1.64x\n", + "UnitCell.F_uc 20000 3.32 ms 1.40 ms 2.91 ms 2.37x 1.14x\n", + "SXRDCrystal.F 20000 10.34 ms 5.00 ms 8.01 ms 2.07x 1.29x\n" ] } ], @@ -373,7 +391,7 @@ "source": [ "## Benchmark Results\n", "\n", - "Plot the median runtime comparison for the direct structure-factor benchmarks.\n" + "The plot compares structure-factor backend medians. The following table reports the canonical C++ `UnitCell.F_uc` cache runs: uncached, cold cache, and warm cache, together with cache counters and retained-versus-expected numerical bytes.\n" ] }, { @@ -384,7 +402,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABEEAAAG4CAYAAACqxPmiAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAdBVJREFUeJzt3Xd4FOX6//HPbkIJQkICSJEWBFQEBMECKMWCFHvDggqCHjRiQcSComJXVCwRVA6IehS7oh5FLFhBpX1BRREBwQZCAglCQjZ7//7glz2EJDAJs5md7Pt1XV6S3cnMvZvPPsneO/M8ATMzAQAAAAAAVHFBrwsAAAAAAACoDDRBAAAAAABAXKAJAgAAAAAA4gJNEAAAAAAAEBdoggAAAAAAgLhAEwQAAAAAAMQFmiAAAAAAACAu0AQBAAAAAABxIdHrAmJdOBzWH3/8oTp16igQCHhdDgAAAAAA2IWZKTc3V02aNFEwWPb5HjRB9uCPP/5Qs2bNvC4DAAAAAADswdq1a9W0adMy76cJsgd16tSRtOOJTE5O9rgaAAAAAACwq5ycHDVr1izyHr4sNEH2oOgSmOTkZJogAAAAAADEsD1NY8HEqAAAAAAAIC7QBAEAAAAAAHGBJggAAAAAAIgLzAniksLCQhUUFHhdRtypVq2aEhISvC4DAAAAAOADNEH2kpnpr7/+0qZNm7wuJW7VrVtXjRo12uMEOAAAAACA+EYTZC8VNUD23Xdf1apVizfilcjMtHXrVq1fv16S1LhxY48rAgAAAADEMpoge6GwsDDSAKlXr57X5cSlpKQkSdL69eu17777cmkMAAAAAKBMTIy6F4rmAKlVq5bHlcS3ouefOVkAAAAAALtDE8QFXALjLZ5/AAAAAIATNEEAAAAAAEBcoAkSp1q2bKmJEydW+nEDgYDefPPNSj8uAAAAAABVvgmydu1a9e7dW+3atVPHjh31yiuveF3SHg0ZMkSBQCDyX7169dSvXz8tWbLE69IAAAAAAPCtKr86TGJioiZOnKhOnTpp/fr1OvTQQzVgwADts88+Xpe2W/369dO0adMk7ViG9+abb9aJJ56oNWvWeFwZAAAAgHjR8oZ3vS7BkdX3DvS6BPhElT8TpHHjxurUqZMkad9991VaWpqysrK8LcqBGjVqqFGjRmrUqJE6deqk66+/XmvXrtXff/8tSbr++uvVtm1b1apVS61atdItt9xSYnWUmTNnqmvXrqpZs6bq16+v008/vczjTZs2TSkpKZo9e7Yk6YcfftCAAQNUu3ZtNWzYUBdccIE2bNgQ2b5379668sorNWbMGKWlpalRo0a67bbbiu3z559/Vs+ePVWzZk21a9cusm8AAAAAALzgeRPks88+00knnaQmTZqUOV/EE088ofT0dNWsWVNdunTR559/XqFjzZ8/X+FwWM2aNdvLqivXli1b9J///EetW7dWvXr1JEl16tTRM888ox9++EGPPPKInn76aT388MOR73n33Xd1+umna+DAgVq0aJE++ugjde3atdT9T5gwQaNHj9asWbN0/PHH688//1SvXr3UqVMnzZ8/X++//77WrVuns88+u9j3TZ8+Xfvss4++/vpr3X///Ro/fnyk0REOh3X66acrISFB8+bN0+TJk3X99ddH6RkCAAAAAGDPPL8c5p9//tEhhxyioUOH6owzzihx/0svvaSrr75aTzzxhHr06KEnn3xS/fv31w8//KDmzZtLkrp06aL8/PwS3/vBBx+oSZMmkqSNGzfqwgsv1JQpU6L7gFzyzjvvqHbt2pJ2PEeNGzfWO++8o2BwR9/q5ptvjmzbsmVLXXvttXrppZc0ZswYSdJdd92lc845R7fffntku0MOOaTEcW688UZNnz5dc+bMUYcOHSRJkyZN0qGHHqq77747st3UqVPVrFkzLV++XG3btpUkdezYUbfeeqskqU2bNnr88cf10Ucf6fjjj9eHH36oZcuWafXq1WratKkk6e6771b//v1de44AAAAAACgPz5sg/fv33+0b44ceekjDhg3T8OHDJUkTJ07UrFmzNGnSJN1zzz2SpAULFuz2GPn5+TrttNN04403qnv37nvcdueGSk5OjqQdZzaEw+Fi24bDYZlZ5D839enTR0888YQkKSsrS5MmTVL//v319ddfq0WLFnr11Vf1yCOPaMWKFdqyZYtCoZCSk5MjdSxevFjDhw/fbV0PPvig/vnnH3377bdq1apVZNsFCxbok08+iTRhdrZixQq1adNGktShQ4di+2/cuLHWrVsnM4s0qfbbb7/INkceeaQkuf58Fe2vtJ8RAAAAgIoLyt33OdHC+wA4zYDnTZDd2b59uxYsWKAbbrih2O19+/bVV1995WgfZqYhQ4bomGOO0QUXXLDH7e+5555iZ08Uyc7OVigUKnZbQUGBwuGwQqFQifv2RjgcVlJSklq2bClpx5kekydPVv369fXkk09q4MCBOvfcczVu3DhNmDBBycnJevnllzVx4sRIHUlJSZHaytKjRw+99957mjFjRuQMEkkqLCzUwIEDi50JUqRx48YKhUIyMyUmJhbbv5mpsLBQoVBIhYWFklTs/qLbirZxSygUUjgc1ubNm7V161bX9gsAAADEu1bJ/miC+GHeR0RXbm6uo+1iugmyYcMGFRYWqmHDhsVub9iwof766y9H+/jyyy/10ksvqWPHjpH5Rp577rnIpR+7uvHGGzVq1KjI1zk5OWrWrJlSU1OVnJxcbNu8vDxlZ2crMTFRiYnuPZXBYFDBYLDYPotuy8/P17x589SiRQvdcsstkfvXrl0rSZHv6dixo+bMmaNhw4aVeZwjjjhCV155pfr166dq1arpuuuukyQdeuihev3119W6desyH1fR8r2l1ZiYmKj27dtrzZo1Wr9+feSSpG+//VaSlJCQ4OrzlZiYqGAwqJSUFNWsWdO1/QIAAADxbmVOwOsSHElLS/O6BHjM6XvMmG6CFAkEir/wzKzEbWU56qijynVqVI0aNVSjRo0Stxe9wd/1tqJmgNN6nMrPz9e6desk7TgL5fHHH9eWLVt08skna/PmzVqzZo1eeuklHXbYYXr33XcjDZ6iOm699VYde+yx2n///XXOOecoFArpvffeK3bGRyAQUPfu3fXee+9FGiHXXHONrrjiCk2ZMkXnnXeerrvuOtWvX18rVqzQjBkz9PTTTyshISHy/aU97kAgoOOPP14HHHCALrroIj344IPKycmJzGPi9vNVtL/SfkYAAAAAKi4sfzRBeB8ApxmI6SZI/fr1lZCQUOKsj/Xr15c4O6Sqef/999W4cWNJO1aCOfDAA/XKK6+od+/ekhRpVuTn52vgwIG65ZZbii1R27t3b73yyiu64447dO+99yo5OVk9e/Ys9Vg9evTQu+++qwEDBighIUFXXnmlvvzyS11//fU64YQTlJ+frxYtWqhfv36OgxUMBvXGG29o2LBhOvzww9WyZUs9+uij6tev3149LwAAAADgVx2ml35FQqxZetFSr0uImoC5PaPnXggEAnrjjTd06qmnRm474ogj1KVLl8gkoZLUrl07nXLKKZGJUaMpJydHKSkpys7OLvVymNWrV0eW74U38vLytGrVKrVs2ZKfAwAAAOCi1jf91+sSHFlx9wCvS3Ck83OdvS7BkUUXLPK6hHLLyclRamqqNm/eXOK9+848PxNky5YtWrFiReTrVatWafHixUpLS1Pz5s01atQoXXDBBeratau6deump556SmvWrNGIESOiWldmZqYyMzMjk3lW5sSoKB8mRgUAAACig4lR3dUisYXXJTjil+dzZ76ZGHX+/Pnq06dP5OuiSUkvuugiPfPMMxo0aJA2btyo8ePH688//1T79u313//+Vy1aRDc8GRkZysjIiJwJUpkTo6J8mBgVAAAAiA4mRnXXr6FfvS7BEb88nzvzzcSovXv31p6uyLn88st1+eWXV1JFpavsiVHhHBOjAgAAANHBxKjuCsv5oh1e8svzuTPH81dGuQ4AAAAAAICYQBMEAAAAAADEBc8vh/GLcDiscDhc4jYzi/wHbxQ9/6X9jAAAAABUXFD+eJ/jl/cBQZ+ch+CX53NnTmumCVIGVofxD1aHAQAAAKKD1WHcxeow0eOb1WFiFavD+AerwwAAAADRweow7mJ1mOjxzeowfsHqMLGL1WEAAACA6GB1GHexOkz0sDoMAAAAAADATjgTJApa3vBupR5v9b0DK/V4AAAAAAD4EU2QONS7d2917NhRNWvW1JQpU1S9enWNGDFCt912m1avXq309HQtWrRInTp1kiRt2rRJqamp+uSTT9S7d2/NmTNHffr00fvvv68bbrhBP/74o7p166YZM2ZowYIFGjVqlH7//XcNHDhQ//73v1WrVq3Icdu3by9Jev7555WQkKDLLrtMd9xxhwKBgMaPH69XXnlFS5cuLVZvly5dNHDgQI0fP75SnycAAAAAPnFbitcVOJPe3OsK4h5NEIdieYncihx7+vTpuuaaazRv3jzNnTtXQ4cOVffu3dWmTZvIPov2u/P/d779tttu02OPPaZatWpp0KBBOvvss1WjRg395z//0ZYtW3T66afr0Ucf1fXXX1/suBdffLHmzZun+fPn61//+peaN2+uSy65REOHDtXtt9+ub775RocddpgkacmSJVq0aJFefvnlMh8nS+QCAAAA0eGbJXJ9MtMDS+RGD0vk7iU/LZFb3mObmTp06KCxY8dKktLT0/X4449r9uzZSk9Pj+yzaL9F/y8sLFQoFIo8J7fddpuOOOIISdKQIUN0880368cff1SrVq0kSaeffro++eQTXXvttZHjNm3aVA888IACgYD2339//d///Z8efvhhDR06VI0aNVLfvn01depUde7cWZI0depU9ezZU82bNy/zcbJELgAAABAdvlkit3pbr0twpEViQ69LcIQlcuOQn5bILe+xA4GAOnbsWOz7mjRpog0bNkRu2/kxFf0/ISFBiYmJSkhIkCR17tw5cl/jxo1Vq1YttW37v8GnUaNGmj9/fmSbQCCgI488UtWqVYts06NHD02cOFGBQEAJCQm65JJLNGzYMD388MNKSEjQiy++qAkTJuz2MbJELgAAABAdvlkit+Zyr0tw5NdQntclOMISuYjpJXIrcuzq1asX+75AICAzizQ4dt5v0RkYuz7WnfcRDAZVrVq1YvsMBoMKh8MljrPr1zvffvLJJ6tGjRp68803VaNGDeXn5+vMM8/c7WNkiVwAAAAgOnyzRK5Plp5lidzocVozTRAU06BBA0nSn3/+GbkkZfHixa7tf968eSW+btOmTaT5kpiYqIsuukjTpk1TjRo1dM4550QmVgUAAAAAYG/QBEExSUlJOvLII3XvvfeqZcuW2rBhg26++WbX9r927VqNGjVK//rXv7Rw4UI99thjevDBB4ttM3z4cB100EGSpC+//NK1YwMAAAAA4htNEJQwdepUXXzxxeratasOOOAA3X///erbt68r+77wwgu1bds2HX744UpISNDIkSN16aWXFtumTZs26t69uzZu3BiZeBUAAAAAgL0VMC/XdvWBoolRN2/eXOrEqKtWrVJ6ejoTcjrQu3dvderUSRMnTtztdmamAw88UP/61780atSoPe6XnwMAAAAQHS1veNfrEhxZXfM8r0twpEN6c69LcGTpRUu9LqHcdvfefWecCeJQOBwuse5wOByWmUX+w57t6blav369nnvuOf3+++8aMmSIo+e1aJ+l/YwAAAAAVFxQ/nifE5Y/JvIM+qROP76vclozTZAyZGZmKjMzU4WFhZKk7OzsyCopRQoKChQOhxUKhUrch5KKmhW7e64aNWqk+vXr64knnlCdOnUcPa+hUEjhcFibN2/W1q1b3SwZAAAAiGutkv3RBMmq3tbrEhxpkdjQ6xIcycrK8rqEcsvNzXW0HU2QMmRkZCgjIyNySk1qamqpl8NkZ2crMTHR8ZrE8WzOnDl7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idvMLPKfVypy7I4dO2rcuHGSpNatW+vxxx/Xhx9+qNatW0f2WbTfnf+/8+133HGHunfvLkm6+OKLddNNN2nFihVq1aqVJOmMM87QJ598ojFjxkSO26xZMz300EMKBAJq27atlixZoocffljDhw+XJA0dOjSybXp6uh555BEdccQRys3NVe3atct8/GZW6s8IAAAAQMUF5d37nPII++Tz/aBP6vTj+yqnNdMEKUNmZqYyMzNVWFgoScrOzlYoFCq2TUFBgcLhsEKhUIn7KlN5j21mat++fbHva9iwodatWxe5befHVPT/wsJChUKhyHPSrl27yH0NGjRQrVq11Lx582K3ffPNN5GvzUyHH3545Psl6fDDD9dDDz2k/Px8JSQkaNGiRbrjjju0ZMkSZWVlRYK8cuVKtWvXrszHHw6HtXnz5hKX3wAAAFQV5zw11+sSHJlxaTevS4CLWiX7owmSVb2t1yU40iKxodclOJKVleV1CeWWm5vraDuaIGXIyMhQRkaGcnJylJKSotTUVCUnJxfbJi8vT9nZ2UpMTFRiondPZXmPHQgEVL169WLfl5CQIEmqXr165Oui+4vO/Ci6rWjbpKSkyDYJCQmqVq1aiX2aWeS2QCCgQCBQ6nETExOVl5engQMHqm/fvnruuefUoEEDrVmzRv369VM4HC7zcSYmJioYDColJUU1a9Ys13MBAADgFytzAl6X4EhaWprXJcBFvsldzeVel+DIr6E8r0twxI+vY6fvi2mCOBQMBhUMBkvcVvTGPhDwbnCoyLHLqnnfffeVJP3111+R+//v//6v2PcU3b7rv3etpbTbvv766xJft2nTRomJifrpp5+0YcMG3XvvvWrWrJkkacGCBbutd+f7SvsZAQAAVBVh+ePNKH+PVS2+yZ38cflG2Cd1+vF17LRm/z0yRFVSUpKOPPJI3Xvvvfrhhx/02Wef6eabb3Zt/2vXrtWoUaP0008/6cUXX9Rjjz2mq666SpLUvHlzVa9eXY899phWrlypmTNn6o477nDt2AAAAACA+EYTBCVMnTpVBQUF6tq1q6666irdeeedru37wgsv1LZt23T44YcrIyNDI0eO1KWXXippxxwizzzzjF555RW1a9dO9957ryZMmODasQEAAAAA8S1gXi5r4gNFc4Js3ry51DlBVq1apfT0dOai8BA/BwAAEA9a3vCu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IcuXL9f333+vgQMHqnr16pHbi7J3xBFHqFevXnr99dfVunVr5efn65prrlFGRoZq1qzpVdmoghjvEG2Md4gFjHWINsY6uIUzQeCJuXPnqkePHlqzZo0kqUaNGtpnn31Uu3ZtSf9bzopfkqiop556SmeffbaWL19e4r6iT6sGDx6sJk2aKBwO64MPPtC1115b7Jcq4AbGO0Qb4x1iAWMdoo2xDm6hCYKoK2197rZt22ru3Ll6/vnnI7c1bdpUr7/+uqQda3ubmbZv366FCxcqJyen0uqFvxVNejVhwgTtu+++evLJJ/XPP/8U26boE4OWLVvq9NNPV8OGDTV79uxi9wEVwXiHysR4B68w1qEyMdbBbSQCUZeQkKC///5by5Ytk7SjU1uvXj1NmDBB999/v3788UcdddRROvTQQ/Xuu+9GBqxAIKBPPvlE48aN0x9//OHlQ4CPBAIBhUIhSdL999+vp556St9++22J7Yp+oZ5xxhk64IADNHPmzMgnC7te1ww4xXiHysR4B68w1qEyMdbBbTRBUCmGDx+uU089VVu3bo10Y0eNGqUGDRro7rvvliSNHj1azZo104knnqiTTz5ZAwcO1Omnn65OnTrpwAMP9LJ8+MDOn0oVLcl3/vnnq0OHDnrggQdKnZHezJSSkqKzzjpLhYWFevLJJyXxiQH2DuMdoo3xDrGAsQ7RxliHaCENqLDyrLV9zz336Pfff9dLL71U7HsnTJigF154QR999JEOOugg/ec//9Hjjz+uTp06qWXLlvruu+905513RqV+VA1FvyCLTrP9888/lZ+fH7n/ySef1HvvvadZs2YV+xRg52uSBw4cqDZt2ujbb7+NXMsM7IzxDrGA8Q7RxliHWMBYh2gLWHlGO+D/KywsjMzyvW3bNiUlJRW7rTTXXnutXn75ZX377bdq1KiRJGn79u064IAD1LVrVz322GOR23c9VjAYZCIt7NakSZM0YcIE1atXTwkJCZo+fbpatWqlxMREnXPOOfruu+/09ttvKz09XZL01Vdf6aOPPtI555yjNm3aaNWqVapXr56Sk5M9fiSINYx3iDWMd4gGxjrEGsY6RAtngqBcirqtCQkJys7O1oknnqgLLrggctvu3HzzzQqFQpo4cWLktp9++klJSUl67bXX9OGHH5Z6vISEBH5JIsLMip0emZWVpfPPP18PPfSQbr75Zk2cOFENGjTQxRdfrAULFkiSJk+erJ9//lkvvPBC5JOEn3/+Wbfddpu+++47SVJ6erqSk5O5ZhQRjHfwGuMdKgNjHbzGWIdKZ0AFjB071hITE23gwIG2cuVKx9/37LPPWkJCgt199922ZMkSy8jIsGnTptnMmTMtPz9/j98fDoctHA7vTenwqezsbLv77rsjX2/YsMHy8/Ptt99+sxtuuMGWLFliZmb//POPnXrqqZaQkGBjxoyxdevWmZnZ7bffbo0aNbJvv/02so8PP/zQ0bHJXXzzarxD/PJyvEP88nKsKywsrGjZ8DGvxzpyF79ogqBcvvzyS2vWrJm1atXK5syZU6F9ZGRkWIcOHSwlJcWOPPJIW716deS+3b3RLCgoiPx7+/btFTo2/GvhwoUWCARsxowZdvXVV1vdunXtww8/tC1bttjPP/9sZmYTJ060evXq2fnnn2+jRo2y+vXr28yZMyP7qFatmg0dOtS2bt1abN/kDqXxcrwrLCzkj7M45tV4R+7iE2MdvMJYB6/QBEG5TJ061RITE+3VV18tdvuaNWv2+KlB0WBTUFBgf/31ly1atKjcxw+Hw3bTTTfZ2LFjy/UpBfypsLCw2FkYRx55pNWqVcvat29vX375ZbFtP/vsM+vUqZPNmDHDzHbkbJ999rGLL77Yli1bZmZm8+bNs7Vr15a7DnIXn7wa70KhUOTfa9euJXNxwuvxjtzFL6/Gup3fhC5cuNCuuuoq+/HHH50XDl/yeqwjdzAzY04QlMvQoUPVs2dPvfDCC8rNzZUkDRkyRF27dtWKFSt2+71FS1MlJCSoYcOG6tSpk6Tiy1/trOh2+/9z97766qtq1KiRPvjgA6Wnp6ugoMCNh4QYZP//2tCiSdPy8/O1bds2rVmzRoWFhTrnnHPUvXt3STuuLTYzvf3226pdu7aOOeYYSdLbb7+t1NRUvfTSS1q2bJkk6fDDD1fTpk13e20ouUORyhzvdpaQkKC8vDxdcsklOuyww/T6669r/fr1e/dgELO8HO92Ru7iV2WPdUW/X4PBoP755x8NHjxYffr00R9//KHff/+d37NVlNdjHblDMV51XxC73nnnnVJvL/qUaM6cObbffvvZGWecYfXq1bPjjz++ROe2rO/f9dS00k5VW716ta1fv77YbX/99ZcdccQRds899zh9GKgCsrOzbcSIEXb11Vfb33//bWZmTz75pNWpUyfStS/K0G233WatWrWyGTNm2MKFC23gwIH29ttv2wcffODoWOQuPnk93pmVvCb5u+++swMPPNCOPfZY++STT+yXX37htN04UJnjnRm5izexMNbl5OSUuO3WW2+1I444InLpA/NvVX2VPdaRO5SGJgiKmT9/fuTaPLOyJwwaMWKEJSYm2l133bXHfYbD4WKn2b7//vuR/e/qo48+sgYNGtisWbPsr7/+sl69etnixYvtyy+/tLZt29rnn39u27dvt48//thmzZplb7zxhv31118VeKSINUVZK/pFNGnSJEtOTrbjjz/eXnnllcgvxry8PDv44INt8ODBxbIVDoetV69e1qZNG6tVq5ZdcMEFxebz2N0vOHIXn7we77766iu7//77I1//3//9n5mZTZs2zfr06RO5vrlofzvvF/7m5XhH7uKP12NdXl6edevWzcaMGWNmZq+++qqNGzfOzMw6dOhg48ePNzOzX375xZYtW2Zz5syxjRs3On+AiFlejnXkDrtDEwT222+/RTqqeXl5NmLECNt///1L3bZoMPvtt9+sRYsWdtttt9nmzZvNrPSBaOeBatWqVda/f39LSUmxxx57rMx6evbsaQceeKAlJSXZcccdZ5s3b7a//vrLOnToYIceeqi1bt3aBgwYYM2bN7c2bdrYSSedVOHHjti0atUq69Spk02bNq3U+2fOnGkJCQn26aefRm4r+jRh8eLFtmrVqsjtTrv75C4+xNJ4N2XKlMgbk549e1pqaqrl5ubajTfeaOnp6fbf//7X7r//fhs9erQdeeSRNmrUKFu8ePFePX7Ensoe78hdfIilsc7M7D//+Y/VqVPH2rdvb3Xq1LEpU6aYmdmQIUOsVatW1qtXL+vXr58dccQRFggE7LTTTrO5c+dW7MEjJnnxtx25Q1logsDGjBljCQkJ9scff5iZ2bJly6xevXp25513mlnJTwyKvr7tttvsoIMOslmzZu12/+Fw2K666iqrXbu2DR48uNiM4UWTI5nt+CW9adMma9u2rSUmJtqoUaOK7WfJkiU2ceJEe/PNN+3TTz+1rKwse/zxx+2AAw5gQqMqIDs7284//3z7+++/7bPPPrO2bdvap59+ajk5Ofbuu+/aCy+8YNOnT4/8QjzxxBOtefPmdt9991mHDh3stNNOK7Z6y+5m/iZ38cvL8a409evXt4SEBBs0aFAk21u2bLETTjjB0tLSbODAgXbllVfayJEj7eijj7aLL764Qo8bsaUyx7vSkLuqz+uxbtf9jxkzxgKBgB1wwAGWlZUVuT0/P9/Gjx9vo0ePttdff92++eYb+/rrr61x48b2n//8p2IPHjGjssc6cgenaILANm3aZC1atLBrr73WzHb8YrvvvvssKSkpsg530RvGcDgcGWBCoZB17tzZLrnkkjJnZX722WctJSXFjjrqqBLLru18GuU///wT+ffixYvt/PPPt169etk333yz29qvv/56O/PMM8v5iOG10k6v/vzzzy09Pd0WLFhgP//8sx133HHWsmVLa9WqlZ144omWnp5urVu3tr59+5qZWW5url144YXWvXt3u+WWWyp0bHIXf7wa73ZuvBV57733rGPHjhYIBOztt9+ObGdmlpWVZevWrbOCggLLy8szM7MBAwbY1VdfXaxGxD4vxztyF79iZawr2u/ChQtt0qRJFggEbPbs2cXu29XatWutffv29sUXX1T04cMDsTTWkTvsCU2QOFc0YEybNs1q1KgRuTb4zz//tPbt29v5559fbLsiRadCPvPMM1azZk17//33S+x7/fr1dtNNN9mTTz5ZbMDZeZDMycmxIUOG2IABA+zWW2+NnHL7888/W/PmzW3cuHGRUzILCgqssLDQ3nrrLXvnnXfsuOOOswYNGtjrr7/u1tOBKNtd9/6vv/6yevXqRU6DXL58uT399NP26aef2rfffms5OTn29NNPW9OmTe2XX34xM7OtW7dafn5+ZB+7u3ad3MGL8W7X/X3//ff29ddf27Zt2yK3nXnmmXbIIYcUO9W3yObNmy0cDtsLL7xgBx54oD3//PMVeOTwgpfjnRm5i2dejHU7L3lqtqPZNnjwYLvtttvs559/juT11FNPtS5dupSYrHLt2rX20Ucf2eTJk61x48Y2aNAgy87O3stnApXBy7GO3KGiaILEoZ2v5SwSDoft8MMPt5NOOikymL366qsWCARs3rx5ZrZjkNu2bZs99NBDkdmUzcw+/PDDMo+1u4Fr8eLF1q9fPzv22GNtzJgx1rZtWzvooIMib0jHjh1r7dq1s48++qjY991xxx12yCGH2CWXXGK5ubnOHzg8tfMvqR9//DHyCdLOeezVq1fkU6vSjBgxwoYMGVLi9tI+7SwLuYsvXo53O2cyJyfHzj77bEtKSrJmzZrZscceay+//LKZ7XhjkpiYaPfee2+xP/xWrVplgwcPtqOPPtpSU1Nt8uTJFXgG4AUvxztyF59i5W+77du325133mn169e3c845x1q1amXt2rWzhx9+2Mx2fNhQo0YNmzRpUrHv+/LLL61bt27Wrl27MueMQOyJlb/tyB3KiyZIHNl1IPn8889txYoVkWvtPv30UwsGg/bWW2+Z2Y5T0gYOHGiHH3545Hs2bdpkgUDArrjiimLX6JW2/7Js2LDBzj33XOvXr5+df/75kU+mli1bZieccIJ16dLFzHYMaJ06dbJzzjnHFi5caJmZmXbllVea2Y4/3oqU9osf3tk1Bzv/sfTbb79Z79697d///rf17t3bunfvbjfffLOZ7ZibY/DgwXbVVVdFzr7Iy8uzN954w5599lnr0qWLtWzZ0j7++OMK1UXu4kusjHdmZrNnz7Z77rnHBg0aZN999519/PHHdtZZZ1mbNm3ss88+MzOzG2+80Ro3bmwLFy40sx3zM+Tk5Ni0adPsnnvuKZY3li2NHbE63pmRu3gRS2Pdo48+aieddJKdf/75kUsK8vLy7LLLLrNDDz00cunM9ddfbw0bNrSFCxfan3/+aWPGjLElS5bY999/X2x/rEwUO2J5rCN3qAiaIFXQrqeG7erFF1+0/fbbz7p06WINGza0kSNH2vLly83M7Oyzz7b27dtHThebO3eu7bPPPvbcc89Fvn/WrFn266+/7lWNl1xyiVWrVs0yMjKK1f3555/bPvvsY6+++qqZ7fjEokePHrbffvtZkyZN7N///ndk+/JOBIfo2t3PIhwO2w8//GC33367nXTSSbZ582bLzs62adOmWVJSko0dO9a2bt1qd911V7E/zMx2nJnRs2dPGzt27F7XSO6qnlgf77744gsLBALWqFEje/HFFyO3L1u2zE4//XTr06dP5LYWLVrYcccdZyNHjrTq1avbPffcU2xfNN5iR6yPd+Su6on1sc7sf7lr1qxZsWXkFy5caCeffLINHTo0cttBBx1k7du3t+rVq1u/fv2KXYLAm9DYEetjnRm5Q8XQBKlidh6s/v77b8vKyir2S/PTTz+1gw8+2CZOnGiFhYX2ySef2BFHHGG9evUyM7PVq1fbPvvsYw8++KCZ7fjj57LLLrNAIFBicKjIG8GifaxcudI6depk/fr1KzZb88aNG61v37520003RW777bff9qpDjOjbOWMzZsywjIwMu/POOyPLjF133XUWCASsd+/etmbNmmLfO2PGDDv22GPt6KOPtmeffdbatGlTbDK2DRs2FLuGsyJ/kJO7qimWxrvdvTkZOXKkBYNBe+mll4rt79lnn7WDDjoo8qn8l19+aSNHjrQ+ffrYzJkzHe8flSuWxjtyFx9iaawrsms2ir5v6NChVq9evRLLKl9zzTXWr1+/SL7/+OMPe+utt+zrr792dDxUvlga60qryYzcoeJoglRBOTk5dumll1qfPn3sxBNPtNdeey1yeuM111wTWdViy5YtdvHFF1tycrJdd911kZngb7/9dmvUqFHkE4GVK1faM8884+jYjz/+uJ199tl29913l7nCRtEAdt9991nXrl2Lfcq+bds2a9GiReQavrIm7ULs+fbbb619+/bWtm1bGzdunN1888327bffmtmOZWY7dOhgPXr0KDYbfZHVq1dbz549rU6dOtasWbNi1yUX2d21oeQufnk53v3zzz+2dOnSMvNR9Obi119/tWbNmtm1115rGzdujNz/xRdf2L777lvsj7ad52bYecUGxBYvxztyF5+8HOv++usvu+mmm2zChAn2+eefl7pNUWY2bNhg1apVszvuuKPYCmw333yztWzZssy5S/gUPjZ5OdaRO0QTTZAqZtq0aZaammonnniiffzxx/bCCy/Ypk2bLBwOW15enp199tn29NNP24MPPmh169a1vn372qJFi4rtIz8/3/bZZx8bNmyYo2OGw2H7+++/rV+/ftayZUu77LLLrGvXrlanTh17/fXXIwNP0SBXNGBt3rzZjjvuOGvdurW99NJLtmLFCnviiSesWbNmkWWs4A/fffedde7c2a644orIqio7KygosAcffNBq1Khhq1evNrOSjYasrCwbO3asJSYmRiYl3dNpmOQuvnkx3hW5++67rX79+ta2bVvr2rWrTZ06tdTtinJ36623Wps2bWz69OmR+/773/9aixYtIqes74w/zGKXF+NdEXIXn7wc66ZPn2777LOPnXDCCdahQwdr0qRJsbMmd1aUn1tvvdVq1apljz32mP3xxx/266+/Wvfu3e3aa6+lweYjXo515A7RRhOkClm6dKl169bNHnjggRKDUNHXw4cPt0AgYO3bt7fXXnstcv+WLVvsqaeeikwoNGfOHPvxxx8dH3vOnDmWnp5uK1asiNx2zjnnWI8ePeyNN94oVoPZ/wbA119/3VJTU61FixZ22mmnWcuWLYt9Qg9/uPrqq+3QQw+1tWvXlvpzNtsxqWiHDh1KXZqv6N9//PGH9evXr8Q16WUhd/HLy/Fu6tSp1qpVK/vvf/9rs2fPtuuuu86CwaA9//zzkU9dd/1UrKCgwDp16mQpKSk2YsQIu+OOO6x27do2fPjwYsuWIvZ5Nd6Ru/jk5VgXDofthBNOsBtvvNHMdiyP+8ILL1ggELCpU6fudhLVVq1aWSAQsHPPPddat25tPXv2tD/++KN8Dx6e8mqsI3eoDDRBfKis6+GuuOIKa9Wqlf3+++8lti/aZsWKFVajRg277777im3z7LPPWr9+/eyTTz7Z7bHKcscdd1inTp0sOzs78j1r1661Pn362LnnnhtZVaO0/Z133nl2xhlnRCalLO+xEX07T8i26y+43Nxc69Spk1111VV73M8rr7xigUAgci1maZ35jh072hNPPFHiWKUhd1VfLI13RaftnnPOOZFTz4sMGzbMOnfuXOrZREWfUr3xxhtWrVo1Gzp0qI0cObLYmxXEjlgb78hdfIilsa7o/mXLlllSUlJkDogil112mbVq1arE2SZm/7t89O2337ZAIGCvvfZase34HRs7Ym2sI3eoTEHBdwKBQLGvg8EdP8Zvv/1Wxx9/vJo0aaJQKFRs+2AwKDPT/vvvr5tuukmPPPKIjjnmGD3yyCMaOHCgRo4cqf79+6t37967PZYkffrpp1q+fLk2b94cua1p06b65ZdflJSUpEAgoFAopKZNm+rcc8/VTz/9pE8++aTE/goLCyVJI0aM0OrVq7V48eLIfeFwuNRjo/IVFhYqEAgoEAgoJycncpu04+eZkJCgX375RXXr1i12XxEzi/z7xBNP1EknnaThw4dL+l92i3z++ef69ddflZKSEtl/EXIXn7we73Y9diAQ0IoVK5Seni5JysvLkyTdf//9Kigo0FtvvaWsrKxi35eQkCBJOvXUU3X00Udr27ZtGjlypE4//XSFw+ESrxl4J1bGu52Ru/jg9Vi3detWfffddwqFQpH7GzdurDp16mjp0qWSpO3bt0uSHnnkEWVnZ+v1118vlnlJSkxMlLQj/127dtWTTz6pVq1aSVKxfcNbsTLWkTt4xrv+CypqxowZ9tBDD0W+Luq89u3b17p167bb7y06hWzmzJk2ePBgO/vss23o0KH2999/R7Ypq1u6ZMkSa9++vTVq1Mhat25t7dq1s6+++srMdkxelJaWFqmraJK17du32yGHHGJjxozZ7b5Hjx5tXbp0iVzCAO/t3MnPycmxa665xk455RQ77LDDrEePHvb666/bhg0bzMzs+OOPt0MPPbTMfW3atMmmTZtmZjtmsa9fv7799NNPxbbZsmWLXXjhhSVOlyR38c2r8c6s7Al3b775ZmvRokWJ40ycONGaNWtm33//fYl9FX0qP3fuXGvevLk98sgjxSZvg7diZbwzI3fxysuxbte5ZoouD928ebNdcMEFdsIJJ0QuuSr6PXv33Xdbo0aNSt1fUe6WLFligUDAJk2axHwzMSKWxjpyBy/RBPGBr776ysaNG2e5ublmtuPUyIMPPtgmTJhgDRs2tDZt2piZ2YgRI6xZs2b29ttvm1nJFS1++uknu+2222z9+vWR23a+JjgUCu12NvqTTjrJhg8fbr/99pstXbrUevXqZd27d48sqXfttddaampq5A+sol/Ko0aNsq5du5a6351P5ezXr599+eWX5XtyEHW33nqrJSUlWf/+/W3y5Mk2duxYGzhwoNWuXdtGjx5tZmZPPfVU5FrNXRUWFtoDDzxgw4cPt4KCAisoKCjzes5df1mRu/jj9Xi3pwl3w+Gwffzxx9aqVSubOHGimRVfWaNOnTr23HPPmVnJU4KLjnf++edbmzZtSn3TCm95Nd6Ru/jj9VhXpLS5ZgKBQCRP06ZNs86dO9ukSZOKHX/p0qVWv379MldkK8rhaaedZsOGDWOVtRjj5d92ZuQO3qMJ4gP33HOPPfnkk5HBZPny5ZacnGwJCQk2duzYyHbffvutNWrUyE455RTbsmVLif3cddddkcFqV3uaNfmXX36xevXqFZs/4eeff7bTTjvN+vbta1lZWfbLL79YmzZtbNCgQcW+98wzz7RLL73UzEr/JKK06xHhvZ9++sk6depk9erVi0yqtrMLL7zQGjRoYK+//rr9888/duKJJ1q9evXszTffNLMdv7C2bdtmmZmZdvjhh9srr7xS7PuddOfJXfyJhfGurAl3jzzySHv//fctPz/fLr/8cmvbtm3kk9bCwkLbvHmzdejQwe66665S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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAABEEAAAG4CAYAAACqxPmiAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAdFBJREFUeJzt3Xd8k/X6//F30rKhpWwQKPQAKgKCoALKciDDvcADIstZPSoiDhQVB+JBxKMFRY84cS9cBz0quEBF5Au4QDYuhBZaRkvTXL8/+CWHWgp3StI7d/N6Ph48tEl631fSdz5Jrtz35+MzMxMAAAAAAEAF53e7AAAAAAAAgPJAEwQAAAAAACQEmiAAAAAAACAh0AQBAAAAAAAJgSYIAAAAAABICDRBAAAAAABAQqAJAgAAAAAAEgJNEAAAAAAAkBCS3S4g3gWDQf3666+qVauWfD6f2+UAAAAAAIC/MDPl5eWpSZMm8vtLP96DJsgB/Prrr2rWrJnbZQAAAAAAgAPYsGGDmjZtWur1NEEOoFatWpL2PJApKSkuVwMAAAAAAP4qNzdXzZo1C3+GLw1NkAMInQKTkpJCEwQAAAAAgDh2oGksmBgVAAAAAAAkBJogAAAAAAAgIdAEAQAAAAAACYE5QaKkqKhIhYWFbpeRcCpVqqSkpCS3ywAAAAAAeABNkINkZvr999+1detWt0tJWLVr11ajRo0OOAEOAAAAACCx0QQ5SKEGSIMGDVS9enU+iJcjM9POnTu1adMmSVLjxo1drggAAAAAEM9oghyEoqKicAOkbt26bpeTkKpVqyZJ2rRpkxo0aMCpMQAAAACAUjEx6kEIzQFSvXp1lytJbKHHnzlZAAAAAAD7U+GbIBs2bFDv3r3Vtm1bdejQQS+//HLU98EpMO7i8QcAAAAAOFHhT4dJTk7WtGnT1LFjR23atElHHXWUBgwYoBo1arhdGgAAAAAAKEcV/kiQxo0bq2PHjpKkBg0aqE6dOsrOzna3qDjQokULTZs2rdz36/P59MYbb5T7fgEAAAAAcL0J8sknn+i0005TkyZNSv2APH36dLVs2VJVq1ZV586d9emnn5ZpX4sWLVIwGFSzZs0OsurYGj58uHw+X/hf3bp11a9fPy1dutTt0gAAAAAA8CzXT4fZsWOHjjzySI0YMULnnHNOietffPFFXXPNNZo+fbqOO+44Pfroo+rfv7++//57NW/eXJLUuXNnFRQUlPjd999/X02aNJEkbdmyRcOGDdPjjz8e2zsUJf369dOsWbMk7VmG95ZbbtGpp56q9evXu1wZAAAAgETR4sZ33C7BkbX3DnS7BHiE602Q/v37q3///qVeP3XqVI0aNUqjR4+WJE2bNk1z587VjBkzNGnSJEnSN998s999FBQU6KyzztJNN92k7t27H/C2ezdUcnNzJUnBYFDBYLDYbYPBoMws/C+aqlSpooYNG0qSGjZsqHHjxqlXr17atGmT6tevrxtuuEFvvPGGNm7cqEaNGunvf/+7JkyYoEqVKoW3MWfOHN15551avny5atasqZ49e+rVV18NX7933bNmzdK1116rl19+WSeffLK+//57XX/99frkk09Uo0YN9e3bV1OnTlW9evUkSX369FH79u1VtWpV/fvf/1blypV16aWX6vbbbw9vf+XKlRo9erS++uorZWRkhE+/ifbjFdrevv5GAAAAAMrOr+h+zokVPgfAaQZcb4Lsz+7du/XNN9/oxhtvLHZ537599cUXXzjahplp+PDhOuGEE3ThhRce8PaTJk3SHXfcUeLynJwcBQKBYpcVFhYqGAwqEAiUuO5ghD7Mh7a5fft2Pfvss2rVqpVSU1MVCARUo0YNPf7442rcuLGWL1+uyy+/XDVq1NDYsWMlSe+++67OOecc3XjjjXriiSe0e/duvffee8XqDO1j6tSpuu+++/TOO+/o2GOPDa+oM3LkSE2ePFm7du3S+PHjdf755+v999+XtOdxffrpp3X11Vfrs88+08KFCzV69Gh17dpVJ510koLBoM4++2zVq1dPn376qfLy8nTddddJkoqKiqL6eAUCAQWDQW3btk07d+6M2nYBAACARJeR4o0mCPM+Ii8vz9Ht4roJsnnzZhUVFYWPiAhp2LChfv/9d0fb+Pzzz/Xiiy+qQ4cO4flGnnnmGbVv336ft7/ppps0ZsyY8M+5ublq1qyZ0tLSlJKSUuy2+fn5ysnJUXJyspKTo/dQ+v1+vfvuu0pLS5O055Shxo0b66233lLlypUlSRMmTAjfvlWrVlq5cqVeeumlcMNo8uTJGjx4sO68887w7Tp37lxiP7feequefvppffzxx+HH5LHHHtNRRx2le++9N3zbJ554Qs2bN9fq1avVpk0b+Xw+dejQIdwwOvzww/XII49o3rx56tevn95//339+OOPWrNmjZo2bSpJuueeezRgwAAlJSVF9fFKTk6W3+9XamqqqlatGrXtAgAAAIluda7P7RIcqVOnjtslwGVOP2PGdRMkxOcr/sQzsxKXleb444+P6NCoKlWqqEqVKiUu9/v98vv9JS7bewLTaOrTp49mzJghaU9Xc/r06RowYIC++uorpaen65VXXtG0adP0888/a/v27QoEAkpJSQnXsWTJEl188cX7rWvq1KnasWOHFi1apIyMjPDlixcv1scff6xatWqV+J3Vq1fr0EMPlSR16NCh2PYbN26sP//8Uz6fTz/++KOaN29ebBLa0KlI0X68Qtvb198IAAAAQNkF5Y0mCJ8D4DQDcZ2UevXqKSkpqcRRH5s2bSpxdEhFU6NGDbVq1UqtWrXSMccco3//+9/asWOHHnvsMS1cuFCDBw9W//799fbbb+vbb7/V+PHjtXv37vDvV6tW7YD76NGjh4qKivTSSy8VuzwYDOq0007TkiVLiv1buXKlevbsGb7d3vOPSHuaEaGG077m/Ih2owgAAAAAgEjE9ZEglStXVufOnfXBBx/orLPOCl/+wQcf6IwzzijXWsp7YlSpZCPB7/dr586d+uyzz5Senq6bb745fN3atWuL/U6HDh304Ycfavjw4aVu/+ijj9aVV16pfv36ye/36/rrr5ckderUSa+99prS09P3eUhRaB+l3W8z0+GHH67169frl19+Ca/QE5rHhYlRAQAAAG9gYlR4hWcmRt2+fbt+/vnn8M9r1qzRkiVLVKdOHTVv3lxjxozRhRdeqC5duqhbt26aOXOm1q9fr8suuyymdWVlZSkrK0tFRUWSyn9i1Pz8fG3cuDG87xkzZmj79u0aMGCAtm3bpvXr1+u5555Tly5d9N5774XnOwnVMX78eJ1yyilq0aKFzj//fAUCAc2dOzc8cWpoP0cffbTeeustnXrqqfL7/br66qt16aWX6vHHH9fgwYN13XXXqW7dulq1apVeeuklPfLII0pKSgo3Hv460Wro8ejdu7fatGmjYcOGafLkycrLy9P48eMlMTEqAAAA4BVMjAqv8MzEqIsWLVKfPn3CP4cmJb3ooov05JNPatCgQdqyZYsmTpyo3377Te3atdO7776r9PT0mNaVmZmpzMxM5ebmKjU1tdwnRp07d66aN28uSapVq5YOO+wwvfTSSzrxxBMlSddcc42uueYaFRQUaODAgbrlllt0xx13hOs48cQT9dJLL+muu+7SP//5T6WkpKhnz57F6vT7/UpOTlbPnj319ttva+DAgapUqZL+8Y9/6LPPPtONN96ogQMHqqCgQOnp6TrllFNUuXLlYvOg/HV7oW1K0uuvv67Ro0fruOOOU4sWLfTggw+qf//+TIwKAAAAeAQTo8IrnH7G9FkszuOoQEJNkG3btu2zCbJmzRq1bNmSD98u4u8AAAAAxEaLG99xuwRH1t470O0S4LL9fXbfW1xPjAoAAAAAABAtNEEAAAAAAEBCcH1OEK9wY3UYOMPqMAAAAEBssDoMvMIzq8PEKzdXh0FkWB0GAAAAiA1Wh4mukXNHul2CI0+c8oTbJUTMM6vDxCs3V4dBZFgdBgAAAIgNVoeJrnWBdW6X4IhXHs+9Of1Mzid3h0LLv/71sr2Xi4U7Qo//vv5GAAAAAMouKG98zvHK54CgvHHajlcez705rdl79wwAAAAAAKAMaIIAAAAAAICEQBMEAAAAAAAkBOYEcYglcuMXS+QCAAAAscESudHl98hxCF55PPfGErkH6WCWyG196/vlWuvKO/uW6/7iDUvkAgAAALHBErnRlZ6c7nYJjnjl8dwbS+QeJC8tkRvpvvv06aP27duratWq+ve//63KlSvr0ksv1e233661a9cqIyNDixcvVseOHSVJW7duVZ06dfTRRx+pd+/emjdvnk444QS99957uummm/Tjjz+qW7duev755/XNN9/ouuuu0y+//KKBAwfq8ccfV/Xq1cP7PeKIIyRJzz33nJKSknTZZZfpzjvvlM/n08SJE/XKK69o6dKlxert0qWLBgwYoIkTJ5Z6/1kiFwAAAIg+lsiNLpbIjR2WyI2yeF4ityz7fvrppzVmzBh9+eWXWrBggYYPH67jjz9erVu3Dm8ztN29/7v35XfccYcefvhhVa9eXeeff74GDRqkKlWqaPbs2dq+fbvOOussPfzww7rhhhuK7XfUqFH68ssvtWjRIl1yySVq0aKFLr74Yo0aNUoTJ07UokWLdPTRR0uSli5dqm+//VYvv/xyqfeTJXIBAACA2GCJ3OhiidzYcVozTZAE1aFDB912222SpNatW+vhhx/Whx9+GG6COHHXXXfpuOOOkySNGjVKN910k1atWqWMjAxJ0rnnnquPP/64WBOkWbNmeuCBB+Tz+XTooYdq2bJleuCBB3TxxReradOmOuWUUzRr1qxwE2TWrFnq1atXeJsAAAAAAJSV99o7iIoOHToU+7lx48batGlTmbfRsGFDVa9evVizomHDhiW22bVr12JHdHTr1k0rV64Mz71y8cUX6/nnn1d+fr4KCwv13HPPaeTIkRHVBQAAAADAvnAkSIKqVKlSsZ99Pp+CwWD4EKK9V7spLCw84DZ8Pl+p24zEaaedpipVquj1119XlSpVVFBQoHPOOSeibQAAAAAAsC80QVBM/fr1JUm//fabOnXqJElasmRJ1La/cOHCEj+3bt1aSUlJkvZMZnPRRRdp1qxZqlKligYPHhyeWBUAAAAAgINBE8ShYDBY4qiGYDAoMwv/c0tZ9l1azVWrVlXXrl117733Kj09XZs3b9Ytt9xS7HdCv/fX//9rLfu6bMOGDbr22mt16aWXavHixXrooYc0ZcqUYrcZNWqU2rZtK0n67LPPDnj/QnXs628EAAAAoOz88sYSuV75HOD3yIwUXnk89+a0ZpogpcjKylJWVlZ4roqcnBwFAoFityksLFQwGFQgEChxXXmKdN+hpsHevxdqIAQCAT366KO65JJLdPTRR6tNmzaaNGmSBgwYoKKiIgUCgfBjsvf9DgXur9vcez9mpqFDh2rnzp069thjlZSUpCuuuEIjR44s9nstW7ZUt27dtGXLFnXu3PmA9y8QCCgYDGrbtm3auXNnRI8FAAAAgNJlpHijCZKdne12CY6kJ6e7XYIjXnk895aXl+fodj5z8xAGD8jNzVVqaqpycnKUkpJS7Lr8/HytXbtWLVu2VNWqVV2q0Dv69OmjI488UtOmTdvv7cxMhx9+uC655BKNGTPmgNvNz8/XmjVr1KJFC/4OAAAAQBS1uvldt0tw5Od7BrhdgiOdnunkdgmOfHvht26XELHc3FylpaVp27ZtJT67740jQRzy+/0l1h32+/3y+XzhfziwAz1WmzZt0jPPPKNffvlFI0eOdPS4hra5r78RAAAAgLILyhufc7zyOSAob5xm4pXHc29Oa6YJgrjSsGFD1atXTzNnzlRaWprb5QAAAAAAKhCaICg38+bNO+BtODsLAAAAABAr3jvGBQAAAAAAoAxoggAAAAAAgIRAEwQAAAAAACQE5gRxKBgMKhgMlrjMzML/4I7Q47+vvxEAAACAsvPLG59zvPI5wO+R4xC88njuzWnNNEFKkZWVpaysLBUVFUmScnJyFAgEit2msLBQwWBQgUCgxHUoP4FAQMFgUNu2bdPOnTvdLgcAAACoMDJSvNEEyZ7aze0SHElvnO52CY5kZ2e7XULE8vLyHN2OJkgpMjMzlZmZqdzcXKWmpiotLU0pKSnFbpOfn6+cnBwlJycrOZmH0i3Jycny+/1KTU1V1apV3S4HAAAAqDBW5/rcLsGROlVXuF2CI+sC+W6X4EidOnXcLiFiTj+T88ndIb/fL7/fX+Iyn88X/gd3hB7/ff2NAAAAAJRdUN74nOOXN07fCHqkTi9+rnJas/fuGQAAAAAAQBlwJEgs3J5azvvbVr77AwAAAADAgzgSJEEFg0FNnjxZrVq1UpUqVdS8eXPdfffdWrt2rXw+n1544QV1795dVatW1RFHHKF58+aFf3fevHny+Xx65513dOSRR6pq1ao69thjtWzZMvfuEAAAAAAAB0ATJEHddNNNmjx5sm699VZ9//33mj17tho2bBi+/vrrr9d1112nb7/9Vt27d9fpp5+uLVu2FNvG9ddfrylTpujrr79WgwYNdPrpp6uwsLC87woAAAAAAI7QBElAeXl5evDBB3Xffffpoosu0t/+9jcdf/zxGj16dPg2V155pc455xwdfvjhmjFjhlJTU/Xvf/+72HZuu+02nXzyyWrfvr2eeuop/fHHH3r99dfL++4AAAAAAOAITZAE9MMPP6igoEAnnnhiqbfp1u1/62wnJyerS5cu+uGHH0q9TZ06dXTooYeWuA0AAAAAAPGCJkgCqlatWpl+z8kywCwVDAAAAACIV6wO41AwGFQwGCxxmZmF/4WUdxtg73070apVK1WrVk3//e9/i50Cs/e2FixYoB49ekiSAoGAvvnmG2VmZha7rwsWLFCzZs0kSTk5OVqxYoUOPfTQiOs5WKGa9vU3AgAAAFB2fpXve/uyCnrk+32/R+r04ucqpzXTBClFVlaWsrKyVFRUJGnPh/xAIFDsNoWFhQoGgwoEAsWuq1SulapEXQeSnJyssWPH6oYbblBSUpK6d++uzZs36/vvv1efPn0kSdOnT1dGRoYOO+ww/etf/1JOTo6GDRumQCAQfkwmTpyo2rVrq0GDBpowYYLq1aunU089NeJ6DlYgEFAwGNS2bdu0c+fOct03AAAAUJFlpHijCZJduY3bJTiSntzwwDeKA9nZ2W6XELG8vDxHt6MJUorMzExlZmYqNzdXqampSktLU0pKSrHb5OfnKycnR8nJyUpOdu+hLMu+b7vtNlWuXFkTJ07Ur7/+qsaNG+vSSy8Nb2vSpEm6//779e233+pvf/ub3njjDTVq1EiSlJSUJEm69957dd1112nlypU68sgj9eabb6p69erRu2MOJScny+/3KzU1VVWrVi33/QMAAAAV1epcb5zuXqfqCrdLcGRdIN/tEhypU6eO2yVEzOnnYpogDvn9fvn9/hKX+Xy+8L+w27eVa21lGZaSkpJ0yy236JZbbil2+dq1ayVJbdu21cKFC/e9v/9/X3v06KHly5eXYe/RFXr89/U3AgAAAFB2wXI/2b9s/PLG6RtBj9Tpxc9VTmv23j0DAAAAAAAoA5ogAAAAAAAgIXA6DIpp0aLFAVd36d27d7mvAAMAAAAAwMHiSBAAAAAAAJAQaIIAAAAAAICEQBMEAAAAAAAkBJogAAAAAAAgITAxKgAAAOABLW58x+0SHFl770C3SwCAUnEkCAAAAAAASAg0QVBuevfurWuuucbtMgAAAAAACYomCAAAAAAASAjMCeJQMBhUMBgscZmZhf+FdHi6Q7nWtnTY0nLd38H462MVzW3u628EAABQUfgV3fdQscL7sYrFM7nzyPf7fo/U6cXnsdOaaYKUIisrS1lZWSoqKpIk5eTkKBAIFLtNYWGhgsGgAoFAievKU6T7Pumkk9S+fXtVqVJFs2bNUuXKlXXxxRdrwoQJWrt2rdq0aaOvvvpKHTt2lCRt3bpVDRo00AcffKBevXpp/vz5Ovnkk/X2229r/Pjx+umnn9S1a1c9++yzWrx4sa6//nr9+uuv6t+/v2bOnKnq1atL2tOsKCwsVGZmpmbPnq2kpCRdcskluuOOO+Tz+SRJzz33nB566CGtWLFCNWrUUO/evXX//ferQYMG+73/wWBQ27Zt086dO8v2IAIAAMS5jBRvfBjNzs52uwREkWdyV7mN2yU4kp7c0O0SHPHi8zgvL8/R7WiClCIzM1OZmZnKzc1Vamqq0tLSlJKSUuw2+fn5ysnJUXJyspKT3XsoI923z+fTM888o2uvvVYLFy7UggULNGLECPXo0UOtW7cObzO03dB/k5KSlJycrKSkJEnSXXfdpYcffljVq1fXoEGDNGTIEFWpUkWzZ8/W9u3bdfbZZ2vGjBm64YYbiu135MiRWrhwoRYtWqRLL71ULVu21MUXXyxJKioq0p133qlDDz1UmzZt0pgxY3TxxRfrnXdKnw09OTlZfr9fqampqlq1amQPHgAAgEeszvW5XYIjderUcbsERzo908ntEhz59sJvXd2/Z3JXdYXbJTiyLpDvdgmOeOV5vDenn4tpgjjk9/vl9/tLXObz+cL/3FKWfXfo0EG33367JKlNmzbKysrSRx99pDZt2oS3Gdru3v/d+/K77rpLxx9/vCRp1KhRuummm7Rq1SplZGRIks4991zNmzdPN954Y3i/zZo107Rp0+Tz+XTYYYdp+fLlmjZtmi655JLwdkL+9re/6V//+peOOeYY7dixQzVr1iz1/vt8vn3+jQAAACqKoLzxYdQr78eC8sbh/m4/np7JnUf+nuQudpzW7L17hqjo0KH4vCWNGzfWpk2byryNhg0bqnr16uEGSOiyv26za9euxZo23bp108qVK8OnHX377bc644wzlJ6erlq1aql3796SpPXr10dUGwAAAAAAf0UTJEFVqlSp2M8+n0/BYDDcPdt78tLCwsIDbsPn85W6Tad27Nihvn37qmbNmnr22Wf19ddf6/XXX5ck7d692/F2AAAAAADYF5ogKKZ+/fqSpN9++y182ZIlS6K2/YULF5b4uXXr1kpKStKPP/6ozZs3695771WPHj102GGHRXx0CgAAAAAApaEJgmKqVaumrl276t5779X333+vTz75RLfcckvUtr9hwwaNGTNGP/30k55//nk99NBDuvrqqyVJzZs3V+XKlfXQQw9p9erVmjNnju68886o7RsAAAAAkNhogqCEJ554QoWFherSpYuuvvpq3XXXXVHb9rBhw7Rr1y4dc8wxyszM1FVXXRWeFLV+/fp68skn9fLLL6tt27a69957NWXKlKjtGwAAAACQ2Hy29+QPKCG0RO62bdv2uUTumjVr1LJlS5ZmdRF/BwAAkAha3PiO2yU4svbegW6X4Ej7p9q7XYIjyy5a5ur+PZO7qn93uwRH2rds7nYJjridu7LY32f3vXEkCAAAAAAASAg0QQAAAAAAQEKgCQIAAAAAABICTRAAAAAAAJAQaIIAAAAAAICEQBMkClhgx108/gAAAAAAJ2iCHIRKlSpJknbu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IJs3b1ZRUZEaNiwelIYNG+r333+PyT5vuukmjRkzJvxzbm6umjVrprS0NKWkpMRkn7GyOtfndgmO1KlTx+0SECVeyZwk1am6wu0SHFkXyHe7BEfcfB57JXdeyZxE7pwgd9FF5pwhd9FF7pwhd9FF7mInOdlZeyOumyAhPl/xJ56ZlbjMieHDhx/wNlWqVFGVKlVKXO73++X3e+PQpZCgvDFgee1xRem8kjlJ8ssbh/gFPVKnm89jr+TOK5mTyJ0T5C66yJwz5C66yJ0z5C66yF3sOK05ru9ZvXr1lJSUVOKoj02bNpU4OgQAAAAAAGB/4roJUrlyZXXu3FkffPBBscs/+OADde/e3aWqAAAAAACAF7l+Osz27dv1888/h39es2aNlixZojp16qh58+YaM2aMLrzwQnXp0kXdunXTzJkztX79el122WXlWierw8SO1x5XlM4rmZOYQTzaWKXjwLySOYncOUHuoovMOUPuoovcOUPuoovcxY5nVodZtGiR+vTpE/45NCnpRRddpCeffFKDBg3Sli1bNHHiRP32229q166d3n33XaWnp8e0rqysLGVlZamoqEgSq8PEkhdnHsa+eSVzEjOIRxurdByYVzInkTsnyF10kTlnyF10kTtnyF10kbvY8czqML1795bZ/p9YV1xxha644opyqmiPzMxMZWZmKjc3V6mpqawOE0NenHkY++aVzEnMIB5trNJxYF7JnETunCB30UXmnCF30UXunCF30UXuYqdCrQ4TD1gdJna89riidF7JnMQM4tHGKh0H5pXMSeTOCXIXXWTOGXIXXeTOGXIXXeQudirE6jAAAAAAAADRQhMEAAAAAAAkBE6HcYjVYWLHa48rSueVzEnMIB5trNJxYF7JnETunCB30UXmnCF30UXunCF30UXuYsczq8PEK1aHKT9enHkY++aVzEnMIB5trNJxYF7JnETunCB30UXmnCF30UXunCF30UXuYsczq8PEK1aHKT9enHkY++aVzEnMIB5trNJxYF7JnETunCB30UXmnCF30UXunCF30UXuYofVYaKM1WFix2uPK0rnlcxJzCAebazScWBeyZxE7pwgd9FF5pwhd9FF7pwhd9FF7mKH1WEAAAAAAAD2QhMEAAAAAAAkBE6HcYjVYWLHa48rSueVzEnMIB5trNJxYF7JnETunCB30UXmnCF30UXu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y0PfEs2bN88OOeQQO+ecc6xu3bp28sknl+jclvb7fz00bV+Hqq1du9Y2bdpU7LLff//djj32WJs0aZLTu4EKICcnxy677DK75ppr7M8//zQzs0cffdRq1aoV7tqHMnT77bdbRkaGvfDCC7Z48WIbOHCgvfXWW/b+++872he5S0xuj3dmJc9JXr58uR122GF24okn2scff2yrVq3isN0EUJ7jnRm5SzTxMNbl5uaWuOy2226zY489NnzqA/NvVXzlPdaRO+wLTRAUs2jRovC5eWalTxh02WWXWXJyst19990H3GYwGCx2mO1//vOf8Pb/6sMPP7T69evb3Llz7ffff7devXrZkiVL7PPPP7c2bdrYp59+art377aPPvrI5s6da6+//rr9/vvvZbiniDehrIVeiGbMmGEpKSl28skn28svvxx+YczPz7cjjjjChg4dWixbwWDQevXqZa1bt7bq1avbhRdeWGw+j/29wJG7xOT2ePfFF1/YfffdF/75//7v/8zMbNasWdanT5/w+c2h7e29XXibm+MduUs8bo91+fn51q1bNxs3bpyZmb3yyis2YcIEMzNr3769TZw40czMVq1aZT/88IPNmzfPtmzZ4vwOIm65OdaRO+wPTRDYxo0bwx3V/Px8u+yyy+xvf/vbPm8bGsw2btxo6enpdvvtt9u2bdvMbN8D0d4D1Zo1a6x///6WmppqDz30UKn19OzZ0w477DCrVq2anXTSSbZt2zb7/fffrX379nbUUUdZq1atbMCAAda8eXNr3bq1nXbaaWW+74hPa9assY4dO9qsWbP2ef2cOXMsKSnJ5s+fH74s9G3CkiVLbM2aNeHLnXb3yV1iiKfx7vHHHw9/MOnZs6elpaVZXl6e3XTTTdayZUt799137b777rOxY8da165dbcyYMbZkyZKDuv+IP+U93pG7xBBPY52Z2XPPPWe1atWydu3aWa1atezxxx83M7Phw4dbRkaG9erVy/r162fHHnus+Xw+O+uss2zBggVlu/OIS268tyN3KA1NENi4ceMsKSnJfv31VzMz++GHH6xu3bp21113mVnJbwxCP99+++12+OGH29y5c/e7/WAwaFdffbXVrFnThg4dWmzG8NDkSGZ7XqS3bt1qbdq0seTkZBszZkyx7SxdutSmTZtmb7zxhs2fP9+ys7Pt4YcftkMPPZQJjSqAnJwcGzJkiP3555/2ySefWJs2bWz+/PmWm5tr77zzjs2ePdueeuqp8Aviqaeeas2bN7fJkydb+/bt7ayzziq2esv+Zv4md4nLzfFuX+rVq2dJSUk2aNCgcLa3b99up5xyitWpU8cGDhxo//jHP+yqq66yHj162MiRI8t0vxFfynO82xdyV/G5Pdb9dfvjxo0zn89nhx56qGVnZ4cvLygosIkTJ9rYsWPttddes6+++sq+/PJLa9y4sT333HNlu/OIG+U91pE7OEUTBLZ161ZLT0+36667zsz2vLBNnjzZqlWrFl6HO/SBMRgMhgeYQCBgnTp1sosvvrjUWZmffvppS01NteOPP77Esmt7H0a5Y8eO8P8vWbLEhgwZYr169bKvvvpqv7XfcMMNdu6550Z4j+G2fR1e/emnn1rLli3tm2++sZUrV9pJJ51kLVq0sIyMDDv11FOtZcuW1qpVK+vbt6+ZmeXl5dmwYcOse/fuduutt5Zp3+Qu8bg13u3deAt57733rEOHDubz+eytt94K387MLDs72/744w8rLCy0/Px8MzMbMGCAXXPNNcVqRPxzc7wjd4krXsa60HYXL15sM2bMMJ/PZx988EGx6/5qw4YN1q5dO/vss8/Kevfhgnga68gdDoQmSIILDRizZs2yKlWqhM8N/u2336xdu3Y2ZMiQYrcLCR0K+eSTT1rVqlXtP//5T4ltb9q0yW6++WZ79NFHiw04ew+Subm5Nnz4cBswYIDddttt4UNuV65cac2bN7cJEyaED8ksLCy0oqIie/PNN+3tt9+2k046yerXr2+vvfZatB4OxNj+uve///671a1bN3wY5IoVK+yxxx6z+fPn29dff225ubn22GOPWdOmTW3VqlVmZrZz504rKCgIb2N/566TO7gx3v11e9999519+eWXtmvXrvBl5557rh155JHFDvUN2bZtmwWDQZs9e7Yddthh9uyzz5bhnsMNbo53ZuQukbkx1u295KnZnmbb0KFD7fbbb7eVK1eG83rmmWda586dS0xWuWHDBvvwww/tkUcescaNG9ugQYMsJyfnIB8JlAc3xzpyh7KiCZKA9j6XMyQYDNoxxxxjp512Wngwe+WVV8zn89nChQvNbM8gt2vXLps6dWp4NmUzs//+97+l7mt/A9eSJUusX79+duKJJ9q4ceOsTZs2dvjhh4c/kI4fP97atm1rH374YbHfu/POO+3II4+0iy++2PLy8pzfcbhq7xepH3/8MfwN0t557NWrV/hbq3257LLLbPjw4SUu39e3naUhd4nFzfFu70zm5uba+eefb9WqVbNmzZrZiSeeaC+99JKZ7flgkpycbPfee2+xN35r1qyxoUOHWo8ePSwtLc0eeeSRMjwCcIOb4x25S0zx8t5u9+7ddtddd1m9evVs8ODBlpGRYW3btrUHHnjAzPZ82VClShWbMWNGsd/7/PPPrVu3bta2bdtS54xA/ImX93bkDpGiCZJA/jqQfPrpp/bzzz+Hz7WbP3+++f1+e/PNN81szyFpAwcOtGOOOSb8O1u3bjWfz2dXXnllsXP09rX90mzevNkuuOAC69evnw0ZMiT8zdQPP/xgp5xyinXu3NnM9gxoHTt2tMGDB9vixYstKyvL/vGPf5jZnjdvIft64Yd7/pqDvd8sbdy40Xr37m3//ve/rXfv3ta9e3e75ZZbzGzP3BxDhw61q6++Onz0RX5+vr3++uv29NNPW+fOna1Fixb20UcflakucpdY4mW8MzP74IMPbNKkSTZo0CBbvny5ffTRR3beeedZ69at7ZNPPjEzs5tuuskaN25sixcvNrM98zPk5ubarFmzbNKkScXyxrKl8SNexzszcpco4mms+9e//mWnnXaaDRkyJHxKQX5+vl1++eV21FFHhU+dueGGG6xhw4a2ePFi++2332zcuHG2dOlS++6774ptj5WJ4kc8j3XkDmVBE6QC+uuhYX/1/PPP2yGHHGKdO3e2hg0b2lVXXWUrVqwwM7Pzzz/f2rVrFz5cbMGCBVajRg175plnwr8/d+5cW7du3UHVePHFF1ulSpUsMzOzWN2ffvqp1ahRw1555RUz2/ONxXHHHWeHHHKINWnSxP7973+Hbx/pRHCIrf39LYLBoH3//fd2xx132GmnnWbbtm2znJwcmzVrllWrVs3Gjx9vO3futLvvvrvYGzOzPUdm9OzZ08aPH3/QNZK7iifex7vPPvvMfD6fNWrUyJ5//vnw5T/88IOdffbZ1qdPn/Bl6enpdtJJJ9lVV11llStXtkmTJhXbFo23+BHv4x25q3jifawz+1/umjVrVmwZ+cWLF9vpp59uI0aMCF92+OGHW7t27axy5crWr1+/Yqcg8CE0fsT7WGdG7lA2NEEqmL0Hqz///NOys7OLvWjOnz/fjjjiCJs2bZoVFRXZxx9/bMcee6z16tXLzMzWrl1rNWrUsPvvv9/M9rz5ufzyy83n85UYHMryQTC0jdWrV1vHjh2tX79+xWZr3rJli/Xt29duvvnm8GUbN248qA4xYm/vjL3wwguWmZlpd911V3iZseuvv958Pp/17t3b1q9fX+x3X3jhBTvxxBOtR48e9vTTT1vr1q2LTca2efPmYudwluUNObmrmOJpvNvfh5OrrrrK/H6/vfjii8W29/TTT9vhhx8e/lb+888/t6uuusr69Oljc+bMcbx9lK94Gu/IXWKIp7Eu5K/ZCP3eiBEjrG7duiWWVb722mutX79+4Xz/+uuv9uabb9qXX37paH8of/E01u2rJjNyh7KjCVIB5ebm2iWXXGJ9+vSxU0891V599dXw4Y3XXntteFWL7du328iRIy0lJcWuv/768Ezwd9xxhzVq1Cj8jcDq1avtySefdLTvhx9+2M4//3y75557Sl1hIzSATZ482bp06VLsW/Zdu3ZZenp6+By+0ibtQvz5+uuvrV27dtamTRubMGGC3XLLLfb111+b2Z5lZtu3b2/HHXdcsdnoQ9auXWs9e/a0WrVqWbNmzYqdlxyyv3NDyV3icnO827Fjhy1btqzUfIQ+XKxbt86aNWtm1113nW3ZsiV8/WeffWYNGjQo9qZt77kZ9l6xAfHFzfGO3CUmN8e633//3W6++WabMmWKffrpp/u8TSgzmzdvtkqVKtmdd95ZbAW2W265xVq0aFHq3CV8Cx+f3BzryB1iiSZIBTNr1ixLS0uzU0891T766CObPXu2bd261YLBoOXn59v5559vjz32mN1///1Wu3Zt69u3r3377bfFtlFQUGA1atSwUaNGOdpnMBi0P//80/r162ctWrSwyy+/3Lp06WK1atWy1157LTzwhAa50IC1bds2O+mkk6xVq1b24osv2s8//2zTp0+3Zs2ahZexgjcsX77cOnXqZFdeeWV4VZW9FRYW2v33329VqlSxtWvXmlnJRkN2draNHz/ekpOTw5OSHugwTHKX2NwY70Luueceq1evnrVp08a6dOliTzzxxD5vF8rdbbfdZq1bt7annnoqfN27775r6enp4UPW98Ybs/jlxngXQu4Sk5tj3VNPPWU1atSwU045xdq3b29NmjQpdtTk3kL5ue2226x69er20EMP2a+//mrr1q2z7t2723XXXUeDzUPcHOvIHWKNJkgFsmzZMuvWrZv985//LDEIhX4ePXq0+Xw+a9eunb366qvh67dv324zZ84MTyg0b948+/HHHx3ve968edayZUv7+eefw5cNHjzYjjvuOHv99deL1WD2vwHwtddes7S0NEtPT7ezzjrLWrRoUewbenjDNddcY0cddZRt2LBhn39nsz2TirZv336fS/OF/v/XX3+1fv36lTgnvTTkLnG5Od498cQTlpGRYe+++6598MEHdv3115vf77dnn302/K3rX78VKywstI4dO1pqaqpddtllduedd1rNmjVt9OjRxZYtRfxza7wjd4nJzbEuGAzaKaecYjfddJOZ7Vked/bs2ebz+eyJJ57Y7ySqGRkZ5vP57IILLrBWrVpZz5497ddff43szsNVbo115A7lgSaIB5V2PtyVV15pGRkZ9ssvv5S4feg2P//8s1WpUsUmT55c7DZPP/209evXzz7++OP97qs0d955p3Xs2NFycnLCv7Nhwwbr06ePXXDBBeFVNfa1vb///e92zjnnhCeljHTfiL29J2T76wtcXl6edezY0a6++uoDbufll182n88XPhdzX535Dh062PTp00vsa1/IXcUXT+Nd6LDdwYMHhw89Dxk1apR16tRpn0cThb6lev31161SpUo2YsQIu+qqq4p9WEH8iLfxjtwlhnga60LX//DDD1atWrXwHBAhl19+uWVkZJQ42sTsf6ePvvXWW+bz+ezVV18tdjteY+NHvI115A7lyS94js/nK/az37/nz/j111/r5JNPVpMmTRQIBIrd3u/3y8z0t7/9TTfffLMefPBBnXDCCXrwwQc1cOBAXXXVVerfv7969+69331J0vz587VixQpt27YtfFnTpk21atUqVatWTT6fT4FAQE2bNtUFF1ygn376SR9//HGJ7RUVFUmSLrvsMq1du1ZLliwJXxcMBve5b5S/oqIi+Xw++Xw+5ebmhi+T9vw9k5KStGrVKtWuXbvYdSFmFv7/U089VaeddppGjx4t6X/ZDfn000+1bt06paamhrcfQu4Sk9vj3V/37fP59PPPP6tly5aSpPz8fEnSfffdp8LCQr355pvKzs4u9ntJSUmSpDPPPFM9evTQrl27dNVVV+nss89WMBgs8ZyBe+JlvNsbuUsMbo91O3fu1PLlyxUIBMLXN27cWLVq1dKyZcskSbt375YkPfjgg8rJydFrr71WLPOSlJycLGlP/rt06aJHH31UGRkZklRs23BXvIx15A6uca//grJ64YUXbOrUqeGfQ53Xvn37Wrdu3fb7u6FDyObMmWNDhw61888/30aMGGF//vln+DaldUuXLl1q7dq1s0aNGlmrVq2sbdu29sUXX5jZnsmL6tSpE64rNMna7t277cgjj7Rx48btd9tjx461zp07h09hgPv27uTn5ubatddea2eccYYdffTRdtxxx9lrr71mmzdvNjOzk08+2Y466qhSt7V161abNWuWme2Zxb5evXr2008/FbvN9u3bbdiwYSUOlyR3ic2t8c6s9Al3b7nlFktPTy+xn2nTplmzZs3su+++K7Gt0LfyCxYssObNm9uDDz5YbPI2uCtexjszcpeo3Bzr/jrXTOj00G3bttmFF15op5xySviUq9Dr7D333GONGjXa5/ZCuVu6dKn5fD6bMWMG883EiXga68gd3EQTxAO++OILmzBhguXl5ZnZnkMjjzjiCJsyZYo1bNjQWrdubWZml112mTVr1szeeustMyu5osVPP/1kt99+u23atCl82d7nBAcCgf3ORn/aaafZ6NGjbePGjbZs2TLr1auXde/ePbyk3nXXXWdpaWnhN1ihF+UxY8ZYly5d9rndvQ/l7Nevn33++eeRPTiIudtuu82qVatm/fv3t0ceecTGjx9vAwcOtJo1a9rYsWPNzGzmzJnhczX/qqioyP75z3/a6NGjrbCw0AoLC0s9n/OvL1bkLvG4Pd4daMLdYDBoH330kWVkZNi0adPMrPjKGrVq1bJnnnnGzEoeEhza35AhQ6x169b7/NAKd7k13pG7xOP2WBeyr7lmfD5fOE+zZs2yTp062YwZM4rtf9myZVavXr1SV2QL5fCss86yUaNGscpanHHzvZ0ZuYP7aIJ4wKRJk+zRRx8NDyYrVqywlJQUS0pKsvHjx4dv9/XXX1ujRo3sjDPOsO3bt5fYzt133x0erP7qQLMmr1q1yurWrVts/oSVK1faWWedZX379rXs7GxbtWqVtW7d2gYNGlTsd88991y75JJLzGzf30Ts63xEuO+nn36yjh07Wt26dcOTqu1t2LBhVr9+fXvttddsx44dduqpp1rdunXtjTfeMLM9L1i7du2yrKwsO+aYY+zll18u9vtOuvPkLvHEw3hX2oS7Xbt2tf/85z9WUFBgV1xxhbVp0yb8TWtRUZFt27bN2rdvb3ffffc+txva76ZNm8IfaBAf4mG8I3eJxe2x7kBzzXTo0MG++OILy83NtWHDhtkxxxxTLJuzZ8+2Fi1a2MaNG0vd/oFqQPlze6wjd4gXNEHi2L6ewAUFBfbUU0/Z8ccfb+np6fbee++Z2f8+yN16661Wr149O/PMM+3777+3FStW2Ndff21Dhw61Vq1a2TvvvFOmWhYsWGCHHHJI+DSE0P6ef/55O/roo23KlClmZvbhhx9aUlKSnXPOOTZr1iybMmWKNWjQoMTkk4gv+2oEfP7559apUye7/PLLi90u1OlfunSpHX/88da9e3crLCy0tWvXWteuXa1mzZrWqlUrO++886xdu3Z2yCGH2Jtvvlmmushd4oin8W5/E+4OHjzYtm3bZt9++60dddRR1rdv3/Chw4sWLbJWrVrZN998U+q2abq5L17HO3KXGOJprDMz69Kli11//fVm9r8jSLZs2WLt2rWzK664wgoLC23hwoXWq1cva9++vb3yyiu2aNEiO+WUU+z8888Pn66A+BOvY50ZuYP7aILEqb8OXGvXrrUTTzwx/KHPzOy4446zc845p1iHNLQcWrNmzaxKlSrWuXNna9y4sfXv3982bNhwwP0uXbrURowYYddcc43dd999xa475JBDbMKECWb2v05vXl6eDRs2zM4666zwShyvv/66nXfeedapUydr27Yt8y3EsQN1yu+++2479thj7aWXXtrn7W+77TZr3LixffTRR2a25/zQN99808aOHWtjx461hx56qNjt9zffDLlLXG6Nd2Z7vn3/6aefbOvWreHLZs2aZbVq1Qq/yQp9wzpz5kzr2LFj+PmwePFiO+SQQ6xly5Y2cOBAq1Gjho0cOdJ27tzJh844FC/jnRm5S1RujnVlmWvmkEMOCc/xsH79ejvhhBOsQ4cO1rBhQzvjjDMsJycn0ocA5SCexjpyh3hFEyQOlXYo2amnnmqnn356eLKs9957zw455BCbMWNGicMg165da59++qnNmTOn2BJRpW27oKDAxowZYzVr1rRRo0bZkCFDLDk52caOHRsemO69915LTU0Nf/sU2tYTTzxhzZo1sy1bthTb5l8PVePNWfzYe1k0sz1vss8//3wbO3aszZ8/P3z5ypUr7YwzzrCzzjor/GY9GAyG87Z48WLz+Xw2b968fe4jpLRzMskd3BjvzA5uwt3Qt1dmZj/++KO9+OKLdsMNN5RYhhLxIV7GOzNyl8jcGOuiMdfM008/Hf65oKDANm/ebKtXrw5fxmkH8SNexjpyBy+gCeKi0BM4NKDs/YTevHmzTZ8+3b766qvwh7+PPvrIjjrqKLvlllvCA8+gQYPshBNOCL94/vjjj/vcVzAY3O+L5L/+9S/r06dPsTdTL774otWsWdNyc3PNbE83tnXr1jZixIhig+C8efOsWrVq4cHprx86mZk5fv3nP/+x5s2bW6tWrSwzM9Pat29vdevWte+//z58m8cff9yOPfbY8AvV3n/fN99806pVq1bqxKJ/fUH+63XkLnHEy3hnFrsJd0P75s1ZfHJzvDMjd4kinsY6s+jONbN3vslc/HJ7rDMjd4h/NEHixN5P8FdffdUqV65sbdu2tUaNGtno0aPDT/grr7zSevToYW+//baZ7enmtmnTxk455RT7+9//bj6fL/xmal/bLs3jjz9uEyZMCL8AB4NB++6776xx48a2aNGi8O3eeecd8/v9Nnny5PChapdcckmJyY0Qf/Z+oxQIBOy+++4zn89nDz74YLgDv2zZMqtWrVqxbxy3bdtmF110kZ144onFlj7bvHmzDRkyxIYOHVrmmshdYnJ7vIvVhLu8MYsf8TjekbvE4/ZYZxbbuWbgvngc68zIHeKfX3DN5s2bdeihh+q5556Tz+fTZ599pn/84x9atGiRXnnlFS1btkw33HCDvv76a911112SpKuuukqBQEBvv/22Nm/erFatWumBBx5Qu3btZGb67rvvdNpppxXbj8/nO2Ato0aN0h133KHk5GQFg0H5fD5t3LhRSUlJ6tChQ/h2AwYM0OTJk/XUU0+pb9++ateund566y1ddtll0X1wEHVJSUmSpOzsbPn9fqWlpalWrVo6+uijVblyZUnS7t27Va1aNa1fv16SFAwGlZKSovPOO08FBQV64oknJEm//fabMjMz9eOPP4b/9sFgMOKayF3iiKfxbtOmTapataqaNGkiSTIztWrVSueff75ycnL0xBNPKCMjQ4888oheeeUVnXvuuXryySd1//3365NPPlHfvn33uS+/n5fUeBGP4x25SwxujnXz58/XihUrtG3btvBlTZs21apV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", "text/plain": [ "
" ] @@ -421,9 +439,98 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "44121b2e-221f-48b5-b520-051ab38a1e8c", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " points uncached cold warm speedup hits misses evictions resident expected\n", + " 2000 154.9 us 155.8 us 106.4 us 1.46x 7 16 0 32000B 32000B\n", + " 20000 1.67 ms 2.16 ms 1.18 ms 1.42x 7 16 0 320000B 320000B\n", + " 200000 20.36 ms 20.90 ms 14.21 ms 1.43x 7 16 0 3200000B 3200000B\n" + ] + } + ], + "source": [ + "def benchmark_form_factor_cache(n_points, repeats=7):\n", + " if not CTRuc.HAS_CPP_ACCEL:\n", + " return None\n", + "\n", + " xtal = load_crystal()\n", + " h, k, l = make_rod(n_points)\n", + "\n", + " def run_unitcell():\n", + " return xtal.uc_bulk.F_uc(h, k, l)\n", + "\n", + " set_backend(\"cpp\")\n", + " original_budget = CTRuc.form_factor_cache_stats()[\"budget_bytes\"]\n", + " CTRuc.clear_form_factor_cache()\n", + " CTRuc.reset_form_factor_cache_stats()\n", + " try:\n", + " CTRuc.set_form_factor_cache_budget(0)\n", + " uncached = time_call(run_unitcell, repeats=repeats)[\"median_s\"]\n", + "\n", + " CTRuc.set_form_factor_cache_budget(original_budget)\n", + " cold = cold_cache_time(run_unitcell, repeats=repeats)\n", + "\n", + " CTRuc.clear_form_factor_cache()\n", + " run_unitcell()\n", + " warm = time_call(run_unitcell, repeats=repeats, warmups=0)[\"median_s\"]\n", + " stats = CTRuc.form_factor_cache_stats()\n", + " finally:\n", + " CTRuc.clear_form_factor_cache()\n", + " CTRuc.set_form_factor_cache_budget(original_budget)\n", + "\n", + " return {\n", + " \"points\": n_points,\n", + " \"uncached_median_s\": uncached,\n", + " \"cold_median_s\": cold,\n", + " \"warm_median_s\": warm,\n", + " \"warm_speedup\": uncached / warm,\n", + " \"resident_bytes\": stats[\"resident_bytes\"],\n", + " \"expected_bytes\": CTRuc.form_factor_cache_expected_bytes(\n", + " n_points, stats[\"species_entries\"]\n", + " ),\n", + " \"hits\": stats[\"hits\"],\n", + " \"misses\": stats[\"misses\"],\n", + " \"evictions\": stats[\"evictions\"],\n", + " }\n", + "\n", + "\n", + "cache_results = [\n", + " benchmark_form_factor_cache(n_points)\n", + " for n_points in (2_000, 20_000, 200_000)\n", + "]\n", + "cache_results = [result for result in cache_results if result is not None]\n", + "\n", + "if not cache_results:\n", + " print(\"C++ extension unavailable; cache benchmark skipped.\")\n", + "else:\n", + " print(\n", + " f\"{'points':>8} {'uncached':>14} {'cold':>14} {'warm':>14} \"\n", + " f\"{'speedup':>9} {'hits':>7} {'misses':>7} {'evictions':>10} \"\n", + " f\"{'resident':>12} {'expected':>12}\"\n", + " )\n", + " for row in cache_results:\n", + " print(\n", + " f\"{row['points']:8d} \"\n", + " f\"{format_seconds(row['uncached_median_s']):>14} \"\n", + " f\"{format_seconds(row['cold_median_s']):>14} \"\n", + " f\"{format_seconds(row['warm_median_s']):>14} \"\n", + " f\"{row['warm_speedup']:8.2f}x \"\n", + " f\"{row['hits']:7d} {row['misses']:7d} {row['evictions']:10d} \"\n", + " f\"{row['resident_bytes']:11d}B {row['expected_bytes']:11d}B\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "debfda02-5292-482c-a49b-9ca9821af021", + "metadata": {}, "outputs": [], "source": [] } diff --git a/meson.build b/meson.build index 7d4ac50..ac89e35 100644 --- a/meson.build +++ b/meson.build @@ -12,6 +12,7 @@ project( py = import('python').find_installation(pure: false) pybind11_dep = dependency('pybind11', required: true) +xxhash_dep = dependency('libxxhash', fallback: ['xxhash', 'xxhash_dep']) cpp = meson.get_compiler('cpp') native_args = [] @@ -196,7 +197,7 @@ configure_file( py.extension_module( '_CTRcalc_cpp', files('orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp'), - dependencies: [pybind11_dep], + dependencies: [pybind11_dep, xxhash_dep], install: true, subdir: 'orgui/datautils/xrayutils' ) diff --git a/orgui/datautils/xrayutils/CTRuc.py b/orgui/datautils/xrayutils/CTRuc.py index 5c3f8cf..a70797f 100644 --- a/orgui/datautils/xrayutils/CTRuc.py +++ b/orgui/datautils/xrayutils/CTRuc.py @@ -285,6 +285,72 @@ def ctr_accel_enabled(): return CTR_ACCEL_BACKEND != "numpy" +def form_factor_cache_stats(): + """Return process-global C++ Waasmaier form-factor cache statistics. + + The cache is used only by accelerated :meth:`UnitCell.F_uc` calls. Its + resident-byte accounting includes retained float64 Q-squared and real + form-factor vectors, but not short-lived call snapshots or output arrays. + Reuse requires bitwise-identical computed Q-squared arrays and complete + 13-value scattering rows; numerically near grids intentionally miss. + + :returns: Cache hits, misses, evictions, capacity, and residency details. + :rtype: dict + :raises RuntimeError: If the C++ acceleration module is unavailable. + """ + if not HAS_CPP_ACCEL: + raise RuntimeError("C++ form-factor cache is not available") + return _CTRcalc_cpp.form_factor_cache_stats() + + +def clear_form_factor_cache(): + """Clear all process-global C++ Waasmaier form-factor cache entries.""" + if not HAS_CPP_ACCEL: + raise RuntimeError("C++ form-factor cache is not available") + _CTRcalc_cpp.clear_form_factor_cache() + + +def reset_form_factor_cache_stats(): + """Reset process-global C++ Waasmaier form-factor cache counters. + + Retained entries and the configured capacity are unchanged. Pair this + with :func:`clear_form_factor_cache` to start a measurement from an empty + cache and zero counters. + """ + if not HAS_CPP_ACCEL: + raise RuntimeError("C++ form-factor cache is not available") + _CTRcalc_cpp.reset_form_factor_cache_stats() + + +def set_form_factor_cache_budget(bytes): + """Set the process-global C++ form-factor cache capacity in bytes. + + :param int bytes: Non-negative cache residency limit in bytes. + :raises RuntimeError: If the C++ acceleration module is unavailable. + :raises ValueError: If ``bytes`` is negative. + """ + if bytes < 0: + raise ValueError("form-factor cache budget must be non-negative") + if not HAS_CPP_ACCEL: + raise RuntimeError("C++ form-factor cache is not available") + _CTRcalc_cpp.set_form_factor_cache_budget(bytes) + + +def form_factor_cache_expected_bytes(points, species): + """Return retained float64 bytes for one matching Q-squared grid. + + :param int points: Number of reflections in the Q-squared vector. + :param int species: Number of distinct complete 13-value scattering rows. + :returns: Q-grid plus one real form-factor vector per species. + :rtype: int + """ + if points < 0 or species < 0: + raise ValueError("points and species must be non-negative") + if not HAS_CPP_ACCEL: + raise RuntimeError("C++ form-factor cache is not available") + return _CTRcalc_cpp.form_factor_cache_expected_bytes(points, species) + + def _ctr_accel_module(): if CTR_ACCEL_BACKEND == "numpy": return None diff --git a/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp b/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp index 5afd49c..2929ce0 100644 --- a/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp +++ b/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp @@ -1,7 +1,15 @@ +#include #include #include +#include +#include +#include #include +#include #include +#include + +#include #include #include @@ -14,6 +22,206 @@ using Array1D = py::array_t; using Array2D = py::array_t; using Array3D = py::array_t; +constexpr std::size_t form_factor_cache_default_budget = 256ULL * 1024 * 1024; + +struct Hash128 { + std::uint64_t low; + std::uint64_t high; +}; + +Hash128 hash_values(const double *values, const std::size_t count) { + const XXH128_hash_t hash = XXH3_128bits(values, count * sizeof(double)); + return {hash.low64, hash.high64}; +} + +bool same_hash(const Hash128 &left, const Hash128 &right) { + return left.low == right.low && left.high == right.high; +} + +struct CachedSpecies { + std::array row; + Hash128 hash; + std::shared_ptr> values; +}; + +struct CachedQGrid { + std::shared_ptr> q2; + Hash128 hash; + std::vector> species; +}; + +class FormFactorCache { +public: + std::shared_ptr> find( + const std::vector &q2, + const std::array &row + ) { + const Hash128 q_hash = hash_values(q2.data(), q2.size()); + const Hash128 row_hash = hash_values(row.data(), row.size()); + std::lock_guard lock(mutex_); + auto grid = find_grid(q2, q_hash); + if (grid == grids_.end()) { + ++misses_; + return nullptr; + } + for (const auto &species : (*grid)->species) { + if (same_hash(species->hash, row_hash) + && std::memcmp(species->row.data(), row.data(), sizeof(row)) == 0) { + touch(species); + ++hits_; + return species->values; + } + } + ++misses_; + return nullptr; + } + + std::shared_ptr> insert_or_get( + const std::vector &q2, + const std::array &row, + std::vector values + ) { + const Hash128 q_hash = hash_values(q2.data(), q2.size()); + const Hash128 row_hash = hash_values(row.data(), row.size()); + std::lock_guard lock(mutex_); + auto grid = find_grid(q2, q_hash); + if (grid != grids_.end()) { + for (const auto &species : (*grid)->species) { + if (same_hash(species->hash, row_hash) + && std::memcmp(species->row.data(), row.data(), sizeof(row)) == 0) { + touch(species); + ++hits_; + return species->values; + } + } + } + + const std::size_t values_bytes = values.size() * sizeof(double); + const std::size_t q_bytes = grid == grids_.end() ? q2.size() * sizeof(double) : 0; + if (values_bytes + q_bytes > budget_bytes_) { + return std::make_shared>(std::move(values)); + } + if (grid == grids_.end()) { + auto new_grid = std::make_shared(); + new_grid->q2 = std::make_shared>(q2); + new_grid->hash = q_hash; + grids_.push_back(new_grid); + grid = std::prev(grids_.end()); + resident_bytes_ += q_bytes; + } + auto species = std::make_shared(); + species->row = row; + species->hash = row_hash; + species->values = std::make_shared>(std::move(values)); + (*grid)->species.push_back(species); + lru_.push_back(species); + resident_bytes_ += values_bytes; + evict_to_budget(); + return species->values; + } + + void clear() { + std::lock_guard lock(mutex_); + grids_.clear(); + lru_.clear(); + resident_bytes_ = 0; + } + + void reset_statistics() { + std::lock_guard lock(mutex_); + hits_ = 0; + misses_ = 0; + evictions_ = 0; + } + + void set_budget(const std::size_t bytes) { + std::lock_guard lock(mutex_); + budget_bytes_ = bytes; + evict_to_budget(); + } + + py::dict statistics() { + std::lock_guard lock(mutex_); + py::dict result; + result["hits"] = hits_; + result["misses"] = misses_; + result["evictions"] = evictions_; + result["resident_bytes"] = resident_bytes_; + result["budget_bytes"] = budget_bytes_; + result["q_grids"] = grids_.size(); + result["species_entries"] = lru_.size(); + return result; + } + +private: + using GridList = std::list>; + + GridList::iterator find_grid(const std::vector &q2, const Hash128 &hash) { + for (auto grid = grids_.begin(); grid != grids_.end(); ++grid) { + const auto &stored = *(*grid)->q2; + if (same_hash((*grid)->hash, hash) && stored.size() == q2.size() + && std::memcmp(stored.data(), q2.data(), q2.size() * sizeof(double)) == 0) { + return grid; + } + } + return grids_.end(); + } + + void touch(const std::shared_ptr &species) { + for (auto item = lru_.begin(); item != lru_.end(); ++item) { + if (*item == species) { + lru_.splice(lru_.end(), lru_, item); + return; + } + } + } + + void evict_to_budget() { + while (resident_bytes_ > budget_bytes_ && !lru_.empty()) { + const auto species = lru_.front(); + lru_.pop_front(); + for (auto grid = grids_.begin(); grid != grids_.end(); ++grid) { + auto &entries = (*grid)->species; + for (auto entry = entries.begin(); entry != entries.end(); ++entry) { + if (*entry == species) { + resident_bytes_ -= (*entry)->values->size() * sizeof(double); + entries.erase(entry); + ++evictions_; + if (entries.empty()) { + resident_bytes_ -= (*grid)->q2->size() * sizeof(double); + grids_.erase(grid); + } + goto evicted; + } + } + } + evicted:; + } + } + + std::mutex mutex_; + GridList grids_; + std::list> lru_; + std::size_t budget_bytes_ = form_factor_cache_default_budget; + std::size_t resident_bytes_ = 0; + std::uint64_t hits_ = 0; + std::uint64_t misses_ = 0; + std::uint64_t evictions_ = 0; +}; + +FormFactorCache &form_factor_cache() { + static FormFactorCache cache; + return cache; +} + +py::dict form_factor_cache_stats() { + return form_factor_cache().statistics(); +} + +void reset_form_factor_cache_stats() { + form_factor_cache().reset_statistics(); +} + struct Matrix3 { double value[3][3]; }; @@ -464,23 +672,164 @@ py::array_t unitcell_F_uc( const Array1D &coherent_domain_occupancy, const double /* uc_area */ ) { - return unitcell_core( - h, - k, - l, - 0.0, - true, - false, - false, - basis, - f_factors, - ref_hkl_transform, - b_mat, - r_mat, - r_mat_inv, - coherent_domain_matrix, - coherent_domain_occupancy + const Inputs inputs = request_inputs( + h, k, l, basis, f_factors, ref_hkl_transform, b_mat, r_mat, + r_mat_inv, coherent_domain_matrix, coherent_domain_occupancy ); + const auto n_points = inputs.h.shape[0]; + const auto n_atoms = inputs.basis.shape[0]; + const auto n_domains = inputs.coherent_domain_matrix.shape[0]; + auto result = py::array_t(n_points); + auto *out = static_cast(result.request().ptr); + + const double *h_data = ptr(inputs.h); + const double *k_data = ptr(inputs.k); + const double *l_data = ptr(inputs.l); + const double *basis_data = ptr(inputs.basis); + const double *ff_data = ptr(inputs.f_factors); + const double *ref_data = ptr(inputs.ref_hkl_transform); + const double *b_data = ptr(inputs.b_mat); + const double *domain_data = ptr(inputs.coherent_domain_matrix); + const double *domain_occupancy = ptr(inputs.coherent_domain_occupancy); + const auto domain_matrices = effective_domain_matrices(inputs); + + struct PointGeometry { + double h; + double k; + double l; + double q_para2; + double q_perp2; + }; + struct AtomDomainGeometry { + double x; + double y; + double z; + double weight; + }; + struct SpeciesGroup { + std::array row; + std::vector atoms; + }; + + std::vector points(n_points); + std::vector q2(n_points); + std::vector atom_domains(n_atoms * n_domains); + std::vector species; + species.reserve(n_atoms); + for (py::ssize_t i = 0; i < n_atoms; ++i) { + std::array row; + std::memcpy(row.data(), ff_data + i * inputs.f_factors.shape[1], sizeof(row)); + auto group = species.end(); + for (auto candidate = species.begin(); candidate != species.end(); ++candidate) { + if (std::memcmp(candidate->row.data(), row.data(), sizeof(row)) == 0) { + group = candidate; + break; + } + } + if (group == species.end()) { + species.push_back({row, {}}); + group = std::prev(species.end()); + } + group->atoms.push_back(i); + } + + constexpr double pi = 3.141592653589793238462643383279502884; + constexpr double two_pi = 2.0 * pi; + constexpr double dw_denominator = 16.0 * pi * pi; + py::gil_scoped_release release; + + // These snapshots are deliberately made before hashing: exact numerical + // reuse is defined by the computed float64 Q^2 grid, not source h/k/l. + for (py::ssize_t p = 0; p < n_points; ++p) { + const double h_in = h_data[p]; + const double k_in = k_data[p]; + const double l_in = l_data[p]; + const double hh = ref_data[0] * h_in + ref_data[1] * k_in + ref_data[2] * l_in; + const double kk = ref_data[3] * h_in + ref_data[4] * k_in + ref_data[5] * l_in; + const double ll = ref_data[6] * h_in + ref_data[7] * k_in + ref_data[8] * l_in; + const double qx = b_data[0] * hh + b_data[1] * kk + b_data[2] * ll; + const double qy = b_data[3] * hh + b_data[4] * kk + b_data[5] * ll; + const double qz = b_data[6] * hh + b_data[7] * kk + b_data[8] * ll; + points[p] = {hh, kk, ll, qx * qx + qy * qy, qz * qz}; + q2[p] = points[p].q_para2 + points[p].q_perp2; + } + for (py::ssize_t i = 0; i < n_atoms; ++i) { + const double x = get2(basis_data, inputs.basis, i, 1); + const double y = get2(basis_data, inputs.basis, i, 2); + const double z = get2(basis_data, inputs.basis, i, 3); + for (py::ssize_t d = 0; d < n_domains; ++d) { + const Matrix3 &mat = domain_matrices[d]; + atom_domains[i * n_domains + d] = { + mat.value[0][0] * x + mat.value[0][1] * y + mat.value[0][2] * z + + get3(domain_data, inputs.coherent_domain_matrix, d, 0, 3), + mat.value[1][0] * x + mat.value[1][1] * y + mat.value[1][2] * z + + get3(domain_data, inputs.coherent_domain_matrix, d, 1, 3), + mat.value[2][0] * x + mat.value[2][1] * y + mat.value[2][2] * z + + get3(domain_data, inputs.coherent_domain_matrix, d, 2, 3), + domain_occupancy[d], + }; + } + } + + std::vector>> factors; + factors.reserve(species.size()); + for (const auto &group : species) { + auto values = form_factor_cache().find(q2, group.row); + if (!values) { + std::vector calculated(n_points); + for (py::ssize_t p = 0; p < n_points; ++p) { + double value = group.row[10] + group.row[11]; + for (int term = 0; term < 5; ++term) { + value += group.row[term] * std::exp(-group.row[term + 5] * q2[p]); + } + calculated[p] = value; + } + values = form_factor_cache().insert_or_get(q2, group.row, std::move(calculated)); + } + factors.push_back(std::move(values)); + } + + for (py::ssize_t p = 0; p < n_points; ++p) { + double amplitude_real = 0.0; + double amplitude_imag = 0.0; + for (std::size_t g = 0; g < species.size(); ++g) { + const double f_real = (*factors[g])[p]; + const double f_imag = species[g].row[12]; + for (const py::ssize_t i : species[g].atoms) { + const double disorder = std::exp(-( + get2(basis_data, inputs.basis, i, 4) * points[p].q_para2 + + get2(basis_data, inputs.basis, i, 5) * points[p].q_perp2 + ) / dw_denominator) * get2(basis_data, inputs.basis, i, 6); + for (py::ssize_t d = 0; d < n_domains; ++d) { + const auto &atom = atom_domains[i * n_domains + d]; + const double phase = two_pi * ( + points[p].h * atom.x + points[p].k * atom.y + points[p].l * atom.z + ); + const double domain_real = atom.weight * std::cos(phase); + const double domain_imag = atom.weight * std::sin(phase); + amplitude_real += disorder * ( + domain_real * f_real - domain_imag * f_imag + ); + amplitude_imag += disorder * ( + domain_real * f_imag + domain_imag * f_real + ); + } + } + } + out[p] = complex128{amplitude_real, amplitude_imag}; + } + return result; +} + +void set_form_factor_cache_budget(const std::size_t bytes) { + form_factor_cache().set_budget(bytes); +} + +std::size_t form_factor_cache_expected_bytes( + const std::size_t points, + const std::size_t species +) { + return points * (species + 1) * sizeof(double); } PYBIND11_MODULE(_CTRcalc_cpp, module) { @@ -489,4 +838,9 @@ PYBIND11_MODULE(_CTRcalc_cpp, module) { module.def("unitcell_F_uc_bulk_direct", &unitcell_F_uc_bulk_direct); module.def("unitcell_F_bulk", &unitcell_F_bulk); module.def("unitcell_F_uc", &unitcell_F_uc); + module.def("form_factor_cache_stats", &form_factor_cache_stats); + module.def("clear_form_factor_cache", []() { form_factor_cache().clear(); }); + module.def("reset_form_factor_cache_stats", &reset_form_factor_cache_stats); + module.def("set_form_factor_cache_budget", &set_form_factor_cache_budget, py::call_guard()); + module.def("form_factor_cache_expected_bytes", &form_factor_cache_expected_bytes); } diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index 423f0f6..f381978 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -2397,7 +2397,6 @@ def setUp(self): def testStructureFactorEqual(self): self.assert_structure_factors_match_volume_normalized_reference() - class TestCTRcalculationNumba(StructureFactorValidationMixin, unittest.TestCase): def setUp(self): original_backend = CTRuc.CTR_ACCEL_BACKEND @@ -2458,3 +2457,32 @@ def setUp(self): def testStructureFactorEqual(self): self.assert_structure_factors_match_volume_normalized_reference() + + def testFormFactorCacheReusesMatchingGrid(self): + """The C++ cache shares Waasmaier vectors across UnitCell calls.""" + original_budget = CTRuc.form_factor_cache_stats()["budget_bytes"] + self.addCleanup(CTRuc.clear_form_factor_cache) + self.addCleanup(CTRuc.set_form_factor_cache_budget, original_budget) + CTRuc.clear_form_factor_cache() + CTRuc.reset_form_factor_cache_stats() + reset = CTRuc.form_factor_cache_stats() + self.assertEqual(reset["hits"], 0) + self.assertEqual(reset["misses"], 0) + self.assertEqual(reset["evictions"], 0) + h = np.linspace(-1.0, 1.0, 32) + k = np.zeros_like(h) + ell = np.linspace(0.1, 2.0, 32) + + first = self.xtal_unitcells.uc_bulk.F_uc(h, k, ell) + cold = CTRuc.form_factor_cache_stats() + second = self.xtal_unitcells.uc_bulk.F_uc(h, k, ell) + warm = CTRuc.form_factor_cache_stats() + + np.testing.assert_allclose(second, first) + self.assertGreater(cold["misses"], 0) + self.assertGreater(warm["hits"], cold["hits"]) + self.assertGreater(warm["resident_bytes"], 0) + self.assertEqual( + CTRuc.form_factor_cache_expected_bytes(10_000, 5), + 6 * 10_000 * np.dtype(np.float64).itemsize, + ) diff --git a/subprojects/packagefiles/xxhash/meson.build b/subprojects/packagefiles/xxhash/meson.build new file mode 100644 index 0000000..1ff127a --- /dev/null +++ b/subprojects/packagefiles/xxhash/meson.build @@ -0,0 +1,19 @@ +project( + 'xxHash', + 'c', + version: '0.8.2', + license: 'BSD-2-Clause', +) + +xxhash = static_library( + 'xxhash', + 'xxhash.c', + install: false, +) + +xxhash_dep = declare_dependency( + link_with: xxhash, + include_directories: include_directories('.'), +) + +meson.override_dependency('libxxhash', xxhash_dep) diff --git a/subprojects/xxhash.wrap b/subprojects/xxhash.wrap new file mode 100644 index 0000000..0986175 --- /dev/null +++ b/subprojects/xxhash.wrap @@ -0,0 +1,9 @@ +[wrap-file] +directory = xxHash-0.8.2 +source_url = https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.2.tar.gz +source_filename = xxhash-0.8.2.tar.gz +source_hash = baee0c6afd4f03165de7a4e67988d16f0f2b257b51d0e3cb91909302a26a79c4 +patch_directory = xxhash + +[provide] +dependency_names = libxxhash From 743680b54747eaf23e57b7acbbd4fa585c2c264c Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Fri, 24 Jul 2026 00:54:02 -0400 Subject: [PATCH 12/37] perf: cache scattering factor lookups --- orgui/datautils/xrayutils/CTRuc.py | 57 +++++++++++--- orgui/datautils/xrayutils/CTRutil.py | 41 ++++++++-- .../test/test_scattering_factor_cache.py | 76 +++++++++++++++++++ 3 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 orgui/datautils/xrayutils/test/test_scattering_factor_cache.py diff --git a/orgui/datautils/xrayutils/CTRuc.py b/orgui/datautils/xrayutils/CTRuc.py index a70797f..052556a 100644 --- a/orgui/datautils/xrayutils/CTRuc.py +++ b/orgui/datautils/xrayutils/CTRuc.py @@ -1368,10 +1368,9 @@ def split_in_layers(self, ordered=True): self._reorder_atoms(np.argsort(self.basis[:, 7], kind="stable")) layers = OrderedDict() - if not hasattr(self, "f"): - self.setEnergy( - 10000.0 - ) # populate f with some values to enable in-place modification + has_form_factors = ( + hasattr(self, "f") and self.f.shape == (self.basis.shape[0], 13) + ) for l in layer_numbers: # noqa: E741 where = (self.basis[:, 7] == l).nonzero()[0] @@ -1385,9 +1384,13 @@ def split_in_layers(self, ordered=True): uc.basis_0 = self.basis_0[idx_low:idx_high] uc.dw_increase_constraint = self.dw_increase_constraint[idx_low:idx_high] uc._test_special_formfactors() - uc.f = self.f[idx_low:idx_high] - if hasattr(self, "_E"): + if has_form_factors: + # Each layer owns its array: layer-level fitting must not alter + # its parent or sibling form factors. + uc.f = np.array(self.f[idx_low:idx_high], copy=True) + if has_form_factors and hasattr(self, "_E"): uc._E = self._E + uc._formfactor_state = uc._formfactor_state_token() uc.refRealTransform = self.refRealTransform uc.refHKLTransform = self.refHKLTransform layers[l] = uc @@ -2435,9 +2438,28 @@ def setEnergy(self, E): for dispersion and absorption correction in eV """ + shape = (self.basis.shape[0], 13) + state = self._formfactor_state_token() + if ( + getattr(self, "_E", None) == E + and hasattr(self, "f") + and self.f.shape == shape + and getattr(self, "_formfactor_state", None) == state + ): + return self._E = E self.lookupScatteringFactors(E) + def _formfactor_state_token(self): + """Return the species and special-callback state used by ``self.f``.""" + special = tuple( + (name, id(UnitCell.special_formfactors[name][0]), + id(UnitCell.special_formfactors[name][1])) + for name in self.names + if name in UnitCell.special_formfactors + ) + return tuple(self.names), special + def addFitParameter(self, indexarray, limits=(-np.inf, np.inf), **keyargs): """ @@ -3390,6 +3412,12 @@ def getFitErrors(self): return err0 def build_selected_basis(self): + # Preserve the legacy default-energy behavior for callers that compute + # a structure factor before selecting an energy, while allowing layer + # construction itself to remain free of unnecessary database work. + if not hasattr(self, "f") or self.f.shape != (self.basis.shape[0], 13): + self.setEnergy(getattr(self, "_E", 10000.0)) + if self.layer_behaviour == "select": if self.start_layer_number == -1.0: warnings.warn( @@ -3806,13 +3834,20 @@ def lookupScatteringFactors(self, E): else: self.f = np.empty((self.basis.shape[0], 13), dtype=np.float64) - for i, name in enumerate(self.names): + species_indices = OrderedDict() + for index, name in enumerate(self.names): + species_indices.setdefault(name, []).append(index) + + for name, indices in species_indices.items(): if name in UnitCell.special_formfactors: - self.f[i, :11] = 0.0 - self.f[i, 11:] = UnitCell.special_formfactors[name][1](E) + row = np.zeros(13, dtype=np.float64) + row[11:] = UnitCell.special_formfactors[name][1](E) else: - self.f[i, :11] = readWaasmaier(name) - self.f[i, 11:] = readDispersion(name, E) + row = np.empty(13, dtype=np.float64) + row[:11] = readWaasmaier(name) + row[11:] = readDispersion(name, E) + self.f[indices] = row + self._formfactor_state = self._formfactor_state_token() return def plot3d( diff --git a/orgui/datautils/xrayutils/CTRutil.py b/orgui/datautils/xrayutils/CTRutil.py index ca72b22..3c4d6f9 100644 --- a/orgui/datautils/xrayutils/CTRutil.py +++ b/orgui/datautils/xrayutils/CTRutil.py @@ -32,6 +32,7 @@ import xraydb import warnings import json +from functools import cache # random.seed(45) from collections import OrderedDict @@ -644,7 +645,16 @@ def DWtoDisorder(dw): # returns a,b,c for Q = 4pi/lambda * sin th (instead of s) -def readWaasmaier(element): +def _normalize_species(element): + """Return a stable lookup key for an element or ion name.""" + if isinstance(element, int): + return int(element) + return str(element).strip().title() + + +@cache +def _readWaasmaier_cached(element): + """Read immutable Waasmaier coefficients for a normalized species.""" xraydb_t = xraydb.get_xraydb() wtab = xraydb_t.tables["Waasmaier"] @@ -652,22 +662,37 @@ def readWaasmaier(element): if isinstance(element, int): row = row.filter(wtab.c.atomic_number == element).one() else: - row = row.filter(wtab.c.ion == element.title()).one() - # if len(row) > 0: - # row = row[0] + row = row.filter(wtab.c.ion == element).one() c = row.offset a = json.loads(row.scale) b = np.array(json.loads(row.exponents)) / (4 * np.pi) ** 2 xraydb_t.close() - return np.concatenate((a, b, [c])) + return tuple(np.concatenate((a, b, [c]))) + + +def readWaasmaier(element): + """Return Waasmaier coefficients for an element or ion. + + The database result is cached by normalized species name. A new array is + returned on each call so callers cannot modify the cache. + """ + return np.array(_readWaasmaier_cached(_normalize_species(element))) + + +@cache +def _readDispersion_cached(element, energy): + """Read immutable dispersion terms for a normalized species and energy.""" + return ( + xraydb.f1_chantler(atomic_number(element), energy), + xraydb.f2_chantler(atomic_number(element), energy), + ) # incorrect for ions!! def readDispersion(element, E): - return xraydb.f1_chantler(atomic_number(element), E), xraydb.f2_chantler( - atomic_number(element), E - ) + """Return dispersion terms for an element or ion at energy ``E`` in eV.""" + return _readDispersion_cached(_normalize_species(element), float(E)) def atomic_number(elementname): diff --git a/orgui/datautils/xrayutils/test/test_scattering_factor_cache.py b/orgui/datautils/xrayutils/test/test_scattering_factor_cache.py new file mode 100644 index 0000000..0634f40 --- /dev/null +++ b/orgui/datautils/xrayutils/test/test_scattering_factor_cache.py @@ -0,0 +1,76 @@ +import unittest +from unittest import mock + +import numpy as np +import xraydb + +from .. import CTRuc, CTRutil + + +class TestScatteringFactorCaching(unittest.TestCase): + def tearDown(self): + CTRutil._readWaasmaier_cached.cache_clear() + CTRutil._readDispersion_cached.cache_clear() + + def test_cached_values_match_xraydb_and_cannot_be_mutated(self): + for species in ("Fe", "Re", "O", "Ti", "O2-", "Fe2+"): + coefficients = CTRutil.readWaasmaier(species) + baseline = CTRutil._readWaasmaier_cached( + CTRutil._normalize_species(species) + ) + np.testing.assert_allclose(coefficients, baseline) + coefficients[0] += 1.0 + np.testing.assert_allclose( + CTRutil.readWaasmaier(species), baseline + ) + for energy in (10000.0, 20000.0): + expected = ( + xraydb.f1_chantler(CTRutil.atomic_number(species), energy), + xraydb.f2_chantler(CTRutil.atomic_number(species), energy), + ) + np.testing.assert_allclose( + CTRutil.readDispersion(species, energy), expected + ) + + def test_repeated_energy_and_species_groups_do_not_repeat_lookups(self): + cell = CTRuc.UnitCell([4.0, 4.0, 4.0], [90.0, 90.0, 90.0]) + for index, species in enumerate(("Fe", "Re", "O") * 32): + cell.addAtom(species, [index / 96.0, 0.0, 0.0], 0.0, 0.0, 1.0) + + with ( + mock.patch.object( + CTRuc, "readWaasmaier", wraps=CTRuc.readWaasmaier + ) as waasmaier, + mock.patch.object( + CTRuc, "readDispersion", wraps=CTRuc.readDispersion + ) as dispersion, + ): + cell.setEnergy(10000.0) + first = np.array(cell.f, copy=True) + cell.setEnergy(10000.0) + self.assertEqual(waasmaier.call_count, 3) + self.assertEqual(dispersion.call_count, 3) + cell.setEnergy(20000.0) + self.assertEqual(waasmaier.call_count, 6) + self.assertEqual(dispersion.call_count, 6) + self.assertFalse(np.allclose(first[:, 11:], cell.f[:, 11:])) + + def test_layer_splitting_defers_or_copies_form_factors(self): + cell = CTRuc.UnitCell([4.0, 4.0, 4.0], [90.0, 90.0, 90.0]) + cell.addAtom("Fe", [0.0, 0.0, 0.0], 0.0, 0.0, 1.0, layer=0) + cell.addAtom("O", [0.0, 0.0, 0.5], 0.0, 0.0, 1.0, layer=1) + layers = cell.split_in_layers() + self.assertFalse(hasattr(cell, "f")) + self.assertTrue(all(not hasattr(layer, "f") for layer in layers.values())) + + cell.setEnergy(20000.0) + layers = cell.split_in_layers() + first_layer = next(iter(layers.values())) + before = cell.f[0, 0] + first_layer.f[0, 0] += 1.0 + self.assertEqual(cell.f[0, 0], before) + self.assertEqual(first_layer._E, 20000.0) + + +if __name__ == "__main__": + unittest.main() From 06d7563e4a5904a83b14aa81839141118ba93e36 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Fri, 24 Jul 2026 13:29:25 -0500 Subject: [PATCH 13/37] build: statically link xxhash for binary releases --- .github/workflows/build-wheels.yml | 3 ++- .github/workflows/release.yml | 3 ++- meson.build | 9 ++++++++- meson_options.txt | 7 +++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index f1d8e46..e6edfff 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -16,7 +16,8 @@ concurrency: env: CIBW_SKIP: "*-musllinux_*" CIBW_BUILD_FRONTEND: "build" - CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--vsenv" + CIBW_CONFIG_SETTINGS: "setup-args=-Dstatic_xxhash=true" + CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--vsenv -Dstatic_xxhash=true" CIBW_TEST_ENVIRONMENT: "PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 MPLBACKEND=Agg QT_QPA_PLATFORM=minimal" CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: >- diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb7771f..80318fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -121,7 +121,8 @@ jobs: CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-* cp314-*" CIBW_SKIP: "*-musllinux_*" CIBW_BUILD_FRONTEND: "build" - CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--vsenv" + CIBW_CONFIG_SETTINGS: "setup-args=-Dstatic_xxhash=true" + CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--vsenv -Dstatic_xxhash=true" CIBW_TEST_ENVIRONMENT: "PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 MPLBACKEND=Agg QT_QPA_PLATFORM=minimal" CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: >- diff --git a/meson.build b/meson.build index ac89e35..80e95da 100644 --- a/meson.build +++ b/meson.build @@ -12,7 +12,14 @@ project( py = import('python').find_installation(pure: false) pybind11_dep = dependency('pybind11', required: true) -xxhash_dep = dependency('libxxhash', fallback: ['xxhash', 'xxhash_dep']) +# The default preserves normal Meson dependency resolution. Wheel builds +# request a static library so they do not acquire a libxxhash runtime +# dependency from the build host; the pinned fallback is itself static. +xxhash_dep = dependency( + 'libxxhash', + static: get_option('static_xxhash'), + fallback: ['xxhash', 'xxhash_dep'], +) cpp = meson.get_compiler('cpp') native_args = [] diff --git a/meson_options.txt b/meson_options.txt index 7538b5f..372fa85 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -4,3 +4,10 @@ option( value: false, description: 'Build C++ extensions with native CPU flags for local benchmarking' ) + +option( + 'static_xxhash', + type: 'boolean', + value: false, + description: 'Force the pinned static xxHash subproject instead of a system library' +) From 1ebf7668ac7c9b0ae913cb7c6c07553106ee35f7 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Fri, 24 Jul 2026 14:50:33 -0500 Subject: [PATCH 14/37] perf: zdensity C++ backend --- benchmarks/ctr_zdensity_accel.ipynb | 208 +++++++ .../ctr_accel_backends.ipynb | 320 +++++++++++ .../ctr_zdensity_accel.ipynb | 113 ++++ .../.virtual_documents/roi_sum_accel.ipynb | 516 ++++++++++++++++++ .../benchmarks/ctr_zdensity_accel.ipynb | 272 +++++++++ doc/source/index.rst | 1 + orgui/datautils/xrayutils/CTRuc.py | 21 + orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp | 188 ++++++- .../datautils/xrayutils/test/test_CTRcalc.py | 18 + 9 files changed, 1651 insertions(+), 6 deletions(-) create mode 100644 benchmarks/ctr_zdensity_accel.ipynb create mode 100644 doc/source/benchmarks/.virtual_documents/ctr_accel_backends.ipynb create mode 100644 doc/source/benchmarks/.virtual_documents/ctr_zdensity_accel.ipynb create mode 100644 doc/source/benchmarks/.virtual_documents/roi_sum_accel.ipynb create mode 100644 doc/source/benchmarks/ctr_zdensity_accel.ipynb diff --git a/benchmarks/ctr_zdensity_accel.ipynb b/benchmarks/ctr_zdensity_accel.ipynb new file mode 100644 index 0000000..4dd80e3 --- /dev/null +++ b/benchmarks/ctr_zdensity_accel.ipynb @@ -0,0 +1,208 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "density-title", + "metadata": {}, + "source": [ + "# Unit-Cell Electron-Density C++ Benchmark\n", + "\n", + "Benchmark `UnitCell.zDensity_G` for the original NumPy implementation and the C++ kernel. The C++ kernel is intentionally the only accelerated path for this calculation; special electron-density callback models retain the Python/SciPy implementation.\n", + "\n", + "Timings exclude one warmup call and measure the standard atomic-density expression only." + ] + }, + { + "cell_type": "markdown", + "id": "density-setup", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Start Jupyter from `benchmarks/` with an installed `orgui` package. The benchmark uses a synthetic unit cell with two coherent domains, so it does not depend on external files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "density-imports", + "metadata": {}, + "outputs": [], + "source": [ + "import statistics\n", + "import time\n", + "\n", + "import numpy as np\n", + "from matplotlib import pyplot as plt\n", + "\n", + "from orgui import __version__, get_build_config\n", + "from orgui.datautils.xrayutils import CTRcalc, CTRuc\n", + "\n", + "\n", + "print(\"orGUI version\", __version__)\n", + "print(get_build_config())\n", + "print(f\"C++ density acceleration available: {CTRuc.HAS_CPP_ACCEL}\")" + ] + }, + { + "cell_type": "markdown", + "id": "density-helpers-title", + "metadata": {}, + "source": [ + "## Benchmark Helpers\n", + "\n", + "The profile coordinate `z` is in Angstrom. `h` and `k` are reference-frame reciprocal coordinates in r.l.u.; the result is complex electron density in electrons per Angstrom cubed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "density-helpers", + "metadata": {}, + "outputs": [], + "source": [ + "def make_cell():\n", + " cell = CTRcalc.UnitCell([3.0, 4.0, 5.0], [90.0, 90.0, 90.0])\n", + " cell.addAtom(\"C\", [0.2, 0.3, 0.4], 0.12, 0.18, 0.8)\n", + " cell.addAtom(\"O\", [0.7, 0.6, 0.1], 0.21, 0.24, 0.5)\n", + " cell.setEnergy(10000.0)\n", + " shifted_domain = np.vstack((np.identity(3).T, [0.1, -0.2, 0.15])).T\n", + " cell.coherentDomainMatrix = [cell.coherentDomainMatrix[0], shifted_domain]\n", + " cell.coherentDomainOccupancy = [0.65, 0.35]\n", + " return cell\n", + "\n", + "\n", + "def time_call(func, repeats=9, warmups=1):\n", + " for _ in range(warmups):\n", + " func()\n", + " timings = []\n", + " for _ in range(repeats):\n", + " start = time.perf_counter()\n", + " func()\n", + " timings.append(time.perf_counter() - start)\n", + " return statistics.median(timings)\n", + "\n", + "\n", + "def format_seconds(value):\n", + " if value < 1e-3:\n", + " return f\"{value * 1e6:.1f} us\"\n", + " return f\"{value * 1e3:.2f} ms\"" + ] + }, + { + "cell_type": "markdown", + "id": "density-correctness-title", + "metadata": {}, + "source": [ + "## Correctness Check\n", + "\n", + "Before timing, verify that the C++ kernel matches the NumPy reference for a multi-domain cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "density-correctness", + "metadata": {}, + "outputs": [], + "source": [ + "if not CTRuc.HAS_CPP_ACCEL:\n", + " raise RuntimeError(\"Build or install orGUI with the C++ extension to run this benchmark.\")\n", + "\n", + "cell = make_cell()\n", + "z = np.linspace(-2.0, 7.0, 2_000)\n", + "CTRuc.set_accel_backend(\"numpy\")\n", + "reference = cell.zDensity_G(z, 0.37, -0.22)\n", + "CTRuc.set_accel_backend(\"cpp\")\n", + "accelerated = cell.zDensity_G(z, 0.37, -0.22)\n", + "np.testing.assert_allclose(accelerated, reference, rtol=1e-12, atol=1e-12)\n", + "print(\"C++ zDensity_G matches NumPy for the multi-domain test cell.\")" + ] + }, + { + "cell_type": "markdown", + "id": "density-benchmark-title", + "metadata": {}, + "source": [ + "## Benchmark Electron-Density Profiles\n", + "\n", + "Use the median runtime across repeated evaluations. The profile lengths span interactive plotting through dense optical-profile grids." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "density-benchmark", + "metadata": {}, + "outputs": [], + "source": [ + "cell = make_cell()\n", + "results = []\n", + "for points in (200, 2_000, 20_000, 200_000):\n", + " z = np.linspace(-2.0, 7.0, points)\n", + " timings = {}\n", + " for backend in (\"numpy\", \"cpp\"):\n", + " CTRuc.set_accel_backend(backend)\n", + " timings[backend] = time_call(lambda: cell.zDensity_G(z, 0.37, -0.22))\n", + " results.append({\n", + " \"points\": points,\n", + " \"numpy_s\": timings[\"numpy\"],\n", + " \"cpp_s\": timings[\"cpp\"],\n", + " \"speedup\": timings[\"numpy\"] / timings[\"cpp\"],\n", + " })\n", + "\n", + "print(f\"{'points':>10} {'numpy median':>16} {'cpp median':>16} {'cpp speedup':>13}\")\n", + "for row in results:\n", + " print(\n", + " f\"{row['points']:10d} {format_seconds(row['numpy_s']):>16} \"\n", + " f\"{format_seconds(row['cpp_s']):>16} {row['speedup']:12.2f}x\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "density-results-title", + "metadata": {}, + "source": [ + "## Benchmark Results\n", + "\n", + "The logarithmic scale shows how the native kernel changes runtime as the number of sampled profile points grows." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "density-plot", + "metadata": {}, + "outputs": [], + "source": [ + "labels = [f\"{row['points']:,}\" for row in results]\n", + "x = np.arange(len(results))\n", + "width = 0.36\n", + "fig, ax = plt.subplots(figsize=(8, 4.5))\n", + "ax.bar(x - width / 2, [row['numpy_s'] for row in results], width, label=\"NumPy\")\n", + "ax.bar(x + width / 2, [row['cpp_s'] for row in results], width, label=\"C++\")\n", + "ax.set_yscale(\"log\")\n", + "ax.set_xticks(x, labels)\n", + "ax.set_xlabel(\"z profile points\")\n", + "ax.set_ylabel(\"Median runtime [s]\")\n", + "ax.grid(axis=\"y\", which=\"both\", alpha=0.25)\n", + "ax.legend()\n", + "fig.tight_layout()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/doc/source/benchmarks/.virtual_documents/ctr_accel_backends.ipynb b/doc/source/benchmarks/.virtual_documents/ctr_accel_backends.ipynb new file mode 100644 index 0000000..f3863ce --- /dev/null +++ b/doc/source/benchmarks/.virtual_documents/ctr_accel_backends.ipynb @@ -0,0 +1,320 @@ + + + + + + + + + +from pathlib import Path +import statistics +import time + +import numpy as np + + +BENCHMARK_DIR = Path.cwd().resolve() +FIXTURE_CANDIDATES = ( + BENCHMARK_DIR / "fixtures", + BENCHMARK_DIR.parents[2] / "benchmarks" / "fixtures", +) +FIXTURE_DIR = next((path for path in FIXTURE_CANDIDATES if path.is_dir()), None) +if FIXTURE_DIR is None: + raise FileNotFoundError( + "Could not find benchmark fixtures. Run from benchmarks/ or execute " + "this notebook from doc/source/benchmarks/." + ) +XPR_PATH = FIXTURE_DIR / "0V12_calculated.xpr" + +if (BENCHMARK_DIR / "orgui").is_dir(): + raise RuntimeError( + "Notebook is running from the repository root. Start Jupyter from " + "benchmarks/ to benchmark the installed package." + ) + +print(f"Benchmark directory: {BENCHMARK_DIR}") +print(f"Fixture: {XPR_PATH}") + + + +!hostname + + + +from orgui import __version__, get_build_config +print("orGUI version", __version__) +print(get_build_config()) + + + +from orgui.datautils import util +from orgui.datautils.xrayutils import CTRcalc, CTRuc + + +AVAILABLE_BACKENDS = ["numpy"] +if CTRuc.HAS_CPP_ACCEL: + AVAILABLE_BACKENDS.append("cpp") +if CTRuc.ctr_numba_accel_available(): + AVAILABLE_BACKENDS.append("numba") + +print(f"Default CTR acceleration backend: {CTRuc.CTR_ACCEL_BACKEND}") +print(f"Available CTR acceleration backends: {AVAILABLE_BACKENDS}") + + + + + + +def set_backend(backend): + CTRuc.set_accel_backend(backend) + + +def load_crystal(): + xtal = CTRcalc.SXRDCrystal.fromFile(XPR_PATH) + pt100 = CTRcalc.UnitCell( + [3.9242, 3.9242, 3.9242], + [90.0, 90.0, 90.0], + ) + xtal.setGlobalReferenceUnitCell( + pt100, + util.z_rotation(np.deg2rad(45.0)), + ) + return xtal + + +def make_rod(n_points): + l_values = np.ascontiguousarray( + np.linspace(0.05, 7.0, n_points), + dtype=np.float64, + ) + zeros = np.zeros_like(l_values) + return zeros, zeros, l_values + + +def time_call(func, repeats=7, warmups=1): + for _ in range(warmups): + func() + + timings = [] + for _ in range(repeats): + start = time.perf_counter() + func() + timings.append(time.perf_counter() - start) + + return { + "min_s": min(timings), + "median_s": statistics.median(timings), + "mean_s": statistics.mean(timings), + "repeats": repeats, + } + + +def format_seconds(value): + if value < 1e-3: + return f"{value * 1e6:8.1f} us" + if value < 1: + return f"{value * 1e3:8.2f} ms" + return f"{value:8.3f} s" + + +def cold_cache_time(func, repeats=7): + """Return a median with the global cache cleared before every call.""" + timings = [] + for _ in range(repeats): + CTRuc.clear_form_factor_cache() + start = time.perf_counter() + func() + timings.append(time.perf_counter() - start) + return statistics.median(timings) + + + + + + + +xtal = load_crystal() +h, k, l = make_rod(2_000) + +set_backend("numpy") +numpy_uc = xtal.uc_bulk.F_uc(h, k, l) +numpy_xtal = xtal.F(h, k, l) + +for backend in AVAILABLE_BACKENDS: + if backend == "numpy": + continue + set_backend(backend) + backend_uc = xtal.uc_bulk.F_uc(h, k, l) + backend_xtal = xtal.F(h, k, l) + np.testing.assert_allclose(backend_uc, numpy_uc, rtol=1e-12, atol=1e-12) + np.testing.assert_allclose(backend_xtal, numpy_xtal, rtol=1e-10, atol=1e-10) + +print("Available accelerated backends match NumPy for UnitCell.F_uc and SXRDCrystal.F") + + + + + + +def benchmark_structure_factor(n_points, repeats=7): + xtal = load_crystal() + h, k, l = make_rod(n_points) + + def run_unitcell(backend): + set_backend(backend) + return lambda: xtal.uc_bulk.F_uc(h, k, l) + + def run_crystal(backend): + set_backend(backend) + return lambda: xtal.F(h, k, l) + + rows = [] + for label, factory in ( + ("UnitCell.F_uc", run_unitcell), + ("SXRDCrystal.F", run_crystal), + ): + stats = { + backend: time_call(factory(backend), repeats=repeats) + for backend in AVAILABLE_BACKENDS + } + row = { + "benchmark": label, + "points": n_points, + } + for backend, backend_stats in stats.items(): + row[f"{backend}_median_s"] = backend_stats["median_s"] + row[f"{backend}_min_s"] = backend_stats["min_s"] + for backend in AVAILABLE_BACKENDS: + if backend == "numpy": + continue + row[f"{backend}_speedup"] = ( + row["numpy_median_s"] / row[f"{backend}_median_s"] + ) + rows.append(row) + return rows + + +results = [] +for n_points in (200, 2_000, 20_000): + results.extend(benchmark_structure_factor(n_points)) + +header = f"{'benchmark':<18} {'points':>8}" +for backend in AVAILABLE_BACKENDS: + header += f" {backend + ' median':>15}" +for backend in AVAILABLE_BACKENDS: + if backend != "numpy": + header += f" {backend + ' speedup':>14}" +print(header) +for row in results: + line = f"{row['benchmark']:<18} {row['points']:>8}" + for backend in AVAILABLE_BACKENDS: + line += f" {format_seconds(row[f'{backend}_median_s']):>15}" + for backend in AVAILABLE_BACKENDS: + if backend != "numpy": + line += f" {row[f'{backend}_speedup']:>13.2f}x" + print(line) + + + + + + +from matplotlib import pyplot as plt + +labels = [f"{row['benchmark']}\n{row['points']} pts" for row in results] +x = np.arange(len(labels)) +width = min(0.8 / len(AVAILABLE_BACKENDS), 0.28) +offsets = (np.arange(len(AVAILABLE_BACKENDS)) - (len(AVAILABLE_BACKENDS) - 1) / 2) * width + +fig, ax = plt.subplots(figsize=(11, 4.5)) +for offset, backend in zip(offsets, AVAILABLE_BACKENDS): + ax.bar( + x + offset, + [row[f"{backend}_median_s"] for row in results], + width, + label=backend, + ) + +ax.set_ylabel("Median runtime [s]") +ax.set_yscale("log") +ax.set_xticks(x) +ax.set_xticklabels(labels, rotation=30, ha="right") +ax.legend(title="Backend") +ax.grid(axis="y", which="both", alpha=0.25) +fig.tight_layout() + + + +def benchmark_form_factor_cache(n_points, repeats=7): + if not CTRuc.HAS_CPP_ACCEL: + return None + + xtal = load_crystal() + h, k, l = make_rod(n_points) + + def run_unitcell(): + return xtal.uc_bulk.F_uc(h, k, l) + + set_backend("cpp") + original_budget = CTRuc.form_factor_cache_stats()["budget_bytes"] + CTRuc.clear_form_factor_cache() + CTRuc.reset_form_factor_cache_stats() + try: + CTRuc.set_form_factor_cache_budget(0) + uncached = time_call(run_unitcell, repeats=repeats)["median_s"] + + CTRuc.set_form_factor_cache_budget(original_budget) + cold = cold_cache_time(run_unitcell, repeats=repeats) + + CTRuc.clear_form_factor_cache() + run_unitcell() + warm = time_call(run_unitcell, repeats=repeats, warmups=0)["median_s"] + stats = CTRuc.form_factor_cache_stats() + finally: + CTRuc.clear_form_factor_cache() + CTRuc.set_form_factor_cache_budget(original_budget) + + return { + "points": n_points, + "uncached_median_s": uncached, + "cold_median_s": cold, + "warm_median_s": warm, + "warm_speedup": uncached / warm, + "resident_bytes": stats["resident_bytes"], + "expected_bytes": CTRuc.form_factor_cache_expected_bytes( + n_points, stats["species_entries"] + ), + "hits": stats["hits"], + "misses": stats["misses"], + "evictions": stats["evictions"], + } + + +cache_results = [ + benchmark_form_factor_cache(n_points) + for n_points in (2_000, 20_000, 200_000) +] +cache_results = [result for result in cache_results if result is not None] + +if not cache_results: + print("C++ extension unavailable; cache benchmark skipped.") +else: + print( + f"{'points':>8} {'uncached':>14} {'cold':>14} {'warm':>14} " + f"{'speedup':>9} {'hits':>7} {'misses':>7} {'evictions':>10} " + f"{'resident':>12} {'expected':>12}" + ) + for row in cache_results: + print( + f"{row['points']:8d} " + f"{format_seconds(row['uncached_median_s']):>14} " + f"{format_seconds(row['cold_median_s']):>14} " + f"{format_seconds(row['warm_median_s']):>14} " + f"{row['warm_speedup']:8.2f}x " + f"{row['hits']:7d} {row['misses']:7d} {row['evictions']:10d} " + f"{row['resident_bytes']:11d}B {row['expected_bytes']:11d}B" + ) + + + diff --git a/doc/source/benchmarks/.virtual_documents/ctr_zdensity_accel.ipynb b/doc/source/benchmarks/.virtual_documents/ctr_zdensity_accel.ipynb new file mode 100644 index 0000000..2459620 --- /dev/null +++ b/doc/source/benchmarks/.virtual_documents/ctr_zdensity_accel.ipynb @@ -0,0 +1,113 @@ + + + + + + +import statistics +import time + +import numpy as np +from matplotlib import pyplot as plt + +from orgui import __version__, get_build_config +from orgui.datautils.xrayutils import CTRcalc, CTRuc + + +print("orGUI version", __version__) +print(get_build_config()) +print(f"C++ density acceleration available: {CTRuc.HAS_CPP_ACCEL}") + + + + + +def make_cell(): + cell = CTRcalc.UnitCell([3.0, 4.0, 5.0], [90.0, 90.0, 90.0]) + cell.addAtom("C", [0.2, 0.3, 0.4], 0.12, 0.18, 0.8) + cell.addAtom("O", [0.7, 0.6, 0.1], 0.21, 0.24, 0.5) + cell.setEnergy(10000.0) + shifted_domain = np.vstack((np.identity(3).T, [0.1, -0.2, 0.15])).T + cell.coherentDomainMatrix = [cell.coherentDomainMatrix[0], shifted_domain] + cell.coherentDomainOccupancy = [0.65, 0.35] + return cell + + +def time_call(func, repeats=9, warmups=1): + for _ in range(warmups): + func() + timings = [] + for _ in range(repeats): + start = time.perf_counter() + func() + timings.append(time.perf_counter() - start) + return statistics.median(timings) + + +def format_seconds(value): + if value < 1e-3: + return f"{value * 1e6:.1f} us" + return f"{value * 1e3:.2f} ms" + + + + + +if not CTRuc.HAS_CPP_ACCEL: + raise RuntimeError("Build or install orGUI with the C++ extension to run this benchmark.") + +cell = make_cell() +z = np.linspace(-2.0, 7.0, 2_000) +CTRuc.set_accel_backend("numpy") +reference = cell.zDensity_G(z, 0.37, -0.22) +CTRuc.set_accel_backend("cpp") +accelerated = cell.zDensity_G(z, 0.37, -0.22) +np.testing.assert_allclose(accelerated, reference, rtol=1e-12, atol=1e-12) +print("C++ zDensity_G matches NumPy for the multi-domain test cell.") + + + + + +cell = make_cell() +results = [] +for points in (200, 2_000, 20_000, 200_000): + z = np.linspace(-2.0, 7.0, points) + timings = {} + for backend in ("numpy", "cpp"): + CTRuc.set_accel_backend(backend) + timings[backend] = time_call(lambda: cell.zDensity_G(z, 0.37, -0.22)) + results.append({ + "points": points, + "numpy_s": timings["numpy"], + "cpp_s": timings["cpp"], + "speedup": timings["numpy"] / timings["cpp"], + }) + +print(f"{'points':>10} {'numpy median':>16} {'cpp median':>16} {'cpp speedup':>13}") +for row in results: + print( + f"{row['points']:10d} {format_seconds(row['numpy_s']):>16} " + f"{format_seconds(row['cpp_s']):>16} {row['speedup']:12.2f}x" + ) + + + + + +labels = [f"{row['points']:,}" for row in results] +x = np.arange(len(results)) +width = 0.36 +fig, ax = plt.subplots(figsize=(8, 4.5)) +ax.bar(x - width / 2, [row['numpy_s'] for row in results], width, label="NumPy") +ax.bar(x + width / 2, [row['cpp_s'] for row in results], width, label="C++") +ax.set_yscale("log") +ax.set_xticks(x, labels) +ax.set_xlabel("z profile points") +ax.set_ylabel("Median runtime [s]") +ax.grid(axis="y", which="both", alpha=0.25) +ax.legend() +fig.tight_layout() + + + diff --git a/doc/source/benchmarks/.virtual_documents/roi_sum_accel.ipynb b/doc/source/benchmarks/.virtual_documents/roi_sum_accel.ipynb new file mode 100644 index 0000000..d5df082 --- /dev/null +++ b/doc/source/benchmarks/.virtual_documents/roi_sum_accel.ipynb @@ -0,0 +1,516 @@ + + + + + + + + + +from pathlib import Path +from types import SimpleNamespace +import concurrent.futures +import platform +import statistics +import tempfile +import time + +import numpy as np + + +BENCHMARK_DIR = Path.cwd().resolve() +if (BENCHMARK_DIR / "orgui").is_dir(): + raise RuntimeError( + "Notebook is running from the repository root. Start Jupyter from " + "benchmarks/ to benchmark the installed package." + ) + +import benchmark_roi_sum_accel as roi_bench + +print(f"Benchmark directory: {BENCHMARK_DIR}") +print(f"Benchmark module: {Path(roi_bench.__file__).resolve()}") + + + +!hostname + + + +from orgui import __version__, get_build_config + +print("orGUI version", __version__) +print(get_build_config()) +print("Python", platform.python_version()) +print("NumPy", np.__version__) + + + +cpp_backend = roi_bench.import_cpp_backend() +backends = { + "numpy": roi_bench.wrap_numpy_backend(), + "cpp": roi_bench.wrap_backend(cpp_backend), +} +numba_backend = roi_bench.try_build_numba_backend() +if numba_backend is not None: + backends["numba"] = roi_bench.wrap_backend(SimpleNamespace(**numba_backend)) + +print("Available ROI backends:", list(backends)) +print("C++ backend:", getattr(cpp_backend, "__file__", "installed module")) +polybg_func = cpp_backend.processImage_polybg_Carr + + + + + + +POLYBG_ORDER = 2 +POLYBG_BACKEND = f"cpp_polybg_order{POLYBG_ORDER}" + + +def make_args(**overrides): + values = { + "seed": 12345, + "repeats": 10, + "rois": 50, + "shape": roi_bench.PILATUS_6M_SHAPE, + "roi_min": 10, + "roi_max": 100, + "bg_min": 5, + "bg_max": 50, + "signal_mean": 100.0, + "background_min": 5.0, + "background_max": 50.0, + "mask_fraction": 0.01, + "disk_images": 0, + "threads": 1, + "disk_dir": None, + "json": None, + "no_numba": False, + } + values.update(overrides) + return SimpleNamespace(**values) + + +def format_seconds(value): + if value < 1e-3: + return f"{value * 1e6:8.1f} us" + if value < 1: + return f"{value * 1e3:8.2f} ms" + return f"{value:8.3f} s" + + +def print_table(rows, columns): + widths = {key: max(len(str(label)), *(len(str(row[key])) for row in rows)) for key, label in columns} + print(" ".join(str(label).rjust(widths[key]) for key, label in columns)) + for row in rows: + print(" ".join(str(row[key]).rjust(widths[key]) for key, _ in columns)) + + +def run_one_polybg_backend(func, base_image, mask, correction, rois, order=POLYBG_ORDER): + image = base_image.copy(order="C") + all_counters = np.zeros((rois[0].shape[0], 4), dtype=np.float64) + correction_counters = np.zeros_like(all_counters) + start = time.perf_counter() + func(image, mask, correction, *rois, all_counters, correction_counters, order) + duration = time.perf_counter() - start + return duration, (all_counters, correction_counters) + + +def benchmark_polybg_backend(func, base_image, mask, correction, rois, repeats, order=POLYBG_ORDER): + warmup_duration, counters = run_one_polybg_backend(func, base_image, mask, correction, rois, order) + timings = [] + for _ in range(repeats): + duration, counters = run_one_polybg_backend(func, base_image, mask, correction, rois, order) + timings.append(duration) + return { + "name": POLYBG_BACKEND, + "warmup_seconds": warmup_duration, + "median_seconds": statistics.median(timings), + "min_seconds": min(timings), + "max_seconds": max(timings), + "counters": counters, + } + + +def integrate_polybg_image(func, image, mask, correction, rois, order=POLYBG_ORDER): + all_counters = np.zeros((rois[0].shape[0], 4), dtype=np.float64) + correction_counters = np.zeros_like(all_counters) + func(image, mask, correction, *rois, all_counters, correction_counters, order) + return all_counters, correction_counters + + +def process_polybg_disk_image(path, func, mask, correction, rois, order=POLYBG_ORDER): + image = np.load(path) + return integrate_polybg_image(func, image, mask, correction, rois, order) + + +def background_pixel_counts(mask, rois): + counts = np.zeros(rois[0].shape[0], dtype=np.int64) + for roi_group in rois[1:]: + for i, roi in enumerate(roi_group): + ys = slice(roi[1, 0], roi[1, 1]) + xs = slice(roi[0, 0], roi[0, 1]) + counts[i] += np.count_nonzero(~mask[ys, xs]) + return counts + + +base_args = make_args() +print("Detector Pilatus 6M") +print("shape", base_args.shape) +print("roi_size_px", (base_args.roi_min, base_args.roi_max)) +print("background_strip_px", (base_args.bg_min, base_args.bg_max)) +print("background_counts", (base_args.background_min, base_args.background_max)) +print("mask_fraction", base_args.mask_fraction) +print("repeats", base_args.repeats) +print("fitted_background_order", POLYBG_ORDER) + + + + + + +args = make_args(rois=20, repeats=1) +image, background, mask, correction, rois = roi_bench.make_inputs(args) +reference = None +for name, func in backends.items(): + result = roi_bench.benchmark_backend( + name, + func, + image, + background, + mask, + correction, + rois, + repeats=1, + ) + if name == "numpy": + reference = result["counters"] + else: + roi_bench.check_results(reference, result["counters"], name) + +_, polybg_counters = run_one_polybg_backend(polybg_func, image, mask, correction, rois) +np.testing.assert_allclose(polybg_counters[0][:, 1], reference[0][:, 1]) +np.testing.assert_allclose(polybg_counters[0][:, 3], reference[0][:, 3]) +assert np.isfinite(polybg_counters[0]).all() +assert np.isfinite(polybg_counters[1]).all() + +print("Available accelerated ROI strip-background backends match NumPy counters") +print("C++ second-order fitted-background path returned finite counters with matching pixel counts") + + + + + + +ROI_COUNTS = (1, 5, 20, 50, 100, 500) +roi_results = [] + +for n_rois in ROI_COUNTS: + args = make_args(rois=n_rois) + image, background, mask, correction, rois = roi_bench.make_inputs(args) + bg_counts = background_pixel_counts(mask, rois) + bg_total_px = int(bg_counts.sum()) + bg_median_px = float(np.median(bg_counts)) + bg_min_px = int(bg_counts.min()) + bg_max_px = int(bg_counts.max()) + + reference = None + row_results = {} + for name, func in backends.items(): + result = roi_bench.benchmark_backend( + name, + func, + image, + background, + mask, + correction, + rois, + repeats=args.repeats, + ) + if name == "numpy": + reference = result["counters"] + else: + roi_bench.check_results(reference, result["counters"], f"{name} {n_rois} ROIs") + counters = result.pop("counters") + result["counter_checksum"] = float(sum(np.nansum(counter) for counter in counters)) + row_results[name] = result + + polybg_result = benchmark_polybg_backend( + polybg_func, + image, + mask, + correction, + rois, + repeats=args.repeats, + ) + polybg_counters = polybg_result.pop("counters") + polybg_result["counter_checksum"] = float(sum(np.nansum(counter) for counter in polybg_counters)) + row_results[POLYBG_BACKEND] = polybg_result + + numpy_median = row_results["numpy"]["median_seconds"] + for name, result in row_results.items(): + roi_results.append( + { + "rois": n_rois, + "backend": name, + "median_s": result["median_seconds"], + "min_s": result["min_seconds"], + "warmup_s": result["warmup_seconds"], + "speedup_vs_numpy": numpy_median / result["median_seconds"], + "bg_total_px": bg_total_px, + "bg_median_px": bg_median_px, + "bg_min_px": bg_min_px, + "bg_max_px": bg_max_px, + } + ) + +summary_rows = [ + { + "rois": row["rois"], + "backend": row["backend"], + "bg_total": row["bg_total_px"], + "bg_median": f"{row['bg_median_px']:.0f}", + "bg_range": f"{row['bg_min_px']}-{row['bg_max_px']}", + "median": format_seconds(row["median_s"]).strip(), + "min": format_seconds(row["min_s"]).strip(), + "speedup": f"{row['speedup_vs_numpy']:.2f}x", + } + for row in roi_results +] +print_table( + summary_rows, + ( + ("rois", "ROIs"), + ("backend", "backend"), + ("bg_total", "bg px total"), + ("bg_median", "bg px/ROI median"), + ("bg_range", "bg px/ROI range"), + ("median", "median"), + ("min", "min"), + ("speedup", "speedup"), + ), +) + + + + + + +from matplotlib import pyplot as plt + +fig, axes = plt.subplots(1, 2, figsize=(12, 4.5)) +ax_time, ax_speedup = axes +plot_backends = list(backends) + [POLYBG_BACKEND] + +for backend in plot_backends: + rows = [row for row in roi_results if row["backend"] == backend] + ax_time.plot( + [row["rois"] for row in rows], + [row["median_s"] for row in rows], + marker="o", + label=backend, + ) + if backend != "numpy": + ax_speedup.plot( + [row["rois"] for row in rows], + [row["speedup_vs_numpy"] for row in rows], + marker="o", + label=backend, + ) + +ax_time.set_title("Runtime by ROI count") +ax_time.set_xlabel("ROIs") +ax_time.set_ylabel("Median runtime per image [s]") +ax_time.set_xscale("log") +ax_time.set_yscale("log") +ax_time.set_xticks(ROI_COUNTS) +ax_time.get_xaxis().set_major_formatter(plt.ScalarFormatter()) +ax_time.grid(which="both", alpha=0.25) +ax_time.legend(title="Backend") + +ax_speedup.set_title("Acceleration vs NumPy") +ax_speedup.set_xlabel("ROIs") +ax_speedup.set_ylabel("Median speedup [x]") +ax_speedup.set_xscale("log") +ax_speedup.set_xticks(ROI_COUNTS) +ax_speedup.get_xaxis().set_major_formatter(plt.ScalarFormatter()) +ax_speedup.grid(which="both", alpha=0.25) +ax_speedup.legend(title="Backend") + +fig.tight_layout() + + + + + + +THREAD_COUNTS = (1, 2, 4, 8, 16) +DISK_IMAGES = 200 +THREAD_ROIS = 100 +thread_results = [] + +thread_args = make_args(rois=THREAD_ROIS, disk_images=DISK_IMAGES) +image, background, mask, correction, rois = roi_bench.make_inputs(thread_args) +bg_counts = background_pixel_counts(mask, rois) +thread_bg_total_px = int(bg_counts.sum()) +thread_bg_median_px = float(np.median(bg_counts)) +thread_bg_min_px = int(bg_counts.min()) +thread_bg_max_px = int(bg_counts.max()) + +with tempfile.TemporaryDirectory() as tmp: + disk_dir = Path(tmp) + image_paths = roi_bench.make_disk_images(thread_args, disk_dir) + print(f"Disk image directory: {disk_dir}") + print("Disk image format: .npy float64") + print(f"Background pixels total per image: {thread_bg_total_px}") + print( + "Background pixels per ROI: " + f"median {thread_bg_median_px:.0f}, range {thread_bg_min_px}-{thread_bg_max_px}" + ) + + disk_backend_specs = [(name, func, "strip") for name, func in backends.items()] + disk_backend_specs.append((POLYBG_BACKEND, polybg_func, "polybg")) + + for backend, func, mode in disk_backend_specs: + start = time.perf_counter() + if mode == "polybg": + serial_counters = [ + process_polybg_disk_image(path, func, mask, correction, rois) + for path in image_paths + ] + else: + serial_counters = [ + roi_bench.process_disk_image(path, func, background, mask, correction, rois) + for path in image_paths + ] + serial_s = time.perf_counter() - start + serial_checksum = roi_bench.checksum_counters(serial_counters) + + for threads in THREAD_COUNTS: + start = time.perf_counter() + with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor: + if mode == "polybg": + futures = [ + executor.submit( + process_polybg_disk_image, + path, + func, + mask, + correction, + rois, + ) + for path in image_paths + ] + else: + futures = [ + executor.submit( + roi_bench.process_disk_image, + path, + func, + background, + mask, + correction, + rois, + ) + for path in image_paths + ] + threaded_counters = [future.result() for future in futures] + threaded_s = time.perf_counter() - start + + for i, (serial, threaded) in enumerate(zip(serial_counters, threaded_counters, strict=True)): + if mode == "polybg": + for expected, actual in zip(serial, threaded, strict=True): + np.testing.assert_allclose(actual, expected, rtol=1e-12, atol=1e-9) + else: + roi_bench.check_results(serial, threaded, f"{backend} threaded image {i}") + + thread_results.append( + { + "backend": backend, + "threads": threads, + "images": DISK_IMAGES, + "rois": THREAD_ROIS, + "serial_s": serial_s, + "threaded_s": threaded_s, + "thread_speedup": serial_s / threaded_s, + "threaded_images_per_s": DISK_IMAGES / threaded_s, + "bg_total_px": thread_bg_total_px, + "bg_median_px": thread_bg_median_px, + "bg_min_px": thread_bg_min_px, + "bg_max_px": thread_bg_max_px, + "serial_checksum": serial_checksum, + "threaded_checksum": roi_bench.checksum_counters(threaded_counters), + } + ) + +summary_rows = [ + { + "backend": row["backend"], + "threads": row["threads"], + "bg_total": row["bg_total_px"], + "bg_median": f"{row['bg_median_px']:.0f}", + "bg_range": f"{row['bg_min_px']}-{row['bg_max_px']}", + "serial": format_seconds(row["serial_s"]).strip(), + "threaded": format_seconds(row["threaded_s"]).strip(), + "gain": f"{row['thread_speedup']:.2f}x", + "ips": f"{row['threaded_images_per_s']:.2f}", + } + for row in thread_results +] +print_table( + summary_rows, + ( + ("backend", "backend"), + ("threads", "threads"), + ("bg_total", "bg px total"), + ("bg_median", "bg px/ROI median"), + ("bg_range", "bg px/ROI range"), + ("serial", "serial"), + ("threaded", "threaded"), + ("gain", "thread_gain"), + ("ips", "images/s"), + ), +) + + + + + + +fig, axes = plt.subplots(1, 2, figsize=(12, 4.5)) +ax_time, ax_rate = axes + +for backend in plot_backends: + rows = [row for row in thread_results if row["backend"] == backend] + ax_time.plot( + [row["threads"] for row in rows], + [row["threaded_s"] for row in rows], + marker="o", + label=backend, + ) + ax_rate.plot( + [row["threads"] for row in rows], + [row["threaded_images_per_s"] for row in rows], + marker="o", + label=backend, + ) + +ax_time.set_title("50 images: read + integrate") +ax_time.set_xlabel("Worker threads") +ax_time.set_ylabel("Total threaded runtime [s]") +ax_time.set_xticks(THREAD_COUNTS) +ax_time.grid(alpha=0.25) +ax_time.legend(title="Backend") + +ax_rate.set_title("50 images: throughput") +ax_rate.set_xlabel("Worker threads") +ax_rate.set_ylabel("Images / s") +ax_rate.set_xticks(THREAD_COUNTS) +ax_rate.grid(alpha=0.25) +ax_rate.legend(title="Backend") + +fig.tight_layout() + + + + diff --git a/doc/source/benchmarks/ctr_zdensity_accel.ipynb b/doc/source/benchmarks/ctr_zdensity_accel.ipynb new file mode 100644 index 0000000..353369d --- /dev/null +++ b/doc/source/benchmarks/ctr_zdensity_accel.ipynb @@ -0,0 +1,272 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "density-title", + "metadata": {}, + "source": [ + "# Unit-Cell Electron-Density C++ Benchmark\n", + "\n", + "Benchmark `UnitCell.zDensity_G` for the original NumPy implementation and the C++ kernel. The C++ kernel is intentionally the only accelerated path for this calculation; special electron-density callback models retain the Python/SciPy implementation.\n", + "\n", + "Timings exclude one warmup call and measure the standard atomic-density expression only." + ] + }, + { + "cell_type": "markdown", + "id": "density-setup", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Start Jupyter from `benchmarks/` with an installed `orgui` package. The benchmark uses a synthetic unit cell with two coherent domains, so it does not depend on external files." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "density-imports", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "orGUI version 1.5.1.dev72+g06d7563e4.d20260724\n", + "{'native_optimization': False, 'target_cpu': 'generic', 'allowed_instruction_set': [], 'native_arguments': '', 'cpp_compiler_id': 'msvc', 'cpp_compiler_version': '19.43.34810', 'host_system': 'windows', 'host_cpu_family': 'x86_64', 'host_cpu': 'x86_64', 'available': True}\n", + "C++ density acceleration available: True\n" + ] + } + ], + "source": [ + "import statistics\n", + "import time\n", + "\n", + "import numpy as np\n", + "from matplotlib import pyplot as plt\n", + "\n", + "from orgui import __version__, get_build_config\n", + "from orgui.datautils.xrayutils import CTRcalc, CTRuc\n", + "\n", + "\n", + "print(\"orGUI version\", __version__)\n", + "print(get_build_config())\n", + "print(f\"C++ density acceleration available: {CTRuc.HAS_CPP_ACCEL}\")" + ] + }, + { + "cell_type": "markdown", + "id": "density-helpers-title", + "metadata": {}, + "source": [ + "## Benchmark Helpers\n", + "\n", + "The profile coordinate `z` is in Angstrom. `h` and `k` are reference-frame reciprocal coordinates in r.l.u.; the result is complex electron density in electrons per Angstrom cubed." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "density-helpers", + "metadata": {}, + "outputs": [], + "source": [ + "def make_cell():\n", + " cell = CTRcalc.UnitCell([3.0, 4.0, 5.0], [90.0, 90.0, 90.0])\n", + " cell.addAtom(\"C\", [0.2, 0.3, 0.4], 0.12, 0.18, 0.8)\n", + " cell.addAtom(\"O\", [0.7, 0.6, 0.1], 0.21, 0.24, 0.5)\n", + " cell.setEnergy(10000.0)\n", + " shifted_domain = np.vstack((np.identity(3).T, [0.1, -0.2, 0.15])).T\n", + " cell.coherentDomainMatrix = [cell.coherentDomainMatrix[0], shifted_domain]\n", + " cell.coherentDomainOccupancy = [0.65, 0.35]\n", + " return cell\n", + "\n", + "\n", + "def time_call(func, repeats=9, warmups=1):\n", + " for _ in range(warmups):\n", + " func()\n", + " timings = []\n", + " for _ in range(repeats):\n", + " start = time.perf_counter()\n", + " func()\n", + " timings.append(time.perf_counter() - start)\n", + " return statistics.median(timings)\n", + "\n", + "\n", + "def format_seconds(value):\n", + " if value < 1e-3:\n", + " return f\"{value * 1e6:.1f} us\"\n", + " return f\"{value * 1e3:.2f} ms\"" + ] + }, + { + "cell_type": "markdown", + "id": "density-correctness-title", + "metadata": {}, + "source": [ + "## Correctness Check\n", + "\n", + "Before timing, verify that the C++ kernel matches the NumPy reference for a multi-domain cell." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "density-correctness", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "C++ zDensity_G matches NumPy for the multi-domain test cell.\n" + ] + } + ], + "source": [ + "if not CTRuc.HAS_CPP_ACCEL:\n", + " raise RuntimeError(\"Build or install orGUI with the C++ extension to run this benchmark.\")\n", + "\n", + "cell = make_cell()\n", + "z = np.linspace(-2.0, 7.0, 2_000)\n", + "CTRuc.set_accel_backend(\"numpy\")\n", + "reference = cell.zDensity_G(z, 0.37, -0.22)\n", + "CTRuc.set_accel_backend(\"cpp\")\n", + "accelerated = cell.zDensity_G(z, 0.37, -0.22)\n", + "np.testing.assert_allclose(accelerated, reference, rtol=1e-12, atol=1e-12)\n", + "print(\"C++ zDensity_G matches NumPy for the multi-domain test cell.\")" + ] + }, + { + "cell_type": "markdown", + "id": "density-benchmark-title", + "metadata": {}, + "source": [ + "## Benchmark Electron-Density Profiles\n", + "\n", + "Use the median runtime across repeated evaluations. The profile lengths span interactive plotting through dense optical-profile grids." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "density-benchmark", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " points numpy median cpp median cpp speedup\n", + " 200 518.0 us 228.0 us 2.27x\n", + " 2000 866.9 us 754.1 us 1.15x\n", + " 20000 5.51 ms 4.42 ms 1.25x\n", + " 200000 121.83 ms 42.75 ms 2.85x\n" + ] + } + ], + "source": [ + "cell = make_cell()\n", + "results = []\n", + "for points in (200, 2_000, 20_000, 200_000):\n", + " z = np.linspace(-2.0, 7.0, points)\n", + " timings = {}\n", + " for backend in (\"numpy\", \"cpp\"):\n", + " CTRuc.set_accel_backend(backend)\n", + " timings[backend] = time_call(lambda: cell.zDensity_G(z, 0.37, -0.22))\n", + " results.append({\n", + " \"points\": points,\n", + " \"numpy_s\": timings[\"numpy\"],\n", + " \"cpp_s\": timings[\"cpp\"],\n", + " \"speedup\": timings[\"numpy\"] / timings[\"cpp\"],\n", + " })\n", + "\n", + "print(f\"{'points':>10} {'numpy median':>16} {'cpp median':>16} {'cpp speedup':>13}\")\n", + "for row in results:\n", + " print(\n", + " f\"{row['points']:10d} {format_seconds(row['numpy_s']):>16} \"\n", + " f\"{format_seconds(row['cpp_s']):>16} {row['speedup']:12.2f}x\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "density-results-title", + "metadata": {}, + "source": [ + "## Benchmark Results\n", + "\n", + "The logarithmic scale shows how the native kernel changes runtime as the number of sampled profile points grows." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "density-plot", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAG4CAYAAADYN3EQAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAPRxJREFUeJzt3Xl8VPW9//H3TFa2bCBLStikStk1eGWxgK1K0UpRq5RbFRStaBQVXFBvZbkUaG+L3JYBlCpYayu1KJdWXGLLKngpIBWEgkgQLKEpZMgEwYRkzu+P3szPkARm5kzynTPn9Xw88ngwZ74z38+Ez8ycT845n6/HsixLAAAAAGCD13QAAAAAAJyPwgIAAACAbRQWAAAAAGyjsAAAAABgG4UFAAAAANsoLAAAAADYRmEBAAAAwDYKCwAAAAC2JZsOIN4Fg0EdOXJErVq1ksfjMR0OAAAA0GQsy1J5eblyc3Pl9Z77mASFxXkcOXJEeXl5psMAAAAAjDl8+LA6dux4zjEUFufRqlUrSf/6ZWZkZBiOBgAAAGg6gUBAeXl5oX3ic6GwOI+a058yMjIoLAAAAOBK4VwSwMXbAAAAAGyjsAAAAABgG4UFAAAAANu4xiIGgsGgKisrTYfhWikpKUpKSjIdBgAAgKtRWNhUWVmpoqIiBYNB06G4WlZWltq3b89aIwAAAIZQWNhgWZaKi4uVlJSkvLy88y4agtizLEunTp1SSUmJJKlDhw6GIwIAAHAnCgsbqqqqdOrUKeXm5qp58+amw3GtZs2aSZJKSkrUtm1bTosCAAAwgD+x21BdXS1JSk1NNRwJagq7M2fOGI4EAADAnSgsYoDz+s3j/wAAAMAsCgsAAAAAtlFYAAAAALCNi7cbQZepbzTpfAfnXhfR+PHjx+vFF1/UnDlzNHXq1ND2lStX6oYbbpBlWbEOsZYvn7bUsmVLXXzxxXryySd14403Nuq8AAAAaDwcsXCp9PR0/fjHP5bf7zcy/9KlS1VcXKy//OUv6tevn26++WZt3rzZSCwAAACwj8LCpa666iq1b99ec+bMqff+6dOnq3///rW2zZ8/X126dAndHj9+vEaPHq3Zs2erXbt2ysrK0owZM1RVVaVHH31UOTk56tixo1544YU6z1+zoF2PHj20ePFipaena9WqVVq/fr1SUlJ09OjRWuOnTJmioUOH2n7dAAAAaBycCuVSSUlJmj17tv793/9dkyZNUseOHaN6nj//+c/q2LGj1q9fr/fee08TJkzQ5s2bNXToUP3v//6vli9frokTJ+rqq69WXl5evc+RkpKi5ORknTlzRkOHDlW3bt300ksv6dFHH5X0r/VCfv3rX2vu3LlRv14AABJFU59yHU8iPf0bTYsjFi52ww03qH///po2bVrUz5GTk6Of//znuvjii3XnnXfq4osv1qlTp/Tkk0/qq1/9qp544gmlpqbqvffeq/fxFRUVmjVrlgKBgL75zW9KkiZMmKClS5eGxrzxxhs6deqUbrnllqjjBAAAQOOisHC5H//4x3rxxRe1e/fuqB7fq1cveb3/P43atWunPn36hG4nJSWpdevWKikpqfW4sWPHqmXLlmrevLnmzZunn/70pxo5cqSkf51itX//fr3//vuSpBdeeEG33HKLWrRoEVWMAAAAaHycCuVyQ4cO1YgRI/Tkk09q/Pjxoe1er7dOd6j6VrVOSUmpddvj8dS7LRgM1tr2zDPP6KqrrlJGRobatm1b6762bdvq+uuv19KlS9WtWzetXr1aa9eujeLVAQAAoKlQWEBz585V//79ddFFF4W2XXDBBTp69Kgsywq1h92xY0fM5mzfvr26d+/e4P133XWXvve976ljx4668MILNWTIkJjNDQAAgNhzxalQN9xwg7Kzs/Xd737XdChxqU+fPvr+97+vX/ziF6Ftw4cP1z//+U/95Cc/0SeffCKfz6c333yzyWIaMWKEMjMzNWvWLN1xxx1NNi8AAACi44rCYtKkSfrVr35lOoy49p//+Z+1Tn362te+poULF8rn86lfv37asmWLHnnkkSaLx+v1avz48aqurtbtt9/eZPMCAAAgOh6rsZdZjhNr167VggUL9Pvf/z6ixwUCAWVmZqqsrEwZGRm17vviiy9UVFSkrl27Kj09PZbhQtLdd9+tf/zjH1q1atV5x/J/AQBwC9rNoimda1/4bMaPWKxfv17XX3+9cnNz5fF4tHLlyjpjFi5cGNphzM/P14YNG5o+UDSZsrIyvfvuu3r55Zf1wAMPmA4HAAAAYTB+8fbnn3+ufv366Y477tBNN91U5/7ly5froYce0sKFCzVkyBA9++yzGjlypHbv3q1OnTpJkvLz81VRUVHnse+8845yc3Mb/TUgtr7zne9oy5Ytuuee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zVeJnDUhEAgoOztbZWVl590XjmqBPElq3ry5BgwYEO3DHcfr9crrdenhNwBIcEF5TIdghFfOPiUoag7/PndrvkrkrJmpw5877MLizjvvDGvcCy+8EPbkAAAAABJD2IXFsmXL1LlzZ11yySWK8uwpAAAAAAkq7MJi4sSJeuWVV3TgwAHdeeeduvXWW5WTk9OYsQEAAABwiLBPmlq4cKGKi4v1+OOP6w9/+IPy8vJ0yy236O233+YIBgAAAOByEV0JkpaWprFjx6qwsFC7d+9Wr169dN9996lz5846efJkY8UIAAAAIM5FfYm5x+ORx+ORZVksHAcAAAC4XESFRUVFhX7729/q6quv1sUXX6ydO3dqwYIFOnToUMKuYwEAAADg/MK+ePu+++7TK6+8ok6dOumOO+7QK6+8otatWzdmbAAAAAAcIuzCYvHixerUqZO6du2qdevWad26dfWOe+2112IWHAAAAABnCLuwuP322+XxuHelRwAAAAANi2iBPAAAAACoT9RdoQAAAACgBoUFAAAAANsoLAAAAADYRmEBAAAAwDYKCwAAAAC2UVgAAAAAsI3CAgAAAIBtFBYAAAAAbKOwAAAAAGAbhQUAAAAA2ygsAAAAANhGYQEAAADANgoLAAAAALZRWAAAAACwjcICAAAAgG0UFgAAAABso7AAAAAAYBuFBQAAAADbEr6wKC8v12WXXab+/furT58+WrJkiemQAAAAgISTbDqAxta8eXOtW7dOzZs316lTp9S7d2/deOONat26tenQAAAAgISR8EcskpKS1Lx5c0nSF198oerqalmWZTgqAAAAILEYLyzWr1+v66+/Xrm5ufJ4PFq5cmWdMQsXLlTXrl2Vnp6u/Px8bdiwIaI5Tpw4oX79+qljx4567LHH1KZNmxhFDwAAAECKg8Li888/V79+/bRgwYJ671++fLkeeughPfXUU/rggw/09a9/XSNHjtShQ4dCY/Lz89W7d+86P0eOHJEkZWVl6a9//auKior0m9/8Rv/4xz+a5LUBAAAAbmH8GouRI0dq5MiRDd4/b948TZgwQXfddZckaf78+Xr77be1aNEizZkzR5K0bdu2sOZq166d+vbtq/Xr1+vmm2+ud0xFRYUqKipCtwOBgCQpGAwqGAyGNQ8AwFm8cucpskHzf180w+Hf527NV4mcNTN1+HMbLyzOpbKy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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "labels = [f\"{row['points']:,}\" for row in results]\n", + "x = np.arange(len(results))\n", + "width = 0.36\n", + "fig, ax = plt.subplots(figsize=(8, 4.5))\n", + "ax.bar(x - width / 2, [row['numpy_s'] for row in results], width, label=\"NumPy\")\n", + "ax.bar(x + width / 2, [row['cpp_s'] for row in results], width, label=\"C++\")\n", + "ax.set_yscale(\"log\")\n", + "ax.set_xticks(x, labels)\n", + "ax.set_xlabel(\"z profile points\")\n", + "ax.set_ylabel(\"Median runtime [s]\")\n", + "ax.grid(axis=\"y\", which=\"both\", alpha=0.25)\n", + "ax.legend()\n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e74da763-db71-4a36-8e1b-cd06fcc6473c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/doc/source/index.rst b/doc/source/index.rst index c297512..76082ac 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -22,6 +22,7 @@ large 2D detectors. acceleration_backends entry_points benchmarks/ctr_accel_backends + benchmarks/ctr_zdensity_accel benchmarks/roi_sum_accel api release_notes diff --git a/orgui/datautils/xrayutils/CTRuc.py b/orgui/datautils/xrayutils/CTRuc.py index 052556a..d156872 100644 --- a/orgui/datautils/xrayutils/CTRuc.py +++ b/orgui/datautils/xrayutils/CTRuc.py @@ -3738,6 +3738,27 @@ def zDensity_G(self, z, h, k): "This will result in numerical errors in electron density calculation!" ) + # Special electron-density models are Python callbacks followed by a + # SciPy convolution, so they deliberately retain the reference path. + # The native kernel implements the standard atomic density expression. + if CTR_ACCEL_BACKEND == "cpp" and not any( + name in UnitCell.special_eDensity for name in names + ): + return _CTRcalc_cpp.unitcell_zdensity_g( + np.ascontiguousarray(z, dtype=np.float64), + h, + k, + Qpara2, + self._a[2], + basis, + formf, + self.R_mat, + self.R_mat_inv, + np.asarray(self.coherentDomainMatrix), + np.asarray(self.coherentDomainOccupancy), + self.uc_area, + ) + rho = np.zeros_like(z, dtype=np.complex128) rho_i = np.empty_like(z, dtype=np.complex128) diff --git a/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp b/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp index 2929ce0..f68a4ad 100644 --- a/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp +++ b/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -356,11 +357,15 @@ inline double get3( return data[(i * info.shape[1] + j) * info.shape[2] + k]; } -std::unique_ptr effective_domain_matrices(const Inputs &inputs) { - const auto n_domains = inputs.coherent_domain_matrix.shape[0]; - const double *domain = ptr(inputs.coherent_domain_matrix); - const double *r_mat = ptr(inputs.r_mat); - const double *r_mat_inv = ptr(inputs.r_mat_inv); +std::unique_ptr effective_domain_matrices( + const py::buffer_info &coherent_domain_matrix, + const py::buffer_info &r_mat_info, + const py::buffer_info &r_mat_inv_info +) { + const auto n_domains = coherent_domain_matrix.shape[0]; + const double *domain = ptr(coherent_domain_matrix); + const double *r_mat = ptr(r_mat_info); + const double *r_mat_inv = ptr(r_mat_inv_info); auto matrices = std::make_unique(n_domains); for (py::ssize_t d = 0; d < n_domains; ++d) { @@ -373,7 +378,7 @@ std::unique_ptr effective_domain_matrices(const Inputs &inputs) { r_mat_inv[row * 3 + left] * get3( domain, - inputs.coherent_domain_matrix, + coherent_domain_matrix, d, left, right @@ -389,6 +394,14 @@ std::unique_ptr effective_domain_matrices(const Inputs &inputs) { return matrices; } +std::unique_ptr effective_domain_matrices(const Inputs &inputs) { + return effective_domain_matrices( + inputs.coherent_domain_matrix, + inputs.r_mat, + inputs.r_mat_inv + ); +} + py::array_t unitcell_core( const Array1D &h, const Array1D &k, @@ -821,6 +834,168 @@ py::array_t unitcell_F_uc( return result; } +py::array_t unitcell_zdensity_g( + const Array1D &z, + const double h, + const double k, + const double q_para2, + const double c, + const Array2D &basis, + const Array2D &f_factors, + const Array2D &r_mat, + const Array2D &r_mat_inv, + const Array3D &coherent_domain_matrix, + const Array1D &coherent_domain_occupancy, + const double uc_area +) { + const auto z_info = z.request(); + const auto basis_info = basis.request(); + const auto ff_info = f_factors.request(); + const auto r_info = r_mat.request(); + const auto r_inv_info = r_mat_inv.request(); + const auto domain_info = coherent_domain_matrix.request(); + const auto occupancy_info = coherent_domain_occupancy.request(); + + require_shape(z_info, 1, "z"); + require_shape(basis_info, 2, "basis"); + require_shape(ff_info, 2, "f_factors"); + require_shape(r_info, 2, "R_mat"); + require_shape(r_inv_info, 2, "R_mat_inv"); + require_shape(domain_info, 3, "coherentDomainMatrix"); + require_shape(occupancy_info, 1, "coherentDomainOccupancy"); + if (basis_info.shape[1] < 7) { + throw py::value_error("basis must have at least 7 columns"); + } + if (ff_info.shape[0] != basis_info.shape[0] || ff_info.shape[1] < 13) { + throw py::value_error( + "f_factors must have one row per basis atom and at least 13 columns" + ); + } + if (r_info.shape[0] != 3 || r_info.shape[1] != 3 + || r_inv_info.shape[0] != 3 || r_inv_info.shape[1] != 3) { + throw py::value_error("transform matrices must be shape (3, 3)"); + } + if (domain_info.shape[1] != 3 || domain_info.shape[2] != 4) { + throw py::value_error("coherentDomainMatrix must have shape (N, 3, 4)"); + } + if (occupancy_info.shape[0] != domain_info.shape[0]) { + throw py::value_error( + "coherentDomainOccupancy length must match coherentDomainMatrix" + ); + } + + const auto domain_matrices = effective_domain_matrices( + domain_info, + r_info, + r_inv_info + ); + const auto n_z = z_info.shape[0]; + const auto n_atoms = basis_info.shape[0]; + const auto n_domains = domain_info.shape[0]; + const auto *z_data = static_cast(z_info.ptr); + const auto *basis_data = static_cast(basis_info.ptr); + const auto *ff_data = static_cast(ff_info.ptr); + const auto *domain_data = static_cast(domain_info.ptr); + const auto *occupancy_data = static_cast(occupancy_info.ptr); + auto result = py::array_t(n_z); + auto *out = static_cast(result.request().ptr); + + constexpr double pi = 3.141592653589793238462643383279502884; + constexpr double two_pi = 2.0 * pi; + struct DensityAtomDomain { + double delta_z2; + double delta_para_q; + double center_z; + double base_real; + double base_imag; + std::array term_coefficients; + std::array exp_dpara_q; + std::array exp_dz; + double phase_real; + double phase_imag; + }; + + // zDensity_G evaluates the same atom/domain geometry for every profile + // sample. Keep the z-independent work outside the profile loop; the + // original Python implementation obtains the same benefit by operating + // on a whole z vector for each atom/domain pair. + std::vector atom_domains; + atom_domains.reserve(n_atoms * n_domains); + for (py::ssize_t i = 0; i < n_atoms; ++i) { + const double delta_z2 = get2(basis_data, basis_info, i, 5) + / (8.0 * pi * pi); + const double delta_para2 = get2(basis_data, basis_info, i, 4) + / (8.0 * pi * pi); + const double x = get2(basis_data, basis_info, i, 1); + const double y = get2(basis_data, basis_info, i, 2); + const double z_frac = get2(basis_data, basis_info, i, 3); + for (py::ssize_t d = 0; d < n_domains; ++d) { + const Matrix3 &mat = domain_matrices[d]; + const double x_rel = mat.value[0][0] * x + mat.value[0][1] * y + + mat.value[0][2] * z_frac + + get3(domain_data, domain_info, d, 0, 3); + const double y_rel = mat.value[1][0] * x + mat.value[1][1] * y + + mat.value[1][2] * z_frac + + get3(domain_data, domain_info, d, 1, 3); + const double z_rel = mat.value[2][0] * x + mat.value[2][1] * y + + mat.value[2][2] * z_frac + + get3(domain_data, domain_info, d, 2, 3); + DensityAtomDomain entry{ + delta_z2, + delta_para2 * q_para2, + z_rel * c, + (get2(ff_data, ff_info, i, 10) + get2(ff_data, ff_info, i, 11)) + / std::sqrt(two_pi * delta_z2), + get2(ff_data, ff_info, i, 12) / std::sqrt(two_pi * delta_z2), + {}, {}, {}, + 0.0, 0.0, + }; + for (int term = 0; term < 5; ++term) { + const double exponent = get2(ff_data, ff_info, i, term + 5); + const double exp_dpara = exponent + 0.5 * delta_para2; + entry.exp_dz[term] = exponent + 0.5 * delta_z2; + entry.term_coefficients[term] = get2(ff_data, ff_info, i, term) + / std::sqrt(4.0 * pi * entry.exp_dz[term]); + entry.exp_dpara_q[term] = exp_dpara * q_para2; + } + const double phase = -two_pi * (h * x_rel + k * y_rel); + const double weight = get2(basis_data, basis_info, i, 6) + * occupancy_data[d]; + entry.phase_real = weight * std::cos(phase); + entry.phase_imag = weight * std::sin(phase); + atom_domains.push_back(entry); + } + } + + py::gil_scoped_release release; + std::fill(out, out + n_z, complex128{0.0, 0.0}); + for (const auto &entry : atom_domains) { + for (py::ssize_t p = 0; p < n_z; ++p) { + const double dz = z_data[p] - entry.center_z; + const double dz_squared = dz * dz; + const double core = std::exp(-0.5 * ( + entry.delta_para_q + dz_squared / entry.delta_z2 + )); + double atom_real = entry.base_real * core; + const double atom_imag = entry.base_imag * core; + for (int term = 0; term < 5; ++term) { + atom_real += entry.term_coefficients[term] * std::exp( + -entry.exp_dpara_q[term] + - dz_squared / (4.0 * entry.exp_dz[term]) + ); + } + out[p] += complex128{ + entry.phase_real * atom_real - entry.phase_imag * atom_imag, + entry.phase_real * atom_imag + entry.phase_imag * atom_real, + }; + } + } + for (py::ssize_t p = 0; p < n_z; ++p) { + out[p] /= uc_area; + } + return result; +} + void set_form_factor_cache_budget(const std::size_t bytes) { form_factor_cache().set_budget(bytes); } @@ -838,6 +1013,7 @@ PYBIND11_MODULE(_CTRcalc_cpp, module) { module.def("unitcell_F_uc_bulk_direct", &unitcell_F_uc_bulk_direct); module.def("unitcell_F_bulk", &unitcell_F_bulk); module.def("unitcell_F_uc", &unitcell_F_uc); + module.def("unitcell_zdensity_g", &unitcell_zdensity_g); module.def("form_factor_cache_stats", &form_factor_cache_stats); module.def("clear_form_factor_cache", []() { form_factor_cache().clear(); }); module.def("reset_form_factor_cache_stats", &reset_form_factor_cache_stats); diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index f381978..69f1f7b 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -2458,6 +2458,24 @@ def setUp(self): def testStructureFactorEqual(self): self.assert_structure_factors_match_volume_normalized_reference() + def testZDensityMatchesNumpyWithMultipleDomains(self): + """The C++ density kernel preserves the atomic density convention.""" + cell = CTRcalc.UnitCell([3.0, 4.0, 5.0], [90.0, 90.0, 90.0]) + cell.addAtom("C", [0.2, 0.3, 0.4], 0.12, 0.18, 0.8) + cell.addAtom("O", [0.7, 0.6, 0.1], 0.21, 0.24, 0.5) + cell.setEnergy(10000.0) + shifted_domain = np.vstack((np.identity(3).T, [0.1, -0.2, 0.15])).T + cell.coherentDomainMatrix = [cell.coherentDomainMatrix[0], shifted_domain] + cell.coherentDomainOccupancy = [0.65, 0.35] + z = np.linspace(-2.0, 7.0, 127) + + CTRuc.set_accel_backend("numpy") + expected = cell.zDensity_G(z, 0.37, -0.22) + CTRuc.set_accel_backend("cpp") + actual = cell.zDensity_G(z, 0.37, -0.22) + + np.testing.assert_allclose(actual, expected, rtol=1e-13, atol=1e-13) + def testFormFactorCacheReusesMatchingGrid(self): """The C++ cache shares Waasmaier vectors across UnitCell calls.""" original_budget = CTRuc.form_factor_cache_stats()["budget_bytes"] From 8e5dcd1e354b3663171c5c2064e0dda5d3fe2c6b Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Sat, 25 Jul 2026 14:47:48 -0400 Subject: [PATCH 15/37] feat(CTR): coherence of film epitaxy along z --- doc/source/ctr_structure_factors.rst | 23 ++ orgui/datautils/xrayutils/CTRfilm.py | 175 ++++++++++--- .../datautils/xrayutils/test/test_CTRcalc.py | 238 +++++++++++++++++- 3 files changed, 391 insertions(+), 45 deletions(-) diff --git a/doc/source/ctr_structure_factors.rst b/doc/source/ctr_structure_factors.rst index 70c6187..6e151e9 100644 --- a/doc/source/ctr_structure_factors.rst +++ b/doc/source/ctr_structure_factors.rst @@ -175,6 +175,29 @@ upper and lower amplitudes as The result is therefore in electrons per lower interface cell. +The lower material is a correction to the semi-infinite bulk, whose atoms +remain at their unstrained positions below the nominal interface boundary. +For every represented lower-material layer, the interface therefore adds its +distributed occupancy at the strain-field position and subtracts the +corresponding sharp-bulk occupancy at the original bulk-lattice position. +This remains true when the statistical support extends below zero. + +The ``C`` parameter controls the out-of-plane coherence. If +:math:`z_0` is an atom's position on its material's independent lattice and +:math:`z_1` is its position in the fully coherent strain field, the generated +position is + +.. math:: + + z(C) = z_0 + C (z_1 - z_0), \qquad 0 \leq C \leq 1. + +Thus ``C = 0`` leaves both materials unstrained, while ``C = 1`` gives the +fully coherent interface. The ``offset`` parameter translates only the upper +material and all subsequently stacked components. It is expressed in +fractional lower-bulk :math:`c` coordinates, so its physical displacement is +``offset * uc_bottom.a[2]`` Angstrom. Positive offsets point toward increasing +``z``. The statistical boundary and the semi-infinite bulk do not move. + Crystal composition ------------------- diff --git a/orgui/datautils/xrayutils/CTRfilm.py b/orgui/datautils/xrayutils/CTRfilm.py index 0ea94b0..63d3df3 100644 --- a/orgui/datautils/xrayutils/CTRfilm.py +++ b/orgui/datautils/xrayutils/CTRfilm.py @@ -408,13 +408,35 @@ def _parse_float_metadata(string, name, default): class EpitaxyInterface(_LayerStackingMixin, LinearFitFunctions): - parameterOrder = "Width/cells Skew/cells" + """A distributed, coherently strained interface between two lattices. + + The statistical profile controls the upper- and lower-material + occupancies. ``C`` controls the out-of-plane coherence: zero leaves both + materials on their independent, unstrained lattices, while one linearly + moves every generated position to the fully coherent strain field. + ``offset`` translates only the upper material and subsequently stacked + components by ``offset * uc_bottom.a[2]`` Angstrom. + + The semi-infinite lower bulk remains on its unstrained lattice. Generated + lower-material domains therefore add the distributed material at its + coherence-dependent positions and separately subtract the sharp bulk at + its original positions. + + :param UnitCell uc_top: + Upper (film-side) interface unit cell. + :param UnitCell uc_bottom: + Lower (bulk-side) interface unit cell. + :param str type: + Statistical interface model. Currently only ``"skellam"``. + """ + + parameterOrder = "Width/cells Skew/cells Coherence Offset/bulk_frac" - parameterLookup = {"W": 0, "S": 1} + parameterLookup = {"W": 0, "S": 1, "C": 2, "offset": 3} avail_types = ["skellam"] - parameterLookup_inv = dict(map(reversed, parameterLookup.items())) + parameterLookup_inv = {0: "W", 1: "S", 2: "C", 3: "offset"} def __init__(self, uc_top, uc_bottom, type="skellam", **kwargs): """ @@ -440,11 +462,11 @@ def __init__(self, uc_top, uc_bottom, type="skellam", **kwargs): self.fixed_ucs = kwargs.get("fixed_ucs", False) self.set_ucs(uc_top, uc_bottom, **kwargs) if profile is None: - self.basis = np.array([0.0, 0.0]) + self.basis = np.array([0.0, 0.0, 1.0, 0.0]) else: - self.basis = np.array([profile.width, profile.asymmetry]) - self._basis_created = np.array([np.nan, np.nan]) - self.basis_0 = np.array([0.0, 0.0]) + self.basis = np.array([profile.width, profile.asymmetry, 1.0, 0.0]) + self._basis_created = np.full(4, np.nan) + self.basis_0 = np.array([0.0, 0.0, 1.0, 0.0]) self.errors = None if "name" in kwargs: self.name = kwargs["name"] @@ -600,13 +622,17 @@ def stacking_height_absolute(self): @property def stacking_loc_absolute(self): - """Return the nominal interface boundary location in Angstrom. + """Return the translated film-side boundary in Angstrom. - A Film uses this location as the origin of its requested total width, - while :attr:`stacking_height_absolute` supplies the physical support - it must not duplicate. + The statistical boundary and lower bulk remain at + :attr:`loc_absolute`. A Film uses this translated location as the + origin of its requested total width, while + :attr:`stacking_height_absolute` supplies the physical support it must + not duplicate. """ - return self.loc_absolute + if np.any(self._basis_created != self.basis): + self.createInterfaceCells() + return self.loc_absolute + self.basis[3] * self.uc_bottom.a[2] @property def layer_state(self): @@ -652,12 +678,24 @@ def createInterfaceCells(self): sharp_top = ( (unitcells >= loc).astype(np.float64).reshape((-1, n_layers)) ) + sharp_bottom = 1.0 - sharp_top + coherence = self.basis[2] + if not np.isfinite(coherence) or not 0.0 <= coherence <= 1.0: + raise ValueError("EpitaxyInterface coherence must be between 0 and 1") + offset = self.basis[3] + if not np.isfinite(offset): + raise ValueError("EpitaxyInterface offset must be finite") + # The upper material owns the complete generated support because - # Film starts only above ``stacking_height_absolute``. The lower - # material remains a signed correction to the semi-infinite bulk - # that already occupies the lower side of the nominal boundary. + # Film starts only above ``stacking_height_absolute``. The lower + # material is represented by an addition at its potentially + # strained position and a separate subtraction at the unstrained + # position already occupied by the semi-infinite bulk. occupancy_top = probability_top - occupancy_bottom = probability_bottom - (1.0 - sharp_top) + occupancy_bottom = np.concatenate( + (probability_bottom, -sharp_bottom), + axis=0, + ) a3_top = self.top_layers[0].a[2] a3_bottom = self.bottom_layers[0].a[2] @@ -674,9 +712,15 @@ def createInterfaceCells(self): ratio_top = a3_bottom / a3_top ratio_bottom = 1 / ratio_top - h = 0.0 - - for p_t, p_b in zip(probability_top, probability_bottom): + coherent_top = [[] for _ in self.top_layers] + coherent_bottom = [[] for _ in self.bottom_layers] + ideal_top = [[] for _ in self.top_layers] + ideal_bottom = [[] for _ in self.bottom_layers] + coherent_height = 0.0 + + for cycle_index, (p_t, p_b) in enumerate( + zip(probability_top, probability_bottom) + ): top_strains = p_t + ratio_top * p_b bottom_strains = ratio_bottom * p_t + p_b physical_top_index = np.flatnonzero( @@ -695,9 +739,11 @@ def createInterfaceCells(self): relative_layer_position - (self.uc_top.layerpos[self.layer_order[i]]) ) - mat_top_i[2, 3] = h / a3_top + top_strain_and_h * top_layer_offset - - uc_t.coherentDomainMatrix.append(mat_top_i) + mat_top_i[2, 3] = ( + coherent_height / a3_top + + top_strain_and_h * top_layer_offset + ) + coherent_top[i].append(mat_top_i) mat_bottom_i = np.copy(mat_0) bottom_strain_and_h = bottom_strains[i] @@ -707,28 +753,77 @@ def createInterfaceCells(self): - (self.uc_bottom.layerpos[self.layer_order[i]]) ) mat_bottom_i[2, 3] = ( - h / a3_bottom + bottom_strain_and_h * bottom_layer_offset + coherent_height / a3_bottom + + bottom_strain_and_h * bottom_layer_offset ) - # h_bottom += bottom_strain_and_h - uc_b.coherentDomainMatrix.append(mat_bottom_i) - h += cell_height - # h_bottom += bottom_strain_and_h - # h_top += top_strain_and_h + coherent_bottom[i].append(mat_bottom_i) + + ideal_top_i = np.copy(mat_0) + ideal_top_i[2, 3] = cycle_index + top_layer_offset + ideal_top[i].append(ideal_top_i) + + ideal_bottom_i = np.copy(mat_0) + ideal_bottom_i[2, 3] = cycle_index + bottom_layer_offset + ideal_bottom[i].append(ideal_bottom_i) + coherent_height += cell_height loc_rescaled = loc - unitcells[0] uc_no_loc = int(np.floor(loc_rescaled)) // n_layers layer_no_loc = int(np.floor(loc_rescaled)) % n_layers loc_remainder = (loc_rescaled % n_layers) % 1 - loc_mat = self.top_layers[layer_no_loc].coherentDomainMatrix[uc_no_loc] - self._loc_absolute_ref = ( - loc_mat[2, 3] * a3_top + loc_remainder * loc_mat[2, 2] * a3_top - ) - translation = self.below_H - self._loc_absolute_ref + coherent_loc_mat = coherent_top[layer_no_loc][uc_no_loc] + coherent_loc = ( + coherent_loc_mat[2, 3] + + loc_remainder * coherent_loc_mat[2, 2] + ) * a3_top + ideal_top_loc_mat = ideal_top[layer_no_loc][uc_no_loc] + ideal_top_loc = ( + ideal_top_loc_mat[2, 3] + + loc_remainder * ideal_top_loc_mat[2, 2] + ) * a3_top + ideal_bottom_loc_mat = ideal_bottom[layer_no_loc][uc_no_loc] + ideal_bottom_loc = ( + ideal_bottom_loc_mat[2, 3] + + loc_remainder * ideal_bottom_loc_mat[2, 2] + ) * a3_bottom + + top_offset = offset * a3_bottom + for i, (uc_t, uc_b) in enumerate(zip(self.top_layers, self.bottom_layers)): + for coherent_mat, ideal_mat in zip(coherent_top[i], ideal_top[i]): + coherent_mat = np.copy(coherent_mat) + ideal_mat = np.copy(ideal_mat) + coherent_mat[2, 3] -= coherent_loc / a3_top + ideal_mat[2, 3] -= ideal_top_loc / a3_top + interpolated = ideal_mat + coherence * ( + coherent_mat - ideal_mat + ) + interpolated[2, 3] += top_offset / a3_top + uc_t.coherentDomainMatrix.append(interpolated) + + for coherent_mat, ideal_mat in zip( + coherent_bottom[i], ideal_bottom[i] + ): + coherent_mat = np.copy(coherent_mat) + ideal_mat = np.copy(ideal_mat) + coherent_mat[2, 3] -= coherent_loc / a3_bottom + ideal_mat[2, 3] -= ideal_bottom_loc / a3_bottom + interpolated = ideal_mat + coherence * ( + coherent_mat - ideal_mat + ) + uc_b.coherentDomainMatrix.append(interpolated) + + # These domains remove the fixed semi-infinite bulk at the + # same unstrained coordinates used by UnitCell.F_bulk. + for ideal_mat in ideal_bottom[i]: + fixed_bulk_mat = np.copy(ideal_mat) + fixed_bulk_mat[2, 3] -= ideal_bottom_loc / a3_bottom + uc_b.coherentDomainMatrix.append(fixed_bulk_mat) + + self._loc_absolute_ref = 0.0 self._loc_absolute = self.below_H - _translate_domains(self.top_layers, translation) - _translate_domains(self.bottom_layers, translation) + _translate_domains(self.top_layers, self.below_H) + _translate_domains(self.bottom_layers, self.below_H) self._end_layer_number = self.layer_order[-1] - # self._loc_absolute = self._loc_absolute_ref + self.below_H self._basis_created = np.copy(self.basis) else: @@ -990,6 +1085,14 @@ def fromStr(cls, string): else: basis = np.array(sline, dtype=np.float64) errors = None + if basis.size == 2: + basis = np.concatenate((basis, [1.0, 0.0])) + if errors is not None: + errors = np.concatenate((errors, [np.nan, np.nan])) + elif basis.size != 4: + raise ValueError( + "EpitaxyInterface requires Width, Skew, Coherence, and Offset" + ) # very explicit searching for the lines containing TopUnitCell and BottomUnitCell: # noqa: E501 sp_str = string.splitlines() diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index 69f1f7b..e02d014 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -1678,7 +1678,7 @@ def test_epitaxy_interface_rotates_cyclic_layer_order(self): self.make_layered_unitcell("bottom"), fixed_ucs=3, ) - interface.basis[:] = [0.5, 0.0] + interface.basis[:2] = [0.5, 0.0] interface.stack_on(0.0, 12.0, 1) self.assertEqual(interface.start_layer_number, 2) @@ -1919,10 +1919,216 @@ def test_profile_interface_owns_upper_support_material(self): self.assertTrue(np.any(top_occupancy > 0.9)) self.assertTrue(np.any(bottom_occupancy < 0.0)) self.assertTrue(np.any(bottom_occupancy > 0.0)) - total_occupancy = top_occupancy + bottom_occupancy + split = bottom_occupancy.size // 2 + total_occupancy = ( + top_occupancy + bottom_occupancy[:split] + bottom_occupancy[split:] + ) self.assertTrue(np.any(np.isclose(total_occupancy, 0.0))) self.assertTrue(np.any(np.isclose(total_occupancy, 1.0))) + @staticmethod + def make_epitaxy_cells(): + top = TestLayerStacking.make_layered_unitcell("top") + top.a = np.array([3.0, 3.0, 4.0]) + bottom = TestLayerStacking.make_layered_unitcell("bottom") + return top, bottom + + def make_epitaxy_interface(self, coherence=1.0, offset=0.0): + top, bottom = self.make_epitaxy_cells() + interface = CTRfilm.EpitaxyInterface( + top, + bottom, + profile=SkellamProfile(0.5), + fixed_ucs=6, + ) + interface.basis[:] = [0.5, 0.0, coherence, offset] + interface.createInterfaceCells() + return interface + + def test_epitaxy_bulk_replacement_subtracts_fixed_lattice_positions(self): + interface = self.make_epitaxy_interface(coherence=1.0) + + for layer in interface.bottom_layers: + split = len(layer.coherentDomainMatrix) // 2 + additions = layer.coherentDomainMatrix[:split] + fixed_bulk = layer.coherentDomainMatrix[split:] + np.testing.assert_allclose( + [matrix[2, 2] for matrix in fixed_bulk], + 1.0, + ) + self.assertTrue( + any( + not np.allclose(addition[2], fixed[2]) + for addition, fixed in zip(additions, fixed_bulk) + ) + ) + self.assertTrue( + np.any(np.asarray(layer.coherentDomainOccupancy[split:]) < 0.0) + ) + + def test_epitaxy_coherence_interpolates_generated_positions(self): + incoherent = self.make_epitaxy_interface(coherence=0.0) + halfway = self.make_epitaxy_interface(coherence=0.5) + coherent = self.make_epitaxy_interface(coherence=1.0) + + for layers_zero, layers_half, layers_one in ( + ( + incoherent.top_layers, + halfway.top_layers, + coherent.top_layers, + ), + ( + incoherent.bottom_layers, + halfway.bottom_layers, + coherent.bottom_layers, + ), + ): + for layer_zero, layer_half, layer_one in zip( + layers_zero, + layers_half, + layers_one, + ): + domain_count = len(layer_zero.coherentDomainMatrix) + if layers_zero is incoherent.bottom_layers: + domain_count //= 2 + for matrix_zero, matrix_half, matrix_one in zip( + layer_zero.coherentDomainMatrix[:domain_count], + layer_half.coherentDomainMatrix[:domain_count], + layer_one.coherentDomainMatrix[:domain_count], + ): + self.assertAlmostEqual(matrix_zero[2, 2], 1.0) + np.testing.assert_allclose( + matrix_half[2], + 0.5 * (matrix_zero[2] + matrix_one[2]), + ) + + def test_epitaxy_offset_moves_only_top_and_upward_stack(self): + baseline = self.make_epitaxy_interface(offset=0.0) + shifted = self.make_epitaxy_interface(offset=0.25) + lowered = self.make_epitaxy_interface(offset=-0.25) + expected_shift = 0.25 * shifted.uc_bottom.a[2] + + for candidate, physical_shift in ( + (shifted, expected_shift), + (lowered, -expected_shift), + ): + for base_layer, candidate_layer in zip( + baseline.top_layers, + candidate.top_layers, + ): + for base_matrix, candidate_matrix in zip( + base_layer.coherentDomainMatrix, + candidate_layer.coherentDomainMatrix, + ): + self.assertAlmostEqual( + (candidate_matrix[2, 3] - base_matrix[2, 3]) + * candidate_layer.a[2], + physical_shift, + ) + for base_layer, candidate_layer in zip( + baseline.bottom_layers, + candidate.bottom_layers, + ): + np.testing.assert_allclose( + candidate_layer.coherentDomainMatrix, + base_layer.coherentDomainMatrix, + ) + self.assertAlmostEqual( + candidate.stacking_loc_absolute + - baseline.stacking_loc_absolute, + physical_shift, + ) + self.assertAlmostEqual( + candidate.height_absolute - baseline.height_absolute, + physical_shift, + ) + + base_film = CTRfilm.Film(self.make_layered_unitcell("base_film")) + shifted_film = CTRfilm.Film(self.make_layered_unitcell("shifted_film")) + base_film.basis[0] = 30 + shifted_film.basis[0] = 30 + CTRcalc.SXRDCrystal( + baseline.uc_bottom, + baseline, + base_film, + stacking=np.array([1, 2]), + ).apply_stacking() + CTRcalc.SXRDCrystal( + shifted.uc_bottom, + shifted, + shifted_film, + stacking=np.array([1, 2]), + ).apply_stacking() + self.assertAlmostEqual( + shifted_film.pos_absolute - base_film.pos_absolute, + expected_shift, + ) + self.assertAlmostEqual( + shifted_film.height_absolute - base_film.height_absolute, + expected_shift, + ) + + def test_epitaxy_parameters_round_trip_and_legacy_defaults(self): + interface = self.make_epitaxy_interface(coherence=0.4, offset=-0.2) + restored = CTRfilm.EpitaxyInterface.fromStr(interface.toStr(showErrors=False)) + np.testing.assert_allclose(restored.basis, [0.5, 0.0, 0.4, -0.2]) + + legacy = interface.toStr(showErrors=False) + legacy = legacy.replace( + CTRfilm.EpitaxyInterface.parameterOrder, + "Width/cells Skew/cells", + ) + parameter_line = next( + line + for line in legacy.splitlines() + if line.strip().startswith("0.50000") + ) + legacy = legacy.replace(parameter_line, "0.50000 0.00000", 1) + restored_legacy = CTRfilm.EpitaxyInterface.fromStr(legacy) + np.testing.assert_allclose(restored_legacy.basis, [0.5, 0.0, 1.0, 0.0]) + + def test_epitaxy_coherence_and_offset_are_fit_parameters(self): + interface = self.make_epitaxy_interface() + interface.basis_0[:] = interface.basis + interface.addFitParameter("C", limits=(0.0, 1.0)) + interface.addFitParameter("offset", limits=(-0.5, 0.5)) + + np.testing.assert_allclose(interface.getInitialParameters(), [1.0, 0.0]) + interface.setFitParameters([0.3, -0.2]) + np.testing.assert_allclose(interface.basis[2:], [0.3, -0.2]) + + def test_epitaxy_rejects_invalid_coherence_and_offset(self): + interface = self.make_epitaxy_interface() + for coherence in (-0.1, 1.1, np.nan): + interface.basis[2] = coherence + with self.assertRaisesRegex(ValueError, "coherence"): + interface.createInterfaceCells() + interface.basis[2] = 1.0 + interface.basis[3] = np.inf + with self.assertRaisesRegex(ValueError, "offset"): + interface.createInterfaceCells() + + def test_epitaxy_split_bulk_domains_feed_all_profile_paths(self): + interface = self.make_epitaxy_interface(coherence=0.6, offset=0.1) + interface.setEnergy(10000.0) + + structure_factor = interface.F_uc( + np.array([0.0, 0.2]), + np.array([0.0, 0.1]), + np.array([0.3, 0.7]), + ) + density = interface.zDensity_G( + np.linspace(-20.0, 20.0, 401), + 0.0, + 0.0, + ) + optical_profile = interface.optical_profile() + + self.assertTrue(np.all(np.isfinite(structure_factor))) + self.assertTrue(np.all(np.isfinite(density))) + self.assertEqual(optical_profile.shape[1], 3) + self.assertTrue(np.all(np.isfinite(optical_profile))) + class TestLegacyLayeredCTR(unittest.TestCase): def test_legacy_xtal_uses_corrected_interface_support(self): @@ -1944,7 +2150,10 @@ def test_legacy_xtal_uses_corrected_interface_support(self): self.assertIsInstance(xtal["RuO2"], CTRfilm.Film) self.assertIsInstance(xtal["TiO2toRuO2"], CTRfilm.EpitaxyInterface) - np.testing.assert_allclose(xtal["TiO2toRuO2"].basis, [0.35, 0.0]) + np.testing.assert_allclose( + xtal["TiO2toRuO2"].basis, + [0.35, 0.0, 1.0, 0.0], + ) interface = xtal["TiO2toRuO2"] xtal.apply_stacking() self.assertAlmostEqual( @@ -2132,7 +2341,7 @@ def make_crystal_with_errors(self): surface.basis_0[:] = surface.basis film.errors = np.array([0.2]) - interface.errors = np.array([0.03, 0.04]) + interface.errors = np.array([0.03, 0.04, np.nan, np.nan]) surface.errors = np.array([0.1, 0.15, 0.2]) film.unitcell.errors = np.full_like(film.unitcell.basis, np.nan) film.unitcell.errors[:, 1:7] = 0.01 @@ -2172,7 +2381,11 @@ def test_xtal_and_xpr_plain_text_round_trip(self): self.assertIsNone(restored_xtal["surface"].errors) np.testing.assert_allclose(restored_xpr.werrors, crystal.werrors) np.testing.assert_allclose(restored_xpr["film"].errors, [0.2]) - np.testing.assert_allclose(restored_xpr["interface"].errors, [0.03, 0.04]) + np.testing.assert_allclose( + restored_xpr["interface"].errors[:2], + [0.03, 0.04], + ) + self.assertTrue(np.all(np.isnan(restored_xpr["interface"].errors[2:]))) np.testing.assert_allclose( restored_xpr["surface"].errors, [0.1, 0.15, 0.2], @@ -2203,8 +2416,9 @@ def test_ctrfilm_fit_parameters_and_errors(self): interface.addRelParameter(("W", "S"), (1.0, -1.0), limits=(-0.2, 0.2)) interface.setFitParameters([0.1]) interface.setFitErrors([0.02]) - np.testing.assert_allclose(interface.basis, [0.45, -0.1]) - np.testing.assert_allclose(interface.errors, [0.02, 0.02]) + np.testing.assert_allclose(interface.basis, [0.45, -0.1, 1.0, 0.0]) + np.testing.assert_allclose(interface.errors[:2], [0.02, 0.02]) + self.assertTrue(np.all(np.isnan(interface.errors[2:]))) self.assertAlmostEqual(interface.profile.width, 0.45) self.assertAlmostEqual(interface.profile.asymmetry, -0.1) @@ -2271,7 +2485,10 @@ def test_crystal_fit_vector_updates_ctrfilm_components(self): crystal.setParameters([0.4, 7.0, 2.0]) crystal.setFitErrors([0.01, 0.2, 0.1]) - np.testing.assert_allclose(crystal["interface"].basis, [0.4, 0.0]) + np.testing.assert_allclose( + crystal["interface"].basis, + [0.4, 0.0, 1.0, 0.0], + ) np.testing.assert_allclose(crystal["film"].basis, [7.0]) np.testing.assert_allclose(crystal["surface"].basis, [2.0, 0.5, 0.0]) np.testing.assert_allclose(crystal.getFitErrors(), [0.01, 0.2, 0.1]) @@ -2306,7 +2523,10 @@ def test_example_xtal_and_xpr_describe_same_poisson_stack(self): ) np.testing.assert_array_equal(xtal.uc_stacking, [3, 2, 1]) np.testing.assert_array_equal(xpr.uc_stacking, [3, 2, 1]) - np.testing.assert_allclose(xtal["TiO2toRuO2"].basis, [0.35, 0.0]) + np.testing.assert_allclose( + xtal["TiO2toRuO2"].basis, + [0.35, 0.0, 1.0, 0.0], + ) np.testing.assert_allclose(xtal["RuO2"].basis, [17.0]) np.testing.assert_allclose(xtal["RuO2surface"].basis, [-6.0, 1.0, 0.0]) for name in ("TiO2toRuO2", "RuO2", "RuO2surface"): From 2dd5e7903ec9b3bdd3d7527ef0bd888fdebb0090 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Mon, 27 Jul 2026 21:52:28 -0400 Subject: [PATCH 16/37] docs: note PoissonSurface basis now requires three parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two-parameter PoissonSurface basis was only ever used in test code, so no migration path is added — this just documents the change in the Unreleased changelog entry. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ebb22..b398219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ Scientific and analysis additions: py3Dmol or Mayavi explicitly, and can incrementally add unit cells to a shared viewer. Covalent radii are interpreted consistently by both backends and can be adjusted with the dimensionless ``radius_scale`` parameter. +- ``PoissonSurface``'s basis now requires three parameters (``W``, ``alpha``, + ``offset``) instead of two. The previous two-parameter form was only ever + used in test code, so no migration path is provided; a saved ``.xtal``/ + ``.xpr`` file with a two-parameter ``PoissonSurface`` basis will fail to + load. ## [1.5.0] (2026-06-07) From 051b43bbfa743f7bd76fcda67b3add1e259d86fd Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Mon, 27 Jul 2026 22:15:43 -0400 Subject: [PATCH 17/37] docs(phys): document F_bulk lattice-sum denominator fix as breaking UnitCell.F_bulk's semi-infinite geometric lattice sum used the raw, untransformed l index instead of the refHKLTransform-converted index in its attenuation phase, in both the numba/C++ and plain-Python paths. This was only correct for the default case of a component using its own bulk cell as the reference (no reference_uc set); any explicit reference_uc whose out-of-plane axis differs from the bulk's -- including a plain scale difference, not just rotation or reindexing -- was affected. Present in all releases up to and including v1.5.0. Document the correct convention and the fix in ctr_structure_factors.rst, note it as a breaking change alongside the also-unreleased PoissonSurface and Wyckoff convention changes in the changelog, and regenerate release_notes.rst. BREAKING CHANGE: UnitCell.F_bulk now uses the refHKLTransform- converted l index in its lattice-sum denominator. Bulk CTR amplitudes computed with an explicit reference_uc whose out-of-plane axis differs from the bulk's will change. --- CHANGELOG.md | 19 +++++++++++++++ doc/source/ctr_structure_factors.rst | 35 ++++++++++++++++++++++++++++ doc/source/release_notes.rst | 7 ++++++ 3 files changed, 61 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b398219..f601605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,25 @@ Scientific and analysis additions: used in test code, so no migration path is provided; a saved ``.xtal``/ ``.xpr`` file with a two-parameter ``PoissonSurface`` basis will fail to load. +- Wyckoff-parameterized fit values are now interpreted as absolute atomic + positions instead of deltas relative to the Wyckoff position. Wyckoff + parameter fitting was development-only and never part of a release, so no + migration is provided. + +A ***critical bug*** was fixed that affects bulk CTR calculations: + +- ``UnitCell.F_bulk``'s semi-infinite geometric lattice sum used the raw, + untransformed ``l`` index instead of the index converted by + ``refHKLTransform`` when computing the out-of-plane attenuation phase. This + was only correct for the default case where a component uses its own bulk + cell as the reference (no ``reference_uc`` set); any explicit + ``reference_uc`` whose out-of-plane reciprocal axis differs from the + bulk's — including a plain scale difference between the reference and bulk + out-of-plane axis length, not only a rotated or reindexed reference — gave + incorrect bulk structure-factor amplitudes. This bug was present in both + the accelerated (numba/C++) and plain-Python code paths in all previous + released versions, up to and including v1.5.0. See the CTR structure-factor + documentation for details. ## [1.5.0] (2026-06-07) diff --git a/doc/source/ctr_structure_factors.rst b/doc/source/ctr_structure_factors.rst index 6e151e9..f63a7b2 100644 --- a/doc/source/ctr_structure_factors.rst +++ b/doc/source/ctr_structure_factors.rst @@ -30,6 +30,41 @@ area or volume normalization is applied. and return electrons for one lateral unit cell of their source ``UnitCell``. +Semi-infinite bulk amplitude +----------------------------- + +``UnitCell.F_bulk`` sums ``UnitCell.F_uc_bulk_direct`` over an infinite stack +of bulk repeats along the out-of-plane direction using a closed-form +geometric series with attenuation: + +.. math:: + + F_{\mathrm{bulk}}(\mathbf{Q}) + = \frac{F_{\mathrm{uc,bulk}}(\mathbf{Q})} + {1 - \exp\left(-2\pi i\, l_{\mathrm{bulk}} - \mathrm{atten}\right)}, + +where :math:`l_{\mathrm{bulk}}` is the out-of-plane reciprocal index *after* +conversion from the reference unit cell via ``refHKLTransform``, i.e. the +third component of ``refHKLTransform @ (h, k, l)``. This is the same index +used to phase every atom in ``F_uc_bulk_direct``, so the phase advance per +bulk repeat in the denominator is consistent with the periodicity actually +being summed. + +.. warning:: + + Versions up to and including v1.5.0 used the raw, untransformed ``l`` in + this denominator instead of the reference-transformed index. This was only + correct when ``refHKLTransform``'s third row equals ``(0, 0, 1)``, i.e. + only when the bulk cell's own out-of-plane reciprocal axis exactly + coincides with the reference cell's, in both direction and length. Since + ``refHKLTransform = B_mat_inv @ rotMatrix @ uc.B_mat`` (see + ``UnitCell.setReferenceUnitCell``), this held only for the default case of + a component using its own bulk cell as the reference (no ``reference_uc`` + set). Any explicit ``reference_uc`` whose out-of-plane reciprocal axis + differs from the bulk's was affected — including a plain scale difference + between the reference and bulk out-of-plane axis length, not only a + rotated or reindexed reference. + Surface structure on a rough Film --------------------------------- diff --git a/doc/source/release_notes.rst b/doc/source/release_notes.rst index 498dd3d..c7eb1c2 100644 --- a/doc/source/release_notes.rst +++ b/doc/source/release_notes.rst @@ -10,6 +10,13 @@ Unreleased (2026-07-19) Scientific and analysis additions: - Added optional CTR intensity-resolution modeling with constant or gamma-dependent box and Gaussian functions. Calculations can convolve irregular existing L points or sample the crystal structure factor with deterministic quadrature. +- Added py3Dmol atom-sphere rendering for Jupyter notebooks. ``plot3d`` now selects py3Dmol automatically in a notebook, can be directed to either py3Dmol or Mayavi explicitly, and can incrementally add unit cells to a shared viewer. Covalent radii are interpreted consistently by both backends and can be adjusted with the dimensionless ``radius_scale`` parameter. +- ``PoissonSurface``'s basis now requires three parameters (``W``, ``alpha``, ``offset``) instead of two. The previous two-parameter form was only ever used in test code, so no migration path is provided; a saved ``.xtal``/ ``.xpr`` file with a two-parameter ``PoissonSurface`` basis will fail to load. +- Wyckoff-parameterized fit values are now interpreted as absolute atomic positions instead of deltas relative to the Wyckoff position. Wyckoff parameter fitting was development-only and never part of a release, so no migration is provided. + +A ***critical bug*** was fixed that affects bulk CTR calculations: + +- ``UnitCell.F_bulk``'s semi-infinite geometric lattice sum used the raw, untransformed ``l`` index instead of the index converted by ``refHKLTransform`` when computing the out-of-plane attenuation phase. This was only correct for the default case where a component uses its own bulk cell as the reference (no ``reference_uc`` set); any explicit ``reference_uc`` whose out-of-plane reciprocal axis differs from the bulk's — including a plain scale difference between the reference and bulk out-of-plane axis length, not only a rotated or reindexed reference — gave incorrect bulk structure-factor amplitudes. This bug was present in both the accelerated (numba/C++) and plain-Python code paths in all previous released versions, up to and including v1.5.0. See the CTR structure-factor documentation for details. 1.5.0 (2026-06-07) ------------------ From 774d6cd9e2c40a7fbc73bcf814238c2b693d6172 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Mon, 27 Jul 2026 22:27:12 -0400 Subject: [PATCH 18/37] test(phys): cover loading legacy value_kind="delta" Wyckoff parameters Wyckoff parameter fitting's delta->absolute convention change (2fa9e43) was covered for newly-created parameters but not for loading a pre-fix, value_kind="delta" parameter dict through parametersFromDict/updateFromParameters. Add a round-trip regression test that simulates the legacy dict shape (no reference_value, stored value is the delta) and asserts it reproduces the same basis as the current absolute-value convention. --- .../xrayutils/test/test_CTRsymmetry.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/orgui/datautils/xrayutils/test/test_CTRsymmetry.py b/orgui/datautils/xrayutils/test/test_CTRsymmetry.py index c41347a..5a3c8bf 100644 --- a/orgui/datautils/xrayutils/test/test_CTRsymmetry.py +++ b/orgui/datautils/xrayutils/test/test_CTRsymmetry.py @@ -21,6 +21,7 @@ # THE SOFTWARE. # # ###########################################################################*/ +import copy import importlib.util import subprocess import sys @@ -408,6 +409,37 @@ def test_wyckoff_coordinate_parameter_preserves_rutile_u_symmetry(self): expected, ) + def test_wyckoff_legacy_delta_parameter_dict_matches_absolute_result(self): + """A pre-fix, ``value_kind="delta"`` saved Wyckoff parameter dict + must reproduce the same basis as the current absolute-value + convention, since old saved fits stored the delta from the site's + reference value rather than the absolute coordinate.""" + unitcell = self.make_rutile_110_unitcell() + unitcell.addWyckoffParameter("O_4f", "u", absolute_limits=(0.2, 0.4)) + unitcell.setFitParameters([0.31569]) + expected_basis = unitcell.basis.copy() + + saved = unitcell.parametersToDict() + (key, param_dict), = saved["relative"].items() + wyckoff_settings = param_dict["settings"]["wyckoff"] + self.assertEqual(wyckoff_settings["value_kind"], "absolute") + reference_value = wyckoff_settings["reference_value"] + + # Simulate a parameter dict saved before value_kind="absolute" was + # introduced: no reference_value, value_kind="delta", and the stored + # value is the delta from the reference rather than the absolute + # coordinate. + legacy_dict = copy.deepcopy(saved) + legacy_settings = legacy_dict["relative"][key]["settings"]["wyckoff"] + legacy_settings["value_kind"] = "delta" + del legacy_settings["reference_value"] + legacy_dict["relative"][key]["value"] = param_dict["value"] - reference_value + + restored = self.make_rutile_110_unitcell() + restored.parametersFromDict(legacy_dict) + + np.testing.assert_allclose(restored.basis, expected_basis) + def test_wyckoff_site_parameter_displaces_fixed_rutile_site(self): unitcell = self.make_rutile_110_unitcell() couplings = [ From 683b75b5fc13967b9a3aa2d28eea9b6fce8a859a Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Mon, 27 Jul 2026 23:07:26 -0400 Subject: [PATCH 19/37] test(phys): split legacy .xtal regression into three tracked cases test_legacy_xtal_uses_corrected_interface_support's fresh reference values (added previously) disagree with the pre-existing CTRs_reference.dat baseline by ~2.6%, and it's unclear yet which is correct. Rather than picking one, keep both paths visible: - Restore the original pre-beee738 test verbatim (minus two assertions on removed legacy internals unrelated to the physics question) as test_legacy_xtal_reconstructs_reference_interface, comparing against the unmodified CTRs_reference.dat. This fails now, intentionally, so the discrepancy stays visible instead of being silently dropped. - Add test_simple_unitcell_bulk_structure_factor_matches_reference, reusing the already-validated testdata/0V12_calculated.{xpr,dat} fixtures (real fit-derived reference intensities, already used by TestCTRcalculationNumPy/Numba/Cpp) for the un-layered unit-cell + bulk case. This passes, which localizes the CTRs_reference.dat discrepancy to the layered Film/EpitaxyInterface/SkellamProfile path rather than the underlying unit-cell or semi-infinite bulk calculation. Determining which of CTRs_reference.dat or the current code is correct is deferred until other PR feedback is addressed. --- .../datautils/xrayutils/test/test_CTRcalc.py | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index e02d014..605a7e4 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -2165,6 +2165,151 @@ def test_legacy_xtal_uses_corrected_interface_support(self): self.assertIsInstance(interface.profile, SkellamProfile) self.assertNotIn("support_cursor", interface.toStr()) + # End-to-end structure-factor regression check for the legacy .xtal + # fixture (Film + EpitaxyInterface + SkellamProfile). PRELIMINARY, + # UNTESTED reference values: computed with this module's own + # CTRcalc.SXRDCrystal.F() after the interfacial-strain fix in + # EpitaxyInterface (commit beee738) and the F_bulk lattice-sum + # denominator fix, but not independently verified against real data + # or an analytic result -- unlike test_simple_unitcell_bulk_structure_ + # factor_matches_reference below, which does compare against real + # fit-derived intensities for the un-layered unit-cell + bulk case. + # These numbers disagree with the pre-existing CTRs_reference.dat + # baseline used by the (deliberately still-failing) + # test_legacy_xtal_reconstructs_reference_interface below; which of + # the two is correct is an open question, deferred until after other + # PR feedback is addressed. If this test starts failing, do not + # assume the fresh code is wrong and revert to whatever makes it + # pass -- treat it the same as the sibling test: investigate the + # discrepancy before changing either the code or these numbers. + xtal["RuO2"].basis[0] = 17.0 + xtal["RuO2"].basis_0[0] = 17.0 + xtal.atten = 0.01 + l_values = np.linspace(0.0, 7.25, 2000)[:8] + calculated = np.abs( + xtal.F( + np.zeros(8, dtype=np.float64), + np.zeros(8, dtype=np.float64), + l_values, + ) + ) + expected = np.array( + [ + 17238.5440045948, + 7001.2889938137, + 3852.1662569049, + 2726.8273234247, + 2175.489988698, + 1855.6217274457, + 1648.0075146735, + 1501.0723678209, + ] + ) + np.testing.assert_allclose(calculated, expected, rtol=2e-5, atol=2e-5) + + def test_legacy_xtal_reconstructs_reference_interface(self): + """Original pre-fix regression check against the frozen + CTRs_reference.dat baseline, restored verbatim (see git history for + commit beee738, "fix: EpitaxyInterface now is again consuming Film + so that interfacial strain field is correct"). + + This is EXPECTED TO CURRENTLY FAIL: CTRs_reference.dat encodes the + pre-beee738 interfacial-strain behavior and was never regenerated + against the corrected physics. It is kept, failing, to keep the + discrepancy visible rather than silently dropping the check. Two + assertions from the original test are omitted because they depend + on removed legacy internals (a 2-element EpitaxyInterface.basis and + a ``_legacy_support_cursor`` attribute) unrelated to the physics + question being tracked here; see + test_legacy_xtal_uses_corrected_interface_support above for the + current basis format and support-cursor behavior. + + Before merging, determine which side is correct -- the frozen + CTRs_reference.dat baseline or the current, corrected code -- rather + than resolving the failure by editing either one without + investigation. This is deferred pending other PR feedback. + """ + repository_root = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "..", "..") + ) + fixture_root = os.path.join( + repository_root, "examples", "CTR", "test" + ) + xtal_path = os.path.join( + fixture_root, "0001_fit_2V036_reference.xtal" + ) + reference_path = os.path.join(fixture_root, "CTRs_reference.dat") + for path in (xtal_path, reference_path): + if not os.path.exists(path): + self.skipTest(f"Missing optional CTR fixture: {path}") + xtal = CTRcalc.SXRDCrystal.fromFile( + xtal_path + ) + + self.assertIsInstance(xtal["RuO2"], CTRfilm.Film) + self.assertIsInstance(xtal["TiO2toRuO2"], CTRfilm.EpitaxyInterface) + + xtal["RuO2"].basis[0] = 17.0 + xtal["RuO2"].basis_0[0] = 17.0 + xtal.atten = 0.01 + reference = np.loadtxt( + reference_path, + skiprows=1, + max_rows=8, + ) + l_values = np.linspace(0.0, 7.25, 2000)[:8] + calculated = np.abs( + xtal.F( + np.zeros(8, dtype=np.float64), + np.zeros(8, dtype=np.float64), + l_values, + ) + ) + np.testing.assert_allclose( + calculated, + reference[:, 3] * xtal.reference_area, + rtol=2e-5, + atol=2e-5, + ) + + def test_simple_unitcell_bulk_structure_factor_matches_reference(self): + """Sibling check for the "simple" (un-layered unit-cell + bulk, + no Film/EpitaxyInterface/profile) case, for contrast with the two + Film + EpitaxyInterface tests above. This does not use + CTRs_reference.dat, which was generated from the full layered + crystal, not a simple bulk-only one (see create_test_data.py in the + datautils repository's examples/CTR directory). Instead it reuses + testdata/0V12_calculated.xpr and .dat, the same fixtures already + validated against real fit-derived reference intensities by + TestCTRcalculationNumPy/Numba/Cpp above -- this is real, + independently-verified data, not a frozen snapshot of current + output. It currently passes, which localizes the + CTRs_reference.dat discrepancy above to the layered Film / + EpitaxyInterface / SkellamProfile path rather than to the + underlying unit-cell or semi-infinite bulk calculation. + """ + fp = os.path.split(__file__)[0] + xtal_unitcells = CTRcalc.SXRDCrystal.fromFile( + os.path.join(fp, "testdata/0V12_calculated.xpr") + ) + CTRs = CTRplotutil.CTRCollection.fromANAROD( + os.path.join(fp, "testdata/0V12_calculated.dat"), + RODexport=True, + ) + pt100 = CTRcalc.UnitCell([3.9242, 3.9242, 3.9242], [90.0000, 90.0000, 90.0000]) + xtal_unitcells.setGlobalReferenceUnitCell( + pt100, util.z_rotation(np.deg2rad(45.0)) + ) + # This legacy reference file was generated when amplitudes were + # normalized by unit-cell volume. Canonical F values in electrons are + # therefore larger by the reference-cell volume. + reference_scale = pt100.volume + + calc_CTRs = CTRs.generateCollectionFromXtal(xtal_unitcells) + for calc, reference in zip(calc_CTRs, CTRs): + expected = reference.sfI * reference_scale + np.testing.assert_allclose(calc.sfI, expected, rtol=1e-02) + class TestStructureFactorNormalization(unittest.TestCase): @staticmethod From 7e5d1f44ceb7804f2757c8654c3905a41f3694e9 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Mon, 27 Jul 2026 23:11:02 -0400 Subject: [PATCH 20/37] perf(phys): bound the dispersion cache with LRU eviction _readDispersion_cached's key includes a continuous energy value, so functools.cache could grow without limit over an energy scan in a long-running batch job, unlike the new byte-budgeted C++ FormFactorCache in this PR. Switch it to functools.lru_cache with a generous maxsize. _readWaasmaier_cached is left on plain @cache since its key space (element/ion names) is inherently small and bounded. --- orgui/datautils/xrayutils/CTRutil.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/orgui/datautils/xrayutils/CTRutil.py b/orgui/datautils/xrayutils/CTRutil.py index 3c4d6f9..0797aa8 100644 --- a/orgui/datautils/xrayutils/CTRutil.py +++ b/orgui/datautils/xrayutils/CTRutil.py @@ -32,7 +32,7 @@ import xraydb import warnings import json -from functools import cache +from functools import cache, lru_cache # random.seed(45) from collections import OrderedDict @@ -680,9 +680,15 @@ def readWaasmaier(element): return np.array(_readWaasmaier_cached(_normalize_species(element))) -@cache +@lru_cache(maxsize=100_000) def _readDispersion_cached(element, energy): - """Read immutable dispersion terms for a normalized species and energy.""" + """Read immutable dispersion terms for a normalized species and energy. + + Unlike :func:`_readWaasmaier_cached`, whose key space is bounded by the + small set of known elements/ions, ``energy`` is a continuous value, so + an unbounded cache could grow without limit over an energy scan in a + long-running batch job. Bounded with an LRU eviction policy instead. + """ return ( xraydb.f1_chantler(atomic_number(element), energy), xraydb.f2_chantler(atomic_number(element), energy), From eb05d70f22b40897ddd8ea0617d866fa87329e82 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Mon, 27 Jul 2026 23:12:39 -0400 Subject: [PATCH 21/37] refactor(phys): dedupe hardcoded hc constant in CTRoptics --- orgui/datautils/xrayutils/CTRoptics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/orgui/datautils/xrayutils/CTRoptics.py b/orgui/datautils/xrayutils/CTRoptics.py index 465dfb4..dadfc85 100644 --- a/orgui/datautils/xrayutils/CTRoptics.py +++ b/orgui/datautils/xrayutils/CTRoptics.py @@ -136,7 +136,7 @@ def optical_profile(unit_cell, include_coherent_domains=True): spacing += 1.0 thickness_fraction[index] = spacing - wavelength = 12.398419843320026 / (unit_cell._E * 1e-3) + wavelength = HC_KEV_ANGSTROM / (unit_cell._E * 1e-3) scale = 2.8179403262e-5 * wavelength**2 / (2.0 * np.pi) if include_coherent_domains: domain_matrices = unit_cell.coherentDomainMatrix @@ -249,7 +249,7 @@ def water_optical_profile(water_model, noUC=30, z_step=None, z_origin=None): z_stop = water_onset + noUC * water_model.a[2] z = np.arange(z_start, z_stop + 0.5 * z_step, z_step, dtype=np.float64) rho_f = water_model.zDensity_G(z, 0.0, 0.0) - wavelength = 12.398419843320026 / (water_model._E * 1e-3) + wavelength = HC_KEV_ANGSTROM / (water_model._E * 1e-3) scale = 2.8179403262e-5 * wavelength**2 / (2.0 * np.pi) profile = np.empty((z.size, 3), dtype=np.float64) profile[:, 0] = z From f9e5029838c20932e74ecce3e0021216afd3a444 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Mon, 27 Jul 2026 23:15:02 -0400 Subject: [PATCH 22/37] docs(phys): document stack_on's below_state and below_component params --- orgui/datautils/xrayutils/CTRfilm.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/orgui/datautils/xrayutils/CTRfilm.py b/orgui/datautils/xrayutils/CTRfilm.py index 63d3df3..ff49649 100644 --- a/orgui/datautils/xrayutils/CTRfilm.py +++ b/orgui/datautils/xrayutils/CTRfilm.py @@ -359,6 +359,17 @@ def stack_on( Absolute top height of the object below in Angstrom. :param float below_layer: Top cyclic layer identifier of the object below. + :param LayerState below_state: + Optional pre-built layer state for the object below. If given, + it is used as-is instead of constructing a fresh + ``LayerState(self.layer_cycle, below_layer)``. + :param below_component: + Optional underlying component (for example a :class:`Film`) + passed to this object's ``_bind_underlying_component`` if it + defines one, for subclasses (such as ``PoissonSurface``) that + need direct access to the component below rather than just its + scalar layer metadata. Ignored by subclasses without that + method. """ bind_underlying = getattr(self, "_bind_underlying_component", None) if bind_underlying is not None: From f7698a9f6af99e526d2fddce28150904809d273a Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Mon, 27 Jul 2026 23:34:12 -0400 Subject: [PATCH 23/37] perf(phys): make FormFactorCache lookup and touch O(1) find_grid was a linear scan over all cached Q-grids; touch (called on every cache hit) was a linear scan over all cached species across every grid -- the bigger hotspot of the two. Add a hash-bucketed index for find_grid (bucketed by the low 64 bits of the Q-grid's hash, still verified by full 128-bit hash + memcmp within a bucket, so a bucket collision can only cost extra comparisons, never a wrong result) and store each CachedSpecies's own list iterator so touch can splice it directly. Verified with a 500-call stress test at a tiny cache budget (473 evictions) with no crashes and correct hit/miss behavior, plus the existing cpp-backend test suite. --- orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp b/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp index f68a4ad..b3aff99 100644 --- a/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp +++ b/orgui/datautils/xrayutils/cpp/CTRcalc_cpp.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -43,6 +44,9 @@ struct CachedSpecies { std::array row; Hash128 hash; std::shared_ptr> values; + // Valid once inserted into FormFactorCache::lru_; lets touch() splice + // this entry to the LRU tail in O(1) instead of scanning lru_. + std::list>::iterator lru_it; }; struct CachedQGrid { @@ -108,6 +112,7 @@ class FormFactorCache { new_grid->hash = q_hash; grids_.push_back(new_grid); grid = std::prev(grids_.end()); + grid_index_[q_hash.low].push_back(grid); resident_bytes_ += q_bytes; } auto species = std::make_shared(); @@ -116,6 +121,7 @@ class FormFactorCache { species->values = std::make_shared>(std::move(values)); (*grid)->species.push_back(species); lru_.push_back(species); + species->lru_it = std::prev(lru_.end()); resident_bytes_ += values_bytes; evict_to_budget(); return species->values; @@ -124,6 +130,7 @@ class FormFactorCache { void clear() { std::lock_guard lock(mutex_); grids_.clear(); + grid_index_.clear(); lru_.clear(); resident_bytes_ = 0; } @@ -157,8 +164,16 @@ class FormFactorCache { private: using GridList = std::list>; + // Bucketed by the low 64 bits of the Q-grid's hash; find_grid still + // verifies the full 128-bit hash and the raw Q2 values within a bucket, + // so a same-bucket collision can only cost extra comparisons, never a + // wrong result. GridList::iterator find_grid(const std::vector &q2, const Hash128 &hash) { - for (auto grid = grids_.begin(); grid != grids_.end(); ++grid) { + const auto bucket = grid_index_.find(hash.low); + if (bucket == grid_index_.end()) { + return grids_.end(); + } + for (const auto &grid : bucket->second) { const auto &stored = *(*grid)->q2; if (same_hash((*grid)->hash, hash) && stored.size() == q2.size() && std::memcmp(stored.data(), q2.data(), q2.size() * sizeof(double)) == 0) { @@ -169,12 +184,7 @@ class FormFactorCache { } void touch(const std::shared_ptr &species) { - for (auto item = lru_.begin(); item != lru_.end(); ++item) { - if (*item == species) { - lru_.splice(lru_.end(), lru_, item); - return; - } - } + lru_.splice(lru_.end(), lru_, species->lru_it); } void evict_to_budget() { @@ -190,6 +200,14 @@ class FormFactorCache { ++evictions_; if (entries.empty()) { resident_bytes_ -= (*grid)->q2->size() * sizeof(double); + auto &bucket = grid_index_[(*grid)->hash.low]; + bucket.erase( + std::remove(bucket.begin(), bucket.end(), grid), + bucket.end() + ); + if (bucket.empty()) { + grid_index_.erase((*grid)->hash.low); + } grids_.erase(grid); } goto evicted; @@ -202,6 +220,7 @@ class FormFactorCache { std::mutex mutex_; GridList grids_; + std::unordered_map> grid_index_; std::list> lru_; std::size_t budget_bytes_ = form_factor_cache_default_budget; std::size_t resident_bytes_ = 0; From 21513d377b59b2794c7032fd0b97c27ad6e88f71 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Mon, 27 Jul 2026 23:37:45 -0400 Subject: [PATCH 24/37] test(phys): add value-level checks to the split-bulk-domain smoke test --- .../datautils/xrayutils/test/test_CTRcalc.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index 605a7e4..b5bdd58 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -2129,6 +2129,39 @@ def test_epitaxy_split_bulk_domains_feed_all_profile_paths(self): self.assertEqual(optical_profile.shape[1], 3) self.assertTrue(np.all(np.isfinite(optical_profile))) + # Value-level regression check, in addition to the isfinite smoke + # checks above: these are a frozen snapshot of this module's own + # current output (not independently verified against an analytic or + # external reference), so a mismatch signals the split-bulk-domain + # code path changed behavior, not necessarily that it is now wrong. + np.testing.assert_allclose( + structure_factor, + [10.959877214261 + 24.053170411198j, 0.769317301749 - 1.23830767953j], + rtol=1e-8, + ) + np.testing.assert_allclose( + density[[0, 100, 200, 300, 400]], + [ + 0.0, + -1.130402540575 - 0.006963145549j, + 0.6212434003285 + 0.002480857561j, + 1.108345606774e-04, + 0.0, + ], + atol=1e-9, + ) + np.testing.assert_allclose(np.sum(density), 40.00674724814 + 0.031011485600j, rtol=1e-8) + mid_row = optical_profile.shape[0] // 2 + np.testing.assert_allclose( + optical_profile[[0, mid_row, -1]], + [ + [-12.0, -9.65554085169e-10, -8.98126154061e-13], + [0.6, 2.222695401541e-06, 2.067477009624e-09], + [10.0, 0.0, 0.0], + ], + atol=1e-9, + ) + class TestLegacyLayeredCTR(unittest.TestCase): def test_legacy_xtal_uses_corrected_interface_support(self): From 4b3dd1abbc306433e5753d2ea697215bc675af2b Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Mon, 27 Jul 2026 23:38:46 -0400 Subject: [PATCH 25/37] fix(phys): handle empty coherentDomainMatrix in accelerated F_uc backends --- orgui/datautils/xrayutils/CTRuc.py | 35 ++++++++++-- .../datautils/xrayutils/test/test_CTRcalc.py | 56 +++++++++++++++++++ 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/orgui/datautils/xrayutils/CTRuc.py b/orgui/datautils/xrayutils/CTRuc.py index d156872..bb4418b 100644 --- a/orgui/datautils/xrayutils/CTRuc.py +++ b/orgui/datautils/xrayutils/CTRuc.py @@ -363,6 +363,20 @@ def _ctr_accel_module(): ) +def _coherent_domain_arrays(coherent_domain_matrix, coherent_domain_occupancy): + """Return accel-ready coherent domain arrays, handling zero domains. + + ``np.asarray`` on an empty list collapses to shape ``(0,)`` instead of + the ``(0, 3, 4)`` the accel bindings require, so the empty case is + reshaped explicitly. + """ + matrix = np.asarray(coherent_domain_matrix) + if matrix.size == 0: + matrix = matrix.reshape((0, 3, 4)) + occupancy = np.asarray(coherent_domain_occupancy) + return matrix, occupancy + + class WaterModel(Lattice, LinearFitFunctions): # path = os.path.split(__file__)[0] @@ -3466,6 +3480,9 @@ def F_uc_bulk(self, h, k, l, atten=0): # noqa: E741 if ctr_accel_enabled(): h, k, l = _ensure_contiguous(h, k, l, testOnly=False, astype=np.float64) # noqa: E741 accel = _ctr_accel_module() + domain_matrix, domain_occupancy = _coherent_domain_arrays( + self.coherentDomainMatrix, self.coherentDomainOccupancy + ) F = accel.unitcell_F_uc_bulk( h, k, @@ -3477,8 +3494,8 @@ def F_uc_bulk(self, h, k, l, atten=0): # noqa: E741 self.B_mat, self.R_mat, self.R_mat_inv, - np.asarray(self.coherentDomainMatrix), - np.asarray(self.coherentDomainOccupancy), + domain_matrix, + domain_occupancy, self.uc_area, ) return F @@ -3591,6 +3608,9 @@ def F_uc(self, h, k, l): # noqa: E741 if ctr_accel_enabled() and not self._special_formfactors_present: h, k, l = _ensure_contiguous(h, k, l, testOnly=False, astype=np.float64) # noqa: E741 accel = _ctr_accel_module() + domain_matrix, domain_occupancy = _coherent_domain_arrays( + self.coherentDomainMatrix, self.coherentDomainOccupancy + ) F = accel.unitcell_F_uc( h, k, @@ -3601,8 +3621,8 @@ def F_uc(self, h, k, l): # noqa: E741 self.B_mat, self.R_mat, self.R_mat_inv, - np.asarray(self.coherentDomainMatrix), - np.asarray(self.coherentDomainOccupancy), + domain_matrix, + domain_occupancy, self.uc_area, ) return F @@ -3675,6 +3695,9 @@ def F_bulk(self, h, k, l, atten=0): # noqa: E741 if ctr_accel_enabled(): h, k, l = _ensure_contiguous(h, k, l, testOnly=False, astype=np.float64) # noqa: E741 accel = _ctr_accel_module() + domain_matrix, domain_occupancy = _coherent_domain_arrays( + self.coherentDomainMatrix, self.coherentDomainOccupancy + ) F = accel.unitcell_F_bulk( h, k, @@ -3686,8 +3709,8 @@ def F_bulk(self, h, k, l, atten=0): # noqa: E741 self.B_mat, self.R_mat, self.R_mat_inv, - np.asarray(self.coherentDomainMatrix), - np.asarray(self.coherentDomainOccupancy), + domain_matrix, + domain_occupancy, self.uc_area, ) return F diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index b5bdd58..4129076 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -2770,6 +2770,62 @@ def testSetAccelBackendCanSelectNumpy(self): self.assertEqual(CTRuc.CTR_ACCEL_BACKEND, "numpy") +class TestUnitCellEmptyCoherentDomains(unittest.TestCase): + """A unit cell with zero coherent domains is a legitimate state (e.g. a + film layer entirely consumed by an EpitaxyInterface profile, see + test_profile_interface_can_consume_entire_film_width). ``np.asarray([])`` + collapses to shape ``(0,)`` rather than the ``(0, 3, 4)`` the accelerated + backends require, which must not raise. + """ + + def setUp(self): + self.original_backend = CTRuc.CTR_ACCEL_BACKEND + self.addCleanup(CTRuc.set_accel_backend, self.original_backend) + self.unitcell = CTRcalc.UnitCell( + [3.0, 3.0, 6.0], [90.0, 90.0, 90.0], name="empty_domains" + ) + self.unitcell.addAtom("C", [0.0, 0.0, 0.0], 0.1, 0.1, 1.0, layer=1) + self.unitcell.setEnergy(10000.0) + self.unitcell.coherentDomainMatrix = [] + self.unitcell.coherentDomainOccupancy = [] + self.h = np.array([0.0]) + self.k = np.array([0.0]) + self.l = np.array([0.5]) + + def _assert_zero_amplitude(self): + np.testing.assert_allclose( + self.unitcell.F_uc(self.h, self.k, self.l), 0.0 + ) + np.testing.assert_allclose( + self.unitcell.F_uc_bulk(self.h, self.k, self.l), 0.0 + ) + np.testing.assert_allclose( + self.unitcell.F_bulk(self.h, self.k, self.l), 0.0 + ) + + def testEmptyCoherentDomainsNumpyBackend(self): + CTRuc.set_accel_backend("numpy") + self._assert_zero_amplitude() + + def testEmptyCoherentDomainsNumbaBackend(self): + if not CTRuc.ctr_numba_accel_available(): + self.skipTest( + "Cannot perform Numba tests: _CTRcalc_accel library was not " + "imported. Is Numba installed?" + ) + CTRuc.set_accel_backend("numba") + self._assert_zero_amplitude() + + def testEmptyCoherentDomainsCppBackend(self): + if not CTRuc.HAS_CPP_ACCEL: + self.skipTest( + "Cannot perform C++ tests: _CTRcalc_cpp library was not " + "imported. Was the C++ extension built?" + ) + CTRuc.set_accel_backend("cpp") + self._assert_zero_amplitude() + + class TestCTRcalculationNumPy(StructureFactorValidationMixin, unittest.TestCase): def setUp(self): original_backend = CTRuc.CTR_ACCEL_BACKEND From e2e7a4c0b9e4b4b8a0bbd8898c80bc6a28381e23 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Tue, 28 Jul 2026 00:13:31 -0400 Subject: [PATCH 26/37] fix(phys): skip CIF import test without ase, fix Windows temp-file reopen --- orgui/datautils/xrayutils/test/test_CTRcalc.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index 4129076..68d56cd 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -32,6 +32,7 @@ import dataclasses import copy +import importlib.util import unittest from unittest import mock import os @@ -45,6 +46,9 @@ from ..CTRutil import generate_surface_termination_cells +HAS_ASE = importlib.util.find_spec("ase") is not None + + class _FakePy3DmolModel: def __init__(self): self.styles = [] @@ -335,6 +339,7 @@ def testFromFile(self): self.assertEqual(str(restored), serialized) +@unittest.skipUnless(HAS_ASE, "ase is not installed") class TestUnitCellCifImport(unittest.TestCase): def test_cif_import_orders_atoms_by_fractional_coordinate(self): cif = """data_ordering @@ -356,10 +361,11 @@ def test_cif_import_orders_atoms_by_fractional_coordinate(self): C1 C 0.4 0.2 0.2 Fe1 Fe 0.9 0.1 0.2 """ - with tempfile.NamedTemporaryFile(suffix=".cif", mode="w") as handle: - handle.write(cif) - handle.flush() - unitcell = CTRcalc.UnitCell.fromFile(handle.name) + with tempfile.TemporaryDirectory() as directory: + cif_path = os.path.join(directory, "ordering.cif") + with open(cif_path, "w") as handle: + handle.write(cif) + unitcell = CTRcalc.UnitCell.fromFile(cif_path) self.assertEqual(unitcell.names, ["Fe", "C", "O", "O"]) np.testing.assert_allclose( From 8ae28146088e7dbdbeadb55b64b1c652a27b3dcf Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Tue, 28 Jul 2026 00:13:36 -0400 Subject: [PATCH 27/37] ci: install extendedfilesupport extra so ase-dependent tests run --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea4603c..a8feb3f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: run: python -m pip install --upgrade pip pytest - name: Install package - run: python -m pip install ".[speedup]" + run: python -m pip install ".[speedup,extendedfilesupport]" - name: Run xrayutils tests run: >- From efa90439e916f15272b34e0c9bad0c4d3efa9472 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Tue, 28 Jul 2026 00:28:46 -0400 Subject: [PATCH 28/37] ci: run all xrayutils test modules and install symmetry extra --- .github/workflows/tests.yml | 6 +++++- orgui/datautils/xrayutils/AGENTS.md | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a8feb3f..5fcb393 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: run: python -m pip install --upgrade pip pytest - name: Install package - run: python -m pip install ".[speedup,extendedfilesupport]" + run: python -m pip install ".[speedup,extendedfilesupport,symmetry]" - name: Run xrayutils tests run: >- @@ -63,3 +63,7 @@ jobs: orgui.datautils.xrayutils.test.test_HKLcalc orgui.datautils.xrayutils.test.test_DetectorCalibration orgui.datautils.xrayutils.test.test_CTRcalc + orgui.datautils.xrayutils.test.test_CTRsymmetry + orgui.datautils.xrayutils.test.test_CTRresolution + orgui.datautils.xrayutils.test.test_CTRoptical_profile + orgui.datautils.xrayutils.test.test_scattering_factor_cache diff --git a/orgui/datautils/xrayutils/AGENTS.md b/orgui/datautils/xrayutils/AGENTS.md index 891d8c4..1fc53f6 100644 --- a/orgui/datautils/xrayutils/AGENTS.md +++ b/orgui/datautils/xrayutils/AGENTS.md @@ -100,6 +100,10 @@ Run the narrowest relevant regression test first: - `pytest orgui/datautils/xrayutils/test/test_HKLcalc.py` - `pytest orgui/datautils/xrayutils/test/test_DetectorCalibration.py` - `pytest orgui/datautils/xrayutils/test/test_CTRcalc.py` +- `pytest orgui/datautils/xrayutils/test/test_CTRsymmetry.py` +- `pytest orgui/datautils/xrayutils/test/test_CTRresolution.py` +- `pytest orgui/datautils/xrayutils/test/test_CTRoptical_profile.py` +- `pytest orgui/datautils/xrayutils/test/test_scattering_factor_cache.py` Use `ruff check orgui/datautils/xrayutils` for local lint checks. If a change touches config-facing unit conversions, also inspect `examples/config_minimal` From 7151d68390fc73d278f5dbe4dcd47fcc5981b200 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Tue, 28 Jul 2026 00:28:50 -0400 Subject: [PATCH 29/37] ci: run new xrayutils test modules in wheel smoke tests Install the extendedfilesupport and symmetry extras via cibuildwheel's CIBW_TEST_EXTRAS (pip install [extras]). --- .github/workflows/build-wheels.yml | 5 +++++ .github/workflows/release.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index e6edfff..cf91c70 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -20,12 +20,17 @@ env: CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--vsenv -Dstatic_xxhash=true" CIBW_TEST_ENVIRONMENT: "PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 MPLBACKEND=Agg QT_QPA_PLATFORM=minimal" CIBW_TEST_REQUIRES: pytest + CIBW_TEST_EXTRAS: extendedfilesupport,symmetry CIBW_TEST_COMMAND: >- python -c "from orgui.datautils.xrayutils import CTRuc; assert CTRuc.HAS_CPP_ACCEL; CTRuc.set_accel_backend('cpp'); print('CTR_ACCEL_BACKEND=', CTRuc.CTR_ACCEL_BACKEND)" && pytest --pyargs orgui.datautils.xrayutils.test.test_CTRcalc + orgui.datautils.xrayutils.test.test_CTRsymmetry + orgui.datautils.xrayutils.test.test_CTRresolution + orgui.datautils.xrayutils.test.test_CTRoptical_profile + orgui.datautils.xrayutils.test.test_scattering_factor_cache CIBW_TEST_SKIP: "*-manylinux_aarch64" jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80318fa..a8c51a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,12 +125,17 @@ jobs: CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--vsenv -Dstatic_xxhash=true" CIBW_TEST_ENVIRONMENT: "PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 MPLBACKEND=Agg QT_QPA_PLATFORM=minimal" CIBW_TEST_REQUIRES: pytest + CIBW_TEST_EXTRAS: extendedfilesupport,symmetry CIBW_TEST_COMMAND: >- python -c "from orgui.datautils.xrayutils import CTRuc; assert CTRuc.HAS_CPP_ACCEL; CTRuc.set_accel_backend('cpp'); print('CTR_ACCEL_BACKEND=', CTRuc.CTR_ACCEL_BACKEND)" && pytest --pyargs orgui.datautils.xrayutils.test.test_CTRcalc + orgui.datautils.xrayutils.test.test_CTRsymmetry + orgui.datautils.xrayutils.test.test_CTRresolution + orgui.datautils.xrayutils.test.test_CTRoptical_profile + orgui.datautils.xrayutils.test.test_scattering_factor_cache CIBW_TEST_SKIP: "*-manylinux_aarch64" with: output-dir: wheelhouse From d2c3bbadd17c11a7408814d9244933ecc5be3555 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Tue, 28 Jul 2026 01:05:16 -0400 Subject: [PATCH 30/37] fix(phys): fix float-equality CI failure and package unpackaged test fixtures --- .../datautils/xrayutils/test/test_CTRcalc.py | 53 +++--- .../xrayutils/test/test_CTRsymmetry.py | 20 ++- .../testdata/0001_fit_2V036_reference.xtal | 96 ++++++++++ .../test/testdata/CTRs_reference.dat | 9 + .../testdata/RuO2_TiO2_Poisson_etching.xpr | 170 ++++++++++++++++++ .../testdata/RuO2_TiO2_Poisson_etching.xtal | 170 ++++++++++++++++++ 6 files changed, 485 insertions(+), 33 deletions(-) create mode 100644 orgui/datautils/xrayutils/test/testdata/0001_fit_2V036_reference.xtal create mode 100644 orgui/datautils/xrayutils/test/testdata/CTRs_reference.dat create mode 100644 orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xpr create mode 100644 orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xtal diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index 68d56cd..6926368 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -2171,18 +2171,10 @@ def test_epitaxy_split_bulk_domains_feed_all_profile_paths(self): class TestLegacyLayeredCTR(unittest.TestCase): def test_legacy_xtal_uses_corrected_interface_support(self): - repository_root = os.path.abspath( - os.path.join(os.path.dirname(__file__), "..", "..", "..", "..") - ) - fixture_root = os.path.join( - repository_root, "examples", "CTR", "test" - ) + fixture_root = os.path.join(os.path.dirname(__file__), "testdata") xtal_path = os.path.join( fixture_root, "0001_fit_2V036_reference.xtal" ) - for path in (xtal_path,): - if not os.path.exists(path): - self.skipTest(f"Missing optional CTR fixture: {path}") xtal = CTRcalc.SXRDCrystal.fromFile( xtal_path ) @@ -2248,9 +2240,18 @@ def test_legacy_xtal_uses_corrected_interface_support(self): def test_legacy_xtal_reconstructs_reference_interface(self): """Original pre-fix regression check against the frozen - CTRs_reference.dat baseline, restored verbatim (see git history for - commit beee738, "fix: EpitaxyInterface now is again consuming Film - so that interfacial strain field is correct"). + CTRs_reference.dat baseline (see git history for commit beee738, + "fix: EpitaxyInterface now is again consuming Film so that + interfacial strain field is correct"). + + The fixture files live in testdata/ (packaged with the test suite) + rather than the repository's examples/CTR/test/ directory, which is + not included in installed wheels -- this test used to silently skip + on any CI/CD run that installs the package instead of using a + source checkout, masking the discrepancy below entirely. + testdata/CTRs_reference.dat is trimmed to the header plus the 8 rows + this test actually reads (`max_rows=8` below); the full file is + still available at examples/CTR/test/CTRs_reference.dat. This is EXPECTED TO CURRENTLY FAIL: CTRs_reference.dat encodes the pre-beee738 interfacial-strain behavior and was never regenerated @@ -2268,19 +2269,11 @@ def test_legacy_xtal_reconstructs_reference_interface(self): than resolving the failure by editing either one without investigation. This is deferred pending other PR feedback. """ - repository_root = os.path.abspath( - os.path.join(os.path.dirname(__file__), "..", "..", "..", "..") - ) - fixture_root = os.path.join( - repository_root, "examples", "CTR", "test" - ) + fixture_root = os.path.join(os.path.dirname(__file__), "testdata") xtal_path = os.path.join( fixture_root, "0001_fit_2V036_reference.xtal" ) reference_path = os.path.join(fixture_root, "CTRs_reference.dat") - for path in (xtal_path, reference_path): - if not os.path.exists(path): - self.skipTest(f"Missing optional CTR fixture: {path}") xtal = CTRcalc.SXRDCrystal.fromFile( xtal_path ) @@ -2680,15 +2673,15 @@ def test_crystal_fit_vector_updates_ctrfilm_components(self): self.assertAlmostEqual(crystal["surface"].profile.mean_change, 2.0) def test_example_xtal_and_xpr_describe_same_poisson_stack(self): - repository_root = os.path.abspath( - os.path.join(os.path.dirname(__file__), "..", "..", "..", "..") - ) - example_root = os.path.join(repository_root, "examples", "CTR") - xtal_path = os.path.join(example_root, "RuO2_TiO2_Poisson_etching.xtal") - xpr_path = os.path.join(example_root, "RuO2_TiO2_Poisson_etching.xpr") - for path in (xtal_path, xpr_path): - if not os.path.exists(path): - self.skipTest(f"Missing optional CTR fixture: {path}") + """Fixtures live in testdata/ (packaged with the test suite) rather + than examples/CTR/, which is not included in installed wheels -- + this test used to silently skip on any CI/CD run that installs the + package instead of using a source checkout. The full, canonical + copies remain at examples/CTR/RuO2_TiO2_Poisson_etching.{xtal,xpr}. + """ + fixture_root = os.path.join(os.path.dirname(__file__), "testdata") + xtal_path = os.path.join(fixture_root, "RuO2_TiO2_Poisson_etching.xtal") + xpr_path = os.path.join(fixture_root, "RuO2_TiO2_Poisson_etching.xpr") xtal = CTRcalc.SXRDCrystal.fromFile(xtal_path) xpr = CTRcalc.SXRDCrystal.fromFile(xpr_path) diff --git a/orgui/datautils/xrayutils/test/test_CTRsymmetry.py b/orgui/datautils/xrayutils/test/test_CTRsymmetry.py index 5a3c8bf..d71a2dc 100644 --- a/orgui/datautils/xrayutils/test/test_CTRsymmetry.py +++ b/orgui/datautils/xrayutils/test/test_CTRsymmetry.py @@ -279,6 +279,18 @@ def make_pyxtal_generated_unitcell( model = CTRsymmetry.model_from_seed(seed, surface_spec, tol=1e-3) return model.build_unitcell(element) + def assert_variables_allclose(self, actual, expected): + """Compare Wyckoff ``variables`` dicts, tolerating float rounding. + + pyxtal/spglib can return values that are a few ULP off an exact + input (e.g. 0.11999999999999997 instead of 0.12), so this avoids + exact dict equality on floats. + """ + self.assertEqual(set(actual), set(expected)) + np.testing.assert_allclose( + [actual[key] for key in expected], [expected[key] for key in expected] + ) + @staticmethod def coupling_factor_vectors(unitcell, site_id): grouped = {} @@ -364,7 +376,7 @@ def test_wyckoff_query_exposes_oxygen_u_couplings(self): sites = unitcell.wyckoff_sites() self.assertEqual([site["site_id"] for site in sites], ["Ru_2a", "O_4f"]) self.assertEqual(sites[0]["spacegroup_number"], 136) - self.assertEqual(sites[1]["variables"], {"u": 0.30569}) + self.assert_variables_allclose(sites[1]["variables"], {"u": 0.30569}) self.assertEqual(sites[1]["status"], "metadata_only") couplings = unitcell.wyckoff_couplings("O_4f") @@ -536,7 +548,7 @@ def test_trigonal_general_site_exposes_u_minus_v_couplings(self): Lattice.hexagonal(4.0, 6.0), ) - self.assertEqual( + self.assert_variables_allclose( unitcell.wyckoff_sites()[0]["variables"], {"u": 0.12, "v": 0.27, "w": 0.34}, ) @@ -638,7 +650,9 @@ def test_symmetry_metadata_round_trips_without_pyxtal_construction(self): restored = UnitCell.fromStr(text) - self.assertEqual(restored.wyckoff_sites()[1]["variables"], {"u": 0.30569}) + self.assert_variables_allclose( + restored.wyckoff_sites()[1]["variables"], {"u": 0.30569} + ) self.assertEqual(len(restored.wyckoff_couplings("O_4f")), 8) self.assertGreater(len(restored.wyckoff_site_couplings("Ru_2a")), 0) original_couplings = sorted( diff --git a/orgui/datautils/xrayutils/test/testdata/0001_fit_2V036_reference.xtal b/orgui/datautils/xrayutils/test/testdata/0001_fit_2V036_reference.xtal new file mode 100644 index 0000000..7669762 --- /dev/null +++ b/orgui/datautils/xrayutils/test/testdata/0001_fit_2V036_reference.xtal @@ -0,0 +1,96 @@ +E = 20.00000 keV + +# Film RuO2 +0002 occupancy = 1.00000 +- nan + +Width/layers +(18.00000 +- nan) + +UnitCell RuO2 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (0.80569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (0.69431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ru (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ru (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (0.30569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (0.19431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ru (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ru (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +# EpitaxyInterface TiO2toRuO2 +0001 occupancy = 1.00000 +- nan +type skellam +Width/cells Skew/cells +(0.35000 +- nan) (0.00000 +- 0.00000) + +TopUnitCell RuO2interf +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (0.80569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (0.69431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ru (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ru (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (0.30569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (0.19431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ru (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ru (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +BottomUnitCell TiO2interf +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (0.80458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (0.69542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ti (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ti (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30458 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69542 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (0.30458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (0.19542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ti (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ti (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19542 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80458 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +# UnitCell bulk +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (-0.19542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (-0.30458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ti (0.50000 +- nan) (0.00000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ti (0.00000 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30458 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69542 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (-0.69542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (-0.80458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ti (0.00000 +- nan) (0.00000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ti (0.50000 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19542 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80458 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = -1.0, 2.0 = -0.5 +layer_behaviour: ignore diff --git a/orgui/datautils/xrayutils/test/testdata/CTRs_reference.dat b/orgui/datautils/xrayutils/test/testdata/CTRs_reference.dat new file mode 100644 index 0000000..29aa069 --- /dev/null +++ b/orgui/datautils/xrayutils/test/testdata/CTRs_reference.dat @@ -0,0 +1,9 @@ +# H K L F_HKL phi +0.00000 0.00000 0.00000 905.72572 nan -3.00000 +0.00000 0.00000 0.00363 367.33955 nan -3.00000 +0.00000 0.00000 0.00725 201.34023 nan -3.00000 +0.00000 0.00000 0.01088 141.76241 nan -3.00000 +0.00000 0.00000 0.01451 112.43899 nan -3.00000 +0.00000 0.00000 0.01813 95.37011 nan -3.00000 +0.00000 0.00000 0.02176 84.28214 nan -3.00000 +0.00000 0.00000 0.02539 76.45271 nan -3.00000 diff --git a/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xpr b/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xpr new file mode 100644 index 0000000..684cac7 --- /dev/null +++ b/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xpr @@ -0,0 +1,170 @@ +E = 20.00000 keV +# PoissonSurface RuO2surface +0003 occupancy = 1.00000 + +W/layers alpha offset/layers +-6.00000 1.00000 0.00000 + +TerminationUnitCell 1 RuO2_termination_1 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 13.0912 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (0.15285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (0.09715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +02 Ru (0.50000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +03 Ru (0.00000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +04 O (0.30569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +05 O (0.69431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (0.40285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (0.34715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ru (0.00000 +- nan) (0.00000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ru (0.50000 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19431 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80569 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +12 O (0.50000 +- nan) (0.00000 +- nan) (0.65285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +13 O (0.00000 +- nan) (0.00000 +- nan) (0.59715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +14 Ru (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +15 Ru (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +16 O (0.30569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +17 O (0.69431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +18 O (0.00000 +- nan) (0.00000 +- nan) (0.90285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +19 O (0.50000 +- nan) (0.00000 +- nan) (0.84715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +20 Ru (0.00000 +- nan) (0.00000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +21 Ru (0.50000 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +22 O (0.19431 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +23 O (0.80569 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = 0.75 +layer_behaviour: select +layer_cycle: 1.0 + + +TerminationUnitCell 2 RuO2_termination_2 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 13.0912 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.00000 +- nan) (0.00000 +- nan) (0.15285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.50000 +- nan) (0.00000 +- nan) (0.09716 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ru (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ru (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.19431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.80569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.50000 +- nan) (0.00000 +- nan) (0.40285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +07 O (0.00000 +- nan) (0.00000 +- nan) (0.34715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +08 Ru (0.50000 +- nan) (0.00000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +09 Ru (0.00000 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +10 O (0.30569 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +11 O (0.69431 +- nan) (0.50000 +- nan) (0.25000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +12 O (0.00000 +- nan) (0.00000 +- nan) (0.65285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +13 O (0.50000 +- nan) (0.00000 +- nan) (0.59715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +14 Ru (0.00000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +15 Ru (0.50000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +16 O (0.19431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +17 O (0.80569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +18 O (0.50000 +- nan) (0.00000 +- nan) (0.90285 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +19 O (0.00000 +- nan) (0.00000 +- nan) (0.84715 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +20 Ru (0.50000 +- nan) (0.00000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +21 Ru (0.00000 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +22 O (0.30569 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +23 O (0.69431 +- nan) (0.50000 +- nan) (0.75000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +layerpos: 2.0 = 0.75 +layer_behaviour: select +layer_cycle: 2.0 + + + +# Film RuO2 +0002 occupancy = 1.00000 + +Width/layers +(17.00000 +- nan) + +UnitCell RuO2 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (0.80569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (0.69431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ru (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ru (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (0.30569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (0.19431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ru (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ru (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +# EpitaxyInterface TiO2toRuO2 +0001 occupancy = 1.00000 +type skellam +Width/cells Skew/cells +0.35000 0.00000 + +TopUnitCell RuO2interf +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (0.80569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (0.69431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ru (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ru (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30569 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69431 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (0.30569 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (0.19431 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ru (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ru (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19431 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80569 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.5033 +- nan) (0.5033 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +BottomUnitCell TiO2interf +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (0.80458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (0.69542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ti (0.50000 +- nan) (0.00000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ti (0.00000 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30458 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69542 +- nan) (0.50000 +- nan) (0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (0.30458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (0.19542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ti (0.00000 +- nan) (0.00000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ti (0.50000 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19542 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80458 +- nan) (0.50000 +- nan) (0.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +# UnitCell bulk +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O (0.50000 +- nan) (0.00000 +- nan) (-0.19542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +01 O (0.00000 +- nan) (0.00000 +- nan) (-0.30458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +02 Ti (0.50000 +- nan) (0.00000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +03 Ti (0.00000 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +04 O (0.30458 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +05 O (0.69542 +- nan) (0.50000 +- nan) (-0.50000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (2 +- nan) +06 O (0.00000 +- nan) (0.00000 +- nan) (-0.69542 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +07 O (0.50000 +- nan) (0.00000 +- nan) (-0.80458 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +08 Ti (0.00000 +- nan) (0.00000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +09 Ti (0.50000 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +10 O (0.19542 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +11 O (0.80458 +- nan) (0.50000 +- nan) (-1.00000 +- nan) (0.3719 +- nan) (0.3719 +- nan) (1.0000 +- nan) (1 +- nan) +layerpos: 1.0 = -1.0, 2.0 = -0.5 +layer_behaviour: ignore diff --git a/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xtal b/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xtal new file mode 100644 index 0000000..4e310a9 --- /dev/null +++ b/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xtal @@ -0,0 +1,170 @@ +E = 20.00000 keV +# PoissonSurface RuO2surface +0003 occupancy = 1.00000 + +W/layers alpha offset/layers +-6.00000 1.00000 0.00000 + +TerminationUnitCell 1 RuO2_termination_1 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 13.0912 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.50000 0.00000 0.15285 0.5033 0.5033 1.0000 1 +01 O 0.00000 0.00000 0.09715 0.5033 0.5033 1.0000 1 +02 Ru 0.50000 0.00000 0.00000 0.5033 0.5033 1.0000 1 +03 Ru 0.00000 0.50000 0.00000 0.5033 0.5033 1.0000 1 +04 O 0.30569 0.50000 0.00000 0.5033 0.5033 1.0000 1 +05 O 0.69431 0.50000 0.00000 0.5033 0.5033 1.0000 1 +06 O 0.00000 0.00000 0.40285 0.5033 0.5033 1.0000 1 +07 O 0.50000 0.00000 0.34715 0.5033 0.5033 1.0000 1 +08 Ru 0.00000 0.00000 0.25000 0.5033 0.5033 1.0000 1 +09 Ru 0.50000 0.50000 0.25000 0.5033 0.5033 1.0000 1 +10 O 0.19431 0.50000 0.25000 0.5033 0.5033 1.0000 1 +11 O 0.80569 0.50000 0.25000 0.5033 0.5033 1.0000 1 +12 O 0.50000 0.00000 0.65285 0.5033 0.5033 1.0000 1 +13 O 0.00000 0.00000 0.59715 0.5033 0.5033 1.0000 1 +14 Ru 0.50000 0.00000 0.50000 0.5033 0.5033 1.0000 1 +15 Ru 0.00000 0.50000 0.50000 0.5033 0.5033 1.0000 1 +16 O 0.30569 0.50000 0.50000 0.5033 0.5033 1.0000 1 +17 O 0.69431 0.50000 0.50000 0.5033 0.5033 1.0000 1 +18 O 0.00000 0.00000 0.90285 0.5033 0.5033 1.0000 1 +19 O 0.50000 0.00000 0.84715 0.5033 0.5033 1.0000 1 +20 Ru 0.00000 0.00000 0.75000 0.5033 0.5033 1.0000 1 +21 Ru 0.50000 0.50000 0.75000 0.5033 0.5033 1.0000 1 +22 O 0.19431 0.50000 0.75000 0.5033 0.5033 1.0000 1 +23 O 0.80569 0.50000 0.75000 0.5033 0.5033 1.0000 1 +layerpos: 1.0 = 0.75 +layer_behaviour: select +layer_cycle: 1.0 + + +TerminationUnitCell 2 RuO2_termination_2 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 13.0912 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.00000 0.00000 0.15285 0.5033 0.5033 1.0000 2 +01 O 0.50000 0.00000 0.09716 0.5033 0.5033 1.0000 2 +02 Ru 0.00000 0.00000 0.00000 0.5033 0.5033 1.0000 2 +03 Ru 0.50000 0.50000 0.00000 0.5033 0.5033 1.0000 2 +04 O 0.19431 0.50000 0.00000 0.5033 0.5033 1.0000 2 +05 O 0.80569 0.50000 0.00000 0.5033 0.5033 1.0000 2 +06 O 0.50000 0.00000 0.40285 0.5033 0.5033 1.0000 2 +07 O 0.00000 0.00000 0.34715 0.5033 0.5033 1.0000 2 +08 Ru 0.50000 0.00000 0.25000 0.5033 0.5033 1.0000 2 +09 Ru 0.00000 0.50000 0.25000 0.5033 0.5033 1.0000 2 +10 O 0.30569 0.50000 0.25000 0.5033 0.5033 1.0000 2 +11 O 0.69431 0.50000 0.25000 0.5033 0.5033 1.0000 2 +12 O 0.00000 0.00000 0.65285 0.5033 0.5033 1.0000 2 +13 O 0.50000 0.00000 0.59715 0.5033 0.5033 1.0000 2 +14 Ru 0.00000 0.00000 0.50000 0.5033 0.5033 1.0000 2 +15 Ru 0.50000 0.50000 0.50000 0.5033 0.5033 1.0000 2 +16 O 0.19431 0.50000 0.50000 0.5033 0.5033 1.0000 2 +17 O 0.80569 0.50000 0.50000 0.5033 0.5033 1.0000 2 +18 O 0.50000 0.00000 0.90285 0.5033 0.5033 1.0000 2 +19 O 0.00000 0.00000 0.84715 0.5033 0.5033 1.0000 2 +20 Ru 0.50000 0.00000 0.75000 0.5033 0.5033 1.0000 2 +21 Ru 0.00000 0.50000 0.75000 0.5033 0.5033 1.0000 2 +22 O 0.30569 0.50000 0.75000 0.5033 0.5033 1.0000 2 +23 O 0.69431 0.50000 0.75000 0.5033 0.5033 1.0000 2 +layerpos: 2.0 = 0.75 +layer_behaviour: select +layer_cycle: 2.0 + + + +# Film RuO2 +0002 occupancy = 1.00000 + +Width/layers +17.00000 + +UnitCell RuO2 +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.50000 0.00000 0.80569 0.5033 0.5033 1.0000 2 +01 O 0.00000 0.00000 0.69431 0.5033 0.5033 1.0000 2 +02 Ru 0.50000 0.00000 0.50000 0.5033 0.5033 1.0000 2 +03 Ru 0.00000 0.50000 0.50000 0.5033 0.5033 1.0000 2 +04 O 0.30569 0.50000 0.50000 0.5033 0.5033 1.0000 2 +05 O 0.69431 0.50000 0.50000 0.5033 0.5033 1.0000 2 +06 O 0.00000 0.00000 0.30569 0.5033 0.5033 1.0000 1 +07 O 0.50000 0.00000 0.19431 0.5033 0.5033 1.0000 1 +08 Ru 0.00000 0.00000 0.00000 0.5033 0.5033 1.0000 1 +09 Ru 0.50000 0.50000 0.00000 0.5033 0.5033 1.0000 1 +10 O 0.19431 0.50000 0.00000 0.5033 0.5033 1.0000 1 +11 O 0.80569 0.50000 0.00000 0.5033 0.5033 1.0000 1 +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +# EpitaxyInterface TiO2toRuO2 +0001 occupancy = 1.00000 +type skellam +Width/cells Skew/cells +0.35000 0.00000 + +TopUnitCell RuO2interf +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5456 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.50000 0.00000 0.80569 0.5033 0.5033 1.0000 2 +01 O 0.00000 0.00000 0.69431 0.5033 0.5033 1.0000 2 +02 Ru 0.50000 0.00000 0.50000 0.5033 0.5033 1.0000 2 +03 Ru 0.00000 0.50000 0.50000 0.5033 0.5033 1.0000 2 +04 O 0.30569 0.50000 0.50000 0.5033 0.5033 1.0000 2 +05 O 0.69431 0.50000 0.50000 0.5033 0.5033 1.0000 2 +06 O 0.00000 0.00000 0.30569 0.5033 0.5033 1.0000 1 +07 O 0.50000 0.00000 0.19431 0.5033 0.5033 1.0000 1 +08 Ru 0.00000 0.00000 0.00000 0.5033 0.5033 1.0000 1 +09 Ru 0.50000 0.50000 0.00000 0.5033 0.5033 1.0000 1 +10 O 0.19431 0.50000 0.00000 0.5033 0.5033 1.0000 1 +11 O 0.80569 0.50000 0.00000 0.5033 0.5033 1.0000 1 +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +BottomUnitCell TiO2interf +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.50000 0.00000 0.80458 0.3719 0.3719 1.0000 2 +01 O 0.00000 0.00000 0.69542 0.3719 0.3719 1.0000 2 +02 Ti 0.50000 0.00000 0.50000 0.3719 0.3719 1.0000 2 +03 Ti 0.00000 0.50000 0.50000 0.3719 0.3719 1.0000 2 +04 O 0.30458 0.50000 0.50000 0.3719 0.3719 1.0000 2 +05 O 0.69542 0.50000 0.50000 0.3719 0.3719 1.0000 2 +06 O 0.00000 0.00000 0.30458 0.3719 0.3719 1.0000 1 +07 O 0.50000 0.00000 0.19542 0.3719 0.3719 1.0000 1 +08 Ti 0.00000 0.00000 0.00000 0.3719 0.3719 1.0000 1 +09 Ti 0.50000 0.50000 0.00000 0.3719 0.3719 1.0000 1 +10 O 0.19542 0.50000 0.00000 0.3719 0.3719 1.0000 1 +11 O 0.80458 0.50000 0.00000 0.3719 0.3719 1.0000 1 +layerpos: 1.0 = 0.0, 2.0 = 0.5 +layer_behaviour: ignore + + +# UnitCell bulk +return +Coherent 1.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 1.00000 0.00000 0.00000 0.00000 +6.5807 2.9692 6.5807 90.0000 90.0000 90.0000 +Name x/frac y/frac z/frac iDW oDW occup layerIdx +00 O 0.50000 0.00000 -0.19542 0.3719 0.3719 1.0000 2 +01 O 0.00000 0.00000 -0.30458 0.3719 0.3719 1.0000 2 +02 Ti 0.50000 0.00000 -0.50000 0.3719 0.3719 1.0000 2 +03 Ti 0.00000 0.50000 -0.50000 0.3719 0.3719 1.0000 2 +04 O 0.30458 0.50000 -0.50000 0.3719 0.3719 1.0000 2 +05 O 0.69542 0.50000 -0.50000 0.3719 0.3719 1.0000 2 +06 O 0.00000 0.00000 -0.69542 0.3719 0.3719 1.0000 1 +07 O 0.50000 0.00000 -0.80458 0.3719 0.3719 1.0000 1 +08 Ti 0.00000 0.00000 -1.00000 0.3719 0.3719 1.0000 1 +09 Ti 0.50000 0.50000 -1.00000 0.3719 0.3719 1.0000 1 +10 O 0.19542 0.50000 -1.00000 0.3719 0.3719 1.0000 1 +11 O 0.80458 0.50000 -1.00000 0.3719 0.3719 1.0000 1 +layerpos: 1.0 = -1.0, 2.0 = -0.5 +layer_behaviour: ignore From ce0828d8643c9684f684d52ff6d5e51f6c9ade22 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Tue, 28 Jul 2026 16:43:04 -0400 Subject: [PATCH 31/37] fix(phys): anchor film correctly to the bulk at lower end --- doc/source/ctr_structure_factors.rst | 15 ++- orgui/datautils/xrayutils/CTRfilm.py | 52 +++++++-- .../datautils/xrayutils/test/test_CTRcalc.py | 100 ++++++++++++++---- 3 files changed, 133 insertions(+), 34 deletions(-) diff --git a/doc/source/ctr_structure_factors.rst b/doc/source/ctr_structure_factors.rst index f63a7b2..915ac0c 100644 --- a/doc/source/ctr_structure_factors.rst +++ b/doc/source/ctr_structure_factors.rst @@ -227,11 +227,18 @@ position is z(C) = z_0 + C (z_1 - z_0), \qquad 0 \leq C \leq 1. Thus ``C = 0`` leaves both materials unstrained, while ``C = 1`` gives the -fully coherent interface. The ``offset`` parameter translates only the upper -material and all subsequently stacked components. It is expressed in -fractional lower-bulk :math:`c` coordinates, so its physical displacement is +fully coherent interface. The coherent field is anchored to the unstrained +bulk at the lower edge of the represented statistical support. Its accumulated +displacement is propagated upward to the upper material and subsequently +stacked components; it is not removed by forcing the strained and unstrained +lattices to coincide again at the nominal boundary. + +The ``offset`` parameter adds a translation to only the upper material and all +subsequently stacked components. It is expressed in fractional lower-bulk +:math:`c` coordinates, so its physical displacement is ``offset * uc_bottom.a[2]`` Angstrom. Positive offsets point toward increasing -``z``. The statistical boundary and the semi-infinite bulk do not move. +``z``. The nominal statistical boundary and the semi-infinite bulk do not +move. Crystal composition ------------------- diff --git a/orgui/datautils/xrayutils/CTRfilm.py b/orgui/datautils/xrayutils/CTRfilm.py index ff49649..2785cd9 100644 --- a/orgui/datautils/xrayutils/CTRfilm.py +++ b/orgui/datautils/xrayutils/CTRfilm.py @@ -487,6 +487,7 @@ def __init__(self, uc_top, uc_bottom, type="skellam", **kwargs): self.below_loc = 0.0 self.below_H = 0.0 self.below_layer = -1.0 + self._coherence_displacement = 0.0 self._initialize_layer_stacking(kwargs) def set_ucs(self, uc_top, uc_bottom, **kwargs): @@ -636,14 +637,20 @@ def stacking_loc_absolute(self): """Return the translated film-side boundary in Angstrom. The statistical boundary and lower bulk remain at - :attr:`loc_absolute`. A Film uses this translated location as the - origin of its requested total width, while + :attr:`loc_absolute`. The coherent strain displacement accumulated + upward from the fixed bulk and the explicit film offset translate the + film-side boundary. A Film uses this location as the origin of its + requested total width, while :attr:`stacking_height_absolute` supplies the physical support it must not duplicate. """ if np.any(self._basis_created != self.basis): self.createInterfaceCells() - return self.loc_absolute + self.basis[3] * self.uc_bottom.a[2] + return ( + self.loc_absolute + + self._coherence_displacement + + self.basis[3] * self.uc_bottom.a[2] + ) @property def layer_state(self): @@ -782,11 +789,6 @@ def createInterfaceCells(self): uc_no_loc = int(np.floor(loc_rescaled)) // n_layers layer_no_loc = int(np.floor(loc_rescaled)) % n_layers loc_remainder = (loc_rescaled % n_layers) % 1 - coherent_loc_mat = coherent_top[layer_no_loc][uc_no_loc] - coherent_loc = ( - coherent_loc_mat[2, 3] - + loc_remainder * coherent_loc_mat[2, 2] - ) * a3_top ideal_top_loc_mat = ideal_top[layer_no_loc][uc_no_loc] ideal_top_loc = ( ideal_top_loc_mat[2, 3] @@ -799,11 +801,41 @@ def createInterfaceCells(self): ) * a3_bottom top_offset = offset * a3_bottom + # The lower support boundary is the physical anchor shared by the + # coherent field and the unstrained semi-infinite bulk. Translate + # both using the ideal lower-lattice boundary location. The + # coherent statistical boundary is then free to move by the + # displacement accumulated through the strain field. The + # displacement at the upper support boundary is passed to every + # subsequently stacked component. + upper_layer_id = self.layer_order[-1] + upper_layer_space = np.diff( + self.layerpos, + append=self.layerpos[0] + 1, + )[-1] + upper_boundary = ( + self.uc_top.layerpos[upper_layer_id] + upper_layer_space + ) + coherent_upper_mat = coherent_top[-1][-1] + ideal_upper_mat = ideal_top[-1][-1] + coherent_upper = ( + coherent_upper_mat[2, 3] + - ideal_bottom_loc / a3_top + + coherent_upper_mat[2, 2] * upper_boundary + ) * a3_top + ideal_upper = ( + ideal_upper_mat[2, 3] + - ideal_top_loc / a3_top + + ideal_upper_mat[2, 2] * upper_boundary + ) * a3_top + self._coherence_displacement = coherence * ( + coherent_upper - ideal_upper + ) for i, (uc_t, uc_b) in enumerate(zip(self.top_layers, self.bottom_layers)): for coherent_mat, ideal_mat in zip(coherent_top[i], ideal_top[i]): coherent_mat = np.copy(coherent_mat) ideal_mat = np.copy(ideal_mat) - coherent_mat[2, 3] -= coherent_loc / a3_top + coherent_mat[2, 3] -= ideal_bottom_loc / a3_top ideal_mat[2, 3] -= ideal_top_loc / a3_top interpolated = ideal_mat + coherence * ( coherent_mat - ideal_mat @@ -816,7 +848,7 @@ def createInterfaceCells(self): ): coherent_mat = np.copy(coherent_mat) ideal_mat = np.copy(ideal_mat) - coherent_mat[2, 3] -= coherent_loc / a3_bottom + coherent_mat[2, 3] -= ideal_bottom_loc / a3_bottom ideal_mat[2, 3] -= ideal_bottom_loc / a3_bottom interpolated = ideal_mat + coherence * ( coherent_mat - ideal_mat diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index 6926368..fdc240f 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -1829,7 +1829,8 @@ def test_profile_interface_support_is_not_owned_by_film(self): interface.stacking_height_absolute, interface.height_absolute ) self.assertAlmostEqual( - interface.stacking_loc_absolute, interface.loc_absolute + interface.stacking_loc_absolute, + interface.loc_absolute + interface._coherence_displacement, ) self.assertAlmostEqual(film.pos_absolute, interface.height_absolute) self.assertGreater(film.pos_absolute, interface.loc_absolute) @@ -1972,6 +1973,56 @@ def test_epitaxy_bulk_replacement_subtracts_fixed_lattice_positions(self): np.any(np.asarray(layer.coherentDomainOccupancy[split:]) < 0.0) ) + def test_epitaxy_coherent_field_is_anchored_to_deep_bulk(self): + top, bottom = self.make_epitaxy_cells() + width = 30.0 / bottom.a[2] + interface = CTRfilm.EpitaxyInterface( + top, + bottom, + profile=SkellamProfile( + width, + tail_probability=1e-4, + ), + ) + interface.basis[:] = [width, 0.0, 1.0, 0.0] + interface.createInterfaceCells() + + lower_layer = interface.bottom_layers[0] + split = len(lower_layer.coherentDomainMatrix) // 2 + addition = lower_layer.coherentDomainMatrix[0] + fixed_bulk = lower_layer.coherentDomainMatrix[split] + layer_id = interface.layer_order[0] + layer_origin = interface.uc_bottom.layerpos[layer_id] + addition_origin = ( + addition[2, 2] * layer_origin + addition[2, 3] + ) * lower_layer.a[2] + fixed_origin = ( + fixed_bulk[2, 2] * layer_origin + fixed_bulk[2, 3] + ) * lower_layer.a[2] + + self.assertAlmostEqual(addition_origin, fixed_origin) + self.assertNotEqual(interface._coherence_displacement, 0.0) + self.assertAlmostEqual( + interface.stacking_loc_absolute, + interface.loc_absolute + interface._coherence_displacement, + ) + + top_zero, bottom_zero = self.make_epitaxy_cells() + incoherent = CTRfilm.EpitaxyInterface( + top_zero, + bottom_zero, + profile=SkellamProfile( + width, + tail_probability=1e-4, + ), + ) + incoherent.basis[:] = [width, 0.0, 0.0, 0.0] + incoherent.createInterfaceCells() + self.assertAlmostEqual( + interface._coherence_displacement, + interface.height_absolute - incoherent.height_absolute, + ) + def test_epitaxy_coherence_interpolates_generated_positions(self): incoherent = self.make_epitaxy_interface(coherence=0.0) halfway = self.make_epitaxy_interface(coherence=0.5) @@ -2142,27 +2193,34 @@ def test_epitaxy_split_bulk_domains_feed_all_profile_paths(self): # code path changed behavior, not necessarily that it is now wrong. np.testing.assert_allclose( structure_factor, - [10.959877214261 + 24.053170411198j, 0.769317301749 - 1.23830767953j], + [ + 12.235092835903 + 23.933759897760j, + -1.787823797289 - 0.028488983072j, + ], rtol=1e-8, ) np.testing.assert_allclose( density[[0, 100, 200, 300, 400]], [ 0.0, - -1.130402540575 - 0.006963145549j, - 0.6212434003285 + 0.002480857561j, - 1.108345606774e-04, + -3.115325591995e-03 - 1.894807083594e-05j, + 4.311764632911e-01 + 2.127681623261e-07j, + 1.019686887022e-05, 0.0, ], atol=1e-9, ) - np.testing.assert_allclose(np.sum(density), 40.00674724814 + 0.031011485600j, rtol=1e-8) + np.testing.assert_allclose( + np.sum(density), + 40.017177763177 + 0.032567796562j, + rtol=1e-8, + ) mid_row = optical_profile.shape[0] // 2 np.testing.assert_allclose( optical_profile[[0, mid_row, -1]], [ [-12.0, -9.65554085169e-10, -8.98126154061e-13], - [0.6, 2.222695401541e-06, 2.067477009624e-09], + [-0.441200041632, 3.042840635292e-06, 2.830348707724e-09], [10.0, 0.0, 0.0], ], atol=1e-9, @@ -2191,7 +2249,8 @@ def test_legacy_xtal_uses_corrected_interface_support(self): interface.stacking_height_absolute, interface.height_absolute ) self.assertAlmostEqual( - interface.stacking_loc_absolute, interface.loc_absolute + interface.stacking_loc_absolute, + interface.loc_absolute + interface._coherence_displacement, ) self.assertIsInstance(interface.profile, SkellamProfile) self.assertNotIn("support_cursor", interface.toStr()) @@ -2200,11 +2259,12 @@ def test_legacy_xtal_uses_corrected_interface_support(self): # fixture (Film + EpitaxyInterface + SkellamProfile). PRELIMINARY, # UNTESTED reference values: computed with this module's own # CTRcalc.SXRDCrystal.F() after the interfacial-strain fix in - # EpitaxyInterface (commit beee738) and the F_bulk lattice-sum - # denominator fix, but not independently verified against real data - # or an analytic result -- unlike test_simple_unitcell_bulk_structure_ - # factor_matches_reference below, which does compare against real - # fit-derived intensities for the un-layered unit-cell + bulk case. + # EpitaxyInterface (commit beee738), the F_bulk lattice-sum + # denominator fix, and the deep-bulk strain-field anchoring fix, but + # not independently verified against real data or an analytic result + # -- unlike test_simple_unitcell_bulk_structure_factor_matches_ + # reference below, which does compare against real fit-derived + # intensities for the un-layered unit-cell + bulk case. # These numbers disagree with the pre-existing CTRs_reference.dat # baseline used by the (deliberately still-failing) # test_legacy_xtal_reconstructs_reference_interface below; which of @@ -2227,13 +2287,13 @@ def test_legacy_xtal_uses_corrected_interface_support(self): expected = np.array( [ 17238.5440045948, - 7001.2889938137, - 3852.1662569049, - 2726.8273234247, - 2175.489988698, - 1855.6217274457, - 1648.0075146735, - 1501.0723678209, + 7001.3340402313, + 3852.2553000918, + 2726.9468772925, + 2175.6251991885, + 1855.7582644482, + 1648.1329478054, + 1501.1768061163, ] ) np.testing.assert_allclose(calculated, expected, rtol=2e-5, atol=2e-5) From b595e0ebaead5e16eec0c3a71569cc91cb621d58 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Wed, 29 Jul 2026 01:06:12 -0400 Subject: [PATCH 32/37] feat(phys): extend epitaxy interface strain coupling Correct distributed bulk replacement and add strain_coupling and offset controls for transitions between independent and fully strain-coupled lattices. Propagate the resulting geometry through films, stacked components, structure factors, density profiles, serialization, and fitting. Preserve v1.5.0 compatibility by loading legacy two-parameter text and HDF5 records with strain_coupling=1 and offset=0. Document the model with equations, interpretation guidance, RuO2/TiO2 density plots, and an executable CTR example notebook. --- ..._epitaxy_strain_coupling_offset_density.py | 358 +++++++++++++ ...epitaxy_strain_coupling_offset_density.png | Bin 0 -> 664223 bytes doc/source/ctr_structure_factors.rst | 224 +++++++- examples/CTR/RuO2_TiO2_Poisson_etching.xpr | 4 +- examples/CTR/RuO2_TiO2_Poisson_etching.xtal | 4 +- ..._TiO2_epitaxy_strain_coupling_offset.ipynb | 341 ++++++++++++ .../CTR/test/0001_fit_2V036_reference.xtal | 4 +- orgui/datautils/xrayutils/CTRfilm.py | 229 ++++++-- .../datautils/xrayutils/test/test_CTRcalc.py | 491 +++++++++++------- .../testdata/0001_fit_2V036_reference.xtal | 4 +- .../testdata/RuO2_TiO2_Poisson_etching.xpr | 4 +- .../testdata/RuO2_TiO2_Poisson_etching.xtal | 4 +- 12 files changed, 1385 insertions(+), 282 deletions(-) create mode 100644 doc/plot_epitaxy_strain_coupling_offset_density.py create mode 100644 doc/source/_static/epitaxy_strain_coupling_offset_density.png create mode 100644 examples/CTR/RuO2_TiO2_epitaxy_strain_coupling_offset.ipynb diff --git a/doc/plot_epitaxy_strain_coupling_offset_density.py b/doc/plot_epitaxy_strain_coupling_offset_density.py new file mode 100644 index 0000000..4b8a269 --- /dev/null +++ b/doc/plot_epitaxy_strain_coupling_offset_density.py @@ -0,0 +1,358 @@ +"""Generate the RuO2/TiO2 strain-coupling documentation figure.""" + +from __future__ import annotations + +import copy +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + +from orgui.datautils.xrayutils import CTRcalc + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +MODEL_PATH = ( + REPOSITORY_ROOT + / "examples" + / "CTR" + / "RuO2_TiO2_Poisson_etching.xtal" +) +OUTPUT_PATH = ( + REPOSITORY_ROOT + / "doc" + / "source" + / "_static" + / "epitaxy_strain_coupling_offset_density.png" +) + +FILM_THICKNESS_ANGSTROM = 150.0 +INTERFACE_WIDTH_ANGSTROM = 10.0 +TAIL_PROBABILITY = 1e-4 +OFFSETS = (0.0, 0.02, 0.17) +STRAIN_COUPLINGS = (0.0, 0.5, 1.0) +Z = np.linspace(-52.0, 60.0, 6500) + +COLORS = { + "total": "#3f454c", + "bottom": "#2878b5", + "top": "#e1812c", + "displacement": "#6f4c9b", +} + + +def _prepare_template(): + template = CTRcalc.SXRDCrystal.fromFile(MODEL_PATH) + interface = template["TiO2toRuO2"] + film = template["RuO2"] + width_cells = INTERFACE_WIDTH_ANGSTROM / interface.uc_bottom.a[2] + layer_spacing = film.unitcell.a[2] / len(film.layers) + film_layers = int(round(FILM_THICKNESS_ANGSTROM / layer_spacing)) + return template, width_cells, film_layers + + +def _make_model( + template, + width_cells, + film_layers, + strain_coupling, + offset, +): + bulk = copy.deepcopy(template["bulk"]) + interface = copy.deepcopy(template["TiO2toRuO2"]) + film = copy.deepcopy(template["RuO2"]) + + interface.profile = CTRcalc.SkellamProfile( + width=width_cells, + asymmetry=0.0, + tail_probability=TAIL_PROBABILITY, + ) + interface.basis[:] = [width_cells, 0.0, strain_coupling, offset] + interface.basis_0[:] = interface.basis + + film.basis[0] = film_layers + film.basis_0[:] = film.basis + + model = CTRcalc.SXRDCrystal( + bulk, + interface, + film, + stacking=np.array([1, 2]), + atten=template.atten, + ) + model.apply_stacking() + return model, bulk, interface, film + + +def _densities(bulk, interface, film): + bottom = np.real(bulk.zDensity_G_asbulk(Z, 0, 0)) + bottom += sum( + np.real(layer.zDensity_G(Z, 0, 0)) + for layer in interface.bottom_layers + ) + top = sum( + np.real(layer.zDensity_G(Z, 0, 0)) + for layer in interface.top_layers + ) + top += np.real(film.zDensity_G(Z, 0, 0)) + return bottom, top, bottom + top + + +def _shared_displacement(reference, shifted, c_bulk): + """Return lower-material addition positions and offset displacements.""" + positions = [] + displacements = [] + occupancies = [] + for base_layer, shifted_layer in zip( + reference.bottom_layers, + shifted.bottom_layers, + ): + domain_occupancy = np.asarray( + base_layer.coherentDomainOccupancy, + dtype=float, + ) + for index in np.flatnonzero(domain_occupancy > 0.0): + base_matrix = base_layer.coherentDomainMatrix[index] + shifted_matrix = shifted_layer.coherentDomainMatrix[index] + positions.append(base_matrix[2, 3] * c_bulk) + displacements.append( + (shifted_matrix[2, 3] - base_matrix[2, 3]) * c_bulk + ) + occupancies.append(domain_occupancy[index]) + + positions = np.asarray(positions) + displacements = np.asarray(displacements) + occupancies = np.asarray(occupancies) + order = np.argsort(positions) + return positions[order], displacements[order], occupancies[order] + + +def _annotate_offset(ax, delta): + start_x = -28.0 + end_x = 28.0 + arrow_y = 3.02 + ax.annotate( + "", + xy=(end_x, arrow_y), + xytext=(start_x, arrow_y), + arrowprops={ + "arrowstyle": "->", + "color": COLORS["displacement"], + "linewidth": 1.7, + }, + annotation_clip=False, + ) + ax.text( + (start_x + end_x) / 2.0, + arrow_y + 0.15, + rf"deep bulk $\longrightarrow$ film: " + rf"$\Delta={delta:.2f}\,\mathrm{{\AA}}$", + color=COLORS["displacement"], + ha="center", + va="bottom", + fontsize=9, + ) + + +def _plot(): + template, width_cells, film_layers = _prepare_template() + c_bulk = template["bulk"].a[2] + data = {} + interfaces = {} + + for offset in OFFSETS: + for strain_coupling in STRAIN_COUPLINGS: + _, bulk, interface, film = _make_model( + template, + width_cells, + film_layers, + strain_coupling, + offset, + ) + data[offset, strain_coupling] = _densities( + bulk, + interface, + film, + ) + interfaces[offset, strain_coupling] = interface + + density_max = max( + np.max(total) + for bottom, top, total in data.values() + ) + density_scale = 0.82 / density_max + row_baselines = dict(zip(STRAIN_COUPLINGS, (0.0, 1.0, 2.0))) + + figure = plt.figure(figsize=(18.0, 8.2), layout="constrained") + grid = figure.add_gridspec(2, 3, height_ratios=(4.4, 1.15)) + density_axes = [] + displacement_axes = [] + + for column, offset in enumerate(OFFSETS): + density_ax = figure.add_subplot(grid[0, column]) + displacement_ax = figure.add_subplot( + grid[1, column], + sharex=density_ax, + ) + density_axes.append(density_ax) + displacement_axes.append(displacement_ax) + + for strain_coupling in STRAIN_COUPLINGS: + bottom, top, total = data[offset, strain_coupling] + baseline = row_baselines[strain_coupling] + density_ax.plot( + Z, + baseline + density_scale * total, + color=COLORS["total"], + linewidth=1.45, + label=( + "total" + if column == 0 and strain_coupling == 0.0 + else None + ), + zorder=3, + ) + density_ax.plot( + Z, + baseline + density_scale * bottom, + color=COLORS["bottom"], + linewidth=1.0, + alpha=0.92, + label=( + r"TiO$_2$ contribution" + if column == 0 and strain_coupling == 0.0 + else None + ), + zorder=2, + ) + density_ax.plot( + Z, + baseline + density_scale * top, + color=COLORS["top"], + linewidth=1.0, + alpha=0.92, + label=( + r"RuO$_2$ contribution" + if column == 0 and strain_coupling == 0.0 + else None + ), + zorder=2, + ) + density_ax.text( + -50.0, + baseline + 0.09, + rf"$\kappa={strain_coupling:g}$", + ha="left", + va="bottom", + fontsize=10, + bbox={ + "boxstyle": "round,pad=0.18", + "facecolor": "white", + "edgecolor": "none", + "alpha": 0.88, + }, + zorder=5, + ) + + delta = offset * c_bulk + _annotate_offset(density_ax, delta) + density_ax.axvline( + 0.0, + color="#777777", + linestyle=":", + linewidth=0.9, + zorder=1, + ) + density_ax.set_xlim(Z[0], Z[-1]) + density_ax.set_ylim(-0.08, 3.38) + density_ax.set_yticks([0.0, 1.0, 2.0]) + density_ax.set_yticklabels([]) + density_ax.set_title( + rf"$o={offset:g}$" + + "\n" + + rf"$\Delta=o\,c_{{\mathrm{{TiO_2}}}}={delta:.2f}\,$Å", + fontsize=12, + ) + density_ax.grid(axis="x", alpha=0.15) + density_ax.tick_params(axis="x", labelbottom=False) + + for strain_coupling in STRAIN_COUPLINGS: + reference = interfaces[0.0, strain_coupling] + shifted = interfaces[offset, strain_coupling] + positions, displacement, occupancy = _shared_displacement( + reference, + shifted, + c_bulk, + ) + visible = occupancy >= TAIL_PROBABILITY + displacement_ax.plot( + positions[visible], + displacement[visible], + marker="o", + markersize=2.7, + linewidth=1.2, + color=COLORS["displacement"], + alpha=0.35 + 0.6 * strain_coupling, + label=( + rf"$\kappa={strain_coupling:g}$" + if column == 2 + else None + ), + ) + plateau = strain_coupling * delta + displacement_ax.text( + 58.0, + plateau + 0.025, + rf"${plateau:.2f}$", + color=COLORS["displacement"], + ha="right", + va="bottom", + fontsize=8.5, + ) + + displacement_ax.axhline(0.0, color="#777777", linewidth=0.7) + displacement_ax.axvline( + 0.0, + color="#777777", + linestyle=":", + linewidth=0.9, + ) + displacement_ax.set_ylim(-0.06, 1.24) + displacement_ax.set_xlabel(r"$z$ / Å") + displacement_ax.grid(alpha=0.15) + + density_axes[0].set_ylabel( + r"Vertically separated $\rho_{00}(z)$" + "\n" + r"(common density scale)" + ) + displacement_axes[0].set_ylabel( + r"Shared offset" + "\n" + r"$u_C(z)$ / Å" + ) + density_axes[0].legend( + loc="upper right", + frameon=False, + fontsize=9, + ) + displacement_axes[-1].legend( + loc="upper left", + frameon=False, + fontsize=8.5, + ) + figure.suptitle( + r"RuO$_2$/TiO$_2$: density and occupancy-mediated " + r"interface expansion" + "\n" + r"15 nm RuO$_2$ film; 1 nm interface width", + fontsize=15, + ) + + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + figure.savefig(OUTPUT_PATH, dpi=180, bbox_inches="tight") + plt.close(figure) + + +if __name__ == "__main__": + _plot() diff --git a/doc/source/_static/epitaxy_strain_coupling_offset_density.png b/doc/source/_static/epitaxy_strain_coupling_offset_density.png new file mode 100644 index 0000000000000000000000000000000000000000..f07f08c60f502ef00fff49e35d27468768fbfed4 GIT binary patch literal 664223 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

>kjm@*)3PBQKF3$|qiVRg&1OhffyD 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�~oiVW3D+2S5JlPw!){X*9!wT zf2ZqcJ#E?=j_XHRU!ACd$^TqTCtp%s_AM%w1JA*#wuaRojt`XZ+s}>z8vY(uNA4+a zLk_W0|KzC#BaK}UO^V@Gl!|5X5Y2X{@)Zk0zgnd0G%*~$$GZ7&hI}+0PhUMj>T)QG zoN`d*2$ekaR@5Zw99q0+3rDUK?F(Zc#m9&D`gVU&*$FRT{e9MNl^PQmhS^j(a@HHH~)%l~kT+v5LY*GBH@ZDmz$E(UZN zz`iGio%sYuC0+T=5NsSgf;bXe0RBLXF);B4lR(Nt6wh2yGa30GtTmsLi+jV*p?=a3 zyV}3RnmStD)pfcNs|xo4H`G@$@m*keTCdxSEwWViZ_U+iyE52&-Wky!*V6X$KmISf z4Y!8A>$IB0tEqV6`2P1F{39BHT7gWSCz@tl!FKy|Ve3!CPkFcBhmuda2@%AT!o6X~ zHnWv#P)rIHy*dGf=SkaM=m;V^~2oCtVji)OrmhTma zk>=+{_icR%_7+aBn60wHRoq0W=$Vs4Sr8dBGdlLT=HXPzFUyZ_85kgIk$jiUvQ8C% zFj-GvYRX!#8Zmc&P_xh3zK`CrxTG!aKZN^(HtO_1@;@i`mb`@DWKP2eOtveHZpNpU zKiyC&E+6cb>LC%AuY^u(w_KsIf#kbk;>I1toLy1p8JOdl_;B=@?WVu3rx?MN79E|b za`OgTMR8J%KF4ij04+B5@>0+tk3vDO(}^>eD#obQgVKN5wh><_b3OTU|GBOPG(B?| ztH{^P@U3%LOG)}Gl;0at9UjuFv;Mgs1-Dl+EOvIQ(E?*U85G@fo2&)r^$jm1a;_-$ z&=^Og@yR2|bnj}OIvrJ7OEuUTE1_8ep{Ljp-S3~GOk%yatxMVDF_-F^IH9MX-Rhc> zbI*V1a#%rDGYfQ-6K#t^8B5h0_dg$3T308MZWU@gMLkhxVx}RK+D+NN6C^>`^phCG zoK}UL5k5@NJ|;|D2I^P&U6%iRe!Bl(+xG1XP2>w^4M^gyJ((Z(BYnx!dAZTyK#|&O zMsrTS=&|tme~9b74;=;**b)iJw0AqmllCJ`iO+sSi?}G9 z>fA|LB9X$RTh+Ri8y2*IG>Pl;uTv#e$8Apih0$NI|C2gEklY;9V2AfB$GkpV$2*y4O{2tGXbC~c>EVm;%r~C{-|H*@81YaMwcli@6w!!WCd#4 zk9;H%er5da=a%sL_GgU6$Qv?sb^l0pG_Rsv_wlyyzvai4*v4ucnP{8TvheVB2gglx z^kBKIOP`^Qra|HmVr7ngd5Y!OOpaL&H7#Vs!0iArm&Nyd_<%IUOvNTh{`JDWUrGiq zC-s*LujNd1bU+cDwDVq><8vn1G@MPobTx<0RF7dK`qpr2HZXx1dYbdbTrgsyFM%f4 zjvCog@yVgAoN99on^*isEl|On!fzj>(b?mFo7ZiJJtj-O8g!dJ;9}?@WmX;l!;a&~ zJXsT_VahJpGz8+~1qje2yp?5{)iXC{j7dqG3gT*;#vb4&-7KdkW z3^4)YfSKAI44!sAA%H%(ko9(3y>9ZofaIx~Z?3(|_AKYy9>u?(+x5dza2KovT^>@$ z5+6s0?$nTe3GWWe7o{ z=UL`~FOD1DKX*Y*GyAY9i);`Q!NuK3sEcgHfRa8L;hU_gFpQtqv_;?3JaqVK;rysy zjB4uZNjF)ZU=k_X}uTCt9w?(<4aY_ue zWM7d@%K0u+Hix7Ff|otYCe66iegZ%$ZUBnT#-bjAqUL@7Gt{ia?X?W&=|><7WLvXr z`S$v1PxAY}DXth4i@QvmxKhI^0${MHc9D`a02f(_3@EPmb?_R%Xjoy&JxI0ydf!iD zTgCDe+-!pz-Q7`oe-QJMsbX+8M4lCULl`6a$H|r}S@&FB1eLVQ;>QvGD!1fYSMv0n zJ{V2?Q81g0Ez{v*84NK8zZP36%*Sr_{k<%D@XqioeS8yQDjFbeq@!;hh=IY{J{6c{ zj@aE_dH$eWh)1?-Qd%1#He&GU8VC{0VN8fzj5fngKA#lHXp1f((n=zo7TEXjS)#iC zGB8C1apMdMWGf9ywbwtH`~W%r_D}fX3r#VEfT|ei=*wShE z!OHtM1&ZaYA?l1$7tIf z<=CZ~?t=$K@`GZ5SZbwwr0W^=XqoD zW$iza2iEIX1p}^9SmGf-VPETltS41e_kAEn;nx{2G>GgTZh~G5Bw{buTz9J~cu>qF z$_tlg(O&;+oLWw^BY;Es4?n6t@2odB0z7o87ZBpnf(&naPQTg(Wld0O-9MVh%2Xl@ za>*FeZq=OJU7wMB^1{!0{&zI4q2W2bYVw-NJ?sxvyK07j9;tOMRgL5)<}xgV|M`RW zEz~}LHS=TM)mecbn1rZ01~Ck#HBt9rdDzI2;*=JcL)yyI*^_2i=vSuFo_)T!UlOol zw_0ij`s=ED*7{UO!IbMI?Hn(K*UdobvhNI@f_mTf;$r$z60tn~DlSMH- zR5P-t(rHU>RQO;2`2;YY+Xw;8sxdk&KTOfz?LHJe`7}5c*vf%ZXA_W=> zB!pL*(TYip5&vy$o0Kt0@%F!un6sh!W~2-~%?4LbF4j$MqDdOr6vJ8IKyjnuZ%BftxKWws|Q^k^H+?f zGs;En*xN@VGK4QCh&MC%~)Wf@Be5z=eWB6|NWnA+gs+c z-Qs(-Y};D4t<|!ZmX_Vg*0Ppuob0}@eQv+szuj)9Q>R`}T#xIzU$uJEEr8HA2#52z z7ezNPY2a~knwLil^Gm~DS={lk9O)UGYdNxGAv^1`Fi_Ck&laM1?#ulp?(%i?Cz_TKXUY5MS}F&P%h zr{=8;18}os-&j$0+3Pjhg{dp{k{Sr)-tnurn)1nQApSK22B|kj}O_V zF*OXk@X9Oi@A$mVZw-fl^jgQl>cs;K?rW?KFp^(D`P!$h6SN$+8QZG)ff7c0k~{}>fjw4 z(eD54TShe!^0w-LB1wv&@O?!{)dYvUZ#M#0AtL%AVbbrn1czpY`ev}tfjomtcw%YO zzgAKmGLKc~L-+QFVHpu@vzO&%ZSY-V^#J#xUdZF4*Z*gJu!)K$*jBZ|zaPB6P>kE4 zlrg4oO>vYJ<>)b7#&Z}H*>`h@1p#x2pdF|o!daR>Mwpa(oAW}(V1sH)bvgHTvQZ&7 zeO_y*QRt944ygRAy_%n?ZBFvYuomGUDFQG(tQJyF%zpW?VfIfl*8isLDr1kHIhpNd z0K_0}Gxml_RF9F8Q=k=*-i_Zvf%N&}{J}aeW6FMs@PCuTL8@R$BBNzt6=!HtGk;#` zi6|yXcJUzQ?*+e?k)L0_&UK;8HiR*3dxC0O02Mryw^4U=zq<3W0|L=l5Ab>PmrXfn zWr^reg;7E)T*k3C3pGD;+%-pdk1c~Rk zw>!fL3#4Gndip2yaiDtWO3YkwX=pj`fYe=5i4{*)WC}O>qO*%+(pFTFK&;HroD*nQw<#ZhdGO)kH~?z0R8VyF}E<`6jnG86vGUEQ&cQ)osRU(rS5 z_IB^f!^Xcz%@9B{+At+%=cL@4ip_vvI{+U;lqpQs@M+;Pmk}23H5vkb59TMn!TKm3 z9-WwYKTPW%@*9+T-S$F%>1ak<76xN+NeI~UQZKi=c?q>(Yo2cPnpdtZ%BPMhX|@I? zJsx!yes?akR9PxA#bE*S8ABwIke+VFUE9WzZ$9U!rsQ}LfG4cTI_U(;kB%N+@(I2N zWj33OEX$?xMy}I&6#C5+h=4@BFQThfv!koj+1|P)1ba#9hhlfG_*gMax ztfw9(o*;Lfyak=8%ZIP0{)ohyNQ@%u^gd7UKFgkFrotCt5v!&>|B%ZY_S}gD6}?Gw z&73+V+NF_69I7JJ^QAone^s=DsYhW1_P_cqpkme*5Oc+waVz#PfZ5; zZWUb0MzauXKOUEJDc(f{aL}or_Sv7;u(3#IWu3IrgYClze5z%nv`>--*k4It!UT!=!FX@es}53XdEz z6#p=U^1@0yAtj=ed2Lrv{IAe%is^vwH~ia|ZScl=ovBlpx@e!&24BUu_@hYbLVeYe5B+8( zo7c-0y)~sBl~6d$n3zJ?y8>$tRe2ei#Qyid9L(?x9=UuOu_+ zK{;ZL^Au7`Y7K}J04J>-p$^{(H`H8W#IY1J$fk6>6Yrq}XuVbB?qasj>A;@e3!hk);{tw_luWBUAq>{bp6$%Gw7$F}JBY3RfDeD(e9A3xglh0+ zZJI6E9O#NNti>?HZj3PrPR=bz=WO6k*L*OTJy0p#QO+y6>;-3;?=Hg9z6>#u7n&wp zPhZY6t*c>vN#Cc3yy*NkhK^=DH2p>-?UJ0nV6)9pYW=(KeitVp!)|?qOUu}_M#|Z& zqPyAHq#&eI)Okj_93Noy-3PqidtUv>i)9{jx|x>QQBLs2{VMWk2Hrw*?9SSFu{e1L z5DI(HIt#q}K-Cvykhc>ng7h1rg{9^H%6VokuWTy0 zLg5P+O!FQGOj-!{j$83`*_QHEB6^5vt)wNovnDosH$={AnSlv|7DH3tGQ$~*>J zxFMuIPqh!63RqIg=-&-)_+;vtKg%rllky+RmxW?nVAa7Zr+!m-4jE@9VkZzJMH6q9 zV8qna8d9iA0w^u&Rg6CfLxVm8F8r9!@&XvvX|p0@zh_##F1ilBnNrkQP@qD?v&q*s zroi&#|NRBfORQN2Rt_xsOXm~-u)q=N6@8E~+3Ne(H@8XCmMF=qvQ^xeqa|ElZHxn@X$nkx3R(+X3WPce1N zt7iyn*nVzpO8zoREG**{zeqbC%OEy=QOyui$ZPwmuetPhcGiNoKWg~YbfyLonr9Q& z60`rl>VThR%UpSZde%*WzpNBTo&zZubMcgPF{rCdNUq?^VL?cq;yuVeO#3z^%z^0pqbE?w9S^7>E04|pn4eWG+sM8^1Mq-q z-qRdfoNiOnLW4ZC1IQ<+MlFfphK5k!Tb=xmp7gIc;9=P7z|`Gh z@L{9c0t`s(Kw{K?VeuBBaV#!XygOa8=)%1%@bP9*zls$B*dUKU|AE|TnZ48S(<)t} z*-I`au<|_>3i-slc{_gg9X+d?)q2L}9&$JNzm5~g3QBz+KFHQ4qaSef(}uUz9nng! z+e|bna5ReVCcY)r&k8wVaD%bTrZ@e7H1N00bZv)0vcq-xEDGu7h(xSJ?L##ZtH96V zdB#7=lV1gZT+*Fc&zOx<2X#7~e8+AB#4m(Gfd3iyRLvf$j*1Bps&kNPA2GrWo zxNwZvUdRB~+BE2H>^u}1_K6ndNHU>_ANObMP$8m0ve)17nx$QVnu_a&`SF^m$mwX} z!@CF!oec?w*UZMhv(kz6ZA5K}jOo1c3d+y<2GtQkS2WxPGEBXmc_blXZ=^D?Hw@-+ zol8e1Y=rsCH#>^Is#bPNqb%yJKYYMkoIc}u%G{yw5IS$`5R){_eePQ~yT9_xz39u{ zu~3$;5yJ&jk9DlysMRj6G`UrzL0qAG*@V@;ctWh1&1Chgcl}~tX#Wsr$NZoc2S(YV zgdzdHZ4JlI+=ZRm3Q~6+8hUyvcLLJs8CQC`$EXS>#7|)c7oO$LDz;|X{Wx+gR+3fj z?fRGjPXkoZwCr%fRK}C#RhN6Gvw(&r0*RY9!UBfbN1^v2z1Ld5Z;i03Om1qc8bu2U zoP{TW1-olwl7{7hzL^NVP|+jqfMO$KPSH&uCs@h7aBtU`EtUbO4beM!$22`|^jV)= zpVA4n@9Z2~1d=}H!|a4hWNVfsx=YjAt>~Swte=!;PFL|KB!58clw|)>8Hgu=2t_!3 zci;gpCIJc5)ds_Fc8yvS-%a7hX(IvN;w%?nWibfD>UnW++`)@Hob9t6k)KfR;*`;X zbIH?M)1)XC#JeQ?mRA%|Wq_s;yPqR;9W7CJc;&a`P~2S=wg(Z~QQ*J+v-fP!lQn0s zA(JsKh|*Cd)8}xzylTgVIpmBU0{3E|inCr6VEMGWgpaDI^Sk%}0$ADa+rKg3%$4WB(!%YBm#y zUsw0U{`@NPuZw2Y_j+Q>mfBbecS_C`{|1KA!60nZ;{)CucRM?^Zc>tM$(hTp{g5LyUNPS zNUf8_?MC+4Gl%)J@4SVpNsh#v&nKxLEitkJ*{%AO#y^*Cqk^nx?IlBA_*Ld#tfLte zE_~!T{gL-#qpjaasC(I=XSao^R3OfhX2g275-D%Kco*E{t%yiN9)PZCXAPB zE21BOHkpC%7LOkmEznvIM96{q)WFCJ8>kp9{onWz|N1c@Z=912@qlk|G4}J}kXj8H zXi2IiO;4EgGyU|*J-br(8nxUvVTwXU%|)nx!AvFJDlF+@98I7O*yE|k3H-v|Y?slu z=5ayzF5g-*S}*!lrEOE{-C?E99KHEqJz-bG1(W6rT=*c<^*I$2e~*`sp!Pwh(oqjK5zYce8j?o|oHR@7g|uV%x{q z)XC0)lHq%>emG{$ch$j+e_ z^>PaJY#9uPW=7TN8BmLG;gcDU=3Bh#$=tm)$(Cgo{B1?PUGd``b87kGsXqa$Fco6O zNzC2SWk?!ddp_m{F`EB&As2=)@6% z;=K>*+o(+#!eoIupGJCp4(5>v)7Uczz?{609zjA;vp?ysJZL8BL(tztPFMpc_20F z6~qx0KwDYxJ8G#T)5Ypy!%E-)M{w%ieiq25;{2v2zvpb5@}nv?r79#BNxMAJmN&u% z<6f2w&^SK#AB(&p;v}B__CnJPk`Q;tXsC*{*|x<1OTrW2x} z`}EP$!lKr8@hgb>XJ}Bq(?!R9T!4jSa=GD$`uh5o`&^$v(J$0s0F}O#OXh*Gfzs#J zCl;wh;2z6`-ME~dGY`vZrm1d3_*noSjQL0L9ktnhIq7*B2#clX)_#juo#o`e%>0mic|9Jm3+9yyCTfzo-;R+ZJFxy?l z3rJFWuO#PgL6+^;z>5R0U&xFWCex1(IKMR*M;FaaJX;h7amhtPlWbe^%{!1}y9z=cti|q-jEti_XHBsJIACxzN@^bm$J+M63@NuIvb9l)i6Ph_(DGxR zd*$5s%WZCVEyX08W}pBL$b+ueeQs3LOa>UfaL$95&12PAgG@#g$oPro6$UWCvefdh z9pk)55Wy#DEaX&!c-z~kWAECY!_LsVH`Tm8BMxEFZTi=yuInFXi_K2zh~D)uu(p%O z?^l0C>Y(^@@gk1B7%nI@t(8R=u;1!;*}COF$Zyn{fB6b1-tb9WaGBG?m3b1~Y&oXq zZYJfhAo=E%K0xI+p#xJ-Fk8w)Va_PtOzc{ua=u)~p&XwKX>fk-8o}9xesr;uB7%>^ zl8Q?S)t!8Zc#F-=%w@5Q^}*XFnGu=|JuuNZ%t`YBTNf){z6z&&!O%CD#FtmVCwKui z1@<0}w;nFubY4tMS53p}j!OTHX5BtCJ1P0i9}RWHFz6fbROJj4H#c_Y??GD}w$|2V zV$9l(Wwx=?uLe%tH(v3KP{#bq(ge~E9VJo#r|9I8>5xId7g~%dPIJF>NX<-jl^zyf-;UAL&!KSi=I5uPtAoU-p0DTd+5vJd6(mGL#lAnfpI7RLjPeV zZrCz{?}zusJpxhNngbN4wj&Ee@7DBd)vv~<0n_O08p_Ur{(xvgM4x!*&+!Ygm)k!I zi>rSi&|rZd>fNhtKfA5iHyZ<01<$ATll@(5z&M&sSU4{4WtR^6LcU22bGzfhkM+Fk zf_1B=k6iVkeXZSNC*3q&9O&7sei^OaL=b)G5f~bKU$x_DSk%O5cmZS>UeIQlqF=dk zi|fCXLLdw}2$b!yE)%}Py&$@7^M!6^+&+WkJ!|V*x#)PaP1ZeXPYmPN^sKD04rw{6 zX;As{a>@j4{DUM+^^HprVwy(0c1sCrQWOuXg=Oot8inJ@!i&(Y!l|K-&gk04nLA0G zY+Th%fP(r|HJQ1Ubyc|t?|k64iSL)pgrjTW+;uke0^GzaRLk~3^h_koJ%UI7HTLTR z569-9vCemtzn%kO5U1%pS|-r13m?NZD>7c*|2_*1b2!!xiM?she6>ewk#Xyo2~1dp zDQR%UtPUpVvuImaV)1gzu4z#(NB=c0nK8ZJam=+d-O<|OC2Du$#!FV6b;RE@HXKHS zLHV>QuZbjfrV%V%XpH~mfw=lITiZFC@I7vb-v%aX?l~7J`zsOY*Y=K1&4ZYDMo$30 zE%33IPFu0#LiI~8%Gho5G@$?*!CI%VNHbbiR=4a+>kBWC(=)f7bG~qH0Mi;#pcTJw!NuEB#ddwGg!L8s5 zii_7fn*KfBbw~I< zbr9RdFJLG;$|xcu1N+ogu1;mw-5*8Rc;sIj`7&7^)T5&mbZdfzh8B|_>7AUJH6f_Z z-j7ZDV;o|eyO=Bnc{Z)K_xeQzk5Rbd3}Jh@a6Fmp=P!0FAylH1<0*cfi#TSxMfIRl(X!yiX-(Cfw-s}9EHL@oWmXoqvg`yY z#7(%q*pfc zE+t$am)N{ihZub`v2{@uals?D6s0kB(hsTM44Ijq+Am@#Yqqj8GH1?x5*F3;Lt^h9 z+&=X=Bv2NRuA80hd9}DR*%R?uL5I_ojZSY9sRNSBrg=gs;D7yBy(0=-#>#eXo?p3J zDS5IKsxhW#lCL`6axQ#HK$pNg@!P}9RoYuZz>BF6BucS=NCe=V{;OLV)$9z85PjJq zp}-WYuNBER_xC3^e40rEfhIi%Ib#E6t++04eu3g0YY}DR<6j7ZOiY z_DB&7H?b++8dd7Grs-IXZg}|N^7!Rwjt0O#)7`9F{FH;s%`r~98k*MooS@Pl{TS=f z8*%scJOLYGvHOx0myF(^@88$K_gb#o2od7hOpW_bLkGayd^U8xBIdSN6;NI|;V-WR z7g{iOWt`V%sKBuTG%jF)Y9_8}3Xy=wvC~bdD7{RxT zxz{+(i5>(!CP|QA2_K`yiq1}|*zNn7-vXe9E((9WvVF_wv}a1Y`FVe&yARNvp-6^< zZdv-H$MAKHU~u8+JAWwOPS$+z{D^5iXBhz4kSA54cn!#6F5@zeb`%sEfdYkV=?>l* z#PsN4tjJDFjd*RNY) z`v&BFw6n5;ijE=M@_QOiD&D@PC*i$|bwX75$D1kkw;rmdvb?REIjRPiBD}ng%1@p|86-eOL&8Q>lR>Inljf|fjBusH-(GB%ILQx%$#cpR z7k3$;ECS?)6_z5oBwZZVKNkz{^=Pq{AAi#WAHP>fZnQkU`EsNlocxF-rR^WTwj2@< zHG?juWYf5$;23L5a&~bAr+J5FKw^)tY4h;_$c-j(PZl{i?A{Z+_&>AscWV$i=nB}# zAGG?jJG&IRDR10HfQUJHZU#GJtegYx)?A1V9w4I4`&ms{i+-v61rw3e4 z%{?D49UO}ZoT{>~M&c~c-UYS0S5YTTFq5$3OJ0-bT?<^8dQ;EQ5m6n36O=JT zx$Cd8DNs2aCxgHKDlG1c??(|j6&nyKK|N{?G*sS^h#$i&1P*`$u>HFe3?a+V+;~=h z19`oC7Y=t6wAnu=_$J3kM}2Z3N`FXT93`gF#8QYl@YrL&2I26vT61$dhIJ5d?(LZi z8NG^)-FM!1IKx&WOCNE2p8+yae>|CBI3?Y@usPn&8lV>Oo!0cR2@1xPl`#mN8e{aK z7(PhoIS=3sb}c59L1V4Oi{4(A-bjD@0zAuxrSlU|EZzmE&Ek$+S?M|YP&#d}(BWT? zr+}viM+vwJdJ9Wka-<>X5Q5guz09vg6kM`IW4&~ocGh~`c38d}{VKsMW{Q#CIL~`x zUr`QkN|ii)Hc;a=)m08>xbl30JSn39XP)~r^gmPlT5j=QQj{QVc+D;~t$qB%Tqge8TBojg=59e!- z7B=o)IQdfF_~yk6v_qJx6jciBuRDM9e|@molxM#KecN~p+~BV-#gvy%Yz%Z{Q&mW| z-g^t!bM%Eq1?VUr6dx{+SM8$WHK0uLrl*@At^@{#61;w&ExaEe<@~A`Tr4cyT_?v7 z4N87V2gaded*L$T2aYGwf1#lP!awB;r5l7>m=QwXq%N=KtNrJhfOU!W-Q}aRIa}+y zHY>U@v7<^?qXwyjs72B~P9|Y+03b~(>Oq7p63r;LK;_Y#DhiVQZlrM0i*vlwlA;F6 zlKnI&XgJvF5T1Zs2Ra<<8%W8Dy`)Vqit&2sBA0#UIPVK>)nWf-6VQ(%uL~7O4a{S? zdtwgQ=ehq|CCyYWopjzX`*=(G!vdZwvt@5R=++U~d!8MOS^Ax0xeWM+^vM?+;ClPU zFCTwn_xY7r9Qnxnz8jVgYFREPwPp5Xs9f>8OJ!PgwebpRoVO8|n!xvsMsmfDk^^kX zf_=#v$RO4KG7n6C@fxJrdkX0~YRigTxx@pH@jt)Q?Tx%LEDz}?r+thUNraRDqBa%Iij#FoM-|~L=D-a!&iWvT& zo)8Z3BfgGH&_0~KzRyq;-z!zCpLDNCIho~8UxxLKaMGX8rIu5bjJ6*06ZF`=V%3YP zHgw6+M2{TJ$S~tMJMr;lp(HW9|FW290W50cG+321w}Tq@YaSGx_iGp0hF$L>D=5JP z6nPc)toCD9N^NYP6*+Vim%ZEUb!AL2Ty}3R#R#ey-+pQHzR@vUeQa&3d9Lo}$zx#xFULhu<}m%Dt*gHj zrvhLx=*a?{9-gN#D0#+-W1D!69G8ee;E%@zzbucPgit&r2tqx-Ae`)9mL#%)t^PX& z{Efg{uph)lIx+;Om1zXiM`OxhZOaavd+CMO_EcUm-`dtBQB>oDlUqa{dN8&lsp7kb zMBkiG|2Cok5h8MO;F3u2k0=Aa@Vn^y*YJgw@K{7Aj0Cy_25f%Y8*l#{jKKKF|C%?z zrEe$oxAdZywY4?#o7XPKG>{a1M9jTw__cN3elckMD<@o4Q5eseR5yXUy#9{>N}P1x zLzhNH*y~;U`$ND=^F_+DmyOLRS|;-%u#Sqn-M>9wIKB$rHs-ya{_4B`nfuNt%W{2v z{g2p}Si%6TTmODE--kYmL11>LmhHJ61vjOW(;jER*_@G!27l=O8n{|GBj*OhBZTkk zFTz^^1+F^#`gC0F6aVLgj$7s90xG$g)il%UFPN6N1+J~sIDS;_7Ty8~4G*g~WGUVX zsw~?qV;t4&MPkn64jZ`$)i1yD0M)vFHWKe!5-R~#<2PvBxXYhi=jKA@TYPxtvIVr6 zn~Cx_JLb+PiN~YO4ZHS*MD*B{@XFER{W$rD2a9nffzmKhb>FTt&{dq?^c}#KxIy8H z5IOc+t0l<^hmnx$&d9$&D{u4FX{NqIiG`&rB)H4Rxp_pnag%)0nNuGsN=J^%fT}E* zAaA$Mq<5^2Df(Ny2mr%u)L8}}fE?krSHIN{XL160MpA*n+P*g62;G40cI}TihZ`uB zS0;13?^hpWwp^UFU!>|ZKo&$?j#4e7i5nBbG={fGD_|Nd*`BhryS!{}Vie@M=Pa}* zXmkUxq@2&6{hRl*>k?p#CInxmkV^bk%v{Ic%aER9onfI%6FazUGN(vQeY z7j zW{G$}uf$CMHH$}w2gp;YPn^97C3p~t=EZAi)WyXerXwQogvLCcx3|Syg-btn;IeS- zwAF4-m>=>y%hAu)MtFueSgoFqB@OFinr&H~B^ew=DVghO)sVqpf+Q~((5c1*u3g9J zrvPjG_T)}y9rk-`ruRD=?82=K)1S9{GnHcoma~ zau#F5r-5L9>Fa1ROUA0{c66ras)2hueE5-XNmp3~af&%nzA+Xf-S%N&-X2-ZQs-h5 z>0Qm_`pVFI=lkpZz+T#q+DPt9N4wB}H3C+oa2rPnpc>QO zidMYOvtpo0w0EYd6^3BVL5H3nE>5_kI5*oP;I2IT=G4eHj5jBr+?Z%@on{k~xTLZx zeSi7go-A%FIQj^_UAa{c!BKP#ucR{$)=RbX-dx?bmJfn|j1t7}_p0A#D0~NEQi`T` zEZH##e{LY%2&Y*m>UHCjElmf@IOGlT-ixXVS-;9{BW4F3jD zTK=XJ&mOs60yGMA;7fxKU1d6P!y_B-Z}&r^Hz62r-2Vz4dKi9zkpY?gVcb$ARKVqbiba-%(uZD-uq^35m@7#Ow&3)0fdNCuro}HgC1nNh~Fp(rK z;zw9ni4Td?$w~{<3h2UQzuL}t&L)nBiBKYyX+BzBaOWMOp#p6 zWiLwYx4r*%uf0&w(KGEs5`Z`~s5_hA#c7n`7h-@+im{)+C?#rg3R(9ROx@2;g9mMa zson)-1UQ>%i&TU|wc0NH0U{nsg@&fHVEfR>z&t3H#zScdWP=xf$@CDaEavV5Vb<{^ z{lMNIUlWxu4Q4FVn|jMitb4iP*V^LDC_6x5Zl2SxSPyH-W-9F+gN28ea1xi^lFibz zh$Htr9`x`Yfo`d#AC=p8c@{#`T>m6`DL#7ke6O9HYD^y^P)geEE@}Nwj#>Fll+`ow z8g*!JP^r9}{6lWkH+9n}Z?Q)a041axB}HlVISSOki6MMnY(U||ld|g<>IyfuyNj0# zfs7Z>q=M7b(+5Q9bULfpuCHg3x(6hFekgs7kiZL;J8QF4!mP%IZC`VE=t$P0H+Hk9%x^3`LkdC{1p1LUrbWXE+UU=hld++n3uX+_5V08c4Hr0>CA>dEvBGcl z;&OLW(rXZI4iGicQ<^LqU1U=6f3;lfj!Cr<=lEHaVDteb8Z~G6%cd8?j4zwt!6*Oc|g3wg~SiPrxDm^1Vk)GYgtbRN&9Io_0+ z#8LGZ2ZGa-Zi5NR2sKiI3G!2+NwH9$? zwryl~q~aWIjMXX}{>L9U%6KtL6TxXwCRi`m+qH;7u!dz)5d_C4aGyW^6pVdz!4-;YZ6y5gZQg;Lgf#How6#?WV8v`<)H1z;1wzGxco2pkhX<;Sax|(hfUi+}JHn^8| zodJuJy4IIZ2z^7kyas!DahQ*bR`yGO^GqSb)&cS-2}oXp{AzBFU~!V3$c?1^q6+4_ zk^5QWqKTay>JXU+>DyIW7lHkYgnHY&y%1L zeO6qk0UxXw>le3kXS}xVpA#dOf3QdO|4HGyvjXd5TF+p^Fm#J3nohI%!rpw(XsPBg z=#I>*>sD#uSu0W zW3S&FfWwW0hAxA}={z4B<>lFSrDfV1-`vT7V@*8#y1@&9Xl?WAC}Why&3pv?ER^39 zY18`z3nt$kL=EncH;VqFOe`-oLX`4#Y^95{a;gwIw|{%Vzx-tIKB*S{Y{Ye^%_<1Z zNV6#8($hCSd`LAR*+ua&vzmJLGb(keHnr3bWiBWpBse8P_NstnxQVZJs|Cy)q8!*r z6uy4yVp+0tos&*9;HY$ktvNe&eUg+UNc11Rt@QeB@Szo&~HS^T7 zf4wYZAQ@Xdi;+kAT$dhGEQngf>?sAW@V~}zVki4*_OU-rZEXS|WF_edGe$qCy+2>6 z!%^U1rdCrE3d$I-9)r&+T}Ny<-=G>@-)(#xNtpzll`-G*axwJ8CWw= zIn^A_`Z|}}G$zQ(!NR!FS9j;m?3_F@GBUGb6+2s}J1Mmsot6v8g)#A(MpTf>eCiD3 z+#-79V1L{AeVfn|l`Y)=G%BPB!aD9Ce;=|V)1W|>Iety9%mvi}ARFzMp*_fl&3mLb zA63;I^0?+-8n(>AC^6C!#-4uu$)lmWI=Gfop47NNF1~ONU8K$gtNWaeTKim2wO&r)Q{P~$~5TL6>Ce*gfE_Q zN~@R^Wlyq*bYUI+`2CpkJdBwgJ-XuQnD*nWD`F?x4QadsdGGij<_lTTY;x$I&QEDy zTAH1n6M;;M=r>l0sRQ^`5!iCq8rg2`^|&3@t|t>znb4K3m}I%T?7k1iXKG`-XC$m! zfqs@SAc^sf33HbjPh*P8m#G&I{r;zMrS62FZ+|rQb@n>FnR7u(f|byiD2hz%p#}7a zaK5r>es%_tJN@(q=eI56ezcNow|YMj6=v=94{1$=EYLGr{o_gg+s3bXRANA^O`yYa zr1UY^6S-=!r zrx7V`Na}k?3dDwrmOa;#ZYay_%znTG%Ty9vf3?aO=9co9H8H$mdDtnvXuyRn?eWeA zGEy@kkE>$sS~NYkRi7oJ5oy6#0P~Pd^@>~-rhjaWkXd1Otx^;Q&pi-A~pIP!z zW*!C+B%PvV=1z-=!$h4<*;6fFz=QViYVgIHb~M;t+YAmZT&kW|d2>{}u%7Swu5Oq}%a=4!rXk<{48^3d)-*ccd{(`lP{cRog4Az!Dox za$0qU8Ugx1?Q1%XsJff1xKNN^yFpPp6ZGFF{GQ2#{e9Z41LG)5dL!y&wYqoeZDpvK zVY&Eb5QG@I=lhxbk2t?eNn0WOutmi-Z#i<-v@vlyTXyo8zmxV+ZvvoLOwn!v)W7|( zW(k(}?1h(bd`J50ek{1i^hsXC5E~w5iM9u6gMN72w8hl%M1I;KZa}(4uKK{tPu04! z+%}PV@0fu0{#{F6><^lvm(UtZQi2%+3iP(eeTz|<kzyQ~hRGMZZSU2zXN}m14p< z*=9{S=%^`qnS6cUrxabg60znb`KxOdF}r zh`Bg~ifEpS6SO=^ch&{9I&qr9sc}~)Uq-O{YvKrMO>Ue5bDtZrRP&dp29l*|+^>ry zFmVxL>;5dvAWM|*x&(IVL;VMrP?|xH_Dt#`^yxn;YeNzerUIpg7oun+B^qM`)Pw#~ zS*(G*Qk|#u;Iwz263snHu-#x?^O|>sD{8a}T6Q7ml$>W1HfCwfp zLf+gVpNH&|H8VMS1ng-czUdY8kHK*GtpgoXibPp0B{D_1-nO#oRvhFoxFB9Je_m|e zh%{o4ti4VpW@7vr$7L`^_otiXnHlC6=CSG`=@`Km|2&1rSRIdq4or*5AsiGz;^UK_ zEkgwN-xCA&$)so%GoXdj*#bnsZt{oTBV9ZFLwM~Ujz-D={v9uRmF7=RU&QZz)k^gL zjkF?dteu4+&>WhRN^avFOhcyTYPsAp=Gryd?%mfO|dNfcwiLG*nF%;cIzPvjw=lawnD;l0^0is<|n7M?NAO}Pn?!lyoZ;jA+|DsGUDJ52&5iGV)dd2~UGRX~7SX$nfPjC8={~Q6oCrXB+PNBIO;gc-y0^l! z^tnBr2gNEJjB`%*0vKsD&{Bgn&i;4?m7@^+>SbtYNlf>!Cy(_E_4P(hPR>qe!dq2q zD_rz8wvLe?63^YTB?VWqcpPA{`##xF^^p{HUjEh#Jh>wla|*E=d6tDuq*vxoSuKOX z{82e=Rb=rsV4%fRC@KiEZm1q>qYx{ol0{b?53{Gji7!+#wxW6r?#H39x}F0y4pQLv zaWlh0G%#|6{H=et*0V}`$fb>cb9{b8m&~V;P=N%f1b7cNM%R=wbF4(=OYG1rEDDV@0v^Z`Ehrt()$K$TK5e(01*FeVbFZHjz#sut8KRKUbA^v>@nVmz=~ zpj5(8NZ{n(Zu20?WxKDkMuW0G9zmBMYnd=#nV(kaYf5dd{`5iAvIh@_8$>+zLkB zB*_ZaU*c+KDe@GO1ffrIu4U7>OTW2>?s0ltZCWU>^Bqckh?TTMPUjffjhg(9aM=c` zjxmBs8j;}t)VT#gDe@Pq+C;d2^cA6qj#k8c`OZ&H_t$6blT|r#nhdj88Qz<561K4a zmaxhY)w9czqcQoj4ySKAnl%WF+kG1A3H% zDK|3fq}8~%&tl)T^2YX-but${>}9N@Qg_;e{QGZ5|Qc2yu#E zI04H%@nWNKaGj)+-E%2>{LoI&hHVZ>-LUgcnLdmdmkNn-i*nsL93{aSUl!BSc4f!i zdz1Q(WVYWQe&~OSh4@fq?12d!YkLS9AArp#GuSo3@}{P3T`$DyL z7KAbI-)9SY@@geSgkc8x9VsaBfd}~kTRlS-NiKf$hg$|yL{HFw?s>K{%+FYp+Q{~E zKPAtz?jwyM?luGqnyV=9=clkdCg`3fa65HuR`s=2vI#<{;76=qq#sR@m!PP^$F5G! zEK9b=u3H(IK2^9&!_04U$Som0lM2Ltr%MgxEh~>^Uj9PA|Jjr_!xLu>hW=MXw8zyw zKRaqkD`-8l-b{BVAGg(#??W?nb&n5Jn3q z-AV}3(g;cp1co5I_xJv@zqaSO+{-=Zdp>m@<-VTXFqdq8@;mrwPB1hyq(T&thrb56 zwmpAU<*NPr@@Vu$$M8GC@5B_;PiABu53pwrps*o=J2viD4V5rTn$ zb@jhM{S%+wgpcMtXngu$EboKqk?mN9&-45QlIOCQ?nX_P$?l@9S%%6-yL2u|y6Ncj?w2(YR%n%}hK;`_p>WR7ZGX z8NES!eUE;$+xm(u_KGfZM>|>dMpcG%518ov>&CtK5_m`YU=0%%OM(a*T&GOZLcFo6UOw$$Y(#6qNWXcD0t&;x{QrF}|$e5L!qq5-Fm*qKOvp1xvh9 za}ceML&@RgdX|ub=yvnY{BT!ZL?se+pnqDicA_MDs4Q;A9@@mTwy8Djvh!@uyc}za zO0~Wdn1I;WaWcg+8w4LM^n~{=0?h7T5LXQ1TSvGra307lwQe4TFF9|G3vA@vNZQQ)`$E^}Q)H88K+ADM~z=2YHH4w0i?AjG=w?@3)b^_-GUYTM^+4zcsTW zMjGQwOv+51*4EY6&kl%C{ZY30iSIJc~_dyksqzDyytNS2=&jrGx}4dam?< z)!9Y2m2RPPDwx~LTDX=mMX56q0ZKzmQ8a9}c7azj9XQ%r{au5hP%ubdwe9pq^?sp; zL^@S{xet#d$_dV-x_|kPGV%ikkNYfC!)%v)>oT4;0U&ATbnyu?jQjWbN4Xwr;R?dP z+B7Amekag`>`m<&0ALq}dy6mmpPZg$R6XAGM1Sb2gt%+quO;c8k}EoShzMlr?c2s_ zHC(HmKi67>UO|6P-4K~C@iy8L3swoR|-P=?XLjAawX7X7h+&;=O%k^4Lex%Gu}Cat=)hay#qh zDm|QN2-z8Q9bMp+dVP$1nUzEdn@u^KjRTI@)@(T^K3bV27-i`Ex9LK<({KMJFC1uc z$uuucFFsxV{2Ezb|6hRd!;{Tph9Mt|F(4@Ji{>E!atnm|FPb=@gT%$fJubh#IJGIBsWRfc@eG5W(czo7c=4<>|Kr&B~stPqYGS}dNxim9cbhC9nm5p5TgdIx5)@zp(I7}Yy<@tNV}@l%TzTr znMuaJNE3YkIw?-7B3_}S)G!mD$=GK<;nOk`buT?1TK^rs>zjqV4T`X7YXBAA5rADP z9GZLGpu4Hc*{*iJSe)dEJG3mE+_cb)_ERjfoT4zj7Hmo|-=9D!`|>jT%1TaYKC5sT zS!R{TefxH{6OTLMJ&9rKj}fN-gVyM8M)vih z=g9vb1yf*y5tAi8b1|IIOvknkEOwKPAB&4lpRa$Y$itXge%=UC%h^hu{Wg&3ts=>U z-?t?u+j;3_Lm(eG*)PrCE8**uGH!KTQ?B5Sfm7q)!t5K2=G+4ut)S>TukBI>MfflQ zZ$Q9qax*fkWj6M-PSexyt_M#vP=pQ}yj8 zGqcqwY;kyiVS+tumGgM615Xw*7in`J_kl_*>(pQ{Zbz37!lT6Ya6kBQg>Ns%B zk0p|+&?YVcF+2tGx)#ITWHvi6L2T3q*@`-Av0eU>%)gawr7GmVx(iZ`5u(KX8n3DNd|HE%Rp`k<6SHx34p{o=TF`#JCio+vPFr=P1Hh*QoJ_0qeS5Pq|hVA&Aw< zO2fLSROSTkdQLa2_vbn}0E1uM5+^EtZN~;FWRx(a63UmAHm`Lk?1 zhom3IQ;m2{pTl4a@?qZyn+T>l3)C8p`nS5T2wHUL4wA!+geWmZ_A&avJp;EDh`R~c zi`8N&SRYuchVT*%Cb4)wgdvtLQg#IRKDe%}MVEvIdXV!E&XiW(nahF8d~M7++CpI5{-BPVU;i`xFYiQzs4DpeOo9^6rmR;*jr@T* z&Yxs}+HBjzby(u-2iYLWH;N{-kBC#-JfETLY=@n@Y@IgPqBZAp2HPjY#?q75R3=9d zg%OE~tCfS=26MClSmZRGpWVtv1cm*7p%Oq@y;^$JH-GaCAAbvgCII9c1>{ZNz4+_* z*s00ORrtZgmOTXZFN7Ap75?*#Q05-{Kg}bWRc+z1o9@6&=!18wH}0dml@*bMpAXHW z?~Uw|@1H-{`At{XSG&%y?pEEBU;cstn`a@tz{byxO&P%3ybQ#Dg0?mvPBzhu>+3u2 z`dy3&ias3u2S|e`C!5FU16{uNB`Pu-WGmWHKmT0o(;SyJq#GlT(eC8>m*fjhZoT1z zG#7E=Vv>s)e>lqW$z`P6H#f}=^xEgdG;#vd6SX9GIC=API=Ot=f4(B2>1WTeEG#~F~jtK_kdNsp``bH@{O*YNZKTF7bZ zdzKlTf9i0rk!66XmJJv3wm4>EA8TV`VpzB^m=$edRcI;q%pf`E2%$Oc3N;XO~ zobIrTEd6UngZ{+edBsgyW=3T=j00DVm?SG%lX0 z;`Ples7`vSa{NU)C!JAYL79eT6goC?qB;SxqP_0S(D$ameuxR2yB$gwf+A*3S0E=S zI}+~>e5<)$7HUOQUvIiRoZ?37i6ZV1o<7|oGV5?bv)U#K^DNfomS-hE1Ilr_$_WD7 zw2(MH3ed^OO&AZ)VqZEi6U(`p8Xoi~7?9pI-&&CDGFU7{L(1XYdWy#ieuY}yYn-?d z1G1WDZ`x1UeB)Pr>@g-svfJWngJgn*hatm)di)6kE|{(Xwikk8$8%-uO1TRO6UXP} zN8Qc2FiCOq4UoA8zQWHuAITE+SQ(aJ}zVG*yMpK;!^`!BLy3Tvl*($3N5I{oSv zAs6+J=R!Ik^)w(xPJn*s$!nITUvNa)4Ak<%yWa4JAZK7*IeT18!;z-y#oE{r0eYNq z9QSv92}F{gy`666CLe7lxwO1fm- zx`;Q8!!$}Uijw8u-Yo8WF@c*^xGGu_THvSQ4GQ&+lUS*&PKDzvz#{VxvH}TC0sQ%4 zIE$h&tIiBfZwg60XXQM;6D6`|tD&E!sU?>KbY@d95GKFT=*b$PsE=z220lqQJt7nj zyHNq@DwJ2gi!0@I_3Jip8s&i$#qrZZxdVAo+He4oDM?$qTM;{2K+%&WDk*Ny$)mBv z+r)lPAa~(B$cUDW#hR8~7M)R1?%D_un?#Yd))mDLIjRWEk*`FwLzSLkMk=MOy}r#B z5W@}oCpNmP`=wCYx6pq|Y|}-oNd+_W`VOl0Gzjlb%efz`WX8&Gz2JR*Qj}m2t+g1T znth!A~6{qLdOp-m%AIk z-Us+)3OhuEhcVu>oBG>UC1gN2MGq-pwT(dRs12uTvT@E>==@gLlM^ zrbu{pR^jqnbEH;^!T4`>jV>&?z9m3(Vs=M9inU^kNvv&~my9D`m^f z-0izS)>=htN@}6gSG=JX5KClJO>A+AVEcw?5l0jt+Lh`5;Ogo=fYWh$0_6kiHRf`O!^wb#;Y0aS!cLwl(+uDx6ZJ=Eh~%(4WZrnf*%e zanTS>So(ezS|Z8^Gd^!RPg{F(Ox)AsDl<@i}lbh693@sF{t= z!4d=#$9(@!KyP`Sh;!{&5CdE@-X@TP#P5G<4(bO)B{auvVsqoot$A#m+S&5q4>MdwvetGRS~4K3Lp50Rk=^$82%|XnX3Q zGEoRUabAwFd%bTKPIS2kB7(w1i0iyOGIcR2=4VX90zDLPEh+g}Dp+b=CCfmH zpCi1bOO%U!{;H+fxi@C~QczeFX2l^!82(O=jKuiahapWU591fm!e;1Qb}S#YCE(9@ zE202S&Jr|Dnt}3taJL_A@)({@l)UuO!_P4_J#n;C7fEU5*I}>@?}$@pVZD(b&Y?2ZZwlfS z{=5C>w?X!?6d=*dFRh^QAtoP-*y(2M+$pTLr6(UIe*p?TWjf!ic!g380nd--tORr4 z)}s^?$#F`gbZN^k=$$6)%5i@wSW4=#gLN3`$O>qF6XC{N(A&K){C0MRhq>k1LtMjI zlaX!v-uHKN(F)0KdeCQ3GKzM>7{5$MM`tg)(MwHch5>(I-wP+ihpA-q`N;MUVKe1E zk7Y|36cC5x&M3?`1QxK5F@?y(YF`f|t~s#dtzu~cyhL;0Z!9oK?%O{Yt9{OdJ=Uh^ z&ivLj*7kM6vTP$?L$gt2&w3e~N=#O~HS+8O96hXD_UeK_yZu*hX<9^X>CEcO*U3hl z*-_3K8OB`cR$cj(@S+`j{&M(4=6*{tGh5Sa@N>1eU*EcYk1Jn7$hO9Rj#c_xwvo20 zx|l@Ty+4s$rvg?MJ~fsvT&;Us&W%PO&z^uF*w$6UPU~RpQqj2L8L7xf!0{tLVxi&Q zrqf)iY5=f&wY+m7BeGqsYh}aV$1Vto{?s?H$5}XGvRhY4g3V6x|GN{ z9O6zYd@*;+?g}K+^N@HtBpRy5quotPvyH}$mmdR9c7-)x8%Hg%d4TO~i6yoCKZ4(} zADmu5{3sGpK)#=~CuGu=Bf$nR?_T+S6D1C#QwR7B^toqgJt;@=_U+v7S)FF2^VGzqIb%dQI=JS+ivCq=e z<9uHQ-u-?5ZzbyaNL>FFl9FK6|GMie7oi1DVI)t69N)+~i-StuS)>M&a7JW|h<5QA zvheOIAoR{4;7T;vaO}5Q!(DMUqF_uPI%zN(olY-+z3&vH>t@MCtN7+pgnJ=zN@bBx z-up!RNmtKG-nFJgZX`Dc3cehao*z;-`C~<2vz9CyT_mMNdgz&I!-(Hq*oJIocgWH3rBR= z*}e@JH8}C(r z0vQ)=@Zy+jy_2XunQ-QROUFe@_b*Fi@HhW1c7i~m0OZOqdiaHSwjSKul@Gk|Bw$H; z!s6g-lP`Jv7yv&h`n$G*h#;1jAQpM{_;99DrH5LmAsEnnVbU1;gD1Ccv4zVFVS^QN zx3+0>eY}GGBoeq{7&~~7q7S9m`uzOye&+GN4bgm$F&GlQ6HpuOeyTK$?hEQ$~- zb1+Ns=PzW>7{GEda7p_VGeo#tfGp-yVG?8)sF`MW!r*V|#CzQMd~^n85!a212u~F}+hXaN( z-BU()Zx(9gbQ?O}hndgLZ)__n@D&i@y3_|Z#ZwcCW6(2FU*o~>W$DqFizcw4)G{37 z*ziUv^F??VJlRryahvnQ1+IL9a1p*)6yfI*0yWb+aBn=;r*k;B09?SyqN1UF2G7M2 zqJ?E>sS%IX*YL?c4z#DAJb&WHtg*lx z;E;ops{GRu!iwJRGmWnXB1g0G2=hH+RHZf5sr=OqF3oFt9e%EhF>4=YQKFOe`lV>L zET3wPoCQks&Ewax%xV{fj3+@qbXgco0AE3hk^(xKnCi9LM!41BEsv}|BHp^$;ETa2>Z{(>9L@zjFy(F03v_FPmE0d zF~CG1^+#vj>&W%zW|ujSSu90qcfxO&N1W4UA5Xq)q3?8>>fbFzXU%643mOZG9U&eBLH^~%@D=^gG9Qe9?ojZGe#kDk5x0z61C3|j;c zGp;g+mlfqVddDcoPEmx#T4M+-_00x+s~9+WVH2Ge_N=lBOai+8q<8sbTQ4j_8S3XD zSBjH5c27HE%OZghg7>aA`@&Mdx=~Io=;_`z@h%zZf?7~4NqdD{Qy6mC@g|8nes~yX zgGxOPmTy`+rza0FqR5iNZ|d+j`a*uBO4+2(X$^HPI`F+(?8T3j_edAzW~qC30Jp&f z5wUn1S}Idbg?jXWqg0s4SputDu{iE-run#wBezfmg-DX0eYf4(s*IQ(5MRARt08_t zWRK>skmk^Atzt$UGnPn5AA>ZZt~*1Tf_>E;^;wGY(smqF#;JMp?*>q!PHm;cxCCzb zIfgk;{a@NJ^X}v#W{zE6wI+8`q@rYAa}Gh)-E#YN|Nm40VhtlLQqf~NY-+z_8@-3? zGor_M_!SOYdBOQ1T*R7NzWs9_Fbj>@2tUu+dU#_YV4^r;VaZ*7B>oLJxqypxPGcL; z*AuShob|!5a^i)hrIgqK&m~78Q}6UI_+pP-wsW71a}G^Lj^XMg$yeni8}n;yz8aG( zbtrkyjHRRF-x=b?jVLJjNLJ31!vWaU8Jo4mDyC?;HRjY0VvG9i89XfBkA=4P(V0e> zNOp93iXis<+d5gDKL>bNHCVhh2T^b+CU1hyZV|9wfseUeHML`yFLoaseEbaEReo5lO0S!5$kJ&bDwH(fakZxmKfXtM z$`Hf{Rt#S{8Ufs!CjZSS@Z0^|MqK&w`fe^~H@DH`7aMWFhQr5xV4Y`_F?Y=Nwl&ZT zX#u7d*h?J}&Z3_4pJszxWk|%}I}BaAghKk8o|kOKZ$VH}GFkeJX*kR~|5#T$+AHg+xW8~P2A~ND3P?j z0hx*pj?%X>i*TnoRnCEo5MQWC_C%_x*gLo~RXFB|s)RuCww7#p3^l4Mvf&cL;-(|G z?G?{JU{mmYwG%Lp1nrQP)T;gcPiciFI>r>niN#CCZ4>ds-alHQ_iE7$y~|Yd7^9h@ zR|6bdd;Ra|=(m&FB6XAFbG6YdO@fN(*sIs2omIM+LAL=%!+)1I=8a#yV$sTt4L<+$ zp)Bca)-E5vFM~QbCENME7324f-38Q*du@~Qx)Llq7VrFEe$~aC#^4TsX%Zl>M-IJ{ zMoDMCLrl$w1_PC#9K&eW&JB2nI#xd6jd}I;dMejpQWK=eU*{+Vo!*db-#X6eVTU-l zZ`VI-V1`5pa_yan`4o4TR_qZ#cCbd*FC+rc{baiLs*3O$@5BqVrc`08*Y3`*-a9+} z_~rjD4v_1!!}Lbf`(|RpmLM@xgUwG&%EX0#D^GD+ZrzdpSlZ$4*mrh>bhH|$j^PN5 z%St3ar`i}iSb`d*PQi-ubS_*(Uyzyi>OoME7Gxdn3(%c=W%C!flYr)zI(>VxOsl&IqY&d z-XQxMQbr>3n<)O+k&nG_7IGYB5Z7Y`Dk2Ux%wI$vf!|u#$x8_XVO8{3ue(-}%fAIP zc;_Z|`C7zs>72Te5#XU?3UrBN?a?8#XP&RRMpKlGZ-LWVp@(zZ-WVSki52bm%aV@P zhk;Nd4YH8x89|o1L>vFq?5FMt3G%{)lsrQEUNxtdUP}9Se!p(v@CB7BUC7hFjja>=dYCe){xTU%UxY>b&l%)94t6K{--$lR z;sphmvtqs9T1F!cO2&Ln>*t|k$n9Qa79>+4eVis#R$=Yj%TAid62zdnw7ISz4@_oref0|mm=<8z=-SIrKhG?=hfVs178)|a z8ceU4K#8SWULQ5W{!rYrpN(coE2S%<%u41iWDm$ zOEP?MdLQ}Z`0*^0JVWnyOSXW#0USs=Hbmni7bX4w?#Ef~8grUGX zJ1Cc$!@B#qT_pa--O-U!Q?qp>sX2n)=q?Z7)ksHf1V(E?M6tzOmiv= zH>)tsO=H#E2gaoZ_6%oM8^BJvR|$v4ovwvME$2PySM!p%p4eCYU_yOqzrWGosUdR^G&cse6t+wrJ~`&Ej1UQABa5}g72M(-0ziCGm(2( zL*2j7nP3X4iK~nbI}4af-{fq*KnJjXySu2-3d~Fu9aL|(5;EW!0pS{YgcN-$+q|kr zrkz81Av(BNqjS{J>fkw>x|XA4M-IpGPtTt8xEQ`Hfw1uhX@nvYmee%xw))SS&a2oC zy3vgdnZqE>!|oL8Ro$4Pa9?QNybv?!V8&P5H4J)olrPT8)&=~J*sd;cOphLsMw(L- zz3^)J5G7`-y^)Drrad|HrVmS1{)UCm&?`-^=x*|+MjL&=&Jk|Rm?tFCj@{Sf?%4nH z+HJSz*@PHgEDjUT0QlkK;OO}|U%|V=a3YB#_Mv~n58-lOiiPJ_;0+Z?vLrj+N>5bU zG6{HneO1|UHFRp4I@gf!z>~8nAp=2AY>>QvZUWugLx)OVi>(pH@A_Id`ci=oqER1? z40_hO*Lg@|LC4aQFyNaySQ$wXa0tOGEe$)>lVNcky2Io#n`PY)O~csd{c!0Dj-$(h zDF+9-mtu6Zw1a=%*bI7apB=#2XZywyTQ9V~e_3-Dvw6gJOatHojCNR23C( zz=?(#C3gr!8pCb6OMs-xr>_TiT^f-O6*ocqjPG7Y9FggwU>V2xu3T0_4b) zv7F@OuOJb0P>gaba3e)Rt+bOv+%1nc`_FS99^}T3LOl8}-rQRfM#wq4Z$H!~`MRG$ zg+r97s78j?Wy%o^n`g?PR~fw5+iExcp1$vV%mgH<^Fm?*Nj#K2_p>H&wZ4^MdN#TiKElwpYkNydLZ zx4c7X&U14Q8u09S@k{QsmfX60ZN~Qhc=x`%OCwyzAUjsk!=`Ft$fE~~JK~_D$cp$6 zHP^_hUwQKv8`STJlm}U&=w5`;^>>|FM8;4xbZN&JEm~gg9Ic2_iYD_l#9lLi(jZFt zpEN_nHoF%UGx^c)93y-IAtC&tFYhMy2T6Gx1thssW90&%Je^PH9AWG~F~f@K1D3{v zqbJ)nom@wpF8;cw$Y5=X|5YjU9_2_G+sk`ZUU;ddvGP=E;Bln08jScE-81*@`B*Cn z?i@Te*HI|KZv6Z`#<;?gFXsAANWoWoV%&uCq?FMl7|}C0kqX%t^9UL4;(e`=2_;G< zQR39Wr_6n|N8(^$5M(X>rSB6pGiJpX1sW~I&;gkIjua|__$i=T(pmQIU+O;H!lk@f z0B{HTYio8_)S|#WbX98UNaXrSv=1D4+lzfCH4aV^)Y4K`J2D3{?f8FKs~sPdhfgo5 zn+3++o^@%rda~3K4Q!3q+G?fO*UKN&S67O@auJikSw=g{`u@$u!WK#rGqXEt{86^* zmHc#5338>tlnqB_Y?r@u2pDI?i)){y1qsT$+vVn=rKb1%GFN$fXD0;x5?{X_@$cGK zgr4FI`jLX>QBj`l?YmdX=Mi=gv|M%mk061IVToM<(32TIp|zJ+pGXldJh3ht;*rz^ z@3%CMl5M)m^q;U8M91E;1v>fk?a;M|`tb`%9_P6WN;>{vGgHnN4`nX^F^>qo^Sj?B4DH{W$OPEp)z#JWb?+JL=|V9S5OkO{#oI^Qx$cEWAN)s` z`_*UKXn8ciHvC_l1i$-+q|0*i_9F~|r_-4I!Ta0Tbkh5*u2Eg`bcyOByTJ*e-}JRL z#}n`{Un?C42ic5zQY-BYBa$aU%xBrMb`8sEr65zo%33*rMw{NeDgkmS6(_}hP{Xj@ z%V%*RWED4U7sv1x^=hvM<1iaCZry$3e9jN+D@EcHpjy@?6<~q$SKO1=?=V;ntI*Na zbanD_fnUSq0|%S(D;MHs(7l8-Gpj+9IDg{9p7>YN^Dk~IE=uknLAEX#$^XdT#1P%o z?S9r?guA^WqIdM*qDBuv_~lXPYYw|qayDUgB;S$@HrXB56b|be4B+Kkn*BikuwhIgra%< zi4Uwo8(M4LN}7yBL=y2D3n-}_Iyvfb!F^C_67sh(Xnol(T)$3Z8nJ39@=j2WN0!?I z?KmVR>Cz?)RN(82v+dto^eHL0x}>%lrVt{cJaXA=GzedJFRjb~jDa5gs}6QxRpe#N z=1|NNXM(F6iO`Vy!mXp;x)Hsa>7Qemr6CYQZgR~Ce9*%8_}vv*2u|tUwD{yXx;2%z zPh1e7W0B7zdPGqlXChL)6qQ<110?C?lHXAzowPsRXzJAR;=Md~Yo%@S5GsClR}1-E zb|lYKL<%_9^kZrw+8$c=JPTT#Rsam-iic9~3 z=+gAASR*Jl#Blj6rkrwQl(?j;Zyb`39Comfjof+GzfXu4!7wy5JpmepH!vB;%X4Chn80YZ&i|WF^hv8is zp7g(-X^h?>RTUQqGCi#k5lTSNpG3{FS!;h6(mWhYw+l4&PE-j?W73hs!8X?@oBP?R z&MwQe`w^_jn7ugY-bp&t$ZyU=5g0Qa{Ole23$C=%`o`eMp%m2#bEcx1IVB|(kKfL~ zF$t|j`2wASeoZa@id~7q7PkbSIm`2ukU(I)!1odi=v;|hSK_e=7(b{}{Q#X*(i)!2 zkAIA;YugPvI9I`?zT%~{AI(mBZC=rw>NSvB`DtsUP8XskKyD4==}1)*<@#%KVBPJ{ zO5)k2LQvP_@br51k*?whA@A43I>|95`dGRsiJ!usG?kb(E8J}J=k6)GQo|RV(n8SD zu&6_6#!&)W!>4$zFA@l3cgKs?PpV4OIPCt)(A%NWC+gUd1z++qWJ++gnN&x!0<=Gd zuIi`E04Tn5malg5Ov|Yelspn}b;T;2|7|q)=p1(5!`e-LJ$PP2AU%7%af@d;GOGEm zD0L!u6*rb`=V`B;P`yRm+lF>th3^*T47fW8N)jfFSbQsd>M+Jk4F7@})k^~2biTn5 z!ia9&^Ky73b`&ghi6doHqDE+eWsBDzK4+aYp{QTz2j9No0245Wp_d*3oGIW|N}Cxh z2;iSBUR};xP9Hqrpy)jeBWb?>B~GF}ekh`N*7S;_tkTIN78WX~<)x?)IXzY>_K>F? zyMufe4U=vEATZ?N83YDD6QOY@J3Wq!JUe22pGy;$- z*0Tx9fTl}`LWt4}z6YG-li2wfcr@vhed{=x3KE@E%Ebop312d%_{RKCFFASx22}c{ z%R>HuY5MhH<~hDUYx9m@kFGY76=#R6Lu|-PUFe2KK_m0dIhocSVv#1K>7O*@6~1CI zdLeMm0I8mkm=xk~kdJYHF>Z6)#n=q7eAO2vHYv99haP|l-nKMN^q;x>;9e2D6Fp)0 zytNNNdrZZl4*_*(_r=SVs+s4&6h-Xn>T2BXME3A^sc)x7<{w9j(IV&`@hLbQ zSe+RJTh8pJTZ(?Zu3QUsINsbQRaKCI!`N|zu@|li^r_oIvbK(_qFT5l>-x)l&YB-~ z(wIZUFSAn&9cz@8=_LML-hXkb`gLrByL?wZt#}{)>Ck>v6{%M+*7czg3#mK7N)tMKLHX zSX0(shbH~Xs^+XH4vCJIohEtl8FM|smtazti;_I#!+Uqxn>e&C*I!eN#E1{NF(m$+ z!kkbg*B^q^`{X46Zj|Pj!9C}qUaj7xB))WH9R7^3+76RON}~HqXpeaUkp{QgO0_fj;J$0XvcgX=2nNj3a8c%dzM?10^9+fUR1;2v})(F+5J$&Cmy)3az8 zp=IJq5%urYab5fGSi(f~=`u4*LAudyvgp{O;X=hx&JaaC4CacIe`m;hVQ?6Td}Hw62{!U5_NnGPJ1b@GJ9Z* zF_2oStT64$l4*g4I5bhSNjqV3_e&?}#=`njo53X1zl-^yYsI*P@9~F>=pO1`yGkh< zeev(xId}I4xdC;C_$zpBgDIHzPo^<1Jr3^2LJ#(+uqB)933gco=@6A}%>6p=c2Wxk zR|`@FO)aD$N@J1g4X~2uK7S=Kr%-ChM?6w~uHc}zbW{nF-h6uJC!c{BX(0)!+i=4Z z#KQ`bSEg!O;)_tm*L?@H(bH}LM{x=4D0DPT^nHGuIzKB} zdSz?LxvCs{{_3pnbkKKS%S7$tc?^c7UCP}^cI`R$w(&5(oYFjx8kZ0iWppiG#od`Y z&~Sc#Na}l%X5^uQq+3;FQfrR8zSerPi;O_x`d!^>(_#NOY5ny0z4~;Nqbbu}{D&M|gIg5lLEM9JvGN^6(+#=a4XTAIcF% zL~UNEx)?X6D#A_l>y>H+3UieF@Z!la7B4|G3z?tDNX6WRusl&c;Ih2kYjf5vAX`CA z{Rr@}cp=dXlFiuLdYx7^zjr$<$x>TDU}U_0>3CpxOK0iRHRLNVo#e0R@%xo?0c#>T9cmbp$G>Y1U{m-h&ebc@{CBZ?8?;5>7u#UyW131y3jtyK@m=*uAKKMNh=pM z;Uv5m8AL0qNCNJdqz#$iR6lHt^x?g6Mc9jH5)4ghFQ)n^?qYf20f3nq8Od-DdReV8 zAa+m~`HV&+$YpaM*eyPK7Wx>t4#Z#@H(Q}eqqZ;_UiHzd=kkmIlvMJ6Q^bFoHG(a$Nid-xHiP8?&SV3lpvdd}`Jd?RndlBJ|t1zBW)1JwJYtkKkxb`omN=rS9+V ztNi^*n~Dl8$gh1;%`;|dUmoft_F(bjB2GwZPbYPh8%w8tyzb5P`*p6-63r^yEUs_k zZ(}573l_}p0@V`ZY+zS0?+Yi@Hr67NTXq*$zu=5`{p9GYd5&pYi)Vakv%G;7Dg(?nPIJ)>u~6}X6-#@}E%NOOBYB&)o;BPZ8WLTz zKwavfaUJTjU*vs)+Sei(4yKe`Vupm&+TW*WQ!%UwR0G#bwwK1_XluOf;dU@aIZrGj z>CgC8`Eo5Uy!uCX{g_qt)8D>s+pmJwI?n!df0<@28-ea0`pEMWAI;f7)4xH$jZ1cW zSYQH+sSv7V=85q=;cL;?{PgrJkXpM&0r&kz&R&*me`zUrQ<+SWILpTleBqlpIN&ks z&RWZ*6mN;4FpYP6y}FreKw8cB@Vvfo^6e>YVl%Fe6Z2)OsFV$3-D987v46VB1JNi;voYv- z1>vf;Q&i7R84&4HpBB#1C*$zaelwVmlCLU}8p_FP&it{awK@)&hCkTjDhbNWTwEc) zF}ushw_&&X^75}qHvjq@D^m>Co-e%YW)h3evyWheCQ5=xDGR(Z&e^<;@E&Yp0;ezn zDkr8Q6Erb-T;{qg#)pjaI-O6ts84Q(33$4Cw(~N4?E8fzxGn(h)r^hiBnveeH%<*I z`SMq_GwBa-5Y6b#nfn(7Fj@ty_v;$>PMY(yr1sq=$BU8y^`f8mcB19W!se2)LGD(b z;dGB_rRS>g+Zq~x`eA|1af}S7z7E>4hg%XxsZEG4LGfZMu=3L`HR`-={6u z^=`Q~V^5=aMgqp@uwC==8)r4$QlfY9g0wEgj^L6!-9zL4>yLijO;i@C?CI*g^cz)O zoo48WgoYtINhi{77f7B-DopRz@(!Lnwn;xoRXV(ru@FIV)N0O?E4C!50oTtdW$shz zv0`={*BR;Bq?4=xOyMD%!a8{Q2D?3QNrvV9ib< z3XL4aXw0n6dpte;HQ=~vVhtOCe{P6i79Jg%bqJZee(3N!c%ax1K}9m5{wbZWf`xx3 zXE4Y|4IfwhYP0Pt^7D?%4ZgPP`nElC*hPXH@7eS6g0HBl%yE5d9#irsJb^a~Zl08` zq*S=y2lv!bS3s9*-BOI*A!L@n_&d5O$}Mf}jgY2N0PhDUZ(+vQ`i2(auDxFVY{Fsq z4XrC;QK|~}?=Ou-;+PjAhOpxM;N=SWc{KmLc!(aipX2b{`q%x7!94yFd#+~W-MJ$B1z8tb-fcW6Y%e#O%*CEt$I5H#_KF>2O@fxqePINrVU0}ZV-9QU)@z&mb+uO|ZCE4mfvbwd>7n^Ai~c{a_kRwQfloIZi46+E)t@>Xl!`djdbcXSnx;!j z+V@y__GbKc!Qhjr(GQmF|4(l>W!cfDApdyBat0G3uhxD25X^-ojBJCo*DXpKk;J* z^PoQQ)IJ<`ofXw0!$gld7Zr7p5aQ-*ZvgzJwZmRYbmlD}${bin_=;~-z7dQrF{??C zk+4Yoas1^?k$?p%pW?MR9dFSDV`giAUC!b}$Boc8ka^5o=<{al-r*yIFw5Z16x!7b+IIMyg70>C$%k)!w7=YGG zQ&zs9hk$C?5{Lv6Djma>yd#5C^ackVJHq4#Rsxonm?QMe=hQ?HNhR(gR=O>N97YN| z()c%|{-T(%t}v-5YFOxKFa`46mZh=`tumed^FvfAn2}ZBiE&L!JY{mMDa}bK7|2a3 zTkl{e9=d?obYWJwaModue4~XLNr1@E9+W7eKO|aO(oAJY$ zKc$L9RB5#h9ZGrpv>g`Aq$QHywXPJ}W<0e*C5=D^B;@xaa*bPctT4t&UCWRj>rDd{ z>>juB=a}VjcMVd2>^Iqb`gV0o5r$I|;{lCP>Bq=@rJsx(OH!Dy&3wlEaFo!X!DOjqhGc}XBO0Nb!)=@N77Y>Mb&oQp(G>( z=`Km>?go)A>6Y#ugaM=*1?f^!8bmsVMjkq(Vd!wEp_}jUem}+Q8qOTXFxP_qw+It)2li+rzsw9@TLSGX9@%9ykcz&kD zH_cKN4OO)bD17_2ph{=?j6Eo(qc@Jy5-Sm+86V+Wwrdg@lLd66$IKDsAH|m>Qfr@( z^`Zk)ak)4+K$U03It0W#+}tc{Vtge=of81-Dg4TH^_Sj%8KyaRS_7A+oM;K?K}h`? zkM-91q&?i#a=?h9?-kYy{1c;$n(3pF zb~#l!aYy3zy&!hjiu`G8#{){@828xU_-34fju%Z6kjBo>H?z3aseZ+n0p9klxPJ2I z<4j*k760-p-KSeJO%R+d45Eajy%>Jl5;>C)Ft)3xMdze}2lb#OQIfPr2XO30f}|Pf zBE|Z%b@g6X*SBB zM6Orc=-ko$Qju+0a8DMLbBSku^2=eGsU1+;zXr(2ZsD?Y0ND9V2m{E*Xhz1y2*A7K zgYT|J4REdVSj1!1SK;^M4h17sd!!e!-KxSCUyW|jUr;eiT$^0|jVUL$oJB;CR34Nq zNaLg}@5C%n;f5ucqOAdO5K-%+D56%fG=s&YQP&aGm~!AFtc25m{LD^<&;AO!*3^(Qd{vST5pK~Vr7}STKO-gEK-cMZ?9xf9v#1bbFOWy zs9(8nQqekaiE`N#$u4X|j`^3YEK?j_{Uat}@UnsJyBdnJt?-4NF!zYECTQYar`*$@ zpA4x4YSR;Z+|CJeMiCSAWbpvLBxM^nEg{AL`7`*!zG*JYl17Yfsu@@y>ltFx{p!Zc z@N>vW<=naG-@V|0+!VNd&q|vtP7}c1c|5hg#E?Jbu*y4l-x3lFsc3&F4hd%OMq_G- z4T`O7Uhvh3k&=InlABzeWG$$}Y`i&|(HGkUOanmlkqW!Y;>0M?*NK$~=WF#Dqhm~B zq2WEnsebO(wC{DL%?vm{U$SXh+@z=zEwL%S98U?tiA8Qk)8=$6ttzotr|`v+3mEmR@Nmc}ijlw< zqNb*If617@k$YGcCMQSAkW0&~k!wkl6d1KWS-EM~97c$y$8KityrvL`Yu$m%o>m?g zaTrST%UXrWeR715i-JOAb>kIHEfFHt?mKK(E+_c|C7Z>mYw@U_mC7yEr`w!ECkEC! z@Mk}Ev=BvY(hqMP67`w_(6q<{@JX4e5dptS6*nIYghMo=zy6(<+?=7=L2WZVbW&Gl zY+F>4pvEIlW-OiFk7w+K^bP6lV85}rB6w>_pEkOU+MP6{Q?jrs7vacnnofVxFee$b zv=T~Us@-G=pi!1sNg1I@@y+bPQgh29%>J&)3G-JR=vFpEMG6?wK@Cnz$-sr z`r}JeXO%Eek_`2!%QNWe^-p|>DAsN55g0_l_$F5U^XR)8B$q_*piE1n$?UTihZmc9D@v znv4i_0d-Q2xx7-dn`xoXX=2*G5+=QG7h}DO+D6g!K^;J+=13#e)4Q`8BhE%AYsV6h-uEq#$x8M6&{p}VSxC*&P^?d^Wy4;J3FH3g|eFIZf%WB z@Z&(_t3v!ffesYDs}JQB6ev!8Xzx1qDDB-s;jr*vjpYzN|I`tR9DeXo6ulUPFTYRb zP8_m?WmX=qw5wHDB#PV@0302(Ic1%a#Z_xJV==^;=A6&LUIX&ukY4PYYp^G=fRl$A8Iu!bp>iTY|h)fRpf=c-+&C8bT424-XY zb}wE;Y0|Ix!9U(nZo~VO39M@#APYyys^uMi^tP|>PHHz^dkzc{z(_R!in3pJs!Y+3 zD+m7E0a6Dax25AZeI_@7)cfDIvnP(2@3D`+Fm}pBu>PD!r}Yh{+JmQtvM$u({bE*g z+@6m*bD~r${W@$E@gCWuv>u4ogH`iLcR=?!^m}L{ zkIOnQv1IzsmvBXP|H#;aE*g3&TB_s#c14JL`dm`9m5+z=qG+`+O?_Ndjj{~G(B7%i z+G*4)W>Xtse;@j^;cXsn`R(}LDan$c8?ab#Qp=d4lb==-CoO${k0e0r4fn{J|LZdu%+Jfyj`c1~T|BB<1hNC%O5GA}@gMYq+21<5&D&wV?aFJ4Ao7G{y(0hGBx z#&3<)RDT$*iU^2iDT+G9psQ6A7<-16P0LPDF%mmg5uhUsM)pMy?3Y|0#rCeSBwtv3 z`4HF_eDKdc>NVT~{9@b#B>j5dwyt-5)s{r3cJLF&N9tl;er9fj&P%v^wN{yZr}gNPG-{hKnEt^-^5ll-q@=y zjl^hNE{9an3UBZ82EdMG;dF|tdC`bv2BPgP_y@&%6p+N0*Uxw9K(-Wiyuqe2^&D`r z>wMEsW2;_&$*!hX-1m*rEmw45(XCV+e|+Z9WehcsjmiB5c^2wOJQinzpZEK2Kj$98 zkE?+QK6E)|E7CYK^S5ZFg>wdT6D%g1--__Vh#n3FKk3;_&J1Vf=QxUdDaub3%_wg^6qCOoMeED$z&dEC##6EO22aYWa;<;J z2f09sN{tkaNx6n6uLPxyk;mqQaN zjaoL3@pVhDOi%_10~s2Z=&}b-$+ic}p!Q{7WI4dWVmx@XU>}XzZ)K3g+9h(orv&{= z#%#tkEL^h8%O4oh%1?BkI+_+r;bTgn@Y1CmI36!z+n~G@kLVTWS#T=*Q!hddkB0NR zwS{fP5b2J;*0BT)%AyJeR$w)Sw9igU9sDWX3S`U!)EzBBU54E|f8Jr;@UZV{61gRt z1giMgw?znv^fietHfBC0M&8RlqJffQP+w1bM+#lkis)B?BCJT;r2$3Vr3Nr_fA|Zf zsl?bKXOl>_O*LDD#CZNej7_~^5$^{}Fq}|~xiS^VM`DE92S1A6>mMgg1N%9z;^gVC zx4D92fNsU&;r8NL0%c-q3ZTo@xvq%j1l{gu@Y-NKn>1Rkw)<-%$9LFf7?IP=ml5q- zi+@LZ)y=4;Q!Eb81=NI`P|fy1l8eolb0)zluiDAK*ZOEQIs5r)BIy}j@zQ1>_dY-* zw5310$ot4?5h5Mz;3j>$tPs=UUB^_xMON#3UrZ1g`8LEJ3l5g;uJ7zLV?EQ=ym1de zO~qUF=r5B)10B9C6O34%1}b;U!(q*GD&=C-lh70k)*oPti55>rT2cH}cjW5(S^={V z*2LL@wKpcLV28*#oF6E$gb_M|X4cXIz|2b4S5L3B2+z!7npE@0x4-XuCWZgWSBF~5 zIs)mY>$t=MQ!bi*Zx-JjPO2bJJLTD77$CRYg%eoIukLM3wf)<0}9!@OdtI~c{ zAY%}oSqrlh{5ik=<^hw!FZ80_@?sz-3S&)LN{A#a+etS&(N>d~GP;AH$R#GfZBv-B z?TSr7CdB2|ZIjGX^+Ue)umTJCjIgHkcl;d!13+W8clY|r2M{fB96dRG?$*05Nqjid z&{L#5A$8K=Wgamy@AG=l38>th;V?FNjF=J$LK#x_;=P(BQge!3GkbD64>fzGS8WXr zHg18mUSz2nY#U5qD_veN9#@Fu&KhdMRzUt_i7&o=D z-7U`S^Nj?P#J(9+(Cz{2NpnA9=jL49-wM;B&JTsE%M<|qIo82WQ&Hu-b$Cra11y!K z#UB8N?{yZvo6zc|)_PU%_T=)76}%4{$?VlKR57eKM#*%`U62k3#xg1l)K@WvF%vGy z82GuU8Lsw@1V{VW+LQ%p?-Rw~WSa?Gms&3oI7jT7b$B&%aRxn~N?ej#N2iWg{)iHc z~+(>MP(oYtpF0Y%)GdU9WV)etnZ%2;i!^1X=(tjj6wUq`H zb`5Of^&Y;=T(Ltk?5D-1z$kZT#Uca-WImW;#B_2PQNH__LR2k<_DBA}>+>8@}@w3FArVIJ_z)KMUxbbB)!H_a{!c2i{rLak3J8 zj^wq@==*j=MN*y;u&7N_|B&~(EZ6G&`Q3|(H5$;wqWxhR71h`n>m!6&qwzIziE90k zL$#*&%zBJvjkv8KZkuw2^F1n35BZSa*4}!tB%}`g%KF>^ zeqY!~bNfX8Bhsw!Vb2}0Hbk#0E-w#dQF9_9(1e&$-J1>Xy1_bKdNz^Tj(%3KU@-|9 zpZ#(0;P00r>{q`|9+UM=)HuraLmtA*0c{FI6FY5IbmxOuWYb?9Q%MB_cBSXVaEfh~Wn<@+h%?Fud{5;9{Lz=^2pqM%`+@nh|Ay ziSN{H5Sq?W5pg5#dEZGO+QQXEC9Xj;sNb z2iXcedhAFtDwKKirKg@V)_#z#wty08rpR_m!U*!8OhoZQ~Z z`~b|(g01(ng%Q8KMxF#+7OfuDwFphTw;OhjAKe%7ovmG08mZ#aIpsuC@Xe0*_xn$Y zju*d)PVHl>i(fdO@#SaSRi51Wb8*ymj+gDuhx}V#$8WC#mmN+F@1B93Zm^sISAa{Z zn${zxGrsZL-eTmh#cicZ&5!CW~ zk{+v&%^;EdetlctOhN3WtY?e(s!1ayg^(LklVCD-G&3BPIZ@2u-Y`{{NF}`^!4GEc z&{pbJ1153fGymg`hnH%>V z7RFc>6SVzjMS(xTC5}alqpZT(VhcinWv~mz-L*m5vO}@Z%hJ zKndVN&V*)jK2$W-hHL7UWuc7fyX)QsEO+Lc z{Z;fV%9@&L%25=hdbhQ|V3V$ev74YbsmK5_FWYb&?pZ5ysOfs0Cd>Dx8t8v~9~o1= zym0HHRX~-+=SS`LnVIEGEQ5lbH|9RcTM;n0qPQz_ z!*6wBF*IR^$)Pzt0nex34jZezL;_cR!eD);P)zj13ZL8 zLfs=BC8lJJYH+E)KXGL11#zc-9nHy6TUwI;?&o(Yz>NAjs9JBS>BeksT~pb==I;P+ z=38RUPgHQJMcQ_=n1@v}MD-|M`_uYhgn@W|(`h={Z@&Bf_pjqGNEGIl`oxPeoB6Ls|M{e#D3U#c$=e5 zX-m?krgI*rnY-Ef{?k-f7|`Im+^g{8`cH>O>wpeDHE_A>Eh&NMfL(wG8 ze5{%Ze3Pp0f&f2tfzsM<(r}tT=n;jXq}6ao94a-^tM8mU9@suX0(a{D4I1~ zX0dp4(c_?32(X7 zYb7Bw^Y$6vOKfcHjf6LDizh`ujQr7!09@JmrZCa3UPd6V`d>kem%lrZOB!5Q>PbZ= za}lM##JM0=oL!v4X88cp9o0WRgx6$EP?M8aliQtL9SP}(3mN}0On;xS$c+y8P$j=# zn9T;3yEcvW^dee}Ve_aQPL_&QbRL|-3E4Vt$&u_k3ztlpZ4ez(+kw^aU7S4Et96}ZTb48b=A~smF;a^@SlG49WD#pwMpSE&h zF4bI40*Rbgl71h=4S2FaQHArKbFl^D>h^xp^mRUFKea735#R~7SIA7%%-?j0Bv5*1 z!FpDP((xU>=QyGSE%9 z;nEV%Uf>i{=HBGW>orQ6<^JiPi%qGw@{@M}tM;Bz1z3qOjOaU)Gkb~>x-2-IRs~yt zxS*%yhw*&eu|m!wI-usg|KKCS_n|7)#DU>1ON-&ckVy)aa3}|H-1D)mG^-y=gFU)m z7*(2F{NT6@aOjp@=i3(rWUz!c+v7%HrVr`aE-=5|fl-52=N1m?y^YyC(xKLy*awwypG-{&B%u)!Zi|lQO*Ag^ z;X-Nc4seUsjw&GY&Wms@b7c-}Y%TCrb_nH?*F8J{h(B>hC+P!7B_iu%Or8aZg2Y`NM&Hw$j+VA?4L} zBTKB+S%huizqp=p4atX`6E8Qn{3^hP4y!@r0|w}MEEnzCIQ!+z_$fb_Ps?qs% zotLc9QX}K>%U~jI;Cmu17Dh||xX(AJMF6sdSgCxxf62Zmvd7o>ERTuxh$R0tNky}t zB0EZ`+Z)&`^Ks<#X-^ySl!c{D8uKvrm7;&*1IyGAL^V3R^(798f9~Zzy88EeFbcqT z@!wcJ2K=jhh=4Bra#;!=+-fg%FtZ+!(bood{OH6DN@@_&6Ql8+1578x$zPl|hu|Yi zTgyp^yV8GJqYe5wyWVdKNXL#ZxWXh6!c9%%J&nt;i36;cR7uH&B4$k^@HQZ}1pbC7C2p!*5 z@S@sZ+oi>&1l)<-{w!{m=yv0R@ZJ9X+xKJqsByLFXhjB^cz-SUM$fBn(s9YQ$}+uR zzEv2>^NNf*T_29io>JIf*IZ7OlAXOqf3+|~d3dxRp%{4K=ykry-)4d^dp!c!eYL?J zI6LoFj;$;#K+op$fVEB7-wORgV8dJA!~?Cm{q(&t_id7MaOWeynVt#+bxA#)+zIbr zL6Je?=X)v557hx6Q2xz7m9 z9@Fo&M((5$FZ=DPo!kY^x4MW0UEJwtvJv5{FnY|5&i$ynZ=uS~?E&+YRHvyjhQr(z z8dr1&{(Ss(oe#?c!aP#*vg@+Pv`>bF6BBT$V7Y;UjjjvW(%liexu0u-FNSyQDh=Ck z%Pf0&@~MgNo_0!@EZYU(f@|hM77vfHQm|+=8}2>m<0kX<_W09l(8&xR*V;g~8wv=R z1*|t8^mvUx7ivBDJW;{FSOrxxkd{ey%~Q}avulRVwL7TpxbA~vQC$5<}Fo6 z4IU0O9=cq^V7aIby0VPKy$^oN;U8PDQrt#NIBafp#W1UXApwqu_vh(@bOPeoNrQ3O zHBj=q!#2Fwe)^!`e+%k{#b%ep~=kGyX zm4@~1gKBdZMlVzK0 ze{J+lqaxzWP(V3>_il*Z<6sid$=+)(z}wCX1bVuxk?Qk+-k6`cEwXqhc`StFMwT!Y zDZLn;V_wDkq=9$0Z0EjROwzOMX}NSMNa=T)`R5~V1`mYX6}r!La)V9#d3^f8F6RB$ zJJtMnv;%T>_3?=b?jWyw*1bcx5^rYL=$v`jY?|%Q^4K%Zc2xFN2o2EP%$Y&RaOd63 zK`*qjL*G>4U|P2Ce5pHP`%aBcSks0e7QefX13-c6%^SOQc5_im5O;8QMe}JER9cJh z{%8)`1^tQ>#)MReBqYv>R&9aJ3Dq)~AkeNWI~Xp}E+nO<=837Gsl0tAlb_|F+)m=uc~;CtN1`#T)Q9-}_N5le>y%FOq~cVmLc%bkx%CWun_#L7yT z`NIZu^ei+OCak658hi6u-C_oY)kXhDOG!L^cynjzsIP17)g%1r&X3ajI}#`0OCz?s zoOC5=5OYj6c8rHT=65CIeDk6pU%N$@>@c zQ5_FjAcH9A#DFVvkBRJiBN=t7jM zW*y5*R5%pSmBD;>Y&TMPLF3XjWi#xtN0;Achbz$+sNv>DV+M9W%E~53R23%iUrpj7 zFUF(4*JH4typKUifF2+8+@sZ4!63GoyQmfibsADacxFlHgF(BbfP8SWix}qhAX>3A z?bi!z^cPVl8~12GRhT@^g*t7MaR9_RFUXaeHxs+_YQ&H5n;3j$(%c}X~Sljap)DUJBwqoR^*$@+t*<3P7B)$4zLOClw~$wzec`YdZcb+9|(dL0?`HYa6+NEi|B< zk+jD;=rC_mm_L=@TmctB=)7$^=i|oKpP~^XW*tIubh#kEjdv@DJn|oWaRAZ zRf3|&K@-&G_A?NK&&-TMeym#pcj`F9?xOLKehm=u0s+jy8B8NcZVV)PoHKK1lTzsOg_#ow$kXl#s$Tp@!xPa=YQQb_=-VemymfAjTe1rn(KYJsF_#l4}-tkq}9m+n%7 ziJ-@hX;W+cDon1|c5ZakxN)P2IVaJ|0~;YvfVb?@mxvN3cYSP59i{q-P%;?QWD?xq zzlwvG^gZPo_CyHu^nx>UG4q3IF4yO!k`AlWupDrSzpF{mB%1Jj+ zE%yb5-E7G}BEjvH#~pXnptWn*1G&`g?r~h=TKGZEkmyo}jnO)>gmQzcw1Qq)^aiiZ8S6E;2G8Pawf@M;=ZJ9jik~gVxwn~Ir_%&!%u@(CFNe?AljlFqbobm6W7r}Qp}d#akl{+Elf&_v zUyA;sq|{iX+rbQAk%@S7mHl7M?|!)-M@A{temWGTtbOW~ts{XdWqwBlW?Lf#&bWL@ zGNOQoj7D;m!vRa3wh_M4!upq7?bp1vV;Q!2-60D9VWT2nAN+{QxD!g^U{ISzIwgwy zWu3um$~dA4<y%R5YAU6xL!54I04YlsP!FdfsitjiEkfzNEImrk)U?~`+hz*k&*vhSE z7jzkLUaLw4Vf%DfS5uW_okuy%ZZyoLubt)JVRyRE?*fJPk`g_9p`SSqKuFJkTF{lK<;E5!-v58#)DXXTyX-0ixLX|4n_M#6^TdR% zFrZFw3IgRTc)dRzA2;Hpst{^f{Zda4aqyDM5-_;{5|c3#b@$6x6-4u>$VmK*^m|-` zs)SE0Wo&s6S8Bkrht3u)q+cb?^6)kZ2p&P0K!Kj*FZB~($ARMKi|U)=&9Ba1`lrpU z8icB4HvyTM`b{61(`@zNM)B~;%{XKre&|!cUHC=>qK~j-ECcgxFf>r@1>N^wG&M#Y z0E_hjw==MhWYi0lJMV0`=50@9Cf~_lE*jU3NwH5=8))h#0)rL2c5DWkN_He>pZw`? zMJ%THN$)XaFy2r= zt@j@IHGY0vKu&hc4A?)iu%&LI;7CyRz9hZMtBwIilQ$%0vX0(_Fo~UP+iG;ss?j6v zbKzPOc;oK%w4Sjx<8cx_q{C$GR9Z*lb48I`CE+Rr_>=&nhVC7XC3oQO+aJF_+j%`1 zsRo81v;%LyXl&GuLtcc)Bo-F%PuuD;52|H~Qg!iyZ2rz1xt^I-nlz9fw_hPi-et;- zbfEvVFn-BnbNVsx>}O53mv=yv4|jZ1(kW?YRR>kQEjhfjqwqC=39(&fx>(qzHi(F3 z4NQ-BgjjG#TSFhzyf~){Dm@e&z||?@y`Q`3o@+9G0YR*0*E)kfFRu1*6V9aLpxta+ z@u>kcsd7S_27Vzxr!JGKiS?@;AZaKl%pacrv?E`n!`duiw)|;RVBYWYEuc|}sMyqy z)3-whp{1L8Qzu?;NFi|t{=9(CGUW4lMqO0)+Q`Xuyef5@Zzdb0e84%|l>!d!+kj~U zYv}bt@H+JAY!DP;eAJAdB!M6Vtr^2^nZ{TLgFv-Mg=#%^g51=R!*Dq_vfj};`UarG z8hoQ~f&vm=>k4x}^U?1DI;PRZi|5V`Ks(9Fv$=O?;cAs$0mfpRmeJQz~_Pd9Rp_ znWYrVvn8yvzQA1Fj`<;Tepv(UJ`2`GoDS6)1#U6`h>w{U!C^W)F8CpDncN>a~rL*_bo0&#gz%SxxBk-ba^! zL1NkitN)6){oo&Xn6=3Gs!4_6U&wi_zZ&wDAc4nkmt2-qpX@bM=X_+O8DAwZm>ei6 zl>E@BvnqF(F6rKVG4S8SpMiweB)BAE>n!iCb?rqb3gbj{6|}e;=t9+OMyz@V)MUjQ znFWP~S?(zjj(ZAJQxOCONKRF)E3T?8@s>_k(IPwY2_ypw4JUg+})w0#{X)m>-=|j;8FUA zH0yEE((iuUWF+IpPR1rs^#0ic|ZkMK?Oo-3h4!f_!19yoUNko!H);$pr>!> zT&YM&pC>Hq)A^4@nKg~MlbeLERr7gzw|KF!OTUiVss#-J`O#`L>*9I}-~82)*Ic6O zTf9PB}IPrn?JF)Dbslnm#yxjp9a@+PASpI4y}P~tH|bEXY$e{p6c3PnWU@WUo#Tb zdyrw!{OD-{48g8vwUn^x?!C@-p83;2w6{OYsf*K}-~6S?9epd`H!|t>#XudXq0a8( z-jz;2cU;6{DZrG*@)=_K_opW7EZu7rtX^T2!mJ^C1(0K#1&s6|mmxjkPCjpNuYW*Uu!^z^C^&29{!-GAiALotxsL?YH9k?IO zl@<7|>*UTHa)@)l7eL(UeNFP61q-N3MFZYSdETD?yTi^eTv}Wlbz1oC&u$iAjfwl% z_ZgW_uJuSeAL#k3RNlMebUyy&$`lEA zp>!(`Eq$Ks_YPAVKp;S%*(MA;=|*opkqtAD$V*<8Dks818wp;(D$NgASI|j4SZB@? zG!H&r@r&s4>t%x1dIpHgF6l`h;A&H{W0WH&jY6nfT~XyFR#pr;doA9UKLDb7SQW|ND{{c8FbjXw9KP{yqC`=Uhlw)_{i$# z`2T1(qWdaQ+}JUF>j6%Rzs(Py4h>0{a$kwlExy?+quUbA!|O>~c;4+j68|fA zN|5b1;W4XAu>W8Op3HPS4?x4)T=7dl{g1t-)3sb0NT4I;C*?Onf%VyvrtJf{tTr8; zU{>vI#|`bWXy&{iYEbQIuYhgcOOW{W_4E!j3P5{?4L(B2;$MAz*SF4)xcg$`yp)eD z1eAb*tJz`Ug;oP5z{{H7?#)~WKX%(=p zSqSyz%q;DdlLK2LvUl!;*9Ml@PQ&S3a$CBD*>U-{T zpK}d<+TI-vGYB z?k-t>SXRk;bpSQU0@X!m)l&=*s4%bjS(K`)w-=&K;wh&E(x-i)-$fdrZNO+T!9xQH ztTw#ozRKKH16hwj@UrGk0XPhCbjM!h#CXMjq=SEsd{nzmc~jtHZ~I@J_u)8X2d$Fz zH99OH_{|Q~>VS1PjRS~2*j+&YTq}MZeNlNM;ezoKgE;~8T?9a3rxMv$RwPO`E)$Qa zmwk>={%m{B*Lu8mg$~oQ?0EW{TzAI_bq?D z((MQWIft*l2YUgt@68tfv;TpEj|%CThZ*_6jTOJ2PkD~rCVsu622HC5T*gg2?#=_= z*f#y)1DdwjI@tt0;M4%or&N89{`~6R`f+G<{aD){#(bCM-()1Fz5;0-9mql}pGy5- z_<8$cG5*Np;|TquB&JQu>6f0_Q1f%cNW{9asbO=5Z@jlRNis%fTzp(N>c5Y_gtte| zPL|tmnlHvgzljl#<H(z~LI?0CRhfn_YC~>jtb6>3*XjO!(9vso|UX``&EJsz; zbRYqDgH~Pa;cdNr;ArcG@I~brqN_J)GhAnkJ9Vpxx44>&uN=KkhYY_6nUllhTy(7M zM0@$Ji$VCYXj&DmK5>jQ2mBuy_~h%L#}N%@H{1pqau>~Kr^m&VG1RGKljsV(d8ib^ zd--Xi@cGm092-1VchCIqkFHbPk5TY8|2esJF(i@QEdskcpYeyyGeiOe%QHMv9QMCo00x(MRg3?5knw)=&a-cq} zmVf_Nz>=XVnyTr3@?S+ww_4LmgxFCh9*G&a?&kK4YwWpl4~wFPAeZL&mXv`SQ_B?f zUS1=4i~$9}=)>u{5_RvkzIogo%_qV^s^H`@qYfZV<^al2SoX%Am0|TiQSyJq^^Kh zO&NT;4_;dzbYC>w-OT-kr^A-&zQJm@21Jf7z2Hvjt5v&eDgc^<$Q5pRRlGI=%hnb+ z=Zx=qOtF@(ccRmD7wbQgX^b>%q)VB3v$}RzWo%kCfQiV2 z+h@I{;jZKo2jPozKAzpqDED5qru7|TH$Irxcn97ZaE=FZ?x=eZ%z4g7QpSRY{{qdD z0+#8^l;zRdM?cdqnpGrJ-Pwm39w3A8Bwa@uK7W>>y9~H5vqLj4N*HOHRZejZpg%mC zUR|YIlS=xkUI(m+HUe&DC-Wv}7o*vNRyHD+C?TV7c|;&tq+WYd z37hM&x4xsY;-?4V^gaa z2H;G)Kg!u2J*QoE$ULt)@G{xzypDoNt!=Q+Cz*G!>WXqtx@$3*24w2uqG%ul6#r3u zCgw7YEOzTe>+0Hj9%^NkSpNMd0R9nAqf6liBh&eATiN_)^TQWfO6yxwzk|I293faD zAY)%KSIvhh05FA~hnddv&6v*|x^#1!n{+@5mThPzC|iR`R|pL zJ#Gy6X=H#}1_SI2wI>|=POU?lo zztyl5AZhi{-Lz(i4wKe+u|zc@sUQWAcW0nSMt&V44Tf5*J3S9c*MK?OMxV|&vDNUV zy5#FC3H!bfqLGmADK36h1tE&_IIx~;hW!HCliGlYt1PdLLR9=hG)+4W;+Tv!M{gLu_Jm^BF!5L+t4El z-stlCPF5z1n$A*I zG0h{Bj;FS6rQU0n>Glkj<|GCj1!L%&2cYaM2S9J~Dr#DYg_gZ3T%mBY9IvsP9X?p& z-^IjrGoUI3r;wLB8!s|uTBZ*NG9Ua1mZ_|k97c}C<)n_~miDwBVBF+1dLi0Czv#-zCjhAluE_>H=QhV6HpXQSctm^MLwfZ&PtBm1KSd2xq!{ zK`W^ZV%*%(<`0*sKo+!aK1pV;3$d^T(lpai>n_s$MVgrVrn#G-Ic(APhA)TEgs1w2 zX*UDFBGFNopnA^e`VTjsmY+h1mhNvBp=*)ZZiB#&dx3(%ZftTCH7PnGOi%3JGpOMB zEBdpGUMu%OL(;gJ`90Irwy+Wh5yD%9phQEuYjz2(HfB7=1sDHSsG+Ad>&@T44k zG7O-u93N^7nd1SY8b}A&Iwm!48?f&tT*^eguBhQqY|MG$B8mfBRr5Ul6L1=p{a`w$ z7LUx`s`V}QLwY;JF6K^F#~MYvsMg{&K5P$8^PE@YmWz6x{ErmaY50%(Z%WKDK-urG zbN`Q{YYva2@1n6A+ji2pQKQDz#x@(Paq^~d(%AOKwr#ty(b(!Y{r;Zk+1=Us&BZzA zo?GfrgX5(7X71@IavbBbzJZqd+VkROd#%!nJVT}Q6}fSO;P~=vaLzoLv9GEvY_(lf z`_lg-Iur>vp>APIf~RWm*7L!GiWKHaeh z%O!xk3P>+T&elk#rchlsz_CFhp=yq&%(oJxz*d3S)nQ1I72D~V^w_jTvaWbDDq00S z#?fUDX?aCeO(`;a+lD-U(Gv-|Wd$HX7`z<`pBm?+=SzU8ku+?zVoDg@4lh9=b_THg zn$vFuYBMWZqTAAdYYNGKJ^C!MY>h%FjjN0eDj*yHMF}ppPQC5vKm7f6R_T#;b{WHm zmJ2AO)fsnU_#P48u(L0BQx_A>iuxlvw+{xNk*}6+Jd~M}Yk)B5+4 z{Ko$AF6>V+Fo{#Zn?CH2wWITW<@|6tj{70&kiUF}Hfw4-90t(B>>CI2dNY{ zrCGjQ(ck^{>c5@i|E7l34I@5L!-r9^MuUlNx-s>=Ja`VvC1>!&(lDUBs+F`P)30f8 zbY=5r&f6Vc%l7X0wQ%=arPm)Grz$C-p2FZ4n|Glp{Z8Rc=acG)5ola z`E{e^36tq?Q|zIgVe%6XDTNUtPWoQbATmI~(4^@7{FgbF4gBi~E+Kn5ABDrtzPAuO z3SF@&3Ldy(Iplkpq{!9oupW9)Xz6;VgnZd>+?#CKDPDI3Dm6H+$}P4}|aS*c5~Ce_5;h>jiav{Z?}2`oH#Kw^u5=XS5_I zy75eiF?KkF%mdGX<8T6y!a4z0lddc0C#He*H&?G%ioKm^g21A(w%-F8{O#@tL9CoZ zz2baR8sQXxLe=qic*$;HC|UpNe^h`b^4j6X`TgcQyr)wToa%&tQW5Z zve7CXE#-}7?AP39eMfV;K%LPSfOj|s+ZgQmZOi*tM#oMtCz7v0ehqT z#p<}1dJ+L<$ZDeRPfS&R&ni=0w!ZP6Rk%znZ+TrpXS#hzcplHvIljr1q`j7WZPxSc zh#dzeNY4Z9f45*9WqY$0D`flx_BpTo&IOmDzZa|lgC@P!EI5%*#|6>&#DpB9P&m4B z2uNHA*5A|2z=)c5WN?|^DKf6TFT0rDE;`YKV2ny5kgBWeq-k}Voo@dZmmsl)nryu^9DA!$95Nz{=2mBUjt1x?c`12JJW1$m#t97AaGNz9*+p*R7MT{a8a*w zl7}Y?j5yV=m;Hymq0V0L^lk=DF7LN_* z5RM^%k6Zp*l!bNPZD_AP5-KyY#1JojKPm%6jZQ8udLNp2uo1#atifpmfDT3=49dy{ z=N_y?O3p8o)L0E>ROU6f@GDZRC26dRP{5rG*)0GUk zB4aps2xRHSX{J^B%?2$c)g3Q{z)N1tq6<^w1w2TL8l^0MSpK1)=zuEIniA-=UQ)nR z1~o#&=pg(1YLmXQ^hSp{5pX9!jx#>tKoD%@-AQ};NRGh~bZtk(Y-_byaGqcoP%X*BHlU+Bba{k}yVk_A8i7-SUP3JQr- zW~SQ%3fu(q{F6Cz2Siw>X~Dnwc~B$aJFgKD@;F4*Gxu2@TX;+BSO?bt$zHQd5}Eh} zU@XR-W-w)d98mhY&s9TOjU8CFMhoSr0hQSFfVv~^V{_pGY!kjz@Y^d8|M%$5LyZ0I%U<7>thJ$OLLwdO@Zd{+=d zU#wgpV+ER@L`Awi(*1AofuvK*peweTxh7tU2E-|PQ>i=1%}&~D2B-FOMO_Hq@W-rd z?mm)4M@LU;L3P)|Pmy!a3vFofz1IiNz18@WgIxDbxzEUdzUFwXrE&sIEfV15dHlGq zrUXnzt*;8$5-Wp?iU>&a1-mK*Et5MNp~|8b;|_P=qaiw)tI^7rw2$`lX~x+E1%rTY z5v8btZ&LI!qE(Mo-nte^nI|QuABeoQe)acRIet_0-#6GJshUnp{Gx!0g2G|VY1JU=CNTcqq6Ky%o`voiZ>-R1pHbI+sTb`=ssb9HUx}a8g zh+_`ssojGdb{2%P|*=dD>aBw6CZ;!Fc|fhZ6}(Le^b8 z8g&oX&6@@k6SKplobfG!tL*l~P@i(zlf-fX!U&Oke(&IV*}nnftFH#+o0^g^FnMD? zs*dWcS&^4V>Q@cRtAUiIQE3qAr^;{_fwjJ!GQ zQ$k>Rq++GosbU4)rZig{xeQoV;lI032|f*ud2SYNu8$e)sQQodwA>(&9rVCGU+UP( zLi5Agu&u7v+;<>6jvP8Swb18`r~AnRm58f0H3Y;%#@GIbR`_#UoTP(#0+ht0P1Q0L z(FG{BU1)`Q7)N5ZwoZ*GOwd~|`statF;bU%nawTy0e+nc(m=u9DsD zbTweOE}pKSTlw|p)_gRPP~VDH?RmM@t29Lpr_3sJa4`L^)V8&+Sqd-;b7dF_Y;^_g z!{=|RwP>2vAysEu_r36aXJ-fxz2O2$te!Y~W8V@Jr!P+g4!j?CbTE{DoMk;u`rodk z4F9TRnS?qqCFzZ5s_O=Vc4Hp(XiaSIhmcxgA>i>LAqK%`WcVu7v1NDG_;1a8HYzG= zns{DBrFJ{%pvmmgl+OX9?WS_REVGIwxKJ~S>Ez#D8?uNN6&g~*M#=MEcZbeQxcg72 znk;rUcG!muG)n3wI}t2~T~KXRdsIL10Qp=RH82UbAwcsD(j;Y9EYFBG!QJ&SHAEml z#jGB{Gl4SaY$<&Ci4-Fu*R0f$84&%cf{8+6K)jAko|6sb6C9EOS}NJGNqwggEEMdt z-6f%<4QLG&EtcG{m7bOL_2W@n|0d?p$2;G~C;`29EYEjLlY&Lt<+SjP>`kGijU`Ik zwjigLH)HwmFfDO&7Qh)~;t88=1ThKU_#|VA`@oq`l=^4(7O=Qeg)FzFQ~6C2v+P(Y zHwinp!NWD(a~!C3aH_6HIl?b0519TN_=}~a9TKgprE*{R3&~@mCovxIr0nHQ|3-OI z>>LdUl!}#+jEp`f!5sCAPaGs^8gd?8JT#%IKysGm8!AzLoe9pna2Hvg4W-r@;RdMs z^s~R#s&A>I7T3Ez{b)LOFPJDv5}|-Uq&9QjSzFDeQh4bKBo%3DX2X9v@d<-a@3Mkz zZ&@C&@%`Tp&QAaQ!OFfYP!hDKH9q0IYj@l4H!fhKT#3O+=ZIRd)0RwZ4Do3EYkhh5EH^^MKVJk-OJ;eKTv#(qaBu<2gag(}7C!7aIOG}%9mDIG}-#5pBOn-dxr(bI{z2J^d1sMj)s!AzU z*ntHP3&1D|^gE#s&j5*9l;{m>Qq6yvcw|*uz&zS|l$+?`ag-B`E`$*ygRAZ3si+Jn zZt|eUwKe3O#q0HJ{nZU9DD4wU>~7`35FxM}>{EDY+0wZEl8{EAVP0u|fe-3u@~1z^ zO{^8nuPW8d4Og2cGJ^n+z2~q&IhA(J#%!SOU~I9Z&7MAKx>H^a4e<|Zg{{jG{AgiM zv8QC;D{XK#tm4=N#AjRu2nz7otW0m+02>`u$RgY~aBK(rl@lEE#cj+ikhrpw2xLcQKF)M8U2k|m;TVDsKU4d?6Jr_hE1gnA z-YQ=b^(Di_0(pJ{#>eO3*o6$VX>a{SZdM^`Y@$Pal?`Bxrn{pw@!=uPzSMZ@u)b^y zMsb&GX0&SgDOpa|Y|R&2h$&O=*&Fk<#_dqt-96-0SCg&Ym0q=vX;*?7+hqLO*FFR? z{)CWZ4ZR|pnjTx>bvwWo=+){?%yuc=taB;CpFbwP!ef6k_{N6a)5rVtrr$KdSiHLe zFDwTr6m{ryZxe4UdFs2l*sV#w>0Hw-GHHI+J?A&%-Bd` zp*#?W1TVPb-rY)ve8;Yg55i?M+i^y2E^|)oG}8QJ8FHL7)yLHck)-wjw}ycy+?^(R;%x z9CrI5oMaNvUBFz+L0Htz$Z~v!{KoZ@kfh!#{@pHvkb|uX*m;r?CeZb|2~W%3%%P}M zooCVgD+*BHYt&1lu>I#{MYqjh7i)W`^ITtP{W(1!_u6;kC3b=@b@C9vJAWE}{FC`P zU(^HeERf(@?qm4+EWuz#qp42`#p%ms=2G(AX5*#sz>~8n*Sfo1*1hQY$C0h#>Dl>B z{hO%DF(ixq*Q9p8ZG<;jYPP!1W8}_8y4&l#5XxLKpMf zPLNr$_1fa%_J5%VJli?w>b1Y2?ECwBQ-0hYb>n8IRh|8#nT?5c%o6U(wGw2aUf(Sg z8mDS2*PAjrS~I?P0{DU#DS#FQ)la<^gd@8>-t7wef7rgQ`@a!eW_we}7GcU3#fM8l ziuj^C4xfvO1Y~h)W9+2?$^d2qn@qfa10K)^Q@cRC%=G;ry9OYstGCEor;YnBkm#^Y z5JoS4@ERo3d)G$J3HLrF_vNO?`;Enq$9gHgRk=Y%eZPT*Rdp@5M=x!a{VsUc9a} zY(yv>3YqSQH`_a4wFS?3ChGtrusZ=sc&krwfl1x3AF_9!v^`(?(TMi$?;cF9IglUy z&a%*E=GP+uO&V)_bv8Gu1|-D1wA1V%M{`qq3EpK@-P;_VfgjF^JRG2d7%gG$C~J3^ z*|)uc%COlGg)rjB$RnOSoLv?+a?Q+gForq%V*lmwKco*R2w>|PcG6Zu?-*4J_8={n zi2+$;>z54Agh3X38A}Dy>DyZa4nBx=X+_!FvF3@4B9h z=J=vF^o+&uCTTf26>1e3(Gu)WZ+`RMg#K32Dx?CMfkkf>ZWY7O7ia?*I@}JMXVr`* zkM!5I@&DRE^x$IU5sjZJF`7oVPOFiC(q~D;IBzr`0dKAe^iMRg>ds~vwN~JZv z8#YF@>khGSj+*VJwHRX{T%h*)I(dYKn5UR)ub2l8ZUkX5Bi1^3cq}hn#-?Ypo@I~r zHD7w`e?H17v<0`dW&b!WbGdNId5wyRE(7<;jg{A||7l>nh~$DaWYwPdB#D-kUQk_) z2HA^gDVr{@$$PI+o&*5wjMy-+j!;}~5#+ksM$o?~D1xG*W8@hE8M1KD6EMc`FnNPB z^YV7A!v$pLLchthO3i{Xatp=m-b*{W8!=(At)&Q9b`>3!(X$Pvj9OKp+(GKg;}6^l zQJnldlC49j{ER(|B69gQTdSDPbH^iWoe70pl@PVyHVp|b_@h)H1dl}+7jAvMC&E?? z>|g!m9`2+iBY}fko)kDrUEe=ep1I6SqShTC0!4$A2C-wD=lMe*fz~`}%5)s3XZiRT z=ukuEklf@H;9Ipph*fzA$rP;@J9)ij)Uk8n@U_R1wjcZm50fmFsR~o%T5F@D#?}$7 zXDuE&e+-z)ZSaYRQrvz)mL^UV$c{e2{ggD~A|zl~6+J4)n|~e)c{*nHeC7)$P!;G!d{lj z@__h}Lk?k*L}Ea@%wX+!RuciXk(87)w_)3c5e25`@QGZYBx<{G@Ax4*FU@)*9q50E zlB=o)s2XmC4cg!=V}}%+kzX-Y7jzaR)YdvNYm}q3#N0QsMWK~B?GjckBd)xehTS)g z#CSx-#^1r1z~fgYx5ezTS({MK4NIm_K(%j7WjlB%*SzYUy9-cd?|7bOsbufroA2U_ z2c^Z|K}-b=+KfK?pt!^N4jfUgEJF(`#Yx*?pbWtB1rGu-@8{2-)^>LB7$y)A5pp$J z%wR7aL2b`sMv>96tkb&Pnl2SAhKQcVrYh2e(F!OYlALjDh#x1^i1K9`zXVr-`Fg{F zKMI`%i47>=CAHy8W{}TiZxH3V`>Q%dR{xJ5-qzOkYSp%*Yoo4Y4i1z-nP=7;`!2;W zPp79u_{7BVWf1}ug$;TVL9+6*s86EVBQ{pnranG^L>V|AW2c1a>ALuY$<_Ao$VexD z&Jr3c6dY$yAoulMm^FwR-JF#@PrYxEYqglMlNpEjyG5u)N85tM+vRJDOGcqb!KXTH zZQjQhDRkk)%N5KhEE9MMtZp1d-#(|n@|;-Tc%ND6W*IV*&F52I3rfH~*ZqNy9IMf) ztTV#UE5{L+lGtUSK;#>Fe|;RqqK8L4j2s*|xY?Q&V3XJMh>dtUMM~s2zNa4G$09jM zSk}~_{fu5=`@O{9NQB$tkNY)AKDBUhizh9_9A{>Lz2znOJ2bRfxN2w`iv}YjxU{M& z3+kCzLsdu7|1X4ZJl`p}N773O9T5X3X^{i{(*Peom!Q0F^3iEl8<%q=g=k!Z?ieFiyo-5}ZV5Dl6~ zSlT780rOI2_s6zzvgkh56M#REsxlX7Y{GR?y-}}9}JVHTgh}O!=PL;vG zl|M(ZB*6{7B9mE!fi*5+wEY$CIl%wQ$8y?Ft?+ z*cPbz3+(GcE*GAWkO)XfiVB7azOGZ>L9Q@N537T|OHjU~{HV4L@i<_M{<~ zN-0r08E9{1OojvEt~0R=O_DMBMJuCw-@@xQrzta#;QskUo%(>-NmL@qht=KHRi36p zeQY0zSqvSX(G~g3o3DWaweOqExSmI41x=%9V^o-zD1M-Smfkx98p!~qn215#sgRh z8m$VAB>!FVAni(xv{=m3a;Xxmnb#17%5wf3bQ9{8fJuu2W9LsA5xv;Bc&3dyAR$+C zw@G!3&^sR}_k3Sw7)kyk8`(l(PuMX8nwaPOyFAhJp5Ghu1(bTcFp`Nua!y^i>0yON zj~;J#lSh=efoSZIH)0#7>1#4uiWk8o6FE`A*dczXv&j!Jie9tv-7y%V@({HD@nZ#c z)oLHZaDS*&k-IlGdd$qOj((260Ffj4B`OWw&v{tu{epitz43$^VEr8Kq^K%oXebPL zX*4u7D&bu4GI!U60f}x=U@fjE0k;GH9?Hq7_DyRwWYJ>Srv4Tb{)9qpX*M3?F_0|V zmlDjZsYjVp@`{Elzk7TU`JB0qWhQPfm05LXi*|@-(!9Rvy`5|4?O;fzFv$XTQ#xtq zm#VJfFHm4n@!TiuYev`ndRTsAg8NaEt*Hci?$*TY$XD3n8x?l0JDX!fuY z!jl**0xl_GXaFy{M$D*LGEG8KOWe87i8FI?J{UN;KeTU%wnh9Pf&7-w@tm%#M=cbp z>O;;?sa3=w#ABypqajvC(Z9#gN3l8aT?Qrhn1_&!3f>_6;g7mE4VjX>8!A zO+65eI5mpp@gfPCfV;sJ2m#d*ma6){q~G4(%=Ckqm+vV>C>3H8Qep z#uyjdlD8#ERAq_3+nn{8)RrFZuKcI%BfPjY8o?>zBc!{&W)2e$^QG2YM{;T5FWrO& zFg?TYxjGRsJs`{*7&{-87J-0AB^@wnoSQk1g59;W#?lapMpIY7)DEQ)$3O?|imHQC zbUvs_DY6Q7dOx0CZ17eJi|N_x37IvRXp(NcRrLfJXiEnskOnDTadvuPt!daP3~1L> z`S`xbMdI*Eg@;k$;f!7%Bu^Otofub}knJ=bcP$Pb`S|yqlbcDwU0g#$rlxUH8}!nT+7eQV&P)9XRtktBU>{{n8)vuS-;_7;>N@_cZg8%^CM-64@fbhs+Nq%G|6M$kAWB1v#BntC#dL= z4Me60@Ck`iG2!{CNq!Si(l@ty?Js#h!r|1w+x!lVib*eU2PxaDE90v;@)Agbb;B0Uui`t6(tu}jJCw3@ zo|&E#9Q^I=pcOB06tx6Ruumezu;1F!ke8-(c4;Sy;9I91cFfOGPeN2V1-HWu1q);x z9MFm#Puhsh-=3btnLdF_U)~Vk%MJfupSwT4gL8q6GnZ+xqT^3f( zxL4;3k6gQIr}w2(316e` zuzBgNMJE+wPb*h=NNG)HWX5uQ0)nDih3sIbE47Dru2zm=6Z@dJ z=g@!5@3W_qvGB(e)lrrQsfvn zWB-?`a{Wk}Q{|hAlJ; zsvFvYgSz`E1b1GYD+k0fW!l2R((K|!fhxzzxm5;bmE2_ru9FlxjD?IevYT6@nkhIF zdUuP+ECgw4OBJmY5km+0hfc(A3@7yi8w?MyjSaW17k`9w;%Dhf2z|a{)NM&*tBQ)M zqOvlAatXx=2V46z)mw*b$x(5>;!mGGsa4t$Vdn-++7#864GyM8bgS_d>nMsOn`HAN z=`(U8*PJ#+ZV*2zU+X4reX_A!bs}r%DUUeS!9Y;~9NHpW1gNMA8f39Oh_mp1`eQKN z*%!P=Ex$>d$!G4b&;s3@R$e*6kUVW1dOq0Cf5y;$Po+f&3|6O2Gz84Rv62S+Z*f^(Ec$8Jz< zqd|CC4y7wMv(tRj!*sNOb~V4bIwug)i(Si7^WP|rx-ELwDG{NyjZLy!P56ByM=2V` zz%)UrR|w766#P`JIv6KVT7E%CiHR=Kw{UxkSs1xe9P{ufusaqnZG4XZIr-X#& z;9T;U@Kw9yXqlF3Qek1?A3uQxS-$X)IqvV~DIpGEN9Kz~n_wf9Hi>(BMr00IEJ`QO z;3WfD8}T3-6dZ(bNAAO`gT(}`iH?Xb+ZP|-z}Q&mX$h4NY{HqXWskri znHC|Q-&J+%m9y3kIrX3qRxXnXbk0!dc+@#!VkuXKbsz^1GEJwj3&=I*^e6Tzcu6c` ziz<9{9;9!3$vCmIRNAA&NXMI|)q`I7MjcqICM>6lBjp87B_RON)DNW`g|n@3xyXs8$m$})TsuK}00 zOD$c~PLiC+%E&T)E~!)P6vD$foS$E3QX7D^vO6mV+ED|eJ;`WJLFPnqf4UKE6$4sY zN%@=m*8E}vYEPReH;`NOdG@Q9ciXuU6#^6kqGC>QM;mJ7BAAibwGWTsq^Gi`pw1MT0}VtKB}bsKf0)a~?^Dx4A~tQWOm8ye&Bg^-Z#DEQZx7jt&E zpKuQt#KiwKS0^CslG6h&KWzD7!`|)f$7J}q_4?US4caVfj25bW;my^bdkLRF?mut& zhzLm2F?(kykJXnKRP*CHJot(U3%QDZBz}{r&66S7OH#LbJ&h5|ovLExbCNQZFQB9{ z+YpVQjr;+4(-3I7CZ5C0vraC$&{EC`G zraJ$fn1#fig-JDr3D6uYraMJI&tnRx@|#wzp4i08n2Ga}W*dM_1=<(1^lV)?zxDUu zxsLjFhF`Pu(?*&WdWtz(wa|L9vPcufN0s4rsj2DGQOWa7VBj(4s8#+KiCN&{zA@87 z$ynaF_bAhy51bp^VSfIbRGo;L=*a1N@m_D3|6~!46OPV~&-=J2lm-^Lxi~mDzE!ii=BT5X&wN$U-u`lE@&=ni@GZ!08F!D5fKzgcRf+M~Vk^N!u*HtS# zN`(8Ry~{nBmTq_isE=M}yWG0ApAQeHw6yz9zSdqevl4;Z+% zy9XO6Go%Sj%=ZTm94jB)gVarGbwDn;{zku&mw5i*mldiv`h%hHQqr*n@QHDkO<9E7 zO2wlE0>VxhkKF1a6@TRpp)!`+ave#kg!7X^us2myRu9vjsGr2(XJ=Q1v6O>LmUzp6 zH^~N*qrBYqA2iasA#>yA;VUB(ev@CsasLiM&&q1<st z+H$fb5f5hTkrAck4!>|#%q%9Iyt=p~YMnu#Xht4b`p+zR-Tnbh-=8}x!S>j-4YVcG zr!iOPWMrYCy)r>nVERR$X${4ylQA)&j15*3-FTC+uwM`g!uSJFi33`ewI(117A#t< z`-s-96-LadNUp1}x;j}|*~~{h^xuXB>i5l!^(jCjKgrI*z>=aL+7JAv>aev8-?LvU z_@-WMnDey2bt^e=j(l)-Q95|7z+nk+p-%g@( zG&ACrW9%^uLY(PceG<8=4x3j2K#m$Xzx)G74twoCNpQ@$@|UYbjz;wtMZyD z9V459X5KGAS2Tp zq$(&VP=jR{^Ulo5n}U}h9Bdhn5ebWRFL4V^(o`R7bK{l&F`e{${Oq!SDZg$d{39y> z7WzORH@+Z9FMHagR(We?;3N1M+$|%)nj|DywV#>X8&kHDn*+hP1}`HgmlAF&ZU46y zik6nPI0Bov7(3JMyLD2ykoaJvsqKlqI2{54f-9Chs6C7mF(@WP=G3-v{g2P%d+hmY zyP*cXF-O{y>u?JA9ZeRg`cF{y`G3k4-}EVk#lq4f3P^bTok;!ScNqeXK0cJz*4CcK zw+u4-vJ_~sV&AU`2w1n-%u$5~Jy1l^t*8mx$!9p3heu7~Efa|MiMN9CF%5j*S&$+R zoU{Q}-J$EYci#=ic}yFx=3phk>v_vzW`14(lnEyv-r9tjx4J{a!jfg=aFl+%fhG+6 ztkr7t(iMNUoQrHypjm!$KZ*@_vJ-l3#yhz5hfsjgvtm?JmN#JyLSpHB%o&q+=ySerIZi{P#4$XDh3 zNk1|Tc)+M7`@_Y_@$&P_nnrR?sko;l=-A0~L3SxdRW`JQRb7y3H_Kgn+tLnJ9CQzw zG|sLqDVm4el0E?y!afH=5IK`;Rh8{O#K`0sF(IV*5LcG_cCJyKk-wsSk3$-my+q^$ z*cirI6!dG_13k=X&2Q!h6pjSyL3G^d*9I!J?s^*92K)?^G<-Rcs64qeo~o6jqD+h< z%M-nc8tR-}T=MPWx$L6~feNzDyXv#FVJ4V5LZT{8;Uj9+Db;1Z!Bl_@Y0OW~CyE}W z${gsi>J;U0aB%MK?!W_Lwi#;x<&%RM6{n*uQFNY7^yvrCq>(I_zHFM@w|6%IYm-0e zl>(FlNc{lSA>M@BB66iyI+98S0ODoB;iSr2v>I8>+j=t^kd()m7F<*!*Wq^gRn(|H zgJosrarrQ(OL{WEwp`aYH*=VPtHE(%SWwW28UfHDiq3WNjD8xv_tG{YCq`_BX2-774g{+xQ(4q{p!o zK-V%hJ{~#aSdtzX+)%Cl1X(LEf+qY?aea0U7{biY&yO2*&udT0&--~vC%oX+hrj%! z1$ZZdV>J)a(h9JKTOx+P;a3RQcnN?O4jn;oTkqg18jJr%p=2x1WXlC@A=Mk_?4&RY z0UHzqg^1)tJ0C%ZHwE{JCTyn{(ocn+AV5oec4r2y0d!S;E*^*DWK*!sJ5 z5l~VtMGqO9<0LgSmw{l2MZ*)#_Rh#el$CRB3~^5SA?K{H>5>vP#}fsK50&Lr_(3{~ zdcZ|_?XreG$hp!>V?(88MrBwjV_8lBV@;(ggsY6<82htlYO;Qxy=gEcxe%!AB*UP*?o{kyb=cz26YH`q7o=;lIURhe@)=IK4>div=LnJ_02v`Gfrc5~tRjs3|!;N*0 zwYQg#rAgO~c3U?m;TCF4$G7@hg@(z3BV&?_n|pq3iIwUdK#X(4vcJ2L8|Z4rPFf`J z#Zai!u1F4umu-H*@9b_tErcJ#dO__Awg^p$kZs<`D$I@1pTV-kMwY$%z(Dvx#jhlc z@*@yk*s`yoqXRQ?Fdva?X=%}x_CI*Ie>4e*@3Q`_0wE}Xg6+pm#}2+L)V1GDEmL*r zTt_H*6P-G4AH>4KVws;@MRf?faH|V3VTx>N$tQ=-`JM6#aIZyu>-qjJk{JjN`5&Sl zPR-tzGtSx`#l+ejwPHj@7#4@q5fv3>g()dwXITl-QwQnGeKSpvSK(&h2&lrqP)5AI zCm&drS%?hN#QqEfg{f!&9i1GgUS)Q-jN1%tXhE09C4x5HedkaFyGLc%iw~CUQiXdX zMaptbe-RRzYTkMe7%ij)CnhG321PftD5JnZV_7g^PC+SjQaW-AjB3j(e?19{jT#Gt zK?e~vj`)}#IOn@t1$@ESIeo0|cRm!rk*MGyf(;o=kC+93@I)*061nj)^K~~UGqV<% zVx|bhu`wpD;J)}djwBnXFd27qm2XRSUwWg(;_@mwGC86Hx(f@n67Vx?lBzFpjp#HB zRFwFq0`XjOXwl`arx(hG28_5`Kd%o+dnYE-zTE%Kn|12?a4!Ly!iM!UEWH9jG6G7* zGGk;$BnWEpBdR}fPhVL}UZG%3laZb|U)_`yl$@gCI8#DI zx;a$^;ry=F;NWK)2-xjCCE=9DQ#VZ*S_G&Q%wKs;wMl)H_nt8s%=<@pb8BTWd^gCE zrC^Vj)3g%?R)H1Z7Tks^_GpoXKN{qgaK?;_0Jn_)#&>sj83sWCHRS$%{bHb3h4*Zf z=Xao-npa0+W?(6x|C{^6+Zd7t$vH9JlbZV=lYy1BNY7NCiHRvs+VpE_X;p!;X!69A zvp#iFCN(^6(%3}z`ris}8*I5mkh*o(IElV)BsP+&&X@w@V&!P6N@RzsreTS8%f{kt z%&MJ6V1(@Utg&jo46yzOH&UX$2W~s#(ofl~6USc;8lZGv_z_#bu&dm5CYO086OV-{ z5Sr?M^vnAkd$d#4O68?(ehqM7+tyQ|g4`Ei*r#SG(;Au@tiZD+wozpU)pRm90yef1 z*PkkPsR81ULojZH+8Z7;;9#gy<6c(3m zDAOow%#&oKnJP%FT)Smvjyd8h{je^R3>`v{JV~`*JX=1Er*UoS>hrr2_z(H!7Qn`@ z>27!=LXN0nZ>p=S1F8MwbYY0shO27UX=BbTj~oHaDgse9Ix2l|OVTA3J;a?1(y)!O z_U&B3(cmk-wTPS!Xf5%x$#AR5(ng%Jy*xiH?)Deq{NAKp>C=7-5Ask#$HE72kcxR% zGkM}BV>;_~DzPXPRj32o&~N{V3N(QiaWi4&~e8z zkxd~E2k&@rkm9^*o)Sk)@luQR;p{BtB~f(f19&99UCpz$aj{k8o4CaV3W>jMDhn&+ z)4+cI1C3QU3^!U;3sf8~e%QFyuYZyf*mD=^IOb2X)D=G6xd);7Qqel&9*#aterD8& zs}=3&);TJ#bjWQjas%If5g9W(r>8mMQpD|sfrZ5+*jJxmb)gWOU0)yGe1|V-gJAHF zMERSFa{?IUI^)a(yBtu7nSU|F^8e5UBv;ocHR+W9@)WB2I~QQKaV_Pus!NsC=nDYcC!~OBbW2q(Du)KZa&+_x zF_j_NhZb*af582GzCQX|2b4)Jhu{a7)RNH93MyM|iKIil^HZYzqYZEK5CPP6;2hP( zBwC+T?U4t#%A7AQZ3q|fR5g6{ek?;W78&?dC|)iKuSnTAJIE85nHV=q$nv7Ji>W#7&Rx%!opJ2!e-cL z=%kWoK_Tz!0{@CO^5_xjKQK77{pW=EGCwFdG07HB)W$dn%K}-T>rGP9Gc_aO?8XP^ zivcKYR^M78IszTa#4-tdN|>|_9|m4jy_p~${Hm&xZWiPIh<1Hj^_6i^H)BK5$+{Ix zgehX})oNF%6c+CIPfSGBdwlNxbn!}NP7+F@p{1AK`T?M0if&VoD)$*)MtFB)@Y&8N&pLS^BwxYB!u?p4gQ8U!R zPXZ^qbt86LN)rWsbIXg?)4Wg8Wjg6G1}*n=@j8n+ZjgB zlZ*42yXc8xL|~W`kTJx2oI3UrjKqgIprD}wWssDVRLQwkC{9Y^90LZIYwF%(lJtl% z#SRAef-d&~L|o|0f)NsbV}m5uSc7)$!?|NTFeJWra_~kHlJKZszGow)TTbh=%tdk7 ze0EAyBeN^4nHkuMNjh{jLk_TmT+Iz8%RSM+88mVf`9sCqjwo=Xt|eY*Q6g~gIv?n! z$;iJKG=evmUV?hAmG)&(Cm2@LNF)9>le-a+@!cyp#S|`WuI2L;W9^x`y1Ru_LSpu! zYTmkv{t)CLqa*FQ>=D&jUjy}FlfNvG*c1BXjaqh#1bsv1CvhUwf)Pp2AD7ejEwI~g z)bavF+g%7u1coeq;TrQRQNNP3(e^VD#M(W`G^L`lG3>FZzj==p9_P>Cod~pL*lVzh z_Q*(SB@;!*9|~0#UjCp9^P#9Dj!^&4i$A{HO`h44pf9wk7g5!sm3AHaeBEDpUMc=C z<_4#BW(-#Grot^3r&F;fB1Q;`-kPB2iydz3&gm2Cz;|{szq)TrvoPX$XrU{}B7uFw z*$FI%`4jrPoQ2DXTv!s;-;LWUTw?nH(rG(5RtRLjRdQu1H9TbIGC_*}O|b^Q z`3&aT$Nt?kmAWsut2Qk&o%M3Xx_#@j1Gu|ftX#ynFEiZKIl)2XjyD{9JU7*vTia(2?%=BHtVdxpGqc1{QvZmH3(^S- zT$Vd_Dl;20Kt8R@U zT(*SUm>M=%JE~iq&`VchXO^P$W454+mOlpLn3IQF3Z^$8p%k1Cl zuI8)0qM(+0v4=>|sC^)W2?e&ZCz%k?MkbN@Z}tZaJ>n)Mj%~i9mRD5dZ|7n;Al%&2 zGIEIhL(27CBIL%nyaomb2NP4k%a1^>gM{N0&+!nSUgvFNZOuSKD1iYerqU(gK-_MO z9i#fpIf<#{{TZ?mNL)u>I@uM@Fu{2SRgvXx9%F!)AeI<_W(5M(5>AsOk0hDG3ssl@$FunsF^YWn2*h*VNhpO6nOJU)Elq^7-mx>8{ z)U%PPf-3_wh-$@wGKnd;w!6ZM_mgTQJ0yo!kPUWA6}*BppCj|%e8w7K%JjlQViZ!J z-^Q!WSBes`?z;y+rtvybCoMMpUhR1tHs!^{pqe+_i2(C0fI2fVHEq3L(k-j1Iy|fH zK5Up*bXjeB`&$PK7b4&IdtP<`u)1=rQGU-SJU%l9c)K^sXXt$!ic}UzO8)hAU^Bz~ zxgXTktl61V(cf2x39DbX+IEWg}#1O znBS3>mH&mYgeo5+*9n)pFr%y#Hx#rmzt{_Kv3_xPA&etKiZ-f_B?Xm908OG01l2-1 z|MF83r-t*3?mGLFDoo=64k$g`K47~0MFVb=x1z#E)wpu@SwNqUcN9r%dNVl0h9^?*d6st8cLQAF?C}g;omf87vidP|Fwy{1X!Jvym9z@zHCO^33 zQPbhzCbe~h=J?-7gI^KD1{w1?YB-O9Jt>hA;#2HjwUh;AeyeN^gVXsH4$hWFHAW>c zOL~p&$Z7d?8b6DO<-5qAou1;8669$FqX0{0pHI&&xw;FQstjOzrp9H5d6r`B`w-Vs zy@|91l|Ih#ux*$kq0XOfY*UWP%gQb7CJ1KeXlN90I*xt%XO!GU!`V?NJ$Xf^^W#;4 z?TWOcg})_g#qE8AS8S9pt-PK>w~f&+lL}fSFY$hoBT&N$kYrwmBZ6%kFOyd7jEC{o zmbLWN=#Z)J`2%@{iGpU4v#Jq3PCB94pJ3v#$^Db%#u>Q+odeK>G@zNup;F+H_ha

p6l5KSa1@yDVRhUSgLc2s|+;ccWZ+3O(q*V0rd|w{6VjJOLYutUn6b1uK8=v)~aP zwl(HKg$7g3`IA^9$#%5IW{U6B=v1L@endRRLB~`tB2&F-miXHkm~herD&SG*P0Qu= zmCSc6RXoIFin^?&NQqQkARyCgZhPqa(PZ;M_p&jWZ{4@WUbyYh9CtN38PW+wIcK7) z>2ZDXSHQM|46HK}oQg0*{XdS*GODev+rmJh6u079w79#y&{7bFcNxxtM^3LrN=*{kY*;c~naA+mlVlqdi?zxn9XD zP|oF2V?zA%!!#g2Ii$OM+UVy`ir5^8cZ>Ulh*}Lwc22We!P9n?f5(f%643g06vIqm zeDrqvhT;vnP(S145`b73_!K;nR<^w*oz})(W^-BnCuG!cnPeO-cnS<-nq7TcPInmn z#5{d5Q?u%My{WKdyb4_RHGLit0*_$l3cbqZPe1TJowZ=a_)u)$LiYIh z6obyy zjgxEWUQ1_)3w1z1zx7U31LrNX0%f&e*N>A6hql{_5($?7jQlzPZD;1oDSENQd+VvLO7QmPSMo}dl$JqF zh4lKcgOT<<_4*oFzRK6sHV{M9xuX`mQvb;<4;2hu^a{EmJcicrbA5-pS&KO z?nUN6wN`-@74~W?-Wn&fLP$T1(g=!*nQ-an-(lT4XPndA z92`h!?5dTj0M`u=>u6E(X~$IovIdncg-O@pGFnsffz8W?uOz`6TAI2H*zR0x(QBQj z7E&hiUn|R$+)P5lGlPqYw8b`7Rw6bX>9pGFbX5n-rzy@|L9SI6m1&l?Jn5G9R%-qJ z(d_k7$oeq*w4=jE1$($@GUG7h491cnVWjY-RWdKZ{937+A(`-MJEq;T05T+Yaay9K ztgc|8fp226O!-0kEi8I0h`Z!QXjt7T2{oD_{a2@ZyS{JxuzZYMdsD-jE7!LWy@ab|{ky^!|DJMXlWjps|1 zA2cc13Dmh}OU)=|V1viThK;wQXjX0%qr1~JQ5tM`Hr`%eqc&$d|L1lL!t|emWxRM{O-5(&o-M@!+>hyf`d0`?9GhLWy1!}wi>7fQI6dIl&DB^CcBhc;ZykN-{ zm%802i7d-#W{iVQOqRZhg1=b7p`Kn8TAsioiJg9n7JYeSvD}>CzVR;Q^4$5x9K(Dt zVUXPn?%iiF?oNJx{j=|P;hP;b<$HN$ff^-na0u8ax$j*x?`$8v9}*YpCK|mL?1@%p zo4Vg#jI|_N$7ZQfy%xgPmufl9j6Z<=9}(7Gt&P>Box%#g%Immyp00m<@@!>>L&3nH zqVCd?yBcNf{Ha|r#az*m`r&!Dqj=o@zyMjONAIO<<|u`8CG09;fv3h`$Gi18I`)g~|k{?LC$Ua`h*|?M#j42QS*z7mF;P=$0O;_4iAhamuO* zkk1#!w$wYHaEn>oiW=_)mgDrfCNVLitAqg3uG87?jXnX9b)T0=UbiDy4-b`|!4fok zY1DBo`yjvddYj|g&X{DKxs~kf|8jc+3q=rRw~IHirG;D3Me zf0lPtB!z_`jXxls`fO3+>ugN^8|i0w#6wqc#+Q*D)w>oEd3|9_SX2CuNu7{@B)@g$ zQFp>Ew)Q6|fRU-Z%esDgzT``XmX4%aqL&=$j{ko>y=-0{*866z0;I#()iW(E+JKfQ zlqwrHHyKP`eo&CN%Va2YRqNwd+#*j*(rDUkXzCGRD6#n;F)vA(T^u<`niQK9zJ7Rf zb$5WT0LoK}v63)Db0exYa8bblY&z}O-Sk0IhtBe2I@>;A0I~sY*~+6zpiw9CeylSG z3P?-yO8gYZ$Rb}=iP$j(dFJEdI^{TcIyzXM==cUNLA2A~H{LVa@BKpy=%Q=ZB|>aY zh8Mnm1Q;p{V`Fh#7v`ZUNF@!ycV(-{7yF~yfp4`j(c$3)qZUt0B-lir0QL)iR5#vQ;UeI+m_S2Y^Y~x_EX8}15 z*48H;x3`TCD=4hP*B9$pVFb^8P(oT1+cMBs8@Hz;FRxvp(+Ck^A#4$g}@1zkw+&4(X zz64|wO}yNC3JC1N39!DMs4X3s`lFx;IFTL`D-Djo=5V1A-NllW?ih+ zI6Gl3EN=&g!(iiIZM=|s z@qRN&gY6#C(n06T8N&Fh--{=&+O0MEx+HLHV$2?ulXvy*sGBu?Y0Kd61hCCN1Lkfl zl%3fDFR#b!XQ!lpulj|e1RC{hfvUGopp(WdPnGO;p5T~mbxWUdV)cuw=k{sywa`^* zWoLOa7<*J+Y+5e+e;hu3_+Jw8c$_I-Oy=J+KU^4eyTTqbJ=S+#BS zA*NA>0*iyqei+I%w0M5=B~fF}nL^b6*u~Z~)O;OcgmmTf@W{7OB>H+2zpq>NuVUtu zi)vdQQZHVKqx*C~u`PduJy!LvF0M!X_8vVt5<0iA+>sfl^OsYeiM2ode}%Lc7d!HE zJ^Y85YabN6ror_6ukZ#5ngPA*E?A?0(J`c^If0uwS?nDA@*L&Pb^rWW1hzc-iQn^5 zBMNr5!qW7r(UIfgR5^$GK5R**<&yVHNTda`F_X6Yyq4n>At7D5F}9Ep<1i1V-@S+@ z{#bxqj}XbT1Obw)+-T3+9>d>*>pzAO$2tehMw9oUCM5OfMG4#sQ*d~d)=Jb<3Gvb5Y>_r3rE$=>X(NA=n=2ElKPzP_(F zxBt|(PkGW&E&pokeakBw;b)?pE~vSULvS9<&aO%^j$)ysD1o=ms_gjn8a~p2kTy$jE)afj&^K{6XJsr^8!baof{SSxf1Hc9D<4a^8AzLb8bNerW73(9u zyCg>J+REDr_ZRGObQwBT@T5*HEQEMSSASQMMk=>EOY6(5jx~CEo~6dAQqj|6RIvzG zlE{@ zv{=M_L?{Y3W4G8^*r1UN!ej~zY1R+eZ$HbJhS-18V(yzsPOJU_%C)W3XIAtj0)8Pk zWPiN0k*;d6(iy;|^h&?D4zY(&LHc?9##`L@(N5A<&4mmg1B1>ox5^MmG&99V_+J@X zVk#>KeF#Q)ksb&`xEC9*nrFv>93@!p)LZ&~{3}o>2M2oq5 zrLg?n@Yz<%MsD3Tb2VobpJ=t231AbQ&*Jt}fv{^vbldz}y`HSB=kD_INgePYqaay47|j;b;n{a@#6R3Y zysc}}ajI!`p{y(%eau=ghGE92)bIo zkh3cdvQqTxC>G8h_aWnP$o@04WwdHR1liVFCZ(&%>7r;|e-j84E&s-k9Ung(r>&St zV4d2#X57jfMo8OaB^MEsH!lIIJ;Kc_$}J z&vyS$SQ3^HNz9rS>5-9=I$)9DKKRq3uJcy~gL%W7$Ff37wR7=m0=G`I1n`*U&jw%YF=VlnmVGhEndWt<;L$P!nSb;O;^c1x z4%g}I`CAytaE+k`8i-zI<~T#y0bXZTO9=Y73iP&u1Hx84pP!u_Xe3{lXh~R@5o&VN1L_p z8A|7%uH)0XS{6Dc`42*eZkR;aT=ngBW#TI3nA!F9@|^be0LcJUVmjX5itUuH=ry1E zn~N|)<6hGVndo?dIu+KA@e77S&5Q zwN|{a0dPY!kqBzoG}Q$7Dy>`pkjHzHq><1BS(0O#oYEk7-XE+j=VDeke%KSJW1A2r zt2T{Y1-sI)DVKZ+KrlXK!gbKB8*ASn6J?AZ!nCk0o{PfmBhWAHD)hJfeP>@p`S4=Z zM+|%!3XP_Vi47uct(Seun0g6!A2G}^wTHO1amI=Q28=At{iJR6^mNW^q)oAL(2M8Q z)%7IxiL~94GQZM4NI%KkF*+Pu&R0HdwvE>m7A@*Q4Bf4=iV8*PZJnyF_D#w8Q&g>~ zxKd3J7)8jc$2JxT#H80D4m_NXPfvg2HczFLRf>eO(8@DiZ;PQA6fqXbEc&}i2>{WI zf-f*wSL^=Q2(2cj-+~xw>K5XpMjt_&|Gzu@h=Cw1~U)EtRqayCr6)T~H zH`i?c`t}nVEOUGyO+5+ueQ3qft){R{8RpR}`d`5+=j&qH^P*q<-C4AEMa~rzWd34h z>KeJKxyjeuP8;d@2FXcfMLb5}wF-k{Vlb8nA-TT<(9Co&gZ=W2dC_Z^PV1yqZCqSx z*5aID@MPx+6&253c7aYeyfF& z5)wd%rqIq(Zva%WJj<_s+d`l|LaUmPlCWEom(0a#TdPeEuqLP4=oS1QfXSyX9KF#R zGG+ev9UoYB%$e)mFe5?}GMf*^r)W(PKEh*Sv*7Dk+gcA;D?JPN3a>>*|MPtS@?L=6 zMRC&SUh1O!roOfVS$I+=b4~!dP;Cjl($({pwxMyvsFfg3^I+q=b-RE@-(J^g>ZxhN|c>oi%KOZk&sk-l@` zXVW#2{QYAho8x1??5?r>Jxc?sE?b1F8MT+4k)n!!#YI4AhF#jvzaqV7(ep}d@kJLu zUeMOnR1@dksV~hN*XdLj|F`x=HIEY0j6v&%uvy(b6QDH?Fynu5JqKuLP1p!jpS43; zCQsm>kh*#Y4QS8*an&xBEfu($nx+6GGH^a1Bm;?*x^PL=C;b8p`@ihPBI!@$VQo_i z^txbjfBsA2ciupd=;wGbR4;ka7iglx6P-$Cwj=E;gi_e4^Cd$_sTd|dv>KUHt^ofg z)R@T2(+&OIB0cX6|H~Ldzb7UbTJ1jWpP@s2ETJ9W#Y{wD$f{kWk*lt{u8knery@V# z+3(-?S1z26)ciLe@!t!=^DHLDjdHd6Oi;y?ZjdOW6F0tI;O+6-h(u5^kD<8hY3^3l zTqENW>R3-1!5-ei35aFmUA1d26zGG6jcfRMJbTQPVrNC3b#+n(jl{^*%^#O6@3DP_ z%h6e3=p*JiS#6&V3UjKZTRGGE?r+$E-7>11<XD7#U3`gf$f%-1$qQ0v zDA?gN^LhLLLiIEEzv-e-B$B&+jScJl{d{(m&{bu3b2b7^hNG0(nU%_;R`>fmmuyWz zmXl-9eyPhe&ZJ^|vVO-je<4Vag`ZG66aPOyj7YD{6-X}3Xwne0rJ^so4&D_uG5g`g z^E?8{+2z1E;nF}it5VflC~=3Kw2^{?GVsBY?r~#Iu%)3m$|+u=-q&@NdgtR9_=2o- z^5m8&uCI`pR5;Bz2LP40qn7!80C1yXN^D#T)jl0C~2E!x9r)}<9&RKqz`Zd%zwXF=xF_mpLnluLE;0)Pm zI2Rdj7Ukm@_(gf9gO1eT{RNTD(^tm_+8TE>CT&cTlL4`;7ZX5OS-jL%(fExD2Cd*zDh`PXMRHeIFGTP07~x*r*4TS^dST_d6=5+cDZe)jSV%LgAl)el-yPD}IL%l4<}{c|%Jh}i0B%yE zpKfdCEUH;UWTU)S82*6(3DsTX`5Aq;G@$aRth(B&1e$0GN36l*h}ZV;Cl~5QeR4LI zNG%DJY-MRM-0$xF+nAo)*p^aNXAncXQjC<(xOlVuk#As>)V`sxIa^eAyd}wnnb>cB z`jKfLF0{iIbEyijfEk!{|IN;=m0P0;X5!)|PctL^oC$pHQry7#W6oJizA;en$jEk$Js`_0$LCVqUS|-sg(FvCcGOG+%?)s4r z+}p=yfaRD$ot{vqSjD#_{ZO@Jn+Bhss+|D-Z+SJCL+x|1m#xk_0VBrB6S;rqw2!bG zQ-&M~&;3YH)?K#~?U?%X%*?0s!pvJac^LJI1VrA~Jla%-h$%~I*89d%1%u{g;T(xo zi!%q0j-yVSy^CNgrpqq;Raix*(SE${xh$jGH>Hz6mYHU(o$p0xxz8&9Fq-whX0jV zs{TfM@B#P+-)@wy67HFaoz&$PG~z8IcDk3u zA_-)U_BBjdCeTyz&dyntyfkyB&-D;H46FUYYYC%IW=+I7*Y}3%$;X8WK07UtX&o%A zdh)YK8&jh1kjyyvFG|Fs7%F1^J$380f>QMX@KFk1bbv?z{8O016`v^fgB z@?uegk1Kisxm9chxvD7px7&JAkFu?oI&Q8;zsag|NVKt3C3Bws3f)^k=ih$<@N+I{ zWETz&3qS~gTG3=3+HE(eMR7i|(*1fK1iPzvghK0bYyMpL$B8_9?VDK)3AA~9=ReqA z9nM9eQ=Y$WpS2ZEfxB@a7m z+$|j#jE0j2!o9K`_cwoK_F(@*XEJ#-{L}CQxb-k{Z)9L#z?9)?-HW)^hZ&xFf8-7a z{@25AY9=z+z6U!)b!dQ_*_(Oa+>9DxxDG&!jWZC$>}=X~9}~Xnv)4 z>de%BziuCjta_QN zq6ds-gX?hJ^VqCUz*fqxB5!m49gP{v8|@Q4^rl5TCr zX8vj5eq@j@nyL2sc<))*;CFU&f7z(JFkcQvBD-2KGVffeY>aXDk?@$kIHXT~#ZsJ5AAyWwt}#Bx@A%JR@}DKPEQcksA1Yvw`O9HOh`KJepPJqv)z~ z<>Y)xd#0rs*dZt2x_vBK^QL!HQav02SnCIMEPNKnZ`-3fr#JK=h={r%eHV+$_N!>r zPR*kMMZH{ti;0ehv~!38zPF8zm^A-01|JH3u89Q}W_A?o;6ykTTi=n&Tb&InVt5@= zZADpoc_M2^th<=|V6SH8L0^O&52L@Qg3jwkX8YZ~AP3e^0gCQM@8Y3UTTrIRB!t}0 znZ6Ae6*p4)Af_yYNq~LaptF@$0IfcGa}lj9Xb!vZPjE-oJpLGw!pr#So8uU&<#R=O zQ+|R0b#4e2d+jIWX*|+~J8t7yBUw&S#`+z!6Df>8!-2_I z6F>3BvJL+m9bqh0&18?!X7$%tw!LxX3Hq?)p=~kbiK>m|OLW&>>w-&tvaUT^&W}7AFeFdA5egkQ1$g?hX zS62J;qTdoXgP)4-lTf4=>HL%*Q@n2SWCDUpb43sugi8B<|)70RwI%psGw&y8sa84+HICuGR!#)|+I(`o1q)-JLiQz&9QwNqZfOCdCjs68cK{hBtv z!DFYo@8L~AXut?%I@gNWRqq9XlC0G)?8Ln*Cq)TSRzvXuS|ND)Z)4X7rgWZ^=S+k# zi8bEM{)M~2wL8N~;+A82<`c%7-E|+31u~ZP|W z(|!%uH8dZIvmAC%_s6(WR&o}PaYyx#g9L520zEy5*6{7eXE{``LqEC4hz|4*u*oU| zd&k(|Xs?|TduVgrp8Z2^h&AJN;h?s8;L_NG4`bor7xni)KI!!*3yQ51a%_#M%i(4b zddRddip%ehfypQ=O#ynXASGShG42K3Z<>o2b^-4yD(W5+eIie$A*aw~wF-l1K=l0n z&5u8K(fKZ>g46?S@}GxVJj#Z2j@7v>))*`(c-VPHVnj)aVn1$jN(-_B)5dDn>b$f| z{v{-otc}l#JgMrV)~uqjGjr2ueWdk$oa5CTJI~ZEI9r0E^aeq!d*()yTd2Q(3hX+@ zh4gg~U1ECQSu*Z6a~}DA`h*OUy;D(_YPIG*ge1T-xI?*^xe>@ki-KBqA}Y~SF_rYy z2Od&to&9@>7FeakFg0mYd4oQ@k|e+waMrJ3i>pX!{x*L$3n^C}D?U=itOFpmJ;SAN zAy2i+wPSeRxOnW#JNH1gjq?{+`7lpj(57JrDw9sFxT|vlsXh!-dlZxACdmw7i(^GkCd7iSlRu@PR>UiRjm5ZHzQbPusli7vqDvOEmc*%&;HW*Ti^o;vR7Wb zDTmf2wkem%ClFpC8oD*(7HK$h1c5{m&ShkLkSkfOGyAdI?jN#cFX2pPba3`4>o?9+ z{DhiNO#{X){Z6u3$Hll%r*LVL^)iYZ9vra$8~^`aN+Otqo6<;DOvvxIT~WRB7VtF| z%BEeT*;LHRx*Gd*@UTDEd4@{V7}pp!NW#lU<-cRXAGh`gO2cX;d&~0xshUBLBIfXwBIX`By8a zj$R%+K?SIX#(yE*{g2pr--gqEbbr|Vmlxf2NEtf!WZteY`NG_xPgbTS?8{c~_$FlB z6I8gSXq`m_8wq2(9{) zNGkibBsaF9sI%lw>%zsy7s}08ALOTTzo8~RHK+SGO`*tSg5rOQo#4l!jqF&iIp_5*h$DLo{m!2H* ztDaL+l5=`SO4eEN^l|NN#r!?SSm==*Zgedm8|C1DQYyUvJ)TdG=??THL9E`YkMru| zJ3In{yaODtzzTZ#W@l+$<_NOIm6$rHVdZ{|K}*P7!-$)cogVMYCxOJ0+SvrBC;vb? z-|Qxg6J6?QSir0#t>D&ikBowRcs8%j?Q90rE4XI)cj^EmRkPZ2T&zf$I>^WA&oXokP;2NGlV06_cc<@{@fq11IPvt3aOu3^ z!)Palh@c1(|JqyL*t9+BI{@CDS; zE>ldLFe*Omm6K<{G+sF3_v{t7&3lC*2<&q&gz$dOEMg`5NE&9;sSvA6KVSWI_4Txs z!9RLa!4F^`^?t_7sa0ST)Mk<)M3wro2%W5wWNinPu^7_PyFe1C5^;&y9xM4QXqk8z zTE@!K^VVLa-^mR>pQy7GW(AM?|c)y~}y@sc;Oc_k~H~#)7n#7vkkzzL0^Y}XiUal29Teq#1sbDyRH4|@YY?7;W zm|NSMv%F6@rlg305iPWR3G8a=<^fZ%B(-)c4$d8DHX4ehH7Hg62(*VgoWfyh)~WPJ z)Uq^e`^`&4cl4vvV~EvD25&_>Y1iTmBJfXgAPN){vx)25lyv z*C-WgPGI_So$3N<-*rO9uXhT2^=+C`^`-%lebe!Qm4z^3AHd+7MykKI6eIe|v|}&j zi6;Zd=7Knnvd+ekmmuIb@ubr(KDO6}ex+v0`0Zhrqkifl zYOiiU`$*q)H!-0rfJ-1eiKoJCf>5))C(JDd6+=l0Wcg^9J~wi{2I1r35E!tF-H9dM z>-T>TBiKwf@TYrj^CIG~g9I8#hOf4JW8}qEP@Jq@iomE(ME{{!fLaT1%FA7j*g$$v z+oyydXJ-Cch*_h;H(lsoK;CXnp&YTlqUL2aUQDd#;)ULT z!SS|Oi_R%6Y_mMEjTR9HQ1D z0W!8ebCZQuy2B5VAr?l=j|3-852qdj<0cYmhsnFVcf&2&9s>~z9c(>iM>v^jBAk#= zVZJQfw{alcC98Iy)BE}fndq5aYnLDtGJ`+y@K_e9j{lPoUJ&2B^*oJoMFY_$cd*I} zep^_pPIW^#_fZ5mCrfxTN?mHW*Qb%2)6Q$OuO*g85bnSj+fGjN?q9!_-rwvsnJxQS z>!*Q!6(@Nfo_;>&;M+Z^oEcMV!Rt$ME4(DeN-fp+`| zLMNX{g{rni3e1mZYf=JPB6G4X-0y|`u`ri6vwr3cB%Hz(z!7t>CA2(h=9AsmvmvVJ z6s4?(rf+@dx=V2g{J;Omf$7#;JZBszLy2(lxa_LoyN=aHk!}R*QWQspP`&0WGdnzJ zxoN`6Zt|_}r320x|L&#ls=u6UML<;5>9A z**9j9vQyV(f*!o^!Pr(?t)jc1Lu>9}+7 zaaBwu`Te?Q#Hb0nPUAAoBnF!xXXZc4v)`&xntY9`j%YGz^NGoNa;{}OlbD#F0?l-uzGBj5LZp9r`OtLI zK&(gR^6-b-KC{WwrPeH1SvevydJ99tAlgR`;mz@O@An^@c;;_CJ!|UHC%0Zaq+2Z< zufJLy1{W01$C$M1LUE!u_ODZVc`Mu=c>_!uNw%&XEWl0-{Lo!f6Cj`WyzTa5@}RAN zPo~ZRNFPEHC|>{Tw&?+$rS*o=)P&)auKO)6{KwWqWlqM}1k<8#e02wE+F+5m^$9YV z@6Lz7WfQ6V3OR4Z$x9YG9L{R&x{c7li^arhB$H2`A|n!4-e;z!MNX@W@B}rSUy?zuL%YIKOh0X~i6Ip2VB>hyPjE zoP|1%mDIEY+e$wS1~FX*Wd~!aVBZEI{;7OQC*4*&b^J2RRa0%WR;j?c zip4=SRAZ~O>5f8?SW(YKC<~b~DyAEfimxwn+gz-H#=(6TdLVfFGWtn%cNO7O=ysB{ z@2fFf9t0Bat|xE%H^f*_*W)O$#_odL(rDGT*R~$o4OHl^GfAm2 zTC6+vKbDQD;p^#pEtzq`A&CbD@oy1u)4#uPDr_(2&6hNQea4hBJi`ULgx*&mTOR-@0;j-JH6j{F}mW!~1e!biIaYIMeb(L_{*&A*m&22VfTgI#JMG}_T{FiADYXGAFOTu^}{I@ZaA?#JTc5rT(IL=dS6nuSO<}|Le>S_1Dk8+yc9(FxF+w!`m*Gdb1A#Q6TA4kK4v3 zTsskh4(-48B3#r~SUY#cqgS&P{PetzXgJ}l+uPcje(p48dLD;mc;$LUMlU(TC+p|0 zzn5_;(6AYLImc6c(giIQgs9F!TVLJnx_g2%{Enq-?7V`0J)gJW=7sRYIzU$W?SDOO zZc^YeNL@iLc&;yf-Hq8sHrQ99mt?X~_FUV$+0N3c_oNos_uMq}eylP*65-?E57ukw zA3r@uJCYYw>eWu0!3nam#_U8V(ix%tz&LM*;w2C6j-{V8oTzD9{$o&ZOPT78N4#kN zcDqKG=^@n2qP<8QNj6i0XSZ@nq}X!RQW_c_+X>#m!ESzuL(wCv9Y!dgGYYZO4cRm% z)oBHx>F|us9Y>O7t-3FiAG$85Ol?aRg3M>w>NjSYlG`v+OlnW!%t!1>K%ml3^(8V;&;L zs;^*R1iN0HN9#euvbLCjJXpuWjj)Prm zGLy?o)4gk&_ycA4GM;-Lr}?l76ibKBy^n3;dnU&?*uy|t#MX66UJUjEcF1A~`*&SI z5@UiRk={dU2})(z%+|k$64&0g_^uhwTZxD5^&6>gPO0CuGp-UPe`{I-X7Yg{RG^u8 zTg~#D()D8;uR-M6xAXhO-kDIXeEG?GVxgta>*xU%FL1c%q;V|-j5=TCGgNU)_c&J(M?I!ZsEWi+e084Cy$teblgmyyF*C(>e+@pYH0M+#msk z|4HcArGDUHA3(c>QgDmwjwaKFC_IW9B!jBVM$N}5)8l6bv~WuCR9ZYBUzfgEE5-G} zFfuk|Dd;8RaE4*re(|lbk(YSJ1d8eblb@n>atAtVd$%ysXMi)do$NL1xB|1Fy2;tJVOY@F?) zaQ}SJPQ6Bzb?lR`bN^i}*ZoGvYoJvtI6$hQ^U}v{)q9&=(rG=FuC#%?*^fp;zr+s% z?DN9@JJ{M}Os4f=J3_6gwxuc)H%U3NGV7?XIokgA9^35M@FG>Up)_)1SP@7KZm^=G4SNN&~-;TU08)+sFK zm$`|-_*w-~yB`PX>6!fuXmjIqWX<5Q+3$0Af)1^*{ms|aGe__&9S_EH9ol$gcq7Gh zq?OBlw|n?D1Ai^Re#ms>l>^QV1|Hp!pYXievuY;U^YldQ3C=F^ zVq}zsmRUul(C?Sns#l&s<$jeKqsHb|;fDY_+kl8C^1W3}x5ml3W|XEEXYM!m?z=Pi z;ATv099LTxcGkZ0+9)8M8_v;n`%`sXbOHRtwgIb1ND=s@`6^n<@Hw~wico8QV9hX; zP9vUgJzy0~8l%z_mK@uN)QAvg z6JYfh+TO3BRndLdc^)i2`fOr*F{_qR&LbWS+k?I5NonA~s@|VA8PbZ6T=l15i=1$R zq!KdBt=`4RA2Ua|{b&n%C?26#F#Z(ULlFC?5+bf&M#3wFTvhc2lrgqx^<6efKfkbV zrwHc?p>kI!NfB=`GNp{f@+V4zM?txfRYEF(M9%Qm6|H)IO4wyq2h85Xt`VHjsitvq z9N*Fv$Z=KtBDBG>Zex`>BaU{^1pYL``R0eh#Lpcr!l}0h!aa|kv=sJJKP=Zz+aA%W zl%#EHD@0e3 zROtfeI5W=cJVwc_u^E15frZQD-{?tvn&EylXFsvS)~-LHmR#x-6(6%UhKpaaQ(r&2 z?=m25ZLr&C678A}{<|EEZxGlRB4j=x6Ll~wNuF`ca8{+IWF+MCu2*s~l$psvwJq4y zU?+)#!(yQE!KeM<*#JyCsTfOlcfIN0@28w7ijH=(LmlOzHJZev|Btfs1pWa^PEI4y ztu={sxfpc+@4D|tQ1sb~-hoU$9x0F{zB236s$<{uhkJA`_$^t_r^8%-b6S?wi5{7r z8z}U!Oh4EaWjLZ9WO;^LKNt2cIPIBPcZ^vTW$;mLNn!$Zmo}^6z~;0Le~?$r{vG#d z@?YVV+cegC&IvwwZp2(FagRI2m{JeEtuL@IS5t_pe5%eqk+L~>1$H9Gj<_LRD>FlY zT1~^0*!oj6)n$<7CD-r!=@PnsGc)S}PGZgwjS-S|mkklVT@1=+6c|MAT616R32eT0 zldi;R(hNc@Ff+=@$;j?@X+AF*<&xNh2%m68i<;M3KTE7MPJF`QCP3A74tr`Ob-z)v z6;&MX<+nYPd2@^+au7Iru$uuLIxY6&8AWJvAqxErVRDvCl5V(PQJ_-<7hRauX@% zyjF+&Q3;6&5Pf>ByA08Qe4-=uN5$GQdTD#cmlW%v|0>_ufLtHAsOw zZcyO_Cd2V`stx)!m_oaYe&6N^)vTwCS_l-G3Wal&J?GncfckXvcu;ask8~jK3%exT4 zuk;KFq&t8e>CrejjjyoiYBB!!s(G_aMYGk523o`UxZMh#JaM(vZ4InTFa`4Y6*&j^Uc>3&@xrK5AV}V5@W8-O&6QcZ#&}%Scot{_ZD+Q`RhN*%2=1@{z^+B7^L^uB zz&KALJ=r4ek{-0*zlo3DyGjvcQz5H1ra-g|QDen1pi(yq7Te?R8*TaADK*g@X zml%lprFIhWJ7tRrNG>yx#Q(au?Ybz$^!EqB|EZ)Z9)~X)9)Ers=%Wgdq9ifx%tJ8_ zl=6PDZoGt?`hbGy2JM8AUG~OdJO=nkF=K{Y;oGrV=f>!ZaZXDkzb&vj7!hYQC)g+o z2A;hNxSr7%k`0m(^a8!4RGg#!4Td|al#F|TmE#w2`jn?1!cOTM&Ihb{ikLv{Zbz#& z1}8qmqq$PsUJlkLXmWanMCUTkx42Y3js9`Lr;Hd#z{yf!>`|Nxy z4AvxxJ7wYqDFotwV`ATwI4@L}n~Y3or2gbbw2!4Kn;Zxwq31akNaCk18kMZX-}|HVAb&P(CZ=_%fJt4zKgaX zbrSC{MW>HRyfS5rSZv3qx;3=nk2ChEC2(MVI9$bizaaZ9NT5 zZq*eHv1Iw`e%fW~(W2IL_@=}MUGTlI_W(IsK5uI*H(sPgnI2`bG?>gfz!R&XH{C0$@0Ge$jg+)&?s7Y) zv2}0~5mE%4kY8zWaA=cQ4Irr&;HZ!gM0~LOM_|Mklai=%fL*}4xHed zgXq8>aSi8wHOe?muhA=E(c!yOA?HBS)0>#01Gx)O&rWE{^a0 z1u7K^?hnFP9W{v&`Qa;z=)$4gwDsr2BA?^Kw%}EV_r*3a|IXz8O+4_$x9O>bQA$yE z??_an+u;39$gH5?`|l7JNb>;)uVBRUd3(Afncwf1X@>2*-p5|@iu72x%HE3heFos9 zY2bhVf$YbJqD(Ii#_OaS3U@NrHzUBvZq~Jk|fs5kdl zTXceKL}(d4Jy#iX+C6)z!7u+{LKZ3}Q>;Ct8e?laO zbg~BdEi%Iz8}T*ty}nFzUMd1JtR!tUM{zaAe4-)Y5qmk$7hBbX;Oye^h|?QW`{q4b zpcmzNn)B@OxR-o%dP?c7z_Y^22cDe7Jnz`SBlF*mK}C(K@_5s=Z@KzIIH4qJU_j=7 zV?^e$DK=6{92$kZ^K?>4#ZMnADtybw+VOyKe0(e=C%4-Mh0p+ZKnQtT%rm)1=)gQW z08}&|6%w1H&LRhP3A=A}Q^g2D&R-wlAD^B3_mbmMV<9*(q9Z7*Z$aC+ls!E?e9*F5 zV5Td}{kk{r{i+N3|8aHIVNL#D8<0jAAzex%EuAwEPzm{>ba!{x=ner<8W91N2I&~x zDV+mG_o$89-pAjc?{)FluCeWUp3ig6eeQG4eHW#RD@I4Vj@HdJepLRjNd47>7$*=h z)3AH#uP(obJ6mD=X7z9H-9h>s>bis#fk3R!!;^NZNFAgd;r@O7{V2cj;J`Ndh4%X{ zdfFyzrnW5}K+xLvKCvM~SZ}^DnA#@S?I}>9b#y#$zd0ju)aE;obl&`&rv989w^>H^ zdb4(%0++d;HQ-@M&JV8K#u^~KFMFS}`d(Fz|Ie{Y`8FZ>egzH5?vO6YRRJ~}X@o|s zVCcZswpQH+Z)%ZAO(v)lNhS0;Y5yn5cwXM750JrYAh6){=0y>MWb-{B!<}u^2A;1V z6Oxidz>?9xP5+#(Qxkx6o)IySV1DE3`f2Ih$*>^`(t3xV8+T33LNS;U?Xq^-J$rsB z3FrXXJrU%JB0bP9oZ9odPW1Wc@r++CjMQP~$Mau!c=$C|Qxt)x>-{xHfL76!9rve{ zyuRP9oQL2HK&Ki6^xipSWa0`Nn0fxs;h-O50NgptCcWb7J)lkY1I3hdKk5zohv5R zkeVYLPkJy5j6?fwCHm06UyYvEo1zi;ch$TOv=w|YK?~k5N_KLlG}wZ*nGUr1*LT{k zBLLsSX|sbELJ<748w5XRj!TjZOFFX2f4U^rcwnL%-RnTVcu(1>ygZP;lizuLiwslC z6IQtX0oSdzd{Wtp(m~&M?f^oY?mbyWn*v!HZg$dNO>!%5?hWdlbDvg;_{g?Y`4*L# zwr+Ftw~_J#H1_GHW3&A`%l@cdjwzoMTzf?3&iBB1FRR`?`iLj5W9-XSkTPj_ z@5pf^&iQzEzR_8nHn%FP=0<|JJ@B2kLd&@fg z`77xm&jN;y%tlaLUE)2HIPQsd45r{1Gg5Uz(p|0j;&3n0|Jkip+99W=ezVhI{?H3N z*e=99qUs)kH{L_N+d4fI_#`-wyoEdqwm$vG%$Nt1(M5yX7Rr};5QD7!luo6tcB=1> zuguE`m^)R0s8@BtT1vkQYNq8& zd9Yag9Qm#p{3>3ft6!r>mI?PUD@a~5)OgxEapLbzPxF-Z_~tOpFX?BZHGG28j#Ajl;%l=*no zfZODDXbC@)iWWB{IyxnqH6TIOxi#tcm04h->pFeEh>yC2hQj5ead~$FtIMMZRDj1- z(~c-)lJdCy#Ml{nt;Eoxj5wcmo3UCN9CKeWGPavD3=q(sl7bz}gcx(X9&RTx`SqBU znLxil6p%LFM`%0^5c^V+Y#w_9)9CIl9eenLyGAIk5J(Ph}6V5 z6y}@lmKIqzOzYN(w*y~a53NiFdkD?N{^+;MufJX>NuJpbFf#3ST9-r4t{V#8R$iZg&1y6})Sqmj#{a#fTH}L6s zMf=Uu|Flw3Q(79QcQBK0H8rUj8Mb4xn83gPY5DGk-Zo;;1iyZ}u<;6JF*>*AO-y9+ zVgqdAsH9Oc0>OQ4HYbGg^>bfs+7X7V8nOUL%NW%UmiAr4!p6qV@xIyH6fhcDmeyPL zfrW3|TZ%*DA1Y3_!05G!Gplsb8fi={4s@thmtS#?E#R9$CVZfX}co^4DHJF+aux|Ok%!% zy^$bl>)p{LK>p&j32S z-wO++56Wr>vT{4`kt^NdAO%%DFXlIHlZMD>huF9rKkhZKp${p3IIysPO93hjB^i^= z?v1DUBrclqlq%v)J>`8KQ@Or0S`?_aRQUlzw?OpxD;Q(hH zTQ0i${tzHq&l>^W$M?El_egvxiYO@u4oV)wM zOv&VzuMZE_w|d|7H}$`}^q1es_afGiD;g`!^SF5f2+j&*?)|#^b3p4U6Z(AT5 zhCf^%^~Tc<_mI``+c-)nTo+2mga{>fkL!U^T-AFmoUTk-)1i9T4Z|TjfY_ zvEgJ>&+t+-z+2PA;HEfw&;H^*8>xk8-5=eFYFJ+mZH?rn^;WDvzs>FGHL)dc{=mft z)?>FT$4iqIg2yg$LZ{SazG!#{hW2udf2=fa%d&b$gIEOT!G|lMg%Hz_!!p&gFM`G8 zZQ;x&fsr$8+!2+!)q(m662Jk%i>L80g&8k+V?v(SJ1eA8CCf(Tg6|YRdI@NzP&n#B z@c}Sx4B26~ci6P<%GkS`?Ie)h>c|L!ZQM&x#B zEHMceyLhIL!aaUAbCSF}>G_WJx)r(aw$S*mc9+SsbisJH^85060V=|<=MWSdPRo^B zkTWajp=0rD+Wz{>cd?1YJ20WU*l6?>zQ9o~2M3oM{JP)l(cv9@YwqJRDS$(MAKCvW z6H?UAi>?lw#4#?X1#4uC_S`0<3MsnkdOuR$8rgZV z`4x~>-^+Y?q5GVFc2ed`iiXjK<_GtuH`@E5nz&kWtlUWM#57(~HZw zI1mkB&x2vo4QLZjm7K7Q*tMU9kL3K3U<~k~rX6SA8Vm$SaS5I8ipeThW{lp#s3%0M zO@Za!cM0aorvse*=CiVH`S+J?Ocj%Rv+Pg9d*6L^FY*Gh&iX$g^sX;j&{1EN!KeILKgjTt8lN`LTYh;~T6jQb-e8l)^QuNO~@5#B7oSHsra>w~1e572Dn zm5=H4g0$Rfg!~Q%zY$x*kE$lt+L@?HwT5sK=SI*HY9{dGM9hm4cv!6lrzm^)v@9$4 z-QW^x|2b*c>cAxMR34$I26Df419M)A!y%D8CbE#m3np_aBifaxPJ3}5mjnH10l`0) z8|4wEc7LLsoB$U5aBPT)(5mmf8oo03z_3AI6RmNAIv}|DEA-@Qvyc|I-gyAtqh&}^ z$R{Adm8HzmoDUck@OYKWZ;q*N7aO>6v&kiZ6|TsIpeVWW+!f2wyiuSX^3qmCc+Mn< z2oikdr^LFZyf1g$_8rN2m%)0kec8A=IpPU*83RW0HjJj=Hii%97ayO zt`1OZb?c0zP0jf{0A#V=e^iTwf#feDk$;c~b<6xV4<-07lX@Qwb5D$C1HDdlV5y!3 zaz|n8yUzNE<`Ma$M?Lg5d4u00&ojjj$o}yFMZ9*T28c zfvifSu5aA$>vT>JYatfPmewo#TZRFbA4L^R6cweC6#~H^@EwoW=#eGI_(pNIGY7zj zaI*%){PXFUot1d@>@Pq?;IOf?H9}IMe^(?`j&oF<+pqiRsco=){r%4dxGxX<&@#~A3we}G-LuS76t8o@ygw;-2>38sW;L;Y` z&jLw3fuQjqtU`kAyDzu5M%E{lSvGqit%~T*a6s(beR0p`U$UrqCfbN>zvbom^_X8O zgs-wSEa4)vQj|6aA_%C4B`YjE@ivz6Cjr=pPKs!!#}a~Ha$!NyB_#v~NeWS;$}CBP zj@GuepMb=cP!gTuF)kh7Z9NeRtPIRmTA7-L0PyZ9Aofoj{D`F;BX^|V&xDMM$YMNfPY2f_1G;vJqylbezq_h7Kp3Tb34E)TE`f| zsFrAsb+ETJy3()$Y;#wn3lb1SH=^qUe)7;_GqiPfI7u20H;Yd-fB_{%IrK&Cv#@Tr zewI;cT9Y;RC)L1fnB=e5$b&J7-19`K!FT`ltBxCyX8dW9*n_;ha_9~t z=QmOWByo*f6$Hh^v22AFzS{@_LQuA`S$Rpx*Wt>R$a-xNsf&xVZM3Y5_2f&ZI_;3q z&KDJJdxG}AdmoRQin$}5Bx~4QZphStnHE4pIf_}fJ!8@PFHyhSVB}6C^OOw+P3h9U z#i8i5=*cl>;QgJZ@MyCx=_G36nEQ#sinc zRR7td=K#C=)!2iI*{K}M_ZD@wCf|^n4R8Kg3fOy&CCg?RtS`R;>|uY`9IZy@+4Uhr_LFN&ovx$++)7Jsx1YE728z9)LeN}>UizXMMD_z=FKwA9dEF)63(1)6o$31qGN}2Y ztUg#H3qxmwUMOqX#`}ugD6E8X@0BnL>#d(xGx>X_{JA7`(?hY~$o2SW(DMeSxstL6 zS4UWg-P7S<9u9IG+9jnL}#viGf7smsr21?0H3^6u#! zTpl1#AKqJouE<`Scc>=$5Of;X|9V;6F|R7u*0%R+u-`n|IgF?d`5$;Aae?a-vppxj zgZP}cV{x-b2A0qRR^zs}+L@T0%HO_T4Jh@So%+Xe-i8{%B^@z(rs_W*e<0W&-p?AN?0+Y047V=8+ zeAi~9<#14Ir&f&fbL-&6yqh=~C1OiVcLL zI4$UHOLl4rb{$=;X1}Z9R!G^r^=t5O&2iF*Cl>o!uCWDcVccaDua)M`GD!CR{W7us zFAq*Ei`~SL;s_btSwxd7GVa;UFhpH?xy&-LM3B(21Vq{y3j8 z(=Mfs?|lY851p^M1-B`*tN<4BGCvM#b@@fP{B0K^1|v!si};H&N4HJ96o7YHe8cLI zhF6@!RFh~;l{~P>?vcEs*0pA}3)Xjl6$@UQF=QaL%A)<|<_5hl?fTQ3$D_l!0ki)Y z^S*z;zHST^?66RPwl1BCQ!lwquADNj{x-E|-Rca;oF97MLR!ZFZ@fcc_|E?O{3k`y zv%Jx4u#<@)$Q9#QdH5oM+pIjN8589ZVqNc{pYvY2guX3b;5SlB>77IAixsE3K z2+MJBHNWic^4lnASxIFCl<}I5w_A4iXwp2|(ZHC+dVtD1H8;P6ZlHL9S>6JQuNo!YY{q`~PH6N7)YbG=sdoA1Sazky zZN>Fk;kfU<5G0#nYI)fZa9p^eZ#xIJew%>x;}X||s#8KVYM{-TM8n~Lp%Hj(?}VD9 z_WnV*@6F^bAj$6f???3YHEkEm@(S_c^@y~g_xYB+`*Jff)@B1(tYNEzKU%-RhlcCN zV=Qq_&m3G3U0~xrkz$9riuHQNO1Bn?_ItGQKY`4CXR431wETtx-N=B=*c} zS2VeK0zl({{GjeB4GhukW{yGay6`co+sYZn3NRo<z7HMlg}x_5kExt?jkdb=?_HqbVrAVc zzC9yPHpI9m$EJ;6?C{Z=137KxPhRpt;_lM$^OA0c+-?hq)K{xtW`Q}du5757h;w}n zKj-IRi64{FVx4x$!66kFS`pR3hlKOi0&DdNGBsXage(e*CT9vVg@I0h^r;y7o@!c!^*`bC)y{sL!>1!*4ckx$ack z8^$aE4SmOI2*LCIpLw7qwmnJ3Pd^;sKYt1;CBA4S59SjxX=Qpr{-JuM2(zdhYys}y zg=~JfpZk2luy^r>-ThC36f2d4=Wf;dj$H>hfL)*$Tp&t1sf-nTrTLpco@ZBr^4JMp zi_eS7oAcY@b zK#f!ITD=vR7Sba3V~X|o@*U`{W#= zybqetJ(Wi+448U|pDZdW8iBY%CtH-nzQNg6h3^2X(XFkm#g|KURfOb_ z+6Biv5e3s{nId!-7`Gg(=qM1Ays@4fg-3ce~%D&w)?TcvPonQeap8?+< zNKB^mh}-gsR1X9OL}J8Qoz@C(YfzxyT7^MIem49pqrE-2qU~C6-`)XB1R6AcBsr>b z*lS(uhAGrQT+YFyg0V|--EnC10*aUJuBIT@vL``nBfZWGHL(X3jlN1A$4j|jS6H`( zd0ou)5iq&M(t;tw!I{Q`kuBX1D;&3;m~r*>-Z?Fo`Jyl8??s1P8hkw6ue~IvRu+gt zD)YghRS!OCk3TI@Siq!}>yetzH@uYc8~AS zv&k9qvuWaz2Go!9P*Wyb%ZX0P9=)6uO^pBD+Rd>XH=dZ4rR9VkN_+Tit}ZR_GyuSN z1tcf&QRPwor-6JHC`5_*_bmZ_qUAlv@`cgZasYnb8@^Rdt72o>be5 z)6LERXN$>)EirA^oh5gdO|(^ds=J9ShQWwyrk0Nef5>~h&I`En)&X)ZYtftM+%vGN zhuTN}%}2Der;NNZlbh6>k$3x&7wW(Tx)Z95krHos)y3v}yr*p4=f10K%M6ZH8R(@o8sP0`J^HJGzZ+;JTYTkSCLzmCMN`pW z_~yBa6f(Ct;WOdf`4Vuu;6}++J`GxpokC)%bLcDOuWTX~>9Tv*r`}k1$<5)jNaue0 z+ViZc$YVX_)1F`Fh)d5SP|!78Xc+X`(aSA&XU9nb zT2>J=`kc~5>!ySFT1%@&aUx65PcLZ@OLFz1gepTgQ+m5Ao6e=w)40Kf&X3!;tGi@y z?nAcS=X#q4Gs$FlZgt*7xevlmlhFtA{$)~I&hSws)@c&*jUi+v9hm4rqh52iIQXAl zin;Fp^{p33fT#FeoeqNYUuKZI33>XD=ougq)z|M!wKqf;I9>i+c=&_cSVKOI`m6yo z72pq6FMt2Qx->WAu~1UYk)<6*d1H?08p{X0@n+a^XVN!-Pw%AwcOGCfV=|vufr9f+ zCU?_tX&vOg$P~;Smi*zSZ;bQmcGYyP1^j^AdxOfCGiJGN7+sefo_LS)-SxXYqfW>y z3p~Hz94TZX@pq=JDAu4Ifn#I(8Y(^QTUi*T0n2Oamm|z0M0DFDBOO2kLN+p_P6r5AQdvLnV-MtQCCva{=yY~x@ot_^z^`%RsLi-*sLYv#8VzHdg^^0Z&5 zrzz-OGn1+1{mRy?U%5oz)ArtFqiCO-#b0E@-Yto-ec|&;{-+5hA*(`;`+jt&>wMi? zgVPb2REt$nQD<*&nBVc?>ycdQ&9gwV+RT3P3l_l^yJ1LSZb}kE>nrn#fDu*8v$@5H zL0oUy?Lp*`r$Xo#W@6edO`wn9eiBW2!18qN)(>}oX2!>AwJ00$iaii*XZu8fk-7U`FK*y}46e~I^x1KRQUA=G+8Gc11xQUX%9+9s?o!di&y%AHw5_ekjJbL2m~0l zkSvU}0lcJw2*|$A9q0j#fhTRB1!zbB)iF^S8Ls*kuLt{5Bi6MkChrwZAmDTN?M6V5 zz2;7f~Y8iR_V-JGdJG0wzkc&;U{92+cV!DQ;IM!WWUjP zms$PR%R);u{o@Qr&v+Ka8x{MupdeuQf{jBe#!uSA`l5@Gfo0n?_(@(~9)Aw_pFd2L0+J$G;vIpfACG!-s_T7OhP;@FKgq=1Y6zq7wz8; z`zmh}zOF>CubV~rEXNnEE@%$Y<-Ofp5K<3fVP>{+2^OI!cw$*xGV<_(fpw=v9xnp- zx>m~OYFflX96WWNH!mZ6h*G8(xDT7G5dsK%il7B$LK%6W^c+V%5*0Dq7-((s^;6HM z6W4QWy*9!&deF_(V68x87Cc{Wy@>W_s;|awP5naQeaDV<5rm*)k7Q-reW+-3#$pi$ z5(?INt)|7u=aDKM?6&bi_;_fMpwIUrjgss`T`DE*pn!7G#`$28F8<=gZ8VkFIwPUX zjvPbjU0xob&*8M>zXt5^yj=$(gb#Xl$ni{_li?sSIJhm-r>vGI*GcIF{d|DgiREBWooQjpzXcPi2mor9}ch?8@z_w)%S8|Xq!$k(a1fU74Kr>5W%4#hh04T8lE9gL|3dkHrc9~by zLSw#aCIf$~lAlu}k*@u;%vg>l*AgEz?+JJ;tl=a~7HlT)doQl~Yf_T`3gwFt0+8~vQ{ z(^*mKDpBYstT&H|c!4?hNZgmZoCQ9FZAkOqynWO@%h~*;bLFBiR=)DK+VO$Zat~{< zX2{&7apjM-Aa9-8Lcg&b%J>MgqLvgp!{ptczk1)iUJ!ekg1(pcbwqRJV00^}=EO^w zV;yYTqWumGgsbZxA<=7%E2|-yhr07q2R`}oSot!I7%B<;p?asg+h1F{RRwrE!Jc3y z@p8zDD8cY^$stAK4aD15i6&vP-D)Sd3ND>5#l*w_2@*2TBkkQkj@QIxuqqC@=7}R9$=@J7&wd-UHou)zjD^R9%RZq*9daJ09!uLrY zK%t^4nA4t1i+9A&hUM?x!z6{Xs$dO)2kveOcqFjbS{|Zgd4BJq&NoZ-iJZeF5G)zyhhS#H00&}@CUd)SlX zvweBk1Uk}j4{zT4Pz zlHLK7{>sV(e;r_oJ6aGylw_d-2K4zTEt}!*>}=BOVQuqD-ZbK3@e8Yidi)W85A0$! zmgqt<>8iDp*DG}~RvDE2%F- z)wn)6H<7EdF|ENI{ma6^ef++TaZ=*e8mj{(dh+s*H|5_%hw95>L*=S{3ut7|h?eVl zsY6wH6Dg~#B{3Qn3it7U$PfD)1zox2(yoL>1R)nlAh-J|KaDVA`gz0Ruw8+QJ*y>& zeHSzh@KGsoA2yT+S-(kZg{cst#)tyem!f`Pm{-pB8Saq1$dF^2$J$ZhZ|gT&cSmC) zUj3B28~%BJ+SCV`yFN~vzZft{8PfF?zR;ATA5v?*nd$M9`o5=WDZu#d9a*1RN}7n> z2fVQbD0Pw09X4~USXR0F9sSPZNFH=TsR&Lv5330F)L2u$*Le1jrCXm>^7>9L?PFq+ zTq`O!osD}`QV{rUp_yfwA^&%Zx_{r2;|PvQnSAtDkK~IJI}>q1&3C3jMVvuZnudt> z5V(RDu>ve@cTEo3{-=xu+T`-Y+?zq?gJFD%Hv2O@66V2WH6$deJ-+rZKv-fr&X+&z z_`~teTx5G(&`}-A*OP3>pVIRgJF>e$0zz-6=(d5jI8Cl(`tNiR_(GGGeXr>{PT}_A zM?QfkbU#)bv5#C}c*8ZPDjTlLv1#O>rN+GpP98lZ&f$C9SQqz3wy)3c<(CtV?MpU}{6pCY_{Hi;!4N-x=7!@#Ow$~1O;s_;Aay0M|KAflg- zlr!O#FZUzjCy$8NXXaL)Kh5okY)OrLa*4%ot!dc zU9g9ShW5*z=AbQOlrNEwabme9VOy{SiqK|y7BEnF_KSOB5iOD2q_c5v&s$vdv^5O1q2Y(PtTbB?TNj=Va92) zxNSpfTjaNunBAc7C@$N2H1z8+Tj(B=aTrQdB7djPS0k|wllmxP1WT5BmT*mfQK=a0`uhDCC3rTR6` z_oz1%-V%udn0v~y3YyQyUJsQZh?hTVX;zz$lIiO9{36&jRb_6Ue3-sqmIVBWLRpPH z39=f>!j1I9tEZB?>TFn|CqG8^`z{?w793q>V>u?DT&$J``Htxunwmr~cz$=huvEQW z&8@f{*dBY-?ln4WtA8%|Nv)HiPV&8WxCgjds2$z@>w2#Ka(gPpZZ*W!Pk<}%1bqiz zxfrrHME+NyX&<=~3z?@ae446s#3K6XZj-a|EON$nz7-$5*ZlC~F(}Ts5Y8!6>$N5G9UXWI4f%r_-rVaT*)r~I<gqL#^v0oIho8r*c{Am*8q+v0is zBY{NQwmpaE^{!>-K&lel{Vb)Mm48huL#sd>!JdKY?w@SC!xIDN@+s1LoPFfU+*Dw; z`L+maXDk=mlF3fGoA%=YKbF*%Z3@;7 zhx$s4_F1Zb4_VSvnX6RfVS0T{E7(cw@Qf>&zq@CjPT!=7EVH-{NLGA2E9XD+AQpx2 za@oncO}o#k(m0G)3iQd)r4ah1ypDn;79|d*rx|?~f16G<#pLgWJWE6~zOE1;Bf*iZ zI`YxK2B~)wCto{onf7o8hmZAY6dbH@V_|)9DwI#@#R)KBO4z~*Im~$AdWv+Jw{tz| zmE8Weote*;bOfJ!RY0!9>`(dMNn1q47?~wM;}a}|EuHvOa(|o>W1T_02FCU^=NKqA zk^c5LGQK@8`#4pFSm_0u)3TIMkF5te;NO2Y*9DPuCeZJ zDMRl2LuIFCJmtr8q+SZvYk8?c*oe1>3nC!a*w*j|S1XK1Q_#5cyv4XHOZ>Vmk>%TP zc*2T@V4yiKOICJv>Sd|MXon4mCEQY3bga>VdREMtmG!=toeS;}t%h9@6}YbC9SVqnIS4%E*6;&Vj%vi%y}ncOs7qw)$6 zQRn;Uw9|f$N*?pzu(=&S}1uxt&Ik%HAJ5j8O6czXgBdqVHSTLb12Hxob z4k6ff?6Df(ZvJ|Qt|iz=BcM$tST(}g7V|zUMWP6>I4#dbH$!LP>4#& zzghWKo_^%mHJc5^1EN!P^{v^m-P{hQ;bv*#IU{a@kKVpFFM`AJYygS`-&x~ zIxyF-y2~7Xpkiw!QkuTv7IbJg!F=U526Ncyu^W|LkYW zo`Ef_8Gj{{+&MWAo8YLHTcHN=v}4uS_r2HjAkV@%kC;4~R@X?Nil32r@Zszt96Lf7 zFI$r#(X`7ZI6N*h|M*-!v#yM#5*EO#-~j!E(Ib5F;p^AOV)tpg85rSb@>J;?lfpyh zn*58mEkA>h)*lMlF)lO~ta1`pj?2EY)iJNT`g9d%YuvUIUCjK9WcwCKQECfqKMz+E z%1GrH(2(5>v{h?^=aaFjlWq>j*EpQMPd#zAwIL4^otRPfyLy417ZN?(qdobOOrcw9 z5?xQ$zSNGagtKPUV$;f}$BzcCpT0p|bC-0hd#|wqp|oGea~spr*7sozlrKvN8d><1 zERNNLlg!ud=Zii8V$bZl255^bz73MEdEH2YHDa_B!Mds-i8&FJ-8v0x?2>iJcAYur z6Dg>F2*&hW+=*?D3ap@zX~XZ0Xu6wNSA;OsD6ugi21PP^XB6v<$~`VaV{6C zpRZ37s!<2DHe~*01KFhvh^ZF$sbBU(=C()b>@-B{zc9Cthc=!eLII%G?zvT}V&gIQ zt}QK~?$kddijt*L?6WKQkhvX+?md@!v!{QDtSaaMN`iqn)^N4PGQ_fY({9 zhTHm~Q^WGpfq-Svu8*qiG+m;pUv(vZCXh{@FGgjd(N3(EjHxS;w#DPhxhH=)X_U{l zHm$J3@@K4>5=(d^4f?t_wCPt(ODbQAstsas2>(Y7O%lvSbu#GUqJE}{sCGfj+ydD< zuT}Yq=T95qPFwlWZXO29o^};T89(JNNPb=qXb@)=dkgv!A`qhQy+(e`PvrENuo@>c zs~rGYmxn9qch_yZ8_Qdz%`+W+lQxzkmt|tCpVt#0O|KrEa|bH{MjbGe5;?jMQf38T z)^ypJzbYXoj(i`y)~^{fVrT1rDAcs~7$~xqRqxz}5mE{Ww$*67>-_%TN*4P&n2@W6 zlb7y6&T)u=@jIfeTbFzerzMH87RVE8fhFzJyd0g|M`UkWBR_D>9ZuCa{difVa&j)B zx_I#+xS0q`vD>Wmku*(zk;hcn@3-Hp{JH3oQ9Um21Lkc=SZ?!^1qM%UVGVOUe>{t- zS}T~VobCdrOXnNb=_5=nMEf>UC-!WW@y2+Aa0tAhiNgGf$|sR+&>J5|qQqVg%spw+ zSZm~ao`|e03cC9y=w`{>#m)VL_|v;oL-+&~V#lQ*X2C?c$KUQh<6yfxutooI-UxMA zKy~^HIoiR>Y8WxIBohF8e0Qcbg}7#~m)tVaf<-}=U4_b2iOa=BU2hlq3pu_l70%Up z!x=G|iHeI|sI32%efuVV3cy$_Y;83*gh0@E;JSXz&q~QpU7F49qaLq#Zu2x;RDFJl z&Sc^5-^8hkpa;3X9NX#pU@UqG+%(QFq;L%}xrHJ@T^(7}$bN`PAgH0WUIMAm%f72> z<<^tO?%FH4mE|^Fx1H5=)yGvKf3LzM)}-wyZ%j4uz6+inMF~BO);oRK(kn#pEE54@ zKdM+I;%Ms&JaYeh==O6!U(JK{^$t*0KO!!;xre=2I%pac{Merpl?BR?{^?NEP_A^( z42#TtQ{Y)?A#xK>yV+c9cuUV5qt8TGUTWB+`cKty>1B5iSlNAr z=>k1CZ+HA$3P)FOUgB&;oqelr9&RwlroRMrUKRplaG%;>NZd*pCtc~R(m*{dC5B0x z%{~)%o7YvTP3M+@UD$M&k&wquks=4h;O}LA^t0xa$0R{Gg?$>GMI}sMK&E2i;_GL_ zLIhYj0mn^r($D5b;pH=lC}_(LyTbyVUBfqAUdK+lyA3T-*ZFA*)D~vj;+>Xk2>H<4 zcc;dsWYD7~$W-A=gxwqY6=V3!!Vtk9ACF2pAjXV;kOl0P5)q$Dao#dLQ zkhdc{gM^(_@$!reqf2OJu(8OpK7^NQ$bSEJtNzqteQ6FFD^A*EIssB>1-0L_^8!f# zj|zgdHWjVfV*|>L5nqyVo75UU3yl|Im8J*Z0#Vk&*>11T4=L1BBXD%xFSm6c@V#6% zF|E>%dDN(>Cm9xW^*r#Rqbq54x0-1{uT=n6#*j*iqH1Ah`2#vt7HJf_m#7{|i4l@(R>DyOy|BQu z(ODJvjuChlw(^NMv6jOPY$aj9p*6UuAS%GznKx|bQ?#%19es6zBQH74XPp;8 z!HP*A&_5a_wQV~Rdpr~Rw=L-fk9O4YmJr(`zU~MQ7QrmrCfP$rhD6XkO{};JP62se z{xw+9p|23J4l4-Q8YX;gPIq26Op)Js+lyLA!WWY}<@<5qp~a^ZbXCc-bkcK&S#8_k z6jMm$+l@`ki_*-;D8HyparEH>jVz{ zfm*8s3b9WH4$*s3c$KjFN#KP~x+|hhUNEZnew%YCPAdu9HbP;!>R1Cu6543ee z^^IF`w0-!n9zPOz(L0j%NmrgRb9PzrQx?^@(u^iI^4j=EJ^zb)BXCIWD6__Wxdp(9 z{VKy7#`&H?{<|bUCMEyg080EM&!m+;n-xP#_HJaXmu-V0;qEl~y&sBp^6nyX$%4N? z_;<22sa1bY(!sGm`E7YS{nl@hCIHO(iSDxvN=WRzU*mRHcUz9Y5XTA zGo1PYDl62X8v%UNqIj*uXRQ0I*}LcH(V+?x_`b(el|?hj<0Qi&@VEqGsu73`zoo-8 z00HpOV0&#vtqC^xBF&yWIZDLG$OE(b0n0?*9u#N_713>OBAsjdLrg!cWcB7zLlBZK zp_KQoH5ap+8fZ2YZIb{L*jb^!|I&SNcJ2l_XNN1w=m5%+yTo^!7Pp=Cp(J^_am};K z)nXPe#VZt!VghU4K27Vj8n5UHKiQP|bF3S3UK&8NmQLOD7yDXw zLId0mZg>BH;YtBbe;lvuVXmFqf`UEY3IJa|pn&c@K2Z6AIBiv<)6)z?p6F#}cl8`# zGvN5%zkNl7>|R|DI5Ck+PK!`IpmA*56PwTEz}h?`X!q88f#|6aX08O7m8D!8_ByLK z&d@d_!Gs9r1wcihPF6*EA|+(^4N*M*iZ-lo+u`8nGbtvo=FVs*E_U;=us)vLa{UIo z&VCca+10<2%VNPe2Xtw-;?)urdq9*xRXquzXr_58Op{eNEXG@o6+?@`(* z*80JyUAT@mVOuf=MZ5%_2fp{uuGhZrSlUd@d(VMVej5;D@ql;J{(%k^r(jwEA>+_h z%P8P!dp$D=dtO{emMCP1&7n_07D@h#E_NS2_S}B>KTkpbhgjr{;sQE`R04SuxkH<0 z%o_##`zI(Cpe(+;&eQvnc(X?1uX{FYnw#oQF+wfV=27`y^urNFZW9@vQ@@WeWJR`@aI4nC|Qgs)F1pK_>o z>|H%TxjH`CNFi@-q0+THi3Z*85+^WxoL4NUlTEAzwj&s`TS$U~H-UsYIS1+np9E3A zTQ!JRqlP_x6pVYpKeh7`NE3!RQd6GG1PAmamhAXXSAoFb!pJwAFZAglS2vR6nWD-@6*>dgW`^ljn0s&e^Q@{o8^HMZk` zfCqxXGCH6Bomli$QD(+_gQ(&pj6aBYV(!b$ZBEr@Ad~JCP@4m?+VSnNE3NnQRGhnUxiC2qSp2r z>@=k>@Bw+{O$esVj}l(`&J9RDedEqS1~{;$bpbK2;(x3SlW;WyXlx}yMJj@^-Q&>} zBN&Az@!ZUj0E;a9zj!_k`InA3P5`yO*gWOyX2qwZlfG6+kaRNF^OQ~Kl;O+&#~g)Z zOG7vX6V3#Z-pxr)g#mlDwgLCn^Lv6HSgyrCupp={UUZx88ebDLuqsrp*-S3R(w$%O z8Ohol6U(M4B7e_K-Ys)hHD>|US||~$jrP(9jkR|}4TS2S*8sav4~vcYJac#>Q=h1M zuH-2YOv!w*{6IF1@6o@HdH+mWlG3vuC*Ssn1Ng3wpVeg_l3IAWqjG&A zHWs;HdEdNN#S71fThj9+yyS0N3-R99_lMMi*q9t-OO#8&B;h{inMa0ZPMCvc)Z&$Ct`{!Pnb4SnJR}3S} zIe9Yu715#=MN|V~<;qgVQ7T89VNknw5)JDsiRE-!AHzV0lK6<-t8pI(2R`RE@ws1H zf2P!5sf}L2-B+8f4bQs*bmt~wiR!FSLz4k(S_B%;c^iP z-QHx#xkAe$h689iE_4C{)p*h8rYF_uwg>9)OC-amkxfc1^<~II^i6N(HCwi%j`#*GCOcVm{MKz*p%8#e7MdSv zdS~l7j{Pl+6tMn`&%Xsy%jAH5he!NDRng9lDEHjj2o6GqHk&u*(ra=b5dgp5j)DIpjlmIr_r0)3U*&IRI=mJ z(NKvHszZhN;``!1bVn6w4KHD?n|8DMn!oz|U z<%hnb@4wV(8<+3oui(0By(Jh~Z;vru2#m*N7Pp-cj(x}fh=b$~J&R-2=>gSAsJ7_E z?12DoQl0~xAj$Q%V3zxhIJ^bXY;BaOT38QmnUL#z%2PjKyz8VkQmkj|NJ79f0al+M zc}!sf-X3$ZcHalGAmvD2apO?k=E6IU+_0U*`_4}C7n2Bx03MqYWJ)~8W)}>rd_f{+ ze)h^Y%f@JSH%jf+aU>_+13kl;RCtUVK;1bz@#NZdgi*uDxI3T|P3I=EpsSwS7Y8{oweJ4C@W zH^E|16sj7w50|p!O1x6Yu)@_~XUJ&~5e#c0%rvNBM)Y_;w(BCHg*5D21+8a5c~QDK~)3%2={%k5gGP(T|*9@lTszrhLK^ye2> zU7GfIi`!M~sykT3P`kV+$Ha1V|EQtAC4=8Qn(d;hX=fG#syt4I)E3%QnLO|NJ=k2k zN_*{`^0@q`(j^Qx)-+aQjj|q=&OvT$4%IE-u*K_`?*OnqECc&e%Xn#Dt(C}Nh$?Bt z1yigH-%}2f8#06J3oP|PDaWkk&$?taruyw8X78?u=_77>EU7;6Moh_Q67eiFph7JP zOaadgR_wtkta;O^Im{dPmCpsQj}Zdp635E!typGjpPi_hGo&z#COUaNq`TJ_!>^7s zBP~ZuO{>6bS_u#(<(o(}Z7oKc<*jrZ1cN9XYd(GfyLDDk&jVz@QkcNqW6my>FvUyucu8!F}ts zO`~hX&V}hz zF&D`v|M>JqGMGF&lS`SZ3t~<=-)M2hiZc0w7=XlZEttJAW%NM>z;l&Ay_2U`Z9^^r z^1ftf33K~Z@*J`IWTorfdZtrUAJ&0-o5$U&REL!XkJfmgZ@*g|Xh&K#|K5${|8%?> zu^*5z$5u!BU=0wFqrxvVjhtj~-T=(TeJ9w;z1Ur8es`SdbgJ~BgA8yoVluDNwU&R{ zi}x>jZEiz*3cg{Oo9uNIe>lJPx(x%7OaR2H?as?>M03|>o1Njr%)?;g31Q0=ZCAE_ z@$bpcyvAiB#`I31Bmf>qNm2}fM!PsxVjgbH3&)u+43EJO2Ewv*Kml;p*{|k)m_1tJ zB^Vov**)$BfxW3A{n7sJqEDDs^ZIFkTu(kqx_3*j%El>YZ7uat$9M_U#*NZZu-Zl7 zcTwn9JLFgBi6C)uI;@r#)^J-Z%8t3%Xu4rk#s69z&riTz&vQTg;s(tD&?hFct4*4l zc9oH+1tSkNaQ{HO4QnS6@FE>pwLN|`TU%i-O)%5M&g~qbQ6bri#n zYProHIlIYka;1CAU9APlZ+#dlCH%$=$&2W%{FQ#G2T55Wp^A}&;as5tZJXgw`R9N2 z60BYJ4(I9TW0o@^Essn2O4EO~#(D}%=Ic7ceW7I%Ana|?Ud#A>_)$Du(I_~LGE z0K0n4tM%S(c7R+PLOhOKgccl^X|(b=Fnk7c*%=Nk%u2V+X~_LeyJK_s{^G2oWf)P$ zm$V&WVB3IQ6PqW_;=n}9u!iaQQ1eu5wILimiiivD;=;kaY17T@mLy{#1GanuQ7woQ zz~>TNB_g#B-5kbVkBqW&{U?)dJ$(hqrFW(a|0$Dp)sC6K%O=f8it)88<`CJ!xzl3-o19D|0ZP!5?yK_HYN&s4t4&&N^(365OFqD z=V$hE6chOU2qt{q4a?JEJXByxyEYejbH1*8X%V(TI0|yR_E2wk(dctP2i&{X+e{DZ z!6LI0Ny2x!+y*Z_Pb%)8HRe5P$Vw0tsP9XB&W`8!JpO*O`_kHMWNSgELgFbll>+62 zih@v7N))yYFjN4-r%38IFf$`3v3}%DM=^MOekt4TC&xSpVZ=w#kBC~w)6@=*Ho;qF z9i2=Q(l+C&t)Wp+!07G^=KeIeDq+l2ZJ$?OLJRG0fzT5_%aU#)$FOMG~vR`YG5sqaI;u zGcrPy%x|y!gQK;q^SC?{`ZY{;hrbrG2f6HOoE-iFQ-`N=V$^(Ki|wZH(+;??DFGlm z+B4W?q6fjq-Qd| zk&}L3m?lYv#W1ul;@0#oc?JEAP;q;0$sF>vJ0tlnaZ$OQjSq;Wj5O*<6<_mAIcFN! zTaplm*XssOUtHYZhO;3yUEc0P7Kd{p_bosF8uMQBzrN<@j|{<#mEp&T8W`iZNswS} z$aO-UI(0o^LlNJj!cCyPvjVv_kPeA5)kx4Sy89}Z>DJ9SUC0d6aBpefWh6TA35~vF zpqtRufYy-{ZW;&3mYg{$h~)Y>n#xP&PD8p}4H%jFUX}KDW4bN`L`x#*|ClO!VvclP zmzKmL1M6)q371t9gHGSS4=FxSG2*Rlf9F7^938^%Crz(sJOPSx{NncB(5l4mikccD zUChCtkq8oHx7{83yo)0`B@rDwO*mND?9>K0oBZt5mqJ$Be?+I_XMDCCxQzklh zP|i4NN#peGcdW}=PeM9XGWhhDcEE z*2eG5RI(K#9IUNg_UTP9aM5;qu(`lf4`1}WfgyaEJLymoUy9`k`A*(kRqGu510>LJ zcL|z~M(1R&GyFup_aCMNcc*Bwd3l5!vl+`2^endVv1gb}m;tlXcDH!vv&o%^lByOK z(q@APbU|WMEn?f%qK{9a2=^1^xD5lN`>=r6ZgPNPBu5mza->8`&H!l0Vyivt!!f|{ zFfmD|J~$d!yr7ph4D|hc5bNh3PFY)R59-78W!@vcRq2VUF}J5?pPotfM%IJAqZ+Sg z{~nQi84c9m0hLo*?Pw^Xq$<=AWid+|AznUGPY*l>FR4?LFvJ;}#3b)p3H=QqO5LE9{WgJb7#<7{S9^VRjX~--Bx47QQhO(Td9V~WsZH9lf zVD~-Z7gR)1mRD1slB|i;KVTNoa7Mi&#bO!krZ&jkLX^QX#LT;YU2HD*BkSx*CozpHZ@FWkXwk?gCH1!=g$QnF#JZGLo z4ypq@;0bx0q3!LiHCCns)>Cm?v)4Ky79U_4s_r?|lhUVB_pc9a1qZ8tkxkTdF7j1@ z*fDahnwE>>$(tMlN)VrsYYC8F5(l^$VDfhhLensWBD5NhW(O|1sG}P;F~dm>{k&x` zmhEX?4iP+i#!QZ#&3Xay>vTmztA<{}mzB7+Z3`xdKoS51GQiO;QB5)1imXyev>g3y z&W^6+(Mw5a70kW~s11Rpc;uU(s~9Wko71QOxA%%hDvvN80?o1GAKa@8g@4z3nxLY; zmJjhA=Z|>#a0t>zChk|w?iE9ph<@@Odvyv9dv%Vk`|+klBBv9$R?U!pSZ zn0Pk$1G|#BY6Z=k32Q(gP^^qQqw`?((84%>O8e9OiY72@&#Uv@Y;C00CCbY%GxWkV z-*MAJ5n952Y$^+_Ky%~AU?)LFm!0HN!b;Om=9F4gT#jH==C#T+JB; zb&T#_B60l!U{$1KeW5-;-z#!Y0CJtG2j3w$j4tueR6knG0SNun-C$3PRTGeTB&YD8Zxk zus7fX@lZXXd>h-#`mOQ_sB!@XGObMNktCq<+4`DJd}-5ZgrP4ZQC=w+pg;x8A>?N3 z3!v8le9642Q6)~#)Y=MByoDj4S92BvPl32MJ)1#ZlUV!}hVUywVO9E+2wC7N!wxdf zHN|z4PdjN*y?r02XCy`lGHOxQ2i5cGeP0>kgt)PU)%koX)t;z}m}h}s`y&W-r=+t~`%1(5d*Eu|MyXY^5{oIB;~Y`_vD;N|m29y|!*2X!HvyXlbm z9Yn>oRdRkauCuk6&?4scz+mo5XG1@b=z(k)*}w?2CEu9`_LN z-pGfjN7}P)8GNAS-+2|vW{LQLKREEw9qU)GhjD(ke~JC0v{Mq{sc7R!OEtJ@PI8V_1(%~uUD4^262jV>VM!kks{H0^>R;^d`6CMEHWx zDt{@;C1q?nstNY{2rDuiG$Hu$;!yC}_uHj+qpUb=)yG=IS`}jx&z=cU#>qlh-+Jy` zHuMul+|y;4yqq?leQQ2bfoC`Fa$Q*Y`@+J+b%z8F5%mk1mWJN|MO*kcine$&wUtQV zGh37`26oNVIk}ZgC$=bf>}@wlR`7zb9R^?mA#46F z_zRC=boK1LAFcRlHN;wte6IE+a(Az>p@HDn2ev)-+DhFCAy7U~0LoV;cc_7F1Wv6F zNUeUkr&$Y63X|2Io}L3)lW8||D&UWkf?UZ$9oNz??Ah|dT!N(HFeu1^$xgGBBSO8_ z6vvbM66e6fjsL*Qjbdk}mMexf$p5&ZlUjd-4ZHrb1X*or8gXz~w)V9?gbhzsJD7J- zEud7{=4Br-#lX=o;9hT{q$rnL0$;k+%aj8ooK=4&IkJY*Ml9!(ERsMDzaK*b1~1Po zZFBPj;xmU-fma@k8ch?<#uk_`I;f+g-*;8$Uu>G}Zc;7iJY!;P5&_%~nDlknl>$*XDwC1Y6-69_5pe^0hZD>o;={4`h&l9)jDB3PzF^KmTN z2O1si^C}DI!&drCDQtFbFT2hV4`jtvemO1OB&aO^ibL)@kiV(~KT5rM;D>sD>AIEJ z`$5mMmeKM;>3g9Vr5}L?t6SXbx=(*mcbhp+!)Klk2R%4iyk^)Nc=%z@{h=AA^L*o_ z#1`>?Y7}NELXhwsp4Tqe?e(JIM@*K!TF{uFti>5SSm9tqrt%CDD_`=v>H0oA%1H=e z_3s$d%8$0D#I>GIQ_`YsZejmK8-3fjc`Hx*$hQ-!f!tSQ)aIEaEYyR|HRlhk?vOH` z2v;bAV3|~l?OCo;_?zx!U14sO;P7iyGT=p5mS}>g0Fw?$jorD+La$t$#ar)4n8 zD?x0J793BC6|@POA3dwJ074fg;ewo%efhS(>v7ZzJ5u3_@vN}QG+BxU8(2jk$P<-_ zKsSICkvCe$abv5ls<`x{Z-;{Dx86*n9rW^;Gzm+sNYqba-+IUeJb60SDI!ZX(|25C}gM!!Jf_`n8rYK;PX@sv80KoRn9a@P$kS@{X5WZO zVi|suhQiQLkC{iwZbsv9vw~Zy0AN$I2MQIztr}otaJvU^La4@`0x!R^ zHtgcy6_1j-uiJyYuWO@^a-5ig5>96|Tin&?Sq0Rl>;zk=HPk$wFJx>~eDU1zaWaVT z!S6u@3P6_)fL8)gL_bY}9DQz(KpEXg_-h-GN9>1f50eokG9 z&#(DMyuadRvk9h9*3+$MQ$q;O-VIoHsh|zW1_n*aK=E1H`Au$6!%Xh={KL$y9U`h` z!X|ZreVt{-(hR#g7Zy{$N1Ej33VM_pGckJ5CUyH+mrTd8t*`3}@$vBlK({cXb`X17 zv>QzyN1H7QH|ld#S~vj1?rlM)rIMCXpri=P`x}wr7b5TPC^LBlcG%_dojXyD?{BMl zxYFtX|J~1Q8uQ{u&efCR8dg?5?PWmAaAyUc0(gg&i)eCyR9Vq)x}Xr`YQljhYn04I zjul41Fw-zZFVjL)1*U!J@N>!c;EiF#d9u069#%^T9n)g)vJ!U)OeBj+4SFg8N;({8X9g;>? zGsL{+mX^~p=$*m{H;-uXCFc*L5#O+VmPhz`+#`HYZn_V^6>k0PpmJF(?*n`BLG6B7 zol}SI;n}P?z9V(ipk`nk8C1tQ8?pE0VZNdCS6lCM4;KR3Z8JrY=L6s%wOh_{O;w;8 z>2RF}^1zJ?6p9@1%hB}Hg~M~FlGW=cl=N@O6<@=bGpQ9a15n3Tx(J=1ic9L!Q;>Z9 z02$Fuefx|d`MjvRy_!Md^2562Cl(&l2=R7K8lGe*@M57}K zn!dV6N>)2lJ;Wm0kh*$oh`u11Q#Ji_F0A-Nb}gSj+j}awO+;g@M=+eP9P~eE^WyW` zxu`?4r>R`8XA2dJe)Pdq7I|&)GSIV&n#p}NRY?q zu4coAf`2+Xt;mNK7UE`3_}l40wAl>O<-G;AgghUfpd_kkkKR$F)s*XbAb~WX54Ps) z*l0S?ykD%jeVr*oPokfGbIU9ISx0r^0p#PAP5v;m>qw9feExGUyTXUMyc6b;1-ANOIF+GpSobc$gAUn^0{ZT!?H25Id*0I z#6z+LaNfg4I7C7ABWsE2(@(rloS%S0^YGrQ`>b9Ng@L_*Iv9g>)!Cp(Pn3H05T1r? zE$@sRYp1{W>AAZucS){OmN|&F#^R_N3Y#qu4Iks22*al#&}3N=W(ds#vrUN;a8S@YQeINM@I7+zK1ihHjg_l#J> zG@57dT&@r+RTf&;-sPDVv*{Ly@&1v9NDE8Ok;DFmT^|k29-7+k)^{d#Iti~VV@(Sr zC@~WG-8f;ErVS&+OzC>1quX3uJZmg?N&2?$Pba2yGZ)xe96c$^(jWrFVWc>SFCgKd zPAD{vU_YhCLM=Y5xWz4nsmxKb={yzT*u^=wianuOLqTErx7`v-61;(o187DL#Y1^; z*tSN57;u(bn`XU*?HW7ww1tFaM zewt0gi8j#tR2WW>b{2*Bin+sX&}vSkUVCQ2{W?#!jmN^0z zZivrTj5#x{hxmqKO3T5Guj3#{VNc3jIC@IX*y&Qf>Gmvb@Rp3lr=aZ)BP7tZkq7Q= zjzR2{+~?=QWA-DE!^NjPUlIK}p6(0x2F~l$oqiZ$G{sXs2wo?5qlmW5*TprFyZ4Ui)cGl78mz8Bl4|iC#oU(Lp47RoBPOmrb*Dv z?qL{$^RN4tSkE4#zk?%-?ofL&_FWYFf!eg2PeLcP;wd>kOb_{;as8%fJ)#R51*m0s zGjiCTqJ`K)9SDzUh%VX$y(tnOkqrO$wO13b!QCtuWt=}=2q``-PBR)hIy&;3SNPra zHmfJPnBW!kA!)$q6Zq%xiVa6dR8-WnqX?axoIDG*ptQ87FhBj-cfoA=pg&)stp|gF z51|j)P%$uMREBjr_Mf5t^Y8UDB%Qy5|Ho6Gs@jn-nC=zK{QNvr06vbShaDVPmSv#) z9SDvPQ1s`K69kASCqIcPn3D4bLz&QmbF_NQ%39!IxK{!x*f(?9|o%qUB@xkoxcgZFHAp$yK6m<_d7`ac*nd8s0e*OBzeZ3X~3lkR?M=VJ2 z_ZQE2{`ne|90}--i;Aw%=$B={{3r2$-Wd!3L*`#X{_m&Oj=$^L0=IgDjyjr{n^livo{F;s0zFbcAsrpY9`J4P zeDr^l_J6$MHE2ire{Ijd90LEfs0fLr`REa7y9;OU;NWs^|L=55|2Z89zQCs@Fc?8k zHipwe8tgX7qa>XF+n-T?7=L^RI{qS+540VO|vM{4oJzBrf_<#Eo%AG$e#Ftt*=FXGLNdF(xe!ngE|2OSK zL`2RjK{Q9;w3e^g*`5C_A^)3psoZj-ehfzS$|r@&1OM$${)=`gDJknwA%yk+?pppk zfxLpkXF2y3%mSknFHZPB{&#}?FMojw zQew|5eZIS+&aJJjqnmB0k_Hcj@BcYnkn5l6{G*lW{o)u@H~vS2C@T7NqVO{QkK{c# z_?i0}1xde`ER;;}p!}nDp-cbYnq>NqCi(M)|IgoQia&(?A>#l38&&pSBIxGE3(n($ zh53%#b|t3#{=Lgx=-(Zr?fZXA`}p=NaIoC}OmIz2>B4z-3ht)STq$dA}tHcCG;{N2VaDv7*(m)oKKc4ddC&16qVH3{`&O8_4+-f5?$Q+WU+Pt z0*MRZ`T4B!WKr3ozuSS^(~tyaCG|3($x!+3@N_cn;|E+pLxPdED-4~)^Jk2Mit2Wt zr-<|Quc@ehBjrg7u&+BvK>IxC&(_Xp5T&Ih*6E=)Ha7j5blXYVJYM+5LBC6-M8|h? zrR2Q+>o@&j(Fb+s13z`=yME)J7p|wc%pHbud|{=f%yH*CwUevYxVmqp&58zGx9#zf za9Ts+anjCVfJQ;;H&V3V9~$9{ye0h9T?XCj8{k}^UyD)4D9oq)vMA}@;V}C(safaw z_+!f*J}@T(t^`~bJt7f&;X|4`%9Cd1pL$v@1${y1l=tc^Q4&j;;>5kh3A#$JEkTJN z_)jPMNK0U-;_%Q3_Th4Og3b%am$><4 zXZLCpLGJrh+lkom`GMOKxR1x{Qm-d;`)a?;i)^8LYpge;gUD&_bD>h+QbOC6U?@I^ zqJ2wmx-&+C_BBaYmI63=-f!9|qeX1W?V&s2sLo643BmUl`58V+ZO0B{=Qxb+xPtfV zR{H9kC@HTWC*0i%kDb>}POUa=-r;S_;Bd{`BONsCs*D*@gH{W-f>&(G8nfunV7cx# zpY3s2$W@%fEHy7e5KqCeS}=DHT!Ovg0`^&3u3U5W3GmWoUdL~_mbE^> zdtTqPPkW)cd1#r#BKpzKGucIf*%6-&&HPxU$iQpabplWGX z;o=p3>-kWO*wpU9yQLq8_b?9UVR(Wcu`FAc1+(0wXKQT^oh%|Xk;&`sj$Mf_1_QO>@F zYa#iIAmrRVXZ*$8HELTo!h%=mmg_&V`09+~6{o@EV3~dyfBwo*Cknc*)p7KP`qgk= zHrq23cSw`**UBtm`<5x=HosGrdbA1?EZnBi0v?iO3>4|--{V5Ae6 zyCb~p!?#=ye`tDVKyIAMEtdL3P`bn*H~Cwxo*jt)7-mt}Di>+c_hWLqr{SdKJ@^)3KtK*Mx;yt1b3}-Mp+?o#&S=*WF30?{Sa@;@RZcqmx~$<#knE zTPcq>o=@h7l)Yef=9VA(3t zT-1ZiwFo~rP{L+4rf6O`OW(L~kIElxPd(L;T~b9L$9JC!U42?a-lQXpx5Z=C`%U<< z^OwKPTc_b*9&RE+M5i7{vN*ze&mOkQxwi=3JGjZ4Oqy90%Em!mPICMKpo59sPPVnU zMEtg2MDQQKdbpbMT3Jlc_CtE`D7cpJ+e0i@&b;mWxA;ewCvVy&uvIZsmzOps#L zib$j_`Kr!gWoq+Y$h~FuupTqX{9xh8!f8IF_^2zK_n?)lZK3l0Zn^oj)Yy5t^NzYb zja|i;10#G$)1lDV`GRH*kM7wfI1z_KY#@Gh#rP0p3;rtXo| zS-Qpsqgv?jyR^a%&+mWZ2NNr+-g&3Ru%^RSg8ltUF1jGc?%k@zj>5%jf#Iu%hWhK; zrmm@`s}r-6AjVT%#*RtX-@XxMR`t0{x4*4gnHm>(?KW}UA>#~{a4bViOcKVmG*gQX zW#-M?Fka@X+CS%y^ z{-mgCZ0t8(<3d{a%B>5MQH+F2nYQN9J@4s8Na^NWiu55APhPJ}cTialsmn}DL}AUi9;>5bPHCn=m(INBpg3}o{XTxrba!Bvj) zu4Cu^odS&&g$+w?#;p>O6@SAPAVi#&bAe$5T;yr*-0tm70t;p}bgQ=`PwJ4E2b_|? z)#B}*(rh$$X}{Fgu1ujDWKOM)lp4U17R?^>In1RQc;0&|MW1_{oco2aLT#BX20I-E zBc%ITs~nP(B5`;($$ZBq2iWthG<`-NM={V1>A>v>o$vb0Ze^YpR&SD_Y`x7>(RrLo zKGl4YPU{FG9BVgST9V-4f*lX?R_f zZUm}HBr_l&P;0-;l$Wq?DWFIY;&BqbIp>z-(%eVg&q_JaS3bg+zxIO0-=Zzh)=PU8 zy}-?#NkSlY#Y)iiU^1w{dpHSLT$;WlqH0X9TI#B`_$6y3?0h4_N6d015EnmR-m?;J z@MziF|6<5~+sOApf(`-H%CQnk%0-o%XCQ9w2J!e9d|~1sMAE@PEX#NVI04xXKv<<$ zLx&I_Drq}edJWClqw_LP1^bqZi|{mvxKB)Xte#fm}C&eN9yKu_O}T#{XIxIAwN>@B7Y`H<)b-uQA0zBYF4mQth?)~$7}2( zk71Zw?mneneO?8pqtMl+v#Scb@1R?XS;E(0TKs26aveo|Bh7uh6v)7$o- z2?2DvHtrN#HRxjs`sNd>_vo_ zHV2CcDOJm1XPz@JysW8*o^$T!F?F(id<(8+HAN3O?bG45qm{dPqfYNJQKjf0tuh&}_sQ}81SMng ztLASjJUic>Z+`V4D$y?2*Q~t7*lf+GC4SjgeYxSeRZo|OwnTZKM4K#3+2Z8*ae2C( z-nsvZG)nrm8A^s|PJ6Az`R=U_`f?!>kUQVE+uJz9w+rz{0`k0d->b`hh3iU`OAvbfuo}>VK8`FDJXvKFk;J9zMHF)}tz;ke@D~w5W;n%R>|pV{71uh?*o`_9 za%ihAz8G>_UQ+I}xz*vpQ#rdDH}sCP%1fD?Kw*`V+qQ{*km3F!FP5EykW>XjMXr4m zJ<4yD94@|nDl_K#2N}u|Vzp`~hicgR-Spe;CWTM9-)@&*2QH2}`aaHy;R{Zt^R-W* zlbFyMiKWbLnTmOs!{yzPuKI$Py|J)sYIL#hPufx`OA=-R~xJJ67cc zC|Y%XC@ig*qrIH&7`UZ0ra;)d`O=ntu@<~?UJV%cm}Gl{aQHlRLK8vrWp0XQr2Oy) zR-){r;)6}mW{H99M`D=2qs5@A$=lS-tTWAN0TJe^hpYAT&d#TZ40L2s&{$3P!ILam zQ{(6&wE^0G(fwi~14~oR?b+v`3klWjq1dWU-!tAlnc_M>)t{|Nmh_wzjMB|9;_a>j zM9evhhJ)ze14YTRc5?~++!WRy5t#P)B$(qUEMGc^&uDu=g)tQ`1n*{MX686#@s7+3 z(|#|J?W!-}9h#7nq z;^-Y&Q(sdW+oV5GR5O5uB(Dw~xb)5LIbpuHF*iJ-@i;Ne+7-)+@^R}NAEF5%cpS*A zT5V-hDmXnpFvKg{O+9l#)%#LBc-l9Ai%$?D{#3tTlH*Ppomd$o6q` zt91tyJsX2Ddimn#uV0y^rNg*fEhc)&_$i}vnTE-v<$D|H3K)Gp&}JLXbQ`V(7Yh%XOw3 zX(K$PJ^9DhT$7YsdNm>J({1LG88?1G8JH6hz~1X5G+nvt)GQBT2eT_E zV!VdkLoU&=Yt^cK)N)I^Y(D22X`AaS@cHav~ z&{LA^88?@SR_&&wJo~*ju0BU^{-rZEAz{@Q^Ie~ptVqb&$DedbKU-X^o+X+r;>Dq` zVmTE3Y;|FAe-78e5G)w3u#AEm{>D^=`)ejYwnWpL{?sG%;;z?!O`pM*-NKeu!>1ZR z+f=!yNgiq2)I|D`;?>m(rcNw@waMb$r3l;S3t!yrdpCvD_pjg5FvX0e1)JZ0Fbei-(_{cYw7)Z|RXW2I(EH!E5lnWfYt@)n&*^&|4e zRM)6jdjYv)*G^JMw>YNGzbH58jm3Slm3^u^F`6}BGXAPnG1}CQq}noWIAwFZkR4nn5>2&&h0R(yU(UmTbKCvD5Is^2J^E!97Q0@i!$BnjPdlY%$Iu$l*D)=(0mf zt#~}Peuk8FH>xLg)tq;qej!Fp{tr(1Qw|B2si%#l=~>y3r;O zsT?q6qWuwBYAr$>CRwfWd>l>}eq(nlwk9zz_;t&BllNn>MlnX8h>5>n{u*|An%3W! z?sSGJQ~mf=UQUi4+)Jn$qP3wIq-erw>AM~=Z`^k9s3(qa@q<$mxh{3Z@ zXCjY$+-a6O4!a{1j31+{mzIzJ2zu|M6cxtC(?bE}PzjwwPj>uYa#WM)zH~Zzt5-U1 z9U)aqAInX-!dXjbTz({a>W;f97(awKpFZoor6IN&a9w2VtYoY(EyogWjQg=2b3utue4tv;uaiXdX%34TdP-CRIJwGE#OT`N@Yx+lrN zCupDRx^KD(b^v&Zxi$SpNA5L_UgJjfG@@9X72ds(Z*{TY5uj{JOvXg>nU@iA zZOJ-KwjU364OQZ*7&yE6BLO-Fj0Vn$_ZzcUXSi}bn{79;KazNLJv_EOlBym=Zt*o6 zqx67hqt~s@$2Kj4EJvw60CeDGt{;+4DyGiMN?E&9V6e^sYcG#}c73ce!S;>FDw-ww zk9i)8j2-$oHahCYb@NWF;iwPJ0i)Xjv%mWyUM+^u-XFx2o<;8ZdGZ})te7FIhzT#uUVY^yLX2(Sp zxWiK^QxB0YyF&%dLMa;4AB^4#$~dE^5`S`rbO{m+NWL8Q_5LdB%tjsc^k6Cs_*_a+ zxn2RoiAj!p!Z*!%0^O2LlA*YN9DV^%|H*f^s+&L>B>~`cOfSf$*v_u>;b1BpDNwk_ zh%DjGq2>)C4^OZrJPi7sV{pUpLh{h)a9Jd9*?tcotH@l_0x2U&cKRfjc@h0U_!6p1 z$%>ns2}vG~0)X~9s35}aQ8c7<%T=u8a!sp7lK;_-G{bf1!22!ODb@O}HHY_Ifzx+~ z7u)pR&Y{n6nXpS}?vkwGgQ{bqA92A?PcOexOUr`tzoUOeU^|!Ba+c3whUW!b>S+sm z*Q6aMSLA}`CmR10OC4!PHjV`uoCQsPLUhFAkB`FvOozj)BfnsLohcc5v!HmDNGG)x z{Vw`gh)>v!zyCCpB-#kdXh3au9f7u~>5!S<#mZk?4eFiQJ{#&XEEJ61dPHw|7_7QB zhCKzo!lsS@6ETF&9a1?k^fG5#4r@|-gwjOT_U7Nj94!-5_i_LU$e1`r*fnhtPQ9L1 zG@VCe**Ji;O+glv8Hs$yYO}yM^2}?u19|L5-g z#HeRR$-Lh_$vKZ{M}P$azdEqJPQNn`M01#9AsC(f63*ooO$BB-YpyqL((^%8)ERPH|*G50~ zy6+e|BdHP5Vz209{4HDM7`d0brvnw7vdYgE>YTI6k6Z;z$}z6)61=e|&&6u}IDs>O zu0RqTjO)vCr_>R&`L2f`duH_|@H#^xU>zM(2)7i(02+sMf+#=~G!;aJ9Fa8swwpIy*eQK)V|MLKk64d)|C?9m`+0mk;2ikS`HSy@C+r1CJ&BbL4uU zole5cy+Q|ndJK&dkp_pqI6h7t=nW;4pnUOU^niPrb3-kr5KZ>vxSOWbDDATh>NB0{ z5wniX7c~2AL{Ni5IRcEj$Lg?48neK%7%yylb^2O0%6bdCd@ALtxd7{QY6J@rd&~jP z?pi<&s61sDPYX#)bI-+Ws9r^HO%$ed4ozMV{Hv>@&p6z@3b^3q7Y0LcF}ZakY~7I} zhnVCunoR@40&%~7f5HB9UK62gsryz7e!r#J-hEJRh}-C+B=i!&DMLF|G#|f(`R>$u zO7!#RPv^T(_$kopcO}@Y^f3&h+7&YjzY+Dtyc-6l@~FT**B!1ykMiXyl$k!K<@1O87^VUGhaZwO`&cFUsZYcJ@oQLT zYQDI_!=VWJo)XWeDU9AN! zb~f^itG{!ID~L(+@~G zu_1`=WBhN9K32JOmZjpAYI&3MZ?|8kOIfoa-8imSH=#c;U@ushAV?#%T%!-XxgO_ExFH?6l>R>Ur$3%IkMwq!0d)5A<~qYI$=0U^F^CsWY0=Yqq0Hk5VDBaaM0cR zMJuBXD5pcXe86GLXvf6wg5Mz|wrJQ9}(ScYfnL5f>JQq54aV`CV+mS`wYpC4trxwrswJ0rzyo zVPzvZj3TJ6z4@*tdPEi74af4L-3RxSqinA=*F>Y}OlRTvY8;O9<}$bDQqupnEBZ1w zjs|a&J2>7qLSd7Q++~qyG(+oK8pL2AZ8SX)qdHMv_g`O`ii#=`oWb$1`_+cp*+h;y z@}Q_d+r@J3hymy!1dibJFSfmi+m>+MLgZuXp|F_0b13I6_5WV-^lUSmx{11?zp~<&N#x@ z13P{$le4OsmqlFGS6d9_!Ync>Lb_(YSUH>{Pgno7yhZ0z9hb+YElJ}C#9z}G(VCf? z^S)=zxz51x_dFt4Hu}8s1aqD=>a5_?{%P`dD*0=h?lX;QBm}5&l}JD3;m;<7M7DPQ z%&5++_l`lct1wQE5N#)i@FN9V@o_tB)H(2LBZF zAY0{`ErjY-Nh#P1L@gk;?4u!iXJuO2t?QfVDFaWdqD!)$%_+4r`~2ArCFa=Y74INt zapdjNmneCc$}Q4Cwu;f_PBNXhrjj&N*rjyy3Z2(4a1ZBJ1K#9axWp2@htk$%QBv0H zG;)n7aNk|F+Ml0+sVm0|OLjGcWq;{<5JSAiefZvtD~OE>9+_IO>j$Nw)C}eanQ9u- z!QLOKn8to1X8xCQO%?Ky!`VZmTm>({IMSagEr! zl`P<6JWrp^Pk&~)kcy+>p5@6PNa=tUo#&6(17H))9n- zUd+s#++4URZ6_j_rQ)h#5J8sI$(cmHF52zX|9xY!Da*^k1NEvUD6{I)^-h{tdhCL& zz7tI_#x}k85AHDdGMMU}InS?nk~k*IdYt8)$=xp6@TCYrTdlh3A-FXEE(uz6vJ>&O zmmFp(syDqO_)-NHhWYC9!<83_0D6U*j5+@%RqoZ0nekm`NO;Ol(2T9*%e6>;KqZNQ-8fCxyuo2qJERViVlvz{A1V6eoJz zuRy=S^Yt;6I~XEtq|#}cD+bW%(5OHCI5UVqr;>Sm^|<{{-t4iSIt~vn@7V9(g{}(u zHSwpyx{XV@CLJNTX)shx;`C+4uIBwILxA_+93RTNcIu{n#Vodr>p`wJk{@{WO@kG2 zz}{U#j?9+2Jdnm63+|l*rir&6HdE5D{Ux-B`M6`is}ss7dK z!bAiaP4_kkuiL`u8HAu9=|!g z@YoZioAjdCMjZZNdaA-MYX}>Bawu=hzpA6tgji`G9&asUOZqRaOHWU4JuXSJ-WESO zJ|4Vyw_K*yYC!ew`bO2dXL}J#yR2Zf;G$1y{at{iAOU0Rv`k@}z;9+ooe0?YjQ~Ss zx98=q7_rXsQcc<4dN(Z+GLUi3M|^Gd62WE^2v!@SdxBFt0tH&x*)%J^(CN{FfLYF@ z@ilW2nqImJe(Dv%*9^+lJE8#^Qrg~FnF&6))yEtDc(7&|t2RIW7g1js71j5)O(RNo zNsbI9h)Bp#LkK91fdT^(0wRrc2`Jqt-K{c+(kU$K_i6%+Pi zp9J(s*+w?@9~mAIzqWb}rUV=QAy~?FL+GioJpU2LpIEB6cE=+`m zIaE4a=bOS=f))!*KMs%Pn;+f$0`CTrTYBvUX4B9d>&)LoHc(i)GEb*Q_GELLN6lwz z(F=?Ua=t%i6l+}J?#1Ru#06r(M1Af{czBbp0Ztt8d$p^II*k1-yayb2CKK2&%}oz+ zyKcZp4AF*P{M>rC(+iiTQ1{GwSDSYZCJOO&wU4!3`_Q{frvptp^yalZM%}P*HMdfe zxXvp4nD#KCr|gK*g>-$QL~UtI31FlK1EKDPRoC~vp&|b2v^_ap$=U3JgrEKj)*JST zaNch_GfQ=jdwg|O4$dj$WERBDCCxE#Cc)|piBQg*$iz7!!6rEM{MH@u_5!+gV$7GJ z=`wZ74}2|l7WJwgN#fqFwmO~Z*c56v5OT7wDc`QvLByR`murOM=j``hsGigFH+AdV zKd@WE$Qr$M2IKn!Iw6>(`^yk4krEDx$G+-IKJYLkpx)9cotN1~=aRhN%Q}S&)+=t; z7m+zYA(0H@<)KM9RvPP3Q(aaX?+@kc!`*Qi8u!Y_lMpYjYC=Exz*XSw%^5nF-ZpwT zL^^DWGry#^Ep;HzQGLX|{x+mm$woyameg zqZ5Xy1;5#=yGGj_y>ykz3Vwpxp%tCMq*BJim`;4by!CF?2NhbG&hxUzpwU%H81Qi0 zf^NPBqhb1kFB7s4+_Jt2SG?>S5&pY)i|K2^ybSn|*dh6g!~4AfbcL%=Fn`m#p@9KE zD`XV`mfhi)d3JgE)3;u$LghW(vF?F7J51S2(Mavmc9oONjP~Jrw_^Qmoc%ED2nB^< zVSH*++o|O0xD{`psUA-9^XxS6X$}3UPuayG*IwY9(Mw0f)da^xN>=KQ1nunmQoF{` zJ;tY=O+{dI*{8zs6P|5Nes#SQUli21yB$qrH^ux*5t}=2BN;Y$6ugzcUPlRGI-#NX zS^CiJB(VD&tmo$x=lL0{V7)x`qE^V4bNWGL0^15&>tN5&34HSFM5Up0E%{}Pd)7`S zt|%d`4g$Wm-dZri*|AK^{*Q{u-__|dygm0J)+vgU`Z|rLAYyxN^sA-I_OBv_45sYP zNs8Z`2L4`qEGus5??Z^f4D4~KA$P4ml))+=LX&9JkTIl`46p_Z4Oya|78T0j@i(Pfe-#y6!0+QkR&>n)~uMhp6e_~l_$ zhIcNg_Ji;H@=QT$$(`f;j=uTJhr3yV!iL_SREuEv!3a9UZX<)%9)onf!J_zlQ8K zd>~}k?LqycFT%*=&>e9mUNB>IhPw>qb0p_UJI?Wg-{oe3dB8lW*;XkuG#XKUTtN`lwIlO|8?Jx)_MEC zfJks$T&6O;;9&DWm|?6t&hZD=l+@4BSq|A?l=n(o2V1`lQta$H!n7T0ola03mmUs# zP}Fh6rajFXQ#Z5IO~z=h_s1 zuT=f+)WDFvP7PeqH;KnWgRfhv9!(VL5twttLGgT6E1)OT9<$NMCa6dUFE+3H!%Vj|*zRbu!uV78jjq35 zvHf`qJYC;=#odEMiOTEg<|#y62vwxEbgwefoYISX`s=>V9;hJmQ+%mix%*;=^`UG}w(L zF?@WB%GjQ6yWM64qI%ZK-UMrM!TY!^2z7dQ@&g-+#1NoIF&=J$hwl{czhG%ehCpG* zoZy!B>t(yAtGE6mm$IYqxd+zovl@E%k&u67`sl5zQQchV{)}2Ctb=60Juin3fI?0) zkB{m9$P|+_17rOE%Ecp~qp}x0BTwJX(pSx>ix*KNB+~qGZNrOBb7Dq*cS&*ve#SYo zLn|1;;M4LO&qNX|R8An)WYq>3Q9*Qrh2f>;R&3ZHe@pDf&F*_;O!e0rqz9$toN*r7 zpQ+X~hF~V@mgFI(ugY|03`~A2OWEKZo_JZ(jVf!n?obt4uKr7+Lqh?fg7tW_cGqN% zJ2dV%EGYjuXocx>)76UG`Vm;?d~sVSbolP;AkuIols)mjb<|$YPHpfse=_&!cZ&|X z)*Ey9-0#~1Ju{x(?=$~9kO4%V> zRf%dYvC}`THrQQ4+PfDx@Ax^BEhqAU8wW6xUd>D&lIz=WTU1QHQ%=yhL@w z-iCg!ZTYYJ-CkR_ohhrJn^_cFnmrJk(l(UgoUk8yuE~k0ozd42Z*19jhlBnxCVnxcFbB^GHPGz41cO)dR;;c@69_R!KcPYgEttO4ICqduIHvzfE&Le1F!utr(AZui>*s z^?BVYQuiaG_xNaTl2W_@OD3u>c=Kr}A-Ks) zNbDobqst>&IX(ziST)W3#jR^thNgFAz@#`=(7|O)xb%)9abV%nuHY|q^Jxut*BJwK zm{yYU@6Psu`2Q_89(@u(JDq3H)QSu4Z=~6L1Y{Vqx{2OWF|o$P%DGS?!OrzXZ>~gz zd4GVT5#7dS#7%((-uLEWfedV%8G|9{;EeoD{ASZ5a7appOLU0`^VGV0ie?j+5kF-F ztBF6E&6+Q#`S*IC#PY5m#?&V+O%!)&RFb$#Hsy-Thp^7w-fb^bR%`Ze<=&2DGwZ*$bA z&F(1rGoscl0p-zOdx>d$puJ#ksg7`x_m;nCTLf`u2M$W-T^wi&yq7?-Bn$bD^Oi*f znLl3jT!QDS5sd^yl9;oyCBTy8tN!GijXjt&FiCa(4Sii39#qBm)nV3W=Fq)5U0#M+ z_#_=(Wzsg}Ya$LHmGB+zVsnXn-LbIID=zAgjBTw5VZ-I#aphK{QtcSws|6%c8C8Fo z;|kCT4=3hwLN==(ymh%5d83K_+s$i z5L?5Z_%O5ftE#os0mT=%KL4j*R_>Gtxf}4m^ z(HwyDn8+#ogpK1_u7N0~(DE5APi^3e{#&`s#hIXw?ds$x_J#RUvmRdT{X=BN;AQtI z+n8&>{bPgsV~Aq{uc$ogO@dnE6U`dF-{eD)BW41J4$h6$t%WLK;Dk=_I}wbaem==+4K4$=QAaksPXUAD1NTK z)XikQ!END2lOZyp=8R=^W!hZj_TPjd|L{U*dEP{A1Hstnw92fB*&)|#ZM$ar?d~|% z;aFn7#9`|6v`kOo$x;TA0Rny4b6F25M_>4x`&a^sQZXv954{1aUM&1e9ECHGKFskw zP;zt>7N|cm*7w|LwXm@{JXs}{5RCB*J^VC4W#;*9WN>rfv$WQ;@5z}^mAg~W@kg|( z386pZV5hHHg*&PI{fDb`VIMx0sT#ceIREvE!4e(l#}c{{>9W+VBvE%>z5S0XL54ml zOSU8^6mQe=np9Wt)Ibz=IU*8fY!>b z?%Lovv{+3fD`9Adw7(!RXDo` z7;LDas=$9QO`*`xgcn-B%rXs-1Bzuin2c(v|*%eloqJ#k7rD(JbAic3fGcYJjyo1{~ zV!_W5Gf_c5&)HZtBEz3Y&Hz0lfaWVVaCe=8MI;WBpyL0;Q15q^%I)AmhwiO6)eoqo z75uqB5Z(WNQ@{zfcbu7o8K>izz+MJ}79_#9a^?f0k>AsaCIiU8hZMJa4Hm5`_p;`@ z!-L&B;bMyo2biE!fAZwX(aA_d+U1KE5~-pWs;{TbYHgmS1BOHz=<4=uHwmD#jpCW3 z{&i^=E!vYNkDUeAy=oP`vcisp*8E2bQXrg+N}@ERcUdsx5|)7o#iM9PLt3J)7RB=7 zW&c+U*qyiUY-r@fR{X$D4=9|hZStHhugqLF5)yMF>bVzFziXYd(q7?7PW3U&)>7=r ze`s#sLaTU3)JSk&_k*0h_?kCPirmdY6~K7lJ+I&Z_?V;Mq~GNsz7i*25gxm!`@5HH-g1PFns<`CeX2@DK2i z3&JSiG|(wQxy~8=4Z~nPvom~ANAb?cCdgSRXndZ~Qn1+2DErhqX-@C^@uSWvVw+=8 zzjO~1if9LansY8$2gT>8W%`b#vKqO=;yaiK@wIR%5+<4mpksaWvkZ#Fd)@Nc0UkO2 zK&@_G!Pieauea%)ND%7W zka-a7cLB6~uGD2>z^lat;G z{qA><1~Ui*ISVAJf`RalZ%I6>1Uj^h)RUZv=!FT^p!@e7)-9&r5;yt@Vp->8P~oiD zt7qh3lT60NWu+lSd7EwbS>u0fjDuMF1@Q;3EBJD?8+k>V8)bOsVtP*7$EZ)V^hOl`=S!I^w@L^{kRG{a0lV*KpE9b z`3MsOuKge^wK$%Sl0%qDgOT|+S$CS<^XgDWJH{bdhjo_o`^5cqWt;25i%R%~&p)s- z4(}iAH5v)@k%x&TMlc+>zVJ29k`8@D+`xCGwly%Vj_NX9s3uHME`ByN`?&IFTjkib zz8ox>TFgpsijclmS6F!+09j|`kli_Y8ZQm6DA(NjG}CxW9g2jRtAcjk5B3f~N6HLz zr1xLEc+oPlu4XZ&C2^ZB@{TAyg+PxTLg|s=ULl=BGSUj)M6yjK3XWQ5sOft+#V;n~ zoN?IOneO3;OWKHb6mR(*wsUQxL%mVuV$T@GfC(u>%*rs%iMm!HAk^D4dO}d5NMsZp zjD3Duo;MnB(PA*!MwnIHOU@b>p+zf0IJv}GC4!r#mmz7^aUE<;1^@A%Ka{qoc`~w| z?#J7$xztChC07I?&HlEr-G8m+CQM8X~Q6Kw=TRQ!?& z+Z^uAp5nBl7Ibs#pb^rJrI(q=Ak+4HZ}nRhR>CWr>ZK-h>wrj9`Vl{7$Yivk3~us^ z*Hm_H3YD4Tj48Q3Ig&56ifzYtji_f!)5GwUO$_H)ZPV%LK#Wf01dCn+n?em<9nnsNGUNo>#X(WBguP(28=XAP?B+HcfS>hno;ZQHv+AUOzGD=74LRlycP=D(V` zM8Mo=Nof+M2T8nm$Gv%7+1t-!?gr_-1YE~o{fF=^h(odhpKK;PKa48UnV{wVocYnDF z@W?(+>vZV8cjkmPh|JNSabvbG(J`9}a&M(%oP47M5OxUWF1k@X$yxd11R-aoK9*I+ z(qwceoW#c|a%i*Zi+&%~HqVtTjiJu{CH4}Z zZyTs)jAQF~HEjB4-P%ZA^qoNbvVLtIHHW)Y_smzC%A2P%MzWXw#$=ofI;+HO3vl&` zK7uDw7!osoBUjBfMu!f@jk}x+w7j`%^;*M9q;<#%y^BJh+?s#Cgu`LWSgqVIQ=G_^ zaqWVXB(g}ls?-0BuK3?p@55IGj>76Ro-w(**zz(R&nm=m`!rq)X+WY?H%HV`4(0nE z>@L-LL@Dn&(r=UI=jA{a_mch+=(}8x9zT9`Hz+IBC`S;HWN{F{8gHM((a*0C=l&_=}Jm=Xv)Uc@TCU zx8V<2_T=Xw8zv8`888UtzkHM|$390Xy-oos)s|kY;1-u%ZJ8T`#U_n_1L)odN z^MMd{!8WWPgTH5xP&eU8)>Jgx<2h;^(@30chVdLoX3z z{mJXn($XN%=tth!fR{qoa86>$O_P}{phYXRqXkg5%D-+obFUCi?+@f-!YsnnVI>dm zV>iaiNdpv@4A9rJZmp8eVLqW3rp83?F{<4agP@N-aM^*nAJZ+@jn7?8_xXeyG>Tch$dI{2CB&6*4$8zuQVz?626c zmtiA!W9I1wOA6D+dTuUqk_^{NZyXkCsvs_eW#rA_)a|)In-utU)9PDY7KBZvRMKFo z5lX1V^OPk@x>euw54pVmLSg?{9kIs|}CG|3iFPYW0EkDDC% zh8eT|B(A=B(MP#@xrYS$cPkU0sH3#D3SPQ?+FMM1E0oA@$hX;=tS}WqRUjueD8!q%Jriqk zZ$&2vLM6B#Dszym1%i>{KoLeet2CCil>a@rp-lxFrMTExLQ(8+qiMD=ooWVUq_`qg zX06wN)k?OL$JV-~A94BBjGZx<_`)5T9*nh7R0a(u8~p!gk00zKy_J9V)oXs-e32#7 z-E@j9LBW~MeLu{Y5-mo2wu!u%6O9C&OctC6bgSFm*W0otao%XZ!>Ty&+fTehaJMWr z2CsJ<^P<*K>BShe|L?Ncmkcd5(=lF5x4u4I% z7We?qcR^a^RJw$)`!2CPP6vIOC5}y>qda6}D2?<+!n96BN(lV9Mh*9)`%u+7?nm2u zb$lPpk1>!azMnJBnw{FwH~t$G?>4dPA}7<+NhS|!x3s_1rGeHoS2V2Lx_RFLhen}) z%9l?Ae_-=u{cuXfOQ3eO$@pl3k5Eei9J?zz2PpM5HB3II?TQvQmaTh&^R zhL)!}pEc_eb0j<-cowL^gQBu2i&Z|0@?}H$%GV|QT%@O&xRE&3kRg^EJ=wL^#(5SC zp-R~+aE89zDD1sDg|UF;Ixv8P#m&b@)kU{0Y|gLXK!)wwSD<;ylHzRhuFE4|ib#hy zZ%NJbQH_wVb9CHG_YX5dpQUo!rKWOD8O!#xO!ILPxscpG1X2m^8#GtC?=1F5ryn9M zr8;Hta20FXPgv8q1p>TCr4fym&$Fj zR`B_z_#Z)8K$@)fnko$qGL4N{{ogi4WFUu44nbD|6*mkFxg^y zq?gc4ZLu!?0<>tw`Aout>Kr@W0eXu^fki34Gp!Ko?t_z)nJZJ^pol**|{FoVMk0^&bp@0NF|{v)x=8-uF(*UwK=z>yFyFHh!y| z<1OyEAo%svhGQz)7>?z1?@O(?MgP%Su=Vr|`!`%KbK25N<$}7~v%aamOs&mVCI5QWgsr}E)ftuOcdP98vbUCZ6 zjD%Au;^&6gIo+a}nen=%q3$s|HjpGd83Ri_BG$ZvINK@0&lyTJdbzvwMioXXeYR7f z5l&Ckk5>4;aSORmdd-D6`vp&I^~J3$emWDOFZEYIr50i)?i2YrRGDB~L^Aet7tTqs z7Ay}UQ8YwI^-MA`jeS3@8VN0>a~O~RqG3vNc9SvxEjMfSb5KbO!F`rt>S9Q~sx!h43NijS=LXa%2t@Hb}(`rP@jzbs{>ZUDbyEgV;Eov7_W7@FRH zh`wLMnQbr4eV}hx{HCbMkszU!)TCBW@?pmHpcKB8y(te1N-gB3eb$cf$TN5ZjQMKJ+H_vI`^N9*em;2ik?DYwl~Q zbE-S3VH4dB6?m2#M}aHd8agI%43}*u{*ygS}gc5FTZGB-SV*!-&Jkuuv5;fJ|mWQP5HlD<@vqT0L z#oI%6AJLZJA|}?wHe&rIi4`2wrWfvVL&rl!rSB|`140=$pfy$jkgB~vZ&0;EPRyiM z#m&JjDi3j5M4|Mbc}OF$w)lfTEoe|L=Oi^qAj#=A*g*(bH5m>9P z<6FqTk&rIUH&=F^-`5a=M^$HWLk$>uvtw@R8*m|&Z+TXxWas*c+JG4m9GBiYp-};G zt+u0+e_9fjM_jR=*BPJ_1S@SBhi^-*#|7}Y8zXq|SAo|Yj-ReD{^Dw;fQRXZdQHsE zHOE2P#fU8I*Oc?g35kbpkE@PiNyIV7?Yh*#@4c13YdBb64JxpECDDFB)w(X!A%!;s z&#d2d_qL$rt;%!$K|FFgvqoazM`e4rojb4*f#7DDSNSVj#}kPT&sKnP-ME}&;{KN! z;q$dek?r$Y>s8KJG%udPu$wE>7Qzh|)qO!ha7usXg)89hI35Rqj1_64AAGf(H|0dv zki3`kuTMMtFq+J9XAy^*)rt8a%TYr!f?*^F(g9%vZ(yTGO7#wAf@NJ>+k9%!TUjxr zuE0ugig%6^hjojMK9p!-j>7x?tA$}1K zTm~bjxvq^vs&?Nj+LKj_74KF29FiRY9qO8*wlRr;lOgMjD2>FZkd|PCR`Gw+4oka8 z+|YxD_*FT9R0@7B%}vRb_-N$2HN1i}g>28gDRF0Amt8pmPnFI}x`%q2&8JX&Ql zU`0shOnBpT0JXyX--t@mFf|;&RC1&AN1vCrysNXXm zbF&j)1oYla+@&2tQ*@%O5Ayw8qb@0t%@a<;5x}U?wN!El2M6=yu zN8wfnIu-;cV-)7S2H_DHc$o@Uc$Lb9&=wcYg>!1sn)S6 z?9v}dW5Yd)Nw0BraEs;Id%W3r^*9U7B5wZ){3g%5@^aec6i0QC7lXvFBOt|u*zuku z9c@^h9#$s8Y8@)rA4WJ}t8g*r8Pv8yl`9-*8cl0ECJHpgl)kvAnd7cB&ctW&mmKIb zR$I^wcZgzZ3w+yAna%i*?dG2+{*%pP*5q#i#da`X*3Bng14Q7?PI$w70h-BfEP3@} zMVuneWJszJA`AL}-i&o2&i$&s&56HiN`L0kEeQbuvy(m+mUyM0o9esD$Ab!S&!{|{MC1Ew zJJ%L@6jH=7;3DFBU^B2o>h;+mj8@K>@mK})ZYj=1g$*nkH!H)v!zZw_B6)jI z`o(U2Y1QnPP4_yh*R-{GX1q~!-LUQ*u8q9{n~PA>V&ab4FoW61D`&y$c;>$F>Vc6I zUy2NXNA&o6R|$Qd2-oqV^mAxZijw$z@=B@%!X2NHyg;CTpW6&rhNtvM*>uRl8FAlS z=;=z*m~9UaZl<+fLFWrH14PWN>z3^@s;2=j`?lp_A9ByqUezVMqKJBhyV+jQr@#>pF2Jl;%p;347E znIzdKqc1mnKXh?D_PHLtS@62hl26vKJxntuN+SlTfEy~EFhG?fccq=a_?7cE)FB*1 z_K0Ve7(p5OlfWt2SVvg(ps;FtHXRir2a!F_@`#uHXWe{WU$+%mbyLzDV7ifQtg=;! ze4I~{vp3if|6)avU@@Cpt-yi=s!;dsCZt<7rRd2yVqZ(|wt=+0H| zjw5aVuJsyhY&OTmf%(+Ok+qlk@ z(xBG%O7CCDv%ChP^o@$?h;iG7o1hf}MvPdpEkd)cLu{sZ5}BMVmIGHdV~3c2yGM1g zjch-u0OJrRxL3}Ak&^%#-&lvOa_4Xtp=u>kOqWtu%gXq>S}L+}rou1&Ki|p)N#Orb zd9?cl8w=yYg70$~3z~`W%>DO~s~oycxJW6jHt`~Pm%4iI`kEmzois>7f`q#A6#bKW zigna1W_o(l6guuM2j{84itqwJHU~aKy924ZO&w-8gU#a-r<4^HDUx} zlCMz7rgxtMYp3XgAy@(dAPOEXs8opjf!Vn4z=+)kKly%*_jn^e5MoNG3b=9=JPO)E z!W$~6wH?|DImO8q-j2NjH*MOh?pXom;M}c|tDUvJDrPolbIrJt5c=HL_>Dw9)0baT zWQ(UQ*w1^*T;Wbx-(+?TOXt$$+J6*0Qh3b`A_xm)59j+^eQglqRO^xv7 zL9%Uw|KTAiuz?FH$~V|Hes~0gS(%^~bO1gjNu`6iw5`PF*HF(DbuK%;H$)Gk<4)Xd zIIVEYW?fW%ezT6=$*y13ri_x!TX&Cn?Ac&s0%XtV^)O}rWYom`OSBnAu$I2|(a6#Ff4)>VW1nHo}=j9~h<=}#X zmSGHq#z99AM#U-vZ<-Seo(tnuxD68}a+4Ueoa?;pe+S^Pc= zv@6Tw$|9wIZnNR&1Gs1P8-h9eIhOx4^P|sWJII({Jm6Pyqyv zD+Cj#HXl(c^)Xf_d3w1EA9RnG>Vz`i!?PxJds%NvLTWCOsKz8meXZy$f>O<$U=b&| zUrOFY{TkAGQc)AP;1-7|e=@(R5WgoU2+XA>14uV4At#^-htR-9#wSa%3RS2 z5e$<#G#U~8zW0K)(&KWOD+ne+zVAaVPj6RYj?k}1iK;K(h3d_1840Q9kZAUoxuxew z&MKD9ez5`Cp3`zv2}K1n`GI9ovyq_STSJs!vBthj!DO7S7v>yj)n187vtB}OP0Th` z&BoU=m|VYOL6?DN*%WGP^Z2^K9S{4#K1l*yF^l=uR}ZC>b0(y;`G3wZI>o|@6Q$|W zoW%dAqlf^H|1(?5hUDkT4Vdy^5BS4^PB^U-mSQMW*ZKrxg`iQcCGk&e^2qlgiCC?i z`pG-`ur@tp>(DKbhB?Xh1 zjlv7@-kgRMboO_@L0jzjRZ0}PMIqUYx`DR8<3?Maqr>{KSvrL7i=CdvlH)Ac2;G0R zvU~ig%cnm=Yi&ujs@1@>H@lkZy;p2k#suE4f1@ztg8`mZ;KGqUO(r_u@)3(h(1LFN z$P5=b>8ANT;iu#$F>xX;oAzvHxUy!oQuR5q6sB)Mco&;k7 zo^;8stD9WU$IWm>6*)DAvL>knEj1*}$#jNOs8N{^h8{}Gc^NKI!frgw#;<6no$NZz z88g*1jzbsAjw?|`l+(vv!?b;;gM}MQNR1ZIZ2u*=fV4(v0 zRv;AuLyvaq1WCGvW7_k5$@9NADpv*oOtVI$FE*KpXZ?U zNdo{A9Z$4<_)0_Jk{~m{IhjEM%gW?BBGIiA{1!bK&%kv(bSnO^y31ZfFGk5j)$~Ee{tnNY zOAmG)uwUh;NXQ7+kfwsGWMKP#olT%fm0LK0?LWJNu}7{wSQ_72Xqc+B6dp$EMb@E{ zxXKaSkM;MXJFX%88IW71FwbhkkxuH`qui=@hX_$|5^?>SgwI>=alVf@-WSc0NN^YG zkdx_IyG!$2s$ZoBJJ9*JDtB~OD9>+}7Ub2g>aKCiwX?tGGPqb*&Zci1fLnmy6#1XZ z_-7y-##0`9YpU)1 z$HoR7jB)dWa_f1Sk?8>7`3$`CX26eMFPOD0$NcCForbPy`!;UB;P0Xw->e*4+}T(Vcpp|qykZl1FVR)@)h8Y8(nEXDh2ODs&$!B#cX%Vg^H;m$pA)5iaR zW$l=UxmVKusYK>X_98$j^qMtEsmx08omFg7virR5wR?gmCYI;r9Ay)Bk_P}lVfHd? zbOv7t&_5q;^@%7$hI5C_qz)jNMoxC!QqDU&7D*=@RV-{S7!s-09Vch!5b1i$q|lWB zx_AO+{oV5pdmcXnuSmwqUVNcVUb4ls3Ghhm>f z^*f(Vep|H(BGV=^e8KPUt}Ta)97ixt z(NVeT)RO-tsRUh}7>PkB5zdqCtQ7LuBuGwb9OQsuUqwlOk(5pr5T82NtKQ!I12A~6 z-$BUb;rGw(=wgOldBbNe6)W?}l#fzWSo}N4sNVh?LWs!L*6oWcy1omfi-Fe3#-F09 zQ0XL`pcH0vlGh(OYTg7@Gj5r?jet0;rYT_(fOB45-A(kgkIZJnrFUsX1u*!-N?`of ziQoeeiG_y7lU|?bH?*HU4bru26*4C2`&rlBC~dDR?(4!Wb_N8Yh zO-f*+2o|CKIeG^Gh-}4MOC@|OW}5~M-z^1h7uXPK){2YxLHp8BXav=oNnmSCz=WRy zYPrk^?=^P(O#OoR+xQ&Xjs`R)Ba|kDMATH9-ax31lqH63w$OAyC@Z|G9)~G;G#~+b zry}&OOQd?p6la5{6(#n2GT(nils^RpJ{bEi0uYMq@At+z4N+fnl_)->8S87T?#<=ct5jQq#q%r!X+*w zd;p=`DE!sAEOMD*vY*^?RFupMP7nzz090Bud`@(*WX;TS+k3h40jYZCoM*f|+~d|5 z4z;}G1GhOOT?uZg4Lj6u5i)~mo=>0#*l!TBpQT`-KrUL;_8Jrl>H-|S;M-LWt>acWe}Y#VEH<5#{XGrX8+#qIN{Zci?Oyv zx%*t4;#V%An4W})u=uP!2d;Ftj(QKoW{a0EA9>aRXFrClqmZ0o>)ajS&^D`-KS4azk1rz-1SrGf<4UB#yAC8ym_Pjy!ShQ?Jnv~ zt&}-Z*FJl2F1Ks@D7Ke+1D#V^UN*aGZus+rgz3pJt}ps&kyhvpaxEbp&-`(JBEcB7 z2SZHWM-U->pSzr)M9yAPiwoWj%);0zI1b@H*F+h&dxwlBr!(D6*fRsuejVI#k^&tkK$BQsDTjIi6opUiR`Z*)|&tfM#`|v@atP-qY|+>pbr*x2`=t zge4s8ewjnR^uQQ&1CMQa52ZlxS-urzYTb_i5cDg2&t1ofu}Fh5-z>R@VVG(nzHm#b z%#lIggs^cGT2+@GqlSA)i7wt%My|$x5I_LPXE)b{POw$O=fERJ zN5ymJMtXCVd6$|#mapmEV4ZqLQpWN>CIc)wc9S%ha&B%_D@i^z+uF6;O`%wFl?#SG z?Jom=QyXlJEt}77(Va12*)NO0iP4j8oSy^uE__b& z8%(BZkFk8GFPG@99QO>ifO^0AJ)I#bmg{_x%XWp##Z#xcwl*%$OC=e3iPXmKipgM) z%}Sy_f1VhxgU-1j<+9k5Qq9_$MEzndz6E@uOMMrei*>PSZ^BPz?989ObeG;d7s_of z;i9SrYEId*{g#h&SpGKe!uOMwA{bA^jQzj6Ig;GrhC-FqfP`O2zoDRgW6Ac8XaPz^ z1!bUn;;zqUg^lAwHyOcD)!#3$%&-`Iw^1d<@&1Ebw%iGRr!0_!BWr!Z4%ffI`S$&} z&)AuRXX0hf*`;?%B7=|>Hs=JM=s+3yyU{;Sr!T$F&LbOWR}TlfzVoNpAA)U7vwPg9 zkMqOp#Z99^*{$FK_%1zgSzNa%uzrr2dl4F*cG3Yy>u=izXGhBo_(t>ff$2ZPNAx*P z(q4Ukj`jK9qPcDAW?!o&iNk>nIL792XL{Y1K#n~H-IYF@&3c@%;tqh>qrqvx zpGhKC_9unYKmYkz`|8Wbz7L&ho$}CVUeU^K8R2B)*71aXjIq8G*G+9WZH8C#B@vlz zW=mW>`G({{Gg-yDW0hBwOpF6oBYh+oo8h)LzAvoA!V>TRaTSsf$siHhvS z{Rfyxr!#OSVey$$iTfBoKj(8nBb>Jdt)Aj5?rCGaIMf*Z^j^5$i&0(bm@jJ`eQVM%&2v%O^L8(210w4@3PHvz zL+sFXWLaOnFijt!;x+a3IDtVs$<3a1xv^Jl0_adYv`XG1x$`r+Dk}u~0iO2hwe-J> zf6&8?(_N2RllL7UT+Cq()G3QM^XlWx zwpm;ccu!y189^?j9`vZTGwp?nK|0 zyx%A`3T9)O+~6weeWV$#LIY3ET?&{W1EhXc1b6*o`UaFT6!^*W4B{4(k=)q#i$U+G za6?md^6P%6TA2hQro!fQ8W-!0?ilff*RjYP#t8nA@}cU0=lQ`%iSZ(D{{hIoWK3dT z39%s6HFm9kE@}RA`9@Z40t;@N+5^&z{F26`KVQCGStU;;!dSt=4IAtG7_m+5qI3U& z@6E%d@>JV59IHF|X^j(ZH-O<~NEfo6)?o6x4nM}I-h21P*_w+_TSbpJA-IV_VPw4w zoWD0ulQdQ3(|M3dv}r?Tyfz)zZqeW_t*?YR_`M(BqbK&RvLSm2C(CFU+3PALEiooa zSCzW{P@d|iKNlx*t56e{bVNa55hu=9mCMDTx4d-@*QWi06P57p(`PPXn8?PGb$LdI ztbXb}?Kz#Tv2^J@X*XDuHR6i3{nt^DBk|V;rFq_G%Wa3D0m93@eEr@nkwR!<7I8N7R*V7 zVJnMP>?_IB6?$bD9u7OC!_V73dAn&WN>c$+1FG=(4+QT~eOZ~BQ(fz$Pc)tl+mo$X z>-S@E2d=AZlaP?$Pgel$BeKGNaLI3L8XxBpiR*7UN2f_^~ZC zz&c4ZR`RRoh60%M^Nfi)OrCh01h4Mo)?_A&(!?-%1O6MpMGm^-Hp3rQf#JxQydwB+ zOu9$N>0`Tum1IGfg|tDv-er+ZZF%H z=$?v(sztc=8YRPg*nvUc4J<$bXzH$*z6CysmJDTZ^dYibgLt2{Na`v_cGe-vnVxb}Qe?yc;m%jh$-0+DwWym+dXoo$CU zBhnb>z_iThUha;XT%f}<-3}3kNcuDh)Qx2CdcKrwEUX(#G;KK5HKxgowdd-y?l4ik zxV}wSjSA~Q%dBtN0o&&rppf4_^6q{y_KLM{vHQ?$c~60#oRsPdNdo@KRZxRUGZF9UY_&9YwU@Ib!>OCLmL^y zQDnlH<^&y#zh_8%Oa*^PjDFM{`}G!8^I)3Xf_qrVEz~J@JiID_jr&x+;U4yY%@mPx zTQYD8@V06K2oNJ>PV7`FJ~Yz~Sbq2P2=upaMNX-(Yp2j@?C9z{)kXBEga0BM?A~l# zU{+)%?O@4|5$itRWlm|gyvG6C94jr1N;%1r3I`0>hzLfY(N$HH@`ICrqKpz7{A2sL z+UBQ!UE=kp6%TzN4TOurvl7NnzMg{R+qF)QHO5kkHT8u-Pp57 z5|-6JzT6RIt4rHMNz#S}BbNhv!}lHSq`mohPt#uCQypVirJsDN1pQM%bNvUhW>plc zYI@nQr$N)RPE|A%Sjf;}sCc^N#<@rC&F{>fi*H5xG{>u7(Dq0yL+4_P1`FqUv-Tew z>MUD#&_^B)8I3f(-cZ3dz9}|YeMrd3GW!M4Wsj8l^4-?je@iBWd`M4g2&+#HD!1_dtbcRBo~M-elaLB9NzHnaAW0`CZ5t+O?fxId1iRwIo>}N@m#Qh ztC}ThoD(Gs%uWQK&9H zIzg-@sf2}k)zfN3gMCKVI#trZ-}p0@s-@J;NTxVu){Q2K>00SU`oDI7)hD`R>7pWf zbDHLRVx5A&LiLRgd!e&zh9r7iBC^oGp3iv4kVlg0?Mi6f@4H-FoaU!AQ0ZMU;nZ~)~ z?zlst*pJ6df71iVGJ6$RM<{{cH-6JZPyCwVqj#(H>yeIO3vch-KztCam~$$$dYd7> z&3lv2t_^#_Mw|3gDOnP?GBhKQ`Tux&%Ydlb?t7S)A*5Rx0Y$pIM(Gljl5U3Xj-iGa zq;8raC8VW8ngOM27(iMHX=zY+&wW3?|NG_eg)eiRbM3wMUTg0K=RPRLpI!ewEN6Oo z^~?E^b)~6~kbfc&cVWe!2g70&`to}1P@M9MD}7e$&e#y7aSaEftJO)=EboHgKSWn! zFU2R^UqC$D44WJG?6vJr_zhm^26l-c4587!T|++19O>cfeXFlSu;PK?Z%l0PKMlrwbnReM9UjCweTabSA*UUmC??g#iN$A<~Km6P1 zC*{%Nhclq{zpRW09@<}1Zk50D@TBibk!~LEJ=h>ebm^UVNIIW%6weK--7EO_fc0jqqRjtQO)SNuOknRF`jaK z6m5IPq-m&68qMr?F*{?CfiqU zCa!M2^o;hJezxYjH=a)laJu_aTqgu*UM{H4Q#GO^n~?$H}x+I!0wqVj)i$O0?-$kQNNHo{3sVt(;0rq;jcGoz-X zX5GyXE0VqLpCrBSy&?>dw#P^$zbGQO%Ncv+gwb%Ng{H|+9M3ed@k}h*lxof zntK}Vjx&KA6?!x)Pr`imaCX&`7CLG0fO(5_yCJM$33puRxgAl-m+v`_4aJc%!(VF< zbBX)A0`cq0Xd}!?5Uw5~YzZOt)I5t?&Pcw~v&Yh#%}+}9ge{%D?qX9Us&&xT^0MAGd7@_%BtR zV;=|blaT@bbJOzIwZ{=j3kK#RsY*z%6sKBK&$UkD#qtzy+YeuEoR_0$_E)8z8~;+x zd0E{PbsEIBE9oQmQ`XglN+`)y-=byW327gPZ2WD3Z)hW)>jD?~r`7vQwUS zsajh66Kx^3sEG!h2$*Wwwuqv(!s?E$FfLa!ZjMXE>!oY@aYjC4FI~qfoK~HT`tEML zoXVum&HmRP#u)jm#sxlY=(H7k+vLYV!2+qtHQZIah3OamXhCb7wCy{1cRkhjs)0Bz zXrYC3cLe8cECrv-oo1Va@_TCv|I}s+><}H>=Q>7KX*xNiVR>>Q<~t5|5HmLF_y;_%BHmfv<(PgknlHblC6pUxR}ELutG!)-#s#MY_j(YYvVmsMa*WzJG_%``g38l){>5ZUBj!x3 zPhT_+8yx5O#mt{gr^jZp#B6K={!e$b*r7Bm$g0U)d{<(rRLpUp0B{gaag53r615uo>VTt+P zw@y0IgPaa8`WYtM9wM&xh&fnBQa3?|sdqLaCk(%sYl8gFWxlkz4>99PY~b>W1*9s~ zi89~pPD+H`?;rm$nL35Rx(}BhRBl@z_W7rPQAefYKg>PYatI&j+g)kpTDR#RxLZ+~ zoJ8^+Y#H@pe_)r2+*V*2jE;7P#+Wj#e-1iCz*sy`aF z+UNat9nSsdv7#qu%_Lok{a=*7zQGBv^}WWYt6$%|+<4i9Xw;0j2wDdZeoRfeikAs; zE@9mZ)cx;DP4z|vuTLLkxnR^ym6AN0P+(qd#p?R3&88&1PCGd}&SpxCJul_L)E zRHA^=*bF)`;BXtpCiqrNcCuv%_`ORM({JH>0oG9*aC@vyC+$Qlts*8^CVOJXQ!V_c z2W|?_^>}lLC-L1N=u=7*P3utP)woa!#>M@2xV1T2TREc4{Q2G8x(5A3e8wD`5}c%fpzGn$4TcJ>zxKSW@!hU z2=AxqBH)6UMOte35)}XVo-3Jp^d^=!h)tP5w-YZ!k*3DdN&8{~_N0S(P25$(>>cl2 zSV)SlCO+%9ZRX8%oX8Ma$`DZV$`Z>YE{;(Z7A}}Qt8b|I5PGn6-uckDreb~*_J{tq zFKmYE+xdl!kFW2mwTHiJI{!WF?d^5WdlWa{H`r4|SGC%b5NHV1K+d8vD@HomubM>P z9J5 zsIBJG#d_D?niYwCdal}Lyf`a3Ya)kDUfT>};O=oss?_82W;~8xFj|g_`@MJ`b@RhY z&dZ6J6pDhAO*4|5@YN^@A+Hp6Y)QSG8?H zrdPqoUoeJyUgqPmL6AAM@3?zbbe-fuzrWq+a!PU*(&>Ta3<=br#4Jh)>Aa9_ra}yD zbAR%4g&Sg>FX<%ENGU16IBUbn&5S?04hRTar)AA`IVgWtGH-JlxB)(_xfhG&=JX8u zB6{u<;iP0n<;!Av?vd{sj!GJ+|9vg;XX6iy`32@v!E)t2H-7hAXQy|8F7GmI0Ziy9 zl^+;q+R)FrPVdDvHmhhAWH#n67|fFKu2RnfJJmIB*?3FKMNs)Ad=Xqg*;Agi(@*~) zF2|&}J~CrW?|r{UaL7@7is+KeX-D+9Y#kFUO}vkh&VofK(=uHTAb7idj0zB?<;e( z6Whkn{^@!g&e5M&3BpPff!iQ{q%6~WFy-~c6kqx06D2H@K8=ILh1IHZ0(i*#_icMz z0Wn`%rW)mQ#~oyt`jB01-gRVu-$x0&_;E`ebd;em_Wj9#(|9)ueKOFdgTB`cf~^?K&>deCrEH@M)h)qptV1_Q&;zrKCs~RRc;7Ojxpc) zRD@Pa9s8g5hB&Mar8^RlZg#lJG(fpn^7{wuea!TG_}fp&pY>sF0n&K1`b8t`n&iQf zjZI2QDzNy0cm_RW7Cu8w79te zp7IfUb}-v}`n6e3tEL9PUBN1u6WPRR5heD3f-?Qc@=zhRPR7r8Y^7N!C`YS>NrQ^L zs*h=VIw32ukm)OP2K?k{bj7Jy0YuvDO}Z_Sy3b1qbeT^LSC)5=5=8FS-Naq4aPa5U z&cLiHJ!zK>cFGCX@w3h_oiC?}X|O4aK;10c7i>ylu{4>kw;-tvTe>#H;*;DHHVPZQ zAyJ6INFGSi%_;V@1-a~cDqi;`$ zdmb#&5u)1+ecXCq-#+UCwI(4Yj9X+n%RbR`onPc|L#)Ea#?`Bq6m%7^d4DI<{ACnV z)c%dcUegCjjnQM&B~R}aX(cr?@aI_A<+qV*vO9){+iLap5;$W;k*xcnGw|l=!3~_+ z@{Gh`Do@CZS@gtJ)m%Kay_b-??sdofT(LXkBMc&Q=6Qg9CNt9njuLu=;UAbR7q6z%jPrM1~JFW`M>*MYdY z(5Xv&d{fZO-Y(404<69y8Vp=XGsws#W1k5k-i-gV+wpCl+DC=&WlPZPdC{^S&HNi5 zJmG|7H(h(CZ5tWulsD=lv4-}k#nnt7cD@%u+y(<3yp%l@FjV}aVI4wPJ*jn2Id)f? zM9YpmiJ##3mUR~A+z?5*iW6iohzQ#H@#+5^x5!r;K6+FwS-*00(#$^#v*2{T-}I4s z9qR$l>&SG3K*?$0Yt{}jO#&K8adF~W=oTXRgU41!@H5qpH3D&UO_h!}>zI`~2I#CO?y~S~*CJ_=iXbQ)!)2T8M zB&T#yKs;x&n$q0QpSIYgVl3Sh*=eY_4lR!F#?ryXnz^rQD`Dyv%%n2KWW(bbq*#f9 zA8XrIjniy$1lQ}eU%GA?JkkZ>7Z0&)1Q5t{k$gfH5a7m9HMY5HS7+>=%Wpy3xb5P02o%8X=0}J9&mmH-ap$KL9PREZq`Vc6{*;E)$ zd#m`x7}F*M=dMzYY!(SI-bvQ$ed|Ou%O5pyCHDb`XD|XfXhftY>?HQ_vcUT19O?mPkoI z^>bJqT}CXosA*cbt~$E9Iwr2sd5P-cHR>o6=q*c-zntw+Fb|PcD^lYO{T6xX55DB^ zPB&R;DV-hmjB{BUSf_FwoBFGaO~J$hLzGACJbvyE3O3)(EbA(Fw|u8MhyA6z1p@aO ze6HsoGGJzHA!BXN&2~57;uS$0Kj{2e{V5JNqvLf=oWZsuBd_O%X&nk2P%DjS$STZ^ zSqn66)big8{H5o_+d!>m#H@5_Yp%u7MHCgpAxAg=u6H(m?uj)vmA9xRrD9~?w#fRY z6A=H=P6;Tq2=V>asIT)u{Qw&dy1hD9GgWYtv0KmKxXxJ3Epxc-)?$bAC83-BKz0AsS7;0bi=eHfI<KqgRsm0?f8gA23mw^ais@o(Pb43BA9FYOE z27)ZErgUvF#vau%z`(@7Py;KzsjIC2vFvSQ3Sr=!CN#TsGR0*ubZ>k*LikFI1rS2@=BF1Ufl02RnN(zZOn6Ok7Ow|XMUaJWen>{8q*f0n$epdQ zWI4+%T$)Y;;ckvFi@8iZSuCNN^S!oh5uB}S^3hheR_Qj;$d?zv-@ml zAglcGytnh&wl|!<4hRJf1+kQcA|BNH`ECpa4!_g2l4-2sI=eF)I#Xt0y)|A^mNMA zSz$8Z(okl^BPzIgAqN9rmt=q z`T@hY6<7{;pUN5yH|}vYzAfhux$qfv!lHCg{)R0kIY5nO{asfMVPo4rh0(W_E3OY* zA~rD^6Jr=Djo23L{=Q_c>dsv9;(@fk6IsrKb7ijFijXVP3B^c1JFicly zQuE)qn9<-@26{2spF``KaGCcs*8*U)k1HGsZ1OZ6-OQ%pl1?tJj&L{GmEc_0_vSfn zB|Q@&5#le?KGUm`yA=oaX_(hky|OZYy2rR8@Thi}iBdqD9aH>p`zu}1@swfsjB3Be za4*mPh&?2E{*SZ>wFYUl_y=^ruMZ30{shqqOJFicQ$Ufdxn_tEb(96JwEKpp`uWU# zCL&zy874vTh8vkGP7tRRi=N8nc+E?TFY6!qfmfT54B^eOfy#|!*shdQkj*Q-jM5%A z%1VG8{97-wu{EiFTbg5egNo~`h|E~=kB}6~9dEG4>uyXS%v*nIHw6`PjbnUg^g(5g zad;dpYJ9w0LsF+_U3MFBRC>LX>bWFjPw(~p4Idp%oN`KVm-j}6n&07x&k&}6R^9;jS#_}Du zb~9>|Xy2xgGZj{??^Qj-axM6GvW2>kg;BeNkBL!l!9B28{#ITyOAZ*)Wev;bKcs*V^~M5QBSZBfqb|fNvZMA{OS(~ROYeIJII%=A z2VGXXB9vnm(*Ql5b*Rb;^!krrYMNF@)~IH)liyA6vS=)W%Ivfb+%|)kZ}Z5*Ru5_k zuMt8VW4|VDe0R@sRoRr@IcOJC1UHU-WWPLaT(C1WGn>)5|Dp5W1J5I4J!cvCPE~OD zk*{#zS&py9IXb3CbIY5NNA}pSqG6Ky<>a;)S;oTK`3=WYw9@rrimogd^r!DjS04!o zmYqt)%cyj#QE5G3g z)4ql!*)zzi<6ybGj=c(R?gS-sFP#h^6UOM!!sw(coM}5#p5O2NWcDeG=lqee$svoG z&2d;MJqEpmAgsTb>x(NPzU$e)aw;PV@}_YRHX5{sv}?0=sfKXD4qVjRd6^Zl`iyZq z;oYpF3oks%Meqgc9SKdSWuX`1QN5H z8Y$2*^*4=^Z|SKYj>8$eTfiqYRQut);9j4NE$}RNO|xWr6(J-;D=lf$iit49_^GWV8GT9~5D$a2hALCw93AFGW`` zBy0U6ipkDrwi^?qV1h4FA;=InnmF{B0Xt+{aM;wNs!2QDFVA`qi795bud}&ZrP{YEcg90_zYLcq0g=`A?jzU&U2S^R&We{ykWGM2L`e z&>#*fDL0uT>eUl*mmSRm`f*&b`a8!2mC_(1YogKjO*WA0X&gH{xMlWMN64mg`RHwz zq;r%d??k;;dfZ<|Pnlq+qz2dsZGuC8dKoc@ziOVI(;oznMf$O3xipxI3ciVd6%#Az2#Q(kK1<@+r0lErm0%LIlSk@<8Cb| z+1Phr!$Yr94N<2nwRw;)5-N?oe4Qfcbl9cUw33_gX{&iJ+kqi#HZxRodh2UI^wDhO zV8-L8C9Xr96UP{GT_h>k$(54ZwV#l9*x*ZatmtDpG3SP-!8HqzYA@$?dVAsO^{3~u z6FUmfmo`a--J|FoV+yHcYvc)L-az?bw`@F@Eb$jxm8f5+*ss&)Q{}81A9=jWo+jl= zq-rK6E)cVttG`G6Be`TC{q4^a{z1L)0{D^@vx@}75W@>~{Ybk(y0+5;IGapjEl9@k zZ#+{5uW!U6XHMTWC`CcT)#>9JO<)Lc{O34FkomJNnaUC zQ(Vl~5|p!VUjE}_8Wcp)mfS1jG4oE9lq1`_+O5U+f%xKwyUa6|2?fyGI4e#;04{3L z=CBAhl0J^}@g%O{or=oLz}L+o?J_KvMRQHicXIg-j!%WItzz7hvEmpe^p0t{>*7RF z)48+MdZeY&+?nc1K*rMXYeD)l$Ml1Je^ z-%V#aLkQUvLc7b6c@gWf(N<*lUP(u@jw}>r0IZ=I4IFRQ?u92k0ve<2&^e~wE`H&o zH231CTXK~zx2a48WE|773VF&#Pk%fQ>iR0Bhc>o|+_Kx8*l~Rgpp-h)TlP(&X&EtA z{|3%4124hI^|$DsO(qj;zt5%>G)8PoAf+VjDJC`VblKnm18xU%b2+L8)=)aK+QoX` z?SvKSvq35^mH8v)!A*L1lVol=dxb(R4Mg1d@k}IGoL>01Su6GP;#CSkR%W6R=lKpi z(g;xjl{+ULwfEr_CY$Hz9TVmHtPSBw{qN`j{h?4q>XhpxY z(P{U7JbaU)_{3Q3QL3L68iQa!jHavUn@1x9*yV1()MWZ~?i0E3AX&6OaIf+RqJHt} zud!kkv>w^!lG+;8mFVR)gJ$AI?P3J}2yil7&Lapo71-|1@doKUL&aG+dOh)ShW(~w z68-5xW%L_FFkt`waU?#3oWj^(>d6Z>gA?e%bn8W2*x#Nh@z7E8!sF|LdrY&bSsJqD zXMp23|NaPK_4|twvPi0Zv(?-0s7pl4*AtwE9!P2C2M;<0S;Zhk9H)fw?Lt`P6D`)##p{2YMvaU)U|Gla@8{Apdsn|UscxhY|&6bvY zN74Ca979{Vsh8x%cjI2GWQ%@6_tmJ!pyaK2AI7fO%QGZekM8<~ad{+oRwwzW$AuM4 z%lyU|fT|eqCTxdc5c4(;1!NdBB}EZHvhn!X#neNejY#!Pvj;YU($V#H8UI85}V=KxpiGKNzXDg)Tx%O7l>BF_Ky))?>N}V zHXwcC1cIJ8vgR?6f*n0g43UF72Q`K*f%ryPw_ z6cjJ^mGX+xHFXi%O(*Gw+HY<>VsD@>hN)uY9NF;Tu(M6tkdEKn?oI|nh3F1MI4++zMWp%5V4Oc?Zghv+tg8OG@xFiEgox1Gl5@Gq)$ zgNL)O$c)dX`>Wlb$J7WAB2JW)PCoZ;V*Ne~J1+>qkXoI6{ZenLaZ;}H&dta&^68Hp zhW(%L$&YXf+v)~dwmF1&#M{Dd;v~k=8@>($wR$KeD5)R@ag#qse@D!dRTmzAe-fX*dM<8%S6&O;pE8s> zi8#h%D=~l=f?KjOAi0@L5Y}FTmz?FGZPViGb#U#!{ix&_78264p_2g8oeU}6Xr)Ha ziGrW3lnn$$8A|TG1Gx`1cNc(9cSwDbb8|{yP76;$4kKTOG*^w?olp4_C(BjWkp=p% zvB^~OY#Ckcfj2@Q2kBsMW#`TSE&60I1>{sMBw8}inlBledGbVYqXrd~B%-%Wza_Y; zA|9nu0Z4w1!W~ye%Yct#&FZi41g3CT=)9=q3CY$fk0TVjmwkqVE}eT6&B6Jbv3NVt zrw75YmEGBY=?~82X$k`)JfhR2oB=YBt+r5Nz!;3PO*Cf(SK7+3JcA#dk6UVz2J0+` zb4Bb_R@0&JxBmR;zd4&lpzj+#gh3HON4U4V=D&~ijWWdV^U~5NZvXZ?Tv7aAi%N`x ztNi}HSq$MLpeVWhe0>(9R@S7|-=qW4vUtR;JeRdTBy%x> zw!m$!Z6+vb8hT#6ruAQ6zsiTow+~>jHU_+b|LKajs3RdZi#B5!{Tcs>fZZR|KA9yw zOyAjDZNPKJ!N!3(n%73Lc)ctQxUu{hfva|Hw64(l@eCnbSY0&+KQ)KLDk5TNQ`)SE z$UiCySX)dRrUO1n0j40Oq;(z$gIssGU)NJST z!O?eQ%Rh+b?!3gyJeVP&It&tBt=hjDd?lNYX2lEKUZfKw*}FtpCU6xE6V0*tS5 za-|Ru@Ql5z798L&S%#Z*8XX=9<~cOh&C5mCb1t`j+i2v$d$L_7y%)Md{-8iBVaQc$ z$o1V7*_2-ri$fp%JFkHnSp8forSN3GO4b$u6s05s!rnN!A%+PsB~WH>C=bWA%GDkU zk9|XK1&v<<0hs4k^5A>KrhR8)3Z0RI{_5w(p#?x2OV zg?e5G-oBru{>Ayx$*q%%sAm=v#=B}pp~E3Mhm&s^Xg5Y3XIUn_YXn=wk%d!|ARR$A zm@;TPdCgjY{N|aee_uO0TFR+Zu9F8!8f1`;?>O9M^!?94vrb-%9IXppdc5c#;^aFx zh?bO#5h54?WFNd0fgmEUWbWVXu$yf95BOl4Mql?s3Nyqn0%lheLQwr#HSynwzy20b zrS-E;o8l>aRyMTwR;{2jG&1;7`PtZI6hHSWIa@F$E`NjMV$=xUg-pv(Upju{&T{hQ z5-X~#6{p+NdGi*5>I9#O$R}6gL8H9H*9rs5v zPKrH%ch3Ae?C<=_MbASdbMfe?2HLQwy-UHW1ojzXn71 z58rxED;+;^;ES))w$=d%kDb2M@7){Pd90E{v1c@~% zl)I}zA@qZbdv8Nh@T(7W)OL&H-PL_UY%fxf_w>q;+%L2^xLHQbf6D%S3z-!IFIAqr z?bH@fLmK!hOr_nve}a!f1UYG|f2Kxf$wzWqOw&uaOiL|Y*ca1ud3RUC1)xDb0x4gY zh}gk_!&0702{34g)NlG_0UZXQlO&~z8z;SO!(BbgWMP*!k#MUvA(PSu6}(2mcc z3wcw8!yhhKLozB}u>3fZY%UA+#caldVtW7SII%9?lS9(N-uITvA}Z1XWEleR4=3!$~P# zJqfpcFZCTh93*~+5oTZaF%>Hl&}9BZ7WjC0C?1DB-0l564wE;1|K4BTO`|^{U^BkK z=#}X*#Q>Q-6{;*SSJ2uDi?YlvkX zgJsPo6(4SK&_?GM-AonnF*MTzzEFl^h|iQ_tfCm#i1L~XHccQjVpV&Mq5fPa6`oCVFFvB{-hEE%LX;lyTfaRzowu5=2 z8-{1|PT2NkSl$H8bhNb_th9A0R$RSAXTBUkn5FA=Vr#}{jkjJ! zUv5)XGHsRF=C|b@zFPCz(aE{`%rzqn1~1!4j)qwW|JKiNn#=6cIJe z*h*b|QVgdoG%Qas9A;HfyF_siSzA)i?3I8Tz`Ap@mCZ<63jyka!6^I6wVy4vh!d*= z39{UW<)D1qj-`Y2ijRbaTDebTIyT*%h|lMd4FjmyJs0xxUR7OQ)~7x{vsF7b*}(Q+ zPXlAyY7Od{1y`#|qTg;iav*V{U!9avuS=u0`qY@WvC>KVs-?fyfdQo*w-I;x36!!R zYGL9AOTsR(?NLsZ9#Iu0xiE6$Y;UENqUgvm=&@f-9Z9N%{sKbsZ`vQe*2*?Q-iiec zIx7rh6E&|9*EOf9rQYBt?pnwPlB~7a$5}0g3|trUh|fnPOH=yA!$kB_z7@lS7i<@S zT0W+;76RdkbRu^G505(|n?(feD1uof?I_SH=D9hh6Rv-Hct=y^_8a9O|Ms=^_X*lK zhffRFCOT{ertB_n0Z2%%6xTSQx}VNzQ>H{>q=5K~ z?j8Y4Z7Iw)wNF}W&|b0q{>A=;&N3!{268MEDc9`#iR8(TV_ZqGoQE|egV`SYErV0l z+ACL`x~RwLV7;6kA^5>8&6x^^D*!8Y;4yL?vadwoMFB4FUIG9nmX~$>ND$qkxJiLq z?A^H?82mD#D9q^04|+D&7CW@1)`AYdFjoE{|KM2j`Y zQ9CnPKAbV_EcLX-19<|6PXBO^8cV>0ww#Rm6ZjJ9>$)kQe;3%wN$aSM3>W*{*OAOZZn!zfQY0BiRS?xv zHw4@Xkqz84QzR0C^$;%MN`HLwx?q`VqfM5zm-?Yz+fEb2CMB!_Ps@M(*P3P!M{AwT zBlBHy@?=|>D@EB{v`sCbhU@c^S@&DT6I7+JHoh7+)CN;riFm_DD08=8g1Lq6LN!gy zz!EDc)D%uWcjWz_1yA4_AWl3hHm~ss>IB4&o5Z-ZE<5V}%6-i-u^4~Js!mIV65amZ zqJ_s1?qwAep|yFNcmoEQbZ1j#y6Ps!32zKH^{va>ps!-|?*9s}jSVa_^-U~dE8+~_ zShFhb4APBDCny{{R_lpyh5&z)=Vp3kM5*nJ2%TZ><0cS{7|ubuZAa>s*gc6UBs!a4 z5aF{)P^{>pe}>}f`d*Eyv@gb<5WBRAxb6za%uZp=C9}oc*4&(`A@phjv6=&tQ32NP z8HM2p`=ZI%4i&tkf_8SU_h1{1Tl^LIA2KelPRM(4BNk{o!cZI(O@x&fGQ>4*XSjSD zPknh+`vA3wdoGbY3zWuM{M+1)SepCYlDK|7*Vm?<(9OV6cP~c(-(eR&#b>f~?ntAI zwAgky;L!|eZDp7ZoM?J9@Y&Z9`089ORf?3Z6paH-XYLD?F4(4zSzA zn)^5!&FU-x{R44Um00pR&!o*XTBYFRbLG^Plrn*t6{xMyrjIozhsTt?VQT30D|7=0)oDdiQ3$Wk3#Q;wI3s=r_%`#i_kdL;Mmp^i@?2(g6pgi#iUzq)Tn;I6d6P`S%0CE)`?8b@BB&@H zb=olbl8j$pHef-^q+))e3E%Dp#ReShK!a8L5T+2sK*ZF4|Q^D6D@;7!0>8%>n-BB5F04fj`BF{{aC zMEBTtSTC9*3*W~T;%uB|I9hX8`o{A>t4d3)pk9)`-K`D0ahav&-H!v5d>`TdU_ihH z6nfq;0oN@3usS^yIOh@ullpU?BJXBTo77eRX z59`Vx6`=Q!5Y_&t!L>Kny{*^z-X#sxaa3V>=&nc^hO!`T6MXjUVl{H`Nh@r`jzj=0 zl_$pj&6rHDQp#*5dx===Q(AvoWW^N9p#R7`!#-e<*NAy95MryWuS&hFxN%r zcc(5f#unecF4og*AU4c-%yzdP#I!3WAVAhwN)&O8rsnn?0%pKn6~ou9o}8VwHfDHf zm>_KR*D3S0^TA87#{`D@3Hg#akTa42*;bfY_+T3B#Ez}#7UlN9yvHT~M;}NM9v*5B zf6Z^bwtFJ0Nf9BrQJFwl0kpL?%}L3oX+oxH0-%{~tgspu99tmO zJ1&X=s<;UO9pF5lwcbx!6h8xN zaoDngU-QO)67$Q%lAgMl4_@Tbj;SZ9Uw?XXW5^m&6J)cCK>26^RN?0rkpQM~Ral+X z?Wpm{;j_~DT;rel6^AU6qr-n_t=kn_%YQa9gLLcp?j7*7CW zu${H81|1FCEf2jdsrRVWXT>faHNiLg_PKQOCU0`_Z>zYOl0ZyF%Q}0-1#3vH;|qno z+iY_n3^BHqyQur`K_yj$vmC~{D1`EEys~~3NV`el2paAIQwbM7^-#X@-dL_9sAW*N zo7U&pcvx|4cZ(}8>PP^MlZ(#+0}dmi;?e$do8@4Nq40gXCyu04CRH_zD&nR7CY#cq zFh8_|jXPrsSLs~H9lSSARuM0z5c|vbG6o+owcG2_P;gd-lXZV=^T12s>iC*Aaw;<3 zE^6^KyBV3Ns4#Q_;MWLfZ&CNjF{*tg8LI^N4b`g=UHUkmjzeEfv}`kyO$i97YG7hg zKL9ZmUTdp@o1iHIEyesstXNU+hPsDuthBdIRYSUHS7BQ6@nl!drz8+%in%PUwx!k~ z3t%$FjAv_+`3NXhv`R^bf;5r`$_2mnKPl13dFkDm>B8yJvTP$0HS4R* zGU&zM0YY8dq7KcB@M|chmtICYh;%z!^w_biZ}`R)?Uq*W>Ur7xfp%40Hx?CJ zh(a{Z^oa6u8QqdD-m&Ab0;U&K2|wIfDa)4UmtaphqsF+SGT=~BkJ+mIXs;t@tt~(U zzQ{n;LDAoEQgm`uTnkJw*k3&%jjPAbkbSAl*pQKNagkzN;d_ zIxhMAG=&`G#n)G&x#O%`FYCy%PssOp_1cUM5XFdxcX3ZGu2sgyNuP4=`qnL!opz)m z$Z`|%$!wph5}O{|cCxTYowr}&QPutR;S8ak+9EUMgD4NPeCj^!D-&O76DLikZHZ z)-1JG2!FXh5VY(wPS5daKga3=y|x6J4XXC&dv_4@>#V6M&H93mOTnudYAEz{b#WEk zca1=ao{+Xvae7LhS15HVPT|v z>LVZ29$o!_=`uume>Jyi#F6*PipN+9 zI?(`W@V!xE1h2F#u^6*qNv}6Woos&WITW|BQitJUwN2<<200Y{Cxg>wAG~( zsRCH(uDL&&zIs zTUu1dWTsqv-90h4T(B?UOXp5-gqpwFxc+|A>%87tStw0VKlU~P87Q0X-xp|}`{yxc z?1GDwv#3rOt_3!j_&jjCj2F3OL{VjIv#JnfY+rrnV@;-)fWz;g@~1nerXH|+eQ^8{ z%;4l?5u~FW95rz&fVIz*T0+Wpz}^518kigqzX`vZyKZNRhXsZMa`e=1X(-RVhsUEP zf6N*e6XpJFkj%!)|FHy!;D}S_g#bv&I$E0Cp4tgmkD7J|KV|mf8;F=3bdOIw<@9om za4ATa<2s5tReX+1Ju{lSCBK4ft=5lN;`V>`f65bwEfaB!CWQ!CFJ7;49-u^MC`0B% zMc~vaoy`SK01T@OD)~%gF3+lu$HeosY3%%zVY#u%=j1Pj#V?7&9z8ye+hnkSq>?&m z24SlI5hhdcT!K?zkU!0R7J@ZiBx#9gn!{~Xhr|*>QgSbX*iQ3U5ujo`h4iEz!kV=U zAb~@bu(+u$JN=Rvh1f_!BVChpU-XN4d?b^peCo&n2qz(*NvTT`N_is?LoKuwq-5jv zEPWbQa(vW;^p9hISsPmyZ1?+nbq&3HamtaDYgD#RWq*8WuQewoChnl;2BJ`76mXw1 zTbkh%#25gq6oueD1k!Ey* zN+Y2(NXjS~FkqvkLApbc6eI@HAq`52w184WloY)`{=WbF$#@Dcc3szb9_MkqPZ40k z&|#(Z<2hbC1ts@W-mzQOlMIk;*H1~eA@vIO9+5E;{H=zxK3TZ0PbFvn zzr)l|%T9wTPiA}ho_`#Ty%#BXDa_;b)_M{n$Q6fqi33iU9G>#U+y?uB>Cz1k1p6`% zXYt1`;nO4aIr{S%`s5is)Xu!vvyeaF-)|D2rjzh{E$9NrS)CkA#;wP)7L3EK7Zs0+ zF#OJ%h=VpmvL*qyiHYE82SaJTH2){Ijko_|ZqdX|$t+c$I|r=pjZ~d$qfSx2O7;cq zG2DEytAgHu*ZMIK)sA7vFi3@)?bs#sH#%KR<-xR(T+b`8!}6-wW+2oa#~c4V=W|L( zM)8+$MbBu>Fc!^sDi-t@%dRTx44tG&TI)}I4Eb}QQoz-=UMk^|eO+X>mCyVlhc;^Y zo{x3@AcuI%$6yj&N24iIXZ#d8h{5%o(3w>FhE<)2wj)q`DX}&G{r8XTZYhP9^|Gij z6}Itg5~5EC0O0G7M;=ZSd3;kRUvdd58 z@tu69WJwpy(%L=j0)u&hUo$BTSMIew;zTL_(6?;|d7D zyS>aC*V(U;<*(!g#^@}iA^{7^Vo%1)1qv@7M=NV06Gaa38+(uFj2s#@T1;`%*;W$xCLO{D%pSlKxX z40>H3qf4?|Uq%uTbG{)0_$Wj{Yzqg!)bQ|}CidaOyF?4MqbQ~K(xykc$nb0Ds*L8h z<9|Bd8GX>Rdvrs>3q1OAUvb+ls1GfeC2quJ)-5@P7}YW+eEf)_SLwDry@oT1LX7dJ zdrlQwW%|^}?bUDPAjV{yl>c6zJ4T&-xAsk8Nf4$sLJONW;=0APdxO_3s|DjTge^(< ziw+S+6n)N+lN8Yn!`k3-X}*aSH^;NH#XkYXw%VL{nY2BG;(XFFZ@r!S_5auRe=4FB zJdX0#fC+$2V6=?Wo$hVVJk>;hER^w-5kZUZ9s91*X@8wu|C~S%nA}v;@f3>qGtACp z!Q-BVMNtf5-$rrZ=kwhBusMwc)fkZ2lI(`JzYao?Mqq{rFS>FRkHka}Y(?%jXsF`K zFqS_PtTYY$@C)j0E>w3%_kq`{SVWO1kjG#q98+ zt5G9Pdp}!$B3RDO!$1ef(n0JYE*!EVVe_muP7mxB$2x!xZ9ncR>qr5(AMR6|Aj1$TXK@KVXJ*ZeSe9@ENa8dnpwON3=J zO1l9@7kCh9mHXnzMma^Bl#|$`qgxr{g7Roh!;Pl) z82a?dEYwmJ%+6-5+ut(Ix4)C(EBl;s!L9OM*NR-qETD;;AzPZw@dW#_#}!2J`oO(N zfQ3>A!;_E|2fA&(N>TBpygml)S(x+@C9n zBMlrHh2%FOGaqMC1t>q-4rWV&x~fNittrHIkjB`0P6!7Yo3@Vx30~T*Zz&f5pp~xm zTc6O2u608ILa;Xd=5$uB7<6VgLXdP2WGLPLyEk{Wco-mImyIcPH^FhSw^qzx5UU)y zY>(FHa@Z>|NtwOIqH+?`h0;AHr$(n_&wOSpX68EXD_C1}<^Lk6pE?sn0_Eny@YV6Q zd6kIdEMKpd*2)iyRyW}Xe3Z5Z82_8}oFA>YWu7{mNolQ0&@HK|J+yI3`~?T}&FvUH z&pGe!N?m@;&4!1K!2(U*s5hGW=T!7?oJwJR3EnOFq(|}HLZTb@45mzPTe}QJg&8bp z!}f}H0-helJubk`5tacu)+>j8eZVkd(?abz7shq*u^o82onisa?!@PSmOKFe&c2d2 z@v(gIXFkk?O9?V(P%^%K>TyDe!~*aVH)Nfr$Bzw9MdwR+qgT$yVp;c}B%1>i4#r=b zROf7|do!Vh5K?0~7~{DQsl=&73|)(D`KAE^U2 zQXB+fg~{`_7~88yxye1l`KG=|(1F$VYDIeHNnAOQd}_tyHi~He33NX%I5z!$9*{BG zAV$MtEOyyoWN0MF^+A&MqR^-z=j&qFefW>yeedm}v6wgw> zbK;PSIT_Iswuy0h^fHtf!jpoS(5c>U&bf(_G5zi9P> z@s0>%WPFXpXK8!;;Voy!K$lPkHQa42)5RTAv2w6=|Gk z+wS*_LqJGGp@sG7Spbeet){NkZvA^!4x)eOjNJav&d3p+ktUy{cuD!?y^F1pw=VAz zGG|8d$cRZ7n3BBhaW)TtUVO|W!3mP5e8eTZ$>S$N(yY6ZMWx}>#2aVBo?U$`F!=Jg zm&rwxq{)aE@iX)o&Zz&mmE(dwgo;%xHWDyl!oF}2a7*x06nUB<>qvR zOX@Yoz|n z-R>j|?RN@nyO4_WX~ux?DX+Eo^sezjB)x{{zl86Rp>YKIth$%v+}_R z_fo|mIZn(n{sD;=XpT)3zl=}k&15Qam;3oeAc=j#z~7379 zk#YmsB{MLJ*B#@vP3CV}@uzlAx+IL>*xAlIGi79$R44(*EOC%=0T4o@eUns6?z)ue zwxk3~^Lf~05ORoOh6C=0yFADq9B656nB|(Gjm3ZHY^!lzY?U2*wicUuv%Ht1X{6t_ z-aClHx0@7|=wIa8_1?38Q#~xjrvQ&e05)9z9u?v4!B!9U`(ArNfc33wcy=FfRBLSg zbYtR57k4l)5$A2QyikFilZu%h4z!Q77)?fP>5B9zmxxxUO;e?*NeTX<;2;ni(~xvF zV>YFMx;mG3`t^%|_j^~ZVS~rt98?FBrzE;7xzl!}x}Y6a6SHNb!PD&hmD0=OOx#dc ztKEqVBM>OPf{K7GO=k}e^vs-rn77n2frp?;FGS?m$UuG@c3L>0f7!x8oGs<$owv4= z%hT8cYpb2EmM?3F@~&3t7s3w3>bSdl_~eEecVwV9apjcwjZdqraq89GmmR=r!XuovMN7YKY~| za*y%d{c*V}m%iDpOJgqHQIJp6(Q=pkQ~@ZRgs;7YHNy8@QKuYKY-7o< zj?^Sg@#DV@KMowkC6DZhJuMky`C89}3ie&uq*c2FahNWv(bMs54&BMM;>KV@I_oJ} zv^#8g92BqESngg3Q20%CD}Q@dX}FShu;aU*68}s<07){(1aI-Z4Jty@;VDFX%kBsR z#)fGOMCEr~HsoU=w9tdP5M%d(q!m~{d4l*1T{+2PNLqx4(V!~fw_#=)ObJlyoZ+Wq z)1ijtEQk!?<^%X7aB}43+TdorNY!A&Y&5FJwqx4b)Pcsr>=H2qf6zKY{5@2I_93hB zdGkQ&nX692$SFzGyAp}I(mr^4_$bXi1x^kH#7p<>I${aQ##&M)8gBoFQsGmE!qw^a z1Ad>UD;6zxK4+*-Q*ur7y|Q?5CN92RImlPjEQ_D?W&Svqe<^>?GQg&Py|nezjpjG^ z0g`@SiE=}boMS;9_7gD2X@85nw291FJ2UA|%1zYB36QGAS&qvm+`&{7p=YK)jR^7RAiIZOrB4&6Cygzd z<~T(A>gy|AjINadmy6pM&n`ogyw1{PLvmWB{{43s#~Om{?#a zY(??M`Kju%r-ex7M-hmjgDBDjoehbW!t1^J_2>NNt#>HoQ3lhIQf#Inut2Z(q8GDn z>Z&pfa`9bsP=R;^g2Pg$A3yi)78u0Mi6hL59T`aI4UeMi_nVJk$L@jDK<78VZsByf zCGsx2N;H<>G*<}zaO3Y&oB%CMzFc-vZ}2rRioGC$ofky46ZAI7J3drwl8yORM? zSe$PGHVZ`QO<^7)6VXh!QXKo_O3XSjtDrRy^T`#xeZ}A1>08Zz?7p*~_^}ZmBqo9PizY$bM3v+uQF@smfeO5}+^67BGXM@ggy!Y+5CS1~*7#S`wD! zJQ} zTu3Fn+&p%7qsB9wl%UfaLKB}qCcfXV|NZ8>UVowz8~CpQ;4Ki|m*>7e;vfp_d!e>z z(#56IC7Dx7c=@gKHr61vt=yM{xKWR`J<#37u=CuP zx`3rG0GOgY@k!*O*^CVbC>eE=RA@6rifzf?DY>jw!-ol_x&`sLQtRUHwVd(CM)#bO z1%6Kt=Z&mv0dpz`ms0Mm(O(e<>zDaId)Ti{uS?mlTz^{pz5eUFEdksb>nFdjes4js z_x5=)1w}=3Dt|`)=KeVaeguYwe!o?W6YE=0b!Ias3joyf6(=KsI<_WuT|Nfq4o(>S z_sB_xR}+2)5Z~A_RHi=j+Q<&-I1Qe^kkax+1V*hF-uACD zn#danb+|RH2x+vPAog1o+&hl8Ko0z1C#elJ-?T;Xwa{J!sFK908BgwgA zGT&ysgcM&s2wYk;1PCEbC*ZU;^~^s#;?V$AqgSpro+RdE*%mjF6}O^ig8F znGj|+DLRp$KNwslv}}YS`4m|7;C&7*H?Cdcy=n4kUE(^KY3-uyG)*bzpHcv-*BnbC zs?fY&jtmHfS^KX6zGkz6+3YhHAn6svnmD_hH#b;6v|@WF#8?Ba!EXmlcD4U56F>WV z+edwy$XsmbH?IIK8^Oi>5QLO7Haoj2P(DQ)&+q}CC26`|Uo;QD$!@m}%;b@riRzQv zgIu4ZPdRGGPL&!6|CsCj*0w=x6@Hc}#GsFYyeS^GZwyJF3_6tg1rR66y90Y?yTnot zNSK~lH!Kc53Q=A|4!snic=*uk&BWP+L};9EKSaw2b)ezk^6t(5u8a=~R{e^}+-bypdJ#)8U@owqH!p`55f2=}o`vD)9y2SebOJ*ch_c zaWK9n>DHGKh%8ZFxD@Y3zg_laLMy5DDJB!R^?;!usM_z#mZyocoJfxNVHgjlVr>?6 zV4xIGTb^)MU#xPy&AR-Wrs3P!f%Mk97QWBll&L0OiS(gnntX z)gno08SR3E?K{8;-L~cG@dXUYgvP|Y=_PdhouB5m2?>bHfHdEKG7v9a;tY1HD5aZOViz+$G zZ1bHpajp^v4W6~=ZMdLrQ3do|J__-2XJ^;kUhIAVO)8{X@!imd%BsUdUPovn{vrc7 zSvDk>^W>``#C|pC;f>O>+%$ID{<7W)(bAlvu?HM1a3CM{9zHKf z^*5vEx>>}tECQpeE#0_zpxRlGH`0?=tp{^*N%IjI7Lf7Vj23B(3A}C$Hby787Vv94 zrk)AX40oXv=Vnz*l1<5tG2N6irAyJ5>Fkj3V=?5#I52-a>f6sIz(qIMc2F`An~IBK zpqrKbyqM98P6E@nYLp$u>05-TL}FkL1YYtrOUKpWM$D6~B5u}8smpX4PoHg9stE+q z(>0}(5X$6BToen~nSyQ@sa76`Jp0<;#Yn6kogP4>2&6eJ2#a5KR(bwDbD%96v-lIiC=Ev~rG zIIYTh!Zg7QI@hHt8yabZHUq=+9ji8G;!T!_rAd>ncLvo#CFo7ysd>9y^@fwE%tA%Z zHFFWG0Y{!Q9X~3L!spE|FWTna=?K=n8y@*Wjv7lsLh-^9@BGGJQOf0+4gpc0RYzJ{ z{H&wjsJH*8&{nHQ;=B?!@_A3-n+^bCn3saJ9Oh`brFV~lwRE^#M4nk%_D1zQvq>`C zb4oSm*j+r95ydB+y20G3wbSxGmv~SF7kae3{PH6Bh4HkT6q8+;LDhSnGVv+Mb+=DO zpk$%Zw|BQ~>uo@ON&5uUFK+4APEmAgjz4><= z`}F!TGy}8!N5)UBBl-cw{U!_Q_2pdYYZgy&EEy zIfJGPuOi9<1_$z-mz?@=t2YPC+WsRS@+H+VB>={UJXB_zlZEg6qAoMwBO)_XGL;0X z;CvA%-m6^~O8K66g+B%ck&}LwkR3qF1$V6ACa0%U)+X+crN`6j!1?1gs#XSElfM#v zy{KUm%T{nWenob4>V>}Dh2;^!V0|B1N}}{AfF&~#93&^Tr>XGtZWO;Fn>tCe2oQrC zyol|Enj;yPY=H~)X>ZgK4GN{D;Z@9WdiO2PkmOTrs8XFtA^TMMZ~%)e4{jgMz?Lq_ zin~zhX`Dt*K~;C$O?Q#>Ca&k2Dz~`8bS4qj03J=x`{8acSKP+h4{T@Ey{f|}GY4*V zi=u3s$<=>X&pb!}vGbJ4Q~UdXU;9f!#xuY5jt=)H_pwj&3!)%DLDGK7!v(m`m?Zw^ z#%~OTOt-C=9YL}08?*+4N+b?Cshs3%*b4cRj^c*zDg7c8tFEq=W%={~GqL;J%)sqP zE&aipy|eiVZ8dU}1ls}{4TwF#pMmpVxdhG^lH_k+R!Ir|Y$tS!qFiG@Bq~>w5GR|u z@B##_fZ_K8v1$r+1(GY%L!5`ItWSRoG7j4kO*ayC)!%vP*%&S7`Xjx$mq=*z_S^s2 z0h-rJR%oD1at2RzGmp!-jCgCOWnQ#N6FpPN!a@uufhYqk*YJVIM12=oDd2d=RH(`& z#+Xj~;T`#s(TRaNdNaIyIKGFTE8Ph>esu*NBZzh|+Y#O*+zRB^)m9nIagsXyvTSrSI%lY# zXRDLL^aiJ}CvZGej9DN4P0v`D_JrMZk%6l%zd8*uoLHGQ_{Y9{W$V7Dw?UJA15n#4 z7$+OL@0Ps1Q+BBg;eqQq3Ua#6e}3p;*d+6hqTAXMbtCM6+V}HUL1KM5-e}}A9Cv9X zoc4Cx^N>=c-boNss;5 zNc<1i$;%NDlVifKXAVwP-+UPx@u1L)BJYWNA;rjINiLsDGTrG^I0aD5RIy1H@Z<0P z3MmjrZo~$>O<3*A2PrM6=LI**CooWV(C*YL(PyhFrxp}JZR*Ek zH_7Zl@wjY#1mSS8MFz(A$tjAl>}z3V!gak>^`3h;pB0|9j|OZeMB8bf2?yxt<`O9y zuFNXi%5$fyC-&}0;^MyV@HJU8#t}f1mnM{fIKViWkP1tqGWYVBe|Z=~E(y94%ZqO{ zr)E%!kGV}T_YaEZR{Sza0^pjDRgBBycS>SHJadL8b0sDyGTI@`C;q!#i0{daMY5d# zUL+d(pC_IkJ|z@}1#5Gvh<=rp38L9z_pB4d*fjxuII*{n!1>>|%vckCe{vB!Hq;2#v=oHIXfe!X^bYyaDUREE=7E9Z%0*t{8Hf5Nk(HFpMQilb1Dn1SPA7r;dVl!{zOc%Wz4c-Fik9H zTtg9mA`b>KiJRx_Z=Bpn4iqOMCL@3nh5ud+|7ulKimvT1K&9mmk)aX zRJn-z!WjVINzM-Xb`;%zFc8P?iI3NhBMb*$WOn7&<5noivoZaZ>8}7=h5SO-sqI*QHRr!ziqp=F0CIl}xKe_2mXFgH9_G)-hZ7Ff=iPmk-2U znu;KPe_EL5+n=Tsc0kWFVW;+Yu2`({9S5hMt6dr;xy~UrhW>;z^GtZWK#dl~JV|Ut z!UU}EK(dk7(IzWLhjF~zcQ<)5*IpMQl!~XnWfcGBj`)#LOkuD8k%BRfb%m*?YAP1w zN$`k0t~n>3dm}-a)N4&($Xzi4caE41^nD;1xA)Rm@HU&|F}&Gm!_Xn+VG*e)z7q!N z-k{k}Zi{uA5CH-#qjv8SZ8r=N5hRcDW#Ke?cX4);#7|fxG47NA^wqEXsrmoxfmtEy zs*lav#h>|UYQ+u9lmQ`$vw<*v_$Mk{GBR3JMg!vPqnt;Zp$`U>gVBOKv4$Dl<$z(; z5npSQx$-WJkiiHnl~W((-_Y)7I@G+!xGda)wy7lB3C&M^ekjuTWYK|NNB^$J6ovYv zY{-Ys{34{%vuZEXxzO27z*EME{Yd)Y^p_Pn+Sg+)wLm$>_uY|@)BaE#Zg;kDlor0* zRL>t`DyOcD`b>lWS+>6~f<<=D4#@g@wpxQV0V7o!;47%EE;7v=Q}z0r1QE?;c4PoF zf69q@ezj6u$A{Y3!jC~5X7*;?f`XeoK{zjT;HL%O{CVM4qMxXS->FQ zV9Y5WbQ8#CUg#WdMif2CY#9iH4Tw>nXs2UK!;^^#Wxk7Ij^W#B80CXFcA*`iaR?Pe zp7MQEZf{rh1`NV3N7&#QkFfrF(anBOxQG;|G;>5FM^lZQ2*No!tkm~17Q$!L z@KEU0!Ad}Lx8c=|wasQbaDvyn=rq-q9U3KZ)j*)jE-~O@Whs*>S)`e6%NczYJ_QXE zqyB5c8^Ee(tClZY61F_)I&7ZCIc7wKt0w0o3UoTeBqZMl1#RZDW43Fz+1-<et67f~86d$`l#E`wGxUDlNkPdlXdtg{gyBynL+C`pa~i zlIcj@V*PSLF}iPz^)Gjav5$pjw0)(nFk(RSMm(!UpjjkORCyp|4o%@(Z3~w)6xRwt z*p&F}`y>A1flEeVn~Gf4$Vm3AQx=2!z?~SA)yLuWfot-Gk2Kq8FIMIg-#;^Mf;Rlw zfE9NTHqkTB20A6q2DnNzNy#`iEs*dVT=P2%ToNpwzLg0%)G` z=DJnylra~pa#>>mKfc_f>y^+l=vB=(OK5HkkeUtb{P?aFUat5-f|c&WVM|XHu>iFKt;qQNpA>cZn+0gn1jTY& z)Wl3T8zA`~i1c;W<;J+RkjT53l1Sj@$~~lo;{l+u$1zbkdF6&b&ms0Z1yRh5r8M?) zXn8Y-io3kP+W9%%0?nT$;nQZ>9nO!DkdGIi!iOkXqjqB!cA3+DQDtu}BT_l?wD4%a zv0*G>_;*;YLJ-l^qO;u!F(I+cr5U~xagE?gDIj+=Dc7@qL8K=quDQ}`0_`|rV!oJbOD2vn>U|4 zS|!anp@OeDv%IECGTO$Q2bt@zJCniz5PK9Tu4WkBKTK3d+(GdHo-_zcETd%+IqbCC zkzd6IHSGX=6iu`tzUTipcRe{$U<#XgGp~d4U1T6`P~D=~J`tyjM-vb@BN$(~r$kwZ z?=6k)LCpi4)^j=n>Le`Nt{B%o{n3qlmY*;}_!%;R5?{<^qMpZ$5fxMhS#op2C*zuE zN`AooS+k}R;L?$5R>OcUYwx#7@ zM4`=hN=8p+jEVcIf@n2$pY&xwz%FD3HFo_MN8=s(=>nP!)!kJeJX0?b%6~W|)V#J1 zInAgP?{x6c#r)0IP#>EAPZgH|r86gjp;Hhqb}X4T1il`nTKxf2DqReoH5X!fTt=7>laKMB0P`%^N_ zwMe(IGh_(Yob5=dv-M?_bdK88EGCfhA;)F5b7Qn@ZdR@v=P56i#arXQPH@2|rPOll zy5e%C^zr&Rij>eOsF3K4Khk<4%_hr+eEU+|_+mn6;ql?TJGa`B`4KAD3bE(eMKyMkt4La~6}mF{c;8X|~VK|VEDytgEo zp!`!|AQkA>M^Y)EycOf$<^bc3?vRSba!i$u6*{FrHmMA+J$lk36cS1tygfkoFi%x= z9G)i+UmE2AQFK<9z=Y!F6Vl#^%Ye4N{fd{- z$HDxk!R@Kj16+*(_w4~or$p_j*AxjSd%@@fTB;_pua;{TXP=U;9PSN5+5-lF2-vVY zYw24OD2or{O?};TUYheTO}Ocj_G873(5JVDBPX?QI!K&eG`&s zCXAK*1D{3aFxP`$u&Y!BcOTgOtPk!^WK(&57+GIhYwi(YiP!FUcT(NyPVRljA}rMI zrfD_W>|7VrVj{r1^k}=Y9(U&g|2Ms}GfqTb;yq%%=HAU3@tCcK^fq?w(r!m3w1!cTXArCXrAIssCnGEA(8c zh<`l4`~%#8yaW7Sl;|xm=+-%rbLd^*frc6<+u5RhaUN3mA}V(+M`-wD+-oEb_-CAi zGMk?<^Y$w;3z$%7tpZkPU7Rfv|M#|^iQW>+fi^IEqrG{QgK)M@XOd@liW_W*XYHwz z%tV<^JAax3sZVe#x?|_D^qDO&rlLKwf>hQ!R{Pksg-twB_$k7`Yv8@#kY-nzI7iDX z0s&Iaod8WMWdYs%eV&3aL1CRheg&qZ3NBp>Hw$NAFk`uRnVaqq4W_yqT-(n##YxS2 zhf-cs%x9O0^Ucm+k zDU<;)#rhYmyA(*3cSIG5G}80t!x@rZLyR@tWgqQ7F(vkrws;Ns1lgH;3r{2{-vCl+ zg_{(&4CxUV_yXoLL@U5~OH}9mAGG%alUq~DoS}EWX(Cb{{)DA6pF09kFF{wCn8=qR{q0;GZ}4IX ziEp%OMevJtB|Rh0_kK!ewQvpyq5SL623Una9{AG{z*jG$*g-UI>`4hH#(*^T1s@2b zLf6@opsHoD+iX^HlSG4?nP2A3Km&`ONTCD{wU9~LrODYu#fW4+jE+TSSs?X%mReb7 z{TF0Tv;XKVN-1tTA$>11Cd#1a9UH}H2=C5Gx#^E0U98P#U2SEWd33&QCDmiS>U|6% zv3XTqkjB2Tp1%^Oa%cMH`$Orp0`LHlFW&DZ$50w|yop>>u&MT{p!vyI{lq-{npBTF z%QsHS2(DIHTv2}t;eg8ruD`v|fcc2!Q+o*Bt&@-aO{JeESbl7<=ZJs&hoI@{9xNBi z-1~*;tHKfMa}Rc2cJ4P0Ly)3gBFxiia#!|%h zve3-Enu3@I(t#9m98P#@s{9>IAcuy1Cl~pzc#`wRs=DMRSfjBvz`SUxDj~TZ`)r@& zN__0W&2-&|Ilp52Md?F>>IN5~bFX?_(O*ArUYZ069YEngkVPN0mt@pUX zW+mw%m&|TI)6Kq#$JDCzAzjEhxZC)9OH5g&YTmdZHaFGbt8OH5Sw3TPVNv0%?4Ng$ zY0o!vimPAT9#-$K|6Z-`j-<(dwEuW^BiF(IFWd_lHcDG_kbznQgmd~NI>rM873bxT zT7oaGT4d%k*Eu_>3s3=0-W0}Dea&xG_(X-g#7=lHIs+Eo<>gsN1*cbHmPuw)q+?HV zg_!H$!=~yWJgawwL2pVhy;b&S!RBdQ0CN}>L=@l?l>c93%na!cYpvDQ?nu!h6dMNY zqQbuWuJ0Jd$Vj3KWSzw93dY^lv3R-FKzjusl`K2W1(OMy#&m%lLfU!%ZB_{#Y!Ta= zXkU4n1g>Cxv8v+FW%sF^MDuJbk+d=XTTvKJE`xnGPZrfC{suuioe_d(WiuMwI^K`( z{qxiBB!a9xqq8`MX%#Ozj@%#jmV!5fib*3*qi;fDanL{KjgjzDLDn3|PoO>Cqq0Ki z{JkBta~oH@7$W}=#&pHXX2{c6bFZi%1~Lk(^ThaXngLieUDJoA3?%l7^x2BnX8@Ug zOiJs1q(i}D@V$?3>6W~n6ZI20?J^Gw6g7SZ%Xvn5MS*n zN)_lj&+E=`X1C9s{rkwlXH|%w-~Hd5LhO5WskNQMt!t%ZH!7Uox}i~S{27o><^fb` z4;y{r*5D2ogNFU&bT2FwqH{jz+D09IzS^wU$>dZ|UH3M$**Ov0qv*_eBbeq=YM`3R z;Q4^^%NoQ`m7na@huh$G+izWe^zrak4Ajp5?sE%Vh4`AK1iIh3y;ir96J8I3NXC#{ zU3R?_-l!b~95H5|xcvoX=s+7!4Q*g%9YUAbpD{sszkj=w;=b7u@l@kNb}{Ye@lvyZ z^f=7IaQSeqg8{tUv=o_R@?Md;V1AY01u5SUl0Yo<;Q0M@?TeXj-x$H(Q=UC%c7I2& zuHt0x{8_io4K5q`IU&?Lj5_$WqRTiD8?yJEsH{@s>-mo@Tc*C&441qg1(sh70{*U2 z5Ix@_vo_cWV0=sILcM9z8IkU~W|e)v?@Fv*0^tn9p_8QNjJXpe8guwn^XZ%41}wXD z`_JS3F`K?R%ZSQW4?Z&eLk=;5H3GQo8#|gEGT{&Y^Ny#$BB)dB{|%fHG=r>Vzbzn1 zpcG9!Sp`uvkR?2kT6)Of8y=pSIcA9HJ`VW~8zIS%|h z1Sfe0KbMY_ygob%HmXN&q2_`6`e1~{E4-3G7<1a|6m7|r)?Pzeph8@3cyB> zAEy7ry(^9Sl>(SH#LXh{31UZcYX-b;kU9TKu|)>in?~6x)wB$!#b_NHPQB@TeZ;6% zkI+F>*I#w+Wvr{pw1*&(;wGWkR4=(bl2J?GL}1CFbhJc09Qv(DM|sRbNm_dYypPPm z*G*l4d3OL2whoXey!LT_Vvqie z*aB(kj!V|RUx`aY=(# zx^yrIw&_W6%T^j0Oe2}fXZ)`nmR-`v{Qe;{RUElVA31i_66F7*4@r+LGC7aB-aG!o zXj>$qO!hAj*AW3x(bfz&N+`pIh8zVr8DmWJPVvs~{r3%i@W3=1cZeV!%lmb&hmN;b z^Farx7~=LW;4`z58zzR4C=N%kTUO(Rtm29HJb1v9HStF8rmpvqc0k{3e?xged8_q^ z{AXDa`gdZAJ;@3U4gWIHi<^KBQ${uKe2n+SSH9Iqh126j3kjHCz8u%$6MUy70+M#C zM5uJLTdyvVdPgn%#Zv_nvel2lm6YNjUEG8)-xM^jD{&U1dHrX`rz@u!- zfgqzAbpv`ZIG3^pOCaU|23umGVfrEPH){uDkF`3U5`#ePj_LwfYUm*D?H{IB%|8L< zFC{aH`!2&}?dZyMcQ|_}Rq==JRF(!|{gHibc1iN~eg&#rR6z5s(s*LD;1vtrjg>AQ z6c5xvwE_N~TLQ0A`mMqI0!tz1&pCX5I77WkO1Oh;d+x(2YR38#bmjwtpRi$u>oX)$F+H&iR<>pqJ*^FAa{AN-@eEd!Kl5Z>j<+qi}@= zoXPOH@TLQe3i{k5;}5NbeoW}w&eDriCWAH5#(CRWnrb{JUuSpkXnyXe5uV|xcX;;b?fU!h z6zKUM+1h{pvDY50)~3Q|pl35)1z0D;3U^+;cdPXgN@>xNj zI`nD_Zw|>-#*>^0SLI1&ZtzI|m2ju4TOP$m(+f0OwGcsQrUeMZxV)_6e`CG{e*dHF z5Kk^Aq4*Ko;-bgwfE~sAlqxt7Gutd!D|kd8rdfka+(lgSh3l)NT%Dk-(;XIkVN&q- z_zSjPlU`?pokiwczT}FhET3iO6I|niRAPFXZwb-Qj&U>6)sM|mgvjJXd-#O}+snV} z{WZy&4kP^t0VD)b3jiExS%5pB3?&ug(4LZ_!}TAPqQeZX3WD7;V3X|y!+QcQif7yl zk5}qM;$rpSx^`Sn1}E2K?_R{Byx@=S*3>#&BA$_Pr>U@aBk4<+dBC$?`BSvi2FWJahYXyJFv)!0pV)btGH230NZHe9ShAmuk4cK@f(#F zmQ;CSHF2;nq)zaW?_l#fs+SLF%QZH2+c4B+mx@Pb{Q&i1fK&{(Z=A9*ZDx-^Q#>s! zJR{7HbHZw;=x!`E$6%+w-B(>Y7<^v7J)}CA_o3O;^ly%f!1u?OS_C+yHC&CSG!`Ex zrFeP>*PO@Yqv&Z+J(Yr_FR=%|emoGElWbf~P3N3n@3c!&ph*sN4+;AkUnIBT;t5->s8+kMTeMEf~x?D4SKdG<;-eQ>OZ?bf}t9+1KFcysrv5=^!priS+7 zend(ffsE?EEUS^&JFtGq9)FnZo!Aqy*P=VlP;q={PB^`uZLrMz7iT0gCL2~?=BsIj z8eASqPi)qUw8K{bgl=1_GpJb@&Ve$W3&(O*7@6cvBhPvD0{ft}Od8gyFs~&cce=#D zok@aCdbiCoF&ou?Aq{XrQ$wCcCnKlA)Lo5CsU|4TA&}QUDCEro$en)gr>HRL9V44N zV&G4cv@N;t(0twyd;jx*F9)B0>PFX4N_B8Sy2Fnzbg#DveX`TGZ@5oy^Z|j$G+QcM zq!39T^hdwfA)8$N?GQmvN%fBjp`V7)jDIU%DdHxauMDq0dXg3<0Q=5RIYK&6)f&p% zM@aXS>@+Nu<;_$3HiVLI2XtVhv`@Qn{%dbfzn+cz6V~ItqzE zr70e4;EbEl+8f8;;~w4Nzc|FV`?%=2H^u9B!%Tg{I7+>_t^OTL{rs2@&V6$ z9!JFBZblp2%chB~!Tz9{2P_?E42Vfi&@v(dF%mUYSA=+_m@xhn78$v zfeBGDiRheSk0 zJeWmvG&;(JYn_HsXM{4J8vh?lXB8LK|9ySw?rs|v>F%zfdCu?qe{NtdxZri>ob%ax?X}(;mEm&*Gl38=w8|@XB0iK@nkJQ^Ai+Yr zp|W`6So0x#RAU`ePb^UNU`uilisX+ivt?V$OIfs>?qPn_CBYpp)^2!Qwuh--N)@vxRfKZb zeyh_@oFrPQ@DY4_{9io(cj*=?!Uj~Xm=nF3-E+g{Q?##Zu zFXdMu&z`jViZ!=e(~;2E9yHXHG?o0^Z^OWcIRAV!)P3Q@4Ay9QKGI{;QLt6F`D8=M zIup14OpX>qn@yQD1Ypr@L&;d+3a%+7=2Q)%bE0-u>seeYBC|R6vYHE@JJKwVjD*mR zm;=!^^$kQw)s3BQN2X6;yIYFAIFij2R?`a~>r*6p<WntLPz&F7k2cSO#Ia-O9Tq`Yv{&ms-4v48*P zf%d&m(FrK(!=DP*BK&E=KA`{)9Cn79&(tKfr3te!rhn^gQNjuaLsMB*e`P}zuA9BZl3h1U3NF5u=y zwz;CB(oLg(J>j?<8L&Y>;@m@MB`d;Q~Pue_43K=z+&cP|)dF6c(j8E$QSZ!F9ho|7neglh-9fOHz z2&#Xu!HyPPOR`Ft*OX-vSO^{j8{Z9Ekw&^3J{m>U7IE_m9{nfpR%oFFH8DgfAYj0M zn+CSIOGVvASr|IPf5HvO;QuK@=r7DJd!ophum9P1~Tdn2YQwx%kS4KfnD zYum8I0VUg4Wu5bou7fNiLbhZD0tBD@dN4>C(3xu1H{Z5jC}pSwcr^V8E!ysERvi{@ zqAMJALtEz8)7|0MUW69NSYq^Jza@|mo6=%5g3M6?1ba0&cex8GP=c3iF~+s0)CpJo zIefH!b;SrF16$Ox2h(9D(Dfk;UFV+9leOC~#|y$9(+XK^g_6Og9y%v>{0W<6U|zZS z;|OdaSLvUqOTCYCb}0(38n!f5pQW^z3Sbar28G%tP)%hI~D& zK0_R19?@2gyVB)%A^BJr+ErE8yyYkuIz;+YSWd0p2wl1bH0*gymh=6G+A znWd8fadRWe46)M185b0pk1`OA%7mw=OA?b(5M#YVTozM43ax3uBvTIeGZJ}02#Muc z3KnuQ1_CzU3mqCNF#}m~dY7ORYD95zZ2|!N=(v8axovnrR7>2et6Is=R^uGkDRWB5A znYhb&;j`Z`4v@WJykl}v2)X$2j!7X|$*7LM1g%|?vwb!(tAsxciBkf~5YoO}JQ$Os zdTy^GE-7fRx6_^p-J7^6kswHV!`K7>^;gT=nZ@FPEEoVAxFUBCCK6tCCSB**j}^Aa z6Qqo>5lWVI%pNwzQ;-h0wfa|@1|zKe|I*t{;cM0$ZXCd!}J``<)qdXI-z z)w6;p{%-+?$P%<01hwHJMUBR{Up3HW)RhWOnS^{_53{MUr<`QGWizB{jdK zRH)i}H9g8Xqy99CGlTMSA16Z&aMDl<5#Pfxed76MZwpX4*&*B>777Br>7JQS)I?kR zjVUU1+~ICJ-*H=q&zzHM4=&8ri>D;onDMjzCs&1g-ELup|M(&ULKLTLVv8E1>u?(A z!{dr{SjX%AEx4Juuk$~FP+V#S&6$;AQySHzVMRy=t4U}XITCFI{Y;OQ-WX68cD_Ug z8}-Hqhy8jxqH>H=S>U`OM^lQC_Q}y%h!BDOJ>mX_FaPylvJ>I0rov>pkA$?Ewd^dT zJZzT^DCf4KFbH?E6`K2L0<5xP+1;^jnR0hX=zt=Qgm$9(EeZP^qNA9G9pB|%^kr*# z?d@!q#Oqa|e%~Me`Kp$hgU-iYNrI<;!^05|5CHsq_2|d>T=qKb#9TP;b)yWVp|1;Q zUeO-XXkJ*sd-@9G76{^24zEo_);85)!2WU#MXJOi%2MsKT_B-F#kKNB>xeKL`3<@lPh$tHmb%V25!ocqbxtGQIeZoA0BdC^d~9)e=SM`D6- zaV6sYk-H=Uhk#`I(1x^oN7>t}F-a9YNe3(j#?;iu+Yvgu+WD+KrjI=m z;__1!#qf|O!>H>x&~`2=VUR9ydx=HqPdZv1x%aYJ{5q|iXd+*UrN-FP&o?ok33L)_2&d?t71ai21B$(+qIocin zw36y(Y4yeGI#sZf^{)OAc2*t-8xU-xi}dx`wQke&6$2G|_^iAJr;Z}cxvTy!4SEBX zRDV5wrYR;d^+hCuKKO`6$i2a2h}fJD#9z=SSe#iv`4C|KZbG3%lzvy0KrDKpzTkOc z)%WXmZfI^={T0U9nVLFue|!7xlLhj3t&gNggSFC+9sjV-(-&S`Bupo_m0&KY9xY#u zjULcAqxxMN=T&0DHq@HtX~#u>t5&y%q}6d@@bB*=>`mi8QtA;|AyDppX*UrK3bzvq z0b@WoDR^dPPu=&b)E5&UMnPqBKD@EF!gceb7{S=SPip8lD<*zfe=GWbV`O+FrWa*G zzk5hxjq%-kG$`5(lm@eZp>!JYczrehLx$pnZ=_-1+WWC8-bsiO`GUza@Am-4rm8~h z-^slt8;j5<$= z9DqI_o2qu+WBAki4wF1T=IamMNh8~v(OZ0yFDx8zVAx>1Y>w$8pzo^G;l)%2DH(48 zaPa06Az3#2IzPqSqzjIQ$E*-7lZGTI7WL|utZWB`byo=@r6{|xenY1WwyUl# z5iWrl&Y57>reCZbLfJ+G_G#p_6ebt&a(*}6c)WRXW$VHZZu>Nv&9@X2 z>h;=BGf>>psFq(Aq0f{L)uxwn*Ux93oFVz~@SAvFXv%xJ2oLBLi{a8)if^fnyCtv_ zk{XaW%RNf|a9Qx0c%5-)jn56WFKt52b4g_HP0oW9AuhFb9I|owr+24*VfXo^pe;$$ z9~hIA;K-^{_uYw6ou0Xv0Rott_z-0`25?ZapsSx3I(2p5PFcN|Ij0`uB@HZdr=+bW zZR!af!)id}RR4fE@691lGyEv|(P{_Hp z90LU6VrBcEX-N1v00c?6)w(x61D>ndj#T026qDXK2TXPf1Z9DO2TOD*g0Ewe*a9;E zi%8wDT8FkP%l97uSjBv$-FvY2Z6hO$nOs-bit;gCZElbr^iT!$p`T+H-k5(~G?nQ* zIWhg<4?FW`wrm^i>%gO9P`J_Inh5x{2exk(G79cBYzcrf;n6DK2*T3WA_3yGO zb&(xu%D}FUkH;QcKbrX7AP1%CSBy>n6AU3R`AQ3k@QHgm)As(E6<;dTll{-(4@n!t zJj!!3W37`&4uiknQsR|vZg8*neTD0UX2L$)G~ zA+)x}lX>ccinAvpWJ>)A^;~mdRci_L-;rSmB+v#NTHwyHnQ@uSs2K7wiFb!F3Omf1 z1HsgKWIC^C@lHICSF{^Lu$D`)U^ksbZ-|7ada6Im2agng4d{iiU$`k_e&B<39-qEr z{B}wV4`abB>a6Yfs$afOf26~pgQpE+=ioy5K4c#A=`K-KL^k^~ z&7&QbNlCUGu2cyBa0m-l-VHp%5)!lzAyIf}4;y zL+Zj4#YwOC2K72s=@xcl*vFVcQETTgn5V#kP^XZZ2r@k3?Q-qk--jX=;?~m85Qbmn zqDw+`y>uidl#1b^u;YOlBf3NWr4883e5o4 zgB-k=(oiT^(EQQwdp&LRN3v|2y-HMCYP*O0LBuRX)^bQh>-Zzi4mkA0zZ|Ya1Nd3f zV1dt>z|qsj68`JiWCMRvkVXkICD}y$SWs08TB`Nnf%{N&C;skNrXB*YL|YBwvS*u` zmR#?q!*|h+-wrTYRn}28F!4=f3sq8^L7)?pzs%K5h=`Wv*i%d{{k_Qnwz*atS*$h_ zWX-9>#HSIl8}d5@)EvHP`%8pp;EKxJX*_}eG*n#!t$u~Ki{?SN5&?XxXs};R!u9&> zIagy-HpdwE0kSo~NZd15d!RpIF`aBl=J*^3C2rD09!Y{$MYVxjgtHkvI>VFHcmuO%<-b0`{1`3Uu4L`tgLuty{G^ldt@A?=S=r&c$P~hl z@bpK$+GBHD^E-NHCGy{v53zEi7MkZ#cyUZjK-)XL@6Nr-wJYaF@UAWr=cyN4p?gvG zwXJp30sfu9X(XZ6|2m;=YPK9Pr}Vr6I_s8YsIh@suQ5~?CT?&aT08$zuUH1_2gZUp=NKqSrR+9Vey2 zwV|a;pM0jDn=YZJC{~w}KcaAuJ=a6||3vZ5O?w~Z=f=)^~ zC;-mYAd`>(6y!Lk10oh4N!+@8@9uSZMRnZ zG^G7{zG`w_=na8ZqBGL5(3FaPP5HQuZ^@>yf8OC5Aje+kuoW5QmVFL~d=MFh$TM0| zyrB>v8+rEBQbQGATovjZrak!G>ZoTFaQe_&-k;@J19mUW!yXFwh~jm*tLL|hO}zn+ ziA*;1NCNc1LN~zE;a>PEFNLRyYNFM@R`x076Amr5KUVQl>4Oj=-8;F7_eK_nyjcq4 z-NleYd^qfs`7)AIZ)7^DjmO(ImLC%^@1!UU-@us0SC+1@Aaj#sGEKUwmw4L}s}Bw2 zX5QBKp23BMmjgkqdzZQiU&Tc9aBMY7T5P9fs|85DBt`Jdo??pdsxOb@(^x)x&CFn4GSE$Sl zInQrSRT6U$qNYH-zlFISiH$41UEFd>IGt!FO+N?aoL7F9V+1dluw-d^lIm$f^qY6V zq&@qS!z=%LD4r$XP$*zHXz0t{f#s#eDs;>LJ1(kg7rn&_$2~}itOLd*wB`f5JQDSz z+#eJXmWe|YRqXPDd&bP66cbX6KI*`k@ia!#mThXJ-jyrh;NqV~itW1cY;e4GC7f-P z^z|nV=7!D_Wk0Vhk$vlJ%iF=MaLaGL1VW~6zz%JulmXxR3t)A^qj>GDH)HaLCfV@K`J#luISqB_IvS_wl^X46k)qt>YN>e- z@d=ZiAuBxmzz!eaj)_;n4isF>(gAs_Vj`a&k$}b$WiwqH9-aEYbb_uHbdV{D@D!K> z*U*pcAd)wl;eu@jsd#AJ7<}fx*?*A7X6GFx*^q7&4Q-sJ`@CbljCM`PEfBY6U=bw8 z^6f#HgoF3hAp5q}K(L@A6WpYt7PLscia@Vt&HH+u3y?o;Z3J1JVzI;c2v21*8596> z8TtDec4;W$^u{cBi&77+jMge%q3I_s9??)pT661Hf`p^U!qYEbmGLjPW}nYAz_Fpr zRhF{8dFRs`JOiLi+~36m$bwfpTcZ+C{Rcu1!7McBlxau8lTG7M%gTs zz`I6my<^Ubi|;`sJT7|22BCBD16$>fPJHuZ_f)Z=a6VFJD}+7OkA45jimE*EZ0m7w zZ}G}LOC3%xVA4m(=~HM22q7G};l9D4&nUIGcZ*a&0*QoV{EPI`I7?f2k|+DoH(?^3 zzW%Sd)1$Mgbe%Edl;QS$o$s2vcWOFrfPYxI_^#?ine+!%%GN}%J-f>3*I%c=E*F{? zz(L0Ra`U>`p0(|hgZ^KINv_uuju)njdw&=HXT5*vN-xEtLOz8517_xOIT%AfkB|J` zH~$+xli*ofi$F2=n?VpVLZKMcmTwUp`31bwF{Uhzeytcun%g%bUsT}K_e01!$hN_( z_oH8`sV)%~GJ=xDD1O&_TvaVl(x|N41+Wq&u=!1NqfvDs+wjijgG-sZ zR{0|`oiD>~^bJH7@^MbfL5OR+*sUkZE2IjMFOu(#%TnT+W~x;(xI5~yU;~JaO2x24 zZQ@%gEPf!!8ffslzNfHsa5Ml&7{c83NL7j&+#Q32E?OYHpCY|d?0<&c^=&ZkKopQk6_bN(JiBf)PVn^F+?5&4LDUqgcx#)og#z;wXFFd~ir zPr9xb*tHx`YY&vObO>R;0~ecd+3*uE@XLeO{xZ-VXyJddCXDb(3xtL2-OduIw0!z& zTVJl;px(&%X+FF!g^(D|Sy%NKK_e(l&RiwLMFu@e>iTCnjHghY*-rZ0+`fZuZ9J*S zVh_3pH{E|SxLtgZ$gvaDS)EmdvjX|RPqnbKA2Ff~siYH%tgU?N^wUZi*1*ifrcXun z%gEa2g#`S0?_ptHN^Ghiqy&x+G@#JYi`j9kkS(T!DEa*?R!ZD9mLxr5o_Ce(Kk#8X z^SCmkIw4%_s^DO$k;M5?HvDhxCV$AMCkX-$PwjVuhU6Hu66SIeGn~)SVa{>}Bajf> z6s)khQ<27!`xh*<`+=?tAG)<6Q~CFgXC0aE04Js6Z6N7H?ovVz6i+&6_7Oe7GJ&B! zI0S>08X56Xp~{0eG`0;Fo}v%|kbi?nbP7F>SG(~|K-P2xkDh+i3XoZ{_O#5p@6 zWA7U8GmAa(|1}pK6pU?%KbxYCP))va<-Pv(coKgoGKDc8;5?_js(Zn_*Y=*qlpz@- zfSbWdyec$ym}=VWyG;_ur#chA=1-M1c+{nOx*ELWWR=9lq&{4!eHsK#g2 zuyCUd+QX`w|9`M#h-S>*z#$F_agaL4IJ$<@_JliVQ81lgv1xiC1UnCk6$gBHqhGE{ zCmWDxV?-OlW8Nm_ktBjUKO`8N8}$|f2rF%g2ecz7MVq;5=xKaqzA=}gBfp~2OD8ne z?!5B^^NFysJEw>jE}Bhs8DqXHM+j_;szUzZOQiR*Y6NJVB%+aW8{N|iq~~tnfdUla zQqHvCneFJ4aNCN*5+lXRqQ%2_KEQ4*keAs4%cWwwb`MQr18ApqzGM8t2_w4ZTFK&t zON`%zP#;vFHx=5%lHj&sA|tRTj4RT$xISA+rHkBawlRkb1f2Z&5|vUh6WV5~Yv8Fl zk-1jTW=fHWJF%g2N);LiDDNw?K&b~p^>`cCE=8e}gox{l4>7Ir@}H)y8fFL_szY&p38{Gug{8Xgm#kSL zlf_-ESO^_`1utuSKi*nIIPpzDlj!%`8vqt3M1JoLV!>NsbaQx#`NjkHh}~$LTjih2 zy7i87X$60B<$98lPrRt&X;q7$H#LtRJ6+JmTz{343cLM+uhyyEWXARPtKXk1Fe+i2 z^OHrC_c_8<^CcO^+06)OE&SI2eAp}l49Xa)dk+T0viu0$%N0Tuajm|wzehmymfQsG zkTP30>%6PRrHS74K*nxpYp`M``DW z*vx(wj#~l8e9Ztw3~cSQ(!Nj9<}T6F5kQlsh)kt{3#7b#Y z>)j0u8$IVZ@^`*iNccUazZ~a1*I%bLwtbI{joqKf5xvgxzj7+=d`f%e8Q%kgL8^$#VnB9t)^$d4bn z%yX5PlT3dyn&8idm=hZG-=#=OBx!(}^`Av)7in2?$a)LFGh>-~bY31_L;gO!7uaXd8 zBABCPqdK&95E?O*w0*sr&AXU;*sQW2d6e}-m&iL z7ehvMdcY0S;+dRZ%u;R?%_KA>CYod6nH;APtx-rt@ivAoG1|_F48r6yrs5*-|Q?Nu> z|IAQ>_nA7&>#s&cH~!i`JyZfQjC6k^kF)#Y1GVz2sH4>=ebcc$Z9A;C(`7)U>TVu^ zF-)83-dfEYqnP^Me4cKY0|W@v?x+?|>#pWCMaUF!+ubfvZ^2Td%_hAck5w01r}-K72Brbb!EkG4MQs`*aDnv zFTD{v+`o6G8l4wLN4VDX5R+`M&ZhBNY5ry0|7#v~mBU6ckK#B;1}*Dh)O z_+h)vFlbnN{n(hL<8qgGJ(p%|VE+l5|9Sq|*Y9P|KeyvrxieYf`9PfjZ_1LV;Ge?> z$qNF>_y7Iye?5EkeW>y96{J!v>wbMEU$4ltGs3MQ3?nZAHWU(eJW+f++zF%oTr8l1 z3qKGD%{HD0S4(`|ez5puKX$cDCnaU1Cb#t|W`M7A@s3tj_L5vJHtyj1V(S?Sw?>~w zVWsP6q?=a14g{}$75K~CqWebQQVfocl2Ks@{#!~$)F>?>Y-16O+Pp$;4M`hsS`6V7 zHqlE?Ci;p{;~6tLH{y93s*6O#IRvGMs|laceILgu2rctd!Xa32Kx4`#jU`-27Pd0@ zLtlJ0phJ{@XX(A|v5#ib1)mi!$~099*O~6L8~vr1b3XlKL%j3Dc(@U$kPLwhhE3u+ zG1kU(SxSVTV-N%&BT+M1=y3qH4j$H9N(DDMsm6B&Rcv69)`i$I4_{!dBgfw3^Vm0E zvid%v6Y-eVuW{Kfu-DybaC3{TOQA_F){i0Zk0iCB8xM)W1e)NBlU%U+*^k9JuXh+G zxrkwqwa3-8r)8pmYnKESETnK={JYR^EjxcwBc9oUt`k7t0F10n)Q863U%=d>JYjFX z@6X(~kzpe1#4_y83i_gEK9U z%wK&p-gLN2p`LhNnBzR^mBd3|xGZk|9vCdlrL&Cw&ysiV+moJZ9#?)Z_J9%$mr&0m zf$Itsrm#m9-zoeo-u}PilV|T3zc|la4&CY=oGm55Cr=G2rqXIL=_67~M44NJ)Qg)o zYY?_UAfrTZ-HJ%?__zZf88|owfE7Jl(`zWbFl++;v=idLpGN` z3wZfDXsBA}Jd5UEjD{*>GVXzQ2ve*P`^N^OYT)9X`Fj^#G&!G-2n`EHO!XNcFmX%K zeLikxqCX>|2T3!@ova=&oyf|t@|^1FMRD6 zF;2xWV2hjEw{IWz_V%uEUNuPK#8)%sehbakk2 z1iIE76%dgY2QjZ0NG~#H{tNX;oB_zK2#qMX_Tkpy#5*^6{V?kkc^;6VtjrdoES&b@!$+w zOMLtW`YGcM7a8_Z=ah`zt4*pWYPaRBT;bf$OIhWX;jF|T0IvBxes7~MKGTLb(0Bx8 z2!C(FB$29*-WVEe3~!kE1he1y$u6Le`AZ!=JT)JZ)Ytf-=_e@|%)TvpcaPvo%OoGwJdz=+fmC-~y~<*+8q7l;>W2{dC-43?6F z9@3ljzsa;Ol9EUiW;6gooGLc89y_~>J0uk%ptRa} zfTy2LH>1!eE{A|8FRyhDPkS0p@|5fww}|H}>u2!|7W)?RUUV8WOlAZ<`w;R+NqpAi)(DllaW}gqoRhf9QZn@!k)_toL1@#} zuA=f}@844}p!Z|bzb9nAvDxEcADAH|2oJv8aNbWs(s|FtZT(>@2jk37OjR>A&#UiM z5^t)R4Nb|&^bJGiJH1$KAoA?;ydu&Buc){eo!k&Zm3s7}0(?dACZmEjA&rIiI$%uM zO3T^wgcT)<6f^v-2Cia*#BKM_z!pDq_Dw~XaGJY7ntYnaDVn7?=AYq=v>V?$Tf>U# z>OV^AGTji|KU#C7>eK#=L+}-Wr&#yS*?=6kXv2r|xk`@CC=f?Ai>NUVw)&sdTv?+M zcb^*aQ=V!eMubnmhaG2b;o*rly>{K#-=;_PC))ji31vo?Pkh%uG=i-V^OUguODuYJ z7=n4E%C#gE?x4S2?>#G{D6y6S~;K>}p;oz@1$?we-x(<8qr_%~({C&pz7KUey`*AyW{t z<=Fqq(j67%@35=XkA@m~H0JudPp{FZ1xdbxWE~b(sLYF z_HXooG*wWZXfIlxa>-n5tZM^bfwGd#V$5MXPQEzG`*Kf$L5E>lpyVHP3K;MGV*k12 zsc0Z$-QnO@Mjf|rS(j@E?0!LIm-b^BM8?ey_vn0F5moEE-D|@QBAJMa-&?}H>JCT4 zw`-HhhC7i}1;NC@Zqa)zL*j$!CxN(P1os_mB?V}2))WD-CaK;CEs3+bwzogxS@6D> zbevhwD1YAkY@7bi7qNS9`;j84FW*N$@#X$MV4kw{%8cCEf z4-ejyjrD2-uZZiIC_Sre=2lKl&gqM{{w^T6&Ym+QPj6KhMYj8dk4iNN`Y=uJs13Pe z(IIv!{tra6sEeuMxhJ}&2<_Ooxz6&NA@O=WK0c22gPz@s1Cr$a{Og0My5wWMr1#QB zxDA3)i=@A+^WNFZN~bblsJgW>{a|`6XS)6TRN47Zc>tE!1BZw28=pOJfG)c$freC$ zU4k-vQs<_BpcTB?qH8BA|8>A}6LU(IE`X|;``an2nV6!$P1oBuxZQi94up6N1kIZ6 zXRH#MyQ|^}=Y>A%M=jq@mu&c4f$#PsT}K`HE`N+G;I;LRxm-VQsV&Kjp0*$PLJA(-SwDW#~qd6F_lLSP$V9LEMBTt0V*s}{d*==biH=_!XY`ViEYwf^<-rUZaKE43`|!lP6YOI&U~2LFa8Nm zIz9BP4f*}KFCfO=z=Y`HM?t1r(6HZcC#gUxLiH{Xx`7x))$I|1bap}yNUyTwkUDTk z@;rF~Wl^?(9J}1iwjLLZcb3cJkYAG&5|Nec6SmyyJ%7&&2yPxgc#)h3_M{CQQa+rL zP5x{5_1`L@tvPteW~7(^I#8UCx(*={R(N&@iDR_9M3nA_*t-xG)zjv$@6wdRCN-`2 z$IEH|ek&(fx}$bTp64q|v;(*mcBQm`YyF=$edKdjpDAeZe3cLP+SxCE`YerJ`ns`| z95eOr^;xEkp&qw4Y4~RSz#!y=6!!(dg_SxlpSAx%Cc?CzW1y8q#mR;gXY3wi2pZi; z5lmr}m)?ndLEkV@pJNE!`l~Bcji4&EKuUSOrg%nmEb4<@J7A^g8VP?+`endHPnB-X ztG$#_bd(ai7E?$}&_-1&ERQgjFCs%2?6zfuNyW)!eJSRFB?Q#BYJZ8HP+dT{`XMA= zM`Pq`FN6jALB;pT(&xV{Bi;@cs>@#|mtQ%`YK0kbG7txn&NP=Lh6QrDZF|>tcl|^7 zEQH>}plybSNq7fDQOB4&P|} zoGC$Oq4v zqPs_gc;xv9lf=GXFqj2ZMZVri%YhP%N``XcuZ(9FZUp{smg*#W@F9`&9{)l9b)RNxY3=3P!(^C*Y#z)r&tnEJM^bTmFe$MF3WXj#pZQDT z;XMz&Uiec!2?8af-{9FD6;OoGOJ34BwQ%c zS~Sj8CL1uNWinF$jsI{S)KLfUu~icoY?sfOEhe=oE9lG6PgN)l`ptc^#iF~N5B=H? z7?S5`VX{T!lH_!uSNbOI-5B)o$CUiVhKxhvW*+BJDn=WK1i=a)B{jc1QvFJ%?*B4E z&xnLdYF&XQ@R=J9o#7KlT5Qo_tee#})z>VoW5}51@5KqtYzQ6lq7Yao$!s%*#90n5 z(B~AAj!-#gCs&UIer_j6LC1HownNZ}M92v4-sO5v{U$e0BJs7JA`QlY8J{=-x4j48 zYL{01!(`fa-c3N=Tu^E3o6v||v?X+eggN$!gVI_(itj^t%3tFv z*+7n|)pbDh-{7;iQk(*Pl4c5UQ@TLY;S=EY)vX<|6>h?lrxq zdRY=Tk#ti&s}{5Co~psNY;X*VrQzb&n$^#)YLg)(|4?EsGl_LHHj0Vu%7U9hnXIq) zt5Lq+2{!p&zrsu<)M*~BW%kb8#zC@9o^twR^%C!s&evla#h0x@Nr83A?yJr|URe%N zgak%H((X~$8tBnMjc^*>t$;-yDPw$e*vx3)%3QPq4@f+gQz(g4!dFcWJWg|kiMFR{V#%DvAb8;_Xzs19NEr`+{(EqaFY zVt5N28^rsWBQ-8*W8d9wcffp1l(TMmu|8qF=)Axt22Dlw%}9c}`vh)cxLsL?0dO5^ z`1U*ltpJ_+AIAw`C5MY%MS@UUooGGbTgVyOtKZHQ7?x{)?7Gsyn|$u- z_b0FO$$O**aoB3&ob)7w+%gqQw!}Ax(<9~w9`(f$UYAq+D0gCBqbQZGt8BBTp+*jzjesTJ>^1TJj z@JX+`0y|rhhHVk!n&S<|+$7(2bF?`iQ+>?6p&S~$1OoDnLgdN3{alw9LJlIu%{9gf zZMI^iVo?sS&lcO8Kyzp?$q@mgE* zM*CX+Wr(KWzcSeyc3K`S?*{H39h7{zSfLX_DYA6*zkRxb&b+N{`{kkrAR+C61576c zJUwVGp}i8|G@vXRWkXO3Oo)umHyKB018_>WX)yQ09R*SW^%ceBKO1AkQ6}+NwG|JU zb}HD6>P^E>PffiP4@f@N>5%<-*V~&5pO265?;xKk;xtWWSUmh|uqG8#&YOOen=4n` zLJIm)#5_2)W^RmQ8!3|;CNBTWe0(^#aOKs5Ybl!wR;oelob|Q_j3t9HW?t@ZL^DvQ zKrBoiXKttr=|*A^-ft$VUFEx+QE8;YqAD+9)3YX;3-@(`vS)0^HgxuKN35NX#srXC z&Ru`4-r}O%OSl>7gX(&Dmp6~B}kpktMX~2^kf}$ASAKCASq8k8t@xbyp94gS6 zbjp9J^ZnP5trPfj;O+iHu(gC~vEMM6Of0>4#z%t?*|M+3H9>@jA zRQ(zYy?&~jCXv&kTuMIryxFLFF8XI-2ny@V$9{1##V@}Ysp!VB ze0p)zRq_uQ#!yiWH%Svjt@(^U?>zdnPB>3TmcB7cB>R*855lfuo;;Ae2%bd z1_>hoa{RTRj2$?>*Tio-x7K;d65SMcKLKKk z9=)(!>;zAs9~*p7bL=f5JF=*JTPH@MJo~$bj)&wht|AdR_&sg4?hL6^cDp)L;OvKf z@pcd7#^y9_fzn-J7YDK2T$gf)n$Nf0*v6yLg>1U%Uh|KYMDNl)_x3AZgUNG$T6-~1 zr)>q|n^b_x*hljr+GA}=U}^q-L~h=3G+s25nrjtL={C8-H?MNGIlxv}BC8W28>z2w zhV@LMz2x}@EQXC&4pMU|KOmSqKH^waOS0Y_R0%b#b=hY=I7^oQ!8LUaxvT`TrkEw~ z6#)t3ifC%Pwd1IDf3)*y^k65=wAAVK)~U?t`L9z+AxmN2@9O`QkZv>Fr%u3g>g2Ok z-}>6>|C)QXcL7n6eCn0-agH{9C^21phFSaVT7P(@di@86K9B6e%p~Q3)s)EJxJZTi zUT(aqv6+TtdLLnO+C|Hj3L0i2WZDzENIk--{$DipVvnxO3o196FR8kV4Wk4mf9bPR zA^)}HbIX=!ppXL}=~o$zvB+`S#qfyHb(atC`{-JDYxi5r z+uF#;NJJLuI3pm@1J_C!K!H^!>js){ciYp8aHA%S(b_lDNgCTt2&s_*u`*49CcVO7 z1ndgi7!FU?K?;KS$x)ntwiMzO2=uo*A8#jruq-#QtCFl@xTt%8SWy_J-Q>_P93X6y z=FM8l{O1<27POHQ9xM^#bs;3Qp|Ts0(N zZrO>JDM*6Jw@u-bL!YqGoy63!a7X+u#0a-Kp{_H`0wl4e;b}F*a%Zqb*_VV~ou#I{og7&fw6?ijbI4}A; zzt&t`)q_{4Q^WVUyBCsWw3nS6N0qb0Q-eqxI^ET?%jks=j2g64Q@|e*mFAjk z@6k9t`p<%o&cefU@$QJ=*EaNJ0Nz`l8Nz4c!Fd3u)=TFP^hi09{+rgXY4%r+90bs; zV^f*u9d}qRs|#6P8S6~kk>7k>pOO4I2vN>R!PyZ=vBI+C5IL=KJ)0KY>kt&T8C^R$ zU)BuiC3C~e-r?@bKAZ^s5a&$rjuj#Mstt0{-ystv7_EoXg|jecp$5BiGJ{+UD>uJn zKJqJSt2lq7W-9asfl7qw=i{prqF_`Nw~CMMmW14Xvy^cbicbbKdPXYw8PbFjM|)>= z;QnVz7At^x$Kx`y)^KX8GP#v=^L9crj6eVlcA1&Z@`~Nay3`S`j^|_!eTL|qTrmBa zpUy7QYG~z6-39B+B`;9nxTN|di7b=B=F&b_A7TA`iBR`3Z!EtH;)D?RUJ1SmUOTSc zI$p1LBRl|K#n*>v&gW_7moeaN=uez0UQdutyyPs% zeXGOK^|b@SF($I8OE)ra?a*hh>*b1#70+l>SGRHTln6t&(8)My-0S6vx zO-;RqmQR*=Qlll##2JX$#oHJUdRt^=yF)P-mVAYFe5LogGG*?N!6>K6ijDZ#ILobX zYEUsqzCdm^2@Z9WS>hk>M#?iAp3+3dHX|bwE#vvvti;{)-nP;LX2_JU03~dwt<>FJ zMYn@SCh3jx_UnrQcd?j9BmYonQw@{sj{XzHGNjU+S1%XG=IF76Z6bbrRxeJL zE5ZcGC9$!V(~BklkEO2+Yw~~J21H6iKqRDVba$$ZE-67oR2l(k>Fyk*G*h}$y1PL@ zIz|s9wo#*b?(gsa@L~tA?s0H$pZmP7vp{BTs>5(E{+EgI^2B6qbNb^cl`Vy)=#L7I5sKxfH=C(hRmLl!X-22sg7-D~-0{Cl>hK zl*Q}bsmIw;4@&Qv`f4J%3!V(J{Du-;(Z!u+a10hehaL@lHl4{*8>x$p6tgQ4`y3hb zPRUEN=qhclc^S8ao;}7(YI1`~v3ZkD@2KKN>p1%?MAiAvWog9DaTM75pcKJD zqh+PfmM9S=Wawk8nxx~Vxx?(nrv;7W_I6Xm{$m28k3n}gI4U($4@}EI6dk?1z12vL zVG~}!e~g2CpSOND)_1qDvU*5z%v-DTCh- zU~`LkFeU7Me;h2go5^Ov8KAg0nHJT`FvVw)jn#7g;3`-*E&&Ket9fj`S>m~0@BLcy zgwP+|`v{c-Dd+nspoxFb(eqh*2SJQfa@1oYbJB?U{N+!i>0QjbBf+VzHTP?>lE2zG zJOZeX^{vfACj*JXNP0UjzwH-ZV70SGp+XH zYh?Z~FX!hmcXB852f6P^q3lB_bW!xNx!`TMd|sO&jX_u=LtAAr=&$$*B4%rRtIY4N zm?vuQWQp2Usl8rM{Y>LT!%Sl^-qMkP08k8pwd+v(L4$tI(U9(_2w%`DT~TM^P(Q`mPY)$Bqt|0yJ=fegA`|J$8}P>>Ke>^x-- zS7>30EHVz=T=krmRUDhqvt*828KTo1)G|SOx4)%Slf+OkovDs|Fv*)x%+-*U`Q_a4 zAnmXj@7oXoykXe7V*Lgsw4O`pH^*&=Lq$^85;M$xV3Zk@TYv4DlM&G3#qY_-rZx_w z`qq%Q#rAd1ToGHJHk0prySaDqWGarI|8*w$G%rg3R^+{jNnC^~ds;0{bU;K#1PQ&W z&4Ck_uJ^>`gaJ0c&KtEc9p2njP<1nnHUSIk)NY~sH~(YWHHK{aE*<KK{;?%$_J+$TwzzvDO|}{Z+XTG9&$^#&OCg z#ud%N=idc%EAygX0@))g{a>W$$*0P|U^nl=`S)3iX5VU{Buy)lcyfP50JvN&(o z`LE@hQ>6Sk=mEPOE{kAd&aJv>sWR+mij1U}^W8ohAd6}8KpfW(w0^Zrf8zE9)JaA< zzHonXY4h*Q*A?T{#*}%(8WGN#>|gf9(Yw>T8@^YViAp`>rUQAuoSx4N$~Ecn65*V^ zG}&V5_rpqr8PzgXUNQa;V2S@4TNS-1&?sMMr+ziN2`S-KBfRS z%M{Pt(0f6IC+0;MQht}aE-gE96xEEZoe)6QExJ!#W%H;Og^#hta#Lr$>33Wu7BgN9 zt?8Zy=16`$ULjOw8L8|3s{L>&7#`>}cAc2FAG%jCE|KARvcKi`F^dRC+GPE5`}i1$ z&{Y=|4DywCP&GNp{v~R?O&bT8K9P{&<7!$E`??a=R+*_IAb++HS)rb@p>aa8a%%N# z`4zHTedEfh8lF=%O6vm*J%8vhYQB&;lg)XY6{u z+mE!W$}x>A5?9U+T^_#ok={*9XTF~@l>RbGjg%ynR{?`z$iasusplv_OnOAfQYTY1 zWwZCG0bl|SDn-}axl@fog+7m{M?tlUOSxJW2uv|vV|CzYv+K=!o|VE=WEULNj(gcI ze#H?D`3+B5sCk+G`BI;*4ADNX!2@l$NVs5UonC3cUg{oV0!gi6Ijo&|H)xaYv&UUd z!;-xWM5`9yNU!pnbPq(<9{?!vBkpBdvn1i+Bn;OGB&De*$~ZXn+1yY7w52k{C*>fA zx3G`P7s^L&m^ImC#bv2s(W}Up6L{bm0IiZX)hO>#{3L{_$iDf(P~iE$k_kFsf6!Jz z^;N_Ie_`Y_z0w4RiDAQmP1D7n@*E^SFr z|AVtehQSUvVJD~e$_(lsd&pf+L7q-oxn8cJJ3P>V_p_bXh6W5d73$j!`J?;EN|y9> zVJ!6^VRcDX+;|Y9a!keWW)W25y84|3re<0I@Kp-|F`sa8__%I2r zB4sx$nV*DyYcF7z;oFmr?@w~*80e-Jnu~zAwn`hiI3+iDpC3xiFy}o4w0q|!^s9Wu zQSLXQWJQ8rd$3hy*ul1C7Ff8>V}d|5Pf(Q&EDK?bpZ*}z2KxOq^`J$E{zS!vC=0c8 z3C6$nm+f&Ta=%7>UsE5!$TQ|SI-$)hp1?`xJTY{zoOD#Ax45#mq6a|Ae_d#>eP2B> z@m+X;a1Mz5k6RPq#Jh2hFS966RH?kCarggse=NPONyeNq=h(>< z|5V`FV~)0lpD83{`>~Xd3V_ggCfBQgTcp8vRAi)9df7aw&)k*l_peutG>~x*e`F&? z8>vHXUOjC;>(2(iH66x8xR5U``zM=r1lvn5-&(rK56bmJLrIZM?uC3OE6ziH`4Y;L zbVWb!+T8HgN-H_+A(N4>ln3QFORB!k4s5s`tv^M0P5OL0=!e~AK7<&T-!;t90+It6 z(%Iy;D(JI_b@}B=luKKfkV>-&pY1TQ8-iBbwg;bJwS<)k=s``K zWwK-aPU8k!(C4x33elyxI_XnC7dn*_{iLR>7`7#gIscb~5i7}w3ry3&siTV;q6X3E z7mGg&g;YLBxADO2{pv5f z=cHQm+Xt5}^}4PY=stJDEicbd-6}XRYb2V>$h_NcuQB1H>xERj@dFOLDNl%>oOiM6 z%TKS*Z9QR?wrZzoAg0)h*XQJ~wEkWb@4)SSMoTotb^bx`ursR{15Nq4QIX@c-_z3H zRy6__!s;2$ZoR3Y&N(-l(g>1An@IEt3V#p|yFrcE|6nwEo6?yFan2!{^3+xsgomo~k? zU-)psSY>l~wqK>a+NuTliz8S%CZx=Dqk>S=JXT^8{}f6 z-;k};P~CLTt`qQzJ4V!Y!g2y*s2LoQVW5v5=639z(9|pj-S0VFh?a0hCVDIofwWb! zVc#Z`x7JtaIHzRb+I zEI-%_7Du6yH>v}TpXZAgM^9mQHZm*YQ!JlNgL(oQ=EHi9+T;zwbP2#dvNfW-|w8FShnd@ zhk1#%&-ZRV;L%-0=XqmP z`^!$2#WYC6h7j#qh&hlZuVh@$ri;G_kZBJM*B|t=JBbQE9j} z5@JNyRqfCP6i?~Y2cYF!Z~`+*3!IwParXuf7w2gpLk=mIR9VOxV5vToW1=BY4tzm2 z2X{8wpEr-muMkhBN2q;`&(U*Z-Pk$PS50Aqb5Q=o0P6yKEvvC{Xo&8LtnSROxTe?*2ggSdo6l5Si>cgpD67Vu*hs%ntX8{O6eT8g&HCcvAr0F!zs03@^n^U5vMVM zNHk@qE$>4ah|Ivz&u{bW^nKxUBFQ(u&%g?7!0-FLvv<>gxG zgLn*XP3cgq*ppzz{TwynurgYB4Rd<~LN-oM1hk|-N8)s)3sQ{!Mtv}=QFKPZs{-;}vz$Ya}7$5w^}(nP~Q zDwGkAwvgd9o9*u}n1Rr_GE=^96-_S7dS(SQOTf+BE14|rIx@=I@%QLsF1G?UwIz%~ z*+IdOW;%ZIU`pF(G^N1k-7ElW`nuHhi=aZA_C*(!lo0c-TdlNmr_(Ap^5?W%M=afBt%_AkjJI_*pW<79LRI=L1wqM!?da@tgJv+R$?MasIMvh zoze(KF!aVnFIgvD8curxkKk(XN5L{7vNS(Tllt8!^Vx!+L;VSt9?c(MUZrE2({KI% zi%60`33!mYjtSVQnu1;n!w%Ptt?k>6=rw6biwg-(50+`8Scx9|sv1@@`U zFWOaSBrBe6&FFbJEMAb(GbolgqCy*MP`?R3_Dg%l=y)y!8S^tdw|cHpWE-(h~plHC7akFih^t_@{Yah|4Yz2-LdN{0f|nLHYPFMXXC za0Z!9^;=AxT3)+AkdjV``^|`dC|kUoK-_A1Uo2XOJfQ6Yj;ek&cAWFJKHQW~Uh}{w zJMZW3q^`Sg<(!;iCm(JnVRZIigG$~2obL5}XU_3zt8&PgyUT})x7S2~H!(#GH!Q)t z$KhWhM$Fo!()_#a9YY`ofOak(5<1GssFv!`ryWYgdM3TvV7dWL?+(oILIy^rxl$}u zSW3Ac@9*?Ku6VX80r)$GFLXOxT@Kr0>oKL)es5bU_LkgPn{h$kAiQ}43wfokmEn0iN<2Ey0uddI_0vdgh!#5^y1a%NW8AilGiZjN*OYb8n|C>fv3{?i9({g)BfDa7pu4%+u0c-#deA);>;ALtZSxGaT_4FT-Xq~{@GHsTUPns zUXmAf!h3cOLlObMkHc=qzex1eP0Fc5AFdvbi9I|=GdlzQ&#SHth@YdnNqmv(RN}y_ zUKzCEg^TC~G%SBLHx*5Op zCM=DBY^|c|-Yy3(#Pk*mXhZ$x+QQKlewnt0jLp<7=k?sM_&GJ(Edy#8gSVIoIBHAN z`{3?m>?_kN83MEdxCNnm|86U{5TLnFO(G2v@%a@sUAZ{5p- zgR{C7rjDYyLQC+7wNIKE%m5fs*@m4DRi8g*qnwbAhX(Tk3wZ{67-g}AQk30F^U{30 zr6WGs0CZ*597|cbt?_SnHL$Tz$Jz{MD4#T`u&Y03&j?+QicG=gC`a(0*o!O6_Jq9U zf5DV^*fY$$Rl%v-v<)KrAxZi!JI`pO)dhm%KrSuc+pSOP>Y7W^=nx_M*su=i)wDBc`UK$WTA= zULjbzhuW?3sKu~2aV+&C4%Ej!;5d!i_R>lw702oNgiA}l@lY{q$p8GwU|aGjWjytR zC15?@sP-k2`ZgvlXf;RcNMrE$`wote;t^=)lveZ7KxjB|Ve1FE5EUV<9zAdT505q1 z2=kN`qyY)}PHuUtJ;|^nU?fMIIMGJM7_+6mIw)U{-QWr7wt_8$|6fcg;2Zr9s7UAb z82a`vd&u8}J6?J-Ghc^&ynK9p5O7x|kFqv3G5O;L}3zr-ii=I?h+GR{q zr3J`@CCky*^xm{_0Q22U0zST}_m06V>h%Qk%8r{Lv_$F4xRtPSjVEHlx$EeIAy2pX zLf%vwpr2$;s}CZ9Qu%{Dv{vl&-Q#7X^D;;&5U};Z7PaA(Mhx3L1 zc5lPfNQmliUUZT|5r8^I+d3139COC=G6_RWwapXbG&>&{S|NH$VkLGk+q+$Jp$*1m zSRT3npj^o!E>HL$Tt;v*<>-PGWPrPO)`Q7oCMF;IM23?Xm4j}Jh`f-%p4#;rn6c91 z3lisCaqIWf zH||F4WZ2Pm4s1=`-}Nb8fxTho8be1F&R?uZH+tuQb6GiXZQAEJyYHRS-?uGLYdE~` z3fu;jAp;*BG>H@qS^!dK9h{K*lMwXQzKyuDPPnGag#9FaJS5)pB{btvAp}4CgYhi* z9exmzcE+5GZF^H&{$rNX)%HelbRrf}{ogD-zLBL#9r*bO!PKl(=mJF`Y?n<>hg(fzNt&pUl@*`u z5htenx!!onen9oeJVIRoTL6pV6e%o!8aDym5e<*5pKm77H41>KrvNCl{-K3fu4lgL z)elM4rCV3sC@wgq_Wf=CocbCA%GyPS$0Mq+6$YoV<`i@mgV#Be7fjixY$ZNjFd;9) zJJ(-E*w+|MM>|Ikn8P$&YfL`8?>gs~zpcuWe*iv-1f-7%vn%It4Y`I2W6yY)q^(g@CJ`}sI%r;!_4QCQ90%;uvufOMj6N| z0k|OY!)P1xY$H;{q~hRo(0lD|o+N4LwoNQN-Mi=P(`I9u^ybfpt)GkH)XO@%}1K1lk1uVCRT3@3B z>9C(Uetl2dw$xG>yC^%`CYM{C_Ob=_WkOH2OM*{)9GSvaX)}>IGb23s;jXy2geKlY z>*tN&d55x`Hb_x3Z#Xd(nS0?L@4-*S}ZUfSLF#3=?O)4VP1Kpn9td=H7}7cclhxPalrx8It(Hkjk@_%=qhkYxTwjB> zfUqEjl(LI)U6nT#Q8=0O8L3~dxmjGdMTv00MIlkiTzOt`x@qnvWV|JT1vMjA0$jKv z99+h!)AgFl$XZ|_i95#(52pMPPtfxiLLFt!HLW&F{BObY(GvP~v8d?xls%vfC-id7 z^YgFk#VZXqP93vi8T?m;USC#b##;l^Ig#5ygL0HW)CSg5*oK}elG%IUh<5xmD}FJ{ z>oQu)ky_o4-DMc_iy4-qb}tey+`3}Eo`7`8huyO$22NG=d!pLM8*NIEr`Pjt(WJ?z z=NL2;7-H@Q-cA^XK+ZlGv&nUR(32E|>kkJibZh~m{VU_CG+~#?byiC+Rs`V8(yd?v zZE8B^5=JWWq^je3c%JPE0vQt%ushs`Zbz?N=dZzvgg7DN55OiyL&sP9Sr>Dm^4a?R z>MC@;tf2JG3StJ-T36{{o5|6nTCNOaPE+jolT`2lZ94Cpjm*9|SN`Z|r=HYqR6OML zlDr$Ladf+m7V>9<@4HF4U*H*ATsX+e$+A_x30S@J@fR}tIq!Ww8*%nuu>GEb{5|>n zz|9psX!42oZucWNTpN&9$F;ZJ-a-9BGC6vona|{$vd@FiMYB#Cmi36#EBd$Wb+PIV zIHr$YsNSs|dzv71C2jwL=UWHTj4ZhYfZQ`xJZi`w@MjvtGPuy$TeO?vB8D%nS=p<1 zE?2DL@WzkeIRV|&9`9$$o*Dn8e}92vQY=r0xapeFYv0z?`rF|XAiqX8*UMwbEo$Bu zakz59cW{?47&s?2d4qR<{JJ2m^JKb18|b`XH{4f99!_)lF`d_Msq%tFEB8UmLFTK# zsCU}wXnM5mcNrh2%1L3i&b$94>p(IAG&x%$m*=x*ZrFoUThw{B5QWNkTe2+qJFx{> zpD>lj-uWqW8A37u{tbh9nc#d=@{FjEJl*-Xgm2M}+Fl&RAIv;!ehhrmxCtFwJOU=A zlM%16*W9#`;hE+C)vVclStbnFW_jTR*L3tIH~6k@dasA@#0BsuvggT};r+#|d|562Ly8kL0#KAiOF4j^_P&NgYc8Tjq)$99l!tr5A{(7N@+x`dy+=4da7MU$m28 zH!3vI<~Ih8YS!+S;C!2Mku?|W7y7Fi0^i?LJO7kUP2gyIoA1Wa?@uA{km@5&Y<>g~ zH>nE&9(5?J4iISJse8S*k^uViJz%apN zGSbd2&vP+H9p;xS(sN~$czL!Z3aViHHDP3Y~drIo}-=ds!f8*(k zTKqv~v3!!s_$?#F(wt`z|G}w_UZ%Zhi=G;i5m(Ca{aGKQWCj;Ze^`V*AH6F$2gvluf=N2*}e$ z9`Pu~Mg+VyeXDP-O|f4Jh2t)MZLxNf+tvn266NA%J&{jygpBNYEjz>0RduM zQA5eMZX#S9o!*8j;tKUB`a$&6pfZr2P2UVbJsxD~e~8c? z3zEnw*K2s>FQh!NzjQ7BPQZ%JM0>fQ|C7Bty>gVRY`9>BDl`8{#(_G(-YYq0_>pz zlB}5_)zGNRUd%P=S)9X39iL4@Sn4bbO* zFCMMP<{Vzg0NjICTKX;v+i@Q^f9oBce1P+q$ainA#l{s&pK|j7%@RAhtm!Q+r@Y4) z;U3?P3*DVernOC`((`0=1~gfm;d^Cj`y~+i_Ldt&9_r6x>S-SZFB8mOD5-j*gO zIpfy>wfw#S{4D;X!5@g|<;khj(Q<3NLhZVw$kjHUGn{VYzCZXJO|u$@86*G{YzHoUtv;`c1rh%^bDC-=2S}<@+5U z7lFS%YTH%6!m`+UJ2BN~dwJz?iL8kaiCB@Afc<)^YI{hbH;K(W{`%NJLg4J>;j#!= zH|bLkoxzUqug7HVV=66ZY-E478$BnTOUGFcknu)OqD~WcVOOM}rt6}XVn^{nY`Gad zkTXE3bUgSnA+!xl-7v2d1oo>h0&0DTBRo$tVH#dPbOvaTU>6OI|8r|HPxml=Va5^O zes_%jCMU(>gjJ%?@iwkA@qVDO(-qxGeTdD!;;SGosgU#$0c(W^{mRHExia-x4Y>hq z;@`9az;dXahW`2BBZ;a2t4@$H-}BOZlIDw*cAtnGuXT3Zj^aqgOK|5ndJwT6!+&tT z4?b?SveQ5v9<1~YQ+~hr;UbfJ*UHNPfEXp$HtWhYe&Ec!d$m`mlsqmgOHZkv+6LDZ z=}m}G>rVm_Qgt>eZH-mw$X+z*&Hm?8j~&ng5;y9G<|}RA>E#gZ1uL1-ls&&^f-BT$ zCQ)06e9oZc-E3VkG#KM;p*fKL3fP7cjv&up{e%t^alno2Qej z-ZIRozg^%%e#e&^&2!gx(=q%~0lRQw&|xRQaZ`j8pJf37Nx&B~rMHMyTY+`wC}feg z9iHyHe^?-4^({4u4S0^RSn-^_i7}#31q8mTbjAl68Y#`afVDu?r`h;`$|({NX6{DD zJ-=tG+jNY=pR=#3va+iuGXm#GVx|1gNk{P!MFGyAi~_t|wJTeIvVaseB0$xO`EcGp3{VH55GPW#bbIW4U=*#w#7 ztG`A5@%Q6Ja_u*#=!Z#p&vrrTVPB!_YkXQDcJRT!bwcjV1vMJ6miOhiy!bZkGZ!b@ z4;wNrzPHqIgZe~XJ)`%oP1TMjJ0g~Mz3$UCdfPxJ$falAR6?O1dhH{e#{8yqaYqTc z`C`>!7e%W6wLRyO^)}lPQO}RpOxA{17kluDVp)XJ^t~@zUp-6D)1#cx*(Q}|k5QeWAAwwW^u6aan2?B$r`ga5|P$8shdCx-!t1?{AuB-W>tj;T*8ZF=Sb zrt_lhBLLH@oLTvQsQTa&AkH$yH}&szMCLje9*6>bn;$u;!?Wao`^kr^7ZA_~gH!I; z>JyQ?ZbaQrNT;j=F05y?AI`LA0`5LO%%1^r^K_gL%@1xehd=ygm@c5|+@;`8?XA0bOOW&Cb0Q{Ilz1*o4W zV!6Es;W6iKY^VJ@k2W=Ifu$F&E7F%|>t~)@84L6bE~bwZCJuq#(ta1UoJ|`kb-Fh_ zVtk8n1DeekL8Z*~r)jCNd+JE_Nq06aG=G0e8dny}H!*4J?5t+6AMO1+hsa-h@KCOj z0R2=)9eF_p;0n4U7^z~8BetAj!E?VMe?)TBfyuW+R-?}<(znPpS56&_(T~r2Ufr9X z&~`k_YT6O*evFML?(QXj6?A7I^Nj|X`q3qdT)YG@bwjiyId=T5flYFUztf-)bFQ^% zCqyB@?VIXpGuFmdfnP6>V2^9BaA>z}ypwPfhM{QQmfGj(`;lpI#`9qWmE*N~FU=ht zT*%|B_=pvlk4*iWaHkk#oZuX|`oD>XF3~OzmD0+H>Kh-y_r-_gAD^FgQJmnDCiY1E zE22MwM`@F}JX8#L-pX!3inzZb%C3wn=A~&jpLDE00qT{iv>%cKPP9p zMN@bTcU7{O4_?G2{NWZVjZ6GK6)DquXdd;tM-6)ROZ8uB=gnKZo>!JfB4k=(OC4oU zzjp44KV6KNi!Yi~)iV9GE%8CmjNHYJRgw0|AsA_|hVKj@L>j9r!{T@in5EX+$HxVS~h z!ncI!#$*&U2YzsBI{0FK{~tX^PSySSF%b^ z|9C66_vKjIKJAXWC;2`t)12OevQROo2Nkb2A52hx_y2GRxy(vTOUG&8mJfkAD__(S zAl6g7dWVN=Sfm2peU1mHo#~yd0{Q|0Vcujakj}vBpp!zZvNfet6t|F0czb4!mr``z zxggJ1G8U*Rkzm8K1*E$sS(!*b3?LQCW}`ooI$p#IL`L49LGNe4`5}tsGTrLmzKe}M zj3+~>>3f*vkU=C81C;OY!vIC(pLd{!b1WaFLS;}2yqWVkDbPoWx!M*`w2nA{Gpf43x`_hO(JZ=b+VZ`5OO~@San|R(%n5Zm6$B0x-A!R za4vOeVygUmUAl!~#x$^rp&!cRFp zH)y5gaKzoSVh2Tj1gF}q3&JTkZU70OZL_hL#RjieI<)fx+$Z*K`3TKC+1S`VRU-lK zBz}wXRY9J7F+$c-gg#T;NVBxr_>%Jm+OF-%|A0hE@>0+57qTHh0oJ2(stP|h@a-3F z7RkvuKH);><(WI|R|9t4TOatMgV{CG3fR~^hsK1C34DX0d2;$agjdZ+?a3vs9ep)1 zJ6|e#6qD0%2R~Ur^FNdM)A5BULARZsEVNDw`x@-+Fvxjt=hLgUMVaS+QBt8FFdD}QfI>^3W1ETt5j`M4QTr-RT?xncP2Vbe=k!vi!uD|8l&gC>t6(hD!aH-Elzr#GzJKM9d(syUE8M?lqg|7{2T(Fb{-2fEz> zcPsp@qNg;!rX+E{i3=<0l36ZpiCjp;f{wM8799uc>V)ke$>kQ7?|<;TuMMW)<$_!6 z@Nfx7J>#?jat^bClx%(`29?4X!b)3y{rN)HpV$c-ypiz+>pyBLE`5()L$|@R3`56t z@Vftjor5A5(@r{mGTqe#hBLd=FcZBGq6oy{8oTkeu`ix1>jX2u!qj>!#v4V+{Sz); z_39 zG4|aR^rJ+Z%J&4eQCF;&UW$?ea*r^O)QpT+dn3H|oI4|2fM?+9uDGlKl5KNJerjXu z%v6uwURn0@FMpqD=z+xjL6diOXMx`UnzIF}Zi>M&b`rddFcGaHZ9^MN{XU$_1?1C8s)Z^SLb) zd!?_=zj)QOYoZnjPhgR!c^y2Wsm8_S(rqSI(v=q=7m%>$cC+=xOiAAv@8vg1HkU^j zfvyvhg92nY+*UdQcy){A1gF|yBoXVo-y}>{w3L$NJvymz;t18}dd#hZxoPp9J^7UiJcFx{;U7Xs;48bTP%vY0 zpR4s}QnSU9H5D}M%Iog#jV9as@yRB+VObp=XF*og*E2zOHT_ay<_YsBZzKp+lZx~A zhQ)|qRQECFW8$)S3iiBW_|uJJ@8Z6UP4MQ+)WqNrLC1DSuO$VO(cx1)D+`{1m{%C| z+(PdwUS3RjERLnUPnjpm8xvTt`@;7Jo9THg>Nx5!twu7d>B>R;{^|+$A5Q}CL@tLe zN|;-Mo8P^gJjMJ~UZjI57P_3{m!?K(>6M>PM@>PhN=s(##ID-bT#45sJUl68!vg0% zr$=*~&ktCVTeTt{X|+;JHN}4Rf?J&&L;P^K-^t{bKbtZ2(s)cLU{9u)SjwT-tuMtA zR#XEfhyr7Yz2I$KZ8Tp)rO7rf}2!8pQuZX!}8{7+K;JN?Y z=Q=r1lwUn=FP7K0^OBJ@`QrBe7De*oQgK>zCRl`4wLlmz zte3WCJM@M{(>5(yC<|jH@J`%rP48{S3t?d;395Y4OgmvueD4PyLP0o_;tk&(hl8*OQ4G8vK;?;UR@EJ&)<#7~(G~6cK8C`(t$(<@zh= z%7Kn2H$R_>y^h-K`J?nHredi5sn8~M|5$xXEmmHy0!HAU*MF+hsDGY%sZoS+DRu3i zPr&2g+;Qai&nR6Fp7@ZoOM%yRnPABv5yH-ccQl)TC&&ZGz<=9O=60?$6b zZ=6E#a`Gw&WCp}pKjsJSlj`eogqxI(z7nK@yad^ zGIZ~<5Q=h>%C2)UcP5pcgyOvwJU}@OVo752vo5afR5N9$Sl6KfRc8pYb; zw3L&+@_Hv3NQ&{2@tGipc6qkB0Sk;h0kA- zV0;qH5Qxkx$Yz0v3;HlXAu$N;-(IqH|2j1bwR^!t>z%;0YtS=z9h!Vh?2J#YqMsb$ zDb|`eJ-QTAH%PfEU*0F+FIr~MoN;~5^gx0!jh`g<;}C-BpdmHE+!aNx`QwW>E4i?O zkf4x?HAtFm7_3#UM8@orsBsqT)_cv0g^VWHxz)-ZElFkLuuzoEpHc35xk;REj25MAYwegaU?j5RfQFIeM!a~j4>fOAOz+n z`@_;@~H+_TDzF|Bo&hz%WmmNtu|%>VcNXW2KoNL z(06h1Paj8`kz{^&!GY7vN7SenjN8(y_f0fApSbV@(y>>+)D4^$LHyqAz5p3Hd@Etk z^s|8#ylu3sCds?-!dPd!IG8AZUodKb_k<}nEn|Z`8no=vJeo{l$h`5CY?w9H=E)N# zu8B7$2HKYO@1~7<4NM4NrgKtqGA$+?*{EwS`rpPvYG^9vKBpDtMT!EMozFLv(+KB>tuuNCv;0kwak zzr7MkYj$K_z2xBkR{VW6P>+RuomOsGEsBtI?EomQw;sf_+tyQM^@(GYLDtaATBnj% z-TmrsgZ74nJr&Bmpex_N;Z71_{;Ugo~# zIlh_V7{c#IoXLL*LiKbiewU|YTmKd$$5TG3WIeZ~uq-W&8+@l4{J|DAotDmIkQgOs zy!W$=KARwk&``BCmi?{d5sl!*@AaLBPMYRP0r)y0)v%5cG09eEuDr_{ZfrlZ%5+Vw z)oG95*7fPw0U|^#^6pR8FJ~zh9L`A{hq<4$9jvPQ{b~OW>$e~04fOBVjh&Zr@ax4X zf48y-qvWsCE*>WEIlxOE(~xo~7JSUPt?%NsA|d*TBwyr$x~#l>J? zZOJmG_ZN)#sanO$79XW#zT)@R1#zXHJza*(#ruI(UY%90q3T4m@Wl_bya3Zxpd!+L zJHzgywB5Ixc2$+JZjBs>92lJ27B;V2EdHmg6p6o3Gx7E(8~R)+Qt#bNeyxkQLuKU* zICekQCNh%arT%a0)Ab^n2Tt=VS(5-9q>m8`)-+Y1Xqss7Lv5%R=q0p{)ETr4)Abx{ zlMh%Ce~PDT0O6u)D`^QA-x+Sa3htw_6kO2jwnQeR6(9WF(mcJTS3l}3Pbozn2zI4J zh(F;iFij=%VNgI8vW=YCR=IwfUfX6xvG6PC2f4fO@T`%c zfu-5^(Z_blD`(}-`6SP6bg;3B+?QTc5I;V}Y5xABjMk4x!8B(^X9TJ-Y9VAqX!qh( zKj!b>`Jv39ahm_f(lv(H)ppGjwXtp6wi?@NY&&Ue+g4+QZ8mDGZ$Iz#{Y$3@pDjCU>X~f8Mm@5KI5yK(O zt1tubPM1W3aG^NEDWxke zAa3WO8;{bJn_^8_W1V=7LG7Gb{<71sh1g;G;P%i@J4A5LA-L z*;X+I2bELL2H|1$2(^D`-B9hTh(cC>ggS0{b3JgSYciXJfxX=NGWrM-sVj%oEIHAJ zG_zBS`+w3n#gmWYW6hey$woyRHCw(KU2V|c+TFo(kN!20(nKPQu$5I801r6Rxy-{A zd3yTwQAw*!V&M1aNP@0HHvlFK_06l27sHTUB*-}56LW-}RQZxbS1SsRpkC|OdvaBM z0&bBYM8E{k5uetcE)I)??-yRSJbQoq*qCSO!thgc7=4cmvh#y6977kiWF4-N^~;pV$gK^gK-Z}vryn-dVE8h`mdKy4sUVfovf2KW=QJ4qKw!7gwy$@Jil}vjPa^Lk9Mo#;|V_zmji`YD@AjCF~TxW?Kx$zVTO= z(@?=F;Yk!5L2+^{s!Mr(F+vRxg%e=HM&W{R%~9HM6_KGbGYV4=63!_>k=6?s?vrW4 z(4WxYyU_58VYl@OCaChH5C|Pzq?OkV73gfIK$(o5 zcw~xa!W|o_fCMQ@ z6jFGJ6|E%s7F?9?FA-PehLD^&nL%Kp_Nbrr+LMmF9Ps1T%}VONvB+rGq{}L299BEb zSJVErMkWI8HFf0+LrT14jHlRtayX|o=nkR5{${T~mZ(&|l7ZBiJ#$n=y2$IS zpw30TVWsE}erPe6qhncNX9Aqdtm0t}x*zEQw*i|#dhP~KspM(?g-qUM3B|^GY*U*3O&YX$1>FJ5WMp3)#NheC=(;BLawb#WN1_~ zZLOEMcUS01I)O&G`0mL;^1;@(A#K%OAE9c7-hdrREWdN)w)nDWC-7xTp1iF8lvagq zEQtq=EMiM0`>uWn{nn#m_u+KtU#3DC4PltEW2L5gyq#ox^IqxqtAXgcPCrrp6ddyj zO?8bv^QPXvGsap=!s19m-x;w&2eHnZFz82p86uqyqjD9Ct!a6Vpps$48L`ygNeSaC zO3`K2Sr^mO)~T|&Z0zj|{gZ`y;6!C)7Ogd9 zzB6hhPMM~$-$o4Y=M)r_OKowbqPC+;C#A(!F;EOeiWPN&*~C?<0!ge!7f=DtO;UqK zAh44s=QN?LaPmoLvslaf2ESa1 z(fW#t&K%yivJ+8kuka>H5sRt`RGI=k!LXiHQT?7#uR{pJW8Bot$~zD$G-PKUV3t8W zXffDD7lhHd%VP*b+5FYW0@JYk5-syV>GPNIr6o886zTKf%7B@rFIq?Dap7P3xVk#= z6h>0Un?q%Aep|Pd=Kh{f<;yfzy$D} zW+G8@l&qR?eVn5rin!A%BUb*BR)M0n@Ji?a^&TP~|ANJ|EHY$URI`mW zIgeEu_T3=lXz|aqt(4>J9#vfL8uZp69n4NvXl9e(vo&MUYf)&dw?JY6q&!pLRIi<+ zn29r{6l3Vyx`eXBo9-5U=UVc0HQn(!r9yB15r7+39FZBC|re#A-cCl|{QxjkhXZb;g7H zS)W@5%F2IZyZd-MYIWsB5K6>8904yQ&M2fcV=*ioG#0^8<`_n10ZY_n_=VMDUQ|V= zu8|-%@|F*SSzK-kzK7daLY_HwaJj=jKJnSiQQcqB5CRMV`tB|2>14=J#mQ@7Mro^7 z1M=N|jthbE$(ABzB!tPK!3hHJb{$3Djzqn|#HS!md}Ems{6SNZjar6@jw)%zZ&$l2 z>S-W>bX1eqDpLY=p7bPskqXl!QW13)(x%&6)(q&OaFjb-9JED^*+)fWNVW9j*SR;5 z!qron`)$suOj-zd5XXSaDMARmTPOdf;Hw0l>It0wfZP!C+}CK9atuucoCgvAc)~;p z0HPc719FXwR5Rp03fWYGTOrE_D=pdP?YO|z7i8gl#9~C8OuPiyr6<8S3N|6BqWnc|@u61N{N7O1F z4uxwm8fuW&m9-3hfcQEdj{0t>e84aE1!6RWk*B!SylDUdB&-C?mu$ASt%~1Ug*-v- zOgAhrP9Gy$kif#yFKoy3oNDn`ETjdFYDAF!hH`3x<4ksslSrHxv8V>VYwA?cLx1%f z_TpdOT7)T`XB?)8=s@8vScT}2Wlw|{d2r?+>@SO1yttxjEwU8sy}S|R#Y$}c!pfl) zmw#;g0f3X<)s{vYRj($vS7=Pe>W2u~$h*scerM-^cqR=<7)oS;6Wk#)H?vefy@`~x z51tr3@=#u97ru+T7cN-xz*Byer9YF4+4R!~YW%`yLOrFzc-YeKT{;PpDACn|K=9I? zPgen9{uVnh6Xmg3)FLLSFwYZWsVvjVE2i-=W?-Uk7P4^mtrGnX8#9)s;r%%Jrv6#h zy?Zig|F$U=WC*mDDUfGeNl5S4_O1}jn2G`D>wU$Faz|RZk$?+siaeH*D8}qEQ~%|I z1@_vmzpYP>!}k2w*ZUry{kh|RU5q90?m{6A$}=3HEQVvYuyyR^CjW+GxGeGpF`-9G zAV-frhv{NVc{n+(y6nKK_qL*&H{sBoovtXXvN2HXY5i>)i}J6eqf`1WZX*2=z^{M> z9VjoI2p6ir8)s0vOGn{>0$FYS)8+I-IN*0$iJ6(>$|W&u86%zxY3z2Dfq^ z-c*8J=E|-LX`Cf{6wjU@AKV1B^Qh-UAt_e%QS!dRiP2!x&k^>9x;mPo{RnGPrd+ua zMd2|KBrbux^xikY+P(_VLB1hrhB#qq0&l36hgoSx`^x&%_ zzH1%ElysH1I(@(`IGI9afJkeBITnM%n8pR^FlLvgtaVaMEA451SJx&gsEJ`Vh`%Kk zWfYsSY;I9nz6^YUE&H`!nH&}7U|wsBl&{V=?GoUTuCGN_rahuTbf4oip*F zD3GpyKInOmu_GlA+)A#IlaaJ#a&kjrP*>&T=ix-{H0paQn=If-Gv(OuY;HR%BMUsI z3O+zDSxI>Cjn;@g(f$f8^73rQ85D4fEXD~|!7nD=;TPeq0_;Ir`3f6jag^r=oIgj7%&QClpQGYs(-UIS*Ua6wy%{zyi*PdBMD@>!>B~xXK`< z9c_g9>)d*PrxDC)Oa@K?)=sScJM7JdIgo;UHka(&{LM#-2dDtCBv7Osg1 zsry8Ifs!DiYRCH|<7LQz0GcPNAa@|bexGrJ7By=jm14N6GWzbl4NMSj=`EJ9P`b;P zgc|9du8zVKh!ShOUo2TN7Vui%Jmt|VsV4c)>1^|L&O0e@mI7%e;x0ej!y`#b;mBq{G+^g6q6KNH= zg2{f0!NGb=OPhx$wT}WGVydsKR+9~}opIzw#Dvi}Kjb5~^>tNBy4l7hJJ<4R%~s@Tf2NT5=S6H`y^hZ`4k%zjZdnlCXZtMvo9&e@QvA|$YO zm;;bsWnlRGBDk4QBS}S(?Pcsp=b#21R}Y)VjEkzquy1@48l*!7f+Gqxhl@*)#ZGLY zKYEAkQL-N{ro++7DWwSR1c~MbbUjqwS-RUh@+e`{@oal~)!i$LOqgoDB=VNo@%6<> zOZ3Pz3{5z*l_ijyV;!-`Cx8A!%}+^oA^vPPPa`H!PeE5f%m*orM%>+D$V{>Z!MPKd zPV*SNz04cSD##^R=pWC&n24)9!z%UdiVvV>L07cY=6Nep>`GY;=*qxCYO%qC$|=QE zdX?C(e2ArmsTo}z!-(7_c#i23@{FW#IjSRJC3QxxG7Vh_=*`X9|05#4M^VvHC$xi} zQ#P}fDXeeh4weU7-*~<7NQNk5{j70F@q1vO_dMaA6 zCC5XjI7RhHFWxB}L%~5YHPs^c3 z%82%6TEEVLcW%{Q1iAG16j~xnBx$*Rd(dI#$Z}rwh}Kc@ara@zaZ^zG(3*^Tr`SVl z8}`ouZ3;l7dgTi++xH+ZHEPCket&)!(Z5zvh7v(m61#>?`@S3Mn_m^SD5mWO$=R5Izh)DmN7Nk!2kP`*k!TT zk==n2Vw{2d&}qC3xxd}I#&hc%rnlj9a89pG-tqDM)HihLE`-S|mP|RdRKhy0Zrx;Z z>5S~oxcL91044_N|VNE5jDVIm8aI30R_)ojOOak$OSyXcoQ~>rqyUNQ+i;< z(-3wJHp7G2qXNB#a<7`=B$fkgXuniR^HXq4(V_{uB;BW{YaTL6!#u_>d{m-N6MT1cUtKLuA|IbhgrIQBQDv!SC#E|+Hvxx=C&E$y7QO?si zDUL#2jC`iOp1VJiVrr`qAlzDwn!*~98f}Ut$gND~C z>lY*@=}#^r`7@-7HL>n*%{&- z@%7)v!X3E}vO+jz;16FTk!&C7$Q%d^2$|H>*^qc`QC%n2*gt~w)KD{w6wj0++$b3e zw_QhH=TB*34OR3sCfZGKFS&UI#N)v<{tbd< z0;f{rpWZZdonEczO7$Ag@wp1?WHp+X7-waYTAmHb`A#L5gtQ?GzWjmw^#k%9wQR!I zr{lSD-_z5FRj#fz*hujim*EQLe&XdBRkMc%c3@8Rdwg@8!c3v1!JCP-)+ClRMrsPo z6}&1;tDn0GDVVlaj+I6KTZ?D_F?0%b!XR+<8=Snz0)PBspR=L`DtfT#9j-KeRs*u$zn-Y5Ut z17TwWA>R1Zp)|DNuq8w=P3ENe+6M9V$BLGQ#YDoR<5qsY6m6nhpJdz2Std3DGjHext{UeAlXWD!sUMiHO2azMAnvXb|^2uRvNy6 zC=ZGLpqWd)Y(4|Y(&Sf*z9i%jvRlj=9e^(E#*2jn6u!n(anePR-V3xtat0(G;o;`V zW%*L)K*O*y8P5?reR+LL@z%Q_hxfVe(nS|^Ns_TM# zx9piP1b=Vr%Sg6Uo#+Y~xF0<~&&_5#$Qm;z8sO0TsW&e{>2H`aVxCp5psH1@E>vMb zE240A&FuC1a&v3rkcUhavgu8;tc(n4y*=?T{4l#T+g$wt%qZ12(*{O0IBjfnX)^dyMa0!-mx`TNk$?P9SsM&? z-XPE03o+d6h`%hk)|`d!zm9?O@5BiQT2&*StDFC(PZav5xFvmI98D9AlBq$A)}Kv& z5C7ctcHmxOUnm*vtXoeZTc=sZ>cWL5-GY94PaeT`(I%}Z)uJn5yqno%2-yF>s4C!j z)V%UX1))D4+)_D_OR;}hi#|?RYoD^-7%hO4bSy$;OTyxy?PC6Dw5q`SpVc zXek_$24!_G~X)ia!CA4gU7 zpI|1>QydpMzy|aR5-E-fR|%*j#nNX8u(6r|w*`;~a%e7MFjc; zO|)5um#tf@JFELaJ)PCjlN_36**^UAMUrMsq^q^OlE<4NdK3uiiL@;(wV2tOjr6Qr zMt%#%$Vomzu6_z=WYk? zj7AC~pzj0O663~_c9c{2WWyv$1LiBafi{lHwF_)F`5JWXrofe|VpW2k% zJ}6C$C}>+l67(_eu0-<4s6SEo<{6M+l-Y0?;+)VLUP2N{-~MZlg|xpRlZzn5-QiT~ zRh}e)KO05dldI-G!Cm8OikuE>h;d*;wQ3bU>$Ar%?A}L4H>S42di_()mq5B$S-W@_ zC2S{%{I9lUkQle$E15K77l(?RA?J;lBd7b+BYwLn>)l26q`6v}bqJM)lz(9?#h;Tb z1mui}<<^8{?!yVZe<5cvcXootGitQrkyvN05xctT|M>IaZXcJ_HLp{NGe2%Qdb9Fo zjK!l@2Y76Ga~05ee;qs8a|*qgRQP7kjgY@g*yd#r+sG6J(<~yB2TJj0CWo|ad`NxR z1tSvnUMk*s{7qM_S+JalLHAHgy<3!7eP@pL%(Wz=TY(0TuUmnIrJj@7!rm-{wgq)0 z_5G{nVw&UluB?evL^iBUnmRhJH8P$xcd~a~t%@3jL z_yZ=hP;s3%4)cVaPz=2UCl}e|79`mLlkwV^2DR^F7r`7}Y}SnA%f3mF+SpG6Aw_Wv0I&F!kGyS$6QrZ6%xUxRwEwIou;>Vb)z%*VwaeSwmT9X~& z)KQr^rI!Srq89b}_i$*zWT2TfbE16gZ*!K$jLn9}gKsazNd9wQ%5H%${Enl{t5yue}qqOaD&jy z*g9){c**>@5SBD9Kl_f}rfwig5jGd`?7nkaGMfz323u}2N`1jQ-R7cx0UHk=Um}ND z-OjrBIOI&G1AN;VzEs-}a2~=KDc7^tv>iPQ@c_}(nJ;RG0%DrAFIeo%!K8yz_=Yi2)c+#ZkKkgvF#{D)Qc8L z!kV5GyF86q6`WV99osFPzKeU}*p+57wFatwzWH5SJTs)jy`UQ5BhUKD;QR^vPIHXqMWG#-d^f=LH5`nDOC4cfy}do;$%sTe%w2= z*Qgup%whk&cwXm8aqmSMUyRtjbq5S*z)%gn8F0ATFH7`VvVr@IO5tMW)r_Z=y3#v`;G?_|A^Q3;CHwm8MI5ZXsGdBh|dF?v{GTJle1IWFswDfeapf8zt%8`~~B!<=Y zKOd#bKSheUeoBL!#b2+w03>Ny0A*!s|Tiw1Zb40=1AtD<=j^uyV*YW=o?TVm%B zMuwzci@~s{Z)h5~mr$V=_WT(mPER>kUbpa&d)M`L9q-C3f7w44WeG0NoFw**O+IDx z@tF#WgoDD}zNW6SI$*GRQs}u>^*{hUA(&f!04Z6u$#XC*hbLvL>yP;F(n9jXP=v<( z#VYRck1(Iw)0|=1NURmuF3`-3unFn2p;o3`SVj5xFwCJ@NxgQn6FXa`MYUuEgf*>9 zI}0qR88R~(07JjzQMxse{|!RYX;?Po389akw;NdJm*1BucJXHp=@W5wgJbiVr>WO0 z2q!ZE)LAk{S)eH*Dajapt=W2azle-#lL$1CSjs-gs8yr!SKvORoXX(wln1lF@@P77 zV1o4Kpiz3*D;6#A)X)_H0p6T5_p5me{GJvWn?x*YH{YhILjm|v!E+VI^(1z2PBKVvDf z>87E4Qw~rOJh@~Uw zy&G;!aGi;zx#F62%H+t(o$)CDmaCN-ScM=sxRrMAtHu}dK`F}QOw^q^`=ys@X6)Rp zJYOCse!hp1KT5SxbnWmgZ{x0_L^$)eHYKU*XFg5FP=EYrgVIZAeo1Q4LwZJ0S7pE_MRwJ)a4p}o9?ljGa`#N+(AY`0jP!4ZVM+rLyTG2-$2?E zO0w~h0fI{xI+XgSg0^_cL=j1h;Ls3dxsx%+Hww08RGvo<(uU#@DJ#NK<58~e8;gbW z*#?-(%F(DSfZV65FBX^0H=1CoERbcMxb0sz>@S?FS6KqOUE(3D5AP4-iq7NNeY=J8 z#dA)Ty4Cm971n2WZ^4fczLt7qQAGzKw`$Kt+`qnnFVcsFgDn}$op~zaDND0#@E z<8BPC$=f!!li|1fQLwrxE6x~wkvopo;DCqUfuHkbO$Gga#4a%J;s{aRHO*TA8#POq zWcBD3BO_VJ%0)Uzb>I|Dwm{97IF_P_%IBF<=ZeC^dUvko?L+I zb^efQ1Q*ANVLkm;ilYDBouEv4xkXy93(jqSHh)*aOI8j4gLdnGc7w@l$|zEub>5l9 z&3?PaFJHnPpH7^noOX78lC_;>ZI^!{AjDVtqd|hTZaCGww0{5F7Jlwx?v7YhE+^?c~i25Cl)+S{zEOaYEVUrEdB3z>5#Z$udrM-q@))579PHV& zt}CmQ+IdI}JxwkEqew&o!7<;u;r+qDZ<5H7Gcv=%!1`htvYsTw0#JRJ)KQtL^xrHA z6P61h#|XH*q*3%WUS2mUza%YXU7gajOej}fg-nDD>L^RQtAn7g(n&Lyf5m}Uysjj>4}3P3+Ez_5e^HIDpSol7a>O- zoHeQ0xB>9LNzG-(+l+Ke}6Gqfp{er14s;Lj~A=RMbj+MB2C9z1`TMS?&v{j&dyFf71PwZHZLoH=Z zsC*>}Jyqgv(OU*DSO4~NM$PjodM6g z;U{PvIak%u>Pbz23K-H+nH2gVx}Rcqkb%YU6{-SS|%yuzG_wFJJ2fdcwO zsm7w4O^2UWM$rTTQRfK8qL4Q?sTqV&EjL9!El#INR#yRpqe1Mb;3+=hD&!HR$E@%m zI(Yn|3;Ytz&s_+$&p!WnjH%AM$$m4X8i-takT(NB4 z>dayK&l~RY@ys>#hQJ?Pd}aZ*=)5gId!%_TW?^5T*wid+6M0#Wm!^%EZA=H=~h)sw0FxQ{*iA$WVxw6Gl}DbY}0fYP_}M4u_Qn;ab8ek>N5{WUDC z{eu!474$6E5G3=*)bO*Xz@A@_l{bux*JG|M(N3B;4dWV&Rb*#pwbN^p3A)CP zK$o8(iqz*Wgar!1w5d+})`>NY@OpU2&9ri z0((hZUEN%kU2T29Yc6y1qo^7n4JvOpF6gnQp@QKouTJ(*(RT;Mf4R9jF9Yna|9DaA zN?uO4E6Q{7F9W%BU(Rs95p5E=X2tgyu^!H`Vlm`$>|RsW-lI9x1TWG0k|n1<2PD54 zYmLSD?2=c9^AQ!>SmJ0_=;XcT!qup2LOlAx4c>J->x{9Ns|svob!i z;>-h3S+`XwHYkC&qL!90Qb;+49Dc&xyJwmkQ3#SZr7U11L;^{D(cw=jpF}?4Ayq{A zZpv!K$;{QAKx8h_HkOv&W@vK6(jr8x=o65+>^fW?7y`=U9!DcwdAAbGSUq4h9tQ$R zS`jiy$@@iAn+D8n0a9}74yQPNK!zZUH>p0GC{7M`au_l_F6l^pH8?V#m7~;{f*)`N7vYfh?a6dvJw2o@Ddo z^>vZ^y|vY@r2bDs0}L$J%I7G_SyQImms~yjD9wUjyNSy1h=?vXV^i@DD%;YvyTC%& z>q=Vo!kaw`h3X^)bX0vAwe}Y0t<}%#w0Ab;|U=2KDa&NokluT2%7~67TQL4UWSH zih&M2pif4te^>n~`OE@{K%q!2S9^O$t2{_?tt~E2Y}r4Ip_q!FB|S)boKOHNd%Z)Z zSkPP(%u5(4=Cm_|q8AKeJ*os0z#$q7{;`RCAYeM!{V|O6ACFgEmPsOab^krPF4Utp zUos(RU0Fqp{FuTi4n^pHrrr7c;Jxza*hJUq-fiJyWItC@(t-=Cs_DI92)&rwbnEIxt&Cg~*uwG942N1Jj4x$@Rz+Z;<>hLnfv8KCpi zq$im3VCVz7HH8L6)lwTQ+%EN;aed;2OT)rrSQ?43*>ppRbXPpkP#P6}mwxMK{UWm$ zm1-tV?PU3AxJwSnG-#%N)}*XllzTODf-Qj^)6r5(nA-f$_u=f?t?dJwQ25)oKeqfj zwU>Bb!ow?kZRHcy-}N};4g(Gc7MtXnqnZ3~bfGcr^w;lE7;bf>)qH{H#+K79)zU>T zo;L%)U0skReQrOwrmfMupYqIYoVJ5>tQ_$()=pME7~7Qjio<4ExeoADD__j)y*8L- zZ4g)-8I}-mdxUsx&h-uF9o(Tu4yEg9R~0qkIoNw+;dr;Ai5RxZ_f&(OkfqUOH6X6nh~i={A~6^+#qXgv@;QAe0( zZh~PrsuTA{RT5xcPt%{?HZtOXn|L$D20q zc1%3pV_QL>&ljdoG@+M|ZU~UR_r9I&pHC7{k7oA^#)iix)57$>dnA1Kb44W-Y+;XyS%ncV;K&6eQ;CyDclyTtBe(Jh zp@U$!K3>wF0H`VU9=Fuu4*7mdSymCMu+#u|L;dZg?QSc)-Y%Pd?78>x6XNrM=@TC0 zar_6#X=6wq}zmVU2^B0mD?3D_XD-1fGYUzu+d$w%K%Tw017{gW89 zRr3yX&ZoiVa+GLcVUpML(RE^^D3We&2zfppbU6w09wNK>y7xe7r!HB$MyA}9|a9R34tn!rRUD^^73NXqILb>+joz}2gxKs zPA4s=R9yGb@BFa$1+8y+A_471)4&s^>s@~Jba(Tyb$^Mk6&&RE@BOo<+kBX*C%JP~ zUQ%5h1M}#3;iKH5y9Pq2`@suv$h=J&_y9|5x0EnJiA^uu_xtmy^;+*fNxR{-@1Ufi0V|#4xGaFQ zQ`5Tr%}$=f$1@$)B_HLhwvWRK?NkRhUxO!|}SVz^{a!;Eb9i zwV#Vj^@r7LV4(1L!r^J9`r1VSRZLkODW{p_=YCB@Fs_Q8cg%PKj zIKSA(myG~ksQG#J#iR2MxRai^-mb1b6hV#kCz@Ov@IMrZK5nPGBW0Nc4gMg}gB1#!&=|T`_lKon-Q{)OhMu; z|7)9C8g5#k>b;_lVQLtXjtD{uw+fhVcnq%yt3d>qwvL5Lu?vFK>3hlNrj^u3V5 zu&40t<|joOqa`OVa1i8Y3?u?SJSgDln_b$j7 z^$C_UKtD+Q`y-%cBI0w$06=_d)j7?JFo}HEb@k%eAh)>c+9 z7f_f3$<9hLl=BqQeZBpXD6tYZ76zZHGAApW^U*(lqWD}^8E8D3k2}Qx95~i(@q{SR ze|-f~Kmv(~CrmpYYx`wrS?OSI=&5H1d5!P)4TUkilVNk*I6U#!fm~l!PwFzE^ULvde$UZrhCFO+(Y{|1jW)9uxnHX%L8gJR!*zitlMDF{wY-b#} zggAr1J92u^&R%qa9C0}f=lfCAScvcc&|GJ2l7h{4&+)PcB>*;d+<3RpIUSz+`?0XA z7SsO)!|q~u-|1rG4;D}d{O_^+YvbN$cSY{K>CKZF!GeEqFaG^Tk2&m3^jG-je58Z_ z-TB!{Iyubm$BW?UtKlDA_XvwOu}oJ}@tH}+t4{1mHH7RU&`Ln{bH8|l9yLYX{y554 z#`qD3FA&$ei{kUH*j=bk#MwN`Ce9xOy|5G~yYrj$gE&+7~isP#`n%bzLE+UKX2 z!-*y}Pkrsj<*&gNr#NF~-09Rd;&(Fm=LwG>AUPjss1z#?0Hv9=t-w0p`|-1^YM;ly z0z@X4XX;OT`9Atxgs1k#Dq3Ngt^STmR=xDyp`ndp@NKnKbV=czfnr+y+br06Il>A!Pd4o9?dA7&1yGvfTi;B&G4E z?t32sG89FTg%PrTegqqvn;^)yqqVAQ3xii$5bp!oz7>djX{%=F1B&x^$1x-*ydSJi zDB`3rhrf|W?`ih&m!N^CFWz6@9V1W=SJ#c(+shdp2=t1C-^Wz<8zmq_KAX$r+^$=F zc=!66I+P>foEXG?3AL~d5m?aC6%(ttBZ;LSCPp6?HAc&Sck;)PHy4_ben-g9g9I}O zj5{aP!|~kij9VW%lvx;=b81753PPLS*w`93U+57ZbnD~MYo=M$U56D!ywj5HSM+h& z^80G|_f;1XsO|cVXy5sCbFT375uDrWydrM{dO#az)ACA1}@!b zmzJCoKl_xG{ITVfu#srl@g0Qm^Gke_iQCb@{I9{|8#TZaD5Y41Av5 zF8Cag|EnE){vY$(Rrxos5K_pc!gP)2u9P-CMwpN(Yhq$^w=M%wUVu5&$N8FBCo8+& zFZYNxQ;HJM=TOwt)su{?DUxZv5!MiYR&YICwd5L-(|wW2F0I?iM5<`D!6RteVQEnjaZ( z{3#wXN!p{WgP^$t=E(ZDhc-OK@Im94j?_5U$te9%hJ%9h-^2)pzQ(U=$eEfhGiPU+ zK_Ng$XN$ZF+G9OyW=axsQ>T~!PZ<5%s;$d}4oij_dq(*zwE;e&0Y!R^DR3+RvaGJ^ z3(<^&0o1X?QPTxCA8W5Ke|+D$EylIa^a&FhE~gl|vNYILr2TL4&nxe5kX8fsP452# zFBr1E&O6LYb>EAIssrTYHvT-Cq@<_q${M_Nc2FjbZmOgQ>jW>}eGftg+{QP(4|?U* z^ut`A7dB^}R7ui4zbuGIXQ@A4m_BsZTiL|E3p|Q(Zsib%>_?iSUUfc_Oi-eI-12P) zZ8G}TkxM^WUJ1%gPweq+e^6F7d1fkyF;C>d5u<@y)5)?NCLaHm23G}wfd3IjQRF_aw5a4V7Q&TN%4SYHX2b^sFL%=BZnpc1nPJ)!r}wm9YuNa@x#%V zzXe52z6o@Flo=?q@XjH0D}-Ajog`j1ANv6s?Qi$1!5251@@GcW$i2-+Au%H(lK&OV zy)S5D8yW|{l)bNX-xH_*-2OQ{H2beF05KJyM*&YG3bmvU>wbxLsM_WmOmNn+g-USZ zi(V~`%BIBO-wQ_iL7F?dQwO69m2mT%ILq?C!tDg%9@BC%kOuJn*ydF1+@w(n6Ub*- zbaB9zBjn>J^l`@2B|iwe`B(qf5BFWg?pq2DLi0#B1|H_>(o#x{qC0avRSby2Z<29F zrl#-)pKF@bZ>E6m^xr=vGn>gX+X3_D65B)g{S5()yvCQr-KzI zAN%pu6^|jWdw~yO5Z{kW5j%N=P`rc;W7TiDg62c0Ri)+&zGD*;^zK)i;fqxoD(&J> zT=hCNPvhNBEiV&s@Kza_{pKY{UNwLWGIJCKAo(l*Hnc%;M&RS&nx`q0H5~6NB13<< zN#A}>^Kw}A{MC>+EJQmtZq%fcAVt$=?>E&|mfc_nNEn~*v*OTCz5rSIj{+2yw8Nab z;tF-0zyh^Wm^st&+@Y=?OYoED-o?)DGQw{Sk{sZmvJpU66bp13xy?ZLej|6%e_J88jeVZq$%s+L0RNmHBR!GiAtAHPB z@<$4>M4OF&B^00>(^~PBoX8pA?AoX#xC$oS6nxQSdV{Od}^y!W2DhFh<@ahq+YOMGCUy3S`r!meX?=pYb)`2E}uIix7}%BviD zl93$L2g~WLQeu;}!6q&)a-x748p3|zxF4c2`pq6)-G0~hc33Ph*yAT0Mq+A1pI23_ z*Q#!NjmutmW=kIJxgwho%xF)W*0gMp0-#pS4|{QdQvREu|1bDs-?gg=3iwLo4a?N^ zj~2A^`F7>s>;EzQsUajebXvU{4ulG=JfXm~AyCtroURijFdttbPVlgMfKv$!Kd}BI zP)U)HoOWv=-hXGV$%Sgw?o(-}$Uhmq z>V%EY78li-GbgP%C_|fWtb)?TNCPGFJ-(0?nLHVMzQr$WaPl3CP!<`P+u#sN$lDP} z^xrn)xZn;0;}5fKuUJQ#V!k`KvJ!r{`68!}aiPw>^| z5v?xOl*CeG)8=MOMd46UmL}mx{_l@0WSD0L!-A3QwnX={1AuJNb03aRIu^qrs>>VtsaN94VUVcW*c#P!0zt{2GC_V zFVW=XYH-s%K7}p z#BfI{#vzb4mnuHsv5Fb(WdIh6mCdO&vj`tWySx3ac%yy&u6Y!ZjC*dg`yJRUlTpah zJlo(jG35!+;7M8yu#tv0V8#fLM+RC4Q!lSD)^SViNzD_mfYPH>?hV@Qb1{`#__`ee z0GV6bo%giouZz}v-SQ+7-0}x8$kVCm3sWZd4#c!bH040udk6%7%1v9g|EsXV=k~z1 zu+@s6%$`!(MgwS4Z`>v|BdyBw|Iu`nVO4g`def!S-Cfd+w1hNBBMpMm4bt7+4T6L; zg3{gH-6h>A`7Pe-oZrN?_Y-So?ixV|E5F#eRhZK-``lAsJ!4&^4{_sl&aYf({pU%R z5{nnyA9V4QB+)8$W4nk<{F9yDa}T#vzM<4{(oN6n3YtTv9y^Pxsyn38ag{dDqC-O? z>x-)o{0zTF7YesiAZ_YOKENaA3Uj{1&0Wy_c_5>;4zK;CxH3fJh|GjEqv}7N<4!l@ z5AIBJXhTfc7Pc~)m=)b07$kq52Ehjhom~k(B44@p_R!iMx)RQmYUSHQ{(5?GWx@K~ zVc)r`0L{>8f9xS&zp2r{?~|j0-u1k)+w)5ICI7#Fv2Q^jXwimzJjVk6 z#|j5QD8bHn`m6f1%nO5^a>2Pu!%Y5U$T^-T0H@X&?AJl*E+7DDI1z*4DD}6L8s1Yk zEi4XHEoWC1AB`PPS{_3rjjh~rtUTUr!tKD!UGk|0 zfh$q2?Y$2UOVc?`m*pBK8(S1*@79m|1Dob>lNs#JohyU;k?5(>N@OGF>Y0wB#QxAt zKoPC?Hz*XU&^b(?`X za9GkNqJND6yhdb$#)`rv{Hsm(?atF**HK4Xe8w`8^>;QBy+1S0ZFKDa{ zEsxu(LRFFVSlTRg|8|X4R}`zGOrYYJyFNzP7hv?(yB~)30oqcB??iZ-8ToYdkV8|Z zHp1bTw1;(DICgQiFYxO;yo5_g%$A;5f&{d6iYWJ!W) zPctGEqZ5mK-$+uWDMyV0hl^u-e{$l#N-FZ)Mn*!`^EcCFH5zCEhlanMHZFQs_%R&1 zDeu>iA-~gsqrqB~^Py_r&+%nBCpu}l5k0)$A_HCVda;F9-b#rSsW*Z9ji(>bbS!zz zHt%1&>$%hrP2jesi{}}=D3DP_HzEJEF>qkh68Y}91r^|@q`3M|DAP~}b7$O3PNOh$ zFBad%1Cm2wGTNTrUp~1L1cZcWTd*wBoHJkA1~!buolr>$NZL_H8u4xfP7zz4H&Ld~ zu#f}o+Nm`*zez)t2EIlu3 zn$_pCz+LCTtur|q|H1MGco(Z?(pf!k%q)+;d{?);>zCr{JrsnWWIpY&0KEMnTQ{_% zq7mx(Up&g$_{!tH`|<6g=k4?r&t?Omb$jNg6OCw8R0}9qPc?jr531UqKf_t7N3sfu zLTq?e>=DYFpJ;U4rny{?12v{LS(+XQm>c&Abi60aZij8W3s-8_$s@bW+0q^T>4DNt zF~;Hp#Uy34V5q*1yP$R5KW816$H@INz^~apb!)7Rl-+Fh?&z1QI}?%^VpQ=%U?8PB zKSSL;+bj1=n~#=n?54R|+h1fuileQX%d~G^y;-Nl)d_Pab%lMt2^OZ#3ROg1f#m#5 zl`j~)IUNJw`TTZkn}4)`Z+3=v)xH}x0D?L%n7cetg~GyFp5d;KU{Qsyv6!2-WSkyCFP-bF1t<*PQ`RxZo_?`l_~)2&-11Qi;d%Zkc!#T8^2Sr`L5 z3nF#9l~v-XbiB6J=G<1_*Zup`XltWB^akSXdJXq-`Ti5`cB{22?Er*s_-M!y!#-*p zrvu77!qxhu<(_28{{F;nXXgGaTwkw*WVPNMwY=_*|7F7GJ@E@iAEHUsG<*14Kx7<* zmJ>{2cXeUYdJpC0HEd(M>L0x^F<7p2!}PwBvTk>R@$6Q`yX_yYew1(fcT4X}j`7@0 zIoMrQk^b`RBVygU-CrfAbMfA7-*6w~oF-8MJL=YQWzTjoN0k&pK(Byh+hG4X^TXJk zF-GsaS;ywt2CZ;2VzkiNE9QpF9p06NHzHXledg-#ID!>aH0C#>woe^!lt9} zTABFBNjxOW6`W3@Fn7G%CyR84eg1ao*EH{GI#z*TV=UKtJZI{~OUMVT5Y42IJ z9zx-FnC6<$dCRyG-n|8THP)g2em!U-D_mflbeU_nEmtRX1Kr z>WlAinB?QPNXL1?^bN*WFJ_?wQlfz{U5H4>Z?3mXlZl2-<)Kv-x@78l2XK$ z8LX$@Y&;&Fe>1Jzsm*v&W zsTpBmL{u<6<+UCHBhs_o_qeb%Qf^!;8Ph{nS{!&Dv@j$na5_JoG*x&7aan1$9|q1f zqwoFQJ>FA`e{Vz+Sy=5lahM^6-3Vz@8U5-GP$^c9_lT%Y0a0qx1VKIgse;evg!`@f z-VW&vh3@C|8dLYqU~L)0F}|r$9DKp(Z>IVZ7)$pXn6BzgM@vh&$ag|>RVXc%86>Co zr|@hFTF~zZ3xNTYzH}F0yz8CpP}F#cYoWq@noD5%%^HmR0^-@ilp*s2EL|?UiN+N) z7)rxpXfk*GxyP{vQ@5oU zg))|wbAG*Vk4#{m>3z+!T5#<2hEy;Lfz%UPcUkbiS7~e4u1uKbYpeBVOe(h5Y&7w= z@-$G`+`sPnBf_|7;^`lDAF|i~E*^J$!__S?0!oqf2Jrk;c)O#FwB8~&$q&ImJV}YE zv12f_KWB#BC1$JzVllsd6S;OT+=90Hctg5oaP0+s{=4%ikgc#wEKWe4D(yU@`efCe<=aoywneALn3F>E z_DnLo5y)2B##n1zsGdi-?OT4_`vrA22OY&#ePiH&XaF&w49F(P;~Azp`4z=4mgvJX1t=YLO%ww=SZn;o*{9bz+9yd=?^ z2s^qFgWhM14~sY6x#h|L9U;#(V*R8=J!{wfsu!Fb!NQOD$KJPWIv)R6Y~ME8;7Xt; zKR;-{)ZDte2EtRZKhT*}(g~sJ@5{Wl19go2fHvd8+Zm3;;}YrNI_cf%dC5Y{<9csa z$8ha(VA+jefJMbD!&q$B=>!FWjO=YOpL6ndsAe-aE&Fb>OkEeeR52rv>FL zDiyFC4_n}Yd-_9<9`gCa|@Gz(Ha@J1&76cb>LzcOh zGFmwDz`72u!i>$zn=C{rpbu_~Y`YR3cEF((4SBr~_vux%z!@j=b9cq*nHOILxip5C z(=s^n-$$il+>RJ-*gNzl-obS;g+yje3GJNuG>O> z`I5Ko(JV@{-<8f z52WWRy@C?l8IBmhMZ||I**jt}Aowb<c*eOXB?vb{4B$grVmjyxC*h4(54xG%Xql?xk}- zY+nHChQS%ihik}9T}?|`4T1dPv3(8N8GZWWQ%pB6(BJ5}{&Tvz1rn9K^To&a3og96 z5?Dy6kQp>d{qF`Mk=~HC`Q#O)o$2d8+;!F8CU*3_-gEh54UWD0ft z3fNf6gkfRo4YsE9h9Uf2`x^t^#v3#ad;!T**1rOJKjPBXvMJ{^G}`6rWKbi%wA#3t zs)H)QX|^_9%Q-S=Vbdg`hE0q&^YW9UKlt zxG98+`*GOqo+S4UIIuLA+ka|kY)pON7R>&IhaZm4co44V4b-i_<|m%~K~Dk?LA-r* z&05QJZPL4Xf^IX1&ohQglo3cNA?RJoKmh2Ni=`)I{0r81B|MiUPvNU9^4x});bRb) z{_}1D1gAB93~z`3brF!fW*95~GwS@Znq>ow(Om#x0Vmb!$12iy3bW-u9H_S(0CT*a zd83?d!<=iZ3ZQb34Mdvd!UoP^c8_N%T>CR7>Atsbne(!`hY!BmmHR2vm*wUX{mxg| zpPRbm^biuxa=qhgy1V<7(fD}Uhlkx-p*x-XbbrXP=b9R!e`Jc3zn#d6o0PkfpSfdS zoE=l}4N_?r>7hGoIahS!3~39Zu~MwEW@;WiIUqGz9pT#Neo6zJ*W;aSmi$Nz8+E+6kvo}z$%vYsc* ze|%?ovtDLvpB=M^zJW1&>ey0_Va-;`{7t{;L-g~N_i6O=!RqOd150sl^zT2Lag60X z#)?l+xVqO4TuElbVh)A~@=V$u=uth>DW0pe2xrZtnbFz$7lb;lXP?aj8$^7lHSAoT*XW$$Pc_}cm z6%FvDCKsX{)eavDet$DjixRLa=k4Fj>{aw~LqB%kWz%u(Ph=ofjiY*az@1$?b|?Ms zkU${y!V*ccd7ttbOgP?x^-sS#RzWq(*0Z%sqO?Q#jq7^4E@gXR<+5JxPSVEJdiUGS zBcRQqWH5}LLoT0>$Qjv&fcBNX_f?7hW;qy}UQVkcmz8`H@5f=Qy6e02|Ei5yvR?Vh z;<~7t-V}!CP8+ZTGezg$u}aNC69M6wNJP!TX#mTY2>qY&V`S45Ws& z*538v2K5YaKs2`Dm#s_gK*ijoI$R}T%%*c$J$TlKskoV@zglG9(X{fdn1Jb>H+^Jp z`qmT334GWp#C<^tdOmCD`g>**!IDLf>+3lXzg)qGe&5MS;uvp+FYUJcNq+>?w!NS#C7|G>Rn7=9$B5cM>N`HHm!i7r@-@m zv8yYAK?uskPG+?Y0>0tzw2+AgB)CUbD37VB8N2+_88lyL5gVmw;&+md^7JrsD-w*C z(Jo)8FrEDyc2WniG^6FR{RQ2%CRw3+g9M$*gyv(~#pQL~c zO;d`;si`@>u0W2p2jkF1_YD0|XdyI|r%MvXs86$qg@LCpn=E+vduR*L$VH|d8S;~^5~)J%4J&5IQ!TX>^;M|x229v@DME}PV>8UO z2sdcCq-k`m4am4ny05bTscif0+j%PmH;1_moxsv`V zcZbL1{mtzI)O?k>I!uE`*%SgN&@~bihnl$IP!SLl77}@9xu1OoInh9v zm#jCAp&lGXq!JjvC1_b(GQos^42cx~*uRNN0aCSa!Z)d*RRO{lf}GoQ8AqdfJ#;_4 z%Eg1Jsv_JRB>Je!`l5qm$GnuT2kjECUhp0T{VIuq- zPo4^6u3ifBi)YO%z_Q=bg3nZJFH?@>F;go>n z?oR&Qb=1}u(7+s@a}d_Rpo^a3alCMW54^&NAoqak^eHNOYLoaFH2mM()47wx!r2a& zs85`*aJ@xve5;~-WkA3evfXAG&#KbPb(p5=16a$?5!zg;pT-Hg2M&TamPQ)$waw=P zCB@ZJZ(}u& z#r(^^BC&8KHv2QIT|gGYWKhtTN}DgX5U>a+oZdKA!ij}gPe+`tXgJg&^%eobT{`}^ zU(-a{iTD)@^Tz@lAEM}x+@g_j8HdK-vyZUK`EAx3jQtFe)ZMWM28`8GLLPD}X3m%! zs1ROzopAbjyy(bDM#cepiea)?M6-&o(T#*ZRgnR7rXx_65(n!i+C@QNXV(1Yx+)D# zHUQTzL~`5Aabz=JjtPPU>;>S}Rj0{PN92acFVQ<<+nIdAxhI>w%j;bzY9+QvoGn*y zA$9V2CV%ZhVA%g>h^wrm3?zS?%;i=2o=h%< zOxuY{#ZO_hHY3dv9okR|VDkA;IEiPhGl8{)0J#fU)lvO8!1w9by)e6wprio-WNd-i zn9(IaF%Rp&4sbvsOfT-r#HdcrISn96el+aLayg5c&>M znKpkwkJJue^S37b8j#Amx)7j!q>7WSF+&>kv1FZCDAx@a^;7!n!21*SgQ*<|0NTS# zqQW_(l@p+n{&+)ke?2+Z>+E;ibNrR3R2z!z4@9Jqh6*QECV!gB(ZW6W&IScfx)=2q!9sE&kg%Opx`oh zQ0+Z7mMz@~&-jD4g>*gf;Zz|!V?mE44SBYcVH z<+{>+`xMR6hN);lbp#xCx6z~zSkDj3&n3tiP^lk;6c4%fQ6^7wU$F>X;6)uSm+RdJ z$!ME!B*=R?IXZ7Pp|Et$+>`d8db?G*MFHoQ#>0lj?aA|{E`US4|a8k@Woe@IM~35GXBQ_g!PfIH3A+JY|F?8$4pi&);e$9*LT@b`Bl-!{HJtp7{4 z-JYKPiNxhh+N>$pEA}LTZ}H{lYzRMS(e`iW+@3obj!*Xq7pK_HK%2Y)G_L?!x+gvE zIu<-{64@w#R9a8s93H23whDTnJ>R#(givkqmVf1nl%i|B_|3DbUX=_Gi>AzOeIQvW zG_n9E|7Z(YZ_*)ugR{Sv1SPkEl29$s>V}~I7)aK4hw%|Oxw`yn@r=UK9Og&^bJB!r zDArVITIOY`_3>kbkE@pYc^FQZL2_k$&!07+KbOWGd+!P~EdsQ;;rG zK!9}SVSX7sF4Oba`y4WF#z3IBc61{&bS+geT5lwHU3dPr`RtBxzsi=XAnwTJc;!8x zMN3>{{=8t1IsCl_!8_9gfx$)q;`bzls=~9hQuM~$3-N{ER|MgbE17^D@^!hqL3SqxrL6D z9WviJAFJ1L^*h2Zt|W~na|Ic7AITWM=1r)-FXQ2ovt>(Cn*IKmO(d2S7O1L<;7Rb6J3h^uEiW$ye~te0xA`GV;Mo>Wx13?<$_bRT9K{tEFCYcP z!Qq;CLKz+Wk$Ad6@rKAsCEC00z&HT+vxXxnTY-b2BThAbbx3lpUAFbK*=&;iyn*M0XE; zCNXi^SZL^^28U)qzlj*k*;3qSLlevRwb6PY84XP#(2u`I-vdT08yvDC1Gis&I;5E3Tbv}C3?Q(uyb(#ly1F( z22p1_%b2&6RQny|6i;LC`RI7evv$+Ic?IIKdW}NLH@*v;s zW;tLc?MIZucTdXi5x}pYXUwjE;9Bax93A6+1Vxj?mlQb(ghyw+3k^IS24f{Do}rzF zW?dKyCzF(a2GzWKeumZei|ahT2Dn2t#}?Oie~now^@r^KfCiV(kQeO}zxPmAZ9x)lJxReW5HS+2S=acDjdPdk!{SoQK1IPnIzg5e|kiCz?w%B*g%T^Qj>f_*ffg|R%79M;ppG45I(TIFi8S?+u zUX8>KFJ}IZU3msamokNKkXZd)=mda<4(D$|(`qmOJ({=R$JD-;=hUzGV;@ zBHCN$QZn8uk`sX!sKfmXd&zV4eFJpLik?vxyH+Ke#oArI!)TDn#44sGMVFA;)i53X zSY_Bsi&t}W5fe_hN|m9g9rba`y>*xP3Xr35Yy42wln`dIm>sL{Ew^@KG93N;IiXe-DfMxeEm@v>)=KlHEIgbq`29z$N74>gktSG>qU^{ujd3M{Y^|9fh1t=L~4)m-f z>_&))@Z(1iI*We}8b=HIinGH>Lua|}YpKm$%y2U?1Ky26F{sqs0OJyl8yMAOmMHTGIz~9j5TIV)J zTZrC}Jw(VVQk{VD62EUlnDl7A@Na1T;XmkK^cqsp+^bD@K}4gK(CLXKSO2&{;B*46 z1yw=27qktsksC|~;WAuy-q6KFwKur2f*jtU)G}v|s=ddncTIc#jiNt%+G~*-d zh=_rVR6~j+{oh@9)Qs{9L$0w3g}_H!%d_|p z;}OjWmWO-*ffx?~DsASOT~P1l8RG2(NOn}Obemz=ZCND{YE3?tB+s>_GO3pnX~hl( zf=J{eBE;kz*wGR2!q*7mPs`$j@xP8jlFEa!|B&<*dn|=XGG<9 z+A+~Z?97p`iao|ixUkIgp;_wd`+zTjl*g>tp9cm9i^wt?3Scg|B71W|o!b5EgCnA{ zP>n*x_yqZ~M)?a*J>K*X9xVoUpWH}?PM-Yh9gL_r65CMF?+38(s($4Cyq!z~BSTrE z3j{EjP?CrcX?UdMcnzZu?!n{NptODRJ`Mpiz_)m2Np&thCl+)=r+ zk?{u8#@$j_2i6dcXc?IL6FMw0l%&wU{{1U}_*r)D4g1c<8OY6*P)JFX$p?c!25T`f zzNVarO7*pX_Mm!Bd5j)zf<^xds)&L>Wj}%lCU0zc6&wu`TYAP20~Us@2#x-b*1ppF z5H~ClV@u17Pk9aQZXlh!Xx}}*-hDfguTd|BL3_%Z#}tkw&Ol7Q9>;_lLmGG}fSS%> zQ&M{6hIK6wy6-icvFXv@QSvrKJxES&C_H)MXe_lel)U2rv<85~!E<8YYBTMYdV1F1 zCV=r39Tfdw-Rdve;~)MNy)yBSv@Te>e8J!QO~(2!ao?~Q*@moom+DAiY9yFeX)5V| zhvR{nXB%IBkExCj9E6S>I6gU97>5Yanj4!w>e-zOkcoqsKq;3?9>bXm2nBS8@9r0j zvS37hFRVpHd0%Ud??O!21IZ;>(*}vZ|0zhLxIw)fxEWcNh!T!;vbdDPJ1z-)YafJSm&KdRv3Aeo6sk-ai@00PM69c!s2H@E*f= z?vIy0x0nAq9Z;7e1Qbr-zk2oB5ZpBkWyr4sJx-iFSnd*s7`Uy~4wT?`jqY?+aAfh& zVEveO;}>>&jpU86cf};1ssa!rB3ZyckNH5w$|}=~)*EfsOGk-#au{>M^-gl0Dr#b; z{JZGclV!J{vnA0xd+}P~n>NI_{;6MZ+}PXOs&EM{mi7zF($V9aN5671GDmUqkY2rs zwl#X@D!FP0Nmq0{eeL5&ARwkL^_yc4VOvIT_*tklEK;Gm2uYqLTzV394;`Num@qv$9gB zyIm3S`n zUGYWqRnF&&ZKkq1hc*OENuPPd^@UL1bjT~$yMTaRBnQauP*||0N3T2@nI>k!bcqH9 z=6Du$`@V=Sd9#LUS8M5{8Aubr2!+KZlvf-z;FkS?Z(5n1$fzL}0S5&!HKT`ZhXuPT zvBeW{;TBU;GxM^Z!CE}fT1ZjE<}7Kb{KUHUDy8=OV5w-Elq(Xmsp$~=Hmaz!{Jnp8 zRBz(kbH2u^k}encwXARI&cPla_G`MHn;=rT1Iu}d6Q{tC8>up3>U)P87((InzQ_)8lX|(RBv!%Mxwc6V!r&4je+zi5$2Q%ZL?w^-xT*labP+%P% z{V~*!$!u%)O4rr z8BtfjM7K(dH^I+R9)yA1?5=7ct%B(QG*byGW=%AVa<>LhF+p~$<@5;te z(TzbL5fRl6*td-69w#8q;Po9IP`h(Qfs~rDnrg&8c{nya&ejUiR5YO_ybjj1gU;!` z99(}@N*U6z9+;D+GD#_h`s&R=kMMw=35Z9AeLm2*xL~>Wpic?b6IhuP4gi{&z(6t= zov%OG0!!)wC|}RaPIrhS);5H4#c!cP+8&y4_vC`YToMW%gvuMr6ui>0LT>*GnmaMg zz>&o400SWOm_22+0}05SW>p&9O^CEeOp!w6**_u`6CjZaLnDN~TG(D;3Nu&ZS@cK`Q#tg_q= z+8pG(zB+|B^m+21r!OIonN9AEaTrqhgbp0^0ODp?>bw^7%7VOysfW?WvNagU!R&;^ zSJ?~X6w5mbc!=h2Q(_r>scV;&d!IBFm&1j;mG?fqpGNrK4>qeOHz+Cn%&wSMVj1=K zBQsE=7M$_2&C-cIzT#Q!*%oe9FC#(h%gxdgOW7$Z3Z7;d%{D773efBh?_)fq;I^6R z5}FC4DP6axDs6rb1OtV!X0INmY*Ot{ek+d?yt=pY_u@7Z8cm<)IiGDcNQ z@riY1QS>x*>Iw?pOb>fRe8r&h*i;*l5HsBiH=wy#HlIy{NybTb;112$Dfc~gNI7^zivNGBekK+x3Z2Skv9(07&`hpQfKHk^-uP-CWw=313Kq0Yf(YZD!DW>17f&3sUB@uzcB z1vG`{yq0gQMWyZ7iU(@=tjW@oof}i@O5gWLYG%psQv#dCXCnlVZ>Cui+*=4{Aik=h__XU@l44rxG`Zs{5RUHn zr&hTa`>hIk932zzh+h@sAlvMZp6fT@ziDUNPTl>HEgD$ZVb@`=qf$62oeFycsEy-_ z;pJ#?3gD6mRh9hvkqx9%bJZJL#8t3yABloEW>gM+_zvZWsK~s$>U4r|pP}IN%*;I2 z@%+;QCvmPns{H=3Rd&n&>dTrj0cz{ndFeK{u)v@pL;1IzMv_jPd**{9a7+TB2xb5u zeGY!F3+sjUfpTgg`-e4o357=M?ydieY7OGn^>cpCVIIr%5wy?MT|6wGgwe(m3pm#j zL^CpRg$1f{i601%?Ag2|MPh0%GhI)?A$}A2iFK1S+fmYSq_p5*DZV(Q94JLgzi{eQ zNEMmuX^DNC?uzvKFg)^ero=0CaX#e5jeadf$MxtakS6 z*G-?20Lw(BGZ7pVjI@LOL^KoDVLj;#pXnsO*JL}j_pfx9=K2B`gAX+4 zVUB(loU*JR@;_qXEu{#y-kyq@bl6ikoy`pvSx{B{)N$5|qbmQX!cHIl>ar)2f+pv< zxjzCsUmMN5EJ-@8By5*0M*#GU@<8uTivd;h&rcq@tR!KFJ6?Ci0~iwON}*+th$PS} zcF~8x6UPdOgbxW>iororF(W3d1FEv-eHcPRfxp&<4~u;O0*EQgj!!f}ecZKaCnrB@ zdrUV_-)m$ibEtYEM;Qvu$@OIfvwR`3$YJ7<>FVwE<@G_*F)?1;4&AxiEVe1DMRCx) zWycN3dO7uey3$wD*7T8{GK8~>0-9-1cpFw1XN7YZwLgX=o3v?IW3E>X`Z|Yph8YE` zWBZm#PA$$>+&5iaUX!QF*^Q=yAQf=(aCsf!mB!IxIQ|<+>qOyI6K_>EiN- z5(Fz41Z4TsmjGa8iWu>xI)GFR%ekA2n@i+D8VnDei4yAC5b>Ix#t0 zalkpf;--^bw741#Ky1?;|NW}`toEye-PH|emxGbf%uM1C@mwM_R=#(mYSEf91}fUx zVo>3p&4Q;0&|nXxqk$tdP@iFjLC*CQ$-^MO+Dw14$;4>~(V7C`8sA4dkleaE$IEq{ zI5WRpAd?QmsoDSM3bqnYBgE3$>n$+v@pX`qjba|+eGcWDvN7yYaZzNUjYw#%O)iH2 zPI7YC2CA4ul(vhc^>+#sNteA#rv^{wvS?NVrG>UOv^ysw8S1BpntfUbY8QjucRhiL z`NfLHRidFdE)Z$SP@q?csgjhO*l|qOWj?<;>V*UG&|?9X$RYMbeU80^{l6c2rksN@ zf%Dp6vEcPWbq$`jQY3|DHL6Gy%#!B=i3?RP*L0`QoJBF!&ecZjaSM5Q8eI;$LDPj0 z72=`PN!rYmD%gF6tAa>8ImyvyvRh|p?W~PcI6483^3z0&8f0xM4md8ie1KNwt?>yg zrS~W4VdLc-ij{*ze&~#S?{r;#`0yi*ZmVtMQk*dCYY(u)PS7)v@ES7-pJ-^P<@&I$ zD#?AktgH(SMFOT&S3!rVvpyqDt}(J+RcWn2>wh<|yWC>rsA|~C{kuU-s8SE^mYol7 z*{<%S)2k@%BbdP|Ye)cJunpil>1H1X#)Gzp3!fTM0Q&Hsz;b|FBX48h^r)PKHeyge3vhzx7Q znVPu&4)PNLH$VL#_7%9;MS-^sL=>s?CW@aU8qX;o3>n}6k`-IYDB=S}B-fR;pM{$n zJU}|k#Gv|)T?Ygy7tdy#!JCPEWB${z4LAQpgEv-^oylnUFVoan%aiD1mw_2BcpRb( zzIS15ag{l0DXVUE=zNgMLqVNp z5DCVF2tC7T;eOZN4ptENb?eDc(=_w8)Y^zh6}7-Br(@;(n0UFE6(e5t-UdpzHg-3c zAAc}{9uoWz7yb2Kj~meMWYoQZxGk|Oh|w>Est`>k52i%^Shnx+b}KCwuJ`QHr5>H)NK1#S_ZU+MmBDtcBDYJyDg_U z>S~C6OB9i8ci@|H?fi+iI4J5opjbYM@+FI?H^D$`$X;cSMo0bOK7QA*+-%DMTj6Ap zoMJX(ky2!1i0&(bCTLCKf=56iQAlDsGb`0tX#91^av45tjGdP=f`ms{{~Gm`T^E~3 z7Om!z8~rN_CWqtlTw&&4{m&ZdU<+7E%32f%c-l)NrgJC#vk;`WG}QBFKri$0Eg9yJ z(`LdUQM+2gI9EZAw}0HlG)K%KDN)bd9B*=XW?Gu+azi1KF|wcoe#x{VF<{?Q zhhO-xH6^aB%68vw_(T*eCFxY*{wvHEjtVfO_wd*p0 zd*+h$3)pfZ!&viMp&g&5@CuG4Zxa)bx)>R9o}yk{TDe)HCDqm(+lI1HMF-fja)xq1 znvqz0!+mL@;{LN*lQlYQ`|!h;yjbkjV&A|o8Ok$Uo+@vKlG48cT1><8tXegK{j>;s z$O5^|0qiM2dD`(kueazXpuf?ds9?NaHk_!4gufS&x(?Y!Wi8~M#;7v=_rHk9 z{P=#VyO0YCW95?0z%XQO+od|+kw=E3(!{j(&Lp)OJ6oS3DuI&=V^U>mV@r>?;s>qS zADN2sRm3xi-q6xH7X{CrpP^^ityoN*6V?v-UlS%$A-x2l2|Qg^*PTw3)Zs-8FLPFC z`=5B-G~OQAc)+Fp_Eu0!{3m0D;cZWo&B$!C5+d0VaJ5^dUIVz+X)nwG%}}DjxM|;p zG~}8LJ8mk%NOZ48w$KHxyII#w_&XOm_eu~-`!Lx0NL$|Um*;jgTPMK47OGROi3VF; z^ldrZVp=%6=I?FBhQ~79YIt=&yOOOOJx^moF++TP0K}MMYucZf)#8dco+X+O`2{$% z-pMaNK@6^t#kK?m{6N!hsL<>QbNUZva75qAG~k`dqGM7IYAn^i#w&YUt4!SnzTtX4 zR4>m?Ce^Ilp=#meYGLOrmz(SFjp@%TBjwhv0(!9TYtAm$PU%4N#L88;?Vv?Y1Y!cT z?+_e8$#>!D>7}K=7@oFpxUiaYIN3Ye zj6j8?4z3x>@j?s?l*+M$xg}I9wQ}uyiW3=snim|GE9gdv1DtVUj}u;UuN6)V9o8RZ z_hh#GtGonKtq|j|Y4!J?3ZE(f9&HjuqWm2J(qqP%~K$`A+O@# z1uC-It=Jz>1{@DAXUDhy@@mT(<`N>xsbWzB_&KMR7GdRwzpiu5Z#$xK%#QRsgt6t$ z(LC$gPcHhi%hRDiel3ouJbj1RmcC@o<7RJ51@h`rOm10V^5Q|GjMY=;>?EtuwDy?6vX8u2e~*#9!Y4vU!TRtiJyqBf{JF^l3kU>N50EA0VZdG3h9O5fb~PX``xdp zXaF;YEf5!4Cu8vD?r;6gHR>DXHz_hI83YRF(4;k$vDkP8{P?1ALt&fo;Y^f_ zwF?&_#F)w38*|SI5T%b=G)sQ@C1Vg^srRIj?9L0bc122T*DaAg)^ZGNpK$YGx68g{ z7QwLs%$6J@)ZULo1U^oa0+FoIUqMdJ9=bTFVgc4 z=?43$hMuMmaxy%0!~Slho#LubdCCi*o7bzYW=6fgcqRW~phbaEhJX=Oq zJ6!pI!tdvb(DDVQ1UhiBH&F4r*pOnM@~C4KoBa9d13x2w_gQQ_Xj*%XW9Q7s;7bZ+ z3Pi-rCE9xJMck%#W{Z{Ll-Zo{=ge~9d-w8Up=`|Y-jP(YLfh)%Wbm~!zkwMHUU6%r zBiY2{Vzgdn*B3LhWNX{W)!AOC3PPU0Z`$}``j*QW^>z<;$Kcu@Pz;+|GnsFiRnJ?B ze?fDJPiI73zgdo(es8R@d+6+-2K0B%3KYIRkpYzGi?E_?ZbtG%B-xTuw5$38sgh>N z>Gu0d-o_d{ccCUjcU(*jxnJ^QcSvP_oc6bO64^raLAfR?urD7u=lMfQWWqCwDmJ0i z@~f!6gD5Ns>#;jR@EMkLGj*n8BF)~C#~F#VS-V6tD;LxjA~ZL5; z&VA0_zb$F>)Iu)}2ztNL!10^x&@wG7Rxa!Z*IIrAgCAR|voIYWh8;a#UOufrxP?yx zmMV1TJ-+R~8jCMRfK02GB$#83GE(mhK1JZnCV`tEgEoal(ixs)iPfkw0 zTOT`~wY1m6uc79d?GU!=DRbH9Lvv#%nO%;<$J+QIuF#yfEk_07CRKlcMwx#{TyZ=L ze%9F(sv3kEVHS9*t~ z)PGjhe|txLvVP4vAQ$>PHawgvM@w5K!@eM^>6URGo_KHyvQG4W7MX@MVJ(qU0hzKF1+#hlCkL7Fuvq)IGm@!u?`+@B7z z8?2ia6*cFIC3D3|vkDFk1>k2ch+M!!o5FlO%4gFG>I5mJ<&RM@pJSX!NJs;K*IRON z_?hb52G4PgR+nBw{-Moo3y^wad;l5I3@!BoUa6qA%ee0iX8+u{9GRu7?{w7htfZp= zAqCLyuD#*)8v#n--_B)f+dz1XC}H}%dAW1~u0C?vQhupsa0I#P=h`@(xJ}63;u<48 z0ot*{(}FYC_vq2rv^rcvzpGY! z!l_O0BZ=8Pu8P3-KV>%+&C1=`vsm^v|^wHFV-wg{k~b{QsM*bcjc96nWkao%&WM))Z?`cojF=}uuB zGqx0bkA8ssefe#i)@a1^>gm>n81R124Za61^_iIw&@VwdZtf~>TU{%%iJ?DsH!jyx z;&G5lqGPl;`BL1Fg*UFf*xOG;M@IEnRhL<(x~{4)2ggLygH3oQNdJAFu|C_rk(bZc zROa?Xx&rkFkTTz+!aIp428G2^7Gfun*dOHq4)y7+X(?DgEPN=75K~8_d#+85N8gd8d2vr0K93` z7|ZLmAvEh0Y5|+&hP0=3`1j~Prhd=1tqQgceP6TVUPVyQ{-GRr#irj;-OzY^;91$& ziQT^=K%q4>{-CKpG!C9$3WTv)t_iM*rI>bU^>#sI>Fn%m8>j_ZUImYO$5PMVht-;N zWM05ViW_kAdT{iS0Ig>)J7}sCxj3&thv>R$8%wWQ_m5oh0FIZYgT~aas!e}kPOWE` zlL7z{y$Cn)+Ce4DyfC)l2{m_JIyMzRuXY#GaSRx?ub!K-x2GPy-wJ^V6wxxwl%Z|8 zcWd`%-_5z&CSnVDO(~tkgxUr1_}9jWRQ@&_Cxp5f6Bp#5le>ZvIZarlnLPhSqQmGe8@j} zfwKk|$Kia!tSb*6bO-(VtQ1@OXfD_hQf*vt5%SwW>pe7h;m2Bp@U#sv6TG zx`p~+VW(wff2T~Fr%8wF0gRO4?C%nP4fP*TFRssCA?wwBj+(i>yEXAa>Xj{4$jmns zI`MPac_)NFu>T7Hlo@c+!T!1}Y7p6OKevZ=nYg&10uO>r zVmYfUJ~*FG-v=!Kl~PL|IS=Ge`IAkp<||ei=uvKmsFN87!dpi^67#L#P`J?J{2kD` zop^b>k~Mf_C=C35q*2)a zOL&!>f8QR4HeTSFSFm~&C!_OnW!}eqsp$D@^(6%>Lc(zbS7T?R>)vG5@!t2L`TUi@ z|98S_b>~{xg>V{ETz;>;r_HKJtXBEw{rQO# zRf3#TM_a#STt5&R=nRt3_^`*!xa_9h2`?_Idc}XuWIL53Q#7-(COYUUV{KcogOZ{3 zy)tT}9l``RDd5cRuKSuLC%1EUTF`Jt@nbZ-#{DVL_R<`)`^Uy}t-00s`am?83abBpvNfGh z@Y}I)c9$n&sv0m0muRl*8)|`>g0wW+Ry*6wu{vL0b!mRIcH1%gfg4!4$wE%6+uL=P zm>QPWV)RKc#s6Ip+@soR=46c3)D;n;XWX4MHInuVfBV-7Oi1+#LWFrDeV;$7>|NTqDHIjR3QVy&xm-h7n85kFHGD z?A%caZ}P)uuMZ$JmCZW*@KsX)97;R#l5x$(VC>EHrIk~OoJoPXmG#J2F0?q-3SaNZKB*59W!(s#!uJxGH~&Ip)!} z9sjhS+w;3ci#^!>Uh486b5ndmHseof^S5$0Z2%Oux27vTSoWk}P1!p>kD%@PVGi(m z1A+oDAzQwF4dOKUztwRLAfv18NE>GPZqmCA1niVrNB*6#-_ixLr-|3sk5&G;FoT={ z5>@Hz0Hx~fcM|b(i&c8Eh&wVmjdg@@jvbuddN*Y`(J?W3KP=#(x5JUEP0UUzSPl~C zY*f-5X=&NOZhYy#l^5jPFW1`#07b$#O|BMx9MA5xK?QCz7japb8egbgcr=88Jjxe% zceEeEqHm9HdR?_L^GLpd7ilyKX3A&T*&_8xwkJC*&;i~&2;6|Eg(24_!Ut19z4{~} zDTxsWa@G3``=4Q8UQkG&kA*2?a{9ft=4L@Xr1F>v8HVg+SzK*LNqb`x@7!gHZa&)CEWO5w(b)PAass@o+>*z=kU3Y1DbP*523{3ned}nO7Qz zeYxwJYV91*U4&#HDKyz6c5|Q7Ri#in!us2z7n{R)0C+B-(=z-?0H|dyCavLG_x~g_ zY{5@6bl^aQ+jrW$DI)XFd{2l7=3Rw>DUp}}X>kwRa-`Dd5h4-i1l>Nh8hjCqUkj)X zb90blMmM1101%jO3h&>AmBcW>R4^yxXvu~&1vDgc=gx@RSMay5wk2D^5HguW@Cz@h zC3-Te;}<7Wy1Xz63Eq4+S!HCC5>TiY+RtSr z4Fob7Wi_d(V^D1KVI%t?YK4-ei>r*mbyYb{~7I; z3T^WFnUx~OP&iXM%+9+-{_|3pk1&~y`q|xZ%7i_-dX+S7@Cnq5)zffqhlhkG7Mbq9 z@BRawz}}qlf6GT;w5XT~+(PPp2RDUaBK23?hFVt?W)mP8Eu7i|?7a_1;XoQ_lXCA! zs9Fqfc@Rv`UwIBsBMASw32R>x24GurS)=lPdU8Bz!IN9i_ydp$1HE%*7c2A;QAE!E z?e>V2;S|jRu`UkB7>)nwkim_zzP79=JSM(pVm3y3*z&IFL<_ZxTrIAF8N3D<0OfY( zHMaQeLg4l(v)&m+31(A{ayk9(;bKPe<=$Kjsd|r~Kv7Wiz8NPM7KVu1x>uc#YC9z5 z7BtRS-B}819r({L0|e`$WTs^>!N6Fsl5dh_`%B;Z0x*mUMtvTKi?8AWD;j4kEKwfROqjK58%oF*o+<$jYphV0K{$V$!_+C1C~ z`}Mdp@6f?nT#F1ljKhN_5nvu@wXg-}5`pMraFsgmhB9<{t%To2XavY_0gppL1jgdHa&-e!yzMM~2ChXLv#S2-M*&9#pYyMi#NG10mH4&i-% zO*V_&VHNB@y)C!_WjdClpS|aSCeh1vM@hTAA&(BPFr<26e&Lw*J)qB4lA;!kGeb=F z!ZkCu_&c*Wat=FJN)4PmUJ=(Sgpbniq-n7Ua#F`5W;#<$)<_9S`hh(n%~w9_0zZGI zgulhjWI%OkGc5@Ui=!C-@CioifKnQZ4{5xj>@PYOk>V;+QsE3B; zCOak!o)I=%(}5=?!vnRu59F8O$ssrFOM7P%5#k-$m2iDcZvhmjXmRAy1D=s4j5=FI z(+s%*CBJ6(&H|>Ol08#N+32J~WwJ+ylS0>p%Atc=(MO|BYKA6mzu{ma$I4ocssZL$ z+Yp*iH(B-ZNjK$82~(9})gf;T*sn*I!G!PlSoUiea+%ZQ=JZ!|V!l3K78rr20d^_s z2j8)(IH958gVdu*B$#qNR!uotoOvtf9xX;0Oze{q`uwEv|E4zIea>G5{$t^twE$c7 zE?8|I{Zpxe9%BjH`qdn1jG8V5zx}v(y34A0f&kE+wBx?(jop0;KJI|@o67?|on}P) z?y;tZm83};*SXwN!LrX}j?(%6=$@BX9%I0sOwafM_&?vBev=~6@gJg9^Ak75e3QZg zJ{dom=BfbhO|hL5H(u0M^3wXeAP8_J#?UrowGyJ#;`BM@Yga2mKIVw*(&6<*d0d~m zyLR92X>3djb{!YQeeRX1YL&Olm;!(AoW#{Rd*02h+Htawe6MC{uIH?$AgTN-4txFC zkpBgx!&61Wg09VFn91Y^`$O<@Gd>@(%V3a9d3CwCWkRSgsgMgnCtqq>d$R#|lbnTE z$IX@XeC>oRwflrQ`yS8P8QBnU1Y7St;VFDN{{1?07XTS5=`=BAta1aWUH zPPx_*tf>II|Dl%ck%luq6d0Dh)TM0SEi=>bQLYRwmRC6&)3RcrBoqB-2M}`^TP)z9 zT9;J=o5chzyWOR6R6Wl!Rv*T`HZ^hGphr7_)_jA09#8M*B*!#5;Nm%*{{RC-bNb@7 zPVYOz%+dk);lgBvEMzXA%98n&43Z$?u`hnvLe|mM72`eqldQ4lbAIS{q@yNJBA}Hd z*a-|5Ag=N_??NRoV+OwpminGW6Srz|{S^Q&rp<{Nd3b216mkl3A}ZrJc8EgwGsewh zH_?atT3nACHrNCNf=$`Pkq(n-rLh2_!4w&wc`RiOoaKUg_^rG&wHLEkya4$~{I|}v z{RJPw@ITjsCV1s~7umOmsDa<*MYQS03HE~VG|elpnifG)`&C$vM~Bc9`IkI!9}11gSJEo-wH#y z>!>m)rL50S^7r$<@bS)0FfzoiUueWvIKAvNNIhKc)1M?yr0Ruly^kXsvVOJnsf-2H z|CZR!0*V>}tpA%D7-PEvwp0#^uwXqd8}lnjo9P14|1jA&+c-pFiTm(U*8hj>oJam!|qAfLE1 zqlPF?-%i%{uYWZTT{g~ZX zkzk-U^ZT(L0su|VGf-y?*k$2YH{ul476p>Q`#p7W^~7NTjq8%Y1V+btsd|`->s`f zGf^ezvDMXhC`u9iC^Z)zi2P-_eX5uko5J??L3uFb?-r85a@v;vAFVDOhvu+RKDf1* zS_Gd6;TLyPX91(WhckpYTyg+`5z|aO9^op>4V`-ju>)J?9HBOCACRCw=xxNOeW@HPe zQ2`Vdd|X1ua()Uw(g$sZXpQZszX?<^!pRoSdN^kY{dstibuVvOi&aet>KwwAw}T7XJtOpxP0!^1Rz8_K}1q{PV}0 z<^zZOf=-=`b6|*|rGnr#^yEr@$V1lg4>lz3b7g(G&JxckS>&c(;IYnb1B(_(6A=89 zT$Jwkw$(5B_3Jn&0T}=DxJ6ItNMXf2elh#W_yI0t3oS-|XSz3N_1QQArwOx#a78nA zQN=tjM~(lB2k+XI9n|mexMeTa8_3Doy1)9B4LrqUY>ha=eC1*d*g;q+?WjWruCKQ9 z*KO!Qj0j-ZY4ds@M;BJ;`*(3b5ZgQxrOCX#mNx#~r=s)6QY9`7G_bekh~hG~M0t;$wg#>EYY!D(%=ev4tAX008} ze%^UUZZXmim<)(*i*T!3#blXaoF6nZ?2L(iRq>;e2f+b5QU>YA$1hJAs=!A6Q$}M* zQGGoM5X57gAb!?4Gx!xwDBBe1XF??;K0|wOCng;Dh*!N~H^*}5!lTnr`Px@`fv{Bv z*m&%25FwUf6*Vc56q(j``jOfZ%x;}0fQ|gUbAwO3FaMkK^Raie36F@x0!>FVLo*w>C?KNSSIqT=e zUfUYm)hrp?yzg_T#TGxS=>!Mkv*m1|5?dw{f)7(tz4!qHCBdSE_f%;5JuLEzMkmr$6p3V-RLD(NqvnI~QW18%9;vj&(|1YO(GwU1 zY^I|0(N&dogcP;h@Gq68S-emj!IXExc+14Jqj(vJNb*%Uqi_Bvo$em03r}yaE8bSL zo%i3pXpoQsroGSM0%d++D%CPF;Hmbkd-LBFfC@gZM#kd&zHA@P zPcd#${`U6vH-Ud{9PDqr1id=5=fB!xAjk>LlngF<|6C)>$c!bjD70{vhM1eWPgzML zM4()WVIWYxR_!PtE-m{9q;O=eHfwiNS3bRVCbs zYYIGXMc4^y-o>%z94VjeBt$k~VF)I$201HDW+`=&^sk;#8+iXUZ;<$ZvoQHv(a{(B zVfb9_;G)BP0OZ~eDoBP|Z5Y%?g{`rL+8q&{N>g<9N{MF}(sP42^YWC-N91CyAD=f9 zOr#Qhm6oS}ZU$qE?2m9Ev00KJ*u!*sH6kZj9pC;|14kjkv^T1-e}eef``c&9il7K9 zeAL;#{RJNg>!6$V(IzuR9QMk4mrtOYu+s~S{GGT}soko&ZHO!NR4x7-csslMF3p&R zJTj9oP~+XYQ7iDgqL zj(35gwZh7lG?I$ETF1?v~8EX|$1}!znRwh-zgdn`wcrD0|}ydnyumT5VyvcN)_wOD)~CF@4cY+z2khuK|v=^G2!z^ zlKQC2HVeMkxjB^|N)4qwjg}CFjH4V|$CHws$I{(CPlX&_>wcu$VYin|OoU$Z>#~DY zEg6|HVs*+tOn9-Rj^pCQ{CpdA-JnT4IY{>Y*-o|DZNKOB(TeI;{ zpE^P8sAP**;oDYO2_srw@7T+((ex%^3EPyB3UNPiq1S~!;X2N6Jh;GgYuGt>!q;4y z{^M2tps!@9{3K5C0q#<$a%$q}=u?+lolz4I!Vm^m51c@UGoJ+ckf@7gF=|l~l>%3i zrOaRXcW4wdOVMTWt1qCx6ADYy(5RE=PZgIp3PjDyh>yLCVz|mX3o4FW!#bidw1F=E z5b8hf8}1zmC#X72wic&eIwC%i)tE)}x#+f8I&}rFF!Xa`*oC&QU;KfvwXl7ns*A0S zZ$%X`QMQTI5!hmhc(}`>;=#a3R#N8HlxErP#jky|)-s`BwAwIcYpDtIHSCxvQ%z^k zx>h7R_{mC{BSaA3feasTeqd2MK5sG4@~xxOG`aG6Kay0(1hLVN43u+_=>F3UamIwv z8mYXO7Z)#@YXzraN<9mW9P^h98Ws{3Gal&-d7i%o+3Gv1#=JiX!I@Z|o+PKPcK;Rz zU7wn=$Y}N>Tw44Gapm+%Z04>H^pQ!x-FCa|4+XPU1Jm z9%qhoq(ofi(2ko2yv!zr&NzGBk^e80Gx&*P%`; zpd-0v=&p7Z@PkCA^0V2)JTH&0y!~cJFZZ=cgNRh52#B)fQA#t)`CQxK{P< z$6ZqLiGOu)<&@^Pt>@9duhC*`6c;Drz6pFfWl*R;gd^xKTM?LP3;XEu7YOub92 z6oa3b9TBm~_soW+hgZ_VdRJw7?$&;pon=WvY};n-4kQ8EsBW9Qdz>lPuunO4jfxuF zD0U^g$xY&%2czlOjN7Hp{QSD2YI_!5FlW_h+E@y9zuDCalQyKAN_LnJ@I)1M)H+_C z7?lz!QsD(EryzF~p zee6J$VU|u)(-JBVrfDO-nB98Q+GqE}5+`=eR?yn%cDD_kV_SDOnICNBaabk4o8$ht zR#@m?T(hG2B`}fZ!h-4#ef{1ou1fOl zSGLTCY{L$+rOUo~^3Y3lXC5{=Wa*(vFXUJG49RFdcWNW8F0FFo@r!vUSgeq<<&jaA zH}ZKzAqRqJ%K?ve>w5C&OomT9a0oLvco(s4csduOV|#Czl}3L1Gv%hk@>;!k#BS2z z;sA+XpPIk_(ian(B&0abU~FTODn+LizkQ97Uh`1=c4Zaxn*zxhd&iKY}6%CwRPG5C&;zxQ2)$N(s@? z_Uep9DHiWq&#Ds+5-@_b5=(nhPGG14K0$Fc($A;vjteP#PnJZrX8bnsLrS5#f8|O0 zo#CydP-=2f!?)t7%npD%8$lUkHC$rL@bVXD9yBDy09xrTRCA~iVt|n5*knC!&ip<8 z3rBYI7*K0ZtS?RN5l1o)6RZngs-E7Pq;|(@Ed6- z61unfYNnwu_Mz95{pQ6Cmp{KO!@s($3&EMC z0hn&%!k)!c3#GyYA^5OX8x|TG!mKvVyEnImIP{(B9RJs~p?R8>4LSAPS6@!QU5u1d zPmiC6>!EQg?wc5{Xm_kZo_N-V>*q!dK|mclF;PWJdvL8~*yl7xcR) zcJ|qs678JQ*k}?Mzo%RGuuC~ty`UJbTuH?xpC7-vzag?o;I9lQ{L{fU%J|tYz;G)M6lplXu>D;)q_kOam?!dz*JenS;`~r?ut{2~yOX&3b zSkp=e&bLiW*DkG*9kq+gKK0uXMQ7LtxNo(-D!P)|q1lg3NUW2_k6DvCZ-0ZY zS56uE>5xuyf?V$Ruo4hbON$m3%aC@V^`HszgT0?0WI87o-`d~<;-2#`BW$`nq2{rWly8H3|(p-@0{SoUhpFKOCkkC=4o10y#_+O-&RC zaH3R57x$8X#<}3IVLtxgeKV>QyPC}M&P0C5$}23Kuc|n|=z7$&swm7TxfVTyv`tSb z*0|<-JgFNz(ocbIW+alSy`6hH%{!(DHuD|cs zzJBE{-7o3Ed7 zk-hF)T%vA!`$OG5CV%yc@(Vy!nwE;ItrO%ern?QuQh-c{ZlO+FnUDW)3+X^RP-wcL zl!6R(?!^9`K|ow_lc-2kN--U;0v9bG3M|3&l&cD0>AN{zTH52y>dg&+=NBlp_XlNs z#1HBf)x&Otfqyn=JLj(P|As^ILEGzgT#`aRpEVsvGCb*4o4svt5DV+Ti?|(yC2Pp8 zzfw>@pkbG9CBB2;=J;6K5N^8oyyzM2x$ROv=v_PQE`N|8JYN^xyN|>UBjDq{qTWdo z{>;d_Op!$D_JRFCw|7Lg2|3o!;O?$BJl_Hn*i7~la3_e?k&1JnHFxC556s8?+U5q& z&rirrCg_*AvDn0CgcKE@n49REPj ze|w?-Zs815@WwxEyPMO)? z8mhl2UV?SbdF1k)LvD$<96RJIvJw{=kefNwo2f%!9xQvNiW}<&|5WlXcQiWRZ+E?h zX?EAfn52*=E}8x`F0Hrg`YZsCDOf3>qEK^6;CU{p38@C6S=!sOUu5}nNv=Xl6KH=d3mWSNqKS5q}erGSe2X?8#l zrc{J-FH#*{h_TJJQ&-F4RrfW_h4{P;#Xl=>civ@E4IGQuDGf-gM)mgg_j8r?(|JTb ze?RYAbam2frGA%e>d-I~^Zu(%JROY-Gc0>JSFKIXv|W zYG?ni_~m(u+9|jTG^R-WA|?E^oJUVb%@bb81M>cw6(aN5ZP@@hByb(E3LAzms5gi& zw){!=yO8l_t_Vr?4+o@fgKoc9zTNS6H^==&X=dd7?6``G&kVn86VkFnQzjdgW7|L6 zT+^-pJ`SEWrnm9aOCb>LeaxEncubd+TI*Piu;}0 zF8sr)HC|6@Mhh5qdHg4tLmg>It;T+E%=1_CK#>jnt~=!uMa~7FtCH&c?=N!v9<%B# z_?o+YFD_WQnpv?>P|;nrJ9+bTh_m%8BPNfzZB5SoK;OA6c--%px~uDPgWG0}a2hbh z)HT$3DxJ2G|2>TV>Hb+RT4P-&%O&Kl+WbPTkUhwDGQv?F64hrS6tr^uh|qAO(bjHJ8unOPYB5WyIB zOBguHSqFj~d#`o>Jid%ovwqUPkABAIGk``D9eHd_i&9|jnEf?K_03oDCI)bM8%s@- z_EsMi+mVGQq!gnEsrrO^OEki>*>Rg#qnEq&YmB)iq!>UzC&g2RiAI;jixDuC5M51<9m}h>5tKd3sHL)hYfCr6dmR)s`Kp){_4q__=Bpe9sP zBFwDMAv zm(I$Rtvu;Y;V9->smxlL=z%_z@^*$_A7`E{{`ic!XGtqo(!*a#*6&26rKLI6nC8_` zTgAvikBj^c>b`FIjj9LeAS0r~)z@ncg46mEe(-g;2&d0y2hDuF_6p8=#>rW@i7LUa`Kj&U z5g#a+7dihCkmAWS(?iu<`QT$@M9%fAOgxLei&SS>2G0LLx{K0<)LZI_9w8KO=zUzN-zvNgD6*@T)o^ZvGO zN6v0Aa40h(zQDg|>sOc8yRV)wy+hQ{CQ&G`fv0u#;FD2y(fo5;P0l?moW+Lm&%wKo ziLDa6>WAkP zkHsXLqY|=xs2pu1-ykVTMhdA$F3JN5*HD zcXyOU^3mRg8$ywjylyn^p(xO)T7Ls;fyBi0$m+Pbxob$e{cznlRADryJ@5-YMN3B* z1Z9!=uOcKv3)OL^I3NJlMfRubD7ev?&Wt8DODHQZzae{+gc`u8#Rma^XAo0NZs+t6N#U<5)SIET3?FX96^399EqDp?nGhj$BiG{RcAz{5X3( zMLSgQZVuOs>JWMcq_LT$y)S3B6cFQWS-l3nW<(Z|h~OZG`Bu)At$|O@6O!Z)XxK^! z*xO8mD1nZx<3!d(iPg^s)E%QzYPy;_Ly`?eABrK_StQ7smloHa00g4_MvtW&QWjb8A|`Km{N0K9OXYNJ!7fzN_x zmD%y4Ems6e|2sbazra&oMwzoXbkgZU7>cD&9hHVgqz!WW=@C3<)du71y%IY|=6~kX zC#!P?uZtq>+-8xl)G*oU4N?1tgc5g9(@Ey{glLk{;v`H44Rk9~k-k5h#8xt&GWd{x z+^BCW&sh#pwL(Qja<(Ex`RZ7>!&$^cPtSB9EvFA6cJpSB3aA>gyq9x+f+UsS)b3w}pc-9%}FnSGTVBE3lzTZp)kfM3?%a&78&8Bd~8d*8DEDdNs_9vO$+Stj|d3sw2=DQO#?{PVZ z{)53eq>5kAcRRP3GgqPvsMXc@e7`+{E{*)yCeTR}ljO34uNCllcbb(cf|AQjF zfE7D)REKbjlE^7oW10gmiFjWgFYK%A#KwdxoQ&pu-fFgvVqK2+-@;EJaz-Vj$`}`4 zAV_s`ik>WqOa&Ln-wAj50(3AC@M5Q&N5AoWH%iSb>a*^u$=S*d3%{$&jhS#y7nsLm zH#ac(A=pZVa3q$`?yX$T&HC+(I;x&XGf`uz@&}4?34iR2WHdoUPL;FuI&4rV8xvSc z9h|Ap%x`o(9X$xc2It!A*LduIV3NoF2y+fqQFgF3E_Q$3arc zv9DFvPaK3K7)b8Ir{JW1YjZQaWp|3L*dg{jyEcav5qa{!%kOq0#T>F!*d14_SEM3& zuvEinY#N-Rxn0HMz}hJG=gQa zQ7ptoFbKYlKAH~+y01s3G@sI%eK*F^m`pvpNmuTF$Lot`SQxbgiXpMpIMORvsB;-P zRj4Ku$juqU@&(9`2q8RpkB^85PRHBFr?FGr=I9hgvE0U;9lj&uH5*d4fHXUrbnB(J z1^7J=5AZv_&!weu8EhkmoK;V#hBs1Bkq5IRy2LayMvTQH7pB%7^Wr5`B7vSFa!&4I ze!sU^#6l5@yww_quC+@dJbmjQi!EP#Ug1whnVLgX@(dfI28UknS1m;e;7m=;*(?K{ z+58?bmK2nehIsvRCpvr>N)MY%gpaq{6eZDt$hCx|f^o{ zUBB5gPUlDIeR;C?3QvaMo5Kf5NaA#$%$V=}>Rvcro$cPJgC{j^(K=N?^3q15sDmbgTv{S~j}sNoVyWtfNo0Z6gkg$CKEk(uUBhikKJnPjHd~X{mo<- z{u!U+8r?(`1>2?H@$!=7MlEGyN?#z+qETzP{ZJeH?s0|KnlwtEv}uaGQKffm>RW^X zfBvLw@m3k5dCP)+b+)G|e~H-5yh$979J6-YF8XYet+bCaMvGn7Yx)W%WnX{l*G0d# zHrGqarh0VQ_sq)W zdW8@e#HYXq-}3e(o-!WMjFLjS!pkd{{*&nahy3ALn&tu*w!bPWbGmPDA^`!W&|MxM zc~yEXw~e*%)BSl&s?eGG;vF;|W!0}eArWvrJot{+XeiY*)bnmsd_2_iTg!GsshB~! zP23)a{$e2}RrwZE+0mcOybYp|X=GDYftnaSj_Tu^MYkLrjF6PQrMX$GTI#X6-i`sb zjgBMYPiLZ|gbz_!F~Ti#q{;#va_jk%1c)@Lwe?X_ruyI76fg0lw}XubYI;8e?mQG5 zba$bWSUr2<$l%_u`L@qld+qLTpPu?q@lgsy|KR0S<=+1El@Y<|DT?m|D3{`!9SAVezQ@fFbS*7DLa z{FXNvxlAvXj1%zjvEfdX$1QHX7~)`lIc2~JWpMK5Bh|11l$4}80n2tK9yiO5)D}GJ z9k|I1p{O>lz3j=&>VBAl71oat((0H&R04G;egf?)SMhxRlAyRkqJ#@6#C)n%tkpM@J;% zLX~G?(UO?*EK{+=gfoOvlC{S_Q^miK(6yi=RZ0|L%(xw!-D`L68b*GQ*g904!g!`8 zq99N2T|?;T0INwxoQdQ}fy{XkDc;E;e{6GQ832KbL!v$)Ur=>ta5^4B>hD)K`oQy_mx@m81%eeHBm?OSW>iKrK;NzOCZq4gU_ercc zwIoC%Rky|=PbXsubIT~2%xH?2CDok<9_1vbSwoJn{~;mhBzCIn2ss5QcS5ODRo8oB z!3`vddepyAe{z1gF38q;`d8cy1d1izRv}-;Tl&&68S5@}hBrv{aoyV8&cy>m@mtsG znTNU+Tu7&0R9QN!_hPaeE{9L2fXCY*NU0e@a6fD5aXMIhs>>Gy?~}^5(mAmUNYt^g zjmC-@!~#-syc31LL7*A*9|WFvW%?kzBUY-&`x6;E>fVQ?pSh%GCp;bkyrpF8@B`O8 zA;w|orzrTX+!C)ZJ|s@;OXID9%A%*Bv)#*rG@-$4(Hwp#L-Q&ytxMo_K6y5l`u81g zqB)NDx~=Yc>6T_0z&$RdZF zx@YO^VJ?oPnP`roXqZz2a^FowO-IAWf%jj*7?x>-vf8)3@r9XqZS^5Ci?>Z&++7(j zCf}`&SES7md;|Njc)uJNjd%6=bw7=!&7Ce5#*%-4i^5yEZqs`>?}G(`N=7v=QfY&W3D@r|J8*?*$!@@n`e4UlDR5RQD zv2|X5ily=Pix*Fd`Ul^amzLK9;%tNIn$%*T-gR#cx7@oOvfa7W55N|~X?dtxJsVWx zq%0`-CM@m7Px6`*qpsW~YUuNZ2>sPQPpD5i#`vc?uU(*XVU8;?l(vFfI}f&-|5zqh zB4f2Kg8knri9wp$J@-TY7{tPFV5-|OaG=NUjwJy;oq_r!d0ifg51J>&<|z&6n*Jt& zkXvk}r$oSDDD9z>96_POG!+#;UtKOH?tAxdG|@t=3{ve_N_&FTEpsM{?+Tq4?pU+g z#IR!>YYk3rKg1Y{UzXFc;a$C|*&K$e@}({ZDCr_&K5B!m&GbOc%75JlZv{C+&kBK_&^@& zVI@(u`&4Y?g5h;7OszY(LmWJ_`s}wz*2t~<?0K_O}pd!c?V?y}zL zwfR$qD0TzgN#S(bH1r^ zsXOyDdNimay4iIjWv$J37a)>kNJ(5_MAC-~JD9e>)%i>nRuGncGm-F1zHGoXn(Em+VfF!O-Y1Ec=1@xFN=C9x~OKo~PWa3`aqDFCj zeWSeM4D9FF3N0b4n-#X6mphnn>%^bK)6Fy(=R+1{NA_`oQfzICX99Nh>3NGxb{h-}=5mIWU$!K>SWjO9S1XuiD1}kdU)j ziIRyac~h&}0^A7$3-`!r)2{+l;E!)5n@cUOCF|VU<4)&upQ9*UtJ4S7Cp*j^Ry1cz z*qmRiv9Q}}EYIPX5+wlH!O-|b?;UJl=+{c#MIo0z}Ln&V!4#4 z<1|HbHu9fF+SU7$ukeV7NalMH`600ISLg9vHI87zEL#A1a+b7A_-oPOjJ22&O?JIx zuG`jG84iZj6v`^fnvb`}4h^B4Y0Er}q0BMhPIf;21s=-C%`4pe;db=%_4tFxN-`JR zQEtxoR1eJ1MQ)Erxqoqedr(Zhc^E3GjF%+MX4Uibg~!m)*x8gKgAjKpF!_mgv1#RM zP&Dnb{KwgV^MS$t&4Rn_fsB%iF5${744VL>yQ>qD(Fecsj%LH+f%#iJ$=Le>SMcsE z+P(UlfGr$VI64It%`eTaN@*O}K@?(9ODQ5G)jeRx!oRkIlk(zK*l0KXx45z~hL_T} zU1n$yv)RVj=;J|~X8K*@gTH&cAkbEm*Rw$ifZW>3jW8Ug00XJcePyMk=JV(xlnoG) z8P{t)ybt5&P}XgCUjs}zi)+m!!a@@=7t?}Q?M9;+x%sQIc>=w(zfG7q`c35bg#)sj z!^ZNGL>%It&9^6#!}ygqw4lLgVoSxr8J*ao!4t{~lz(v4NMMXH%|mwW-0j-3#jBp1 zc*>zh%J8z;Vh;Z$C_JQ%Y>{^O1lh46lP6|%&x<~C|K~mCtQ&x&TUv@tpYXM`A7w?V z8LrV#DOCwCGWpV$wf*7niRlZWWSM=W(>FVwCPTikQyZ>`LyzEmv}J+hi;ibbI-2w} zbO%YcmCuT|k+Z)wBN32rR~#CN?Sja6ehEfAj|8wWc4pJM!w0Pq{TwBI>p#5RKMGao zziGSV)K;;dHjW(o%*B*ln|VVwaq-V9l56%Chv*|S6-TcM zLrO@94*sKDQ6j~6iuR8CFIl)PW3sxSXrB)D(FOKO409IcQkwD2kP%p<@ziuLz)1>Oi(;gP+%HGP~#$c=# zu9rVhmDu8joKhEC+6?9RQhDH!9F*lvs@vmSY05L@E(JftN52&N!4l={~CE)$iswT;IP3KTd`@m`=X)HWDR5W6nwYDZiLS4(}FKk z;-9L33yvCy_;9Sd+650-;*&r{ql8D<4B5H->Hu#`>v}^>?{mr|L@hn5e+(*T50$D? zd7e-EEVYBFNo_uwNuybeRFr?Ww*H-e!n1HL5U#|`;c{Xb`MdIjCNK0LlyKqZ~#dJnt2-nUwO>W!fl081UK=|3_C(*{7K^j@VBB-JxO=`ofVw!bGw$&o zH*9C3vgv!#I@?lOB?Ppay^aEl1n=fJHRk59u}Q~jEzq$xnNvYpOuOq{9g5#|`HsJr zF&WfHT>ewJ=z37f^}GN0_w;VhoVw=6v055ppZ)gk?l^XQrTIWqO7<5Yz~zDDVXZD? zZjUY5A7CQ9IF=RswX7es8cr7Q zf*EaYa+Huimg`1SK&PT+FvctL5=4ifKp>FVQg@zYoJ~P15IqomaC6~9rn35*R){ax zCv{Cy^ruKI)6JyvyVy5Bm?tkGoqDGa!onL&&6YgifJb)k!bty)>t>%wpMHAL9~#t* zq`HIH%EFSeUfk2$J%W$juhgG^?AGj$dOqZMG^-fM$;n~y8gxaJ4k4s&_`$PwK62o4 z-~FL=+kJziwWef$*g7uTjRa|3Us8?oxt|CE{L-2;r1DSE=bM*0=N<)Vzzd=hehL;L(a?+!n1XGp!$z@y?s}>gqTBhR+6Z0_S1PpJSkv8m z7}`EAAw8VeHnLjG?RbPdJIz<<-h#Dd)YPESg>Dhx<8u2KQ#d-^+Xr|b0G!n<^4WE@; za*V{m+oW?(#^2l3Szisu889)1w)t?Po93$CMnGmf7h!@zG?nJy%>HsaYI8zN zG&DN?Uukvr>MEk&v6UWAhBll!pj@C*E3Q8uYvt<=^N%Xc|gJ906plyMHPl_0;FQs=QFPz z(*gn^7lI)2>-fp8;^K8~z*QTCaIf`;r;0IoUpVS^NnSt=uxG2;A0s-hUKUhXC=CH(QajJAt zJt0jT9OT<*x7w3fGKppctFHrFp!VaV7)GdV*#w;9sa z`VIkmDQh{eU7vi3#mi8Sgj7h~cMz%nbG~0FNuaIOs-x%p%N0-R3Ko^qv?ZtO z4V-x$b2XdUv&HSjbdIu!XYfPkW8~kz3}JV}7NTHZG*Z67f~*UuVV*`ZA@BOw;LeuF zc#LG5`m0c`Ge{tYJf`Y03}4e|59yEUws=rLJr*)74W+IGI0NXX#dR9lPC%z+y# z3eQKEwk@&ws}5s!4qp|c1I3fcHy336_orIa01BAd5`ErLRI3X1b_Qz1{NX3fIqyOw z#jmz&ll|ry@elMEE4QjAKb>R7;&+D=O=QFnBe?Ms+Qrk^-7#W6XE;85rc7GGUeV;cp zz}xIL-bWj>L^wlA@4lx*hQx>|T#57|+^bydj{eHja#&MlX6G-?6&WxnE(rz3mX7l) zMAW2;48LZrig_aaV=xLae}XD+D!HpF{l z2|SOr>O*H?Su}SOl#C1=Gh(y8$E<+zwH#|Yi@I~gE3Px`(COyLob>eo0hIX|(Qb@Q zwfnJM4SuK6aJx*4;`j-ZH=Xn_LQI=#WU)O zN)!1rNLQdfvL@K_bFRW5xAh!p<7E|+U5Ougo1D(+%trVTro~5Mv zdLPi~zs|Cc7F<> zQ?b_S`Qbmurn^|F>(&zIcR%@cEWSWDp}WVC{R)F8xHwc)9iaEt9*EgBh zFd(PyFggN^H;T%;CJMx1QtwOQT>oc6IR=}Ec*MgV_zREGS1i#=t*b{fMz6>1{ZC)K zvB;E}qaE7=V{SEa1McY!z4kSM#Y$ggoG=y*Tu}j7!xZ32$fzwmSTlQwhRnkwiHMewH<_CtbCV{%k%8 zmyy&0k_+Q`%*CYVnX?*%_LHx!>UULDYIPR`Q#X~6&m4-AH7|^56c!fta`j|BBB;A` zy0F>wU|)v`Z)8O7>A1;_4wsU%MA_uT{2}Kn{4L|3{QTUeO|Lwu;K*FQj5Y63MNcoU z0uB17=h13wyE*lxl7x>p)q{O~!F5)Xm&aB{rm56nXlJ9$yvtc`U&Kwn@}{l_pic@y z1XH;0s0SslwqPDGNpOb&=6W=o%vV&>0#`SKkjbF=XTlcwMeFNlL+>Z!3{9>vX*7i{JexfOlZh`S-Iw#DOWm zoNzR)`cHdn+~bkcMH|>KEoi0@sCEP!UHIZFbcQ!zq_5`wwphigOZtL(6G%4~P^c9DY;|HCqrEX~J0Djb?+c+7ZiIc@1 zbO}YC9lSr%Uy9auFxYYuX-0nME1e@MRv;MVcCFe2G--$3`BVVGz~Q~e1`PxKc-=;J z{@0F9)5cOEXYblC1?i)!rP6NE;zqrX{r%V|z|_Ff43o6D*(mNSL&Yux4`YMaAtQt@x2KMP_&H`Ofm|y7lhV&FGGGo& z2o%3;SU6*y{$-&dGDxb@@}OR}{dEG~y?VUGlmVb!C^@M@-H3S#txsle>v3#OPXLFY zhqI~{_xl*MMZ`Kwh5e?@1@h6x;e*i7j$UatVyZ|MRz(AH9s7Jy}Aw;Bs?HJD-;giF=D z&Nb}WeA@l+WPWxJ{)^-1I_(1Bk>iSQZD_-$#IBQB+}I)_M#5eYngz$T-sJ*Q7Y_BI z5t+lwUuG5Gnn=y{d)zlW{rD?vHmFJjWWQ&8;=3#vzj99d7AIF^{#LJQz*9hW+IUX$ zc`h$I1_xMv;n?wxxL?#bA1c%iv~;E$mNalAFFIfGQa9eobt+mh8?;6nt`1f7lj+<$ zhegOB5%#oIqW{v5^GG-9Ey;QwHvOy~{F^qM?8A3&|FXY6+B|$@h&$_#w{eJDvgekO zA&T(a5ooWMt2)8%bDqcWriJAw_W2^v`s4)pb|1Ny5h^ZozO^wKaZ5wP5FsvyDm&wt z&Wy)`p#aQGY-6e>=c(*iRY(?UQG`-EjYi-|6~kWxes!j0)wN!%^uLK8Hhgn-!!j|* z&SXs^@P`Lz+0rW>=Cxc25T-=<2ttKXqvEhlg~Mmm5QETN!z^xQu7own9{UbD%o}Kh zg$8F>`<^<|@$tLK$3NhBBL?eAWeG+!gv;q0DkEH(+B$#sIuNOuTTC1mYedd?+*khX zy<39tY%^!W4R9WC9UtVKkiNfyFiuXtM79Mu3Ede$ZT;p+J5X+XlPMebDz{0}?PiFv zjNCZ0)a-{EL+IxrHU7GVh^Eb;S3iT|lr{d@dC-j@I4L*-?6PVFum#af2 z%2&JYUgZq5Jgx-Po{yFyw4*1>9-U$J2xM@@@qbJ<(Ybhd3Q9MF=FIqcxjJS4c16}T zAQ^6)isi^sv(*x7OXN!6h^}W}ULQW7fJvh;PyK#O)!|Q*#8`-7T#jT&%%9+^&gWUl zW-8b_HO!Audf}k^{fjf>meZhAkhT6}BtvC;JdEHBUqH?lUi}NI@K1MLn~xOi-!0gJ zwgq_9 z#P!W463=~-KO@L=%#4K#bQmP~%40{PUet6^`|-lE-(ilDIc8n`GCapg#{A4+C}6R^ z@>xcQDCgO#hPPP}A+Qlvg$xXzxVK?WQ`Y*k5j1I1#(ZIgV#*S%fA_uJiU zK_e+kQyh;q2)Ri+Bj-79T>F8y=l{!Et7!lx5$57hhF8u*J&?d$ThJ!p%gmV z*d6j3r#~#?<_~bLM$3_hz0x zKI_(a(?*e@<8LbmJb7@(s(mLtAch5z`h`f7`DFt`wIT2j*G)gI&f_wW*5&=GqwBo^ zW+dV|&>W_?S~u0b@9q$UoVg}z%+))gUZe;9bn|<7EM)XN`nCT_&>P8!72QG4mx}N^ zZ~=ga0HP^XNEoc^c*Ek15M%IuykLJJBia6A26718v-6~ohg9RreJNhxoJ>dt3=z$>D6RUQ-0_ZJ$dAfv*cCzA zaEm0Dym4StWt2ub!ro*8u9E>@L8Z9y5$)aavJWS&b z_^rm!>@uiPtnh%}@glXWacv_LuFRv^YSu+V zA^b|IzGY6U$3uK0s$mW)IP|U0@RC^B6nyl*;4i-Qx=wZ(lN7EO=-zL={#VhSb+$c0 zq{-BDH5J_-E|?6yl&mVku>$s zDc5HQv$rqXs@-wDF&IMXj}yFc!ghFiI5|2ge1Ml>q^C7;*R-Mdy@p#x<*&wC(;iP6 z3oUL+%9_g}46}I2CzO2IV_hDK)q%8KwZD`;P1SOfN}SC?PA$80jBmKx2G(x)M9gLkHG0vpUWxeHw7FRG zQd9CPlLt&yXy&;+yssrzgI%%tFVccBuivg7i5@yw-1Dvl^z998oQ|H^Y?@h~j6sqkR!~^&r^(IxsifDm>?za9}OYj~iahPApPn^>0VF09Oa4_1{8V?BnRUbpP zQ-pqvb9g0y{qnC zGnyDi0`=)&n;K5U?Sm)=>_Ef^qtCw3VOmLNmtYl8$noht4K~9t$ z1;^EN^}jO+X3afx?FfvU9eQr$BU(V^Pw5d1Xb@}NDgJJ#xPj-pp3UuN;j#&nQN9GC za(2(q!V|6T^1iKQl7A)nZWTU}Qn_INm`Qhot;4!p&&6EwwasXz0roJsw5if-+JNCE zrFHLsrP{oIWL4s+V=rn+-GmgGJ9^mK5J!rcSvd?t^7QI9AyL1=U(5KU6bTGaQN%dD zwCI!)75OEPwA*?@tWGp3A6k4}{PI$-km|>(7dz@wIDx8)U{cLW?O!J)hFCtOzE7&m zq*j3uK2f}~4AVYj(YRJdV<)=WV7!K(F@IKQNWs&Wzxvd{cs_68aDp}(DH73cJo()P zqMNH@fy*hx-kW#i=25dRn0)%NHfRj?^TUqxxbx5<^!_lyn81cqP!av@;wtX>N z5&il*=vG%bKk_8*ZO)xRn;E309oNen^OGI@cjQ>VfnQ*)BWc0j8e(s4IHuss0p2ur z_Zly+Yjbldz;A>+TkAG7MToL;Z?4VAoz3MggWej9us)HBH;Hj#N}PK47q+a;N082?uCbcZL~;SW0Y7{m%hBwFvq~5QlUl}B9fbdWTg-a z9E?rF%6WIa5D^ja^ywY!qd*g*3R$Zh04FgjYfUK9zIOrbjY%u%VwIKt&+ZA>^qJCQilcL_#w5_b6cNDK!Si6+ zf+dL>os4hfKZo(ex%_GG%P}+bq+c7(PbbgMa1O1m^q@GskNZL7E`7bW8T`w9-&dDP zt%hIY|CK0Wjb!U;H)?!1jfnQ#bp+o$D8DUVdl(v$GLp3}?)$ww1o%#51^86&=gP|} z?ze!*oKFom>O9Cin7)P8WDsAYZ!LpJ-RTMVJRcF7HtS-Z^=xZf0mG@oT%8N8hPMB{ zzkSzGi3`ygR(0=y7?0z7`;FQiYHaLU%Pr>p>$1(`e_aEEV^h1Pfh@3pzjVO+pM7wU zVTaCDH&$9?2ATggA>jY&AALzBL=|jB7O+(Lbi0+`w^q9+c8j`j=pm|tz$*9@A>TQ^ zQi85(WUF}jaCv(WT9VqP>9AqW;k^3FxZGw&426`ENz=66j{vU(9%my`T=RdIyT7P6 z3?_Fv=e}vSX`L4>@;cu>&fk8K-%U`|>iZOG1!aQVWjvqBEci56H#7(*YCLuMOV9~I znYK1nviUm5$l8H|$Jm9PmfdrzEMmU)*yjcscL5nuFlHHmprixw|DFuK}2x2=2b0E6NTY{gOpkxsD zFY12%KIY)UrQ3PpPpq6y7Y6jtVf#4$zx10S+W?aGZSw3I^J5UmD;tW=*-=+#3FrRu*7H#4U$oMzdKbk9w0R$-*9U1XryU<|#;a~3pnYpg z(J8``Z~}e^9iCRCC_r;r#yz5w`*oX#fb|%E1CCnAhQ|^H_jJVpV-~Mh-eZZP@hXw^ zEv*04$p-Doz|dP&(`5=H(L2%hR-Px};!uJDoN0!L3x^#*IFAcxO%9V1%J(-6nefDO zlfjCWUQm6S#g>Vkr+Y0#B}UX?0J>1ubP2nhU7kbx zHqK;s+C?dOewLR#g4Swp>81Ft#NE|pHD2O}d=IOKuRINl`Z8h+KH6`j7;%yfy^PZ5 z7geR#meBNyuP?p!USWun)rIX<+<_M@j4t#MP0B&c!X8+TWFkK{{N1Y?!Us(A>nlDP zk>y1jE&`!hH&iS)mn07buz4!bD%V@+nb?YmhB|GpSUg?Fvm!u&W5OGi1Q{k$tE%g- znz-1>`{cx!eQMCw){ak0E76A_SVn&g-q>1ro+9M$lOC#FyMB7BevDXI;Rkw5_#LVa z92smGYll=oazP3$P=AqLy|Ss%v9`85V|!{M1mbOQkJGf-oN0|QN5+)(+U_49$mi!@ zvBqY!K70QlC3SfHJbr%sgA{)Xcx%bRK_P`bXG1lx=`@!-|rt&iK(;J7DDm z*y%Cz>PTZqeynA3yTW+?0D$&te{nqqJ%9`(Z3VNWO*GLISpd-bHQTWc-)A#%m7hx$ z>h#6wohrW0q;D|Q44XD)H ztL#d5vwo+GPQMy`zvua;a?gtaZT*Yo#}J8JpBIxf+k7K`NRD)#cc&yeHYw&XBT!Z> z_)lci9X4+_`jmb@Q3pJUat+>ojz%3FMzc80lE~BB&JfaRGH3~UR?FvzagO03KL|G` zGV3=&X{kpM=IzAaRSv^q5615=5aTz`mx#PKrh;<)KSU3=(z5x_ z;5g`i0qr*!`9WYNyKeQ$-KjpOYUg>)RdRo~|3=az^oS#UfBsd)hr{U>mekA+z;w|7JPT<`;hDZ#2oVMVm>MQC*mT8Y$>S?#8>6HhFAWOC;fOv z6jZuL{i^~8Q0X#09-{iGCX?;;h9k48r%*Hv11UL@sVp2(}hNtn#R z1(t3dVA30GjCy^oVv;}e_lFzFoK4;ZJ)hC)x;AI?2WJ4`-10uoWZ7!fOU)mPBsQBo zY7^^%hH)jnY2lMN*Zj?nDE1){cg0%%M^*FV<{yLR;p$z7woLQ*mm^)DLMPCFj*yWZ z_Aap2$Z*7atqs^lgJzBE*`7l0h8oFY(%Lsfzxjj_wKr=wqjDl?4NYQ#O=1y=V>sl; zZ}@1XF4d+=Lgq;9S~Ug>H!0hDvFP6p&*x4QE3n|U<{$X+P-X3&6!F^^`I|n6d0t&K ze8lPCOdD|xXa8+UCVPwXuv@8_e~DIH^A{)hBaaTp5e-;^KI#!J%(RTv(JK$gUi`9s z>Kh(6Jv-Yx^PGx!kq_@*WIKAs=3m>ki!>=Ri0Hk*HbkS7#=p$DgIzvR1a}73|AlJqoX+v$xqr`iHS6Zj zs>wr~p!l3BH~$|-p~KmctX>y*up^+KX@+KGZZ3r z2I6UwhC`;UT?Djv2vF#fN1`v#>ju7>nH8>jqXU?jNuK9*I^$~PJ98h+Ru(@t<7lmd zy*&xwD1bjs0>Eob+=A7IP{V5JGPQ5Fj!U|ovek=)8Vsy@j%GlFojYAWFb2$e;0mYh zU9<0jAfq#8+2m$Na-}#9zREkGY{fEcy-b@DOn7){iQfe&Ct_^I0v)&}heRq+TwTDs z7$p_e(XZJKb#o6{S^BId2hi#&peFKZk<`=D)U0#9(ZWIct-F*}l~$OaHs%0yksq>;hYfaVjp}5WqCSAb zZ%;A~Q{@|m%}s&-0|db=;3|3U;sQ7wuKT?2%^lbMp{7a5+{u*(?6su70P7lR*j3G~ zEZk4>4SwXOp9{EegTd0kSeS#*CBSqn*V+pZHG~0QDllIH7aTaQ-(D=!PW@HlG)RxO<945DY zPwIP69mI2_^B;dNyrS19dtVBvx==9BSy5G0?r^a8R?(>^=Cn1}O%s?$#lUcUF8|{X zFreiuf4M?DdGJRr2#PrElA80L^|XW8e{UuJ`IB9qeGv8F&IsN4c8<^6s-N9Ql!GDk z6zo}58yNo9lH&(b!X_o1s4WwUSbN~V^QlC1qNsZhV^PLfcoh4cTz4wmyzZYh z2PzyDIa`4b^@H+|=yd0sDY0+c=_kf6At*~KIt_!KhZuE7$ubPY+$*fi(hIa|#@Ab| zzi|%v6HM2?|D|(KKsDIyv5)IZL|W*W2gbuV82c#_^cdQtS_==9n-kx`adzolKdNNQ z72QeO%OrG&gDti zC+%W}#R5%wK=9w-jZHjMBzI{N(+*ceSD+QeMKJz}Doi<~W{s*VE*wj6P7XE&?=^xp z10qR}M+jF0SLgEAK2Z}0S{AHTB~p%`g_n{R;WA2}j@bbZ+*(I2-W0{cr5#BTXLR1@ zN@fQbEzkaQI5Ke zrT(iKZ{K=3;g(Kxe}3-7nh)w%Cdp#!dI&iG5@x)FI6UB5Z10yG^#o?+LVg}R; z_wF_;gSsaj&g4Tj2bkBs^-vDSc%OKj#8zG?e_V(5ycn6ll+76!EVO99g0Q=oz3K`w z9Tx<*NB6vA)y#&n{D@s-d8yv+3SE18Cx#V+65EPL&7xyuqy=<(=|`n#)kzi^ByIOj z_h)=Se_2-P*D-ko!2Oj!e`FV4xgRV-CcJd%eo9^ zeGj0;DI1?{*JMyhEo*vs(UC3W6L+CY_#@i&d~1aiq@2Ci-P70KF9A4w9k}ORY(Qvx z#KZut-mymB!XocKH;57!5w}+hpM&!cAfnl=wWCH4e9h~HZbE#s{3PTaRD10*qprS& zRRTDt0FTb+b?`~|n7)5dqN#mB&@jaUo(^*6o^={j?TM=>0q5NjC={-ofPpJz{4-BK zucp2|ifqB2f`=*nt;5C6AAZ?%=SAM^m!kKNjKb+6l%g|NTik))-zQBGM=krdP21}k zylG%pQ}R4QqWEDP10e8l9f7)q$dRb_ui5)Xo65V}+z1TJn7sbszg3p0d(y*%$Miuk z)@^|6^3>s<23i@76@00fMeQZL{k=}IXkASb&W;nBRd#tW6O9&TXaMQ>f(u6Z{nF6x-C8;rKTa8c7g{7|~fDz4N|Pf$;Pv5yq7 zT$1+o;7y?XT~LEHe5SK1uFBMKuC9^)RIQw}dZj!3F@g}4VRoXmL|FNM8M0cf*con` zY20?*@j-sD4#&r#Vun!6mj5Ua-M`E1Pb!Ns`wzp$2cQ;kSFA0Ts}HC3IVmzWt4ZzKH}T0A(vZ-w=JjHx(X+LNaNwl< z%1k^QFk~|XLjfS4Fj{bvB|>BXf?WUZrz~we0Mxw z7E)zN+DEAixQ`q!yfyY%o9`A^0?fN=!Y0hO*|$o8n`Ua+v!w>mh#4>tl0H@0XA5;t z@@1&Tj1~|K9ddZ}V`f6QsHxL)y;>Xw)@W#o>{e@#bCje_s$Y~Ijt9rJw!OqANgZgM ziBRAG>qjX740Rw~ugmG!0GgdM5zmO79W{hg>7-gQOz>AZwg>x~Lb zV<=8DVpj~}En{DiIgHN!ms_jf+>=kFu*GM^TdWND^)x{b>Hw$2_df1|Vi_49>TIX> zu%ADiaVwA3m`VP2&M=l@h2i!V@(sdXpcg*PJFtKuFZ63DOTl$VA z)NYYb)w2h(329_xb`l-Q{rGs8g};;(+MRJN*C+#I2GPUTJLqOu)e+i>B&dLQ;*xK-%N&xTNXrVRU8N@WYu=`=ezD|sBue-+ zW;R=y8Fi=MX!2-6n4fH-<^7BQl)iqNk02UN<>^M9KrYU?c{=SOg^i>Xhj!e$|B}o9J}_5gW%o_yBajLM0=SP+j& z+Y?N3=XSPCdY?AL7NJUD=n5Y65#ub|UR{Cd>_g$lgdqxIQ{DVRSrJePs4i$O{1_lPt z?T@D(9mmC42nYz>cE8eJ4=j~Z7C54-2@942mnmJf&<=F3Y}8EqhWW2}y=&nyGtBxg z!`Mg!gCol}`{+zyzB_Lpd5>ERSKnpRl}qDnbv$**BGlydtzvz-d3M`f@mEc^jq>uf z=iYo@)}w6G_J#6@Q`#?7EO}bIuVV(aNk3hmWWS_#zI$zcBz;M1`1H`7Y)qQ7kZE4` zEXpn~(!{TTE{AG4VA1%s-|z6R*Uf{O1l&=xi8W>G=fm8Bzfy+puLK)a$^9!EGc zlzP3H=7pN{>FdiySZyeL0ig7mE$~Z$BFS=G^Akxg$3Z6C#IW} zbSj?2NtnCsKXYItx#$fzXUQKCReyUl64zFE$rID=diN-_WdZjVeck=#rcvpSSrmZt zVVAUCwy{gYnzXt&)={MkBA>O`j$GC6(H$heQT#T?GxGMmQVNUhK$weNh|b%7q^k(< zdQg=UZpJf5{5B{9Eyem#lw(fyc0>aB;Q@qs52s;$S^qJb`LYf5G}EoMV0-Fyb>+Oq zkMDhm1U_Bs-20nMg}{$XCULAKJ1cU6OkCq<)$ubo*QZCsZz~9biTdUDT%?Bb=L=n~ zN@zPqQPb8PFBv3mX#Re%Q1O30z$zJB+`6V-=!-vfT?{e}f5JEATl8a}A4piY)AtH&4t+Ezj|T{$<~ z>%PcT7R}4tzy3r0AUPf?Dpw`KaoSE~KHdC7n$%UriD}^W!iuzxtw=qw9Rj%L?Tm9OS?7KICmowf&X#H*f3+w{I?Y zn(5t}tW8O~!9b+LimNHc4!5@BcGTppb69U2(kg~HW}jq!vvN|{{tlAu?qp!>c7#Mf z{TTg@gdLndPg_O9ZnxQDBmB5aEo-tdc0p`KE5V^ljTi7QN64yNl^(XU%bvW8jLu}B z3G$)Y^#?=J4U3=Y5GYIOYKK+&rxpt(=S&*!!Cm{FJ@>AiI&`Weoa#?>d>kzNxfa;I zuAr(rQT(^GDQ?5I|Dv4dqeysF=Q~((QE(&lT|T`$n(qC?9{@)m%u)*?M=l-x}zA?Dm^m8|@LuU(dzF30|ba_32nPU+O!m;CbT9dr0ze?oA;U-iS= z*`AQjA8ccr7#YsG9sL$cu4gt?%X;xOz!y76P5fx>!7hfYJ2(0WA+?nwYBR@{TUiC- z;&REj*B`zEUT$?oSm#6Vk;wPOFQ<@%@k(5AZg#J9MkUiSMtHHk^`H7r zZ`tC(TrLYs3)B{-|vVX`HUR}{sfE*k6 zTcdC&Lkv&^RD!Y?V72a=15bH$l6DGSgyT`pd|ANk_o*NBv@9;>GZ@%Y=^><7N<6@L9NoD8l*w`mPO4$%oxhlLde6Wj)(5=qtLZ@J-_z9>#<9TvT zU$@m*P6_Sn7T;IjO#PSi`+p`I5AI8>#P*GNQ`K#*NqFV_dNfuRHc%Ow75TJxV{e7| z(G$Qw9F(j=;FdEZpQrCe>;=Zm7tAne`_j%3x(9{-@%GkHaV_7vC=vp}Nsu7HNpKQ0 zxO)ih7Th7YO9)PI3-0b692$}kf(3VXcXxONd!IYb-sgA6{o}oN*BCHX_v&6~ z&iT!%S+z`$t*WVD)6DrAE6X*5%@UBcEqocTnKCV!oEE&F+%X({yx$Mt_UWzWV>>*e z?5!W;{w>Iz;{u(CM_}13tiZE4nf{eN3Zw)bY#mKS(Vq6BD2q_}z+CbI$NSXc@WnH) znS7+KP`bTcB#2?=b#d-`gjGdDxXu+uY_ndhrxW63j3#C*fl!$nymF*_86Vi_v{_)n zq_*~0Z~qIPRZw}742j&gTN#)tTZBhn++pOm4D6v`*~{iWD|TAxE@J$o!tpEi%>5_A z&n#rLUZ-5e90E~i9wwt6@#}R(%=MEF$*D^WNP|x1R1%>gRHo;@M$ak~gz9Q9WXh{6l~v;5e?^6U^Szhw3c(M*VSi zn=j=jp+a(x$!9JO_B!Ov3|^XzkNc{-4`R*bgWw3}UhA(mh2&nsfgm*^_s6Dn)-f7| z>7l{=vmZ=0GZDHM=pjg}EPB}0vjfWNS27hv+i@&A+3^He<(3ML5hUJSB~0i&-~UtzI+=Zl$5ug1n+QHp-}%Aqfsdx z=rcHZ8j;Z#jE=>+FnM@t!C#>!#V#vj*k}*7ChgmBo;QZJeF#F^a zdK;Mh`3su9qjwX?NDEm3ZRBfvuVwxLe z@Ou{Mar#k)$of3;AwmyU`q{ch3lL5;?KcPBkF{@Z=fcYBn@|$qR=d7ow{T!gSi*d# zDCCEI7O#6K73}Ef*kKG`?VTPtEa%T&1|z8(dhioA)(gu|=SDAdeNjQ6<}3BpniUWL zF}VUH&2t1~3|*<)W~8MH&maO>A!Df3tM$6)U%O=GPcKr{%o%-1pK@KmsVy}p1)o#vNtt2vZ6Hu{?D6~XSMPU4>cO)kkT`?Bb4OoqRSg-z>msT(LRq`P@`Be0Wqb}yN1C=tKB)@7}-jc+vj zm2gBC@Pl5mNBsWWLMCnes5-o#b1&=I(VCjHn;B=0Dc|c(7OAdN6p0*?@%=+TMR$e* za)AQ(BjD<0)e$kReXqnl8)o!IuPXy8R+w#zi z5=T;h$72WDDEItj`=+WiRaFwl^9$AYJG~i7?PW`%%3*)p44$Ehp&-kDkkCL9RldYs z8xIF{;*Nr_2Pr(xH~LVn?Th2+LNQL4ZN`ovZ!-In>HoNSK3gjy!19?A&wX7ej-5rl zfA!;OyjM-&s`B?`;;45!_&uEvw|H-8`tLj`_H4+{IvWo$Y36sSv%iFOcb6ivRlE!| z`b@1~Bno8@{QRTJA*Wy=K!!Zc!Iv7PUK3t|D{0*G<0*EQ04PkA>~ag1I~i3!1^~=e zg05)u+Wtf{nNx8>A}1a3M$z}e@W@OAb~Z&-K*R4Uu9fSvc=Kd?5mXe^!&62Px6&D5}Zz<8}I7*8d|um0sAMfnfI z=Jk{0Gd}%Ss9anuI&{sRK_E;er&e-VeV1i@#Se>=dc{~odPSS>jbO!w0=q;al)(Ho zNoJ3uZCRxiUHdYqWjfr@JZEt#u!IZkyYu{s%UR06VZ*}ldntY>K1{Ceqfd%yA~^j; z&XK4m_X56w7fkzlbk*}jeDqVncM3BYUm^ecysw8$@gHcpt&gCS!tqpHe2ws}G!{mb z+LuU8?mG9i7jgJYMA+G3Mk$}AH;<8yBq&~V4@saY!vqm90q)L>r;0#2m2LKz#FL5G zjDn_^1-NV6tlE~zrbDQqlm-yB7|~(mfzqNDzMIwTj?~VWV)VnUTYYTb??HcFTN6i3 zLv$G>nZsV9&>G;x>CT1~p1;PG%$tPiv!}{`vZivO{S>>+#XJ^}eX!XHgP31C%b8~1 zK|V%jbg02y_6jGk|H%VFiJkRb^T^FEr!M}L>Wg27m3&OXo~%49oN48wmJ7wt%)vsw zU2(GH1NpBmuD2f+eP7HyOOA-M-)dLk^{-Wtyj%WonpV2{PEas|6NR^*9rv51Uno?m&^LERUM%h8wYm0+bskH%O9`Y z{hnvA!tfZcr#&C9N+pX^b)q9$*4egcxM-`+Mu^LxLHr}6ByGaR@+W=gxm$sYxmdXN zpfr%t@+&4xoD7OrdDTTJl%1B`>Yp@d0IjeVxy7Y8@O_(uUGKFtsw7Hg(T`u@igD*# zN*;9?{VzAR6a=FUja%vqR~mh*2m8M0q!xdeRgWEBSGO;SO?l4$leg4!p!((d&pWt= z(FzdX{PdRQnU32Z^l$OIi~lr@(7x{W`nClSvOwzc!SYnB0T!6_pY^6$_P176${tsk zU^d4j(b_%~^g6uqMnz&YW; zu?%z>(HCitvz``K5-1*!eQ68Cs-C@P3LjzJbe0ZV-62N*An12JO9|flF6xwCsJ;zH zx_MnWXew=a^Uxw*s$7~$b{v0HIL_lN*TzK z=C-DBMv0)L^@M8?oNQ^x0)8g@h$L90v(}pmPs+?Zn0I`h#-hw~HbjPwGxBsk*VyCz zqod3AY=t0T`~n$=vpCd)o*Yw-Pf4v_8yuPG+@2sA7oMnd@xAhG{GC((F6ga}LVFmA zb$-zgL;GpX?Bzr{JJ<7KPj(SmpZuiuJjbI-+40*Qz+}kkqzRWlx?)~o3wv4S z>W%^+R&x^Rc=Tgl6LDy=w2!GbhTE&hWJnbU68ONM)#Mu^JaT)wV1lE9qs(SE)2K;f zQxp(QywJK#ED!Lmkmc)zo()zDN!80!KNpMV>4d>5G<3}|E z1y*8vKH&xNm1PHWv(>~5{p`lZS@WRBh@ec~qmso48dv!o@ynWH=m34?h8jL_N7iw- zMZIcac-W-=;09+?IbOTg+;1M?YJ2PS6ZdoH&zfgAJclQIfYeJKGd>G5Fy2%1;N@D& z+D1oxiPGJC9m5t_a-)Q~)_t?2fOLFy7G#rb2eBSE-UYT z+l-gOuoB;VK4jX1Ejg%Aops=eM$9~p5W>U(`FEfm{yF8MFA^unEu#pEU*nMcP;x^Z z7$Q;rNDi3@x;=lq>Esb$2}!~{KW@ZLpZ#XV7Xu8yaZKCS5DfSXP@p-1ooIjsvo-Mr z!{RgftuRJU%B)dB`xSKpj8d`ql7lc>N_-f_;1gcVM{uure8mT0@;x*8>*GLl2-mrI zOjJusi2A{mTn zo86J5PY{On%+As33tJ5nYCu*oFTgzK+ms+?|8Kk+N^HsBkKa*=V=8GV{Rjp#zP^d- zw+=9aYy;MAzLA>S*D%uGICuw3v*fc{_Fs;a-SpucwA7z>pT86i6wCG(#xsvJl0ZKB zDP8=Nyp!kP2DW*^vALRGi@`2!gTcC^80}#KjitF| zIU8&I#6OD|HQdV>*ABF^DPu7b-dD8E?NXax?|N~MUlxzQ>X#0CpKH4CY+BGbhe=(& znKg<2+N&bRUsw|5Y@tn^SK}RPO?t_EO@wm|lHFrUL>3+vRItGVgo`5GnzQHjh|Q;( zBrq=!u42mpCpkQ>wrLHpnLuuXHE0yHN=;g$nE58`8vK{)%v_#=RAl1K>Pt#|R;j4M zi-9ja!KCbRW02p4L&<9Hb+Lm4@IX%-B2I-%vEL*yfEC?6AQcK~+(@9DRX(c$PL>+9 zI16luATbSq7iC=5H_4^1KNO<*qv{Ul1{SQSHW3 zxWIQ;(*@N&_a@Qa0+~*xOgiG*zRcpiJNE&$h`T#H2>npSnWlNImv6?PM$fZoL>;?t zUsh{Cv4j3-m_bzSO}XoKddDgH*3<)9{LR9FDltlkK8{8n$&zY+4?Lz%1nVzZ6uZTEWay)KR|Sm#4xnl6;5dBwI7*G@i;~r@>Og zFTGBin##)G-{vE}!*b@@L6P?4%q5xYKb*KlZnDdt=Uz6h{jE0>-dA{gvx1w3ATKyy z7g)inBzLl9Pb$sOe)D+Fk3BOo#D?Ujix&xUxj+-j+Tw(#khJE@P(NxUMz+Tg!`~mx z5#ulImMbhrQ@-u?<@>uu?`b07QWQmsh*bB|;ORvf&fYCOx|?>6zzq;@rwiWl}2(rjwHeFQv>KfCAs$8wic z2$k2#H)@mn*q?`l))TP*Q0VU2pL>BKOHIt&BpB76me+tJ*uBcChO_ZY$NeUnmow_A zEz^=is{fgXrkJhbQsBw&Jj%QQ^@V8;GU&>gxkXYTCc?{Qk$!G6q*SpVi6M_?b-kle z*i@{`RXs>AX0G2PmgXe@gH+=&#_8FJwUCc6?;r|ae3r0(QNyyHOuN&>Ll+TEz>DE& zw1WYZgbrotc@(}_q_YK_g8gDpC;b3LZ#vLQ5&4YwHu_hcQe9i9*uM~Klq{@6!PoA_ zJ(40g(rY+(w=x%_Xw(hc&+D}l3t8T(DYb(6r#VGG{DiRU>_vEl6+YjUTEIz0bGl@L zSqqWR7k5|qo4bqIL3tk-x3rW>9FR&}^S;;l zoG+DgOf)og`*ZJU;jQWBGKnp-= z#(@}SGF)7I2BgRL+l5-UdzZ{zyk>YOyLWeo@iRy!ZeitP#Wu4)IIjdVgJe4G_xuDbZsdkNXG?ew)ArARKhQzxK(lQR1$x8~O>$c|zt{df{&{(_nD>ik^NFVi!7E_tfPeIrk;sw^Wf%)z&0Jb@QU$DYK-~@yS~DpX8=Ov~ zC)0eVk|5(hsSzj2Op%N>dPj-kTOc4;5cMuhB1{>OX$ECnbB8m^w*!8?o`@m{R3ut~ zg;gWA3inwYCS<`SBODJ_CA~sALG}$SwprlWiRH7oR6h%_B6$MmTVP2h!uVLnb?D%Q15|iWl9^rZV2Am*okRQh zibGhgVE={PGo1;r&&-(10%=^aiass>gx$M^XCZU*ZNnwr-KTXXFt-LI-#@yE$C&0} zIl4SqmkNrr0TL|(uO$WF1$qi@7aoWAwjZ+$0GWE{}5erq_X#v4WcaWWk9 ze)_kk%sz`lt$Ue*aE3`OS4JJszIUV|D~RCHYt#fZP2;VlObSn6#B zBX^mHxmd&jL7dN3t~`5()7>&_LV+2)KV-L8=iv4W;jKjo%1GEfT_PT`wXKsp#1Ve8 zW}!j*jn%;13ww>0aAKVD)-(bTZ^Y?oRPhQxtvoxf``)=ss1yw6IV9h@=D=GLiuFhfwLPA1RWlGBVFF zc{J+d7{38>GD&8QI5?)#r+`OQkFV@tjz)d?O#~p6;tJX}k8&_-0XE!)E$lBu_sk*c zeK|Tzgv^z+iU!)D)uRc%#N~LCa>uZN>$euSg!q4=|A0hbZnYa|5`QXzZYr{ODnXc( z$oG?*vFYaiRhveOlqS)szS+|x@7CK+Z#J&W2D~Yf%e<3fm11Ze2Z&i@_vh?e*&4l_ z<>~HfKX!=rN-(OYO&r z@Gq8w6!?T#*is^;Y+}_5WJI=4`>R}Mup4VvNG5HudMcrorr#A*Lkh1cR}U2d-xDi% zP~h93PWKs^94RXf#VlOVsPl3bl6wL*dZHd*veW$(LRoF5DHKot;SJD-A0Lfh#b2^- zc?t?lm2ybkQHxZ+^3@m%xHEN6n)i)}1Rm1XolKb8W_RUqydEvz@K13UN;Ffc;|k=S zxZ8mOxFt+p4=i4f604d8ib(nlx#~l#m%NZR(-jJAT2PIkPwq##x;b%sSU73~w3Wkgz7s(OCxZQM6y`i2JpiBrxRE7V zev|Nmav#bV&2ea8HiO>z=J6S=*d@t9Pua|Cuc6wA8s6gV9)2I@^3mrN-{S4@9TEL5 zi2rR)+w29?nwa{cS@zz2yB44JXS7laTqEe+&RPtNQ%kxeDQPLSx!WqxISQ$6Ny^xN zLiO>w_1;o{Tdi?4e(O`X z$eK0U-X+d@!+2-q@hf+g{C)gL?XLsCWClBts(tjk&;FUr*rMZ(fVCCtshAxj@QVjS zg@x9vl{X>?VE){vS%SkQIItz2*R; z(FE{-_SEbJ38Q$KuH{cmr!^m+Bg>PLjfcwVGJvEd1oiPZfBU#Ft%A{K>$x?}`9iS=bAQh%hAC7b zmmA`npBRJnk+I0IKwV3vK6R4ou}XiXf6APpNVh*oEl-Cd0r?^d@$!at3K>D8ghi`~ z7;8d2ct(&x&I{>iWU)}b1}R~Bfqb$oX3`Fef#;DAXx*=iA~D*{FqZ7R8=f;+{mI&T$>G@vWLW4`t9T6iHo~?Y#T#GWv!s2zP z>D68IXP;A^U*6%~>pW-u7wn87&nmx0hM{#pELUi&bdW+`X19cnzL&5HhlThV76_Y{ zfT*1qY~*414GRQ4Mg4}w#==UVg8eCmrO6GAdL)C`>*Nw!Xq%rUyG8`t&6JW`Nt;p@ z{^q{Ja3;;@TkRcK-#9`nKVzLdt1EN|>>f8vU~~(_nTORmze1K6-job7!TGCJgk|P( zQ9c2o(m?Cj%Yax0P9!QFX;jHC8v^tPDGWG35Y5%W=Q637H( zBY(*~Ofa!TBV^S93tO;w;Q=wB&P+i_7LjyAmtWu^LS5qLyFErus)ui0&d~L?`0;VvNj3TO@Z@Bc|QT z!{-xuv05@~tIPvZ06>YYU_@l`*H^E6%{T;%iwrBVcEs&CNohL{tp}n`#QK8t$)qZP zn5^T7-;MLg4v7X_ILXPyOd0&mSQGjpQ>D(*D+KmE2w&FDK4l2JhzcKUtR^V7eDkT` zx|Y(pI*^Vr%eC40%pOc1N;fG6MJWg{;-X<`epss#sC#C71tB|N#^6DkD8R_iR&-Bo zSSkvn;GI&-ep*HWaw5mQO4;IIbaKg~Y9Uspgo~9omdZe3=k2GOb3hJ6c)EXI<*L|( zEhCKmzEvDC`4N#%gX##8YJbv3ypV&7-6e5=E?_H=auVOvt9b{t@6KVoS8Z8A@BQ3c zT-5g>(S|Bsjp=)5*b_~=`HFye8yv?rslCBQmKPvHa=O8MkwF2%I3euUNx6BvSEHQd zVAU4D2-odKc!u^Wsi)fI!^FmXG=YN|B_nEE zTP-!CS>2*hT&iqgF=JX<%~(CxFkEe3T~JD>jIH!2Aq&!#2YE#g3o(h7G={eD2cU}l zeEPNXkKfqQfkgdL{ZxHS1jkw0tUZ5}mY-4K4??Bt_WU-gkx3Q8=2Pm9kLJVj2wmMD zX?x55{akrW+w^tl!fTvFH==k-GWyD*OnOt9G;1oQRmCB&S`K)(m2? z?TpDnjyg4UYZR?&-+~nX>AQ>Ca+RTgKC0HpZ(A@%sGO>&)4Pa7={U$;sx?xdHf5L< zA#n;kLIE>Wbl8zLQ?9GlKq z&R>9cTPvULp;^|WOspR5wwk|HC8+IiyHD)yd6n0W*N>8JHB5-YQA<7h8qG}qu9pj< z7%;2Rq&~Ivv-4SCPk%u+a%{|M|2EE)26hE&x3u%7?~>bkR+OmS&MEtlj56_!XRn6m z^-efEab+HZ!ggL+;>TDLH$D6HB^6kvz^{R{yJg-=J`vUPUG91 z1^HcxQW3oRpyl87O$Jj}@Mr^TJo=Hy&1WVTH6^Ae3n8YayX7>Z!g0>OUqFnoC6M_o z-LZSL*f`Tx&rxzV9w(TrxG$W_$px^;(6qJFSbg6(w7R8DDX(l?=2}$C z#o)}Bp;%DM%qb0b1xia+{|~mdUWbb+feAi==Ku0 z`EBQA*0xO2p7kPXGHWd$I`B06ta>WWZf2|T9t}BceAQ27udoQM^;Oti^x?Sumk0nu*KGoz__xb<6gZn_ZyLv%Ry$AdFzlgT8x?vUN2w+g>*pgHIe8 zVMYBSjov$E+h3g4a~|K$kz_dU`Ar<*%G7BX>JTGPLrCsQz!V)jwcIz+017It`KqJb zqTo}=7gOm|Es<=dg7VtYq5-_&s8r@*wpS-Uqf_!@IkgGaBMmQ3Ac+B&4!y(ppBrhJ zT5})U2y}y|2>k}LFuuzvs`$RqT*2BOGGK=BOl##@IEn)!fWb zJOrk~Z-hF=G2Ehb%h<19WAWarFoueC1a=y>aRY0OO(uKnn1!U7Oz{-aG=;co7`H;J zv-?7!k-D4riWBeE!R6Oxm*d@|@R2D04Y$>$6SswLVQ=VG&5R|uHr9naE1MO*I%6T32=)uZ+1Ik8F*EVHJXVocvm(#wajj0 zgQrsvx9El|7PT+BRA|(`iT?Plp)O;s4k4#VcQg;Ak7)R(MTH zx1ONq-1V*}ilO*hiuNVV@P*n0@)b49w-rqu`fh~`eP|N92eBl%TCNYZ920%xAgX$d3qkezeb>m{4$M0rw^v0JGPwxSWC1m06ksAO}GuK(OQ0dj{# zXKeA+hY4nsl_`kOry>P)6$xW0W0$Y9j5pYaqusj1#P3Js`=w8(iLGwg)C60E_#)n! zXmoRY-}czr{e&L;WgO}8XIeUxmWwQlv9hR4cPzqBA0!Cr2$ZJQRYO|yc3sW7-p`!O z`M2YFskNH1P>6ExzHDixEtOZdifOvdP)KDz*+?n8oq?QkDp>=8U>a36Jz=k%mVs4A zZW&qrYOT(7+u`8}t$u$mMGRk+>6PIolrML-7#XooA_@8@qi(5A+&e$oi z)lrMLhT6x~PigA=l1Yb37o8}ZRX5Y1ylUDcXr(B5j{K^yVJ%lw%+ky&IjGf`ntOyS zuxB`rHo~08>*Di5ZCDPMrIh_dT>7q3pRWTNL)Qomd@DvWwEn|q+=Nkcaj35H37<0C zxdvrC5j(|GU7!tO-f~VukwezI9u~z|W_OIj3Mxgyt{d;e*5mSF#SJYaC9C;#RWmmq z-x1q*1m^)u&1I5H{T!mSbWm6`Q2u}qa! zkuCSf>MH=6&iFJYcQ09h=lR3guf%qP;T@-z)ep)%rkYj2K4;w?B1h|-?Cre+>Yj}& z6beS;FBUi&0^lHUeE7Xp`)3I=qAI&l5WFzC_XqR5LJ)eo>*(Mx09}jnXb#MJ7grI;OU2ur2g0;wsaf;Bg3- z=``_)Hz=`DTNrA#U=j<}53SV8K6r{!qPts15v5+y4$IMaUvS~~vT`E3cc*#Ix4nw; z-19gpUy!nia<6b&9YY@ziV+cD%<^=-5iF%EPiQAp8tui>P8LV4bZRymW<{;vHqHq$ z64#DVu`V1rZFq?w91l?-+MHvM7I*9#Uz_`dwsN9TSE)hRwY75a*pNkL#po?t(_2+f zZWHr`-{z%L^5v06on|Z}#jmxY$SlVf4alB~xQwL8+f8S^eGY}xOftgmtL75&;VGxu zH#wao!`Ia~8kO}b7S6-PklA8@djWbK)&!f;sad`1G)3n7W`R?LUhwC=27ja+JED~o z21p~AsC7wmK6EG)4dpo`$Y?_pM_Bk}ULDi)KY^ksjbZn<@avWmDxhGXN;hb2i2dwFfhN>(Zbm11PFCuQ;mRY!c2 zRe@8!&HEiGr`Qu(g}|kFeeM1Rbwa`3$auMXqA)^BwDl)bfayxTkHCzRD~A`&-e??h zgERH8T($jMLB%{e?EpGqWEULEg7|le0cIE^;u_(G&lgs97$*kpR<;LaX~e_Bn_vue zebJPGud0yY7E14JP`95g&Ckho5(S*c!-VoUoc$e-fV|`s?W^wB?=EV}<8h9=(mefu zB#NZbamvNxM2{?^^V{n??~Kk?qR2djtq(d~u|BjgaQ*D@BhqdtWiu)r*R_TW8?tjOx{ z=Hv9$a+Ec4DOv(0`Mnr^%xKh3BMwIq>mtyqNu4ol{FyOU)ZDk0ATB5vAWhch zNl?AhZ$G>pTZAOrjki+Fu)ApX(Y>_{xbJ^_a?!yJSEn(-<@uDI=eO$jXw3qD(iC5# z6#@?%GDKv@aiP3*u;)lgMu}IZ7xnHb6`tS^6m-d>Cu6GWlU+^Fvg+EfHm3`A|DhUB z0;_Oan@W*0Zc|g!NXp#&{94X<=-s9MwY#}=iu;x4cnb5oWDt+6*u<__MH=lija%6M z{^sP4`3t$d>gsh(0J&9E;H#uFU_U?R)n0lZ2VTo4^y7IwCw-1|N2p#Mx8QR!vj7*? z(XxQZl7gr-|3N)-e;<0Ar|qrfMELTJouxbK2pv9qqv0fFD{d`Hp`ka+BXkmFW#!A3 zyDNUl!ILrF@}{Pyn-bVxsupXi-4JZZPhmLkTBHt0iOL=BBVRIC_{{xpegR~yuYU^T zC%fM@E?@)21a%f->B8|xCek0t}%&P*XC`vL%46*S%Z$Q4_=$ zR$Wkc>(z*gJ_jnxb&B5%4xonE{jWr>38q;9>38BA$BSR2Up~M@whD1VdWWYGh3Y@El2zQEthDZJ!Q+G8mK~t7lYrKj;>*aKf8vB0l(xvj{KT z{jxF(TsC77CZafM6xh9Uq!OQYzJ$J=rap11QMc;Y57&L zkSs_gY?#lrm<05YQLr^?pV3GLn8ghAwj;wx@C9Ib$r;!jX)IMGyJIJA_x&ibuh?c+ ztlt2R%8Pm=7%M?W@`K30w;uy$Tm>jxMI6()#SP4C0`vmXabRJHMkkX(U!w{$2Fh5u z9`=rN(#nKndx{C#P0HXH9$J2YP~gE5C&1t-Bdp1+U(y7ch&fBi$pq-X-? zRf43La;~oRlr&USqE6fiiVxRGojODxPJB**fWg2}J{5BTzi9fop@EC{?)u2>_F_$0 zRTXdVxVc0{^#TOQ_UWnTSdmiWLuO&+5b=dO3E7? z93&t5>+6*d4FTu>^%Ec2{?ktg{Xl_%5g>d72Me~!C@d*?_ep?scH!X`KIq|Ej~>oU zW$#B)fIM>^0R|=mZF6(;cu<;YBR4?@ESJ=7Y6V>d#|t9xH1Ci8wENNU5qY@x!#n@& z-_sC1^>~~;(#R&%)=0{Mu&0Wio={2}3JQweD3~j8Z&3)6zfe(WX&j}IiAgtWA&Hp& z(shWT5V)|A_LlE__8H-t9p$r2x(9F|>@Amgd8|tLY&UQzaF5T{mROuQvxPh{GE@Mv zKoolaHD<}!D?N9uGjtf3NW*tSv8@Y1@8BOA^y}%r8vJwQuMYgT<^r)n{!47;&#sg; zG>QfX6)tXWT>}5Q1=rhuNspf|9_Tvz$Hp1AlM&UE&EoQ|9jA6~qK8}d|9B8FaK00K zKeQ5*0Y>U7O`io0nNcZb$sRs~shXoQiT78-zAt>?5xD{qet+d{N*g!c52SlZ|JNZw zed>bfvy8hJ!mzD*lx%#?ONQUC8})Ae@38@6g=Ta11)~=$ILw|i`pFGWr{C?vkp4AR zfk7nTQs~j!hf4u`e+}FHFj7h}`eNz)>*v7BNIB|z`To}-AoL(0|9ThZiU0kW{QIE9 zM*e?&H@je#N#5j% zo2PF-}KJOe}>0_B7(Z zDFDzFOP59ykEl?IcPZWf(3=Nc;Ssq8mv~azA1<*Ut3SZclN+{r&;9iBWsAdN@jaoF zb?uCn>En2FI^M|&z0Zf-Cci!0b-n5|wp6Xg>b@ZsSoAeXxEp+y?Yje;hN#oeIPNGm zEcBU>4BE0w$7(%#0GWSv+1>W=46)tOy(9iNzjKQ4&i%aQM0KK{cL3LJL!%BqSvg-{mO zbUj-$%?f=`^2zUeXT?=jtpC~02pk>^yRX=~Q%S0Gn%RcGY&s72eHm($Y#pGc!AoD6`up zk$TAZH+RX`zwIlq;;godzdBeSIv_y*Pooh`H;=J7!S9uE0(j&O1Fo(1+Ent!R@}V` z{gA(mkA$S;z>`-gnE% zHZ}FdT9A+D{|Da0-_G5K*9C`%hyR<$BtiMmvnoLijii^dX+;0|VZa3c?J(RwGu!qT zc>nQZi+yWo9ZRRQ3`11;#Y?=KGjd}&Ay(En4xLJFxNG<@SEC2LzW*9S7_OWd6d|tY zq>#rwZmyNhmvdqW*Mbu-*wOeh)nf9|72kWgHMMdZQUN_1)Lk`ikJspoHk3#W?--?O zAt=w9^*=zz5b(|Vry_mV>s~wB_<=|=9Lm&OaI!TlXr=XJ)7M#xS-y8sL0 zlbVDg6yL)XjO}+!G9dG;g+mS>AO8S-FLD?tt4)im<_PyD4zhYQrLLlYZWBm(woDeK z-);QKVRWNW*DkxeI`PIq^>bp=7!2>JzTE1$8$mlu7ac(HQIWs*llvc6S691TAGA|R z^51O8#yvdPhwclAi#<5jK@91A`JFMZEOw{sH-`&jdbzgffq%OZ#xdJJWz$l6cYD^7 z#N%q$)cVNBNth8XxF|16Fyh8K&zzQE`3%v}pE7+6tZ1Vf_-)hWI71RsPD=S4wG(jC zG41)PBv@hUCtxq{R+KGUzWg z2Gn>_VPWt;omJ8ruuBbJPb#-+`wQn3zVmr_BO@cp=sel#C9OhXClKeygnCa3`>q>oT8=9N~-Q$~#^ zvK(xeD>D~1il1d=h`n9BvTeqg)AmhTIO;6iWj=Xs%V}J0JTBF!lMK+S>V6}pNR1sr zv^Bduu8n-6FNOKQ*DLK!O9v$zu zVn|05yKh%YREAQ@q;e^PjR~`ids7Nc(mho;raua&WIggZ6$KCK&CAg-|EA@V?nIsJTu~5|FGn<@qLlZ%%&$_b`dR=AYajWubNd!)~Rjijl z>g2h_y16Q>)Z^n~5ttBvMbp;Q)40G^AU?RGliyeD%+~ueqs$)U$w%!Wa`YKJPlB6- zR4RZ`ga}3JN!9z~b7j5N?T!8AK}3y-L=BW6K(C;=S%*DYM&bNNPvc&*rt_Rcsd1%EGCY|#y72sT6OK8!e6RuaW{5}a+%n_L3URK@iodbS=f%&b5l+GZX1kSN^az;Ic52e& zyG_>`qE327*Dj!fJsxHQUd^b587LH+u;YsQvkU*g{_)S{G#NJWmMWfB*}%y;biJ`_ zATl@K@6?_t@Dgr(WjN41rt8g6K30rHAtn-{O+ZNK*oLFJ4z^hs0e_`n{{|)W!0Z`) zjRuE8O0)C*h0q zURJyWRD5QUXeAYuJo{cw@lZUb(N+xpra3I7ds%QjTCFxqcb;|4-)-0`gZ47f&_q!> z?#)6+E_!wVLkP-tr~(Hy`4j_2z`dx@w7(<)&tl^G>EFmrFr)atkUNIbbrVXftz94c|hfy6%RZT3?5dm~<5-HbKRUn1NNQObN^ z@pw-g6T$Gwkz&T;K$g!Cs!VseN;NBG#%5V^XF_#=Kf0o60R&4ccWzg%bJK2T9CC)m zmjtY%BR{P!@0F86U$Z~$rY5y&k+8EiC$HVi(5Bn_z|FRlP4=;Z^oFs@DgJlaSSAf_39FYZwa zjT77Gm7)5=%DW-ld}_@xt$hf<`x(1#hC>&gB!2H^u`mKhfvq{Wtx^U)4&7HIQdl9? zM{Ebd=lygrzfi!857w=fHZVAmeT0!_1k6^T$`!BxNDyZgL7U_tlqu#0J=QvplX;_ zPD7eZ!82EtX!`}?sOxzWN4b$1NF3db6YjqHClOI=|0zmhVq&xtPM^ov?oLlPoPfAE zBl(3(^7XwfyYPgST!>P}h8Q~gl_SD^$^fk`I4UEi%BSOsK+}xRE!sJuRsUvS@~Im= zE^f2~S^Y|@&zNPT`mM9-C+WHOT{Leiyx3hgUjy+fZKGxnFzYbph)Ye+>r@OBTk2n= z1p|Sn5!>1ZPKcAYU!5bsc6Sk>m4 z^rc1T^dxiCa<$a+GBNE%ibccv0JfT((@!X#y_8`rq)+;I;(Sq0ciT5|AWExjC(#6vC`#7-hTi7eu0GtTL|EqAv! z)=SW{*`X%aqsgkd#l=NW+@=SNt}-3SQdwitX;O=3b=jY1-@$|NQA2txJb8ZNRC=DJ zM28n!RMSO4!!y}6>nB8ppGVf%npgD)p6NqTp`~1BEw>YHZLG!CZ(0QSOwTfduK^9X zD+L9zuTFTlc-*9Q)==>SY`qXT99*2~y1>Io==>|%k^GscuiBD1Q|)LFr!biM z;9I!*CxU*xGLc&3MIYAMXV(7}Br#QGHpFVuOT>G*o_+s-N2)ExJgp6Ycgw&xHZ~?b zNEX(_Z2af>%F2paYXF)?-ABF9bIOf4Rh=lxRA7e?p%^eQ=%U|Ah8yChsun|^mR>`Y z(5&}}yPpS;1$O6U@aM>xsvbxmRSg-9Wel{rzj#CW9LWsyEv{r1i};6;Og-?7PYE^6 z8x%!7k(GuywB{9t8|BJ|p%G2EGK~>Fi$}HHjIc1a(Jsz)KwqDfVgJT^(_{R z4?8G!h!bY4$C$hhP-Z}+)Nnd!aRLGnP?1wQ`uMP$$pUCQEj4w2gx7JlQv@2!qQ92N z^+gkG>4kZ*iidiZG73jw=a8GIxJI5dd^p`;B}J%GKbv&K%xzgw%~~C5#bYW+ATH{@2a(UQ3$}A~kNO3`+8I`|Aw;;)dO<*&J?wqbmJ7Xcx+i zY|^p$7g*`OOQtb_!)su*V06J}Hk1fGH5ftB`dbdM7RqlObaWAs= zYrV?pe4eb<({HV&YGl$q4ugr%18*<-ylGijzRfJb`M8kbVUf&=wcLm5bj|c59HAWD zYiLysS}N8>kBR3L;lr5}$_==>YmT@`naM-RI?e0fzs4L~blp;Fu2uoTo^pK;bj~G( z@v3Y<(+Rge$EXf{1rrwLE%o2ws+d>+W8J*(KciX!h<)SCJ*@?{MjS5Hti?SN3nwb1 zQOq+1gPz20m6YbXn&=eKx8YzuTB7bY{|WJUF_g*t_ZR%Gj*foLwUlh`-8x|)nHFbX zVMsxGpo&Bms;bL0owK4FYuW>0Mz}7GfJAFXmVL^0GoSBBquo>--z=d0rlUje?3i65 z*KM94*P$RI`gzt%FO%r~l#W5?R%~XPC(6jt`g>rg3m8S~qx6BLSnRBDB56c5rD#Rc zefwY81#6y2qW?yQ-;;U)?u+O1?y{EuiSN5wIA3U#)%st2-^94>7_0fnPZ^C{*Wl6e z>AVjWC-M%+jYejmrS}AoBVBK(R!r6JHMI3z5@^Zq$K~|W?$}UP`@C_Z=Kv8dTY=%ONrbRvrgF?T*1D~p z;G>o-8j*5N?~MJa`Z9z&Vyf7OpPJ6_9+7JxoEp8uLalAy_?-mw`p2mg8<>)6)o5{xH{dr94Yt8&5q#HC@d`O>h2yI zuQWJ09+GW`HXuLP;3}qnW;>9Ir&mX(YgN_S$QaHmPK_rU`RSX0;u&eV zyRqr{`bL;BfzJ31pa=$lH@)!*kvNia#$iYXbSBhZ1oXlEKJn(W{E&X5e0JX(N`5;m zM4PKT0*1RR5xN%ofzhy$QbvR43brl1_s_rX3Gm;iH9^ zWfVbxk{MZT?^T(?NR0m&hLS|g#muC>zCMZ7EZVyH!dNDqPg}deA-K!Q!otFFHPNI9 z0#UfRJQ?yuKv{Nb9O>`R5GQsOK@SuL+u@Ef0lX~O#(?o@Rj0;Q0FkxxbI@W$zxzy4 zXT(Fk=;b{TDdDwDj{e0+eG-zXuqUHig6MsbM|?S=>2^1g>yF(xZkcz9x>*E#nB1wf z^G~;PGx=tJHg2*Nwg)W_hxNK>BqFe@AHfSs{y(g}1yq&W7B-A}jALOCN~(Z}lt?!Q zv8B5~De3MsK|yL$3ew%uvMCjn5^0c4NjFGKe{AV|NF-H8Rv32a)iC#cdfae z`NVt&^m;pCC@~Q_T%8J9j7+Mc@Md?DlB#Ozz|Lxk>tY@K zil6S*O^?l0|9Vd-GQ9>yMy5c>p!WK~ZPY6K<6dl%9972`7HcsIv9|cKN-7s6R(6cS z7DDeyUwl<*_q1SW`01Y06Sj+O4zq%a9A+D5n4_xX_koMfNBXfCsTmaT`no_J8&H8~ z8B&HrKyGXs4e@U@^6M_v1#TSEDoUzNgqfO|8yDU!-T7G~oB^FXYsE4ao)tXz^3#3- z5%Do=9{YBA@jZ6>%y}epV5!mPASr}GgSKi#XBf^MHyQ5gDH`%q$X1ifz+IKiY~a=6 zTA}W|g^KX*y}r>q76bFF{{Vo&r*+{bsu-0YFdkc4H;DZ6tc=1sJjoE$#j6;;?d3L2 z?aRMdwJ_So7{RhWyKC@rDwY2y>) zHhWqMM*OR2yNc^_%QR!5>3foc2X1uFa8cUzo{s!k@eq3LkO9(W9C6Re$_HkRCFNJ; z{yQoqA(4X$0G-;cexAN6lp^Qo<{Zi&TZO6YRK0QBCO{OV&HWanMUNs38^TQ{Ta%gb zKuFNssxSVW&jjytLU?yQ5YBAx6XYb>)+ZS?ODz4RsBJ!;2HT8_j<$C6)bw;%aPaA+ zZv+Gr#-?UwE44I%64;!wIliNg`v~6M+tVv%efAV`bYJj_;*ERnAyC*e34rROhe26E zbk)bFGG!%~XR+5;CPH$tR`tX`4r^)))9@>Ad;9ol`EHQZ8nO4R!{6r-5Y7HRiU>-; zdK!ft9UYVPjJdt@xJKo7-2Kk_g&%mc-uBJa+G}Xwbs~B^VtQtU;ywl#s&Bp?(5jHe zUddb7yn*!GmUZ_^^k0#AkYFBHBANH#8CbhBgjLk&TAu;W1uf&oRiyPwxQ|?V!@6Y| zgrwp_l$@jsU+pB7RJ*TKaf2dJ%W(pHk2l}PZB>eS6c}ex$V4S+)8I#-x&0O_O2(6s z>blGlYU231#gZ=}pjFkzIj(B|VHOyA&CAOR(?osUS%BU!iJXGR?&)-~Nh^Y+LdIhq zms2`Mh7==>K`3t@ZyUi={K{tXe+L}!o!NAnu}kj#4}eIPhd%xu+0lb*OwRC<@|hG7 zOZ_fb&|xMm2K4|8o}l(DoQ9iCZu#~G;tNL#Od_u`F}1_(O(;4x^J$JWj4DtZUO+a> z_=2;BHppTWJU-Q@85SLQC~N}2?{Ps0?d)dHy)HAz4}1=S4j;9i)XXZsqXrToseC1=HY)tJN2jQZcnM^ z;RPJot-rskUG^nrT*YZEmFoxaULIrHN6;(};+ji^4*c`bZ)2#O1 zQw*OTg!1yunkQoNdo1!DJIbyYs2_dG)IG~}f=rt!mCl)hdJfa&2n9@$RE6Q4w{MsL;ruMtrYtuWM^QzL) z(tD1OXUEWcE~MUj=OdR+xe3L}uJya+9y)$54m3kz5|XK{rKYZ-Pkue+*$cxp4cA^& z>}*ZSdilOTO(3#t->&s>B)vLRF^HwjsL}iV+0=)P3^Dhi#4}_%@~%dtwll40KwI7SE%<9d>0uRNu9>KrKG(vb4+T4EC2InKW;UK zp}tNQI3WUpyHWS!hN&pwUW&x4B2+(#N_dcx_MXE^clknE?PyPdS*$okf~JHKh{1|Jp|E zc))&wgjAaGOXzTJf5&2*@@-TqZ@N5Fd=k`#q;Cfa9{fR4@F+r6$3@htVkMlS>WsVj_ zF9B&^XhA#tLg}8eTIa2GdlR?}(=;=MchoQ7p=06#Do|a1yG|jQE$MIy(!2UbWYb_j&DZlkwS42t^3){2Yc3 zAe>c~D%1Mpv1%W}uKF;>l?L4%1&@v8;DLEnS67^jz`||8m3Bph&PQ!ahbS*Y^@CIA z!d@wY+XJ35y$H}(JoxOwbX-OhD^@?cS1i`1ZrG1q<$N18M2XiqxPM=AQqmOwWADG` z?5AviXvmh-%WF3P1<#WFPvPMT25?*no_X(=y<8>-!D-Q5h`ts&A45d3nzb~gvyzUb!ZJ%ydBhTc$q{^EmTUe7IOV7_Ea z20YyiqKAg`&_3cDd#QhtPlc8{hd)S)&y{7ULtMBc!DX5jL#o9uM)P?s!0tAW(rf)+ zZjtA*W_;zQU6;1gEPDzTdj_MhHrJ;aJ$%Li6u`DZzZNlC?p zHn%-7`1`?obUz1aUJ_Wj`MlP+t;+i54!E9O!_Y@3QSbh&;TwE%7Dx$h3pjM46={d( ztj9xxtD}>v+LkIiFz>kcD%qHv&kH!eL5eBdY^y5nWLbfQC$uIbwZVBu;q<$36)MPb zNbDBv-}K5dxoflyM@cJrUKf)rnL8?a>RuBNK8H2IujYsSG-h#Dukx(0EFC3*0^ArY zteYt2&lk4>#jTShHp)-=WYQ5sD`41Z&x_gnKBhIn7n(2&p?d;aSp|}WKrEYp2DcNC zU)pd0Ka1(+9}kRw`Bk5v&paUuVraPGBvizRn&IwgTX^7j22y~}d&f(%R&b~w40Zcy zpQY-(d+})3mDFT8qP7t{aKGG*g$8)3^WFxL-j5#2`MJCokw`IlGTx)ND$#sn6Wock z7(&72phxfHXib31SvZdQjVq_hy7BnXP7xz(#z?UEGPFx+#l>8I^AcAw3bKo)>Iz70i!aN8 zdf_0PA=2l&)ZulT@ZUB5BdDn5FV3-DYK3#31JeqG6!0eb+rRkk7=G}z!9V}5b9Pbs zpI^x9=)`cDe~SY!F~?T5ez0=w#&-kiTqcl3`( z%=lSH#gs;nLEH1!$w!wC4G~=g>`dc{qbiT7=-104bpTITE9}XGNK{8hX zOE?s>6#&`*X^I31PZQvipc@1MZGYu=s>HI|AgMN`Yp0Z;HLIBoYwO4f?0}wW``P*t z)=jXa_YE~dM@{hjt5E?1J-s-zqxs!uc6SAS-K{$RJ&=!$?wRd`{!mFyE)ITm(MxXI z5eey6*F^z7wxvk>NxB1_RbbTE3g|X*Ys($6iDJIY)JZfC3lr($Co1ZFH@OqT-Vgw_#)o z5D>@Pg8J?$YNRmAfYt6n1zXm9K>T?+pd&g!%!-qV=9{${F0mcGqwTpqs*!I1ZP$~t zGR;tR5-UA6KVKsH7^MzNHT!tX%u}xs>8J!*R!w@Wmo=TeA{ z|3Qb5iO>cuMWJAE2w+s#HB(!%*aNZUf zHjp6o?60#PMHoJwm7RD_9}}nci+ca~pZEgbO%%*&^fY{~<1xd2@W4fkf}>8Pr)`%3 zC3jvRh_JRPJfV?utv?nj5Cyq+ETlmPWi|s%JFI;=43NBLEjt_2xeeEjZ$nqmj`}T( zf~RMtjPSNST1r*xjaGWZXMQcoe}Cpn10SoG@Xo7nPu#`U&g3hqs{MQzc)wHJg=PQI zkZo~#tLGrQ^K5@Lglb91zt+|URS(L+So0)iqN5BTi2>5Q%F3#0X=$0%H)#)Duq3ns zc}EVj>7DaVvKHkyBMW;h>4#UKNWwpXJNY%jXGi}^R*=sof9MAj6OR~5A zuvgZq*%+ly9D028=}l%lbVn?KHHiR9xfH;Al45d|o15FjrwSmZ4`CZeSWFDnKV6Y@ zW-JK_Su#!in-8y+l+suv%J6F3-<@#oHm|b-a1odMa+`P_UzAy{lMCkpCsS{hdI2ql z{O@fgNB~8KejT#EyD2XdrKE4gdQt{QBbE4oDegR23FRN#8zQOJZjwkGIPu z!{mYJhhZ`DsuK^S0%s|7$~3on(GR(P{1&hUmU%N!R7O@@kw&Zbv_RHw>`Mz>gjNF7 zm`PAt(77^n1^}mo`$G{CXu;v$C|)@A%!z03x5hpGEA9b=V*co*aQA#2*-Z+{yzPqNJdZpi|*G`Gvj7Ja1-m zzKRt?#$~RS^`W~O&gzxTfn6PON{3Jx-ccf?P{4r%T1LoaY10yT4I;MbnVD^&nunD2_Rs!T_}6oEykjDVU=BKanh zLhJ{hl1?F-1=L49GPrcVec)NZ4_!n*X0}i>eaRx)ZDgWelb)gE8cjejSW%7HnHuu- zN*{mzES9a7cN2~RzNxfCgGlDXVGHZ-h!6tm?JMs%#puY$Z9#7!izmD9$94d=P6Dgm ztY@wFJ!8-<5uLf=NqD&N*3PH5MLiKT(AfHO6+-*uXq zlamw94BluN)QaO~@*C{I(Nb#}Sby)gz7x8(&s?5mw^+Aa0?2y%C3+@>DN=04kH08? zNClw>0r~W;Ye=QD4qR0u)Lp6(-jT;HgJ&S^?OQccE$fE}k|8Q8+|cCXv(vb(#Rg8e z06+|U;vgX)uy+2jxqX$HIgEB?vQ7M?56#!pd$|2U07NeLmY}ih^Ot@i>ieu2llI_z+WDg$deRP z@y#h+Qu5V?TPAhe@JR&pk}|;8#vtpnHqfIW#0SxGa&b+;4>oQqMwI&j9mnRl8^|*z zv@t=0uKzAL4QQXT+JbmjH3?^9Gwg+I*b7Rjv7R_{F3l*ty({pq{nu>64DK0f_a6^Q znAGA99wSLhO1h0LyjSgAE9t!NyYS`lVP&aP^+8IXvK|H~bIL%Gcn- zw)NJJKKdNv=o(->wV=z_bSqT+@yK`xcc3l~OyuBL$jTkFQfj6SGAS|nF`*Z|T0ICX zmQ|M;G_$S?@ueDN0c2kuz zRVG0j??iCpu8BnGxSD(!f`vvIft}* z-F4ZR63p4CDtHr8=g23DnSjo*aPk-dUiq&M5;|ruLf0Qvzy2kNq)9Uu!YPOWFVt+2 z>^NDnV>9mqi4d+`nbEcSNNJg;WaSmooTmzTkTx2); z(hRbG_S%4#?)$rP-uG^c#k;^UVBhaD&Q{-WLn5@ExiH|4++LO43u*m=ynoM#{@1tm z(8`&WGAqwxyQn?-`50aV83!))vMgoioYi1pgj#e>88lK*qpp28mf$;mk`|{*wSL=g zEGGncCD|`Esd_&Of6Y?n9XOwTESH~c%qBF4p=-%2CUFCwBGEJA=WY7IrDpNcsTtyN zwO^1Zp_eeqrq7K(uVQ8c3tUt?-_MNbp220pyoZWk4q8A(}k0H z%5W7y|F|P9t$gj=i$ZWRLVqU-r`d-me}azhs~<{W{d2+Pz~KIR5xEo-iKBJ49Su@z zwS}6{IBIZtCA#;ENDZrj<0YotO$88;B{%1acL>MA)(xUED30g+?!0e!bFUiw7$YPW z)E>*hI#7y?&4n^>4$~<;mSxgromnkl`c9H$DZLG^1o=cY~iGIs=Boqv}W1!HEn+)$4lf)q&@IpuK1%4I_XVK1J(Z&@TM_s%3=s%|! z8yncQ{K>gx0);lxc3`9JAqcQ|oEt>KK!^zy$>WhfMtHU%;91r+Tk{MYVZat`1|NBUIdz*`_GA?i@x1>}hL>A7bHtiw6(?f{ZjHP1 zy&e9{F)M{zCta%MDA#A~yE%;Q^Iqqq30RV~K>Xp8&zMPLes*xN&NlyaYt(1plISwP zx7HEz2laxi zW4jZgOw?W*B0YD^E?L}?ok^bGwSO^lQd4OSZaP*Izz@=Ow!vc30tCGSK+M~gwA0hm zoEG>(E{o{^{<&vjpve8_L*&ET`HqIf{OI3mj=>&DaPx*Fw7u0eYn@NKqR#;d+NP@O zL5Fdji1!(>Ev3Z(?Je*fQbQv47JX{T_ji6&PEALGwF{C8Q=UqdDy%00-|C%1{VJ!+ zl-%e@s<=f}RF~>hsj$ZtCu#kp*^y(1rbp%p!|$Dw?*Xgm#JS@Q8wmX4iElG+wdDW5k-=P~<=|&38JwQosEHK{En(B*d5IkZ zm#ka9>*{P0Xf?;(SkQeU)54@sW`pzjk5Z1)@RdHhdo8IXRx@t55zl!?72Ma` z6WsKfZ!}jgT2VtUV9r+7#(edv0z6+wzsLpd!Q0xpx|T-n0jB38ynCDQnc&`Q{YIMF z5YzmWr7`lD&LKi73KMd6qZcxQD@us<1h;%3Vq$qj4-@$KyY1=h&-jS5_tKYdS9hn4664AKO*%r%ylxS%1aqh!$;7W9W z3jzxNA_lbiG%Xg@*EDc+zz5}?zmI^n?8_G^FwsUNI>~JP>=-rHcEC??FK$FS)2GE%^3%>G6g4z_);r^*G1gMV>PmdKDw_HLr(#E{k zZYFTMREt7~Pmm=cMglm@b^!9Uscq?)kjqGq>qLziU=fN%h-1AQ%mhykBs)@Q)4}N> zVDRgG>G}TErt+MNtt_~+>*0B0#4aL1OM9hEG4xdkZl2g;-}&eUA|)QymT;|)yZxJD z>@l0yuSZHjLM&p(^pPud&&u7CfLty(9%O|+3YjX96{D;#XQ@e8H}$oxF9;7T7^FZT zU9|#uaE$^Ze~@vrMjIli7mb+!y(j}afqMFi^|7c8)Z_sKt^n2yvF4n5ACDkajt}IT z_A0MkV~VS=C0@KtIbW*7Ce8k}W$y+(2>ACPnoQnY*0!vOh}%L=!q;_$clsFaH1Iqa zB5nvGe$#O6qyXvZqu$G_!lY9LuXM%nw#V4*N3%oPOT}e0tARIqb}eiEWW^b}s@?5* z2e`=@uz+1RXG=H@YJ*fFkQ|D5hBOc%;J9hCDCr~V;4w8dUH-~HKLMqp9XLcKKnK;= zXfcRD+tU<**d}8)l1idHoU_jFC(r%iBJvd_-D06VfIsm2q?K-KSqf_R`TseD<##q{ z_7mVySC{8PuKF%X**_;_aEsg_q1dtB`3?UQLVXUS_Ly+3^pnzVJ44$!!)8Z~H+DU; zAmAX1hd9MVeiZ$j7svSvNB{d|GRocW27v=`qrk!Ryn>`mqV@ax$189{>hqZ@fU1@c zqyeo2s!-m}_PkeSLc&cXrYoyzC&M~3p2Zg-TR%!B0HGtQs_hVl)X$WZl(d@PI04WA zkAYx4J5E}7#c@*bji0A`55r-l8QW{7nF1S|(euSu9C^W4t?f}`fY=HVB}5bgVgr_G z@&)_XZ}a(xY*0{8D0G~~t39Q`0G{BREo`&Lo1|i}m0OFFXeA-@$}&t+n?`IM;s8Kf zoeEWyX+iP~N?w8uo6R2%+yDOfeiy<_{qAFS9t%5D5}H-<;uAyUH4T%#jrs|Objq%C zmAqi6eOBl%d_N9Q9$3=#@?~M${sfJ|Z(u&6&E>B0dl<|(X%7aiPgh^7P-UP(x5#DR!+R^GaQj;ph|QA15Eogm+qe>2uYCO zDc^P3VuwhkN7C#!@Hn=t=o>nH!&ly*xnkJPF!82tdZjqh@2BG=NyT!6%OckX9Z=>I zy_sSrBXu5x8$jVo*n=CGx}sFePe>!Pe%6v#k%G@uVhpoyp88^AEBm;;B7f4>NS1Rtjq_uPt5%KHD2!JB3rn%gq7%N&J&+{WL5(nSwhRi z;ix9Gir=O3ZgGj{( zOLYiZ1aDP7a5PWvNS)7Wts9`0df)I6Ir}?vv@?;-475RoJ9qA6HUL{Wi8sp)yLS#L zT>8W^QAEG=+hB+yQGenGA5hR;AZTrEZT~r)>+}4v321C4qdrB^Siqo{L@@3%w{e{G ztPa1}Fb_mOv+e2lpkyaqjikWcvg@tkfQlKdqLWxevxP&C0km5fG;1hyba@3ke+n=B zIwzDPmDZ%C++kz<0QKaj!{bw{sh+1ygyrt<8YjV28mi{}B4sK;mo6rks4ci`He_t^ zvVQBD+$s#riNh5fjBt;+2iyx{+~#;aHYQWaz%+xHCFw{Gj5vi2IA);R+yMKL0~kKd z8Y4MvOSN#|LDVA#Jwimu;Qsw-P&q>WM?(nrd0^D+=)}{*V-Wetm*{!Pu)VjquAPkk zCW?J@(xgq^HiZw(>F9@WCu)NjE3f!7b#B+XYwt`yudkQpFWlZo5cK`g`=GH#ILJ44 zWMz%z+pj}RQUlaN73SU~Rw1=)oBM<>)G2m7<|H8^&9{_4yn4kk7~y)h9w&iMS>H#& zo6rs?ST15V5lg|@rPVfN3p(YX^#sHSyMo23&SzTht<3*?!fL?q2d}NkTNYbJ;z<@j z^q4M+-Xg5{mP|Zn8W^*=L}8($dftQ_)(y+Xx*D&d-s%xJ&HmJ{&X|RmWc~c>^aP=& zZeOUcuS%c^3h>@gSxN2BOpdh-g52Hn`^$lKLT8kva=AQzJf==_n(xQW7WZ(tt(vQc zcj#4u7=p?m@Q%i{K^OCmLpk~Hj|(R7Ze7P>)-;J?r7DfnF2@L8G2J{OJZ=J1x!f?8k3pxHMb2nIfur+hbn{<;L1w@k}GFAVYeZvGxY%iMB*Pbe0L%V)cDnM@YetC~F0PoLeGKqY1p zuRJBMqETU|YvQ_G>{Mv^shDKCVtZ?Uv{$3CKX1jb|4ysra2@#j%z#0gE#e30{PG7x zt~t%ZX-$57fBz5Im6mPZ+~(!13p9cl{%e_Os93bCy;89*a9A~-+=_53uAATB#i>YO z%QM@OAB4t)B}iIQVH`O!nRGww!_sDSHQ}sDe zEV-u!W)oxrkM?P8OA1GZS0b&y=3Sj4-`PEUGv4}Cd|WaJtT`cH$xf*|!M^JFZrNJHu43BB zC<)U!02IE4D?d#yy#D;xtiZn`L!VrD{G#B>W;aU2&~}@ru;4ti4*9bL;D!E^AuwSl zI{Ix?QpFYv zsY6}vXYnE^bjZcBgIJC>4#R?{)Qp@KC?W_`+4tNm{!}$unIT3`X?y(n6I;#SU&Dhx@(gb! z$Lv&Z246Z0N#6#mm^7isI8hH>lUFQ22txiH5(K=j9lMzny)UkN+&FLa_6HraU6YR) zT+nZ2c2I!deK>4dx6PAM~2S0-$fcn(=9#&w+5a%7eA7i*Ee z#A|+3Md%NE<(wgQXS{Bn*OGIT}- zugIKMMJhI1FKv%6>`np#pklbH4zLuRF_Y>HWG=P?P{@8ZYlY>obbVtFZ2881_%@!~ zv)~r!JRXM}0Ja1>Iu(ht3NNNZ$Q!Y4`r@P|-};m0PqZ=yx@U05?%WhEHOtRmYNNv- zPPC7d6odWzx}TLq_lA~3?V|{28sw4xpa?)Ar0qH@lFC`@?+9UZ(U>CjNX z{0?3Bg{KI^qZDBz*y`ur4xe0^XrT!Zho<$6KNwj{Ec^bBk{(o7PvsU*Jlzz=4v>b@ zCg0y9faFVb*4YM}fLemF0=`Dlx(T_JYKeCheUDc+$F+!0eUTJnygz=R2wMM~%N|xi zolHF$fC!g;V#_o758yI#Qsm*%hcsGFVaaP|+4Ao=xKse#Q})nS^jn>cGUM3JrDkNO zSB9`RBR{LOgEEzcjxK{$qVD-_hK&|0*?xWdB6*l(20ydVxSy|Yv!JUzQ&2L&Bn!1_O|8A`xtBeTfe2*9^EpZ}R1yfgKg_=gzuZ!6cSuWF2rwJedsEX@$4n zj!D>~DX5@n{!8(y7PN8yQ9;l3b@bPO)c^X{Um_;U*J1RnnWp-K$s1j|V~~2lVDoJi z9Xe)HGwjx0I|hClKau9H8P<9|y1vnDcl&_Fr#%-!WG~b&Xid=+rp$T^%Tk4zDF-Q1 z=}Kec%*4FyS#B${K3@xPXr;;Q4*rnXESB!rSe{kB3ZuPXHVdE>5Z=%h(-&KA#@s(4 zXCz~q$p&(E(|xbZ-HJ8;Vym2BrgXbN3Q8|qU4P!}SgEMDO`JddLtTm?eI`@ykj{nyg z@NW^xu@=pYUYob$<$hX8%iO@_R1Pb_A?m9s_3vqr#PBGPIiEz;4FDTi5Q75i8|kRT@166zLGwXzv*pEI4+;jyx_NARtPHp5bhK$3*!&5XZOX)ItP)s;GFHauuc?d01%hS`d z2`P~g5*n-%e>GSWFUOiT#H@stL`Uq#=Xh~btBRpvQ6x2KC~WfJ%vJoqeuavFspRskQ&l?IlY8LYU&3Q&+Ct{f_^BUIZMuE-i&Q|Ff+96V9uhNDt`iqgLV)Hqi6@wn>#rS;hWP_17hzB$J{pAL znGgXn1GaBxrW_5511PnVUc7kW>_bXCs%gr(lq`BxTQNtj8Ee9>)SfD*$U>R(A*UM+ zxN}$@K5)vzo7nH3GVScQyj!plO{tF$r^ox7APbYt=r!@|j>piryyj)FDM3r^CBk!0KXkHU{fp%G-)?G#oLdt_v$ z5`qFe-4CPO4+g~V8uPay!druc_+pDFXbAfDh$prhibPmR3druWH{E0FN40z)9hejk z9z5te?9XjqM;9>xS1(5{-d2L)S=t93Hh;ZU~zKhpnFzkNm>=;Q`Y|Rn5^HlTbs9_r-&c6*^X3*CV$! zhPJIZ7q;HB^>|$|U?4gU(QfdpFc1|I9rq?WI@3vVba%7Qu#%s({Oi|Q&XR+-Hf+CY zC|f$gLAqiOf7L-JzrHEdS{t&Nw%)xo=F`l~xFy{Am9R`W%97Kom=^xcEvw9o zPr$2dL&eWR9A8zIJ7yu7e>0fV$Y=#D&$`1}(PXU9P$D!RWm9_kNrWre6>-(JaosOAV8aWP8I0jjpz{8BN_fH~zFgszQj#y~XHEE_$J5?wu%S#jweNXid=5rZNE-sn4KM=CpdboOuJ8^J zyv`sd{O409P8W9w%VD8)_2$bJn7dqRMz*q`$*8Q`l>}j#Y7WDf*hUVxUmbGe9TDIc z3G9E0jkk`+Tqw+pqbD>(og#*K>>`8|yagXv6Rq(yiBgpYWkiVmkX&T&V6d#XOP8Cb)Mh7+dG>kR1UP?k7by;RX`biRhW4Bp!>sG1TS#Y==Xaas(kte5`-%+5w`how7K5YVSW*VunEckMA6v4W(-4CA zXfOcW3QW&@+`|~yQ)c!ge9WXo70iw|->bdAp6Llzb%Ysn`Q67~aVf$q5NM0+Ucz(0 zq{NL0nk6H{mJl+W^HcwN!r7e+9tLm$Lo%WWuz{guhS;mQl~*8=mbM;?$*|p zNGvn?f7x@bj_344}Pd2QL0YOx6Z{Qj720EV_toP$X|0boA; zJ!63k7eCAQ!FV|F4adn&iJmmYz)oIaGKM978g5!LSY{WAj;8}ubqJ&CfM>bj%5`F! zc2tVOsMN#h)D^Bww44?n%XBx3Mnm-$ElS0r&xH2fGi$2^_iQf1DHa2p5{h6tt==;% zMt>f z@@c5dj5D~Ohwc>jiPspjCFqzM|a0D%-gK-X7+IZY9( zf?<@fRaI&gz(gZ{Vj=+ujT#PbwYpjv`KCmE(SY3eesbPQxeG~!QV~E}UdJ7KAovkj zIj}@DL%b#jB5rrKH`lsky*3n)DG{-b0O^r+8T>?(Adi*7aS(>ZCP2VhP-U>t`U_jl z#A8CC<|luiMZw4hu)p@Z&09oxNMa%50pCeOLpT9l1T`8d4pe&VIfc;hkJn#|W3%RQ z(=2^(G;L~I0-Xsw_S*X=Fgs+=UI4R7u;}RD6SS@GB9IhRz6X^IZc*(guztJ5wZ5|D zVOMB5S}f6nxlfhI5E1Zfy?U`lgym8maM>LyE=ro+aX5HD7(8T0b2PXNCm$cwp;vl2 zQ1_vToHuP?52M-rGMI*AsDAX&O`!AFt~f7n9=!CFY4B3Gc3b09=6zoFzUr@8qFVDE zNeT?!hQ<_SzDX{vHMBa&TO&P@h~_JAM2e}aEFHOoZM>%_RxGLCL=m?zPDLKZGi+F& zT)i?QIC$Cx4r|ENUN`p!XAp90hf4a?@P4?}))Gi4Vvepq1ZO*ue&NT0lYqqeMm6ojECvRxuduRO*CJ?V<$jV&0*nvrcPCvAAkpgkOhZ2)XBP~X&r zA%MXNBn|l>=g~=mP^NxSGNn;TyGVS1r7z)fVAUsM=6$-?uGZXzM-o-q-46dz%fymw zShO``O|`XxuT|VI-5eu>Hdz%0go6&l6_x&_m&jp0IyX3V*}vZAqvE|T^X-Y^5YDAo zKP6m>7KCEjj9}zje2NyI)>G|*ValP4lM^+EFL3F(rh0*1WinbHhQeOUP&c3t&iJ&2 z3{&3^3pZPNE|H_YH$1-3^xK!8qo$H#B(tyHr#vsU)C z@?2(eM?rd*4MHEf?Pq77Q8na4MmMuK|3*>RiH!8+nb)gndGEW%W|QxihDJx^WNmSP zU8tdF0!FYXJubs|(^iL;`|pF|Q&z1sTFeK7@|NTDO8CD-i!zoG7Z{{msVI%3tERhu z$J{Tlja|`x6$sfndIYFve?__hPzNADz5FHgpW({lFnf`bJ)}PLgwnA>$VDEA+aelTgyLWbs~XN z(RdMl=Eq@9FO|2(?>MB-W0}-pcyBn-dAiF)M@weE8gV-@>Y1pwV`E+=6f#A?n8cee z6hh%&AP$=-pGv_DghY;#Ph>eXWQ>;~EkKDom9*DanRMl8!3?Jk7EzWm^6LiAypNyH zZ!$!#G?YJrphXHy#L-_BUkAw}f)*<6MeMikKA4S^gxs5`7lR8WVtW{Dnh<9xvL3vN zX@!mqGXt04$$!soI<|NUhE^#{8$OPS;nEy6bijT%cywHsT?6DS^6}b}Utv_jk)Mx) z_5LiYc5LqX6Y%ryOrMiGMMvrG!u%Jm71PqSN5_h9q#TyA@)ohRI`Xg?d&<%?i0a5i z{~$NTKOc@<-goBAe$16WFaB}!nzjCg<)KI9pZmhg3>~ry4eRFv!#c5a-T4)ned-?l zxunJ%_s72Q;u9CXB)W_haujE#=SWMhSeRxV%PeKdO4;C2w;7L{y6)k9=ju@sw>{)&h%bKqFr~_V_ygeMfmcfyk7LZ5+xYV#v{F0@e#)zBrdPLa(Y(22p#dktK* zt{yCLJG{v0PBCnuKD#DC=wfs?@m&UyK0Ab~gUI7y4>rhJo0BXB#lkAt8ev&ZkU96T z>{2hb8<&Ad4VVsw;sOeQMMT?px+^yVW}|@1$sVR9PI;4CLcnb$-U+UZ1lHHr7d45r z5pZ^M9e?R!~A_2i;43lYkHeH6bq!Qtd^TWug9$Ji0ReGS;f>NWvJuPod zXJ=bZlGBy}#yi!19dF7+{T=RoN-W-gF&3|J39sIk=A!yV{=#=vMGu}=-&%RiB;PVz zTKm4JK~zteR+w*B9RBeOb8^_zt5;4Y8*&kQy-T%G%xv#G(-IJQ$iyiV7LRG~Srs>) zjMMrJoR+4RPC&=dVnUZY-d-{8{jp&w(Wx}FV_M_B?*y!XJrNC(HIrJ zsCL##30!xjhnuYnOqzj9RAdhG`3_U|g0DFcG7K=JBV}yNC|kWi3<;QM`o707`9Skp z%H>*>BAj7Pi`}PXy6G%-ztrVt;4!w@wDIVe$)_~q{^4@dt{4;r4Byw z)L!g9%ehlq|N1944Qb7c=CRHUNAW~Q3ljyMO5KM7v3N~=)3g(2U7cbU+Paxo->t_v z+*MbSu#4_tO7>f-I&njb?xVzixSCJbEAW`FC`g)8u-$)IoRzB_rkyZ`_%p8FglX$_+-=`b;mvq_W;7^9fc9Gdp__9O&Z&}bTE9i9H^;@FM$ z)Dc_`+K1GgZ1cb{7nPJyt59wt-vNi$&Q^6!YH~7Nsg|5v^SGmS18&al0&=hcUzh_& z3gemQX*oGEgVt)$Ilm6z9XnT=lL@GEa|0+bSL0vJ5kbsNre8Lz<3vMH{5#q zzl$<^SQOx^*1~fmOjndQB-HM@$&PhYf4IysljKjayWdwLg}<>5bZF@v|V#Qpi4AXKrGe6**ilv zIu66`lWD4i$*p5K5Fd9XsX&NNQJg~XX7%TAlaVL3C&)EZpkBqJKRzfL|Dy016zJgM z^Ez|j5$ELQo(3$|0VLB^%mzf=d!~!w!o<)b_DL{HA3|G==&fden^rw`YXnV%ajbPf z16DR#0zH!uv&lcO9sq#cNzFpC1g3{1)Zl>8f-SF#JYvW#Spk+rq+Tz>57J0ZPTtPw z5_~7%9&N=}mz;jg?sP06tT|tE(vv<#Xo^cKSlqHceE4v@wfon9#hi?fzUEB{>rTHyQ#_|GdOopGZ zO}R{&b4mc8%cILu2GMQnrXpBYO9Jja_$BnASiv1`wsQA&{a}O z0(V$7-x$;+L6mXZK$paa+EWb;l0fNe2`mcWA;Syh645-`u-ZKXs7c z?i-l85y@+3WDDcME@+SD7J&vQUuwM&{dqvhKT0FHIQ*IE)LN}?&*HjtX%1uC{WMd9=a-wP;_vEQ zXp=q&v*09pI>JmM^Jin))>bZ41;W%E20E+xbDXB3RPH}+Z2#~PZbz6O&4z-UQzqC7 zVY)=fYtv#zZkK&iUFcB|P3~jte30UB?OT;Yi5N&;+E5?|J}~;z4rRoQU==|5D>(Z? zDZ$^6BqlE}Z@bE!5)Kqj_X{=nT>21w{;_|rB&oUc3K-F0b#T@c;xL;WZdn{p zXgo=ZO3vp?8RIhTUn9kexI_!xG(%uZ?8M2^d=AfaH_n4^eX&0ahHutZt|t6THhm(0 zF^JHIAF$kn!M@-al#UVLHLRiLwN0`to8>}-BVmTu2Fy50&&O?8+q(uBfy{!Z|>d4M;EJ%VmV|yt8(pZL6@}Pg{tdWC0ig%7JIg!Y$X4~ zkU(RZUchH6`Q-8saf`=>P-*`w1i->nsTiWSJrY7A`l zuWj_^R8(lAAxS_#07#AvFa83!1lb}_aQ6m*%3v1k;vSa=)4%Xc3P7Q!ZT^?;P0i6-)9_QXAQ*QMLjN$EjOcaMn6T!qnVbY6rbDz132rMfPK3JEheyvM&t@O*4-p_o3Srjp+k9!JEt~8X6Y+p>z&gXuSBb%ce zh@je-{U49Ivoq9EI51K=asc49u}=af#R@e?BBjz|;qP=qZXY4$pr<)%tJzdESZ>qpkN!F5d$5uJVm_k2gh zi0a)t@u0Emq|&6ikl``;l>g_xHQ>H8B!fHP)uK~Q=L#r%@cL0tQpCCnSTt|E`%LEW zU{a|b2RcRk8*@oVP)vFjwC+I?R?9TWy zXbT*{K#nLuxAj0Uwxb1TU`KclvyggpLq03?ur4A6Tox^1o0dXv@JdnDt~|6Is9rh$ z4diYaOWwvS+Jf1-o6ha3fkFYI4#KOu-_0FAVi0Bt7V^{4FtM~wV z@~xga^z<|Rg;NKaD!?3#r0!A44hhj+cRt4w|;)Qo!rN+QOZ{ zmVR8G0&e1Z?iD)`W#t5}Ci^bUi#?OwSivP~ojq|IFASVu5hJCT*o7B0V|IwO7UF#c zcg#t^@q27MiT7A;D4k9UIKGoK4c%!e32w72IR;f;RR!hc;4A=*+ud;j4(N*+)0m%A zJcp`(_kHIe(*WjWBt=JWbN`KbzaS8pYQc~9MWVaYnC@AC!3W>^4=RT0nZrh(DOc+l z01?Xtmt|+$nboPGCh7UJ@avW7 zC_}00ui-4T1>L&CY3*S?)AWe~(ytg@-`d%QgY|VIPX_fF_t%TSsR#s%qyR+7V{X@>DP!1WYNmN8nTkpNCgmIVjru zFSO4(7nu((&->$aL>eXEu;B{@lGW7JQl@CNvV8_Q4)Xs>`DY>jea49sp;-J3?%ia0 zyg?gh^){(J{m=IgO3yk-G08>0Lz3HJf(-%3(ZT_UO8<|5+Cn(j#=$HKKH`E1sz5$R z8cyf^jz?iFEt7`~3e|La=0k~2TT<{9^{42-Y#T?e?`w zI5;@8+}lCwd|Y2&KOsnLZ{z=75=*SnAL;`=U)P;ycJJwQKe)W0qz{QjMt3rwYtQch z@kAO=2mEwwJjUxdDu7Fxw|ljGWch^0k#;aBee${^^2c&5mzUB3Rx4@qRDcy(`u+Lo z4pjP!wO3pocd9mOxmG@(^Ll{zdwdO|Axz0Q^R?YX%uUn6`|{c0=2R!L+vt9T zPQ!sGi7ZkC1S7{A(pz^?&oLCKYay`!>FV*QtmL`?XHt(NKMNu_LhQ*z0Z$XtlY=^L zRc*?9E{7$mLRGM6ehT#w_x{vXL+@*;y=oS5hK2eY4y;F>85LuQX~~bE zo^{EmRNeN{iPku*wGd6q%z;}yVts{F#UW)-5ZyFJT9)8gfuyz*so+_3T?g-^6iFTu zQbDm!zBBaHX>huNL`_M_N9Q-{OaDseUBGAL?dLX;GCNM+zcPFzM>$CNpNPc??93H_ zM-P09-&uIGtY~G+VNn(>x{^V*^6cjalj3_BWdATBt7SSDg8NQQoKS_+YWaO_jB{<= zVQII#QQXbSD%nM@p){Yt9FfKVnV)=Ew{WI5s`n4{uguTiSsSB1eGohI!NS0rw>%E2 z)4MWH8j$ucdBljqYc77qCIoU_!!=@FQ0;Qd93C?~7q(jBdZ! zKOlg2;1@CqY5IHRd^|WMyOTIBJJ+)gvl>)ph+CnZy7(9x4St=IxG`Vg!*&<`SxP_h zOk%v6F9pVEvUD~tn5qD#okf-rs0(B?x>?jfmV+Qu1m!u+^}dgZp%Mr}=1xIzkF^tN z$I}8dZwW=y29tvY^3?Gf#X-5^PzOgpEk6I&O>=^eTb~c(D{T|$%uD(yU*m7^_q@Wo zqH+|3>mtRz%l6p;f>_9rck^yg@6OJNAUblbZ_3LH+83v(2P?ah7XV?!eJPb=gi%M= zeI6?aE>&Us^NTH}FeL699P7tFq96!bB66{3=RB?;hb(+T6#fE^;-xi4kv-*Qda?$? z>HXF%kkstFe|kZooPB_Om8FV1eRAt&Uu3eGyD;NPuKxstT0+;yGOVPlp|n(IrljR^ z_8;)Z&u+WG4=S@49hpG<2}q0vB=1B%DcXf1nDvXc-I=vIZ z!m?ZKu+}bfEK8bK4_+QIYT~nQH497t6M6F$WqA0PtdOfAOWb&gn5qFSuLxc(NDTmH zksy6c<2NBL4L*J*01u5H8kI?Kzk1MrT^0O#1=X*ej37sKu0c+=$ISTHwD*v|vikP{ z&G$}9M;BUfVK$bj zr;T;VqH-!)yUxXa(i{Smd;rmq|4>D6VFv9)5OlAh9(>!-b9b|tYR`M z<=KY%vjZ3B(PNI@O0In&zX4|(_f?UGwbN7vJ*js?GPcKBrCEzh1LH#?rZ3q{oea0D zrWuFNHS*E9_G|Z-O0=)J{Ch>ctu(z4yWd7i=LOJ>L42*L ztql)^RpkGN452*KD;(kyPt1R|n{2SMu88(Iv;7dMk3I;r>#rFVDJ% zpSi4(ro|l82}ivMsb1;w+rw%;Oj77m0c(jhMmj+^E2?C(C~VbsF+H>suNbe&=g_9I z6x?SGr@LK01P?Xk#>fOxw=%r6wcuP;HXM+QICDa8*%sJQ@$J7|VwG}IY5p*9jU_B#;7m-yZT4gxCwd-{Jd4CotQ0Kl+mf2QIY$HkM;R^lHp}~G_fV4fv zxyZ7m15jz5d-ypzoiy`qn3AlVDwo)|-wHP}M{lj60XlfFV1qjD3{I3HV6_*Vpmwv7 z+!}G=r<8YjE8vi}<5h*EIHdZ>n)>H+=_Ta!kT$t(zv6dW#|HY}&!#0rr(JCb2S4Od z9I#wRVt$O4VsMz;&Smh`YYU3BG4Z(?D3T<@t9a={qhaj{y`r&V;Goq9)UQk{(&jq3 z7pQaeC&gf~_VEkb8#`%Ct(TCiRue6p4%r5->zi6X2BV8t15YnG2U{l_upW+yjh!g9 zDbg|kZ9mL0$pqgXv5EN&gbxB+&}onssNcEs*{jIf(h`IY$hLEZwe;T~@ihx;f3pTQ z;_BLSY(K0N+p3?qw9>Y&2Mk^1q*a>J{lldng$90Bcfc(aj1aQ~DBX9i3R<4j@NX z84BX^7Ut~7IGHW7FiC@fda&RRr7F2tuTUaZ=9#Q*^G+5{30O5T^0Xu%VyVwh1$ z+zN*_2cG|hHX-^!V-~yqG;Z#nwUI!N>-L-_tv;v(tUZx~d?6w>`fzRjL@(wS-6c9- zP}UonyvpWmF+t#mL~Buf63!@L%a=jIHgEi8exA$Nd*X2SC!K!w*RA{aWN$S_WUVd| z`ajx#*g2ed|4kM=OIaH+E4jC6ZIn6fVXP)w&AVMli~4kj(<{We6Q+DA#NM>-=*m!G zM0^vU!(gvu_r(egh#32^18dL|9ZoC-rISY^;RYx}kWv)Rnz*|@j7J1gh_p;g6r9z8 zDAWR1r5^C?0z(pTzg!6~f{-+RyeYpBd@=qvZ@D5uMf{?@(ji#!^NS#V*IHZ&L--V4 ziCOX6k}1vmqLJbr_fht47;UobaBC@5y6 zq6wc4KZG%ZPX~|SJg6MKGIhvv zz?mHn5Il2dMb-y%`A-%DWlR16GSlN?>vP~@ z`(KsJUw>lv$>fiMHvPCdC~o4Lvj}}j&icApuedNOvPSN~%g>WSpk|g1#p@)1nCm)F zc|)qh&qqU^zwfGY5bmJpbPvFpt)i{{CFgBSTwfigah=nov0ITk_B=z#saPPX&U@Df zhZ&}4vuU!U1|L^LDZq_pH678?5-Xu?aVcL&N%ceYZr2@9M`)`OIt2m=7vRx$7?a*> zw2QdEB6ApF!jUB6L>mzqc`*D0Q4dy$S+~r51_l?&QGssZe^~eH#UH$VO?!CKmz ztrQizx%}Ey_NKW6xdZ{-x_A__+2yLpq@G{()+W{a!T}i3G@yrbkdt6v8bKcUlJDn6 zUJReZ%bmjpjlneksq&1U+X&aV@PK<<x1qzowob2>%Sb1x1`2tpFafX_OrJtFk{0m)tDDk$1`RjBM z>TUq9AYWkRXh1ASw!=aXhT`QysiXtsdCUj1hE=t-V@7^K$)~#aeQGGS5835DG2`PWl`gi zHxlnx5phm}duy~$>hHj51_r5Snp54t=}t%lsEIg_J}rc$|Bmh8@ICw7n$Qm>^8ZV0 zd=mmxfbxtV(*Jm0)_{<1Rx_I?U;FJEjq~IM8i#&{2z83b?;=Fz``WW7Y;8IsQ7Dnd z%=-~$J$_^S7Jt4XF?vXpdPhTr7trC-!T`C)6&C_Uwa z7I&b45Euu0ln0a(Y_!4aB{=|w1{b}e7MLJ}Q0IdmA|f{VKLE$sI!B4a)#L?Sr@D|O zm0S4sP8uwvY$TvCwNT%9$eS`3Of2okrYbcJ_h-|@y5U@FHtYe4#`y$tUjA`63UdmG zquFBx6I_-NaIMe66qS|ioET04>G7EbQi^lJKb}?sKa`2Z@|Uy@IxmPYlIrQ=84*7; z;|j70yN1V2b@9yLSsRZ_MknVSPHVyPDB_F zF8z&*irXo_XuQ8y_@^;#s{X|d(u4!qwwqx9o)MqA2e5jye*Pp z5DbEW4sZG2TyMEf+5e!G<42lm?2c{Mzi{oua~w}C2GHiXa^(Y-LQQ3E(B|{vNm*X z%v3w}*GgK9z8Ucyl!Ov__ky*&o9SmNj;(dKS2{@uhqmn4JQLp?JUqcZKn`yRi|n}S^$WT~F^Ae;#WpwAnlx7_Xdqx$>5x>#;l1 z9ZBA~c)CRwCGV{n*{wCtC?-5z{f4;&zr9DvS2;{abc2K=US2UX17lsa?XgTWHD5`& zDndJ{xAv7uC9=}3xT3P%6(gDtry(5EY?H~bbG(|a`|4#t$`u9IaBl%jyfNs{?)~^lm}ZW-4cYE6f_nKK^c-&Lu65qb>q0=lB0Yq{kn2FT&5sp#%)KO2?>nzIb zkFq}AR#YEgLQaJ%waVxp%YKR;Np;7}H88|kD7|(_oD(S=b|b02a&T^7!NPlYaeptD zVdk{K#&hs_q%s1xiQO9C_ngqxl~9`o~WBcr0(W#7R3{Mc_!jv<#^}ltIdd5zz8fcjR^u z_&xtC;5|*iS!aha1SSZl;FoWNu?pd`=sw;=VciU z#z>Q<6%lrLcT#Prs0i4NxRI{Tb`Ua z=J+|HF4IbCao@@P4y+wSPx-aRCD2^Yi zy}EfswALt6*VHy2=2!4E*W@2C2iGMyos9`(EChx8GD(~(|o_72qAUVCv6&Ewo z1O7DnpF<2le)8cSR}N$vjQBDP8-9VEF85*mb}%ai0KD1uDhWkBr;_`6wvZX1RpAeM+=(;xoK<`>o%&ykOiE2&h>F4A( zf=clGi5I_4c^26l`X3)Y6J7FCoHiBP)&fB!!ZsYcsQV5W45l=;x#eNRhzs(t+iLQ# zQKc^9QRD%&7XoU>OKs9(e{BPM>Vs`S#s6LzT?px|ep69)F0mK$=u>}TZW3BpTG`UU z{cAr|N5o;5bd|FODM1STFu{QStH!iZq7WlzXq1bLAOK_M*`P4O2H>LcOGEnoIO zsA#$5PJm5WFxIq<7G`%LjOzZX!G0=mL*zOz!!RLp6#BuP~zr#CoOpvGTysrzMn7cPN?Pn18M->!T8HL zjojHUS;3uf$dy``Ic+-a&R%Y#{@AL=A9$i0R#4F#5qtyB4+bj;2@W%yWg3(rwT)9 z+i9FLV^H}inV4|@<<=YaXTZ>DWn1O8p!}s zVaG3hl8%H$Md!*CAjo63KU2a#^koqM7uLO|v~qMRCGMHH!r|gyXH`*U91H#~@BczF zf+e1v2_Kng1#~CW?hVpTqx3XTLOO+Z-JH@Lpae@H*1@7b{s3-z^Bh+X?hFbt+wt3& zStsc%0H+|40|A*rD?`QkgV0bTr*;n-EknSz@eL)fTR6h&z?zF>@Ekd9Fs-zQl+DqF z#tmpyeyEB3{UQ0yN03zE4W}P-g%ut);UFQVtDkyCX{~<0G6~h>d_8(fV>9HMfI^ZF z6sYn!B-eM>9o#nQ*un?|To#fUK^ebm;2Tq&Q&&eX2L2C*Fg=EuxM13Nv?vtEn~^pE zB*=~>K%Mx?bl6o1Aqy%5Sb>NrWP}1D%y=5?>$!fcESM1Rv?Qp(C?*1$w67AJ9A$!+ z-}0J5C%yG>rHER~f8nS@0`)kWisiO-7@uOmtq0N-?Iou`r*1w3q4ODTao;11x)BX5 zxAcaAUzSEc`a>4cKUfQ(Q+x*fe6?u^-R`F?(c8dT{5j>ZavdT5<6|$cELdK~8crx2 z`?*8K>-w6(yd$()O`!a?+8`{%BlS4y?s+Q&aUM|M25thl_|6Hu7?ZT%LN?2ZZfF4w zp#+K@xW-v$Y`TpG{HqA)>$te37Ra$)j^$fRK6}SiVsK?V%8<$eV;ct28C>1Oxs5Hs zH>ST<9)rnAE^rj+hU!(J*RC5_vSsM>fkx{pY{3D$iWBY(8Ca|u7-45v0HrA2UyO;J zVLDiVPwyOLMT6D^O=fAT!5d+Xm0#sD=Xx<-pNND)w<0H7Yh~_nd^@w^G=2#rF`xYE z*t?w>uw4*Wb}=(4QFj``Sb5y!C;5S zIV%@F9Y@0$mK5r8t;9q_(!Obd0?Svpc&j8e3xJy%2^(_as|8-~iCjfNok2-CDG}}g zg4}*6?;6r^heZ+95tm{pFF?f*5uv#auEP+= zNCxOzDZtw!xa*JKkMbFQdHG+F`$wf_monfbz$eEfShhQL)lWiVYib(cL&6o=ELX&| zTDq3|k%6nTMY=?HkF3_iee|t3{$-LM>o$#vLL|uJGCV0)>HSyR+Y-_<3Iccr%d2~I_I-j_yCZHTVDiGU zkncnicO`6y9&BZX#u}b%!x$xaq$U!l_)XU8t8F30r)~dFmSUk3Mx?ImPKxTW-1Uz~ z6s9S2qOf?ezDAjZw^9}v!|_t~=9YeImfqEJ%|nq=7A(pWKS(1q$XU-1>?eo6s&xhR z3pK)(Az{W^0L0&j82q5xEVq0cQGlha@GB$jJDAd0R*~J6&IS}z0pQ##L{v!n>sn<_ zISHO?TGAbWNJ1JXVUefA5li!}g-WbaW^)mtD10ieP>je2x_{ZK#V4v)FQ^MBjIzvX z)^zCX7u~mvw82k#uiR;aV?$ph3)JIc#yYJ-5tu?#_W-Y-@yE7C6*-q3JMJ3n5-PCL zV*9gV({v$*@x0l02|4;r$@5F&_CtpsJ4>5*E|wRoqaEic!%Sj!V$U49Bs?irE(Kj> z{&jOUL?b1(C7!2}kqDgGh%zk0WI_FCHNF4`WW*qb0*IC)i|mjq!PCinqzNmCJj-4) z4+1f?0nj+=q}=UjV!7D-&2OsL->G@aDII0P`&Y8gH)oWk}GxrTW&Ny*V* zVzIs6ttBjR9mAkuKTxwCzs=Yv6j`<~#k4lB{h_nOAiou}&E#Z1STpr){!%;vW!}`Y z>b^#(GVqu;4}7bPb=H1OE4FPjyqZ^;NI+>hZ42w;1J_uHCS`g$m)8kZP!b#7`YIB& zqJ7r*h5vg$t(1QuWUUT814LU&VI9E$RC4$cTCzija-p;f)O}YE#4ym&>hvtMgV1la z_$)t(gb&L<_3BZt#Z9QxLKWm%o{5H~{aR!tsq*^7&sf%6R#@wgkZ@T0?XG=5n<=e= zvpbwN_B`#Woez=tcW60p)Wds3w-I9Q)hoQB5#taVVzdre@Q0`A~cg#bQ)xYab zGv!TFB)xOYz~ayrTnNzim$Zg#Z1hIi*mjM5zA%Vz7%La^6{D=_=Oc01IrH1^Foi4F zKy8m5O-i4J};;C~hJi$s?V%nTo7*3_is3pUE#}P;_Pr zTZi~GE{K6_7#C^AGg51(%YUmd!^`wqhDlp11>off)KlUQ1?ZxHVl%n`r%J-1Y<}-q zGao|{Jg+MS_GA27@Wqi77(WG5ujKzOAb|e+@1XS$wPM~bOT>EBD(vE&Th;E^>F4?} ztMiPc6des8>T%v5@?VrUn~6g)L{OXV`yExj);<}!@7WjDKzfNm*Bq_~3Q}06=1XZ% z<$^tt4xIH@g^XGCy#=<*Ffc;#(yfQ9P-E4WhAPR8veSn?#cqWSblBb>4KOO>A%TrP zpKbmPAWDXpMgWzCjtR|`lVvL0{ytEUkt36U&tF49hQ~x0@KGXtsadEA7iKRrs;`G zG~4(3A8k$G_gz2j?c#H$KrDA1<@ybiYr0UHGh~{)FubM|)sAYP_Vg6%zbZXH61}j~ z80KmFO=&S?M7Y;yZsMP$Wp_04!$c|O&&@j^?2AU2MEBE0=^q&snd3a~7VYjiPRV9! zsbrPV#l0_~6JGvEEEvV?(AcTPI*na^MyYnCPt57yV5AO^?abP7ms7pcv>s(sQ}-90RG2&&7;KW_bW7)D5L+ z8#s=zYZyV`q{g(~{*-hPpJAgB@Z9wZP)j@6`D3Y_9S_)Lcp*{xM0;sU7aEE*VO2fO~4?4^| ztyPMQqdWz0^mzWxy$50DRHt6#UO9b)_^52i48?HAngd2a6jv5cM@neo}V6N0B)7bG+sSI+1U(>imV9N5+ zjs?hXyG%E~wVMFaHtMvWd&cePICFUW7NCH`R!SB$AH9%1_JV~$3EL^Bb7lAQDJXNo-Gq)vmp*G3TGh+*?AcTbBf59D?>s$ zK+-vRZV{#|w&L1nwyy%)sFMJs{n+n?TdMa|xH zd9}XJ3b`8hQ|dY;2K0u6CUjlM@rs-SamIH>XRiqsfgrZ(kjk~JV6%;Ad+835;e=d> zJ)J_QduHNfgL>eZm}hZy1C0n=7=Q}5wa|OoTa(8Ha2$zT>B$OIxj#7*lYvE%yOlfq zNJ*CC%)Et-nTmsFtdotks{3uHPrJ*@M^2F+En|Lnw2UFmks*3c{`L&V;aXqC<;sQm2V)o#^Hy^*RCl0h%0%yxiN?!{__nI8bA1)U3{yX2OP9ZP z@_ftedhb27KxaSvye`Uow=o{c`5sdX-CwN4DM2m{jn&utX*Gbtfbvf&w(?joS7+&j z5Z~kF^Zebf$b)gjF#|#QP+akzOI^TLr=mee(PCMlWCtwAnehDaNa+YFk-U6Kds$l$ zvTu5%$*`R!53C`#@vm2V#Pg@Yrzns1PxW>tRHB$`p#wa8p##t7Y8+s1taMSHxFnP+ zbIZe4f7#2sy$VmKO#S@<5~9kV|B7kfdVA9m<36+_*ylrs`n76k1-MesPcX_H?!G21 zxSl_2oaD!#w=ee7am;&wi(tR9O~>^O@3j*gegYu=1$Z8q$=k4EoU2)I2@VF8S3&`cyQ9;#`V$sp~?^7TLVlO{5IA zCnSZ1s%vT!h4o`K7Lja{+TGSbKJoo><~6+)zcJq4(ymKFB{Bv(4fo(V?dJ>g&rt~1M0oeusrOs zE>KbQBw%O@PZjU9)NHfuwdYGGC+%4=--&?g)jj|M%8ZN+9AT{d7LWGBblk`eGzP#$ zK7huT;l(gw@$v~F)AuXpT{SZCo1w`dJvdwHMIxL4M9RA|jNMaN%)WThOSF+ftm8Mk zrxF>kZhwH3;vgPoJu>EcL&~!M|{lSn>$oC~$g!8Jd80u_W~t9BN}}>QK5!vc60tk0??(1aO37@`0&e7N(vWt2Cd0Z6g4++b61)wT%!W^LKwaRJzl3L``yFVd z7F{b*LH;6_Z|4eJqtJHl6@?3eMlm=Vq`T%_)sR^Ru1m za2A2!n>uE;AI48dn!(kW)LO&I53eGwwai1IC}pEs-X`Occ8U^NCt#H*$iuJ&X-Z4Q z$sCrJG$R0 zxf5Zg!E&7V1vBOD*B#;bUz?Vi6MBKc{~!=G!YR#RZIO98D?>DNAit_lNf$5AP$q@8 ztPgy-#u7D}S9AHp=Cb!x=|9UQKy5Y;)h8L)s($J4k^_VS$}kDACT7B&FG4ZoL> z%dIt$6-}G>uD*S|uB8XA3a1Tp@kDmcXYUjx6NRsuq9s7$C)fD^ie{aB_RBe6Lvhid zOHk8INSsMak;jWpk{{(Aj00MiM)~a$-J*ikrwvR8cmjU^qZn=? zz2rUb+f)t#30eHnsGhX0){u@^m*$QaVy<_fFbX%MA{ZNQD0MG&a}cfJ?=Ef3!hROu zjIZMxNB>s&X?gRo3{bbDHEw~leb8T$`5`D)lNZ#t5zCtz}KIg5ZpE@P*qh8pP5X(YwJl3k^Z7CN={)wXWVT+B=%8gF#}69 zetYb!9^Lfom9fLY2N4KT(8`PZxIg7lpa-+2OuI@6^+B~}oN85>+P=$onqynhwdE=} zhN2ag&C%l;%|$@A*`LgN-FyuxRDQqGi_3|Oq&S%N?NK@4PS1Sk(23NsTy>wk{gw{x zgJph5n2|!h*-`lcwcs4(9cqG-tkL$+c*GXwB!?k?4>lt;PlT}`1+=#^ji*&_raVZE zI0l6WRdbW~kZ~iquYuRDxt5(Inr&0)@tr&mH0ebV481ks0rS`uuIK(T%c{zuxuf|D z4Q%mEpb}waZ3$aEPo}k%g#^3e33d^uHp~8eoO;kmBj1_87}bp6p(plfCbYp}DRwC(T_spmwZIs@*4>T$L*7BwMzWd6uMrGIL?MiTL)uz`$Kvykh z-SMHy6Ej7|v^6`{Z$d!ruU}rtuuYhs5uJpGT{du`?l9ODs}@0pi~zV}v7$g8gst3N zM3GeRz|iA$4KA{^Zvy=b>-AHsy<9qfsUaYNOMyO(ps~h$EAX_w2a+ZJMSp>W%6&>J z7Z|7bbm8?qV~Uf&uO2$_9)!hWFvONo8W~{$5|jg>El<ofD%o;-v zoYGCW_-dP2Ln>C5w&BZQan3b{4VX!8D ztaz|LM024RQ{a)fI@p6E+*-Pl&{;CHke+NbJhfMj90gFnG}$6~D=GnMrW{6AkO)K> zRAm0-5=$$1-@r9Ev^ux2E0Htqq@`druvtMc(Tn10NW6m&9H=;5VT&hatAo-^I*aI$K_m@GWJi0*KpKTKYBO=4w#RbcAS8i4Wc` zX=%+=Z@U{fg67jJIMP5FJ)d&}>ziz08EC$FIkpfb5g#-Ag@=UxhE5>ce7boE1H#=Y zlsvreqZ7RM&tdSNghxVMIU)Cn%H4LUa{Mx?3CA3@{qynnzhc0lzFdev&fY{yz!Xih zE@nVx6oS$Lnu)R|_4y>0qT}P@Q}Z~p!L3G1{S;b@BqN;_v?NbAwaFBQWY7cz^5 z_dhs;@UvJ}F+*lAV%cE@(NVJgQv2dQ;*_xEyYps#A! z)v3(~JqDW6&9z=8Uf8x2YKulf4JjmbP7}Acy%II21Tj{G2!c|=K$y-ptXNt9dH)#f()4^r}C^cppo8frXj+w=C^7orZ|S6 z9DTH+V5AWGK`B$I@pBV_{(L7zo|%EUON(i3Ax1*}xLTx@ZEv?rW^;EYp5wY6dsF+G z{3?wn-#BsF*HA{GS&i?J`F5HcbW0{PZ<}caIa%3E4~c1T)>uR7+IAD9v6Y@4y8|MxfjVKq?a52 zbdw8N>VJXY@Pr%UL}bz47Vs4zhR}Ch3PZ1u{xuS(uwd%Z5IfzzTJLQ0N%fqVMPra)fruNXUHVbCd*-06G^h zUtvRJeqr7Zp;sKgy)^;KkG|gKzgpE z1oOsIl)jo-V7ujn|C(lS0RNh}#xdoyT$e5SXIqICcJH9D=Ii25{elh}` zl0#fu(XKS{)T?vabtl_A$U5Nw(}DWWo3Z=mGnTOvz3yYZ@zQI4?w^*-OK#ncyBqmJ zz=z00Y9P2+d#3Y&xyds)5Ful;?D}_ z4H~u6Su(qpK@aR>a9{o-_o%TOI!ndfCb1P0IW6=3C8?oPm~;mqg-monZC!Mw@EUs^80`8n;e9 zG88IXvB&(9Ut5DIQ?Q8+?q+UA#NZ7}bX!!ff{(Edj-(ny)}7&f^rQvnDC4c?-%@w> z`Fi44E82!=-gvG>7|!kbE$TSuYa5Ae&xz=>KQ@PkeTxDzPm-e^4K*RJ=j(q^Ha49z zYn=(ZApD6$c6)8);hZyGBD*Ifbb5H}?!K-D&zT0gl_tJ3*rHnF4J4k|PHg%*oxc+J z|6~55aKGZ~g3wQTUaZX~Cx6d^C%yBvJNk$i&^1bfPStapDZdcK@X=A;*xWLZwVy(9 zvpED5N)s1fg&SN_y`v=cS-9%j{0;stACs7lkL?r7n`G`u%bG1x+WhYry6-3wI0useA=s?^VKk_7Tr(bJut!y*- z+;xgPK8r`MDz|>_3ukYg#X_P_LXnpaMm*_|Xe7wd_G&=gXDOPoZf4t$rqgsf{Isdr zM(^@CI|mel$Qzq>s`-QF9T{F~`rjivd9`7JlNeP|Db4eSc2gJ}^tpy}*DqSP*W2y7 ztHgGOijxMfCqzH0Wv?CcTg{`LP_J93A$*-5U+sPmIVE`D-Nki?D$Y4G_+3hSpVT2d zaq+l`6Vt=!G`Ym~CC^7CyaLI*l+|-M`8rWO+)T{0;a0P8k|f^kOs5pnlNFrewf8Be zI;Bj=@mN(nVLspApYGBX3G1X4N7LtX;rA_mV;4VBm({#eM|T_UhTHhDRxqSHvJug} z=#!xR?EyH|-9s8-!ua@D9yGoBJx_WUblW1lbtAZs(p)Dq+arsquTHaBW~E)G{8bsi z!-j(U)4iQcrloIvt@?XlvtC9tF{6_+A!G9#M~A_p zs~*CF*k^NkxmFD66yF2NTySKLzI1;5gOGu8ON@5Q?spRSC)8JM_&x6MA#N8e_G{M= z>3rT0Dit?**Iq`;#890f|H6f15eB`R{Jk6{jJe#`6WiwUY*RSW)J_*6dVk=euUBzL zn>qjOj#uz^emk_^L;)I`L;2P^AR9rafZ@u}V?cN*xdX0wHWjmm0zp}HgFlLYyJN6? zm0MbH?Ts{*U}-i%bV+kGaL&47=h@0ed(1hLp5(tCE7tzG-~{3{O8eB1h-m(rlMOiCS2{*+W1HQqG|CLO z-ijC4lFCjk&3Vx{TtP_eBcf1=!rM(~DqJ(HF>RcQrZn!I=FwZ;7yaIMA_=Y(@z~Pe zoN~|4ang#Umk82j+~d~LM1L@9i_V7L72d%v38&9$b<=x`x1`X3zu`Ve65qpm070}P z{08+qpEgyhK4i(<@)~24uxE@XdA{m)>PuvV(q7(SDC2$^T1_Mf6!)v0D z_M{Hf0g<>^Qf@7>{qiIOGxMj$@f{^YqC)c zkvm@=Ci=1OLW=~_JRyTb zc<3l*l!hWYrOnAF524}b9?U;MI%=gmVBlu9zyDr^G5BJ9l~qUo(&33n$x zX8h)nG@!Ayr}WeZjkdYGr=9I-G$zzuZME)3(K&hgmbQ(2Pq@vyQDSatFmo67p zMCrCpTe_X%?YF8AqkVR~5+RwI^;tA{@<5skLu|ywE%>p72GxX{t&fMBx!0ANP1%oH zmJBbRDPuV}hl#c|E^WmLkBG!^JXBE?u(>{mIENfG82T_f3AzZIMa6TlVvf}Q?R_K2!vxMq4hpI2ee6*zA21XPMkDdm~!6$K4pcGDjHuN}TzSs4g!b2%dGQ@leTG#LNqe z*N9C4RaHmWAVhDc^W!z|QVvFrLk%Exi%t%~th^Ic_m3i&4;Z`nzI-z3{V z<_SfThNGc?8Z`%cc)c~#!LxL^4%E=2=eE!dEPlB(C$GN`N{R3!QxPm8N=lQ$`p(^qt8<7;$q^}%tScV=`G~xkQkFy3pLuq9c z3QNJR`EGgnMdBAPXlX$V@l-W5m|5(2-rT*GpIvjN6P)3)z-LnC{e$cL@k(nX#w$VI zmk?W1msil4RpJ2iFE@US$PET$*KHn{px|955~4Ma9w(i$caffQK|S(uva4XiWRm0e1Ly`?jdSUbTNZX(p8%7)wN*i5wWR*CN<&~F&7VGk>c}3O`62-FRtzS+O6ZnwFt*iE;4S)#-wS=P6ir7z>H@sn!Um>Z^Y3Tt^@&ILCy{W+QK zsZ;`Kx_b{Fe>kP+t4sF*$VFUsR)lf7hjGHGfMxsH`vSlhuF0c#Jkw-+FJxwyUzz$k z^Ke6C?#we9m%xh)et!2Jz5{Br0{N*!o}TfvW!q<7#jY{Ex)3!%Y>ZXFcO~C9`Dn5` zRf>VgX)ORmD)3e~YG~8x=BnIVIXwKQ`^S@9qKPHWkI|e4yU*ar0!kk|2rI=$KSM$b zK`4ytVdE69$p*=$XBtT&jJP1JbJpv_(bw5c^eQAN(irH~g$|ljj1@y_3ocQs_H&in zrZGN`sc+pB*!F9)b&{6rBkX;z%OTAmyU_VRHQ> z!}%4K+$-tjJ5Qf)>ghM0m(k@tCS)LfUaI{2r>R@UTk@N7D!|x*{4X-gTVxM&ZNw$9 zE`wS>+je+8@9><~7KsnB@$ z7JQ{$D8;L`2Nw^FWYH_IeuywD5OfSM#B~~R3SyWs*x!l*ul#H;JgCvVbr9>u@WjO2 z2sRB17}Yu0=)F-*H+y+_!^cB35u8gzXuwe3w=edk6=nUj-jvuIHvQqWs-f+e?2VyY z>KjMWICtk*pa-w(%_}+gpPStRgLS%#i3}Ah!Kqtyix(SOM?@#LrlN_vVfIzQ(CGQj z*J~~RQD{Y{d90`ebAtOz&f?MAK#bAK{<9cLX`rsl(#jzH*c*19%6Oz1`rB3Sue5IjsYfDT@2wGU6*_I6 z1+o-Gk!j}X?`|uqJP!Qk?KPPa7kw-h%aQ~-?k@1hE(tP@MqFxQJUd>-6`yEa-IEsC zHX}s(1aY?8eNb%j6FvL2jr-zu@5M)d{P=&xoe4D5-~a!WN}ob|DqD-F$X@oL6pghN zWep{JmdS2tp^|NcRF)#LL=>_o2@#cLvJTk|Gg&9b@PFN@&!_tQzyJR^|8xGo-)~Nx zIcJ8q_r34eecji6-TQhz){Awl88qiPJn^o<_xXq?U&A#0ixI^X+_eHxF=OVkU0M5b z)zY`xUiuD1F^37dL#4x4$NjNV=5wqUtAe(z7rQv*NwVESu)cpQTIRdgj|Wgu!jn@< z{PXT$fu2)bSyN{z;1@j?1?Kj6lz|Z_r)u(Z=&OmZv%^^&s?nY|9Gb?6`q*FdT}+H4 z3dLTGlhup%p`6wwogSEz?JQ2#Q!blL#gHyP)L;8?deOz-D#BzvgC59JL}Qz~x+6YB z&L>B-_cXSq62}NKajTUw_q2#JP2as+ex@i?n25Vz$d>8;&Ix8W{vlJ=pQ|Ok+}xQ3@w+Jg z^nOx06%nMknDB8DZvuKh)YHa3u{^cmy0d9K=N^wY28?usei)x;9D>tt5PxccM*9H@ zr|{+=;VtY>?Jhlfzi&0}9lmbH6|HhkQhlTu0q_y5q*Dj!vIfc(M&#awwLyN-?jYgF z)w`A;1GD8BB9~*|8y`DU&OuBGS;=Wo>;A2+(i>h_7v>4K50_#j=mQusEjGN-so%BT zkrboA+j`~emzrMrPBOJ4xnzHun`x{2>-cNKQt}s%V+GQ_v)(#u>g|8T{C1gz&(84O z*|BB2oGRGJ)Q7ThQtbocx?NwCRm7%eZrHUf5C%v$QkQNjop84 zVL&j}wd>^)n-e4bbFw~gbaze*@Y=9N6H21rFLfRaWXn9U_wJA>wMLY%F{sTGKduM) zhW=J3`7OR{masz&XZ2my%AI_S%JjW4);--{)7ZGKD;@8|7tos-QOa{c`WuHQ^gdhS zGTX=KB~@5~i>`QKYvKxAYjWs{!dNkh&J;V6Pa#i^1#WPzgl(~`Dr%1alb= z25x5`a>+_NaehhJ-v++R&;Jn5(iqo-vL#BW-^L!EE$KDMYgclQ_PUr)W;i?2hK(tQ ziZVu=bh~g5>d&ebc3x8$(Q7H_>dfcF!l?2tV|abH5+kRwb%72;P2Xl>b62$TDAAu9 zdTP$jPz=|qL&7f(+d!@EruVCppG)|VANEv|Bx-{i99_LT-56a|ZG2UoL}^ItdHIWB z<8&YA-AWUa(I6#Wds_iwXi1-2beek;p+~9-1W?1+Of^1P|1f=rtkx&VS1+6_y_;h3 z(&Cg6fkv0Thp8xUQ8!=LCAnp8<@VfrJs12$&b>Sa5odp5=Ec+O2Mm!BGKJ*1GmaT5D}Auyh$F9hnkW{;Npnx=SZ4~|)9&a5dIMA1W1 zUD<~um;=K8g8wd@mo+3Urxx=*4&kx~SCnj=F~B8Q6cX%tzxzJ|%@j~8lCLl6O#3jP zDXD+^sL0u<*m+E9Vkg)_xg!1w%>r+^{k%DnL_KA4e!CU5`4+f_o2p4doIBP9eUl_= z*YO<|F~9t6c7xc7FnLD{T9-Q2OWAhHIkeM09&!m~Sh7;KFr0bpTpz~1wI=#Xbr=-r z-n-Ep6utY}e488>{a!@&%|kO=HLhhK_X8z%AiJ-T_jV_G4t7u&RG?_n^c0g%K9}c1 zpYdJp#+gR}#kbtNJW1Y`?PgVU+Gfc2mtzGAp@PKaVF~^_iy!Nb&Hh^3H~VL_cyKf~ z-18=~S$m#tCb0v6$zbkF^N`Q<65-*KcE>VzwdTE8EWJ{{ve*Bt-3+KnQ`8kS8XIp_SIObmKsP0SX6_k$?_K~IytBOuZ5a~i=WucOHLY%2HPS5cHYE@-I zHTm^kFM?QBtaVyxxhnhSwaXtDN4r>$mYtV>oXs1QU%sMgjx_lL|Bdt zGG_Q`k#cz~>jg{PLgMUn7M|gUJKt?@ly3~B7*dS3mLU9>s5!2wp}xu=^Y<$;u^DvF z{l3hJi$V)uz(v>kJjAtF8egf{JHh5Msh0_LJaG0;S}}3bUa8C(yr&FIqKKGDYCzYANGhyiO<+PyAvCZ{FO+oq!>DOWJLwG?n~cuw^tUF&Ny)Aw+5u&epH!; z!Q}|ig{DU>oIFYeYN(T`rFjR0=mr=|D(>))M?2lAUYpl!z#Y}#=w29lZC2`Tl6xH# zjc$|6Jj_4$cKq#5KM@t4uiaz{m3lCHCDq2>{>)hYfN$l_+wUj(Xl@><#`*1VrZbo3 zqg;*&wVd(1)@dR5u}h|L!D^!fVKLM&YQ!yXMo(1+_UDeDz?Q;Z=`i^f|3U4T0L%Mo zIXi0T<9%tFw8mK4kXMzD)md{di^YE95v-`D()+^7RRWb3c?-TI?wqkwJeQ9zq`~kx zDbz-i$NAh5GSa}503_kzAW*C9Tl32UiNZdvSM9wLkl(sZMg&HH3H^E-M;yd z$kHyt{_L9_irg$=0%thH!o|!ev|vG4OuEgFMX-KJYbqL6rbb6r_|!qzJauB?Zwnf? zjmA6$Wv2#b*a=A_#%Dy-KRD?3Oyil+B0K&v!=kMu*Sf6zPORh+(}Kwg*2aX5WZlL0 z>8o&*sx&199r~n~to7Wtha4lY52%W_Sf}jWH?qX;%*ga$+k_Xs*qO!f?m5OVDH=at z4yS8J?BOIcB$H>(Vmt;``gC`svVM8oWNT|pOkXs5l3e)Q&Xzhj3}?lji8R$fO~_wX z+BIgY*M#!szx3_bfeB8mHYd__<7wEx_4i9%*DEXym(iu^wxxth7=rjx`%GL+%`Zg@h!m=X*d*smRZ=0c#g)>U#)V$(T1m);xX=(YvjfqGt z?D63*1cY6b>0E0B+tb~{F2h#3aw-M+KcXs&#~tm=hh;o>>6PTfF8D|2xw4mbgOU7B8XD@TDw zbui~Ne<>x2Jy!$=$&7QmBbRTJC_S8f9!{MIHj29p6#k5Z2W&*l(PHcGxObh3P54q7 z72u-B7PdXSpXyp)%S(w{BE94L< zaFlo_Lf1p?Q&pnQ{RCa#UJ)hl!Lm)k1~t>5_O+ow?(k%$tlrTl9$W2tdTOVgHmtU3CgAje$4B(L+ymb6o`@`1!IykwQ5256`5uBJ&IB@t>b_Kwk2Qn3 zM7H{P%J6UT@J5Jus1}(tT>>h0JCOe{nTy{v^%?gFXvx6w`EB^O;!xAa!@+yz{;(il z9_&EI(zE4IC~n_rAS8UTmVe56>beF}3mweb58BMGurO*ta*y-@Pv(ld+G&&ACuM6# zev+qCf!^4xa*a?hze}E=7GJoFPhE2ZnvkZu)Z6o~6=ME;ynCJEeaG?} znq5LgS_X29ejLyYejq7rMQL2EIpO;L#;M%%vvCiLDcjhIk+5_w*cP-!3tSu-&7fP^ zeU^ot3m_MkXs@`n(bm@X6i7(&R58IzS)dPH;^0=nAu|O0k5AuvDsnBVLVTM`Jg03y zze1ZZg%sfV8AIw`os_GqML)vI28RF#01TIX88TWixtmUDjv*A9uR~qt&QMg@ zO50Q~GhbIgtb(+~p!+oUk^?eFPM?fRDSmT2z%Y_eukK`0Cm^$wg|ha8EgD;_lkuHZ zJHs(asb!xLA~HwZ%lyzN?r;*dp}?!Cd}`}}Gd~Sve@qBtw2Fdlfnhm|R?7FLukbbW z(+!w;((udDLe$2h5Ta2T1*KOCNLA%)gQ}++j&C#a5!l>geC<&v9Yn0fYNF`p*PE>P zn3@uohFH;v+5)-vAE^wGZA1vb|2>t_l!AFeVo=v}Ql*;JkS7R#kfeZ@h zJ_%sqxQ(RtB#ReIOy%Dx`5tCUejB9Zk;os+eI^w@zWQUqzKN+@zR3h=TP789o0;iv z%UXuG{!Z2**slo}D1mxhNTlR@-c<$>UZe{xFbs0cn5Tsb`FJF^XWf|0*Ix!ztAHfg z7vGQq^~GPe8^V!Ic|bv`Y)??xu5uiX23snjaz%H3Bm~jD2|?tl0YHULnfKUOvKart zPYLhi)&{eM^ma1nHs}yg792rcI%i78^4dY>lD%T+fV)ehwhY>cu(7pmL@W|!MY?(R zJw!bB0R&Q%0E38UM2Emfj**~G;KNV7eBp_+tx zC7MwuR5I@!gBrqX+KPO-3y&@U<95}ur7`wvVe8!&6`H@MGt2+75Pnd_0nM*0Ie$qN zvp@fv9|G(bfR1WGp)d5GMTnf5TZdC1V8gT{qoesxc2@DY+(8nUVn8vtFL~ki*YQ`Y zR3WdI`VbNs@TQ0~i@mW8MeoQv}6Tqo)QpZl=a#-u)BGz>>wz1H?|P0nB<~%(t=(lnNuZ5VoOtV^Dxr7 z&n%wMKvY!FP5{}RZAzXmnb-)>zZue30!YhUuu~>}0~ktx-vSlNG=S)Un(eSIh{;HH zDzdLnm34FOI+Dy=m%F!jQ_RPXjtnFdj#R-Dfc)F%F@rM2_W7O3@7}#Lp;;6!h4b_( z2f+_#!3x<9h;0Ra2FnoWZu;{3ug&Qh{lJCt^sG_$sO{LYvnRRuLc1q0)3OS8#x0b z$dE!z0U%`-FrtQXIo}Ddcn_c8KvUATF zm+f#78q~IID>r%|X%haISn2o2qGj{WfZ)|6^wH>NPM>btfA585&3ay+sZX6VKUPED%p$3*S`T{s84_*9H1LNRks z4ULROHJk@OJc#azf+j!wqsa(F^dLT-s^Q!KJEAn0BXRcaSB!}`V7+#LNKzq^yy)cM zKIqyWI=;#kwH9PCim$@+{R=WGK!&nZFoy_tEf@eeIpDnd+o?jg9OYSR(p_n=>@P8u z5PEBa?|_DL)~m0pXh2)6U&5+Dnrq27!aDf+`i4eCk_#!2%3{?gkSAw5fLfXIVtC6k z6Zf7Qj41qoR6I7M6((N3eq_6(g_-=C?C-F3M%#l2Jbgs}B0#bvKOSZm40NIN72ncQ zxMzC`4B(tB&lMq^heI09$`j$C0<7|ka`)v>KI3l(N~>s!ZUq&QVrUA2TW!aInLYqB z%Aj2W9U1UOB9Ue5;gjg_;CET|3vq-H5wXsS|fDiQ`J3)5?|y* z{axjLJde-iCSZaR9~Zt?Dcb-Q;yozS!@5tnaTH$-ZhI8MBxGj)bUbik|LOR=-#h*Y z>iA}6$8#1@3dt}&aKLATaXaGI{xhczs{4}z(}mh_&jai3Jgpf|6rqnc7>ks;}2~*9Bsxc5rN!Od`=G zO#GpO;WZfzdKdykFbWte{&rwB$2edQOddi=>91?Q?Syx=w>!F*n*<*P&)oMEiQFR? zTdP(4GlZHmrDqsq?<`OR8F@(2iGWr&jWa9!^^)SL&zd#;fb_x%TNIloWSq;@2GY8b7F0|DgJ=YQez^&^G(FmsmJ(KzIoPEg#~P43X$MgkNU>I0O*&VF+}Q z4o3o@0!rIU0fP&)MCT=76cBcOThHv@o!!eJp;Q5#&veIt&~)d3AK-=uuu80ICrsJS zAFVKxTVa!BIIrEI5rEkA5>ri0P3MAygoK7M1nXowwyDYq(VMKRLU>_1nFB?rPZr!T zz*bpsm^dZ42w7~6ao$5rhQawVARwS`0>*~w@}*0ZCaQO#^c6rbA!Y5_dLbH1pp?)| z7{Dshb!c#3zW%47JP>bz81;wmgidA&1iW4`SbEO#UUn2Jx89UpR{W+md*yl)KX$Kf ze>Tf4?{+NGKv?cdIu#_$BNk>G@FSqg-=beS5y?Kis$gGpZ?2f>{8z4NfPiTdD5Z1< zUR=|O;nB@3eaq@&tJRDiybd}{mJFoEE$Qfh0Irouo3eK!{EyIY>s zQ)^q5v=qD?csdpclN7>(Umd)mzc%Q#8FF!mnw!HlsAI{UsoAsJB!p?oVVy;8WF+Vb z8}y)9i-8jf*V@YdOtr8kz0sEGOhTTXkOX5?6H$IWJ43)lyfL`OH8t2#WAIKz;o%QQ zMjV&AShoc!*O9Y>t?w6(zvJGG^_m%lxiZLewAQ`S4D6KoggM}mz~tL{6M7K5+vTCZ z4f_1Ua^atLO9Z=hy6on|a*_f9??fO_m8*7@y?iNMFrS;{qm9GiP%Xg$`!;DvggN<)^7=-aKmd96U9ehsy)NL{P-B4oo~a z+F~7qYFjt2y0H;L^OHmnsNapHb-0AWVkM5@&>ka4L$YSdFe`z0gjKy(RF*;Z<92L% zI+IEYnh#E|v;(S=oQo;sbW=te1)$5=&j_gez@ZRQk5bSUxI?ID?3WDzF8U*;FEt)C z;Kq2rf}mwZ&aEwPIS2k|9Bl1u1AHp@3c(8TxsG~VXP@)n6*%R!?uT`;t3k6qz#~GF zQ1ovGc8$Y{>F2BUz+D~y8@spn8HZmAiUJ{}5cv}{x`8tgZQvh6i=vJtw%pF`U|&sR z@A}~WN0!jegFD&y6HVGgGl0!+M1vN1w{1mzd7OfrBU%uECwBrkM4H2rhna|HZWJee z{;I$`;mfbS^BzwAF}wTQ!7Nm8XZ$v7f-JFIQKniA$sTlP%v;x&&(&!Xp)~iIuPjD7xHpm5Cb(~+`R*$F};bynNofh?Rj*__f+oRyVq%V0WcV9FKbF{;i>q9FdMJ8cV9utya1wULoS%1pdI2PfV4PfmJ zD1(#cX)v)ck0UaI)#?Fm^68aRuK+gL8sZ0=vUU<6pbLkSA9!Y1flb-L6!mizFR)kQ zSyr#??ISx0Nq`PPP*#ws+iJJl*?3=l>_eIG{c|q%?;%cI9aw?bdsERnhrvk5x;!cPPptbyeF@1Alc`OvHv=4ag4ys3_3v)&(y02ZiocFKgv3>VTL>}+4S{5#LV%g(zjhgG$`=T<`5E%9qLkWORq+X-)Gp6RF4n{5&y z$Ccn7$jSTu^Ak-e3?>eY>rP0BAhruKdUju)KPqoT84{^rCg))f0p608Wgu5!^aL&9 z1ZwJI6(HJ;;ZgFGhDf+tQ>>(^y9WJ{m}4kw#j+6#Z;kPgv3PyP=F+7-tGPt$fKU@J zaQ3;`ApqJ$^j3w+xPw{|i*8g}2vJDUkWN9C_~Lr4yWdiuYi4mcq}EtItq6 z9{_?*41AeeqB>HJy}11>Dr`C~0B>6ONSoYq_EV5X)2fFs!@2vlKWJ?V3+rV(wWd~A zACs4tcZPJ}WSKGaODg~*qD0P0HYcJ7EamsBg_#Zx@RaX?2wDKTgb|R_het;2y6+SX318 z{XWuD*HX_xsk4M*@Rrbp2Hq;*MP|cUU=fJ&HFb4$G4tOl>SCk@b7-NVp@Wcx)qZla z2Ix3~2P)p?mLMiLP_h~pG2>?*=eBn1#5WFu$QsYluS8F*G#PiO0l_ zwdWJPk@n1S(O>s%whwj0#y&WS*Z@2xXJFpKDBHncFk+Ovu6!8OS|%D%l9!*AB?d&l zT9A8w;X3>o&v;f)AV(%<=}t5db>NDA!)R3f99&ul(34xrqv$RUL&H%gRz6ZF&V46r z)M=hNb?U<)40CTt#9#?=adFr&JeUDto+d1~;h;muJ%QE|FE?NK_C5(+Pde^5gJ5GC z1UC;)-9&$_l%nE8m@@VOs?ZbgyB%fc=`?DCt}^2;2=nZM&b0bKFN1vmd_*HQp12M(0&`mwPF9PFe zX12YiFAI8k?Ah>aX=y1m16zXfo_l`3Ng?J<&cb#>j&uL}c}TE$K)eh+8zqc!0h}UM zkZTke)Pnrp-wqKhp-;B*@|FzA0~HHe5xGG~$C5_r)K`>&>mJAddQ3meYor^p2fF(D z`fBRxFCgDlJU>yZ?>zwQ668f*=H@<0OiUbLz^HJ5jH09)r9*M*qiPIjsvadm;2l?A zUoVi0XCVLd`%Uk;ioD@QSo>RnaZiFNSV4HzTI?}-_o2f0rU*E~Cph@Y$<#i`ihY1ozgVG7 z!DcsRe`by`V0AYEV;RQmGefttUVOgadjM0?Kic|Ym`cr>jfJbKNc=W(PwZ>9$!#0j z0j~Bo9A|t2lZu6fMfCLitqnrX?e_V`69qjzJ$3QQ$}kVqKv+uvW$onT0?^!SwQJ4U z4ZriDL3z>ptf?fH00+V^5;Kj@76Yb;jm_3m=->T*jOjyF9336w04SVcQX&sJ&zGh^ z=#%9&yAbS*u&~?~fHzS~y+m+f-Jb`_ for the conventional relaxation +definition and its diffraction treatment. + +Newly serialized interfaces use the explicit four-column header +``Width/cells Skew/cells StrainCoupling Offset/bulk_frac``. ``.xtal`` and +``.xpr`` models written by orGUI v1.5.0 used the two-column header +``Width/cells Skew/cells``; these records remain readable and are interpreted +as :math:`\kappa=1` and :math:`o=0`, preserving the fully strain-coupled, +zero-offset model. Saving the reconstructed model writes the current +four-column form. + +The optional companion ``.h5`` file contains fit definitions rather than a +separate crystal model. A v1.5.0 two-element interface baseline stored there +is likewise expanded to ``[Width, Skew, 1, 0]`` when loaded. Existing Width +and Skew fit parameters retain their original indices; ``strain_coupling`` and +``offset`` become fit parameters only when explicitly selected. + +RuO2 on TiO2 example +^^^^^^^^^^^^^^^^^^^^ + +The following density profiles use a 15 nm RuO2 film on TiO2 with a 1 nm +Skellam interface width. The columns use offsets :math:`o=0`, :math:`0.02`, +and :math:`0.17`. For the TiO2 lower-bulk lattice +:math:`c_{\mathrm{bulk}}=6.5807` Angstrom, these correspond to +:math:`\Delta=0`, :math:`0.13`, and :math:`1.12` Angstrom. Each column +vertically separates the :math:`\kappa=0`, :math:`\kappa=0.5`, and +:math:`\kappa=1` densities. +Gray is the summed density, blue is the TiO2 contribution including the +semi-infinite bulk and lower-interface correction, and orange is the RuO2 +interface-plus-film contribution. + +.. figure:: _static/epitaxy_strain_coupling_offset_density.png + :alt: RuO2 on TiO2 interface densities and shared displacement profiles for three offsets and three strain-coupling values. + :align: center + :width: 100% + + Strain coupling partitions the physical offset into rigid registry and + shared interface expansion. Arrows above the density panels indicate the + full bulk-to-film displacement :math:`\Delta`. The lower panels show the + shared displacement :math:`u_\kappa(z)` obtained from the generated + lower-material domain transforms; its film-side plateau is + :math:`\kappa\Delta`. + +The :math:`\kappa=0` density peaks show two independently periodic lattices: +the TiO2 peaks retain the lower-bulk spacing, while the RuO2 peaks retain the +RuO2 film spacing and are shifted rigidly by :math:`\Delta`. No density +feature is bent into a strain field. At :math:`\kappa=1` the offset is +accumulated through the occupancy profile, so both distributed materials +follow the same displacement and the lower-panel curve rises from zero to +:math:`\Delta`. The :math:`\kappa=0.5` row is the exact intermediate case: +half of the offset remains as registry separation and half appears as a +measurable expansion of the interface. + +The lower curves are plotted at the represented lower-material layer +positions rather than inferred from peak maxima. They therefore provide a +direct diagnostic of the coordinate transforms used by ``F_uc``, +``zDensity_G``, and the optical profile. Crystal composition ------------------- diff --git a/examples/CTR/RuO2_TiO2_Poisson_etching.xpr b/examples/CTR/RuO2_TiO2_Poisson_etching.xpr index 684cac7..3d249d3 100644 --- a/examples/CTR/RuO2_TiO2_Poisson_etching.xpr +++ b/examples/CTR/RuO2_TiO2_Poisson_etching.xpr @@ -104,8 +104,8 @@ layer_behaviour: ignore # EpitaxyInterface TiO2toRuO2 0001 occupancy = 1.00000 type skellam -Width/cells Skew/cells -0.35000 0.00000 +Width/cells Skew/cells StrainCoupling Offset/bulk_frac +0.35000 0.00000 1.00000 0.00000 TopUnitCell RuO2interf return diff --git a/examples/CTR/RuO2_TiO2_Poisson_etching.xtal b/examples/CTR/RuO2_TiO2_Poisson_etching.xtal index 4e310a9..5e787ea 100644 --- a/examples/CTR/RuO2_TiO2_Poisson_etching.xtal +++ b/examples/CTR/RuO2_TiO2_Poisson_etching.xtal @@ -104,8 +104,8 @@ layer_behaviour: ignore # EpitaxyInterface TiO2toRuO2 0001 occupancy = 1.00000 type skellam -Width/cells Skew/cells -0.35000 0.00000 +Width/cells Skew/cells StrainCoupling Offset/bulk_frac +0.35000 0.00000 1.00000 0.00000 TopUnitCell RuO2interf return diff --git a/examples/CTR/RuO2_TiO2_epitaxy_strain_coupling_offset.ipynb b/examples/CTR/RuO2_TiO2_epitaxy_strain_coupling_offset.ipynb new file mode 100644 index 0000000..6144338 --- /dev/null +++ b/examples/CTR/RuO2_TiO2_epitaxy_strain_coupling_offset.ipynb @@ -0,0 +1,341 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "overview", + "metadata": {}, + "source": [ + "# RuO₂/TiO₂ epitaxy: strain coupling and vertical offset\n", + "\n", + "Compare the `(0, 0, L)` CTR and adjacent $zDensity_G(z,0,0)$ profile for the `EpitaxyInterface` strain-coupling and film–bulk offset parameters.\n", + "\n", + "- The RuO₂ film is approximately **15 nm** thick.\n", + "- The Skellam interface width is **3 nm**, converted to lower-bulk unit cells.\n", + "- The first sweep uses `κ = 0, 0.25, 0.5, 0.75, 1` at zero offset.\n", + "- The second sweep fixes `κ = 0` and varies the offset from `-1` to `+1` lower-bulk `c` lattice units.\n", + "- CTR curves are vertically separated by multiplicative factors; density curves are separated by additive offsets, following the other CTR comparison notebooks.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "imports-and-template", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib widget\n", + "from pathlib import Path\n", + "import copy\n", + "import sys\n", + "\n", + "import matplotlib as mpl\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "\n", + "def find_repository_root():\n", + " \"\"\"Find the checkout containing this example notebook.\"\"\"\n", + " for candidate in (Path.cwd(), *Path.cwd().parents):\n", + " if (candidate / \"orgui\" / \"datautils\").is_dir():\n", + " return candidate\n", + " return None\n", + "\n", + "\n", + "repository_root = find_repository_root()\n", + "if repository_root is not None:\n", + " sys.path.insert(0, str(repository_root))\n", + "\n", + "from orgui.datautils.xrayutils import CTRcalc\n", + "\n", + "if (\n", + " repository_root is not None\n", + " and repository_root not in Path(CTRcalc.__file__).resolve().parents\n", + "):\n", + " raise RuntimeError(\n", + " \"An installed orgui version is already loaded. Restart the \"\n", + " \"kernel and run the notebook from the first cell.\"\n", + " )\n", + "\n", + "\n", + "def find_example_file(filename):\n", + " \"\"\"Find an example file from the repository or notebook directory.\"\"\"\n", + " candidates = (Path(filename), Path(\"examples/CTR\") / filename)\n", + " for candidate in candidates:\n", + " if candidate.is_file():\n", + " return candidate\n", + " raise FileNotFoundError(filename)\n", + "\n", + "\n", + "model_path = find_example_file(\"RuO2_TiO2_Poisson_etching.xtal\")\n", + "model_template = CTRcalc.SXRDCrystal.fromFile(model_path)\n", + "model_template.getUcNames()\n" + ] + }, + { + "cell_type": "markdown", + "id": "model-construction-notes", + "metadata": {}, + "source": [ + "## Model construction\n", + "\n", + "`Film.basis[0]` is a number of structural layers, whereas `EpitaxyInterface.basis[0]` is a width in unit cells. The requested physical dimensions are converted using the RuO₂ structural-layer spacing and the TiO₂ lower-bulk `c` lattice constant, respectively.\n", + "\n", + "The interface width is the Skellam standard deviation. A `tail_probability` of `1e-4` truncates only the far statistical tails while keeping the generated interface support shorter than the 15 nm film. The film surface is otherwise ideal; the Poisson surface component from the template is deliberately omitted so this notebook isolates epitaxial strain coupling and offset.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "model-and-plot-helpers", + "metadata": {}, + "outputs": [], + "source": [ + "film_thickness_A = 150.0\n", + "interface_width_A = 30.0\n", + "tail_probability = 1e-4\n", + "\n", + "template_interface = model_template[\"TiO2toRuO2\"]\n", + "template_film = model_template[\"RuO2\"]\n", + "\n", + "film_layer_spacing_A = (\n", + " template_film.unitcell.a[2] / len(template_film.layers)\n", + ")\n", + "film_layers = int(round(film_thickness_A / film_layer_spacing_A))\n", + "actual_film_thickness_A = film_layers * film_layer_spacing_A\n", + "interface_width_cells = (\n", + " interface_width_A / template_interface.uc_bottom.a[2]\n", + ")\n", + "\n", + "print(f\"Film: {film_layers} layers = {actual_film_thickness_A / 10:.3f} nm\")\n", + "print(\n", + " f\"Interface width: {interface_width_cells:.4f} lower-bulk cells \"\n", + " f\"= {interface_width_A / 10:.1f} nm\"\n", + ")\n", + "\n", + "\n", + "def make_model(strain_coupling, offset=0.0):\n", + " \"\"\"Build a bulk–interface–film model for one parameter pair.\"\"\"\n", + " bulk = copy.deepcopy(model_template[\"bulk\"])\n", + " interface = copy.deepcopy(template_interface)\n", + " film = copy.deepcopy(template_film)\n", + "\n", + " interface.profile = CTRcalc.SkellamProfile(\n", + " width=interface_width_cells,\n", + " asymmetry=0.0,\n", + " tail_probability=tail_probability,\n", + " )\n", + " interface.basis[:] = [\n", + " interface_width_cells,\n", + " 0.0,\n", + " strain_coupling,\n", + " offset,\n", + " ]\n", + " interface.basis_0[:] = interface.basis\n", + "\n", + " film.basis[0] = film_layers\n", + " film.basis_0[:] = film.basis\n", + "\n", + " model = CTRcalc.SXRDCrystal(\n", + " bulk,\n", + " interface,\n", + " film,\n", + " stacking=np.array([1, 2]),\n", + " atten=model_template.atten,\n", + " )\n", + " model.apply_stacking()\n", + " return model\n", + "\n", + "\n", + "L = np.linspace(0.95, 7.0, 2400)\n", + "H = np.zeros_like(L)\n", + "K = np.zeros_like(L)\n", + "z = np.linspace(-150.0, 170.0, 6000)\n", + "\n", + "\n", + "def evaluate_sweep(values, model_factory):\n", + " \"\"\"Calculate `(0, 0, L)` amplitudes and real z densities.\"\"\"\n", + " ctrs = []\n", + " densities = []\n", + " for value in values:\n", + " model = model_factory(value)\n", + " ctrs.append(np.abs(model.F(H, K, L)))\n", + " densities.append(np.real(model.zDensity_G(z, 0, 0)))\n", + " return np.asarray(ctrs), np.asarray(densities)\n", + "\n", + "\n", + "def plot_paired_sweep(values, ctrs, densities, *, cmap_name, colorbar_label, title):\n", + " \"\"\"Plot vertically separated density and CTR curves in adjacent axes.\"\"\"\n", + " values = np.asarray(values, dtype=float)\n", + " cmap = mpl.colormaps[cmap_name]\n", + " norm = mpl.colors.Normalize(vmin=values.min(), vmax=values.max())\n", + " ctr_stack_factor = 3.0\n", + " density_step = 1.15 * np.max(np.abs(densities))\n", + "\n", + " fig, (density_ax, ctr_ax) = plt.subplots(\n", + " 1,\n", + " 2,\n", + " figsize=(13.0, 5.4),\n", + " constrained_layout=True,\n", + " )\n", + " for plot_index, (value, ctr, density) in enumerate(\n", + " zip(values, ctrs, densities)\n", + " ):\n", + " color = cmap(norm(value))\n", + " density_ax.plot(\n", + " z,\n", + " density + density_step * plot_index,\n", + " color=color,\n", + " linewidth=1.15,\n", + " )\n", + " ctr_ax.semilogy(\n", + " L,\n", + " ctr * ctr_stack_factor**plot_index,\n", + " color=color,\n", + " linewidth=1.2,\n", + " )\n", + "\n", + " density_ax.axvline(0.0, color=\"0.25\", linestyle=\"--\", linewidth=0.9)\n", + " density_ax.set_xlabel(r\"$z$ / Angstrom\")\n", + " density_ax.set_ylabel(r\"Vertically offset $\\mathrm{Re}[\\rho_{00}(z)]$\")\n", + " density_ax.set_title(r\"Adjacent $zDensity_G(z,0,0)$ profiles\")\n", + " density_ax.grid(alpha=0.18)\n", + "\n", + " ctr_ax.set_xlabel(r\"$L$ / r.l.u.\")\n", + " ctr_ax.set_ylabel(r\"Vertically offset $|F_{00L}|$ / electrons\")\n", + " ctr_ax.set_title(r\"$(0,0,L)$ CTR\")\n", + " ctr_ax.grid(alpha=0.18, which=\"both\")\n", + " fig.suptitle(title)\n", + "\n", + " colorbar = fig.colorbar(\n", + " mpl.cm.ScalarMappable(norm=norm, cmap=cmap),\n", + " ax=(density_ax, ctr_ax),\n", + " pad=0.02,\n", + " ticks=values,\n", + " )\n", + " colorbar.set_label(colorbar_label)\n", + " return fig\n" + ] + }, + { + "cell_type": "markdown", + "id": "strain_coupling-sweep-notes", + "metadata": {}, + "source": [ + "## Strain coupling sweep\n", + "\n", + "`strain_coupling = 0` leaves the bulk and film on their independent unstrained lattices. Increasing `strain_coupling` linearly moves their generated interface positions toward the fully strain-coupled field at `strain_coupling = 1`. The film–bulk offset remains zero.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "strain_coupling-sweep", + "metadata": {}, + "outputs": [], + "source": [ + "strain_couplings = np.array([0.0, 0.25, 0.5, 0.75, 1.0])\n", + "strain_coupling_ctrs, strain_coupling_densities = evaluate_sweep(\n", + " strain_couplings,\n", + " lambda strain_coupling: make_model(strain_coupling=strain_coupling, offset=0.0),\n", + ")\n", + "\n", + "strain_coupling_figure = plot_paired_sweep(\n", + " strain_couplings,\n", + " strain_coupling_ctrs,\n", + " strain_coupling_densities,\n", + " cmap_name=\"viridis\",\n", + " colorbar_label=\"Strain coupling κ\",\n", + " title=\"15 nm RuO₂ film, 3 nm interface: independent-lattice to fully strain-coupled\",\n", + ")\n", + "# strain_coupling_figure\n" + ] + }, + { + "cell_type": "markdown", + "id": "offset-sweep-notes", + "metadata": {}, + "source": [ + "## Offset sweep at `strain_coupling = 0`\n", + "\n", + "The offset is expressed in fractional lower-bulk `c` coordinates. It translates the RuO₂-side interface atoms and the complete film while leaving the TiO₂ bulk fixed. The sweep below spans `-1` to `+1`, including half-cell offsets.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "offset-sweep", + "metadata": {}, + "outputs": [], + "source": [ + "offsets = np.array([0.0, 0.1, -0.1, 0.2, -0.2])[::-1]\n", + "offset_ctrs, offset_densities = evaluate_sweep(\n", + " offsets,\n", + " lambda offset: make_model(strain_coupling=0.0, offset=offset),\n", + ")\n", + "\n", + "offset_figure = plot_paired_sweep(\n", + " offsets,\n", + " offset_ctrs,\n", + " offset_densities,\n", + " cmap_name=\"plasma\",\n", + " colorbar_label=r\"Offset / lower-bulk $c$\",\n", + " title=\"15 nm RuO₂ film, 3 nm interface: offset sweep at strain_coupling = 0\",\n", + ")\n", + "# offset_figure\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c808045-11c2-4037-b1cb-71413e32762f", + "metadata": {}, + "outputs": [], + "source": [ + "offsets = np.array([0.0, 0.03, -0.03, 0.06, -0.06])[::-1]\n", + "offset_ctrs, offset_densities = evaluate_sweep(\n", + " offsets,\n", + " lambda offset: make_model(strain_coupling=1.0, offset=offset),\n", + ")\n", + "\n", + "offset_figure = plot_paired_sweep(\n", + " offsets,\n", + " offset_ctrs,\n", + " offset_densities,\n", + " cmap_name=\"plasma\",\n", + " colorbar_label=r\"Offset / lower-bulk $c$\",\n", + " title=\"15 nm RuO₂ film, 3 nm interface: offset sweep at strain_coupling = 1\",\n", + ")\n", + "offset_figure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd521878-e73d-421d-a6d6-4176fc9767be", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/CTR/test/0001_fit_2V036_reference.xtal b/examples/CTR/test/0001_fit_2V036_reference.xtal index 7669762..3ecca0c 100644 --- a/examples/CTR/test/0001_fit_2V036_reference.xtal +++ b/examples/CTR/test/0001_fit_2V036_reference.xtal @@ -30,8 +30,8 @@ layer_behaviour: ignore # EpitaxyInterface TiO2toRuO2 0001 occupancy = 1.00000 +- nan type skellam -Width/cells Skew/cells -(0.35000 +- nan) (0.00000 +- 0.00000) +Width/cells Skew/cells StrainCoupling Offset/bulk_frac +(0.35000 +- nan) (0.00000 +- 0.00000) (1.00000 +- nan) (0.00000 +- nan) TopUnitCell RuO2interf return diff --git a/orgui/datautils/xrayutils/CTRfilm.py b/orgui/datautils/xrayutils/CTRfilm.py index 2785cd9..9d80cb4 100644 --- a/orgui/datautils/xrayutils/CTRfilm.py +++ b/orgui/datautils/xrayutils/CTRfilm.py @@ -419,19 +419,31 @@ def _parse_float_metadata(string, name, default): class EpitaxyInterface(_LayerStackingMixin, LinearFitFunctions): - """A distributed, coherently strained interface between two lattices. + """A distributed, strain-coupled interface between two lattices. The statistical profile controls the upper- and lower-material - occupancies. ``C`` controls the out-of-plane coherence: zero leaves both - materials on their independent, unstrained lattices, while one linearly - moves every generated position to the fully coherent strain field. - ``offset`` translates only the upper material and subsequently stacked - components by ``offset * uc_bottom.a[2]`` Angstrom. + occupancies. ``strain_coupling`` controls the out-of-plane lattice + transition: zero leaves both materials on their independent, unstrained + lattices, while one linearly moves every generated position to the fully + strain-coupled field. The strain-coupling-dependent coordinate is + + ``z = z_unstrained + strain_coupling * (z_coupled - z_unstrained)``. + + At zero strain coupling, ``offset`` is a rigid registry shift between the + independent film and bulk lattices: the upper material moves and the lower + material remains fixed. At full strain coupling, the offset is accommodated + by a displacement field proportional to the statistical upper-material + occupancy and shared by both material additions. Intermediate strain + coupling linearly interpolates between those endpoint geometries. The semi-infinite lower bulk remains on its unstrained lattice. Generated lower-material domains therefore add the distributed material at its - coherence-dependent positions and separately subtract the sharp bulk at - its original positions. + strain-coupling-dependent positions and separately subtract the sharp bulk + at its original positions. + + New interfaces default to ``strain_coupling = 1`` and ``offset = 0``. + Text models and HDF5 fit settings written by orGUI v1.5.0 contained only + Width and Skew; loading them supplies those same defaults. :param UnitCell uc_top: Upper (film-side) interface unit cell. @@ -441,13 +453,26 @@ class EpitaxyInterface(_LayerStackingMixin, LinearFitFunctions): Statistical interface model. Currently only ``"skellam"``. """ - parameterOrder = "Width/cells Skew/cells Coherence Offset/bulk_frac" + parameterOrder = ( + "Width/cells Skew/cells StrainCoupling Offset/bulk_frac" + ) + legacyParameterOrder = "Width/cells Skew/cells" - parameterLookup = {"W": 0, "S": 1, "C": 2, "offset": 3} + parameterLookup = { + "W": 0, + "S": 1, + "strain_coupling": 2, + "offset": 3, + } avail_types = ["skellam"] - parameterLookup_inv = {0: "W", 1: "S", 2: "C", 3: "offset"} + parameterLookup_inv = { + 0: "W", + 1: "S", + 2: "strain_coupling", + 3: "offset", + } def __init__(self, uc_top, uc_bottom, type="skellam", **kwargs): """ @@ -487,7 +512,7 @@ def __init__(self, uc_top, uc_bottom, type="skellam", **kwargs): self.below_loc = 0.0 self.below_H = 0.0 self.below_layer = -1.0 - self._coherence_displacement = 0.0 + self._strain_coupling_displacement = 0.0 self._initialize_layer_stacking(kwargs) def set_ucs(self, uc_top, uc_bottom, **kwargs): @@ -637,10 +662,10 @@ def stacking_loc_absolute(self): """Return the translated film-side boundary in Angstrom. The statistical boundary and lower bulk remain at - :attr:`loc_absolute`. The coherent strain displacement accumulated - upward from the fixed bulk and the explicit film offset translate the - film-side boundary. A Film uses this location as the origin of its - requested total width, while + :attr:`loc_absolute`. The strain displacement accumulated upward from + the fixed bulk and the explicit film offset translate the film-side + boundary. A Film uses this location as the origin of its requested + total width, while :attr:`stacking_height_absolute` supplies the physical support it must not duplicate. """ @@ -648,10 +673,15 @@ def stacking_loc_absolute(self): self.createInterfaceCells() return ( self.loc_absolute - + self._coherence_displacement - + self.basis[3] * self.uc_bottom.a[2] + + self._strain_coupling_displacement + + self._offset_absolute ) + @property + def _offset_absolute(self): + """Return the coupling-independent upper-material offset in Angstrom.""" + return self.basis[3] * self.uc_bottom.a[2] + @property def layer_state(self): """Return the terminal layer state of the physical upper support.""" @@ -697,9 +727,14 @@ def createInterfaceCells(self): (unitcells >= loc).astype(np.float64).reshape((-1, n_layers)) ) sharp_bottom = 1.0 - sharp_top - coherence = self.basis[2] - if not np.isfinite(coherence) or not 0.0 <= coherence <= 1.0: - raise ValueError("EpitaxyInterface coherence must be between 0 and 1") + strain_coupling = self.basis[2] + if ( + not np.isfinite(strain_coupling) + or not 0.0 <= strain_coupling <= 1.0 + ): + raise ValueError( + "EpitaxyInterface strain_coupling must be between 0 and 1" + ) offset = self.basis[3] if not np.isfinite(offset): raise ValueError("EpitaxyInterface offset must be finite") @@ -730,11 +765,11 @@ def createInterfaceCells(self): ratio_top = a3_bottom / a3_top ratio_bottom = 1 / ratio_top - coherent_top = [[] for _ in self.top_layers] - coherent_bottom = [[] for _ in self.bottom_layers] + coupled_top = [[] for _ in self.top_layers] + coupled_bottom = [[] for _ in self.bottom_layers] ideal_top = [[] for _ in self.top_layers] ideal_bottom = [[] for _ in self.bottom_layers] - coherent_height = 0.0 + coupled_height = 0.0 for cycle_index, (p_t, p_b) in enumerate( zip(probability_top, probability_bottom) @@ -758,10 +793,10 @@ def createInterfaceCells(self): - (self.uc_top.layerpos[self.layer_order[i]]) ) mat_top_i[2, 3] = ( - coherent_height / a3_top + coupled_height / a3_top + top_strain_and_h * top_layer_offset ) - coherent_top[i].append(mat_top_i) + coupled_top[i].append(mat_top_i) mat_bottom_i = np.copy(mat_0) bottom_strain_and_h = bottom_strains[i] @@ -771,10 +806,10 @@ def createInterfaceCells(self): - (self.uc_bottom.layerpos[self.layer_order[i]]) ) mat_bottom_i[2, 3] = ( - coherent_height / a3_bottom + coupled_height / a3_bottom + bottom_strain_and_h * bottom_layer_offset ) - coherent_bottom[i].append(mat_bottom_i) + coupled_bottom[i].append(mat_bottom_i) ideal_top_i = np.copy(mat_0) ideal_top_i[2, 3] = cycle_index + top_layer_offset @@ -783,7 +818,7 @@ def createInterfaceCells(self): ideal_bottom_i = np.copy(mat_0) ideal_bottom_i[2, 3] = cycle_index + bottom_layer_offset ideal_bottom[i].append(ideal_bottom_i) - coherent_height += cell_height + coupled_height += cell_height loc_rescaled = loc - unitcells[0] uc_no_loc = int(np.floor(loc_rescaled)) // n_layers @@ -800,11 +835,30 @@ def createInterfaceCells(self): + loc_remainder * ideal_bottom_loc_mat[2, 2] ) * a3_bottom - top_offset = offset * a3_bottom + offset_absolute = self._offset_absolute + occupancy_low = np.min(probability_top) + occupancy_span = np.max(probability_top) - occupancy_low + if occupancy_span == 0.0: + if offset_absolute != 0.0: + raise ValueError( + "EpitaxyInterface offset requires an occupancy profile " + "that spans the film-bulk transition" + ) + offset_profile = np.zeros_like(probability_top) + else: + # Normalize the represented occupancy profile so truncating + # its statistical tails does not leave a displacement jump at + # either stacking boundary. + offset_profile = ( + offset_absolute + * (probability_top - occupancy_low) + / occupancy_span + ) # The lower support boundary is the physical anchor shared by the - # coherent field and the unstrained semi-infinite bulk. Translate + # strain-coupled field and the unstrained semi-infinite bulk. + # Translate # both using the ideal lower-lattice boundary location. The - # coherent statistical boundary is then free to move by the + # coupled statistical boundary is then free to move by the # displacement accumulated through the strain field. The # displacement at the upper support boundary is passed to every # subsequently stacked component. @@ -816,42 +870,55 @@ def createInterfaceCells(self): upper_boundary = ( self.uc_top.layerpos[upper_layer_id] + upper_layer_space ) - coherent_upper_mat = coherent_top[-1][-1] + coupled_upper_mat = coupled_top[-1][-1] ideal_upper_mat = ideal_top[-1][-1] - coherent_upper = ( - coherent_upper_mat[2, 3] + coupled_upper = ( + coupled_upper_mat[2, 3] - ideal_bottom_loc / a3_top - + coherent_upper_mat[2, 2] * upper_boundary + + coupled_upper_mat[2, 2] * upper_boundary ) * a3_top ideal_upper = ( ideal_upper_mat[2, 3] - ideal_top_loc / a3_top + ideal_upper_mat[2, 2] * upper_boundary ) * a3_top - self._coherence_displacement = coherence * ( - coherent_upper - ideal_upper + self._strain_coupling_displacement = strain_coupling * ( + coupled_upper - ideal_upper ) for i, (uc_t, uc_b) in enumerate(zip(self.top_layers, self.bottom_layers)): - for coherent_mat, ideal_mat in zip(coherent_top[i], ideal_top[i]): - coherent_mat = np.copy(coherent_mat) + for coupled_mat, ideal_mat, profile_offset in zip( + coupled_top[i], + ideal_top[i], + offset_profile[:, i], + ): + coupled_mat = np.copy(coupled_mat) ideal_mat = np.copy(ideal_mat) - coherent_mat[2, 3] -= ideal_bottom_loc / a3_top + coupled_mat[2, 3] -= ideal_bottom_loc / a3_top ideal_mat[2, 3] -= ideal_top_loc / a3_top - interpolated = ideal_mat + coherence * ( - coherent_mat - ideal_mat + interpolated = ideal_mat + strain_coupling * ( + coupled_mat - ideal_mat + ) + top_offset = ( + (1.0 - strain_coupling) * offset_absolute + + strain_coupling * profile_offset ) interpolated[2, 3] += top_offset / a3_top uc_t.coherentDomainMatrix.append(interpolated) - for coherent_mat, ideal_mat in zip( - coherent_bottom[i], ideal_bottom[i] + for coupled_mat, ideal_mat, profile_offset in zip( + coupled_bottom[i], + ideal_bottom[i], + offset_profile[:, i], ): - coherent_mat = np.copy(coherent_mat) + coupled_mat = np.copy(coupled_mat) ideal_mat = np.copy(ideal_mat) - coherent_mat[2, 3] -= ideal_bottom_loc / a3_bottom + coupled_mat[2, 3] -= ideal_bottom_loc / a3_bottom ideal_mat[2, 3] -= ideal_bottom_loc / a3_bottom - interpolated = ideal_mat + coherence * ( - coherent_mat - ideal_mat + interpolated = ideal_mat + strain_coupling * ( + coupled_mat - ideal_mat + ) + interpolated[2, 3] += ( + strain_coupling * profile_offset / a3_bottom ) uc_b.coherentDomainMatrix.append(interpolated) @@ -1046,9 +1113,31 @@ def clearParameters(self): self.uc_bottom.clearParameters() def parametersFromDict(self, d, override_values=True): + """Restore fit settings, including v1.5.0 HDF5 companions. + + Two-element legacy baselines are expanded from ``[Width, Skew]`` to + ``[Width, Skew, 1, 0]`` before applying the stored fit parameters. + + :param dict d: + Interface fit settings decoded from the companion HDF5 file. + :param bool override_values: + Apply stored fit-parameter values to the interface basis. + """ self.uc_top.parametersFromDict(d["unitcells"]["top"], override_values) self.uc_bottom.parametersFromDict(d["unitcells"]["bottom"], override_values) - super().parametersFromDict(d, override_values) + interface_parameters = dict(d) + basis_0 = np.asarray(interface_parameters["basis_0"], dtype=np.float64) + if basis_0.size == 2: + # orGUI v1.5.0 HDF5 companions stored only Width and Skew. + # Preserve their fully strain-coupled, zero-offset behavior. + basis_0 = np.concatenate((basis_0, [1.0, 0.0])) + elif basis_0.size != 4: + raise ValueError( + "EpitaxyInterface fit settings require either the v1.5.0 " + "Width/Skew basis or the current four-parameter basis" + ) + interface_parameters["basis_0"] = basis_0 + super().parametersFromDict(interface_parameters, override_values) def updateFromParameters(self): """Update basis from the values stored in the Parameters""" @@ -1089,6 +1178,18 @@ def parameter_list(self): @classmethod def fromStr(cls, string): + """Deserialize a current or v1.5.0 text interface. + + The v1.5.0 two-parameter representation is upgraded to a fully + strain-coupled, zero-offset interface. + + :param str string: + Serialized EpitaxyInterface section from an ``.xtal`` or ``.xpr`` + model. + :returns: + Reconstructed interface. + :rtype: EpitaxyInterface + """ with util.StringIO(string) as f: # parse header line = next_skip_comment(f).split() @@ -1105,9 +1206,12 @@ def fromStr(cls, string): ep_type = line[1].lower() statistics = dict() + parameter_header = None line = next_skip_comment(f) while "Width" in line or "=" in line: # parameter header or statistics line - if "=" in line: + if "Width" in line: + parameter_header = line.strip() + elif "=" in line: try: splitted = [n.split(",") for n in line.split("=")] splitted = [item for sublist in splitted for item in sublist] @@ -1116,6 +1220,15 @@ def fromStr(cls, string): except Exception: print(f"Cannot read statistics string: {line}") line = next_skip_comment(f) + if parameter_header not in ( + cls.parameterOrder, + cls.legacyParameterOrder, + ): + raise ValueError( + "EpitaxyInterface parameter header must be either " + f"'{cls.parameterOrder}' or the v1.5.0 header " + f"'{cls.legacyParameterOrder}'" + ) # epitaxy parameters sline = line.split() if "+-" in sline: @@ -1128,14 +1241,18 @@ def fromStr(cls, string): else: basis = np.array(sline, dtype=np.float64) errors = None - if basis.size == 2: + expected_size = ( + 2 if parameter_header == cls.legacyParameterOrder else 4 + ) + if basis.size != expected_size: + raise ValueError( + f"EpitaxyInterface header '{parameter_header}' requires " + f"{expected_size} parameter values" + ) + if expected_size == 2: basis = np.concatenate((basis, [1.0, 0.0])) if errors is not None: errors = np.concatenate((errors, [np.nan, np.nan])) - elif basis.size != 4: - raise ValueError( - "EpitaxyInterface requires Width, Skew, Coherence, and Offset" - ) # very explicit searching for the lines containing TopUnitCell and BottomUnitCell: # noqa: E501 sp_str = string.splitlines() diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index fdc240f..1b273b0 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -1830,7 +1830,8 @@ def test_profile_interface_support_is_not_owned_by_film(self): ) self.assertAlmostEqual( interface.stacking_loc_absolute, - interface.loc_absolute + interface._coherence_displacement, + interface.loc_absolute + + interface._strain_coupling_displacement, ) self.assertAlmostEqual(film.pos_absolute, interface.height_absolute) self.assertGreater(film.pos_absolute, interface.loc_absolute) @@ -1940,7 +1941,7 @@ def make_epitaxy_cells(): bottom = TestLayerStacking.make_layered_unitcell("bottom") return top, bottom - def make_epitaxy_interface(self, coherence=1.0, offset=0.0): + def make_epitaxy_interface(self, strain_coupling=1.0, offset=0.0): top, bottom = self.make_epitaxy_cells() interface = CTRfilm.EpitaxyInterface( top, @@ -1948,12 +1949,12 @@ def make_epitaxy_interface(self, coherence=1.0, offset=0.0): profile=SkellamProfile(0.5), fixed_ucs=6, ) - interface.basis[:] = [0.5, 0.0, coherence, offset] + interface.basis[:] = [0.5, 0.0, strain_coupling, offset] interface.createInterfaceCells() return interface def test_epitaxy_bulk_replacement_subtracts_fixed_lattice_positions(self): - interface = self.make_epitaxy_interface(coherence=1.0) + interface = self.make_epitaxy_interface(strain_coupling=1.0) for layer in interface.bottom_layers: split = len(layer.coherentDomainMatrix) // 2 @@ -1973,7 +1974,7 @@ def test_epitaxy_bulk_replacement_subtracts_fixed_lattice_positions(self): np.any(np.asarray(layer.coherentDomainOccupancy[split:]) < 0.0) ) - def test_epitaxy_coherent_field_is_anchored_to_deep_bulk(self): + def test_epitaxy_strain_coupled_field_is_anchored_to_deep_bulk(self): top, bottom = self.make_epitaxy_cells() width = 30.0 / bottom.a[2] interface = CTRfilm.EpitaxyInterface( @@ -2001,14 +2002,18 @@ def test_epitaxy_coherent_field_is_anchored_to_deep_bulk(self): ) * lower_layer.a[2] self.assertAlmostEqual(addition_origin, fixed_origin) - self.assertNotEqual(interface._coherence_displacement, 0.0) + self.assertNotEqual( + interface._strain_coupling_displacement, + 0.0, + ) self.assertAlmostEqual( interface.stacking_loc_absolute, - interface.loc_absolute + interface._coherence_displacement, + interface.loc_absolute + + interface._strain_coupling_displacement, ) top_zero, bottom_zero = self.make_epitaxy_cells() - incoherent = CTRfilm.EpitaxyInterface( + independent = CTRfilm.EpitaxyInterface( top_zero, bottom_zero, profile=SkellamProfile( @@ -2016,28 +2021,28 @@ def test_epitaxy_coherent_field_is_anchored_to_deep_bulk(self): tail_probability=1e-4, ), ) - incoherent.basis[:] = [width, 0.0, 0.0, 0.0] - incoherent.createInterfaceCells() + independent.basis[:] = [width, 0.0, 0.0, 0.0] + independent.createInterfaceCells() self.assertAlmostEqual( - interface._coherence_displacement, - interface.height_absolute - incoherent.height_absolute, + interface._strain_coupling_displacement, + interface.height_absolute - independent.height_absolute, ) - def test_epitaxy_coherence_interpolates_generated_positions(self): - incoherent = self.make_epitaxy_interface(coherence=0.0) - halfway = self.make_epitaxy_interface(coherence=0.5) - coherent = self.make_epitaxy_interface(coherence=1.0) + def test_epitaxy_strain_coupling_interpolates_generated_positions(self): + independent = self.make_epitaxy_interface(strain_coupling=0.0) + halfway = self.make_epitaxy_interface(strain_coupling=0.5) + fully_coupled = self.make_epitaxy_interface(strain_coupling=1.0) for layers_zero, layers_half, layers_one in ( ( - incoherent.top_layers, + independent.top_layers, halfway.top_layers, - coherent.top_layers, + fully_coupled.top_layers, ), ( - incoherent.bottom_layers, + independent.bottom_layers, halfway.bottom_layers, - coherent.bottom_layers, + fully_coupled.bottom_layers, ), ): for layer_zero, layer_half, layer_one in zip( @@ -2046,7 +2051,7 @@ def test_epitaxy_coherence_interpolates_generated_positions(self): layers_one, ): domain_count = len(layer_zero.coherentDomainMatrix) - if layers_zero is incoherent.bottom_layers: + if layers_zero is independent.bottom_layers: domain_count //= 2 for matrix_zero, matrix_half, matrix_one in zip( layer_zero.coherentDomainMatrix[:domain_count], @@ -2059,106 +2064,277 @@ def test_epitaxy_coherence_interpolates_generated_positions(self): 0.5 * (matrix_zero[2] + matrix_one[2]), ) - def test_epitaxy_offset_moves_only_top_and_upward_stack(self): - baseline = self.make_epitaxy_interface(offset=0.0) - shifted = self.make_epitaxy_interface(offset=0.25) - lowered = self.make_epitaxy_interface(offset=-0.25) - expected_shift = 0.25 * shifted.uc_bottom.a[2] + def test_epitaxy_offset_interpolates_strain_coupling_limits(self): + for strain_coupling in (0.0, 0.5, 1.0): + baseline = self.make_epitaxy_interface( + strain_coupling=strain_coupling, + offset=0.0, + ) + for offset in (-0.25, 0.25): + candidate = self.make_epitaxy_interface( + strain_coupling=strain_coupling, + offset=offset, + ) + physical_shift = offset * candidate.uc_bottom.a[2] - for candidate, physical_shift in ( - (shifted, expected_shift), - (lowered, -expected_shift), - ): - for base_layer, candidate_layer in zip( - baseline.top_layers, - candidate.top_layers, - ): - for base_matrix, candidate_matrix in zip( - base_layer.coherentDomainMatrix, - candidate_layer.coherentDomainMatrix, + self.assertAlmostEqual( + candidate._strain_coupling_displacement, + baseline._strain_coupling_displacement, + ) + all_top_occupancy = np.concatenate( + [ + layer.coherentDomainOccupancy + for layer in baseline.top_layers + ] + ) + occupancy_low = np.min(all_top_occupancy) + occupancy_span = np.max(all_top_occupancy) - occupancy_low + for base_layer, candidate_layer in zip( + baseline.top_layers, + candidate.top_layers, ): - self.assertAlmostEqual( - (candidate_matrix[2, 3] - base_matrix[2, 3]) - * candidate_layer.a[2], - physical_shift, + for occupancy, base_matrix, candidate_matrix in zip( + base_layer.coherentDomainOccupancy, + base_layer.coherentDomainMatrix, + candidate_layer.coherentDomainMatrix, + ): + profile_fraction = ( + (occupancy - occupancy_low) / occupancy_span + ) + expected_fraction = ( + 1.0 + - strain_coupling + + strain_coupling * profile_fraction + ) + self.assertAlmostEqual( + (candidate_matrix[2, 3] - base_matrix[2, 3]) + * candidate_layer.a[2], + physical_shift * expected_fraction, + ) + for layer_index, (base_layer, candidate_layer) in enumerate(zip( + baseline.bottom_layers, + candidate.bottom_layers, + )): + split = len(base_layer.coherentDomainMatrix) // 2 + for index, (base_matrix, candidate_matrix) in enumerate( + zip( + base_layer.coherentDomainMatrix[:split], + candidate_layer.coherentDomainMatrix[:split], + ) + ): + top_occupancy = ( + baseline.top_layers[ + layer_index + ].coherentDomainOccupancy[index] + ) + profile_fraction = ( + (top_occupancy - occupancy_low) / occupancy_span + ) + self.assertAlmostEqual( + (candidate_matrix[2, 3] - base_matrix[2, 3]) + * candidate_layer.a[2], + physical_shift + * strain_coupling + * profile_fraction, + ) + np.testing.assert_allclose( + candidate_layer.coherentDomainMatrix[split:], + base_layer.coherentDomainMatrix[split:], ) - for base_layer, candidate_layer in zip( - baseline.bottom_layers, - candidate.bottom_layers, + self.assertAlmostEqual( + candidate.stacking_loc_absolute + - baseline.stacking_loc_absolute, + physical_shift, + ) + self.assertAlmostEqual( + candidate.height_absolute - baseline.height_absolute, + physical_shift, + ) + + def test_epitaxy_offset_strains_shared_fully_coupled_layers(self): + top = self.make_layered_unitcell("top") + bottom = self.make_layered_unitcell("bottom") + interface = CTRfilm.EpitaxyInterface( + top, + bottom, + profile=SkellamProfile(0.5), + fixed_ucs=6, + ) + interface.basis[:] = [0.5, 0.0, 1.0, 0.25] + interface.createInterfaceCells() + + for top_layer, bottom_layer in zip( + interface.top_layers, + interface.bottom_layers, + ): + split = len(bottom_layer.coherentDomainMatrix) // 2 + for top_matrix, bottom_matrix in zip( + top_layer.coherentDomainMatrix, + bottom_layer.coherentDomainMatrix[:split], ): np.testing.assert_allclose( - candidate_layer.coherentDomainMatrix, - base_layer.coherentDomainMatrix, + top_matrix[2] * top_layer.a[2], + bottom_matrix[2] * bottom_layer.a[2], + rtol=1e-13, + atol=1e-13, ) + + def test_epitaxy_offset_preserves_integrated_top_density(self): + z = np.linspace(-100.0, 100.0, 4001) + dz = np.diff(z)[0] + + for strain_coupling in (0.0, 0.5, 1.0): + baseline = self.make_epitaxy_interface( + strain_coupling=strain_coupling, + offset=0.0, + ) + baseline_density = sum( + layer.zDensity_G(z, 0.0, 0.0) + for layer in baseline.top_layers + ) + + for offset in (-0.25, 0.25): + shifted = self.make_epitaxy_interface( + strain_coupling=strain_coupling, + offset=offset, + ) + shifted_density = sum( + layer.zDensity_G(z, 0.0, 0.0) + for layer in shifted.top_layers + ) + np.testing.assert_allclose( + np.sum(shifted_density) * dz, + np.sum(baseline_density) * dz, + rtol=1e-6, + atol=1e-6, + ) + + def test_epitaxy_offset_translates_film_and_surface_without_resizing(self): + strain_coupling = 1.0 + offset = 0.25 + baseline = self.make_epitaxy_interface( + strain_coupling=strain_coupling, + offset=0.0, + ) + shifted = self.make_epitaxy_interface( + strain_coupling=strain_coupling, + offset=offset, + ) + physical_shift = offset * shifted.uc_bottom.a[2] + + stacks = [] + for name, interface in (("base", baseline), ("shifted", shifted)): + film = CTRfilm.Film(self.make_layered_unitcell(f"{name}_film")) + film.basis[0] = 30 + surface = CTRfilm.PoissonSurface( + self.make_layered_unitcell(f"{name}_surface"), + profile=PoissonProfile(1.0), + ) + crystal = CTRcalc.SXRDCrystal( + interface.uc_bottom, + interface, + film, + surface, + stacking=np.array([1, 2, 3]), + ) + crystal.apply_stacking() + stacks.append((film, surface)) + + (base_film, base_surface), (shifted_film, shifted_surface) = stacks + self.assertEqual( + shifted_film._layers_to_create, + base_film._layers_to_create, + ) + for base_component, shifted_component in ( + (base_film, shifted_film), + (base_surface, shifted_surface), + ): self.assertAlmostEqual( - candidate.stacking_loc_absolute - - baseline.stacking_loc_absolute, + shifted_component.pos_absolute - base_component.pos_absolute, physical_shift, ) self.assertAlmostEqual( - candidate.height_absolute - baseline.height_absolute, + shifted_component.height_absolute - base_component.height_absolute, physical_shift, ) + self.assertAlmostEqual( + shifted_component.height_absolute - shifted_component.pos_absolute, + base_component.height_absolute - base_component.pos_absolute, + ) - base_film = CTRfilm.Film(self.make_layered_unitcell("base_film")) - shifted_film = CTRfilm.Film(self.make_layered_unitcell("shifted_film")) - base_film.basis[0] = 30 - shifted_film.basis[0] = 30 - CTRcalc.SXRDCrystal( - baseline.uc_bottom, - baseline, - base_film, - stacking=np.array([1, 2]), - ).apply_stacking() - CTRcalc.SXRDCrystal( - shifted.uc_bottom, - shifted, - shifted_film, - stacking=np.array([1, 2]), - ).apply_stacking() - self.assertAlmostEqual( - shifted_film.pos_absolute - base_film.pos_absolute, - expected_shift, + def test_epitaxy_parameters_round_trip(self): + interface = self.make_epitaxy_interface( + strain_coupling=0.4, + offset=-0.2, ) - self.assertAlmostEqual( - shifted_film.height_absolute - base_film.height_absolute, - expected_shift, - ) - - def test_epitaxy_parameters_round_trip_and_legacy_defaults(self): - interface = self.make_epitaxy_interface(coherence=0.4, offset=-0.2) restored = CTRfilm.EpitaxyInterface.fromStr(interface.toStr(showErrors=False)) np.testing.assert_allclose(restored.basis, [0.5, 0.0, 0.4, -0.2]) - legacy = interface.toStr(showErrors=False) - legacy = legacy.replace( - CTRfilm.EpitaxyInterface.parameterOrder, - "Width/cells Skew/cells", - ) - parameter_line = next( - line - for line in legacy.splitlines() - if line.strip().startswith("0.50000") - ) - legacy = legacy.replace(parameter_line, "0.50000 0.00000", 1) - restored_legacy = CTRfilm.EpitaxyInterface.fromStr(legacy) - np.testing.assert_allclose(restored_legacy.basis, [0.5, 0.0, 1.0, 0.0]) + def test_epitaxy_loads_v1_5_text_parameters_with_compatible_defaults(self): + interface = self.make_epitaxy_interface() + interface.errors = np.array([0.03, 0.02, 0.01, 0.04]) - def test_epitaxy_coherence_and_offset_are_fit_parameters(self): + for show_errors in (False, True): + with self.subTest(show_errors=show_errors): + lines = interface.toStr(showErrors=show_errors).splitlines() + header_index = lines.index( + CTRfilm.EpitaxyInterface.parameterOrder + ) + lines[header_index] = ( + CTRfilm.EpitaxyInterface.legacyParameterOrder + ) + if show_errors: + lines[header_index + 1] = ( + "(0.50000 +- 0.03000) (0.00000 +- 0.02000)" + ) + else: + lines[header_index + 1] = "0.5 0" + + restored = CTRfilm.EpitaxyInterface.fromStr("\n".join(lines)) + + np.testing.assert_allclose( + restored.basis, + [0.5, 0.0, 1.0, 0.0], + ) + if show_errors: + np.testing.assert_allclose( + restored.errors[:2], + [0.03, 0.02], + ) + self.assertTrue(np.all(np.isnan(restored.errors[2:]))) + else: + self.assertIsNone(restored.errors) + + def test_epitaxy_loads_v1_5_hdf5_fit_settings(self): + interface = self.make_epitaxy_interface() + interface.basis_0[:] = interface.basis + interface.addFitParameter("W", limits=(0.0, 1.0)) + interface.setFitParameters([0.4]) + legacy_fit_settings = interface.parametersToDict() + legacy_fit_settings["basis_0"] = legacy_fit_settings["basis_0"][:2] + + restored = self.make_epitaxy_interface() + restored.parametersFromDict(legacy_fit_settings) + + np.testing.assert_allclose(restored.basis_0, [0.5, 0.0, 1.0, 0.0]) + np.testing.assert_allclose(restored.basis, [0.4, 0.0, 1.0, 0.0]) + self.assertEqual(restored.fitparnames, interface.fitparnames) + + def test_epitaxy_strain_coupling_and_offset_are_fit_parameters(self): interface = self.make_epitaxy_interface() interface.basis_0[:] = interface.basis - interface.addFitParameter("C", limits=(0.0, 1.0)) + interface.addFitParameter("strain_coupling", limits=(0.0, 1.0)) interface.addFitParameter("offset", limits=(-0.5, 0.5)) np.testing.assert_allclose(interface.getInitialParameters(), [1.0, 0.0]) interface.setFitParameters([0.3, -0.2]) np.testing.assert_allclose(interface.basis[2:], [0.3, -0.2]) - def test_epitaxy_rejects_invalid_coherence_and_offset(self): + def test_epitaxy_rejects_invalid_strain_coupling_and_offset(self): interface = self.make_epitaxy_interface() - for coherence in (-0.1, 1.1, np.nan): - interface.basis[2] = coherence - with self.assertRaisesRegex(ValueError, "coherence"): + for strain_coupling in (-0.1, 1.1, np.nan): + interface.basis[2] = strain_coupling + with self.assertRaisesRegex(ValueError, "strain_coupling"): interface.createInterfaceCells() interface.basis[2] = 1.0 interface.basis[3] = np.inf @@ -2166,7 +2342,10 @@ def test_epitaxy_rejects_invalid_coherence_and_offset(self): interface.createInterfaceCells() def test_epitaxy_split_bulk_domains_feed_all_profile_paths(self): - interface = self.make_epitaxy_interface(coherence=0.6, offset=0.1) + interface = self.make_epitaxy_interface( + strain_coupling=0.6, + offset=0.1, + ) interface.setEnergy(10000.0) structure_factor = interface.F_uc( @@ -2194,8 +2373,8 @@ def test_epitaxy_split_bulk_domains_feed_all_profile_paths(self): np.testing.assert_allclose( structure_factor, [ - 12.235092835903 + 23.933759897760j, - -1.787823797289 - 0.028488983072j, + 12.290590935844 + 23.878872280467j, + -1.767926251260 - 0.197899416448j, ], rtol=1e-8, ) @@ -2203,16 +2382,16 @@ def test_epitaxy_split_bulk_domains_feed_all_profile_paths(self): density[[0, 100, 200, 300, 400]], [ 0.0, - -3.115325591995e-03 - 1.894807083594e-05j, - 4.311764632911e-01 + 2.127681623261e-07j, - 1.019686887022e-05, + -3.006212046217e-03 - 1.665286072977e-05j, + 9.802746914877e-01 + 3.098533417278e-03j, + 1.082893246501e-05, 0.0, ], atol=1e-9, ) np.testing.assert_allclose( np.sum(density), - 40.017177763177 + 0.032567796562j, + 40.033115045982 + 0.034943462837j, rtol=1e-8, ) mid_row = optical_profile.shape[0] // 2 @@ -2220,7 +2399,7 @@ def test_epitaxy_split_bulk_domains_feed_all_profile_paths(self): optical_profile[[0, mid_row, -1]], [ [-12.0, -9.65554085169e-10, -8.98126154061e-13], - [-0.441200041632, 3.042840635292e-06, 2.830348707724e-09], + [-2.17046762, 4.10072617e-07, 3.81435849e-10], [10.0, 0.0, 0.0], ], atol=1e-9, @@ -2250,7 +2429,8 @@ def test_legacy_xtal_uses_corrected_interface_support(self): ) self.assertAlmostEqual( interface.stacking_loc_absolute, - interface.loc_absolute + interface._coherence_displacement, + interface.loc_absolute + + interface._strain_coupling_displacement, ) self.assertIsInstance(interface.profile, SkellamProfile) self.assertNotIn("support_cursor", interface.toStr()) @@ -2265,14 +2445,11 @@ def test_legacy_xtal_uses_corrected_interface_support(self): # -- unlike test_simple_unitcell_bulk_structure_factor_matches_ # reference below, which does compare against real fit-derived # intensities for the un-layered unit-cell + bulk case. - # These numbers disagree with the pre-existing CTRs_reference.dat - # baseline used by the (deliberately still-failing) - # test_legacy_xtal_reconstructs_reference_interface below; which of - # the two is correct is an open question, deferred until after other - # PR feedback is addressed. If this test starts failing, do not - # assume the fresh code is wrong and revert to whatever makes it - # pass -- treat it the same as the sibling test: investigate the - # discrepancy before changing either the code or these numbers. + # The historical CTRs_reference.dat file was generated by the + # pre-beee738 implementation and is retained as example/provenance + # data, not as a correctness oracle. If this test starts failing, + # investigate the geometry rather than updating these values + # mechanically. xtal["RuO2"].basis[0] = 17.0 xtal["RuO2"].basis_0[0] = 17.0 xtal.atten = 0.01 @@ -2298,87 +2475,17 @@ def test_legacy_xtal_uses_corrected_interface_support(self): ) np.testing.assert_allclose(calculated, expected, rtol=2e-5, atol=2e-5) - def test_legacy_xtal_reconstructs_reference_interface(self): - """Original pre-fix regression check against the frozen - CTRs_reference.dat baseline (see git history for commit beee738, - "fix: EpitaxyInterface now is again consuming Film so that - interfacial strain field is correct"). - - The fixture files live in testdata/ (packaged with the test suite) - rather than the repository's examples/CTR/test/ directory, which is - not included in installed wheels -- this test used to silently skip - on any CI/CD run that installs the package instead of using a - source checkout, masking the discrepancy below entirely. - testdata/CTRs_reference.dat is trimmed to the header plus the 8 rows - this test actually reads (`max_rows=8` below); the full file is - still available at examples/CTR/test/CTRs_reference.dat. - - This is EXPECTED TO CURRENTLY FAIL: CTRs_reference.dat encodes the - pre-beee738 interfacial-strain behavior and was never regenerated - against the corrected physics. It is kept, failing, to keep the - discrepancy visible rather than silently dropping the check. Two - assertions from the original test are omitted because they depend - on removed legacy internals (a 2-element EpitaxyInterface.basis and - a ``_legacy_support_cursor`` attribute) unrelated to the physics - question being tracked here; see - test_legacy_xtal_uses_corrected_interface_support above for the - current basis format and support-cursor behavior. - - Before merging, determine which side is correct -- the frozen - CTRs_reference.dat baseline or the current, corrected code -- rather - than resolving the failure by editing either one without - investigation. This is deferred pending other PR feedback. - """ - fixture_root = os.path.join(os.path.dirname(__file__), "testdata") - xtal_path = os.path.join( - fixture_root, "0001_fit_2V036_reference.xtal" - ) - reference_path = os.path.join(fixture_root, "CTRs_reference.dat") - xtal = CTRcalc.SXRDCrystal.fromFile( - xtal_path - ) - - self.assertIsInstance(xtal["RuO2"], CTRfilm.Film) - self.assertIsInstance(xtal["TiO2toRuO2"], CTRfilm.EpitaxyInterface) - - xtal["RuO2"].basis[0] = 17.0 - xtal["RuO2"].basis_0[0] = 17.0 - xtal.atten = 0.01 - reference = np.loadtxt( - reference_path, - skiprows=1, - max_rows=8, - ) - l_values = np.linspace(0.0, 7.25, 2000)[:8] - calculated = np.abs( - xtal.F( - np.zeros(8, dtype=np.float64), - np.zeros(8, dtype=np.float64), - l_values, - ) - ) - np.testing.assert_allclose( - calculated, - reference[:, 3] * xtal.reference_area, - rtol=2e-5, - atol=2e-5, - ) - def test_simple_unitcell_bulk_structure_factor_matches_reference(self): - """Sibling check for the "simple" (un-layered unit-cell + bulk, - no Film/EpitaxyInterface/profile) case, for contrast with the two - Film + EpitaxyInterface tests above. This does not use - CTRs_reference.dat, which was generated from the full layered - crystal, not a simple bulk-only one (see create_test_data.py in the - datautils repository's examples/CTR directory). Instead it reuses + """Check the simple un-layered unit-cell plus bulk case. + + This does not use CTRs_reference.dat, which was generated from the + full layered crystal by the obsolete pre-beee738 implementation. + Instead it reuses testdata/0V12_calculated.xpr and .dat, the same fixtures already validated against real fit-derived reference intensities by TestCTRcalculationNumPy/Numba/Cpp above -- this is real, - independently-verified data, not a frozen snapshot of current - output. It currently passes, which localizes the - CTRs_reference.dat discrepancy above to the layered Film / - EpitaxyInterface / SkellamProfile path rather than to the - underlying unit-cell or semi-infinite bulk calculation. + independently verified data rather than a snapshot of current + output. """ fp = os.path.split(__file__)[0] xtal_unitcells = CTRcalc.SXRDCrystal.fromFile( @@ -2457,10 +2564,10 @@ def test_bulk_repeat_follows_reference_unit_cell(self): bulk.setReferenceUnitCell(reference) h = np.zeros(2) k = np.zeros(2) - l = np.array([0.17, 0.41]) + l_values = np.array([0.17, 0.41]) atten = 0.07 - hkl_bulk = bulk.refHKLTransform @ np.vstack((h, k, l)) + hkl_bulk = bulk.refHKLTransform @ np.vstack((h, k, l_values)) repeat_atten = 2.0 * atten expected = bulk.F_uc_bulk_direct(*hkl_bulk, repeat_atten) expected /= 1 - np.exp( @@ -2468,10 +2575,10 @@ def test_bulk_repeat_follows_reference_unit_cell(self): ) with mock.patch.object(CTRuc, "CTR_ACCEL_BACKEND", "numpy"): - actual = bulk.F_bulk(h, k, l, atten) + actual = bulk.F_bulk(h, k, l_values, atten) np.testing.assert_allclose(actual, expected) - np.testing.assert_allclose(hkl_bulk[2], 2.0 * l) + np.testing.assert_allclose(hkl_bulk[2], 2.0 * l_values) def test_constructor_propagates_bulk_reference_to_layers(self): bulk = self.make_carbon_cell(3.0, [0.0], "bulk") diff --git a/orgui/datautils/xrayutils/test/testdata/0001_fit_2V036_reference.xtal b/orgui/datautils/xrayutils/test/testdata/0001_fit_2V036_reference.xtal index 7669762..3ecca0c 100644 --- a/orgui/datautils/xrayutils/test/testdata/0001_fit_2V036_reference.xtal +++ b/orgui/datautils/xrayutils/test/testdata/0001_fit_2V036_reference.xtal @@ -30,8 +30,8 @@ layer_behaviour: ignore # EpitaxyInterface TiO2toRuO2 0001 occupancy = 1.00000 +- nan type skellam -Width/cells Skew/cells -(0.35000 +- nan) (0.00000 +- 0.00000) +Width/cells Skew/cells StrainCoupling Offset/bulk_frac +(0.35000 +- nan) (0.00000 +- 0.00000) (1.00000 +- nan) (0.00000 +- nan) TopUnitCell RuO2interf return diff --git a/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xpr b/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xpr index 684cac7..3d249d3 100644 --- a/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xpr +++ b/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xpr @@ -104,8 +104,8 @@ layer_behaviour: ignore # EpitaxyInterface TiO2toRuO2 0001 occupancy = 1.00000 type skellam -Width/cells Skew/cells -0.35000 0.00000 +Width/cells Skew/cells StrainCoupling Offset/bulk_frac +0.35000 0.00000 1.00000 0.00000 TopUnitCell RuO2interf return diff --git a/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xtal b/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xtal index 4e310a9..5e787ea 100644 --- a/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xtal +++ b/orgui/datautils/xrayutils/test/testdata/RuO2_TiO2_Poisson_etching.xtal @@ -104,8 +104,8 @@ layer_behaviour: ignore # EpitaxyInterface TiO2toRuO2 0001 occupancy = 1.00000 type skellam -Width/cells Skew/cells -0.35000 0.00000 +Width/cells Skew/cells StrainCoupling Offset/bulk_frac +0.35000 0.00000 1.00000 0.00000 TopUnitCell RuO2interf return From 3cb9c390aa51f45d453a9038a5906eb36fd99d97 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Wed, 29 Jul 2026 01:18:56 -0400 Subject: [PATCH 33/37] build: bundle third-party licenses for statically-linked xxHash and pybind11 --- .../pybind11-BSD-3-Clause.txt | 29 +++++++++++++++++++ THIRD_PARTY_LICENSES/xxHash-BSD-2-Clause.txt | 26 +++++++++++++++++ pyproject.toml | 1 + 3 files changed, 56 insertions(+) create mode 100644 THIRD_PARTY_LICENSES/pybind11-BSD-3-Clause.txt create mode 100644 THIRD_PARTY_LICENSES/xxHash-BSD-2-Clause.txt diff --git a/THIRD_PARTY_LICENSES/pybind11-BSD-3-Clause.txt b/THIRD_PARTY_LICENSES/pybind11-BSD-3-Clause.txt new file mode 100644 index 0000000..e466b0d --- /dev/null +++ b/THIRD_PARTY_LICENSES/pybind11-BSD-3-Clause.txt @@ -0,0 +1,29 @@ +Copyright (c) 2016 Wenzel Jakob , All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of +external contributions to this project including patches, pull requests, etc. diff --git a/THIRD_PARTY_LICENSES/xxHash-BSD-2-Clause.txt b/THIRD_PARTY_LICENSES/xxHash-BSD-2-Clause.txt new file mode 100644 index 0000000..e4c5da7 --- /dev/null +++ b/THIRD_PARTY_LICENSES/xxHash-BSD-2-Clause.txt @@ -0,0 +1,26 @@ +xxHash Library +Copyright (c) 2012-2021 Yann Collet +All rights reserved. + +BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php) + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pyproject.toml b/pyproject.toml index b75701d..ff2379b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,7 @@ name = 'orGUI' dynamic = ['version',] license = "MIT" +license-files = ["LICENSE", "THIRD_PARTY_LICENSES/*"] requires-python = '>=3.10' readme = 'README.rst' description = 'orGUI: Orientation and Integration with 2D detectors' From 46be5d019032e58bd745b134f4e1fced4bf7bc68 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Wed, 29 Jul 2026 01:41:34 -0400 Subject: [PATCH 34/37] fix(phys): correct strained optical profile construction Preserve areal optical content under normal strain and prevent transitive merging of layers beyond the z tolerance. --- orgui/datautils/xrayutils/CTRoptics.py | 21 +++++++++++-- .../datautils/xrayutils/test/test_CTRcalc.py | 4 +-- .../xrayutils/test/test_CTRoptical_profile.py | 30 +++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/orgui/datautils/xrayutils/CTRoptics.py b/orgui/datautils/xrayutils/CTRoptics.py index dadfc85..aa5d5c4 100644 --- a/orgui/datautils/xrayutils/CTRoptics.py +++ b/orgui/datautils/xrayutils/CTRoptics.py @@ -162,7 +162,19 @@ def optical_profile(unit_cell, include_coherent_domains=True): ): domain_matrix = unit_cell.R_mat_inv @ matrix[:, :-1] @ unit_cell.R_mat domain_origin = domain_matrix @ origin_vector + matrix[:, -1] - contribution = float(occupancy) * optical_density + normal_stretch = abs(float(domain_matrix[2, 2])) + if not np.isfinite(normal_stretch) or normal_stretch <= 0.0: + raise ValueError( + "Coherent-domain transforms must preserve a finite, " + "nonzero surface-normal layer thickness." + ) + # Domain transforms used by Film and EpitaxyInterface encode their + # physical out-of-plane strain in the transformed layer spacing. + # Preserve each layer's areal electron content when converting + # that spacing to a homogeneous volume density. + contribution = ( + float(occupancy) * optical_density / normal_stretch + ) profile[row] = [ domain_origin[2] * unit_cell.a[2], contribution.real, @@ -346,7 +358,12 @@ def combine_profiles(*profiles, z_tolerance=0.6): stacked = np.ascontiguousarray(np.concatenate(populated, axis=0), dtype=np.float64) order = np.argsort(stacked[:, 0], kind="stable") stacked = stacked[order] - group_starts = np.r_[0, np.flatnonzero(np.diff(stacked[:, 0]) > z_tolerance) + 1] + group_starts = [0] + for index in range(1, len(stacked)): + group_start = group_starts[-1] + if stacked[index, 0] - stacked[group_start, 0] > z_tolerance: + group_starts.append(index) + group_starts = np.asarray(group_starts, dtype=np.intp) group_stops = np.r_[group_starts[1:], len(stacked)] combined = np.empty((len(group_starts), 3), dtype=np.float64) for index, (start, stop) in enumerate(zip(group_starts, group_stops)): diff --git a/orgui/datautils/xrayutils/test/test_CTRcalc.py b/orgui/datautils/xrayutils/test/test_CTRcalc.py index 1b273b0..4ec59c1 100644 --- a/orgui/datautils/xrayutils/test/test_CTRcalc.py +++ b/orgui/datautils/xrayutils/test/test_CTRcalc.py @@ -2398,8 +2398,8 @@ def test_epitaxy_split_bulk_domains_feed_all_profile_paths(self): np.testing.assert_allclose( optical_profile[[0, mid_row, -1]], [ - [-12.0, -9.65554085169e-10, -8.98126154061e-13], - [-2.17046762, 4.10072617e-07, 3.81435849e-10], + [-12.0, -7.72508074e-10, -7.18561204e-13], + [-0.209418853, 2.94939896e-06, 2.74343239e-09], [10.0, 0.0, 0.0], ], atol=1e-9, diff --git a/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py b/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py index 48e4c18..c4c8f19 100644 --- a/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py +++ b/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py @@ -68,6 +68,19 @@ def test_domain_translation_and_occupancy_weight_optical_contribution(self): np.testing.assert_allclose(profile[1, 1], profile[0, 1] / 3.0) np.testing.assert_allclose(profile[1, 2], profile[0, 2] / 3.0) + def test_domain_normal_strain_preserves_areal_optical_content(self): + cell = unitcells.unitcell("Pt100") + cell.setEnergy(self.energy_eV) + unstrained = cell.optical_profile() + stretched = np.eye(3, 4) + stretched[2, 2] = 2.0 + cell.coherentDomainMatrix = [stretched] + cell.coherentDomainOccupancy = [1.0] + + profile = cell.optical_profile() + + np.testing.assert_allclose(profile[:, 1:], unstrained[:, 1:] / 2.0) + def test_film_and_crystal_combine_layer_profiles(self): bulk = unitcells.crystal("TiO2(100)").uc_bulk bulk.setEnergy(self.energy_eV) @@ -107,6 +120,23 @@ def test_combine_profiles_merges_nearby_layer_origins(self): np.testing.assert_allclose(combined, [[3.2728, 6.0, 8.0], [4.0, 6.0, 7.0]]) + def test_combine_profiles_bounds_full_group_span(self): + combined = CTRoptics.combine_profiles( + np.array( + [ + [0.0, 1.0, 0.0], + [0.5, 2.0, 0.0], + [1.0, 4.0, 0.0], + ] + ), + z_tolerance=0.6, + ) + + np.testing.assert_allclose( + combined, + [[0.0, 3.0, 0.0], [1.0, 4.0, 0.0]], + ) + def test_structural_layers_are_added_to_continuous_samples(self): combined = CTRoptics.add_structural_to_sampled_profile( np.array([[1.1, 2.0, 3.0], [4.0, 5.0, 6.0]]), From a3f9aee39a6cef4eac059060ef13a248cbfca039 Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Wed, 29 Jul 2026 11:49:54 -0400 Subject: [PATCH 35/37] fix(phys): vectorize optical reflectivity and wavefields Solve angle arrays with NumPy-vectorized wavefield propagation and preserve finite p-polarized behavior at exact critical angles. --- orgui/datautils/xrayutils/CTRcalc.py | 31 +- orgui/datautils/xrayutils/CTRoptics.py | 432 +++++++++++++++--- .../xrayutils/test/test_CTRoptical_profile.py | 124 +++++ 3 files changed, 509 insertions(+), 78 deletions(-) diff --git a/orgui/datautils/xrayutils/CTRcalc.py b/orgui/datautils/xrayutils/CTRcalc.py index 735d2f1..88247eb 100644 --- a/orgui/datautils/xrayutils/CTRcalc.py +++ b/orgui/datautils/xrayutils/CTRcalc.py @@ -41,6 +41,7 @@ from .CTRuc import WaterModel, UnitCell from .CTRfilm import EpitaxyInterface, Film, PoissonSurface from .CTRoptics import ( + _specular_reflection, add_structural_to_sampled_profile, combine_profiles, simplify_profile, @@ -1195,8 +1196,10 @@ def wavefield( amplitudes ``A_plus`` and ``A_minus``. Slabs are ordered from the incident medium towards the substrate. - :param float alpha: - Glancing incidence angle in degrees inside the incident medium. + :param float or numpy.ndarray alpha: + Glancing incidence angle or angle array in degrees inside the + incident medium. For arrays, wavefield quantities have the layer + axis first followed by the original angle-array shape. :param str polarization: ``"s"`` or ``"p"``. :param float delta_tolerance: @@ -1251,28 +1254,20 @@ def specular_reflectivity( ) alpha_array = np.asarray(alpha, dtype=np.float64) scalar = alpha_array.ndim == 0 - angles = np.atleast_1d(alpha_array) + angles = np.atleast_1d(alpha_array).ravel() profile = self.stratified_optical_profile( delta_tolerance, beta_tolerance ) def reflectivity(pol): - return np.asarray( - [ - abs( - solve_wavefield( - profile.values, - self.uc_bulk._E, - angle, - pol, - boundaries=profile.boundaries, - ).r_S - ) - ** 2 - for angle in angles - ], - dtype=np.float64, + reflection = _specular_reflection( + profile.values, + self.uc_bulk._E, + angles, + pol, + boundaries=profile.boundaries, ) + return np.abs(reflection) ** 2 if polarization == "unpolarized": result = 0.5 * (reflectivity("s") + reflectivity("p")) diff --git a/orgui/datautils/xrayutils/CTRoptics.py b/orgui/datautils/xrayutils/CTRoptics.py index aa5d5c4..df3e212 100644 --- a/orgui/datautils/xrayutils/CTRoptics.py +++ b/orgui/datautils/xrayutils/CTRoptics.py @@ -21,7 +21,8 @@ class Wavefield: :param numpy.ndarray z: Sample positions in Angstrom, ordered from substrate to ambient. :param numpy.ndarray psi: - Complex electric field sampled at ``z``. + Complex electric field sampled at ``z``. The layer axis is first; + remaining axes match a nonscalar incidence-angle input. :param numpy.ndarray z_interfaces: Interface positions in Angstrom, ordered from ambient to substrate. :param numpy.ndarray z_reference: @@ -29,14 +30,14 @@ class Wavefield: :param numpy.ndarray n: Complex slab refractive indices, ambient first. :param numpy.ndarray kz: - Renaud normal wavevectors in inverse Angstrom. + Renaud normal wavevectors in inverse Angstrom. The layer axis is first. :param numpy.ndarray A_plus: Downward-propagating slab amplitudes. :param numpy.ndarray A_minus: Upward-propagating slab amplitudes. - :param complex r_S: + :param complex or numpy.ndarray r_S: Specular reflection amplitude in the incident medium. - :param complex t_S: + :param complex or numpy.ndarray t_S: Transmission amplitude at the substrate boundary. :param str polarization: ``"s"`` or ``"p"``. @@ -515,6 +516,17 @@ def _normal_wavevector(n, k0, alpha_rad): return q +def _normal_wavevectors(n, k0, alpha_rad): + """Return normal propagation constants for a vector of incidence angles.""" + k_parallel_over_k0 = n[0] * np.cos(alpha_rad) + argument = n[:, np.newaxis] ** 2 - k_parallel_over_k0**2 + q = k0 * np.sqrt(argument) + q = np.where(q.imag > 0.0, -q, q) + q = np.where((q.imag == 0.0) & (q.real < 0.0), -q, q) + q[0] = k0 * n[0] * np.sin(alpha_rad) + return q + + def _admittance(n, q, polarization): """Return scalar optical admittance for s or p polarization.""" if polarization == "s": @@ -524,10 +536,306 @@ def _admittance(n, q, polarization): raise ValueError("polarization must be 's' or 'p'.") +def _propagation_terms(n, q, thickness, polarization): + """Return finite state-propagation terms, including the ``q = 0`` limit.""" + phase = q * thickness + cosine = np.cos(phase) + sinc = np.sinc(phase / np.pi) + if polarization == "s": + sin_over_admittance = thickness * sinc + admittance_sin = q**2 * thickness * sinc + else: + sin_over_admittance = q**2 * thickness * sinc / n**2 + admittance_sin = n**2 * thickness * sinc + return cosine, sin_over_admittance, admittance_sin + + +def _propagate_state(field, derivative, n, q, thickness, polarization): + """Propagate the continuous field state downward through one slab.""" + cosine, sin_over_admittance, admittance_sin = _propagation_terms( + n, q, thickness, polarization + ) + return ( + cosine * field - 1j * sin_over_admittance * derivative, + cosine * derivative - 1j * admittance_sin * field, + ) + + +def _propagate_state_upward( + field, derivative, n, q, thickness, polarization +): + """Propagate the continuous field state upward through one slab.""" + cosine, sin_over_admittance, admittance_sin = _propagation_terms( + n, q, thickness, polarization + ) + return ( + cosine * field + 1j * sin_over_admittance * derivative, + cosine * derivative + 1j * admittance_sin * field, + ) + + +def _solve_regular_wavefield(n, q, z_interfaces, z_ascending, polarization): + """Solve angle vectors whose normal wavevectors are all nonzero.""" + admittance = _admittance(n[:, np.newaxis], q, polarization) + interface_r = (admittance[:-1] - admittance[1:]) / ( + admittance[:-1] + admittance[1:] + ) + reflection = np.empty_like(interface_r) + reflection[-1] = interface_r[-1] + for interface in range(len(interface_r) - 2, -1, -1): + thickness = z_interfaces[interface] - z_interfaces[interface + 1] + phase = np.exp(-1j * q[interface + 1] * thickness) + reflected_below = reflection[interface + 1] * phase**2 + reflection[interface] = ( + interface_r[interface] + reflected_below + ) / (1.0 + interface_r[interface] * reflected_below) + + A_plus = np.empty_like(q) + A_minus = np.empty_like(q) + z_reference = np.empty(len(n), dtype=np.float64) + A_plus[0] = 1.0 + A_minus[0] = reflection[0] + z_reference[0] = z_interfaces[0] + + down_at_interface = A_plus[0] + up_at_interface = A_minus[0] + for interface in range(len(interface_r)): + field = down_at_interface + up_at_interface + derivative = admittance[interface] * ( + down_at_interface - up_at_interface + ) + A_plus[interface + 1] = 0.5 * ( + field + derivative / admittance[interface + 1] + ) + A_minus[interface + 1] = 0.5 * ( + field - derivative / admittance[interface + 1] + ) + z_reference[interface + 1] = z_interfaces[interface] + if interface + 1 < len(interface_r): + thickness = z_interfaces[interface] - z_interfaces[interface + 1] + phase = np.exp(-1j * q[interface + 1] * thickness) + down_at_interface = A_plus[interface + 1] * phase + up_at_interface = A_minus[interface + 1] / phase + + psi = np.empty_like(q) + for ascending_index, z_value in enumerate(z_ascending): + medium = len(n) - 1 - ascending_index + depth = z_reference[medium] - z_value + psi[ascending_index] = ( + A_plus[medium] * np.exp(-1j * q[medium] * depth) + + A_minus[medium] * np.exp(1j * q[medium] * depth) + ) + return ( + A_plus, + A_minus, + z_reference, + psi, + A_minus[0], + A_plus[-1], + ) + + +def _solve_critical_wavefield(n, q, z_interfaces, z_ascending, polarization): + """Solve angle vectors containing an exact ``q = 0`` wavevector.""" + # Propagate a substrate radiation-condition state upward. For p + # polarization, scaling [F, D] as [q / n**2, 1] avoids the singular + # admittance n**2 / q and has the finite critical-angle limit [0, 1]. + if polarization == "s": + field_top = np.ones(q.shape[1], dtype=np.complex128) + derivative_top = q[-1].copy() + else: + field_top = q[-1] / n[-1] ** 2 + derivative_top = np.ones(q.shape[1], dtype=np.complex128) + for medium in range(len(n) - 2, 0, -1): + thickness = z_interfaces[medium - 1] - z_interfaces[medium] + field_top, derivative_top = _propagate_state_upward( + field_top, + derivative_top, + n[medium], + q[medium], + thickness, + polarization, + ) + + incident_admittance = _admittance(n[0], q[0], polarization) + normalization = 2.0 / ( + field_top + derivative_top / incident_admittance + ) + field_top *= normalization + derivative_top *= normalization + r_S = 0.5 * ( + field_top - derivative_top / incident_admittance + ) + + field_reference = np.empty_like(q) + derivative_reference = np.empty_like(q) + z_reference = np.empty(len(n), dtype=np.float64) + field_reference[0] = field_top + derivative_reference[0] = derivative_top + z_reference[0] = z_interfaces[0] + + field_at_interface = field_top + derivative_at_interface = derivative_top + for interface in range(len(n) - 1): + field_reference[interface + 1] = field_at_interface + derivative_reference[interface + 1] = derivative_at_interface + z_reference[interface + 1] = z_interfaces[interface] + if interface + 1 < len(n) - 1: + thickness = z_interfaces[interface] - z_interfaces[interface + 1] + field_at_interface, derivative_at_interface = _propagate_state( + field_at_interface, + derivative_at_interface, + n[interface + 1], + q[interface + 1], + thickness, + polarization, + ) + + if polarization == "s": + amplitude_difference = np.divide( + derivative_reference, + q, + out=np.zeros_like(derivative_reference), + where=q != 0.0, + ) + else: + amplitude_difference = ( + derivative_reference * q / n[:, np.newaxis] ** 2 + ) + A_plus = 0.5 * (field_reference + amplitude_difference) + A_minus = 0.5 * (field_reference - amplitude_difference) + A_plus[0] = 1.0 + A_minus[0] = r_S + A_plus[-1] = field_reference[-1] + A_minus[-1] = 0.0 + + psi = np.empty_like(q) + for ascending_index, z_value in enumerate(z_ascending): + medium = len(n) - 1 - ascending_index + depth = z_reference[medium] - z_value + psi[ascending_index], _ = _propagate_state( + field_reference[medium], + derivative_reference[medium], + n[medium], + q[medium], + depth, + polarization, + ) + return ( + A_plus, + A_minus, + z_reference, + psi, + r_S, + field_reference[-1], + ) + + +def _specular_reflection( + profile, energy_eV, alpha, polarization, boundaries=None +): + """Return reflection amplitudes for all angles in one vectorized solve.""" + profile = np.asarray(profile, dtype=np.float64) + alpha = np.asarray(alpha, dtype=np.float64) + if profile.ndim != 2 or profile.shape[1] != 3 or len(profile) < 2: + raise ValueError("profile must be an (N, 3) array with at least two rows.") + if alpha.ndim != 1: + raise ValueError("alpha must be one-dimensional.") + if not np.all(np.isfinite(alpha)) or np.any(alpha <= 0.0) or np.any( + alpha > 90.0 + ): + raise ValueError("alpha must contain values in (0, 90] degrees.") + if energy_eV <= 0.0: + raise ValueError("energy_eV must be positive.") + if polarization not in {"s", "p"}: + raise ValueError("polarization must be 's' or 'p'.") + + if boundaries is None: + boundaries = profile_boundaries(profile) + else: + boundaries = np.asarray(boundaries, dtype=np.float64) + wavelength = HC_KEV_ANGSTROM / (energy_eV * 1e-3) + k0 = 2.0 * np.pi / wavelength + n = np.ascontiguousarray( + (1.0 - profile[:, 1] - 1j * profile[:, 2])[::-1] + ) + z_interfaces = np.ascontiguousarray(boundaries[::-1]) + q = _normal_wavevectors(n, k0, np.deg2rad(alpha)) + reflection = np.empty(len(alpha), dtype=np.complex128) + critical = np.any(q == 0.0, axis=0) + + regular = ~critical + if np.any(regular): + all_regular = np.all(regular) + q_regular = q if all_regular else q[:, regular] + if polarization == "s": + interface_r = ( + q_regular[:-1] - q_regular[1:] + ) / (q_regular[:-1] + q_regular[1:]) + else: + n_squared = n**2 + interface_r = ( + n_squared[:-1, np.newaxis] * q_regular[1:] + - n_squared[1:, np.newaxis] * q_regular[:-1] + ) / ( + n_squared[:-1, np.newaxis] * q_regular[1:] + + n_squared[1:, np.newaxis] * q_regular[:-1] + ) + reflected = interface_r[-1].copy() + for interface in range(len(n) - 3, -1, -1): + thickness = ( + z_interfaces[interface] - z_interfaces[interface + 1] + ) + phase = np.exp( + -1j * q_regular[interface + 1] * thickness + ) + reflected_below = reflected * phase**2 + reflected = ( + interface_r[interface] + reflected_below + ) / (1.0 + interface_r[interface] * reflected_below) + if all_regular: + reflection[:] = reflected + else: + reflection[regular] = reflected + + if np.any(critical): + q_critical = q[:, critical] + if polarization == "s": + field_top = np.ones(np.count_nonzero(critical), dtype=np.complex128) + derivative_top = q_critical[-1].copy() + else: + field_top = q_critical[-1] / n[-1] ** 2 + derivative_top = np.ones( + np.count_nonzero(critical), dtype=np.complex128 + ) + for medium in range(len(n) - 2, 0, -1): + thickness = ( + z_interfaces[medium - 1] - z_interfaces[medium] + ) + field_top, derivative_top = _propagate_state_upward( + field_top, + derivative_top, + n[medium], + q_critical[medium], + thickness, + polarization, + ) + if polarization == "s": + incident_admittance = q_critical[0] + else: + incident_admittance = n[0] ** 2 / q_critical[0] + state_sum = field_top + derivative_top / incident_admittance + reflection[critical] = ( + field_top - derivative_top / incident_admittance + ) / state_sum + + return reflection + + def solve_wavefield( profile, energy_eV, alpha, polarization="s", boundaries=None ): - """Solve the layered unperturbed wavefield by stable Parratt recursion. + """Solve the layered unperturbed wavefield by stable layered propagation. The profile rows define optical media at layer centers. By default, their interfaces are placed at adjacent-center midpoints. Explicit boundaries @@ -536,13 +844,17 @@ def solve_wavefield( Angles are glancing angles measured from the surface inside the incident medium. The conserved tangential wavevector is therefore ``k_x = n_0 k_0 cos(alpha)``, where ``n_0`` is the last profile row. + The propagated field and admittance-weighted derivative use their analytic + ``q_z = 0`` limits, including the p-polarized critical angle. :param numpy.ndarray profile: ``(N, 3)`` optical profile with z in Angstrom, delta, and beta. :param float energy_eV: X-ray photon energy in eV. - :param float alpha: - Glancing angle in degrees inside the incident medium. + :param float or numpy.ndarray alpha: + Glancing angle or angle array in degrees inside the incident medium. + For arrays, wavefield quantities have the layer axis first followed by + the original angle-array shape. :param str polarization: ``"s"`` or ``"p"``. :param numpy.ndarray boundaries: @@ -572,72 +884,72 @@ def solve_wavefield( raise ValueError("each boundary must lie between adjacent layer centers.") if energy_eV <= 0.0: raise ValueError("energy_eV must be positive.") - if alpha <= 0.0 or alpha > 90.0: - raise ValueError("alpha must be in the interval (0, 90] degrees.") + alpha_array = np.asarray(alpha, dtype=np.float64) + scalar = alpha_array.ndim == 0 + if alpha_array.size == 0 or not np.all(np.isfinite(alpha_array)): + raise ValueError("alpha must contain finite angle values.") + if np.any(alpha_array <= 0.0) or np.any(alpha_array > 90.0): + raise ValueError("alpha must contain values in (0, 90] degrees.") if polarization not in {"s", "p"}: raise ValueError("polarization must be 's' or 'p'.") wavelength = HC_KEV_ANGSTROM / (energy_eV * 1e-3) k0 = 2.0 * np.pi / wavelength - alpha_rad = np.deg2rad(alpha) + angle_shape = alpha_array.shape + alpha_rad = np.deg2rad(np.atleast_1d(alpha_array).ravel()) z_ascending = profile[:, 0] n_ascending = 1.0 - profile[:, 1] - 1j * profile[:, 2] n = np.ascontiguousarray(n_ascending[::-1]) z_interfaces = np.ascontiguousarray(boundaries[::-1]) - q = _normal_wavevector(n, k0, alpha_rad) - kz = -q - admittance = _admittance(n, q, polarization) + q = _normal_wavevectors(n, k0, alpha_rad) + critical = np.any(q == 0.0, axis=0) + regular = ~critical - interface_r = (admittance[:-1] - admittance[1:]) / ( - admittance[:-1] + admittance[1:] - ) - reflection = np.empty(len(interface_r), dtype=np.complex128) - reflection[-1] = interface_r[-1] - for interface in range(len(interface_r) - 2, -1, -1): - thickness = z_interfaces[interface] - z_interfaces[interface + 1] - phase = np.exp(-1j * q[interface + 1] * thickness) - reflected_below = reflection[interface + 1] * phase**2 - reflection[interface] = ( - interface_r[interface] + reflected_below - ) / (1.0 + interface_r[interface] * reflected_below) - - A_plus = np.empty(len(n), dtype=np.complex128) - A_minus = np.empty(len(n), dtype=np.complex128) - z_reference = np.empty(len(n), dtype=np.float64) - A_plus[0] = 1.0 - A_minus[0] = reflection[0] - z_reference[0] = z_interfaces[0] - - down_at_interface = A_plus[0] - up_at_interface = A_minus[0] - for interface in range(len(interface_r)): - field = down_at_interface + up_at_interface - derivative = admittance[interface] * ( - down_at_interface - up_at_interface + if np.all(regular): + result = _solve_regular_wavefield( + n, q, z_interfaces, z_ascending, polarization ) - A_plus[interface + 1] = 0.5 * ( - field + derivative / admittance[interface + 1] + elif np.all(critical): + result = _solve_critical_wavefield( + n, q, z_interfaces, z_ascending, polarization ) - A_minus[interface + 1] = 0.5 * ( - field - derivative / admittance[interface + 1] + else: + A_plus = np.empty_like(q) + A_minus = np.empty_like(q) + psi = np.empty_like(q) + r_S = np.empty(q.shape[1], dtype=np.complex128) + t_S = np.empty(q.shape[1], dtype=np.complex128) + regular_result = _solve_regular_wavefield( + n, q[:, regular], z_interfaces, z_ascending, polarization ) - z_reference[interface + 1] = z_interfaces[interface] - if interface + 1 < len(interface_r): - thickness = z_interfaces[interface] - z_interfaces[interface + 1] - phase = np.exp(-1j * q[interface + 1] * thickness) - down_at_interface = A_plus[interface + 1] * phase - up_at_interface = A_minus[interface + 1] / phase - - psi = np.empty(len(n), dtype=np.complex128) - for ascending_index, z_value in enumerate(z_ascending): - medium = len(n) - 1 - ascending_index - depth = z_reference[medium] - z_value - psi[ascending_index] = ( - A_plus[medium] * np.exp(-1j * q[medium] * depth) - + A_minus[medium] * np.exp(1j * q[medium] * depth) + critical_result = _solve_critical_wavefield( + n, q[:, critical], z_interfaces, z_ascending, polarization ) - psi = np.ascontiguousarray(psi) + A_plus[:, regular], A_minus[:, regular], z_reference, psi[ + :, regular + ], r_S[regular], t_S[regular] = regular_result + A_plus[:, critical], A_minus[:, critical], _, psi[ + :, critical + ], r_S[critical], t_S[critical] = critical_result + result = A_plus, A_minus, z_reference, psi, r_S, t_S + A_plus, A_minus, z_reference, psi, r_S, t_S = result + + if scalar: + kz = -q[:, 0] + A_plus = A_plus[:, 0] + A_minus = A_minus[:, 0] + psi = psi[:, 0] + r_S = complex(r_S[0]) + t_S = complex(t_S[0]) + else: + field_shape = (len(n),) + angle_shape + kz = (-q).reshape(field_shape) + A_plus = A_plus.reshape(field_shape) + A_minus = A_minus.reshape(field_shape) + psi = psi.reshape(field_shape) + r_S = r_S.reshape(angle_shape) + t_S = t_S.reshape(angle_shape) return Wavefield( z=np.ascontiguousarray(z_ascending), @@ -648,7 +960,7 @@ def solve_wavefield( kz=np.ascontiguousarray(kz), A_plus=np.ascontiguousarray(A_plus), A_minus=np.ascontiguousarray(A_minus), - r_S=complex(A_minus[0]), - t_S=complex(A_plus[-1]), + r_S=r_S, + t_S=t_S, polarization=polarization, ) diff --git a/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py b/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py index c4c8f19..1221dde 100644 --- a/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py +++ b/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py @@ -380,6 +380,125 @@ def test_single_interface_critical_angle(self): self.assertLess(abs(above.r_S) ** 2, 1.0) self.assertLess((-below.kz[-1]).imag, 0.0) + def test_p_polarization_is_finite_at_exact_substrate_critical_angle(self): + delta = 1.0e-5 + profile = np.array([[-10.0, delta, 0.0], [0.0, 0.0, 0.0]]) + alpha_c = np.rad2deg(np.arccos(1.0 - delta)) + + field = CTRoptics.solve_wavefield( + profile, self.energy_eV, alpha_c, "p" + ) + + np.testing.assert_allclose(-field.kz[-1], 0.0, atol=0.0) + np.testing.assert_allclose(field.r_S, -1.0, rtol=1e-14) + np.testing.assert_allclose(field.t_S, 0.0, atol=0.0) + self.assertTrue(np.all(np.isfinite(field.psi))) + self.assertTrue(np.all(np.isfinite(field.A_plus))) + self.assertTrue(np.all(np.isfinite(field.A_minus))) + + def test_p_polarization_is_continuous_at_internal_critical_angle(self): + delta = 1.0e-5 + profile = np.array( + [ + [-40.0, 2.0 * delta, 0.0], + [-10.0, delta, 0.0], + [20.0, 0.0, 0.0], + ] + ) + alpha_c = np.rad2deg(np.arccos(1.0 - delta)) + + exact = CTRoptics.solve_wavefield( + profile, self.energy_eV, alpha_c, "p" + ) + below = CTRoptics.solve_wavefield( + profile, self.energy_eV, alpha_c * (1.0 - 1.0e-8), "p" + ) + above = CTRoptics.solve_wavefield( + profile, self.energy_eV, alpha_c * (1.0 + 1.0e-8), "p" + ) + + np.testing.assert_allclose(-exact.kz[1], 0.0, atol=0.0) + self.assertTrue(np.all(np.isfinite(exact.psi))) + self.assertTrue(np.isfinite(exact.r_S)) + np.testing.assert_allclose(below.r_S, exact.r_S, rtol=1e-7) + np.testing.assert_allclose(above.r_S, exact.r_S, rtol=1e-7) + np.testing.assert_allclose(below.psi, exact.psi, rtol=1e-7) + np.testing.assert_allclose(above.psi, exact.psi, rtol=1e-7) + + def test_vectorized_reflection_matches_scalar_wavefields(self): + delta = 1.0e-5 + profile = np.array( + [ + [-40.0, 2.0 * delta, 0.0], + [-10.0, delta, 0.0], + [20.0, 0.0, 0.0], + ] + ) + alpha_c = np.rad2deg(np.arccos(1.0 - delta)) + angles = np.array([0.1, alpha_c, 0.5, 1.0]) + + for polarization in ("s", "p"): + actual = CTRoptics._specular_reflection( + profile, self.energy_eV, angles, polarization + ) + expected = np.array( + [ + CTRoptics.solve_wavefield( + profile, self.energy_eV, angle, polarization + ).r_S + for angle in angles + ] + ) + + np.testing.assert_allclose(actual, expected, rtol=1e-12, atol=1e-12) + + def test_vectorized_wavefield_matches_scalar_wavefields(self): + delta = 1.0e-5 + profile = np.array( + [ + [-40.0, 2.0 * delta, 0.0], + [-10.0, delta, 0.0], + [20.0, 0.0, 0.0], + ] + ) + alpha_c = np.rad2deg(np.arccos(1.0 - delta)) + angles = np.array([[0.1, alpha_c], [0.5, 1.0]]) + + vector = CTRoptics.solve_wavefield( + profile, self.energy_eV, angles, "p" + ) + + self.assertEqual(vector.psi.shape, (len(profile), *angles.shape)) + self.assertEqual(vector.kz.shape, (len(profile), *angles.shape)) + self.assertEqual(vector.A_plus.shape, (len(profile), *angles.shape)) + self.assertEqual(vector.A_minus.shape, (len(profile), *angles.shape)) + self.assertEqual(vector.r_S.shape, angles.shape) + self.assertEqual(vector.t_S.shape, angles.shape) + for index in np.ndindex(angles.shape): + scalar = CTRoptics.solve_wavefield( + profile, self.energy_eV, angles[index], "p" + ) + np.testing.assert_allclose( + vector.psi[(slice(None), *index)], + scalar.psi, + rtol=1e-12, + atol=1e-12, + ) + np.testing.assert_allclose( + vector.A_plus[(slice(None), *index)], + scalar.A_plus, + rtol=1e-12, + atol=1e-12, + ) + np.testing.assert_allclose( + vector.A_minus[(slice(None), *index)], + scalar.A_minus, + rtol=1e-12, + atol=1e-12, + ) + np.testing.assert_allclose(vector.r_S[index], scalar.r_S) + np.testing.assert_allclose(vector.t_S[index], scalar.t_S) + def test_one_film_matches_analytic_slab_expression(self): thickness = 35.0 profile = np.array( @@ -415,6 +534,11 @@ def test_crystal_reflectivity_polarizations(self): np.testing.assert_allclose(unpolarized, 0.5 * (s + p)) self.assertIsInstance(crystal.specular_reflectivity(1.0), float) self.assertEqual(crystal.wavefield(1.0).polarization, "s") + grid = angles.reshape(1, -1) + np.testing.assert_allclose( + crystal.specular_reflectivity(grid, "p"), + p.reshape(1, -1), + ) def test_crystal_simplification_removes_redundant_bulk_layers(self): bulk = unitcells.unitcell("Pt100") From 5ce82e1b6c9f3ccacee264e2ef62cbab8e85ee9f Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Wed, 29 Jul 2026 12:03:12 -0400 Subject: [PATCH 36/37] fix(phys): correct ionic optics and Windows wheel arguments Use ionic forward scattering factors for charged species, quote Meson setup arguments correctly, and preserve multidimensional angle shapes. --- .github/workflows/build-wheels.yml | 2 +- .github/workflows/release.yml | 2 +- orgui/datautils/xrayutils/CTRoptics.py | 21 ++++++++++++++++--- .../xrayutils/test/test_CTRoptical_profile.py | 20 ++++++++++++++++++ 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index cf91c70..8be61b1 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -17,7 +17,7 @@ env: CIBW_SKIP: "*-musllinux_*" CIBW_BUILD_FRONTEND: "build" CIBW_CONFIG_SETTINGS: "setup-args=-Dstatic_xxhash=true" - CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--vsenv -Dstatic_xxhash=true" + CIBW_CONFIG_SETTINGS_WINDOWS: 'setup-args="--vsenv -Dstatic_xxhash=true"' CIBW_TEST_ENVIRONMENT: "PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 MPLBACKEND=Agg QT_QPA_PLATFORM=minimal" CIBW_TEST_REQUIRES: pytest CIBW_TEST_EXTRAS: extendedfilesupport,symmetry diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8c51a6..c83738b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -122,7 +122,7 @@ jobs: CIBW_SKIP: "*-musllinux_*" CIBW_BUILD_FRONTEND: "build" CIBW_CONFIG_SETTINGS: "setup-args=-Dstatic_xxhash=true" - CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--vsenv -Dstatic_xxhash=true" + CIBW_CONFIG_SETTINGS_WINDOWS: 'setup-args="--vsenv -Dstatic_xxhash=true"' CIBW_TEST_ENVIRONMENT: "PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 MPLBACKEND=Agg QT_QPA_PLATFORM=minimal" CIBW_TEST_REQUIRES: pytest CIBW_TEST_EXTRAS: extendedfilesupport,symmetry diff --git a/orgui/datautils/xrayutils/CTRoptics.py b/orgui/datautils/xrayutils/CTRoptics.py index df3e212..1be53ae 100644 --- a/orgui/datautils/xrayutils/CTRoptics.py +++ b/orgui/datautils/xrayutils/CTRoptics.py @@ -148,12 +148,27 @@ def optical_profile(unit_cell, include_coherent_domains=True): profile = np.empty((len(layer_ids) * len(domain_matrices), 3), dtype=np.float64) row = 0 for layer_index, (layer_id, layer_cell) in enumerate(layers.items()): - atomic_numbers = np.asarray( - [atomic_number(name) for name in layer_cell.names], dtype=np.float64 + forward_static_factor = np.asarray( + [atomic_number(name) for name in layer_cell.names], + dtype=np.float64, ) + ionic_species = np.asarray( + [name.endswith(("+", "-")) for name in layer_cell.names], + dtype=bool, + ) + ionic_forward_factor = ( + np.sum(layer_cell.f[:, :5], axis=1) + layer_cell.f[:, 10] + ) + forward_static_factor[ionic_species] = ionic_forward_factor[ + ionic_species + ] forward_factor = np.sum( layer_cell.basis[:, 6] - * (atomic_numbers + layer_cell.f[:, 11] + 1j * layer_cell.f[:, 12]) + * ( + forward_static_factor + + layer_cell.f[:, 11] + + 1j * layer_cell.f[:, 12] + ) ) layer_volume = unit_cell.volume * thickness_fraction[layer_index] optical_density = scale * forward_factor / layer_volume diff --git a/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py b/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py index 1221dde..ee86cd3 100644 --- a/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py +++ b/orgui/datautils/xrayutils/test/test_CTRoptical_profile.py @@ -41,6 +41,26 @@ def test_pt100_matches_xraydb(self): np.testing.assert_allclose(profile[:, 1], delta, rtol=1e-7) np.testing.assert_allclose(profile[:, 2], beta, rtol=1e-7) + def test_ionic_profile_uses_ionic_forward_scattering_factor(self): + cell = CTRcalc.UnitCell([10.0, 10.0, 10.0], [90.0, 90.0, 90.0]) + cell.addAtom("O2-", [0.0, 0.0, 0.0], 0.1, 0.1, 1.0) + cell.setEnergy(self.energy_eV) + + profile = cell.optical_profile() + + wavelength = CTRoptics.HC_KEV_ANGSTROM / (self.energy_eV * 1e-3) + scale = 2.8179403262e-5 * wavelength**2 / (2.0 * np.pi) + ionic_f0 = np.sum(cell.f[0, :5]) + cell.f[0, 10] + expected = scale * ( + ionic_f0 + cell.f[0, 11] + 1j * cell.f[0, 12] + ) / cell.volume + np.testing.assert_allclose( + profile[0, 1:], + [expected.real, expected.imag], + rtol=1e-14, + ) + self.assertGreater(ionic_f0, xraydb.atomic_number("O")) + def test_tio2_100_has_two_homogeneous_layers(self): cell = unitcells.crystal("TiO2(100)").uc_bulk cell.setEnergy(self.energy_eV) From d2b7fb118b2fcf6371855c24414dc8e68113b05b Mon Sep 17 00:00:00 2001 From: "T. Fuchs" Date: Wed, 29 Jul 2026 12:08:28 -0400 Subject: [PATCH 37/37] docs: update CHANGELOG for PR 65 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f601605..980a086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,19 @@ This is the changelog for the software orGUI, written by Timo Fuchs Scientific and analysis additions: +- Added interface-based optical ``delta``/``beta`` profiles and layered + wavefield calculations for s- and p-polarized X-rays. Scalar and + multidimensional angle inputs are supported, and specular reflectivity can + be calculated for s, p, or unpolarized incidence. This provides the optical + wavefield foundation for future distorted-wave Born approximation (DWBA) + calculations; the full distorted-wave scattering amplitude is not included + yet. - Added optional CTR intensity-resolution modeling with constant or gamma-dependent box and Gaussian functions. Calculations can convolve irregular existing L points or sample the crystal structure factor with deterministic quadrature. +- Added Poisson-distributed surface occupancies and coherent out-of-plane + epitaxy/strain coupling for film-interface models. - Added py3Dmol atom-sphere rendering for Jupyter notebooks. ``plot3d`` now selects py3Dmol automatically in a notebook, can be directed to either py3Dmol or Mayavi explicitly, and can incrementally add unit cells to a @@ -26,6 +35,22 @@ Scientific and analysis additions: parameter fitting was development-only and never part of a release, so no migration is provided. +Scientific correctness and performance fixes: + +- Optical profiles now preserve areal optical content under surface-normal + strain, keep ionic forward scattering factors for charged species, avoid + merging layers beyond the requested z tolerance, and remain finite at the + exact p-polarized critical-angle limit. +- Optical reflectivity and full wavefield calculations now evaluate angle + arrays with NumPy-vectorized kernels while preserving the caller's input + shape. +- Corrected film and epitaxy-interface anchoring, stacking, and support + ownership across strained interfaces. +- Fixed missing support in one-dimensional electron-density calculations and + corrected atomic-coordinate stacking when splitting unit cells into layers. +- Added a C++ electron-density backend and bounded caches for atomic form + factors, anomalous scattering factors, and accelerated form-factor lookup. + A ***critical bug*** was fixed that affects bulk CTR calculations: - ``UnitCell.F_bulk``'s semi-infinite geometric lattice sum used the raw,