From d7d26513b04bf0d7eb1e23799eb69c6befc5daef Mon Sep 17 00:00:00 2001 From: gbrunin Date: Fri, 29 May 2026 14:01:29 +0200 Subject: [PATCH 01/31] Removed .npy files and directory logic, use Result object instead. Cleaning visualization module. --- README.md | 6 +- docs/examples/binning_ca.py | 5 +- docs/examples/slicing_ca.py | 5 +- docs/examples/visualisation_slicing_traj.py | 8 +- docs/source/tutorials/Binning_method_tuto.rst | 48 +- docs/source/tutorials/Slicing_method_tuto.rst | 32 +- .../Visualization_slicing_droplet.rst | 16 +- ...zation_trajectories_comparison_methods.rst | 168 ---- docs/source/tutorials/index.rst | 1 - .../Visualization_slicing_droplet.md | 20 +- ...ization_trajectories_comparison_methods.md | 144 ---- src/wetting_angle_kit/analysis/__init__.py | 14 +- src/wetting_angle_kit/analysis/analyzer.py | 101 +-- .../analysis/binning/angle_fitting.py | 235 +----- .../analysis/binning/results.py | 91 +++ .../analysis/slicing/parallel.py | 55 +- .../analysis/slicing/results.py | 53 ++ .../visualization/__init__.py | 42 +- .../visualization/animator.py | 219 +++++ .../visualization/base_trajectory_analyzer.py | 298 ------- .../visualization/base_trajectory_plotter.py | 16 + .../binning_trajectory_analyzer.py | 168 ---- .../binning_trajectory_plotter.py | 199 +++++ .../visualization/droplet_slice_plot.py | 225 ++++++ .../visualization/droplet_slice_plots.py | 758 ------------------ .../visualization/method_comparison.py | 314 -------- .../slicing_trajectory_analyzer.py | 234 ------ .../slicing_trajectory_plotter.py | 147 ++++ src/wetting_angle_kit/visualization/stats.py | 44 + .../visualization/surface_plots.py | 143 ---- tests/test_analysis/test_binning_method.py | 31 +- .../test_analysis/test_slicing_edge_cases.py | 15 +- tests/test_analysis/test_slicing_method.py | 20 +- tests/test_edge_cases.py | 32 +- .../test_droplet_slice_plot.py | 142 ++++ .../test_droplet_slice_plots.py | 223 ------ .../test_method_comparison.py | 106 --- .../test_visualization/test_surface_plots.py | 84 -- .../test_trajectory_analyzers.py | 185 ----- .../test_trajectory_plotters.py | 180 +++++ 40 files changed, 1511 insertions(+), 3316 deletions(-) delete mode 100644 docs/source/tutorials/Visualization_trajectories_comparison_methods.rst delete mode 100644 docs/tutorials/Visualization_trajectories_comparison_methods.md create mode 100644 src/wetting_angle_kit/analysis/binning/results.py create mode 100644 src/wetting_angle_kit/analysis/slicing/results.py create mode 100644 src/wetting_angle_kit/visualization/animator.py delete mode 100644 src/wetting_angle_kit/visualization/base_trajectory_analyzer.py create mode 100644 src/wetting_angle_kit/visualization/base_trajectory_plotter.py delete mode 100644 src/wetting_angle_kit/visualization/binning_trajectory_analyzer.py create mode 100644 src/wetting_angle_kit/visualization/binning_trajectory_plotter.py create mode 100644 src/wetting_angle_kit/visualization/droplet_slice_plot.py delete mode 100644 src/wetting_angle_kit/visualization/droplet_slice_plots.py delete mode 100644 src/wetting_angle_kit/visualization/method_comparison.py delete mode 100644 src/wetting_angle_kit/visualization/slicing_trajectory_analyzer.py create mode 100644 src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py create mode 100644 src/wetting_angle_kit/visualization/stats.py delete mode 100644 src/wetting_angle_kit/visualization/surface_plots.py create mode 100644 tests/test_visualization/test_droplet_slice_plot.py delete mode 100644 tests/test_visualization/test_droplet_slice_plots.py delete mode 100644 tests/test_visualization/test_method_comparison.py delete mode 100644 tests/test_visualization/test_surface_plots.py delete mode 100644 tests/test_visualization/test_trajectory_analyzers.py create mode 100644 tests/test_visualization/test_trajectory_plotters.py diff --git a/README.md b/README.md index ed08870..78e819c 100644 --- a/README.md +++ b/README.md @@ -71,20 +71,18 @@ parser = XYZParser(trajectory_file) slicing = SlicingContactAngleAnalyzer( parser, - output_dir="out_slicing", atom_indices=oxygen_ids, droplet_geometry="spherical", delta_gamma=5, ) results = slicing.analyze(frame_range=range(0, 50)) -print(results["mean_angle"], results["std_angle"]) +print(results.mean_angle, results.std_angle) binning = BinningContactAngleAnalyzer( parser, - output_dir="out_binned", atom_indices=oxygen_ids, droplet_geometry="spherical", ) results_binning = binning.analyze(frame_range=range(0, 200)) -print(results_binning["mean_angle"], results_binning["std_angle"]) +print(results_binning.mean_angle, results_binning.std_angle) ``` diff --git a/docs/examples/binning_ca.py b/docs/examples/binning_ca.py index a8c9225..b948ffe 100644 --- a/docs/examples/binning_ca.py +++ b/docs/examples/binning_ca.py @@ -35,13 +35,12 @@ analyzer = contact_angle_analyzer( method="binning", parser=parser, - output_dir="results_binning_example", atom_indices=oxygen_indices, droplet_geometry="spherical", # Interface fitting model binning_params=binning_params, - plot_graphs=True, # Enable plotting for automated runs ) # --- Step 7: Run analysis for a frame range --- results = analyzer.analyze([1]) # Analyze frame 1 -print("Analysis results:", results) +print("Mean contact angle (°):", results.mean_angle) +print("Std contact angle (°):", results.std_angle) diff --git a/docs/examples/slicing_ca.py b/docs/examples/slicing_ca.py index 5499180..2d42ebe 100644 --- a/docs/examples/slicing_ca.py +++ b/docs/examples/slicing_ca.py @@ -27,7 +27,6 @@ analyzer = contact_angle_analyzer( method="slicing", parser=parser, - output_dir="results_slicing_example", atom_indices=oxygen_indices, droplet_geometry="spherical", delta_gamma=20, # Azimuthal step for spherical slicing (degrees) @@ -35,5 +34,5 @@ # --- Step 4: Run analysis for a frame range --- results = analyzer.analyze([1]) -print("Mean contact angle (°):", results["mean_angle"]) -print("Frames analyzed:", results["frames_analyzed"]) +print("Frames analyzed:", results.frames) +print("Mean contact angle (°):", results.mean_angle) diff --git a/docs/examples/visualisation_slicing_traj.py b/docs/examples/visualisation_slicing_traj.py index f7b4222..ded3cf9 100644 --- a/docs/examples/visualisation_slicing_traj.py +++ b/docs/examples/visualisation_slicing_traj.py @@ -49,15 +49,15 @@ print("Per-slice contact angles (°):", list_alfas) # --- 5. Visualize the Droplet --- -plotter = DropletSlicePlotter(center=True, show_wall=True, molecule_view=True) +plotter = DropletSlicePlotter(center=True) -plotter.plot_surface_points( +fig = plotter.plot_surface_points( oxygen_position=oxygen_position, surface_data=array_surfaces, popt=array_popt[0], wall_coords=wall_coords, - output_filename="droplet_plot.png", alpha=list_alfas[0], ) -print("Plot saved as 'droplet_plot.png'") +fig.write_html("droplet_plot.html") +print("Plot saved as 'droplet_plot.html'") diff --git a/docs/source/tutorials/Binning_method_tuto.rst b/docs/source/tutorials/Binning_method_tuto.rst index b323530..7fddfd1 100644 --- a/docs/source/tutorials/Binning_method_tuto.rst +++ b/docs/source/tutorials/Binning_method_tuto.rst @@ -74,17 +74,15 @@ Example trajectory:: analyzer = contact_angle_analyzer( method="binning", parser=parser, - output_dir="results_binned_example", atom_indices=oxygen_indices, droplet_geometry="cylinder_y", # Interface fitting model width_cylinder=21, # Width parameter for interface fit binning_params=binning_params, - plot_graphs=False, # Disable plotting for automated runs ) # --- Step 7: Run analysis for a frame range --- results = analyzer.analyze([1]) # Analyze frame 1 - print("Analysis results:", results) + print("Mean contact angle (°):", results.mean_angle) ---- @@ -93,39 +91,30 @@ Example trajectory:: Running this example will: -- Parse the trajectory -- Compute the interface shape and local contact angle -- Save results (if enabled) under ``results_binned_example/`` +- Parse the trajectory. +- Compute the interface shape and local contact angle for each batch. +- Return a :class:`BinningResults` dataclass holding angles, density fields + and fitted isolines for every batch (no files written). Example printed output:: Number of water molecules: 4000 + Mean contact angle (°): 94.58987060394456 - xi range: (0.22795857644950415,41.63623606829102) - zi range: (7.54989,47.3742) +The returned ``results`` object exposes ``mean_angle``, ``std_angle``, +``angles_per_batch`` and a ``batches`` list whose entries carry the +density field (``xi_cc``, ``zi_cc``, ``rho_cc``) and the fitted +droplet / wall isoline coordinates. Feed it directly to +:class:`BinningTrajectoryPlotter` to draw the interactive density +contour with the fitted semi-circle: - Number of fluid particles in batch: 4000.0 - - Binning with model: spherical ... - Advancement: 0.00% - Advancement: 35.71% - Advancement: 71.43% - - Fitted parameters for batch: - rho1:-3.387136459516587e-05 - rho2:0.03389671977759232 - R_eq:37.22899870907881 - zi_c:9.244210981996149 - zi_0:6.265045941194059 - t1:-4.384696208816467 - t2:0.07378719793487698 - - Contact angle for batch: 94.58987060394456 +.. code-block:: python -A heat map representation of the particles density and the fitted semi-circle to get the contact angle. + from wetting_angle_kit.visualization import BinningTrajectoryPlotter -.. image:: ../../images/bin_plot.png - :alt: Heat maps density particles + plotter = BinningTrajectoryPlotter(results) + fig = plotter.plot_density_contour(batch_index=0) + fig.show() ---- @@ -134,5 +123,4 @@ A heat map representation of the particles density and the fitted semi-circle to - Adjust ``xi_f``, ``zi_f``, and the bin counts (``nbins_xi``, ``nbins_zi``) according to your simulation box dimensions. - If the wall surface is not flat or the system is tilted, pre-align it before analysis. -- Use ``plot_graphs=True`` to visualize the binning density and interface fitting. -- For multiple frames: ``analyzer.analyze(range(0, 100, 10))``. +- Multi-batch analysis: ``analyzer.analyze(range(0, 100), split_factor=10)`` splits the frame range into batches of ten frames each. diff --git a/docs/source/tutorials/Slicing_method_tuto.rst b/docs/source/tutorials/Slicing_method_tuto.rst index 8f8f941..eee0bf3 100644 --- a/docs/source/tutorials/Slicing_method_tuto.rst +++ b/docs/source/tutorials/Slicing_method_tuto.rst @@ -60,7 +60,6 @@ Example trajectory:: analyzer = contact_angle_analyzer( method="slicing", parser=parser, - output_dir="result_dump_spherical_slicing", atom_indices=oxygen_indices, droplet_geometry="spherical", # Geometry fitting model delta_gamma=20, # Azimuthal step (deg) for spherical slicing @@ -70,7 +69,9 @@ Example trajectory:: results = analyzer.analyze([1]) # Analyze frame 1 # --- Step 7: Display results --- - print("Analysis results:", results) + print("Mean contact angle (°):", results.mean_angle) + print("Std contact angle (°):", results.std_angle) + print("Frames analyzed:", results.frames) ---- @@ -80,26 +81,23 @@ Example trajectory:: After running the example, you'll see something like:: Number of water molecules: 1320 - Analysis results: { - 'mean_angle': 94.46, - 'std_angle': 0.0, - 'angles': {1: 94.46}, - 'frames_analyzed': [1], - 'method_metadata': {'frames_per_angle': 1}, - } + Mean contact angle (°): 94.46 + Std contact angle (°): 0.0 + Frames analyzed: [1] -The ``analyze`` return dict has these keys: +``analyze`` returns a :class:`SlicingResults` dataclass with the +following convenience attributes: * ``mean_angle`` — mean contact angle (°) across the analyzed frames. * ``std_angle`` — standard deviation across frames. -* ``angles`` — mapping ``frame_index -> mean angle for that frame``. -* ``frames_analyzed`` — list of frame indices that were processed. +* ``per_frame_mean_angles`` — array of per-frame mean angles (one per slice + aggregated to a single number). +* ``frames`` — list of frame indices that were processed. +* ``angles`` / ``surfaces`` / ``popts`` — raw per-frame data passed + directly to :class:`SlicingTrajectoryPlotter` for visualization. * ``method_metadata`` — method-specific info (e.g. number of frames per angle value). -Per-frame raw outputs (alfas, surfaces, popt) are saved as ``.npy`` files -inside the output directory. - ---- 5. Tips @@ -156,7 +154,6 @@ inside the output directory. analyzer = contact_angle_analyzer( method="slicing", parser=parser, - output_dir="result_dump_spherical_slicing", atom_indices=oxygen_indices, droplet_geometry="spherical", # Fitting model delta_gamma=20, # Azimuthal step (deg) for spherical slicing @@ -166,4 +163,5 @@ inside the output directory. results = analyzer.analyze([1]) # Analyze frame 1 # --- Step 6: Display results --- - print("Analysis results:", results) + print("Mean contact angle (°):", results.mean_angle) + print("Std contact angle (°):", results.std_angle) diff --git a/docs/source/tutorials/Visualization_slicing_droplet.rst b/docs/source/tutorials/Visualization_slicing_droplet.rst index 5bd4830..911c70f 100644 --- a/docs/source/tutorials/Visualization_slicing_droplet.rst +++ b/docs/source/tutorials/Visualization_slicing_droplet.rst @@ -97,21 +97,19 @@ The visualization workflow involves the following steps: .. code-block:: python - plotter = DropletSlicePlotter(center=True, show_wall=True, molecule_view=True) + plotter = DropletSlicePlotter(center=True) - plotter.plot_surface_points( + fig = plotter.plot_surface_points( oxygen_position=oxygen_position, surface_data=array_surfaces, popt=array_popt[0], wall_coords=wall_coords, - output_filename="droplet_plot.png", alpha=list_alfas[0], ) - print(" Plot saved as 'droplet_plot.png'") + # Interactive view in a notebook + fig.show() -Outputs -------- - -.. image:: ../../images/droplet_plot.png - :alt: Droplet slicing method visualization + # Or save a standalone HTML page + fig.write_html("droplet_plot.html") + print("Plot saved as 'droplet_plot.html'") diff --git a/docs/source/tutorials/Visualization_trajectories_comparison_methods.rst b/docs/source/tutorials/Visualization_trajectories_comparison_methods.rst deleted file mode 100644 index 908d91e..0000000 --- a/docs/source/tutorials/Visualization_trajectories_comparison_methods.rst +++ /dev/null @@ -1,168 +0,0 @@ -Tutorial: Comparing Trajectory Analysis Methods -================================================ - -This tutorial demonstrates how to use the ``BinningTrajectoryAnalyzer`` and ``SlicingTrajectoryAnalyzer`` classes to analyze and compare contact angle and surface area data from trajectory simulations. - ----- - -Introduction ------------- - -The ``BinningTrajectoryAnalyzer`` and ``SlicingTrajectoryAnalyzer`` classes are designed to analyze trajectory data, specifically focusing on **surface area** and **contact angle** statistics. These tools are useful for comparing different analysis methods and visualizing results. - ----- - -Setup and Initialization -------------------------- - -Import the Classes -^^^^^^^^^^^^^^^^^^ - -Ensure you have the required classes imported: - -.. code-block:: python - - from wetting_angle_kit.visualization import ( - BinningTrajectoryAnalyzer, - MethodComparison, - SlicingTrajectoryAnalyzer, - ) - -Initialize the Analyzers -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Specify the directories containing your trajectory data: - -.. code-block:: python - - directories = [ - "slicing_analysis_CA/result_dump_traj_2k_reduce_binned", - "slicing_analysis_CA/result_dump_traj_500_reduce_binned", - "slicing_analysis_CA/result_dump_traj_1k_reduce_binned", - "slicing_analysis_CA/result_dump_traj_8k_reduce_binned", - ] - - # Initialize the analyzers - slicing = SlicingTrajectoryAnalyzer(directories) - binning = BinningTrajectoryAnalyzer(directories) - ----- - -Running the Analysis --------------------- - -Analyze Data -^^^^^^^^^^^^ - -Run the analysis for both methods: - -.. code-block:: python - - slicing.analyze() - binning.analyze() - -Example Output -^^^^^^^^^^^^^^ - -:: - - Directory: slicing_analysis_CA/result_dump_traj_2k_reduce_binned - Method: Slicing Analysis - Mean Surface Area: 2770.0659 - Mean Contact Angle: 91.7015° - - Directory: binning_analysis_CA/result_dump_traj_2k_reduce_binned - Method: Binning Analysis - Mean Surface Area: 2748.5427 - Mean Contact Angle: 91.9236° - ----- - -Interpreting the Output ------------------------- - -- **Mean Surface Area**: The average surface area for each trajectory. -- **Mean Contact Angle**: The average contact angle for each trajectory. -- **Standard Deviation**: Indicates the variability of the data. - ----- - -Visualisation -------------- - -Plot Mean Angle vs Surface Area -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: python - - slicing.plot_mean_angle_vs_surface(save_path="mean_angle_vs_surface_slicing.png") - binning.plot_mean_angle_vs_surface(save_path="mean_angle_vs_surface_binning.png") - -Plot Median Angle Evolution -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For the slicing method, plot the evolution of median angles: - -.. code-block:: python - - slicing.plot_median_alfas_evolution(save_path="evolution_of_angles_slicing_method.png") - ----- - -Method Comparison ------------------ - -Compare Statistics -^^^^^^^^^^^^^^^^^^ - -Use the ``MethodComparison`` class to compare the two methods: - -.. code-block:: python - - comparison = MethodComparison([slicing, binning]) - comparison.plot_side_by_side_comparison(save_path="comparison.png") - print(comparison.compare_statistics()) - -Example Output -^^^^^^^^^^^^^^ - -:: - - ====================================================================== - METHOD COMPARISON STATISTICS - ====================================================================== - Slicing Analysis: - ---------------------------------------------------------------------- - slicing_analysis_CA/traj_2k/: - Mean Surface Area: 2770.0659 ± 15.2001 - Mean Angle: 91.7015° ± 5.6130° - Overall Statistics: - Total samples: 196 - Mean Surface Area: 4001.0215 - Mean Angle: 91.8326° - Std Angle: 6.2027° - - Binning Analysis: - ---------------------------------------------------------------------- - binning_analysis_CA/traj_2k: - Mean Surface Area: 2748.5427 ± 0.0000 - Mean Angle: 91.9236° ± 0.0000° - Overall Statistics: - Total samples: 4 - Mean Surface Area: 4022.1019 - Mean Angle: 92.0876° - Std Angle: 0.2391° - ----- - -Conclusion ----------- - -- The ``SlicingTrajectoryAnalyzer`` provides more detailed statistics with higher sample counts. -- The ``BinningTrajectoryAnalyzer`` offers a simplified, binning approach. -- Use the comparison tools to visualize and interpret differences between methods. - -Additional Notes ----------------- - -- Ensure your data directories are correctly formatted and contain the required log files. diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst index 6c96180..0e0f617 100644 --- a/docs/source/tutorials/index.rst +++ b/docs/source/tutorials/index.rst @@ -11,4 +11,3 @@ Step-by-step guides for using wetting_angle_kit. Binning_method_tuto Slicing_method_tuto Visualization_slicing_droplet - Visualization_trajectories_comparison_methods diff --git a/docs/tutorials/Visualization_slicing_droplet.md b/docs/tutorials/Visualization_slicing_droplet.md index ccbf4c3..649ba79 100644 --- a/docs/tutorials/Visualization_slicing_droplet.md +++ b/docs/tutorials/Visualization_slicing_droplet.md @@ -17,10 +17,6 @@ The visualization workflow involves the following steps: ## 2. Import Required Modules ```python -import matplotlib - -matplotlib.use("Agg") # Required to prevent Qt conflicts with Ovito - import numpy as np from wetting_angle_kit.parsers import ( LammpsDumpParser, @@ -84,20 +80,20 @@ print("Mean contact angles (°):", list_alfas) ## 7. Visualize the Droplet ```python -plotter = DropletSlicePlotter(center=True, show_wall=True, molecule_view=True) +plotter = DropletSlicePlotter(center=True) -plotter.plot_surface_points( +fig = plotter.plot_surface_points( oxygen_position=oxygen_position, surface_data=array_surfaces, popt=array_popt[0], wall_coords=wall_coords, - output_filename="droplet_plot.png", alpha=list_alfas[0], ) -print(" Plot saved as 'droplet_plot.png'") -``` -## Outputs +# Interactive view in a notebook: +fig.show() - -![Droplet slicing method visualization](../images/droplet_plot.png) +# Or save a standalone HTML page: +fig.write_html("droplet_plot.html") +print("Plot saved as 'droplet_plot.html'") +``` diff --git a/docs/tutorials/Visualization_trajectories_comparison_methods.md b/docs/tutorials/Visualization_trajectories_comparison_methods.md deleted file mode 100644 index 45e77c5..0000000 --- a/docs/tutorials/Visualization_trajectories_comparison_methods.md +++ /dev/null @@ -1,144 +0,0 @@ -# Tutorial: Comparing Trajectory Analysis Methods - -This tutorial demonstrates how to use the `BinningTrajectoryAnalyzer` and `SlicingTrajectoryAnalyzer` classes to analyze and compare contact angle and surface area data from trajectory simulations. - ---- - -## Table of Contents -1. [Introduction](#introduction) -2. [Setup and Initialization](#setup-and-initialization) -3. [Running the Analysis](#running-the-analysis) -4. [Interpreting the Output](#interpreting-the-output) -5. [Visualization](#visualization) -6. [Method Comparison](#method-comparison) -7. [Conclusion](#conclusion) - ---- - -## Introduction -The `BinningTrajectoryAnalyzer` and `SlicingTrajectoryAnalyzer` classes are designed to analyze trajectory data, specifically focusing on **surface area** and **contact angle** statistics. These tools are useful for comparing different analysis methods and visualizing results. - ---- - -## Setup and Initialization - -### Import the Classes -Ensure you have the required classes imported: - -```python -from wetting_angle_kit.visualization import ( - BinningTrajectoryAnalyzer, - MethodComparison, - SlicingTrajectoryAnalyzer, -) -``` ---- - -## Initialize the Analyzers -Specify the directories containing your trajectory data: - -```python -directories = [ - "slicing_analysis_CA/result_dump_traj_500_binned", - "slicing_analysis_CA/result_dump_traj_1k_binned", - "slicing_analysis_CA/result_dump_traj_2k_binned", - "slicing_analysis_CA/result_dump_traj_4k_binned", -] - -# Initialize the analyzers -slicing = SlicingTrajectoryAnalyzer(directories) -binning = BinningTrajectoryAnalyzer(directories) -``` ---- -## Running the Analysis - -### Analyze Data - -Run the analysis for both methods: - -```python -slicing.analyze() -binning.analyze() -``` - -### Example Output: - -```text -Directory: slicing_analysis_CA/result_dump_traj_2k_reduce_binned - Method: Slicing Analysis - Mean Surface Area: 2770.0659 - Mean Contact Angle: 91.7015° - -Directory: binning_analysis_CA/result_dump_traj_2k_reduce_binned - Method: Binning Analysis - Mean Surface Area: 2748.5427 - Mean Contact Angle: 91.9236° -``` ---- -### Interpreting the Output - -- Mean Surface Area: The average surface area for each trajectory. -- Mean Contact Angle: The average contact angle for each trajectory. -- Standard Deviation: Indicates the variability of the data. ---- -## Visualisation - -### Plot Mean Angle vs Surface Area - -```python -slicing.plot_mean_angle_vs_surface(save_path="mean_angle_vs_surface_slicing.png") -binning.plot_mean_angle_vs_surface(save_path="mean_angle_vs_surface_binning.png") -``` -### Plot Median Angle Evolution -For the slicing method, plot the evolution of median angles: -```python -slicing.plot_median_alfas_evolution(save_path="evolution_of_angles_slicing_method.png") -``` -## Method Comparison - -### Compare Statistics -Use the MethodComparison class to compare the two methods: - -```python -comparison = MethodComparison([slicing, binning]) -comparison.plot_side_by_side_comparison(save_path="comparison.png") -print(comparison.compare_statistics()) -``` -### Example Output: - -```text -====================================================================== -METHOD COMPARISON STATISTICS -====================================================================== -Slicing Analysis: ----------------------------------------------------------------------- - slicing_analysis_CA/traj_2k/: - Mean Surface Area: 2770.0659 ± 15.2001 - Mean Angle: 91.7015° ± 5.6130° - Overall Statistics: - Total samples: 196 - Mean Surface Area: 4001.0215 - Mean Angle: 91.8326° - Std Angle: 6.2027° - -Binning Analysis: ----------------------------------------------------------------------- - binning_analysis_CA/traj_2k: - Mean Surface Area: 2748.5427 ± 0.0000 - Mean Angle: 91.9236° ± 0.0000° - Overall Statistics: - Total samples: 4 - Mean Surface Area: 4022.1019 - Mean Angle: 92.0876° - Std Angle: 0.2391° -``` - -## Conclusion -- The SlicingTrajectoryAnalyzer provides more detailed statistics with higher sample counts. - -- The BinningTrajectoryAnalyzer offers a simplified, binning approach. - -- Use the comparison tools to visualize and interpret differences between methods. - -## Additional Notes -- Ensure your data directories are correctly formatted and contain the required log files. diff --git a/src/wetting_angle_kit/analysis/__init__.py b/src/wetting_angle_kit/analysis/__init__.py index 7abcf09..d543b6c 100644 --- a/src/wetting_angle_kit/analysis/__init__.py +++ b/src/wetting_angle_kit/analysis/__init__.py @@ -19,7 +19,9 @@ def contact_angle_analyzer( - method: str, parser: Any, output_dir: str, **kwargs: Any + method: str, + parser: Any, + **kwargs: Any, ) -> BaseContactAngleAnalyzer: """Return an analyzer instance for the requested contact-angle method. @@ -29,8 +31,6 @@ def contact_angle_analyzer( Analysis method; one of ``"slicing"`` or ``"binning"``. parser : BaseParser Trajectory parser instance. - output_dir : str - Directory for output files. **kwargs Forwarded to the selected analyzer constructor. @@ -40,13 +40,9 @@ def contact_angle_analyzer( Configured analyzer ready to call ``analyze()``. """ if method == "slicing": - return SlicingContactAngleAnalyzer( - parser=parser, output_dir=output_dir, **kwargs - ) + return SlicingContactAngleAnalyzer(parser=parser, **kwargs) elif method == "binning": - return BinningContactAngleAnalyzer( - parser=parser, output_dir=output_dir, **kwargs - ) + return BinningContactAngleAnalyzer(parser=parser, **kwargs) else: raise ValueError(f"Unknown method '{method}'. Expected 'slicing' or 'binning'.") diff --git a/src/wetting_angle_kit/analysis/analyzer.py b/src/wetting_angle_kit/analysis/analyzer.py index 65a70fa..c55089a 100644 --- a/src/wetting_angle_kit/analysis/analyzer.py +++ b/src/wetting_angle_kit/analysis/analyzer.py @@ -1,24 +1,22 @@ from abc import ABC, abstractmethod from typing import Any -import numpy as np - from wetting_angle_kit.analysis.binning.angle_fitting import ( ContactAngleBinning, ) +from wetting_angle_kit.analysis.binning.results import BinningResults from wetting_angle_kit.analysis.slicing.parallel import ( ContactAngleSlicingParallel, ) +from wetting_angle_kit.analysis.slicing.results import SlicingResults class BaseContactAngleAnalyzer(ABC): """Abstract base for contact angle analysis across trajectory files.""" @abstractmethod - def analyze( - self, frame_range: list[int] | None = None, **kwargs: Any - ) -> dict[str, Any]: - """Run the analysis and return statistics.""" + def analyze(self, frame_range: list[int] | None = None, **kwargs: Any) -> Any: + """Run the analysis and return a method-specific results object.""" pass @abstractmethod @@ -26,15 +24,6 @@ def get_method_name(self) -> str: """Return the method name identifier.""" pass - def summary(self) -> dict[str, float]: - """Return quick summary statistics.""" - results = self.analyze() - return { - "mean": results["mean_angle"], - "std": results["std_angle"], - "n_samples": len(results["angles"]), - } - class SlicingContactAngleAnalyzer(BaseContactAngleAnalyzer): """BaseContactAngleAnalyzer implementation using the slicing parallel method.""" @@ -42,7 +31,6 @@ class SlicingContactAngleAnalyzer(BaseContactAngleAnalyzer): def __init__( self, parser: Any, - output_dir: str, **kwargs: Any, ): """ @@ -50,49 +38,36 @@ def __init__( ---------- parser : BaseParser Trajectory parser instance. - output_dir : str - Directory for output files. **kwargs Forwarded to ContactAngleSlicingParallel. """ self.parser = parser - self.output_dir = output_dir self._processor = ContactAngleSlicingParallel( - filename=parser.filepath, output_dir=output_dir, **kwargs + filename=parser.filepath, **kwargs ) def analyze( self, frame_range: list[int] | None = None, **kwargs: Any - ) -> dict[str, Any]: - """Run the slicing parallel analysis and return statistics. + ) -> SlicingResults: + """Run the slicing parallel analysis. Parameters ---------- frame_range : list[int], optional Frame indices to process. If None, all frames are used. **kwargs - Forwarded to process_frames_parallel. + Forwarded to ``process_frames_parallel``. Returns ------- - dict - Keys: mean_angle, std_angle, angles, frames_analyzed, method_metadata. + SlicingResults + In-memory per-frame angles, surface contours and circle fits. """ if frame_range is None: frame_range = list(range(self.parser.frame_count())) - - frame_to_angle = self._processor.process_frames_parallel( + return self._processor.process_frames_parallel( frames_to_process=frame_range, **kwargs ) - angles = np.array(list(frame_to_angle.values())) - - return { - "mean_angle": np.mean(angles), - "std_angle": np.std(angles), - "angles": frame_to_angle, - "frames_analyzed": list(frame_to_angle.keys()), - "method_metadata": {"frames_per_angle": 1}, - } def get_method_name(self) -> str: """Return "slicing_parallel".""" @@ -102,70 +77,62 @@ def get_method_name(self) -> str: class BinningContactAngleAnalyzer(BaseContactAngleAnalyzer): """BaseContactAngleAnalyzer implementation using the density-binning method.""" - def __init__(self, parser: Any, output_dir: str, **kwargs: Any) -> None: + def __init__(self, parser: Any, **kwargs: Any) -> None: """ Parameters ---------- parser : BaseParser Trajectory parser instance. - output_dir : str - Directory for output files. **kwargs Forwarded to ContactAngleBinning. """ self.parser = parser - self.output_dir = output_dir - self._analyzer = ContactAngleBinning( - parser=parser, output_dir=output_dir, **kwargs - ) + self._analyzer = ContactAngleBinning(parser=parser, **kwargs) def analyze( self, frame_range: list[int] | None = None, split_factor: int | None = None, **kwargs: Any, - ) -> dict[str, Any]: - """Run the binning analysis and return statistics. + ) -> BinningResults: + """Run the binning analysis. Parameters ---------- frame_range : list[int], optional Frame indices to process. If None, all frames are used. split_factor : int, optional - If given, split frame_range into sub-batches of this size and + If given, split ``frame_range`` into sub-batches of this size and compute one angle per batch; if None, all frames form a single batch. **kwargs Reserved for future use. Returns ------- - dict - Keys: mean_angle, std_angle, angles, frames_analyzed, method_metadata. + BinningResults + Per-batch contact angles, density fields and isoline data. """ if frame_range is None: frame_range = list(range(self.parser.frame_count())) if split_factor is None: - angle, _ = self._analyzer.process_batch(frame_range) - angles = np.array([angle]) - method_metadata = {"frames_per_angle": len(frame_range)} - else: - angles_list: list[float] = [] - for batch_idx, start in enumerate(range(0, len(frame_range), split_factor)): - end = min(start + split_factor, len(frame_range)) - angle, _ = self._analyzer.process_batch( + batch = self._analyzer.process_batch(frame_range) + return BinningResults( + batches=[batch], + method_metadata={"frames_per_angle": len(frame_range)}, + ) + batches = [] + for batch_idx, start in enumerate(range(0, len(frame_range), split_factor)): + end = min(start + split_factor, len(frame_range)) + batches.append( + self._analyzer.process_batch( frame_range[start:end], - batch_index=batch_idx + 1, # Pass batch index + batch_index=batch_idx + 1, ) - angles_list.append(angle) - angles = np.array(angles_list) - method_metadata = {"frames_per_trajectory": split_factor} - return { - "mean_angle": np.mean(angles), - "std_angle": np.std(angles), - "angles": angles, - "frames_analyzed": frame_range, - "method_metadata": method_metadata, - } + ) + return BinningResults( + batches=batches, + method_metadata={"frames_per_trajectory": split_factor}, + ) def get_method_name(self) -> str: """Return "binning_density".""" diff --git a/src/wetting_angle_kit/analysis/binning/angle_fitting.py b/src/wetting_angle_kit/analysis/binning/angle_fitting.py index bcb772f..d2d6417 100644 --- a/src/wetting_angle_kit/analysis/binning/angle_fitting.py +++ b/src/wetting_angle_kit/analysis/binning/angle_fitting.py @@ -25,15 +25,13 @@ """ import logging -import os import warnings from collections.abc import Sequence from typing import Any -import matplotlib -import matplotlib.pyplot as plt import numpy as np +from wetting_angle_kit.analysis.binning.results import BinningBatch from wetting_angle_kit.analysis.binning.surface_definition import ( HyperbolicTangentModel, ) @@ -44,14 +42,7 @@ logger = logging.getLogger(__name__) -# Force a non-interactive backend before pyplot is imported so figure -# generation works in headless environments (CI, OVITO subprocesses). -# Only switch if no backend is already attached to an open figure. -if matplotlib.get_backend().lower() != "agg": - try: - matplotlib.use("Agg", force=False) - except (ImportError, ValueError): - pass +_PARAM_NAMES = ("rho1", "rho2", "R_eq", "zi_c", "zi_0", "t1", "t2") class ContactAngleBinning: @@ -69,8 +60,6 @@ def __init__( droplet_geometry: str = "spherical", width_cylinder: float | None = None, binning_params: dict[str, Any] | None = None, - output_dir: str = "output_analysis/", - plot_graphs: bool = True, precentered: bool = False, ) -> None: """ @@ -87,10 +76,6 @@ def __init__( binning_params : dict, optional Grid definition with keys ``xi_0``, ``xi_f``, ``nbins_xi``, ``zi_0``, ``zi_f``, ``nbins_zi``. A heuristic default is used if None. - output_dir : str, default "output_analysis/" - Directory for log files and density field CSVs. - plot_graphs : bool, default True - Whether to generate density contour plots. precentered : bool, default False Set True to declare that the trajectory already recenters the droplet at every frame and atoms are not wrapped across periodic @@ -104,8 +89,6 @@ def __init__( self.atom_indices = atom_indices self.droplet_geometry = droplet_geometry self.width_cylinder = width_cylinder - self.output_dir = output_dir - self.plot_graphs = plot_graphs self.precentered = precentered if binning_params is None: max_dist = int( @@ -146,7 +129,6 @@ def __init__( self.width_cylinder = self.parser.box_size_x(frame_index=0) elif self.droplet_geometry == "cylinder_y": self.width_cylinder = self.parser.box_size_y(frame_index=0) - os.makedirs(self.output_dir, exist_ok=True) def _initialize_grid(self) -> None: """Initialize bin edges, centers and cell sizes from parameters.""" @@ -295,126 +277,30 @@ def binning( rho_cc /= len_frames return rho_cc - def plot_density_with_isoline( - self, - xi_cc: np.ndarray, - zi_cc: np.ndarray, - rho_cc: np.ndarray, - circle_xi: np.ndarray, - circle_zi: np.ndarray, - wall_line_xi: np.ndarray, - wall_line_zi: np.ndarray, - batch_index: int | None = None, - clevels: int = 20, - scale: float = 0.75, - close: bool = True, - ) -> None: - """Plot density contour with fitted iso-surface approximations. - - Parameters - ---------- - xi_cc, zi_cc : ndarray - Cell center coordinates. - rho_cc : ndarray - Density field. - circle_xi, circle_zi : ndarray - Fitted circle isoline coordinates. - wall_line_xi, wall_line_zi : ndarray - Wall line coordinates. - batch_index : int, optional - Batch identifier for file naming. - clevels : int, default 20 - Number of contour levels. - scale : float, default 0.75 - Figure size scaling factor. - close : bool, default True - If True, close figure after saving. - """ - name = ( - f"bin_plot_batch_{batch_index}.png" - if batch_index is not None - else "bin_plot.png" - ) - plt.figure(dpi=300, figsize=(4 * scale, 3 * scale)) - plt.contourf(xi_cc, zi_cc, np.transpose(rho_cc), levels=clevels, cmap="jet") - plt.colorbar() - plt.plot(circle_xi, circle_zi, "--", color="black") - plt.plot(wall_line_xi, wall_line_zi, "--", color="black") - plt.savefig(os.path.join(self.output_dir, name)) - if close: - plt.close() - - def save_logfile( - self, - n_particles: float, - param_strings: list[str], - theta: float, - xi_cc: np.ndarray, - zi_cc: np.ndarray, - rho_cc: np.ndarray, - batch_index: int | None = None, - ) -> None: - """Write fitted parameters and density field CSV for a batch. - - Parameters - ---------- - n_particles : float - Average number of particles per frame in batch. - param_strings : list[str] - Formatted parameter lines from model. - theta : float - Contact angle in degrees. - xi_cc, zi_cc : ndarray - Cell centers. - rho_cc : ndarray - Density field. - batch_index : int, optional - Batch identifier for file naming. - """ - batch_str = f"_batch_{batch_index}" if batch_index is not None else "" - with open(os.path.join(self.output_dir, f"log_data{batch_str}.txt"), "w") as f: - f.write("Simulation parameters:\n") - f.write(f"reduced_particles_number:{n_particles}\n") - f.write(f"model_type:{self.droplet_geometry}\n") - if self.droplet_geometry in ("cylinder_x", "cylinder_y"): - f.write(f"width_cylinder:{self.width_cylinder}\n") - f.write("Fitted parameters:\n") - for param in param_strings: - f.write(param) - f.write(f"\n\nContact angle:{theta}") - msh_zi_cc_grid, msh_xi_cc_grid = np.meshgrid(zi_cc, xi_cc) - msh_zi_cc = msh_zi_cc_grid.reshape((len(xi_cc) * len(zi_cc)), order="F") - msh_xi_cc = msh_xi_cc_grid.reshape((len(xi_cc) * len(zi_cc)), order="F") - msh_rho_cc = rho_cc.reshape((len(xi_cc) * len(zi_cc)), order="F") - csv_data = np.c_[msh_xi_cc, msh_zi_cc, msh_rho_cc] - np.savetxt( - os.path.join(self.output_dir, f"rho_field{batch_str}.csv"), - csv_data, - delimiter=",", - header=(f"x_{len(xi_cc)},y_{len(zi_cc)},rho_{len(xi_cc) * len(zi_cc)}"), - ) - def process_batch( self, frame_list: list[int], model: Any | None = None, batch_index: int | None = None, - ) -> tuple[float, Any]: - """Process a batch of frames and compute contact angle. + ) -> BinningBatch: + """Process a batch of frames and return its fitted contact-angle data. Parameters ---------- frame_list : sequence[int] Frame indices in the batch. model : SurfaceModel, optional - Pre-existing fitted model instance; new model created if None. + Pre-existing fitted model instance; a new + :class:`HyperbolicTangentModel` is created if None. batch_index : int, optional - Identifier appended to output filenames. + Sequential identifier copied into the returned :class:`BinningBatch` + (defaults to 1 when not supplied). Returns ------- - tuple(float, SurfaceModel) - (contact_angle_degrees, fitted_model). + BinningBatch + Per-batch container with contact angle, density field, fitted + isoline coordinates and fitted parameters. """ xi_par, zi_par, len_frames = self.get_profile_coordinates( frame_indices=frame_list, @@ -437,80 +323,37 @@ def process_batch( msh_rho_cc = rho_cc.reshape((len(self.xi_cc) * len(self.zi_cc)), order="F") x_data = (msh_xi_cc, msh_zi_cc) model.fit(x_data, msh_rho_cc) - param_strings = model.get_parameter_strings() logger.info( - f"Fitted parameters for batch{batch_label}:\n{''.join(param_strings)}" + f"Fitted parameters for batch{batch_label}:\n" + f"{''.join(model.get_parameter_strings())}" ) contact_angle = model.compute_contact_angle() logger.info(f"Contact angle for batch{batch_label}: {contact_angle}") - if self.plot_graphs: - try: - ( - circle_xi, - circle_zi, - wall_line_xi, - wall_line_zi, - ) = model.compute_isoline() - except ValueError as exc: - warnings.warn( - f"Skipping isoline plot for batch {batch_index}: {exc}", - RuntimeWarning, - stacklevel=2, - ) - else: - self.plot_density_with_isoline( - self.xi_cc, - self.zi_cc, - rho_cc, - circle_xi, - circle_zi, - wall_line_xi, - wall_line_zi, - batch_index, - ) - self.save_logfile( - n_particles, - param_strings, - contact_angle, - self.xi_cc, - self.zi_cc, - rho_cc, - batch_index, - ) - return contact_angle, model - - def process_all_batches( - self, batch_size: int = 100, save_angles: bool = True - ) -> list[float]: - """Process all frames in batches returning list of contact angles. - - Parameters - ---------- - batch_size : int, default 100 - Number of frames per batch. - save_angles : bool, default True - If True, save angle list as numpy file. - - Returns - ------- - list[float] - Contact angles per processed batch. - """ - frames_tot = self.parser.frame_count() - logger.info(f"Total frames: {frames_tot}") - angles: list[float] = [] - for batch_index, start_frame in enumerate(range(0, frames_tot, batch_size)): - frame_list = list( - range(start_frame, min(start_frame + batch_size, frames_tot)) + try: + circle_xi, circle_zi, wall_line_xi, wall_line_zi = model.compute_isoline() + except ValueError as exc: + warnings.warn( + f"Isoline unavailable for batch {batch_index}: {exc}", + RuntimeWarning, + stacklevel=2, ) - angle, _ = self.process_batch(frame_list, batch_index=batch_index + 1) - angles.append(angle) - if save_angles: - np.save( - os.path.join( - self.output_dir, f"all_angles_{self.droplet_geometry}.npy" - ), - np.array(angles), + circle_xi = circle_zi = wall_line_xi = wall_line_zi = None + params = model.params + if params is None: + raise RuntimeError( + f"Hyperbolic tangent fit did not set model parameters for batch " + f"{batch_index}; cannot build BinningBatch." ) - logger.info(f"List of contact angles by batch: {angles}") - return angles + return BinningBatch( + batch_index=batch_index if batch_index is not None else 1, + angle=float(contact_angle), + n_particles=float(n_particles), + xi_cc=self.xi_cc.copy(), + zi_cc=self.zi_cc.copy(), + rho_cc=rho_cc, + circle_xi=circle_xi, + circle_zi=circle_zi, + wall_line_xi=wall_line_xi, + wall_line_zi=wall_line_zi, + fitted_params=dict(zip(_PARAM_NAMES, params, strict=False)), + ) diff --git a/src/wetting_angle_kit/analysis/binning/results.py b/src/wetting_angle_kit/analysis/binning/results.py new file mode 100644 index 0000000..15d9202 --- /dev/null +++ b/src/wetting_angle_kit/analysis/binning/results.py @@ -0,0 +1,91 @@ +from dataclasses import dataclass, field +from typing import Any + +import numpy as np + + +@dataclass +class BinningBatch: + """Per-batch output of the binning analysis. + + A batch is the fitting unit: a contiguous group of frames whose + coordinates are aggregated into a single 2D density field that is then + fitted to extract one contact angle. + + Attributes + ---------- + batch_index : int + Sequential identifier (starting at 1) for the batch. + angle : float + Fitted contact angle in degrees (``nan`` if the fit failed). + n_particles : float + Average number of fluid particles per frame within the batch. + xi_cc : np.ndarray + Cell-center coordinates along the radial/in-plane axis (1D). + zi_cc : np.ndarray + Cell-center coordinates along the vertical axis (1D). + rho_cc : np.ndarray + 2D density field on the ``xi_cc × zi_cc`` grid (particles · Å⁻³). + circle_xi : np.ndarray | None + Fitted droplet circle iso-line, radial coordinates. ``None`` when + :meth:`HyperbolicTangentModel.compute_isoline` failed (non-physical + fit). + circle_zi : np.ndarray | None + Fitted droplet circle iso-line, vertical coordinates. + wall_line_xi : np.ndarray | None + Fitted wall position, radial coordinates. + wall_line_zi : np.ndarray | None + Fitted wall position, vertical coordinates. + fitted_params : dict[str, float] + Fitted model parameters (e.g. ``R_eq``, ``zi_c``, ``zi_0``). + """ + + batch_index: int + angle: float + n_particles: float + xi_cc: np.ndarray + zi_cc: np.ndarray + rho_cc: np.ndarray + circle_xi: np.ndarray | None + circle_zi: np.ndarray | None + wall_line_xi: np.ndarray | None + wall_line_zi: np.ndarray | None + fitted_params: dict[str, float] = field(default_factory=dict) + + +@dataclass +class BinningResults: + """In-memory container for the binning method output. + + Replaces the legacy ``log_data_batch_*.txt`` / ``rho_field_batch_*.csv`` + round-trip: every quantity needed downstream (statistics, contour plot, + per-batch angle evolution) is carried as attributes on the batches. + + Attributes + ---------- + batches : list[BinningBatch] + One entry per fitted batch, in batch order. + method_metadata : dict + Free-form method descriptor (e.g. ``{"frames_per_trajectory": 100}``). + """ + + batches: list[BinningBatch] + method_metadata: dict[str, Any] = field(default_factory=dict) + + def __len__(self) -> int: + return len(self.batches) + + @property + def angles_per_batch(self) -> np.ndarray: + """Per-batch fitted contact angle, in degrees.""" + return np.array([b.angle for b in self.batches]) + + @property + def mean_angle(self) -> float: + """Mean contact angle across batches, in degrees.""" + return float(np.mean(self.angles_per_batch)) + + @property + def std_angle(self) -> float: + """Standard deviation of the per-batch contact angle, in degrees.""" + return float(np.std(self.angles_per_batch)) diff --git a/src/wetting_angle_kit/analysis/slicing/parallel.py b/src/wetting_angle_kit/analysis/slicing/parallel.py index fd67f17..c78cd26 100644 --- a/src/wetting_angle_kit/analysis/slicing/parallel.py +++ b/src/wetting_angle_kit/analysis/slicing/parallel.py @@ -1,12 +1,12 @@ import logging import math import multiprocessing as mp -import os from concurrent.futures import ProcessPoolExecutor, as_completed from typing import NamedTuple import numpy as np +from wetting_angle_kit.analysis.slicing.results import SlicingResults from wetting_angle_kit.io_utils import recenter_droplet_pbc from wetting_angle_kit.parsers import BaseParser @@ -56,7 +56,6 @@ class ContactAngleSlicingParallel: def __init__( self, filename: str, - output_dir: str, droplet_geometry: str = "spherical", atom_indices: np.ndarray | None = None, delta_gamma: float | None = None, @@ -69,8 +68,6 @@ def __init__( ---------- filename : str Path to trajectory file. - output_dir : str - Directory to write per-frame results. droplet_geometry : str, default "spherical" Geometric model identifier (e.g. "cylinder_x", "cylinder_y", "spherical"). atom_indices : ndarray, optional @@ -91,22 +88,20 @@ def __init__( satisfy the precondition will produce wrong results. """ self.filename = filename - self.output_dir = output_dir self.delta_gamma = delta_gamma self.delta_cylinder = delta_cylinder self.droplet_geometry = droplet_geometry self.points_per_angstrom = points_per_angstrom self.atom_indices = atom_indices if atom_indices is not None else np.array([]) self.precentered = precentered - os.makedirs(self.output_dir, exist_ok=True) def process_frames_parallel( self, frames_to_process: list[int], num_batches: int = 4, max_workers: int | None = None, - ) -> dict[int, float]: - """Process many frames in parallel batches. + ) -> SlicingResults: + """Process many frames in parallel batches and return the in-memory results. Parameters ---------- @@ -119,8 +114,10 @@ def process_frames_parallel( Returns ------- - dict[int, float] - Mapping frame number -> mean contact angle (failed frames excluded). + SlicingResults + Per-frame angles, surface contours, fit parameters and method + metadata. Frames whose worker failed to produce a mean angle are + omitted. """ if max_workers is None: max_workers = num_batches @@ -129,7 +126,9 @@ def process_frames_parallel( f"Processing {len(frames_to_process)} frames in {len(batches)} batches " f"with {max_workers} workers" ) - results: dict[int, float] = {} + all_angles: dict[int, list] = {} + all_surfaces: dict[int, list] = {} + all_popts: dict[int, list] = {} with ProcessPoolExecutor( max_workers=max_workers, mp_context=_MP_CONTEXT ) as executor: @@ -138,16 +137,12 @@ def process_frames_parallel( for batch_frames in batches } completed_batches = 0 - all_angles: dict[int, list] = {} - all_surfaces: dict[int, list] = {} - all_popts: dict[int, list] = {} for future in as_completed(future_to_batch): batch_frames = future_to_batch[future] try: batch_results = future.result() for frame_num, mean_angle, angles, surfaces, popts in batch_results: if mean_angle is not None: - results[frame_num] = mean_angle all_angles[frame_num] = angles all_surfaces[frame_num] = surfaces all_popts[frame_num] = popts @@ -162,29 +157,17 @@ def process_frames_parallel( exc_info=True, ) sorted_frames = sorted(all_angles.keys()) - - angles_with_frames = [(f, all_angles[f]) for f in sorted_frames] - np.save( - f"{self.output_dir}/all_angles.npy", - np.array(angles_with_frames, dtype=object), - ) - - surfaces_with_frames = [(f, all_surfaces[f]) for f in sorted_frames] - np.save( - f"{self.output_dir}/all_surfaces.npy", - np.array(surfaces_with_frames, dtype=object), - ) - - popts_with_frames = [(f, all_popts[f]) for f in sorted_frames] - np.save( - f"{self.output_dir}/all_popts.npy", - np.array(popts_with_frames, dtype=object), - ) logger.info( - f"Successfully processed {len(results)}/{len(frames_to_process)} frames" + f"Successfully processed {len(sorted_frames)}/{len(frames_to_process)} " + "frames" + ) + return SlicingResults( + frames=sorted_frames, + angles=[np.asarray(all_angles[f]) for f in sorted_frames], + surfaces=[all_surfaces[f] for f in sorted_frames], + popts=[np.asarray(all_popts[f]) for f in sorted_frames], + method_metadata={"frames_per_angle": 1}, ) - - return results def _create_batches(self, frames: list[int], num_batches: int) -> list[list[int]]: """Return frame batches of near-equal size.""" diff --git a/src/wetting_angle_kit/analysis/slicing/results.py b/src/wetting_angle_kit/analysis/slicing/results.py new file mode 100644 index 0000000..769749b --- /dev/null +++ b/src/wetting_angle_kit/analysis/slicing/results.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass, field +from typing import Any + +import numpy as np + + +@dataclass +class SlicingResults: + """In-memory container for the per-frame output of the slicing method. + + Replaces the legacy ``all_angles.npy`` / ``all_surfaces.npy`` / + ``all_popts.npy`` round-trip. The three parallel lists share the same + indexing as ``frames``: entry ``i`` describes frame ``frames[i]``. + + Attributes + ---------- + frames : list[int] + Frame indices that were successfully processed, sorted ascending. + angles : list[np.ndarray] + Per-frame array of contact angles (one value per slice). + surfaces : list[list[np.ndarray]] + Per-frame list of slice surface contours; each contour is an + ``(N, 2)`` array of ``(x, z)`` vertex coordinates. + popts : list[np.ndarray] + Per-frame array of fitted circle parameters; each entry has shape + ``(n_slices, 4)`` with columns ``(x_center, z_center, radius, extra)``. + method_metadata : dict + Free-form method descriptor (e.g. ``{"frames_per_angle": 1}``). + """ + + frames: list[int] + angles: list[np.ndarray] + surfaces: list[list[np.ndarray]] + popts: list[np.ndarray] + method_metadata: dict[str, Any] = field(default_factory=dict) + + def __len__(self) -> int: + return len(self.frames) + + @property + def per_frame_mean_angles(self) -> np.ndarray: + """Per-frame mean contact angle, taken across slices, in degrees.""" + return np.array([float(np.mean(a)) for a in self.angles]) + + @property + def mean_angle(self) -> float: + """Mean contact angle across frames, in degrees.""" + return float(np.mean(self.per_frame_mean_angles)) + + @property + def std_angle(self) -> float: + """Standard deviation of the per-frame mean contact angle, in degrees.""" + return float(np.std(self.per_frame_mean_angles)) diff --git a/src/wetting_angle_kit/visualization/__init__.py b/src/wetting_angle_kit/visualization/__init__.py index 1a02dea..bf10e4d 100644 --- a/src/wetting_angle_kit/visualization/__init__.py +++ b/src/wetting_angle_kit/visualization/__init__.py @@ -1,37 +1,19 @@ -from wetting_angle_kit.visualization.base_trajectory_analyzer import ( - BaseTrajectoryAnalyzer, +from wetting_angle_kit.visualization.base_trajectory_plotter import ( + BaseTrajectoryPlotter, ) -from wetting_angle_kit.visualization.binning_trajectory_analyzer import ( - BinningTrajectoryAnalyzer, +from wetting_angle_kit.visualization.binning_trajectory_plotter import ( + BinningTrajectoryPlotter, ) -from wetting_angle_kit.visualization.droplet_slice_plots import ( - ContactAngleAnimator, - DropletSlicePlotlyPlotter, - DropletSlicePlotter, -) -from wetting_angle_kit.visualization.method_comparison import MethodComparison -from wetting_angle_kit.visualization.slicing_trajectory_analyzer import ( - SlicingTrajectoryAnalyzer, -) -from wetting_angle_kit.visualization.surface_plots import ( - plot_liquid_particles, - plot_slice, - plot_surface_and_points, - plot_surface_file, - read_surface_file, +from wetting_angle_kit.visualization.droplet_slice_plot import DropletSlicePlotter +from wetting_angle_kit.visualization.slicing_trajectory_plotter import ( + SlicingTrajectoryPlotter, ) +from wetting_angle_kit.visualization.stats import TrajectoryStats __all__ = [ - "BaseTrajectoryAnalyzer", - "BinningTrajectoryAnalyzer", - "MethodComparison", + "BaseTrajectoryPlotter", + "BinningTrajectoryPlotter", "DropletSlicePlotter", - "DropletSlicePlotlyPlotter", - "ContactAngleAnimator", - "SlicingTrajectoryAnalyzer", - "plot_slice", - "plot_surface_file", - "read_surface_file", - "plot_surface_and_points", - "plot_liquid_particles", + "SlicingTrajectoryPlotter", + "TrajectoryStats", ] diff --git a/src/wetting_angle_kit/visualization/animator.py b/src/wetting_angle_kit/visualization/animator.py new file mode 100644 index 0000000..039d6c8 --- /dev/null +++ b/src/wetting_angle_kit/visualization/animator.py @@ -0,0 +1,219 @@ +import numpy as np +import plotly.graph_objects as go + +from wetting_angle_kit.analysis.slicing import ContactAngleSlicing +from wetting_angle_kit.io_utils import recenter_droplet_pbc +from wetting_angle_kit.parsers import ( + LammpsDumpParser, + LammpsDumpWallParser, + LammpsDumpWaterFinder, +) +from wetting_angle_kit.visualization.droplet_slice_plot import DropletSlicePlotter + + +class ContactAngleAnimator: + """Generate interactive Plotly slider animation of median slice angle per frame.""" + + def __init__( + self, + filename: str, + particle_type_wall: set, + oxygen_type: int, + hydrogen_type: int, + liquid_particle_types: set, + n_frames: int = 10, + droplet_geometry: str = "cylinder_y", + delta_cylinder: int = 5, + max_dist: int = 100, + width_cylinder: int = 21, + precentered: bool = False, + ): + """ + Parameters + ---------- + filename : str + Path to LAMMPS dump trajectory file. + particle_type_wall : set + LAMMPS particle type IDs for wall atoms. + oxygen_type : int + LAMMPS particle type ID for oxygen atoms. + hydrogen_type : int + LAMMPS particle type ID for hydrogen atoms. + liquid_particle_types : set + LAMMPS particle type IDs for all liquid atoms (used to mask wall parser). + n_frames : int, default 10 + Number of frames to include in the animation. + droplet_geometry : str, default "cylinder_y" + Droplet geometry passed to ContactAngleSlicing. + delta_cylinder : int, default 5 + Step size along the slicing axis (Å). + max_dist : int, default 100 + Maximum radial distance for line sampling (Å). + width_cylinder : int, default 21 + Box extent along the cylinder axis (Å). + precentered : bool, default False + Set True if the trajectory already recenters the droplet at + every frame and atoms are not wrapped across periodic + boundaries; the per-frame circular-mean recentering is then + skipped. Setting this on a trajectory that does NOT satisfy the + precondition will misplace the contact-angle overlay. + """ + self.filename = filename + self.particle_type_wall = particle_type_wall + self.oxygen_type = oxygen_type + self.hydrogen_type = hydrogen_type + self.liquid_particle_types = liquid_particle_types + self.n_frames = n_frames + self.droplet_geometry = droplet_geometry + self.delta_cylinder = delta_cylinder + self.max_dist = max_dist + self.width_cylinder = width_cylinder + self.precentered = precentered + + # Initialize objects + self.wat_find = LammpsDumpWaterFinder( + self.filename, + particle_type_wall=self.particle_type_wall, + oxygen_type=self.oxygen_type, + hydrogen_type=self.hydrogen_type, + ) + self.oxygen_indices = self.wat_find.get_water_oxygen_ids(frame_index=0) + self.coord_wall = LammpsDumpWallParser( + self.filename, liquid_particle_types=list(self.liquid_particle_types) + ) + self.wall_coords = self.coord_wall.parse(frame_index=0) + self.parser = LammpsDumpParser(filepath=self.filename) + self.plotter = DropletSlicePlotter(center=True) + + def generate_animation( + self, output_filename: str = "ContactAngle_Median_PerFrame_Slider.html" + ) -> None: + """Build and write HTML with slider of median contact angles over frames. + + Parameters + ---------- + output_filename : str, default "ContactAngle_Median_PerFrame_Slider.html" + Output HTML file path. + """ + fig = go.Figure() + frames_list = [] + frame_labels = [] + median_angles = [] + for frame_idx in range(self.n_frames): + oxygen_position = self.parser.parse( + frame_index=frame_idx, indices=self.oxygen_indices + ) + if self.precentered: + liquid_geom_center = np.mean(oxygen_position, axis=0) + else: + box_size_xy = ( + self.parser.box_size_x(frame_index=frame_idx), + self.parser.box_size_y(frame_index=frame_idx), + ) + oxygen_position, liquid_geom_center = recenter_droplet_pbc( + oxygen_position, self.droplet_geometry, box_size=box_size_xy + ) + processor = ContactAngleSlicing( + liquid_coordinates=oxygen_position, + liquid_geom_center=liquid_geom_center, + droplet_geometry=self.droplet_geometry, + delta_cylinder=self.delta_cylinder, + max_dist=self.max_dist, + width_cylinder=self.width_cylinder, + ) + angles, surfaces, popt_arrays = processor.predict_contact_angle() + median_idx = np.argsort(angles)[len(angles) // 2] + alpha = angles[median_idx] + popt = popt_arrays[median_idx] + surface = [surfaces[median_idx]] + median_angles.append(alpha) + fig_frame = self.plotter.plot_surface_points( + oxygen_position=oxygen_position, + surface_data=surface, + popt=popt, + wall_coords=self.wall_coords.copy(), + y_com=np.mean(oxygen_position[:, 1]), + pbc_y=None, + alpha=alpha, + show_water=True, + show_surface=True, + show_circle=True, + show_tangent=True, + show_wall=True, + ) + frame = go.Frame( + data=fig_frame.data, + name=f"Frame {frame_idx}", + layout=go.Layout( + title_text=( + f"Frame {frame_idx} | Median contact angle = {alpha:.2f}°" + ) + ), + ) + frames_list.append(frame) + frame_labels.append(f"Frame {frame_idx}") + fig.frames = frames_list + fig.add_traces(frames_list[0].data) + fig.update_layout( + title="Interactive Contact Angle Evolution (Median Slice per Frame)", + width=800, + height=600, + margin=dict(l=80, r=200, t=80, b=100), + xaxis_title="x (Å)", + yaxis_title="z (Å)", + template="simple_white", + showlegend=True, + legend=dict( + x=1.05, + y=0.95, + bgcolor="rgba(255,255,255,0.8)", + bordercolor="lightgray", + borderwidth=1, + font=dict(size=11), + ), + xaxis=dict( + mirror=True, + showline=True, + linecolor="black", + ticks="outside", + showgrid=True, + gridcolor="lightgray", + zeroline=False, + ), + yaxis=dict( + mirror=True, + showline=True, + linecolor="black", + ticks="outside", + showgrid=True, + gridcolor="lightgray", + zeroline=False, + scaleanchor="x", + scaleratio=1, + ), + sliders=[ + { + "active": 0, + "pad": {"b": 60, "t": 40}, + "x": 0.2, + "len": 0.6, + "y": -0.1, + "yanchor": "top", + "steps": [ + { + "args": [ + [f"Frame {k}"], + { + "frame": {"duration": 0, "redraw": True}, + "mode": "immediate", + }, + ], + "label": f"{k}", + "method": "animate", + } + for k in range(len(frames_list)) + ], + } + ], + ) + fig.write_html(output_filename) diff --git a/src/wetting_angle_kit/visualization/base_trajectory_analyzer.py b/src/wetting_angle_kit/visualization/base_trajectory_analyzer.py deleted file mode 100644 index 6cfe6cb..0000000 --- a/src/wetting_angle_kit/visualization/base_trajectory_analyzer.py +++ /dev/null @@ -1,298 +0,0 @@ -import logging -import os -from abc import ABC, abstractmethod -from typing import Any - -import matplotlib.pyplot as plt -import numpy as np - -logger = logging.getLogger(__name__) - - -class BaseTrajectoryAnalyzer(ABC): - """Abstract base for trajectory analyzers that compute contact angle statistics.""" - - def __init__(self, directories: list[str], time_unit: str = "ps") -> None: - """ - Initialize the analyzer with a list of directory paths. - - Parameters - ---------- - directories : list of str - List of directory paths containing analysis results. - time_unit : str, optional - Time unit for the x-axis (e.g., "ps", "ns", "fs"). - """ - self.directories = directories - self.data: dict[str, Any] = {} - self.time_unit = time_unit - self._initialize_data_structure() - - @abstractmethod - def _initialize_data_structure(self) -> None: - """Initialize the data dictionary structure for each directory.""" - pass - - @abstractmethod - def load_data(self) -> None: - """Read and parse data from files in each directory.""" - pass - - @abstractmethod - def get_surface_areas(self, directory: str) -> np.ndarray: - """ - Get surface areas for a given directory. - - Parameters - ---------- - directory : str - Directory path. - - Returns - ------- - numpy.ndarray - Array of surface area values. - """ - pass - - @abstractmethod - def get_contact_angles(self, directory: str) -> np.ndarray: - """ - Get contact angles for a given directory. - - Parameters - ---------- - directory : str - Directory path. - - Returns - ------- - numpy.ndarray - Array of contact angle values. - """ - pass - - @abstractmethod - def get_method_name(self) -> str: - """ - Return the name of this analysis method. - - Returns - ------- - str - Method name for labels and titles. - """ - pass - - def compute_statistics(self, directory: str) -> tuple[float, float, float]: - """ - Compute mean surface area, mean angle, and standard error. - - Parameters - ---------- - directory : str - Directory path. - - Returns - ------- - tuple - (x_value, y_value, y_error) where: - - x_value: 1/sqrt(mean_surface_area) - - y_value: mean contact angle - - y_error: standard error of the mean - """ - surface_areas = self.get_surface_areas(directory) - contact_angles = self.get_contact_angles(directory) - - x = 1 / np.sqrt(np.mean(surface_areas)) - y = np.mean(contact_angles) - yerr = np.std(contact_angles) / np.sqrt(len(contact_angles)) - - return x, y, yerr - - def get_clean_label(self, directory: str) -> str: - """ - Generate a clean label from directory name. - - Parameters - ---------- - directory : str - Directory path. - - Returns - ------- - str - Cleaned directory name for plotting. - """ - return ( - directory.replace("_reduce_slicing", "") - .replace("_reduce_binning", "") - .replace("result_dump_", "") - ) - - def analyze(self, output_filename: str = "output_stats.txt") -> None: - """Load data and write per-directory statistics to a text file. - - Parameters - ---------- - output_filename : str, default "output_stats.txt" - File name written inside each directory. - """ - self.load_data() - for directory in self.directories: - output_path = f"{directory}/{output_filename}" - with open(output_path, "w", encoding="utf-8") as f: - f.write(f"Directory: {directory}\n") - f.write(f"Method: {self.get_method_name()}\n") - f.write( - "Mean Surface Area: " - f"{np.mean(self.get_surface_areas(directory)):.4f}\n" - ) - f.write( - "Mean Contact Angle: " - f"{np.mean(self.get_contact_angles(directory)):.4f}\u00b0\n" - ) - f.write( - "Std Contact Angle: " - f"{np.std(self.get_contact_angles(directory)):.4f}\u00b0\n" - ) - logger.info("Analysis saved to: %s", output_path) - - def plot_mean_angle_vs_surface( - self, - labels: list[str] | None = None, - color: str | None = None, - save_path: str | None = None, - ) -> None: - """ - Generate a plot comparing mean angle vs surface - area scaling. If no analysis output is found, run the analysis first. - - Parameters - ---------- - labels : list of str, optional - Labels for each dataset. If None, directory names are used. - color : str, optional - Base color for all datasets. If None, a default - set of unique colors is used. - save_path : str, optional - Path to save the figure. - """ - # Check if analysis output files exist; if not, run analysis - for directory in self.directories: - output_file = f"{directory}/output_stats.txt" - if not os.path.exists(output_file): - logger.info("No analysis found for %s. Running analysis...", directory) - self.analyze() # Run analysis to generate output files - break # Only need to run once - - # Read data if not already loaded - if not hasattr(self, "data") or not self.data: - self.load_data() - - # Set up plot parameters - plt.rcParams.update( - { - "font.family": "serif", - "font.size": 13, - "axes.labelsize": 14, - "axes.titlesize": 15, - "legend.fontsize": 12, - "xtick.direction": "in", - "ytick.direction": "in", - "axes.linewidth": 1.0, - "errorbar.capsize": 3, - } - ) - - # Create the plot - fig, ax = plt.subplots(figsize=(7, 4.5)) - - # Set default labels and colors if not provided - if labels is None: - labels = [self.get_clean_label(d) for d in self.directories] - - if color is None: - color = "purple" - colors = [color] * len(self.directories) - # Collect data for plotting - xvals, yvals = [], [] - for d, label, color in zip(self.directories, labels, colors, strict=False): - # Read data from the analysis output file - output_file = f"{d}/output_stats.txt" - with open(output_file, encoding="utf-8") as f: - lines = f.readlines() - mean_surface_area = float(lines[2].split(": ")[1].strip()) - mean_contact_angle = float( - lines[3].split(": ")[1].strip().replace("°", "") - ) - std_contact_angle = float( - lines[4].split(": ")[1].strip().replace("°", "") - ) - - # Use the data for plotting - x = 1 / np.sqrt(mean_surface_area) - # Convert angle to radians for cosine calculation - mean_angle_rad = np.radians(mean_contact_angle) - y = np.cos(mean_angle_rad) - - # Propagate error: d(cos(theta)) = |-sin(theta)| * d(theta) - # d(theta) must be in radians - std_angle_rad = np.radians(std_contact_angle) - yerr = np.abs(np.sin(mean_angle_rad)) * (std_angle_rad / 5) - - ax.errorbar( - x, y, yerr=yerr, fmt="o", color=color, markersize=6, capsize=3, lw=1.2 - ) - ax.annotate( - label, - xy=(x, y), - xytext=(5, 5), - textcoords="offset points", - ha="left", - va="center", - fontsize=6, - color="black", - ) - xvals.append(x) - yvals.append(y) - - # Linear fit - if len(xvals) >= 2: - xvals_arr, yvals_arr = np.array(xvals), np.array(yvals) - coeffs = np.polyfit(xvals_arr, yvals_arr, 1) - fit_line = np.poly1d(coeffs) - intercept = coeffs[1] - - intercept_clipped = np.clip(intercept, -1.0, 1.0) - theta_inf_deg = np.degrees(np.arccos(intercept_clipped)) - - x_fit = np.linspace(0, max(xvals) * 1.1, 100) - ax.plot( - x_fit, - fit_line(x_fit), - "--", - color="gray", - lw=1.5, - label=( - f"Fit: $\\cos(\\theta) = {coeffs[0]:.2f}x + {coeffs[1]:.2f}$\n" - f"$\\theta_\\infty = {theta_inf_deg:.1f}^\\circ$" - ), - ) - - # Set plot labels and title - ax.set_xlabel(r"$1 / \sqrt{A} \; (\mathrm{\AA^{-1}})$") - ax.set_ylabel(r"$\cos(\theta)$") - ax.set_title(f"{self.get_method_name()} - Modified Young's Eq Plot", pad=10) - ax.legend(frameon=False, loc="best") - ax.grid(False) - ax.set_xlim(left=-0.001) - if len(yvals) > 0: - margin = (max(yvals) - min(yvals)) * 0.2 if len(yvals) > 1 else 0.1 - if margin == 0: - margin = 0.1 - ax.set_ylim(bottom=min(yvals) - margin, top=max(yvals) + margin) - plt.tight_layout() - if save_path: - plt.savefig(save_path, dpi=400, bbox_inches="tight") - plt.close() diff --git a/src/wetting_angle_kit/visualization/base_trajectory_plotter.py b/src/wetting_angle_kit/visualization/base_trajectory_plotter.py new file mode 100644 index 0000000..1254408 --- /dev/null +++ b/src/wetting_angle_kit/visualization/base_trajectory_plotter.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod + +from wetting_angle_kit.visualization.stats import TrajectoryStats + + +class BaseTrajectoryPlotter(ABC): + """Abstract base for trajectory plotters. + + Subclasses own their own data layout (per-method result containers, + directories, etc.) and must implement :meth:`summary` returning one + :class:`TrajectoryStats` per trajectory. + """ + + @abstractmethod + def summary(self) -> list[TrajectoryStats]: + """Return per-trajectory summary statistics.""" diff --git a/src/wetting_angle_kit/visualization/binning_trajectory_analyzer.py b/src/wetting_angle_kit/visualization/binning_trajectory_analyzer.py deleted file mode 100644 index ca15706..0000000 --- a/src/wetting_angle_kit/visualization/binning_trajectory_analyzer.py +++ /dev/null @@ -1,168 +0,0 @@ -import glob -import logging -import os -import re - -import numpy as np - -from wetting_angle_kit.visualization.base_trajectory_analyzer import ( - BaseTrajectoryAnalyzer, -) - -logger = logging.getLogger(__name__) - - -class BinningTrajectoryAnalyzer(BaseTrajectoryAnalyzer): - """Analyze binning trajectory data using circular segment calculations.""" - - def __init__( - self, - directories: list[str], - split_factor: int = 1, - time_steps: dict[str, float] | None = None, - time_unit: str = "ps", - ) -> None: - """ - Initialize the analyzer with a list of directory paths and split factor. - - Parameters - ---------- - directories : list of str - List of directory paths containing analysis results. - split_factor : int, optional - Number of batches/splits to process in each directory. - time_steps : dict, optional - Dictionary mapping directory to its time step. - time_unit : str, optional - Time unit for the x-axis (e.g., "ps", "ns", "fs"). - """ - self.split_factor = split_factor - self.time_steps = time_steps if time_steps else {d: 1.0 for d in directories} - - # Initialize Base Class (this will trigger _initialize_data_structure) - super().__init__(directories, time_unit=time_unit) - - def _initialize_data_structure(self) -> None: - """Initialize data structure for binning analysis.""" - for directory in self.directories: - self.data[directory] = { - "R_eq": [], - "zi_c": [], - "zi_0": [], - "contact_angles": [], - "surface_areas": [], - "time_step": self.time_steps.get(directory, 1.0), - } - - def get_method_name(self) -> str: - """Return method name.""" - return "Binning Analysis" - - @staticmethod - def circular_segment_area(R: float, z_center: float, z_cut: float) -> float: - """Return the area of the circular cap below ``z_cut``. - - Parameters - ---------- - R : float - Circle radius. - z_center : float - z-coordinate of the circle center. - z_cut : float - Height of the cutting plane. - - Returns - ------- - float - Area of the circular segment below the cut. - """ - h = (z_center + R) - z_cut # height of the cap - if h <= 0: - return 0.0 - if h >= 2 * R: - return np.pi * R**2 - if h <= R: - return R**2 * np.arccos((R - h) / R) - (R - h) * np.sqrt(2 * R * h - h**2) - else: - h_small = 2 * R - h - return np.pi * R**2 - ( - R**2 * np.arccos((R - h_small) / R) - - (R - h_small) * np.sqrt(2 * R * h_small - h_small**2) - ) - - def load_files(self) -> None: - """Load and sort all relevant log files from each directory.""" - for directory in self.directories: - log_files = sorted( - glob.glob(os.path.join(directory, "log_data_batch_*.txt")), - key=lambda x: int(re.search(r"batch_(\d+)", x).group(1)), # type: ignore[union-attr] - ) - if not log_files: - raise ValueError( - f"No log_data_batch_*.txt files found in directory: {directory}" - ) - self.data[directory]["log_files"] = log_files - - def read_data(self) -> None: - """Alias for load_data for backward compatibility.""" - self.load_data() - - def load_data(self) -> None: - """Read and parse data from log files in each directory.""" - self.load_files() - for directory in self.directories: - # Clear previous data for this directory - self.data[directory]["R_eq"] = [] - self.data[directory]["zi_c"] = [] - self.data[directory]["zi_0"] = [] - self.data[directory]["contact_angles"] = [] - self.data[directory]["surface_areas"] = [] - logger.debug( - "Log files for %s: %s", directory, self.data[directory]["log_files"] - ) - # Read all batch log files for this directory - for log_file in self.data[directory]["log_files"]: - with open(log_file) as f: - text = f.read() - - # Extract R_eq - R_eq_match = re.search(r"R_eq:([0-9\.\-eE]+)", text) - if not R_eq_match: - raise ValueError(f"R_eq not found in file: {log_file}") - R_eq = float(R_eq_match.group(1)) - - # Extract zi_c - zi_c_match = re.search(r"zi_c:([0-9\.\-eE]+)", text) - if not zi_c_match: - raise ValueError(f"zi_c not found in file: {log_file}") - zi_c = float(zi_c_match.group(1)) - - # Extract zi_0 - zi_0_match = re.search(r"zi_0:([0-9\.\-eE]+)", text) - if not zi_0_match: - raise ValueError(f"zi_0 not found in file: {log_file}") - zi_0 = float(zi_0_match.group(1)) - - # Extract contact angle - angle_match = re.search(r"Contact angle:([0-9\.\-eE]+)", text) - if not angle_match: - raise ValueError(f"Contact angle not found in file: {log_file}") - angle = float(angle_match.group(1)) - - # Calculate surface area - A_seg = self.circular_segment_area(R_eq, zi_c, zi_0) - - # Append data - self.data[directory]["R_eq"].append(R_eq) - self.data[directory]["zi_c"].append(zi_c) - self.data[directory]["zi_0"].append(zi_0) - self.data[directory]["contact_angles"].append(angle) - self.data[directory]["surface_areas"].append(A_seg) - - def get_surface_areas(self, directory: str) -> np.ndarray: - """Return surface areas for a directory.""" - return np.array(self.data[directory]["surface_areas"]) - - def get_contact_angles(self, directory: str) -> np.ndarray: - """Return contact angles for a directory.""" - return np.array(self.data[directory]["contact_angles"]) diff --git a/src/wetting_angle_kit/visualization/binning_trajectory_plotter.py b/src/wetting_angle_kit/visualization/binning_trajectory_plotter.py new file mode 100644 index 0000000..1dd0d8e --- /dev/null +++ b/src/wetting_angle_kit/visualization/binning_trajectory_plotter.py @@ -0,0 +1,199 @@ +from collections.abc import Iterable + +import numpy as np +import plotly.graph_objects as go + +from wetting_angle_kit.analysis.binning.results import BinningResults +from wetting_angle_kit.visualization.base_trajectory_plotter import ( + BaseTrajectoryPlotter, +) +from wetting_angle_kit.visualization.stats import TrajectoryStats + + +class BinningTrajectoryPlotter(BaseTrajectoryPlotter): + """Plot statistics derived from one or more :class:`BinningResults`.""" + + @staticmethod + def circular_segment_area(R: float, z_center: float, z_cut: float) -> float: + """Area of the circular cap of radius ``R`` below height ``z_cut``.""" + h = (z_center + R) - z_cut + if h <= 0: + return 0.0 + if h >= 2 * R: + return float(np.pi * R**2) + if h <= R: + return float( + R**2 * np.arccos((R - h) / R) - (R - h) * np.sqrt(2 * R * h - h**2) + ) + h_small = 2 * R - h + return float( + np.pi * R**2 + - ( + R**2 * np.arccos((R - h_small) / R) + - (R - h_small) * np.sqrt(2 * R * h_small - h_small**2) + ) + ) + + def __init__( + self, + results: BinningResults | Iterable[BinningResults], + labels: list[str] | None = None, + time_steps: list[float] | None = None, + time_unit: str = "ps", + ) -> None: + """ + Parameters + ---------- + results : BinningResults or iterable of BinningResults + One results container per trajectory. + labels : list of str, optional + Display labels (one per results container). Defaults to + ``["trajectory_0", ...]``. + time_steps : list of float, optional + Per-trajectory time step applied to ``batch_index`` for the + time axis of evolution plots. Defaults to ``1.0`` for each. + time_unit : str, optional + Time unit shown on x-axis labels. + """ + if isinstance(results, BinningResults): + results = [results] + else: + results = list(results) + self.results = results + self.labels = labels or [f"trajectory_{i}" for i in range(len(results))] + self.time_steps = time_steps or [1.0] * len(results) + self.time_unit = time_unit + + def _surface_areas(self, result: BinningResults) -> np.ndarray: + """Per-batch circular-cap surface area from fitted (R_eq, zi_c, zi_0).""" + return np.array( + [ + self.circular_segment_area( + batch.fitted_params["R_eq"], + batch.fitted_params["zi_c"], + batch.fitted_params["zi_0"], + ) + for batch in result.batches + ] + ) + + def summary(self) -> list[TrajectoryStats]: + stats: list[TrajectoryStats] = [] + for label, result in zip(self.labels, self.results, strict=False): + surfaces = self._surface_areas(result) + stats.append( + TrajectoryStats( + method_name="Binning Analysis", + label=label, + mean_surface_area=float(np.mean(surfaces)), + mean_contact_angle=result.mean_angle, + std_contact_angle=result.std_angle, + n_samples=len(result), + ) + ) + return stats + + def plot_angle_evolution(self, save_path: str | None = None) -> go.Figure: + """Plot per-batch contact angle as a function of batch time. + + Parameters + ---------- + save_path : str, optional + If provided, write the figure as standalone HTML. + + Returns + ------- + plotly.graph_objects.Figure + Figure with one line per trajectory. + """ + fig = go.Figure() + for label, result, dt in zip( + self.labels, self.results, self.time_steps, strict=False + ): + times = np.array([b.batch_index for b in result.batches]) * dt + fig.add_trace( + go.Scatter( + x=times, + y=result.angles_per_batch, + mode="lines+markers", + name=label, + line=dict(width=2), + ) + ) + fig.update_layout( + title="Contact angle evolution (per batch)", + xaxis_title=f"Batch time ({self.time_unit})", + yaxis_title="Contact angle (°)", + template="plotly_white", + ) + if save_path: + fig.write_html(save_path) + return fig + + def plot_density_contour( + self, + result_index: int = 0, + batch_index: int = 0, + save_path: str | None = None, + ) -> go.Figure: + """Plot the density field of one batch with the fitted isoline. + + Parameters + ---------- + result_index : int, default 0 + Index into the results list (selects which trajectory). + batch_index : int, default 0 + Index of the batch within that trajectory. + save_path : str, optional + If provided, write the figure as standalone HTML. + + Returns + ------- + plotly.graph_objects.Figure + Filled contour of the density field plus dashed circle / wall + isoline traces when available. + """ + batch = self.results[result_index].batches[batch_index] + fig = go.Figure() + fig.add_trace( + go.Contour( + x=batch.xi_cc, + y=batch.zi_cc, + z=np.transpose(batch.rho_cc), + colorscale="Jet", + colorbar=dict(title="ρ"), + ) + ) + if batch.circle_xi is not None and batch.circle_zi is not None: + fig.add_trace( + go.Scatter( + x=batch.circle_xi, + y=batch.circle_zi, + mode="lines", + name="Fitted droplet", + line=dict(color="black", dash="dash", width=2), + ) + ) + if batch.wall_line_xi is not None and batch.wall_line_zi is not None: + fig.add_trace( + go.Scatter( + x=batch.wall_line_xi, + y=batch.wall_line_zi, + mode="lines", + name="Fitted wall", + line=dict(color="black", dash="dash", width=2), + ) + ) + fig.update_layout( + title=( + f"Density field — {self.labels[result_index]} " + f"(batch {batch.batch_index})" + ), + xaxis_title="ξ (Å)", + yaxis_title="z (Å)", + template="plotly_white", + yaxis=dict(scaleanchor="x", scaleratio=1), + ) + if save_path: + fig.write_html(save_path) + return fig diff --git a/src/wetting_angle_kit/visualization/droplet_slice_plot.py b/src/wetting_angle_kit/visualization/droplet_slice_plot.py new file mode 100644 index 0000000..8cec03f --- /dev/null +++ b/src/wetting_angle_kit/visualization/droplet_slice_plot.py @@ -0,0 +1,225 @@ +from collections.abc import Sequence +from typing import Any + +import numpy as np +import plotly.graph_objects as go + + +class DropletSlicePlotter: + """Interactive Plotly slice visualization with toggleable layers.""" + + def __init__(self, center: bool = True): + """ + Parameters + ---------- + center : bool, default True + If True recentre z coordinates by subtracting mean wall z. + """ + self.center = center + # Colors + self.oxygen_color = "#d62828" + self.hydrogen_color = "#FFFFFF" + self.surface_color = "#000000" + self.circle_color = "#0A9396" + self.wall_color = "#000000" + self.tangent_color = "#bb3e03" + + def plot_surface_points( + self, + oxygen_position: np.ndarray, + surface_data: list[np.ndarray], + popt: Sequence[float], + wall_coords: np.ndarray, + alpha: float | None = None, + y_com: float | None = None, + pbc_y: float | None = None, + show_water: bool = True, + show_surface: bool = True, + show_circle: bool = True, + show_tangent: bool = True, + show_wall: bool = True, + ) -> Any: + """Create interactive Plotly figure for a single frame slice. + + Parameters + ---------- + oxygen_position : ndarray (N, 3) + Oxygen atom coordinates. + surface_data : list[array] + List of surface contours for selected slice. + popt : sequence + Fitted circle parameters (x_center, z_center, radius, extra). + wall_coords : ndarray (M, 3) + Wall particle coordinates. + alpha : float, optional + Contact angle for tangent construction. + y_com : float, optional + Mean y used for slicing; computed if None. + pbc_y : float, optional + Y box length for periodic slicing. + show_water, show_surface, show_circle, show_tangent, show_wall : bool + Layer visibility toggles. + + Returns + ------- + plotly.graph_objects.Figure + Configured figure object (not saved). + """ + if y_com is None: + y_com = np.mean(oxygen_position[:, 1]) + # Select slice in y-direction + if pbc_y is not None: + dy = np.abs(oxygen_position[:, 1] - y_com) + dy = np.minimum(dy, pbc_y - dy) + mask = dy <= 3 + else: + mask = np.abs(oxygen_position[:, 1] - y_com) <= 3 + oxygen_selected = oxygen_position[mask] + # Recenter if needed + if self.center: + z_shift = np.mean(wall_coords[:, 2]) + oxygen_selected[:, 2] -= z_shift + wall_coords[:, 2] -= z_shift + surface_data = [ + np.column_stack([surf[:, 0], surf[:, 1] - z_shift]) + for surf in surface_data + ] + x_center, z_center, radius, _ = popt + z_center -= z_shift + else: + x_center, z_center, radius, _ = popt + fig = go.Figure() + # --- Wall --- + if show_wall: + fig.add_trace( + go.Scatter( + x=wall_coords[:, 0], + y=wall_coords[:, 2], + mode="markers", + name="Wall", + marker=dict(color=self.wall_color, size=3), + opacity=0.7, + visible=True, + showlegend=True, + ) + ) + # --- Water molecules --- + if show_water: + fig.add_trace( + go.Scatter( + x=oxygen_selected[:, 0], + y=oxygen_selected[:, 2], + mode="markers", + name="Water", + marker=dict(color=self.oxygen_color, size=5), + opacity=0.8, + visible=True, + showlegend=True, + ) + ) + # --- Surface contour --- + if show_surface: + for surf in surface_data: + # Append the first point to the end to close the contour + closed_surf = np.vstack([surf, surf[0]]) + fig.add_trace( + go.Scatter( + x=closed_surf[:, 0], + y=closed_surf[:, 1], + mode="lines", + name="Surface contour", + line=dict(color=self.surface_color, width=3), + visible=True, + showlegend=True, + ) + ) + # --- Fitted circle --- + if show_circle: + theta = np.linspace(0, 2 * np.pi, 200) + circle_x = x_center + radius * np.cos(theta) + circle_z = z_center + radius * np.sin(theta) + fig.add_trace( + go.Scatter( + x=circle_x, + y=circle_z, + mode="lines", + name="Fitted Circle", + line=dict(color=self.circle_color, width=3, dash="dash"), + visible=True, + showlegend=True, + ) + ) + # --- Tangent + α arc --- + if show_tangent and alpha is not None: + z_line = min([np.min(surf[:, 1]) for surf in surface_data]) + delta_z = z_line - z_center + discriminant = radius**2 - delta_z**2 + if discriminant > 0: + x_contact = x_center + np.sqrt(discriminant) # Right side + z_contact = z_line + m_tangent = -(x_contact - x_center) / (z_contact - z_center) + # Tangent line + z_top = z_center + radius * 1.1 + x_top = x_contact + (z_top - z_contact) / m_tangent + x_line = np.linspace(x_contact, x_top, 100) + z_line_tan = m_tangent * (x_line - x_contact) + z_contact + fig.add_trace( + go.Scatter( + x=x_line, + y=z_line_tan, + mode="lines", + name=f"{alpha:.1f}°", + line=dict(color=self.tangent_color, width=3), + visible=True, + showlegend=True, + ) + ) + # α arc (left side) + alpha_rad = np.radians(alpha) + arc_radius = radius * 0.25 + theta_arc = np.linspace(np.pi - alpha_rad, np.pi, 100) + arc_x = x_contact + arc_radius * np.cos(theta_arc) + arc_z = z_contact + arc_radius * np.sin(theta_arc) + fig.add_trace( + go.Scatter( + x=arc_x, + y=arc_z, + mode="lines", + name=f"{alpha:.1f}° Arc", + line=dict(color="gray", width=2), + visible=True, + showlegend=False, + ) + ) + # Label α near mid-arc + mid_theta = alpha_rad / 2 + text_x = x_contact + 1.2 * arc_radius * np.cos(mid_theta) + text_z = z_contact + 1.2 * arc_radius * np.sin(mid_theta) + fig.add_annotation( + x=text_x, + y=text_z, + text=f"{alpha:.1f}°", + showarrow=False, + font=dict(size=12, color="black"), + ) + # --- Layout --- + fig.update_layout( + width=600, + height=450, + xaxis_title="x (Å)", + yaxis_title="z (Å)", + template="plotly_white", + showlegend=True, + legend=dict( + x=1.05, + y=1, + bgcolor="rgba(255, 255, 255, 0.8)", + bordercolor="gray", + borderwidth=1, + itemsizing="constant", + font=dict(size=10), + ), + yaxis=dict(scaleanchor="x", scaleratio=1), + ) + + return fig diff --git a/src/wetting_angle_kit/visualization/droplet_slice_plots.py b/src/wetting_angle_kit/visualization/droplet_slice_plots.py deleted file mode 100644 index 78139de..0000000 --- a/src/wetting_angle_kit/visualization/droplet_slice_plots.py +++ /dev/null @@ -1,758 +0,0 @@ -from collections.abc import Sequence -from typing import Any - -import matplotlib.pyplot as plt -import numpy as np -import plotly.graph_objects as go -from matplotlib.ticker import AutoMinorLocator - -from wetting_angle_kit.analysis.slicing import ContactAngleSlicing -from wetting_angle_kit.io_utils import recenter_droplet_pbc -from wetting_angle_kit.parsers import ( - LammpsDumpParser, - LammpsDumpWallParser, - LammpsDumpWaterFinder, -) - -plt.style.use("seaborn-v0_8-whitegrid") - - -class DropletSlicePlotter: - """Matplotlib-based plotter for droplet slices: surface contours, - fitted circle and tangent line.""" - - def __init__( - self, center: bool = True, show_wall: bool = True, molecule_view: bool = True - ): - """ - Parameters - ---------- - center : bool, default True - If True recentre z coordinates by subtracting mean wall z. - show_wall : bool, default True - Whether to draw wall particles. - molecule_view : bool, default True - If True draw fake hydrogens around each oxygen (schematic water view). - """ - self.center = center - self.show_wall = show_wall - self.molecule_view = molecule_view - - # Colors - self.oxygen_color = "#d62828" - self.hydrogen_color = "white" - self.surface_color = "black" - self.circle_color = "#0A9396" - self.wall_color = "black" - self.tangent_color = "#bb3e03" - - def plot_surface_points( - self, - oxygen_position: np.ndarray, - surface_data: list[np.ndarray], - popt: Sequence[float], - wall_coords: np.ndarray | None = None, - output_filename: Any | None = None, - y_com: float | None = None, - pbc_y: float | None = None, - alpha: float | None = None, - ) -> None: - """Render slice figure and save to file. - - Parameters - ---------- - oxygen_position : ndarray (N, 3) - Cartesian coordinates of oxygen atoms for the frame. - surface_data : list[array] - List of arrays with surface line coordinates (x, z) for each slice. - popt : sequence - Fitted circle parameters (x_center, z_center, radius, extra) - for chosen slice. - wall_coords : ndarray (M, 3) - Wall particle coordinates. - output_filename : str or Path - Path to save the PNG figure. - y_com : float, optional - Y centre used to select atoms in a thin slice. If None computed. - pbc_y : float, optional - Box length in y for PBC wrapping; if provided shortest-distance used. - alpha : float, optional - Contact angle in degrees; if given draw tangent line and arc. - - Returns - ------- - None - Saves figure to ``output_filename`` and closes it. - """ - - if y_com is None: - y_com = np.mean(oxygen_position[:, 1]) - - # Select atoms near the Y center (±3 Å) - if pbc_y is not None: - dy = np.abs(oxygen_position[:, 1] - y_com) - dy = np.minimum(dy, pbc_y - dy) - mask = dy <= 3 - else: - mask = np.abs(oxygen_position[:, 1] - y_com) <= 3 - oxygen_selected = oxygen_position[mask] - - # --- Subsample for clarity --- - rng = np.random.default_rng(42) - keep_fraction = 0.70 - sample_idx = rng.choice( - len(oxygen_selected), - size=int(len(oxygen_selected) * keep_fraction), - replace=False, - ) - oxygen_selected = oxygen_selected[sample_idx] - - # --- Limit wall region under droplet (±5 Å margin) --- - x_min, x_max = ( - np.min(oxygen_selected[:, 0]) - 5, - np.max(oxygen_selected[:, 0]) + 5, - ) - - # Only process wall_coords if needed - if self.show_wall and wall_coords is not None: - wall_mask = (wall_coords[:, 0] >= x_min) & (wall_coords[:, 0] <= x_max) - wall_coords = wall_coords[wall_mask] - - # --- Optional recentring --- - if self.center and wall_coords is not None: - z_shift = np.mean(wall_coords[:, 2]) - oxygen_selected[:, 2] -= z_shift - wall_coords[:, 2] -= z_shift - surface_data = [ - np.column_stack([surf[:, 0], surf[:, 1] - z_shift]) - for surf in surface_data - ] - x_center, z_center, radius, limit_med = popt - z_center -= z_shift - else: - x_center, z_center, radius, limit_med = popt - - # --- Plot setup --- - fig, ax = plt.subplots(figsize=(4.0, 3.0), dpi=300) - - # --- Wall atoms --- - if self.show_wall and wall_coords is not None: - ax.scatter( - wall_coords[:, 0], - wall_coords[:, 2], - color=self.wall_color, - s=3, - alpha=0.7, - zorder=0, - ) - - # --- Water representation --- - if self.molecule_view: - h_dist = 1.0 - for ox, _oy, oz in oxygen_selected: - ax.scatter( - ox, - oz, - color=self.oxygen_color, - s=8, - alpha=0.9, - edgecolors="none", - linewidths=0.15, - zorder=1, - ) - for _ in range(2): - angle = rng.uniform(0, 2 * np.pi) - dx, dz = h_dist * np.cos(angle), h_dist * np.sin(angle) - ax.scatter( - ox + dx, - oz + dz, - color=self.hydrogen_color, - s=4, - alpha=0.8, - edgecolors="black", - linewidths=0.15, - zorder=1, - ) - else: - ax.scatter( - oxygen_selected[:, 0], - oxygen_selected[:, 2], - color=self.oxygen_color, - s=6, - alpha=0.9, - zorder=1, - ) - - # --- Surface line --- - for surf in surface_data: - x_data, z_data = surf[:, 0], surf[:, 1] - if not np.allclose([x_data[0], z_data[0]], [x_data[-1], z_data[-1]]): - x_data = np.append(x_data, x_data[0]) - z_data = np.append(z_data, z_data[0]) - ax.plot(x_data, z_data, color=self.surface_color, lw=1.5, zorder=3) - - # --- Fitted circle --- - circle = plt.Circle( - (x_center, z_center), - radius, - color=self.circle_color, - fill=False, - ls="--", - lw=2.5, - zorder=4, - ) - ax.add_artist(circle) - # --- Tangent line (based on circle–surface intersection) --- - if alpha is not None: - alpha_rad = np.radians(alpha) - - # --- Determine the contact point from the surface bottom --- - z_baseline = min(np.min(surf[:, 1]) for surf in surface_data) - # Use the (possibly z-shifted) circle parameters set above. - delta_z = z_baseline - z_center - discriminant = radius**2 - delta_z**2 - if discriminant <= 0: - plt.close(fig) - return - - dx = np.sqrt(discriminant) - - # Choose correct side (right if α > 90°, left if α < 90°) - if alpha > 90: - x_contact = x_center + dx - else: - x_contact = x_center - dx - z_contact = z_baseline - - # --- Tangent slope at the intersection point --- - m_tangent = -(x_contact - x_center) / (z_contact - z_center) - - # --- Extend tangent line upwards to top of circle --- - z_top = z_center + radius * 1.1 # extend slightly above for visibility - if abs(m_tangent) > 1e-6: - x_top = x_contact + (z_top - z_contact) / m_tangent - else: - x_top = x_contact - x_tangent = np.linspace(x_contact, x_top, 100) - z_tangent = m_tangent * (x_tangent - x_contact) + z_contact - - # Draw tangent line - ax.plot( - x_tangent, - z_tangent, - color=self.tangent_color, - lw=2.0, - ls="-", - label=f"Tangent (α={alpha:.1f}°)", - zorder=5, - ) - - # --- Draw arc centered at contact point --- - arc_radius = radius * 0.25 - theta = np.linspace( - np.pi - alpha_rad, np.pi, 100 - ) # from horizontal (0) to tangent (α) - arc_x = x_contact + arc_radius * np.cos(theta) - arc_z = z_contact + arc_radius * np.sin(theta) - ax.plot(arc_x, arc_z, color="gray", lw=1.5, zorder=6) - - # --- Label α value near the middle of the arc --- - mid_theta = alpha_rad / 2 - text_x = x_contact + 1.2 * arc_radius * np.cos(mid_theta) - text_z = z_contact + 1.2 * arc_radius * np.sin(mid_theta) - ax.text( - text_x, - text_z, - f"{alpha:.1f}°", - fontsize=9, - color="black", - ha="center", - va="center", - zorder=7, - ) - - # --- Axes --- - ax.set_xlabel("x (Å)", fontsize=9) - ax.set_ylabel("z (Å)", fontsize=9) - ax.tick_params(axis="both", which="major", labelsize=8) - ax.xaxis.set_minor_locator(AutoMinorLocator()) - ax.yaxis.set_minor_locator(AutoMinorLocator()) - ax.set_aspect("equal", adjustable="box") - ax.grid(False) - ax.set_xlim(x_min - 5, x_max + 5) - - # --- Legend --- - ax.legend( - handles=[ - plt.Line2D( - [], [], color=self.surface_color, lw=1.5, label="Surface contour" - ), - plt.Line2D( - [], - [], - color=self.circle_color, - ls="--", - lw=1.5, - label="Fitted circle", - ), - plt.Line2D( - [], [], color=self.tangent_color, lw=1.5, label="Tangent line" - ), - ], - loc="upper left", - frameon=False, - fontsize=7, - ) - - plt.tight_layout(pad=0.1) - plt.savefig(output_filename, dpi=300, bbox_inches="tight") - plt.close() - - -class DropletSlicePlotlyPlotter: - """Interactive Plotly slice visualization with toggleable layers.""" - - def __init__(self, center: bool = True): - """ - Parameters - ---------- - center : bool, default True - If True recentre z coordinates by subtracting mean wall z. - """ - self.center = center - # Colors - self.oxygen_color = "#d62828" - self.hydrogen_color = "#FFFFFF" - self.surface_color = "#000000" - self.circle_color = "#0A9396" - self.wall_color = "#000000" - self.tangent_color = "#bb3e03" - - def plot_surface_points( - self, - oxygen_position: np.ndarray, - surface_data: list[np.ndarray], - popt: Sequence[float], - wall_coords: np.ndarray, - alpha: float | None = None, - y_com: float | None = None, - pbc_y: float | None = None, - show_water: bool = True, - show_surface: bool = True, - show_circle: bool = True, - show_tangent: bool = True, - show_wall: bool = True, - ) -> Any: - """Create interactive Plotly figure for a single frame slice. - - Parameters - ---------- - oxygen_position : ndarray (N, 3) - Oxygen atom coordinates. - surface_data : list[array] - List of surface contours for selected slice. - popt : sequence - Fitted circle parameters (x_center, z_center, radius, extra). - wall_coords : ndarray (M, 3) - Wall particle coordinates. - alpha : float, optional - Contact angle for tangent construction. - y_com : float, optional - Mean y used for slicing; computed if None. - pbc_y : float, optional - Y box length for periodic slicing. - show_water, show_surface, show_circle, show_tangent, show_wall : bool - Layer visibility toggles. - - Returns - ------- - plotly.graph_objects.Figure - Configured figure object (not saved). - """ - if y_com is None: - y_com = np.mean(oxygen_position[:, 1]) - # Select slice in y-direction - if pbc_y is not None: - dy = np.abs(oxygen_position[:, 1] - y_com) - dy = np.minimum(dy, pbc_y - dy) - mask = dy <= 3 - else: - mask = np.abs(oxygen_position[:, 1] - y_com) <= 3 - oxygen_selected = oxygen_position[mask] - # Recenter if needed - if self.center: - z_shift = np.mean(wall_coords[:, 2]) - oxygen_selected[:, 2] -= z_shift - wall_coords[:, 2] -= z_shift - surface_data = [ - np.column_stack([surf[:, 0], surf[:, 1] - z_shift]) - for surf in surface_data - ] - x_center, z_center, radius, _ = popt - z_center -= z_shift - else: - x_center, z_center, radius, _ = popt - fig = go.Figure() - # --- Wall --- - if show_wall: - fig.add_trace( - go.Scatter( - x=wall_coords[:, 0], - y=wall_coords[:, 2], - mode="markers", - name="Wall", - marker=dict(color=self.wall_color, size=3), - opacity=0.7, - visible=True, - showlegend=True, - ) - ) - # --- Water molecules --- - if show_water: - fig.add_trace( - go.Scatter( - x=oxygen_selected[:, 0], - y=oxygen_selected[:, 2], - mode="markers", - name="Water", - marker=dict(color=self.oxygen_color, size=5), - opacity=0.8, - visible=True, - showlegend=True, - ) - ) - # --- Surface contour --- - if show_surface: - for surf in surface_data: - # Append the first point to the end to close the contour - closed_surf = np.vstack([surf, surf[0]]) - fig.add_trace( - go.Scatter( - x=closed_surf[:, 0], - y=closed_surf[:, 1], - mode="lines", - name="Surface contour", - line=dict(color=self.surface_color, width=3), # Thicker line - visible=True, - showlegend=True, - ) - ) - # --- Fitted circle --- - if show_circle: - theta = np.linspace(0, 2 * np.pi, 200) - circle_x = x_center + radius * np.cos(theta) - circle_z = z_center + radius * np.sin(theta) - fig.add_trace( - go.Scatter( - x=circle_x, - y=circle_z, - mode="lines", - name="Fitted Circle", - line=dict( - color=self.circle_color, width=3, dash="dash" - ), # Thicker line - visible=True, - showlegend=True, - ) - ) - # --- Tangent + α arc --- - if show_tangent and alpha is not None: - z_line = min([np.min(surf[:, 1]) for surf in surface_data]) - delta_z = z_line - z_center - discriminant = radius**2 - delta_z**2 - if discriminant > 0: - x_contact = x_center + np.sqrt(discriminant) # Right side - z_contact = z_line - m_tangent = -(x_contact - x_center) / (z_contact - z_center) - # Tangent line - z_top = z_center + radius * 1.1 - x_top = x_contact + (z_top - z_contact) / m_tangent - x_line = np.linspace(x_contact, x_top, 100) - z_line_tan = m_tangent * (x_line - x_contact) + z_contact - fig.add_trace( - go.Scatter( - x=x_line, - y=z_line_tan, - mode="lines", - name=f"{alpha:.1f}°", # Only show angle value - line=dict(color=self.tangent_color, width=3), # Thicker line - visible=True, - showlegend=True, - ) - ) - # α arc (left side) - alpha_rad = np.radians(alpha) - arc_radius = radius * 0.25 - theta_arc = np.linspace(np.pi - alpha_rad, np.pi, 100) - arc_x = x_contact + arc_radius * np.cos(theta_arc) - arc_z = z_contact + arc_radius * np.sin(theta_arc) - fig.add_trace( - go.Scatter( - x=arc_x, - y=arc_z, - mode="lines", - name=f"{alpha:.1f}° Arc", # Only show angle value - line=dict(color="gray", width=2), - visible=True, - showlegend=False, - ) - ) - - # Label α near mid-arc - mid_theta = alpha_rad / 2 - text_x = x_contact + 1.2 * arc_radius * np.cos(mid_theta) - text_z = z_contact + 1.2 * arc_radius * np.sin(mid_theta) - fig.add_annotation( - x=text_x, - y=text_z, - text=f"{alpha:.1f}°", - showarrow=False, - font=dict(size=12, color="black"), - ) - # --- Layout --- - fig.update_layout( - width=600, - height=450, - xaxis_title="x (Å)", - yaxis_title="z (Å)", - template="plotly_white", - showlegend=True, - legend=dict( - x=1.05, - y=1, # Position legend outside the plot - bgcolor="rgba(255, 255, 255, 0.8)", - bordercolor="gray", - borderwidth=1, - itemsizing="constant", # Ensures checkboxes are clearly visible - font=dict(size=10), - ), - yaxis=dict(scaleanchor="x", scaleratio=1), - ) - - return fig - - -class ContactAngleAnimator: - """Generate interactive Plotly slider animation of median slice angle per frame.""" - - def __init__( - self, - filename: str, - particle_type_wall: set, - oxygen_type: int, - hydrogen_type: int, - liquid_particle_types: set, - n_frames: int = 10, - droplet_geometry: str = "cylinder_y", - delta_cylinder: int = 5, - max_dist: int = 100, - width_cylinder: int = 21, - precentered: bool = False, - ): - """ - Parameters - ---------- - filename : str - Path to LAMMPS dump trajectory file. - particle_type_wall : set - LAMMPS particle type IDs for wall atoms. - oxygen_type : int - LAMMPS particle type ID for oxygen atoms. - hydrogen_type : int - LAMMPS particle type ID for hydrogen atoms. - liquid_particle_types : set - LAMMPS particle type IDs for all liquid atoms (used to mask wall parser). - n_frames : int, default 10 - Number of frames to include in the animation. - droplet_geometry : str, default "cylinder_y" - Droplet geometry passed to ContactAngleSlicing. - delta_cylinder : int, default 5 - Step size along the slicing axis (Å). - max_dist : int, default 100 - Maximum radial distance for line sampling (Å). - width_cylinder : int, default 21 - Box extent along the cylinder axis (Å). - precentered : bool, default False - Set True if the trajectory already recenters the droplet at - every frame and atoms are not wrapped across periodic - boundaries; the per-frame circular-mean recentering is then - skipped. Setting this on a trajectory that does NOT satisfy the - precondition will misplace the contact-angle overlay. - """ - self.filename = filename - self.particle_type_wall = particle_type_wall - self.oxygen_type = oxygen_type - self.hydrogen_type = hydrogen_type - self.liquid_particle_types = liquid_particle_types - self.n_frames = n_frames - self.droplet_geometry = droplet_geometry - self.delta_cylinder = delta_cylinder - self.max_dist = max_dist - self.width_cylinder = width_cylinder - self.precentered = precentered - - # Initialize objects - self.wat_find = LammpsDumpWaterFinder( - self.filename, - particle_type_wall=self.particle_type_wall, - oxygen_type=self.oxygen_type, - hydrogen_type=self.hydrogen_type, - ) - self.oxygen_indices = self.wat_find.get_water_oxygen_ids(frame_index=0) - self.coord_wall = LammpsDumpWallParser( - self.filename, liquid_particle_types=list(self.liquid_particle_types) - ) - self.wall_coords = self.coord_wall.parse(frame_index=0) - self.parser = LammpsDumpParser(filepath=self.filename) - self.plotter = DropletSlicePlotlyPlotter(center=True) - - def generate_animation( - self, output_filename: str = "ContactAngle_Median_PerFrame_Slider.html" - ) -> None: - """Build and write HTML with slider of median contact angles over frames. - Parameters - ---------- - output_filename : str, default "ContactAngle_Median_PerFrame_Slider.html" - Output HTML file path. - Returns - ------- - None - Writes HTML file and prints path. - """ - fig = go.Figure() - frames_list = [] - frame_labels = [] - median_angles = [] - for frame_idx in range(self.n_frames): - oxygen_position = self.parser.parse( - frame_index=frame_idx, indices=self.oxygen_indices - ) - if self.precentered: - liquid_geom_center = np.mean(oxygen_position, axis=0) - else: - box_size_xy = ( - self.parser.box_size_x(frame_index=frame_idx), - self.parser.box_size_y(frame_index=frame_idx), - ) - oxygen_position, liquid_geom_center = recenter_droplet_pbc( - oxygen_position, self.droplet_geometry, box_size=box_size_xy - ) - processor = ContactAngleSlicing( - liquid_coordinates=oxygen_position, - liquid_geom_center=liquid_geom_center, - droplet_geometry=self.droplet_geometry, - delta_cylinder=self.delta_cylinder, - max_dist=self.max_dist, - width_cylinder=self.width_cylinder, - ) - angles, surfaces, popt_arrays = processor.predict_contact_angle() - median_idx = np.argsort(angles)[len(angles) // 2] - alpha = angles[median_idx] - popt = popt_arrays[median_idx] - surface = [surfaces[median_idx]] - median_angles.append(alpha) - fig_frame = self.plotter.plot_surface_points( - oxygen_position=oxygen_position, - surface_data=surface, - popt=popt, - wall_coords=self.wall_coords.copy(), - y_com=np.mean(oxygen_position[:, 1]), - pbc_y=None, - alpha=alpha, - show_water=True, - show_surface=True, - show_circle=True, - show_tangent=True, - show_wall=True, - ) - frame = go.Frame( - data=fig_frame.data, - name=f"Frame {frame_idx}", - layout=go.Layout( - title_text=( - f"Frame {frame_idx} | Median contact angle = {alpha:.2f}\u00b0" - ) - ), - ) - frames_list.append(frame) - frame_labels.append(f"Frame {frame_idx}") - fig.frames = frames_list - fig.add_traces(frames_list[0].data) - fig.update_layout( - title=("Interactive Contact Angle Evolution (Median Slice per Frame)"), - width=800, - height=600, - margin=dict(l=80, r=200, t=80, b=100), - xaxis_title="x (\u00c5)", - yaxis_title="z (\u00c5)", - template="simple_white", - showlegend=True, - legend=dict( - x=1.05, - y=0.95, - bgcolor="rgba(255,255,255,0.8)", - bordercolor="lightgray", - borderwidth=1, - font=dict(size=11), - ), - xaxis=dict( - mirror=True, - showline=True, - linecolor="black", - ticks="outside", - showgrid=True, - gridcolor="lightgray", - zeroline=False, - ), - yaxis=dict( - mirror=True, - showline=True, - linecolor="black", - ticks="outside", - showgrid=True, - gridcolor="lightgray", - zeroline=False, - scaleanchor="x", - scaleratio=1, - ), - sliders=[ - { - "active": 0, - "pad": {"b": 60, "t": 40}, - "x": 0.2, - "len": 0.6, - "y": -0.1, - "yanchor": "top", - "steps": [ - { - "args": [ - [f"Frame {k}"], - { - "frame": {"duration": 0, "redraw": True}, - "mode": "immediate", - }, - ], - "label": f"{k}", - "method": "animate", - } - for k in range(len(frames_list)) - ], - } - ], - ) - fig.write_html(output_filename) - print(f"Interactive HTML saved: {output_filename}") - - -# Example usage -# if __name__ == "__main__": -# animator = ContactAngleAnimator( -# filename="../wetting_angle_kit/tests/trajectories/" -# "traj_10_3_330w_nve_4k_reajust.lammpstrj", -# particle_type_wall={3}, -# oxygen_type=1, -# hydrogen_type=2, -# liquid_particle_types={2, 1}, -# n_frames=10, -# ) -# animator.generate_animation() diff --git a/src/wetting_angle_kit/visualization/method_comparison.py b/src/wetting_angle_kit/visualization/method_comparison.py deleted file mode 100644 index 5ef0cdb..0000000 --- a/src/wetting_angle_kit/visualization/method_comparison.py +++ /dev/null @@ -1,314 +0,0 @@ -import os -from typing import Any - -import matplotlib.pyplot as plt -import numpy as np - - -class MethodComparison: - """Utility to compare contact angle statistics - from multiple trajectory analyzers.""" - - def __init__( - self, analyzers: list[Any], method_names: list[str] | None = None - ) -> None: - """ - Parameters - ---------- - analyzers : list - Analyzer instances exposing ``directories`` and required API methods. - method_names : list[str], optional - Custom display names. If None, uses each analyzer's ``get_method_name``. - """ - self.analyzers = analyzers - self.method_names = method_names or [a.get_method_name() for a in analyzers] - for analyzer in self.analyzers: - if not hasattr(analyzer, "data") or not analyzer.data: - analyzer.load_data() - - def _check_and_run_analysis(self, analyzer: Any) -> None: - """Run analyzer if expected output file is absent for any directory. - Parameters - ---------- - analyzer : BaseTrajectoryAnalyzer - Analyzer instance whose output will be checked. - """ - for directory in analyzer.directories: - output_file = f"{directory}/output_stats.txt" - if not os.path.exists(output_file): - raise FileNotFoundError( - f"No analysis found for {directory}. Please run the analysis first." - ) - - def _read_analysis_output( - self, analyzer: Any, directory: str - ) -> tuple[float, float]: - """Return mean surface area and angle parsed from stats file. - Parameters - ---------- - analyzer : BaseTrajectoryAnalyzer - Analyzer owning the directory. - directory : str - Path containing ``output_stats.txt``. - Returns - ------- - tuple(float, float) - (mean_surface_area, mean_contact_angle). - """ - output_file = f"{directory}/output_stats.txt" - with open(output_file, encoding="utf-8") as f: - lines = f.readlines() - mean_surface_area = float(lines[2].split(": ")[1].strip()) - mean_contact_angle = float(lines[3].split(": ")[1].strip().replace("°", "")) - return mean_surface_area, mean_contact_angle - - def plot_side_by_side_comparison( - self, - save_path: str | None = None, - figsize: tuple[int, int] = (14, 5), - color: str = "purple", - ) -> None: - """ - Produce side-by-side comparison of mean contact angle vs. surface area scaling. - Inspired by plot_mean_angle_vs_surface(). - """ - plt.rcParams.update( - { - "font.family": "serif", - "font.size": 13, - "axes.labelsize": 14, - "axes.titlesize": 15, - "legend.fontsize": 11, - "xtick.direction": "in", - "ytick.direction": "in", - "axes.linewidth": 1.0, - "errorbar.capsize": 3, - } - ) - fig, axes = plt.subplots(1, len(self.analyzers), figsize=figsize) - if len(self.analyzers) == 1: - axes = [axes] - - for ax, analyzer, method_name in zip( - axes, self.analyzers, self.method_names, strict=False - ): - # gather one point per directory - xvals, yvals = [], [] - for directory in analyzer.directories: - mean_sa, mean_angle = self._read_analysis_output(analyzer, directory) - - x = 1.0 / np.sqrt(mean_sa) # same as example - y = mean_angle - - ax.errorbar(x, y, yerr=0.5, fmt="o", color=color) - ax.annotate( - analyzer.get_clean_label(directory), - xy=(x, y), - xytext=(4, 4), - textcoords="offset points", - fontsize=7, - ) - - xvals.append(x) - yvals.append(y) - - # linear fit if we have ≥2 points - xvals_arr, yvals_arr = np.array(xvals), np.array(yvals) - if len(xvals_arr) >= 2: - coeffs = np.polyfit(xvals_arr, yvals_arr, 1) - fit_line = np.poly1d(coeffs) - x_fit = np.linspace(0, xvals_arr.max() * 1.1, 100) - ax.plot( - x_fit, - fit_line(x_fit), - "--", - color="gray", - label=f"Fit: y={coeffs[0]:.2f}x+{coeffs[1]:.2f}", - ) - - ax.set_xlabel(r"$1 / \sqrt{\text{Surface Area}}$") - ax.set_ylabel("Mean Angle (°)") - ax.set_title(method_name) - ax.legend(frameon=False) - ax.set_xlim(left=-0.001) - - if yvals_arr.size > 0: - ax.set_ylim(min(yvals_arr) - 2, max(yvals_arr) + 2) - - plt.tight_layout() - if save_path: - plt.savefig(save_path, dpi=400, bbox_inches="tight") - plt.close() - - def plot_overlay_comparison( - self, - save_path: str | None = None, - figsize: tuple[int, int] = (7, 5), - colors: list[str] | None = None, - point_labels: list[list[str]] | None = None, - ) -> None: - """Overlay Modified Young's equation plot across all analyzers. - - Plots ``cos(θ)`` vs ``1/√A`` with linear fits and extrapolated - ``θ∞`` (contact angle at infinite surface area) for each analyzer. - - Parameters - ---------- - save_path : str, optional - Path to save the figure. - figsize : tuple[int, int], default (7, 5) - Figure size in inches. - colors : list[str], optional - One color per analyzer. If None a default palette is used. - point_labels : list[list[str]] or None, optional - Custom labels for each data point. Outer list corresponds to - analyzers, inner list to directories (same order as - ``analyzer.directories``). If None, directory names are used. - """ - if colors is None: - colors = ["#0A9396", "#bb3e03", "#9b5de5", "#f15bb5", "#00bbf9"] - - plt.rcParams.update( - { - "font.family": "serif", - "font.size": 13, - "axes.labelsize": 14, - "axes.titlesize": 15, - "legend.fontsize": 11, - "xtick.direction": "in", - "ytick.direction": "in", - "axes.linewidth": 1.0, - "errorbar.capsize": 3, - } - ) - - fig, ax = plt.subplots(figsize=figsize) - - for idx, (analyzer, method_name) in enumerate( - zip(self.analyzers, self.method_names, strict=False) - ): - color = colors[idx % len(colors)] - xvals, yvals = [], [] - - for dir_idx, directory in enumerate(analyzer.directories): - mean_sa, mean_angle = self._read_analysis_output(analyzer, directory) - # Read std from output_stats.txt (line 4) - output_file = f"{directory}/output_stats.txt" - with open(output_file, encoding="utf-8") as f: - lines = f.readlines() - std_angle = float(lines[4].split(": ")[1].strip().replace("°", "")) - - x = 1.0 / np.sqrt(mean_sa) - y = np.cos(np.radians(mean_angle)) - - # Error propagation: d(cos θ) = |sin θ| · dθ - yerr = ( - np.abs(np.sin(np.radians(mean_angle))) * np.radians(std_angle) / 5 - ) - - ax.errorbar( - x, - y, - yerr=yerr, - fmt="o", - color=color, - markersize=6, - capsize=3, - lw=1.2, - ) - - # Annotation - if point_labels is not None: - text = point_labels[idx][dir_idx] - else: - text = analyzer.get_clean_label(directory) - ax.annotate( - text, - xy=(x, y), - xytext=(5, 5), - textcoords="offset points", - fontsize=6, - color="black", - ) - - xvals.append(x) - yvals.append(y) - - # Linear fit with θ∞ extrapolation - if len(xvals) >= 2: - xarr, yarr = np.array(xvals), np.array(yvals) - coeffs = np.polyfit(xarr, yarr, 1) - fit_line = np.poly1d(coeffs) - intercept = np.clip(coeffs[1], -1.0, 1.0) - theta_inf = np.degrees(np.arccos(intercept)) - - x_fit = np.linspace(0, xarr.max() * 1.1, 100) - ax.plot( - x_fit, - fit_line(x_fit), - "--", - color=color, - lw=1.5, - label=( - f"{method_name}: " - rf"$\theta_{{\infty}} = {theta_inf:.1f}^\circ$" - ), - ) - - ax.set_xlabel(r"$1 / \sqrt{A} \; (\mathrm{\AA^{-1}})$") - ax.set_ylabel(r"$\cos(\theta)$") - ax.set_title("Modified Young's Eq – Comparison") - ax.legend(frameon=False, loc="best") - ax.set_xlim(left=-0.001) - plt.tight_layout() - if save_path: - plt.savefig(save_path, dpi=400, bbox_inches="tight") - plt.close() - - def compare_statistics(self) -> str: - """Build per-directory and overall mean/std statistics for each method. - - Returns - ------- - str - The full formatted report. The caller is responsible for displaying - it (e.g. ``print(comparator.compare_statistics())``). Returning a - string instead of printing makes the output capturable from - notebooks and tests. - """ - lines: list[str] = [] - lines.append("=" * 70) - lines.append("METHOD COMPARISON STATISTICS") - lines.append("=" * 70) - for method_name, analyzer in zip( - self.method_names, self.analyzers, strict=False - ): - lines.append(f"\n{method_name}:") - lines.append("-" * 70) - all_angles = [] - all_surfaces = [] - for directory in analyzer.directories: - try: - mean_surface_area, mean_contact_angle = self._read_analysis_output( - analyzer, directory - ) - angles = analyzer.get_contact_angles(directory) - surfaces = analyzer.get_surface_areas(directory) - except FileNotFoundError: - angles = analyzer.get_contact_angles(directory) - surfaces = analyzer.get_surface_areas(directory) - mean_surface_area = float(np.mean(surfaces)) - mean_contact_angle = float(np.mean(angles)) - all_angles.extend(angles) - all_surfaces.extend(surfaces) - lines.append(f" {analyzer.get_clean_label(directory)}:") - lines.append(f" Mean Surface Area: {mean_surface_area:.4f}") - lines.append(f" Mean Angle: {mean_contact_angle:.4f}°") - if all_angles: - lines.append("\n Overall Statistics:") - lines.append(f" Total samples: {len(all_angles)}") - lines.append(f" Mean Surface Area: {np.mean(all_surfaces):.4f}") - lines.append(f" Mean Angle: {np.mean(all_angles):.4f}°") - lines.append(f" Std Angle: {np.std(all_angles):.4f}°") - lines.append("\n" + "=" * 70) - return "\n".join(lines) diff --git a/src/wetting_angle_kit/visualization/slicing_trajectory_analyzer.py b/src/wetting_angle_kit/visualization/slicing_trajectory_analyzer.py deleted file mode 100644 index d427296..0000000 --- a/src/wetting_angle_kit/visualization/slicing_trajectory_analyzer.py +++ /dev/null @@ -1,234 +0,0 @@ -import os - -import matplotlib.pyplot as plt -import numpy as np - -from wetting_angle_kit.visualization.base_trajectory_analyzer import ( - BaseTrajectoryAnalyzer, -) - - -class SlicingTrajectoryAnalyzer(BaseTrajectoryAnalyzer): - """BaseTrajectoryAnalyzer implementation for the slicing contact angle method.""" - - def __init__( - self, - directories: list[str], - time_steps: dict[str, float] | None = None, - time_unit: str = "ps", - ) -> None: - """ - Initialize the analyzer with a list of directory paths. - - Parameters - ---------- - directories : list of str - List of directory paths containing analysis results. - time_steps : dict, optional - Dictionary mapping directory to its time step. - If None, defaults to 1.0 for all directories. - time_unit : str, optional - Time unit for the x-axis (e.g., "ps", "ns", "fs"). - """ - self.time_steps = time_steps if time_steps else {d: 1.0 for d in directories} - self.time_unit = time_unit - super().__init__(directories, time_unit=time_unit) - - def _initialize_data_structure(self) -> None: - """Initialize data structure for slicing analysis.""" - for directory in self.directories: - self.data[directory] = { - "surfaces_files": [], - "popts_files": [], - "angles_files": [], - "mean_surface_areas": [], - "all_angles": [], - "median_angles": [], - "mean_angles": [], - "std_angles": [], - "time_step": self.time_steps.get(directory, 1.0), - } - - def get_method_name(self) -> str: - """Return method name.""" - return "Slicing Analysis" - - def load_data(self) -> None: - """Load combined .npy files (angles, surfaces, popts) - from all directories and compute mean surface areas per frame.""" - for directory in self.directories: - all_angles = np.load( - os.path.join(directory, "all_angles.npy"), allow_pickle=True - ) - all_surfaces = np.load( - os.path.join(directory, "all_surfaces.npy"), allow_pickle=True - ) - all_popts = np.load( - os.path.join(directory, "all_popts.npy"), allow_pickle=True - ) - - # Calculate mean surface area for each frame - mean_surface_areas = [] - for frame_data in all_surfaces: - surfaces = frame_data[1] - all_surf = [ - self.calculate_polygon_area(surface) for surface in surfaces - ] - mean_area = np.mean(np.array(all_surf)) - mean_surface_areas.append(mean_area) - - self.data[directory] = { - "all_angles": all_angles, - "all_surfaces": all_surfaces, - "all_popts": all_popts, - "frame_numbers": [item[0] for item in all_angles], - "mean_surface_areas": mean_surface_areas, - "median_angles": [(item[0], np.median(item[1])) for item in all_angles], - "mean_angles": [(item[0], np.mean(item[1])) for item in all_angles], - "std_angles": [(item[0], np.std(item[1])) for item in all_angles], - "time_step": self.time_steps.get(directory, 1.0), - } - - @staticmethod - def calculate_polygon_area(points: np.ndarray) -> float: - """ - Calculate the area of a polygon given its vertices using the Shoelace formula. - - Parameters - ---------- - points : numpy.ndarray - Array of shape (N, 2) containing polygon vertices. - - Returns - ------- - float - Area of the polygon. - """ - x = points[:, 0] - y = points[:, 1] - area = 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) - return area - - def mean_surface_frame(self, surfaces: list[np.ndarray]) -> float: - """Return the mean polygon area across all surfaces in a frame. - - Parameters - ---------- - surfaces : list[ndarray] - List of surface polygon vertex arrays, each of shape (N, 2). - - Returns - ------- - float - Mean surface area across all surfaces in the frame. - """ - all_surf = [self.calculate_polygon_area(surface) for surface in surfaces] - return np.mean(np.array(all_surf)) - - def get_surface_areas(self, directory: str) -> np.ndarray: - """Get surface areas for a directory.""" - return np.array(self.data[directory]["mean_surface_areas"]) - - def get_contact_angles(self, directory: str) -> np.ndarray: - """Get contact angles (median angles) for a directory.""" - data = np.array(self.data[directory]["median_angles"]) - if data.ndim == 2 and data.shape[1] >= 2: - return data[:, 1] - return data - - def plot_median_angles_evolution( - self, - save_path: str, - labels: dict[str, str] | None = None, - plot_std: bool = True, - ) -> None: - """Plot evolution of median contact angle with standard deviation. - - Align trajectories by truncating to shortest. - """ - if not self.data[self.directories[0]]["median_angles"]: - self.load_data() - - plot_labels = ( - labels if labels else {d: os.path.basename(d) for d in self.directories} - ) - - plt.figure(figsize=(10, 6)) - colors = plt.cm.tab20(np.linspace(0, 1, len(self.directories))) - - for i, directory in enumerate(self.directories): - median_angles = self.data[directory]["median_angles"] - std_angles = self.data[directory]["std_angles"] - frame_numbers = [item[0] for item in median_angles] - median_values = [item[1] for item in median_angles] - std_values = [item[1] for item in std_angles] - time_step = self.data[directory]["time_step"] - time_values = np.array(frame_numbers) * time_step - label = plot_labels.get(directory, os.path.basename(directory)) - - plt.plot( - time_values, - median_values, - linestyle="-", - color=colors[i], - label=f"{label}", - ) - if plot_std: - plt.fill_between( - time_values, - np.array(median_values) - np.array(std_values), - np.array(median_values) + np.array(std_values), - color=colors[i], - alpha=0.2, - ) - - plt.title("Evolution of the Median Angle") - plt.xlabel(f"Time ({self.time_unit})") - plt.ylabel("Angle (°)") - plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") - plt.grid(False) - plt.tight_layout() - plt.savefig(save_path, dpi=400, bbox_inches="tight") - plt.close() - - def plot_mean_angles_evolution( - self, - save_path: str, - labels: dict[str, str] | None = None, - ) -> None: - """Plot evolution of mean contact angle with standard deviation.""" - plot_labels = ( - labels if labels else {d: os.path.basename(d) for d in self.directories} - ) - plt.figure(figsize=(10, 6)) - colors = plt.cm.tab20(np.linspace(0, 1, len(self.directories))) - - for i, directory in enumerate(self.directories): - mean_angles = self.data[directory]["mean_angles"] - std_angles = self.data[directory]["std_angles"] - frame_numbers = [item[0] for item in mean_angles] - mean_values = [item[1] for item in mean_angles] - std_values = [item[1] for item in std_angles] - time_step = self.data[directory]["time_step"] - time_values = np.array(frame_numbers) * time_step - label = plot_labels.get(directory, os.path.basename(directory)) - - plt.plot( - time_values, mean_values, linestyle="-", color=colors[i], label=label - ) - plt.fill_between( - time_values, - np.array(mean_values) - np.array(std_values), - np.array(mean_values) + np.array(std_values), - color=colors[i], - alpha=0.2, - ) - - plt.title("Evolution of the Mean Angle with Standard Deviation") - plt.xlabel(f"Time ({self.time_unit})") - plt.ylabel("Angle (°)") - plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") - plt.grid(False) - plt.tight_layout() - plt.savefig(save_path, dpi=400, bbox_inches="tight") - plt.close() diff --git a/src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py b/src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py new file mode 100644 index 0000000..eb44725 --- /dev/null +++ b/src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py @@ -0,0 +1,147 @@ +from collections.abc import Iterable + +import numpy as np +import plotly.colors as pc +import plotly.graph_objects as go + +from wetting_angle_kit.analysis.slicing.results import SlicingResults +from wetting_angle_kit.visualization.base_trajectory_plotter import ( + BaseTrajectoryPlotter, +) +from wetting_angle_kit.visualization.stats import TrajectoryStats + + +def _shoelace_area(points: np.ndarray) -> float: + """Polygon area via the shoelace formula.""" + x = points[:, 0] + y = points[:, 1] + return float(0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))) + + +def _hex_to_rgba(hex_color: str, alpha: float) -> str: + """Return a CSS ``rgba(...)`` string from a ``#rrggbb`` hex color.""" + h = hex_color.lstrip("#") + r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) + return f"rgba({r},{g},{b},{alpha})" + + +class SlicingTrajectoryPlotter(BaseTrajectoryPlotter): + """Plot statistics derived from one or more :class:`SlicingResults`.""" + + def __init__( + self, + results: SlicingResults | Iterable[SlicingResults], + labels: list[str] | None = None, + time_steps: list[float] | None = None, + time_unit: str = "ps", + ) -> None: + """ + Parameters + ---------- + results : SlicingResults or iterable of SlicingResults + One results container per trajectory. + labels : list of str, optional + Display labels (one per results container). Defaults to + ``["trajectory_0", ...]``. + time_steps : list of float, optional + Per-trajectory time step applied to ``frames`` for the time + axis of evolution plots. Defaults to ``1.0`` for each. + time_unit : str, optional + Time unit shown on x-axis labels. + """ + if isinstance(results, SlicingResults): + results = [results] + else: + results = list(results) + self.results = results + self.labels = labels or [f"trajectory_{i}" for i in range(len(results))] + self.time_steps = time_steps or [1.0] * len(results) + self.time_unit = time_unit + + def _mean_surface_areas(self, result: SlicingResults) -> np.ndarray: + """Per-frame mean polygon area (shoelace over the frame's slices).""" + return np.array( + [ + float(np.mean([_shoelace_area(s) for s in frame_surfaces])) + for frame_surfaces in result.surfaces + ] + ) + + def summary(self) -> list[TrajectoryStats]: + stats: list[TrajectoryStats] = [] + for label, result in zip(self.labels, self.results, strict=False): + surfaces = self._mean_surface_areas(result) + stats.append( + TrajectoryStats( + method_name="Slicing Analysis", + label=label, + mean_surface_area=float(np.mean(surfaces)), + mean_contact_angle=result.mean_angle, + std_contact_angle=result.std_angle, + n_samples=len(result), + ) + ) + return stats + + def plot_angle_evolution( + self, + stat: str = "median", + save_path: str | None = None, + ) -> go.Figure: + """Plot per-frame contact angle as a function of time. + + Parameters + ---------- + stat : str, default "median" + Per-frame aggregation across slices; one of ``"median"`` or ``"mean"``. + save_path : str, optional + If provided, write the figure as standalone HTML. + + Returns + ------- + plotly.graph_objects.Figure + Figure with one line (and ±σ band across slices) per trajectory. + """ + if stat not in ("median", "mean"): + raise ValueError(f"stat must be 'median' or 'mean', got {stat!r}") + agg = np.median if stat == "median" else np.mean + palette = pc.qualitative.Plotly + fig = go.Figure() + for idx, (label, result, dt) in enumerate( + zip(self.labels, self.results, self.time_steps, strict=False) + ): + color = palette[idx % len(palette)] + band_color = _hex_to_rgba(color, 0.2) + times = np.array(result.frames) * dt + central = np.array([float(agg(a)) for a in result.angles]) + std = np.array([float(np.std(a)) for a in result.angles]) + fig.add_trace( + go.Scatter( + x=times, + y=central, + mode="lines", + name=label, + line=dict(width=2, color=color), + ) + ) + fig.add_trace( + go.Scatter( + x=np.concatenate([times, times[::-1]]), + y=np.concatenate([central + std, (central - std)[::-1]]), + fill="toself", + fillcolor=band_color, + line=dict(width=0), + name=f"{label} ±σ", + showlegend=False, + hoverinfo="skip", + ) + ) + fig.update_layout( + title=f"Contact angle evolution ({stat})", + xaxis_title=f"Time ({self.time_unit})", + yaxis_title="Contact angle (°)", + template="plotly_white", + ) + if save_path: + fig.write_html(save_path) + return fig diff --git a/src/wetting_angle_kit/visualization/stats.py b/src/wetting_angle_kit/visualization/stats.py new file mode 100644 index 0000000..549f539 --- /dev/null +++ b/src/wetting_angle_kit/visualization/stats.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass + + +@dataclass +class TrajectoryStats: + """Summary statistics for a single contact-angle trajectory. + + Replaces the legacy ``output_stats.txt`` file: instead of writing to + disk, the plotter returns this dataclass so callers can both display + the block (``print(stats)``) and reuse the underlying numbers + programmatically. + + Attributes + ---------- + method_name : str + Name of the analysis method (e.g. ``"Slicing Analysis"``). + label : str + Display label identifying the trajectory. + mean_surface_area : float + Mean droplet/cap surface area in Ų. + mean_contact_angle : float + Mean contact angle in degrees. + std_contact_angle : float + Standard deviation of the contact angle in degrees. + n_samples : int + Number of samples (frames or batches) contributing to the means. + """ + + method_name: str + label: str + mean_surface_area: float + mean_contact_angle: float + std_contact_angle: float + n_samples: int + + def __str__(self) -> str: + return ( + f"Label: {self.label}\n" + f"Method: {self.method_name}\n" + f"Mean Surface Area: {self.mean_surface_area:.4f}\n" + f"Mean Contact Angle: {self.mean_contact_angle:.4f}°\n" + f"Std Contact Angle: {self.std_contact_angle:.4f}°\n" + f"N samples: {self.n_samples}" + ) diff --git a/src/wetting_angle_kit/visualization/surface_plots.py b/src/wetting_angle_kit/visualization/surface_plots.py deleted file mode 100644 index 15844c5..0000000 --- a/src/wetting_angle_kit/visualization/surface_plots.py +++ /dev/null @@ -1,143 +0,0 @@ -from typing import Any - -import matplotlib.pyplot as plt -import numpy as np - - -def plot_surface_file(file_path: str) -> tuple[np.ndarray, np.ndarray]: - """Return x,y columns from surface text file. - - Parameters - ---------- - file_path : str - Path to whitespace-delimited file with at least two columns. - - Returns - ------- - tuple(ndarray, ndarray) - (x, y) coordinate arrays. - """ - data = np.loadtxt(file_path) - x = data[:, 0] - y = data[:, 1] - return x, y - - -def plot_slice(x: np.ndarray, y: np.ndarray) -> None: - """Plot a 2D surface contour line from x and y coordinate arrays.""" - plt.figure() - plt.plot(x, y, label="Surface Slice") - plt.xlabel("X-axis") - plt.ylabel("Y-axis") - plt.title("2D Slice of Fitted Surface") - plt.legend() - plt.grid() - plt.show() - - -def read_surface_file(file_path: str) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """Load surface file returning x,y,z arrays (z zeros if absent). - - Parameters - ---------- - file_path : str - Path to surface file with 2 or 3 columns. - - Returns - ------- - tuple(ndarray, ndarray, ndarray) - (x, y, z) arrays; z is zeros if file has only two columns. - """ - data = np.loadtxt(file_path) - if data.shape[1] == 2: - x, y = data[:, 0], data[:, 1] - z = np.zeros_like(x) - else: - x, y, z = data[:, 0], data[:, 1], data[:, 2] - return x, y, z - - -def plot_surface_and_points( - x_surf: np.ndarray, - y_surf: np.ndarray, - z_surf: np.ndarray, - x_points: np.ndarray, - y_points: np.ndarray, - z_points: np.ndarray, -) -> None: - """Render 3D plot of surface curve and point cloud. - - Parameters - ---------- - x_surf, y_surf, z_surf : ndarray - Surface coordinates. - x_points, y_points, z_points : ndarray - Point cloud coordinates. - """ - fig = plt.figure() - ax = fig.add_subplot(111, projection="3d") - ax.plot(x_surf, y_surf, z_surf, label="Surface", color="black") - ax.scatter( - x_points, y_points, z_points, s=10, alpha=0.7, label="Points", color="tab:blue" - ) - ax.set_xlabel("X") - ax.set_ylabel("Y") - ax.set_zlabel("Z") - ax.legend() - plt.tight_layout() - plt.show() - - -def visualize_surface_with_points(surface_file: str, points: np.ndarray) -> None: - """Convenience wrapper: load surface and overlay points. - - Parameters - ---------- - surface_file : str - Path to surface file. - points : ndarray, shape (N, 3) - XYZ coordinates of points to overlay. - """ - x_surf, y_surf, z_surf = read_surface_file(surface_file) - x_points, y_points, z_points = points[:, 0], points[:, 1], points[:, 2] - plot_surface_and_points(x_surf, y_surf, z_surf, x_points, y_points, z_points) - - -def plot_liquid_particles( - positions: np.ndarray, - ax: Any | None = None, - color: str = "tab:blue", - subsample: int | None = None, -) -> Any: - """Scatter plot 3D particle positions with optional subsampling. - - Parameters - ---------- - positions : ndarray, shape (N, 3) - Particle coordinates. - ax : mpl_toolkits.mplot3d.Axes3D, optional - Existing axes to plot on; new figure created if None. - color : str, default "tab:blue" - Marker color. - subsample : int, optional - If provided and smaller than N, random subset size to plot. - - Returns - ------- - mpl_toolkits.mplot3d.Axes3D - Axes object used for plotting. - """ - if subsample is not None and subsample < len(positions): - rng = np.random.default_rng(42) - idx = rng.choice(len(positions), size=subsample, replace=False) - positions = positions[idx] - if ax is None: - fig = plt.figure() - ax = fig.add_subplot(111, projection="3d") - ax.scatter( - positions[:, 0], positions[:, 1], positions[:, 2], s=8, alpha=0.8, color=color - ) - ax.set_xlabel("X") - ax.set_ylabel("Y") - ax.set_zlabel("Z") - return ax diff --git a/tests/test_analysis/test_binning_method.py b/tests/test_analysis/test_binning_method.py index 1665c2f..d915055 100644 --- a/tests/test_analysis/test_binning_method.py +++ b/tests/test_analysis/test_binning_method.py @@ -50,63 +50,48 @@ def binning_params(): # --- Unit Test for BinningContactAngleAnalyzer --- @pytest.mark.integration def test_binning_contact_angle_analyzer_with_real_data( - filename, oxygen_indices, binning_params, tmp_path + filename, oxygen_indices, binning_params ): - # Use a temporary directory for output - output_dir = tmp_path / "result_dump_cylinder_noplot" - - # Create the analyzer analyzer = contact_angle_analyzer( method="binning", parser=LammpsDumpParser(filename), - output_dir=output_dir, atom_indices=oxygen_indices, droplet_geometry="cylinder_y", width_cylinder=21, binning_params=binning_params, - plot_graphs=False, ) - # Run analysis for frame 1 results = analyzer.analyze([1]) - # Assert results - assert "mean_angle" in results - assert "std_angle" in results - assert "angles" in results - assert len(results["angles"]) == 1 + assert len(results) == 1 # Cylindrical droplet on a graphene-like surface gives a contact angle # around 90-100° here. Use a moderate band so the test catches gross # regressions but tolerates the inherent noise of a single-frame fit. - assert 80.0 <= results["mean_angle"] <= 115.0 - assert np.isfinite(results["std_angle"]) + assert 80.0 <= results.mean_angle <= 115.0 + assert np.isfinite(results.std_angle) # --- Multi-batch test: with split_factor=1 each frame produces its own # angle, so we should get one angle per frame, not a single collapsed value. @pytest.mark.integration def test_binning_contact_angle_analyzer_per_frame_with_split_factor( - filename, oxygen_indices, binning_params, tmp_path + filename, oxygen_indices, binning_params ): - output_dir = tmp_path / "result_dump_per_frame" - analyzer = contact_angle_analyzer( method="binning", parser=LammpsDumpParser(filename), - output_dir=output_dir, atom_indices=oxygen_indices, droplet_geometry="cylinder_y", width_cylinder=21, binning_params=binning_params, - plot_graphs=False, ) # split_factor=1 → one batch per frame → 3 batch-level angles. results = analyzer.analyze([1, 2, 3], split_factor=1) - assert results["method_metadata"] == {"frames_per_trajectory": 1} - assert results["angles"].shape == (3,) + assert results.method_metadata == {"frames_per_trajectory": 1} + assert results.angles_per_batch.shape == (3,) # Each batch can either converge to a physically-plausible angle in # [0, 180] or return NaN (signaling fit failure on a single frame). - for angle in results["angles"]: + for angle in results.angles_per_batch: assert np.isnan(angle) or (0.0 <= angle <= 180.0) diff --git a/tests/test_analysis/test_slicing_edge_cases.py b/tests/test_analysis/test_slicing_edge_cases.py index 17cb94b..a17ef22 100644 --- a/tests/test_analysis/test_slicing_edge_cases.py +++ b/tests/test_analysis/test_slicing_edge_cases.py @@ -1,5 +1,3 @@ -import os - import numpy as np import pytest @@ -105,13 +103,13 @@ def test_calculate_y_axis_spherical(): def test_create_batches_few_frames(tmp_path): - parallel = ContactAngleSlicingParallel(filename="ignored", output_dir=str(tmp_path)) + parallel = ContactAngleSlicingParallel(filename="ignored") # num_batches >= len(frames) → one frame per batch assert parallel._create_batches([1, 2, 3], num_batches=4) == [[1], [2], [3]] def test_create_batches_many_frames(tmp_path): - parallel = ContactAngleSlicingParallel(filename="ignored", output_dir=str(tmp_path)) + parallel = ContactAngleSlicingParallel(filename="ignored") batches = parallel._create_batches(list(range(10)), num_batches=3) flat = [f for batch in batches for f in batch] assert flat == list(range(10)) @@ -130,7 +128,6 @@ def test_process_batch_worker_invokes_pipeline_on_real_lammps(tmp_path): parallel = ContactAngleSlicingParallel( filename=trajectory_path("traj_spherical_drop_4k.lammpstrj"), - output_dir=str(tmp_path), droplet_geometry="spherical", delta_gamma=20.0, ) @@ -146,17 +143,9 @@ def test_process_batch_worker_unsupported_extension(tmp_path): fake.write_text("not a real trajectory\n") parallel = ContactAngleSlicingParallel( filename=str(fake), - output_dir=str(tmp_path), droplet_geometry="spherical", delta_gamma=20.0, ) results = parallel._process_batch_worker(batch_frames=[0, 1]) assert len(results) == 2 assert all(r.mean_angle is None for r in results) - - -def test_output_dir_is_created(tmp_path): - target = tmp_path / "nested" / "out" - parallel = ContactAngleSlicingParallel(filename="ignored", output_dir=str(target)) - assert os.path.isdir(target) - assert parallel.output_dir == str(target) diff --git a/tests/test_analysis/test_slicing_method.py b/tests/test_analysis/test_slicing_method.py index 573f934..cc28ae1 100644 --- a/tests/test_analysis/test_slicing_method.py +++ b/tests/test_analysis/test_slicing_method.py @@ -75,16 +75,10 @@ def test_contact_angle_slicing_with_real_data(parser, oxygen_indices): # --- Integration Test for SlicingContactAngleAnalyzer --- @pytest.mark.integration @pytest.mark.slow -def test_slicing_contact_angle_analyzer_with_real_data( - filename, oxygen_indices, tmp_path -): - # Use a temporary directory for output - output_dir = tmp_path / "result_dump_spherical_slicing" - +def test_slicing_contact_angle_analyzer_with_real_data(filename, oxygen_indices): analyzer = contact_angle_analyzer( method="slicing", parser=LammpsDumpParser(filename), - output_dir=output_dir, atom_indices=oxygen_indices, droplet_geometry="spherical", delta_gamma=20, @@ -92,14 +86,12 @@ def test_slicing_contact_angle_analyzer_with_real_data( results = analyzer.analyze([1]) - # Assert results - assert "mean_angle" in results - assert "std_angle" in results - assert "angles" in results - assert len(results["angles"]) == 1 + assert len(results) == 1 + assert results.frames == [1] # The fixture is a water droplet on a graphene-like substrate, which # gives a contact angle around 90-100° (literature: ~93° for graphene). # Assert a tight physically-plausible band so regressions in the # slicing pipeline are caught. - assert 80.0 <= results["mean_angle"] <= 110.0 - assert np.isfinite(results["std_angle"]) + mean_angle = float(np.mean(results.angles[0])) + assert 80.0 <= mean_angle <= 110.0 + assert np.isfinite(np.std(results.angles[0])) diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 90d7ebf..f362e47 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -120,21 +120,17 @@ def test_hyperbolic_tangent_compute_isoline_raises_for_unphysical_fit(): # --- Factory rejects unknown methods --- -def test_contact_angle_analyzer_factory_rejects_unknown_method(tmp_path): +def test_contact_angle_analyzer_factory_rejects_unknown_method(): from wetting_angle_kit.analysis import contact_angle_analyzer with pytest.raises(ValueError, match="Unknown method"): - contact_angle_analyzer( - method="not-a-method", - parser=object(), - output_dir=str(tmp_path), - ) + contact_angle_analyzer(method="not-a-method", parser=object()) # --- ContactAngleBinning.get_profile_coordinates --- -def _make_binning_analyzer(parser, tmp_path): +def _make_binning_analyzer(parser): from wetting_angle_kit.analysis.binning import ContactAngleBinning return ContactAngleBinning( @@ -149,12 +145,10 @@ def _make_binning_analyzer(parser, tmp_path): "zi_f": 10.0, "nbins_zi": 5, }, - output_dir=str(tmp_path), - plot_graphs=False, ) -def test_binning_get_profile_coordinates_empty_frame_list(tmp_path): +def test_binning_get_profile_coordinates_empty_frame_list(): """Empty frame_indices must return empty arrays and zero frames.""" from wetting_angle_kit.parsers.base import BaseParser @@ -165,14 +159,14 @@ def parse(self, frame_index, indices=None): def frame_count(self): return 0 - analyzer = _make_binning_analyzer(_StubParser(), tmp_path) + analyzer = _make_binning_analyzer(_StubParser()) r, z, n = analyzer.get_profile_coordinates(frame_indices=[]) assert r.shape == (0,) assert z.shape == (0,) assert n == 0 -def test_binning_get_profile_coordinates_concatenates_frames(tmp_path): +def test_binning_get_profile_coordinates_concatenates_frames(): """r and z arrays are concatenated across requested frames; z stays in lab frame.""" from wetting_angle_kit.parsers.base import BaseParser @@ -186,7 +180,7 @@ def parse(self, frame_index, indices=None): def frame_count(self): return 2 - analyzer = _make_binning_analyzer(_StubParser(), tmp_path) + analyzer = _make_binning_analyzer(_StubParser()) r, z, n = analyzer.get_profile_coordinates(frame_indices=[0, 1]) assert n == 2 # Spherical r is non-negative and the per-frame center-of-mass projection @@ -196,7 +190,7 @@ def frame_count(self): np.testing.assert_array_equal(z, np.array([5.0, 6.0, 7.0, 8.0, 9.0, 10.0])) -def test_binning_warns_and_falls_back_when_parser_has_no_box(tmp_path): +def test_binning_warns_and_falls_back_when_parser_has_no_box(): """Parsers that don't expose box_size_x/y (plain XYZ without a Lattice= line, custom stubs) must trigger the fallback warning and still produce results via the legacy arithmetic-mean centering.""" @@ -213,7 +207,7 @@ def frame_count(self): # box_size_x / box_size_y inherited from BaseParser raise NotImplementedError. - analyzer = _make_binning_analyzer(_StubParser(), tmp_path) + analyzer = _make_binning_analyzer(_StubParser()) with pytest.warns(UserWarning, match="does not expose lateral box sizes"): r, z, n = analyzer.get_profile_coordinates(frame_indices=[0]) assert n == 1 @@ -221,7 +215,7 @@ def frame_count(self): np.testing.assert_array_equal(z, np.array([5.0, 6.0, 7.0])) -def test_binning_precentered_skips_box_probe_and_warning(tmp_path): +def test_binning_precentered_skips_box_probe_and_warning(): """precentered=True must bypass the box probe entirely so a parser that lacks box_size_x/y is accepted silently, no warning is issued, and the result matches the legacy arithmetic-mean path.""" @@ -251,8 +245,6 @@ def frame_count(self): "zi_f": 10.0, "nbins_zi": 5, }, - output_dir=str(tmp_path), - plot_graphs=False, precentered=True, ) with warnings.catch_warnings(): @@ -262,7 +254,7 @@ def frame_count(self): np.testing.assert_allclose(r, np.array([1.0, 1.0, 0.0])) -def test_binning_no_warning_when_parser_exposes_box(tmp_path): +def test_binning_no_warning_when_parser_exposes_box(): """The fallback warning must NOT fire when the parser exposes box info; otherwise it would spam every real run.""" import warnings @@ -284,7 +276,7 @@ def box_size_x(self, frame_index): def box_size_y(self, frame_index): return 100.0 - analyzer = _make_binning_analyzer(_StubParserWithBox(), tmp_path) + analyzer = _make_binning_analyzer(_StubParserWithBox()) with warnings.catch_warnings(): warnings.simplefilter("error", UserWarning) analyzer.get_profile_coordinates(frame_indices=[0]) diff --git a/tests/test_visualization/test_droplet_slice_plot.py b/tests/test_visualization/test_droplet_slice_plot.py new file mode 100644 index 0000000..a860e1a --- /dev/null +++ b/tests/test_visualization/test_droplet_slice_plot.py @@ -0,0 +1,142 @@ +"""Smoke tests for the plotly droplet-slice plotter and the animator.""" + +import numpy as np +import plotly.graph_objects as go +import pytest + +from tests.conftest import trajectory_path +from wetting_angle_kit.visualization import DropletSlicePlotter +from wetting_angle_kit.visualization.animator import ContactAngleAnimator + + +def _synthetic_droplet(seed=0): + rng = np.random.default_rng(seed) + theta = rng.uniform(0, np.pi, 400) + r = rng.uniform(0.0, 15.0, 400) + x = r * np.cos(theta) + 50.0 + z = r * np.sin(theta) + 10.0 + y = rng.uniform(0.0, 20.0, 400) + oxygen = np.column_stack([x, y, z]) + + wx = rng.uniform(20.0, 80.0, 150) + wy = rng.uniform(0.0, 20.0, 150) + wz = np.zeros(150) + wall = np.column_stack([wx, wy, wz]) + + arc = np.linspace(0, np.pi, 60) + surface = np.column_stack([50.0 + 14.0 * np.cos(arc), 10.0 + 14.0 * np.sin(arc)]) + return oxygen, wall, [surface], np.array([50.0, 10.0, 14.0, 0.0]) + + +# --- DropletSlicePlotter (plotly) --- + + +def test_droplet_slice_plotter_returns_figure(): + """Default code path builds a plotly figure with the expected layers.""" + oxygen, wall, surface_data, popt = _synthetic_droplet() + plotter = DropletSlicePlotter(center=False) + fig = plotter.plot_surface_points( + oxygen_position=oxygen, + surface_data=surface_data, + popt=popt, + wall_coords=wall, + alpha=90.0, + ) + assert isinstance(fig, go.Figure) + # At least the wall, water, surface, circle, tangent, and arc traces. + assert len(fig.data) >= 5 + + +def test_droplet_slice_plotter_center_path(): + """center=True triggers the recentering branch.""" + oxygen, wall, surface_data, popt = _synthetic_droplet() + plotter = DropletSlicePlotter(center=True) + fig = plotter.plot_surface_points( + oxygen_position=oxygen, + surface_data=surface_data, + popt=popt, + wall_coords=wall, + alpha=85.0, + ) + assert isinstance(fig, go.Figure) + assert len(fig.data) >= 5 + + +def test_droplet_slice_plotter_with_pbc_y(): + """pbc_y wrapping branch.""" + oxygen, wall, surface_data, popt = _synthetic_droplet() + plotter = DropletSlicePlotter(center=False) + fig = plotter.plot_surface_points( + oxygen_position=oxygen, + surface_data=surface_data, + popt=popt, + wall_coords=wall, + alpha=85.0, + pbc_y=20.0, + ) + assert len(fig.data) >= 3 + + +def test_droplet_slice_plotter_layers_can_be_disabled(): + """All show_* flags off → figure has zero data traces.""" + oxygen, wall, surface_data, popt = _synthetic_droplet() + plotter = DropletSlicePlotter(center=False) + fig = plotter.plot_surface_points( + oxygen_position=oxygen, + surface_data=surface_data, + popt=popt, + wall_coords=wall, + alpha=None, + show_water=False, + show_surface=False, + show_circle=False, + show_tangent=False, + show_wall=False, + ) + assert len(fig.data) == 0 + + +# --- ContactAngleAnimator (not re-exported; import from submodule) --- + + +def test_contact_angle_animator_init_loads_fixture(): + """ContactAngleAnimator.__init__ wires up parsers and finders for a real fixture.""" + pytest.importorskip("ovito") + animator = ContactAngleAnimator( + filename=trajectory_path("traj_spherical_drop_4k.lammpstrj"), + particle_type_wall={3}, + oxygen_type=1, + hydrogen_type=2, + liquid_particle_types={1, 2}, + n_frames=1, + droplet_geometry="cylinder_y", + delta_cylinder=20, + max_dist=50, + width_cylinder=20, + ) + assert animator.wall_coords.shape[1] == 3 + assert animator.oxygen_indices.size > 0 + assert animator.parser is not None + assert animator.plotter is not None + + +@pytest.mark.slow +def test_contact_angle_animator_generates_html(tmp_path): + """Smoke-test ContactAngleAnimator on the LAMMPS fixture via cylinder_y geometry.""" + pytest.importorskip("ovito") + output = tmp_path / "animation.html" + animator = ContactAngleAnimator( + filename=trajectory_path("traj_spherical_drop_4k.lammpstrj"), + particle_type_wall={3}, + oxygen_type=1, + hydrogen_type=2, + liquid_particle_types={1, 2}, + n_frames=1, + droplet_geometry="cylinder_y", + delta_cylinder=20, + max_dist=50, + width_cylinder=20, + ) + animator.generate_animation(output_filename=str(output)) + assert output.exists() + assert output.stat().st_size > 0 diff --git a/tests/test_visualization/test_droplet_slice_plots.py b/tests/test_visualization/test_droplet_slice_plots.py deleted file mode 100644 index 43989c7..0000000 --- a/tests/test_visualization/test_droplet_slice_plots.py +++ /dev/null @@ -1,223 +0,0 @@ -"""End-to-end and branch tests for droplet_slice_plots.py. - -Smoke tests confirm the default code paths produce PNG/figure outputs; -the branch tests exercise center=True, molecule_view=True, pbc_y wrapping, -the no-intersection early return, and the ContactAngleAnimator. -""" - -import matplotlib - -matplotlib.use("Agg", force=False) - -import matplotlib.pyplot as plt -import numpy as np -import plotly.graph_objects as go -import pytest - -from tests.conftest import trajectory_path -from wetting_angle_kit.visualization.droplet_slice_plots import ( - ContactAngleAnimator, - DropletSlicePlotlyPlotter, - DropletSlicePlotter, -) - - -def _synthetic_droplet(seed=0): - rng = np.random.default_rng(seed) - theta = rng.uniform(0, np.pi, 400) - r = rng.uniform(0.0, 15.0, 400) - x = r * np.cos(theta) + 50.0 - z = r * np.sin(theta) + 10.0 - y = rng.uniform(0.0, 20.0, 400) - oxygen = np.column_stack([x, y, z]) - - wx = rng.uniform(20.0, 80.0, 150) - wy = rng.uniform(0.0, 20.0, 150) - wz = np.zeros(150) - wall = np.column_stack([wx, wy, wz]) - - arc = np.linspace(0, np.pi, 60) - surface = np.column_stack([50.0 + 14.0 * np.cos(arc), 10.0 + 14.0 * np.sin(arc)]) - return oxygen, wall, [surface], np.array([50.0, 10.0, 14.0, 0.0]) - - -def test_droplet_slicing_plotter_writes_png(tmp_path): - oxygen, wall, surface_data, popt = _synthetic_droplet() - output = tmp_path / "droplet.png" - plotter = DropletSlicePlotter(center=False, show_wall=True, molecule_view=False) - plotter.plot_surface_points( - oxygen_position=oxygen, - surface_data=surface_data, - popt=popt, - wall_coords=wall, - output_filename=str(output), - alpha=90.0, - ) - assert output.exists() - assert output.stat().st_size > 0 - plt.close("all") - - -def test_droplet_slicing_plotter_plotly_returns_figure(): - """The Plotly version should build a figure with the requested layers.""" - oxygen, wall, surface_data, popt = _synthetic_droplet() - plotter = DropletSlicePlotlyPlotter(center=False) - fig = plotter.plot_surface_points( - oxygen_position=oxygen, - surface_data=surface_data, - popt=popt, - wall_coords=wall, - alpha=90.0, - ) - assert isinstance(fig, go.Figure) - # At least the wall, water, surface, circle, tangent, and arc traces. - assert len(fig.data) >= 5 - - -def test_droplet_slicing_plotter_center_and_molecule_view(tmp_path): - """center=True + molecule_view=True exercises the recentering and - water-molecule branches.""" - oxygen, wall, surface_data, popt = _synthetic_droplet() - output = tmp_path / "centered_molview.png" - plotter = DropletSlicePlotter(center=True, show_wall=True, molecule_view=True) - plotter.plot_surface_points( - oxygen_position=oxygen, - surface_data=surface_data, - popt=popt, - wall_coords=wall, - output_filename=str(output), - alpha=90.0, - ) - assert output.exists() and output.stat().st_size > 0 - - -def test_droplet_slicing_plotter_with_pbc_y(tmp_path): - """pbc_y wrapping branch.""" - oxygen, wall, surface_data, popt = _synthetic_droplet() - output = tmp_path / "pbc.png" - plotter = DropletSlicePlotter(center=False, show_wall=True, molecule_view=False) - plotter.plot_surface_points( - oxygen_position=oxygen, - surface_data=surface_data, - popt=popt, - wall_coords=wall, - output_filename=str(output), - alpha=85.0, - pbc_y=20.0, - ) - assert output.exists() - - -def test_droplet_slicing_plotter_no_intersection_returns_early(tmp_path): - """When discriminant <= 0 the function should close the figure and bail out.""" - oxygen, wall, _, _ = _synthetic_droplet() - # Surface arc placed high above the wall so z_baseline = min(surf z) is far - # from z_center → discriminant = radius² - delta_z² < 0. - arc = np.linspace(0, np.pi, 60) - high_surface = np.column_stack( - [50.0 + 14.0 * np.cos(arc), 100.0 + 14.0 * np.sin(arc)] - ) - popt = np.array([50.0, 10.0, 0.5, 0.0]) # tiny radius, center far below surface - output = tmp_path / "no_intersect.png" - plotter = DropletSlicePlotter(center=False, show_wall=True, molecule_view=False) - plotter.plot_surface_points( - oxygen_position=oxygen, - surface_data=[high_surface], - popt=popt, - wall_coords=wall, - output_filename=str(output), - alpha=120.0, - ) - # The function bails out before saving — the file should not exist. - assert not output.exists() - - -def test_plotly_plotter_center_path(): - """DropletSlicePlotlyPlotter with center=True triggers the recentering branch.""" - oxygen, wall, surface_data, popt = _synthetic_droplet() - plotter = DropletSlicePlotlyPlotter(center=True) - fig = plotter.plot_surface_points( - oxygen_position=oxygen, - surface_data=surface_data, - popt=popt, - wall_coords=wall, - alpha=85.0, - ) - assert fig is not None - assert len(fig.data) >= 5 - - -def test_plotly_plotter_with_pbc_y(): - oxygen, wall, surface_data, popt = _synthetic_droplet() - plotter = DropletSlicePlotlyPlotter(center=False) - fig = plotter.plot_surface_points( - oxygen_position=oxygen, - surface_data=surface_data, - popt=popt, - wall_coords=wall, - alpha=85.0, - pbc_y=20.0, - ) - assert len(fig.data) >= 3 - - -def test_plotly_plotter_layers_can_be_disabled(): - """All show_* flags off → figure has zero data traces.""" - oxygen, wall, surface_data, popt = _synthetic_droplet() - plotter = DropletSlicePlotlyPlotter(center=False) - fig = plotter.plot_surface_points( - oxygen_position=oxygen, - surface_data=surface_data, - popt=popt, - wall_coords=wall, - alpha=None, - show_water=False, - show_surface=False, - show_circle=False, - show_tangent=False, - show_wall=False, - ) - assert len(fig.data) == 0 - - -def test_contact_angle_animator_init_loads_fixture(tmp_path): - """ContactAngleAnimator.__init__ wires up parsers and finders for a real fixture.""" - pytest.importorskip("ovito") - animator = ContactAngleAnimator( - filename=trajectory_path("traj_spherical_drop_4k.lammpstrj"), - particle_type_wall={3}, - oxygen_type=1, - hydrogen_type=2, - liquid_particle_types={1, 2}, - n_frames=1, - droplet_geometry="cylinder_y", - delta_cylinder=20, - max_dist=50, - width_cylinder=20, - ) - assert animator.wall_coords.shape[1] == 3 - assert animator.oxygen_indices.size > 0 - assert animator.parser is not None - assert animator.plotter is not None - - -@pytest.mark.slow -def test_contact_angle_animator_generates_html(tmp_path): - """Smoke-test ContactAngleAnimator on the LAMMPS fixture via cylinder_y geometry.""" - pytest.importorskip("ovito") - output = tmp_path / "animation.html" - animator = ContactAngleAnimator( - filename=trajectory_path("traj_spherical_drop_4k.lammpstrj"), - particle_type_wall={3}, - oxygen_type=1, - hydrogen_type=2, - liquid_particle_types={1, 2}, - n_frames=1, - droplet_geometry="cylinder_y", - delta_cylinder=20, - max_dist=50, - width_cylinder=20, - ) - animator.generate_animation(output_filename=str(output)) - assert output.exists() - assert output.stat().st_size > 0 diff --git a/tests/test_visualization/test_method_comparison.py b/tests/test_visualization/test_method_comparison.py deleted file mode 100644 index 3fd3078..0000000 --- a/tests/test_visualization/test_method_comparison.py +++ /dev/null @@ -1,106 +0,0 @@ -import os - -import pytest - -from wetting_angle_kit.visualization.binning_trajectory_analyzer import ( - BinningTrajectoryAnalyzer, -) -from wetting_angle_kit.visualization.method_comparison import MethodComparison - - -def _write_binning_dir(directory, samples): - directory.mkdir() - for idx, (r_eq, zi_c, zi_0, angle) in enumerate(samples, start=1): - (directory / f"log_data_batch_{idx}.txt").write_text( - f"R_eq:{r_eq}\nzi_c:{zi_c}\nzi_0:{zi_0}\nContact angle:{angle}\n" - ) - - -@pytest.fixture -def two_binning_analyzers(tmp_path): - d1 = tmp_path / "result_dump_runA" - d2 = tmp_path / "result_dump_runB" - _write_binning_dir(d1, [(15.0, 8.0, 6.0, 95.0), (14.5, 7.8, 6.1, 96.5)]) - _write_binning_dir(d2, [(16.0, 8.2, 6.0, 92.0), (15.5, 8.0, 6.1, 93.5)]) - - analyzer1 = BinningTrajectoryAnalyzer([str(d1)]) - analyzer2 = BinningTrajectoryAnalyzer([str(d2)]) - analyzer1.load_data() - analyzer2.load_data() - # Generate output_stats.txt for both - analyzer1.analyze() - analyzer2.analyze() - return analyzer1, analyzer2 - - -def test_method_comparison_compare_statistics(two_binning_analyzers): - a1, a2 = two_binning_analyzers - comparator = MethodComparison([a1, a2], method_names=["Binning A", "Binning B"]) - report = comparator.compare_statistics() - assert "METHOD COMPARISON STATISTICS" in report - assert "Binning A" in report - assert "Binning B" in report - assert "Mean Angle" in report - assert "Std Angle" in report - - -def test_method_comparison_default_method_names(two_binning_analyzers): - a1, a2 = two_binning_analyzers - comparator = MethodComparison([a1, a2]) - assert comparator.method_names == ["Binning Analysis", "Binning Analysis"] - - -def test_method_comparison_side_by_side_plot(two_binning_analyzers, tmp_path): - a1, a2 = two_binning_analyzers - comparator = MethodComparison([a1, a2], method_names=["A", "B"]) - save_path = tmp_path / "side_by_side.png" - comparator.plot_side_by_side_comparison(save_path=str(save_path)) - assert save_path.exists() - - -def test_method_comparison_overlay_plot(two_binning_analyzers, tmp_path): - a1, a2 = two_binning_analyzers - comparator = MethodComparison([a1, a2], method_names=["A", "B"]) - save_path = tmp_path / "overlay.png" - comparator.plot_overlay_comparison(save_path=str(save_path)) - assert save_path.exists() - - -def test_method_comparison_side_by_side_single_analyzer( - two_binning_analyzers, tmp_path -): - # Cover the `len(self.analyzers) == 1` branch - a1, _ = two_binning_analyzers - comparator = MethodComparison([a1]) - save_path = tmp_path / "single.png" - comparator.plot_side_by_side_comparison(save_path=str(save_path)) - assert save_path.exists() - - -def test_method_comparison_check_and_run_raises_when_missing(tmp_path): - """_check_and_run_analysis raises if output_stats.txt is absent.""" - d = tmp_path / "result_dump_empty" - d.mkdir() - (d / "log_data_batch_1.txt").write_text( - "R_eq:15.0\nzi_c:8.0\nzi_0:6.0\nContact angle:90.0\n" - ) - analyzer = BinningTrajectoryAnalyzer([str(d)]) - analyzer.load_data() - # do not call analyze(), so output_stats.txt is missing - comparator = MethodComparison([analyzer]) - with pytest.raises(FileNotFoundError, match="No analysis found"): - comparator._check_and_run_analysis(analyzer) - - -def test_method_comparison_compare_statistics_falls_back_when_stats_missing(tmp_path): - """compare_statistics() recovers via in-memory data - if output_stats.txt is missing.""" - d = tmp_path / "result_dump_no_stats" - _write_binning_dir(d, [(15.0, 8.0, 6.0, 95.0), (14.5, 7.8, 6.1, 96.5)]) - analyzer = BinningTrajectoryAnalyzer([str(d)]) - analyzer.load_data() - # Remove output_stats.txt path is never created (analyze() not called) - assert not os.path.exists(os.path.join(str(d), "output_stats.txt")) - comparator = MethodComparison([analyzer]) - report = comparator.compare_statistics() - assert "Mean Angle" in report diff --git a/tests/test_visualization/test_surface_plots.py b/tests/test_visualization/test_surface_plots.py deleted file mode 100644 index 3759bf4..0000000 --- a/tests/test_visualization/test_surface_plots.py +++ /dev/null @@ -1,84 +0,0 @@ -import matplotlib - -matplotlib.use("Agg", force=False) - -import numpy as np - -from wetting_angle_kit.visualization.surface_plots import ( - plot_liquid_particles, - plot_slice, - plot_surface_and_points, - plot_surface_file, - read_surface_file, - visualize_surface_with_points, -) - - -def _write_surface(tmp_path, columns): - path = tmp_path / "surface.dat" - np.savetxt(path, columns) - return str(path) - - -def test_plot_surface_file_returns_xy(tmp_path): - data = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) - path = _write_surface(tmp_path, data) - x, y = plot_surface_file(path) - assert np.allclose(x, [1.0, 3.0, 5.0]) - assert np.allclose(y, [2.0, 4.0, 6.0]) - - -def test_read_surface_file_two_columns_pads_z(tmp_path): - data = np.array([[1.0, 2.0], [3.0, 4.0]]) - path = _write_surface(tmp_path, data) - x, y, z = read_surface_file(path) - assert np.allclose(x, [1.0, 3.0]) - assert np.allclose(y, [2.0, 4.0]) - assert np.allclose(z, [0.0, 0.0]) - - -def test_read_surface_file_three_columns(tmp_path): - data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) - path = _write_surface(tmp_path, data) - x, y, z = read_surface_file(path) - assert np.allclose(x, [1.0, 4.0]) - assert np.allclose(y, [2.0, 5.0]) - assert np.allclose(z, [3.0, 6.0]) - - -def test_plot_slice_runs(): - plot_slice(np.array([0.0, 1.0, 2.0]), np.array([0.0, 1.0, 0.0])) - - -def test_plot_surface_and_points_runs(): - x = np.linspace(0, 1, 5) - plot_surface_and_points(x, x, x, x + 0.1, x + 0.2, x + 0.3) - - -def test_visualize_surface_with_points_runs(tmp_path): - data = np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]) - path = _write_surface(tmp_path, data) - points = np.array([[0.5, 0.5, 0.5], [0.2, 0.2, 0.2]]) - visualize_surface_with_points(path, points) - - -def test_plot_liquid_particles_creates_axes(): - positions = np.random.default_rng(0).uniform(size=(50, 3)) - ax = plot_liquid_particles(positions) - assert ax is not None - - -def test_plot_liquid_particles_subsample(): - positions = np.random.default_rng(0).uniform(size=(100, 3)) - ax = plot_liquid_particles(positions, subsample=10) - assert ax is not None - - -def test_plot_liquid_particles_uses_given_ax(): - import matplotlib.pyplot as plt - - fig = plt.figure() - ax = fig.add_subplot(111, projection="3d") - positions = np.random.default_rng(0).uniform(size=(20, 3)) - returned = plot_liquid_particles(positions, ax=ax) - assert returned is ax diff --git a/tests/test_visualization/test_trajectory_analyzers.py b/tests/test_visualization/test_trajectory_analyzers.py deleted file mode 100644 index cfdfd5c..0000000 --- a/tests/test_visualization/test_trajectory_analyzers.py +++ /dev/null @@ -1,185 +0,0 @@ -import os -import pathlib - -import numpy as np -import pytest - -from wetting_angle_kit.visualization.binning_trajectory_analyzer import ( - BinningTrajectoryAnalyzer, -) -from wetting_angle_kit.visualization.slicing_trajectory_analyzer import ( - SlicingTrajectoryAnalyzer, -) - - -def _square_polygon(side: float = 2.0) -> np.ndarray: - half = side / 2.0 - return np.array( - [ - [-half, -half], - [half, -half], - [half, half], - [-half, half], - ] - ) - - -@pytest.fixture -def slicing_result_dir(tmp_path): - """Create a directory with the .npy files expected by SlicingTrajectoryAnalyzer.""" - directory = tmp_path / "slicing_result" - directory.mkdir() - polygon = _square_polygon(side=4.0) - all_angles = np.array( - [ - (0, np.array([85.0, 90.0, 95.0])), - (1, np.array([87.0, 92.0, 96.0])), - ], - dtype=object, - ) - all_surfaces = np.array( - [ - (0, [polygon, polygon * 1.1]), - (1, [polygon * 1.05, polygon * 1.15]), - ], - dtype=object, - ) - all_popts = np.array( - [ - (0, np.array([1.0, 2.0, 3.0, 4.0])), - (1, np.array([1.1, 2.1, 3.1, 4.1])), - ], - dtype=object, - ) - np.save(directory / "all_angles.npy", all_angles, allow_pickle=True) - np.save(directory / "all_surfaces.npy", all_surfaces, allow_pickle=True) - np.save(directory / "all_popts.npy", all_popts, allow_pickle=True) - return str(directory) - - -@pytest.fixture -def binning_result_dir(tmp_path): - """Create a directory with the log_data_batch_*.txt - expected by BinningTrajectoryAnalyzer.""" - directory = tmp_path / "binning_result" - directory.mkdir() - for idx, (r_eq, zi_c, zi_0, angle) in enumerate( - [(15.0, 8.0, 6.0, 95.0), (14.5, 7.8, 6.1, 96.5)], start=1 - ): - path = directory / f"log_data_batch_{idx}.txt" - path.write_text( - f"R_eq:{r_eq}\nzi_c:{zi_c}\nzi_0:{zi_0}\nContact angle:{angle}\n" - ) - return str(directory) - - -def test_slicing_trajectory_analyzer_load_and_stats(slicing_result_dir): - analyzer = SlicingTrajectoryAnalyzer( - [slicing_result_dir], time_steps={slicing_result_dir: 0.5} - ) - analyzer.load_data() - - surfaces = analyzer.get_surface_areas(slicing_result_dir) - angles = analyzer.get_contact_angles(slicing_result_dir) - assert surfaces.shape == (2,) - assert np.all(surfaces > 0) - assert angles.shape == (2,) - assert analyzer.get_method_name() == "Slicing Analysis" - - -def test_slicing_trajectory_analyzer_plot_evolution(slicing_result_dir, tmp_path): - analyzer = SlicingTrajectoryAnalyzer([slicing_result_dir]) - analyzer.load_data() - - median_path = tmp_path / "median.png" - mean_path = tmp_path / "mean.png" - analyzer.plot_median_angles_evolution(str(median_path)) - analyzer.plot_mean_angles_evolution(str(mean_path)) - - assert median_path.exists() - assert mean_path.exists() - - -def test_binning_trajectory_analyzer_load(binning_result_dir): - analyzer = BinningTrajectoryAnalyzer([binning_result_dir]) - analyzer.read_data() # alias for load_data - angles = analyzer.get_contact_angles(binning_result_dir) - surfaces = analyzer.get_surface_areas(binning_result_dir) - assert angles.shape == (2,) - assert np.allclose(angles, [95.0, 96.5]) - assert surfaces.shape == (2,) - assert np.all(surfaces > 0) - assert analyzer.get_method_name() == "Binning Analysis" - - -@pytest.mark.parametrize( - "R,z_center,z_cut,expected", - [ - (1.0, 0.0, 5.0, 0.0), # cap entirely above cut - (1.0, 0.0, -5.0, np.pi), # cap covers full disk (π·R²) - ], -) -def test_circular_segment_area_edge_cases(R, z_center, z_cut, expected): - area = BinningTrajectoryAnalyzer.circular_segment_area(R, z_center, z_cut) - assert area == pytest.approx(expected, rel=1e-6) - - -def test_circular_segment_area_partial(): - area = BinningTrajectoryAnalyzer.circular_segment_area(1.0, 0.0, 0.0) - # Cut at midplane → half disk area - assert area == pytest.approx(np.pi / 2, rel=1e-6) - - -def test_circular_segment_area_upper_half(): - # h > R but < 2R: between half and full disk - area = BinningTrajectoryAnalyzer.circular_segment_area(1.0, 0.0, -0.5) - full = np.pi - assert np.pi / 2 < area < full - - -def test_base_analyze_writes_output_stats(binning_result_dir): - analyzer = BinningTrajectoryAnalyzer([binning_result_dir]) - analyzer.analyze() - output_file = os.path.join(binning_result_dir, "output_stats.txt") - assert os.path.exists(output_file) - with open(output_file, encoding="utf-8") as f: - text = f.read() - assert "Mean Contact Angle:" in text - assert "Std Contact Angle:" in text - assert "Mean Surface Area:" in text - - -def test_base_compute_statistics_and_clean_label(binning_result_dir): - analyzer = BinningTrajectoryAnalyzer([binning_result_dir]) - analyzer.load_data() - x, y, yerr = analyzer.compute_statistics(binning_result_dir) - assert x > 0 - assert y == pytest.approx(np.mean([95.0, 96.5])) - assert yerr >= 0 - - label = analyzer.get_clean_label("result_dump_my_run_reduce_binning") - assert label == "my_run" - - -def test_base_plot_mean_angle_vs_surface(binning_result_dir, tmp_path): - # Two directories with the same fixture content so the linear-fit branch executes. - second = tmp_path / "binning_result_2" - second.mkdir() - for src in os.listdir(binning_result_dir): - (second / src).write_text( - (pathlib.Path(binning_result_dir) / src).read_text(encoding="utf-8"), - encoding="utf-8", - ) - - analyzer = BinningTrajectoryAnalyzer([binning_result_dir, str(second)]) - save_path = tmp_path / "scaling.png" - analyzer.plot_mean_angle_vs_surface(save_path=str(save_path)) - assert save_path.exists() - - -def test_binning_load_files_raises_on_empty(tmp_path): - empty = tmp_path / "empty" - empty.mkdir() - analyzer = BinningTrajectoryAnalyzer([str(empty)]) - with pytest.raises(ValueError, match="No log_data_batch"): - analyzer.load_files() diff --git a/tests/test_visualization/test_trajectory_plotters.py b/tests/test_visualization/test_trajectory_plotters.py new file mode 100644 index 0000000..03f3ea0 --- /dev/null +++ b/tests/test_visualization/test_trajectory_plotters.py @@ -0,0 +1,180 @@ +import numpy as np +import plotly.graph_objects as go +import pytest + +from wetting_angle_kit.analysis.binning.results import BinningBatch, BinningResults +from wetting_angle_kit.analysis.slicing.results import SlicingResults +from wetting_angle_kit.visualization.binning_trajectory_plotter import ( + BinningTrajectoryPlotter, +) +from wetting_angle_kit.visualization.slicing_trajectory_plotter import ( + SlicingTrajectoryPlotter, +) + + +def _square_polygon(side: float = 2.0) -> np.ndarray: + half = side / 2.0 + return np.array( + [ + [-half, -half], + [half, -half], + [half, half], + [-half, half], + ] + ) + + +@pytest.fixture +def slicing_results(): + polygon = _square_polygon(side=4.0) + return SlicingResults( + frames=[0, 1], + angles=[ + np.array([85.0, 90.0, 95.0]), + np.array([87.0, 92.0, 96.0]), + ], + surfaces=[ + [polygon, polygon * 1.1], + [polygon * 1.05, polygon * 1.15], + ], + popts=[ + np.array([1.0, 2.0, 3.0, 4.0]), + np.array([1.1, 2.1, 3.1, 4.1]), + ], + ) + + +@pytest.fixture +def binning_results(): + return BinningResults( + batches=[ + BinningBatch( + batch_index=1, + angle=95.0, + n_particles=100.0, + xi_cc=np.linspace(0.0, 10.0, 5), + zi_cc=np.linspace(0.0, 10.0, 5), + rho_cc=np.ones((5, 5)), + circle_xi=np.array([0.0, 1.0, 2.0]), + circle_zi=np.array([5.0, 6.0, 7.0]), + wall_line_xi=np.array([0.0, 1.0, 2.0]), + wall_line_zi=np.array([6.0, 6.0, 6.0]), + fitted_params={"R_eq": 15.0, "zi_c": 8.0, "zi_0": 6.0}, + ), + BinningBatch( + batch_index=2, + angle=96.5, + n_particles=110.0, + xi_cc=np.linspace(0.0, 10.0, 5), + zi_cc=np.linspace(0.0, 10.0, 5), + rho_cc=np.ones((5, 5)), + circle_xi=None, + circle_zi=None, + wall_line_xi=None, + wall_line_zi=None, + fitted_params={"R_eq": 14.5, "zi_c": 7.8, "zi_0": 6.1}, + ), + ] + ) + + +# --- SlicingTrajectoryPlotter --- + + +def test_slicing_plotter_summary(slicing_results): + plotter = SlicingTrajectoryPlotter(slicing_results, labels=["A"]) + [stats] = plotter.summary() + assert stats.method_name == "Slicing Analysis" + assert stats.label == "A" + assert stats.n_samples == 2 + # mean of per-frame means: mean([90.0, 91.667]) ≈ 90.83 + assert 80.0 < stats.mean_contact_angle < 100.0 + assert stats.mean_surface_area > 0 + + +def test_slicing_plotter_plot_angle_evolution_returns_figure(slicing_results): + plotter = SlicingTrajectoryPlotter(slicing_results, time_steps=[0.5]) + fig = plotter.plot_angle_evolution(stat="median") + assert isinstance(fig, go.Figure) + fig_mean = plotter.plot_angle_evolution(stat="mean") + assert isinstance(fig_mean, go.Figure) + + +def test_slicing_plotter_rejects_unknown_stat(slicing_results): + plotter = SlicingTrajectoryPlotter(slicing_results) + with pytest.raises(ValueError, match="stat must be"): + plotter.plot_angle_evolution(stat="bogus") + + +# --- BinningTrajectoryPlotter --- + + +def test_binning_plotter_summary(binning_results): + plotter = BinningTrajectoryPlotter(binning_results, labels=["A"]) + [stats] = plotter.summary() + assert stats.method_name == "Binning Analysis" + assert stats.label == "A" + assert stats.n_samples == 2 + assert stats.mean_contact_angle == pytest.approx(np.mean([95.0, 96.5])) + assert stats.std_contact_angle == pytest.approx(np.std([95.0, 96.5])) + assert stats.mean_surface_area > 0 + + +def test_binning_plotter_summary_str_block(binning_results): + plotter = BinningTrajectoryPlotter(binning_results) + [stats] = plotter.summary() + text = str(stats) + assert "Mean Contact Angle:" in text + assert "Std Contact Angle:" in text + assert "Mean Surface Area:" in text + + +def test_binning_plotter_plot_angle_evolution_returns_figure(binning_results): + plotter = BinningTrajectoryPlotter(binning_results, time_steps=[2.0]) + fig = plotter.plot_angle_evolution() + assert isinstance(fig, go.Figure) + + +def test_binning_plotter_density_contour_with_isoline(binning_results): + plotter = BinningTrajectoryPlotter(binning_results) + fig = plotter.plot_density_contour(batch_index=0) + assert isinstance(fig, go.Figure) + # contour + circle + wall = 3 traces + assert len(fig.data) == 3 + + +def test_binning_plotter_density_contour_without_isoline(binning_results): + plotter = BinningTrajectoryPlotter(binning_results) + # second batch has circle/wall = None + fig = plotter.plot_density_contour(batch_index=1) + assert isinstance(fig, go.Figure) + # only the contour trace when isoline is missing + assert len(fig.data) == 1 + + +# --- circular_segment_area static method --- + + +@pytest.mark.parametrize( + "R,z_center,z_cut,expected", + [ + (1.0, 0.0, 5.0, 0.0), # cap entirely above cut + (1.0, 0.0, -5.0, np.pi), # cap covers full disk (π·R²) + ], +) +def test_circular_segment_area_edge_cases(R, z_center, z_cut, expected): + area = BinningTrajectoryPlotter.circular_segment_area(R, z_center, z_cut) + assert area == pytest.approx(expected, rel=1e-6) + + +def test_circular_segment_area_partial(): + area = BinningTrajectoryPlotter.circular_segment_area(1.0, 0.0, 0.0) + # Cut at midplane → half disk area + assert area == pytest.approx(np.pi / 2, rel=1e-6) + + +def test_circular_segment_area_upper_half(): + # h > R but < 2R: between half and full disk + area = BinningTrajectoryPlotter.circular_segment_area(1.0, 0.0, -0.5) + full = np.pi + assert np.pi / 2 < area < full From 3909a7b601eca7cbe432b071d832481b15a44ddf Mon Sep 17 00:00:00 2001 From: gbrunin Date: Fri, 29 May 2026 15:43:49 +0200 Subject: [PATCH 02/31] Trajectory format reading failure and empty processed frame failure to make things clearer. --- .../analysis/slicing/parallel.py | 12 +++++++- .../test_analysis/test_slicing_edge_cases.py | 29 ++++++++++--------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/wetting_angle_kit/analysis/slicing/parallel.py b/src/wetting_angle_kit/analysis/slicing/parallel.py index c78cd26..d7a5ec6 100644 --- a/src/wetting_angle_kit/analysis/slicing/parallel.py +++ b/src/wetting_angle_kit/analysis/slicing/parallel.py @@ -7,7 +7,7 @@ import numpy as np from wetting_angle_kit.analysis.slicing.results import SlicingResults -from wetting_angle_kit.io_utils import recenter_droplet_pbc +from wetting_angle_kit.io_utils import detect_parser_type, recenter_droplet_pbc from wetting_angle_kit.parsers import BaseParser # "spawn" is required because parser instances may hold un-picklable handles @@ -87,6 +87,10 @@ def __init__( associated overhead. Setting this on a trajectory that does NOT satisfy the precondition will produce wrong results. """ + # Fail fast on an unrecognized trajectory file: workers re-detect the + # parser type inside their subprocesses and would otherwise swallow the + # error, returning empty results for every frame. + detect_parser_type(filename) self.filename = filename self.delta_gamma = delta_gamma self.delta_cylinder = delta_cylinder @@ -161,6 +165,12 @@ def process_frames_parallel( f"Successfully processed {len(sorted_frames)}/{len(frames_to_process)} " "frames" ) + if frames_to_process and not sorted_frames: + raise RuntimeError( + f"None of the {len(frames_to_process)} requested frames produced " + "any contact-angle slices. Check the worker logs above for the " + "underlying parser, geometry, or fit errors." + ) return SlicingResults( frames=sorted_frames, angles=[np.asarray(all_angles[f]) for f in sorted_frames], diff --git a/tests/test_analysis/test_slicing_edge_cases.py b/tests/test_analysis/test_slicing_edge_cases.py index a17ef22..bb725b2 100644 --- a/tests/test_analysis/test_slicing_edge_cases.py +++ b/tests/test_analysis/test_slicing_edge_cases.py @@ -102,14 +102,16 @@ def test_calculate_y_axis_spherical(): # --- ContactAngleSlicingParallel internals --- -def test_create_batches_few_frames(tmp_path): - parallel = ContactAngleSlicingParallel(filename="ignored") +def test_create_batches_few_frames(): + # filename only needs a recognized extension; the file does not have to exist + # for _create_batches, which is pure logic on the requested frame list. + parallel = ContactAngleSlicingParallel(filename="ignored.lammpstrj") # num_batches >= len(frames) → one frame per batch assert parallel._create_batches([1, 2, 3], num_batches=4) == [[1], [2], [3]] -def test_create_batches_many_frames(tmp_path): - parallel = ContactAngleSlicingParallel(filename="ignored") +def test_create_batches_many_frames(): + parallel = ContactAngleSlicingParallel(filename="ignored.lammpstrj") batches = parallel._create_batches(list(range(10)), num_batches=3) flat = [f for batch in batches for f in batch] assert flat == list(range(10)) @@ -137,15 +139,14 @@ def test_process_batch_worker_invokes_pipeline_on_real_lammps(tmp_path): assert results[0].frame_num == 0 -def test_process_batch_worker_unsupported_extension(tmp_path): - """Unknown trajectory extension → worker returns failed results.""" +def test_unsupported_extension_raises_at_construction(tmp_path): + """Unknown trajectory extension must fail fast at construction, not later in + subprocesses where the error would be silently swallowed.""" fake = tmp_path / "trajectory.bogus" fake.write_text("not a real trajectory\n") - parallel = ContactAngleSlicingParallel( - filename=str(fake), - droplet_geometry="spherical", - delta_gamma=20.0, - ) - results = parallel._process_batch_worker(batch_frames=[0, 1]) - assert len(results) == 2 - assert all(r.mean_angle is None for r in results) + with pytest.raises(ValueError, match="Unsupported trajectory file format"): + ContactAngleSlicingParallel( + filename=str(fake), + droplet_geometry="spherical", + delta_gamma=20.0, + ) From 77c53ccaa2c30fcdbce717de22eec3477b186b17 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Mon, 1 Jun 2026 08:38:32 +0200 Subject: [PATCH 03/31] Faster convergence of circle fit. --- .../analysis/slicing/surface_definition.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/wetting_angle_kit/analysis/slicing/surface_definition.py b/src/wetting_angle_kit/analysis/slicing/surface_definition.py index 619d2a7..362f222 100644 --- a/src/wetting_angle_kit/analysis/slicing/surface_definition.py +++ b/src/wetting_angle_kit/analysis/slicing/surface_definition.py @@ -155,7 +155,30 @@ def fit_density_profile( float Fitted ``zd`` value (interface location). """ - popt, _ = curve_fit(self.density_profile, z_data, density, bounds=param_bounds) + # For rho(s) = d * tanh(zd - s) + h: + # h ~ midpoint of the density signal, + # d ~ half-amplitude (positive, since density is high near center + # and low past the interface, so tanh(zd - s) goes +1 -> -1), + # zd ~ position where density crosses the midpoint h. + rho_max = float(np.max(density)) + rho_min = float(np.min(density)) + h0 = 0.5 * (rho_max + rho_min) + d0 = 0.5 * (rho_max - rho_min) + zd0 = float(z_data[int(np.argmin(np.abs(density - h0)))]) + lower, upper = param_bounds + p0 = [ + float(np.clip(zd0, lower[0], upper[0])), + float(np.clip(d0, lower[1], upper[1])), + float(np.clip(h0, lower[2], upper[2])), + ] + popt, _ = curve_fit( + self.density_profile, + z_data, + density, + p0=p0, + bounds=param_bounds, + maxfev=5000, + ) zd, d, h = popt return zd From d284e3ef6fe6a6ef7e90590c9e3db50b01532efd Mon Sep 17 00:00:00 2001 From: gbrunin Date: Mon, 1 Jun 2026 09:54:42 +0200 Subject: [PATCH 04/31] Removed contact_angle_analyzer wrapper that hides useful API. Bug fixes. Forcing cylindrical parameters when using cylindircal geometry, same for spherical geometry. --- docs/examples/binning_ca.py | 5 ++- docs/examples/slicing_ca.py | 5 ++- docs/source/tutorials/Binning_method_tuto.rst | 5 ++- docs/source/tutorials/Slicing_method_tuto.rst | 12 +++---- docs/tutorials/Binning_method_tuto.md | 8 ++--- docs/tutorials/Slicing_method_tuto.md | 14 +++----- src/wetting_angle_kit/analysis/__init__.py | 33 ------------------- .../analysis/binning/angle_fitting.py | 5 +++ .../analysis/slicing/angle_fitting.py | 32 +++++++++++------- .../analysis/slicing/parallel.py | 25 +++++++++++++- .../visualization/animator.py | 9 +++++ tests/test_analysis/test_binning_method.py | 8 ++--- .../test_analysis/test_slicing_edge_cases.py | 16 ++++++--- tests/test_analysis/test_slicing_method.py | 5 ++- tests/test_edge_cases.py | 16 ++------- .../test_droplet_slice_plot.py | 6 ++-- 16 files changed, 99 insertions(+), 105 deletions(-) diff --git a/docs/examples/binning_ca.py b/docs/examples/binning_ca.py index b948ffe..87256a6 100644 --- a/docs/examples/binning_ca.py +++ b/docs/examples/binning_ca.py @@ -1,5 +1,5 @@ # Import necessary modules -from wetting_angle_kit.analysis import contact_angle_analyzer +from wetting_angle_kit.analysis import BinningContactAngleAnalyzer from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder # --- Step 1: Define the trajectory file --- @@ -32,8 +32,7 @@ parser = LammpsDumpParser(filename) # --- Step 6: Create the contact angle analyzer --- -analyzer = contact_angle_analyzer( - method="binning", +analyzer = BinningContactAngleAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="spherical", # Interface fitting model diff --git a/docs/examples/slicing_ca.py b/docs/examples/slicing_ca.py index 2d42ebe..81dbf68 100644 --- a/docs/examples/slicing_ca.py +++ b/docs/examples/slicing_ca.py @@ -4,7 +4,7 @@ file and prints the resulting mean contact angle. """ -from wetting_angle_kit.analysis import contact_angle_analyzer +from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder # --- Step 1: Define the trajectory file --- @@ -24,8 +24,7 @@ # --- Step 3: Build the slicing analyzer --- parser = LammpsDumpParser(filename) -analyzer = contact_angle_analyzer( - method="slicing", +analyzer = SlicingContactAngleAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="spherical", diff --git a/docs/source/tutorials/Binning_method_tuto.rst b/docs/source/tutorials/Binning_method_tuto.rst index 7fddfd1..78b5e1c 100644 --- a/docs/source/tutorials/Binning_method_tuto.rst +++ b/docs/source/tutorials/Binning_method_tuto.rst @@ -39,7 +39,7 @@ Example trajectory:: # Import necessary modules from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder - from wetting_angle_kit.analysis import contact_angle_analyzer + from wetting_angle_kit.analysis import BinningContactAngleAnalyzer # --- Step 1: Define the trajectory file --- filename = "../../tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj" @@ -71,8 +71,7 @@ Example trajectory:: parser = LammpsDumpParser(filename) # --- Step 6: Create the contact angle analyzer --- - analyzer = contact_angle_analyzer( - method="binning", + analyzer = BinningContactAngleAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="cylinder_y", # Interface fitting model diff --git a/docs/source/tutorials/Slicing_method_tuto.rst b/docs/source/tutorials/Slicing_method_tuto.rst index eee0bf3..59242d3 100644 --- a/docs/source/tutorials/Slicing_method_tuto.rst +++ b/docs/source/tutorials/Slicing_method_tuto.rst @@ -35,7 +35,7 @@ Example trajectory:: # Import necessary modules from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder - from wetting_angle_kit.analysis import contact_angle_analyzer + from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer # --- Step 1: Define the trajectory file --- filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj" @@ -56,9 +56,8 @@ Example trajectory:: parser = LammpsDumpParser(filename) # --- Step 5: Create the contact angle analyzer --- - # Using the 'slicing' method with a spherical model - analyzer = contact_angle_analyzer( - method="slicing", + # Using the slicing method with a spherical model + analyzer = SlicingContactAngleAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="spherical", # Geometry fitting model @@ -134,7 +133,7 @@ following convenience attributes: """ from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder - from wetting_angle_kit.analysis import contact_angle_analyzer + from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer # --- Step 1: Define input trajectory --- filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj" @@ -151,8 +150,7 @@ following convenience attributes: parser = LammpsDumpParser(filename) # --- Step 4: Create analyzer for the slicing method --- - analyzer = contact_angle_analyzer( - method="slicing", + analyzer = SlicingContactAngleAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="spherical", # Fitting model diff --git a/docs/tutorials/Binning_method_tuto.md b/docs/tutorials/Binning_method_tuto.md index 745bfc5..9dd825a 100644 --- a/docs/tutorials/Binning_method_tuto.md +++ b/docs/tutorials/Binning_method_tuto.md @@ -33,7 +33,7 @@ tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj ```python # Import necessary modules from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder -from wetting_angle_kit.analysis import contact_angle_analyzer +from wetting_angle_kit.analysis import BinningContactAngleAnalyzer # --- Step 1: Define the trajectory file --- filename = "../../tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj" @@ -65,15 +65,12 @@ binning_params = { parser = LammpsDumpParser(filename) # --- Step 6: Create the contact angle analyzer --- -analyzer = contact_angle_analyzer( - method="binning", +analyzer = BinningContactAngleAnalyzer( parser=parser, - output_dir="results_binning_example", atom_indices=oxygen_indices, droplet_geometry="cylinder_y", # Interface fitting model width_cylinder=21, # Width parameter for interface fit binning_params=binning_params, - plot_graphs=False, # Disable plotting for automated runs ) # --- Step 7: Run analysis for a frame range --- @@ -125,7 +122,6 @@ A heat map representation of the particles density and the fitted semi-circle to - Adjust `xi_f`, `zi_f`, and the bin counts (`nbins_xi`, `nbins_zi`) according to your simulation box dimensions. - If the wall surface is not flat or the system is tilted, pre-align it before analysis. -- Use `plot_graphs=True` to visualize the binning density and interface fitting. - For multiple frames: `analyzer.analyze(range(0, 100, 10))`. --- diff --git a/docs/tutorials/Slicing_method_tuto.md b/docs/tutorials/Slicing_method_tuto.md index dfb809d..aaea597 100644 --- a/docs/tutorials/Slicing_method_tuto.md +++ b/docs/tutorials/Slicing_method_tuto.md @@ -30,7 +30,7 @@ tests/trajectories/traj_spherical_drop_4k.lammpstrj ````python # Import necessary modules from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder -from wetting_angle_kit.analysis import contact_angle_analyzer +from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer # --- Step 1: Define the trajectory file --- filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj" @@ -50,11 +50,9 @@ print("Number of water molecules:", len(oxygen_indices)) parser = LammpsDumpParser(filename) # --- Step 5: Create the contact angle analyzer --- -# Using the 'slicing' method with a spherical model -analyzer = contact_angle_analyzer( - method='slicing', +# Using the slicing method with a spherical model +analyzer = SlicingContactAngleAnalyzer( parser=parser, - output_dir='result_dump_spherical_slicing', atom_indices=oxygen_indices, droplet_geometry='spherical', # Geometry fitting model delta_gamma=20 # Smoothing parameter @@ -114,7 +112,7 @@ using the 'slicing' method on a spherical droplet from a LAMMPS dump trajectory. """ from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder -from wetting_angle_kit.analysis import contact_angle_analyzer +from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer # --- Step 1: Define input trajectory --- filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj" @@ -134,10 +132,8 @@ print(f"Number of water molecules: {len(oxygen_indices)}") parser = LammpsDumpParser(filename) # --- Step 4: Create analyzer for the slicing method --- -analyzer = contact_angle_analyzer( - method='slicing', +analyzer = SlicingContactAngleAnalyzer( parser=parser, - output_dir='result_dump_spherical_slicing', atom_indices=oxygen_indices, droplet_geometry='spherical', delta_gamma=20 diff --git a/src/wetting_angle_kit/analysis/__init__.py b/src/wetting_angle_kit/analysis/__init__.py index d543b6c..290f1c3 100644 --- a/src/wetting_angle_kit/analysis/__init__.py +++ b/src/wetting_angle_kit/analysis/__init__.py @@ -1,7 +1,5 @@ """Contact-angle analysis orchestrators and per-method engines.""" -from typing import Any - from wetting_angle_kit.analysis.analyzer import ( BaseContactAngleAnalyzer, BinningContactAngleAnalyzer, @@ -17,41 +15,10 @@ ContactAngleSlicingParallel, ) - -def contact_angle_analyzer( - method: str, - parser: Any, - **kwargs: Any, -) -> BaseContactAngleAnalyzer: - """Return an analyzer instance for the requested contact-angle method. - - Parameters - ---------- - method : str - Analysis method; one of ``"slicing"`` or ``"binning"``. - parser : BaseParser - Trajectory parser instance. - **kwargs - Forwarded to the selected analyzer constructor. - - Returns - ------- - BaseContactAngleAnalyzer - Configured analyzer ready to call ``analyze()``. - """ - if method == "slicing": - return SlicingContactAngleAnalyzer(parser=parser, **kwargs) - elif method == "binning": - return BinningContactAngleAnalyzer(parser=parser, **kwargs) - else: - raise ValueError(f"Unknown method '{method}'. Expected 'slicing' or 'binning'.") - - __all__ = [ "BaseContactAngleAnalyzer", "SlicingContactAngleAnalyzer", "BinningContactAngleAnalyzer", - "contact_angle_analyzer", "ContactAngleBinning", "ContactAngleSlicing", "ContactAngleSlicingParallel", diff --git a/src/wetting_angle_kit/analysis/binning/angle_fitting.py b/src/wetting_angle_kit/analysis/binning/angle_fitting.py index d2d6417..f1bae4a 100644 --- a/src/wetting_angle_kit/analysis/binning/angle_fitting.py +++ b/src/wetting_angle_kit/analysis/binning/angle_fitting.py @@ -85,6 +85,11 @@ def __init__( satisfy the precondition will produce wrong results. """ validate_droplet_geometry(droplet_geometry) + if droplet_geometry == "spherical" and width_cylinder is not None: + raise ValueError( + "width_cylinder must not be set for spherical analysis " + "(it is only valid for cylinder_x / cylinder_y)." + ) self.parser = parser self.atom_indices = atom_indices self.droplet_geometry = droplet_geometry diff --git a/src/wetting_angle_kit/analysis/slicing/angle_fitting.py b/src/wetting_angle_kit/analysis/slicing/angle_fitting.py index 7714bae..319d71d 100644 --- a/src/wetting_angle_kit/analysis/slicing/angle_fitting.py +++ b/src/wetting_angle_kit/analysis/slicing/angle_fitting.py @@ -1,4 +1,3 @@ -import warnings from collections.abc import Sequence import numpy as np @@ -67,6 +66,26 @@ def __init__( Azimuthal spacing (degrees) between radial lines. """ validate_droplet_geometry(droplet_geometry) + if droplet_geometry == "spherical": + if delta_gamma is None: + raise ValueError("delta_gamma must be provided for spherical analysis") + if delta_cylinder is not None or width_cylinder is not None: + raise ValueError( + "delta_cylinder and width_cylinder must not be set for " + "spherical analysis (they are only valid for " + "cylinder_x / cylinder_y)." + ) + else: # cylinder_x / cylinder_y + if delta_gamma is not None: + raise ValueError( + f"delta_gamma must not be set for {droplet_geometry} " + "(it is only valid for spherical)." + ) + if delta_cylinder is None or width_cylinder is None: + raise ValueError( + "delta_cylinder and width_cylinder must be provided for " + f"{droplet_geometry}." + ) self.liquid_coordinates = liquid_coordinates self.max_dist = max_dist # Store a copy: predict_contact_angle mutates this in-place per slice @@ -87,17 +106,6 @@ def __init__( # at room temperature by default; adjust for other liquids. self.density_sigma = density_sigma self.delta_angle = delta_angle - if self.droplet_geometry in ("cylinder_y", "cylinder_x") and ( - width_cylinder is None or delta_cylinder is None - ): - warnings.warn( - "width_cylinder and delta_cylinder recommended for " - f"{self.droplet_geometry}", - UserWarning, - stacklevel=2, - ) - if self.droplet_geometry == "spherical" and delta_gamma is None: - raise ValueError("delta_gamma must be provided for spherical analysis") def calculate_y_axis_list(self) -> list[float]: """Return axis position list for the chosen droplet geometry. diff --git a/src/wetting_angle_kit/analysis/slicing/parallel.py b/src/wetting_angle_kit/analysis/slicing/parallel.py index d7a5ec6..ea90f41 100644 --- a/src/wetting_angle_kit/analysis/slicing/parallel.py +++ b/src/wetting_angle_kit/analysis/slicing/parallel.py @@ -7,7 +7,11 @@ import numpy as np from wetting_angle_kit.analysis.slicing.results import SlicingResults -from wetting_angle_kit.io_utils import detect_parser_type, recenter_droplet_pbc +from wetting_angle_kit.io_utils import ( + detect_parser_type, + recenter_droplet_pbc, + validate_droplet_geometry, +) from wetting_angle_kit.parsers import BaseParser # "spawn" is required because parser instances may hold un-picklable handles @@ -91,6 +95,25 @@ def __init__( # parser type inside their subprocesses and would otherwise swallow the # error, returning empty results for every frame. detect_parser_type(filename) + validate_droplet_geometry(droplet_geometry) + if droplet_geometry == "spherical": + if delta_gamma is None: + raise ValueError("delta_gamma must be provided for spherical analysis") + if delta_cylinder is not None: + raise ValueError( + "delta_cylinder must not be set for spherical analysis " + "(it is only valid for cylinder_x / cylinder_y)." + ) + elif droplet_geometry in ("cylinder_x", "cylinder_y"): + if delta_cylinder is None: + raise ValueError( + f"delta_cylinder must be provided for {droplet_geometry}." + ) + if delta_gamma is not None: + raise ValueError( + f"delta_gamma must not be set for {droplet_geometry} " + "(it is only valid for spherical)." + ) self.filename = filename self.delta_gamma = delta_gamma self.delta_cylinder = delta_cylinder diff --git a/src/wetting_angle_kit/visualization/animator.py b/src/wetting_angle_kit/visualization/animator.py index 039d6c8..8933231 100644 --- a/src/wetting_angle_kit/visualization/animator.py +++ b/src/wetting_angle_kit/visualization/animator.py @@ -122,6 +122,11 @@ def generate_animation( width_cylinder=self.width_cylinder, ) angles, surfaces, popt_arrays = processor.predict_contact_angle() + if not angles: + # No slice in this frame produced a usable contact angle + # (e.g. fitting failed everywhere). Skip the frame rather + # than letting the median lookup crash on an empty list. + continue median_idx = np.argsort(angles)[len(angles) // 2] alpha = angles[median_idx] popt = popt_arrays[median_idx] @@ -152,6 +157,10 @@ def generate_animation( ) frames_list.append(frame) frame_labels.append(f"Frame {frame_idx}") + if not frames_list: + raise RuntimeError( + "No frame produced a usable contact angle; cannot build animation." + ) fig.frames = frames_list fig.add_traces(frames_list[0].data) fig.update_layout( diff --git a/tests/test_analysis/test_binning_method.py b/tests/test_analysis/test_binning_method.py index d915055..8e34c2c 100644 --- a/tests/test_analysis/test_binning_method.py +++ b/tests/test_analysis/test_binning_method.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from wetting_angle_kit.analysis import contact_angle_analyzer +from wetting_angle_kit.analysis import BinningContactAngleAnalyzer from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder @@ -52,8 +52,7 @@ def binning_params(): def test_binning_contact_angle_analyzer_with_real_data( filename, oxygen_indices, binning_params ): - analyzer = contact_angle_analyzer( - method="binning", + analyzer = BinningContactAngleAnalyzer( parser=LammpsDumpParser(filename), atom_indices=oxygen_indices, droplet_geometry="cylinder_y", @@ -77,8 +76,7 @@ def test_binning_contact_angle_analyzer_with_real_data( def test_binning_contact_angle_analyzer_per_frame_with_split_factor( filename, oxygen_indices, binning_params ): - analyzer = contact_angle_analyzer( - method="binning", + analyzer = BinningContactAngleAnalyzer( parser=LammpsDumpParser(filename), atom_indices=oxygen_indices, droplet_geometry="cylinder_y", diff --git a/tests/test_analysis/test_slicing_edge_cases.py b/tests/test_analysis/test_slicing_edge_cases.py index bb725b2..12041bc 100644 --- a/tests/test_analysis/test_slicing_edge_cases.py +++ b/tests/test_analysis/test_slicing_edge_cases.py @@ -54,8 +54,8 @@ def test_spherical_constructor_requires_delta_gamma(): _simple_predictor(droplet_geometry="spherical") -def test_cylinder_constructor_warns_without_widths(): - with pytest.warns(UserWarning, match="recommended"): +def test_cylinder_constructor_raises_without_widths(): + with pytest.raises(ValueError, match="delta_cylinder and width_cylinder"): _simple_predictor(droplet_geometry="cylinder_y") @@ -105,13 +105,21 @@ def test_calculate_y_axis_spherical(): def test_create_batches_few_frames(): # filename only needs a recognized extension; the file does not have to exist # for _create_batches, which is pure logic on the requested frame list. - parallel = ContactAngleSlicingParallel(filename="ignored.lammpstrj") + parallel = ContactAngleSlicingParallel( + filename="ignored.lammpstrj", + droplet_geometry="spherical", + delta_gamma=20.0, + ) # num_batches >= len(frames) → one frame per batch assert parallel._create_batches([1, 2, 3], num_batches=4) == [[1], [2], [3]] def test_create_batches_many_frames(): - parallel = ContactAngleSlicingParallel(filename="ignored.lammpstrj") + parallel = ContactAngleSlicingParallel( + filename="ignored.lammpstrj", + droplet_geometry="spherical", + delta_gamma=20.0, + ) batches = parallel._create_batches(list(range(10)), num_batches=3) flat = [f for batch in batches for f in batch] assert flat == list(range(10)) diff --git a/tests/test_analysis/test_slicing_method.py b/tests/test_analysis/test_slicing_method.py index cc28ae1..3b1def7 100644 --- a/tests/test_analysis/test_slicing_method.py +++ b/tests/test_analysis/test_slicing_method.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from wetting_angle_kit.analysis import contact_angle_analyzer +from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder @@ -76,8 +76,7 @@ def test_contact_angle_slicing_with_real_data(parser, oxygen_indices): @pytest.mark.integration @pytest.mark.slow def test_slicing_contact_angle_analyzer_with_real_data(filename, oxygen_indices): - analyzer = contact_angle_analyzer( - method="slicing", + analyzer = SlicingContactAngleAnalyzer( parser=LammpsDumpParser(filename), atom_indices=oxygen_indices, droplet_geometry="spherical", diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index f362e47..95bbe65 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -59,11 +59,11 @@ def test_contact_angle_slicing_copies_geometric_center(): np.testing.assert_array_equal(center, np.array([1.0, 2.0, 3.0])) -# --- Cylindrical mode without delta_cylinder/width_cylinder warns --- +# --- Cylindrical mode without delta_cylinder/width_cylinder raises --- -def test_slicing_cylinder_without_width_warns(): - with pytest.warns(UserWarning, match="width_cylinder and delta_cylinder"): +def test_slicing_cylinder_without_width_raises(): + with pytest.raises(ValueError, match="delta_cylinder and width_cylinder"): ContactAngleSlicing( liquid_coordinates=np.zeros((3, 3)), max_dist=10, @@ -117,16 +117,6 @@ def test_hyperbolic_tangent_compute_isoline_raises_for_unphysical_fit(): model.compute_isoline() -# --- Factory rejects unknown methods --- - - -def test_contact_angle_analyzer_factory_rejects_unknown_method(): - from wetting_angle_kit.analysis import contact_angle_analyzer - - with pytest.raises(ValueError, match="Unknown method"): - contact_angle_analyzer(method="not-a-method", parser=object()) - - # --- ContactAngleBinning.get_profile_coordinates --- diff --git a/tests/test_visualization/test_droplet_slice_plot.py b/tests/test_visualization/test_droplet_slice_plot.py index a860e1a..437618d 100644 --- a/tests/test_visualization/test_droplet_slice_plot.py +++ b/tests/test_visualization/test_droplet_slice_plot.py @@ -122,11 +122,11 @@ def test_contact_angle_animator_init_loads_fixture(): @pytest.mark.slow def test_contact_angle_animator_generates_html(tmp_path): - """Smoke-test ContactAngleAnimator on the LAMMPS fixture via cylinder_y geometry.""" + """Smoke-test ContactAngleAnimator on the cylindrical LAMMPS fixture.""" pytest.importorskip("ovito") output = tmp_path / "animation.html" animator = ContactAngleAnimator( - filename=trajectory_path("traj_spherical_drop_4k.lammpstrj"), + filename=trajectory_path("traj_10_3_330w_nve_4k_reajust.lammpstrj"), particle_type_wall={3}, oxygen_type=1, hydrogen_type=2, @@ -135,7 +135,7 @@ def test_contact_angle_animator_generates_html(tmp_path): droplet_geometry="cylinder_y", delta_cylinder=20, max_dist=50, - width_cylinder=20, + width_cylinder=21, ) animator.generate_animation(output_filename=str(output)) assert output.exists() From 18228155fb0a11e531fca8cf696c156b71052386 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Mon, 1 Jun 2026 13:50:35 +0200 Subject: [PATCH 05/31] Refactored analysis classes and parallelism for slicing. --- docs/examples/binning_ca.py | 4 +- docs/examples/slicing_ca.py | 4 +- docs/examples/visualisation_slicing_traj.py | 4 +- docs/source/API/index.rst | 4 +- docs/source/introduction/Introduction.rst | 4 +- docs/source/tutorials/Binning_method_tuto.rst | 4 +- docs/source/tutorials/Slicing_method_tuto.rst | 8 +- .../Visualization_slicing_droplet.rst | 4 +- docs/tutorials/Binning_method_tuto.md | 4 +- docs/tutorials/Slicing_method_tuto.md | 8 +- .../Visualization_slicing_droplet.md | 4 +- src/wetting_angle_kit/analysis/__init__.py | 28 +- src/wetting_angle_kit/analysis/analyzer.py | 134 +------ .../analysis/binning/__init__.py | 11 +- .../analysis/binning/analyzer.py | 96 +++++ .../analysis/binning/angle_fitting.py | 2 +- .../analysis/slicing/__init__.py | 12 +- .../analysis/slicing/analyzer.py | 296 +++++++++++++++ .../analysis/slicing/angle_fitting.py | 2 +- .../analysis/slicing/parallel.py | 337 ------------------ .../visualization/animator.py | 6 +- tests/test_analysis/test_binning_method.py | 8 +- .../test_analysis/test_slicing_edge_cases.py | 76 ++-- tests/test_analysis/test_slicing_method.py | 14 +- tests/test_edge_cases.py | 22 +- 25 files changed, 508 insertions(+), 588 deletions(-) create mode 100644 src/wetting_angle_kit/analysis/binning/analyzer.py create mode 100644 src/wetting_angle_kit/analysis/slicing/analyzer.py delete mode 100644 src/wetting_angle_kit/analysis/slicing/parallel.py diff --git a/docs/examples/binning_ca.py b/docs/examples/binning_ca.py index 87256a6..dc1801f 100644 --- a/docs/examples/binning_ca.py +++ b/docs/examples/binning_ca.py @@ -1,5 +1,5 @@ # Import necessary modules -from wetting_angle_kit.analysis import BinningContactAngleAnalyzer +from wetting_angle_kit.analysis import BinningTrajectoryAnalyzer from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder # --- Step 1: Define the trajectory file --- @@ -32,7 +32,7 @@ parser = LammpsDumpParser(filename) # --- Step 6: Create the contact angle analyzer --- -analyzer = BinningContactAngleAnalyzer( +analyzer = BinningTrajectoryAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="spherical", # Interface fitting model diff --git a/docs/examples/slicing_ca.py b/docs/examples/slicing_ca.py index 81dbf68..900e375 100644 --- a/docs/examples/slicing_ca.py +++ b/docs/examples/slicing_ca.py @@ -4,7 +4,7 @@ file and prints the resulting mean contact angle. """ -from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer +from wetting_angle_kit.analysis import SlicingTrajectoryAnalyzer from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder # --- Step 1: Define the trajectory file --- @@ -24,7 +24,7 @@ # --- Step 3: Build the slicing analyzer --- parser = LammpsDumpParser(filename) -analyzer = SlicingContactAngleAnalyzer( +analyzer = SlicingTrajectoryAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="spherical", diff --git a/docs/examples/visualisation_slicing_traj.py b/docs/examples/visualisation_slicing_traj.py index ded3cf9..955a3fd 100644 --- a/docs/examples/visualisation_slicing_traj.py +++ b/docs/examples/visualisation_slicing_traj.py @@ -7,7 +7,7 @@ import numpy as np -from wetting_angle_kit.analysis.slicing import ContactAngleSlicing +from wetting_angle_kit.analysis.slicing import SlicingFrameFitter from wetting_angle_kit.parsers import ( LammpsDumpParser, LammpsDumpWallParser, @@ -36,7 +36,7 @@ wall_coords = coord_wall.parse(frame_index=10) # --- 4. Compute Contact Angles --- -processor = ContactAngleSlicing( +processor = SlicingFrameFitter( liquid_coordinates=oxygen_position, liquid_geom_center=np.mean(oxygen_position, axis=0), droplet_geometry="cylinder_y", diff --git a/docs/source/API/index.rst b/docs/source/API/index.rst index 97e82cc..8542b65 100644 --- a/docs/source/API/index.rst +++ b/docs/source/API/index.rst @@ -28,7 +28,7 @@ Slicing Method :members: :undoc-members: :show-inheritance: - :exclude-members: ContactAngleSlicing, ContactAngleSlicingParallel + :exclude-members: SlicingFrameFitter Binning Method ^^^^^^^^^^^^^ @@ -37,7 +37,7 @@ Binning Method :members: :undoc-members: :show-inheritance: - :exclude-members: ContactAngleBinning + :exclude-members: BinningBatchFitter Visualization and Statistics ----------------------------- diff --git a/docs/source/introduction/Introduction.rst b/docs/source/introduction/Introduction.rst index 288ee7b..349609c 100644 --- a/docs/source/introduction/Introduction.rst +++ b/docs/source/introduction/Introduction.rst @@ -112,10 +112,10 @@ Examples of these visualizations can be found in the respective tutorials for ea Troubleshooting --------------- -* **NaN angles**: Usually occur when the surface filter removes too many points (empty slice). Adjust ``surface_filter_offset`` (default 2.0) in ``ContactAngleSlicing`` or relax slice width. Ensure enough atoms remain after filtering (>=3) for circle fitting. +* **NaN angles**: Usually occur when the surface filter removes too many points (empty slice). Adjust ``surface_filter_offset`` (default 2.0) in ``SlicingFrameFitter`` or relax slice width. Ensure enough atoms remain after filtering (>=3) for circle fitting. * **Empty outputs / NoneType failures**: Confirm ``width_cylinder`` and ``delta_cylinder`` are passed for cylindrical models and ``delta_gamma`` for spherical model. Parser must supply box dimensions for automatic max distance estimation. -* **Multiprocessing hangs**: Use the batch-parallel wrapper (``ContactAngleSlicingParallel.process_frames_parallel``) which employs spawn start method; avoid invoking OVITO parsers inside global contexts before multiprocessing starts. +* **Multiprocessing hangs**: ``SlicingTrajectoryAnalyzer.analyze`` uses the spawn start method; avoid invoking OVITO parsers inside global contexts before multiprocessing starts. * **OVITO ImportError**: Install with the ovito extra or via the Conda command listed above. Verify channel priority and version pin if dependency resolution fails. diff --git a/docs/source/tutorials/Binning_method_tuto.rst b/docs/source/tutorials/Binning_method_tuto.rst index 78b5e1c..51275cf 100644 --- a/docs/source/tutorials/Binning_method_tuto.rst +++ b/docs/source/tutorials/Binning_method_tuto.rst @@ -39,7 +39,7 @@ Example trajectory:: # Import necessary modules from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder - from wetting_angle_kit.analysis import BinningContactAngleAnalyzer + from wetting_angle_kit.analysis import BinningTrajectoryAnalyzer # --- Step 1: Define the trajectory file --- filename = "../../tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj" @@ -71,7 +71,7 @@ Example trajectory:: parser = LammpsDumpParser(filename) # --- Step 6: Create the contact angle analyzer --- - analyzer = BinningContactAngleAnalyzer( + analyzer = BinningTrajectoryAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="cylinder_y", # Interface fitting model diff --git a/docs/source/tutorials/Slicing_method_tuto.rst b/docs/source/tutorials/Slicing_method_tuto.rst index 59242d3..b00209d 100644 --- a/docs/source/tutorials/Slicing_method_tuto.rst +++ b/docs/source/tutorials/Slicing_method_tuto.rst @@ -35,7 +35,7 @@ Example trajectory:: # Import necessary modules from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder - from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer + from wetting_angle_kit.analysis import SlicingTrajectoryAnalyzer # --- Step 1: Define the trajectory file --- filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj" @@ -57,7 +57,7 @@ Example trajectory:: # --- Step 5: Create the contact angle analyzer --- # Using the slicing method with a spherical model - analyzer = SlicingContactAngleAnalyzer( + analyzer = SlicingTrajectoryAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="spherical", # Geometry fitting model @@ -133,7 +133,7 @@ following convenience attributes: """ from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder - from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer + from wetting_angle_kit.analysis import SlicingTrajectoryAnalyzer # --- Step 1: Define input trajectory --- filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj" @@ -150,7 +150,7 @@ following convenience attributes: parser = LammpsDumpParser(filename) # --- Step 4: Create analyzer for the slicing method --- - analyzer = SlicingContactAngleAnalyzer( + analyzer = SlicingTrajectoryAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="spherical", # Fitting model diff --git a/docs/source/tutorials/Visualization_slicing_droplet.rst b/docs/source/tutorials/Visualization_slicing_droplet.rst index 911c70f..7a0276a 100644 --- a/docs/source/tutorials/Visualization_slicing_droplet.rst +++ b/docs/source/tutorials/Visualization_slicing_droplet.rst @@ -28,7 +28,7 @@ The visualization workflow involves the following steps: LammpsDumpWaterFinder, LammpsDumpWallParser, ) - from wetting_angle_kit.analysis.slicing import ContactAngleSlicing + from wetting_angle_kit.analysis.slicing import SlicingFrameFitter from wetting_angle_kit.visualization import DropletSlicePlotter ---- @@ -78,7 +78,7 @@ The visualization workflow involves the following steps: .. code-block:: python - processor = ContactAngleSlicing( + processor = SlicingFrameFitter( liquid_coordinates=oxygen_position, liquid_geom_center=np.mean(oxygen_position, axis=0), droplet_geometry="cylinder_y", diff --git a/docs/tutorials/Binning_method_tuto.md b/docs/tutorials/Binning_method_tuto.md index 9dd825a..1fbcdce 100644 --- a/docs/tutorials/Binning_method_tuto.md +++ b/docs/tutorials/Binning_method_tuto.md @@ -33,7 +33,7 @@ tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj ```python # Import necessary modules from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder -from wetting_angle_kit.analysis import BinningContactAngleAnalyzer +from wetting_angle_kit.analysis import BinningTrajectoryAnalyzer # --- Step 1: Define the trajectory file --- filename = "../../tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj" @@ -65,7 +65,7 @@ binning_params = { parser = LammpsDumpParser(filename) # --- Step 6: Create the contact angle analyzer --- -analyzer = BinningContactAngleAnalyzer( +analyzer = BinningTrajectoryAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="cylinder_y", # Interface fitting model diff --git a/docs/tutorials/Slicing_method_tuto.md b/docs/tutorials/Slicing_method_tuto.md index aaea597..707584a 100644 --- a/docs/tutorials/Slicing_method_tuto.md +++ b/docs/tutorials/Slicing_method_tuto.md @@ -30,7 +30,7 @@ tests/trajectories/traj_spherical_drop_4k.lammpstrj ````python # Import necessary modules from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder -from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer +from wetting_angle_kit.analysis import SlicingTrajectoryAnalyzer # --- Step 1: Define the trajectory file --- filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj" @@ -51,7 +51,7 @@ parser = LammpsDumpParser(filename) # --- Step 5: Create the contact angle analyzer --- # Using the slicing method with a spherical model -analyzer = SlicingContactAngleAnalyzer( +analyzer = SlicingTrajectoryAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry='spherical', # Geometry fitting model @@ -112,7 +112,7 @@ using the 'slicing' method on a spherical droplet from a LAMMPS dump trajectory. """ from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder -from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer +from wetting_angle_kit.analysis import SlicingTrajectoryAnalyzer # --- Step 1: Define input trajectory --- filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj" @@ -132,7 +132,7 @@ print(f"Number of water molecules: {len(oxygen_indices)}") parser = LammpsDumpParser(filename) # --- Step 4: Create analyzer for the slicing method --- -analyzer = SlicingContactAngleAnalyzer( +analyzer = SlicingTrajectoryAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry='spherical', diff --git a/docs/tutorials/Visualization_slicing_droplet.md b/docs/tutorials/Visualization_slicing_droplet.md index 649ba79..e8ed106 100644 --- a/docs/tutorials/Visualization_slicing_droplet.md +++ b/docs/tutorials/Visualization_slicing_droplet.md @@ -23,7 +23,7 @@ from wetting_angle_kit.parsers import ( LammpsDumpWaterFinder, LammpsDumpWallParser, ) -from wetting_angle_kit.analysis.slicing import ContactAngleSlicing +from wetting_angle_kit.analysis.slicing import SlicingFrameFitter from wetting_angle_kit.visualization import DropletSlicePlotter ``` @@ -63,7 +63,7 @@ wall_coords = coord_wall.parse(frame_index=1) ## 6. Compute Contact Angles ```python -processor = ContactAngleSlicing( +processor = SlicingFrameFitter( liquid_coordinates=oxygen_position, liquid_geom_center=np.mean(oxygen_position, axis=0), droplet_geometry="cylinder_y", diff --git a/src/wetting_angle_kit/analysis/__init__.py b/src/wetting_angle_kit/analysis/__init__.py index 290f1c3..7333577 100644 --- a/src/wetting_angle_kit/analysis/__init__.py +++ b/src/wetting_angle_kit/analysis/__init__.py @@ -1,25 +1,23 @@ """Contact-angle analysis orchestrators and per-method engines.""" -from wetting_angle_kit.analysis.analyzer import ( - BaseContactAngleAnalyzer, - BinningContactAngleAnalyzer, - SlicingContactAngleAnalyzer, +from wetting_angle_kit.analysis.analyzer import BaseTrajectoryAnalyzer +from wetting_angle_kit.analysis.binning.analyzer import ( + BinningTrajectoryAnalyzer, ) from wetting_angle_kit.analysis.binning.angle_fitting import ( - ContactAngleBinning, + BinningBatchFitter, ) -from wetting_angle_kit.analysis.slicing.angle_fitting import ( - ContactAngleSlicing, +from wetting_angle_kit.analysis.slicing.analyzer import ( + SlicingTrajectoryAnalyzer, ) -from wetting_angle_kit.analysis.slicing.parallel import ( - ContactAngleSlicingParallel, +from wetting_angle_kit.analysis.slicing.angle_fitting import ( + SlicingFrameFitter, ) __all__ = [ - "BaseContactAngleAnalyzer", - "SlicingContactAngleAnalyzer", - "BinningContactAngleAnalyzer", - "ContactAngleBinning", - "ContactAngleSlicing", - "ContactAngleSlicingParallel", + "BaseTrajectoryAnalyzer", + "SlicingTrajectoryAnalyzer", + "BinningTrajectoryAnalyzer", + "BinningBatchFitter", + "SlicingFrameFitter", ] diff --git a/src/wetting_angle_kit/analysis/analyzer.py b/src/wetting_angle_kit/analysis/analyzer.py index c55089a..03b6e84 100644 --- a/src/wetting_angle_kit/analysis/analyzer.py +++ b/src/wetting_angle_kit/analysis/analyzer.py @@ -1,139 +1,13 @@ +"""Abstract base class for contact-angle analyzers.""" + from abc import ABC, abstractmethod from typing import Any -from wetting_angle_kit.analysis.binning.angle_fitting import ( - ContactAngleBinning, -) -from wetting_angle_kit.analysis.binning.results import BinningResults -from wetting_angle_kit.analysis.slicing.parallel import ( - ContactAngleSlicingParallel, -) -from wetting_angle_kit.analysis.slicing.results import SlicingResults - -class BaseContactAngleAnalyzer(ABC): +class BaseTrajectoryAnalyzer(ABC): """Abstract base for contact angle analysis across trajectory files.""" @abstractmethod - def analyze(self, frame_range: list[int] | None = None, **kwargs: Any) -> Any: + def analyze(self, frame_range: list[int] | None = None) -> Any: """Run the analysis and return a method-specific results object.""" pass - - @abstractmethod - def get_method_name(self) -> str: - """Return the method name identifier.""" - pass - - -class SlicingContactAngleAnalyzer(BaseContactAngleAnalyzer): - """BaseContactAngleAnalyzer implementation using the slicing parallel method.""" - - def __init__( - self, - parser: Any, - **kwargs: Any, - ): - """ - Parameters - ---------- - parser : BaseParser - Trajectory parser instance. - **kwargs - Forwarded to ContactAngleSlicingParallel. - """ - self.parser = parser - self._processor = ContactAngleSlicingParallel( - filename=parser.filepath, **kwargs - ) - - def analyze( - self, frame_range: list[int] | None = None, **kwargs: Any - ) -> SlicingResults: - """Run the slicing parallel analysis. - - Parameters - ---------- - frame_range : list[int], optional - Frame indices to process. If None, all frames are used. - **kwargs - Forwarded to ``process_frames_parallel``. - - Returns - ------- - SlicingResults - In-memory per-frame angles, surface contours and circle fits. - """ - if frame_range is None: - frame_range = list(range(self.parser.frame_count())) - return self._processor.process_frames_parallel( - frames_to_process=frame_range, **kwargs - ) - - def get_method_name(self) -> str: - """Return "slicing_parallel".""" - return "slicing_parallel" - - -class BinningContactAngleAnalyzer(BaseContactAngleAnalyzer): - """BaseContactAngleAnalyzer implementation using the density-binning method.""" - - def __init__(self, parser: Any, **kwargs: Any) -> None: - """ - Parameters - ---------- - parser : BaseParser - Trajectory parser instance. - **kwargs - Forwarded to ContactAngleBinning. - """ - self.parser = parser - self._analyzer = ContactAngleBinning(parser=parser, **kwargs) - - def analyze( - self, - frame_range: list[int] | None = None, - split_factor: int | None = None, - **kwargs: Any, - ) -> BinningResults: - """Run the binning analysis. - - Parameters - ---------- - frame_range : list[int], optional - Frame indices to process. If None, all frames are used. - split_factor : int, optional - If given, split ``frame_range`` into sub-batches of this size and - compute one angle per batch; if None, all frames form a single batch. - **kwargs - Reserved for future use. - - Returns - ------- - BinningResults - Per-batch contact angles, density fields and isoline data. - """ - if frame_range is None: - frame_range = list(range(self.parser.frame_count())) - if split_factor is None: - batch = self._analyzer.process_batch(frame_range) - return BinningResults( - batches=[batch], - method_metadata={"frames_per_angle": len(frame_range)}, - ) - batches = [] - for batch_idx, start in enumerate(range(0, len(frame_range), split_factor)): - end = min(start + split_factor, len(frame_range)) - batches.append( - self._analyzer.process_batch( - frame_range[start:end], - batch_index=batch_idx + 1, - ) - ) - return BinningResults( - batches=batches, - method_metadata={"frames_per_trajectory": split_factor}, - ) - - def get_method_name(self) -> str: - """Return "binning_density".""" - return "binning_density" diff --git a/src/wetting_angle_kit/analysis/binning/__init__.py b/src/wetting_angle_kit/analysis/binning/__init__.py index 0e1b379..c3b400c 100644 --- a/src/wetting_angle_kit/analysis/binning/__init__.py +++ b/src/wetting_angle_kit/analysis/binning/__init__.py @@ -1,10 +1,17 @@ """Public exports for binning contact angle method.""" +from wetting_angle_kit.analysis.binning.analyzer import ( + BinningTrajectoryAnalyzer, +) from wetting_angle_kit.analysis.binning.angle_fitting import ( - ContactAngleBinning, + BinningBatchFitter, ) from wetting_angle_kit.analysis.binning.surface_definition import ( HyperbolicTangentModel, ) -__all__ = ["ContactAngleBinning", "HyperbolicTangentModel"] +__all__ = [ + "BinningTrajectoryAnalyzer", + "BinningBatchFitter", + "HyperbolicTangentModel", +] diff --git a/src/wetting_angle_kit/analysis/binning/analyzer.py b/src/wetting_angle_kit/analysis/binning/analyzer.py new file mode 100644 index 0000000..3685bc6 --- /dev/null +++ b/src/wetting_angle_kit/analysis/binning/analyzer.py @@ -0,0 +1,96 @@ +"""Trajectory-level binning contact-angle analyzer.""" + +from typing import Any + +from wetting_angle_kit.analysis.analyzer import BaseTrajectoryAnalyzer +from wetting_angle_kit.analysis.binning.angle_fitting import ( + BinningBatchFitter, +) +from wetting_angle_kit.analysis.binning.results import BinningResults + + +class BinningTrajectoryAnalyzer(BaseTrajectoryAnalyzer): + """BaseTrajectoryAnalyzer implementation using the density-binning method.""" + + def __init__( + self, + parser: Any, + atom_indices: Any, + droplet_geometry: str = "spherical", + width_cylinder: float | None = None, + binning_params: dict[str, Any] | None = None, + precentered: bool = False, + ) -> None: + """ + Parameters + ---------- + parser : BaseParser + Trajectory parser providing coordinates and box dimensions. + atom_indices : Any + Indices (or IDs) of liquid atoms to include in the density field. + droplet_geometry : str, default "spherical" + One of ``"spherical"``, ``"cylinder_x"``, ``"cylinder_y"``. + width_cylinder : float, optional + Box length along the cylinder axis; inferred from the parser if None. + binning_params : dict, optional + Grid definition with keys ``xi_0``, ``xi_f``, ``nbins_xi``, + ``zi_0``, ``zi_f``, ``nbins_zi``. A heuristic default is used if None. + precentered : bool, default False + Skip per-frame circular-mean PBC recentering. Setting this on a + trajectory that does NOT satisfy the precondition will produce + wrong results. + """ + self.parser = parser + self._analyzer = BinningBatchFitter( + parser=parser, + atom_indices=atom_indices, + droplet_geometry=droplet_geometry, + width_cylinder=width_cylinder, + binning_params=binning_params, + precentered=precentered, + ) + + def analyze( + self, + frame_range: list[int] | None = None, + split_factor: int | None = None, + **kwargs: Any, + ) -> BinningResults: + """Run the binning analysis. + + Parameters + ---------- + frame_range : list[int], optional + Frame indices to process. If None, all frames are used. + split_factor : int, optional + If given, split ``frame_range`` into sub-batches of this size and + compute one angle per batch; if None, all frames form a single batch. + **kwargs + Reserved for future use. + + Returns + ------- + BinningResults + Per-batch contact angles, density fields and isoline data. + """ + if frame_range is None: + frame_range = list(range(self.parser.frame_count())) + if split_factor is None: + batch = self._analyzer.process_batch(frame_range) + return BinningResults( + batches=[batch], + method_metadata={"frames_per_angle": len(frame_range)}, + ) + batches = [] + for batch_idx, start in enumerate(range(0, len(frame_range), split_factor)): + end = min(start + split_factor, len(frame_range)) + batches.append( + self._analyzer.process_batch( + frame_range[start:end], + batch_index=batch_idx + 1, + ) + ) + return BinningResults( + batches=batches, + method_metadata={"frames_per_trajectory": split_factor}, + ) diff --git a/src/wetting_angle_kit/analysis/binning/angle_fitting.py b/src/wetting_angle_kit/analysis/binning/angle_fitting.py index f1bae4a..de05c42 100644 --- a/src/wetting_angle_kit/analysis/binning/angle_fitting.py +++ b/src/wetting_angle_kit/analysis/binning/angle_fitting.py @@ -45,7 +45,7 @@ _PARAM_NAMES = ("rho1", "rho2", "R_eq", "zi_c", "zi_0", "t1", "t2") -class ContactAngleBinning: +class BinningBatchFitter: """Binning-based contact angle estimator using density field fitting. Frames aggregated in spatial bins form a time-averaged density field. diff --git a/src/wetting_angle_kit/analysis/slicing/__init__.py b/src/wetting_angle_kit/analysis/slicing/__init__.py index cdea9c4..770bf52 100644 --- a/src/wetting_angle_kit/analysis/slicing/__init__.py +++ b/src/wetting_angle_kit/analysis/slicing/__init__.py @@ -1,17 +1,17 @@ """Public exports for the slicing contact angle method.""" -from wetting_angle_kit.analysis.slicing.angle_fitting import ( - ContactAngleSlicing, +from wetting_angle_kit.analysis.slicing.analyzer import ( + SlicingTrajectoryAnalyzer, ) -from wetting_angle_kit.analysis.slicing.parallel import ( - ContactAngleSlicingParallel, +from wetting_angle_kit.analysis.slicing.angle_fitting import ( + SlicingFrameFitter, ) from wetting_angle_kit.analysis.slicing.surface_definition import ( SurfaceDefinition, ) __all__ = [ - "ContactAngleSlicing", - "ContactAngleSlicingParallel", + "SlicingFrameFitter", + "SlicingTrajectoryAnalyzer", "SurfaceDefinition", ] diff --git a/src/wetting_angle_kit/analysis/slicing/analyzer.py b/src/wetting_angle_kit/analysis/slicing/analyzer.py new file mode 100644 index 0000000..9482e44 --- /dev/null +++ b/src/wetting_angle_kit/analysis/slicing/analyzer.py @@ -0,0 +1,296 @@ +"""Trajectory-level slicing contact-angle analyzer.""" + +import logging +import multiprocessing as mp +from typing import Any, NamedTuple + +import numpy as np + +from wetting_angle_kit.analysis.analyzer import BaseTrajectoryAnalyzer +from wetting_angle_kit.analysis.slicing.angle_fitting import ( + SlicingFrameFitter, +) +from wetting_angle_kit.analysis.slicing.results import SlicingResults +from wetting_angle_kit.io_utils import ( + detect_parser_type, + recenter_droplet_pbc, + validate_droplet_geometry, +) +from wetting_angle_kit.parsers.ase import AseParser +from wetting_angle_kit.parsers.base import BaseParser +from wetting_angle_kit.parsers.lammps_dump import LammpsDumpParser +from wetting_angle_kit.parsers.xyz import XYZParser + +# "spawn" is required because parser instances may hold un-picklable handles +# (OVITO pipelines, ASE Atoms with C extensions). Using a scoped context +# rather than mutating the global start method keeps this side-effect-free +# when the package is imported. +_MP_CONTEXT = mp.get_context("spawn") + +logger = logging.getLogger(__name__) + + +class _SlicingFrameResult(NamedTuple): + """Per-frame output of the slicing worker.""" + + frame_num: int + mean_angle: float | None + angles: list + surfaces: list + popts: list + + +class SlicingTrajectoryAnalyzer(BaseTrajectoryAnalyzer): + """Trajectory-level slicing contact-angle analyzer. + + Frames are dispatched one-by-one to a ``multiprocessing.Pool`` whose + workers each build their own parser once and reuse it for every frame + they receive. The per-frame fitting work is delegated to + :class:`SlicingFrameFitter`. + """ + + # Per-worker state populated by ``_init_worker`` in each child process. + # In the parent this stays empty; ``spawn`` gives each child its own + # fresh module-level class object, so the dict is effectively per-process. + _WORKER_STATE: dict[str, Any] = {} + + def __init__( + self, + parser: Any, + droplet_geometry: str = "spherical", + atom_indices: np.ndarray | None = None, + delta_gamma: float | None = None, + delta_cylinder: float | None = None, + points_per_angstrom: float = 1.0, + precentered: bool = False, + ) -> None: + """ + Parameters + ---------- + parser : BaseParser + Trajectory parser instance. Only ``parser.filepath`` and + ``parser.frame_count()`` are read in the parent process; each + worker rebuilds its own parser from ``filepath``. + droplet_geometry : str, default "spherical" + One of ``"spherical"``, ``"cylinder_x"``, ``"cylinder_y"``. + atom_indices : ndarray, optional + Indices of liquid particles. Empty array selects none. + delta_gamma : float, optional + Azimuthal step (degrees) for spherical analysis (required if + ``droplet_geometry == "spherical"``). + delta_cylinder : float, optional + Slice spacing along the cylinder axis (required for + cylinder_x / cylinder_y). + points_per_angstrom : float, default 1.0 + Sampling density along each radial ray. + precentered : bool, default False + Skip per-frame circular-mean PBC recentering. Setting this on a + trajectory that does NOT satisfy the precondition will produce + wrong results. + """ + # Fail fast in the parent process so the user gets the error at + # construction instead of a uniform "all frames failed" later. + detect_parser_type(parser.filepath) + validate_droplet_geometry(droplet_geometry) + if droplet_geometry == "spherical": + if delta_gamma is None: + raise ValueError("delta_gamma must be provided for spherical analysis") + if delta_cylinder is not None: + raise ValueError( + "delta_cylinder must not be set for spherical analysis " + "(it is only valid for cylinder_x / cylinder_y)." + ) + elif droplet_geometry in ("cylinder_x", "cylinder_y"): + if delta_cylinder is None: + raise ValueError( + f"delta_cylinder must be provided for {droplet_geometry}." + ) + if delta_gamma is not None: + raise ValueError( + f"delta_gamma must not be set for {droplet_geometry} " + "(it is only valid for spherical)." + ) + self.parser = parser + self.droplet_geometry = droplet_geometry + self.atom_indices = atom_indices if atom_indices is not None else np.array([]) + self.delta_gamma = delta_gamma + self.delta_cylinder = delta_cylinder + self.points_per_angstrom = points_per_angstrom + self.precentered = precentered + + def analyze( + self, + frame_range: list[int] | None = None, + n_jobs: int | None = None, + ) -> SlicingResults: + """Run the slicing analysis in parallel across frames. + + Parameters + ---------- + frame_range : list[int], optional + Frame indices to process. Defaults to all frames. + n_jobs : int, optional + Number of worker processes. ``None`` lets ``multiprocessing.Pool`` + pick the default (``os.cpu_count()``). + + Returns + ------- + SlicingResults + Per-frame angles, surface contours, fit parameters and method + metadata. Frames whose worker failed to produce a mean angle are + omitted. + """ + if frame_range is None: + frame_range = list(range(self.parser.frame_count())) + if not frame_range: + return SlicingResults( + frames=[], + angles=[], + surfaces=[], + popts=[], + method_metadata={"frames_per_angle": 1}, + ) + init_args = ( + self.parser.filepath, + self.droplet_geometry, + self.atom_indices, + self.delta_gamma, + self.delta_cylinder, + self.points_per_angstrom, + self.precentered, + ) + logger.info(f"Processing {len(frame_range)} frames with n_jobs={n_jobs}") + results_by_frame: dict[int, _SlicingFrameResult] = {} + with _MP_CONTEXT.Pool( + processes=n_jobs, + initializer=self._init_worker, + initargs=init_args, + ) as pool: + for result in pool.imap_unordered(self._run_one_frame, frame_range): + if result.mean_angle is not None: + results_by_frame[result.frame_num] = result + sorted_frames = sorted(results_by_frame) + logger.info( + f"Successfully processed {len(sorted_frames)}/{len(frame_range)} frames" + ) + if not sorted_frames: + raise RuntimeError( + f"None of the {len(frame_range)} requested frames produced " + "any contact-angle slices. Check the worker logs above for the " + "underlying parser, geometry, or fit errors." + ) + return SlicingResults( + frames=sorted_frames, + angles=[np.asarray(results_by_frame[f].angles) for f in sorted_frames], + surfaces=[results_by_frame[f].surfaces for f in sorted_frames], + popts=[np.asarray(results_by_frame[f].popts) for f in sorted_frames], + method_metadata={"frames_per_angle": 1}, + ) + + @staticmethod + def _build_parser(filename: str) -> BaseParser: + parser_type = detect_parser_type(filename) + if parser_type == "dump": + return LammpsDumpParser(filepath=filename) + if parser_type == "ase": + return AseParser(filepath=filename) + if parser_type == "xyz": + return XYZParser(filepath=filename) + raise ValueError(f"Unsupported parser type: {parser_type}") + + @staticmethod + def _init_worker( + filename: str, + droplet_geometry: str, + atom_indices: np.ndarray, + delta_gamma: float | None, + delta_cylinder: float | None, + points_per_angstrom: float, + precentered: bool, + ) -> None: + cls = SlicingTrajectoryAnalyzer + cls._WORKER_STATE.clear() + cls._WORKER_STATE.update( + parser=cls._build_parser(filename), + droplet_geometry=droplet_geometry, + atom_indices=atom_indices, + delta_gamma=delta_gamma, + delta_cylinder=delta_cylinder, + points_per_angstrom=points_per_angstrom, + precentered=precentered, + ) + + @staticmethod + def _run_one_frame(frame_num: int) -> _SlicingFrameResult: + state = SlicingTrajectoryAnalyzer._WORKER_STATE + parser: BaseParser = state["parser"] + droplet_geometry: str = state["droplet_geometry"] + atom_indices: np.ndarray = state["atom_indices"] + delta_gamma = state["delta_gamma"] + delta_cylinder = state["delta_cylinder"] + points_per_angstrom: float = state["points_per_angstrom"] + precentered: bool = state["precentered"] + try: + liquid_positions = parser.parse( + frame_index=frame_num, + indices=atom_indices, + ) + max_dist = int( + np.max( + np.array( + [ + parser.box_size_y(frame_index=frame_num), + parser.box_size_x(frame_index=frame_num), + ] + ) + ) + / 2 + ) + # Fold the droplet into the minimum-image frame around its + # circular-mean COM before any cylinder_x axis swap, so the + # ``box_size`` argument is in the parser's native frame. This + # makes downstream radial sampling robust to droplets that + # straddle a periodic boundary, and is idempotent for + # trajectories already recentered during dynamics. Skipped + # (with a plain arithmetic mean) when the user has declared + # the trajectory pre-centered. + if precentered: + mean_liquid_position = np.mean(liquid_positions, axis=0) + else: + box_size_xy = ( + parser.box_size_x(frame_index=frame_num), + parser.box_size_y(frame_index=frame_num), + ) + liquid_positions, mean_liquid_position = recenter_droplet_pbc( + liquid_positions, droplet_geometry, box_size=box_size_xy + ) + if droplet_geometry == "cylinder_x": + liquid_positions = liquid_positions[:, [1, 0, 2]] + mean_liquid_position = mean_liquid_position[[1, 0, 2]] + box_dimensions = parser.box_size_x(frame_index=frame_num) + elif droplet_geometry == "cylinder_y": + box_dimensions = parser.box_size_y(frame_index=frame_num) + else: + box_dimensions = None + predictor = SlicingFrameFitter( + liquid_coordinates=liquid_positions, + max_dist=max_dist, + liquid_geom_center=mean_liquid_position, + droplet_geometry=droplet_geometry, + delta_gamma=delta_gamma, + width_cylinder=box_dimensions, + delta_cylinder=delta_cylinder, + points_per_angstrom=points_per_angstrom, + ) + angles, surfaces, popt_arrays = predictor.predict_contact_angle() + if not angles: + logger.warning(f"Frame {frame_num}: No angles computed (empty list).") + return _SlicingFrameResult(frame_num, None, [], [], []) + mean_angle = float(np.mean(angles)) + logger.info(f"Frame {frame_num} - mean angle: {mean_angle:.2f}°") + return _SlicingFrameResult( + frame_num, mean_angle, angles, surfaces, popt_arrays + ) + except Exception as e: + logger.error(f"Error processing frame {frame_num}: {e}", exc_info=True) + return _SlicingFrameResult(frame_num, None, [], [], []) diff --git a/src/wetting_angle_kit/analysis/slicing/angle_fitting.py b/src/wetting_angle_kit/analysis/slicing/angle_fitting.py index 319d71d..284e6fc 100644 --- a/src/wetting_angle_kit/analysis/slicing/angle_fitting.py +++ b/src/wetting_angle_kit/analysis/slicing/angle_fitting.py @@ -9,7 +9,7 @@ from wetting_angle_kit.io_utils import validate_droplet_geometry -class ContactAngleSlicing: +class SlicingFrameFitter: """Slicing radial line method to estimate contact angle via circle fitting. Depending on ``droplet_geometry`` the droplet is analyzed by sweeping in y diff --git a/src/wetting_angle_kit/analysis/slicing/parallel.py b/src/wetting_angle_kit/analysis/slicing/parallel.py deleted file mode 100644 index ea90f41..0000000 --- a/src/wetting_angle_kit/analysis/slicing/parallel.py +++ /dev/null @@ -1,337 +0,0 @@ -import logging -import math -import multiprocessing as mp -from concurrent.futures import ProcessPoolExecutor, as_completed -from typing import NamedTuple - -import numpy as np - -from wetting_angle_kit.analysis.slicing.results import SlicingResults -from wetting_angle_kit.io_utils import ( - detect_parser_type, - recenter_droplet_pbc, - validate_droplet_geometry, -) -from wetting_angle_kit.parsers import BaseParser - -# "spawn" is required because parser instances may hold un-picklable handles -# (OVITO pipelines, ASE Atoms with C extensions). Using a scoped context -# rather than mutating the global start method keeps this side-effect-free -# when the package is imported. -_MP_CONTEXT = mp.get_context("spawn") - -logger = logging.getLogger(__name__) - - -class SlicingFrameResult(NamedTuple): - """Per-frame output from the slicing parallel worker. - - Attributes - ---------- - frame_num : int - Frame index this result refers to. - mean_angle : float | None - Mean contact angle across successful slices, or ``None`` if no - slice produced an angle. - angles : list[float] - Per-slice contact angles. Same length as ``surfaces`` and ``popts``. - surfaces : list[ndarray] - Per-slice surface point arrays of shape (M, 2). - popts : list[ndarray] - Per-slice fitted circle parameters with the baseline appended. - """ - - frame_num: int - mean_angle: float | None - angles: list - surfaces: list - popts: list - - -class ContactAngleSlicingParallel: - """Batch-parallel contact angle analyzer for slicing method. - - The frames are grouped into batches to reduce problems - related to serialization by the parser and to distribute - the cost of creating objects. Each batch is processed in - a separate process using ``ProcessPoolExecutor``. - """ - - def __init__( - self, - filename: str, - droplet_geometry: str = "spherical", - atom_indices: np.ndarray | None = None, - delta_gamma: float | None = None, - delta_cylinder: float | None = None, - points_per_angstrom: float = 1.0, - precentered: bool = False, - ): - """ - Parameters - ---------- - filename : str - Path to trajectory file. - droplet_geometry : str, default "spherical" - Geometric model identifier (e.g. "cylinder_x", "cylinder_y", "spherical"). - atom_indices : ndarray, optional - Indices of liquid particles (subset). Empty array selects none. - delta_gamma : float, optional - Additional gamma constraint / filtering distance if used by slicing method. - delta_cylinder : float, optional - Y (or X) half-width of selection cylinder in cylindrical modes. - points_per_angstrom : float, default 1.0 - Sampling density along each radial ray for the surface fit. - Influences the computational cost. - precentered : bool, default False - Set True to declare that the trajectory already recenters the - droplet at every frame and atoms are not wrapped across periodic - boundaries. The per-frame circular-mean recentering is then - skipped (using a plain arithmetic mean instead), removing the - associated overhead. Setting this on a trajectory that does NOT - satisfy the precondition will produce wrong results. - """ - # Fail fast on an unrecognized trajectory file: workers re-detect the - # parser type inside their subprocesses and would otherwise swallow the - # error, returning empty results for every frame. - detect_parser_type(filename) - validate_droplet_geometry(droplet_geometry) - if droplet_geometry == "spherical": - if delta_gamma is None: - raise ValueError("delta_gamma must be provided for spherical analysis") - if delta_cylinder is not None: - raise ValueError( - "delta_cylinder must not be set for spherical analysis " - "(it is only valid for cylinder_x / cylinder_y)." - ) - elif droplet_geometry in ("cylinder_x", "cylinder_y"): - if delta_cylinder is None: - raise ValueError( - f"delta_cylinder must be provided for {droplet_geometry}." - ) - if delta_gamma is not None: - raise ValueError( - f"delta_gamma must not be set for {droplet_geometry} " - "(it is only valid for spherical)." - ) - self.filename = filename - self.delta_gamma = delta_gamma - self.delta_cylinder = delta_cylinder - self.droplet_geometry = droplet_geometry - self.points_per_angstrom = points_per_angstrom - self.atom_indices = atom_indices if atom_indices is not None else np.array([]) - self.precentered = precentered - - def process_frames_parallel( - self, - frames_to_process: list[int], - num_batches: int = 4, - max_workers: int | None = None, - ) -> SlicingResults: - """Process many frames in parallel batches and return the in-memory results. - - Parameters - ---------- - frames_to_process : list[int] - Frame numbers to analyze. - num_batches : int, default 4 - Number of batches to partition frames into. - max_workers : int, optional - Maximum number of worker processes. Defaults to ``num_batches``. - - Returns - ------- - SlicingResults - Per-frame angles, surface contours, fit parameters and method - metadata. Frames whose worker failed to produce a mean angle are - omitted. - """ - if max_workers is None: - max_workers = num_batches - batches = self._create_batches(frames_to_process, num_batches) - logger.info( - f"Processing {len(frames_to_process)} frames in {len(batches)} batches " - f"with {max_workers} workers" - ) - all_angles: dict[int, list] = {} - all_surfaces: dict[int, list] = {} - all_popts: dict[int, list] = {} - with ProcessPoolExecutor( - max_workers=max_workers, mp_context=_MP_CONTEXT - ) as executor: - future_to_batch = { - executor.submit(self._process_batch_worker, batch_frames): batch_frames - for batch_frames in batches - } - completed_batches = 0 - for future in as_completed(future_to_batch): - batch_frames = future_to_batch[future] - try: - batch_results = future.result() - for frame_num, mean_angle, angles, surfaces, popts in batch_results: - if mean_angle is not None: - all_angles[frame_num] = angles - all_surfaces[frame_num] = surfaces - all_popts[frame_num] = popts - completed_batches += 1 - logger.info( - f"Completed batch {completed_batches}/{len(batches)} " - f"({len(batch_results)} frames)" - ) - except Exception as e: - logger.error( - f"Error in batch for frames {batch_frames}: {e}", - exc_info=True, - ) - sorted_frames = sorted(all_angles.keys()) - logger.info( - f"Successfully processed {len(sorted_frames)}/{len(frames_to_process)} " - "frames" - ) - if frames_to_process and not sorted_frames: - raise RuntimeError( - f"None of the {len(frames_to_process)} requested frames produced " - "any contact-angle slices. Check the worker logs above for the " - "underlying parser, geometry, or fit errors." - ) - return SlicingResults( - frames=sorted_frames, - angles=[np.asarray(all_angles[f]) for f in sorted_frames], - surfaces=[all_surfaces[f] for f in sorted_frames], - popts=[np.asarray(all_popts[f]) for f in sorted_frames], - method_metadata={"frames_per_angle": 1}, - ) - - def _create_batches(self, frames: list[int], num_batches: int) -> list[list[int]]: - """Return frame batches of near-equal size.""" - if num_batches >= len(frames): - return [[frame] for frame in frames] - batch_size = math.ceil(len(frames) / num_batches) - return [frames[i : i + batch_size] for i in range(0, len(frames), batch_size)] - - def _process_batch_worker( - self, batch_frames: list[int] - ) -> list[SlicingFrameResult]: - """Worker routine executed in child process for a batch.""" - try: - from wetting_angle_kit.io_utils import detect_parser_type - from wetting_angle_kit.parsers.ase import AseParser - from wetting_angle_kit.parsers.base import BaseParser - from wetting_angle_kit.parsers.lammps_dump import LammpsDumpParser - from wetting_angle_kit.parsers.xyz import XYZParser - except ImportError as e: - logger.error(f"Failed to import required classes: {e}") - return [ - SlicingFrameResult(frame, None, [], [], []) for frame in batch_frames - ] - try: - parser_type = detect_parser_type(self.filename) - logger.info(f"Detected parser type: {parser_type}") - parser: BaseParser - if parser_type == "dump": - parser = LammpsDumpParser(filepath=self.filename) - elif parser_type == "ase": - parser = AseParser(filepath=self.filename) - elif parser_type == "xyz": - parser = XYZParser(filepath=self.filename) - else: - raise ValueError(f"Unsupported parser type: {parser_type}") - except Exception as e: - logger.error(f"Error initializing parser: {e}") - return [ - SlicingFrameResult(frame, None, [], [], []) for frame in batch_frames - ] - batch_results: list[SlicingFrameResult] = [] - for frame_num in batch_frames: - try: - result = self._process_single_frame_with_parsers( - frame_num, self.atom_indices, parser - ) - batch_results.append(result) - except Exception as e: - logger.error(f"Error processing frame {frame_num}: {e}") - batch_results.append(SlicingFrameResult(frame_num, None, [], [], [])) - return batch_results - - def _process_single_frame_with_parsers( - self, frame_num: int, atom_indices: np.ndarray, parser: BaseParser - ) -> SlicingFrameResult: - """Process a single frame and compute mean contact angle.""" - try: - from wetting_angle_kit.analysis.slicing.angle_fitting import ( - ContactAngleSlicing, - ) - - except ImportError as e: - logger.error(f"Missing slicing predictor dependency: {e}") - return SlicingFrameResult(frame_num, None, [], [], []) - logger.info(f"START processing frame {frame_num}") - try: - liquid_positions = parser.parse( - frame_index=frame_num, - indices=atom_indices, - ) - max_dist = int( - np.max( - np.array( - [ - parser.box_size_y(frame_index=frame_num), - parser.box_size_x(frame_index=frame_num), - ] - ) - ) - / 2 - ) - logger.info( - f"Frame {frame_num}: Parsed {len(liquid_positions)} liquid " - f"particles with max_dist {max_dist}" - ) - # Fold the droplet into the minimum-image frame around its - # circular-mean COM before the cylinder_x axis swap, so the - # box_size argument is in the parser's native frame. This makes - # downstream radial sampling robust to droplets that straddle a - # periodic boundary, and is idempotent for trajectories already - # recentered during dynamics. Skipped (with a plain arithmetic - # mean) when the user has declared the trajectory pre-centered. - if self.precentered: - mean_liquid_position = np.mean(liquid_positions, axis=0) - else: - box_size_xy = ( - parser.box_size_x(frame_index=frame_num), - parser.box_size_y(frame_index=frame_num), - ) - liquid_positions, mean_liquid_position = recenter_droplet_pbc( - liquid_positions, self.droplet_geometry, box_size=box_size_xy - ) - if self.droplet_geometry == "cylinder_x": - liquid_positions = liquid_positions[:, [1, 0, 2]] - mean_liquid_position = mean_liquid_position[[1, 0, 2]] - box_dimensions = parser.box_size_x(frame_index=frame_num) - elif self.droplet_geometry == "cylinder_y": - box_dimensions = parser.box_size_y(frame_index=frame_num) - else: - box_dimensions = None - predictor = ContactAngleSlicing( - liquid_coordinates=liquid_positions, - max_dist=max_dist, - liquid_geom_center=mean_liquid_position, - droplet_geometry=self.droplet_geometry, - delta_gamma=self.delta_gamma, - width_cylinder=box_dimensions, - delta_cylinder=self.delta_cylinder, - points_per_angstrom=self.points_per_angstrom, - ) - angles, surfaces, popt_arrays = predictor.predict_contact_angle() - if len(angles) == 0: - logger.warning(f"Frame {frame_num}: No angles computed (empty list).") - mean_angle = None - else: - mean_angle = float(np.mean(angles)) - if mean_angle is not None: - logger.info(f"Frame {frame_num} - mean angle: {mean_angle:.2f}°") - return SlicingFrameResult( - frame_num, mean_angle, angles, surfaces, popt_arrays - ) - except Exception as e: - logger.error(f"Error processing frame {frame_num}: {e}", exc_info=True) - return SlicingFrameResult(frame_num, None, [], [], []) diff --git a/src/wetting_angle_kit/visualization/animator.py b/src/wetting_angle_kit/visualization/animator.py index 8933231..7f99db1 100644 --- a/src/wetting_angle_kit/visualization/animator.py +++ b/src/wetting_angle_kit/visualization/animator.py @@ -1,7 +1,7 @@ import numpy as np import plotly.graph_objects as go -from wetting_angle_kit.analysis.slicing import ContactAngleSlicing +from wetting_angle_kit.analysis.slicing import SlicingFrameFitter from wetting_angle_kit.io_utils import recenter_droplet_pbc from wetting_angle_kit.parsers import ( LammpsDumpParser, @@ -44,7 +44,7 @@ def __init__( n_frames : int, default 10 Number of frames to include in the animation. droplet_geometry : str, default "cylinder_y" - Droplet geometry passed to ContactAngleSlicing. + Droplet geometry passed to SlicingFrameFitter. delta_cylinder : int, default 5 Step size along the slicing axis (Å). max_dist : int, default 100 @@ -113,7 +113,7 @@ def generate_animation( oxygen_position, liquid_geom_center = recenter_droplet_pbc( oxygen_position, self.droplet_geometry, box_size=box_size_xy ) - processor = ContactAngleSlicing( + processor = SlicingFrameFitter( liquid_coordinates=oxygen_position, liquid_geom_center=liquid_geom_center, droplet_geometry=self.droplet_geometry, diff --git a/tests/test_analysis/test_binning_method.py b/tests/test_analysis/test_binning_method.py index 8e34c2c..99fbad4 100644 --- a/tests/test_analysis/test_binning_method.py +++ b/tests/test_analysis/test_binning_method.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from wetting_angle_kit.analysis import BinningContactAngleAnalyzer +from wetting_angle_kit.analysis import BinningTrajectoryAnalyzer from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder @@ -47,12 +47,12 @@ def binning_params(): } -# --- Unit Test for BinningContactAngleAnalyzer --- +# --- Unit Test for BinningTrajectoryAnalyzer --- @pytest.mark.integration def test_binning_contact_angle_analyzer_with_real_data( filename, oxygen_indices, binning_params ): - analyzer = BinningContactAngleAnalyzer( + analyzer = BinningTrajectoryAnalyzer( parser=LammpsDumpParser(filename), atom_indices=oxygen_indices, droplet_geometry="cylinder_y", @@ -76,7 +76,7 @@ def test_binning_contact_angle_analyzer_with_real_data( def test_binning_contact_angle_analyzer_per_frame_with_split_factor( filename, oxygen_indices, binning_params ): - analyzer = BinningContactAngleAnalyzer( + analyzer = BinningTrajectoryAnalyzer( parser=LammpsDumpParser(filename), atom_indices=oxygen_indices, droplet_geometry="cylinder_y", diff --git a/tests/test_analysis/test_slicing_edge_cases.py b/tests/test_analysis/test_slicing_edge_cases.py index 12041bc..079178a 100644 --- a/tests/test_analysis/test_slicing_edge_cases.py +++ b/tests/test_analysis/test_slicing_edge_cases.py @@ -1,18 +1,18 @@ import numpy as np import pytest -from wetting_angle_kit.analysis.slicing.angle_fitting import ( - ContactAngleSlicing, +from wetting_angle_kit.analysis.slicing.analyzer import ( + SlicingTrajectoryAnalyzer, + _SlicingFrameResult, ) -from wetting_angle_kit.analysis.slicing.parallel import ( - ContactAngleSlicingParallel, - SlicingFrameResult, +from wetting_angle_kit.analysis.slicing.angle_fitting import ( + SlicingFrameFitter, ) def _simple_predictor(droplet_geometry="cylinder_y", **kwargs): - """Return a minimally-initialised ContactAngleSlicing with required attrs.""" - return ContactAngleSlicing( + """Return a minimally-initialised SlicingFrameFitter with required attrs.""" + return SlicingFrameFitter( liquid_coordinates=np.zeros((10, 3)), max_dist=20, liquid_geom_center=np.array([0.0, 0.0, 0.0]), @@ -99,52 +99,34 @@ def test_calculate_y_axis_spherical(): assert all(g >= 0 for g in gammas) -# --- ContactAngleSlicingParallel internals --- - - -def test_create_batches_few_frames(): - # filename only needs a recognized extension; the file does not have to exist - # for _create_batches, which is pure logic on the requested frame list. - parallel = ContactAngleSlicingParallel( - filename="ignored.lammpstrj", - droplet_geometry="spherical", - delta_gamma=20.0, - ) - # num_batches >= len(frames) → one frame per batch - assert parallel._create_batches([1, 2, 3], num_batches=4) == [[1], [2], [3]] - - -def test_create_batches_many_frames(): - parallel = ContactAngleSlicingParallel( - filename="ignored.lammpstrj", - droplet_geometry="spherical", - delta_gamma=20.0, - ) - batches = parallel._create_batches(list(range(10)), num_batches=3) - flat = [f for batch in batches for f in batch] - assert flat == list(range(10)) - # Approximately equal split; each batch is ≤ ceil(10/3) = 4. - assert all(len(b) <= 4 for b in batches) +# --- SlicingTrajectoryAnalyzer worker internals --- -def test_process_batch_worker_invokes_pipeline_on_real_lammps(tmp_path): - """Run _process_batch_worker on a real LAMMPS fixture in the current process. +def test_run_one_frame_invokes_pipeline_on_real_lammps(): + """Drive ``_run_one_frame`` on a real LAMMPS fixture in the current process. - Goes through detect_parser_type → LammpsDumpParser → predict_contact_angle, - so it exercises the worker code paths that subprocess execution otherwise - hides from coverage. + The worker static methods normally run inside child processes, so this + test initialises ``_WORKER_STATE`` manually and then calls + ``_run_one_frame`` to exercise the parser → ``predict_contact_angle`` + path that subprocess execution otherwise hides from coverage. """ from tests.conftest import trajectory_path - parallel = ContactAngleSlicingParallel( + SlicingTrajectoryAnalyzer._init_worker( filename=trajectory_path("traj_spherical_drop_4k.lammpstrj"), droplet_geometry="spherical", + atom_indices=np.array([]), delta_gamma=20.0, + delta_cylinder=None, + points_per_angstrom=1.0, + precentered=False, ) - results = parallel._process_batch_worker(batch_frames=[0]) - assert len(results) == 1 - assert isinstance(results[0], SlicingFrameResult) - assert results[0].frame_num == 0 + try: + result = SlicingTrajectoryAnalyzer._run_one_frame(0) + finally: + SlicingTrajectoryAnalyzer._WORKER_STATE.clear() + assert isinstance(result, _SlicingFrameResult) + assert result.frame_num == 0 def test_unsupported_extension_raises_at_construction(tmp_path): @@ -152,9 +134,13 @@ def test_unsupported_extension_raises_at_construction(tmp_path): subprocesses where the error would be silently swallowed.""" fake = tmp_path / "trajectory.bogus" fake.write_text("not a real trajectory\n") + + class _FakeParser: + filepath = str(fake) + with pytest.raises(ValueError, match="Unsupported trajectory file format"): - ContactAngleSlicingParallel( - filename=str(fake), + SlicingTrajectoryAnalyzer( + parser=_FakeParser(), droplet_geometry="spherical", delta_gamma=20.0, ) diff --git a/tests/test_analysis/test_slicing_method.py b/tests/test_analysis/test_slicing_method.py index 3b1def7..45852c0 100644 --- a/tests/test_analysis/test_slicing_method.py +++ b/tests/test_analysis/test_slicing_method.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from wetting_angle_kit.analysis import SlicingContactAngleAnalyzer +from wetting_angle_kit.analysis import SlicingTrajectoryAnalyzer from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder @@ -35,7 +35,7 @@ def parser(filename): return LammpsDumpParser(filename) -# --- Unit Tests for ContactAngleSlicing --- +# --- Unit Tests for SlicingFrameFitter --- @pytest.mark.integration @pytest.mark.slow def test_contact_angle_slicing_with_real_data(parser, oxygen_indices): @@ -51,12 +51,12 @@ def test_contact_angle_slicing_with_real_data(parser, oxygen_indices): ) mean_liquid_position = np.mean(liquid_positions, axis=0) - # Initialize ContactAngleSlicing + # Initialize SlicingFrameFitter from wetting_angle_kit.analysis.slicing import ( - ContactAngleSlicing, + SlicingFrameFitter, ) - predictor = ContactAngleSlicing( + predictor = SlicingFrameFitter( liquid_coordinates=liquid_positions, liquid_geom_center=mean_liquid_position, droplet_geometry="spherical", @@ -72,11 +72,11 @@ def test_contact_angle_slicing_with_real_data(parser, oxygen_indices): assert len(angles) > 0 -# --- Integration Test for SlicingContactAngleAnalyzer --- +# --- Integration Test for SlicingTrajectoryAnalyzer --- @pytest.mark.integration @pytest.mark.slow def test_slicing_contact_angle_analyzer_with_real_data(filename, oxygen_indices): - analyzer = SlicingContactAngleAnalyzer( + analyzer = SlicingTrajectoryAnalyzer( parser=LammpsDumpParser(filename), atom_indices=oxygen_indices, droplet_geometry="spherical", diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 95bbe65..1e7ad11 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -7,7 +7,7 @@ HyperbolicTangentModel, ) from wetting_angle_kit.analysis.slicing.angle_fitting import ( - ContactAngleSlicing, + SlicingFrameFitter, ) # --- Invalid droplet_geometry should be rejected by both analyzers --- @@ -16,7 +16,7 @@ def test_contact_angle_slicing_rejects_invalid_geometry(): coords = np.array([[0.0, 0.0, 0.0]]) with pytest.raises(ValueError, match="Unknown droplet_geometry"): - ContactAngleSlicing( + SlicingFrameFitter( liquid_coordinates=coords, max_dist=10, liquid_geom_center=np.zeros(3), @@ -33,7 +33,7 @@ def test_predict_contact_angle_returns_aligned_lists(): length. This guards against the historical bug where median_idx into angles would address a different slice in popt_arrays/surfaces.""" coords = np.array([[0.0, 0.0, 10.0]]) # single atom = no tanh interface - predictor = ContactAngleSlicing( + predictor = SlicingFrameFitter( liquid_coordinates=coords, max_dist=10, liquid_geom_center=np.zeros(3), @@ -47,7 +47,7 @@ def test_predict_contact_angle_returns_aligned_lists(): def test_contact_angle_slicing_copies_geometric_center(): """Constructor must not retain a reference to the caller's array.""" center = np.array([1.0, 2.0, 3.0]) - predictor = ContactAngleSlicing( + predictor = SlicingFrameFitter( liquid_coordinates=np.zeros((1, 3)), max_dist=10, liquid_geom_center=center, @@ -64,7 +64,7 @@ def test_contact_angle_slicing_copies_geometric_center(): def test_slicing_cylinder_without_width_raises(): with pytest.raises(ValueError, match="delta_cylinder and width_cylinder"): - ContactAngleSlicing( + SlicingFrameFitter( liquid_coordinates=np.zeros((3, 3)), max_dist=10, liquid_geom_center=np.zeros(3), @@ -74,7 +74,7 @@ def test_slicing_cylinder_without_width_raises(): def test_slicing_spherical_requires_delta_gamma(): with pytest.raises(ValueError, match="delta_gamma must be provided"): - ContactAngleSlicing( + SlicingFrameFitter( liquid_coordinates=np.zeros((3, 3)), max_dist=10, liquid_geom_center=np.zeros(3), @@ -117,13 +117,13 @@ def test_hyperbolic_tangent_compute_isoline_raises_for_unphysical_fit(): model.compute_isoline() -# --- ContactAngleBinning.get_profile_coordinates --- +# --- BinningBatchFitter.get_profile_coordinates --- def _make_binning_analyzer(parser): - from wetting_angle_kit.analysis.binning import ContactAngleBinning + from wetting_angle_kit.analysis.binning import BinningBatchFitter - return ContactAngleBinning( + return BinningBatchFitter( parser=parser, atom_indices=None, droplet_geometry="spherical", @@ -211,7 +211,7 @@ def test_binning_precentered_skips_box_probe_and_warning(): and the result matches the legacy arithmetic-mean path.""" import warnings - from wetting_angle_kit.analysis.binning import ContactAngleBinning + from wetting_angle_kit.analysis.binning import BinningBatchFitter from wetting_angle_kit.parsers.base import BaseParser frame = np.array([[1.0, 0.0, 5.0], [-1.0, 0.0, 6.0], [0.0, 0.0, 7.0]]) @@ -223,7 +223,7 @@ def parse(self, frame_index, indices=None): def frame_count(self): return 1 - analyzer = ContactAngleBinning( + analyzer = BinningBatchFitter( parser=_StubParser(), atom_indices=None, droplet_geometry="spherical", From c9e09a70c824aa96ef364e54f78ac79af9521d52 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Mon, 1 Jun 2026 15:45:30 +0200 Subject: [PATCH 06/31] Slicing loop optimization. --- .../analysis/slicing/surface_definition.py | 247 ++++++++++++------ 1 file changed, 165 insertions(+), 82 deletions(-) diff --git a/src/wetting_angle_kit/analysis/slicing/surface_definition.py b/src/wetting_angle_kit/analysis/slicing/surface_definition.py index 362f222..216a6f0 100644 --- a/src/wetting_angle_kit/analysis/slicing/surface_definition.py +++ b/src/wetting_angle_kit/analysis/slicing/surface_definition.py @@ -23,7 +23,7 @@ """ import numpy as np -from scipy.optimize import curve_fit +from scipy.spatial import cKDTree class SurfaceDefinition: @@ -43,6 +43,13 @@ class SurfaceDefinition: # larger values broaden contributions and smooth the interface. DEFAULT_DENSITY_SIGMA = 3.0 + # Per-atom truncation radius for the Gaussian kernel, in units of + # ``density_sigma``. At 5 sigma each excluded atom contributes + # exp(-12.5) ≈ 3.7e-6 of the peak per-atom density: well below the + # noise of a single-frame fit, while shrinking the inner kernel sum + # from O(N) to the active neighbourhood of each sample point. + DEFAULT_CUTOFF_SIGMA = 5.0 + def __init__( self, atom_coords: np.ndarray, @@ -53,6 +60,7 @@ def __init__( density_conversion: float = 1.0, points_per_angstrom: float = 1.0, density_sigma: float = DEFAULT_DENSITY_SIGMA, + cutoff_sigma: float = DEFAULT_CUTOFF_SIGMA, ) -> None: """ Parameters @@ -73,6 +81,11 @@ def __init__( Sampling density along each ray. density_sigma : float, default DEFAULT_DENSITY_SIGMA Gaussian kernel width (Å) for density smoothing. + cutoff_sigma : float, default DEFAULT_CUTOFF_SIGMA + Multiple of ``density_sigma`` beyond which atoms are excluded + from each sample's density sum. Set higher for stricter + agreement with the dense kernel; the cost grows roughly as + ``cutoff_sigma ** 3`` (volume of the neighbour sphere). """ self.atom_coords = atom_coords self.center_geom = center_geom @@ -82,33 +95,52 @@ def __init__( self.max_dist = max_dist self.points_per_angstrom = points_per_angstrom self.density_sigma = density_sigma + self.cutoff_sigma = cutoff_sigma + # Spatial index over the atomic coordinates so each ray's density + # sum touches only the active neighbourhood of every sample point + # instead of the O(M*N) broadcast that previously dominated the + # slicing hot path. None for the empty-input case, which causes + # density_contribution to short-circuit to zeros. + self._atom_tree: cKDTree | None = ( + cKDTree(atom_coords) if len(atom_coords) > 0 else None + ) - @staticmethod - def density_contribution( - positions: np.ndarray, coords: np.ndarray, sigma: float = 2.0 - ) -> np.ndarray: - """Return Gaussian-smoothed density contributions at sampling positions. + def density_contribution(self, positions: np.ndarray) -> np.ndarray: + """Return Gaussian-smoothed density contributions at sample positions. + + Atoms farther than ``cutoff_sigma * density_sigma`` from a sample + point are skipped; their kernel weight is below ~4e-6 of the peak + at the 5 sigma default. Every (sample, atom) pair within the cutoff + is enumerated in a single C-side call via + ``cKDTree.sparse_distance_matrix`` so the per-sample work happens in + one vectorised numpy pass instead of an M-iteration Python loop. Parameters ---------- positions : ndarray, shape (M, 3) - Ray sampling coordinates. - coords : ndarray, shape (N, 3) - Atom coordinates contributing to density. - sigma : float, default 2.0 - Gaussian standard deviation (Å). Larger values broaden contributions. + Ray sampling coordinates. ``M`` is typically the sample count of + one ray, or the stacked count of all rays of a slice when + :meth:`analyze_lines` batches the per-slice fan. Returns ------- ndarray, shape (M,) Density values at each sampling position. """ - sigma2 = sigma * sigma + n_samples = len(positions) + if self._atom_tree is None or n_samples == 0: + return np.zeros(n_samples) + sigma2 = self.density_sigma * self.density_sigma prefactor = 1.0 / (2 * np.pi * sigma2) ** 1.5 - differences = positions[:, np.newaxis, :] - coords[np.newaxis, :, :] - ri2 = np.sum(differences**2, axis=-1) - den_contributions = prefactor * np.exp(-ri2 / (2 * sigma2)) - return np.sum(den_contributions, axis=1) + cutoff = self.cutoff_sigma * self.density_sigma + sample_tree = cKDTree(positions) + pairs = sample_tree.sparse_distance_matrix( + self._atom_tree, max_distance=cutoff, output_type="ndarray" + ) + if pairs.size == 0: + return np.zeros(n_samples) + contribs = prefactor * np.exp(-(pairs["v"] ** 2) / (2.0 * sigma2)) + return np.bincount(pairs["i"], weights=contribs, minlength=n_samples) @staticmethod def density_profile(z: np.ndarray, zd: float, d: float, h: float) -> np.ndarray: @@ -133,58 +165,112 @@ def density_profile(z: np.ndarray, zd: float, d: float, h: float) -> np.ndarray: """ return np.tanh(-z + zd) * d + h - def fit_density_profile( + def _fit_density_profiles_batched( self, - z_data: np.ndarray, - density: np.ndarray, - param_bounds: tuple[list[float], list[float]], - ) -> float: - """Fit the profile and return estimated interface position. + distances: np.ndarray, + densities: np.ndarray, + *, + max_iter: int = 25, + tol: float = 1e-9, + ) -> np.ndarray: + """Fit ``rho(s) = d * tanh(zd - s) + h`` to every ray of a slice at once. + + All rays of the slice share the same sampling grid, so the + Jacobian's structure is identical across rays and the per-ray + normal equations are independent 3x3 systems. A batched + Gauss-Newton solver assembles those systems on numpy tensors and + calls ``np.linalg.solve`` once per iteration, replacing the + per-ray ``scipy.optimize.curve_fit`` (TRF + finite-difference + Jacobian) that dominated the slicing hot path after 4.1/4.2. + + The closed-form initial guess (``h ~ midpoint``, ``d ~ + half-amplitude``, ``zd ~ midpoint crossing``) seeds each ray in + the basin of the global minimum, so plain Gauss-Newton without + damping converges in 3–6 iterations. Rays whose normal equations + become singular (e.g. constant density) fall back to that + initial guess. Parameters ---------- - z_data : ndarray - Distances along the ray. - density : ndarray - Observed (smoothed) density values. - param_bounds : tuple(list, list) - Lower and upper bounds for ``(zd, d, h)``. + distances : ndarray, shape (M,) + Sample distances along the ray (same for every ray of a slice). + densities : ndarray, shape (R, M) + Density values per ray. + max_iter : int, default 25 + Hard cap on Gauss-Newton iterations. + tol : float, default 1e-9 + Convergence threshold on the max absolute parameter step + across all rays. Returns ------- - float - Fitted ``zd`` value (interface location). + ndarray, shape (R,) + Fitted ``zd`` (interface position) per ray, clipped into + ``[0, max_dist]`` to match the bounded behaviour of the + original per-ray fit. """ - # For rho(s) = d * tanh(zd - s) + h: - # h ~ midpoint of the density signal, - # d ~ half-amplitude (positive, since density is high near center - # and low past the interface, so tanh(zd - s) goes +1 -> -1), - # zd ~ position where density crosses the midpoint h. - rho_max = float(np.max(density)) - rho_min = float(np.min(density)) + z = np.ascontiguousarray(distances, dtype=np.float64) + y = np.ascontiguousarray(densities, dtype=np.float64) + n_rays, n_samples = y.shape + + rho_max = y.max(axis=1) + rho_min = y.min(axis=1) h0 = 0.5 * (rho_max + rho_min) d0 = 0.5 * (rho_max - rho_min) - zd0 = float(z_data[int(np.argmin(np.abs(density - h0)))]) - lower, upper = param_bounds - p0 = [ - float(np.clip(zd0, lower[0], upper[0])), - float(np.clip(d0, lower[1], upper[1])), - float(np.clip(h0, lower[2], upper[2])), - ] - popt, _ = curve_fit( - self.density_profile, - z_data, - density, - p0=p0, - bounds=param_bounds, - maxfev=5000, - ) - zd, d, h = popt - return zd + zd0 = z[np.argmin(np.abs(y - h0[:, None]), axis=1)] + zd0 = np.clip(zd0, 0.0, float(self.max_dist)) + params = np.stack([zd0, d0, h0], axis=1) + params_init = params.copy() + + for _ in range(max_iter): + zd = params[:, 0] + d = params[:, 1] + h = params[:, 2] + # u = tanh(zd - z), shape (R, M). + u = np.tanh(zd[:, None] - z[None, :]) + residuals = y - (d[:, None] * u + h[:, None]) + # J columns are d/dzd, d/dd, d/dh. J_h = 1 is folded into the + # normal equations directly (sums / counts), so only J_zd and + # J_d are materialised here. + j_zd = d[:, None] * (1.0 - u * u) + j_d = u + # Symmetric 3x3 normal-equations matrix per ray. + normal = np.empty((n_rays, 3, 3)) + normal[:, 0, 0] = np.einsum("rm,rm->r", j_zd, j_zd) + normal[:, 0, 1] = normal[:, 1, 0] = np.einsum("rm,rm->r", j_zd, j_d) + normal[:, 0, 2] = normal[:, 2, 0] = j_zd.sum(axis=1) + normal[:, 1, 1] = np.einsum("rm,rm->r", j_d, j_d) + normal[:, 1, 2] = normal[:, 2, 1] = j_d.sum(axis=1) + normal[:, 2, 2] = n_samples + rhs = np.empty((n_rays, 3)) + rhs[:, 0] = np.einsum("rm,rm->r", j_zd, residuals) + rhs[:, 1] = np.einsum("rm,rm->r", j_d, residuals) + rhs[:, 2] = residuals.sum(axis=1) + try: + # ``solve`` interprets the last two axes of the RHS as + # ``(M, K)`` for batched LHS, so feed it a trailing K=1 + # axis to keep each ray's RHS a 3-vector. + step = np.linalg.solve(normal, rhs[..., None])[..., 0] + except np.linalg.LinAlgError: + break + params += step + if not np.isfinite(params).all(): + params = params_init.copy() + break + if np.max(np.abs(step)) < tol: + break + + return np.clip(params[:, 0], 0.0, float(self.max_dist)) def analyze_lines(self) -> tuple[list[list[float]], list[list[float]]]: """Sample density along radial lines and fit interface positions. + All rays of the slice share the same sampling distances and the + same atomic neighbourhood, so their sample positions are stacked + into a single ``(R * M, 3)`` array and the truncated density is + evaluated in one ``density_contribution`` call. Only the tanh fit + and the (x, z) projection are still done per ray. + Returns ------- rr : list[list[float]] @@ -193,36 +279,33 @@ def analyze_lines(self) -> tuple[list[list[float]], list[list[float]]]: Projected interface coordinates ``[x_proj, z_proj]`` in XZ plane. """ beta = np.linspace(0, 360, int(360 / self.delta_angle), endpoint=False) - rr = [] - xz = [] - nn = max(int(self.max_dist * self.points_per_angstrom), self.MIN_POINTS_PER_RAY) - param_bounds = ([0.0, -10.0, -10.0], [self.max_dist, 10.0, 10.0]) + n_samples = max( + int(self.max_dist * self.points_per_angstrom), self.MIN_POINTS_PER_RAY + ) cos_beta = np.cos(np.deg2rad(beta)) sin_beta = np.sin(np.deg2rad(beta)) cos_gamma = np.cos(np.deg2rad(self.gamma)) sin_gamma = np.sin(np.deg2rad(self.gamma)) - for i in range(len(beta)): - x_dir = cos_beta[i] * cos_gamma - y_dir = sin_gamma * cos_beta[i] - z_dir = sin_beta[i] - direction = np.array([x_dir, y_dir, z_dir]) - positions = np.linspace( - self.center_geom, - self.center_geom + self.max_dist * direction, - int(nn), - ) - distances = np.linspace(0.0, self.max_dist, int(nn)) - density = self.density_conversion * self.density_contribution( - positions, - self.atom_coords, - sigma=self.density_sigma, - ) - interface_re = self.fit_density_profile(distances, density, param_bounds) - rr.append([interface_re, beta[i]]) - xz.append( - [ - cos_beta[i] * interface_re + self.center_geom[0], - sin_beta[i] * interface_re + self.center_geom[2], - ] - ) + + # Per-ray unit direction vectors, shape (R, 3). Matches the original + # per-iteration construction ``(cos_beta * cos_gamma, + # cos_beta * sin_gamma, sin_beta)``. + directions = np.column_stack( + (cos_beta * cos_gamma, cos_beta * sin_gamma, sin_beta) + ) + distances = np.linspace(0.0, self.max_dist, n_samples) + + # positions[r, m, :] = center_geom + distances[m] * directions[r, :] + positions_rm = ( + self.center_geom[None, None, :] + + distances[None, :, None] * directions[:, None, :] + ) + density_flat = self.density_contribution(positions_rm.reshape(-1, 3)) + densities = self.density_conversion * density_flat.reshape(len(beta), n_samples) + interface_re = self._fit_density_profiles_batched(distances, densities) + + x_proj = cos_beta * interface_re + self.center_geom[0] + z_proj = sin_beta * interface_re + self.center_geom[2] + rr = [[float(interface_re[i]), float(beta[i])] for i in range(len(beta))] + xz = [[float(x_proj[i]), float(z_proj[i])] for i in range(len(beta))] return rr, xz From 89b494c03b4e139e6b3dd84809743930f3472499 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Mon, 1 Jun 2026 16:48:50 +0200 Subject: [PATCH 07/31] Added progress bar with running mean. --- pyproject.toml | 3 ++- .../analysis/slicing/analyzer.py | 21 +++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ab9fbf2..169c79a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,8 @@ dependencies = [ "numpy>=1.26.0", "scipy>=1.13.0", "matplotlib>=3.9.0", - "plotly>=5.24.1" + "plotly>=5.24.1", + "tqdm>=4.66.0" ] [project.optional-dependencies] diff --git a/src/wetting_angle_kit/analysis/slicing/analyzer.py b/src/wetting_angle_kit/analysis/slicing/analyzer.py index 9482e44..cb4da7b 100644 --- a/src/wetting_angle_kit/analysis/slicing/analyzer.py +++ b/src/wetting_angle_kit/analysis/slicing/analyzer.py @@ -5,6 +5,7 @@ from typing import Any, NamedTuple import numpy as np +from tqdm.auto import tqdm from wetting_angle_kit.analysis.analyzer import BaseTrajectoryAnalyzer from wetting_angle_kit.analysis.slicing.angle_fitting import ( @@ -161,14 +162,23 @@ def analyze( ) logger.info(f"Processing {len(frame_range)} frames with n_jobs={n_jobs}") results_by_frame: dict[int, _SlicingFrameResult] = {} - with _MP_CONTEXT.Pool( - processes=n_jobs, - initializer=self._init_worker, - initargs=init_args, - ) as pool: + running_sum = 0.0 + running_count = 0 + with ( + _MP_CONTEXT.Pool( + processes=n_jobs, + initializer=self._init_worker, + initargs=init_args, + ) as pool, + tqdm(total=len(frame_range), desc="Slicing frames", unit="frame") as pbar, + ): for result in pool.imap_unordered(self._run_one_frame, frame_range): if result.mean_angle is not None: results_by_frame[result.frame_num] = result + running_sum += result.mean_angle + running_count += 1 + pbar.set_postfix(mean_angle=f"{running_sum / running_count:.2f}°") + pbar.update(1) sorted_frames = sorted(results_by_frame) logger.info( f"Successfully processed {len(sorted_frames)}/{len(frame_range)} frames" @@ -287,7 +297,6 @@ def _run_one_frame(frame_num: int) -> _SlicingFrameResult: logger.warning(f"Frame {frame_num}: No angles computed (empty list).") return _SlicingFrameResult(frame_num, None, [], [], []) mean_angle = float(np.mean(angles)) - logger.info(f"Frame {frame_num} - mean angle: {mean_angle:.2f}°") return _SlicingFrameResult( frame_num, mean_angle, angles, surfaces, popt_arrays ) From 35b78f3442e17fd40647d387b5a7af74b5ab869c Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 09:22:32 +0200 Subject: [PATCH 08/31] Removed unused utilities from io_utils.py --- src/wetting_angle_kit/io_utils.py | 54 ------------------------------- tests/test_io_utils.py | 44 ------------------------- 2 files changed, 98 deletions(-) diff --git a/src/wetting_angle_kit/io_utils.py b/src/wetting_angle_kit/io_utils.py index 183bb26..ad9c95d 100644 --- a/src/wetting_angle_kit/io_utils.py +++ b/src/wetting_angle_kit/io_utils.py @@ -59,60 +59,6 @@ def assert_orthogonal_cell( ) -def load_dump_ovito(filepath: str) -> Any: - """Load a LAMMPS dump file via OVITO and return the pipeline. - - Parameters - ---------- - filepath : str - Path to the LAMMPS dump file. - - Returns - ------- - Any - OVITO pipeline object (typed as Any because OVITO lacks Python type stubs). - """ - try: - from ovito.io import import_file - except ImportError as e: # add exception chaining - raise ImportError( - "The 'ovito' package is required for load dump_ovito. Install it with: " - "pip install wetting_angle_kit[ovito]" - ) from e - pipeline = import_file(filepath) - # Add necessary modifiers - return pipeline - - -def save_array_as_txt(array: np.ndarray, filename: str) -> None: - """Save a numpy array to a whitespace-delimited text file. - - Parameters - ---------- - array : ndarray - Array to save. - filename : str - Output file path. - """ - np.savetxt(filename, array, fmt="%f") - - -def geometric_center(list_xyz_point: np.ndarray) -> np.ndarray: - """Return the geometric center (mean position) of a point cloud. - - Parameters - ---------- - list_xyz_point : ndarray, shape (N, 3) - Cartesian coordinates of the points. - - Returns - ------- - ndarray, shape (3,) - Mean position vector. - """ - return np.mean(list_xyz_point, axis=0) - - def detect_parser_type(filename: str) -> str: """Infer the parser type from a trajectory file extension. diff --git a/tests/test_io_utils.py b/tests/test_io_utils.py index 6ef4101..5c7e7ba 100644 --- a/tests/test_io_utils.py +++ b/tests/test_io_utils.py @@ -1,8 +1,6 @@ """Unit tests for :mod:`wetting_angle_kit.io_utils`.""" import os -import sys -from unittest import mock import numpy as np import pytest @@ -11,9 +9,7 @@ VALID_DROPLET_GEOMETRIES, assert_orthogonal_cell, detect_parser_type, - geometric_center, recenter_droplet_pbc, - save_array_as_txt, validate_droplet_geometry, ) @@ -41,33 +37,6 @@ def test_detect_parser_type_rejects_unknown(filename): detect_parser_type(filename) -# --- geometric_center --- - - -def test_geometric_center_simple(): - points = np.array([[0.0, 0.0, 0.0], [2.0, 4.0, 6.0]]) - center = geometric_center(points) - np.testing.assert_array_equal(center, np.array([1.0, 2.0, 3.0])) - - -def test_geometric_center_single_point(): - points = np.array([[1.5, -2.0, 3.7]]) - center = geometric_center(points) - np.testing.assert_array_equal(center, np.array([1.5, -2.0, 3.7])) - - -# --- save_array_as_txt --- - - -def test_save_array_as_txt_roundtrip(tmp_path): - target = tmp_path / "values.txt" - data = np.array([[1.0, 2.0], [3.5, 4.25]]) - save_array_as_txt(data, str(target)) - assert target.exists() - loaded = np.loadtxt(target) - np.testing.assert_allclose(loaded, data) - - # --- validate_droplet_geometry --- @@ -83,19 +52,6 @@ def test_validate_droplet_geometry_rejects_invalid(bad): validate_droplet_geometry(bad) -# --- load_dump_ovito (only test the ImportError path; calling it for real -# requires ovito and a trajectory, which the other test modules cover) --- - - -def test_load_dump_ovito_raises_when_ovito_missing(): - from wetting_angle_kit import io_utils - - # Block ovito imports for the duration of this test. - with mock.patch.dict(sys.modules, {"ovito": None, "ovito.io": None}): - with pytest.raises(ImportError, match="ovito"): - io_utils.load_dump_ovito("/nonexistent.lammpstrj") - - def test_valid_droplet_geometries_constant_is_a_tuple(): # Constant should be a frozen tuple-like sequence so callers cannot # mutate the package-level whitelist accidentally. From ada3a4075bc46d2b5d60491f5815b312f7f48d83 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 11:21:05 +0200 Subject: [PATCH 09/31] Removed width_cylinder from arguments. Fine tuning other parts. --- .gitignore | 2 + CONTRIBUTING.md | 5 +- README.md | 8 +- docs/build_log.txt | 83 --------------- docs/examples/visualisation_slicing_traj.py | 1 - docs/source/introduction/Introduction.rst | 2 +- docs/source/tutorials/Binning_method_tuto.rst | 1 - .../Visualization_slicing_droplet.rst | 1 - docs/tutorials/Animated_slicing_droplet.py | 0 docs/tutorials/Binning_method_tuto.md | 1 - .../Visualization_slicing_droplet.md | 1 - .../all_alfas.npy | Bin 372 -> 0 bytes .../all_popts.npy | Bin 950 -> 0 bytes .../all_surfaces.npy | Bin 7187 -> 0 bytes src/wetting_angle_kit/__init__.py | 1 + .../analysis/binning/analyzer.py | 4 - .../analysis/binning/angle_fitting.py | 35 ++----- .../analysis/slicing/analyzer.py | 6 -- .../analysis/slicing/angle_fitting.py | 95 ++++++------------ .../visualization/animator.py | 36 +++++-- tests/README | 16 ++- tests/test_analysis/test_binning_method.py | 2 - .../test_analysis/test_slicing_edge_cases.py | 65 ++++-------- tests/test_edge_cases.py | 6 +- .../test_droplet_slice_plot.py | 2 - wetting_angle_kit_JOSS/paper.md | 6 +- wetting_angle_kit_JOSS/paper_old.pdf | Bin 146909 -> 0 bytes 27 files changed, 120 insertions(+), 259 deletions(-) delete mode 100644 docs/build_log.txt delete mode 100644 docs/tutorials/Animated_slicing_droplet.py delete mode 100644 docs/tutorials/result_dump_spherical_slicing/all_alfas.npy delete mode 100644 docs/tutorials/result_dump_spherical_slicing/all_popts.npy delete mode 100644 docs/tutorials/result_dump_spherical_slicing/all_surfaces.npy delete mode 100644 wetting_angle_kit_JOSS/paper_old.pdf diff --git a/.gitignore b/.gitignore index b3216da..a5653eb 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,8 @@ docs/build/doctrees docs/build/generate-stamp docs/source/changelog.rst +docs/build_log.txt +docs/tutorials/result_dump_spherical_slicing # PyBuilder .pybuilder/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e9bccf..4a3964f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,10 +86,9 @@ parsers' handling of orthogonal cells and periodic boundary conditions. ## Adding a new contact-angle method -Subclass `BaseContactAngleAnalyzer` +Subclass `BaseTrajectoryAnalyzer` ([src/wetting_angle_kit/analysis/analyzer.py](src/wetting_angle_kit/analysis/analyzer.py)) -and register it in the analyzer factory so it can be picked up by name. -Add an integration test in `tests/test_analysis/` that +and add an integration test in `tests/test_analysis/` that exercises the method on one of the fixture trajectories. ## Pull requests diff --git a/README.md b/README.md index 78e819c..3add82f 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ conda install --strict-channel-priority -c https://conda.ovito.org -c conda-forg ```python from wetting_angle_kit.analysis import ( - BinningContactAngleAnalyzer, - SlicingContactAngleAnalyzer, + BinningTrajectoryAnalyzer, + SlicingTrajectoryAnalyzer, ) from wetting_angle_kit.parsers import XYZParser, XYZWaterFinder @@ -69,7 +69,7 @@ oxygen_ids = finder.get_water_oxygen_indices(frame_index=0) parser = XYZParser(trajectory_file) -slicing = SlicingContactAngleAnalyzer( +slicing = SlicingTrajectoryAnalyzer( parser, atom_indices=oxygen_ids, droplet_geometry="spherical", @@ -78,7 +78,7 @@ slicing = SlicingContactAngleAnalyzer( results = slicing.analyze(frame_range=range(0, 50)) print(results.mean_angle, results.std_angle) -binning = BinningContactAngleAnalyzer( +binning = BinningTrajectoryAnalyzer( parser, atom_indices=oxygen_ids, droplet_geometry="spherical", diff --git a/docs/build_log.txt b/docs/build_log.txt deleted file mode 100644 index be9db6d..0000000 --- a/docs/build_log.txt +++ /dev/null @@ -1,83 +0,0 @@ -Running Sphinx v8.2.3 -loading translations [en]... done -making output directory... done -[autosummary] generating autosummary for: API/index.rst, examples/index.rst, index.rst, tutorials/Binning_method_tuto.rst, tutorials/Parser_tutorial.rst, tutorials/Sliced_method_tuto.rst, tutorials/Visualisation_size_changing_droplet_infinit_angle.rst, tutorials/Visualization_sliced_droplet.rst, tutorials/Visualization_trajectories_comparison_methods.rst, tutorials/index.rst -WARNING: Failed to import wetting_angle_kit.contact_angle_method.binned_method. -Possible hints: -* ModuleNotFoundError: No module named 'wetting_angle_kit.contact_angle_method.binned_method' -* AttributeError: module 'wetting_angle_kit.contact_angle_method' has no attribute 'binned_method' -building [mo]: targets for 0 po files that are out of date -writing output... -building [html]: targets for 10 source files that are out of date -updating environment: [new config] 10 added, 0 changed, 0 removed -reading sources... [ 10%] API/index -reading sources... [ 20%] examples/index -reading sources... [ 30%] index -reading sources... [ 40%] tutorials/Binning_method_tuto -reading sources... [ 50%] tutorials/Parser_tutorial -reading sources... [ 60%] tutorials/Sliced_method_tuto -reading sources... [ 70%] tutorials/Visualisation_size_changing_droplet_infinit_angle -reading sources... [ 80%] tutorials/Visualization_sliced_droplet -reading sources... [ 90%] tutorials/Visualization_trajectories_comparison_methods -reading sources... [100%] tutorials/index - -/auto/home/users/g/t/gtaillan/lib_python_Hydro/wetting_angle_kit/wetting_angle_kit/contact_angle_method/sliced_method/__init__.py:docstring of wetting_angle_kit.contact_angle_method.sliced_method.angle_fitting_sliced.ContactAngleSliced:1: WARNING: duplicate object description of wetting_angle_kit.contact_angle_method.sliced_method.angle_fitting_sliced.ContactAngleSliced, other instance in API/index, use :no-index: for one of them -/auto/home/users/g/t/gtaillan/lib_python_Hydro/wetting_angle_kit/wetting_angle_kit/contact_angle_method/sliced_method/__init__.py:docstring of wetting_angle_kit.contact_angle_method.sliced_method.multi_processing.ContactAngleSlicedParallel:1: WARNING: duplicate object description of wetting_angle_kit.contact_angle_method.sliced_method.multi_processing.ContactAngleSlicedParallel, other instance in API/index, use :no-index: for one of them -/auto/home/users/g/t/gtaillan/lib_python_Hydro/wetting_angle_kit/wetting_angle_kit/contact_angle_method/sliced_method/__init__.py:docstring of wetting_angle_kit.contact_angle_method.sliced_method.surface_defined.SurfaceDefinition.analyze_lines:5: ERROR: Unexpected indentation. [docutils] -/auto/home/users/g/t/gtaillan/lib_python_Hydro/wetting_angle_kit/wetting_angle_kit/contact_angle_method/sliced_method/__init__.py:docstring of wetting_angle_kit.contact_angle_method.sliced_method.surface_defined.SurfaceDefinition.analyze_lines:6: WARNING: Block quote ends without a blank line; unexpected unindent. [docutils] -WARNING: autodoc: failed to import module 'binned_method' from module 'wetting_angle_kit.contact_angle_method'; the following exception was raised: -['Traceback (most recent call last):\n', ' File "/home/ucl/modl/gtaillan/miniconda3/lib/python3.12/site-packages/sphinx/ext/autodoc/importer.py", line 269, in import_object\n module = import_module(modname, try_reload=True)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n', ' File "/home/ucl/modl/gtaillan/miniconda3/lib/python3.12/site-packages/sphinx/ext/autodoc/importer.py", line 172, in import_module\n raise ModuleNotFoundError(msg, name=modname) # NoQA: TRY301\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n', "ModuleNotFoundError: No module named 'wetting_angle_kit.contact_angle_method.binned_method'\n"] [autodoc.import_object] -/auto/home/users/g/t/gtaillan/lib_python_Hydro/wetting_angle_kit/wetting_angle_kit/parser/__init__.py:docstring of wetting_angle_kit.parser.parser_xyz.XYZParser.box_length_max:6: ERROR: Undefined substitution referenced: "a_i". [docutils] -looking for now-outdated files... none found -pickling environment... done -checking consistency... done -preparing documents... done -copying assets... -copying static files... -Writing evaluated template result to /auto/home/users/g/t/gtaillan/lib_python_Hydro/wetting_angle_kit/docs/build/html/_static/language_data.js -Writing evaluated template result to /auto/home/users/g/t/gtaillan/lib_python_Hydro/wetting_angle_kit/docs/build/html/_static/basic.css -Writing evaluated template result to /auto/home/users/g/t/gtaillan/lib_python_Hydro/wetting_angle_kit/docs/build/html/_static/documentation_options.js -Writing evaluated template result to /auto/home/users/g/t/gtaillan/lib_python_Hydro/wetting_angle_kit/docs/build/html/_static/alabaster.css -copying static files: done -copying extra files... -copying extra files: done -copying assets: done -writing output... [ 10%] API/index -writing output... [ 20%] examples/index -writing output... [ 30%] index -writing output... [ 40%] tutorials/Binning_method_tuto -writing output... [ 50%] tutorials/Parser_tutorial -writing output... [ 60%] tutorials/Sliced_method_tuto -writing output... [ 70%] tutorials/Visualisation_size_changing_droplet_infinit_angle -writing output... [ 80%] tutorials/Visualization_sliced_droplet -writing output... [ 90%] tutorials/Visualization_trajectories_comparison_methods -writing output... [100%] tutorials/index - -generating indices... genindex py-modindex done -highlighting module code... [ 6%] wetting_angle_kit.contact_angle_method.binning_method.angle_fitting_binning -highlighting module code... [ 12%] wetting_angle_kit.contact_angle_method.contact_angle_analyzer -highlighting module code... [ 19%] wetting_angle_kit.contact_angle_method.factory -highlighting module code... [ 25%] wetting_angle_kit.contact_angle_method.sliced_method.angle_fitting_sliced -highlighting module code... [ 31%] wetting_angle_kit.contact_angle_method.sliced_method.multi_processing -highlighting module code... [ 38%] wetting_angle_kit.contact_angle_method.sliced_method.surface_defined -highlighting module code... [ 44%] wetting_angle_kit.parser.base_parser -highlighting module code... [ 50%] wetting_angle_kit.parser.parser_ase -highlighting module code... [ 56%] wetting_angle_kit.parser.parser_dump -highlighting module code... [ 62%] wetting_angle_kit.parser.parser_xyz -highlighting module code... [ 69%] wetting_angle_kit.visualization_statistics_angles.base_trajectory_analyzer -highlighting module code... [ 75%] wetting_angle_kit.visualization_statistics_angles.binning_trajectory_evolution -highlighting module code... [ 81%] wetting_angle_kit.visualization_statistics_angles.comparison_methods -highlighting module code... [ 88%] wetting_angle_kit.visualization_statistics_angles.graphs_circle_slice -highlighting module code... [ 94%] wetting_angle_kit.visualization_statistics_angles.sliced_trajectory_evolution -highlighting module code... [100%] wetting_angle_kit.visualization_statistics_angles.tools_visu - -writing additional pages... search done -copying images... [ 33%] ../images/logo_wetting_angle_kit.png -copying images... [ 67%] ../images/bin_plot.png -copying images... [100%] ../images/droplet_plot.png - -dumping search index in English (code: en)... done -dumping object inventory... done -build succeeded, 7 warnings. - -The HTML pages are in build/html. diff --git a/docs/examples/visualisation_slicing_traj.py b/docs/examples/visualisation_slicing_traj.py index 955a3fd..4110aea 100644 --- a/docs/examples/visualisation_slicing_traj.py +++ b/docs/examples/visualisation_slicing_traj.py @@ -42,7 +42,6 @@ droplet_geometry="cylinder_y", delta_cylinder=5, max_dist=100, - width_cylinder=21, ) list_alfas, array_surfaces, array_popt = processor.predict_contact_angle() diff --git a/docs/source/introduction/Introduction.rst b/docs/source/introduction/Introduction.rst index 349609c..b381128 100644 --- a/docs/source/introduction/Introduction.rst +++ b/docs/source/introduction/Introduction.rst @@ -114,7 +114,7 @@ Troubleshooting * **NaN angles**: Usually occur when the surface filter removes too many points (empty slice). Adjust ``surface_filter_offset`` (default 2.0) in ``SlicingFrameFitter`` or relax slice width. Ensure enough atoms remain after filtering (>=3) for circle fitting. -* **Empty outputs / NoneType failures**: Confirm ``width_cylinder`` and ``delta_cylinder`` are passed for cylindrical models and ``delta_gamma`` for spherical model. Parser must supply box dimensions for automatic max distance estimation. +* **Empty outputs / NoneType failures**: Confirm ``delta_cylinder`` is passed for cylindrical models and ``delta_gamma`` for the spherical model. Parser must supply box dimensions for automatic max distance estimation. * **Multiprocessing hangs**: ``SlicingTrajectoryAnalyzer.analyze`` uses the spawn start method; avoid invoking OVITO parsers inside global contexts before multiprocessing starts. diff --git a/docs/source/tutorials/Binning_method_tuto.rst b/docs/source/tutorials/Binning_method_tuto.rst index 51275cf..45758ed 100644 --- a/docs/source/tutorials/Binning_method_tuto.rst +++ b/docs/source/tutorials/Binning_method_tuto.rst @@ -75,7 +75,6 @@ Example trajectory:: parser=parser, atom_indices=oxygen_indices, droplet_geometry="cylinder_y", # Interface fitting model - width_cylinder=21, # Width parameter for interface fit binning_params=binning_params, ) diff --git a/docs/source/tutorials/Visualization_slicing_droplet.rst b/docs/source/tutorials/Visualization_slicing_droplet.rst index 7a0276a..8b0fdf3 100644 --- a/docs/source/tutorials/Visualization_slicing_droplet.rst +++ b/docs/source/tutorials/Visualization_slicing_droplet.rst @@ -84,7 +84,6 @@ The visualization workflow involves the following steps: droplet_geometry="cylinder_y", delta_cylinder=5, max_dist=100, - width_cylinder=21, ) list_alfas, array_surfaces, array_popt = processor.predict_contact_angle() diff --git a/docs/tutorials/Animated_slicing_droplet.py b/docs/tutorials/Animated_slicing_droplet.py deleted file mode 100644 index e69de29..0000000 diff --git a/docs/tutorials/Binning_method_tuto.md b/docs/tutorials/Binning_method_tuto.md index 1fbcdce..049a89a 100644 --- a/docs/tutorials/Binning_method_tuto.md +++ b/docs/tutorials/Binning_method_tuto.md @@ -69,7 +69,6 @@ analyzer = BinningTrajectoryAnalyzer( parser=parser, atom_indices=oxygen_indices, droplet_geometry="cylinder_y", # Interface fitting model - width_cylinder=21, # Width parameter for interface fit binning_params=binning_params, ) diff --git a/docs/tutorials/Visualization_slicing_droplet.md b/docs/tutorials/Visualization_slicing_droplet.md index e8ed106..3ac488b 100644 --- a/docs/tutorials/Visualization_slicing_droplet.md +++ b/docs/tutorials/Visualization_slicing_droplet.md @@ -69,7 +69,6 @@ processor = SlicingFrameFitter( droplet_geometry="cylinder_y", delta_cylinder=5, max_dist=100, - width_cylinder=21, ) list_alfas, array_surfaces, array_popt = processor.predict_contact_angle() diff --git a/docs/tutorials/result_dump_spherical_slicing/all_alfas.npy b/docs/tutorials/result_dump_spherical_slicing/all_alfas.npy deleted file mode 100644 index e075e962c866013d2f27e4b924165351f13e1a16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1J^HzN=-wH2~Km8O(b7Nl|&vPUp6 zFfjOA6moQSv=?#)6>@2KGe=Efz7F@C58M+dI0uva-IMH diff --git a/docs/tutorials/result_dump_spherical_slicing/all_popts.npy b/docs/tutorials/result_dump_spherical_slicing/all_popts.npy deleted file mode 100644 index fdf39e88a17a1233c7ce7ab2124fec31fec38450..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 950 zcmbWzTSyd97zglK?`>ITS!TM9tL`PYH7#>pZ^zx#U6m)mKdjgf0fc}t~SMammI3}!6uIuB!~xptYw>7&`c&tdV>>{*eeBD0k$@&ft) zCB77Db2quX&1##6q1CP?Cq89i7)!Ibj-hQHw+}N-HY^5W<(1fNU%3U5iD-uqy~qlo zeh8x=TwxNiE2IM=gr|1g?4`vJSsg+UM5P9z0-b(XLxEUf3au8gy%tz&Hk&K{u*Gkt zVhro7fm(=Wty);Ga1ahYW`l#xh@oI3ml4(uv5pnAK?9pObTb8Ut7rji`4=THY_$gT zWX%1M-pMK>%A6G`pKC}Ido=uUWUd~GuAep$FR-18kxXFZl@qoh)i%S@g#xKyr1D0@;?m)-$woBw zwy|28n2O>P2c~U*9lz*hjWi7DK|{tFataiJq2!H`iGHRl#)wY4Lj1B`85+Cw zxjyMh3HtG?<8?jB8aps#1Pv8u?4&>~7@53rxBFv~|EB@<9~>O%?vWyCYktB&tq!I3 zjdl*bV*g_ohOD5G%^ABX*drJ@ykRVSKU)-UMDHH@yQ*SIG^p&GtY6TfPv_=c)9LIS zdokn&4Gm}HQJ@u!eBO{myjcD{VL&sEKn|r%L(Mn7gpQJ97p!?UH=>!jK4!3@wyr=+T&S-1%mhH4GSxR+`38VO9SHWXn1{ diff --git a/docs/tutorials/result_dump_spherical_slicing/all_surfaces.npy b/docs/tutorials/result_dump_spherical_slicing/all_surfaces.npy deleted file mode 100644 index 7a9a58ec1e534b06ab15ad9038b4e0a6b8273bc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7187 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 None: @@ -30,8 +29,6 @@ def __init__( Indices (or IDs) of liquid atoms to include in the density field. droplet_geometry : str, default "spherical" One of ``"spherical"``, ``"cylinder_x"``, ``"cylinder_y"``. - width_cylinder : float, optional - Box length along the cylinder axis; inferred from the parser if None. binning_params : dict, optional Grid definition with keys ``xi_0``, ``xi_f``, ``nbins_xi``, ``zi_0``, ``zi_f``, ``nbins_zi``. A heuristic default is used if None. @@ -45,7 +42,6 @@ def __init__( parser=parser, atom_indices=atom_indices, droplet_geometry=droplet_geometry, - width_cylinder=width_cylinder, binning_params=binning_params, precentered=precentered, ) diff --git a/src/wetting_angle_kit/analysis/binning/angle_fitting.py b/src/wetting_angle_kit/analysis/binning/angle_fitting.py index de05c42..b933014 100644 --- a/src/wetting_angle_kit/analysis/binning/angle_fitting.py +++ b/src/wetting_angle_kit/analysis/binning/angle_fitting.py @@ -12,9 +12,10 @@ Per-bin volume elements: -* ``cylinder_x`` / ``cylinder_y``: ``dV = 2 * width_cylinder * dxi * dzi``. - The factor of 2 accounts for folding the symmetric distribution into - positive ``xi`` via ``|x_centered|``. +* ``cylinder_x`` / ``cylinder_y``: ``dV = 2 * box_dimension * dxi * dzi``, + where ``box_dimension`` is the box length along the cylinder axis read + from the parser. The factor of 2 accounts for folding the symmetric + distribution into positive ``xi`` via ``|x_centered|``. * ``spherical``: ``dV = 2 * pi * xi_cc * dxi * dzi`` — the annular shell volume of cylindrical coordinates. @@ -58,7 +59,6 @@ def __init__( parser: Any, atom_indices: Any, droplet_geometry: str = "spherical", - width_cylinder: float | None = None, binning_params: dict[str, Any] | None = None, precentered: bool = False, ) -> None: @@ -71,8 +71,6 @@ def __init__( Indices (or IDs) of liquid atoms to include in the density field. droplet_geometry : str, default "spherical" One of ``"spherical"``, ``"cylinder_x"``, ``"cylinder_y"``. - width_cylinder : float, optional - Box length along the cylinder axis; inferred from the parser if None. binning_params : dict, optional Grid definition with keys ``xi_0``, ``xi_f``, ``nbins_xi``, ``zi_0``, ``zi_f``, ``nbins_zi``. A heuristic default is used if None. @@ -85,15 +83,9 @@ def __init__( satisfy the precondition will produce wrong results. """ validate_droplet_geometry(droplet_geometry) - if droplet_geometry == "spherical" and width_cylinder is not None: - raise ValueError( - "width_cylinder must not be set for spherical analysis " - "(it is only valid for cylinder_x / cylinder_y)." - ) self.parser = parser self.atom_indices = atom_indices self.droplet_geometry = droplet_geometry - self.width_cylinder = width_cylinder self.precentered = precentered if binning_params is None: max_dist = int( @@ -128,12 +120,12 @@ def __init__( else: self.binning_params = binning_params self._initialize_grid() - if self.width_cylinder is None: - if self.droplet_geometry in ("cylinder_x", "cylinder_y"): - if self.droplet_geometry == "cylinder_x": - self.width_cylinder = self.parser.box_size_x(frame_index=0) - elif self.droplet_geometry == "cylinder_y": - self.width_cylinder = self.parser.box_size_y(frame_index=0) + if self.droplet_geometry == "cylinder_x": + self.box_dimension = self.parser.box_size_x(frame_index=0) + elif self.droplet_geometry == "cylinder_y": + self.box_dimension = self.parser.box_size_y(frame_index=0) + else: + self.box_dimension = None def _initialize_grid(self) -> None: """Initialize bin edges, centers and cell sizes from parameters.""" @@ -268,12 +260,7 @@ def binning( bins=(self.xi, self.zi), ) if self.droplet_geometry in ("cylinder_x", "cylinder_y"): - if self.width_cylinder is None: - raise ValueError( - "width_cylinder is required for " - f"droplet_geometry={self.droplet_geometry!r}" - ) - dV = 2.0 * self.width_cylinder * self.dxi * self.dzi + dV = 2.0 * self.box_dimension * self.dxi * self.dzi rho_cc = counts / dV else: # spherical droplet geometry dV_per_row = 2.0 * np.pi * self.xi_cc * self.dxi * self.dzi diff --git a/src/wetting_angle_kit/analysis/slicing/analyzer.py b/src/wetting_angle_kit/analysis/slicing/analyzer.py index cb4da7b..bc24d4e 100644 --- a/src/wetting_angle_kit/analysis/slicing/analyzer.py +++ b/src/wetting_angle_kit/analysis/slicing/analyzer.py @@ -277,18 +277,12 @@ def _run_one_frame(frame_num: int) -> _SlicingFrameResult: if droplet_geometry == "cylinder_x": liquid_positions = liquid_positions[:, [1, 0, 2]] mean_liquid_position = mean_liquid_position[[1, 0, 2]] - box_dimensions = parser.box_size_x(frame_index=frame_num) - elif droplet_geometry == "cylinder_y": - box_dimensions = parser.box_size_y(frame_index=frame_num) - else: - box_dimensions = None predictor = SlicingFrameFitter( liquid_coordinates=liquid_positions, max_dist=max_dist, liquid_geom_center=mean_liquid_position, droplet_geometry=droplet_geometry, delta_gamma=delta_gamma, - width_cylinder=box_dimensions, delta_cylinder=delta_cylinder, points_per_angstrom=points_per_angstrom, ) diff --git a/src/wetting_angle_kit/analysis/slicing/angle_fitting.py b/src/wetting_angle_kit/analysis/slicing/angle_fitting.py index 284e6fc..b3fafee 100644 --- a/src/wetting_angle_kit/analysis/slicing/angle_fitting.py +++ b/src/wetting_angle_kit/analysis/slicing/angle_fitting.py @@ -29,7 +29,6 @@ def __init__( liquid_geom_center: np.ndarray, droplet_geometry: str = "cylinder_y", delta_gamma: float | None = None, - width_cylinder: float | None = None, delta_cylinder: float | None = None, surface_filter_offset: float = 2.0, points_per_angstrom: float = 1.0, @@ -52,10 +51,9 @@ def __init__( delta_gamma : float, optional Angular step (degrees) for spherical droplet geometry (required if spherical). - width_cylinder : float, optional - Extent in slicing axis direction (y or x) for cylindrical droplet geometry. delta_cylinder : float, optional - Step size along slicing axis. + Step size along the slicing axis for cylindrical droplet geometry + (required if cylinder_x / cylinder_y). surface_filter_offset : float, default 2.0 Offset added to minimum droplet height for interface point filtering. points_per_angstrom : float, default 1.0 @@ -69,23 +67,21 @@ def __init__( if droplet_geometry == "spherical": if delta_gamma is None: raise ValueError("delta_gamma must be provided for spherical analysis") - if delta_cylinder is not None or width_cylinder is not None: + if delta_cylinder is not None: raise ValueError( - "delta_cylinder and width_cylinder must not be set for " - "spherical analysis (they are only valid for " - "cylinder_x / cylinder_y)." + "delta_cylinder must not be set for spherical analysis " + "(it is only valid for cylinder_x / cylinder_y)." ) else: # cylinder_x / cylinder_y + if delta_cylinder is None: + raise ValueError( + f"delta_cylinder must be provided for {droplet_geometry}." + ) if delta_gamma is not None: raise ValueError( f"delta_gamma must not be set for {droplet_geometry} " "(it is only valid for spherical)." ) - if delta_cylinder is None or width_cylinder is None: - raise ValueError( - "delta_cylinder and width_cylinder must be provided for " - f"{droplet_geometry}." - ) self.liquid_coordinates = liquid_coordinates self.max_dist = max_dist # Store a copy: predict_contact_angle mutates this in-place per slice @@ -93,7 +89,6 @@ def __init__( self.liquid_geom_center = np.array(liquid_geom_center, copy=True) self.droplet_geometry = droplet_geometry self.delta_gamma = delta_gamma - self.width_cylinder = width_cylinder self.delta_cylinder = delta_cylinder self.surface_filter_offset = surface_filter_offset # Sampling density along each radial ray; raise this (e.g. 2.0 or @@ -108,65 +103,41 @@ def __init__( self.delta_angle = delta_angle def calculate_y_axis_list(self) -> list[float]: - """Return axis position list for the chosen droplet geometry. + """Return the per-slice center position along the slicing axis. - For cylindrical droplets the slice positions sweep from 0 to - ``width_cylinder`` in steps of ``delta_cylinder``. This assumes the - simulation box origin is at 0 along the slicing axis (the LAMMPS - convention). If your box uses a non-zero origin, supply - ``liquid_geom_center`` already shifted into a 0-based frame, or - pre-translate the trajectory before analysis. + For cylindrical droplets the slice positions sweep across the + extent of ``liquid_coordinates`` along the slicing axis (axis 1 + after any caller-applied ``cylinder_x`` rotation) in steps of + ``delta_cylinder``. Because cylindrical droplets are designed to + span the periodic box along their cylinder axis, this is equivalent + to scanning the full box length while avoiding empty slices. Returns ------- list[float] - Y (or X if 'cylinder_x') positions; spherical returns repeated center y. + Y positions of slice centers; for spherical, the droplet center + y is repeated ``180 / delta_gamma`` times. """ if self.droplet_geometry in ("cylinder_y", "cylinder_x"): - if self.width_cylinder is None or self.delta_cylinder is None: - raise ValueError( - "width_cylinder and delta_cylinder are required for " - f"droplet_geometry={self.droplet_geometry!r}" - ) - return list(np.arange(0, self.width_cylinder, self.delta_cylinder)) - if self.droplet_geometry == "spherical": - if self.delta_gamma is None: - raise ValueError( - "delta_gamma is required for droplet_geometry='spherical'" + axis_values = self.liquid_coordinates[:, 1] + return list( + np.arange( + float(axis_values.min()), + float(axis_values.max()), + self.delta_cylinder, ) - return [self.liquid_geom_center[1]] * int(180 / self.delta_gamma) - return [] + ) + if self.delta_gamma is None: + raise ValueError("delta_gamma is required for droplet_geometry='spherical'") + return [self.liquid_geom_center[1]] * int(180 / self.delta_gamma) def calculate_gammas_list(self) -> list[float]: - """Return the gamma tilt angle (degrees) for each slice - of the chosen droplet geometry.""" + """Return the gamma tilt angle (degrees) for each slice.""" if self.droplet_geometry in ("cylinder_y", "cylinder_x"): - if self.width_cylinder is None or self.delta_cylinder is None: - raise ValueError( - "width_cylinder and delta_cylinder are required for " - f"droplet_geometry={self.droplet_geometry!r}" - ) - return [ - 0.0 - for _ in np.arange( - 0, - self.width_cylinder, - self.delta_cylinder, - ) - ] - if self.droplet_geometry == "spherical": - if self.delta_gamma is None: - raise ValueError( - "delta_gamma is required for droplet_geometry='spherical'" - ) - return list( - np.linspace( - 0.0, - 180.0, - int(180 / self.delta_gamma), - ) - ) - return [] + return [0.0] * len(self.calculate_y_axis_list()) + if self.delta_gamma is None: + raise ValueError("delta_gamma is required for droplet_geometry='spherical'") + return list(np.linspace(0.0, 180.0, int(180 / self.delta_gamma))) def surface_definition(self, v_gamma: float) -> tuple[np.ndarray, np.ndarray]: """Sample interface lines for a given gamma. diff --git a/src/wetting_angle_kit/visualization/animator.py b/src/wetting_angle_kit/visualization/animator.py index 7f99db1..1849bae 100644 --- a/src/wetting_angle_kit/visualization/animator.py +++ b/src/wetting_angle_kit/visualization/animator.py @@ -23,9 +23,9 @@ def __init__( liquid_particle_types: set, n_frames: int = 10, droplet_geometry: str = "cylinder_y", - delta_cylinder: int = 5, + delta_cylinder: float | None = None, + delta_gamma: float | None = None, max_dist: int = 100, - width_cylinder: int = 21, precentered: bool = False, ): """ @@ -45,12 +45,14 @@ def __init__( Number of frames to include in the animation. droplet_geometry : str, default "cylinder_y" Droplet geometry passed to SlicingFrameFitter. - delta_cylinder : int, default 5 - Step size along the slicing axis (Å). + delta_cylinder : float, optional + Step size along the slicing axis (Å); required for + ``cylinder_x`` / ``cylinder_y`` modes, must be None for spherical. + delta_gamma : float, optional + Azimuthal step (degrees) for spherical droplet geometry; + required for spherical, must be None for cylinder modes. max_dist : int, default 100 Maximum radial distance for line sampling (Å). - width_cylinder : int, default 21 - Box extent along the cylinder axis (Å). precentered : bool, default False Set True if the trajectory already recenters the droplet at every frame and atoms are not wrapped across periodic @@ -58,6 +60,24 @@ def __init__( skipped. Setting this on a trajectory that does NOT satisfy the precondition will misplace the contact-angle overlay. """ + if droplet_geometry == "spherical": + if delta_gamma is None: + raise ValueError("delta_gamma must be provided for spherical analysis") + if delta_cylinder is not None: + raise ValueError( + "delta_cylinder must not be set for spherical analysis " + "(it is only valid for cylinder_x / cylinder_y)." + ) + elif droplet_geometry in ("cylinder_x", "cylinder_y"): + if delta_cylinder is None: + raise ValueError( + f"delta_cylinder must be provided for {droplet_geometry}." + ) + if delta_gamma is not None: + raise ValueError( + f"delta_gamma must not be set for {droplet_geometry} " + "(it is only valid for spherical)." + ) self.filename = filename self.particle_type_wall = particle_type_wall self.oxygen_type = oxygen_type @@ -66,8 +86,8 @@ def __init__( self.n_frames = n_frames self.droplet_geometry = droplet_geometry self.delta_cylinder = delta_cylinder + self.delta_gamma = delta_gamma self.max_dist = max_dist - self.width_cylinder = width_cylinder self.precentered = precentered # Initialize objects @@ -118,8 +138,8 @@ def generate_animation( liquid_geom_center=liquid_geom_center, droplet_geometry=self.droplet_geometry, delta_cylinder=self.delta_cylinder, + delta_gamma=self.delta_gamma, max_dist=self.max_dist, - width_cylinder=self.width_cylinder, ) angles, surfaces, popt_arrays = processor.predict_contact_angle() if not angles: diff --git a/tests/README b/tests/README index 7209f45..2cefdd9 100644 --- a/tests/README +++ b/tests/README @@ -14,14 +14,20 @@ Layout │ (covers spherical / cylinder_x / cylinder_y) ├── test_edge_cases.py Validation errors, deprecation paths, │ NaN guards, factory rejections - ├── test_visualization.py Smoke tests for the plotting helpers + ├── test_visualization/ Smoke tests for the plotting helpers + │ ├── test_droplet_slice_plot.py + │ └── test_trajectory_plotters.py ├── test_parser/ Per-format parser tests (LAMMPS dump, │ ├── test_parser_dump.py XYZ, ASE) │ ├── test_parser_xyz.py - │ └── test_parser_ase.py - ├── test_contact_angle_methods/ Integration tests for the sliced and - │ ├── test_sliced_method.py binning analyzers on real fixtures - │ └── test_binning_method.py + │ ├── test_parser_ase.py + │ ├── test_water_finders.py + │ └── test_parser_factory.py + ├── test_analysis/ Integration tests for the sliced and + │ ├── test_slicing_method.py binning analyzers on real fixtures + │ ├── test_slicing_edge_cases.py + │ ├── test_binning_method.py + │ ├── test_binning_surface_definition.py └── trajectories/ Fixture trajectories used by integration tests (LAMMPS dump and XYZ/ASE samples) diff --git a/tests/test_analysis/test_binning_method.py b/tests/test_analysis/test_binning_method.py index 99fbad4..19d9570 100644 --- a/tests/test_analysis/test_binning_method.py +++ b/tests/test_analysis/test_binning_method.py @@ -56,7 +56,6 @@ def test_binning_contact_angle_analyzer_with_real_data( parser=LammpsDumpParser(filename), atom_indices=oxygen_indices, droplet_geometry="cylinder_y", - width_cylinder=21, binning_params=binning_params, ) @@ -80,7 +79,6 @@ def test_binning_contact_angle_analyzer_per_frame_with_split_factor( parser=LammpsDumpParser(filename), atom_indices=oxygen_indices, droplet_geometry="cylinder_y", - width_cylinder=21, binning_params=binning_params, ) diff --git a/tests/test_analysis/test_slicing_edge_cases.py b/tests/test_analysis/test_slicing_edge_cases.py index 079178a..df51f01 100644 --- a/tests/test_analysis/test_slicing_edge_cases.py +++ b/tests/test_analysis/test_slicing_edge_cases.py @@ -10,10 +10,16 @@ ) -def _simple_predictor(droplet_geometry="cylinder_y", **kwargs): +def _simple_predictor( + droplet_geometry="cylinder_y", + liquid_coordinates=None, + **kwargs, +): """Return a minimally-initialised SlicingFrameFitter with required attrs.""" + if liquid_coordinates is None: + liquid_coordinates = np.zeros((10, 3)) return SlicingFrameFitter( - liquid_coordinates=np.zeros((10, 3)), + liquid_coordinates=liquid_coordinates, max_dist=20, liquid_geom_center=np.array([0.0, 0.0, 0.0]), droplet_geometry=droplet_geometry, @@ -21,76 +27,47 @@ def _simple_predictor(droplet_geometry="cylinder_y", **kwargs): ) -def test_calculate_y_axis_requires_cylinder_widths(): - predictor = _simple_predictor( - droplet_geometry="cylinder_y", width_cylinder=10.0, delta_cylinder=2.0 - ) - # Now break them to force the validation branch in calculate_y_axis_list. - predictor.width_cylinder = None - with pytest.raises(ValueError, match="width_cylinder and delta_cylinder"): - predictor.calculate_y_axis_list() - - -def test_calculate_gammas_requires_cylinder_widths(): - predictor = _simple_predictor( - droplet_geometry="cylinder_y", width_cylinder=10.0, delta_cylinder=2.0 - ) - predictor.delta_cylinder = None - with pytest.raises(ValueError, match="width_cylinder and delta_cylinder"): - predictor.calculate_gammas_list() - - -def test_spherical_calculations_require_delta_gamma(): - predictor = _simple_predictor(droplet_geometry="spherical", delta_gamma=10.0) - predictor.delta_gamma = None - with pytest.raises(ValueError, match="delta_gamma is required"): - predictor.calculate_y_axis_list() - with pytest.raises(ValueError, match="delta_gamma is required"): - predictor.calculate_gammas_list() - - def test_spherical_constructor_requires_delta_gamma(): with pytest.raises(ValueError, match="delta_gamma must be provided"): _simple_predictor(droplet_geometry="spherical") -def test_cylinder_constructor_raises_without_widths(): - with pytest.raises(ValueError, match="delta_cylinder and width_cylinder"): +def test_cylinder_constructor_requires_delta_cylinder(): + with pytest.raises(ValueError, match="delta_cylinder must be provided"): _simple_predictor(droplet_geometry="cylinder_y") def test_find_intersection_returns_none_when_circle_does_not_intersect_baseline(): - predictor = _simple_predictor( - droplet_geometry="cylinder_y", width_cylinder=10.0, delta_cylinder=2.0 - ) + predictor = _simple_predictor(droplet_geometry="cylinder_y", delta_cylinder=2.0) # Circle center far below the baseline → no intersection popt = (0.0, -10.0, 1.0) assert predictor.find_intersection(popt, y_line=5.0) is None def test_find_intersection_returns_angle_for_intersecting_circle(): - predictor = _simple_predictor( - droplet_geometry="cylinder_y", width_cylinder=10.0, delta_cylinder=2.0 - ) + predictor = _simple_predictor(droplet_geometry="cylinder_y", delta_cylinder=2.0) # Circle of radius 5 at z=0, baseline at z=0 → contact angle = 90°. popt = (0.0, 0.0, 5.0) angle = predictor.find_intersection(popt, y_line=0.0) assert angle == pytest.approx(90.0) -def test_calculate_y_axis_cylinder(): +def test_calculate_y_axis_cylinder_spans_liquid_extent(): + # Liquid y-extent runs 0..10; with delta=2.5 expect 4 slices. + liquid = np.column_stack( + [np.zeros(5), np.array([0.0, 2.5, 5.0, 7.5, 10.0]), np.zeros(5)] + ) predictor = _simple_predictor( - droplet_geometry="cylinder_y", width_cylinder=10.0, delta_cylinder=2.5 + droplet_geometry="cylinder_y", + liquid_coordinates=liquid, + delta_cylinder=2.5, ) assert predictor.calculate_y_axis_list() == [0.0, 2.5, 5.0, 7.5] assert predictor.calculate_gammas_list() == [0.0, 0.0, 0.0, 0.0] def test_calculate_y_axis_spherical(): - predictor = _simple_predictor( - droplet_geometry="spherical", - delta_gamma=90.0, - ) + predictor = _simple_predictor(droplet_geometry="spherical", delta_gamma=90.0) # 180 / 90 = 2 entries; y_axis_list mirrors liquid_geom_center[1] each entry. y_axis = predictor.calculate_y_axis_list() gammas = predictor.calculate_gammas_list() diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 1e7ad11..65395f8 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -59,11 +59,11 @@ def test_contact_angle_slicing_copies_geometric_center(): np.testing.assert_array_equal(center, np.array([1.0, 2.0, 3.0])) -# --- Cylindrical mode without delta_cylinder/width_cylinder raises --- +# --- Cylindrical mode without delta_cylinder raises --- -def test_slicing_cylinder_without_width_raises(): - with pytest.raises(ValueError, match="delta_cylinder and width_cylinder"): +def test_slicing_cylinder_without_delta_cylinder_raises(): + with pytest.raises(ValueError, match="delta_cylinder"): SlicingFrameFitter( liquid_coordinates=np.zeros((3, 3)), max_dist=10, diff --git a/tests/test_visualization/test_droplet_slice_plot.py b/tests/test_visualization/test_droplet_slice_plot.py index 437618d..669c552 100644 --- a/tests/test_visualization/test_droplet_slice_plot.py +++ b/tests/test_visualization/test_droplet_slice_plot.py @@ -112,7 +112,6 @@ def test_contact_angle_animator_init_loads_fixture(): droplet_geometry="cylinder_y", delta_cylinder=20, max_dist=50, - width_cylinder=20, ) assert animator.wall_coords.shape[1] == 3 assert animator.oxygen_indices.size > 0 @@ -135,7 +134,6 @@ def test_contact_angle_animator_generates_html(tmp_path): droplet_geometry="cylinder_y", delta_cylinder=20, max_dist=50, - width_cylinder=21, ) animator.generate_animation(output_filename=str(output)) assert output.exists() diff --git a/wetting_angle_kit_JOSS/paper.md b/wetting_angle_kit_JOSS/paper.md index b4b9d7e..d128133 100644 --- a/wetting_angle_kit_JOSS/paper.md +++ b/wetting_angle_kit_JOSS/paper.md @@ -23,7 +23,7 @@ authors: affiliation: "2" - name: Gian-Marco Rignanese orcid: 0000-0002-1422-1205 - affiliation: "1 ,3" + affiliation: "1, 3" - name: David Waroquiers orcid: 0000-0001-8943-9762 affiliation: "1" @@ -203,12 +203,12 @@ and encourages community-driven extensions. The package is expected to be partic useful for researchers using various types of force fields (classical and MLIPs) or investigating nanoscale interfacial phenomena. -# AI usage disclosure +# AI usage disclosure Generative AI tools were used in the development of the software, for drafting and assisting debugging. Generative AI was used to assist in refining the language, -traduction and clarity of the manuscript and docstring. +translation and clarity of the manuscript and docstring. All AI-assisted contributions were verified and approved by the authors. # Acknowledgements diff --git a/wetting_angle_kit_JOSS/paper_old.pdf b/wetting_angle_kit_JOSS/paper_old.pdf deleted file mode 100644 index 47dcd926fb2e8af9a5dd7073715e1266d9400d1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146909 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 Date: Tue, 2 Jun 2026 12:00:22 +0200 Subject: [PATCH 10/31] Removed particle_wall_type that was not used. --- src/wetting_angle_kit/parsers/__init__.py | 2 ++ src/wetting_angle_kit/parsers/ase.py | 5 ----- src/wetting_angle_kit/parsers/factory.py | 9 +-------- src/wetting_angle_kit/parsers/lammps_dump.py | 5 ----- src/wetting_angle_kit/parsers/xyz.py | 4 ---- src/wetting_angle_kit/visualization/animator.py | 5 ----- tests/test_analysis/test_binning_method.py | 4 +--- tests/test_analysis/test_slicing_method.py | 4 +--- tests/test_parser/test_parser_factory.py | 4 ---- tests/test_parser/test_water_finders.py | 17 +++++++---------- .../test_droplet_slice_plot.py | 2 -- 11 files changed, 12 insertions(+), 49 deletions(-) diff --git a/src/wetting_angle_kit/parsers/__init__.py b/src/wetting_angle_kit/parsers/__init__.py index 9aa3068..8d8b692 100644 --- a/src/wetting_angle_kit/parsers/__init__.py +++ b/src/wetting_angle_kit/parsers/__init__.py @@ -4,6 +4,7 @@ AseWaterFinder, ) from wetting_angle_kit.parsers.base import BaseParser +from wetting_angle_kit.parsers.factory import get_water_finder from wetting_angle_kit.parsers.lammps_dump import ( LammpsDumpParser, LammpsDumpWallParser, @@ -24,4 +25,5 @@ "LammpsDumpWaterFinder", "XYZParser", "XYZWaterFinder", + "get_water_finder", ] diff --git a/src/wetting_angle_kit/parsers/ase.py b/src/wetting_angle_kit/parsers/ase.py index ff96f1b..f9d5b6c 100644 --- a/src/wetting_angle_kit/parsers/ase.py +++ b/src/wetting_angle_kit/parsers/ase.py @@ -114,7 +114,6 @@ class AseWaterFinder: def __init__( self, filepath: str, - particle_type_wall: list[str], oxygen_type: str = "O", hydrogen_type: str = "H", oh_cutoff: float = 1.2, @@ -124,9 +123,6 @@ def __init__( ---------- filepath : str Path to ASE-readable trajectory. - particle_type_wall : sequence[str] - Symbols representing wall particles (unused presently, reserved for - filtering). oxygen_type : str, default "O" Oxygen atom symbol. hydrogen_type : str, default "H" @@ -146,7 +142,6 @@ def __init__( self._NeighborList = NeighborList self.trajectory = self._ase_read(filepath, index=":") _validate_ase_trajectory_orthogonal(self.trajectory) - self.particle_type_wall = particle_type_wall self.oxygen_type = oxygen_type self.hydrogen_type = hydrogen_type self.oh_cutoff = oh_cutoff diff --git a/src/wetting_angle_kit/parsers/factory.py b/src/wetting_angle_kit/parsers/factory.py index 2928cb8..2983e2f 100644 --- a/src/wetting_angle_kit/parsers/factory.py +++ b/src/wetting_angle_kit/parsers/factory.py @@ -8,7 +8,6 @@ def get_water_finder( filename: str, - particle_type_wall: Any, oxygen_type: Any, hydrogen_type: Any, ) -> Any: @@ -18,8 +17,6 @@ def get_water_finder( ---------- filename : str Path to trajectory file; extension determines the finder class. - particle_type_wall : Any - Wall particle type identifiers forwarded to the finder constructor. oxygen_type : Any Oxygen type identifier (symbol or integer depending on file format). hydrogen_type : Any @@ -33,20 +30,16 @@ def get_water_finder( ext = os.path.splitext(filename)[-1].lower() if ext == ".lammpstrj": - return LammpsDumpWaterFinder( - filename, particle_type_wall, oxygen_type, hydrogen_type - ) + return LammpsDumpWaterFinder(filename, oxygen_type, hydrogen_type) elif ext in (".traj", ".ase"): return AseWaterFinder( filename, - particle_type_wall, oxygen_type=oxygen_type, hydrogen_type=hydrogen_type, ) elif ext == ".xyz": return XYZWaterFinder( filename, - particle_type_wall, oxygen_type=oxygen_type, hydrogen_type=hydrogen_type, ) diff --git a/src/wetting_angle_kit/parsers/lammps_dump.py b/src/wetting_angle_kit/parsers/lammps_dump.py index 5afb094..c9e8b23 100644 --- a/src/wetting_angle_kit/parsers/lammps_dump.py +++ b/src/wetting_angle_kit/parsers/lammps_dump.py @@ -266,7 +266,6 @@ class LammpsDumpWaterFinder: def __init__( self, filepath: str, - particle_type_wall: set, oxygen_type: int = 3, hydrogen_type: int = 2, oh_cutoff: float = 1.2, @@ -276,9 +275,6 @@ def __init__( ---------- filepath : str Path to LAMMPS dump file. - particle_type_wall : set - Particle type IDs corresponding to wall atoms (reserved for future - filtering). oxygen_type : int, default 3 LAMMPS particle type ID for oxygen atoms. hydrogen_type : int, default 2 @@ -287,7 +283,6 @@ def __init__( O-H distance cutoff (Å) for water molecule detection. """ self.filepath = filepath - self.particle_type_wall = particle_type_wall self.oxygen_type = oxygen_type self.hydrogen_type = hydrogen_type self.oh_cutoff = oh_cutoff diff --git a/src/wetting_angle_kit/parsers/xyz.py b/src/wetting_angle_kit/parsers/xyz.py index cb58ea5..8ab2113 100644 --- a/src/wetting_angle_kit/parsers/xyz.py +++ b/src/wetting_angle_kit/parsers/xyz.py @@ -142,7 +142,6 @@ class XYZWaterFinder: def __init__( self, filepath: str, - particle_type_wall: Any, oxygen_type: str = "O", hydrogen_type: str = "H", oh_cutoff: float = 1.2, @@ -152,8 +151,6 @@ def __init__( ---------- filepath : str Path to XYZ file. - particle_type_wall : sequence[str] - Symbols that represent wall (excluded) particles. oxygen_type : str, default "O" Oxygen atom symbol. hydrogen_type : str, default "H" @@ -162,7 +159,6 @@ def __init__( Distance cutoff (Å) for O-H bonding to identify water molecules. """ self.filepath = filepath - self.particle_type_wall = particle_type_wall self.oxygen_type = oxygen_type self.hydrogen_type = hydrogen_type self.oh_cutoff = oh_cutoff diff --git a/src/wetting_angle_kit/visualization/animator.py b/src/wetting_angle_kit/visualization/animator.py index 1849bae..c8f9c09 100644 --- a/src/wetting_angle_kit/visualization/animator.py +++ b/src/wetting_angle_kit/visualization/animator.py @@ -17,7 +17,6 @@ class ContactAngleAnimator: def __init__( self, filename: str, - particle_type_wall: set, oxygen_type: int, hydrogen_type: int, liquid_particle_types: set, @@ -33,8 +32,6 @@ def __init__( ---------- filename : str Path to LAMMPS dump trajectory file. - particle_type_wall : set - LAMMPS particle type IDs for wall atoms. oxygen_type : int LAMMPS particle type ID for oxygen atoms. hydrogen_type : int @@ -79,7 +76,6 @@ def __init__( "(it is only valid for spherical)." ) self.filename = filename - self.particle_type_wall = particle_type_wall self.oxygen_type = oxygen_type self.hydrogen_type = hydrogen_type self.liquid_particle_types = liquid_particle_types @@ -93,7 +89,6 @@ def __init__( # Initialize objects self.wat_find = LammpsDumpWaterFinder( self.filename, - particle_type_wall=self.particle_type_wall, oxygen_type=self.oxygen_type, hydrogen_type=self.hydrogen_type, ) diff --git a/tests/test_analysis/test_binning_method.py b/tests/test_analysis/test_binning_method.py index 19d9570..fe78bb1 100644 --- a/tests/test_analysis/test_binning_method.py +++ b/tests/test_analysis/test_binning_method.py @@ -20,9 +20,7 @@ def filename(): @pytest.fixture def wat_find(filename): - return LammpsDumpWaterFinder( - filename, particle_type_wall={3}, oxygen_type=1, hydrogen_type=2 - ) + return LammpsDumpWaterFinder(filename, oxygen_type=1, hydrogen_type=2) @pytest.fixture diff --git a/tests/test_analysis/test_slicing_method.py b/tests/test_analysis/test_slicing_method.py index 45852c0..708f0ef 100644 --- a/tests/test_analysis/test_slicing_method.py +++ b/tests/test_analysis/test_slicing_method.py @@ -20,9 +20,7 @@ def filename(): @pytest.fixture def wat_find(filename): - return LammpsDumpWaterFinder( - filename, particle_type_wall={3}, oxygen_type=1, hydrogen_type=2 - ) + return LammpsDumpWaterFinder(filename, oxygen_type=1, hydrogen_type=2) @pytest.fixture diff --git a/tests/test_parser/test_parser_factory.py b/tests/test_parser/test_parser_factory.py index fa74aba..e751cbc 100644 --- a/tests/test_parser/test_parser_factory.py +++ b/tests/test_parser/test_parser_factory.py @@ -11,7 +11,6 @@ def test_get_water_finder_lammpstrj(): pytest.importorskip("ovito") finder = get_water_finder( trajectory_path("traj_spherical_drop_4k.lammpstrj"), - particle_type_wall={1}, oxygen_type=3, hydrogen_type=2, ) @@ -21,7 +20,6 @@ def test_get_water_finder_lammpstrj(): def test_get_water_finder_ase_traj(): finder = get_water_finder( trajectory_path("slice_10_mace_mlips_cylindrical_2_5.traj"), - particle_type_wall=["C"], oxygen_type="O", hydrogen_type="H", ) @@ -31,7 +29,6 @@ def test_get_water_finder_ase_traj(): def test_get_water_finder_xyz(): finder = get_water_finder( trajectory_path("slice_10_mace_mlips_cylindrical_2_5.xyz"), - particle_type_wall=["C"], oxygen_type="O", hydrogen_type="H", ) @@ -42,7 +39,6 @@ def test_get_water_finder_unsupported_extension(): with pytest.raises(ValueError, match="Unsupported file format"): get_water_finder( "/tmp/does_not_matter.pdb", - particle_type_wall=set(), oxygen_type="O", hydrogen_type="H", ) diff --git a/tests/test_parser/test_water_finders.py b/tests/test_parser/test_water_finders.py index c772be7..43d2428 100644 --- a/tests/test_parser/test_water_finders.py +++ b/tests/test_parser/test_water_finders.py @@ -43,16 +43,14 @@ def water_xyz(tmp_path): def test_xyz_water_finder_identifies_oxygens(water_xyz): - finder = XYZWaterFinder( - water_xyz, particle_type_wall=["C"], oxygen_type="O", hydrogen_type="H" - ) + finder = XYZWaterFinder(water_xyz, oxygen_type="O", hydrogen_type="H") indices = finder.get_water_oxygen_indices(0) # Two oxygens are at indices 0 and 3 in the input order. assert sorted(indices.tolist()) == [0, 3] def test_xyz_water_finder_positions(water_xyz): - finder = XYZWaterFinder(water_xyz, particle_type_wall=["C"]) + finder = XYZWaterFinder(water_xyz) positions = finder.get_water_oxygen_positions(0) assert positions.shape == (2, 3) # Centers of the two waters. @@ -66,27 +64,27 @@ def test_xyz_water_finder_no_water_returns_empty(tmp_path): '1\nLattice="10.0 0.0 0.0 0.0 10.0 0.0 0.0 0.0 10.0" ' "Properties=species:S:1:pos:R:3\nO 0.0 0.0 0.0\n" ) - finder = XYZWaterFinder(str(p), particle_type_wall=["C"]) + finder = XYZWaterFinder(str(p)) positions = finder.get_water_oxygen_positions(0) assert positions.shape == (0, 3) def test_xyz_water_finder_parse_filters_liquid(water_xyz): - finder = XYZWaterFinder(water_xyz, particle_type_wall=["C"]) + finder = XYZWaterFinder(water_xyz) positions = finder.parse(["O", "H"], 0) # Six liquid atoms (2 O + 4 H), wall (C) excluded. assert positions.shape == (6, 3) def test_xyz_water_finder_box_length_max(water_xyz): - finder = XYZWaterFinder(water_xyz, particle_type_wall=["C"]) + finder = XYZWaterFinder(water_xyz) assert finder.box_length_max(0) == pytest.approx(20.0) def test_xyz_water_finder_box_length_max_without_lattice_raises(tmp_path): p = tmp_path / "no_lattice.xyz" _write_water_xyz(p, with_lattice=False) - finder = XYZWaterFinder(str(p), particle_type_wall=["C"]) + finder = XYZWaterFinder(str(p)) with pytest.raises(ValueError, match="No Lattice="): finder.box_length_max(0) @@ -117,7 +115,6 @@ def water_extxyz(tmp_path): def test_ase_water_finder_identifies_oxygens(water_extxyz): finder = AseWaterFinder( water_extxyz, - particle_type_wall=["C"], oxygen_type="O", hydrogen_type="H", ) @@ -126,7 +123,7 @@ def test_ase_water_finder_identifies_oxygens(water_extxyz): def test_ase_water_finder_positions(water_extxyz): - finder = AseWaterFinder(water_extxyz, particle_type_wall=["C"]) + finder = AseWaterFinder(water_extxyz) positions = finder.get_water_oxygen_positions(0) assert positions.shape == (2, 3) diff --git a/tests/test_visualization/test_droplet_slice_plot.py b/tests/test_visualization/test_droplet_slice_plot.py index 669c552..3fc334c 100644 --- a/tests/test_visualization/test_droplet_slice_plot.py +++ b/tests/test_visualization/test_droplet_slice_plot.py @@ -104,7 +104,6 @@ def test_contact_angle_animator_init_loads_fixture(): pytest.importorskip("ovito") animator = ContactAngleAnimator( filename=trajectory_path("traj_spherical_drop_4k.lammpstrj"), - particle_type_wall={3}, oxygen_type=1, hydrogen_type=2, liquid_particle_types={1, 2}, @@ -126,7 +125,6 @@ def test_contact_angle_animator_generates_html(tmp_path): output = tmp_path / "animation.html" animator = ContactAngleAnimator( filename=trajectory_path("traj_10_3_330w_nve_4k_reajust.lammpstrj"), - particle_type_wall={3}, oxygen_type=1, hydrogen_type=2, liquid_particle_types={1, 2}, From 8dced3a425e4c98a171f480c0b7f38dfe12cd117 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 12:00:47 +0200 Subject: [PATCH 11/31] Removed particle_wall_type in docs. --- docs/examples/binning_ca.py | 1 - docs/examples/parsing_trajectory_files.py | 2 -- docs/examples/slicing_ca.py | 1 - docs/examples/visualisation_slicing_traj.py | 4 +--- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/examples/binning_ca.py b/docs/examples/binning_ca.py index dc1801f..01972d4 100644 --- a/docs/examples/binning_ca.py +++ b/docs/examples/binning_ca.py @@ -9,7 +9,6 @@ # This identifies O and H atoms in water molecules wat_find = LammpsDumpWaterFinder( filename, - particle_type_wall={3}, # Wall atom types oxygen_type=1, # Oxygen atom type hydrogen_type=2, # Hydrogen atom type ) diff --git a/docs/examples/parsing_trajectory_files.py b/docs/examples/parsing_trajectory_files.py index caa476f..fc3bb9e 100644 --- a/docs/examples/parsing_trajectory_files.py +++ b/docs/examples/parsing_trajectory_files.py @@ -20,7 +20,6 @@ # --- Initialize water molecule finder --- wat_find = LammpsDumpWaterFinder( filename, - particle_type_wall={3}, # atom type for wall oxygen_type=1, # atom type for oxygen hydrogen_type=2, # atom type for hydrogen ) @@ -57,7 +56,6 @@ # --- Initialize water molecule finder --- wat_find = AseWaterFinder( filename, - particle_type_wall=["C"], # element name for wall oh_cutoff=1.2, # O–H bond cutoff (Å); ASE NeighborList handles the # per-atom splitting internally now. ) diff --git a/docs/examples/slicing_ca.py b/docs/examples/slicing_ca.py index 900e375..7f9b614 100644 --- a/docs/examples/slicing_ca.py +++ b/docs/examples/slicing_ca.py @@ -13,7 +13,6 @@ # --- Step 2: Identify the water molecules (oxygen-bonded-to-two-H) --- wat_find = LammpsDumpWaterFinder( filename, - particle_type_wall={3}, # Wall atom types oxygen_type=1, hydrogen_type=2, ) diff --git a/docs/examples/visualisation_slicing_traj.py b/docs/examples/visualisation_slicing_traj.py index 4110aea..ff89a2f 100644 --- a/docs/examples/visualisation_slicing_traj.py +++ b/docs/examples/visualisation_slicing_traj.py @@ -20,9 +20,7 @@ filename = "../../tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj" # --- 2. Identify Water Molecules --- -wat_find = LammpsDumpWaterFinder( - filename, particle_type_wall={3}, oxygen_type=1, hydrogen_type=2 -) +wat_find = LammpsDumpWaterFinder(filename, oxygen_type=1, hydrogen_type=2) oxygen_indices = wat_find.get_water_oxygen_ids(frame_index=0) print("Number of water molecules detected:", len(oxygen_indices)) From ca817ddd6fa8f3d1dfb4b4c0128b36bd05575d3e Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 12:55:03 +0200 Subject: [PATCH 12/31] Removed deprecated frame_tot. --- src/wetting_angle_kit/parsers/base.py | 11 ----------- tests/test_parser/test_parser_dump.py | 7 ------- 2 files changed, 18 deletions(-) diff --git a/src/wetting_angle_kit/parsers/base.py b/src/wetting_angle_kit/parsers/base.py index fd9c097..e38d127 100644 --- a/src/wetting_angle_kit/parsers/base.py +++ b/src/wetting_angle_kit/parsers/base.py @@ -42,17 +42,6 @@ def parse(self, frame_index: int, indices: np.ndarray | None = None) -> np.ndarr def frame_count(self) -> int: """Return the total number of frames in the trajectory.""" - def frame_tot(self) -> int: - """Return the total number of frames available. (Legacy name).""" - import warnings - - warnings.warn( - "frame_tot is deprecated, use frame_count instead.", - DeprecationWarning, - stacklevel=2, - ) - return self.frame_count() - def box_size_x(self, frame_index: int) -> float: """Return the x-dimension of the simulation box for a frame. diff --git a/tests/test_parser/test_parser_dump.py b/tests/test_parser/test_parser_dump.py index dc6fbdc..a28b626 100644 --- a/tests/test_parser/test_parser_dump.py +++ b/tests/test_parser/test_parser_dump.py @@ -75,13 +75,6 @@ def test_frame_count(dump_parser): assert total_frames > 0 -# --- frame_tot is a deprecated alias for frame_count --- -def test_frame_tot_emits_deprecation_warning(dump_parser): - with pytest.warns(DeprecationWarning, match="frame_tot is deprecated"): - total = dump_parser.frame_tot() - assert total == dump_parser.frame_count() - - # --- Test non-orthogonal cell rejection --- def _write_triclinic_dump(path): path.write_text( From 0a59594d4f63029606601c8a592a7a9de7ed17e6 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 13:22:10 +0200 Subject: [PATCH 13/31] Removed .md tutorials from docs. --- .gitignore | 2 +- docs/tutorials/Binning_method_tuto.md | 126 --------------- docs/tutorials/Parser_tutorial.md | 117 -------------- docs/tutorials/Slicing_method_tuto.md | 149 ------------------ .../Visualization_slicing_droplet.md | 98 ------------ 5 files changed, 1 insertion(+), 491 deletions(-) delete mode 100644 docs/tutorials/Binning_method_tuto.md delete mode 100644 docs/tutorials/Parser_tutorial.md delete mode 100644 docs/tutorials/Slicing_method_tuto.md delete mode 100644 docs/tutorials/Visualization_slicing_droplet.md diff --git a/.gitignore b/.gitignore index a5653eb..8392b0c 100644 --- a/.gitignore +++ b/.gitignore @@ -78,7 +78,7 @@ docs/build/generate-stamp docs/source/changelog.rst docs/build_log.txt -docs/tutorials/result_dump_spherical_slicing +docs/tutorials # PyBuilder .pybuilder/ diff --git a/docs/tutorials/Binning_method_tuto.md b/docs/tutorials/Binning_method_tuto.md deleted file mode 100644 index 049a89a..0000000 --- a/docs/tutorials/Binning_method_tuto.md +++ /dev/null @@ -1,126 +0,0 @@ -# Tutorial: Contact Angle Analysis (Binning Method) - -This tutorial demonstrates how to compute the contact angle using the **binning method** in `wetting_angle_kit`. -The method divides the simulation box into spatial bins to calculate the liquid–solid interface and the corresponding contact angle, for a group of frames. - ---- - -## 1. Overview - -The **binning method** works by: -1. Collecting the positions of water molecules (typically oxygen atoms). -2. Dividing the region of interest into bins in the **x–z** plane. -3. Computing density profiles and fitting the interface shape. -4. Deriving the contact angle from the interface curvature. - ---- - -## 2. Prerequisites - -Your trajectory file (e.g., a LAMMPS dump file) contain: -- Atom IDs, types, and positions -- Liquid particles (in this cas Water molecules: O and H atoms) - -Example trajectory: -``` -tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj -``` - ---- - -## 3. Example Script - -```python -# Import necessary modules -from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder -from wetting_angle_kit.analysis import BinningTrajectoryAnalyzer - -# --- Step 1: Define the trajectory file --- -filename = "../../tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj" - -# --- Step 2: Initialize the water molecule finder --- -# This identifies O and H atoms in water molecules -wat_find = LammpsDumpWaterFinder( - filename, - particle_type_wall={3}, # Wall atom types - oxygen_type=1, # Oxygen atom type - hydrogen_type=2, # Hydrogen atom type -) - -# --- Step 3: Get oxygen atom indices for the first frame --- -oxygen_indices = wat_find.get_water_oxygen_ids(frame_index=0) -print("Number of water molecules:", len(oxygen_indices)) - -# --- Step 4: Define binning parameters --- -binning_params = { - "xi_0": 0.0, # Minimum x-coordinate - "xi_f": 100.0, # Maximum x-coordinate - "nbins_xi": 50, # Number of bins along x - "zi_0": 0.0, # Minimum z-coordinate - "zi_f": 100.0, # Maximum z-coordinate - "nbins_zi": 25, # Number of bins along z -} - -# --- Step 5: Initialize the parser --- -parser = LammpsDumpParser(filename) - -# --- Step 6: Create the contact angle analyzer --- -analyzer = BinningTrajectoryAnalyzer( - parser=parser, - atom_indices=oxygen_indices, - droplet_geometry="cylinder_y", # Interface fitting model - binning_params=binning_params, -) - -# --- Step 7: Run analysis for a frame range --- -results = analyzer.analyze([1]) # Analyze frame 1 -print("Analysis results:", results) -``` - ---- - -## 4. Output - -Running this example will: -- Parse the trajectory -- Compute the interface shape and local contact angle -- Save results (if enabled) under `results_binned_example/` - -Example printed output: -``` -Number of water molecules: 4000 - -```sh -xi range: (0.22795857644950415,41.63623606829102) -zi range: (7.54989,47.3742) - -Number of fluid particles in batch: 4000.0 - -Binning with model: spherical ... -Advancement: 0.00% -Advancement: 35.71% -Advancement: 71.43% - -Fitted parameters for batch: -rho1:-3.387136459516587e-05 -rho2:0.03389671977759232 -R_eq:37.22899870907881 -zi_c:9.244210981996149 -zi_0:6.265045941194059 -t1:-4.384696208816467 -t2:0.07378719793487698 - -Contact angle for batch: 94.58987060394456 -``` -A heat map representation of the particles density and the fitted semi-circle to get the contact angle. - -![Heat maps density particles](../images/bin_plot.png) ---- - -## 5. Tips - -- Adjust `xi_f`, `zi_f`, and the bin counts (`nbins_xi`, `nbins_zi`) according to your simulation box dimensions. -- If the wall surface is not flat or the system is tilted, pre-align it before analysis. -- For multiple frames: `analyzer.analyze(range(0, 100, 10))`. - ---- diff --git a/docs/tutorials/Parser_tutorial.md b/docs/tutorials/Parser_tutorial.md deleted file mode 100644 index 31dce30..0000000 --- a/docs/tutorials/Parser_tutorial.md +++ /dev/null @@ -1,117 +0,0 @@ -# Tutorial: Using the Parser Module - -This tutorial shows how to load different trajectory formats using the `wetting_angle_kit.parsers` submodule. - -The parser provides a unified interface to read atomic coordinates from: -- LAMMPS dump files (`LammpsDumpParser`, ` LammpsDumpWaterFinder`) -- ASE `.traj` files (`AseParser`, `AseWaterFinder`) -- XYZ files (`XYZParser`) - -Each parser can extract atomic positions for selected frames and atoms, allowing efficient and flexible analysis of molecular simulations. - ---- - -## 1. General Workflow - -Every parser follows the same pattern: - -1. Initialize the parser with your trajectory file. -2. (Optional) Use a `WaterMoleculeFinder` class to locate oxygen atoms belonging to water molecules. -3. Extract coordinates of all atoms or only selected indices using `.parse(frame_index, indices)`. - -The `.parse()` method always returns a NumPy array of shape `(N, 3)` containing the atomic coordinates. - ---- - -## 2. Example: LAMMPS Dump File -```python -from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder - -# --- Step 1: Define the trajectory file --- -filename = "../../tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj" - -# --- Step 2: Initialize the water molecule finder --- -# Specify particle types for the wall and for water oxygens and hydrogens -wat_find = LammpsDumpWaterFinder( - filename, particle_type_wall={3}, oxygen_type=1, hydrogen_type=2 -) - -# --- Step 3: Identify oxygen atoms for frame 0 --- -oxygen_indices = wat_find.get_water_oxygen_ids(frame_index=0) -print("Number of water molecules:", len(oxygen_indices)) - -# --- Step 4: Initialize the parser --- -parser = LammpsDumpParser(filename) - -# --- Step 5: Extract coordinates of only the water oxygens --- -oxygen_positions = parser.parse(frame_index=0, indices=oxygen_indices) -print("Extracted positions for", len(oxygen_positions), "oxygen atoms.") -``` - -**Notes:** -- Use `indices=None` to parse all atoms. -- `.parse()` returns NumPy coordinates for the selected frame. - ---- - -## 3. Example: ASE Trajectory File -```python -from wetting_angle_kit.parsers import AseParser, AseWaterFinder - -# --- Step 1: Define the trajectory file --- -filename = "../../tests/trajectories/slice_10_mace_mlips_cylindrical_2_5.traj" - -# --- Step 2: Initialize the water molecule finder --- -wat_find = AseWaterFinder( - filename, - particle_type_wall=["C"], # Wall elements (e.g., carbon) - oh_cutoff=1.2, # O–H bond cutoff (Å); ASE NeighborList uses half this per atom -) - -# --- Step 3: Identify water oxygens for frame 0 --- -oxygen_indices = wat_find.get_water_oxygen_indices(frame_index=0) -print("Number of water molecules:", len(oxygen_indices)) - -# --- Step 4: Initialize the parser --- -parser = AseParser(filename) - -# --- Step 5: Extract oxygen atom positions only --- -oxygen_positions = parser.parse(frame_index=0, indices=oxygen_indices) -print("Extracted positions for", len(oxygen_positions), "oxygen atoms.") -``` - -**Tip:** The ASE parser works for any ASE-compatible trajectory (e.g., `.traj`, `.extxyz`, etc.). - ---- - -## 4. Example: XYZ File - -```python -from wetting_angle_kit.parsers import XYZParser - -# --- Step 1: Define the trajectory file --- -filename = "../../tests/trajectories/slice_10_mace_mlips_cylindrical_2_5.xyz" - -# --- Step 2: Initialize the parser --- -xyz_parser = XYZParser(filename) - -# --- Step 3: Retrieve positions for the first frame --- -positions = xyz_parser.parse(frame_index=0) -print("Loaded frame with", len(positions), "atoms.") - -# --- Step 4 (Optional): Parse only a subset of atoms --- -# For example, extract the first 50 atoms -subset_positions = xyz_parser.parse(frame_index=0, indices=list(range(50))) -print("Subset of 50 atoms extracted successfully.") -``` - ---- - -## 5. Summary - -The parser module provides: -- **Unified interface** across LAMMPS, ASE, and XYZ formats -- **Selective parsing** using frame number and atom indices -- **Water molecule identification** to filter oxygen atoms from bulk water with tools from ase and ovito library - -All parsers return NumPy arrays of shape `(N, 3)` for direct use in analysis pipelines. diff --git a/docs/tutorials/Slicing_method_tuto.md b/docs/tutorials/Slicing_method_tuto.md deleted file mode 100644 index 707584a..0000000 --- a/docs/tutorials/Slicing_method_tuto.md +++ /dev/null @@ -1,149 +0,0 @@ -# Tutorial: Contact Angle Analysis (Slicing Method) - -This tutorial explains how to compute the contact angle of a droplet using the **slicing method** in `wetting_angle_kit`. - ---- - -## 1. Overview - -The **slicing method** divides the droplet into slices (along the z-axis) and fits a geometric model (e.g. spherical) to the liquid–solid interface profile. -This is ideal for study the evolution of the angles among a trajectory. - ---- - -## 2. Requirements - -Before running the example, ensure you have installed: -````bash -pip install wetting-angle-kit ase numpy -```` - -Example trajectory: -````bash -tests/trajectories/traj_spherical_drop_4k.lammpstrj -```` - ---- - -## 3. Example Code - -````python -# Import necessary modules -from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder -from wetting_angle_kit.analysis import SlicingTrajectoryAnalyzer - -# --- Step 1: Define the trajectory file --- -filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj" - -# --- Step 2: Initialize the water molecule finder --- -wat_find = LammpsDumpWaterFinder( - filename, - particle_type_wall={3}, # Wall particle types - oxygen_type=1, # Oxygen atom type - hydrogen_type=2 ) # Hydrogen atom type - -# --- Step 3: Identify oxygen atom indices --- -oxygen_indices = wat_find.get_water_oxygen_ids(frame_index=0) -print("Number of water molecules:", len(oxygen_indices)) - -# --- Step 4: Initialize the parser --- -parser = LammpsDumpParser(filename) - -# --- Step 5: Create the contact angle analyzer --- -# Using the slicing method with a spherical model -analyzer = SlicingTrajectoryAnalyzer( - parser=parser, - atom_indices=oxygen_indices, - droplet_geometry='spherical', # Geometry fitting model - delta_gamma=20 # Smoothing parameter -) - -# --- Step 6: Run the analysis --- -results = analyzer.analyze([1]) # Analyze frame 1 - -# --- Step 7: Display results --- -print("Analysis results:", results) -```` - ---- - -## 4. Expected Output - -After running the example, you'll see something like: -```` -Number of water molecules: 4000 -2026-04-06 20:47:54,562 - INFO - Processing 1 frames in 1 batches with 4 workers -2026-04-06 20:47:54,907 - INFO - Detected parser type: dump -2026-04-06 20:47:55,137 - INFO - START processing frame 1 -2026-04-06 20:47:55,144 - INFO - Frame 1: Parsed 4000 liquid particles with max_dist 59 -2026-04-06 20:47:59,686 - INFO - Frame 1 - mean angle: 94.46° -2026-04-06 20:47:59,687 - INFO - Completed batch 1/1 (1 frames) -2026-04-06 20:47:59,807 - INFO - Successfully processed 1/1 frames -Analysis results: {'mean_angle': 94.4618784164532, 'std_angle': 0.0, 'angles': {1: 94.4618784164532}, 'frames_analyzed': [1], 'method_metadata': {'frames_per_angle': 1}} - -```` - -If plotting is enabled, a visualization of the droplet profile and the fitted spherical interface is generated in `result_dump_spherical_slicing/`. - ---- - -## 5. Tips - -- Use `droplet_geometry='spherical'` for droplets and `droplet_geometry='cylinder_y'` for cylindrical droplet on the y axis or `'cylinder_x'`for cylinder on the x axis. -- Adjust `delta_gamma` for smoother or sharper slicing (larger = smoother). -- To analyze multiple frames: -````python -results = analyzer.analyze(range(0, 50, 10)) -```` - -- Output files include raw interface data and optional plots (if enabled). - ---- - -## 6. Related Files - -**Example Script:** `docs/examples/contact_angle_slicing/example_slicing.py` -````python -""" -Example: Contact Angle Analysis Using the Slicing Method - -This example demonstrates how to perform a contact angle analysis -using the 'slicing' method on a spherical droplet from a LAMMPS dump trajectory. -""" - -from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder -from wetting_angle_kit.analysis import SlicingTrajectoryAnalyzer - -# --- Step 1: Define input trajectory --- -filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj" - -# --- Step 2: Identify water molecules --- -wat_find = LammpsDumpWaterFinder( - filename, - particle_type_wall={3}, # Wall atom types - oxygen_type=1, - hydrogen_type=2 -) - -oxygen_indices = wat_find.get_water_oxygen_ids(frame_index=0) -print(f"Number of water molecules: {len(oxygen_indices)}") - -# --- Step 3: Initialize parser --- -parser = LammpsDumpParser(filename) - -# --- Step 4: Create analyzer for the slicing method --- -analyzer = SlicingTrajectoryAnalyzer( - parser=parser, - atom_indices=oxygen_indices, - droplet_geometry='spherical', - delta_gamma=20 -) - -# --- Step 5: Run analysis --- -results = analyzer.analyze([1]) # Analyze frame 1 - -# --- Step 6: Display results --- -print("Analysis results:", results) -```` - ---- diff --git a/docs/tutorials/Visualization_slicing_droplet.md b/docs/tutorials/Visualization_slicing_droplet.md deleted file mode 100644 index 3ac488b..0000000 --- a/docs/tutorials/Visualization_slicing_droplet.md +++ /dev/null @@ -1,98 +0,0 @@ -# Visualization Tutorial — Droplet Surface and Contact Angle - -This tutorial demonstrates how to visualize a droplet and compute its contact angle using the **wetting_angle_kit** package. We'll use the `slicing` contact angle method and visualize the resulting droplet with the `DropletSlicePlotter` class. - ---- - -## 1. Overview - -The visualization workflow involves the following steps: - -1. Parse atomic positions from a trajectory file. -2. Identify water molecules (oxygen and hydrogen atoms). -3. Compute the droplet surface and contact angle using the *slicing method*. -4. Visualize the droplet, fitted circle, tangent, and wall. - ---- - -## 2. Import Required Modules -```python -import numpy as np -from wetting_angle_kit.parsers import ( - LammpsDumpParser, - LammpsDumpWaterFinder, - LammpsDumpWallParser, -) -from wetting_angle_kit.analysis.slicing import SlicingFrameFitter -from wetting_angle_kit.visualization import DropletSlicePlotter -``` - ---- - -## 3. Define the Input Trajectory -```python -filename = ( - "../wetting_angle_kit/tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj" -) -``` - ---- - -## 4. Identify Water Molecules -```python -wat_find = LammpsDumpWaterFinder( - filename, particle_type_wall={3}, oxygen_type=1, hydrogen_type=2 -) - -oxygen_indices = wat_find.get_water_oxygen_ids(frame_index=0) -print("Number of water molecules detected:", len(oxygen_indices)) -``` - ---- - -## 5. Parse Atomic Coordinates -```python -parser = LammpsDumpParser(filepath=filename) -oxygen_position = parser.parse(frame_index=10, indices=oxygen_indices) - -coord_wall = LammpsDumpWallParser(filename, liquid_particle_types=[1, 2]) -wall_coords = coord_wall.parse(frame_index=1) -``` - ---- - -## 6. Compute Contact Angles -```python -processor = SlicingFrameFitter( - liquid_coordinates=oxygen_position, - liquid_geom_center=np.mean(oxygen_position, axis=0), - droplet_geometry="cylinder_y", - delta_cylinder=5, - max_dist=100, -) - -list_alfas, array_surfaces, array_popt = processor.predict_contact_angle() -print("Mean contact angles (°):", list_alfas) -``` - ---- - -## 7. Visualize the Droplet -```python -plotter = DropletSlicePlotter(center=True) - -fig = plotter.plot_surface_points( - oxygen_position=oxygen_position, - surface_data=array_surfaces, - popt=array_popt[0], - wall_coords=wall_coords, - alpha=list_alfas[0], -) - -# Interactive view in a notebook: -fig.show() - -# Or save a standalone HTML page: -fig.write_html("droplet_plot.html") -print("Plot saved as 'droplet_plot.html'") -``` From c67109d82867eb6b6bbed37e5845aa51b92584ea Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 14:09:34 +0200 Subject: [PATCH 14/31] Added XYZWallParser. XYZ optimization and PBC handling. Abstracting box_size_*. Avoiding curve_fit in angle fitting. Vectorization in evaluate_on_grid. Other fine tuning. --- pyproject.toml | 4 +- .../analysis/binning/angle_fitting.py | 33 +--- .../analysis/binning/surface_definition.py | 23 ++- .../analysis/slicing/angle_fitting.py | 147 ++++++++---------- src/wetting_angle_kit/parsers/__init__.py | 2 + src/wetting_angle_kit/parsers/base.py | 28 ++-- src/wetting_angle_kit/parsers/xyz.py | 120 ++++++++++++-- .../visualization/__init__.py | 2 + .../visualization/animator.py | 13 +- .../visualization/droplet_slice_plot.py | 26 +++- tests/test_edge_cases.py | 104 +++++-------- .../test_droplet_slice_plot.py | 17 +- 12 files changed, 292 insertions(+), 227 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 169c79a..225f780 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [project] name = "wetting-angle-kit" -description = "A Python library to parse MD trajectories from LAMMPS and ASE and measure the contact through different methods" +description = "A Python library to parse MD trajectories from LAMMPS and ASE and measure the contact angle through different methods" authors = [ - { name = "Gabriel", email = "gabriel.taillandier@matgenix.com" }, + { name = "Gabriel Taillandier", email = "gabriel.taillandier@matgenix.com" }, { name = "Matgenix", email = "info@matgenix.com" }, ] license = "BSD-3-Clause" diff --git a/src/wetting_angle_kit/analysis/binning/angle_fitting.py b/src/wetting_angle_kit/analysis/binning/angle_fitting.py index b933014..776c851 100644 --- a/src/wetting_angle_kit/analysis/binning/angle_fitting.py +++ b/src/wetting_angle_kit/analysis/binning/angle_fitting.py @@ -171,33 +171,16 @@ def get_profile_coordinates( validate_droplet_geometry(self.droplet_geometry) r_chunks: list[np.ndarray] = [] z_chunks: list[np.ndarray] = [] - # If the user has declared the trajectory pre-centered, skip the - # box probe entirely and use the legacy arithmetic-mean path. - # Otherwise probe the parser once for lateral box info (skip if no - # frames were requested). If unavailable -- e.g. plain XYZ without - # a Lattice= line, or a custom parser that doesn't expose - # box_size_x/y -- fall back to the legacy centering. That is - # correct only when the trajectory already recenters the droplet - # at every frame and atoms are not wrapped across periodic - # boundaries. + # ``precentered=True`` skips the box probe and uses arithmetic-mean + # centering; otherwise box_size is queried per-frame for PBC-aware + # recentering. The parser ABC enforces box_size_x/y, so no fallback + # is needed. box_size: tuple[float, float] | None = None if frame_indices and not self.precentered: - try: - box_size = ( - self.parser.box_size_x(frame_index=frame_indices[0]), - self.parser.box_size_y(frame_index=frame_indices[0]), - ) - except (NotImplementedError, ValueError): - warnings.warn( - "Parser does not expose lateral box sizes; falling back " - "to arithmetic-mean droplet centering. This is correct " - "only if the trajectory already recenters the droplet at " - "every frame and atoms are not wrapped across periodic " - "boundaries. Provide lattice information in the " - "trajectory to enable PBC-aware recentering.", - UserWarning, - stacklevel=2, - ) + box_size = ( + self.parser.box_size_x(frame_index=frame_indices[0]), + self.parser.box_size_y(frame_index=frame_indices[0]), + ) for frame_idx in frame_indices: positions = self.parser.parse(frame_idx, self.atom_indices) if box_size is not None: diff --git a/src/wetting_angle_kit/analysis/binning/surface_definition.py b/src/wetting_angle_kit/analysis/binning/surface_definition.py index cf6c6c8..e9153be 100644 --- a/src/wetting_angle_kit/analysis/binning/surface_definition.py +++ b/src/wetting_angle_kit/analysis/binning/surface_definition.py @@ -100,11 +100,12 @@ def evaluate_on_grid(self, xi_grid: np.ndarray, zi_grid: np.ndarray) -> np.ndarr ndarray, shape (len(xi_grid), len(zi_grid)) 2D array of evaluated density values. """ - out_fitted = np.zeros((len(xi_grid), len(zi_grid))) - for i in range(len(xi_grid)): - for j in range(len(zi_grid)): - out_fitted[i, j] = self.evaluate((xi_grid[i], zi_grid[j])) - return out_fitted + # ``evaluate`` is expected to broadcast over its inputs, so the grid + # is evaluated in a single call instead of a nested Python loop. + xi_mesh, zi_mesh = np.meshgrid( + np.asarray(xi_grid), np.asarray(zi_grid), indexing="ij" + ) + return np.asarray(self.evaluate((xi_mesh, zi_mesh))) class HyperbolicTangentModel(SurfaceModel): @@ -277,10 +278,20 @@ def compute_isoline( ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Compute an iso-surface circle and wall line approximation. + Notes + ----- + ``scale_factor`` shrinks the fitted equivalent radius before tracing + the iso-line. It is a **visualization-only** parameter: the contact + angle reported by :meth:`compute_contact_angle` is derived from the + unscaled fit. The default of 0.95 makes the overlaid circle sit + slightly inside the density isosurface so the underlying contour + plot stays visible — it is not meant to encode anything physical. + Parameters ---------- scale_factor : float, default 0.95 - Factor applied to fitted radius for visualization. + Visualization-only scaling applied to the fitted equivalent + radius before computing the iso-line traces. Returns ------- diff --git a/src/wetting_angle_kit/analysis/slicing/angle_fitting.py b/src/wetting_angle_kit/analysis/slicing/angle_fitting.py index b3fafee..4196968 100644 --- a/src/wetting_angle_kit/analysis/slicing/angle_fitting.py +++ b/src/wetting_angle_kit/analysis/slicing/angle_fitting.py @@ -1,7 +1,4 @@ -from collections.abc import Sequence - import numpy as np -from scipy.optimize import curve_fit from wetting_angle_kit.analysis.slicing.surface_definition import ( SurfaceDefinition, @@ -27,7 +24,7 @@ def __init__( liquid_coordinates: np.ndarray, max_dist: float, liquid_geom_center: np.ndarray, - droplet_geometry: str = "cylinder_y", + droplet_geometry: str = "spherical", delta_gamma: float | None = None, delta_cylinder: float | None = None, surface_filter_offset: float = 2.0, @@ -45,8 +42,8 @@ def __init__( liquid_geom_center : ndarray, shape (3,) Geometric droplet center; y component overridden per slice in cylinder modes. - droplet_geometry : str, default 'cylinder_y' - One of ``{'cylinder_y', 'cylinder_x', 'spherical'}`` controlling slicing + droplet_geometry : str, default 'spherical' + One of ``{'spherical', 'cylinder_x', 'cylinder_y'}`` controlling slicing axis. delta_gamma : float, optional Angular step (degrees) for spherical droplet geometry @@ -102,42 +99,45 @@ def __init__( self.density_sigma = density_sigma self.delta_angle = delta_angle - def calculate_y_axis_list(self) -> list[float]: - """Return the per-slice center position along the slicing axis. + def _slice_sweep(self) -> tuple[list[float], list[float]]: + """Build the per-slice ``(axis_values, gammas)`` sweep once. - For cylindrical droplets the slice positions sweep across the - extent of ``liquid_coordinates`` along the slicing axis (axis 1 - after any caller-applied ``cylinder_x`` rotation) in steps of - ``delta_cylinder``. Because cylindrical droplets are designed to - span the periodic box along their cylinder axis, this is equivalent - to scanning the full box length while avoiding empty slices. - - Returns - ------- - list[float] - Y positions of slice centers; for spherical, the droplet center - y is repeated ``180 / delta_gamma`` times. + Cylindrical mode sweeps the axial extent of ``liquid_coordinates`` + in ``delta_cylinder`` steps with ``gamma = 0``. Spherical mode + repeats the droplet's y-center and rotates ``gamma`` from 0° to + 180° in ``delta_gamma`` steps. The two public list accessors + below project this single source of truth. """ if self.droplet_geometry in ("cylinder_y", "cylinder_x"): axis_values = self.liquid_coordinates[:, 1] - return list( + ys = list( np.arange( float(axis_values.min()), float(axis_values.max()), self.delta_cylinder, ) ) + return ys, [0.0] * len(ys) if self.delta_gamma is None: raise ValueError("delta_gamma is required for droplet_geometry='spherical'") - return [self.liquid_geom_center[1]] * int(180 / self.delta_gamma) + n_slices = int(180 / self.delta_gamma) + gammas = list(np.linspace(0.0, 180.0, n_slices)) + return [float(self.liquid_geom_center[1])] * n_slices, gammas + + def calculate_y_axis_list(self) -> list[float]: + """Return the per-slice center position along the slicing axis. + + Returns + ------- + list[float] + Y positions of slice centers; for spherical, the droplet center + y is repeated ``180 / delta_gamma`` times. + """ + return self._slice_sweep()[0] def calculate_gammas_list(self) -> list[float]: """Return the gamma tilt angle (degrees) for each slice.""" - if self.droplet_geometry in ("cylinder_y", "cylinder_x"): - return [0.0] * len(self.calculate_y_axis_list()) - if self.delta_gamma is None: - raise ValueError("delta_gamma is required for droplet_geometry='spherical'") - return list(np.linspace(0.0, 180.0, int(180 / self.delta_gamma))) + return self._slice_sweep()[1] def surface_definition(self, v_gamma: float) -> tuple[np.ndarray, np.ndarray]: """Sample interface lines for a given gamma. @@ -181,13 +181,16 @@ def separate_surface_data(self, surf: np.ndarray, limit_med: float) -> np.ndarra """ return surf[surf[:, 1] > limit_med] - def fit_circle( - self, - x_data: np.ndarray, - y_data: np.ndarray, - initial_guess: Sequence[float], - ) -> np.ndarray: - """Perform non-linear least squares circle fit. + @staticmethod + def fit_circle(x_data: np.ndarray, y_data: np.ndarray) -> np.ndarray: + """Algebraic (Kasa) least-squares circle fit. + + Linearises ``(x - xc)^2 + (z - zc)^2 = R^2`` into + ``2 xc·x + 2 zc·z + c = x^2 + z^2`` with ``c = R^2 - xc^2 - zc^2``, + and solves the resulting overdetermined linear system in one + ``np.linalg.lstsq`` call. Replaces the previous SciPy non-linear + fit, which was the slicing hot path's main per-slice cost and + which depended on a sensible initial guess. Parameters ---------- @@ -195,24 +198,33 @@ def fit_circle( X coordinates. y_data : ndarray Z coordinates. - initial_guess : sequence - Initial parameters [x_center, z_center, radius]. Returns ------- - ndarray - Optimized parameters [x_center, z_center, radius]. + ndarray, shape (3,) + ``[x_center, z_center, radius]``. + + Raises + ------ + np.linalg.LinAlgError + If the input points are collinear (rank-deficient system). + ValueError + If the algebraic solution yields a non-positive squared radius + (degenerate sample, e.g. all points on a line). """ - popt, _ = curve_fit( - self.circle_equation, - (x_data, y_data), - np.zeros_like(x_data), - p0=initial_guess, - ) - # The residual sqrt((x-xc)^2 + (z-zc)^2) - R is symmetric in the sign - # of R, so curve_fit may converge to a negative radius. Normalize. - popt[2] = float(abs(popt[2])) - return popt + x = np.asarray(x_data, dtype=float) + y = np.asarray(y_data, dtype=float) + a_matrix = np.column_stack((2.0 * x, 2.0 * y, np.ones_like(x))) + rhs = x * x + y * y + sol, _, _, _ = np.linalg.lstsq(a_matrix, rhs, rcond=None) + xc, zc, c = float(sol[0]), float(sol[1]), float(sol[2]) + r_sq = c + xc * xc + zc * zc + if r_sq <= 0.0: + raise ValueError( + f"Algebraic circle fit produced non-positive R^2 ({r_sq:.3g}); " + "the surface points are likely degenerate." + ) + return np.array([xc, zc, float(np.sqrt(r_sq))]) def find_intersection(self, popt: np.ndarray, y_line: float) -> float | None: """Compute contact angle from circle intersection with a baseline. @@ -237,35 +249,6 @@ def find_intersection(self, popt: np.ndarray, y_line: float) -> float | None: theta = np.arccos(delta_z / radius) return float(np.degrees(theta)) - def circle_equation( - self, - xy_data: tuple[np.ndarray, np.ndarray], - x_center: float, - z_center: float, - radius: float, - ) -> np.ndarray: - """Return the residuals of the circle equation - used in fitting. - - Parameters - ---------- - xy_data : tuple(ndarray, ndarray) - (x_data, y_data) coordinate arrays. - x_center : float - Circle center x. - z_center : float - Circle center z. - radius : float - Circle radius. - - Returns - ------- - ndarray - Residuals sqrt((x-xc)^2+(z-zc)^2) - R. - """ - x_data, y_data = xy_data - return np.sqrt((x_data - x_center) ** 2 + (y_data - z_center) ** 2) - radius - def predict_contact_angle( self, ) -> tuple[list[float], list[np.ndarray], list[np.ndarray]]: @@ -303,15 +286,9 @@ def predict_contact_angle( continue x_data = surf_line[:, 0] y_data = surf_line[:, 1] - mean_rr = float(np.mean(rr[:, 0])) if rr.size else self.max_dist / 2 - initial_guess = [ - self.liquid_geom_center[0], - self.liquid_geom_center[2], - mean_rr, - ] try: - popt = self.fit_circle(x_data, y_data, initial_guess) - except Exception: + popt = self.fit_circle(x_data, y_data) + except (np.linalg.LinAlgError, ValueError): continue angle = self.find_intersection(popt, min_drop) if angle is None: diff --git a/src/wetting_angle_kit/parsers/__init__.py b/src/wetting_angle_kit/parsers/__init__.py index 8d8b692..cb62f20 100644 --- a/src/wetting_angle_kit/parsers/__init__.py +++ b/src/wetting_angle_kit/parsers/__init__.py @@ -12,6 +12,7 @@ ) from wetting_angle_kit.parsers.xyz import ( XYZParser, + XYZWallParser, XYZWaterFinder, ) @@ -24,6 +25,7 @@ "LammpsDumpWallParser", "LammpsDumpWaterFinder", "XYZParser", + "XYZWallParser", "XYZWaterFinder", "get_water_finder", ] diff --git a/src/wetting_angle_kit/parsers/base.py b/src/wetting_angle_kit/parsers/base.py index e38d127..0caac47 100644 --- a/src/wetting_angle_kit/parsers/base.py +++ b/src/wetting_angle_kit/parsers/base.py @@ -6,10 +6,12 @@ class BaseParser(ABC): """Abstract interface for trajectory parsers consumed by analyzers. - Subclasses must implement :meth:`parse` and :meth:`frame_count`. The - geometric helpers (:meth:`box_size_x`, :meth:`box_size_y`, - :meth:`box_length_max`) raise ``NotImplementedError`` by default and - can be overridden where the underlying format exposes that information. + Subclasses must implement :meth:`parse`, :meth:`frame_count`, and the + cell-geometry helpers :meth:`box_size_x`, :meth:`box_size_y`, and + :meth:`box_length_max`. The cell helpers are abstract because the + analyzers rely on per-frame box information (PBC-aware droplet + recentering, default sampling extent); a parser without it would + silently degrade their accuracy. """ @abstractmethod @@ -42,25 +44,18 @@ def parse(self, frame_index: int, indices: np.ndarray | None = None) -> np.ndarr def frame_count(self) -> int: """Return the total number of frames in the trajectory.""" + @abstractmethod def box_size_x(self, frame_index: int) -> float: - """Return the x-dimension of the simulation box for a frame. - - Override in subclasses where the underlying format exposes it. - """ - raise NotImplementedError("box_size_x not implemented for this parser.") + """Return the length of the first lattice vector for a frame.""" + @abstractmethod def box_size_y(self, frame_index: int) -> float: - """Return the y-dimension of the simulation box for a frame. - - Override in subclasses where the underlying format exposes it. - """ - raise NotImplementedError("box_size_y not implemented for this parser.") + """Return the length of the second lattice vector for a frame.""" + @abstractmethod def box_length_max(self, frame_index: int) -> float: """Return the maximum lattice vector length for a frame. - Override in subclasses where the underlying format exposes it. - Parameters ---------- frame_index : int @@ -71,4 +66,3 @@ def box_length_max(self, frame_index: int) -> float: float Max ``|a_i|`` over lattice vectors. """ - raise NotImplementedError("box_length_max not implemented for this parser.") diff --git a/src/wetting_angle_kit/parsers/xyz.py b/src/wetting_angle_kit/parsers/xyz.py index 8ab2113..3963ea1 100644 --- a/src/wetting_angle_kit/parsers/xyz.py +++ b/src/wetting_angle_kit/parsers/xyz.py @@ -1,6 +1,7 @@ from typing import Any import numpy as np +from scipy.spatial import cKDTree from wetting_angle_kit.io_utils import assert_orthogonal_cell from wetting_angle_kit.parsers.base import BaseParser @@ -255,10 +256,11 @@ def get_water_oxygen_indices(self, frame_index: int) -> np.ndarray: data = self.frames[frame_index] positions = data["positions"] symbols = data["symbols"] + lattice_matrix = data.get("lattice_matrix") oxygen_indices = np.where(symbols == self.oxygen_type)[0] hydrogen_indices = np.where(symbols == self.hydrogen_type)[0] return self._manual_water_identification( - positions, oxygen_indices, hydrogen_indices + positions, oxygen_indices, hydrogen_indices, lattice_matrix ) def get_water_oxygen_positions(self, frame_index: int) -> np.ndarray: @@ -285,9 +287,16 @@ def _manual_water_identification( positions: np.ndarray, oxygen_indices: np.ndarray, hydrogen_indices: np.ndarray, + lattice_matrix: np.ndarray | None = None, ) -> np.ndarray: """Identify water oxygens by counting hydrogens within cutoff distance. + Uses a :class:`scipy.spatial.cKDTree` over the hydrogen positions. + When ``lattice_matrix`` is provided, the kd-tree's ``boxsize`` is + set from its diagonal (the cell is enforced orthogonal upstream) so + O–H pairs are matched under minimum-image convention; otherwise the + match is done in open space. + Parameters ---------- positions : ndarray, shape (N, 3) @@ -296,20 +305,107 @@ def _manual_water_identification( Candidate oxygen indices. hydrogen_indices : ndarray Hydrogen indices to check. + lattice_matrix : ndarray, shape (3, 3), optional + Orthogonal cell. If given, pairwise distances are evaluated + under PBC; otherwise raw Cartesian distances are used. Returns ------- ndarray Oxygen indices with exactly two nearby hydrogens. """ - water_oxygens = [] - for o_idx in oxygen_indices: - o_pos = positions[o_idx] - h_count = 0 - for h_idx in hydrogen_indices: - h_pos = positions[h_idx] - if np.linalg.norm(o_pos - h_pos) <= self.oh_cutoff: - h_count += 1 - if h_count == 2: - water_oxygens.append(o_idx) - return np.array(water_oxygens) + if oxygen_indices.size == 0 or hydrogen_indices.size == 0: + return np.array([], dtype=int) + + o_pos = positions[oxygen_indices] + h_pos = positions[hydrogen_indices] + + if lattice_matrix is not None: + # Orthogonal cell — diagonal entries are the axis-aligned box + # lengths. cKDTree requires coordinates inside ``[0, L)``, so + # wrap with a modulo before building the tree. + box = np.abs(np.diag(np.asarray(lattice_matrix, dtype=float))) + o_pos = o_pos - np.floor(o_pos / box) * box + h_pos = h_pos - np.floor(h_pos / box) * box + tree = cKDTree(h_pos, boxsize=box) + else: + tree = cKDTree(h_pos) + + neighbours = tree.query_ball_point(o_pos, r=self.oh_cutoff) + h_counts = np.fromiter( + (len(n) for n in neighbours), dtype=int, count=len(o_pos) + ) + return oxygen_indices[h_counts == 2] + + +class XYZWallParser(BaseParser): + """Parser extracting wall particle coordinates from an XYZ trajectory. + + Wall particles are everything *not* in ``liquid_particle_types``; the + mask is applied at :meth:`parse` time over the per-frame symbol array. + The ``indices`` argument of :meth:`parse` is treated as 0-based + positional indices into the wall-only positions, mirroring + :class:`~wetting_angle_kit.parsers.ase.AseWallParser`. + """ + + def __init__(self, filepath: str, liquid_particle_types: list[str]) -> None: + """ + Parameters + ---------- + filepath : str + Path to extended XYZ trajectory. + liquid_particle_types : sequence[str] + Atomic symbols representing liquid particles to exclude. + """ + self.filepath = filepath + self.liquid_particle_types = liquid_particle_types + # Reuse ``XYZParser`` for loading: it already validates orthogonal + # cells and stores symbols + positions + lattice per frame. + self.frames = XYZParser(filepath).frames + + def parse(self, frame_index: int, indices: np.ndarray | None = None) -> np.ndarray: + """Return wall atom positions for a frame. + + Parameters + ---------- + frame_index : int + Frame index. + indices : ndarray, optional + 0-based indices into the wall-only positions to further + restrict the result; if None all wall atoms are returned. + + Returns + ------- + ndarray, shape (M, 3) + Wall atom coordinates. + """ + frame = self.frames[frame_index] + mask = ~np.isin(frame["symbols"], self.liquid_particle_types) + x_par = frame["positions"][mask] + if indices is not None: + x_par = x_par[np.asarray(indices, dtype=int)] + return x_par + + def find_highest_wall_particle(self, frame_index: int) -> float: + """Return the maximum z-coordinate among wall particles for a frame.""" + x_wall = self.parse(frame_index) + return float(np.max(x_wall[:, 2])) + + def box_size_x(self, frame_index: int) -> float: + """Return the length of the first lattice vector for a frame.""" + lattice_matrix = self.frames[frame_index]["lattice_matrix"] + return float(np.linalg.norm(lattice_matrix[0])) + + def box_size_y(self, frame_index: int) -> float: + """Return the length of the second lattice vector for a frame.""" + lattice_matrix = self.frames[frame_index]["lattice_matrix"] + return float(np.linalg.norm(lattice_matrix[1])) + + def box_length_max(self, frame_index: int) -> float: + """Return the maximum lattice vector length for a frame.""" + lattice_matrix = self.frames[frame_index]["lattice_matrix"] + return float(np.max(np.linalg.norm(lattice_matrix, axis=1))) + + def frame_count(self) -> int: + """Return the total number of frames in the trajectory.""" + return len(self.frames) diff --git a/src/wetting_angle_kit/visualization/__init__.py b/src/wetting_angle_kit/visualization/__init__.py index bf10e4d..6134932 100644 --- a/src/wetting_angle_kit/visualization/__init__.py +++ b/src/wetting_angle_kit/visualization/__init__.py @@ -1,3 +1,4 @@ +from wetting_angle_kit.visualization.animator import LammpsContactAngleAnimator from wetting_angle_kit.visualization.base_trajectory_plotter import ( BaseTrajectoryPlotter, ) @@ -14,6 +15,7 @@ "BaseTrajectoryPlotter", "BinningTrajectoryPlotter", "DropletSlicePlotter", + "LammpsContactAngleAnimator", "SlicingTrajectoryPlotter", "TrajectoryStats", ] diff --git a/src/wetting_angle_kit/visualization/animator.py b/src/wetting_angle_kit/visualization/animator.py index c8f9c09..fd36e05 100644 --- a/src/wetting_angle_kit/visualization/animator.py +++ b/src/wetting_angle_kit/visualization/animator.py @@ -11,8 +11,17 @@ from wetting_angle_kit.visualization.droplet_slice_plot import DropletSlicePlotter -class ContactAngleAnimator: - """Generate interactive Plotly slider animation of median slice angle per frame.""" +class LammpsContactAngleAnimator: + """Plotly slider animation of the median per-frame slice angle. + + This class is **LAMMPS-specific**: it instantiates + :class:`~wetting_angle_kit.parsers.LammpsDumpParser`, + :class:`~wetting_angle_kit.parsers.LammpsDumpWallParser` and + :class:`~wetting_angle_kit.parsers.LammpsDumpWaterFinder` directly. A + parser-agnostic version would have to dispatch all three from a + factory; the rename makes the current coupling explicit rather than + promising generality that the implementation does not deliver. + """ def __init__( self, diff --git a/src/wetting_angle_kit/visualization/droplet_slice_plot.py b/src/wetting_angle_kit/visualization/droplet_slice_plot.py index 8cec03f..8e69204 100644 --- a/src/wetting_angle_kit/visualization/droplet_slice_plot.py +++ b/src/wetting_angle_kit/visualization/droplet_slice_plot.py @@ -75,11 +75,16 @@ def plot_surface_points( else: mask = np.abs(oxygen_position[:, 1] - y_com) <= 3 oxygen_selected = oxygen_position[mask] - # Recenter if needed + # Recenter if needed. ``oxygen_selected`` is already a fresh copy + # from the boolean mask; ``wall_coords`` is the caller's array and + # must be copied before in-place shifting so the plotter remains + # side-effect-free. if self.center: z_shift = np.mean(wall_coords[:, 2]) - oxygen_selected[:, 2] -= z_shift + wall_coords = wall_coords.copy() wall_coords[:, 2] -= z_shift + oxygen_selected = oxygen_selected.copy() + oxygen_selected[:, 2] -= z_shift surface_data = [ np.column_stack([surf[:, 0], surf[:, 1] - z_shift]) for surf in surface_data @@ -157,12 +162,19 @@ def plot_surface_points( if discriminant > 0: x_contact = x_center + np.sqrt(discriminant) # Right side z_contact = z_line - m_tangent = -(x_contact - x_center) / (z_contact - z_center) - # Tangent line z_top = z_center + radius * 1.1 - x_top = x_contact + (z_top - z_contact) / m_tangent - x_line = np.linspace(x_contact, x_top, 100) - z_line_tan = m_tangent * (x_line - x_contact) + z_contact + # When the contact point sits at the circle's equator + # (``z_contact == z_center``) the tangent is vertical and + # the closed-form slope diverges; draw a vertical segment + # of the same z-extent instead so the overlay still renders. + if np.isclose(z_contact, z_center): + x_line = np.full(100, x_contact) + z_line_tan = np.linspace(z_contact, z_top, 100) + else: + m_tangent = -(x_contact - x_center) / (z_contact - z_center) + x_top = x_contact + (z_top - z_contact) / m_tangent + x_line = np.linspace(x_contact, x_top, 100) + z_line_tan = m_tangent * (x_line - x_contact) + z_contact fig.add_trace( go.Scatter( x=x_line, diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 65395f8..35e2b7b 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -138,11 +138,30 @@ def _make_binning_analyzer(parser): ) +class _BoxedStubParser: + """Helper that supplies the abstract box-size methods of ``BaseParser``. + + Subclasses only need to set ``frames`` (a list of ``(N, 3)`` arrays) and + use the defaults below for a 100x100x100 orthogonal cell. + """ + + box: tuple[float, float, float] = (100.0, 100.0, 100.0) + + def box_size_x(self, frame_index): + return self.box[0] + + def box_size_y(self, frame_index): + return self.box[1] + + def box_length_max(self, frame_index): + return max(self.box) + + def test_binning_get_profile_coordinates_empty_frame_list(): """Empty frame_indices must return empty arrays and zero frames.""" from wetting_angle_kit.parsers.base import BaseParser - class _StubParser(BaseParser): + class _StubParser(_BoxedStubParser, BaseParser): def parse(self, frame_index, indices=None): return np.zeros((0, 3)) @@ -163,7 +182,10 @@ def test_binning_get_profile_coordinates_concatenates_frames(): frame0 = np.array([[1.0, 0.0, 5.0], [-1.0, 0.0, 6.0], [0.0, 0.0, 7.0]]) frame1 = np.array([[2.0, 0.0, 8.0], [-2.0, 0.0, 9.0], [0.0, 0.0, 10.0]]) - class _StubParser(BaseParser): + class _StubParser(_BoxedStubParser, BaseParser): + # A large box so the per-frame circular mean coincides with the + # arithmetic mean and the asserted radii do not depend on PBC + # wrapping. def parse(self, frame_index, indices=None): return [frame0, frame1][frame_index] @@ -180,51 +202,35 @@ def frame_count(self): np.testing.assert_array_equal(z, np.array([5.0, 6.0, 7.0, 8.0, 9.0, 10.0])) -def test_binning_warns_and_falls_back_when_parser_has_no_box(): - """Parsers that don't expose box_size_x/y (plain XYZ without a Lattice= - line, custom stubs) must trigger the fallback warning and still produce - results via the legacy arithmetic-mean centering.""" +def test_binning_precentered_skips_box_probe(): + """``precentered=True`` must bypass the box probe entirely so the + box-size accessors are never invoked, even by a parser that would raise + if asked for box info.""" + from wetting_angle_kit.analysis.binning import BinningBatchFitter from wetting_angle_kit.parsers.base import BaseParser frame = np.array([[1.0, 0.0, 5.0], [-1.0, 0.0, 6.0], [0.0, 0.0, 7.0]]) - class _StubParser(BaseParser): + class _NoBoxParser(BaseParser): def parse(self, frame_index, indices=None): return frame def frame_count(self): return 1 - # box_size_x / box_size_y inherited from BaseParser raise NotImplementedError. - - analyzer = _make_binning_analyzer(_StubParser()) - with pytest.warns(UserWarning, match="does not expose lateral box sizes"): - r, z, n = analyzer.get_profile_coordinates(frame_indices=[0]) - assert n == 1 - np.testing.assert_allclose(r, np.array([1.0, 1.0, 0.0])) - np.testing.assert_array_equal(z, np.array([5.0, 6.0, 7.0])) - - -def test_binning_precentered_skips_box_probe_and_warning(): - """precentered=True must bypass the box probe entirely so a parser - that lacks box_size_x/y is accepted silently, no warning is issued, - and the result matches the legacy arithmetic-mean path.""" - import warnings - - from wetting_angle_kit.analysis.binning import BinningBatchFitter - from wetting_angle_kit.parsers.base import BaseParser - - frame = np.array([[1.0, 0.0, 5.0], [-1.0, 0.0, 6.0], [0.0, 0.0, 7.0]]) + def box_size_x(self, frame_index): + raise AssertionError("box_size_x must not be called when precentered=True") - class _StubParser(BaseParser): - def parse(self, frame_index, indices=None): - return frame + def box_size_y(self, frame_index): + raise AssertionError("box_size_y must not be called when precentered=True") - def frame_count(self): - return 1 + def box_length_max(self, frame_index): + raise AssertionError( + "box_length_max must not be called when precentered=True" + ) analyzer = BinningBatchFitter( - parser=_StubParser(), + parser=_NoBoxParser(), atom_indices=None, droplet_geometry="spherical", binning_params={ @@ -237,36 +243,6 @@ def frame_count(self): }, precentered=True, ) - with warnings.catch_warnings(): - warnings.simplefilter("error", UserWarning) - r, z, n = analyzer.get_profile_coordinates(frame_indices=[0]) + r, z, n = analyzer.get_profile_coordinates(frame_indices=[0]) assert n == 1 np.testing.assert_allclose(r, np.array([1.0, 1.0, 0.0])) - - -def test_binning_no_warning_when_parser_exposes_box(): - """The fallback warning must NOT fire when the parser exposes box info; - otherwise it would spam every real run.""" - import warnings - - from wetting_angle_kit.parsers.base import BaseParser - - frame = np.array([[1.0, 0.0, 5.0], [-1.0, 0.0, 6.0], [0.0, 0.0, 7.0]]) - - class _StubParserWithBox(BaseParser): - def parse(self, frame_index, indices=None): - return frame - - def frame_count(self): - return 1 - - def box_size_x(self, frame_index): - return 100.0 - - def box_size_y(self, frame_index): - return 100.0 - - analyzer = _make_binning_analyzer(_StubParserWithBox()) - with warnings.catch_warnings(): - warnings.simplefilter("error", UserWarning) - analyzer.get_profile_coordinates(frame_indices=[0]) diff --git a/tests/test_visualization/test_droplet_slice_plot.py b/tests/test_visualization/test_droplet_slice_plot.py index 3fc334c..db51d3f 100644 --- a/tests/test_visualization/test_droplet_slice_plot.py +++ b/tests/test_visualization/test_droplet_slice_plot.py @@ -5,8 +5,10 @@ import pytest from tests.conftest import trajectory_path -from wetting_angle_kit.visualization import DropletSlicePlotter -from wetting_angle_kit.visualization.animator import ContactAngleAnimator +from wetting_angle_kit.visualization import ( + DropletSlicePlotter, + LammpsContactAngleAnimator, +) def _synthetic_droplet(seed=0): @@ -96,13 +98,14 @@ def test_droplet_slice_plotter_layers_can_be_disabled(): assert len(fig.data) == 0 -# --- ContactAngleAnimator (not re-exported; import from submodule) --- +# --- LammpsContactAngleAnimator --- def test_contact_angle_animator_init_loads_fixture(): - """ContactAngleAnimator.__init__ wires up parsers and finders for a real fixture.""" + """LammpsContactAngleAnimator.__init__ wires up parsers + and finders for a real fixture.""" pytest.importorskip("ovito") - animator = ContactAngleAnimator( + animator = LammpsContactAngleAnimator( filename=trajectory_path("traj_spherical_drop_4k.lammpstrj"), oxygen_type=1, hydrogen_type=2, @@ -120,10 +123,10 @@ def test_contact_angle_animator_init_loads_fixture(): @pytest.mark.slow def test_contact_angle_animator_generates_html(tmp_path): - """Smoke-test ContactAngleAnimator on the cylindrical LAMMPS fixture.""" + """Smoke-test LammpsContactAngleAnimator on the cylindrical LAMMPS fixture.""" pytest.importorskip("ovito") output = tmp_path / "animation.html" - animator = ContactAngleAnimator( + animator = LammpsContactAngleAnimator( filename=trajectory_path("traj_10_3_330w_nve_4k_reajust.lammpstrj"), oxygen_type=1, hydrogen_type=2, From b3c1d7210ddf0fc794e6bf3661a7ce2b3671a37b Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 14:32:17 +0200 Subject: [PATCH 15/31] Bump mypy and mypy fixes. --- .pre-commit-config.yaml | 2 +- pyproject.toml | 29 +++++++++++++------- src/wetting_angle_kit/parsers/ase.py | 2 +- src/wetting_angle_kit/parsers/lammps_dump.py | 6 +++- src/wetting_angle_kit/parsers/xyz.py | 2 +- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5f84c3b..95b38e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.18.2 hooks: - id: mypy files: ^src/wetting_angle_kit/ diff --git a/pyproject.toml b/pyproject.toml index 225f780..e8039ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ all = ["ase>=3.23.0", "ovito~=3.11.3", "ipython>=8.0.0"] [project.urls] "Homepage" = "https://github.com/Matgenix/wetting-angle-kit" +"Source" = "https://github.com/Matgenix/wetting-angle-kit" "Bug Tracker" = "https://github.com/Matgenix/wetting-angle-kit/issues" "Documentation" = "https://matgenix.github.io/wetting-angle-kit" @@ -90,27 +91,35 @@ multi_line_output = 3 [tool.mypy] python_version = "3.10" +strict = true # NumPy reductions (np.mean, etc.) and ASE attributes (frame.positions) are # typed as Any in their stubs, so functions returning them legitimately trip -# no-any-return. Re-enable if/when those returns are wrapped in np.asarray() -# or cast(np.ndarray, ...). +# no-any-return under ``strict``. Re-enable if/when those returns are wrapped +# in ``np.asarray`` or ``cast(np.ndarray, ...)``. warn_return_any = false -warn_unused_configs = true -disallow_untyped_defs = true -disallow_incomplete_defs = true +# NumPy's ndarray accepts shape and dtype generic parameters that almost no +# call-site is prepared to specify (``np.ndarray[Any, np.dtype[Any]]`` is the +# closest equivalent of a bare ``np.ndarray`` annotation). Disabling +# disallow_any_generics keeps the rest of ``strict`` active while letting +# arrays continue to be annotated as ``np.ndarray``. Reach for +# ``numpy.typing.NDArray`` (or ``cast``) at the boundaries where a more +# precise type would genuinely help readers. +disallow_any_generics = false -# scipy, mpl_toolkits and plotly have no stubs installed. Disable only +# scipy, mpl_toolkits and plotly have no stubs installed. Disable only # import-untyped (module exists but lacks py.typed / stubs); import-not-found # (module doesn't exist at all — catches typos and removed dependencies) stays # active. disable_error_code = ["import-untyped"] [[tool.mypy.overrides]] -# ASE's ase.io.read() is typed as Union[Atom, Atoms]; all call-sites in -# parsers/ase.py receive Atoms in practice, so suppress only the union-attr -# cascade rather than silencing the entire file. +# ASE's ase.io.read(index=":") is typed as Union[Atoms, list[Atoms]] even +# though it always returns a list when ``index=":"``; suppress only the +# resulting union-attr / arg-type cascade in parsers/ase.py rather than +# silencing the whole module. ``no-untyped-call`` covers ase.neighborlist's +# NeighborList API, which has no inline type information in current stubs. module = ["ase.*", "wetting_angle_kit.parsers.ase"] -disable_error_code = ["union-attr"] +disable_error_code = ["union-attr", "arg-type", "no-untyped-call"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/src/wetting_angle_kit/parsers/ase.py b/src/wetting_angle_kit/parsers/ase.py index f9d5b6c..c5a5c1d 100644 --- a/src/wetting_angle_kit/parsers/ase.py +++ b/src/wetting_angle_kit/parsers/ase.py @@ -166,7 +166,7 @@ def get_water_oxygen_indices(self, frame_index: int) -> np.ndarray: # ASE's NeighborList uses pairwise cutoff = cutoffs[i] + cutoffs[j]. # Use half the bond cutoff per atom so the effective pair cutoff # equals self.oh_cutoff. - cutoffs = [self.oh_cutoff / 2.0] * len(frame) # type: ignore[arg-type] + cutoffs = [self.oh_cutoff / 2.0] * len(frame) nl = self._NeighborList(cutoffs, self_interaction=False, bothways=True) nl.update(frame) water_oxygens = [] diff --git a/src/wetting_angle_kit/parsers/lammps_dump.py b/src/wetting_angle_kit/parsers/lammps_dump.py index c9e8b23..f803bcd 100644 --- a/src/wetting_angle_kit/parsers/lammps_dump.py +++ b/src/wetting_angle_kit/parsers/lammps_dump.py @@ -161,7 +161,7 @@ def load_dump_ovito(self) -> Any: pipeline = import_file(self.filepath) pipeline.modifiers.append( SelectTypeModifier( - property="Particle Type", types=self.liquid_particle_types + property="Particle Type", types=set(self.liquid_particle_types) ) ) pipeline.modifiers.append(DeleteSelectedModifier()) @@ -297,6 +297,10 @@ def _setup_pipeline(self) -> Any: """ try: from ovito.io import import_file + + # OVITO's type stubs omit ``CoordinationAnalysisModifier`` even + # though it exists at runtime; silence the spurious attr-defined + # error rather than blanket-ignoring the whole import block. from ovito.modifiers import ( ComputePropertyModifier, CoordinationAnalysisModifier, diff --git a/src/wetting_angle_kit/parsers/xyz.py b/src/wetting_angle_kit/parsers/xyz.py index 3963ea1..7ae944d 100644 --- a/src/wetting_angle_kit/parsers/xyz.py +++ b/src/wetting_angle_kit/parsers/xyz.py @@ -167,7 +167,7 @@ def __init__( def load_xyz_file(self) -> list[dict[str, Any]]: """Load frames including the lattice matrix for box-size queries.""" - frames: list[dict[str, np.ndarray]] = [] + frames: list[dict[str, np.ndarray | None]] = [] with open(self.filepath) as file: lines = file.readlines() frame_start = 0 From 8e37ec6641ffcb239a8773a52eadd0b2df1cd5e4 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 14:34:15 +0200 Subject: [PATCH 16/31] Add init to tests folder. --- pyproject.toml | 1 - tests/__init__.py | 0 2 files changed, 1 deletion(-) create mode 100644 tests/__init__.py diff --git a/pyproject.toml b/pyproject.toml index e8039ee..90a777f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,6 @@ disable_error_code = ["union-attr", "arg-type", "no-untyped-call"] [tool.pytest.ini_options] testpaths = ["tests"] -pythonpath = ["."] python_files = "test_*.py" python_classes = "Test*" python_functions = "test_*" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From a4bbeae0e59d5a8302568a7f664f9be72fe54240 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 14:48:26 +0200 Subject: [PATCH 17/31] github actions updates. --- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/releases.yml | 53 +++++-------------- .github/workflows/testing.yml | 20 +++---- README.md | 1 + tests/test_analysis/test_binning_method.py | 12 ++++- .../test_analysis/test_slicing_edge_cases.py | 1 + tests/test_analysis/test_slicing_method.py | 12 ++++- tests/test_parser/test_parser_dump.py | 6 ++- 8 files changed, 52 insertions(+), 55 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 0e812dd..f2ebf6f 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' cache: pip diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 411dbc5..3228342 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 0 - name: Set up Python 3.11 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' @@ -47,6 +47,15 @@ jobs: release_branch: ${{ env.PUBLISH_UPDATE_BRANCH }} exclude_labels: "duplicate,question,invalid,wontfix,dependency_updates,skip_changelog" + # CharMixer/auto-changelog-action above rewrites the changelog on the + # working tree; this step force-pushes the result back to the protected + # ``main`` branch. ``force: true`` is required because the changelog + # commit is fabricated by the action and would otherwise diverge from + # ``origin/main``; ``unprotect_reviews: true`` lifts branch-protection + # review requirements for the duration of the push so the workflow can + # publish the release autonomously. The push triggers + # ``deploy-docs.yml`` (push-to-main), which rebuilds and redeploys the + # documentation alongside this publish. - name: Update '${{ env.PUBLISH_UPDATE_BRANCH }}' uses: CasperWA/push-protected@v2 with: @@ -57,30 +66,6 @@ jobs: force: true tags: true - - name: Install docs dependencies - run: | - # Required to generate rst files from markdown - sudo apt install pandoc - pip install .[doc] - - - name: Build Sphinx docs - working-directory: docs - run: | - # cannot use sphinx build directly as the makefile handles generation - # of some rst files - make html - - - name: Fix permissions # following https://github.com/actions/upload-pages-artifact?tab=readme-ov-file#file-permissions - run: | - chmod -c -R +rX "./docs/build" | while read line; do - echo "::warning title=Invalid file permissions automatically fixed::$line" - done - - - name: Upload docs artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./docs/build/html - - name: Get tagged versions run: echo "PREVIOUS_VERSION=$(git tag -l --sort -version:refname | sed -n 2p)" >> $GITHUB_ENV @@ -117,17 +102,7 @@ jobs: user: __token__ password: ${{ secrets.PYPI_PASSWORD }} - deploy_docs: - if: github.repository == 'Matgenix/wetting-angle-kit' && startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - permissions: - pages: write # to deploy to Pages - id-token: write # to verify the deployment originates from an appropriate source - needs: publish - environment: - name: "Documentation" - url: https://Matgenix.github.io/wetting-angle-kit - - steps: - - name: Deploy docs - uses: actions/deploy-pages@v4 + # Documentation deployment is handled by ``deploy-docs.yml`` on push to + # ``main`` (which the ``Update main`` step above triggers). Keeping the + # build+deploy in a single workflow avoids the race that arises when + # both workflows publish to GitHub Pages simultaneously. diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d648511..4a9b0cf 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v5 with: python-version: '3.10' cache: pip @@ -36,7 +36,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip @@ -50,7 +50,7 @@ jobs: pip install .[dev,all] - name: Test - run: pytest --cov=wetting_angle_kit --cov-report=xml --cov-fail-under=70 + run: pytest --cov=wetting_angle_kit --cov-report=xml --cov-fail-under=80 - name: Upload coverage to Codecov if: matrix.python-version == '3.11' @@ -70,7 +70,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v5 with: python-version: '3.11' cache: pip @@ -81,11 +81,11 @@ jobs: python -m pip install --upgrade pip pip install .[dev,ase] - - name: Test (skip OVITO-dependent tests) - run: | - pytest \ - --ignore=tests/test_parser/test_parser_dump.py \ - --ignore=tests/test_analysis + - name: Test + # OVITO-dependent test modules call ``pytest.importorskip("ovito")`` + # at import time, so the full suite can run on macOS: those modules + # are skipped automatically and the ASE-backed tests still execute. + run: pytest docs: runs-on: ubuntu-latest @@ -93,7 +93,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v5 with: python-version: '3.11' cache: pip diff --git a/README.md b/README.md index 3add82f..cfcfac5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # wetting-angle-kit [![tests](https://img.shields.io/github/actions/workflow/status/Matgenix/wetting-angle-kit/testing.yml?branch=main&label=tests)](https://github.com/Matgenix/wetting-angle-kit/actions/workflows/testing.yml) +[![docs](https://img.shields.io/github/actions/workflow/status/Matgenix/wetting-angle-kit/deploy-docs.yml?branch=main&label=docs)](https://github.com/Matgenix/wetting-angle-kit/actions/workflows/deploy-docs.yml) [![code coverage](https://codecov.io/gh/Matgenix/wetting-angle-kit/branch/main/graph/badge.svg)](https://codecov.io/gh/Matgenix/wetting-angle-kit) [![pypi version](https://img.shields.io/pypi/v/wetting-angle-kit?color=blue)](https://pypi.org/project/wetting-angle-kit/) [![Python versions](https://img.shields.io/pypi/pyversions/wetting-angle-kit)](https://pypi.org/project/wetting-angle-kit/) diff --git a/tests/test_analysis/test_binning_method.py b/tests/test_analysis/test_binning_method.py index fe78bb1..5d98050 100644 --- a/tests/test_analysis/test_binning_method.py +++ b/tests/test_analysis/test_binning_method.py @@ -3,8 +3,16 @@ import numpy as np import pytest -from wetting_angle_kit.analysis import BinningTrajectoryAnalyzer -from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder +# The binning integration tests run on a LAMMPS dump fixture parsed through +# OVITO; skip the whole module when the optional dependency is unavailable +# (typically on macOS CI). +pytest.importorskip("ovito") + +from wetting_angle_kit.analysis import BinningTrajectoryAnalyzer # noqa: E402 +from wetting_angle_kit.parsers import ( # noqa: E402 + LammpsDumpParser, + LammpsDumpWaterFinder, +) # --- Fixtures --- diff --git a/tests/test_analysis/test_slicing_edge_cases.py b/tests/test_analysis/test_slicing_edge_cases.py index df51f01..7df2f27 100644 --- a/tests/test_analysis/test_slicing_edge_cases.py +++ b/tests/test_analysis/test_slicing_edge_cases.py @@ -87,6 +87,7 @@ def test_run_one_frame_invokes_pipeline_on_real_lammps(): ``_run_one_frame`` to exercise the parser → ``predict_contact_angle`` path that subprocess execution otherwise hides from coverage. """ + pytest.importorskip("ovito") from tests.conftest import trajectory_path SlicingTrajectoryAnalyzer._init_worker( diff --git a/tests/test_analysis/test_slicing_method.py b/tests/test_analysis/test_slicing_method.py index 708f0ef..cb9a2cb 100644 --- a/tests/test_analysis/test_slicing_method.py +++ b/tests/test_analysis/test_slicing_method.py @@ -3,8 +3,16 @@ import numpy as np import pytest -from wetting_angle_kit.analysis import SlicingTrajectoryAnalyzer -from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder +# The slicing integration tests run on a LAMMPS dump fixture parsed through +# OVITO; skip the whole module when the optional dependency is unavailable +# (typically on macOS CI). +pytest.importorskip("ovito") + +from wetting_angle_kit.analysis import SlicingTrajectoryAnalyzer # noqa: E402 +from wetting_angle_kit.parsers import ( # noqa: E402 + LammpsDumpParser, + LammpsDumpWaterFinder, +) # --- Fixtures --- diff --git a/tests/test_parser/test_parser_dump.py b/tests/test_parser/test_parser_dump.py index a28b626..3be2a8e 100644 --- a/tests/test_parser/test_parser_dump.py +++ b/tests/test_parser/test_parser_dump.py @@ -4,7 +4,11 @@ import numpy as np import pytest -from wetting_angle_kit.parsers.lammps_dump import LammpsDumpParser +# LAMMPS dump parsing goes through OVITO; skip the whole module when the +# optional dependency is unavailable (typically on macOS CI). +pytest.importorskip("ovito") + +from wetting_angle_kit.parsers.lammps_dump import LammpsDumpParser # noqa: E402 # Path to the test trajectory file (LAMMPS dump format) TRAJECTORY_PATH = os.path.join( From c7ab9ec59276368e5d4ff1fac4b15151f7911aa6 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 15:02:22 +0200 Subject: [PATCH 18/31] Fixing doc warnings. --- .github/workflows/deploy-docs.yml | 5 ++++- .github/workflows/testing.yml | 8 ++++---- docs/Makefile | 6 +++++- docs/source/API/index.rst | 5 ++--- docs/source/conf.py | 13 ++++++++----- docs/source/introduction/Introduction.rst | 2 +- .../source/introduction/Theoretical_foundations.rst | 4 ++-- 7 files changed, 26 insertions(+), 17 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index f2ebf6f..be7861e 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -36,7 +36,10 @@ jobs: - name: Build Sphinx docs working-directory: docs - run: make html + # ``-W`` promotes warnings to errors so doc regressions break CI + # rather than slipping through silently; ``--keep-going`` reports + # every offender in one run instead of stopping at the first. + run: python -m sphinx -W --keep-going -b html source build/html - name: Fix permissions run: | diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 4a9b0cf..d683e80 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -108,7 +108,7 @@ jobs: - name: Build Sphinx docs working-directory: docs - run: | - # cannot use sphinx build directly as the makefile handles generation - # of some rst files - make html + # ``-W`` promotes warnings to errors so doc regressions break CI + # rather than slipping through silently; ``--keep-going`` reports + # every offender in one run instead of stopping at the first. + run: python -m sphinx -W --keep-going -b html source build/html diff --git a/docs/Makefile b/docs/Makefile index d0c3cbf..20f0d45 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,11 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= +# ``-W`` turns Sphinx warnings into errors; ``--keep-going`` finishes the +# build so the user sees every offender rather than stopping at the first. +# Override on the command line with ``SPHINXOPTS= make html`` for a lenient +# local build. +SPHINXOPTS ?= -W --keep-going SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build diff --git a/docs/source/API/index.rst b/docs/source/API/index.rst index 8542b65..b6188ad 100644 --- a/docs/source/API/index.rst +++ b/docs/source/API/index.rst @@ -22,7 +22,7 @@ Base Analyzer :show-inheritance: Slicing Method -^^^^^^^^^^^^^ +^^^^^^^^^^^^^^ .. automodule:: wetting_angle_kit.analysis.slicing :members: @@ -31,7 +31,7 @@ Slicing Method :exclude-members: SlicingFrameFitter Binning Method -^^^^^^^^^^^^^ +^^^^^^^^^^^^^^ .. automodule:: wetting_angle_kit.analysis.binning :members: @@ -44,5 +44,4 @@ Visualization and Statistics .. automodule:: wetting_angle_kit.visualization :members: - :undoc-members: :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index c026a02..103f105 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,13 +9,18 @@ sys.path.insert(0, os.path.abspath("../../src")) +from wetting_angle_kit._version import __version__ # noqa: E402 + # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "wetting-angle-kit" -copyright = "2025, Gabriel" -author = "Gabriel" -release = "0.1.2" +copyright = "2025, Matgenix (Gabriel Taillandier)" +author = "Gabriel Taillandier" +# Pull the release from the package's auto-generated version file so the +# docs always advertise the same version as the wheel. +release = __version__ +version = __version__.split("+", 1)[0] # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -39,7 +44,6 @@ # Autosummary settings autosummary_generate = True -templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # Exclude input prompts from copybutton @@ -49,7 +53,6 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "sphinx_rtd_theme" -html_static_path = ["_static"] # Path to GitHub repo {group}/{project} issues_github_path = "Matgenix/wetting-angle-kit" diff --git a/docs/source/introduction/Introduction.rst b/docs/source/introduction/Introduction.rst index b381128..a43d474 100644 --- a/docs/source/introduction/Introduction.rst +++ b/docs/source/introduction/Introduction.rst @@ -66,7 +66,7 @@ Both methods are capable of analyzing: * **Cylindrical Droplets**: Cylindrical droplets (e.g., water on a nanowire or with periodic boundary conditions), analyzed along the cylinder's axis (x or y). **Slicing Method** -^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^ The **Slicing Method** is ideal for analyzing the evolution of the contact angle over time or for symmetric droplets. diff --git a/docs/source/introduction/Theoretical_foundations.rst b/docs/source/introduction/Theoretical_foundations.rst index 096935b..bc1ac0f 100644 --- a/docs/source/introduction/Theoretical_foundations.rst +++ b/docs/source/introduction/Theoretical_foundations.rst @@ -1,5 +1,5 @@ Theoretical foundations -====================== +======================= The contact angle is defined as the angle between the tangent to the liquid-vapor interface and the normal to the substrate. It is a measure of the wetting properties of a droplet on a surface. @@ -9,7 +9,7 @@ The contact angle is defined as the angle between the tangent to the liquid-vapo The slicing method ----------------- +------------------ .. image:: ../../images/wetting_angle_kit_3d_droplet.jpg :align: center From b80cfc188e4e7f7156da4603cfc7215ed3e8b64c Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 15:21:07 +0200 Subject: [PATCH 19/31] Typos in doc files. --- docs/source/tutorials/Binning_method_tuto.rst | 4 ++-- docs/source/tutorials/Parser_tutorial.rst | 4 ++-- docs/source/tutorials/Slicing_method_tuto.rst | 7 ++++++- .../source/tutorials/Visualization_slicing_droplet.rst | 10 +++++++--- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/source/tutorials/Binning_method_tuto.rst b/docs/source/tutorials/Binning_method_tuto.rst index 45758ed..6d70f64 100644 --- a/docs/source/tutorials/Binning_method_tuto.rst +++ b/docs/source/tutorials/Binning_method_tuto.rst @@ -21,10 +21,10 @@ The **binning method** works by: 2. Prerequisites ---------------- -Your trajectory file (e.g., a LAMMPS dump file) contain: +Your trajectory file (e.g., a LAMMPS dump file) should contain: - Atom IDs, types, and positions -- Liquid particles (in this cas Water molecules: O and H atoms) +- Liquid particles (in this case, water molecules: O and H atoms) Example trajectory:: diff --git a/docs/source/tutorials/Parser_tutorial.rst b/docs/source/tutorials/Parser_tutorial.rst index 91860ff..ded5e5d 100644 --- a/docs/source/tutorials/Parser_tutorial.rst +++ b/docs/source/tutorials/Parser_tutorial.rst @@ -5,7 +5,7 @@ This tutorial shows how to load different trajectory formats using the ``wetting The parser provides a unified interface to read atomic coordinates from: -- LAMMPS dump files (``LammpsDumpParser``, `` LammpsDumpWaterFinder``) +- LAMMPS dump files (``LammpsDumpParser``, ``LammpsDumpWaterFinder``) - ASE ``.traj`` files (``AseParser``, ``AseWaterFinder``) - XYZ files (``XYZParser``) @@ -74,7 +74,7 @@ The ``.parse()`` method always returns a NumPy array of shape ``(N, 3)`` contain wat_find = AseWaterFinder( filename, particle_type_wall=["C"], # Wall elements (e.g., carbon) - oh_cutoff=0.4, # O–H bond cutoff distance + oh_cutoff=1.2, # O–H bond cutoff distance (Å) ) # --- Step 3: Identify water oxygens for frame 0 --- diff --git a/docs/source/tutorials/Slicing_method_tuto.rst b/docs/source/tutorials/Slicing_method_tuto.rst index b00209d..652697a 100644 --- a/docs/source/tutorials/Slicing_method_tuto.rst +++ b/docs/source/tutorials/Slicing_method_tuto.rst @@ -9,7 +9,7 @@ This tutorial explains how to compute the contact angle of a droplet using the * ----------- The **slicing method** divides the droplet into slices (along the z-axis) and fits a geometric model (e.g. spherical) to the liquid–solid interface profile. -This is ideal for study the evolution of the angles among a trajectory. +This is ideal for studying the evolution of the angle along a trajectory. ---- @@ -84,6 +84,11 @@ After running the example, you'll see something like:: Std contact angle (°): 0.0 Frames analyzed: [1] +The standard deviation is reported as ``0.0`` because the example only +analyzes a single frame. ``std_angle`` is computed across frames — pass a +multi-frame ``frame_range`` (e.g. ``range(0, 50)``) to see a non-zero +spread. + ``analyze`` returns a :class:`SlicingResults` dataclass with the following convenience attributes: diff --git a/docs/source/tutorials/Visualization_slicing_droplet.rst b/docs/source/tutorials/Visualization_slicing_droplet.rst index 8b0fdf3..192ec8e 100644 --- a/docs/source/tutorials/Visualization_slicing_droplet.rst +++ b/docs/source/tutorials/Visualization_slicing_droplet.rst @@ -98,12 +98,16 @@ The visualization workflow involves the following steps: plotter = DropletSlicePlotter(center=True) + # ``predict_contact_angle`` returns three parallel lists (one entry per + # slice that produced a usable angle); pick a single index across all + # three so the overlay refers to one and the same slice. + slice_idx = 0 fig = plotter.plot_surface_points( oxygen_position=oxygen_position, - surface_data=array_surfaces, - popt=array_popt[0], + surface_data=[array_surfaces[slice_idx]], + popt=array_popt[slice_idx], wall_coords=wall_coords, - alpha=list_alfas[0], + alpha=list_alfas[slice_idx], ) # Interactive view in a notebook From 9e6ee10f51900e90a28c0850b54b1c1cb307de38 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 15:25:21 +0200 Subject: [PATCH 20/31] Made oxygen_type and hydrogen_type without default to avoid silent errors. --- CITATION.cff | 2 ++ src/wetting_angle_kit/parsers/lammps_dump.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 6018ed4..e55ec1b 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,6 +1,8 @@ cff-version: 1.2.0 message: "If you use wetting-angle-kit in your research, please cite it using the metadata below." title: "wetting-angle-kit: a Python package to streamline the computation of wetting contact angles of nanodroplets on surfaces" +version: 0.1.2 +date-released: "2026-06-01" type: software license: BSD-3-Clause repository-code: "https://github.com/Matgenix/wetting-angle-kit" diff --git a/src/wetting_angle_kit/parsers/lammps_dump.py b/src/wetting_angle_kit/parsers/lammps_dump.py index f803bcd..8efd3ca 100644 --- a/src/wetting_angle_kit/parsers/lammps_dump.py +++ b/src/wetting_angle_kit/parsers/lammps_dump.py @@ -266,8 +266,8 @@ class LammpsDumpWaterFinder: def __init__( self, filepath: str, - oxygen_type: int = 3, - hydrogen_type: int = 2, + oxygen_type: int, + hydrogen_type: int, oh_cutoff: float = 1.2, ): """ @@ -275,10 +275,12 @@ def __init__( ---------- filepath : str Path to LAMMPS dump file. - oxygen_type : int, default 3 - LAMMPS particle type ID for oxygen atoms. - hydrogen_type : int, default 2 - LAMMPS particle type ID for hydrogen atoms. + oxygen_type : int + LAMMPS particle type ID for oxygen atoms (required; LAMMPS + type numbering is system-specific so there is no safe default). + hydrogen_type : int + LAMMPS particle type ID for hydrogen atoms (required; LAMMPS + type numbering is system-specific so there is no safe default). oh_cutoff : float, default 1.2 O-H distance cutoff (Å) for water molecule detection. """ From a3dbcc859be23110af0c2a90f4e4d9f454a1c104 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 15:37:41 +0200 Subject: [PATCH 21/31] Uncapitalized tutorial source files. --- docs/source/introduction/index.rst | 6 +++--- .../introduction/{Installation.rst => installation.rst} | 0 .../introduction/{Introduction.rst => introduction.rst} | 0 ...etical_foundations.rst => theoretical_foundations.rst} | 0 .../{Binning_method_tuto.rst => binning_method_tuto.rst} | 0 docs/source/tutorials/index.rst | 8 ++++---- .../{Parser_tutorial.rst => parser_tutorial.rst} | 0 .../{Slicing_method_tuto.rst => slicing_method_tuto.rst} | 0 ...cing_droplet.rst => visualization_slicing_droplet.rst} | 0 9 files changed, 7 insertions(+), 7 deletions(-) rename docs/source/introduction/{Installation.rst => installation.rst} (100%) rename docs/source/introduction/{Introduction.rst => introduction.rst} (100%) rename docs/source/introduction/{Theoretical_foundations.rst => theoretical_foundations.rst} (100%) rename docs/source/tutorials/{Binning_method_tuto.rst => binning_method_tuto.rst} (100%) rename docs/source/tutorials/{Parser_tutorial.rst => parser_tutorial.rst} (100%) rename docs/source/tutorials/{Slicing_method_tuto.rst => slicing_method_tuto.rst} (100%) rename docs/source/tutorials/{Visualization_slicing_droplet.rst => visualization_slicing_droplet.rst} (100%) diff --git a/docs/source/introduction/index.rst b/docs/source/introduction/index.rst index aa0b168..8a3b0a0 100644 --- a/docs/source/introduction/index.rst +++ b/docs/source/introduction/index.rst @@ -6,6 +6,6 @@ Learn about wetting_angle_kit's theoretical foundations and package architecture .. toctree:: :maxdepth: 1 - Introduction - Installation - Theoretical_foundations + introduction + installation + theoretical_foundations diff --git a/docs/source/introduction/Installation.rst b/docs/source/introduction/installation.rst similarity index 100% rename from docs/source/introduction/Installation.rst rename to docs/source/introduction/installation.rst diff --git a/docs/source/introduction/Introduction.rst b/docs/source/introduction/introduction.rst similarity index 100% rename from docs/source/introduction/Introduction.rst rename to docs/source/introduction/introduction.rst diff --git a/docs/source/introduction/Theoretical_foundations.rst b/docs/source/introduction/theoretical_foundations.rst similarity index 100% rename from docs/source/introduction/Theoretical_foundations.rst rename to docs/source/introduction/theoretical_foundations.rst diff --git a/docs/source/tutorials/Binning_method_tuto.rst b/docs/source/tutorials/binning_method_tuto.rst similarity index 100% rename from docs/source/tutorials/Binning_method_tuto.rst rename to docs/source/tutorials/binning_method_tuto.rst diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst index 0e0f617..4903ad1 100644 --- a/docs/source/tutorials/index.rst +++ b/docs/source/tutorials/index.rst @@ -7,7 +7,7 @@ Step-by-step guides for using wetting_angle_kit. :maxdepth: 1 :caption: Available Tutorials: - Parser_tutorial - Binning_method_tuto - Slicing_method_tuto - Visualization_slicing_droplet + parser_tutorial + binning_method_tuto + slicing_method_tuto + visualization_slicing_droplet diff --git a/docs/source/tutorials/Parser_tutorial.rst b/docs/source/tutorials/parser_tutorial.rst similarity index 100% rename from docs/source/tutorials/Parser_tutorial.rst rename to docs/source/tutorials/parser_tutorial.rst diff --git a/docs/source/tutorials/Slicing_method_tuto.rst b/docs/source/tutorials/slicing_method_tuto.rst similarity index 100% rename from docs/source/tutorials/Slicing_method_tuto.rst rename to docs/source/tutorials/slicing_method_tuto.rst diff --git a/docs/source/tutorials/Visualization_slicing_droplet.rst b/docs/source/tutorials/visualization_slicing_droplet.rst similarity index 100% rename from docs/source/tutorials/Visualization_slicing_droplet.rst rename to docs/source/tutorials/visualization_slicing_droplet.rst From 0a233a3d376aa7c2256f664e9ff997cb36cdff40 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 15:39:25 +0200 Subject: [PATCH 22/31] Removed last alfas. --- docs/source/tutorials/visualization_slicing_droplet.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/tutorials/visualization_slicing_droplet.rst b/docs/source/tutorials/visualization_slicing_droplet.rst index 192ec8e..ee0c01e 100644 --- a/docs/source/tutorials/visualization_slicing_droplet.rst +++ b/docs/source/tutorials/visualization_slicing_droplet.rst @@ -86,8 +86,8 @@ The visualization workflow involves the following steps: max_dist=100, ) - list_alfas, array_surfaces, array_popt = processor.predict_contact_angle() - print("Mean contact angles (°):", list_alfas) + list_angles, array_surfaces, array_popt = processor.predict_contact_angle() + print("Mean contact angles (°):", list_angles) ---- @@ -107,7 +107,7 @@ The visualization workflow involves the following steps: surface_data=[array_surfaces[slice_idx]], popt=array_popt[slice_idx], wall_coords=wall_coords, - alpha=list_alfas[slice_idx], + alpha=list_angles[slice_idx], ) # Interactive view in a notebook From 7f388ced1b942eeff5adf6de92f9339a87b44652 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Tue, 2 Jun 2026 15:56:47 +0200 Subject: [PATCH 23/31] Added tests for analysis. --- .../test_binning_surface_definition.py | 211 +++++++++++++++++- .../test_slicing_surface_definition.py | 192 ++++++++++++++++ 2 files changed, 394 insertions(+), 9 deletions(-) create mode 100644 tests/test_analysis/test_slicing_surface_definition.py diff --git a/tests/test_analysis/test_binning_surface_definition.py b/tests/test_analysis/test_binning_surface_definition.py index 3fd344b..00d335d 100644 --- a/tests/test_analysis/test_binning_surface_definition.py +++ b/tests/test_analysis/test_binning_surface_definition.py @@ -1,19 +1,212 @@ +import warnings + import numpy as np +import pytest from wetting_angle_kit.analysis.binning.surface_definition import ( HyperbolicTangentModel, ) +# Reference parameter set used across the analytic checks below. +# Wall at z=0 sits inside a sphere of radius 10 centered at z=8. +_REF_PARAMS = [1.0, 0.0, 10.0, 8.0, 0.0, 1.0, 1.0] +_PARAM_NAMES = ["rho1", "rho2", "R_eq", "zi_c", "zi_0", "t1", "t2"] -def test_hyperbolic_tangent_compute_isoline_well_formed(): - """Wall inside the fitted sphere should yield finite isoline arrays.""" + +def _fitted_model(params=_REF_PARAMS) -> HyperbolicTangentModel: model = HyperbolicTangentModel() - # Wall at z=0, droplet center at z=8, radius 10 → wall is inside the - # sphere (|0 - 8| = 8 < 10). Densities and thicknesses are positive. - model.params = [1.0, 0.0, 10.0, 8.0, 0.0, 1.0, 1.0] + model.params = list(params) + return model + + +# --- compute_isoline ----------------------------------------------------- + + +def test_hyperbolic_tangent_compute_isoline_well_formed(): + """Wall inside the fitted sphere should yield finite isoline arrays + whose points exactly satisfy the scaled-sphere and wall equations.""" + model = _fitted_model() circle_xi, circle_zi, wall_xi, wall_zi = model.compute_isoline() assert circle_xi.size == 100 - assert np.all(np.isfinite(circle_xi)) - assert np.all(np.isfinite(circle_zi)) - assert np.all(np.isfinite(wall_xi)) - np.testing.assert_allclose(wall_zi, 0.0) + assert wall_xi.size == 100 + + # Circle points sit on the visualization sphere of radius + # scale_factor * R_eq centered at (0, z_center). + r = 0.95 * _REF_PARAMS[2] # scale_factor * R_eq + z_center = _REF_PARAMS[3] + np.testing.assert_allclose(circle_xi**2 + (circle_zi - z_center) ** 2, r**2) + # The contact point closes the arc at xi = sqrt(r^2 - (z_wall - z_c)^2), + # z = z_wall; the arc ends at the sphere apex (xi=0, z=z_c+r). + z_wall = _REF_PARAMS[4] + xi_contact = np.sqrt(r**2 - (z_wall - z_center) ** 2) + assert circle_xi[0] == pytest.approx(xi_contact) + assert circle_zi[0] == pytest.approx(z_wall) + assert circle_xi[-1] == pytest.approx(0.0, abs=1e-12) + assert circle_zi[-1] == pytest.approx(z_center + r) + + # Wall line spans [0, xi_contact] at constant z = z_wall. + np.testing.assert_allclose(wall_zi, z_wall) + assert wall_xi[0] == pytest.approx(0.0) + assert wall_xi[-1] == pytest.approx(xi_contact) + + +def test_compute_isoline_raises_when_wall_outside_sphere(): + # |z_wall - z_center| = 12 > R_eq = 10 → no intersection → ValueError. + model = _fitted_model([1.0, 0.0, 10.0, 0.0, 12.0, 1.0, 1.0]) + with pytest.raises(ValueError, match="outside the fitted droplet radius"): + model.compute_isoline() + + +def test_compute_isoline_requires_fit_first(): + model = HyperbolicTangentModel() + model.params = None + with pytest.raises(ValueError, match="must be fitted"): + model.compute_isoline() + + +# --- compute_contact_angle ------------------------------------------------ + + +def test_compute_contact_angle_wall_at_equator_is_ninety_degrees(): + # Sphere center on the wall (zi_c = zi_0) → tangent at intersection is + # vertical → contact angle is 90°. + model = _fitted_model([1.0, 0.0, 10.0, 0.0, 0.0, 1.0, 1.0]) + assert model.compute_contact_angle() == pytest.approx(90.0) + + +def test_compute_contact_angle_wall_above_center_gives_acute_angle(): + # zi_0 - zi_c = +5, R_eq = 10 → xi_cross = sqrt(75); contact angle 60°. + model = _fitted_model([1.0, 0.0, 10.0, 0.0, 5.0, 1.0, 1.0]) + assert model.compute_contact_angle() == pytest.approx(60.0) + + +def test_compute_contact_angle_wall_below_center_gives_obtuse_angle(): + # zi_0 - zi_c = -5, R_eq = 10 → droplet sits past its equator on the + # wall → contact angle 120°. + model = _fitted_model([1.0, 0.0, 10.0, 5.0, 0.0, 1.0, 1.0]) + assert model.compute_contact_angle() == pytest.approx(120.0) + + +def test_compute_contact_angle_returns_nan_when_wall_outside_sphere(): + model = _fitted_model([1.0, 0.0, 10.0, 0.0, 12.0, 1.0, 1.0]) + with pytest.warns(RuntimeWarning, match="outside the fitted droplet sphere"): + angle = model.compute_contact_angle() + assert np.isnan(angle) + + +def test_compute_contact_angle_requires_fit_first(): + model = HyperbolicTangentModel() + model.params = None + with pytest.raises(ValueError, match="must be fitted"): + model.compute_contact_angle() + + +# --- evaluate / evaluate_on_grid ----------------------------------------- + + +def test_evaluate_matches_fitting_function(): + model = _fitted_model() + xi, zi = 3.0, 4.0 + rho1, rho2, R_eq, zi_c, zi_0, t1, t2 = _REF_PARAMS + r = np.sqrt(xi**2 + (zi - zi_c) ** 2) + z = zi - zi_0 + expected = ( + 0.5 * ((rho1 + rho2) - (rho1 - rho2) * np.tanh(2 * (r - R_eq) / t1)) + ) * (0.5 * (1 + np.tanh(2 * z / t2))) + assert model.evaluate((xi, zi)) == pytest.approx(expected) + + +def test_evaluate_requires_fit_first(): + model = HyperbolicTangentModel() + model.params = None + with pytest.raises(ValueError, match="must be fitted"): + model.evaluate((0.0, 0.0)) + + +def test_evaluate_on_grid_shape_and_values(): + model = _fitted_model() + xi_grid = np.array([0.0, 1.0, 2.0, 3.0]) + zi_grid = np.array([4.0, 5.0]) + grid = model.evaluate_on_grid(xi_grid, zi_grid) + assert grid.shape == (len(xi_grid), len(zi_grid)) + # Spot-check entries against scalar evaluate calls (indexing='ij'). + for i, xi in enumerate(xi_grid): + for j, zi in enumerate(zi_grid): + assert grid[i, j] == pytest.approx(model.evaluate((xi, zi))) + + +# --- get_parameters / get_parameter_strings ------------------------------ + + +def test_get_parameters_maps_names_to_values(): + model = _fitted_model() + params = model.get_parameters() + assert list(params.keys()) == _PARAM_NAMES + assert list(params.values()) == _REF_PARAMS + + +def test_get_parameters_requires_fit_first(): + model = HyperbolicTangentModel() + model.params = None + with pytest.raises(ValueError, match="must be fitted"): + model.get_parameters() + + +def test_get_parameter_strings_format(): + model = _fitted_model() + strings = model.get_parameter_strings() + assert len(strings) == len(_PARAM_NAMES) + for name, value, line in zip(_PARAM_NAMES, _REF_PARAMS, strings, strict=True): + assert line == f"{name}:{value}\n" + + +def test_get_parameter_strings_requires_fit_first(): + model = HyperbolicTangentModel() + model.params = None + with pytest.raises(ValueError, match="must be fitted"): + model.get_parameter_strings() + + +# --- fit (round-trip on a synthetic density field) ----------------------- + + +def test_fit_recovers_synthetic_parameters(): + # Matches the call style used by BinningBatchFitter: flattened + # (xi, zi) coordinates and a flattened density vector. + true_params = [0.02, 0.001, 12.0, 6.0, 0.0, 1.5, 1.2] + xi_grid = np.linspace(0.1, 25.0, 30) + zi_grid = np.linspace(-5.0, 25.0, 35) + xi_mesh, zi_mesh = np.meshgrid(xi_grid, zi_grid, indexing="ij") + xi_flat = xi_mesh.ravel() + zi_flat = zi_mesh.ravel() + + seed_model = HyperbolicTangentModel(initial_params=list(true_params)) + truth = seed_model._fitting_function((xi_flat, zi_flat), *true_params) + + # Start from a perturbed initial guess to make the recovery non-trivial. + perturbed = [p * 1.1 for p in true_params] + model = HyperbolicTangentModel(initial_params=perturbed) + fitted = model.fit((xi_flat, zi_flat), truth) + assert fitted is model + np.testing.assert_allclose(model.params, true_params, rtol=1e-4, atol=1e-4) + + +def test_warn_if_at_bounds_fires_when_parameter_pinned(): + # Drive ``_warn_if_at_bounds`` directly: the TRF solver inside ``fit`` + # keeps iterates strictly feasible, so it's hard to land exactly on a + # bound through curve_fit. The warning logic itself is what matters. + model = HyperbolicTangentModel() + # t1 sits at its lower bound of 1e-6. + model.params = np.array([1e-3, 1e-3, 10.0, 0.0, 0.0, 1e-6, 1.0]) + with pytest.warns(RuntimeWarning, match="at the physical bound"): + model._warn_if_at_bounds() + + +def test_warn_if_at_bounds_silent_when_parameters_interior(): + # Interior values across all seven parameters; _REF_PARAMS itself has + # rho2=0 sitting on its lower bound and would (correctly) warn. + model = HyperbolicTangentModel() + model.params = np.array([1e-3, 1e-3, 10.0, 0.0, 0.0, 1.0, 1.0]) + with warnings.catch_warnings(): + warnings.simplefilter("error") # any warning would fail the test + model._warn_if_at_bounds() diff --git a/tests/test_analysis/test_slicing_surface_definition.py b/tests/test_analysis/test_slicing_surface_definition.py new file mode 100644 index 0000000..f04cadd --- /dev/null +++ b/tests/test_analysis/test_slicing_surface_definition.py @@ -0,0 +1,192 @@ +import numpy as np +import pytest + +from wetting_angle_kit.analysis.slicing.surface_definition import ( + SurfaceDefinition, +) + + +def _bare_surface(**overrides) -> SurfaceDefinition: + """Build a SurfaceDefinition with defaults that test setup can override.""" + kwargs = dict( + atom_coords=np.zeros((1, 3)), + delta_angle=10.0, + max_dist=20.0, + center_geom=np.zeros(3), + gamma=0.0, + ) + kwargs.update(overrides) + return SurfaceDefinition(**kwargs) + + +# --- density_profile (static tanh model) --------------------------------- + + +def test_density_profile_at_interface_equals_offset(): + # tanh(0) = 0, so rho(zd) = h regardless of d. + z = np.array([5.0]) + rho = SurfaceDefinition.density_profile(z, zd=5.0, d=0.5, h=0.3) + assert rho == pytest.approx(0.3) + + +def test_density_profile_saturates_far_from_interface(): + # tanh(+inf) = 1 (liquid side), tanh(-inf) = -1 (vapor side). + z = np.array([-50.0, 50.0]) + rho = SurfaceDefinition.density_profile(z, zd=5.0, d=0.5, h=0.3) + np.testing.assert_allclose(rho, [0.8, -0.2], atol=1e-10) + + +# --- density_contribution (Gaussian smoothing on a KD-tree) -------------- + + +def test_density_contribution_empty_atom_set_returns_zeros(): + surf = _bare_surface(atom_coords=np.empty((0, 3))) + positions = np.random.default_rng(0).normal(size=(7, 3)) + result = surf.density_contribution(positions) + assert result.shape == (7,) + np.testing.assert_array_equal(result, np.zeros(7)) + + +def test_density_contribution_zero_samples_returns_zeros(): + surf = _bare_surface(atom_coords=np.zeros((3, 3))) + result = surf.density_contribution(np.empty((0, 3))) + assert result.shape == (0,) + + +def test_density_contribution_distant_atoms_short_circuit(): + # Single atom 173 Å from origin; 5 sigma cutoff at default sigma=3 is 15 Å. + surf = _bare_surface(atom_coords=np.array([[100.0, 100.0, 100.0]])) + result = surf.density_contribution(np.zeros((4, 3))) + np.testing.assert_array_equal(result, np.zeros(4)) + + +def test_density_contribution_peaks_at_atom_position(): + sigma = 3.0 + surf = _bare_surface( + atom_coords=np.array([[0.0, 0.0, 0.0]]), + density_sigma=sigma, + ) + samples = np.array([[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]]) + result = surf.density_contribution(samples) + peak = 1.0 / (2 * np.pi * sigma**2) ** 1.5 + assert result[0] == pytest.approx(peak) + # 10 Å lies inside the 15 Å default cutoff but is heavily Gaussian-suppressed. + expected_far = peak * np.exp(-(10.0**2) / (2 * sigma**2)) + assert result[1] == pytest.approx(expected_far) + + +def test_density_contribution_density_conversion_unused_in_contribution(): + # density_conversion is applied in analyze_lines, not in + # density_contribution itself: setting it must not change this raw + # output, which equals the bare Gaussian kernel at the sample. + sigma = 3.0 + common = dict( + atom_coords=np.array([[0.0, 0.0, 0.0]]), + density_sigma=sigma, + ) + samples = np.array([[1.0, 0.0, 0.0]]) + expected = (1.0 / (2 * np.pi * sigma**2) ** 1.5) * np.exp(-1.0 / (2 * sigma**2)) + baseline = _bare_surface(density_conversion=1.0, **common).density_contribution( + samples + ) + scaled = _bare_surface(density_conversion=12.5, **common).density_contribution( + samples + ) + assert baseline[0] == pytest.approx(expected) + np.testing.assert_allclose(scaled, baseline) + + +# --- _fit_density_profiles_batched (Gauss-Newton tanh fit) --------------- + + +def test_fit_density_profiles_batched_recovers_known_zd(): + surf = _bare_surface(max_dist=30.0) + z = np.linspace(0.0, 30.0, 80) + true_zd = np.array([10.0, 15.0, 22.0]) + d, h = 0.6, 0.2 + densities = np.stack([d * np.tanh(zd - z) + h for zd in true_zd]) + fitted = surf._fit_density_profiles_batched(z, densities) + np.testing.assert_allclose(fitted, true_zd, atol=1e-3) + + +def test_fit_density_profiles_batched_constant_input_falls_back_to_zero(): + # Constant density: rho_max==rho_min so d0=0 and the data midpoint + # crossing zd0=z[argmin(0)]=z[0]=0. The first GN iteration then has a + # singular normal matrix (j_zd = d*(1-u^2) = 0), the solver breaks, + # and the final clip returns the seed value 0.0 exactly. + surf = _bare_surface(max_dist=20.0) + z = np.linspace(0.0, 20.0, 40) + densities = np.full((2, 40), 0.5) + fitted = surf._fit_density_profiles_batched(z, densities) + np.testing.assert_array_equal(fitted, np.zeros(2)) + + +# --- analyze_lines (end-to-end on a synthetic 2D droplet) ---------------- + + +def _disk_atoms_in_xz(radius: float, n_atoms: int, seed: int) -> np.ndarray: + """Uniform 2D disk of atoms in the y=0 slice plane.""" + rng = np.random.default_rng(seed) + r = radius * np.sqrt(rng.uniform(0.0, 1.0, n_atoms)) + theta = rng.uniform(0.0, 2 * np.pi, n_atoms) + return np.column_stack([r * np.cos(theta), np.zeros(n_atoms), r * np.sin(theta)]) + + +def test_analyze_lines_recovers_disk_radius(): + radius = 15.0 + atoms = _disk_atoms_in_xz(radius, n_atoms=4000, seed=42) + surf = SurfaceDefinition( + atom_coords=atoms, + delta_angle=30.0, + max_dist=25.0, + center_geom=np.zeros(3), + gamma=0.0, + points_per_angstrom=2.0, + ) + rr, xz = surf.analyze_lines() + n_rays = int(360 / 30) + assert len(rr) == n_rays + assert len(xz) == n_rays + assert all(len(row) == 2 for row in rr) + assert all(len(row) == 2 for row in xz) + # The fit pulls the apparent interface ~0.5 Å inside the geometric + # boundary because the model uses a fixed-width tanh while the data + # is a Gaussian-smoothed (sigma=3) step; the mismatch biases zd + # toward the liquid side. Per-ray scatter from finite atom count is + # ~0.3 Å on top of that. + interface_distances = np.array([row[0] for row in rr]) + assert np.max(np.abs(interface_distances - radius)) < 1.0 + assert abs(interface_distances.mean() - radius) < 0.7 + + +def test_analyze_lines_returns_consistent_xz_projection(): + center = np.array([5.0, 0.0, -2.0]) + atoms = _disk_atoms_in_xz(radius=10.0, n_atoms=2000, seed=0) + center + surf = SurfaceDefinition( + atom_coords=atoms, + delta_angle=60.0, + max_dist=20.0, + center_geom=center, + gamma=0.0, + points_per_angstrom=2.0, + ) + rr, xz = surf.analyze_lines() + # Projection contract: xz[i] = center + interface_re * (cos(beta), 0, sin(beta)). + for (re, beta), (x_proj, z_proj) in zip(rr, xz, strict=True): + beta_rad = np.deg2rad(beta) + assert x_proj == pytest.approx(np.cos(beta_rad) * re + center[0]) + assert z_proj == pytest.approx(np.sin(beta_rad) * re + center[2]) + + +def test_analyze_lines_ray_count_matches_delta_angle(): + surf = _bare_surface( + atom_coords=_disk_atoms_in_xz(radius=8.0, n_atoms=500, seed=1), + delta_angle=45.0, + max_dist=15.0, + ) + rr, xz = surf.analyze_lines() + assert len(rr) == 8 + assert len(xz) == 8 + # Each ray records its own azimuth angle in degrees, evenly spaced. + betas = [row[1] for row in rr] + np.testing.assert_allclose(betas, np.arange(0.0, 360.0, 45.0)) From fe1a6e87c68b2adf8a40c7c6cdaba7eadda586b6 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Wed, 3 Jun 2026 10:31:13 +0200 Subject: [PATCH 24/31] Removed LammpsContactAngleAnimator as it is project-specific. --- .../visualization/__init__.py | 2 - .../visualization/animator.py | 252 ------------------ .../test_droplet_slice_plot.py | 52 +--- 3 files changed, 2 insertions(+), 304 deletions(-) delete mode 100644 src/wetting_angle_kit/visualization/animator.py diff --git a/src/wetting_angle_kit/visualization/__init__.py b/src/wetting_angle_kit/visualization/__init__.py index 6134932..bf10e4d 100644 --- a/src/wetting_angle_kit/visualization/__init__.py +++ b/src/wetting_angle_kit/visualization/__init__.py @@ -1,4 +1,3 @@ -from wetting_angle_kit.visualization.animator import LammpsContactAngleAnimator from wetting_angle_kit.visualization.base_trajectory_plotter import ( BaseTrajectoryPlotter, ) @@ -15,7 +14,6 @@ "BaseTrajectoryPlotter", "BinningTrajectoryPlotter", "DropletSlicePlotter", - "LammpsContactAngleAnimator", "SlicingTrajectoryPlotter", "TrajectoryStats", ] diff --git a/src/wetting_angle_kit/visualization/animator.py b/src/wetting_angle_kit/visualization/animator.py deleted file mode 100644 index fd36e05..0000000 --- a/src/wetting_angle_kit/visualization/animator.py +++ /dev/null @@ -1,252 +0,0 @@ -import numpy as np -import plotly.graph_objects as go - -from wetting_angle_kit.analysis.slicing import SlicingFrameFitter -from wetting_angle_kit.io_utils import recenter_droplet_pbc -from wetting_angle_kit.parsers import ( - LammpsDumpParser, - LammpsDumpWallParser, - LammpsDumpWaterFinder, -) -from wetting_angle_kit.visualization.droplet_slice_plot import DropletSlicePlotter - - -class LammpsContactAngleAnimator: - """Plotly slider animation of the median per-frame slice angle. - - This class is **LAMMPS-specific**: it instantiates - :class:`~wetting_angle_kit.parsers.LammpsDumpParser`, - :class:`~wetting_angle_kit.parsers.LammpsDumpWallParser` and - :class:`~wetting_angle_kit.parsers.LammpsDumpWaterFinder` directly. A - parser-agnostic version would have to dispatch all three from a - factory; the rename makes the current coupling explicit rather than - promising generality that the implementation does not deliver. - """ - - def __init__( - self, - filename: str, - oxygen_type: int, - hydrogen_type: int, - liquid_particle_types: set, - n_frames: int = 10, - droplet_geometry: str = "cylinder_y", - delta_cylinder: float | None = None, - delta_gamma: float | None = None, - max_dist: int = 100, - precentered: bool = False, - ): - """ - Parameters - ---------- - filename : str - Path to LAMMPS dump trajectory file. - oxygen_type : int - LAMMPS particle type ID for oxygen atoms. - hydrogen_type : int - LAMMPS particle type ID for hydrogen atoms. - liquid_particle_types : set - LAMMPS particle type IDs for all liquid atoms (used to mask wall parser). - n_frames : int, default 10 - Number of frames to include in the animation. - droplet_geometry : str, default "cylinder_y" - Droplet geometry passed to SlicingFrameFitter. - delta_cylinder : float, optional - Step size along the slicing axis (Å); required for - ``cylinder_x`` / ``cylinder_y`` modes, must be None for spherical. - delta_gamma : float, optional - Azimuthal step (degrees) for spherical droplet geometry; - required for spherical, must be None for cylinder modes. - max_dist : int, default 100 - Maximum radial distance for line sampling (Å). - precentered : bool, default False - Set True if the trajectory already recenters the droplet at - every frame and atoms are not wrapped across periodic - boundaries; the per-frame circular-mean recentering is then - skipped. Setting this on a trajectory that does NOT satisfy the - precondition will misplace the contact-angle overlay. - """ - if droplet_geometry == "spherical": - if delta_gamma is None: - raise ValueError("delta_gamma must be provided for spherical analysis") - if delta_cylinder is not None: - raise ValueError( - "delta_cylinder must not be set for spherical analysis " - "(it is only valid for cylinder_x / cylinder_y)." - ) - elif droplet_geometry in ("cylinder_x", "cylinder_y"): - if delta_cylinder is None: - raise ValueError( - f"delta_cylinder must be provided for {droplet_geometry}." - ) - if delta_gamma is not None: - raise ValueError( - f"delta_gamma must not be set for {droplet_geometry} " - "(it is only valid for spherical)." - ) - self.filename = filename - self.oxygen_type = oxygen_type - self.hydrogen_type = hydrogen_type - self.liquid_particle_types = liquid_particle_types - self.n_frames = n_frames - self.droplet_geometry = droplet_geometry - self.delta_cylinder = delta_cylinder - self.delta_gamma = delta_gamma - self.max_dist = max_dist - self.precentered = precentered - - # Initialize objects - self.wat_find = LammpsDumpWaterFinder( - self.filename, - oxygen_type=self.oxygen_type, - hydrogen_type=self.hydrogen_type, - ) - self.oxygen_indices = self.wat_find.get_water_oxygen_ids(frame_index=0) - self.coord_wall = LammpsDumpWallParser( - self.filename, liquid_particle_types=list(self.liquid_particle_types) - ) - self.wall_coords = self.coord_wall.parse(frame_index=0) - self.parser = LammpsDumpParser(filepath=self.filename) - self.plotter = DropletSlicePlotter(center=True) - - def generate_animation( - self, output_filename: str = "ContactAngle_Median_PerFrame_Slider.html" - ) -> None: - """Build and write HTML with slider of median contact angles over frames. - - Parameters - ---------- - output_filename : str, default "ContactAngle_Median_PerFrame_Slider.html" - Output HTML file path. - """ - fig = go.Figure() - frames_list = [] - frame_labels = [] - median_angles = [] - for frame_idx in range(self.n_frames): - oxygen_position = self.parser.parse( - frame_index=frame_idx, indices=self.oxygen_indices - ) - if self.precentered: - liquid_geom_center = np.mean(oxygen_position, axis=0) - else: - box_size_xy = ( - self.parser.box_size_x(frame_index=frame_idx), - self.parser.box_size_y(frame_index=frame_idx), - ) - oxygen_position, liquid_geom_center = recenter_droplet_pbc( - oxygen_position, self.droplet_geometry, box_size=box_size_xy - ) - processor = SlicingFrameFitter( - liquid_coordinates=oxygen_position, - liquid_geom_center=liquid_geom_center, - droplet_geometry=self.droplet_geometry, - delta_cylinder=self.delta_cylinder, - delta_gamma=self.delta_gamma, - max_dist=self.max_dist, - ) - angles, surfaces, popt_arrays = processor.predict_contact_angle() - if not angles: - # No slice in this frame produced a usable contact angle - # (e.g. fitting failed everywhere). Skip the frame rather - # than letting the median lookup crash on an empty list. - continue - median_idx = np.argsort(angles)[len(angles) // 2] - alpha = angles[median_idx] - popt = popt_arrays[median_idx] - surface = [surfaces[median_idx]] - median_angles.append(alpha) - fig_frame = self.plotter.plot_surface_points( - oxygen_position=oxygen_position, - surface_data=surface, - popt=popt, - wall_coords=self.wall_coords.copy(), - y_com=np.mean(oxygen_position[:, 1]), - pbc_y=None, - alpha=alpha, - show_water=True, - show_surface=True, - show_circle=True, - show_tangent=True, - show_wall=True, - ) - frame = go.Frame( - data=fig_frame.data, - name=f"Frame {frame_idx}", - layout=go.Layout( - title_text=( - f"Frame {frame_idx} | Median contact angle = {alpha:.2f}°" - ) - ), - ) - frames_list.append(frame) - frame_labels.append(f"Frame {frame_idx}") - if not frames_list: - raise RuntimeError( - "No frame produced a usable contact angle; cannot build animation." - ) - fig.frames = frames_list - fig.add_traces(frames_list[0].data) - fig.update_layout( - title="Interactive Contact Angle Evolution (Median Slice per Frame)", - width=800, - height=600, - margin=dict(l=80, r=200, t=80, b=100), - xaxis_title="x (Å)", - yaxis_title="z (Å)", - template="simple_white", - showlegend=True, - legend=dict( - x=1.05, - y=0.95, - bgcolor="rgba(255,255,255,0.8)", - bordercolor="lightgray", - borderwidth=1, - font=dict(size=11), - ), - xaxis=dict( - mirror=True, - showline=True, - linecolor="black", - ticks="outside", - showgrid=True, - gridcolor="lightgray", - zeroline=False, - ), - yaxis=dict( - mirror=True, - showline=True, - linecolor="black", - ticks="outside", - showgrid=True, - gridcolor="lightgray", - zeroline=False, - scaleanchor="x", - scaleratio=1, - ), - sliders=[ - { - "active": 0, - "pad": {"b": 60, "t": 40}, - "x": 0.2, - "len": 0.6, - "y": -0.1, - "yanchor": "top", - "steps": [ - { - "args": [ - [f"Frame {k}"], - { - "frame": {"duration": 0, "redraw": True}, - "mode": "immediate", - }, - ], - "label": f"{k}", - "method": "animate", - } - for k in range(len(frames_list)) - ], - } - ], - ) - fig.write_html(output_filename) diff --git a/tests/test_visualization/test_droplet_slice_plot.py b/tests/test_visualization/test_droplet_slice_plot.py index db51d3f..c185548 100644 --- a/tests/test_visualization/test_droplet_slice_plot.py +++ b/tests/test_visualization/test_droplet_slice_plot.py @@ -1,14 +1,9 @@ -"""Smoke tests for the plotly droplet-slice plotter and the animator.""" +"""Smoke tests for the plotly droplet-slice plotter.""" import numpy as np import plotly.graph_objects as go -import pytest -from tests.conftest import trajectory_path -from wetting_angle_kit.visualization import ( - DropletSlicePlotter, - LammpsContactAngleAnimator, -) +from wetting_angle_kit.visualization import DropletSlicePlotter def _synthetic_droplet(seed=0): @@ -96,46 +91,3 @@ def test_droplet_slice_plotter_layers_can_be_disabled(): show_wall=False, ) assert len(fig.data) == 0 - - -# --- LammpsContactAngleAnimator --- - - -def test_contact_angle_animator_init_loads_fixture(): - """LammpsContactAngleAnimator.__init__ wires up parsers - and finders for a real fixture.""" - pytest.importorskip("ovito") - animator = LammpsContactAngleAnimator( - filename=trajectory_path("traj_spherical_drop_4k.lammpstrj"), - oxygen_type=1, - hydrogen_type=2, - liquid_particle_types={1, 2}, - n_frames=1, - droplet_geometry="cylinder_y", - delta_cylinder=20, - max_dist=50, - ) - assert animator.wall_coords.shape[1] == 3 - assert animator.oxygen_indices.size > 0 - assert animator.parser is not None - assert animator.plotter is not None - - -@pytest.mark.slow -def test_contact_angle_animator_generates_html(tmp_path): - """Smoke-test LammpsContactAngleAnimator on the cylindrical LAMMPS fixture.""" - pytest.importorskip("ovito") - output = tmp_path / "animation.html" - animator = LammpsContactAngleAnimator( - filename=trajectory_path("traj_10_3_330w_nve_4k_reajust.lammpstrj"), - oxygen_type=1, - hydrogen_type=2, - liquid_particle_types={1, 2}, - n_frames=1, - droplet_geometry="cylinder_y", - delta_cylinder=20, - max_dist=50, - ) - animator.generate_animation(output_filename=str(output)) - assert output.exists() - assert output.stat().st_size > 0 From 205a952dd1b0fa4ad27dd6896293fbe589368431 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Wed, 3 Jun 2026 13:43:34 +0200 Subject: [PATCH 25/31] Added pdf generation in CI from JOSS. Reviewed paper. --- .github/workflows/draft-pdf.yml | 28 ++++++++ wetting_angle_kit_JOSS/paper.md | 119 ++++++++++++-------------------- 2 files changed, 72 insertions(+), 75 deletions(-) create mode 100644 .github/workflows/draft-pdf.yml diff --git a/.github/workflows/draft-pdf.yml b/.github/workflows/draft-pdf.yml new file mode 100644 index 0000000..1bc1518 --- /dev/null +++ b/.github/workflows/draft-pdf.yml @@ -0,0 +1,28 @@ +name: Draft PDF +on: + push: + paths: + - wetting_angle_kit_JOSS/** + - .github/workflows/draft-pdf.yml + +jobs: + paper: + runs-on: ubuntu-latest + name: Paper Draft + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build draft PDF + uses: openjournals/openjournals-draft-action@master + with: + journal: joss + # This should be the path to the paper within your repo. + paper-path: wetting_angle_kit_JOSS/paper.md + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: paper + # This is the output path where Pandoc will write the compiled + # PDF. Note, this should be the same directory as the input + # paper.md + path: wetting_angle_kit_JOSS/paper.pdf \ No newline at end of file diff --git a/wetting_angle_kit_JOSS/paper.md b/wetting_angle_kit_JOSS/paper.md index dd0fc2f..c606f2f 100644 --- a/wetting_angle_kit_JOSS/paper.md +++ b/wetting_angle_kit_JOSS/paper.md @@ -49,8 +49,7 @@ bibliography: paper.bib Wetting-angle-kit is a Python toolkit designed to extract wettability properties, specifically the contact angle of a droplet on a surface, -from molecular dynamics (MD) simulations. -The software is designed for researchers working in MD simulation of interfaces +from molecular dynamics (MD) simulations of interfaces between liquids and solid surfaces. It supports a variety of standard file formats including extended XYZ, LAMMPS, @@ -63,19 +62,20 @@ reproducibility across different simulation setups. # Statement of need -Building upon foundational work ([@Hautman1997]), the methodologies for computating +Building upon foundational work [@Hautman1997], the methodologies for computing contact angles from MD simulations have progressed through several key milestones -[@Rafiee2012; @Vega2016; @Carlson2024].Despite these advancements, the field currently +[@Rafiee2012; @Vega2016; @Carlson2024]. +Despite these advancements, the field currently lacks a standardized, unified tool for comparing and validating the diverse methods used to derive contact angles. Such fragmentation undermines collaborative research and reproducibility, as many implementations remain inaccessible or poorly documented. In addition, the lack of a standardized framework makes it difficult to benchmark different approaches or assess the impact of methodological choices. -Wetting-angle-kit addresses this critical gap by providing a flexible, -open-source packa It enables the implementation of novel post-processing algorithms -for the extraction and calculation of contact angle, compare them against accepted techniques, -and establish a standardize benchmark for MD wettability analysis. +Wetting-angle-kit addresses this critical gap by providing a flexible, open-source package. +It enables the implementation of novel post-processing algorithms +for the extraction and calculation of contact angle, to compare them against accepted techniques, +and to establish a standardized benchmark for MD wettability analysis. # State of the field @@ -87,7 +87,7 @@ However, they do not include a standardized implementation of contact angle extraction methods, which are typically developed as custom scripts tailored to specific systems. -Existing approaches to contact angle estimation range from geometric fitting techniques +Existing approaches to contact angle computation range from geometric fitting techniques based on spherical or cylindrical cap approximations [@Hautman1997] to density-based interface analysis [@Vega2016] and pressure-tensor approaches derived from planar equilibrium simulations [@Carlson2024], making direct comparison across @@ -99,17 +99,13 @@ the development and/or implementation of other methods. # Software design Wetting-angle-kit is organized into three main components: -parsers, contact angle computation methods, and visualization, Fig. \ref{package_overview}. +parsers, analysis, and visualization, +as represented in Figure 1. This modular organization separates data handling, analysis, and visualization, allowing components to evolve independently while simplifying the integration of new features. -\begin{figure}[h!] -\centering -\includegraphics[width=0.9\textwidth, trim=100 480 100 200, clip]{package_overviewDiagram.drawio.pdf} -\caption{Wetting-angle-kit, package structure.} -\label{package_overview} -\end{figure} +![Wetting-angle-kit package structure.](package_overviewDiagram.drawio.pdf){width=90%} The parser module provides a unified interface for reading trajectory data from multiple formats, ensuring consistent handling of atomic coordinates, @@ -120,52 +116,43 @@ established trajectory-reading tools when available, while extended XYZ parsing implemented directly within the package. The parser also consistently handles periodic boundary conditions, ensuring that droplet shapes are correctly reconstructed across simulation boundaries and avoiding artifacts in interface detection. - -This consistency facilitates seamless integration with downstream analysis methods -and ensures the system's scalability, enabling researchers to easily +This consistency facilitates seamless integration with downstream analysis methods, enabling researchers to easily incorporate support for additional file formats or simulation engines. -The contact angle computation methods (analysis) module implements -two complementary approaches for contact angle estimation (Fig. \ref{analysis_methods}). - -\begin{figure}[h!] -\centering -\includegraphics[width=0.8\textwidth, trim=1.5cm 6cm 2.5cm 1cm, clip ]{schema_methods_analysis.pdf} -\caption{Schema of the two contact angle analysis methods.} -\label{analysis_methods} -\end{figure} - -The slicing method performs frame-by-frame geometric analysis, -enabling detailed temporal resolution at the cost of higher computational expense. +The analysis module implements +two complementary approaches for contact angle computation that are illustrated in Figure 2. +The slicing method consists in a frame-by-frame geometric analysis, +which enables a detailed temporal resolution. In practice, this approach provides a local characterization of the liquid–vapor interface, allowing the detection of asymmetries and transient deformations of the droplet shape. It is particularly well suited for non-equilibrium simulations or systems where the droplet deviates from an ideal spherical cap. In contrast, the binning method constructs time-averaged density fields, -providing a computationally efficient approach suitable for large datasets -and symmetric systems. By averaging particle positions over time, -this method reduces thermal fluctuations and produces a smoother -and more stable interface, making it suitable for extracting +reducing thermal fluctuations and producing a smoother +and more stable interface. This makes this approach suitable for extracting equilibrium contact angles from noisy datasets. However, this temporal averaging may obscure short-lived fluctuations and local deviations from ideal geometries. +The binning method is also more suited to symmetric systems, since atoms are folded into a single quadrant. +Due to the finer analysis it provides, the slicing method is one order of magnitude more +expensive computationnally than its binning counterpart. These two approaches reflect a trade-off between temporal resolution and statistical robustness, allowing users to select the method best suited to their system. +![Schema of the two contact angle analysis methods.](schema_methods_analysis.pdf){width=80%} + Additionally, wetting-angle-kit supports two geometric models commonly used -in the literature: spherical and cylindrical [@Scocchi2011] (Fig. \ref{geometries}). -While spherical droplets provide a more direct representation of droplet curvature, -cylindrical geometries reduce curvature effects and computational cost, +in the literature for droplets: spherical and cylindrical [@Scocchi2011] (see Figure 3). +While the spherical case provides a more direct representation of the droplet curvature, +a cylindrical geometry reduces curvature effects and computational cost by relying on periodic boundary conditions along the cylinder axis, at the expense of relying on an idealized geometry. -\begin{figure}[h!] -\centering -\includegraphics[width=0.48\textwidth]{wetting_angle_kit_3d_droplet.pdf} -\hfill -\includegraphics[width=0.48\textwidth]{wetting_angle_kit_cylinder.pdf} -\caption{Geometric representations of droplets used in the analysis: spherical droplet (left) and cylindrical droplet (right).} -\label{geometries} -\end{figure} +![Geometric representations of droplets used in the analysis: spherical droplet (left) and cylindrical droplet (right).](wetting_angle_kit_sphere_vs_cylinder.pdf){width=90%} + +The visualization module includes tools to support interpretation and validation +of analysis results without requiring external post-processing tools. +These tools consists in (1) a contact-angle vs. trajectory timeframe for the slicing analysis, +and (2) a density heatmap based on the binning analysis. The software architecture relies on abstract base classes to enforce consistent interfaces and facilitate extensibility. @@ -174,50 +161,32 @@ compatibility with existing workflows, promoting reuse and method comparison. It also facilitates the integration of newly developed methods into an existing and standardized analysis pipeline. -Visualization tools are included to support interpretation and validation -of analysis results without requiring external post-processing tools. -These tools provide representations of droplet geometries, enabling users to -directly inspect the quality of interface detection and fitting procedures. -By facilitating visual verification of intermediate and final results, -they help identify potential artifacts or inconsistencies in the analysis and improve -the reliability of extracted contact angles. - # Research impact statement Wetting-angle-kit provides a reproducible framework for contact angle analysis in MD simulations, addressing a common need in studies of nanoscale wetting. The package has been validated using MD simulations of water droplets on graphene and polymer substrates, yielding contact angle values consistent -with literature results (e.g., ~93° for graphene, ~110° for PTFE), see Fig. \ref{results}. -The reported contact angles were obtained by analyzing droplets of increasing size -and extrapolating to the macroscopic limit using the Modified Young’s relation, -where the contact angle is related to droplet size through a line-tension correction -term, enabling estimation of the infinite-droplet contact angle. -These results are consistent with literature values obtained using -similar carbon-oxygen LJ parameters [@Jorgensen1996]. - -\begin{figure}[h!] -\centering -\includegraphics[width=0.8\textwidth]{mean_cos_angle_vs_surface_graphite_ptfe.pdf} -\caption{Size-dependent contact angle analysis for water droplets on graphite - and PTFE substrates. Values of $\cos(\theta)$ are plotted as a function of the inverse square - root of the droplet surface area for droplets containing between 500 and 6000 water molecules. - Linear extrapolation following the Modified Young’s relation is used - to estimate the macroscopic (infinite-droplet) contact angle.} -\label{results} -\end{figure} +with literature results (e.g., ~93° for graphene, ~110° for PTFE), see Figure 4. +The reported contact angles were obtained by analyzing droplets of increasing sizes +and extrapolating to the macroscopic limit using the modified Young’s relation **ref**, +where the contact angle is related to droplet size, enabling the estimation of the infinite-droplet contact angle through linear extrapolation. +These results are consistent with values reported in the literature, obtained using +similar interatomic potential parameters [@Jorgensen1996] for the MD simulation. + +![Size-dependent contact angle analysis for water droplets on graphite and PTFE substrates. Values of $\cos(\theta)$ are plotted as a function of the inverse square root of the droplet surface area for droplets containing between 500 and 6000 water molecules. Linear extrapolation following the Modified Young’s relation is used to estimate the macroscopic (infinite-droplet) contact angle.](mean_cos_angle_vs_surface_graphite_ptfe.pdf) By enabling systematic comparison of analysis methods -and providing standardized workflows, the software supports more robust and +and providing standardized workflows, wetting-angle-kit supports more robust and reproducible wettability studies. Its modular design also facilitates integration into existing simulation pipelines and encourages community-driven extensions. The package is expected to be particularly -useful for researchers using various types of force fields (classical and MLIPs) +useful for researchers using various types of force fields (classical, ab initio, and machine learned) or investigating nanoscale interfacial phenomena. # AI usage disclosure -Generative AI tools were used in the development of the software, +Generative AI tools (Claude Code with Sonnet 4.6 and Opus 4.7, **XXX Gabriel add yours**) were used in the development of the software, for drafting and assisting debugging. Generative AI was used to assist in refining the language, translation and clarity of the manuscript and docstring. From 028bcef2bcfa08ce115507bd08df327354060665 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Wed, 3 Jun 2026 13:50:33 +0200 Subject: [PATCH 26/31] Updated figure captions. --- .../mean_cos_angle_vs_surface_graphite.pdf | Bin 31154 -> 0 bytes .../mean_cos_angle_vs_surface_ptfe.pdf | Bin 32649 -> 0 bytes wetting_angle_kit_JOSS/paper.md | 8 ++++---- 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 wetting_angle_kit_JOSS/mean_cos_angle_vs_surface_graphite.pdf delete mode 100644 wetting_angle_kit_JOSS/mean_cos_angle_vs_surface_ptfe.pdf diff --git a/wetting_angle_kit_JOSS/mean_cos_angle_vs_surface_graphite.pdf b/wetting_angle_kit_JOSS/mean_cos_angle_vs_surface_graphite.pdf deleted file mode 100644 index 96b2914747745dbc1f568cdacb95967254635af5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31154 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 A)Bpeg diff --git a/wetting_angle_kit_JOSS/paper.md b/wetting_angle_kit_JOSS/paper.md index c606f2f..7d7c897 100644 --- a/wetting_angle_kit_JOSS/paper.md +++ b/wetting_angle_kit_JOSS/paper.md @@ -105,7 +105,7 @@ This modular organization separates data handling, analysis, and visualization, allowing components to evolve independently while simplifying the integration of new features. -![Wetting-angle-kit package structure.](package_overviewDiagram.drawio.pdf){width=90%} +![Wetting-angle-kit package structure. The package is composed of three main modules: parsers, analysis, and visualization.](package_overviewDiagram.drawio.pdf){width=90%} The parser module provides a unified interface for reading trajectory data from multiple formats, ensuring consistent handling of atomic coordinates, @@ -139,7 +139,7 @@ expensive computationnally than its binning counterpart. These two approaches reflect a trade-off between temporal resolution and statistical robustness, allowing users to select the method best suited to their system. -![Schema of the two contact angle analysis methods.](schema_methods_analysis.pdf){width=80%} +![Schematic representation of the two methods developed in wetting-angle-kit to compute contact angle from a MD trajectory. In the slicing method (left), all trajectory frames are analyzed and a circle is fitted on each of those, providing a time evolution of the contact angle. In the binning method (right), all frames are concatenated to fictitiously increase the molecular density of the droplet, allowing for smoother statistics at the cost of losing the time dependence of the contact angle.](schema_methods_analysis.pdf){width=80%} Additionally, wetting-angle-kit supports two geometric models commonly used in the literature for droplets: spherical and cylindrical [@Scocchi2011] (see Figure 3). @@ -169,12 +169,12 @@ The package has been validated using MD simulations of water droplets on graphen and polymer substrates, yielding contact angle values consistent with literature results (e.g., ~93° for graphene, ~110° for PTFE), see Figure 4. The reported contact angles were obtained by analyzing droplets of increasing sizes -and extrapolating to the macroscopic limit using the modified Young’s relation **ref**, +and extrapolating to the macroscopic limit using the modified Young’s equation **ref**, where the contact angle is related to droplet size, enabling the estimation of the infinite-droplet contact angle through linear extrapolation. These results are consistent with values reported in the literature, obtained using similar interatomic potential parameters [@Jorgensen1996] for the MD simulation. -![Size-dependent contact angle analysis for water droplets on graphite and PTFE substrates. Values of $\cos(\theta)$ are plotted as a function of the inverse square root of the droplet surface area for droplets containing between 500 and 6000 water molecules. Linear extrapolation following the Modified Young’s relation is used to estimate the macroscopic (infinite-droplet) contact angle.](mean_cos_angle_vs_surface_graphite_ptfe.pdf) +![Size-dependent contact angle analysis for water droplets on graphite and PTFE substrates. Values of $\cos(\theta)$ are plotted as a function of the inverse square root of the droplet surface area for droplets containing between 500 and 6000 water molecules. A linear extrapolation following the modified Young’s equation is used to estimate the macroscopic (infinite-droplet) contact angle.](mean_cos_angle_vs_surface_graphite_ptfe.pdf) By enabling systematic comparison of analysis methods and providing standardized workflows, wetting-angle-kit supports more robust and From 42d1583b63920e13f9fff8c7b093246b020114cc Mon Sep 17 00:00:00 2001 From: gbrunin Date: Wed, 3 Jun 2026 13:58:48 +0200 Subject: [PATCH 27/31] Added automatically generated pdf by CI to the package. --- .github/workflows/draft-pdf.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/draft-pdf.yml b/.github/workflows/draft-pdf.yml index 1bc1518..afa699b 100644 --- a/.github/workflows/draft-pdf.yml +++ b/.github/workflows/draft-pdf.yml @@ -25,4 +25,10 @@ jobs: # This is the output path where Pandoc will write the compiled # PDF. Note, this should be the same directory as the input # paper.md - path: wetting_angle_kit_JOSS/paper.pdf \ No newline at end of file + path: wetting_angle_kit_JOSS/paper.pdf + - name: Commit PDF to repository + uses: EndBug/add-and-commit@v9 + with: + message: '(auto) Paper PDF Draft' + # This should be the path to the paper within your repo. + add: 'wetting_angle_kit_JOSS/paper.pdf' # 'paper/*.pdf' to commit all PDFs in the paper directory \ No newline at end of file From 75c9c4e500e855647cfd0dbb20c9e105988678f7 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Wed, 3 Jun 2026 11:59:47 +0000 Subject: [PATCH 28/31] (auto) Paper PDF Draft --- wetting_angle_kit_JOSS/paper.pdf | Bin 524369 -> 747472 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/wetting_angle_kit_JOSS/paper.pdf b/wetting_angle_kit_JOSS/paper.pdf index 3fcafb9099a5e03d89c5361e5c522a535f4e5c02..4beb1c891e5446238b429a744a91abe014e44024 100644 GIT binary patch delta 475214 zcmaI7V|1in@GcrnG{MBSlZkEHwmlQuZ!j~lZ6_1k#>BR5+qnJvpS#vMAMUyRp}JT1 zdf(oAcRf|lQ&m0fPdqhA4^SwHNzgMhu)|Y~&G*dxTbk-0fM+9SBDOdA0nf)r%&0)j zO3Wx>ZR29@1bVhHb}<(-H?=o2CuWp0x3hGyB4*)aVj>n0ApTzucxM+Ub7Nb0kClof z7&WwI!hy@Ng7_&^(#%{=IC)}!DSHT&f^Zor2&gzPlH8@QKsbCkH7RKhbuH~qU>P0p zPjU3G;YK|d(AsKqu8tUZw7fme8XMk+nU4>dkK?Ipo6k4nz{T6)hBGwS4^GKA%3sQ0 zVkRZzv{NvVic(^sPC^4nq)L!H_;z+~OK<*G{xnOQSmXahwutU5z6ph0r$y#rK6$Gg zUOTsd2}xty1BVu}1=wAET#;0$w_yJ9%o7Kp9`F}~9vumj@-SebgDGiLyQTuYs(ZIk zZ$1&;%p52qVh|C7Oco(e8G2w2T)(p!#|KV!zoYxf9H38XR6}`NMUbIqJpA@lO=Z+$ zN(_y<`PYm9hRSY>TP$Z9B^I_Gt`oU$>E~HV<4NoF2QV3TZgh8bYjiYP&d2=VW)UWo z9Kw=Sw#H1$&Ym1eEGVpQ)+U|Xm`{v^7SCCZD_0AMvFEPP{VbMI@e5F6evx^?r^ya3 zLeC38MKbJ{+|{zm5u!HBRrVoS;5riwz%HYeKg?gQMbGsQzo`|&4@mhvF)Fglg$gJ6 zojxOU0NA@xjzXs&i<6!z_hF_**|VqxOId&{GC41hEnK#1Yz*Uuf6`0Z5&tOs#C~FA zJNNr8-cqH1vs>(BCPGtcMZ_ulH^tJ9BY0HO_QO;7a}rBIwUQVy^Tb|_jmK7@{+Q8D z=WX*DkTqNor1aXQ?bfn^QG2BZdp?Klo~~h*2BxK@YMP2!a?u%cw9Xx0-VGlH6LNqG z_+&c2k5;7iunZaDv5l$?P2c-kH}G!4!H2D)DuWRYu$4vd9M9J>OBG~)85A(vb-h&b z%kbi*QZQs{p)XASm{TwV-4IJ)Fk2970a#Zs&ca+F&~Qd5TY)ab5U4T8NdfaY*a?C1 zfDzqa@cSGTwg5~A?E0YHEd(xPh9Ex&$h9v{Ob}TC%vaEILf^!GS$!jz2}B@P9l~|; zUz)-nB7*;&mjgwOBNNmggP;Rl@mnSr{x9?jBD=7l&|nVhl(-dGDX z6XZhBxRGuOqIVyO16%tyg&^lG3I~*S6rl1C-d2zUo)2^}DL9qiY{c|ZR5L+(qI*Nmd;C%U`MXLPh!X@@zruSQ9l4t^npyM^ z?F1_k7bDyL;Ppms3+!5Z=(n?PAQDAH4;F9VUjKWdeKUAde&c>ae1rW!3=lFx9RuJP zei4g6kOWmHC`l{{TTx_CB7FM;w<|`f9Bw6lNBjqkTa2bAr8(0h#v{xlkxRr!QiT*J z244!iKrAk${*RhCj}VV^tBjXi8gW^i{IK1g1531?6ay7!9PhB^5ZNAF81BD}9pTU- zGbIhxQX2XM=LGKrO&J*(37JZnSQ!AKP+ewTCOCnC8j+@>xKy>`Z|59&X+&#mYqVFI zSNcnsM1JaYyHzL~jV)P4s9Vy5|ARtm<(LYR#sN*0TBW+3YG>)2$nSEK(w&N0Df3X7 z#1WK4I@(F~;Zlpz&Qkn&jM9eEK{fl*sIuw#v-#e6u^;9?VCEP~`IV_k(@TKYvfH`v zxyQfR>O@6kE~c%FnX-nm_66Db(M+%i&Js*yLCPafS<{2qvr3C8g(zjpIm%^@BBB#w zDHdZ!MKRhFd}H`#ekr+}ecsT(z#klX*2)QwvXcDc!r{g5(>C+%xzEC1=WiBA=2pt* zD_sroifs!CN;0cDR9`b5oC$zGQEfjvwN{JtGR9X1{3 zUY7S+r>yrKcdU1G_lhUcxKoG+*sut+hzdA-;_OQHY9Si=>qjZMmBul-V-8Km+1tNj zTa+WFaq~6uUkA`l8y!m>3mucMQ$hozQ-)dME#i5L%v2k#TaP(r31@*OivtTsi_@`{ z@vM}eX)x)G%=R2zmcn&c^+PpQ)y;O&)n>XDIv3SvO@S2|#Q4bOqjKE%h}7_7QX7Dt%|LVH}W@TpJ$x?a*K3xKc7E8 zT&b$LtpDum83{O5I&}cnV-K*mnrCYIuJ^y6YQJgoTCO|g@&4wna6WN***n|$?r7&o z=*sLU?N;L4GHg*zwq$sqOv3Za3@Luls%2=63mQIkGO8 zKZ1{ou#JDud5U}3*=;=58`sN;508(?3&;Dq{lSa>q493wO5g~1?;V_w+;N^etf)%A zblRNt3o*0KeE)j?1OFPK;_kwL;-D*G65;e9JLuCd&Ozycg9oi+ z`T|Y7zi?Ru3n=%D|Em8buV+O-O2=!YINRWx_1y71-zMhc@{xUdhrJES2+1aei$#fT zh>gxW%TE%&A1ekX4f#I1-#DiQruV1aO~0F%G3(dR)=1aL*VymA?a=P*T{m8j?lMwY zQNg0}poWIlgnFY5Vm&b~wHlER)0A*oJLEq12L8svvSvhO;Hl9vXSTeKHS=-Oy9wBf z&TvqGwB)1UDr7K^I2~Od@Ey3_QNNLRCVYec*j0<33!4L|tO>1kXC!9%XUk@?zxsS( z4@|}p#<|C_XYFHsG*^J%{Z&C4N3HE5SI#hj?rwQ(iD8ClrfZ31IqJOT)OZjfEkf?? zoV)hBg+K-Rfn=VpF259*I>CD0`?XDxc7Mzr}51$M|Vb6{)+bU7%TCYW0-f zZJ!pN7*6qzbFrnW%rIJ)QERiQ$Fc7LF?FW2A7zIY3Zf|3~dR`($VpS8su;EqnLqq+YV}xUw!_(EubmME57f4Xm7G(y+?Ddqpg@99m* zKEUzdGx$+yUG=ar(~9b?=k$|3qzD+l^ zqA(cfCRxrc79{^<^5EKYSVVbo3Lj*sVQesT>s(tno%06nEQ_(^XmwmijI|PIQgpJlz7gfJ-_tRn9?ek>!n+v%$eOF|lYOnL;y%eSu8P~7o ztx)~qyCCx(=iIBqM_*gt+O}oa!N-eZ!4b;2^1Nx#pu8e}7CsRo_BqhHbv#seW(lB8 zh6Adv6&Jt%g%bgfFCFgR{cQFM_xAUQ%&Y1R&b|3hT^Bw2pMp5^IMM87H>^8WKeOID z4@K942hpC1LW#-*wf#=s@Nb?s3ap%1IOnIgN1M7sXu$lU;^TuV;^VWbz~>r?z*vZQ z2dStG(ZNEro~ed%$b{j|?aV-2HTn4w{42ome=^?xKeEft$<6-%MRw2mhoi{b93MZz z-U4JC;p~L+15PR#dRqB^f)PPw4#c>m)Q}jyy&^FPL#L)Tm^EZE3vJ7Hm6|!hK?l{u z0~C}2pYM~Kv%x7J@6n7bzOnK3P7A1%6#F)TX`P=w&xfODztD_niGn|v8~b!j9?;(Z zMJHfz4(2qkVIJT5o^9U(Gh? zH!;cHWEel^v_YZ!bSyLv!nrFi&yKjR1JpaW$bW&wlvYc{9+dnnn42q82+Fvdv8-uy5Wf_AnK$XSzFWH-r#=VN1vz?6r3Yg;eSiK(!jA zo=yBVYip7Tzv$Ee;n!c*=--3sb%fSn`Frp)Vv0l~!S#&!^CU{oqbf<3U%0Vzvc|;8 z7em2w|ISelG5=BQFbw&Jm$*q@FRD%WmpZzmZRKEXg(M9bryn+JLVXrK6MLx|uZ~}! zVB_d0DUK^8b%*ky=UM$MDCPHgDj=Rg7pL-9t6PxZI%uhy0kzEQ>q*0$i@9&qe(F$N zy@(CPlO?{pgw_E9iFZVdq0cCGeC?Bh2h{4MidhkFN5YC$lkcEiwJt$>>W6z+;)x)$ zE}2LOf+m*_fp(z8ui}73Ly~W?LBw7~kYgg-rE-GB8WxscP{Qv#b2!ynFae)zgS|76 zt;8LdpIFBMX};O0Kal2r37C@1hi=vo+8b`?y?ouO=@BeS?vmGJ`qULkiMT`EJYdwS zw4icyEe=ZMw`0PH&Hbi{+2TR}+A}hTKzLf5H{((Wo7<O`k@BkQ+yej>#@h3sm8?%&jG~D@~tq5JW;Nby@7@zwIE@ z4U)BH#e|<GFF4h{Av&Z*>gQ?i74E+U|#*8{_CpXa$ zp?=2&&c{mpnDj3B;`w*3G)a3~yj-m{mg|?RYr#!Q8oJSsNcf6Gl^1K~!c}+S_eaCy zPtE4#MpBWt({5}n4RIxEu6~#syosVjMCXt*q+_8|O50fa>{6ShgPRday`k9AaO9mm z8BXrMlM#o-wK}IAu$+6qIpy_3)!s4Jz;|_zn>#8+;6L-Z@4<6jrR&-1#$aOGGbe9+ z@&K>8xFHGA+C3Mp&r*6&^1aqc|7Vy<$5CGMASRLF@LJFA0OHN?TK3rc|CnUci~cS< z^NVlQes?<&xUU#RQFsTDVW`wEH>k37F*q*?!?mXo8Zv=iWq%^#eA|zCC z)KTpE_!i=ru?Q>=vic(MxvWDJY+;GU9+Ye&A5 ziMCQ2bM6P%_<)>SS^gk2VWu4w7 z-KuE7Y@EVzcjhS$D!S%$_hMin_&nd^7X|+_pI#ezy`Y^xH2XAfxs{A2D}k+b-;r4L zN32@B(9;#a#D#Tc)n@5=)h$NPA=3im<9-F=q;U~{9oQEq%vkfk%H*rbAg`4`v4fxf z%#>-?&5JEml7M|g7gH3z`yca_JCWs+hY8wkxx_3cn1o-7SNGgb`m}=-+sn-$a78z; zXOe@uVsS4J4wYL0Eiz6l584K^e7=LrQ7Xb~TJE~v(l9Q2lH!n4%vv@T=v3yANuWE) zPi${+F&w-J&ZSgVl^=DAqOn{~DlqCu8Z6K0F`>4x=k>^Wnn$N_uM^uPeKqMS7e6MO zr;X_+ze;(dpODMf>f!l2G>a*vnH-B`wbM#5AsgqZU|>I0%>J}@YcSbT$;wzQIy_6s zm%5uCeeZC<8|so#)Rxi@bV}HY6%e}PIKinHaJh0fY9yH3!E;2dvQo+9sQET_1}G=c zwyu}l={fCa+it$d0HHe!ssR$>^0^7vMe{!2PM4-w#!NyH)&FKKL`4aw@U(r^qP_+1 zJ2G#%T)Lq2xeq3GB5LqU9yj#*{!^7(6o;bo=TZb6-nL5d%nVBw;85PYDH{#h`EKV` zZ@f_Iv^j*bLy};nDK%6g!J=lPmo{pdOP|{ViSpElql3@u}+W-n%iO4ocqJex(8(PAS)# z=ugPnK06)O>Ek0h;F3-L;n>PH+r1YSo0UDBqvNMrh+aeB0T}U4aVx zrSDJwBmsxADAz~@+qw5Ku6>hxJG9d9y$&n4HiLixhd7r+Ab6n7-{GKzPx+5Dkzc2E z==6*`KcAaT<(5xvm^}imx#al6%%*?euH6eHXAmq#S$}34VCc_5Zu!|v34tRFqT7SmHyiw| z4s-6Aw)he0i3G|TYfsTp;nTvQ6Xo*FSNT&7`^hTA_!i44oR-)GCpbKDP$CZ<=lDkd zN?NuVpDTvB0c#@Xvj=q?LILzH`Cj#!ri7Px2bI?@sIL_&5$tz>7@ zh~|^p>?dW^`cJiS_z@Bnn1XB(=zSmCVn)_{2<2VtCnH1bz&6``N_OT}82EgG)rjZS zy57uwGo3D-M;`@!1y43ABnL*c5)6BFrb!Yh42J=7pjhaJ4k8KyC52BD1!mIlHvI;B zH;R^gQP$7puhE|~8+7>Wn%aa041UXE)gRL0!=CZFs>-kUqt3Hu5GT-2J>js>fh7UD z_>pt80!27`m~8tW+?wwSe$_}K{R7#<6*KvY<13OVBH7EMhxixnFK@z;gGn1YR`UzG zGn%FEK#;HsKHn-shDbV$M8^Hp%LCFEq8aeIP2=DP(FOt;h$bD4L1jxRoq-@-PO&Dl zEc?S8z;69;;r(-?^*0A{1eg7CD^fbnYn#ow-QNOP3ftdv>nBZKTHU=7VSc!Z+a8np zFKdNemkjTgtBKIoOsEMW;|4`jZ-yUX_8(}Uzy}+}NN<4YdUm`T(lN)z(yi&s?WnyR zwWXH;Qy_zJqU;ZzBM;r^?Th9g*nb6I>!P-s_{QwMU zz%%s8Et>WypYM_fLx#b}(ER7+^2H+4U*%EouWN9zFPQuFj@wS5vltVa)lH#v!3kIc zQTdzVv=c2q5C)MdKX!Xb4m^*ZZd@|5BacW2U3nyVHkFjTPF)<1r?#XxlXlm?8@q%51mtK~D+Y(e8+t@qt~3{$^@lk0iOZ>w1zo@~ zjFOWnejrrw;p3L2ERrl~_bJYS>p%P^pnlY0ILn&~WAry8kK_{8=+2NDmCD(+Q`X)3 z#XzauC)Cg%-)w>MoJKEH22bZWM7k2x1erpsGc(>W+}p)>qNe6_HgU}~V;Raf4Y*pt zm1Qxe7!Q9L>>d1m+O)xbctjZJ?0i7lYz)S!UtElNt$*-AIWU9VlX=+K?_H|f5q_b9 zh~nn$#=XS?UGHcg5hKboDK6&A*u3VmzLWP6~ zRG4%LX2cW!rz3K;W83=2J8@`_0nqt|+?1ovvH8v^>6;Hx5kHS-k$=9pQ4YRf)#!tfXl;4FPc-E#~QxwnwiMa*bnF71Ljc`+t!Q%PRBGCJzqToQJ865fp&hBS&s%s2z*bo?z|a~ zQnz$5Y^AK7>K|c`-D>%UXJz@iFj#4xS2Wcir`7n~RhxYwc`;0$9`P zSsCMM$~5S)rv6_ZLy64UwLq6gPGeo>sX{?Su&FSgjU^#Y)~lml`!dvZU_o=#79kZ} z&q1tyF#&CETKK2fYiYjYvN!~`B`HElYelV^Q$`F<{*DhrS7Bu8D){P3f%~-|Z2j1% zRrukwJ~E!-tuYg!w!uJ2`Mn37t*Q;=*NNLPcF_ix5HILaE$RFMEufJW?}jy;T3(Ll zaiWAKv7jAmh#T3&ofy!X6eD`jcnR@}!h?g7|504F-mR{^HB-JZqPtHn-S0lICueWY z1};JWs;@!4cb6+68kDzPUslJU=FRN;hsMxV)f>9VC4OCEuBqY_6*73kiFP68n6j>U za9O|O!b3Ic-d>}M7w`+IxjW5(9pvxTQfhJSv_6blPH$Bl%3ohuHWH5z>CE=0fwY1& zY!TyKTv|K+y&)q=o-rIj`&D^^rLXk(2mgA~G|z?VY9G9~aJH9Jg7_^iL}g9s6F!EB z3UsPmNnWQz4FuMzV&Sj8e7AVnuJ%y9HwCAf6*=O)GhJvhMZlFcsieH-%-1)Z(h0n# zh_8)u${W3fv{C5K7ty288XkuUn?Y_;uC8Mlp@T2wW2)z|oOix!rc4P*Q>Zn@^@vL2 zj-;V4DHfV}U8V;;EG}eY8Nkl@yyykNbl8CEIK{%kBT_-+AWOIPChqL? z4a$AIf(0!qX_sf5TV(^6x*PhJulv3w56*&mX<5|s*GAFyWY!@W7jsFMuzs0~C;0^x z&DGjnVd2wP!q6VLuu5H_F+J+%JI>YbTFjh2Ehp&bzm>BL5^m=0@YHS)(3X^gB zJ(LCJ5yQv-eu;dR5i7l0W0;JdbDQw*j728mYH%CGY~$mG81z*ORdL-I@M@HDBp#e> zVb^Z*jsiyiI?3*APp)kBA5}~R+r%PYkYs7Qr6NhkM%dj4#96@%7WMP#8R{J93%6w5 zz~`;T`7g5P2q6>|?Ul2I8*81d)E~=>@%`xm3`7?ON|);84>|q1TGa1EayUE-oXRsRlf&1rVR5%NQ1T+$GRa7ij`Mf=43myc9;`oCS&f~f zkG91oLaB(pofcs1MQu-);pv81JzFTXE zfFUyQz6U?sIw+QY?osbdCVjz>L*Kq|42woL-bWZ$+SYL0(r|5_E=5`{ec&+k_Xn)V zmkE?BXZ|U6o9xHZmDqR?EJF)cI3LshW_;9b*|5y6=z9@looG66FuVAip)!@AUFXx9 z{Y`7YL(4s#VZ#ms>pOMPriGu(CVsS^B8$xRk4o;c!F-~(qkoRkkF>~}FI)Bl!kDAX z*uDZY&~g_G5o5lSP#8~K$%aO9;~LOL-dCSxcUe{rizi>4&F$oO>)Mh+!k!Iob4L8f zZS{5Y&*C(Jqazu#ThrM0ImzD)cj0&?C}7%=Qajs zYMH%(I=cdLE)^k%jBQU-{g_H#dJ|?sD-~^CNMS*i%3EOIHh))fifp82B+Uqo!xzys zr)lt8rd7xGVCn+HX`jFk6bvOUHvjb$irAp|M9~sLPHV(_Yz?pqx6yN4B>LC-JJ*Z# z_fXpUmldPDg9Q(#&k%u*{0ZPX-hyGVH*eIz?%%c-?K&55&yH){l@ls7$B&y!NG%4< z%A4D)uU+$HF%M4vPa2YDW{@srAeSo~w#oEr8qE@gZ9T73YXsz`A-P#njKjK8)M9;1 zK)u`7U}Md(dR>pC5@KU+I^Ms$;R&w`vj#jkN$k_18+2`j(VS%s*pI;aM&SHf1GQUwnx{%H7yx_B;EB^M(eVqy_4p*JW)PB-|Fn1gR)&i=NY`OccJW?PAyaWl+1Iu8c^E%^gH*z53}LHA~D($F_#MlbQz z7FF$Y9O9Zw?yaOv#@!g67^|@3nwk^{*P+g;Y9i}d*QEYsup4$8b8smZ%xN3ugpOs* znmUmSAEHCk--eDM(+`Li_Q8hz`Pv%cg>M=qJ~5_LZsbzEbr;quuZvT8-?a zBJdrja6<|xR^2V*?1cIKfPf&1MbglZz%Eh%CCKJ4! zdy<+ZHNZh+AFAmRc9C|=Rz8NOUm~}pOh4RU&sDE$3^xRoi0>cJjiivwCjXpe?_SQy zH?w|f`M`S8Ip=v*G&H_i=yE4o^5tUUOzSgDl;i~YVInmki}N49)8!}ReTPQ!^)mz- zz`^`~jDe_mI)Fw#v=mK#n47u~Gpf3pxctvM(zeEypbs>y%|N3d9PCWQj8f*-mR2sr zENtAwj3U-9&PwJ^qV~29_IBoWF2r2KjH327_D-q}#-^al#LV5SP0dv#ML>6yH+KF> z%*hG*PL=-|I062z6DJ%T?9BhW3je<+PR=r6Jka+y9=aYg9`W&ll~=^M^{E!yUfZI-H<~2;u6qO)si#} zT%4Va-6Bk00)qFS?}9Q3)XI5cp{Nx9_ZNl9`kB1$Zca`=Haqzf(f`*4Gn-`!nLI5m zEz`#?49RrgV70zuMoKulxm_&PkR@_4R+Mw_zYxewx_)qU2~50#E_xZgdpWJ@1HQ9j zBOnEn)k;&N2TpkW6L4FyX$%gAS&}cD57%UIKbqQZSTuNj?D~A~`h4yBd^;30^u3=y z@I3-P4uPS}q)k#m+Yi_m!}Ke0r%%Jrmqog&_M3xrd(x$R)Mxu-h2o#Pb1_P|f&54g(GC zwp89MP%vN5?s)}2mn8Yqb{6=!C88yx=D!-Yo-f5nM2aS}x3~YMovSni>^n{25TqQvD%e-vS2#5Qi3JW=L4>ZD1fY~G{>`=Iri2@F@Lc-l%0ccr3S zgRWar(xSCVrT#;qi3oI6gjr0}CAz%rBMW=H>PB{(zHkc>AnIv1iDb zbMl{aeMQ)Z`RJ0szZn6!8DA!9>_0}gqx){D2>defccS8>HB0Ppa)8kn!TtMLXx66{)MZeBA z^GgOkY=W_xkB{#HhR%mAdT?}P1XsYvWBszEu~8#`I^^HZ>p5%k==s#PD{X9ctci(< zl2C36kQcXOtnmWEnD>S~RFjanVRZFONdI5Ezn}$?H~g3{MAP3WDx;!at5v{zy=wOn zAgZUQruJw5(C6V~V}ttgWt;B5?FzMddNtgCb9XpYvi@+n2SK}QEHMKJnFgV8%0-5G7-Z~mhJ0RhU& z%G1-+UEvm6ZPnFGTwK_LeZuit_T2Gskq5JhCGJ~a+;7-j?qXbAuo@i;Al9ic%!@w6 zz(0JL(=yopjI<46X)nq%r{&!`7r5!S<1bOVn}>ye-;ht;+1;ERT^iJ1?Ta-Bz|wqu zeTSF1hJVu|Kttx>o1$T_R~ZUw?MhCybaoOBN{WeXJuNLQ#l(F3h7&PBH!*C+;>lOZ zsVerB`lj*jp6$Ucmcj$ayJZA51B!$|e_2g`fC#jbtO6(x1|8Z84LWtY8}f*vqJYAq zx3gcTs|7QaJ;=_I+5NF${UN*&7l*3U z{2|=?dVPKE?{R1UTU8v8kO+MG)VEWizS@VyXpvR--CRUovmtC(oOl1i>DO$3MC|2I zqNv0iBaeJ^$e?&K!X{u57I0A2@qjCWMUH|RCdW*!zgVe1tf}-Wp4NipE;(-@=m}?Z zIG?39ct(Zwk**XfH2pk0-|6d(I>NxfVD~sYKc84o*Ve}S0c8h(9ZB8^+n#p2po)8) zcrgV+x0cOA1-d9|&9DDc24YxTJY$AKgKnLSO0McLJY1f}sK7 zje+4Jz-1sE5(de}I^z<&UZHr`foo3@w)wlAP*nY*$nDmwJxvvjW`q!PO*2y-R5$>0pTSD30q!N+W z-;7O$p8FWIFJ3nyS-2MB|Lv(#zs=&N6-2zyq|IP2<6S5A^oWcyqN9=5Owu`Q!AdOr z-iza%En63)u`kSz&dtxxD`lKji&{%sW$O-~5V;Xx0i8ei3y4clq^5%^zEaEX4bf~$ zL!XSC!?#W4Q=OXv$kXI&vVb``y*z!XKJnz$#aa=Oa+rlr44|I^&eQ}F+WiYN_r6mLz6 z8Genn0c>ncaF9%2$xxEb)TPeB24roX*p*KC;f&apj_+9Sv&azA5>WF9hJtBt<;x~B zkShs&Gg_IQogEzwS12|u#u@QQToU^TB>?4Tq=uib*&GA}`UN!`Nu7kPglt_9UPjEk zts1FaBKp-bv2ALpZEA7cpefBgLvQ>6ZPmM6z;GO8G=^-ZUV1z!iEYBGl69_8j~Rf>7UPr&ytP|5&!ETTJ&y6!u8kUI*4H z?P@fM2Uzp-u<20`q<-SMk0_l_A1(j=GLp?L;7~=t%SI*ZoE9?-0X`dI=5LJ~`KQka zD2pvHVzbU3F0~h9pIjGxy#zkfY3sAYKAtCmmGi$^rMb^E(A;XIAXK7)qZPCSbz}V& z5w1)rpSG&kq1OuK1eZr0e}ui1?k8un!vsT@(GQm!X953KYE^1K3<9aq+# zd`|QGX`AH93RW>@$S{#sa{QT__lOEuSAB8GQXvNoy$(wp2G1+R_ZHtCPJTWM=y!NE zfFc8O87dWB({S$WCl*{ac6pA7de7@gFZ;kvCdjGjK%-R~=pS3v3l{xix$;JwLX0}*`PD5ZKQl^DM>HzrscW*BB>A{A5OESUbP_fnNn+!)UD4-W}2Cuk~M6l_n7-4 z{vgM&s<|Y>M;ML2^2YQ#I2g$0b{=>k?h)$~5Dc7<$ge3>#JXw0H&-&+J#1D0(^>dQ zLRTu0#;=(mAfLuBx#BDk(bZWmomHo$UOl2!^OcA>5C&puqezcYH4Bn)DBOPQ8cHSf zFGW7e0Pz4=NUYssF}cBTBV_x>G13*#@fZHQ_!Za`fZ@N=ZwpX%&=OFOuP!iU}6-Jxhy zIz)N)ozKt!tlzKusECMPoS%{Emak4Cvh@^r!Um$&XbdbaXB5u&!sZ)JOMrUM5B1H& znJi~lNY35P0{%ldRVwh!5T5q&mPW#LWriDaPWeA&YzyJ^ySL3MaMR3V3*U1*#r{Gv z#6PtBTa$Z@+^OLYQgY%j|j805d-$<%2M%ez$tm?!*yi{wu6sqj?%IRcYFxx8!&{+ zzN)A(Ne0$ShO7nYUcXx~`-+b%Xplo>ii(I3P9H@mA`=PN_%_qh(TR$R5-GPtuD&7W zJmV(;JUkwE|3X3++20qTqu*G}?!JKGF5)bC;8t$xr;CG}jw%|XNFJp~{!7Vzi)^*y zuB6p#RbS=|q^zD9TxA$Kmbp7j(Z;`cB%xE670JQ^g~QX+@Q@HWPu@#HiQMP^vX<|? zI*4m%s&zqxO(>E&!D484$rR5LS!b~0HUM#S3Eo4ydg$v%!jK&Z9lj7@{i&=yCc-D= zjspnR(cDICjMfMb&(fyk{2R(xl}LX-**u=Kl_pvCVoHrL+F6NV17V_{NKH-@Ywhwq zY$Rlu1lmX_O@nn~gu<9L(8etCW)5{El{0f>i=vQe|8%`7rt~hK7wAOl zF+8H^@>@vN%5!` z*+t#5btoSHDHkcwfe}z8QqUvgV{t$LHBdF{@`GudlSgU~opy26eKp}#2Eu2GQfMEc zK%Qx|BrU}4GWKtZc~h$t**Z)BYB3OZ0u%_Vzc_8LKlk{{QpP_YF&GOA3*&Lx`LJc$ zZ+2#;r+a^zWsSQaef^q0d%TyB<<4tYCpL{?vZ}-=|LNK+1vqd+<^I#n9`$AvaY0dX zr>)+u%h|SsJQ3(w_e^&RFk%!%8n22W)Dl6)?S{qa4=caEz8r)l*sC0Ni0f~rRl)zQ zOej|(N88esgm+x3M2(FAZJn*Jr+4OTg`qyg7f#+#R*G6mCxCIKRLjZJ^kY8E<`F7q zczbNiR90ve79e%Lwcv&r9MJ0?lAq7SVTNjq#hl8`^AOL*6X-goDAFi%1aCHMkLz1La{wJjZ~IkA*iEp=Z5{Q}|C{B3 z|LP8dT%40`UsSqVfDM zA`5gw2zmEbN&u&`IRw|&4DzL<%t_8@4)T7f@N_}qa(mx6HIsZaRl!W}mIm<{by8VVpzW^a+RGRrVPE`39_qNI3dAn@O4YrZfvm+!R@yP<3Ca=fwn^z6v%=pFMm0;-;aMJf5S;)n{1kVV z<>41wvtRo6=y_@B+0($YV8;UvhCe^!%c0<>HmD#~2q}IjSn+Va8lV3+%`z7~{2v%5 zvv`WmFiJ@Vl(Ajf_v#|FzAaLDBdFva+L<8eUMdj#BKVEM_9>#FhRExM&1lUQQ3uHg zWv86lh3^j{{M2%pX4ISD8Njw`M!#AO@84^=y#%6b8oByT8$3yz@91w$mu!BEW-MQp zq5QTRD0GK?u{Us7E%=Z!tMuD22?_nWUD`W^SsM&D5>nij5DmQi{He zAZ=&UnY$16jla5I9au(OGZsh2FmI=yNTZPfRzYf|jRTD#J)1?raN*bEy}53pucp7@ z+0n{Y(}cJ}D~iB}?$c2UP80n+41Y)awX;K&nG~2F^a_;M&Sa7vba|u%nLwcX$33Xf zlRN=~ObC@}+&6B)>NXS~Giur0v}3b}+*2A9SAE_u#ZZI{tX5$i;R|f*_9KHZ=R$!8 z0=LcVZ~8~b%%4%$@q>TGk zDq~X=$6|J^&mx1?=d20;Y#4-LBPn(p*IlG0;|bvQ#J#)R6rAiEJUQVBLx3DBU*#QT z^g%iA=ndcsR5h(z?)vu&YRkdwN5|Ngi5)I1PnK~Nu$5ZUQelH7Q40@@Hx@?g1oEGl zpr#)+bkR0Ws4EF;QIIOrV%7jYh=2Qh*-!=X<0;!fF4NXroAlDlS7XyoI0DxmwH98x zO7$$Bk-N_#`62`NE8mVJuH^=-C9&hpD~-WVam^DYcD0Z zsWaP?zDT0&l1U3F#HmD^ZJ-qGwk%?W@pD>8@#6B*wzJOYsrwTbEFjK5cT%- zbmYQk2q%%T2}yg9otw)hquNx%s^rilB40@Ut}_`{*-&xHj1&va>-9Hz9O)K7l{7Z} zqaLsl)eEaB9%ipaq1^XPeK+@X`nNgw;IeX`)yMl*iDN+-Y0KBuP5HzF@~G=PXY!p* zuob_TYn6K^M!s()N#T~71*-Q7Aj+d>-+m3fCADCKW_19U=^0>mg0Pj3COQdAe@GP_ zWZvj1d!aa?1BXzP|6^I1y)zfUAXd{faKK8+Zlgpym|jgdERs9#XY)*T_I-POhQjPq zH1QC%7D_FcgPn`;0*B*I!B(b_%ly@bS)_pOk`6{X z`vwnUtjFO0HV*gh}f5tO*(9KHPlM6`t)dHeXBn|+YYrQ=zk)NoM2lxF4QJ zDYBrfSb1Znd?44#6E$fL2PJ}SDRfIHC=2fb9UgzFYGwxPEmY_dF)y_=H**OHd}P@N zOqfwTzjyt1@0N73Q#3TBZ0{;Rl z1;wR2Lp?Kg=2+_y561=GwO_FjQ^FvEprKX~4a5?A;?w2f9)oEGO?zY~GohBiY@SyR zU$Na4T?2Edv0z<32l6i*V8NA}F_E=`#Ei39gOcqEVO@r#}J0~Z!$)ihpxyh8{ zqKmWA_Q}{d@8ZCn<#&IEm~(vzY_wff+2;WL z8x(xWSwBnFT6X%mD zr1zKlkL}4Y^PVwK!$X;%@G>pcl`}s!qsO z`NhRTtFZdjOyWw&@x>*NfWwE$hAii-8~L7i!HM>2%Dmw=q1T8O(@S& ztqCry-TbbaRBolaP3%jDe%qekzEQL3OsxoqjgHvrt@vrwHfhBw{mX!+;YQrUrd_s5jcH&4#97?d?4&^^0)={ z5W89ZO{=4KujzdrvpM)@zkdK~{|`s!;FssyhVfd{ylt#NA3(13m(yVvJK(PVqL_1Qn>ai? zJeXGlx|b2{vrDxW6ciLBa5t36wo9mg>@~5-1jh%XXM^(&dTLw`90|uHmU*o%2vevb1(c;;Aj8dZ~U*9&zSugomc;I zc@Y5n#O-{IsO1Q|x@+RzQSP^RnV`b7xgCa}wdGoizWi_$N)}U z>PdCxKq~h2)IIJ4KoZ#T#^!N1eBaJRVrfP({SbwfsO^hjL z5~WtX_=E0(UmQ(P9fpXb>5MMs0_?)LX*{8e_KiFS2{%>H>wXck!(@76$jOXm?pppy zdLrnL#u_BYK5=i4DfSeA@^#|F7i4q4v5_Eik@{faM$qk~Y^XZ_0lLz4l8tQt&`=y1 zj8aHojHb}ujf9pe4aCtu$!Ys6D>qt=HzJ(}e{^whcpmn~BvT)DJuxb90kRC=9i0^^ z{picCFujM=AU@=&#kPO_$B%shp4uU-dEICZ7~h+Ur-B|kzrs9zjC}}D0hNhoaFUgN zu7Ujwn{!ZbUbEZ>myYr=dbYN$%+pQU&=X-rm(2%0IC3pB!fb5|c6>)(qUb@&KE}n5 zKi7pI5_77*X9tIcS>+wQ0;gk(i;MNvt4;aF&wi+a5Vhy!s5Ik#_YBw}i=@!jyG0>ef6qD+IRw3scP(t_5Ebm;iUQ(IG1BJWbFl1IJ%_k2F>Qn&pBgD~U*} zAC6HJY1?S*dcwpb`MM>PN4SyMi52Azxx5GO6Wr&kxX^4cr%+PliYrZFYms=;t{1A zdW8BNyoCnTKlO!|ImPvWk9TOS2c>{9RMpT*Zc*D8Q*UUL7x@Mud0aI+^xlO&xmXOZ z1mxBZos$9gv3;NiH4rxD_7?;R!>~Hs*2bSJZA1uaP=K&}!1tCicp=BE>1xGDH_Fhep{pcKY#uNx|vMiwcl2_3R9wA zy7FHwRdnlD+EN~j?EGEw__FLVWJkeR^$|>`WHqzpbOE$Y&+B-pDq}nSV=KiNB#b5L zN>EIwx>Y3?@%@Yf6V5rHME)OdFq$U9@#x>OU79X&ImJDOZL07Y%+l$2t8VaWoc2FO zz2TGE`c?l%kVkbmk?GeOyKH!yr=^9y==v(oVkP9FXY+B1^X|r6wDbUQP%-ntYt$Sk!F_&A9qETq{Deq3fA@FxGUs zQE}46)ja%~tuS6&*!ThOqvwV#9oUES>x4)E1VhInBO}jS@7G=(+uW{=RaE9)Tf36v z(6O+f-`_f{mst}}tn00+?3YzbfBwz+0jq6Gleg%L)Ag6I3#s^K!uUgm1ja|2-#d0+ z37j`0(GuaB<@Chng4m47TkI5F?q}}x160wp_Ig;fzlom_;0#NLSEWD;S7&m#YDLt9r*>tFo{onvBzKKvWv51NA-FAeK_eM}rQ&W>iQkq5@B`4>d-WuKv zwW_Ce8z&Rfd0AVMthAJ>HuGqEdVC=&OGr=|ENKYD_Y@w*7)~suTv!wz^*x|B;-BFd zCAB>~y$C{c(NC+et6OmX5?7#{f(6JAk&I-Xrl<{it<}pPxsmSF!Q4rsMP&M2RC3PJ zsU6i)YYd@@S%!%%H9h__(M8KEWA%Qv(zDKEz`E)IBjv3(jP57oF3 z_VB&+bTmzvbLt=IjAQPwcR=C+dK|yXq=}vGwwc$Viq#~q*&U&?r2q?$nl#bml=*+X zlB9@;Fz z3gIdw9{OSRpicnYx)b`JwiKtdaRU+YyL)flLn|Tp=cD5CCCTkx2VRT*ZgQ57x_4}n zg1P%KV`oE{a$cOwnT(nYKhlkHYefH_4924Ie=>zDc&E%b%V~2AXfTbVO!$RVEf;uEB!svM_7>r?YfxDbVRorCD8djsVY+ybG5CnLv$Q zbM@tdb00%_C2C81t>SbXx7z|Mtrc{%;RH|5rlm8HTEp7E}vA#Kkq=it=sWPv`3pA@L&oM#|9Pe zHvQpcIOw^&S48w4PILHM{0jib+`OXKxFgZ6&_obzNcYJa6HIE~>$J7EuZp&RZcH2R z934GP6RBOOOt()?DmnSTWTAL=RqRX50im9~lw@&>dJU}=6N=P@lC)2~xW z5btM>UX{Oc>n;rbe|BA;$S(RFYdcio(KRy&#$(`F5>1(hAqo;G(Co}wNF83+NNWs4?9MahP7)Fl znwlxS`ggatrzb;}`?)vlH&vzg3bNNsJ-y9zKmV*!|1OFuQSbOdQ--z~_=maO!c8(e zckx>IB3uw(PE>9Kmqi-K8Gs-SvFU_^+6w(AvUOAMze-d53=}Xt!WClP%q?d0jlWvR zNqCHOCeHo|UMs4Tf!od-^g+ItH>q*RX;&&c>Q%$7Z1rfb(3yvO}{00{=1Fo4{t#E-lj)6 z*3A%tzl)FjZVSc>wsAd80~nhh`B;#&+{kV+d@IJkJy+335^-RL?)A)dqPr*CSE7RL zQZOnejS(?n*76rUXQ zS?b>W_^@zjAo*Mc^{CAl`GZamaf67gre8Goj~TDOMH^nrrk9l<_VF#GnjCLZmd^!V zeRhKuj)oeUV$VYo-AI}%=6nKm2_}zV*yX>hv*0om08Z_|0NMJ8-TDR2@K4sbhXkOv zgy}gfI$w==TyE-9hr!z}p=EJF7!@70I8L=FdDSM-xYFcgVGe^1-aSeYAjQhadAx5l zR69e+hANoButqwpp2M(6JC*?X3{wpQ3oB$=v|>TKoOjqhXGmZSebaFR$$OS0-C*pK zhcOMzMCL*MXbBGPdUDZI*A0tx8y0Z`H71Bt}*_(1=060|cj%q5jZG(V&XZ>x>A^if^|*4=C%u)O1oBggZfmJ;%{s2T}$&-T<0V8@3klL zj7>J?uZi{2lh0bZK_|p8&bY%aK9gfn4e#7lOHIKYroxMom|`Q#g~yv_C&=Q3Fsv7#)4%+~)FnKV)aklSl^dH1`w~sP=LB6Mj+vIKT}w@w-2cxeS;9R6_O_wVe~do_=yp4<)kOqtD52(&hMKPMZ;0PWpB@gnR^ASY7Z3pxhl{ zce>-4^n>j}0ZE+b+mwDOd}Ncbn;GO4%aRqMo4Nf=1E#{~Qn9DxV$>yjllC!ej>lQT z=-Xc#RznWTd5(A9K%HXDf@o^KRv5qgr6J9*vmzue)!(uAK;xz{(Cx)gLt^cMF#kIr z%$hg+Oz*qOhP+0?`lLTlP!4I4!DHIQsN~?6+t^ny-ha(F4Y(o#ihe8m;FRRDTW|_zGyIB> zQEl-pfS~!V%K;Jw{52ovKsD|4dsuT8kMYV{dk>_#($bO~QO!aL*0(yNp0BVR>*S5# zW}f{CUcO4dk$L-)kyqx}c)vwbh=&skIG^2-?Uo(q?%QgS+=7HHp$Wh;tDl>q+McZ->#W%z=_*~ z;p@pJxm*wO-zGWb-SQbsZ{6yqV%@ml11g8SOv`3+m(N0O{Ab}Z_t>`_tkTlwxQFEO=e6b{grT zR$~f#F4Da%u-v2=I0D0BJvvAmx+*T`e~WvIx$b-amxe-)g3uP!)*r7ijl8q^zMo#7 z0PN+T4n5_2w4UkBynYGn%Q6dt^M6!H-rvA8QDsN|moOSlo@`|EN1aH+ak8es#A`7B z1(}*39%B?->SG`5_K;M?5KR63(4ZD{SB2?e(B?NXhksMKsOE#lW@=C6Sk*Tgb#zWx z1!)2ob&sSnHogps?uAM5$~sZ}7C32GWXwf*0hA}FLmK)nnU1$b#Pr~ zbz3kCl5Su&6!ZASHu7Q;RH0RA*Lsgeiw;JM*Pfz7rg3deQw-o(z5HVo1Nf)eLvJ31 zs~gksRn82@dLUWrfT%@zEK2PYf5oNr#zl3|?|)s~Rv?E#wlJuH&HW-haDdohH*qoE z-KX7$Ex3uVPXPLZt3IP3BnpBgH@O;-rXYUExRrpD96IwPIF26zcrYpw70@a;T9*ym zD>_55g2DbHV)bzNlT;4^&x!4ei0zx36F@>(q-nn3*AlM9V_KN;>*&H zG8W+g!|wS5X~z6v1X$>YXe~4+89U^%+?&B7} zPtS=&E}KiICNooMGR_jiiRNH)mtat`WKgtZK?|;9N((FgE!BJeduFfmdDGC&?P-c3 ziuiov39wRzJO_Ck_yWFNp_82Te-sT8-_@Xnj0oEbNE)zQMx_W3t;YO^&m`vS9pY=Q z|200rE%C8|!M~tyFKG6I_k*9fmzYn3ZJ5sKWsu~$0V2M4ToCXd)w+VTO}Jo-FJw|C z2nU?f$$|4L<)AyP&c_vX1rDu+H|wIaQ$Ukv{ER2(;6?vHW%_{Q>W>ATBWyDK&#z)9qrLG6f;)FW3p_lmidOOo z8kDCD7~7W(-QMVKeW;Z$Rb;OWMV{yt#;HOVCPNbp$NV zxUVlncAbb#wOQmm7E`$ut6gSu1D~T8gSQhe4X7Hw#P(`nzH)sIslQ^|vu~D*-YeUn zmXW{HvMAxTEaBHJ6D(m^`$+mUOXzPGph*1{k-Y2ybI^!>-HO2486jAge!Xx&qv)hL zD#4e{iJZ?pAz$$MEGseKjy)`!Z#84Svhf=-lsp&#APZNMTg<%9p`Si?148vPgd&zc z{~$Q=AW^+#wJL_@Fl@N=v9tzZKBzx^N=CC8*TT zs<&MpCLeXd!U=CIPK>ra@B6V%IiHk1;e%dBV5L5nDeZZ!{Pa7N+gKNPO9h0rB5h-{ zTsHqx&@l^!FUJOFyQGri5x0@ToWkhNOh`m6h< zMJUD~w~D%EmwcBWR@dV8ODB;QGSAx8wLVsH1yAKyWd|T+e~ASODLxn=m{U6tpmwXSkj<7h{*LwCMH*Ko1Lb z9cRTR2P1z&(6)v0`R@@Pst5~R1hdI^!{5Lfx7|zKNiqmd;{rBT6rk82Sil6SF&B10y zy&w$@GSnFwG#Qg_0Z3+kb^a#vDk)?e(sTheH3Kr}iPgldm@$HY7f#;>|Z~CpT*K1nK za)7(}d74&Fx{K5EoScXAd5h=aRwL6`n$SJaYh#SWXMYf_Nb>%$J?aO0SMT|*nqqz* zex07K-RyFomAqbFwWjU-yf3!y#kb$3T0NG^pwr6fxWU}sN$_e>Xa3AId!tcEP|+#h zCc0!HDBW6gUepmV{q6N`!eDCz4^B+jB&Q;pd8)r%hbG|dHarapTc`9VS(p-Ct{ORT zy2EhKd=qClzD*q#yU(DCT@vipkI%P^&oz(Fdsbw2@z%hjvf5E9QJjIc4;7fMyi-1b z>19|bXjvvmDxGSHTZ0IYZ_{&#>(xYN%60|a5x!v{N3l&!~K(-Enm;8vXI9qL&bej2c8^=Ke}Ty&gz$yIJV<&6p`|PD=M`hoA*( z^7nx6%LacO=b9?o+2}O$H<$q7Z84KcTubbb-nK7{f!lj$lXEf$MkirmxYtr6l2Z_t z>gFiX7T6TbqgywZdKr|mn2;u4oRwbC7+Zq7W_ z>20NqA`obFvRJOOYJYqBGB}+MyfqdU993bT61A9-b^#@8|`h5P{!6)8K0e4%1fle|+(?Dw9@HmC5*zVkiIEzw@&y z#|NU~<}T?8_LnMCF{@_4aH*LTAd_~l^%82MPo2)9T5`>5_}M=GcRu88y6+@7e>hO8 zSF1&G5HHc2NUkJnc{Ou4*)P1uf0JN>iIx$as{Do&nMQUi?rtx=`bQ6Xs^JHT4C`{r z7%SzJQK##=c)|T9otd#+f-81!m)cc6YHxX)pHVlnXNFlglPO5|_~u6?etlJb7DTWWMLKq1jK z+TMOvIp)J*DqE^U&Vc6aoC6Cty09IqU?mR3Jd#GwBfA63G}|2CLaJf!POZ%T>eWP$ z@VRdmGu@vo-CoNHy`{Im?an!;%$dL5M9Vp!EKJ&;h5`}Jr%4XCbl&c}smr&^&U0si zw|5-Jp*E$t$0&WW&vaIawa-5-o)9dqUd~1Fo&6&Oe?M0t!|3RjtYkA;MS(-{oTF^= zkOY9jVwBko{*e`)DAV)((PyV*!sHI>-`W7EI{)4#UM{BxMe%avbtyQ#Ca; zTUuN3kOJcCQ6u`XF5aaKHw~mH(L1keHH#z{TJmPRy2RH6+pJc5LGW)PaZmN2NFwfD zn{dIE8q~`z=6?l46R*UkQjW=x`F!_k?%{a*0aLE zWCH8=7Y8<8R1#jtr)jx*%a?;=2OUq3-K+Th@$|>7l|eS06K=aQ-g>QOtxe{wM~=&SWU1Pum#KZma%IW(A>?rVSH5`3**#rk^y9;p zV9zftK$v`Mn4c_yV&gi2J}xl6o+%nQ1f9N(jf)+*aIg@@v6h&{hUKS_b%S~M0+xt* z`S_+y*{P$0^>yfC>8$jt)le_(BDPo?Ey;WID_t}A;)ko?&o|@*(*dE9%R>;R=L<0q zp=EMlti=5`?zVR%3h9(TG8-6{OOnIziuDb}Y$cib7#)I$Yy0Qp5>dw`D^TpzINS6P zNOF{%6bCkyO_u9O+TY*ib-W>k<!4k2n9UN`ntBEliP=vZ3Tes+ZUO2EXzkmyQ94rGXf9#H7?t67pmoU2D7GLRkv3i zdBd-BX@e;(1?MA)*W)Q}fX95|7Sf%u2=e;YW@?(J4kSsn5Ew7^c=HW`6m4>Tf%nK>&q8wuRRddv!;X}-y-fp!O z^dZWiO26xe$43x=j}DOso9Cq}As8QEDMKWC{&hVf!$6f~Ldgt=mIU~z>@mz^m#X5F z!Bg&?X%lgs76Uc8b7$vhvE3q0%Zlr-x&0-j!{;l%^s=wJH0rTe8d&tt*V4KJnSM21 z`Mo~J6LDXRovmJj%OGxJhr79~j@#Yk-j1Sc)730w)hxm_0#mE*w493OoBQeR)+N0> zEEd;vhVOpC>Mfhd0465uv-!5tdnKc@f!4Ex)%pZal|ZfI^Q`x9va!y)_rseXVwvV+ z$6Q;>*1XPovy;UkQ@b>;o&Hdk*!4csg7d~kk^n6B2{$w76D-{>6WpicyIpyn6mZ0K zYDDjOA~JU0Zh!YNvzj~t?|;a!^v_T9Ae2O^Qu-sgFBJbrbl~%&`ahg+A^h=D-?=5e z=r>A70&t5W&O2aYKr*;OJ~?D_n^hHAF&=IsZ+AEjCDA7jD>Lb9{7G0@S4sl>HE9Y~|n{g;3$pHcMD zqYCPlEeP9=&bcSot>Ck61vNUpU1&A$r3mnOy}y|ea4{}b+e~f?X`gA z_-PH^GUB`^=T3stfP%%_b2?_J5Eo`YNj-L{kVcjH@CpGJ&U&Tn^V3j6x42*hhi!RP zt1O?V>vfvI9G}bG+3NH4YTp50y?2}Q}%B*CW1WU0JB)XVn$XnPwJcjrI3p1>G z$?G)n)s!r&tcpq3#XQeG#yLD05q=uq_3wB1$UpL?z6HAXZNb-ogcF1X;u1sZXB@fU zDYo3|{Ct{(=ZdRD_A%Ni-{1z&f?rrjNJuE4)t}A3xVQlO)aV#KA3O=w0c3BX7?Mn5 z?QZxa>AZ)B2A>RF8a-Twj36QnSY16utTOC8nQ+2)vy++X$!$~`8NMh^3!Rq(BZ22T zKDWJ*c`zh#IbAw!7G42+PX1q1jk!3i8XQcrXPXAiUoQel=u~Woyw2_(%1e;@%Wkw= z5X%}}yBkAYoK*-bR8H!Fty;%6J zpu~K7%)j26slh1q9ixtqgMh2_VHdmQIx3kfpPhrKy%}Z~Hhh<9X~nB=ZDEjxf+j^}ul(OG{fF$=K(7%g!d;+n_>O(AGMCz(L)u<> zs9$c}SQ={Ahban^?9-#MsVVIh;{LZgvQJ+H$wezs-vpl^JH#)xBi1RyC{FW7!!mwZ5#rS@i;G1nSlGw+W4P-s5dl5S6n} zXXSIf?dW_4vYxE(SG&-vWh1LCk5yLkScN*?k4D#fqxH$ne_wB=L2i^uvq8AT$?0e& zW;_VRa=3I(OlSr7{*uP%_zr>mnHF*G0hLd3Oy)=RPBZN^_Xl`#vJ5cUMc%UMMXp22hz2ILKKV2ML$_^b8&*lxI?=) z`I|U3$Rj(HJ8f4Z_5$H^TRZcF5KY6BjKKOi$O&)KT1{KD7k^0*(p?-?wzdL2ns9UKFOC+y#JDwZnvH{+7Yu_wu%fKmfw|;%>`Q`eEC|$bE zlE~pMUk|pOxaP6?rqF^rv-JZE@QK~gU}GY$V~NPO`wnL23@bN0&QKPa*qguNO!rgW zlV2w?tY5$>8uk2_s_#^CD`~8HUUSP9i0c+bOYS9a9fwO=;#bsi+LdOlw|8>)CERX1 z13)Aw6`~A^?fiGztd63V9XPFi*&fGJakiCL{OTtbQfYG!BX#@33gqgX+KKiu+DPdz zIY7#lupT%W9(Rz)eE;(^gTemP8(WBc{*&V6%QpxV!#f9t0{zTbdYbGY9B3rPT zyI5wQnB*BS6(X_^esLb|?pf^Ke=M*=fL_|L|IoMge>Unf1YvzbB60sVNW;~(i+;Fl z1QsIBgSpDl@LgW0dfUab%@@uS=y)1$tJI4yr=BJvp*LHo7CyJDokTVhiMvkma~@CJ zoR>B8RsahS?rjG>rwJ^~Lee3*>qNQ4DQmt*=VTI>OS{sbcf&A$fr(+dGMGUj_ylSW2tY`hTg=86)OLz61v;wS~q|-g_(ZWEbHwD`kx;k;r+kzBe^0CjIAw1 zF8&b?KiL@FwFl~vmr(YD9{PpeL~cKM{Iv&hf_k*w2I8W>$x{w8Cg7*R*#4pf+;)JV z9rhinm;eoJven~$tQ###;XvyO&W%Ev*qGC6kCPFp>}<3jI**jRmu z3D_rHs?)0MRP#1-s>DiIR_sVidPNIFyiWv{U@@V|H<^N38#Uc_6|9Dnq7VRBrgtRH ztol>;J~j&pQa!(jqpj6IYx+h9F$lv#Qo2nKC|ZK8Mv{h&;Fc?SRjS457ngmURQs=w zh4c@sdTMxn@tlhA%`P|pg7U+IVed~YmU7rawab0r@%;Hh?QzdwbiQ1=pvxIy*>$v> z5K*gz%j=ky6!2)&Sc>evR0_BCgw7W2_lCkC;@nJXb1W{h1;U&=@-E=l zKWr`7G7Em#0&$B27)vBP2QvF3xgN>9m5>Hh0f05_a`-UJja^Hgp_KgT zW^D4^O5Iwc<91)XYMH0q`rt>3BouTJ?NENIf~y zr(7AQxV~l!)?4cLf&yAf0Dk~ZN@~vDFIIx9MUt8#G3SAupFibhSYlsj9#`b9gM{7m zQ{uC$l{YR}LkOjzK{nA?ad#iB3+GO(E!bF}70i7SMfViVI zxMqrHmqNWSs{)$W!omXFtOUEP^s13*3*uLhXm6$pa$~|%r)56@s~?h*l0SZYC>MdW z|A@WuftKPcL`f&6rnH2uysAy4&9~Y@`v`7>yfWRlk`SD=dc2Wb&>@?PH0U+R*L*lC z1}T{irgVO{tBcKUu~WSa1p zUFV;O>KyPu;*jR+?)QK~>j3#cymC_!)*Pzk39g6*;>>hsZw?n&w|Llb* zMMr>hx)J;V=kU0brjk_<^z*m*eOEyJ{U;zGin~o`l%34?gZA?>rcre7czJ5G{FFgm zp?Nv2h4;?JW%XEkGI;lnw0fA>EHHb_7LkP^Z(M;_@mQL`b1#lg3ZKhaa)^#Yf?m+eX7f1;p^%`*blzdU>>p*B{ak5K74~pA>OWr3WKKq+`B2Lv`XNcz zOADOv)4PdSaUU3Sy*5Akk8}+d7Zsg}I7sn|K49j9%udn@n|K6w=z+=d;O@;`UNS3I} z`UjhIX?B0MDmkBWh5Nx`KvEDABk||*HKodM*HjMbIExg zFv&P?e=aksy(=6Sy#Mk#!FuBgCdWrzs4s!3K?yglb*5Su155z8E9W^twUO@UMhw-W z1D;;&NWm|6+s`!C+_v}2|1nU`8Nttdpr0EWadZriPI`?9U zM8wN3Akb>GdzZ~;v;4j#Vt6Q{?D%e$Sy1`z)-+HQ1^icOdK4RcJCvT2%SsU23?;vJ zIYa`D_7a!#HE3vP?!1zcl7fPQii%GI0j2*P{3r-D;?}#7Uo%fgPz3GVGBh41f40>hN=7WOQ z52b))aA(yYioG6A*Q6)cB>Tt0Xb}_H1>v?vg0R7R<12M?j%X%>s~q~YKfPEio?eIa zX?$Ul^a^2NRA6azRiM>OnkF{E3#CM!3CzaV>qhL_7~JUWwB{Y7J7rEc={>D zMrN#(6!G`Mkp7F1a_kSZIfD}W^#61Z)Zd2XY_i7hn zT^Xb0^|o=(dmw)`k*{}B;gBIm6nnwt6gjEw~4EOUe_ORb;^GvR0VTE z4_ILWJ%yYc$^v~?oI`!3Nxum^ZLsx`u|jqJb^qSzj{e0Ef(3Y9URNsZCprG@8ey)^ zfm>R2|9i}lSAd<}@xt1QDpe2Q^11SZMSYU)cYl4l9gb&87x1{d`CPEv&t<*hBOp>3 zc4~6|!Bm4FS>ef&Ve{j41M7CswiGAfNyEkX?no{N2jBS$&(+iI-qe(8`}ZwgrC%nI zlPIi<{ADvvi)`^*`Db5rpq9Q@r_}UD*SeNr4yx4+i0D$s2944iqEkl$oqE-w+$VtG zMXV^HmzMyYzjXG`x5o^HNyOj2g8tH5_;wRx-wF8xYtfP=v!fpRG!ti5dV2oo8_F_Y zQ>ck6YO>wrWa9T3^i)y_n{1TO7ygQp??e^qsY&}+>2V$&E=7e(GNZ315I}T(#QRen zUe#`YEEPd)>|jeqe82z!aE+e#RU#3I!?SZ+TU%?#87*)i*trJrC1OF(1#t1y>gwyu z>Qzf;% zt~`c=?*8oD+}0&?I={M(dhnKUxs6rp>|&w(w96}N#4--Q^T08t7>S+M(stbow61~% zx33_zT)csm@enMzhWhUhu@|oV)$x-ASp9uZJ(cuh3GU97M9b^fx=1Z=|raJL}<44tjkA7 zpZ0eD++B$P0h1Sd@19AYoZIpA%?@zcj>p`z;`&t8;}VB38-)A(>oe6~wB@W(Dr|>x zJ_fL$3jpG{ZSz)-QpqPFACDz|%g_&!<3iVWMJp;6`lvS8b|73}+F>)21eN6H!)maz zvm+rRE1dPdnFg42{ELgjK0y)}p3d5UvlZs1xBP>1VYi@9jfsC9Dh*A|%dZN--buYg z-Q4XqXyC~*AKHo%a;8ohZ_KW#;Z8E#{zhKB+Xtjn?ePQ-ck6F~*;&a*0$N&iFXdH( zLe136R*SGh#=$;Ol4Xcu_IJV?3gXVpTI!T%q+(^d3T%CyO+0s z0Acdu>v17fj*au5D(8o06yHS|is=-L-W_=TAd_M{JB9DTmNC)->3ybyzW!FKj=047 zBp}@E`&)E6aSeI`67r2O%||zWAi2tysK)CxBKLqBTjs=C(MAqfDS-+$E#2i*9)pMrRczk;b={V z44z1{rCmT)Ag(htH}7Iwf`jSzJ%zC22Z)1WspL#WoW@xzkmln9)s0d(Lb!F4J$ePt z?Do$%H>fl&BH#c|tkSX4Uz4xF@|K6n+uxJ0}*$42hu6@2Yj=*E< z8y-fajd=Bj5~5>ewf{GET2oU4E*#0YNIR$#krWF=Cb*j;us7XV6GMLJCjnkoUDgZn z6Up}cZtr@0CW{(j>7I2a@e;Bu4I;^)S4~8a5wK#@($c^mdsk6O3DUFZ-1M}fyu42p zJe1&sB^9%d&iYygB+%Bt&4T`LPmrGO7JNguMR0l>6@}^ zeY-Pt^dQs}VK^sghVS`BoF*eqBD|R_e4j}2tx@L6pv46(Ex>jsVWOgLqE#iwSM~7S z#eDV=ubIG}Q8y8%qNbHYK(4vt-OR>DNZE;%v$SJ^L!8jTlHCSrRf3zbqrF}VN-S{b z6T!j&Ypgp%2MPZo@ z#EyB1#6hI4UEx9E|9ap-@QdRCKa7khONKSN2((!5xptQ0&g@g=iYIvr#crXXIWZc_?lzc8TV~?Y^(7vYo|=a;XnV+1 z`}(^n5M^m)Iq}zCPOc@MkC~a7o}QkZ{DZ&l=;)~bFHk_OfRxw6cWx9yJUSzAJG8t= zaB%Q|D3o@$6-1dVNd(Fn32lu_J5AtPxpZN*64bQre-lrDAp?bUaUX}$-FhY)?x!5o z)J_Od@cGXe@c96)!=wFC*V@|Z*CqU7y#}PB*|DPpXA(mr`RvSLcJQvJB1?S7hM zr4mhf8RIku9iPQGpS!x}%3fhs;sc`U8U~E8PV)Dg0vZIWekUCvvgQDOVRTbl2B=u8xvhCuv)Ztg! zNNbQ$O8?$@q3a{zIEL5V$s$|62DoDFrx)U*UUS=&zF!8z=g($lW+Ia8;3Na*e|t>tYwTo7Z}f!jv7~shvHs_-|u{yBQ1b@ zKgfeQY=+6+o1X#jC5HK8RO##e zzut~@-ir`4v#Z_-Q$feZ0>TauhVl~@{Y-Eq2(zrPwE;e2?~DLKOnkhMO3x;tXS&U; z49|9)o#?&gqT+`G;eSnzkM>=gY>PORV3Qy?kj}fO!s-5;+c3@X*HsV8dl~Is!EqI_ z`cIB-nuvo0ecBu2>I2f7jjuitLy5C+Nv49hNt$7KdfCb&rVO*mf2yO#zL7^tQ+HRF zbxr7b+{bl8)?xxMI>`Osj5_7bHk{{Ev*t&+CE5F>H!Qq*6qmEwz8dSR`_ z3i;ehR8wevg&K<|HJ@64!FWLdXfd-kinSgK`4& zLVoxAM@1l+(!!dOY=$1Zc!+Dg??5IhBR{nFzP(g}{7G0Eu2Ll(LE+HbsUr+j!_E9y z2Wh1ml2z4mGBUn{>6GHnpVn(FkBpeEcPH_-#?uDT4 z@0njGfTTh}qA^l?g$=ZZpHz+;&@IqsL!_nw5fJ^6LfyOen_nOfYk?wN#eb4xUj6Jq++_Hl<)oM4vvTn9lYXTmV zO>`9ljmd6oC<+4hGe}|ObVG)po*(zq6>PejfrYo%=hl{%4h#o^nT#@6%rRT1b?@iW zqWx&=y48^$;*Im zE~j9kQ%w(F`&41Ya%07CZo@M5bC!A!WZpUX_Qq(ECEuqaxX}#TSa4C~_)x&^BlMZ)3$j}YHm34exxa86Ag2L}-)|izaTc8nAyHV84(W{i_J^$KWP+vd%TwWO- zlOkT)(Z2ts#JbPU`OJ>3@pk=3d3PE>U0tX;m)IRdP||-6SF-)UMG$ED_(P{f@A|IZ z`F|W;V^p92-_KZFcKyP#wOq@#&2L^U^9rk$m+jTEUCXv@yOzEC`ri*aJ?NZHr}O>1 z@zUF|KqvRB;VzHQy*+V1SDjcX!8+7d=-sio7)nVf26?4zE0B`-?i6q87cd%^n4QY- z16;ifUEg;iS)us-io&2(5|Mu+K3(SY{B2VPN>sh!GP7cFY)x(eK+Y;f^i(588AoX5)rGdEH#R#%dx}!&ay;GNXlO z97*H4G1mW+FG={MIT~+Ajg0GEt5T}ZKg_3@GN#$r&67WS{1zV0U%zd5A&P*Hu1DZb z^}wkzUhVf2BdF*?#1z%d8=!g``=wse_>pU}rMw2-N;T~4z!-6uHHNun{EtC|83(r?@pB5Zm6k)gM&W; zz1viDivI~Rie#sxAeNphTF5GnYH*@p4JNZ2#>bEgXq8nzx3+GjrtNBxMe(@{SKdC4 zm9~}KVKC<>r%RFmZaSXC1dyh!P)ohCO6ng1Ud*$!u>)}PW68C#`Rc?UrWn47oYr|} zJC`jCw_!9^b)uR*D<)gWjdIf!<_uYOkb8{)E z!?tzPmeqCN1N4^Y@FF+$@*!4^P1x`H3vT}&BFAz|8hG^$|GiKqJEM=<4_aw$)MUHJk*M!z zTn&|6#f{F8IjmzXMapo zwz*5}YQ%)?`Z}XZ5oY`|r@1Fm9sxHi@4FGpw0}$s&A1W|UgoqR z_qviDro9Msn#Czic*0YJ%%)^NC*JG`v-3o^7*tp|ML7+&HvheQBsD%Oy8<2<^F}b5 zxc?-R>eMX%5oN^%@w_(BBVYyYaB_2lf!SJ8z35YCy#r$3GiFT4=MarlRGcn1ge21H zqcVY`j0{O~b};d%)Yav415Pxq+!NzeH}$T; z&+Qu|=&njvBhQIAH$&{>Xd)c`_2qc+V<6KXuR(Gq+pe&scH^li22tSi9~y?2$>}HZ z6+vdK*#2BbS_W9HfX}&+n{!c&$EdHhO-%qk|HH*mHk)ZrcQB$DRD``?{V&r2)3$Z< z#ZJ$S+39KL{gI?afHmj$Wr?!wfSP_aea4BE)_OGSX?>>QdHY_C{I)p=ceXt(Dnib- zm5fHoniNtFzx;=tgiH=iLuZIX^dxzjXYvwYFL~2VjfG6%=_Crt{F z4%AI+X>xqy7%@;&9YtA2&v_f^^1WMe10!li#>U_#yuH2OD>+hzErLBP3uF@iWx-)EyTGG9#Ev>9Xnd8Uk zx_)pBAc@v_9P&_dpNxeq1{V-c6IOnViAA6BQN&9J8qq>KP_k7ItaDv11*av%VN11e>ZLSp4vD}(tf2QPWh>&uKBb#j7|454C7)s ztO~b3=~VYS@r>7oQMSr~aOa>(3j7qIn?ZXCnJHp=Pa-c=M*puF0zHGpj~2_u@7MFd zj*;dYU=rqjiWa1*Im#eac4nM+vzU&`yzQ9|VUgVG`laq&buY=xQ#`tnE>E91O>Mg0 zSIyqJ(xzfwo|D58SWfeS3w`5@dydcR9;*$-ic(YgrUzxm!PA)*Q_Hrd+WHN0nS5a! zvHiD79Y|J_+}FCy*)PtDoYklh&q?0;7-|A6APG~#^@5y>xUqnHR0gZ3vl=75x2UKn zIO;Rw_{hiy8;E5U>ucmrJRuR${S&_@3Qy3tf3(@d60qhz5&8&6ejABs=qLczAW1@B ziopJ6-LQQmzb_xyI$TdC_8JBgY%eX6jRZISE6dBzuJ=b(DCi?SuB}L{o>^YCVlg1V zbxh39TFR+JwmGyJpEX_jwDa@x?be?b7a>;di9-vsG81*ubtM0=A5QR%ee=|&zCPmu zuYU){3##KEjD&SZ+imcCs*TWyF}Uwb|xDZt3D!TtOz_;y-mifLx{= zW$Yz*6_z0U)Mj&Wj8~UoIoxoqd$*8wNatu1U}gH~A2F}N5tnO0K_R8Ae5shm1eFr- z-)H^LF7op7)DiG7ff@{cc9V@DVqm}$h7a!l6rAH@V~@VVBwEJCoj@l05;_Ec@rOCr z*2DnZZf}QKT;OH_3q%~)EW?Pl?Jr}L=&7_C2<62Jbe+PM;!l~$zxhaYe2?!28eLe8 zJIbr7zFHS=0`)=1#&9}|JQ!9k&Hnx!<{RTG2M~s<+!sgcehagEssi51Y>k>C zCW4pPggPpz`>3rG1RLTm;uL?m=#FjjEZXy=Gb2<%4~p_q5ZN9&;38CGhf7qi4-X$3 z5}Y5NQYGv9*_nJwy{F=}k3Id~T<1^$0vm;eg_ec1YgmW7l5qo=xNlA&eVeY z?6Hr%3k&qLK}7cFgUviIi&-HTg(62@lZaxR84-b>;SxB-bt1XYR=;&$)&4PoiiyzRHc`(wW4SK1A|mMP*Q}1gtj%d$MTA% zmU}oguyJ7Q14p4RVX}0{l~Jb@eyh^q1-@1rZdKTz4BB14b!gGPgVk4t>TFc-YNU@S$tOMi?3;|f7s7ag&FOYR_zwo z%z5~<$0;#(b|)kaua_0=LGQ-|)25&}Ws64>50ta0RuIp3$S>a)f=s?ityU&ydFY@_ zBu*UN?~%=`eBT(zRYh%X-1(l6d<@w#H6g5|K_#dJ`kyD)>qW_Nb2m#1G!56pS2al2 z8Tc`*i3T?GU;w|!14ohR|7!_*rnJ76j zSju`pQ9}#MIXm*kSXO(ouvK8k)S{@i{<04$f-9NIKt8wG*L+Ut$%>Arc3Lygu4-`_MCTM>QigCuzCH6wy4i>}`D z`=irsayz4y5aXU5^hU!D-~C0`$%>a4ZEQoJPVq}45&s0`Z1lkM(>?i7*f*)s$<02? z!ax`J7mD9IyaR9?38;#haGdvI$fFQmy)m2rR%oq1ymri+y-Hu|Gcz;e<|6(x{NHZ= zuaf5QAEUY?y_X#`5D++`=}NZ7rP$65{~pDTlB>uQ%L*%sLWMq}ev+n@P=Z6rwekmO zYlh~cXhX=6ozIx2n8g$W9Y)aP6lLe<_Qxdei~Ivjo&EWb2}*Hxj+<>R`Q>MKW!hE7 z9YgD0IDNaB2bII8qhi8g{JBvfekvlb0j^Sp9gdEUd_ZSIi_0N+@{K{d5b^bMJA5H~u8DMHE*ctIQfsyh zHHT20!k4Dgbmi?RW2-E;4G5;l1$p5bFgpozms!w zYoJUUa?^d(x_`vwx_s(&<8zWzDYZ7lO)NFT+$3dDp~LC9p)k;z5RneX0si>ZrEy^; zF=HwinPxj-kp&&MJ*hY87v&%rf5|$$t3Y_`bb2T)dhEDceCrr3n-qRg*bn`qDS;9F zguw8!c$|5i(pg-SRj&Z6067b^TDE@u;J>7l%W2L3n6jO&V4Cq|b{!n>hh7#+CS=O; zhs~`siG=j^hNKAz0yPem06lXoe=f&q7AO^S{Z%lvxRp#O)32KElrlS!!OU3yw#E*c zv9!FFKl8(d0KRZ->MT=gW8`rgQgtOOTG7v8C^4``)prnBI$elJqqy01B3B)0bVn{V zJDdMgNdtdIMn)EF4Z*P?QS`hJ!BEuK*C#B5%UK!EjkiqrjI9L_>V))a>R4uW9~)eC zv0VCZrH=?L+0p&@L(uqsxQZOcXnu&hpq2AX13y0s!G<`nMOQ#g?&$}m4t!kE(HAov zvEUG8&J;|)2=T7i)T1wk{gq--6l_uS<4<%jPjiJYjbb^@U^W!Sxb=+f&EvOHpHFMV z#=ccmC;wg(Uc0)1ut4L@7a!!S$jAR7y$yo(>ugDHZS^-u@fw?EM54OFZMxYjNcpnL zL_Ha`AN#UKXCQbs6{uyyTp!ZLpMU>AAPd~^WkgXCVQ_-OJUBV|@NIH^eY{VDE0bM=Pte)%V<6?KtTSMd#j&=}-{BtFKx6Z@1WQW0GeXA8P!1 z4&gI zU2j>>nFtWE)KUE`4t{b?D8t4%a{vB7A3h5(1KZjjz1x8^ZNP+5h;kN3j3cO(lSU05 zBtvBs-UNhg7RtfiBjo#tnKdxtFc=LXN&b{I!;!(Z5^%V+6)=KrgajAlQG%p_55AI- zZ7WYJj$(}%70l+58mP=1JXtgNsb#?qm(a)Wkp;x!{mp(&K3P3L75oN@d4f3cAzg$q~Kol5oEP|AwQCoBC|=vhv#x{xZ(U2Zd#Fzlj(7V@^CAzw8c&T`{6yl1tE~8Z z(CoG#Im%a|W#iC8_(C^Px242}w#gbLXw79>SQH*xNqsc>QSaZrfG$ndhMw@{i^L{q z3D%hhJ$8Db2#T8M(_F;5<6->QHJ^VrCI z^lp^0DEFJbZ!s=7%PKiag_yF`CX|?nA>F>G-lkhHqEqYR^9r_{LADMixGeIOyKs)~ z*&WTN`!1q4yO2p1qn0xPp-=YGl55+cX@e(qM64FP#{1EeO~WmFQ9ekq0cJQU%eU+| zVm@lt3g{W&hm}qd_rp?%8V?Yx!NtKr^nHRd&U(I7^XteY(#ibzxMItmZI{iebqO~V zk!1{&cu$K+7NKi8-eSmp)AxOClg`{(H=EeEpp2d$!STOV)&?R)*4h#b)oTCL)uHyf zZ~R%E+lZN5cC=%xDsFzbQTC@_J@yaTnm-!;U2;6#J?ZT3jNfgV#W8Ug7`uTwbtt0hjoP1$x3CUzQ->Zj;CVv8E z0a>du9>Idz{vS_dq?b#cXVaoRXl9XpA^GB1=3_8+lAYh>|6(8Zu)Np5v4rzQp4wu# zd5{lQn-erPwfTItq~Q;2i*TJ3o7Ro`TV^RcucEj~7$Irq@)@y4M^L5)Hl@3Yjh$U= zTk3C%tt*k3aSA1nVUe)DoeCJ^xJeLnY&veZ72Acq3aAV<3qBZ9%a=4{&YGnyl+vQH z*T;~_Cci3QKm-&KYYHQD*7d#H}DP z*HPGqy#)~~MP3Tv3RVwyfP|tcA`Y|)(;=64Meo^x(JvrU=om*F59P97GAb0G;^$_c zwSh2r0IVY|#iPE@(if( zj-nx{NGRlip^})E26rbKGm&}C<0 zLcvp<1f-+{cSxlyK>7$3t2j<91<5dIk}kv{INJ%mR`GlXTvboc*ER=#3mqAa`0<4i zs(QJeo5vsb?vikQ$V3qM$u*(8&;RbSW()b;*^LZsrIlF3lH4p|@1*Xg7eTjbbd+@U zooJKqRv1Xh7s*g#t@mK9JRp zM}Cx}2{FOVV?}wJ5WNyCC`x{ZI?}V z9dTWWz4vOe|Hl6vd_R;JUtk@RoTbt=9Fm-saQyn5r}AbD6@kS|{i}#`GB?3OKHLMw^Nr+VOsM6mcnfeyST2 zr;+Dx)Alg^*k;ExGqJV&(>z9ZN4y?!`CVQHJ^UGtQmy>T4XOkB*nVzObe?i3-^Rom z87p9Qef>{G#kr#O|12Y>rvJ%&r66qv$no&-u~kYFM$`AiW;hf*Pij$0iQ zW7?1{Q~$=H&H=ZZVYQR9Yw{2#rnZ;a=&ZuRQ&s~g{+zCAa-|%p5}vjkVO^JU>W5oN z(<8lwB}&yj(^8)Sw*2p=)YXl(8p(C>qsht1*7w?%4NITXbic@+soHQy4?ilt`=a+!tUEG1~IJdF1$cs50$j7RoctpD){c=lSo(=jDGpam4i3+PshU za*B$I^O_totCea~ga$I(wHN#7+@+5o|W_4+bbQc`t4X*KhlA^=~yLm zs_>90Umr`mLYo?~Pr`#zkv}sx_Z^=uuTHI)=udZmR)ot(nD(s@2}%HZ2&M>n0lXG^ zBEo()lEgS$q$qAG7&yA+dG+Mm#nm2A&~16IbtK0htaYvk0dM7kJA9%&(K~#%d}F$t z^XL@YJ;E^vqIQ9kc}K3@TD!WCGMQVd1?9DzrEpw(?wR}&iCu58#n1#{Ehau z%F__P6LjY_*@?$}Kq04H6;;|HyDU7sM#cKWVQ^3w6-&YNJ)mR&*@-MdUHW$4Y)NLv zwpchx%~`g$g|%mt^hta5(CrKIsG99}a4Xt}N%?Dflxhxk)|o!Je8cvl;naVwT=ARp zqrYwcAii#6q}_G{|6VpVN!P!&KK&EDsjPiSpXfy!ckK%VC^Gj;&_Hk}*K#2t)CnfS z`Hrh>!f}J!SMBc_KhUg z6529Q;+PjO;V{b!q6eyeQfK4zYdy29{CUDn21;(M6`3~z4YWOu4s-0-%F%lpvqW;7 zOWj5=8hBa&UTE^%WpE(l^4!S$%edqp8A4eiV^L%vQwZi{>I=gXZc0GvNl+y}9Sg-o znf?6q>C;AspMLlOD4GS^24&C#vLTE>K{3-?CyeBf#tdO}aBpXE8?kY?)qhk@dlrkX zp$~#id?YuQ{Gi^jdGBwk*SN>!>-D(@@a_fi@&Zg9&coGzyJwX%G~eHZlG)Fq*c)uK zn7Xzw6)t=}3X5|kCY5Cy99tOg2e}q#7U<@vc)5S>``p;JUs4YHvf7Q<98v~t#~hv* zn{ca`)iM-cTO1wiiXekaIcIp0?_}6bR?l_qs1GGkEfIPVtfv5VDh~JTH0z_22KQ7 z#ZRrli&Kxy$5DP()3o{b8gExH`u|Y%0Tor3mx(OwySqYAfz!hVmZ$ukW zh8cRv87pAQ`B0l@NV3}bW1rL5I&|KbEyF%P~j%%d5-TV%;GFf)P> zL4aymet})1fZs7cwrRaXt;H2makO{~-UYiPQxoyVLpb$bYX&JQ+0{)O~AE|RS%a{Y`{f)Pgd5?a&L;Y%baq3S6EpzY==_FSzmqd|Cu zdly9dlQ%CnJoC%z8yc{B8L~w-eRc;X78V=|X(q7Ep0>j>@kF59j@8!2Z%28nBP?LE zLaaaV&*bXrv%J%Uci&*WKmqUe8n)g~JV?;Rg>*vU=#h!w6DNl}_}p^Bh;4lWHe9b$~Q!Ndm5X<9Sv`8CPAS zo5SLbB5O^^`C?z3K;iHm_*%goVmcwz`fBimqb~;i9`73;(ZfGQEj#fZ89%BZ#d5j> zf~hnU(*CS^DgK*8U#&luD>fU6(QC40G|0H%y18xq1e7Bbd;96qA-P~qblHVI__|g8 zuC$&(a@)zd&ju5rVl^qtni`2pWz@XPMx>0AW>0$a>RKt|f5%v3L3+MXM~EpS_qhMZ z4^F3`D)Xw>{o4R5yf93zUI+7DmQX|L0o?b$u-#54ruqBcVdCL;eL1p}L%gX#93k3{uWEQ;7_cAo6DnCY|U{OmY8Z zIiV>#5xi{7*JaYj9vw{=w_ipr&(Jx`3`AC!iE)cB$wF4wkP5eRZ+b$Os zP~XQ!D=)~aH%t-gP0(UJ{|Hvv4BgICHZq+y5Vdb?yq{M3Fp_?Be zlMWn>M5{Fq_?*6p0z~py%GctJdWAWGYTTgGG{LX?(`gWO2MLZ4xbcAWcd-Ge?Nr1z zh8-om9DT!}Dbn-j-U}A>{~x@rtgxT&OW{f;+sIX&TEq-`vpGc5c)duot-j#NIV-}B zTVl0FlQ;Id4v^IyQ9e!*7w#2t(pWB8F(iXtZ6NW(gM9Aj4g9p$je^FzJRV!ibx}*zI+m zyWK^h{5Y-H%lq;v@v#j|t%3W7-Pd2uTFx=XfA`G}G9^xw*MhR6ZhpB_9Avq*UE|5B`sKe zc-QHJZd)q@kqS{rGjzbqbs~S!B&8y@h|u4}hl{W)(0G#ncLbyEBkZ?mB5qr2W2#sF z+z1g1`ic*IZ~sj_m0iv?z`avcDg3@nqSl}r2mzq_;U;^AV7u%*yR+(vZmTM(Wuk%I zds4B_@m8D>rVm2%wyA+vjBYC;b95_k{Gwio(1$*?w~=wbfZ}OrJpKc9D3aU;=qc|R z%-Q2M@XzgrhBp<_qqZLv@bH^O@$Hy>CS$7F?OXslYeM+zApG3ZX^AQk1&D+@2$hn> zA~I=_k)ZA|5IJgP$AhOHW&kLKg0J|I3T~cCkF8&2F^|-@m~cJ|qxq}|%hTpG&NBb; z!DwwGgOF5?!txvU;~DzkwwEk$>?4V|5`P`>c1o}qZlDgM{)|4$nqz_u3t+fqFD~k3 zn!?q^S(HGJW=cV#77G2#zVc^MPjlydsgB{QjtGc=QkL!Yj0&{;tj+$0QS3m{{BeAT zC%qY~ofUH>z#km(0^VgJNOT^u-zlMH$q%kGF(4P1^`7KKiDYu4N8qmbxB-gNzJ{S` zoAaFxe~JEW52u6F<39BVIQT|e!Jgjyp%_|U)FFIVNoAe0#PQg5vgP@@dYk&#I_A_m zd6$=et!nPLfB?OB7;p0kS+vNnqW?WRG?LrU=0~y<30u~7+f*0BOqW?JR}EHQI!GBE zIMa|Zl6&K8fYJ$%H+6eM46P~8`|h_FB-!x~yaaA8MXpLwGfSIp6Skj52spNkKLz0gbd*k_YrT1X5QyTAP zIJtOlGHRRQ-P)R%{`dBHR#7opxsA-nQj-tYebvH{3-r#2{A*orw*$w;AO|K@GPvr5 zQ44E10uy{ouM0)UToWo3JU^`HG|4xHBstK3_Yru3B4UOhY>#a_B|fqHxDZwp26?DT zF>tYxl41%5Zj2ZbeuqO7Er6oeN=}B!`)=|Pp&xAMsDk8WQFRj`7$DGAJ#@~rqPOAS z*wEJY0;+95+Z?E`1pT8S`8po?-W&7UVj>!?EG=i9q&Le~5+r{%8E!J&tlHcL_<(fj z#-ec6IUFB@nmk3OZhoRU_A7jzm|v?}yj@?xjnKt{+ zYC=NY|2~S=DU*#;g;^8-rnwrN^^P${cSU$YZC*@JUgWSKIy&{go7bk2it^k9D8SpM z!(jz=!ed3m{!!!_u}!vt^FM#xK!6-S9&pn3wZsgokV=4c7*?WJ$dPQP7Xe4P4@H+| zFZds*(4A;27d|4M$o@W&{gTU$SZhrXJH7W}^WnAgw;rI}K-BLF7swM~S~YQ}!TD_# zPmxkb8tWW;n>pFI17zdC1&O4OvSC;!j&p^wK{wrr&P3p#6U~M3B5K12uzf=*e;jf? zn=jkQY6a~E#X@cz&ADRnYu04iCkBs84o@x*NmXOJ^4+piO;K!ndU_+4pgu(J1FW#E zt?hkL-M3eXF$nyZ)tQ-|F@+((DXPGGETjjnePz$!yPY$=_nFGo662rS?zcA33DNRxKjNFD zrlqB&xZnD~TNrJ*(6*K*VT9cop8eT@cD$!A9O02Ynf5?Nd-8F@AW>WDS)zh-S_LCA zG(B1bLDvV(TywI=itJ&!)^(ZmJEU5YSGx9#fOTIH!yAgFz_U~Ub%Nlt5=f7tI+#gu zk`fA|fGuA%yjQG)R^J*4=2k`l+nmf`D92^~hP)00_dSO&6o4&GpKaPF?GQ8vg|fYy zsRGX2r_-uVgara(Vq#~V=K&@Dc#|#qX_o8@K76t$_8S{A-4laYRVHccE3rg+Z$(-R zTG{z#GCeEo4uBH}kjAu0je3ePu`*$Y)$`{5k+^gD3kvt6TUW#r_qO#A1WQoZ4QTIKBjm9pPC{`}vpJJVc{L|&`a zyZuD#R-6B|XCMCT*`M0*5IFH@&iG03 zvsoJ8FO{s(3(m+K730=B44^`=Xx5`{0MDAhr|hQvIGa!9aZ5ceOFfTYR2sHC?_1~m zp1JzZ4NsCBN!YMdyn+0e=vcju_!S{Dth!=QYH|6sGoXEHdfgX#Nl z$}F8k0KJ?pyv`Y#oG3Ms_XUWRSM`doS#g#EQTeJiZywDwB; z4LcFhZ}CmXwPs?-htvSL2hXHOLyjFwXF_7YH5C|OX&d-dFynXZM#npX>jQdJhhUTmb_HvWlFKCA%FeVTbvG5EdoNo5mR+}9ZcDOFz~4jwt)2c9*C#aX zyZuKgGAMi)TLWdHFOb@gS=&m4VBXnJlJW1hMDgo+PJCbq4KFp7KL8vZp#@vpnYOa16%r7GU@Cbt zGT$GT{T8sEJ9^7pBo+){M1dtcns3N~0(~m&yiR!JYyxN70? z!(5)gD9uqah4g@X*-C45e{R)EOV*0)!!T33``w4FW6i4fbC<)3^=AKuqT_Ny?*#E~ z^Hr}!i@KA(nn8=ZD+Br$(f=ahuUh~QPnMsRurr|LP9`GX z-+9AcMWUK`>Ru5{k}9CzkxPNyd32YOUXu4<@{ zafTq4m{`s3QxhW=B!i_&<09*ETB{dY-^IPgTBYw@h|g-v3Z2^s%nwG@PAa;#hXn^g zaXN@p(>>F90urD8(kd1}-ftX&*}#4CL#KWJgwka^g-CP|pzb)1TI z%u}WWYyWzgT@Y@?i)9gQ9j5&kAq+F9%6UD78xzta?(t!y>Myu2`@KIMir?Yif@g?O zYY+@*%AYehI@YF&E$8>n{}Vq1B^0Pr5M%|#ER>HkT3_n&m9;6==r1i!qL-t&q7m4{H2wQ z>Cbbf{x+`floumjHTKQ=W9oL2lyh1%D~`M`Of_+LqW$rk9h*F z{ek>DqH8qOF>^uB+@xU`%5x+^%k@Z|=l|9nF$r$jC9(KSbrQ5u$bfqP6LZwv-@ z_@&2(3?nUJwf~;_^#n6&s;d*j%=T`8Dj*n#o}b1quY;(L2mfKWN$m%tq48r7^Pq;b zz0zj27<^il$vXeVh@9VA6iJDZkfpwDYK=d6tBXVtV3m0k&~%uao_80@^Uo)p`wTU9 z+8%%6(Gm&<)8LbO6Z;z;F(`y{{&HNCcO`EdDOn|icAngWh_ogao5?L3n8ER0+kykV@}S$vry z_z)&{9@PSnhP?94Yw-rN6)3+gF?G@B67F&8U3?Gv`jA5$+Vw%89VKhq`YQmQheaVF zj|s-pfiA%HTamK=l%;Sum6^~INPOk&d-+r}`>Wt7nmy6}El;T$Eq~y)RkHY}mxqO6MX<`NsN|+@=RT z8l>{%iDWHc`m&Aoep;Hm#JBoCQ3$3m9T|613iO;*LO;U7lrSx*!w2%9!?Z|6%a2Y_ z!`#VY#m)9YS!?1#qbNqv9L!C4Bjg$CPZTyKA4^Ft;VBTFOa+j&J}Pwj-Zz^y{=?cr z>ZQeaWQh?n1(jJG6^hwvU-{ppM{jw$>mA7C1z;OZ{q@+*8)o z%kR(6&dMt)-hTh|zXJrnuB%37p0f0>Clf*}hPgp0M+=J&i(;keBh#Lu?qpiEoIYEq z#9^)OBOAM%DRQWmAaoUU?jPuAESo%yAy4FXMS+Kji#(D>sT%QV2}iai7!t_Tu>wNU zP?8HwL`YoNQFe>Ppar;LIvs%iOOTiiuzHd7sIW?+$thk3{I<{5H)9YrdWMs zuXuR|q8eZ@YuXlC!;1vz-@4P_YSRxB<DfWKC?*^e7h3O$df;-y|H*#d$MXN6)uh@b3)gW)H>C6A6ArOJJv{ zx*EtdGc^fsI;S4Apx_qBMS|lnN#V7GC7bS{iUfEq3#Q?GP0@YfR=fm9E$oqH1e1Y0 zu>VE$#l2K;P7KG}!p-{EkJvV)&cXyBz@N7v6StkswaZr>8g*gj)_Ejtbow;y?)2+7 zIo@3y`M+IzJ&$1XczJ9Ov4j{_%IARcN_YLgGaPSRk5C};ZL`q=MW0_2o!$5e#2;UQ zg@F&<@6p!iuA$M@@YzY$QrQ97l|11xbNS(7 z6R^B_3b)Kwse@Tg^!_KH{}IgH5n%DBQ!bICcQAO$Jq!yik|%Kg*vrp$X0JT?I$!qY za;>JkaM<8c+5XRVU7e}vJoIqJj%*YV0EMnUCmkCnT1`mW3FR=3?AhBu2$9(r#Zww| z?5f~jxv8+yZ6FD2GVMUI5xvs2LeOVIrCFFqP$1=b_l@k{gipz|bt|6gkv6;#iSI3Cvsr&&OA*wa| z=U_$<-0%;TjCxU4e(dTKeHk3qu>dtP2ig7ChHwIrW-;Nr@_~zrO16!@90O%;jp^!$p<=2DxCsp?+2t z7`1YKbW~hb=A0Too>n`~zguoLe(ANEiT(o5Yf10sB#hIqT3a_BoShs1P+`@h5r!tu5_UW} zeadu}5A;)shf)X4{GbjTMvW$16R%D`|3SmiJDmB?jfJ1$x+SVW6_^^fu<(agiOHr@ zYt2jKO+&pWuXe1pG4p_t$0a6R^1Mj_L@~X{Dv7?c{XZrmNDitpL0Z2U$=QJ3K9IH0 zYyU`J!Tbea&1)^>^uvcPzq82WnzZ*nLdTp|k#o|Lc#%k7G$=hKS^DCtFvE=jW7Ipr zpC6uEv>j6EMbY6n(MRltlj{i0g&A=4`u4C1RCm_0f-L5rNc7T^wf8^wz`qCJQk%K# z6G8i>D`OtyK9|W>9RsQ*|Z=utDNx3obv+k+}hfu(_di8JJJ+Y$6 zFP;O7FB0VNq=-8cAmSZaueDXe)gnb=JaDVKCG=H%to!-L`uy~Z?GE|^Z4#F>dGR$0 zkfGXaKDaP4!aP5}dUEQ~SpNzdox9~zX0)rEWDOW1%4%gC;%PMHu^B;8jhZjdoSLC5 zn}2Aw%dt5gfnMk_(NB8nUf}?Qo_y8gcZ9FdUaA`(ZoRvNwhie-RmMB{I6~}Yrf3NS zoM(I`R2t`Oe)|~f_>7}}cct0ApJC2{hgnXg*!uwTUd*$TI+pbrp_lL?VWcX> zKW^1vls?)AVp%=0LK}lha6CD(98VE*438eUAkAOtaRT!~2|MWsl<8-QFf%}^kR!bY z<}!QQMN;G-wX;%T^VolU)W`Zt;N++TZVPPp3z(0m&gb8Ye4B+p?_SO%Vr+;g1qFZa zO$r!@{NKNYt?NxH(h4jsU`+VstnNWlx@<k{io=2wq##WI1@lBmlPc0Fg; z$370hsNeR#nu}o3ab&p`0^A4iuT!6cF`#l_=pi8LHzYdoU6bjdI5EC7$%DMA{h!%g z!J=K85x%?din_W-dF91bgPjAx!s}_l#s27gbhLD~FtV<$8RtV0a5{4c+9M1j+t!nX zQa)wZgD!Qf+9a^N4GocLqrL~@Eq}L`fpTRyj@tQp+PNGsO6Sv0fnfPaJgo5PggCy$ zT{XzYM+JGSFY~@YUKO8Wp8qA^W!rr4wXMrKR5?%4VxYD1w@7O! zXTJ)o#Ea(^M?bS-T&Mt9tZmN&Rn2#+FY5cGFf5rMr~#}AZe@xhgC{RMN{!b*=;Yam zjR+IA(?iqmja^*5Z4>2BRcZM{;slsD!=BHHL=IjkCLTAj`T`xuJkLg0MwN}qL5K;} zN^n%3I+X#P2~Tu{SO6E&8x5C2Ix$D0F zgd$XBOkPs@{CA9S>lh zA3^}0!fH5TBw2!VBWkx#%diBOD5||=IO(Q@5#WgufzjJTzkkCg zmD-FW%E=xEf=24IA*RY~XENgE8zC8|tu@@S*@{LrIpnQb#Ld%Y?kGgo2$JPCJK}wZ zfX3m^EGw}mY*H+4&kcm+ZBczEQqDDz|KpNCZ=U~?T6;1%(U3`4S~#5ipz#;G`yGl!X+U!Jw50v>_%k7gr8o& zh}Y>G^r!yE2ckzFy4%y7_GC@&yN+2xU@tZU*Au5NjHqh8HLR2TQ;`g?4sna zCwErMeT6MSBG`e$$&e%iW#Px_&8__DbU*PU)PfKalb%wNV5b7eG~|v9=aQlavQ$OC zCc45J7Z;Wl6qHG0M$_;WA{AW%2*)3WX0aai3$sF9oQ$rA3adv2JJw969R~YjnXEM7 zzm{a^`qLEbJHAIR*4b6|krgY@S4Fbuz~C=Mn_}SNx7C&BUui8~uVS>jG^K} zIHvhaWrOndA)deg@r`k%*N1ZY!;jllW^?WNlokeR!f8M!))j?cBE{bVKBc%CIX#LI zAz6W)p*-vwSGiYwK3-0xS7I<4#p3ZDOi>4M;?8NXN{b$FALZD?X?eH3Peyv?O0WV0 zSYIo;gM#>*=rFKHr@@KVxN%>XZOG&RD(I3@7KDCu3$x zpq}FYIJ&B^pt>zccPZW72uOEIHv*E<-Q66zLrSDey1QGtOF+82yYKn$n@{+mXYW00 zX4cFf+z*pomWj^8G)2&8($$eLMNq|{hXChGJ=!W!+~cFQ^k;$?Nve-3e?D@Chz{q} zm_cBqy@sqo;q_P>a3C$kh=sBpszHg1I8PcpFtMdIkMJD8=M)+IV^+EOAW-vdhW?9r z-H`a5r+c|MaTFim;@0EJw0E&Iw#Wa-UEQC#xV5bLaF>98i73UuK(M@ACFn5~WVl>b z>hzEA8(Z&`tYb;(bl8Oi9+7+W828+x2uoR2~W*8Xr9I<~dip z8JuAHji~y*XP#FX4`HR=wn%|-hr9K**qP6S+C;DW0^Hb{VeR2L**Jp62T=OWMXH8Y1!r5w5K|Hp z3rytSe*v~z_zkNlnj^Zvua+0>8PHO+I z9lr~e$zCl;DQRA`2 z2LlzZgA>{Bhm*oeFAv*Ix3}K0q!r~Q@n*ne67Km_7^C;2Wt4|y6hsNhT9EFX)}IXq zS_wXpr7Tj&4{ErCPD;3KMeOKo6IxkFC!xV1!m#z#3oR48nc{Bm1 ze^Fh*KRqO6nWdW+nn!=0N|mPxLbO6L57AZ3V>^YZ`E{LK(CUTuJ0vdnV>&)Js;qrC z-RB&vF*dx%?+tz6^snOstVuZk7Cyngt4PpjvlsRC499CsLEHa;kH zeh_+hJkYgiRPrbZn-80Oh7KQ=DTVcBX4HijO2Oxk_%qLXfHi?El3%#9r=1F@pxeR) z1vHNt&8$Xnm^-3Wlf1BTebw60ysgeOY>3;KI=T`Jn6(cX^TXh}S5Y3nN%-4?RhiAh|r z&URR}Mtbo94U#Xa-)N2zrO4S?l_A;8Ld}t{i4T4pW6A?Hg10C-Lh%-G;J-bb9UK{n z1Q%f;XVjJ$F_9!1UF^+0g$Hfr})5m@-RbO3T4w||}zjV~CY}D;D=DsDHnO)_wxd7oXI+)|w=CnIo;Vntr z!F%0Uxkqr9<|>}jW%c~q8Ib3@w>CP}I9H(++Y>-Zy)JuCDjOl!`D`)qDroUf!EjI2 z?g&b9jLPxsTW>iwRuu|E=CENML|gboD19xeuH^WX#V>Y^qVeCrS4$Lh$KdpDLYih5 zqjp(K5DncW3w`^-h?&5ikG$aN+Z1nyA| zGG6HwOzeUX^Wx1l$ z2AN3?70g-%>c|h2?>N&K$}=6_?gILJTsfDXn`4ij9C~#Cq#yivqI}V8ddUh-TTREw z!)IlqS&9K=aVefi%$w+mT$pbGk5G&Xa8}k%8*+pitZ+~X1UyEAQPsP_+6jUI)4$x^ zOm3^GksbtMiU?w%zoV-o2SLzjS8y>gjg5}V<^CvBmu)B-$Yidi34f{nmL z9D)88QTJ^O(7YMn7AB$K%jR)}(!tt3Ohmo)4qeF1@FUK@o|6M`eCzE0VXxMcy0TXa zj6PraE8T({7hRTRaZk}guxt1SC{PsCTzK?)S+33o@ zoXc248mH3L$sU&~2!yO$3s(mR{~7Fq*=86(W5`rKYfdH&{MIO8QSv2XLnePxVK;xu zTgPyiRW1$c1OJlXnKe-?^U1gYt0G=Q2*(@|@Huja*C0)65@hL3!!|h`2oTdF!^m;d zfmYsN-;m6oet3R{hk=2CgJZyU5*F5n`?fNgYRV$JLp2{+t5G}(CY}73xXxA?RcST3P~Lq3lcin2FEDyv{WOsi8gp|&A6y(1;HpO=mNe*`M6Q~uo3G(zP5%8N@XN-f$M^39 z_7wx8VC^S#;&_3@O!F(wG{vAwA0SzSX~q!Qg`D{+az0K9C;y}(WC8t`(Dntk>L^i& z@*V}mu}A2@-xY|W_J*f}bK}W_f$W7Cp;vRG)1#gC9^8pc-=`aTF2{%2qA)p7c z8M5Ui#~^mCE7F;L?S|!#eExSyLGD&d#z1CLTlm~+St}FJM`t)6-CXj600T~0jNx|F zaHqu!Em(83pM6;m14UHZm?P>71qTuO87U~Gzjy*A|2il~gc*n&Iy(A0Ys90YqyMn? zqgXetd!Kit#9F~T+f?BK5Cr9UkW)?_zf}_eMS3(OY zl>qGuI0|UTm)k(}ZsG?THi}*_q&2hm^sv`Gp#X747<=DmctZ0G$mVw+tbFx$JLRZ6 z==8fixw@rNirIgq+Bb_gM-kneQlu~=n!*6fDd{IXJ+$EqFSF!JF0oFy!$CiXox74Hg znZYW--G+a&?V9gHbs!RjlTQaywl+|E?g zunngc2qSpfBzuV(wB&Bnp$8)jAL#2fYj zk*>28uM58wZm7HK5{crK?!~l-mEDmT4+htEJ>5tQGj*}5lBkrSOf2&o4#E&GN03IB`N43R(D6YV~2Z z3#U?0Xfv#9SO#RWqPfmd*iA>2)_ z)$kMK;o=WxgaUy&vZTOh;{k>Kv08>8EY>6<-3WyrSshSp1YFg*p3^s9tMWOydsv{; zHrGUgCZ;gh+xuH~13e~vPT1GzmNt)@E=ASN2t`v#R7^3FD#0KgI1zq+esPs(h72H= z2nXQ-3u!5Bvn<=3&^i_-_=698n(%koiB)uAHq=k(3l=VdBDWOQD3$1{3sTQRCZCDdlBw52fbnNRhdq|g z{5X^+muFmIUA(&IorhWXD~-R&3z?aY3fxCSN_fIXKMk@Yh8e_cIDHi zeV5wgLo|^ZGjY9&_62GWU;)l(5&0;HkYr&7Dd)$8BqV4hFack;>jM|zLR0|>0j5Gh z8M+C*Ps2K=Omg#TC->m@kGDlF^Jc}Z(AYut^FSDj`Rn?U5H6u*f83e)bZaSFO-M`K zw_d~{xQehbCzV2eMmD0AR3DPY9Co6>VUxT9af~#TWaKsV=j;(5ZEr_DcaLWjgl=o; zG`Una;7NP&V5-6To3V(uhX(=-)Ee@oCy{ySq9VpI(>d+Lt851FOwDc=8Rp)ZPxV3Q zN6^S3BIhE<#D_%&yx-lb)r(K_RKop|Z-p$7%;42@M7x`HQYl?9TO5jGzUT?PZSzB( z2Ab8fLAyB^3=IGC=TFkk&P6pV_s7*xLh=!9M4B}A4Y~K1xW>6t2{bH>4*f2;n{mPg z>%TUPtDe3mo~ywCa;xaEqS|R2=Vcz+_pi*yp>}O}Ts4Y_>NtDi5g&Nd-)TG z>+IL(r|H}4-itoW-pIt~YL;^LSW3vva6CEDUXt#2tF;6)ewDDk->q0nf4+PIBWVt@ zgaX)M7NUQa2?1|nvX2yEiyoWZy9w=8r~NV-cDe;AfS8zoE{Cp%kBK;Odyw|5Cnv4ec*K`p64SED}Qqbj0k64k!kV-6|? zli{cr+7f`)A{m!8B>M}JmwH$q`usw?yornP$Arn@aCsN%gAYQPjjb(eCBD=(Bf`gV z`Dkp#efL}^LQ=JPs`G!Z_%BklQ!UQs;Cr9LenbVw9Pyr~8~Q$*N+e}S#HFJ7Kxeth zXW64Iv-YjNxJE;*Jc>ooiip;;k^?|Zy30y_?f@i-;9RVvh#hL*&9O24224VsxHoy%*Sr2lQSwvszj}giI>oTb!o*g|pA<#nEJnzR+=)k-n z&}qHE{N>0z?p?RbSZV1g-*`fJ@+r^nC3P?nn2Hg8wwb#O!){~7tz60gguPn|fKIS@nskFZ@@{bk-rY9#Qv0+h88z<76QM5~g^2E_h4x4#Rdf=&-$We5Oz*v@mL(9ucw zI81s7-oHQC$676%h0Gb8Kh3`Ct4p(PrT>-{v*>0t|2MPE{j43_N(6WCj*gG**IQj3 z-m_$`HIUeUN=J1X4=6b+n7AUc8pgW5yW^@CVz$qFoG4XCu8*OX^4f0VNjCSS;aY z`Iv)x;WWOy@wPk}2E<4)A)b(0qp_OzmS@1Afydu=iT$a3DReLuIHxWu3R%CZ>x4aM zx01wP;q)MRG&&uM#c+$MyL5VL{kpFnd% zXCO}VAtOaqjvSMhC7K@b=e!jNEidNNX6a{|8?;j@gg>7Xr3pzo8TO$b|FW{nrnyyQNkZdW-FY ze(WR=`RugzI@hZ`(9idkLat7i0nky38H;x(TA7MXkM~l2O+>{l1%$p^MfAr<6B+!# z^T2JsR2;>^%iuXWw{M#$$uBX4%DqBA`MAu#{Q4cZ$>_<)eHDq`4}sR+(JgsJSLMyG4dqFv*AqEP;c<*m+26b z{^g#qjRf${(agd^SeRTgBdEd(;PSkI}s=R1sY8oQET2%s8B&S2;UphVO z6{`*a%bA3uME_jAKN7XN9>aw=fLJ6mBO^ys@~=$yerw&_#MRqkdEA?LO9^r}nelb7 zfi>0mJ-+*2^AmgzPgLRGM71&NV2LeTW+I2wS5F9(lF47Q0kgnIY9-^DAq)9RX)3D; zo-(!4jST~T73Yk|=;#cvEKpzl&pJSgF}(n2tSBYC$4~*E=sg~CE=MbMTa555VM(60 z93fD>hW)CZxr|KIyapPIh=sZYR}s}34i1DG^NP-ku$Jw zwlH7O1-y?QFNE3gaybnS&o8^bUSg#4?=IFDyPvIy?$>z^LaMJLxphg@2D*|2s5ep_ zj`3b+3-*NyykGR8gS7*QG_VoI0>vXdt3EzHZ?%(5G?`UMS)P;jz0}e`_~-5u?>Ih0 z9vt&3HS}fr@pm{1#%>PY;xU<@k$~&Wgf=;gZWVJyQE05g?O5%8Xw4!KB-c3}Y0OM5 zco){!|FN#qkpPF4T;3`C`-yWXoXa&>RG7b$L|ZcpU4mpUK?yMG?#JJ(of-@uydJRP zgBW4(y0C#1w?g;~Ef>f0g8o@dgy2|pu+W^JHEuM zj|}bBQY81;KK;N<9AsjZe=ght6Tw4Bt4op90h&l-I?b{|y8XT^^#?>S5DT0mFn)vO z*NAc5Iw}_;n!OHyU--Q}fDqOLJXcZu)Ny5X&D9 zAuQG}_iN@;;e7zOwIyI{x4VQJ`>Alh`k&Or6|r~h08U^9W#TYI{9ZIW3>O3$<^cYR zsiWiPH2qP{bS`@{nOCMIl|_X0jQN$fY&4Rto2MAUE@3Z^qq&lI@kjfv%GH`8ui1bV z(=zKx{a>9OcKx~H-j(`Y7bo}+rxoTCy6p}<9!-;3FMhz&+;9si%ds$-{ldf(9DE1Z zUAjQ42$6z*eAxcnw5~zIr}ka%_4LVgeDX@VsY#1|NkchAO-y_Du~wH-0@hY9!Y+ ztrQx(DC{0$`58x^pqxhRx2PwtI@;%8M{<{!e66W?^`UZlWZR0_%IVV3g@en_g70ZP z#cRRXPk5=I?d_Nm`F|`ILd#|z%U!viF5ayzycd+2CiF1b6zZ0ltv0yTcuaVWr2>2}PBGq_Gco_8nfSh&LnMs@52BS?8kSAag$8!T82I?KM_?hKqle)Y@;;C4ItdC0gu}R_ z$AXcIASS_blKSlNbt*a=s^~j<8sLYV!}LiP{w@`~#mgN8O-Nx>GX5;J`;jhPQ|Rqj za#2ygAvE=DM`S}Q25@0~ud^*LEoETOy@n_Cxsap)5@yC37h-XH6f<_UYLqx#+E|V0 zX&rH>n?K5WKW^Cf^Ol)}99M@VOJ!Y@@D(%%Syov5=r{!xbX?6}{}57#I*wr#*O&D0lc2 ztopqV{W1EHM2tpeZK2%{nLOgp5@QJT&(GV1x4BjzGp@MR`2bM96wZSaYWvv9MK58{ znDU0^I2Eg#v$MB2zk9+TOlUdcFOCL3d1dNCpi=_kKuR$s$ZX{<+eM52>X-Q=iRY!6 zwYf}S&Hu8ik9207peeQbfpEL zNopBDqI9gRbqO=*W|NEL4jJaY+f!J8SZNO6D@X^Vk)+{6#HLMx`?fDNnQ(ryM=9b^ zN8gh;YoEt*KP$EGaBfu>9zlHhnvlVA3Oha4TmsI!2K(gOi->hLErw^`c?F3rAO8DS zEcr^?KIs;gk_uzs@xy?Ly<{KAkNZ9;WT%|KFOzuUsRCXtSE7XK;>UA+M~{cu2KgR^ z>i|lI7x7xaoIjk*CSRlyTB770hm!dN?FJ14L#YfEb*Hkt{Fp?di#J&1mM0OFm!H?$ zE2byIgk1X*UpOdH;M@1wOl|{-k#K5$MPGD0!Rw8l0YUGx1DB>-Y1ql<*O+6*CL8*6 zp2Mg~;Wn2|s5Il*UlU!gcj=eh!t1T-e1Mb9S$KRA7-^mBM+@;(2E4T>2osN->Toj_ zR$-V`Sh9v}kD`h))oY_gGi5Sd9yJG;BPr>C}o6S`$+?FQ|G^#HrO-koG2jz;XP#hd7gRz;9oriexm#bWIeWoRYg^(E1O;>jRVHn&gMV?prgrVO4r1!hR?Fv~iOwkDm9BQF2WSlS?)B zPXURR&M9hDYgeah(D&zSwo_Wi;JVzwHOu{ZMa43^S6^;4bvjK}JDcNR?&OhMR*Nfg z#zRY+`((Coo9{x;k36MdSe-9gjGnJT6d`IlZEQa64GxG7M$^;N7XTVRhv+^C(F6Ip zbUhL+p`JzWT0~_D4x%gcB=U!9Q$ofb$K79GefLkZH+Kb(V1&27+K?pdXoK>2T4@mY zod1YxJTZ8A^eq~(ZFhUsL-5nSH+wz@-jVrz`ue+EpD8|uk3#aJ10A&ki zG!Bjunbl6;w&rFY5Y6~uSEXc4U&^uo$lp3S8N<_C{j)oNrLsn0u*qWUt8zG^_O3?g+}kfUoC9Z*n()=&=BUkjC$Y6lCvd*J z3NKl{rrvm8RI~5We~kXA`P!$)m-xlU=5=%T8IAeT$y9U~Mdl9467jEoED7&~BF4Vt z-k0DoNMlrUSVPG#U;cnMt^!1BP7cIFy2bASddhg|DVrTS>S_LzQO>E<@!!+)h@$N0 zVPjqp0YKvUBD?6U9pVgbS?K9S`9Jg;%pI&o%}{}PQH^X_mPi`Eb1-Sr%#Is_2}8WnVFdNZlxW!M)3m}0~Di%yq2a2=T^8>)+@^A z00QhbSz1?;mn|V%l8Q?*k{$!2sgr(eu4Z_UU}@;upkyRn(&w@Vix~9;!h*AzwGS_7 z4A5?MvGnq4tqJ0S<2Y(V3qWlbHzL4`d9uk5$+}HmCSnMp{G@v<{1|!OTo>7s-Xkgp zU-32NT;=EAo~TToS($_bCNK{Oc9Yr)SVqu?-PZe&+C2J~xMkL#K787~|7luLQE@M5 z52y0v<*C;2sm;?2GQZ%_y1Jz#suMPf;gc@Q(8HYjiPm{M?H^C;a5b)!KCVJ`46PD1 z3TCO!A5Bf%)o`DO@MOAmH|AoA`K$k~Wdm@8)&kggrEa0wIRx8q7Ls()(c#MUK(eAq z0PIo}fsm@Yb`@SNwska#Y8BIq!1MLFmA%l*L4e&GvEI_&tFxUzsyE>?%KF&rp^U`q ztUa6%qy3;(t(_T{TlKl(^w;s2@)pgYfR%h#Gl_IcwQ;_l1= zI=ZQ#{2DUrOzhX+u+{*i>QSWNQKWA|V9ZGan5O#07+^E;)Rso2ffHBJ0BqVkjY!^xh zX3<|+kd6^>;q>1cRFlbjmgGyPXdRxEK=Q*e!5ZWsz{g8~@y;`bhhjo&mkw?_!%8Bl zG2cDt-aT@9Z}w}et2v{Us(g?TP++nQT{5(D z{bGyHa8r8;IDAE~#vR}reL{ry0XWLcE({XKYr&)V4+L+vwx1Ryjn_EABY^R#36dWL3FWtJpzULgTf4MT zx3cFj4hYjEhTUgG(?e3T|0q5QxE~&Exk@GAoB)XFejHYt9S@VPGWth>MOm?F@6KCD zdN;k^xrz4$%>M*mHdYht+ywBLQ2Zp%L^)p z96<8=faX|?RZHdF!m}?oU!B=L7s6n(RvqLpPa-5ydBlW_E84b_hq{+0`N0D$fpOS= z;ri>@NQD(ZDaN}5VlDv)>rq|1Q61cQ*Yx?XHlep%wkt=|{k{DXOW7Ozzp~#Vc7k>NV;UVpSA&#??H&JT_(`@NP77Sq%QTwf%pWF5`Z}FGU2W5->UDQB{+>2OB zwR}pw*gk)$kl8c~qQ1d8RmF@1i`T~2OH_WQ#6%wPj4nkjq-uV$Pr7n(1m=Trz?nEk z*<6~muR4Tod|Vv*k@y->=7$_ZIftpjFfC$1uX?a}o*L`7sq2j&(n`;-q7qDuekSg{K-6(Q7TxtdlMxdmak z_+D-5nEUimW6dsZNG%@84p$k_WvlAooxU0FC!NuVuI$Y-rJDrO12$r%AEdDQ|3fVn zxyc+1$=1Q<1TkQV_uL{6p-xvu2GMqEP5LZYi2_OGMV~3kminL9D1W;6bIEU`5ApeK zNmRU%n8k+q;9+@+e*6yd<2X7oPlOlGwSiTEG?~&bTUuLA(L-xMUsFGeZ?y0CO60PZ z9Fkhmq=t-40evBlD9tV!HeuhIGQQb_@)}b(R!!XoS;Vk_z8=wJ*@CVvV7qXCz)zTy z^A-$TDP(yBXV3q}*nkI&0gjYVFiPkPl**6KnI*NXRY%5YoA8@Pp0Bu{jcAvVw?Q(W z^ro6a95D1~1^886o50*etz>Ll5sdI!c_LT|BVrp4X5DzL7z+D}O4urg9} z(H7V&Jnx8oUE##R`t4gxASDAu{rlqq%762H>SiB){rhpnxKRxZl&EtdI->4>y@fBT z9Kyg`J1EXgVUB%i;Hs8qNQSzZz9A+9O$Ze!v_JrT&Hf;U51hBfHMOTc$kRon8Tk$x zFxt{bax+FPNX~tS@;Lq+4O-)k>5$t-uMXnbnUGbt-~$ow`>E|O)XDL7sAC+))l zor2+|Ddd}TLU>WadWTK1x%A3mnBeoxs)6L?9~V@y z2|#*dICw-@uh~JP0V?C7MDlbPnQfKM4Y*QfKdcu?P{$_%xdw9U&dA%#8$Q~BaN_uH z8ZNKCZBsA8Pq8Vd6IfWGqBvbGEsWsqr?Yu;{+L@~=3S}fxtW9m#=znwg{H4LL}M^? z7n$ds`RC$+SFq`K7)qmCBaWEAkvbS?^}e@ra=Mb!$$90;fZmBIeoCMIgy~o$fI0Jx4E6U3Q>~%#f6g4<8SH(z;Ck`7IO{1 zfGRTNdvuvmVychdp1H$>LjY*$Z#lm>3z!1z=e^W4#@DuqQsH^n$_D&t_(HurkMZ?)XzUp0|YFa*E zd?Y%M{)P!X3eN1H2{cguGs`Ju)lvRP!nyybL2(Fl^==%9?H>-a8NzI?NnCs3Gj5B& zf>PDIsrRqT*;N7OjjG0Wy2d$L0Tdn2rK{?9Iyiz zJmFxw_pjm=N#8ycm%^F{)mH{QnD>vgmbI>Jd5_8BjyL||T9Bq5&s>%b?P9|y$fu3D z*Nh7uc&x9D&6af`V%C|e#{)k@gvXUvO%9IiQ=t#?!P6o8MptNgC{Ox(GXKjSL5+Zt1+$ z0|qfR^;b`R*#xbKx}}J^2*`rF7do%8FXhWEevxvg#h4e&AO&mv$I!n4ln2|rHBP{v zO%9vf;%Sx$HJYIgy=vJTe!+^s-(4gwJ+se zUnnyNb=&^{f}+qvJo2ckm3b&SaZrkO@ATQ zzG8sb$x759}udS<1c}1ixF<0XG`4F6U@|fYo|j;B6L*%tK5Br)?K~=^wosp z@wciV7900?`e4jQ$q^)Dk2X%WC<1otv>g1hIQtTrPc^NcIV@9I5E=&+#5AGRFOyb5 zRJ-cF!aF}j42lUt#*?6!Amh}vRXCO}6_X#T-UkQj!mNDvjUs!12ggL*`cB^!vQ zP!9xLkJR)=)l^hI5E0o>`~w0gXDm48xKS$SL-XoQ!|tld>(660_xA2WrwoV!@}-|$ z8OGn@$rwaXt?cDcm(vTc)2(&cV+W^(%0VWuDWEP?yjudFp=Mk-MWLy598;ZuCum1XyiXnm<&yP2-LG zd!)+mEwn?7S3{=C!zGA~b(_`Eo77T^9_5X}%&{5KfJ_LZT;m9w?FQ8(_P$>M-6NXN zXcSj1rg>0{2FjXzTvm5@YIkJ%(O&8)o#M%^2qbe~jGHy_UfWSxn^E0Tz-__k zsJGwxHDttL%hZa93PKO5apSy}2O-j^bls z{NVoXWxNG5e@Cs z50DcH_7^@U&YEc6WP`_P_iJ!VS9jh-*IOQ)nfaJQh7%Ry1xcTlK|rtpwGOtYKNJK- z{1<%+5@67`KR$n1ZQpDnCntw+4-kQfewMO;3+?3wE4D}CS61VV;ocpgZO`9 zmw!$ETNe8Fauz3|R)bEi@#tGs7WD5%!0e%oZf2$7@hfX`=c(<}~qPD--` zGGD~@FIuAokL#>PG|pfq>ULoQlx&INJagn5SGfpsEo~y>r5Xq;sWW@roQh^jOd8{Z zLea+||H-jnzAgr=_JyLh`98Zj4|op!ApcSgnVQmH+UwA9qZJPtXc zX<)!2EiW%G=$#F537Q+7oD7RWEi?94W-^gCcep2BZrEfCW9o*kM*A@eDnJ8eE!uK> z`1?R}TN`wI4CMIsC0L>Q18Xtq*Dr*>=tFELArKrF3U+p9rRrtMG2q&KN&tUx?o#m7 z;&7YPr=TGO{l>@ z;-~+_g_46qcRmPqpcO(nsUc7yyg&eS{I?=1DvDht-F5{UX)&zuovoY?G9JQUx~~s9 zc4(km{>mU6U&Z*K=Bvl6aG_KTovymojr)`3H`txmTU1nx;Demf6vm5MaY9UZsx6F3Q_ie3Gq4vYv5gG~thLIFui4m*Xl4(SD%nSMgQ(EI^kS;%gMM>-M2@LO9( z#vFvS#1Igl|lVxH3;pps`&8G9*v<2!4gU9hM*Yah<81~Ri}5(-G@UQd40i#g0lPB!&Q4ujL{ zg^*P1fbj7rucyLk$6VE;qX4Q#GUKDNKsc{rK`39b_iWb{;Q zt^B4*D?*K&M4o9;Jan92>1=TXTnXt&Yb_eTT9OF(0y9}>!LOV^`!rA9{b!L@d`CIm zcT(+EULbs{LyGvG*!!>j2`}h{cl;X}9x~axFN{{%Ldp2=PaN^--WlXsW(98A0#>?M zk|b!0FevDGPS4_4c1X>k(W`Sb>V<=bL$R3QZ`$UVd$?ZfvtP zCBq5EHIR2icxPr#LH2=s_{pUT%~w=V01FEXbkuj^Di{YEnEPMpW0=(W16Y$u-G@zm zxNjwNYbZB2pwi0gqBC`)1NuP_*nU=HnnNd5;J=~uYRc6Xak5Xz{DfLOJ!OWa98=Qo zJ>u_MWyH;315Kt~??$2nMc`ovoy}oflj?%iDN#A-t5qpwQ$H~oMzmuUM)X?66M*Yv zkVu$ZJf{@)I@9W028}0%*I9ItT*{sdT!|B0ANh2`>9CL_zKmjh{1|9OlD1PzP7ONg z34aS5Vc*gGRJS#>CH!vJWG^~hXFiFB0kPpVdl+|5WYKRhpP^I*k|xCk^S>q>9UT>B zmVQlRV`E=lUb0kNNhEhz7%{s+1D_8!R;UlJUufSCxGxjH;eCV0X2RZYn}ts9IIwUX zW&&JTf(ZPC;ADL;Z@jC z<#$gMma^4kR6QBP>7xqQ7@3rc0QK(?_Y?e0|4D(c(-}1q2$8TcPcoJ@088VUuKv#O zA<5fScrMVIYoQWA()w>NEmAfKI`ox0L)jK`lw z?Qv}srl%8rFKx1CZW=*C_6WZ{OY$&bTWWS9l!hr>gaN6%0HKX=g(;*Kvj%NcU2)2& znEt%4-Nmxb8Xg*F=Cr_=0qxiop}UaFT~IYIdJlhD*nCvoHzUkwl-14B5twZh7AZ%4 zcolu*yvU7jC*{)4w_{HCBl)2*SSSzCiEP;)_(MSH?E^caw5_PJ0kU=pTaU}~B?VDG zB3HQqykSNZn|NB6c1GO{u8#Yaw^k3I!||H2;$tlTxmJz7Ml+C=eMwJWoPfy}EJ^LW zI0i?PCfUaqmx?C6kpL=y>}+iEv}Cg4$+D&bc}QCnIy%d4E$|!lAUd&aQe(kh(_HGb zI9qACK0Ys6rIkL%ds^svGiA0`Nlgk03aSZ$hS=TPOJntw1~~>6TNgNc1Pi+paeNLo zJ^V6aPYRzm!1#1%yS#H2lL-Gh^cL0%Ir~veG$1`a|Kxa?Y9O+9xubVVp!(Wd3b}r9Bz|AF8{A`` zdO5qo*%F)A*&C1@<*d;&@KP_)?M~0yy2B3=gPp+k#4ys|(oh@FQ}{heL(a6Cmr1gZ zeF0g|!+CrPqn?U}rZZ`xTGYTGC-)E^ib}fdt`+|69(czEc9v^$Ja!A3FOK4yBr4{2 zKiWHTWlNW!!t5ii{Z(%Hlq@SJD;s)b+Izpdzn`wTLJq2?y5wmKPxfd@W30(ZPv*YrL#2*3J!)b=G&hCcWx#+%{98Esj0aEsR{4V z>A=_ZhI#LXsz{E3P>h;1Yyw3Sih>XILi{8a^Qfw&m71CwL6gHpcN78z1!W+-0ep_( z_cydK`)nbf#beu9YV&RAA}#!<(uSShFfBvh z{rn$g?-*al_r;CIw#{FYGAg|z2?LkUuWL!)og`&yKdtj^G zlCC)oTCqOvWq&f3{p#n94^_Jw#Q_%6?vAg4%_1f51OYVZ?|A;uxqm8Otb3E~gv`3r z_P>#G-fM0r|Gf&hx?49c6J&Vg;EYn>OPN>aXwqlqVT?A(Ml&%nNrbXi(Qvbd(@+TYI7pT+s5rwqdp;)V%Wc~O7#R>lor8Dzn?Ytqv5GF>MG&(bf1@L5}z{Q=3?p>=L zhA#=-Fs@gZZ_u*5aC;9=i_QZ`*}l*d_Mkh0kXaADMeSAIT@mQ~YkEbav*T-c>MuFU z{Z%C+%L_E=87MSV{L}o~!5Xhkt%Oa7$#kLfBgVlSGHh6XM zpFHfNA`0E7m$Vt`5w^Wj-vqvLGEH>Iz{@tbG0OGD13wD-#KT(1WfIPNOOVwDa_8z- z^8J;q&&9!-t%MCqij>vtXOf34-RT{69|1Cbc70xbuKy5-65z7Kk`a*rq+m|_UTbmF z{xp!snkbjJCP9U{`DbHejp6ZHy1NT0y5)>L+OOte>;PTp31V7;V-(qqOxQvUM2Lqs zf*y;D|5F#VD~k%71`S<%sqtc93B=|lLcDz;%Xdh%LlpnrYYB2^fslV04wlwr6e$bK zyHaM4=8s3NHUq~~e`lbalH(F~c>}fPSx2>V6_~S7U2?sgrG+lh{|<7*U##zq{+s+5 zZZrjj8v^;M+j?h*8P&BVQ~jDgsWeF;<2K*T5Wjt@dLrE4re_6>sl6O>Nm+i7=!;%4 zL3z&ikMt_*FG7QwYlvNN?byp3GW0<74dM6<B0CfwQC zITK22t##E78qS{+tnVp1jwpHht5%=|Lgx$QMqMV`ahHMSh`mGuHzU87o?l<_@$sqK zIJaGspl%U>o|j6ctf69#FPs)A!am&m<(g_9GHM<^#Er2$`8awMC!Xu%KJys_V2VZskCJ0w>bat>^mqunjfaKI&2hgMAteG&R3c!$Cz zmx5J0=cEt@8`Zr#O<%ar#&p^?TK6JSXX?HSFaSft@PHQvn3C!a5;z}#Br7lw`GZty z!dt;M50@!9*-hII#Iz>rczXF3(NGb@%J4T3+MQRwSk@%RwFrO|4Sqc4I19~i{}4dZ z>o=eILW0+ura)7)7IFx4f;ehn%vlC}ykDW7jyTGkdY%bJdO|w@M89Mch%`rY?#4?jD{TtfjQB9(4d#b13rrOA=f|F zL@jP>Xy;T=rl5ild-$LSp$`Woce@-3#|+J*h)7yP^ox|SdU}Q9s!%!(`iHOPg%(JD zNIQotJv;q^p*=BtNH$s!p5Tr#yau@T%LEO`h#pKes53;v>2u_RaXJC(>jC2>{O^Y= zxR{*Xn$hsdz(29+quZHmc*>n;{11%j){*eN793`@$Q)@@mCQ0mPp4fVvKfWHE42fm(RbU;sfZGb2BvR_h(UKj$3wjqz>^yPlFdiL zYQo56BCO=Pmt5b;Kg|xyy?nIkF|D>|y?U1dQVS<1Cx{4>IU6r8FY&NyL}pOuyn@2r z(t18W9yK4aa|x0S=Jzq~$N9ewNZq^yRFUYy*TIo9f$PUfJ?KLmXB5wrB!M(S9>w%$ zLg904Ffk3DP(FucBTxp+N+G_2Rw?JO^&r-(72kw08t`h&*nS`Hg> zFz9>JoaMl@ZfLb~!J=b3qSsRW|Qs&n;JI+8h4H^)}Smma715BmFDarT*x!gx%cTU zgeb_65r5cH(kG&?^(yATcWZN73-Y_PSUn5zGI#@!X(rGJ1S^OfI~dN0@joQsO%zzX zb^zh09zuM-yW<_OqxI+TUxrySbTY~^vU>&d|6!GrE_Z?h6o|brB9~x2(#`L$jbh!Aq$zaj zCqOoS7lwj#MSGhi5mOaQrL26Vl%xTdw~VC0;74nCj!F?(OA?_lDgyJEVV`0Sx&+); zzb725H+g}k=Qp+G*xc&o^ZCV@lba(qUJlX{? zgng#W1 z;#4%HfWZd;Rk`5h>X%io8xi^coj z%|Rac2MtZzqD|-iG~x;i=v~kE1u4|q?HfU`AgPlMAzD+wEsmp5T=%dg`cdf2QoIGE zZI!dv6Pbx})}?Ig!Dz!g@r}QNBpPtwgMQwFDVz*!>|7x*EPJCRf1@dXy9|0N>`0=K zNTD%8e+3y|LD`XrTbI9&|81roDaO=-;x#<8q|?NV`T zaEn>sYR@SmVQH067(LX`-BW+I{hl{&eb|^`?jqg%uJ8;BWv=1>O%|+@Oi7fh?`r;? z{KDlOyaqgSszlU#eQ}#@T%U4J^-nzpsH)`sgEW z)NQS@9x*vnL}C;GGsNkGr7?>}S6gTH%Wdk|+-6TFy0fy=g`d(+(!WS)Vdzdu$hPUC z!O8~aKRJE%3~g9m_`h1=ley;xB1j}sSD#PC@8J?+QA@$>4sA{Lhqdh2wQK6Wf5Gqo zJfB){C5a?r{H{dZaB?EO2)Di2IAWs-yqQ^5)3ef#|GfJim>a%5bVibS;t1fskHskE zI7{_d>Oe9NPaNHZF%AdZ{lzYjP5>$l; z^&2V&A62CrPA$cnMuElDiWXIxPo;#bB|69{0X#_9AUbaQ~LvXqfjv{&R;P;xFZCgY} z!l+?ORvi?+YO4fCZ=_ZUxt~~alpE&FiQ^P9^{p2Z9iHdWt-39pn)}sRy`aNW#eh4q zgbqa7Ja9F`eIMRt&lEd|b4DuUxbtp4HqvYt`t_4_MtiK)KtJhbuw`;EkhAX9Y^z`=1*XV;O z;B~4W!Bdn&KY;21*i|=M*JgCsi2r=-NE#1+N%S(o^Yu>Lk&AO{?_wt+CHg7gJGeRQ zS?DF#<$F(+Gz!ioA7Ua_+h_783PrQMu=#lqC4?y9MMczxrGHLc-P}T?i3$D$jXR6C zLw*|W(0RPsi6DR1t9@YVT_3LMd;90;O3BWS#r`7fO9H(z z1fB0~Y;3Hoj`sH@sJ_qK%u{`rvHU7x)il9re|B-dECXYqP7pdKs zQmDFzlX-ZGzuVgouu!b{+YA^8DqE~(<0zNzd;n0b(5?9yDWH?#R)fk|XG zD4<#mLON(5hy^av9TatdCuTa5hhD}+#!ALHc{W_%k+~7&zEJUEau9vi36%-cS-Nj$ z27*Ljw?C{B6{f7YrA2BVR*D*9-kOsv;pp_#cEaMx*s+mh&lgtb1ok=JH_GdLt$B2` z4dp=C9`3O0pt2OiIq#2Wp>s7wP9w;HZwU4dHJC^mJ-ys@pX?JljP=_l6t9e-ioPen zK_(YKNb5`8Z-TwMG3xga!;rxSRc+4A^}SL$;IQ{#1hd2JJo&S?Iq!gYo>oVtd>Zq1 z^7XU%|9$^|_x<07|IdrlM6nQxnSan9V6E&5fH}DWHK`Jns6;e5)@kz~umztTXyWO` zIf%7+?W6CqV~eY!qXWwPPJnwcw}8NHVt=XbRxTmp3e|9Yk{A| z*Nl^wPoaq-`*0le#K_eGkz3i-@iN%+y4~%~QL~LRH&@!F^98cKS3`O(JHA%1H3jVz z+ zmRTdjVgHOYtk@&)>(`_3>Bv(!k~oK!TP1GiRBGG%XL3atrE0RMjJ9)ruZvikvEf?; zaKLGz(dc*#t#D(tibnZij{*03?}G>8-6F(DFK{U14tYtSYHNqXkbyLt6Zct$U_E>IvoS$u_;4*@>+D|Z@n2+>74xA6%E{SYji$*gg1$9g zh+%xIGgc6r6hm(B)&-Q>zRcd_Tj%h)d27!+_xkC1DpyxjoNjjUlaQj)UT(BEhJ}Si zaYnd>m`8Xe-!K{d2S9y8mS(FOL%Ia{!2gTMsc#PBT=TPmRW$8p8?H!ETl@Jx($Lm| z$)WmCS0Tv_3)C{MyB-rxu~a_{o0Q0j@?UHb*H;EbZ8x?;zPvy~rq~MFPo)?!$oEpwcMxmQKX>+du9gl5`KihLEQ$!7CM&o%Td;5sL>Z&S>LRXa5r&yubkM)ex=}K7N*3BbwO=tFl2t8+Dx;| z7S}wBj?i$88mH#PBNN}KkG=EGe{)bfoCef%kZ%HMCH_f|C@>F9Czvncxw|ygcElvh z7*JfizfN8oWj}6gl?w~jHefImFA3jY#wx7}vkDHV+laUJYg1VgTS^*K2dAb< zu^a<%b`6ck@=ghG%9SVlgy&Zd*_9Y`rLypUe)JI?AEA;(KZrF_;&09v4**2LE&J+O zok4ou8P%KJGyPcIqjC#N(WvhypM&xoQ0}zgI ztD3d!x?R+lK)>VWT=Y5}WIZS1t?aV0XK7#&nd$5T!OyWV+R-@GhL5N13NCG?jca43 zY2l{ykc)gV4NhQ^@S{~M?lNPTc^HOkEx*=2>Bx*G{@p_?nC4Ppk44e_%;wK&CSK}d z?cRdnj*$Imkm0!&Q%}>!)@Jkm@b>`~=vzn{WELoxP4DlLo`x?_-4a_L^prQoVnu*- zJzYw14fk)!nfbknRJwN{T7skGpL-|E#2NE4kHqf2Xi!O0y$^d1Z`q_C z0kUgN6q`5Dmzl^+ZDqx}#A8qYUXi#hpoip8dhVM~+A#6o`_pw%%nufAxrlW0)xwK; zrg^wID?0X_kr5=OsTuky^=_Ub^GaZzN4GF4TJibn<>gfSj|hLqM~QPUiGez5kfZ8; z@AMSAm;&UJ7Nj2(PG_GC=z2P6<=KTZ+({a4AlV2b|5wjf&?}o!;)U>+$NSeLU#65X zt_NPnv)?FRt6O?q@Qja6^(RDve*9!a0gMW})<}a_KK!BnrX?gKBoV1K4#xuw4h`v> zI0GMEdcT8be+Ekq5~;Pl+=DJC@eQeI+ajuyuvqLII1!YvKat+@`+#qntMd!)UQt!U zK|YOGpGgLeND`ey6%$;Z4?{C*I}gKpfw}H@T;T>n))9g|A6F=PygI zu*vveMw4fzr~bDAz->}90JB0U=|z3gaulglBXM+*U149T&3KGGydVZ33cww8KTvN% zs}n9g-$j%z_$k8%r$u<3&>&Xc-LCXy#AKs;Er)P|#FH5&={recRlzB_3DpkOCz=)q zpSjc0w=w2F(bUBLzp(=gKo;r zW5i3LYuu`v$?ETmTihnM|FW?P3Qdr8QsiNdx*i}#AX>S!o8@}`MW;pID5}EgYq;8{ z(#0V7a8oXEZPvhX(BhgDO0GT_6G+_MD}|WU)c3vqA(YuKRKy+tNZvE!O;Rhr|8kEf zd?2!9l+mz3i^TNt7k(|0=qn%@iqb~IbRWgLm{PGsbCiU!ocJLf-yL@QBRX`53RD1f zPR>@QBao?lCLC*EW-UpVJt*NVE`OWi7iYk{@jmWjG1D{DLgtYwL|+T)-V(RLg?NO6 zEU`OcTYvgDo2I$}u$}u$&l72URT^SHVhr}{4E8h({osJ1)~!l0QW->RTv4Lbw6Rk7 zyue}-kvyo9BB4pl37=;upb0 z!sRi1}QvUWNF_^c27AZ@-SDbEZYncaEk#!wAaqj-51B6+uv=jUy%v8k8<8} zEwC%LJy+#`&!BGLy~}*M0;`*TgnbglhhzvHWb9{KcnC22DQ(=GsstLjOh)NiwN=V8 zedp0z#x3!*=8s~dAdt!H>AqoRb2OJ3jT8OKwLYN| zLqzb*i9Yi4=KH;_9YYhxGR<}9u#DT-%E<_AX?-Q2_iZbaxnI!EZ=cERY}|-?94?DP zoL2|~I^g)QNvhq^QcGwz&qZ^Ce%|2qOT&ZLx)MKZ$xr`rfbS0;z{k>hyE2}T0ujPiqZU@C`Cpz|Hwcnv{%rWuq+v z-??R5zqwPx7xJc-!j{v!Y8d=xdZYLVvt`)al*&nfF$ z#yc^R_r9eT+q6LH!rU<=v>0|!gxYsv6r{Ed6NUnheL>2yo5(c!zvu zv?gaB!2TSeRX`_}>~{&9Y+AiTrpGAtejaVx-PJWpO#p%Xp7N1=&g=NvwZI7z3{hj8 z#08HWVv!JRy?OwjeoreCo@n>7`s` zF?0$nR-CCyFEwz6?++hS0lLH#p6bo%`od?X3khP*kb8bUr2%i>m<7JhHIl4m93Y+t z5o$!DYEyVeG27%A&?Z0s`xiUkx2a|HH2##f6;?#sL`iNwJ}_wt-5}Q8o@uWv^Z)08 z_W$hMhVT!LlCvL3!}Ns226!k~DV$7gkpu)NSk!%-Eht#Dl}&9e%-kth)I3bx|L-TV z4klLM1}z(NcWVl6E>;Q_84DXLYj+BE&My=!Vm9t>Di*HdP7cmajuwvY6ucBH;!gHX zu4>LEX5e8G7M?a{7SgUJAO}av#@^k+6@0cgakr4LFmnRT!95f#9Ie1_W@G2!Wup)R z1rh&yAi24_T9`N>d9R%tc!}U>VQ&?AiAI0Lv5yRlF8q4-hk}UTv#9=YB`%MNNdD}T z=|E~3_^vSW6Zaqzy=tAYC}ZTE>+7HHt_DAktv{M=&(vS8R^HAhoVK!foHkK{35MvT z*rEZ|SW}!L@U?}$J->3T#X_(uFW##sac-gYLwlYr0$hVk@e>QnH6_*DN1T}Gh8Atn z*Jsaq{=8MION|bl{d__O?sEa}eue+mf{LN)rlS9#Np_T`gaIF{Iw(l?1 zerP#x5YuErb!PLK?L~g>2ujZ4Syda#Z;YJzIU3uAX!IGH(U-8pLO+t@F|%6Spxj!#qz`bH z`PyYO>TA_qr?S6(IbAG(W5|9vd{=HU3?mKo#4Y zEu?Loz}DIbNpdseu^W)A=|hYydr7fH@j_hBnU4|6MSpgc&*B1i6TwIcTm-JcqNh7%iPlOMc8V zLP(IKHZwpga9)%9v64zL!2*cmR4c2rsL|r9-={!T5lp}zpJ_u8Qbv)))6X?r7s*i<7|P9<)p(&e zd&(6IeC`voD%{gpL@93D`4QE;R*1vLsqS2mMrw*hRQ>DQ1gUMJob7*gGJ{kBSlMt# z4UtzyY6ToFMK8TW6_bF`vEK5VtL!;W9-(=l6x0tLP7b14=|M+-wk!l%1i@$ykyuyS z*8AxmB=2GRIoT@&+T=P!)^X`=g}aYP*U!Xvi_(goQR(N%an6jD5)uhW0u2s8m>N~# zr+_=Ph$C@Nm)t<9mDWguP*o@^Qy|&Qr+9gmZylL&fCM3?P7=WDfH2}m&W+u1XS|u-_6Y zOz_sBy%=OdmE_~>bktJa7E~19_7o3F<@h9THe4WNr&NRbUK~Z7O5vdc* z4e_jaQ&W9s)12a%;1OTQ)1P1+vnp6#5k^EIIiIsU0?E5Vrv`QWjd%6R%zm^aXv05^ zVvl{r{8UA~5r>cywfEdRh*PGPO}z^5O)%xRZTxZP2lR2cu#$s3^v12DtMq_CgM1X+ z(Ql6P9Q=J8_4wrJ-o!KmZV<0ehN7gX9D*Z4{fm*?83cXsG={+$FExz`PN>N_V$nMA z+6yvI3Mlxufl(tAO*1Pq>I^QjE)UDKMm@N=Y5{jYELA+$tgmv_T!LV>mp4qukn_Cd zcQ|HwfIN7G&{5`&j@L)OQY zXw@vPtIg|r+f@l$>C`(Oh0*5Okl0&^XO!m&0T40d=LFhYifT{#``h#0S0#2t4CKe( zm8SSWJ|`ssfO10WFLIEV;d=i;%LxPzSPZ+u0hd@`r_}liy=Lxg%2B_Fsa(! z4?y&B%(@egVUHpdltBBf+%6wcsNrKQU%;nStn_LO1d%q~esc+xIOaJpavfVGwgS%= z_ZYEkfuXr%Yb7g`pV>CKMZ?GZU?*SUCOq3FHi?xkYp?vd}m7>sPUNR?N4N zOeFI*6t`=0K@BD(gGVJ{{;WU2V(Ar5K#F~-kd=iyoy{kAnkCA7~pzWgO zw#~e$yElWE>>x*Amhi!DXo0M7__3MGeCA1+fG0IxY4CtQ3o$F|zTs&^;ty?QJ|5#1 z^;rz!M8Uw3XC!Nw5{>Rh%#Eu1d2=sLqGD}tP#lTzZcY{kHTHL(&i|+~HFfyU-Y}N= z2EyX#2XNjl9LzyHFd^5QkTzF^0wGMhg6`!+^%dK1&ICP-!j5XalDv5I}^_ zZHolS+EMX6st5FWwU)wVYCAW!^@`u`mlC9a_~yxSt5B6UU_?_0SjTAh{#Ssu+b=ah z3DNEH`L6;e|9}})VVDCawO5!9dMvm_xGy$}%pxZ$K*ewzbyBv7jI|jcD|Is|`iueL zR*~XD^Gj%t2fO*s^(qK)r1k(jKpcV*k!3)Evv$eOjA}Y6@-RD>utw-{o;W-zO@d^_ znr0#djlM_$=UJ+{>k$<n{kmWt8#m(J$wDsv%R|Zjcux{ z4FZ1I6q~TKaSX^r9Gxr1~Q> zk!%32f;XuR{gwL^=H7ccXPgMTGFKXwerp&jZvcilszZSL???@ELes(n;b0mn zT?M+Lc8G_BA|1nKZQ(GO^wst=)@ywA?H?sF(}V~;|15n}A^zY|dWDw7vR-$H6>wk* zGI-(s?ZY4KBp$2yt~nsQc^PjJ991)|x)akWkY7p0?6jmnj2FkLk}%S-UctyZ%TkNO z>XIc8>n5Hj+%pTL4zb1y{ft8{Vo3W&^J19>MdO(I*Eq!xFzV!wWTNhKfF3bosN&moho|N>Uw=9<(r|i4GM=IG5@je4f;B@y&O6=DuVO1#Mj@3peFZ) zIj5dBILP$`l2cPQplPOOft5|=c$9Z6#g%CGS4u7~PoSrKbs@I#)M_^~s2%)GWrEE% zsjP!ZeB32^@jdt9PPd}p&KI*=e^W%ADBb#-%5Mo^LPN2*{3-e(OCZ-ML{km5gxL7K zy*s|&ifY$Yx_2Dc<5SPeERa|AcFuUrk+S;v1yd-?Nd8Je;@>7hY81nwP5(X%jiZO# ze*y2>s5D5V=I4$M;Iv8Q;07xDYSU{m zKc)qEp4eb@lO|AHTI)}g`%&tRlo|C~mlBJut3}E}!QwPGY=m#7LCM3oYHxD==VI7| zgs7wN*5VmnhP}l_3x3Cpi`*|?dNT-NNqB^`DD2SRgK4@{_)D~eo!31EHrCMNlXP5s1V&Vy`8Eul03XE0NT$opUNsr85)JKeC z?#($(-rp49DcP?hDVC=co9T;pEu@^@9aMe96_;{W#=`t`>l=|oh2pDzFY`8I$9EEFXgx}9Fa7N+JqP_?FkdqSWt9g+f+w>3yeXyp`_vQ2yHFq7=y9R zw80o*jBO21?cyLEy&`(i_9sauwqA7{foGat7g&&x)qH??EP8jo^NB+b}_tpk~-yW*E zxGr5`xJ6IkxUTT6w=&k_R-YM$#u1AO6Zu{izPcswv)37~AZqQz8i_F-gzlUXgn~YW z1kJZuCmdH2tnmrBb(CKUI(BRvXnP~N|EzX-DcRXHwtlS?mJkcG2GY`oa+{@+yVnOo zhT=2%Ob=dw*Fi8O;lJFXccv16bOx0u=64iPm5*r8*tgvPbH6N%jHl83Vce1(yRRow zoEe%ie5kylkDytXt$@`oiYRl=><0>C^9hMZhU%LeglNQAsR#83e2=k?(-GD6HDlNlk$g zMKfB(r*VmdkuqlsBBo!+*}hpNvaFeeduz=JCFS(yF4&vwvoBSFQIU9_MbdtUl$AeD zdXr8Dk^>BUNsBB1Yhj}d>jpC1Cja7U`FL@3K5m{vR_k5cd21Qc@rqG3nk5D>9k2-) zv76%LwRl1-iu=r{>jZpQ)Ahp&SnMi8(NewqSVAl2-XiCz@(OCB9`Jm~_1t0&`?PlT zBZ>w;bV}mdi<6;YlPIJGs+Hp+V)c&KY(`=!Gfm-c3X2bWmg^3kzY%36z-LlQ%67gB zE_Q>#XLPVTn3l>nFUTPvZS0 z!lU>KwB`~X!+LoCmMBdFqYz&XVVE!T^b+zE6Gv)UWYd$ha9G?SC3b+D93*xtJ2&*H z2N`Q4yPkr?#J2?ow=)RxjC2E}^tKGPoXprcGkL=eA`|7#_Ze{$bS8o!9Cd4Rsy8fe zRI+J>P{n7=Za~W)uMt*ATrO6@Tx9I6uXR*ehe$X?ilC2?;#9|8@;*+X_#&n{dqI%mTF;2gH~h{#J&_H#eG#AvpCFuo~t>MjJeg|Z*phimd?uRqO@mH zVa2oh7Qbfo@nL_LQh~pBBShU3!Lj>hsT>th6 zs``Iuz^EdEEEX%KmTsM?m0y_{*DMKZJ#q4yE?Z==fW)Z`$+PkwbflCBLg*FtJ3az! zz_nK*t(6s_#|lF>Akyr|Nb<;h9x{yVpq^+IAs-UlvMO47BuR+qZ}Ycblo-6~*c<#v zZ2b1bF_`w_@LQM zm_z6EKcT$^EuK0IX04Y${s8_~re3+M^>_;NQ!Lyr3d;h_tW{Jh7q7vGo z5Xs$xqEzS7Jk<0XH!;p$`o{F3K*qtANlZlrc8iW4(5KjdC>s8j2~Rbk(k6a?s$Z~Y z>-Ho+$f+Ekj(}Yw>E#-PaXMH&%*BA!4lL)J7Rwh*tm-ZET^n^{scuA2Q&H&|8`m5_ zr_77&ZNZv6yn#%F)X1SEdrGm6#`DrmD1%WNg9_>g*NM1a}xcMv-Yh(fWpK&x* z4eKtS)p@Kjk|@`Nnnrd-Zzr$240HaULp5m6~Q6<>HTqRzcFFrgK@|VjlNqfpW@P zuS9`}R5<3Hylo7n;$LTr;g8%h{O&DVrNzasYa0@%sZ3nu1?;3TauCFUPw{fw70{gVt zec_2Wt0qy}Ra^08YoeOHXiK^4HLZ0%0`=x^boM>1?x-=}jqfj)%|km7C#mfz18zo)rciDd-SEdPcWJfDqASER+p29_7Zkt0#K(T~D@gIt z2w`s`QUBKLq^pO{#o*Y%NA20QG+jJhcJ!dQjgE<39)a_3v0G%dUO1xnHJ1DBvezu2 z+^(-drt0$WNgyeq2E_mA@|@|7@442m$Tdpf5qKSqFm+Qgh7*NY;zp67JeR%1Bf>-qa3G$seXs0i9@ zS0ux;`{mWI4DoK{E6+C3lDx@BoU_!@Kex;{7mjhIO{0p$YY?(|SJQMW2 zSbM{V!{ZSwV1UY&d((x@`%1PM&F|;BmCFRNGxb)=yeTKgbLH-vtgBygq{C$!j=$d@ zi!Oa*!tQMUqg|@9Qbk+32C`d2_)7#%eYC0I{#+FsIa+h?LXQ1_husn!?y;1?n;X7^_*L(Pqg2C+aosh&Hhv}{BsR93pBkaWj$NHuT2w+xEi?) ziYk9dzJ4E?E>EbVy-6_(O|Aasoa(jcV?`1fG%m1I_XaM@d;j z_0eJpg+2^Vy@Lv4?VRH6Qo8c+BJ;fQm}DAx{Hty0iWt%gU=3;jo(2!@tAw5FceCPK z56Y?m7yk!43Uo1fr+_<|FE3H(3jU(^Dg9R_6v&E2l2rtut$aiD@{v8LP#AV` zj7!2v3`aoloY$$E96Q2gdeEG|!iG02dsm>8|7`S{jZ1Na2(CVoQg$oVlx&0Czk~$# zEk!N;bBYJ;s!LXG!AWP^ml%)4&85ZHwkgahTEMCzjNhKz4Ur>BjN6G;3D;*ys&>IZ z_tQWiJ)>Z|<*q@+$sJSs1o2qW*Xh9d^pzZ>4JU;F)?%L2d&2mTsGM@DxI?T@DGxs# zFTY4Usq8(7I4qeO)%K(EEg5`@Q=|EMF?{@#$)5--{+ye8E#qd9O4pL^Jv~E(V9*YF zQvrqkoK3`D6mD=|#k{lmL$BA+mO0yck;02`F~u!1){uqsy7rkI=xXDH3m*m*ez902 zzw&b2O537I{A9_d6sXOCIV?(dO4Vu$3@^dtE>&NGTBY?(6~{=BeLR79zIWE+i0Wui zxli63p&O%M0CvYF62!|Wvj+(=TTc}#ZvpKgXTqIQOaxNXO@G*OqnRR~1~r5w(iY<5 z@E9kBq?D7!zn+2VU5^JVS7Qro;V!sO0#eQN_ond>hhJ@>)%~=D+%Tefv?PwP{^H5+ zVw2PK_+Ovi8bQeYuUYmB$U-tN1Q*m9J>_+t^dZbj;?iKQRZvysX zuzm{E%r2sX5*6Q*!u`_eLz8D93p6IG7#&DSNEXxsuJc@#5-`jEV=GBI9b(KOAvvqA zzB@rDtPAsC9_9&|Gj6rBhh|(w_)ca3P-Bl&%+WNiS@NU`;(6&zPD->Zul{b*_~tGgI@SaK?oRt=&@%t zmL6#NLlZ(BOXx>cg`qSg=NGSv!$3iQ_oY97eGM9$b-;SEymstThkN2)9>t`Y z^1PPrsv@t%Qf&wuDjN|8e~tsE@SNxZwXG6!D{o`PODoWq)|(m~Tpeg}^zLZngh^NN zw(R~&wr4*sxF#{WBa;qyi}zNCO$j+oKio1`wC~)_Xz|xDe>LZLWzlOY-1L z9Git&s*|#4B^T%-uH-onX-0IMAxF9&>e7h`;iQ1bJklJj3GL7p zvC38HBovff|BlA?R-UBp1fMULb&=ggr5_FUe}*TsS}#_9`MBM!Z5=8T6D}5fFAe$J zFP~Ad`O&VK#34b0yRNX}d>87`?26z>@4|(%#^dS!Q=M2a-L3~9`*D|)3v3Aa|cxf8~Lv**&BFjQqb zzjg-k2`nygj+^oWbC2-CS{ZtgGc6t}{!m)}3H)mZv4;sx{7|Q?x?~aF|BJMOsH^I+ zPJ72wYu;K0@W`sjuy7&Jh&jZPX2GU(B<^uDl>p^UWlLPIW@ zc5C3_X|;u)PuIK)PgC@fBo)uHP9R&V^zxfeR@ie7vx0%3aL9i-hx)hvUwNCm4GMJU zJhj_tG>rkh#amP6p5=mB8vEuQgG;5Ve}}mY01s4mH(crbHU!-WQh|I%crRWm znW{|%`v4i&xPZkKlU?L71(Ko_+h@rG_O68D{m9wEiNWe>!X3G{=7c)Mjo{cP3ln?V zDXy(|&JGyK+bx5vA}*`C4ytuqhKI)Ao6AXx*{5>S2fv8>2Y0rK;=G3?cse7sJm-=W z)Z5KHfgakyM#`P0q9$D_8PKC*{4{Xy27bp}D;Lje{E+!Qg*GaW)vNSqt&yeSs3A(- zAZ}tB7u94pL1YWndw7s+d(?Besi9^g1MJY*mGSRxwa6OIraQ*{ZL;bY%YHOE2N%#t z_|%{AYe|zVM;~*a`vJRBL4EmLs1b)9)4f0$P<4~~Pjrik}oZ{Y7Vk zfsov7T4lA**!ZhpM;J{NIc4_h4Z5lp4iB1W0pifbBG;$Y!P&}^ya<8si!FstD$e=v zqs}e49%2KhS`9Ok5`T#bOqvnev+^xFGo}9CUkzU9In=%KCJa?h4UG>7XijPTg?rUf z(HWM>{TxYhT1R(@O`u;}Vf)LPw{73hAJDlFdXX-qXOLwU|7jkZVfLvf^CHv@da8Lq z3>v}eh_NC(^tp2BvG0|aWtd6Zf~el5NHRsrBq+37ug7dlASf}NSK5(?NfR1L=7uo%MqXtUgZ-M^?Z;rc|A9~PN zhjxQwL&G-NUEloQdlJHaJfF}1flX=VnDz46<%xRCX`ohk$QoF|-x@UhvGK}jB1^Z1 zZ0{j!xlqE3E1D(09v`Dj+x4vbA26ld^CiujWXAIMLBbDdf1tZHxxjMr!)squ*D|Q> z$?i1B(^haHz9QN+*L#m68n280kAda!HP)j3JMMOLpE@+zM3Yorb*m9DHu;?BUxTc) zT~oR#bC;Os>B#z?>pRu;s&R80^Jh%*UUZ$-MR3tiE7wc;1|PN-t?B!lB_Nod!(xJU z#9(1cKeMcaY>r{s*aI0;_3(e#`|`M&y6=A?GDm|lh7^hBLDE1a6-`Qos5D5EG?83{ zq)79iLW2w?QK6EgK~f2YMnY*2LLut6*Y2F-mIj{3^Zn!Z`h366r+v=Zd#(4H*V*UZ z`{dohq|K4{Zoci+k+XG!__~W44Ii$Wyvxzh?Q?E^c$Lq>9oMg#EPZZ#_#o44 zrU#(&r9_g8+nXj^HQIM z$zvwx#GP;3aVE8HUH1;@NB8~pc4*G9j^&XOQdLxc%jz*n-GwDDRy)gYbsJ+I_lm^p z+nx$oF1S}KJ4*NM2bC@h)qI#b{*-NFWQN7X zE@5vO?ipEhTVN&gR>2qQ9WM)SA3x|ff_5NDUjE$Gz|ZD$rwPntt7OsHdv^sdOC@tj zeMZ&>y(L*Pn@7%8H=6xq%PICLnNK1(wlW?UpEX5zPPyY34Q&yY)kVA7H!KwSFx_a+ zo%GEKT;9%HQ6DEPs((le$T`9_}-4QCm*&*9e2_A809~f zrP)6uWUFVuh!Sc2%q#l)+`e&kep0BrdvJ>CFzXS+MlG7z^m(3ccUtpVSsF{}sVo7v zm_)x%#Vig&r6)IbbJZBmV`*Svut|AU$n^Cr(^>{W|9irhB`miZ)7oYWtTQtnW283n z*hbYOa}=^hB}eeFl%D@0$p7}%g5}o9Qv`kYa9rXFVJ!&Yd)umDyiJ&9KjAzrUR2mu zV+EJ0@W^!LaZ?@rzPq|x`DrRI)~9`s@lblXH(ue3_4GE*b`CZkaV{Ytox1gJ{d~%o zC86)EpIU2ts@ym^Ywb8TRW_{|V{0a6mvL95q|0Yt*B@D!bMXA~&UeF{zf5&e*pM~; za@y*fv-fQXZ93=f`MmQ@(T8gDy5{_XiqJ1kU3X{4<+fZ468=zjJ@O{|0hVk=mxxoe zOrhw?kx#}%j8fs0z4jza!Td7Al4r{=m+ao+(K2i)3k%IB=&oh%qJ<16!#Hz3v(_x% za^2hW(8t}*Gkt<{OGd_i{1B9!ZZO*M*1}2N&(*3#&lN6^-m}UkOlZ^En0eLcCner+ zeJOe^w7mYsoDO+}!@l_a84)KNiNH%cO1)FuX=%sxT=ngDiaf8|TC#neE&u87Vh?8> zm(~qSx_HcLOLCffuxe-06Jf!isn-HTKID#4lP?X7Rm))fB(`4o<*I@MTgtZ-EBkv# zr)Hgd1rQoTlfU4MO+6a2Fo7U##Xm&qKE>SH^LaK=!bQGo`k?WJvjkjxB9CdofF_!N^%HaMIo4Ee4Q;12^)s zYHJAT86Pq#JUsW|zKxL&YP$R!${w#43b{7&v#oPssEc=oldj~oK%rvx$BHKoXU#Gm zv#Z(I=Y&$c+dQdZvs8+6;;x)2F-)4}bba1zzbzq5b)GM_DCbVgp1?R_j!kOq<&vu@ zB9r&XPO^=Xl9RFDZbqwP6K1g;CL*IfbE}SZ=jt6^J0g1NjYT)gY!6mv{>XCD z-v91M`WTjdVc(O5KX|toWaZ>Wuuk2~P^D8yXJ@`xRZ~DnTsP=2lOy+J0Q|T=v@f z5`~kjE4asB_uiE`E!5(}zG0JtPfS6de~Y-Qut%u7cumEs?~2_rArrGQiyT>|ungCh zUHIgob+S%Uko0_-P|S|4-Fu#`)Yvlj^x7z6=BdM1MvPMOWYF$PROzs}sh;#DkKuyL z2+Q+)awp|?m{hCH;1#_d2F~Sb+Fc zT4(V}Rkx5gqYU0T1WA9Ay;o@*tWbeoiY}TRKfm?<%4Y$?3KSl^f6t}yZPFx93EQ|x z&n>g$J11Mrw~6xgoO^i&O+DV_V9BQFz>ueGEjPcYgvtes4&vrX9VT{k-AmS*c}wmh zn3F8+eTL<~h`rxfTBI=9B5m&x!3(llqhh9I(mGB$tV|1Mc9uI~FK7NJmh~IY&UOdE z$u0(-X0N#o-->ve2elgSPY9Pe@a2Uft5CS( zr0st5I^D10)eLP#VuGczK9>)SzaOlru$*P`^JlD+ukq0QM@M<8jXV?LH>^=vES@;L~^7xXN~ifyr^!fx@W9f#@VgB8v<>LHk7?B6%EXr$c@Y--+sYp zFXM9!foe5YqwPs6x)1xUcfFDGj?IH{i{brkb`$K)dBoJ#J*%KKSUnAK(f@j&+ae-D zv4KfMQikVJ&}2ULNYBMPJb2$0M?6t92zj$+2Fr9K6Wadk=goN5`U)s2e{{52GoQz4 zShIz!zT`fR86wel6|6Q*WLU!4dZf`U`^_OAPm%LNAEe(Gv(DR>q9WTE*z|7s#06`F z7YDhn=3sHl3VEt1!SbPA*WFyX>(LC=#t?Dl&RtCH0xlOc+aH}=eM(GFGuGGkXoS34 zj@!ZMi!1NN+^nEo>Bx%B4$^mFdNHw7Jty0b%hGMb$RdTT{Spx~itdNc@(X?H@rnEV z!IX8fUybEPuxxiX;#;$00gYKtYO}<8<2vTcY%dp1xGlS#_lbP^VJ1hOxt+s{e#EGr zmz-?Gnx8#l(W^&?lw?0lx;#rezG%(9C(hRoWiJT37jU5TQ}iO^-88TF?8(CtowX;Z zg{y!07^bJ(@?5dNrGDS=A0@%%rIatj1jcekY zHzpvwalY64hB}A0BdS>AWgfaHGEF+{`Q_8Z13FP7g;`de;E@-cvROi#pXXGCrCAi` zEIw)ekEfe6eA7L(cyzVy3wY9k9*RAXh;`vGjXay9ApO`$NmsElQlVwq$@j}r7+?NyyPqs}`*3+JO;7Y1 zgI&CKQogPGy=*%di+z0PO_#GVn_99f&$yTcuXq)!pWc=JnwL6?XYS1R#}Y;4w_KRM z&vxu`0f8smCvxd5W1d&L|HhXsw?yQ1-AjvCNU(I;F!@f&?M`uU3Of9{-MEuc=eE^X z=0{qa-=`h0|1@S=$Wzwm#{wsgp4(_JwwkuEa;qIzdaHjBqe{YKX>F-x^QukLJDH}8 zHMKRDgqnVqBM=CC>VE%sm=<{!fN)y7qj}6$&ap9Qf+{P)~yyCmRp7K#n7_-kXx4YP~ z(q?{UVwKVhH*>3|$?O$-@3rzCa}?He7_~SpeP`FX*=MctwIwepe=*-ZjW#^((6KeT zR&jNUo_%UGGV)Cn6)a__-J>2>EgbXg$jt5YA2DP`pG>Bmb5nK+-o=}EqD?Eld*#kV z+sNfs??2tT@~V(?8S`L5Bu62q@u*61s_2{*yK28mb3C*3 zJ$2q^El>HnB^=|mOjg`ww0-9;clFJJuO}@-jwH|1jmdpz)9F{jwafpCKtKfx+kxby z)>oS&1(63=q+ zv5d`US4Sw9Ci4ec-9Dyr&{=AJ_fgT-F~^?^bVTab#fu&w-f zsmStdh31vlY^#s*##cz>sqP&WeeTh=0@-R>keJQq5I2^NsWqE6uRoP+tZz5&yZe{i z*1R0i&fG~!jobozrEYhAHuyGdpTKU{hAjRQa?3W_r&n%xwVC7SkLxq{gdZDzyCWik zH$QXuEEV0Rz)jjQoMN{(%rZSw88|Dh!0_^@E{C%hrMDd0r1Yp%{9R~pRvG8=GZd2c~vhl=3`itx~1h2qt@{~Wvh8(7Ykl_ zXKQipi(6oz`sKKSIj?ipZ;D_tjLuH3>^!?aqT&NbTMGAeuXhZe*`%x8&1bLQuzON` zk;=m*-Ab3tXA5?IUsQFzFk{X4xYHcWj(0O84_6$*hR)p?H6gwNMRnI}%XZNt~O=xkY-vrp4gxurVyFWBVQ%(Pq>A#v^f_GzVa zGLB!gde0haETQT0m3DOY7&-1bE#COm7C$b;WSvtiQ>x)Bd0BCNLrJ}%N=fu<$;0L^ zFKO16@fwX*`(AHWKb^h);*9wpY$_J>Mft{!k$1g*RDmaEwD01i5e820s#EkQ#1=j+ zZ)PnkUf)=$+f_8L;t-?MNoI#L-c?nJjwOcHBdnBfy?J)_Qd+{e{4|>NZY9Tw9hzwe zO_YNT6ZSCs-3zoa(YmRx(7bzOL`YZ&-FvUzd6o|ooVOmC zs=h(zz1|UDYxMPvy9&`9p%z=CG`-bCk7{mZSgXmtYdRZjJ(?U)$m4L`$BDk-v7&3z zVVAcGV@++g65lN`wF$m?XZUCu-FIMq`GyMJ7gU6W#KfuZr_g;tCDuUIcHYv7nGNe7 z&%UhmZuFLscD$(Yl#ARDs~2^zE)*S=9{i(IRdcSFR$xTTwEafGD#-o5_(nxdoj~jU zAwk>Q`1%P=x^a!y?b&8Rr3=iz8z0j=^YpdPZ1wJ4TB~_C1$8-2o82t=CRWGrjW#d4 zkMTE_BHEO3dv>{gdux&Cq;;ingGU+RxOm|XQSYtBv1IuPwDD#IotbudIy4y--o9+5By}Ya4 zE+IbIr*Pr?gqCx0Q7Xx$(P06p88^=fC-a!=2=5ntZEN6u#eu1Ww_&ICjbz<-FFua7 zox5k{)Y;p8v|RO?rtg`lzu_Cl_a9c;ZVlf=?oZ8p{NeL1z1L%H)iyBJ8g_PAM~FQb zpO*SD?eoGUXxNYAr^ZE%qvXo{o9~XOUrRT1enl$b?nF-OKJ{Ht@JqS3n`ONnB zIyqHo&y_Cnd1=&MIPEezWQ)pM_C%G3WqJ%BZV$U&WwmOAYKT4U%9yDl3*vPbvWAUk z^++EZY{OOI{%-bQ=h%QAH8oD?=H9VBYr%1J9l4=(b3e*nW@$1Y?7;A-$||f zu+3U0)$>`RNrmd|f{QyD&+#ndc+7BTa*FMk%hmCbN|#5u?<#3F@L8U?efy)Q#lz;v zymtxra`UhE%21hdW%I5>Rk5@u(NCuMJ;-sCo;IV_DtT*h_L~^r4Z+1uW>(Xr#vUxS z8^tbCA1(KQS-tJxK|QV*o;xkAcWcFK6L(eH$}Zq@UZ_WKRLmYirBpJ#PuVX#*lPf^&F*pju%8B=QX@}4Hqs@or& z*Qk|hG`W~m=HJO%D-;uu7Z*Qcn312-=G6GtjAf~A&dU#868Vylm@Y1Rq2;nN8GG}C7(P)F{G|%z&Z!%exT{j$B z5%Qct&OR(WxPd3X@EzlR@wwitw>4$$d_qs8-;bF4G@$xswUNv5u`6BDO0pi6yg&GE z%KA3%1NHN7FZIjV&ZTwFuXqu^P;#zgNoR6kjr-yP3zGu@w~K`h1jL?2+)>@FKEg3} zQxi>P%C*nSq_Y;UekHBE?moY7=3V_zrOjubSJe2et(vy=P`I|AMn`CDjfY#_Rb_dp zZ(*CqU%6l4dgj%pM9)Q2vR<2;bjD0Q7}q^s+T7vr>14wK{|xO{uU<80?C+W`T;`vj zkoNsP=R5Jn#^GE2Q$M#)ohZpGS>LMvrda+`MV8VoT2)$bSgIlGr)lv8!p+>rkMr^? z`_;3qIvR1qwa!Q3`kL?8Ji=w-Mx-Pns!W?t;dzy z#-ks2n;01X@Y_e4aeChNAgEtmT8LxG>xN%thvZLXp508YGJZ8VG&Qr3_bIuFjF)O`{%;t7}Rgii=YwD>_ z`W%xQE^j+VYaSC+Q@+!|E5=E}Z^{CtwioWf@w$y4*Uhe3*FynQ>hcRgRQRcYMGF9m$BG#WgY@ZTQpx;^i?{AkIgi6z=H zYs(9@YwABY=g;z91?Gqx>Q;aO*v|9#uyY1L<&HZniqwqCev&L=+JSt_HH zPS5>(ugNS<_zdSE6Z1kADUWpin>;gC35kSp2$|4)&a&O+InutmW_8p7ktc==*EFa5 z9=dc&RrGA^j@hkuLi0W+7{1h!k6AFfh135^@#E>w-rZW+dFuVwGz(KV{VtE?H2&ej zlV>+wzZFy)nfdwR24>OoQ}~t3&OLqVXvWn2JnmjRXXdz)8{zqtiD$Cd71*NNP96F3 zJty|G%4NM)ewy~e1)u#UcI>O#Us9aEbM)9dVQW1WrwB(hN=DD%czPtKJGT35`e`2R zu%qD_su!9+x}B&Vr}o0ga9u;;yXrkl-QLx2-?OtLBjNQAC;$4?<14R34ZGuEyQ7ZD zq;-?qq#cYCx0r4G8othHFGJ9ehvxHn(>=$92Wqp7X%=_BY28>r6WF66bTxXZ?BW%l z_kMET?bRMrXu-Fs;HqoPnzhA`f|sxh@0DF#z4Ky%nFaF##wl-hXr!g69;lMAlCRd4 zHLTW)-WaxCz*^XKTV;;ARdA|UnLhH#AhDv-1naES(A^S|T9?BPJUV7s#Xajat(BEa zemjrR9{nB1p9~9`b!~(V?T4M|r-%1)Es`Ra8qKZ@VWUMHv7f_OmdJN)OD}hH54S$(*D30%=|QgRx{~=)z%nMi|OvES1cA~tR4|JVqbou%8L}Ui)_uKCr7vomX<$c za=#d9u~M?}i1W)6mlg->e@J`IcA|7#jU!FGL0J4&_r^Q}uPe%4>*AwI4`*72y?)3P zti)00U;g6W-R~{;($zZcX;K`~{tm0dB=6JwR|efp=3V)1_v)SM9vM$R%^l+x!da;t zXOy{oax)*RiH43}Sx&9N>ha?f&RH%J(k{8q_h!3fqaFA16AM0HlQ!Refs?J6$)kOK zq1eSY+4+|gwpp)Zo|Cnz;#=M~nI(m*McdLF+BW*0D6=onedaU1Qi9QA@($V&;dfiA zq9iOdvY$S%yUU;8ZftOL`r@+566N!O&j7*TV*`j!0N;P;N1G7VKElP<;NtePivc+4H65n*BhmoDrR+MqMK_Kn_cv8OjwSoBsbY~qxBtN6iaNeTNirlTHF z?DsAAUd#^REK)pi^pRRl8|#LUGNw3-h1T_px0}ycSaQREibRc;MRmh|pD43rd!=Kp zs{%?{EZ5Drz%zYq(U~w|8Iv@@mQ4%TMYU#f?mit%oBjHtjCi6ft(^b7;2pMU9lMv8 zI>cLEY)V|~6CJLtT{C}4fb);FHL)p=FXV}A+b~l+zCtK^t=(M*6ZJ&JrfYti{Vmss zy@`GwazFINi(3DKp8^i98x~)*=T81HZgsX0(-c+ls?ZskdnT$aojU7Gf_YiPF0G5x z{S@AuNicJcdP!56_&l@RSkbF%-@IWB**pC$Cr!J9zW$u$de-HIm{QSvlbN|!)*ce^ zt`~T*h?LJ4*W$xZXDKGe?N#W6AZYk>N6 z*LT}?)_IS5rB=1n;o zu$@y<#lk*gOxTDp<2AM>&n`dKD-%6)_=kM!*M#WVX|rn@%;RqOMI6muU2)}3bk|j_LiPZptVjf6R%yj>8t(SG2hk~WZQ3Dc6M@myTq!6 zOXA;(^7&+*Lc0#RktnD(nU-+OA!Up_V< z&EvfGk^F5Q&!xq7EZWZb?8Ro+sC~M}Uub=uXCl8&hGuy%iup#{WahP8vU|LhJ+_{W z%xB#tr)g)c9KX-cb|w>hs7g3r^!~&vdBLMjAFEx>zOCa)(3?XNb+ZbRSUS!MtoITr z=&<$@4VDpjqS9t|M>6IK>oeg?%-xoGfny9uj^8uy@~54)=~rkU9-g%|{>sOHcbn}# zMtcvd2ZgC#QbQxzFDTm7^zKu;Rz89W~mn>)5;m2NpEqBEQ|Lk?WBnq$mjS!H=0S~8u0f*lx*#- z(Qka9|HZ#3BE%)2u})EMi3a)=k+qK2#^?t?$V;nj%xo35p&uK;KR>d~!OE0t!A=tk zQ)|=B=*L60qXnCtjkYgDKPZBJ3&g?Jo=aLuQAtTqP+U}yCMdoX4TR7kAt)#zOpF!K zSVT}zSP`AWA%@OHWe@^;+pUf!h&4e0tpO^6;^JsBS|q*_9Y7EfLjMGXCD9li5@-%S z&@~_|B9HDN1j00cfvCVDn1ClEZkPaYMw7*14k8WHiL2KZkm?8S~b47tJVKWItj|Qrup8)3~a3F?a2o(GkLFh3Pw2*$TC}IQ#$3`c# zB!*6~)F9Z39%hvEaE%ZI%q1d${2xmMC+G$}aZ)NE0$wMEK!Bz|0&*VMn;e5CDE9}( zpe=NWAf|9nlmv!O#Nj>?PHZKzzi=O^AfCuTlt7Dc5Q56k0+Jw>0tkclz$9pl|Mq1H z%+MT653L91qz5KS19gDS&_s$DLW_wZ<|KMB0m%sbfU+?u9lG{Aj*5W;FG4UHRy#;Q zIt@Wk43ML1IE=u~=sGw~FAV??I%0TCZ${7x%pqt>pnD)a(1I+4=%M@vYC!<-M08OU zBqbNYLO2FfGrCW$MeFbw?A=EoiY90m=mRf^f1E$Tz92193u$|t@<JOXot)hR8H@a>bHfG!10eH!#x<%1a=Tm9b7}E_{W6%fWu{(aAa&=BEo3>h8Vhu ziwq({ZysuluJ@H5H-=ziV1jOvUFTq$MdqgU#PT4FaJmKCgO?AM7Y3mLegbQ;R)G%T zKf+>2|NUy9KVo28Bq5dv-T%i~5}ci+5B(o?0u0cBQzm#PprlVcf15etjpLu*uD^^4 zP*hGw=irpsIs2w=!odc#IVm*=jSe&q*`HnmXbc?+Mc_Y>7~%iv)QCUb2_0jQ64h;7 zGXIUl$Z;u+_DD<#j3bB!o{er`m7*@Fm%T~bgNERsN8X1R$>dM9H?;lxGNuR7-x#6n z)dLK!JyicmDPbWwCL|U^e?dE9$Pe(73%U=939=;2#ZRZujv>c``=TgfMA1_e%mEO% zJ3{BU@r2v~tr+Q3m`5M`L!tl^NPs5~k$$ns00d+^0@Pq@aX5oOum+M5gg6Zr!vNWy z%=!4C^LGgo8p6a82uugo!WjLfA_K9AWa{}l<5e`@G~Bw zJ5c580RZ_Q5$nYiEh5!8_+qpcF(Wcrk3_&60mHrnJ_OqA^KgTmmf#^l1J#4{Xdyia zJf`Ws_f zufhU|&_Pu`LMj~l5Q2pIan3;AO{pFn6HJ8eW6uYZfmYBQ@|k}C%?xgTYoBJ54|#e}0*EN0(=&%4Oc4WcVDmvth5g~zP^JkEDj9*Ymy@Dvf7<2r0!}YPzx^Lb z*J~1;)I(iI3n79?%>d9rqY3hVuW$kay*%_Mv40F|5P^R7e}f@M_o&#WFT?*H=&$|% zkWvyrDYfPux;O zz=>c1c^~E?g7_T`B7&m>=Ad-lcMiHuRZ#pCj4pt{ByGI^+aGaq0`_3K1&~PkV?@sX zMPdXNKn>7Q_ul`(JscDr$Z|x_2Z;r@V+c8BLP`%npeZG?AvBqTF!~wNKx|Kz{>$OoRFT6)Pv(291t819b|_?Ee4JCl?>K{ zlY#1C4X7T3A=KYrdr!mLsK6XR^da~sk6*oBw<3+;K9bZB2!eRfgU4fG*#AKY@nVQ) zgcv6nxP}gD0a{FYKGqZvCf@P=CZircpfx0=2!uDHDFXE$@#)2g^nRQdK&8kW6cA7= zfI~pZ#DkrbUOI9N4Cu#*4`~Y03B_qYLxG9}LNz^kophM~h!GkCVx+zMdjC+gdiH-w zFGUFdj~_zl@;|*9K*cCB@b(*+0MrdOAL=2bPwe-V{?{%T*`JyO_Cbz;&k=yWQRl2;z7K>47K^M+AZJfFO~-V4$Qn`vHkV9WezO{qFx` zM!%zSM4VoG8U4B|IrCfF27k&a5D9Iw7pg`8E$}gdHVr&|P$pm!ScFW1kG?7=Rf+ro z69wWhAGCuK7VfGbmmtmbG6AB8c3z464-PW~A@FWYcz`VU%Qh}F9ds2En+Cwcbwr~l zije&wjL5c$k`fTps~f<9gkX0B5EBO?pe00@EFM^T zFa^3suBYaLw!mDVYFLM8l7IVj#&rEZsUi|FVP^n?7b8YPb+doo%o6tR`kSs2FZ=?K&f`g~4b~IVD9Mq%C}JQ9Qa6?andLvM zhQC?=0T7|7Kkf;UW+}%cO+^SR0-wJ^bg9syGyoIOb#N#g4QL@&=TQ9|nl~7|U)nj^ zQ2iXDHYh;|Ei_>u35ROsdOrU}Z+k*=(u@C~m;1Hxq}Oc!V`TsSZ~VNDCQ#WBf$O!U zitgd01bH1Dloo#5dBOgDBpwpbK=TQp)PqZZ;E1)PPYw9_uOSl$5GP9WZ|4oPhBPbb z=s)-WA#p(NM{2}7lcWkksy<uCm?hKYlGgzNCR~9XjJhS zV1w}IWVDbzqxDL=a1Q~6Yh>a1z5M}Xf841+T}Qe6mlh1P0@aXkFtTjobEs~lG=!E0 zrA25L(}TQ%`1P{??`$m4cQjZV01h!HP4}nm91TlCZ);MOU_(F*#DNKD2caIKpZght zL(n1GIn@6Xb-tH9A&rtw2dYKHa5F(y-TLq6dYtw5+c~g5Asu!e#EeQJBmktH-|pu~ zO~M*-JpxZ9FoG6Ub@Au_5mWT9_Xo)73DA!TZ6KxfXav-Z7*UQ14GEfqg#oRB3F!o+ z7Y>}A1{0_SU zUs|9AQnN_g^zGcw_kX|~oDfYU)$^Z?0T4P+Z9w)%9T4^13`oOJ)es2MgQ+b!0w8q` zL`YK+n)`KsueU(IwsXk-1KAo>2}!7zRuCh4t^Cr@{b@S~@!yLe5sm|a8mgb8X!W5> zUhmt^_4I#zoT8V+f7>U(Z0H~W(f)Ej_ovQ_>_4El6G(9f15S$0!7<5RfK(~E+>>HS zXM}a+m~hMi^9J7kLz8it{nLJ~4-Vu`#(qs@MujX2|x`}q5r`<9taqjrg0L2 zf`aDs>K({6kg$l(2!}E_^uR#>*Z(0^6Apvb4Gu;WA)Ez~TCrVW4jTWj|0A{Xzy6O5 zGYGH$^?y{9AZ<`x5D_(0W+bDDNLK&r|NcMg|4?BR1qy7&bQ0Ud&H@l&m;N^quto7{w zp@q1!1t1V2s0qQZS_WB6UwrUW5InlUTKEflMF<@9htN~vw`%>p=+Au45qm0fMbO00~VhJpcD1icnMMpp`%SHcQW50s@Oc zAO^i?5zmIWD@GvjJ+lArTd~M37*ZSLZsw3*NI}x~VoI0+XFbpcG7WVf&87WL(}@=# zZ9qPt#>nvyy+7JLp@jTN>*xC)kQmzp)Ca1Eb2O$$OEE|H2YjRge*hnV^+P^UA^YQ& z8rcSaISf6$&@-XNAT1C@>crp(Z-#6C3LyUcAF4>dy@!tg^rJ@VA+~QA4+M^+LB&5c zkyJU7n0})OY4fLM{@%Es&wqh)kPN_WLqzaI0u;GhN@ws)M58C1A^s6V z(r$2ZP|5m$B*FLHR>RkkhK7uw0l)u4Xn|ya1o^3{KN+KibnRB}-8ST`G#nyO4hQm9 zAn@Ng=+FHBi==}gNB5}MCQIo*0!r5Zf1_>!C}p&wo;qqe*|K zsu5Z4H;NkVcQ*=q)qdFHLu&6;{SbKA6QqQJImp5K&gr#Lfq(xD#VOQkY9gsBxDJgZ zq*UnHNGBxjfx)kCAuGZ)=zbAT z8l`puF)0!5-+{=#`F0DP0Oa!k)+E6TRoZ?&{|EL2lt>By2a@BuO|;^@2%vlDUvJ5Y z-XEuRBq{!SM?XM%Gz#_y695w=X)oe^2xEfaaD4!wOB7(7l<2D=nhSnF^%QUpf`N&E z7Sw-w=>{V9&%WJ4NRNn8wnzNvO-{q;`bdhk0V)Fzg#*li=8%&AviCoLl-SD&Q{pfN z$3xno9@ilLar^=^6cU4~A3_Ig3pNEGK~iBG66(jG(KUJ+L%%-;`xCK9zp$^pa5w^b zLL*cbgL4EP>`A{KkI^;!B^2os2;8sj7_vV|fwc2;@qal6GoS-}4)+T9_dmc^L7La$Mdq0^X&GZ65RxbbuUXA8cY9jY5B+tE2TH<3jVub=%t_JKic|DSZm z@cV0&#C`1jC!H~FQpmJQN=XVsm>1U>!q;#igOJDH|3?c31cB~h=Ol{{B7otqa* zA%{Z}81#)I1Yn^0LK~`*;pP;OLMu+s91;D~s@M-LWPgwZi1n#VFqvM`-ziMw-VRkW zKx>FTNgMJ%ef1MpB<)YA6Wa(DA=}{tY}!X^tV$F$m?#j3`Jf$?9sl^#k%)i4guvSe z%Zh*N(~&f4d!1h0;4lCT>`q9Boe)V&ZxTF4+5z*?kI}TAb95d23!fwU)K720nx5$B ziGM1a5yYtWYiI&Jzx73i8sY?CNbsZ|P)?3O^oj%_$p3T_(o2Uk6>=DA0aQN(kUXd6 z0z-7*J`nr{))0RO?376jQT-nfM{x^a$T4MZ5>;C=k3_x|sY1c`7QNW$K|U60cOL!1Xl&Ghmkus@wj{%UXcJHGNu|3}!K z^of7d+tCX)(DAaM+xH^_&RUiR)W z_s`Z2*?-7yyC4E&jlgvtycuOea5mCRpkAD6kVUfs=dR{ z|9$O=ko~RxZm9MS|NakI*C|gWoCEs;@(HR(Ajar6<;CC=_#F99Km8qcT$n>oi>|#x z^?xV=z!YhO-Z7CSC~tYBD19 zkNZ0W1~;TY8*%~yLxwSK?-2i=j)s_1OcBDNcQ4V}A^JPYbV!K)+Pj|b|A49xbJ7p! zLEtg5_&-R~DE|M`{tihECLlG7wB5VCL;9yAr1NUD=D)iRfY5;}Cw%|+uYLOkIax2@ z$ZYh&_1E6@^nZYe3=hKAzqxS^Fr;sxV8MXi`;!owMiKp!anJs5 ze~W=JItB#?WBqnnF~a~LB*BkJ z-|5FJFc<&+8@3-&NU+KV>II434jAC-h5ioo;Zj7qM`~b>nFBi;Fhi3E1BJ_Je?1+cehj)# z`2Et-;pczC*~qtFz!;?I#T=og&H?=&KAPdRXdP$*o(Z57gKyCA%VM~I4tfTp=VMiY zHvY*AX`~&W zmZ%5VXMpD~`~RRGx{!f!F9swtkcfT^&iET)N0|ZEgGs6VpT5Mvz65jZkT6E-q?hy; zVrUM4qJV*l(KS2|f$Oiy8MyufA+(5M@;Al+2sDh$48owsmw5k6gd3I^*$`9?HG!V_ zpwffn;9T?|`)ZV=2Ovmihu?oDRR)nTC;`$*0U$Wq5az&i2LXhi2f_Z7HzI;S7)#v? zB>@sS%#cLU90=WkX!Px%@#jB*=`Skpqdw9UzqZglTIo|OgIzee+z$hT=KO*!;Abw{)0hIFg{=ku+|HVEqi0ubyq5o9} zjq5*YTd)^3MrQcAO^U|gsH7S}G7>Ck0v%#RLBe$d=F+8F+$n)$qBU4h2zQjG5ii32 zeHkML*eS`pKL}$?6y5)4yP{D4|Fnn@l9J8HzbZ)h^Pk|YAStd$yzJQ}0~3BfxFP}m94J$gcTjI={-tr251 zjfx)d34D$;H0Z7_=^|l>ai97Rj)4xU zWD?GYLlT}1Kw|I#kQ#X~B@tpsodXRthq?!V&^7e0*R`Je(Eq_=U<4rn#L@9rdOPqP zpb4}I$0XHG0mStm5F)1iBkZ}3!wXILg#fHYN8i3~Kuw{!lxC3N)cgOx+}HiCPayk~ zc0mKMJ&Eq$^>sh#Q1n4lBrj?V_NPvMb4&oGoD`koJOLRG9i#xjjP$92%q9l*2XFq{ z{{fm_`2x|W2Swl3(T42nkQg{3AR|I#Ade<%0hSK#ql045ml?4hZ}I(F6X^E;K@-3L zV!Us-z&(->8smKMn;R0iNg*0epiP+q=-}`FK#UT$qe>>6|7l-{G=%ApG2heS_7o=& z3<-e!1j;cfDA=DK(ZA{CAgTOmE7$Y=Z$OMt{mWji$J+@yzuC&6_(y7?w%`6$FV{zt z5Pn0pauEN>DgH_?hv<;B=u0>F6#Ccy&pT56AF^F9KOoSP^__A|a2`Yr90=~=cAPLD zT%%}!FCai<-N5sSYD%t$xxM#)!Q){e8SCKDz~tAs{h3M$^Wh4L!WQQTg86T>a%lgL zkOG1Gi5{*IIpFtuxt{cmHGtNG%7G>p2Izq{h|v3~C9?mA1?c!!y&U5B$E_UV|LgM` zLi$g7xqs5iQS~3xMX$PlJqEu(2UR5r=j+qT_1K^4-AKD3kN#;RI$&@g5$XjNU84v4 zXRTa+^}i26{NM=AL+16q#fc0%00#$}I~YJRBr$0G{;v;#f7;8DTKJPz?!VdpC2UU* zi1GvQ3D9Ow#ijfHC%T9iAV>U%n~4Lra{U0pc}EG^AJ=)(1TgV$0O^Mzr2*u?RMLa^ z2M5Ei)1X=rsT0(LwehEb`eO_JZU_`@->71=D7#HdJ}(TXqi{ew!w6T|X%7#Y^qoYm5Agm=umY|9GmgWBq&e z-D?koC!E@l;G^z&=7-yL52HO@0fx2X_I$h6Fve+%u44?91}ERUP=L-LWbf9zQH z5obQoe%$t!*V%B?@ZpuQc`bAKvtETH=*Taxzq5RN^!hUiM-R&fJYVQJ%I{OIiByr! zQO+>_IkZRF&v(q0b&YW?7x8IK{B-$yc}IiP=SAPU59Jpjk7Ff15cnjmt>`%W^-Cv(XHByCzG-PkgY$~-FSWY`HsuK>tU11YSosm@k)N;M zSnCzfaQ(=I(sBa_gR2~;L)T|TrI^dKCRn;{t9vxZ-XgiQd^hK|C)(S0&_YFx0-L2p z4)Gp-lM^YNw9-SJR(bVoH6`)(cKm6n7fM!_FU`u!4QX!dx@`tuLF8 zcqnZ7Y*ED)kz2!Sac#tI_gI!a=^SUxxe4kEs$#KQMo+whotM_i)>@M(z9K7d=%lcTK0YPl{a@C4MLS!iJ_R3l4MoZ#_$dyPG-D_1(lRes;80`CcI4l2B6p*&YmSZ6~^cRClC)NNpn&+;ckkP;ZI&ss|7t$1GyjF2yzFJ zb8!ozMwlNBJGd^gzCl6x+Raff0u^;aW9II(?iSFh*qNsGt*WcAuyVD#`eJKkXA?JL zd#@wv-X?BuD%3O0C!`GfwzX2Evf`AhtI4+b@_Tz?N}Aq|oKqWM`}vI0j-9lf9?l!t zByZ~T9&it@yvcn!BQwIg*iCL$yULyO=glA0F4lHD$K;W%xK8twajSXDMelJNH?=xe zXMYp-du_Q>$GYanMz^ai(-4YQG}*u)B(j2$`I$F|h1tO|UZc)rUgGQ8 zc2UG#>%{KR_Ekxl+=68BR^hOLI7lNE-2nEA-bw^S{1r)$>- zQSUq*0l~48vY2>SZ-jJBKQRAHz561E(JbW?XK~jGCDPo?^+lFObWKcKo;;CL-pSNk zrE;a3rkDL_YZ<}Hl_o)o3f&*}zbva>VI7js^>V{}aqCZ#(<*MOZ73Y?Y!e}GJ^Z?D zjCjoIR9j7x0x6j>KHHX#ldfC;e6lyw4ng5$=`$^xiqt+k3Rk8uid_0yT~~K+gzB2> zGHOpeO~r4)b28JBztm<|s`#!o43&2sXB4)~nIc@7+HfN!ZO@bY3QyNQ^M33- zHIR2r=I5C7D5XjRJFQhhkG@y6u61P}_p*YGKYaEFNt;pbqbECVEUj80bm!=ySW_jZ zwDaf2GCUW*9x`Til|#7+&C_Sb=EmG&$(bKC?i>-CysfEm)+H?t8`>^{l4=zE?C|0z$;&6n>eZP~7Fl9f@2awVAIFUq-h~;#TzaA`yqB_HGSs;? zeSUi8TgpnsYtqlkWt41&&sc6W_F~hcXX@+pA3TfRyiD7*u-*FVi=6orrku1|%5_Cd zWgT~>@U61T>vttpWUt|T+r@Q>#%EH(?_?@FE^vqGxOLAbpNp41=2`G#Io0jj@a<$F8 zlv?X$#(eB>rSPho@6y}M<%KV88}7N6+c;7v>gEZQ_>bu} zlSWM2_98{wx1v?@$&(tzC{MRqHC?gnZB;6?DOYw(W#1?G#k~))(B?l&Tvh7*0IN;Hu3)%jxE32MYW^mh; ze<*dCshVG`8+nJ3g*9cWK@)pmYG%oD221CO2QF#OYA*low8+!%$AO)e@5kLWO>>Ay z7p?G(JV#4!OO7xtlQ3Y_nPFQ}<@-s!!6HtF|D2sqj_?2wO!68VV~Aw zwncM{avKFdN4)2uIoR#2t?<^}SzI_>q#^3C!SqTE=~0nIE3Y=b$&ZaZuath(^rNlD zc+pR06J2K*9DJ5&Fry?~Q)>DAmT?c>-c=O(D6#6}>n>-d^jC84o0MH=rH7ty{N&4F zdq7#;Y{vX)d7?T`)t zS?Z$XNREqXmisp)Ue+>;C^Y#&TYN=GSO3Xp16L7=1}FQSY{mQU>`Ls8Hdc7soyGRj z?<(81g3tS%vt2zFMV77=a=U19>_~@5U5adWVoi>1U{Ir1lZun{4B@DPNdZ|VPwzF? zS-n28$0&5&M{l`Zs?O5Jq7POzR&=mdu3@#-35YGfW+}uHRc>yxdDu!9K-f7Sa zNjxL8a+NzrXSVGIhMTryT3-jBY*bj=6z??7g1Pw9Je$RCx@S|8Hit6wWP^zZT3}R?aUN`Y9!#G`VWNvFB#xRU?#N960%Qr9$U0yE`jP8P7X=PivJj z8FsO5ghr;vle?GrXQw~&Xc1X2xl^ZI>(seX^S0U_d88mD$9ZukM^gA|9_iRp)67@P z@0a{o7(r9@UTj=Z_)XALOJk11#`A`eoyi=nGin%XXSQpI@TmuGYfWc9)*>h>8@%g< z^{R;zIu=)6b(zVl|9*GZilZX=Ck-#|Pq)9-TD7?KNOxKU;cP$03BO47aAKAxNw6SVZ@IPK(X+FCk3W@Y)i-yEG$ma*&3v(Py4g4_smzmY4g%2g!z z)jdWtkMdaE#p+_{!~3$?cX$w=Y$DH|YIClV*-T-&sd9{wMLRC+Nn?6!$MGgRW|G9+ z+PrekXZHH9BqPFZ(R?^(m)o>UHqDvxY2U~q^@*+P>YvHC%rSDkGj`;UZ-Fr_$$X}s zpFhTgrcKfs+4!)|{lVcYv+brPhKmHgn4?;um{ zVSweNEB5h0d7-*h(#B}R zxbIUY3wf(rvg}>=CfmnQ)Wm|7CVNHULC%Ak@$=V5r#_stb5okdv%3){$&0jZt$I~- zVeKZ5j_9NJUv$2j=AGuo*RZ-lYKh&4gnHit<^r3Vyp7W(1-Kl)@A5@Pf1YxkbY_OeOHc6#eN5`8FYWuk{W+AVTQ_9gA`NKKxU`2 zE0U(&bMA88*SKo=TjgB4C6&h-n#=W_mY#Z^GTl66PSyRL_ioO8@nvg+g}(oY{hMQN zT>0p{i??u@mPXM;v+3*Y0^cW^Z47;`sLW$GlE$8Kb;W^5hNNSsyc>01cSzZXrU`hn zEpdsonKp`3Pg>^xarPHLQT6{HHcm(*q0&erBDqO-Nl2-HgtW9Y(hDL|A|V1IWgrp~ zN_U6SUD72WAyQJ$Io|I1{{A!nnP=vixyPFiyPSRT+U2_Tyw5rCsmDg#0ikzyf-Adw z@ac25x5&Z1pJAd7-LvYdyb-R?gpS@{__^NU(-b>&@t3y;ftsyM42y$J<;KhGdj;<= zbR;cu^F%l2R`uiFC_+r;d@}Oni@vrIPg{0oRfAcoSGyV0N%(=@;tJtao60M?ksh7C z43kGwBXbH#=)b;B$9?%r4eez&CF0=njss{?A}eL{JQvTm$1lHsE+L8(J7#Lo>Sn zZzDR)|IdsL1_y?1;H(Y%Y=DIt0K+->e{(k>17IKr7yWl1U>*ktcmKiri?YEG;N%ee zSPb|W+))NUh6w`;ICcR87XQY72EYz741vU8ZvrNRi2o*lz!w3dK?F7lu-gtpU>_a^ zJU#;Z_%I*<0viAZBtT*lz<>xyYy>zE0f~(O?7_p3*a&bq8wQ5_H-j{Q0}+5p;(xi} zKm;T<0vw2d#6|!rf+4XHfL>roYy_Yf81TIO+W-&&g^d7I0|SN#|J?#~1H7XCEr7{B zu);5IWROf0#FVNjg0_w14CmY0NudQ*a$#3Ff=v-&4z!&<#us8v*DBCWegwbOQtCZ2z_`$_CH^yAT5w*a$#30NeiFf&qSXVZaI-0VoHK zoyr>k-N65i<$=2)fdw`K&<*_GEFQQ84J@z`fNtQz*a$#3aDV{+K!m@8yT1z{0ziQOHV1SA2MF+Q0Ym@@@NWS`00{7J z0Ym@@@NWS`_&dY<4^2P@48VZ@Zi4~!zySteS3n8?0@xK01E2tQ1>}H82_CqBLwp1e zSHK}cg2yZ15F=qgNpOggFrXzk0(k!aZ4~GU{(ndbg!vyTfu`U94zTwG!u*ewKvnSn z!4l{S{y$m*Wx@Z4OQ0?I|9A=11qXh!C$v!@4TMY>&=?$ICOGzlL(qf)jlm&k!hpu$ z5H(>yV{izYFrYCw#7)pGa0r~BQQ!a_{{8%eQUMO6VZ#8G!6A5pVu3^S1l0nE@CmvF z4&f7&3moDnU>9J8eYt>kfkOlZ^#TVG`uE6^0Vo(a1X0j1fcddufXd(yMnT8GA&!EQ zf&T|m&~-p#a0sNJX5bJ>LCwG+l!Bgt|79uW?<*0g3=Yv0R1F-$Dd-wF#8Xf+C5M@CJ!LgXd7=RXnL!bpM1cyiqY6uRY7W5DtVl5~l@Sw)O zx`D=ksNkK$`)=KnMn{g@6zY2O2{_2nMBvfDjBy z3jrY*lokR)FeohqgkVrw2owe@ZO~c>2*IGWfJ+6jVSvgI5P|_~0W0V=12l$!5DZES z0U;Qa76L*rC@lnpU{G2Jvf1Oxs9J%xl240;L)AsF-&5<)PjDI|noP*X?gYSYtPaz=$gPuY{2u1)M0oS2p!vGy2Aq0b(LP7`zHHCx_3~CAqAsEyY5<)Pj zDJ1ZhRiKRm)j$XaJ%xl2i~u@9LI?&mg@h0cY6=M<7}OLJLNKT)B!pm4Q%DHGpr(+b z7_7HJPaz=$gPuY{2nIX_te~d}=m-fR7}OLJLNKT)B!pm4Q%DHGpr()zf5Q0H5p&$f< zVggPr#C8F+f`SkXiU|cF7!(rPPx51$QKr^8r1S5e~P!NJaF`*y? zgJMEK2nNN3f)EUf3AjEB8x3d$4Ivm56B_I_$s*G!q&^FcN45IQRu^1`VvR zrUbW{%_Q374pN}zE>7%#W~=AIF#vFdM4HPmCS(_G1(Z3;byAneL8V= zMt?%_B|iTVf-Fr-`JQkU0qWtdHSC3zF#R)8*ZB-q!4C!Jj)A+A^(N;wd&|D7wN9q> zEaxYO)#nRB=L_{G3+D%WGG*txGAGqUTG>?JXrm4$P0t1w&W}W5&i@?NpEcL}o$PXo zoylo2V**ZnpG2P@htxW7w3w9ccf zk3Op}oAtMO8UJ%;&A;Tc7;EIu^HWL;X5cUh<2#gSJdQCgGhIu-)Yq{#!UDGf?u5+5 zbB}s;-fRmor7RRW3=}R|*jeh`*$O;1yuVn!rQ%dFqIV!<8VF}<=UYB!5jqXD^9j6k z_1CMyg(KgS6{4Sk$9vvsAGVe-gEr&mekYR{>5*!WtrX`hXZE8--5YgI+|Twj>#8aQ zAGry%)9X$zZ64KYV*D!0C)lm&=yRQhf6vuA)ekY(9VvijnPB-e zr?Y0ky`R%)^#a46mCmM=DGnDa54s~AP@Z|d=&bY*r;$`GZu+Z=qalTQWy>b3bqOCS z-hLD4Gl~*i8n`_rPg<~|XE|-?HLgai8qIq+aXgaCH{##B)_|$D?_VvioOoR5L{?m# zuKUYpefl1n%q04m!+QTKR0f|+On2)g-EgDcF1ODWp|*mdV`^i*o+Flfc4Wm>9pt}1 ziP&@+k*{S|BnQ}d);uaLOKhUG@M3SdG(qd{k@aMLoz}wRQ`o>Rt-n`i%1>5mW0a{A zS;rHhG@iGQaz4aj)@b|Qm-@|=Q>Cnz?zMgSK5YLWTJUkDMdeaWD3x(;>6Np5$LO+9 zs;let^R)6_+@pHS8GoLhl9)a>Whe79eI?V*bokwWch|4YkbkB!@D%3&KW93LGc|+u8ahFR*zg z`F=d{jZ}jt)pASu5Z}EaILb1acapWJM*8OMO^T)1$9a`Y`)`>n9tk{6zJI5Alrv>r zYEP3$tGj_Le(R?atYt$j687Y-kiXO_Usme!hi;#!MkVL;Pnbuxp_}%q>{)kt{iWm; zZr~M~Cde=Kkyh5MK6x7>A9z1O$4y{3@rU_cl7g0WJh$SHPiEttbBe#*_jvm#T}I&^ z%GSNsgO&Pm4t3XFvVWA14zX#>n&FpNe&^e)S30BLG*NnL8*l3wl_)X!OMQ+i9ZCP@ z7R06hwzY%>*d7XV0HJ3P3qlZE^Zyko|__74sv-xsd zH?#z1bv1%(%hTJRwVr(I{F(G=E9q*?5lZCGPIF~<3|*Jg*`9Y``_#g&Ly~|r5xV?v z)Uu~>YR zYNL3xdeUmYef}izdLe)~kaY6%mv$Cj=>ntUF)eo+jMVao5qiH^B`H9ve0OJb)bi-y z=yh#|r_q-4VvuOv{QAyG*+R(pP_ItD(I2tdip|x+c4;vKQg?-|L4%3U83#vYRr!sh zmIYm(4cke3?n+@2eGAS;Ex)#K@Put>xQbgje*Hdb`LZqhiQvjW!lKWUTW`*NyK0Lz zVk&L+O^}a9ipT3%zEMb`mx{GUujWVfe=c81&#<5EyZDWa_Uf;p8@GFzO{>SQtvBfX z^t3He_$H9DRn$LvFD+#^`7FnjF3rzib=_AJKcIghETC@n7}G3is$?f7JaXI0@U^>2 zVcCzV0gqyX&+X~=V=5|h^qG9dweno+Qa=ugoAyj+yVh+kx1voce>Wpdz4lAGRz{Q= zZmpYd{&)-w(42=4IiU(Cd%#@Wl7{W_-zMe8iu3mEvMs9%8!%0|rTL_RZZ*96fr3 z(uOt}!`3NkhSL6J(Vq?%`j-?QWUP-06q=?a)s4uQ1|GO$2$S26D3`0IRDxf1E=0`_ zh@Xoek9xX3aM)_M{k$-6&*aaMoXWze<(Zk?{j-hluRg4m*CncorL5TW-0zk4E(;L2 zpV#N+rh19l-L-Dzp^@rxBo{Jt`TkUbcf)b~V7q6_+P0+P-g4p7 zB93xi5ymKoEcZh7PHT1A-3P;a#%tafc_P~B-hQUOUnE-Yo6POsLu1vvSCDzN%gp6Z zW5(2GJPmk1O82b(9&&i&`H)+na^v=lDXVb|%g`U}(c3>4zD4~)O5hx5x)_Sk=|z3=xk zSIKl@WYA+DN%xLGeQGm*lwM4s>t3CLMaWcPlywz8y9XlgE}dt?{fl+A7C9`%bDi((_{ zwKxBokYN47tUe*-sA`$f;>vUd>uUvzODt+zY9CXyH{vn6#LOL=oo(_KO(kX*Ie8W7 z?5~^5Q0MM;^VGP~E9#Z-DKvWA3f+uH?2=#xUtV_iu{bGM9-VHJ5OrRz&M>_~K_*~C zxp#P$cwYt87U}8zgV^MOV@CVv`1h4mo$gZa0&RMVSnH)W-}z%B{d*2F&${_+);Q!8 z3b=TqN-i>B{HeRM8z^2p>VSQDgDheBV1H z4E|Ec%YN*Q*S%mRI@TM${gVrAp0>N$Pcb^jyEH{PV#WOt$p^S0KHt+SY##Oq55!`94ZHr_@49Jo4dqAA^>5@2v~>eA;zaf=uq zf#ulV?^_rQe{caq^!4AL4YNdNsM}w91AG)m>gjQO+dx#Cs zpbuWD$oQEcP4|@697XtqQn*n`3f|Ky6ap=`P$gWPn5900oT}8WG3VRmCQmNF+uV1B z7+;Xx7Q9=q)9hjv><=G53;Qm7Vv9=dN^N$$xmuj+p(URGG%s1+{d;-l;ix&HW!ZoG zu1c>tT+!XQSD=u#dHceH2LZ)&7Opu2y%x9J=Wbk&Gj`%-e?4e0OQ!zYvB!?DW!2w4 zr#_jqIS#{sFtRAV^7fI~{#RQ%g*kXZf4-hEIx4MiYzP6Z|%RB=bWyEMn{^ z56CCk-mL1z>@6+F8}q^z*j^{%t`F7RH6jnm>(CzeOL-ut_^r5O@Orr8x95z3m1Sa< zZmzFcuVI4uq+h058r*ukTcqnU9VvpIG`}>9P!e4$Ax>>&Bx6}xYFblMs%=9$moeKX zDr`oHxv;#53>H{DcKhVTZoM1b&{#q+Hh3}2_`YMXP^?PnN)Umt6K?$&j?7!b_K5nk z-=aH7qg|XCed9Z-yB!%b(po3G6=N`_-HMDCXf4d~%Er^BBA)pJ8B{75Ot&~YTFAgIi+t@OJ5lGWZtM= z+wc^_jAr>~J#bCxbq^Tr=pS)E<#=1M{%5Rg&zQAc{`z`lT?Tt5}?%+ zP__BpQYNF#ozkhuHeAVJgXi#$(C!7+*vxDW@r1#*MHJj#EEn%5%)X(gv#wn4&AF5P z_Noi!!|4aq-Cu7%WN+4ma|^9u(n;FoikVp!Zra(2wGc5o$oMY@BPnwhX2mI$do7(= zLKnQd?4;Tb-jyux`G0?7bzCgbJHHTgM%{Cta&Mu?G*G$;)j>IZgce z!Jt>l-R3JAgDZBF-3lk`u%937)UHw_RT}H}V}7^VzD}Gf*`x^B62{kWOzAncQ(!4L zZXJ@OY-N3Z-0E+lG11&5^=!uHF=g!H_C)VxfAODmWXF>67}1|}d8gasLL;YhpG`eF zJjR3$LYqzNjrlaqaR!ZKdbeBe$x!Yr6(F>Bu4Yt8Xsy7`e8YUx7e+-N?*;BlcA7HR zwPSSZs=`+8)mb{l_|4Zy^++uF4%ReqbGyTbive$ z%hP7{`sqbVp40U-%t3#>!KFc^`ahQ_C{NvrOlR3YU;@R)v;Dw-9tEavNO=2+u8h7o z-R>6RIi`6#zug7=e=gaSV!v7F+(Lr|^YBO?Gced$;+-zIO+Wa^llf;}(z@s*UCfx| z)Fa~4O({RuoS%VZ!$)2etKPnw_osZcSZ6#=Zk3<-h8P5d2~o6WC%L*x-RF=`pZ-Rd zv^>~bDJnymvFaOOc$zJwIh>un*Lv!U%j@IoVmei&Wx5rG?|nel+Z?FXJQmO)gn7R7 zUF3Z2v2^Qu4Wa${3F)_syB($+Yr7q+)O+8f&(;iQPJd==9@f7xJbP+?M4!7PIT^Db zH+SHi5fS*tz^j&fv%j>O2_xRD%)y54|o+u zPqy;7*LItfOThJ_-Jev0PD8)$TZ)Akq`Jhk^0J0NmQ&#=vM64}jrKf|>M;08gFy&c zXmi2hszy^BnR=RN18kwz*}NO~YEFhx{uJfHOSb#7*=L*Q0+aRU&LVKLN9@P%+I6)G zh9|Zu-BO>7jS~d_?n*5;4wayoJm3u5w0PD+b=jyE%^2WfGB!Sif9=+npT{M)F%N#9 zT>ebd(fOvkCmI$+%V{)X!OMJXP#nZ}Td1H+Vf=(7r3wYG%zXS zkJyo%t{YX zu9FL+-S<|OL&)zfK6p{7?$I1E%=q~lb<@T{N5DfGmoiGD=fq~8B#CDyva5EC%QnaF zP7fQhq`ZTH0p;qA@VtjG8`uS(NhROT1LLrEQO2i>enQRyr
    asi=(fBM&eNBaHJUA%5iGo#C;j`kjI9p%c1t2=m^f zDfVj@`nt6Q({H9*L%#$V>@!}xS^L5l_d0M+du?OCFKy-5_k()-RcCSH@uRL&H?Qq= zodEZWkcok+I#OvI2XiN~#+R~X-J^eWHtyDBOb%F*_pC7fDyuoRZH}9)JXsU>m5f+v z3?~+l5_I}jZL@yzb$o%jyR)K~JJT2QhqRn46Qhmi(nV7fSE;h1l1LW#I6;8#=55Ox zho~8k;M?#QIc^?1>hU^iZTn(|qe*k(p9*g+8y1GTTb03U8%GVw$IW*-U#Yz4((gsn z_E$fo|NQO6wRp>sZA@1+m`vP*zeP)Vw}m=x2wZyGTei`$WZI`}u82XZ09%u^B0rT_qFA zNo|g0esfbnEy68bHUHKP*D`^v`$ie2mA}28Ejv!F$ZJ;!f4_sz?%#J!Ai^~sCj?O+ z%yqmMl*?noUq3HV(ADKBG`a@ovA{IyOpM>3iAg1)Qk`rKCy5{O9r(%gq9k5Fg7V7G zNhu#>JxgUW*+r!Nj#X{v3BOhOoW5XP<@^yJotAJm&XE*& zFBNzdyV3UwZVTUCQ7=?q_*Fn}vQO`%e%lo_Yx97_#=%_x{orKWgf#NWB@UTszmnp| zuJ!i(T8stjv&RE710^-yb^(EN8yIJXj$Ue0ewlmcZ)S|ne%D(am5$$P7?LqLdl%(- z6w|h6GVJ;Gu>%wPH)*pU4t)JfBgvf#={Mfw?|&xs8~pZ*wYOU-tyaM4n;-DPY8sz| z$7d)6emOgvw&qx*WV}>aMN9GkHZTAFsNLyteSyUF;jDOFL-E7C>jW{Jf4s{uf`z1_ zvK3|pPTCo*;kW8a+GMxN1g>sQ(yY$yti6kVcHyz2_I-Y>`Dq8~_gT&3C3QY_QgyCg zqU~<*H8M^-Z``_g3A}B&cH5lv!TTcITlpIPp>YGm1a5A!8r#%q31-3aEdkDI@|;bdntU0u>6fxBkoJx6bc|N1ye5_#4^T^!|HM?m1z{zJ6R;fPunOuFn*z8NYrAds4 zP@buzvXdbzAGO-oDpP)enytBHp8A)bHlHq?zgHRP3N^d%YXx(JQ8~{p z!*{)}c|P=5U7)qpGk``+!!?fNgiSVAigpU>0<8@8M8>a*;8b_V_cFP9k%JT`-~ZsL4y z|5rPu@1>nLTzralL9Mu~QmHrNyfq0=U%yVhnN>Lup-wj+hI7xAV@~TEi;ExECFXFc zG|%InhztINkAI{_;`i>id`uvIrK1@vYjM#?uwk9?P1+UXUFw#Aoq`pLR3U1ya>>-2 zEuRD4_4bUqUA|ys^CS@NU4wZz8%$dcwN@npgXeq-1&3rLF^Q|P zC!}Z`@%R}ubt3OuLncB3)7PPq2^sjk)O1f%8D}3XuUif2YjSlrO0oa>$a5*#`f2$M zrpVW}cy#;Aa!c@6Z}1&i;X`?%tqyqnwNX)za(IVxWitsYc8~KX51!w|j12@C64FK6 zuExHJC0p-UBQ!p%9Kwa&HF9Kmq}?g#Q*?Y%MQi%NmR7yK=X$4*P-P@q{p=q>~_xxI4rnk5Bed#-s;P{PSvU3{I zEDd(uvn1iQv>u6U;yepy;+Q0kbV6D`l}PCykzM1Vy5s%wgd(>+Y7}i$ac9ZvO_|O% zQcc!-yEAa={AJv71xa4S)5vtl8>IgJqZ|qE^Ao(dF~}StaN@gYoF=lq(X?j4{W`g@ zlLaj~;nspmL)bjUb-&4WoozIfHB+qUti#Afc7HXyGWp6q8?INa-9Z?Sj^DBq0o$a{ zc2XX;Ah{a|$#3M1s1jZ}bL^wz zNYMU#P{XhjvZ>MKx5^T#A5LA;U8u5Sy!OO+k(QdD1V@EfOkarzCwnNeWR|rpwcRF_ zIMAc?Y-8W>$?8~Amtz@b{#p5{uWY{a{isocBl4984l|t!!;sXCyvF;jin&hB zlBw17R#xbZ3+*=T5}8M`sH*o-;4FIw0}oD79&OFM)zmeCReuL6EelahhS#z3V}CdH z7ZviURY$>nZ1E$}Y_xKli-VgzUu!?pN{PYER5D8*^@s`T2wV>Eo+ax{tDs$F(IfqW ztYFUFHXHZRmALW>A&S8Zihenkpx^RZKqb6KE?$|ly;p^f$fa1xEt>fK9C^h12omk& zTPyV8S@b%3-KnMgPl84nZg^0BeI47-3o~71lN)}>sF3mdqcS|NL(bxs8nd6Ar}q zv#V;87sOQ)c`*-o32I(6$#4DLL-d@>ct?PI)y#ul{It)Mz^~eThR&q5FZ?}4u=X>S zMlq(nP+ALCoU;l}jC_X2S2txITux3^V!2CC8pk>~7k^#iBoUZ#tNWmK#`?gigkRQ7 zRfD0yT+=6hmMZDR!JtlnTDE!h-9|HzjXUN zp0l+)#loH7-A8LdB}_q3TSJF{cU)UOK;3ZH}YUo72in~;fzQW3{UNG zt>VE0Z+7u>SF<@pDSn)a{FSQCIjD|K$=|P|dFC`Di~}FfzrrEP^gEQeGrT&CDw5S> zBQeF8{Fw_=ESOXq8p;N-G*E{Y5oYrjzj|99oEq(MZP$z zTSoj%*RtE{@vhH}yf2de_S%T~Xwz8c^{5kT=(A7nE8^RcJ>%>p+Gz<3%Ml!Sn`&mA z#_AV;bc_u%WVp0?Z8E&b2w)34@BAqA{^lZwNbhG#->~t?PZ%C@wcoy>{Y>1kCbzT) zh3lwPhm+JD%?Q^_x02=3i`70y6fGHEH4^)5T6W$fuI~6_XSCBUEpt z*fw)4F?D6qXA9iPWylnfXP3FEpP&^YPI>jVXzP&8dW6CK#|;veCoc*a@#(L7;EyLv z5U>xsa#2U!Xp;-YXz~@|34a({qzK}viR9pT!5g2MiYeMc!i)QRQGjPno4&cf(a-^z5!X8K5SPEo$AAzc%fIJXME)~#&gZOTg% z?E5qH5XJgPzjw{~R&bY_gx#`|j3(N?>-ZiiJ6gHF*W7e`m~lsdT`i-fCZ54!49_vn z;JRv%4q1`TF(x@JQir{nQJVaX_m0z$pgt9XLCW*uE3$ae_AfHW0i4P(F-ozJ-*QCk zEh-mgpBpz1WZtFkPtg519|8|Hd0e(3et`bz7ks6Cn7@^u0hwfN;g0%(v+%}PnL24! z_9lh?1GmH{HxgV^UocA;Jg(%M9Z`Kg9UymIXyaFA0>3Zj$ySXk$!IBuSLJp0_;u27 z@_PfxcXAeDCpdHJFaG&jw2Y&5V6__Z++N9nEFHydr(=gFg)CCazaJ`NaTr#^VfvX> zyIXbI0XWECG+9qW8Od_mzh3=KDo0b!UDZCS^Ii{4HH}FPJI77c2@M1IJB_#*a*zP|Fd~%{i=P_q>hsbBc{5(`Ya6Wd_Bj(zI&^cEuoE^s3)$? z(RncW(~*ptr6#L~%OiRv#w7m%&ewBUr+cffHq1z8=DV{NFA*8FKe@?FtmeZ>AkNR>>~xbR~t!pm86 z(@M0d>cVs)CV{DPjLctFN@dl0duO(AO-h$|xWi7HqwG=L?D?6m$GnF?=3YuA?Kaik zV+q_82C?gxi<31;n<`EjcI;Af`YM{=4u~cSJf_e3g->`VTAtK0u(8(@zUF>anN4I; zs!#t<=8NhT#+Si99+WIDB`=Y5n*jLBUeS;vMS!G36jlbZmuwk_35u8uH@I9ULKM+HQX+Kk@0bA zIk`1ujI0v#Q?Xmw>q}UkzTEO#jgUG8_8!6jb~(3~(`3URDF0OWk@k$T<2G{33fjJD zDDIsyQ?^#>6lYd(@#3Olo+YraUF z_~X{qOf!Ovpi7DPOjpEjC`QEG2;fS1mt55(YH+VI1k)vJbnS)Z>sjS%znewb^B6~) z6sHqPZhF1pomA|9JJQc#e1n%Syg+~p|7P)(&Pe#{h}>;U*=vnkll*h1D&Jc~x#Iog zB0pwkybF96G9fjk^CvS(!h5YnB(p+o;OV=_HC3wmqo#>>-crFO&L5|er8%qX8(Tti zvl*5}md;f%74zFTsa^pBsx1Y^d1-yH=@!4FDC0Ykj6}h6H8XSOYVk2_vU7}_ z`;B_~jk2kI7Db#FURTPx^wEaH%p|nUo(VnOEN-qLI5o-@7h`&)qp7+xN23zQ1>pjzZ`Si)+``@5UdM;M7uuoXx>Ecpx%~{+=9Xn|n&k-m{vEyHcP1^XXuX&A;qiPH zb~l8h&owYgWH&$mPA8K|&0t6^(W^op$$x6-%pAP7@g%C^1%IWss+W!>lYhXu4(v*?O{sy$kMrrdbo>bcR#lyA;vDRZ+jvz;cdQ{HUbeCo zM_$o_`&n8A-whVej~sSghZ#h)Y`I()uKvEFsEDsc7*;qkyv1{MjTBBzLYqo*d_+@w z<9-wwQul_9s5Nzv$#5b`-2wY zv!1~lwt`{akXQ1j_B3Rf?Zo@-y!Ra_0dN41$|veW8d?-^UJvyp9wTudC?A<;{?zLK}g5SU3((jGO-U{6Ot zDIs^J@s!Vr7jNo)kzVjK-A*X+1?RWjEG-Gg@jJNAkM*U?WtmPzSRe&v#F%o~v}c;{kVgrD3u zZF5>j&560Z6D!%>P%MYLL$gf&EY9~K?ku1By~{SC^E4!6?_Z04P=P;CmAz0AZ|=7c zN3qPo{_Osa-6q-dxmr&hz7IT+GqIk5ox<1ey>E;fCeE?vP^67-&EGN9uJsO={fyLB ziI=&2+F*Mz)!|ZEZ0>Yg<74Fu3&31K@g}eRK~MP!My}kuz5ddh2@AyUKBloB6hA$K zRi9UKv+6Ud_|Lffd3+U4kY~Fm+V*lI)`U<@Ze6wBS&`W4Hcwghn5v4xHcgP!*wk93 zaFXNn#Lb0E1)r!rwzMOX$lp2M6lCkwB{C*%o}OUz2u$1MW?ENH{zyIb^N5DT zDhL^;eG{X-Y)?okcEPqW{h}1%55)^O?4jQq>^ZN_AJL>e@_+@M(OHLzjwBEEF(MDB z0}Nv4UbvceP^)%u<$jNQN6Asao=m9u;SO(Xd{95;{pG~{0wT*lxUStS!p^dF3hl*S zHFeR%sAy6Uo&mArD9Twnkgu15^Dsc*Hn71G04^KPe zdNZ_;DMrH2qLm*Vis6Du(L)iwFP6}NXCj654-)>|etQk1xr z9DiKyRol4n*|p|$ysZ#HsI5ESw!>ACIdUa5U8<)osa&YLJ*-DMi8_t^OzldW&{jl~ zic~X~{5_f&;RqV>I~aY+EneRA%jI6d{5Wnf?IrczZ|mD@ychNdK8D`bTUB>=7YODv z^m}QIVfcuE%goviZt?C6EVie|+<2$G^dV6b_;&CUmzOMK?vI+ECI|Bccch1HlC-D< zRGB4NG)3>dmOU9}=I#xs@e|Ef#0i>Dtm&EZ{|;|gU#MbH-!-m&H)tKVa%_4*o<@E< zprH4|`-J@cfgb}EfeizaGQe|7vA#lXoEz?fG3Ah6jah0NdYkEDNw2=O9r|Ty(bB|hFZO)lnI`fOc-t5A5zp(TUlKc zQ^JS``kIL0BV_&iNZET5%AVJYY%nhcnNm^dm@+0S^kfoFv6%l_{6dyYZpk61kxp}8 z;YSD;|2&z*)jz{&cQ3nYB})Hz917FRL{B}ho9H9EPepW#ikc=;g;M7$C;y$7Eiu_F z?FyVi_hJkk+}1NZj(c|Hbp6?f)DKmDuPVf1*a{yw5V>YJR$TF2oqfhjM-?%~W~5AA zB-b|+C$jjlQl82J$2XUoiy~i~p18@7TVW--HXB{f^;DF+>w4jtiD zMZ4yVv7H_s$?Ov}_nyY0ekm+Z?0r_kV+Q>c;K)Y6qC zFhYpZaE3${YF}HKy~gHyd#TdVdhw=b5xHo~r}^9Xe)w}HEL~r{Z<6I8Z12QedU;&t zN~ilWj3VVqOpcHm&Ae?eqhft1rVQ6!_>dCww%uJtC%T5n*eJMzw*m1AOQbo^H; zv*&qS_)j%hyx5%LX3K~<*xjU=e!XPM$ZM@deDd!jQm>PFh0j((lg~M0jOKe*;$0u{ z`jE8Y=|}d6*VoPih3ORUsv*de(*CeyhJ88c&BpAXZkpkjJ3P^or5$}v;F=V=J>6+$x+P?I*`pD%Hsjh>2&3?$o zx=ruME7Or_FH!RD+Kon5A9r^)ukNvwIDIZ6SG;9<$>A>ey>{>PklwH6SwkuK&u^A1 zmq*^_!RJB~zxjl|XC}il`&DoLSw%sUhoC-&b+6OShGgRDg}r?o(`_^5oL;_~_|gx) zT#ebwRX1_Ar7!4A<}gU;r=pqhX?~ssOdm*8e&s5N>U`q;d9D7(ZH^#?H)VM5iS7(7 zDCG9a-;3yg`W#FzMp?<6) z8%F2y6&1CP)s9($r#x)8ILB$Lek>`ICVM5pj;n@sbnqmv>x_{vQA6UkEex6htq+#I8~RZo24rMd~~nS{cQbyM$QiR zMJ*=vG?Y&u?^Q6(EBcKkJOXv$iWh8Utd9wVo)cuATfH2FFO zK@$ZRzUZg+(FbqkPRYtWYeJdw#vO1Ycwdi9X(s=vfQ+V z$kGU#tk|x-CK28AnR#-p;SQ&2UEP(4hu`GLF(Kq48tj*cm9?1;FO>J#hENcSB6EsU zo2-)CTqE%a5xraHbixA#T0FA@jT&kLwN#IeK8C!yG(M7Gp7;4qb)9|Zy@Y1`58V6D zxd>`had%aHv#7UO@VIWt=i=Pada8Q-_Z9P4C-!qq2$7t%HTV2tHIuNm;>W{Gmpg&? zF(vE~>J2UCp{u^f@Zr|8QjYulD~<|I#Fs2g<5&(!J| z%M*V2{!w1SB9&&1T04ee?JbUi@XUk}bq3yf4PDD`0qs-@Q{0ju7Cf8QFC26+Qn1uJ z8-rpM&s_J9-|p#zq3Ev7`Kb1A^ASlGpBNE8|$ zj4N0~SX9SvKI3lvV)i5FYSVU&b|#Bq2cAT%uF84?wYtetANSq&rgM&c9W|+ORQ$+P z4AY+h!@HW4lYC#Dka!mTAEz|RaTjAJ7%$~J>FADpTClnUF>I(O}A3AwH_T9-hO(?xq849_c6(VFdK_Nx!np0U`JfeMAkRu2feO5j$mKXFw(sa+|TCM>GA(0xCo7VJC6P;fE9CLYK7b0_y+ z-CV&P&yfqGX?ii@>mITe*I&}6OFxb;dTzBoxvk=27U{s=)GdEMv4FuKGK7M6ZEPaz zt$xtsx5*a?*h;dm)ZA~Z3(3|H(V#$&Q(!u)b%}7adqQXt(m7)nnij2Z8pNxh}KD#$xqjxF}&M5??39Ymc!N~JjVAvSQ~Z@TAj7QRZ~(e}U%6zkTp3(1rd$1Uvn zh|jqFItFCDW(l6%=G(JY5n48uh1;{_4pwmwn3b-!=S#Qb+T)P15#I_ys&^0!V=g#z zw)iMm>L`<%p-x9|0ywyg3RHT7S@1Q{B>DEN9Q17Gk+F0N=EM2&Nkw{hblp*9wxr_? z)gv7~%!c{6I*NuL3#w;S2@DghbkcEL^3j|my8ii*rST`OXo4its9LYy;JY(_Mr@u- zh91FV>t_&~&Nh>Sx0R&N5}nTrF{qh3*Zb&%-==8~wK`@WRt21L38#?Nw3}L_c{2^s zXHIlRvd6Wv_53zyY_YGBYbdH3S4&*`4W*Ya<7a%4$xt{Qcg z9qz3TzeqLSeNgdR3VkuB8`ah)vQ98>O8FqYg=$PH1#a-j7@34;+Ew;p3izG67aAvD zN3No-TyuAL!Dn%lIVD^8y+8?qR|r{1d?X#%+4@*S`Dd0onVG%4`1xssUqVAF z2aglNI;fEn75|8k=^p37jrq)L;+T&oHBAGu<#^{q2X^IKa3oH5JIkZF21F$PLfSQ6 z!*9x;H`?%CKJk^cV(d~TrKC{7SAPimddEDN zUSGLUDmy|h#tiNS4=TkOXjr%wr|(!aP4@L)S__$~8zStM-=OJaqhzF#jA_i)E~hOb zkmI0OCdk-+RE9I~E+($xq`n5{~30 zn~Z6*OcWiu=p#DF_-~unWU?w&m6H!WO&yU`)UDb53i~6r>>%}?xl95$inQXpP35S) z(ew;Sdn1|mhxD9igf)Uqx>AiM6sOjC^}1OX@%NaQNE74h>@Fv+6tW9{Fn}H=^N-ao zBstw<3XZSI9*Qr*Kg5|V^okVEEA~_S%|OMZrERyE=*uD8*2(0~;*)R1=rcAYrm7}? z_l8^@(b@ZceTu0#H7%O@?jjF+u1;r{i^&=`m-BHyWh%d2M&>H|;64f`YH^ui(k3Q- z9wXFX3Y-&j_t$NhRjY&Wb&Q%_ej3i&4`$#GbS|!qJrbG`CRj-w$>>H$ye4!%v z?Sn9&zz^r@)V{{bU_K|V-Dw|B-njVc#RCTI#>SOxzfosyGCDkD3t_AChlkJL?WE4ORuF+n%uHWav}Y}9iwdDL}CI0Ks_ z|1ty9k#+MY9DSwkAsRUxwGS-8j6`n3!h;pz&#g`btB#D&L`cKQ`4dgoJl+2?PCpK-a8ckjV9KmBE_(cm2F?3bg#NI+mz!9X&uR-T`fsJ!ef2k( z(!7DN9$qgu0oO1`wAs8yHtM1t5AcU+L}lKDUd^BnWpekR@A7+7Gtxm_MA-Hf_|Bh2 zf`-sK*^?;3JzdLp37F3Q6a!ju+&@#i_S!kfA4YK{P#-;yqwn%$0$Dm8d>P@&Q22c$ zN-DqWp<2Mg5qPknXc|BK*9f3bY9a&r$c1En$82aQ@J08j{ukH~!N{3?1R|w{=^ro_ zMF+cAoFHjH?ZGim@X_3Htu6~l4V5b~qsc@Z27+Z>)#%)D-B$?Hu8kTebhUCP>J6xe zd|jD2tpjuWUu{3(^AlPXNgR=ZR#xuv!>}%6VN|e2cXH?x+7_5PDC=6S*O-r?W*KkX z{WbnP=Zy6CF)w*o{useCK)zU8THbb}-7-mJF4X_EeifdO+1>b^p3#$L-r9}&7u_8~98+EmBvS*%pLYBj}%QOw^Zf=C$1=W;! zdT*^Xb*=&9slr0C+RlitIOF-2~#@Hg>y^|q195+6&7 z*8UNkbr}xo-qBMBH3v8Nnl^SB#RVC;1n*?^n(M)&ET5*dw@Ddtgo9EofJBi@En7sbgO87`YqtyKMoCH zM&$YNLyo(!^yJB1>b)@0-|mo4iuiKvW!Bp0z|=fV#UjuFd7`5Pc z3!=K&qm!$aw4Q4=KDVcK=$#+ZIq7CYSpw#RjwlCdAw29I$eIvg#aoA=&L-!t#QMFh z+j-`Gp}qG1_VauO<7W2z9}SGx%rdw`Z*Ht#fP5JL$3GQ!aYAHdAL(U6BXXvzGs4%H zC$|S~ z#YEvqF~3Zy#hA9mCjFv}j1UY{|0(-7R%!vmLxCzv+&bZW+D}*j$rOKGQHY=4RMm3{ z7|t2gTJ{?&(d1-{GbD)d#)ZNX+@HKQbDRA{5KXC(4>=QHIWK8osCf9SQlo6P^Fpdi>gAEZIy@{Bkr7sPmpjt_`YJ~YYACGnQVh}dL zjK!8CEOx5cE6B)UxZ)`s8NR}kAmGb!1t=Y@J%MO=l=GWUw%{`H=OPF5N{H}cMzxl^ zUQh(e{(0YD^YYufbc*{ghbqnFTc0l;@&^?u7fHDLdxh{>0)QA!3=~fcJy~yI7oF`OyW@c6$Xaq>IFethCiKH0DPHMHwmK^jNim8 zR28sVvwa@DX4PTMyeIH-XJi^q@W)tDk{ zj3M<;W>8tZ1LBl}5}5kkmarsO5(&;Cp4JyLyOaWMh(XRbLyHR?TSgob^=d+mAheu1 z@nX7H;ZP+wDFR;=Me;X%RtP9LF^Gx__9gT*nMJ=58$n4i4dN+TO9-2KPB;udCW*=l zi--)(hKYKCqoBXIGb~=Oizy91sQ2cN)6M{uoMLebxGE~15ReTXu4V7CphZf8`|$#? z9UrO+TFB53ydbN=a{Jc+K=Ev;oX+-{@BC zM`gJWetrTe0sKBqiBN%W89mJ@*qnLf{`PxAk??z75yP~)09%;EEqH>-Qy0va|DtY| z+;cnQT_Uv{|C4KrBb6hZw5KboVN(MM8Rp5l)E7@%sb@6JB25qA$*F83& z9hm|GUUxN=FN4)(KG_=sj_clx(70F7p+#u7{yZnD&fO0PmSjWtqV4Zf36YkN$ zBEhGa)YTZfk$2`6VC9hqiyA62PZ%jGTNmnm@)dB2Ip86o;r6|=-CsMt0ojqLq^u*9 zX!$s1m%s$SmtT$)X&6kn{;>Gma@ZyElr5-^Zw)hUa2Nn*C?O8haAfT2Nf zn9kV{Tp}dopkA!`OP7mlw}FmWI5o16&-MXmwD!fOt33IH!wC0P53uy49(E{3%pvHk zcuP-f zaWA)7^m?E5H!g4rz!Zwf7|$V#u5KRkc4(rtaVzS``I*qS3qx#TD_bF`EH(5W@k4y3 z$nx1wV@vD8G%T&DVBxPvVO!Tb`{!Yk*%cA zvH?|U2Vm4H0%eLz@4#PsV3dl2s;i?^7_p-Wgd_;M>=!bOumnDrl`?39+yrxj6#Yje zU%CaZnl{PVFn_edneqn_VWCv|swG6j|9s1zW&kK-9ri!LKHkmmmP5G)&`CS!vI)}nnmoqGbrr`%AD5jpm;@+Pxq zrLIIf81);&4*XJONPz|g46019?*(YOoDt!_O-tSu|0X|#l;yxc3eyy;zl8r&1^^0B zsxu-lh8~yGUh9N^Sh~E^)2V?U*(Vtc`v3Fae%gq!Ys95Hu&A7p0y8jQGG@&hOE8PW zpO`)bs0jqmKF}aB#2P3$Z5MtACK-v@f|!En=n0LGHZ=yc5mq|>DD%2%gITl}-|7G- zXfu%O3gT-j;Eax)5P1Etp^l%`-#EO$a3uW%?Mjsa7gpa3MuX zFdq)cbaf=_ClZJj{}levF87#rQwZsp$=d36WpCqD*8##nlSl;W=y1%RjeeUmRWdB7UhFFE_+ zWRz7==hqw|>0MiqX~K!b%}z8zD9+?EBX5nP8;?J5?L64UwwDzrxn5Mb3m$JBs?s}J zx4c4L@u%j(`qGliFsdZH-?S#mk}Dz(`1?ZjiM0@ zA&l?<-7qRT4Tly;LV(V`P6Yz?QPW}=0`Ld1m-g1dc=cT&8ZxB~a}rNYH^I^v*DV{K zK&7*~E2h4fLDs1RS+Uk~(j$nM3|maO>ia{ zqa4YwHo}n;RY8RQqdvUkIKgFXk;WD$*zRA1lt1c4o|BsgdZ$F}`!7ficj)9k zBFUCKwj|*Vkw35|8<6sblIFMP{5t4UO0ZUO zCh^Pvk?nBM^kvVfTgaYZ9;lAIO9%M~Q#@kIr*e-`KDkUlOSgp{9A3EjlvrJzSiTPwtYl+$U|zYiH~?MMjhR_w5>4NH<_ z$vmtkA1|#if&?P9Lh}kWEh-iQ<|gXv@BxJ{9n@0JTgPlu5?e&ADXv^@wed{8-S(c1 zmnObZY7_sMr~aDgrhQc~@Hxg9aaC7=p?E7uZ=Ry#W|@8dQIMAQn=jUeHlM(TL*x5n z(4HfSDpjTMI{=hCw+M?q#jy;2T@4I#xWl7>TmGCb8NsM_s9pknuFa{6hXY=5v4D%mKWBq#1f)?-oq`YN@xS};TG zC=Do}V%qIIUTL=0o(F$be{}K*&6cDDl)&c2zIVKxjQ-*kIpCQU|1j;QbmOE~?ygL= zQs=&+BLzBQJFr2|{WS4uj*@{UaY<0OpT;M>qMKZ!ppFCKv>p+uXX;>|$X}Q-YJSiG z>o8S#HJM@fUrBR_sDLRw$pb`}vdbbetl8~Kfr3{UA8Yw`jzm|XUhGlM9^Zs8bG+S) zM5=N2zCQ($Kl(!^1q%`&wO!%G<9YDlMnwpqw1K{i!`_vQXpixlRNzOSDA zgcH7}Z&OM9I{+q&6-FpjN1*W2Mm{R=EkHs1=$$MKLq5G2fUvSZz|8GazF$r&f}aX- zU%nCKPlCYAXF#tguO^yY!4O>iuswXb{;ISUWFX?&wIxKVa;bs1fA8JOuOh3#|D*R75|!yCZu2K5!vu?G^%%cH0*{@Rj<1(i2qBm zFsX*LUE3sXBCq}iN&jxj1?h#;<#>5mV)!WcEl~5aPR=4CkudsoFb*YPUST1tJTkxL zqH0bH%fetIkT$k|0igY5fQq<(nTSb-PXruz*lN1EK&OlhGE!Qmb)7-a(f9Rh?dkRf z(JbyyUGYIYAy!MWh>@?nl~N$vBbZOxF3QJrH0C8#+27t{)tHi!NZOZE35_{;;cDVK zR>Fbb=`=!3UVHixYY6vzt)0xo7(cZ#fZk8@WmeTHMD(nx_I4MF@lB5YF92bYd*G=H z{uIar9tQ(E^5d*r6UC#CC`@)MOWIZQ-c9XcgtS<>M&#*ym1Zo}el|ENcK62D?fB;A zYe)H)4=-X_hr>eJ-s2H&fZvk)+!go&NQP_hX{k|_L5+ju>0UcnX#GDAa%aUQI_h5m zZz|d6$$3Tkcrg^H!e@>|U2$5v3gbHiQ*6CC5+t%tRe#5ec5wu-I?ab8+bqbZnr`b-}w5WlQAm<3|o9c?^3D^>&*c&&TgjMjATY}V`t-{zj`VL+~f$76f-)gnt3mLBz=TkUUP(2&x; zXLq)SJO0fqCf<+jF~=wEbtPgH?y3mE|H@Z@NsCQIe+&V%Sc@I*WMJ(W&8{9~KP}wK zvbX0uyAkSRe*N8BFVo145@)F)^D;m1Ve>tF{zL9(yzSq7=YX)>@Z|sBXvKWW_x*2M6}zHmKSB|uE@g>gZwt0{pe>f^CSEVW$qUX|W`c`>uKvmQ zG8#a;KR)c*tUHVH1L_$ErwR59B&ZMf$HlQG(LQk+0aMA$g4lCGXr=6DHzBr;@E39a|JNeHEW%D5)d$2e)4*V zhFUyj=ythCKJ;4Jv9xck0p5VPON{X(=>wl4r;5Z0f5jj~&RnF9h3`x!PZRoP@4?Z@ zyVC3(dc}8T#1QWB?i(s@h_q^Bbr6l}3j1j*A}#lClE3YQ)e5+PDfrt&)Pq^wB3p`7 zKS&7zo;mKo{l?ka&46CV04DrWn#|KzBjh&M zJ_qR|B=!^S4Z$xKmX~Ocs~&>yrO0)BYs3^EKW|cQfhJGeS?epJjYrCfnM6uf8Bfln zw3wpi{{aE<`2BaffE4tYlfoQrZfdpruUqV=2zpBf_(4w`t!=#hN_2eM=+FB{*M2)}WLYubk!@yjtSO5m^9Y zgH)vvX0u{X!vg8afEAcB==6liKJlM2RyGViX2uEZWq8=lkC|k}hd+hbvA=3bM%5=8 zyB#AW<#@xh0V*@Yg!?tNXCJI zm%kjG67c_`1J3=muszVu5K=!AnMhG)Q!46DY2x{9sNHC)Q1BYqA-6xJghs)LToNHB zCxb7mWlX<+oeKYCv8o#^#a`_8g7;CLoos|6F@{ybJgtT1No09zasm@VGOn;sM&_7I z+J$7V7O7O=l&Bu&P_hx}aZtVE$&PtStQ}EBE0#7`3LF*JtrqED?)NUvkWc4;*hj0T z&zsXua@0Dip<0{JRDgpjM{0%v*K-OD_fOfQv8WgOB|^axN~y-t@Zp*fiG#154t|+9 zgG(oSXErISyqtc(q`r}jVnXr5|YMuT?r z%4lCBb6crC0q;UwEsF9R2;2PhYK)MVW=w=_pQm$Rpn#h2r6pJBAGDb0oHV^!t1lL< zQ%G90u+!36be2z2D`)}FSUQtD5hM{CFN3S$1CD*b&A%tX;2~LCt%4_hkcF{)$gNUN z^2UeUjs@1GplOebVQ*sO#rx=y7tn%g zz+^781X6HK`9~_|Z>k=Aoe9CKpv3L59x!Nogw{)P6FR+6Q0DR|R$YjPxD5{-AgB(z zK}AC=Hz^9u?ecs|i?9ju4osKTz48EcQCIz~qDkEZ#!)!mO*2a5yzr=81i_m(s(fl< zyC8MH9_F}zS%xddZtcdhM$Ic*2XL$c^gh>!YhGvy*qJ~NLS34NX&kShB4sV)UNR~* z=RLvoi#QmFAtik=L(=<7(H?xa$LqbnO@dsPHFHi<)#F)(wNeg}l71UUH)`K7JN9bM z5^65zOUt=bEruu)YW@h}yX&AFp5wbZRkIrfF-7S5F<#lze6e9v#GW6kX;t8WyOzW0 z+gqaTNnxr!o{{-K_JW~hj)iu}#jp*N9M~TzJMExR*dlN`@C;ced6t!qsWh>_iISb} z{QBy}EJTSUT4(b7(N98qY8v>Q-Q(R&_Z|*DIa@u2Rs=&R8t8$+*x_tPFa5&N2>>@Q z#3Qi!F5V%TF5i>TkQU5y&|hu_Z1*}-UxbY?}0HW6W)5^#&6Jf!o(8!RG}dl|c;ylf#mf*)tR- zDP%HkiAhmA%f)+CmaY`AUu@dpukqmBa1IH2adq7_td?#q{HroTe?T^isl3<5$WWVJ z`@;VWdvu}I&S}jfZzn>C(%{pegRvPV0Yp_^M2#UVRnzRHCj4EH3IG5*&y1Q2~$dD&b_0 zL1~^Tf5P!h(9#YmdcV}Y33q~aO+Jzf)Cu%iIXnsmerfd3raG_EVAby<7BDeFAzEG0 z`3%%|pTLkcjdT2RZ8aVlFV_$!dRE*8BqL*e{H3}ibc2Eql>n{3v*chL0<^SajJ1Xc zB_23oCeG*6b>!1 zz;|sY#7d7sG7C}`fu4S~hmTI$@*tVD{ za#>QSK#pjsph6tjD2a&-+jz)`Bp>XF!UI`SbqsFD$siWHf1`f<$%0T55wAf!}fPH~uCNZQHMo8NftU=Q2riDKE?A>3j&ZTftsyZSuy2Soqp_X90 zZF}usLZh%Q@bjTIE+rtO9s7AcF7)Uc`p6;pr!?uR3%X)H-i&Qz0m3llSEW2VTPD~q zzAq_;U|{wgU@m<sX$`7h-2aOCZ z*wO7aZjXDAgl!}?M%!XRGua+c)uuQ7_veDDV@5kj6E$e|m~)I&H-EzS_1_B9R^R8Z zF;Nk^QA3_n4#2+~;Oj^7Xy50b1Mat%bb+I~8z8Jy&%3HthzZBfH(yHvU;lxZA?K-s zsr-YT9=D`?R)Wmo=9%ilf6Pgl9i{3xO<3{1A~)pdo3p0th)5HgE6=O?N2d_9H(|e0 z9W)l*q<-Dq?t1?fx$>mp{0{qQiF50mS6O)PGq{N;lde;)5&o-guuYTWvEPL;o~M~i z?lf;kcT(C>>vqePs0MLa^-8@lWy~}LCOW

    37u~^c5aPNHR=6U1|#@yPm-CQq9{1+f;t+J5%NwS7KXoCVROEe(2F$B;?Fo z`zA^SeC-=gzvf)ZHM;H~$#3XunRz3IeFR{i9PrpWa0t}o7e_K=??)!X5#pAU!TnTn zJi&LcpstsV{=e3^7bGf6U;enEU#;9CjEQpQ)uwYHd|i5cBHWpC;N1dk?QL&D0m`O4 z!iHvq8_#gp3A8SCb{SAeqjHt zIw_BzPz_Sz*?ne(!s)$Eokw!c=pS9rf@nFMVxPx27)1^AQ*ylJXT(Juh#-ZcKX(|Pis7f zuF3+9Am8SI{^y9LnGGifj9L#Qxr@eiE$J|JF{RaD&U7^&zQ;+_;K0aBDeh|>ziDx6 z#!?r+V~0JVze3KQ@5^$zum!q-kl_g}2{DW08~-UIKHgd?uhnky--8U(28}Y(UX@9o z&9orx#2A(Y@cU1h&Rt*=Hw$aDORXJ;jiEcq*k3g$S;lJ{x~+5)&SmjI>~^{sn`|n& zKoSR5)qQD_iSVYewbowW)|omXA0ycZi_P@PT~x8ryW+nlQEi?a_7T+L3L-?WV2NyZ zh|O$s#PnSKM)+c zVn{bdO35HQ2o#HVb}LW4EK>WI@*DjGjt4qXsUI`U4IMfmE?|@HPA_8I>^&=#M@}R6#^LMr5Gb}{;#G4_#Lx+&N#%ZJ--9WKIl!jGXcq^1hSqFq_~2trt@t> z55slls<5P|C6I>j&)>^=f+g$2F+mUvyvkHm?|`~UmKtK${(mFmFP87{SWnFCujT)q zGn}t{0GFb;A;rLx=&Yy-m{Qc@uIjX8nBHrlkyhR92tboA(Abo6GF7Uz0U*mCExcC2qxx0c7 z$Fi4R+E;*TaBxWxfih?p-~R1JxhieULz|0vNsxs8=aIN$|4dNa$WY>sO&g7=p-8GM zmfTA><^4a9UbFsbg%+G~*Ui8u$8oAEe)Nbi9woiuljtcG&4nL-FO(1{aVtM&1LcP# zv~NNNHP-8o4uMx*m}yAs)C*9#oo}rj2IMGu{Ag#o(qgGXgF=(Rv%E+S{q54oDm>E) z@}?Zz$IAYk(hG=tPvWZj8-(V!UoTV&gKBRjIR}%g(JTgA56<;*HcFhccsIoI&ThvqUam%$E8oNqs2q~ zDaq_6?}Tg6yquW_vg}PkUoD`1v)9 z+|;^uhCTqjd#Rf=Q25+esIUK{LqvW{SWf>x7frh7?DAWxEHM7UHX!DMgKk!mmpS-R zT?cYMdQtY{JG)-%6$MNQvsb;})G#xvAdQ!uVVZgCjC8i}%t-D{06OHkasO^#RM0P< z6X#&}=AMWFK(4vVV|(23SUYQr_$MU$E1eTIC>$haOX*M+zNnJV%a(PB{HiXZJAn-T zOp z_nw1NAKgk+&dqFUHIS*}=SCfSe+~Il?v02;u3R?36IdkNzA!OUo`_&-Gt{d^n-!Mr z_NYEaji;=RSy6x}!O9<3#{b2ZMHsN7%Jgc&!piX%c zHUj1X4I!@~CESYy?+jz298O!$qJ3Hx8v_FmD^wH7V@e1C6Pv8^R*3fSt``%+>p@D!mbgQ@T%JFLV>@q0|T7eTcGj$6BEmgTRW~4-;oJ2OAcNleK|z`9DbP2aa>XIS?3J zW)cuumcGays7{ii018g_)hqv5TYm<@`oLv?)VJLsQEaN`rv7E&^w{;XjiwkOzF>>6 zR($xyIs+h^XsQcKeKnE|=bTGx75!ODRxPy1P3D4e39S}xcq`eeY-g}QL*A}-@H3ZQlzCJ)Q~bRJaYP6?+1JqU#54?j-%V>+nesrl*IOsKF41K_cG9@P3$I^fPD0LWp5GtZ_aJ zY!f-l@7(Eo+@=)O5O!Xb=}|twAd9A@0SL*NUo(*R4ix+>JHNJiF)1qd%MYZkXJ_J! zE2j>(IUjoNbdgfHOR=a>TUCtBsmG4RLh-}2C(;ubG#szRRk1A=-qhi59~|!8gtW~T zi*HB95z*7kFtOvrm`M;6Mm5#LBwYkQlb6PC~XYP;%NJ0!y*xQ_T(^B<6f2j_I*odx^2dyA~f;bL-8iQ$K- z;xsWOvzFCMx(P>Gn7|0V^p{&`a~IMnPbgmlR@0qKoJx3m0?v}1Oa>XsEQAzObU1R!5+|X74h82Z33-Je{Qxa0 z&Jujir(uYZ!(ebh1>OFdf=$6!Zl<>&c8#!G*Pn6nQp1T%64iz3n6l_wa394P}$nS)@(Med%};(&*vJ(KOs5yC?;#ctzE;R_9ecP|bW* zVf^vM-ywU$9@VRX3_}0ZI;|`$T>U{@^*E{cNvXY(^#9 zqkEnU7lQbM^`#0Nm5lDTiX}0teI*j&67(qe^ut_H=4Ru`<+N5MmI$fxlvx{+7j>e?V+-aeTb?ZE(kmkxwz>?o;KcX9hnPr!v9DVTOS1$P^BO zCNxK1LY?#|e+f)y+8JZg)a|LfwZxNPt_LFp?n6mR+*5&33Z7krJHwuli}cc0nu|0Y zB5WwS7{w4oTRHToJ2)k=;20Uomh`~FwI@j8I?@N-QR1A01l-?@cjil`g1#GBu*Z*l zEt3T(FM=%ycS?U|J@_c0|Ci{IdRDa21JgNM^t?5ET%uHrypxHlUk;rrqZf}0hJHrG z4lhbZ(ou}8g2H-12Cs7!5$)#3{{`Qd5_cHM?58BxDH4crJyCEHD#m}Cf-|TarRsI1 z)R17wyrPZ_PR@o41lLIW4-UbJ#@5EQ0MT6n>(B$cJ{7`BY(2!I+&$m4l2D~Z5!ztR zO#@wI(f1%;PSw<~i^R^7;{**T$~VT)c5z@Og}VKS{6|x@Gs{5xhL#M$wKBcWMH@;k zz#7jUVXx5WAMkmGM%RaAV&=hMq$@}zx`Zy0@wB4{YUug!dH91AZx$W~U!VR6sM`q* z$5PNa{~Z1Lqe7HM8s>oCCgf>lb+YA)dN(HMkMitIbAWPiS*xQG%HCRoCqRP-+9hpN zvevEOI3`3%(vhMJ3Xke#GWfpaudWjSqDLeEt&Vp6TCvw)*z zifNyxg89`+^HQCl9fs^lX=Kgm23hrL&EKCq)#lM2 zy6Rxi2NKhU5s@Bkak~U{MmU7zw(yDqvj!!GZl&v&Et*Z?8r6*>3(8OH0!0|KD_a+C zwl2a{?lMTD+P`4(uc@J_fqz(Au5m>bea}(+3h$nTyG~fM-_2j0{iR8!O`A}~bV4~Y zt*q=S)B}Ij2(aEujbi9yQRUGxTDM<7GgFO4)J8I|=Krz&XHGa=jl?e?dHTvDcM)b3 zt$^d(rDNG#E7!UXZTpfER97GLON!$ZjMSCu{7$Bon7Ovzq;euh9Y_^p+GjrH^Fx(u z6@0;>Q+wlMVXJC1gCWtJ#o=`1`ENF^wZ!|yBLj?Z0~J?IdToTYXS*%w+e0lYufzgt z=X|mJWlxWWv_4o+M@?E67(~?K>EHN1+PxtP#N17Eq?5HZ-b5vbh!$PDMbiX#eD#GL z?68x$UDw;V8B|uCL6Ab{x*hwBP2V_TbMRF{UHu zo`166XR8^xHO=BH8X( z>e?;2?A(n9Zva;v;?%BC@t&oiPuMBWu%6Y-`VoRPRuytdgG99DEDsm-LB<}t zJB3ErC)7})630v1vk=DYMT570ag7%#siz-5%57%P8u)o}q{(@krN&RoHYu`{j$dN& z@k~c@E!pt4FTlX{wAe|i=Zs9g=FiCu*UvSav{S9Q&m5i)#}BYAPY;E`Ery4ufp zJ7Czbdwz_PLQ!>kkKSoZGivjhFIIqOm@s}8B@6>HeFTWJH>BpQ%!LZ3YJ~Iu1`v1nYSoG+HQk~D}!8e)`XI8jqk3lwi z&q?G4_~C{tAHQeK>&5Y1Ft6?F9B47aPcjCe@Cd<=rDe=ZLTrewWZQ^_pC7HN_aC2c zxM!?s+(fDyv~b=cBUv-KHvC_G#SaWUBVM2Z{VF9~^xzCK$!Ef!IaL5;ss>w<#a&(R zn@`obH8XRG(UMi=#}gIZX;CGXM2c8+0}qDTSi!T=Yc8QO^gDJRiJwbaM;N**7ntRMS4KdF<$650Za6{$?G8@C8tF ziSw?Ow^$hS6x~@0MxB&Y$}oxi#$#5}<(hcG;T8E%To#z06}Iq6#I8AdfqcO|1BS0| z!;N6Rn060t8+0em8up%pQ>k5FdoIdKqv-q?k^lJH3%eT6)9WB_tDaP3!%w;?wC2}K z;tNFQ6xhjV!!xPQ|LB3;9S=+$+emz6uRf~zZ-zhHb}KluZ%?22n^f9f^_{}R(=b&S z8$Vu^VL*@%_?ke{`!L=K5~wu-(UH$6!%l^kTB(p&nGbl)3ALGc1hduO_qG|UeWPPe zYLbNluJQkfuzaYTeT^%8y|wnX@R>XRZw+mmcW2Lj{MG4-k9e+8xh-eSRI<~_Low*a zn%IQNxoS9?3!N;}tFF6qw~~+V2iT$)-epPW!~ws@XLuZ{gV$bN z$sq~09n=h!ifm8HAf@JE{ru6j1qiel`Z);~m(^&+;O|!YcpwHNl3xS%PgWj<+EwtwKbz0f&!V6u*EM?CxrFl5zxe|nnt;1B*yt{o9i-Q=M@& z|1wtYcmWam21|pC?~WhZJh`h#q1d|hh%^!>tAxAo`P|q69q_YT-Sl>nD~D@<;kn1I ziKNPwMX6p@n)Bw(aJgrGQn2kNK7M{W^dBG{SBv`a;_D zrhC}BEk5#Rn`6MGY?H|m9(@M~`?h-p54*P6IXU*s{Ul>p48sOT14OqH4!qN!hRI6J zy_`noR`hS;Jiu{JisG()%Q8K5t)N;4mL3^m1TJw_WaaPw@-o<%m{gV5;eYK$Y|>!x zUzwg5uWW&=d!DBWBKK-%v*sx~wr6c>s=(xW)&-Ms(cI0a^mn#uq3fq2*=TL_>6LI^ zCrY2uER|bMa}{GTjD&ADLn_HH{RpzKMk{T<5dq|<KWo)b(=lr2=jW$O7rJ{4@c=Ht~ zxxV$D0;bbtkPJ+#Jla+;(1CBnq=GaLvb2S&R*l?oe@2PcSOn@4y&tKE^heeI!OhIV zwZ!<&Xzl8}HV+Y+7LTxZjtW{hsP5>M5`!isMlc-L~c`}}9A z^oL_YCVnCuqK5h@6T7Qc*;;Yx)+~-+t-2Kg-@dP3o2+5o4_2TWt=?TiMexhrBH(C3 z0*{p;Lh8E^^h@ySOtgV;-o$BKr7AsgDIo^^3PB+O(3$6wXOAh`Rw5AWk28d|xm)WO zXpR7l;F|v4GYWS3U9l=RjcI?YyQ#7(^$ExJeitj15vVg$-#Ilz-y(Ap*MPs>7pztf z*uH6oTc_%=(635JiQcTz7+JZ!Vp;uAz>0Y5(1KB1CH1#dGX;x}E<{F?WNibw`wxD{ zrz$H3ovCI<3qmIKE?0-*;HY0YIIo=K^^ENTtQHg5j{EFnY~Od4&B)Cp(YeLSBz5h1 zj-&VGmZE~X>`m~{(XU#oX5Har)3CJS3K$HkxAXk*@Eg^z)08fy34bH$x&4KP0}>8p zOs;Xv#nVXB#jYMu#m5Xittp#09kLir{x>UT(olIkLdQQzy-liXqfH9fPp-lWn#hAw z?@j2rHC}$EQmKC`sr&9F1255FM1NfcRKc2(mbj~y`dqe3O|?9AxahcoFA%h=ttMuw zW;?dlJ#x@At9$7)^WuO$T;ZZ0xvf`6HL9+<|yZM1sYHQ`Ny;aev#vKQ=IG ztm@4>NFOOZJ8PbuPuZo~-5*Pu59%8q&pAHl%84S3L@B=&qA!QjzW8O{W0wP)aD^)Y zUFuORsKk?J8nPPN229$BC7x`_1vd4!Gg_jW!krXWWq*U8o2UPIiCK>UeHX6VzEM@U z>fDT7tR`SAO0!v5Nz={KVq(jD8YJmef9XakJ}p~{)5D{e@%g0kF+MPy>6?>cPiZCC z5p35tOnFGF-vtwO~f%jU5QTYvLO^qgv=zpBR*g< zhf4|l5c=kXy+u}G9iaJNFJDM&hs46vRyJB9OJhF}`k?KeLF ztK80*qR8=_$dCqkj<-N7JCq8JtTe!GLMnr&ZQ`^0WU5fE&9@0t*x*E=0+-d^EDQV_ zEguOrra!luv^k^ppL~QPyH3@XAe-k3jir^`p2}RVn_2xPlQXYs))IbQlaIvTd1xd( zN!PLjmX6Cm=Q^5zl(^;$I#!L6$QGqimeS!e$(VTV+f?@EY{;PVAXr>NN4KKhx{vh< zn7w*<`XBkVli2V{EHD_+Ih|z{2n)~ZFz2X^(ZXtD*FBe-+rjds=&6_}MFKARBCG_p zd_KH)(lLv34SUz(Ntjcn-&!P2?ne=K7@sKVShvZdXq9|`oit-C+~T23QOK-5Nzv6v zn*PiiPR%PeRVLUb=NJ9TA#wEpEW|T{6lkv0QTLjxpNG(4kjud6zT6xw2_?Bfoe5MYH7xixhst?dit8Cu3oKAS-%tOhO58j6{`^GwERT; zjp;1O;49I5@#g6lX{aBAoe5T^k#M0)QuP~_ zJrS>&FqVcnH6_s}wW*lU4>uDfPVXPiVK>p{hc`hPS4YS+!Hb#h)8`IvoX3Vawd{x^$Rzm|94U+)d%3S(YMlwp>F%9giN zGcPC)nKp#Yt|-ViNc^1yfx_}hs&ZWw3U6zWEB!x}s-mF-H532-6?&nyc*tzTTYLbFkOczC+fjZi1i&QGp})d|5$@_ zB}oQc@`Muh&=6hjV$y=hXJMxLQ%^EwWBR@ch7-jb304po5;e4apFQoKoZs)xjL+`% z`bTB1Kfmq!PxUwt@q8?+f~gNWdukngXD#$yY%IZsD~Ab)Y{@qaVMJ5}DVfK79K<*a zvWLG}j3w;EJ?at~{D(X6bN_GLu@#u->s#?oACKV1S}pkMIv2F1}jgiq}w zxqcC^8l_}eSBMgrDm3tg3xS{`Bz|54bN>jr0Ndf(K99?h_ci)KU9!da-uPv^o2~2U zeupfV(UC3t=6~=@nd#MOu`73XB5NlphwK|Ly5)Inbd1%shMNDs%7B)Tc)1O?Y!*Mn z%9}<6OqW2F{fWLb{Vr6m`MaV$y;iu;#?}+hG}qwvQrt!ySZK2 z$Qy)-eT*Z5rXS>5(~))am}m5aJR{<&a+t%r-u{1dy>(br(YrP*qJUCLw}F6^phK4e z64D|ejS4f;4N`6zC8ZfsK|nf&21yZt8M={{lu%MY-4~G=^|!8izVWYhQ}CtOgrB1P#->8l z>bb|L@RM_TD|Jdm+Xc+fTb3v_)~6KYQCd>FB)zIExDKPE+BMbB%&e{$N~r=zN7t}F zVE3kGhs}mnr zqfPP0*#W5$mu&3`Y;!HJEz6+o7kpbE=OMGdEoFh9$09R|bxpkIJxI+F!U5+~jezhe z>T#%%AL#Y*eb<+ZWk(+;O81|!{Meda zplozrNKFmIDdAuL^MCOAtchXL5dRS{fpIwA62?{jIDS7|ggueSH42ekRYchlgZXY3 zzZo6l`ql40A8rl=txOX9;3qCKfXswikku^2<7ukIsYj9OWp(al1}d*8L^3N+MpDY; zn~XBdT}&m%i^%T~rdAQO?F@HYUJzNS3d9+2Tu8M6#VHTeNaf%b0HJR4sIK5`KtSUs)ULb)8Vgq!_EjexsP(Y6(-k9>E=Tfhd z#}*9Z&Bch(dEae$*)|u0#nBH-GC=X4c7Xwz+s~)!(jv_ff+a1;u0)A(TXg}##v4JU{uMkBbAopg?+d8|bN!Wj$>uIDuvb6`8^QT*{s0`V6?pb?8&_YHYgyfO z86uCt^W(;`fpB157IEq!fe1AMf|YQ2L;>Hl zoC89EtPEYuMVJ~*Ac7&bARPeNBL3keIO>hRgvW%75sm-#97;M-`u=sge06|oqyxCI zAC5zOypg!9jt_{r3i7B3$TfHk+QbabO8~(&4(7WuEyz2qEz72sp|-J<8B#AkBb*0EczHJ-9iu@vzUD z&-HvAlfNx@=XC3`bO3Io70zxx)8w{_YN)=^av8%@o}qak0TBs7PrbnfV?;1YJ%X9z z+3B|~Vf~eqnigRUWQ{0cGqHw$n4WE!>|*0CN>#x@PE339!A%!K_|Jsb_N z1mn1E_}#aQ-!5pIyY!(PC`8yD2weWTOs?yHpiuI$3a1Q$?zGk|+d1PJTyXI`bpDc! zLW?OA8Mf2qXnhM~M0z2c-U8@Uq;45khh+Io*A#wivyyVyCEO!W6V0khbawBQyy@ic zhRBYh$?9|&lILUM^8%I-;lkr#$Bk(3rs1`B38T0>doL&JB_QL-`yyEkahB5K4}|se zQfeLu@9sh(1$BxAN0^aP`53~ck#lB7nYwu?Hf~IDt^J>nYgc%l=&)Ls5D6bzM9bs8 zO~0*J_hQrLK@`-{Pcn@(UM#4S7>P)oZyHCZThDNg@a>a_Ec0vQc)AjJ$3CVkk$;%C z(86Vk`ifJmy>@U1*#BoEOg6WmZZ$BAp%1duMC9xMw02 z$WeOiZRk)`L zG`NJykil6$7Tevk0(1>^n)vRQ*YiVacNQO&(-TJnNv<402;xlS#bCH^VRKzUYLtxC zzQDmV^36yY+y;v2KImek#Bq6Ts3_TSd3DG(iNn@>4r~3`QjrifnvcJ)I))tXr{3G7 zch5>sZN#KCZvq0V0DnZGv|Fe@BJ=xh&Sc4F6uxG`2X7l&go>jH`CGs=_db~w=t+l?jpso60O|bdo@XZ@<7+?cz zZq=pzw{}IJ9GB;Ziim;{kY?s`Ttm~kV%ogBsF#q#rfD{f=_wJ)y=UR~p@rvnJq1Q;daQke$VDF=;_i|A|GVte*Jb80Z5eM+YP#y1;+GWzh64lLjrv+|B>dPbX-Ud z!>YrG-a?lDz^X!f#|JXsH;JNiBc`vQ*-ll{#G`IequlkE7|FR(Wq_b^Am|S)NFeks z`vP{Wm?c_>czHZSX~9!kq|~93c6)I#N}!WK2IC>wlD%dssxydQ*naBfTGI*{wE|B0a1`M#}C-P}}8#ObL|DKBN$^e(5az6Fm zvsw-q7nuTnHl^2wJ_HuF9Q;JqBktiIKKC=YG56*%3(A9HASEJT0En?c@x>AS9{ z;$lAMZK>x2yy6eJe54H1wt9BI59HO=7CK7}PA9zxNBr-dtdjz29qAD7^y|_FYepeA zc1lgWPF^f7(mO5B7fAWqwRdrHu9yD-KtC&QW)47TduEN#2%+-(??S7%wa2=F&))n!VH0eOo%L#Q)TN-b&#;v-i~ijFhB4g% zYZM_c+IKob_+9sC^BV=GskN?^x_g8QTnRg@G+A`_)le*#-(RbP5yH9ZX|YKF4=W>{CTCg*u}{!^}<2G`5~7tFxkHb zV6yzAP1oO@zrSrYvoN!Kc(r^rElDgTyk^H~C<)>bl~_eydv$N=Q)EZtKlB|zsH&XQ z6}9wd{A>R55bK9Obc&8m{0{t$Lr#-5HQ-lYek*&JMUjIz*^L0p1^%y+N3}~mvU8%W zbC1;t51)ikCW~QCgd9Mw%i@{GQUx>r*W06y)1|u4(l;)a;2Q*Li}h~E;2>?;Ex5t% z7&U5321m894ZmY=--IvOcEV1`L#ssF;CbxUklHt=<*lJcg-oPMM-U==VI6J~cvrj^ zMg)s*GFox=d$z&WD^W4xX<{yiR=Le;X%oIFHLF8>H)P@D3x`+mGopj6LorzF=n3cq zIuW9Fftwxue2Zw%+!gUndCS-<<%16!FiYJW!3Yp>UAg!aZCnG(G4NG|Gj(pyONeZZ zwG(}R>$rkGKW=tIp|9T0ZG$1h8YF@t>^TJ7%W@NOFV>Mi%`;pE$F=#Ic4xy@pn2pi z!Xp!T!=eohp-BS})N5jhqC`jJKi4^|kA4sWk>Z|)PIg2-l4kDZ8w~%z527~Yb{e_~ zKV;SD(nTOY;$Q}3wmkHZE3E$2c=xoU-9&*x(tQ$p_beqPOA>kwQ=fDUS>AK^h^w+i5k8k7!CZXsP6{=)|R z5GOS8A3nMA1D@UG2c7Wf)u-TTL*%)`noL{(Mp!HnXo&e@zwMU&_futnB+srXCp;>t zH~5Cl75<1M&`%+w*e6(U??SzXcmV_G~G3iHIyj^nf z6`YzG&Z-j(cw+?4;Y*?&!xQl-{A`%zEMfuPsEcWET9{2 z7zWALh7LawvOzD2A=EFUkkZSroe=l8V3UYkRl(aNKM`*=zG9#Xye(lF?2ed9x|UGk zcHh#4*a@~&l?ogF#v~{Wf1OVU=4hDpdR)VYyeR|gklT)rPs0-rxc!eMcG(lb_nM6Fl*wWN3#Nq=-=IEX5qk|71Nvh3oi&_||1nVD(k2-!kVEC3 zBj{xdBC-!b?55K9V#EJig+o5%Tnem}YCJIJ))3zSYdcz)#vhT|tJaBTd=Z2gij?e) za0qimT0MU`@l`Rw5&4=Bpd7sfXJvq`vagFFn)&cL_1=Y32Hz1~J20-nc~-oG{`V$A zVUhXVU5HAJ5Tz*iKn!Ew?}R+(IFH@?06Plima9bX--&g7r>Lm?ZmCWuR=X6A=I3HAPN~S5trj#|`w}YG!8MJsXgW$L z6?s}UB{XD0)XR6n{R$sOgSwZ`fCxl5G|$lM<2*Yngx=0A*~M&`u}UpRQl>GQF^`75 zTqR3Ghh3Y;{;dZ&B%7ue+wPcl^l_wghlO=BhfAzCqC40T>BiWOj@h`<%lARB9qnn= zhF-HLL@m=^Lg}sCOPdIi`e-5Ih~!BOLiBX@I3Z2z9gz~oo#-R0OQ_{MOECzOZ~tq0 z`ws3v*fk01d7iPCugl|MFcC zgcif33hn9<_xcR3kA3(Q_xYPQMVi6QpTkujgvYGZh~($7Mxs{LG0I|yFf@q!Xyn`P zZfO(hz?)c6NGGne3DGl%^m(QBK+OokiyIHBUa)FJGMbP4Kr zTo42J#Vks`el|6I)n-&e06FOMcj9d3BRUmevZLF7=W)EpDd%xrwh34B|KvF?*SPX+ZClpljfPZg%!G;W}rXi1a8%{ox^VR7QF<%m2SX~Z!LZZ;x^rYeu8=o z{3Ufw4Dk7x^VkEH?M3X-z?Z6%+uH-_1_4xa*f0sWk71l9+zG84i2ae~@la?)NlA$k z{-~H7IB7cEVOaSOz~lqh2Vl{PeSd2BC7<-GJNcMi8M!UUb zMQv?i>*Jyj(9CYmHvr+iGoyEGN9$cW!7z}WN@AESLb!<+_+~61Ba0gK&-UOaks+}+ zfRypEKA$JqE%SN!jF zk#Bpev-1$vfxW8*t$(PE7xfy5;-why$BS}iK%DPwtj`8$(7#&2!PY-qVVn(OcM!MiRVhxl@6ang_D68?1e{uw1y;)Av>_w%Ml96W6qO zEWqq;T<}9|52d4}N$0WNt`G{DHwYF>z1VA@u7J6u0!1brO-cp7nW(;(=2?%hnNksR zaYc=4Fmx<|8}HNgV%;QR)MIuaa6~@yQz07ZJ$>eWRwaq(8+GY(6q z(JPB@wy?W7>h~8ND=uB`d-^a8<=WPXd7-ZC*%bB$(U(LQwU@+<-vs!dJ(Ah_R&|eM z~9OGnabz_}Wf%Tlox3h^ug5qpI66>eNQgVE;5EI|9(Rxur%=emoeK))M!C`AXo%PTbNB; zUz%8m=x_;Lt?iz8UYiR27oc~b6Dx^M?5ZcfH`_5=UUXXiyPSfrFv0wg(q8F7dlwN8 z({mWp6Tg-)N-KdeeI~&4i_z^d({AD-VsgK+>|z2=Li7$xi@5^2_OAUnnO~nuWNYO9 z`O=d#PrgBT++X@NzMK!%269(<*jgW5^R{Vq@=iN|aH&(D;}>JL>R6cnZ`6JEe*RV# z*)>5&q;)c&0=BR;x{SRn)9CzOL>h=&KsHwKV7q2kd->vSk6Z!iYNQn)N6>5vtXQYf z!Nz_B8Xicw5Cr(sYaCehg3BQQl^)2jKCr^wg0M7U0HJPS#o<_sqdm0T6K031u zAZjN7X|LtBC037k!gL$%ujdL#Cm`CWs4k!st&wwZfbo<4Ge}RkympxpWO6 zg?n)OC%Q2PTUab0YQTV>^0%Ww#Ld!z6}TkgF8qp776_>Z5z3eac%XE&4RjZdaF-FhWfVo* z(V>zUHAY+p;BL5Td<;xJDLDepH|3Wr{gQM&;%7KfFE(+p1s&di*kw+I)3fSXuM@0* z+f5IinLtSwCl92ZBV%t7`IW>xy*umxK1w3!CE@Con~}AM^`ik`Wfc_L2nI7-#bkd7 z;bJ|HjkwE-x-*>)sQO;xdP&UQGgeftoUs_DTe65Jg3#`RQJ>Zgz zX@JYxe{g*|+=DPbA?nT+04NM%yN?>o@)U>;3S|17mKV0LCm?gBUO)kgei4hO=pErt z3oZ(!q0RgqA>@o@C%R5+9=nl8k%o35Mde=qw1_S6pDp?a)TL=8MIndW=df7+AcV_$ z6vE}BBT~d4&VFM_)5_0lL>mUO4J)>?vEGbg2orfR1m*KNER&hJ>z@Smv^roVYZjO( zGvad2etz8Af@X9NLhQx^bP9@H2q<1^6Icy4AP~s>2MIkiy=o|Qs=!vS zTDnpvdV+zjx2OQ1D_O^2glf?+IJW*(rx&2AC;-VyD;BX_{{#5r#EoKOLSRP$>^$ru zx4455h{z0B_HNfTymjhh4rGH46g`$lrqNLAS4ofoB@wUc;L^+28RidH)-SAhw2)E` zR%BaPLnOZV8brH+TIzvZ?hHYcPNz>iy#1eBpA<)d0@{n^!#OU;c{iyYRme4?LxY^`yCWiL~~ zsR7Fr76j*uuMGI`Y2wDG4Uq!8Stkp$DXc(gq@jU>zV0DD(3rIV`6`KM7J_fsU6D4U zP^g~%D` zj~#wAJg@OONU5}=10{DKL4{^Z;w+owTi6Fq_#}Bt5r9|i=97maf_!aQ~Fz#vC^rJtRYt1caYVm}IE$qI{Wg#vPT}7{j z%vLn?Lh|e@`Ygu@sqKajn`&>oeIDZTtFIh@%Em91EOGq%@ zq6+w2%OnI(-A^oFDkKq%`uJSCr3-7Xrv?id#o!^;NEU2RkUhA>FoT87P7tajnzLkD z313v=uk^BfN7K^4m21RLw^a>ZZ2^!jTq#a`{84*d0Nn)8lV9ym2 z7y$vEM+e_Gp$Lcppvab+IB=W0x*iujkfehG#Jxoa!>mNWFZz;h3f!c%e=U=NGnDjz z;9Gv<9f-GEL<^LvOq>6V8Q}Ajz|X(~|Cau4A4;E?y#|VSf&fwYp)&IrqP{1KSUvnlRxbM04x7fY~WzYI=2`u?mmNnbRgB!#bhaxFsjV z5_nJq-P8&5;#bp6yD_b6by9smGk~)I2RUv3e>g~`@Li;g{V|XJw>#fW^hY-bA}~|e z3=fCBlGPgk}d431X7et76>_vSq)j* zMkGMulnlL)6tMrLD{vB-zzE_k`Gh;|D0vI}x7>001(?WNKaQh8&b(M|=dK3Uk;OrX zYhYrh^oGx`-ikulaFM~&y$ez@fD3V{|0S>8$eo2jAy;n)#c zVj|Y1-<=T+uAOR0&LAbGxX;3!Q0Vt+AF7K^EZAG%$YR7oQ1Kc{nG zVE#P=&(|l}m(A`YXEKVYO{a;D{^W@6uCsMuAOZ=0N$$RvQX^l2l=zO>LsuV0D}@g^ zd1gp@fuiyetjao<#$m)7{j#=u03cQXSl2J;f|UZ-tBPI^R?ctD?#lp{Ynlb2L*sV# zD!|6byHA}9?GXc)GQ7e&_oi4^GVXp>ZjYX&5K%F z?&~}j@8PX8WS)HetcjQm`ccCsm=@n2n97yfH}+y%;JM-o^Dfb} zz7wL&P81u@=W+b9(s z%hQUD4PySUw=mHz{4D+F#bSH|LTxeZ#te4w!4~{?z_TN_z`NOP_#Jy2v}TIWSw6;Bg%IYa`YJ$d3Y z`fwG;$0*y19Y}?VcFSiW`&kEKuvkE}!xE@n;8sUJFCiL4cV#{UsE?Iuga9TFL}!a) z!ZG#!v{b1xw?X0XOJupB_N3-0bA0nzS3HqRRRzlnAU$ZeG&#kvis3y5~O7eKUQ z0nwi989jpqM0>*T4GTcDUqW~|APFo2&}h>0fM*v(d`vV4JbMe^*)PC6JCux0Hb-8P zX2$aM0-_ywP0;ORK(xaOt45Z#1NjjN5tba_LdooP4+x!>NPO3944|fTO zc4X8HwgZeXl02D_qSbtP755*aU8ckP7xK;)u4uEE0*oC)+rt3Qi1CIW`XUrc0*}|6 zh6q9FfV}!V0N4T$?a7{AfTqS_{V8Dm^V=!lu64f;*tC65HQ-=oMgE6q@0~VrSU!Dh zaPd%U2H4QIkD)(X)WyjG$z#X~cOeyV2?14~xET(Y&#dB%zo&X;&S1Ub3aV4Vg;{Tw z;TZ%hr^jI)3J=g@4IVWnRRT6=E(_iZU=12X5k}S2NOuRM27V$j7dHd}&1L4!{%Cq; zLoxa?Z*4d@A%mZbK)lcG@-hqVPJTvv3KJs#*#h)!-Mg>ofDB;`MCXCLGlOl6pgW4(1X9A6)BTF-5iiIN7O)rLnC3o6PoM~g7C`u9j; z)$T$1f|$QNEJ)MoRD(eZBE+KpjbsA^T`RWG8v$5$LBO&j0m}~e3Rrg7+eI#^;H{OP z2s;OsuekkxvFtVXc`Se~%j6629X1J4=Ac959dWo{WSitOKsh$=XW2i&KJ zZ>DM`>7)(cud{W>PB6j>sK#2ffNs8TPLUy(MFo=4^#85G))QG%tb0 zYN`q%?(+wt)m;L&U+*CmznchRSp3P5iF1;%av(GsS0Gh8iV4*8hFtVyXkGXY8<=SN z3IUQCY*G6K#O_Oc6gQ3#0HWpC?e@B{z(NKgGVzOqo84`k8(mzhmf)^TBbxCoNYhDp zCrkWdg7=W#(Pv0Hj{vlKPOmvKg$BzuW&9gh-M5 zB;;Ha;P}VhlGw+S&l7K(ki{;}4$Ilzh$JJy-N%30Q}y?RFQ(-4TajkbKNTI-{<&=q zDE8{gr&iX-?PvcE&o<6#>(zSO&pyAXKRbj@H=(o0XW9Qe`SZ6<&rY|`JgNRRpRG7- z?`=c*kN@rO*6gW-|Ks8i#ohky0i%$R1PPY5QfN!_(WrElUw&8XpfnIbVaVf zgi_$_L0tK-0Tt4l9VyMH$2WJ%m%oguwmUs%Fj1ixbNol${dx`(EVPUfJhT6&v|B(P zYDjtHaE6YKR6Ii?|0zK@wID-w!gQYX+NlbyqeN3TO`Bu+tlQkAM-wBT-mgTz9vHLd z{Mz=en$Jd^yDqvp^(@9?&wX|F^E#Bv2#w?or4N~#ChH_l{=0RL)=J^!pXPsw^cL=D z)?3b8!Qk1K*_}3~YAv4LKJVg_5`_u56r_VI-&(?Bl{1%_J*`JiZ$X}h`Wj`;2fhZ)bY(|VGhdNsrxnDfg&*T;G;pa|QyY6pglC)3R9IP)Z zbfc9baew^JZd?2(j(}UNsV!^;cTCaz@s&6KB-7<8kY;6I*QwGb>iLR(&{ag}gsyEF zR>wP=e!BkDgKeN15t;=>`8+G{Ubz`l(M`+fKg8vjg?=zk0v&|U&cv;K$$6h&{!41Y zv;eE{D$|YQaY|jVBEi3cTk~=^X{&-N+2W;bb%$0gi#H5hTzN}>%`mUF2m# z1FHA6b|H;-coC1USjVObTjj}4`}yS7>lsyD?1?e-U0YutJfUlV4&Jge_pB6T+{kSF znlD)QjZ|0RyNjXw@zj8F#H3>9P4O)CLp8Lx#E$2UCyCq@IW|j)m#1^sN@)+-r?*Xi z-O(bPDl9Y0IZ=_cG1$jtTS_y_sMzI=#Bn*^P0tnB%@akHYA!Pz<#1VlnuvS7xskUc z@n|Y5BbMjhf%e8@dT5a37uU$c{e!GMjN)y_-TV2=$8uAvE0xm@pTo$>^$7BBQa2Cq z){^ZNky2yBH2zk|r}Nz=W{n6nyj!84^TCSyEv7um@SC;Gmx9tt#78}BI#SGuqG<<>lQEVb9PD?>dGe*lh?73&Jgo)Qpj` zI78MqCzI8IQfmPrp3_2EC>{#snoMc`Lem8L)3wj0BW(NI&&}QzOn+&jM_ew}*vTzu zzJICDqWa4)bdU6W8kXAXyWJv6<<-=}aY(;{2zM)mLfr5ewhNTPklf9KACo2wI=N}~T8HtO|evnQty z9~I(edsov#3!3V!I=?o~3i;u>R<)gp9XTB zz*WCVyi!$nT~_yLNK1J>TTXCffQc!0CbR5~!hanNQ>M%pf^PCk`(2Mi`0CkR>u~k^ z!FI2R3t=omY8O$bj5@kiOIgf;X6+w*I&UcGG z8{1oX#Y%p-nYo+W>|WTmqDa`EC2_Uk-BOhv-MNi|>JhbeXmqKAbQJ4gMt84|@rjE} zB~#pFM%b-xhSBv8V_`m3>#to(h6VE*wl~zjsgU(7Dx{N>ln%;{l>V71Xt4XJG3QQe zY}eRt8yER_f3)EDlln5UN9%fj`CLzF$bQ71(!YO%vh>VePl}sr-p~yY(DM%Vq*%hyW<+}zwcuRZUtkeeOm z5C1CATrYbm|MGd!PvxTi%EatH8AmGCHUiyRLqDtSECo82k=Il9Dd zD!)gQoZV1HQzTsAc>PMz3;$%w>+-@6CW<*DsZ#oDhjgs9AQJ1+=*t1aDmib=sAZ0u z-|r0zYCV10vvVsyY~FByG{;AOx1-wTqUXVd-V<8-9On9oQZ~L?b#g-5ldYPdVUriR zX@<`X;yqcMzdjpOMLlAEcQ(0F(16s-e{{EZ@_~IRDxxpt-zdg5)pgS*C>T70@W_jQ zxXNIW`22(X@+zpwn(QTdLUKr=-jAx>@SRA4!!P6`WkJ=)ggs6C`Q6PU)dCmvaaSL<%bmZ9Q!oCP8m z94kDG3GQpFu{b|}DCJG0@7uG(=7HLz9G%bA%O@HFEg;6zCSkpe2!vFwi?=8a2%-FuyFc^;oIhByX%qce%rT zOm*lf`2$Djx_kx`wTtDo^#b1O0%Lke1HP;4VaOB_p$%~@^@M`l*E&skUsV+AK6MYP z7hKAVo-1&OiF_}3YC7vxAtw4bIZ`mjm$`C=9o2LjCwjJ@6a3=v;mAvW+<8vpc;T-h zixVP#Le_bxck@}gb?aYvhsKgVf4=`gSMMJ`M+MpLWeE9>$WY=@*>x37H%hX;Z(to`ze06d<2_ZLDqU zcL^aOcbdI2|LV@*<4B?X2umvQ(1ABMRIa_BlN5Or^I{N(!$dPc8P#I}0h?1F(DhGy z#Bqx9f*BAL(w@_+*Dmcl^KT?*LShpad3s-^JE)<3=uDq^8Z z^nD_1&Lm&ysoEEpg7tb{;pQ(tPf~uhuAOw?&X=l5^(L%1#Jn+pkwRb6i2TrPhpdE% z1Sx!Y@+Gj#ieGuM^=IhEM>$0z6FEqRhlnn{3o9=m^}A7lZeAyG=jd-gs-C!$v#j1@ zs2&+!F`8!wrN7(Is+!8YaQDgkgp+&Lvf5I#Ghg+DM}mkx6}`GeR;%UqzVt|=>SdO* z)ATsk3yBB*asw5Fr;R>k-)$u4C_7V7SGqceExxwd%5y|c-plsSZhXqk)h$%R|688R z{d!J9ODwc1&6j=9&X9dUZiGj_V>We3ku%NADBJO_a0LtTNZj%}BSBVxtW;pC<@9FA`Fs?F=-NKrN>3e*$Pfg*9DN7Rp) zITQ`Ji+QJ|gm?tRT@4m5M!e@tMt@(NWmRQ+T(|~}uakWxB^mH6#tb;>b7{^w2UwqO zA){XjV$>=!)IFDtvZ&WHwhl<#Q@9Vw`CnEB^w~OWh5+Qv#c^NG7R5D9PsXlB+f^R7 zn`|eyvmGz@btwA%>$#7!)razos-u?P4lYEqz1P|nwUtum6E(K}Is5$SkRg2AN?fDx z=;<63@lK0COoWPQ(h2-LmK~89+GG8mPQC22PtI&4=EB-VlMI%3^MaP!-e^d-^?6R2 zd3;aOAKH_rb(NgCI+>-g9%y&t>ni^?jW}lhZFOnnfG|P$Qt-~=m)UCxw$1bA1n%vnqw6ohJ<3TkEQAy)kn)Z(Zaj4^kg|eX??DwiS^$>+>o@$K-wgvyqv% z5f3R%nH}QlpHvTOi|%76EdCiwdGS>-U!N?F+oJb_NWhXWMA2W$s1S1%St#EpHEy)(u!;~R=|AB>F+o?i9CJ!Gg+A> zFmykQGFL>(G|2Mh+z$d{H@&BrRni`*Fb^ z^JA?lC~U@6xNC3D#$NGyL`vW3@#fx1f?FNgYOMP``u>+BcTIlhmU9S@YTxj_X7*^< znpUv``d(o2yzS(M5e?hJ7d85-m#934D>?SU44!721jYpD#%T@4YHi2)r+lkyyyfra zUSnh_>vT*~A`@&_GTgvE8n;e%>9NbWBknrY05;H@6OGFcS7oo@0_)lN@5K?3npYL+7&JZr|ak#!-7J4Hd16$H|3X;-tgw zgKQ32_bxna%=cBaa=ceC#>UBPvpG{;#Yf{kUS;D0l%I%(X18&n?4~~7mHptLQp1UOF{G=7QD)PKi&0}uzH?sw9!|k9;WJT=RNcT5X$lFi z)Fn?Ty{?0kuc(|)PZO-)2jZO7zb3x0B>S)t3hgKc5WBFR#4ZJXxqCurSZOu(pl0-a z%6^QQ&9Z9Z(RTbhqvdRop$V+|j=pbjo$afmA6P6F$rKtE|Ed1I_XYj@hxcfL*DH-a zuXi;%%SD%!dJ3vxQUnmX7OCS!!d_Lj+LqT;jl|7veZ3T^_UQ>mjZRR2x-4@jw>eUO z0|glsPcCO?glG`SgQfefpVgiFwm-hvUXi><8z#xToz22OELO5mfQ{lo(@RM)ZvY%KC*XIAo7L&@ZBZOk#n`G5N z&!J)KF)bnUlc(jZP9D;wUx;QihfbQi6**NPiRy-HZ79U>TU7p~3)H>VVc@&YTI^q3 z!31y743>*#aN2KwZ)|f@xTZXztn=ic=+cD2)T~6J?arTC?){r6**h1}R6){0RCb1! zmT$?Msk#=>1kwihOJ>~l} zn~?mCLyfBD#IVh(CHc&|ul2;D_i4Dab-Q1R3f&h@*{0Id&PtVjqE51e;9yJQvhsYy zeUtUm#S9t$!jg`|`u9|?3T>nff^mUyx6+Vl-kcL3N~)SF+zZj^ca(&SL%;Y0GY=B) zJoPhr?ZY0&*6Ct+^<()j)YNSK=;OyX=2sISuV(8&E`A=H+tV|nb19z02{(TQ^)I-u zK8d|DYhn5^M?w1bq}JGVP288zlDB2=-7Q3Q==XE-ltcMl)4Q8;iB;I*jX$B3Z52h8 zI9hW{Mn3kAcz?YSceB=9=g&scu-@Kr!BRx<%2F6nSKP zcZOCs(Mt7VHoMLH8?i%>b4R6(=R4^nj4DSk-#94A>rX8-uiU8{D+kJRW>&Q5a4aW# z@)vrIR_Hq`HcHrL(OY;YuyF+DRkA4vk65tnoTrTPlH<`i5f-`%^-ldubm;GICxi0J zw(|BAk1BtdRa~qirpi$ns%TTQl&y7mK}Jn$xp~x^k5Jc1?GGVpZ&iANkU45G za^gh4qSZZA-i?%M#p+&W@Xf3W zf42LhEKxpwzl!Ezo1~JJQHE6C!@>ELqG<I}Q1`%`YCCOFAq1}7N5~|?)=w$s7)l6>IW%Lpq$*KzgMw4iO8~UTgm~MNgmOI5pof# zU3D%S5jXnD;5p{g-@63HOWzX+>+`wW9e3C5DVrHUFld0)&y);E*b|kcjf~*~whXPnkb5h6*V=lm)4GUYUM+p8J_8 zgKnYF%9)G1DN?qp>Mj6*%qjd zBV+olIq#vFc5rWQuJ?9do-Z!wxLN7=l$@x*{dh#eC; zsOsDJcJw;EhT|w1mE!I8e>w!OQw<+uf|&Mq2^or~Jp(EfGm>3H%BI%apL$x+e!S}Q zV$!mG9F$of!WIO%mxXUqSE&oPZT9t&BHH$>zCcA{@!XfMx-1@Ff=UFA?DL;q-!`#A z-$f|}=`22pQFKNsK<~3{sKl5vE3lKzmcE#$J-=g@D3vhP1YMgpg|Txf!;!bmI{QNu zQw#WN31#Ls-V8k8jI@8^o1)+xA8#9GpN{Zh}6k|W2He0$HKp&+u3Kt|#mx<6&| zA=Qiq#sxM;Z02ra^D=YGH3rI#D3XUi z^9^y6F)Ie;++|g#S?`tCIGc-ti{(yQ1R2+95xh*(OeRds{*!s~j|z$x9@29f#rCt; zW+lGmCCyGdSaap2>vtOJMhe+*4KKPm3Rq=c#^rhI&+?>9#d1Fw{Kk>&_;%Ir z&Iv=G(aHNQl%DT6z<+BKe8F8$z=#~H24TEhRg}sPof!8n9gEez{E#aRoq^x@ zUh4ddc-h!m_k*&YL`Sw!-qBto+(VUWkIJWwDEnS_!8!5WMO8PG)s2D^Nze28rz!>S z$U?sm zE;BndQEq(7Y(GV@he2JjioBjo2e~=)Me#3?=WlK#MyDOfeZp2YcMedlZD)wpbeLtN zJlnWrO_q%*A--l~sbA=B@~U|M!RwMdrK;MYz;O!q@4_SoGdU%^=X<)p^G4_T(>B!F zZI2wRk|JaYIrAJLIogmP_akYFr5#+3&)pfy=2UaXm`S5OP?N>iVoa6p7#W?>@G$Dl zjQ$-MqXzSl~zZtUY!2OzNoxNLmXAXUyB$o=B;&s`hq`&WIscK!-cwygZ*y>r;r*OJVT0=G8V zy|`wie4v zsUo%xTlzmx!gA|{%HPBJH$K11tv~8`KZ>rDe<_O~xm>N|PR*2oVrPEFJ9kA#{9L8a zS+MsjdG%EO-G1vwrCA1!jR9AOMV0O_?4Ld@W3m;SF(hw&brHc*;$F?P_45a#aqrCy zO^1WGO4MzPdGt`=8(vZw%S{JgM1Q8P@Vc#R@KL8$3kxuN`IYbABtKuwUz4@VbCHtf z8<98A57q0+`6i7pZ6ztlx;pecUn=N@uz`*#XQf1-gZ`DaU29a~e3M2->xh_a7V_eD z^9UE)NV)0b2lT7&@{cW4bM*h*j_c$%R-1%9kyHlbP@vd>C zG{B@`IFi;=#ZLb+GgaWA;7_}FG8_4;%C6ZPb1RLTrEiVLNvNNjXS~{z+B)ujbv@+= z<_b$sfO4Q_cV_^Xj(W(HhW^dOlK|e8LgQGC+3Vj|A*Ri$WU1RnW2^#~KTl{p>@JvA z5sw$HzUO5SQW0)%HRhb0D3ZXiQ@|09VO)CJ_2qFL|KAF&vfK|>bcIAd)9B2nS2usM zm-arFgud#_Y>!mz&PE$o$iMQqY~{ea@5RKyWTiAilnIFiYaA?gzWo0H(?Bf0+kS01 zw3#H$cy@eE_j+5qe*y7X(_eJ};IY*TwXxcsS5A3*iF}s!K-nzmi5|YL19d4?KuS7a zbz^{Zja^6$Sm{;7_sE|w@zxdGP!+>^lB`BjCRU=v_JIL4>zQCut$@7h!fPXiC~bme zv0n`Jirl-Zy8Ct*DZ9EE{HR)MQ!`Z!o{`<;D3cse*y)sbf9~l6xE!(Q{WmqUN_jGs zo7g(p^xIbzO3Oy6;TxpQWwQF)DqI5eQ8lG4~(sSBhQ|}B?75BqMNI+@?<`jUSzd{)WWdqCY}hS zQ6B)xDWEO{2yG~#DtA>uvrW<@A!TK52Dlu-klE3Hf8X#|nuyPvS;$}za&)X#rqiZr ze)Q@JT?QMG=5EvEb`N2seR_4kEbj$Poi`j66t3>%sQI+aIw<=pvU?YIv`KrGH=TCA znd%j6W*09fGpLq=CE4nk*@vZ^w)(0J3;KEObu)npz^pEk(AL-9unf%!TumjqX2q14 zB%VEie@mLkYCUiEswcC;`t3_y6Uup48N9{`fqohifcN;S3&F;z#~*&J*rbZ%W@c=| z$5OLxte>^~tz%IH)S+*R)A;0y9M7+PF)0aDM)WOY4Cx|$l*EJKCoK>3D^^XZNd`;v zL2#35E05K9Y4{zxUhF8eAVXh7*0DJ!B)8lHPs zX`~_c!uRh4Sanrw(v zZxVj9DHip8()QSQB<4-R>#XH(-HI9_m88PYqS3C^X$GvU%1myDeSP#+z{f?B*Tf6R znd#MHv?(&AUXIAS?$A7l4}dmRW~jUg1C~66UUfRiuIP^O05b_mr6AW`$|*7ue`v~U zYoAlFIKP-fWf-~eo1*ebIs}`|t}@GvjvRwTAuTfbUF z+DL1wyowTNs<)-i29+Ki$;AoP7B{_=b@XvYEUsr@s%QTIr0-Ap|ziKa4u?p`kB7LPD zvlaF4LdVeNhQxZb@`EHV)#{f~zQGrbX3Hx}t0yNGW{481%OATE2Wy89fSey;avW8&Ddh4nU`O+k1tpoa zlP7T1v5elmS?JJ zO>V5JFGbMI*79kAe@<6HD+O3e-$8|{=u=dr7Fb%~;O)~@PR?5XTIebaGysdgQ0X4BjC8;dwme!QCS8b!zw%DGKd4O`n)%aWbf5m3FwbX+*14&pfuV#%~ zNW^PVDFZnc31?5>Dmf~Gl37nL`y=#W8`QC5aclYDMtD_Lf3 z8J%DqNdG0yZl_gl#-cKamyO289+s|k#$DoDEC>Q0aPM^g7|o1N_PZB+DVq>@s#M6;PD zP{GyR)d~o_0Kl@C^qZ^`*S@z)`V*4u=e+H&Llt%IYFumx^ISLj(@&a_NTjaWLXsZU z;BiQKRR?BCGyqxdT9V4?qA(-VrY6%U#t$2*e@qmdY)XNV)lIKRt?EdVF_D)WvZ^X| zVK=ZPb4^36+I9mGKat!9N1|b3d8%^I7uA3*d+f$m13__!Yg*(geYPUbeEJA3hbnQD zcZaGc^X+02W8t?1m z8C>;={F}Yr_0cRh{o1xpTS9J_;qP9bu;23{2Bm8zYrUQIVLa?IZcSv?R zUKzFVNa6ADN2_?b)|Hxoc>yCPCYrMfshLM&l&dd9Y%&DH>1Q}x>TERJ)Y${Le@aiA zl+E4G#*^vkh0XGHC}j(gm-_xtyFihA?b$*cNc5{CTT+_213<^ASVA^p5+er>e+37{`lA806B@hIDP#3! zn<-ifT9uKv(5u0BA;UBL zb~g0QN&UuPOO)F5A9x6p0z^1tHYyKrqr=DfM@+qe0vx?46%6f84edZ z+Kjh!a{HIvL1W0x?@oPGf5n=@tEz`SjOp$yR}A*`Qeyji5rU1~$Rmd1xhnkk!2%!z zF~&ss)3+mgcqj8p@-Mck4+q{$@d`#z8|~~eYGE9XWuQO{E4CVf@d`qCUTtV~-q+bP zxD@WOqi*(yCyS@q5^qSMDT$Bf;Bj++u*#Jj8_-reMY2k?j&M8~e|oH@`xS#bve8a$ zjxhSUn=E@mtH|vfX;VFhU%-f|@&RV&{Xj}2tnwx_IfHMf58-*K-e$O^&%3{@nlL?{ zo1Nj&f@h{e&5G92YZvi!vonyt+_XzGT2Q$I{y3UrjquyqwpThptzr-jsC=N(_jYCY z@2Ds$w%QVi1JX6&e+ouW-6_A=^L{^euza!A5DZrk!t-iFtMk6jp26h+p&okolksTw z6rD7aS%Qq*k&~aFihtmvp-5CPl*hXZ@<+e z9u;avW@y(Q1g5K5yE0>J_y{MYg{etwbjITOM>JgOY%<)`$pg3?BJ4rm><~{TJQu1;nu`9|@7_Ul zStl&)a}_rkIyNgWL8^Vz911<*jU<0ZJ&XCR9&*L)e^w_aO@CFvl2Nj`nzmzEP2~+S zW^?!2FnD9T%2uRSX~`y;0c|o{Q&}$X2o)XTG?@pQdmn{Xn-E^=GqY*{F|;g&>=gKD zHv+=$5@9UQg|K&L)#_$Ph_5F??N+tK1cT)(wmDV{mB%ollvN@RsO?z(w>W9!B>b3Nsjt zt6MSOw=#g!w*SJ`WEpl@n2g$`z^^oO%mS^!l1{X+Y)i~aZsGT9STG2^8rTXisatuk zR+>$fT;*r0AcUw@E*!0UPsmKC7aia#-sRcKe@oTA?cf6>bvItURKSHnNSjGFuqn+Y z&+@X{s)=7$LEu(ZYeJ7d@B6ZqVh}w zNY3v6QVmkJad-RGqh-C7>KaRf>=^9Xf7=8^)5ky*`gS05qq^P$gRO2-pO!BC2%?zz zO#&RDS#0}4Cry6^@~BTyJxN`rmIg)Cdo>qU>Wt9PP&s>DO=Ur5f=b;n;3Sjua1&qo zwiPtFx`GT$x_sOCyc>H@t?jCMZAf1$f24Y#>oFP^3;>Z<(rrzO-vvP+-7%X=f4Fq3 z02p14nkQ-PJt)oR<+|IkOe(o*-KK2mhrazIc@l>$K{R@kHoZgaO7g$ ziiB>L(=tn*E_q&*U^JfznZ71 zuo)o>3y|HSnYX|hD-Fj5%F4Th$TVi_7(Qh=1<4cDaCsg>V(DgtjXt$H7f@=Gfh$}L z!p*X5I!qq^>=9fNE{m?`X1{teATgDyCIdnM6VssWZ0wn&0>m?q5f zT6d=MCRxA?Q_mqE+ZKjAf5|rRSBo44zz&*bpe7WPq=!%@tD@U-o3T%Ih~Z0^KN8R}e# z>eh`xN`LnP^H8nQR2!%Az3LO!Z~;?&FU!SR&0|Xz%_a{gz+KAge+)61jcCn%_L78| zAlk5KT2AsAiGyyg+lM$Di;u+LGZ=2R6mBY2XEcSImVA}13YKN*$f-1hG*8p)N;_c; z!cz1Qu9szYdG-h{$1RbHH&uYPNA;1>GB5%Ub+9#?xG~>+bL`%kbAY}HBqlL$%bdal zzVpRM4rVcNa#*9lf8^)1>8~U))?3yCN}mv^lM7W6;iwW@W4ZHd=gT4tG}SI!NqWlW zwrp^804g(s&q7A&aC>EhVguqhlpxqyMoP+(dZ1Nv0A&1kELrvUP5$=Q39B;aBC0JF zCPQ$I&f<#?GdN{Ah$)qCiK*w9@2+IHB%v!Ga&@9bAAr+Ge{i`greEycQR`8qWo(pG zfK`wO6!fJXPK57N_0PwrfCQ8c?qIIdA(bO{|TIPGZ*D z%e7=MQ#FO*d!nwTX_1Os$_h?{6@c%vYb494UUarjnGmf|m4CpoP>As%k0rWcWgaN) zgQcjyZkS#ee<`CuO9~ux!nKf!(4bl5w;~#OXIonXnMN=5mSW(5l@(<0B{hpMy!P1{ zYXvJ*vMShxo<4xfF-r=ucgL(p6`NT;n{7HDK2~2g*Ai|n_Cue*UI>UOE|8rB&8pmw zRUepr;vo|g%wB%Z+y0B?VUrOJ-<^$m&Y{;r^{zfre`Up+LDHONa?2M6(A-2=%urEZ zBUWJQ85k-a#$u9`WIHV*qgW#)8b_0mcNWI4xC`mV{jt^gR%{%eImdlyee5R5_ zQeriD9g8YC3bTw%8|p=xYT=-%RU@i42Ng#)nQdAL!caAq45v@vk|(y$>&>q9sQUBD zoI=xpe~gpm);dqjmZb&-5Mruax{_3p+70-6K44M0qM7jtUXedfm9vh+YT9RE7YdLy2%e zyevr2bRixA>DZI3Fi~B}=dko8LXrpB2F=S&e{`~-1DeQ!X=2|+M=HC5Mq){#%*xhL zp)O5cEF?}2%ha+B40L^Z|5yD=&-yN>zCD?P*C50YqlNvX!>QMo*C1}#`^Gm$N>Ws9 z>my)N-G?m%< zf9&*Pr7Hsj*M}@K*`a$^Xgq%Lc<3mxjz>+wj!rek=&YmyG__+9&?WVbMY17lp$J+M zS4_aENYoE+TVX*(zjwh7_K4*pri|9{5}hb5^FlU92`xEG>DQ8!J3D;>mm`+MwQuej zZjYwmnF?t=M9Fv2E(cq{RO!v|vS~hLe__oesNj@^vPg9tFGsZdV4k)7)u{LKeK(r0h<&bh*;KeDW9!vWf3eK+ zhh*^CGr07CwtC;2UF*Rtd;+>nCVt5p2K!I5$7M;Vc?gpSRRN0tg`pIbf@DD7IJ8Dg z)r|1Y-0J7N?Z0s9JxqU=b2byJO>Jw;NR-81DJkRUucnI>w=+vdc@ioJ8luThYlkLU z7M3a%Qek_OFEyG_#&z``gPp>p$Z+;K=N98r7G^kAtvUY2h&Y@Yj zOAyFx*_nX~SC=ESnWn#LX??4jrMW4a5|)g~&w1P5HVzj&3`Ev)c&fIVe_)fZ@0&=# z6Cq@4de;sM(mF}gY6ZIzKUJ-izjh}%vshNTGtuL$RrKuak`D$my8?5qCMd(7?mNG0 z?@{3@foeSptyyWcZko&~Xe`dUj?zgBBRH#kY1`!Sq*keqwfi#g@a!2}buJ})?|Rqo zj}~Wd##!|`4=q_Vhg^`Ff1CgM(gL*+s0M`=7y=6Szc3Wv6g+ zPP0Fn?^c#X$jpKiZIy`Qoet5B+EQ#mc};fLl}HH<&$A^|AvD~_AUr139X(Oq!xS}Y z?igETc@sS4(hPNMe`7Z(opBse>G|)P8?m=D&*;3!*d0g2nw4) zE!nwbb%YtFw2SKI*eDCl1I531{t@gyYZGXR$Fg9Pe*jW!H3Z`o%y78c+3LKl&j;`= z2VB>ixX1pexH4{6OuMDSKyA!^cUiJmbYzd%0#gq;3z$HJhvV7Uti}f;#<0v;S0y20 zST0@2K87~=B&_I3#q*CKe~$vBsGoQ&br^r&=#0Ice}?C!&Njn6o!tM`{KKrNZPah!R)Q}=eaLtqHcEIPD6hrrXT*lV5*e}}W-Fw;kk5j1YgTpD6ny!QVi?%Z~q z>5{a3?yK-I(sMM$lYW||qn0Z53x0aOAcx;w2>6G+v-pmYt$sCbDfALFveC%5n|)cN7aie5Z{t>FQn3N7obIF(I|&%n*?_5G^4 zM#Y@*kFf^ZyqIi~T%QEN7`K`vfA)#Pl!#KaSrPwR!pmaA10bmdES2<_QHu~IrcvCN zwGNFFYOP>MErhHcu`=+ILZ+i=ZO_t}qj+dEUH2@#l~vd_eVe?{W;fmRIL zDX$bTBCMcZhy4MrHwB1>i0CrU6DwDl#1vumz2}$uiw*`~_)3JxETYOgLWHVkp71zT6f7V@&95*^2`f*g&thWa?xyp;3y9>Afz zn(Ck@!2tGF9SmMf7lI9nq?r55>eB)#4vDAZHo?3ED@6M_aQdYdCxEgKP+h_hj)Y2G zTR#20?^iezlmnXJzZCclW{hO?nJIB&$LG^fWfp1?-FJCVe*q{#wIez(4GZB+ zAS}u8rGw(t+(lz!k`dm&VTns)za?2zGc%1*gs068`9K}3d;u=faFs@Nkbdn&fxnu8U5IS?fnozx&K zG17%56h&=FGg-3`e-Bjf4C$}d1$YU8SZM|zTJQ68FIa!d!8mOY`Equt3se#OlaBc~lQJag^CuYIIy63q- zvozfx;vwt+kWM^rW7ZC9WI(7+hJBes15pKCEWl5iMQd0(f3i4tgFumtm6hod?p||% zcYC*fG+hZ2#_sJ@NWE20BRnExzJqiwCb3Td&)f&4P$B0v}HL_=IfC+)Sq_;Ank*!TZVn#A4 zh9a@Tx;{YSe~gR(U_SdKd0xBb54M73?efX+ooy z%v3Xc7$Y!|lu=bd4Ah*ccS;-CWzEr`$-7Lab{@e?nbCH39ek@x9@aKGWATz)H@nFH zbe&D4HQ?;vHTy`6RiUMA!%~q}B`&l|AqABOujPB+f3GUi&`QE#7ySbHWlL#W;xLM> zavP23Dd@N?!1Q8z2QMv4j3LX+Yfw3s6b0?>>N|?6k-S<79T)-UgAsR6x?s_|0# ze;8q3$mHZMb5*!YpoMj&Z_4}X74|80GD;K_D&ox+bTrgFIP)nCp~sHtQe2%#o+l;o5p6%)Q(Qp6B-Q&$)gK$uU3zM@L8i`yfjyt0Z_l|$sz zU@*HQ)w1^tj)F_qd6t^)Z-w#7RZgV4e}{6cJgzyO=-t8R9-OupYPxkjWh^xrfSR%l z9(&yCja<&cO#jyNOJ=%E?-z*#ToVHfTxDChn5!1CElUBJ8`M9RB6e9!S)v;RWlmhl zMOoKtG?O2YD+A={+0jar=9NoF4C8C4Bn$hu^+0C<4Rql$CB5dvtBC30OC!Cie~C!0 z{N@q1Tjm_u2nzN$fNCwY{VJEy@UxD~B3~9j!teKgi2Bn(`)pLbRje6aq9`E+3pXiE zNCAlCkl@j|EJ+%n{y1zawFPRX4~2?OWGy&%2<}X>JNNy*Uv07wVj&5H)J~R=&|-=# z^{-qEMw>RjFg&N!g#bo)#;iqhe@{(E{lcj&1*=>8DYn}*%Y0ojDTR{0)}1WH6=8l5 zg3$s6?vC|s!HI=EB6--^28xQANlZvS*nLBiFA{XI5GF#9!C-L->xD^CNE0DHoy8GF z8A5Xo@=4mx?cM*OuvFKbpNy+_=G;RGhqMVV17EwRuoTo0J@!^1I4+Vte;w&Ru`MCj zMxH^2nBqX<9Xr^@-*>xy6;<+D6u%MyM~UZfB?FN(&R>8giqtFg28L>OK+jWGvH+}S zi#CpIXvPS$^*Duq#3;@BSCTKX16}k=2_0#RmD{bR`G5~1`6*3L)aa!aQ!|;NJDL$Hl+2!_t{B=e@n9juY$b}ASe_)A;)8bbKQLaM{(69EOio2>Tg9UV~t7$ ztxV>7*7-&2KmrFEI|U5=lBWV%6VX=s4DDAPn#36|sCx}|Aoty#U;10;#Y2E;DJ>;J z%@*?h$;B!@LINoR9v?Je!Pq2$0UA^jBe?-zH0;nO&^f;twz8cae=?k3fq{urYCrmR zeI)S{ek3wt4KDMT+V7BAA3+>vxsqKHIHKCnrWL$#BE6>xd$t*Fdzw~F(>o#%T^G+b zj*CZ~qXkxV=iZMZimO3&@LWW_Ot-_4Mhy(;6-Bw(YPlL&fyuPiPYef@GDJ(|ebeq5-}Wg|JDMyYV?#^K;acMbNG+S=rE{BRDw zhFUA`L1f6~TPJy!PXx)Y`N_wIRrGGgjP0P@_he`6|Lj?$xb-hc@E(xTw>+(ybd zS`lIDGFQ@rx(Q7j9mYD_KmpP;+lcW~2M~sj<>66C*F_HGUezJVHp{ z1JF}jU0%1(N)n1<{V49s8w3kLWT=qrOArm}-1q2UP^eO?M@4zB+v6zGyk(db>C*-Z zl9|YEe|fquU2rH;w=G#jk+tGW6K6=x8%;hY@%uV(PSMygLZLrE21^jh9U^8i_8n@Q zCPi~_1r3Fjr4_T%xO=?U)ng_-uePRnk>Ac}@)N)abPk>f7pY7gJyw?9xS2^BVDGLE zqR4*}MQ==zcXBsm4miW$kfo+y3^pb-Bp3 z!}VVF?s`80x+gw!Dgs)1V>Y`3AL;}t3vf%6fil`TttTD6pMSoH0rIwFgV!#I0q}Du zfBmhk+aE*161k5}o8?>YoNNe!wUr8JPki#(qmdHkw7dE3XB$DlZee!&@=27Paa&q@ z+_tU1y=i@3<)+8y-R#}>Q55JiC!^qvY3FX#p21t`;e@{9xhUuyW>v3PyZF3xfLe(& z``Wtwv8h~J-L~P9AZg<5RbgAv(E8nUe@G6%nYuSW{cIzIumvZPv-#v2R%?%&w)Lks z@%OyOwZqN5e82017%*mwbIeVWgYVl>47OH<4Zo2vCx!5N!R*eJJ_LhZjI#xlvR%*?VrIEx};%!)8DUAyW>CR++iNIsIl{LlrJ zRx`IMV)&fbMo9}ioW5NVhe2ek47%2jlqrW3T)<`psITjhWG`RtQ)eUj+H<gZQ* z+vJNbrM+z{XotZ`_}QZ+&Y8UM^|#kH-lMZ~Y<2rdN7#$>{HDiE+vc}7t_-$%i-i!V{|Y~B7?g+rO=mo}AI>N@8wdD8hDyYfX@K{-A z#pm?Y>7v((x-VIuX9u4bWETQ3HJ`29pBjr_%i6{?ZB!0f**C2SeNu>7do<9kLOQR% zy|#T^dPBz!7XRjxBsK+a_w<*H{b93APUG?oIYi~G3Obwe>bst`E3-$xV3E{ zeO_QP8kD2WW83`J^YN~2+ZVBEeZI11hwGbNe|Z}N6*E_?6E>d&E)nU@wZl!@=BL-z z^&;0EH#hVBu8(Gd-^9Q>GnUZbniAB@q5Qc9_+PXdfXyETNw2ko&kOQhNww9rb@NjI z9t-1Y+ct001zp`-e-t}5l7H>G_Q-s0lVigA+iTm$1-1dIynXvg2`#tQw>>`FzWny4 z^?8-g9yfQhbKeI$0-cb~?hD_TZH{eq+K>bwzp8{5}MhLF->bAd!A}Y`K=_w)wH=^WC&)}H+S>>zK{b5PW1Ei(eK^9NH!sr zVmgxLdFw*rd|qTuHxv1yEOrftea57d`aoywwD1b2d_Pq!RBJR70_O=;qF0^p1|f!o93?i; zfXwi6e}k}uV=Had(EeijuY-}wx6MO>nrKUNRN`Y$SRi3k!M4E+nE0#UlE|0mpnML- zrkhX?7o1hgz%Xzk%pH3Y@i*d=)G@$!apA3FQp~rrH&roZXefy>#@aB%hh*V8jG8ch z9nJvUyX}KGs)-vu8AtETSQgai5qFq~mshC0f4o8A9;|CX^-2Mmh=FMX{e?F(G8jPS zqKZcmX&l^Ub084+`h5{cWnlZqS%i&;6$fw6CS<~v)euh3-7xc6@j-lMtmPBMbgq$; zB3}qav4)02;{{Io=JIH2_+nir6WS!L&sSX#T}?)V3yvL8t4JeGJWQlq(wLK%Pqte( zf6Y*U5h1ha+i8ZT00YYV=!;w|5K5`(u6%Rr`o&egD7Gsm9tUw&Wz{i6ax+L z0Qyw0)|riC%o5m^a;6w~fnA(zS zF1g@2sv%G;m+4u{g55A&8uxDdAdWaio#ew3d+&=QR9rASzs`x73SzOuQDNste?kxn zpo7P{Yr9kv6n*ef=s;*Tg4R4izD}f}CMNUu-F{!KYNGE1NU#kwUp_DLQ?O3ROlE&0 zjJj}n6D*8VowupCQ5>60F4mKYAT8r@1J5H&VCrnEWF&&3$Q!`f=lyDCeJf95O;Xa;kn zKi}^#3gmop0+S~)d@wXLKTXKZV7}^P=gV9)Z*yXOJBeCjNvAfPnhp#=3`@*p5ML2u zBe1+;P8u_rVSU0cv%TVDNj}lQz(${b-MitVI&R`BPDao>Gqz*gD%R)Mi9SB`Xwt0{ zfGA=??1}=LaldptKP?dzf2?O6cj@~DbyGvumbE<>P9*o;ZjS?~PpbXe^y;{WjM3+% z54{y2%$W*$YF8wgM@$c#&sqj9guEsoR8Bf&;@=m!_W(Ulaz2R~OYex>BTmCcX}SPs zOsIH{#%5ji^n4q8LEPcl3EPA+*ayrt?B3WCGF{oVLj_j-mfi78e10W|R8Q-Bg&6v;_>GX^xB@|lJesebM?-P41FIPmNYiBRUL8-Dq(}XfPNa_5K zgqD?g#dgB0(nZKS7jTozJ86PpZm7TzcOMtJv`Nx}eRTMh_W@zhmtA<|d^%fwo9tQe zZf&fP#2^U;f0)qpStoNMZJsh!NRl80r{KNYKAMhnt~?h<<(*j9lo##2F;LmSzU=dY zSxL3j>U98Xt6p`|M~kj;${q-{OP3Yk8#&u`h5{ciL*K?FM|c4u$BG1 zbOi4b1m?lwA!d9-dM^RjAH&G^0)}__7>s-!Gv8d=e+@uW?B<1C$$;o1@o~7W7R%f_&C;6v2$T}4@#DgGAKGU>S+LH z(*duOdmUga2Tb6ROTO;i^}*K2CiiB&7gnSgc1ZvDI{5TnDJ1jF!~> z!lR(Lf4hVc8eqf=tR46>!(;;~rI5XDUqlfs^9Hwqs7-DGjmf0^&|DeQd8<8NJLSy6 zVTQLc3?_AlQZbMn8wM~j1Uh-cO%9~=IV3MlTv{8WD`eUrJviw8 z4`w5xe4Y)Xcjm91tPCQSyn}2g-+W#e^T@??J)M#`YccPTpGGimIP_ixOoQR%DAW>h z6;Yl0Zm%y+Xx?sSAfq*UMM2~m3dM#x^^hgjD5RS7te8yZgs%^}6^~sen2JB6b{N99 ze^$!`2l{u=$-vl|BQ!z4^if*7Hoa4YyBBGF2F7kINA&?W-&qiw!i0PwZ4;_cOoOcu zSDgNW{R5^-dXr3A#^umtoiN}Lv30N4y?Z{Ii3qck$)ZyK-!X_ellF?bud|0s=#l*8 zpY3X-V00dTe=}KF_pAEmo33De;_+AHU{QL^$&eVxWTekN z@eG2qpp^2OnN0aW^v-L}E~j2{nXTNC+)#xF0@icDaF+l<23&e7K3^uqvlcIz_kO!1 zfth}~r1_gm>VM^sdA|AHdc&#f`ZqgG3oBnvO$GUb>{L>aHa>dAW&9XtPi8)ce@Y}y z9IxDV&0hlT`{jHI{qO%QU4O^SJ7;9Ob4EV&#AWh6J}+pMjBHM*e7PX~Fm(LJv&!Cv zjwzczH=l@SM!r7}34U^Z`YWDUZpde&ruleiy@`48*FLnkPQ-b#b8JDv{QucQ`e;F=d8ANwx;@?y zc(1oKpwdt=H>k9q7jKRvjz`g$?QFk#C;y9(E?INAbvnX|RrDIrtT?Y;KQ2ykzJfBN zR)8UIy*g?5#uiZ7e%{{I$CR?5YBaamvo@8eMf=n)07EV51ku)05z5Lze^j<|MF_h~ z?u+t$Mdg)mlMHZ9$kfU`^~O~WvEnhw zFP#v|6o(qsTC$NK8wnN;d+||uV#?57Pa>-th?>76pmX2p^+g0xL}{wj72Z5PJ%mt4 zsV?*Qk|dt{mB7qRsdSPbe=SwNI+9zS%$D~U$3*TRsRR$Oq9ufEh_;aVqAh0>mt;(D z#2qMzF}tdWL?G5%#0&7CR_sb!pB$dCpzi~QCf`Pi6KIdhwoCk0eCdG>a&AGvfY?uE z5NwpPjYQwxZ6C!DpE(KCEpN=)dMmi#@eY~+J7iPCJ(sSBQyo9Le+Z0@;2(JcRk|}I zJX)2uWUNpM?A{}YzIFR5iWsHXU@>Wx5P%{KtBY#kNSK*k6s875adAX%Q+SKG^!C9Vdr-W+L6*WmzC7`xkq)TtNJC^juVR+|8Ne_qgNV-CZoYjn)RohvbG zT&ce7e%0qPDKSzVrC`azw7H$=6Sn{+3Cr_ULs$&a`jAcK)n z8HnzC)^(4fL`ju<$lhHagiucW>bVfAZ%o#DYA8|?Ed^JCh#_-V&Vk2+!Yq}O`^E8E z0bFn;i3Vj(f34JTIP+gS$fbu|Ky-8PhStLO+q8Hw|179Ey zmVDHFbrP$Y!={pd($D1{L%W5~p^K+=kTmt|iB_0Je{fD5*fT|1!q*d)DSNkk5JM^P zqbFnNjhR}qCWuWkW5B2-(aI!HCFr`mJ!OoThd`x(q0~!AHEGeJFqF~w?4aX>e!1`V z`)VrE;(%HJ7&#wJY z;R(Th8;-oNX2IA^F|8)*N{p#AF&Wys>!YcN%R9NLuJ81kLvqopax#VeCnopgc`_Iy zP5yEik>7pBbif)y3Dku^ugR(h%}AgFOxH@Te|Ofj-L79)^`H|Osz?mZG}wl5B?Jj2 zEFu~Q0n#v_mkXQQtszUfr+ z9vb=9BqX2Z$uh|_tBO@wS9GPVDH{F=BMA-?{$-pPrnlW1XFy-UvL-BnnGKa72?!FPffmPC(esZykn*Q|pnQ4ksE1Z~ax&6VO^<}6+>QwXbH0K0bke|{BP zpjR4{899_viH=KQD+jGdS>-0v2<9i3+b!?F+1SKPG_xQ!&|w|wvSzJ;x0!ZVB(@}< z%v1)VDE3kyuhdu(mZn#r_yvh+b)Z}(^8_2Bf;)+>7RANbR+C56TuL#jXu}7z5E)BF zx0LgA1@L+0Dng?hQk7h7YI2U*e|rW;v8BEL+1PrgtOL+Y335}d3pWt(RC7st0LW~V zQtu%T*GaK~Mkw8M=m!ASB<>H+piamI_uXz^fw~g19VCyiDomvZCJ4UFq;$uAn(YiY zRyL_rFj3$Er$qt-?!T&tlkGORD!{K(unTdn*%VW)EWB*mTB91&9U*RZe}{Y&NStb)r4b3B3$@Fk8^rAV{=CM(s=`uLtCANu$+wz4|9;A`;D22 zkki7wMgV)q6c!(~mMDrVWM&0cKOEpKZ9BJp6h`XnWEi~_q-={ie~Lz(F?xA}lZYY1 z&jU9^Wr-NzOHL9SJOO~@1$8_WK&8>>W8UCaw(LKvw~T3IZzNjq21? zY3s-UsX$i0g@Uk1v-Ki?=&X3&flFu!L$e1!l!OKdh6&0;0MQ}4I-wd0Bm~~Ck!HMx z+XNf3ciTsCl=$AsIC>{e8J+$H(ug&`kop&xN=_fF0~oFwXh;Ay&hpMS9ILLmAi4l( zvLokQQ!A&Y`);={!U#4W#XI^PV*w|!XQd#k9azey>|Qz! zS56#TF(lG9uF3{LV7Al&)RLbFdphB{3fo}vw?RnTx#=S~mym2HW9XeAW&DOJI%4cx znc2_E(;l(8t_`QB#1re#>w)BiSsUo@f z4OIc&2B$@`MSK!%!q}0Ps=Q#qSp-CvGGjsE(?Oe>oc_5~%3?aAH4+|tux9O8fG9OE zg*rZU1XKwGb~MyY`Dr&MV8U$37=eM+69$=rPj(R6q|{PkrhkgxE)GAk!>CRn#{>GX za=DdVyG|$H@A@E&B=K{y_}1QuQHDBOh0!EU*+U&(#|zhrUJ!`XsZD^i9lB8u`}x38jz>?4Y$I$v`h)J05=VLU-{fZcW2ALIaaaFk0Avj}sRlUFBk zgw(s>SEui1QP~&ar|tsO_v!&Y%^alhKBTcNNst zZldbw*D_mxMdiuC+iN02nuGBbaw3D8(*&6-g&c`mw?lWlv^ZUKW5lD;gGjaKsUc!<2Te z*xmP<^<6BaWR%N(J5}t0I1swVln$(!9TE8cY{j@Pz_o1t`Q;?50cd-0Yw>vsq#%U> zK3RdbUw?P*`AE4jo!?luH){b~BKac5XC?o}>thy@d@1g*PRW93X}4T33W7}8!0JF0 z#R~)>1O=APQ}^9&Uxg5I%y>D#t0Z+5^iii=PSG7AX%nV|PqJ0hUrh;1UJwXBj#!0i zap)QVt?$F*1*Rcg1MGB+`AVOxcvyq2p47$kbAJgpj(|3&3C0nVX_LdIKG6255ik;g&B9THlJxRD}@08Yngq_*F z1{tM=sD)IT@o?vDl!ESY5RR8eVHpr*3mP)}6&-b%ih7hs_WFHALxwWLE?BzN4WJiS z!GB@=p}X6J8q_BpeE<<^uxr+P5LuT-4C5kwfbj0XwV(;BoJ2|pO6#Ks;_E`M&s zMltID$i{eQmV1P+80(zBP>#@bOK^)Xi>?PThBVeHB8!Y>)-R1l6Q#h=gHC6An@)raUy+@|mm?`4u2X4?zUTiUACD z7qJGqXV+{3Vqj3cW@bwR8VYSbC|$=arqh6weFk)PE*$nWkIsA&u{$??G!db)dG=)X zPE8GL2092&rjeN!z7`BAw!-13geG{bTEfxCyfDJNG!_F*Ln!&cg}d+e`hOyZ$i5}a zlv@&`d1za0Kw&X(V#+BM=A|fC6LrMo`u%BY7BELQOSNL1WnqN%xLlP6IMhfe0vr*2s|(Z?iyI*t_YYWFwzB8%6I- zSd*hDoM#!yvsN3AG@pa?U_&tmX%wP}IU8gjI-^-anxu&!OlbeW-ebf^ce;I*C8DYc zzmrK~I4>i3(C+Llw13-*swWzA!Nb6S%NWg@FlnCA)sNC_jr_Ciz27M0fFsLUuc=sKP;?;w(>HZM}I!avz%-L8k%l%Uei ztC(eVl8=&Fx$iy)$La#y3-kXIkaKF@Fb8=@B&tj7;V*6sgAA zj8A4VAjq?Y?t$50uYkl3hu8Z~r>~?C9`bj*iHZL^N(ciz25pOx2NbLfrjDTd+5rRn z@9eI7(v(RVQ)VeNqSv%c1t{uAc|$^%T;(vLi@6FbAHIwRXdjWQ#=(-Xp>=|HiE@fz z$Dz3{gApMeS%0%id$IWrB?RENjh=|pVt5mi6om~KGiovNYdbf6Gz&>?_+0$-x8|yW zc=D!DDhYZx_8jA(oQc>HS|m(DaEvG=O3tf|kY&-_BN4XdPdTx@ZePR@?4>qP-GO6N zNn6H(npp^t9qdgoROXDv@d&{ail)3t=0>JLNfGK&qJMZbCJPGkhNEM^9;GyC$RQWy zN`4FCigp4rV`g1DA|}x}*{Mr|Zf0PWB%I}fj6fl|fdH8$Fr-pg%LR#*Fa{_-C2z>; zF03R)q7c**>kb`&z1u#Th6;ClGK}7-rvZN}4b&tnU!ShK3fl?`@fgDo|4% z3L0)ydVgZ0gmb6lZ9C6LqGEg9z6c}aaOafzCWqacRChrHmt6!Q(;T28SKvmz0kJbP z)ZTb$0e~gkro{5XA!3ra34I;_ih;~-l96j9gg}=CJ{sR-MnSlML?9sLfifMstU!;b zcfqt|V4(zBue||{B@sD0Lc@}(!%_x0q!?DU4}WSO4Lv=|v^*nn>UQ0`=YtrMi0{dz ztiLf~&ETG5D5Fx8l!lEBQ7_D+Qb?BCThv)oAH+~Y+7M9dvczR(3<|ZA@pRwq_I23| zk9BmPz?Y&S%|$SJQI)I+kJWqYm3$&<8KNl~L<7n|yq zNH)=B0+k%hmW)WXG^0#)x}BRoE}XHkI2S~3Ojj!~8hYQ8V|%=APe4iI(=Qe?|%s)shK#6*`xN0u}1nOP%(kA&E~kp&7_$*zX8N01}5i#76t8Z_k3h62dahlFZsf0F{Nbil9&PfSoZ^pn6_sf-27eCQfRN5YwB^o!R=n+wH3m zLUk;vyZye(5UK8|2t}h13GaZ!c~5MJ5~uRUzJD4>MwP^O z1}(rzOmMW!zj9pwg&rDTQ=V5Gx>R-0rrhQ2o*k!kA>H~dQ;?N%E1bLlkSK@1;DAJg zscIL2Gb0b4%gsrAF6|Lv5QwbtapbRXeOb72} zshYKl(*ckwo8gQBj=hH@E?DBwhuG`(Rrx;9?6Fx6gke}h_rjZzL4{SY5J2~lf!2jt z57Gd;X}vWqS=8tP27eMOo}_h?O1NO0L#-MCSwaYfLG~!<`N9NBDNtV2N{!2bBa-by_8d}>*5RKF_0CPdaD+bVjM3le`v)2V zotN;_(ME#z2S$edoZf&;vP06aIl4v?WtIvDC^eS;rZKMrNPj@rzIXeoa7L0@a`sYO zhG7=w=AD@%1c0R)M8BXM>!Jn@#u<=D>+Iopzi2?H1dnT90?w~xwsFiBMr7(dPJ%m& zCnQNV^qi$2Jy8Z_B5TNUvRk5bTLSKex-X}=jJo@@3vo<37;*$_ID<$p|YgR;2s=bC_f^<%oc!h^`vO-aX^>Dszhti z;Rj4CQU_I1B?Hi>$)t|&yWPH`dLRo9C`zK{Q_k+=_uABrspnT+9gV+JDIs4oQhi6AaiC3eqSJ%dKok zc$2aPy_+$Lm_~w&h&GtmvG{-V9zlo#_Mi-NouWNK@I=PEJNDF*yG0)7x_93%M4*oq z_4eavyfbM{ExGCFjIujNjt6Hbl;44kj$kBW7*QOvG+={HG7`#Alb~jpRY)N5zT4|- z@?syzi+@h=5%P%*`dh2K{s|2*u+7Htp2Hun%~Y#`V^%`r8^!qo*#z2-7Ba$!F%hx^ zQlHnx4pHer$n$#HG}kbKnJmKuf-+$o833A?m>obL+h8VydL&)P2x0a+Y~m(!6+g{- zj+W5`qubbZiLK<5pAkY*Jl~z$K8PZu@y`Ne$A3F>)|$OJP|JK~a!`&aDxE~BVxyKg zqyfH36>|!Ch`1zu2_slmWU@1`blI-#_4_)GMm0@5(zgpDE~!pPJp5d7xWt5!C(Pfe zDvW_Hg2KpAo01)h0Q*ZAnVj;ldn&GZ3n;!yA51Z#APRBIHir49u?R*G)yx5(i)V|d z7=LmlcSfOv2jrglr?Tm%6jXYM(-1=dMZ)JQRJCM@pp|%ZnBFKVeZS)u5kxBTR0O>- zV_lJwN8dtT@EI+t>*u9vTZ?5V(=6ND`=xfV8i)y>m>ViBRCxo*n9yM^gR!~qc6uB> zl|2|VR9O|5fo#j?r7{f(4L`yDbT&J*C4Xn{F`+-#Uj53qm$Q$FUL>WeI+2v2P%>c= z$SkmOp6Vn~Xp2HfWi^&~%tUp*umXQv>Y#{0(R{*&tVl-F)&&x>^l%@lqXQNmIHrn^ zXvA(M{yL?3+*O+c1g)ILCa4tJ@^r7)z5CveBJ{fEvDWE96umKJU2WdRL^8mrjDI&C z9Z>`j1PY~*urVmU5iuyF@*o{0HB-ol5W&ft#~YG#_uYP91QPn+2J_ECo=gTE1fy3{ z=R`9VCjtpy9bE#;6_N~mx%SNOx8?PITq*>SG58#Yz<@O>ldF?OrY#J6c)MA4m3;Yu zYh)1l@@-lF_`KFxl|yK{7;i|<@qg_;nG20vf>Xrc+o}oclQ<}nF|U?yqcSFFOAU*l zy_-IWB3X%_or~U>xvuCO$j>dD$h+hgxoPZ0A~I~SMI~M^MW%qwRDh~BE0o?BTI$!y zk)y$nz3=w>DvCJ1%h=LobgUgdk!V?)3k0b|fo0F+(ZC-nvPv~J!hi6st8%=W z%&eo&#nh7xn>QjpPh}FJJsUb+ymX?`^zAvQ>c(VdQU3FXog6`fIV)`x;=up#?KT8? zz&1pRDSVs#6Dn*>q`3e*A!vGm_%tO@j8((leIEr<`^9wh#@w~@n2LbxVl1-N`Mfxb znh8?`mMaPvJ`p;Us4VkNXMeYQzqm{B9>k#P3IT-N>-L4-8hd8tScGo(CJLn+ZwC7- z7y~`CH*;uVnVJF@R^b>s>sujwK@gRS*V)I#oieTLzd3JG`Qw^6ZFk|td;;j}&DhVGP ziSHK|t%hWdl*0n8w%6;6AVO@Ui;bvqQ<)O&W3dDdrdBJQcHIgN&m*qO9N7rk95A%7 ztBXH{qXVKNkxarnwtqNW)EtJ`ER^bukD+sPYOI5Tp@BnF9Kd%i+!RT$D#brea1T_T z4{%B@V>0msWUdxD2FYkAQi&cB9l(Fb#QiaB!@-^{*H)SC4uO2e=>pNsbz$17p)T^1mK8NTj!OFGVtC)BTD03 zDnsZ81NjY70bfOVn~e{5=>zLDH&L(!s77g*^dYm@O!%=drcxn&Nc0jK;&d>m8mcOM zn-f6H%jgQ$>wn%&AA}Hd=UGXI@lGLZ?du?Gp;IM+T@+}+j-&2MeF`L4D1u6pLoz); z9Flm7Xd6$%3rCb{tZ?<&Z{5BUpzVl4F#S6si8z`dKno=hO%;TxL|l?CXOilT4RRUfPW#JcazH1%KaL0!ILZLjE%iQCr|RCrnJa znpDZZ$Te2h&s;Y`NugDRpV^{7JB^x%$k~p15^WE~Byt7tF!yfyNP<>BdNPLIsf5j$ zRhFS(le2aCs2ch|>?V=z$;L{6GI5fsFll@4f00;+1v3yP;91PRDDv-gdmKcDGZJE0 zh#Y0B<$n>#oPGcrrv(P@@P|4s<+KGe6*1u!`~jziuvjZ_ew&aa8gLDf4QcBWAuI|e zK5%m5+a>7QhOI+@8}-tQC>xYu8o|o-l-64mOrUYpD;J(B$R7pAz?bCXVhFDlAV?~~ zu%y%E8DasK17O6jJ9oVwM4+d*)>A<=->HSoTz_9+umfh7fjuR9GWbWB8;bt64VXjl);VRVYc z?0+C!WF^IQCxP7*EHXqR0EOLE#jdd}lomFy8AkE!`8ilQ#K<-QkZhPJL`{CKsXdC&VnEXMb#T zb&6FX0?G@y8ATSyZ+VD!W_*s{G_(KA>{)qAF0;e?e%G%;E3~FbD^F_aWK0y~GHIqd zM>vh;1xzq%Q5&N)#z4O{8RcRpWYR^5D=Q+%5|?ejV%67+!A0Y8VaO6zNhX%MH3vnj zERrrNwXzt>TSL?#CzinE;HC$XvVSGQN{W2c@P~{qbb8B)$%l%5)Uz{ZNIgfG!&KA2 z2;i);d;f=Fh2h@G6Wn~KK6V1N7quvALKE8u%@fF(Lm1TiFpy-56bz)qcIjZ;Sq3yW zv5iG8j53!Ya(>_M`c+tI`66+}1ZNeS53U3>NS9wS6{<-#NL^0@3C87(CVy?|1VyzV zpvc=M11JSzF0RZ3puVF+?TvRRZGYtZx#BXqYZ@a6VfCvBS88Y=b72XsZSHwmD!f>7 z{)0?`3VX@h(pZYnj`Bz&C$8A;4+;pQPAjIKWJ@g8+-)+_6T6S#D6+sto~^yjH|D!Z zhm0m524%_0GWU6LW+@&eK!2K!j0#ppAbZgLrQlr*piLMYjY94gidX4uXYRYbzDh80 z_Cp+vIf>Ow2RopcxP8PFv&OU&XdpSbydV*~5$kvsx{28skdq6s?67xU;{aEnBS09fX-=`e;qmMglN0 zIJYUFyD=!h_GQ$aNonJ7N-+bhQE1+h)W-IFL5xfa0^H1i7b?UEC%Cqhoc5ScQS3v|v zRV+y+twB1Ai(m=_EhuiPRHu2n0mi4_2C?IvP2Ae9tEwounQEL%&+n2|e zRB2Q4+sUHGDt`^hZ|}B`<{}=#$qdtcW1<@kUz9Q-hyfd%P!rgkP{$UJklV(YJ~q_U zFAZKkxDf}8tY+R{tGr#!;Yd*3_xpaePr__kCLh}S7#w*nWsn_?-H%W^ogH+S*aQ<0 z=RUFo9D~?UX}lxhz($u{3iN8hswf)DXeg(l3>edr0Doq3Yfa!8M=at1X@WYE6Ju&P z+iKFA3B^b~h?zO>Gq(WwrHxBL5eXPv&!K>;M;S8eOp7%+E+&at%$OmG(>5@r-+cf_ zyCijYc9-&a*Ou3O5U~(ydNO-Mmu_Vhz zSO#85ZxG)qUTdeEq7G(Qf)T)gwdr4R0Ty5`cij?uZpz8zEY(3nM>Q>tUG-k3x`ZT2 zbO6T&ppJIsNDV{~PLES$W6D+A_eXFPSK}85+<*DTlz2`ZIgfZ498$sRX_f$O0X~V> z5qU!e3xvM8jLEDDu4X3O>^S_wuZ#t8x93-Z#o|`MdT0Ualr^{zR?|^+3g?s$>rP## zSXPw<;3$?8S`d|!Hvupjdlt=C1%|mo0BD@WXEC|OrkYEh6D2K8crA0*m=nFfP_ohF z)_)_9gqD^xCiEbB4}36?INaa#&{Dxzh6S$<>f`Ewgl>knMW9Ca3xS`6GOS!aRq zphRwDJ4fh~?)LrCQ487`2YNvi6X$t3YJUmOmcb}W60204r}7dnf|5-#@$%lHMl!EP z8P*_~%Mk0qO0fc2rr?UE@?D{+fVHlLHqKoLlNzMP3DQ`eM{pmImRGMT)XH^fqP~AM z(ruhuWY1&{MM_XTr^YDDBY;wfXoJElhTe@_r7}F!t0iVPBoTKXz@b$ekK=4$y?--# zo+-Ejy%k1lHcrKQn`{S!hX;tJjg9QID2Z_>0y)Q!TpZcV0SECwNn1r!eed{XRB84n zZ$)JkXOM>zA=oA$WMM6!sCsy(3u~qq4IH4L+{TLG8K0R+rsQrtK4 zmt&F)$ImU$alBMe3`ost0&R@?xm*`vW+Ps>C->M%7z=B@?%RYX{@bI6pmH_i7%a>&Z7HwjT3R zWE%xctsH`v2=W9?g9(9WzchdE`&D2`Y!Rs?0KcOTsawj6kaTh&(lN^69e)DNGbBxz zH#vY3ZA@oU1ORgkoKY*8`ZWZNnT9I9TM>py@MK0tj5Dz(s(EW&ky*@OKi_ddJ!=2nCUU$X5sx9i!O7M-A8cf|EE!W zGPK^BJIC8j3V=bf29m1WIe!d}OaYI@4}85!GQ3a@$SSI$jHMhojx!rhGDeE^3g8LsB+jP*`iN#0PMVs4Y+L%2av zMcClMMmd#txs>$S=Zafdf$c(pRa~*5$3l}=U*v6f-|hNkSOK$|tOE@RYfi>XSfxe8 zDI@_@dMRnJ!d40^tbaYKW6v>osa)D*$FfwAtB|3*E2%-M@L^KjhG^Ux`OmoHAzC>= zP=mj)lvA+IM!F7@Z_JJqe>j`+Tm41}fvbYwJcHnkUNbodb%42z&a?>EQ8bM!s0k_< zW$7t?%d0F~Up&MBj?N+Mek2ESkPq>kwb!2x`V zYePqtmx}XIS7=j(QxQqw4k$eM-<--cSI903svy`LBq}2;*|t%-6ak)HdmtnFAj8dE z)g1Dwq9P?klz();qe#$DaUGEx+;Ga?8W>)sJ13P?5>k6l;3%pnC!A%B+Z*%i188K? zwpAirGIsd9j1G4mXnr&!t#FT4C()ac&Mp>0P4rRnu%)?9ON8KV*T-=+D2vqXS)0 zaYw~!x$fTmeozgP!0f@#1=Sm~?So>1q0fo-1W@gyplU!PDXgh^&JceaO;G*JT~Tc% zAxty8ZGWj#s~H_V#o%7wFQSS!VE9P!adD}I8BU#+`Fy;NI)Ci${U5{?uHis7&&AanlkKPvP3>HD zGpKm81P-`ijyzG^;}BSiR(Oj`SdlYjSyLZ_G>n=(gGaybcl{!)nDP=6fBYmI|IZ7O zZrDOBQRPX}JCgS&)b+0Bgn^<)3MYE4P1WX)xQJ#`VTDMBikT?>LAs0EaOj!(`Hqxs zG=CbELZ&)ke4GiRc~GFzbEu{nOoS@Z5o(lnM#S}itN0PmOnm9MRu*A8wI)QMZk3kr zH-8XPV13Sq)LZlHY{nDY@+`SvD8E;f2g%Bn&0nudK%Bz_SX;YLid$Gy!gzP}eSW zrs|7F^``yH@1bNt(sJ8KXL)M>`K6i7mumW(UbSTaU-0D`o~@dI&=SPY@XMzo>c*FT zH*MQXSerMOyXkdvXM4}#C@^Z$r+-cc#v3!&q+&|}*oulK%4lqYt(V(KSczn5!46nB z_lq?s9nc(?IU3`Y`-RMFpy6`wXzHOJaNq6v1+%F(wis+e^@l+f31Cjt9KyQNKaIRL%1($IWz@8&#($S9Kh-)_ zh}`jW8$r|5bxA%qcK{I?`4SZEClDyDN>j3@)+A*>qKRLcy-4`mdV(IH*@f#$$QjT< z8-<*bgkLMZT6<66faa9+nUezC{>H4eVIAQGc>X#dmszH67pCJL8jAx}pG^;+7n6`R zFq2L5mtP|h2~uO*$Xs5?+kf#s*Jhp4P)6Bw$`VmgzuaEHvyu0SfEqG>+s|T5i1zWO z&$Vg$>uc+Jp_?ApTiUz*gJ{s`IExeUHzu36&s#%WH5w?vW z>y7mQ0^{0@SZqS+cH1i=d5zk@_6nYj0oZR&6@L3!){z<~ptaXcjMnu+sETRcuF z^nPhZD}+?!CJ+8O3cl$>G z=y(_>qv4%WAC&m>vM>5wG%N$z|126nPT{e%eRA6$dpzPz+kf_DY@46&ZS8Y?znh=m z#sZt@iC4q+v&?s;O#9O5wsCU-o95@WZaQ6WYVZCJ!eI%dli~2zRBF1ljOyv*%#RD| zne~F=9p2h&jzORWa+o#>Fr>A4`)k-dRRheqY3!)Su#F}Ox;BFuCm(s;_A17XWZ^eg z@NE3JPzD{2yMMvf#0K-q-dDK8&LU%C{o5ee z4&G-F;HMLMIBV;dpL#ssZR6%$d})5Xw{54-7rgoTO(=l6o8ld;pUe+&m2D_&8#ke_ zXjgQ``$fcRaP`;R z@K&2qtA7PvfyEBBHlQArGV6W6>*MrQC?Km=n%Ytg{k#NkuaMHgD%__5NNOmp;DMU) z>F!yO{fZzk9x2dFZY9C%VwO*j4JQI%=F)<%C@EyXM<{a{20R+!NPKby6#>DbbOpYb zm~v5A;}a8L1jM~o(JE3S?!XU{%uu-)$P{9nbbo+OxIqodD4GFj3EF};o*zojRGG!z z`~C{NnOnni5OV%~zw1|l)xkZfijI+Ff{pokkyZtCD0=aw>LoYHS_KXO^xwnql6ox; zxqrEZN1*^}S|x^Z4XyeDLYAz=2qywWk?smbsDGlw&&fzp_97|9BNm|7t2^sE!W*ga1dDnL7YU$ z_4LNfsQgKk_scj5=TbPINQ4K3M2xc0ihuZZ4F4i(rC!pgypQ`OaI$QHH32|KAmP5( z^^3rgQ6RctROW7GCFV|@D$r@mxF2`N*GT?HXmY8|!`(@k6H*fm!MYr1SkP{>2$O}n zU`^rkg-HgOzl>FUw*cG0bP&XW`D&uD6Wgg)MFDM9R$l?Zh1IE#c9eP{p%WenOMfPP zKiBZC2Ul4@amMIs4g3MPO2>R|nc@dSZukBVqAKq4Tp0&7>Nl2gc-9dp=z=XSZ(`YE z5(vNnOgu+}`vtvY(v{p5KD&!1rU4^tgF;*z(2r!Yy4&@OutKr2B0Puln?z3tD>z1m zr#rnA_M$0B2g`~ffDpwK$OTCHa(@x1FvtV^<6?ls9b%PGBPBC)!8vG#F{LJ6B;jEv z0HRglw!tQqktT(pg2|W`f0)n!b3NTgM^`O3=zl}{8lX!I)rH)k(M{H!j|(ChVz37w z$H)pwAX|1nfloow_qu!k2VrGNS93C~-k7jOLOWN1mN0Yg5PY@xOp0FIw|{p;9E62q z*VPA1MPb7t`Ax8)rRzn|B9aq<65s9mejHZPa@RlHKnp*bnSTi)4_k*kUCTaDu%h~%7QvFx*uljNk$R>kn6Mpr#w-Q9 zB=>^7CvXs0lfdHoP6gH*)4q&yWNr2K3xsQV!6C0c@PN|87}cc_Cc+ZRnA}uGI$Hb{ zlPmWFXv#55(kr`Nzoa28;gdSdB4yH_V1K$uRj?#NErsr; zd973gSDCKxS_nLvoHAyIB86fzLSCBMbfZjR-QxYruqDJTnHhb;?#al|hGsCeY=TK( z!M@nyiPBeL{AGEr!4F<$J3SE1LdjHhM!_?&cxJQ`!=!rm{*TJey2^8fMqoN`l#2F6 z%_djahc!%dg|JTu)qk)ns87pObd+n2sfl$^IkZspS;XPk8MGWeExVn+6h}-CXgS+4 zB$fI25*8P+(^EDe&ks()0}w_fi_D(1|nh@||T)N<6hA zewjlxJ{77B0OV8bD)B4(3P;vt*)Sq+PT0 zm=@j?Oeh3TwecMT@|b9P9Y{R^CP!9rjd#2Lf#T9y*TxLmpR76_ggnqY5nppIGKWa? zz$eKSz2L#_kAK$)&{A0k(3&{yiBgB$H0d;DBfKI#B%&Rr@w~~*vS(7%fU%sQr)Ko* z5%FDkbvS|sSXoY-0lTDzjY~otT?S=u@mD6&$Z=?J#}YumFpR5^uFnXV$6-Qzovf3P zP9wSC)3g6H4%vB1$edG5?CG6i)64-9Xx3x$%v3FY9)F;HBwLhIKwvV=HrU)omIuqg z6bmQ*1GC^~u?%#QbNBtuUrKSZv#N0l#gAc;C6bAg>I%#*8LXuQ>x06kh)2q*T&2qI z)|azdcQ)_LLwdbB>%$@Mh5d-Ezgu z11wssaeq`xN(6t(%i%{Eh+2mT24abh0&qk5ju0%ugb}2fR*Q-doKib`PvJ-oU(tPX z8q04?p&K-!B9Kc>?ByA=2vM^cRraPYY^5PBKge4LBE@&0MKF&5%df z?fb?4%Sdb&t*Q_qJ4&pd7qfaR1!`%oNP0|8d4D!}jXE$^=&QPh_(ajrPmH%!EgoSme}@nT2uM~O>Act!R=xk{)rVKX)dQmw>n z?R^nuF}YUH=m3Hhh1?J~uO;rR)g|X{s5+pl!4+We0pM`=8651v*eT+WdoH%#n3I?I zeSe3}nk&0tz|j=o>#a${EZ0Uh2)E_?#mGc-Dey#SDX@_3L=%(GLYxc1v%QXA*)5=A zkiczLa_bXeO+p;Ebe#<=nXbfnip(G9&6hJL>q*#V>c0mg69-}tx~{^kjFV~vr4 zSUUkw6Y|H3AcKW`HT=y@<%WtG4V7HcwQ@Jq@v)-7%!C5cLTW^CKUK|0 z1mG-K5QpG-Nvc;=vZ)pS3D*%xEcM!Clx3JN>^y>lu+qNZBo+X~`}?M>?x6G^uOoRF zA2<$dq8bsXS>DT3B<=H0{*TOMjMp9&=7N z9IKSh>Ya2W_$QEOR1}rG>Jx^!LsrkKddYjJ;9Y~Tf*h@sjhON-5BO4R=spI$Pi$hZ zn9LfH+0X}-AKKm%IP&`7%G=qvdZ##bqX5A%ztwEuN+BAk70o%-=IAdH5 za|C>A%_u!6*WRkMi#uD>C>9R%40L=G>^3SyW^CD3t7xqY?aaI)H5mrk+x+!Wj)?KfrY`` zeZTJ)frV2Cm~<7yEwQa*#$qMny{U@ls4TGqB;C8 z)CMvaSQ>eXBxIAt9)I%{X(l*3oc3D6qjT|unPT)2fkhQ-CQPPAg&JYSo`*TCOVUT7 zf-TyD25&WrO`WJhK#EzoPjtTHtSD09-t8aR%EXPIjH-7+v87lD+pSWkDQpjzBIE<3 zT__Tma{M7jDlSvhnlt(*0yi-KBhRQ55egE%8m0YN*RNs<&VLrdsK{9&im9p-c-txW z6_9KMtYH*{5)iqWTsdzgB+m&yn?*AiiEwl|^;$dQ<5Ak4lVXvp%KS|B;w;P*NI}>% zEhUJ=O9$*K$&5x?jLkkX6FQ127*+~La-n6q>9YLGGKH0C=*Pt+iGXcru9Rja#L3iJ zifJz;dtMRDx^6rNLQ@ro@{3@^@Y$dpzvSPPcG;b0vDeL%+8$$!lg!k1X-%aCWE7fDi zW5j~qR2;pCEA(JFyXs}tK@En^5EglH9nKgPdqK95 z?PqTiPQ!M+N>bQGZPc!h8;-b(Wi(|jTj$4_*toL}lZ%MWd=wmapTJRE5qw|^PsPO~G~v`r z4wRrbr)Hyq3j6MGTPs-w6w=>=qPRj6Q@+q(Nm-t@Y}|noC8g0wM^%c-M1~Dh3c|Sp zDmD?@i~Lc%d4KV263gG1ZD`|Gm(2? zlhMgpLKV!D6PGTJ^F0Yevh;Xl$C@?BLXsz5HEjyqB$S}BWZdOj#U&8A&m`j}-Eqir%Ak#eW(_W~&3YD({OKG5Y2 znG9N`rXB!1RU9>ASuKQ{_E?niBshz4S3?6`P{A}%QRjzJ3WP#Nl@E$NaWOGoQ|*Nb z#mGu9?keK2(SbSU#Q+yP57oSY64oIBVL%eSU@sb2!$}2_2gzEK}%-$AywQM(>mdO}?yFP0V|Gmv zWYj^S3cVJ5aqJ5;zmj4wBS9N3s0w&Yy$qrDy+`nZwcXN5Y-xF`DP-v!k%S62yOo7> zt%_(X!AH%gZDBW0)miY8P~I9#e$e?#R1MyrgKe#hYJU+@2-HI& zk1c2xWK9J(_=_>RD#Stx;-IN|Xre->Ebq07Mf8v5I)Yrb_*FiTC1c!6k&%77L{?e#}*@IaYDK zbzymG1&}~J1=FepSc4#X!=yIjL);UdSs_8qsv1EuP*Q$)kWzIFgnz6|XB4LF!#fo_ z^g64H0t}?clGGY|#?y^0M1;Dv(2J4H%0r#FoGW~Mqc6?4Hj32|4z7$F)Aj)Hx_bx^BvQNlX1`JzD#WhUgj zBu$(aei8%HP=5*CiZ*Yv1Ck$Xt1`sH+^Lckszjs6>3|UFl$~|@__z!bY)yy~c(B@aKpccmzhX4a2D&@TVOd70n}t( znmd?b2jLM~#;%0Do^p}y>V3cOSE9ur;H8gZ@J*Ri(0?o@%O(pz0=$Z_N@GtP1*MP0 z6XV;d37{dm^0K%M^5!N$YL}B6LAivYcLwk!_J2vJNkaQfxYaKSu>WxR+yDB%yFU1DKmXHz z{m;)Q*B(!fjSX5>^$JF$!dAibX(;gtBeg?HATs|;T>E~1{n4K{c37ia;r1~s%M}7^ zuZ9x-^RJ)!8_)iX0<=~2mop`OGUBDSzW(u3Z*y~XBa?1oyPMH<|3z>1?2XeTmw$!= zKcfFqxS&}7{Nty-{LzzkckoK6~WK5Y7mGtJzh^ zxs<(Z{`#@M=jgy$uXR{MBpNE##DBlKz~}J5>=6g@IyBt+jkv$E{W+`anO<@@-(^;?yPd(Lyt zdG0f3%6GTD5_It5n4FS?K?!LWr+>7aKS6S@WX*t8`=>=-wKM;Z_bBGX%Zk0m_Tycf zd6_r9*e6D3+U|;DZSNO4T^`=&)xMeA4h|UOuWzSH{|SD_DoE6Q}{kyMDaqYElamfAhq;_BZw{>`vBfhV^ zS7IJAuMzdz<+jxG^R`?(Fv}~!{LS9*@zKrwqt7ncdff9&MPpTsOvAK$rxw56m))V! zwP?+m)B717s-3YYeZk&--;R0b)MgZzl@HpoCp_!s?4plbrc7{+>$O*NTac+=lJd_X z5%;gH^k`=j-)%;_yCy4tz16oJ)ZE!~&5~qO^T1lk6R(l?HIyyptjU}DYtHkO-&2q7 zDPNi1Wab$apQA5z$1I*7U%s-Pddbb2OUv8uYJI@R`mGB6?Ix^`oMiubKz{Il_cC^A zov*$>%;<2)d9OY?NjFD?l&7t#eWm%x&Q~e1dHGdML3@ey#Gc_ZFL5`<~xBryBf%{h4Q@bM|T7AO6j)SK_rvj-i!?UHiYWi){17PwUOgQ6z}U z<(oWRTRXLP(9~IS@Z7*K$sCXp1+T+QJF4!^`Nq@PX;+h}bE2irdW$tbk}k%V*!h8q zMVC_pc-iMvUmuT|xmuK5*2m++$DkMQ0qES8WL$2m@yd2t7q|5a4Vq3v49Qh8Pipq{ zmFBQ#pY6u>nQ(ZlXscSamt^t$MH>$-9~Cu7$LR2ieINF2DVe(Obl%j3t-s8@6TW__ zn#o&0FZPC$?hSt&cxca8zPIMjd%?~{ARFEV7|1+Rlu<^R10Y zG9vNU%OS@C)GYKynF!a}S&R$;Noe{Z%3;b+?X`gwKsr2E>V^>QS=M;`Wk zuD$?#;>n&4sh4rN`r+e`dwT{LbyvzwzPEm|(&_QP z-)i2^Ios1Hen<~xLjUdDdke=Es98N7K_ZfGn@bL{F!Y!YGpR|?neN@ z6|_9{B3^l1&AV3dem>D(f0h<}uo+m|GyX?t|51C@uhs58_UOf_j`>$NffxQxeOjJ6 zZ-nH&MzSO*Hc$VM2>evpsd(+K|CtXY) z?>$|ldwJRASq&b`l;8%>2O8W)@H z`8HoC?~+ajkA8c+_OEHWuY9lC;^`54clEWOzAEtb+@aU}SNA?<=;@v36rB^7e(qz> zz~SqB547}CJ-JfvSEn+fjbO$N$HgD!4PWbg$l!8sqsw!YbnU{4N2eBN=gwUbe}3Zz z(``MT8-Kpi^S;M$iDhy}CyVW=v%ilTZfme-$k>H(>keEf(M{fV#yrZx#P6J@16Nay$c`TOaki^3JZq=XawE z-`sL=h*N$W*JQK)>w!T%DoPf&Y*S=>KvJV@(YDW9?IT6e2b+642lvt2A!xtWA@%g2 zimVf=&J$v8chdZHea)rjFP=|~P4nJ){+5T+wKvlaC5CmmJU-gLaZ}aItGS&=5!RP~ ztO?An;O+ic?AAP9C>l&gm~ZRnUG17RVz_#xdk2ZL>q8xrj{Cl6hJ~)vN*H)yc38Jn zoqxMaIve;5i#Iwp=c%RJ{_*R4+IFeL(iD= z56WjPTeN-5u$NY0mt*{$Om+w>yh7iMvpfYd1GpwsRRduE{Ke*~2b&Ozq_4+orJg=JL~F3r!xZ zSZH;*nev-o4?5U|E?=VDxto`{LDTkgN>@&L@nK|5(y2~9yEMN~Oz_!wa$~!t>0`&e z?>F|TfH<^o-OX`xy_M4UO9pxUngsr<9pBNYf5Elf)?RJjEcIRXsUT11QDnjy*VTI8 z2iFd|ZnoE;@?pYRSEqOp4Q;T0CUK?J z^m~y_PpolYe$JyZtvKWE(TP`d+}GJSbUgTThH>Dph+@6>eVxu`7A`+HcIT0*iSLw; z#*Hb{e%mRjMBJ!fKgUO_E3XEokLle`=1$Lx1!!(1-Sra)g8 zTw8hh?1C}<^*=2=k)u5fgm&?;N3m`a>rX5DztUfATk0PXcK*8Yl8smYdp-NN*mBi{ z><;$BUFI}c+vn9NrI5ZV{p7 ze>7%~WVupmhcBv)^)H?$HF|i5?=)_kJF3a)<(6l&3p)iz&oUYO$<8bC^Up@cUnR}8 zk45`Nj!@3_y4TzxCB?+P!^l~QzRjP9tqmZf5;yIs&@dW!DZkZc=hXO-PDhgmmQK_K z+DT1h!cMQM&U3O>Ua$(Qcwm_=abL3c!wQ$zgM6AVYA&hTrd{y(kW+HvjAiLd&p51q z^83x`y(UA>FI_8f+)y^RM_g;O6PMb)Q9m<2J}YP1*ZFfF_dKM!<-oF;hpRj8I_0Gi zcYa#s>=AJnZ$6zwm<5D(_Z(DJetS$v^Fu^`v)~?Oq2^@+n_P}J(fj4S)#l6M!p!I| z^UqZ6+&=w^TZY{(L6yX|deTbkl#l5*6P7<5)AwHM<^C`(ax!FZMY{3_3(Ca_>yWi+q)>37hZl{CjK$j@v(=VaCUY7`_3A-0pHBY?q928c_Z!6pdl|`-zdxO zwof^CiD!v_%2cD5eSNYXOfkMWr`v#nG(u(3Rz0&%1A0m9C$^e!St+R6s@(tm(wsqW zMkkGrUDWGJ>vt1$HSV_9-Lt2^R_p#r;#?5N7fz%tHR4<9&ns@G)zp7lv#mj{lgE^= z?N$=W+tH`-E5HlC414(Rc`WHu=Bu?MbKLvVF+0ppr^igcQ+eNir|Z+4oT{FJ{_)z? zDo+d@vU?MqyPv%)Np(sZJ-?;(?_=OV?|t6kx2R*W>6!u4wltrxeaXhNCmu9D*t)-( znO<<-N#c}D_tU7nBGa`W!E)l+?xb~ekrXcpQktDWJ|%f})Ut`D>NaBa*(NkC5GS^nV0 zOFupJm~*tl)x+u=_9lfi`7yuE-9C{U$C*~ds9yQ8Xp?=vdAr8;D|qp_$-u$;Tt)u> z8E04b`S0zsJGgFMDV+LA#VAxn{&1h+m|HG z-*0{(qfxWCR#t9rUpAzU)$87Ozz~fKCgIz&CTA`>Z$D=JN&l`LqLc-$GfcLO zIBkBHfvjY$=G+w zPsJ&c)|)0YepO(%zol94U8XL(6ePNVcX(e;)UlO zj_>jO9pTcft@z+_gH>kD`K7U!#uYrU%Ux~|n`qNzq@JDRR7_}4*6mei3yeN{+1hs* z>L*CmY_?!pe9Vay%fTb(TN||S-CkJ9-4e6j06 zwJ#y}%~x79GG3i<@A{eHUW+cowYaX6@=2^Ubz<<4Az9^1Q!3wOw~XIm3Ld%h5xTc^ z@^E>6z;xZ^5=RpU=f26SE0tbuX!NLs#yRuL{t@p??R3P8gFR>TPzsAXCn@;k9h~ZP zcgvVX$)Nd3F1)?hU1?6ggjH{bsr1uZzIWb&?F;g+`lmeVu%FOdy7!j%=(UFu-U_}y zyg7Ada|ct=ym^a^ZdtfIdQd*!{jmBn-}WY#MG-@LylQWrBL3Q`Vr97_@7UX**Xt*{ z2BQGuNVK zoqFr+-1b&rqmTWLZ~xY;;9zf+l(;7acTDG(H3_-zvH5`?LbMYv&({)t4_ z5wqiG#8nSW4f^&w&pf@bM(tgf{2@-yt$Rncn{aJb+re+IM8~J>ae4S!xF*Bp!=TRF zzkgh>YZ&{uNv5#oIJtJjtkIv&7mm3VxoeL{jas<2&x()yqdvQg^Slq98MccQkBoD2 zy1aS(fu^>iRB)#_Wtc?#TSL<)IraVY?Mdq_L!R%BHCLZ;Wjc6KTxP(}K9Ttox9yua zA*7#Op3CP!=LUaWxU}0^pLP{-**5~8Tb}KKIXQ0OG(EZ)1}>B zwyrF^aJfux&w9PD>8aVaH&$z%&7S|})Pw;sIq~KIX~oy6CkM=Up))}3nETdyF{x$A z%{!#eh;aN^9h>SBz9hA*$^7}*tp|SBcl8X;s-E!E*)uF=`D!og>ythj$E1cYSdyA_ zF!V=a>w%ZTmZWCn{7U}a*Hd5EJT*z;68uua>p%0fc>>5#%aYiXPr(VLCLMkWt81Q{ z49;C7a?0S<&Uszej*?CDNPv{j1Z58Qv{2Fnn1l`4!$J zVaEFwV9$Wkce~yJ$wf~Z1teQ5nM8OQE{}3+h$vP?)BuN>LT6nuX(mE7Bz%+ng)6d=)8vg#byQ zN)!|0X6TyNITCy}_4TnSDQr{>p;8|fj_R9 ztMFRue2;so+dg>&XxWYEYxvSmsVH}zm#Nw0#F~qKv$jksHjwqNe$}uGZk=I_e^|){%mQ=ckVUjsbDmBmz=R)DPMWLb-7t@vkNR+*!?+ zzaG{`c`R7r?R7v~sehtc%KG5=Gik5NBx7{qoCIbu;s5#kw*R&>W8W#Yvg_+5p=V57 zGR~Q2h`(%2C~f`qMdo6Ig_kS#CRM4%ojp|UkQwmAb6nz2zRUK;0j|kraa~JQQcG9}(xUt$7uK`4$qFkp#^ijP()9&} zzq?H`f9p5kivHC%cV-F-ypvx`ewVAJwECug&SZC~`icDC?w?Itq_vA$lRZW6MZ~o1 z0~ShqpUzhIiI?Ob_0?}SvA^=JO0%Nnx<8K2eEaZ|WQNX`Q0rC72M?;dEDHH`=31+T zamRYDj8h$&wDMKRE{AXl` z{<~Z8Rri&9r@zwDUXane`opa9n#rBxR=+Swp8d?>>1OTTWXjOK&pNl*H9tu?qB6|p z@vviW_?j7#u3&}5Cd16VN5LrS;_%4W#>@3nm*DjQi%pu18rRXipj}?4;HR^|z~XYL z^Xn##Ro`^Cbafi}b@Ho=U{1(R9IXEQOz}kz$xV;A-YKP(dG{^vj-5AhX!*B_K^>O5 z%?7`2>Ls;j6=l^)MoXd=-#Ju0eRSxa!y56=r!DK=qIlF9T~UW_|K&A%mAnFs_eWj! zFW=Snt#Z(m6~~nZ?wM>p>IuMgXiYoGb)~5rzNzo}Qn+|z=-!1niLZB=HLKZvfk<)7 z%&{%i{UuT9$KRi~bzjh{+lM7)6`u!dDqnrLp`xcx+dWs(z3yH(z4!t6c0;$h-%s+| zxMI_Qe(guab&1Zs>#)OT(z1ZL%ZfHUF@4c1ab4`eH`MqudBcLalb!pXZ*RHWpsHW? zvxjDzOH~B($INo*{d(2Rb$brC>wWHq^R!9&IojKN{4?iiNgNxA+^-zp&|PZ^-?1p* zrIJRHe;T<+>Giu4MXtf;$c*fjJ-1zV z+p^)uPupV?{Z4P+R&1}?-l9`z`)|L?SN45*D9&kd+O8=tK9+?~xmjGea`c$(GmUD3 zADb$#jx#elXXq1b-o3K9Bp*n49e(*w@j5vD+qD5(tAhS}Rxv$xcCVBl8+yNM(X{d6 z;59A-vu=#IS$YtFCeHV9%}sGW+R|vp6<^<-dQV5zL}o|Fc^f5f|29PZPW1F$W_{8Z z=BWEOzUn;1(9v$<6;B~Qa{l#;84qR{@2`!~PupU(ZfMZjn==D;40=26>^R9KSF^B; z{E4y3ez~tLsxKtYDP87%<92@HkFPsgTYl9I)p_%^-@5tVP68#VIC1Eh9@V3cJ8Zt` zJ^8W20qsu1maTRu)FSSTz1>F(*&`O-o&?g{#Znc?CH%xgEFmK#z(uo1FCm zyYEvjTbQ^iA!de}`o26c!S@)K_Pd8h+oax6vEb$DGm821dQ|Wx?B7iKz0e#W=kO3boPU@vOt++9_X3D0MPGdW2XdJhx~gx8#MFQA0q2a)?)!es9J>BV`@S8%rkyxhx>$L|jVxkTdN+j54TXJUKFjH!#_h}qS@ zj;T+3efg@ZhU$Sem8b8jzZ5&TbSP>wd8+*>gU*B92Zb%3yFD<%(Wn1`)jr$O=iJse z9>2fY)XhJDpm8_f?v|TZcfD1E`#F|9z12(d>kv3O)A*2|o2-3@BzMmnG3aaTqCWPc zKfTXR6J*Yw?ojJIO)n~O2?&q^Nuqy>-NmojUk%sC_5Kz7c)%Bt+MF4^ir?Oe-r^eM zCVYQ!-`p83ZryVeR}3|b-Kz2^ch#bSE_)`n@K7N~27u#_n}IoGaZ``a@&+#ZqOX{UJ+ z{GiN!mkyrOH08Echv*3F$JSq0KibiJh=iCqCE{(>i7iiV&(0bc+asiHvvHwz`*uIA z0i~lXxu9g^Wb^gjI)^vhbb0Sm(M+q+Gu3vZ&NUOLOnP#0@ty*=@U`K3U!wirv<~mz zdrXKyiuN*%q4P5r4LFwGWJ2~Gb@SO9&MM7%Q68{cE%c7zkci&tJfkho1o)``&X?x2TfU7jNcOZ*T5wz<)T!%HjSNn?_YxHg*Hgj5nCM zb{4>H9Mj2K!*E>LL^I3N|3#&J?6J{NpmQR|bJC|;y9fO(m8PGqObu0aUYx!0)}+z* zC5~lL`=`99>fTRxaqGu7-}96W4(I%^Bh_E;{IATn3 zbEXCR3xwu2 z0yD1#^ZW#6qlZnI;pY=9FdH5+C7Av%E^`BC3(P$Ig64(<`uGJ2s9)zEIM-*mU$9^t z_(x|K@N50Tz%Lm%+k2WHXs77E_xtnrfr1GFv;JYhuET@9gZ%_H{j8~Pz*FGhATV=s zni~dw4|DK;D{Ha9(u(@5YvA0Fc>)`25&bbUm+7;D{Q?DMF0;TdclPs{>+2^V-VR$q zmb5fDHx96MW+^0K()pM0bxciO_=j&Dh5ww7>+$ zSzu!$tV1IYMrb7fUvgk95MUM$?@EMtPLT*LDaaf!xK4%wWBE^DtdL;h0TAkk!UkfF z6Nohoh=sYpLMVSKHV7d5y_NzX)VXnhm}3ktXtslF1u-rcUZ`XjE^SLxkD$VfI^6~Z zBg=e7t^hFdiEoQc{H4Z3`%&J6Nic)GtT3I3z{XaV?@acx5Ln2ch%F-F$=`?X zm=q!(V&)h_OqTP^5CiOGE|>JSsHATgyt9-%od*X7R*x-p_0Dv zxiRs5yf**?srF(Kz6cY^lAl3fY%GY}Cnzii6DLW8Rk&*;EQC*j|?$X?o%X|K-*KzbDM^a%H< zVG=7U{~9dN1pREpZ~|Zl8j1h{rX@sm zfp7?KCNERqiYkGvxh&zCBnD&(xklgC9Fg%v@$}UaYgnDfco4Rrr+^4XAjjp?+!pk= z3J-$H4gI^PZ>@0c3 z)&i02>sImgh=K+4CG@on=Ui~+i7e^m^Mrja^!1g38#}7N>k_}i=M(0U4Ccay!j`_2 zqAyO7%tc@MAW&!##Vb?@+X!$hRJ5{>1)omb&z8Q>;aml?LkwICE2J;-eS5w>5wK{c z#0FOBvFXHs!|1CJ4j53`dxtS3k2wO{8&ND-d4FZw$ z0E97dUE(95KGSf)W$Q$1?VR&q2N$hkD>@JA5~3<{s)S+>4isDhvB1`eR;W2psL+yc zhH2?n5ftiFI!DcJn6R>vm3K~9F#sSE9fVbs1UkH9D~aG7pyi!v=H(R?T254S4&#=s zmOv{zzB=MWON88TVhJj=0H+8BH6dm%50yYCcWez8IFWxU?6icX018#usgf8jv8BR~ zo#tSE!=*Ud(&7~7T&QkdKD=n@M*$c@xXXZOyD~&VUnO&}U^|$8wzQ_h!NTsiXt6{A z7Sj7OkW@TZg%Rb;8mQy4mgPH>w^*4%iwp`-5J54C5?iXQa!v!A6bw#7R@deGU$zQ? zvfPo9g`m;OkDV`}ukATtu+bL)^G`XCvfoM7x|+BJ68dt#A;J8U{mzyi1{FmZ#l^M> z;6je}AaKCgVj?hSfi2vA#||?aM46|;Oq-aQxDcR$R(o7=Mxl{qzkJfLDU6;w6dVm* zTgYMya0#4|6ikeXX543EV`EzzS^6`14BR@%-(}gL+Ip!b+?{B-#uc0mtnPy=0X(I$ zKR6pG2XORZlof9c5eVuZ_7I1A*PKm95%+4^b`DDdC0(IE8tx^YE?82?|0 zL`YkZIEZk~EFj1fnDB%Qqq;51gh#Fx@(qY}8$km@dz2a!%|GQnO;HL+jItVNk-wYq zRf%hxLII=(9rmdMg0cYB#mGx9B1Gw@6NrrL##bR4Z=p^JTKsWw0tHD%3JhXn8b57O z?mVL1q;`bMRv*GXPM+8>Dy{W)JSMTRlvb1fCVb)i1$Dj1ZgJ=PVG_`|}tq2HG>Z zN4!Kxd!BGt5|NeAFyfe~-j>H7DWB|}VdGkcz!dP*Xxxb=J_^*Kpe@YfW1ReWv_5Tzi+0`DK z#pH^m2$XNdi0Yn{P$f3(2_=kIGx!YK;fO#?QQsqYE}h9|%uZQ^wCVv0&?5@7LnL$t zav}|5>L{@4vG@#W)%&HqMPE3S!>vhUWlF&@T!GW>9z@2(5FXc~CZ7s1VY?5szc_oz54J5L=AY|_W5j)@f5_gG2u7yDjYLh}Lu zLw7oyq`_780gcsr`51A=6kkR3dwqaMpM zAndOP6aQ_Hr!j0O1fqIZACtzI*U2KJtq%;9ls?vk2)P03K0On#s|(I9=AXQMBBZyk zPZ79I-gA>e11tR49+C#N5^nCJ#0&Wb6J_p`niq--!zC8*<$Aa#F$xWw09et6bobHv zn9=P}fCf78<5K%5G%~qQo&iAif^e^&EC1&1`yqK$q>|WrDPX73uy`p-@pFA&Hfq@bfzZ^2qpCkB|IIX<OP@xU{mCBmY1?9r=F7rp{o40OjHRxmOKV8j|542l4#J+2F1 zH6^lNk0xrjHxQH68~RX8CI&xRh1cS=mAA0W2b>CQ+{bt-Vz{G^CHINZjy`Z{@$(H} z0O-~|tYF~NK2z=!(~c@dIzM#-)47}i0~_`+t_l{z-F>Wm<^d2W$~l$U^72VB_^cGvltkD@C^9^alNaL z$zsfEDF!`_LKgd@TMXo>5O+U@fODa$6{{-~f+NNQ$r)gRr$2K-76TDOF{IU@d_$tu z=Kz2PI`3ml84wJA_bQ3OAfgz}AECT4;qnFOO7K;DxI&IPA7}@_l48V2f1hxdKC$R4 z6>PAXA8WA5gg-_*1Ah(RR%$rvW5F>lDk^cAb3GugHTs2J^k%$Hce7xUpl1JVWV?_zoNmRP)Wy$E?i4|0Ea1X_%WSVVwA&=Ncs(K zU+6U-93VKOOP2MF=mP46oVX-Lnfu6z?Rg1=e};TeVUv*s;(9|LgSMy0+4?9w8I(ro zl0Izb0&?Y_JbmK7dHP7nFdh(+p=|yD;jw6vq}ug&_|z5wmA>n zzChRY;iNJJhCY^(j574m1#{0R`fOk#KQ>7jP+9%TKKbmYtfTThGHPQVWl#psy+K#? z;Vk|2u0DAr?4cNG>Z^P4+yY|ZK6xU;216jO_wg}_j3tPpjC|zj2&#jDZ|cJ+9siSy zPmFZ&1x(W>TeaZyy`xQS16h;I@hfNTBmQ#<5s|kB_B-BYk{8bkQTr zC-Q)(PjB?%wEQ^33_9hrEF@x-kBIl-G3M7-PI zKnzxIfFN_uV_*nH3<>ModwWWBNHkGlFU{j3y3&e0EAKQT}MmhP&g8n=p z8AJE-Vbdu=7pcI;$C_7AHamOk6u6&t=$R zj0&+jN#9PuDE6EfLu>lRQtj9hx}6Uj9{Nn69KgB-6Po|Uy!S^vwxmOmlQX#HFA7h+ zk5B%5Sn@IW7>Wb_=*L7&E+fov>Bba_y3A*V8B;h0pF^d7opKDQ3@Ym5q%sBOJq{j3 z>ep+?mUI?!!VH&&Oov$=4=`we8)hgPkg3R)&<%aqRAd?toCcUZn$R3&->cV=EuowG zu+o|W{~l9F26#|3KwTiyO0p$N*LLqK5oJ=F(8@% zYTo>#CR@_h_y!HKdLti$#h6Y}a40ASp!H-+_^Li!QW+g!|CanHD<5?@e-vf6{057% zy2NKi8BDG=725;8XQ2^s$IKvFe*+)yv+X@Dl%I_ zcl2RB4h65>WlKybL!X??Yzf`dhs~KxvGPAz`oPB!WnZQ<6PGW_2N|}Zl7dl}^Q<7_ z91J4o<@9Cfo<3}N=yQSQJ(#QHJ^Vs*q_K}0l4*akC45sK&WfqP*~cD|QPw`GL)j9# zrw{AdC|K0Tjy0sQ4{U_}qccM{_2I<>Fa8`nC=Q_T&@N?5=$bxkE@dir6nOjCcV{Sb zpUkIhNl$w0766-1nTDf~3K-)*%Hc=rf0R>MVhP>Yhc(>lt$y-@07Y#~JcvO+PH%=T z?Zc|iD)9TU!wqTptJ9l_!B-h26qjxevPNd~W0g>L$w68DCMn>epuNI?gFGPH5hD$bOBO& zGf{S?~epqR_-VexV&DeS~(hn%ro1rWGuu3&>Lm*Wy z8Eq!g5lHFHgrJZSV7RIZU@(~pkQ-p~Iy107DRu$M>&(y%emDaRxMH9-KvtOq_p%5; zQk@yTzz^4I6`(VNyD%6Qj?N5(W(1fC(|FOu*jtPU!(}t2Fw~9y%m`!a%V4HfjDg;I zVF_K^hc~wB>OHdsW6dw1FHj8SkIoF;+J}?8KJb&=soR19&WB1b&iP<80a9;f zhv8u-4@d$u9!hT}F5hAB8NP}sl&$S!@fmAK7NQpS{n44BTl?_F*uRkg()d@eGeh_G zVa21^_{ShM(4wJYtWIa9uRb_AgDcj+OeUmnhdIwQaB3&Y_*btpLs$6W)R~1c6CkI4 zGLHvE0rkFS=n_A?;ij-3VB$ga12UbNu)i-)Y1s5-D$43ufJJF6eOZXK|JCWs5=-b3 zKWuoYv{7LHW0x9~{ZCG3hVJsi8fP*uATw`RI@41CqwfQ3jWpq#I{VORUn$y3J$%wS^!LOKE|otembC{JkE z0u1E8%m~OLG?u&?(ze^{eU<_%=B8B1># zBCUax-b@%gX0RDX1x@A%WU(1bXBHwYfl^;HeY4I<7M$f0?gpgFB|~pUxdG+%X6TMT zya5KE0odgdwdIfYH4{Ug$OjlUKQpK!j3nXe%qTA)N$Jd>R#F7Qi7;H*LQ$9k zA0T^}LHPisdNXvpAJ*7XU;|{&F(@10AH5m6+YhVWta!N}V^&6O_mlTE)4mOMoMF|Q z|IGs^{EG+h&;ATc=#oFIL#Bc)f9wkZqzSN2ZzfJZV~`mxof)LR%=^czmX!HV{@P!N z+Vdy3Hv_)w4`;$SXhOYd>DmE)H00JIh6daTRkQM>y z^~)yD8!F6XOaEA5#=(KQ2c$%EdI7T~bn_opaaMu%j}vC7OMrT@8M^-uFP=IcU~(F} z-k>~yGO?L(FKw_mtDF2;amE&zQT{(Ukr}!J5Ss=_%>fGRf9$%6wEsy(X8007oIQZd z2gr;u_K=CR0m?*XBK(DXkYSr>s57ba0I-6L0|b%$f0kyz*8yTQF$)#g|JWik%Kj%O zGDG(PVilQXUO-Ne$xAa9pkY*ac`jYVYzbWkh%L@Q@>?i&0Wz^5i~!WM`Db~C6^)ra zZQ^7z3%C`KEizjmoq)8+Y(@WQI9M=>%oZ|BASck|ugok^j=*}6nRxuFf!NGK(Kt!Agcr8M!g5X0%-|6mI>r@EBG!zTwM))Jauyb<9I}Xpujzp92k@* zkbIxTTMocL?*ecV0o)G=>?hz$9t)}s05d=vQkV_4=vcuw0pcv33Jig)p~eDb2qeAs z^5Th!Zy8k+-&4RAG7}(k2B0d9yg+M#+yofAj~55dgE}{M>SOIQEug6nj99DyLui2H znH;dy$O^Rj5G=UPh3c7QHb6$^Q|BU-PZpF5kTg5UGbP&};Q0_M%NYTNE9(Icnfs3w zU_w@aq3nMY5b98|VFlj;h%3-&EYR!+Kv-B%*)M&DrW;>{v^mZT2B@eHvrh=; z#ij-Q>^`SKqe9MHKID)FK(7@B}oiznO($SIyECm|8_VS^1i3vd7--GF`0(ZNQ=BO43WV1pY0L3w4) zXha5guxC4r2C0nafo#{rZX5a7`#dt9XPz*+&kNdqMS&Zvg; z1$uu5YmTR0qA42I58%AxuuxzQWRC_Yb08%>YZ1jO8j)Kr^MWK+&~1TOPYG@lr0OU` zPemEhV@D+i$+G$FUl6eK0Y{oep6a#>EO_!{>PxYVhJ%LyOLGKEw zw6T#-8`Pdas$J71EgsTe7SbeG z`GmI=#1^dnW7TbCMnNW%Sut1ZSs>R0nmnVFC+M0$yjY+)53Xo|z$bq$sM-&(v?Ifc zDOr$yj+mYJI3EC~Z&VD{~ef8wVU4yikw)!0% zZP42SoO}Q-1|iN)Y9L@75YjmaqK)DJkH?&Ak566FP#+x8wm=JHp%t|_CR+D;ys``I$%x+EUiQO{xie@fnbB~5yXoH zJ|S=t2Q?vp#L*`YeW93uz7OKGZ8)h7ZYAVQ2uLg8-LKSTJ9L>K@O@$%t1V@ALS~?` z9`lmEluA*c0mUvX%K*DPx7mIlfkJ#8vu+-t@I^1+z7K3fa8*zJdw^p^%_d4g{}|8b~sdo z(ONMF3n#*m>jS~z2*{Z~DOmuzLl7?@c=2Z$pro%mQN(97p#N%VK(KoOoRXR)e1{;1 z&yYI=0iU%eul%9`!5${Sx#`7{7WJSZC-3aCL|O;w6RQ4A@d0#^AXbeK?jPjj5Yj(L z1M0%#0~MFP;AZz|K<&cU*T7ALRLC*LPo#+ugj_pvB%kjK06}{KTulzeaxym|vrz+! zMdXb((l;R>Ng-O9?@hi_VHQ(d-3J=WDqAH1wKOdP=NFi(z>)kV+v4%uNt(3dkCp35cx|g z>27z0$s;vJs*+w!_`p5@-#Lhr;^00)&KQdF5dzIan~ZD92Ty-OR}NwwMP*(>MjEki z5-pKd!u@LeIC5xX2D#ysab*@l7P+x6+t41uB4CC3-Haa&PNkurEU<)o2sz}2^bmG# z!3SbTgD)GjRIqH2EfgS^4T2{N63DP7%m~Ak_Mm*qyo0O=XTnu@qu^?T{VbRjF#C5nMB5}Hb@sC5IptB?wWkyScfhf z#71gV?+JGia>^&tMMzulr?sX4(Vi*xbbwWohFb|aA%}e1FgPJCV8K@*cW6=l0Ce>r z-cy3h27nkCBM;I}c)kr_x0`BAcEigzCMEL{ayX3M+CAi`umLRS4`voq32 zNFzC_&j)fLZPH_h8P;+GcN214L`XLwy#^-f5nq)oHK5rH))inoCaUHr*gwc_b5Q#S zNpMW<$j(NTA_ZMQh!;@ZB?2ddZ z;UMjWfDWR_Md}SzORZ#%LRKwhM;LMyA~<}@%| z>ySie{(Jxkc5Q%jD3I=W;s9bl#vcHI!^vHzhPCYkGi-9qR_)P$*rB!a>EMHw#op@JGmLL|U8igGt@i4HtBE6T)AD z4rwfup_yk6)KF`>QgH$gm!d;8I&g;}C-9K_3c>9tAZ##ILmqU;rM=Tw;1dG7_#mBz z)B%yj7W{>zyB0Il@Lj@7_c>5`!wNN)Dc1^VF>GSRj|Zm$YYDLFY+$LcbO8c~1UO55 zt-zq5n5@B4-?rp^F@GvRM178vGZ^<`TFDHD%qEBBH8rggv4*07n~waY;8dXU z04{!}Dh=FW2&y^wlFJ*E!*E?+8Vc;{0H;1Jea@Cc&+OYn>C>x<0t|GXI;5);AhSvx zW`J4XBQu$&@Gk*|G!=pX6OeKJD7gc)sgT^q1&E3=xT%mLGiH>bhJrdkYGl4Me>6bV zp#1=is*VWI)f2U+kOYhdWI}pwV0Qvo*%|IF>9chl zHsAt6-5LNu`eeTW6d>q+LTue96>SQ+NWF zAyrC$4F;ujn*k%%z|RHK-IP%JkPUbK^+o{qu@&lacUiKgCQ3c zVmP25;-5c^(r)PCn-gz1=TMp1@GtR(=nrT-``!7WPdxZz>~z|k^`A&c6;yUgTY zl_PzYz*E6epSvFXIB+OXaR689Pz48WF9blCSdjL@mcuFOhL#x`G{6)%3t27?sX1NI zxeV~)CrPXud_SQT+*C* zTI1nF8&*K@!GK*pQTquA;VykrYc#kh7+~CBbS5(v{)Ntv&O+-k6rIse#c~b>C!N8a zg`6luIt%|?dNd|ijpYXdP>_oPu+|MzcjGFQdQUk+4z<7Icj{;``lLfG~oK)RZ;3x!d0AoneNJk-k18OjlQf{G} z3V~e*Pn6Ms>h22}$HVS6rN%l1#gphe--Ps^M2&0EMTIyI$QlSM)cp+W-I-iGC~1J6 z3F0SH1(eoCIn;)+-bh_$96;30a`}j}26Bbsd#N06>gp4|s}LhiOTAefEbQ(^>UdHR zY~;Hs{CM(RduH9l1p>5Tie72VYSE<5AVna6y-SaD^rbo)_>w}57t&J859d_a+PXE; zMOZ&Zhx<`p75Jt?T*m~6P3o56pyCh#%2r4}+1jcNPlY@@mC^BMv$la3c5@}s&?y8J39uf{zx4*lZH3Eb(uV`PG5|jrY<-UOj+`54EMLAga$DhvSyUN?FDk?t zXn>mkg$0nl!phm8j7D~5mQh>>0_a)6JB~ToLf>yHDx)zg_(tTgx%>cdDzFC&@J1Lk z{i!l4zld3S9jFMw@MFo0F3bqS1p-8v%rnRm zI9T=v=@pYA5P*+kL2yB|03lAtko4M!OJA%vf?Fd^gWxUNx}mEE&Nyy2b%tror^(b z$W?{3+vdf5I@%h%`pYbY9F>DHL?a!9fAm56b$Pm^-2(muvUyh~yJ4)KG7BM#-B_kb zsWz)9fxvrm=p`|#PTJ8GnnkX8y!m+=>ni9H&u zddij)vZ^Ns3F?}F7Sg6Jr-ZZy-%e<)z(L5qCO|p}!8i@{4fs+*oW(|FB4p$ZhtH56 z!u)8kgeb2kGs18sKj1N$ZIBybjOvf_4X#^76@S_i!r?KTX|KRB$R0kCjzPN^YP5l` zBE;xMleV~jua#)epf`9pKR%XvmI%I&5G$a669bX^2oJaYY z8JR>~sKK`q;>4rCJIIbWYVmh|LmCK+)_}VKdy59EoifiL zw{|k7P2?6rFm39SJ(H<0qitUeN^G)~gp7`cbIpk|4U$9GQr#JJBOz9yQ)V1wVBvIU zNZ%km1Z-VLRZ!?!Lh!rclcE)v2ictVH}jx2`G};{PTKLti8h@5nruBG z3WTS18>n^-zLyXuKg;Zc3@AV;0yhP6*96GDguVU)be3h+V00$)46^8qE!84TgY;cT z(ngxjp!WkP0Y=h&TEQl&Pov+K%*hxFd=y}#z%|Iajj}# zOx7pEw}I@Do?*k{fe!_&%XJ%iwU`1t+oTBilpBs5ERBqz%e1NVZG`bF?&GIao0AvyB3qAWKBI0bQ12n;_ky zdG7=rjWn}3fN)X(XmJ0*@nnOv2?9WR5N0ZV-!yXR28?a`sh_yl51b?OhK8q7G`3zUe2!uAEIhF<(lh4?zRBb@jQJgaL z1?SKVs%p}_$37Koi)?^WV*@t{QV}MSk1(W3Fd!3be~~g{!(xG^KlgA@y5ROK8Vk%O zh;!A04ZYfgllfdMD3@^I9LfQpsKt6f=$p8?{E{H98sM-m)3vy~F(k=)>E=nS8dN_xJ zQ`(Rj1<{m-e6ulqIH!(LgBsl^H7uZiYX*=OK^oA9uQOX5*Ak z1~o5N!1wT%>uNq9!!Jh7J2krU|>1kIWOdEpEV2x=9?HL_7G4YvwXA;*|- ztWi?|?cw@$7O2qD#+;KwSOL`$0^@|ZIfQ5o=uJtz&V$t=hW(kuhOuRn*#ud{#tt>4 zPY~P>7?5H4U=@dS%;201&LS%F3!+a3`7&cb2HNDF3zRwlT{wu9t`*n?*|~$X3*IcC z-jnOX3^QE$0`gyG6Z~74p$MSWG7x4RFHz$sbk`t|QnB4c+sI6Ve+e@L0g%p@foV`W z|21sNz=r~M{Y07u7heGpw$hauVYn^@K!nNOTg-|u3jvrA{S%FWG9cDiPgyzzJD36zLbdbe~c=;Clvf5j9n9;8sBnul>y`s737i)|eDL zq!S1#3}6KXeS&}j#YiCJn!!Frz?^DBYi=B##uaSz0YSwX6qTQW0-^06yP%oNpt*B`sehM>fUKp17Qpx^1b3SCRn_*7H_yyv`mA8TK(HfvmUpn9 zv!BmgUq8VShadcwJc(2yWyPEY5P0#R8%4pX^9B7u2hAx9;4&5hxm+F{YS3<4#jlab zYldD6=J^TChE16;DQtUxTXJ7 z3knYO^Pa61R`h=C?4a8NG=F`VBsAWh-Rr*}WVacaYvwi0i<;2Yn>-;|8qw;oIlsVd z;AN*@wY_@>`ffO0+;LZNc2BJ}E^pr~-gWHm?I-K}qWbSrC88GGNWGvsbcN~Tq%EdP zhfgkYd+cpjoVxzAXw1~*hNcCZcp<*m4W5@Soi_GBWM0uelhcoLz{;C(?N&CvUS#oc zee&d3S5E`W_I*dXOzlYY)Ss%QVlRoa9)5PTi>H=(vFDqz^#P}gn>HWbmKdz0w$k#T z;YZhz=6eLm&SfhPob9^FXQZ;)${xjy9{-5aTQYDo;cUo^chP*=;*IL-CU#Sm_q5P-n%ALeZT}X5gJT`` zt&|LE`gB|H<#F2EMrSSUyZL&b&?dzz1GI_<1h2?T+ctc_%87|P&bv)c*WI_K$!0ax z0k>A~59pgbI<8B<&UYFqbzD~(pLprjQ`7KeZ`wZZc-7cJyq{NCl(ssk#M`pmrE`Qws3hD%yU-SKXk+tJAG%cQ2U_O3IN3ga|=w|DH_Y4^zD zmxkL{94U?&xwn(4b^6L};x*fq0|q+izcIaJ{;B`#fhz)1f2BHkyA0d1@7;R8#&h? zmo)#3ui>6nTAy!k*2zD3b$+JV7&M=8w)Pl~gR#5>4OU)vtq&MC=*p`msheHEby%W{cmL>gitiE}8 z+O=l~vNF27Odj>U`_lYU-Nv>F_txi}Zu?#L=)@zRU(GudG`_v@(PhodUfjytb8bZG zws_rz?@p;NnHg2G+B2!scwak1gI6{sYEhX5l8d$N&7&(@<+?SUW2!U!zh)izZa#z4 zmxLO=xiIqcS&a*4GaQXyv=|$ZmRpb+|6F^+mwTTxa$?UW`njurnb@&j>6KFo2vT0jeoo#V}Q!2 zi?3G)1$9W1SS(rh{={ATJ(pFqAD>J;HEZnakB_324yi@DH_pGL9j_Pua$d1(ls<3ZpmWM?E+{2vZkq0wH~+`tb6rAKA9*=RHEGMp*v?}XO&l82 zf4J4dW3gA~t$wmH+%&P%lI{bKPj5G6U(moy)`vp3O&Rt?cf0AJyIlj1kAB$FXVU9u z#Qq*(6OJ1@Ub*q;c0@amTf@7a>@RU2$?)Q7|_xJZvAM?KF zJm-1Nv!3TX=S&%s;5iX5Q60?^E8i)5qx9sGo5YS`JzQr;CC?oUdq7e{#kA!8fHb*L zy4#iPxL)w<&-BT)89CI+M+ZMhT@~w&Xq*4&N{XEnmfqJf6OHxE9Bk!W)_uRPOtbLl zZIgHK23&sA_8}jVV#*#fR(|OwJL{prEaUOxqNPbCV>@N$ADl4CPB~%p zsRJErQR0OPRqtvwE;OY%urri*`p_Ndo=3omBlZi!`%I_G$SFX7_hRp=aCM1oKN4$Al=zjh{6xFiBfJ6~F*>}(W zol9>!_gw!(G`@VshwW+iQI^G=WYwv8`n2b?1-+tf#XkGqO#ul57A89e1l`E)(2i&1 zim{IgTtRV_v?y{le&&t4cQ^MQzw{iOacODPX+A9pV_P%e3N-sWq|6_#xU7_ne0Epj zqh``0OZJM!_v+G~LXMH_di_mJ_aH)2Qcmf~y+`Ry%ET`eX}ujXvrm`8?15+)?7C2M z&%G%yzjaVozb)jp9bd1K=n~f~X$(29%|+yNvXFr50-d;ipK@LRdt|PFs6kPBP~OQS zSx=E)h-b}A^_~OFC$dKP_X}k=$c9gy2)>(@)TX?wV3Oi^0sfFFNp41`- zJquG(4xemyKYy`l*M7@G*?H1C%EdU0l3XmA`zfAwwX{3Vy#!>2USGNQg=l3yFpL~d z<)W?L(HwA-S>LG9kz1{?2Fp0@5_>$T!0t3Xb3r~zdcnkm_p(EHn`c|-XE(;3qq0&o z(N=q?f~|aZFB1udd*}2YD<@TotSA{ud^L*hSr&=mJuK~TmLR+8)!7>sFg8!g+KT<5 zS3-!|wCqXrm&-l@rV{MC`kiybV?>#2-tD=geu83`8+|Zs?tD?y2#d+owSE?fccXka zIhW3P!aHU75@L?tii^2>hS%7_|r z+%bOMD1Xs5Rl*$hBupo*^hL-+Rmbc*RRvYAn>8mPXX|yZq&^1-@XLR4T5!`mZd!kjBHOPIQ4vKqMJt?)x( zdkzCzzE%ELaWc4#_scjL_tniCVKQV>oE*%FGTE14( z3I;a7b+L~7j^geT9XZ8O|LF@>7vE+)L0WK5?DMt~3*xMo8sus$cdQHeY{ePdkfGBW zppO;#ulh0|VfTq?yc%1{hj_2UF2~Z>rIh-9_Ma&5^r^)KY_+6nFg?j>JAcY-%h{*<0g5(!L!oyaK0QP3n;K%cdGVD=dBCT={V7&2sNh#NB`s$8MUvlCOL21w*~6<|{a$ zG9q|5F{;|vr&6|X>6@cVW@pP%`{KRs;;E(O?+c5g^UIS9Q{|{Dqm4^TuZmsgzmHO| zyL~x1KYC^GA^XCX-<4ep>@%LKx2uD|aPHf6%YopB7E|31`36rH_@RB@ zwCdNzn)?#YXk_O4XfmW}I()sJa(U~xk{L4Y|&$VY?v{%?A zj6LNyz=$>oMB9>}-afOBDg1(!uYX<`Z<{$}s;V5{cEUU&TNUVz_tj&5BpCPR!;G7c z-QdU_$8Ng|7o$^6GClSs)r=~JwsU5iPbp;$vg|cH_OJmNe^<0rJ!$;i3mOaSI%t-c zAgO>lZDEj?btm?6HSFClNX+PBvQsa=`c>2IyW{yH- zW>aF!)In)jwqOQ8gF#y#?x3g)c>A=>@6}5i2BYRQy4>NH`%KPLV5t0jDcDSkE;Q3| zlRlB7B+-uLxF_K8&ds_`%!UA?G+^s(0gBDryDcVNCd8OYa_JNn!3=T?*JZC4voGcA zgo!GP7*&kLgid?RD@EO!r>P@(UO(MdA;PYnT{BeLao0}*&V2X{g(+<8$)cIAYE-vm zUYYNxML!KfeRg8{On|sp@0G!?^Rr(D3{R9yEshS3&MnrC&WwIc6uBBIo)>UwesXX* zuTjal0r-#yg!&bK+VZ2`CVqoXD&0GEq(~8p3~+ zm>}RWEqMT?0^Grh*N;%?yaTFttVrd-OcOVfS@&SU^E%B$!66pw!}k^S?^^7wG>B!b z%PI5yEPb1_j+=AO(Bm^6PUIy>A9m7_(jo?d{Z|wy?&c9!79C;aKu11mDn7!<8S^44 z_M$oQ)JLf%s&ub50q8He^$-qb`MD!S0}a4+dd zBnwrt%%NBTFF{S5oqA!wla}bMXu~k1ch}UcE8ELyl3Y2(*w#q3F8HhY;Y`qaUZ$s+ z(unePYI)9JZ~031?p#G(Q{}}dulWhEWxur%d)+Y}*mJq0%;y}n9sU=y`=8)V8qLkD z=vO~6X^TtqGjp9&OM^2XeY%S~Vip(u+~z0e^Hh} zr!am;$yFn4K-0Kj!qB*3m^GKQSa!s^_|>lDxPISxSpO)Qo52f#ljMZm^83EcG0A(9 zc91lntChP0EgSN*B-_tv-$e(+J$uUmgMcC4zexNtCp z&NG?DlEzE^9w7d}UBrZmfo=aW0pf()uWm}!(*;=s>>|q2wbJrCQDt6{kVGvsxchFF zY1XkMQbQ7{v(!W~XAI4bSX?ZVqdiJ8oV8yDRRAd&>`V4km+&r6W_aMKu6#5qnDs?l znU3^Pz0`ffMwZ8qlDKFHQ7txk9e5YaRVpAaBhcu4vCJ$UAZ}=gv_8P$oLX|=i#)Zy z>j*DtvOIM@QwLT;v5NuGpQ{rXc-vgV6fcxEynJTh$!N_D^nZ$(hjims|PgcUvT z_x0cIhY3#=mI^2z)o}vf$+YLYbCM}vQmnIK)*}~U%8M0Rp7?e)Z#-r^ z-PWae<-22eC;#{2lA3qkdzmKGtdEK5HlHbccA_}R(aeFk^3k61FQ2`Q-DTX5$-Xm`%^7|u;Cd(4 z$g!^EqF5oZjL3-1b-SbfI@ms{YhPPSfB?asrnp3T0zRymA|8#FpdZ?bPSZv7oy@FmQ48^QvA|8r&gwoiMF$R@zvf8 zv4B%Vv$Dbd99q#_26X}quR57P`8MAh z|KADSpDi2KgiaI`I$!aiy}RELb>}`muLi$IgvzP{MuoX0*wu@?5FOKXA7j4_b)m|K zx}XuRs`?FcPX#H7Y0?^UpOF&N$foj<-E8Dg&28X8uUvbV=*2L?iGCE`OoMyW-` zWUo18-g_IP3x5=+QA2{cRc#Z`mTwxn&{gN907}wc(0?dI7=3u4Xd@lEYWUhO8gkr( z*&483h99PSCF-CvuTet)iw6y#g07L}W&W$43QR|-T!KA(s`E71I7lmZqxNV?R}c(X z5A=C4%7X6}T&Vx=$ebc7ij%~1 zPS6X*k$WPSaFfinm;?fy;qYd~!z&eWzLHm^bnh2;dn@-sBfcx?H#Fiz?!;%ydtWk2 zZCcHz9(*I2f`3t(n%#34#o4ys@8S1HuM(!1YUKq*xLP~H^e18O%yYCgddidEf)QQQ zGg@)A)Qp_7=mB)BwQMUJy9k3s{TW1S14E61JQFFlA!3(V42dAG$^T_Rh|9VkxVRO zqtvCHekQK;?3iGxD--BzHQ0<;;#oNO(uFz5MHubzGFN%s;UOE953C>6)_>#<=xbBI z5P`Dh(M*4oLyzgw@v!S`RQqOwyFr-)99^?J=NjiO6^ix(SBpAFj~Dk|>ACv6r0YuW zRo9MDzolZJsG}u#d5(HK;oGN@<;B{qjut0-;%X~94|E^+j+42Q!1`Y3Pd5(*|JtSg zSzNctrTPNfU8=#1Xc=%s|AlHx#73r9#Vx_@F5cqm_y2iTm7OJ zqEkhs6;G-qy&VbjB^>PZVmW+DjLFAGKQHW?`8;JhD)hSMWhuJ1$xNV2y$QNhd4@qR z*&b}#o%oP)m|djaQ~TPyk=vT|!jL&mIQQ)UCf>dgsrV=Q3|@l318clX4SQnk{|S_| zWLoRGciobq(?;K_)x;Qyl6ET%o-MRU;xlA8iMhjB#*wI~%2-%fJmNOlGm$S4h!hpgJ;2AY)gprrf5-`pY%Gb*yh zSvb9QpGsFmI;c!>9hy z!0i&E8cqt~D(@0ZcoAAaaak7(C2z3Cc^#i?b-idTfINa&xxY+C019nO>988d*2(md*H;q zf5RD)KQYQ(4hi3TIqTttdcYN(?<%Z-sJ_i*^D$BJoVe7i0;K>}h+deCn8s9()%xtrN;nzSHK#HbFZM$dvcdy zDASq7dPKBa250o@)t>Q#M$dBaY@!9Ypyeaghg)*g4NEfD4qxlDFtE;7IZDD_U`^8C z_}J)rU@Xnc-CB#n$F*S6-q!*p!ZRc9?3Wp}DN_&DC`$sR?-4`1hfNfTCR-`C>=S4k z;S~3YWEc(2fVSY*3fSR%8m-{h8KNn}*8)G?54pYcPQsfxS$>!ynMSi;L3^JnnO0Zb z_0Z3cupD(2ge3PuNWIw$c3;ARVd8!Gbz7@MVwt+T);Y&iWZcVWqdkx+H9I_KlS-@P zsf`Y&3v-gE%10P!1$uC0pFO;;}&HjU+pZGkJ$u@i(<>t3cuguSWQ3wj}S^41w8BG5((&K?T1!NdU4| zv%?7%_;7(W#yUf zOw6(boFuQVJI@4<*pmN z|4QWdqkN@{b7*nH&`iIl61$J|PcHTPP%JFN=4Z4OMj!DoJpVF1ISkCrcFx5(P|nUT zz3I66ZrJ8Y)KSE(rh}9pukD~!6XuiwEEFkb(eny|XHOa&KG87{v%DvIoTN-sY1YB(J%GBf+IoS771ftd$% zi<26amr0N7)?PH4lVSZ*U4OcU#ZO6jaAx#X2g}R%{gDu22cedmEMm`Rn>#~vQ@9dm zGU7j;H#+t|x?%_~5&R%aUJ3~wda_YI+y{}jb>y{~2F0~T0 zj~`reo4$D>&B|a=cf?=cFk@GMv8m_r2><-->&H>_X;bfb14hbcrVA<_huF>gw9^BR zNGz>X$_Y5K?WjymNsQVB&a$j-qT-<=&JF3V# zQ0?zA7ipeYXdkJuuREP)){{Q3|6bpB;<0zkoI~jjXa|4k^JMxto$oH;k`*WNj#U;t z%s%gSTq^^JXHe3#@X?5)eoG+>Ru@P-dS{kY+PatjN^O}j|d1l#Jsmo$4E|hmu+9rgi#G~@M zj+fi&)d)u0XVs=<^^{5G)ZP}d3VHRUz2?bc&Y@b@msss?-IxBt1_!gm2I5Rsh#TS) zIVN%y?+MR$`H-k(J+?2;7`d(7kPtT?`W4U#^6#g9EKWN;G>5de!uE<9?DR0#fAi^% zVAs1+m3jMOZHh2=$(S(-7dQ(Kp}AtO(*5Vd1JMisCHuf^@)@niSq29*MDAr5^nEK- zdXickvuV({q9*gD-h1d1$|s*E7f4X+8-T10i9i+B_=xNmU|HZXQi{-;DhCg!P=}2;ap%og z=X;xJ8Te~)GqXp@Cp;BgovD&qMss)Ak`K>HAb}y5#O|-AZ8emSNb5; zS444f_Dm3hhE(S~$5-~rAu%3Sfi7;$Y2RIkC8h4t*uZwYl50mdSKK+8q^eIqM8{o) zNJH(v1F<%3MfBg#iPDm|YchQMoS;?|&53}gF*c-R7FS+%jqO=Jspr@9nt;AZ=$5Aq zFtGn%7x#6#2Du%lpM3c$lDE(lmH6Vq!Qm8I8ySY=!VnR2Dua~IrtV%X0kOB855BtN znl2E}Ioj!6qrj?zmCA<7<+f&o2GhKHEzr)H^l`UD{k}$%gH-|%r+^)cnFX*|Jt1S$ zKF$#ni9I5R9#{s|vsLE=+Oi;YnFn?sCm;kISW-BbIlA{!)X-Q*evVOK{`UM^Lc)KAMLm~o zbjFqNbO-U#0(GQ}=rPL5BcaE`q+@6mkjgUeqphU6F4)ZMQ_P-$@llVR^5i0IBT!tp zz>(4~PJOYiirY-Q#!h++8bNJK9}-#q#SDUK)+K_oU-pCt=D`3ncat1Dqu;l=OTzsQ3yys#*s2 z<<{L9#Ujn($S>PGg_Fdki_HXsg|KS}GtICkldF+cjtNdyS(nG-bcL4sm0Uws4aSXv~rF z{;n_=oxD#&YG-)(3jyA_mT}E7W+eyT!_^drY^4eoUkp&Tgyr_^+uQMkM=Pf;MR(ro zB^5AknXlyZdTQ5@@Apz)OKfMO&5P`qp+5gTi9Ol233pXP^FXY&g2Id>7g1C%n%67yH02k^r~f_b7??S zn96S6+JyH>nl^7oqn#WHrdkbUoOFZ7LQ{u^*pA&g#DFs8Y#(dYX0qoWW>BTkr^_&x z$N_dcEH!3P)sCt42%O;_@IEj5YAOC>nqS`8PQ||F3rP>7h*`f#=9Z__PdLE5t6CyD zZD|w8>*tNM1H1008l&CAT-3pgTMtW>XXs1z=~oEbrzh=V|J<)Tp;7k0T@FUU z(MtD$v05H>yeF+&-D*T8P;m}&^dc3JNJ%SjlYFu$JE7U(hM1GCTkxmKdlzDeCZ4o; zJ^RXGfhm`cy zC!p`e0HN67@MgEy6i?pgzcwSWepYBN{7SXla6Wfv>A}z$n)WEQ=PR1=x{Qyf@}Iqo z8m+d?r%{lV`)XGbY{K`A*=xTH;aJLHN@%UT)CVaAOt#)>W;Lnd$l;+lQi_j;mu!;l z-!4!0$>ex8Ps$NHmlUw)>U$RVcMDGr$-CD8oso+nA~7`cyEz{Q)aN8Q8eDcJS09+A zczAyy!ZSum%~~?j(SUW2>ZRJUmK>W5{b(Yxwz#r#=)|4fx*yF8eCcT-9><=0`!r@h z_2v2um(z>Y435#vDzd%W#ZxbzsJuXQ>0Kihp(vQk=)3^ca3>Lab>duQUCDCT{T1ft zS1$vPn)ujOm{|c9l|km&uJOjf7$(Zf_Z=)^@1#=oty8h#EZPQ96g8Q>ltqM3k6M^s z^31s+DDN*~6E|=tzuHGPvp2PsMuz_3ASzVv?u5yQ@LT3V-|y4cP|k$Rf3R*sIgBH+~>>~aXCsTp`sM-ZfiT@s1Ks3B~@Ij}~f z5a9V8aEYp^48>UBh*&*s#eP}}ZHczkw1&bExTk9;E!T+R6yU{u-Ifm`bT{~qFb@$* z^sN=dc=K7YKjYgP#;Zqu)S$LjtMjP{VFl3y2eq?8VZhTZAkw&5{CyfYMe*I&ab~}O zut&lWvbIpC4PJ18S2S%fLrO|sOG^goxPJENWYBKPo$~xYJ7~>~`vOoE2*^nZ+@U2D zcwz~%246!~@(0K*XPF#=ckAv1`0;lRt!EN`wVvnU1FstS$$CCVq!z^@j9Q#Jvb5{X zr==YvyX;gtB}^QGvyRuDlXx28C-Jm;T$b$S!=z-}Ld$Zq>_UZyXM+0mgZLv>mVvpF zFX2T?oh9Gz)KX}#sPhkoa@6!>YrLR$TMQ5fzBiH`g5Mb?x}rMO_?kU0h^}WLaR-C= zzHf=7E50NXQ*!|&MNBYsjm#l!rM%u1JAv3D8Cn9_Pzz=nRNB<-2uKZ54tY(1??>sU#=}#evFy}hB8+K zXqopTmqwX(Gfn#^o(XF2q2!Sdq+@IJyBk9kC#{wSt8r#BtU1}foQIwV?v>}=ZKdBo_hgAX1RHloZ3zVWD2HH*QJ1_G^Jf- zR3jV#OAl-7qIMoU?#}#RY}D=A++bd(0(!AaueB{KEOxH6Pm-3#2zGK8^QWb%Pe*p` z>$z7vgnX3R*IFTVli1SRef|v$LLl>Q2Rifg>k}FFCPYZo#YYh0XVv$s7tg-(qR>i+ zta+=|NA>=NA(PGQYmMFELQOT2eZXKfmEyr;XA4A%p05zRycqN>*AZ6JkGPgQS7AUH zcteSpB)qrUWn4Xm>U&#Y@c|+xZrEt=v5#fJmtM5`mQK9wFD~DKj`rl9PEEhZb;yX| zfp-5c3&k~?0NoZIt-Yft@48Pb~pws-owfYVtw z!YQB0-WqLf*B|cRbK){_lyXY!JZ zoKqf>-&dN9lelx9Mm6!KC@fTru<#ZlXo8KvhKMay`2|(I4bpW5p3iCLndr|h5h(Ze zi+1ev#e`bY?wI_hGn&$8Gt@7#bEHchD->ZrI-mW)K4Qh>eCWzKva9Ehs7qBMF3$pd z`Z9bESbW*dEO?IGw|OASPhbFaJYvi}B@^&?q1OKKG4D6F=P~EvXe2KfzMbyfomrgc ziAowFJ*9HM)@u=zl2S zN$O^a<5%&@w^r}1mTTXeE#Vnudpl0#b^Wq%oZrZlFL8<1SXTMV+pX@>m?Ne)yGj~_ z)m^^adf!Zb!~8|d-D>Uo5!HSou(S@g%kwNL>OOkOSx;#7AG4eFoT1ctLYEa8FLhuD z;7e!J&*Ih}h5GhK%6M7{QRE(fN2DWUyYpSo?n&%v?*%G5u9(GO1ukh&ufUd+wL3~$vfu3b_|+DAbon`Rgx*k2)RJ>+|jQ1X!j%aq{}P& zdY`4XB;~L_a92%;D|Nfzte4=%opYUph!vpZFFH%d?qD!n&99Vnd~T3cV7}C>TMXX3 zL!3!qmz3pV^7H9ddfSk>*xCAnEHRTMGH1O~cJ+N$AR#e+uOs?!LZ)@F(xe~iQOcK_nMT*+Hv?6YiH^6xxKw@J?~1@tQuwlY3#y+n~*9mhE{yS$j;I|Qt)cB zhr7HvHM+N^2T&!*M_jpd7D0CDXm5Ej-)Nzj#pP;flhI|rcZPnak~4JuAf)Bo3d<<~ z3uhpiAe|xQ_-kTXju$2cA^uqg->zT3x2KdkhLEm-mHW6q$G%aC(`7YJjU;QKcG?#8 zt`(EUp1dmqXGXq0_&SdwO}ZZH+N9;pE=W!{4X?kkTdmBi#g62>s_!`LQu9HM%8}aW z>yf5rL38aUZ`EhpM*`&PY7cYwSyFOMI{?R|9OkDm$2y;IETq!~E~KUQlJb5H=5-@t zWtBdC$|0=hacNWPYaa3|JqvME&&7BY(k9<2p+9iDNw><LmVByI&OXK)X*=wg%JW!ozPU5GubEsA z@No#)NqP8Hp#4HrZ|x@^78}ZC4uASbrXsP?doQz#U5P)hUbn!=J(Hxaqwntu)wVnD zk~ccIc<`Kj!Nm(`Oh!B*rFpIJp*<*tmJ`xNHn%De-DbR3KJ4v38CU7Z?9X4sY!lre zOZv=(uk}qJRQFtnJE1d~{>+I5?r&I?L)1V*)j2hu$)K3{0(XD&Ta?XCV#&(8+}e8B zC%AR}JT%4XPg-2KBPxwN_VEp0q^^`Xt;2f_h@gta(!icm@{>wMoa5XVl=z9crbOLUB$Or zjN^h`!~U;MM=^%iJh;+dVCNi8kH8&?3HplP@60vA&__`Fu-cz8=tB7)xU=*I?`z!08NP%?P<2Ac>k?f;J8bW1@z3ApdwhW7sdu*D8?9#z`?KXo0x_Sm${$qA z42;CsFjDUeWEImeI!yb~?}6kDb!0G`~U%L#H%-k+lM>SG#wLlI1jC`80~Upz@ZfLaA2H zjM57A98F+9r7F}HJV+6G_l%bQ0ZRU?^m~BTvU+fOfW}RWtXD5(<~1;lcOK|mL!WyM z1T7ydPB#J1l(5L9EE#(GS!pfOydg7yW>FugG8tw*kQ}`uIa9BSpwc~Z|imW)v97nAmyUX_wEqRJF&SI?iI3pbdy?2A0|43fkD=q;0wGRX@B zE0>h$b>bssv2C@LTRBFlk)xVod=B2!8+}C_GHhv+?d--H=%w=Tp9BAi4xDp zIQHPlaQ<3SAz8@xf|4l|@%B<(@&lQ+BnXrk2^ML{n4!r*06mg==$H5Q< zfP;KiE4g;DV~Ea~t3p+%;^D2N6|F4qh0}!5!X6gW=e|q4dlbP#IwzTiii$|8QXbOy zN@_+;8`pHI>5NXMAMvYO@$nDk-nGZ_-V49&{mVAtQAa{Nhw(I+?Igw-J7QeUy+?qb^?HlGjfS1s@3es1q^SvH9PP8B6ge{!6Pk` zd2-*kJ?He?3Wnc%H7h-$c{SJea#`_VvGgZi&NC$3ygdzc%UKc+GrVIKGLC13iCI1!P^Iz9e!cLixn@r2_^anV3&ocz>e@Kw68Pk}`8_yk zlz7D^x}|pN^AlQH^wJxp_>}d*&+P1+&?%h-J~!ND@})VPHnco-mE+=D4#_F{>y}H+ zf~hBB)9r%~_$jO17fK_sbUZU=o(SVjZH(je5*jMxBs^Yxc1(!&GZ$R~p`U31#x(Zm ze)e?RCK2@y4{A?cO3ktEP#Ki{IKSAk~ipL53w>HC!WyKl_6iWm!`ABCrV zf+Yd3!pF_?AKyA);pXy2>?)5zN>SC`e(rCSgI!0k=@_3ResAnd(xr=6J~3hnu(^u8 zQ7vy?@8>5aiWqVvy%g^wj`*M;$MEo6B}H%;Utnk@GN_7<9`mS@puA}}q2P%NLkSWm ztIZGi5E4A%%FK>5lspencFSyHXJzgV3;m2@2V$#JAAeDToU6)jr&heLklK`Nl9tk} zM!`YIQSf1rH+|6g^1V!gr|<369Vq(}gD9`k2$<(K)dov*KUliy!f>6M;X+a<%=`X3 zTZ_7~es#^OP3gV&B$&Ut$RrfbBm zUF}jUX_Vz{<2Pa-K?WS^J(I?(%n>FU)+%!w_(qi@Yf8&G-5SvgKS}Pa?4krM+CQIP zM@ewk!M;|}Q?ud9m-*WdKOf_{i?OM06BW{=e)dI5!l3w2^x;#tgs6pUN9gmtsk;S} zZ{6smy4J-+A{!U~gzdFTSt6e=^{*<^;h(^b+)i?4btEYf!({fnD=;iWs-^CBPQ z3BCYPi-DKk88h@1dc~1NfXCR(WZWwEjGZ#Bv}*`S$W<}R@t?gcn%8!-)!5jrB)#m6 zW6DyYLc19=T+MZQPl(&l(byMU&ZHVVmK|!wy5V(6i-I;wZTGQXH|rTe01 z-GHz;54OCZRfF*}|EnDTTtk8O99!YUY%_#(zy;k2c23&?U-rVBxJxe50@g$~&(hF# z@A4>llPt^_7Z4}?;`?l5^LQEgsk^kd9wV=pHSEYqBx3Qn=BGhv;5z-Z#82az(3C2p zt0~Yyr|6}4sk<~+t~l(*9pS~Hsp(f+j_6noLtPUw2&ouxDcI zdt_9`y%t`wcHjE;Ns&(huhnm#yWsG&UbNO@XjUfe@bvewyHZ})t1xU9XK1M=)An32 z5vXzvrz?x+VO)Ikx-M`Kd%~G@?8#;4*(zqg0w7u4PUMxE7&)?SkeslwVv#bB??7Iz zVeuY?>R_%*#Eq;T!Bs1hW^GpzTkXw;#6DP-EVDWE8jD7AyzX{A*pXy=YqWRye$0yL z^7omGY%>aZD=DliMS|KdzQ^~9qgIv&$nwYe8hPPPNwt|X6^qX28?tKj&!6Rg_-u^c z_`3~o?{tRaG;q3drII?q=3}VLaOeTy90wyup{Ur%M)MD`jY9j)zMX1Z_}<>cTmJlU z;^i-{w-w(E7VbIoVFKEBRj{DuzGkFxT}!BDP|S44RAnVEvyYF#;Ml45!TP?PB+m{{ zHOxJt1eOd-LYN+@&AIoxB4AZS$q0YD$l`kzw*dX_>Dxi9Qdw?+OJ`4(!>d^@#WxM? zb(!14$17!UdHTWIA&=Os>&ma6#xj2NHBNA-k*l`<)WsCS%=g~A`ssV;$3Y?|4TqmR zB{(U{Yt)u>?RZ3um+Z_%hU&qqv=?)3rSFXY-KwLk1ldTW@pJQlmt?LdtJV{lBI1rz@2t*ouUAt8=dmMDynqbMB|jy6UJfq&y3 zv(t&~hd@MZ;U+>F7i88NfuBU_%&=H%A$E2L2M0C>ZZ@>7DLbd2pddR37dsahE7*b+ z*9J0MV5}4LSG|=1`3^p5d+)V{R)WPDiq+a!GH?!GxERz9RFJKtZk7Ps|^77PZ%r|1&7+g@#Fv~s1*W( z%kyK{jiDGMjGY^l!;R_r+3c7@?Kc48bPt8Zpz*LUFf)V|6vGZS_$|@VQZp3=eoEWA z_+Z(sO@ECH1}lU5E&j`ZcX|C_`vzUB{9k3eyfqfa1FEVN+7_`vA!sOItJGoP?3-KQ zNyK;jHHJ8Wk#@u)P#DnQY=Fg098RWyEkcD@Z}-Vn z@KduzTO(|-PQM0cz1HfN0P*l}8ux2hSg-{S&Nls9<;xE(ltCga|3VQ>a81??JZR## z%(u{*OyXR~>JQ$LNJ355o~(|Ke`tw>;GB##)E0wSC18EeZ_RsbfWQs0I{$0%#UyYZ z3%8pHS$!^o^9$SBY@mRHgGUo=3xFCUWNnKuK{|?oUAFXFd;HUWxNaM&tF0j~_9rF+ zZy~lo-O>vzs^0Xoj?^knt0M7Z=2jcxr%DrHv;Fy+bN%f}T!RMS=4oC0@Dzz@*+Q+Y z5%5){x4zmk6>AW-0I-Kz+TocerUmNKHV{8v{($p?emwCT!@qKtqep%7f$VpPt4!cs7(q5+Hz&(ZKU8e?Z^D! z_`g}~w`j-DbZXr-fq}^u$Xh!8fWHm=AEt6${{CUL+M*OV)Wj~V$(j6xEph`mtt#CX zjDM|Jptaco!b#|7iuEVju%S0=dIkJ5ilwbBe*prS;h;9*6v!3=stN>ZX$i4M z*y5Zo1_A~?5L@t!IufUX7zo+~0ze__8n}(9|45(ywiR6E|5l5E=a)PR7pK7x+xYcQ zvuj=cHrTbQ4gazo{J}0@JHIg22pGsc7|!%Tam5xmV+k_qU*Z$+n?C<%DuL(I#Z}q* zPuT?gU5B;7sI4-yU9Wyut?>Nc3Tm_9uUn0E723qDe@0z^zvndmlnol|S}kP>#bCDK zhG*2zEgY~Z`uJhSehKYIv!8?91o5xA0<47@e+g23 z1A@fhjU2-E4?)ysAgd#8*8iXRblilmKL2-O1mJIn+F+2)E&-8Wv+`@v)PEc!0Jx|d zT&J%}%tpYoElR*m0-29dYqqRv)HLKZl_8S2>>9YbS^kbj0M)$HxMY?42%i321nrfZ}R|uz~;|lg8#zgZSgpu z|Ftu=TuX|mfPQtG`u|I2Z-Z^y_`kt7TqYJ=ef(}1jf3|Kw*4Rxf+J`XjR2T+{lDY@ z0Hy|@rlz(CQ@k|c@}8Tp!y)(Yasc%ITj%ZH=KwDCyg?&?&wKyhaR9&nB4&(6TS%{F zB{ncyMeN_5eF)@#BL8co;i4~G&J#!N`oaY;!#~=vQ4U}h6mJnQX1}p|*-&FpivF|M z41giFW%pNMZWT&A7wmDTQ+`O!Kf@m|{`im5XaAIa0&w{iF||zwYL!d~3V(`fn>7CO zt^&ZFsrp?G{1m7EPD23Pw%7k52Y}T=ge@FEVlZ}yH3mQ&kf0Msfy?`!ir;_D5a4$< zy#7=rtic?;1{7ubpKHbH`u}%#Yc}+Kt9t$yJ_Fe1el|}i?l2+57DpaFlZ-Q6KkU>h zTX4_7cpBWkS<5lwY#eyLZq*C9~2He-a3KmI!v`mZzy z_{V}Qc}-1~4L;#{0&WIu3NF_@&Q^y5K>pVrXDh3I@PDgF<5_}`-^#3iAsqWL{<>~$ z#pAzFtzT%?4_<-mUkF&b1jE{*QQ-3UFZuOPvkOoEKa_(;VvIMd)^BlZyZ`xzmYu&H z9&Fp5Ogz}n_n^bF4tgw-N-fA2FMhv%Ud{6-Yb?XO5Be$Ak;9p+f{MGX2AKL%C zfHXy7p_Y)fbMoui@wGkB&D_~az7VjP&;Rq)znC3hyM2lMcb$>8hSD~gek5`JK+qr9 z0j$?~{IfrSmgowDLWK?&tdca(`E>N!2DlQjaB=D zpL1g;EJ6yz>wmfdu%`c8ZO?|&`A3UyB&@;wm?IJ_o&o0{mnOp?oe|*vGq}AB!y>R- z^J4$2q|7S)Tgk_B0~cEV54ZtrP3Eo&_f~ZN9d4}M(t*qW<4DEe`0&$c`WMFQA5a+J zugm>@HYa!%VZh5saQBI9Ge3XJfPwj^|GUKx7@>jn??1}#HqYn!&Oajt@+Wj|ZvETl zYSoPW*uz}+|EqJjvH$wVZG=DCzttx{N;$#k0k=mhy z&Bn*gZNTluHTYtjY`km0emq;x|BGxK5fD;_+FBssU>T$xC|Q4aybk-vi%SS&O)O~C zaK&=#WuP$d`+%_^3E<{{6|hZxuFl(uod+!P#sddd&m{@9453{ zZ72p-AFe0qag!{<{^QlA#&~V`VNmd4HF#&rk0+qdT-&GyOMHba!KF1IN_QRvi2D;l zCw2*e#e!9itl%e01gixSD+DG!)SR$pU=0xNa0zZfjxXx6!=iC^91d|G+ zD8zp<{!fO(VX=BXM)5P^2Zu2sl_e z#{!Xtqro!*Xoxa6u}H9u9qt{N|Aa`|+M$pr7KnnKB@(Pwl|{at3k&fkCi9EEQoGV+& z$4$t3s@QhhoNTfRIP<+Gbz-X4AjS&dJx-u}td0t7DT-Ykc2j4(F#KTt;7=OJ06?hM~O5Iwc_0G6J0SO1sdVu*)G49pC0sk5NxUJ2mk{mI~jh36Au6F>Z zLs?m9J#do0AZBL=!i2*Fjrm|ae5~L<36V@_5<+(&-g&2oGImofNO5Rd8B&Re1W zC2vm$t4JghvN+=8qE;Aik7CpW?bgv}|ywGMPs->pEwzy!b*MuVtw zt~Xu_R=`JF+ioq16oVs7pmvs^SgjJh?d^J3e5r}hj|v4a`ogWe#IR#d05a_eGTJp8<@f*gE&>wWM~w>AV7RBTm20Z>QRHFOmN z+`Fx6cpXIM+<@Z20R`MO1~=@E(r3AN^G+=wl=qS zKFn`pWdi=4Ur1CO2`UuK67k0&L@9vYCr~EP&QDJ& zYjtxsIk1z`1#Er;eKxQI4wPnDuw7@iI1?BLe+aDV#;zaKJ}||Ph7=k@jlldQ;yvNW zNFu}_t&E+0sz!#*4<|qotC8VE%m5|Eg1|>10m+H^{}52jQ5h3JfDn?vjMkUHK-kTM z3HBdF7ru$`XF(*1%#RNfX;4n=KiZB>O4AHT0u#)S#hchVNPrSE|16Ot)c*t%1hmE_ zfB+#V=Y%AHr@*mNIk}ZETINM4G5QA|ZekeB0Og#Jgs-27i}Y9ebq4h+2#AvhN|y^fHClgz*Djq`wt^&nwFhT1qrZ{j4Is!AuORVFAPHpS;Z9J{?F2Q&IB$|Y;DZ2 zy9!JZYofLXqfuuEE}GAYV+&CMp8v*YF>-L2it;a+83_6 zXMl1}NTT&GjDShc2&Q*QgAz6h=}2Pk{~=g@o8Q25LO0;-4~+T=@AaPu<<$A{Ba!(R zO5nc=JSS`?ZvBw0z{7x$@tknf;X;9y-q~8s$!Gxea`rVD^d=IT|1Y7OJ(3_10F=d^NBugjvjvngOiu*Jz!p}2BSr=Rmk3MB-wQ5)Er0w6IvA>F z0cb^tA2Q5I4`YgB_#8rl=%Vv)xWXDf9e)k(WbutCwmAO>Z53dI{5`ZJA&ZlLxKbuT zZfD5r!WM!L$yA|*$6v(!-Z?@G`6@){m_!!4eyr~|U;ZzQ5Ikf-{qKwuaAI6Cj~W@6 zdiWe>2nYwJu$BB7TD*}62d7%b0MsynKMn~}OA@qbTRrL36Gj&Jb9ody#^f+~mF~Y6 zT40v_D&#K40Ptcr1TDEj9L!iVA`8nuPMTXHbWBPu7M2MXBMdD`$fD&R&Z&5z4vY{y zsU@G#Lh}>O43af52rUE~a^O`wq5MBFwKJdk7+a{Bs6WpKv=Ed05m{(Mfd4l90sC)f zx@>(il9*8+Yk)UN9feNKOnK%H0E7OA$MgaahF$Q-nY~&jK+ZIr!Tx&;v40`MBmoUJ z@l4QzC&~X0+g}?q0vkMA-x)x6IgtZzg5*rYdn()jro~hj({MUNvl0aK#D6miHYnGG z@SduFQCtnx=>{}=y~5+z}`lKi^*fAKHeRuhYo<}XOX=|A#AJI#}xNcG?e?f=8}mn{1D zfAR0^&TITH{>AEI=H6|HZ#}3VBXm6iKP(3C6`M|NSriC4&sZG{GOUlZ4`- z|A*}_F2KXjQ_L=*^}qOcLM*X-_Bt@2+drC|$!@96ZT z6@LDUaQr(*XrUq@5jrN3#jYQ#{^lzU4I=~(+5h+KFK#8+5)SP6>>mmZ1nUfH_5h^Z z@mYk#LIV*xCZU~T*a#y_hWamz5Ikr}@}&Py)qim-vA3Sy$2MJd;W>CB$3rk9{sCW% z{J};-|%tsis#|37HHnph6H|C0=LBy@RU zMzql)i3c|H{uO+*3A**J#F8x9eA$bvWessR|>6Z}txPRbcNvS92B7e~fij7gv3{Lgtq7?xirAkP50cX(c)#W4E}$dYjXDokF`U_tDOcSi*gXh}jA z@_z{o24@kH{AiM#Edmx9*8fVr8Zc}_W)&v*P%l{LqqwEV}=Lm;oec zi{Y&C_d*N$zq2}gG7D%i+%^NU|0nD(d`n4^T9TlJJnk8h1tvoze1HD$p(P1ffB=vE z%L6HB`JcbwcSbiFnjglZ#9Hn!Y>&?;vyGDoIrZuQbW0o|E+HH)5%r^|IaYr*ySNiO zfR*V?Dw-rYp6XYjA0nZ#68PKpJDfry=(o-&&CvN1A_o2M&?ZREbfL!PU*;Wd96k7O zL+78dZW$pqobPlrFy-BAc(ORrj-{q!XKH0^^A|~I+z8V!1xV1*xTn!l10P~Ko)h%r z!}*6Bz$Wish9m~QdMqmFXb5iy@Dcnw0HOQ;VYrhZv2zeu;v1+)%Ke*oI{@E)l09R* z?Sqd%s{Z{0g0S721Ar6v4`RV4HrTv5#lZB}AHtasd(+S#5F{~v#f<>pe(?Sq60In4 z1W7ZX!~i5c|Aa>Y*+dY)G`qrZpH~r_cDe+6{{@Tyd@i0$EKVRed3GLM>?ZZ-JZAd{ z&HwjGlqm67v4Uxk2@0?PPR4)O&k`GKenb$!+@?NDD~KN|%;@Ay_&9__5WN3xb_k-a zF#i011K>peIk8R0`6e&~Cn&%oh%K|>K1ysXf&l(k_aCSkJ6l5T1n~D)n?gh3D^(J8 zD)b;G{|0}YL^Q!SxL}{ccXo(vKMo%>l#}Zx4~{rL6*fI7niTW?*Bu;t%}EwS{B+#} zj&p?(n7r!tuW+2JYYLDFM-s1lg83F0aRmKOQm+apck3zo{*9M2jg@fXh?yUjd-=!WNHqWRkHitZewbB8Z;Yv}trdP(5N^NFd0j9mBTrfED;o#D6p=n(?QoI`IATGYbiN(_tWyQARNL zKdTX>VJhgj!~hv(1ib$hH}^kwJS#N>{|{SinC8DvbKhoEt0apf zN%cR1DuF!Wh~0nK_7hH&%{9R?Lw*Irf8_G!FmW=WTQmr6AqXS>{rfo61Pi#QI7}FS z2`*41C$I_8Fp+9NC4tL81JKn3Yzp^ zMFL>`3peM#iv%X7G_e%|a9MQRp2K*NY+)8|a9bx6yuF>y|MN2^vAxCUKeEnAD9pk| zS@@MJ%==eh8;-|}^W_h~X~6OyAsX-%5RzYnEF~BG?mo2t{U53z@cugzYM+EW0{AXY zyu^XfCeC0K(7R!1`<62JRkvlkcqAUYb4+-aV(%J<~I0g7L%mvB00STbXz~n#@TL+&VkicQ^CFFm9+!Rv<#ot7d zfSi8*frOxWs>6UU6mZ{gMj$8p5AbAiZb;4zNKpSzlgYUuIX55y0#&bh47a9<@bX{izAfls{D)?4=YpQ$BQW92N)vE|UeyRj7J?$% z^hJb(L=OuIBT)u?j^;Mz#!lwj5YEnrja{7WEv@XF5!|jq{9-%^J{trI!RLrT3L^M+ zA%q3NFYX8-@c&8(A#ouDpFG0G*x3%jcX;BfErJ71AfL7jzq7kD2ZGNHu?s-(?L+W6 zBlz?X930qX@avAA~MQE|E;s5i@7t{vH*W{c8=6s%r8hd zKq{laBm~p239TAVE-7g+_)cM-o!$N4uALyfd6WRkBU`|Q}nsoY{GqVes?T;`DN!s@Z%h|#0-o$F4bOzb4)cE}4bSxXW>zG#v^!X{ z1NxtY9a+&oC=v{PWZ@7v{%9qF85^HT=8;Dx;;+BAIs~E*QkVpovkoCJ;T*UG__yKC zG*biV|9=?nOw%Qk`JaCh?pPxr-3qfa;QS;UAe}RlOqmT+>xJuCBJaN;TcCq8G=$)$|K|T55V4Ev|Rw^f0AN|hzthTAf)8s(7A0i z9RTS^KH@G0&k|Be44VtC*Z1(WInqdopY zXd(RzS3dI}1>mbtz}(Rex21TI!Jnj-Bxp%O7JmMk2pw~U7Ja^DCS)h-FGQf7|GuFA zo&TILMOY1l57@l7ArP&<{|n^{E*-6Z;Tdg$%9l{*{M_Ud8VRm%X7TiPagNc{XKTEH@&DH<2y_1Ozf(cb`u|H61d!|OC;k?a3?qQPg%x%QE51FN zcaH$DkRpFlX(HIfS{#^M;h7{z^#fA#Twt3phcNw$=hy+x=CIA5$3TmSM*#UhXhk^b z68uw-0R8?Q{JH5$cgt)5XpRC2Ah*e5}^Z)HB2Bt`#-^&_xi3A3B~$9_^tt-h~gv^+EM~t1;n6**R>%IC)nIDm-vea z9Yiti&J;&17!gcj_7fop)jGqw11 zkfe`6|98HiB?;L{`JdM`CtSes3<{C^=W)_9-z`a#@(=US2}mtI9V9_Z60(2D|6;?J zsfJ;$rk0?k#n1o$gUlxmS$k{fwd=r97rg$rSqU#0WEYr1&lGaV2Q(20fcifO1C7}q zkRGz)jaMfC;r^ffCGGun^QEN!e_?;|mE){Ln5^~}pAM2_cYf?I>3yUBN9`|(-5W5Q z`R6httPX_V=#8 zyEWaugZoeBfD5;Ea6*Xc1kC-XMABc-Q23*eJs*w6A3xOkr}^mV=Re6AUCkU^;m&_f zi*;v*BkT+Z!I6l;i1WYyBNc$xe}?CX;nqK|Du9gs7h2e7TcMuV|2gY`iAm>NxCQke z0!qM>?-Eq}Ae^Y;nP6D-`O{f(>_kBan>NSK^5B>T&hJyTFfITF^$y@B|7eiVwx7WKe?kxj;2eU4N&gQ& z44fchHc+DL4YNfOAmi;<)5M5`VhQAbOmhwi31C&Ja zt$#fJX^WqK!P|`4LOCZSvHBm50obEW00HvUrl%h0CH4LDBtVJVyxAZ*tM_038A`m= z3XT|vBmaOT`2JmZW@x&D4h}@#0yX|lV(ZUS`||&lP-5nvDwVk1n+=p0BTA^0g0}T| z&K(X(@csup)q?#Lv0?rI<(!ZN@h7GG5befH6Usm5E@yImte}@PO}qacGbY}i;79Ce z{|Ck%lZNxpb11Rt|CcQ(e3i;W(!U*YnP{T<=2cza6tBY*rY;pa)8A3=D4 zKhBBwM}p7KV*Ys{U^es52LZF1e;x>!-TdSyV3zZfoq*ZSPi_KcJwKTVnEm|ZC14Ko zla+wE%uh}N<}^PU37Fgbcw*Di1`9dEt{$@&{$nP17iI*4`h0;iSPz&}f0RolN z`GBF{%n7~H@x!*&ou5L3Xpc(9n+53?XIx=4pkY-pHK3OfjnEBs<64+yxwkJPQDA8m zhk}UwP_)^r=<^iURc=y==J4xApY4}#+`9Nw$KhdakNp*)>?NMT{2x-o^M|%{`eg@{ z*RY$5*N0eK&aV1e-mr_i4A5M=Z|y1II13Qfdd!pSKsD{_vegv1&%E~cR?9!(*v)e! zfB!xDp8ckAner5z%sZdB3=}dwu#OP69%WKh&T6(3=X7>Z`4Z3k+!nFB{+@ut6K&xu zHocdkkd(Wn-B<58X0}yPSvuBtd35lZs8GqW4TvMYal6ku#h&8IUY5mmZIeR<8~s_J z>p3e$>3!yq+fP&EHFvPOKl6XP=tz&a)x)yJ48=|M!UYfQs#Iu{x9pD3((yeYa*AI& zT_#g9vYN?Il-1%^R&jpyJIz||mNKep2vz)wVhdICaLVe_+dm1>gg3FRH5P8BMw1F zc8v=*rk~F~<(?dR)LlJHxWl+n@k{i31LvsGu4udK-#?saTlDSe+SqPqO~a#9zQ>FN zHr(Y+tEkyhW3A8|@wEHVq9L_@?G1<1*_#GxZn&VhwD*k^U!mW%Zx7Rss!_Ln-9hgA zo)oYg`_g_OH;S?gQ1MMi?sK?hM8CK`>Rsrga4EXFeXTk6gNK>>Vu#-s?X~p^WD;uR z&gok))D!l8{6=>~Y=dmXm}%Ju%KS+0u+GjOVxFF*yZz6pCERc_ytBG?YsBrSXwPK~ z#b@?v?qpprHF~mQXZFIIylk<tIr#nWWG;Hab<2zv%bNl)qxcL z!7Hb4PIaF$Cn|Y7k}YUtd2H>HqTu&SimvK7t&|CDMxBXxWmwm>a{VH`4?53F%_L)A zJbUC;u=>;^4;%jn1tQ9T=jJX*Kf7DI zl3E*7W2kHQ{)(+RchO3f=~Bo()gO{Zg|`cSS#`D@Z?Atkc<;TT{FQC~k=wbKr_gQd z+d#ME&Qgi*<;uo6wMjs|3ca8AoBXmX=i4Oo?9THijxJVk^kgyddiADpoya4#wv+)Q zIcJuen|V`O*Y=E!H;z$A=xpi;UvZg&2|!7}v@?_sL|Rdv}CB__H7aDS_k{7DFM#MTPjqB@hPCXpZ182r(q) z^D=gJ_LxCMp-|B-ih(eU@g{Hzh3jSGm;x}R<*gv7r-xy2| zfqn{WZ7}OWPl*tawRbc#chrZd6f_V}5>OV9(-#B~LI@#{%Dv{M&fpS+#U=QWC{cu% zAovbPAu)ugm;}Fs1VT(m3|y5k!hi}PpyguX3~rNvs+FC!fFeRvKxX@P0caut8B-`~ z7SIyV*{clw#d=XIuN({*_DlCCOB1QPckirOXDt-}hBy1hPoEIV}A|W7$ zlG`IFBq4%QoJKzytC{Hsut4B1KnR867YA`biJ`y+pb(Qki}OnepR;UmRqAL2(2MT!)ybFhW=aTn9=7T#pFIPcQ($jQLzSX$AmLkZ8{3tO3&(APF_IW{J zesN&}&rhhwiQR{Esg1p(7U&?GBQTl^)QZlaA8dyJFe*(z!QFY6mNV!RgD^l^5A={h z{fj`m%tDYPPpD5tArK9$?o|Z!2)GFJ1EAk5pr>JS1kz*BQ_8kN2oz>4=udkb?M<~n z=UgBBR$dVypl$B%41EXyb-f(k-c2rjaz7y=F^ie}9c+sc0uC039ULu85c;@<3CNqf zTA7;f-6abL2GrruLZGqq5fUIZfCy+MpnnDk5m5;t@He6$(gq-hO&SF;-0V2*3vQ{h zJhVfQ;uVzpb6g=V zxF3OB=4PUwf9N_raNQi~dx+7lHzJ2tcY{2m|4lRB%Ch?BhCK2>x1UJ-*)_8E=X`R+ z13Q*`Hgy&a3^e^7@G#uY9jnd39k5D|?fvP{{sJ|#=dBLeYtF|6$MU~e8@Y4&@B#rQ zpzmzjYG$flUk<+;S^*!9DlFSK=5b@(|K|g0&-UuexyyYFtNE=v)pK2G1TNcrv41Qd zd@z6aiu=Bwdi#!d)SOo{W>(BhZ@ck}LDP=^j_9Sx7cb5pke2(MEBZB_XMI(>`Qv*R z--z$n%PGH9VCCEOoX6t>xjxIEuTxt_SuDpG#0Ml^lV99qaaQXK-CK=N-~63#IF;Sk zaP2V{7TRH;xaFgH>m?P-&`u*-+mIR>+E;4o1w4`W^L{wzUZfO}-^|Qe%h}j7WEuCE zzl!a<@$(y}LM+F|@7Q0x<)YOd|4MCWtw3d10l8ENN^?=9$2ay@1)EH*r(z;$uW;gf!Sha4B2Mo2ORAs^gFWkm`NT->usFO3SR zr^uUl{^ga2#eP}xTZNb?RAaloE>+My_hVIa_5+cJKLmidJ82x}4Jnf!J-OF%pbqs~ z;Y?ijhBe;#GRhnU7v=jDO77aeJr|^}sJ`?5_D7F8<)1c)N?H0%mz%8~9Z8FG1D|9i0Z#d+)9$7s8l1)WBf>q5VHbPh^n zFIc^(ML#(5NYOfg;k@xp`oJfvdM+EeJUa8Bw3Q><{S~`pfPeDs^!r|&#nC%X^jaQa z6inZL@I$wS&etu)?Au#XesJt?piEIO5OxqV3_6@{+vCy6&|gmxPpz;ugxj@a$i?D8 zm%#_8eQ95sM;&_eRBX=Qp>9{dPFZ*L8PdDOLe#BN`EqWU>R0y_T+*TIj6-=YtKD>H zHwx?0^}x7=pbLi;c~DuP-9kv*u+;&3D1--phN>)}EUL*VDyi^772V0}l+ZE0g%d&) z^GR8ZkH{w^C@PBJ6A=VonI$SHE{+gEg5QA-n2eJtqQLQt0S6^>D+^0!P}PYb zKvjbIBcG52I2z{72{LvTHs&CW0$R@Iwz>!j@MC%t&xnc$VdjKNZ6cuU5Q8+xUYLCV ze+9X(6Z)AzQUaLFN{E0@6Ve{X_ZsXj*vWA0iBY-~Q}vlE)~EK|Tyn%jNLD&c;h0)y z)~~dO)Zx@`fzB^CH7}STHiSlezIN?J{o%}@=C^G^Ds~c&O-v8)3YJ|qf3@sX?b$5Z zcHOg#=C4uoY3YhKKt;YFZ;vdm7{}3=qFZ4f@1z8>cPI6w7QR0Dm9EapB+LDTzwTfd zU--@8v+O~?&t<26;&J0>2dxQRiX+2;$1%2-D(D6CnxZPtY~~Zu`u%y`Dx(pZ+HF$Y zeCq@TV%~!vE_I?B1M9; zC@cBrN>8z;`>*;GE=DHU=$iX63>Q%=-*s&HNjak1R*rneS%1_*ca6WA0oA>mQXG+* z3%2`x@=3WAa~W{|qSH>##wjy?weQ>@x9xW)jpO3iRV%%l*&YpWU*GK8W91kw;2z>? zD|_ww_fy0BHSWECYqx>*bDED>s^}Q&PTM0!%pDGnqvMSgp80tT*I3jypgA;rw1a{g zfb%BIIH0WpLO{U|%rAkjqyTxw3}h}VXAkhDvI4T;n`qI-l^EY**v-rsJj?{LxD(p_ z5W~11AfsyA>)2VL^HxG)0Gf@aaMtvI3Nzb8;DB$sNFWD#8YXuj?_z=;fqe)lyEfq% zfc8+!!wHlHWxK=nXb%85UIY|NJd-Ux=`g@hnT5#~ zM@kSBI@?e{b)^Mpv2DV%G_<<_)yu}iDqSBgk9n^NkMLU}bFVtKLMz;6@&}C9MA{%*%QpW{Y)GNx~NEFy^Y|8QfirCYvfY8aA673UAC(6OmNt+!TM! zu)N^DN)+|)C97K$99hl;M@hVj_0$9|T)Zaznv(I1Dwi*3u$*m1?TI%g7mwRzes|I5 zx0DDe>NN^6<9RGnb=~rinyY^M*VC0}UO9%J<3DTot>K&QmyHqBMFHV8$Aw#5&aP7* zeA4AMh~%#*{d6!2(c8l;tM5{%>{GSEOy(!$=sLBqm5rQoV+*LVc;aJ|0t3`P1@i)3 z&Uq%+LIy-WCh#}gDild<5cCVu+EmHg+O1=h&?j)8zSE-l(CuwiJbrmyCshqt`z?eE zHb#V3(ch+}8+_Md{xkFF#`rm1u6Zr&w4R*^nC;I#$v}{R?P0RV8-XB*-Y5YqRdV!T|N4C07!=#KW$= zE>B$bm1nEl*8BYT_@me|t+So>?lyh##EgoyH1kulO>gY!glAeWE$Y=hGspbv7i}#F z&Hr7q()3frw~g-0Mc5k4R~${FWYrFCpl=glY6&?h`o$@gGqX_r&dGBYx11GT-KkmY z?$QAa2jpBY*&%m=FNL;%&D|a7TWZ-pvb@e=wCd}j@n0-EX^R>t8*5^><#yZ2Ey#QP zmaQ+?y}>TY=TWc2f70g7`1e%8CRjkzx$un;fy+}by^CU@cYpiq{*UzNleDe2`oL`N%&yFXcOWeik| z8|_N~7Rc(lJjNJ*@U`2M#&IU&ucoabqojy2{u3=V#7sY!;79O@g75Yg5(Q&mA;_9f zT7DrRFp>gIzk?D$$n*f9N*+j>J}HQJmDExY4@AmOMk>4weXj&%5krElIpRK)-{cD2-5Tk*0-%s+8y(a z)ko;&yY23w+xI@S)nOdJOf$l7&BgiQfxAPNv9jPoemxhtjxxtgc55k=xt|Azh{7uB zF8b43c|T|0+t|=4x(nd@w)FIl-!#D8z=i-6^&ZC4PlZHGU#VwV1=rVjLr zXGoZq&ohB9K>wX%YGoSR*X{kbuEqlEc)e z)e{SqWvX>C7Ju89zqa@UnU#>e2kjc>2_gU~Ib>Qe=T1*`DUc|sOnHcy8<^d#>0ziQ|9V8O` z6}WZ-@H^;N=#fz<02&1SYn(%Zgz~hYV*ib*SXUGEJ*TZSSW-zWz2Pwfw7oD zX5oGYCQ(E$XYbZUd#RS6_1Vb~wdZKk{c3Cb&JP0~byBq}YaX1+?HudQbJOVA^xN^h zp;vO<(CB+Z%Jag)vF_Wym{`0R<>$|OVOyl~Vl+59Bu$5RU)u$h#qE!MM&t^LmaSxY ze{OWixz*{^JpHEZK#t|AoGY(|6^|BYX5TCxp)OW@Vo=p-qy#*8!yP?#CDuLhi_qh| zsPlEmu}jCU0=0tzrv;+d3B)wNjCFr!(kh@$|LfAw=3m?F%YiQ^a(}Dz>Ri`9VVvdJ zbUTPwH}zm{nN5y7f7sxe2+D$ZnYV|4hrpGG5pT{Pi@8*`=+*NaK$VrVpJwHHBy~6| z^NW|?PMBowpkVVmoO6@$;U_7;KAlt5Cdnv&3eW>e3#GdlVpc*_^#)#j>QXS#+f`!1om8cxWUfKK`Hy-)*69L zHOgc5bmQks`{VWsr2o=P2jn{h&c{DDmyOulxtHd?_mH=6oYW=+-_6TfyxdM@J_nSO zW8d$wzLlEv>mA*Z&G!SV)!Jk3a$A?3uGaXp*Y(m47jMUS-*+-){*DO;x%X`e)=1;} zAbccZq07dMduQA(gzvEaa+miQb>6vgvA8P5d_^8b0ZXk%j{W+X=9%LXC4fnAN!0Z> zQqH5vZ1&f#Mv4!-udQo7E-2vfh0j#K@n`X1`_l&k)Pp`qADa9|w=5@V%0pLl&e8CC z=!v~Nbov_I(gWXYne$L1sX7{&V!) zYtPJ*wG4NT$sXr0*DSlTM)~Z)wQGSu-HwL5`Z1==NP|TWL!(&UQ1Wa>P`>H;WE#F} znCp?(RjS@~t^BC#_8(jmT5dMU@QdF0$)qEb92=YKo#&k_yP@PYt-55g4u|a$gZBp2 z2Xzf~4X=HsF?Fa|+>emSrVIb+FerF_v?29af7}nXm!4^T>7P$;>95>G+r*6gjRbyI zS=^3cXiRHU`(z&zy0X<~E=76E zL{b?9qxH~Ki6pWXxsC$9~Iu= zaBqX<=fi_c>AOGDuQ{vXFTHks&WGP`P&TcCbzO+sUO`T5`O47-FLK9JnCp61|P ziMnIWA&QZ=H*L@<4p#_!Ze8n`{FJVZqy1RL{@p`4%^g05;`BxSOS>f~BPtLSsYwff zPwQAps)X$TluB3b(tEx&}%MD~zTbXvbdOS#Y!VEuyoJCA!RC@Lt*sax|% ziYTwA><*+-Tdvl%yJv|$gU*uvE$gcI;;))t?M*D}WfyH78rox2x3h>LrljfYZYd>( zWo}1r)A4e-S6Y9ni>Rf{WypSQ*lCdLOF9-D!HCYPX26}JxGnE%2 ze3pOw`sjzB%9n4_z!mRb&gbLB66ghewuuFvka4H0F`=tvJ-+xF>)OEa2k+L(WM&GqJ$-$Pb?tNli$;kn?1{nhcC8SRsziNDp@=Sux~rO^boWAo z+TIbQheGZ4dMUuRcTF1Q@Q&neDO0x&YtdK$V07SPQCcmw>RppIz)`L}-bPvfI?-0# zw7&MCMW4D|v}If^EoI(#fyYmJng^#w!%WATIt}{w+1wgu77_XRej%lq`rwk`C~eCQ zfwq{IVUm|Gwxrbo8+T?09Dl~NEb7;?6~3Ii&r>Ma(^hZHSlvi`f!Czj;eK_Lm+tD6VRql)Qb$?Zm?RvIkY?SA^fnQ{VwZqLD ziMLW8rifU3CkX{6rAuTP9%!#@(6&?vxPl68*qt2^X*EK1XRWy6P^2Vqq`>pc&2Rl@ z1ih`hWp8ZWb=+<1s!JDX6^k9#ee&MKlGJU`T=aYO2kH~5l?l;)nQxd3R_|%5;@Seo$zP|+ler_NmukS7eIWkOFL}y`cfZ{5-N#_-zYkb+ zEy8o%+l4z8bsQi48SY(YVI(~y_FEt~X2pAHQ*~XoPR-AX`uoqkUhrCM!7IC{tlw1P z8XZe>PwKa2aD^xZi)j5k#{AKEfdHG3vGg&U>R`ZR$J!0cO<6g)Pq2J8s*9{!s}Hq|+DP z`Tj^x&vhErTwnVbz66OiI>D8(TRyO^PCL2SxcrR4=S6<$DCHHKHzxj)Y^6n~^ z=M)QRfu~`Q$2^^z>Dm#l=Nu4ghEGt(DRo)M-EN~W)k_wAI9e#`bN_(1bY#}21H&rbbV<;bt_-bMi?rM}Lwdg}kj{{@GPI0ykA*F(@h!%}C!z`h0 zk#sLW^8QzeRGoJQjbytR0bAx#U=~ScK2;UCM;#Ohw%>G?jgyQWAQPbEfoe zF4HcI{NOUuotyG=uX~4RFg%P!M!Kqs# zJeM@DAB?+=Xl+^3TwHW9T_HT?`EK)6#v%W<(hD1Sa5yRNvz8Gaq{as$JOq>YcirW+l9z4`Il zLIY<{;{#IP@@cORwVMTq$2$z{{BZPjaYSQ-kn`8jzK0cop^a}S&8cFqnew!nF;~C7 zU%4okvb!azu=B~c&n1phc4os@o`v)7d>BRden3b-uSD%ojN36&!17J+an|w@>(%v} zDp5v@ADA_ZSF0a;oY<(A!?)(ywZ2=i5r>KkU22|>IP7u#*rXS(5kb2=U*h1m;Bs2K z)8?LuC2xX4e>iPSb}K0r)w?alC(TS5ATL3Ar(ODUoCSlsy(QQ2sOKl1n;Ctkc|l8; zLldZ|-=X@rw9<*|b+#BCK%smHxFLFYXX(Pbg&Q^~?YuN5U%~?Tup|tM>%5|o0hIT7 z?c27N{><52_bPx)rm>H^JJx96K0w!Q@1f8{&9O|RKOTRC`LM{+w-{o89vKnlrL5Ux!V zP4PNinKD5=%t|F@$q^0hk}%$FBfAxRlY%d*=RLnCluY3EqRJxGd7R-rm}(*QVjxA{ac%6UnXwQ3I+Vr4(bx)d3D>T;{wuZNOD!Qs%ToJZ5%!8vvdqtT|3jg<${FYis zzjcqAFEWZH^L|ZwP`!~;Z^bvN6o(|u+73a$z|&+tKv=xqIJ-3d5+`z#^G1!k1{9&5!!6#_dc(9& zFXqVyq9~|MPhB|F6(o^!I{u<_xN`FD4oRugmp%e4TN~TH$JNNv$$Q<*+Dy;>_@x7_yp`}b5f^inTf>l;n?O1693_l%V{OK&<*@*`bpWd=9s zXD+l1Ztm~+daj_2wsK3$Ez_Fl?gQ-I-MsIkN`b6(9Q!I*`dCG%;(nXbD(z6O@o(pN zj#$=n%+lMQ`pBKK&8MG?#xSwOyj=G7Vf*(h=C)tFZ0S5Ho<9!u4>8p;ky#q~=-Bm; z+v~;-{$4qJvHR`$(${@zr=O@~sD1qAmgu4CE@XZE%(XzLKnsD470dRoc@k!~ef7B( zgUFgll@w80k992Lf}OHDR~17qtFRZ_kJJBtR?In(hMi3fcAuP@o}M};4Lkj)D6x#J zAeeyR6BPw};Gnh(At59zBMY|1K|iBYvJ)9usQij6#)=3^;Adp{goMGY*Yt8Mn43Wt zi?QWcbmnAAPL@D979iLpjzkhJ#|B+8u&)+mI^EgJ`^8-614|g@Wut@#v*=g+f`V$M%d|wMwxnahZ zH8eo8`4@xd_pCY89!c9qf3!Ae)<5#eA?s?YYQ>jN^O4*0y0xgmG%>LR46_KGA% z4Vl+BBrKO5@NT}b$?_|YjP%J9?*lfL$~bU-<(6^t@1SH=7FXGOp@;Lv0h#+dqh+I^ zf8G>J=@;p|ODD=s^{49 z)8y7w+`fD8GBDEAF@BuY_Hw?x+d{pF@D;8LSB456r25Me@=Rh!Ft-e^B?Gu(GT00; zwMzl)iy~BrAtARs6J5A8GXS3T1iLxF+%m4J41PbKMP)FziY+RG3IhPsuwdd=1fAan zQ?p=V7zw7Gr~Cu6&QMYr`XrcNhH}JWQ0p7`ZzA&yWq46wIu|7*g#9(tUM2#57AXW} zqQSp`LxY)P39x1l{s<+Np@#sdy%GEwN>oGNP7Z*CQp8}p9hg2w&jYq^f_Y%D(F6%* zghjyuHTo-93>N_#Qlammf5M_rHw)OG41H7r{iC=L67y#$0X^|Cu-6T&nuE(02f)Qb zs4cweci=>X|q}b}XzB@7;cwQyutmW~r-S_LM z6>e!1@_uYMwI$h{`2e$@>w3xbmg|;Y_8*4Esjbb+Uq*cDeKnSsJHRR2@LkDF_|uD{ z#w#j-uSfH=w8Bpp6}}d`x_H-+-&J9)woVR(-0w@bB!5>fTqvyh6VduDHfQ56-O?vo z5}sp*_9C8J^mnlFGA~|!v8gW$_~P`-nNo|^?7YB^y&g5Y@^omqSZgeI{(5B;-8T~6 zZrHv4$NKLpQL55M%<8RA#3=?-z0(jqpju3mDV1*nl!=;~>j}Rc8}fBVrMfOtCznkAN9ZwP3gxqDXtWl*Az2f?f>t@0S`YP6z-coeB{pGbrs#sz|YbD?1w)BMy zFW35wB}Dlxw6Y17K=c-II(Z+zRu}*A7UhYGi^eOo8aIycz1TV=sue7?sY+OSV`ZB? zg-uym`Th82pzz8^*WxE9#|1jM60WR@S4@as$G7%!lke!Qv(!g7*~!b*`tKZC!B!D^ z(u|>->s0BU2D#HKjT&~1lqxYQDP9!fyz*E^_PIQx$}=T#e?;hWGe%_^&H(w)HNs(q zA>zV8s++qxPrgL>n?osjnDo2db=JnIs}iNu8>2F!#=gF5 zX#4uO=%E@?J?v(yL$}lZZyEVbQM(^_20S{an`E^itdSx^w{4^SuOwgXEl+54vjZHT zO5LO9e&@WZ-0l*y2@v*}!Gc<^O6ks}*049+vT-z-dw5H>c2neM1wQigPYJrWPos{; zuBWf!j^e`g#n-%7M&6a|}As1pNwW1R&zd3e@JjwRD09BX)??>CK`7*=|;JLs^bt#H{x>*CkVaP=}%!3SSK!C~ui zw(JFMszn~(Kkyd|gnixQ41@&Q_pMpdnz+?F-MNsnwQRVOvi^+uf_L;ri*`OidG}wu zaifYo{GuF16ouFCpuU)%-)s3r8*Y@?@wp~S=O1znFgnl`-zEQBxoRvxY`xVere_g+a#7LIf(JZq-X;6i-#f(Mx$A@m#ShWXTxpM=I0Lk=72iuY zi$3`n6?V>YTgkcf>r1LBK0SGx^|AZ)W@KJDU1QCba^o|<4eqY4zqU>?*sI{)l~Xknt;##hyMUE&Fn;rIMQ>cr1(V2W6*KCDT%v z@ObWBE39P8?!LctkX`%aNy$Npwh^G-;IZQ4ea>GwYnv*<>1s4|4tVbIUu^q%QHlaG z@yFQMNh^m#Z!-3B7Rrr(bj?pjr4AnQcj!N7MP)N=|v7R&dðqoP)Ud z>!7{zqg2()N|}T5LyexV?f8;S*ZjN`vbFvsJE!ms$FuTdRk>;$8GVh6`+q(@oA?D_ z54q>6eUH;6p-AOz&MzaCkj`K#zIMJ<#`ZVmJI?7S`=A9K_w^@TJr`IQJ0trZaoj7wXW19=i!G#5{^8r@iubl0Y^D#`kybY#T} zceMfA?A$v)5cIl!iiM`E-uHLz4i{Ow%Deo!3-#RtYHr(pX&)(kIj}0#h-N$OSRcj&STm^bvJ-=S3?$sh*hOdk;jhHH)%Tec(8;bd%wJ4Uz`{nIvmI? zTP3UTh($ro&1RLnr&mi%dVGGA<@{0&sTB)9QRS}_<+<89Fd2Bj zrQV*fxlmV~Z`s(xixmBV$Wm6B?pm(c?M&5M-&4D-Az5FzSxU30|KVA-MZiYUC#oC*+mru%VTD=M|GM7h^G(QZl^>?L5lPQ2zjXOIeYX=A0(pv=r_@6D!&K_xKD=WO6c7C&vZtxBf?ud}})Y*$E z2bi^2vkJG1*sGrwRoY*2#Ng3UyZr5n}>2x zh|T;qVm^4WMALcCruMj-O)n+QH7^A^c7O#4uOB6O>u+oAYXM?aNA3nnlr#5M=mfj$ z=v~^{XnjM@x!%dTR!gKV`i}U+M;x@xPvhh7E?=`amHk|bsgFqJ(4HIF$N`Gp^XJ4Y zturhtj7osix`N?w?!)%GhgtJ{UnKS)JY)a$?oHIi2Vgh&$;`jCAvphvV=nvw0sg}Nl)DtcQwq-eCot!1VW zf>AQvT-T(cst-jNtWn6bjP$Sckkl@#$IEhB*3zJjHtEcx=fE2_~^ya$d9|S>6-3nylxVER=%EDlOi+gw20== z>7dwxB#la5@q=#6M_i*mmz*^kHeJZw%=P-E$O38O-$iGhw5M3VAEo&I6#y2itqfgq zTX~)0aOGIZnYhRf8trh_A)gOhBmHF0ufAg$nca8FDd4d1z2lbj`XS+_2OlmU^!F%v zv(zh`EjO-)+T1!ox2v^&WZ60KU;3x+vKoBTixN2KL!DFJVd3!fw!YW-4OcV4gGb8- zH@w((z*+BwV}F#_Rs%N_y%~>TY6!qt@TSw!zEf`Cj<iAlhtg@O7q$_4`pmgW6+5k54}8S{wShT02TIOx^0~7_EDTOn;*Fx53Q8j0=E4 zM248ZTF3g;N)(zOQa1}zR6VQMc=P^t9mi!a+VpIfbFNa8&bR0GK2+4?hrF{cBF?w7 z+|>Ts6?tUsva5XOUIYi6e=booV){djuUal`vs(+t7q`VV_dlwiZev}{Ubsf;L*pjF zYGih(^S&mFX8{*Eo8_Gmvk`Iad9jTckG^Q)Y=f zGdGI2zULRTzN>S4V_>h@z6zP(4dU|RI;R9Otz%i-yIkGrt~&A&)e>5 zR+a~#So%|N>EV|zqRXVmYJMZfuUCybjg4aah*__19|He?l-O-MW?Jxb~A zwXxlstZx2fRUYcnytQiIp(}6l%-gc-=?&S08b&*Tj=E2t4@Nzf8#~3djTmk5&*zVc z|L$?4j+>Tchwx_St<+AvS>~IzOVX}Ls|#GN;UU}fXw8+&;g2s!s~gzTe;V3f+amDx zsgYv)+ejZfRZk7S?V(2%By+CoU#+e#ORammBm9*~v}9c5&f!giT5Rori8$4x$~`)c zX+DXi-}yNJm&lh9!Tj7_FHXokH&ZwZ=BzjETAaeTF7>)qdd7=0{hNc4ORF8Opm?`M z?TUUR_#j6}C+PJPe>WDhGu21kb4FN>`WzBXRX^z=_>e#5he4iQP($y4O1LcD<7*E+ z*$ZWl?N>qO>|tWsy559uPZaV*ZV$&WyO$?-76@lko}oYIy+DDm-TcUVWr;%R*_CHvl&TUD5>z?g4#!SvhU1-kqrz!mX&L4 zVpUe@yrvzx%2J0T!~k?K&MW#}C68O{?rk(ip2sz^A5ioLB;m)d)E6(YMw|#pMe5j+YS7v`cyXEO(t($7w zmj&{!byOI-A-`^kh=+?1HL|l9)yDGq+xvjy-L!3Yek-)`8Zzi53bfyeUm2}H9qYC) zE-$dMc1M@ASN{=u+0vYvOkj=vd;X%5^vGU{VopyhxdgwePmjTC-w#t|skZSRc04ki zx>mq{R7Y-Enn20Qk7w?j($_ehxi<4R-Dnd$!T7fa3VmlGYF+-b>3|7km3UiuUkJu zyL%bdsd33ip5kp6KG0$zr)UV={Cwn-!TP2ykHc=sw8ke_(j3&TzUN|ih3mntl|cr} zon?!64~Y4nt1^1equx%x^Fhq`mX6*3A8l^|mQ~ZW4NHrp(jXxq-QC@dbV#RkBXt2v zNp~Zi64DKl(jn4~goJd1{QJ7pTkrdM?)Q8D|3AJ%_h!$YnKf%>U$bM)xz5$S-;5Z* zq7nQoK^}TY_!uN4{fJ6l2)Rp$nhMSV9RV@wp1iQiC=0rzyo7LJ->6^C15tzs8f3zH z*enUBPQI8tsvDvkc#H6Pbjx)meX=Gc0wP7kk;=roFD*s>A^Nr0qsNMV zy1r4NVhoQPsJ?U<<{FS2{EwUeShJ#Z2Q%Nyiu74x`_krhIxU0z8iQ{A$%^{rwW_ug zh;c$~PccBzJ?n&#Q*nP_i?I>QDDkhU80(?&0Q7&fb^b5Cdj55K{x@4JU}O7<^9B9s z*aMj?|Cm4jpK-qbs?c9XSnw?d1UAA#?n6Jpz7RX<|1GR9fWQA`gazA4*_Z)jGq3~h zOu!BR%>wzXz~T@1FFV+V`oG-3Za$5`e1}0Q!Z90EU1QF#K`?6b8U4umzZzi3z+!1vs|@rcuC?05}AT1!Iue zf$HGDtN=h6qA!qU0gSkSIrc~CA4x7?ISO3#SCL;>U=U#6WnWleGlEz>@-&Qk7V5WIXB@QH4rpU(nKg)-J8e z4h|+UM2})Nyq_fXs3Q!@T*Hb`2r9YjKIz9?mOgZGdm-U`n!;{uox;zP>T}*(J&Sgd z``)el*A63*PG?SM01~)m(qqbFWQCc60vQz9n@ZwmO)+R* zdi#2bB~?Tc8_G;2oY7+_pP96Hv!$_>vMu~Ojt!Fa=JfEb#b;!KCT=I3;mTBF&M7lc zfJlz&>k=FC7pA$6R`nFRHdbVaIgCbe#rFxWRQpIx{az$bP-s7iFS2GcWJ#?7kS(d~ zG66k3)O?!xN(0trLzVB92TV8&@rP=~tIVykMQ!4XM`xZmhkLkw*%9BKk!aAe;^2`r z<1W=P-QP8{D+&2L{hCUR<5@BV6W=o%kQJLI0Gp|n$e+#3-NwA@y|`nk6DdZHDUI#N zOtF`LEL!d`=)NJ}la|~rXF|b@9hUSm)DTa0gLu$n<(5y36FiA26Csi<|!o=Um-*B~w z!+x!NaHpAI>d=qih9Du0dME(Itg>?2wqF4gY>|zOe~>6%8;6Lvp(($+ay0#m4$g!u`|Y-3zvb$k#<6GFD$}=$Oh3 zxu+OKkDMH1o+M{fawvvd4OhX7NMl69L>W}!NxP5Buu0tcjgGADCF z%+iq$!OEQ(_fcu-sFzkMR2|+$n)O8b?bVh>(DFT#0-_KY!rX7u@xC94L5%3TV|#OO zo=*g9BvBs+RFefokR6+jffnZ1Z5IO`rg04R&g6tnE8SO&@1;LOqd<Meo$Zv78B1I!(jOixbU1=&UJDEFWP{B2#Zj{d^e2g^ z2ZW40@6%>m?K>oB^OYy|%3mc%o}*O|e>kP}KN^p{>>VGAbc2)pH2!IPoFMgN$l%`1 zWSHa^`Xp$p`7d3t?A#An2&!nRPA|>hMC$t@*nh&q7Ey96l*z!&k(H4b_wmtx_awoD z^xXzF%sd$p$uwfhE$<2_-pXDWCt|z09VDptz3BqaC*sbeX(YZzh;gD<6&?o;=hD>U z72&(+O9s)J2>**xa~5{yC$(JG80cl9|s7wB{*rE}%E~eUU#t zRZw2au?O29h5?o&R8oj3vZ*J=XO;X4Zlob1+25+1-fq9=?TNP6^}>zM<15`8e^IXB zfc7|an_P}(8OsPI?JeZ*2;q1hq)fU73ce<==WZ)&ENg7PH?`uin-|p>#q-uboFrg$ zFB<q9FWK8xy#iNvHNkQ+l%kOQ*`YqKTA#nIP7S1~eK)HmNXpMJ1OW`ibBa-!=DsJCZ3Iy;y$QZxGd62u6S0tQ9rVk z?$P6|^1XTlHG7I$A(`8#vUT|V!-@qJfwJW*O*-@##lA|$V4Ntb6V?0fG7oF({Vz{D zaXj%;lt-om87tbhm@vIwUfewu8X5_OKV1AE9@(wP)G|GJGbwQapORg2I{+=~73t}V zU>$-MhtOZ+w~PoH(BZ6m5q)v)BGW5>KwY19P!v5Wm1;a^VcU%A^1Vb!(D8)>mB=dP zQ+Ir%+1n7$XJ@(AiV@+F;TqrW!p{ zvWG5^t0UD{-B!wniPL|mEzag|kw-971aqin97A=mJJ)#Vndvpfb0UbxuVs6m z)Cbiq!=t0Kdc_hxx$d*I=#_;F;BN@bF-PmkrDg4idI@4iUiMzl{M_q#lr}PiXCO_r z7)0zA+pF1)c-@1^IOP~}{;{d!4l}^KQJ}e*0PWgd(320Bl?NNZ`8h3Sf_4Z zgKj$s4x2_6w>5eg_k?PIhA-BWqK>U4O+B(?jNXT8Q_{~VS*4bYYkbQVb>VUG8I<|K z5K3XphNV+XrJLFQJE?}4;4*S`@(prq33J;3^!w7PRBUnKTP79v6L3mxg&nHkU|P{8 z(29^i6_%|D*CmlbIfI5W!cBk5B$~SzwyVp>JB$61$Tw$ zfQVO|vT72lKc9lZ`XCZs$RL?a84ny=s%t6}-byZKmrjki&cZ*+jRK>n7TGug(s zr>jRQZiX@nv&me#9g4&9=Jw_`d%?m*&#j}>Om+zSRX*Wt7JaW$pzA3TUv7-1#z9cU z+BWMp>l|$sHW8<5_-Iu9c4MBltt00m5y)6sAj}Op6ob2U{h%<1un7Gs(StbA7q(U` zz+L=;!hKv~D`rn`JG=`eZ}IaswuN6$+V%rBLP5ilEH*vjh~R?zY;(r!h$*nAs3>aG zf@RcWaa&|i^18yWbT2$34yr!fb#=E=Gy9}S{U_CMrf7j6}9Wwx#ECxklA$9>m3j)qk2LTA-6MT0YL4EQCG*TY5xsf6N#U_(=IkpS(tei41(JL~>{sI9)hllXJ7#2%%7-WWJiuILs;NAik#)^fF!uQ;x7~DaD^#$V9r4@$v zZG5*gwcFevFXyenWb)q* z6MiTm40rco>H-;Xvu#UpexP>7sU29IdpNgWv2>qZ#fuonfo@JKSJz6O3J$brUR`?P zK6aR`l57sOz~FW2gUcEG{eZ*EWF0nxrl_v=gJK)D?gw5Tu6xhZ6}iWff|RT4a&y*d z$iC-v_D=6SD1!F*czV|~hFl`QoD&b9$w9++q|8brqBB6jnd_jP>S$Wu_4K}z{z3p~HH+Qt4iwitXzZNp>R?r%UAma9&*P!wJ0r7*~;TnC^(SX)Q zqMM3X4S`@nq9cc>i0*ui9T2E8u=2|)760sY4au4N{Mc*J)Ch#fYLDj=65>$cl$4W< ztE;FRds%O}ZfQ!Ab^43)HJZ4$BKQ$$(ox<(d_27MLv*adi+Q(L0+lNWyUmkQN| zEL&x2IMrrT8RDzc%aUmW){c7BA80?$Y`f??XH{<&Km8u%l#E~{6)e=mV0o-?IlS5x zxHV@%kNW&RK}e`j4VyBkQYAy10Hk({GW%L%5bkCrX@IOg00#Dwla&B9kw`T!!1OY! z$wUxtxE^H3*L{Q+W&b#c-vKsHb^jr6Ki+OW%-NHNY~# zRzjv^bv)I2sa3!t8+D2KXnIHuMW-4iQKd(!t%<|&ZF9C~mNAYNmBQ5K2HByMr(_?x zfdI{2M{=$E`jz}Q!=sCP+L;1d$36gEcPaL>jADP2NbvgydTVMi6o*6wmb=6TIN zEg9pQWgLQ{dW_g^=()ae6L|YO_4Vki7!*ah`%qV=yrd#`sGX^QTy?mxR!+ooar&Xt zZD_ES^>$+`%h<-u?mODOc^;-8U}*qM_uIVv+YSG}Tkut7)Wy}re_QZ>1~Bk@z{Qp2n|E8=0Z1#UqRsnd#zZ>xXld_8YzhI>Qf=Gk@B+_R8 z6X5qZ1HQi1st{)P(lNCe@$0iEE-{xF^rRN5_F1*6b-PxBx(gLNh|(s9bNeO{mWGyf z28WyT+)l$S$;hoO)}6`J=a_F0hV_c58C?_%uj14`7*tRl4n$vmbcuF;&8)B+~yHj6a%f6_gymwXq zS^6Q#n`G*vGl}mYAJ@`t5Qkm4yzFKTf7}O?j=*x;_m(>)Zr;SmZ>>aK3?m~1&$nLG zCKmH3Zw4J2@R$aoD$Jf*#HNR!G+kJ&sfV6jsSyjD?UrG^pLT-nd}&!&RGGFtNJO1f zPjNt<`fh%Sjl}A^*5-7L;JxR}iPR{4nJ`Gqh9R*LV1U;VlkP7O_E1|&qc(5#DPNRb=$Zw$qjJaa% zvDf%Qj*Zet97K@i5#{~79$dn*P??~A%#0EiQV~`GZ>!GXpPs6+sgnQNc(TS25#_x< ziBYogJXry0ch<-WJE5DEFzbZOyckqBs>QET+e(%g!K|eGSjIF2`$_d<6v-=*Kv+CX z7V?jo%RGKoa8ro7wDk(WVInut$(++1|Ixypcbnm5UfIIwk;eQ|*0(dvgvBLjH7%E> zo8C5oD-A8lk&HzH7|JQgrxByy?U$%QNUYZkF`~w&( zfFpoM69nFY4b>1UJ^*!Q1+dF3z~6rodHhdlaCz_s1|b7t!Q2~vZD2Sw3t(&q7ADyN zml5!Jupu8@9Bh0C_@u$+^Z%swfKYt=!Rx^RSl7XoxdC(hU%~8g=J*y~*695z&X4(wM>0 zY(3Df)(@4LQG=xrzP2E8osg1r*J9>Sk%|jFRJI%+@CEoowni~k(Usn3Qf~|m3C#1) zU++Q1UbikeEp0X&_hzte?y8;9wAD=Fp|ZQsbg$TN;3w(qGS%rBZQ)ni)P>}L?Ajlc znl}3QEWR;s(nL9) zMX7Gq`_G)CBU7wGoowwRy>@NppiJ|2M-&w_N3t%()l%Wsr)2_V5m03e_`#U%11%sNjIobo~c+l<{*{au_ z^d;!nx+L^%><$Bt#*)xTJJTC4*aOh9j2Z4+$kym(R*d=;TB=dDPemGEq!a6OJ9PrNi!{aH!j=L!55^feLV`I9OH zh2e1wHQ}=gdzmMN=@XF?3^E|OuPX2pw*67FqIk-l)W-dCD4_UtH>igoX$ei?uV_mQz zsD8XCv_Pi7Xc14Kb82-Ci4D%@#F^){ZF)~~t+n=c_1q_qxP0nT4Hl3Jjfw~Jf~80F zk;Hl|>&DTd`NzwkkUQ0R+J6(wQ;)WXL%jIT$>zQdbR(_|h(`G?C7SiYrYYAu$FZCmL#5n7&?Rd>+z$;YZA9< z2A-9sPuM4UJ%qXJYnZ`NO#3)U)yKHVu<&xKonw?^7W>Rsg?&CRfa~b169Yb8sHv-O zB-%c~AaiHQy*cPJ+fITpx_4r3N1Y5ra{SIdw<8G_ge0s&Swj}o&qVqkIAY$U=|o~A zD}2w}eK(-R?Va=40z^+jD6|M~L6#8^w?%mKVD4=MCc(laO~(@SB<$lCVTndh-x(Za zSUV`c)Hl7;o$zh#sKQlNM@)emsew{Ag;AV94~~B+q8m8)%|1pMo)a&bnQ9Gnco{?6 zefaVq?OOQs(#fW^q9|eDO)p(iR~6-m5NU4rQ*i*h&`lhXdk6}>oBha(?@31#AtLrn z7oS{oJo)W2OT(zJ7Rfa3V1uZE+3W&j5`{0BilO?i9EPwDp!YT3G7)-m%0VqYa3A}; zpE4NQ+)rBUCovl@MSxcb5I(CrB0mZhKR2aN4Z`w zdW4}#h2DX2HegU)9IB!hUe^32JLx^jpj)Qh>Z^tVR7xTRzL>`uL@Ai@q%BHmu1}Az zYN}aqea)EP6?}L)OW!^);~tvP#2IeBIwBN#i@%Im3#B)I2!iE;K6&KxqF+!TgJ0)a zl-pBQep*^~{!1@mX%DOks4MGr0+DPcjQ;qnOjj(nD;Yc069SJw^iRsqrjhxT2pvB? z9U+@K^hbFIQ7VV#MrlQ_J&>!^bIE3{~%P-;v8c7-DOl_O>%er5}7Ck!rfj zV^ou3uB(n{V=K%-Ul?-`#<)3&5PVd(?l_|8DNf zk%D`RMqcW~StVkK}t$3&qMsK}26E{9dNSkH?Pv=f>vLmNe=_gSjz(^bCX zcQ=2jJhn=DU^z*+<;y!(N6s!FAE7by;oczm*<@ev-0OT&)GwmS^FvZsS?z27*La+o zNP;{%HqO&{pk_pM=nPL7T7I^s75&F*5@Fg-Mb#;&LUVXbkxn6Tuw0%&_2~77eTe0P zn%Hh@?x^h{VjsB=bBn8RwhG`<6N->V;Ho+mxwft!GHe^Zlv_{T{+0sYsM7j^)$t*D zIu}ZTl$7F1Z1sF4oDNjND4CZKlHOvF?L13SZ|$lNZ!l+jBx{4>N-KR(IAIt$e(RCOdGMiN=J({oDta9^WHN z4{-|m*?Hb9MT0*|%ZyoSL<;n4y{yo33j5&cwM!7KP3ycx@oFfa2q;2ML}wD=g!0%+ zES2#^VVq!6d;JWP7?W)`Wmsw-hI;QuZPUUay0m;hefX%Idx_n4xEMajT||mpFEFf~ zKH@vKAEcb}H|1v{j3Pu<&C#Y>>Fn$6Lp4I#Ug@(SNkyj#+@mm{b*MY546S(GQo%XI zvQ!58*r+Y?Mus{rHxW}cUat|25h~y-iGUvRfI)0(>$h)&0<#sE3A$6Qb+2rvytf4& z5NDOE&q^6%xMIbkHDo21SqexKb%`~hNfO(mo*@RGz$hNH;X1OedgwmE{G^non&irV zZ1$}i8XEsI`SZQm>Cz{UYc)?b+HrciV6Pa%K%oekI5Sz$%PZy6cFAu=M)um4<=VO^ z9Wbk(Jl9)14x2M~MC8AWrw|RARTT)!JQMCrBu)5iOr6iV^Ob%pNMea*&1;A66Ox3` zn&_HQ+;AI0xaBxO^;Tqlb_x5aNax&o=`4;Vok+4ATwD)U&?nMbj*BCon9i);Y}DRv zNszdNOvl30NoDxgeVER{krM}m`S>(OGXD49Zo?$To(eM@;OJUTu!x@*_%`x3_SzdC-EkFzS8yeQ4uN zZ9V~^P~C_0QFu-so7Z)`z01wG37uwj3p${1*9)56Ha5j3@p6|PjJi4{RKSQiGTO0} z8E#{@)(jeWv*`Fx-5W0Qfeue9k)Ahn`AX>$>nGG4NuAJ|;1@+}Mgnh$Q-}EivERyc zKB@|BGGMhv9CNF+XW}?okg26Jn!D_{w;1lDlW_PDj>7$Vw;{CH;Gfxl#q| z^YbPAH`uKtZ}{W00?~(?&bRO!`r$r<&W%@})*RFn7xB9iAEc<#lG{1S`-V62RsISU0l4a}c*Br#w6!-dG z+7mlW1n|dNFJi=ah?{Iy?I>|HUZE@t9&lwk$oo1R65KNq!D0)doQHjGJRhal1JaxC z?8S+kfeCX+gjNh5mSeeg3Fa4mSB)fZxXReOA-+zKiPAJi!91S-e!Sj2*!zRx*+-V< z_X!e9eVZpx`wZF+XqGjZYDJu}$0K@q>tv$FMtskG)efx|1^5Qsyu$`wn^LF}iU?wS zOVneokG>5yr^S2>Z6KH}aVZHc&IF>+D^Zd_pBQTWy2{pKUiXmJ@l{gYsE*^gwDHtu zVcdWwNt)HNuDB}=ZW%YX9qdvvqM6lZa}1l}ojQe;n3CD>xo@`LTwcPQByCKf*6%$x zQ?f|0y*DRB=~a*zeaxY9PFO)G7n;`8tleQcDl6*dp<`&QHMd8=vf7Kh+8YCMjZ@6_ z#$=CH*hEw9m=6dHRqL*Knz)k*_f2~Hos4zpAd73P5&(^ z1+HQQXRY$`6i_jyyIaYEUJiWo7%)9XN*-&SU4km=C|q*-l)*nz8~4J8=fe_$=2U%n z2A{Biu3&ni9NNm)VA-f*FS_Kfpw|>5mFWRJUz_eDGagJ%E{Wb99C}`$fI3439y`$C zy824DeiDt`IB<1G=KqTJU_Z`<4J#@RXCvwr|CSIXmSq@G^KxaUH7ShS&5N+mcV(%$ zcrl&iVWE9w)k%@AYY4>mbg~8X?&3c*9c1X0Hrkd}CE#a7Dr8^YL>o%lxPY>_3KI#x zt+1hFr5Q5N;HHZ1PGC+LNEWoL;^E*vB0TpMPihGQIG+TVpi_~1NfVz1-H1Ln40x}F zXdW?!vVt|7OSQeFv(=7Pu~%Fl5~2CQ)8rG*1@bpxQ8QC5Q!Q7*2Cu3DtLvHrRsr+9 znDg}WX7m&Vgy&&>emkR=6)_-8qLqbr2I`AL!k=^I-#5HVGw2|m8$D`EdGqA*f{It* z0w?A^F?>z9ciu<13H~newgl$Tv(}p+o}GHtZzR5ZiRV(Kx$5*W3|p@g(oz~z8VB66 zLd^&#`w8KYkTn>esl;K4i{;dj$yxhozOq9LudDFHfo>EzN_%I#%ARWo>ezSR&>Zqq z3nQQxYl$T{=E;$pwm&d#d;7I}Ywv}o-~O@gfmh$S=DaT3CB##y2(CWK8?j!OM%o#-iw2>t7U#WhEV-vB z=%4s4NIX-+W*!PHb4BElRE6z&z3Uh#ozztu` z8abf%4Xln* zaPOMiG{Q52Cupxe2W{O8H`2lLih4V)`mzp-NaX!Y!-7d#LbQphrDPAwFyrmK{xqN2 z9*0AnZr)q9%c^uXE-$EguD34K-2BvQsytsrY(?}iZ#9==zkq1&ZS9}ddMV27I1>+n zv_ZsL&R>@_8fMLx3OsT4wt1ImuUb}!iVfQ7XLtt^vAwPDpMoM!$zam-W5~nVE>2zrT&eL=Kh`@K2Kwp!Da0YEKoh^M`1$ptgJF+ zpCB)^YhSe8yS}@}^d6s=Qy!%ta-R$}(_)bahak2^MUyO_oaeNVw?3O@OuBB!xZ19N zfxhtQCdlShehS_+TodnTp&?9&jQcj(;Yv%FpDrQy$JxaBsVGCGdG5q6dv z^=_$peyDpz;%eZD zMPs;&1qy+1@YYbi7YR>2iiw~QeGvW*HoG-#)jy1d%8$U1n<%k4cHsl!KnThRQu>8noJG;LC!I><5o_A z`}f4>r%=7HPQL`$1V%|fy%w* z(+k<4PoRj6-i7-xZuSQ#6t9xFHS=|_MwNBk>912s36^2_-UTX?@&;U-EAwvFQREfxvd#)}O;er;etNQ7-o<+!cxc|RNvE1()gHSzu}yLQk3T$8mfh&;H7M^?B` z_~C+_m+i^WD31psPl3B2H79AFZV`CoV?qX`Y63l$RY{IFH|wvXl6SG+jH(QKi0esz|Ye$nYmK=-Re!Y2%n&u*3lka(OSoa0b zs`}vx=sho^)On_@!sgdXq8G;)gU@0Z*MJqaU4VM)OU$H+fGFs0r zu!aF0PiM++1}y{%-i$UjmGA3Wy-f)(&Z~;e3vl{cdQ~}o)yP6gTN9FXN{*-PiRpw{ zOo(I{g<*7H?t9AIsl!IpadYRi+t;=+y?-g)t0+q9cmEYp_hVi>rJg5JdvGB|%PaXS z%fe$asvV2Qf%@-OLeVm-+?0-qd^mxm*Mo+%COW7Yprpe5JMKQt#iDf)v8; zd_sgknBGs+Kq;7=#6EKX@_0WKc(>13JNAGLCfiKzIOL|ZhlnD=s2kRNPvWtmH_v>PqcYPiL!-QsE zrZ8QTCL`jp!c1Qu%Cjr)Q%-t+Q8-9mR{4-iT}?80_<3#c4%g0Kio|!vyozNwFQdht zCLXu^oaIZ`CRO#LKIj-|1!>>O*(!&IMy12e4fEZ^exywm%o;zsWAma4N%KgDq)cn} zNh(4ulLFJ@&t3TfDgB=msA>I24MjG?}^@iLm2*yy7dp5w;zjJ ze_My;0{E4FuXp{AK8T!vx6ohfT7NRtl6ewK`)1wiG5rk+8!v~DS%d?q)F#+bmDTnW|c)p4o7W9?{X-V1~iBBt$JrEmEv2?yJ4Ni zJ2Hm1^L2}xo8?uT`z^fZwAYooYE9(dwBMLex!G#h_9QmM^f&%?$IlP5sZSiuIneorniZ8gZQq=6qD^*56?+(3=!% zTRCyaRFJ&{MXF&i$lQuM(Cty>5{&z@Q7lB|=Z3M7$xAzp)8&#=3`t8%(NIV~6M051 zJx(spLM~rPF6yr>oQ^C-jffB+>i??aNpe&VPz6!Cqi7}0{sq_5G<*fbfy@7)j^8=L&ycUo(P3l%*JLE! z#OyOGdEgf^`RjlKd>~q?^1|jo_K@`=4`>VY>;~C~+6TT3Q}C7ko3|eryGNE2SIvLr z7pegNDD6?}pRUcnmKl6?&DCsN>1in1FJ-vN)eKeXY3h|?)@wVf!kC}TBiOJKA|Tl3 zZuFh2yOpV~BW+tkm$P(<@Af%&}@*LUy%$|EuDd=hyc_8+#;heI4TR|=w}daU_gJ$ zQF1hO1^a1&bN`U_2Oa^$_*2-=h_!&QAAS54(fc35SbnOj0FEsCQ{CT^A-R9o^5;ka zS^_SX|EVPi+yjW3K-^(S_7BPb)EScd56M3}2}u4?`rlOk*&&GFpZfo$GUx&1n*KvF z_z?{mSs+5rf9m{~$^awaZt2xVOJEr3_u1Z47Ohxw$!ksy{mUJ68q>%Wi7xWD3p)Y6C&% ze~b}O0`#+Oc}rspLl+Zab4NorLu;TFW}uZHef}-ynW34bsVR`j0%ZOr;->@^V>@GG z3(Fr>fjh)MRa9|yv9fb>vI3W20_y*`!l2*MRqb3&Ujm(H1u}nJ&EI+Irsjq~J#g25 z{5Gk~EX`dUO&N?0?ZM&b831*eA#awzsQe=fP?nkHr@((?u~Gx{zrQs7M;>qum^pvm zJE-h|+a$ORNVsvJ2}l|c{L``iM;cHZaNPcJWgrE>B7V-zA0^l!GJo~}QUnCK9)J%z z-z`Xvm|4Iu6OTZ@2lz?(Jr6u|BwXK7;zi5&Xp@A@x+K+(Si#pVJ~#lNQn z@K^?_G6UZOKW-v_n>&A-hmf$mqK3|f)^KM3*6&AEAgH(*5Mmby@eA(u zPkEwtwkDR~EAu19C@?P^NvNEFIN(66QAljkp9$dY9vCRF(((XynNiKj^q0zv@}|y) zCLo|+z~%mNGXR9!o7xIPt~h|tgS7XzaRFbmU-uNqbBjusm6HS5ICYtrm^8pUQ}E9D zubn0M$QHaa2k+>?JHtQDfq=9%I1Q->$+iL~A@v|)c)&?rCN^OES@l;!34G=ycn2wH z0Y0(=?;!2{DrFBo18LG2d<2mWNkS9?LE8OQ*6c44j^LwTry<2%z-J&0LE5kZCm|V- za}Y5me^zn>C;sQr0OW%p-GXTLTXl%U-zr0rf79jnfc_r4|11EJ04V?&Bqy*!ka2|^ z{U{HNKcrnq&wq)4h=KxixwL+%c%A8ASnzB*5rH zC11;t_0n8*v;6XzTyl~i=xEPxPxgZxqDG{Iq9f>;dbOC_{`>JWzz;q06+16&s23Zw6`|} z^9LDPI{|^Gl~lzw86-e1*4841wzlBuqiSL4L<0Pm`(Nwy$K>FIysG>)IUqL;MWYug z&Nd{B3M2r%@Q?RR;JFI$x&Qts2j-utnCah$fmr?}V%?sWl^p>m_HBm#he$;1jq8T{ z54c=+EyjWj^X3-tzcb-6M>HRWG2!DJw(U2h;mz4_=)kEf)1xCqe_UB#U-3R5iikr? zk z&=_-x1&_0)N+5JqmR#guK1*Cu6&t_w#$%MH-Oc1f(l7>iEQ6jY8TYtjHoj<% zvJvu#%*a2W>tf-r)U1tt6pH%kRUF;8tG_B8wFT<4DwF;w!#?U4nxKSCOq9w%mI?ny z(uJ>H^OJu-rTHNCn%W}8l&|I<)mY&JIa!PhYOG`i*Ul&gjxl(%&Lb0jn?B#sTpKOr z9C0&Wo?d5Fab`9#Q7SfmC)4!yFx5u~;r@N^NvrYY&F;f1jQUkF@C1b;Gx?9Qn{@T-tIzbLuxdG?=KE-G>t8vP&UlVp(_X4xU zvvoc+F7=P?CYeaTXK1P0`?`D4^y2njJzvI>&6O9c)nc8fG9B$L?DC^K2cqNP?fa7k zgc_!d0#ZaOV(bkj{UY~Id}YHdd8Qj=P*)S_#=OcgBl~qcK$*z=4h;5r#x#mb4J5|w zZcnnMxPsV~rj77qO$Y72TDH>N=~g^u$Dc=dR49g17#bLqr1U6VC5-}SsC97Cu}jXW z20;#5VGWKj30=`=0$#=uD&t`fC7Kgs2vJ;%SVXD8Q$FF4CX$5L;k?VZqcfNd)nYLu zes2^WjjlH5V}aOSV4tC3K7@-rAteP6In9bj%hBhuch}n#cZ>yZ-xZgjRH0{W?S$Y; z5ZN5~y!n_x$hWILxN-<$F|UV5gUUmf{Cp*^r{}%3;?S8>;2o-^Uz%`g=v32xPs_L2 zg1~J2| z+a(-^pmXTFvO51;{}Z_G6OWdD&g#%5yqyZm+1|_Ry~;~8Y!mh77t)`^9R~B<$;?*o z6LyU~rPn#f)%^@eVjB&ghp~$4!R5T63%#K zCqecS`p(K$gSP(L@oH;u6BYMOTPc@gDuUA3QZU&CXjW}*Xi6=82Ay#A-S+i7yER|9 z9%xr_)BCeI$K%&)!dm^l(4P@T=o~4($m``*Os*4I2zNnNRArWozEAIj8gFpa~1 zRbb@mEEaDhJZ}7TMy-EYcO8DlF9*(D_?VwmBa4^}TKG}VANHfe-)#7FB6I)bp&Jwoen^fFLSap; z@|)D{#=T;gauru~HH2py1?{y^bQsMG79%ap=MFYIeYZf5bK~U%UvH1OCT6{ zZ)Mw3wdhOrqI1+=kIk8d8`w`suD--Ldr(pX%J?K}K*$!kv10OR%-{0cgKc)1%Ffg$dsavEHCL0;uQN`RHbQjhgY z8zdi+r$ik#FD|asPBSbpZ!>(%Sk8T<=CPi)DE`JT+fh_7YyMn$SZ-cr)VPeA#wcS^ zqVU;-&^tGm`ENwk61bz+oCa=U`Jb*SGatYcwmhqev4BgtyV8+Z~bX7#kab- zeu5wi@%QuEdX@@;Z)|DH2_qWn`H4z}qUsK(s3MVc5o=1<9RLEC$fYP7Hm`_{+e=)E)Q+0b+x5LA-z%RV+PBe2!suBWqZ_0Ls0dJn5&tK7cm!9HXc6U&Bw z@`xTJMJgkPlr0v*afOVgw=jSkW}CD^hzx&huu;}JNz~jphSze=k`umoeSMvA{&wH# z+i=VEZi|3bskd#6LcyoYhHX!03rVd&xh>W7;p}wbbcIhvDWU}@dZk*Yc2}DFYU1$G zh&--x`_l&FPp!UASGE|lTYiLByF2mp>h*$nme(bOqQq84$jevKdx}x@96jDr8(Cx<~t}m8ZGYc45Eo7Umq)v>!qNMd|Sxp5Og#_ zvi_AY&b&+|<5u^j59>PI@X;!7|G*Ayd0J`nRrb-9`+9h6JSVZrqTaL1sSo{acFLf| zJ(b1OlIA;&^;i3GXy(+!x6+pvWb2u7X!vTRCdk z-!VefsiBJf8OvGH0=Y5eRLu?Xxv(3kPc|+uLq&oik1i)`hru(oa^Amxt0iu|lx2g` zbl0`Nz9b$?P+dKv_+5ui*;OD*MxE}hvnk3jr=gZgtm%N@Lf9rqSQHG4pRFh(3bc?Q0SN-K zh9qOyL^c)Kz64OhVp!Y3gf<{iLK0as^i)|h)h}&pmv!!`Tfc|@`Jbx$a_YXE(Mv*) z0cb_&gGa)!%M>IpB4FYTsJ#etuj_v|A}HQII-^t=Dk)^P)_|F(+t@Bd48S4L483Xy zKHRWP0gLV=t_#c}a?%2k9(A873Igd%FT#R!qo5v=A5U=IjSK443K1?*wECWH0cZBa zNb5c4Z+Q7juUZlzD3Gy%9@<=e4*9Re;QPC|yyeM(;=sw_#%3vK(Bvx9Z+F_H=F0X|0 z*7Bd-Q2Jc>u4qOcw(vL4SF@PKV8tkpwSKnHKQ#MqgCx;iIP>d0P*-k&mn6=iJmY7nY2y{gvju ze{?w7D{OMV@a#Te@7|fQt}P?ie`$olq!Dsb<9TSGX7S<=d=;5B;TWrE(WOm*XcI&f zw`P!L{~knGh~<%mq{$jDN>FCQ(}l~oLL{?L61r+%Jp&PQM%FkhA0>sq-#l8YUJOKB z7lz9TkwC~8$EG1nRqdA`bzt? zPZYfm6Z)XbklmBRxPn2F{3DHh$+7>^6?n6X!jxTa28Z<7R4WOrM$e8iV5O5XJ4|>o z(h+^i@S&-%VS${F%3$#+!`MRP(OOSKdTy~FaMT8ESe8${mx;GbuQVu3DbdhMjzz&c zV=sEO1z+fgooLpeT31vf&|(J*J=pkziUO_&qu94dgVBm%tUO`zy}@qG3S7SiFt%VcW3k=#}!ZPnH3KY|zBnC|8yJvb@xMBGP{S&leE|Rp*C5UmL=>!p{{$eV5R;`J*kV4uqH(iFuzLpgOt4|W{)?&YFV%oHF z>ZQJY-h!Zl#@Ig;C1}R=(DB)YBNgqw2`m+m=ItZCFw`L!KI}R3mQ~;FIWs_Um)Yb+ z3F1`HGaUI-;|v}9k^eQkh zx;+rF=~!i=*{4AJ)thM5ZK7FwDznZJBe4lUUBGq)ujcHKFlUR&4_!a{h8R09bUAqA zyTI6urqtj@Q))!WL|s+C+;~Mp1Y(WU7;i0rFyo zk3eDBdPCcCQmXpRcd(e;V+&m-t|lCDNEz3|e>~L8i+j5MNac;K5ytMSkGT%2kI6+& z`CPk2?@HRkocEX0xI>!RyQ<_&EjBihIj+$IR%6oz2@PD&yrS&7p_rV}>Vj;`xnrS{ z-qb!e=lTP#53;K%Jw(De{}0I^aP5Nf%hl{-hbgbf#&_0e4c{G`{5Kb?z|`d4V(+oY z@a>>u&5`$}oh7^D(i=MtG0nM2)6|{{NPOb1(vIE8tc&(e+mVSMa=MG6+itddnK9K19qItAEGOl|U!fxWnlTi9L5TaMaO>EezBEQGm-eAI03jtr*` zkKa}q<)o`)jhQI!wNU%ObXteObG$8NN#5kCp*>{y?)%PACWL!;`WXM`+|K=2G;;^} zWVBA)&z7BjSPgE@G+<+7p0$I$i7y{@z3$n_*|6`7z&9G-u4^4RABL#S@?pdITciFo z0^cJ3Mr}-%1smAKSB^@mYZ*B=WAqz;Zv?*DIIXUI#D4}M&5C1N@-?Eq5`k|Kf3;R6 zOMwmHuMu4(08bB{)iC-We{TdnH}WTp|4Ddp=)49ZE$cX2lfPN?O$hvjwZ5d3OO_5> zmA^rhNO*qe9L2~5_#J<51U@(NCB{)JduW|mL2P5by66s}?T|kO(esT6{DrmtsFa-O z8sS;PSwADgEH$03)qg3hH@LD`gk$>_uT2+TYj@QhbxbaNi9VA`cKkvGcg!rg>h zL`A#Mrp<8=rCN|-IwnMZt_f>b<@i%{eE*11b=igs%mj9F4xR zn-q)3Qr`Cdmm92p1Cpaw)%XHc6I_V3I5JPoZq{k=lGb=fSI z(YSGn!68AOu6~N)PmVd<-td&Ka-n9#H`$TFz54DgitQ8_^;G%p$Xod*(}D}~n?(wX z22SzK@fyM!v)+HQ$Ly@FbK>)s&HEs7dpG|4pgGNV>+AL`{T}(}_n$Wn4X?|w*Ck%( zA7zg^zvlJpi%BP*j5O%G=!(Wv7ncp=Gj9xvoN{(Wq?fh+H-+zE!gm)gtk87-APC*H zaOBhOo64@}G}J9oxZ&*jy{_s@`I`}cCO!Cb!NqZJ$!@uQAAY1%95?TJJUTw?M8Qg@ zxE~Wm!AuW9z^+LioOa@Tdj z)nc!WTfQXz*y@~n(a-$m+FIWkeD2o;g}O}dVVKF#EGu(SO~%m4Yc9Tuxway5?!~pE z8?VQd#;*0)f2=yFPXy;^wBsa!!%2qy-nsd@iJKhk3mo>vTW9<|bH=0GX<6944}WtP zt;;$8ae45}<_SZ;&-rrrNYh<8*&$w4(Z^Q0o!m5I=Y-LFhFHw`tj`>`LtXpssZ(jD zGG_`-4G7vI`+CBJ;?$L*A3at3-QITGvd1HB@08G^dl#GDjB}eR@a@t#f6=JgT%pjo zv8!@;-I3K%i*I+8Ngg6Iv1tMLkMXRZiU~JE_E@ZXuJA@H>`IXeTiG>X(yPh+zu$^3 zvaF4D{tScA$ zXAkZ_(r35ttWkz31}_Y*YuIl;Khey<&Ed{aePzam-;-8uwe#vTCc9x|z@f2KWn-@QtGemKH%=_jIR8786GxWi-U4Ix{&m&ul9r9iV zoaj9%&S`C&$u^#~O3LCNl?PQkzm3>lk)xOBb7H^Bi0%7&XKd@U-0Jnn28%znLesEw z4|c^z8Ki`7a8HP{c8=^_GV|vLXKX^=PS2=sGxqZi${kqnIj7U`7q|5)W!1aX54vJq zu!`j@>KuA`sr{>wuQ>|Z+-J@Qqvf`|K4Pm-F_f`p`oy<4Lc4gzbRTf}$+z!Xo36b1 zaA}+F7u|i6v-XtnU4x1q&#$o#4Ox^Zv`}Agy!^r;i-M=i-l^*8DGRGher)hHZ%TXZ z{N%a*C;714)#G}nuijMqetTBa;Ya4|#G7~b2M-G@{T?LRQF>?CcTTha;?>!M4n-?n zpYVZKdQz>%Y4J|yVZVgKM*ZTh&iYVvyl9GiYM_PcT%VAM11;=2m3`hRyY0*(ENygG z3&rd5U#u-Ar`=t*=Khv%iZNC{oO}YOZ|hd>wS9o;E9=|cd*uJsy*NN!dm?*gA3MVl zZzjiBXAbJ=HQgnBYPP6PL%&7V`v#X4$sb``)jjl`aWFPX#WBll+hm-^)WaIj$PJ?{_}V)<@dk$Gm%lF>U%-Wb&0^*^;{ zcb4~c_3itI1?)NNk~!47IgR^S&D3geZC0->Q$&v@_HcEKI=AKd!BCY%QK9;^JqZS< z@868is!h5SG^eu8FkaX-wsE-X3ettI;P;fP7m4L!jEHKzMA*Iy-#`?<9AM!ds2CD z_qDuUHF+1UB8|Sh_^4f>a$fO*To1Fc#j(S>X8o#MXX4|Y{c^>$`8wxgY!61M zHSD%JxPHU^(Ys4k&pOq(D<$nu&Hgy*tw95K+4n4$6T3bizcH@s$m;Z%viWJQBh`*^ z0yKlF4_H4mTABE^N%46}uempuFP^cdN41-1-KwtV&NtY9@sD~wL6aND2{t*f|Gehd zWRad8=vg=W&bl~z?Yq@8lh&1=%c`2V!;Rh9VpFQ8vxswN_;S^WM+XlHDZQ(=XkRGT zqds8C-V4H{Co6}C$t+?mDdt@M=CM~8FzYvCMTq9J`9sGh$ZmXcTjgx|0oz)u?sB?+ z8?vgC?51ZfJ=#6;$?Rbtka*NcE%F57=x;uK49zyl=bSn%{@NEg$DEJ4SQHh)2J-tg}dv zTO0qfVpflCi%Mpf_quSadgzgJegpr6r90PJhDP16e6xyw)L`yT&ob+GUli+ReoD1) zKKjnr$Ub&piDHk^D+PCYj$gQTF>7DWt3WN+yxHLuLkzaam8N`)I=Wz;C~o%NeTNob znXqJ;a@X(Y&u@Lym~g6KOSoW4`n7)N?&4Hbhiqe?-}8IxgVSS%xw4Mhiqc1x$>uCRchvFAC;zD~Tm z#@s)3mrGM^qw<7$x8W{B4S$@dGP}qXZzE4zFA+7qo@elG$}Nm#&FIZ*JBAQv4wkM^$YKBEOu`cy8|5;7%jopg zV{_Yn`X7fFW8>VvKV>Xsglu8k_lrM`#S-xT^>G3|k9}xZxImW2mcHIr@AadGqZqjNBoEQ;B+Q1 zW;+{Bbqxtx81BddUo+B$|Ltrz(=#|AG|1gE7@XXu2L-s>dWJX}g2SEREYI)|!^wWG zUY=(7A0jW$3B=z{b~cFd70O?2 z|1!~RQd03a%sH-mCx_JzgI+cd-e4HFWmdAou=6uc_KF;{{nhs1p6`vz_XG-EDoXwC zK3{fm&x&cDH;#_F`eA3|_t0P83VvS}I@UXscU4llmGtUW^1>UL6>mSRyI1!~W9ST{ zYHpH|poF(!kF9m6vFNO-oZgA`K95c}e=!Nza%ZO5!=$a}&C9whaJwe2+JEDkqvLMv zQ|Bq^&F!tMfB&hC{_Ro61z{zw_czZzR$o;d(Az+>IU?r$UHf3ca@iA&bDMSEPtUEL zQT;q-qtoLO?^RaSPp#&k`g$YHE~aro(#p>KXe+zx_2~;L%j~vaknNcwTHT}nnQc#2 z_;>M{w0zpFtM_f6jv1d3yZe37`@Y_5&MDR0eEXqiWX-)Va*=lGCO3M`J-%LEL+jHa z!TsbLeGhc$xi`%9klQ8R%}*tODr-k2fZmd@q|U-+E3Ls9PaN!}aVMUf7VOD8TlZM7%zaGpbQ!0uriUR@ma zYDJ~U=i>01C4Vf|&Kvh_-rJK?ho8L_IPFYYb;{n4T`Y4yt6ZKqtMu5hiHDfkTvfmI z_B~{#RR4K2{rw@iaUaH{SR8e9>^}B;h|j&~^Fx%+{@~ai=9rI)bn}YMHjc>koLZDP zyw3ApMEnfiAm8m1dNUnt3YP{ka-znoGlI4btll5rMN2eopw7J`#+Q3Kb&S={PSWzt-{dh-l>DDj%Td-mI2iE0U<$F9XUh=4LkJL2yaWQVc<-}p{cN|H&ZN@B4-@dN9o;N#LN5}deuhQFn z-D=DHuRpKny}hTUaq`IgUH6w}EpHGUtk{1;X~gDRJI-C}{BC8xWj-H$${MfwRT{+h zzps>Z;@1I%Cbz#bv%y;=r?AFV@U?*l+Q%=QCw2 z`mMYtcgrtIE=WGeJFz12i9Wx%S!Gl2iB`vhDo*8FUtKbJ-!PB>X5Hx*?`-|Y3;Xa+ zbZS}ivA@4Wk$;*_i~eT){=U5NEq229{Z=Q3=K)hrE}G_PJb2FdSM@9Yls{JclyEzL zTXyMq_k=(G3FiwxO~1MC*q-uS+hJd<_g6Z2J^iV0_xhoO$Icgrh3DAFu4v3C&ke*L zWLtzs%{8riZ)mQ4W=XPJh)YS-gTfztqxdkHBY!R4Uzxb)21nGqVacQo4O6`;8Gq;d z`EI`QCm^7CMYCm}AGe=`#_Jc3Tb3%DpJSrNEMhQTrEBL``1Q=lZPX7wXEH{6YUocO zitD#mcABQ`7I}47@a9!2qVB#n#zmWc-pbm0t@+2rW$sNs;=3O0etV&8?vTuMd%LxL zbS4>xUdkA`X+_-n&s(0(T9dQDAcZ|%|LvGS_dU8@(DZoaki__r}JX$ncl z298^&WOecS^;44uai@;07dnV6H4c6G`*~pnBj;=N!*R7?%NwRnz3I2^QN607$gy(1 z)m{0MwcBQK>pIQ%xjZ-DOZUwdXTLRvi%z-q_N+=RaeTE;y(ec^)R!aXtDZg_=sEw% z=?B+V)SUO$d^SRJ0jIN5;`yz)Ro|w)IX1fMj?5mbYCDzcNqNl}^V^5W;o4$3<+)rP2BRL}YvEz-a0aevj=lE$g8)<4roe`BK% zDST&qIK<#&ed(K?%8MAwJvZ7Ny3IPK}B=GkS;L zn$=(591->syR>s?!EqtZ_|B1-8Ob4=9@ zM^W+BX%jXd^^RX~S-~yaaANM&m)2({$6SeEj0nGPvOs@o`r&PxPDTufOyxB^R!ZMcf1K#@7 zt-VGUt}L)H(TaGMSF#X{v8`aqr=PM>INl>CVXqTg(&JcX zb~omKahmo_EvfL{hfYhiO~(9L8F*sDUR^ckz_8^rOM5m>e9Bw>W^Lsrx1ULyR{cH1 zaNZd)!$_e zmnjCl(+ghLG;9=Ox#n#4{;z=ss?!%+xX$b~pknBZaHhw;u2y=!`5Gm0r%t^3cEzx~ zzrJtf^$~`pvvm(|3%klXfVmtA{>%`Cc}|$=DSP5rHW-cAQ=iI>jdK;}li@&+XTrSl}GwQqRm%<45 zZkbVTnY!=QAI_Gq58LRpuex;IlxGu`k5F0iYjpnFF*6))e4O-thH7k=tFuQvd(pMv zhIY9qXUvB&`)!A&9C~8x{OSE0i>AOUUuLD1j?CZIukMs@MX$w^zQwxiX*`lwJoJro z_^v@C+{arM#)sZm^u5I8w;D6os>0JLT`_B6gLd=MjsAB}l=L5Z*oU>ot2ju1o~+!e z0Sk?XoY3=m=y%f8@J#(-i%*LyJ6kzn3s^Z%b3^=}t{2TH{pg|NTN<)EwO91D&{^8$ z?{c18-r%3sHP$yg$iaE1(u+TeRVEcHtgYUa9I#q^_j$ARY0Gbsu_|vvZ#N8k9Jcfd zW4Dj<(thPtD}9`MDusK$c>25|J?5*iR!UJ{zdOHPSG&7xIb7Myc;x@ex@&O8z4VW9 za}?gq%FW##91}Z0^ep4#NPfhN^w`wx558JxRZ3cF=iS)omUlCa_f{!n?SrWYc z9b@g|kOc|Ko6c7DP~I9?a<*@S&7Ioi-E2+2UJJeb$6ERInLCL-4n-rBi=PhOS-jic zrD3c~nbrzJgZFm5YP^e+Jzkc*KC{QxaO~cOfb=;7Qysm>zHB&NsO4DrIe3I~sKLV& zo3UC}zB?4{7j7EgH_<-ktSz&f=%s>f;rA-7nCi6fdwt?NyXfU+<}6in$O&HbNXyCS z=#-k3?+)k8FR!kxZ}iyO$>&MXt@wih#v_~mIQ>2Q+w1KV8^2@veNG;FH*LtQbB=HF z_pE5Vr$4^gW&0n#(XuXG%Ti8nTQ{`m7w^}VoP8>j&5N!YoBkPiE6X@TW(3*EI`(e?!6}#$$-!EEif2et`>{4Op`m*QG93qohub!S=ne{PwO7C-5evIBW z&E`;yLX4>2orj0R)2iezD#xm8uGao;J0@{`|D25?nbbc&+yX5|=vWThT{`dUXQv>m zfO(e!kx_aQ!@o{&eRAgg@!6L?J4}*) zyR7h>?VNmd!>%Fv^8LrJWy-wxTPu)1vdzo=d*5W)FddysuLqs5Ie)2B!73Sfh?xvS*}HcXBqJgsoTf@wHrh>gNj^y3V`=~t*z%!F3(t( z!*w>a3qT6=>uotGx1wd!hZeThZNUX-S)k!=i!E%p5&bmwDPJ~0#FLN@TG7%MTOKYiNKio< zEe}$K0_||j`>L$Fh=;R9D!@iGLdMxgh71%7HcLzg9;6Ej@`h`&t3*7UEz)R>XrluU zYA$3taCBKf8>vdUI5qLH`5;7m^KkXp5K5u-;8B~B%MT~wJ z|0aOUml!m*(wIwIl=(1Wks!0#y6n~{^C4$YkR5KxZosTel$1o`=_kvGJ~r@Ss%pat zaswa(jI1`Ec=?1vhhRe`i=l%l-H}zqN^Z;AV_N+r5E~oQ7g(`8*jg%D0S9OSz=9U7 zuzjTv19lY&aK1=CarALPAXVM)a)JLPe&Pg_#m)2}qdzwNuB7J|`wrq$@8(3o@ZCnsYRW=E%1Q#eP8^l_6Uv|Doh$wF~)(T_`Ihq)Ai>?`cp8Ye{5I5CSMWXc)` zA*UA0x?b(7;}l$cBiFJGQzRx36xbBc(BA zbt{Skkc~=l$T)4Jk}!+DQ?RjAp~KIWpwhLTf{iH)UG#CL1{Pf=8zeHuX(J7pzOKkv zs?_NLW=OUf;9LOkfVo)vT6cLaQyqIyE*mZ~Lex4M$;>h$7rWsAA&D9+BkF2bC98oA zd@LI(GQ#;I9ccOv@GK*8(G!E_i7PbdeW@Z1*wGgYd?FhrGQt@o4Vpf7u#Cvn4+o9Y zqxFc%GNLShM=E4DiCBorM`P2>Vv$RqILNI}APepSQqfrxQ+p;GECSDL`ykWE48UE< z-VTBWGinLYl-19)N>&|P@Ep*Yg{Xck8ks?i)msnskQtz)wDwD^mT7zY#F5Nm^v1GY z$SxPbF}yUJ=_`UPV#ZIxOwJo@$}$+ejoBM}F@eFv6e?wxh(PshPZ2tilZ#<{Uc>be zB(DKQ5=C&QK`PQTYHchwxe|^5(Sl-I8$H;RwQ$QT*;wq^CE2#)J)I{b7F>0aP*&Os zU_U4evfyqDGPK}qq=tQ~mJP%1UM0b$FEH?kfgP6yXM z4!ZWWWjGFH?E|3ou(TRkPt5NQDaJHDZ5(pd12M)=%Q$z%F{Z3~_iDk@#zj><`i`9} z?tfK1Li21$l`;^B0SJhL)*1QLKn2V}MYi%Z${_3t64j+K1?hd3_I9_dKaNN1?7lR=Af29eU{c-~QuAe7hJFt<;0Jj(VDU{=GzKLNOfRg!Rx=nOHr-r*So|En!y5Q;b|#95zZ z*I?)0kdV?Rw6LJ8P7Z8~GT)Nh2q7GW6l(K*>w!|3@lIB;L!@ICl+^zTm$KYJUEbj< zfV<}-2`-I1;FAkodma#4I5N7p_O|<*>;i0QJqa$2WZ<_ebYh`fOa{tA2cuj4UDmH- zcmU<}c07JpfVgC(w}w9tak!BLoXy@9A{{~N>^YT?qy2RZ{yu5IlP zYJ?Jw0&Qx^RK?Q%0EtkDXBuhHG%{KU6W2dleL@jOfqwZH9O0oLE{&H)IR%RAZOnjj z3Q3104Qmd>C0(ssH2nN0>u^A;>E$G?Bf6z zEU+G37?vV76u+gCgb|IDfNFXxYJFT~AggNN4mJlMPrx0T`^Y^}!+)7!jBbi$n_+ea{>e(Oco-Xr)-(a+$2XsEy?+GU7pe zQDu%s9=Bd(gPSTazGB2gd znkKfmDis7O_1 zxB}ETi8>NBsmDX1h6F;PhTBZleHpIUZAVFJ(0QzY3Obn@Bzc8RNZu&8AZMsyy6T|+ zU_;rP9Ap}oO4-C#xtJM7u>TP6!Qu;JY(n!!fdqmY7?kB!)| zl_aEe#*{2$;z<|~p>6EJCRA_~Qt@NP#Po-N1O3t%5@s67ORSemMpvqE5Gptdv-?oS zdTjnW5>gs@Kx~yuB9-C+Tq#GExW%)6zRCxe!} zO2Hwta8#thYIGmW*%0U~9H@s&p{B7v81^bkj7D{L8li503#$$8v76qv0+XIEZH`32vr;f7%Z&lV&)vid=aRc zt&D~j%x&C!Y(xAEr-29B2>fVJaELBTz+oYNOY03uA(t=l%C9GNm}7vZWzRJ)8s>c4 zzkbbvbHARO%s0U%y)m2CdCK{2CZD6X4p9snyuqY6>bvOE*V4iPuQTQkli@e9mll3= zI$|?tm+hgHs`b3h)_n#(7QRpD{Yne-ahj&QzM)LWV!eyBolw1|N6Pgx9@l1jIStw- z%q+U7(NDgiD7?z=){R9TRRs^PXf3N66xl1oZ1B}X_b0NV)AJU8cZxVZeH^pbwwpCR zoxenT%2mnEUwvky=vdsfH9e;)>tB!9TF}SHIdD+iB&R`F87@JkgPfPj4~gEgj$?9W z)~U%aJyfsGbly-l=$NUVocx>&f%?v#*G`1Yabv6RoVluK)NIG8a)&k>P6_21+g=M_ zbWG-oV_X{Jc#-DTcYeduruWRc-Kla-&pB2Dvxcj?1R0gu&J29GbHqeZW>UdHZ~G%& zrfD;MY@RWdAM}$wsI*{p>C$;GZ?5d4wL0pRY;s>Ehg(*uZ02&a+sjvb%G|L$V$j)5 z-}=^(I;^ho8U0ng= z!RkROoqzTAR*+fQ$B?nDhkUmlZPz*y3kT zlAg#7QhBQ&mwhnSr;CfZNV(JZ?9O&3H4Z#=lU1S}jPP59-7R{{*1e25p7YK!%HptH zo|@~8eC`|&gKZw4Rph4EJsdc5W?vbu#F*g8z}f{9keX&8Pe(7DP=xyV`nV0tXti{aZ5vm2*8x2Og&!~HSGqd)=!|C19PjR*8xgB^i!_Vjr zH}3T*t7oDgiEnQ|EBWI+J@@gdRr}>F|f8-Cmey9`djKJ~#4lbMV=hU%TyJVIlJ%?su(m z_4w3JDk)}F7k{&spOWG4%wiSDF3T$Vnzegv23PCwVefm%>LTH6(cn+@JtJx|U;kOQ zPju{L-K+F`q$}y)1K(~_?OLa z^t}1r;D^GE$48n&Y!({M@vd2M@!i%r-RnH;OVqe>>rLGYCH_z){q3Lbt5(Pgr?@E+ zZ}h|GKT^jw8nt8a&QkDsZT*uje&eyF`zMZV#LOCT62LxQXJ}$8xr|^DhtxqWa`N@6zI!&K#k4IkyK;pJhYML(r}R8f|gKNIMj1#2vY$hm75aU~fD?PJbQoOHTpDsh z3NEEW59c3mMlt4N%Wjb1(xB?*1 zDtFy@MmW|~DoG6*M`IlFNV@%26Nh>v4W|iUQ#uS>8AMb(I_osN+SN*EqT)%uQj9~~ z#X=EU#zRgMOEv>);m2pBAk(+N%R!8!0VUd$dJgqS8p^WZZt*e^2QiXn(Mb*ROd1hn z>Y227z95GQCxZFH^Vg(E(>D|0AO_Pdx8+;P9`#_lO#oj9-lGY^jatp3Po3qEhtx#k zklga-5Vx~XoW5)cCXgHJNI|Bt`bkZu!MO;Wc;PDvT7vu^lW8zz=z@)$j0YJxnx>H- z#B0yUf*~n!5SO%2L3Rja=wS&~U~h;6FKS7hJ!yQO0*5@DMo@!#IDKjgVg{gsPXriXio(j-^2fO6rrW2(=CK<8>w@sr_q=eUeKw|Em9q6r>sY#%v^9`Gv_Oog!N>IK- zAju%p3YYxOAPEW+xtL2mpN4R~92sulRVIkGkH$2?Ma-wgB?P7kF74 zxlnp13j<_^5L!9vd=D8CF(2$*aG_o<9c%g;DlYLlH1Wx(HP)?f(&G}6ISMt5^#xbP z0!)4kDS6Ncmv|GJ7;by9mP=^mlyDjD0PfrIl5lC%F}TF*&m`fJ3ksLe%29BkCeR!P z?zdQafTcJtywfE`3>x2q#3f#YCWcNBL$Y1vLMK1u#uhkyYGOM)!JW7VmZV72NDSg_ zXkxhS#Y8Tln^Qzu4XarI4xe18ol6IqzP<$bfr0WTl`|xT7cL>6qfmp}0g2c}8&Z7f zYpc1$o6w}=+t#OY3CSFV6fjv0TelD=F_g`v<4Y$iNPQoI#Dru{fx6|*iWNRUM@*kX ziZqRw+Z%$2=fExJBWgpCPk%tKI@}lBUfAo{VQ}$-SSy7Q4;~?y|2xFX{cv_5%|Yml zG->jOVh3gG2eUKK#^taF{y>1?BAr6$yK>JZTZKUA;>toCM=8BRz*`tJFx|yqbize> zh0xeuafx@MiE{`d-qda(Kmpx=ja}4Hq?R@cA?`Qe?}co62C1Dw#9IDR5MP`=QrV71 zRgO#c3PGrHTxz$FKf#PxY-BJg!1QTFTxpLGX+e%lZ4zSCg}D*DU=h*((aBx1SqQ;h z&}6rrld0W80xvN(0Kka-WE#aoF4Wyy^D+Tga)iw#gmTKf433`5fL#+}3yQ`U^m565 zAw*u2RO7h+Gz`(jGIxRV{n?e0G^VptML34E#JD9-{D)) zItea)yL&t-YY-eRX`siWwgv&yn(6yF8>T z2z{xIM|K4vo;gX0jYkOKs3Z)$gf?I&PLtwGCsOij6>uiB040(zj}X66NH;RTO-O@> z;C*RF&XVBLC#&(KOh9ltw5V!gxbPr^f(s_ms+jUG05|0V2`+uw8V{r_goM(jt?{V+ zKfrdvdMqKIgp@`efH3|CQb-h3U}0YjUJSAnTYQ}amqs2)*?qL|KqB8#yL|vDEm6dS z8n;vjPowD0Bb$C8G6J>fhd91W%&8f?4@>(VDabT(0^$5YTFK)PA~%u~xgd)*7m|?D zNCzpq51bC9U05Eq-3NHDLLbaO39z^EAqg&>foR zJhHq9*%$5S);ddwGO_jVfwwN?F2B$sp7D%pv?eNJ<%x+TQ~z zWjv@}OFeTm$_+fS!w2G-BjjsTd~u~LIZ94qbC^z`|7GyO`=`N26tLQ3Ot0paa| zr-s&<6_4860~g9LKY0>T`V=xA!qx*kTM{XO+Sa2@A>+ZF?hd3Z+1R5sCA8{#JZfVP zh#FVOFeepKkZIJK5S|`nI#7Fhv`S+l9?H~%MnR89tcZ(?TWgS8$80=mQx6zqTpD zDZVt)fo$XfTMN?t2R^lr2hhtl{*SLS;|UP|zT(%J84CrZx0xlIZs@~OHgDKDK=3om zeRSOFM+0&$GgWqsOpvo*9O?3UPqee$v;BV!Dk5fY7rE~L&d+#aKW4$5eD_>Gzwm&v z#+T3aSJrrMP4VJaZ1Lg=YR>K$I^bf;oE+BSx?9Q@)sL)tV(#_Xuio#js>n`<9DOBh`HXd8A z`ZXmm&E4OnCvQ|r@_o$x+XkJ{+k$Vn2X5RgU;J!YvD>i)S5-W^mzZ`pc{Gf%{ql_i z*V7ittDU#6JrV6XL!KRdU2y}JrjR?o-<{x>D+OCHm5Cd)vh74Cl7_+o<3h>Jas4tJ=xUBfE`{a$huXSj~sO*L*cM1m?*`b=p|B ze(%$g^{RV+benRlT3cRbQ0@lZUN5;0%5%Hz&bPYnkvD&o06@{6B6lldz%8qfi{+=E z&C|AidD1iL*>#@%?d$zbM6aW-u2LA|F|{zV6W{)}Y&V(VHcmQvH{BmZT^KrlRq~ih zmA&;>-sUq7n3kn2lmXu|RBLrXP2lL)y;9^pZ0~JqSI>;?lx(P#sj$qoH0naYWw{ZJ zF%zN+1joA%`Mlow`ic@(QREPl);lZ^^%86&Sfmr2R!-7U8F@MnAHo@_e%tGB}C1={CJ%nm0- z=|9%K`fjOm^u5iaD>vG%D4kls5N=a-+Y>c5*CfCHq2E@Wg9>)3@vpDw%nwzvj&wN_ zF6v?GJfhdtH=8eXN;$_=)mlAVRY6!@aOK3=#0x49N1mwM==fuR%#KbUvQ?V8J$h)R z-EHu!U5r`d%jVXd5M17BS5@}Pv!LJaE`^MzddIp&Uf!KNN#D@Trt|YlqGf-Ye%1f{ z`Qz(@_pk1}dS8*-?~==<3m1O>{M+y){m+V~ul1EzM{`zupRu5-`DIPbp4?j1XZsvO z;{A(8o|}mHn{VUS{goLSA8UZI%;b7ue+6=qR(CX(5aQ|xb%Ex?S27|+RdGgcQP}my zlF~AbRiynmi+t&wPw8ooMdJ$lZxa$y`cz7O`$-mAVdqm$vcOtuZ|t`WbD4+_ex$v$ zN@IA*LoGy+rZ%yExd{w6$DLzD0Ms43kjN*`wpvlQdYAL5XItQ;xd8x1HA(cXEc3~; zEyQ*zpL)0jHFrL|!Xe#u(8y-;WDAB^EVUA+H|oh2)ZCBGB0YCBQn>wOi>$fxsV7@d zbLYd`8yy*1(rk-hucYSAr=D%WGz>NOT~3nmrBQR|lP6p4HFrMsWUEzk7x7V6_B6^8 zeDahF5oGEi7Zg*7?Nkp^km=Lh`Q#B7?43!2BR=(r3&3sJPF?FQ371Aug5Q45MV3w9Bj4E+7reX4| z3sEqrXI;1iFXAJ|T=XgNe2OtwTZI2P=F-97t|}ki)#yN8kf&WlUTYa35OtY)+SRJW zgIFV8vPGk`%}0#9$ft>ajJ(9Vsv5+gE4i!e`6`X3`P{MC(cft8OMiP2dC zQ6n))U7k-p62oXEBT!Qh#R#z;>2*(I^(N28TFKES)>F^M#M01G5g$1lqmdkl z*%(fawp2$w8-qKpe5Bt!jS3ujMAjPSHsOMLL?#Y1p}>g*@ODSXr4IplTt+}|*>#1K z7Eq7Na3Kx=#`BLfVEPmc0eN5s0gDB>7Qj?>uuYE>bnwtj@LxkS0rk+VO^*}68z0i4 zrZ0O4$g{Kdv=&g$&Y&JAfR{a_0MobwDu%V7`EYod62y(qX1gjuDVe2wL++8!xC$2q0>Bxm0FP3=Gc1v>#$)}_Ks<07qqY(#)4Ce%g*ZHu%5qz3_w`R>0w2n5s~1W>RO zAnga}1PtFgK|UWz3U&f&`++vWPJr|vpih$#5U*Aup(Y7-0%{L}Ho;B+m33)K(7INkA}!B-l|~5)gtN%8h`|-W%mc z0D6?RlM6!kd813qnyxWsNq$4p`E+ zpQzB-s0g7-{||9gNXY6a)UdsR*V%tO3yR7h=308vBLP6$PH=~&aJe+kJ(1u)iRa!?x;w5fJNlu-eVTBDFy zcmGF%rZy^ov4*Oh5X$S)v8GSg6_TwATJv42)+nU5DuAk;5X$RPfN50igv7!-Da_=G zLTy&irrHTR*rz~9?7fpvi(kJ;)ImaDN0k+*+6j^F1oXAtLWDa388D?g0oJD436b^$ z^a(RUggpUCwG&d?6SS#zLMX3GQ<^??Mo6|QAf7=<)lNulRRC2xAxdy}-Vbw!t z5hS}Bz=sbsN}SeRb0M{>LH;0y8s=lbTp|)eU0sUHG_Ldu5!MFe<1@9j!9hdjdH@)8 zbArZZ5aDVd1(%a#KtI%&!92c-he>OlDIixOl@xfP;UT$ z5jO~FJZ2b^T@Hw(!P4clw%f+kE(hcJz%;@f6 zW%J-Y`vOkf+cV^-jA{DIhh_mk5Ox;ZJt)Uq3`2&k75^0t+uH7Y*|}X>T$H> zXpufUcU$^}U;E;*TXub>c&m!G4-Pp}_RPB~^tN4Sx%O1{i}-1yKe;!=Zucvmy(ePA zq+S(2CLTR~S2y8LM6fl&4C z)$x7wyXM~sms3@9+?-?ZDsI`S80|0}wek7!kG_7nRnc^JPW9{)amn#rZj5l}-4O93 zD%AQlF6i~%M8!H|OW(LGUWBHH^({flww|UZSlaqV{7t#BJL8v~ihH*66Z=DNRke!2 z0o})Hf**O&#q{SFXZ!gdES|^cPP#fk?(8aEW#ijFFPqd<`wa`Sy!d!^FH>PqfWFa> z6RFW(Ojhu}cr#pYPXIqL`FYF`o9dLQV?>&!;q?bp)@Svr|Dj&BDS-cFAj7qw`$6x7 zI4OUEteuaxBwf(<}=$%guB4_KC~$J>F%h ze&MJ_v1ZS0$J?P>SU$F{jCz1lWhKHKe9pA_>FuV}k(Nt&;=<#j%$)|fta zo~CJ`;tchB2SdQ;45)YNy3NIEto6-<>>cq7D@0FK^JVMZwmeO8*qfYEaChQn3zuj+ z4e%AN)yP7t{cQK0)1K=o2>Pk|Zl0FTba)HEcx{}sH#s`*5ksd3nU0 zV{1XE6XaURbmBYfmX{+-xogO-MD$X>21lK@;w2WhVz~`|NZi# zzUEGOL%@sXh^8NZfBnhaV|Xc3^g@)k;;-npX7hU```(vUiGDYfd)2BhjoF?0uRogI zYx}Yi;?^p9`g;V21bMpp;r|QIdd6@zC!PQPqne8XLWA5rgU5_9oE{Y5ZtEH1XgJ;6 z!f=*nc!=R-KUXhLGvbd4#2=HL4JU?&SlWiThIoPxB#l(55PW+Va^Ar5uF#EXDGxd zj(lFF9>wjsEZYau8^??Qs1e;4ov}vE<4Cclp2vY$_s7Pqq?vENPqvI?1vb|fq#cK9l@A-6!+Z}KVYM#aCEX6F@?j=t8J-{dI|?6 zG3N<5n^99Z8oP31#1u{nHGJyqc|Ef z2VxY5mzCDtOk?U%9B3TXvF6DTbq5zi#3&Arb(;)AJ&MC)ovbLQj`>VsI)iv43L>2; z1Tm8%Bd4CpC0GMt&$xrfamI*o97#VlrXI(^LH_rt%y57jRS;>c;E0(VIeSpgC6|z+o(tzDWDUO1PqCTI&y^1!t(`c3P&ek@)Qns zS70|uf)~_7xPsXbFrpo#6);ITAjD}>gsEq6;Gm&~ap&L_5MFPQ7CdM?ZIB0X1ZqHJ zek+lwZNK>Q8rEteDw!X2UR$d$N72EK!`C2V#9QUUk|L_0{MBm+JW%3DN9 zp}-8Uxu9waz}z46nh!)7@Ou!Ykj7U=^#i*W z063z2(%9#K4{TpPNxd)l4$AT=?irX^zCR3m2Roj#j3kXA#C##MolxgdVFwvq6;oLZ z!wxUXNE4mT{6f6gij)|o@(Z;&1SI-`Kv;^9KIn92bV-*8iHJ))q0}}Ju!f!sf~f@x z_0kci6S$;bgcvwEwNM*Iv=Ln?1QH$P9YJe;ku;BJK`(X6rFM@n4+WDCmJ!N6f<f5=l1+F-s(;HUKzf;~BLSHp?AsB>@m2 zil!81%AyG{55V3=f^89&5z0-1#`~Ax1KanU;Kad^$lla`65W;qnGO63oJLZKo7Q$y z(o6zP5Nc<25Nus+U~h#HV_#>kJ^4kVmM)2?TyyL;%ORBpq=&PvMfj z66jn)Z7czD$!fq7l(Phlm*v3+B05cSu&E8#VX)_}gpVZ3T!K~^NYY&bO>b&@2?%`m zDww!Y{t`4^`T-GdUn=qSB8irV+F=5)q(An0HPZu71?4kAYs4juCd3g3uc~g{69AAX zOC^lBK@5yI%4&i}DICB;XtZRGQ=3gd;32W#q6hdrs3w!ngd*uVAx+n_q^v@j)Tw-)7rZ?%>4NkdKz(J#< zhB?H8OH2TG2g|dd9JY%%xyJ!HhYC38iB++q31I&Z07ukIIvXAEiS2*`SyQ6XVNsh; zyj_nsIxHylOVxN<=}o*RizGncZ(C@~-Yi1qN6{M$Tzg~F5}8YJ;3#F5#$3XZG@wA{ z5*D=s#rlmfmvpcN1qf36QYl3cLg7aR8*H}X`40;G(gdMVp<)qR`ecHTNex%@k@*j9 z<4)QF2?7OvY2b7cMAC^uOc2t;Ds9=NlmT6$ANicCkfnORptr16bmE^=t?MBh2 zP<>5?*&F3aL2E)GcJ|49A)}{urGU2$EieK&s$SBloUq`mfGpihl^sIdNAU%OeLZ+M zp|&qg6*@UiZ0?hT4IWO3>`mzUD9k0>VM0NfQ_xBfNp}kIxrB_F+MZ%a3d9V>e5p9o zxm>`4bAf+ygAnpjagNxDcfu^BMFow58cWinLL6suWv4c&$WH@$N!2b~c3>f0Drg0c zsFLIiLhVxlf$Qyoz)?mObgF7BNvDdIh?DmUSkzV(;B^ujn9p8(n8WH|uL|&ih=!6} zfDzwukO1Dkt27oG!uJZ;+qn67x`!Cr zhPs8|?-{TN2=X(W=^6b0xB~|QCDBxdk8It=lck9~ceGUP0HgPq86mjy}CYuoWQ3sZQbNG!q$?bc} z*Ls0h*&%*|__bbO|B~JIrkwC6b;-v5E|w~*gZH@kUw*z|p~>{&!qXu#rt^pQ%I;J9 ze*k1ukAf9B65gAc=tq6#-xQi_PV6L8W@*ZtgT&tP)V)+SI<=Ay`$G8 za4$~r?N(bhSb6)OwZ)waZOtqOjJNfbsV$mlZRaz?q0{g++dU2q>~c<%UQ4l!fZn zwznp!=3Lv_>(yI!%y0YYzIvQ#{RcY#EInf6QOl0$I>3CP+V<3OjD(FYc9G`q*fH7z zY(>6mqk@#wdkj3debf43&Nog>oVBj2yQOnKU5}&?j#*DzY?Huk>m#G^yTQGf1xZUR z?A;D-{L-bUho(j1;PGlJZ)^>>@OHyCewp+^kFWfoa!tbCz(Kov>=q98Sk}`kuInqy zJE}2zi+VngpYg3X+b+(+b;FUH4APK zwy_7dp{M5dJ0f?p8^a@uCpDgk#6DE`L$i#(VutMB+M>I zvZ%kC4Sthn?lgJlx9r|ZrFvFF3QBS}_Q-Zx7-BXjHD^hN+4Xhny=NUxn^oDzYJabD z1>oN}eU`?;PD6NW8OLU6>?)j{ZtBe(avDCY=y+mv{ zQ0zeUmQ7qkB9dgd0qp(B@1+HaoARs#y3rZ4@5|zqyYAqP=a@J?eB2sUzHNS_fF_`voZ zoit`jmcU>aULt~xSgWCPk%=woUN1hqNU;R+2o-Fld}cHVHfl^rr;f-*j0vTJO+6-@ zaRtB6%Z68gq@G54F}K$~K`vvpX^3p<2_e9&i=DlSYoBc7n2^SaG8;7}Bu!M<)MG+$ zD+#RovQc9~I;A)^VoWGS5b7}@4ED~O;DwFzZZPcudgQ>6#=!&|F)+l@OHhS+U?@fp zhHq@-)R4}m88tN|=Mw6vA;=~DvFclpnoEQLRMA4*G zutkgw@kdgEAjCx=RIdgT3V1IA6MOI&(i=HIr1Nx$7$8csgtF6wmmvmJK;Xy$BAvid z14MEGq8=dXdV|%h(5D1&{3s^XbJBUIfQ^_PN=2M{c6j_5ey4zqoEXxXOAr%7Y0Q)t zIN&s;?+cg&5fejWWBPW4IFbXya6_mQazP`<%C5 z8WC?9YmXW#7pR=FtrEXF&4E{eqvR4A^r~5w#_{b^{5b9Z#7im9N{2Gh4gvt zf)8vj$&$}h9O_XatD3nEVD9KCAqO!fl*UZiV#1=6^cufW#Q{ImKKs#_wmBeew0k!i znI+W1Juu?xgVnqtfFmSXI?Fsq(yc?HZ)hv?975VhJ(7cK@#7Q+D*GMzND}W3BcEMK zM-vXA>Z34&n?CB;#Oq8uw)g*Wh`T^2zCgQ_@{cg$Q0?zXsu1rEBLSDn1>4${tp&e{WK2LH?|gvgHyc1}IM;K_j+zjU1GJXJ}$ zc~Hcea)b#wV(^~DZ}5@qU_TEKsrH+xQp~1y^lEa_;MOU$1kY=RD6j&vTy7^C0pJ2kn3JyoRYM^uLij5Z(CG#jFnH0RmT$ znnM4_SRUYo#5KU5-sO*)C3Wf|CjdU-$);bJ^6Eq zLrUON2u8w0LHhF92I7GK$9Ny2=zjx#AOrsAnsRl-4*-@7@WnP!gEJdwVj}c}5pGuYm64Jy)z!|~SNjIU*={cr3KL{t7unXC@}5ll@DhuXwL zK!5YRmN~V5gMR!=Oa3+3tD}DO(@?`XHHxqwYZ;aN8}|d@`A-E|9ry#ag&K}Z{$u11 z@Iqqn|A$ilc94v;E%crC? zuG^@=Iv7I<3}kJ2{{Jl}F{Cs;C7x@y6M3eD1G1JWV+;ix5LiL}Nf~2S#{t>5gBqla zF<>*$|2msB3}7*UK}z}ge*#zxDT7aGoNaefgN>z_f1f{6Ft9MPyT%2M+OJ)e4?_X{ zQU;&WV6*L^25U+TF@gWLXJ5lG3PalE z|A)=~Gu4VACGRQmY|TiVh!inD)|BV}^^)*7|FgKktd98sPBF5CcQFwyH8E}f_h7GK z_76h=^W!fCSsn8OD9GM$VPL4FP?w>AhqPj0iT%}P0`dCwINCD^rY>)W| z;TkktC5By;RGfe!h9#6RKGt@?DHJNP|L!x4Ssmje08N~hl;AzqGzUlt-s6ug|CrU` zJ&4Eo=P^VD`LE$UFcjcDUKd~;yax(L4UWcRC?R{S<1uePYz$5G$4R(X%W>W!rY@3EGuf~5fO@u#X_SBLiiBM#IXO9|OyEzekBS3&mp z$6aDqNA>_b3xwB8P=h`MSYihMFF(JggH5am{JDt{?lVl#7CvQTX>SF}3&3%%f?o4l z8qCXWN8x#iA|}nrjIZ7$42!4Ww2&@g1Oo2QCSi(!9C$jaIl?Uqk>Jxy>%H?&=G_+7 z+mvHZ9pcp6k&<4aQK=L)-|964397hV&F?ZyOPX;wW9H#{n0s-25Cb`pHG!7)@wxK-Ny)*V&ZdN(P?hwfHCL?VeL`>W zBtcQGBRBoqMvG^&YMIr_hvg#`{gR)3dn&&a#HeO4dfOl}pgKA98(pJ-G2!lxyM>B5 zS_y&&+&&)OFJ2P*?6GzF4r4VMiEnZhknicJ1p>p}X$9_v^Qnj2k}&R1^Q!MZAGNh= za7aJow%yp}a_rA@pVAu4y5zz*7Zi1D|n^t@Cuc8bqWM;Y{L8pk>M zRKF5Gg?;n#WtxK4lBnCq@=UbvS?Bh>P8;86C}>F@XEA@*C$O=izv0^{Wg7m2TW#f` zga9WkgPsU27V6jU1i**T^Bs-pbZjkW-g3EnSBKt`PpUupISpks#!v6nb25xSLpVj~ z8n=%Ivlxd2C-sQ~^vn77;xyc1<=_V&lDBQ4TMjZ%+ecA9Ar_ zTehrxy%4i2ky@_jo|66*2A33+LA|(f&GUhV(>*`rSg7N~1=JWj@10~Fxmo>6|46`X z=>jN>-#;2KR9^SLD_ zpe?XjCFhWZ9EC03^}!d0Lg}4i z=j7gUjil~~>pR9^y@CGtO+RyT#~jdMjC>4RSp9$@I+@T(_3W>ZSI$eCCD z$~MnP+WN)_wOsWL?f~47jaNa*St#6F`mE}MXJ>;?vQ}v@J6WWNif*1vSI~MP7=T0F z5!g6PZ@l*=AK;honinqcD|N5%y+$#S%2W%6_bkoE0}r;hOjm6>8VP@P%_2xzD}fN; zh1(_-C7b&50e4@*uuo58N_du3w~lVfirI`g?Nhw_q;gymV zr=`VteAlmd!u-RP@86w5EE*Jx4kBVHRr}VK=l{)K{yo^MF2{(2%`Za@dTL-PRr}VKaSCg) zKO@)Gt9{C{)bLI`xHB6{sDHQtP!Tx8_j$OUb2ex;CT~gTpy1a)ab@KYZ zG<)^(9(d8XhX|J3Z&%X|E1Nj$@r=x+sC%h^bwviFyQtX|o>p1+ATJWPdX zMkto|*06AcrBL4cOF>pI@71WniB>Gd^4?l12!-P0aFE$q)^oRoAFhPdd!+c-$=2jx28P*@75PsLH^mW6f1jcIU6Zd z_Wl~|)hm0%?X680h+6Vr7xl0dih6%DzUtzUhy=uYiNrXCJj0qsBNPgHAa2Hp{F7r{ zy`XnRn`pBs7WCFqy-7Gfe};abp!d&abL^@GJ#e0kIlM*(uKZ&E>!Kc(Qc>?OzE?d( zL9G0)IBud2-={~Mo0KYhYgzimQmE{K1<${|&gzxD3zvz@UrLp|wJf7zDOL9V^g63o z_K04m{0bav<0w`3){=1?rOF;){2vv^tzOw%e--vRxc|DShoe-~`$Iu+s~7c%SW6ZL z@VvxPD(bDJ4a89@>isb>)sluJ|ubTp@sBFf(yOpRw0z)|S-gho>W-N`oyqQ15s!cizCf;Q)C zcWwqpDkTCmP76COQAr%BZb|)A~$x|6yqZ0SA;bQ zp!Xr^DwrnZX5N}$K9JjEYl@;EE^^0dO;I#0xrehRPz`cdWKB@?A-5RT1mzfV(_T$b zcOkdI)dcf~+*no<)LF>=S~bCJAh$u)M2XUpK?ya1wvfAYY63YDohhU$tO_9q*)KXN}vU(?WB7^wj3NhBmWI1EV)6OCdgR> zY!LG2Ksm??p$QVv;P(@AtiqaLj6r)m@=Z8eGMGI09K4v^lSva4-oa5w@;wCbH~Gcj zGcs7WuqKcT*u6u#2dpH2@k$?suRG5@w?076O{!rK03x`I7-=tN#CJ-;V!J#mu38o$zkq3c5OK!#|tSJftJ~Sc^ zcv6!4#lcrlfcR}#z{|-66mSK+ozyD~z74z{5sIh?juuVMpo1%b0g_lC_&&e^Io}Hi zuv3nlLj{%!us}{u0s^o=&iVlYuz<)0SO8jb4;k<@V7$k1WmjISU|iHfGwvb6;0tQzyOj8kwgNpfH))=Bpi!)B-lfc z%2CAI01Jpu5(N{0R5l@A!9lcGa>Fq2Y`_BImqfwbz!J0P#QVS~AdX2Cj60T??}e{` zNkdAc5(&TpF*!*j(6m^@Il<`8Sj0QQ?g)#xCrA_s3rlXr0v-ogKs*%esz?b6;^P1d zh>HUF#k5$&N5Ni<|D zx4?jLB*nPI+X#Rl%L{gkIO3K8a0SMp#Ub*7;bw8<{*&MeU;&9A0O~6(4v`lMm;oFH z4Uy(6>`igR`XTXgctDVK2F6mwAyEYEXmNfx!@ZI7DFZenA`}FyOjrafrZRNIM)NFBrWJhsX=|>NsLT3H}Yh zI^qy{!B}xPL|(AJ#}ktY@NEzZON&Rs3KTG3ctl`uEPzJ>3m75|Ph2q(pG^P+A_!m| z@#F*lAgUDQ!XwTMg+|fhk>CQ3An=GcLjm#NN#Oqlz{>U6pwf_I3~d(-VDZ+!Xw@c1*{_;*b`Fx1;;CR#GRoq zI9fdUKoy9M0SkybgF_iS;?7{eBs}8IP{2Ckk$3}+a`1>ZLjm2vBi;;!MbqLDZw3cI zcnEQ4Fd7gZac3xC9`T4fgJUB+;?7{~9Wip=1w`v0;z!&W2%Bid5O0P8swjqdGZY95 z#YoFdFxuc}ijnti0RqBsi6QO`#*h(1+!-8gi6QO`z_`$gkxs|MPXH_+?hFpZ#1MA| z*FZxRpEs2|wWImVn3$?&eNFZ0SQ0g;F*BP3vL8X zK;#8C{U#vtg2QS8A}_f2HUW_r++v&Xd;JSSZdw8&Ff?%qj7SU(Mid0%6bv_@5NV*n(oh8PX&?~=Un6@IxL-2};Yq3kX~K~_Aj#$w?wkxLL?l2` zq9TZA17SXRDB{}C@X}ZW@oi`zMG*ZX%mAYaD1;gCF`&qf7;Xd%C^9n;SRsIiA`T7> zz5^(P8K3}wLYM(Y5-cOgx{QVw%OZ%ALj&yq6f!a(iUey6gc;6{WQ91s({1|d#txOFC= z5N3d;gXI#!42}T7B*G0CF+d^AfO!BYgc&>#6CCW59tuViPzW<%k^l-}2AEL5C&COE zX+R;&fEffRgc-2dhQO)};RZ+nPzW<%egO($2DmdoA%TW225(ON<^3e zUjY=t3{VL`AEMfxyfFDUdNXh!l7|a2wnHo7D<0OGA+EJ!W(i4+(MG~z$ul{=u2kpVF&Sof3F3j-TnQUi1N zd$7@kjU?#}U><{{06a4A`Uggv0VFasu+b$oi6&mdf`=l06ayPwQe$J{HJBO1lftb; z0EI9E8(lQwO5tI;OVql|7>TXHA z1~Y@WRM4XWJQRt6;9eAfLYRS#E~#T6{5@cB07X(@2#{cCNFDBo7XdRE#HoT_477kk zMg}&zq<&|_I{`DKCSpViFhgq3MWn#UU=W`QXEy+aFay@}BS17=8v8^E1#eu#WxfJnj2AW;&` z2YHt}@fyqwB2aLo6Fd_UD13Ai?7=`J3a6IHn}~^DgqcAk3jYA|c31csY;=F;ZNSk* zxVAtZL-@ym>=s#|a8ioAqmcMLm>EQ%@b4CderIpslofez8Sz#ir{DP-ENpatCve~) z0$DP$MB&pefFi3G7B;%SQ#kPOkvEyZ&qTpuLIesQB@x9!NSFkVEeHk3x{ih8-rtEF zEFAa#&g8&pGC+}yH5QI^Nx2;O6>y|W&e(v`CaKnmBUx6MHa4>}un|6K2Fhx(2Kts) z4It!nBUnpeQU5;QIQFtClSeT)DSsulFLTQe^Jt#~d_VlQ25+^I-_+W~7A` zN#Do^E`MFbl^E;V?=$@^ zx+_0gN>?mb=1UiKSD^1JKfmjiEElhgl&<)v*ec8jHZzng&#x?wLJOA5ze-nragOUQ zk7O;@NPW3IEzG*#<9q4Kdq@*noQ8glcCYv>&crTv({NYld2QqTDP_{M(zZOOJI%Vd zKe?yt_VJxLXl^`rCqR(@zKDjm*aRBeD3#!EkaGBVXI4N$W& z85tZFP3urr=*C$tl;ZjvBgRxGMlY7iJgfV;Eqit*7S|_Pf6|g0YMJT(H6!me)uOy$ zH)OdIBKy=}Y~J-yZDlkK)QbAP0xg#{)mG$7Ep?ms<}*>bme8k~=gtgvC=11x_Whb% zZivN)_J`~Zs(S%N*WE2$p4L70erhzU!{Arpl4JSfQSim(>B4S%@5vn}qjg1n7Nc$H zXT82GM9bvRcjk4JBq#2+)pB!O%=bxnk>7#);CMBCDZ}2qzpYASfdJsTmvlwHLt6t)5?U*HMxFd&NFY6imWA~d{ zm}5`txWq5r?VV(eA;Q!)sp^UGN$?qKI6g| zhk^=IZ^_0akB<7N?8)MDQ;uo;&tFWMW`~_odF+fj7Sr9<3fvvx)G=_kTFDN2spV%(AB_Ffi?)4W60W=)$og83 zyV)#U_VeJ4rLOYo!hz9`k(y8MseZYh;CO~gynHr^FnRoSV2`KZ{;cks$NL-nlDaBy zrr*5*S=_uQDr1oViM$codFNvClS?mlY{5lDzkgj-bnkYr_&1}+#haeXwVOxi-H_kk z$~q?Ko&ELz!=2lu6;DfD=8FcT_jKFI{^Z}5{^VA4R1@AX$L&)~McaPOza5Q;6)p%Ye5vPNVPBq=)fxB`n?35=5ffu- z75&ZhN#;h6CtvML#x>dBL(vna;hAsU$A_Ks6yqPVLhKX%(wRB#*$vLQiZQVY`O#x# zF%r(;-}mlPZI>o%p`YdH9aXbv)$xXoz**;*%nbcPwUV{(W9ecu@Ey_XT@X5 zFd&C>bf)}itjWU6ue@n5LQ}<|S#Ea)v2nh3Tn!VAV(*cUrp%+QwbK_Y^~X%Nj|~sk znibnm^gPcPKCfGBY4mg8c7U={S7GbOK(_znSOqJdgRQ7j9%ACt)o3mIQEoM?)mmF& zZ}^G7Na>SvtAdp_-#3ZSVM3?!Q@Vt4GhL(Qof}ikT6LaQ1<9mevKd-zSH!pR<;iEM z#Yd}Jgt(;ymSlZBK&Trm9T?@cRi=0Eu9KJU9{8CSYdN*7UgvneUFgmdV_|O1sO)6X zhIq!yHHTyhkLy)2;NQaC>u7*dvHH|SXxuSvU}29-S{x^VBbx z&#pW|H%PMd&2sB`vLz>9$AE6&r!3w!jKvmlZC;R^?! zp~^}Rnm3JRomzp4ENf3hPd^NZKFaRxwj#|}pV%?)74Txoc%|#PdTGG(!_{&nN2gh4 z>S=UyIip$xwuxWRVtY6y!Q9Wf^}P7wb8L}{Pd^v%6WFk2p9b=eEJrQS^i8Lv3~bTb94lT88oXFec5BP0HrqgZA-aoTW4@*_IQZG!7hznXPx= zEVYSz-gZVbXCtR|zD6Nk#-92L_UP?}hBn(XuC_Y18obo8M2RH5JFFY@!Mf;Nr-DhV zYQWcUTWHX(@vs348}zJM*3^dI%*9iC;?lGO)BD3a#&&+W^l>Bgr$}K@3no{48g|xx zL+9t`vZYe^nesw&B6yxN{;iq@^=D3(#hGrlSs&1r+`A51>)C|kjfI-c59CGVMd z*{m^1nr>IW_HcU637qFrwTJFk!Ti|6u2Y;JrxQ;bmt zv&CREtVNN<%(h@gMP{t+^@75k$gk9=Hg)8jk7A3glkiElZo|mthj$o#=^HS_&9Xkr z4cKU78~!;r;JC7ASlNexz9A|W>FmqVM@@wqW5m!Q{Z8T=n)r{Li(lYbhW>d!Pw=+YH zXuhG@n=6@N9rcUspYbilcZK~@ybX0(I_8(SyKAe5{TzNa?I?QEdZD7DS=+Ssapp@g zhl6=ZW-;Q!3%4gNyA8%)3P#mEOk<@X=-yVtS=e&CxLo6Pxgrh=v2t=NFgk{Z^mj75 zvU%~N*t zcB_h$mtaT9k68&;Z<`-FzTIencT233mv%%oldGI2Q}NrS`#y(`p7_C=-`RI0X?|a? z<-28TCW-I7y7^W1Z^xkgTI*fxbBzv0h1pJBS1ZOHYzM8ke_C$Z<8B*fQ(E33-(q>C zk>~DKmxuWd=03MvwTC=jckBGhif(i0^HRiiO8DO&ZYr&Ve0V!hQ{N&j(lmPt?UEp-bm2KlPm*@m5r#f;G}bS(BRJdlaWx!jOCNke_{ zaKQa(k6gd#J0pgZNkzVZP)rLMf;!RnbOx?p>>d?+Ep}T+Q@uoubIEy%tHY$MTY4Y;oACE%6gZZ@$RpW zcGRBL%Mr5g)jIp8)h%LBrvztkT{!nU;bC~Do@_{;b5|Wq{-tP??cTt~u8FmpH*YQF%)YPEmY~+)B*3Jb#m~(Ib3NTG5w@El4u_fTC zSebS~RIpxOe7kntSBo>pmzCmv#7ykQUF7e;9@SUdIi^?M+vTe$=ft5Sx}{bIZ<5@} zHCw1?QkBM|aR2F-D;+Z$9T%ZxLSB}l+|z!2z9$ini$ZT!d}nr(YF08?IdX1OvF9Cp z>nC}cr|9#++UKG?xEwu{9U_0VByoI+JYasgaa&B8L*$d=mp`b9AI#HBZfI7-S8hJC zkkhFt6)<(NsK&+Y*B9fRJZjBJKo|AJ0M3RYwM_3 zf$PyO{VW&DwGTM4o%bu3#@cP`NZZ+izC~zH+Tk|Wr0_!jn-2A;qq(m}M}it>kLqaI zyUOHk5`l6_l{+x|Y(_S8=UH^V+i|pAjj2arJDZI`Bb7&x^HuH>7*Bbo%~9K|n3|7R zE-s}E2b8`XDw^679UW_@J>QZK>Ef6B3t9_VTI`|2?e-m<8Lav5V|=@_={vL`^A?8| zsjZKr&7OA7@D*RuSSr(OQOt5mvhA_2)4k)OJze+4hGtqfe9``Nrued$(!!oBv5ea2 zQ{NW7OKXSiUrXKo%6@9Os7JWos&g`huF%5qQiRXbLZLZ%UTw#Pkf2BJ9zutg?Q?{k zBfdIh3)dSLTX&A`EZLb;@IK4cUG8dBpF@_^?Sn#Emfg>~M?DU|E9xE{-PW5aIcb0Q zW7gK;qL!=-YjBRrsw#{s++`7uufpSeI5M-OeV^xoQto;L4Xc zAVxLqv3sR?7Z>#sU+DHRHEKH+45x^>>S+9|Ew7+o=HVIT+qe&kW|WdH%B)ixGlPVr|_Sjn|ne>`>A1*E1;+y=7OZbA6P3$+m>{g0;b>voS|260v?XYI=)(O^2ZHgU?o|_f*#nA;x{hz0Rm{SDbot98TMkT&XpT-53MGyP z9}&*1c>QIxy*TrDDTkuYgk!R9(vz{d(K81wwoKSF#WMZS7IQ6as(bITFfv$H>Rgj7 zYZh6SWN}HL@~!93q*s^D)T9kxN{8y=wrhv~tZokJvP)AC|@kT zr$mkZGxMlTm+p~)k)m$(nu@rejn<`USy5+P(+g9kTswPWct&&yzg9-Y+F)ieI`q zzt4Yfm;V4S6dsamnO;U7mP12cI2VOfjotS8snY8x(%gBcIEm zyEj1Q`!|$DWoq~(Yoho0LYGm`?24yEG(qOOGC{D|7+@#sCa<=`9^TWKI zCBcV_s-9RDhgMZD{F-lB`32=}s}GegW;!l?1FzbEGOYYK91%aS4P_|GW=6OaOqnOv z=9=7?b$BS9IlzkbUpcsW@tCw8YJ^2FJbEP6;lUfWq-V_!wz6TnghmTmJY7aoL^sS{ zb}9E&o=RC>TG1(8_E{MYplvO?3jK&MzUr{>z|ZpYd;2R5k!lQW^!jI@#`sF{z&x(9 zP@&>WxAz(5RZm!ON4}8^6TkQ&_-RL5W?Srb8iNy@(D5xiHa5n#HX-ykzP&wi+CjR5 z;aWD2h9UZJ`@v9lyO7uu@g7$W>fL#8R7mS^mRD7`Z_PFJMz@-;Z#%E4W$T-?mmM3t z{Zm+FQBhdYrPkuCu?7`YzBO2X8=$5VsF0^b{t^QNEDR>@SX zSBeZ^MkP;1@EVl%)*j~l_8}2_-$`VIvx8P!;%dl{k+Aa@$7SQ#eonEnPk~sJErG9b zyqB|SDTrFU>_YoS5r6HijXyjb%4ToWFzCj;Ryyw!*+$o~=q5I?mGPMI#v#_v+Ex$?AAIkC6yBc|s5g&EzB zE$DvJYeyvSxxM<1mW9nSNL|xyuHQTXB8W=1tn*n;5&j!oBvy9hJd>>)Uy^%mvHR zGsj6zTFEL0%NjObPU}&B!0gC#bQ?Nd^eBoguChN7$Hgs&Rk*p`O0{x8Fhm=YnKVjI zdSe<=!|o@t53jJzIC+ov>BZI$?4R{3JBD`3_8oZh8CRv_XCZmJIOf3KUhyUBxLwhL zTPt44_9V!a2FW@kcK618CvceL-NarV(GW_iGR{Zu5-Az}mGR(^)6ruX2Z7e`mUYSK z)*JoXSXk|$Jx=R7Emi^y<1@Ua3PoHKQulY4tlT-3uLFh;r0 z`HSq$ydC)V*$ICa_d#qQHi~B2tXj#PQaXb-V!1l#*;``w zln(jtvpT~laISWr$8EI2df7Qb+QNIQ=)8-;>|<>vO}rfH_wm%brgU38d0*5(5iHMo z?`r6R5`bG>;QnICLh~^~pIou?+)2r!Q*2hT@0T9OTCMYIQn0i>Mi;Ap-ESkoTjDv! zp-rG>1kd;}7D{0IzK>z!&p1^M@Y|Sm!bQvJYu?$=tRJXY)=IXv@$Zl>If+@~{@_{R zwu^~|e&3ajkb2>KKJ$kU(Yn?FkW+o0<7896VE&G}Hi^hNecn{t7l*0uSRPFY`Sd}8 z-}ChYDep~$vyw@V=Fc^9DVPdIxZ;$HcW&9(cQBewn458fY_LOeWh8Cz33rjPtzo$; z8f|5p=_lJmuUxA6u=i@hr(?}gO?7ANSv>|#@Lv2vJf=;@L{fOoow4#K%TGLj3~dsr zSs$>RHF%?rzDs+n5_@?IU#|4xowT6V8wt*`${juaLp+^{J5a|3QLc&YFLh@6PsV4} zD?n$^8jxJXvi=RH#Fk$)1{e1(9Wm0W^hvqB?YrJ5WsGWxP}5w*LHD5BEJ=Pbx4nkb zhoe{rEbs9|IOJ)WjS=1nTVG1eF^ArbYnNLlrw_6QYrWhdHGOv}q2VXa(4g#YuZ=p7 zorn3Pr16;Eh2-+ts}=U_Z$FoJobK?)fBk-|#i>Jf!f5g8LfS99`Ph^3x)p(rthZfZ z5x-J8pl#WfD{`N^4Sc*)dz84Z5+o11r|i_`;I^PUwpqyAI%liDUCz?5ZA-Ealwh7a z{*r0t*BjGU=Dc%i9@#6slKO9QyX*LOCbcqUyf-9ndZRRE``M@mz7F3lHZGzB zI4Z>2Pj>3;vG&x{o4`m!^T&l)s^`#{*%hD6T;F1SVCI|Kj9kM0>s&j6m^wlV=1%%L zvwqt@ZrPPOjMv$yW9uw3%6CS#p5+Bpv5wR3PUQ7kb7A=^g~g(V=iQ>bfhVFm7|J)$ zm#Q7KPM8pWpXVu?=Ggu`RNUTUZrlEP+!clH!Z}m@;S+uNAwPed%$aiwc_}ehk87Xp z3UG1n745>^Ti};!TVZq>dbwm7yL+9((>uk}-PtQ1mpaC0nbdOkE7h7e=Q&3E7en=V z=B=s2-rC$T3oa$j9qwxUifJ_q`pM#b!+{IeSj}B0PfC`4W3h^=>I{u#U-`K_3pH@J zDXQFcpTM_Woa&|%QhrxhxHK&yDsI!Bwvh4h>uajcYho$f%N?8xr`nj_>2$Q<9vOct z!=M+w&wLH>7Hs%%=c8(S?86#wQNgPas};AoWl3tw?_ueZZ98Tuu&zHu$aur+fjtoc zCo|<-F0OQkPwm|R+5C#(p)qe7H{f^U^HDE6U7{W1@2X;+R8zasUM`>Pe(uBD@fyBy zpT&-HVW()NbeeBAEG?emBQDcQl2YHx}iY^-}xv2HqKr{@ny3KhQmaDCo% zk?1rCs4ZGq`R*4#u#)-e|TgpLp|t3g?t*nacG@iNw#pKnZG znD3uMdpIojcXY(Q;fp@qDO7bP)H_$K;gm9;>i!tD%H_i{N{L5GMtOv^Z*%gN3CO>W zJ-?x9{Pt-pGnXG8tr|)^4Umzyr%26)QO)x)30JuLkD%mB-fW*WY#61EV^0VOsw~Lg z>T+N1h;C-}ggxKR>@ktOwo}hFZw!30KQ494?L}J^P3Fxsm+*`saYj``xvH${uZlJj z&u>?qWm$gHn;5UUPHkgV;6T2Nr-5H;-u2_JTNn*aRM%Y}-s}}OAZ>#lg=X|V)jd3Y z!D}w!OG3=)hC^Em&gH!+q}@#8)tOw?*Lo%Lw)Rk8T-!~4i<2IlG{vpVtOC!|*QMV3 zJU?CkG>iS-qa^zAYhPt$qJ6HUjd2&VTF*0~yF#xBM4D4gaP?=fRdx=y@Nz|(~LFhJzafG>Sn z$Dv(seQ`2o&pM%=1^PLojvRdsAE}U^SKgVOFSJqh>!UgFgb(7Jp<>#jIw=aVh^6or zr%&Bf=-mENS?ThEQh>>olJZb0Kl4gOrGn&f#%%F~kfETeM-hGs-yDN-Wg~*0waiTN z3qGq=I5We^y6c;M|15*o2K^W1KOSYjQ{$tLsgq7TvXN&0ft|+&hOVx(oTH6vW==Gi zW+~p10do5{mG(J@DEvGX;_>D<^FC-H2G8yOG5WU4_Rk4ZdsO##lq=MGs-%24@z}RD zXJ^mmeNlI+g?x&ucv2lM#?>k(*%|m1&5U(Et<&LL0qUt(PJb1ry9?&P9R zy3TaBVg)zMH!JeGWtMlNdjsd0n?6l^gYoNxi8}Z1N2Y(9Sb~8(d`@6!d!oA6`1|T$tmRt$!Y+6g+in z*rs-Z(=^;f{VFapH&X3IvirMW-nm1#L9e#)`x>4Z4k8;)v*x^gRJ6a#jCK1*!h^)* zt#3C~b)~#J;fg9h#zk**TA+&Np~H=vb@6UzZic-qyc3vcgosL0!&o_do()WEIYpgHw;$8l67 zN+UXZyqaqSZ{L|(yO%e=$UjMmp7nvpi8csYY`w|++;t#uV|&bQ`{$h3(Bo%MWG1E` z7f@!$@N(^$2-U^9c3nZAdo)@l!Oy;5M)X$hhKSO-U$=MnvHkeYvDLkS|2=zj*xm;R zkNfsZ3|jcDg-ePnr@;#oVAcU zX&tv=Z^^a9PYUDLzX!1&tJ-%_<>HeewX^yd6Cm3wK4WMYe`=g3?U%L-o+@FtmW=$l!m4u3*({z=JuLs@ zy4iNI5vXV5F4j(STXCFiS=fg3?0b6c$vde4_(w5R!Z(_*v)96Vg< z`k1tKhotMbsa?zSqYvnUUj2 z2y3=#Y@lGZ$D_}-R}~JUk_s)yCs4P8)_)T^xDxs)>GZbU&#s%CiM|mLr7QO?-o&!j zYOjHojR<|=##dV`Z}=YE?SJyR^yUPyiaiD>#_)HK-$fOC6u|0S)ipAqtvu<188>>f zS*j!I!pvggd8nqrDS`Ew?D!+O0~-R}Fmvx8h_I-o+q^k?V9OpyeUqKyC#V*Ko0-qO z_)@i?qB|mcC#uZ%>ZgQ_kJLN5597vVRq)Ih6<3Cm_;g|CUkjV)j;Y>Nj&58K5zBpH z>$b;ePg0+cLDKZV*>sagmCTe^m`g!%o0!7%&uW>rYt7INL2_^Q=NES7c#5jfPS(<( zrjLKHXxg*OPGIMaim^Q>Iy>&r~eUh-%N_Om%Z)Q_) zYpU%Bxf5wYSLivspE-Ru&(->j+xlp?y6#p#u?5vP5a%2f>x1hY>x_aVA{w8#s#*3k z^vODCQr%5%jl1s)J!*BnhW>D<+U7*PktJ=R^5w7U@!eb}1aQe(cMInS_v{jqt_!l$ zNT!bnJ9lufwFIR9M}*30)cmYOz7Lde-P8!rxgPv@-}sLCJ{JN5vyDo7gq*>`N8WE6 z4ykU{KcNam{XmyRZj~slkog{RqcSh0PfP9I`Af+Uh8YcNgziEE%W;n?!++4o+TNdw zJj%%}w3Oc~U7z5XP=cD^xA%LUz{1-#c&OqX$2o8Ou}uU)qoeD6ug8QI$i9|6#z1JI z`l`Wl!jpCLUhxTa&p4GmR&r@it9CVSG~7tomu~1=+{*5b8Tla3P?q|PsX^$*#V-e; zG}m>hWewV5gGmO8&mayv&R$dmyN|=fjUSh45A?>>i^m%mEXYZP9RFN7`M_{KDWu5p z{tk|mAFY^E4$22U++Z$q&$JmYeG)Y9klS1G5egMu>EPGLov^}son=Pv6*FSCg|y1e zN*5DQLyOWhbDLDI>J6xI7|-2V49PjmPUqEAGcF$;lgi-ikR<~}zgz$4%08-4`kPue zGfuqZoGYY@I#>|wDCXDCCep|A;>v#gq>Ff~6~iIB`h$#o%6Qgoo_1#OgEgX62ZA0~ zy^N>UvB}o><~i0tt-~ujr}3cn1b35?qRhA9vHPm+pLmq;`&BI?82p@k6*%8bo*uAs z+HJ{lnB$W!-_Zc{p1qLcS@rU7EM|=zBa5CJnuAfdykykfB_n3^nQ>Gy^EwS0%a1}_ zBb7pBw6GI;HBa5c)-^nwx>G-X#yU?fh_PV5aM3$xi@w2g*Fc+-4peB5qbxI2G;Iuq zj<)+=?VZhtvs^c3a&0pqY+TqO<=r_2?PB8!U5TMr+5(Q_fjmOI&_TP{U=8O@dZhyV zudaT7b6n9Es}TC>e4FX~=-C4Mou6ssPj4#|^$H+Z)1{h){iOTwO3fH|%lavU|Jhvu z&&PfS9MQ11U6-LDJ(PaNt59_xhvKg-G*#z*>7?RXl>PSYDyt#9`V`5-lz4WaUHC$# zgkSvCU3a)eiW7qrgxX_|K;!$4q>4*EQFUat!+#On(eyD=11nN*yXCluZ}24k-jN$+ zjW2_LZc=~BMtJl}i)L8Vtp`Q? zayvqjWtw^Y7z1waew~{5gzH(JDjnBr)iS|!Go?3a!KpcpS^D>VuJT>q1r47byYY}g zdM?LfpI5o7^Qn0+p^3d;c1fiNZBN~KB4z1>&#rea6GBcKujj@dE6jF_{<^$=r^as9 z6Z=Z-brtk82RJ-^WTb|9u4v?iqQ&W?x6$60eHx%}`ssT=mQx4bGS@f0Rp*JVTt{Vk zr?o=!+!PgGF2mU7*Nk+Vh1xGPPe2D;^vsT+XSYl-ezPnb8DeXVe<|+v@PV1bcEj!I z!MgDiXxR_PE=gx%EOj?DT-sKxH1g)=#dS`PrWEPKIyDC*%MPZpQ= zfYI}Hmwv{wO5QUnurU1bCR#}M@@~wP#8>xnxgB-xWRumMs9(+a@SA2Qbne*FH#_iarZbcsb^yO8L6jRUs2_AjV)*H4|Hx#sZ z>FZOLyF8CZzN+6;nIx_=xz16m&2pA~1BA`Bz1)J!x68=5FjT)7YZdlJWBld*Dv6`j z=Nv9?wqq&ms1&rz+&?Zo)wxao)OGt=X|JFaPJR`Mc@>F>dS;#{eW^D55^M_xG^{<1 z4_vpBzel$tJIrP*nJ*-lH}$ly0qq{&OP-1M8R+!-(w+CbNITyUlY7gfabM=TUHn4r z&~dFf4cf0xhYx*}&0uq^ezQ*g^MRt!GrofZ482^}+ZEXD6ZmChKb!dQ9rBFe7(Dec zZlmG(sZWfVpX@hX*d9})f8$;)=iT!?{QAxLwp3Fz?ESVuPd6?e_6r)xJ)?O;;6xbg`b~%D)gt$&o9dgl9FAkEJyTXO@ILh|p7PKF`?&NKObwM_Qv3u1!*3%A}70ayW zycXQPYtuKnT>l$BAzV;e{q@ZX69?xUB43FvK|e#{dP4{0Yl1#%oI}x{wGQ%plwDmF zt%2T?%AYs*(So;E!5E)?+N>2_F>dZkM*$DzyiE~o9oURO*gTn%p3pSk)#mdokH zlcpmAcemPkW|~c&>Mzkt5bB8=NMcvht1rcz>54WPh~zn;L{RO;L^*<&%tIB=#Uo3v9s6c_C~rl1*v&&F&zUQ*-@2c z*O#_6DJ{|`9qRJSxbGyebxY-?C&!Y!*YRItS^P2I*iRU7eZ!@}I>Atz_3ecDtf>E) zY4fVH3fDzF3zaM1(r%ZEnA>%fCEeZq?%f8y8J;t^^3yTUt!p?d*Rx93+F$waIPzY1 z=<~^AFZWHeiv_y(ESbydS#fqT+z!iaR{OfYgE3&D^I)cmHrvVPIY&x+cxW46RDX)i zue))crsvnuuP0v2-w1GXAIE;hWzm>^Q+OTsLab!iz-5BLK z;FD=&ZR+*f0sZyl=hB}2mb$J1MRlVd^fBLMAx_))P~|Dv=l)6gs1=Twg4$_%<P9%O?+=fy<5+>RYEw)s63Ghh~j$REqRa_7C4DH9M0kK-+9#!fMAo!TI!F{$0sa zohkPD8|wDHdEDI4wG|h3_m^Ero4z}zt z|DM3F;|%KjEaNL)bgeJW;#v-tW<1$^^utiCwMfl0N;4&W_`*`Aok)GcTz`p8S)Xmj zt7~fU;$b5Z)0G1lPXtB*!+cVTP#k_ZQKV>r6aj!O4-`cA=w6y2M+u7%K zd3&#F8NE)rXC62xLL*&%bcyl7&k+Ngljk4(s!{h}s_Z(<`B)wmtgFW$tvmb0`mThD zwIOX&j-Y5EzqZzelV?l3t7#HUsC$BF)F8tglYL$NNy`CN=!q-JbgBd+V||O3DIC zE_*(DCl@0+zG3GMR;P@{O&`|(GScu6UsUla@W0973hk)-aiLi(Y|OFeMM`za>4<&L zXuoA%(zB6EedMm!vcIP%0cuTH*K>~Va_D?CI;Jtc#7et#k1cb^!7*Px776Md0bX9v z-84M!7Y@Y*=3T}+x~|0klH)w&%qjhSBwc_(*@$_U(RSC5zO_wbnW)H!nlIYiE_@km zmV%ofH^qD#`=FD!2-aMgZhn2mNBD!{k5M%X6SC9KE(vblyncnb<;)Dkk|rm#ahr9= zeO##H_8x8Ps|FH@VGYbYCdYiM-NjF8Yjg0nB|0g{oJ(WS*!q#{*WsTz+yz(m1V?X3 zT+fa_|H@u{9sBpx0~fMgzgc5euyWf8+acvU5072D-IKR&|9Tq_%%G8ln}Yw`L@tX| z`rujLCv@giqb~LfNuHRn8^_Nf$i)F~{ZF`k}IaWFcEu z+iYNbmiG=cyH|H!SmV3`(*r^0eXkQ#9Srw{al}g=zng!oC*9{uoKobSg6;GN=lH)T z>G&keLAN80S08XPB+ z**U_KF~%EM`!@S-vpm-3=+GtO_+-mVXd-;R*48)ZcC@R;HUk(O4_*u;907Rg=KzO;2B<84JV=yD(V-c&ZBK z(buh6d*!Y7?h{SzVv@a57Jr05pTW~4bf|62PT7B|Nu|S}sCcp5Jjuj4>{_7D_6q37 z%)kLtZXWY})JAEGC3cb>ou{;o!edVAx9PO3)0PY#7)94S+T+OJ#f8>ZQ&?8q^gKr| zO_R#UYae@LL+jU8BTw(E+waLnHQL!-k7*+Oe7!-!UPLYUhmLQtN7HKs|FNTylkPQ_ zs|3|fy9U4An0TNzQ0$gl$eCKc!LVr0da4`; ziiYx?bjmvPd)uc@UcM>zEZQ+{X0Opkmz$3(HrV|?uHGrSvS?cuj$N^Btk`D7wr$&5 zaZ)iWw(V4G+qRR6?Yi0f+#l^c&DYsRA7hUGsZS+aQe_E)@YJ^BZd~JambolM17Va^ z;P1l&VnjkL>`=WmIpLajc2C@ctn0>Q34+41XQMaBx;;fMkCcj{uz0P$tZnq%vkpXI2!lU+ zrwK~%{9e6Vm1-=ZBznx}sJ(vSBSVTxv0$=WH(A*VPyt)Nzf6o#q+cTfX@w*M#PMNz zlKG76V|~`EsBqG(xep);Pqwb=I-?V^2Jf0*8g-C;yTwk(@+Yw<@K4c3vLFlFTC6i~ zJjYdL~#t$9%cwsdC1stOq$#C z{&-z^*j!(cR1Ep2GTwdi^``tV(7O7;5;n&PQ(}KAmguptZ4w<(1t3vF)C^YBk`xe| zqC4u9+1Bcpil5f^H=F9{NM4=Q(;CL=LJ}hg?^jRU$NY%URc$=FRkL!SK39Kp!jr6V zo(Owe>o$WmVhdAFLt-Xc_I~Kj&y`lKi6b&5a1*Y^rK>L(23aF|@;a?D;6!3N%2q!F zwdTt5e&m|=y|!6o7e;sbi!tOq*o%!DBPdp;D|BX?)Y;)mozzVNoB+-H6@x7RCui7f zh84+}g(fJAS!OV(I(}|nPRn}@To>nvu&9-k6Q5rxw|<>*rl!se(lmeS73XvQ2Tsnb>}oFIqMeuXGd zb+OlQToIR?pyGg{iFAbDa!Zw~C^BB&QMNb+&461_ zcZ@asdjux6e9H>ZP4^8nJ8a&(BkFSJy$a+Xl=CqS0_c`WS&-Y2m#CAMME z;f4qM93BkM{-i;p3XM!G=S(^n`B3?6)ZO{({o}7WP6}Fk* zk(;Ng;c43D#&s&yJ9S{A({TeQmjA3ZeBZs7We>{lQn3h7(g&KirtX@_Bhb0wY?j)TL{$an4}zVi4glRP564 zWO7*DZR|pUFV-dKsK=4%MeIXmp%61Pq_$h=PqYDTRypQ`l1gM(_Uk${88em`qcFzV z5FIhCL4otjWFQno z^e{h7zJP;CA63Rd`B6ln)~9U_5n)(;yqclU!^mVRJ@$Rq94a z+YJ@xVbb7A9$8Jv7edei7n`b^C+v|)^Q>4GCIJ!v#g_iVNCV6bnib{|u*Rj^eFXw~g`?cvficY^~jm z`XRpi|4Px&{SuG$YhD@w^ENLWwwrDz(f}ngQ=&@Cb;L@kZ1T1&aHbkTqPWk7^KO*B zBh7UIHqg0$cL2qOSexW43yChQT7}#|OuuaOOr}oDt_3f(KXnXq`)*ZAml@jga>o0i zEbwrL7KaKE?h<$>UNroAAJLL2#&B~JS2R9A5w|O88zDf1$HAD!SGlE|)0HAO~!ofNpSU=<};gV6lg0<+r=AuZnAmr&crPsSy>j zDdj8=f4EH(UQj863I159(&-jiGKaz@MGC-mi|Goo>q@sSu6CN7!_@g*=UMHamInc zWK1HWqzbxz=@X2PK7QHpx_-&RNJJ87LV4W*9|>A|$+!~}e|oqMxf#FC1tKhNfk#0- zugOJnTJy`>jBR?oGjL#_-rx>HB5iz_K`6A7R5EfcXmlSRMglmV^!8TV`RARmY)8f?I5)1=q?xYM#VJYfd zR?vV1)z%RxZWSDMPl=UL_U#Kf8?$CLFoP7p{+2M-jM#0;RI59j#*)n>ZX~SqaNFW( zz+BSO^?q|JnD?cD{2*g#7N#cI2M4qCQ;M0Ai$|oWw>TSCq49W16{_kj6aI%t%vo(< z;q1%yFIquzq?pTYx7HS@6s?xx7%r@Ai~=Y-Cj3C=U|6>Ww2qCI+)?S9xRdT`Bbk3j zM}gh@#YG9E2Tk+w&dJkpz5i3Y3WV7tp5?-J+TH+}K1JQK$2r2F&au?8$|^zVd3$x- z)aZwCbq(S8FuoHa0H>T)4lR<8;XCu{eO0@Ri7tvS%gdH-jJ^Znkh@pY{z|PaRyOG2 zfKL$dds{`eaSmpMwL-e?6D3QED8I@$Tyb~XE9#dKL>__ZXHmj3qvya_8bBM9<(`ur zLKUbtcc?-gV#O(c9>R~wjTeVmgd%(|^b%yg7V+EY{JU2e+H4XCLb8zg5$S-bv@a_e z@M1tl9Zno*p5_j`d*a|&NmIyQj^Rr1qh&rMMf1_Z$v7!vKuNhow1@U&|gO4Jp>%T*)DJ@f&aP;B-+EqS`$~}Tz z`5_?otJLN2(@pd(98RB;bY;)6w>&%A#Za=}Kc)?Do?WEj}N}Mvw(J9~k!L;W+4?;SpO#T|{z}*}Sps zQGF-A>0wG{(BTo&etG{0$;lV^4u~iFBUm|)aku@X^vz#De)AXW-~5ICv(1gFbkVcd zev-eLTQsn+0*6WjPE76x%j+0nkNWI7 zYlBJNc#a!9)qLPf%BRNziPlOJxdeH8zZe40sQXZP=g$#w?jfPbKfqcho3@c{3y5U zgh0B0&7;g)`T4K?C{O=A_D%Mr|5&H|y*{@m=1y}5gT<*-1RrXmm&k8MpIdTeS%m-U z|0E76>mvSlTyVumD=}$NxJOCf-?Lg4Q7pf`%3)k8REApFzU?=^OSswNNn8Ux=1BXJ z`v;V9{p&o&+y9Vzmpv7_(Vr4DUi2CR0{DpJZ4G;n&Ng&JO=bhdC_urC$VrOqb-r-F zumHVCIYjtQ2v3x9YH0P5<7-8-_~pxWUWv>pPMuRFe`jc&L#@>F%Rw9^3CB;5>nfzq zR!R!BNyda)sX+ZL&ArIuCYi>lqq0Yb>XloiQ|xXKmDatCOi=kQWQ*P;!d$z05{OCq zkbs?Ro?&B?gU);!Y|YkL=FJILa$DY&iGL|TkVIJfke8}T^|y&KpNa!gzFmD$77beA z!)|$|KPgFK+azCs$Bf%TT4+u?{M?^T+82#KoF|*iS>B`%*&BerVug#i)T=D}%I)1Z z)OABo6#dSlKpO8c`B&tgaDH6N3RuL%DvFf1A&(Q+D!eMGqGTnPhltwxw0aYfWMC^~ zLK6Va!;bCQpekXP704poC#Ti{w#$N+?UKiBS=!k5@DN^5MQD#EhOA3FN-GOUi%@^W zbZ&U-XFB9x^meIXX-+bgDhi?sr}>RA*%z!OD3dsg+c_ed>pG@ftthO04lKzizL-)$ z0b`v_%U?(=dLrYjbZhvK`my6H)NV&~p0 zSQae{bF$m|#bGhDYNDJAlW6TbAu<|kEAw}$h`P+qfz2o>?h8{+7n!FEtkgIRBwCo3 zqQVOR<^a$Y$66vjLc1A|2L^g-l<-MC)2WR2f)Wq0Om#h)i1|G5tLbGVnHt8X?1D@A zKg}=*19Kn0`z?0QxL*PpQ(5OClBaCX_Eu)B7QOP+&+5b!ij@r&I%KGNT8fk(*^x4A zU}?gWVY^>kfM+1ln^_KDpY@v0mz6M#p;5k36Ok@Ge3g%nof0u3rq>)#$0ShKQyTe^ zANum8EcmzfpQ1$xAj=yB?E3`#Xr(Ixsk#dOWln``Cn0>dz z*4H&zPt_oFT&|;K+;`m_|MHefXNbi~9RcCJcYBKkK*= z;DLJQ6-JY)cBy6I?%AzhMOo-jP+d1QrMM>9zKuBcfb8dGD6moWBv?hW? zneuJNVZflH)Kqw546K@#xSDGD_&+PxVfW3JZ*o98hA6rA1JlG0lP_dY;>dK)msVxr zeZp(#CGrA~B}L__1uJX2yvaAavHlGcBDBY9H1cKCh2KZ;Zo-(^$yhZ1G?y5Osf6r>l~6M>7Tf%7i4C?6@636pM*D?wqIpPexQ;WaUCSEx)aoboo+8 zu1$hZnyR5x7wORiAfrFMTp=U8u=)V{(#0od9Iv$|!XH3<)>kNN^1j-1o_H~7G`boD z2}j*SSktTGHT3h^%Wc@A{-+RS8a_(mQS#x<1T70ckUGMM#YVtd&5r0dhZ)Zcs%#z^ z;#r1-Ms#i*wEvJ=$~<;?&SIcq!U3YiqsxJFfj<@&ix)yfz- zoi%oAad|2upp5I(3mQJqe{x(d9ecbmxr}6=-p?^`M4%RIe?8o-N;cnoh7IXtp2UgB zrwlOb_}FA~^iwHra3h6G^0<+b@U=$-9{qMYR|PEs?FVr~Beb=K|8NA=@10ryd40P? z<+tI&u(m~Q41g9fRb4UVjWvp=E=8bMB)Y6C036sv>z4rx)bcMasp4P<$-X!Fl}w!y zo_9ddENFWFsz;u5;}&b1o*f(>d?|}n@RKDAZngViW3q6lV7pg$#RV6@Miv%Gf-63y zU0|#2#(o~RHWn^Crv!`&%N&JBbDBWbg)vO7&`#u|N%JJ;fl3J`Bj*i<3F*CnNG88? zHP>&hN*mW&$r?9KvepXbJJemsKV7R0M+1hTu63)f|3jqmV@v0*xR<=A&t1}alst+l zDeosjPxUED#X_N?2u~WF-ya8tAxU~SM%W?u>sU<>Y}~xBGTIcuOJze6D*YMkwk`^d zTPoEY(NxE5z2osB8v1GqTe(_uFQIIiu)=vX4V6b{U|t)oNTj--patpuij?|erI`H# zI@I5}obl!(kGiHY2bLqSCSl@YoN`ks>5ASF(MH%CoQ!!u#5F?NXb=D#0?$wWfwGb3 ziVfxb(Hwpc)Fz(F==Qqxo42b8p1EcHT&Zb4=fx3HC38xKT`zayEWugQ(g$nia@~cJ z=Q~=K;+4&k&rf|fG9Xz!SD%#IOjTQl45A;wF%TQ*Zh)sP2ba&YS7^d6FN=39aLrFJ zUf02c?^~9gR%S1{Rdxq19>DlEtRg~~^zCGfEfvIqm#;j6h!95NDWOA;S49m;Qqqpl zL5J5hgR;_GqcRjT4Cxj(ELQc6mI%g#8tOO=G)kBJo#Qy|Ysw{C1-rh-MC)?R45oXh z|4WI5AH$uhW)`P$nGJZ~NI+G*`&ZWR?~j_dut=k)!_|w(AUtJSkS53PnF# zhnb&$@f=yRO6=_d0ge#$3(Al7$tt000y!DYcX}A{X@@+Q4~VI91u+I_h%{Y1YBXH} z8os_k_V_|4Jk@Z(AIU+X1#m*8zpnzChHBuqry_)_)v2jU_-^K^;s$^OzTyY0+r!-0 zMA07PbV)p6g(>ST2s=WPA(V@TxOp80>*Ya8pwL;9)Jk?Cm>Me63GLPogfZ_Qs9HJ@ z52y+!3{>Wcx$bNus0^Vow@x&tkkIbvO(g%ovS^|(l7||BW#{0L4u+U55J@!02Rg!h z3mpV>=tIPuC;fCIA3?&F=Rbyia22fA7d1ZB zN-X&M{$e7{y<4Vd8K8QNScGxZXHU_B#Luq7`^ndu_k$hP6%fCvy}7@tEFh8zednuO zzf|aCaGL-D)Vz7Rf;?sN3x%6H!8K7_=@`DWiK1K!kar1Ls;iLq$KOd=X{qc9j$3^* z&Jag~0CM)?7O1gXDqPj@{7R@sFDPA)@{q2v;I)*&U0K${6_$mrxB5jl7eWz zQYa;OQ1Aw=`_N|zb*7cYAvmU`-h{;%K=Z_z%v=-#93t(ka|tj|q|ruWyA|Ft2rVkW zS&%2=9|M=cQv0+4r;b>OQ2Wu;2h+XnH+Qk}t6vBq=~F7p1;XM91!;Y%gY*0~YN7D+U}wdcewod|(Ho?ph1szL)Y&G2B0Qn5wE5|p?v`{#%AHa6EH!}%8rlKFQ0 z&E)YZaKlq+j8DGM&kh_lTr~~o^C-2zv99R0xIxw2eEF?fxAeRh6)xAAi1x8mwJRU|b}!A+oS#No^HG4V5Cz%NW>rE{ z(JfLLJh~@%cdQJ5=aq6`KL)dBUfNrL0V7-{GZ>SB1$-HJx~Y0 zTEoH9O0ZEYcG|u$@F|@LVqAJSg)w0D_9Epjt>SS-aMRg=09#{(0oANc-ri|`0#slK z_ew2B*x`zALMPuF`)A~mgb6|~G~v`c4&EHf+tl`8qkm!yDvsUxMis+8eYehYh>M=p zzy|XEATjxNOnXnXKkj-&(jZSbN!$rg9`Y^G``u266``}!{lBaKbiG+u@=RsbF6#5> zH<rvNja#Ao5xw?gk4dx4%@xc{=Cr%CKRcZ;qV2OOBmVHt2GISH?~%9|Xp z>#t=EM|wi@7CbKpg*%LbKF3UKo>3Imzy2E~Fz-z8^zU2s*~pTcIbV^vg8R=EBhP?Dsa*vn0uT!)ni)8W0e76a-+&fHBJ6q)Cm~i;3 zKkL5O#*UxzpYPqWY3?F;ZK2Dwi2u&d@KUtFS`oT+0Yt*eI(Z6%$4_EgG);99H>H3IFdKPhKba1HkWy z9DAt_CJrN#PA3S2MSD>0DCjVZ^t)3E|&3tj3 zSI#bx69lO5KoVEwQ`R9^9mun9ptcmq7nC2&y*vD?XG{^WQ#R( zLbX~GA}rRG0>{?IE)6j%OdMec)Y<1E9!`RV4VaN1eXU9P(LJ%AXT#!$`2e~nh$GMs z)pt{*l6qdTr(+y}YBa8!%JvWQkQa(R_pdx>+m!1Nh%wXCQnO6^qgB`#bq^HOW}4Ue zJwG4?KnIm>8yp?;(`A)r7X{k)tFFwBu0L-IXYZ|nz*wG6&0>z`$l*OP@?-m91+Q>} zBTlbUe^qdjg#=>GOIm2X>S7_;X5sjc`X%e5cs8{u4j~3a)?A~zJaysZ61LqHpaA|e z2DlsQv=x{Hi7SZxQ>qQAD|)gmPJodU78AacvU2YLLP2@>=3=c&=}4?SDpT{?u5`ss zJT^BE(1aLY_DXe&fzoU+KuIoeIW90p!{ko3^qo$PBbbR>tWnS7Q9YZk{SR22$tP~U zq&B9WRCEb>kvyJ+XttlcYVE}L?K>>V##(W$Z$jkeqC6pdnikgNe)jCFZ@O!cDJNlO zmN1Ai?}+F>9sq?gA^L{t*6!%lmTk!S-6jBl=3t$LxK8OGQQYc(q9HY^m`AIn?1p4Z zLQrTB>yd`gO0*liG`5#h?PSnsipN20kTXRbLYNlAIw~hBK6FKOVI+u6=MntVVKKQa zYqW-lo`~g^Xjtfi*r3Y8f9vm@BTOA8{Yh2`AR0;goT7XKIKYmx(C5_6%{dTGy`}2` zZG9wJ7JPgklq6_Q_vOkFh<~aWc|Zm;kXYE}|>xZjIs zY$m~7cU73NF&ff|$n;*<6U9!s!nW@;- zsk2wbVJ%8I!|ZUiI*5v)!ZjhHn!CLNnO?@B70p$^)cHa&j%J>q6rjMha9o961f=t1 z1kVW{?3Nh&J75LTL{v^*Wzp&$pp++PVQIsg3uJKhAMvu}kNSZvg6rnnYlI}B)!@ar}A9)&w*JGfW=q>FW6C-+@bymxb>4cVB z1bVd=HULf$>2g?QEwp~iaR6d~fAR}hXU^M4l+@q8ol3r(q@Yi6jPrY|@$QxuUTHoO&tuct(HW!Sw;BYGkSB&A&f|gUkZNVXP%5d4DF% z$aZKBMHX}rL3lJeSNLZ>EGQ~{fOEWADgy%a{5-MFlL>NE|7I^ew7{&JQefrFs7OuR zrB%o49SOGC-6_pasJRR>vZ2gB(pabg$%1$H~85Y$x#L4@x}z086RQ_`lM;W7n0xsHoQH-MMoY^ zRlGlI{!3c&^t^*tFYiI?9>rGr{BPt4+#FrvUCOS1BdD9$w!&LEn{w(a=h|tKy!jnZ zJ1)usPZ<=J_+kLirgD_dcIZe(nW!8<30IuzdMYhNZ&%*wl4snK@;UCd5xXgw5%ipY>7!%JKopP6 z@w7YQWfHrf+z(7m39Rv=5j^qF2hQs%1a*^0#3w4AVo(9SloLd9`i(vGT~~Y(;!@B4RG|NLgb_R(|-yP z{ylu!gY3Y*^YUSOwx4BbGAg%zvO3#iS+P6Us?TIWDvX@;u;{XgL)b6C=~8( zE%8XKRsIXuWg}bHnw`{}4L${OWj&;_&O?yb`RbF%)1zsG;JgvVBTVyT2UO34GaMjo z#V&NxaC+bHNYA>@vl2~MfE6-J!c_3Wsd3b#q}AqZY zCB~XX!KJ0jPlh-@zal6`WmOm!NyVk>PsbUzi&td-pK&i7m3m}O0vUJzLWMcUj<|VI zr;TWRkkJT0Pv92d#L}$a*a!4A!$u#~dYAM!-`QF)VNM9`m4*V`%ow>-)2tw7G(D=9|byXF90gvd#(K*Z|Y)6ukyl9*soebKgH18l#MoKS<2I{eyC$>&o{ zq?cKJ0$dUv?n{IF-e+i%^Pnj(@;%P@_@8xDdi}g*l$M-uQ<9pxB*Zf-_a#Qi;niCr z->0sgnh=MVcb+Mnb`5WHT)O=EWvj;bv}!|OUPAy7-t|Yy3fiH*vui$B%6?9_da|LC zh6Y3_3FO$IZGQWTbFy@^2X`FP;Hl zZ>w8Py;k_%m`ZQ*GFd#Oq?wu#%<_*!|*u%V~m7Xd;9ih#ME-Vz80Hun@8##oOOw?7w6YGKcJ< zmd*k!7txh(utLMZ9Da;}=*~7VGj~VV5tta{l|M{%p^eCoKBkVX2nrrqlkW|X#y=fDkIuu);4a1R}L(C{G<2Ib49w&f#z#LIk32 zUYyNLYRMoNngrkQdD^llP;c?m>|AqC$7f#AcN3*Ur9L`&-6bZ1K*i4`R)dKAl9c^# zJ9*7XY|K_zhvj<|#Il>5{6tFx^xtT2T2OYuLe z*QcthfZE3}E)dy4l9Xd;DefrEv`|V*J|oj?C~M`c)v595+G7OxTNsH@I(fSD7!4kw zEm9@LJ@kV13tJ=PgRF$t5!9!GU)sVN{*%wMCsROt$GuzNR>(YtVb^y^Fb;l6oxN*J z{BDu?+_X$3l$_us@(KHvqrzi1#FuKx$jE@7ivCmv;QSv>ea({%UQD+X&JnceVXcJddZVz-g!x zWKLU0W#DrvH;8z9enNph@E{0tafSC%cQ!k;9tBM z&_8XPGjAAZi!YRDzAO%03G}KI7-O+eIK$PEh^`+y#s)B3M_JDQ{l+C4iXW@nWwS zEwUnt3f|h}lWG8CD7E{Bh+S{w_B)>c!{f}P5r2EhO@qFafU{Lk)%)8$!LZXVJ_8WM zir}n&7&pJMbD+X{HyYOMdV@=^oekq&1FJCkKv~HK6>R#R>5h1(N;uz?2W}ezL7Y|RLF3ax%*R?S_}Cmd|7_) zj*a@Q)qsXjA%>k6$Bl2|VB2y-{hLWxs6nZn>e}`jfO_Zv=>u8>D?;RY4M0&{hW%hT zAA&lGc(=Ve{#3;+5;P2sJFZTwm4HBA9aTS0xLi?>rE~-D+c>FcB?+R1A60=oURUu2 z1Ez+p%$3nx&V&zi56)yZ2|Q}Y&RQKkPq58nQ?95Y4}=YPQobYQ;MEoh=*)dmaZ22-9ULWc(@iX- zu_R@84Lk#SwV)BAV^dD2%*m>_02l!L9a2X+UX|ys^dC$M2g6DhFB^G`ZI+79{pRtG zVo>vpGcst7zc{#*Z}XZM+)l&9#RhcG3{0bM|EecrA97}&{{%h$QBPAy2Uh$&|ILwL zLpFz6oC47Sl%W18wBv|fq+bxWfrHOZ<4Z1{*am*gXeYAH+1AYR^<1_{xq0t2p`ONw za8*K)$Oao-X+q6O@Gq8CCC46T`e~uPO#X>RH~_MJ>2n%|*l*nH6jDdpK75ttS4fiL zSv4X4BrCOt83yO~*h(dvmyK4&31EK%@rA4Egfu{e-zxS^7HQ!uB~NeaM3i-Iu2LiV zNu7YDb7n}a?;7pb!fM6fFKL1?QuJgO)$u$#Bi*Gq5_}C@7(g}mKX+6(ip_mu7SvdG zn)wvxId9QB8!YT4B3MaGWOqy8}y;b+AdS1flH z_2T_!oLQV93R!@E()_s&4KPQEnR6zzWE%!r67_2*LSnheiuWfh0h`PIdXaU=Z_M#+ zY(0EG!L!oz8Q=Sk#M45@I@Mn4>>rAlQ(v9at33>Czfx$B5DKX#7L`&5X(W&Kcew&< zmgKwe9?iB{t0968c$c>c9L3Mx#s^g_1SJkl8PVqWaltD-^RB~Ac!6M}`||QGdU>$~Z?JZ9V(?V8luOMcBPP{Hb! zk*}}k-kNn3X>lFgdX&qprt@>>_#-)dRNAL;kQ<0@k*|C|q^1B(S-#Md#VTzbd$^`^ zQ^JGFNY6+4NYGkz>s&miQ!;-1^%#6?U8HkhhRr2;k zuklAxdF?Ve<%fng%r0hb?3K$FFMZCz+}zuoJBy(4wa1iw7{7$VMEw;_p;gQb4@kUI zUD~TVhryAMX#(YG6;fmtWuz%_C1mLjl94$yzW424>yXF;0(0+K37t(lIK55_8Fa4B zTX1KZq|9C!vrpc*t^7hA22X`^H$l@(hI zS%=lAr(E$%U&p67Gz#T{<;rWPc1-vkKqq8y;aK|RE_LvcOK(R-hLXj27*lkCwOu*tEel)op~?Q$RIoULG}mhP z>jo2r^MUKq&&A6qvA2>H#<)1BfK%<_Zc#O2p;uW%+zhvb&6E%+?CoUHd}(7L`lkl| zGce(Z9aSxN;FJBPMs%{-1Q&YHIO_unyo4Cv&-B|=pg=waNn2j8_g}a=3{@ZKZt3O*>08beK3%j6F*U1$;4Aw9}PCJ|BfFlus~_6Dk)`lK zC6qC-feIBh;#9bQsFPG_JtE1n%i2GW2?9&q@Z% zW0lL@Tz9&~z-7i6H`vyXzUoF7fCZA@+@}=CMTNFe-o@oQoRFC1$ZP}#)?1TOnO;qhX9zW@ z+uC^_;z+cg6#LR(#n$FV72o3m77#dzYq!k`;@+x-8x&tVE`73w@u2TyG=xn{s~Q<- ziSPqSxj_Q_bQmOzgTaa~Kz5(o40*4HfquU}M7l6BP6BX8>E4e#nIQ4`Z=>5rvN@LQS5$yw;{Fq0fg*0y5%%zLI@4Ix#wwyyn! zU|_wOqeKll_R@$-=*Vair4{jPul}aWnzngS`QFX4xaLVtB+g7?mx`3!dTjgdsoH}T>@2- z=q^cdh}xST5;3=aMp%Jrh^zz5xd(R!3SsXFqQlV&x;Q~-M-4-SGf(uuq)ia-g6NaM zUft0yu@LM{*EAj7S)mh&rLm1p*aN^Q6wEbenWZPQr+Z6Skre)< z)cyiT>6(A&%gTi#lY5q97_Xi9Rc;QIUxG>qh&kAUoc7)5(cF~Wt>p33kRkG1h+5@6$@fH z&mV3vyp^<4#*R7<_>C1!MHYQLh5!PUn&i!UrFD#u2YR{psNlr_`AY;r`B|*GVUZ1H zE)4GOhz}QHjIfL#xOCW`jWkkkC!jig!&-{cptIZE`xC^$z5*m~=RS};+0v4e@7VrM zi^29dgH1y(g`WZUocY=w2)3*J!fIq)-Af(g2l|q5v73aFJpFA{D|0RxZF+jv;V2yS z=OE@)V-5FaeXrOfPND~J50u%R@gc+0&P9;0Gioh?R9ptEYb;*+FQz82egAj?=zWL* z)o1+tK{R|5O+QRu0XE9Ukh~2hw{$fP13gTei;?W@@TcI!o#N~m_*D$|^j2A)FIssY z;0cL!SOk})+qiPVl$S1VGFRrbRQb0;1cmvPs-kma1dZ46=fcYKT$TaEquRi~W%_vm zd`J{$3M-a;L+vkTKFkrG0Nk$MqvZ>YT#eRYSo(^++)zv8Q+I|tNbigx!ksB33Z?aT zKSH&eP}_o(F;`l1fTpU_@sXsq<;QsG&Z9M_B4TsoSWL5cYh>EKdvB`Y++F=ys()sctuLh5KMw)dh_VtwB-Jl|S<8TTpIun(-ZqCZy2ND3HJ|6bekA zGH(&bzqW(j{dTyQ1>!nz1XI9YfqvYIg6A(3KpDC7fD25RLB`r$OD;vgnWP&#n&Lyi z=5KLx<>?xeRV&G~@bLWFmKFUS=y$^szmVi#jsYOAF!#e&?J+Ep;ISrRd$ zh{n~yQ~br4Zg&q;#R)#T4T84DK&Mq*@0~vxXiDk`pD?s z#c2$cUwR%GHEj@cd>bUB7B7mZ*HyIG;60q`zz!r|XsV1&C(>xnrYuQ?bbc|MXMFQp z@wo3emq?z$$%x`*ZOE2)$v>^6KT9o79F~l`nO{ zVg5D8z?Arsnb&u{7kz31h3WI4>~kRY>WVi0s}3i%`yKs{IxHANYK60H8vIb(2jzeM zuZa7k4Sxlh7f<(_|9EvAvuhGpPa84jI5(x`L-b-%f8&6k=DgdC40a!YdQY-(aS-RT zdhhsVKT<~igNjZ*gMM7(bFS?s(G0-q{`%dD3IT3Z^$%F$WCW*4Xnt-0{^1OhVflP3 z{_TN4lS(_qqK7M3zy08_ra(o<0!xg;E=85)+B@k`!0jpNq4n~7PqKuihJF9vdNZ@| z5=-Y8M6T?s9E6n=^uc(c-6YW{m_bYD4^TWd=gW10o394MeJ^Z0JA=vW44{%m(IF8T zN6lxvDHY+jKS5Bpa){sY-tga*aj#8hQd5uT_j~}|!b`Na+&fl8%gawsiBIZLVNebH zKi9b~0YvL>c+h2MhPa&Y9$gpOJHhT9C|Hl{^(KRUCUG9$qALN8<&o3<;f?d}|Iuml zt_-}QTW5~_w-uC{-iUy8eG&w}wbIA@sgOWnyFKt==5%6rz6~K029lN!1^TttB}y+ew*tywU$`qd7-Ur7Q_i^qiftME?)E{<>GYkq=eR2}o`=Rj z&m{w6v)@qZZt=)%BHAvcR@7(`lp2`ku!rK4de2nY|0c8?-&LmNn z4V{L2!Xax^IvPQi;8}O*HeM@i+!W@!;i+!dt4*5h6lPYoF9eziWqp8&0&s)q&;K!m zA4dVb{r0){1(G^w1TnyX{UAhQ+?H#snv>o6Dq`9={#!PoG~Jf3)ZfF z2-?m3@SGL$Av@@R@`Q?s4naPMIErX2^JPLWkBV~>dy=F?kjX=LZzm-NqdK^xu%#^I zGuDm5cb0`zFj}4P+G$QANey&LqZgQIPo{lfP#GU3-T;GrH@raNZmzEfE>c}-*-foQ zQTf0`Lws|;*UBi7k1i+SaK$|mV*(8MC1Bdylh3a zK3#67D*yYq^GR}xT;Ig^oI{&q`5qmF0dI?7+u^kky=wbUSc-&<8n4}k5?3De?YWiT zTtqCkAeQMn9TTnxQAw%MKqSzyf|12q-!O>_%I+t=?!!AH#!NNQg09Pc587SCV_iia za};qmZ2DL*B@J~d&4<)8%I(d@zz{&BPq}3GxME4gaCV; z?STOS>CfHvFcaDOyD5kmQP#}HTQn=LAS6}~NeL>N%KIZgQ6$}Uni|mPpm&l76WuP_ z!Q|7;X@=-;wo{*nM1ceWrgmTLmaU7m|7#8f>&E$yq&PwI-|4~|;&M^3f(`rG!$z-% z1HHQrcQ4QW|9+^HsTY=pdoF!2!=idZhPQzp$o)1Ik5j}mRQMYA{h`tF~4V5OglD=~epA8A=I%FG^Q>VCwRp=|=!m4rh}vtp;S)<7$f zmkV2ZV(D6bg@f)j&38eBg!~XI17#sfo;k=M2JFcPzQu;TT{ zTAy%gEytKkWc361rz(8OZ4b*bVEl+Df0Vv^)A|jJvzC#_BxuYy^Vjg<={hGb?62dc!UkSt$VK z_A8smFJ&XMr;+Vuj?901pg6J51v?H(qLsJS%h;qY*C5J_eUb4c3g!U1EYtM4;Kg7E ztOCOGY9kV$v#b%dD!-$miK){2Xv=1lmQ7v|W^u5xc|NN$ZaD5TJmY5oD$E>I-@BEL z0$q9jz|Z<>!$>zu2e?>^(p1qoOlZTYLLCAM^ZC-X5LwydcRApu;2Y?;6d}D5KaJUh zx%4vm)TQA1ZUnbxR`ZOf6y>jhvV{&=FFq>_w0Jh4{$cV+&+|Kdq&y-AeyjX%ihp+0 zDcbRyfizoz#kmOzTo&Qinu4$1%;;|Gm`27>{G%bQSx`=aVRbk`0}rOroO=yF@YJf~ zwZ$-Jl1EsH)NS!?q(}T>gSZvYVoS^r6`1Ty93=%#gxonQ<$@?+LXtmDj_P}s7Ogmw~B&FixL9T3&PT( zlsqD(G>X)Mh#*MEN`oLsvvjAhu#}W^|7UmA*YEqQ^FN34+1dNdGjr$8t+~&#J2Hk* z3$uz5(?Pa;>?eAgx-A=D(GtP>&2q2QH0O>}`a|N)!3Aw+>2{j6I4Q*{wi=4-p`I5< z&RB;H^7?&uvQT8UG|jurF7?S+TI1qfWBHm!BH_7I*WUPc;XRX%csjD1LPU+*DXeP>|Cp0p2RNd*$v!RDx zWkt%?oXX*x8kpAU>#{kv9{A0LZH6hrnRhwGi;3lwSRD0 z7QX@;`@BX=!`D@@klFZ(K+zvYD027lXA=gGG94%&qPNEYdc5bq-HI~NlVQ#dRpMB% zzhwsTcd;KURY)8+$a=>7pe=XVS zy7~kA_n%LvNUK|DISq;55mBtMIygxiba}1UzL3Wv_R95swk!3|-ut|pK!nWKk-?T5q;~ z%0axwPKat)p(^f$OR0Z2C0EyxQnZK*9$D^-vLQye7HIZoYWBAjvcY??Ls4RfLe_)p zT=2S*qFmjS+!z`O&3;K=RCx>++_P+++ZZWRzgrD^ChVJ~`GQA+7-3F~7?`Vu#Z<#? zYv2acq7kj!+~JqXhbXsy&frEalvT!F;&CNGI1?i*&MO-uRgG7-mU)bkF=PX8RgIC# z#zI4p*!cz?sPyvymdH|Zv!^~WMn>nxM2Y*NOsZj{jg+F5W2=hn)FLEel^5GEIjKi~ ztq3azF$&SuMm}SwglgDlTjN#8y~wbrGv!;9B0r9X6@-|v(8Dki#4e?%(|5gUj4TjY zVJ`S)QlvpT5n>1|-UjzFlobnPY%Jt$NsOfqy`RU4xBLzTR(gd}!p5t+1`si8q~hr5 zBo3i|bd=eIbcE6V)qee^?yc;~Lxx~Yf2f?k#`WiGQvbs9e4|eRf~DUw@8r zYmtxkn5JL3UQJ^seoV3~CBJg_7NzX+ws`Mc*Op9Wzdgs~%E5NDZQ%51)-H5eJ9ON3 zbZJ+U_PnDVf5Cc(vC-w1=Q`vlL@R?XDfR3BjJUAWYoA~4QcVTZjsOzb=NzW&OMSHE*q6g z2f=Z7cKM>neNodu0jWg;Nf2$kaB3RS1dVD6cG4yJ`%c+!))ICwy%VZ7*R`=wQ?(p zyPjEFPkPhpw04Ob>C;uuF_zjO57N!*_e_VZ#_A9c{9$O4g7K=`BWESA&Bj7Am!g{v zE4Cw7CYf~fvdax`Qi?7`QHQKWF}Um7tPGbq8&1s$4x@ITK_uR8P86SHbcOWE`pzZ* z0Y^<99fO2UA)3dpO!m;L6FDM>#j;g4ryM~hb~*khi3WkojuWL-jE^k4o;--RL>AV((06~4xn`t%Y_t9`>%;*p&D8y~(S|)? z-rl_)NftdagS~~?{#9?O+V|sTO2YJm4rkN#grDCtGyl5%-fGl~;@*%9^>1 zlKLtuEg24m6LWl{O+zZsTG{wDG1KoF{@$YZp>J|zUdtK5z;r)0?B*?K+Vm`21^k@o zDRyoc9r8@w{k*bZYEfg>Fy64}IMqIG8)yNnhFm*zMRnA68-0HH&5`!hbJBh^XR2Z3 zUpe8n*ZPCzE>nnZo`);Mydy&BofS1)+bxi75&1&zgZeal#TdzFEYxhw3GZctE1-fQ zB812(1lO16=W}_Dg<_Taha+?8Dq*+*>Xt@*P$RymPuGQvoeZmC_kuL~hk;qJnX2gY*F2lbg!7*Tv4{$*yv z`dNR()&^kiY)ozmzp)Ugzw55`02}i%uf{O@pg8&PbA8rmA)uh75JiV_!Bc`mND#W> z;KOqTJ}6;zQ`=RYYM8#5FKTO^4Gx2Y`MQ8>zFy~pdPam8TiP+D6l}0l=t$yft|4py zt9BU${DNMlD*eM)O~@@F1omHP$SY$o1r5E(rRyRG9+X^klM8OE2lSC}{-V6>3-@c?EGjO& zTvwSB(s|(+8Wo+VKnx{-O;1z3eh*b+q4;I}evghNNLzTEa{JCG@Jp$=F%0_baLkEQ zk*QjI^2V#QmsR?M0!OMbPQ(ZeH9=og*gf@r(hOA!QBB?_?R(s(5Dr$v2*EmKh)Gv2@NHpkb+b78@@hWkVLOFb zfZY8EWKJ3joec%?nHX?^4Zk>@aeiP%ZrFIOpVaLV@P?7OF?RX6*Byuu*z=U&DTEUU zroBgFfwCMWMy&9q=F-vJF&5g;1frP+j*##gh>s6k@^%8b;0ohXzNr4s2Gy{mVi(|b zX`rtnyZZ;F{yon=h^6LbVUbwP4EWs|Nwx+TexO(F?{n$Sm=GZ(zFf`nHXNW9O#luy z!vXAB&e)4js=@pdMnnj~AL{)t8>zTFNPWrKQFg{y#N#&B?G7*UPIeF0CZLHfv)1iYD^&Fpg7$9`(4`;8m^D$ z>u?IxpCMrEL{bgA#063gVK)-2D&P%^4Sea|^>90qYK$WY08zOmjC}B3UO4!Ms9sJi z{?eyNoIWYy^vMv{7psNIT<}HcTEG5loMJ^yR%0+=e(Z6lmUsbHnA4}Y>8bxb^ICt) zQD$Q7gr@R9$7N0-R$JUdOU!6QL2|)buF{_gf+iTznFLWU3^D@{ z*E(R;m_i>E8%W6rL26No(^S*>Mjw<75roJ)X-3jP4J_{!(0Y)KH3F`&Thqm|8QQt; z>{zAE`=Qw6ld$I z^bJQgN;#u^J`qj#PL#rAQjYG-eW@%F8dEXYw2&~$#xY|p9A6C+G^L4f;G{vt4&)5ZSt)tlzoHIT#?-``~Q0+18Oz zmNj^)5I7raaUS$7!uHMV7U9?&`vjBmwbu}H6VpYi_rkLo*{xygX^nYfi`z+RiWO~P za|`1W;$18WDRQDW^OWx+kKbRNO@|Ss!}iKmdOQ^Fue#?L`d_5kddI$8us$%GjuAYn zD#Ct3UQ;9?#i}s8;RY)8RcrOI|DM`}k| zTC90SteUon(mj#D3~bvx9gAajJ81E5yA^ad_RIW=i_=R+v_;T4EB-~6=`v}=wDZ+7YYQR`cezsKv>h!A!XuP%>{`N|(P zv7f~pW06j==1%`0+i%M1q;NAv*kW2(X%dV0_AknJ;pxUlk*G(lZ?GJDfHGFa!%z2$ zx3^tHtrtqs4Ubw|A4LKLm+{kwgBch4P7BjO8w#pUPgId`Ri}4xuz@(tk2u&X9OB#m zJ4N?*&%m++N(J$n1pzlz<7>-(m z#9=1xO}Qj{Lr+aZGZv=sd7{Hu3rJ6zs7z=U*%PQ<4WAsVJQhAx5Bipy0H(klBlN0; zL=B0&cLJWyQ?ukSi#>BqkaCO-)O>3gvZl@8HvFF{C$aa5shSZ17h&@sSIxEsQ@718 z4LHJ5FJm)$)HO024K1i#uV~L@y?D0SxaFlI%!0d~HMoj#cQw-Gq1{kl(N4hT%9DDQ zdas|}4p*6bkU{?T&*kkue#4(n?bidU6d(&vcBR(ML-}`{wTAa8uaoPe< zKdvh?yi1=wRaXp%28$5(g*CNA8Mg%vSX`}Dfd(og5OJ`3*0Lq5!h}gXrm9X3>RDi? z9O4#jgE{oBkB?C6HYPg13Vj-i_)^}+B!Q}RDCJjFb)HiS&G?TzIh0&M)(>Iae!x+E z)wAW%!%+E)sjd~DN9ffGeX0|Ap<4mqOvB<|_2A;v^5qwC66K%mlLI5brhrk37BloG zr|rc+R?9{@$(~BrHGHzBD!gGw9&qw756e&bPut+s!RzBaIDP^{wg2#w*0gdfmA{_( z>3m|df!vRF#xO$dy?ot)ZFL$=>!!cHg^vwPl%?K z)13%q=b?GD<{yVC*J%VBM{^&1FupJCF-4fXUIb-Mugd+0GIJ^XIZoC-nLvr^S>wMj zf#zd6?^;y%#wKgo*4;Hc!#M*h zsLHZ7Cg^s>z}KdghDO5>rtB0KOiwYtgENiY6gcFT4lln)2&%I%ol=(CWAUjv=&3s! zZ8o*l-Pyc}`|%dh?s6TnMYj36M?=5ci}>{mbRxC&oISY5|IkJ2*E6{f8g>+p|sl55Dg2J{aN3vGsaR zld!R%4;EZTnos`wMRqJ{v$mxzVAlvdAmATxj_3Iz^ow=;0wmN_kvD$(L%df6h^O%U zqOaESLYaJ%ti^OULL)y(;={;qNnFI4^~F?dYIJR~#f0d$g9L8%k1L({-3IOx`rF@1 zOQ7;4P_B+x_wsib7+c!bJZrqPST02_W0m<`+8pnp|Am2`m8yX$x&KQD`7`F9@mHb$ zA*x{ID)3=op+cZ3^Z~cH#=oK@)?Xc0#U;cI;kD!vr~w@7wZ9Qi%pI|D40uUE1Xus` z!T0=4fv_valW4^D9auN3J`R(9P;@G6)dbpu^``%cUuHJ-WS$EJ;vF!40zCc)#o-?( z{i}~dZ1W^Gbkg8LCl)7!4To2u`Urk4OEKiLZ4GX{X)b(+e<3))@&>Ty>)$p2=>zsINkrELGqj^_kF zBhXiTIN6nf7z0|Nj|~+kJCLo!?9!2}4KcrC81zfm*lfUm^W+$!BvLZ9?}iD{)C(&; zaKubR!v>lfg9s;(oL2LkIIbi@QctQ>Sw zew35UiyJ5S5q31wM(hl1gt!@!@iXABP~zsv7es;CUK_OP$93Y^(Tru|m2eWgg9M#D zE2l#j8P`Q6E?c-3i-%O3N`Og~Zy}H0a&owZym8Cv2^LqOn;=d70Q?_TCi1c4B4Rz! zwHHv1#el;k-4ITTCkjYLujB1K=T zMlW>BqE*$R_bLv5x4+D+8*UhHcXZ$hD!7Yy{6l%unu+*}TYMXjUafH)@w#I15^7j} zS8HV;Vb{dFh36$E#{}JQ?ih@1y?;QQxUdsiI3^pp}VkA+nt)-oUIEZ$@w1`7rt&T(SoQRUn2QY zhZUfbYmwg@n?(mplGhXMrQhny>QVZm(;<&Bb39W~touEFxb(+%;uw&U0(+Ue0rF&rC{t*ZylviQ zcSp-`MRs&?3oWkhDL~fkFT&ByWSuNTlPdHJ-~VbGKHhb1HoELC(is!j7316$)0`|c zpYl&fd>i^hin?vlt75?ilmU;oEckGHtRIKnX%B}k!wvp*_INskx!R(1iS1VXW6T8j zJ~Du3N3hwXZ*>MQ`D=N6TJ8kWaxky_W5bZg;f_BX3TmBldhOq4)Z>y#jp~Madm++E z=n^U9scbPC%Gw#TsWc>w28CA2Q5%iBASvH+IoV6iwWNJ+s%7`1FYEqP9j+|>k0{5g zzq7|nL~(HYW_X9K`vZ|jm;i2o-~l%UKGkJ7Zpc}dn8oniWFRBZ;jQ{1G9X1-?4i$g zT&G>cK>J_hRCvfhihOO^?4zACY5qYpN{{J&W;jYO{$sfwx-nAuanVg#TT6q^t`Ri* z;-ZtHs9n7DRMfH5ClYH#u@kQ<1oaa)sck3sBjs*honRRadah`2n$dfY<;HW(0K<## z#g!zau_a{6hc^5^lOv_I^%?ccCVeG_7prueAb!Qt#}ePFYy+?5K9Hx(WR|A|CEwJX zYkj-M>MSZcl7{ziCd${22+dpnmaZdwWAjARctn6^3Ie~SfxHmWqo6n8mn$D`D|3DAsc>v#{$0X*+mD< zr<*0L2}R}e8D}7PRQiiD|KZ<;^T%$0 z<>A}luD`O;0Yg41;0M4Y_z3|5rw0(EY4on8gf5owR%ciX4FL3SCK$v-aa zzdABi+||GOK)ruD{qcM%7T}peC_~9fK5&p*S1hvCf5JH1(lgsajzj$PpiQ&yNt85~ zYyviQ?lElOH~!gjKml#tzmx#q2T&w`4+iDq#>}5-q1oIMoC>5GM8U^grAs%_`RH2~ z**K;68?pH>rHXpq4M`_wNhJI)JpNZ3Pyerd044q}PQjBxs2GY^%vo(yRJs@c0jQgP zJXYy`A+WCSRg?T9`P*wj})9+_@_@2_Acy$j;A4R35?FY{()#^7oa;pMcLZ1 zVfY_E2R>9mS&}aX;k=-E7xtog$3Id zArhT2xI83eC|D-DFvU{>%qGv*OV*6a$FAa*z?rFISqksJOSth8V;`4|9E6aR; zZQEXqlv#yN>Vz(&Y~e|FyD)A*x2a4d4&DVa-w3LuUS7F}GPW4VkyhwK+`ubPtV8qF zAC$Ab>l@lw&-ya)tbAmp-r@b85o5y{YgOf^cn=7CA24KIoG$Bu$b!u=tKMSm6r5YY z#^5&ts0~i?B{ta2b{fKZMhx|Ai*skpP>PUts!)*%*0tG!+aTOYp+pj^ZXjM){xF;~ zkVHz)1$^Hd5fG1F({zLsEdozo!|aUlc$#^)4H!c(;n7bvQM1) z&_;7hp<}i=Tr+N`6KxeZ{V;3yqPG9|!_lRkr03yhzy}4o4_}>cXFc@_PE9@=c3wuPzDw<)D9P>UIreQ;8 z23mUPYI*ZxGTuZywU+LQB+4mlP$bLJb1Qcn{xqE?dN)K-(m`UQcxa+T+q$@^ z#}r>{JkG&VgF5e$-&(|X>L(Q1Snj+M&Kgt1_xdMn030U5KNZ#b)9Dp0ce}$8MP;oc z7h{T4W3dI$yZ8d=Nl{P$?flOIs2e@cDQp3BB^(q$!z2Eu0_a^(0KN080D2Bv03C#! zeiuMLVhf-t+%yJj{%}nGRzo;SpZd0IS5RNtU<$K;At&dJ%zwI!8Nc3Wae*57^VLGL51S+YnvulLlsXR)g(JSc_9}n5IoMVA|YGo zoWB29ThoIi!=aLQiMPDVc`&MI=@K|7QAC;ggYvFx(V~*ROr4Z#*;J@^mD{Ku4F#>cw_JS}Dw^aCLN478LEwERIp`qM7$$F~22 zg1h=J%F@$)!2s}&-AOaLKl><|{xuhV!C;L&bxzt9(XVS20jFB#w^0W=@TqXCA^RL)AI>KsFqh$D2x{z6tniR5nz zM1#8kNcc|5qDWg&rx-Xa7>=2M!$yANruzZqjsB9dW>~MH4OLv?FCOuH{YG5c*%7g> zAnN3R8|M%Hj}!6*YG_@VwrkalLAum=SpQ732I zD*xc$6h7S5e^KIba-GA;6`%ne1O4;7UyVg1-`Ou8f6IR&$PC6lSegU?^#RMlFA9$$ zx&MXoU;E_8%;U^R8)rt9;B(;nNo(HD2T6qK5KepW8uK^e^W6XcMFQmk;sD%(rGn+* zmkN|eoHG8fDa@VZ!$xM#i$<(jf`tHt@GpVjO>WGX`@R)Q@FvzSfxt`uLO}V)EdSb+ zfA#tME}G4KRQR^-T&Ya+L2SM$P%cTFR{XP-{UGgxJVmCXs~?{J&FY`M0sa59fqH`W z-xU1P{!pxNI|`OLgVI=XN_yZ4DxQC)?Ej@hcohVhE>l2Cw)QV0gmAV9j1XAlA0xz@ zBA^`mZK-&B#rMItf%@BuVjop9`nyz$d4KYVB9lUtsE9h1u)&zYH&wwg8zO|U8hTe? zd2hGY1f6N^rWH*vQJ$BWv|@KEtuTj&pJI^jtDxg_r6)ogL@~b6N7!HgsJHOUtOWI^Ozx*7Sm))I;1ga_Mye-DYJ3k%Fx67!>2)G$>+}qd&ZmcYGL*k>S+W$ z^CY10`Py-DV_KQIR5!z#bv4diuR~v5UTUJh(`7Zi3leTY(NpM^S6hcP*H z6p5qZUaB0!4)t?ia?58K(Kb82H#AJBf68>LUrT6(Iz;vBV-JLT>$+GnL}+*33H9$p zYaR9-SV&iSwAN1@(jOah>3V(OEf!xFC<_%k`)OyHdWp&U>K-oKuZd|Po0R(SW_PD- zpJk}|&<*N2ye)P({YAdo>$W+aQk;)+ZSi_j#2Nv@poNUwk%REj=(z_cdvggtPHee` zQhjpNcyeAPiOK6`<&}ue!;(d}l1q%ArJ0hhUxz8yKlas}GHpqzqb&Pwr=T62ed;6( zD(sJ9Os%c6sh6fy+>jp-$D9|{mR&BmF>ChXl0c83(ys4Vb+Oe~=OZ$A5#QTLQf{y& zE3&>0iLYk9XAp#Bk7Mx*H65mFn}43h0dr34G+U{AFlswWvhMS;}%wJ}-tngrU&d%jLlb`Ww5v>A8ts_p8L+NNFAWOMg61 zaQ=FH)kkeFQ9Yi~p3I8&Oo_?2hgAM4NtX(^84T$L^@1>hs$72E6!y~;hJ_AxCyCEc z(AU$(s+WIdP>s!C()AgdB{kwbS>{WpbX|k-s7s6XfX?}{Z#$%l?>?@0brjaNn3EMl zxNTZnR)kH63H}W+t@y(6NS51 zFR?xvIn$stAnw;LE!SLNL@izI+TygLR0YYRbQr|sSB|tI;FOJ=iQd9jOcf8~DqWSg zR7Ht1ZMN91D_moDA|OLF@iSh|t|R|=tlmk-MD{|X%mMLsThQsfrX{_PtI7pr$sPLa z1Om_#QY6|$W9Ui#xqLW0N?*oH?ZUaC;nKTw~pf8P|j`C0ozw<4?l z3hG+rr#m*Eh?~{F_uo77(U*y^%;n?OusY@4ZT|AI6^4er<0i2_r>?THTkK^H$_BJP zuX-8?1(VQe@|1goeNZRAl1)@eP0CIB1`;G+F%Lb*V{Dp`s%SIjWoLQqa)AXS3IAqD zI92fe^Q`)weTbAxHUtr~aKLE-59&CrdqkD0KDmY`EuZVdw~24hT9(7qJT2iuNSL+k zii)}_REJ!6ZwG9D$6ANV!&dwVQzk_o2^~NBN8DI-NbP&$q;veyLY3zpHAq`rg;3nA z?m@SodFWVb_!%$2J+g9hU?WS#M%@B}!&L4RH(n9AdIeQ@+T(HA=cJ{DmMV{^N~-bL>Fhf??6M@y8QF;?&3&C4 z6x^7uylcF5#I#GY{$&TLAS343bBObnd0etB>?Qb~0Ujx{`RLE@;$C0$4KN%{m=BSw z?-pl#M@FjSB5!Z9S0o~4hA5$A5He=$nCEFS>O;G8So)y5#E0q67^??aTVr(UtSMw> z-W@;7E2}0N$iFxDenp?;+py{bl3co*G0}}xmM_q%7HQWWZJcj)%kL8u(S$?n$h{gbVacrob@cyecGEuK}`+QEWyN)M=Y3soLW(~mbWW-F|^ocv^s zY}Na?#iTd?*0f5eDR`i)wZRCw7)jFE`ZIX!35spSn#{3R=TnH>{Fd=0z4B^@neT{N zgxn8e0Sj9mI(pP6R8Fc4$<1BORoaD61vs35LbDx-^?Ry6 z)+iHihFnuQ6ImGy`|%1%(QWb4romui-?^W@ZL4a_fm(T1{Ys}X24sBrE=wfTmP%=jgzL)mwn}Ypl9$CY|&%Y z?ben-+PMgHPxeFfGXrVwGfv6L8<}=>-`*4y(oMB)1PRhft{`t*^R-u=u2c_R`*^H9 zR4IVTsG5`|al2y`*Psr5sHA;M}q69~KwMZ-%@QD{A{PxIB$sb*}ol zG1VSas?#j7pek=FeP~M-RSLPE-dywCO<-@K{wyZ8vHM11@whn?%}2RlKOYais6qbm z=VL_zD9Z+@T}95^UF^UWd`V7y%St5`bVYLuvn629Q+MD0H{CUsT(9dHW0XH6o++>ErGiLkeZG&je zKG-Z+RIM>EkoF3txEEN4I2X=ruy;PVHAuFH$kg$P*u~tgxV4WbL$um0HljRh8s8_X zDtr2f)~c5B(`v0YHpxy(KS@yk$$jSndRChi`hKgjaXyh&Y3@SKv&FioRey|gT1K0_ z63x>?2am0bYY>*J+d3xC7$y#)<3<8ItPjnR@Ri zrZiSOI~`%3_7IiGhz@^tjgMyM+@8b)arta)?xBxNi)Qo?^L)f`%NJerC z8#gvu@32So(DfrN!bO`T>e{`FlZC&>iIY&92-`jlfl-X2C5#M$MK48W*%M_8KF>(j zO}CjKm2Q;h5ioWjb?xWbsp>uvN>g`bXZ|OF2XSVTT$b3e`P&?RkT+lJ)>|Xh2XA_g zAty5&dY|S$M(P-SZHuDgCuIhoOy*dZ^*rDSCJ5zgoHWs2+vCmDfrn&qxdj#ox=;Bs za*|wsNLmqh(v|(>>lEb_s-%}+KNAL1ZSbbqGt9JXFv-U}t1XQ?Qu-~vm%9{LeVv~anI4R7Z!U#??@G-bq9~!$Q7aX?6djx3y4b@OonxOMj8k zlfRRy>_HQ1P1lUeX-@7 zYGk@#`2Cf7BF8o6GZ|*P&%BX$but62-bcFrn0d5!S-|i8kCRiukR?2NzCVgb>{7^T z3#;%V{c^A5J=9)9=&aNiGrGh5&jU5IE3LM&2vb$U-HeiI=^MHd&tqax!t{X=>dp{Y z!$)%Pd)}~nGBMh!F_k8H*L|e7AD+1XO*Ai21XchyF&|4TtQ?`JBMMQLbW)~lp*XrR zoj-4frrJPeX{mUC-#==9^)=UJ<#@);tr|b3nA0Y#CoV$^{Y&|S7^l`X0sTweBmy4^ zz1gTGk%HPeMhhG$V{$pSxQ~gw?=A~G5vcpBd*jHGf=-kDS$ZdjJja0-4`_rx$a=F9 zl`DyFs`axJ42pcLqpw-8?6RsS2<(+OVK{u43OAER$v9!_Yx_gK&24d3Z3-(byY9hXg#&qUZsi~->(gNA}9XcDC|QXImJl7(3DSD z9M_z~$|YX@+lt92imIBy0p+u5h_$28=bfaoB$G({I0q}f4Wc{ID)vS6$SzaT6;V;+ zFL%Ys#0Eok*(zAAH$94~>AE3+b6%U68xK(zYfZFN;uC~4zUmg${kP<)R zX{eqtw8eE>VC-A@OQk^MM@ji(u2xypu>#bSA(>Uq8oa{w$zvNL>JFm|)0cQkwrrw$ zx-TAkd%U`R?(uYNc*iNO7kspQ9ve#ZEX&)QOu7M`3;AR`)+>FV`_WUWNkfG#LkrNb z(NsSAVV?SJwMfXABVzP|M%yegQ&#Z>1v_71Lqewa2ecDn0}ld_Uc)w3*JaP^$`;Gt z77F!T+R;-dINxf=zY6=TSp7EWcBPFmm96~aFLIG7f`&K4Os5fkOE%N*?wImb)nyD? zxXSGJh0$)+a&zCKGrp31zq_t1GxDn9Bh%P~YdlLjMaC&QQ1z**>u29y-P>JB(0CcR zlAW6$q<8s2g*Vxoxa~LG7z2dmjHx@xMS?e{w;+8c4}tDZ^XcXS(FZ{A!X$*{0dn7L~v znS-z=L&I3-B_nAgN^8fDN6-Op*dNlUeNSS(?Yb()I^Xea7urf3*f;K~pxwe$aeNUV z)K@03B)HU`H{O^;`ZebUPXKYPpu#P;dT-TzgF`Aq!FB1`!?I_8?>TtBT~g>t(tUdCn!SGYou?klJB&HUeacNL9l2c&OhdPM8W)Mpcg@e3 zzAa-o`Gc-4?Qz-6t~LBCua{}^+h?%!96aq(qHNJk*^p6|mrp-> zo!8nVWD~N_2~$xlLZjZ*UKx%Is%fyFIBIit!ivp2wf-G zfZ}A8Ku2DPX0P$}z24eS-DstdDR>Nd^b4Cy>(Vbf!{zVZ~sb8p0~3GV4xqf`txI|NRI8>V z$0O-$rH%-(MX~QAm+bOtAK!?T;z==$bM%%6KM>ro;IuqrMjnr(cWd#_G&#W+JWNpc zMKw+H3&FGGXle#R5>c2&#ZZdY@pocQ?MN9oqifKqf)CQ-S2zw@b6J~$A1gc+sR=%p za&|JG^9+W34+_4J*n={26gKt0qFsi(OVWHa&nNlw5r=->9H(_sSuKFjBvF=c<)|Z4BF|db5TNm#_@kRtZShNXD*=i>hF=R<_I3{=>os z!TP0RS4$p|Hj2TtdSr9^rNpoCKi6Q~7vi-}@M>k#G|E(*sZ(KdqqhA?=~5J9#;`a! z@WV;aP?EPlH;6hJU98lz7HzH_%!iIT^Bw&p~y4P@| zuGq1(uIv+E6r%^70)4DJ1GKd}dsZ1gB&WAbHR6O$IQ77|kS~5>&amldg z>Tx#KzyPT3oV;7BiB}%EaH@HbMC|q}Mzm(4qGMga7b3QjrbeohybZfTmAX33Y(w7^ z-UT!YAsmT|E_di?zB{;2bd}-ppsu->RMp9H-KJwn9toPy9wUfky=XFoH;vx=PEE0H znQ6NAPfR_8PjP52MLxLI7?Jpj{w|C$?Be@sx|ZY*kPt-xxsuiG_f?C?^oUFX3Y5vw zGYlLDKUhB#@8hOLl zNzaVf=+Pv#a_zm3XJ_spK0@EmYKTUJ6UV1@2ZmG<-cRfx1xddO4R>ha-Epp;OcXS| zOGl@}NYr$~b9!_?>CfJC%BO#T>On-c5L5WvEEt4T8cJdarAX%NOJA1sCX%~;HcryD zSmA0~J7cxEeEz4d#kP<#S|u)U@2jxa<+rhXIs_kUYRHM)=w%7K9ZVw32^?)d)6*W( ze{C(T3AkpO{E*f=ghEr%1!}LdI~@ZXmxT|RZ&W&c z(bgA)&bzw%8LQ={&}EvQl7=NNHyY5C_vY=)Ow4NVEpDB!D?07TM(e|~Dz!gK7g6<&bJ3Em~Obl_7h`!WGeca#>Kus?JOdoi3feYy+(*e(MVl z4C2sJZ&Y6?+^GDrmK((Q^bCY(DsVeBoqaiW!qEm zE$)bmwF=56sXnrzrgKlLqPaH$#`uRXqX}Jx_9ps&y0E-DqgtfWH)3uXlzIlWdl!;V z9Z%t7uBs;&we24r&S_|K^NBP4SYOJOyiDAAPpp#mlXEL?J@Y{!i_RmSG9soW7F%j^?R8Sc54`$DVCHUlILs>)Q(`RCiZzjC8B*2?e+N8J-rqW&$T?##TSy_ve?^ zI$y@VGqbxS+_fGyGuO`T$dznS$E_2bX?gr}G~bzV9aZX>D545b-Qhlk+O>L`Gya9| z-Pcryi%wnR<#ZIGJ34)>GE9?k8tbok-|CKc!M**{&V_lGXsIAy#JZQ>Ont~;1ToWF zYE2mB&ld2P9y|zp>6|c76*;RY_Yqx~e`9e(V4}S}_5~H)K!*@gopvU}#?XHCjVlp6 zDW4MxVI6+2jcqxvU%K2FmJ13BDwEe4j1@?uISRWd zn&?PxAU#r!*XN&K`<}EaSWA(~Zq37-Od&W+vKZIqc>DM>|LO&rRVM!d?~Bl3x{<~* zO7-R?jRmcyubFvoJ1FSf`l;TxgD*6CV!lmiO)NEshQZ@IEx1k)?@-7e_;AyUR`fSi zOW8t+rRyL*wFE5>*$WBXWkyY2l)5LucqxLaN#ZoSWM@dN-|>4qW%-B6jVy)xI~`=) zdyP#@hN~5kk_6wh4WM;`=gbhI;=QOA$KaXRXO|OZWz;XJW&6|b99g`J5;#prY&qn6 zlkiaZ3yTo_oO{Ex(n+PmQxj(QzFvxWP7w0+`;peRu)-K3h4l4IAs_2DWsIr&+Kf;? z1+qJH?%j~_t%3N98NG$E2rlbo<$+*E^NR$8Qt`rV6&^=e6f?iMSAyydl;TKHsOnSs zOe=8>ez^dtW)UVx5kuD?$38_tzZ++EC&Sz((Nnnzv2bg$IfC3^ZQrJhvFygHufyts z1lGp-cM4Q$bXV_E5K@14y6>snKCc7g;E}!~{*YOkRkWV>aYqLn4RPL(NU{!xvC;!uRx4Sf3@r?!F$#^ z+n3f{?bdb^$w%@lCU?V6HGd_(pDH_eJI|OfqO^&ae{1btw5Ws2yF{on+>cgH?u~Vg zK~yD4ggK^_N88z#IDMc=XD8QtbN5hOXCQuzG=oF4xuKTv^QRA`{3;#Bd_14yG%0Sp z?g{?7a_3Q}3`{Sp>hn~a@@`E0*%awa=WVBv&7y!C9x$E=qE{jrY>}pU4ldHM%0I( z`FauwOd096tlsPu%x0DtL)VgDF9AE6_Mk%at+iIiOM^Es=NLMuo0q`%s7Y+*a|X_{ z$_6seLvPPD=>a7VEsd<2Ki~0~f}1(3?ka0c17mz^S z_(iEx4Y^!8x2>m>==YK>t-Y-}Nk-R)#skUOYrC;Nt|zg3?{xwZztLyo3B6Qydf3h8 zuf?hz(wAh{4C&i6WQ1G9mTTWp`2Nu7#Nk% zZ5hW+jR}D~iM=JagalFA7?G#1HS!w|qWkQpa?bF(@pNW=nf@?u=isdVs7ralF#4)# ztjBFbM9MfA5CgzM32i z+SX*V?hy=!1>5*yo`)2hU!m~+G=qbL6#(Z)C{;_03rI~GP$R6UZ3jh}a4iwvlY?N+ zl;tO(Bz>A_o3D_N4460wzdwMFZ|k{c63 ze{69ionsJ6M(NhcB_ZT!f3$!_a`dno#jCErt_Y!8K255s@cgs+RsV}std#c_Dt+l4 zGbTITc1JKC6`LZNqb-!=<+S=`INyMahHl41kzPIHdXd69XUI2Y0s($Hz0&$Xi1V=O zoMfxO>7_Y7h}_$Q8rOE*;Qw?06Uu2*JF1VPjK*n1fSezik>fb)e>5^UI_2Uh2N{c! z^ViiA_!L;C-o07QQ69A|LE4ukU-(jtiDa?&30WZr_0DXS<=hb}t(Y+*)?_G)b4R6Q z?k?a@O{nBm*Ixrm<`f;@Ug^jr%q7I3X3f<`=cw-FEcZy~G)V^ZzGQ*evnT>YGZ|SVlDJem0SIkh=>_kSR`ET1~?&x!! zSrH=D=7MADJtGr=WyV|`+BP|+8HTNZ;F2?jJT6J!AHc`m9LDs`5vx9$c1ICvwyN>* z-biEp+sZR_N*Bj?WCi#r`YdMP1yv}EKL42EBou{Hx!R}8f5}zPUkhDnW;Nxjty%RM z9DGliLY{UUtP+LOrsW4Q1ki|w#kS)zyRAPn{8UYR$a2PJYsYprrq?;|X0NEtMmhr> zz@E$2iC|mJY68^67M7%9Nq6+lq&;gJ>9&RK3Bm)IBR^k_e@@T!lRd_#iOxWG-BNnf+^uRvF;dGjmD2wUZtEWO^R z4D?JNp2d3AddrpuW;!5!GEq$)&V1r6fR{o1ZedgGY^B}A( z!zY+WqJlp6V&X?4kgBKCkA`>vw5Zx*zaZ!fwTqM$f9c$59Cvh@b`krOChcXC9y2Q_ zDvwGD?HH}Ad&(oCZJmcK>`|tFe1iB*?8dTkwCQ+Ko!gQJ>R*9XzkCEA!EAgYcS9>b z>I5)iXnGDcco9=3jgsg#z4Qy;3w?I6Pf?Ru6oXSbeE?*#JFrF`UzIw=w*Tn(TT~&* znYr?6e?)^87x#{L=KhigKOLPmeVhy<*{Gc)EUYjNvz&zOP0P~*9u)UFDHS6mXd}>& z*<`{5a*q=MPhB^&1mn26kObq>&(l2bGQ5?@v%qAY#ASRjT>uVphS@ZynUwl%B6P0t z8Z=cHO`%Ih*|vR`^K>A1zxhw$qzC`>%oH5ilmd+`Z@MP#pVu^POvrMBttv}hup2n~ ze?2Gd(M$-imPplxI?SSofXr}DIVZ%j+Oo%PteOxMgt%U8i;&vw(*SF4kKp4_MIGhc zp{hQudla2?w6Ta9Z?SN5!~egI@%GE#kMEJCa`xZ7_~ls5;au2vR2B`y#zg(Q z$#+8Yc=XsFZofjW1`Y(mCS1X8q*^gef9-EP76wR%Nt<@ByYb};c5}Ge)oZ-3i)Zjr ztw@)On`JKb$y9l`8|qKss~`M&|UP_+r5J2de)YHg$r11)I=w)&|i*BoP+3Vm!UD7em93p-JOP;x_SVg($i*T zbN93HWP19*W_evs*_6oBeSf)Kf54HvHpc*Ptw{O9SkV|A;4sEmHZ`yjKmT0!{B2Q= z6icy%Zl@EqWe1u|4OuOc6m=!09Blpyn$iMIQDfOp8xv0c{C`hzwv~k(Ks{70icz%I zSRs>GeMA>DoT{7D7<=~GCkFto#%dKLnyoJ7IH#TI2@|zbs1d+gU;Io5e{Dl%BJ)gs zU9|u^!Vc<`fy0;1xD^+V;M20)XajdYKkd;fOYDK$ebX*L?tXq)L&&n#-NQ7FG==4A zeK{Fo<75%pZiKC!8pt^|gCC94{0f~iw3U#feg4%r$r70;gLo`lrcA51`&?}=r(dn% zN_Usxo-S_xV>nPgba(CZe`qZ?ZJA2(`Wz-1ZiWMD0`-hN%xZ9@WOz5foDKfwNd3mf z@uN%0f8e1RDZsP+3Z0SxZtC!L{?+!|s)o7Pibuk&x~KNiV`z&f-_7AdSD*2gE^hy0 zIB*QP`Q14mb+Ja_)yapxz;t((D<<~!QeyjiDg--rBU@~a=Q^SPe{NY`8UeKS}A?y;k8 zMnrnFvIpCE6l%aM`eaS6XwTrGZ-E% zc*azyThUtjOR0Fe84So@3U_H%4}Z^!up~J)zno3}O6@RPtknn)sNA%9b(J?Oa%F}R zU%igOy^@R(uV6L)o;5qe=7bP9}VvEWMztB^}ziZ*kZFtd^N|jWp2`?e>6gWa8%2B?q>M)joBTN zmb=#qfx=$cBi2+Uchb+7rT26w{`IZK4!t?2Tr)>qaMp6s;6C9 z!A37tllLM5-u^5-+dUv6P>?_!LF_o3O(1&V#_n!}f39=9kz`}UZZ?S~mczSOC@{~$ zeXxB6t8s1uXPQ68W3k_&?X}b07SF$0!=>&n!%ba0fR96jJ?NVOA&)LQK>%gZANISo zTDo$KkmG+8hYU5b@)G6q$dhU%=psf7$61KVmhNHJPA|u(e~>R3514_-!!ZEJp7`p_LpHRH2zQfB;&SqSfT169fd@ zrCN;}b0NuKIo)xD;CiO&RZGcAPKeFXS^wq31XEV26K2(zKsBHyYFFE=^gND2bGY~0 z|Fn9Q;|C0;Qy)f8Xff0-zVdLg``e7p@&p<|e{^B)?0j|Iyocd?4;5wrj5m8tJy{vR zW#9jTH8~b83y{&|=!xFbPg{qkAON;JUVX3p3saAEZ{^S;Fe`jIvK})CwHeqK&X3(7j4*L<-@nY!aq_kot5?<3~sob4w;T%5-O9@|SJ(aXaqtf(_;YM93nBFD7&u1T1>!+?`?){_{-_qRz!o3T~+HalsAg3r5Y z?@?=8S+5PHua!TddSLo7nl8AL27q*1r{i~lAy6`#kn;egn+3pdcbb(aba47@8b%uT zvLq&(4jp1e8}i`jGA0S7{2KMz2nL?%zUOa-2 z3YUY|b2F~eqiEh^JT3EP$JYu95-T+CMPa&lY~trL27+E_*6|M4W_yG1W%1cu!JFx zwhj2JgG2$y4tlXvGRCY#N5qR~@Kv2&ZswOIKkYzg^QIH8Mkm!Ie-n~AIqEe}pDmkoQs%qwwh>iT=aO>_ zMf}8dP{3H-8sa8SlrT) zOvo&@6z(ilmt_8Uf3&Z%RlzZSla{BZhme?R5yn~#g~OL&@t^Y+e)OTY;KDUe~u}D%9z1tN=6ZIBh<5P zK(xjLfnph1rx)f}>`rVmOisJ~eJ9`Eg79SKoQi5og%JqO(V6<ZnobBkO%w#Tt0%2yJGys-W|2-!&>6f%$XE8;xt2xbVgSF3wh%#;X8&(Wl)DWau={1naFJ6~RHX zoNdC($E4_-0~tpzMoTf_fRz>GLb-vE#mmS#FxDRPf49+z3%z^*AIB^@#NHjV9(8QS z^4YNI`0!!%Wpgdc&8hw1CvX-5W?GO<QSa+r#Qe{AT&1<76U5sYlIaTBLhIirc;`7z!?!`cw;ju{GfoslrCpg%ooN>)(E=fpAR1>cQQAMIKmXTvR*QOH@2%20q!quh; zZ=g~vI<=&&AN)vPJb{mTV*9+_46R4qpQ}!p1-y?{q)$CDwk*{+&#km9?j%*{b_4l( zZqTAkM>ErJfRUUj!2WaH_19@U3OG5q05C&}f9>BU$32D8W^5u0)RufPEMBFJZ*JzK z10hT|(06TZ`i{Y%&hX!uVxzEdw87|*CLB<_9I&97Mm!Xx15Z++iJDG6&C-`Jl5EH} za9(bvlLbDYsnbE>Jlu5T#IB$zv1n0dW$SREE}bkEBu-j$O?s}(WnA3bw8Zpu7JpwA)Qir2npRB=R0FGiqRzlTw+XmjPy8%L;TBAY^I5CuW(jtnls9H{AELa(A^s(cMrLPP`aC68q$PU8$RF;au#czCoibdfF z5gMW;j7W@D6b|RSEcMz!+mI23GKS~rf40ztcMJ|;6`B#lB-ac(ut!Xe*l!!TsZOMq zc|kUZ39V-8d(5qvJG*=WA4e>jYv0^6lt)wWm@-owH=FZGJslo|iHuR=T#?1I#xu*j zg7MI7HyX`U?Bi+=Vc{CV)>Zv>Gn2tr&)_2i+Uk98hE{)+0nNF=wA?}&Up72$GM1O| zEm;Np$C{P=5KI|#ZX8-C=4ys!f5qJD&voDbLh5biKZ|pAGpil{zhg$yW-p0AHg;W( znfOn}BxGZ)lQ7wP%iVRw=`^QWO#I2EMz1E@_yA0;bVf|fEp@AfI%P%yo4IGr z2VvI5oP{yjz=vjZbDY1DnlztfJEli0(p~I8FplSWv&^4eK7o&%UV6mdfBYWmqxKqf z8sw(N$&#L8g)3Nn(GbY6?7f%|LMIX0py^}&(&tt+mgeScie}oF{JHM?+s5Gmh5?ne zCv915HGxgOxo<)P&x0Fx=vukM0&AVbX*Cn>@rIXg2X@oGQU}M0c90|Ra z)AOY!JiK}apMs0Y-n;C2eY7}ROlLLgJh)`x9P)tG-2T^>9#9+6T8cf8-wd3I#MXGS zvHls01k^6<|N@pq4_?d9a& z8n1J981Lue);}eothV9iMzTJdI;SBIcT{L9M(!R&4Q6=Ce<8d0O+uRfn%@oYfJAW= zDz^68?>-q_6dQ9lp%D&++D?C!vl(XT$u4L23Wu|riN#$Pu-d3p=lU(b9-8QA(O)_Z zjqw6v^Sap3>%Ogve<$!!LQ0&%%{h%en(t3uOp-UOyx+D%iop=PDCj)PTt{zrGK?d8n7bzkJ} zFx<|?t$$2V+atai3XhsTV|K<=pJC4=*_Q3Olyk(6i>_qPe`&@HXT$D8bDR1j6*aW$ zp{NV^bKUjVEFw}glTccn#6XufH9a|!136Fsz?1v)D4sv5<6shy*}@a~AL47dBFDVM zk)O!;^yxpyO;{A>j#W#ZesYQxB0LTObedvdB#UMdFH{^Xjf72{GzV9<)3*AM%Q4@K z3Nq_7EkmV7bjzWykdWDI&6$-I>pHE_y5h35Ix=tt4FibG-lw##Jf81y}viY zAjfyenPU%sTLhx^H})G(26tOl@$9yIl9pjFV>geN(YgE%ed60N;!1zT)33(gX4aX7 zF&&O2hvl_PZ+bboH}0$4-G$q^xbu%mKU~xh&BWKC&(LfT;(`0>?|o z@od=H$LQgdn!sB8XJGThrexYQCNr&JKkr* z+JI%IFO40%2HT_w%_!YyqH}dKdj;4n@$dHqe|Q_$CZJVdQ*$4Qm4&hpE_4|h73U+h2+TClouZw5! ze{qngKlo_0M-!#cc6GuGJC+*o0lN3mfZuTXM-P9Svf(gc_G%7i12QSI{5Z6wb1JNU z#|y)teHtHc^?B0B*QkH%{JW9D23WR|Bpr^0yW&4P-EA-D-_83{SD*2oF7E%Me=uE| zZhkb{qtrfZ>r=yq)hW`$DU1yNoiXA-e-%H?$9E5Z8{uzq#&*Z?Yz;z7%`c6>{iHJ0 zdAk{#_$gByxO=UT++i;^UcqW~ULK_~@mPVGFIn@U*WLJX1-m(1?d~<)*TpmV2;8H9 z{${?rJ!<@m;#c8V2Cu?ivfPac`!Sc;1Gi%ueq%j0znl$ZxdtM6XbeB{*i4cSf74r` zR}^$t<9+TcSdH97flX0AUyn`Pqlle`wwFO>wXREzUGArK^#DF@fY?zt1EM@!6EN{N z1cFs5E{2=p#qlst(l5Es4~;D4#fq}Es&j`H-`O&G@jcDDxvRdv?XF?R{OktSz%7gE8Xg5CCHHAL08=SLR8?&56Rm?rEmn?Iz)Pm_ zF3ieGn;;lCTDP?|Mr&V`EL)L0{qKt!`v}!f`Q(lrOvZ~BAXi%gL)Lc~I13Bgs@z4( zEcg^F{2ILxBm4dUJ_QyxUGBn&+mk8N112!SZ65`KMJ-W@YDL3Njo*8&e^#vov#{3P zNPYxZoy=jv^Uhf>*v130qu+Y|>c7}ggI{MYC6+3Ps(!+V@SL@qQ`m_JG6g8GQQVz1 zx4M41qj#SY7f9+g)me?%HBm+koi6K3APzo=g`(D|m1tHPgi&^$ zo9)LD{+bAf!OzGlDNmNIfB&`uFWM`y>=S8PB+5o(Dds*7G;gQs{rv%av{&Ok=*?n) z_NWgAUd+h~Hb^9?+@D{cYL(&;@tmMdU|fPKg!4Jn>5p-%C#!5iRTpar#}EC$cx6Yw z_x-KT1kM4K<-geAZ&1e2Mjzv)&U*8H5XaX?pjIOBSfyut?M@Uae@8YfU-+|Noe2a> zB)*)e;^o+d#+o@cj73oe689H}-Ghi~j7%aqSq&1z2lV(uU3jakOn|$;?GTU}lvx{y zTWD4*E+{maMNple&tzryMH;Qj;k(bBn!;=>3lpv$!B-didUwQnGzZJ_xMp zCH&G^O-PBF&@=gue>p^mvOI(St7R5mLO{$kLnZoCr#RuF@2`UvoF-(JV`MY2=g-^h zlIl6BoXyn95vv&h>@ zvwoXJZnrWglyri5YooTIkx_)&kzt=>=rMQEi^?spdzSH9<0@L51b`TYVU z3aCnz(0b?w8p?CtzgAWh&Z*a9z83K znh<)Iv@6XAF>O*`kOSD~Vt6SvUyH;!871ns;4G-ZAm@2k?`1)h>h%`1jjw`tQ#P=L zg<0FY`%~=jZZUP*=mj%X20ViJL$04;L<9I8$}=?XK&zcQraKrMbY|bXc@|=>qSqyj`!kmx}2ufjx5Qy>5ZF| z?79`(=`dUUocM`(!L%g%GiNRSKJ%kaJUUfIe_1_m6K>yYziFLuStm^~UGS z6!WzN#u4PP@l7<81)*o5_3kEm6_FbgfM}-P9%zmm;RPF;FfPqS0V)Xg(-&|rI<4k| zfA(T@dWMf?EZVC1#;mP^H&8;76XVcL#8q-~Q4upv$qV;JZH$s2?0ot7_IEag zkr8V>!zmN771i6`h>`S#S7g5hRLj{rxF=kC&R1L0=SRzU#VXg`=W^s&aa`k`Xz%Ff zZo{E7XiYbhVPVUaF;}1PppQK+2!aq7cl!64A9kjT^nO7kDogDK-ZH0N$%U+I2V-8F2gS;eM7EfhN0_E?#2~)T-ZDq`uidBPidZmJ zmnrDAS(Raj3Cn|aUG&{uSj|zuc1!1oMo?vcjZ&@Ewx7i^>U`GzSj5Y`knsEZ?^gZA zH=2j%N5PuuOH@lpg2F8?X6cAmf2~Ff3S&J;^V9oa7o$2=GiKn%YtLQro;v_{`WOpa z>igV}j4Xgy2m%4M69pu+NRfs9Ws}g+CgYb5&n|SKB%X7~S_Jo0if4Ujba zuO!YOd4TLdQ_hak^H3Emf1uX0_!ur*4hy~K>V;5=VM?HWZSnU};SF8g89O&$%?6C&-u4K%-3pU?-@^^ zd$l?HjSlNzcg<%Ia zf&Bb5pp~sy$n^ZG7#KMX4sM8xxq|osK9aJ)%5XBf>>VQO%^SzLu0)qa9Z@bhF2?Jg zeUNs~UKeXRuB+5Le;^Q5504qg!-LLIyjJyO?e`=qYBev08Ro-qJ3Z2%fl+!zQm)%u zz6_!Q9mP>Wm^7EaNs9j6_b_c&*-U>`%3loA9zXmK_Tubc_{#cdHxEVi_HIC*uOZ#f za+U870|3VH|3JIQR1Qk54PC?p9!+@v(|X+h%O><6zCB7Df25V@CmbZ~qFNXJ@Du#Q z*GDiX*R)Jnj}QowM-tPIAHiP&1n@(8KF{`V`UwBls!6cZlM=ZOc}%I&2m;_utc_7S z>ALYWN`Se`;p&)6{QW>VXskPH`q`xSwUUT~194}48!$L~1aTU7k}w+0Of%XNgI~Fe!1FxrJ*WxjjZe+!2t~l zg4eZsv8?+JQaQAiW!)RSgv zQCtvggV$IPh-VfTA-=grsdBvd>Qn1JBlVyfGV|QekGY157Wm5}k*!0QaDz-UNHmdMI^B-EPk~dUX>Hu4$h7!eL->%=8$~=O{`+z` zyV}?xLSa6N43-dy9m2I3c8A9L5TZGC1qFnae>DnOsjnXHb5$=cVb zSv)R99;su-%+>3Q>4X8!R((f`<~vgK!4P>BX8;ytC)qzJ4oVEo(e_VwCurO{g~Ehm zf?qbRn)uQ_?e0|7NJy0d2VOQz) ze@&noGZB|##$#;%?R|7T{%#(I$rqc2225 z&iC!(0~sK0OEmbH0vVuw4y3ycw@5sPFTa*`N;DbTp(lD(O2LB*no`n(*f7#ts zjH{1Tv}#rYlsJ<>iz`{@)u9+6>Q)`p`K2a0>9c7cSQK(tWvdJY;1wQ*l5?c4Ybpt^ z(QaXg4iq0i2j>LCbZL-2ldeU5w>gFDNbWxa#?q&-vbok9z9=BZ+z1WQwTphFV^WL& zI)sENWJIo8p&bE{QAT7(IRr zi3=6{>U~VS=tA1tN2zvbc*3_CN5Icy!;jxS#@NS+xyx6fraQlCT6zI_7&Al%Lw<*3z!EJQ^u}lx8xy$>Q(k%0<6mT9Z z{dJdJuJ9S~6Zf6Cj_p#gee<@r+l67{)Fe_%F6j;mnm7~OxWExwjx3^hHH(`N0X0DY2(Ic6MGx2n?l z`0ZoN{-W-_r!|$iOEff0jW&^tIib^4lFS|Ilqv#HtOX*D--v1Q6*dw|b0jKSjY~;#}=x z_Pwdn)$O6M*s{{L$BgLn(Qy-w-#*6duTug<-UV>*r7 zsO>$@oB3(Sj5IR}b8P$V+nD`zhDRgeOdj9Nf|%<2gtwvnmyf~QBOcRjOZmR&J0g$* z-+4_0V)&#Q0CpR0n)gLu9oqR-1OnlQtI4(A+xBC(_q&a48*z*tJ9f-?Z0zydy9flR z-c$Y}EMQGee~5#&8IQ63x3|&bDQ`1wtNFg}dy3#4DfnQ(k>;&rCa!7i7zop=a9OCH zY9#FNpi=2U1PMgGqim()H0`@=CDA@t%1V2w-{XXr3U=vU&LoQkDzg1P@*h4^0 z*8}mQsO}n^ea_@0_S4zl(R$XIESd7a9zA>$*Ok!he;#HDl&D14>9Z+AP8s2}fesoB zAt0y_$XrSlM1I457H-Or?H$tZQ3W;LVML8~EG~2x<;0(K_gYj-PuoivkmPmx-OVvE z5ds!@@;4ps*Qn6d$&&9W5?*3{QG`AjuVkMhx#ym!t#hC_UPKF#N({(8YFct(4ppFP zvll{ge_gcUpoT*8$m^vV-!Pg6QDg%Q$VgwVH!Q)}%2*y? ze-+W0a8mi}F&%;$X$!sOBvB{!Q$TlO6}%7F#H zC`X@+SVAy60C(tsm*?!|G+A{IW>9mWYEI&_@%B>MU)Y%zn@XTXLNpY{fo(Q=1>)Jf z59G+S3CLa?R%S~xFSDl8Evq9OCaXd7**~|WvDB!CqQkYSeuzJCM6m*fLtz6ubMtr% ze-(JK?D1hqB&^S$PL=3N8MR*GvWQG-5=VoYACOB*Qcu3VX&QM*Ls5)KGmHK@bjK9n zU`+wxa)fGY=q>nvG7wl&*d21Jvqj&LB92meRf;|t@obf3;GlQ_d@8!u(MGgoq+)we z`{DsH$*5Gb!(YWA!|T^5wZ5g0D6wmBe~us`)U$aXND+PZvvL158YLlTvZoL=AZ5&* z(&MS^AOk|B_9}Kz95s-+K-d7Yq-;wuQwY4MUF>i&cOXdlCO`~T>U5~DUb&YXP^N(0 zw(LF*%$NaqicZUU9e6`kRcRa|G)r#NWUI>W(f0msyZMIe?mxqMlwH~2Q@vxl}JbVDm&)4P3w#-*$jcQ4jwXi z2@q*|Adl32FGL8|4lL63QX66rM*d@MCIlEq6Hna6+!CLclU3i7B0a~8Qq(?~e}Uql z65bTif>X&O8MbSxF*H@)piEgBCc_WJAuE(=NgLTF#xT9ClL;h6C-Xj#eLCV9gxeGNS(;>Jxq$ z^NOzp@kCPw)~bE~Y{B>Pf4G6Ccu|5r8L_pao1s28>jj?WBqCEg{2V!K8ncSLHshy# z7pnD8-DosPlB|GkI)=;-iox+nex6QyUx3U$0pApBRPJ6Eq?g&E>+T~lcVR{j?2xQC=@VeREA(pjwE_QhU_I>-Q`{hUkj zY|(c@zW6BdRU!Id$hUUYy|ZFKGV7?y-wir*RAEbp%Sfszsh!{k7(l!o7+GDk=uxL5 zll_9^Nuww8K9HgTfA>|z(}{v~$;uo6QIhB|MvBZiG@WjT-fkh)#73O8W=e-$D_}g^ z7`ARxBWUdcK&w6{ju3xM$7!Ti6gMH$F34t1dvv#Y?GOmzk2%e}BBEi(Xm&EW9IZa^ zpcN$$zCu-1nj7$~2G1Ob9C%Vu=2QtPilTdmr{yg-ho?xTSkZ;q7we;~BX!o5H{VNZ>- z4!m=sZW6s?f6kRV|6_JlnK9+)1TCs98iLT2!0R=jt znKO5EVs&(e<}s+6Ud#!=8F}y@k-g~gu(}~hH-OrO%lcDADXC&}+Wz2;sn_{z-Uo6N@YO+ie`#C@DSNprfOmlg=Emir&iDfK zUZ7mR*(R8(pcsDU)#}hp-ArLOP?}`@y*6qq8=b)B zmzi*!f2{hBX=HaGX_wc;=##~=k?J3eP=p{TeM2VN?sOtF_98mG;|e|VDoE}wF)D5Z zg{<;%n_!3rQb-|Z(>{=*D5#&2B(-L|cxg-|oHGwxOx4>l!=XwW4dT-P=Cw(CuLP#h;WR<0MdEVwyvWI{4?Hvz ze@Z*ZlEg}@*ZpZrW3cyZszkpJ`Rhx~tyk&7WL|MPfp{kg-mykgGwnplk^5&%1x7 zTvq#6l-A&J7_!rc9vgBF$=jT)`JN_1f6QJC7Nz_@u7en#v}VwKXVg{DU+LcsYLf*5 zp-hn!J?Yd1Ti%?wav6kR%D;7|?#Ux)y~=NX=n0ND?w>pc3(}+A;$5&p`Smv*so+c? zr9AFTCVimx&Wl+TWf9>L>I-5+RXq@8J$E|X1watzi-vo87!;4XyhPsn?U7>pe*=%y z-+83^Cy&f+=S_GDOkJ0MI2l@9`C@8HLGO-CO1$5ohk|(~XSdG$sKXY3^A?*h~()SsZSN@t{z{Eo) zS3XTpc*+JAAvtV_Q|upY#_y{>w+JZ01Bc`__4dKA?JOdv_XSrr9TAEohe}mV8%*Oa zEI51qRdHg9&|YsMDjNuzfBytP=jX|+4QdRvGefsAVL+Sx;Q1qRS_`+GZux? z34WAduYDHLWQ?))(cMH=5R`(au%ZNjY^E|#zs!vkG*~_*?Ru;iac^5WfWk17zRie=qx(UBhOloGtsF z4Dp>83w6`@qzolk@Yn;}4F%2-BEm&_IdyYIDshY= zGRZGp2jA>ov>3{1e-Ou;!uylH1k65r8gN1vs8lBE$;1*^pNgYDWFg_|n3k1THWgas z1?0lt!q~-ihyH{tW4NFQ71J;)pU}VZ31$r}q(S{C#r>j-S!zWKNM&U{I$J4}u??pml zggR0Riq=ka3Yaa`G4T?=tUqo=@tCY^>lD~D@jHEQbU`?4HIxi`M5O_`Kck-8&LM%4 zEcTGIRo@Yze;D}H*F-3PFjzZ70g)1ENw5;Q3=K2ci8*8~y5^#=?1XED;(`l7G+O2~ z8ddBPc^SmQgNi23=6xVW6x!;>lq9C6^1exfaPER3qk;hxnhE#Gpq>sMu27X)CV^FC5Vx*R|Y zphgY|e;PeSha~_h$P_i;LQc;~o8E_Qn(zfmg2Elz*ZCgM+_H$ELJg27hb6H(-V4T-af;)lKoL+wv|@fq|Ag6?1B2qW%1c6aI%* z(VHp;iNVCC*gnkPMxz`kpG{@m@5lfAc4~=73zBSuvS}{h3vi6+z24+oAY@ z>PdX}5z>KC07@V)MD?1edccgNbReyuc&*jH{e9{OR6XECIw}%}Gqlt^;)IZp!a|~h znnxnIn5Jgb9LsekIzW?JwIb-eQ>ekFL@;$zBGf5;Q8@)jjomMGdOpX@W`Jy&RC*7L ze|%GW{^#OHA=0cYR;~)JfM{7HjieRK7H3YI-fo%N0EURuAfYZyPN`3pH z)Dbh+tiNCX9iASWR7rr>#43MMti-ZbvL6vM7Nwq|DJ1$sliv^KA1wg_qB+&2{JHPv zRoqOet54BUGO~aB``i!ZDyST^`mHFoe~4ecG4?xBKFV$rvSAaO7*H5Yv%^0Xqr%KG z*-%4Mg}@Q8(=;(|tF(@YbJ#?43TBn6%}qGrV%bm$J{MkX-cBJYFuOdiGbT?=Pum(S zR8<(x__kow;%&fdX2*^=%5s%(7{)`8MY0dkjdWdos}s;{sI1(~HNA}OG> zN{EeWgk*OmqZEJ@f%{Wuko#N(VBe>GRMZuK?I3srRbfcgYl7&P=}{;bb@!CR>@hFg>6;i2|(yh#TvWzXlmj)ii*) zSL&|FR+=@&nITmRV~TQ}u~8qxUyIa*r_L*73gR8sRlZPlTWz?hq$^;fF_c03yqqoh zUPYJw?nM!5pA{j~)D(+%f517NX{id_DP-?X00R}Va5zB8Riz|H3GFJ{Q%B8#geyzj z&Zd1-T}sFftx=pv(Cat=n~tgj)@-W3(rlTI6?isctX?bFGa%NW%5>8mlA=!ACi!`C z-liGNoU4*WV4k$Ts390D0^-u8=H#c>F)+>$UnVcqj~nLu8#js*9wIf?(O_Mo%eB-vTY{$2sVa#13d*LR9PrL1Evzm5g0+> zp-weR)uaMZ+o{r2f0tlR)Dj~KM2D&sAOxB!PNggqx;j-gB$^P|-wErU{@O%q$q_KA zw6hn7tQ#(bxpyRv`kF+g4TmT;pm0>mG5acQ9YC zYs~Ug9QuZYzXD zGUKvn094Foe{)l@mGh#fQ$1HIif&vR(oPnAubfMjY%j{tCySJ`a#S3(1UNd1_}5?D z%9g7`t}@l`rGBIsl8~NM_B(WYVOrLyP(Xo;ODOPX^F9(HLcwLt3^}PJ+5Qq#f$Wj< z8s&NoVH3JVYF0&Xe{;;HIF67DRim7=m7r%PQ4eE^f8e4O0v=TnCRr@e39+-gPcFR3Itdrhh3SIDg>$Vx|ba5x^S4|8ycE>)+U zt@@4_3F7C)=sh2Tt?vSCpsO zS7@_Dh1tu3Q&9kdNWs~X?}-qK@I|Yr#Loh`fA&7-s<9l!Taj^%8vP(@{jCab;A?ts>}TK|f}CMX||76_*7?of!-ZzS!J_!kLog3f;Xa9sMm7QXwQ#)R~gQ8mlaHs^`;~vW)@7SK}bQTBA-GD?0WdM}SH}5(9iQQ`&wxS@S*Q#_+mf zT|SHjJOaTNb$sTa%S`WS3ceI}*h$I)X))(EK@>zYWf>ys64EAas|%L=sh_9QJ`y3| znBj7uuaeM}w~tQJYhv_CXbqaMBi5@oqZ9|V5K42=&UDlJe`EI5 zaJ*iHz0yf9q(-q<z<3F)?wCVFQZA4cRb{j4L-1XzFF(~b z_+KDL=(z>B#UHC%`)tj3gh+4kRYmkkRE+`2a9OL1Eh=Qq_O+?#G_F;25%2@w1Wr>S z1J@W{J#~IU)v3eDa!zJ_Btml;$bTxsq^e2J-~mI2CTv6Z16}r9DlWz`1=l3A2`wS2 zXdycu0<~j8D8q0LLlHZ=yk>N0pDqqfOuixNM1F;mqbnc+&5B+aYL=aDF`_@xCR7Zx zELB4&GYdYEE*etTuEnAwC7R4O+Sz$6 zJW(TdQoLV_4k=7w=Tqv6LrZs4NAG%}m1PqZ2PA+*9scFGI!b>(>jN1g`W7HlEmTs}nQ2@T(M#x0j=gag3}v z*SoX%nsX-ne%bddYgm_FOrDibhO8+GtFkO9q3A?Ge#t#gMad^s2WAzp5pVH6Q1p|7 z)Dvq2UPqdmVh^@Xqu=L!)IDkzYQ?+ZB@fA;cS4h~$eLqzO7+#%Nq@tKU~%n$mMHc>n^eV2lfdS< zTzL->$U~13O4SGjZFnm?2yO7?6iP-iB?5||m8g43Lf^br;P;EZ7i{D^uS(G;1J=YS z68!C=ppr`a4k) z=2L~X`k?2;wve^uSx+$Lybl8cF8k=QpH>%1rG4_9R1rwl7pWp4KdSo2ro2Z~SNil| zcmf#;p^VLo?Nv=FT2w~c1Z75%!729)y#q+1#%2_myjJdNxbd%}63CW#Jx} zErG27#7>Q6HMf7A^br)o75_ysi6i1ML69&)jim`P=GgV6#{lor3ph0xTvY%w@O^gBC9l4 zNfr@G)Go*BNMUw64=UGt@|4va#2WxRGpCA_0iA>@fJNN8 z!1A0Sq7%4rd>#rEy)wH9My?PLn#!9}A3cP`JAW4ohzkNx9!UaM$kHp{MW-bPE4X=9 zD2>gcWCmzhP<2qs0EgtqDx2+NKT^Qc8^hmG7q-jUn(xSvKzv`k%4+(gDzYW3Lm8B! zke$4aBWBt}GL#nDo99_WZOBjo+Ax=Xz2ahV3_H6Sx~F8nPy0A+2FH34_O!XgD{aQ< zO@EG_94RMLdaAW78G?o|<&`}ZDU2|m^K?yA2|luu8IP^566ulha7a~ej>2x@wwr=| zvKvOFt7?&Pa6>xcQtqw=BlX=3*{|oNoMsUAYE^6BSVpzo;1VLT(J{E?6k;6hl?phZV9zoT&Kgp5)Ee5xI%kHs#V?6`=dI)!FYI}!kC3UFbKph)Zv2godvPJhK> z6`N8)WIIM<@v6p*nT@-}VuC%Cli@^UpyLM=PiTKz^F4x_#Gkz=KIOAID2xysr-n^& zwz9OERsr;}6YfPFye1J<23qAqnafivh?sWT&Y9wgXVX3sA%I5#IRzQVwye*kgYpc* z%Mttf>h5w&-<%j4u~(y?gd6!@ z8O8NTu7cKQN`#i65{JrsGE-9gG=2NgYpgmbxh%6$m%VO5sCcGOf`$On)TTM+6`X8% zZU6UW-xDK_@~dL>$?|GE7mD`8Vd12rzJm*AkQW}aW2}NN7_<%Q_sCh6Pk&U2ng}O2 z%+Yn$$-IvuM9OKE>^BKbVIqNzDF_MVQi<=RpkW9>$nD95fN-aBbj>z=PW_&m`wm|~oD}PZQB1qr zi9;>?=sIN?3+s*`gRs7(o+u#^3REK+3%0ZvVC=`@HCvKsx#fArq5!8}-%ok;@M6lN_D|+sD*!q1VQFs=Xuv@0!OR?1Ne9-nc1>4SbcPiY zx4Q8Jts9iWrN-IOssWJgp7v6K5O}_H0>qVpcf?*6IwyCIDjJFq+eoCvnS9Lg3A@^W zTm_1YTA^_O!(FG zYyD)fnpPI`v^<1XRaEm!_~0>;uI?nwr#t9kSQTh3IQ$e7tEq!5se%FE)5M@!$Fpf4 zwR)5V`~m-gnHeerQ7cfK-Pt+Jk#631M$OKmwunft5Pz$y0N^39s97ZOG@C?^HdeCm ziQbBqR+}PWE;K^9RHB4UP~yS_8`BY_K^zuaSrhOkX$xi_gHwbW@m@r*!PFfK|3|GZ zgdkwIqFANco@np{#@tM9bR(+;9_Mnl?gt>y^~GL~_Q{|%x#S|(8Dw`{j%&}5BfnEN z+PotX!hZ;}PHjb$4bBm~$*c%a)5$6Xkofa-*2m3@CXQ$h>LbJx>+NqPv;CbmK(B2c zxc4}JY}-(-iXOA%7~deyr>f#E1P)f=s7+t61VW#eVF##mL*&_BjOOe_Fr91oht5$d z3ZZuOY2&3)`e>$6*4b*pb#xKN-oc3L(^F@<&ws%(>Oa2h;8pS5s zm9u#t%TY61Yf5>ktH;HtQm&()OOsw=#At>h-9ZK&Tm*^H1Z_%mD6_A2l3~}&zcuYje&{S~89?)I^CHLr`9ICVN*cI=6 zB2*IWmsvHGHS`r(*16 zfo)@ZblRVDH4*2pm+v#wUL>Td+<}xKQPSxm5LsZxpQ;2=81qC(<~A01Ob2y7aRb|5 zH(E;g`gR4tW%Z3Sbwx7 z<;&T+&!s4fko`q3zvWL#kx4|4;xitK!A?ohRAK-++9pb?tHgki%8Q&JsSSyY8X}TN zWy6A``*}L=13?1+dn8fa3Z5*xAc0;IM5&8aOnWI~J?jy;u7G6Vk8RhclizB4Z7XRI zX^rne2u!g?M!8O+$c#B*PnOo8(tnIUzScEzi01lhQU92wlud^bLnwL}yQVb&SCBxoraCw0gnFKbf095TJd7Wq%dQHBEAXY)RiBHVWoTTT%j%YVX063raJ zxB!s4;`B&56J7Qwk3#*SBrDlJa9{@&;WANCTy~Wt9b*JQii;8!MG|Ry9^iPf=}69^@Kbn#(9}fZQx`yS z&}zP~`<@_`H`LJwW7l&Ed4H!leR`n|45+g%dfM#OL%|amvjcP}P+9a&%?>C!g?LZJ zpz;b{2zfT`1H83yEk9Is`z&gqG&7#K4W)xAQB-ZaI%x}w

    rM=_%YR1Loi3kVP36vHtPc+0&JwJ8&T<| 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 ZS=o76f_-kGf#Ko7fo?uNvB{sh{U243^eO-V From 05fa2154ee3d54eab069bea8f2451b038c99e815 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Thu, 4 Jun 2026 14:55:01 +0200 Subject: [PATCH 29/31] Tweak binning contour plot. --- .../binning_trajectory_plotter.py | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/wetting_angle_kit/visualization/binning_trajectory_plotter.py b/src/wetting_angle_kit/visualization/binning_trajectory_plotter.py index 1dd0d8e..277ffbb 100644 --- a/src/wetting_angle_kit/visualization/binning_trajectory_plotter.py +++ b/src/wetting_angle_kit/visualization/binning_trajectory_plotter.py @@ -154,6 +154,8 @@ def plot_density_contour( isoline traces when available. """ batch = self.results[result_index].batches[batch_index] + dxi = batch.xi_cc[-1] - batch.xi_cc[-2] + xi_f = float(batch.xi_cc[-1] + dxi / 2) fig = go.Figure() fig.add_trace( go.Contour( @@ -161,7 +163,14 @@ def plot_density_contour( y=batch.zi_cc, z=np.transpose(batch.rho_cc), colorscale="Jet", - colorbar=dict(title="ρ"), + name="Liquid density", + colorbar=dict( + title=dict(text="ρ", font=dict(size=16)), + tickfont=dict(size=14), + len=0.75, + y=0, + yanchor="bottom", + ), ) ) if batch.circle_xi is not None and batch.circle_zi is not None: @@ -181,7 +190,7 @@ def plot_density_contour( y=batch.wall_line_zi, mode="lines", name="Fitted wall", - line=dict(color="black", dash="dash", width=2), + line=dict(color="black", dash="dot", width=2), ) ) fig.update_layout( @@ -189,10 +198,26 @@ def plot_density_contour( f"Density field — {self.labels[result_index]} " f"(batch {batch.batch_index})" ), - xaxis_title="ξ (Å)", - yaxis_title="z (Å)", template="plotly_white", - yaxis=dict(scaleanchor="x", scaleratio=1), + xaxis=dict( + title=dict(text="ξ (Å)", font=dict(size=16)), + tickfont=dict(size=14), + range=[0, xi_f], + constrain="domain", + ), + yaxis=dict( + title=dict(text="z (Å)", font=dict(size=16)), + tickfont=dict(size=14), + scaleanchor="x", + scaleratio=1, + constrain="domain", + ), + legend=dict( + x=1.02, + y=1.0, + xanchor="left", + yanchor="top", + ), ) if save_path: fig.write_html(save_path) From 1585a4902a3152f4837c6d8af70184539977c20d Mon Sep 17 00:00:00 2001 From: gbrunin Date: Fri, 5 Jun 2026 12:02:05 +0200 Subject: [PATCH 30/31] Fine tuning slicing trajectory plotter. --- .../slicing_trajectory_plotter.py | 88 +++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py b/src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py index eb44725..78c537f 100644 --- a/src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py +++ b/src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py @@ -86,6 +86,8 @@ def summary(self) -> list[TrajectoryStats]: def plot_angle_evolution( self, stat: str = "median", + per_frame_std: bool = True, + running_mean: bool = True, save_path: str | None = None, ) -> go.Figure: """Plot per-frame contact angle as a function of time. @@ -94,48 +96,100 @@ def plot_angle_evolution( ---------- stat : str, default "median" Per-frame aggregation across slices; one of ``"median"`` or ``"mean"``. + per_frame_std : bool, default True + If True, draw a transparent ±σ band around the per-frame curve + using the inter-slice spread within each frame — shows how noisy + the contact angle estimate is at each instant. + running_mean : bool, default True + If True, overlay the cumulative running mean of the per-frame + central tendency as a dashed line, plus a transparent ±σ band + of that cumulative series — shows how the time-averaged contact + angle converges as more frames are accumulated. save_path : str, optional If provided, write the figure as standalone HTML. Returns ------- plotly.graph_objects.Figure - Figure with one line (and ±σ band across slices) per trajectory. + Figure with one per-frame line per trajectory, optionally with + an inter-slice ±σ band and/or a running mean line with its + cumulative ±σ band. """ if stat not in ("median", "mean"): raise ValueError(f"stat must be 'median' or 'mean', got {stat!r}") agg = np.median if stat == "median" else np.mean palette = pc.qualitative.Plotly - fig = go.Figure() + band_traces: list[go.Scatter] = [] + line_traces: list[go.Scatter] = [] for idx, (label, result, dt) in enumerate( zip(self.labels, self.results, self.time_steps, strict=False) ): color = palette[idx % len(palette)] band_color = _hex_to_rgba(color, 0.2) times = np.array(result.frames) * dt - central = np.array([float(agg(a)) for a in result.angles]) - std = np.array([float(np.std(a)) for a in result.angles]) - fig.add_trace( + per_frame = np.array([float(agg(a)) for a in result.angles]) + per_frame_group = label + running_group = f"{label} running mean" + line_traces.append( go.Scatter( x=times, - y=central, + y=per_frame, mode="lines", name=label, line=dict(width=2, color=color), + legendgroup=per_frame_group, ) ) - fig.add_trace( - go.Scatter( - x=np.concatenate([times, times[::-1]]), - y=np.concatenate([central + std, (central - std)[::-1]]), - fill="toself", - fillcolor=band_color, - line=dict(width=0), - name=f"{label} ±σ", - showlegend=False, - hoverinfo="skip", + if per_frame_std: + std = np.array([float(np.std(a)) for a in result.angles]) + band_traces.append( + go.Scatter( + x=np.concatenate([times, times[::-1]]), + y=np.concatenate([per_frame + std, (per_frame - std)[::-1]]), + fill="toself", + fillcolor=band_color, + line=dict(width=0), + name=f"{label} ±σ", + legendgroup=per_frame_group, + showlegend=False, + hoverinfo="skip", + ) ) - ) + if running_mean: + counts = np.arange(1, len(per_frame) + 1) + cum_mean = np.cumsum(per_frame) / counts + sq_mean = np.cumsum(per_frame**2) / counts + cum_std = np.sqrt(np.maximum(sq_mean - cum_mean**2, 0.0)) + band_traces.append( + go.Scatter( + x=np.concatenate([times, times[::-1]]), + y=np.concatenate( + [cum_mean + cum_std, (cum_mean - cum_std)[::-1]] + ), + fill="toself", + fillcolor=band_color, + line=dict(width=0), + name=f"{label} running ±σ", + legendgroup=running_group, + showlegend=False, + hoverinfo="skip", + ) + ) + line_traces.append( + go.Scatter( + x=times, + y=cum_mean, + mode="lines", + name=running_group, + line=dict(width=2, color=color, dash="dash"), + legendgroup=running_group, + ) + ) + fig = go.Figure() + for trace in band_traces: + fig.add_trace(trace) + for trace in line_traces: + fig.add_trace(trace) fig.update_layout( title=f"Contact angle evolution ({stat})", xaxis_title=f"Time ({self.time_unit})", From abd7f9ad286470bfb164cfab314ec1984a42b4b8 Mon Sep 17 00:00:00 2001 From: gbrunin Date: Fri, 5 Jun 2026 13:33:35 +0200 Subject: [PATCH 31/31] Added time unit tweaking. --- .../slicing_trajectory_plotter.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py b/src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py index 78c537f..bba6ca4 100644 --- a/src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py +++ b/src/wetting_angle_kit/visualization/slicing_trajectory_plotter.py @@ -88,6 +88,8 @@ def plot_angle_evolution( stat: str = "median", per_frame_std: bool = True, running_mean: bool = True, + timestep: float | None = None, + time_unit: str | None = None, save_path: str | None = None, ) -> go.Figure: """Plot per-frame contact angle as a function of time. @@ -105,6 +107,16 @@ def plot_angle_evolution( central tendency as a dashed line, plus a transparent ±σ band of that cumulative series — shows how the time-averaged contact angle converges as more frames are accumulated. + timestep : float, optional + Time between two consecutive frames *in the trajectory file* + (i.e. dump interval × MD integration timestep). Applied + uniformly to all trajectories, overriding the per-trajectory + ``time_steps`` passed at construction. This is **not** the MD + integration timestep — it is the spacing between frames as + they appear in the dump. + time_unit : str, optional + Override for the x-axis time unit label. Defaults to the + ``time_unit`` passed at construction. save_path : str, optional If provided, write the figure as standalone HTML. @@ -121,9 +133,11 @@ def plot_angle_evolution( palette = pc.qualitative.Plotly band_traces: list[go.Scatter] = [] line_traces: list[go.Scatter] = [] - for idx, (label, result, dt) in enumerate( + effective_unit = time_unit if time_unit is not None else self.time_unit + for idx, (label, result, default_dt) in enumerate( zip(self.labels, self.results, self.time_steps, strict=False) ): + dt = timestep if timestep is not None else default_dt color = palette[idx % len(palette)] band_color = _hex_to_rgba(color, 0.2) times = np.array(result.frames) * dt @@ -192,7 +206,7 @@ def plot_angle_evolution( fig.add_trace(trace) fig.update_layout( title=f"Contact angle evolution ({stat})", - xaxis_title=f"Time ({self.time_unit})", + xaxis_title=f"Time ({effective_unit})", yaxis_title="Contact angle (°)", template="plotly_white", )