From 7a0457348f2fb4e24e2e2f2541c7630c9a9c5f3d Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:51:46 -0800 Subject: [PATCH 01/47] update install instructions (#1744) --- docs/installation.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 18a03aacf..507c36921 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -9,9 +9,6 @@ We try to follow `SPEC0 `_ as fa - OpenMM 8.0, 8.1.2, 8.2.0 - **we do not yet support OpenMM v8.3.0** - ``OpenEye Toolkits`` is not yet compatible with Python 3.13, so **openfe** cannot use openeye functionality with Python 3.13. -Note that following SPEC0 means that Python 3.10 support is no longer actively maintained as of ``openfe 1.6.0``. -Additionally, if you want to use NAGL to assign partial charges, you must use ``python >= 3.11``. - When you install **openfe** through any of the methods described below, you will install both the core library and the command line interface (CLI). Installation with ``micromamba`` (recommended) From c4dd615b36c18cf96ae32576b2716d3677dc95fe Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:56:02 -0800 Subject: [PATCH 02/47] update manifest to include all .gz files (#1746) * update manifest to include all .gz files * add min openmmforcefields pin --- MANIFEST.in | 2 +- environment.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 810b641c6..6e18d4c72 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,7 +8,7 @@ recursive-include openfe/tests/data/ *.graphml recursive-include openfe/tests/data/ *.edge recursive-include openfe/tests/data/ *.dat recursive-include openfe/tests/data/ *.txt -recursive-include openfe/tests/data/ *json.gz +recursive-include openfe/tests/data/ *.gz recursive-include openfe/tests/data/ *json_results.gz include openfecli/tests/data/*.json include openfecli/tests/data/*.tar.gz diff --git a/environment.yml b/environment.yml index 06dac5663..3a536497d 100644 --- a/environment.yml +++ b/environment.yml @@ -19,7 +19,7 @@ dependencies: - openff-toolkit-base >=0.16.2 - openff-units==0.3.1 # https://github.com/OpenFreeEnergy/openfe/pull/1374 - openmm ~=8.2.0 # omit 8.3.0 and 8.3.1 due to https://github.com/openmm/openmm/pull/5069, unpin once we've qualified 8.3.2 - - openmmforcefields + - openmmforcefields >=0.15.0 # min needed for https://github.com/OpenFreeEnergy/openfe/pull/1695 - openmmtools >=0.25.0 - packaging - pandas From f55571a3adfaf3f628681a29df6da3982d24b989 Mon Sep 17 00:00:00 2001 From: Mike Henry <11765982+mikemhenry@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:00:59 -0700 Subject: [PATCH 03/47] bump cuda version to 11.8 (#1749) --- production/environment.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/production/environment.yml b/production/environment.yml index e28daf6da..e5447364c 100644 --- a/production/environment.yml +++ b/production/environment.yml @@ -2,8 +2,7 @@ name: openfe_env channels: - conda-forge dependencies: - # Issues with 11.8 on lilac - - cudatoolkit==11.7 + - cudatoolkit==11.8 - jupyterlab - notebook - openfe From 951b887b136c74b471d6d72e825536012339e37b Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Thu, 11 Dec 2025 12:11:31 -0800 Subject: [PATCH 04/47] fix deprecation warning on view_components_3d (#1750) --- openfe/utils/visualization_3D.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openfe/utils/visualization_3D.py b/openfe/utils/visualization_3D.py index 25bcb5646..351d559c4 100644 --- a/openfe/utils/visualization_3D.py +++ b/openfe/utils/visualization_3D.py @@ -137,7 +137,6 @@ def view_components_3d( py3Dmol.view view containing all component coordinates """ - warnings.warn("Use gufe.LigandAtomMapping.view_3d() instead.", DeprecationWarning) if view is None: view = py3Dmol.view(width=600, height=600) From f3427bf96f9f6fbf5320ed4ec2d45569b2299221 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:38:45 -0800 Subject: [PATCH 05/47] expose KartografAtomMapper alongside other AtomMappers (#1751) --- docs/reference/api/atom_mappers.rst | 1 + openfe/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/reference/api/atom_mappers.rst b/docs/reference/api/atom_mappers.rst index ec31b609b..b30d441f6 100644 --- a/docs/reference/api/atom_mappers.rst +++ b/docs/reference/api/atom_mappers.rst @@ -21,6 +21,7 @@ Tools for mapping atoms in one molecule to those in another. Used to generate ef :nosignatures: :toctree: generated/ + KartografAtomMapper LomapAtomMapper PersesAtomMapper diff --git a/openfe/__init__.py b/openfe/__init__.py index 207b84efc..cb689bbed 100644 --- a/openfe/__init__.py +++ b/openfe/__init__.py @@ -71,6 +71,7 @@ from . import analysis, orchestration, setup, utils from .setup import ( + KartografAtomMapper, LigandAtomMapper, LigandNetwork, LomapAtomMapper, From e5a3c876f3ad209b7d1c221c57d1654b96744022 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:09:43 -0800 Subject: [PATCH 06/47] fix psutils import (#1779) * fix psutils import * swap import order --- openfe/tests/utils/test_system_probe.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openfe/tests/utils/test_system_probe.py b/openfe/tests/utils/test_system_probe.py index bfdbfbe07..f5a47fb62 100644 --- a/openfe/tests/utils/test_system_probe.py +++ b/openfe/tests/utils/test_system_probe.py @@ -6,9 +6,13 @@ from collections import namedtuple from unittest.mock import Mock, patch -import psutil import pytest -from psutil._common import sdiskusage + +try: + from psutil._ntuples import sdiskusage +except ImportError: + from psutil._common import sdiskusage + from openfe.utils.system_probe import ( _get_disk_usage, From e86ed7c564a73579a9fcac3604b72b4202f526ed Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 21:09:02 +0000 Subject: [PATCH 07/47] [pre-commit.ci] pre-commit autoupdate (#1778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/tox-dev/pyproject-fmt: v2.8.0 → v2.11.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.8.0...v2.11.1) - [github.com/astral-sh/ruff-pre-commit: v0.13.3 → v0.14.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.3...v0.14.10) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a18de9747..e890a1cdc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,12 +19,12 @@ repos: - id: debug-statements - repo: https://github.com/tox-dev/pyproject-fmt - rev: "v2.8.0" + rev: "v2.11.1" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.14.10 hooks: # Run the linter. - id: ruff From b516872b0b2898632019f994c8f43ac0560fd830 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Tue, 6 Jan 2026 19:21:09 -0500 Subject: [PATCH 08/47] Disable NAGL as a partial charge backend when OEToolkit is installed but not chosen as the registry backend. (#1762) * NAGL can no longer be used with oechem installed if you are using the rdkit backend. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> --- news/disable_oechem_nagl.rst | 26 ++++++++ .../openmm_utils/charge_generation.py | 6 ++ .../protocols/openmm_abfe/test_abfe_slow.py | 2 +- .../openmm_ahfe/test_ahfe_protocol.py | 4 +- .../openmm_md/test_plain_md_protocol.py | 4 +- .../openmm_rfe/test_hybrid_top_protocol.py | 4 +- .../openmm_rfe/test_hybrid_top_slow.py | 39 +++++++----- openfe/tests/protocols/test_openmmutils.py | 32 ++++++++-- .../tests/commands/test_charge_generation.py | 13 +++- .../tests/commands/test_plan_rbfe_network.py | 61 ++++++++++++++----- .../tests/commands/test_plan_rhfe_network.py | 40 +++++++++--- 11 files changed, 178 insertions(+), 53 deletions(-) create mode 100644 news/disable_oechem_nagl.rst diff --git a/news/disable_oechem_nagl.rst b/news/disable_oechem_nagl.rst new file mode 100644 index 000000000..fc73b3e82 --- /dev/null +++ b/news/disable_oechem_nagl.rst @@ -0,0 +1,26 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Due to issues with OpenFF's handling of toolkit registries + with NAGL, the use of NAGL models (e.g. AshGC) when OpenEye + is installed but not requested as the charge backend has been + disabled (Issue #1760). + +**Security:** + +* diff --git a/openfe/protocols/openmm_utils/charge_generation.py b/openfe/protocols/openmm_utils/charge_generation.py index 2dcdd0edd..ad3697ff2 100644 --- a/openfe/protocols/openmm_utils/charge_generation.py +++ b/openfe/protocols/openmm_utils/charge_generation.py @@ -403,6 +403,12 @@ def assign_offmol_partial_charges( errmsg = "OpenEye is not available and cannot be selected as a backend" raise ImportError(errmsg) + # Issue 1760 + if HAS_OPENEYE and method.lower() == "nagl": + if toolkit_backend.lower() != "openeye": + errmsg = "OpenEye toolkit is installed but not used in the OpenFF toolkit registry backend. This is not possible with NAGL charges." + raise ValueError(errmsg) + toolkits = ToolkitRegistry([i() for i in BACKEND_OPTIONS[toolkit_backend.lower()]]) # We make a copy of the molecule since we're going to modify conformers diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_slow.py b/openfe/tests/protocols/openmm_abfe/test_abfe_slow.py index aa714766e..c4c64bd76 100644 --- a/openfe/tests/protocols/openmm_abfe/test_abfe_slow.py +++ b/openfe/tests/protocols/openmm_abfe/test_abfe_slow.py @@ -13,7 +13,7 @@ @pytest.mark.integration @pytest.mark.flaky(reruns=3) # pytest-rerunfailures; we can get bad minimisation @pytest.mark.skipif(not HAS_NAGL, reason="need NAGL") -@pytest.mark.xfail( +@pytest.mark.skipif( HAS_OPENEYE and HAS_NAGL, reason="NAGL/openeye incompatibility. See https://github.com/openforcefield/openff-nagl/issues/177", ) diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py b/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py index 206ff98c7..cf471130a 100644 --- a/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py +++ b/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py @@ -537,8 +537,8 @@ def assign_fictitious_charges(offmol): "rdkit", "nagl", marks=pytest.mark.skipif( - not HAS_NAGL or sys.platform.startswith("darwin"), - reason="needs NAGL and/or on macos", + not HAS_NAGL or HAS_OPENEYE or sys.platform.startswith("darwin"), + reason="needs NAGL (without oechem) and/or on macos", ), ), pytest.param( diff --git a/openfe/tests/protocols/openmm_md/test_plain_md_protocol.py b/openfe/tests/protocols/openmm_md/test_plain_md_protocol.py index f7dddfb03..60c7e8c47 100644 --- a/openfe/tests/protocols/openmm_md/test_plain_md_protocol.py +++ b/openfe/tests/protocols/openmm_md/test_plain_md_protocol.py @@ -243,8 +243,8 @@ def test_dry_run_espaloma_vacuum_user_charges(benzene_modifications, vac_setting "rdkit", "nagl", marks=pytest.mark.skipif( - not HAS_NAGL or sys.platform.startswith("darwin"), - reason="needs NAGL and/or on macos", + not HAS_NAGL or HAS_OPENEYE or sys.platform.startswith("darwin"), + reason="needs NAGL (without oechem) and/or on macos", ), ), pytest.param( diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py index f5ea92cff..615eb8e8f 100644 --- a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py @@ -652,8 +652,8 @@ def test_dry_run_ligand_system_cutoff( "rdkit", "nagl", marks=pytest.mark.skipif( - not HAS_NAGL or sys.platform.startswith("darwin"), - reason="needs NAGL and/or on macos", + not HAS_NAGL or HAS_OPENEYE or sys.platform.startswith("darwin"), + reason="needs NAGL (without oechem) and/or on macos", ), ), pytest.param( diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py index a8875142d..0cd31b08c 100644 --- a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py @@ -6,10 +6,11 @@ import pytest from gufe.protocols import execute_DAG from numpy.testing import assert_allclose -from openff.units import unit +from openff.units import unit as offunit import openfe from openfe.protocols import openmm_rfe +from openfe.protocols.openmm_utils.charge_generation import HAS_NAGL, HAS_OPENEYE @pytest.mark.slow @@ -28,14 +29,14 @@ def test_openmm_run_engine( # these settings are a small self to self sim, that has enough eq that # it doesn't occasionally crash s = openfe.protocols.openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - s.simulation_settings.equilibration_length = 0.1 * unit.picosecond - s.simulation_settings.production_length = 0.1 * unit.picosecond - s.simulation_settings.time_per_iteration = 20 * unit.femtosecond + s.simulation_settings.equilibration_length = 0.1 * offunit.picosecond + s.simulation_settings.production_length = 0.1 * offunit.picosecond + s.simulation_settings.time_per_iteration = 20 * offunit.femtosecond s.forcefield_settings.nonbonded_method = "nocutoff" s.protocol_repeats = 1 s.engine_settings.compute_platform = platform - s.output_settings.checkpoint_interval = 20 * unit.femtosecond - s.output_settings.positions_write_frequency = 20 * unit.femtosecond + s.output_settings.checkpoint_interval = 20 * offunit.femtosecond + s.output_settings.positions_write_frequency = 20 * offunit.femtosecond p = openmm_rfe.RelativeHybridTopologyProtocol(s) @@ -110,6 +111,11 @@ def test_openmm_run_engine( @pytest.mark.integration # takes ~7 minutes to run @pytest.mark.flaky(reruns=3) +@pytest.mark.skipif(not HAS_NAGL, reason="need NAGL") +@pytest.mark.skipif( + HAS_OPENEYE and HAS_NAGL, + reason="NAGL/openeye incompatibility. See https://github.com/openforcefield/openff-nagl/issues/177", +) def test_run_eg5_sim(eg5_protein, eg5_ligands, eg5_cofactor, tmpdir): # this runs a very short eg5 complex leg # different to previous test: @@ -118,11 +124,14 @@ def test_run_eg5_sim(eg5_protein, eg5_ligands, eg5_cofactor, tmpdir): # - runs in solvated protein # if this passes 99.9% chance of a good time s = openfe.protocols.openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - s.simulation_settings.equilibration_length = 0.1 * unit.picosecond - s.simulation_settings.production_length = 0.1 * unit.picosecond - s.simulation_settings.time_per_iteration = 20 * unit.femtosecond + s.simulation_settings.equilibration_length = 0.1 * offunit.picosecond + s.simulation_settings.production_length = 0.1 * offunit.picosecond + s.simulation_settings.time_per_iteration = 20 * offunit.femtosecond + s.forcefield_settings.nonbonded_cutoff = 0.8 * offunit.nanometer + s.partial_charge_settings.partial_charge_method = "nagl" + s.partial_charge_settings.nagl_model = "openff-gnn-am1bcc-0.1.0-rc.3.pt" s.protocol_repeats = 1 - s.output_settings.checkpoint_interval = 20 * unit.femtosecond + s.output_settings.checkpoint_interval = 20 * offunit.femtosecond p = openmm_rfe.RelativeHybridTopologyProtocol(s) @@ -158,13 +167,13 @@ def test_run_dodecahedron_sim(benzene_system, toluene_system, benzene_to_toluene Test that we can run a ligand in solvent RFE with a non-cubic box """ settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - settings.solvation_settings.solvent_padding = 1.5 * unit.nanometer + settings.solvation_settings.solvent_padding = 1.5 * offunit.nanometer settings.solvation_settings.box_shape = "dodecahedron" settings.protocol_repeats = 1 - settings.simulation_settings.equilibration_length = 0.1 * unit.picosecond - settings.simulation_settings.production_length = 0.1 * unit.picosecond - settings.simulation_settings.time_per_iteration = 20 * unit.femtosecond - settings.output_settings.checkpoint_interval = 20 * unit.femtosecond + settings.simulation_settings.equilibration_length = 0.1 * offunit.picosecond + settings.simulation_settings.production_length = 0.1 * offunit.picosecond + settings.simulation_settings.time_per_iteration = 20 * offunit.femtosecond + settings.output_settings.checkpoint_interval = 20 * offunit.femtosecond protocol = openmm_rfe.RelativeHybridTopologyProtocol(settings=settings) dag = protocol.create( diff --git a/openfe/tests/protocols/test_openmmutils.py b/openfe/tests/protocols/test_openmmutils.py index 15310c6b6..9511f2bcd 100644 --- a/openfe/tests/protocols/test_openmmutils.py +++ b/openfe/tests/protocols/test_openmmutils.py @@ -826,7 +826,9 @@ def test_am1bcc_conformer_nochange(self, eg5_ligands): # but the charges should have assert not np.allclose(charges, lig.partial_charges) - @pytest.mark.skipif(not HAS_NAGL, reason="NAGL is not available") + @pytest.mark.skipif( + not HAS_NAGL or HAS_OPENEYE, reason="NAGL is not available or oechem is installed" + ) def test_latest_production_nagl(self, uncharged_mol): """We expect to find a NAGL model and be able to generate partial charges with it.""" charge_generation.assign_offmol_partial_charges( @@ -839,7 +841,9 @@ def test_latest_production_nagl(self, uncharged_mol): ) assert uncharged_mol.partial_charges.units == "elementary_charge" - @pytest.mark.skipif(not HAS_NAGL, reason="NAGL is not available") + @pytest.mark.skipif( + not HAS_NAGL or HAS_OPENEYE, reason="NAGL is not available or oechem is installed" + ) def test_no_production_nagl(self, uncharged_mol): """Cleanly handle the case where a NAGL model isn't found.""" with mock.patch( @@ -887,8 +891,8 @@ def test_no_production_nagl(self, uncharged_mol): "nagl", None, marks=pytest.mark.skipif( - not HAS_NAGL or sys.platform.startswith("darwin"), - reason="needs NAGL and/or on macos", + not HAS_NAGL or HAS_OPENEYE or sys.platform.startswith("darwin"), + reason="needs NAGL (without oechem) and/or on macos", ), ), pytest.param( @@ -897,8 +901,8 @@ def test_no_production_nagl(self, uncharged_mol): "nagl", None, marks=pytest.mark.skipif( - not HAS_NAGL or sys.platform.startswith("darwin"), - reason="needs NAGL and/or on macos", + not HAS_NAGL or HAS_OPENEYE or sys.platform.startswith("darwin"), + reason="needs NAGL (without oechem) and/or on macos", ), ), pytest.param( @@ -961,6 +965,22 @@ def test_am1bcc_reference( rtol=1e-4, ) + @pytest.mark.skipif(not HAS_OPENEYE, reason="OEToolkit is not available") + def test_nagl_oechem_not_openeye_error(self, uncharged_mol): + errmsg = "OpenEye toolkit is installed but not used in the OpenFF toolkit registry." + with pytest.raises(ValueError, match=errmsg): + charge_generation.assign_offmol_partial_charges( + uncharged_mol, + overwrite=False, + method="nagl", + toolkit_backend="rdkit", + generate_n_conformers=None, + nagl_model=None, + ) + + @pytest.mark.skipif( + HAS_OPENEYE, reason="NAGL does not work with OpenEye when using the rdkit backend" + ) def test_nagl_import_error(self, monkeypatch, uncharged_mol): monkeypatch.setattr( sys.modules["openfe.protocols.openmm_utils.charge_generation"], diff --git a/openfecli/tests/commands/test_charge_generation.py b/openfecli/tests/commands/test_charge_generation.py index db843c305..ae481cf44 100644 --- a/openfecli/tests/commands/test_charge_generation.py +++ b/openfecli/tests/commands/test_charge_generation.py @@ -9,6 +9,10 @@ from openff.units import unit from openff.utilities.testing import skip_if_missing +from openfe.protocols.openmm_utils.charge_generation import ( + HAS_NAGL, + HAS_OPENEYE, +) from openfecli.commands.generate_partial_charges import charge_molecules @@ -122,8 +126,13 @@ def test_charge_molecules_overwrite( pytest.param(2, id="2"), ], ) -@skip_if_missing("openff.nagl") -@skip_if_missing("openff.nagl_models") +@pytest.mark.skipif( + not HAS_NAGL, + reason="needs NAGL", +) +@pytest.mark.skipif( + HAS_OPENEYE, reason="cannot use NAGL with rdkit backend when OpenEye is installed" +) def test_charge_settings(methane, tmpdir, caplog, yaml_nagl_settings, ncores): runner = CliRunner() mol_path = tmpdir / "methane.sdf" diff --git a/openfecli/tests/commands/test_plan_rbfe_network.py b/openfecli/tests/commands/test_plan_rbfe_network.py index dda23dac2..ed3805123 100644 --- a/openfecli/tests/commands/test_plan_rbfe_network.py +++ b/openfecli/tests/commands/test_plan_rbfe_network.py @@ -10,7 +10,10 @@ from openff.units import unit from openff.utilities import skip_if_missing -from openfe.protocols.openmm_utils.charge_generation import HAS_OPENEYE +from openfe.protocols.openmm_utils.charge_generation import ( + HAS_NAGL, + HAS_OPENEYE, +) from openfecli.commands.plan_rbfe_network import ( plan_rbfe_network, plan_rbfe_network_main, @@ -70,8 +73,13 @@ def validate_charges(smc): assert len(off_mol.partial_charges) == off_mol.n_atoms -@skip_if_missing("openff.nagl") -@skip_if_missing("openff.nagl_models") +@pytest.mark.skipif( + not HAS_NAGL, + reason="needs NAGL", +) +@pytest.mark.skipif( + HAS_OPENEYE, reason="cannot use NAGL with rdkit backend when OpenEye is installed" +) def test_plan_rbfe_network_main(): from gufe import ( ProteinComponent, @@ -137,8 +145,13 @@ def yaml_nagl_settings(): """ -@skip_if_missing("openff.nagl") -@skip_if_missing("openff.nagl_models") +@pytest.mark.skipif( + not HAS_NAGL, + reason="needs NAGL", +) +@pytest.mark.skipif( + HAS_OPENEYE, reason="cannot use NAGL with rdkit backend when OpenEye is installed" +) def test_plan_rbfe_network(mol_dir_args, protein_args, tmpdir, yaml_nagl_settings): """ smoke test @@ -218,8 +231,13 @@ def test_plan_rbfe_network_n_repeats(mol_dir_args, protein_args, input_n_repeat, pytest.param(False, id="No overwrite"), ], ) -@skip_if_missing("openff.nagl") -@skip_if_missing("openff.nagl_models") +@pytest.mark.skipif( + not HAS_NAGL, + reason="needs NAGL", +) +@pytest.mark.skipif( + HAS_OPENEYE, reason="cannot use NAGL with rdkit backend when OpenEye is installed" +) def test_plan_rbfe_network_charge_overwrite(dummy_charge_dir_args, protein_args, tmpdir, yaml_nagl_settings, overwrite): # fmt: skip # make sure the dummy charges are overwritten when requested @@ -268,9 +286,13 @@ def eg5_files(): yield pdb_path, lig_path, cof_path -@pytest.mark.xfail(HAS_OPENEYE, reason="openff-nagl#177") -@skip_if_missing("openff.nagl") -@skip_if_missing("openff.nagl_models") +@pytest.mark.skipif( + not HAS_NAGL, + reason="needs NAGL", +) +@pytest.mark.skipif( + HAS_OPENEYE, reason="cannot use NAGL with rdkit backend when OpenEye is installed" +) def test_plan_rbfe_network_cofactors(eg5_files, tmpdir, yaml_nagl_settings): # use nagl charges for CI speed! settings_path = tmpdir / "settings.yaml" @@ -372,8 +394,13 @@ def lomap_yaml_settings(): """ -@skip_if_missing("openff.nagl") -@skip_if_missing("openff.nagl_models") +@pytest.mark.skipif( + not HAS_NAGL, + reason="needs NAGL", +) +@pytest.mark.skipif( + HAS_OPENEYE, reason="cannot use NAGL with rdkit backend when OpenEye is installed" +) def test_lomap_yaml_plan_rbfe_smoke_test(lomap_yaml_settings, cdk8_files, tmpdir): protein, ligand = cdk8_files settings_path = tmpdir / "settings.yaml" @@ -413,9 +440,13 @@ def custom_yaml_radial(): """ -@pytest.mark.xfail(HAS_OPENEYE, reason="openff-nagl#177") -@skip_if_missing("openff.nagl") -@skip_if_missing("openff.nagl_models") +@pytest.mark.skipif( + not HAS_NAGL, + reason="needs NAGL", +) +@pytest.mark.skipif( + HAS_OPENEYE, reason="cannot use NAGL with rdkit backend when OpenEye is installed" +) def test_custom_yaml_plan_radial_smoke_test(custom_yaml_radial, eg5_files, tmpdir): protein, ligand, cofactor = eg5_files settings_path = tmpdir / "settings.yaml" diff --git a/openfecli/tests/commands/test_plan_rhfe_network.py b/openfecli/tests/commands/test_plan_rhfe_network.py index e7de92629..a3780d79c 100644 --- a/openfecli/tests/commands/test_plan_rhfe_network.py +++ b/openfecli/tests/commands/test_plan_rhfe_network.py @@ -10,6 +10,10 @@ from gufe.tokenization import JSON_HANDLER from openff.utilities.testing import skip_if_missing +from openfe.protocols.openmm_utils.charge_generation import ( + HAS_NAGL, + HAS_OPENEYE, +) from openfecli.commands.plan_rhfe_network import ( plan_rhfe_network, plan_rhfe_network_main, @@ -54,8 +58,13 @@ def validate_charges(smc): assert len(off_mol.partial_charges) == off_mol.n_atoms -@skip_if_missing("openff.nagl") -@skip_if_missing("openff.nagl_models") +@pytest.mark.skipif( + not HAS_NAGL, + reason="needs NAGL", +) +@pytest.mark.skipif( + HAS_OPENEYE, reason="cannot use NAGL with rdkit backend when OpenEye is installed" +) def test_plan_rhfe_network_main(): from openfe.protocols.openmm_utils.omm_settings import OpenFFPartialChargeSettings from openfe.setup import ( @@ -102,8 +111,13 @@ def yaml_nagl_settings(): """ -@skip_if_missing("openff.nagl") -@skip_if_missing("openff.nagl_models") +@pytest.mark.skipif( + not HAS_NAGL, + reason="needs NAGL", +) +@pytest.mark.skipif( + HAS_OPENEYE, reason="cannot use NAGL with rdkit backend when OpenEye is installed" +) def test_plan_rhfe_network(mol_dir_args, tmpdir, yaml_nagl_settings): """ smoke test @@ -175,8 +189,13 @@ def custom_yaml_settings(): """ -@skip_if_missing("openff.nagl") -@skip_if_missing("openff.nagl_models") +@pytest.mark.skipif( + not HAS_NAGL, + reason="needs NAGL", +) +@pytest.mark.skipif( + HAS_OPENEYE, reason="cannot use NAGL with rdkit backend when OpenEye is installed" +) def test_custom_yaml_plan_rhfe_smoke_test(custom_yaml_settings, mol_dir_args, tmpdir): settings_path = tmpdir / "settings.yaml" with open(settings_path, "w") as f: @@ -201,8 +220,13 @@ def test_custom_yaml_plan_rhfe_smoke_test(custom_yaml_settings, mol_dir_args, tm pytest.param(False, id="No overwrite"), ], ) -@skip_if_missing("openff.nagl") -@skip_if_missing("openff.nagl_models") +@pytest.mark.skipif( + not HAS_NAGL, + reason="needs NAGL", +) +@pytest.mark.skipif( + HAS_OPENEYE, reason="cannot use NAGL with rdkit backend when OpenEye is installed" +) def test_plan_rhfe_network_charge_overwrite(dummy_charge_dir_args, tmpdir, yaml_nagl_settings, overwrite): # fmt: skip # make sure the dummy charges are overwritten when requested From 7c02bc53fe8f7911eca30557fab83fc1a112c859 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:34:04 -0800 Subject: [PATCH 09/47] rename openfecli fixture to avoid duplication with openfe fixture (#1755) Co-authored-by: Irfan Alibay --- openfecli/tests/commands/test_atommapping.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openfecli/tests/commands/test_atommapping.py b/openfecli/tests/commands/test_atommapping.py index 48c558e56..8b459de6f 100644 --- a/openfecli/tests/commands/test_atommapping.py +++ b/openfecli/tests/commands/test_atommapping.py @@ -30,7 +30,7 @@ def mapper_args(): @pytest.fixture -def mols(molA_args, molB_args): +def mols_AB(molA_args, molB_args): return MOL.get(molA_args[1]), MOL.get(molB_args[1]) @@ -89,8 +89,8 @@ def test_atommapping_missing_mapper(molA_args, molB_args): @pytest.mark.parametrize("n_mappings", [0, 1, 2]) -def test_generate_mapping(n_mappings, mols): - molA, molB = mols +def test_generate_mapping(n_mappings, mols_AB): + molA, molB = mols_AB mappings = [ LigandAtomMapping(molA, molB, {i: i for i in range(7)}), LigandAtomMapping(molA, molB, {i: (i + 1) % 7 for i in range(7)}), @@ -104,8 +104,8 @@ def test_generate_mapping(n_mappings, mols): generate_mapping(mapper, molA, molB) -def test_atommapping_print_dict_main(capsys, mols): - molA, molB = mols +def test_atommapping_print_dict_main(capsys, mols_AB): + molA, molB = mols_AB mapper = LomapAtomMapper mapping = LigandAtomMapping(molA, molB, {i: i for i in range(7)}) with mock.patch("openfecli.commands.atommapping.generate_mapping", mock.Mock(return_value=mapping)): # fmt: skip @@ -114,14 +114,14 @@ def test_atommapping_print_dict_main(capsys, mols): assert captured.out == str(mapping.componentA_to_componentB) + "\n" -def test_atommapping_visualize_main(mols, tmpdir): - molA, molB = mols +def test_atommapping_visualize_main(mols_AB, tmpdir): + molA, molB = mols_AB mapper = LomapAtomMapper pytest.skip() # TODO: probably with a smoke test -def test_atommapping_visualize_main_bad_extension(mols, tmpdir): - molA, molB = mols +def test_atommapping_visualize_main_bad_extension(mols_AB, tmpdir): + molA, molB = mols_AB mapper = LomapAtomMapper mapping = LigandAtomMapping(molA, molB, {i: i for i in range(7)}) with mock.patch("openfecli.commands.atommapping.generate_mapping", mock.Mock(return_value=mapping)): # fmt: skip From d1aa5ac5c1dc972a798c9d41af40892ceeff8258 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:01:30 -0800 Subject: [PATCH 10/47] openfe gather: add progress bar for loading JSONs (#1786) * all the profiling * make progress bar pretty * revert typing thing * news * all the profiling * make progress bar pretty * revert typing thing * news * update expected outputs * update expected outputs --- news/progress_bar.rst | 23 ++++++ openfecli/commands/gather.py | 70 +++++++++++-------- openfecli/tests/commands/test_gather.py | 3 + .../test_cmet_failed_edge_ddg_.tsv | 1 + .../test_cmet_failed_edge_raw_.tsv | 1 + .../test_cmet_full_results_ddg_.tsv | 1 + .../test_cmet_full_results_dg_.tsv | 1 + .../test_cmet_full_results_raw_.tsv | 1 + ...ng_all_complex_legs_allow_partial_ddg_.tsv | 1 + ...met_missing_all_complex_legs_fail_ddg_.tsv | 1 + ...cmet_missing_all_complex_legs_fail_dg_.tsv | 1 + .../test_cmet_missing_complex_leg_ddg_.tsv | 1 + .../test_cmet_missing_complex_leg_dg_.tsv | 1 + .../test_cmet_missing_complex_leg_raw_.tsv | 1 + .../test_cmet_missing_edge_ddg_.tsv | 1 + .../test_cmet_missing_edge_dg_.tsv | 1 + .../test_cmet_missing_edge_raw_.tsv | 1 + openfecli/tests/test_rbfe_tutorial.py | 1 + 18 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 news/progress_bar.rst diff --git a/news/progress_bar.rst b/news/progress_bar.rst new file mode 100644 index 000000000..bd0999a29 --- /dev/null +++ b/news/progress_bar.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added a progress bar for ``openfe gather`` JSON loading. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/openfecli/commands/gather.py b/openfecli/commands/gather.py index 730118b1d..a99e4ee8e 100644 --- a/openfecli/commands/gather.py +++ b/openfecli/commands/gather.py @@ -7,7 +7,6 @@ from typing import List, Literal import click -import gufe import pandas as pd from openfecli import OFECommandPlugin @@ -613,9 +612,8 @@ def is_results_json(fpath: os.PathLike | str) -> bool: # 1) find all possible jsons json_fns = collect_jsons(results) - # 2) filter only result jsons - result_fns = filter(is_results_json, json_fns) + result_fns = list(filter(is_results_json, json_fns)) return result_fns @@ -643,35 +641,45 @@ def _get_legs_from_result_jsons( legs = defaultdict(lambda: defaultdict(list)) - for result_fn in result_fns: - result_info, result = _load_valid_result_json(result_fn) - - if result_info is None: # this means it couldn't find names and/or simtype - continue - names, simtype = result_info - if report.lower() == "raw": - if result is None: - parsed_raw_data = [(None, None)] + with click.progressbar( + result_fns, + label="Loading results:", + fill_char="▇", + empty_char=" ", + bar_template="%(label)s %(bar)s %(info)s files", + length=len(result_fns), + show_percent=False, + show_pos=True, + show_eta=False, + ) as bar: + for result_fn in bar: + result_info, result = _load_valid_result_json(result_fn) + + if result_info is None: # this means it couldn't find names and/or simtype + continue + names, simtype = result_info + if report.lower() == "raw": + if result is None: + parsed_raw_data = [(None, None)] + else: + parsed_raw_data = [ + ( + v[0]["outputs"]["unit_estimate"], + v[0]["outputs"]["unit_estimate_error"], + ) + for v in result["protocol_result"]["data"].values() + ] + legs[names][simtype].append(parsed_raw_data) else: - parsed_raw_data = [ - ( - v[0]["outputs"]["unit_estimate"], - v[0]["outputs"]["unit_estimate_error"], - ) - for v in result["protocol_result"]["data"].values() - ] - legs[names][simtype].append(parsed_raw_data) - else: - if result is None: - # we want the dict name/simtype entry to exist for error reporting, even if there's no valid data - dGs = [] - else: - dGs = [ - v[0]["outputs"]["unit_estimate"] - for v in result["protocol_result"]["data"].values() - ] - legs[names][simtype].extend(dGs) - + if result is None: + # we want the dict name/simtype entry to exist for error reporting, even if there's no valid data + dGs = [] + else: + dGs = [ + v[0]["outputs"]["unit_estimate"] + for v in result["protocol_result"]["data"].values() + ] + legs[names][simtype].extend(dGs) return legs diff --git a/openfecli/tests/commands/test_gather.py b/openfecli/tests/commands/test_gather.py index 498f4a2ed..9ea27b59b 100644 --- a/openfecli/tests/commands/test_gather.py +++ b/openfecli/tests/commands/test_gather.py @@ -140,6 +140,7 @@ def test_no_results_found(): _RBFE_EXPECTED_DG = b""" +Loading results: ligand DG(MLE) (kcal/mol) uncertainty (kcal/mol) lig_ejm_31 -0.09 0.05 lig_ejm_42 0.7 0.1 @@ -154,6 +155,7 @@ def test_no_results_found(): """ _RBFE_EXPECTED_DDG = b""" +Loading results: ligand_i ligand_j DDG(i->j) (kcal/mol) uncertainty (kcal/mol) lig_ejm_31 lig_ejm_42 0.8 0.1 lig_ejm_31 lig_ejm_46 -0.89 0.06 @@ -167,6 +169,7 @@ def test_no_results_found(): """ _RBFE_EXPECTED_RAW = b"""\ +Loading results: leg ligand_i ligand_j DG(i->j) (kcal/mol) MBAR uncertainty (kcal/mol) complex lig_ejm_31 lig_ejm_42 -14.9 0.8 complex lig_ejm_31 lig_ejm_42 -14.8 0.8 diff --git a/openfecli/tests/commands/test_gather/test_cmet_failed_edge_ddg_.tsv b/openfecli/tests/commands/test_gather/test_cmet_failed_edge_ddg_.tsv index ba72b1488..8aa5dd8a1 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_failed_edge_ddg_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_failed_edge_ddg_.tsv @@ -1,2 +1,3 @@ +Loading results: ligand_i ligand_j DDG(i->j) (kcal/mol) uncertainty (kcal/mol) lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 1.1 0.1 diff --git a/openfecli/tests/commands/test_gather/test_cmet_failed_edge_raw_.tsv b/openfecli/tests/commands/test_gather/test_cmet_failed_edge_raw_.tsv index 209b47fa9..4d7f5bf77 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_failed_edge_raw_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_failed_edge_raw_.tsv @@ -1,3 +1,4 @@ +Loading results: leg ligand_i ligand_j DG(i->j) (kcal/mol) MBAR uncertainty (kcal/mol) complex lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 -12.54 0.06 complex lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 -12.31 0.06 diff --git a/openfecli/tests/commands/test_gather/test_cmet_full_results_ddg_.tsv b/openfecli/tests/commands/test_gather/test_cmet_full_results_ddg_.tsv index 5bf680b00..a9bbcca54 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_full_results_ddg_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_full_results_ddg_.tsv @@ -1,3 +1,4 @@ +Loading results: ligand_i ligand_j DDG(i->j) (kcal/mol) uncertainty (kcal/mol) lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 1.2 0.1 lig_CHEMBL3402745_200_5 lig_CHEMBL3402749_500_9 3.6 0.2 diff --git a/openfecli/tests/commands/test_gather/test_cmet_full_results_dg_.tsv b/openfecli/tests/commands/test_gather/test_cmet_full_results_dg_.tsv index 9fffe4269..4fc2ae0b0 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_full_results_dg_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_full_results_dg_.tsv @@ -1,3 +1,4 @@ +Loading results: ligand DG(MLE) (kcal/mol) uncertainty (kcal/mol) lig_CHEMBL3402745_200_5 -0.3 0.1 lig_CHEMBL3402744_300_4 0.9 0.2 diff --git a/openfecli/tests/commands/test_gather/test_cmet_full_results_raw_.tsv b/openfecli/tests/commands/test_gather/test_cmet_full_results_raw_.tsv index 5ca825842..1dd30fa3d 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_full_results_raw_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_full_results_raw_.tsv @@ -1,3 +1,4 @@ +Loading results: leg ligand_i ligand_j DG(i->j) (kcal/mol) MBAR uncertainty (kcal/mol) complex lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 -12.54 0.06 complex lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 -12.31 0.06 diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_allow_partial_ddg_.tsv b/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_allow_partial_ddg_.tsv index d42874527..0823aad6f 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_allow_partial_ddg_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_allow_partial_ddg_.tsv @@ -1,3 +1,4 @@ +Loading results: ligand_i ligand_j DDG(i->j) (kcal/mol) uncertainty (kcal/mol) lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 Error Error lig_CHEMBL3402745_200_5 lig_CHEMBL3402749_500_9 Error Error diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_ddg_.tsv b/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_ddg_.tsv index e69de29bb..989bf55d3 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_ddg_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_ddg_.tsv @@ -0,0 +1 @@ +Loading results: diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_dg_.tsv b/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_dg_.tsv index e69de29bb..989bf55d3 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_dg_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_dg_.tsv @@ -0,0 +1 @@ +Loading results: diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_ddg_.tsv b/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_ddg_.tsv index c16311faf..c314f5621 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_ddg_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_ddg_.tsv @@ -1,3 +1,4 @@ +Loading results: ligand_i ligand_j DDG(i->j) (kcal/mol) uncertainty (kcal/mol) lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 1.2 0.1 lig_CHEMBL3402745_200_5 lig_CHEMBL3402749_500_9 3.6 0.2 diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_dg_.tsv b/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_dg_.tsv index 9f6c94f30..4c4ce05cd 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_dg_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_dg_.tsv @@ -1,3 +1,4 @@ +Loading results: ligand DG(MLE) (kcal/mol) uncertainty (kcal/mol) lig_CHEMBL3402745_200_5 -0.3 0.1 lig_CHEMBL3402744_300_4 0.8 0.2 diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_raw_.tsv b/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_raw_.tsv index a203a4e43..999936b22 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_raw_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_raw_.tsv @@ -1,3 +1,4 @@ +Loading results: leg ligand_i ligand_j DG(i->j) (kcal/mol) MBAR uncertainty (kcal/mol) complex lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 -12.54 0.06 complex lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 -12.31 0.06 diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_edge_ddg_.tsv b/openfecli/tests/commands/test_gather/test_cmet_missing_edge_ddg_.tsv index 56dac09e4..7efe75da3 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_missing_edge_ddg_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_missing_edge_ddg_.tsv @@ -1,3 +1,4 @@ +Loading results: ligand_i ligand_j DDG(i->j) (kcal/mol) uncertainty (kcal/mol) lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 1.2 0.1 lig_CHEMBL3402745_200_5 lig_CHEMBL3402749_500_9 3.6 0.2 diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_edge_dg_.tsv b/openfecli/tests/commands/test_gather/test_cmet_missing_edge_dg_.tsv index 6fe7ea244..7573757a7 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_missing_edge_dg_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_missing_edge_dg_.tsv @@ -1,3 +1,4 @@ +Loading results: ligand DG(MLE) (kcal/mol) uncertainty (kcal/mol) lig_CHEMBL3402745_200_5 -0.90 0.08 lig_CHEMBL3402744_300_4 0.3 0.1 diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_edge_raw_.tsv b/openfecli/tests/commands/test_gather/test_cmet_missing_edge_raw_.tsv index 451fa9bf5..339578aa7 100644 --- a/openfecli/tests/commands/test_gather/test_cmet_missing_edge_raw_.tsv +++ b/openfecli/tests/commands/test_gather/test_cmet_missing_edge_raw_.tsv @@ -1,3 +1,4 @@ +Loading results: leg ligand_i ligand_j DG(i->j) (kcal/mol) MBAR uncertainty (kcal/mol) complex lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 -12.54 0.06 complex lig_CHEMBL3402745_200_5 lig_CHEMBL3402744_300_4 -12.31 0.06 diff --git a/openfecli/tests/test_rbfe_tutorial.py b/openfecli/tests/test_rbfe_tutorial.py index bb06ed654..aac32d421 100644 --- a/openfecli/tests/test_rbfe_tutorial.py +++ b/openfecli/tests/test_rbfe_tutorial.py @@ -111,6 +111,7 @@ def fake_execute(*args, **kwargs): @pytest.fixture def ref_gather(): return """\ +Loading results: ligand_i\tligand_j\tDDG(i->j) (kcal/mol)\tuncertainty (kcal/mol) lig_ejm_31\tlig_ejm_46\t0.0\t0.0 lig_ejm_31\tlig_ejm_47\t0.0\t0.0 From 55056150f0b27ac256783abfda15cf6b4326faf4 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:49:56 -0800 Subject: [PATCH 11/47] add dill mock to fix openfe docs build (#1792) --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 1b4b4723e..1e6bd07df 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -103,6 +103,7 @@ autodoc_mock_imports = [ "cinnabar", + "dill", "MDAnalysis", "matplotlib", "mdtraj", From 0537f840afaa77638553f5820fb165dad023c9af Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Fri, 16 Jan 2026 20:08:36 +0000 Subject: [PATCH 12/47] Fix issue 1795 (#1796) * Update method name in CLI YAML documentation * Fix command option case in CLI YAML guide --- docs/guide/cli/cli_yaml.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/cli/cli_yaml.rst b/docs/guide/cli/cli_yaml.rst index 84825ef72..7ca6304f4 100644 --- a/docs/guide/cli/cli_yaml.rst +++ b/docs/guide/cli/cli_yaml.rst @@ -10,7 +10,7 @@ This settings file has a series of sections for customising the different algori For example, the settings file which re-specifies the default behaviour would look like :: network: - method: plan_minimal_spanning_tree + method: generate_minimal_spanning_network mapper: method: LomapAtomMapper settings: @@ -29,7 +29,7 @@ All sections of the file ``network:``, ``mapper:`` and ``partial_charge:`` are The settings YAML file is then provided to the ``-s`` option of ``openfe plan-rbfe-network``: :: - openfe plan-rbfe-network -M molecules.sdf -P protein.pdb -s settings.yaml + openfe plan-rbfe-network -M molecules.sdf -p protein.pdb -s settings.yaml Customising the atom mapper --------------------------- From b4631c6831847af5c028e397ceb7b46633ed7415 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:12:28 -0800 Subject: [PATCH 13/47] manually add absolute settings news item to changelog (#1821) * manually add absolute settings news item to changelog * add link --- docs/CHANGELOG.rst | 1 + news/absolute_settings.rst | 27 --------------------------- 2 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 news/absolute_settings.rst diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index 7b262e8d0..6b3ce9778 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -14,6 +14,7 @@ v1.8.0 * Added experimental features ``openfe gather-septop`` and ``openfe gather-abfe``, which are analogous to ``openfe gather`` and allow for gathering results generated by the Separated Topologies and Absolute Binding Free Energy protocols, respectively. These commands are experimental and are liable to be changed in a future release. * Emit a clarifying log message when a user gets a warning from JAX (`PR #1585 `_, fixes `Issue #1499 `_). * Disable JAX acceleration by default, see https://docs.openfree.energy/en/latest/guide/troubleshooting.html#pymbar-disable-jax for more information (`PR #1694 `_). +* New options have been added to the ``AlchemicalSettings`` of the ``SepTopProtocol``, ``AbsoluteSolvationProtocol`` and ``AbsoluteBindingProtocol``. Notably, these options allow users to control the softcore parameters as well as the use of long range dispersion corrections (`PR #1742 `_). **Changed:** diff --git a/news/absolute_settings.rst b/news/absolute_settings.rst deleted file mode 100644 index dc4d715c1..000000000 --- a/news/absolute_settings.rst +++ /dev/null @@ -1,27 +0,0 @@ -**Added:** - -* New options have been added to the ``AlchemicalSettings`` - of the ``SepTopProtocol``, ``AbsoluteSolvationProtocol`` - and ``AbsoluteBindingProtocol``. Notably, these options allow users to - control the softcore parameters as well as the use of - long range dispersion corrections. - -**Changed:** - -* - -**Deprecated:** - -* - -**Removed:** - -* - -**Fixed:** - -* - -**Security:** - -* From 78639eeb6bee93d83a04f7725a176e5311024853 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Mon, 2 Feb 2026 19:12:57 +0000 Subject: [PATCH 14/47] Fixes docstring to clarify argument does not have a default (#1819) * Fixes docstring to clarify argument does not have a default * Modernize typing --- .../openmm_utils/charge_generation.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/openfe/protocols/openmm_utils/charge_generation.py b/openfe/protocols/openmm_utils/charge_generation.py index ad3697ff2..332eda519 100644 --- a/openfe/protocols/openmm_utils/charge_generation.py +++ b/openfe/protocols/openmm_utils/charge_generation.py @@ -7,7 +7,7 @@ import copy import sys import warnings -from typing import Callable, Literal, Optional, Union +from typing import Callable, Literal import numpy as np from gufe import SmallMoleculeComponent @@ -112,7 +112,7 @@ def assign_offmol_espaloma_charges(offmol: OFFMol, toolkit_registry: ToolkitRegi def assign_offmol_nagl_charges( offmol: OFFMol, toolkit_registry: ToolkitRegistry, - nagl_model: Optional[str] = None, + nagl_model: str | None = None, ) -> None: """ Assign NAGL charges using the OpenFF toolkit. @@ -126,7 +126,7 @@ def assign_offmol_nagl_charges( This strictly limits available toolkit wrappers by overwriting the global registry during the partial charge assignment stage. - nagl_model : Optional[str] + nagl_model : str | None The NAGL model to use when assigning partial charges. If ``None``, will fetch the latest production "am1bcc" model. """ @@ -208,7 +208,7 @@ def _generate_offmol_conformers( offmol: OFFMol, max_conf: int, toolkit_registry: ToolkitRegistry, - generate_n_conformers: Optional[int], + generate_n_conformers: int | None, ) -> None: """ Helper method for OFF Molecule conformer generation in charge assignment. @@ -223,7 +223,7 @@ def _generate_offmol_conformers( Toolkit registry to use for generating conformers. This strictly limits available toolkit wrappers by overwriting the global registry during the conformer generation step. - generate_n_conformers : Optional[int] + generate_n_conformers : int | None The number of conformers to generate. If ``None``, the existing conformers are retained & used. @@ -288,8 +288,8 @@ def assign_offmol_partial_charges( overwrite: bool, method: Literal["am1bcc", "am1bccelf10", "nagl", "espaloma"], toolkit_backend: Literal["ambertools", "openeye", "rdkit"], - generate_n_conformers: Optional[int], - nagl_model: Optional[str], + generate_n_conformers: int | None, + nagl_model: str | None, ) -> OFFMol: """ Assign partial charges to an OpenFF Molecule based on a selected method. @@ -312,11 +312,11 @@ def assign_offmol_partial_charges( * ``rdkit``: selects the RDKit toolkit Wrapper Note that the ``rdkit`` backend cannot be used for `am1bcc` or ``am1bccelf10`` partial charge methods. - generate_n_conformers : Optional[int] + generate_n_conformers : int | None Number of conformers to generate for partial charge generation. - If ``None`` (default), the input conformer will be used. + If ``None``, the input conformer will be used. Values greater than 1 can only be used alongside ``am1bccelf10``. - nagl_model : Optional[str] + nagl_model : str | None The NAGL model to use for charge assignment if method is ``nagl``. If ``None``, the latest am1bcc NAGL charge model is used. @@ -443,8 +443,8 @@ def bulk_assign_partial_charges( overwrite: bool, method: Literal["am1bcc", "am1bccelf10", "nagl", "espaloma"], toolkit_backend: Literal["ambertools", "openeye", "rdkit"], - generate_n_conformers: Optional[int], - nagl_model: Optional[str], + generate_n_conformers: int | None, + nagl_model: str | None, processors: int = 1, ) -> list[SmallMoleculeComponent]: """ @@ -468,11 +468,11 @@ def bulk_assign_partial_charges( * ``rdkit``: selects the RDKit toolkit Wrapper Note that the ``rdkit`` backend cannot be used for `am1bcc` or ``am1bccelf10`` partial charge methods. - generate_n_conformers : Optional[int] + generate_n_conformers : int | None Number of conformers to generate for partial charge generation. - If ``None`` (default), the input conformer will be used. + If ``None``, the input conformer will be used. Values greater than 1 can only be used alongside ``am1bccelf10``. - nagl_model : Optional[str] + nagl_model : str | None The NAGL model to use for charge assignment if method is ``nagl``. If ``None``, the latest am1bcc NAGL charge model is used. processors: int, default 1 From b55b14983af873fbb7d92c64796edf0869d00da3 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Tue, 3 Feb 2026 07:52:34 -0800 Subject: [PATCH 15/47] fix ligand network cropping bug (#1822) * fix ligand network cropping bug * move to upstream func * whitespace --- news/fix_image_crop.rst | 23 ++++++++++++++++++++ openfe/utils/atommapping_network_plotting.py | 12 +++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 news/fix_image_crop.rst diff --git a/news/fix_image_crop.rst b/news/fix_image_crop.rst new file mode 100644 index 000000000..3d1f1d31c --- /dev/null +++ b/news/fix_image_crop.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Fixed bug in ligand network visualization (such as with ``openfe view-ligand-network``) so that ligand names are no longer cut off by the plot border (`PR 1822 `_). + +**Security:** + +* diff --git a/openfe/utils/atommapping_network_plotting.py b/openfe/utils/atommapping_network_plotting.py index 90ad66808..fc69ddd1f 100644 --- a/openfe/utils/atommapping_network_plotting.py +++ b/openfe/utils/atommapping_network_plotting.py @@ -142,7 +142,7 @@ def xy(self): class AtomMappingNetworkDrawing(GraphDrawing): """ - Class for drawing atom mappings from a provided ligang network. + Class for drawing atom mappings from a provided ligand network. Parameters ---------- @@ -167,6 +167,12 @@ def plot_atommapping_network(network: LigandNetwork): Returns ------- :class:`matplotlib.figure.Figure` : - the matplotlib figure containing the iteractive visualization + the matplotlib figure containing the interactive visualization """ - return AtomMappingNetworkDrawing(network.graph).fig + fig = AtomMappingNetworkDrawing(network.graph).fig + axes = fig.axes + for ax in axes: + ax.set_frame_on(False) # remove the black frame + for t in ax.texts: + t.set_clip_on(False) # do not clip the label in the network plot + return fig From 3ccd3187febd0ebda6c6e70735fff88dad10cf7f Mon Sep 17 00:00:00 2001 From: Alyssa Travitz Date: Wed, 4 Feb 2026 14:08:49 -0800 Subject: [PATCH 16/47] fix pydantic deprecation of copy method --- openfe/protocols/openmm_rfe/equil_rfe_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfe/protocols/openmm_rfe/equil_rfe_methods.py b/openfe/protocols/openmm_rfe/equil_rfe_methods.py index 514237634..9a20c1e10 100644 --- a/openfe/protocols/openmm_rfe/equil_rfe_methods.py +++ b/openfe/protocols/openmm_rfe/equil_rfe_methods.py @@ -584,7 +584,7 @@ def _adaptive_settings( # use initial settings or default settings # this is needed for the CLI so we don't override user settings if initial_settings is not None: - protocol_settings = initial_settings.copy(deep=True) + protocol_settings = initial_settings.model_copy(deep=True) else: protocol_settings = cls.default_settings() From 5eec5ec97beb98e4b6c9a41960ae454faeab9973 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:14:03 -0800 Subject: [PATCH 17/47] fix pydantic deprecation of dict method - septop (#1831) --- openfe/protocols/openmm_septop/equil_septop_method.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openfe/protocols/openmm_septop/equil_septop_method.py b/openfe/protocols/openmm_septop/equil_septop_method.py index 87f7aa7a0..9375747bc 100644 --- a/openfe/protocols/openmm_septop/equil_septop_method.py +++ b/openfe/protocols/openmm_septop/equil_septop_method.py @@ -2021,8 +2021,8 @@ def run( "topology": topology_file, "standard_state_correction_A": corr_A.to("kilocalorie_per_mole"), "standard_state_correction_B": corr_B.to("kilocalorie_per_mole"), - "restraint_geometry_A": restraint_geom_A.dict(), - "restraint_geometry_B": restraint_geom_B.dict(), + "restraint_geometry_A": restraint_geom_A.model_dump(), + "restraint_geometry_B": restraint_geom_B.model_dump(), } def _execute( From f74e644f455fd251ce179a17cd46153b7a0bd177 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz Date: Thu, 5 Feb 2026 07:35:55 -0800 Subject: [PATCH 18/47] Updated CHANGELOG for 1.8.1 --- docs/CHANGELOG.rst | 17 +++++++++++++++++ news/disable_oechem_nagl.rst | 26 -------------------------- news/fix_image_crop.rst | 23 ----------------------- news/progress_bar.rst | 23 ----------------------- 4 files changed, 17 insertions(+), 72 deletions(-) delete mode 100644 news/disable_oechem_nagl.rst delete mode 100644 news/fix_image_crop.rst delete mode 100644 news/progress_bar.rst diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index 6b3ce9778..2a842200a 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -4,6 +4,23 @@ Changelog .. current developments +v1.8.1 +==================== + +**Added:** + +* Added a progress bar for ``openfe gather`` JSON loading. + +**Fixed:** + +* Due to issues with OpenFF's handling of toolkit registries + with NAGL, the use of NAGL models (e.g. AshGC) when OpenEye + is installed but not requested as the charge backend has been + disabled (Issue #1760). +* Fixed bug in ligand network visualization (such as with ``openfe view-ligand-network``) so that ligand names are no longer cut off by the plot border (`PR 1822 `_). + + + v1.8.0 ==================== diff --git a/news/disable_oechem_nagl.rst b/news/disable_oechem_nagl.rst deleted file mode 100644 index fc73b3e82..000000000 --- a/news/disable_oechem_nagl.rst +++ /dev/null @@ -1,26 +0,0 @@ -**Added:** - -* - -**Changed:** - -* - -**Deprecated:** - -* - -**Removed:** - -* - -**Fixed:** - -* Due to issues with OpenFF's handling of toolkit registries - with NAGL, the use of NAGL models (e.g. AshGC) when OpenEye - is installed but not requested as the charge backend has been - disabled (Issue #1760). - -**Security:** - -* diff --git a/news/fix_image_crop.rst b/news/fix_image_crop.rst deleted file mode 100644 index 3d1f1d31c..000000000 --- a/news/fix_image_crop.rst +++ /dev/null @@ -1,23 +0,0 @@ -**Added:** - -* - -**Changed:** - -* - -**Deprecated:** - -* - -**Removed:** - -* - -**Fixed:** - -* Fixed bug in ligand network visualization (such as with ``openfe view-ligand-network``) so that ligand names are no longer cut off by the plot border (`PR 1822 `_). - -**Security:** - -* diff --git a/news/progress_bar.rst b/news/progress_bar.rst deleted file mode 100644 index bd0999a29..000000000 --- a/news/progress_bar.rst +++ /dev/null @@ -1,23 +0,0 @@ -**Added:** - -* Added a progress bar for ``openfe gather`` JSON loading. - -**Changed:** - -* - -**Deprecated:** - -* - -**Removed:** - -* - -**Fixed:** - -* - -**Security:** - -* From 46faada929f4221a2c653a16d5a8fb5658c6fad7 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz Date: Thu, 5 Feb 2026 08:06:45 -0800 Subject: [PATCH 19/47] fix changelog links --- docs/CHANGELOG.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index 2a842200a..7d0e99c85 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -9,15 +9,15 @@ v1.8.1 **Added:** -* Added a progress bar for ``openfe gather`` JSON loading. +* Added a progress bar for ``openfe gather`` JSON loading (`PR #1786 `_). **Fixed:** * Due to issues with OpenFF's handling of toolkit registries with NAGL, the use of NAGL models (e.g. AshGC) when OpenEye is installed but not requested as the charge backend has been - disabled (Issue #1760). -* Fixed bug in ligand network visualization (such as with ``openfe view-ligand-network``) so that ligand names are no longer cut off by the plot border (`PR 1822 `_). + disabled (Issue #1760, `PR #1762 `_). +* Fixed bug in ligand network visualization (such as with ``openfe view-ligand-network``) so that ligand names are no longer cut off by the plot border (`PR #1822 `_). From 55d43736599611f29edd4a2e2ecc8a1e5c01c95e Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:02:37 -0800 Subject: [PATCH 20/47] add openeye w/ python 3.13 test to CI (#1745) * add openeye w/ python 3.13 test to CI * only run openeye on unbuntu w/ python 3.13 * add explicit openeye no --- .github/workflows/ci.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a3a5ac2f9..2d47c3784 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,17 +36,14 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "macos-latest"] + openeye: ["no"] python-version: - "3.11" - "3.12" - "3.13" - openeye: ["no"] - include: # NOTE: openeye does not yet support 3.13. + include: - os: "ubuntu-latest" - python-version: "3.11" - openeye: "yes" - - os: "macos-latest" - python-version: "3.12" + python-version: "3.13" openeye: "yes" env: From 84170d12adb3e28e598a8474372690207f7be872 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Tue, 9 Dec 2025 09:52:47 -0800 Subject: [PATCH 21/47] bump python version for conda cron (#1748) --- .github/workflows/conda_cron.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/conda_cron.yaml b/.github/workflows/conda_cron.yaml index e98989543..c96f00a94 100644 --- a/.github/workflows/conda_cron.yaml +++ b/.github/workflows/conda_cron.yaml @@ -22,9 +22,9 @@ jobs: matrix: os: ['ubuntu-latest', 'macos-latest'] python-version: - - "3.10" # bump to 3.13 after 1.6.0 is released - "3.11" - "3.12" + - "3.13" steps: - name: Checkout Code uses: actions/checkout@v4 From 542f966d4e01e24f34c668f93e0320ae61b55a3b Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:45:40 -0800 Subject: [PATCH 22/47] remove mypy rdkit pin (#1753) * remove mypy rdkit pin * try more explicit import * Revert "try more explicit import" This reverts commit 00cc2a37294280922d27505eae2eefa29e488f3f. * disallow attr-defined error code * try globally disabling attr-defined * try specific override * per-line ignores --- .github/workflows/mypy.yaml | 2 -- openfe/tests/conftest.py | 2 +- openfe/tests/dev/serialization_test_templates.py | 2 +- openfe/tests/setup/atom_mapping/test_lomap_scorers.py | 2 +- openfe/utils/atommapping_network_plotting.py | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index e66bf3005..5d0aba75b 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -36,9 +36,7 @@ jobs: cache-downloads-key: downloads-${{ steps.date.outputs.date }} create-args: >- python=3.12 - rdkit=2023.09.5 mypy>=1.17.0 - types-setuptools init-shell: bash diff --git a/openfe/tests/conftest.py b/openfe/tests/conftest.py index 5e304b695..a1bec3b2c 100644 --- a/openfe/tests/conftest.py +++ b/openfe/tests/conftest.py @@ -117,7 +117,7 @@ def pytest_configure(config): def mol_from_smiles(smiles: str) -> Chem.Mol: m = Chem.MolFromSmiles(smiles) - AllChem.Compute2DCoords(m) + AllChem.Compute2DCoords(m) # type: ignore[attr-defined] return m diff --git a/openfe/tests/dev/serialization_test_templates.py b/openfe/tests/dev/serialization_test_templates.py index c0cdd3341..adc6bf8c6 100644 --- a/openfe/tests/dev/serialization_test_templates.py +++ b/openfe/tests/dev/serialization_test_templates.py @@ -27,7 +27,7 @@ def mol_from_smiles(smiles: str) -> Chem.Mol: m = Chem.MolFromSmiles(smiles) - AllChem.Compute2DCoords(m) + AllChem.Compute2DCoords(m) # type: ignore[attr-defined] return m diff --git a/openfe/tests/setup/atom_mapping/test_lomap_scorers.py b/openfe/tests/setup/atom_mapping/test_lomap_scorers.py index 5e5ba8586..f956ee8d2 100644 --- a/openfe/tests/setup/atom_mapping/test_lomap_scorers.py +++ b/openfe/tests/setup/atom_mapping/test_lomap_scorers.py @@ -9,7 +9,7 @@ import pytest from numpy.testing import assert_allclose from rdkit import Chem -from rdkit.Chem.AllChem import Compute2DCoords +from rdkit.Chem.AllChem import Compute2DCoords # type: ignore[attr-defined] import openfe from openfe.setup import LigandAtomMapping, lomap_scorers diff --git a/openfe/utils/atommapping_network_plotting.py b/openfe/utils/atommapping_network_plotting.py index fc69ddd1f..13d899d66 100644 --- a/openfe/utils/atommapping_network_plotting.py +++ b/openfe/utils/atommapping_network_plotting.py @@ -42,7 +42,7 @@ def _draw_mapped_molecule( molA_to_molB: Dict[int, int], ): # create the image in a format matplotlib can handle - d2d = Chem.Draw.rdMolDraw2D.MolDraw2DCairo(300, 300, 300, 300) + d2d = Chem.Draw.rdMolDraw2D.MolDraw2DCairo(300, 300, 300, 300) # type: ignore[attr-defined] d2d.drawOptions().setBackgroundColour((1, 1, 1, 0.7)) # TODO: use a custom draw2d object; figure size from transforms img_bytes = draw_one_molecule_mapping( From 2de749d1d23a97251bfd0164e1cd3a7911945623 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Mon, 5 Jan 2026 22:03:09 -0500 Subject: [PATCH 23/47] Make the multistate hybrid topology samplers not require the HybridTopologyFactory (#1768) * make hybrid samplers not rely on htf but instead on the system & positions. --- .../openmm_rfe/_rfe_utils/multistate.py | 61 ++++++++++++------- .../protocols/openmm_rfe/equil_rfe_methods.py | 12 ++-- .../openmm_rfe/test_hybrid_top_protocol.py | 37 +++++------ 3 files changed, 65 insertions(+), 45 deletions(-) diff --git a/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py b/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py index 8c6b4eddc..299a846f6 100644 --- a/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py +++ b/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py @@ -26,14 +26,15 @@ logger = logging.getLogger(__name__) -class HybridCompatibilityMixin(object): +class HybridCompatibilityMixin: """ Mixin that allows the MultistateSampler to accommodate the situation where unsampled endpoints have a different number of degrees of freedom. """ - def __init__(self, *args, hybrid_factory=None, **kwargs): - self._hybrid_factory = hybrid_factory + def __init__(self, *args, hybrid_system, hybrid_positions, **kwargs): + self._hybrid_system = hybrid_system + self._hybrid_positions = hybrid_positions super(HybridCompatibilityMixin, self).__init__(*args, **kwargs) def setup(self, reporter, lambda_protocol, @@ -73,15 +74,17 @@ class creation of LambdaProtocol. """ n_states = len(lambda_protocol.lambda_schedule) - hybrid_system = self._factory.hybrid_system + lambda_zero_state = RelativeAlchemicalState.from_system(self._hybrid_system) - lambda_zero_state = RelativeAlchemicalState.from_system(hybrid_system) + thermostate = ThermodynamicState( + self._hybrid_system, + temperature=temperature + ) - thermostate = ThermodynamicState(hybrid_system, - temperature=temperature) compound_thermostate = CompoundThermodynamicState( - thermostate, - composable_states=[lambda_zero_state]) + thermostate, + composable_states=[lambda_zero_state] + ) # create lists for storing thermostates and sampler states thermodynamic_state_list = [] @@ -105,16 +108,20 @@ class creation of LambdaProtocol. raise ValueError(errmsg) # starting with the hybrid factory positions - box = hybrid_system.getDefaultPeriodicBoxVectors() - sampler_state = SamplerState(self._factory.hybrid_positions, - box_vectors=box) + box = self._hybrid_system.getDefaultPeriodicBoxVectors() + sampler_state = SamplerState( + self._hybrid_positions, + box_vectors=box + ) # Loop over the lambdas and create & store a compound thermostate at # that lambda value for lambda_val in lambda_schedule: compound_thermostate_copy = copy.deepcopy(compound_thermostate) compound_thermostate_copy.set_alchemical_parameters( - lambda_val, lambda_protocol) + lambda_val, + lambda_protocol + ) thermodynamic_state_list.append(compound_thermostate_copy) # now generating a sampler_state for each thermodyanmic state, @@ -143,7 +150,8 @@ class creation of LambdaProtocol. # generating unsampled endstates unsampled_dispersion_endstates = create_endstates( copy.deepcopy(thermodynamic_state_list[0]), - copy.deepcopy(thermodynamic_state_list[-1])) + copy.deepcopy(thermodynamic_state_list[-1]) + ) self.create(thermodynamic_states=thermodynamic_state_list, sampler_states=sampler_state_list, storage=reporter, unsampled_thermodynamic_states=unsampled_dispersion_endstates) @@ -159,10 +167,13 @@ class HybridRepexSampler(HybridCompatibilityMixin, number of positions """ - def __init__(self, *args, hybrid_factory=None, **kwargs): + def __init__(self, *args, hybrid_system, hybrid_positions, **kwargs): super(HybridRepexSampler, self).__init__( - *args, hybrid_factory=hybrid_factory, **kwargs) - self._factory = hybrid_factory + *args, + hybrid_system=hybrid_system, + hybrid_positions=hybrid_positions, + **kwargs + ) class HybridSAMSSampler(HybridCompatibilityMixin, sams.SAMSSampler): @@ -171,11 +182,13 @@ class HybridSAMSSampler(HybridCompatibilityMixin, sams.SAMSSampler): of positions """ - def __init__(self, *args, hybrid_factory=None, **kwargs): + def __init__(self, *args, hybrid_system, hybrid_positions, **kwargs): super(HybridSAMSSampler, self).__init__( - *args, hybrid_factory=hybrid_factory, **kwargs + *args, + hybrid_system=hybrid_system, + hybrid_positions=hybrid_positions, + **kwargs ) - self._factory = hybrid_factory class HybridMultiStateSampler(HybridCompatibilityMixin, @@ -184,11 +197,13 @@ class HybridMultiStateSampler(HybridCompatibilityMixin, MultiStateSampler that supports unsample end states with a different number of positions """ - def __init__(self, *args, hybrid_factory=None, **kwargs): + def __init__(self, *args, hybrid_system, hybrid_positions, **kwargs): super(HybridMultiStateSampler, self).__init__( - *args, hybrid_factory=hybrid_factory, **kwargs + *args, + hybrid_system=hybrid_system, + hybrid_positions=hybrid_positions, + **kwargs ) - self._factory = hybrid_factory def create_endstates(first_thermostate, last_thermostate): diff --git a/openfe/protocols/openmm_rfe/equil_rfe_methods.py b/openfe/protocols/openmm_rfe/equil_rfe_methods.py index 9a20c1e10..06fb8614d 100644 --- a/openfe/protocols/openmm_rfe/equil_rfe_methods.py +++ b/openfe/protocols/openmm_rfe/equil_rfe_methods.py @@ -1128,7 +1128,8 @@ def run( if sampler_settings.sampler_method.lower() == "repex": sampler = _rfe_utils.multistate.HybridRepexSampler( mcmc_moves=integrator, - hybrid_factory=hybrid_factory, + hybrid_system=hybrid_factory.hybrid_system, + hybrid_positions=hybrid_factory.hybrid_positions, online_analysis_interval=rta_its, online_analysis_target_error=early_termination_target_error, online_analysis_minimum_iterations=rta_min_its, @@ -1136,7 +1137,8 @@ def run( elif sampler_settings.sampler_method.lower() == "sams": sampler = _rfe_utils.multistate.HybridSAMSSampler( mcmc_moves=integrator, - hybrid_factory=hybrid_factory, + hybrid_system=hybrid_factory.hybrid_system, + hybrid_positions=hybrid_factory.hybrid_positions, online_analysis_interval=rta_its, online_analysis_minimum_iterations=rta_min_its, flatness_criteria=sampler_settings.sams_flatness_criteria, @@ -1145,12 +1147,12 @@ def run( elif sampler_settings.sampler_method.lower() == "independent": sampler = _rfe_utils.multistate.HybridMultiStateSampler( mcmc_moves=integrator, - hybrid_factory=hybrid_factory, + hybrid_system=hybrid_factory.hybrid_system, + hybrid_positions=hybrid_factory.hybrid_positions, online_analysis_interval=rta_its, online_analysis_target_error=early_termination_target_error, online_analysis_minimum_iterations=rta_min_its, ) - else: raise AttributeError(f"Unknown sampler {sampler_settings.sampler_method}") @@ -1247,7 +1249,7 @@ def run( if not dry: # pragma: no-cover return {"nc": nc, "last_checkpoint": chk, **analyzer.unit_results_dict} else: - return {"debug": {"sampler": sampler}} + return {"debug": {"sampler": sampler, "hybrid_factory": hybrid_factory}} @staticmethod def structural_analysis(scratch, shared) -> dict: diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py index 615eb8e8f..620fddb2b 100644 --- a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py @@ -236,13 +236,14 @@ def test_dry_run_default_vacuum( dag_unit = list(dag.protocol_units)[0] with tmpdir.as_cwd(): - sampler = dag_unit.run(dry=True)["debug"]["sampler"] + debug = dag_unit.run(dry=True)["debug"] + sampler = debug["sampler"] assert isinstance(sampler, MultiStateSampler) assert not sampler.is_periodic assert sampler._thermodynamic_states[0].barostat is None # Check hybrid OMM and MDTtraj Topologies - htf = sampler._hybrid_factory + htf = debug["hybrid_factory"] # 16 atoms: # 11 common atoms, 1 extra hydrogen in benzene, 4 extra in toluene # 12 bonds in benzene + 4 extra toluene bonds @@ -414,7 +415,7 @@ def test_dry_core_element_change(vac_settings, tmpdir): with tmpdir.as_cwd(): sampler = dag_unit.run(dry=True)["debug"]["sampler"] - system = sampler._hybrid_factory.hybrid_system + system = sampler._hybrid_system assert system.getNumParticles() == 12 # Average mass between nitrogen and carbon assert system.getParticleMass(1) == 12.0127235 * omm_unit.amu @@ -518,7 +519,7 @@ def tip4p_hybrid_factory( shared_basepath=shared_temp, ) - return dag_unit_result["debug"]["sampler"]._factory + return dag_unit_result["debug"]["hybrid_factory"] def test_tip4p_particle_count(tip4p_hybrid_factory): @@ -624,7 +625,7 @@ def test_dry_run_ligand_system_cutoff( with tmpdir.as_cwd(): sampler = dag_unit.run(dry=True)["debug"]["sampler"] - hs = sampler._factory.hybrid_system + hs = sampler._hybrid_system nbfs = [ f @@ -691,9 +692,10 @@ def test_dry_run_charge_backends( dag_unit = list(dag.protocol_units)[0] with tmpdir.as_cwd(): - sampler = dag_unit.run(dry=True)["debug"]["sampler"] - htf = sampler._factory - hybrid_system = htf.hybrid_system + debug = dag_unit.run(dry=True)["debug"] + sampler = debug["sampler"] + htf = debug["hybrid_factory"] + hybrid_system = sampler._hybrid_system # get the standard nonbonded force nonbond = [f for f in hybrid_system.getForces() if isinstance(f, NonbondedForce)] @@ -785,9 +787,10 @@ def check_propchgs(smc, charge_array): dag_unit = list(dag.protocol_units)[0] with tmpdir.as_cwd(): - sampler = dag_unit.run(dry=True)["debug"]["sampler"] - htf = sampler._factory - hybrid_system = htf.hybrid_system + debug = dag_unit.run(dry=True)["debug"] + sampler = debug["sampler"] + htf = debug["hybrid_factory"] + hybrid_system = sampler._hybrid_system # get the standard nonbonded force nonbond = [f for f in hybrid_system.getForces() if isinstance(f, NonbondedForce)] @@ -902,7 +905,7 @@ def test_dodecahdron_ligand_box( with tmpdir.as_cwd(): sampler = dag_unit.run(dry=True)["debug"]["sampler"] - hs = sampler._factory.hybrid_system + hs = sampler._hybrid_system vectors = hs.getDefaultPeriodicBoxVectors() @@ -1598,7 +1601,7 @@ def tyk2_xml(tmp_path_factory): dryrun = pu.run(dry=True, shared_basepath=tmp) - system = dryrun["debug"]["sampler"]._hybrid_factory.hybrid_system + system = dryrun["debug"]["sampler"]._hybrid_system return ET.fromstring(XmlSerializer.serialize(system)) @@ -2153,8 +2156,8 @@ def test_dry_run_alchemwater_solvent(benzene_to_benzoic_mapping, solv_settings, unit = list(dag.protocol_units)[0] with tmpdir.as_cwd(): - sampler = unit.run(dry=True)["debug"]["sampler"] - htf = sampler._factory + debug = unit.run(dry=True)["debug"] + htf = debug["hybrid_factory"] _assert_total_charge(htf.hybrid_system, htf._atom_classes, 0, 0) assert len(htf._atom_classes["core_atoms"]) == 14 @@ -2222,8 +2225,8 @@ def test_dry_run_complex_alchemwater_totcharge( unit = list(dag.protocol_units)[0] with tmpdir.as_cwd(): - sampler = unit.run(dry=True)["debug"]["sampler"] - htf = sampler._factory + debug = unit.run(dry=True)["debug"] + htf = debug["hybrid_factory"] _assert_total_charge(htf.hybrid_system, htf._atom_classes, chgA, chgB) assert len(htf._atom_classes["core_atoms"]) == core_atoms From 98d03c617058781f5c49a593bb52444a3b6abd71 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Tue, 6 Jan 2026 10:02:33 -0500 Subject: [PATCH 24/47] Migrate validation to Protocol._validate in HybridTopologyProtocol (#1740) * Migrate & add validation to Protocol._validate for hybrid topology Protocol --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hannah Baumann <43765638+hannahbaumann@users.noreply.github.com> Co-authored-by: Josh Horton --- news/validate-rfe.rst | 26 + .../protocols/openmm_rfe/equil_rfe_methods.py | 558 ++++++++++------ .../openmm_utils/system_validation.py | 55 +- .../openmm_rfe/test_hybrid_top_protocol.py | 358 ----------- .../openmm_rfe/test_hybrid_top_validation.py | 594 ++++++++++++++++++ 5 files changed, 1028 insertions(+), 563 deletions(-) create mode 100644 news/validate-rfe.rst create mode 100644 openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py diff --git a/news/validate-rfe.rst b/news/validate-rfe.rst new file mode 100644 index 000000000..d7036e8d4 --- /dev/null +++ b/news/validate-rfe.rst @@ -0,0 +1,26 @@ +**Added:** + +* The `validate` method for the RelativeHybridTopologyProtocol has been + implemented. This means that settings and system validation can mostly + be done prior to Protocol execution by calling + `RelativeHybridTopologyProtocol.validate(stateA, stateB, mapping)`. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/openfe/protocols/openmm_rfe/equil_rfe_methods.py b/openfe/protocols/openmm_rfe/equil_rfe_methods.py index 06fb8614d..8c782acfb 100644 --- a/openfe/protocols/openmm_rfe/equil_rfe_methods.py +++ b/openfe/protocols/openmm_rfe/equil_rfe_methods.py @@ -53,7 +53,6 @@ from openff.units import Quantity, unit from openff.units.openmm import ensure_quantity, from_openmm, to_openmm from openmmtools import multistate -from rdkit import Chem from openfe.due import Doi, due from openfe.protocols.openmm_utils.omm_settings import ( @@ -117,161 +116,6 @@ def _get_resname(off_mol) -> str: return names[0] -def _get_alchemical_charge_difference( - mapping: LigandAtomMapping, - nonbonded_method: str, - explicit_charge_correction: bool, - solvent_component: SolventComponent, -) -> int: - """ - Checks and returns the difference in formal charge between state A and B. - - Raises - ------ - ValueError - * If an explicit charge correction is attempted and the - nonbonded method is not PME. - * If the absolute charge difference is greater than one - and an explicit charge correction is attempted. - UserWarning - If there is any charge difference. - - Parameters - ---------- - mapping : dict[str, ComponentMapping] - Dictionary of mappings between transforming components. - nonbonded_method : str - The OpenMM nonbonded method used for the simulation. - explicit_charge_correction : bool - Whether or not to use an explicit charge correction. - solvent_component : openfe.SolventComponent - The SolventComponent of the simulation. - - Returns - ------- - int - The formal charge difference between states A and B. - This is defined as sum(charge state A) - sum(charge state B) - """ - - difference = mapping.get_alchemical_charge_difference() - - if abs(difference) > 0: - if explicit_charge_correction: - if nonbonded_method.lower() != "pme": - errmsg = "Explicit charge correction when not using PME is not currently supported." - raise ValueError(errmsg) - if abs(difference) > 1: - errmsg = ( - f"A charge difference of {difference} is observed " - "between the end states and an explicit charge " - "correction has been requested. Unfortunately " - "only absolute differences of 1 are supported." - ) - raise ValueError(errmsg) - - ion = {-1: solvent_component.positive_ion, 1: solvent_component.negative_ion}[ - difference - ] - wmsg = ( - f"A charge difference of {difference} is observed " - "between the end states. This will be addressed by " - f"transforming a water into a {ion} ion" - ) - logger.warning(wmsg) - warnings.warn(wmsg) - else: - wmsg = ( - f"A charge difference of {difference} is observed " - "between the end states. No charge correction has " - "been requested, please account for this in your " - "final results." - ) - logger.warning(wmsg) - warnings.warn(wmsg) - - return difference - - -def _validate_alchemical_components( - alchemical_components: dict[str, list[Component]], - mapping: Optional[Union[ComponentMapping, list[ComponentMapping]]], -): - """ - Checks that the alchemical components are suitable for the RFE protocol. - - Specifically we check: - 1. That all alchemical components are mapped. - 2. That all alchemical components are SmallMoleculeComponents. - 3. If the mappings involves element changes in core atoms - - Parameters - ---------- - alchemical_components : dict[str, list[Component]] - Dictionary contatining the alchemical components for - states A and B. - mapping : Optional[Union[ComponentMapping, list[ComponentMapping]]] - all mappings between transforming components. - - Raises - ------ - ValueError - * If there are more than one mapping or mapping is None - * If there are any unmapped alchemical components. - * If there are any alchemical components that are not - SmallMoleculeComponents. - UserWarning - * Mappings which involve element changes in core atoms - """ - if isinstance(mapping, ComponentMapping): - mapping = [mapping] - # Check mapping - # For now we only allow for a single mapping, this will likely change - if mapping is None or len(mapping) != 1: - errmsg = "A single LigandAtomMapping is expected for this Protocol" - raise ValueError(errmsg) - - # Check that all alchemical components are mapped & small molecules - mapped = { - "stateA": [m.componentA for m in mapping], - "stateB": [m.componentB for m in mapping], - } - - for idx in ["stateA", "stateB"]: - if len(alchemical_components[idx]) != len(mapped[idx]): - errmsg = f"missing alchemical components in {idx}" - raise ValueError(errmsg) - for comp in alchemical_components[idx]: - if comp not in mapped[idx]: - raise ValueError(f"Unmapped alchemical component {comp}") - if not isinstance(comp, SmallMoleculeComponent): # pragma: no-cover - errmsg = ( - "Transformations involving non " - "SmallMoleculeComponent species {comp} " - "are not currently supported" - ) - raise ValueError(errmsg) - - # Validate element changes in mappings - for m in mapping: - molA = m.componentA.to_rdkit() - molB = m.componentB.to_rdkit() - for i, j in m.componentA_to_componentB.items(): - atomA = molA.GetAtomWithIdx(i) - atomB = molB.GetAtomWithIdx(j) - if atomA.GetAtomicNum() != atomB.GetAtomicNum(): - wmsg = ( - f"Element change in mapping between atoms " - f"Ligand A: {i} (element {atomA.GetAtomicNum()}) and " - f"Ligand B: {j} (element {atomB.GetAtomicNum()})\n" - "No mass scaling is attempted in the hybrid topology, " - "the average mass of the two atoms will be used in the " - "simulation" - ) - logger.warning(wmsg) - warnings.warn(wmsg) # TODO: remove this once logging is fixed - - class RelativeHybridTopologyProtocolResult(gufe.ProtocolResult): """Dict-like container for the output of a RelativeHybridTopologyProtocol""" @@ -612,21 +456,337 @@ def _adaptive_settings( return protocol_settings - def _create( + @staticmethod + def _validate_endstates( + stateA: ChemicalSystem, + stateB: ChemicalSystem, + ) -> None: + """ + Validates the end states for the RFE protocol. + + Parameters + ---------- + stateA : ChemicalSystem + The chemical system of end state A. + stateB : ChemicalSystem + The chemical system of end state B. + + Raises + ------ + ValueError + * If either state contains more than one unique Component. + * If unique components are not SmallMoleculeComponents. + """ + # Get the difference in Components between each state + diff = stateA.component_diff(stateB) + + for i, entry in enumerate(diff): + state_label = "A" if i == 0 else "B" + + # Check that there is only one unique Component in each state + if len(entry) != 1: + errmsg = ( + "Only one alchemical component is allowed per end state. " + f"Found {len(entry)} in state {state_label}." + ) + raise ValueError(errmsg) + + # Check that the unique Component is a SmallMoleculeComponent + if not isinstance(entry[0], SmallMoleculeComponent): + errmsg = ( + f"Alchemical component in state {state_label} is of type " + f"{type(entry[0])}, but only SmallMoleculeComponents " + "transformations are currently supported." + ) + raise ValueError(errmsg) + + @staticmethod + def _validate_mapping( + mapping: Optional[Union[ComponentMapping, list[ComponentMapping]]], + alchemical_components: dict[str, list[Component]], + ) -> None: + """ + Validates that the provided mapping(s) are suitable for the RFE protocol. + + Parameters + ---------- + mapping : Optional[Union[ComponentMapping, list[ComponentMapping]]] + all mappings between transforming components. + alchemical_components : dict[str, list[Component]] + Dictionary contatining the alchemical components for + states A and B. + + Raises + ------ + ValueError + * If there are more than one mapping or mapping is None + * If the mapping components are not in the alchemical components. + UserWarning + * Mappings which involve element changes in core atoms + """ + # if a single mapping is provided, convert to list + if isinstance(mapping, ComponentMapping): + mapping = [mapping] + + # For now we only support a single mapping + if mapping is None or len(mapping) > 1: + errmsg = "A single LigandAtomMapping is expected for this Protocol" + raise ValueError(errmsg) + + # check that the mapping components are in the alchemical components + for m in mapping: + for state in ["A", "B"]: + comp = getattr(m, f"component{state}") + if comp not in alchemical_components[f"state{state}"]: + raise ValueError( + f"Mapping component{state} {comp} not " + f"in alchemical components of state{state}" + ) + + # TODO: remove - this is now the default behaviour? + # Check for element changes in mappings + for m in mapping: + molA = m.componentA.to_rdkit() + molB = m.componentB.to_rdkit() + for i, j in m.componentA_to_componentB.items(): + atomA = molA.GetAtomWithIdx(i) + atomB = molB.GetAtomWithIdx(j) + if atomA.GetAtomicNum() != atomB.GetAtomicNum(): + wmsg = ( + f"Element change in mapping between atoms " + f"Ligand A: {i} (element {atomA.GetAtomicNum()}) and " + f"Ligand B: {j} (element {atomB.GetAtomicNum()})\n" + "No mass scaling is attempted in the hybrid topology, " + "the average mass of the two atoms will be used in the " + "simulation" + ) + logger.warning(wmsg) + warnings.warn(wmsg) + + @staticmethod + def _validate_smcs( + stateA: ChemicalSystem, + stateB: ChemicalSystem, + ) -> None: + """ + Validates the SmallMoleculeComponents. + + Parameters + ---------- + stateA : ChemicalSystem + The chemical system of end state A. + stateB : ChemicalSystem + The chemical system of end state B. + + Raises + ------ + ValueError + * If there are isomorphic SmallMoleculeComponents with + different charges. + """ + smcs_A = stateA.get_components_of_type(SmallMoleculeComponent) + smcs_B = stateB.get_components_of_type(SmallMoleculeComponent) + smcs_all = list(set(smcs_A).union(set(smcs_B))) + offmols = [m.to_openff() for m in smcs_all] + + def _equal_charges(moli, molj): + # Base case, both molecules don't have charges + if (moli.partial_charges is None) & (molj.partial_charges is None): + return True + # If either is None but not the other + if (moli.partial_charges is None) ^ (molj.partial_charges is None): + return False + # Check if the charges are close to each other + return np.allclose(moli.partial_charges, molj.partial_charges) + + clashes = [] + + for i, moli in enumerate(offmols): + for molj in offmols: + if moli.is_isomorphic_with(molj): + if not _equal_charges(moli, molj): + clashes.append(smcs_all[i]) + + if len(clashes) > 0: + errmsg = ( + "Found SmallMoleculeComponents that are isomorphic " + "but with different charges, this is not currently allowed. " + f"Affected components: {clashes}" + ) + raise ValueError(errmsg) + + @staticmethod + def _validate_charge_difference( + mapping: LigandAtomMapping, + nonbonded_method: str, + explicit_charge_correction: bool, + solvent_component: SolventComponent | None, + ): + """ + Validates the net charge difference between the two states. + + Parameters + ---------- + mapping : dict[str, ComponentMapping] + Dictionary of mappings between transforming components. + nonbonded_method : str + The OpenMM nonbonded method used for the simulation. + explicit_charge_correction : bool + Whether or not to use an explicit charge correction. + solvent_component : openfe.SolventComponent | None + The SolventComponent of the simulation. + + Raises + ------ + ValueError + * If an explicit charge correction is attempted and the + nonbonded method is not PME. + * If the absolute charge difference is greater than one + and an explicit charge correction is attempted. + * If an explicit charge correction is attempted and there is no solvent present. + UserWarning + * If there is any charge difference. + """ + difference = mapping.get_alchemical_charge_difference() + + if abs(difference) == 0: + return + + if not explicit_charge_correction: + wmsg = ( + f"A charge difference of {difference} is observed " + "between the end states. No charge correction has " + "been requested, please account for this in your " + "final results." + ) + logger.warning(wmsg) + warnings.warn(wmsg) + return + + if solvent_component is None: + errmsg = "Cannot use explicit charge correction without solvent" + raise ValueError(errmsg) + + # We implicitly check earlier that we have to have pme for a solvated + # system, so we only need to check the nonbonded method here + if nonbonded_method.lower() != "pme": + errmsg = "Explicit charge correction when not using PME is not currently supported." + raise ValueError(errmsg) + + if abs(difference) > 1: + errmsg = ( + f"A charge difference of {difference} is observed " + "between the end states and an explicit charge " + "correction has been requested. Unfortunately " + "only absolute differences of 1 are supported." + ) + raise ValueError(errmsg) + + ion = {-1: solvent_component.positive_ion, 1: solvent_component.negative_ion}[difference] + + wmsg = ( + f"A charge difference of {difference} is observed " + "between the end states. This will be addressed by " + f"transforming a water into a {ion} ion" + ) + logger.info(wmsg) + + @staticmethod + def _validate_simulation_settings( + simulation_settings: MultiStateSimulationSettings, + integrator_settings: IntegratorSettings, + output_settings: MultiStateOutputSettings, + ): + """ + Validate various simulation settings, including but not limited to + timestep conversions, and output file write frequencies. + + Parameters + ---------- + simulation_settings : MultiStateSimulationSettings + The sampler simulation settings. + integrator_settings : IntegratorSettings + Settings defining the behaviour of the integrator. + output_settings : MultiStateOutputSettings + Settings defining the simulation file writing behaviour. + + Raises + ------ + ValueError + * If any of of the simulation control settings (e.g. + ``equilibration_length`` or ``production_length``) + are not divisible by the timestep or the number of + steps per iteration. + * If the output frequency for position, velocity, or + online analysis are not divisible by the time per + multistate iteration. + """ + + steps_per_iteration = settings_validation.convert_steps_per_iteration( + simulation_settings=simulation_settings, + integrator_settings=integrator_settings, + ) + + _ = settings_validation.get_simsteps( + sim_length=simulation_settings.equilibration_length, + timestep=integrator_settings.timestep, + mc_steps=steps_per_iteration, + ) + + _ = settings_validation.get_simsteps( + sim_length=simulation_settings.production_length, + timestep=integrator_settings.timestep, + mc_steps=steps_per_iteration, + ) + + _ = settings_validation.convert_checkpoint_interval_to_iterations( + checkpoint_interval=output_settings.checkpoint_interval, + time_per_iteration=simulation_settings.time_per_iteration, + ) + + if output_settings.positions_write_frequency is not None: + _ = settings_validation.divmod_time_and_check( + numerator=output_settings.positions_write_frequency, + denominator=simulation_settings.time_per_iteration, + numerator_name="output settings' positions_write_frequency", + denominator_name="sampler settings' time_per_iteration", + ) + + if output_settings.velocities_write_frequency is not None: + _ = settings_validation.divmod_time_and_check( + numerator=output_settings.velocities_write_frequency, + denominator=simulation_settings.time_per_iteration, + numerator_name="output settings' velocities_write_frequency", + denominator_name="sampler settings' time_per_iteration", + ) + + _, _ = settings_validation.convert_real_time_analysis_iterations( + simulation_settings=simulation_settings, + ) + + def _validate( self, stateA: ChemicalSystem, stateB: ChemicalSystem, - mapping: Optional[Union[gufe.ComponentMapping, list[gufe.ComponentMapping]]], - extends: Optional[gufe.ProtocolDAGResult] = None, - ) -> list[gufe.ProtocolUnit]: - # TODO: Extensions? + mapping: gufe.ComponentMapping | list[gufe.ComponentMapping] | None, + extends: gufe.ProtocolDAGResult | None = None, + ) -> None: + # Check we're not trying to extend if extends: - raise NotImplementedError("Can't extend simulations yet") + # This technically should be NotImplementedError + # but gufe.Protocol.validate calls `_validate` wrapped around an + # except for NotImplementedError, so we can't raise it here + raise ValueError("Can't extend simulations yet") - # Get alchemical components & validate them + mapping + # Validate the end states + self._validate_endstates(stateA, stateB) + + # Validate the mapping alchem_comps = system_validation.get_alchemical_components(stateA, stateB) - _validate_alchemical_components(alchem_comps, mapping) - ligandmapping = mapping[0] if isinstance(mapping, list) else mapping + self._validate_mapping(mapping, alchem_comps) + + # Validate the small molecule components + self._validate_smcs(stateA, stateB) # Validate solvent component nonbond = self.settings.forcefield_settings.nonbonded_method @@ -638,11 +798,68 @@ def _create( # Validate protein component system_validation.validate_protein(stateA) + # Validate charge difference + # Note: validation depends on the mapping & solvent component checks + if stateA.contains(SolventComponent): + solv_comp = stateA.get_components_of_type(SolventComponent)[0] + else: + solv_comp = None + + self._validate_charge_difference( + mapping=mapping[0] if isinstance(mapping, list) else mapping, + nonbonded_method=self.settings.forcefield_settings.nonbonded_method, + explicit_charge_correction=self.settings.alchemical_settings.explicit_charge_correction, + solvent_component=solv_comp, + ) + + # Validate integrator things + settings_validation.validate_timestep( + self.settings.forcefield_settings.hydrogen_mass, + self.settings.integrator_settings.timestep, + ) + + # Validate simulation & output settings + self._validate_simulation_settings( + self.settings.simulation_settings, + self.settings.integrator_settings, + self.settings.output_settings, + ) + + # Validate alchemical settings + # PR #125 temporarily pin lambda schedule spacing to n_replicas + if ( + self.settings.simulation_settings.n_replicas + != self.settings.lambda_settings.lambda_windows + ): + errmsg = ( + "Number of replicas in ``simulation_settings``: " + f"{self.settings.simulation_settings.n_replicas} must equal " + "the number of lambda windows in lambda_settings: " + f"{self.settings.lambda_settings.lambda_windows}." + ) + raise ValueError(errmsg) + + def _create( + self, + stateA: ChemicalSystem, + stateB: ChemicalSystem, + mapping: Optional[Union[gufe.ComponentMapping, list[gufe.ComponentMapping]]], + extends: Optional[gufe.ProtocolDAGResult] = None, + ) -> list[gufe.ProtocolUnit]: + # validate inputs + self.validate(stateA=stateA, stateB=stateB, mapping=mapping, extends=extends) + + # get alchemical components and mapping + alchem_comps = system_validation.get_alchemical_components(stateA, stateB) + ligandmapping = mapping[0] if isinstance(mapping, list) else mapping + # actually create and return Units Anames = ",".join(c.name for c in alchem_comps["stateA"]) Bnames = ",".join(c.name for c in alchem_comps["stateB"]) + # our DAG has no dependencies, so just list units n_repeats = self.settings.protocol_repeats + units = [ RelativeHybridTopologyProtocolUnit( protocol=self, @@ -816,10 +1033,6 @@ def run( output_settings: MultiStateOutputSettings = protocol_settings.output_settings integrator_settings: IntegratorSettings = protocol_settings.integrator_settings - # is the timestep good for the mass? - settings_validation.validate_timestep( - forcefield_settings.hydrogen_mass, integrator_settings.timestep - ) # TODO: Also validate various conversions? # Convert various time based inputs to steps/iterations steps_per_iteration = settings_validation.convert_steps_per_iteration( @@ -842,12 +1055,7 @@ def run( # Get the change difference between the end states # and check if the charge correction used is appropriate - charge_difference = _get_alchemical_charge_difference( - mapping, - forcefield_settings.nonbonded_method, - alchem_settings.explicit_charge_correction, - solvent_comp, - ) + charge_difference = mapping.get_alchemical_charge_difference() # 1. Create stateA system self.logger.info("Parameterizing molecules") diff --git a/openfe/protocols/openmm_utils/system_validation.py b/openfe/protocols/openmm_utils/system_validation.py index 0fd3c3518..3e8ed5c50 100644 --- a/openfe/protocols/openmm_utils/system_validation.py +++ b/openfe/protocols/openmm_utils/system_validation.py @@ -95,23 +95,24 @@ def validate_solvent(state: ChemicalSystem, nonbonded_method: str): `nocutoff`. * If the SolventComponent solvent is not water. """ - solv = [comp for comp in state.values() if isinstance(comp, SolventComponent)] + solv_comps = state.get_components_of_type(SolventComponent) - if len(solv) > 0 and nonbonded_method.lower() == "nocutoff": - errmsg = "nocutoff cannot be used for solvent transformations" - raise ValueError(errmsg) - - if len(solv) == 0 and nonbonded_method.lower() == "pme": - errmsg = "PME cannot be used for vacuum transform" - raise ValueError(errmsg) + if len(solv_comps) > 0: + if nonbonded_method.lower() == "nocutoff": + errmsg = "nocutoff cannot be used for solvent transformations" + raise ValueError(errmsg) - if len(solv) > 1: - errmsg = "Multiple SolventComponent found, only one is supported" - raise ValueError(errmsg) + if len(solv_comps) > 1: + errmsg = "Multiple SolventComponent found, only one is supported" + raise ValueError(errmsg) - if len(solv) > 0 and solv[0].smiles != "O": - errmsg = "Non water solvent is not currently supported" - raise ValueError(errmsg) + if solv_comps[0].smiles != "O": + errmsg = "Non water solvent is not currently supported" + raise ValueError(errmsg) + else: + if nonbonded_method.lower() == "pme": + errmsg = "PME cannot be used for vacuum transform" + raise ValueError(errmsg) def validate_protein(state: ChemicalSystem): @@ -129,9 +130,9 @@ def validate_protein(state: ChemicalSystem): ValueError If there are multiple ProteinComponent in the ChemicalSystem. """ - nprot = sum(1 for comp in state.values() if isinstance(comp, ProteinComponent)) + prot_comps = state.get_components_of_type(ProteinComponent) - if nprot > 1: + if len(prot_comps) > 1: errmsg = "Multiple ProteinComponent found, only one is supported" raise ValueError(errmsg) @@ -161,24 +162,18 @@ def get_components(state: ChemicalSystem) -> ParseCompRet: small_mols : list[SmallMoleculeComponent] """ - def _get_single_comps(comp_list, comptype): - ret_comps = [comp for comp in comp_list if isinstance(comp, comptype)] - if ret_comps: - return ret_comps[0] + def _get_single_comps(state, comptype): + comps = state.get_components_of_type(comptype) + + if len(comps) > 0: + return comps[0] else: return None - solvent_comp: Optional[SolventComponent] = _get_single_comps( - list(state.values()), SolventComponent - ) + solvent_comp: Optional[SolventComponent] = _get_single_comps(state, SolventComponent) - protein_comp: Optional[ProteinComponent] = _get_single_comps( - list(state.values()), ProteinComponent - ) + protein_comp: Optional[ProteinComponent] = _get_single_comps(state, ProteinComponent) - small_mols = [] - for comp in state.components.values(): - if isinstance(comp, SmallMoleculeComponent): - small_mols.append(comp) + small_mols = state.get_components_of_type(SmallMoleculeComponent) return solvent_comp, protein_comp, small_mols diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py index 620fddb2b..60045bcc0 100644 --- a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py @@ -30,10 +30,6 @@ from openfe import setup from openfe.protocols import openmm_rfe from openfe.protocols.openmm_rfe._rfe_utils import topologyhelpers -from openfe.protocols.openmm_rfe.equil_rfe_methods import ( - _get_alchemical_charge_difference, - _validate_alchemical_components, -) from openfe.protocols.openmm_utils import omm_compute, system_creation from openfe.protocols.openmm_utils.charge_generation import ( HAS_ESPALOMA_CHARGE, @@ -196,21 +192,6 @@ def test_create_independent_repeat_ids(benzene_system, toluene_system, benzene_t assert len(repeat_ids) == 6 -@pytest.mark.parametrize( - "mapping", - [None, [], ["A", "B"]], -) -def test_validate_alchemical_components_wrong_mappings(mapping): - with pytest.raises(ValueError, match="A single LigandAtomMapping"): - _validate_alchemical_components({"stateA": [], "stateB": []}, mapping) - - -def test_validate_alchemical_components_missing_alchem_comp(benzene_to_toluene_mapping): - alchem_comps = {"stateA": [openfe.SolventComponent()], "stateB": []} - with pytest.raises(ValueError, match="Unmapped alchemical component"): - _validate_alchemical_components(alchem_comps, benzene_to_toluene_mapping) - - @pytest.mark.parametrize("method", ["repex", "sams", "independent", "InDePeNdENT"]) def test_dry_run_default_vacuum( benzene_vacuum_system, @@ -970,246 +951,6 @@ def test_lambda_schedule(windows): assert len(lambdas.lambda_schedule) == windows -def test_hightimestep( - benzene_vacuum_system, - toluene_vacuum_system, - benzene_to_toluene_mapping, - vac_settings, - tmpdir, -): - vac_settings.forcefield_settings.hydrogen_mass = 1.0 - - p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=vac_settings, - ) - - dag = p.create( - stateA=benzene_vacuum_system, - stateB=toluene_vacuum_system, - mapping=benzene_to_toluene_mapping, - ) - dag_unit = list(dag.protocol_units)[0] - - errmsg = "too large for hydrogen mass" - with tmpdir.as_cwd(): - with pytest.raises(ValueError, match=errmsg): - dag_unit.run(dry=True) - - -def test_n_replicas_not_n_windows( - benzene_vacuum_system, - toluene_vacuum_system, - benzene_to_toluene_mapping, - vac_settings, - tmpdir, -): - # For PR #125 we pin such that the number of lambda windows - # equals the numbers of replicas used - TODO: remove limitation - # default lambda windows is 11 - vac_settings.simulation_settings.n_replicas = 13 - - errmsg = "Number of replicas 13 does not equal the number of lambda windows 11" - - with tmpdir.as_cwd(): - with pytest.raises(ValueError, match=errmsg): - p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=vac_settings, - ) - dag = p.create( - stateA=benzene_vacuum_system, - stateB=toluene_vacuum_system, - mapping=benzene_to_toluene_mapping, - ) - dag_unit = list(dag.protocol_units)[0] - dag_unit.run(dry=True) - - -def test_missing_ligand(benzene_system, benzene_to_toluene_mapping): - # state B doesn't have a ligand component - stateB = openfe.ChemicalSystem({"solvent": openfe.SolventComponent()}) - - p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=openmm_rfe.RelativeHybridTopologyProtocol.default_settings(), - ) - - match_str = "missing alchemical components in stateB" - with pytest.raises(ValueError, match=match_str): - _ = p.create( - stateA=benzene_system, - stateB=stateB, - mapping=benzene_to_toluene_mapping, - ) - - -def test_vaccuum_PME_error( - benzene_vacuum_system, benzene_modifications, benzene_to_toluene_mapping -): - # state B doesn't have a solvent component (i.e. its vacuum) - stateB = openfe.ChemicalSystem({"ligand": benzene_modifications["toluene"]}) - - p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=openmm_rfe.RelativeHybridTopologyProtocol.default_settings(), - ) - errmsg = "PME cannot be used for vacuum transform" - with pytest.raises(ValueError, match=errmsg): - _ = p.create( - stateA=benzene_vacuum_system, - stateB=stateB, - mapping=benzene_to_toluene_mapping, - ) - - -def test_incompatible_solvent(benzene_system, benzene_modifications, benzene_to_toluene_mapping): - # the solvents are different - stateB = openfe.ChemicalSystem( - { - "ligand": benzene_modifications["toluene"], - "solvent": openfe.SolventComponent(positive_ion="K", negative_ion="Cl"), - } - ) - - p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=openmm_rfe.RelativeHybridTopologyProtocol.default_settings(), - ) - # We don't have a way to map non-ligand components so for now it - # just triggers that it's not a mapped component - errmsg = "missing alchemical components in stateA" - with pytest.raises(ValueError, match=errmsg): - _ = p.create( - stateA=benzene_system, - stateB=stateB, - mapping=benzene_to_toluene_mapping, - ) - - -def test_mapping_mismatch_A(benzene_system, toluene_system, benzene_modifications): - # the atom mapping doesn't refer to the ligands in the systems - mapping = setup.LigandAtomMapping( - componentA=benzene_system.components["ligand"], - componentB=benzene_modifications["phenol"], - componentA_to_componentB=dict(), - ) - - p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=openmm_rfe.RelativeHybridTopologyProtocol.default_settings(), - ) - errmsg = ( - r"Unmapped alchemical component " - r"SmallMoleculeComponent\(name=toluene\)" - ) - with pytest.raises(ValueError, match=errmsg): - _ = p.create( - stateA=benzene_system, - stateB=toluene_system, - mapping=mapping, - ) - - -def test_mapping_mismatch_B(benzene_system, toluene_system, benzene_modifications): - mapping = setup.LigandAtomMapping( - componentA=benzene_modifications["phenol"], - componentB=toluene_system.components["ligand"], - componentA_to_componentB=dict(), - ) - - p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=openmm_rfe.RelativeHybridTopologyProtocol.default_settings(), - ) - errmsg = ( - r"Unmapped alchemical component " - r"SmallMoleculeComponent\(name=benzene\)" - ) - with pytest.raises(ValueError, match=errmsg): - _ = p.create( - stateA=benzene_system, - stateB=toluene_system, - mapping=mapping, - ) - - -def test_complex_mismatch(benzene_system, toluene_complex_system, benzene_to_toluene_mapping): - # only one complex - p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=openmm_rfe.RelativeHybridTopologyProtocol.default_settings(), - ) - with pytest.raises(ValueError): - _ = p.create( - stateA=benzene_system, - stateB=toluene_complex_system, - mapping=benzene_to_toluene_mapping, - ) - - -def test_too_many_specified_mappings(benzene_system, toluene_system, benzene_to_toluene_mapping): - # mapping dict requires 'ligand' key - p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=openmm_rfe.RelativeHybridTopologyProtocol.default_settings(), - ) - errmsg = "A single LigandAtomMapping is expected for this Protocol" - with pytest.raises(ValueError, match=errmsg): - _ = p.create( - stateA=benzene_system, - stateB=toluene_system, - mapping=[benzene_to_toluene_mapping, benzene_to_toluene_mapping], - ) - - -def test_protein_mismatch( - benzene_complex_system, toluene_complex_system, benzene_to_toluene_mapping -): - # hack one protein to be labelled differently - prot = toluene_complex_system["protein"] - alt_prot = openfe.ProteinComponent(prot.to_rdkit(), name="Mickey Mouse") - alt_toluene_complex_system = openfe.ChemicalSystem( - { - "ligand": toluene_complex_system["ligand"], - "solvent": toluene_complex_system["solvent"], - "protein": alt_prot, - } - ) - - p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=openmm_rfe.RelativeHybridTopologyProtocol.default_settings(), - ) - with pytest.raises(ValueError): - _ = p.create( - stateA=benzene_complex_system, - stateB=alt_toluene_complex_system, - mapping=benzene_to_toluene_mapping, - ) - - -def test_element_change_warning(atom_mapping_basic_test_files): - # check a mapping with element change gets rejected early - l1 = atom_mapping_basic_test_files["2-methylnaphthalene"] - l2 = atom_mapping_basic_test_files["2-naftanol"] - - # We use the 'old' lomap defaults because the - # basic test files inputs we use aren't fully aligned - mapper = setup.LomapAtomMapper( - time=20, threed=True, max3d=1000.0, element_change=True, seed="", shift=True - ) - - mapping = next(mapper.suggest_mappings(l1, l2)) - - sys1 = openfe.ChemicalSystem( - {"ligand": l1, "solvent": openfe.SolventComponent()}, - ) - sys2 = openfe.ChemicalSystem( - {"ligand": l2, "solvent": openfe.SolventComponent()}, - ) - - p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=openmm_rfe.RelativeHybridTopologyProtocol.default_settings(), - ) - with pytest.warns(UserWarning, match="Element change"): - _ = p.create( - stateA=sys1, - stateB=sys2, - mapping=mapping, - ) - - def test_ligand_overlap_warning( benzene_vacuum_system, toluene_vacuum_system, benzene_to_toluene_mapping, vac_settings, tmpdir ): @@ -1752,68 +1493,6 @@ def test_filenotfound_replica_states(self, protocolresult): protocolresult.get_replica_states() -@pytest.mark.parametrize( - "mapping_name,result", - [ - ["benzene_to_toluene_mapping", 0], - ["benzene_to_benzoic_mapping", 1], - ["benzene_to_aniline_mapping", -1], - ["aniline_to_benzene_mapping", 1], - ], -) -def test_get_charge_difference(mapping_name, result, request): - mapping = request.getfixturevalue(mapping_name) - if result != 0: - ion = r"Na\+" if result == -1 else r"Cl\-" - wmsg = ( - f"A charge difference of {result} is observed " - "between the end states. This will be addressed by " - f"transforming a water into a {ion} ion" - ) - with pytest.warns(UserWarning, match=wmsg): - val = _get_alchemical_charge_difference(mapping, "pme", True, openfe.SolventComponent()) - assert result == pytest.approx(val) - else: - val = _get_alchemical_charge_difference(mapping, "pme", True, openfe.SolventComponent()) - assert result == pytest.approx(val) - - -def test_get_charge_difference_no_pme(benzene_to_benzoic_mapping): - errmsg = "Explicit charge correction when not using PME" - with pytest.raises(ValueError, match=errmsg): - _get_alchemical_charge_difference( - benzene_to_benzoic_mapping, - "nocutoff", - True, - openfe.SolventComponent(), - ) - - -def test_get_charge_difference_no_corr(benzene_to_benzoic_mapping): - wmsg = ( - "A charge difference of 1 is observed between the end states. " - "No charge correction has been requested" - ) - with pytest.warns(UserWarning, match=wmsg): - _get_alchemical_charge_difference( - benzene_to_benzoic_mapping, - "pme", - False, - openfe.SolventComponent(), - ) - - -def test_greater_than_one_charge_difference_error(aniline_to_benzoic_mapping): - errmsg = "A charge difference of 2" - with pytest.raises(ValueError, match=errmsg): - _get_alchemical_charge_difference( - aniline_to_benzoic_mapping, - "pme", - True, - openfe.SolventComponent(), - ) - - @pytest.fixture(scope="session") def benzene_solvent_openmm_system(benzene_modifications): smc = benzene_modifications["benzene"] @@ -2290,40 +1969,3 @@ def test_dry_run_vacuum_write_frequency( assert reporter.velocity_interval == velocities_write_frequency.m else: assert reporter.velocity_interval == 0 - - -@pytest.mark.parametrize( - "positions_write_frequency,velocities_write_frequency", - [ - [100.1 * unit.picosecond, 100 * unit.picosecond], - [100 * unit.picosecond, 100.1 * unit.picosecond], - ], -) -def test_pos_write_frequency_not_divisible( - benzene_vacuum_system, - toluene_vacuum_system, - benzene_to_toluene_mapping, - positions_write_frequency, - velocities_write_frequency, - tmpdir, - vac_settings, -): - vac_settings.output_settings.positions_write_frequency = positions_write_frequency - vac_settings.output_settings.velocities_write_frequency = velocities_write_frequency - - protocol = openmm_rfe.RelativeHybridTopologyProtocol( - settings=vac_settings, - ) - - # create DAG from protocol and take first (and only) work unit from within - dag = protocol.create( - stateA=benzene_vacuum_system, - stateB=toluene_vacuum_system, - mapping=benzene_to_toluene_mapping, - ) - dag_unit = list(dag.protocol_units)[0] - - with tmpdir.as_cwd(): - errmsg = "The output settings' " - with pytest.raises(ValueError, match=errmsg): - dag_unit.run(dry=True)["debug"]["sampler"] diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py new file mode 100644 index 000000000..35ef57da0 --- /dev/null +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py @@ -0,0 +1,594 @@ +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +import logging + +import pytest +from openff.units import unit as offunit + +import openfe +from openfe import setup +from openfe.protocols import openmm_rfe + + +@pytest.fixture() +def vac_settings(): + settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() + settings.forcefield_settings.nonbonded_method = "nocutoff" + settings.engine_settings.compute_platform = None + settings.protocol_repeats = 1 + return settings + + +@pytest.fixture() +def solv_settings(): + settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() + settings.engine_settings.compute_platform = None + settings.protocol_repeats = 1 + return settings + + +def test_invalid_protocol_repeats(): + settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() + with pytest.raises(ValueError, match="must be a positive value"): + settings.protocol_repeats = -1 + + +@pytest.mark.parametrize("state", ["A", "B"]) +def test_endstate_two_alchemcomp_stateA(state, benzene_modifications): + first_state = openfe.ChemicalSystem( + { + "ligandA": benzene_modifications["benzene"], + "ligandB": benzene_modifications["toluene"], + "solvent": openfe.SolventComponent(), + } + ) + other_state = openfe.ChemicalSystem( + { + "ligandC": benzene_modifications["phenol"], + "solvent": openfe.SolventComponent(), + } + ) + + if state == "A": + args = (first_state, other_state) + else: + args = (other_state, first_state) + + with pytest.raises(ValueError, match="Only one alchemical component"): + openmm_rfe.RelativeHybridTopologyProtocol._validate_endstates(*args) + + +@pytest.mark.parametrize("state", ["A", "B"]) +def test_endstates_not_smc(state, benzene_modifications): + first_state = openfe.ChemicalSystem( + { + "ligand": benzene_modifications["benzene"], + "foo": openfe.SolventComponent(), + } + ) + other_state = openfe.ChemicalSystem( + { + "ligand": benzene_modifications["benzene"], + "foo": benzene_modifications["toluene"], + } + ) + + if state == "A": + args = (first_state, other_state) + else: + args = (other_state, first_state) + + errmsg = "only SmallMoleculeComponents transformations" + with pytest.raises(ValueError, match=errmsg): + openmm_rfe.RelativeHybridTopologyProtocol._validate_endstates(*args) + + +def test_validate_mapping_none_mapping(): + errmsg = "A single LigandAtomMapping is expected" + with pytest.raises(ValueError, match=errmsg): + openmm_rfe.RelativeHybridTopologyProtocol._validate_mapping(None, None) + + +def test_validate_mapping_multi_mapping(benzene_to_toluene_mapping): + errmsg = "A single LigandAtomMapping is expected" + with pytest.raises(ValueError, match=errmsg): + openmm_rfe.RelativeHybridTopologyProtocol._validate_mapping( + [benzene_to_toluene_mapping] * 2, None + ) + + +@pytest.mark.parametrize("state", ["A", "B"]) +def test_validate_mapping_alchem_not_in(state, benzene_to_toluene_mapping): + errmsg = f"not in alchemical components of state{state}" + + if state == "A": + alchem_comps = {"stateA": [], "stateB": [benzene_to_toluene_mapping.componentB]} + else: + alchem_comps = {"stateA": [benzene_to_toluene_mapping.componentA], "stateB": []} + + with pytest.raises(ValueError, match=errmsg): + openmm_rfe.RelativeHybridTopologyProtocol._validate_mapping( + [benzene_to_toluene_mapping], + alchem_comps, + ) + + +def test_vaccuum_PME_error( + benzene_vacuum_system, toluene_vacuum_system, benzene_to_toluene_mapping, solv_settings +): + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=solv_settings) + + errmsg = "PME cannot be used for vacuum transform" + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=benzene_vacuum_system, + stateB=toluene_vacuum_system, + mapping=benzene_to_toluene_mapping, + ) + + +@pytest.mark.parametrize("charge", [None, "gasteiger"]) +def test_smcs_same_charge_passes(charge, benzene_modifications): + benzene = benzene_modifications["benzene"] + if charge is None: + smc = benzene + else: + offmol = benzene.to_openff() + offmol.assign_partial_charges(partial_charge_method="gasteiger") + smc = openfe.SmallMoleculeComponent.from_openff(offmol) + + # Just pass the same thing twice + state = openfe.ChemicalSystem({"l": smc}) + openmm_rfe.RelativeHybridTopologyProtocol._validate_smcs(state, state) + + +def test_smcs_different_charges_none_not_none(benzene_modifications): + # smcA has no charges + smcA = benzene_modifications["benzene"] + + # smcB has charges + offmol = smcA.to_openff() + offmol.assign_partial_charges(partial_charge_method="gasteiger") + smcB = openfe.SmallMoleculeComponent.from_openff(offmol) + + stateA = openfe.ChemicalSystem({"l": smcA}) + stateB = openfe.ChemicalSystem({"l": smcB}) + + errmsg = "isomorphic but with different charges" + with pytest.raises(ValueError, match=errmsg): + openmm_rfe.RelativeHybridTopologyProtocol._validate_smcs(stateA, stateB) + + +def test_smcs_different_charges_all(benzene_modifications): + # For this test, we will assign both A and B to both states + # It wouldn't happen in real life, but it tests that within a state + # you can pick up isomorphic molecules with different charges + # create an offmol with gasteiger charges + offmol = benzene_modifications["benzene"].to_openff() + offmol.assign_partial_charges(partial_charge_method="gasteiger") + smcA = openfe.SmallMoleculeComponent.from_openff(offmol) + + # now alter the offmol charges, scaling by 0.1 + offmol.partial_charges *= 0.1 + smcB = openfe.SmallMoleculeComponent.from_openff(offmol) + + state = openfe.ChemicalSystem({"l1": smcA, "l2": smcB}) + + errmsg = "isomorphic but with different charges" + with pytest.raises(ValueError, match=errmsg): + openmm_rfe.RelativeHybridTopologyProtocol._validate_smcs(state, state) + + +def test_solvent_nocutoff_error( + benzene_system, + toluene_system, + benzene_to_toluene_mapping, + vac_settings, +): + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=vac_settings) + + errmsg = "nocutoff cannot be used for solvent transformation" + + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=benzene_system, + stateB=toluene_system, + mapping=benzene_to_toluene_mapping, + ) + + +def test_nonwater_solvent_error( + benzene_modifications, + benzene_to_toluene_mapping, + solv_settings, +): + solvent = openfe.SolventComponent(smiles="C") + stateA = openfe.ChemicalSystem( + { + "ligand": benzene_modifications["benzene"], + "solvent": solvent, + } + ) + + stateB = openfe.ChemicalSystem({"ligand": benzene_modifications["toluene"], "solvent": solvent}) + + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=solv_settings) + + errmsg = "Non water solvent is not currently supported" + + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=stateA, + stateB=stateB, + mapping=benzene_to_toluene_mapping, + ) + + +def test_too_many_solv_comps_error( + benzene_modifications, + benzene_to_toluene_mapping, + solv_settings, +): + stateA = openfe.ChemicalSystem( + { + "ligand": benzene_modifications["benzene"], + "solvent!": openfe.SolventComponent(neutralize=True), + "solvent2": openfe.SolventComponent(neutralize=False), + } + ) + + stateB = openfe.ChemicalSystem( + { + "ligand": benzene_modifications["toluene"], + "solvent!": openfe.SolventComponent(neutralize=True), + "solvent2": openfe.SolventComponent(neutralize=False), + } + ) + + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=solv_settings) + + errmsg = "Multiple SolventComponent found, only one is supported" + + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=stateA, + stateB=stateB, + mapping=benzene_to_toluene_mapping, + ) + + +def test_bad_solv_settings( + benzene_system, + toluene_system, + benzene_to_toluene_mapping, + solv_settings, +): + """ + Test a case where the solvent settings would be wrong. + Not doing every cases since those are covered under + ``test_openmmutils.py``. + """ + solv_settings.solvation_settings.solvent_padding = 1.2 * offunit.nanometer + solv_settings.solvation_settings.number_of_solvent_molecules = 20 + + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=solv_settings) + + errmsg = "Only one of solvent_padding, number_of_solvent_molecules," + with pytest.raises(ValueError, match=errmsg): + p.validate(stateA=benzene_system, stateB=toluene_system, mapping=benzene_to_toluene_mapping) + + +def test_too_many_prot_comps_error( + benzene_modifications, + benzene_to_toluene_mapping, + T4_protein_component, + eg5_protein, + solv_settings, +): + stateA = openfe.ChemicalSystem( + { + "ligand": benzene_modifications["benzene"], + "solvent": openfe.SolventComponent(), + "protein1": T4_protein_component, + "protein2": eg5_protein, + } + ) + + stateB = openfe.ChemicalSystem( + { + "ligand": benzene_modifications["toluene"], + "solvent": openfe.SolventComponent(), + "protein1": T4_protein_component, + "protein2": eg5_protein, + } + ) + + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=solv_settings) + + errmsg = "Multiple ProteinComponent found, only one is supported" + + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=stateA, + stateB=stateB, + mapping=benzene_to_toluene_mapping, + ) + + +def test_element_change_warning(atom_mapping_basic_test_files): + # check a mapping with element change gets rejected early + l1 = atom_mapping_basic_test_files["2-methylnaphthalene"] + l2 = atom_mapping_basic_test_files["2-naftanol"] + + # We use the 'old' lomap defaults because the + # basic test files inputs we use aren't fully aligned + mapper = setup.LomapAtomMapper( + time=20, threed=True, max3d=1000.0, element_change=True, seed="", shift=True + ) + + mapping = next(mapper.suggest_mappings(l1, l2)) + + alchem_comps = {"stateA": [l1], "stateB": [l2]} + + with pytest.warns(UserWarning, match="Element change"): + openmm_rfe.RelativeHybridTopologyProtocol._validate_mapping( + [mapping], + alchem_comps, + ) + + +def test_charge_difference_no_corr(benzene_to_benzoic_mapping): + wmsg = ( + "A charge difference of 1 is observed between the end states. " + "No charge correction has been requested" + ) + + with pytest.warns(UserWarning, match=wmsg): + openmm_rfe.RelativeHybridTopologyProtocol._validate_charge_difference( + benzene_to_benzoic_mapping, + "pme", + False, + openfe.SolventComponent(), + ) + + +def test_charge_difference_no_solvent(benzene_to_benzoic_mapping): + errmsg = "Cannot use explicit charge correction without solvent" + + with pytest.raises(ValueError, match=errmsg): + openmm_rfe.RelativeHybridTopologyProtocol._validate_charge_difference( + benzene_to_benzoic_mapping, + "pme", + True, + None, + ) + + +def test_charge_difference_no_pme(benzene_to_benzoic_mapping): + errmsg = "Explicit charge correction when not using PME" + + with pytest.raises(ValueError, match=errmsg): + openmm_rfe.RelativeHybridTopologyProtocol._validate_charge_difference( + benzene_to_benzoic_mapping, + "nocutoff", + True, + openfe.SolventComponent(), + ) + + +def test_greater_than_one_charge_difference_error(aniline_to_benzoic_mapping): + errmsg = "A charge difference of 2" + with pytest.raises(ValueError, match=errmsg): + openmm_rfe.RelativeHybridTopologyProtocol._validate_charge_difference( + aniline_to_benzoic_mapping, + "pme", + True, + openfe.SolventComponent(), + ) + + +@pytest.mark.parametrize( + "mapping_name,result", + [ + ["benzene_to_toluene_mapping", 0], + ["benzene_to_benzoic_mapping", 1], + ["benzene_to_aniline_mapping", -1], + ["aniline_to_benzene_mapping", 1], + ], +) +def test_get_charge_difference(mapping_name, result, request, caplog): + mapping = request.getfixturevalue(mapping_name) + caplog.set_level(logging.INFO) + + ion = r"Na+" if result == -1 else r"Cl-" + msg = ( + f"A charge difference of {result} is observed " + "between the end states. This will be addressed by " + f"transforming a water into a {ion} ion" + ) + + openmm_rfe.RelativeHybridTopologyProtocol._validate_charge_difference( + mapping, "pme", True, openfe.SolventComponent() + ) + + if result != 0: + assert msg in caplog.text + else: + assert msg not in caplog.text + + +def test_hightimestep( + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + vac_settings, +): + vac_settings.forcefield_settings.hydrogen_mass = 1.0 + + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=vac_settings) + + errmsg = "too large for hydrogen mass" + + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=benzene_vacuum_system, + stateB=toluene_vacuum_system, + mapping=benzene_to_toluene_mapping, + extends=None, + ) + + +def test_time_per_iteration_divmod( + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + vac_settings, +): + vac_settings.simulation_settings.time_per_iteration = 10 * offunit.ps + vac_settings.integrator_settings.timestep = 4 * offunit.ps + + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=vac_settings) + + errmsg = "does not evenly divide by the timestep" + + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=benzene_vacuum_system, + stateB=toluene_vacuum_system, + mapping=benzene_to_toluene_mapping, + extends=None, + ) + + +@pytest.mark.parametrize("attribute", ["equilibration_length", "production_length"]) +def test_simsteps_not_timestep_divisible( + attribute, + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + vac_settings, +): + setattr(vac_settings.simulation_settings, attribute, 102 * offunit.fs) + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=vac_settings) + + errmsg = "Simulation time not divisible by timestep" + + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=benzene_vacuum_system, + stateB=toluene_vacuum_system, + mapping=benzene_to_toluene_mapping, + extends=None, + ) + + +@pytest.mark.parametrize("attribute", ["equilibration_length", "production_length"]) +def test_simsteps_not_mcstep_divisible( + attribute, + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + vac_settings, +): + setattr(vac_settings.simulation_settings, attribute, 102 * offunit.ps) + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=vac_settings) + + errmsg = "should contain a number of steps divisible by the number of integrator timesteps" + + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=benzene_vacuum_system, + stateB=toluene_vacuum_system, + mapping=benzene_to_toluene_mapping, + extends=None, + ) + + +def test_checkpoint_interval_not_divisible_time_per_iter( + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + vac_settings, +): + vac_settings.output_settings.checkpoint_interval = 4 * offunit.ps + vac_settings.simulation_settings.time_per_iteration = 2.5 * offunit.ps + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=vac_settings) + + errmsg = "does not evenly divide by the amount of time per state MCMC" + + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=benzene_vacuum_system, + stateB=toluene_vacuum_system, + mapping=benzene_to_toluene_mapping, + extends=None, + ) + + +@pytest.mark.parametrize("attribute", ["positions_write_frequency", "velocities_write_frequency"]) +def test_pos_vel_write_frequency_not_divisible( + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + attribute, + vac_settings, +): + setattr(vac_settings.output_settings, attribute, 100.1 * offunit.picosecond) + + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=vac_settings) + + errmsg = f"The output settings' {attribute}" + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=benzene_vacuum_system, + stateB=toluene_vacuum_system, + mapping=benzene_to_toluene_mapping, + extends=None, + ) + + +@pytest.mark.parametrize( + "attribute", ["real_time_analysis_interval", "real_time_analysis_interval"] +) +def test_real_time_analysis_not_divisible( + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + attribute, + vac_settings, +): + setattr(vac_settings.simulation_settings, attribute, 100.1 * offunit.picosecond) + + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=vac_settings) + + errmsg = f"The {attribute}" + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=benzene_vacuum_system, + stateB=toluene_vacuum_system, + mapping=benzene_to_toluene_mapping, + extends=None, + ) + + +def test_n_replicas_not_n_windows( + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + vac_settings, + tmpdir, +): + # For PR #125 we pin such that the number of lambda windows + # equals the numbers of replicas used - TODO: remove limitation + vac_settings.simulation_settings.n_replicas = 13 + p = openmm_rfe.RelativeHybridTopologyProtocol(settings=vac_settings) + + errmsg = "Number of replicas in ``simulation_settings``:" + + with pytest.raises(ValueError, match=errmsg): + p.validate( + stateA=benzene_vacuum_system, + stateB=toluene_vacuum_system, + mapping=benzene_to_toluene_mapping, + extends=None, + ) From eaafa9ac5d008f90d04b32d54dbf7636f0c44519 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Wed, 7 Jan 2026 08:15:35 -0500 Subject: [PATCH 25/47] Move rfe protocol (#1769) Move RFE protocol files around --- openfe/protocols/openmm_rfe/__init__.py | 12 +- .../protocols/openmm_rfe/equil_rfe_methods.py | 1520 +---------------- .../openmm_rfe/hybridtop_protocol_results.py | 239 +++ .../openmm_rfe/hybridtop_protocols.py | 631 +++++++ .../protocols/openmm_rfe/hybridtop_units.py | 693 ++++++++ openfe/tests/utils/test_duecredit.py | 2 +- 6 files changed, 1576 insertions(+), 1521 deletions(-) create mode 100644 openfe/protocols/openmm_rfe/hybridtop_protocol_results.py create mode 100644 openfe/protocols/openmm_rfe/hybridtop_protocols.py create mode 100644 openfe/protocols/openmm_rfe/hybridtop_units.py diff --git a/openfe/protocols/openmm_rfe/__init__.py b/openfe/protocols/openmm_rfe/__init__.py index e400cc3d3..ca1ae0fd2 100644 --- a/openfe/protocols/openmm_rfe/__init__.py +++ b/openfe/protocols/openmm_rfe/__init__.py @@ -2,11 +2,7 @@ # For details, see https://github.com/OpenFreeEnergy/openfe from . import _rfe_utils -from .equil_rfe_methods import ( - RelativeHybridTopologyProtocol, - RelativeHybridTopologyProtocolResult, - RelativeHybridTopologyProtocolUnit, -) -from .equil_rfe_settings import ( - RelativeHybridTopologyProtocolSettings, -) +from .equil_rfe_settings import RelativeHybridTopologyProtocolSettings +from .hybridtop_protocol_results import RelativeHybridTopologyProtocolResult +from .hybridtop_protocols import RelativeHybridTopologyProtocol +from .hybridtop_units import RelativeHybridTopologyProtocolUnit diff --git a/openfe/protocols/openmm_rfe/equil_rfe_methods.py b/openfe/protocols/openmm_rfe/equil_rfe_methods.py index 8c782acfb..cdd80c863 100644 --- a/openfe/protocols/openmm_rfe/equil_rfe_methods.py +++ b/openfe/protocols/openmm_rfe/equil_rfe_methods.py @@ -1,1526 +1,22 @@ # This code is part of OpenFE and is licensed under the MIT license. # For details, see https://github.com/OpenFreeEnergy/openfe -"""Equilibrium Relative Free Energy methods using OpenMM and OpenMMTools in a +"""Equilibrium Relative Free Energy Protocol using OpenMM and OpenMMTools in a Perses-like manner. -This module implements the necessary methodology toolking to run calculate a -ligand relative free energy transformation using OpenMM tools and one of the -following methods: +This module implements the necessary tooling to calculate the +relative free energy of a ligand transformation using OpenMM tools and one of +the following methods: - Hamiltonian Replica Exchange - Self-adjusted mixture sampling - Independent window sampling -TODO ----- -* Improve this docstring by adding an example use case. - Acknowledgements ---------------- This Protocol is based on, and leverages components originating from the Perses toolkit (https://github.com/choderalab/perses). """ -from __future__ import annotations - -import json -import logging -import os -import pathlib -import subprocess -import uuid -import warnings -from collections import defaultdict -from itertools import chain -from typing import Any, Iterable, Optional, Union - -import gufe -import matplotlib.pyplot as plt -import mdtraj -import numpy as np -import numpy.typing as npt -import openmmtools -from gufe import ( - ChemicalSystem, - Component, - ComponentMapping, - LigandAtomMapping, - ProteinComponent, - SmallMoleculeComponent, - SolventComponent, - settings, -) -from openff.toolkit.topology import Molecule as OFFMolecule -from openff.units import Quantity, unit -from openff.units.openmm import ensure_quantity, from_openmm, to_openmm -from openmmtools import multistate - -from openfe.due import Doi, due -from openfe.protocols.openmm_utils.omm_settings import ( - BasePartialChargeSettings, -) - -from ...analysis import plotting -from ...utils import log_system_probe, without_oechem_backend -from ..openmm_utils import ( - charge_generation, - multistate_analysis, - omm_compute, - settings_validation, - system_creation, - system_validation, -) -from . import _rfe_utils -from .equil_rfe_settings import ( - AlchemicalSettings, - IntegratorSettings, - LambdaSettings, - MultiStateOutputSettings, - MultiStateSimulationSettings, - OpenFFPartialChargeSettings, - OpenMMEngineSettings, - OpenMMSolvationSettings, - RelativeHybridTopologyProtocolSettings, -) - -logger = logging.getLogger(__name__) - - -due.cite( - Doi("10.5281/zenodo.1297683"), - description="Perses", - path="openfe.protocols.openmm_rfe.equil_rfe_methods", - cite_module=True, -) - -due.cite( - Doi("10.5281/zenodo.596622"), - description="OpenMMTools", - path="openfe.protocols.openmm_rfe.equil_rfe_methods", - cite_module=True, -) - -due.cite( - Doi("10.1371/journal.pcbi.1005659"), - description="OpenMM", - path="openfe.protocols.openmm_rfe.equil_rfe_methods", - cite_module=True, -) - - -def _get_resname(off_mol) -> str: - # behaviour changed between 0.10 and 0.11 - omm_top = off_mol.to_topology().to_openmm() - names = [r.name for r in omm_top.residues()] - if len(names) > 1: - raise ValueError("We assume single residue") - return names[0] - - -class RelativeHybridTopologyProtocolResult(gufe.ProtocolResult): - """Dict-like container for the output of a RelativeHybridTopologyProtocol""" - - def __init__(self, **data): - super().__init__(**data) - # data is mapping of str(repeat_id): list[protocolunitresults] - # TODO: Detect when we have extensions and stitch these together? - if any(len(pur_list) > 2 for pur_list in self.data.values()): - raise NotImplementedError("Can't stitch together results yet") - - @staticmethod - def compute_mean_estimate(dGs: list[Quantity]) -> Quantity: - u = dGs[0].u - # convert all values to units of the first value, then take average of magnitude - # this would avoid a screwy case where each value was in different units - vals = np.asarray([dG.to(u).m for dG in dGs]) - - return np.average(vals) * u - - def get_estimate(self) -> Quantity: - """Average free energy difference of this transformation - - Returns - ------- - dG : openff.units.Quantity - The free energy difference between the first and last states. This is - a Quantity defined with units. - """ - # TODO: Check this holds up completely for SAMS. - dGs = [pus[0].outputs["unit_estimate"] for pus in self.data.values()] - return self.compute_mean_estimate(dGs) - - @staticmethod - def compute_uncertainty(dGs: list[Quantity]) -> Quantity: - u = dGs[0].u - # convert all values to units of the first value, then take average of magnitude - # this would avoid a screwy case where each value was in different units - vals = np.asarray([dG.to(u).m for dG in dGs]) - - return np.std(vals) * u - - def get_uncertainty(self) -> Quantity: - """The uncertainty/error in the dG value: The std of the estimates of - each independent repeat - """ - - dGs = [pus[0].outputs["unit_estimate"] for pus in self.data.values()] - return self.compute_uncertainty(dGs) - - def get_individual_estimates(self) -> list[tuple[Quantity, Quantity]]: - """Return a list of tuples containing the individual free energy - estimates and associated MBAR errors for each repeat. - - Returns - ------- - dGs : list[tuple[openff.units.Quantity]] - n_replicate simulation list of tuples containing the free energy - estimates (first entry) and associated MBAR estimate errors - (second entry). - """ - dGs = [ - (pus[0].outputs["unit_estimate"], pus[0].outputs["unit_estimate_error"]) - for pus in self.data.values() - ] - return dGs - - def get_forward_and_reverse_energy_analysis( - self, - ) -> list[Optional[dict[str, Union[npt.NDArray, Quantity]]]]: - """ - Get a list of forward and reverse analysis of the free energies - for each repeat using uncorrelated production samples. - - The returned dicts have keys: - 'fractions' - the fraction of data used for this estimate - 'forward_DGs', 'reverse_DGs' - for each fraction of data, the estimate - 'forward_dDGs', 'reverse_dDGs' - for each estimate, the uncertainty - - The 'fractions' values are a numpy array, while the other arrays are - Quantity arrays, with units attached. - - If the list entry is ``None`` instead of a dictionary, this indicates - that the analysis could not be carried out for that repeat. This - is most likely caused by MBAR convergence issues when attempting to - calculate free energies from too few samples. - - - Returns - ------- - forward_reverse : list[Optional[dict[str, Union[npt.NDArray, openff.units.Quantity]]]] - - - Raises - ------ - UserWarning - If any of the forward and reverse entries are ``None``. - """ - forward_reverse = [ - pus[0].outputs["forward_and_reverse_energies"] for pus in self.data.values() - ] - - if None in forward_reverse: - wmsg = ( - "One or more ``None`` entries were found in the list of " - "forward and reverse analyses. This is likely caused by " - "an MBAR convergence failure caused by too few independent " - "samples when calculating the free energies of the 10% " - "timeseries slice." - ) - warnings.warn(wmsg) - - return forward_reverse - - def get_overlap_matrices(self) -> list[dict[str, npt.NDArray]]: - """ - Return a list of dictionary containing the MBAR overlap estimates - calculated for each repeat. - - Returns - ------- - overlap_stats : list[dict[str, npt.NDArray]] - A list of dictionaries containing the following keys: - * ``scalar``: One minus the largest nontrivial eigenvalue - * ``eigenvalues``: The sorted (descending) eigenvalues of the - overlap matrix - * ``matrix``: Estimated overlap matrix of observing a sample from - state i in state j - """ - # Loop through and get the repeats and get the matrices - overlap_stats = [pus[0].outputs["unit_mbar_overlap"] for pus in self.data.values()] - - return overlap_stats - - def get_replica_transition_statistics(self) -> list[dict[str, npt.NDArray]]: - """The replica lambda state transition statistics for each repeat. - - Note - ---- - This is currently only available in cases where a replica exchange - simulation was run. - - Returns - ------- - repex_stats : list[dict[str, npt.NDArray]] - A list of dictionaries containing the following: - * ``eigenvalues``: The sorted (descending) eigenvalues of the - lambda state transition matrix - * ``matrix``: The transition matrix estimate of a replica switching - from state i to state j. - """ - try: - repex_stats = [ - pus[0].outputs["replica_exchange_statistics"] for pus in self.data.values() - ] - except KeyError: - errmsg = "Replica exchange statistics were not found, did you run a repex calculation?" - raise ValueError(errmsg) - - return repex_stats - - def get_replica_states(self) -> list[npt.NDArray]: - """ - Returns the timeseries of replica states for each repeat. - - Returns - ------- - replica_states : List[npt.NDArray] - List of replica states for each repeat - """ - - def is_file(filename: str): - p = pathlib.Path(filename) - if not p.exists(): - errmsg = f"File could not be found {p}" - raise ValueError(errmsg) - return p - - replica_states = [] - - for pus in self.data.values(): - nc = is_file(pus[0].outputs["nc"]) - dir_path = nc.parents[0] - chk = is_file(dir_path / pus[0].outputs["last_checkpoint"]).name - reporter = multistate.MultiStateReporter( - storage=nc, checkpoint_storage=chk, open_mode="r" - ) - replica_states.append(np.asarray(reporter.read_replica_thermodynamic_states())) - reporter.close() - - return replica_states - - def equilibration_iterations(self) -> list[float]: - """ - Returns the number of equilibration iterations for each repeat - of the calculation. - - Returns - ------- - equilibration_lengths : list[float] - """ - equilibration_lengths = [ - pus[0].outputs["equilibration_iterations"] for pus in self.data.values() - ] - - return equilibration_lengths - - def production_iterations(self) -> list[float]: - """ - Returns the number of uncorrelated production samples for each - repeat of the calculation. - - Returns - ------- - production_lengths : list[float] - """ - production_lengths = [pus[0].outputs["production_iterations"] for pus in self.data.values()] - - return production_lengths - - -class RelativeHybridTopologyProtocol(gufe.Protocol): - """ - Relative Free Energy calculations using OpenMM and OpenMMTools. - - Based on `Perses `_ - - See Also - -------- - :mod:`openfe.protocols` - :class:`openfe.protocols.openmm_rfe.RelativeHybridTopologySettings` - :class:`openfe.protocols.openmm_rfe.RelativeHybridTopologyResult` - :class:`openfe.protocols.openmm_rfe.RelativeHybridTopologyProtocolUnit` - """ - - result_cls = RelativeHybridTopologyProtocolResult - _settings_cls = RelativeHybridTopologyProtocolSettings - _settings: RelativeHybridTopologyProtocolSettings - - @classmethod - def _default_settings(cls): - """A dictionary of initial settings for this creating this Protocol - - These settings are intended as a suitable starting point for creating - an instance of this protocol. It is recommended, however that care is - taken to inspect and customize these before performing a Protocol. - - Returns - ------- - Settings - a set of default settings - """ - return RelativeHybridTopologyProtocolSettings( - protocol_repeats=3, - forcefield_settings=settings.OpenMMSystemGeneratorFFSettings(), - thermo_settings=settings.ThermoSettings( - temperature=298.15 * unit.kelvin, - pressure=1 * unit.bar, - ), - partial_charge_settings=OpenFFPartialChargeSettings(), - solvation_settings=OpenMMSolvationSettings(), - alchemical_settings=AlchemicalSettings(softcore_LJ="gapsys"), - lambda_settings=LambdaSettings(), - simulation_settings=MultiStateSimulationSettings( - equilibration_length=1.0 * unit.nanosecond, - production_length=5.0 * unit.nanosecond, - ), - engine_settings=OpenMMEngineSettings(), - integrator_settings=IntegratorSettings(), - output_settings=MultiStateOutputSettings(), - ) - - @classmethod - def _adaptive_settings( - cls, - stateA: ChemicalSystem, - stateB: ChemicalSystem, - mapping: gufe.LigandAtomMapping | list[gufe.LigandAtomMapping], - initial_settings: None | RelativeHybridTopologyProtocolSettings = None, - ) -> RelativeHybridTopologyProtocolSettings: - """ - Get the recommended OpenFE settings for this protocol based on the input states involved in the - transformation. - - These are intended as a suitable starting point for creating an instance of this protocol, which can be further - customized before performing a Protocol. - - Parameters - ---------- - stateA : ChemicalSystem - The initial state of the transformation. - stateB : ChemicalSystem - The final state of the transformation. - mapping : LigandAtomMapping | list[LigandAtomMapping] - The mapping(s) between transforming components in stateA and stateB. - initial_settings : None | RelativeHybridTopologyProtocolSettings, optional - Initial settings to base the adaptive settings on. If None, default settings are used. - - Returns - ------- - RelativeHybridTopologyProtocolSettings - The recommended settings for this protocol based on the input states. - - Notes - ----- - - If the transformation involves a change in net charge, the settings are adapted to use a more expensive - protocol with 22 lambda windows and 20 ns production length per window. - - If both states contain a ProteinComponent, the solvation padding is set to 1 nm. - - If initial_settings is provided, the adaptive settings are based on a copy of these settings. - """ - # use initial settings or default settings - # this is needed for the CLI so we don't override user settings - if initial_settings is not None: - protocol_settings = initial_settings.model_copy(deep=True) - else: - protocol_settings = cls.default_settings() - - if isinstance(mapping, list): - mapping = mapping[0] - - if mapping.get_alchemical_charge_difference() != 0: - # apply the recommended charge change settings taken from the industry benchmarking as fast settings not validated - # - info = ( - "Charge changing transformation between ligands " - f"{mapping.componentA.name} and {mapping.componentB.name}. " - "A more expensive protocol with 22 lambda windows, sampled " - "for 20 ns each, will be used here." - ) - logger.info(info) - protocol_settings.alchemical_settings.explicit_charge_correction = True - protocol_settings.simulation_settings.production_length = 20 * unit.nanosecond - protocol_settings.simulation_settings.n_replicas = 22 - protocol_settings.lambda_settings.lambda_windows = 22 - - # adapt the solvation padding based on the system components - if stateA.contains(ProteinComponent) and stateB.contains(ProteinComponent): - protocol_settings.solvation_settings.solvent_padding = 1 * unit.nanometer - - return protocol_settings - - @staticmethod - def _validate_endstates( - stateA: ChemicalSystem, - stateB: ChemicalSystem, - ) -> None: - """ - Validates the end states for the RFE protocol. - - Parameters - ---------- - stateA : ChemicalSystem - The chemical system of end state A. - stateB : ChemicalSystem - The chemical system of end state B. - - Raises - ------ - ValueError - * If either state contains more than one unique Component. - * If unique components are not SmallMoleculeComponents. - """ - # Get the difference in Components between each state - diff = stateA.component_diff(stateB) - - for i, entry in enumerate(diff): - state_label = "A" if i == 0 else "B" - - # Check that there is only one unique Component in each state - if len(entry) != 1: - errmsg = ( - "Only one alchemical component is allowed per end state. " - f"Found {len(entry)} in state {state_label}." - ) - raise ValueError(errmsg) - - # Check that the unique Component is a SmallMoleculeComponent - if not isinstance(entry[0], SmallMoleculeComponent): - errmsg = ( - f"Alchemical component in state {state_label} is of type " - f"{type(entry[0])}, but only SmallMoleculeComponents " - "transformations are currently supported." - ) - raise ValueError(errmsg) - - @staticmethod - def _validate_mapping( - mapping: Optional[Union[ComponentMapping, list[ComponentMapping]]], - alchemical_components: dict[str, list[Component]], - ) -> None: - """ - Validates that the provided mapping(s) are suitable for the RFE protocol. - - Parameters - ---------- - mapping : Optional[Union[ComponentMapping, list[ComponentMapping]]] - all mappings between transforming components. - alchemical_components : dict[str, list[Component]] - Dictionary contatining the alchemical components for - states A and B. - - Raises - ------ - ValueError - * If there are more than one mapping or mapping is None - * If the mapping components are not in the alchemical components. - UserWarning - * Mappings which involve element changes in core atoms - """ - # if a single mapping is provided, convert to list - if isinstance(mapping, ComponentMapping): - mapping = [mapping] - - # For now we only support a single mapping - if mapping is None or len(mapping) > 1: - errmsg = "A single LigandAtomMapping is expected for this Protocol" - raise ValueError(errmsg) - - # check that the mapping components are in the alchemical components - for m in mapping: - for state in ["A", "B"]: - comp = getattr(m, f"component{state}") - if comp not in alchemical_components[f"state{state}"]: - raise ValueError( - f"Mapping component{state} {comp} not " - f"in alchemical components of state{state}" - ) - - # TODO: remove - this is now the default behaviour? - # Check for element changes in mappings - for m in mapping: - molA = m.componentA.to_rdkit() - molB = m.componentB.to_rdkit() - for i, j in m.componentA_to_componentB.items(): - atomA = molA.GetAtomWithIdx(i) - atomB = molB.GetAtomWithIdx(j) - if atomA.GetAtomicNum() != atomB.GetAtomicNum(): - wmsg = ( - f"Element change in mapping between atoms " - f"Ligand A: {i} (element {atomA.GetAtomicNum()}) and " - f"Ligand B: {j} (element {atomB.GetAtomicNum()})\n" - "No mass scaling is attempted in the hybrid topology, " - "the average mass of the two atoms will be used in the " - "simulation" - ) - logger.warning(wmsg) - warnings.warn(wmsg) - - @staticmethod - def _validate_smcs( - stateA: ChemicalSystem, - stateB: ChemicalSystem, - ) -> None: - """ - Validates the SmallMoleculeComponents. - - Parameters - ---------- - stateA : ChemicalSystem - The chemical system of end state A. - stateB : ChemicalSystem - The chemical system of end state B. - - Raises - ------ - ValueError - * If there are isomorphic SmallMoleculeComponents with - different charges. - """ - smcs_A = stateA.get_components_of_type(SmallMoleculeComponent) - smcs_B = stateB.get_components_of_type(SmallMoleculeComponent) - smcs_all = list(set(smcs_A).union(set(smcs_B))) - offmols = [m.to_openff() for m in smcs_all] - - def _equal_charges(moli, molj): - # Base case, both molecules don't have charges - if (moli.partial_charges is None) & (molj.partial_charges is None): - return True - # If either is None but not the other - if (moli.partial_charges is None) ^ (molj.partial_charges is None): - return False - # Check if the charges are close to each other - return np.allclose(moli.partial_charges, molj.partial_charges) - - clashes = [] - - for i, moli in enumerate(offmols): - for molj in offmols: - if moli.is_isomorphic_with(molj): - if not _equal_charges(moli, molj): - clashes.append(smcs_all[i]) - - if len(clashes) > 0: - errmsg = ( - "Found SmallMoleculeComponents that are isomorphic " - "but with different charges, this is not currently allowed. " - f"Affected components: {clashes}" - ) - raise ValueError(errmsg) - - @staticmethod - def _validate_charge_difference( - mapping: LigandAtomMapping, - nonbonded_method: str, - explicit_charge_correction: bool, - solvent_component: SolventComponent | None, - ): - """ - Validates the net charge difference between the two states. - - Parameters - ---------- - mapping : dict[str, ComponentMapping] - Dictionary of mappings between transforming components. - nonbonded_method : str - The OpenMM nonbonded method used for the simulation. - explicit_charge_correction : bool - Whether or not to use an explicit charge correction. - solvent_component : openfe.SolventComponent | None - The SolventComponent of the simulation. - - Raises - ------ - ValueError - * If an explicit charge correction is attempted and the - nonbonded method is not PME. - * If the absolute charge difference is greater than one - and an explicit charge correction is attempted. - * If an explicit charge correction is attempted and there is no solvent present. - UserWarning - * If there is any charge difference. - """ - difference = mapping.get_alchemical_charge_difference() - - if abs(difference) == 0: - return - - if not explicit_charge_correction: - wmsg = ( - f"A charge difference of {difference} is observed " - "between the end states. No charge correction has " - "been requested, please account for this in your " - "final results." - ) - logger.warning(wmsg) - warnings.warn(wmsg) - return - - if solvent_component is None: - errmsg = "Cannot use explicit charge correction without solvent" - raise ValueError(errmsg) - - # We implicitly check earlier that we have to have pme for a solvated - # system, so we only need to check the nonbonded method here - if nonbonded_method.lower() != "pme": - errmsg = "Explicit charge correction when not using PME is not currently supported." - raise ValueError(errmsg) - - if abs(difference) > 1: - errmsg = ( - f"A charge difference of {difference} is observed " - "between the end states and an explicit charge " - "correction has been requested. Unfortunately " - "only absolute differences of 1 are supported." - ) - raise ValueError(errmsg) - - ion = {-1: solvent_component.positive_ion, 1: solvent_component.negative_ion}[difference] - - wmsg = ( - f"A charge difference of {difference} is observed " - "between the end states. This will be addressed by " - f"transforming a water into a {ion} ion" - ) - logger.info(wmsg) - - @staticmethod - def _validate_simulation_settings( - simulation_settings: MultiStateSimulationSettings, - integrator_settings: IntegratorSettings, - output_settings: MultiStateOutputSettings, - ): - """ - Validate various simulation settings, including but not limited to - timestep conversions, and output file write frequencies. - - Parameters - ---------- - simulation_settings : MultiStateSimulationSettings - The sampler simulation settings. - integrator_settings : IntegratorSettings - Settings defining the behaviour of the integrator. - output_settings : MultiStateOutputSettings - Settings defining the simulation file writing behaviour. - - Raises - ------ - ValueError - * If any of of the simulation control settings (e.g. - ``equilibration_length`` or ``production_length``) - are not divisible by the timestep or the number of - steps per iteration. - * If the output frequency for position, velocity, or - online analysis are not divisible by the time per - multistate iteration. - """ - - steps_per_iteration = settings_validation.convert_steps_per_iteration( - simulation_settings=simulation_settings, - integrator_settings=integrator_settings, - ) - - _ = settings_validation.get_simsteps( - sim_length=simulation_settings.equilibration_length, - timestep=integrator_settings.timestep, - mc_steps=steps_per_iteration, - ) - - _ = settings_validation.get_simsteps( - sim_length=simulation_settings.production_length, - timestep=integrator_settings.timestep, - mc_steps=steps_per_iteration, - ) - - _ = settings_validation.convert_checkpoint_interval_to_iterations( - checkpoint_interval=output_settings.checkpoint_interval, - time_per_iteration=simulation_settings.time_per_iteration, - ) - - if output_settings.positions_write_frequency is not None: - _ = settings_validation.divmod_time_and_check( - numerator=output_settings.positions_write_frequency, - denominator=simulation_settings.time_per_iteration, - numerator_name="output settings' positions_write_frequency", - denominator_name="sampler settings' time_per_iteration", - ) - - if output_settings.velocities_write_frequency is not None: - _ = settings_validation.divmod_time_and_check( - numerator=output_settings.velocities_write_frequency, - denominator=simulation_settings.time_per_iteration, - numerator_name="output settings' velocities_write_frequency", - denominator_name="sampler settings' time_per_iteration", - ) - - _, _ = settings_validation.convert_real_time_analysis_iterations( - simulation_settings=simulation_settings, - ) - - def _validate( - self, - stateA: ChemicalSystem, - stateB: ChemicalSystem, - mapping: gufe.ComponentMapping | list[gufe.ComponentMapping] | None, - extends: gufe.ProtocolDAGResult | None = None, - ) -> None: - # Check we're not trying to extend - if extends: - # This technically should be NotImplementedError - # but gufe.Protocol.validate calls `_validate` wrapped around an - # except for NotImplementedError, so we can't raise it here - raise ValueError("Can't extend simulations yet") - - # Validate the end states - self._validate_endstates(stateA, stateB) - - # Validate the mapping - alchem_comps = system_validation.get_alchemical_components(stateA, stateB) - self._validate_mapping(mapping, alchem_comps) - - # Validate the small molecule components - self._validate_smcs(stateA, stateB) - - # Validate solvent component - nonbond = self.settings.forcefield_settings.nonbonded_method - system_validation.validate_solvent(stateA, nonbond) - - # Validate solvation settings - settings_validation.validate_openmm_solvation_settings(self.settings.solvation_settings) - - # Validate protein component - system_validation.validate_protein(stateA) - - # Validate charge difference - # Note: validation depends on the mapping & solvent component checks - if stateA.contains(SolventComponent): - solv_comp = stateA.get_components_of_type(SolventComponent)[0] - else: - solv_comp = None - - self._validate_charge_difference( - mapping=mapping[0] if isinstance(mapping, list) else mapping, - nonbonded_method=self.settings.forcefield_settings.nonbonded_method, - explicit_charge_correction=self.settings.alchemical_settings.explicit_charge_correction, - solvent_component=solv_comp, - ) - - # Validate integrator things - settings_validation.validate_timestep( - self.settings.forcefield_settings.hydrogen_mass, - self.settings.integrator_settings.timestep, - ) - - # Validate simulation & output settings - self._validate_simulation_settings( - self.settings.simulation_settings, - self.settings.integrator_settings, - self.settings.output_settings, - ) - - # Validate alchemical settings - # PR #125 temporarily pin lambda schedule spacing to n_replicas - if ( - self.settings.simulation_settings.n_replicas - != self.settings.lambda_settings.lambda_windows - ): - errmsg = ( - "Number of replicas in ``simulation_settings``: " - f"{self.settings.simulation_settings.n_replicas} must equal " - "the number of lambda windows in lambda_settings: " - f"{self.settings.lambda_settings.lambda_windows}." - ) - raise ValueError(errmsg) - - def _create( - self, - stateA: ChemicalSystem, - stateB: ChemicalSystem, - mapping: Optional[Union[gufe.ComponentMapping, list[gufe.ComponentMapping]]], - extends: Optional[gufe.ProtocolDAGResult] = None, - ) -> list[gufe.ProtocolUnit]: - # validate inputs - self.validate(stateA=stateA, stateB=stateB, mapping=mapping, extends=extends) - - # get alchemical components and mapping - alchem_comps = system_validation.get_alchemical_components(stateA, stateB) - ligandmapping = mapping[0] if isinstance(mapping, list) else mapping - - # actually create and return Units - Anames = ",".join(c.name for c in alchem_comps["stateA"]) - Bnames = ",".join(c.name for c in alchem_comps["stateB"]) - - # our DAG has no dependencies, so just list units - n_repeats = self.settings.protocol_repeats - - units = [ - RelativeHybridTopologyProtocolUnit( - protocol=self, - stateA=stateA, - stateB=stateB, - ligandmapping=ligandmapping, - generation=0, - repeat_id=int(uuid.uuid4()), - name=f"{Anames} to {Bnames} repeat {i} generation 0", - ) - for i in range(n_repeats) - ] - - return units - - def _gather(self, protocol_dag_results: Iterable[gufe.ProtocolDAGResult]) -> dict[str, Any]: - # result units will have a repeat_id and generations within this repeat_id - # first group according to repeat_id - unsorted_repeats = defaultdict(list) - for d in protocol_dag_results: - pu: gufe.ProtocolUnitResult - for pu in d.protocol_unit_results: - if not pu.ok(): - continue - - unsorted_repeats[pu.outputs["repeat_id"]].append(pu) - - # then sort by generation within each repeat_id list - repeats: dict[str, list[gufe.ProtocolUnitResult]] = {} - for k, v in unsorted_repeats.items(): - repeats[str(k)] = sorted(v, key=lambda x: x.outputs["generation"]) - - # returns a dict of repeat_id: sorted list of ProtocolUnitResult - return repeats - - -class RelativeHybridTopologyProtocolUnit(gufe.ProtocolUnit): - """ - Calculates the relative free energy of an alchemical ligand transformation. - """ - - def __init__( - self, - *, - protocol: RelativeHybridTopologyProtocol, - stateA: ChemicalSystem, - stateB: ChemicalSystem, - ligandmapping: LigandAtomMapping, - generation: int, - repeat_id: int, - name: Optional[str] = None, - ): - """ - Parameters - ---------- - protocol : RelativeHybridTopologyProtocol - protocol used to create this Unit. Contains key information such - as the settings. - stateA, stateB : ChemicalSystem - the two ligand SmallMoleculeComponents to transform between. The - transformation will go from ligandA to ligandB. - ligandmapping : LigandAtomMapping - the mapping of atoms between the two ligand components - repeat_id : int - identifier for which repeat (aka replica/clone) this Unit is - generation : int - counter for how many times this repeat has been extended - name : str, optional - human-readable identifier for this Unit - - Notes - ----- - The mapping used must not involve any elemental changes. A check for - this is done on class creation. - """ - super().__init__( - name=name, - protocol=protocol, - stateA=stateA, - stateB=stateB, - ligandmapping=ligandmapping, - repeat_id=repeat_id, - generation=generation, - ) - - @staticmethod - def _assign_partial_charges( - charge_settings: OpenFFPartialChargeSettings, - off_small_mols: dict[str, list[tuple[SmallMoleculeComponent, OFFMolecule]]], - ) -> None: - """ - Assign partial charges to SMCs. - - Parameters - ---------- - charge_settings : OpenFFPartialChargeSettings - Settings for controlling how the partial charges are assigned. - off_small_mols : dict[str, list[tuple[SmallMoleculeComponent, OFFMolecule]]] - Dictionary of dictionary of OpenFF Molecules to add, keyed by - state and SmallMoleculeComponent. - """ - for smc, mol in chain( - off_small_mols["stateA"], off_small_mols["stateB"], off_small_mols["both"] - ): - charge_generation.assign_offmol_partial_charges( - offmol=mol, - overwrite=False, - method=charge_settings.partial_charge_method, - toolkit_backend=charge_settings.off_toolkit_backend, - generate_n_conformers=charge_settings.number_of_conformers, - nagl_model=charge_settings.nagl_model, - ) - - def run( - self, *, dry=False, verbose=True, scratch_basepath=None, shared_basepath=None - ) -> dict[str, Any]: - """Run the relative free energy calculation. - - Parameters - ---------- - dry : bool - Do a dry run of the calculation, creating all necessary hybrid - system components (topology, system, sampler, etc...) but without - running the simulation. - verbose : bool - Verbose output of the simulation progress. Output is provided via - INFO level logging. - scratch_basepath: Pathlike, optional - Where to store temporary files, defaults to current working directory - shared_basepath : Pathlike, optional - Where to run the calculation, defaults to current working directory - - Returns - ------- - dict - Outputs created in the basepath directory or the debug objects - (i.e. sampler) if ``dry==True``. - - Raises - ------ - error - Exception if anything failed - """ - if verbose: - self.logger.info("Preparing the hybrid topology simulation") - if scratch_basepath is None: - scratch_basepath = pathlib.Path(".") - if shared_basepath is None: - # use cwd - shared_basepath = pathlib.Path(".") - - # 0. General setup and settings dependency resolution step - - # Extract relevant settings - protocol_settings: RelativeHybridTopologyProtocolSettings = self._inputs[ - "protocol" - ].settings - stateA = self._inputs["stateA"] - stateB = self._inputs["stateB"] - mapping = self._inputs["ligandmapping"] - - forcefield_settings: settings.OpenMMSystemGeneratorFFSettings = ( - protocol_settings.forcefield_settings - ) - thermo_settings: settings.ThermoSettings = protocol_settings.thermo_settings - alchem_settings: AlchemicalSettings = protocol_settings.alchemical_settings - lambda_settings: LambdaSettings = protocol_settings.lambda_settings - charge_settings: BasePartialChargeSettings = protocol_settings.partial_charge_settings - solvation_settings: OpenMMSolvationSettings = protocol_settings.solvation_settings - sampler_settings: MultiStateSimulationSettings = protocol_settings.simulation_settings - output_settings: MultiStateOutputSettings = protocol_settings.output_settings - integrator_settings: IntegratorSettings = protocol_settings.integrator_settings - - # TODO: Also validate various conversions? - # Convert various time based inputs to steps/iterations - steps_per_iteration = settings_validation.convert_steps_per_iteration( - simulation_settings=sampler_settings, - integrator_settings=integrator_settings, - ) - - equil_steps = settings_validation.get_simsteps( - sim_length=sampler_settings.equilibration_length, - timestep=integrator_settings.timestep, - mc_steps=steps_per_iteration, - ) - prod_steps = settings_validation.get_simsteps( - sim_length=sampler_settings.production_length, - timestep=integrator_settings.timestep, - mc_steps=steps_per_iteration, - ) - - solvent_comp, protein_comp, small_mols = system_validation.get_components(stateA) - - # Get the change difference between the end states - # and check if the charge correction used is appropriate - charge_difference = mapping.get_alchemical_charge_difference() - - # 1. Create stateA system - self.logger.info("Parameterizing molecules") - - # a. create offmol dictionaries and assign partial charges - # workaround for conformer generation failures - # see openfe issue #576 - # calculate partial charges manually if not already given - # convert to OpenFF here, - # and keep the molecule around to maintain the partial charges - off_small_mols: dict[str, list[tuple[SmallMoleculeComponent, OFFMolecule]]] - off_small_mols = { - "stateA": [(mapping.componentA, mapping.componentA.to_openff())], - "stateB": [(mapping.componentB, mapping.componentB.to_openff())], - "both": [ - (m, m.to_openff()) - for m in small_mols - if (m != mapping.componentA and m != mapping.componentB) - ], - } - - self._assign_partial_charges(charge_settings, off_small_mols) - - # b. get a system generator - if output_settings.forcefield_cache is not None: - ffcache = shared_basepath / output_settings.forcefield_cache - else: - ffcache = None - - # Block out oechem backend in system_generator calls to avoid - # any issues with smiles roundtripping between rdkit and oechem - with without_oechem_backend(): - system_generator = system_creation.get_system_generator( - forcefield_settings=forcefield_settings, - integrator_settings=integrator_settings, - thermo_settings=thermo_settings, - cache=ffcache, - has_solvent=solvent_comp is not None, - ) - - # c. force the creation of parameters - # This is necessary because we need to have the FF templates - # registered ahead of solvating the system. - for smc, mol in chain( - off_small_mols["stateA"], off_small_mols["stateB"], off_small_mols["both"] - ): - system_generator.create_system(mol.to_topology().to_openmm(), molecules=[mol]) - - # c. get OpenMM Modeller + a dictionary of resids for each component - stateA_modeller, comp_resids = system_creation.get_omm_modeller( - protein_comp=protein_comp, - solvent_comp=solvent_comp, - small_mols=dict(chain(off_small_mols["stateA"], off_small_mols["both"])), - omm_forcefield=system_generator.forcefield, - solvent_settings=solvation_settings, - ) - - # d. get topology & positions - # Note: roundtrip positions to remove vec3 issues - stateA_topology = stateA_modeller.getTopology() - stateA_positions = to_openmm(from_openmm(stateA_modeller.getPositions())) - - # e. create the stateA System - # Block out oechem backend in system_generator calls to avoid - # any issues with smiles roundtripping between rdkit and oechem - with without_oechem_backend(): - stateA_system = system_generator.create_system( - stateA_modeller.topology, - molecules=[m for _, m in chain(off_small_mols["stateA"], off_small_mols["both"])], - ) - - # 2. Get stateB system - # a. get the topology - stateB_topology, stateB_alchem_resids = _rfe_utils.topologyhelpers.combined_topology( - stateA_topology, - # zeroth item (there's only one) then get the OFF representation - off_small_mols["stateB"][0][1].to_topology().to_openmm(), - exclude_resids=comp_resids[mapping.componentA], - ) - - # b. get a list of small molecules for stateB - # Block out oechem backend in system_generator calls to avoid - # any issues with smiles roundtripping between rdkit and oechem - with without_oechem_backend(): - stateB_system = system_generator.create_system( - stateB_topology, - molecules=[m for _, m in chain(off_small_mols["stateB"], off_small_mols["both"])], - ) - - # c. Define correspondence mappings between the two systems - ligand_mappings = _rfe_utils.topologyhelpers.get_system_mappings( - mapping.componentA_to_componentB, - stateA_system, - stateA_topology, - comp_resids[mapping.componentA], - stateB_system, - stateB_topology, - stateB_alchem_resids, - # These are non-optional settings for this method - fix_constraints=True, - ) - - # d. if a charge correction is necessary, select alchemical waters - # and transform them - if alchem_settings.explicit_charge_correction: - alchem_water_resids = _rfe_utils.topologyhelpers.get_alchemical_waters( - stateA_topology, - stateA_positions, - charge_difference, - alchem_settings.explicit_charge_correction_cutoff, - ) - _rfe_utils.topologyhelpers.handle_alchemical_waters( - alchem_water_resids, - stateB_topology, - stateB_system, - ligand_mappings, - charge_difference, - solvent_comp, - ) - - # e. Finally get the positions - stateB_positions = _rfe_utils.topologyhelpers.set_and_check_new_positions( - ligand_mappings, - stateA_topology, - stateB_topology, - old_positions=ensure_quantity(stateA_positions, "openmm"), - insert_positions=ensure_quantity( - off_small_mols["stateB"][0][1].conformers[0], "openmm" - ), - ) - - # 3. Create the hybrid topology - # a. Get softcore potential settings - if alchem_settings.softcore_LJ.lower() == "gapsys": - softcore_LJ_v2 = True - elif alchem_settings.softcore_LJ.lower() == "beutler": - softcore_LJ_v2 = False - # b. Get hybrid topology factory - hybrid_factory = _rfe_utils.relative.HybridTopologyFactory( - stateA_system, - stateA_positions, - stateA_topology, - stateB_system, - stateB_positions, - stateB_topology, - old_to_new_atom_map=ligand_mappings["old_to_new_atom_map"], - old_to_new_core_atom_map=ligand_mappings["old_to_new_core_atom_map"], - use_dispersion_correction=alchem_settings.use_dispersion_correction, - softcore_alpha=alchem_settings.softcore_alpha, - softcore_LJ_v2=softcore_LJ_v2, - softcore_LJ_v2_alpha=alchem_settings.softcore_alpha, - interpolate_old_and_new_14s=alchem_settings.turn_off_core_unique_exceptions, - ) - - # 4. Create lambda schedule - # TODO - this should be exposed to users, maybe we should offer the - # ability to print the schedule directly in settings? - # fmt: off - lambdas = _rfe_utils.lambdaprotocol.LambdaProtocol( - functions=lambda_settings.lambda_functions, - windows=lambda_settings.lambda_windows - ) - # fmt: on - # PR #125 temporarily pin lambda schedule spacing to n_replicas - n_replicas = sampler_settings.n_replicas - if n_replicas != len(lambdas.lambda_schedule): - errmsg = ( - f"Number of replicas {n_replicas} " - f"does not equal the number of lambda windows " - f"{len(lambdas.lambda_schedule)}" - ) - raise ValueError(errmsg) - - # 9. Create the multistate reporter - # Get the sub selection of the system to print coords for - selection_indices = hybrid_factory.hybrid_topology.select(output_settings.output_indices) - - # a. Create the multistate reporter - # convert checkpoint_interval from time to iterations - chk_intervals = settings_validation.convert_checkpoint_interval_to_iterations( - checkpoint_interval=output_settings.checkpoint_interval, - time_per_iteration=sampler_settings.time_per_iteration, - ) - - nc = shared_basepath / output_settings.output_filename - chk = output_settings.checkpoint_storage_filename - - if output_settings.positions_write_frequency is not None: - pos_interval = settings_validation.divmod_time_and_check( - numerator=output_settings.positions_write_frequency, - denominator=sampler_settings.time_per_iteration, - numerator_name="output settings' position_write_frequency", - denominator_name="sampler settings' time_per_iteration", - ) - else: - pos_interval = 0 - - if output_settings.velocities_write_frequency is not None: - vel_interval = settings_validation.divmod_time_and_check( - numerator=output_settings.velocities_write_frequency, - denominator=sampler_settings.time_per_iteration, - numerator_name="output settings' velocity_write_frequency", - denominator_name="sampler settings' time_per_iteration", - ) - else: - vel_interval = 0 - - reporter = multistate.MultiStateReporter( - storage=nc, - analysis_particle_indices=selection_indices, - checkpoint_interval=chk_intervals, - checkpoint_storage=chk, - position_interval=pos_interval, - velocity_interval=vel_interval, - ) - - # b. Write out a PDB containing the subsampled hybrid state - # fmt: off - bfactors = np.zeros_like(selection_indices, dtype=float) # solvent - bfactors[np.in1d(selection_indices, list(hybrid_factory._atom_classes['unique_old_atoms']))] = 0.25 # lig A - bfactors[np.in1d(selection_indices, list(hybrid_factory._atom_classes['core_atoms']))] = 0.50 # core - bfactors[np.in1d(selection_indices, list(hybrid_factory._atom_classes['unique_new_atoms']))] = 0.75 # lig B - # bfactors[np.in1d(selection_indices, protein)] = 1.0 # prot+cofactor - if len(selection_indices) > 0: - traj = mdtraj.Trajectory( - hybrid_factory.hybrid_positions[selection_indices, :], - hybrid_factory.hybrid_topology.subset(selection_indices), - ).save_pdb( - shared_basepath / output_settings.output_structure, - bfactors=bfactors, - ) - # fmt: on - - # 10. Get compute platform - # restrict to a single CPU if running vacuum - restrict_cpu = forcefield_settings.nonbonded_method.lower() == "nocutoff" - platform = omm_compute.get_openmm_platform( - platform_name=protocol_settings.engine_settings.compute_platform, - gpu_device_index=protocol_settings.engine_settings.gpu_device_index, - restrict_cpu_count=restrict_cpu, - ) - - # 11. Set the integrator - # a. Validate integrator settings for current system - # Virtual sites sanity check - ensure we restart velocities when - # there are virtual sites in the system - if hybrid_factory.has_virtual_sites: - if not integrator_settings.reassign_velocities: - errmsg = ( - "Simulations with virtual sites without velocity " - "reassignments are unstable in openmmtools" - ) - raise ValueError(errmsg) - - # b. create langevin integrator - integrator = openmmtools.mcmc.LangevinDynamicsMove( - timestep=to_openmm(integrator_settings.timestep), - collision_rate=to_openmm(integrator_settings.langevin_collision_rate), - n_steps=steps_per_iteration, - reassign_velocities=integrator_settings.reassign_velocities, - n_restart_attempts=integrator_settings.n_restart_attempts, - constraint_tolerance=integrator_settings.constraint_tolerance, - ) - - # 12. Create sampler - self.logger.info("Creating and setting up the sampler") - rta_its, rta_min_its = settings_validation.convert_real_time_analysis_iterations( - simulation_settings=sampler_settings, - ) - # convert early_termination_target_error from kcal/mol to kT - early_termination_target_error = ( - settings_validation.convert_target_error_from_kcal_per_mole_to_kT( - thermo_settings.temperature, - sampler_settings.early_termination_target_error, - ) - ) - - if sampler_settings.sampler_method.lower() == "repex": - sampler = _rfe_utils.multistate.HybridRepexSampler( - mcmc_moves=integrator, - hybrid_system=hybrid_factory.hybrid_system, - hybrid_positions=hybrid_factory.hybrid_positions, - online_analysis_interval=rta_its, - online_analysis_target_error=early_termination_target_error, - online_analysis_minimum_iterations=rta_min_its, - ) - elif sampler_settings.sampler_method.lower() == "sams": - sampler = _rfe_utils.multistate.HybridSAMSSampler( - mcmc_moves=integrator, - hybrid_system=hybrid_factory.hybrid_system, - hybrid_positions=hybrid_factory.hybrid_positions, - online_analysis_interval=rta_its, - online_analysis_minimum_iterations=rta_min_its, - flatness_criteria=sampler_settings.sams_flatness_criteria, - gamma0=sampler_settings.sams_gamma0, - ) - elif sampler_settings.sampler_method.lower() == "independent": - sampler = _rfe_utils.multistate.HybridMultiStateSampler( - mcmc_moves=integrator, - hybrid_system=hybrid_factory.hybrid_system, - hybrid_positions=hybrid_factory.hybrid_positions, - online_analysis_interval=rta_its, - online_analysis_target_error=early_termination_target_error, - online_analysis_minimum_iterations=rta_min_its, - ) - else: - raise AttributeError(f"Unknown sampler {sampler_settings.sampler_method}") - - sampler.setup( - n_replicas=sampler_settings.n_replicas, - reporter=reporter, - lambda_protocol=lambdas, - temperature=to_openmm(thermo_settings.temperature), - endstates=alchem_settings.endstate_dispersion_correction, - minimization_platform=platform.getName(), - # Set minimization steps to None when running in dry mode - # otherwise do a very small one to avoid NaNs - minimization_steps=100 if not dry else None, - ) - - try: - # Create context caches (energy + sampler) - energy_context_cache = openmmtools.cache.ContextCache( - capacity=None, - time_to_live=None, - platform=platform, - ) - - sampler_context_cache = openmmtools.cache.ContextCache( - capacity=None, - time_to_live=None, - platform=platform, - ) - - sampler.energy_context_cache = energy_context_cache - sampler.sampler_context_cache = sampler_context_cache - - if not dry: # pragma: no-cover - # minimize - if verbose: - self.logger.info("Running minimization") - - sampler.minimize(max_iterations=sampler_settings.minimization_steps) - - # equilibrate - if verbose: - self.logger.info("Running equilibration phase") - - sampler.equilibrate(int(equil_steps / steps_per_iteration)) - - # production - if verbose: - self.logger.info("Running production phase") - - sampler.extend(int(prod_steps / steps_per_iteration)) - - self.logger.info("Production phase complete") - - self.logger.info("Post-simulation analysis of results") - # calculate relevant analyses of the free energies & sampling - # First close & reload the reporter to avoid netcdf clashes - analyzer = multistate_analysis.MultistateEquilFEAnalysis( - reporter, - sampling_method=sampler_settings.sampler_method.lower(), - result_units=unit.kilocalorie_per_mole, - ) - analyzer.plot(filepath=shared_basepath, filename_prefix="") - analyzer.close() - - else: - # clean up the reporter file - fns = [ - shared_basepath / output_settings.output_filename, - shared_basepath / output_settings.checkpoint_storage_filename, - ] - for fn in fns: - os.remove(fn) - finally: - # close reporter when you're done, prevent - # file handle clashes - reporter.close() - - # clear GPU contexts - # TODO: use cache.empty() calls when openmmtools #690 is resolved - # replace with above - for context in list(energy_context_cache._lru._data.keys()): - del energy_context_cache._lru._data[context] - for context in list(sampler_context_cache._lru._data.keys()): - del sampler_context_cache._lru._data[context] - # cautiously clear out the global context cache too - for context in list(openmmtools.cache.global_context_cache._lru._data.keys()): - del openmmtools.cache.global_context_cache._lru._data[context] - - del sampler_context_cache, energy_context_cache - - if not dry: - del integrator, sampler - - if not dry: # pragma: no-cover - return {"nc": nc, "last_checkpoint": chk, **analyzer.unit_results_dict} - else: - return {"debug": {"sampler": sampler, "hybrid_factory": hybrid_factory}} - - @staticmethod - def structural_analysis(scratch, shared) -> dict: - # don't put energy analysis in here, it uses the open file reporter - # whereas structural stuff requires that the file handle is closed - # TODO: we should just make openfe_analysis write an npz instead! - analysis_out = scratch / "structural_analysis.json" - - ret = subprocess.run( - [ - "openfe_analysis", # CLI entry point - "RFE_analysis", # CLI option - str(shared), # Where the simulation.nc fille - str(analysis_out), # Where the analysis json file is written - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - if ret.returncode: - return {"structural_analysis_error": ret.stderr} - - with open(analysis_out, "rb") as f: - data = json.load(f) - - savedir = pathlib.Path(shared) - if d := data["protein_2D_RMSD"]: - fig = plotting.plot_2D_rmsd(d) - fig.savefig(savedir / "protein_2D_RMSD.png") - plt.close(fig) - f2 = plotting.plot_ligand_COM_drift(data["time(ps)"], data["ligand_wander"]) - f2.savefig(savedir / "ligand_COM_drift.png") - plt.close(f2) - - f3 = plotting.plot_ligand_RMSD(data["time(ps)"], data["ligand_RMSD"]) - f3.savefig(savedir / "ligand_RMSD.png") - plt.close(f3) - - # Save to numpy compressed format (~ 6x more space efficient than JSON) - np.savez_compressed( - shared / "structural_analysis.npz", - protein_RMSD=np.asarray(data["protein_RMSD"], dtype=np.float32), - ligand_RMSD=np.asarray(data["ligand_RMSD"], dtype=np.float32), - ligand_COM_drift=np.asarray(data["ligand_wander"], dtype=np.float32), - protein_2D_RMSD=np.asarray(data["protein_2D_RMSD"], dtype=np.float32), - time_ps=np.asarray(data["time(ps)"], dtype=np.float32), - ) - - return {"structural_analysis": shared / "structural_analysis.npz"} - - def _execute( - self, - ctx: gufe.Context, - **kwargs, - ) -> dict[str, Any]: - log_system_probe(logging.INFO, paths=[ctx.scratch]) - - outputs = self.run(scratch_basepath=ctx.scratch, shared_basepath=ctx.shared) - - structural_analysis_outputs = self.structural_analysis(ctx.scratch, ctx.shared) - - return { - "repeat_id": self._inputs["repeat_id"], - "generation": self._inputs["generation"], - **outputs, - **structural_analysis_outputs, - } +from .equil_rfe_settings import RelativeHybridTopologyProtocolSettings +from .hybridtop_protocol_results import RelativeHybridTopologyProtocolResult +from .hybridtop_protocols import RelativeHybridTopologyProtocol +from .hybridtop_units import RelativeHybridTopologyProtocolUnit diff --git a/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py b/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py new file mode 100644 index 000000000..08214a12a --- /dev/null +++ b/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py @@ -0,0 +1,239 @@ +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +""" +ProtocolUnitResults for Hybrid Topology methods using +OpenMM and OpenMMTools in a Perses-like manner. +""" + +import logging +import pathlib +import warnings +from typing import Optional, Union + +import gufe +import numpy as np +import numpy.typing as npt +from openff.units import Quantity +from openmmtools import multistate + +logger = logging.getLogger(__name__) + + +class RelativeHybridTopologyProtocolResult(gufe.ProtocolResult): + """Dict-like container for the output of a RelativeHybridTopologyProtocol""" + + def __init__(self, **data): + super().__init__(**data) + # data is mapping of str(repeat_id): list[protocolunitresults] + # TODO: Detect when we have extensions and stitch these together? + if any(len(pur_list) > 2 for pur_list in self.data.values()): + raise NotImplementedError("Can't stitch together results yet") + + @staticmethod + def compute_mean_estimate(dGs: list[Quantity]) -> Quantity: + u = dGs[0].u + # convert all values to units of the first value, then take average of magnitude + # this would avoid a screwy case where each value was in different units + vals = np.asarray([dG.to(u).m for dG in dGs]) + + return np.average(vals) * u + + def get_estimate(self) -> Quantity: + """Average free energy difference of this transformation + + Returns + ------- + dG : openff.units.Quantity + The free energy difference between the first and last states. This is + a Quantity defined with units. + """ + # TODO: Check this holds up completely for SAMS. + dGs = [pus[0].outputs["unit_estimate"] for pus in self.data.values()] + return self.compute_mean_estimate(dGs) + + @staticmethod + def compute_uncertainty(dGs: list[Quantity]) -> Quantity: + u = dGs[0].u + # convert all values to units of the first value, then take average of magnitude + # this would avoid a screwy case where each value was in different units + vals = np.asarray([dG.to(u).m for dG in dGs]) + + return np.std(vals) * u + + def get_uncertainty(self) -> Quantity: + """The uncertainty/error in the dG value: The std of the estimates of + each independent repeat + """ + + dGs = [pus[0].outputs["unit_estimate"] for pus in self.data.values()] + return self.compute_uncertainty(dGs) + + def get_individual_estimates(self) -> list[tuple[Quantity, Quantity]]: + """Return a list of tuples containing the individual free energy + estimates and associated MBAR errors for each repeat. + + Returns + ------- + dGs : list[tuple[openff.units.Quantity]] + n_replicate simulation list of tuples containing the free energy + estimates (first entry) and associated MBAR estimate errors + (second entry). + """ + dGs = [ + (pus[0].outputs["unit_estimate"], pus[0].outputs["unit_estimate_error"]) + for pus in self.data.values() + ] + return dGs + + def get_forward_and_reverse_energy_analysis( + self, + ) -> list[Optional[dict[str, Union[npt.NDArray, Quantity]]]]: + """ + Get a list of forward and reverse analysis of the free energies + for each repeat using uncorrelated production samples. + + The returned dicts have keys: + 'fractions' - the fraction of data used for this estimate + 'forward_DGs', 'reverse_DGs' - for each fraction of data, the estimate + 'forward_dDGs', 'reverse_dDGs' - for each estimate, the uncertainty + + The 'fractions' values are a numpy array, while the other arrays are + Quantity arrays, with units attached. + + If the list entry is ``None`` instead of a dictionary, this indicates + that the analysis could not be carried out for that repeat. This + is most likely caused by MBAR convergence issues when attempting to + calculate free energies from too few samples. + + + Returns + ------- + forward_reverse : list[Optional[dict[str, Union[npt.NDArray, openff.units.Quantity]]]] + + + Raises + ------ + UserWarning + If any of the forward and reverse entries are ``None``. + """ + forward_reverse = [ + pus[0].outputs["forward_and_reverse_energies"] for pus in self.data.values() + ] + + if None in forward_reverse: + wmsg = ( + "One or more ``None`` entries were found in the list of " + "forward and reverse analyses. This is likely caused by " + "an MBAR convergence failure caused by too few independent " + "samples when calculating the free energies of the 10% " + "timeseries slice." + ) + warnings.warn(wmsg) + + return forward_reverse + + def get_overlap_matrices(self) -> list[dict[str, npt.NDArray]]: + """ + Return a list of dictionary containing the MBAR overlap estimates + calculated for each repeat. + + Returns + ------- + overlap_stats : list[dict[str, npt.NDArray]] + A list of dictionaries containing the following keys: + * ``scalar``: One minus the largest nontrivial eigenvalue + * ``eigenvalues``: The sorted (descending) eigenvalues of the + overlap matrix + * ``matrix``: Estimated overlap matrix of observing a sample from + state i in state j + """ + # Loop through and get the repeats and get the matrices + overlap_stats = [pus[0].outputs["unit_mbar_overlap"] for pus in self.data.values()] + + return overlap_stats + + def get_replica_transition_statistics(self) -> list[dict[str, npt.NDArray]]: + """The replica lambda state transition statistics for each repeat. + + Note + ---- + This is currently only available in cases where a replica exchange + simulation was run. + + Returns + ------- + repex_stats : list[dict[str, npt.NDArray]] + A list of dictionaries containing the following: + * ``eigenvalues``: The sorted (descending) eigenvalues of the + lambda state transition matrix + * ``matrix``: The transition matrix estimate of a replica switching + from state i to state j. + """ + try: + repex_stats = [ + pus[0].outputs["replica_exchange_statistics"] for pus in self.data.values() + ] + except KeyError: + errmsg = "Replica exchange statistics were not found, did you run a repex calculation?" + raise ValueError(errmsg) + + return repex_stats + + def get_replica_states(self) -> list[npt.NDArray]: + """ + Returns the timeseries of replica states for each repeat. + + Returns + ------- + replica_states : List[npt.NDArray] + List of replica states for each repeat + """ + + def is_file(filename: str): + p = pathlib.Path(filename) + if not p.exists(): + errmsg = f"File could not be found {p}" + raise ValueError(errmsg) + return p + + replica_states = [] + + for pus in self.data.values(): + nc = is_file(pus[0].outputs["nc"]) + dir_path = nc.parents[0] + chk = is_file(dir_path / pus[0].outputs["last_checkpoint"]).name + reporter = multistate.MultiStateReporter( + storage=nc, checkpoint_storage=chk, open_mode="r" + ) + replica_states.append(np.asarray(reporter.read_replica_thermodynamic_states())) + reporter.close() + + return replica_states + + def equilibration_iterations(self) -> list[float]: + """ + Returns the number of equilibration iterations for each repeat + of the calculation. + + Returns + ------- + equilibration_lengths : list[float] + """ + equilibration_lengths = [ + pus[0].outputs["equilibration_iterations"] for pus in self.data.values() + ] + + return equilibration_lengths + + def production_iterations(self) -> list[float]: + """ + Returns the number of uncorrelated production samples for each + repeat of the calculation. + + Returns + ------- + production_lengths : list[float] + """ + production_lengths = [pus[0].outputs["production_iterations"] for pus in self.data.values()] + + return production_lengths diff --git a/openfe/protocols/openmm_rfe/hybridtop_protocols.py b/openfe/protocols/openmm_rfe/hybridtop_protocols.py new file mode 100644 index 000000000..a78a81099 --- /dev/null +++ b/openfe/protocols/openmm_rfe/hybridtop_protocols.py @@ -0,0 +1,631 @@ +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +""" +Hybrid Topology Protocols using OpenMM and OpenMMTools in a Perses-like manner. + +Acknowledgements +---------------- +These Protocols are based on, and leverages components originating from +the Perses toolkit (https://github.com/choderalab/perses). +""" + +from __future__ import annotations + +import logging +import uuid +import warnings +from collections import defaultdict +from typing import Any, Iterable, Optional, Union + +import gufe +import numpy as np +from gufe import ( + ChemicalSystem, + Component, + ComponentMapping, + LigandAtomMapping, + ProteinComponent, + SmallMoleculeComponent, + SolventComponent, + settings, +) +from openff.units import unit as offunit + +from openfe.due import Doi, due + +from ..openmm_utils import ( + settings_validation, + system_validation, +) +from .equil_rfe_settings import ( + AlchemicalSettings, + IntegratorSettings, + LambdaSettings, + MultiStateOutputSettings, + MultiStateSimulationSettings, + OpenFFPartialChargeSettings, + OpenMMEngineSettings, + OpenMMSolvationSettings, + RelativeHybridTopologyProtocolSettings, +) +from .hybridtop_protocol_results import RelativeHybridTopologyProtocolResult +from .hybridtop_units import RelativeHybridTopologyProtocolUnit + +logger = logging.getLogger(__name__) + + +due.cite( + Doi("10.5281/zenodo.1297683"), + description="Perses", + path="openfe.protocols.openmm_rfe.hybridtop_protocols", + cite_module=True, +) + +due.cite( + Doi("10.5281/zenodo.596622"), + description="OpenMMTools", + path="openfe.protocols.openmm_rfe.hybridtop_protocols", + cite_module=True, +) + +due.cite( + Doi("10.1371/journal.pcbi.1005659"), + description="OpenMM", + path="openfe.protocols.openmm_rfe.hybridtop_protocols", + cite_module=True, +) + + +class RelativeHybridTopologyProtocol(gufe.Protocol): + """ + Relative Free Energy calculations using OpenMM and OpenMMTools. + + Based on `Perses `_ + + See Also + -------- + :mod:`openfe.protocols` + :class:`openfe.protocols.openmm_rfe.RelativeHybridTopologySettings` + :class:`openfe.protocols.openmm_rfe.RelativeHybridTopologyResult` + :class:`openfe.protocols.openmm_rfe.RelativeHybridTopologyProtocolUnit` + """ + + result_cls = RelativeHybridTopologyProtocolResult + _settings_cls = RelativeHybridTopologyProtocolSettings + _settings: RelativeHybridTopologyProtocolSettings + + @classmethod + def _default_settings(cls): + """A dictionary of initial settings for this creating this Protocol + + These settings are intended as a suitable starting point for creating + an instance of this protocol. It is recommended, however that care is + taken to inspect and customize these before performing a Protocol. + + Returns + ------- + Settings + a set of default settings + """ + return RelativeHybridTopologyProtocolSettings( + protocol_repeats=3, + forcefield_settings=settings.OpenMMSystemGeneratorFFSettings(), + thermo_settings=settings.ThermoSettings( + temperature=298.15 * offunit.kelvin, + pressure=1 * offunit.bar, + ), + partial_charge_settings=OpenFFPartialChargeSettings(), + solvation_settings=OpenMMSolvationSettings(), + alchemical_settings=AlchemicalSettings(softcore_LJ="gapsys"), + lambda_settings=LambdaSettings(), + simulation_settings=MultiStateSimulationSettings( + equilibration_length=1.0 * offunit.nanosecond, + production_length=5.0 * offunit.nanosecond, + ), + engine_settings=OpenMMEngineSettings(), + integrator_settings=IntegratorSettings(), + output_settings=MultiStateOutputSettings(), + ) + + @classmethod + def _adaptive_settings( + cls, + stateA: ChemicalSystem, + stateB: ChemicalSystem, + mapping: gufe.LigandAtomMapping | list[gufe.LigandAtomMapping], + initial_settings: None | RelativeHybridTopologyProtocolSettings = None, + ) -> RelativeHybridTopologyProtocolSettings: + """ + Get the recommended OpenFE settings for this protocol based on the input states involved in the + transformation. + + These are intended as a suitable starting point for creating an instance of this protocol, which can be further + customized before performing a Protocol. + + Parameters + ---------- + stateA : ChemicalSystem + The initial state of the transformation. + stateB : ChemicalSystem + The final state of the transformation. + mapping : LigandAtomMapping | list[LigandAtomMapping] + The mapping(s) between transforming components in stateA and stateB. + initial_settings : None | RelativeHybridTopologyProtocolSettings, optional + Initial settings to base the adaptive settings on. If None, default settings are used. + + Returns + ------- + RelativeHybridTopologyProtocolSettings + The recommended settings for this protocol based on the input states. + + Notes + ----- + - If the transformation involves a change in net charge, the settings are adapted to use a more expensive + protocol with 22 lambda windows and 20 ns production length per window. + - If both states contain a ProteinComponent, the solvation padding is set to 1 nm. + - If initial_settings is provided, the adaptive settings are based on a copy of these settings. + """ + # use initial settings or default settings + # this is needed for the CLI so we don't override user settings + if initial_settings is not None: + protocol_settings = initial_settings.copy(deep=True) + else: + protocol_settings = cls.default_settings() + + if isinstance(mapping, list): + mapping = mapping[0] + + if mapping.get_alchemical_charge_difference() != 0: + # apply the recommended charge change settings taken from the industry benchmarking as fast settings not validated + # + info = ( + "Charge changing transformation between ligands " + f"{mapping.componentA.name} and {mapping.componentB.name}. " + "A more expensive protocol with 22 lambda windows, sampled " + "for 20 ns each, will be used here." + ) + logger.info(info) + protocol_settings.alchemical_settings.explicit_charge_correction = True + protocol_settings.simulation_settings.production_length = 20 * offunit.nanosecond + protocol_settings.simulation_settings.n_replicas = 22 + protocol_settings.lambda_settings.lambda_windows = 22 + + # adapt the solvation padding based on the system components + if stateA.contains(ProteinComponent) and stateB.contains(ProteinComponent): + protocol_settings.solvation_settings.solvent_padding = 1 * offunit.nanometer + + return protocol_settings + + @staticmethod + def _validate_endstates( + stateA: ChemicalSystem, + stateB: ChemicalSystem, + ) -> None: + """ + Validates the end states for the RFE protocol. + + Parameters + ---------- + stateA : ChemicalSystem + The chemical system of end state A. + stateB : ChemicalSystem + The chemical system of end state B. + + Raises + ------ + ValueError + * If either state contains more than one unique Component. + * If unique components are not SmallMoleculeComponents. + """ + # Get the difference in Components between each state + diff = stateA.component_diff(stateB) + + for i, entry in enumerate(diff): + state_label = "A" if i == 0 else "B" + + # Check that there is only one unique Component in each state + if len(entry) != 1: + errmsg = ( + "Only one alchemical component is allowed per end state. " + f"Found {len(entry)} in state {state_label}." + ) + raise ValueError(errmsg) + + # Check that the unique Component is a SmallMoleculeComponent + if not isinstance(entry[0], SmallMoleculeComponent): + errmsg = ( + f"Alchemical component in state {state_label} is of type " + f"{type(entry[0])}, but only SmallMoleculeComponents " + "transformations are currently supported." + ) + raise ValueError(errmsg) + + @staticmethod + def _validate_mapping( + mapping: Optional[Union[ComponentMapping, list[ComponentMapping]]], + alchemical_components: dict[str, list[Component]], + ) -> None: + """ + Validates that the provided mapping(s) are suitable for the RFE protocol. + + Parameters + ---------- + mapping : Optional[Union[ComponentMapping, list[ComponentMapping]]] + all mappings between transforming components. + alchemical_components : dict[str, list[Component]] + Dictionary contatining the alchemical components for + states A and B. + + Raises + ------ + ValueError + * If there are more than one mapping or mapping is None + * If the mapping components are not in the alchemical components. + UserWarning + * Mappings which involve element changes in core atoms + """ + # if a single mapping is provided, convert to list + if isinstance(mapping, ComponentMapping): + mapping = [mapping] + + # For now we only support a single mapping + if mapping is None or len(mapping) > 1: + errmsg = "A single LigandAtomMapping is expected for this Protocol" + raise ValueError(errmsg) + + # check that the mapping components are in the alchemical components + for m in mapping: + for state in ["A", "B"]: + comp = getattr(m, f"component{state}") + if comp not in alchemical_components[f"state{state}"]: + raise ValueError( + f"Mapping component{state} {comp} not " + f"in alchemical components of state{state}" + ) + + # TODO: remove - this is now the default behaviour? + # Check for element changes in mappings + for m in mapping: + molA = m.componentA.to_rdkit() + molB = m.componentB.to_rdkit() + for i, j in m.componentA_to_componentB.items(): + atomA = molA.GetAtomWithIdx(i) + atomB = molB.GetAtomWithIdx(j) + if atomA.GetAtomicNum() != atomB.GetAtomicNum(): + wmsg = ( + f"Element change in mapping between atoms " + f"Ligand A: {i} (element {atomA.GetAtomicNum()}) and " + f"Ligand B: {j} (element {atomB.GetAtomicNum()})\n" + "No mass scaling is attempted in the hybrid topology, " + "the average mass of the two atoms will be used in the " + "simulation" + ) + logger.warning(wmsg) + warnings.warn(wmsg) + + @staticmethod + def _validate_smcs( + stateA: ChemicalSystem, + stateB: ChemicalSystem, + ) -> None: + """ + Validates the SmallMoleculeComponents. + + Parameters + ---------- + stateA : ChemicalSystem + The chemical system of end state A. + stateB : ChemicalSystem + The chemical system of end state B. + + Raises + ------ + ValueError + * If there are isomorphic SmallMoleculeComponents with + different charges. + """ + smcs_A = stateA.get_components_of_type(SmallMoleculeComponent) + smcs_B = stateB.get_components_of_type(SmallMoleculeComponent) + smcs_all = list(set(smcs_A).union(set(smcs_B))) + offmols = [m.to_openff() for m in smcs_all] + + def _equal_charges(moli, molj): + # Base case, both molecules don't have charges + if (moli.partial_charges is None) & (molj.partial_charges is None): + return True + # If either is None but not the other + if (moli.partial_charges is None) ^ (molj.partial_charges is None): + return False + # Check if the charges are close to each other + return np.allclose(moli.partial_charges, molj.partial_charges) + + clashes = [] + + for i, moli in enumerate(offmols): + for molj in offmols: + if moli.is_isomorphic_with(molj): + if not _equal_charges(moli, molj): + clashes.append(smcs_all[i]) + + if len(clashes) > 0: + errmsg = ( + "Found SmallMoleculeComponents that are isomorphic " + "but with different charges, this is not currently allowed. " + f"Affected components: {clashes}" + ) + raise ValueError(errmsg) + + @staticmethod + def _validate_charge_difference( + mapping: LigandAtomMapping, + nonbonded_method: str, + explicit_charge_correction: bool, + solvent_component: SolventComponent | None, + ): + """ + Validates the net charge difference between the two states. + + Parameters + ---------- + mapping : dict[str, ComponentMapping] + Dictionary of mappings between transforming components. + nonbonded_method : str + The OpenMM nonbonded method used for the simulation. + explicit_charge_correction : bool + Whether or not to use an explicit charge correction. + solvent_component : openfe.SolventComponent | None + The SolventComponent of the simulation. + + Raises + ------ + ValueError + * If an explicit charge correction is attempted and the + nonbonded method is not PME. + * If the absolute charge difference is greater than one + and an explicit charge correction is attempted. + * If an explicit charge correction is attempted and there is no + solvent present. + UserWarning + * If there is any charge difference. + """ + difference = mapping.get_alchemical_charge_difference() + + if abs(difference) == 0: + return + + if not explicit_charge_correction: + wmsg = ( + f"A charge difference of {difference} is observed " + "between the end states. No charge correction has " + "been requested, please account for this in your " + "final results." + ) + logger.warning(wmsg) + warnings.warn(wmsg) + return + + if solvent_component is None: + errmsg = "Cannot use explicit charge correction without solvent" + raise ValueError(errmsg) + + # We implicitly check earlier that we have to have pme for a solvated + # system, so we only need to check the nonbonded method here + if nonbonded_method.lower() != "pme": + errmsg = "Explicit charge correction when not using PME is not currently supported." + raise ValueError(errmsg) + + if abs(difference) > 1: + errmsg = ( + f"A charge difference of {difference} is observed " + "between the end states and an explicit charge " + "correction has been requested. Unfortunately " + "only absolute differences of 1 are supported." + ) + raise ValueError(errmsg) + + ion = {-1: solvent_component.positive_ion, 1: solvent_component.negative_ion}[difference] + + wmsg = ( + f"A charge difference of {difference} is observed " + "between the end states. This will be addressed by " + f"transforming a water into a {ion} ion" + ) + logger.info(wmsg) + + @staticmethod + def _validate_simulation_settings( + simulation_settings: MultiStateSimulationSettings, + integrator_settings: IntegratorSettings, + output_settings: MultiStateOutputSettings, + ): + """ + Validate various simulation settings, including but not limited to + timestep conversions, and output file write frequencies. + + Parameters + ---------- + simulation_settings : MultiStateSimulationSettings + The sampler simulation settings. + integrator_settings : IntegratorSettings + Settings defining the behaviour of the integrator. + output_settings : MultiStateOutputSettings + Settings defining the simulation file writing behaviour. + + Raises + ------ + ValueError + * If the + """ + + steps_per_iteration = settings_validation.convert_steps_per_iteration( + simulation_settings=simulation_settings, + integrator_settings=integrator_settings, + ) + + _ = settings_validation.get_simsteps( + sim_length=simulation_settings.equilibration_length, + timestep=integrator_settings.timestep, + mc_steps=steps_per_iteration, + ) + + _ = settings_validation.get_simsteps( + sim_length=simulation_settings.production_length, + timestep=integrator_settings.timestep, + mc_steps=steps_per_iteration, + ) + + _ = settings_validation.convert_checkpoint_interval_to_iterations( + checkpoint_interval=output_settings.checkpoint_interval, + time_per_iteration=simulation_settings.time_per_iteration, + ) + + if output_settings.positions_write_frequency is not None: + _ = settings_validation.divmod_time_and_check( + numerator=output_settings.positions_write_frequency, + denominator=simulation_settings.time_per_iteration, + numerator_name="output settings' positions_write_frequency", + denominator_name="sampler settings' time_per_iteration", + ) + + if output_settings.velocities_write_frequency is not None: + _ = settings_validation.divmod_time_and_check( + numerator=output_settings.velocities_write_frequency, + denominator=simulation_settings.time_per_iteration, + numerator_name="output settings' velocities_write_frequency", + denominator_name="sampler settings' time_per_iteration", + ) + + _, _ = settings_validation.convert_real_time_analysis_iterations( + simulation_settings=simulation_settings, + ) + + def _validate( + self, + stateA: ChemicalSystem, + stateB: ChemicalSystem, + mapping: gufe.ComponentMapping | list[gufe.ComponentMapping] | None, + extends: gufe.ProtocolDAGResult | None = None, + ) -> None: + # Check we're not trying to extend + if extends: + # This technically should be NotImplementedError + # but gufe.Protocol.validate calls `_validate` wrapped around an + # except for NotImplementedError, so we can't raise it here + raise ValueError("Can't extend simulations yet") + + # Validate the end states + self._validate_endstates(stateA, stateB) + + # Validate the mapping + alchem_comps = system_validation.get_alchemical_components(stateA, stateB) + self._validate_mapping(mapping, alchem_comps) + + # Validate the small molecule components + self._validate_smcs(stateA, stateB) + + # Validate solvent component + nonbond = self.settings.forcefield_settings.nonbonded_method + system_validation.validate_solvent(stateA, nonbond) + + # Validate solvation settings + settings_validation.validate_openmm_solvation_settings(self.settings.solvation_settings) + + # Validate protein component + system_validation.validate_protein(stateA) + + # Validate charge difference + # Note: validation depends on the mapping & solvent component checks + if stateA.contains(SolventComponent): + solv_comp = stateA.get_components_of_type(SolventComponent)[0] + else: + solv_comp = None + + self._validate_charge_difference( + mapping=mapping[0] if isinstance(mapping, list) else mapping, + nonbonded_method=self.settings.forcefield_settings.nonbonded_method, + explicit_charge_correction=self.settings.alchemical_settings.explicit_charge_correction, + solvent_component=solv_comp, + ) + + # Validate integrator things + settings_validation.validate_timestep( + self.settings.forcefield_settings.hydrogen_mass, + self.settings.integrator_settings.timestep, + ) + + # Validate simulation & output settings + self._validate_simulation_settings( + self.settings.simulation_settings, + self.settings.integrator_settings, + self.settings.output_settings, + ) + + # Validate alchemical settings + # PR #125 temporarily pin lambda schedule spacing to n_replicas + if ( + self.settings.simulation_settings.n_replicas + != self.settings.lambda_settings.lambda_windows + ): + errmsg = ( + "Number of replicas in ``simulation_settings``: " + f"{self.settings.simulation_settings.n_replicas} must equal " + "the number of lambda windows in lambda_settings: " + f"{self.settings.lambda_settings.lambda_windows}." + ) + raise ValueError(errmsg) + + def _create( + self, + stateA: ChemicalSystem, + stateB: ChemicalSystem, + mapping: Optional[Union[gufe.ComponentMapping, list[gufe.ComponentMapping]]], + extends: Optional[gufe.ProtocolDAGResult] = None, + ) -> list[gufe.ProtocolUnit]: + # validate inputs + self.validate(stateA=stateA, stateB=stateB, mapping=mapping, extends=extends) + + # get alchemical components and mapping + alchem_comps = system_validation.get_alchemical_components(stateA, stateB) + ligandmapping = mapping[0] if isinstance(mapping, list) else mapping + + # actually create and return Units + Anames = ",".join(c.name for c in alchem_comps["stateA"]) + Bnames = ",".join(c.name for c in alchem_comps["stateB"]) + + # our DAG has no dependencies, so just list units + n_repeats = self.settings.protocol_repeats + + units = [ + RelativeHybridTopologyProtocolUnit( + protocol=self, + stateA=stateA, + stateB=stateB, + ligandmapping=ligandmapping, + generation=0, + repeat_id=int(uuid.uuid4()), + name=f"{Anames} to {Bnames} repeat {i} generation 0", + ) + for i in range(n_repeats) + ] + + return units + + def _gather(self, protocol_dag_results: Iterable[gufe.ProtocolDAGResult]) -> dict[str, Any]: + # result units will have a repeat_id and generations within this repeat_id + # first group according to repeat_id + unsorted_repeats = defaultdict(list) + for d in protocol_dag_results: + pu: gufe.ProtocolUnitResult + for pu in d.protocol_unit_results: + if not pu.ok(): + continue + + unsorted_repeats[pu.outputs["repeat_id"]].append(pu) + + # then sort by generation within each repeat_id list + repeats: dict[str, list[gufe.ProtocolUnitResult]] = {} + for k, v in unsorted_repeats.items(): + repeats[str(k)] = sorted(v, key=lambda x: x.outputs["generation"]) + + # returns a dict of repeat_id: sorted list of ProtocolUnitResult + return repeats diff --git a/openfe/protocols/openmm_rfe/hybridtop_units.py b/openfe/protocols/openmm_rfe/hybridtop_units.py new file mode 100644 index 000000000..b1ca0b786 --- /dev/null +++ b/openfe/protocols/openmm_rfe/hybridtop_units.py @@ -0,0 +1,693 @@ +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +""" +ProtocolUnits for Hybrid Topology methods using OpenMM and OpenMMTools in a +Perses-like manner. + +Acknowledgements +---------------- +These ProtocolUnits are based on, and leverage components originating from +the Perses toolkit (https://github.com/choderalab/perses). +""" + +import json +import logging +import os +import pathlib +import subprocess +from itertools import chain +from typing import Any, Optional + +import gufe +import matplotlib.pyplot as plt +import mdtraj +import numpy as np +import openmmtools +from gufe import ( + ChemicalSystem, + LigandAtomMapping, + SmallMoleculeComponent, + settings, +) +from openff.toolkit.topology import Molecule as OFFMolecule +from openff.units import unit as offunit +from openff.units.openmm import ensure_quantity, from_openmm, to_openmm +from openmmtools import multistate + +from openfe.protocols.openmm_utils.omm_settings import ( + BasePartialChargeSettings, +) + +from ...analysis import plotting +from ...utils import log_system_probe, without_oechem_backend +from ..openmm_utils import ( + charge_generation, + multistate_analysis, + omm_compute, + settings_validation, + system_creation, + system_validation, +) +from . import _rfe_utils +from .equil_rfe_settings import ( + AlchemicalSettings, + IntegratorSettings, + LambdaSettings, + MultiStateOutputSettings, + MultiStateSimulationSettings, + OpenFFPartialChargeSettings, + OpenMMSolvationSettings, + RelativeHybridTopologyProtocolSettings, +) + +logger = logging.getLogger(__name__) + + +class RelativeHybridTopologyProtocolUnit(gufe.ProtocolUnit): + """ + Calculates the relative free energy of an alchemical ligand transformation. + """ + + def __init__( + self, + *, + protocol: gufe.Protocol, + stateA: ChemicalSystem, + stateB: ChemicalSystem, + ligandmapping: LigandAtomMapping, + generation: int, + repeat_id: int, + name: Optional[str] = None, + ): + """ + Parameters + ---------- + protocol : RelativeHybridTopologyProtocol + protocol used to create this Unit. Contains key information such + as the settings. + stateA, stateB : ChemicalSystem + the two ligand SmallMoleculeComponents to transform between. The + transformation will go from ligandA to ligandB. + ligandmapping : LigandAtomMapping + the mapping of atoms between the two ligand components + repeat_id : int + identifier for which repeat (aka replica/clone) this Unit is + generation : int + counter for how many times this repeat has been extended + name : str, optional + human-readable identifier for this Unit + + Notes + ----- + The mapping used must not involve any elemental changes. A check for + this is done on class creation. + """ + super().__init__( + name=name, + protocol=protocol, + stateA=stateA, + stateB=stateB, + ligandmapping=ligandmapping, + repeat_id=repeat_id, + generation=generation, + ) + + @staticmethod + def _assign_partial_charges( + charge_settings: OpenFFPartialChargeSettings, + off_small_mols: dict[str, list[tuple[SmallMoleculeComponent, OFFMolecule]]], + ) -> None: + """ + Assign partial charges to SMCs. + + Parameters + ---------- + charge_settings : OpenFFPartialChargeSettings + Settings for controlling how the partial charges are assigned. + off_small_mols : dict[str, list[tuple[SmallMoleculeComponent, OFFMolecule]]] + Dictionary of dictionary of OpenFF Molecules to add, keyed by + state and SmallMoleculeComponent. + """ + for smc, mol in chain( + off_small_mols["stateA"], off_small_mols["stateB"], off_small_mols["both"] + ): + charge_generation.assign_offmol_partial_charges( + offmol=mol, + overwrite=False, + method=charge_settings.partial_charge_method, + toolkit_backend=charge_settings.off_toolkit_backend, + generate_n_conformers=charge_settings.number_of_conformers, + nagl_model=charge_settings.nagl_model, + ) + + def run( + self, *, dry=False, verbose=True, scratch_basepath=None, shared_basepath=None + ) -> dict[str, Any]: + """Run the relative free energy calculation. + + Parameters + ---------- + dry : bool + Do a dry run of the calculation, creating all necessary hybrid + system components (topology, system, sampler, etc...) but without + running the simulation. + verbose : bool + Verbose output of the simulation progress. Output is provided via + INFO level logging. + scratch_basepath: Pathlike, optional + Where to store temporary files, defaults to current working directory + shared_basepath : Pathlike, optional + Where to run the calculation, defaults to current working directory + + Returns + ------- + dict + Outputs created in the basepath directory or the debug objects + (i.e. sampler) if ``dry==True``. + + Raises + ------ + error + Exception if anything failed + """ + if verbose: + self.logger.info("Preparing the hybrid topology simulation") + if scratch_basepath is None: + scratch_basepath = pathlib.Path(".") + if shared_basepath is None: + # use cwd + shared_basepath = pathlib.Path(".") + + # 0. General setup and settings dependency resolution step + + # Extract relevant settings + protocol_settings: RelativeHybridTopologyProtocolSettings = self._inputs[ + "protocol" + ].settings + stateA = self._inputs["stateA"] + stateB = self._inputs["stateB"] + mapping = self._inputs["ligandmapping"] + + forcefield_settings: settings.OpenMMSystemGeneratorFFSettings = ( + protocol_settings.forcefield_settings + ) + thermo_settings: settings.ThermoSettings = protocol_settings.thermo_settings + alchem_settings: AlchemicalSettings = protocol_settings.alchemical_settings + lambda_settings: LambdaSettings = protocol_settings.lambda_settings + charge_settings: BasePartialChargeSettings = protocol_settings.partial_charge_settings + solvation_settings: OpenMMSolvationSettings = protocol_settings.solvation_settings + sampler_settings: MultiStateSimulationSettings = protocol_settings.simulation_settings + output_settings: MultiStateOutputSettings = protocol_settings.output_settings + integrator_settings: IntegratorSettings = protocol_settings.integrator_settings + + # TODO: Also validate various conversions? + # Convert various time based inputs to steps/iterations + steps_per_iteration = settings_validation.convert_steps_per_iteration( + simulation_settings=sampler_settings, + integrator_settings=integrator_settings, + ) + + equil_steps = settings_validation.get_simsteps( + sim_length=sampler_settings.equilibration_length, + timestep=integrator_settings.timestep, + mc_steps=steps_per_iteration, + ) + prod_steps = settings_validation.get_simsteps( + sim_length=sampler_settings.production_length, + timestep=integrator_settings.timestep, + mc_steps=steps_per_iteration, + ) + + solvent_comp, protein_comp, small_mols = system_validation.get_components(stateA) + + # Get the change difference between the end states + # and check if the charge correction used is appropriate + charge_difference = mapping.get_alchemical_charge_difference() + + # 1. Create stateA system + self.logger.info("Parameterizing molecules") + + # a. create offmol dictionaries and assign partial charges + # workaround for conformer generation failures + # see openfe issue #576 + # calculate partial charges manually if not already given + # convert to OpenFF here, + # and keep the molecule around to maintain the partial charges + off_small_mols: dict[str, list[tuple[SmallMoleculeComponent, OFFMolecule]]] + off_small_mols = { + "stateA": [(mapping.componentA, mapping.componentA.to_openff())], + "stateB": [(mapping.componentB, mapping.componentB.to_openff())], + "both": [ + (m, m.to_openff()) + for m in small_mols + if (m != mapping.componentA and m != mapping.componentB) + ], + } + + self._assign_partial_charges(charge_settings, off_small_mols) + + # b. get a system generator + if output_settings.forcefield_cache is not None: + ffcache = shared_basepath / output_settings.forcefield_cache + else: + ffcache = None + + # Block out oechem backend in system_generator calls to avoid + # any issues with smiles roundtripping between rdkit and oechem + with without_oechem_backend(): + system_generator = system_creation.get_system_generator( + forcefield_settings=forcefield_settings, + integrator_settings=integrator_settings, + thermo_settings=thermo_settings, + cache=ffcache, + has_solvent=solvent_comp is not None, + ) + + # c. force the creation of parameters + # This is necessary because we need to have the FF templates + # registered ahead of solvating the system. + for smc, mol in chain( + off_small_mols["stateA"], off_small_mols["stateB"], off_small_mols["both"] + ): + system_generator.create_system(mol.to_topology().to_openmm(), molecules=[mol]) + + # c. get OpenMM Modeller + a dictionary of resids for each component + stateA_modeller, comp_resids = system_creation.get_omm_modeller( + protein_comp=protein_comp, + solvent_comp=solvent_comp, + small_mols=dict(chain(off_small_mols["stateA"], off_small_mols["both"])), + omm_forcefield=system_generator.forcefield, + solvent_settings=solvation_settings, + ) + + # d. get topology & positions + # Note: roundtrip positions to remove vec3 issues + stateA_topology = stateA_modeller.getTopology() + stateA_positions = to_openmm(from_openmm(stateA_modeller.getPositions())) + + # e. create the stateA System + # Block out oechem backend in system_generator calls to avoid + # any issues with smiles roundtripping between rdkit and oechem + with without_oechem_backend(): + stateA_system = system_generator.create_system( + stateA_modeller.topology, + molecules=[m for _, m in chain(off_small_mols["stateA"], off_small_mols["both"])], + ) + + # 2. Get stateB system + # a. get the topology + stateB_topology, stateB_alchem_resids = _rfe_utils.topologyhelpers.combined_topology( + stateA_topology, + # zeroth item (there's only one) then get the OFF representation + off_small_mols["stateB"][0][1].to_topology().to_openmm(), + exclude_resids=comp_resids[mapping.componentA], + ) + + # b. get a list of small molecules for stateB + # Block out oechem backend in system_generator calls to avoid + # any issues with smiles roundtripping between rdkit and oechem + with without_oechem_backend(): + stateB_system = system_generator.create_system( + stateB_topology, + molecules=[m for _, m in chain(off_small_mols["stateB"], off_small_mols["both"])], + ) + + # c. Define correspondence mappings between the two systems + ligand_mappings = _rfe_utils.topologyhelpers.get_system_mappings( + mapping.componentA_to_componentB, + stateA_system, + stateA_topology, + comp_resids[mapping.componentA], + stateB_system, + stateB_topology, + stateB_alchem_resids, + # These are non-optional settings for this method + fix_constraints=True, + ) + + # d. if a charge correction is necessary, select alchemical waters + # and transform them + if alchem_settings.explicit_charge_correction: + alchem_water_resids = _rfe_utils.topologyhelpers.get_alchemical_waters( + stateA_topology, + stateA_positions, + charge_difference, + alchem_settings.explicit_charge_correction_cutoff, + ) + _rfe_utils.topologyhelpers.handle_alchemical_waters( + alchem_water_resids, + stateB_topology, + stateB_system, + ligand_mappings, + charge_difference, + solvent_comp, + ) + + # e. Finally get the positions + stateB_positions = _rfe_utils.topologyhelpers.set_and_check_new_positions( + ligand_mappings, + stateA_topology, + stateB_topology, + old_positions=ensure_quantity(stateA_positions, "openmm"), + insert_positions=ensure_quantity( + off_small_mols["stateB"][0][1].conformers[0], "openmm" + ), + ) + + # 3. Create the hybrid topology + # a. Get softcore potential settings + if alchem_settings.softcore_LJ.lower() == "gapsys": + softcore_LJ_v2 = True + elif alchem_settings.softcore_LJ.lower() == "beutler": + softcore_LJ_v2 = False + # b. Get hybrid topology factory + hybrid_factory = _rfe_utils.relative.HybridTopologyFactory( + stateA_system, + stateA_positions, + stateA_topology, + stateB_system, + stateB_positions, + stateB_topology, + old_to_new_atom_map=ligand_mappings["old_to_new_atom_map"], + old_to_new_core_atom_map=ligand_mappings["old_to_new_core_atom_map"], + use_dispersion_correction=alchem_settings.use_dispersion_correction, + softcore_alpha=alchem_settings.softcore_alpha, + softcore_LJ_v2=softcore_LJ_v2, + softcore_LJ_v2_alpha=alchem_settings.softcore_alpha, + interpolate_old_and_new_14s=alchem_settings.turn_off_core_unique_exceptions, + ) + + # 4. Create lambda schedule + # TODO - this should be exposed to users, maybe we should offer the + # ability to print the schedule directly in settings? + # fmt: off + lambdas = _rfe_utils.lambdaprotocol.LambdaProtocol( + functions=lambda_settings.lambda_functions, + windows=lambda_settings.lambda_windows + ) + # fmt: on + # PR #125 temporarily pin lambda schedule spacing to n_replicas + n_replicas = sampler_settings.n_replicas + if n_replicas != len(lambdas.lambda_schedule): + errmsg = ( + f"Number of replicas {n_replicas} " + f"does not equal the number of lambda windows " + f"{len(lambdas.lambda_schedule)}" + ) + raise ValueError(errmsg) + + # 9. Create the multistate reporter + # Get the sub selection of the system to print coords for + selection_indices = hybrid_factory.hybrid_topology.select(output_settings.output_indices) + + # a. Create the multistate reporter + # convert checkpoint_interval from time to iterations + chk_intervals = settings_validation.convert_checkpoint_interval_to_iterations( + checkpoint_interval=output_settings.checkpoint_interval, + time_per_iteration=sampler_settings.time_per_iteration, + ) + + nc = shared_basepath / output_settings.output_filename + chk = output_settings.checkpoint_storage_filename + + if output_settings.positions_write_frequency is not None: + pos_interval = settings_validation.divmod_time_and_check( + numerator=output_settings.positions_write_frequency, + denominator=sampler_settings.time_per_iteration, + numerator_name="output settings' position_write_frequency", + denominator_name="sampler settings' time_per_iteration", + ) + else: + pos_interval = 0 + + if output_settings.velocities_write_frequency is not None: + vel_interval = settings_validation.divmod_time_and_check( + numerator=output_settings.velocities_write_frequency, + denominator=sampler_settings.time_per_iteration, + numerator_name="output settings' velocity_write_frequency", + denominator_name="sampler settings' time_per_iteration", + ) + else: + vel_interval = 0 + + reporter = multistate.MultiStateReporter( + storage=nc, + analysis_particle_indices=selection_indices, + checkpoint_interval=chk_intervals, + checkpoint_storage=chk, + position_interval=pos_interval, + velocity_interval=vel_interval, + ) + + # b. Write out a PDB containing the subsampled hybrid state + # fmt: off + bfactors = np.zeros_like(selection_indices, dtype=float) # solvent + bfactors[np.in1d(selection_indices, list(hybrid_factory._atom_classes['unique_old_atoms']))] = 0.25 # lig A + bfactors[np.in1d(selection_indices, list(hybrid_factory._atom_classes['core_atoms']))] = 0.50 # core + bfactors[np.in1d(selection_indices, list(hybrid_factory._atom_classes['unique_new_atoms']))] = 0.75 # lig B + # bfactors[np.in1d(selection_indices, protein)] = 1.0 # prot+cofactor + if len(selection_indices) > 0: + traj = mdtraj.Trajectory( + hybrid_factory.hybrid_positions[selection_indices, :], + hybrid_factory.hybrid_topology.subset(selection_indices), + ).save_pdb( + shared_basepath / output_settings.output_structure, + bfactors=bfactors, + ) + # fmt: on + + # 10. Get compute platform + # restrict to a single CPU if running vacuum + restrict_cpu = forcefield_settings.nonbonded_method.lower() == "nocutoff" + platform = omm_compute.get_openmm_platform( + platform_name=protocol_settings.engine_settings.compute_platform, + gpu_device_index=protocol_settings.engine_settings.gpu_device_index, + restrict_cpu_count=restrict_cpu, + ) + + # 11. Set the integrator + # a. Validate integrator settings for current system + # Virtual sites sanity check - ensure we restart velocities when + # there are virtual sites in the system + if hybrid_factory.has_virtual_sites: + if not integrator_settings.reassign_velocities: + errmsg = ( + "Simulations with virtual sites without velocity " + "reassignments are unstable in openmmtools" + ) + raise ValueError(errmsg) + + # b. create langevin integrator + integrator = openmmtools.mcmc.LangevinDynamicsMove( + timestep=to_openmm(integrator_settings.timestep), + collision_rate=to_openmm(integrator_settings.langevin_collision_rate), + n_steps=steps_per_iteration, + reassign_velocities=integrator_settings.reassign_velocities, + n_restart_attempts=integrator_settings.n_restart_attempts, + constraint_tolerance=integrator_settings.constraint_tolerance, + ) + + # 12. Create sampler + self.logger.info("Creating and setting up the sampler") + rta_its, rta_min_its = settings_validation.convert_real_time_analysis_iterations( + simulation_settings=sampler_settings, + ) + # convert early_termination_target_error from kcal/mol to kT + early_termination_target_error = ( + settings_validation.convert_target_error_from_kcal_per_mole_to_kT( + thermo_settings.temperature, + sampler_settings.early_termination_target_error, + ) + ) + + if sampler_settings.sampler_method.lower() == "repex": + sampler = _rfe_utils.multistate.HybridRepexSampler( + mcmc_moves=integrator, + hybrid_system=hybrid_factory.hybrid_system, + hybrid_positions=hybrid_factory.hybrid_positions, + online_analysis_interval=rta_its, + online_analysis_target_error=early_termination_target_error, + online_analysis_minimum_iterations=rta_min_its, + ) + elif sampler_settings.sampler_method.lower() == "sams": + sampler = _rfe_utils.multistate.HybridSAMSSampler( + mcmc_moves=integrator, + hybrid_system=hybrid_factory.hybrid_system, + hybrid_positions=hybrid_factory.hybrid_positions, + online_analysis_interval=rta_its, + online_analysis_minimum_iterations=rta_min_its, + flatness_criteria=sampler_settings.sams_flatness_criteria, + gamma0=sampler_settings.sams_gamma0, + ) + elif sampler_settings.sampler_method.lower() == "independent": + sampler = _rfe_utils.multistate.HybridMultiStateSampler( + mcmc_moves=integrator, + hybrid_system=hybrid_factory.hybrid_system, + hybrid_positions=hybrid_factory.hybrid_positions, + online_analysis_interval=rta_its, + online_analysis_target_error=early_termination_target_error, + online_analysis_minimum_iterations=rta_min_its, + ) + else: + raise AttributeError(f"Unknown sampler {sampler_settings.sampler_method}") + + sampler.setup( + n_replicas=sampler_settings.n_replicas, + reporter=reporter, + lambda_protocol=lambdas, + temperature=to_openmm(thermo_settings.temperature), + endstates=alchem_settings.endstate_dispersion_correction, + minimization_platform=platform.getName(), + # Set minimization steps to None when running in dry mode + # otherwise do a very small one to avoid NaNs + minimization_steps=100 if not dry else None, + ) + + try: + # Create context caches (energy + sampler) + energy_context_cache = openmmtools.cache.ContextCache( + capacity=None, + time_to_live=None, + platform=platform, + ) + + sampler_context_cache = openmmtools.cache.ContextCache( + capacity=None, + time_to_live=None, + platform=platform, + ) + + sampler.energy_context_cache = energy_context_cache + sampler.sampler_context_cache = sampler_context_cache + + if not dry: # pragma: no-cover + # minimize + if verbose: + self.logger.info("Running minimization") + + sampler.minimize(max_iterations=sampler_settings.minimization_steps) + + # equilibrate + if verbose: + self.logger.info("Running equilibration phase") + + sampler.equilibrate(int(equil_steps / steps_per_iteration)) + + # production + if verbose: + self.logger.info("Running production phase") + + sampler.extend(int(prod_steps / steps_per_iteration)) + + self.logger.info("Production phase complete") + + self.logger.info("Post-simulation analysis of results") + # calculate relevant analyses of the free energies & sampling + # First close & reload the reporter to avoid netcdf clashes + analyzer = multistate_analysis.MultistateEquilFEAnalysis( + reporter, + sampling_method=sampler_settings.sampler_method.lower(), + result_units=offunit.kilocalorie_per_mole, + ) + analyzer.plot(filepath=shared_basepath, filename_prefix="") + analyzer.close() + + else: + # clean up the reporter file + fns = [ + shared_basepath / output_settings.output_filename, + shared_basepath / output_settings.checkpoint_storage_filename, + ] + for fn in fns: + os.remove(fn) + finally: + # close reporter when you're done, prevent + # file handle clashes + reporter.close() + + # clear GPU contexts + # TODO: use cache.empty() calls when openmmtools #690 is resolved + # replace with above + for context in list(energy_context_cache._lru._data.keys()): + del energy_context_cache._lru._data[context] + for context in list(sampler_context_cache._lru._data.keys()): + del sampler_context_cache._lru._data[context] + # cautiously clear out the global context cache too + for context in list(openmmtools.cache.global_context_cache._lru._data.keys()): + del openmmtools.cache.global_context_cache._lru._data[context] + + del sampler_context_cache, energy_context_cache + + if not dry: + del integrator, sampler + + if not dry: # pragma: no-cover + return {"nc": nc, "last_checkpoint": chk, **analyzer.unit_results_dict} + else: + return {"debug": {"sampler": sampler, "hybrid_factory": hybrid_factory}} + + @staticmethod + def structural_analysis(scratch, shared) -> dict: + # don't put energy analysis in here, it uses the open file reporter + # whereas structural stuff requires that the file handle is closed + # TODO: we should just make openfe_analysis write an npz instead! + analysis_out = scratch / "structural_analysis.json" + + ret = subprocess.run( + [ + "openfe_analysis", # CLI entry point + "RFE_analysis", # CLI option + str(shared), # Where the simulation.nc fille + str(analysis_out), # Where the analysis json file is written + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if ret.returncode: + return {"structural_analysis_error": ret.stderr} + + with open(analysis_out, "rb") as f: + data = json.load(f) + + savedir = pathlib.Path(shared) + if d := data["protein_2D_RMSD"]: + fig = plotting.plot_2D_rmsd(d) + fig.savefig(savedir / "protein_2D_RMSD.png") + plt.close(fig) + f2 = plotting.plot_ligand_COM_drift(data["time(ps)"], data["ligand_wander"]) + f2.savefig(savedir / "ligand_COM_drift.png") + plt.close(f2) + + f3 = plotting.plot_ligand_RMSD(data["time(ps)"], data["ligand_RMSD"]) + f3.savefig(savedir / "ligand_RMSD.png") + plt.close(f3) + + # Save to numpy compressed format (~ 6x more space efficient than JSON) + np.savez_compressed( + shared / "structural_analysis.npz", + protein_RMSD=np.asarray(data["protein_RMSD"], dtype=np.float32), + ligand_RMSD=np.asarray(data["ligand_RMSD"], dtype=np.float32), + ligand_COM_drift=np.asarray(data["ligand_wander"], dtype=np.float32), + protein_2D_RMSD=np.asarray(data["protein_2D_RMSD"], dtype=np.float32), + time_ps=np.asarray(data["time(ps)"], dtype=np.float32), + ) + + return {"structural_analysis": shared / "structural_analysis.npz"} + + def _execute( + self, + ctx: gufe.Context, + **kwargs, + ) -> dict[str, Any]: + log_system_probe(logging.INFO, paths=[ctx.scratch]) + + outputs = self.run(scratch_basepath=ctx.scratch, shared_basepath=ctx.shared) + + structural_analysis_outputs = self.structural_analysis(ctx.scratch, ctx.shared) + + return { + "repeat_id": self._inputs["repeat_id"], + "generation": self._inputs["generation"], + **outputs, + **structural_analysis_outputs, + } diff --git a/openfe/tests/utils/test_duecredit.py b/openfe/tests/utils/test_duecredit.py index 28edeb089..67652ad78 100644 --- a/openfe/tests/utils/test_duecredit.py +++ b/openfe/tests/utils/test_duecredit.py @@ -34,7 +34,7 @@ class TestDuecredit: ], ], [ - "openfe.protocols.openmm_rfe.equil_rfe_methods", + "openfe.protocols.openmm_rfe.hybridtop_protocols", ["10.5281/zenodo.1297683", "10.5281/zenodo.596622", "10.1371/journal.pcbi.1005659"], ], [ From 6357e81701a2de9ee6f102177ec76561887af457 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Wed, 7 Jan 2026 16:33:37 -0500 Subject: [PATCH 26/47] HybridTop Unit methods (#1770) * Break down the Hybrid Top protocol unit into various methods. --------- Co-authored-by: Hannah Baumann <43765638+hannahbaumann@users.noreply.github.com> --- .../protocols/openmm_rfe/hybridtop_units.py | 1285 ++++++++++++----- 1 file changed, 948 insertions(+), 337 deletions(-) diff --git a/openfe/protocols/openmm_rfe/hybridtop_units.py b/openfe/protocols/openmm_rfe/hybridtop_units.py index b1ca0b786..03102435f 100644 --- a/openfe/protocols/openmm_rfe/hybridtop_units.py +++ b/openfe/protocols/openmm_rfe/hybridtop_units.py @@ -22,16 +22,26 @@ import matplotlib.pyplot as plt import mdtraj import numpy as np +import numpy.typing as npt +import openmm import openmmtools from gufe import ( ChemicalSystem, + Component, LigandAtomMapping, + ProteinComponent, SmallMoleculeComponent, - settings, + SolventComponent, +) +from gufe.settings import ( + SettingsBaseModel, + ThermoSettings, ) from openff.toolkit.topology import Molecule as OFFMolecule +from openff.units import Quantity from openff.units import unit as offunit from openff.units.openmm import ensure_quantity, from_openmm, to_openmm +from openmmforcefields.generators import SystemGenerator from openmmtools import multistate from openfe.protocols.openmm_utils.omm_settings import ( @@ -49,6 +59,7 @@ system_validation, ) from . import _rfe_utils +from ._rfe_utils.relative import HybridTopologyFactory from .equil_rfe_settings import ( AlchemicalSettings, IntegratorSettings, @@ -56,6 +67,7 @@ MultiStateOutputSettings, MultiStateSimulationSettings, OpenFFPartialChargeSettings, + OpenMMEngineSettings, OpenMMSolvationSettings, RelativeHybridTopologyProtocolSettings, ) @@ -112,25 +124,128 @@ def __init__( generation=generation, ) + def _prepare( + self, + verbose: bool, + scratch_basepath: pathlib.Path | None, + shared_basepath: pathlib.Path | None, + ): + """ + Set basepaths and do some initial logging. + + Parameters + ---------- + verbose : bool + Verbose output of the simulation progress. Output is provided at the + INFO level logging. + scratch_basepath : pathlib.Path | None + Optional scratch base path to write scratch files to. + shared_basepath : pathlib.Path | None + Optional shared base path to write shared files to. + """ + self.verbose = verbose + + if self.verbose: + self.logger.info("Setting up the hybrid topology simulation") + + # set basepaths + def _set_optional_path(basepath): + if basepath is None: + return pathlib.Path(".") + return basepath + + self.scratch_basepath = _set_optional_path(scratch_basepath) + self.shared_basepath = _set_optional_path(shared_basepath) + + @staticmethod + def _get_settings( + settings: RelativeHybridTopologyProtocolSettings, + ) -> dict[str, SettingsBaseModel]: + """ + Get a dictionary of Protocol settings. + + Returns + ------- + protocol_settings : dict[str, SettingsBaseModel] + + Notes + ----- + We return a dict so that we can duck type behaviour between phases. + For example subclasses may contain both `solvent` and `complex` + settings, using this approach we can extract the relevant entry + to the same key and pass it to other methods in a seamless manner. + """ + protocol_settings: dict[str, SettingsBaseModel] = {} + protocol_settings["forcefield_settings"] = settings.forcefield_settings + protocol_settings["thermo_settings"] = settings.thermo_settings + protocol_settings["alchemical_settings"] = settings.alchemical_settings + protocol_settings["lambda_settings"] = settings.lambda_settings + protocol_settings["charge_settings"] = settings.partial_charge_settings + protocol_settings["solvation_settings"] = settings.solvation_settings + protocol_settings["simulation_settings"] = settings.simulation_settings + protocol_settings["output_settings"] = settings.output_settings + protocol_settings["integrator_settings"] = settings.integrator_settings + protocol_settings["engine_settings"] = settings.engine_settings + return protocol_settings + + @staticmethod + def _get_components( + stateA: ChemicalSystem, stateB: ChemicalSystem + ) -> tuple[ + dict[str, Component], + SolventComponent, + ProteinComponent, + dict[SmallMoleculeComponent, OFFMolecule], + ]: + """ + Get the components from the ChemicalSystem inputs. + + Parameters + ---------- + stateA : ChemicalSystem + ChemicalSystem defining the state A components. + stateB : CHemicalSystem + ChemicalSystem defining the state B components. + + Returns + ------- + alchem_comps : dict[str, Component] + Dictionary of alchemical components. + solv_comp : SolventComponent + The solvent component. + protein_comp : ProteinComponent + The protein component. + small_mols : dict[SmallMoleculeComponent, openff.toolkit.Molecule] + Dictionary of small molecule components paired + with their OpenFF Molecule. + """ + alchem_comps = system_validation.get_alchemical_components(stateA, stateB) + + solvent_comp, protein_comp, smcs_A = system_validation.get_components(stateA) + _, _, smcs_B = system_validation.get_components(stateB) + + small_mols = {m: m.to_openff() for m in set(smcs_A).union(set(smcs_B))} + + return alchem_comps, solvent_comp, protein_comp, small_mols + @staticmethod def _assign_partial_charges( charge_settings: OpenFFPartialChargeSettings, - off_small_mols: dict[str, list[tuple[SmallMoleculeComponent, OFFMolecule]]], + small_mols: dict[SmallMoleculeComponent, OFFMolecule], ) -> None: """ - Assign partial charges to SMCs. + Assign partial charges to the OpenFF Molecules associated with all + the SmallMoleculeComponents in the transformation. Parameters ---------- charge_settings : OpenFFPartialChargeSettings Settings for controlling how the partial charges are assigned. - off_small_mols : dict[str, list[tuple[SmallMoleculeComponent, OFFMolecule]]] - Dictionary of dictionary of OpenFF Molecules to add, keyed by - state and SmallMoleculeComponent. + small_mols : dict[SmallMoleculeComponent, openff.toolkit.Molecule] + Dictionary of OpenFF Molecules to add, keyed by + their associated SmallMoleculeComponent. """ - for smc, mol in chain( - off_small_mols["stateA"], off_small_mols["stateB"], off_small_mols["both"] - ): + for smc, mol in small_mols.items(): charge_generation.assign_offmol_partial_charges( offmol=mol, overwrite=False, @@ -140,227 +255,407 @@ def _assign_partial_charges( nagl_model=charge_settings.nagl_model, ) - def run( - self, *, dry=False, verbose=True, scratch_basepath=None, shared_basepath=None - ) -> dict[str, Any]: - """Run the relative free energy calculation. + @staticmethod + def _get_system_generator( + shared_basepath: pathlib.Path, + settings: dict[str, SettingsBaseModel], + solvent_comp: SolventComponent | None, + openff_molecules: list[OFFMolecule] | None, + ) -> SystemGenerator: + """ + Get an OpenMM SystemGenerator. Parameters ---------- - dry : bool - Do a dry run of the calculation, creating all necessary hybrid - system components (topology, system, sampler, etc...) but without - running the simulation. - verbose : bool - Verbose output of the simulation progress. Output is provided via - INFO level logging. - scratch_basepath: Pathlike, optional - Where to store temporary files, defaults to current working directory - shared_basepath : Pathlike, optional - Where to run the calculation, defaults to current working directory + settings : dict[str, SettingsBaseModel] + A dictionary of protocol settings. + solvent_comp : SolventComponent | None + The solvent component of the system, if any. + openff_molecules : list[openff.Toolkit] | None + A list of openff molecules to generate templates for, if any. Returns ------- - dict - Outputs created in the basepath directory or the debug objects - (i.e. sampler) if ``dry==True``. + system_generator : openmmtools.SystemGenerator + The SystemGenerator for the protocol. + """ + ffcache = settings["output_settings"].forcefield_cache - Raises - ------ - error - Exception if anything failed + if ffcache is not None: + ffcache = shared_basepath / ffcache + + # Block out oechem backend in system_generator calls to avoid + # any issues with smiles roundtripping between rdkit and oechem + with without_oechem_backend(): + system_generator = system_creation.get_system_generator( + forcefield_settings=settings["forcefield_settings"], + integrator_settings=settings["integrator_settings"], + thermo_settings=settings["thermo_settings"], + cache=ffcache, + has_solvent=solvent_comp is not None, + ) + + # Handle openff Molecule templates + # TODO: revisit this once the SystemGenerator update happens + # and we start loading the whole protein into OpenFF Topologies + + # First deduplicate isomorphic molecules, if there are any + if openff_molecules is None: + return system_generator + + unique_offmols: list[OFFMolecule] = [] + for mol in openff_molecules: + unique = all([not mol.is_isomorphic_with(umol) for umol in unique_offmols]) + if unique: + unique_offmols.append(mol) + + # register all the templates + system_generator.add_molecules(unique_offmols) + + return system_generator + + @staticmethod + def _create_stateA_system( + small_mols: dict[SmallMoleculeComponent, OFFMolecule], + protein_component: ProteinComponent | None, + solvent_component: SolventComponent | None, + system_generator: SystemGenerator, + solvation_settings: OpenMMSolvationSettings, + ) -> tuple[ + openmm.System, openmm.app.Topology, openmm.unit.Quantity, dict[Component, npt.NDArray] + ]: """ - if verbose: - self.logger.info("Preparing the hybrid topology simulation") - if scratch_basepath is None: - scratch_basepath = pathlib.Path(".") - if shared_basepath is None: - # use cwd - shared_basepath = pathlib.Path(".") + Create an OpenMM System for state A. + + Parameters + ---------- + small_mols : dict[SmallMoleculeComponent, openff.toolkit.Molecule] + A list of small molecules to include in the System. + protein_component : ProteinComponent | None + Optionally, the protein component to include in the System. + solvent_component : SolventComponent | None + Optionally, the solvent component to include in the System. + system_generator : SystemGenerator + The SystemGenerator object ot use to construct the System. + solvation_settings : OpenMMSolvationSettings + Settings defining how to build the System. - # 0. General setup and settings dependency resolution step + Returns + ------- + system : openmm.System + The System that defines state A. + topology : openmm.app.Topology + The Topology defining the returned System. + positions : openmm.unit.Quantity + The positions of the particles in the System. + comp_residues : dict[Component, npt.NDArray] + A dictionary defining which residues in the System + belong to which ChemicalSystem Component. + """ + modeller, comp_resids = system_creation.get_omm_modeller( + protein_comp=protein_component, + solvent_comp=solvent_component, + small_mols=small_mols, + omm_forcefield=system_generator.forcefield, + solvent_settings=solvation_settings, + ) - # Extract relevant settings - protocol_settings: RelativeHybridTopologyProtocolSettings = self._inputs[ - "protocol" - ].settings - stateA = self._inputs["stateA"] - stateB = self._inputs["stateB"] - mapping = self._inputs["ligandmapping"] + topology = modeller.getTopology() + # Note: roundtrip positions to remove vec3 issues + positions = to_openmm(from_openmm(modeller.getPositions())) - forcefield_settings: settings.OpenMMSystemGeneratorFFSettings = ( - protocol_settings.forcefield_settings + system = system_generator.create_system( + modeller.topology, + molecules=list(small_mols.values()), ) - thermo_settings: settings.ThermoSettings = protocol_settings.thermo_settings - alchem_settings: AlchemicalSettings = protocol_settings.alchemical_settings - lambda_settings: LambdaSettings = protocol_settings.lambda_settings - charge_settings: BasePartialChargeSettings = protocol_settings.partial_charge_settings - solvation_settings: OpenMMSolvationSettings = protocol_settings.solvation_settings - sampler_settings: MultiStateSimulationSettings = protocol_settings.simulation_settings - output_settings: MultiStateOutputSettings = protocol_settings.output_settings - integrator_settings: IntegratorSettings = protocol_settings.integrator_settings - - # TODO: Also validate various conversions? - # Convert various time based inputs to steps/iterations - steps_per_iteration = settings_validation.convert_steps_per_iteration( - simulation_settings=sampler_settings, - integrator_settings=integrator_settings, + + return system, topology, positions, comp_resids + + @staticmethod + def _create_stateB_system( + small_mols: dict[SmallMoleculeComponent, OFFMolecule], + mapping: LigandAtomMapping, + stateA_topology: openmm.app.Topology, + exclude_resids: npt.NDArray, + system_generator: SystemGenerator, + ) -> tuple[openmm.System, openmm.app.Topology, npt.NDArray]: + """ + Create the state B System from the state A Topology. + + Parameters + ---------- + small_mols : dict[SmallMoleculeComponent, openff.toolkit.Molecule] + Dictionary of OpenFF Molecules keyed by SmallMoleculeComponent + to be present in system B. + mapping : LigandAtomMapping + LigandAtomMapping defining the correspondance betwee state A + and B's alchemical ligand. + stateA_topology : openmm.app.Topology + The OpenMM topology for state A. + exclude_resids : npt.NDArray + A list of residues to exclude from state A when building state B. + system_generator : SystemGenerator + The SystemGenerator to use to build System B. + + Returns + ------- + system : openmm.System + The state B System. + topology : openmm.app.Topology + The OpenMM Topology associated with the state B System. + alchem_resids : npt.NDArray + The residue indices of the state B alchemical species. + """ + topology, alchem_resids = _rfe_utils.topologyhelpers.combined_topology( + topology1=stateA_topology, + topology2=small_mols[mapping.componentB].to_topology().to_openmm(), + exclude_resids=exclude_resids, ) - equil_steps = settings_validation.get_simsteps( - sim_length=sampler_settings.equilibration_length, - timestep=integrator_settings.timestep, - mc_steps=steps_per_iteration, + system = system_generator.create_system( + topology, + molecules=list(small_mols.values()), ) - prod_steps = settings_validation.get_simsteps( - sim_length=sampler_settings.production_length, - timestep=integrator_settings.timestep, - mc_steps=steps_per_iteration, + + return system, topology, alchem_resids + + @staticmethod + def _handle_net_charge( + stateA_topology: openmm.app.Topology, + stateA_positions: openmm.unit.Quantity, + stateB_topology: openmm.app.Topology, + stateB_system: openmm.System, + charge_difference: int, + system_mappings: dict[str, dict[int, int]], + distance_cutoff: Quantity, + solvent_component: SolventComponent | None, + ) -> None: + """ + Handle system net charge by adding an alchemical water. + + Parameters + ---------- + stateA_topology : openmm.app.Topology + stateA_positions : openmm.unit.Quantity + stateB_topology : openmm.app.Topology + stateB_system : openmm.System + charge_difference : int + system_mappings : dict[str, dict[int, int]] + distance_cutoff : Quantity + solvent_component : SolventComponent | None + """ + # Base case, return if no net charge + if charge_difference == 0: + return + + # Get the residue ids for waters to turn alchemical + alchem_water_resids = _rfe_utils.topologyhelpers.get_alchemical_waters( + topology=stateA_topology, + positions=stateA_positions, + charge_difference=charge_difference, + distance_cutoff=distance_cutoff, ) - solvent_comp, protein_comp, small_mols = system_validation.get_components(stateA) - - # Get the change difference between the end states - # and check if the charge correction used is appropriate - charge_difference = mapping.get_alchemical_charge_difference() - - # 1. Create stateA system - self.logger.info("Parameterizing molecules") - - # a. create offmol dictionaries and assign partial charges - # workaround for conformer generation failures - # see openfe issue #576 - # calculate partial charges manually if not already given - # convert to OpenFF here, - # and keep the molecule around to maintain the partial charges - off_small_mols: dict[str, list[tuple[SmallMoleculeComponent, OFFMolecule]]] - off_small_mols = { - "stateA": [(mapping.componentA, mapping.componentA.to_openff())], - "stateB": [(mapping.componentB, mapping.componentB.to_openff())], - "both": [ - (m, m.to_openff()) - for m in small_mols - if (m != mapping.componentA and m != mapping.componentB) - ], - } + # In-place modify state B alchemical waters to ions + _rfe_utils.topologyhelpers.handle_alchemical_waters( + water_resids=alchem_water_resids, + topology=stateB_topology, + system=stateB_system, + system_mapping=system_mappings, + charge_difference=charge_difference, + solvent_component=solvent_component, + ) - self._assign_partial_charges(charge_settings, off_small_mols) + def _get_omm_objects( + self, + stateA: ChemicalSystem, + stateB: ChemicalSystem, + mapping: LigandAtomMapping, + settings: dict[str, SettingsBaseModel], + protein_component: ProteinComponent | None, + solvent_component: SolventComponent | None, + small_mols: dict[SmallMoleculeComponent, OFFMolecule], + ) -> tuple[ + openmm.System, + openmm.app.Topology, + openmm.unit.Quantity, + openmm.System, + openmm.app.Topology, + openmm.unit.Quantity, + dict[str, dict[int, int]], + ]: + """ + Get OpenMM objects for both end states A and B. - # b. get a system generator - if output_settings.forcefield_cache is not None: - ffcache = shared_basepath / output_settings.forcefield_cache - else: - ffcache = None + Parameters + ---------- + stateA : ChemicalSystem + ChemicalSystem defining end state A. + stateB : ChemicalSystem + ChemicalSystem defining end state B. + mapping : LigandAtomMapping + The mapping for alchemical components between state A and B. + settings : dict[str, SettingsBaseModel] + Settings for the transformation. + protein_component : ProteinComponent | None + The common ProteinComponent between the end states, if there is is one. + solvent_component : SolventComponent | None + The common SolventComponent between the end states, if there is one. + small_mols : dict[SmallMoleculeComponent, openff.toolkit.Molecule] + The small molecules for both end states. - # Block out oechem backend in system_generator calls to avoid - # any issues with smiles roundtripping between rdkit and oechem - with without_oechem_backend(): - system_generator = system_creation.get_system_generator( - forcefield_settings=forcefield_settings, - integrator_settings=integrator_settings, - thermo_settings=thermo_settings, - cache=ffcache, - has_solvent=solvent_comp is not None, - ) + Returns + ------- + stateA_system : openmm.System + OpenMM System for state A. + stateA_topology : openmm.app.Topology + OpenMM Topology for the state A System. + stateA_positions : openmm.unit.Quantity + Positions of partials for state A System. + stateB_system : openmm.System + OpenMM System for state B. + stateB_topology : openmm.app.Topology + OpenMM Topology for the state B System. + stateB_positions : openmm.unit.Quantity + Positions of partials for state B System. + system_mapping : dict[str, dict[int, int]] + Dictionary of mappings defining the correspondance between + the two state Systems. + """ + if self.verbose: + self.logger.info("Parameterizing systems") - # c. force the creation of parameters - # This is necessary because we need to have the FF templates - # registered ahead of solvating the system. - for smc, mol in chain( - off_small_mols["stateA"], off_small_mols["stateB"], off_small_mols["both"] - ): - system_generator.create_system(mol.to_topology().to_openmm(), molecules=[mol]) - - # c. get OpenMM Modeller + a dictionary of resids for each component - stateA_modeller, comp_resids = system_creation.get_omm_modeller( - protein_comp=protein_comp, - solvent_comp=solvent_comp, - small_mols=dict(chain(off_small_mols["stateA"], off_small_mols["both"])), - omm_forcefield=system_generator.forcefield, - solvent_settings=solvation_settings, - ) + def _filter_small_mols(smols, state): + return {smc: offmol for smc, offmol in smols.items() if state.contains(smc)} - # d. get topology & positions - # Note: roundtrip positions to remove vec3 issues - stateA_topology = stateA_modeller.getTopology() - stateA_positions = to_openmm(from_openmm(stateA_modeller.getPositions())) + small_mols_stateA = _filter_small_mols(small_mols, stateA) + small_mols_stateB = _filter_small_mols(small_mols, stateB) - # e. create the stateA System - # Block out oechem backend in system_generator calls to avoid - # any issues with smiles roundtripping between rdkit and oechem + # Everything involving systemgenerator handling has a risk of + # oechem <-> rdkit smiles conversion clashes, cautiously ban it. with without_oechem_backend(): - stateA_system = system_generator.create_system( - stateA_modeller.topology, - molecules=[m for _, m in chain(off_small_mols["stateA"], off_small_mols["both"])], + # TODO: get two generators, one for state A and one for stateB + # See issue #1120 + # Get the system generator with all the templates registered + system_generator = self._get_system_generator( + shared_basepath=self.shared_basepath, + settings=settings, + solvent_comp=solvent_component, + openff_molecules=list(small_mols.values()), ) - # 2. Get stateB system - # a. get the topology - stateB_topology, stateB_alchem_resids = _rfe_utils.topologyhelpers.combined_topology( - stateA_topology, - # zeroth item (there's only one) then get the OFF representation - off_small_mols["stateB"][0][1].to_topology().to_openmm(), - exclude_resids=comp_resids[mapping.componentA], - ) + (stateA_system, stateA_topology, stateA_positions, comp_resids) = ( + self._create_stateA_system( + small_mols=small_mols_stateA, + protein_component=protein_component, + solvent_component=solvent_component, + system_generator=system_generator, + solvation_settings=settings["solvation_settings"], + ) + ) - # b. get a list of small molecules for stateB - # Block out oechem backend in system_generator calls to avoid - # any issues with smiles roundtripping between rdkit and oechem - with without_oechem_backend(): - stateB_system = system_generator.create_system( - stateB_topology, - molecules=[m for _, m in chain(off_small_mols["stateB"], off_small_mols["both"])], + (stateB_system, stateB_topology, stateB_alchem_resids) = self._create_stateB_system( + small_mols=small_mols_stateB, + mapping=mapping, + stateA_topology=stateA_topology, + exclude_resids=comp_resids[mapping.componentA], + system_generator=system_generator, ) - # c. Define correspondence mappings between the two systems - ligand_mappings = _rfe_utils.topologyhelpers.get_system_mappings( - mapping.componentA_to_componentB, - stateA_system, - stateA_topology, - comp_resids[mapping.componentA], - stateB_system, - stateB_topology, - stateB_alchem_resids, + # Get the mapping between the two systems + system_mappings = _rfe_utils.topologyhelpers.get_system_mappings( + old_to_new_atom_map=mapping.componentA_to_componentB, + old_system=stateA_system, + old_topology=stateA_topology, + old_resids=comp_resids[mapping.componentA], + new_system=stateB_system, + new_topology=stateB_topology, + new_resids=stateB_alchem_resids, # These are non-optional settings for this method fix_constraints=True, ) - # d. if a charge correction is necessary, select alchemical waters - # and transform them - if alchem_settings.explicit_charge_correction: - alchem_water_resids = _rfe_utils.topologyhelpers.get_alchemical_waters( - stateA_topology, - stateA_positions, - charge_difference, - alchem_settings.explicit_charge_correction_cutoff, - ) - _rfe_utils.topologyhelpers.handle_alchemical_waters( - alchem_water_resids, - stateB_topology, - stateB_system, - ligand_mappings, - charge_difference, - solvent_comp, + # Net charge: add alchemical water if needed + # Must be done here as we in-place modify the particles of state B. + if settings["alchemical_settings"].explicit_charge_correction: + self._handle_net_charge( + stateA_topology=stateA_topology, + stateA_positions=stateA_positions, + stateB_topology=stateB_topology, + stateB_system=stateB_system, + charge_difference=mapping.get_alchemical_charge_difference(), + system_mappings=system_mappings, + distance_cutoff=settings["alchemical_settings"].explicit_charge_correction_cutoff, + solvent_component=solvent_component, ) - # e. Finally get the positions + # Finally get the state B positions stateB_positions = _rfe_utils.topologyhelpers.set_and_check_new_positions( - ligand_mappings, + system_mappings, stateA_topology, stateB_topology, old_positions=ensure_quantity(stateA_positions, "openmm"), insert_positions=ensure_quantity( - off_small_mols["stateB"][0][1].conformers[0], "openmm" + small_mols[mapping.componentB].conformers[0], "openmm" ), ) - # 3. Create the hybrid topology - # a. Get softcore potential settings - if alchem_settings.softcore_LJ.lower() == "gapsys": + return ( + stateA_system, + stateA_topology, + stateA_positions, + stateB_system, + stateB_topology, + stateB_positions, + system_mappings, + ) + + @staticmethod + def _get_alchemical_system( + stateA_system: openmm.System, + stateA_positions: openmm.unit.Quantity, + stateA_topology: openmm.app.Topology, + stateB_system: openmm.System, + stateB_positions: openmm.unit.Quantity, + stateB_topology: openmm.app.Topology, + system_mappings: dict[str, dict[int, int]], + alchemical_settings: AlchemicalSettings, + ): + """ + Get the hybrid topology alchemical system. + + Parameters + ---------- + stateA_system : openmm.System + State A OpenMM System + stateA_positions : openmm.unit.Quantity + Positions of state A System + stateA_topology : openmm.app.Topology + Topology of state A System + stateB_system : openmm.System + State B OpenMM System + stateB_positions : openmm.unit.Quantity + Positions of state B System + stateB_topology : openmm.app.Topology + Topology of state B System + system_mappings : dict[str, dict[int, int]] + Mapping of corresponding atoms between the two Systems. + alchemical_settings : AlchemicalSettings + The alchemical settings defining how the alchemical system + will be built. + + Returns + ------- + hybrid_factory : HybridTopologyFactory + The factory creating the hybrid system. + hybrid_system : openmm.System + The hybrid System. + """ + if alchemical_settings.softcore_LJ.lower() == "gapsys": softcore_LJ_v2 = True - elif alchem_settings.softcore_LJ.lower() == "beutler": + elif alchemical_settings.softcore_LJ.lower() == "beutler": softcore_LJ_v2 = False - # b. Get hybrid topology factory + hybrid_factory = _rfe_utils.relative.HybridTopologyFactory( stateA_system, stateA_positions, @@ -368,54 +663,161 @@ def run( stateB_system, stateB_positions, stateB_topology, - old_to_new_atom_map=ligand_mappings["old_to_new_atom_map"], - old_to_new_core_atom_map=ligand_mappings["old_to_new_core_atom_map"], - use_dispersion_correction=alchem_settings.use_dispersion_correction, - softcore_alpha=alchem_settings.softcore_alpha, + old_to_new_atom_map=system_mappings["old_to_new_atom_map"], + old_to_new_core_atom_map=system_mappings["old_to_new_core_atom_map"], + use_dispersion_correction=alchemical_settings.use_dispersion_correction, + softcore_alpha=alchemical_settings.softcore_alpha, softcore_LJ_v2=softcore_LJ_v2, - softcore_LJ_v2_alpha=alchem_settings.softcore_alpha, - interpolate_old_and_new_14s=alchem_settings.turn_off_core_unique_exceptions, + softcore_LJ_v2_alpha=alchemical_settings.softcore_alpha, + interpolate_old_and_new_14s=alchemical_settings.turn_off_core_unique_exceptions, ) - # 4. Create lambda schedule - # TODO - this should be exposed to users, maybe we should offer the - # ability to print the schedule directly in settings? - # fmt: off - lambdas = _rfe_utils.lambdaprotocol.LambdaProtocol( - functions=lambda_settings.lambda_functions, - windows=lambda_settings.lambda_windows - ) - # fmt: on - # PR #125 temporarily pin lambda schedule spacing to n_replicas - n_replicas = sampler_settings.n_replicas - if n_replicas != len(lambdas.lambda_schedule): - errmsg = ( - f"Number of replicas {n_replicas} " - f"does not equal the number of lambda windows " - f"{len(lambdas.lambda_schedule)}" + return hybrid_factory, hybrid_factory.hybrid_system + + def _subsample_topology( + self, + hybrid_topology: openmm.app.Topology, + hybrid_positions: openmm.unit.Quantity, + output_selection: str, + output_filename: str, + atom_classes: dict[str, set[int]], + ) -> npt.NDArray: + """ + Subsample the hybrid topology based on user-selected output selection + and write the subsampled topology to a PDB file. + + Parameters + ---------- + hybrid_topology : openmm.app.Topology + The hybrid system topology to subsample. + hybrid_positions : openmm.unit.Quantity + The hybrid system positions. + output_selection : str + An MDTraj selection string to subsample the topology with. + output_filename : str + The name of the file to write the PDB to. + atom_classes : dict[str, set[int]] + A dictionary defining what atoms belong to the different + components of the hybrid system. + + Returns + ------- + selection_indices : npt.NDArray + The indices of the subselected system. + + TODO + ---- + Modify this to also store the full system. + """ + selection_indices = hybrid_topology.select(output_selection) + + # Write out a PDB containing the subsampled hybrid state + # We use bfactors as a hack to label different states + # bfactor of 0 is environment atoms + # bfactor of 0.25 is unique old atoms + # bfactor of 0.5 is core atoms + # bfactor of 0.75 is unique new atoms + bfactors = np.zeros_like(selection_indices, dtype=float) + bfactors[np.isin(selection_indices, list(atom_classes["unique_old_atoms"]))] = 0.25 + bfactors[np.isin(selection_indices, list(atom_classes["core_atoms"]))] = 0.50 + bfactors[np.isin(selection_indices, list(atom_classes["unique_new_atoms"]))] = 0.75 + + if len(selection_indices) > 0: + traj = mdtraj.Trajectory( + hybrid_positions[selection_indices, :], + hybrid_topology.subset(selection_indices), + ).save_pdb( + self.shared_basepath / output_filename, + bfactors=bfactors, ) - raise ValueError(errmsg) - # 9. Create the multistate reporter - # Get the sub selection of the system to print coords for - selection_indices = hybrid_factory.hybrid_topology.select(output_settings.output_indices) + return selection_indices - # a. Create the multistate reporter - # convert checkpoint_interval from time to iterations - chk_intervals = settings_validation.convert_checkpoint_interval_to_iterations( - checkpoint_interval=output_settings.checkpoint_interval, - time_per_iteration=sampler_settings.time_per_iteration, + @staticmethod + def _get_integrator( + integrator_settings: IntegratorSettings, + simulation_settings: MultiStateSimulationSettings, + system: openmm.System, + ) -> openmmtools.mcmc.LangevinDynamicsMove: + """ + Get and validate the integrator + + Parameters + ---------- + integrator_settings : IntegratorSettings + Settings controlling the Langevin integrator. + simulation_settings : MultiStateSimulationSettings + Settings controlling the simulation. + system : openmm.System + The OpenMM System. + + Returns + ------- + integrator : openmmtools.mcmc.LangevinDynamicsMove + The LangevinDynamicsMove integrator. + + Raises + ------ + ValueError + If there are virtual sites in the system, but velocities + are not being reassigned after every MCMC move. + """ + steps_per_iteration = settings_validation.convert_steps_per_iteration( + simulation_settings, integrator_settings ) - nc = shared_basepath / output_settings.output_filename + integrator = openmmtools.mcmc.LangevinDynamicsMove( + timestep=to_openmm(integrator_settings.timestep), + collision_rate=to_openmm(integrator_settings.langevin_collision_rate), + n_steps=steps_per_iteration, + reassign_velocities=integrator_settings.reassign_velocities, + n_restart_attempts=integrator_settings.n_restart_attempts, + constraint_tolerance=integrator_settings.constraint_tolerance, + ) + + # Validate for known issue when dealing with virtual sites + # and multistate simulations + if not integrator_settings.reassign_velocities: + for particle_idx in range(system.getNumParticles()): + if system.isVirtualSite(particle_idx): + errmsg = ( + "Simulations with virtual sites without velocity " + "reassignments are unstable with MCMC integrators." + ) + raise ValueError(errmsg) + + return integrator + + @staticmethod + def _get_reporter( + storage_path: pathlib.Path, + selection_indices: npt.NDArray, + output_settings: MultiStateOutputSettings, + simulation_settings: MultiStateSimulationSettings, + ) -> multistate.MultiStateReporter: + """ + Get the multistate reporter. + + Parameters + ---------- + storage_path : pathlib.Path + Path to the directory where files should be written. + selection_indices : npt.NDArray + The set of system indices to report positions & velocities for. + output_settings : MultiStateOutputSettings + Settings defining how outputs should be written. + simulation_settings : MultiStateSimulationSettings + Settings defining out the simulation should be run. + """ + nc = storage_path / output_settings.output_filename chk = output_settings.checkpoint_storage_filename if output_settings.positions_write_frequency is not None: pos_interval = settings_validation.divmod_time_and_check( numerator=output_settings.positions_write_frequency, - denominator=sampler_settings.time_per_iteration, + denominator=simulation_settings.time_per_iteration, numerator_name="output settings' position_write_frequency", - denominator_name="sampler settings' time_per_iteration", + denominator_name="simulation settings' time_per_iteration", ) else: pos_interval = 0 @@ -423,14 +825,19 @@ def run( if output_settings.velocities_write_frequency is not None: vel_interval = settings_validation.divmod_time_and_check( numerator=output_settings.velocities_write_frequency, - denominator=sampler_settings.time_per_iteration, + denominator=simulation_settings.time_per_iteration, numerator_name="output settings' velocity_write_frequency", denominator_name="sampler settings' time_per_iteration", ) else: vel_interval = 0 - reporter = multistate.MultiStateReporter( + chk_intervals = settings_validation.convert_checkpoint_interval_to_iterations( + checkpoint_interval=output_settings.checkpoint_interval, + time_per_iteration=simulation_settings.time_per_iteration, + ) + + return multistate.MultiStateReporter( storage=nc, analysis_particle_indices=selection_indices, checkpoint_interval=chk_intervals, @@ -439,100 +846,98 @@ def run( velocity_interval=vel_interval, ) - # b. Write out a PDB containing the subsampled hybrid state - # fmt: off - bfactors = np.zeros_like(selection_indices, dtype=float) # solvent - bfactors[np.in1d(selection_indices, list(hybrid_factory._atom_classes['unique_old_atoms']))] = 0.25 # lig A - bfactors[np.in1d(selection_indices, list(hybrid_factory._atom_classes['core_atoms']))] = 0.50 # core - bfactors[np.in1d(selection_indices, list(hybrid_factory._atom_classes['unique_new_atoms']))] = 0.75 # lig B - # bfactors[np.in1d(selection_indices, protein)] = 1.0 # prot+cofactor - if len(selection_indices) > 0: - traj = mdtraj.Trajectory( - hybrid_factory.hybrid_positions[selection_indices, :], - hybrid_factory.hybrid_topology.subset(selection_indices), - ).save_pdb( - shared_basepath / output_settings.output_structure, - bfactors=bfactors, - ) - # fmt: on - - # 10. Get compute platform - # restrict to a single CPU if running vacuum - restrict_cpu = forcefield_settings.nonbonded_method.lower() == "nocutoff" - platform = omm_compute.get_openmm_platform( - platform_name=protocol_settings.engine_settings.compute_platform, - gpu_device_index=protocol_settings.engine_settings.gpu_device_index, - restrict_cpu_count=restrict_cpu, - ) - - # 11. Set the integrator - # a. Validate integrator settings for current system - # Virtual sites sanity check - ensure we restart velocities when - # there are virtual sites in the system - if hybrid_factory.has_virtual_sites: - if not integrator_settings.reassign_velocities: - errmsg = ( - "Simulations with virtual sites without velocity " - "reassignments are unstable in openmmtools" - ) - raise ValueError(errmsg) + @staticmethod + def _get_sampler( + system: openmm.System, + positions: openmm.unit.Quantity, + lambdas: _rfe_utils.lambdaprotocol.LambdaProtocol, + integrator: openmmtools.mcmc.MCMCMove, + reporter: multistate.MultiStateReporter, + simulation_settings: MultiStateSimulationSettings, + thermo_settings: ThermoSettings, + alchem_settings: AlchemicalSettings, + platform: openmm.Platform, + dry: bool, + ) -> multistate.MultiStateSampler: + """ + Get the MultiStateSampler. - # b. create langevin integrator - integrator = openmmtools.mcmc.LangevinDynamicsMove( - timestep=to_openmm(integrator_settings.timestep), - collision_rate=to_openmm(integrator_settings.langevin_collision_rate), - n_steps=steps_per_iteration, - reassign_velocities=integrator_settings.reassign_velocities, - n_restart_attempts=integrator_settings.n_restart_attempts, - constraint_tolerance=integrator_settings.constraint_tolerance, - ) + Parameters + ---------- + system : openmm.System + The OpenMM System to simulate. + positions : openmm.unit.Quantity + The positions of the OpenMM System. + lambdas : LambdaProtocol + The lambda protocol to sample along. + integrator : openmmtools.mcmc.MCMCMove + The integrator to use. + reporter : multistate.MultiStateReporter + The reporter to attach to the sampler. + simulation_settings : MultiStateSimulationSettings + The simulation control settings. + thermo_settings : ThermoSettings + The thermodynamic control settings. + alchem_settings : AlchemicalSettings + The alchemical transformation settings. + platform : openmm.Platform + The compute platform to use. + dry : bool + Whether or not this is a dry run. - # 12. Create sampler - self.logger.info("Creating and setting up the sampler") + Returns + ------- + sampler : multistate.MultiStateSampler + The requested sampler. + """ rta_its, rta_min_its = settings_validation.convert_real_time_analysis_iterations( - simulation_settings=sampler_settings, + simulation_settings=simulation_settings, ) + # convert early_termination_target_error from kcal/mol to kT early_termination_target_error = ( settings_validation.convert_target_error_from_kcal_per_mole_to_kT( thermo_settings.temperature, - sampler_settings.early_termination_target_error, + simulation_settings.early_termination_target_error, ) ) - if sampler_settings.sampler_method.lower() == "repex": + if simulation_settings.sampler_method.lower() == "repex": sampler = _rfe_utils.multistate.HybridRepexSampler( mcmc_moves=integrator, - hybrid_system=hybrid_factory.hybrid_system, - hybrid_positions=hybrid_factory.hybrid_positions, + hybrid_system=system, + hybrid_positions=positions, online_analysis_interval=rta_its, online_analysis_target_error=early_termination_target_error, online_analysis_minimum_iterations=rta_min_its, ) - elif sampler_settings.sampler_method.lower() == "sams": + + elif simulation_settings.sampler_method.lower() == "sams": sampler = _rfe_utils.multistate.HybridSAMSSampler( mcmc_moves=integrator, - hybrid_system=hybrid_factory.hybrid_system, - hybrid_positions=hybrid_factory.hybrid_positions, + hybrid_system=system, + hybrid_positions=positions, online_analysis_interval=rta_its, online_analysis_minimum_iterations=rta_min_its, - flatness_criteria=sampler_settings.sams_flatness_criteria, - gamma0=sampler_settings.sams_gamma0, + flatness_criteria=simulation_settings.sams_flatness_criteria, + gamma0=simulation_settings.sams_gamma0, ) - elif sampler_settings.sampler_method.lower() == "independent": + + elif simulation_settings.sampler_method.lower() == "independent": sampler = _rfe_utils.multistate.HybridMultiStateSampler( mcmc_moves=integrator, - hybrid_system=hybrid_factory.hybrid_system, - hybrid_positions=hybrid_factory.hybrid_positions, + hybrid_system=system, + hybrid_positions=positions, online_analysis_interval=rta_its, online_analysis_target_error=early_termination_target_error, online_analysis_minimum_iterations=rta_min_its, ) + else: - raise AttributeError(f"Unknown sampler {sampler_settings.sampler_method}") + raise AttributeError(f"Unknown sampler {simulation_settings.sampler_method}") sampler.setup( - n_replicas=sampler_settings.n_replicas, + n_replicas=simulation_settings.n_replicas, reporter=reporter, lambda_protocol=lambdas, temperature=to_openmm(thermo_settings.temperature), @@ -543,63 +948,259 @@ def run( minimization_steps=100 if not dry else None, ) - try: - # Create context caches (energy + sampler) - energy_context_cache = openmmtools.cache.ContextCache( - capacity=None, - time_to_live=None, - platform=platform, - ) + # Get and set the context caches + sampler.energy_context_cache = openmmtools.cache.ContextCache( + capacity=None, + time_to_live=None, + platform=platform, + ) + sampler.sampler_context_cache = openmmtools.cache.ContextCache( + capacity=None, + time_to_live=None, + platform=platform, + ) - sampler_context_cache = openmmtools.cache.ContextCache( - capacity=None, - time_to_live=None, - platform=platform, + return sampler + + def _run_simulation( + self, + sampler: multistate.MultiStateSampler, + reporter: multistate.MultiStateReporter, + simulation_settings: MultiStateSimulationSettings, + integrator_settings: IntegratorSettings, + output_settings: MultiStateOutputSettings, + dry: bool, + ): + """ + Run the simulation. + + Parameters + ---------- + sampler : multistate.MultiStateSampler. + The sampler associated with the simulation to run. + reporter : multistate.MultiStateReporter + The reporter associated with the sampler. + simulation_settings : MultiStateSimulationSettings + Simulation control settings. + integrator_settings : IntegratorSettings + Integrator control settings. + output_settings : MultiStateOutputSettings + Simulation output control settings. + dry : bool + Whether or not to dry run the simulation. + + Returns + ------- + unit_results_dict : dict | None + A dictionary containing the free energy results to report. + ``None`` if it is a dry run. + """ + # Get the relevant simulation steps + mc_steps = settings_validation.convert_steps_per_iteration( + simulation_settings=simulation_settings, + integrator_settings=integrator_settings, + ) + + equil_steps = settings_validation.get_simsteps( + sim_length=simulation_settings.equilibration_length, + timestep=integrator_settings.timestep, + mc_steps=mc_steps, + ) + prod_steps = settings_validation.get_simsteps( + sim_length=simulation_settings.production_length, + timestep=integrator_settings.timestep, + mc_steps=mc_steps, + ) + + if not dry: # pragma: no-cover + # minimize + if self.verbose: + self.logger.info("minimizing systems") + + sampler.minimize(max_iterations=simulation_settings.minimization_steps) + + # equilibrate + if self.verbose: + self.logger.info("equilibrating systems") + + sampler.equilibrate(int(equil_steps / mc_steps)) + + # production + if self.verbose: + self.logger.info("running production phase") + + sampler.extend(int(prod_steps / mc_steps)) + + if self.verbose: + self.logger.info("production phase complete") + + if self.verbose: + self.logger.info("post-simulation result analysis") + + # calculate relevant analysis of the free energies & sampling + analyzer = multistate_analysis.MultistateEquilFEAnalysis( + reporter, + sampling_method=simulation_settings.sampler_method.lower(), + result_units=offunit.kilocalorie_per_mole, ) + analyzer.plot(filepath=self.shared_basepath, filename_prefix="") + analyzer.close() - sampler.energy_context_cache = energy_context_cache - sampler.sampler_context_cache = sampler_context_cache + return analyzer.unit_results_dict - if not dry: # pragma: no-cover - # minimize - if verbose: - self.logger.info("Running minimization") + else: + # We ran a dry simulation + # close reporter when you're done, prevent file handle clashes + reporter.close() - sampler.minimize(max_iterations=sampler_settings.minimization_steps) + # TODO: review this is likely no longer necessary + # clean up the reporter file + fns = [ + self.shared_basepath / output_settings.output_filename, + self.shared_basepath / output_settings.checkpoint_storage_filename, + ] + for fn in fns: + os.remove(fn) - # equilibrate - if verbose: - self.logger.info("Running equilibration phase") + return None - sampler.equilibrate(int(equil_steps / steps_per_iteration)) + def run( + self, *, dry=False, verbose=True, scratch_basepath=None, shared_basepath=None + ) -> dict[str, Any]: + """Run the relative free energy calculation. - # production - if verbose: - self.logger.info("Running production phase") + Parameters + ---------- + dry : bool + Do a dry run of the calculation, creating all necessary hybrid + system components (topology, system, sampler, etc...) but without + running the simulation. + verbose : bool + Verbose output of the simulation progress. Output is provided via + INFO level logging. + scratch_basepath: Pathlike, optional + Where to store temporary files, defaults to current working directory + shared_basepath : Pathlike, optional + Where to run the calculation, defaults to current working directory - sampler.extend(int(prod_steps / steps_per_iteration)) + Returns + ------- + dict + Outputs created in the basepath directory or the debug objects + (i.e. sampler) if ``dry==True``. - self.logger.info("Production phase complete") + Raises + ------ + error + Exception if anything failed + """ + # Prepare paths & verbosity + self._prepare(verbose, scratch_basepath, shared_basepath) - self.logger.info("Post-simulation analysis of results") - # calculate relevant analyses of the free energies & sampling - # First close & reload the reporter to avoid netcdf clashes - analyzer = multistate_analysis.MultistateEquilFEAnalysis( - reporter, - sampling_method=sampler_settings.sampler_method.lower(), - result_units=offunit.kilocalorie_per_mole, - ) - analyzer.plot(filepath=shared_basepath, filename_prefix="") - analyzer.close() - - else: - # clean up the reporter file - fns = [ - shared_basepath / output_settings.output_filename, - shared_basepath / output_settings.checkpoint_storage_filename, - ] - for fn in fns: - os.remove(fn) + # Get settings + settings = self._get_settings(self._inputs["protocol"].settings) + + # Get components + stateA = self._inputs["stateA"] + stateB = self._inputs["stateB"] + mapping = self._inputs["ligandmapping"] + alchem_comps, solvent_comp, protein_comp, small_mols = self._get_components(stateA, stateB) + + # Assign partial charges now to avoid any discrepancies later + self._assign_partial_charges(settings["charge_settings"], small_mols) + + ( + stateA_system, + stateA_topology, + stateA_positions, + stateB_system, + stateB_topology, + stateB_positions, + system_mappings, + ) = self._get_omm_objects( + stateA=stateA, + stateB=stateB, + mapping=mapping, + settings=settings, + protein_component=protein_comp, + solvent_component=solvent_comp, + small_mols=small_mols, + ) + + # Get the hybrid factory & system + hybrid_factory, hybrid_system = self._get_alchemical_system( + stateA_system=stateA_system, + stateA_positions=stateA_positions, + stateA_topology=stateA_topology, + stateB_system=stateB_system, + stateB_positions=stateB_positions, + stateB_topology=stateB_topology, + system_mappings=system_mappings, + alchemical_settings=settings["alchemical_settings"], + ) + + # Subselect system based on user inputs & write initial PDB + selection_indices = self._subsample_topology( + hybrid_topology=hybrid_factory.hybrid_topology, + hybrid_positions=hybrid_factory.hybrid_positions, + output_selection=settings["output_settings"].output_indices, + output_filename=settings["output_settings"].output_structure, + atom_classes=hybrid_factory._atom_classes, + ) + + # Get the lambda schedule + # TODO - this should be better exposed to users + lambdas = _rfe_utils.lambdaprotocol.LambdaProtocol( + functions=settings["lambda_settings"].lambda_functions, + windows=settings["lambda_settings"].lambda_windows, + ) + + # Get the compute platform + restrict_cpu = settings["forcefield_settings"].nonbonded_method.lower() == "nocutoff" + platform = omm_compute.get_openmm_platform( + platform_name=settings["engine_settings"].compute_platform, + gpu_device_index=settings["engine_settings"].gpu_device_index, + restrict_cpu_count=restrict_cpu, + ) + + # Get the integrator + integrator = self._get_integrator( + integrator_settings=settings["integrator_settings"], + simulation_settings=settings["simulation_settings"], + system=hybrid_system, + ) + + try: + # get the reporter + reporter = self._get_reporter( + storage_path=self.shared_basepath, + selection_indices=selection_indices, + output_settings=settings["output_settings"], + simulation_settings=settings["simulation_settings"], + ) + + # Get sampler + sampler = self._get_sampler( + system=hybrid_system, + positions=hybrid_factory.hybrid_positions, + lambdas=lambdas, + integrator=integrator, + reporter=reporter, + simulation_settings=settings["simulation_settings"], + thermo_settings=settings["thermo_settings"], + alchem_settings=settings["alchemical_settings"], + platform=platform, + dry=dry, + ) + + unit_results_dict = self._run_simulation( + sampler=sampler, + reporter=reporter, + simulation_settings=settings["simulation_settings"], + integrator_settings=settings["integrator_settings"], + output_settings=settings["output_settings"], + dry=dry, + ) finally: # close reporter when you're done, prevent # file handle clashes @@ -608,23 +1209,33 @@ def run( # clear GPU contexts # TODO: use cache.empty() calls when openmmtools #690 is resolved # replace with above - for context in list(energy_context_cache._lru._data.keys()): - del energy_context_cache._lru._data[context] - for context in list(sampler_context_cache._lru._data.keys()): - del sampler_context_cache._lru._data[context] + for context in list(sampler.energy_context_cache._lru._data.keys()): + del sampler.energy_context_cache._lru._data[context] + for context in list(sampler.sampler_context_cache._lru._data.keys()): + del sampler.sampler_context_cache._lru._data[context] # cautiously clear out the global context cache too for context in list(openmmtools.cache.global_context_cache._lru._data.keys()): del openmmtools.cache.global_context_cache._lru._data[context] - del sampler_context_cache, energy_context_cache + del sampler.sampler_context_cache, sampler.energy_context_cache if not dry: del integrator, sampler if not dry: # pragma: no-cover - return {"nc": nc, "last_checkpoint": chk, **analyzer.unit_results_dict} + nc = self.shared_basepath / settings["output_settings"].output_filename + chk = settings["output_settings"].checkpoint_storage_filename + unit_results_dict["nc"] = nc + unit_results_dict["last_checkpoint"] = chk + unit_results_dict["selection_indices"] = selection_indices + return unit_results_dict else: - return {"debug": {"sampler": sampler, "hybrid_factory": hybrid_factory}} + return { + "debug": { + "sampler": sampler, + "hybrid_factory": hybrid_factory, + } + } @staticmethod def structural_analysis(scratch, shared) -> dict: From 83cedc57f77a50a18437ff51676325513a4d5e57 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Thu, 8 Jan 2026 18:55:19 -0500 Subject: [PATCH 27/47] Have two separate system generators for state A & B in hybrid protocol (#1772) Make it so that the states A and B are generated using two different SystemGenerator objects, allowing for partial charge transformations. --- news/issue-1120.rst | 25 ++++++ .../openmm_rfe/hybridtop_protocols.py | 15 ++-- .../protocols/openmm_rfe/hybridtop_units.py | 89 +++++++++---------- .../openmm_rfe/test_hybrid_top_protocol.py | 65 ++++++++++++++ .../openmm_rfe/test_hybrid_top_validation.py | 26 ++++-- 5 files changed, 157 insertions(+), 63 deletions(-) create mode 100644 news/issue-1120.rst diff --git a/news/issue-1120.rst b/news/issue-1120.rst new file mode 100644 index 000000000..55c8286b7 --- /dev/null +++ b/news/issue-1120.rst @@ -0,0 +1,25 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Endstates in the RelativeHybridTopologyProtocol are now being created + in a manner that allows for isomorphic molecules that differ between + endstates to have different parameters (Issue #1120). + +**Security:** + +* diff --git a/openfe/protocols/openmm_rfe/hybridtop_protocols.py b/openfe/protocols/openmm_rfe/hybridtop_protocols.py index a78a81099..4daabd08f 100644 --- a/openfe/protocols/openmm_rfe/hybridtop_protocols.py +++ b/openfe/protocols/openmm_rfe/hybridtop_protocols.py @@ -322,12 +322,11 @@ def _validate_smcs( ------ ValueError * If there are isomorphic SmallMoleculeComponents with - different charges. + different charges within a given ChemicalSystem. """ smcs_A = stateA.get_components_of_type(SmallMoleculeComponent) smcs_B = stateB.get_components_of_type(SmallMoleculeComponent) smcs_all = list(set(smcs_A).union(set(smcs_B))) - offmols = [m.to_openff() for m in smcs_all] def _equal_charges(moli, molj): # Base case, both molecules don't have charges @@ -341,11 +340,13 @@ def _equal_charges(moli, molj): clashes = [] - for i, moli in enumerate(offmols): - for molj in offmols: - if moli.is_isomorphic_with(molj): - if not _equal_charges(moli, molj): - clashes.append(smcs_all[i]) + for smcs in [smcs_A, smcs_B]: + offmols = [m.to_openff() for m in smcs] + for i, moli in enumerate(offmols): + for molj in offmols: + if moli.is_isomorphic_with(molj): + if not _equal_charges(moli, molj): + clashes.append(smcs[i]) if len(clashes) > 0: errmsg = ( diff --git a/openfe/protocols/openmm_rfe/hybridtop_units.py b/openfe/protocols/openmm_rfe/hybridtop_units.py index 03102435f..611a75b59 100644 --- a/openfe/protocols/openmm_rfe/hybridtop_units.py +++ b/openfe/protocols/openmm_rfe/hybridtop_units.py @@ -257,10 +257,10 @@ def _assign_partial_charges( @staticmethod def _get_system_generator( - shared_basepath: pathlib.Path, settings: dict[str, SettingsBaseModel], - solvent_comp: SolventComponent | None, + solvent_component: SolventComponent | None, openff_molecules: list[OFFMolecule] | None, + ffcache: pathlib.Path | None, ) -> SystemGenerator: """ Get an OpenMM SystemGenerator. @@ -269,48 +269,34 @@ def _get_system_generator( ---------- settings : dict[str, SettingsBaseModel] A dictionary of protocol settings. - solvent_comp : SolventComponent | None + solvent_component : SolventComponent | None The solvent component of the system, if any. - openff_molecules : list[openff.Toolkit] | None + openff_molecules : list[openff.toolkit.Molecule] | None A list of openff molecules to generate templates for, if any. + ffcache : pathlib.Path | None + Path to the force field parameter cache. Returns ------- system_generator : openmmtools.SystemGenerator The SystemGenerator for the protocol. """ - ffcache = settings["output_settings"].forcefield_cache - - if ffcache is not None: - ffcache = shared_basepath / ffcache - - # Block out oechem backend in system_generator calls to avoid - # any issues with smiles roundtripping between rdkit and oechem - with without_oechem_backend(): - system_generator = system_creation.get_system_generator( - forcefield_settings=settings["forcefield_settings"], - integrator_settings=settings["integrator_settings"], - thermo_settings=settings["thermo_settings"], - cache=ffcache, - has_solvent=solvent_comp is not None, - ) - - # Handle openff Molecule templates - # TODO: revisit this once the SystemGenerator update happens - # and we start loading the whole protein into OpenFF Topologies - - # First deduplicate isomorphic molecules, if there are any - if openff_molecules is None: - return system_generator + system_generator = system_creation.get_system_generator( + forcefield_settings=settings["forcefield_settings"], + integrator_settings=settings["integrator_settings"], + thermo_settings=settings["thermo_settings"], + cache=ffcache, + has_solvent=solvent_component is not None, + ) - unique_offmols: list[OFFMolecule] = [] - for mol in openff_molecules: - unique = all([not mol.is_isomorphic_with(umol) for umol in unique_offmols]) - if unique: - unique_offmols.append(mol) + # Handle openff Molecule templates + # TODO: revisit this once the SystemGenerator update happens + # and we start loading the whole protein into OpenFF Topologies + if openff_molecules is None: + return system_generator - # register all the templates - system_generator.add_molecules(unique_offmols) + # Register all the templates, pass unique molecules to avoid clashes + system_generator.add_molecules(list(set(openff_molecules))) return system_generator @@ -528,38 +514,43 @@ def _get_omm_objects( def _filter_small_mols(smols, state): return {smc: offmol for smc, offmol in smols.items() if state.contains(smc)} - small_mols_stateA = _filter_small_mols(small_mols, stateA) - small_mols_stateB = _filter_small_mols(small_mols, stateB) + states_inputs = { + "A": {"state": stateA, "mols": _filter_small_mols(small_mols, stateA)}, + "B": {"state": stateB, "mols": _filter_small_mols(small_mols, stateB)}, + } # Everything involving systemgenerator handling has a risk of # oechem <-> rdkit smiles conversion clashes, cautiously ban it. with without_oechem_backend(): - # TODO: get two generators, one for state A and one for stateB - # See issue #1120 - # Get the system generator with all the templates registered - system_generator = self._get_system_generator( - shared_basepath=self.shared_basepath, - settings=settings, - solvent_comp=solvent_component, - openff_molecules=list(small_mols.values()), - ) + # Get the system generators with all the templates registered + for state in ["A", "B"]: + ffcache = settings["output_settings"].forcefield_cache + if ffcache is not None: + ffcache = self.shared_basepath / (f"{state}_" + ffcache) + + states_inputs[state]["generator"] = self._get_system_generator( + settings=settings, + solvent_component=solvent_component, + openff_molecules=list(states_inputs[state]["mols"].values()), + ffcache=ffcache, + ) (stateA_system, stateA_topology, stateA_positions, comp_resids) = ( self._create_stateA_system( - small_mols=small_mols_stateA, + small_mols=states_inputs["A"]["mols"], protein_component=protein_component, solvent_component=solvent_component, - system_generator=system_generator, + system_generator=states_inputs["A"]["generator"], solvation_settings=settings["solvation_settings"], ) ) (stateB_system, stateB_topology, stateB_alchem_resids) = self._create_stateB_system( - small_mols=small_mols_stateB, + small_mols=states_inputs["B"]["mols"], mapping=mapping, stateA_topology=stateA_topology, exclude_resids=comp_resids[mapping.componentA], - system_generator=system_generator, + system_generator=states_inputs["B"]["generator"], ) # Get the mapping between the two systems diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py index 60045bcc0..99e577cdb 100644 --- a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py @@ -710,6 +710,71 @@ def test_dry_run_charge_backends( np.testing.assert_allclose(c, ref, rtol=1e-4) +def test_dry_run_same_mol_different_charges(benzene_modifications, vac_settings, tmpdir): + """ + Issue #1120 - make sure we can do an RFE of a system with different + parameters but the same molecule. + """ + protocol = openmm_rfe.RelativeHybridTopologyProtocol(settings=vac_settings) + + benzene_offmol = benzene_modifications["benzene"].to_openff() + # Give state A some gasteiger charges + benzene_offmol.assign_partial_charges(partial_charge_method="gasteiger") + stateA_charges = copy.deepcopy(benzene_offmol.partial_charges) + stateA_mol = openfe.SmallMoleculeComponent.from_openff(benzene_offmol) + + # Give state B gasteiger charges scaled by 0.9 + benzene_offmol.partial_charges *= 0.9 + stateB_charges = copy.deepcopy(benzene_offmol.partial_charges) + stateB_mol = openfe.SmallMoleculeComponent.from_openff(benzene_offmol) + + # Create new mapping + mapping = gufe.LigandAtomMapping( + componentA=stateA_mol, + componentB=stateB_mol, + componentA_to_componentB={i: i for i in range(12)}, + ) + + # create DAG from protocol and take first (and only) work unit from within + dag = protocol.create( + stateA=openfe.ChemicalSystem({"l": stateA_mol}), + stateB=openfe.ChemicalSystem({"l": stateB_mol}), + mapping=mapping, + ) + dag_unit = list(dag.protocol_units)[0] + + with tmpdir.as_cwd(): + debug = dag_unit.run(dry=True)["debug"] + sampler = debug["sampler"] + htf = debug["hybrid_factory"] + hybrid_system = sampler._hybrid_system + + # get the standard nonbonded force + nonbond = [f for f in hybrid_system.getForces() if isinstance(f, NonbondedForce)] + + # get the particle parameters & offsets + for i in range(hybrid_system.getNumParticles()): + # All particles should be core atoms + assert i in htf._atom_classes["core_atoms"] + + # offsets + offset = ensure_quantity(nonbond[0].getParticleParameterOffset(i)[2], "openff") + + # parameters + c, s, e = nonbond[0].getParticleParameters(i) + c = ensure_quantity(c, "openff") + + # check state A charge + assert pytest.approx(c) == stateA_charges[i] + + # check state B charge + c_diff = stateB_charges[i] - stateA_charges[i] + assert pytest.approx(offset) == c_diff + + # check that the offset value is non-zero + assert abs(offset) > 0 * offset.units + + @pytest.mark.flaky(reruns=3) # bad minimisation can happen def test_dry_run_user_charges(benzene_modifications, vac_settings, tmpdir): """ diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py index 35ef57da0..a48fc755f 100644 --- a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py @@ -151,19 +151,14 @@ def test_smcs_different_charges_none_not_none(benzene_modifications): offmol.assign_partial_charges(partial_charge_method="gasteiger") smcB = openfe.SmallMoleculeComponent.from_openff(offmol) - stateA = openfe.ChemicalSystem({"l": smcA}) - stateB = openfe.ChemicalSystem({"l": smcB}) + state = openfe.ChemicalSystem({"a": smcA, "b": smcB}) errmsg = "isomorphic but with different charges" with pytest.raises(ValueError, match=errmsg): - openmm_rfe.RelativeHybridTopologyProtocol._validate_smcs(stateA, stateB) + openmm_rfe.RelativeHybridTopologyProtocol._validate_smcs(state, state) def test_smcs_different_charges_all(benzene_modifications): - # For this test, we will assign both A and B to both states - # It wouldn't happen in real life, but it tests that within a state - # you can pick up isomorphic molecules with different charges - # create an offmol with gasteiger charges offmol = benzene_modifications["benzene"].to_openff() offmol.assign_partial_charges(partial_charge_method="gasteiger") smcA = openfe.SmallMoleculeComponent.from_openff(offmol) @@ -179,6 +174,23 @@ def test_smcs_different_charges_all(benzene_modifications): openmm_rfe.RelativeHybridTopologyProtocol._validate_smcs(state, state) +def test_smcs_different_charges_different_endstates(benzene_modifications): + # This should just pass, the charge is different but only + # in the end states - which is an acceptable transformation. + offmol = benzene_modifications["benzene"].to_openff() + offmol.assign_partial_charges(partial_charge_method="gasteiger") + smcA = openfe.SmallMoleculeComponent.from_openff(offmol) + + # now alter the offmol charges, scaling by 0.1 + offmol.partial_charges *= 0.1 + smcB = openfe.SmallMoleculeComponent.from_openff(offmol) + + stateA = openfe.ChemicalSystem({"l": smcA}) + stateB = openfe.ChemicalSystem({"l": smcB}) + + openmm_rfe.RelativeHybridTopologyProtocol._validate_smcs(stateA, stateB) + + def test_solvent_nocutoff_error( benzene_system, toluene_system, From d85e0ec1d98a39f56a2c17ff5889467596cf30e3 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Thu, 8 Jan 2026 19:06:15 -0500 Subject: [PATCH 28/47] HybridTop structural analysis via API rather than CLI (#1771) * change structural analysis from using CLI to using API --- .../protocols/openmm_rfe/hybridtop_units.py | 78 ++++++++++++------- .../openmm_rfe/test_hybrid_top_protocol.py | 2 +- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/openfe/protocols/openmm_rfe/hybridtop_units.py b/openfe/protocols/openmm_rfe/hybridtop_units.py index 611a75b59..1d7965b32 100644 --- a/openfe/protocols/openmm_rfe/hybridtop_units.py +++ b/openfe/protocols/openmm_rfe/hybridtop_units.py @@ -10,7 +10,6 @@ the Perses toolkit (https://github.com/choderalab/perses). """ -import json import logging import os import pathlib @@ -1229,42 +1228,62 @@ def run( } @staticmethod - def structural_analysis(scratch, shared) -> dict: - # don't put energy analysis in here, it uses the open file reporter - # whereas structural stuff requires that the file handle is closed - # TODO: we should just make openfe_analysis write an npz instead! - analysis_out = scratch / "structural_analysis.json" - - ret = subprocess.run( - [ - "openfe_analysis", # CLI entry point - "RFE_analysis", # CLI option - str(shared), # Where the simulation.nc fille - str(analysis_out), # Where the analysis json file is written - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - if ret.returncode: - return {"structural_analysis_error": ret.stderr} + def structural_analysis( + scratch: pathlib.Path, + shared: pathlib.Path, + pdb_filename: str, + trj_filename: str, + ) -> dict[str, str | pathlib.Path]: + """ + Run structural analysis using ``openfe-analysis``. + + Parameters + ---------- + scratch : pathlib.Path + Path to the scratch directory. + shared : pathlib.Path + Path to the shared directory. + pdb_filename : str + The PDB file name. + trj_filename : str + The trajectory file name. - with open(analysis_out, "rb") as f: - data = json.load(f) + Returns + ------- + dict[str, str | pathlib.Path] + Dictionary containing either the path to the NPZ + file with the structural data, or the analysis error. - savedir = pathlib.Path(shared) + Notes + ----- + Don't put energy analysis here, it uses the open file reporter + whereas structural stuff requires the file handle to be closed. + """ + from openfe_analysis import rmsd + + pdb_file = shared / pdb_filename + trj_file = shared / trj_filename + + try: + data = rmsd.gather_rms_data(pdb_file, trj_file) + # TODO: change this to more specific exception types + except Exception as e: + return {"structural_analysis_error": str(e)} + + # Generate plots if d := data["protein_2D_RMSD"]: fig = plotting.plot_2D_rmsd(d) - fig.savefig(savedir / "protein_2D_RMSD.png") + fig.savefig(shared / "protein_2D_RMSD.png") plt.close(fig) f2 = plotting.plot_ligand_COM_drift(data["time(ps)"], data["ligand_wander"]) - f2.savefig(savedir / "ligand_COM_drift.png") + f2.savefig(shared / "ligand_COM_drift.png") plt.close(f2) f3 = plotting.plot_ligand_RMSD(data["time(ps)"], data["ligand_RMSD"]) - f3.savefig(savedir / "ligand_RMSD.png") + f3.savefig(shared / "ligand_RMSD.png") plt.close(f3) - # Save to numpy compressed format (~ 6x more space efficient than JSON) + # Write out NPZ with the analyzed data np.savez_compressed( shared / "structural_analysis.npz", protein_RMSD=np.asarray(data["protein_RMSD"], dtype=np.float32), @@ -1285,7 +1304,12 @@ def _execute( outputs = self.run(scratch_basepath=ctx.scratch, shared_basepath=ctx.shared) - structural_analysis_outputs = self.structural_analysis(ctx.scratch, ctx.shared) + structural_analysis_outputs = self.structural_analysis( + scratch=ctx.scratch, + shared=ctx.shared, + pdb_filename=self._inputs["protocol"].settings.output_settings.output_structure, + trj_filename=self._inputs["protocol"].settings.output_settings.output_filename, + ) return { "repeat_id": self._inputs["repeat_id"], diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py index 99e577cdb..b98bc949f 100644 --- a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py @@ -1981,7 +1981,7 @@ def test_dry_run_complex_alchemwater_totcharge( def test_structural_analysis_error(tmpdir): with tmpdir.as_cwd(): ret = openmm_rfe.RelativeHybridTopologyProtocolUnit.structural_analysis( - Path("."), Path(".") + Path("."), Path("."), "foo", "bar" ) assert "structural_analysis_error" in ret From e93b9f662d9d490a45b165e5fcec57b166663576 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:06:55 -0800 Subject: [PATCH 29/47] making some language clearer on github-facing things (#1758) * making some language clearer on github-facing things * : * add pre-commit link * clean up --- .github/pull_request_template.md | 2 +- .github/workflows/test-example-notebooks.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 37742b463..83beb5427 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,7 +14,7 @@ see https://regro.github.io/rever-docs/news.html for details on how to add news Checklist * [ ] All new code is appropriately documented (user-facing code _must_ have complete docstrings). * [ ] Added a ``news`` entry, or the changes are not user-facing. -* [ ] Ran pre-commit by making a comment with `pre-commit.ci autofix` before requesting review. +* [ ] Ran pre-commit: you can run [pre-commit](https://pre-commit.com) locally or comment on this PR with `pre-commit.ci autofix`. Manual Tests: these are slow so don't need to be run every commit, only before merging and when relevant changes are made (generally at reviewer-discretion). * [ ] [GPU integration tests](https://github.com/OpenFreeEnergy/openfe/actions/workflows/gpu-integration-tests.yaml) diff --git a/.github/workflows/test-example-notebooks.yaml b/.github/workflows/test-example-notebooks.yaml index fce82d422..df27c1fb7 100644 --- a/.github/workflows/test-example-notebooks.yaml +++ b/.github/workflows/test-example-notebooks.yaml @@ -12,7 +12,7 @@ defaults: shell: bash -leo pipefail {0} jobs: - test-conda-build: + test-example-notebooks: runs-on: ubuntu-latest steps: From 1bc6abe664e4a1bd2e732caa378def6ea9cc29b3 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Thu, 15 Jan 2026 19:41:03 +0000 Subject: [PATCH 30/47] Moving AFE Protocols around a bit (#1775) * move the protocols results to a single file and deduplicate * rename base units file * move units out of method files * move a few things in init --- openfe/protocols/openmm_afe/__init__.py | 18 +- openfe/protocols/openmm_afe/abfe_units.py | 466 ++++++++++ .../openmm_afe/afe_protocol_results.py | 546 +++++++++++ openfe/protocols/openmm_afe/ahfe_units.py | 174 ++++ .../openmm_afe/{base.py => base_afe_units.py} | 13 +- .../openmm_afe/equil_binding_afe_method.py | 866 +----------------- .../openmm_afe/equil_solvation_afe_method.py | 493 +--------- .../openmm_abfe/test_abfe_protocol.py | 11 + 8 files changed, 1243 insertions(+), 1344 deletions(-) create mode 100644 openfe/protocols/openmm_afe/abfe_units.py create mode 100644 openfe/protocols/openmm_afe/afe_protocol_results.py create mode 100644 openfe/protocols/openmm_afe/ahfe_units.py rename openfe/protocols/openmm_afe/{base.py => base_afe_units.py} (99%) diff --git a/openfe/protocols/openmm_afe/__init__.py b/openfe/protocols/openmm_afe/__init__.py index 48919cd0c..314179c56 100644 --- a/openfe/protocols/openmm_afe/__init__.py +++ b/openfe/protocols/openmm_afe/__init__.py @@ -5,19 +5,25 @@ """ -from .equil_binding_afe_method import ( +from .abfe_units import ( AbsoluteBindingComplexUnit, - AbsoluteBindingProtocol, + AbsoluteBindingSolventUnit, +) +from .afe_protocol_results import ( AbsoluteBindingProtocolResult, + AbsoluteSolvationProtocolResult, +) +from .ahfe_units import ( + AbsoluteSolvationSolventUnit, + AbsoluteSolvationVacuumUnit, +) +from .equil_binding_afe_method import ( + AbsoluteBindingProtocol, AbsoluteBindingSettings, - AbsoluteBindingSolventUnit, ) from .equil_solvation_afe_method import ( AbsoluteSolvationProtocol, - AbsoluteSolvationProtocolResult, AbsoluteSolvationSettings, - AbsoluteSolvationSolventUnit, - AbsoluteSolvationVacuumUnit, ) __all__ = [ diff --git a/openfe/protocols/openmm_afe/abfe_units.py b/openfe/protocols/openmm_afe/abfe_units.py new file mode 100644 index 000000000..0cad0bb66 --- /dev/null +++ b/openfe/protocols/openmm_afe/abfe_units.py @@ -0,0 +1,466 @@ +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +"""ABFE Protocol Units --- :mod:`openfe.protocols.openmm_afe.abfe_units` +======================================================================== +This module defines the ProtocolUnits for the +:class:`AbsoluteBindingProtocol`. +""" + +import logging +import pathlib + +import MDAnalysis as mda +import numpy as np +import numpy.typing as npt +from gufe import ( + SolventComponent, +) +from gufe.components import Component +from openff.units import Quantity +from openff.units.openmm import to_openmm +from openmm import System +from openmm import unit as ommunit +from openmm.app import Topology as omm_topology +from openmmtools.states import GlobalParameterState, ThermodynamicState +from rdkit import Chem + +from openfe.protocols.openmm_afe.equil_afe_settings import ( + BoreschRestraintSettings, + SettingsBaseModel, +) +from openfe.protocols.openmm_utils import system_validation +from openfe.protocols.restraint_utils import geometry +from openfe.protocols.restraint_utils.geometry.boresch import BoreschRestraintGeometry +from openfe.protocols.restraint_utils.openmm import omm_restraints +from openfe.protocols.restraint_utils.openmm.omm_restraints import BoreschRestraint + +from .base_afe_units import BaseAbsoluteUnit + +logger = logging.getLogger(__name__) + + +class AbsoluteBindingComplexUnit(BaseAbsoluteUnit): + """ + Protocol Unit for the complex phase of an absolute binding free energy + """ + + simtype = "complex" + + def _get_components(self): + """ + Get the relevant components for a complex transformation. + + Returns + ------- + alchem_comps : dict[str, Component] + A dict of alchemical components + solv_comp : SolventComponent + The SolventComponent of the system + prot_comp : ProteinComponent | None + The protein component of the system, if it exists. + small_mols : dict[SmallMoleculeComponent: OFFMolecule] + SmallMoleculeComponents to add to the system. + """ + stateA = self._inputs["stateA"] + alchem_comps = self._inputs["alchemical_components"] + + solv_comp, prot_comp, small_mols = system_validation.get_components(stateA) + off_comps = {m: m.to_openff() for m in small_mols} + + # We don't need to check that solv_comp is not None, otherwise + # an error will have been raised when calling `validate_solvent` + # in the Protocol's `_create`. + # Similarly we don't need to check prot_comp + return alchem_comps, solv_comp, prot_comp, off_comps + + def _handle_settings(self) -> dict[str, SettingsBaseModel]: + """ + Extract the relevant settings for a complex transformation. + + Returns + ------- + settings : dict[str, SettingsBaseModel] + A dictionary with the following entries: + * forcefield_settings : OpenMMSystemGeneratorFFSettings + * thermo_settings : ThermoSettings + * charge_settings : OpenFFPartialChargeSettings + * solvation_settings : OpenMMSolvationSettings + * alchemical_settings : AlchemicalSettings + * lambda_settings : LambdaSettings + * engine_settings : OpenMMEngineSettings + * integrator_settings : IntegratorSettings + * equil_simulation_settings : MDSimulationSettings + * equil_output_settings : ABFEPreEquilOutputSettings + * simulation_settings : SimulationSettings + * output_settings: MultiStateOutputSettings + * restraint_settings: BaseRestraintSettings + """ + prot_settings = self._inputs["protocol"].settings + + settings = {} + settings["forcefield_settings"] = prot_settings.forcefield_settings + settings["thermo_settings"] = prot_settings.thermo_settings + settings["charge_settings"] = prot_settings.partial_charge_settings + settings["solvation_settings"] = prot_settings.complex_solvation_settings + settings["alchemical_settings"] = prot_settings.alchemical_settings + settings["lambda_settings"] = prot_settings.complex_lambda_settings + settings["engine_settings"] = prot_settings.engine_settings + settings["integrator_settings"] = prot_settings.integrator_settings + settings["equil_simulation_settings"] = prot_settings.complex_equil_simulation_settings + settings["equil_output_settings"] = prot_settings.complex_equil_output_settings + settings["simulation_settings"] = prot_settings.complex_simulation_settings + settings["output_settings"] = prot_settings.complex_output_settings + settings["restraint_settings"] = prot_settings.restraint_settings + + return settings + + @staticmethod + def _get_mda_universe( + topology: omm_topology, + positions: ommunit.Quantity | None, + trajectory: pathlib.Path | None, + ) -> mda.Universe: + """ + Helper method to get a Universe from an openmm Topology, + and either an input trajectory or a set of positions. + + Parameters + ---------- + topology : openmm.app.Topology + An OpenMM Topology that defines the System. + positions: openmm.unit.Quantity | None + The System's current positions. + Used if a trajectory file is None or is not a file. + trajectory: pathlib.Path | None + A Path to a trajectory file to read positions from. + + Returns + ------- + mda.Universe + An MDAnalysis Universe of the System. + """ + from MDAnalysis.coordinates.memory import MemoryReader + + # If the trajectory file doesn't exist, then we use positions + if trajectory is not None and trajectory.is_file(): + return mda.Universe( + topology, + trajectory, + topology_format="OPENMMTOPOLOGY", + ) + else: + if positions is None: + raise ValueError("No positions to create the Universe with") + + # Positions is an openmm Quantity in nm we need + # to convert to angstroms + return mda.Universe( + topology, + np.array(positions._value) * 10, + topology_format="OPENMMTOPOLOGY", + trajectory_format=MemoryReader, + ) + + @staticmethod + def _get_idxs_from_residxs( + topology: omm_topology, + residxs: list[int], + ) -> list[int]: + """ + Helper method to get the a list of atom indices which belong to a list + of residues. + + Parameters + ---------- + topology : openmm.app.Topology + An OpenMM Topology that defines the System. + residxs : list[int] + A list of residue numbers who's atoms we should get atom indices. + + Returns + ------- + atom_ids : list[int] + A list of atom indices. + + TODO + ---- + * Check how this works when we deal with virtual sites. + """ + atom_ids = [] + + for r in topology.residues(): + if r.index in residxs: + atom_ids.extend([at.index for at in r.atoms()]) + + return atom_ids + + @staticmethod + def _get_boresch_restraint( + universe: mda.Universe, + guest_rdmol: Chem.Mol, + guest_atom_ids: list[int], + host_atom_ids: list[int], + temperature: Quantity, + settings: BoreschRestraintSettings, + ) -> tuple[BoreschRestraintGeometry, BoreschRestraint]: + """ + Get a Boresch-like restraint Geometry and OpenMM restraint force + supplier. + + Parameters + ---------- + universe : mda.Universe + An MDAnalysis Universe defining the system to get the restraint for. + guest_rdmol : Chem.Mol + An RDKit Molecule defining the guest molecule in the system. + guest_atom_ids: list[int] + A list of atom indices defining the guest molecule in the universe. + host_atom_ids : list[int] + A list of atom indices defining the host molecules in the universe. + temperature : openff.units.Quantity + The temperature of the simulation where the restraint will be added. + settings : BoreschRestraintSettings + Settings on how the Boresch-like restraint should be defined. + + Returns + ------- + geom : BoreschRestraintGeometry + A class defining the Boresch-like restraint. + restraint : BoreschRestraint + A factory class for generating Boresch restraints in OpenMM. + """ + # Take the minimum of the two possible force constants to check against + frc_const = min(settings.K_thetaA, settings.K_thetaB) + + geom = geometry.boresch.find_boresch_restraint( + universe=universe, + guest_rdmol=guest_rdmol, + guest_idxs=guest_atom_ids, + host_idxs=host_atom_ids, + host_selection=settings.host_selection, + anchor_finding_strategy=settings.anchor_finding_strategy, + dssp_filter=settings.dssp_filter, + rmsf_cutoff=settings.rmsf_cutoff, + host_min_distance=settings.host_min_distance, + host_max_distance=settings.host_max_distance, + angle_force_constant=frc_const, + temperature=temperature, + ) + + restraint = omm_restraints.BoreschRestraint(settings) + return geom, restraint + + def _add_restraints( + self, + system: System, + topology: omm_topology, + positions: ommunit.Quantity, + alchem_comps: dict[str, list[Component]], + comp_resids: dict[Component, npt.NDArray], + settings: dict[str, SettingsBaseModel], + ) -> tuple[ + GlobalParameterState, + Quantity, + System, + geometry.HostGuestRestraintGeometry, + ]: + """ + Find and add restraints to the OpenMM System. + + Notes + ----- + Currently, only Boresch-like restraints are supported. + + Parameters + ---------- + system : openmm.System + The System to add the restraint to. + topology : openmm.app.Topology + An OpenMM Topology that defines the System. + positions: openmm.unit.Quantity + The System's current positions. + Used if a trajectory file isn't found. + alchem_comps: dict[str, list[Component]] + A dictionary with a list of alchemical components + in both state A and B. + comp_resids: dict[Component, npt.NDArray] + A dictionary keyed by each Component in the System + which contains arrays with the residue indices that is contained + by that Component. + settings : dict[str, SettingsBaseModel] + A dictionary of settings that defines how to find and set + the restraint. + + Returns + ------- + restraint_parameter_state : RestraintParameterState + A RestraintParameterState object that defines the control + parameter for the restraint. + correction : openff.units.Quantity + The standard state correction for the restraint. + system : openmm.System + A copy of the System with the restraint added. + rest_geom : geometry.HostGuestRestraintGeometry + The restraint Geometry object. + """ + if self.verbose: + self.logger.info("Generating restraints") + + # Get the guest rdmol + guest_rdmol = alchem_comps["stateA"][0].to_rdkit() + + # sanitize the rdmol if possible - warn if you can't + err = Chem.SanitizeMol(guest_rdmol, catchErrors=True) + + if err: + msg = "restraint generation: could not sanitize ligand rdmol" + logger.warning(msg) + + # Get the guest idxs + # concatenate a list of residue indexes for all alchemical components + residxs = np.concatenate([comp_resids[key] for key in alchem_comps["stateA"]]) + + # get the alchemicical atom ids + guest_atom_ids = self._get_idxs_from_residxs(topology, residxs) + + # Now get the host idxs + # We assume this is everything but the alchemical component + # and the solvent. + solv_comps = [c for c in comp_resids if isinstance(c, SolventComponent)] + exclude_comps = [alchem_comps["stateA"]] + solv_comps + residxs = np.concatenate([v for i, v in comp_resids.items() if i not in exclude_comps]) + + host_atom_ids = self._get_idxs_from_residxs(topology, residxs) + + # Finally create an MDAnalysis Universe + # We try to pass the equilibration production file path through + # In some cases (debugging / dry runs) this won't be available + # so we'll default to using input positions. + univ = self._get_mda_universe( + topology, + positions, + self.shared_basepath / settings["equil_output_settings"].production_trajectory_filename, + ) + + if isinstance(settings["restraint_settings"], BoreschRestraintSettings): + rest_geom, restraint = self._get_boresch_restraint( + univ, + guest_rdmol, + guest_atom_ids, + host_atom_ids, + settings["thermo_settings"].temperature, + settings["restraint_settings"], + ) + else: + # TODO turn this into a direction for different restraint types supported? + raise NotImplementedError("Other restraint types are not yet available") + + if self.verbose: + self.logger.info(f"restraint geometry is: {rest_geom}") + + # We need a temporary thermodynamic state to add the restraint + # & get the correction + thermodynamic_state = ThermodynamicState( + system, + temperature=to_openmm(settings["thermo_settings"].temperature), + pressure=to_openmm(settings["thermo_settings"].pressure), + ) + + # Add the force to the thermodynamic state + restraint.add_force( + thermodynamic_state, + rest_geom, + controlling_parameter_name="lambda_restraints", + ) + # Get the standard state correction as a unit.Quantity + correction = restraint.get_standard_state_correction( + thermodynamic_state, + rest_geom, + ) + + # Get the GlobalParameterState for the restraint + restraint_parameter_state = omm_restraints.RestraintParameterState(lambda_restraints=1.0) + return ( + restraint_parameter_state, + correction, + # Remove the thermostat, otherwise you'll get an + # Andersen thermostat by default! + thermodynamic_state.get_system(remove_thermostat=True), + rest_geom, + ) + + +class AbsoluteBindingSolventUnit(BaseAbsoluteUnit): + """ + Protocol Unit for the solvent phase of an absolute binding free energy + """ + + simtype = "solvent" + + def _get_components(self): + """ + Get the relevant components for a solvent transformation. + + Returns + ------- + alchem_comps : dict[str, Component] + A list of alchemical components + solv_comp : SolventComponent + The SolventComponent of the system + prot_comp : ProteinComponent | None + The protein component of the system, if it exists. + small_mols : dict[SmallMoleculeComponent: OFFMolecule] + SmallMoleculeComponents to add to the system. + """ + stateA = self._inputs["stateA"] + alchem_comps = self._inputs["alchemical_components"] + + solv_comp, prot_comp, small_mols = system_validation.get_components(stateA) + off_comps = {m: m.to_openff() for m in alchem_comps["stateA"]} + + # We don't need to check that solv_comp is not None, otherwise + # an error will have been raised when calling `validate_solvent` + # in the Protocol's `_create`. + # Similarly we don't need to check prot_comp just return None + return alchem_comps, solv_comp, None, off_comps + + def _handle_settings(self) -> dict[str, SettingsBaseModel]: + """ + Extract the relevant settings for a solvent transformation. + + Returns + ------- + settings : dict[str, SettingsBaseModel] + A dictionary with the following entries: + * forcefield_settings : OpenMMSystemGeneratorFFSettings + * thermo_settings : ThermoSettings + * charge_settings : OpenFFPartialChargeSettings + * solvation_settings : OpenMMSolvationSettings + * alchemical_settings : AlchemicalSettings + * lambda_settings : LambdaSettings + * engine_settings : OpenMMEngineSettings + * integrator_settings : IntegratorSettings + * equil_simulation_settings : MDSimulationSettings + * equil_output_settings : ABFEPreEquilOutputSettings + * simulation_settings : MultiStateSimulationSettings + * output_settings: MultiStateOutputSettings + """ + prot_settings = self._inputs["protocol"].settings + + settings = {} + settings["forcefield_settings"] = prot_settings.forcefield_settings + settings["thermo_settings"] = prot_settings.thermo_settings + settings["charge_settings"] = prot_settings.partial_charge_settings + settings["solvation_settings"] = prot_settings.solvent_solvation_settings + settings["alchemical_settings"] = prot_settings.alchemical_settings + settings["lambda_settings"] = prot_settings.solvent_lambda_settings + settings["engine_settings"] = prot_settings.engine_settings + settings["integrator_settings"] = prot_settings.integrator_settings + settings["equil_simulation_settings"] = prot_settings.solvent_equil_simulation_settings + settings["equil_output_settings"] = prot_settings.solvent_equil_output_settings + settings["simulation_settings"] = prot_settings.solvent_simulation_settings + settings["output_settings"] = prot_settings.solvent_output_settings + + return settings diff --git a/openfe/protocols/openmm_afe/afe_protocol_results.py b/openfe/protocols/openmm_afe/afe_protocol_results.py new file mode 100644 index 000000000..14012a335 --- /dev/null +++ b/openfe/protocols/openmm_afe/afe_protocol_results.py @@ -0,0 +1,546 @@ +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +""" +Result classes for the Absolute Free Energy Protocols +===================================================== + +This module implements :class:`gufe.ProtocolResult` classes for the absolute +free energy Protocols. + +Specifically it implements: + * AbsoluteBindingProtocolResult + * AbsoluteSolvationProtocolResult +""" + +import itertools +import logging +import pathlib +import warnings +from typing import Optional, Union + +import gufe +import numpy as np +import numpy.typing as npt +from openff.units import Quantity +from openff.units import unit as offunit +from openmmtools import multistate + +from openfe.protocols.restraint_utils.geometry.boresch import BoreschRestraintGeometry + +logger = logging.getLogger(__name__) + + +class AbsoluteProtocolResultMixin: + bound_state = "solvent" + unbound_state = "vacuum" + + def __init__(self, **data): + super().__init__(**data) + # TODO: Detect when we have extensions and stitch these together? + if any( + len(pur_list) > 2 + for pur_list in itertools.chain( + self.data[self.bound_state].values(), self.data[self.unbound_state].values() + ) + ): + raise NotImplementedError("Can't stitch together results yet") + + def get_forward_and_reverse_energy_analysis( + self, + ) -> dict[str, list[Optional[dict[str, Union[npt.NDArray, Quantity]]]]]: + """ + Get the reverse and forward analysis of the free energies. + + Returns + ------- + forward_reverse : dict[str, list[Optional[dict[str, Union[npt.NDArray, openff.units.Quantity]]]]] + A dictionary, keyed for each leg of the thermodynamic cycle, + either ``solvent`` and ``vaccuum` for a solvation free energy or + ``solvent`` and ``complex`` for a binding free energy, + with each containing a list of dictionaries containing the forward + and reverse analysis of each repeat of that simulation type. + + The forward and reverse analysis dictionaries contain: + - `fractions`: npt.NDArray + The fractions of data used for the estimates + - `forward_DGs`, `reverse_DGs`: openff.units.Quantity + The forward and reverse estimates for each fraction of data + - `forward_dDGs`, `reverse_dDGs`: openff.units.Quantity + The forward and reverse estimate uncertainty for each + fraction of data. + + If one of the cycle leg list entries is ``None``, this indicates + that the analysis could not be carried out for that repeat. This + is most likely caused by MBAR convergence issues when attempting to + calculate free energies from too few samples. + + Raises + ------ + UserWarning + * If any of the forward and reverse dictionaries are ``None`` in a + given thermodynamic cycle leg. + """ + + forward_reverse: dict[str, list[Optional[dict[str, Union[npt.NDArray, Quantity]]]]] = {} + + for key in [self.bound_state, self.unbound_state]: + forward_reverse[key] = [ + pus[0].outputs["forward_and_reverse_energies"] + for pus in self.data[key].values() # type: ignore[attr-defined] + ] + + if None in forward_reverse[key]: + wmsg = ( + "One or more ``None`` entries were found in the forward " + f"and reverse dictionaries of the repeats of the {key} " + "calculations. This is likely caused by an MBAR convergence " + "failure caused by too few independent samples when " + "calculating the free energies of the 10% timeseries slice." + ) + warnings.warn(wmsg) + + return forward_reverse + + def get_overlap_matrices(self) -> dict[str, list[dict[str, npt.NDArray]]]: + """ + Get a the MBAR overlap estimates for all legs of the simulation. + + Returns + ------- + overlap_stats : dict[str, list[dict[str, npt.NDArray]]] + A dictionary keyed for each leg of the thermodynamic cycle, either + ``solvent`` and ``vaccuum` for a solvation free energy or + ``solvent`` and ``complex`` for a binding free energy, + with each containing a list of dictionaries with the MBAR overlap + estimates of each repeat of that simulation type. + + The underlying MBAR dictionaries contain the following keys: + * ``scalar``: One minus the largest nontrivial eigenvalue + * ``eigenvalues``: The sorted (descending) eigenvalues of the + overlap matrix + * ``matrix``: Estimated overlap matrix of observing a sample from + state i in state j + """ + # Loop through and get the repeats and get the matrices + overlap_stats: dict[str, list[dict[str, npt.NDArray]]] = {} + + for key in [self.bound_state, self.unbound_state]: + overlap_stats[key] = [ + pus[0].outputs["unit_mbar_overlap"] + for pus in self.data[key].values() # type: ignore[attr-defined] + ] + + return overlap_stats + + def get_replica_transition_statistics(self) -> dict[str, list[dict[str, npt.NDArray]]]: + """ + Get the replica exchange transition statistics for all + legs of the simulation. + + Note + ---- + This is currently only available in cases where a replica exchange + simulation was run. + + Returns + ------- + repex_stats : dict[str, list[dict[str, npt.NDArray]]] + A dictionary with keys for each leg of the thermodynamic cycle, either + ``solvent`` and ``vaccuum` for a solvation free energy or + ``solvent`` and ``complex`` for a binding free energy, + with each containing a list of dictionaries containing the replica + transition statistics for each repeat of that simulation type. + + The replica transition statistics dictionaries contain the following: + * ``eigenvalues``: The sorted (descending) eigenvalues of the + lambda state transition matrix + * ``matrix``: The transition matrix estimate of a replica switching + from state i to state j. + """ + repex_stats: dict[str, list[dict[str, npt.NDArray]]] = {} + try: + for key in [self.bound_state, self.unbound_state]: + repex_stats[key] = [ + pus[0].outputs["replica_exchange_statistics"] + for pus in self.data[key].values() # type: ignore[attr-defined] + ] + except KeyError: + errmsg = "Replica exchange statistics were not found, did you run a repex calculation?" + raise ValueError(errmsg) + + return repex_stats + + def get_replica_states(self) -> dict[str, list[npt.NDArray]]: + """ + Get the timeseries of replica states for all simulation legs. + + Returns + ------- + replica_states : dict[str, list[npt.NDArray]] + Dictionary keyed for each leg of the thermodynamic cycle, either + `solvent` and `vacuum` for solvation free energies, + or `complex` and `solvent` for binding free energies, + with lists of replica states timeseries for each repeat of that + simulation type. + """ + replica_states: dict[str, list[npt.NDArray]] = { + self.bound_state: [], + self.unbound_state: [], + } + + def is_file(filename: str): + p = pathlib.Path(filename) + + if not p.exists(): + errmsg = f"File could not be found {p}" + raise ValueError(errmsg) + + return p + + def get_replica_state(nc, chk): + nc = is_file(nc) + dir_path = nc.parents[0] + chk = is_file(dir_path / chk).name + + reporter = multistate.MultiStateReporter( + storage=nc, checkpoint_storage=chk, open_mode="r" + ) + + retval = np.asarray(reporter.read_replica_thermodynamic_states()) + reporter.close() + + return retval + + for key in [self.bound_state, self.unbound_state]: + for pus in self.data[key].values(): # type: ignore[attr-defined] + states = get_replica_state( + pus[0].outputs["nc"], + pus[0].outputs["last_checkpoint"], + ) + replica_states[key].append(states) + + return replica_states + + def equilibration_iterations(self) -> dict[str, list[float]]: + """ + Get the number of equilibration iterations for each simulation. + + Returns + ------- + equilibration_lengths : dict[str, list[float]] + Dictionary keyed for each leg of the thermodynamic cycle, either + `solvent` and `vacuum` for solvation free energies, + or `complex` and `solvent` for binding free energies, + with lists containing the number of equilibration iterations for + each repeat of that simulation type. + """ + equilibration_lengths: dict[str, list[float]] = {} + + for key in [self.bound_state, self.unbound_state]: + equilibration_lengths[key] = [ + pus[0].outputs["equilibration_iterations"] + for pus in self.data[key].values() # type: ignore[attr-defined] + ] + + return equilibration_lengths + + def production_iterations(self) -> dict[str, list[float]]: + """ + Get the number of production iterations for each simulation. + Returns the number of uncorrelated production samples for each + repeat of the calculation. + + Returns + ------- + production_lengths : dict[str, list[float]] + Dictionary keyed for each leg of the thermodynamic cycle, either + `solvent` and `vacuum` for solvation free energies, + or `complex` and `solvent` for binding free energies, + with lists containing the number of equilibration iterations for + each repeat of that simulation type. + """ + production_lengths: dict[str, list[float]] = {} + + for key in [self.bound_state, self.unbound_state]: + production_lengths[key] = [ + pus[0].outputs["production_iterations"] + for pus in self.data[key].values() # type: ignore[attr-defined] + ] + + return production_lengths + + def selection_indices(self) -> dict[str, list[Optional[npt.NDArray]]]: + """ + Get the system selection indices used to write PDB and + trajectory files. + + Returns + ------- + indices : dict[str, list[npt.NDArray]] + A dictionary keyed for each state, either + `solvent` and `vacuum` for solvation free energies, + or `complex` and `solvent` for binding free energies, + each containing a list of NDArrays containing the corresponding + full system atom indices for each atom written in the production + trajectory files for each replica. + """ + indices: dict[str, list[Optional[npt.NDArray]]] = {} + + for key in [self.bound_state, self.unbound_state]: + indices[key] = [] + for pus in self.data[key].values(): # type: ignore[attr-defined] + indices[key].append(pus[0].outputs["selection_indices"]) + + return indices + + +class AbsoluteSolvationProtocolResult(gufe.ProtocolResult, AbsoluteProtocolResultMixin): + """Dict-like container for the output of a AbsoluteSolvationProtocol""" + + bound_state = "solvent" + unbound_state = "vacuum" + + def get_individual_estimates(self) -> dict[str, list[tuple[Quantity, Quantity]]]: + """ + Get the individual estimate of the free energies. + + Returns + ------- + dGs : dict[str, list[tuple[openff.units.Quantity, openff.units.Quantity]]] + A dictionary, keyed `solvent` and `vacuum` for each leg + of the thermodynamic cycle, with lists of tuples containing + the individual free energy estimates and associated MBAR + uncertainties for each repeat of that simulation type. + """ + dGs = {} + + for state in [self.bound_state, self.unbound_state]: + state_dGs = [ + (pus[0].outputs["unit_estimate"], pus[0].outputs["unit_estimate_error"]) + for pus in self.data[state].values() + ] + dGs[state] = state_dGs + + return dGs + + def get_estimate(self): + """Get the solvation free energy estimate for this calculation. + + Returns + ------- + dG : openff.units.Quantity + The solvation free energy. This is a Quantity defined with units. + """ + + def _get_average(estimates): + # Get the unit value of the first value in the estimates + u = estimates[0][0].u + # Loop through estimates and get the free energy values + # in the unit of the first estimate + dGs = [i[0].to(u).m for i in estimates] + + return np.average(dGs) * u + + individual_estimates = self.get_individual_estimates() + vac_dG = _get_average(individual_estimates["vacuum"]) + solv_dG = _get_average(individual_estimates["solvent"]) + + return vac_dG - solv_dG + + def get_uncertainty(self): + """Get the solvation free energy error for this calculation. + + Returns + ------- + err : openff.units.Quantity + The standard deviation between estimates of the solvation free + energy. This is a Quantity defined with units. + """ + + def _get_stdev(estimates): + # Get the unit value of the first value in the estimates + u = estimates[0][0].u + # Loop through estimates and get the free energy values + # in the unit of the first estimate + dGs = [i[0].to(u).m for i in estimates] + + return np.std(dGs) * u + + individual_estimates = self.get_individual_estimates() + vac_err = _get_stdev(individual_estimates["vacuum"]) + solv_err = _get_stdev(individual_estimates["solvent"]) + + # return the combined error + return np.sqrt(vac_err**2 + solv_err**2) + + +class AbsoluteBindingProtocolResult(gufe.ProtocolResult, AbsoluteProtocolResultMixin): + """Dict-like container for the output of a AbsoluteBindingProtocol""" + + bound_state = "complex" + unbound_state = "solvent" + + def get_individual_estimates( + self, + ) -> dict[str, list[tuple[Quantity, Quantity]]]: + """ + Get the individual estimate of the free energies. + + Returns + ------- + dGs : dict[str, list[tuple[openff.units.Quantity, openff.units.Quantity]]] + A dictionary, keyed `solvent`, `complex`, and 'standard_state' + representing each portion of the thermodynamic cycle, + with lists of tuples containing the individual free energy + estimates and, for 'solvent' and 'complex', the associated MBAR + uncertainties for each repeat of that simulation type. + + Notes + ----- + * Standard state correction has no error and so will return a value + of 0. + """ + complex_dGs = [] + correction_dGs = [] + solv_dGs = [] + + for pus in self.data["complex"].values(): + complex_dGs.append( + (pus[0].outputs["unit_estimate"], pus[0].outputs["unit_estimate_error"]) + ) + correction_dGs.append( + ( + pus[0].outputs["standard_state_correction"], + 0 * offunit.kilocalorie_per_mole, # correction has no error + ) + ) + + for pus in self.data["solvent"].values(): + solv_dGs.append( + (pus[0].outputs["unit_estimate"], pus[0].outputs["unit_estimate_error"]) + ) + + return { + "solvent": solv_dGs, + "complex": complex_dGs, + "standard_state_correction": correction_dGs, + } + + @staticmethod + def _add_complex_standard_state_corr( + complex_dG: list[tuple[Quantity, Quantity]], + standard_state_dG: list[tuple[Quantity, Quantity]], + ) -> list[tuple[Quantity, Quantity]]: + """ + Helper method to combine the + complex & standard state corrections legs. + + Parameters + ---------- + complex_dG : list[tuple[openff.units.Quantity, openff.units.Quantity]] + The individual estimates of the complex leg, + where the first entry of each tuple is the dG estimate + and the second entry is the MBAR error. + standard_state_dG : list[tuple[Quantity, Quantity]] + The individual standard state corrections for each corresponding + complex leg. The first entry is the correction, the second + is an empty error value of 0. + + Returns + ------- + combined_dG : list[tuple[openff.units.Quantity,openff.units. Quantity]] + A list of dG estimates & MBAR errors for the combined + complex & standard state correction of each repeat. + + Notes + ----- + We assume that both list of items are in the right order. + """ + combined_dG: list[tuple[Quantity, Quantity]] = [] + for comp, corr in zip(complex_dG, standard_state_dG): + # No need to convert unit types, since pint takes care of that + # except that mypy hates it because pint isn't typed properly... + # No need to add errors since there's just the one + combined_dG.append((comp[0] + corr[0], comp[1])) # type: ignore[operator] + + return combined_dG + + def get_estimate(self) -> Quantity: + """Get the binding free energy estimate for this calculation. + + Returns + ------- + dG : openff.units.Quantity + The binding free energy. This is a Quantity defined with units. + """ + + def _get_average(estimates): + # Get the unit value of the first value in the estimates + u = estimates[0][0].u + # Loop through estimates and get the free energy values + # in the unit of the first estimate + dGs = [i[0].to(u).m for i in estimates] + + return np.average(dGs) * u + + individual_estimates = self.get_individual_estimates() + complex_dG = _get_average( + self._add_complex_standard_state_corr( + individual_estimates["complex"], + individual_estimates["standard_state_correction"], + ) + ) + solv_dG = _get_average(individual_estimates["solvent"]) + + return -complex_dG + solv_dG + + def get_uncertainty(self) -> Quantity: + """Get the binding free energy error for this calculation. + + Returns + ------- + err : openff.units.Quantity + The standard deviation between estimates of the binding free + energy. This is a Quantity defined with units. + """ + + def _get_stdev(estimates): + # Get the unit value of the first value in the estimates + u = estimates[0][0].u + # Loop through estimates and get the free energy values + # in the unit of the first estimate + dGs = [i[0].to(u).m for i in estimates] + + return np.std(dGs) * u + + individual_estimates = self.get_individual_estimates() + + complex_err = _get_stdev( + self._add_complex_standard_state_corr( + individual_estimates["complex"], + individual_estimates["standard_state_correction"], + ) + ) + solv_err = _get_stdev(individual_estimates["solvent"]) + + # return the combined error + return np.sqrt(complex_err**2 + solv_err**2) + + def restraint_geometries(self) -> list[BoreschRestraintGeometry]: + """ + Get a list of the restraint geometries for the + complex simulations. These define the atoms that have + been restrained in the system. + + Returns + ------- + geometries : list[dict[str, Any]] + A list of dictionaries containing the details of the atoms + in the system that are involved in the restraint. + """ + geometries = [ + BoreschRestraintGeometry.model_validate(pus[0].outputs["restraint_geometry"]) + for pus in self.data["complex"].values() + ] + + return geometries diff --git a/openfe/protocols/openmm_afe/ahfe_units.py b/openfe/protocols/openmm_afe/ahfe_units.py new file mode 100644 index 000000000..9fb9b03da --- /dev/null +++ b/openfe/protocols/openmm_afe/ahfe_units.py @@ -0,0 +1,174 @@ +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +"""AHFE Protocol Units --- :mod:`openfe.protocols.openmm_afe.ahfe_units` +======================================================================== +This module defines the ProtocolUnits for the +:class:`AbsoluteSolvationProtocol`. +""" + +import logging + +from openfe.protocols.openmm_afe.equil_afe_settings import ( + SettingsBaseModel, +) + +from ..openmm_utils import system_validation +from .base_afe_units import BaseAbsoluteUnit + +logger = logging.getLogger(__name__) + + +class AbsoluteSolvationVacuumUnit(BaseAbsoluteUnit): + """ + Protocol Unit for the vacuum phase of an absolute solvation free energy + """ + + simtype = "vacuum" + + def _get_components(self): + """ + Get the relevant components for a vacuum transformation. + + Returns + ------- + alchem_comps : dict[str, list[Component]] + A list of alchemical components + solv_comp : None + For the gas phase transformation, None will always be returned + for the solvent component of the chemical system. + prot_comp : Optional[ProteinComponent] + The protein component of the system, if it exists. + small_mols : dict[Component, OpenFF Molecule] + The openff Molecules to add to the system. This + is equivalent to the alchemical components in stateA (since + we only allow for disappearing ligands). + """ + stateA = self._inputs["stateA"] + alchem_comps = self._inputs["alchemical_components"] + + off_comps = {m: m.to_openff() for m in alchem_comps["stateA"]} + + _, prot_comp, _ = system_validation.get_components(stateA) + + # Notes: + # 1. Our input state will contain a solvent, we ``None`` that out + # since this is the gas phase unit. + # 2. Our small molecules will always just be the alchemical components + # (of stateA since we enforce only one disappearing ligand) + return alchem_comps, None, prot_comp, off_comps + + def _handle_settings(self) -> dict[str, SettingsBaseModel]: + """ + Extract the relevant settings for a vacuum transformation. + + Returns + ------- + settings : dict[str, SettingsBaseModel] + A dictionary with the following entries: + * forcefield_settings : OpenMMSystemGeneratorFFSettings + * thermo_settings : ThermoSettings + * charge_settings : OpenFFPartialChargeSettings + * solvation_settings : OpenMMSolvationSettings + * alchemical_settings : AlchemicalSettings + * lambda_settings : LambdaSettings + * engine_settings : OpenMMEngineSettings + * integrator_settings : IntegratorSettings + * equil_simulation_settings : MDSimulationSettings + * equil_output_settings : MDOutputSettings + * simulation_settings : SimulationSettings + * output_settings: MultiStateOutputSettings + """ + prot_settings = self._inputs["protocol"].settings + + settings = {} + settings["forcefield_settings"] = prot_settings.vacuum_forcefield_settings + settings["thermo_settings"] = prot_settings.thermo_settings + settings["charge_settings"] = prot_settings.partial_charge_settings + settings["solvation_settings"] = prot_settings.solvation_settings + settings["alchemical_settings"] = prot_settings.alchemical_settings + settings["lambda_settings"] = prot_settings.lambda_settings + settings["engine_settings"] = prot_settings.vacuum_engine_settings + settings["integrator_settings"] = prot_settings.integrator_settings + settings["equil_simulation_settings"] = prot_settings.vacuum_equil_simulation_settings + settings["equil_output_settings"] = prot_settings.vacuum_equil_output_settings + settings["simulation_settings"] = prot_settings.vacuum_simulation_settings + settings["output_settings"] = prot_settings.vacuum_output_settings + + return settings + + +class AbsoluteSolvationSolventUnit(BaseAbsoluteUnit): + """ + Protocol Unit for the solvent phase of an absolute solvation free energy + """ + + simtype = "solvent" + + def _get_components(self): + """ + Get the relevant components for a solvent transformation. + + Returns + ------- + alchem_comps : dict[str, Component] + A list of alchemical components + solv_comp : SolventComponent + The SolventComponent of the system + prot_comp : Optional[ProteinComponent] + The protein component of the system, if it exists. + small_mols : dict[SmallMoleculeComponent: OFFMolecule] + SmallMoleculeComponents to add to the system. + """ + stateA = self._inputs["stateA"] + alchem_comps = self._inputs["alchemical_components"] + + solv_comp, prot_comp, small_mols = system_validation.get_components(stateA) + off_comps = {m: m.to_openff() for m in small_mols} + + # We don't need to check that solv_comp is not None, otherwise + # an error will have been raised when calling `validate_solvent` + # in the Protocol's `_create`. + # Similarly we don't need to check prot_comp since that's also + # disallowed on create + return alchem_comps, solv_comp, prot_comp, off_comps + + def _handle_settings(self) -> dict[str, SettingsBaseModel]: + """ + Extract the relevant settings for a solvent transformation. + + Returns + ------- + settings : dict[str, SettingsBaseModel] + A dictionary with the following entries: + * forcefield_settings : OpenMMSystemGeneratorFFSettings + * thermo_settings : ThermoSettings + * charge_settings : OpenFFPartialChargeSettings + * solvation_settings : OpenMMSolvationSettings + * alchemical_settings : AlchemicalSettings + * lambda_settings : LambdaSettings + * engine_settings : OpenMMEngineSettings + * integrator_settings : IntegratorSettings + * equil_simulation_settings : MDSimulationSettings + * equil_output_settings : MDOutputSettings + * simulation_settings : MultiStateSimulationSettings + * output_settings: MultiStateOutputSettings + """ + prot_settings = self._inputs["protocol"].settings + + settings = {} + settings["forcefield_settings"] = prot_settings.solvent_forcefield_settings + settings["thermo_settings"] = prot_settings.thermo_settings + settings["charge_settings"] = prot_settings.partial_charge_settings + settings["solvation_settings"] = prot_settings.solvation_settings + settings["alchemical_settings"] = prot_settings.alchemical_settings + settings["lambda_settings"] = prot_settings.lambda_settings + settings["engine_settings"] = prot_settings.solvent_engine_settings + settings["integrator_settings"] = prot_settings.integrator_settings + settings["equil_simulation_settings"] = prot_settings.solvent_equil_simulation_settings + settings["equil_output_settings"] = prot_settings.solvent_equil_output_settings + settings["simulation_settings"] = prot_settings.solvent_simulation_settings + settings["output_settings"] = prot_settings.solvent_output_settings + + return settings diff --git a/openfe/protocols/openmm_afe/base.py b/openfe/protocols/openmm_afe/base_afe_units.py similarity index 99% rename from openfe/protocols/openmm_afe/base.py rename to openfe/protocols/openmm_afe/base_afe_units.py index d28a630ca..d91f0afa8 100644 --- a/openfe/protocols/openmm_afe/base.py +++ b/openfe/protocols/openmm_afe/base_afe_units.py @@ -1,9 +1,9 @@ # This code is part of OpenFE and is licensed under the MIT license. # For details, see https://github.com/OpenFreeEnergy/openfe -"""OpenMM Equilibrium AFE Protocol base classes -=============================================== +"""OpenMM AFE Protocol base classes +=================================== -Base classes for the equilibrium OpenMM absolute free energy ProtocolUnits. +Base classes for the OpenMM absolute free energy ProtocolUnits. Thist mostly implements BaseAbsoluteUnit whose methods can be overriden to define different types of alchemical transformations. @@ -30,7 +30,12 @@ import numpy.typing as npt import openmm import openmmtools -from gufe import ChemicalSystem, ProteinComponent, SmallMoleculeComponent, SolventComponent +from gufe import ( + ChemicalSystem, + ProteinComponent, + SmallMoleculeComponent, + SolventComponent, +) from gufe.components import Component from openff.toolkit.topology import Molecule as OFFMolecule from openff.units import Quantity, unit diff --git a/openfe/protocols/openmm_afe/equil_binding_afe_method.py b/openfe/protocols/openmm_afe/equil_binding_afe_method.py index dc12f1a8e..c619f285c 100644 --- a/openfe/protocols/openmm_afe/equil_binding_afe_method.py +++ b/openfe/protocols/openmm_afe/equil_binding_afe_method.py @@ -24,18 +24,13 @@ """ -import itertools import logging -import pathlib import uuid import warnings from collections import defaultdict -from typing import Any, Iterable, Optional, Union +from typing import Any, Iterable import gufe -import MDAnalysis as mda -import numpy as np -import numpy.typing as npt from gufe import ( ChemicalSystem, ProteinComponent, @@ -43,16 +38,7 @@ SolventComponent, settings, ) -from gufe.components import Component -from openff.units import Quantity from openff.units import unit as offunit -from openff.units.openmm import to_openmm -from openmm import System -from openmm import unit as ommunit -from openmm.app import Topology as omm_topology -from openmmtools import multistate -from openmmtools.states import GlobalParameterState, ThermodynamicState -from rdkit import Chem from openfe.due import Doi, due from openfe.protocols.openmm_afe.equil_afe_settings import ( @@ -68,15 +54,11 @@ OpenFFPartialChargeSettings, OpenMMEngineSettings, OpenMMSolvationSettings, - SettingsBaseModel, ) from openfe.protocols.openmm_utils import settings_validation, system_validation -from openfe.protocols.restraint_utils import geometry -from openfe.protocols.restraint_utils.geometry.boresch import BoreschRestraintGeometry -from openfe.protocols.restraint_utils.openmm import omm_restraints -from openfe.protocols.restraint_utils.openmm.omm_restraints import BoreschRestraint -from .base import BaseAbsoluteUnit +from .abfe_units import AbsoluteBindingComplexUnit, AbsoluteBindingSolventUnit +from .afe_protocol_results import AbsoluteBindingProtocolResult due.cite( Doi("10.5281/zenodo.596504"), @@ -103,418 +85,6 @@ logger = logging.getLogger(__name__) -class AbsoluteBindingProtocolResult(gufe.ProtocolResult): - """Dict-like container for the output of a AbsoluteBindingProtocol""" - - def __init__(self, **data): - super().__init__(**data) - # TODO: Detect when we have extensions and stitch these together? - if any( - len(pur_list) > 2 - for pur_list in itertools.chain( - self.data["solvent"].values(), self.data["complex"].values() - ) - ): - raise NotImplementedError("Can't stitch together results yet") - - def get_individual_estimates( - self, - ) -> dict[str, list[tuple[Quantity, Quantity]]]: - """ - Get the individual estimate of the free energies. - - Returns - ------- - dGs : dict[str, list[tuple[openff.units.Quantity, openff.units.Quantity]]] - A dictionary, keyed `solvent`, `complex`, and 'standard_state' - representing each portion of the thermodynamic cycle, - with lists of tuples containing the individual free energy - estimates and, for 'solvent' and 'complex', the associated MBAR - uncertainties for each repeat of that simulation type. - - Notes - ----- - * Standard state correction has no error and so will return a value - of 0. - """ - complex_dGs = [] - correction_dGs = [] - solv_dGs = [] - - for pus in self.data["complex"].values(): - complex_dGs.append( - (pus[0].outputs["unit_estimate"], pus[0].outputs["unit_estimate_error"]) - ) - correction_dGs.append( - ( - pus[0].outputs["standard_state_correction"], - 0 * offunit.kilocalorie_per_mole, # correction has no error - ) - ) - - for pus in self.data["solvent"].values(): - solv_dGs.append( - (pus[0].outputs["unit_estimate"], pus[0].outputs["unit_estimate_error"]) - ) - - return { - "solvent": solv_dGs, - "complex": complex_dGs, - "standard_state_correction": correction_dGs, - } - - @staticmethod - def _add_complex_standard_state_corr( - complex_dG: list[tuple[Quantity, Quantity]], - standard_state_dG: list[tuple[Quantity, Quantity]], - ) -> list[tuple[Quantity, Quantity]]: - """ - Helper method to combine the - complex & standard state corrections legs. - - Parameters - ---------- - complex_dG : list[tuple[openff.units.Quantity, openff.units.Quantity]] - The individual estimates of the complex leg, - where the first entry of each tuple is the dG estimate - and the second entry is the MBAR error. - standard_state_dG : list[tuple[Quantity, Quantity]] - The individual standard state corrections for each corresponding - complex leg. The first entry is the correction, the second - is an empty error value of 0. - - Returns - ------- - combined_dG : list[tuple[openff.units.Quantity,openff.units. Quantity]] - A list of dG estimates & MBAR errors for the combined - complex & standard state correction of each repeat. - - Notes - ----- - We assume that both list of items are in the right order. - """ - combined_dG: list[tuple[Quantity, Quantity]] = [] - for comp, corr in zip(complex_dG, standard_state_dG): - # No need to convert unit types, since pint takes care of that - # except that mypy hates it because pint isn't typed properly... - # No need to add errors since there's just the one - combined_dG.append((comp[0] + corr[0], comp[1])) # type: ignore[operator] - - return combined_dG - - def get_estimate(self) -> Quantity: - """Get the binding free energy estimate for this calculation. - - Returns - ------- - dG : openff.units.Quantity - The binding free energy. This is a Quantity defined with units. - """ - - def _get_average(estimates): - # Get the unit value of the first value in the estimates - u = estimates[0][0].u - # Loop through estimates and get the free energy values - # in the unit of the first estimate - dGs = [i[0].to(u).m for i in estimates] - - return np.average(dGs) * u - - individual_estimates = self.get_individual_estimates() - complex_dG = _get_average( - self._add_complex_standard_state_corr( - individual_estimates["complex"], - individual_estimates["standard_state_correction"], - ) - ) - solv_dG = _get_average(individual_estimates["solvent"]) - - return -complex_dG + solv_dG - - def get_uncertainty(self) -> Quantity: - """Get the binding free energy error for this calculation. - - Returns - ------- - err : openff.units.Quantity - The standard deviation between estimates of the binding free - energy. This is a Quantity defined with units. - """ - - def _get_stdev(estimates): - # Get the unit value of the first value in the estimates - u = estimates[0][0].u - # Loop through estimates and get the free energy values - # in the unit of the first estimate - dGs = [i[0].to(u).m for i in estimates] - - return np.std(dGs) * u - - individual_estimates = self.get_individual_estimates() - - complex_err = _get_stdev( - self._add_complex_standard_state_corr( - individual_estimates["complex"], individual_estimates["standard_state_correction"] - ) - ) - solv_err = _get_stdev(individual_estimates["solvent"]) - - # return the combined error - return np.sqrt(complex_err**2 + solv_err**2) - - def get_forward_and_reverse_energy_analysis( - self, - ) -> dict[str, list[Optional[dict[str, Union[npt.NDArray, Quantity]]]]]: - """ - Get the reverse and forward analysis of the free energies. - - Returns - ------- - forward_reverse : dict[str, list[Optional[dict[str, Union[npt.NDArray, openff.units.Quantity]]]]] - A dictionary, keyed `solvent` and `complex` for each leg of the - thermodynamic cycle which each contain a list of dictionaries - containing the forward and reverse analysis of each repeat - of that simulation type. - - The forward and reverse analysis dictionaries contain: - - `fractions`: npt.NDArray - The fractions of data used for the estimates - - `forward_DGs`, `reverse_DGs`: openff.units.Quantity - The forward and reverse estimates for each fraction of data - - `forward_dDGs`, `reverse_dDGs`: openff.units.Quantity - The forward and reverse estimate uncertainty for each - fraction of data. - - If one of the cycle leg list entries is ``None``, this indicates - that the analysis could not be carried out for that repeat. This - is most likely caused by MBAR convergence issues when attempting to - calculate free energies from too few samples. - - Raises - ------ - UserWarning - * If any of the forward and reverse dictionaries are ``None`` in a - given thermodynamic cycle leg. - """ - - forward_reverse: dict[str, list[Optional[dict[str, Union[npt.NDArray, Quantity]]]]] = {} - - for key in ["solvent", "complex"]: - forward_reverse[key] = [ - pus[0].outputs["forward_and_reverse_energies"] for pus in self.data[key].values() - ] - - if None in forward_reverse[key]: - wmsg = ( - "One or more ``None`` entries were found in the forward " - f"and reverse dictionaries of the repeats of the {key} " - "calculations. This is likely caused by an MBAR convergence " - "failure caused by too few independent samples when " - "calculating the free energies of the 10% timeseries slice." - ) - warnings.warn(wmsg) - - return forward_reverse - - def get_overlap_matrices(self) -> dict[str, list[dict[str, npt.NDArray]]]: - """ - Get a the MBAR overlap estimates for all legs of the simulation. - - Returns - ------- - overlap_stats : dict[str, list[dict[str, npt.NDArray]]] - A dictionary with keys `solvent` and `complex` for each - leg of the thermodynamic cycle, which each containing a - list of dictionaries with the MBAR overlap estimates of - each repeat of that simulation type. - - The underlying MBAR dictionaries contain the following keys: - * ``scalar``: One minus the largest nontrivial eigenvalue - * ``eigenvalues``: The sorted (descending) eigenvalues of the - overlap matrix - * ``matrix``: Estimated overlap matrix of observing a sample from - state i in state j - """ - # Loop through and get the repeats and get the matrices - overlap_stats: dict[str, list[dict[str, npt.NDArray]]] = {} - - for key in ["solvent", "complex"]: - overlap_stats[key] = [ - pus[0].outputs["unit_mbar_overlap"] for pus in self.data[key].values() - ] - - return overlap_stats - - def get_replica_transition_statistics( - self, - ) -> dict[str, list[dict[str, npt.NDArray]]]: - """ - Get the replica exchange transition statistics for all - legs of the simulation. - - Note - ---- - This is currently only available in cases where a replica exchange - simulation was run. - - Returns - ------- - repex_stats : dict[str, list[dict[str, npt.NDArray]]] - A dictionary with keys `solvent` and `complex` for each - leg of the thermodynamic cycle, which each containing - a list of dictionaries containing the replica transition - statistics for each repeat of that simulation type. - - The replica transition statistics dictionaries contain the following: - * ``eigenvalues``: The sorted (descending) eigenvalues of the - lambda state transition matrix - * ``matrix``: The transition matrix estimate of a replica switching - from state i to state j. - """ - repex_stats: dict[str, list[dict[str, npt.NDArray]]] = {} - try: - for key in ["solvent", "complex"]: - repex_stats[key] = [ - pus[0].outputs["replica_exchange_statistics"] for pus in self.data[key].values() - ] - except KeyError: - errmsg = "Replica exchange statistics were not found, did you run a repex calculation?" - raise ValueError(errmsg) - - return repex_stats - - def get_replica_states(self) -> dict[str, list[npt.NDArray]]: - """ - Get the timeseries of replica states for all simulation legs. - - Returns - ------- - replica_states : dict[str, list[npt.NDArray]] - Dictionary keyed `solvent` and `complex` for each leg of - the thermodynamic cycle, with lists of replica states - timeseries for each repeat of that simulation type. - """ - replica_states: dict[str, list[npt.NDArray]] = {"solvent": [], "complex": []} - - def is_file(filename: str): - p = pathlib.Path(filename) - - if not p.exists(): - errmsg = f"File could not be found {p}" - raise ValueError(errmsg) - - return p - - def get_replica_state(nc, chk): - nc = is_file(nc) - dir_path = nc.parents[0] - chk = is_file(dir_path / chk).name - - reporter = multistate.MultiStateReporter( - storage=nc, checkpoint_storage=chk, open_mode="r" - ) - - retval = np.asarray(reporter.read_replica_thermodynamic_states()) - reporter.close() - - return retval - - for key in ["solvent", "complex"]: - for pus in self.data[key].values(): - states = get_replica_state( - pus[0].outputs["nc"], - pus[0].outputs["last_checkpoint"], - ) - replica_states[key].append(states) - - return replica_states - - def equilibration_iterations(self) -> dict[str, list[float]]: - """ - Get the number of equilibration iterations for each simulation. - - Returns - ------- - equilibration_lengths : dict[str, list[float]] - Dictionary keyed `solvent` and `complex` for each leg - of the thermodynamic cycle, with lists containing the - number of equilibration iterations for each repeat - of that simulation type. - """ - equilibration_lengths: dict[str, list[float]] = {} - - for key in ["solvent", "complex"]: - equilibration_lengths[key] = [ - pus[0].outputs["equilibration_iterations"] for pus in self.data[key].values() - ] - - return equilibration_lengths - - def production_iterations(self) -> dict[str, list[float]]: - """ - Get the number of production iterations for each simulation. - Returns the number of uncorrelated production samples for each - repeat of the calculation. - - Returns - ------- - production_lengths : dict[str, list[float]] - Dictionary keyed `solvent` and `complex` for each leg of the - thermodynamic cycle, with lists with the number - of production iterations for each repeat of that simulation - type. - """ - production_lengths: dict[str, list[float]] = {} - - for key in ["solvent", "complex"]: - production_lengths[key] = [ - pus[0].outputs["production_iterations"] for pus in self.data[key].values() - ] - - return production_lengths - - def restraint_geometries(self) -> list[BoreschRestraintGeometry]: - """ - Get a list of the restraint geometries for the - complex simulations. These define the atoms that have - been restrained in the system. - - Returns - ------- - geometries : list[dict[str, Any]] - A list of dictionaries containing the details of the atoms - in the system that are involved in the restraint. - """ - geometries = [ - BoreschRestraintGeometry.model_validate(pus[0].outputs["restraint_geometry"]) - for pus in self.data["complex"].values() - ] - - return geometries - - def selection_indices(self) -> dict[str, list[Optional[npt.NDArray]]]: - """ - Get the system selection indices used to write PDB and - trajectory files. - - Returns - ------- - indices : dict[str, list[npt.NDArray]] - A dictionary keyed as `complex` and `solvent` for each - state, each containing a list of NDArrays containing the corresponding - full system atom indices for each atom written in the production - trajectory files for each replica. - """ - indices: dict[str, list[Optional[npt.NDArray]]] = {} - - for key in ["complex", "solvent"]: - indices[key] = [] - for pus in self.data[key].values(): - indices[key].append(pus[0].outputs["selection_indices"]) - - return indices - - class AbsoluteBindingProtocol(gufe.Protocol): """ Absolute binding free energy calculations using OpenMM and OpenMMTools. @@ -761,8 +331,8 @@ def _validate( *, stateA: ChemicalSystem, stateB: ChemicalSystem, - mapping: Optional[Union[gufe.ComponentMapping, list[gufe.ComponentMapping]]] = None, - extends: Optional[gufe.ProtocolDAGResult] = None, + mapping: gufe.ComponentMapping | list[gufe.ComponentMapping] | None = None, + extends: gufe.ProtocolDAGResult | None = None, ): # Check we're not extending if extends is not None: @@ -838,8 +408,8 @@ def _create( self, stateA: ChemicalSystem, stateB: ChemicalSystem, - mapping: Optional[Union[gufe.ComponentMapping, list[gufe.ComponentMapping]]] = None, - extends: Optional[gufe.ProtocolDAGResult] = None, + mapping: gufe.ComponentMapping | list[gufe.ComponentMapping] | None = None, + extends: gufe.ProtocolDAGResult | None = None, ) -> list[gufe.ProtocolUnit]: # Validate inputs self.validate(stateA=stateA, stateB=stateB, mapping=mapping, extends=extends) @@ -910,425 +480,3 @@ def _gather( for k, v in unsorted_complex_repeats.items(): repeats["complex"][str(k)] = sorted(v, key=lambda x: x.outputs["generation"]) return repeats - - -class AbsoluteBindingComplexUnit(BaseAbsoluteUnit): - """ - Protocol Unit for the complex phase of an absolute binding free energy - """ - - simtype = "complex" - - def _get_components(self): - """ - Get the relevant components for a complex transformation. - - Returns - ------- - alchem_comps : dict[str, Component] - A dict of alchemical components - solv_comp : SolventComponent - The SolventComponent of the system - prot_comp : Optional[ProteinComponent] - The protein component of the system, if it exists. - small_mols : dict[SmallMoleculeComponent: OFFMolecule] - SmallMoleculeComponents to add to the system. - """ - stateA = self._inputs["stateA"] - alchem_comps = self._inputs["alchemical_components"] - - solv_comp, prot_comp, small_mols = system_validation.get_components(stateA) - off_comps = {m: m.to_openff() for m in small_mols} - - # We don't need to check that solv_comp is not None, otherwise - # an error will have been raised when calling `validate_solvent` - # in the Protocol's `_create`. - # Similarly we don't need to check prot_comp - return alchem_comps, solv_comp, prot_comp, off_comps - - def _handle_settings(self) -> dict[str, SettingsBaseModel]: - """ - Extract the relevant settings for a complex transformation. - - Returns - ------- - settings : dict[str, SettingsBaseModel] - A dictionary with the following entries: - * forcefield_settings : OpenMMSystemGeneratorFFSettings - * thermo_settings : ThermoSettings - * charge_settings : OpenFFPartialChargeSettings - * solvation_settings : OpenMMSolvationSettings - * alchemical_settings : AlchemicalSettings - * lambda_settings : LambdaSettings - * engine_settings : OpenMMEngineSettings - * integrator_settings : IntegratorSettings - * equil_simulation_settings : MDSimulationSettings - * equil_output_settings : ABFEPreEquilOutputSettings - * simulation_settings : SimulationSettings - * output_settings: MultiStateOutputSettings - * restraint_settings: BaseRestraintSettings - """ - prot_settings = self._inputs["protocol"].settings - - settings = {} - settings["forcefield_settings"] = prot_settings.forcefield_settings - settings["thermo_settings"] = prot_settings.thermo_settings - settings["charge_settings"] = prot_settings.partial_charge_settings - settings["solvation_settings"] = prot_settings.complex_solvation_settings - settings["alchemical_settings"] = prot_settings.alchemical_settings - settings["lambda_settings"] = prot_settings.complex_lambda_settings - settings["engine_settings"] = prot_settings.engine_settings - settings["integrator_settings"] = prot_settings.integrator_settings - settings["equil_simulation_settings"] = prot_settings.complex_equil_simulation_settings - settings["equil_output_settings"] = prot_settings.complex_equil_output_settings - settings["simulation_settings"] = prot_settings.complex_simulation_settings - settings["output_settings"] = prot_settings.complex_output_settings - settings["restraint_settings"] = prot_settings.restraint_settings - - return settings - - @staticmethod - def _get_mda_universe( - topology: omm_topology, - positions: ommunit.Quantity, - trajectory: Optional[pathlib.Path], - ) -> mda.Universe: - """ - Helper method to get a Universe from an openmm Topology, - and either an input trajectory or a set of positions. - - Parameters - ---------- - topology : openmm.app.Topology - An OpenMM Topology that defines the System. - positions: openmm.unit.Quantity - The System's current positions. - Used if a trajectory file is None or is not a file. - trajectory: pathlib.Path - A Path to a trajectory file to read positions from. - - Returns - ------- - mda.Universe - An MDAnalysis Universe of the System. - """ - from MDAnalysis.coordinates.memory import MemoryReader - - # If the trajectory file doesn't exist, then we use positions - if trajectory is not None and trajectory.is_file(): - return mda.Universe( - topology, - trajectory, - topology_format="OPENMMTOPOLOGY", - ) - else: - # Positions is an openmm Quantity in nm we need - # to convert to angstroms - return mda.Universe( - topology, - np.array(positions._value) * 10, - topology_format="OPENMMTOPOLOGY", - trajectory_format=MemoryReader, - ) - - @staticmethod - def _get_idxs_from_residxs( - topology: omm_topology, - residxs: list[int], - ) -> list[int]: - """ - Helper method to get the a list of atom indices which belong to a list - of residues. - - Parameters - ---------- - topology : openmm.app.Topology - An OpenMM Topology that defines the System. - residxs : list[int] - A list of residue numbers who's atoms we should get atom indices. - - Returns - ------- - atom_ids : list[int] - A list of atom indices. - - TODO - ---- - * Check how this works when we deal with virtual sites. - """ - atom_ids = [] - - for r in topology.residues(): - if r.index in residxs: - atom_ids.extend([at.index for at in r.atoms()]) - - return atom_ids - - @staticmethod - def _get_boresch_restraint( - universe: mda.Universe, - guest_rdmol: Chem.Mol, - guest_atom_ids: list[int], - host_atom_ids: list[int], - temperature: Quantity, - settings: BoreschRestraintSettings, - ) -> tuple[BoreschRestraintGeometry, BoreschRestraint]: - """ - Get a Boresch-like restraint Geometry and OpenMM restraint force - supplier. - - Parameters - ---------- - universe : mda.Universe - An MDAnalysis Universe defining the system to get the restraint for. - guest_rdmol : Chem.Mol - An RDKit Molecule defining the guest molecule in the system. - guest_atom_ids: list[int] - A list of atom indices defining the guest molecule in the universe. - host_atom_ids : list[int] - A list of atom indices defining the host molecules in the universe. - temperature : openff.units.Quantity - The temperature of the simulation where the restraint will be added. - settings : BoreschRestraintSettings - Settings on how the Boresch-like restraint should be defined. - - Returns - ------- - geom : BoreschRestraintGeometry - A class defining the Boresch-like restraint. - restraint : BoreschRestraint - A factory class for generating Boresch restraints in OpenMM. - """ - # Take the minimum of the two possible force constants to check against - frc_const = min(settings.K_thetaA, settings.K_thetaB) - - geom = geometry.boresch.find_boresch_restraint( - universe=universe, - guest_rdmol=guest_rdmol, - guest_idxs=guest_atom_ids, - host_idxs=host_atom_ids, - host_selection=settings.host_selection, - anchor_finding_strategy=settings.anchor_finding_strategy, - dssp_filter=settings.dssp_filter, - rmsf_cutoff=settings.rmsf_cutoff, - host_min_distance=settings.host_min_distance, - host_max_distance=settings.host_max_distance, - angle_force_constant=frc_const, - temperature=temperature, - ) - - restraint = omm_restraints.BoreschRestraint(settings) - return geom, restraint - - def _add_restraints( - self, - system: System, - topology: omm_topology, - positions: ommunit.Quantity, - alchem_comps: dict[str, list[Component]], - comp_resids: dict[Component, npt.NDArray], - settings: dict[str, SettingsBaseModel], - ) -> tuple[ - GlobalParameterState, - Quantity, - System, - geometry.HostGuestRestraintGeometry, - ]: - """ - Find and add restraints to the OpenMM System. - - Notes - ----- - Currently, only Boresch-like restraints are supported. - - Parameters - ---------- - system : openmm.System - The System to add the restraint to. - topology : openmm.app.Topology - An OpenMM Topology that defines the System. - positions: openmm.unit.Quantity - The System's current positions. - Used if a trajectory file isn't found. - alchem_comps: dict[str, list[Component]] - A dictionary with a list of alchemical components - in both state A and B. - comp_resids: dict[Component, npt.NDArray] - A dictionary keyed by each Component in the System - which contains arrays with the residue indices that is contained - by that Component. - settings : dict[str, SettingsBaseModel] - A dictionary of settings that defines how to find and set - the restraint. - - Returns - ------- - restraint_parameter_state : RestraintParameterState - A RestraintParameterState object that defines the control - parameter for the restraint. - correction : openff.units.Quantity - The standard state correction for the restraint. - system : openmm.System - A copy of the System with the restraint added. - rest_geom : geometry.HostGuestRestraintGeometry - The restraint Geometry object. - """ - if self.verbose: - self.logger.info("Generating restraints") - - # Get the guest rdmol - guest_rdmol = alchem_comps["stateA"][0].to_rdkit() - - # sanitize the rdmol if possible - warn if you can't - err = Chem.SanitizeMol(guest_rdmol, catchErrors=True) - - if err: - msg = "restraint generation: could not sanitize ligand rdmol" - logger.warning(msg) - - # Get the guest idxs - # concatenate a list of residue indexes for all alchemical components - residxs = np.concatenate([comp_resids[key] for key in alchem_comps["stateA"]]) - - # get the alchemicical atom ids - guest_atom_ids = self._get_idxs_from_residxs(topology, residxs) - - # Now get the host idxs - # We assume this is everything but the alchemical component - # and the solvent. - solv_comps = [c for c in comp_resids if isinstance(c, SolventComponent)] - exclude_comps = [alchem_comps["stateA"]] + solv_comps - residxs = np.concatenate([v for i, v in comp_resids.items() if i not in exclude_comps]) - - host_atom_ids = self._get_idxs_from_residxs(topology, residxs) - - # Finally create an MDAnalysis Universe - # We try to pass the equilibration production file path through - # In some cases (debugging / dry runs) this won't be available - # so we'll default to using input positions. - univ = self._get_mda_universe( - topology, - positions, - self.shared_basepath / settings["equil_output_settings"].production_trajectory_filename, - ) - - if isinstance(settings["restraint_settings"], BoreschRestraintSettings): - rest_geom, restraint = self._get_boresch_restraint( - univ, - guest_rdmol, - guest_atom_ids, - host_atom_ids, - settings["thermo_settings"].temperature, - settings["restraint_settings"], - ) - else: - # TODO turn this into a direction for different restraint types supported? - raise NotImplementedError("Other restraint types are not yet available") - - if self.verbose: - self.logger.info(f"restraint geometry is: {rest_geom}") - - # We need a temporary thermodynamic state to add the restraint - # & get the correction - thermodynamic_state = ThermodynamicState( - system, - temperature=to_openmm(settings["thermo_settings"].temperature), - pressure=to_openmm(settings["thermo_settings"].pressure), - ) - - # Add the force to the thermodynamic state - restraint.add_force( - thermodynamic_state, - rest_geom, - controlling_parameter_name="lambda_restraints", - ) - # Get the standard state correction as a unit.Quantity - correction = restraint.get_standard_state_correction( - thermodynamic_state, - rest_geom, - ) - - # Get the GlobalParameterState for the restraint - restraint_parameter_state = omm_restraints.RestraintParameterState(lambda_restraints=1.0) - return ( - restraint_parameter_state, - correction, - # Remove the thermostat, otherwise you'll get an - # Andersen thermostat by default! - thermodynamic_state.get_system(remove_thermostat=True), - rest_geom, - ) - - -class AbsoluteBindingSolventUnit(BaseAbsoluteUnit): - """ - Protocol Unit for the solvent phase of an absolute binding free energy - """ - - simtype = "solvent" - - def _get_components(self): - """ - Get the relevant components for a solvent transformation. - - Returns - ------- - alchem_comps : dict[str, Component] - A list of alchemical components - solv_comp : SolventComponent - The SolventComponent of the system - prot_comp : Optional[ProteinComponent] - The protein component of the system, if it exists. - small_mols : dict[SmallMoleculeComponent: OFFMolecule] - SmallMoleculeComponents to add to the system. - """ - stateA = self._inputs["stateA"] - alchem_comps = self._inputs["alchemical_components"] - - solv_comp, prot_comp, small_mols = system_validation.get_components(stateA) - off_comps = {m: m.to_openff() for m in alchem_comps["stateA"]} - - # We don't need to check that solv_comp is not None, otherwise - # an error will have been raised when calling `validate_solvent` - # in the Protocol's `_create`. - # Similarly we don't need to check prot_comp just return None - return alchem_comps, solv_comp, None, off_comps - - def _handle_settings(self) -> dict[str, SettingsBaseModel]: - """ - Extract the relevant settings for a solvent transformation. - - Returns - ------- - settings : dict[str, SettingsBaseModel] - A dictionary with the following entries: - * forcefield_settings : OpenMMSystemGeneratorFFSettings - * thermo_settings : ThermoSettings - * charge_settings : OpenFFPartialChargeSettings - * solvation_settings : OpenMMSolvationSettings - * alchemical_settings : AlchemicalSettings - * lambda_settings : LambdaSettings - * engine_settings : OpenMMEngineSettings - * integrator_settings : IntegratorSettings - * equil_simulation_settings : MDSimulationSettings - * equil_output_settings : ABFEPreEquilOutputSettings - * simulation_settings : MultiStateSimulationSettings - * output_settings: MultiStateOutputSettings - """ - prot_settings = self._inputs["protocol"].settings - - settings = {} - settings["forcefield_settings"] = prot_settings.forcefield_settings - settings["thermo_settings"] = prot_settings.thermo_settings - settings["charge_settings"] = prot_settings.partial_charge_settings - settings["solvation_settings"] = prot_settings.solvent_solvation_settings - settings["alchemical_settings"] = prot_settings.alchemical_settings - settings["lambda_settings"] = prot_settings.solvent_lambda_settings - settings["engine_settings"] = prot_settings.engine_settings - settings["integrator_settings"] = prot_settings.integrator_settings - settings["equil_simulation_settings"] = prot_settings.solvent_equil_simulation_settings - settings["equil_output_settings"] = prot_settings.solvent_equil_output_settings - settings["simulation_settings"] = prot_settings.solvent_simulation_settings - settings["output_settings"] = prot_settings.solvent_output_settings - - return settings diff --git a/openfe/protocols/openmm_afe/equil_solvation_afe_method.py b/openfe/protocols/openmm_afe/equil_solvation_afe_method.py index 4ffb94770..7abcde6f7 100644 --- a/openfe/protocols/openmm_afe/equil_solvation_afe_method.py +++ b/openfe/protocols/openmm_afe/equil_solvation_afe_method.py @@ -28,11 +28,7 @@ """ -from __future__ import annotations - -import itertools import logging -import pathlib import uuid import warnings from collections import defaultdict @@ -40,7 +36,6 @@ import gufe import numpy as np -import numpy.typing as npt from gufe import ( ChemicalSystem, ProteinComponent, @@ -48,9 +43,7 @@ SolventComponent, settings, ) -from gufe.components import Component -from openff.units import Quantity, unit -from openmmtools import multistate +from openff.units import unit as offunit from openfe.due import Doi, due from openfe.protocols.openmm_afe.equil_afe_settings import ( @@ -65,11 +58,14 @@ OpenFFPartialChargeSettings, OpenMMEngineSettings, OpenMMSolvationSettings, - SettingsBaseModel, ) from ..openmm_utils import settings_validation, system_validation -from .base import BaseAbsoluteUnit +from .afe_protocol_results import AbsoluteSolvationProtocolResult +from .ahfe_units import ( + AbsoluteSolvationSolventUnit, + AbsoluteSolvationVacuumUnit, +) due.cite( Doi("10.5281/zenodo.596504"), @@ -103,305 +99,6 @@ logger = logging.getLogger(__name__) -class AbsoluteSolvationProtocolResult(gufe.ProtocolResult): - """Dict-like container for the output of a AbsoluteSolvationProtocol""" - - def __init__(self, **data): - super().__init__(**data) - # TODO: Detect when we have extensions and stitch these together? - if any( - len(pur_list) > 2 - for pur_list in itertools.chain( - self.data["solvent"].values(), self.data["vacuum"].values() - ) - ): - raise NotImplementedError("Can't stitch together results yet") - - def get_individual_estimates(self) -> dict[str, list[tuple[Quantity, Quantity]]]: - """ - Get the individual estimate of the free energies. - - Returns - ------- - dGs : dict[str, list[tuple[openff.units.Quantity, openff.units.Quantity]]] - A dictionary, keyed `solvent` and `vacuum` for each leg - of the thermodynamic cycle, with lists of tuples containing - the individual free energy estimates and associated MBAR - uncertainties for each repeat of that simulation type. - """ - vac_dGs = [] - solv_dGs = [] - - for pus in self.data["vacuum"].values(): - vac_dGs.append((pus[0].outputs["unit_estimate"], pus[0].outputs["unit_estimate_error"])) - - for pus in self.data["solvent"].values(): - solv_dGs.append( - (pus[0].outputs["unit_estimate"], pus[0].outputs["unit_estimate_error"]) - ) - - return {"solvent": solv_dGs, "vacuum": vac_dGs} - - def get_estimate(self): - """Get the solvation free energy estimate for this calculation. - - Returns - ------- - dG : openff.units.Quantity - The solvation free energy. This is a Quantity defined with units. - """ - - def _get_average(estimates): - # Get the unit value of the first value in the estimates - u = estimates[0][0].u - # Loop through estimates and get the free energy values - # in the unit of the first estimate - dGs = [i[0].to(u).m for i in estimates] - - return np.average(dGs) * u - - individual_estimates = self.get_individual_estimates() - vac_dG = _get_average(individual_estimates["vacuum"]) - solv_dG = _get_average(individual_estimates["solvent"]) - - return vac_dG - solv_dG - - def get_uncertainty(self): - """Get the solvation free energy error for this calculation. - - Returns - ------- - err : openff.units.Quantity - The standard deviation between estimates of the solvation free - energy. This is a Quantity defined with units. - """ - - def _get_stdev(estimates): - # Get the unit value of the first value in the estimates - u = estimates[0][0].u - # Loop through estimates and get the free energy values - # in the unit of the first estimate - dGs = [i[0].to(u).m for i in estimates] - - return np.std(dGs) * u - - individual_estimates = self.get_individual_estimates() - vac_err = _get_stdev(individual_estimates["vacuum"]) - solv_err = _get_stdev(individual_estimates["solvent"]) - - # return the combined error - return np.sqrt(vac_err**2 + solv_err**2) - - def get_forward_and_reverse_energy_analysis( - self, - ) -> dict[str, list[Optional[dict[str, Union[npt.NDArray, Quantity]]]]]: - """ - Get the reverse and forward analysis of the free energies. - - Returns - ------- - forward_reverse : dict[str, list[Optional[dict[str, Union[npt.NDArray, openff.units.Quantity]]]]] - A dictionary, keyed `solvent` and `vacuum` for each leg of the - thermodynamic cycle which each contain a list of dictionaries - containing the forward and reverse analysis of each repeat - of that simulation type. - - The forward and reverse analysis dictionaries contain: - - `fractions`: npt.NDArray - The fractions of data used for the estimates - - `forward_DGs`, `reverse_DGs`: openff.units.Quantity - The forward and reverse estimates for each fraction of data - - `forward_dDGs`, `reverse_dDGs`: openff.units.Quantity - The forward and reverse estimate uncertainty for each - fraction of data. - - If one of the cycle leg list entries is ``None``, this indicates - that the analysis could not be carried out for that repeat. This - is most likely caused by MBAR convergence issues when attempting to - calculate free energies from too few samples. - - Raises - ------ - UserWarning - * If any of the forward and reverse dictionaries are ``None`` in a - given thermodynamic cycle leg. - """ - - forward_reverse: dict[str, list[Optional[dict[str, Union[npt.NDArray, Quantity]]]]] = {} - - for key in ["solvent", "vacuum"]: - forward_reverse[key] = [ - pus[0].outputs["forward_and_reverse_energies"] for pus in self.data[key].values() - ] - - if None in forward_reverse[key]: - wmsg = ( - "One or more ``None`` entries were found in the forward " - f"and reverse dictionaries of the repeats of the {key} " - "calculations. This is likely caused by an MBAR convergence " - "failure caused by too few independent samples when " - "calculating the free energies of the 10% timeseries slice." - ) - warnings.warn(wmsg) - - return forward_reverse - - def get_overlap_matrices(self) -> dict[str, list[dict[str, npt.NDArray]]]: - """ - Get a the MBAR overlap estimates for all legs of the simulation. - - Returns - ------- - overlap_stats : dict[str, list[dict[str, npt.NDArray]]] - A dictionary with keys `solvent` and `vacuum` for each - leg of the thermodynamic cycle, which each containing a - list of dictionaries with the MBAR overlap estimates of - each repeat of that simulation type. - - The underlying MBAR dictionaries contain the following keys: - * ``scalar``: One minus the largest nontrivial eigenvalue - * ``eigenvalues``: The sorted (descending) eigenvalues of the - overlap matrix - * ``matrix``: Estimated overlap matrix of observing a sample from - state i in state j - """ - # Loop through and get the repeats and get the matrices - overlap_stats: dict[str, list[dict[str, npt.NDArray]]] = {} - - for key in ["solvent", "vacuum"]: - overlap_stats[key] = [ - pus[0].outputs["unit_mbar_overlap"] for pus in self.data[key].values() - ] - - return overlap_stats - - def get_replica_transition_statistics(self) -> dict[str, list[dict[str, npt.NDArray]]]: - """ - Get the replica exchange transition statistics for all - legs of the simulation. - - Note - ---- - This is currently only available in cases where a replica exchange - simulation was run. - - Returns - ------- - repex_stats : dict[str, list[dict[str, npt.NDArray]]] - A dictionary with keys `solvent` and `vacuum` for each - leg of the thermodynamic cycle, which each containing - a list of dictionaries containing the replica transition - statistics for each repeat of that simulation type. - - The replica transition statistics dictionaries contain the following: - * ``eigenvalues``: The sorted (descending) eigenvalues of the - lambda state transition matrix - * ``matrix``: The transition matrix estimate of a replica switching - from state i to state j. - """ - repex_stats: dict[str, list[dict[str, npt.NDArray]]] = {} - try: - for key in ["solvent", "vacuum"]: - repex_stats[key] = [ - pus[0].outputs["replica_exchange_statistics"] for pus in self.data[key].values() - ] - except KeyError: - errmsg = "Replica exchange statistics were not found, did you run a repex calculation?" - raise ValueError(errmsg) - - return repex_stats - - def get_replica_states(self) -> dict[str, list[npt.NDArray]]: - """ - Get the timeseries of replica states for all simulation legs. - - Returns - ------- - replica_states : dict[str, list[npt.NDArray]] - Dictionary keyed `solvent` and `vacuum` for each leg of - the thermodynamic cycle, with lists of replica states - timeseries for each repeat of that simulation type. - """ - replica_states: dict[str, list[npt.NDArray]] = {"solvent": [], "vacuum": []} - - def is_file(filename: str): - p = pathlib.Path(filename) - - if not p.exists(): - errmsg = f"File could not be found {p}" - raise ValueError(errmsg) - - return p - - def get_replica_state(nc, chk): - nc = is_file(nc) - dir_path = nc.parents[0] - chk = is_file(dir_path / chk).name - - reporter = multistate.MultiStateReporter( - storage=nc, checkpoint_storage=chk, open_mode="r" - ) - - retval = np.asarray(reporter.read_replica_thermodynamic_states()) - reporter.close() - - return retval - - for key in ["solvent", "vacuum"]: - for pus in self.data[key].values(): - states = get_replica_state( - pus[0].outputs["nc"], - pus[0].outputs["last_checkpoint"], - ) - replica_states[key].append(states) - - return replica_states - - def equilibration_iterations(self) -> dict[str, list[float]]: - """ - Get the number of equilibration iterations for each simulation. - - Returns - ------- - equilibration_lengths : dict[str, list[float]] - Dictionary keyed `solvent` and `vacuum` for each leg - of the thermodynamic cycle, with lists containing the - number of equilibration iterations for each repeat - of that simulation type. - """ - equilibration_lengths: dict[str, list[float]] = {} - - for key in ["solvent", "vacuum"]: - equilibration_lengths[key] = [ - pus[0].outputs["equilibration_iterations"] for pus in self.data[key].values() - ] - - return equilibration_lengths - - def production_iterations(self) -> dict[str, list[float]]: - """ - Get the number of production iterations for each simulation. - Returns the number of uncorrelated production samples for each - repeat of the calculation. - - Returns - ------- - production_lengths : dict[str, list[float]] - Dictionary keyed `solvent` and `vacuum` for each leg of the - thermodynamic cycle, with lists with the number - of production iterations for each repeat of that simulation - type. - """ - production_lengths: dict[str, list[float]] = {} - - for key in ["solvent", "vacuum"]: - production_lengths[key] = [ - pus[0].outputs["production_iterations"] for pus in self.data[key].values() - ] - - return production_lengths - - class AbsoluteSolvationProtocol(gufe.Protocol): """ Absolute solvation free energy calculations using OpenMM and OpenMMTools. @@ -439,8 +136,8 @@ def _default_settings(cls): nonbonded_method="nocutoff", ), thermo_settings=settings.ThermoSettings( - temperature=298.15 * unit.kelvin, - pressure=1 * unit.bar, + temperature=298.15 * offunit.kelvin, + pressure=1 * offunit.bar, ), alchemical_settings=AlchemicalSettings(), lambda_settings=LambdaSettings( @@ -460,9 +157,9 @@ def _default_settings(cls): solvent_engine_settings=OpenMMEngineSettings(), integrator_settings=IntegratorSettings(), solvent_equil_simulation_settings=MDSimulationSettings( - equilibration_length_nvt=0.1 * unit.nanosecond, - equilibration_length=0.2 * unit.nanosecond, - production_length=0.5 * unit.nanosecond, + equilibration_length_nvt=0.1 * offunit.nanosecond, + equilibration_length=0.2 * offunit.nanosecond, + production_length=0.5 * offunit.nanosecond, ), solvent_equil_output_settings=MDOutputSettings( equil_nvt_structure="equil_nvt_structure.pdb", @@ -472,8 +169,8 @@ def _default_settings(cls): ), solvent_simulation_settings=MultiStateSimulationSettings( n_replicas=14, - equilibration_length=1.0 * unit.nanosecond, - production_length=10.0 * unit.nanosecond, + equilibration_length=1.0 * offunit.nanosecond, + production_length=10.0 * offunit.nanosecond, ), solvent_output_settings=MultiStateOutputSettings( output_filename="solvent.nc", @@ -481,8 +178,8 @@ def _default_settings(cls): ), vacuum_equil_simulation_settings=MDSimulationSettings( equilibration_length_nvt=None, - equilibration_length=0.2 * unit.nanosecond, - production_length=0.5 * unit.nanosecond, + equilibration_length=0.2 * offunit.nanosecond, + production_length=0.5 * offunit.nanosecond, ), vacuum_equil_output_settings=MDOutputSettings( equil_nvt_structure=None, @@ -492,8 +189,8 @@ def _default_settings(cls): ), vacuum_simulation_settings=MultiStateSimulationSettings( n_replicas=14, - equilibration_length=0.5 * unit.nanosecond, - production_length=2.0 * unit.nanosecond, + equilibration_length=0.5 * offunit.nanosecond, + production_length=2.0 * offunit.nanosecond, ), vacuum_output_settings=MultiStateOutputSettings( output_filename="vacuum.nc", @@ -714,7 +411,7 @@ def _validate( # Check vacuum equilibration MD settings is 0 ns nvt_time = self.settings.vacuum_equil_simulation_settings.equilibration_length_nvt if nvt_time is not None: - if not np.allclose(nvt_time, 0 * unit.nanosecond): + if not np.allclose(nvt_time, 0 * offunit.nanosecond): errmsg = "NVT equilibration cannot be run in vacuum simulation" raise ValueError(errmsg) @@ -806,157 +503,3 @@ def _gather( for k, v in unsorted_vacuum_repeats.items(): repeats["vacuum"][str(k)] = sorted(v, key=lambda x: x.outputs["generation"]) return repeats - - -class AbsoluteSolvationVacuumUnit(BaseAbsoluteUnit): - """ - Protocol Unit for the vacuum phase of an absolute solvation free energy - """ - - simtype = "vacuum" - - def _get_components(self): - """ - Get the relevant components for a vacuum transformation. - - Returns - ------- - alchem_comps : dict[str, list[Component]] - A list of alchemical components - solv_comp : None - For the gas phase transformation, None will always be returned - for the solvent component of the chemical system. - prot_comp : Optional[ProteinComponent] - The protein component of the system, if it exists. - small_mols : dict[Component, OpenFF Molecule] - The openff Molecules to add to the system. This - is equivalent to the alchemical components in stateA (since - we only allow for disappearing ligands). - """ - stateA = self._inputs["stateA"] - alchem_comps = self._inputs["alchemical_components"] - - off_comps = {m: m.to_openff() for m in alchem_comps["stateA"]} - - _, prot_comp, _ = system_validation.get_components(stateA) - - # Notes: - # 1. Our input state will contain a solvent, we ``None`` that out - # since this is the gas phase unit. - # 2. Our small molecules will always just be the alchemical components - # (of stateA since we enforce only one disappearing ligand) - return alchem_comps, None, prot_comp, off_comps - - def _handle_settings(self) -> dict[str, SettingsBaseModel]: - """ - Extract the relevant settings for a vacuum transformation. - - Returns - ------- - settings : dict[str, SettingsBaseModel] - A dictionary with the following entries: - * forcefield_settings : OpenMMSystemGeneratorFFSettings - * thermo_settings : ThermoSettings - * charge_settings : OpenFFPartialChargeSettings - * solvation_settings : OpenMMSolvationSettings - * alchemical_settings : AlchemicalSettings - * lambda_settings : LambdaSettings - * engine_settings : OpenMMEngineSettings - * integrator_settings : IntegratorSettings - * equil_simulation_settings : MDSimulationSettings - * equil_output_settings : MDOutputSettings - * simulation_settings : SimulationSettings - * output_settings: MultiStateOutputSettings - """ - prot_settings = self._inputs["protocol"].settings - - settings = {} - settings["forcefield_settings"] = prot_settings.vacuum_forcefield_settings - settings["thermo_settings"] = prot_settings.thermo_settings - settings["charge_settings"] = prot_settings.partial_charge_settings - settings["solvation_settings"] = prot_settings.solvation_settings - settings["alchemical_settings"] = prot_settings.alchemical_settings - settings["lambda_settings"] = prot_settings.lambda_settings - settings["engine_settings"] = prot_settings.vacuum_engine_settings - settings["integrator_settings"] = prot_settings.integrator_settings - settings["equil_simulation_settings"] = prot_settings.vacuum_equil_simulation_settings - settings["equil_output_settings"] = prot_settings.vacuum_equil_output_settings - settings["simulation_settings"] = prot_settings.vacuum_simulation_settings - settings["output_settings"] = prot_settings.vacuum_output_settings - - return settings - - -class AbsoluteSolvationSolventUnit(BaseAbsoluteUnit): - """ - Protocol Unit for the solvent phase of an absolute solvation free energy - """ - - simtype = "solvent" - - def _get_components(self): - """ - Get the relevant components for a solvent transformation. - - Returns - ------- - alchem_comps : dict[str, Component] - A list of alchemical components - solv_comp : SolventComponent - The SolventComponent of the system - prot_comp : Optional[ProteinComponent] - The protein component of the system, if it exists. - small_mols : dict[SmallMoleculeComponent: OFFMolecule] - SmallMoleculeComponents to add to the system. - """ - stateA = self._inputs["stateA"] - alchem_comps = self._inputs["alchemical_components"] - - solv_comp, prot_comp, small_mols = system_validation.get_components(stateA) - off_comps = {m: m.to_openff() for m in small_mols} - - # We don't need to check that solv_comp is not None, otherwise - # an error will have been raised when calling `validate_solvent` - # in the Protocol's `_create`. - # Similarly we don't need to check prot_comp since that's also - # disallowed on create - return alchem_comps, solv_comp, prot_comp, off_comps - - def _handle_settings(self) -> dict[str, SettingsBaseModel]: - """ - Extract the relevant settings for a solvent transformation. - - Returns - ------- - settings : dict[str, SettingsBaseModel] - A dictionary with the following entries: - * forcefield_settings : OpenMMSystemGeneratorFFSettings - * thermo_settings : ThermoSettings - * charge_settings : OpenFFPartialChargeSettings - * solvation_settings : OpenMMSolvationSettings - * alchemical_settings : AlchemicalSettings - * lambda_settings : LambdaSettings - * engine_settings : OpenMMEngineSettings - * integrator_settings : IntegratorSettings - * equil_simulation_settings : MDSimulationSettings - * equil_output_settings : MDOutputSettings - * simulation_settings : MultiStateSimulationSettings - * output_settings: MultiStateOutputSettings - """ - prot_settings = self._inputs["protocol"].settings - - settings = {} - settings["forcefield_settings"] = prot_settings.solvent_forcefield_settings - settings["thermo_settings"] = prot_settings.thermo_settings - settings["charge_settings"] = prot_settings.partial_charge_settings - settings["solvation_settings"] = prot_settings.solvation_settings - settings["alchemical_settings"] = prot_settings.alchemical_settings - settings["lambda_settings"] = prot_settings.lambda_settings - settings["engine_settings"] = prot_settings.solvent_engine_settings - settings["integrator_settings"] = prot_settings.integrator_settings - settings["equil_simulation_settings"] = prot_settings.solvent_equil_simulation_settings - settings["equil_output_settings"] = prot_settings.solvent_equil_output_settings - settings["simulation_settings"] = prot_settings.solvent_simulation_settings - settings["output_settings"] = prot_settings.solvent_output_settings - - return settings diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py b/openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py index c991a573b..d5d963ff9 100644 --- a/openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py +++ b/openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py @@ -143,6 +143,17 @@ def test_create_independent_repeat_ids(benzene_modifications, T4_protein_compone assert len(repeat_ids) == 12 +def test_mda_universe_error(): + """ + Test that we get an error if we pass no positions or trajectory + when calling the mda Universe getter. + """ + with pytest.raises(ValueError, match="No positions to create"): + _ = openmm_afe.AbsoluteBindingComplexUnit._get_mda_universe( + topology="foo", positions=None, trajectory=None + ) + + class TestT4LysozymeDryRun: solvent = SolventComponent(ion_concentration=0 * offunit.molar) num_all_not_water = 2634 From 01daa02aa811c22629a8b2c81c0084b48eaf22b8 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Thu, 15 Jan 2026 21:35:54 +0000 Subject: [PATCH 31/47] Create setup, run, and analysis units for HybridTop Protocol (#1773) * Turn Hybrid Topology protocol into 3 units. --------- Co-authored-by: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> --- openfe/protocols/openmm_rfe/__init__.py | 6 +- .../protocols/openmm_rfe/equil_rfe_methods.py | 6 +- .../openmm_rfe/hybridtop_protocol_results.py | 6 +- .../openmm_rfe/hybridtop_protocols.py | 51 +- .../protocols/openmm_rfe/hybridtop_units.py | 587 ++++++++++++------ openfe/protocols/openmm_septop/base.py | 3 +- .../openmm_septop/equil_septop_method.py | 2 +- openfe/protocols/openmm_septop/utils.py | 77 --- .../protocols/openmm_utils/serialization.py | 64 ++ openfe/tests/conftest.py | 2 +- .../openmm_rfe/RHFEProtocol_json_results.gz | Bin 340286 -> 26759 bytes .../openmm_abfe/test_abfe_energies.py | 2 +- .../openmm_rfe/test_hybrid_top_protocol.py | 352 ++++++++--- .../openmm_rfe/test_hybrid_top_slow.py | 31 +- .../test_hybrid_top_tokenization.py | 88 ++- .../openmm_septop/test_septop_protocol.py | 2 +- .../openmm_septop/test_septop_slow.py | 3 +- ...s.py => test_openmmutils_serialization.py} | 27 +- openfecli/commands/gather.py | 27 +- openfecli/tests/test_rbfe_tutorial.py | 117 +++- 20 files changed, 989 insertions(+), 464 deletions(-) create mode 100644 openfe/protocols/openmm_utils/serialization.py rename openfe/tests/protocols/{openmm_septop/test_septop_utils.py => test_openmmutils_serialization.py} (68%) diff --git a/openfe/protocols/openmm_rfe/__init__.py b/openfe/protocols/openmm_rfe/__init__.py index ca1ae0fd2..f0fe367c8 100644 --- a/openfe/protocols/openmm_rfe/__init__.py +++ b/openfe/protocols/openmm_rfe/__init__.py @@ -5,4 +5,8 @@ from .equil_rfe_settings import RelativeHybridTopologyProtocolSettings from .hybridtop_protocol_results import RelativeHybridTopologyProtocolResult from .hybridtop_protocols import RelativeHybridTopologyProtocol -from .hybridtop_units import RelativeHybridTopologyProtocolUnit +from .hybridtop_units import ( + HybridTopologyMultiStateAnalysisUnit, + HybridTopologyMultiStateSimulationUnit, + HybridTopologySetupUnit, +) diff --git a/openfe/protocols/openmm_rfe/equil_rfe_methods.py b/openfe/protocols/openmm_rfe/equil_rfe_methods.py index cdd80c863..2b0d37e1d 100644 --- a/openfe/protocols/openmm_rfe/equil_rfe_methods.py +++ b/openfe/protocols/openmm_rfe/equil_rfe_methods.py @@ -19,4 +19,8 @@ from .equil_rfe_settings import RelativeHybridTopologyProtocolSettings from .hybridtop_protocol_results import RelativeHybridTopologyProtocolResult from .hybridtop_protocols import RelativeHybridTopologyProtocol -from .hybridtop_units import RelativeHybridTopologyProtocolUnit +from .hybridtop_units import ( + HybridTopologyMultiStateAnalysisUnit, + HybridTopologyMultiStateSimulationUnit, + HybridTopologySetupUnit, +) diff --git a/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py b/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py index 08214a12a..ca865f8e0 100644 --- a/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py +++ b/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py @@ -33,7 +33,7 @@ def __init__(self, **data): def compute_mean_estimate(dGs: list[Quantity]) -> Quantity: u = dGs[0].u # convert all values to units of the first value, then take average of magnitude - # this would avoid a screwy case where each value was in different units + # this would avoid an edge case where each value was in different units vals = np.asarray([dG.to(u).m for dG in dGs]) return np.average(vals) * u @@ -199,9 +199,9 @@ def is_file(filename: str): replica_states = [] for pus in self.data.values(): - nc = is_file(pus[0].outputs["nc"]) + nc = is_file(pus[0].outputs["trajectory"]) dir_path = nc.parents[0] - chk = is_file(dir_path / pus[0].outputs["last_checkpoint"]).name + chk = is_file(pus[0].outputs["checkpoint"]).name reporter = multistate.MultiStateReporter( storage=nc, checkpoint_storage=chk, open_mode="r" ) diff --git a/openfe/protocols/openmm_rfe/hybridtop_protocols.py b/openfe/protocols/openmm_rfe/hybridtop_protocols.py index 4daabd08f..984cc2f99 100644 --- a/openfe/protocols/openmm_rfe/hybridtop_protocols.py +++ b/openfe/protocols/openmm_rfe/hybridtop_protocols.py @@ -49,7 +49,11 @@ RelativeHybridTopologyProtocolSettings, ) from .hybridtop_protocol_results import RelativeHybridTopologyProtocolResult -from .hybridtop_units import RelativeHybridTopologyProtocolUnit +from .hybridtop_units import ( + HybridTopologyMultiStateAnalysisUnit, + HybridTopologyMultiStateSimulationUnit, + HybridTopologySetupUnit, +) logger = logging.getLogger(__name__) @@ -593,23 +597,47 @@ def _create( Anames = ",".join(c.name for c in alchem_comps["stateA"]) Bnames = ",".join(c.name for c in alchem_comps["stateB"]) - # our DAG has no dependencies, so just list units - n_repeats = self.settings.protocol_repeats + # DAG dependency is setup -> simulation -> analysis + # |---------------------> + setup_units = [] + simulation_units = [] + analysis_units = [] + + for i in range(self.settings.protocol_repeats): + repeat_id = int(uuid.uuid4()) - units = [ - RelativeHybridTopologyProtocolUnit( + setup = HybridTopologySetupUnit( protocol=self, stateA=stateA, stateB=stateB, ligandmapping=ligandmapping, + alchemical_components=alchem_comps, + generation=0, + repeat_id=repeat_id, + name=(f"HybridTopology Setup: {Anames} to {Bnames} repeat {i} generation 0"), + ) + + simulation = HybridTopologyMultiStateSimulationUnit( + protocol=self, + setup_results=setup, + generation=0, + repeat_id=repeat_id, + name=(f"HybridTopology Simulation: {Anames} to {Bnames} repeat {i} generation 0"), + ) + + analysis = HybridTopologyMultiStateAnalysisUnit( + protocol=self, + setup_results=setup, + simulation_results=simulation, generation=0, - repeat_id=int(uuid.uuid4()), - name=f"{Anames} to {Bnames} repeat {i} generation 0", + repeat_id=repeat_id, + name=(f"HybridTopology Analysis: {Anames} to {Bnames} repeat {i} generation 0"), ) - for i in range(n_repeats) - ] + setup_units.append(setup) + simulation_units.append(simulation) + analysis_units.append(analysis) - return units + return [*setup_units, *simulation_units, *analysis_units] def _gather(self, protocol_dag_results: Iterable[gufe.ProtocolDAGResult]) -> dict[str, Any]: # result units will have a repeat_id and generations within this repeat_id @@ -618,7 +646,8 @@ def _gather(self, protocol_dag_results: Iterable[gufe.ProtocolDAGResult]) -> dic for d in protocol_dag_results: pu: gufe.ProtocolUnitResult for pu in d.protocol_unit_results: - if not pu.ok(): + # We only need the analysis units that are ok + if ("Analysis" not in pu.name) or (not pu.ok()): continue unsorted_repeats[pu.outputs["repeat_id"]].append(pu) diff --git a/openfe/protocols/openmm_rfe/hybridtop_units.py b/openfe/protocols/openmm_rfe/hybridtop_units.py index 1d7965b32..588ce3151 100644 --- a/openfe/protocols/openmm_rfe/hybridtop_units.py +++ b/openfe/protocols/openmm_rfe/hybridtop_units.py @@ -15,7 +15,7 @@ import pathlib import subprocess from itertools import chain -from typing import Any, Optional +from typing import Any import gufe import matplotlib.pyplot as plt @@ -57,6 +57,10 @@ system_creation, system_validation, ) +from ..openmm_utils.serialization import ( + deserialize, + serialize, +) from . import _rfe_utils from ._rfe_utils.relative import HybridTopologyFactory from .equil_rfe_settings import ( @@ -74,55 +78,7 @@ logger = logging.getLogger(__name__) -class RelativeHybridTopologyProtocolUnit(gufe.ProtocolUnit): - """ - Calculates the relative free energy of an alchemical ligand transformation. - """ - - def __init__( - self, - *, - protocol: gufe.Protocol, - stateA: ChemicalSystem, - stateB: ChemicalSystem, - ligandmapping: LigandAtomMapping, - generation: int, - repeat_id: int, - name: Optional[str] = None, - ): - """ - Parameters - ---------- - protocol : RelativeHybridTopologyProtocol - protocol used to create this Unit. Contains key information such - as the settings. - stateA, stateB : ChemicalSystem - the two ligand SmallMoleculeComponents to transform between. The - transformation will go from ligandA to ligandB. - ligandmapping : LigandAtomMapping - the mapping of atoms between the two ligand components - repeat_id : int - identifier for which repeat (aka replica/clone) this Unit is - generation : int - counter for how many times this repeat has been extended - name : str, optional - human-readable identifier for this Unit - - Notes - ----- - The mapping used must not involve any elemental changes. A check for - this is done on class creation. - """ - super().__init__( - name=name, - protocol=protocol, - stateA=stateA, - stateB=stateB, - ligandmapping=ligandmapping, - repeat_id=repeat_id, - generation=generation, - ) - +class HybridTopologyUnitMixin: def _prepare( self, verbose: bool, @@ -145,7 +101,7 @@ def _prepare( self.verbose = verbose if self.verbose: - self.logger.info("Setting up the hybrid topology simulation") + self.logger.info("Setting up the hybrid topology simulation") # type: ignore[attr-defined] # set basepaths def _set_optional_path(basepath): @@ -187,15 +143,16 @@ def _get_settings( protocol_settings["engine_settings"] = settings.engine_settings return protocol_settings + +class HybridTopologySetupUnit(gufe.ProtocolUnit, HybridTopologyUnitMixin): + """ + Calculates the relative free energy of an alchemical ligand transformation. + """ + @staticmethod def _get_components( stateA: ChemicalSystem, stateB: ChemicalSystem - ) -> tuple[ - dict[str, Component], - SolventComponent, - ProteinComponent, - dict[SmallMoleculeComponent, OFFMolecule], - ]: + ) -> tuple[SolventComponent, ProteinComponent, dict[SmallMoleculeComponent, OFFMolecule]]: """ Get the components from the ChemicalSystem inputs. @@ -208,8 +165,6 @@ def _get_components( Returns ------- - alchem_comps : dict[str, Component] - Dictionary of alchemical components. solv_comp : SolventComponent The solvent component. protein_comp : ProteinComponent @@ -218,14 +173,12 @@ def _get_components( Dictionary of small molecule components paired with their OpenFF Molecule. """ - alchem_comps = system_validation.get_alchemical_components(stateA, stateB) - solvent_comp, protein_comp, smcs_A = system_validation.get_components(stateA) _, _, smcs_B = system_validation.get_components(stateB) small_mols = {m: m.to_openff() for m in set(smcs_A).union(set(smcs_B))} - return alchem_comps, solvent_comp, protein_comp, small_mols + return solvent_comp, protein_comp, small_mols @staticmethod def _assign_partial_charges( @@ -723,6 +676,140 @@ def _subsample_topology( return selection_indices + def run( + self, + *, + dry: bool = False, + verbose: bool = True, + scratch_basepath: pathlib.Path | None = None, + shared_basepath: pathlib.Path | None = None, + ) -> dict[str, Any]: + """Setup a hybrid topology system. + + Parameters + ---------- + dry : bool + Do a dry run of the calculation, creating all necessary hybrid + system components (topology, system, sampler, etc...) but without + running the simulation. + verbose : bool + Verbose output of the simulation progress. Output is provided via + INFO level logging. + scratch_basepath: pathlib.Path | None + Where to store temporary files, defaults to current working directory + shared_basepath : pathlib.Path | None + Where to run the calculation, defaults to current working directory + + Returns + ------- + dict + Outputs created by the setup unit or the debug objects + (e.g. HybridTopologyFactory) if ``dry==True``. + + Raises + ------ + error + Exception if anything failed + """ + # Prepare paths & verbosity + self._prepare(verbose, scratch_basepath, shared_basepath) + + # Get settings + settings = self._get_settings(self._inputs["protocol"].settings) + + # Get components + stateA = self._inputs["stateA"] + stateB = self._inputs["stateB"] + mapping = self._inputs["ligandmapping"] + alchem_comps = self._inputs["alchemical_components"] + solvent_comp, protein_comp, small_mols = self._get_components(stateA, stateB) + + # Assign partial charges now to avoid any discrepancies later + self._assign_partial_charges(settings["charge_settings"], small_mols) + + ( + stateA_system, + stateA_topology, + stateA_positions, + stateB_system, + stateB_topology, + stateB_positions, + system_mappings, + ) = self._get_omm_objects( + stateA=stateA, + stateB=stateB, + mapping=mapping, + settings=settings, + protein_component=protein_comp, + solvent_component=solvent_comp, + small_mols=small_mols, + ) + + # Get the hybrid factory & system + hybrid_factory, hybrid_system = self._get_alchemical_system( + stateA_system=stateA_system, + stateA_positions=stateA_positions, + stateA_topology=stateA_topology, + stateB_system=stateB_system, + stateB_positions=stateB_positions, + stateB_topology=stateB_topology, + system_mappings=system_mappings, + alchemical_settings=settings["alchemical_settings"], + ) + + # Subselect system based on user inputs & write initial PDB + selection_indices = self._subsample_topology( + hybrid_topology=hybrid_factory.hybrid_topology, + hybrid_positions=hybrid_factory.hybrid_positions, + output_selection=settings["output_settings"].output_indices, + output_filename=settings["output_settings"].output_structure, + atom_classes=hybrid_factory._atom_classes, + ) + + # Serialize things + # OpenMM System + system_outfile = self.shared_basepath / "hybrid_system.xml.bz2" + serialize(hybrid_system, system_outfile) + + # Positions + positions_outfile = self.shared_basepath / "hybrid_positions.npy" + npy_positions = from_openmm(hybrid_factory.hybrid_positions).to("nanometer").m + np.save(positions_outfile, npy_positions) + + unit_results_dict = { + "system": system_outfile, + "positions": positions_outfile, + "pdb_structure": self.shared_basepath / settings["output_settings"].output_structure, + "selection_indices": selection_indices, + } + + if dry: + unit_results_dict |= { + # Adding unserialized objects so we can directly use them + # to chain units in tests + "hybrid_factory": hybrid_factory, + "hybrid_system": hybrid_system, + "hybrid_positions": hybrid_factory.hybrid_positions, + } + + return unit_results_dict + + def _execute( + self, + ctx: gufe.Context, + **inputs, + ) -> dict[str, Any]: + log_system_probe(logging.INFO, paths=[ctx.scratch]) + outputs = self.run(scratch_basepath=ctx.scratch, shared_basepath=ctx.shared) + + return { + "repeat_id": self._inputs["repeat_id"], + "generation": self._inputs["generation"], + **outputs, + } + + +class HybridTopologyMultiStateSimulationUnit(gufe.ProtocolUnit, HybridTopologyUnitMixin): @staticmethod def _get_integrator( integrator_settings: IntegratorSettings, @@ -978,12 +1065,6 @@ def _run_simulation( Simulation output control settings. dry : bool Whether or not to dry run the simulation. - - Returns - ------- - unit_results_dict : dict | None - A dictionary containing the free energy results to report. - ``None`` if it is a dry run. """ # Get the relevant simulation steps mc_steps = settings_validation.convert_steps_per_iteration( @@ -1023,21 +1104,6 @@ def _run_simulation( if self.verbose: self.logger.info("production phase complete") - - if self.verbose: - self.logger.info("post-simulation result analysis") - - # calculate relevant analysis of the free energies & sampling - analyzer = multistate_analysis.MultistateEquilFEAnalysis( - reporter, - sampling_method=simulation_settings.sampler_method.lower(), - result_units=offunit.kilocalorie_per_mole, - ) - analyzer.plot(filepath=self.shared_basepath, filename_prefix="") - analyzer.close() - - return analyzer.unit_results_dict - else: # We ran a dry simulation # close reporter when you're done, prevent file handle clashes @@ -1052,15 +1118,27 @@ def _run_simulation( for fn in fns: os.remove(fn) - return None - def run( - self, *, dry=False, verbose=True, scratch_basepath=None, shared_basepath=None + self, + *, + system: openmm.System, + positions: openmm.unit.Quantity, + selection_indices: npt.NDArray, + dry: bool = False, + verbose: bool = True, + scratch_basepath: pathlib.Path | None = None, + shared_basepath: pathlib.Path | None = None, ) -> dict[str, Any]: - """Run the relative free energy calculation. + """Run the free energy calculation using a multistate sampler. Parameters ---------- + system : openmm.System + The System to simulate. + positions : openmm.unit.Quantity + The positions of the System. + selection_indices : npt.NDArray + Indices of the System particles to write to file. dry : bool Do a dry run of the calculation, creating all necessary hybrid system components (topology, system, sampler, etc...) but without @@ -1068,9 +1146,9 @@ def run( verbose : bool Verbose output of the simulation progress. Output is provided via INFO level logging. - scratch_basepath: Pathlike, optional + scratch_basepath: pathlib.Path | None Where to store temporary files, defaults to current working directory - shared_basepath : Pathlike, optional + shared_basepath : pathlib.Path | None Where to run the calculation, defaults to current working directory Returns @@ -1087,57 +1165,9 @@ def run( # Prepare paths & verbosity self._prepare(verbose, scratch_basepath, shared_basepath) - # Get settings + # Get the settings settings = self._get_settings(self._inputs["protocol"].settings) - # Get components - stateA = self._inputs["stateA"] - stateB = self._inputs["stateB"] - mapping = self._inputs["ligandmapping"] - alchem_comps, solvent_comp, protein_comp, small_mols = self._get_components(stateA, stateB) - - # Assign partial charges now to avoid any discrepancies later - self._assign_partial_charges(settings["charge_settings"], small_mols) - - ( - stateA_system, - stateA_topology, - stateA_positions, - stateB_system, - stateB_topology, - stateB_positions, - system_mappings, - ) = self._get_omm_objects( - stateA=stateA, - stateB=stateB, - mapping=mapping, - settings=settings, - protein_component=protein_comp, - solvent_component=solvent_comp, - small_mols=small_mols, - ) - - # Get the hybrid factory & system - hybrid_factory, hybrid_system = self._get_alchemical_system( - stateA_system=stateA_system, - stateA_positions=stateA_positions, - stateA_topology=stateA_topology, - stateB_system=stateB_system, - stateB_positions=stateB_positions, - stateB_topology=stateB_topology, - system_mappings=system_mappings, - alchemical_settings=settings["alchemical_settings"], - ) - - # Subselect system based on user inputs & write initial PDB - selection_indices = self._subsample_topology( - hybrid_topology=hybrid_factory.hybrid_topology, - hybrid_positions=hybrid_factory.hybrid_positions, - output_selection=settings["output_settings"].output_indices, - output_filename=settings["output_settings"].output_structure, - atom_classes=hybrid_factory._atom_classes, - ) - # Get the lambda schedule # TODO - this should be better exposed to users lambdas = _rfe_utils.lambdaprotocol.LambdaProtocol( @@ -1157,11 +1187,11 @@ def run( integrator = self._get_integrator( integrator_settings=settings["integrator_settings"], simulation_settings=settings["simulation_settings"], - system=hybrid_system, + system=system, ) try: - # get the reporter + # Get the reporter reporter = self._get_reporter( storage_path=self.shared_basepath, selection_indices=selection_indices, @@ -1171,8 +1201,8 @@ def run( # Get sampler sampler = self._get_sampler( - system=hybrid_system, - positions=hybrid_factory.hybrid_positions, + system=system, + positions=positions, lambdas=lambdas, integrator=integrator, reporter=reporter, @@ -1183,7 +1213,7 @@ def run( dry=dry, ) - unit_results_dict = self._run_simulation( + self._run_simulation( sampler=sampler, reporter=reporter, simulation_settings=settings["simulation_settings"], @@ -1213,40 +1243,115 @@ def run( del integrator, sampler if not dry: # pragma: no-cover - nc = self.shared_basepath / settings["output_settings"].output_filename - chk = settings["output_settings"].checkpoint_storage_filename - unit_results_dict["nc"] = nc - unit_results_dict["last_checkpoint"] = chk - unit_results_dict["selection_indices"] = selection_indices - return unit_results_dict + return { + "nc": self.shared_basepath / settings["output_settings"].output_filename, + "checkpoint": self.shared_basepath + / settings["output_settings"].checkpoint_storage_filename, + } else: return { - "debug": { - "sampler": sampler, - "hybrid_factory": hybrid_factory, - } + "sampler": sampler, + "integrator": integrator, } + def _execute( + self, + ctx: gufe.Context, + *, + setup_results, + **inputs, + ) -> dict[str, Any]: + log_system_probe(logging.INFO, paths=[ctx.scratch]) + # Get the relevant inputs + system = deserialize(setup_results.outputs["system"]) + positions = to_openmm(np.load(setup_results.outputs["positions"]) * offunit.nm) + selection_indices = setup_results.outputs["selection_indices"] + + # Run the unit + outputs = self.run( + system=system, + positions=positions, + selection_indices=selection_indices, + scratch_basepath=ctx.scratch, + shared_basepath=ctx.shared, + ) + + return { + "repeat_id": self._inputs["repeat_id"], + "generation": self._inputs["generation"], + **outputs, + } + + +class HybridTopologyMultiStateAnalysisUnit(gufe.ProtocolUnit, HybridTopologyUnitMixin): @staticmethod - def structural_analysis( - scratch: pathlib.Path, - shared: pathlib.Path, - pdb_filename: str, - trj_filename: str, + def _analyze_multistate_energies( + trajectory: pathlib.Path, + checkpoint: pathlib.Path, + sampler_method: str, + output_directory: pathlib.Path, + dry: bool, + ): + """ + Analyze multistate energies and generate plots. + + Parameters + ---------- + trajectory : pathlib.Path + Path to the NetCDF trajectory file. + checkpoint : pathlib.Path + The name of the checkpoint file. Note this is + relative in path to the trajectory file. + sampler_method : str + The multistate sampler method used. + output_directory : pathlib.Path + The path to where plots will be written. + dry : bool + Whether or not we are running a dry run. + """ + reporter = multistate.MultiStateReporter( + storage=trajectory, + # Note: openmmtools only wants the name of the checkpoint + # file, it assumes it to be in the same place as the trajectory + checkpoint_storage=checkpoint.name, + open_mode="r", + ) + + analyzer = multistate_analysis.MultistateEquilFEAnalysis( + reporter=reporter, + sampling_method=sampler_method, + result_units=offunit.kilocalorie_per_mole, + ) + + # Only create plots when not doing a dry run + if not dry: + analyzer.plot(filepath=output_directory, filename_prefix="") + + analyzer.close() + reporter.close() + return analyzer.unit_results_dict + + @staticmethod + def _structural_analysis( + pdb_file: pathlib.Path, + trj_file: pathlib.Path, + output_directory: pathlib.Path, + dry: bool, ) -> dict[str, str | pathlib.Path]: """ Run structural analysis using ``openfe-analysis``. Parameters ---------- - scratch : pathlib.Path - Path to the scratch directory. - shared : pathlib.Path - Path to the shared directory. - pdb_filename : str - The PDB file name. - trj_filename : str - The trajectory file name. + pdb_file : pathlib.Path + Path to the PDB file. + trj_file : pathlib.Path + Path to the trajectory file. + output_directory : pathlib.Path + The output directory where plots and the data NPZ file + will be stored. + dry : bool + Whether or not we are running a dry run. Returns ------- @@ -1256,36 +1361,35 @@ def structural_analysis( Notes ----- - Don't put energy analysis here, it uses the open file reporter - whereas structural stuff requires the file handle to be closed. + Don't put energy analysis here as it uses the MultiStateReporter, + the structural analysis requires the file handle to be closed. """ from openfe_analysis import rmsd - pdb_file = shared / pdb_filename - trj_file = shared / trj_filename - try: data = rmsd.gather_rms_data(pdb_file, trj_file) - # TODO: change this to more specific exception types + # TODO: eventually change this to more specific exception types except Exception as e: return {"structural_analysis_error": str(e)} - # Generate plots - if d := data["protein_2D_RMSD"]: - fig = plotting.plot_2D_rmsd(d) - fig.savefig(shared / "protein_2D_RMSD.png") - plt.close(fig) - f2 = plotting.plot_ligand_COM_drift(data["time(ps)"], data["ligand_wander"]) - f2.savefig(shared / "ligand_COM_drift.png") - plt.close(f2) - - f3 = plotting.plot_ligand_RMSD(data["time(ps)"], data["ligand_RMSD"]) - f3.savefig(shared / "ligand_RMSD.png") - plt.close(f3) - - # Write out NPZ with the analyzed data + # Generate relevant plots if not a dry run + if not dry: + if d := data["protein_2D_RMSD"]: + fig = plotting.plot_2D_rmsd(d) + fig.savefig(output_directory / "protein_2D_RMSD.png") + plt.close(fig) + f2 = plotting.plot_ligand_COM_drift(data["time(ps)"], data["ligand_wander"]) + f2.savefig(output_directory / "ligand_COM_drift.png") + plt.close(f2) + + f3 = plotting.plot_ligand_RMSD(data["time(ps)"], data["ligand_RMSD"]) + f3.savefig(output_directory / "ligand_RMSD.png") + plt.close(f3) + + # Write out an NPZ with all the relevant analysis data + npz_file = output_directory / "structural_analysis.npz" np.savez_compressed( - shared / "structural_analysis.npz", + npz_file, protein_RMSD=np.asarray(data["protein_RMSD"], dtype=np.float32), ligand_RMSD=np.asarray(data["ligand_RMSD"], dtype=np.float32), ligand_COM_drift=np.asarray(data["ligand_wander"], dtype=np.float32), @@ -1293,27 +1397,116 @@ def structural_analysis( time_ps=np.asarray(data["time(ps)"], dtype=np.float32), ) - return {"structural_analysis": shared / "structural_analysis.npz"} + return {"structural_analysis": npz_file} + + def run( + self, + *, + pdb_file: pathlib.Path, + trajectory: pathlib.Path, + checkpoint: pathlib.Path, + dry: bool = False, + verbose: bool = True, + scratch_basepath: pathlib.Path | None = None, + shared_basepath: pathlib.Path | None = None, + ) -> dict[str, Any]: + """Analyze the multistate simulation. + + Parameters + ---------- + pdb_file : pathlib.Path + Path to the PDB file representing the subsampled structure. + trajectory : pathlib.Path + Path to the MultiStateReporter generated NetCDF file. + checkpoint : pathlib.Path + Path to the checkpoint file generated by MultiStateReporter. + dry : bool + Do a dry run of the calculation, creating all necessary hybrid + system components (topology, system, sampler, etc...) but without + running the simulation. + verbose : bool + Verbose output of the simulation progress. Output is provided via + INFO level logging. + scratch_basepath: pathlib.Path | None + Where to store temporary files, defaults to current working directory + shared_basepath : pathlib.Path | None + Where to run the calculation, defaults to current working directory + + Returns + ------- + dict + Outputs created in the basepath directory or the debug objects + (i.e. sampler) if ``dry==True``. + + Raises + ------ + error + Exception if anything failed + """ + # Prepare paths & verbosity + self._prepare(verbose, scratch_basepath, shared_basepath) + + # Get the settings + settings = self._get_settings(self._inputs["protocol"].settings) + + # Energies analysis + if verbose: + self.logger.info("Analyzing energies") + + energy_analysis = self._analyze_multistate_energies( + trajectory=trajectory, + checkpoint=checkpoint, + sampler_method=settings["simulation_settings"].sampler_method.lower(), + output_directory=self.shared_basepath, + dry=dry, + ) + + # Structural analysis + if verbose: + self.logger.info("Analyzing structural outputs") + + structural_analysis = self._structural_analysis( + pdb_file=pdb_file, + trj_file=trajectory, + output_directory=self.shared_basepath, + dry=dry, + ) + + # Return relevant things + outputs = energy_analysis | structural_analysis + return outputs def _execute( self, ctx: gufe.Context, - **kwargs, + *, + setup_results, + simulation_results, + **inputs, ) -> dict[str, Any]: log_system_probe(logging.INFO, paths=[ctx.scratch]) - outputs = self.run(scratch_basepath=ctx.scratch, shared_basepath=ctx.shared) - - structural_analysis_outputs = self.structural_analysis( - scratch=ctx.scratch, - shared=ctx.shared, - pdb_filename=self._inputs["protocol"].settings.output_settings.output_structure, - trj_filename=self._inputs["protocol"].settings.output_settings.output_filename, + pdb_file = setup_results.outputs["pdb_structure"] + selection_indices = setup_results.outputs["selection_indices"] + trajectory = simulation_results.outputs["nc"] + checkpoint = simulation_results.outputs["checkpoint"] + + outputs = self.run( + pdb_file=pdb_file, + trajectory=trajectory, + checkpoint=checkpoint, + scratch_basepath=ctx.scratch, + shared_basepath=ctx.shared, ) return { "repeat_id": self._inputs["repeat_id"], "generation": self._inputs["generation"], + # We include various other outputs here to make + # things easier when gathering. + "pdb_structure": pdb_file, + "trajectory": trajectory, + "checkpoint": checkpoint, + "selection_indices": selection_indices, **outputs, - **structural_analysis_outputs, } diff --git a/openfe/protocols/openmm_septop/base.py b/openfe/protocols/openmm_septop/base.py index 000f0b14e..b3af8a4fd 100644 --- a/openfe/protocols/openmm_septop/base.py +++ b/openfe/protocols/openmm_septop/base.py @@ -58,6 +58,7 @@ from openfe.protocols.openmm_md.plain_md_methods import PlainMDProtocolUnit from openfe.protocols.openmm_utils import omm_compute from openfe.protocols.openmm_utils.omm_settings import SettingsBaseModel +from openfe.protocols.openmm_utils.serialization import deserialize from openfe.utils import without_oechem_backend from ..openmm_utils import ( @@ -66,7 +67,7 @@ settings_validation, system_creation, ) -from .utils import SepTopParameterState, deserialize +from .utils import SepTopParameterState logger = logging.getLogger(__name__) diff --git a/openfe/protocols/openmm_septop/equil_septop_method.py b/openfe/protocols/openmm_septop/equil_septop_method.py index 9375747bc..51e0fdedd 100644 --- a/openfe/protocols/openmm_septop/equil_septop_method.py +++ b/openfe/protocols/openmm_septop/equil_septop_method.py @@ -81,6 +81,7 @@ SepTopSettings, SettingsBaseModel, ) +from openfe.protocols.openmm_utils.serialization import serialize from openfe.protocols.restraint_utils import geometry from openfe.protocols.restraint_utils.geometry.boresch import BoreschRestraintGeometry from openfe.protocols.restraint_utils.openmm import omm_restraints @@ -96,7 +97,6 @@ DistanceRestraintSettings, ) from .base import BaseSepTopRunUnit, BaseSepTopSetupUnit, _pre_equilibrate -from .utils import serialize due.cite( Doi("10.1021/acs.jctc.3c00282"), diff --git a/openfe/protocols/openmm_septop/utils.py b/openfe/protocols/openmm_septop/utils.py index 53744b4d7..38d7f1d34 100644 --- a/openfe/protocols/openmm_septop/utils.py +++ b/openfe/protocols/openmm_septop/utils.py @@ -1,84 +1,7 @@ -import os -import pathlib - from openmmtools import states from openmmtools.states import GlobalParameterState -def serialize(item, filename: pathlib.Path): - """ - Serialize an OpenMM System, State, or Integrator. - - Parameters - ---------- - item : System, State, or Integrator - The thing to be serialized - filename : str - The filename to serialize to - """ - from openmm import XmlSerializer - - # Create parent directory if it doesn't exist - filename_basedir = filename.parent - if not filename_basedir.exists(): - os.makedirs(filename_basedir) - - if filename.suffix == ".gz": - import gzip - - with gzip.open(filename, mode="wb") as outfile: - serialized_thing = XmlSerializer.serialize(item) - outfile.write(serialized_thing.encode()) - elif filename.suffix == ".bz2": - import bz2 - - with bz2.open(filename, mode="wb") as outfile: - serialized_thing = XmlSerializer.serialize(item) - outfile.write(serialized_thing.encode()) - else: - with open(filename, mode="w") as outfile: - serialized_thing = XmlSerializer.serialize(item) - outfile.write(serialized_thing) - - -def deserialize(filename: pathlib.Path): - """ - Deserialize an OpenMM System, State, or Integrator. - - Parameters - ---------- - item : System, State, or Integrator - The thing to be serialized - filename : str - The filename to serialize to - """ - from openmm import XmlSerializer - - # Create parent directory if it doesn't exist - filename_basedir = filename.parent - if not filename_basedir.exists(): - os.makedirs(filename_basedir) - - if filename.suffix == ".gz": - import gzip - - with gzip.open(filename, mode="rb") as infile: - serialized_thing = infile.read().decode() - item = XmlSerializer.deserialize(serialized_thing) - elif filename.suffix == ".bz2": - import bz2 - - with bz2.open(filename, mode="rb") as infile: - serialized_thing = infile.read().decode() - item = XmlSerializer.deserialize(serialized_thing) - else: - with open(filename) as infile: - serialized_thing = infile.read() - item = XmlSerializer.deserialize(serialized_thing) - - return item - - class SepTopParameterState(GlobalParameterState): """ Composable state to control lambda parameters for two ligands. diff --git a/openfe/protocols/openmm_utils/serialization.py b/openfe/protocols/openmm_utils/serialization.py new file mode 100644 index 000000000..9b624291d --- /dev/null +++ b/openfe/protocols/openmm_utils/serialization.py @@ -0,0 +1,64 @@ +import os +import pathlib + + +def serialize(item, filename: pathlib.Path): + """ + Serialize an OpenMM System, State, or Integrator. + + Parameters + ---------- + item : System, State, or Integrator + The thing to be serialized + filename : str + The filename to serialize to + """ + from openmm import XmlSerializer + + # Create parent directory if it doesn't exist + filename_basedir = filename.parent + if not filename_basedir.exists(): + os.makedirs(filename_basedir) + + if filename.suffix == ".bz2": + import bz2 + + with bz2.open(filename, mode="wb") as outfile: + serialized_thing = XmlSerializer.serialize(item) + outfile.write(serialized_thing.encode()) + else: + with open(filename, mode="w") as outfile: + serialized_thing = XmlSerializer.serialize(item) + outfile.write(serialized_thing) + + +def deserialize(filename: pathlib.Path): + """ + Deserialize an OpenMM System, State, or Integrator. + + Parameters + ---------- + item : System, State, or Integrator + The thing to be serialized + filename : str + The filename to serialize to + """ + from openmm import XmlSerializer + + # Create parent directory if it doesn't exist + filename_basedir = filename.parent + if not filename_basedir.exists(): + os.makedirs(filename_basedir) + + if filename.suffix == ".bz2": + import bz2 + + with bz2.open(filename, mode="rb") as infile: + serialized_thing = infile.read().decode() + item = XmlSerializer.deserialize(serialized_thing) + else: + with open(filename) as infile: + serialized_thing = infile.read() + item = XmlSerializer.deserialize(serialized_thing) + + return item diff --git a/openfe/tests/conftest.py b/openfe/tests/conftest.py index a1bec3b2c..f2358757f 100644 --- a/openfe/tests/conftest.py +++ b/openfe/tests/conftest.py @@ -23,7 +23,7 @@ import openfe from openfe.protocols.openmm_rfe import RelativeHybridTopologyProtocol from openfe.protocols.openmm_rfe._rfe_utils.relative import HybridTopologyFactory -from openfe.protocols.openmm_septop.utils import deserialize +from openfe.protocols.openmm_utils.serialization import deserialize from openfe.tests.protocols.openmm_rfe.helpers import make_htf diff --git a/openfe/tests/data/openmm_rfe/RHFEProtocol_json_results.gz b/openfe/tests/data/openmm_rfe/RHFEProtocol_json_results.gz index e0bd6e8336234cf493fb7f7976a703ee5ebbce1f..e3a327d232b2097d56a9f47e9efef76cb98246ba 100644 GIT binary patch literal 26759 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ɍB$$NqE=&bmS+-+IKB!i_2v8qDSTv>B-kn3}_LlUbF1&v@5F~D0@NU z>x9l3Db);we5+`E=d5zZKD0&R1F{!APx3#Mco6fbkH-*{9rEv22sLIVdqoH}SQFu16N{W_YTb z%3c#aCtyoL2xGHnAy;Tq>z$kWtj~nd4=)0#2y-!?Wb_>fJ0Y%8@(8^3_Iedt;w$k2 zgS88*nrw2^g<#CTD!xq^{mLu)FUOEeTd@htT5O^*shlj8*3a5;bh2yfb);NlS^-aSTb~cKSQLS=9hibn|`e|uqefM0L<0sLd0B&*)&f>G``{G;UbSQ3cIMn5x zD)9^<@&~gO;fK5T@YAMPuD6mtkoleDrYa2!$Omlg?43efZ3-2PIStuRuH9YVx)oNp zAO#(TnVIX4!^N6(LWVy()|_UVn!p302Hi}71)Qc5N!wp!d)asYj+JCh5XIB6aA`8- za9TT#sZh!!hc>RnznVTR(-~;}(q$w;Jq-PJltFW^MQ>DQ9rs?dlModkonC3P#73y8 zS6~;&Yh1xY?Q*VrKw-n(Az|g6p3EvtOP=l^QiRI}{~Jxlc2` z3OytE#*Bl-9f!E~qB8t(57Z$A7j%q327S0f$q*y4{+LxX5G+8T&_t@3luLtjyRc*1ii_j`5t zp!{_l)`!r>;F2{+-D`#$ar@Wj_t%LoQBj1aQMvgpMgLVeYQigQNv)ZowBv6x#ZsJ; zXB3vEC`?0+Lr9_VK?^yo+Spz!iD5(nG{!alL$_eRzru>z&sd#O;au@-mSOxn~%y0-hJ((Ks^CVJxJ-J_GuMJJ^h};<*E4bzi>8e0pO4xw`uE^ z-{-cE_PVDL6_wB)q2@q-IuR>1>lj$959VGX|W8$d0(f| zPTt7aARR{lsRB^mh0R!>QhqJwJV<*_y$V7@L4JI9T&5=J(i8>+gKIl> zy_TBiuv>q$GpKvJa_cECHy)aOJyWq*w+V=H#V0WOafWEo zbIYykV=1f8jDjKR?+TYRJY`|R>wmz9wsXqlo~d&D z_4S3;6z5GM_I_<%UnMwt8)9=5Qg(tss9c|{Y@SWq6L(xvP6_yR$@@C?T==YXx%RI? z#;JX#HjT%E$El=0l)_cyo`0SWbD1zTna%TNEq?i_|7VY z`rI$FOv!M#Os<^al2z<-38vJCRwhl>McYBUzzE|Trj2E;w*xt59{Oh=)Pn~_(+lN= z3S_v0T`#)Wf{RB{n4270Nx_1q8A6Ly>ID5aV=mhEJLyczIC6AO+0 z((}Y2T7KfyiJp+;fh(tLliKwL3)I=6X4uK;Roa4eed`oKaA;Ke3EPt>Cw|y1Lp@^# z|Ilf225Urgi}k8YAmUeS?jV&{kEAGV4i6ujKaPobD*7P9;I}4!a2_IJj=;jwsTk=- z1XG)fRPLlo4WU;jo)%r6BR!WikAH8|W@bPRx}JX`A^)v|JKfu)zJop@d-lYblBysG zqJ{GK1C`u}ARHTIlo?3w(RVw26>mq@p?}_{a$0KdUp92arLqNuTp!7RXA}Z_Y0B={ zSUPN)!t6BnBGZ0Q7JYNxZ*~}An!(JPAdq;w=dhO@JCNvX+27QWXKg#UnT!+t^m8wS zkr*^Z?Afq^k9D;--LUGFY1eE*P_GbyPsr&>{kMc3!VX|j9o({Q)}Tl6`XKU4o_TTt zB!Y?%MuZ>~HOu^{iwbE5Um75^L(Rm#VCikIat)UQA1D@HnP-)$0AV0pOx{njg4TfT zG5at_#K3~Vougeh0!96P<@genV@DuD$&tR-FjiLC&O(t-1${)L;%GZT9u;ez?W@B^ z^N2GedGx>Qan9jsk7g(&IcHZk20+`To&QDW*ZweAl!V35#`xU)69h}IaV@x!+Z8+S z9J zorJ-B-zGmpOT{(GRatE9Z-M@aF_S6i4Vt@r?w_%=&$!D$X*gBQnBVpMra-&sj=IZl z`E_i{Z0NvcLuv@-heUq-7ar#$QXl}3%tf~1=F4Vj6q&2CA=`5n;2#zsk;I=oy7`*PMy2aSH z1ecS9(mpB9(J8U6I=vMw{xu_V%y$nT$H()NXe7Ejh7uFhi(cJs4-t_|S95n_K^UG` z^Ff0i&{WutuW}6sxvpuAld;ZRLX8|X=J3LwX)+(Bdw!>7G9q}72aqG&<@Qh$L&4|B z@@Pg7bkDiOSmz*+l>}gp{w#rysLyX=2C!gj^WWIjfY2$%Q!=HN@JZAPW0NU?*`oru zQn7}%VAylqOW5OFWN<;QRZd;He8}KGI5RDoZ4Iq$Jnu`ULZ3oU1dRE=R1*Q^ZqOgL zW#Yf|=_H|ydrg}pcQmSLDvIx>ItjW6KKO9j@h=jX(BYxalO$6U6Vj74F+K`^ z%U5REbsSdcDF!RY);Aflhf@l=<P!M=UXN$z3S1LPsowN-!PsvAX#^Re9Nw5?VI0(h+o9FO{r!*6<(b z$#2VlsC(lw_sw_q9}|%*zMx{a{QaiaK89n!VcqBKsg&-Lem@8f$QU$e6Z5oImj9wf zhiU9uV6$kGgY!YlQ}XYqqx!&tj9N;M*q3Ci5{nL-I&=A+YSeTfXFhRs0FTzaDq_{m zLILf+%!+wD_5YFxKjU$judBcNEJnJCgD-$59GmF{Z~M<1!X`f>6d#mZL&mc7{EyKO zJ4VB=)PNkDP&L>&*i-Z5TQ}od5|}LBh;AjN#ZGlgVZ<)W4jTs{*XN35>sGXKRyOSg zRQGJ_2FL_sCfRcJuhGx`j^s|_c{Hs_`F{=RQoes{R%ci)PS?OrTi+3Fud`m#BDTvK z{XRAdHe~zHjCM67HhknHO_gdk z1`441vAI1qh5P(iEduTF-p*~q3Kk_Ii(;;|)^m{n7gkt_?TRRz7WU@rmr;Ie`c1}H z{`p<2Y{OtI!Y;+PYJ+&vDa}K_$J~mM;0GT{!79zQ@Eqm1FY>?0%Ex9Te63$(K1s2H z3_Do%nH=WREc{qdJ2PP5mUqC=U`;O)A~P$pzvCI354UJzzs zwRGBu_m}PEamh&1M1=%i6Gg)|ix#ziYT3^bovV>ch7yFw`1vmrOR}OK+Qh%{)+*D4 z$Bf}~$?OJqS@NFvvEZko?`e_0y0ANX5)o3#9}X<4MLmluqUewj2e$SL*Ow~a@K+_Z zg^2DjI=P#a>*HeE<3LAAl4x?DKFXkltvS<&Ej+QQTJyk!1%UjWf_KI-ezc; z&!?#5ST@!&c6OnrZDP7=KX`*4;i4|3=&HJex+uqKqr9ozYQtGEle`KKG?Jo?ERl4P z0kD&}mb5mhSR?nJ6%HNO=Uk<;!sVV5CkB#gLemm~rjwofhzMnUQs->D0UoH8FlMY1 zyX|MZHE+fW-17_!0HR}^SM)uz6F}a~4TRlkNzX&6<*!8_n(FW;mP)u*W{vSl-HTNb zUw-h&uw#?L-yT@^CNbxSW85;8Jv~+xaVEXD0$V==(k5$<=2mKFKsh<9lkYY}b<0)W zAJNxO)Psuo){H_SHyT;@7G>|)DYFv;qf|jRvqLIBn$6}5G>ReI(yrZ02bye~lajVX z)PXaoZ2D6ob;X8Z*$gBZ#%W}yJwu|tcxT#@wTn$K8O`2g&qCHIi}XfA1-2SZY%!ypRzz);bTj{o`y_ukz`n= zK=Vg=-8Hxycd7(AlQp54{ICu>f~?m;OBt6enfNm9X|C0aLR~y%ji_@FOvdoxly^^y zZ##rO@OX&S#^h~nSld6A)K4M=U6^Zqmf`qCWrw^;_IsCIuxAg?N3>6<9dn#Qg4pX` zqO#Xr-5q-LADYIA_yGic-;bg43<&dxhiU$$m!I}ZZ>SKUKQ0FtFnao zm82k?iL0Z%=e;{j5sYzA>MM;8b0lk(e&-(cR+SH9^Auy$ckCt-_}1l?q(m`ggs!Fe za|k^hH9NXFudPNNbt1g|MnCeQ_6kY z1?lTPa|E-7#H^zhBpH4cvI{6qW8h}RoNUh#dBYcqG3lWyE-QPj-R})mcsu2gZEQ+Y z>`w@RdOCzX%f4xjHEs@0my%`3DDlPSuc4L1@~$}P8Y4IW^gnRzZG>RXwQWPfP~jK_ z5>lh8o%i1Ys6i+xRNBAdl$;B#24AQhI#Z}>nON8}6t0N^k9PiphHvIt=G0SXY2R9~ z{sPVX$S<295gvK-v!z&1d6v12D|1TwkuV#q2RCwvO$`Cb@B(z3R(BSqhc%I4Tz%nLUO-=no_SsjST|3N~^ zx)cMQDgSBkJJ&-)uuGUx9IHybr3mx8q4A`)u4h&hvrHhmGB?=@f=*-{u~|I%-;eHm z6$x74Namo^7d=W9@39uU&ySfElT^aB4e=s;%p9jtKis0okEO!a#D^x@3;* zf-!?Y{TIyU!=@v|f*p(Yr7G%0>{+FsBP=oMn3feYqBmzp<|~>vG&viF$<2fWH)&EI z6$NP1YM(pY{@|XQv%z3pHk1u31`alT{c|f$mu`<*t0=Y%e@og;@FqUWy+yM8a0D%U zH0>WVLF%uM9}GGJ17-I*tm*x~8!f*`bbk5OI%l~WwSyPIS8E(5V4=5%KK(tohU_j{ zoGP|bzTET;jksao)8{pHh8QIMXvboOs`BIpM$-nH#Gyxvk)~q zhMd%BV%(8|R`}Yql-h3*KxdPT2&E_JBhO4)&3Bg&zYC(N|3Xx5EjOG4Y1lc7Ths!* z@H_g{@s`~2pp3_s^q6~gan9nRIVmk0%Vto56s?abcP&!EI@oU0({rA}M&umK+rnEC z>qlZ&O&G0Co00@?-qQ-H61((sP(XL=mIyBJ!;JyO*a24Cp1n9+%7Q=waCoCl90l|TLWyHhAeunAsR zNG?-<>p+!?vXbkSL8nt8l{8@LLfn~X=w%cY>9>|m4g|(28q{0+}3CV@%%Ph4e2&n8*Q0Zm;VMZ z??`w|co~#chqddV_UYU-6NI}gqrfmRK4l)E1ny~GAhwyGn53FS$4>71S^e>%_V|{(ZiDiY*8R^?byC2n(~N6Aeu>tXQkm+e^d~w-;*c~CY1f01 z-wu)nLec^3df;VCHMHZuq-1X{)jX)UW}}2q6%iZsBGF_Cro^gT*F!G8 zbEH>@VAu?O{geEUKI$=QtwEBfpQTs5%zemB^A~Y+LKfjV+VL5AOzfEP?X|ytrg%Ll z=VYHd)w9Zls%!zByt^{|{1=JT+9m^LzuvCXgDnQEwL*WpW{o={>I|f7?5Fj0fGnJd z0U$wmYoo?@qxkQr`76D6x0X+d_JFL^R|X`&cOC~s7WGmvookrRk5Z)eH;`G3ERB#)*yRPXB7@=JsA zyT5+_w*qF!B*xP3Qri=+vJI+Ok9b~u7Y*atE94ghQV-yusf`yI#At~Jy-B@~nWQ76 zJznzDMj6w^CFw9WM;0kWPrjRI?80P?SP8581Rr9IkrGMrQo-RRV;f1Gm56$sB5tf> zCAH*g;pvn`ZPo?d!wgbv%h?v0D9FMHnKYe2so+_9hyly{Ur962%H0IOaB8u-a=ws_TDHlS`mzjTLQoC3+KlfKRdwR?c@dq}X3>EX40J(xf`*$rx zanHUC^35xi3nBBZSHB=+GpLoQplpy_5GI@j3($O*Mf!>U4Z&B>dAg4uJpVwo;ugib zv(S>vhA$UHcy0AoB^+=^&hvHC!(&qZ!+vq44{v#sQuc+P4)6ZG{g>B&o;)(s1QciJ zO%~8laOP|_A!I>%giM?FOuii_%UXO;c@zCbBM{y#WlnGQb_d|Z8?T|-NZ%y3rC)q| zhj94SsFhj47O2y*&??iYfu=(+GC;TG1!}~wQHp!=qH3Y2{=k;QgNcxuJx_rbDqhCX z5ax7OpM*5dng1qm(nzArtV}AhC0m2q70J+spw8vLf9I30!f)aU5=q(4f+d(et1A>H4F(*= z9Th@Hh;c7;XnC`gcLM*$I*p(xV80>pPmBvn$8jYvPr;C9128gIj)KcdVW@{+CXF9B zxKn$khiJ>WS68UmyB&yC<^+BLj+dyu_!P@4C!5G7oip?lWm*2~@psG5ObHtj!2E1Y zx6Bk_X0VTw&xL1?a_63b?;#b!yP~yBMcSiPj$UdSG2rL0%7(qpjHHwdr|zdB0H*y6 ziDb|qTSeXId1#Ge)En_;RL@C1K=Y2dNE8XI!)D64?5%v|a??j~B!a|3L~8~jWy1bR zn#r*8;slSN&b$LlpJ-#v<34i9Tj?7ddI`O$4@RxXSkoc2`Z|w>y$0x&zxlV%*@RQI z?)^3vHvuIJ&t0nUR3qr!mab>P_fW;EYt?I0nLT5zW{pdIj9qDR5BM#)35i7D8HLFRa%~pK!?wbIYcsboY-DO3Oms>(gjkf(ZP^Z7JBt{?y6}+n%{>jnK)X-B3sQ( z5?isxc9-ltW`nwHVxm?!B8=E0x}t+b0CkybS0=Ogld=Bge0hYq^&%cTMeFfcRgtwmz2Wzc^Wxv5CPp;E(=osPqs!i( zfDJ{Xj1`pxH}cO`#ivu2bV=+-+WHpNx_1C5(IoU_S7q}2g@(R$>_sPpvz))|Hhrco zfVA-jiM?r-8P#a9yY6N#<)W8;G?ZAJJw!8en4%;Wj5eU(Ty#{oo8@Gj<4{Os*y`t7 z4W34IX*)Q@5{1a-96ntxX#OlTl-G_dW-3dw5g#e{I_UO91-jA|6Wg9xNphnFaIK_7 zWQ?#Iji}k{6zQx$JPf%yDxwha!XR7qkmLQu{SP^6NDLX#D%;zSa~LxEC1%#19tr<> zl_jeA$YoAwrbClhN_aZ}VQnZ=+U|@iM`+Vb{@C3eK~FTiSSh zyO$3=A~h_=PTEefh1gZu@gX^DP){H4rv#1!o z6fMnWHWN7I;NlGx>G{Jm%5ZLjPt`Zf*?+1N)oP&ypmf`!a6^7WpA_AxWso_BIp~h= z>y~IvQ9OY|*;XK>@hlcv1?`W@RN_t_u@@4!IAw!z3~J)YzW@L|HQpME7nWTgVBz)S zb4V*MSv+l`SVA@}gQ#0R0|`(M&-Ja5L%-}F>Fvt^rl9`WH@awTfALa;@LUtNvZ_D& zVv}6&kMwHBLebpx87sc3IGcBW85~&po9WK|nPCydC%8@Hij9c%kRV1A%?iiHNQN_G znHC+6@!B=w;MY_CzDO28xb|`5`m(p7Hq<>c$k>6gkm$z`ZYq%3_{P9Au51lEKq#{VxHBdclfQ$ zCHyC2_cn4ge#21qbCUyg$UE_OpUQliogQ7Wh>KM*1Y5L+AmVU2N567RF99TLZOHFp z((5hiMnO8Qc_(ur8jLIfvbf}&3zH7(Et1yZ*>9|0v9pz3*q;-~KD0d85ahY>-`mmp z`rL6(&TEHNb6^{WD{lPF9Z@YWo94lh?&SWN#A-0E_{-7aD54D0Elbb3SSqNPu^2z0 zxTKPX4$7~n-tqp^S(}x|Kqj08m(BWGc^lvTk{qfqQ$l1En55+R^P=c*F8VHiuBa#a zzh+Y_cih8(=sRLF*ll~V^X z*zciWFHLLb2ouiAFx;l_SsP328h#8E*b*XXrh2!;YN$iXo!?6f*=+6nNs+~dhQbgz zO~WK?(h)0eGRLkecv8yC?LnZYlAf`1h(q^f+dj^ld5e+p6V1w;jrC_R=9VzQTB;fo z8@{%ZZBX*3>1LuO_TP1O&zR`h;mM=(XhS<|a$Bg5Bbhhu$-eY5%+=L?yt~Dxw2m`j zqtAw>k5x8rzGd+;jmnvHO8X{1vDQz%6t6Ft9T!Sgj}s+QTaaw#*{Rk>3yzw9bG8u$ZhW0)^u1r8ROae zwRAt<%@%A5DssXRD@%y7LSQ}NwxMl3pQEp4*EZ@$Yhm%MSy|uk?|9G$=*L+sk&1ck z)tu1(;4I>rD@|$*?*i{;^q9COcnE6!_x!Zm&jXyuvMEJsrr*wl-=X)Sx#C@GP~i5d z>^MhnMv|ympib^^3dNxnxjM|GxVA13-&9mQEB%aKt3S2N z>y0i}Kb+TwkNGn6p~>(wOVfEGIyT}cef`M3fW2;LN_3P!!ArKDs88HhlDzs%nb^;KQ^8AYR7Hs)PrMAN+RyQhQTj zJ$W#H{ejnexPso2bdV`PhAZ0?#aFo7EB1*BL9n#$Qh}{zKF27UF&|NZhLI|Ue$>64 zNhh0yF-z|859(#84*2HyjM09k(E$He33bR9{&Qd4MX}yo+;{iYwU{Axoj!}5-8Ml7 z%1GkeQH@8<@VD#@Rz=%gkL5Bwo1uu7X<3O-VX`4j|HnAE;imwFWr8);qBB!{yvMOs zoA8wIl0s*IxtiBQv55yf0`!pzB}+1lj$ZsluitVeQH|kp(22v6dYW_T%dp0gi341< z#PI(BEs!&7L@TEhuWHAG{9Qippq?m0x!6FZhPhsJ4m{iji zamQP(231d0aXohy_!JE!ztBhOU{&!NQXEIk|Z*oKCCc?JL=j~Nu5u9f$nuE96mU< zP+0aGwCjQ2Pjrx&fy&yFAxEk3fjm3-y3#{&eLdt5nP54vvEVhhCp}1$Rizwouow-l zR2>P6sYuQcVbGWh8qNX6BtSv-4sq{N^+Zq!nm~b^p*bo5qCMV58lZM}0|w~%4%1Yw zrp76qkvYlA_*LSp6ieV)sEnoTq%br{(RQUh>i)h0>TaPzO`1^axCAaV1$Cf#UOsQe zG?!2j!#T?WNio0^FtT|cN1%2V%nhBy8F1T?`WeFlUjBX?8Nv92wjLU; zm0s+LWOV6H#wG`EC`y{BCrb>&gp<3$TtR(S!H{-eU}%FR4QND~MWF(efgqH>O|u=1 zJbDFj>TRtwQi$~g3|1%ENdodn$0Gndg8@-*#JUD$!m*W*Oi<+mXOlCrB-&}Iii3-A zz^t}sav%-FQFdV*BtFqCtyND_kt0|XNf@gWuL0&vngReQ+NN_z34s_Xd6ktWR@re1 zgt<&iIHhw?*IH9Gv=B6s0ArNmt4_v5gaUu7crl6+ebq#XqDpRpFoK_?Fi2-TYzq+d z42Mhi06(%YYwVWkh;U)kaUly5ko7hfVNNtN;wn7c@#QjQgG$TMS9j!}zrBqi@! z5#vbB1uSQd!DO4fi8e)Jxs$u86s!XYbBwuJ2XXlT{6P&+9P>B;(?w-4$#Hh0;y#N) z(C+TZ5gJjm)Q9Q2p9wi?D1$s2dckm&)H$Iw9dKB9*8;sa1HS`)9M7&3(=A#gSm4Sr z;bzZi$1gJ~LrZ`o8^iAti3^}2WYL%CiHN8Ob$q+-d3y2caWQE;v_iT1_gHQUf!!PB z3a}o;kW50JcyWksaK3p8FYJpnfiT3M4U-2`Q*ceWES&a}W)7ko9MP{~;_Gp4iIs_3k&(z6`5fu?d{mJ3nI6?Gk0uxJKy~~=Hs_M1g_?$qDNE@L| zokLa=%?)I*(~D`lVy0b-bc<_$s*n}z4|c(;gI%f6z(md7l&G|!yoe`mk!I%_C}(O0 zx!^2`EofH+HOt`rD&UY53J$B8w-UkeKurPyG=D5YU@AYYX-x0UZc!BsayQ|Q#5&-~ zaHoeeP)P6tY6lRnQ$3QVZN$8=8pPR7MjsXQT-wKJRdRCTuw*Uw?H4DWSRN3JdkEAoEQ3Otkej02e+q3jf8)!-1Tu8C;!4zy`H z%BXAqHb#>)(rh{BerSii@ztqBQ25sV))pMuLCB_5D9|%F)vRXG=#s=dIgEo~87>46 zjShGX%6iZ=cORT!KzjnU@d1#Q#~j#Ue0=f`wvY%si3S_OP7`5ZiA_ObV4JFA3>qvo zpd*V{0BGFrT2SFD9Pdd=lPC(zk4GFdG16$lLk}m3phPPVz)f7D2PUBZXc-73Xgwnn zHIWmQ|HM;kERzPs0O^s(0rDN1*ru&G_nCqt{Q$L8(TLj4S?cfWr?9By_rGa6i zdOf@^64X6Tb%e{zd&M;*QQUU&j_OVbPc2vP`{Ir4$QM+J)HbZp1lFA-Er>Gh<&SzE5F`!b)pJLAi zAFTL=K`WQ@CqMuIqX+#n6b@uOs1h}>H9(szv7 za6^<^oG6a^2_8F^hOS>%DWHU*25Z{Ml?FU4q@H|q* z0pjb-?1K_IT)Y&bz44SHuxvu4Q*N?RQsoz`MFOc$gu5&_WUc-d51NaTZ7FXO9R`lt zB6RAMN8zt_e(Vg?g<5vhLfD!LG)%}Qh0PS6kXm715qyv%wnN>2NERjFo@Ee zH(4-fQKT#ePk}d_;bJEhW+c))5AB?Xi%Dy`UZ|$9s$JRIN$TKuS~eMTNuBv6u02x`a`#%Wixa(ktygjAlq1V)gc2Tp2S9coIT53pYR<>-B@ zgbvyjn4F-flU5iWrKI5?D-j6o2&4>BC)9_;Wu&TXA_XRjMvYEne3Psoq1-fEru5{K zzr4_W1pjbb?0q#TCmi(}(9e#JCw>HAP9`kXY4rgW@~pF~aZ!>AVmHWcod{-8QcP?M zm3>Qokv~&1DXI+3O&qoiN3+mu>L?MaTEI%P2~@?81@ng)6lZrYqc-Rx2HOMEx0e*q zY?uttqt$(1oP`BvlTL4|Ai-#XB^Xp=9MlNg$O39d3l|;?0fdD*u7P`7Q)*B;5XW~% zybPMR@iLXJrcDePj7*BAtDbsWlmi|jS``lO&jHXoAVtTdSiX_QB!KN5=yi7f+eW>) z5taszhiURr9oGkp6q9oo?m);s$fG$;FqO}$8MEe9;xb6&aNtVYW8c>)J8ST&#cQt! z<8whW!mpXViX>oZ&*R5Ef+c{f##R`I?H|RgrU28TP=K3;3g{|~)y%wNZBp!@V9O*l z#2@M!5&`L(Tvt%!XhMc&TSpm?oFUxDKXf2B{wTPHL9?C{nLzggDZLV#EpsXX;4~^V zW1U|?ry_CmFpF@Mq9v+#F|3K9>fZrtOu(af6^tK~rfI?QNNO zYu4?YX0svmi%12!!M)F@rmyG_0Z-9qMUyDqptYGsC8rulcp3bUcmjScHA3y0(M)mn7AUI(d0DiXr{uI+)Qf^8m&vw+H%hHt zX_vhn4ip|;ILdr#DHL+NWDIBtOOwumu&`;8-Qg^xwt!7_^_gb2mMD5G+EMAK=S88? zAgikrd@+EbdXaVUXtQ)h#RsYdm=r6$i*lfI7Cv#GkdT#mcPKz{IA&ExLnt)@xT`Va zYC5Gy@X8;3yNeMJl_gJDBQ2Ux6p=sihf1V{m4U^n{_w7$BcNvDIbVq#0I4Yds|Q9w z8GopQ(h4$B(4aBkqtbTM*@E5~E1>l49zTP#)YjNw+^@LtL;Jbu;*K+s0d!Z(De;E4 zB_y_*wiNP;4jzF#qAG<9Ga9^@N79=|QDC40_p9<4gUBD=k&)B_X;Q2>112zh@P!F( zpi&)6po~{U(ZvED>rr5g3VFvGEUPR6gQ#-P;M;a=9oS@~8W`JjT5&SzFNYmtIz>7( zM~N?b@!}RRFlC9QjOa5%@TTcmpqI!mtX5KRbd>GDg9>u~q8hVOL0kB_P|tz8Cn)E& z*-S9`lq4%SKcASR?t%ETu@!9C~{R~s*^JiLIh+3a#e6&ZwdkZI;E#DuASuS*RN*R{4Pp$004@et)>9!OawsBI z24VKX=olVF)Yj?2!cR%roXHW66(`9X@XDwNEsj`0BXN{e1U%tLs#vKyL6>_WDJdw1 z&{S(UK2WKHcmv5Y>EH;(AVvi#>@!oe%Vj7Eb8K6^1Jl@4&rYyQfQdMupy)MsM?*WVfr%{1Q?3h7iTMO9Gik;_p2PVL zcGl-8D4Dsof6}9Fiz6)sm8>EM9b!WnRcT_B3)q|ZWp$w9=3`)F6KP-bX*P{Ia881>}q?tcHUaVHS z?FrF>2_d!8Z^+d61aS@}y=r|hG=@c$o1wW!Z+;~G0D_A8lqWe90IoB^n#0zpE<)D^ zhA2CxfR=t7O4681^*#s+5ajv6ji%Y2u882M1M9 zCk1^^kG&h$-H{l^4KiOa!$^l62Z` zrrl{a8Nu}OJYH;u0hJ*MPCBN=_=UhTd-(^rg8C`J=q#M5KWpd?@jy1GP62A9RmvX! z6%HJv6J0qAy?8=DB|J#WO=C1))~b?n)L16@H%Se$hr@`is7+RF<wmY$S8#~VRIEmBL=?Y9RE$S4|G&Z#hHHPE#%A1yv)hiVljU->9} zL>=$y1nde^@rv?Lc4`nlB+>WA6a)aVB#jyYl=q-e$+zhM_<^19aZ4F0G1o7snHn-AWG>L zJrRztkAfiK=b0}hVF87L#`Y;Kw8PPTiqdx0DWE`FhJ#?cdapWsUGR3%6;r$)6G|K% zNO^-1h5A+dp_{TocwYvHhuSp#a?>(Ewm@mvm5s-g0V!Z+hb&>vtfUK4Ig-?qcZEyZ zDeD-9;q9Un-T?7MA|dzWX~k96hLE!XUx;C$IIkMb1c%+>URPzm2$o%>775y9(dK_| zTcx2}YrQ&1)I%E^Qv~Ox*Zm7>3#P$N`+V`rQq>7Ruou$pFdJw|6(#6iJYfK^OksjW zch#(B*AXgw*)nd3u9;s}biRh8oJd(4!z$dD6E(#Zg#UxCNwbDZ?inc81ZAf8ruL|F zE$M0PZZsGzh3tr?bOKvZzMws!H5xMnqFE2{350FV9Hf&zsMCs}v>YPy2(<{X-hu9rr})m^=c_?!Lv~-^m{;{gXp5fRr&AA*+7!XMpmqD&Eu?(t&bND*?rp zCS8Hr6>Svl$>$jm76IW^T>Q9cYU+CSL&p<%0UMAwG^y2h9lb@43Gcr5I{CV2K4IEY zAAo;46`)V%SIk3K!iK73$&QrQ zNj@cO53eOX7oEWdF&uuSk6~+?fQ6)R@x~waLsP4u;L;7j0@QRALuLnsGP`Kl1=YQe zX6wphiH{t*=K&a~LI(sj!5LM9O`t)p>a0&s@fMwJ3~^KT_gP~vgJYu&t!VLD0K-BWc2bo8>gCX~}C(#)I; zAPeFnw!(84lxwIRz%=a5ok6L5xCs;@o!o=Ur)MV}|b92cFt z=FT%&UOLff|Md|^u_=660wx~LTqgz~(J+aFG8@xU;oTu~MGIN#lpzU~ed24W? zGe?v`!p1j)ZmBwThT6^aC^7JyCQha$YnatVUXwA=#y3h84E`5o$M!U&A~WgGO-@s& z$t9(oCz_evm_^HoWK(C`6D4B$ADN5k#51T^M#9n^)SZ8l-Vwl8WJxB;XK~66S|`1i zHz=bf=y;`V_Q7eiZ9l$RajuB}mv@?=nVbv%+19#Tc5eC()z z3@Ib@UEtuHj~iYfn~vG@%rX4dI;$Lt6zAH93xe)u{6w>(g9cj7 zF-AwI0IUZtgQP&3Z{G%$AA}!+=?7Nq2jAEr3@fGHc+av?)I`e6LNafLqQgStzt1-8 z&@4ed#toRmiFCFh@p)*IYfLj^B<#~HxWJRuv;eI=i}{8`OQDfLCpAx+1FAPeS(Ke! zBa;@>it|hm635f9sx)kqxC(d*qu+62CSAU_ku_^*b#xOg^b~>do}K13YIYQc1whji zQIf4fi*b%l%1Fjs=Wmd+t?oxr{UgVvS@{Or(%aqIGO#1Cz1+A{#o%4_h*lM=MhX;D0uC1a!z)i(P^z#1rX zXVn2Kj-pe#UGUqCNXXs@)O0ieTHYJFn^MCEZIYa&4M-?D;g?pQ`^cq;ORtp?-eVUkVtE0%rtMp4K`t9E)tFHh>|RFDMBTXh1*u+x7JO0RiWy3Q{ikZi*8?I}`<{B*McHgOz>lFKP* znhtL>E86u1GG9AXi_7R>SQr!-)mu6k(W;|MptyIRMycl$>28ddCl*le)B_;Y^h6cK z4yvx5ikUNdC2i~^w9|$I^Yj$eOT?lF@Ay?t2TNLRm~QE@ieU6&nRpyjB@l^X8~~08 zX*g?eI7PQ+O?l)%_=w2+i0GzFE(QAQFzG*mr(wP*@+Y6)i_^lG3zf1wBi2Me22c^) zhvI}46vY$MU$UoLqFYd!FxYCJRMzQqThx%=%*TUr0K6xlac5cX+J*wnaP!GeIx~iL zIS^(edNW}l7n+pjWlnXLn+_^DDa)FjuBOBpO`;@Bu9PV?(&woNOV9S^s%)BtEX<9O zL-EO?0YrLp8$euoe&ML}LR;i&$Hmw*teCIE-I_w?fCPKOiI9U9DEwR316hY0fruvI zR3=QRz={H)*QVsnP(TZ<@6v@0rq__ip~W( z?X(W`6EGW&Awe}a&$oc)0@JO1mmco0e8$z&P*a=~gz7HL_n1R3(gnRfNgeq~s0)Jw zbD=f2>tH0su23rDsh3%*SB4uMdF|ljsQ5Ngn5Ex9%!d)uGsVHtVW2)}ufU`B(mLbN z8CTylZ0|lhDs~Nwfe*W7p)S}UcGH>d#X)|GyNVi4Se}n2G$rH5-EssEg=3n)puD6J z#d2x1I#V1BI#AI(Gg8SUV2F+;npV^NpcAM-I&p+zi>u~Fe3}CP6_p4_dn4URMsGLh za=^zq%8D6hgaAOh9Zigi19KFjjuW=E;F2q~FzjuUL7FTiv3s8l5Cnx-Rj5ms$(=!d zofO1?#$$g9D0LjL*inZRJ9%|iv4vH34mV7i^wHcxNY?kPR+-^&(Dn3*65(A8{)iX} z^Ww0$dV1LPiDxH+r};pEambz`_G2S_k`FpHGAU-;;Vj_!sRYpNkepryKdUNoFl5W9LX%ik z!z5*VoO8n)3Rlqw-qlA**XMu1j6Jz-l(%D1h<@>r9AfJho!vouyZ1jpf)2k0)tBZz zIEAEPEt-pE&`7*+UzH&tPIiCzdr1=@eqM8cbymAmloxSMG1CbhRp+HS;t1b+=&Z*% zbC;XI06cmvq7R3FxrU}###`Pn`D?Wge$tA2?tT0yxkdS3+YJCgslA!MNAF&CW-;s< zp}0+FSKMHx&4s}YlieWdP%PTIplS1rbOc_i)OR?^wiH}QDE4(~r(x@;(I+3L6P)$~ zX1ENskJfIT^H~E9fGDBaTwgf)b?~)vctpn>Qh))!Va?H1gUq%MPly=h@3SFN0+p57v-PRWkqmXkC? z&IF>==727aFM{E+;kwhPWi8W9Y42#rBHs0h7QCyH$%yG@pmJs4A*lG=?Gh2e(XXmw zcDx5aGVKX6INa4Kj{)vVx}mvUvlN~cI;g#y36_;M+no(Ep$rEQx7g`XySBx3?b{6Iz;_Mq zA@WQMGG4J4vt5z21kdfhE~%FjD+K#+&@zr?9M}4WufU} zQ-V-6L5srFcCck05h-mvW%E$I@vYF>9kfMC=R7*ho|=AF$d3ol+TVn?oeh07QI$$J ztBnJ$WS%DB zLD(5Qi2x)S+AE3B^Wu|zvkn^38Kq8qt6)<~(s%}ra?bkQG@dVQR<()!QJGN1i0yQ! zI^iUD2BxEF!7_>!Cc@#>6NHrs=ajf7p9IEcQNkngs6NviLLk%uK*i+E9YH&valf2W zgEV;3qSR)RBZ}Q#%Spf6GmSu(?*%ZgG{{Dt{hVDj>{({fKvoa z$-O7wiHyI{kWWl(_EE~d|Ec#I^-z^eGy|zxbMS+;8`<%%IdqMG%c@1f{tQ0($rhH>&)Fo;JY^TP+>Gkl7 zq5B|n7yAvSO1bEPOGF3o5}U&l0%Et8$pJ-;git7sF$WD-E!PnQ+_c^jE)&9W*Ss&5 zQZEGv{LXSyX5!Dlz@}l*F8B(@3dQH}Lo0xBF-Dqv2Ncz{K@PR)@gY5?loY7g<>{K~ zYS9C&nggsX+5o~!)cz`G5+>dQO~MS~Nz%%Y>ZssFZzhCir~ynRSa6cJq6F)3`zvFl z&oU3V4kKt@Zuor%N*A@G#0McTL7aLjdDJ)uA5ljj!9YWrBXCt_M$mR|Z`xIUWWdq1 zj?_FY2-HJ59@xgR1*NG40SGT-nmIJO9CV-w*+1uM3W`;LuTa+LqkEKjKPW=RsX`bB zs3QSE*0>_mglUR8;KmWCHJf7+l+Darv}w&HMkeQxlH&syQ!;dn;!-V7$0%wj__d)u zJAWAUmZVhI(akxLvDUT7q!y=sQgUf+?qxozvlQv<;zqdMy|)FJ3bjWNnBMJ4EY!@C zUR-scnV?mKMkWW(=0WU&q-*ipC-P!pm@3wkFxLkVH-*yRn`G~%qydZ`eVS)HOQ%y3 zn)bp6hwmsUC@epn0-bU)!Jykzd;0F@#rUMwLx z8DFc8e%GlFH!Vd;2L%8C>67+%i5lcHezwgKhLlFB(xP0v9gm4*Nm--ZuV+VC9Uu{` zod4_sJh(BgN0#se^;%G{0)rZdH$L&Kj1&ay;C--4y2W+C#dL)U$^*Uu)j@FrkI>1m z*?~%OYEW7-N%?mQL308Qov|dIo6;RxdJ*dlRDrnLd1;a!C3{Umc^Ys$7!)gMIY)8e zAHll-T-6JNReSP-@_u`IHux-LhQv2#mQUJ{qVsQvbbR2Gwr}cqm+FkOND-74vcdT# zHc&>D&PYn?p_V*H!I7XKo9i=yVP~wG@Fm0QeofG!N($MS_0UBLKT5ZuOg^^`lL0>u zUc=2a1V+;VD@+UmzhQ&T1|KM4jto_BBvqb1Cje^(rzpK}b3F$XJ8NtkPz$j=1M-C9S>rl#~9*(Y46(O?BkkY;`}&Pj*v7NrI^Y%j^krnVD$HAmYa zc!@v??jg_bC#JGiMM*L^?q(hUKQh#tH9G+oTb%`UExl0|#1L&#JM+6Q0)CUs;6W>A4<0)v(w%)?|52}w|LHNnj!B)V7yC>(Ngj5-k; zy{kIo%HcV1Bk4dk82K#Gp)77hwA3m*yaOjxbrL$z&slyVbAU`CG_x08bu!MBCqI~e z2GSf)s74>pQt28kAJHakPPlM{po>osGdtg$xBX$w@zlgahm&Qf<^CWU(m> zKIA}CLjcg4aS|#J9iqmlWqhA#hRGjA`IMF5+O~y*(Wb}dR-T%A0OpS1I*pR1`<23+ zI+1(4h(6>%nkjQg4Ld}3O>Li#jE7{ssH%Wv*(bG=@d0s5vSCi&p$U!REJH`DvqnTE zqi4ENa~f=B?P3!|VDu}45*c!LB`j#0$e1wwpGveOCza_`EZ zI4A5hok0V}VT9z2xYU?4x!%wxH7J8X7v)*5-nd1tO9zs|N5|~2j!8+85fmT6N&h>n z0?{J7JZHUgyC9qmUJ|aFQX^+aeSMiDZIf1%@WO-7_+Ye=DAza?2I=h@;(NgNuCqGr zz=lq4>dN zzzKU~ zPOwZJfdN%rGfi_sDAfMy7c9=&zzhOHxUNKm-odPG&&oju)ZHU*%@$}odU0T6z#Ats zU$iI9e*-J4`19)bE-ODBV>F^pUiWRGy2g~lCC)q(JttgkYFs{<8Bd#zF&$7=+C;Qy zGc?Xlog)j3KbK<6;^1p~W+9L&6bL6{Cd2NA&MI7-G85D}7f+#n-b060Q~nGkI|-g_ zhoz$#;?vuDsaUcGhQzegohV%A95dizw8?$1#1*P0luykmNFldVV8<{`jQAJr#>*h9 z?S5t*rHMm}?sD&&u|1@u8_0R@%mw*D3Kh_i;{>VU8f}Zas7lC=7sxm=A8Xrn*z!`e zqk2+LF#$*6Ho>?El&LvJW3WW``1C<){J3XPF+tvQg0D|-gAbACASzrm<*JJo9%{2O zXc7`ca||`?MleEXy3`YV;3|M0Q^4_vR@-Qll~j}Iv*DDB)0HGC&#|6l()#*&qbFuRi;vko4l5U#<0wk|3&t6|*`bt)Tj6Kca_V^q?Mn|bmHbmGzloT#I8w>>uCaJ=_)ykag zy>!(LokQ@{T89abwl^}1vqPV8QdQ@+c;XMfI1nNVH*yjh!b;%cP&hlZUi5*|lw=)^ zFx;?m4QVJM)O95aoJ~uYH-IE~nTuSdlzT4`rz(kqxJ60DI zu)DG%F_b&{B!{Y+vvYLaTa@vbHN{L@LAQ()t_jIm23>>}=AajjjQSn9xac_xVo>jA zF0&g0r9{Fg2gi0KO~WX(5G~K8{2g@J0>qrW(s*W zg)Y@@03X0-nIZ)vZ^|4SFPf0d0cx`6iBF(NM{;+u<&nxrI=ver`5V$;jJ-18pu*y0 zG|(x}Y+ZG2m2}d2rjW5Xn zM;i>qfbWGa!_6W-@o1cWQYIP{oeng7AUu8}j6XU%%sh&U8;Y}e1Itprbe*Wd2y6E| z=#X91=p>`GH^WQZ!FG?u5`SFZh1M`=7^9!sViQx@M$ns1xXxtIsEo+P*}{VMgzAXu zn$sL&t=0~Hz2MO|^V5nJFF1J(Ij9RYI= zq^Hil^agW2@t&iu8O)TDaaE+n!D>(jgKXw#hnYaehS0C8Gm3_f%SzhNFP>ynsDPy( zNZitE!{7@OKGCaH{lIKwqV^kT3ms{dtG8oYmkGQone2z@f2D9>(jwX|I|(0j0JK#_ z9cS)*o3utyHSF0>oE8_F?U)oXxv&TYfC?#)GyZ>EolC2=O?Q?5N=QpVU+R2F+pZmL zN(h2M0vbaU{QK55>RvVGBjk%_e|zm`t$WpB&N0W~tA(Yn=M?Gk*ZcULO9?0pNPZqQ zEKv+^2&f-FE6XIh5Ld|8_9Xdt!=z&}_c62^ic9ECV6~qN2~&Que8qSCPNku^l-?8u z_Th3&d6*$Fb15HulP*dMhb`v|HZX@JXH`b2pKA%FJNmM8lRN8g45#S?4*jctN$zAg za`T`E{S>5}!E|9XZ=Z9^DK#JsiKF?8OOo<&Z`D%20NbpjBf6EVxsbbpi&)Pl%-^BvD| z^`!#IPaK&GDAq4{U}>a}yI}6WbZX@0V1XEU{rm088tk_*)Lvjo zTXxCm3y`AcRP+a2yw60H(I8P^5bx{9iR2{1L7n>W4r6I&ibBuD+_@wj30pxT#{NaY zN>t7n#3gx8(b;8=#B*BxmkuFg4#l^3%KvH zw1m_Am7^k_&Z++D7jz+adu39mua%;;MIPH?AE{BC`Zxk4k>uH?_S73xO5 z;oUG?^G7v+F9CV3f9(uRY($au#v6W70#GoiG%1pP76y>sP%_fFeupEZbrFHk1$-(U z#H}D+$O=C)gUSf{4yJVJ2fwnq6U^XW9xfjq2caS2%;$E7QhhV&-_lQfMn?b)z(e~g ztQ7D`I+~i`k6V9<=QzA$UHY9|m@+}$1V;Gidszo9T^h>&&xwKWHfOjcJdB@dDm>CI z4D738xf@^%L4ru1A1u+_Mc^w`?D})h5Wlp5N|jIW(l0`(4Et}=Pt`zkNw{kIoURaM z<3G-DX#Qe6m~LwZ-SW?`;k*`^SIIx^gFqKPJ3b0uzFi~cNlsE^rH@Y_p5%N1nR5P6 zN~r&Ux>Y7LpI&&2T8k#|izb9^XR*Oa004U#KFkl zKT;C33^p0qOZz=^nVTb&yn6>$7FFJGpLhs#8#)P+gRfm=qKtPSbxP0aE2NUR6W~@a{))?o zI)~Z9{@CX-;=7hrpH)AzGwL2qt;Rzf8sZknzEe)j*En3g)0S)Scg)c5a*1Xc#N6w=%5aOt!-Vle5iqx5#7nVJ ze(|AL?NFDUCvguRs$S4tWOVJvhLA5C5PrErL?QLmp>Wsu#J3Y6O;L&P4JiDZRNym3 z2`_7&&wMK5a>^GDL{NUCE76sZ+kN6mO_2ns!P)sack++QNu2@huaaVvU*#43F_f-( zKa^)Ip}$fRNS!;LFu6;3k zqB={{m40Gi=FK#(TT=dyp_p_m0brD`7iJl7dvn>0{gcN|p)$t{>^8-SW_B)N;P->1a|8G6J1Xk?V#h+EHno@V}4V%%yhU`G6( ztvD{2DyOMpzqC`#$rTq=;jMfw28cX4TNna;O@;E@G_qNrnF#=NhH#GzyMM}7P-EsK zhz@_Lf57qyn5VUGnF4cjZ$J|JDJ$5y%zFA*AMcIf0w;6f%KbHqj2%yRCqCRa-L03x zep6+>6jS1|#K6wtRbAx~mTpZ6QD5jNV_{eZ8>^BZK?fCORiZdU$}f>#N$2M#!6f&y zC8b+ai@#79eC9x-|I!QFSBRd!2mNZ$Rn`4DMsmF~PAngCK?w-64Q|HF?b$j&TL%3| z9~dOnEWZI)X8nY-indjZOZ}#Jnk5Zcb0nIXj}?;Xppo>Q&+voXq(A4g^v@TZVI2iL z%Ea}{X*!p(VvZk)uqi!;#SfL`%VSWiG#~4n`d`K@4FO|I7UcNmT3I7z+%U1w{l*Q< z?mnZUpVfXvc*CfMS3sZ!PEWxgTW|t&N z{6;DAm>;sDeoBxnvw7J6>*w+WZZ35} z`?=>Sokzh1a>@DDlt`nR4)^cqsk{}O=lcz{`NYHs#56VP{l|DgO~#kE#7O-mxl`zh z!0<8uyZE|vs+_9da6FM&j3Q!)1pLi`WsZdGv5n6hh8Y(}Hb4HX-DtnX6;o{TcjS@e z1a}XMg3sbT!;c@bgu2Qp*-fR@OIXo?T{_?6hpmn=o~2T>D!pxK*pG{0ex_5^@P8sB{_U{VdruZ^i1GRQEG z3h#GI!6RfuLcq<>G>P{GlZy${_l%K(Vuw&m{i!f8*x&%I>*o^;-j3tVzC7@+S)L;P zOStuU7U!E+*pjq-`4e%G_X@;M7QsE?u=-@oBwx2r`*i+K0nb$#5Z^aOwgQt{Yn zmsJI z0o(pOWx$uSv@1|*zc!n48Y^73<*gH$+8{P)!1GT`v{3M8RqnrxWtlAjz;PIU9wRWY zk_Is2qb0FH_)29}nm&1nvZysnnC08=OgXl~Q90p$iRqA@y6oWd`~|2kxhpe{oj-{* z2A2x_h7$65P-UsSZ=}~Hl7Y}}A&x*vzwQZ_J@h_!-Tz!V+1!ktfrNh9KLul69a%py z9R_c-W{NDk5Dlk0>xX>qFWDHT#Fq35QTxShn2qw3kT;a}!NFPPHZ&dm3jjujp}j&} z>Mwq5I>fa zodx;X1%a(MWuxw&!U`*5`~)QIo1bVCK7ukvdj5puRN%mcw7~OoKxE_qiym3(FDDv* z25=nQ6U*1dkWCJFgF$?AG8pKTI6a>Ziy0>j2(Irue(1*5Ad{*4{1lr_c!%UUw9sFB z29N`SJKF~vSKtbg`G6_*X9U%CrctF=tQWqVabf12dZMJylOMtv)FmPaJ^>(joW>y> ztL+QGt6Smicp!W}S_RaBa1Mn!eL+;p0iP0@w&#yabBT%O7v8|hGQ09&_RsCHiA4?( zw|zt{MOh^rfyl}EzE&1Bk{@V^>2re4Q;6)jjK84pB33K`43M8&`K44)-IXHZ^@n#< zKvlBq$FE}|6OIl=Mx$l_GFX_!;<{M<#q zT$;>!Q#$$noWl{D;jIB4^;woe=O9i?qUHW`^IfWLDlE)TO+pJV4iGL^`6HJT>dDQ` zuN}FYnaAA160ba_2Fi>2id>B26;Jp}HZQQ( zDHqo_+9e`^B3k$6hj$?_Vn(Ck=FcaEsbte=s(lWQ*hg%2khtak{IdY}vD1J-@C)dr zh+)v2%l@U(py#RN){5=%mFZwdD0VSu{e+`T2$7dlFn`2XI&4TPN>}^1X56iCq0?Ei zAI-M`Qlq((yWmIh9ysPfT*BnPz4{R@o4l!i=pJKah@VI^zUu@rD@Q6%;fc)Yrz>MA zs@3&DYE#x_1epBHwv&!;&Y1NX4JSF@Xp3>-eI*OB+$GzA{>`xoc9wrMSnUT2p*A8M z2>NjS+=A!;b&Fv@|C89ji=fpIpY)gENj0up^=CtgTOKH8Q?SMt*d^t35`SvtZ`v&` z)zoeNL+hMOVU_A5dsPiR3mJb^y`n| z>M{%CjichZd^!lk;W|>w(grSj8(|^ypib(Dh0%a=ssI75%Wo`pY3x) zOhj1_jUs#2FLG{6AIuZkpFEp1;I1&OA;#F3G03DCZ4f@CAK(K08uB{^Q@2TJ+>)Hc za4&x9DXF&>$*I4k!ib4Rs)Cn&l>+!6qi*H1)-Q1h;Til!F7oudP|?$I`Ig`4xJzqE zYldn*Itpo`OSP&HeXtd>*(-nquVNJExPv(WeW9 zsqfD)=7wIgHFf;`nF4yrnY@sygG~P3JVtRk=%_+YGxPiXbK`RDD_@F|@4X$V>rvf< zwyxiwApi!_d#=nl|V9%1%jqlAvc+60~7@#C{jlX{s*uG+4 zs!z6m?{RA4g=asZVD z)ZT9|GUM?q#$51wC*!&{pxI!~H}dbDP>BMRfc<0W{QGsNc~DzXTb)Ma_ir*t5|GG# ze+z)9IXyjzK;9dqdOPbulgsKKpBKv?4jv$<^6#xMmtbKqg?OL7|3w5umOb)0=}x`% zz95W+IFy(*odeHLtBEv>liN8$-kYRJOB$&&VtQYN$nAjj86Cv?;yBK`{9!R7_$@jS zv}9F~6^)hr{Zjx@jbV&b-%g_bfnd!<5FW?xP2wCx?Ml$*X&n6CmnwW6{qF&nYm~A@ zG{6|-{dGN%p@j2u&b-}D`GlVo4cUDAQI`Kg2nIva_f`mK7?SC82EQe$B?P|;vYoe! zLZo3_U4Z~cK)Ao_@5k`^mS(%$$Zsze4dKZ@WWw0y?=kAUfp5MPR9-uJtc0T*^{j*Bqgkz+L=lXsh!l`V$!S$#24C`en2v)WEd%*gwE~VY+Ki{|7 z6vd4OjUwtD0+^KIS5^hi{eBElm}_NB6$0;DUFg{4LC$7sZxIlJL()Mb@MFJUKkH%% zEG%Nl*Z1C~FvQRnIAgt)s`MrGJJ|TG_a+JG0lKCnIsf_nJ~V_4=tPI=eV`|cS;>_4 z>Ye!#3In%7?%D5*6~!p&$UYn5-6+bN!~YS-ys>}pMTt07COVjsz4tN!D3!+pIk9-X zaM zvHhTMZc0CVS2j-uTp7MNE~@Y6Q_#m*9%m802ZLh{(>_ugfvSC6;kUJ~SMc;5*`@R3 z!Wr+N;pixe$6yY=d@nPnP@cLHQQkX;^}q+MvVrz@&54>csiH30zo&~r5yKgTV&RWd zD5oUq10$h#1;N>aJjb}>_v4B3R`4z}_Vla4pmG7s0DJJ%$NMV?kFlXq`eb^0gO9D6 z7}`n-pMMH{!dl@R`!n2@(mi*Rwmu`@hDgFbI@9hnY zsW2A=GU)y7jkD6}RaMBtDR_(h-0KiM$nKuMT4u?-v8z!_uq=ODW}qB7CtFi6cYY5z zNiS6J#MfVXivWA;ndL?UcgmFl(&n)xNUGihlry5t;$!uD#6tF*Lz}&4zE8su*-YrkPYDgp&Z@ieSKYIK>j_fqC+jq zu}rHBdG~jkp5@-JnL5gCsdmrc;fCZecpLQwYbQ9BD|zh>1@mxOF(WL#1`y=1JYnE1KSYSC`9V5{6VBZ><8F^2$bHM!@WR`B2C1>*M?{+ zB3dEW>brX{0=#CkNj#O8FGVnFyHab#$BvL z2b3x)`*fTV`Ek0XR}3~RcgWQcb9D|^PWp(nwE$w?k%juZ;0OnnKi%tM$cbDW&S^`p zrjW4?$QMNJw7qDS29d}#(Zj!XUsn3-nq~SHCsUzcH%=ha_uGl9n!Ab^5Vv$VjTV(9 z*gu7qaq3&!_0mFp`Jj0i*Cr$tV^#QmU<0b~`Tee0x_Um(pcShnTn6x+Xc{qE|NZ?9 zOpGG`d@KAj^H)4zN)5wlO3Cj!5$G6_2E3RON;&*iWP`BYE-RYouH5Gf@s-Xo@Y>UG>&{W1J6l2?2-S45W3CdT#g{YWl3{ zC(}0!q{BK$FiMAJYk5@z4OOXXi9@dU_jb@FpR13^!WCBEAaRePn^W31j`Daou&@WK zKI4gz+FO|o0MEbE6S}?dMuiSmpZs(uR5(%ydl#cCT{lqp{3(9^D5XsS$z7gWp~Adq zLu!cfF!Oue3Q9ysvpDb4wZ8*OS@!ViV7|V8JD--0aIL0r`_Wuufld%F&Qi&5?J%dn z;soM)3B6-w?Gb;%5Y+8~$O@z?iwgEW@K)d-ae4acU3+7p*ii$Nw@&~7iSa>^2XxcJ za;0FDP>%!SeWbGz^kF9EAN~rVBe0zjGKLZZF9f(+A{+cFnE99Ld{m?K_jCR(z)bI< zbTtuDT?91w4U-jxWZlijkcus(cGWp=57K2y#D~-7tAnspN7kQ(NWJ%1v~wud3QP|- z@(@7^oYNzt8=1ybHr!?9?46+1^u6$%MF%$Z@FZ712?Xbt(EQyYiCz0Yzh5B~&{oy@1Y(r2Srx7=7Y$kwYg05Z_UsZIl=W399mkie&HW ztrzMLP*}y-xmd@q3VvWwZ2SzxSeL7G0;1d9_%ahJQg4`jKWGNAiJZE4Gw%dAM`x0o z2xS)XruX$%#TqKFH;1BG{y^!@8M~`Aya>CTmhs0b{`?t$kavkI#5?q9Y>{L0{i^yv6_ZQb0iBNbLFocgp%@0eze21!|Z4?}#^ zvlw$HS(Q$=SrndHkRT`Tw~LgisM*gN+peVXbfr8=%dI_r1?OC+!rA?eDH=x?XbseT z^!=~S!=q-)2!XBo-Z%qC{tj+(uNtTrLva}MZJvwSdy705uqM*LhyOZP5g;FTz#9t` zcqef4uCTtkQsQ#d3x-_&y&7Q|p{pmEuC6fGp?F1`)*_6-RFjHcsoMT44BkkMDLP|4 zFrh1~V6l@Kv41XbyNJn7sInIfLsSfd07X38ll2gqj=b2DyQ1WuM_^crJOf{M7CmdhTvay&P=9XA7) zQ2uCE1fu+U7>XEh5j?WoBEwL``Vz-elLrvffx=?+GJHIevT(_pcviiCkn4fuMS|8~ zxxDMA+y+9Tkzrs0?xwPLSB^u9ER72lHx6TA)(WJr2A>&s31vyr)ed4tGw-kZCQva_ zZ2b*8px83%;Fv^w|EMKP5$JqL=VU3N)I#+v%H7&Vi=lHVgAw_?fxe`aO+~a5&h1*B zr0R2UDr?TLxcmwjR#XB54Xy$ykW{byNIx1JL^=lb42BV~bI)Dong`>oBzukK(_MOc z2)9j9xjr|QCdMCvz4HFeDb24eIx(f|p`}9+KCgz#lNTiNSaB6b6|OX;cL8cc!=J)< z8)Myiza#G>na#1;NsV?zm1QJ`GkAdQfi{Q|=OkTb$xurqjLzVlx*b1fL6Q415Kc1! zbsov4+LujR_r|EIJg@BeY&&0_`SSKqY%zFwKd(6a%c~W`2>*JM^RYTz7rjc)fsqe! z(bfHs-Dr^!*&ncsLe#75G2Uk9^3nN%tBLsJ@6q6@M|Sr{8L|Bpp(sF;fnv{K+pU<{ zLRFDkZ?d~?$8 zK;NgHn-5Chh#||fsHvZ=yJ#U75k9zjU?2GGPlA}pyI`TX5XW@2;OUq=)qo{E5$y8_ zLKVV{9lI1t(>6Wbqz1>8x{_^{Lf3-vLY3Jou`{LvC?{sg%QX90ZZ(_02!swA1;@8X z<^sr_0~nMPMSRh}=0!iSLg#d+iT7!O&W0%s1d?6@t7}QTtdnYmw}N=A5|^>eELqoh z7*+qmZo*#&q$PP~leAup+@hJ3@i;U#pFW(h@bMv42E3n$4)hx`?rGK4@{lnWEN|v* z@AOGBGgi;t&8~r2$Jn$;?q`aXTK04`;<4PKt3XNxzjU}s%L;0m$N^BG?R5!pkxt)hU!h^?VZGjqqUYPHNB`!>6 z%vxR3pQ25wAjtPOm(*X>O4q{i2Bx|i-=d*l2$6<&0)Cxzblj|uBggU>vR=rDocnrj z2Pc9e)oD>Ox=}dn%DPr}3x^GuDFy*h0OOxe6VE zS2kcKC<0oAyu530AntONPhm0zrqP7FV{_ty6ACLc+e*TWZx#g2^o`h0nO!44E)IUY zA4VJkP_^7pBGb%ZLdqE&m*bhk^t=oCyk7Ft6Mm?pR2o~Q*E`(k7x1%G&sW^P(sndS zGe?B>G$cOd%>V{@pC4MId`;NQ&Cv3uESXTs@Y8wUbOFK;ubz7DJ;nlTj~5n(^36M2 zndZn!_)x=TSW~IIO$lsL$u3LRGLmLZ3;@y^4bCNn_A7mKD7i)`qdF3aTnh*KUOz(1fj zi5X6%y&pdYj&kNuA)y&N{%!&mJXhp23NKwh#2>&#DI{BwC2!d)Su2r-#7*z>xj3UOtW)>Br%2Dm9Z%qhr_KmV6SnJc6LTO8cR4h}8TOy%Nt6A{DCL|)<_Id3 z2onw_c}6`*hRMWGDTO}JBdDtwIb)n?Qj&x@=bTY_xt6yJ_6u_7KYPP)CLeQOmRR07 zP|txK7*d>7TEneMYAeZ&+uzXm3)?>YUM6#M&pvm;H36aq7`SgC=onCZ;h`cgdmnY( zOg&sh=6>62DAG#dq6i$kOQ9x;+>WvB}DoH6EkYyaMrp zQP+@{$@2VzqF148OcI8xclmjs#UXXKyLv^7-xT`A7}fQ)#5jcH;*qeMfd+!1;dnwa z2sqi0M3fc0d-muTbv}L&h;uzcM)|tY`Uv6hGS#T?ORLR3?Ife90s_EKjQydU$5w(9 ze|qeIP(?&uz)ouzBt2eIgiS9bk{-A?fZUe9q$x@S#2J~i|G)~O+a_f?xI%wpH!@T) z8HXYl5CWp-WQT+dCUsBwJLX~Mn?PLX*(mcSB0w>uPCktZ$(~)7Dmg7fuILw(W!LcN z55r)F#KamwsWa4+6=V?;k zBq$drvmTh8UAbv&#s}Q@zdiy9=E!)5xXl4ML_XZ14!J?$q-#{$Nr|f_z-(I4E`LOS z5m(a!?fw+Su6G#dqVWW( zdejA-G%NdX$l>~F%mALWDl)0{Z)HJ($ol+X32%F1`odiVUl$bBu=ja)i8pyxS52nP zA@XJhG&S;b1;kkllNq8869&vZxKzWz`JSTVdNEBogAv27j;YGBREeJL+i>s)137x8KrM_ib!$k!=GOGa-yMoy%BRgQY5e%zVbvKS{Z6GmFWu`#s8?zao$Sm%ZDd+y)=y#57-e%;AHXfRG1#l08?VeiW6KC$;JdM zglsgQ82T?yr!iLAH`Zu+6~V&*aglfNDxu}TJnN>zo({_;)RuLLWN_5_2Bi^OV`eYi zuqUx3(;%o&1fsQmILp%a!fo2Go|9!7Lu*FE9-I{dy}L-1ocHhR7^`0>Ju{_rM~ycR z)dsiDF#A}<5@NR)DQ34bcr=MBQMP5hG%WJ+#wji6a;_8_ULi>;(^!h76~E9CU;Y|IzH&CtO+vL zh0nQ7)+3-ktoa=#fy{A{*1=?}ZbUMdbH$pd%m@r~dJ45=`~Di*p$ZT744{FT-SqUC z@K!>hiX|4X&J<*J+0)xUGD$y!9gkUOK!n{Xb%y8)%28upOwxmb2sk2xI z0!&kJ7zD5h4ein#fFlM~qY0|G;ln4-vZ-EyMbgASr0IUkad5&S?-`s^56jY(M))5p z90p2WrC8ODtt_wNsRVoj0i3wVO}Ar96Ll^Vh>X)yEa0&7)h?M)yzhfaSMH*_aVS~W z43NwY1FYTw0T7D>=ptnom-q^4w~MZyT6mJ{X4Au}cL_j|$mAE{((kOE8dcz4v4D13 zIDNqt&UV>yh4d%IAs#>Pd7trHdd$tKAuwj!?TII7$Do(g>sEQIp#2wvXN=`AeJSfH zFi=4&u8+Oe9$yLq0w&2w3r~F*gp1@SA=E8xy4oZ74M%0vOIxsK6B#@sVyr2KvPTei^wZSB#xbh zJH0Lz!e%(%b`VpeHo4#CF6L2TwsR_buC{7g#Z<7K*yI>V84qP#NsJ>753f?|=sc4m z#bgN<^cO|56m(lDfLe4*8j)K12Ok*LBDe;M>S8tW2D{PUS*icn#q8P*@fgof6alQ5A#Cb5{XE{W(0o==O&ex2ZIaoA8 zF)+Yu7Q4}L4LLflYL&=0&lYcd*C~{r6~-)FvweoDqM8Oh6*5~4M+;;1HB{x6e&d+ zd1g~jP}OiEp!zJqn&5mz)V^I2lQ}?0On}s1O#_0QDcE^1z#_)NGeQUl6&K&O!<2(# zLep~@j+^d72Px`rQzEiiY)hGiNZ00e(aky%v+W!(-Ke-Q1n3Fqk@pBvU5;&(*9}V} zKVPnF&G_M7zvqh=VnNl$+@WxS^zN}${WsZ4LH9N;0m;=>du}lTx|?p z=|)!}%V^RFuga$wZSDCEF$Ju<|A>Z_+lq6Kbi6PxQqmBEB>sLNH$g?miWe=cN8E^R z&hf)Nbe^bp>Q+Jr)pB`u6C5*505v3|F?yiTQblZ0Ue4icO+71xxkXhVY7T1n%*a8p zXRq4Cw?6Pw^iT5wOj85R6JW7m(@@#AVdWOk*}aE`cxHU~&yuKadw(WD{Rl#2V6scZ9xF zmXS>1w`rkGIo6t-P$rw1M4}Kxx_*P?+?z9GO>i0kn7oe}0f1&yjfDJZ>0v>R0iH$+ zLJj~2j9_|oTS7QTQ-|}&DBJY|zCw$r9LyG$j?jVqHl_t5N^5qrAIGK=h z9JZk+1tJV&?2K319swf&nZ{`vmlVS-;E#o`zT=f{grB=%iYkFu6E;iO@QJ!L*}1^l6~%M?IfNYKu4V9(+% z23IW7$|Pr>t-D4mu;$fjVy77zL3nC5`<2p)A) zN;=P6gkD1yq%bSKLg`JGjQSXmxJD7biCY3M;lN79ceDyh8siWAL@5@6>fGXI*a#d0 zelYlh6d6)WBND#U0!5B`5g}6B{ZA$rSRpQDC0zfiA~2M1)8;vljI;bhsrb!m1+m1u z7*9&lMhuIMuZ0xS?sk@WBcZ5^f8P}fl?aM)3><>)l*xQg(!O-7cttlrV+!ixzktN5rQA+%D32b0m_+MQKT3Z_Wrq zIt+(!@iUGpHU_6Ph@`D8Aw7z-3X7%q9w$LnZlw?`eE|{GkVOnZMBp13HWr@NTNw*R zwJY;0)VgCo^E!Gp4h@)1U>+vTVUJxD0oT002J}OD{(xY*GeCSr%>q>Wb2^ZNCy~wx zd2Ok_tugyk3el9iRIr?5AcaRVEB#$Rc>Y#`y(+BaKLoa*2fW3(M-TymAmc*7rnX5G zX~uj*gx@vyjc7gjq=?gkWe>v8PIM4v+QBwafV*hm=Zp+n1H^-i4;jk2b88?xOc|VF zERaIGj+UQ~3a8p+55g#*FmfZN8P!>Gp`SM&$7TumuOJ)1$Nu~}=JiRym29ON{vxhhxvH~%S_8h-;C32IF(t@P|1AGW(w2s|fd{fBf zn!4JHxBHT@Am zquUosSl;)ens zie@|?`n8;2isarFK+&sT$IEWE|G6Hp@dOAbelBoi6{kzL9)=PuXgu!~?vp*M%Cgr7 zRs(?cC?D}0O(e>RvTPjyADqFC3|Rc-K|;ak5m|nVyt3^(j#()5yFLNQo3HWc((T#v=*FO<&`io4TP&K9vZq!KI3S4Gvhl5uL{lH>; zhG~@bJQ(4iFQkN;L6}Sf3x^O#;sE?NuxA!A2d>>OK*_-7CPr>?d{`BTh`rPdColwT z)aJ;olfBn4tjr8gbMyUnW7c^Vy1Mebw@CG2mkkI#BqE;pKD zxcJ*iT|o(T1!8bc6D(;A;+!w3aD^phc?MD9kQ|MFxCi$+9t=!J;EHL-Z)C)bh^SJM z_g^EPT&C9W=xQ*V#lKzlFu{JWUm>l+-)boTn}!Q`8Or3-@13D9QF=_XOoPMHyvdkQ ztejW=W3!3A4sC$v_Rw)UF4CZ4L50KSoG6nO>|27zIQm@~eLUxiPTn0i6{*@puez1q z^d}PUZ7V50Zv463lbqaZU^;`{7G0~^DtXLYIAgI19naEP{za#GkbAY&T<8usr07K0 zQ^AyvFO4G-{a)xt$xS?e2CCj8<4UtGsj^bCcYroTYePqZ1lbz&B3%Twm1PER0%o&# zE#fNrepyL;LtrDN;(Lr$UfD&U#*CuLPK7rW99M0)OY$a3Q%YQUQ^Fzs9WEc|*=7To zki^w0plA$2P3AnbiV7K7p?)HH7u_8)Mh~4A%AmTOk`v*?ZXPTGp7Trx0PPOMeTzIwkQM9@f1~)g(T0_)y>8Q?bVitmA%&Y^5Q1mFRAnjK4F;zd+@4*BF$b@XXnsRUx z-_YPH(!;)N2nE>^I1s0#k>VW$;g}o^5MS4q8x9_v7$gDa!=WF-KT;vXC2UuCujqw9 zmRj&N(HLSVZ*&?M2k(*AqPnIp*8-5Vh!i3Vm>QkanwhG*nyPsZMvL=udQFm>ihADi zw+RB|tw0;{wjlV~F@^IP|2Kf$*DRQWz(VoZ+KSwwe(TfNN zXfS%X3lnVEs#L7{PT$7pig;n7!fXsdAO; z(6R9H)B_g^fSCi3xy=ra=2+Jgs7NxH&3thMVOmCn@@o6J=`%SidayaXZ>z#`jjr<)eSuJg!}Ee#T@&A zMvasdB29W=Kq(LhZU$9epuXoN?Sq^$`U*9*g!2WDrPa1SsBABRQ&R91-ZH5ovv;E8 zw$t9Vy2fr}_Zr*)@nwbi%Z$#jG%P8K^^9>{3cVjq2~V;hMGFZhn{oWP@QMU#p+qdo z-KBO9GwcpqQ3@(i9lVn;Nxtdsv$(CNIL7KpgZUbXtd~v*PP2UQRIMFWrHYR!n6+m!Obh zIvQ#Zqgi zmA}sh^P5S9(FCB1D(>`T`~TUxMkfIyci(@!u5_ zA6B?qafoE-LKsQb;POYQ3MrN;RGyv_(C@&TsWi*kl|NQZWPC{H>~>b^w(Z*L1-2$0 zoOrQQ4EkXV$@;eG^`;C&jt^rp z`=SQ9TyjdmdEi~hoy#ABZ-?z=VM&$zMX{Q}5j3ZDiQLqx!OfkCk^DE{X*kdeV1kR3 zeq;td7zI`($q22x+ap>If{ytBE1slRpgILT@0b$ABm{>!9Clqav!a1F;FDN$Iwk*y zHHoiUbDg=BZt0u_&*36Bxd3uj1zfE$Iw?c%RA|9QerXwH+Ixm^i>P~pMIahhD) z()n|7Cg@5nT#s17FoK1AI6XlF7|SxfZi?o7`c*Phk&Ocr-%B}Ia98Pf;2U8CIt?0W zVz_m#hNBZLXs1?2oMuMU>9F{(cyN#V+#pEsL9-2s9x!9 z5%r%{HhiPmJ6&^kzUQ;p!iWw|^^ZFdrsnV{z?~NZ)*Mm*&~V@vh!WYB^HpOpk|^kLT=(qAYE225ci>^Mkq31uK5S+3CCy%23#K5T@K6PY*Ua~i$UoHCZ#sc9fEr}63{af+hVA`-!T)>9@|JQoMFH=k{&Kc z#0c&Nz(jBh)z7{mWE8URX6V0mKQ+q)>2=#X@G7CGUO^eKHCg;rchM8BrQNHW=TltU13PqWNZ>oZE&GDrlet3*9p zO@x49sehEYi%b-V}nn{`QnH@g4_XaV&6wNlO!LNXY64hZj4+3a=mz| za9vOf(oQT*m!*d)v++0Yj#7cmgYdfX27SswSZ0PJv-V;tQO3|hws1RzI1iwTzLh=G zs;9UdVv1%0r#JIVb6tde*uWH1rUr8aQdJrJ*$4^WmX3lI{4fX14kIi=Fo!LXNz_#|&l4?th^gxv!Z9jP3|vv(i!GK59d0#U*a_H^G>O)OHbXze0v zNcBg-s{}!Zh;fxW_vMi)Rt6uetuWQMp(!G%u1*;?Eo;Q&keLXI+Q6SLMe%7IjX>N1 z-$osX5oUX%zNG!5`-c7z0M02s}!X>A0~6N>6z8kV$KpC?LpH-X z0;cga;BM5BW(|%8jpViC+B3VOq`@gr{WY4R99*26boV5HO@nxp^Ryw5c1Gk_FnvOq ziFDN`AZjtF;nUdpIqLB4MZ}Y%51>SoFyByL`ofhxX1?(ZFJ1#LAU?&?PH6#rR8BBw z;!vp2`zstcjpXIFgW=Vw5wL<`f3Co3Akq-gGWHq{M>>^AldW}?nlfa=P!V>eh;9Rc zQo6{0i6hor^bIfSR8Md}bC9H>c9KnO$CWLFkS3Ux-1OVhW@fi2XQayxiy3c~v-|O7 zvwP!`!E6=uI#LL*$EA}-p2fbx-~`uj>LrS)ATQWZc?b^(=OoTDN`?N?SAta+`MrwL znenP%tC9JImc|m0vjS;FaGJ_NNTP%z3|Ewj68?G;+X79`ijolqKzJ81^Z3IqfD&oR z-8I-YSKSh@gj*b95`&?1lNxmSY%&a*cnB`S0DvtaG|}OM!4h_2*DSDV9H)Y zTyQucr)cTQgkmGD#eU6!r1aDD>u@3VD$W(h$YO(}JKO-eT*btg?+4EOGpHHo8^F?? ziz)l`g#ET5d!RkbK}JJl8Y6FN&fi1Xe@R-#H^E;sV@LMnTyj`8yj3nYbo}lI${rh0 zEUGn#0~+wUHRTXunn6_X@Z=QDd80dl13LUkrJ2NI4R~gbVCa%acA>_DQ|;!FFhpjA z=$f*l#J1cyxpVdtp3@KlP4-**m--vwVYz}CcGvL~hzaZmXv~Dh1>dx;h&fRlW;KCc zR+dRz$51L0OexM957NgO9Cj}24Q8=+upWR7AUEE<9@d05A6p^33!9J1{qt|Gg7-Y9 zNf=k4!f04$Ue!3JJy@a&{Uv;J=r%p!gL?894^DJg} z#skr5K>cHgpfCje_52dkcPO_4i)?|2JUuV@I(*WfK+Epj2~cCRZoqe+!!*xgreI6I zf8XkaY$fm)bz=(&Zm42tqM`bj2@E2@)Ejb^Ibq-pip;vyz7-rn_8m^s3;FxY>^c}R z?BN`EPoN)}CLl=W`@Ka)AKJA?$MtjcC}OehP!`kSpM!wQt_LY3Nf;op3|4Z`hQa?u zKY~Ecol%jIG#G3{loJvX7ZOG;q&2)EtAdJIOw;M~O>Q6+dDzxgcn4S!w_lz2vNx6> zJ`{y$zO1b|ZI8O@tI~1Z+7#V8@L{ZBLQL8v)#Fysf?cK+D+}XQAxfq+lEp{9A6(Pe z7X#OFWR5=!=tN>+aLA}}1=j?ecW04tMB8Bk2^V6@Cem0RmHq@n6lprG9_*@-K4b4y zS}^G^%C%0!RF!OAdP8rn0BY&G0=*J*F&SIDS|+z69ZnSbIQKlK#V$;%>knn74#7|w-)WhQwGdW^m7t;xB^as! zF_qdagBY5%Lnt>4S|#nkDcD>Rt^#;9F@r7|?9bGnBKAi!VIE z5!WJgCCWMknZdvg7zM8&>;TOB0CJH?2m9UP?;?ws2M|Eqki+!o!tan30N4!GHtr;w zRNor>Ezt={Qrb1f@XkYEJLIcRuN|}ZTH>Ks#XUvh8e~IM0yQ{<;Ob5HbB&RVY@lJq z$%JNL1>N8VTSB0+b%DCpA}&siSCy-|qi?`V2X$teBxg7*Zr3vj?E&d3mwsQ24T`tQyJ87m#Yb=9;qbRgg}&Oa2VV*=q1sfbgDai-W;1KCz8Bmtaj`8eS1r2(Xi&3Tm)_`O@Ebt z&8#7qbx1Oe$^u8IWfHeMJI-n@H~^S7&ppRTn1iF5j@GqVxKX&>LNlX~X3jZ?i2)eG zd1y+xRO<}c2xWLM|{4AYMydL+gSXD57qVz3D^<6q&}AvAE#Ta~C>P z6?z7eXi+-O-wT$u5L_VBa^nRbtAbS(`8Bu-1>6xM7R+~X_*x-}m?3<=rbR1w2IH^G z?h}(>MBNr$Byy%T9Y6rR2Lud!u(=Fn32}sHWt;|AcHSh}fT+3c!Oyr5ce5e8R-Yz~ zYY;vP*F+8O_JqfZN|&rS!$p=_4W{KedqdbCxR&fpkswCriY>VnF86lwL;#qS|4V>Q zD14PxsuZMI4fogkQ%5vJGJno5yZL9TkH^0qLct9fG`W?mJTV>0A~{30RX0P<2h;KnNfKF==nJ z#bxyE7=tpc%^|sza4-;3@aHsx^F-GGT=E#_lH+A5vN@EQwS$xFtrLf0!h8_RgbM~b zF)tPjNwqH!K8Rs41xO@?AUxrVbgM>3bj}(IT?BG(#pCuv<9Irr6upU%T~HRnEKPw2 zN$rlc0g!f3Dru^Z{+T6ZD!zhmK<$umwHmBxD3s3*sTkgws(4zFsrD1MuNg8yv14HT z4h$a*RTyG1%3C!Y=Q~}$)damGjJzsgw3!}o8Jy;@ty#un4-Zyu0Xi2^S#}S;-{*u- zdI_OBD==glb6ElnQgQ{|i;^7?H7NJd<_byq+ml@S5a)t8G;{HFwaFgYPz>Xkh?X+6 zbdbOl8iF>^-`#1>&O>K#R^@;08UjcBabcEbbt^OuXtP8>?CkrpvQwPAo`2L zkSw|K))GnzklC|c_jE8E#W=hlW+ilTmcNty1B9+ojM3~SrV4fe%pO*NXbIPqN_Hn# z*FmWu+V1j{?#*b|U1GbDI9L`QdGvM3sw za=oTC^c;FLBkX!R8e%^hF|7c!R6OUQ3=nSB1 zuzo%_7r=at%A8YgNXdjEaJhTt?gokG99`kp0ZbLBr#XAD=q3q=@*PYnn@j_Wb{+*# zXEYxxcuC;Op7os>Fa!X`u3>Z71drSsGAmisEBZGonJp7|G{&|5$~&l4Rk*qM*WOcd ziv@E6X*a$~DlCa_E~<+f(5BE?LNe`8id#|w=_l8=hdTlG3Z)v9+3jg1)wA4FGB}_w zI>vxx;SdHESgP>usMB^)B)i7|RXy2x$Z-@569pC}Be)fe_IHv*MDn`gJDR}@t&zNNUqxdNdQ3ya0<1cGP|+I^)T zv0+94zcAJ$KQ0q<xXYfK7lf8-AgA!oLFNR+-uuIH zBlyzT6g-MdX2FqGj*$m6_VDF{A$DlMBL&Yi=^C(YC-|ZGb1Isn5opwc5VOb;3@!(G z2bL*A(q?EhZ`6dp{{fDPR~9_{7Ob=3wUhU(pm#TMvJAjjwPyF9d===(5*3$+yQgep z8cL->_L{q40fOY<4U(xtR}Id0I#QV^4xdJN4@}89cX5opWlS7i@b8U7fg;5U6t)yE zT8b21v_P@8Q25~#r?~5~NP!k8+TvE+-4|Hg-Q8Ulm)&J|pZ=fR`{E||#Z68!$s}{m zo0F5wnVIj0ka#Lqj8(&nj7sIbvDpH{+g?1HjO@+wdMxVl^Y2-4x}-L1AOCAHKCm8z zFHcWu(4_ESM}(G*cq?Ha&LY@soa=F`|H-#K`;|;jujlE{oo3_fc@yQ1LacyDoUIsS zdpb$ps7rPpLN6E>cecS$V!PL_)D3LQM;i}hMcY~F|N`NBZ-a#B%J z;|cH8dOYW9zaBiX$*}31(2Fy1-dY;f8X&V`qhnbg=VjO#tf$4J7F-vE^?NXefBus3 z$Mt!Jf04SnpzYOkj?}9ciKNZ&=dYtnZM};l6roF$8H*|~j)g!Xi#^R`pM1#zt3P}YmQdL|y{e|ei&^n(Ls>{E7x=J2Z*ZHlOFe&a&7WYPTRC0rIuF#b1R z^nDOF<58YlV2wYH_;7uq=VA?RT92-Ac;;6|=8>tfi-1<4+~~Bo?8?q>8ha|KwSx`{ zb32>hJSQBaIP^UmV)#yKQ$G`MiXL1HS=V-n3ODb!gOE>tk`zTdWCyD=Pak4YX_2KUxW>I*={}lAG`@70!QTlb~J&0BUl56rk z?AMEFxH1Od%^@t^<-_A>YYr6MgmP8R2%AzHIa)vjanxT&4HI> z7#aIN{6hYm^~GY}YegZxkT0Q}WXXXR7ixMm(K8=M`DbTlFQlhQzY54_@oXV?%sT?P zS0jTv#_F~JHw@)?ifmy?vO;#bSi$cyr$P{_&knd%yz&h~sj5Auzk3Q6)&)H6 zR|#_xY1QBx!qxomC+RFJC>5s8rI+ZGvm~WS?|N`4FG~7^s6LI6&Mk(lyJwVHz0`_x zX3sc;krS+J@|uT8C>7_Kzjzm;!6<&`&kkJ8`;_hxHsx_y`rj+_N<=5#f11#Oj`EQf zH=DKM!Yy<)RM8@%2Dk`?D{4`#)&`@-xOnz;={>{fCqP3Ql!k}NeS3iwg4mA>XtPsZ3yDmnI)Tdu{{MNlq{ zCr4D_&Ru`~N}5;oKkd==ziQUki8_s~I~-~QR%@txrQa=Ta(xc}Xg;hJd-JB`qk|ZWZsSIu{Fg;KgnRZ^ zA6f4F1iMyz{%6!GS7Gi65qyROx~uBbxhaBz)bGeuD8jwXnkdX^Tt9oy*7OX;ry84J zl11{Zg-Wgp4~LF4o&d{b{C375Cy7%+Tk7vU%Dnk^ ztS|3f0XS|e%XnARSsx2X^_-tkyrzy3c!TF?kvpWkV(!vvuxNP7+Qz+4#G^JM#K@L{^Da-)4!4x)!@S>j_>y2SsG=y9Ay{&IJlVh zL3Fbqd&DWsRButV|6fz-F>^p1WLUrr|NSyntA`Pq$>;z68MIeHlt^Wy6y8te)$eoT0=wEq`gO!%uW(&uunw z>6tHN_P)mP?*MrtG(Ijr8sOcO*ogm|vB%4g&L>R3r)gRZA&s+})ZI?2>dmy=|IV?@ zeaXnqZLX?wknOzFp`eiv$LSPK)Nh@`{rQ)7w?Umh<1x@m!ZWpai5_4m)GgfC=}C$I zHD#(dB$wuu(5wW{bh!IF!Esr`22$P2jH=XXM;9XvCR{Cz|f&n*|fn0sJF*S#JnW*+w?_=zLu*_ZND7V-3>Ajj7Zj~q#B z`Uz$7966p)97d-}LoHl!pME^&5cBl)wtr2@td!Gp)y6bWf7((a&0`l|YrN8|zhYSF zzf-1*y?v}spHQe5m>ysm=wa%XpovML*|5J9&Wf3InZheH3;G3@>bp^A*$MqC94}iu zoTfo5avb`H)M-kd8jJbCNU5#IOn~fTEC*3`>%qrg%*+MSB7zsC1C`Sy|1dAw4aL+A zlno>vE!VuDiXeb-hI80&H=>HtBNsE$j`%NawH0Oj=?4UNMdox;OD2CxN&cpopECc3 zQ`9v(=(~T_$|C%=u6KFnq?$5F%3#Y~KI*63+?9h^Rpgk6igdpdoElE}KsYQTgZMr} zvHeYh`}=k|%&8!bvg=uaO5a@i>ytUI=Zh3BQSa@`J1+;6`MGOGueGk^pI9qo#i^OG zcaG`NX*Zq#?8a(NE3a6+g{Y-dyQ92&aV=wu|D*ADe)anOLlDcB!;pP^9F3*9<2S(r zN-|cwzebC1mMq1%qbP~q5k4WdP#W^Gl%h@ivFjkuVa&i{NWaEtD)hc=u3xy`S7(8= zyH0icNxQu=?Xqn5x8G@Q_M4$!+#RHztH#TJnkSNYlJRTnu@h^p%bfHOdkCj6ZA^W- zZZSu8i>_1ERwwayRq0bBsf2U0XG$@@TT-E-hrN|h8}ec2+F!*0iBK@ElE*q8h5U^{ z7t!-~fu$q~1wZkB4Fi>^<&MpX7}%y0Ul{}juy~{~gUid5^fp5p+zP>hQe`M<^|H?XzWu*SR63RKaShvw)n zJFx&Gs)<@6ExDdl)>OA97SeED$r9O4>vlWF_7Hm91y&!cqN)+>7KD+jG&4C;F2l>}SIE}+=r;()8N^!Hy6CfH~ zH+4>I-!~3-pz@aSpxU72%S?~NSI_Ab@COLfAN)@TiCx1$O|s`^sgqrPPT>-DA8kpS z>f;>XqMRaoWSW_^JLScnS~d*h{?MettIyJE1hHyM(RL1Eh6?!lfBJcH7Pv*4-8MZ>qhQjY}oPQ z!}C~I8n1{x;kkYMs9azEiZw<|UwXcW>6Z+DU7y0m=n~-~caNu^%tdoc97vA4D{fov z;`u9`$jz~rlVlG03-ZABx%i&6!f?jarD;;lTihEzyIM#2wvz(3 zk`4ccvnjyPN1vn?l4X-(Nbl`uOzeR4!{pDU`F#AkQ*>(imPoT)gr~-Zsr+AP8W4Q5 zrDXo^(-wiE%DfVm>G=xthvyN#snm-!>LXl-h2ssjv70vCVd|Xylp`;Owum zD4&|86njJ`7UkhkC-NcRexx85v1)SD#d#}XYHBDIz&0yH|HK26w40}(+fEZ`>(;r2 z^>v0wxG><)$5}gZmAt=a-o=e>$94G6m|kTwAKRuj=2?7GHG1{ot)Yyt{wlfMQH8F7 zVXqoJdbR^M5?lG@lcb5Sej&;Iolx8K6a_^p38U5Aec~gLA0eO2d0xH6!po38Xu~Qb zX2$4z{QbcrMtsuYfQhPG>xng|Pa2Kf`oWaJ1^#eQ5dJ%HMbnSiylMA15dK{YZr|4< zI0L6aWy<9YgwbLaEGv)_c0y?bxr4yD|GtkJ7KN3FaW-4lU@LJfR(bFppY23N+a6hS zn+=&=*Mxu73n?EjA~u_oK7H;GD^{A?I~Fu9VjMyC_V>^P^-005N$3IBY<|t1=b-QA z%=Y_NB&|y`(b!Mud8u%iN_?_(RRi;a@}D`dYTj#*R8jfP(kC1?wOvzAB~y!LKGrOG z5gD!$RKJP8*SQo5a1GS;An_a^Qpa!J>^wP4kni32-L5z=V9tr_?(tve^7MjVYmbj1 z7|<0^d&$rl%p0DAIm;7>Avs`7fcbdj85Pw>m-{r0-|HT?BUGw(oLYkUIWs3U*r+I`r)l}qY(X{7X zAKM~d!>Gy2HP#1bxxpryazc)G@}FOhU|7iNFL9L?Rb9L7?4i#^}-r$k?MS+f58p!RR!3L>uAkuR}NV`v%(YL6IQ&1zq` zsQ@$Nzx>Q5Qpc+B^23;-?XHZ^w&2h=>&90=iP%pJ>#`dw&tGDFl&y55=pFu`v(r2_ zg$K0In9ko+lG^*qOV4j4a2no87pkuL{w?}e#DP2#;To|*90~t5c}PdV_cBTRWT05a z23L=GO3abqXcpek@A*DZT}`xM$NxP|vaFaRS;Gaf7Q?jWs_n0VAeF=f_L&!^(LQ$+ z&%&EW28fMQOJA{%7tlmkR_TY~IIpdkmwmPbnHk>H-Mnm~unIoC+;VN-V9hq zk#R6QVL_SoCtyX{t2ib41C1u>TS~{c(|Hr#MTDBDJlDu{ENvq&(EigT(`>AR((5{6-_ab#TbD?lpoSQz zerHVTZdL_x=RiE;?f`{J7egsl)AQLMIOfE)#LNM!6AaCs>Z;5!p3E=H_4@xc!jlp_ zY-lKP2w@l|>llA^%Xid{3s0ovgI=;YFhSTkO2j2%KG>*TLi#A}d~MUmNQk?>s-Z zh_Uy63M4)57{mTe4(Orz!p3q(-jDxG%ncUuf^2;RexC19^u4hJDqOnbRve}d1JGp88LYoPegN5LPyT(6y zApEDgC&dJW*(@ow_EHT7gX23YAB2=5V2^4X5x z;de2ouAgQUK3=>8en@$0vHeLt#x5y^naB=C^Q^^njO8gQvEve2^U|`HTJ}u|ZI5@s z!YZDD@*9Qn}PZd_AZ`YF)}Az86A2&g$!c| zVl%d5un(MaL#<@Yl~@$QS@5<4JRQH!x)jeOYXqf*)iX5`UrJ)1jHa+ga|*<<%5k!e267 z+ei8jI#RgC75X!QNOZ0iT!5y}KHR1g4%Z@F)&GGUGpa6#PIu*N!MvO2aa?p zG?HM}p?(qjM!RC{`1$vMa%?MWfwyg&4S(NZ^<0Pkwd?k@{t%^?ev3|_7YcZ#;wqiZ z)X6h%OJ(Dg^mzF9`BPqfq_D_Gna?XUgmQF){kT}v#Pa^D>X1yXpiG*(L}3aNO)Y)@ zCxA-^()YCdE$#{XAufLSUR&ZL6Tf_DwJ6kwsVdC>^wcoDkW+S!xNL zMz0{dn)sp>kH$&RT=&6fBIVx zHQKqUJThk~_3Dw)t~cjK85A#GzjJ*Xf%PO8XN2S9PfplFLROwE?dP5viO$nsrehCR zcnxM$dITAH_k*mXVuMU2U&tFb+322y>#>qBK?Q9kg?%i>ux#bZdN#TB^#~!f{M?^U zXxqBtx`KJ!EF)66&{VJTx&mv!)WK2T++?rIQhS8Cqy*Q-xDQbxH z@XT__SOQv~=_k(fj4*HU%2fY04}bbD87JA=*J77Nn|3KzuJHwdvelQ^1p!W95?tRx z4<8lRMTsd5ZNJ>@R9pTcy$juDSMMeE-iB8l!5^s)sf${YA$$?xIP;;_y#95}BPV zbtX)yZflQ^WBsiVU#({QLXyWN9k*Y$I`EvXoFYvOeW=0d)L30viKGLji(w`~?ThgWHueHT51=pg z+hZ51P8*3-R%fe8l}fh->O7}Tl}VuBwO*Vu`ngGKT~s_B5=dLR+Fw;5YE$jL=UL2R z>zhi^C@7i`ONVo^-Scr&*DwL(}K7)&k8=Vj8QXY-0DAs{Nr(SAw1OZIZJ1iuXMs03*t&P#!t}4 zAJg%L8~2(9XXDu`qx&{%mz6_wT$J7oQSTBJzwkfEAOY#ubLQ!0R{5xB%NFHZp^Z3mz3X0jinmdCFSiu zOuR0R=5x&8cwsa7V~Tt4s~FW6(^mZiJWp=e893NuR@w-oG zqCWGrj1#+(yC-B#qo=OO{>yKze&#ZDG-x&o;eY?uFT~18@?tc}+M_*ncg^F3JvvU< zvyF->wl6~z@MIPxAND34wRZ1rT3G9pW>&7Dlm&C(*NBw zTx8Fa$_U3m+M?(iT!SI>Hv2<6-I?CIlgiwy$+fZKor3ro>7U1i{Qbm6W@F5wup!23 z#CWQr{QcuFNs*-y#P71gcHU*sq^>39g62K zY78PZThsZ^T|)=Bv9wrjXS?R0E^ikm&7K|ZYDV&`xYTr@#yj;*zK)-fmRP>Sxryyr zy@$F&s09aKiY!IJ$jY9N>w34BE?ZHQW(=Jf-GAOJZumCeUIVYxlM8s%%?r6ulhk>> zL@t-UUTZHJeD{NLxpr$SKssOR!BG#-61q-03HP&LAcJjrJz4eBq-_YEtClBaqP%$ zUniViS3!R65BFJ64#nklq)1GS*r*WMq28E$77FfMqcBOM<2JKVa=(aBi~b>R9=H|z zn-)jyxwkjPS)H$er2o&?-Oq}W;dme6ZFkt=WH+ui$=WVcdd z%}g)P)ltT{UoI?a40Trh`&_kbMASAVbq8)-wQ(tVMG>i#itx4D9{$uvwC~)~Q zvGWvL?;-y6dY+dR0Rhpks4uD>WA5H?uCbH%(;=20cLE0IKkn<1_S-kjZBQmm1lY%6 zHCq(wGjJ_ZKH5!u&#@uxToEC0RgEy*>?IW&qsEHhPS=>LWHm(94Qos!jL52d>fN%5 zm&B&twIdu_-5XvpTpS|?cYcCX_G?W#<_vswECRLK8w9W|&` z8T*}0+b5eqZk7ucDv@W$|6FT+XY=+=CXn0ZiUp^3uagGDsx@!B0Q(*O7!N(=))CgV z+jtm`s&`mEL~+`f^@u;g12gRWa}9dMSL^%-G;(TavmuYvU2d9%8I1fRxL#GvBK2T? zBi>zmknJmDH+W@B0G)A18UKb=@_FmXt>MLe)oMZV`SOVObQT?DZ|brSmO2`cL1% zQSfNhw$+w<-nF>~sau^dUa6z}zn2PxApcn>kDP|2Cco_kVF8 z4t@NsppEzPUYqbYsP({=Z^IsEHS<{{QIz%a-2`9ifr`$4Qp=blCys`v-SU z-tOMfltQ>S5W<0hqGZU*%8muHiqHlEX-D^Vcn3TlB2I_)&`Uu0aNpVGQQ`{ZMA@_k z*b4EjZ6X(Ns@h2-|FOHFIkUozVY{u;L7)FkO>!(f-mpf5s^%7cHstPD*yln zF$BB%cF)C+!mXQLC|XFy_<6eC=b_FLM<6!N7TD3Qd>bO+?>uL|;9cFj`lld1x3sX{3JDy++^#h_ z-NoKRAWc_>=jT;-Vh~BlkfG|teHm(C0*q)v9>LK!9$+^&uRxPAfZf3f`q7f`$;{)% z+7lqOJ{X)-)Gp%kcJCGm0GJ@oJQ~x-{Wlu+eqVxYAV%ZK z0bc9#nMxlHB>DYZZy$!%DjGe@x}z5+xvo_F7lHFex> zM}^KrkO`8Vsz)G8BAbHH*g&Y85%p`RH;E7 z16bNw8koi*tm?7>f~);KYN7BexNjve@P5mg+1O1Sa|+wOym?p@?wnhK?VtLB?=qc1 z*AMZkQ+*MT5!Bq;jH&Nq*X@~;&FdsB%Ig(n$cUOhd<4Z^1(hB|ab!XriBJA2-W?+% zn^v-yUauNwXWN$m&ep{){oMMou_g(1ysarM0O-@8Nz-~YznweEm%js^SJSdEx{5qhT z2)b+<=;L}DQQ5hZjygNLh9%417E|XbJy&>+=xUEhd2tS7Hr|kF6j{V{kzWOa0&aJ& zo?&lf8)Nn#YQj|Xyy+k#520GGoVNc1K<*NA)NG6cxt=~8_egZWmqZ2=AtdM?^x2=~ z(1RKq;C!IFMrIBnA^6J2+siuHCnhI(LSa(NLE0I(D4q019ORaJ*6P8vJL( zHjw14l;Mr@YmILZnb;n8WSad03zG zMFUa*1ao%v2L*cjf$D(h1MqL_HR%r2e;ezV{mZ?+k~3q68qkq-l^7^qT$Oy~z;#&N zN&W5mS?mPA69BT|eIa<4c)hmAx8U!FJU(BA!eCdIgpy#}8>oInnFX6J*j5a4n~1tZ zixnXUESv{uOxpN&)Z7pPqs7n-hQ^+J*rlvl zzddy^GCDIo3Il1IB8_MUk8W8>#2qEi*sJFWt?8EnOly|am~SBWrU)Yd)u+wpKvuGd zhj{+?DWu!<2!+wjJX}`0ft$O#Z4{5Yn+BoLs_`kg2G=WC@ggkdP@Pg)RZHn$ zUt$@)Y@GRR?asLDew6D() z-@r7oX}G}%J#Fn+yo<+G(KkmJ`4H5<$7{31XXFfEd}#A>C|jTkp4T*^Fvo_d3al~& ztxs^?Jwbnb{33H=Y{&Nog9aLyAeG-5%Qte;ynDR9LpYbJW`b)^uEFfzvQf%V3TC%G ztdof$gCoqtT(i0Fe&SK8*6~dCB6xfvpjPU72yw%Cm#xtx`IrwxF`?f|BR>V+e1RPapts_7fzw_ghRnP_;xl|6# zzPL}U2f1||Ctd?CW809*=&7sZTXGP*`~@f(oRtMdt>c0j+fP~oL4G%JVA!;C$KTkQ z`gT-_j2#-DdxG2;f$MD#11Aga5O<|TYkM1qf3;%Qm)DN(ix567GgiZ1TU2YM*-R&C ztvfB1;6go#?AV*w!tC?M{uO-Wkbq~lUp^%J98`kV*K-12uCICJ8P9#W{k10L7`yst z7g@FGjKapK(p@QE%)OgHvq7AXBm(`Vb+47Ama*(D8lnhq{Xqn-(y`SSOF6l zh?=Vk)YzCz&$hzUm5#|s&upNO2+M=}cN>*1rA@<#`+(zxnJUDeYLqWJX=)mi zy#AFTrhl!x2JflD!0i9=yQfTIZW3%}poiL~;fN~!H_FO^(wMN3;rgL5eF-=CWK(O@ zpxABf-bA+F64E(y0=-r{6ujXM_PUADwM0n*RafZ7jcSuwfmi;<+xajwswlm)UwYs1 ztKU`K@r0G~Lyv~+J>=}M5CMDl*BgDjeT#&Zh21iwAkxttx?!0KfFM z{(;JL^gu%rfcnvPd&Q5L!oQsK9`0q)AWMov5{n{tNT`(BiynbXON!5Jh0Ny-b+p&L6> zcGpYfutzWkIq{a7PvEIivW@(d(%-`(8DFZ%4im29_4(LG02I{bYj9u#b9+RsltE+w zEsTfp-TOtS-oVv>F5{(;4kA<`?u0E~5p1u+TZHL-D{Kipw@=ujkUWy@=;XC}^@wau zg)Cj~KNpK_nGS}Sg0H+c+&O*yOa5`x?pM*PJ%=ONt!ki~Fn}o&*cq~V)OGC}=e8}k z0_#*BPcJ)iZH_3uTPXvQIG^wGfxV#1=c`o^R9W5Fz`FnFUY9{Sa5A*wz!ZIP=;3~J zPe}Q#x8rsOyT7Eb2RTM_OO$&?jw5hNZhK_oz2faSwu;NP=6xeBvmxNU4kr=} zuL2;bjFeyObzlUk5TG0BK#n?2$$`c?{4hdiPV=%j8(R^$?D>H|#HzTn*BO(Uy zaNgm4oe4nnAeXCFwlkPdo0JVZ7#qV82_v=$U9Q444tX2GSKf_UEBbCXwqlNM^mOjB z+-G9H=2p%GUQs|0lVIcss<8deHEJVpsizIx$hG1cTbGxf=X|Jz8vmz^xtr;Dj~Mry z-e``-ScX1mj)V7+clUwn$8^%R|5Avo>}pxN?cKFjGARjJud6O})yjBl4AT@87EtSO z>?93+Obctq+>L2S80WmW`*|U8;_7w{kvL8aytGhuejJAC&BE;n;oQzBU*;PU8OE%u zcB-ov0`DrcS{k)XFYTJHeY3g8+wKLj$uKf!NNG%u0_VAJ%*+h}AOq|OxYv^CxbY6C z_kZ-E&^-qn+dJ3B{=Y2D!qPxa=ZV$^C=n;%|6x61yooOa2a(H{Yqyt0@*V0mjlNdo zZjIo-p<4WR0>Y}rpDtV8SGeZkt)Z9BvR|96851CN-;;$39`=y7*sUv2$#A_hZrw(x%M$`x@n z^QYQ`EKtoq?voPoG0lJm4s$-|XO?dI8~_^pQC?7?Udf|oAal@9tR+1jwIazIU63c| zW}?ecjB%El%S&7vyKMM*y*JK-a6S`-FiKDM3et!3(f#5}Jv3~93SuK&yy2FZ3>;Sp zhqY6gK;7cXEGfUoOOovSQ<#PGwMU?DJH)WGRZQ`uDEpus>v9Kjur>({-1p+t;^#l- zuQZuA8ALxg3Xg{NJRDhp#fsRN*c`@3k}Y>X@A|Hmb+|%&&bZE2w(v1Vv==w+>E3Hg zkK|1!%IQ-wl9;-crwg%A$jFY&Pu$facyc-qY~!12YAxvgan~7qG=W;ls|wsX8)G^; zFh#iN3MXji`JyE9^$;ft4e?4kquwV%bEyh%mpldAuavghSB3bSdEOR z7HF#fa7?S;N`Ag&oCQ5|x|wQ{E=G-vX{?4N16=#Pp_^GwyV&DWJ+7_aPdvS`3hBIW z0}qGN^Pn3ETCm)fNJ5qFxU$=Jvkv-RLjhQMyWr$DJ-7R1A#YH#xOw;$CBn#nw2X$wnOVVJAKTid7)cMVR1 zJ^Y~UPd!s&th$iayA#L>5PqB?eI;_kRJC!Nv&(f=4sf767{V^CS@E5A&Xx%{@6WaY zyH)Q88cmEh3a;UnF6xn2Zu5>Cj%KjN`8M|<+S?Gl9kSad?QXyoA3W3}cpZXx_jzt_ zDv8i&0{pP|fB-wX&9)W|-aGqm2G)n&A8mfoEDXiJ{PA@NvI+OV0o>Wa5E4J^9OxQ^49`(F z>a?4cJIdR8wJe-sk-cUZK_GiyVYF~h6La)M3 za?qz>k2QI3AbLzF zUO>J6u$BA+R-A94voM}`39y3fyls|S!2)k!}E9{ zP^Pk^7WV6>s)+t3=WJ|Mj3VB_>C3$1)6d?T%ZQ$%OehiPauZ1YIYDwlL1Je0%P^X^ z85jI#9X`kxEg5$7cy=?c)#R|UbQCKZwK{(@zB6gV>~nVuo|ZgXxwd8vDKkW69UMVJ zs{?jS@})AH)01X-)fgqC|w!A(M4usxt14zaX1r0$Eahen;ta zO)DUO649~r$HxNa#ZKU-Eo5cr>kel?OVwgVc-%gCc~l|C{JJV6S5WCFiqIaGsBzbm z9+4dYMh=Qz1<7xI=jC+t$zpA0U;0Vl)M>QPGU5fJq2hE8Jc!_LL|yjp+lfHkJroYitZ|4nLN z3OW?d$gGH(z_x8DBkZ&7@YZ}yf}?eR$dU|2I78>F$UO9W$C1u!kEfxNW*hMB!W=(j zh#FG6`q($pN9f}NMBRoHKX}eukz)jo>b0K!X$CBAX5P#w0B}e1FINwaQhlFm zfi?)y(mzZdqjBIt9Ot7~t#vD=8Ld(%yZy8gdw4hax?$`)vpI+*^S0P&eQW6^3>Wlb zOZHt1|7#?4Ipl;ggs}OxS4;T=x!4YHlREa{4hUZYJLxRP(=P!mV=`lpzjw^kt>ir| zS{c)NT+vIXckT+2MkhBaB?q6)YM?_oT4Kfgt3eApupXoBA$XB}KjO%%U=skKZqLlX z*l|>2y88Aw#V{@S&uxMqGB69+C$8%`_ye2crFt2AiB--)9x~NiMc7Fh4&h6oeJJ#1 zcOeeu@hF0bAv6lbe* zQY$Gg@jIc&S&9o^U?oP6|Hhtgt88z(q4!Ee_gve}*N@0YioGX-QT!?_BG7DIC^k!$ z$opn@aHb1oA<$E9`YuRmE!*P^b)$=)az8B~b>1CZ0d*rWY57?tGc7>TwqOXM+t_$D za?t+YBkbm;T_c;R)uF114*a9m?RTn z&0~_g@hV(>x&@%>KPE;rd|kM_z|?x|_18T2J98gkR-YeAsnC99=_lGDDS5*?wjk?< ziRD|lLyx?<)EyfqwzV^fc?TzzNJ*v(4>Zv_jK{Nkg!iDV{CK3i-~L;p4f&p0 z!Nu)7EKGUZff>H{2I1iC3(WXTDeOLcN2fOcM6Aa`<$j~0kaysF0Lm!w?RAgnSbJrA zVMD&BNs+*tY_AeEHG6}Fw%_Z;edYA)4%& zdOKrKn6__KbC9yU3s?%R5N~Q6)h;dD5T5IIU(C+Wx^Z|!)(ZcrFZi<1q;~(=;O)th zp}~!QllM3WMO}po-xzo~c~0hteZ@bYJF8;l?U-7VS|hVo?c!P*$f_oy%ya&}kb8xv z(l@hECoRj#so-LtV+||CV7sJh4DB=4SgL<}-Y6Nxw_`3aT{w&CJ2h|6F}#?y_TN$x zu#a7u9`#pm^lr@n_1(GDGSxLXH5|T0o_}_L6iFSJY=3e{cZ4^GRHT|X8ZVen)TGo3 z($8_;_1;xARH*4YirskJopq!{|889*9z~~~a>L(lr`! zy~O=ZnO&xw9m9~b?mTw^?myOy-u3|-wRpIa?g&{_uQS}TtueOao|zwtlE3n|*z(Ma zYOR@!=95OM@Xkn$CAJeyZ^^ffdqG-UyVe!VG{}nnG}THjYaSQbr=G=)dNwewD8M!I z(`+DU^MnemVd05JtHg>zrf10uzNu%8Z>`kB&XFBSPDtA_OXqY@aJqDR1Y3gm@d#?iy^jzgTC z6S^u~aaZ0L#39xWFVl?j8Wvu5$Sq73>}s5mT-m>tEjte#a%qSd8@uB=aa$7(uDExo zwS3LCn=P;m&>83kah_%@7jQr#q&rJDmYdYOpREObc5TU;&pI{M@G4BAD7=}}`65DN z#%Srb_1STy(S_Z=h6EZR6&$9r+8> z5n;ktDi^?QwyE4crXuEu4K*JTyOYkbu4nd6=xW-KGPA-jvQ zj_yOGDWVGONfAHYovNHZaE_tbP8BrzZW7w{wK^o-oyv<*@tL^bs3E7H64lam%Fg^+ z9IYJQT&pdkm7eO!Y0Cw7^BrO*dGkJT)|m9qjj^aAjvM#7HSQ(LiT@8XK+L}tHVU{e zc6GbPGWi*v>@+OR7N*%zX*%onnixhCwQlRuLjX*=Z2fG*u{6Hf3qXFh_gy&d-8ab8 z`ihQD{LeSTwCmDm!mwsVUP8u6Ru`nME-9xAR$N7x)#WVXBfxT3Vb7)2H^Hc7*JZR{yN=X|@H)#kbo| ziV0_@%Gu<__tExFR@P3?ilEqLC)+anb2ErjFY#8U2P@hM9q9VRbeIXDoeR5GTm4M1 z52gdU**sH8b9bSb?J<_hhjx(lH4`yAns$%E>CEvpTMn~Fw&`<=H9|X^JeF(%=K91G zDC!z{uvH0dYVTra#N@&j$feGf%^u!VY{w4A^3I*}Y{%csTda}oe4^^RXQ@i9h}p7Y zQoY*wvHQm57M|Jkn0;sUU$&%jOBHW_N_He-DJwk4i^T+c3&cF#?jP_ ztwKf$TGm#D@X6DTFn0X`i&)Ltq-N2NM zsc&1%?l(1sZb#Ad$!d}w%fvF8+@i;AMbu5_A-do8-DJnESo^R#s5?8@#muB@v5J8( znK0FXAt-)cP4C&QO?B82jq9!Q#LVcTrhKbSvg}~li8r_2T)?V0_>WZ_M7 zG*eg0683e%sAcq)JLMqBDoFf$HF#?P~@SXOr?OdU+NEckY72&V~P!KE_| zz50Q$6K(d#4!X&mITOCZ_6hdT-Xx(H-62|dP$b7z%VoHjgqWhs=00KdT{um36?uB5 zk^Q`ylC;+_lf~&Ot~hNEY~3a*gWcv#CNar=V@Id-&}g%Cg*1lg7vF`%UfMpGo7lp; zFwfKkm&GuZGUt$B^GR(7Y|HQ)eIM9LQ`M&IOs@TV_M#?h<^iH};^Ay>s~&yyeGYEHE6pqU;! z1IbUUy_eY#Oro8;5)P}01f)*F30o{bcip@`6pzVakJqprwT-t`!3VH!yY!fQz_0Ls z!#ly&PB{DK^q3WQhm`;Uhp(VfTmzWmw0&)Orgnh*zUB(sOW1+Ol=~`kicDwmZf1&V zI6GA#f%(t2KXbh8OgJ#Dx}Xb&**}xSVqWGDUo4xKY@wK`ui?Z`=4YAe!m*k{ zkKxqK1V9SgT+7%2S~xt{n{Ztg_o?him2Msu%6B;E9e;RPIas~L!msip?ChEXG9Sr*Yil7F zen}>VOGI)6d=p2Fz{Q3sFqqXZCK2XEA@!j;T4Nwp6u8)Pi20*u3grL+z|IuOMoTl`^$% z2gNR$2z}ibvA_mT5hvZ8B^L~nE!$iRz=%+4I1t6mvuSSgbxdv|46svR(!x=j!d*6< zf>_ww5=eN6Cau*>MJF_-yM`9H;EB@bK+QE=bDLE~-D1A%V7Y^9TVb}-R>sb>>#0lx zXr{x=96qTplr2T$5EvgBz$$i8Iv$-+}-D5|oM;Jsunog`~C*03~$+p=)izfj#SBC3p;;V^>0Tb^pW^a^mC2hTW8{r4Gv=AbWI<&F zLFPBFJdODuIqbHrkST97w4K<<_ur%q6K~sS61#-|Y>Q%hZzkU&lM<&an`*MH5;jr> z3SdqZI%USa^&?<=Zem;@qRa!!SOHKv>efzG+?H^6<_(BP^{zUX9AtMP zOWOeamPZ6Dv~=D~)!#M~Nn<_8tTiCQsE*AbVy( zh@Ch)fYmpJaB;=pbi0$H$@EY`En%sdjzrrj?PxtO+nItMQR*EGfk6;si_i^Wp}-7TRGwaC7Nq%?r8dNW+7%b z*ah^0rpY*dB`{0-CH}L;JQmXUPcba!XLFE<-rQ>=^0O~jI}m}-%w}Hk-#gyh;tpEH zoknrnMiKf*sMrYf&5EDahA(1gJabOuf%`@h#A7#Yom(h+JJGmBW~qRN9kl}Lv1I|a z#GFTHZ%#)iP$r`Eu*Jl&;zXN&g6a;x!_L;sUYM+`;fvUkWL!Iwg4vx)5G7ooinBK*Ju3BY{7MyShZU zz~KeBCo+s30s~^7R+<&{02<2#5cUzf zC+HpAG*>_h3qvCeBEBkU0W%Uc99~m?XmE>mF*9?-DWLYYKz7l|cfwSFAk+m%b_81H zxtJa^&6tLPA!4&*Rt0a@5gM3v5I@0#`J{iUP@NRV0$| zbpk3f8=45pEl#zycPnA*WZMN;ggfpC^}_oQpPF6q&m`3r%>sd&T(z)kl_UTdS43pU z+DaJJO=0wiAMI!Msl~f%xb|jJOad*COXj03q1qPWUoap%2bV9C=|!lv8Q7M?`ttxQ z5zriP{{$d_ufw}XDAI(KfgqV(@)$#E*bYPjSDgGa$A!VKY-e)~Aj2G5!aWmLlTkll zGn?1}0aw!}fkv6LVfvl0$nN651%|gPu2Ope*=2nAr8VVtW_Mvr@?|vtbPAZENKs@E z#lXOF6C#AGR!f{YJ8KxWLcC(j;>I5c%+v-L>Ksc z-xh}V@H;-(T^$QQOQYfp0??tvMr37k9u zGr8)kK-UGt6>PuWpDla~TNEH=w=cGP7N-%yFH;)){ zr$!`3_>kNdgEp;z+SFo{7TW;QVH(Zs`aA-YNEUn8(ZFPXtE=z<#JPcqE5Is>Yi*FFqeMJ^j2);(x%zNFa}^C>`XB3{Fb zF?WaPB!XfyHh9~%dZ?zzV%Z+3U48khw_YpY>?$2_eOg@Hnn*|KFPG3n~6lE$)S zCM!CiCIAalGy=WdT5&Kj607vCp9`Ye&M{169^+tSKyh1?gs%`zCEhfCJTazcBS5jb zhr@M1Z?{=nKX&od4P1oyIJa2xYF+^00HV%x$3D`ODZ z!xiVW;;Jvz9WOg1cJiRe%T@5*Cg$sSN!=yP*329vloqS(W_Uuo%%l{7*5^1Bgp$8KTexZ)CC9HLuDjo7)a5lZvliIf*GRbWGd0Lp&pjlVD{sVF0lWljF;~cYsZPil$}rYe~ndVdF)7- z1X~#75d=03NC@w)`J>pGK+ynif%Zh)m%nLV$QGGob4j;Zs4d&O7n zyYUKS-oYHC;)a!5v%jjHEc^z1V$d0OZ+;O+tB+BTSCw+FU;4A+OiAGofQdhqj*<>Ymu+{tmS0x-a5*iRE(7m^kK z%_I)39tcqZkTSh)huZXpyO&8IN+U0VQE&%?jJtiS6Kw`EZc>wlrC3larw$ozoWVry zxtHP97eZiwLLpE3UL`?1{oXZi1L#%SVhdPOKdX zv)1;mCXXX|xRS{BWIBSfw4Qko~4iOE9t zDiD-ep8|U4Cb|M_X2NkZAT1IVw0E5Wnn6b02}V%ZbddG{)h!Og(s?v}}Tl-}IR_u0T9|rfv)}$yu-w7=v5W@&0!K}j@smPJJ(ggTDuyV4xc2|I! z5!Kn*o>3M8V*LaZFctGO2>WaEIMj}StwbiDn^K_8*bdY0EONLkRn4tek^-PUf$e9I zJM({MfFpMT3mR}`>!&*0B2YSW)`2&%mj0O;&k56GHYB(VB=*b%f=CQ|O74@4jJd() zh5DnIIM7^~EZdTXlql)k$Q-2Mj3}=Jo6CGxg6zmh0u$NC3BO3JGLuzg8+~jl-xUq^ z9@-ZAgCl(pr{U(Z8`;#r!lt^xutx*qS;>|4V2D)lIy#AxKw1lEryVZHNpN$x9Iv}H zJ*!X`J_Cp42DV@py4K>MFfGEtIe_lspqijDq6*j26tBB9I20stVS&Nfh@=jh)oyLe zpBzpDg{c})wZ!!V;}NV^qUL+up8+E#V7vqYN8htY!@`?Dn(SBcUlzp{W=!F2h!X^Y zW2JNz4u(*Vi zWID#h%ii3=l3DIej%SfOVD>)YrY+e|iJcoeIOf(tsmYO3q^&}SMwMo#57`T2cunit zSq=_>5JI4M)AuQ&f1&GeXlz~cbf58^fzHwG$sZXl1V&EUAH@^tZADEBKu_^#AE(Kp z6*D^k5$<@D1$#H6)8LRez=9xlTl$q8#q8irB`wz3NOV$o@g}$C-Alt%eXsFsjo5G@ zHR7ub3Ri)9r$D+aBAmp3gA^0qnRtHCQD#&^u(rr`K)nP|6`GPrH8&V=gn0JMt`Xr~ z+ID9_$sV>&z}vVmrlwMasD*%t93Z@~oFXM+8Xn65X(nRyf(M(b6M@nc_psy6K{a_V z3eh-2gl71AqJ9SF#SGd+^21`<#a9Zl&?4RfY5jm+I0#~S0-mSIR~b%d$Mn`E;2uz2 z#S&(a%!6<_TXA@1dR=1<7u% z%Q!#~w#*aH@Nv;<{$fA^=1mZbBo9%b(m`TE+YS1ilqHPJtgKr0r(+xmsR4j^e`32o zYg|C7QVk^nanwE_*}6o2hVZruqbH6EzNm!+Ft!Rfb6&$!Fp)K9j3Z_Khewd6+B4#B zfn`~9;EW68F@^pq&?j%Z+`X29E2LvTs6&0+|8FhEpf+ek9Ikmz%{K;Odg;3E%mEJn9O z(PSdch6X;+f%aiY3S0FkFJW|{Z~*;=iksn#*cCdg#^3|5jDRo=IfYY>kxb^rk#J~a z`j$AQ7Wx&rCP9eHM7d_W?R4B@Rgk~z6q}G&@z(v-Nb!(*CDIGb1WMF22_p=oY84u} z!enamK*Hru+S%?+gcGbt6>&0&g%D@d4iY~D>1KOf@;LDoG~8q~91)}oqygPi25x9( zc3H)SbAzk%Xr~3e1gH}dzrrM*fk^HAxIaf`G=aeqh_)ZWKCZ;TyEm+;5hyJ)q z4YrZH1yRmG4eZn8af*2g1yY9@JCID*48STR9c-xwfTv>vq|-27e!Sth;)E~ox%@bq zR?Ha0jMWLhzlejfYBA^7|BClE!!Ep+Y@lj$3>Lhs$X_m55Rw_^6p5f2AOOUbKq#W{ zM3PjbXi}whoLMl*0<}l9?199)Lg`!w$m551Upm2O*f~q2J+Vwc|n_;OJt6fX5pIv`HmXH{FLeH z?h4swX7tSPx^Etb-5e&|Yupfw`4ZL_rW&MW82f&pNLSlB*{)&J^)-Ungck*u)XO!{ z*tm>#S|D9S25Ls(SPVyC)~8To2%^b3=zI(N2LBi&A0F-!c^QyalWVl-;!=l)mN3Fq z)+kl*m!(V2`x6q$CNaK5`hi zt!!{YWTWVW%q*227F0NSpaX@YVf!K+(jyjZ>Vk@y!9@KOleWqLLW5wLIL^?1I$=x{ zCnU%i@;{V0^k^)Jf<7z$b1}xbA(2e}aa~)S6j)Um;19lI4+kFJx#o)SD@HTP1*U-jfu*^eNK z5ewHya@p+LztlEMY&TV-LcD)=!m z3pY~5Ht^ZdJjLT}^9!2F5lp(n$D2X9DbJaRAO}QXVkiRW9K}wFLIENH(=Y?8%AVe) z7G=2Gq)fnhGDvoCg3a*3(z3%>ND{l1CEG%x5CALSA$$G_M1mM`N|CHIm-D(~H@20ISs4uOeNyBEkVs+BQuDlPz|EfJ^+ zsMCF8l@NkTIN{@$KvR+ShA*&!TE{(Lo_A$!lW!wa#&~tx8mO^;J-f0Qy-Hn40HD-! zm}sG-&Aaj_8$TZIALiC%qeN&%`r8~+tbZH}ThUVFj@|2HN;60)+RguSP{oESY$gWuq4Y;hG%X2WyhzIJqX9r45=Apl7hC})5fPEA#UFed!LBl zOpl9D8K^#05^Ozy!5r*5B3D=}(EI10R{%^?7Dy0piXh^MB)o#R?Y_cLB*ce+As9s= zGxQhuKhT!CuQ~+^6A3bSC?zyB|AWY4s~>p{!ADTeiwx*IJV+9vq^#!>oS>x(ps-+N ziGaqAt0_T!s8nVE_TU7Q%HG25CzSwo1{tReGO`mz!7kT}olVFBSZEc)*#R~!C?t9l z`ZDS#?CJ2?a3Uu6159hku`P?RV=u>?$q7Tc=wPlMAi-iXrA+zxD}#&kARh;oA5({K-f++B0SWfRb*1A_D(hU`-N&N z0!4h};^wfDKqJKo8dL^1WENS({20*sIx;~FYvWrb@F9~N(G9@1%{@!ZhS&uD&e^R{fu`2)oeA41i0D0v z3qjT>elghqu$w}Th{docBA^D0M9c-9t}|iLbzc|R-EOh2ao~WSklY?~%dnTauLfXw8afn5@Pj+}(RBKhL z1Sm|iWgci46dC|fY&07kF?t6ghI0e?z-`k)o(QTI=)K?90+zlC>Rw}2G9HLBcFTyf zsV_7eJbQHH=e&`RB)dwEGgLnzg1IG4obia-?J@{JLmGn|RN<*)p^#-1ItrQvJ_Sjr z_cVuv*u3M%w%~#nGRR5kR<~dzA`&Fu3|{Hk#ZLO!o-`=~5ZPezKE*lCJ%GO%`)lqG z*IlF5(;i}y&VVesL;#GJ;GCSm1vJJGa<_?X3(7bq1Ly(}+?MpdN`~YMH4HV_07?qL znGv@tDkpILJBcOUItQl_SB%5c8!ou$I56QOiA5lU#>6Bj4Z>dW zVL@g_X|~G9%ts`l@0lC&poHCoY}I!bJ2g|9W{*tH%D2as4HOB4G0!6^h8GPwGR1Do z--K-pn{FCeW1K6h!m{Yt8b^<1N6}z{O_W-UZ&(3CyVrg0#kjjI77u&V~OOuaErOFsC6NORsH2#@+U6kyaTv19qYW|?ztY8Sm1Llc(VkIMN`GR^# zECu(Dq}Cb$9Ap7X6${k;7J1AH`485G(g=MNPrw4I;I}oXoz%ZmeynHxK2OVo}v6m-I|ZouaIDGa38|co&Fk8if}`1NbVd*CzlBfSk+} z+7yqelX8K0Wh>N}!CI{Z4SI=awn43U?ke#$cgq!EA&5f1(sIRvw;4wWEl!FqI>^l4 zJ*R>D;DPx9%n)4ts0XPKfO6s8pwWf+HxLXn*jO#|HU@7MfPnyTs!=jdHJMV|q!`0z zw%~Kd(9qE+qPIm?>>< zg~Yyk4XQagP)carck?o`0wih5ERHKxmECF4HJx;8|&RLH5J6ZwP#b z3hVh1KC zmNA_R&@DlETh&8ghQ%LI7~NsAN}RzVqYM<}g*~H+G#H38Oj(^|iUm{fJ}F+NAi_>o z_H+fM>(qQV&TQmdr~oGbGvA=CA>ZNy=yB0s!AhM~2LOKxE^HK$K z!~djvS`+_yI;aFCy=x{e5!^weilP!qH>pGz{Zn8ff#CtRk1tUHsSa2q)}r_u^9!;= ztz%@%pv|E6@P?uQg7R9@-*)!tE)`|iS*>pBX3_9K!ZPk6)u_aR0X2w)iyEusXhPP^VW&9|jUU#L zqJh1lk`?c6pl%+v>t;160S)+gQjr1eQLx*Cwg&XXW02wVgI`Ga(LRa-sU=v@^DJ5c zoU#acadZ|B+(_a^g%)W+nj@h4g1|w7YbHEfh>84>L32!rlGa?LBl(P84jDrdXN__~ z__?;(1Y#}HnE;n$qg!YoyEx7cm^_`Jg4havOy+=IWx?#k4x?+S@F*pc(tvcCOt4s_ zQGQe@f=2Rf32OAMFzr7{>P_!10rAmQ#Uvb5)Om`HWO;)LLfWzPC_jQ#4;B_h7Ge%m zm=)q~jlee9T2B&v&Ar+$f*0gA{-n?N3N=!JaGLR0DDV{A`$? z^bHI~l!~^fAWn6l<`#=XGAH0!qA82|pa6VR<-ClpKgqERwKa(rda(UCR8#1EBTO>!^p+*WsVx%o zD*ZY|ECqqoi_(y@Xk|qK^sF+1Q0F*c1ZqeG5RN;itEeZLiufr|C1ENF&!Mj26N35? z5|fKtqe%-e@T7(Xhg^0WjMxr>*DgaAoL6&!`V&_fnl(nXbe zr&)p7{X9A`P+sTF!$=53-8yylO$EhMfC+^vuBvL;;vMvonum~I!{wNR5>Kv}Pz6%l zXwQ*R2Tr(GC#66~IhoxFF+ookDo))AR}H9xQIHMrc$dKMFe&d>?_jK}np?qXiI}?*gep@Abs;>96_fx>kv>#&uo-Ai2TujE zN8kqbJ4z8V!M?+wxDIG}6FrJ7qV)_Ey@iUA67pY_0LH6=o90J{=K?&%4_#X@M>FLR zs^*~uM3fYmcrDJrUPt#;HV=3LerkvI(t+*>hc8ww4^Vl9gCFI z26T{3!~kBE%MYxFP)BVVy9#2X9Z$mjpv%Q*0syIi&>vVkNUV)ABs5#5xUofHAOt^7 z)J$u(LEWI~2+p1gLBoTh4T3Gma!;g!13|`r5us;|7=~JLSkYkk(Y=iF%N1OMa`<4l z<8u+RcKTttMkN3QKZL*#6(m%Zp`;-`4YD|n7e%GKYl*5D8Yn1h=BQQ6VSQc$uSO@*-YFkH-~4zNHBTh1ihq`(C*{gdKJ z>M&vf$lnlql1GZVAhJ|Nt%2!)6+LA-^jDLm`&^Z{5i|X!`BjdNzuKDQqacH$|s2?PIas4?Py0caa?KVtHxDJ}CJ@d$`WdqGqV^Npv$nAQe6Wl0-2J*jW*Ojo!JV;Zaf@AP~k} zqpU2!sih32VKZE#64>A_k~%Su*1Y6`^j zrob}!M|iQ~Akq?dx=*YDbg9c!iR7;w?JQb@cMDV554t%~95My9zp8Ky*B0BeM58(! z5~59V_>sS*=>v@mY(b+{Io=&?JDP!(JUWd5eF$c(b?c-{CU~W3Pzx?K63la89r`$S zW+14RrR`+k(WGSI#;sXYvOJRdG+%*$RfAyQ^mAKKjONJO0K3vN*|wKT>S$C)e~7Af zk~L_ubO%WCg-C_0_xT+N+!a;f*#fl&qL&-EY?L?x;hHHDWxJQbhRRMQE-If3Py|*P zbss3bz@h-?Y14!26B-&8Z=qUNKTX04%iNsu&twl{MmOl`QYy4Iwug@T32Zq%9SPX_$Rbfw zK_0f!t#ArDX`z-p2~~^sp@ha%@BuOew&2~hVF}n0WB|~gB6`na&2oU~Md_d6 ziBXRW+>n!3fi$Z8aY9L6LtQu0TMIZ0jqvOMLlg4ob|HWO-l*Q>IjOa(Vu!{Af;IMzh}s{rNBuadV~Dv;ys@Zx z&uSJ)Fe=2zSDEq~b%LWiNg2ftbk3`kmqfcY`&I=B2Ek0hZ=eY}fW@M;RSDHpl#4Vk zL?Bw^PsP81;yO*idB}P<979#TM~@SXE4ViL!&5X(nau@CbqZ{R!b-~Yyv;=3~uI_ji29H#$iPLd+P z>mXc7%?#*Q7Aan=n(&<|*zc4n0dRJDU#0u9$JNuQ6siY^itGWx(&j0GJ~OB^GDPF& z9QC{4B9J4h7`H=V<8*<9v~^5QfR>bD1TACu1$8AVxAcZG(*SRv2of*UQFukJkYZ5E z^pwX*4;4&K9H7;!GZOv(8{14*(d5wOtX8@O9t?F~Q8Y{ucOzd+n?=v>PogcT*^5)U zX_ZBJ>m-{nh~(-+P9K@({a5)Oy{VJRCW>fNDs-SXyBY_SK{%(@948z9J9+gk&33bz zf{@|MQOzn+BqfPp`y~Q=vPRk4rTgnkjOY&mcPQ#RZEc9WfGU?I>ey(XwkjX3G~Ip0 z3MpcmtIdI;lUIi)T$DBUN9J88ax9{?`|ILkGkAkO{;J~2bo6Abf&6p^!}hJ8%`i``Jki1!<*F8(c5@< zDLx%SnkwCU4A1HM1@xps6dnys35C~aaUL#!33OLu2N6yqwanZZctJgy*knD{xh0nfg-GG$mO@7)(oDDg8gU;L+b<)^lFc)1<3KJ)R^z$ zpQ$%h5}~5Cs6WFsTopDAnzoiP!BC~p(}lDN2V-=NG9pvJ$&HVch>vPJQtN6v7j0$9sHiMHx_d$+ z6KF`E_<1a!N=0zedUL6QWL!ijgDM|AIOv8+8VcVo>TRhRpOl!!_4R^FNdES^>>e?2 zbVeZHLB#@K_>u~8R8d}63(e7|m1!#q@PmL#eu2Vl9P1LAxuf`IwFZZhkOS|eeJ%ai zDAFDwmnOfU`l+ZI?a>*TL0{pu%n{Zx1jjB#ePs<)od|?>|FF;lWuUH*2nR3TJFqIz z*wpvc#E4=0 z+{*cgK}TI!%A%}y4sr^)LFi~uHlmzZqi&#uEp-2iNQ>AJ>TZEW5)*<{V2jtuOs+t@ zS23y>6tzTzLaYwX1CKOCsbg+F&Hx5*a^BE_NsQIOiCfKiz6G3Wsy z=gr|_cn?swATo{i%CO7P;OaLY$$L&09Y5rf#j(hkAJJYX4?U84YmwfY=>FRLz665g zy~v3Z7&uDTJZVCzthn+4AVFLzqBM_5X-Szywjks&`UAnSW(}fQ0-g!;gK+ug`$>7Y zN^~b|kMrq|xQe=|Fu|qf~CBgknQD>)GTp;}T8kBT8o3+`E$|BMPamH3ju&8B2(|nR> z*F-y#iK@wV-vJ)PA?auU&qN6nzT31alwV_tOI8I1tS-%)1?S? zX!krATELZ)yxMsQP-dYy3Gg)(sr%7cRF~1uesLL_MAJN((U>w* zC97CfnA)RHD4hqY51Gm%{~}wflC05|W`!aR&m*aMYvc^53R}A-qvS|8BPnLIF|-Qj7c^uN$~kyRi4KTKG5vZ#TZ5LuNEGm|6qS03+7qe| z7Fwl6uSq1Js9pLL>|rv2&<9p0CVFf@4@a{4%=KC#-Ud4Z_aKe41|tm)597O<2pLhQ zOFw7YD9-pQIiL!;5>SOd-f<{WP zKo8$2mxZ529#-B(kH%-%M3~*Q@hoA(yLji&VZxfKX$shacUGK7K{K6rDtyS8&45WK zjOT2Y==x4_mEQ5{jkrvsT>~xX8LCm2uwj+9TTGNP*9+FLKQqa9gn8-Rmx}3&T|LSf zauE&yPC8i^GX9YTr{Ql_4rldVz=;H{TAC6L%{}Obi%9DL`4CH^`wQsbs)BHwnQ%pz zLYHZoPRW)75maWPOvxV%Z~~^lqP{q3b-^IT(M1l-!a<`Ip^blZJoT_iaXP-5; zhPDQ4jF^KRNxqMBm7~2CbAz*v7=>AZCy`Tw9}6Qn2qTb7)teSFq#G;^tOKc^!fvYe zgywS^Ar}WK%QGP!rejbIE0g8-Fc9X~RlqC}b5;o7_ zpUyrDgSsc&&WQ1GfH5Tik5K{bMJnR7#qb(d77&|MWF38T(YcBsqyZK<=*0>ewFrg4~=AqW@gM}gy!x}hPv)ADng(NxyOJ4^$)cpMTtm@w&;yH zPP1Axu0b(`OvSo0Y%j!R)_5Cr!7MR8q0$f<5DJw%X3FR-MD?iAVNiWXiA)#NNR0w? znoQHshd3`%vg!*)5{8bRF)x9}O$?M`r&@G*BgUX-0{zS;k2LsCyeTNQw0{Z&49?~( z3}y<38HIF3Di=ywj*b%!NisIKjQ&V;9EM{+Kpl`3!GdbdvDzd3$P68lN1XBKjRKsE z-B2Tir~`&hrj!+mK8}Lsk;1fR_-EvPASlC40Mm@N5s(YJ0KHz-OptBlHE@SX{Cod0D5yL@5x}O&rgBE^Q zS8J_Rog%uJ;l6w%F}+dLG!mIj6;WO7B}d!;MPm@ui(qv10z=2iUl=JKRb^yK#mc2R zb`HuTNley7pOoj>N4f}hRkak$=7fT!M)Uh zm##=|)CfxG)6BHvs3G(|&d{!A%|ndFFDm17$gYhVI$Abh7u251|ApiBXLF4?jnN*TtU^!A_nQ)2mf05q#Zj*?!f{StYfDg6e9dia8Am?%;G^& zYTk~9S>L43$wrO&A{Bs$h#GJ;%JewhjP^0$?Omyf=rTw!oxLe{1*#u(1~At`Wp1$Cp`U=*z>sWl z9AU51bw};U)-iHnRoNcW(nea;3%qG!PcfhyA%F=qF0RJnhP`R*Wu^|8$UP=|bAFh# zOtx^$NueBsjYNmUS~bn`gI~%=ACq|58z>~_x8L|`LyXs?j80L5e9b7b{;*D$O-~!v3R!g0! zM|&Ga_7Q-Zb|AeONHgbxS=y9CL_Hdsf-DxymS`$NZ!NiEV!sTWfH4G#0)Qoss4W8s z3H*?`t)s+@8or|Dt-XbJtCU4n6QF6dnWI@B#jz9x)ksM+76>K@rL8f-5saP`O40Y!e?5}&Gzb`C zp&~g=w_#4Nn|^>dB0KV+(HamJ7M!dy^(z8bs^kF_Nk9jL484U`6|@|lS~OFjTgs&F zeN!E?GmG#pnq}mgM?ZU!^$9mQeeSSLj6h~IrUrkzS^-B8ESZ;FV+r7m&UoZ&8%_8H z3ox{bDX`9JRS8?6<`LZ*j5N3n5Sy4867tB584cjVb_V4fiXxs)0}Aw$0VO3V8idg^ zMIRIL+FTPs9|O2%4X@;qF?dvcTCpLte~x)%M5&8{E2PEiAK;QRdu&OAw}6qQ6GlLOOGp!K3cT%=`h$R#imGGcTIm>jwt5 z4bU`YE>WGV5l2l!RM(gg%>n3(X1n$%&&G+0DQG^d8Y{Lkf2!Em1ejeDAK2(W4b$RsxD}X`Ul}OwPpOaUy{r zHp67e-ugF3B`tajyahTbJiXDmpVCR1sH0B<(g2mOg4XMBhhSkPxNFO6Uje1$On%amyhM}8Yq-R~B)+Xp`_DEf+_%eF1EizDi zSL2|^pbd(L*wES`oW5@-j=zh+B4V7T(Kwp+QF28!ODYDq z;?sjTij5wqfhHcB*(zlWy`g+_BA;G|F96~}6X`__SI40(s=`3y2W6d-c9K_MAvw}v zlwE1OrK(JW0MA@m+JS)(_jGvm=(MKyRRg%%kk1h5=5kx%P1A z>*zg1Rv+h_KF?8#!hk6db$%W3)TF`PfWqW-qSDx87#NnIzC_Pq`tShQiINzEZISY* zAQRE8juJ&Rl!31_hlbL)B`8c~%pXUy@$M7{1^)0kb$WL= zxu_m;G*@MgB>vhoNNpfDC_9DZgEmKMns5u15apuK-e5Yy0S|}>kzht1?FE-;=ER6n zl73ahyix8$xul75QX{*VQ?zL0-9(?m+&B%K zWO!iXQM&WZ?MZ0eH#(N9T!P3moB`6KStaDk1&lej5KFo$(?iKaq}SLSeB~P8KHZs; z3R6)U(NdD^5XlUnqm4tnC@)B}^T<{)w~v~3hAYI(a1JqSe5MyIqqI~5((JMGI=VO} zG0sh%)hyPvHm*g3$4EdD!QQgQ_%lYBMgawEKNR3HKB7`^nH5d+9$n|MCY9jnY5YqJ z2x2(1SjIIHQy@%`PM~DX;~i)~mC2LibJd)k!?x^z=iS8wO+|URH z1dfVMTg1?+^-Gj^?J++-Sxb@nGou zMIHT$ruydM`i)Lr%r}SPHnL!}xTPc)=(I$cqPdx9C-W7E74L_Ebrl&gc_0cLfzU9Z zu#97*b0(36AtjUu>h;vcm!U;cOi5L3B#t%jGLL4g_;?*MU~U0WsqNXUG4f*o8I#E| zNQ^%SzY=OFGkg-`W5=kBSuA!&W%TG1FPkRkNF&x`m^c#yRSS&o8-k5DLgN-Z_3*5x zq+I`8)E*%oMZM!G-BdHy(OHaAGv_-_VWX(;OQFDc?N#M@n4=WNxG<%aLP1D7;S|FE zX)0EpLmmjb=nO&T1#m0UFp~^EsgfF4E)92RFkDCoM46?^^C%%8{Tdw9q8`!eq!_^w zJ|eUSlu(V_GXNu60*E2UafQwb@si$sO*pHDE5ak0RX-8K#&D>z2Jn?SMzAG_VyZeo zZ{%ocZg%IL#Ok+Sq-NE4!37qv8?N;i6$aS&lM^vK~8*8rxGTIyra6cNGVY|^NR zg+>SMajkIxHPxJXyqa|sB~CO*LEAEww6{TGAg+WWjVB!BGEGLGG%Y^doE6sWB&uhN zc7JNC4vz%fbrg2f`(zeT|9&7Bd5KxwEU*UAp|Pn&iAJQZ%jla#TNC(-ZL~ustVHa| zD#WxvyE*m7NkCfE&QmW8C7J9|#4EE5KGPS3k_PgCcSPSAdLi`D1jsZL!}y>MjKcsg zN$7Y=qa!OLnJHqV_i<=%qGL>f(=KC5anpd(#kIj2dKp_q1BNzyE43R-n)J{{$IpqR zu_@E6)^nlxaX7nbtTp#h>V>i=rdgooy(3t~JTfpt9)O!~NRXY5-mup~eN$9N28Z^e zElBx9PB89hhXnh_wHmgDAWL9S3}#>eBO_8ESgzh)7&S=xE32VewC1K!j^>0fu>y4R zqlyV0PldIs2Z<&n&=nb(pr$#a zy$y^Fih$N>^fjZuD0yhwkw!HdTzpOPVpME|yEG)WU(rI3u1w+RQB71u42iCAlaFQx zDjKJZ5p_%}STxEljs?>)G`wUUo)1VV8U`pQ2ZRO*Y!T>C)95Pb17b3C-CW1?+6K-6 z%(JF3eQwpYltH|23xIsd5Rp|3ua>C?jKm;y881e2ScvRdU4Oz$2BU-L%FM}VZ#rux zAw%nN`tztSZ5oGIJzTP?B3vLHJH(#~Q(F}H_ek5K*~Ju29O?H7JM z{n0wKxiybyg5LC5AQvXY&$4DE@pSrV<6NKAUMP=Cby;-E)FA&d|$1i zNe;NRQjExy1>3lTg3u;QhARgNH-2O%O-B1j0^*D@f`iSUSW>Nj(!#WWHkfiJIC=AA=e_HW_xx0Btara1RnS{Z#c8 zz_VZ_(;zC-Zk8gkP=4?X;hWO8IGS24s_KBiBN}SZ8{yVxANU<{+LCn$Vbe{~`+=x~ zbOt_0O7|ER-5YUB`xq*&#+MBglS3YyX~?b>zj2D@kBk-r9-$La8sQ89w0Gj6$yl`K zq^FI1NHc0F3QU2s=nqbTP;^5ibq+S^6%k|TN!OgK6>4mBe<-2`gZoog6ol5SdP)4b zlBZw?tN%C@jpd${yEJ7L$eQ&QbqS!R-r;Q?t(PapOEPtk=qT7mVy@E4bkSt5mD?Gh!` zMO~HP-K}c;<{B2S(x-h56}D)k`b4!qZbpnLW*F|IELsi!5}Z%7NXSVB-b=?#s5+ex zZW{B}!PcZ+mod4zN@#Hnu_|>;2+EWdVK}3xT!Y!+VACkYX#x}sV23n@HCh-jG#A!z zf&qQcR)nXCbzhITBfuALJyqX?O{mwZxt^s0VGZ%_@XWz}*s7K$$pxPaFIZ6R26us{WQ^ zg{eGCfS-fDS2gyG@Wd0m>K{)xlSC$h9t)#-4jMQ;SC^=~n}k$T;Y{0|nD|a)SiRU*dMZ+GB{F4{gEvOdp;DP(AHjd}G6IFBk^R9}1#O~D)Let@O%p@< z@visjEs|+C%5ITj_9HkINhTt?XwpgEkRS|CWQw{wh#CN_xTf?yj__nvYNOCHX^tlA za;4Q~FtV>n^aWCNbX0NdoTQH$U>70CF$x0y5*nK69QV_;)oOw=$|YE%g^DmDdTO0R zo8V#N90+?Jd~&#tUs^zyjoN0E%Xr!Utm)N=_PD+vxp%KhQ zwLY9hvgi=YO9jNI&HQ+|I3?b1)kFDS&5V-=E%-Zf?gwb}7G1gS1MyV(vQbMFD zLdX#mYKR5wKBOteEK~8F=_xpPN!3T~&`yoH=j?P}I z`WLHz?>Y@AspC>0+kjTVrUTKVu7M=?I1@PQ2r_|oblt)&BHm;yLqNJ&9gD<+(8%n_ zY{Bu+gpL+1r6m2}jR3f_L-R1{&@@dYC@GS{MDTL-MU@&cD|IVX5YDnS!lfw#o_HVg zw2%rM)QMEqZ~=e{5=rS4qoyk`743;Yrj{|rRx_O_p)6x~7OJjNysZZck~|zCAO1e5 zI;%JzN;c@H4^NdNiGKkn{nIs}j z)Pz#tMYPg2tuDY9`T>dj0%{+_(uhSR-FXz1O!5e61BPHkn-)c0G#i63=qCff-p-Ye z7mSUhJ3E91^)I}qX5qgi+_+#W&`tucm`q$W6e;RX0cZg-7&G!9|C1J^=Svv8iSYm~ z8D;pl8WSY$01cQTwgPUYLM4-A3v9?FVxd8@D<#3s=`1UuH%+p4EQ{U$_Luc$nY4n$e?Nu*ZHgV)#3Dm^@!aSSU#=?&Kbyk%-Ka*z>#F7du$J(GQbsi zoY7u2)7T0|A}CbA!c`>-%%;!k1Qp%F$S{)|qRUqhO`#o6YRXcg^CjxJYa)=URie`| znJJpc%naq5 zPatR&7DAMGO=?a)sd;}N4VOsI0l?7dVDP5Es5HX^uVxvj7c(R7$)b{4A@=Yt(-f0$ z7-XvoQo4Ehl7MV_%`Rys&jEJH(n1f1who7m3K|D*Tg(!oyp|$Fz^gf0IL{)^Qa#UL zu`yMfuFM)+8Q3@tb5)4~L>%cN(ZF%&q0^+%n7dMep(fO!q9!TyXzZnXFVG@!Wz z^tMo5Cu-P$QRe%BFPb)i+Zh!L)E@KSbmD1IEi*%? zB4#I_wvKjr^q1A4BNH$Hn&u6INL`KvST&%)d3A2T7&7HedZ%<>hQDgcqdfHz^%RW8 zqI-P??}?d2{3~t;mFZDvGh@>sTcZ<$Z%lz34x50y9ytOM;L-=KC%aRAVG2(mJ*!KozA4e*bLGS#Rb63B&{hYpBFh){CX>#VAAqBBPT zlP0y_o?Le5ovjwo8Z8h^;GzMM=5)9lt4G0DX*h3&;o9$~T7$+=o*SvMMJ|$Rf>R8( zryi2z9GF3wf^pBtno3zT$|X85fpw^%vKi;#&)FWTcTpw(P@-%AuKwg@%gM)*NbYDr zKs{R>oqN?YhT^yXkE^@cnWbs2GJGnQMYjT*ME@0uoXACbaFrj&SYbm)U^!@oL)P8k3xe$i%V!+Zc7wiT~A1P>Gk^Uw=0grCUHAvll;L3KY*AMceeE3sjFv zzYKtvFUE<4TMWVV{5-d{BS};Dq4MUv?=daLNfD3#v+^X#vu)>I^s1wPhWDp@e7;BG z2s{ENpP#EYL`d5_Z@Ix#i#&UXG;CD#*k}+ zJ-heW=YoGexW6kAdoQGCzY;Y(LeY6W+o$d}nf`E1%v zq+9&PZU``m)cAZAHd0s8HlnRKpDNGp6r8tv#Quv8ytmUydP4S>XJ=lrP`e8Ch= zL`GlMlg^qJ`=k0}i?f?p5LZl0?~Oq#Ppz=L?qK()u>bYS-n&qYQ;D%PWqw%%2YD*h zwDNWP@HJZGdJNb3`hKP;Z#FpGcWzE%Olr6#)9%$b+ZG2ui?+b|@)JV$$IU;c*Z8o6 z0iTfVX&Eo?C{z@(0LQ}d%eJPNTBoG{zMKF@r`32RZ0?stYBv#2x&Fc$9rm7hXoVsi#%s8 znqEXAnx@qOJ!kN-i4Z+_4+J6!U?-T6 z%inRm00|@4k+5V9oL7`N67a6+c)mxJ*1I_<)Xn;);t)7a&*gP|aS}@;janJY`J))< z3(slx!)(4ITm{1#9P9Cx8blGR!BaRd+gmk9uclZD6Z-A?xrj~i2)q@J>EscsMtoqu zqzqbk>LlQ-w@*ck2d>DD_(q$sa!)jow32VTByjZ!!9RE;RiuNneQcCWhD>r;>ndxQ|^IJsd9}U5E_XhJmX)OJh_kH~I zp4z$L93wXT#K}Z+#y8=7yyyS`sCWg>@zaTI!E4Q7X^yukH6kw`I)L~DKf@C-35CgP z-gTxXTg#cetUnR#fP+lEIbIwq6=E`0#*g*@d$vB*+DI>+MFxK_y345X71O5pjU20{JU!?Y41%* zL694PM5AZO8G)R_C)q4-&(?6yD0O=1paJwG9{QRiF#a*$;}g#aWcwwxfPKc+k0S9R zpbeNft(aq$>oeI|Mz*@V6;HQa@ut34)_m+`+mQas4Gccz_;+AKd?$vb&KBeZMKw7mgv5zPcG zqV{+T3ZMnkz-``MtNh{TQPAp0oxe$~j>#;|#WeVB_aeybOkPgtU)4!%<>9>cUofn( zx$HrKy)V%<3RSCM{2h&hth!u~TlZanb07(J_Y8M$lj0hYWB zb*tFHl#}Op!wUpl?Tik`%Nch?Onfy_>#HQ85rS(5X!=o9)&$m15edJXHnYA>A519f ztGWb4_wbXuyko09U;PuYIZfNoGIt2O2p{zq0~-8FeCKegTyx_3RNg?-!4%l|zHJ#c*>yUlguQEb<7$`sbX_jX3Dz>7A%JRMrI49YOoO zJpjqixF9a~^Bd|kv71B9F2|2mGy!+WZUSZdk!qBVva- z3>*~Z;x~UGDTfx1Z)al8f9>?#hO2}+4XyKMyO^PTABp%oek9$Wf>C|}10K!0D!I^! z$18PIqqvx2B0YGEuuEjxJRtJ7O9w1#e!ei+zE@@PWKJz4PtM;d6bnAcm02RIp6_xAP+ z5maXS&j-fv5?+9HK?2q^dtcupv(y^hzz^E5n<+3u#MAZb?&RP|!TJuSJaBY@1L)=* ztBP!0xM4UVuU{TI@}6km^T(Sc9|!srWbWwv`jFi&Ln-~$GA?zi! z#d{zphFZHrwB_6S-$TiWhWvQk{6dY{ zwWIY(Lgg!_(T6dqclUZ#hFeC};JB*2cOXC|vXSI+w!BQ1GoSOr82pjpoZ70tu*l!? z2oR1cenppk*R2b^;U?AFD<)z?o#m3n2J~)kYbGVLCe#~x%7J;n^0?!CJ$fU zz1Q+Wl35Y=#QHvy)YpR%%#m{bdVPoRkXHtK)J@k97z}QE31B;3M!}x%F|qc#`PU1G zVRp^JeTUDv+C4D9^Oxrus}yI2G=uTWij#2bMr&h#x3l%x-M56@zwSoqy^>=9^It97 znQXVG_#bbG;`X9-4C?0h>xhPrr8LH>9}791WV(G_u{VO2*ukaMH}&&2C);Lt;(n}Q zOKMBl?6tjeiZ#E{c^N6ruPcFEf;@YJuz1r9bG=!K>M*0;LoVE&S;)m}HsD!$YW;|S ze(}m%_DF)UcLlJ+P7hc5Cv_1&zh}q2epVIRWHOA>Ao=;g+%&8MKjn>DNP{i$tszIe zLc@eljY#9>Um$c=l#%-WBT+dp?!q84&Hc4x*|>>GbRb?8`8tI@XBc`vUa#24pb>G0 zROFYQv&McG+136|y*C?Aq>PDud}Sg_QUnpk-!QZV#nxmZCe}ArgTnv56|9j=yz^+$ z4OjeA%lrexkWAeFquTflc$t`7b+$RoK2D;RQ z|K47NNx~$y{oOJ{>4P|=1p3`s=XhW2zvN)Y56SS7O7@?H$8(r=#-s9ox7!?iV7@&3 zPi?*?LrJlkG}<@CJmd>|;r-5E#n-cM?eXg`KMw}Q%@xS~%Z^G)Gv7<@HtY49@!#8_ zf5XM{wS&<5(8SOfzhNDz34EFN2l;Y{G@dtVOPP@K57c}=?Wn)gPa$$7W>bq=m+hrk zrAwUt%DTNUyZehkV;4=0&hyI-%$#;NDSCO+Vvhq=BV~KPT+Q_*K_v7J@8l_c2C&@g zH+^&(BEPHw82?I45Vt4}lgae5h390QdmP>m#V{>kmV4=+uVVq`Mzh99cl_d%V^&3w zh_L8UrZ}8!nB$Fpi@}0~SF+mts>y9b!c<34+AsTDGYlG8&vAY8C*&?rd92BIe*RTg zu01jTX*X3oy5hMofrp>)a3vI)u@Wu@&Cr6|lNH^CwU^ju-YbVx`lm zALFIg+OUXjl4bcS^yT+Z70hPtul0J1Q%UgEplf+=ntnILBlO*8{b)%}u~lPs{%D#Y zt|N+t<$=fYqGO3KP$ch29~c;xT{GbRKL7sj|J~pG?f>|v|NdY9`M>?c|GWR!KmMP; z{ky;UFHg^H)8gL1n__7{)_?r>s^;~tSkzz)WEUUf`QuEnl4*>%?gIJyb0$e-lCV(j z|NZ>=p+96q&KY|@M%(?zmw$AlgEU4&*|{I0-;cR6Ct~prNoaZgeEW+T&<<~!fl&4Q z`I5LIG#$`xyXx=HU1fb4K=Aju{`+HV?*3DjL=4FB`(>Z;H&|Z?H}&@oY&gYY!`si_ z+qr9xq%efDaJ=tw|0^epL>+CO-$%J5dvFz6m0`g7``hR#u1#~esmJ#F>sf$08I&g1 z`u>>fuZLb>7^By=KOf~{5QOz3>>v^MzxPPfk`x+Oy7Tt|Z%Oi5qB0eIQXl+oj1?e_ zhv)an=+l#En-<;$<@fpS;bpAuK=<FCTtDaVFt*<>n#Sl%e|rely|=sH-{%=~{Df(FFVo*AVDvfkcIb!B z_4g5s^kMgKMAqNR_1;+n587dq!rwnix1#SNvDA9{{W+u5A`lGBY<%w}RQp7|-do=H zG;W$^BqBnWa6Es1yeDjnmW2`bTU!Zp)-$p@+_l#4uX`e{*cmtxzGu`CYcw8kgkP9 zS%DG+wBOc6&app~`gQ+KD)EcS&!F%<_Bwj*!|uK?E^LbkHQ#ciL|*8IX~w_b|FV-f za`wPyzFmEJ0G*l^_rLV57OL|r7j4$o@Apr{xZi)#Kl*(Wtf_NSmu<53k&`DlF{CqW zOT9rBX!W;t`}8ez*u9ZR8u7|o+hE|b*Fc%ps~rGR^mrB|VAW5D39=jwuJ3*G+rH)m z81F}f#^2)-(wlSL!FbN!cci2>#k+F!EZ^oBMUj{a7LEB^yLT0#tVzRrJ?47Wk1+gQ zUtFNSpB@O*8l&CR&OigA6+M}n_gc5A1Fn!|ESY=%_*SX67M=HUep~4yNPr3^iN4Rw z=Z^<&%buhYAc^hWVN(arY1f!7-;3e**JlPR4P!TsUnR8qU`!vCcv=wO$L=F>S`K67 zU!ApLX>S3$i!MB_Z*Mr#kXrz8z;V8ZK!b9U-7r&{*5^)ZpEU6^VKucm+T)V9Mi~43 zImXJAnKNlu+e*xV6^hwx_TNK5={&~?am6k5RYLy$mbn|l#P@jUN0{{(psefnOGR`a zze-#q--ocqW1~%yDbw%U=96>vjq3OOKFWnz1<~b*ng07#VJV!1d%7CmKWgqqSfXWf z7iiy_vA0|fPQLHyd+X6_<%n3~SN|TfyKHa+K>(=@e+`Uc5Wa3Uma2RK6Twqq^321& zzfb-TiD<&Q)RF3R1M-B49ksjpn$69|7P8UzeMinbrFNM2@2K_82?PaZ7+QZT-Y__* zChoCsM>)(i=ZKh%zrG)Cln&463TDCD#f499Bqlwsp-)gUuyH5{$>*9s)WV9J)% z`u(dxYLfac;H33##N^~u0o$(AV961F5n+sb5!W_}47|qTW%}`LlL&Shv1gC}whqpN zU^SWL##T>)UOpLb=sH!M=vzN-75QFuvmijA-P5uDzEwRN<52q(bkDba8Vpis@`&8~ z%l8QncZFFy9I5sAPMhKG!@wUN{`cURF;5wKc#6Mo#1cpg937zQvn3n^rHpYxtMVd> zGYh!r<-YHg(ldH&Ma1&`t7pP&bOj(azC%8?B|^&*TL>p|NSJF?^B1}2x;j^ATa1hBmin6O}(TmbyxJ_{o{r*X}K_#KwnSRH*bf=@O zIoLm|ikJwcmn7hwqZY8VghVh>*KB)R?<>oqSd8zKTa9{`#3&z!)A9R!2PEa3V?`VF z`&9vrNe_%(rmDwCw2hys*@!!;lv-mZ*0}kqy2sIFfPt2Km{rk`l3D#5@ zLig&?O9IUD=z*uN(y?z)fN?F5c~>R50Qcy8@&K;2!1m|*2Z&vpjJ%*VcWj?}J*bO^ z{j~W8Ui^Kf8jEyX9=)k9aHrVCJu0MmZUyEE+nIbM)48uqHMfgOJ81d39gt&U`|^Ho z-+pS`60|wp)A_AB;G1R2JQ0HKdoLIvuD0$y>pm~7!qhYa36(nh{hTEulXw~TuPoZ& z%QA~XbUNx(mBcZghUWRbbw>e+w!8w*Q8g9CMOYX-{Z)n>a58LXR!8lbnsM~aaMWj2 zu1FdwZ>BNT(MBC1xIE+2db-$K)td}weyi_&b00cT6GL8Q+x=XGsDkW6{M7E)B2AEA z!ae`~9BuyeCOuMh;ltkorI?eJP*t8W)E?0LUF}U@^XIf1_CdP)&4WGRyC?>L#`z`LSOt8dAeQ+;Z4 zycgL~iyCz_1O2sa`KD8tBP76EJ?@mbqG)wmw)VKY*lh_*11qW~5nuzuv>^sw__uA4 zcUYw04clDx8LBw9k>lWb3O<3Uoj1UY=GGBnB0?X#ryBPsRn4WqV?u*_kPXzQvmRh zZoF#0e$A*1mHI;n5HJm@=5lh(9x4%= z>-Uca#U=Y#VCS!YqPZOHp!LSL|9#Qh@q`eOdL8xF7sD2f7@~sdw_hKcLL^DC_Hh1%*89}1xdqPm+#-?kRgO3IBQRf9;Q~s9MkakBx)(5 z3vmQ2H35^JX;ZsmOsp-$XnG3wl$Gc^^Nm<9SfD8;`0wB}<~1_|ww_zla!CX_g~9U8 z7Azvxf^&}F^tiSvKmwx=gQJ~>tgBl@G4g`f@5f+|D!?>`r%}y>BNlH2x+Ic*b1rds z&Ios}T16{UCAhyU`LWbLDxw7)^*$8)C4|Y?&3LNA9ioc~n0spfejh{1DNG)_vs$($ zOTkT&-s<;%W1*N}Y?leFs*lm%9h#)#jL&M49U)<5M!cOz?P#VmF#%s|b*l)Ljc+rO zI*%%~&xlWrtX-Ds1Y6UYq^UA)uExU>uB?l+G^y{CK@8#akbPYDWnfpg5^aE^o$lup zewX>xG9PU_CZ9VZNq1YQyX77b_+W~ydvT1Do(^V!>lpPMF|;#eGcV3{UQU-K;}o3g znwxYG<*FOax_+%vwlY?Y#B67=XZudz+6c}A?eF{CwahSqXxqQyY1ggFSZt0(seMga zmJG3Uu=Qs4DDe5(BR{CN#))W5A_=EaoqK8Q6TGom^;LgvDr>t_*F<@03|0q{$2)PW-pE>H2ONh0>mkI!);ICV_p^@91%CPRVU>cUzA- z?+hYSn<3Ip-?wLSAFcADHPU()y6xu{b?LmY|B|^~HxY>9 zL`O@tTIX6WSA@NY1IMa4stuv9bcP?9Mz@Qdp)m0{{CBi`ux zj;fTQ9A!8i+gD8mq8#w0P_^avI1;THUxwxVHUC#yuAOa#LQs<3_dLpaz?uxn;Of^F z!{tkp`BDA0ZvWcgRrs7PONTVT3np zs^y|DXiy)N**f-`ny<+|l6hWz9<4YIC3^Aa7(-%@bttD7vF(}`r_RJNY zi9ai5>xL(3DsN`2?E&y;p^X@E>{Y61vFb@Hwzsv0<4n((i>_d;$`mx{8k&c?ZbRf| z4^%ntvig_FZPx(tM++^f;Bj=qwaFmXPr!MaJ)MEXhty$U4A z(>3>NE zXrD_`bw-eJ*m}QC_4Y{dx2U7HHhCr5WTLiM_OF+0$V>;~x-})QqRM!F|Fm!n<*HK~ zzrdQfCF;9x8{{21r-$49eW}wkGMzW?{`(mO+X*u@8U4ML;|XDm$U@`)`#sIV^eu3t z&YGyKiQ#iiq)C|Tah%Ye*xON&zRUWaTq3gHt8{5;XmJ)%2GWVwmPr!AA8~0t*Wyh| z8FE9Aj9qKz>2*R@rKU!4)u9bt=CAlBgW^1~qfftGcZ=!~I%n{&)GdRnd`?H#r2hK^ z>2zc7OtH7NZ&S|^mkb5qJJQWa8JDDNwLUt}xlWOfvg-XPkF*T*Xkd~ zR;~f5^1seDV$rw0KSB1nF*Tjeq!+~CVO=cCx8lwWpao;+?UwS)K;Sq&y04wJ7OF+(|y(BSd3 zs=FAY#qqdktITFFpOTe!I@+t{rqjn5ML$PELFl9ATRz=P~cCJU~CUaeo>l$r*u9_AIUiZuk`YE1ZCOq4y?W-@2XXo!Dz}l3 z)*N6y(P^Cyt_WLT3#P^;l0-^1u4EUj8|#$zzc!!Ab9GgKx)fflWk+57LxaW@I)P5A zgg*lXMPbm%|0+Aq{%9NttwuXcSqEt$d0}cRJ*N<^__o(?jOSlH%&UUq-sX~EZb{uv zhV}l*6IZ$QQbxtGAS_os83w^f={VO$dW9pcc``3c2}-cF%1+c?41JGbA$%QW7U8K6*@ellNQ+x8f zwHkeI?IAi-yx%_x(tZ1*pY8SlBIzRz-JwcwErMKHkaJ3KdvZ|{{w{&Dl@2YQ)GLX! zb3K!SOIP;kXdAZHT98X{GP+GQQ`s}xozq2_zhWx_9MXPP81-0f1k~7>zYX5|@5@>v zL^kvgL|3QH$_0+2bz=I~UZ~p$6XN@-1A9&LMt1}`qE6KmO^In4{CM@;Kq<^*9LM+y z-@^nNC7;TsJ@Lc1Zlh3y0jTCRA+afXbk1EXH?nkA*w4C!%nt!fMR5OONA1>(^h8@* z4HfS%190cy;^0ZIF9~`JMq3%T)*oxnqv2j~8&&y|`7-@n4>aXy>YL!Iazvl4NgEIQ z{cRJHmVU+AD@ zB}_We3`xGZp}!r)Wk~x^<#Se7QzD9HBnj106Bfp-jDzK-EH!Wk(S=?%)~_kRF_L01 z$I>@fQ#(B|(2ymf#FDAo()6daXwEj|PVE%U{9LlEprDpyFkkzpf4B>U0Eb4PW%qkM z>R^)V7OIwji|)QzB*glpY;YCt}%OJ;T9c2SNRVH{~=XI=X zS6KOcjNgS>2UE75o~L56TEW5=Y-(Hh4x=0b_WXsF&<;^Ws#ZiwF$G!mp9fVhETl)L zK9^-m9P64qxcbz$gaxP;j`oOglCNQopwBmH|CMwA^o5cT8WA|e@kSQUPZzO9UHQmQ+v&{iw}7 zlnNvtE|;S@3NGnat|Z~*R1s;|6J9{l;J)iVcYl}>^ue{l5~nOW&i1^vMZoYY9Cfd! z&jdoM8}~_k3*WJKFF3Ftan9qcrzWgnq}mMsYIs0+FNzo7d99xJy{A8X0NtmxM)z0Z z7t}w~QswLv=Og$ru~;ufwkhCkiFWOCyyDiNc!(yWa^fXy{_0QUsmRu$7TXr?8g#6I&X#IWD1I`x| z5XpjQ_+Q;6IxO8@p_p^iK+GtRrs_)*69o>xD;D4it2tsMCU8a0%+^FP_1Qng4X|6h z33Ew?QE`-ue>b&Pe=8uCdLZwqZT)z?W{&-@Ej4IGX*W zvyHRv9cjAW&FU;Kmia7grJCcI+qnlJAx@X8x`wpIBYPaQ$+GeJapuwm!MDXV`xG+H~pkb0K|d&X#=8 z`M{*HggjL(2!NGTh7cC*%9p$Tsx^LpYOPZQEwLvXnmwkpLrNtvBLvbViHxb-W%}Np z+mt6JcS5Tvu&f#{I{49+qtN?&L66~C=Il7OPX!X(V=7=+{ifNw_}eqXpR3adTxTcn z%#Lp-Bx;!4vOo+4${gcp2NS0jG>5oA8tZF$uA z8>rzVzt=M(Om7cOk71!b8(?C{YP8Kwj;$+uP4Q5*G1Ove6LQbl?#=x6q7@;g_R<|} zpd5#ehst1wIQxQ)+{aW77j16}hoP4yBt9#QlyN#(ps4xNaT9fb0_ z+U#tL;UFmGQp&Trq%ai>*V?-$7GoG=VBAYOYTw-Ch)rqE3Ozn08WX2$94W&xmfvaY z;YJGFd=IVL87aUNi=N`6ju1psjWEEtx+&B1>7;HUsg>#*aX)0OxeQhM%q||Tr9R28 z?$859_qKx`XY+GBmc0dLMTy_crd$H}TQs3%T}}|teRi{$?DcGkg?&OPqJ#X(9b%5O z^Yt(4cTyHz38b1uvz0_kAxswKhF0xltW(oX*wZSp=zNwuL_w$Q)doEy{bPO}OKVY4 zzu-ujx;IA)1umVmUO|m4q-65tvi6kdOa-@??2tO4H0k16{(aQ%lGj1h6NBryDGv=zcZz3>>ZAwpG*` zkz1UU3lQYFwNpOQl#BbaT!07f^N4>uDcmK8k!26u1vUH0d@tXefTK0h;JI%3>4)mCaq^xQON+U0PS2F$Yiq~TPXU*B#p}~u$NCVaF(CQ; zJqPzqP=$JC`e{&9+oS1OmXrOx8G7$MR1=wU;d^XkmuN)0tdKHR{Q@PC*fJ=$)@J8| zQ_c!JIZL`R#WZ;TXz16b#$iO&q7-HGtulRW5CrXgm)z=S?FrZ-N|n;o&mZ3sBgHkh zQ6!;hm=Sv@o6Scw65nakaWwB)5U`vcAXjBc57&}->;5+iRzU#|UB5*V*efnvHO*@36|Yss{&cedCT-fHf~izzFk>? zE=W_W68OU4?u8F7OSOu4KccsoUFGZYg-`)o8V&0Ax1};BPXW#4Y$h;mcEtSS(-(rz z+@Htvtka^BL>s3PJh$XymW%|!#U=KUHddE_NbQ)iL4HGtOLDDle%)S|=?h=*;jny<-epq0IMVBb52M@OI8*Y$ z!h9d_DFMXHW)aO=*ARM!P#J0Sn+hhJVOH#OR3lYmEj|A&{M(Xes|S!Bh<{6(O#6~x z1m3PsU~ni~l|o7AZK#Pu1fUv}2dick#_c*#+~mvEE;BpxOJLNawxr#Fj8oN&uy)f- zG|qVO?+1SQ!5Rd?WZkW))7NIy;~jBFynlL~5Q!C@!*{1|u1k(_O5}9RiCZyfu3*;4 zieXaP6TAd09b7{>tk~f_jPS6n(McYP;L4hGmb8U2jtre&^_;5f3Vtm$GFg`66FXqp z(NK^~Ow(70>4mKE3fRZy->rz#(XNhgcGMPjK5(l9OwLn1@OROy5$-ukHd(cs^wtG_ zXdQ3OtA=k+OY=ls*4TjbTKO8#S#1#a7osDHbG4mqv`R6NfIcekr8ivU?;aFY*V5XT zOCno!pE-oOYT1?O$_V?awiu5B3F)34aWsL~IDGTTdmi7KQVm2*4oF!-q7bKOQi~nguCdR`xn=yLd=!i zt>fs^Xo6!=bqcfY?+Y<4A*P1LOY;yo<)ETST|- zwHYt$>tj#JUrZyN?jrzNK&8J~O>J6{&fqgLzc>}%u_P%9GbFu|!zinOVn@bjvNjMz!v5T2T35)=;SM^CV z3<|UqeRfanI}Hgfw58Byi>ob<-yFBpr_)a6nlnNps+PecT)*@z_%$0*E#<#M#4l0z zSYArTenzeu$LiscZHAOlm?^#k2wlIz))e_JLrFT20#rA7!fVAfa`9;Gur=Uc3Y@@` zzc-07w`SYVlTz0$5*&XR@o7nIkf)9*&6BU`9c2o`2x@{X-wxGy2G2{#{mmyfJ+!G= zwrqPdNUBk?Y+K0=6P;m*ol=V~c{Gm#SuDxsBW;y|Q3Hxd zu8x3XS#oQ!lA#HRl}K=wlYjDTfbWnRu2sMg&%0R@Z>`lzr34U|lM|$N#39VootKa1 zYDkwgDffy;$K26mtd_@gB~GF|pk~In#8;dM<(R~QI*u>{mm)$iIkDlpKz+D0owqzW z5w|yYhD<kT6=V1O9Y8`jb9I?l6g(nF!Sa*>C@+TM_jLZY`ACm&@v#Ony%9Q+cn>ZgFSa@}m9>DG{ew{{J(U?cym6MESa zyXz6DlAn;;Z2VqL3e38+vwg_M#p5W&HUmF7NQ{nKV`i>F$&14>Q{UPtbcI9?q|+Xy zD_Kwz-sN}M48IeP=h|ZuuNEdK9I`W)=D9UNxb*n2I7pAS-@N^ z5vWO(h9<=EuF-rY2S|o$;hQ#8%0J}U-S>3MoHhVC;ezU zLVYbOil_g6JkobL*1Z&*N@?8!IgHdf{Z}6jz&bo1glZ;?ivf6uBp>(ny%3Gp$Z0cC3#}Df@A`}fT(ZIUk&N5Ti+AYA&!E=sxt84D z{qtVh=?7NVcZp?ePfTWMA!QK6ZNG*N(Dd0{f_GhbHPzAncnQoQ(_fj(mV+B9MhNJo zYVsw~##8P@<7@63DqCMzv1cc0nGrtQRkbswglq03H&bn>Om}8#*-ysN%o}S$Y%=I< zjL9i;T#>`0MgfeLQCg@d_O%}21EOx|3Ti4z- zt$BOi_-6gaO?Gc6juF^#O#nrd+y||nvY%~EWGA>clMxtBZTj#j=#6Pi_dc^=KOmr` z6VV1yL<-dTn*Y2x`c>V*N88$1``OJ(x}YhT9J3(QilH)cp-TaA|A(d-^WR_FxxfGG$~?L51m+s+o`kkh zK3<|CRpOkM@AefY3=Rs&Cp)SJQ;UwN;lwsq$I&Q=`fv46mo0b87e}Tt9VHUqa3A=f zQs>w661vJBZ-Ik$EfMp5k*3AC!ho#R;tqV@M1!x|8`bpQ7y_ebA8Wtqi9n+JCsw}I zzVu_s5cEWzooOVG;>z4>EfXIup)C!Nvv+T`ugM&OIx0az?s?LrA*bi6D)dp_eA;Vk zsDR7Ef}&26QHy}R6NOT=(N+AO)084vdPlQBM%5}$9n8yUD)+Rm&WwG`is%I-x zgr@7miqgBT(jLq8aIa_D9fkV>sMzRT)LSh`Ae8?T0EX6}4h`e^IjwXotfO7tmo+ia zQ@rsFOziVkdz?*s98Q2AV`5RTD=;JKf<5^T`|moFA*p$whUwIh8_b>>7v4!@AsY`- zJ6kx_`}>Nl4l2E~>mywAo$1cw z$y{5*LISoZ2PS6qW31u{t-dYIDaAgkblqs_MKP>7X7;0j&{*M}Xztj1Q#KI`J4yK{ z>hdnTA!0ZrsODrK>kp+|^Qbu@@ODlmX$JHq$dbkpl<0icZfe58`!7`G{MISQg8Q)V z0`I5kl-&cI(Y%fA8lA6MiH!r%eBRMx=@}s%)P6V79T{LvZo!THecyCfrQo?=y~w`b zhHZ!a@+`3cVA}l@ubF6f36I979QS#A&2Kyyb7Q;mT25xOX)!7iGss{X&cMCU(wnAo zEZpA=Cb~9FH%D#{b^pqFRgaylFS>$gwf4DT1kOl+4NaD{BeGr0*2pSLOy!5$qem}| zChu=(^aT#LG^1a_Pr+X9&AU)UQ6KOhJICredw=|H@@pjH73#3$B1^)#*bEykXpb-5 zn5W^yNNYZ9Mi%?5Tvg)sJaw<%T3MuIX4O}j=<4AD*A^Z4%dxgfsB zWQZ@&iohpZWRXu?hN$x|Pi)xJM2ST`pM%;L9dt!1T6T?1P9P{U$$lLUKaG+X2m47ZDepo*R>FwF_`I_D-^@o?c=Qq?VGK^km{w1ccXO#mjH z0dqO1>$AM1rgpL7t-?L)H$A9l$XzFLuO=8{9G_@S@8fDTR~3kwaW^eljrBBoIp{;=U8_ji1LeCf0Pb?_ zb|UW0K_EN#tj4^zvnE1q4Gy{_{z?J)e}ezMoj|_gj?k+txt6b+c>nc@v<>X@+kaSj zOurr=LDfjolfpP1^K=xbS#QEGh33E2WXba-dqgPssJ^BHU>ENvd$?sSZBbZBIX|wo z8P=&V1GUwsJh#$v;_Qd|Jz7|a(+fz=4YD=loKzmA%XqO0R6*kvEY-Bx;&)mS;4z&A1W3U17HDPM)kOj) zZ`byZ7{64RT4dB=aURqrgZCo_(hQ)A&Ng+v!kNP*SWC>U;k}2TjFfbiq|LqbL6=scp#nE97*oQ^>!LiA{Te@9NK=7W zF>aqg6B5fB6VmDW7q6vYO<7$+^%zn0LEGZG%L`&_%d0*`GnQ+}!sNG+C<6PsjhsB+WVcKbC% zCez(ykFNi!uRVj;2&lHzUKN<(F~irL&(py(Deuts8pb_wWd zQILI4rG}$j-?Y7c1?2;xUpkmAl!Zfl3BSZP(}dC?G`OLjEl=iJgKnDc77a znmcTbhIi60Q&;rAk|K$U!y)k$Zdap|4Kcgk4*px6Z4$$%N!QjInjGsGh32^pk!}?n zdu~h9(jYMM^;XyseVY0)suInQ`-5D|z!4E+x^n4+_1-oShpS0gOnal|X7hkXIH!_p zpty;zby?m%690i?^r(?o)~}Z-gyyyhr7LohwbgkC)cV!iGiHv+V9IxIcd0QVn>F#oL|VKd9#?SB^Swp zX77Q@tqIv}9XWJ1zhk0T8jA7WMfPVFy=+&TSq%h~Eee{Z`!$?BJ;C(mjbLvRl)AH; zfBq2}W1}|Q7YflNA6il`Fk~;eg9|6Bsl&DBLy}diD^TDXXMBm+D97z+J^WNtEN`By?TKJI&b3ux zI=hfbCr(MD%Q^AGeM;PN9YQ`V8^&RP(4I(BBOQLY*cS}gaWu(i}rH&I{Vv`x*xaerswL^yzs zsTxNTxkFxEl+DdqW)3HT8hmE0NkP!Q{8r1?Jtio{ggCbpmdF^eb7(qL8dB zpv(tM9Y@o0RviiNKtr#axOVQGPY`9`PO!Vb`)=m1nwS#OsacK!Bm3 zd-}&snd=_48Y3)Y)M$80Up;ZIHt4Fi)adM28xL!>rztWFN;32vEoB8`&(oMT(3B>ip@g8dnJ(w=oDBP#J1-%HC#?Kxl@fWy3AH+<$=`l@vck9#)~lvG z5Jspr7c^Q4r7x3JUnsW6-jH@n*c<_*jL~$ONn0NYRBX`w1A)QRXDxZr z0S8%eubAPG9A$jt(j6`%$(*?w5S4LvVNAQ0u20i`H0)g}cZ3CWq&u=Dll!Rlksm*w z(X6ZumZ6rzIllDDzoW_Y%-~BR!Fjcg@HJ3IZByH{6)^=3gbZB^qh(X=ks2vP?sMX3 zC;>dqEqaNj@D>M4DCg6?_n&%z(Gt==EW1n4MrTkI_`jtUTs4^??w;(C7WKE~qY>ZG zBIM|f!$&=Und%02dE)$0I?U}deI@3ORMnfU<0|Pjhi=6+RROgp=N(tqGqRSImeMoK zt+j;?tLBgGnXZ{n^rUA6+W8a`4U#Gr7BtBe_iDgOUh2iZL{!w|e^J~WbdzgURyiE# z*e^qu76DA|ME-YgZY?xsO*B$mqK!m8G5o+B(|5E%7tHM9*)#vBr&M#qP#3^pFZuP9 zqHPZl`)j}9DhKm(uRJ%br8)#u_)pWTHD&alH%d8H-x|gy5?BVAWmkoQ_eBk z)FRGQ1czzeMhBCJ_rQ!c@fbT?frh_$FA3C+`Wdzc15E^{uDq zzwos7ZD|5cH$Ny5z2Am{^6v`1b2NWiEhrr<#nmwD9LL_s)%VB1qq@Wh{M(WCHBZdU zB0tVAUy{XVOI+TS9jktd_;Lj7qi(fCEQ-M?#;csz@;D{A!>$}0EIQ7&bf9O4?h?qoAS~IQa*HkPM!4Td6QlV z;3^{MXflhWDb45n2(;$f#Az1SL;@8tou-rGUn6QOdvWep?h3l1=hcF(^)*nFPE5lp z5%hW{F~MxB*V>RzW)UW1*Z7QZAsFowG|hTdk3U>V$$eyuTl;inOKOP)#8uBtI1Wmo z{Vclcs%yKZ8jy8-HJ^dRl_OfCt$%k%a!M&8MwQ$8Q)k3oWWy~hc}<4tA@cfITDnih zWfXmc88y_I5;2J-Yz*+ny7SM@kqMQdCk>zgOBUd^+QnwgC(*X|t9~~Rmb1~SOMsro}mW1937R|CYb(U?~F#(wPl^`0Yd-Rs;YyZ8T9)o|&gF%F}{1K<1 zbXV!F%UsM;GzHrB%T>LuKcGxI%hVYdaqFE4=5RFc-$5>aMt6lmQ;6%_h|zK#nTEH$ zgehi7i!B+Yaj6awjqUxt5xA~jg(5^MK>jnI;-cg@2o%7@6M)h6<)C}NLot0&J#F4S>N>$;h}sPq$jJerPW z3NOOd641BZH&ECC=`G8)`vR6|=#NOy+s##~r(1ocUoaGaHpN)^r+=qS_muj0(r zmTi`#^12H4$ACA96P>c?8vxn$OkpsKBdz7(YH^CQK+CAYm_6c&k+>=?Ir=BNP!PXI zLl-M8NI|J(nxk+EN0}XR>of!{wLQOm0B6;26Xy*Pi^o#l|TDa1cpvSv_ zyaH7G%1H7LhOZ->gZj{o_l^)Kj~1Kff|e+n5tdnv9T<1QMZ9;FwbAg6HKtebUNT_Q zrwK9ePopT*vfLI~m1_-8U0%Vbw4&$Ymp{o;AQISVwd8S0%##@Y&Ms=q(CB{fspIC# z?oMBIP?@dMXhsabjHb+{-OyZJ5@ZsU+pzyj80fT)h(`s`Fu8F+UWSXbQ8oQBI@+F8S|D*?#|M{%2$0dB6I(KlD}SBt5S!xh=k( zr@}s36KiC6V>|C!12$nu;FTV|9*ccRdAH)k&R2_shJ{R7ugvLC_vyiu+UJ|MD)PB8 zr)^o^K2NJljXUFBAu1bKFlc zDW=i+UaeE!E}M=;(ZY{*qX`W06xRi#A8bEndea|x0mCqt^yG*iWp_2*&TL-0tKDjK zu&rmK)7Bw8l2MpMtF;Gth(<64qJHYM(|dJuP9W~qMcNJzf_A1V+S`dCw@bNkO>I(; z(Ff8>ilmmKCc7Awa#^YY>td+0qoD@Qy|=zjEW(ja>}$)a+%o?&M4~y^T8jbL!#JZ< z6*1tPk%j7eHuau-XF|_8@cOmXIhW4vwC3cfRw&(`!x)IVd7c&|5d(3Z{BCxX3OHxn zHFa>{;40t4{iyn!AKXPLDQm@AvDqpS%?;19-T%ah4be9YIE^Hh02;{3wTGN+V133D zsc@=dW12E?G9j%qAb%4ePD`m3#D?xV^CwPDeOz0H;J!i2wKcL=&9T$(H6Lvn2Wi4a zXUICW?6UpIG@FkeRzWYTiWl#>kM?m%RvQrAKA?}x>0Wijm0khp9^U&e;oV*_-xf}G z!_6DI^5o1y2tU5c*APFy^>*eqf&U> zH4~5%%=_Mg!R`fU4mC&eKR4{nefKBlTr_{H@Q|#)?5npu*Vb?*KK84a4YTDPg-UbH@iLHGw2CjW{G9 zF5r5T+cVNL~=a{nLPBr|s?eFm8`7gxc68eq5=S^W ziQEnM*jo0vWS95`iyan*aEy&fD zOfDuQn8T^FDy{@%3(A_0tHt2pI13cN9_(Ya9R+kHG;c4}Dm9b&E7iz+mB*JL2Nq8x z@l^{TVZuUPCgpmm7TJIpxL3@J+IE%XTSmm&(syG?(n@Cf-IkW(tt9bQbx^j3D+x&V zo8MZ52;J5k~@!6#NoDE)&@x@?)HevtUoF;HZ2paZl3MxpGmL9ZGui|1okz- z+eqZ9{i6rGb2eBi^Iil&6TE?%c2;vL6oHVw_QxQ$HwOEb+peo&s{kE4i2!tUJeut{ z+`>fQTvJmu9l~{~s&}LL`l9DlGkSJwizx+AHO9W*p<}6j4CTt2Yx?~){r>(XjL=i- z0~kl?$>jF%nTgBUjy5sa6S80roeCwo<^JdQrL$`*I(L-&_O;Bd?4l%?pUj=_i$3@d z8S)m>fo(Ke&w=j9eSPx2RgaUrXrWD6=C%M>M(m71@Ek36c25+szIK^l9%<4NkN4c} z5jsqZ@T{po7qVpy!hr+U={WvSbWKb^O6evaPs@ck|2kU6mZDJpW%Br%GYobh@g~c> zmQIL5irj?md#hOiFxtK6VhPWUh_zpVw7O-@S8bI;|HM-VHBgKEeA=Uim`7D&_9uo; z%7JzM%hvvy9u%BWonQGmeSdeBJq0H@uDm?5oW9ETsCp@E=ERHxp zTk}=dMqh2rO_yyZytzpyb?@Y zR8LG}$JEV*Azfw2+z=6yCWi$|QL-G3NF{h8X#?8}L26CxE5T#(m2?g25=b;)vSfZ+ zMo^E;h-E=+OPxlLMQvP-9;S^75_NwGbCcrHVh39qG)kR}d624dKUJGcaPm-s)g-lIO9g04y1-j@KhwOmo>TV4Vv{=xYqkwT|-QjC9pf))yH76ES@rS7Kri8~OEzCrgSBXa-@7}C8vupuvad*=#10S}qrVqVELjOt^n+o68i5*8W>{@e&VblEhAroGiUytG&l1R`TSTd<7XjzUYz zsvapPcsTc?NBJ;J7;|BBuA@-kXfTXQ>X^9^PL+EzqMKalI!g`K^f9$-uI9!eTS|j7 zB$)l%t$9&mK-Mk(?bd;eb604qwN!vvG(n7g*R)h%ciL}(Fu17{w17DS`730IhFbu) z!UT?XblY5OW)2Y+xz(_qCuKtJvjIX>0Cr{bywoXG$m5v7zDn((N3i+uDHoM)H ztc|606!)N=No%gA*YKNCF;5ZUfB)>}x{E+(2C+0AUam9}1Y*s~Yi~l; zJ*+4ZBg&{oAe9wGMu?Z+(_79s0jHGJjnE4U<#{>gb}uJAmM#`^MRfyRB=}OvVZKfI zt||taoU0l!-l%5-)k_)GSmR&`9RrNouZv7z%<>vV)UxiL(S z)|;C`0i_1oVp6K{r==TN^R?IWKGJ3n0sHLY;FWxmnp5AW7Ty$OA>YbUv#ceZOdhZ; z;W>M07Y(!gxk1-dZADEF(^zxxwX#R92*&p(=(i^U#?9>$R=hUxp2?5ZWqeV|Cf+;k zg|c<6lX;}yx)NHHyx%=hi~)5ml=8Q4(xO8&Zi(8d#v&I7h}RVd>efhDfDtE1-PtxJ zZ+MCK)LpU8_cfUKjm*M_R*vN@p|EH4Ol?~FSS{1fYhn<-T#>a@hf_U}jBYe*&i}Re zA_fXeV7>PZ^6L53Cd=~Y`Q|Gpfcy9|wNQjqNTm6R#_bujd=JxRKn+m0A+WK$dFWKT zTdSj2*&SvuX73Gw4dvkZP!~5@V}K3>eFz?v`hwT14>$d>_bRQoDW+(`xE}0AM#!2l z)s^r}0+8qy=V?cph&7ncG_?N^>nDd@8)3aFdx=A2eLB~bazeFvmwd@4{q9?b>>Xb; zU+&c|xEghHjLdV(PmyVMmniq<05;bQSD}!)QC~9eiVg)%Z6Gv7x7bL)#}lCv#8kNs z=Rha}<-gC3>NZL-g)tM_J4)1Q540VzJ6?U>U<|$5!1gcfn$&6|jNhV!skzcMBJ#aY zBk!?vLtDfp`xfiY)~jJ6kPuDxUiBMW*ud3yTf5Ln8o3NF_dg;aV}sYw9yN$e6EGI} zy-7urd@hd@duuzJqD-2L+p_$Lsx!6qc9qSFn%|OE#R3QmnSDt|uh1%VUv^7yMQ)G(E-DHg>PN4X)7u7l?E@UX9JoJaqnoO+g1Mdc*y2Qsy^D5 zc7K_P5m(){u`ryTLf($t?l(X|uI?z6Eh5&{J;a@**=UJ^E6CbimgSy zE+}0rNRdP|b#V+V)8lt2k@CIQUM^4k=B@+xlV5KXyVqLF`)*ighgvG z{tmS2{{)7^z&34Fq`7$^jLNTTwY&0B|6tpWfEJO{r4bdjFrQ7Q!L z&@<2oQ?8{MDFjHlc!OxvhUQUVLmm;NwKlB19t)!HEy7k-J>pGgdTpLyCY4P2MgyI} zVkSb4aCk3U7c9>)=W9|h+>%NmJx!+6XOW)e2x8`uW!5$IA>3rho#NCpzfO4hI) z4OpTNLM3ndQIC;OB_iV_^|r!ehsjC)_=d}NRNZZpFw#>VRqhYfu(f3jeN^n}<-W5M z+9Js8UoM&hYJNul`!oE!UdB4&-6ayCHx(|qL`Js(qO^V_X(g7i`~Ol$rka)aJn+nh zfrlCn;Z+37Xvpws&y7uW-$EG3wc*7#qoJZ0&$2cI>CH>40)}b0lFyCgDE%5dv#=AX zG9M6#??{C?G+&qH3y(cH!;I0zGL|_x&CQ7@Kp19?mKGitBmQSjTZ8Mne$YWVL#peX zc83TUwDw<%y&O)KkRz%#=EvMUF)N;F>17}sQ5lJ~S{FR=j&TE?-S~=aa+-b%Dcl-+ zLMe*5W=PRy>-gtKTyINzPPq{;PQnc5U|%G6r_1OckLg$%xE60?Q5jy4^4>tROT z&dw7!!dlFv{k*EsNMjLO*xji)sxV=AK1>Z8&U-cN5FS3Hl(*5zVfuQ_kWAH`Sh<0T zr~K*G=sj@5^n{CfVhP;|9o(RY(!Cx8J(($KsY!xq4bUmRii+&v;=jr6ed}{~^7J$vvl#MmWiorVb5fqhNbuzfI$0a) zu{DQ@j$-axQ|k>MczygBX3_;PvMn+2-22tqDAg!@aGAr)@d(s5&YpWw$JqI6kigHM zIBqPgy0Tr_9J$}Oxp)N8H8_PWy1f$fpjwQWSV^Z+RML&(r=G0kTWREi^E#dohtX^s z;$b6Ti)UnS1NedNBQ2vzyrpg1iQFq&Bj?Pc=}L?gx)wA`mv{%Ghpca`mBSxhAQR^& zoH46IO>?kIAijPz#Sn&L-R+>+)m&!UJ{ersu zuz;mhn$PYXRre3xlF^*le3lScRC{|f(5%XaLx*}?P4qNx$@{t}4+@LrUsNhz+3iar zDZ6u`(03I%+a-^B6zhY?mT{4{R@JycgqDU26GCu8;V*2P<@psuND) zgA9I?9yA&W>;6FJgl@PFRP7p&C^}o~@FoF5=PVMusr0qiq;EXhxA zE*Z*L!_R5Uobo%P6jECl_hHBtcIQgC++lkvg+sxe4pnS6-Elt|7=Q$!bBmAj( z3CEgeM&j*MROI{69pRGAfPCf7SO4rI{c;KM6(}5)eMpZYA6k3ygtGD>Y~L;MLUlBF zxED{*Je4@^cK1wVrY5^ecU^kLAaJQ!SBm3wwWqzv3S{CYWn?d3blJr$!qsY8>Kgj| zn^TJJ1prVoh3BE78>YE zjv|8LUZ``39$g1u@2S}<4N>;+N7b@5)6~p!HJ>UpFd06CwrnH{=;lUMiK)y9NE&W+ zJdr$cO*M{Ja90Y31kO|qb6jyk2PwJqcIO(Yn2qqN-$`pUUH$u{URuLKsu4G#D8|dC zdPNqS=|D|1^qidl(q`7C^|Ud}$+JI?YR22XBe$o8n#Y_pCzyKXa9uFlZN6YN^h#>AZKK6h8i46Pxy!H3hJplh z#bWz?wlTjf84F7#n3j@$>mXA2p9O>z1;)<3y&cz{qObs3YSUjuFIsZvnj(W;>NC-e zG5nm;?LS*(c$ZvK!`JIDw#)&NKy7$vADV?09YfB%~t=_XY-8Y^x zTO7LH_mY^vXRA{sm(u)q4$!uc(c~%rsmRI2UjTK&%9{MvCZ{dL!3S1Lim%`Y8t$?`!02Ia#bjt1EY#@{I zSY1{9)n-$;o)1^XDO-9c*2yVE_|eP{R0q36pJQ$mtS4NCPSzcM`kV!ekHu{==~>mT zd?KX{7sfTUiEe;alK_xLCw1d`0Hnq)dw=|WPnocfcG*2md)5Ht&!u!n0DxO=u8}R4 zDU=*9M)xrlp9pQqDECZJb2Xe#7{b^x+g$v%OBkb+Ue=|xLS0Di#;cI;D-%E}AYFY1Zk&oeg@zg_EQW&e^3!=i0faAhXsY`EAzCIwdh- z^g=dA3&2~nKTUD#4VueDbX>Edv2)?FKI|}5|NjU<4 z-`dI>{gdS+HlPp|ddHUtzAdQFj=girFs2}xl%c|1dd)-)ud24rkfZNo%ssKLI6h#% zsL{YOx>MCh5UyOivNF@SfZQj%snzkj58tPE;II#O_PY-91ptfUvs^9C@C=#-A=pfG zYWbhaD^Fki97mtC2{ql4X<5B8m!v%e=4)#MC45To1+1GwP~89d5s{v;PPTN6csRi< zueCT$PAeBhw5CtBdcT!u*v=<^>8hYC`9)hmhNczeGDUgPo1?n?AfkF%rZguJ+tJ-7 z;DlGu*Ro%upvu3NF>hROG3XIGO6y zk$ttxBBjDZNP}*zT0^Yd2`q4|M@-b>0kJ`IVXByG_ zat~PDn&nrFz)UqM2%E_w)6hMLzfbkDgh=2sJkO~L>RhTt^RV`BWu3p66lXsuV2t*a zUfDNnCQY_*|1PcxGELVet3abU5+xcyM^hm!+4YgYwszbmEGG8R%f>OPhei&FO1)4W zRp`117d?3wg0=Nf4QL30_MB1mj2Y5UA|RF4_u+z0TXIZKI(mzKjycIlbu>oPoafgB zm4xqXZ4?_S$l=vmdTG%hISfwh(I7GrkmuLhm!;jI?e4G#V7nDu6k4QoV?v!Oqwbx3 zdk#Q~{hFoixjZg7vga|CUM9m!O!F~g=v5E!uD8~t;e-v?+}yOj>J<8j)-gzknkc0x zk1I;-QZJ5#5{wGD5q_nH&}Kc>qN<(b(MuvJ_hTQ;W;S}q(9v$?zB@fnDS>FhODiSK&a7hg99L$lXij50t zmmJ>2<1;LYts%|%uvdTmQAR8pL%Y8>Syf#+qchiiRJltFgav*aF|=M&v8FFOT$q-U zJe$hU9GA?7t7~n3I+3?_i^)M%cjL?gcKRFS(1x+S1b_u0JFN|_r+KV#aQP%kXZcv1 z@EAczt7D6(eMNkC5nR>GMp@_F#-gi9QKoI0r#vuw`Q!JsH^OtzfN60k4{bBq?{yB8(7BGlWx37EOW!}!b zwc2KxL2oI$_w7S1ABbOEV(Wtx>zU1RW{&-+>3Og#Ca1R3MwT}or=@g5ZgJjQ%&K=? zsXwrnfHRfM5%r~cRsEC9KvzPOAh;WxomOUwdUmwI*L{4>p-iJV)L>5fE5F8o8_<+?d*^#gXAQ@;)AVwH5}MEgi;#Qp{Ggcq-8mtf~M zML8pU@NkdVf~u_WD@t=Bm8;W`we%!jSrbcArzHNI#`S6pQMRZxslr-9+i?^boPZ$R zY@g^>q@;UsXmmvncUC(GfrK*J74i6kp_dv&X)N8nBojpOt0g%aoLF3l?Ox}7*-JEH zz`-YcpZZW1MHNccBIU>ikykjcH4+a0_dujXaOs5;dl^KdV?(&D{ z^F9cHi49K+Efd0<&~!Y3xpnUEg?6t}d&1V|=1Jf~R&oug-I?wla!SFsw?kVJ^tQ!! z-O)Y6;iCs(cG5GcSkkW7^suePto42Z-iw~Jgr}Cwy+dcUt<7k7in>(K6#{#UFMa~q z%A-~`?D@*A?@4j0*0VeeqGq``by@LTrz~{0Y@JHo?{Kl15?VhVnK!yAG}nf%I;~SO zP{D7NOGc29sQEQF&NA`nKtb=U2!U%$xAuPu30X4=sDbrr?CYX$EYTFKm;erLP06F0 zQ7*01kxAasB&pjou)kMunHBimE~HWQOao@gmYqhj6zP~-aBOyjb3jLc@I>vx1ib_=0Pqd6+zEoc-CsUTwNk}n7hk&iFq>=LRd zbI!1ds*kU^w3^njbhMl;14RM%uJ2ou^HJ~Gx`{4|?IxWPebNThWVF}vc!GC~wYHdW zmuT>$H!bxPBP4r@NAfsomrq_44@LsI+9gf{msQ8(Hgz}&vBl}YxLPD0t0D28R@J8! zVsd~LmDQE&Y1NF7JZ@n|(bc+0?icnRGQib=1n%D^Ek>7(zF|@w@8dStB;M{zNqz3I z?Uw2QUrB>a6ZyAL$Nk4H!Wjm?ztZ*83r8YyxBPe2JvaL0Pteh7eDsLNlrMgRxu|FK zoo;H;gx5rb4k4JZkxY}~`0ts&->;w-Z=ubW(zPb-;!U~!8sE>wlDb@BW<#r|ZQVjo zfS2fC4Nsxb`skfp7UxjBnN-!Q{CY`)<3$U6@ zBMNRtfN?auC@Cpr{gQagQXkE6bjp#_TPrdsndT5Ae|Fn6MKW;jrhe|(p7`SD6ynNK zMwOT)*b96CRm%HYgc114SDgxTFe$XaQ-wiAmx&XS4q<2PI)j%pUe&AJ@sL4Xe5j$C zWsVCh$K@rSsG%FJ7_$jI!Mge|O@`Mmp?9xtGx~yfN6U-jsu~pO6)~m}Bez&w?!9MV zvNxuHder?!NYwQWuLf$GeyUdL3oR8Gr35;qLoG`UszW zXUq=)JN&Y`n~)z_0Q6*A53(`_J(AUVlxD(>fs94trNy`KBr72#2Nh~=1 zLgw1*@d;bps_(itDZ*1_R{+Eppsk)-KX zw-yASxoEKJs9al(5Yo6(Vy>fZb1u-#ZOn4>RTnFP9)uw@-3^0kf-qu|6dTxr?(W}c z6*mx5QFSz!SG4dnX{wBH*4)3zhJiV4XM28)`-SuL9NVCs_bEjE>1f-a!)euPS5c4V zIk|6gyzQo}Yc(**S70px5n4YLSPsb{2`6>nN@!5iY8tk#6T3R8wF>B*TQgX)5wn(# z(c0%|LK^k-08B?+(Gf?9Rqo^R`qIpHk0WnNET=8;Qj}F3LB8M3AD_P2`gr(J=-AL6nytMri1rgdlI#}u5U;| z=_(fPTkG9uda1ts6t189DWyn@Wm%{vF?`l`5giGYF~$vSscgmCXmm0N1Ecu?6RE?P zwX91G2a#IC0n_g&pQl{rD^?(qpr!d}7C|ZzP1U+b#s#|B zdfhKf@ndIhcLCDJ%@8HZmaSS|b25vTJU(?Hbm7+ZBbOR2j^mk)?(;*usL`&RCHniR zVI6ao-BpQF182Y zdA1(B?IZxQsTPa~hSZh&mL0beA4!QuI``XIe}72^5i6JDpvq_i_&J5k+;TMH4hw`& zYDV3Vj1qN}qt&Vw-kBS|nWt=oeJw8|))s4M>&bo*G)7BJ97IzN z{&Q`}4Bq%*X%YI%3 zaJs3^B{-=1+!u;`J@$~X&TSuDpZS?M&85o(q&iMlM!hX1^m)qym+!r5{3$XNY&2q> zs>+p?08Ctqs_xDlEfh;3TCU?eBoXUCt0CgIY5vNPUGTh^$Snbn^|&WcblaXq(>~vO z_YtL+aqQkYSGw@dcTnb*5YK8vCVDTZ*8NEw8ht>he&6$snJ#f^-W-I*JhKHADqOq)V@%BQ?}eLQ_Ey1f)p^0qMP$ zPz9tD>5$M%XbHWi@VxiE`^wxuZ)VS$@7rhgo;hd#*>k>ctwo+sdL$=NTT);Ab6@?w z`aRF12cGHfZ{f|2tbVMg-Hg-|Z!ZK)UQSK_E(tkdKh+SI`u%5sNxMPJFYSc*GCcaF zeVL+pDzBLW<*X%8Xzv+fg*YiJcK3b*%Q6_<;8*Zh^I0IiuQ_a3!C$)xp%?P8;^WXf zVqCg@`PCW+L6iJy@n0!6Op%J8Kc?+8@FGahq&3T=RwIYh)B)mU)pWCY8R`G-MWuKf zYd}99%YElX4tP(0??5L(`S(7?jww&&mRi*w(wR!f@t^viWU_Q|+{24$>RPLxd3WdG z<3_KdUw9ivJsvtegL2=FEG+a%{o9&%@6i|CG@a~bq2Y|}y|EKL6RI{3vq5#__qBgL z98;#&rXiL-!cy(5K74#Hqn&fnQ63GauHtN7m6vN|N_&b}6v<9GQFyMD4-)K&|q~+~SSrER^$8R;fcwn1`Ys zq9Pxx?F!^`Iv zd-3-bmtuvifO8MKu3D)Z2~0}=YL}?+OK?1`c#{)p+t7U72VVV9QGH|!bkDEGFCsDq z`NVI~!|H7SMC8HrP2LtWkrKS8<-|lNP5#xpNYB{ z+3GdwlpLOSgopWI<7b5aU#N(+0alu2OQz3M_+iujFL0$vkgHrcLSiq~kCB4^5&ah& zBY~9*{|VW#Pybi+A1Uw%$@RZs?eg34|4xsQqLsk^1K9xn|CzG-Vf&qw&1H{}aWgTk z(DnMYOX%^x*xV1B^xpt6?mP3R_Md;Ew;JLoCF@ieNM+-%-+%HvZrg}!qV{dUW+DE^ z?6J;2Mpi|QPiosP#_YrXJ6JNZb{cJ8Ju(D;pPhYF)_7^;c3iuPwjUTlq@`yoNrFmU zgMKXF%iw;2%=lxStb9q36C{Xf318NJX%uu^OGMj~j3Vx2Wb;Tt_C1gBiS~@62$76z zZz&L!dyxJr{(0}EQPgqmgH3z!Q5^092--0Eeq76W4TA0eW}nlx!38l+bg$jDpI=Os z^+9XcJ$Pt+_wE|W-R=pN%y?Z7s?z*7?;k&|xWq^j1vvGl_L7}ncmg`?9$+dekg>!4 zz7>XXrrZfbHXb=@LP-O?BF(0}%)`2C3^+Ygi3BQpk8L%ftbq;D<`CZGzw4Z7rf+np zY?lq(h1G=$2gt>nfAKO;t=FWP8YZYZdAO@PZ83C2sychbsSDK(gh@0{@-m~=J#Dxa?)N)HhXx?jR?W#Y$f3syW|B-O}ZJhcE}YvH}X_B1Kj)Hr*rrew5ip%4JgeX z4vN&-77hJP&Ux`)YdrZ)J0kyc^?%<|D5sI|?EjgLTUZLbDo)MbO&^&W;ds-3_iy$- zD;rxjq@t3B2CI3lAt7l(hz{I8I9S;jSUbi}1Rs~z?sl*B%}6HGbl3)ad!G6_v1gfq z|A_C;p_66`FHfHm0(O?3?KWc6Z?JDbV<~3MGw?tNB6uHWr`HIQ!*{QZT=t?ZO|SUu z@&T~`#5HaegKoo~t{tZrLaw{kCa?tM!`@v~Fw7Y=Mju$$)*=sYUG^Iu)56$4gOa53#+y{8GpJ2yX*O?Wp zzPUQRRbu;6em>|3QyAQ)uFQmWmYDE}AG)>t+Ep35b+eU9{w}DaJs`llm6h2K0&`6! ze!zX!jHwva2nxE~I>H}));t3cWXQ+!rzi5fWyCj$7yZ9KCNwN)>_TOPm%^j6%;fw7 zJj-2J1%81{ArRM5A^D~t|88)=T6Xjju%10Rwt-m_bDLc*z}MH7-Szt50+SIe+q#G& zh=!q0#+g=5oMAy87kGPowb|8zPw?5m>B!m2c_6b;<)Yu_B@LhD!x=avxE2^pqN(MEJb#PsaYC7i+o*QZEQaInfYf#5DapB z@Bw!EXWQW!2)0Y?qd_!h1VcT~J_LDvc(lA~@ZRfe3>^S-KVCOr z12(lTI;-^_J+%rVxOoP-0)pFt=u;mW54cKUW%}>oT^1ui=hla$%wIPIx+v zS}R+&+l^l?Q&xq(T z^m>ob;oVmV@Ry(LbN=R_S-E&&>7amL*cRX6F<`44a2{C6KC`=EelQf<&{r@kf4p)! zm)(GqwXXP#omx5kJTgUrO2%}{?&Etph-QH})rBa8-0a>@2wOqknstzUA>jahbc9zh z`I9$t7>@pO!R+jJfxjA<66tl`!K^@om4qbNGnQP>4HG=!zX#CdPKVi7zmr{uSEV)B z!7N-?B7A(P33Vy4!#)-vqKsbR2Aq8b#^+(OJ0m(liN=ko7&~tq}yAO zY{bprx3#WpX2H1-2q?%a2*M`t{1;nH%o#@N8@r)z?p=0qK z62;naq3z*!q%xR%#7%<`8fD8OJU!dB!$pi37dS*>R&oX=SLW1j-qcJ_ZVUJKg5o9p zi%QG4Z!ZLTR!d*6BZX{1^X7qd(!t1BqXNLj!-n+IV@+yoLBs2I8Ohs(k(INZoueJ2 z+}eHB%uI0F9eAtEF?kPl5M@;E2n_GWyt)S(6A6^2@@(_F*vjx}5f8Y~U|%}!Xpxtz zp9rwjSYA0~^zdxZZuPtDf|#=hd*Y^OBxu-O3ru|Qq<~?uVLN+Kj9Z48Gk7N~P!o)M z&)9#sp+F_=B8%mznugguoWsAYMpChzgg1h(-m}wdFFyeBP~&#iyl@9-2=~3s^eCDJ zCdm|GDG(^(v^sgdoqPFRW!oWgNrN+1!XSBepnF0<=NJ{WrkRz)bG8=*e-nKAHNf|h z*6v^>E3VGkKQQpDD@?k%fNa!tv;oJ~)z_YgV zmktHpP8%M3AQ|bF^X*!ZPk_;Pf!Z@OddH^|m#+~6i_RT+Bd_z#%$)(8appmF(~O0$aiqa=wqRQZST zSSNc}Gw^*DzK7T(cd8P&lW?}qch;xZh`Rm-x@09v0SVgvS9u6|iQug4LnFUG_Bfhr zS;^!qQSC=pguWN-#F^j@TmtyGQfK^HgZ&aj#78G!PY#3yPZe`20w1wEe;9{aLW#MEr?y3?we?Z;j9B|t~T{% z&aX!bmm&qO6_b%WD0NM^3j1zOER8wJboadPs%ddc#Ls7iYoR2YWhbp=t+*mM*FeUM z`Xp8lv~&^e!;Dexm>dd_*h;6|@)jf#xnGPxcFwug@S$W0EIm zzJcqC54EwHCmqDVz?wr?#$JV6S>wB_b1%%yNg<(&xYoJ}@y4bfEl>qk@0%4|(wSxM zOTc%>3lGa1q5He%^K1l52-nV69u zi<5OEc3v&HhY1k&`X#q~u87rC0Rd~tkl?09H5vBD;52BJNzU3CVF8_a@kU72=vaB- zqPr1{OS>XBtK0o(eq=)g1nvxJ5m7wo<<82@4mYDz9}iCv)~0|hlLe)9*p)J&kk9<= z%%m_XSaN^8bH5oV?W-d&PMp#l-=pFanmg@`#p@?aJxd2)d+=4c$J#c&2t2CYLD(u^ zOz#qEO*e;>~v!X%QepcfCgpU5tqW>rqvaDJ}3FZsA*wF_+07qLVEN5-9 z*NAK4&|RftDuuBtU4CQ6$RKorLC)5~Ywh!f#p7W7_Sooz4A4D!4OJ=W6G+^0{i%FM z7Gw**+#Y3~UpM>9oU_!r`7nBUvTgnxfF``10ir7R@lSlV-V+B$M`BE}h=!7$lCU}H zKc#)96IpO@5V3NhCEZlQtE7#bBt*m9$229F%nRK8v zt30a&k#m#C;R_s(Cg#u|0M+V)nCsdO^oUGf+vxC&x;L*gGOLF+Gz}nv~y61sTnmh^zzT8s%zl))%t{_K`1< z^`6l{bXarj0oofAG?@7{af-f~CsGclaQ|uhs|AD{Y|Vi9T6@!;b)B#E)ZXte;?mi7 zuu-F5v~{@{xPp}T>*ZS*wGS+iI-c$b5W(Zh_Rldi=b5ec(i+DLpa5vVs5CJNW-&To;zcl6;(5YCkQxdGY@;{$98dPHQJ`L*ni_u|{v6sknX z69>@ft_i?(zdR5m>rWyb-Ne1u=6-xYd)*;7yuHE>^22|*j5k0fba#*tkXxqm8H>I( z^wX%W${dP^W_5_YV!ed@vnw=aMO3zTH}DIW!7X+%-p_S_RKu08eSY$7WNQ?P8Y3r{ z?b987IzTz3DXV#!R5qkF?;7b^+dNK|O8wDA%4z*x_sCCGDNz#K2T!6e?%OT*2MrYx z*>Zxq%(3k9SL@9Pn8)$g3AxNHu&b*hElfcRM3CSikI0^v+x20O-gT&JA1dwSgVRjb^kEEtd#1U)D_aUb z9sIo62u$p!pz$%+1qG>V1RX_=(T$=i7m=5Fs2WfC-$ovieDBCnW(_l$-x97iKLdji ztaC>RnF>{Gkojy`!k$p?9@Ap>noSTQvu1n2)+Zl3(Ae%%MPPhFJMMfu zja4(v48)()@p7I541UINa_jYaZboI6bwt*zf2M#hxC6X(z>nPrlK`18=e@LPALV96 z%2Ub34ueJaz7f#7T}Z2;_i-Cd8;jpTlv%z9DwvoK+5Z}?EXJ1(x#TSec_*J+@P+R= zRw#p|JvUEnG&Ll!>9U1Wz;6EZ=HuYiQy_rWwL!KzrpO;STRwVtg));K&HKm?PhzeZ zn~_IS>P*T=z&B*%r4P1x7A$?xz~SRTuje+;fr}8!BPL)y3CXCukLOb|xl_Voxkt)0Cbi5t|*n(l_K(dAc0i+ui2C zT&Kwf!4>qt{3=$r#$wxdG!LzWTPBV!7AuakV~OaW!XLX0=O_(S`g@iZ46Z=o>{k#Xhi#GdaXGQdERA9 zRx5-Tp6179tHTb9D1w>v%mXDVFhh%sTp{vEo^R6$>@c3AW{MLBZP=Vh*4~Cp06D%{ zGZuOoxz?0pl6}-=Zj9-%g~}YzEsrTP1+Zbq<&;tPNexEZuDt+{V)8m)+7>|(} z(AdGQ{lM@m12YNvueQOv3d|ix#F0mwZ9~Tw34X5yzghb&Q(egko@E^S@(IIMN_t#x z%`e|ua#s$x+Y>@HIR=Bjo$g+!i(a+;&<4TxhMnSIvhguQ>;=xj9%|j8!0HOk-MJLd zR2KsW21`ZB`#7i=h-J~9K*F9aWnfZW0e?}%#i;z?n0rld3D}BCV1>nk%PGD%sC82^ zpOv9g@?1);IPXZHPty7NV)B;KZ%eqUnbd3#^V!yx1p9GzfhdK&A*0*R}Z^H3LPjNwu*TX9q8u=**Oo81RE; zUEbQH1$vrY`ld;fH*EMq)PJ;1T5(4n+t&_S2D~<&&lQC1bN8@@9@OJh0~3Y3HjH`it_*AWlgWh`%}+RATy#;~YMyAh94vHg4JJ(HxB-_% zb}54-Q{oaKJ}3ArsHuE7c~Fe-WyYgp*KoXR4eqx%VX<++`YJNLApw}Zy>4@E=zwGl zkb5Urd^i>CL$NASWqE#91s-C}+)BO-^EvpCWnH#!=VxB>9MpFh922tWE*#j_zHMdN z(IINp$43G0uVoMPX8zZTw5Jzl&KA}BcdQ)__T>X?_5P!)*=@PXoW{Ic&L_oajlq)Q*WVy4B00EC# z-C1~Y0%5mMr2P6u z&2fDu{QmTu_r*Zt56O<2V}XVt>a3tgA6uC>^=a)xA9e`uB1N^Aq3#$!kp2(plC%?s}~wJ9360mR*qj&q8k2wW?$%$cyr* z19eiW+<{ur)WldXEdoRZ8K5FeMxFmNn_e zb-m{u9gBgXbV$I|RA(^j)kB{b-G^f+_`O9;7a*pgqHA8_1Q>VQ{=>1pzi0BoRzjY( zMgyckvIEQ9bY|LjrFy-(jrNf>OKAZ4UGvMI|CIfkchKI!a3#mGu>23enOyA8L{ye)xE)e|!_L3FfreX_;ZaMj*5;LF%>X5~vKwh}^sCP@L| z*TthspMBX1r6uBZJQHrX^DD}d+G;NQ*>P&JZgGRA=+-o1c+hthC;#cl{NO~Ccb(+s z!SH2EH7c7G9zal-&e`8uT@1Dc$h*kGcZE<;%DAhGQkc92%Bk>ZJPOt1lyx*DA4(JU+x9CU7`Jvl7=O==<4jDv4%=t=9cuOs8-pNu8A9K4dG_Ed3xq}1 zUUB}$vUKoTiwM5mYk9AUFHZrk4jX$jH{=2P!_fet8Q(3p&1v>0?$DfHOCPPnE{U6F zd?ITVpW2R=A4Vl?8^9dGM^v+#aiXxne)7YI%>Sy~O~WIsm2QnZ#OCa^(sKY25!|Z` z#;qhc)i+LSf>-$ousHxg2!Z;Njpca@u8E9u4;*y`uB|awUK*Ppge7$NgRK0_>l^_= z3u_}?lAxfgs~n;{uEo;+nwKiD&TpW?b*Jl<-x#UV)RholQDgF*(@m*CKw=N`^G$77 z6NP#uELf_-M_MHBZ(w0q;2*P98oQ3cC~^P`oOx)WU|w4Qs2cAdG$ssB4*@F3>^DUC zfOp`xm2}-T#}`dxmmOdy3vB@3R37tJ#*oUCizCVQ38RbgXtkogj{I8}EKc~}WM1K$ zHJ%r{_RjGzZ$1DB zd~mt4wZ?>M{X%BUD(lqb5X%sPqa0zf!2Gmgcjek&N9(1?RhK;o8ZmhFS`%$Mie}Bs zhd(Xr7~x&OhIE+9XT!e%#LvcVqJLa8$LI=_bM|=PN z)ponRSEdb6^F?JWjV|kEg}6JTYNNW(7YTEyb-Ey*ArD{N7)1PuS4&4hG1>~DJBS<_ zKQ&4XY!d_7WWHv3vFHN1yx=0|GETq%w$Z=7s^e9#-9Lfw{aPp#;~j{^$u`> z@oozeQYc@TzvIMx>9`$ej?w=rafS2TDbG*D)a3Kpkrd*F;(s^{3MM6DEAM{OHnmi*IX0Ih1pyX|vA zKQ^tPMboWb1gK+ig?PRMBU|fMtyQV^j32gs*qGF${YQ&Ei_i$LB?eLn5vLZ{(%uWSv+?A$8H=ul5ETfEH?&YtYMrc z*?ar4Q~EBVKO7uJ3rEB!IlBIICJm{phL(y&>sT1yD&c{5WR1<2=+6BIO4eQ`P^Z+?|%Y{VBF zZQ9a?(=~way~j63QLdU|uj46E<8``c#$f zg+%kr70glt+Lg}!o^6`C019j)+6al|w(O*ca&efCs=I~(^5c(XM-+@%{=vyt*s4mjEa7XG(lx; zQ@4I=vI30h(vFlKDtM(J!2qIY0Z+!1Xofz z5||ZP?r!T@SmyKc>64!gZoK$X(EPkQqRzX3bz=0Be67K1hF!vzjqD~Jgvu`d!(;nv zLoXUx&k^eO1&5m8D~D8U@3h}&!@~lUz8QkrvawbK(O2wWz;nK=89DYto_Om@f{VQ#Fn2u_| zMiRCsZT^(t;%e@RZC5KSx@=&JP0eO2W}`+QvZ73G^CSQ9f41BAs4NQqn$IQ{6$^kh z5~$~5y~Q`C@V552%;g+*XTxS1$BJ-~H2*#?@;wT~evsqno ziZJhriWY4|gVR}%oz;)~m*!PCGfTrZSvCY&P@J$!3W=vY2f)2{mtTMEh+f?wzDMWWvZmrXgHNta-nrrZ)DhEQsq5^pB9E#@p-4^2gv43 z&q3a_Lr1$+b@e$qR8+H_N|JrU24s#Uq>9T$yl=8$Z1S`8XJYC=WUBV!Q~=P&CPC0T z)k`>mIhdy5He}J65%RAAjl=52!+gHQYy=UhkqK-re;J3JDQ=vw36{{Vr_aOo-a+!F zNq}uQUtg^=0E&3Qq;QfST^VGel7~hscPHsd$BDi=~96iN53`=uNfs6j>XHrKHeaMymEu=~E6la^`0+4qQ&N z{z68{V%>Y*8)$C;I=g;qUIiHGXJBV48WhvkgYlnx9xr`Eu9;QyoX*)`r2Ey=a@C@s z3_X-r0I#gM(+3LAQ0I+d{kiMV*q;`+K4(ftR2aUvhc=&MR$uZBskOj!uyk#ERnFq+ z|ny1x* zVMJPlYYn#MrlZkfG|ToSw&hz4h?Rdiz_R=OUR6o#nM(o^v@;BK|PJ3^^upcp|vH;pG6i(Be7{XRV?yp z#@=Eq_Rfkk1UI`saadII6gQ)W#@=eOKm+ggEs8y&&ICSY5}dxxF(?(%EiAh6deMHT zmNID<&e$xnKVnM9nIoP1I-brhSfBJr9N#*)KqanmV;ORVzd8}{W3F_@u zl}xSCLll!|;O~K<*>_^?&kJNDLRp#2XhW3$x;FH%^*~8P4U*@kz7obv32I0yY;|#y zF`8nesJLKF^I4y{3$NN{#GmGdDQpivFLm4R(W2++VjLoN02?b=#{6n6f4f5W7xnu= zqr)PSJZ@^>FMw$d9hHkE6^94kO52pXD$_Fwo%dXIm^wfEYkJS6r^{3)02RveCu0SX-C#5(U9YdbpANEDEY7h61Rktd*9mhiQ(c~Ug8UQsSPjzdGj)d~98 z00U}m+?J{{>NkcW9-@-N!!1@{UQrx62x;-IeQ-4qfV=v3_RK*86Q~kiYA%&ET)xAu zboIQdpHN4~ekj=qSe{eUv5a$Lcc@q&J-Vy~+a?Z54qzdxIpl(TQLB=5e*H&;`4 zJMZxE*ydUN4#Y{siBxITjaTkoWfRAq-EXz0nyM_|V<$>J{ebU}((N3lgPfxvSHlL0 zF9x{3&6dZ7c$+kR2XmH*N9_sI6wYu;=*06*&?6DYzDQW+ug_Dm^*{c?nJje!LqfE^ zNMLx(emZ~s$(k{ncmTHB!aeU-lEQAmU!=Jes6C@U-mgdm8owdYx?8OM`bD?psDb{s z)e3Ij@KqYeD*otCM`lm&&PJtqyS9TGvKmmKGWQl%g$lp&{8^gLNs{q<+2uqJo}<@M zahrOP@I_oUMX%?Ouc4oDBLdna{+vqwA)84W7muNEq@h#-gLVz5HA5cWpj-JjtFSX6tTQ}&rR{`(mM+dsZ$YeR6@nx3Dzhb=U7)UkY zIc-6@Id@bz@!_3NyuIFS+xP~>r{(GSb&TlJTAUdax-OS?f&4-pzQ8mozhXAtFFmJZ z_xyFC;{8MC%T$FQjBAzfcew|-Of?gWGM6tA-cg4JI z(YMcY5l>?=VP+$DAK4?ErXR+zxwrB;-g zDh<;nSrPllV@4g8q1x&Yc>Fef=LT)T+Kf*L%^8+ogK7SaO;~^x!>OCIpoM5#gzO%t zmhiKw^~AP*_jYy9Wed7PzW}>RNXg@eoE&Jr?yQ;T1?6^Z6}R++;meL}1^wp*I4R=u zt6#cxa0^2Tl(2`rFLe;&6O{yQPNk<@DtRe7XW2$0QIarb=T{YW{Y6KPi>qW_t3CbK zOLu$id!@QbxLLayY!B*V8YQWdFT6Is^cQ&x}lLr!*GHzCp8M#E4E{i&X5uF=;| z^Gm)9H=+VQ1r}DmKkW4(9W#j>6fajgQO#lSv`y=oH$KM|@OrBMyTpB(X{b8in*X*W z64v;FmN%W+xhN$Wy+ot~ z*4@E}pajG6gt>c_3sIVKuqXO+X=NnhXQ zvpv1S5UFxlJDYWh*psBA)tt$9K#>H-Ctfj;uq64e-#H`!Id2K~6>Z1CaZNi7Q|M3P z86!1sL`v_or4od@Vii-!AEK8~{)P-SZ|+sEx*CVRdb04kan33w$mpMgQq=`qyKA7S zpum`u+)6_4@+SGVYRn|3IN$NzeqHsffSM7B0j^hO{YAJz(Nyw2iUspF-RPs@L0V*u z9cWaE(GceL%row?Zq_MxFl-+MY|jmNw(vGu2v?o$W;@Kv!6zD4c3YhyOz2})XKb^bvPeaf{D0Ma>EB^-u*xz{b z#_zjw9O>jv=G&aNL3Z_iR*sg5xF0o_k|q;+;uB&u>&BY* zfxL60`k3-9t3SN-v$QQ&Mh$HgBynmTb@!EzWq9#VJmL8W!Z4X*oZ+dVHx+;i;mjwL z)IW?u|3t@`-BAom9KfW^plAYvoPho~NS^64i~zw;j%S)1HF7SC@M{Oe&7;#cQslEmxb*7fZJ;29(&E8>o`!YGx> zuT{U70iv}W=xh5(Lc7LSRz)|#jBGP`mySd>W4{M8whWJD(tZa`JKnG;x*IQF*BKST z8E`e71inF1C4gVe8?<~BMz-@aL(4H(RX3lW?)3n*(-x?=!%6PvnOx7rCp%g4(B!jc zAH8!NZJ<-nANYNVkXAuH5xN-;%&?T56^%2z^@6_elSpkjuVuVI+7~AUEb_Tlw4!kz zSnR2Us!hQRyC%7wvuk?m4kyg%*_TiWVGN5%)IcT!M+%AQ>txzQB+M-aQXWCsu;9(< zt^fO{rGkqBzegnr;YCi>MdV$hP`d}uPiVa<;D+t?{a@Iq|?6If`o`rhxkJ2Vy^=sV%onF8VPpY6OU{;4va~CgS1nM0#Ysqo_vZY#`?E#&|p$UvZiz=?Z zenH96{VjI5#e=6&yI7RpiOxz_MI-Oy8$AQJd?oQD8>qA{3yO3U84#u(y7Q`=&WN(MyP-vjPrzi)Jb%tltch|pgY?}@XNcO(exTQV z6_)qG4M{fs9zVUNzupiWsD{pFU|rYBO1zj~laHM~d-;TvzFsN!oh8q~2Y)F!#{IBO_Frq>Q+~~UY9ZE!XL-Ol z+wsbeqtF|g?>ZfhETL>ZF`JCVP|BQgN+;*IlaN}DkMuo$JN(Xg#*1S%jV{~vTT5W2 zzzDrO>U}7{ws`!8BI(g{hKMWvy11g(HG<0(wDH`S+j>(;FQjA{K01FHrOe9biy25M z#f!vQoY?GeX9X&~=325D?0D9|=f~7hE|Vpd;P9&>y)&B|&pf|eU5#c>y;=9`LEky) zlTWO?^h~JzgB$89Dby~Xgx^p{(v>USr(*j8Gn7(G82)>6i-$&#r`P%{{|){t7wIF& zpVS>~n9iz>cWKcv*=Q%cZtHfq{6PTP>N)o&^U5QFZc4gpIjfl6FTG)61^4T&ZVppo zYFh!T^7uL$*rJ=lDWSq!S|Jik#U}2O)uTT~!w~aW$mdmIsJ5s=EU)CD8bcW;`{1~^ zhpQ81Mj7y907@-{eonw6%?3*gUys3m+AvgUYrLE`)bpr_B=cn7jt>*<64}mFE>7xw zkx!A>+igTm*C}qAdl1iyf9TlGKuN}M*MgFtDWxZoQ{q=fSR_w3<9%)X?UKUIpInyU z(D?7x4b(F|>$PqWSw51Fq!x602lL9$fMm9!lHYngR(f9YMHX?N{N~90Gd;jQ8^jn} zFHONq-|+ah?k^uH%#FAKT_x=)Zkz2$nUeRG71)anH4+!LMQ(DUq7a!tf*fU(ma6$v z(Mp$xDJriZQbV@f0K0cLQ_fLMjNM;W(he^dL~|bfdT`^K4?{avl1%&KWqHEWTh9E9 zj~*e09&rPpa%5LFHpzlGZUL5lbo>Of|SX({^2Sp3$fcC;xf z6Cu@Wf&t_rJ2zQ)J5_!7ZUonn>{efUeEHqMv3-Ni)+mp2y~wPlH}h^i0A74>H`JTt zCNpV>pt(xfti!j?3Z}oe=~sA?{=76vlTjppaa&6eWWUr`;nVhaKUT@~?&7>30@rfWmC?%d3pJ$r zCk1h#)s=NZzGqSKndKGOir!sOzLHrh+m&&N*w>c6YY%$1Y- zcY=wsfq>VCA(b%ud>ZBzu%%&8JzwxzuL@1$z zLFGOJRzRH$t+A?;vgg%pa`UH%U$^+}n-Z+n(K}j@U)`iNofa%DqRwBPT7lZ7N;cfy z%8yEOMZ@k?i$0?0z5AT*R?H)X2Llg8`3273j+v2|C)CLasKq>sPZYiI*3VBeS&qdE zp9FewQPxi-HjO&jR7Yf&p!JeJ##-|vwpG5H3>$5LuL`Q`8gXGuD`=M;hbG^|M9&Ll zzo&g9@x}GAF)ZTAe0Hvtn!FD=DT@&#cJb>IkM*yZeVGk9xe~E3RG11Ghqz^No@wIN zlWz4+t6RK3zRhyd8vWk?-5=+xUYk3`{}6eL+16S=;bZ4&$&1^gwI?Qo6Qe;Uij5psJ62O*&jdc*X$%ouGl`?T>M>EO<0lGUGguu3$)#CB@Y?GBm35 zzulLa;Cq>-D<18Uu2s9{yx0HQ_8WAtK9rV(H~bF=Ic=t$#p&(I5ZiO@YNH}}`dffU z_&k}z{1W%*db4jg6_mYBsU`jc;-){3J4-v;y*IoEWTc_ExU&uVH7yR>xS&O^Ew111Q$8%0?IXKtz-zYf2ZU7PHfD7I=}k04pSM5%C>Y+h5Jk$bk|VH`e5k=Z2%r7#Lr}xr5r0<`9?^nFhZ@_X@tB+F#7dEJ%NZfxN<9bQS# z7S#x@w*@R6ic1 z;MZsUWc-x}{nLkVD?rJrc#?>_{%=8k^W~ujUzpCaT7ZVHOoC{%K8RRu0K@dT#$F~W zzIjB`$7OUwfAHI!^j!)UCZwBQt(DQ6A<32@{HuT=I-X}Q(;o&%jjVs1qQHf6^wpuY zW8OE9S}pS2PUb#{GBhWWSV2>-?Al-L6RVWNk)&c@qB{ zvMV?k?)B8wC_C>qSEsD7X1j|8&qMOR4V#btie2%jtvFDj=<;!XQ(>X6)5-31dg&zI ziwpk6=1f)OrJh*9xkBx>NhT#yFRdybzjB9nB)%#+Bn1!Y{8P=@r78c!bIrT^F}DXdNwYi74G zs4h=d@cn7^U(iT#HfO>sD3u$z1@@l>#Jh3GHv!{h7X6!$&@|!S`a2&cL^tGXz4JDI z%<~bR@s3=4svy%sq0_X_@7xYH0Q!5+OGjA_Oar5$nO&(2JXv8;z zI=}DtOhb|RB`=~K=^dWfIAG&$i!G*bCF zXV2o~*R`eo2LL%h#=me8X_@A**Hf-KwhW6(E4f{7ghmu)=tL*)StFf4lP4g%X%1j? zur&2u;GFSqB{Vb%=&I0lfS=R|M3{iT7KH%OUXVf63~N7SD@#cs95;4}%q5v&a%+vR zt&YMd(ncESUojh~tLOM9Qly4l%RWq|-$Wlcl<|N;9}E z9{r89xCEGz_B>o1@~^HdNhShYAiqxa+LgeJ>k0JIo-UdSP3~|(EYP2(Hn6=Gi&fM; zAfFblD*ZpOmk8?>V$q0!aY9pIkHHBjor0!{hTDl~0wkh>Iah-Abu#yqm6oWq15lxq zLmYftu&!SEwo^q-$M=WYKAsx>&Ui2AC4>>{0V)UjPC(gd!kuayTFU=aA<1xf9*l%7 zjitIo)Pg8*F0^E_6GKJOxVQ*@LL*;5c5U+#h0PfP1(!S_y37saGza&D0Qkjf}$u;9l=!l(koCI>*SCKiRvejO*lJQMXxfHAcX)Flz9RWSGczY5P}0n-67EYneqmOQ*hwXO>Qn| zt?nJsv>X^ajH<09}sh46&UqB4DT`aFM_R(eIw^ ziDG4?oKBRV^2o}X5zk;RDV7IFeO0PVMc`;gqy>9XZ#7~(n*DKG%>|f1%+67m1cC;z zNJeB1G8l7MOp}4ILK_-nQ9kAVEp3xQ794U|T7=*#->; z2|l7v2f>P8f*EXdZ(!yFpy80>f+z;~R4zq9Cjqt@O*ota+c8_1=%uQ3C*Zl!Kp5Mo z)>$>`CP*y;$*Th7M59cO)W23K2vI(OUx+y^_)5=;Rt@3F)4sDV``$;E|{yxk;lXX~>8~3!Ttw zv_32WGVP#?R^UV<1xwBcEERd!Na&MKvHt@SMbH{8QpD$onr1UK6jLBd0T#wKR9dLR=Yb%A#D=;m0s@?AT?1a3)ac@C z4v>j5X94l+TRjsW;T))oB<|YO@(&ngjT#3Y0T@asmm(a56O86OQi3Il#u><0q2{sF zzzHF3d6lvU((LL4aC)N`oMni*d{xc^utQ&iDFo6XkW59#B#lnoxhVWg1FzF5KrnHh z0**^Hy+o9x1(6(Zdcir#mJxbKZzpbYRla2kr1b1QZvth~^=#@~Y(==dK-ZeFCs2S) zWE}v+S-3OQ$c)Un*PBjh}QDrJ)5%{wg(CVzhwoc z6B!?xw~0w;oSq{f5>1%xIR(?T1c0bRwxCrdj?tswj!@d(7h}#jBSB@D(4t>BD zk|!8U8<3Y<2hO0bYMj@(2^XwzJ4!cmcywg7z)4eOn8Qlht7q{IaJiA37CRR#5w!#$ zi~a+82WjHCjn1+8NPY4`xVvg}WCH~uzZ1M+B)(087D07t^gSRs1@?#~n8UKg9I$KH zXUh_&APX!`_b|5)VnLQ?1p`5Q(mXIPltXa-2noUq@A3%gILc|1HihEB`NI(l$A=>2 zEY6`dN~&ry?qVCt1ENb25=Jo5QAE@Q;y(#AnWuop;Hgp8WX2M>G4<#5KWas&Mjb5y_=Y z5JClk18UH`=B5tU=B^bjR+Ot@yBa`)8B_r!T69hLFQ&;8DiCKpphn%s+ z$P_p5;DC6=&LZWek*S2WJlHx!^oCnA56&Mle~Z|10RQ`Wr+0CNj?}GPZg?2^1|?&MCnMBqr%&#)ms#ikhzL# zh*Vk&<|m*Pczo`PmET;(Oh65 zEM+rldBLEQKz1v7Mv=^sX`zjrGfx6C+FQ{m%PO4RI-s7SU4xVkDUVhklMIS3xWj$G zwLm%GHP~{GzRH4Us!E^zfAkRB9RwKzaSI@W?=V z%p&$i_oe6}nn^*T+tXkGfx)36+;nmT>Ow4#v-r4zrv_+2Q}+SEMH<;G`DzrQi{lAF zM$HPgI+QMfupoH{i~|K(q6dA!D*-(O;YEqC3LHBqCeV_BLjlm1hY4!9`S{z3SYrgG`ljj#A)7Z40vg6t!j;mO%0Z zEzzPV7=(IM-HPxQG+uGZ2!e!(qM-`LQBMSrz81gS3?6SZnSm{_Xk;1^FgKYBxHKT+ z8I)c?HSwqD%bBAGo7$8LE+smX7wIB0Wb)Iat5S@LbnxJ{`&)>BcTowQJW_O)&XBf& z&>1XG*w6yu3DH}eWEeW}CK}v40qjWX=$@6R3F$$O1QmpeBit$6cR&@5iq9yiu8hM|l-V0F?tET1|D@1Z8tT`3SxA* z_2WCum2?}nFvE>@48qSc@Gi8H$%|)e1GA`z%@h@#oB@{?8#VdXG$264gsazKmL;kQ zM0rj;D2);6`dLi01TiVleZZQCAZf0gN*DrSm?A1VT;S2e52gkXCEjKfN=Zgld{Z5L z??D3HPEn-*7>&k?E-+6Qmq`mBa}n(gEeS1RPppJRin#1hr1)(UeCALpq;)X+s%D~v zqPQGHXQp3pxhKjS0fK5uEllj%JVD&2Z9x7^;SdzBLhI+~O-o|5g_mp!j^QBB zrbMj>IdE2F%{l}T14B>=L`{c62-F5^%Pf2@{%(uD@#I+_@N$5W2I@PLLnOxPi#I;e z=$G9QrEete3-=Jv;!204rwAcmf+E7H*`oWqVZ5jpL4X_CeYIe-e5O?>IQgR7TZT2a^d;bN&KWea$+<`&WlJhqszyPyLzU? z+YxyWl({glbai_U0i+`iqMk&?dxbVvJdLBbK&*6? zP$){C9CnCVXX)D%j6@=pT!1dX7UmYCtH`Eg`W%T(O537A22v8im+J4R7AWf4sIe4r zp22uiM%qF3EgMoOgaX*Z_zf+A*^aex!5)biL-{>FPDOOya^ERl8si&c3LSStcS zwQ5!Zb;Kw+bKoanOT^(ptpZ^q1bOVfr|c>&H}7DwkYZ{9VBm@Fv;qJ$2ydZIQb#6? zw96nLfRHy82TD4cgp9P5i#pt+!eM(rQf!5q!G#xc29yS~D}wSjsdxn@-fYzxvFc1E zTwPE^TLHQN{5kp8#?S(2)LCG*E;72H=;6z)qE>S?(!^Z>LT+%!7M<>fyHD{W2ijl2 z1Q9dF8OLc=#e+O@WRpmT(bx@J0L`D#-2^0BBpKd)HGxJJ28puKGDsh2SM2KK%1gk4 zVF9SHvBG2+39Ck^D#AP{!#vD|!VsLPSlX)R7=fchg?&LBsdGkBRB+`YZvQ~4&Qc2WypVedRe|TjL$HVg_6Ujq zNI0?4C~11FcawQ<|)4v5HMUV6*}ngi%u4CCnqrqq}E33YI1ULIaGnQ1OZJ#1rT8* z%Bb3*-Bu<0d@~^ErjfOa11nH5w5KsEgpB7DU5N>@P5eNbz@tt93J(f;d!i)|#t@1C z(9tl@WKe9816H^4TqF7_^n}pMN5L_44{5oq3bV2#V^ji{Cyy{kAr6{_Ch#5378x({YgB@O#2YW zQ6Bs^V(qtcGjMQp7n68~cvDBir<8ymUsV$vQ8|ebhFpL(ihjE+vTH~afsKYbCkF-1#=XeE2kSXJwLiZ*0iV^k>Rlo(nd zMgR;I4bX$ty{KqLLZHMgpvt174+vDAM_X1`O_-y%j+7_Jagd~dp4Y*HOLq`)1RL4L zj3KH^*j6!TMtMM;>Oc}lstX(`LyMpanbM%6Anyn>P#r#76JHfH8B^qMX4Sk*MnotR zI2L9pLHUe>8__$K2pAlB3j51wLsfdS!r+sF`GZsv9e8w;mptfIG**H_JP@}On5Pb- z@0 zn3*h+(rVGIN<%+dpUDpE8r*yKIxv2qMN*hTSUDTs0h}NKUY+U3pzKo};^0X#!r%l$ z6`6-Lf*xS!z^~>(mQ~ed0A){|gCrGSP3;JHY;6$Zk>5#RkcWq;CM4!JiOwuCo2HB5 z_+iKFDdRX++`c@ki7SG4av4pEdl~J|0EFQK#T3T~8afJ^(#kY*yYZs@z#Gsh6wFRp z8EWDpI_KBh;IN5d2>#eH+ycHyfcs>XTZB+#h?Ks^kBnRZV>76=!H^ex4;VMdDhMfJ z)2M<0j3d258OJq-D*M32H9 zf;2iLA}2blfdL$H@D_H`&>&kda3k0zUFZ$Pb&gDh#&xV>J;`mbcY+6^7z)J0^+K0R z4jUekbr5f0|EQsm;0;OyaXqdCn&BUj@C0ZIv1$)uO0b2hcPtPzw&rvjH9us&enuCM5ob+hL32AMB89|ich0+ui z7$C-yh$^0ii$W#$f~AkHo#<9n*4?|y23cOE90!Kmv-*)8qp22y--rIAAD5N}mG7fW zN2d^x57c`RT2T|3M2AZ(5MI7W7^=FrQyju263DKV0Og0xTowm)n3GFYYWztgcm z&ulpKxO`BmB`ER(U@-yNuSzAAXqyCUJFEF+7N?!t9W|2BnkpiUfJ}Fh6O&?#{7Vz) zsG;vUWI-%RAKb!VF|!O&@KHw|sYsy;qxX&K1?+{5gSHOX9v@K7tG7g;v+y(YlrCYn z!I+7a3VDxqJrpO;$P5SijAIrZQzZya#mgA95M&O-((1$1cwcZl^pF8girf;+mhSzx z060#Q$RD!f&Bx>iCLe%3g>7{7jst&zc5U)uClp?0rI99@E*wS-il=IkDExV~39E+z+6f?w)kJsHoW zykU-BYFrRSoC}?Lo@@k23RyMr>NHFi;9LCJit8JJ6kM$2$8h>$@XjnQ3DG!%XTGh| ziZP1@sHh*(mm0KwY8v41(A#VqnI5^GRuNxS?+c_8%or75ZXjxRHJF=&v*FV7{f=Inxf|v zZIwO=rwZgb2VB%sL9XwX034`wa+0Zl2rloBFMz<3pfam}B~Xp1C8nZ)m5DAo0%tW* z0u7ID@l^R3uQ=HH+MpIMs=+uMInQ}2C@zVfzBn<%o&6cZs2Z{;FiD?GL_>8*x zq^eAbd^ZjClu#-EtVh+Q5iP0`VxrnlD2W@63B_bZvapApu5iE_ykZ8ZQ6)2mSj@( zAacFxge-WbgwatQgsBy2&SZL`83C3>;)b3E%_U6cK=_IH2M>A?$`Lc#gZv91aZv&U zsB5(Rkd?<`L#D7q#M+@;<`GG?2b^{mcQ8{OD81BZ7gF|(vf?^>76iIUd_+BWcmyO$ z)e}&=qftm7ki46OE*RFZFrx$vB91}CYYH~lYW%D|*yJX%T@1GQKU(+D1;G0xZdHnifj___?`p?Pk< zw!l2wI;KTXm_ZuUnJK|xnk;KUi~dNdYfhh6lw53R`i7ieCt!dI#9=8ApQcLrRNZOj z(R{Y;a0FN{&1zl;H8n$u0mL=lWR5@(R}8BisZ;m^=#pI~wLAIcNg5ACVD*Yv4o$2@xIsYNMnx0N$IClz>*9cGptF7%v3Iqz5a>)7xAF ztu<8pEli0;qN$hC9NjfN2wxWU)_@|JYY)D#tZ|NuT048RQHU1>Mqm2LqGpjP$|guv zgzLI3no*&m&XnBE@F^$+C8Ya;)_wIfA~@NpM65}DtZFD8ymFzKXjRD~fhjQ`2p4_I z0@(p)3J$5lJe>|JgLg*f`%KxO5(U_D4&|?e32mYD8^_Yqfl6H%dNBL@7|lsyh}fWr zt#!cF=o)88%o5$#g!h!4nwq1d_mUZ*WT~0K7#--U_;kWFO5V|SHH+ro{ z3PjTr1$w0^Rx;P9%5Vf(VD)^`4ekb({Hq53soSoPUgk<~^Q-~4LFPepsPPP-&`Usm zX^doUv6_pc-yYT;dbPzJ=Mgd#(4zDb?^Y203lTa4Dvh&3o(D`C)Im#+#)C;HDAT1# zdeBC~4UXuDQ#^>x2_Pqpc+S!OR7F3!XnP|GMZe+7%q5Lki;iq>yItrYiR3~h3jmV~ zJtou0wupO?fPNYcbp$;mw_)#XX}RrK!J<`>WkyqoMtwQ6;^-;|1zj~1Z%on@@7bzB z;EWjxW-YoKNHrDB<&9w>qS1g5ZywX2R(!{$E830pZxaGy1|ZwVXIggL{O$vZV}d8e zewsLi@^%LRFamTN&qY(a;+XMT_9a-eV$al>l{Hr= zwcGSGIWl~iLDdB92#s^p1FJGPnU;B`0Aev%>^itENbK8Mr@<#nNsWd&LgW$?TUj=R z))Pj0jqbThrx2N+7gWmtKLpU8@nR9TNc$@P0OQ}7MXNQ)biLQ0NFswmbqvt+8qS?rMljQ)vQeXv z0Rk|zD01vk-=TkVu7XA0_M1cTHHrUPiP{(&ibpQuNYDl0YN1cOlP#c*aP|^u6^G#k zKR*~65=d#x?gQ{&vZic_s>k09Zki_W7h?!xe$gxj!?M&KmTDz@qR?#$B8c;v2xe$e zf>JO*C6mTz5;?Y*W01i^vpIlQqCNS$*Pe-K?;jF5uVT=;I@W| z)FKKbeyPzhZMQ(KNy#sRv7NYy?N;il1r9-k5fo_BJfOYs7ImtMfK(&fZ%Tk5ms(Yn zJIM3#U7W3TP&KfQWB?Q-Mu#(vlBf$p$)Gx++#x4>UXq_ix`60g=B{SWbX&nF0b7!` z5GNwED-}j;s#~Q5tQ_l(7f3I28$=2kdCBZ>Ly<_|TKrv_&&b z@0btOi-FYCErn;y!qdj2vl)&>`d*@?#k`&_K0Ny{#T*WCD* zCI=INK*lh#4G=O-&Ndsdkmz*K5_G${USKQ?PY)YHu(2|t5gLM|xnt3<16U3a0|=8r z8^OT8B5-e+gAhYQgkc>7@G%XJNf3^!#_I+W4Dv&iGhJJ@cz%#)@%W-cr$N~RP5(kL zD0rs72FFt~04uZ`0TfoA{ z({!vhI3qkSlElaf#A6;KQb_GEF@vnB*Bg~HDhyf6W4Z&1kO967j*+TXD+#D;K;pMU zn_viW03M^o$K+w~8tTKb76QRI#w*(CP(L35g?-PAV~ySydcFFi*vRJE++yx-=yY28oJ##snC! z*N8b-WOL|xCzT#=#*TGmng-7V2~jG*5GK^9FjGpWlThHnkOC5DfB`iTY&6f1eO+;6 zn~6;xMRsJAp%A1}BCNvaVp;IJ0ys#HTQi+W^abk&rfve`OXjEd!J*-mM{4cSBp|qX z6AxPe0?BWSn~mc2Z7(*dY_p{>M@B+Rb38zy^EZ(YDa>L*eUq5MaE0VjN`yBoq6%QUEUD4uDe5d!F^t2$Wn!B}iU`s%&|$!mi)IsQn>;!eORTa@8d4lJJ1W0wE*xxC zxB{AUPdz#6Q~+OFw(q@+Noa}O z2tER>@Zs;|tRCa(E}$en>1R5eWDeXzHzN(e%J0Z?*G zL{h_?vM4bp7WbD_fGD+t){1GSkQp?!H zMrlP+3){_LZT#E_q7vP1^fdvd34|p9E0|qISEU>lr~t+$54^nFSW%=A&)%Xm=y~ zSd}FSP%nXeW6<6_hU8XZgF&Cu<}k&oG#L;3QW`AG9zd+3fgCKYiisBCh$E7HPbbbEDtu^8EPpJ9u8tRC4d3FFg3)_ zXICcz$yjvIdW*5vfMVd`u|xQU%z2rB)Wr&*PQ@^|Mr#P%i7-JDqn6pzx}8QU4!0N> zBKm|Bzu^bU_{G?)EXsG1e4aN3)J-f;8(fc^*`gX7l|AG=DrM9}2$;YO)*!%B;R&ub z=AC-|j54A64hs0UAo+oX6IvOD>_mor@!1n*fG{=dV7r4k4vzC_fc81i>!R+MB}(&D z`?{FQ*0hWO8W&x&qpSD5W1AtEAT3d1#0MINP+Rjgdx}02l8+GHceR!eQ@&_00`T;R z$7#LQTy#i;W3Oqzqd|ofc9aR2ES<6X2_E@K)6hW!sjee^0$GKI0j8z0OLPUu`T!u@3&MHhj5AUd&G3v+l^s6daYC{JKSgZxfx!PxRi7`_1OlaaPf$cnOqJ8}}MH(A_3 zkwNyUNq{27im;ht$O2(3HO7%Pm}EPXc%z~{Bas@TGYp#2GTPLkzyUf-M#cFlN!d1$ z&P^i+U(^>!EXOd&$QI$Dsc3)8T}6v4h%E9S0Wq*!l5)@}lN^iESln4No?x;Y)Nw@V zz`SvUBi-6HZ)v3*9?ZKS0{y9LzCB0-b~(LQIoWXf~lnCM6c}XW}r(Y90+KIAd3Mi+eU9I!4sXGOzaw6 z^rBy-Mn@-T4oLS*Z4+Sd8h|27UjqL#^wX5yUSe%RkB}CF${-l-;TT`sCQT{2Z6Fy` z?tu7a8Fhh4ut;*bQKnVv0aSCeC#KQpM)NkeVXCe}%d)V&lw{FWp$s7+2L!1^Mc~+} z9>Teh`ygfowvwnv3SJdmv@7SEk&I0X8SV;05Gb2dB#k-4xE~}X$onp1^0#O?VQVeT zX)onxOjV9v^TpH~Mw2S(JVp+20 zkamX7#U870iI=G2bYLPf*{nvp=x_&^*W{?7#WN3_RrIn3OSLhTzpn6lhca?iCS=dKgU`Uynh&SrNjU*<_7`M!!vwAIycV&cnu4-_^ zbLvshBGJr@gQ^H64Jd{JJ6MO7pmst5XUO0IF^=iYNp$ZpInF_+xk!yJ!VwQtXeR`K z{Ys&%L*Ph6(OCl5E**!Fr&*oYS80eSFEg59)wIqM!%sDZlxDp=1$Y@Z9%6km9=WGz zHm~woG?HcXCw8*)YY{lYNAUvLVFF_lAU>8!qs1pulHJ?1#V(0-lRjaHb&T<)nUJD6 zMBdG@t#t0lU&$hPaEpph!{h_T80JXS(SiXYyH7q2P95VAh-m$K^PV0mw?dxL)*P|& zOrs^94)x%mAAzc&fo>4r(O`$8>|oy9B0%1lModYTxT?k+A-o|h7$5X?AbBWqfEuT} z*gdbtFdpVwup2c3wv5zElYqvXO4bHBu_m zs-zYyOi9QTjU!n@cmP_do~KzMkq6RD9@vZ2D-_fvSb7mXqT(V|D~4o{+9=r3agc(8 zpQLj1G!V8?{vnF$(VInT0*aSR2$5oJ7#Kzx{NRZTa)DVKVek`2>B>tHllR4j%|vl_ zk-Gui4nqYYwg$PZ1Mw-MRnA-o-w>^H2~(;D;n6TmnJ8(-J+0r-b08{unioKP2Ac^~ zstjTPj8uZRg9(PP-oi%K$1Gxa9>9h%0IKD&jpY3pVwYnK8x^o7?h<%E=0VZR&`|nb zbNEyMWZ>Y>nwGtW*3VLQMZ*~b;Hl_?>8ijLronYe(X)%fWHdlMJu<%?^cib z$%oY(gkoI<8xjHY9Mr&#UI!X;h-72V2lV(^p~_UE2FV;rexlOmplvgK2kT>59Czpi z5cQVP^CeRkS&21?g@~j>U<@jiMtJ!J7YH|`iNm>*eGftx&D^01ge}Oo^v;;HCJz2( z47SgLinUv851eULZr7S)t{Vt7l9_;yqNJGUgrK5QXk7`|7P6FyaVJ3HZl#o$)zHV7SwJ;Hb4a-p)R*PZdu`I zugXIPW)>SjFyE&XkO0l)BgF~ltoNujjm-m-TPT5)(b>}Q^$3OsCkrGNtcwL;ExWcT za5>7OVl-i2piD}a&%~*3gkXbXQWq}O1)9IrWLti zW@;J{30X<1$W2_*QF)}e8H{J)B2qFVE?x4Nn*xU%GcU;C^f>Nth(XUt5BWM;M%3;I zA_&gxD6s)yKr$gNY0)bRQ5^B%qPe&Me`b;d30uu^81%`a7UR3bx6~GpK)`%id01oRFGQ;>IB3nBSR&g( zol;|9DQpG7EQ($8r1AfvVh7M%2yD=0J>QbYb@gkhfjaQ^@EW4UOB(}9^D6v?lj)W` z$`yhYXxMa#`~`U-Pa1)>iyrD}R?lPhdEQQ1Gix9xU3sOAcpGsbi2&ch;7((}GU-Kn zC4&8mZc4Ng^-<5uks4Wi40>7vMnF-DC7Ky67%>d(8#B{wHR?T>HBsCMF^qnp2G!s| z>4$WDl*{R)0OO{Dc2GJ9utLktJjNR*O>3M)m`SiY=Ju+7mR0c!J}e}AhOlZ0;2s zu^ySDNC=dx&nc!^1Kgx!mBus#%4p0#BhVWY6<0=@(5s9Yo(8Y_0s}H#y*1iV=b{nK zI-G&Y5SG)qsLP<@1CXhq3OMW~haF@jH6;2VV!io|9wN$`vmC!X24pY~h^be&$nL4a znuplZtQhq7S;$j?5D9)pFXIfbsVKb}{a{5yy1GgQqdxKNt_|kG!`A{j zGeZpm(3Z0Y~h^}9fI}{wH zMuYPbnTFt=^zJe!hC!NQ?kQdc2_m%)L2VegHNXOzM#N;KQ9C6|%ZvnkiCm)95kwu< zmVrj4j}alHaWrgepuyn?!Vw2p;fE1)Gox8FeQWTvR)^bUal{2BgKAUWMQ$swKNyrG zN}(GAGcoY)y6mDZ=9mXu$z@Qua*){={>$ocTwBD&%y?mtSkio`sQCh`WZ8jr^)v)OdZX9V>y{eQu- ztWj%OvpsRK97{AQr39!2*W7hHZE=-wiu#^s{_rhVuax>zP=wr@UMol)r=Z|Uw3;=Nav+F<4HUu>&E$ihJlYnKMJ9?U9T>cQLWdyod8EAY zwpF7A{~B&Ror1X83^bcO?ZseTpu!V-K8*yXI;k>=mKKy1MBw-?YHygwNNDzta4*rJ z9L!|Qk(goKWbS$&xJ78#A;WgQ2X8e2K_&vDL4g4`YbWro&af+ZfZToB8Ry`kUEo8u z6+(B<%Q6U>VFY27FjkRSV`@0L5^QnQOtI$#v%pm%qleen*>8}eLDoJ|vs&pkwk;XF zsW3{=r%VG`i99h~U65}mg*yj`aKUO)O4C6Oj4@b5EP7)Ik4r%jq#W-n%z;3CQrGzw zn}hE{uMUaL2p&aoN)-XPQ)E}Em}+JM)O!)^kql(I3b^P2mIMPS6IA*D6$1W;WvD5~ z=W#J)QNTM@gc-A_m2f6Egr^h*%|%pL)C)=gz-YcGi#i^dFcCRyici?$OgBKxpXF$1 zASO=azD4X0*hBX!>j=^MO3AjU`K=J-T80Gb+)lZqVxiKMxYs!-UR8P?YaNI zT%Aj+tX*`K|4K+pK|j|0lD1tt+LRCkg9J2+2K@WJ&seM0m?tCzJ>RaqtKM3dImaBA zyWn|Gh56{v!#meH7#ElqRbaV6${0mwDIL}3{XWGcUJo?(S`raULG#Sz`P<3}L~*m~ z!bz`O6YW9Mq+s3CN6~29C#I_a1uk`V6}kTP!S!Zp}^`mvE`xw*Y29*da>L z&HN^lob5QC;}4k+VnjFzcatdV`KpMNhVm^>(y%V`neUo0w^s6Li)TI5NX&tL`K)v& zvRp&QIxZ_5ycsTmz>Q zkmDZqJQg2Uo6Mjc+Ep2nxcW&{Veuyx?G-dx8oG^Aq#r11=rcMi-J$?wZ3XD%Shvg84gp%ooJlM996id%MO;PPgWb91g`-Nj6u4E{#~VY(v~ z4Dm4f{%XvhAxaWSLO}F*WWJ8b5FQKZD>nv1MiZRZS=7|a7aFj4Sq!`U?m1-EWDL+U zTu}>j7_{PODSl>BOw~}RQMq240W56^>arq{Z+HvMcY^<%_AQOE13t7|IVWDi6zooj z>(5s)OYY0Wma)`T!IBpRA%%T-D@`Dz#qe<|cjg+UF$7!`RdZclyh8Et4yH=$*VRWJ zoJ>fGaJ z>R9e72$7Pd{zU(PZ~%B**yra2m;%ILJNp}i!OSfZQlFGl)m_SEz;`HJ;29S&oZFj( z@^_E3NUB=tl7AQ$2ROV`MH&1&vzY^+HizZ2@9>2lnfsJ)=rVmX2wOgvbpJ8rnucTi z?_GKr&NBPt-!8D*B&9@lz&)ETc&v4ExJ4HxQzRQfC^Wj~9kU84S)@YW$I}%Id7=|Y zvVgro7T=B8k3q!6D!`1yF<@p~E}aW?DXG@1@!R<}GIEDNp07H*XV=s)n0x-h$%i}z z@<=6djv6JvGdeyoFV|wK;BiV3eN$d_e^dN!=5L1#CpjwoOC_9*TRL&!U_SB82XwaFx zpV=53WXbFnmr?%(>F{+S&FcFn`-aG$MOL}y_cYYp4vYlS6%P#Ta^;_h!^%Cp!61`d z+*$6|z5>?Y+9yf!ZqY6~t}W4LDZiW%P|<_=T~>qwFPE$U*Bm$70vrvJIL4pt@{o(d zniV>>uMGCP)8GipQf%K~8CZ^@+Yw%OaTtV5*wrjBfWx~0pw66I2z`F`W*ALPI0h&j zugCJ1n-;$zUjYd!4y<8@=*727YfyX?>%5*<3$#4|Hol*Gg{y&1Vl=Ik=1(f2P$XEQ z*M~t?O?Y(@D){p!rZ2~j3A?9ub42B}S;ZylHT`x0d+%hy%o|aM(1hJg4q&H07*Yy-ujs<{5lQy4}S0VH2PL#VV2>V{7CJDDfjo3Wx&J?^}O z9%X}-_qOgx_uw4lH6gaSL4^fLU6H@X54g`VBm@<{W^NR(G*AFzrR`skTW8Mu-6L$P zpJ@jNMp91MDfa})uu@7?!TAB7u#EEa%w)2Fa`^{Gi~pJB zWlS@0kuqY<@Sy+!y1A1i#!wBhu*zk2Y;g%u^I+>(?}mi1!uSnGiFw2^NLdt|KudOI z&YBHuS=3wrndC-XULjFmcgciu7t?nE&gDnFX(9v?tDf#2O>(vtV`bd*o~A4)*!K{F z&b#C8I>jg8{RN{~ny_M+B>l`DNP>ZQiw(pzC_-wA#)GNB6=7$2<^ka-H0P(8lBWS3 zNxPmpP#>Edh0&b;MtUS4fKZ}KxzkKNV@2t7owYf%;1Unccqh6zCOh-9|`TENxPGQ7D=o=QT>we2}j2@6P;R9YvIh;ap z^`^DIG=(gQ5!dN_!Bv>QvLW#4M+tNp1Iz=aB82j(=+Sr}0mVtMw`23AY*{i}m1Fxu zfzY`@j>9YF{#|&RAY6EHUrHl1ImOjTGPbUYhXCjD?IJ-}*C_}QU^@ibenXS-U4+)i zXB@xtiYuHNrd1EEZLGmS0Tx&&(J@ zYLRz|zskP&HA<%ZIw$@sX_SDSVMOtY0E4 zfE-5qTVDIOxPga>-X;A)ydk!HTM^?RD8yG|Kqs8#{+;%ZWREvM)&ZG*Y4S}@$m?XZ zAL5uY9Ay;G&bz3hz^Jcc!VmP$IZzlFE(U@ls6F8FQJnDrs^al=JEm?SWbyQ z$ZYM-Z7bfADAxKNSnz7ADAK4 zR}1_gcPVnle)$Bl2$6ZMfR%Gy?86GpKPv*+H!|@C2D?_j&Tz9flRn&@e%$R0^xO6j5jloVjUm*Dj4R@)u(Jf5&n(-bL{#GjSZIw zs_yjx9WWO=OjyRi=e8IUQX^wo)7}^$|_LRo^XNryicSAgb4eR`Q;7gXFoYm2PYEw!)1aZs_FKrt} z#g?1dPh2b3DRXk!<=ni|rUYQ7{gf=?yff#*DjQGBODU#5V0;Hn`&WQ^Q|HIZgORw z&zylgLOd;my#I&+-^o)+QQA00pVg9uFE#Fy?ksWjcd=v~5sy!IbPE>+vXcqi-;-5 zMI^FPw88ND6-iNdpr>a7QuFQgc~kiejM{yzj2FQ@alX4J7fd5M1Fp#Ln!fo}5*A3| zx^9JsL{V@z-tWJ)%>W)}t`6nlJ_1Osvbn^6rQV5c(;ozQtk)HoBa~rX2A;}4=Lq5l zG^)er@t0B|9G^G}u&(o55NpZp0QZ&a+=GE6d1xd-+#HyN3=N?SP5bp@!DJ7*yNbhW zj)#x~6ha@ZU1;_*GC9&^)#u6HDfH*pTmQKu9JJs10hb zdZ8TVzK~+Jl6Ve6O%faAa5-3)pTrto6_qAa!Yewi-{FD#2Yy`_J!PQK1xpdb@bk{) zK;P^U(m5|13ghXoiRogL-C76OU44;`rL%;RsORX!=8sh{qD`z2CwNN3FG)_)Y*XcoAl3F>bdKp;X$PQJsp(te+m?h~hcfV{7|^w$B| zM5KG_toL;w;!#!=MydVxOVFoawwEb1zMJ9wJ1j-c>?O*~`u+AoZ3ro)$$G#3J}HwU zk!OW^!GQj%s(0O zppHBLys-F@Nc{W#q@8^Ky!qYq$pmM@nfNV>!u0D~&kC%^vt_+KmdWXZcZM$Wts=OM zQc56kP5bvTWdkLwA3vP+-Z%z~huw(}Xni~Qdw+N)B=t<~sJ@y8FdZnW~$-l+RKB91C%K@?d ze#y`wj`S8Iq2``7ik(qs4e{kx4RIJSH8}TeNBjR8(8e z_YHlw5Xn1g#OCkgLv)da;S#f>`Pc>ft!MPXJ@4CLDjbLI`>R3T7d>PktRPKnsNU() zj=zp_SHC0vy_pK^0lx$l(s~boq83;kfa<#q->D9Kp4G#_o_Sw%Rk#l7CT8*Po5pKd zb_p~HDZOJ#UV%V0hxiiS4HisOChuPZ{Z34ryqgF$aKGLopxt4PND<=W1==mtgQUFQ zcWn~Wwq+p1>~^1Xq;MfA2e_zo^$EbN17cSNxq8RTz+O_20)N|I2D3QDnk%~d-A3|L zpdy4=`*zC~K&o69uw3t=3OLD3bjyP1Jw?*S7^rfwr}u&2?Fj|R!a;qGGF^e}s6Nsk z3BzM76M;1MyT6(EU|l#6Tz-eg`|Vg`VQY&oNPb@&^c*;$OP1fwTN*wAbv<3ccwcDN z)j}p2zv;JL8Zgn%I~8h{mSR}b42{!B2(lp zi{bCw|IWU*enmEsDv<%<`*#g#h?o4@Wx|l&wnj4q$9gCX^*ykryfdT^bRWNM%7l^O zdR6k>J7b|G(g0;DoarOB3rZ|9(arDTArO$Qt2BdmXDS(&t~kuIoCImZoANycY6yL+Sc?=UTy)>@g!(l z?;b>{uk!_=`FQ^f0h{ru$h+_69kTR>oyUJF?^=T>%fgen`P&tRZuMli!|MC3+b#hY zMq%Xr`DoNGC+`QNsqt1t!<}6RIGD$KSVcL0B-H8R@FHRu48TJ|@S4Ae1zrwGzXJA; z^nRSs5I9s>e!VX2zj|h)k>*hnWcr@V#uA7bCoJxF+hsBM0AbOYu28@Mwu0*`eB`~! zI%G4I*pBIU*bR^_j6kubA%657)WU6Pdl|Lt*9kCGp~da8IsUEQL6|)Qa%p7ptI652 zS&Uma(RujH9GkOIE*jZa82tkDX%gnQ7YqVR%tj!@3ixTSso+&??`!nbSjw^lXXPh z@1v5AIWU(`S|YzU(?oIP*+BhrnYE?t!JwCsjn76j>eM;}FZn!{r9Mn9ES&mpxetUs zip5(8D|`!SV|IWa==X}*!|q6O<^uS+t=6sXF)}Dj>QJS-M?mzP6kG>s`0dpVpe^TQ zdTp0fC^9%3m~p&ydPY*r+aYZ9_eY_|L15v~sG+(cK$tni5Lw+T5)?wDM`U%yKhFn* zjy0h9XMZ1(6#za{q`ybQT%Deu;ItMlNEyUoYJ<^rbFE1f88OmDJl?&0pkM}$oCgrg zb#Ic`1Bt`;-*@#QL%sqtA0B^xzlNkq=@AIC|vME-X=# zVO9*!qqtC_@?%;~WeF+Z-8FhURj?!g-1}x>{a{XmOIvZr!TcYg2g;ZoOFtrJkY*#v zd5>n}Uec8a-G1LUEIAbMM3X3a+$Rm)&&25^OFR62UR{Ok)FzoOuiD6RWJDP-L!Y~p z)(I>L-J!pocFe*s0r*~#U%`p(BTHvCgf__L+ApyE7t9WBr@OINEibh*I*6j_ndYlsXI}5rm+@jmFfo$`U z-bO&q07ij#`y-{n zEwS_8;Y{9{_8UG#kkI<%kpp8ac>Es9Y&$WXM{Nl ztv}EA?_{*o1ragUS98mQ;FCED+SFn+H)Au5>SWt^%L&4J__&ka@t;qe^;M(K(s{qPVsUio#svEGIy;co0!hqgTo(oblf8 zUPt(!WCZ#CWaFlp^26zdgeT1kTutnqy4zcd!V1rxnykO4h&n>17|2+cYXTV#aDUMJ zXG5ZRPAK_2iw$i&NLDiF;t>VE?XEf+sY{aa(c2wNevs1QZ1J_l(?uXbuE2bB`tL(^ z?Ne9?Olw`F@lF^8PU3T?;o_l-yWpj>`+JVCFQLHzM+ZP?$fIeJs-FAnClCcO6T=rC zt4P-bWG_w;pTFJr-eQ%+6$a&k3>e@hWkyb{1;_C1Po9(N90vH))3^@2h%2qG(c*C& zb`w|=Np3pM@uNU1gk54?%r^+FxJk=e`)F}Oi7ELaJV!R>Tc=#klVPHa$lli5XQd(+ zQP|silN_=dDlbT6?|gvkMMaKFD>%~c>;&}zkLW`_rsvHMkkoVt9Wa>Qk;>$q-igJB zKGs768DGI_XrA7oLQszS?C+3mr%P)x^QmnIM4v%0M=YKKVKBg)Q&Dg=1yRdgI;uc2Y;O;jyPNLfdF!3C922-FuScbL zjIGE6q(UtxR_Hx5j_OW7h|gmhZh5{p&*T*M9akG*d{Elp8ZcLU1r!ws0oI!Bcsi10 z62&8v2m1T7Or4hk>12~;fEg3QMWjLrP<>a9J)KH@hmY#W%`~CN-_T9mlK%;=yE_K} zm_Ztsyv#+zuNEf_&Z|gA?>Q1ofcrRs)UVng%pS@wEmCo>i)_)u8s-34T=OWs%4-p2 z$EJz~w5o<!=dW7|yjJ=0vzBu>wZHGB61(+r>3jR*h>P z;;pX_SW3!dp#JqpIrF@iespXDzBqe=HGm&<-!+9XfcQ@`k#Z%jgbeb!krA!H2Vr_O z@kblOKhAZ<6bOj0{EF*^2!|Gh@iF;uKT?4LhO$x}*k&%=yNIj5bvsyBX`V_z7=z(Kul44V`b6{<=$a`CeYms6yR2@hB1{OG60(xBtauBfdJMhc z>p~p#vLh(~hNApsnulZELtP2~ZUEZHnz+hM65R+j)Vv)qfIpLdm-yp*S zSyvEM@D`I06o>-otdm<+s!a?zCuGao!z|+%lLASO2SdmyLW*IU;S_$J6fvb{fg|`7 z!+s1MCQWuIAu@AW%z!6;zwzJUo{n+|%wc|1L_|2LvhQEvlL}!aGiU!QA1rj)jwAx) z{x%uJ51dOyE()g)bh(AIY=ge&k#Li=13lM#!>KCV7$pXod#~N)>0*V6(^FRzjTIgp3S1eAX!=m#G}~8dxeUMRF1FHhKpH zG=iohy!(VGYB1_lPz%ki!3{Dlij?oGhU*iV4EKg?7T4PavM(<(|Ho>8EFuM7#c8RqmfuD3DaA(|lVL1ft1Rs<9 z#>n%#Zm~w9{7dKLyjeX&IQOIR2}KJ>2sZFTessPi2vKswN5(~nM7)O~5n$bxI#sY?HE@~UP9*#ry8{R+00S}^CMIt^7A3?e z)f>N3!{9s_=Hiexw|OmK+zI3DJgR?9KO+sUR0SghUaDgCSQkiRBC;r-{w@8C+v7?J zF{Kc1F7u$?GLvCS0L?yh>m6RLRKQ_q{*`mAjPKcZng6-~MrJ8XVEkf2p@WG6ay-Cc z-}3S&pz9dK3hlr_L=7pcZ`j!G`gImSOGvJ+)?wOlB{1pn-~f{y_AivH;ts2$Vme&# zep-ouc*7-!Yyz{i#|H;L1R(%f0p5cOqo1<^^F#t+gC z@l(s+EcRHK$^)SEr}iWj)N7H+3Fo?;9DVW z#OD(~tzoJ&>N>KwuxnWcJ7LfwV$!5?y7z}`1az^8BKLu)ITjs)0$lgIiHIiQU{b!Q zYeR&I>G(!a-g|IB_|A99QRC?$9U}Fz0cV(~C>x@lV8vzt1xl<`X6)uZ z!*G95FXA1BcH%XIWHJyFI!MH+fr%wcA=&yFnCPNwReyB^JZdNXoE9 ztCF1?y~Ioh>lmQ@>~c)bU5Y|tWaDYm62*yn6cSLFh;f+k`4ODvcR+}Hs-$X{yA-f+ zUgUTLG0Io&VOLRLGfXt&8(odaKD3)&VsuR3+18rLi;a5)?~b8})@H^aQgv|bPH|-b zlW8V{H7XJh=NJOhr9tyTD--%AD5OWOgx1O5-o(6P@UUj!TF5#}X8#PC&A-MIjQ!pP zni=IGdx^@9W$cwoOr==85YkNcQ@*;U=A6vtQDyl6R~E-KrAyS5lW_xN;JRRn*q@T@ zbV4=q6&5N07!U-;O_JavV-#2)Vd`7&S#oZ~kl+?>&KG<~iI*5RyIDE1G9Z(;g~wkq zG+na2Cn)TchR_D(&kZ!T!pcxq{FLt%ZIYWlDndv~6l5;lM92hA9>bysXak_v6ikXH zGrfwT54?=6JOH!QB+cR;9${2p3H%;-jKOEBVXk`{CUP*;6hv55V)Am6?qir> zw_uSeyfYM%7!wQ?q+G#owk~s3JTieU+^1mTEHNU2W8dyb(gDQ)+P7~)&=(9kD=JUd zc;XNMlz`THDZ+Ats-g5dKSSIdS%&zz`PM9S*BBQnmt%yVZVH3pQH|^x@A#FXmCecX z8zL{|0ZRqgw{Ik{3#PD5d|xB0x{yfqDZ|vPK>(~$JmokSDogofKu6U<_$~oV0oac) z&x{q~fXSDSMu6VYqCGwnt^s&Xg!84HL?c|j-W&w6_9lB6T7F~3@vdLiCT{LX)J-y> zJYcSa4GkiC|GV5$o3g9FKmFS`&Kw9P(k>0apb+R?+Z0YgSD%uWcZgR#o84q?FVP2i~ums9$ z=l36Lqvm4}un_%7ZIL02ANoEaB2dlW`Nf2BiIa-PJ(utoMCnMcs7O5>S(T%P4vlOF-P+iUs>T6s|^{XF?BYL8M+Hd3sO=c$NqQNV7!F>?L8Wz zD`;Md$mq${FF{yL$zYWS(FTGmxb1i+R4=71E)-X7Av(0dD7ORtq4Ig!yEIC#%dj7EjU9M{|GhxO*q95HD$_G-D z(GgfONtKJ2kmPC}Gr$*+B^a)lJ*=^lMh{ep+Ai=E=+sdMji6A$W{aQ~sCd=eV{14d z`@f#7u*0j6L}Mn>>tPE0FXiAtuhme=mGzeN6w1V=D|2Va`BIcVZ%HyxoYGq5TAR0@ z3{?p$m)5yjO^6R2))V+c^nkmPJB78`FJiPb#w*(HD!y z+}v?wp(?hZJmLZPGQS(M0XRN%?MdF3h}p|oI{NJz-Ug2df!lVc+6By2mvSezb*^#U8#G~f&wi(jSmcyD0_xm}6_ z@LOwFpj8rsO(a%$j#~*|7_wn75ujUE7ZYHnrAFAHI)K7LT641tp_Px0MZga10_JRF z$4E>vL$?HZxyYeDnUDjZ1FEHryc9Y*xl3$$I=D{2EsaitSdCJ8C@m4WunOcE9$*tO zDAO?n-C?x;t^Mya;{4zIU>G|POAggxPXlmdV@^eCS0xd7f+`4$Ys?a9P`}2GksJAp zo&s8TqSa9qd6s~}44b>6(4(N=!NY>VM9%?#l8-!f3+b8uOytS|cTnq+bV%|Q1HDa* zy25!^`C{IJ!DSdfX$|LG>nkK;sG+0G4vaO$Ara83To<=Mm`Bjk;6>qfpOy$xmTYYd zUNeuX^8e&?HZZrUxJKU|=p@vJ)2Bn$MkP*V|5mUBFp@Cgk-ZFieJvBgq4Rs}CK5nN zqn5?=r@6jX0ZBW(1z-Q>r$v7bgLCe;A47~5uPZPUmjd6kMSUu|M<#_~*BnN^0MRx) zTc=|JB`iuzTz^2_SnxE-yRyWY9?~95EXdu|$FEX{=}DLaj&P&7{~*6Ys0;~!fp`fm zfXopX5t zIr*4CMh+*TDH}Dv`5fa?<;J&QQ^0_*Dc6XYg(x}Yzn~gvZ3bE=cR|xAZin$QBiN;5 zh%2_oSvQEWvbPKqRI=%l%$Z9qkG%;_QgL)GXT^y}P@;W8mdvuFDX)y^EM|6Z##P;C5sgf6?zRC3P{qHc*Dj3H}Wm zr3EAhm1LHKm$?BPO%&epboUGZfep7fN^inX>qEY{!=X1r<%`mIe)j_V_{xQC+=s7} zWQTlL%Yn(TpdZ+{$%4Q@cLvoj{Nk2fS}l$*f}^Vi$~@evki{kI4FjwVP^$yKh|9M_GQ9r4Yr}2LFvs}S)t(_vdTI| zAcky~*Q3y`;tXulA8E!=aTI!&pwVIY0k85{CA&M^;B1BF80}T|@x>ez0Uyh6#PJ0a zHDVxW<9wRZEhO1#mk}Bg#LL_hWhsy$fTaag2vFg>h^k#SI)PwJ(jiuR7>z(u**44x zJ^k%6VO4ZOtAX0W)xeN3BbfE(VCH}&PY$~7!BCF<%M+Sm=aJz-!X^h6IyMV{zY?S& zp1?BFGd~3@LpVbLn%W!UvDzWzc1R_WQ4iClBhDaRD`>BzG-&vp2efo**vHUaUNZr7 z6N!>5cPulLl<$XoCVEAe3QgwaY7OARnz^V;5&{ce3xTG9Nn*v)S_RpYaP_W%Cuo#+ z2ZOYuStW|3dPRZ@?n5WAF%6PR{5iYETko6#aEC&R)IMEicb6?shfxPo+suT@m_aoq zn)5!5T%(mGTrm+l@BTt`Ask}JX%Cz|CLPLkL8J0JWF}LGF7!|?>*mx=g##>QXatiD z47OMiTb{f=@)Sflj!ktp`~WI*c+_h=BGCNn3_>v{T6sl=vCQ8qJ7ah6ND3Z8=@fVw zhL{{u;7?MS_rN}9;s9?CfWs1HeM<5BX->^^fQ?!y2ary>8e$v;rT_z_8tZRRc{ezu z*p9xHPy=@&ch;<-ZN{7~DHWHR!sQCKOnkT4gxInMLI;VKCou#FmIs-u zkze&~hMmK3Y%3F}KxQBnAx<2(UHRGLK@m<65BM zd{`xC8qjggVW+{`a;xd;GF*hpX%(LO4s=`+wUzpkP$2do!et7kBu;p+rl93Xq-Z(z zjNS%_oH--8I-8xP#7^C!9FhSNcy9%JdMsF$75tz(MXXQBN*_GQ!9$7$|LLt?u#h8H z<@tn;feLg{5Pwg2jP_K0QyM_R%U57~D=n2CO{O~G9Rfa9+0DFp$9HP*76vCFrs3;t zYc6k0qNujd$KMCyq(@~&!6jz;1kalAb-04XsC$tOCRG*0rWItJ4JuqgxbYRILC}`S z!IK&78V1k6?t!x*KNhz}nA3NKH<8&lfA3uhX<29 zKDV2@8G&zyQhL-6hpj+g#c!ab&==^CL&-c%AkcOH*>i5 z02PoFWF-pt-Q)BRg02?me+_Kt)H&4F9PT?+-KQI5rsr1{#6U{vQou)9(N22837@FfkgUkV^M^pAH73vzAQ5M< zaY5KNtKx>2@c_fP{Ob3N9}yf1gq5BfB#A_dG9`kH26Ir6o+Q#RwpN%^6){6Rlu4p{ zPi5~yEDMx+#vQsuVZV8v0FiHDVbH7ZzD&pwB4pX!Hb2PD0+vV=_M9h8G_M`aD&3Jk zDJnpKrXf=Z*P|t=xx?kMtjPGqMhSY?NO2Mi#0sYc-kQmVqV-GNN*a6+TYNV+Zu@9D zJjB9YNMRtuj{4Pz@9X69w0 zse-=uecxtdU|mAO%#wFQwy)V1?GmQitiCm{K-DU#?$Zm!EZv+k%4?k9q3$6i520G} zUL09_NDs%Js*^4ibp)kYt3QdX$}Nm?M>*LF_C?|t;e8IvDddJr%z8;`0N*Mk$_;C; zxr0m!kq%?&tR-&hN^Rt4ol$mZ|ES4{n&j5ZF3u_05iZJ^xdclH&?0ce#nIq@$JK2r zU~r}pI|8A`QjU!1SIcM&=Wd+70#aU~TwTGSIY)%9kywcBUx2E(olzjcKQ7a!ab-zN zMUd7W{wT^XF7fg#469O7<;Rsr|OVEd;06f8<4f5*1n{fIau^x0(V8HxYOM^M62q<%2?%}ACy zi>#$+xTAP6D3xS%8Wh*u0_<1j2<}zCW0cAVkLi*^Z>v!PtPnT^-Q6c)m~_oJSS*wB z8LA_DmPLCc3l~`?!5FW0sfPJVdN#%A=q^Q>F*5Q5S;*BaNsce&M&b_i8wG6=c?ry@ z;77TXABKvr09ztqJyLT}KErU$IH41uWmN^?4H*8`fQI3QGiYH*N(|IumiK}`xn#Wj z-Z`O-Wc+9=n%=l&`UCv5hk+?_$T7NLYKNtmpW=|PHH+~QWbqpqs1==@Ezkk0%<2@G z`<0zRpS|HBlUIYKlo4vOy3*lt&Te-X>M+EO92Y^u2^o&sri-cYwkG2e+7U>5W~21u zJFp-Cn6;}UF3dEt6vZXzQo*;$I|CH$w!1)%(U#&7O0YKDQqYUaD2a*-8OoPq+h$ga zG}ud&<=lfbSC_Q%$Z8FyeQfF>gLK8$SYX$b!cdjy;;NP6UmS80>_#9k;e!Tg7j~P= zO_*d|u|Rk$w$P?AKOie62Z8g(yj!07V-(N0znX`WoV4&L>jaG9Z(>~aHFqW2(h!-G zVB`#gdEy&4vZWxu)lfrsNW3)nGYqZKkXBU!1skSNS)-`R5(6g>kgK%KNbcxoW}x9# z_1LLniERaa1C(h>ogTsThD;sa=ZfA65|@IMPo-c5_C8Dm;u%rc`#UA~6iplNzF13% z1_`byK+_Bv3}6pgW?|bC136UkY%4Jm#%05CwTd)Zd2?go~ zX+%34%hR$ZfsoF&`CU>XE%8Y(nXr_ANX4#H^DzELT)^#&zodI_$~j6fCXy8*MG)Q* zRoOIwgZ&j4kwZ1dk_ywv^&kQcH_2Hn5Cxbaq7`e1T>_P)Z{-;)=69IBA%+=89EMrJ zO=6xQa98r6sW%V>s3HYxkAOH@qekcva$ii1_#ZI5$55x?X#4iyCI)qv>r&VcrhRWZNW~Ld}4;Li5tPv zsjuv34$*15fQ!Apa6(D)Qp-UopCEwK-~d}b?@lDel^Y16U>o7F54BL(o@5UsimV)! zZb4PhcJPGSN+QIC<<@pTX+Tk?h+Bn~S@9ubUh2ZHm=69GCoO*AyEKAMk@yn; z98@_$lA%0R0NKbT>R5?)`TnhCBQCn<#4WtZcpHIkvE}UX9jS90X1T>aYL&VIv$mld z8~PP~zQ}TvF$YVB{p|;Ur5T?TK45do34rm!SSpMu(5eh%);JZ*uGce@ukOXk83d=; z&7VOJ!9ehCTcD~#cgpfsz?d~yOI5jE%P)NT#v-isD436D0vk>Kc-dHC!(04Cq%0mz#T{yR1{Ttu6AhZ zATK~%lWiXj?~wd*)BxX_K3F6M!Y!+N(>KtXq{%u$w6da|eWLnDqwPBaQsz*uqNI^C z*y-VHL*rj(jhLN+v$5|A2D9=hq=KFb>Ukdt1rv^=o7OSF{n(fT6HewVK2YIF9+V{D zu-My(1vVs^OMWusaPY-4pzGn@;|3nb>nyF|!5mj?*H_5anZef#zjz)(7vI&6Lu?RG zf*g298mXf5VUbk(fPj+H;LB~%mMUzETx11ECY+~=1YoF>hosm-DF!J(`NC=E=qTJ} zevN~o_a8cN_8tb%?Wqgdt<-*)F`=+DtEnn!7|$8pv78zM)k7D^Ao(SKLv5flkZ23k79SdkT+Qz_2&-%hK^`9@ zVtj*>f1Km-J4wEpgDIK040xL&Cnl?C1a>OkuSIqj)?jhBlV1N-qA7-WQT>W|PBg-E zcGiszya0i`{egV-d^DH@U_@{e8>*=+m|+Z#n~<3R1`Sa@w3TWPBdHHVp}6OrK~MR^ zv#QDa3FVw9RoD=9{w-gLz5C<^Mg;5l^dJKFEp&^)ExDBMjg_H`J zwK!=ACuwdKq&T7Jm?!9)%o+edd(NZLlqdg=R&R1KB0XA}^Iin-{DECL!Ky9rX@?Tg zVHjlOU|>ff24yQR9GFT3}}t#wQM{}F8p*{h-xg!WTbNF_25>6 zBRQ0DQKQX&IKorXRAGpOd7@&BiynH&;HiqL1!Mn;;GpbeGBBljVnul{D+3sc+_s|K z0v7R9oV4XVxWWTpanaCAYztO+T)^%iLOpQrze8_DlZ?MvEGzcJlFDw} zAcEy9p(9C&C-{|n+p=3BvjIMuk*e%*B#)5leKmlXw-XVQ-c7PKyr!pykakkh zR2CO-hUISLUT#s=r5^1l{qhU*Mq8gwI z;JRo3T6ubs&XT@s(M;tgBO;u(@jY=wUQ$H@;bx>2I3|=nTViTQy8*bMDnW3<%@Rme z32+X30kk_oOo&aOC1DKKb!G;(>-UNpB{bzgrLh?G-g4DdzZsu*IW8c36w0Ir~fEafDv ztz`V8NSQDdW*exXV%%w#fvVC4Kh>us_UN}>CXKQNoa8%yX~x~ zMTVLtDib8VaK@dSlEGD+JBuN!=zs17k&Fz^y_L5sGH@@Pz9@K}M`!xi#cPvPlj*h!tM^#MGRvRXM;O>hw}hp9O;bVd<%cIu?nc~py}OvfW{=_uu-`uZAYduNwjgZ=P+4W zqQw*y2RT?IXQkrsyR5m#Z)%LQa>>OGrt=&ixQ#I^dfF7?Z(0^t6imRG!_4fy1{d01 znKnofgC=X_fzHJXwIcV!D7vJ_fmQf6yNqMAJmWZaB*Yph9N%9E1RnZQRwWaN<(xdi zFM$lvvyb3&FRL`sPE(+|X2FvcY)=SA%J(~ZOWumGyM%UM?0wCemTbBnRP0^Q~c!;`M z$Z8RVA1i?#KIxKthSu;q3`?kJ(QJ#q!pP1nL1wd=Vo<({?i<@Yag)sTZKsV38#q0v zNGRR031d8;bMVOA`dMyX)?PGg8KUZJ!C&svI#lnWruIP%L?ZQ*35mFH7Ym1F#sYp% znr&EIi?nfJi_08T=3G^ldo5If6AO`Te>4OajL z@G=rdivy5Hp^l|!NG&wBO}^$7tj)qFJ(#6eai)}}1dt&yBg55PS@s8LYQ z&r_@Ow@a{m+@OqL!Z~kO$k41L5m;O5h7gS*VHM)>;35oqfm;_QHY4!eWNL#1mh7XT zwrU{JI1Hu{5`U7i>q%P}eLbZdeg%Bc??PVDjYRggaQ(FHOu5hrA}92+QV!o9ehAbW zohT9CFrdvMbf9hU0~1rCL8KONVK(T34JEL&8WOBZD5FhBw(V9xCJ~zhqqi{bVQxV# z5vt)HWb%Nmo$*$&g}25%Q0B0yaHtzsETd+CrX_cuP^?wiF{0TD@)P+Rp2XP`rLxaA zt!yqtibO2iOM7tZV7CVl=^7bgK~^+c5q~s@H6XnqK-zvc^2C)H&|omuBSpd^YQ1x4H3R^v-sX4;dfIU1k zmCYX6GpwcUSquDdk3=X| z9uVJDtW#w0+ed#IYT{~{a>=ID2RLr&IPoAwQ*XM7L22KR#W54k!^EAy!>HFf8XIwA;(MH3#AG5KLW{Tk#QeIX2`#7 z(s6z#Cv!8y#M~5^doipC#+A%e#N8*5HkkAPaKj+Tq@J&kN|jXVLT0HW;>rolCM|p0 zAyR;I2wD)tf?91=K%6%%I92R$vi%?h9a#@ISRkBM8a95rG(j%2x&M$N_Q*G7EU1oJ znX^6w@YyJeAFDCvR)&BlI2}O#-41fI#ARTP>UQPWfKA}<7E{pS$!)DWv?oCygpX*# z;h0G7790sGR4PP8y|lop;6%qu9I{1o5j&N*Oh?dyTB3`rq4bUwkR{Q3p+M-G?Qyq>d0l7QOwtA1dcy(cnt~9cUP^za#>lCutOe0{_T8C zkd*MI=%WPry;=wS5K0X`r6iv)OOZLVJID&YRv{JAUYU}I{~FQ5@Yii-r${&?$3%zs zcopL0MG}(2DPJK^G?JoRdq8NCq|4uVUj{SN5qx%Wm?BHJjF|)N;jn6$mH}FU>H86t|*p-1%O6(Z5{qsLGdu?n z&#frrt60?yIiIcvf6Nh5JE~$tF)P8|Y6s+o^PUk~8QChcK`A7Ak{YZxHRx2F02(2G zP#D(eBY0A&nx@A^vShOg($+(~J7e&o2(Lhf#Tl835&$a&87;hDh9fYXZ3#&B-}g@X z8Ohpd4LUWvBDAsA+%jiUaxv%6_Hh4@G~ud@9{2}EMh$7Hmp5?r)s#zSoPEp`4Cwki zwBc#CLWjw&2y-N>ftvrFmV6LI7DzL^ZoglE^Xf!T*n-?`X`=Uiq#EQKO&WJNPCXBA zxlYLnRe^Ze`o=&@S;KH9P)B(l##k74!$al?rpdIpU9DdjlV!4yrzG~`Uz#~HmGOyf zKpoa-3DC!?6{j?l5#gPa(#ie4g9#C{f+8=JmMtTl9HmY1-};Z;$+LqNHC-)qSgRit+NFp1h@ZJsnI*y8i8;kyel%M4EuH4 zQ%+Gs*6|}WvKY8*(S0H3w9v5vqoOe*WWnXVT(mVB_^wm?(I&2G2`q8#i^fF2xeGWi z&{P>Z&_XXPzx;^`tEfuTk2#N<1WAja!Ov0rUU=V_*D)LkzTU{O0BEr`7$GHR6VHY7 zOIO^f?NNF{*vW7_*pnjprDo0mx+|y-HOGs zsjN`o%oPe=V5SO<3ni?n&RBk7_ySr~LS`NuyYF`hK(+W7VbVNf1Ozm$o0)N84zKBH zNM3HTB;nM+10x<@V%Q*-UtxktT~{@~jPS40gv6%jBr*9&YGO7~4LLI$8AJB3oCP^B zfXQS{k_}UW&hQNA%sTctwWfeu;A#YL%|&R3PnlW4K>TXzARL7FOMwsCnS+@g>E=*n zmy$7D?6s|V3D)t;NeL`>ykX>p^!1@NBqxgQ{M~O5dyUgVpDhQ%{g%mc&C3xWCRCX@ zm@QR4?NrQ0(7r&Qs50Ru<k^t)NB~Y`Y zs|}hyG~9m%+xozX=DDLMmX6h)3M6>N(>w?LZ<<*0{#GeM;}Uq z&L8}aCl??DG9lPBSJ z9KlwLL8PZq=6QO`) zeIi^#V_8$ST?tc4ATwC$zG{dWl3AL>S{9PYHwMf*^+$=r1+g0)0Ei}nRbj=zLlvY${rh8U)za3k0lxD2;2 zf#Zb7VVHcSXsoGBd4(KiqnJ$*t)w|XcXV0BDbbZ4!>D{lXJ;Ejl6^m^LEN;z>FzK1c+j@B+ahGkjhZi zrZ81X_zywsTC%0K)Ig;r*bN4y9@}GgEhm&3W~ zsjb1~dWL8)98*x9rP@lk(HA04hP|f1;QvH-$t)ks!V#jfsu~i^9DA@s=LAE9Q!yp> zW`#IT(k*lC!1-T9>%#jZ<}Vl<%it1;XG#v(#eh6<^5dvGA`ekI27uOG++C@(1ecVu zzyAbdaOgzFT_T&ix&}t=Lz$h)=?Xm7t)%0+W>ijMPXZw+p(hJIfQuv*MXT2^Z-jIN ztjPv8cc~AVAas;qJy2J%b?f9D;|CM6H25O%9biZ85H71|bM_o#4m;$);)mBwKz*@Z ztnm4f2+-TO&F412{eDRRWe`c0;_$l}SmYp5o0(gJE#RAmw&G+$e0NSUyJmCgqc=%% zlbS(q-;QIJaECy2B{2{L@Y@bSSC5Dtc~vgMhT^zAV&Xt+Ikp9yR9$0`B|(>M+qP}n zwmFU0=CsY%wryjkjcI$@wr$(q`C@k?c7NTBtgOn&9~p6??g0)gRc5)*=~U0nX?XYV z5jK7vlLeWW5FK6iRt2>HU%*hBX80pTfE-4RwEdo9dOT>H8Sd5@8F?{{7%QH$U4ed~ z+ra(S%pgigz}TJppLdj_n|DJ?=RbLN*d^3(q$B3!p5$~&K@X|zVu$<@kw=rr*bOi0 z$g6}TSu0N>b2J%!y@SiocKx^KMM24`jx)BR`y#{rkms!~2z75oW|?W~J5^HKTDyiv zc|pK;5cKli-;`%qsn?*1fmKY~t{gS`fd*LX_AcpC8<{7UH}^UMf9Wmln?JT@b4=%D zVcT60Z;jq;;Ea7ZdscCm6J$nwtxqSX%$l7t5>G+9W>{wW!IGzPgBY2syk8Jydqbyr z2W(<8ZYp!PB8r^ImF5GMw`pOmc(QJ|8-|r-q(mB492X?)#cu$;V1(k}9Zvn`V0t03 z$P2 z2|{EaQY(0O>bLb@XC`1!xtx-ou%hZJ93&y$F`^W3Z}KBcN9ZOTA};b~qVPYhy2U2y z!tQF+>!8Ay>AF>ZbB$)kP{_cOMzeI~$We%?rwOZ73T4PvrVug4kGL0Ml^M;KA(Qh< zxJe-PH9eZ0{qn@w(K6jMk2Z^bBMNI5rZ&eS6R@d$mf}j<+<*2uKDL{#Q4Yrk+s%!y6}+; zrU-Oqc0hmT_sK|Fg(xdJB9Vgy*mbERQ*GKqqem|9<#0X<+NYDc(f?lRLO(?Pq!Db9|D>LyN%@wU--Bra8p2`$_O!rBNCR z)%{2@ls@|1urmuWKA0QmYQIEg2a#*a_dGdVR0llP8XKs=CcF`=OrLo{Hzr>urO_nl zBg@FGGTnuSkf9&G$Bcp>G1e?xRik4QH65mec^e-af?On-6w4!U7k!_3j12PdCN~W7 zUzBot`ulLOBHVOoPA%iK{dneT9>!mt-+J4BYX*NeeiTN=I6{8pw~I=@X}#S0c=Xc;mf$O{*}TRwG;zp}xlyTBm)WN`&6 z574Nul6XclTY>249nOV2$6;!`fv$9BJPWK1_=^pX-y;!JE$gg}RP zNie4s2w6AT%OtcqoYa=w8NlYz6|Y}JnFdBI&w$)B56G%7B7n+w;b`jBr6B4^Dew(l zjErjzonQnDLd$!ncV=};L7i(Gth)84s>Kmf$Lot?xZO1pS2zZbl8m}1b3c&V^W>0D z1?r+&GxL0kb_uylKsWg7u1_VU^G_?7H2s{vbKx3bOL6#1a0@n>It-a! z#>5)Ns>-n|4;iK5&`c+N$EFrrG z`#Fo~A=980v=Sx246C9LoJ2C1eS?llaWd47_pNag%9eFepU8|qXo$Ty(FL95&U0A(!QOZpDzD!? zU>3>(NMzCc>s4EZC_d3fh%@#lJ?->KdRo?J3xD5aGAAF9`n-KKFnGA`*xpY0J&?h#H?FIk8?f6oV_xKO*zY$+bVV?bS*&U_j7+r+2L;8s>&?~_8x%$A zQ1hI^!({PxowFH2o?~-6vTg;0IG_ED!)fPuEw0R>27^1ll8V~gkxSN~go(Y`O!kdB z^MC6^a2ipe2O%6P$r{~;^?1;f|wAb!L@Q1$0FjfnKKz1FGJ81rO{cG zRXdG}TM*fiKRT*yQleJL)U{Ubu>JwA=I~;@t;r^CKhfI;izT_du~|TMVR|wF*`4JV#8O+zag=c> z1&=a4!ilp%n)&;FQHX^HbSj8~3|1vjMKXRis7&_uM4S|yblE=2xFAuEjT=oQdKHD! z1_n5|AB~r^E7|0OLg&HLxnMmGK2GML9i@-(s2>aVG>f(0#AXmTR)KfL$YcbXU>WZ6 zXd|?9tFpx5hRplu#TA{1%o`oaM?@(>wBq6*L~}LvbD|&HE)mF}jlu3!wuY-?fct>`>4Y;p`YoIYquI`xq?244>v}_ z$(U0SJ2DtfS40Q(>SaF`l4eZ*1`wAL57HKW zVyacky2=wEZ9@_x1Yk3OOW?7u=Yos-gAG9ZhA6_dbRFDJO`3bvm5_-FgpuqK*yDMO z(0_ioZ!S6I(K2DN!e8%`F$$q32vRy7g!ir#1{4jm-8^42K1W zusyLBU0R+w=o{l_@ z685C9Fd#_-YIi+wbGr&0m_7JR+FYu6VQi{AmY#2BNa)qG4 zHwqr?YIJ$&fpUJZI=<=nV4iAI@)rvoMT0Dq{FfKV$+bIK9nHo8iRd9t_3^QL`2JN( zRY4)J$r`x~O>wFu!q}s(bT|N%72XLiY%F3qKdEhL2BP%qGbS%FIgp0peAky&5rJ`=kK$Iffnv~EPV)yJL%Hma8%C& z9o&TcsLE*h}`j3p%)(lM8~B0J5LvOCVaqzb~A99`tjTNMVE z$i{)SW>_v!uNnAkfzse#!LF)QX;$>Hz;na&RXR}GFRw2`l0<=wFL^K@tAr3;Xj!tb=#8`8@lC+&#{aP6iH+w9nOhn z!p5+1Abyoo4=$sj?nJ}U)kPw6ejfAZ4v36B7mec}U!iX)Eo8sDk#drL z)34LuWbdaX3;;>_R|MdY4!TR`kcU{c}aqZQ8-eN?Bb>J zf=d%gel|-G6;NET40wl1jh+a02`dwfGSa76K@>zkW}EboJO9Lq)78vT!YHSaNUvu3 zcWioNi?~m!!IX^h&Vcm3)1|2@1X;4IZHFt){`f_hcN&vr5?EQD;FHcp;Bf1&P^?yH4`4etdq%%htzZegk^`VG(s~- z-$&+Rae)B2_UWz-wt8RS?=KLXlC%rIwVkOa+FZ~EwJ=_=VYZKRvU*r%8 zGXm)lXqM3;c?updf4zSNKcxHwMY;l@?hQv^D+Y4RB_{rm|EC`^uG3GMy}ZY0q*ge+ z;YG5jGALm7^l=G`kjMiJirJzT%hM1?ZwXYpUw3a)5?UsFZgTzRRVR^M5Z&f2>(}Ao zCh^*Fqag+*zRq|79>bPFEdEpd5*MHQZbFXLzI^p@!B@K`nh;L4 z{qrQm7HtPfTw}%Zu1)@{_y-YAb(~VPUh|M;-GMX%-j5Or1tKk5e2B1-^OwaN`u3;O z?hgrxx3=Ot;wS{Om3wy>Y$07&c~!>liNy65QfP{ooXy@q9`Bc`x}UzUZgcZu z5-7OpV@%17Ps^WYUEh-^{!7s>BS`yr^#TBt86D|U?h zo%mKozsC&lk0C9TJrTOKuN)Z3?Dx`hhICy@=-{>aywP-FzBJd>C zRI6sDLPi!+ETNvDc9!=hcxG^Di0X!kB8zQFumKP`xHL;|(r7#{EGM~wpVBRm+6LQPZe%$!6^#8bZDQ zypm%iRFFQ|DQqLx6|M*EEM!9EVkly-Z6-;l0_riUuqXIjNDiYLz@ufcbjy&&G9V!m z*aaQC3rnI@LG(;QSev0_^tyTaMlb|uXV>M!Q6{a8RP06uMIAV&agmeJ!_$U;nC!*R z@(#<%iaLar!ud8s1Ty46X@H2Iwi~CpQTfV?78a*j`K~t^_A2N(LWQGJIQC4DpbWi7 zb7V2hwo~(Dv%nEt@#X%i%`W=cd(B0)?gNq4+X8^`o)J9)hX0mz0+?A@qtOLg@L=7a zh&v|ew`b^x4L_`C{X(WP1egjj3@Jb$vZ6=_ZpJ3pk@T=z$@Aj7`j737TV@XOJTlRz z>$3#kYednxi2|!LnnEOWXedHbXv20$Z4p&6oPx~YEuzr%rPDx5G6FgMcCQAj?rI>MJiAZNKOpn$1ZafS1(n{wl-2lY z4kMFDQ!ydobnr{v;ne>+Zq4XGCLCFh;R1l*qrZAuNsbIvRRtdFXI?Z%ltmh@co$YV}>hD9Xn^zuqe!! zg?dzC%;XViKkOITS7gYd(BvO+A~=vNYMfA^dUz^rW$yi)O4IM8VUN)$_NYy zKM0f9g9KYyWCx=}c=Qc6^`(vxQSM@qLa(IAtz+nLv)m#@xG0hu95uec>W!4hYBF{Z zI*m%Nx%!y#9I*OGaq%$*nQqa-R}9j;cH0NJ<6v^6Pb9iySO0Y3 zqcXzhN<3@(6XEV+qF0A&QtxnI%?a>B8@wS%1mxCQ`7Bz6a7VNQ0@()TmufyF;gLH} zmAr8?z%WV7EKS9_xKYNa===LqHi1*HG1iS@nw0Y}MT^|r^WVjoW7`Qho!qm4{$IF= zKV*|x8D1*L*Dc5tLUab`>gMSY&V(KF*{79v(ZxNL;F?+G_jc(B5&`fe4S`vdUb zc@R%vPV+F0aLie}*X_N3s{di^S9t4*1+in3 zBIn9{OJvLzKccoEl24OZ+P8{j95SiQMc_c66OrM7_Bv#D)&BdVm%3-4DuPG#y?2(3 z_IiDo6f()Mc2=pdJ5SV@2NipidNLXolOvUOZSW?sM2f1=45kAwa7oIePsb|+VNJ2@ z-Q&yRw*lB=Qu~7sI^?L+=IjL|3DrKjuyhHT6$vy=gFB2MNYFnK;J`G9Ih3!TV??b; zy?0QRL`2leZ)Ns%3>a~z%&g%XeGbJtv(m6~7T@6h436sYB{cWfv{X#Tuo5%cR^9r94x-d)CGp&_)o&h4^C23r7*H>UeFo{;Z^XPq4u~o8+ z8bpM|4Gy*)OJ{7m;A_ywb?`_IdU2frXPj@lnzOt#?ZNnrFM^~zX3Al=*>_NttqG*W za9!knTy>goQ~XADFC`jo&7w-2VM)N`Npuvwc?u&!F?rJ`T1$&i!ar`N_*h>xlt(`t zw;RHr^o8+L@}dP$!cQIIs4$-{Jn5nM!Hhl;!so9NOA<|JMSye3zui^NtV(uCj*N6g zS_?cyZgjL!jM!FtCspoj?rTUQ(%K~ES#W~Rtrxle&XA~tS6;NVu%S(84+7D~%}Tz6 ze7cp}+vEq1R*cO-(C1ipJqeTM_>iJ>hypY0ky*GVd97hG?uIbnl=uSd!g|J)VsyAk zUoYoEJugOm0lNd6vXwAmte6AA5;8%x1!dr3RJ}qL{FCOxzELrZ9;yVv8L!3Go}_%i zPiXlFtgpd4H0VT1E`@=u!n3%GOq-^FR3R}1Ui6JJQ(o7tRx^~f+sni!cNV5zqi|{m zQ^7d;UUT(slA{@{8qUj~JMl#NbEKfGHrm&0=f7szez~ms5At{7`|MYM6tko$u-Z4e zc6QECgh-YVyi{itqJYNTt--WbF8widJhsVi zBN;lsik3!z1j(mMv=&jWfCsuD#Scd6WKo%AHKlZ0w5#XB-_^^PpG&L7>8=(SRSiO`2#U$$Ur`@+u224{!LY_ zNJpM)tq2_IU|f9DBZiFj41>rqi3jV|Pg$RgNcvuXnA|{_ zh$&9IcBgVzPIm|T6gKcOF?N1rn(>B@Dm1`BKKlvM1E9on8 z5;!9oW7EWcaMqe55kT2H9LW-g`g`HBSt}2F^6KUcF%Yc8((W_8(oB(U1+#c05znDx zpvCUkeW~|p(99gAT*~&x`V;9%s}NPO*Mg|2md;E@tU1ydt$F7W ztbZIYf6aSPKdEudh8V;W2~hByvf`*Z%jy79bYAYaUc$i0K=4M^@Ssl;C}y&h2(Zb(E;&5~Q}pvs(0Y>*S7 zLbX1TwTT5+#UpBZ~ z;z$6wjgRVcN>W&k!}3{i!9hKIh~PwVpUtk;cPB|tAtwk{h*9_cE* zW&yX$tlN;3T-dn%O4R7AMa5*6co<_nlyPuGYA@!CI|K5d0}sGz5Jd$-HE@Pvj`AOB zldih1n$dq(j6;5K#9nuo?@ITc$}7kb!JXXZ2FPN#!l601+{C9E^96(GOZe#rPK)F8 zK5i=O3~AO4yNmcI1Q+mCAgF`5#aeUwLqgc!An!-SxSr(PXnTEDOqs9#H0jj^Kv!(g zEz$i%{4<#+WR9wk^oPw3^6Qv@&5F@n_XH*vs+9oMEVECxfVK#=+4je3zubicCRHwi zGGj<3f@q5rL)+&7SJ6kEeb@`AmV)-{Sb|8N{9{S}p7ZFb_^M!yde2;u%-V-E8~f4d z2NvB60n{cAl)o|x7MWzeEulUNbxGf1m$s^%xfeY;BGxZZ!jELHa63dyf%@A9zKMB| zLMQ)w?5LNfuWZP>;vmAxaM>T5?D%rpb*Dq7a|4Br?Nm;L|=BZB1Df;sbJnJg{ng2$_PuE-xUynqsco83D%X zv_`h!nSv1Q*W9=H9f>k9o+7!P^2bIH z_poc-5P1H6LQNm@U#KEVh&$^mCi*0Y;{E%lDj}~87L4{ghOfBhVh$>+bZ)>-T;AXZ zQayqU30BCpKvs`hNr@yYcx7|KH951v*rk1`A~q@6sHE+b*;bp_i&3vBZ*JEK*3hOW z+kVrvEX{%I2(c&gBnxmTEVsO)tupL7u!xlCnK`DbU{ngCGgMPWJF_LS8NLkn)ST1I zzR<*U4?K0Lm8Zu{o_>^sx#ZK1j4HYTuiRF5R?o^uo~Nw26OxDXm-Ao8>}(Cx0b1eh zOJc`>7|+;u z5wwD(p_NcbE z#UVEfHVj~cO;4=TCnfx-FrjO70OiULcC1X!xNmD)IZ3;s@*kf1tcHRn=`7vJTdCjUgAw= z(W$Q*ZW3aCmkjA|VADuQz6*r=3(ot<)H==AA*=ZxlKWLvSj^-P`l7W9slD`Z4b;jp zSoE)|DrUh_ZXBPY=61COWBtAzFu;ukaT#_@`B98ij%^eet&-rt zSBk&S#ehAb2Zr(W1D9@Iupi-?-Y3EWcYaiJTN+Pq&Xy01D-(U$sV!te%{2=m z5l}`-5JWC0u>E@EUvq**L)6<+j2;s=Q|i%{8Gyn525(=peLrXjrzUbEA6D0enWe2h zfekWqq_7VQi6=-RY1~L2b?drYnQL`pdq!=;@f18wJRhEHqLl!ZF%d3 z3^I}~`p+yVQ;Rp4sF&D^YKS!!3=2nxqs0u8&^#>$7@;?H6dtA3J(#Ju8lzCssDPye zjk}j|qBT+-G>qG=?u*wxgI%)PA7C=~Tf>b#Gy<~o+v?iPg+LAE7ePWNHKkptk(}Yb z*iCezOhZmGrkec~Ljg9~^PP^Mx_^1(dkU<2qL8=*uuj!qbWt^Fq9V`#leh5+{95k$ z*cbl17f$)ULH<4!{yP4?X#qZSe}@9U(t&UJ-!Bed&)=UdAK#E+$H5WQmmHs7q>w)h z?~2hx3VK7Y!>8pRvZ}DnyXel%b!*4HjhE;f4YMBCoa>(NpC8tWma)2;Pnsla8NKah zZKDnK9@i?Cm+me8Q?$C8&zdxAtG(?OZPyE%&zlr$?T(Yr{!_f|=54bL*B{pW)p#G* zYL?x*n)8QC{!`AI3~DdEFL{oRy)S2M!wvZ#)|~R{|5FN=k-FT^n$&A^y)PGR%MJM- z*Sxw({(H*6`*O}U-LT_ft#p~G%l)+J+_&RDQ!R!ak89P-_FeAhO?tJP-j_?Z&$@UX zb~%HIQ)e=kUL}n}bGjs_RSB+XGJLiaxspj!4zK@=sP(SMIw~)i8at-0#5(9NnVK4x z?sR{)ZKQhpkLx3XznK?46h_I|DE^2GuORWj!v zrcb|(TY`Q8ueL%qYu5SZKj<6i3~F4-@Oz=^K&iRRsedCVLk*`E&Z%a#p;M+&svYg< zq`Le2?0+tW`_xb-rd0C(K6XO<KiT-8e;&!T;u?U}9<%9~~!=aykg zdZZJ{n`RQ%lwrzxq!Zib^*-1!5HwLw=E5*aV9PdKG*M6I!Z1#7&NlpaVzYy1i*m^# zZZOl-?MNq&cgZpiGRrjdNT-N*$x4V2Vb8!M^GK(h7t1CtHOut(kxnHqmTlZ(mTBA3 zZU)*l8a>x=(qz4b3&R>gCD(AnWWBu$!v+C8w{keua2bqJy}NwXCYo)_z#8NKAG$|G zdqg(rnOvsnbWkqY|L<_nX)e%Nddw4hSPoiVT0UHUSRQ)fe^($uZ~Qyv|6Trn-Yn&_ zAMW`7+f;1=-wv6VZ}0CG7o)Y~c|(>!A8N0K{eACG7su1%iEDehfIg3BO5Y8J&o8%@ z(+&Wj_wyp~b!$F7-odcP_wl3gx*F*Hxc9jEwmY9b{h4;r&F?0KE_&mkm|NdjX$M5N3QTfWS z`|Dv*Tli~#S^28_z76nl_tj%q3z)_H{J0t%FQ3kL5`0bnesIgbOYTsvH5B}O-OTrO z`rJFc+!px$crBk6{xShRWeYvO9(zptA11%{S>_A+*ZX@uH!dp+ANCLd-Jj2rk6V~~ zyeZG2_4oF%z2DQ@cLAU6#QM3*#ammOIj=|m0!G~gthM*gj%>=P&52r+AAEcHS95cWmr}F`myRG3pgtG0Qi0~`afGXsO9(g zJGr!MA6$AQw0s$DRigm4j~{)-Oi!`yD9eyN9&3btAEt5pyX2*39v>^#YUBKU+HT+TVAVJ70-?ou60dvn!Rq z*Rh!an|@EC?LxImqsOJ|u6`cr4)$ls=gypWm+0QF>D=$L9otn~CNR!!jqyE4g5Lspw`;~g)Y{h_3xg5Oj_$UuqHvEcxtH^M zcTBEB+3S`{s>SsoKF^N__bzCX+hA*fW+L@s)EGt-h$L)AZx?!|E}$;kc{P{CK!O zR?hF*Q)2?YPfbz_`vD!^JJRVXYRrpkzgHKB&n;^mUiGMm`g!|81Ki#2Yi}%s{oYRR zoAUB{JiVV^?mMv0w|w7@>N>E7D~ID)iG}@M&jzudjn5X_lZx?T8gZszy_Ws_FZA@o}ZyA-??PLRS#-jzHG|~SLaz_6iBeqQ)@SAcUzcifNJO29fxE=F#)-u1-!|P=Kt@a_Ee{;yISg$-gGwI70jr(4% z)+WB&%}Y0~wA9BRU3k{o z@n2s|HDBPLpHFwkt={W??emuz`_jTM+J2=glHBo3EB(mbIJNT) z+@_xqsF)Jkie&%Ji zz1WBP{U`jIPQd=wSNyn(zWknM75HbSDXT#HMy=C*x)b3i8et82){xuA!J##`=khF@-e9P0x5fpsP7;&^@}{p)<8Ec_{yW zbhfRr|G4QUUcNcrUSw~NPH-li*1!hdA2h7wN#Il>+e>>rM7NGqK53^XpyPXNb#Zd| z2j_lGxxF^~;PgE0Lyt2Zjt?0ki-f;zz6dfNd>o*EwhGK08mjFc zON_K!?zPmka&P>;HqZiGtXJ++zeOV-y!6UwUG0Ga&yyMk;o2kdQO7UbeQ|{6hlEUV zFBV^FoxYC&-_FCS=g`ADomHvlFOeR120#0R+k9=y1C?^Nyy1Gz6})UW-Z`I6`Ik4i zJCXq(N;$gUH@l6{&reH%r^xTb0JXz&e&}}PuLI}oq2POm%bM+ujq3Gq+TmLg%UJnw z<+XUG^b-TDRZfTNd_im2#DQ>|=B3W>d~L1$yvK zExUZqPEPMne(k8m^4JgWVn8p(=HDX&ucYr|?|NH~*?Wd&-_z9ha!Idjxvu!Jx7M## z4tb)$58g(F^|wg#m7;`RDBK?@S)+~c#`|HHT&dhXtL1NxOp3ZV6a6jeUTQcF@$;%3 z_{p|#Kf712{QALsk3ZjKfBs%>7km_@^%MAolqX@5!hFjG_~v+W^8Ic}qv)|$`)ppg zuGSsvP+s@QC%cXby55-Ewvz0Ak27rU`zieS)nl-flz7TB|E=o@N-ra?FI4X{) zO&kCi_^3TOY~`|4v7mx=gP1>*(<8SU*ocOuwDTxf0Tu)27^0tSZ0d zdy<40T>(v09LPLm@y9HD5rzAF63(aah5?O(UbkU&0NT{J%M%% z+-Mz9%SqqxqX)ipq)(VxTJ_xX0AD`ii)?v|gs~BOc;fHEXV2enj?)_%4mL^zz@&0K4~(H$=mq zs+3aRY(6-!oE-2WF!(GSsv*^>A~U>Rp$@8o0JEN$F@BYzI0o=?H?zn z!3T$<@$=!RYgbI6eh17g_Z_GG!~@FTyw2&z-7)LzVOXpUyu#7)^2Gk*i}T6C(P_ub z-yY!=U7r5Q;)|20(R)wW>0q2%gNne1b=9ncc;MYWv_Yfo@)QuO@9;R?b3clAi!xm} zs;@VgPi6YPT{ykaWTb_#v}j)&c)d>C^*tP?0({3#mnL4dshav#;?Y?3DbTP- z!(L0wMN(AJ*75mH{80}4*d|W*$IVz@Umh#^2NTQ*K$LY`MJ@^@LL@~}RGCn+%9n2| zn0ts;FhD|-^SAT`yWa;VskNttx_uCkO#MFQvvv4-68m?oa(dXE{@o;%=>BKEz3EHy zUB2l!eF3F*nw_D?Z`^O9!1LMCi`aI0i)xS47N*4$`{74rCP`n7U;CXC=g$b-@_92dHw!b&VZX#l4V7?=x+imfUpVUe{Z*NXG0n<;^GI6weGIv{KKmJ7 z=re2EQ1h(?ARHIhCoGgfw)aAe|Dm>kC zAJ{mryf$=htXv;~J6i16{@SJdyqL|lm-IcK-SQJRLHxdp_A}j^cF+&>*6TWyZ(}{v z-~m2XUKaZJ-7IeSzb=NW5i1y<2Paf5XL zryh4gL$>V~ixeoiGg zQn%$xUlS+&?cOse1e_`UcucF?8TTnmTM@>SXHrJ$isG-IVIRzE5#( zH+z47$=~*R?dmyPcYq(`n@p@T9Vp5lZhGts4R-S~$M1BT9g7urO(-ue#FVgT=S&CJgG8Lwl| zuXAE~`nzt<)=oQWzsAKox93Cr&0}V2=yRbSYrRf8dcWe&*B)QnZ9Kd!FI|T<83$c% zfQ$Cl7ox)yH#0+V*HiV2+RBXskoNZ72JJqK{jKtNYB^*Xz&+dUe^2JQ-r{TO*oF(} zo?5@VI(W)G9p?7#>%iXRN6u@JT4>o^_h6iCVUDcbYVas~C!*@sH(#8rTxPBj^7DPX zIGblS6zX#Pu|ptCK>)qE|0VrgV(NN?9z-xQ_arVXPK$l72hqQVD{MQGA%@f2c4^h~ zZus|Qe}cS)eLn=V`LI>4uq7cb&Qbi(&C`X+eyPEik?-jQS?UccI zO~=`a8yUi)uD@ckNPoY#gYh#o9lbvw^QEmW8@jgFfi4ODztH@75^q zxqAJj6%Hx+T^*BP4HLN(i=&9{wu&$vjKLJBBG9k@@bc<^IL@QAkLG(XE+x*e)ap_jp5!js=s{ z(zuZV-!rL7%g*hsPi6PaoW}pd=4X(EDRwTEKpj%WX=-yOB`Pu@^yFWe&cHj)2KuMzZNPj?Q{d zNhfyy>tz%Mam~P`($E}Ptt&e2`2=CyOcY8iIjw}*2X)jhZPh;^Im6=)C%d1it)QWs zQc$p{pB83BRg=|{+(qy=J;6@dK$kT(dQ{Sqm&Zl<=*6oeqlqsy zWwS~Sd9Qg${*zSpQe-0$GV*p!lW|gRx%!<9Oe;Fftt?ypN?dbrU>3zF|F)u2T|Lu%5ECIzF z+)AGTFg6aqah}f=qkt$LWXf;W_&bdR0Q@=RcQ&uL_j)KIAdIln+i%ri;A~nNatevU zoJBo{&=)1ELlaIIyNGCvdx>#-oB<`(s3u|E*V^@tM+V)ytUqTBpvK85|2f#yw|pi^ zeI~)e#(hmEpi$Q{aQVkvNjsV~C zUd)glr<5U78VLQ0rtPIl{)`>pQILSutU%5+^AFJ-%FHHEqQ@!ed>5?KrSk&6o2Ke- zh79coN(pkzVEoZp))NWwkxFh@cmA=&EoCNl`120`IV`$XgETh7I~hp|U$aj{&YbjB zjPVnpS4>%BhBMl$h$D3V_B?tPHfLZ^?#CLpza-4bgUaF-6f<36m78}oF)qCh&{L!C zGm4Y~p;hEsEZ0wL0EZ!}GB~=Wr^zLqNJMnn~=z-`PhFKDQ-Fmxwsf|f*1lVm4A zFCBQy?tOdDrxMivnQFDP_aZrpP0l2?)IM3y#fn4PO`hvF;Tbl9>?U!KHpH~K27sBj z)JT~|o5o|j&Y@`Q>>l`Yd&>ts`;nQjvVU5zhm*FJT8ATx5ho^@uMt@iH*+pTp6N;! zinAOJt@az;xYVs?IF`=prAFM?@0$pauHw=_qs12dUC69F1kXDjDX3)q(8^*XA#LUP z=>k#Y96e>ZF_X$tloj0jCI`uQ6S6)*Vj$;aaY$A>v4k!yUezB)2u@UOrQICy9T9(P z<>)kvOYV}dF6A}cyYv^AJd<#VhTz0{pr)o(#S#mW%FP%l)VuvpzlwBpQ}6+A8>pV0 z7I7EMLJ?cnm1+(9hh4%16AZucJIlUHBCqfPdE)-~oYLQL>_UD4h<$REzmi=S6eSXl zd=(3wE`6C*1H)6``~#TsXz;y7Q7B!@oaC)S1Q94CsR>JgO6N#y8?+oIu=z_%Qv3Mq zin2|wR;Ks_KOOb|%eJFPMere&(@1bD{Cu9_yi?NU!c+HO8FUw}c&0jaO zb(1x=TnqV*|4|kvj1TGAxeQkM!jdrDiHZ`3`Ak+lF|AJg)vsFK@ z*->n-q49TLa)b~cb|jA4El+5LWUs+-*&!cqR8)#c#Kz2&tDz9zv*bsWT1YAHSSoIE zQr1Q7x8XH4-w%||4Q!isEFiffE*X2-j5%;I-n37ebz?Fj*srqNq?G7hcCm3PMiTTE zctASn+0(w|rl{vlagh8KfG@;a-q+GHkbDRZMSVM#M`mEiAC&_B4yo^~5wNonj!R%Z z`okvCizKoKYjrKc_g3bXy$}phaPC{CF)Ig=^^2tPm!;>+?m}cC%4xr&{=ht%G|QEC zO3rz}kWpxA3zou@K_3b}sYH%x3lVJ%d`0=ZVXUpLMUWzoBS-eN8xwK;GA6R=YZbQ` zsNGJaVa2(;2JCG}(w5z0gTX0}cQ~xq*o%Q(P;!c~ktELWbzt5_WH&S1ZzsmgpWztc zkl4cX8g6DSvUfo~w~5mGttq*WHPOXbf)t6{NiCq3++#iwN5d}U&AY_XO)X?)d3x%w zaaffx2E`V=qAuo_=ewijuG+u0;jgS zLLw}TH5B;VS#rd~T{`rVUuEpOoJ0TwT&X!;gnldgf%2MvI#hNH-$YdnG{2pqpsJ&& z7CUCaizF7m{$;_2Zn=m)6g$6=%7~Ccr@nN2gW+EkiOj<-A!Eq`%g6h2T-NS88#A3w zr>g`hs$EiZ9y=o)?(^+z4N`uMKPY}|s@dQWz0MLX1RF$-ZfVh5VK&5|*Ivnty@Hg` zYCB2Eq5u$Eu<_`AQD9;32|#EhM(Q(m+=#jm`80wTezLzo+!^VgCsL5wur%nF>`%YM z)%$8W3$PRqZIoIpW1(+%MG+6Sk1tUZIzt$*7Hbg+lXvX4ikN>nvW2W!?~)ch5kA`x zE?}M`N>oJ0mx7=3xI;-gblM9_G~u*{+Ac2n_~7S(%%vmtxVOm;oDuS%kEJ+-~+j{@PS-YD9)FWA@Pxu+@Ggvj$ z7aTR&pDUBOOp<>TpJNylQ$iDSjtEzz_hdCVSf||nvCHOy&pHtaHGRSf(=Icga{|iMCxFL1&hxq?^mpQ8E2-RpD`Iz1QNyc->YF;Zhrld) zplkm$HGJdh$N@XcX(m@`Lps6hNM-%RFQOJt0XZl&UGA(Z0HN$6J!7v|RGKqE)0-LP zYSQf9<*poyPDXBtN2?ZT8?Ezovak?Ee?@& z{zvZ(i}xuW?DHkI; zL2&@4s*ZwJHryw&^y|wyHR)J3)TtX-{7`Yg!WxPS&H1FPrxO97)9Z|PBb}?PlOET0 z5|QOESNLvvvx(*4kdiX)p>AlGLvPf2w3yg6D;zhPz!cxnD4O!=z>SE^Ic_=MUXB5* z;#kvUBOOzWwPgrHalO)l8nTE#oWWIz=+Z_-IXQ}0nrE0I(;ADFzYZf@apdIQ<7Eo8 zcyC~H=!Q+FokHt$>X4sN*U0pCJvL;8#XGZWwUX8q z$KmzWWTT1^p)>ZAsRc%mR1T`0RzO&JR+0O*Zi^Qzl`h>GoDvhQ-MC%RCvu3NmGUyTn?PCe=O;ZL$tEvXWdp6`Nq1!IAfRb%=w zV|A`ASnrFlhI5P@cxOG_V3y$t_5$A0cnp%t(2SVwxlp=L84OO$Gg3p&3LMT>n8y9U z76c4CaMWc;7rSsh@^v%uKu2)DhWXKo1@c~4&#MrFLP45JkkC}7TTb*>=@YF;%rcIT zAZ<%5YIcj0)OA$JTLc@Bco~)#6RShq5~7trkD9b?U0K4;JJ#XZ6^ITDF1-6!n#d_c zo!evAF!sPIOWr%fsrE_$JBc3isGaoF!(m8`0ej?cokkp!&x$o+Y?AdRYlYB9VWmWs zvcAwkF9}6!PpvT7k~J-j^W+G&K5MXpl@e6P{)7aYoLah1<;K>^aYHKXGj>A=cB=*)AhuNxdYY*JOQ*4ZTy z)9JI2m60*mSafTfGSZ_#zv5ejwch0=%D6q?klxW^oFRk*U{k55iJn z?dB-=*71@wb>)+tB(Uxeyu;e`_b2zHM{!~elA<|qVqDnPFOuoT1f61oa)}x4x?Mik zFalg(b)84#+dITy1%ZusKB6N$Ws+LdNn4)~>9*YDS0=*z!QEdBDOV`39RrKq48oE& z%qTjm%HbXtHJWpBh}z*!E_7V|Aovep4q8~PaAv2-Ro%Z zuACdoobIW}%2qjI24auE5aJA=0YLAyP(W*c%E&(BZKvAI1tI#MG>GWkx&^bI=em#+ zgSxf+9LLsn2h)JpU9QKy*-4Z8d>ebV2|y%DX_B^Z2qtK8LmL%lWX=cY-OvTs=?64O zo-sMa11c6C2ggA~9WJC+%Cdqp{_Q88TJad$sctWW4J>aW$ygEaUIxzi+&5rfoTBpqRsJeU-$tIP`iv-jO3YOjU4l?I>LAvCsVF0O)<0dQ^+&vp`YPr(k72`3K*AMYuDb3YaOhIaGtsO%7WA-cVmCz=804<(J=~Oo?vGvl(FNyZ=uoMpkfVtSDQH|QdAR- z@i2}fQIZh@Dh-%{r_FF#J&i*taoVofKtZA^g6UIpFCY&$?J z5Uuph8h(o z9Y^8hDGoIOYmdn#TYyw$urkMPQdDg1U~7`WyF6kX!m8kS@$0YxjZzu#N(6J}^c=|r z+n7GwXj`{Lh)2zdqGwO)zJfg2x%*KJELjY|A)MuOuAQzId47p}qtoj@u_?z;G`8~! z&iI#OdCJn--JFay;#%p)*xlDergp9cn)gf|5W(6}YVw@JXU$r!NfSPTzF~L3C$N27 zxxE{$JmOkqGn)Y!7`}ufB$F>(@B(mw1#FUG6Jg1H<%xAxCLd_Vp#kzs9n2zOE&5sd z3MaiB%_-tw1?fqd!(7)PVyY)ss`P;yrq%q!K2Zu^RdL*tIbEXOOJZB4s&9SM<@;1tOjmx{&_ zvEVBuXq9*Fc~<9$)CRPq$%^L2ZzY<89HS$?Ea=J1P=SGcYDS+jUIBB$bL!dI5C&cA zj7u1L#c^;l62UW-F}qO%GOWiAol|>yh04eANODVbk%X(}`d%D~J%Z{=EEzoMDhm)3 z4^08}twm0P9igB0d|^;IH*R(PH_%~bSD*|dG7No#f=gKTET&Tr5cCGnp3SDbeXr4-^IMEvpjB5o ztnCCI&DnaD-7wPNrJwi1fg%BO;Yj1;2QGyn9$VR!9)XttlGh$R+|fmvnvOt|LEJG&$^B3{xx*!z0Dju4g!S`j?i2khSy+sM;*ghu;GN=9#{I@+ygg9Z7z z3&SCXl7!jNdd)eeZ%QQ%nra=7SnKN)t2o~>?mi7B%TYpAo$oG00S2ryOAWTVW zv@#(K+-EH8b;i*P)p6(IH_6hK>Dvkb(}jdg->Hb$iMx>EnW_S#YaHhS9dDu^o|kUm z%bvYfwLY0q#9E|KWiC%bI0Ev2f`G>*QQ+{{*hncd)tWS49mWSJ!d2a`h;Rhfm!bXu zD~M&f!^sc?Co3@kcIo5`FZe?M=YAU2$c+aD-2lDDR~sA&pv@U~1u}){13k>vi=i|J z1Jrd3WP0C{!_z5L*E*vrK`#Q&0KdC-F+Yp8K0o@Qbc`XMaBZiBW3NAv3!hYQFjKB%Vx}+Pd~RvkLH>HFl5Hgq6yKa7RD^R`>p27s^bViZf2T z;|%^0Rls@>(=9c9B)2)>&BX4>FkxWK z%hd-NdKYHTfUMo?Z& zQH5vaa}g};D74ZJgOYISY@o#G9&dy_9g~B3BklA(DOq>8F?55d5hWijjkWC7h9Oss zX8*eLJUJ|*509PbLCWx(`l@((MA3#9xm;djy-aV1wcc+{UIU-pRB%+@?*L z$r+iZ&X{#@*Xb4vFubK}NUT12X9%U)ixJG2C}&tE>pgmOdYIkHqFo181i;2pAosc- z2;i0jSBFwNdxA;*19NCCAm-SyQ2@$x6$)%E&wrrEz|2sO)fwPgs3%Yr9B*Rfz=H!R zS2T!;N@0C-$Wz4sVSOeK#}c&M~4ZZcZ{@T_+F2 zCpuD5F4Me%cmq$+9Rm@n=wF|dXfZq0+*hfco9-H-15yJLwHt?U2gxTXdk^!>Dj1Op z2&p=BU@K1&z7EQj^-hy10NLRbE-zmj0gL7VHQ;c1(zy&WltNMHm#q~DEs!=<*$r{P z$0SqhAB}qfg2ti|4=6sB^Ls6Gs2O=^{z12hiK#4m8=c~Kh#B4ke5a$iuTPc;%K%5( zc?Q#Rogx#<@dF3;2Yoc!SOiSM)Ot}4>RMu0w+d4b$kPz;hB@`Y%4M7xXANjIu4a}a zaygLq2XvQ%3~DA%{HWO0tA_SVwDuAQp?ZS!z#@c9jaf(+&nN{F?F*px)L@|VVl|Uh zSBesVth*m600EFCOvkfB?O=HTtSh}4c*U;7Oq%6vtgyHUYZI*hX>JG#86pJjq{r-3u#bfEJ@qG|f|>F;=6E=>B;i#IBO?Ik!;2B2au^lenA3q$uF0?+XggKVZTipI z4fG7|fcmSJ!#7w)ciax}?x(j^rl~@oqo{30p9{<#xj)Ovy()riA^T?f*LE;ZE2ZaEmdssjOu_%1#0YQc(pLJEjbOXM{3YgsieLb5>R^9;W6=y1IF)6fw0ypPt zgxpyKdbpd1+GEuNXCHX1o*r96>?9ZI2tH#}U208f>DJ)An@>^ZpB4xN)s-2Cx% zgFrqH(HPxM;aFf~x1wftUz5=m6(Kwp_j*oPpbdKF+^8|KlJ$NAYBD!1N!$e-1Gw4l*;8^3L7U)xnEFW&TqoiRgDXnxQmHq82D3T_ zvG~^dg)7jq7Es`l*X>@H0B@#{l*i6JAofwTQe{miJA>#}7>7ArfX8Q$zIOgv3^C}W zFjen~S-)#1`sjmU1mD#tK|YlANHu;qof5}S31)Iz94=6UP0xq;F+vlj6|*Y&V<&*X zSCg3SYpr>3+Jt(i{nWGR90`^ynALt?O_9~FBNm;IBXkuMRStEqV3KtA>{eU^UP<<` zR)KpqQ$z?NQ*s{%YC1NI^Aa-o+F(@%)sX~1fq0X(8hV|pSP4%z@~6KuZwA^r{d zzRsAaMp%z%xtBKqX+y;z2^=5IKVH+duqC=IAeQSU z$RIpHzP{Gz;kYl1P=#KE_J&CxOy%L@NhlC5s(0mRMojx;oXCQU3dz-a7 zDsXk7fCV%%nYqxHE~lVhQGT*r7$GsPUN@rb^A#2}9Jscp#8iE7qEVu&DuA z{adjj>ZTts&H_s=+`-N;7!eO8fcImQ6&2Q6JJkz`)l%7D%m~&&epmtqCIV?eIR;5H1i4F)_JSThx_ZeP&dA0n4BBa1))U}RGI6XZ69i@Nj7 znzHmby;S1@2T@3hz7&tXQleBordl(BB%y%$5UDAa1r!g!Zy>n0m`afj(I8}cNSNTP zByJU8*Eswq2F<|8*46^XV(lUDfDkhF{Cll+D!|N9a`cUqkUB_dDwraVC8h5YE0}H9 zlr#&)FKBS~tT4m*lVfN#TvQGLoYoo!g0V@yY;{J2;7+W4kWctj;to!a4$*o65-Yys zoPA3?Zn3ntVv5=xO($*wmLc}sDMsW<@HaTFgo5U`1Yv>lVr(xLQCQ_yT&M52em z*+nbLTqDY{&b>NltGnq&1{{dp!(vX2=>iQMhV#wDbP5h;bQ=tFSs9)YUh$jfSyvst5vsz5NAGtkbP#tc!)6qLSzleXw#o#Mtq`~l zmZD|=m$N=@J*0fZ?F5TFRiC25W0=$_ia5uWCfIR1nfXttQslZD->3>{S<&ADTgu zdaQ8dB7|1m0l#AEq7G*iBp4ceA2o&*B%}vmTja!ctAMEnySH-c(X z0|jx0)2IN1_IMd>fIgfCOwjWKrKzJw7-w`w=j4#$SIv7;yb90CWUL$(g@GUi?MfVV zzpjG1YfPwV6Kal2;Q}dW0L}CG{0ybJLWr2wEK4-S08gOEW*$dkc9tRy-NG4Y+tK=+ z+RpK-1pzr!V#etfUjkjbE?c^Rb-KfaObf-@+FjMyqse2G_#XYL2;%COgE8O%81U0L zz)mfWi6}J1IaOVLSrB3m6ddMo!lx zC7ietnh6R%Xf|1ir9h`;Dh@8ffLYNqc_1fn~i2wWv|PGn6d8Wv_P zz`dE|9q8k@yG~8lG$d$f<;3FVo9Gf^l%fuWrA`R4`;is}xRn)7IbN(KTEkZ1Z<#>hfMF@G|gnD{5eR9Dt|pK}5+qHRPpwT7%3%_qoU%ZrJwctF>p-Qwua6taW;;au=^a7YRbOw{J4 zWXXo|Lo)HGGzZ&2nyClK1*;^UNvsHFmdSh-a7coJqiSX<5gHH7BoILJ*P;Yw@^hZ5 z=}mUa3NWm@4R@s00Z&FcJ*_~oiXTurfOswSNSpSg=0(*YZ#o$~D&)DuAIa&WQI%|!u?OlAux_2?o4wXmB{rbSdsap9e5`784qMLLb+3n zRg)oBLyc(q4$w4>G8*DIneE{h@&hZ(+PAElsK@Fh8z1ATdHT;Y&}8L@=UVUx1z1vI`T? ze^3Sz4O(|(vKl#A`X_F!ahVVl6QW111J-+}u??;G7`p{W`vGaGs*2lYk)Jqh)5UxJnY?wu>3ngA$%uuJ(H~k)8F0R3fvD7&L)(r-{oV zp{$O%r!*KLmiFqcLT8VO08_7>oY3b)fGJ7r#Hc-5HjG$M+nwf%Ag8DZn)i2=<`_^Z z>8Hf^LJxN3g-Mgk{U|0MSkct1K8B7ancQ{R>s&Nch?@v!3nAFCshBWXq`{5QoKvjK zC>!{t1bbrltYys9bls9ZJRBs%fU)VyO)R=IG;}CvHdsVSG7{9O^B!rq5(ruwG&aJ0U2r-y{jDyVtHidXn*_ta zs4YsT!FUw?YU^VkKwX%{_v&zofHa!q^bWGg!nE*UGE`mOE&~ZK4P~*^%|Jrr9R*Ry zd0PboiXvk%bP7y3BgHOS%&f@BJj9$wi%BzG52_igij^%UsloA3Iq3?eJ)k0pL7Atg zwsi}T>T8nEIA#n9B+`jM4ZXs=&(%ZPUL{MA%FRn)1O<8EqQlitQwn{6^($Y_=B-jX zh!t2YP&8;OOjjvo8f3K=gcyO0LHdUJ(74P@m19YPiGrvxsEqHM6*QDjb;~4AzV*v1 z>?8Dt&la!iNt$rXYd}969Z&rTz?@E4>HE|NRLFPTti~ph3TijRZY>1UB#Oz|!el?1 zU-Zu;CdHKD^byBdMxt3En;Ip;R0~+?5eur5$AbCe7+lHjgO1vSM+~+HrEiZEh-@4g zphvrNUy_AJvI)~WCP*k+a1u< zVMsoTabu!Lu{C$m4n*vOKAL5MnS6G2%uY{<>!gvx1D9-1yx(B#tm0L}Yfps9xeyuQ z)8f3!G+?3U@!_suDd2jj73LZDk73p+fN2#J;GR%{Tt%pwMOG}5;s6C(x1b^U&`?PP zq;G3oA(5ko3}joQ3~0_!?vo!H$W1;9u3EiZYUahi0;VGC z8ZDRZ?5>N8j-poSrtTpIihc$xbT;3V!+)Y^KLw1%mlH2d2+rt3_A zxdxK8Hd6~(#x!Uskou+(ok}OVq^^@y_8Mx*$JFrE5DzF-Koynb2bk)V*ap;Qh)R|k zC}bJ@pOp!CTdG1G>S&g{xdl>{pf5`_b4yOsqEQ8wY+G%IY?N8MWS32c1BC|*N6M$s zLZQb?#{f!L*>VKxbu~=snLPP}x7$L1sJ3In z8lh++QAGd5U#id+mI8~V{>ZM8BVcCYp6{w10I5j-)rC=#;tz9Bs302!4H^SID$z|h z1$|&EAo+IJ&)_UYjZLBbN*X`K=Z3|dcOwJnuEr_(UbiI`ZZ)(N`icgRARf^rA%jPQ zhj}!;`6>bi8o1x3V+n_;38Z*M6fshJFsZ~cMzm)L*)*I5 zxJ2Htnxv5ENbSHy1wH>28V^Z9NBP;9=b+t_qrbj@(WllPp&jAObFLiZ}&yIOvyo`Uq?f z4H2SQ@w_?OR8Z%T_%sm!q&}$;(D^O#gxqlAwlj{3RXYiR+&SWp*W*#d1K}86Q9g2h zN>(9?DTZS?B3d#ewFhm^gH?_~2orR#Oa=t>3L_beZr`d@Qm;Q4-p*~p(x#+gCQ*Ny zYeVA|J*#z*F7emLj#K*Hq;bOx1gcmRfuw8YVyRMSS2T-zpiXFtA`07u zML0K#8JrTS)4A1r)?H`sA@gMbp5((A@H5ODa^OguT@S-z#ot8 zKvF}$siRf431S#R02c!+>sH8Gas(m;gS5FYvf&Y;w(krUeM-jWI7b*OPLns0l`#=& zMywE#c$7>8+;F5-EU8Y=@HuLyvl|8mrtwsq-C&o55^*9x!8LcHA;vW@ zkyhMtZG20PPrx!;W*p=>n(ts|?V}(ubJ0KHsK>?8mO@Ha$U#GF7^6xiMq0oo@vo}` z6$ih^3^p);K%S6iaKvj#K%P_fLT+g?M&M+{D8iCPAcQ1hM0?heCjrz%QyyhPiwTgW zA~s2Bxg8B?mmXn}(X4w#^gndCr7b~=00*Vx0a(w1$*d5vT=2Qj#jH~g<8sP!rWO-+ zMK2fg{xC2MX+~Oqr?PH0L@P%Kt(9>vrp9kj=P=Ui!GnPqmbGrCdXFYQ(tZFz#eB+5 z4gtXB33eK`#&i+6HZVkoF$GZiNhoP!wr(B-0|@&3;6}@)rwb9BCEjB_Fm?4#&PICe zhCngyS35{@5zhQkCk1^E$KD6|}%wR>VTZ+3Qxa`arc_%}@rx`#7_t(Z-AQTgO< z5*>p>Vyb|0L);( zFfr{E>#_?z(YHCrq1+G*NJ-XN1w4psSXL`3yA{oY4A_gUnBc@BoQ#&`hGHS{12k~T zXC>aQqtc`Kp$rmC)dH;quAi-DOjN-f0ggSzj#bgx5XOmKdT~-qjwaK*!;CA9=eraM z$!7L_hq1_v4pzqWERupi7By*lv)gPymGOU5TH3B;9wSh3RlI(_xYawa@B&;8bDL5o?O2^CjvFQQfT)S~@gV zTD4T&-aTMIW>smqU15l7pleY+YBt$nTE&qsJqlaY z@v1?=E|`iZ%2VppAbe<|Kd>oC0AgtxofJ^!pfJe~cL4H0H_QqAQq7jOdSuEcV%S5o zISaJW0}#UjY)%O=0gzw=$+)UMjad`4;BZ)!+n)yzFsi_J0W1$x(+M*WK$PSb-3TYw zCqU5fbLUGUETB+`*mh}w4hQ>`pl#JDpgFXkPp$xCE5C9)3<)-Li6t8;FDIXRh{GFA&n6ExgyG05ZNqk| z0gFgsGx3M}q1GxWxNG11<*o*OO-y+UcBY5ag(XEbL5+=H28^JjlBq)@R?7sHZol<}T z-j1M|$E&#b0{LY$V4_8gAgVOLH~WO?CS*m}g;_^yO7^g9$**zIR7Su=F3imD;*DyOkY1rh>Bq<+u0)t44 zdocN2#~jKfjv#W8EAkAk7f~&Eo|ri{`K~#6rppTx9r~{=j`At^@+z44a*;Y=fCOQZ z1Z5Lf!R%uELeJBBw|K7dJfIzKmbZ!vd5%ayBJ7(%w{*GAFuU0qB?mlDlO!{mHF$N= z*Tg0|^hTM2q5mRvY_}nW%!Hv^EK{iFl4R$pW;PqMYK%xXb<>_05yOAP7t_Ksgjg(L zVFwNCPr@Amd_|XJi+ol~Za|$hmp4gK6Lh>}n{Av1Z4+q>h8t-se$Wz-IqNzI>;VCD z4wgn2a^H3ZC=Z)wn#AM?Z@lD;A}a;T?c-XW;!D!bIuV_Cr7R2t8C*%)_G zqndOwn7bN}B*8VYo3OEpDM7|3gob|Xl4N#{T684MpL3WqqW~9c)*5nHi-w=1k zZDM&F(-=;Uz}RqD*dl>WtB_FMHsEPS&Xbv^a~$y@-~e zkx5R?Tjqf2&6J9=)in;M$6g`OcW z*|TL{v$~@wEC8C`h>{~Fv>eXaqKtIR<-bACwmXkv`bUpTUCpggy`BJbYoKALDcOwg zp~jEx`GaY?n+om%>KX`&olFV%4=~`c$)nL3_CWWhCjqbaNqKJ!jF}tKl`j9{iO41@-)@mIdb9lM2KzL^!fS{%u zRSY|rx(+60R`e<&c2e4ja1fcEP`y+vdho`t@^m;!>%Gz~9IFgQFV~I7Ayfj9sG$L1 zM94|9hK5u2X?99S4usEI^0tURrQ}kPzfN2J6L=cO7eoGH|6Y<7Y%a9Qj$N@9_!vM% zNFS;ND;SCw++Pm2Te443NEqB|JCzNXZcPX2y+0n31K>RYjay}Th=zj9@M-5KdB#AO z17S9^$%KhsXpzjzc6DAm9aM6W%33^KwZyR|kqDE6GNVR#o01laez3Y`md+OiJt z3Gjx)B&Z*!`z@fkz;ugu>2in5$F3eiO|>Wp(_NJBISxIfOWHm~iToDSMZp1IX!Ujt zMiO>~Q5oO5?NPlHZe)3FaB>#DjTUAd_Ym_Lg7mmJ7##-c19}A+wMXmNp<`Ens%#(j zj>P$(ZW+PD;207Zl9v!sqL&8Mnc-lP zp<-law32DSP#tYFt=jyc6PQ3+I6~OsuHJ~<6!@tPvmmI#~EdX#~CF6 z&~BrN32|VKQkI=?Tnj0=q=iv$+X~WjA*tQRCO}XWa;hR-+O~EE{dHOp6B&<(E}+bD zz+z{a6kEJ{XxXAFyUh)Ynm+0+q!PSmoywTQA=fiDN`zNo{E;~%B8%h1)zjl#-*|R9 zc$!qP{VlDj?k}C%yxm-c5dn>YaNvw(BxgW=mvMRC{Ie6R1SAWI->6Jr3QHttA9=i9Q5_>S;B*j1CW`G)0Jqdq@iM?P?!ZbDZ%?1{)>c zImbv|0BDf&scfIOB6U9KZ05{JTormJY=^DZNS-978nM{pFE}C;9D;UlH~#?rR+Eg%Lgt^r;fcfMW}FGp-qh85EAl-G+J3$Znx6 zN$WBh>4*$^ORskFm;)LZ>$d{rC^#MWf#T>iqZr-B%AGIgH9;s z04puFJn9fFZiu&;B7q+&?h(sC>m_zQlG?;H<^x7jptgGo1IU2++zG8XV3IL4ISM07 zhE9ikk1W)|z@^C7vexN78w_QtZ-fjCdX@yT1}tssTOf-C1r5G~@yI>k=qS}dHQm5S zH2&%RL$KbsNG2vDfv7aX3tE22=e3N zS^Orl?IX}f8&xT}SrIFmae+xLltmtvmjC1U%hJPT#Nsg}Q_B;Bz)PpHl=J9u0|VDQQt&Fq10O`_)`kbl-H3 zAq!gS7tuU~(Lps@E>%c_9!j?=zO=g?yY4W6gV6_=#b9RwN=sG(p*8dBi**>DzNg72*RFi$Z`P~+!;#1R?3%@1@&i2W^hjpH(-Q;5wzSf{?=$SXJi@L zc2j0HkF`>9uc6a+$Qm4gD|6=pR3bN25t)Brf6 zsq);)(=#YlCul+Vd52WC4zm_H$ze0}5&+wt(HT(bl0)G2uxRkKn_| z9V+x`304$DD%}1lI%s!eAI^(>x`w-u0Dzb((e8Kl4Xszg4MZj9-T^~(5#$J)t`FguGE!h-ckI+mS4$i?dnCY8 z(FPD+v-nk35~kh*O@asU7HP$#It#pLG9fxcC%{yS1&h2@39O^-FU3fEnI}?*nben? z-tR!^vKS>cLcl?sc`AL>yp4~TBhX-gkY)s~*33+zdz-W?ePqDVP)GW)PY|SsWDgv+ zY)LY;AOO*YEW3vW%Rz=FbpNc?l!R44uW((Xt$U=rpM;RHR0wu}vIGQK<3Of{X~{CU zF#@$8k(eZ@nVF&uHJ2Kho<}Lh2Vhe&Wuw@7^y#poMuK02_QU#N%v*}2uCtGGDr3#H z=%iLlKS^9#OlH zbPd1l$jb?XtJo>QTpJLdg3^$i9A;BO0E43sdB$Bjxg?z8!Y9LbBnpa(kuJ)7)>j7j`y zW%(jPiu`Y=bZqz}`ljq%x)oTgQ=sJ`Y*Ly&D47bifJ=M&S2qut&fLYTA~e0!K3C z8QTF^9h@q8;nQ&rDE6V|0!K~=JE1PXR>SGCIQ9X?XD*9bU)k*n`YKp}psKu;+i*bu z{z;_YriyqU14bb#7&$=ME>p^?jMq}c&}p;a!$y0<3jNK@9e>9ia?iS2R46l9>WAmeA(?@V*( zsBo-AF*m^{6UN&18R}%yWJy&q&DKyHX+RGd)jU+W-@`sfkC5($S^S? zp$ST_Ci$2|M3++m35Vk}MxBZc?y5Yl49`IuNrr4F@offMR-LI?V} z$4_()SW_&T&4pJ?CYf^c1NSqK=43)W+n%MvFMy-TZfo-%TE`v1&oEiRH%9{uk2ryF z;wWYkrjaT607*c$zl2*yISe4~Rio4%o%OeQaB4HHzT4gHIEB84!HmNh^*`1KuU8 z%_>Sbj~29c>oyIgTcCCyR(gA|HY5U7su-hfk5)USETb#mIe9s9x)z{o!=q5i(jtIR z!}DIsne?lWt|d^LrI4hTFtJ{?ZcBioRs^LL9vW@n;aNC{xo%>xq>z!Ira9^7P{=^0 zlv3f{MFhnSR(_1^{D^KHT1k>fHtk1S)}SCl6^*<_Oz@eVqEQ(VH*rI!1Rb(?D(>1! z6-&@2v>)297vrqLu$A~bk}tD*)krH^{P;T%Ceo>>G*)SrP;9E zcOaoLoW*psTQwp}j2?HRVzEvL@bqsayJ+1*di0P$DV%I54ViYp6jWEkV;-zxK=sM~ zYdd;1nL+lv1uf~OpFoeeF_DFVxx;b23+T8(gi@^UA~dDbox>)D0%=YrL-)Fbxup9t z>ekGHP0nv`qIs9dJ~lwgY@8QQByGuTtBJx{40!4esF}t$DgjYb1~|IQ7WqIUy4kiC zhSXnZ7IE^Y``r$zPBu%9SkxwfK=HaTR=tBr0HcedpoQ2opfIvQ3MgX~IqVW8K1Z@; z1p&DaDJX7-ol_n(U>s&DR>Wn-e9-GnV@rc#1lpvt+)dnq>oS0(@T+q(taFhlG7H5P zobcaK706N99oy@j-G$(6@<_O=rAE(=`Fh)ywnY;qvhZY&53G$=aLrRNNYgb|?t$LB zygK87O*uCXLHAC_dUDsHVcXuqL$FeYaZT-Icb73Ml+MJo1~~(4L7zJkxe-r=%WHC% zt5$twxFiWeYG`{sNm>UnCmzOW0z8&MKmZULt2)CpJrrsBtaE#)KB#BFTA?PdWd zu?r%zp_7V|I3xBpBjm8~q48)yZN4Q9enL2o5o60sO9Zu05KRM1*bq&K6eNX>1c?DB z>Sg^5H~}=%9^_3rYt@p|ry#18#+8AGAXO0zB%L4Ilm)35yv#^HY*oy-m%|^Pc(=eZ za|8^khB{5#ArxtU{y1VjLw?K6CYG7m{8>cg0#YyvL zaLNjQ-o5V*>C<7Ok>$MZ+ro4Wm&2BKp9#(hTbmh|oteqBVT{S3tezXuA~H0|PT!UV zp`XhzrWt$1RmnB)Qp+O2-+9Pt$st zSaJ=R#GGYVC|o|r7+lPAYu`(_LSaIBYL*~H+)lub8IhRfU&Y2dKvu{8JY=OwLQCv& z_Kj^1E$I{FyqUQmKWL!>Ix8E(k7I7O8pJ6RxhWVW>(GFxs{jOr~tg#(V@HYv0S zq|}^qC|DAE>^{hhpY$vX6J*W_xjw}WImEGzs7TSIRhOeY6xkRw2@RrcLyfu-iV$j- zy1@so0{Afl99OiW(MXk4&Gbz;W#bHqBput07H@FzR{R`E*dX~ zfxFnDP(`v6HV~S}sjL#8fz-D%oHVk8EwWT`6(NRx2{EF2UPZGRBqMVP%iv&%(GF=5 z9!}EpA>%;?@q*58-CIIHR)ZxTIIv*6iuodIo0{&9?;$+mq+sL50bWdiLhUIB%m_t5hc*w#?#|8y503(S50us3~ z;sjt;Y3ti68pM@G4Ovor)}sI-hmsI3-wy@Y*q^dPVJPl9h*l$@cVQj+>rNkw9ANt)fe+4Fr$M3wPpn%<_ii9b5wv$6w^gBFvExS$ETJ`5;)gcMy3>_n97vTmVL{}EOwy$Gdwwzi}fsx2;R@oUup=-Zj$fl z(Nt1Pjyd%4lYPD^R8Wv#=S8|NP8z->!qKKcF_3!^%Sf}RPh5?|C&kgA${lFh5T3jd z#UFWxMMlv{LrFI8VOi2km!k$2*3LX&$Tl51B_!?R;HB>1cFz`#Kelg!8iovG_ET%& zn96Mgy=lR9oI$e`k*n3hk~pEVsP0GECe}Lb(AP_@zVT118eZ_^V}aA6VvH`>N-|)S zB8K5m2$$@7Aq(0QY#QTW)*S(J4y31VUiyi0KJ}ha*BCRE5?U3d8LWn6Fvw;e?TjN3 z+YtD=@+g|N%PJ!DuS_xvDiG}l8n34&qV=`xic7<^Xyop||X-J6j zv7wlLtPi8KlHusxXQ-E4Qpv1JIB#n%gc(%*Q#EOKk(&ET<#2{YNt{ zeB%EhbaKtnfobBd$hL=`Xy0HXu|<$Dty@4F)G&;*AtS}u&NL8vh}M?6l@k%m&~(2S zH?;sVhri(zIU}&U|x?pbGof^71y+DM#w%!17kf}4_HS&BWeJB`E zS(IYoz^eJgkd}Jepd{2UfH1-Zv&;{!U7W=A4AT?US6YiS`nqlWl9WlHry~yQ&IARy zI0aawZ$9A}X;9jpC)9I5UK50i>I>F`$BLO+Bo05EiwBx!V~Q-f=XL2&kL2t=nNG?! zsuC*Rta z#xbu9!n%7CxqWu%MFbS$7=R-19TBGlePg;Y@hh4Jyh^y09E|kTI*~n`T*<-q#!z{I zByE``CmTpnbISSyES^PF84Y3u2JrqcCXz^of;#zO3}Y#0icHUi+zF75h^=%Y`rKBq zB9#+^I3y#Ao=xIN+^5mDJA{NiXgRF79Gi;X#nePg7`|wKf)GC?=tY{4?xh9uxtvjz*^l&=8vl6N4YP4id0>;ZU72RF*}jh zcD7+tlW>ACBKP9P@}|Z2WZsJ<56Dl4Suyq%0pq7d+eUGj2o(eA=`*+++u*)Q(h^Lw z(@_CW&r|i)HoB0yy*#OtODRfA9xR;6yiFqTi-OrB(7qv&I9I5Dh9X6>5+)^~w7)^x z+g}Mp$QX^gqCks& z;@m?DI+-U)4kHq~N3SR_#R;L?o;Si_#02-0i(lmTh$~cV?Mk+AE6|M`=G`D%v!Mpy zB>>Mgmop%-Aw?cxZ)ht4AedyDBuQJs0MHvuMk-eaM?mWW0>KL~O9yf*9WQ8w1~Vv) zpzdHwmv;M=)tx{FZ$DhVJPJZxz!|HZ!Bn42`lt947CNHAKznEg!%7C9xTC2EHnu*1 z=P0}*UFyh%C==jK+6ad4Wd&NQG?@QZV&K{447P-uu|-qCkv8?foX2uDfEWS{D}3@uf8wAtKFfYk~)82YHcWs@|2d76R9NZ1bwS}`zsC~@*HLh)97;v@r7iSrTUqjk@sk7 z)eR&o>h5(EPr8?J649kq5xN|CrO=AA?u~P3yrRBG=y~7 zXyKP2BC1nQ9tw7iZyc2fZi;e*ue8EFsX%9n6kgIi7N5$fobteF5tKc;5?l$ooyn7$ zBB`MUW@jsR@}W{vXF$6%DMtB+w4#kr3h{m*&qzW$m87N48IKv@X0@OdHKjH%wu~Dt zHpuekz?g*TYZGD%xFJAM(_jH2f_25bCL0kf4HT#las^ejwO=ine7_>5yr-Cb~&f;{XO$KJ(Oz}EJzD6WNB2Atj; zHhpe-?5VB{^Yb>N-Ud)M?ehTY6;rEAQb1k&Ml{i7|PX!F$SWlEcya|4jrW>&Csnf0_jjC*6aKx8f) zxw}|o?6|5s;o&^pJp#fWt}=Ep#V$(->?~YWFprRQt8$34=_q4iPzDpJk`2&-MR~|k zoFSzh>E(33P7*|NElWzZrV@X^Ft9jKZ`-}FIuTv(2mGp2RUOlcksR-g6N@_+q<|pX zz-FAe-pe`wTc-Pw+zb+HmREq48JjpOYg^g49G>E7meeK9ktk-23dvN^Q2N$)*ey5l z&-pIRy5J1!NZ?^6j_uR54rSRK--8IN++$e$Kw0d^AX{m^){6S}n57_KY{`P`o@-@| zm{G$-Lf7L4W_K1U+EV)m#2ZF6%!)iHWb`T;qm-u2{)Ju}mbJ+hMw1bgkZhW}m`B4= zAx_lH)tjP9)Pjs~maQeGrMGI--Mtj6vB;6MnOy?%o%0wExsoa{TQEzf?Vt&Yq+Dwosv3bz{W2-#T zHy690ZuLC5^T@bBDmh+L0*z`q+#GriE(PWJ7N#~P6GITw)Tp(Mc!5pE!y7`RZYOsV zT>%)3{BOeRQmGPEJseMX7Q=|>0s(stEO8`gkF{GIh8Y(jn;%=+jq+PqG1(?N2$7`5jMKh5BaFx$!*=;WHS%{4z4Q+!$xw27Yacsm_ zJ2BD58YFDItnk9%dopbv-d4q97Ow6dssafBC5{6@-{$$I2>(n7Fbi>3qzzqTbYfBq zttI>+s_a9IuS8zHWHGAUqQ?3_vp4x@dKjcVK|>^UpVw+kszBLg%%LHJ1oO!7_9+F9 zkP!(6H(NA`>jKHegvs|9DkyRYM>#eN1Az?!=rOiVF#UEEZ)V`YT`Zph{)@O}&Egz> zg*A!Gmrdfr@0AumUIY>0kosiIBwaUipVnX1cs}F-;U|W-0-h0Li;t2y(6%Wm@_oHS zUNAUNHculuU63ZRiq~;lv<>LUx=no1wUzIY5je;*GhSQuG-;NJZCPAz@c%QJ zm?YF0_jK&G3c5W?u@N>lXi~D&GF3kXP;p?zL`5o`)uQ~h37E)UVDM*U?%QKoVoNmO2!_@e0fCh?fa!)Mu|asGk}6H6 zm&l7+vxKvFzcb~SGDjuCZO3##Pfc?0`P>GoL+Z+mW6w>IM&MG0-#|jFL6ybw4oj~9 zlF^}^>NwI$+SL;(d+>eGy5B0DY;H!+w1l?zPeGVhA#0QA5O||BlVw@7Xo&8tAN;xP zYz$Ijih2d9J^D7xM!8GS8%lLMI7{4yqNCXW7#fE53U;a6g@}Bf178jXK11SL$=71q z^^5KdD$C4gyUD?*oAsE0F#O)S|5AO&JnAscK=@c9I}6fsLG;$Eyiw<7SRqA>o`8sb zr%9XO5#%w_vWetm;Is=Vfln(SGI9V#4=r{3M5E6@9|!fs;wlDja+)^?#Mg^67~Lsh zdd$OO#t8y~<2(1=-Iy9=GIc-A*sSVzh@L|U?edI94jtUHx@~;WuE3d(HpR3MR8veN zOFxiau+O-9=8b%!Bx~{mI0L$b1%U~G^y4%RA*@y#;2l%u?PwraN2}0vz??&-PBx;F z5BMpfX}#CSrMN^y)8-9Cmb1wpW^T2|Dik?D+-jhftgI4>K=|bNv9hS4`~XW#R)Wr_ zIu1s2M@|C-!8Yh6iDA&3%iOLs@Och=YlZf3nojQs#x4S_CXO;8gkDa< zY~U*uHXs$bs~Ky?*$NgqttD%iZ>32M=T7Q^hIn@r^8hX(@?Y=sN3d+trrzB>#>fCa zp=S6vfy_!sD9U^1ltI6AmGD?)q+jT;A4T&Qv5BG@{^VHl~9Zf#`hf`bGi zT6C865-=af`mW+6O9p zIu75mN5@TEOG;DtSj`r2DxV;QFcs_n^iRM0-us_E{rKwH?c;x0f84!z|9kKKV|3XL zFu*k6;k=^barXgVvGILyJ0ufjlo>xh$u9=9iWUkh>&I(JRJHO!cOu=eUJ|c92>5cr zn9ToM-S4eQBCU?B-j;tZ$`g?IQ-h7^cd2?8csvD#z9YB&b95d!Nvdq*@2{Ln3Pqo$ zCd_gFHjKIdU+&(->(b*Y@BLRClQ0>zYaX0jS{}fV8-ndX!n(*b-zYVfxn*@5vo;;AMgG^pOJVtTa z=%_+gGjsjedDD_@F|?|dAo>rvf*KwVNL(q*()Xq;2GUM?q#$537N!oU8K(oP|uf^*pp%Mis z0sF_$`TFBf^Psk(wpxwI^>;Ez5|GIAeii^xbGmvGft(LW^|sc7CYRa%c)eKuaPR=x zm9HO#xdaP?Da85q^$8IWQTE7dr(5;bxj`5Uu_-ZYS{t5Sts>GePHt-pIUkZHEoh|9 zi0Rx4k=p_5BRYt4kt zAm?E!XAuyBL()MZ@ME4IU+ZEBEKFj_=kwX6FvQRnIAffZs`MrGJJ|S*^C1c70lKCn zS^szad1wd|(1{M!o&#N3%u2enS7*t|VFOG}q`SUL5V=a%h2+xJV zF^6d%sf|F@wq4=3HMduAw==Rz=gEQ7&PK!5kra=?9DLZh%$!1b>Pkd8pB&Z$Uo^`G z+U_+wYS5&Lx@canE(%2qXAp|HH>XezLDUCELgxsAvj=&OamV$KJIY(ZyUf_bwFZOA z1uz5b!BubPT@W5)L!cS?4JHbG48=P1BfmCltD zfnwjjXvA~BXubDjB^wctQ;9^(eb3RsdgId4x7Tx~@&J(|K)~MWWlo()K&S?<*iD49 zKO(`{nnw1L>sqhU4U#g&|3TzC&j!X+mkvK2?(WxG zX2`s;s!ID=IgLxKy%Kjrpbc}1V6sn>W#8x;e=!}I1lO$)(&tgN3gZq z6wJe6#f&i7F@PY4N`5+cp?%Tyt%;6eUcyk!isGu?Si|geePTKs3JP$> z8f+eWsnfJzj1F2Z!O{Ndxdrl4c@Z1ku0LIv!=o-^uwa#6Q_iVkXcgCF_qpQQFv#y) zr~9e3aO?-zfd~}Nn!~+7jv__G!PAClDk54T$LcwIJCK^<=&PNXtI`P?|LS}^*92}o z8-n7PQgxSn$d3WfJ%DK2GHF~jf$wXZJomYD#kw;Fisx*|(lFxjiJdJQjcwFP86Pz& zIK+-MCWOog~{#?iXR`l;+Af#?JuCNQBOF5j6kCeh@2t8z?U-nWYlDd9LuF zv{R#i(#!4?My3_yIugutU1TKJXmr7PFXj3ez$PH;PE5YxPE-g^dG0Y`KdXr`If*(v zW*FC;JA&l5$&tIoQwzzE8@z-0j-70r#Y%KQsS={?j$I-@cDHbf!Gh%uxf)`w_GXon zz9MZcfS5gziTXR?2nUwmt=Gkn6S+8?(-KZiA!8koFNoY>+M*d6L?Y8f4}aQyS?P~$ zmT9LrnF@8gaRQm1pAH<=oK?htSWAb~Xi`~%{gZ1MyYAY|n-*$^51NOzZ9+matP0-` zY(N!0KhH5sSI_rpv|>5A!vMY$O(RC@*Y}^m#3=IDUE$aCYw>_7H4LXNCBM~)K*x|Y z;Kd|Y%Hg*n8-#J5vZRR)%MN-EW;W-#BU)}U)GfBVCz$n38I80|2b4kT&(lO}iRW6p zJ_ZRgpU!MF0r5O#s#1t-sWO*73mb%?%jKh-jk7lGo8kWZ`hDuaUS%(akPxCyw%X*s!n%%eKc8BQ>Wo8vvfKyC-ye;f)F%%(nB> zolxOOA#6`Ey3(-&h1Xm0=Z8{S6p&oyspTrnjW&dWC=WBg)2*OHgfxruE?xWCpp<0~ zuMXzx`Q34~bcAaah1<7cH5TXu@!~9%c-9Vc3M@__t|!+!X4W3@Ck#QYJrG%eRAo`Y z&KI5v{39+;x87=REEFqhfOMV{06=1VP~-vKRJU9y7$wwW!#H1Qtpt6Uj`@e(Lg)xQ z&IlP@iGddaTrH6et`$swqBbF@^hkif&78OFNH%;3(!h;$wR0uAxB$`M!QCf?tU#5x->QI7mp&ZA+Q z3KcnU(`J~Qg#^=@wAjIAH&l!dA(l{6W|$QLltBu-pSHpVa3O=Z^_2l}uQzYoTZB%M zTX)RXN#yPBOb_au=#T)N42WYW3-JbY_Z)QM%u2yJ6#W&g@{;=jMPnJsRXZrX zfX?jTLCH+5$U(NG{9HJvOv0NX|;c2o-~k!5C3=8T6jj1#|!tg}6kGhR;`$pqkAD zKEYZ4(xcgJG)T(QdlW78^WJY6YI!m!pJzk;=|Q{l{d#uSCE3$zC6KKlMs=iyN^WrV<^dOkP(!yrHro9)SZjeS1j*X;0rE{Rwt2zSnhR{%uf#Gtpf5{EMfw1xq1ns$QW=_4ar z4Sk0@l%*Ra&5!6AdpL@s0EsS!wtKdX3Q=;9_X0p;qip26@>-Rbj;#X;gp^DWqE&NH z@o86e02!7?*MR_~Q9?PGV`v};TSy+S=TdHzboFqE$80;n22?S!Pz_v>M2aY?8P3!L z^_M3q52}K>G%BDzRfs?5idW|tCK}DQR!^$4Ga$GmL9Tx*Gq5RRT2t>K|FcdN1B6wt3 ziwr{%>q{I@jXZ#u4ipxnmuA}|DGQstiD%XMg&YqgFA}r{%jFzDX*UoOjWh!$;BG2= zcSJv=h|;)FapN%NX01T_YVetHCs&pP9qk}yH1l?=cLEh7#Zu3(1Bxx94z@|e_lH_C z6oJl7T02W}r537ZQSQbxv=};KP0lvcLd?pik!jW%#>qb(L!_liJG8U(X*;xs!volu_)X zGrEDb8|3?Bj1a&I#7Asw{<>fTDxZ7KGOKwo&I+>ED7L#xPY>a?C@R?=y#Hhk5P3c{L+R*R^H{QlrH}897&ymdLc-Tpe zwu&msNDOCi2ipT}5GBq|I*cVlEfFv}gLi7}_}UAS+?RoHiXl+zl1!?7*|b%AFtRGo zEqh*%ogJNV^Y&0|F?czDo^kk>S1X1Q_Txp)$Le&g=v8_SjC_cTR^2z*jV2k9y#vcA zL_NzM<2>vfKH6V!H4&fuTpC>U$nKsfBc|OV6a{E9Q0y7lwku{Hp{z))^OW+#GTFfG z(SQ&Le()Y8YB7=r<98{K>13+`!0fO|9Cj+=Vwa#dB}geT!cI>59q9YibNxUGY%yec zCN=fFR4ZD@MT8Ho9@sW~o=<|9$h%;nSRsz-YQobo*i-|SbVsnyC2&;;Gj{A!C{0VZ z=_WNeuGE=qv*5ZGj2E)ZUWuJCI)HLwhP+IpUCXIv6BvQeK|{gu?UA_va%Tqy1w|2W z^sm^WZ&;zTyVJzmZi3E+DGdaYnjNcaNW83_YPgSrcq|i_F^pLUuE(AaGEVTXl}52@1O{X}%2-;i-n$yO~l8DqinX5MyopCB`1_1x9i zH8ATKnKBFS;EDW;B%gt-h* zy%Cit{ff9RaTs+6b^tU|VxwT8S_8b3+qwA7U3hHP2B-n(h4HS*z9bt7wxd z2=e{TCAAwh!nSbSfvHxFPti~?gb2-^1pGSb=r~yqM~>yuWW5j(IoEMM4o(C~s?(%o zbVK2=E2~=F#ceiVu8iD@@Wp9!pFk}zWw6CwI!dK6ReGJxjeY??OZ9xo`d8YHB57vx(4L0G ztGsEzAm{r-Ym~1E4|6eSc~X{iC}sHRoI9O>FvP2;o;#N@0o&t+g`s@0CtI23$V&K9 z&C0N#=q99$Jc8Xy*DN2@+2k)>94msHFC+=@i0nk?O{N%c$Y0OLl$kiGreNT%y{O9h zP_!$mH4ktmA&`vo9QiYn?keZ5C>n?@;I4c@GCF|aTwx$iA*-PoS2!dIg&A9o5#2SO zx}!wZHk+qFgoU&Uidcs|tPrjk{S#8UmaCm{@;k>(3`xlOW731xB_aN-?`Fj1Y!5Y{ z>+hK>9rPVa9Ar_24l8!cAcytft&2fTW&jm87ELD>+0ZwwER*viPI-6$UxD5rW;m7h zJp33q%8^Zlgl6pcs}Zo^xgw`gcJtZ%8l?#F?B&xmIzKs{WsucCIM$ICc1d8N)c^R;*KMSHHtWK{Cs5x*M~DO8aJ&YI-%de$Ia?DW*VTkOa|U*Z4{{nh!!2{ zp5@^Lp{!E(ovTRC#TifFh)talmIiFs;U-2$7_M??hBIt$&6Otmmr=^viHswtP$En? znB*DiK{8AvhDs^)jvhf>#mE`sL?b0hn6vg7m6v1KLxw*|gXTUW9q6QeaokGwtp!mW=MP7Ek>e!ij zxQg_79KmyOE)a z$v6}_hY%1wCp#o$FrnI%zhWM??gZjOn~gGWA_5dc>R`JuA=$mkQYD6AkSqEHW!V)x z`rR;?Au+K=Q0fe7iVCuadOowE&Vcb#(OC>I&vnVfibAQa%D=V2^u+&9HajvNa^$AY z-$UNCEzs^)QEc@N16{OUa;&kszbg$#g3@_-_)YXM!WI>S=$<(8ByqfnqEs20ff$ zj5deJn;B5o$j>Su_F|aKxYc!Qx-;=id(>^mA8JSSz<3pgbLWz46$?~zC!>LC|6l@a5*JS+NNx<|vbRlrq zD38tpTQ^27M%L< z(sd8##DNc1tRhmZ^x>@szMN?2PH)6qj}!^41~A?{101+%lqJaD4>P>T9ssa!^;2=0 z08XelDfPfY##(7=GL`8I8^wO9_HjN+sKYi-?0RV$Gaj(bk-*8~<*^Hjuj`k zAd-y{un@A*d}8Q7Y&wmx(%i8|-BuAi3=kK27mwsx4$QN5I_&DOtc04fE|Cn5THiov z#MYSEOE+wjSdwWFR44+`w0=0t()Yq`nx~$VWg0_kM#DBZO9XmXkto^k&+TZVZYVu6 zrL>M3Zyu@*PMv1#V-ZV;-D0E|t)0Q6NmPk6P1Z}pA}?>8(wr`5OQGQvlC*L@c$GOL z%KmT$&QTx~r$G%7P^wZSk(OyCM5fgWs8|aAYzCjSA!ZHnz++Yu2_eWPcT=`H zw$Wu-6GV;++vg@(kAV8H=BJwkGRH+)2a_#ZBN92BGuA|@dtjK|Q>ZPE@3o0Ja!ZjYd$#4Ie&vluh+;ERrJrAx-yLj*T4_c~4`X zI#`yDG{XN-;V@9LRf<(@o67P^HkE*HAb=AWxo+*)&_tcX1R`U%DHd?py0t@Q6z6_0 z>55f!H#Q~HH3KB`gaKAhhX9C00(23g6_@x5>1h{TKNNQ*$IhmQRc|E#MIw`5giF`G zx@%N{d&L0SDQ@=#S6GkBlq;ltQyk*)^X~f^pQXp#oEidSwposNf_4mgN#1Uiy9%05 z7(8Pvhv`ciM}dI~T5)|mYvu5zARu6pjI?mohfcUieiB06RJv7r1i#^^j2v~?Hoy+s zAPi!0YZx4!h;Vknk=#|}coESa2PP||0?}~LZUr3~xHWM-z_CoC)U$X2%AvONtB198 z@~nkNE=NU!6_J9-sf!3(!&in3xH5{X?7k3P&QwCxNW<#U0+}t!6A0%sYrG+~MFjw8 zYH9o;^2!m3V`pKVUKI;rGMq2diK$VW?7KOW@u)D{S(QCUTQ*unSFoPg*p1$26hlTj zf#p$OsTpYBM8d;)Vs%o%%?)#6=LRBl^WD~DaDyu&OnPhGyB22RkPdccMjR-43(V~d zl}$>BfnbG?uJwNdepixZ1v;eW`eiwItxw97%}p&}*5ITHL(KLraCl6!C$WQ0M@?2Y zs!GPER!+sBpjtFUr`YG@rMjf$VgzK+kOZp8Z?at>y@FG!8X-61E53n|)q)sz z4TlD*CJe%j3;@HAC@qp>TDQg{VHdKw)~U1lE+#$Fu*NqGq1Fs^W|W7n8iZpBV! zfqFs4Z&?#(r?~_0X;NNyr{ch!jLuxsq~XS!6Y~KXEIG9^rnU&>2nH<46W~FIJGT1k zb~-4a!%~o@{=hQpr?G1UArl%n>Q(arxlxwObB8LK6Ggnn>Q+yY<71P1&`60q4F`ie zM*oN?v%^9tY|sRv$DRf?W8GgkZ7HBja`q(3bH|1%nR-^nRWq4R}7-a=I2pgT6G z05V#Gq<}uqmLz~8r6?oMXw(x_HS7qeK9jR1ShtAUw<=;XItYmgkov1`fFOGcb{-6{ zh_SF4A%ugBi*K23%E30F>A4KYjqXDSDe7-gBC@gAhB6D0uJvg}H|t2uwza`jL&b$5 zKu z8nAx_z!hj!**bf&aP?s5NH=s9vWzB;@TzPVqpe-{5L3WB>y>DjIjz|DNXK*YA|(wm zNaFn+xd|#dR=j9oZN!b}W^F&5L+gs#lRA~qLA9KY-2~fA5kL*esI}@Sv{VsOl$W(R zo2H(X!rUS&5H&hAd}QRH*t65^fUeTylHEq7Z}3&5|HmkD$oa{@3P$TeF8S=|)*W{e zI><4>|Ho!bXli8=NGh_mwo^jSsl22<4Xwg*jCpDpGF)^zX@HEOgfY7a3GA*a)+rcg zTsFQ_xHXP>4o<*(6vRf{Iya)n*%+y~@{AoXfX5ZvBSJ-u3k6Gs>4?N2*-@e3p(;CL zIFk~_1?Y$6jOjU2kY?7ELI_j6qetxGKLjFRi{U{qD&<^2s)G}kQ7xvN4lL6oN=scaWU*<_uW_oJIg9 zdyW|afM#TkgzVeW!-5C88Ssa~E>Zy5LV|XV1A7#A(Ya!gRwg-nFV$+a1Z!TcCU%-`@`P3V|W!86@TwsN`lqGlk ztBSx-zH~Fsfn*%zA4SFJL$emFxA*hZ?t3((mASolzBC?KGnG6x|&aQH@RmOMwna2WfG1GbTV_ zYSHzeImwayT}f=&6@&9KDl*ZUXepC{i!=b5kVID+6O&5V+WJPYU<}dFe#e-Wqs)}q zPe(}c^D8FC8^N{O4Y40Nt?Se>BpH7Vm^k1) zDH}zc(?vS3Dw?}tI#>}yQ5q818)t+e9fm`=_!*8WHU_&jh@@FtLV6Tu6&6dfbDRWK zxs^h&v;&B!hA3hPA_6;sVPoNGJ(aOwsCH(4g<7}GV_rwE#-;(Y3CzPtbJ$}iMZgt% zz8dsHdH#T4T4#Xxikbze_Ud#X2TvlM1M=FG?X<@1PboxQ?o!Tjj)4>&$*lBq{NVYU z3HHjclHU>7f*$Y@`)p1G2!f0Y0S~pAM3H999U}a$Sl@`&lW&SR%~|#!46Q^5VW#bD z69u@72EO)4_h^84aPT2RS!Zr`goiGJQ;Y?IYuC~8^Ht$g8`*;}3Mh=6h@pFRmR#uj z&c`vc1pHT!4dCQyk`>}u0DkD3oe;cY4w~SZp|=>eG;n@ma5$jfCdaOjZSd*W{Z+0Ze2h|?h} z5VL5TIL!*1Ae=l#K{=V(4xd<4Xj+nD zeHvCf`0N+@u4t8Fos!kzWCoA2O!ze7VTS-p!Jyai| z`GziI2u~NKwkZw!JWMe1qnu)S{DMJ|D1IpLp=kR3p>E6hp-Ap&0w{X*?RZ&@?Z1u( zY&-$NiJx;ES;gtnt-GNF3mVURh5JOCRYlqB1FHc*+bAFL98Dz3i8RbQ06sW_9T~9L z&4Yx3(Ic|_Y@%6k=dRFz=u~mI@R8qvt5{Q{H>Y_4PE=EsLwfr!{kb+-dUz(#G3+&Y-|8itjT;b|;(zM~r+a#E8o zuJN;BsLL6p3PdwDD`2WqeKoB{vR_=DJ0`UL|>W`y1=ywVsBR8$~f(ymwGnkX&7X7@XY%OBw@l)=f$r zVL@4*PL$XrN8=Y)=RU`Uf$0cb(arH288IUws+3^QUm>1crq=N2vNM~-zg_k)&VH|M zA+5sSvMc|Kh6{KZ%H-2NJ40Wh^q6KD4Gv55B4a|aa!&b=%_jOfv;j7^hmKQmkp>lW zDjXipjxt%nzR7uvqu-U$$8oNxKkbAYMxzHW3NzsY$OgU3NZW>!8`aRc=lAE~R2CCj6 z<4UtGsj^bCr-L>`YePqZ1lcs`NxBGZD^qvg1k7gfTEtcK{IHVvn!rX%#d8_6yt0!( zjqXL0l?tybI4+yvF3FoDO(}8ZObMI#ces44Ya0#7ge0z30Y#$|YBJ}cRaD5xaP5nx6 z_p%n^P9a{TIUH3BOB4?Di0x{1!GG&#(Wp{O$_ECkpL47mIOeMJr zYHwDjt83=mv95zQe~H4K)`l4Si-oY~oZo$I|MPfTQFFm0O^Y-e@{SZ^bo zf{WS*4Qx^dqQz5WO%{SYS4TB|3~`pqClqXD^KqOrJN)$XT;7-i! zngDc0n1>;S>uwN&XTMZsA(|TufFE)&yxi@|kn*W8pml{?%1*OJtWv?Nw z193`f!Jk18j>*vg@wNJL!oh}%7%leA={1sESJd;Azl{?hPX*eLw*|q^ z98<`q!F7Pn(fB$b~wrMl~FV_9=h?4cuvX_Mut5*n*bn8+8G8% z)oC*@w^~QI)cp3Kv-Yu1!Ya@<%`%h%g`JF&ZES8Kxf6%as{Ut`?wU;pL$^E))PW z8z6I==^V|mt|w5DWH2-H#TA5U84=2(%;Toda0LGhnF{RFy9 zCzEZ2%)`p62Y(*Kjsr8I9Z7poGB+d@?Ndh%jA|~bTQoFkuc7?4d z1(m1{_9S7Fywlx#vbLV$7^^1@=CdcVZaN`2&9IFplLZb>pem2W@qdOGLs0=M1`Z?z zp5f5eoxV+39oTWR&sdk+)}Ba}BuVe}@tW`fVK6h+3wz+C6_c6I1t_GLj=I_dX&yWK zI%FJB0Y4fR3eK#fIw1*qFsg$Q&9x(I(^TAb(!*;$By?uTHs#7=U#Tgdb#rhH?i!Tk zs}R7fE4u>|?VGwj0*VZaz-bNlTzn>qxs@r4N)do!7r8_NOcfDIV7zGr;wpb4aoDB{ zfr3TGGsaO>oZ~yAtVBL;hLL7AV;IOV6p(Seaa@ad>{PgHrB)jxJafAGb7v|HqZp-d z5kO)FK81=)mIwGSI4KQfrcC{Wte(;f8WkbBeAW{vQJ)B=i|X8ro{ImjnE0^5trdqz zhAxDWWOgopl&X+onL@>Ga{~Gucr%%1Ia=k9RTCK>(m7i@t90A4ZS@>m6Aw zY+xLcnE+-5aG4Xg^kX0?IM7>xMIFh2uCWqqX0_Hbep%8wW~VJ79zzqierU+39}t~E z!U8!r@BOeN=J}r0oFWqz{sbQa5=VnrqvCM_V$__RKU!l#i}&A@f3)>%)U8Z-;A0mD zkt>msSVHEr)v>nW!}c+~ewadetxfO4kidup~?VqF7Dm2pYrKL~d$T=jP7DNd6n}G%U~yV1k2`exy4- z7zI`)$q22xwMVoZ1Re7QhTloIK(z~co<1drNeB*eIP6-{%!&ryfKOtY(=PcxtYPd4 z%^71TRY-PM#ZmF-neZ4Py|B9^2)I!=+z#HQ*xM6!LUSJE%x#qb2o)ZTY^T9$TRMLZ z&H!DhxZ@E^7)G#=54%m!0LG$BuZyBNkA9WRRAl48#QRWo7Tj6-9r#8Vfp!CpG%=jo zSKZc$7PL|;BTh3$)atPKD=ysOJ{JfQe9@>>&uPcuz{9r4jj&oPE}SAYDfp5c5{(L3 z<(>`7R*EaQ(dytyE&FnVjev#<$bTzmjMQM?XTsFP?TC)F17(GH;MSS}TX>*g{l};$ z7Y>}Hqks6 zNC~G>lkj751oJBjGBJ2Ni*Zhj6zoVNUnoP0TuGi< z1G36+Yb%L=21C)A^+IWdCO>86Zod;Hyi}FJV;o8>mKqS6jA@A?DSgB%0H3&#wuoJ@ z)A_9?z*K z>qFwaLiK3p6>(GIUsb#>j8Me@fDcVPwhQiIOF+*|Y>Pqlb&r{d_Lzp$!Wjl^Bk5st zL=3^50GJ4Fq57F`2pNSucRg2QWWdoz)dQ2ZJLpwT5GWF-MJPc_gO~|4F$~v7!r5h{ zK2m=t{h=sNEja_aA}FrzFk)##eGldZA9r(%Dxh&_Vb0d9QXM>vxtADL@x!$8~^IRfN* zaZ=%`pcbT^Seg!#9;(d7-@ICs3Tz&P*M$#gyBvgNW;ilyPevt*7+S~{ZUz_U0aQ_^ zvS&)RDK49sqM5+yjd`ZIF2X)+V3JX$8gm3vRT=!55fZ*F6$LB!VRV=sMp%SkHnT(~ zQDZbkrY0yI_JHDZ&7_OuzU$&2%HAoPCq29*Sq*uCb3y}Q15(7@h*Xu3WGffnOuB3^ z^yCm;CMNiW<2)Cf{v0*~@=X*=PRfKSq)J(I68zO@*nR2bv<1>S~o?-DHiJ95NF@QS12gDJedU ztr3XZ;oGPKF~Tfo)HgH$DnX(*3-uW|l+$1i<&qfK#CnC~wzj)qq$uQCL3Yg;TOA{q zPH&gxvCy%sFv|ms>M1%U4X)?0Wg>{YNx1|k!UU!-gc>w>&1VCkv6|c8#oFN2jtQ~C zz#+RS_)@=qR=LHcMeIBrVK4z# z0b4iuBslLWTCXk_Mg#1a;((^0(THXR5tR-m+TAGTdDG#b(SPrb_3V};#5bW~shQ8l z5Vx4wtIT>FoU*P6rWw-vIy*hcQ*x$cJ2p6+Ob;qZoc)3cm}MFVgrk5-Bd8GoL%1yP zu|sV|^h3-!5Pn$ioD~v>4!mOl?qcuY%~m~7<})=2&lUlI(#s4zrIn@BpJ|#Gim(fa zFEoxOCZ}Lwu0#VEU0!aIs|WWw9sC%{pZEyd5i>ez-=tEO05eBt0zj6l+V$iyf~moB z+=AX|c^V*!F!P3j4$kh%Cyw$wA_DAe{}`SjXhI;_jXaWlgMd#vNq`M~ptQ&|&IZqn` zX{ULP1=A;#nMhY{2ShCfHM|>kzP38NdlB(O?*k~&B+NI|mv-RF9y9KEh7+%W7ZBfK z=t(IKd{j;_dty_l(0La&oI>()na=QP)d*O@@O%!(X&}-N(K7ZL7LK$kks@2`EH$Oe zhM^*?N)ep~1WM^5|0T9q*Q_4>vf$HJo~hV#>)2CR8561Kd7|vy4)q zFX=1Js*C(yMd@^ZRPd;g`GuCoBp_=A(u&|T6`hbo2}c;NC>6!Mdl1_KP0#R>5e7hb z7cujA$1Z>pX~^Bx**0gL60n4eA7T=Np>&h%boo4F7&LJatO%2rG4{0ueX8*ffRBj( z1DBGJ)D93J2{J%6E9;JShUkd7ER?BJ`-0cVaook>HH6(A18POecH<(gBa9p_w71<5 zF1ZCl@I@dPqOBngIGm7!w{&GfF_G3{zh;4?^wad~a3SVMmMe~t#RN&Wa0BRa853jf z51jd9P}9~Oz|x(ADcbG{`)xw@KzkOQjE2ZGM&8t%*NZZrNLt37;N6U|BYScVIV>B_ zDwi8Ne)kP!j~P)6sx^oM8t__c$|l4#gQ(zUlT$S3h3*6nXtQrB%_JUcz%#u$Lzh6Z z3pE~`YHJ<|Lu5vXt|?lS*pxdbXU;sra~eXR$+}Dbl&>Q^ELSkYuG*d)F@fg+8Z)7B z&Nr2ns{cUz=ZI`VQq*V3AoMB2SM)z77}ZkE3N*&IG8j zQ77Oz&taNJF;lRmpVzzEAzKOjMcr6jf*YzBnrNs##smftVCoGy%Q#`+4T{X#)V|>y zLFOJ#(+k=Ci#&BOVt9t5<2`|XM4A96nXmg46@6&t>K)hD)}x5UJY88#hrc!g4!i24 zkR)M%#4=ckP8$aQ7ySqVIqQs)jHJO}3!)sCkT{Sqav+UnD>5snn8nmBoxaEoBqI-- zdKAtEmc;E>=RM3DOAsH5LNs5dtyyi4y6UacvAVS=y0_uOnB9b!v`ebTsh|aOnO3YU zj8}yy8KsddKJxwGn)<#NxRx!my<p)CZipEPX=*<;CEq#}xS7I(EV+-42)P_*C@|~O) z9;Ax3FVCU)Dl>OSv5oSBv%nJM*AfwXn^8{~Ms7zs94Pd0?m142RhU-Q8%3rL&QKcP zX_<@_7gmRrprQ>W7|ISYmD(+W7#eGbP;R2w$~$KgYe(BUSKZlx0)P@^kH1iN;|zH5 z9FIjUtdqcPXD4t$<2{kzk_dMRMlGc+L^6!}SSh&4cn0eN#w}X!wy}{&GX(xX-OJ7g zd@4-`1Da{`4CSoj;tNl(`LzgLiLwqsrZcbuM!_oxI{@Q-0J%t{gZ*x@dl5y<0|+2) z$inpK!uOCB0N5C+rR_;JslM6yTcQ(^q_k>`VNV_c+a_OqdTpP*R{|G3EAGh?*B~3B z63EUW1XpjmpKXj}$Oal#oJ?p2hSLpBuq6a4QwONkTExYv@yc>Fx9A&i9kv3D+gxkU z(+>ZMO0yIfzD9}|M0j+i)|gPoa|fE-q^tws3n^D{4CiXm5KbV6B*V&Rx6~dY%+bbV z7bUhHb{@<)9Nt~XO$hQR9j2UQCN2$*xGpZ82u8rHpaIUElu%A0jvyA)W=I7!d_Xx% z7EyJPwGDV)X&DTD46FnQfFHL(RlDf=?2Iw`bi8z~>{J{F`?w9z$rgc2%?+^-ILSi* zVCJSl+SUel2&@UYutA>K3>KnudK|WXag&w^yHO63YqmtX7xD^^-U6MBN{Y{Aw4$I`hpNYnRzerh$(FX6@i7g z$VD7vOR{ArexEyuS{`98q`Fcd!}0-XtWF<^G==$5pB4m2r3vj?Eq-&RwsQ24T`tQy zTGC9B)>sAw!2(klG0=*k;X9`U5R695Y?6l?N6-aw5LP^xhI31TEwXAV5`eq0Ng0J8 zkc6XEs_SRKe3ZT(&Y?eXWTxn2@)vfkIXBusfyH1cjBJ`rf*Xn~E}`F4vs{2FHDoh} zBU5#bdU8S{fNq_9Mz9lhl*nV&326HOR1K7wY=qAC2Z|vhHH?33HPRJEELN1a;}lJb zXqdX#gJBZiGvT}H5-f)hMdDSEPM?yG_DoLF z6I9D>B2{s7$WMYf!wapI(IOtr4*H7nXc6l>o>OgCLsZ}}xUQXM{++8+88X}rB#Tq#SYnh;s3ycGZt);4= zbwUl~Q8&ombRqLA$;7XMZSm00HzK5HRq;Vr3{xh%G!S z4GWFaFL}}gK4?;UKjQU zt|dEDB#7R*VoGj>%e^^yA^;4^|0O`j6~0O$Z}-8X3aZ?9nsor zD0C6XofVJU4~^sMc!KvPLUut}2(vUe9wfCp)&@Y@PN}4+KKf^tlu_{&d;@BSjH_g4 zO+%r)o{)^;olzB6E7H||;`Y^DCMb3cjNgIbouLXtEJk@FyX}02?YA1Bces&PCX6=H z0}cbH*=%!^@z}wGm0N($MO2nqo$vSBA(UQ1=+1BqnZjI_K!Xshg04x)j))qR`)KA0 zLHXOATzVJhf;cpC@Kv?RHnO1@#xW5srEBRRfhjZuZJ^z|)ts%1&fqM||J-T_9P!(R zS&FG!p>aT)B?4kbJ1;YjDoCCHO6wPh{$evEOD>(YgpvYec0I1MTQD5OIJ_TbC3JEo ze<%3|2wkHXL$iyRD%b@udl(L)C0ti3*%_?5CL%KdJ2#n58midzcrd?-+3EjfUxIS7 zIWPz@(hdzSba%rKvLP`sQEx7%5MMPGs0{y`lZHsKtaL;3qky!Hoe24|!Jph<*F#ib zCX6l>0FE~PJKf1KFuy0!3<+Lz(b1KdEQ*GrT(52#dNw_p5%_Qpnu#RB!xO-a$WBle zdx05(S^Rhh>X!R8e%^J6|7g6ZA|A71(HTHzXZ^fRE`a&$l{u^4kdg^S;IQtQvl}Fu zeRQ}V2QXEjp62Y%qMIZf%6BlSXk;2tv~?+fI=%T=!b<{I_NaSifFS@db`6uw#(CuK zkXgy1PSHP6$xNBRqcP6;THZme%EHaXzqV&eZn0oaAnk^)k_t=Wn-$eb4QNs5EFqcZ zP>Nep0O=>!wz@L`_6nsMl-X(1N~&kMr$lf-JLnh#l7&MUm}9BJyQ5Bg;GXS6?qBqEa6B|D=Tyg)8TCV(xnn@BKG0xkpiv`PiDKA5KvHQhuN zhEX3<7^7UVvjSTcs6&JgCuhM=uUfIan@R+**%VADlBS`=+1X8Z5}7})?^+FwVty!z z54LwP`Lyh+PopgIX5@25hYjo*%B>r6B^eDvFfHEFo0^?G?%@h5D6(kip-02dJ>xb$ z@Su`K@RJWsKn`gLjsPT#f&k3NQG$4f5rQ$()&=7Q_)fahLNR6&w*~&!@Nt3#2_C}c zFCB|ce2#Q$(KpQNc#bj3RB!>)Qa>j}qlJwWMwiatp<>AgMwNU3QY6?{%^78o89OC~ zSZIi`-m$vjA0EEC*UgxDgZcZaeiRmd9#qJ;5=H}S*Z`TUH1U^JZ|K@8-X+SZzS!-D z@`c?ZSG~;W=Kj#P6gSw1BUEBxF_@h|5RFc|uk<4xnC8GQj5W!Ri^N>n6C;i_hO~h- zXE014`60{nc~$sU8M3J%j)pH_`gJDIw9uOToj;fLi^dm#;4!-+5e`>?o>GDZHz}bk zfu2!l8VIvP*CdK`?y~FT1)-%Hh{1hwkU0Ud`}{Du5qxQE3JygkvfxN7$H)U3ySw>d zh!q-e3(hl5x(00934TcSJr&JS2sA2Ah?(RF2A6}p1CuF3(q?GXchrR4{|1hUR~9_{ z;;ggbwUhU(pm%HHWEp_5YL3=_ax2i01u8BZ?yj{??6pP+*C6P%UefP!(^p*y4%gR^!h ztlX|+G0b_}($GyMm@5v4gR9HEq5!ni`XFa-v;(kWY%nDnFEcWZ>T*&v*Yc|97yG#O zw{sWX$~1t6ffu5ic7V}W!IPob#EuY*ri|lEev7jPt7zvzt4cGGIol__Atuufzi%wh zb?(au;#lOdmlRqWj;j(4cJ6S-xd#0CaMJ53Je&Dxo34!HAjI35MPoQJtpx!Ej5iE- zW4aAPDJ#y(a2T9>yG%?lXtJe2c|D9>U_rz+GgIW~~It z2LgbJq%3Z{2TWN=AC>cqxhIgLOgovIId(vEn5xULGc3)uoQD%iMD zpSjL1sTUz9JDU_1;*l)tmDgp|&?ps314`;+I+77gmsN1^#9M0EXgE>T_3>NlF2D&@ zECCBtoYUGVbSObC*e#6GoyRU#A(Ckktm-^*IP?pHU%D!1oQ7g{pq{3NubyTIdlB+n za`%sedHbbsDmwZ$*M^Oge>>ansi2irG*^>AGHId!fjAw}g4&fN&Qp+V3pqO{VCkd@ z>tIZ0@--ME8M;-gW@26`A;mFzaM>Ihxfih$j5W+!9r{D^6gE#fwXAF*BTKw{$~PYqPEU>m34GI9?Wu%2 zcF+-w*+KgRj1x%Rt#-)EXmAEWrarY8BRNLAVN<#7kO$f33z7C|W;YeB!-O!ylV%Mm zLAhvfFhT0Wp*BtSb5Y*%)h$U_Q_S9>OGe|d$^1*UW>oHm+jPw8s6;v%6&v%d1*~uu z25;#EI5jvQ_}ozmmFRFy!S#@Hlah&?g&@)INv671>O#srMsO@IN z2RofX60!=zs32NVGC_>%#!fFdGNERKQ|HuUY|KJl;jvht8(d4#d*!}_ z;$&a+$MCy5u_{Iq8hmWVMY-5v6XoNMr6BIX&UQkPGK3OSe%eSMlM2Ol6JlldyI80S z`$B(idl(*23aMn8ZUPqx3MBc$Imw1CQwCO?OwytlVW25+9+1Ed@4#*$#46$>Pp@N# zlOr>`I2;BXH~L2x;7#7Kbx;x?$yQDgmf-+kNh;%G=M@i&3qlDBH`nh;?-0<6&R{%{ z#7zdLTURdGF@Y*Ok@!$|(9JmzIX<8kXY7Q`nHfV8MOrbTsxEj5iH{WLL_V3|Cdivf zSsp%WjXeT#ALk(;w^^G&p28C)zYEukOs}|-I`v`A}-dxC3 z7h|4iys+`=xwYb8EbE?A0lLTwD0Mj2$pOKLBOM1+@NL$n>jWY`xImsbdRxs1!yt*^ z0_WdV3)~HO>Fcl??#C6jjq&fgQPl;A(p;Q?UV%hHmEFgmvWoscCC1_C8g0@uDp62xcphsPR zVVnUU$nb#oDG!}=8UJB9Y=Y9p(;}cxUCWW5PR$`@Bt4KUsJP|gWPnVArV`ieNSr}t zPH3VnC4QnLMJ$Qx{Nmd<_fV50) zz_lU@!Z4$W0Y%G2WTDrWV_kn(jn6~0FVe>*$HVEuIu(u>72j*(X*?aF;i3r)&Hx+pd5Y;CT*e1O zX(EA8l@&8t)Tj?uGPp%?Q2==0a}@CjoxM0fJ(?hV>W)J-dU%xf)WE>2Hg=Uxl`!!d zvWZGj8&SJJms{7;jur5ujq?qcQVyY2H60F&&NniPS2lIjMdP`F{i)amPd2Yg)rC+@ zF?M&#e27^bwyZh#Os)%!-dFMM){-!hX*AGMSQ+ab;Rb{1brKnQUR0d*3=^#^skEc) zq+5D5dP=eWz zC&c7f32^h(#iVKVkF@4kc4zsKTQU?qnZk>w#{*=Ks8|*A-b7T4@Bu@@UV^PfLROH8 zm9D{@jc^zt2<5_#$xL>GvKWDYqm!U8R+MZ<#$!H-NX{l?(uL--mC?O%lQCM&W=uny zM|I60JQ_(npm(jvLJLS(_EJ+$4ScY2^o<82DN-oXXig`AvoV|>7@*Txi~&6$S-@1Y z7?X4@HYiZ|1+Wtus!7UM)&rbuBwxWKk{l^xG5%Vk6Wjit)hWNpHllv04E_x$qVpzz z5Te&`M!>-FU0skYO-#_J{8K~`q%OLkK_WdVqUFcuLYHZ2c-~R;nqUrMDeP-*l*Q&k z|ATxI$rl(-7l*8DZ50Z1HHx?TmAWQjW0<_flTiueGSDulaXYs=BAVo*>91DXh?m-ACwax zO?0t=4{{RHQX8BNAAyFYN*BLBC&O`Iq8Hg1MG95JiKNe<5Mad=7acf>`$LeI%5c#8 z*o;#sT1JagMI{p*$xf}g$Ce4K8uCap!7!$yNx@C{-yZc-BU2;`uMa!eZf+b0uVJ2g zV1N6rFc)C&D8|@T69D6+Js~A)&b>b?E)|pouu6;}9p~J&9XvIqOE;qgFn5Ug-&Jl~WWJ)@oxmcZ&kV-Ovt#US7MFxr@NNTp@ zzKQ`krjvlY7&hB^thr8uvVhw0>~x(X7#BkY$2*(&z3s>fak%GhfZo9}p@X%#8S2bL zq0y|Rg=EX!?9`OgId#g8Gowc3d@oK`mSpC`V^<6d10c)lpW3u$tEnK%(s(&kgIrGh zuD9IEX;<+SfYHj-*Jz8Gpi<0qA$+^sE;O_NK#goX*FLNb4VORCVVc{ff<{8h?6PyW zlYqz^WoiII8RR)lS;NFo53a=MYJ=`#14m( zmVp{tga=lXI{^`rRT03)x!X+%<1jCS0=t}P83kR}zUJ+b8DS%Q?GjKG$IhZg4>BK` zHuBeQFy9U-D** z7+k+&=Li~?nJjgi*W(fi-4h#`l5K;Ncc; z!(k$j8=F06DGjHnDHd>;)fG@&LIcvtUyN3%5G*oO#pP~}ro>8+GnEk}JE#c4mCP>@ z@icD$!$hTGoZKxcKrz{}KLZ7^p{DFs)=FSTXUEqR4<8A&Ohz2tzXbW{kX3IAKQuYu zaDcF(IT};kN+{~I=%mrm_A69dIrW(Z5*;Gn9_nc7W=F@=lDI^aW@hF?=d0#6lZpVr&$u@y)LZ@zCaleumC5!=nE=k+fY0qx9K$o4L3$w7~X)46Xo79sPd)56r79bB?lhawZ~m#P-^wC#(GXz6At zV98*!Z4JY&09l=H<-n@F*)iB|XH?UPn*p6GIu0$&P$oZJtd|sPcAle9liHZuA?JWk zgA<7?=whB`3`ode=@95h#j|OZFn`+L2rCE{T51J0B5!3htqkKQ@mY z!WL@-crcOoWbrcW;JjGT4b&aYF%{r~1Fxe?auI)B_7W8zix~`;z_rtjW*h9lz8jc($G=&YssxX0UAQ9{ z0!4E_AZN?x-N?8>*4(5EN^|ODNsa4>ger9hHm&L#t~G~7A3o=vZ^RSQjfj?HJ{7jJ zFDAj(Qp@ZG(mjIz;)Hxt!FphV%_Rl03=r5;x^!nxE_-B1%KSgo!O;{kuvvRdj;^Al zBqHD;NthKB{22PRY*M+V{9R_Z&`VQuf`cM64eU&!Gj^laK=~le-9Aekk2~R2QW*b0 zF`BMYUZ07%Fr}MKv-%E(LY=9z+g%@a9~7#gS#nOrJPU*VZTsd>1fq@zcoS;w{4g>9 zmoc{wyuc*26JQuYbh{{vUnWor%{0#3Dl#pTLJjdq_yJnu%HBtw7~^+-txt`gIFZY_ zGMnN4vh* zf*-LaDN^tvp0Z&q87>?zJPnR72AQ0UX8h~3dps!W+a7K!DCRmsqhDyWfLP{?E2Dbt z4IC*%-<0m8Q|reH#~JxCC77ul_Zd}O0NpfmsDW2_(2%y{Mr(Ksh%_l#GR?7r5zih% z!Vq`awnA5?sz`}*VK}4fZC0VcTkCDHRY|fwFkAGs^dPqoWr69CLqQ;#WSMcGj&Ms7 z(%o8OWM~{B`+#bSPK-Hc%K4}Tdy#SzPtxYZf5=}MEJA-f7FJP&u1r;r#@s2t3hc2d z7KX`e8E7h=i@-{Tm8J*7+iF5IfPZ3C2po#A{{j0qkeTsk5zVw2%M|f)7Z_O}Y#3)L z%W@1Bge+LCMv+Q+%0xWbM}&BZGFvmy=)$ED0ieoCJc7wpl66P24ndyh*D^83WQz{Q z8wnx(V8#*vi?d5^lY2i!TJjmyUGsRDXjF}k+8~>2ByQ-Q?MxX&6G0+^w{7|ogx$!m zG!(FBn~fIH2FY&a#Da|{vJpaA7Ou3gdEvJv1>HgYh^9zoRw0iWbfY$$;s{`^<8S7$ zV*%XRW&2(MrbS%8}lYD59i1z9-{KF((i+$yFd2gh0$5Y{W>mON|^2Kb*v zMJ6);?9j6vjXu;XSb2zBTcRoeZckSBr>rihAr^ETm({FCvD7+fusxL+l_H%b=VZ8y zVy1^xcq`*eV=l?vQGPEe*tbO!!`I}|Eb~0AG|seG+*#)rAV{oZoUJn7e3J39sLF*g zYeAU83}@srL-d>xViKCsBy0BQ0w@TIsU!r3+MTCgXdfi)Wc2Lb6AJ9MdQ9O$=X^;)mD$ znJzVs8bPB>*@Ll-NSn)+9%s7B(@S-nTo1s( zPX$86X>BG6#V!Rm1fr~(JD+P&YQ?&S^pO^;*|0w2`?C2u`3)CKc`kIi2D{4t29KO% z9RtqYL)86q8zd>qsymdr;bWcT<>JF4H{-B$XHQW@QlLBrW9oBQ992Ig2hxI`jjU;!tmsTf>|s-a_jmqfBCghQ*%}sAr31Fu}06#DbPwe&rCMI zb*-{gKhc!o08R)_nLxB?f(CdQNqQ8{JqryW!x9@D*1Q=oS4QBONe(Wf#^)!{3$IEg zx!7=1LT@-LOvul6N)kHc2D&}wBk4M<$6*%tE&QgkHLHXzCNS9{n@#(SC)omVtRkf3>iKVq&SYx4&W7Q zw1_j?Q#|xgj0T3ReTo_=+fvIG2SFEzh6eyBB!u?3nO1Y60CrZ`PtZWjIXy)^OZbRq zXD~8w$?csq7lhNpu?yE;zD~roc${ETo@g1X9JX!8^AGZeR7`er*nF2b2$4N-V|3f6 z=)s8GFdTAbT0?CQyY7rGXcwvz0Lb)VCXt{=5IkHhQ+H1UvQU9P`$}3Be(Q6BpwUAVE}MSqCbyi95ceYb%h2RI^weVD`d~MtpgkTCj*K z1I|&a=Mlyob`~3)X9;wd!stW+Xs%unY`_A`FCUKFJw&I$eY}6lRlzzkW#jQacfq zq!1k=nnRVMFri&DJe_N@t`norI2{*{6ld^>F>9Yr(?gAqC#$By=6A#ATIZ)Kap%WK zpHmU0Q5huD9$~l3U`i&2hind!jJ%E@8{rm@U%Em%L6|3(H2?q7C8PX(N#JD!U_>24 z_;e()CQYv_dxW+4z*3SiY{0n`xYW}bBR83{$QBKXeX4Na1V_K7c_4pi%7GVL%6r!bDlK&OBBd541i4b&Ip-MSp|} zT8>yQzG%*@C`$#&Ik{*fpi2n^*?M@yj*|a&axK|0(6(X1V7~B#7lBgce^Q;b0jE24;xmy zqO~EW0fRp`OTcxKj0M)*u{LV>UvuWr#sg>B$=_rmGowbHcJveyex{5F>S?h#ubJpI zT3CbiA(!!okYL84p3-0u7JyrJVv6Hz!FeJDm*?(;6H(^qMPquS9}^*fy%zyXhE5Wn zvYreO+8o^pOH#Ekic}qGKFfWKalvq*cx0s^T?2UU&ZtqT?oF$Dcm;8gu(nVem<*1< zBc36YrqLPK^A@mLdls*!d9EkQ6()DR`g*=ACmyC-FU2wBi6~K+9wTs<|E9b|23CET z;t>-`Y$n%3G!H5aicB9#2o^JJZ>~ge3$s&HX-+>2Rc_+fl8a@qbL3U1A!vD-5EeH! zl8+Yx>tu@&o2r^LmB`U<}m73c z%U;KVL1lZiPK*lk&j56y{(45rg({Z_Uz3uUe<98XUecd@zA5U8?=VRQ^k2qSMd}ly33TNavs20<*#>~XQH=9J&<*Ip zHAY?~Q-LQIvco7^@vo?GO*W#dh&o~(vU`(=2>1{4#FMyH)eK_X!C3DA$r3JwE$%-G z00eBf?=pX2UogJHkl8z8`sCBHsQ($a$$Q2?L>1$V*dZ8N$|J?!g<_dakFxghfrAay zpW!U{3%=5#z7@no&OD6K_15Sb^Q{tZoK(8EGE7}C6cUOnOFIOw_5?*#6@QLNh3RWh zsi9j+!$i#!V=rJhj5gEuUYeK4t%CALsZhZL;Baf%51^&k7K994JhmY_LerG4Rgf85 znsJ(NoJJ!?rxcsHnW*{_kpxzRJtT=KDdQ(d88%gB29fkt$x<=_JOu`OdrR+xVzF-4 z@ibKXIEPzi_PBlHc-5_XVm>n!{v;68SK^u+7px1tj!q7}jCF?RVvsbn?)8vQuBENw z@q$AHKG@Uw<*XUgjb5sUC$lUf@%e1fq5jTCR*w!1#v?El<<1#HF_Ugj^T!5La#Qvf#gV+lZ!(J zux?7Mxgx5co!Qu|X=q){n1s>_drH0ySxX2^308w1Q4a(SdJnnqE$f?jmY})r!Gj16Sit z94SMSW?MDrKlDVgaV;H?!CjHy!D{9HM6Rn;)@~k2L@8%Q)?l8&fK&1pDt;rHs$A&MM^ju{=$aHKiZt4Olf zZ?`6zBNvZFBtsVU#ng#jK0-nWL(pLL)+Rl{>K%B!KF~w$Z*2O@`F)6$~I%j7OrX z&E9MpN;5icqU@5@Ug#h(xJBU}-8>d@9Tk;dYBnbl+VkN1VH;Sw1}t&Ja4gzb-`_z zEJ0DMn*W-@P%tvwSW0Ja2gMc_6shRYFQl$0Ja)W=$;rSRv^Uh@EDtJ^Qn;$?^16U4 z{74n=k*hKT9fZcgswBE%b9ebD_3cJdoMcere8|>N=7$f)E4$U79+HpTHAn!QG2K`i zDA*DF>6vjs&lrH0Z5CoB9d3agGu+UF101?+HdVW}XzHJlc6ydv23(3F4nKq337{aG zc96`Sw!FQ#r&-|-_>aTANG2Kt*w1)i!E4xM?N5LDC*Szyr>|bVd3gTfS^B}VpZwMT z_`x^7^NqjfkKa81xL_rP1nn zih5bp>!My5^~$K1M!hy|7Ww{tdTr8clU|$j+N9Sey*BB!Nw3X2G*hq5`@`zBS+C7{ zZPshEUYqsWtk;%}a|6$EqZOyYl~i6^xCS|R=u|BwN$Oda|0!oqFxmYo}g2_1dY|PQ7;OwNtO1dud$O#$Fs@ zua2;nN7(Bl>;)3`3JH6OguO<>UL;|^%XNwEceyOG%PPApv&%ZWEVRo?yDYW6*5Y1l zF<4z4qc>d~vV7xR+nt z>o4vF821W{dkMyJd2aT*>{S@|GK_m2#=Q{ZUWsup#kkjE+>0^p)fnsL@!RjR7i8Qk zGVUcA_nM4*QO3P0<6f3=ugka>W^9+Ic)!bDn{hABxL0S~%QNov8TSH>dxgfmMB`qg zv0pZW{Vsc%#=TDCUZ`=e)VP;w+-o)N#Txf&jeEJqaoJP$yX+Mk_mYi!&BncG<6gCK zFWb1+ZQKht?v)$oWox?ZO?z|NYd7t+oA%mGd+nyZcGF(FX|LV1*KXQtH^s{qx8G&2 z-L%(k+G{uMwVU?ZO?&O8y>`=OpS*08*PU{|%Vn>;Y?hbZ^0HlC_RGtLxz}#mYd7t+ zoAPDD-S4v3ZrW=%?X{cs+D&`yroDF4Ub|_p-L%(kDwlnGzsp{`X|LV1*KXQtH|@2X zSi3*{^{?0Q^ZL!rt2fU$gw_G{FW-Fj?C#^6*RP*FTmSXV`=?>v9~?C$OBH!nZ=&ibo2uiieaKYafB<>fuUNhkka$nS;tH(`FK@V`%Frcl1g z2Y7gK|L^$gYxvH3@N$J(t6%@@*+0L%`S``nCzrSW(K@AH-o5*p_D@OdJ?}%eSxY9`m zzWSHFcav^!XNotu-QV2bKgMz^u2Wv0{bqfO`)^)W^P7k7{)RXI{zng=Jioj7_Q4U$J=x>)+dL&Xm9qtuYR*~k3T~1_bENz=7)6ixGL9j z_Ym@fY~l50`^J58^XaG0Uwrr*-t)ihd%TwMU+tfH?{0mNn|gb9e?xlw?dq{^`+Y4B z>-x6)Rk!MG_d0m?{C>Sh!Uvvc8rpKgXtT1Vkvd653hl1Nwq9k6X1y%%J}=+CIrr#) z$9t|ncyagnNBT!EUqAolk8a+4^!bPC)AgqxKfnFLN1DIq=f3^slTZ1-`bmnP{Nl5> z-+ukk&8vs|Xa5`4>a&|S>wbRv=2`gm>*t@m{aD}T$@(6;fj6(;JiGho;qGUjzI^`T z07*c$zYQP7{_DxzM?d3}vjseRc)j+XwRH6LKe_pE{n^`l{%ajBatdt`RT)p$4CA7YklT- zKF+g;SFc{a`rL(4K%7xy=>Ufq25`9<~O?I)jp_JyDK{>^8fYJ+|J_~p%; zbsq81e{`ci`}-K_&Tc<@^P7I*{^QU6y4$zUKYsK4#p^HpjV!4b??rz0gSP~l>7U`t z{`nZ5?*Hee=s(k+@UN&pdMHozAN3Xgy1jX+f93c1*L{Eb_xyX_eemjg`r-8d=&zGr z^Po4lfBX-x_1=%}pKZTs(vRKn&%5|f{A(2Zsd0ImWIy|O`^W#ZKllIp^uPQ-AL3!? zBRqch@1MT>m%IP?UA;d4gHZQ5T5?! zAAF#f-|3Rx-hZI=b+>Qj{d}X>>xEPIYTo={>!iSr}?Yz>yN+2)!K_oKdmQlf4e!gKF3~z4^Q_;(la&o zEA>t9^nqr%T;NaM{@^F}O&|0lx8vzA_3!!k!@t%a-|yf5^zom5QD4{>x?bL47vbjq zkN)unI?_2iYE|>gE7ur`}En6_5JbSQZa9IHR!`(OA@8aDDAMDTlkNS!IB_HPA z{`QaTn;U+uPt>0Nnf|VS@Z#lu|G0O>eRb|X+1GI2@$#4YyZCf}{P>aHuU)>Hz4y)2 zpYKic=O4Vj4qyGme!+_S z>MQqm`&`(Y&A)!|;j}Chn_Q5yrzIx;Pf=Bo(*WVXh&izBbuzME!pV{Vfw{QI8@`+NEBy<*>%{r%^!zx`kLbG#4rw*R>8CvgAx?_Hh&{fdYE_kU>x z$bOFZ``z@*t=(7Jy<+SyZ_oGTboio^-%AG-a|d82A*jJ-ad z{&<%z_PE~drS;Bj8QGS1+}D5n>V5s$-BWw2;{H8tf0X%CS8v^47T@c?_=#P+^{w}Y zxu22v$F>;n_Jzt{dc=OMv*U}lyx-yx{>pXtwJzd*PUijhzvp&@JNE-Kx|*^5p*^!< z@5qn)ZNj(qq4$e@-alR*yXSwptIECgj`q>sKc3n>+spp8eEYxK=6ZAY)rHE}JAA+L z2>C5{!!LV|zj8f(^`+dOt^V2H{>$E<+zzt7-kbmFin9mXobRp+Bcxyc)UNh*q3jP7 z?VH|CSBzwLQhmMZ@2iWMFFlvP-huL!o8nhK++VaLzuv{XA3yrlukBvnmrE)1NA6W~ zU%C(NyI+c_hr9Z|o~Qe0>+@1aeP!+ZrB&c>S=fBNWBKcyFJH7-e%T}b8_Q$5%IZ z;CTODuu=H8hmGcqE$J`%=k0v@b$|43KF#`{3)t{=Uw6NM#h1fZ`=1ZN{fdYD^ilt# z|7c(P_dQIvsqd}pi~jj=_q9LTW4M4y*Ol~xAHMz^NcuanaQ4o4DNgRJI2rxN`ThTR z*)P8N4+iPo+=Tb{zimr-*zfXq^ZVibU+fcR7lq;Rz3~43+I7U<*neUt^626HPcQq~ zZaioI*YJM$VYM@2?sKN>-}xARwHw|KUhOJ!eU?ABtz_&yYoFJT5BoLuH?~Xes@4NM z@gcsy@AbbWynS6q_rJ>vss8vM|N8OX0cDpnVQ=C$R$P z`l7%8w}0_^SI_$p2zwPgvvs)7tlRGPxqarvN5jS+%a6Xbe{3IWyPA$a_`Ykv%LP%f z@^Dpa`@G#p=|0z16MAn&^8NTdE5SFscqs$-M;+6uxSN|FTC=&T$$$IRcRd4tM;G5; zZct_4w_og9aisV6^ZtL^@o+)K>d*Gyt=0L)YKfa&ljZfoS5JQZ*Y-#2M!s(q^Zj0s z!{!{je7TKrZ%~i6*X@$+aqM@l@%}yy1a4(ME|J8o4pSiv4(mOx=`K!PC=(2HkT4+OIct_WrG^_-D`GTv!I&dbo+3JuNeWSv4vC z=XI-}-|nA!_vbfH3WCGO&+l%YJ^b?Sqnj5WK0Jfzv3~vYyS<)%H)qZ1Yt_Pk^j$kb zJ=S+wo#kn<>hM1Qf!mKS?ZBmQ+)L%*_pQ0Uw%`|V>+OCzLU>|_{}oQz9pA&>{M|q7 zk++|WJh;j6?Kk!^->dxfQ^Vf8O(FYFlKeedn#v;RJq09_J62(&3F_4^t;iY0@z0v@#p{bAAFSi&%ggGA8gd0<37IcU->o9f6lM@ zh0px&f8#U#4S(!U_wT=#^>=Mt8Drm$-8c&NIhZ>C+5h#=|L%YO4?jWR@qq6C z(En60-k-+J^oOrhuE+9+KmPvbD*5|=cv1>}>2>_-gZ@pA@YmPHZ(5eW;pzP+>+(-O z0gu18hJU`uIWPQK2exnWI0gt+j{foYV;uba$NzmaUH{DwSaj5rzfivi-Jq@sByIe({<9 z)gATx@vr>4U)-So$mP`^)?eSoebxV^8=(s0->_%?>UW)kra?Q~{>T4%^hSUCV_Nh7 z?^t^N_#2OfUpm-+VITgbRrL$c=&yf?e(?OuUw(>z+J5o*{ZC(c^QS+)AFBF=U;1`` z^}*L{=NC8pub%n8dQkmm?7!cz!hZc3{e{p~yRKW8ab?e|+wvOoRt_ka29djHZ7p3Z-PYWG*qgkQQjf9+QN zrCZ}So^1bcQ~u&A{B6(fuWpGyA9KF^?|ZZRr~l{w{da%(H~;Dn|Mn06=0E=XfBgxW zsJVYixrfH*{n3-t2jyGs>+r&60CPRDba>YKC|tT^H+l+p8~*lJBEFZ{){c0#dtG8Eot)?V>}JM! zGHTk{W*p;Z5BmZxfNb`M19$)QvJQ6+4%hf*iHThMS$i_!%yqg$Y$%bK+-S_qCZ(L`@xdx-ou^uVi2`Mo1Z|Edylq@#54P<6uhl`s)k|WyOvtfv znD_S+MX~@7yJ7mn0Fa4~D3x&{oO@Cc&zPJ~`}-z>iMzSDs&yK>Ey2b&>0gS6~|93}zBzrpvd<>B0;=nZSMRjRV^O{KQm7Ud~=VQ2bvn z0Ve`P^0Q@gF>*8R&*iBFAdmW5`U`i{jF-(^oXsDD;eC6?3u5o%j25F_Tb7Z`GqN%J z5*SbPIa-pX_C#Tg^)4F1c!}5Gy{u!AIi2EyDY@yYGp1P({g~<}!#H?Pp{MS=RiM&X z@Rvg)t}sGKYG3eI>I;?)(Y043xSuuw2U%*&QsEmlK# z`&!>h7>d)$892_9D@NwB{RF~|J>fm@lIwR~+?vLskZ^YHe%Lp1{u%e*CWKuE^jLOo ztDQ_%;M!f!$m!hIBU`Y0oF~FW`WLevPR>gCB#f5?i5!P$#(Hd$=b4`I5*m+Je)QEJ z&eE^9dPZU{g)%)YN98sGM6Lv;von6`Z3(P?oMZjpT;spIm&h$$*z5Ql{c=9YFBcA3 z>o`qMhqUO2n-ykV;(}>eQJtc6sw9Z{Yd_|h{5<1u+PL*D{4B4lcAeY}UZDN4i$bei zxAD#4rkbFg;Xp6y)>0DEF_v|LXt+Z00jhdMDhV_8ejn5WX1~m`FZqc_9b$3SXEw&* zdfG=iY&c$@(mV3uB-c1k_-EE*@)&mpc+QEVZR0S#*>AF*>}&s-w~phEJL18qaFkB{ zg4kn-XTzwb&V7E9PFiTY*Gf6Ll;JSW!ige?SFMCZfpepCr=_eygcbI9y9lnkstf4@@$~NhfgHK9 zf>*Yu=gwzysrTSjO4oc&d4uC&F6>Wx1?bGF57*yKigo^x4$d4yaX&q*een(BT6drc zgp>AijK3iomV@CL+q|(#Rbsu{OErYJmQ)$Lf(x;t{hC8`X)sR<6^TKZR2lgOVt_-W=Bj)fsVre9XQ z%4Dp!rYcg4iDwc1wk)%V-=xZj_*kkxk*n3DRrd>4+cr*_jXj~HclXn19vsDTV*8rv zH_$n}Ts2UsDr~;G+53^ELbH^|CF7;`m#;VKC-+s=8246YhP;qRO?Af4l@;lF&H`QZ z@p@i1mXyI!p{9=Ldsnf+kV^P-VWL`{oQ99}+OBaK#qA@s$TsD)S8FQy+Zw;c zO+M22m}|=_KEID5%|u#^cXr)#>$2foWNa_h88Xjqip|5j+v>C2F%#`YX@6AvQ(cR5 zo!cV^w{iB~7B(La)~@5*##mG)p4NBNB22sYl$3%H$HHKzDj=4(`?*&E!RoM|+{_xc zt=a%~tZeX{5qq_ASr)A4nhrcK$fd*d8`o@dpW@~;H7wh;|FjthA;&Yqayjg``O%@S*M@!4Yq zH1{8m+q18+@rd_0RNrbBUK!(@)?aNp23P#}Vk`gF!(H|~_iR7hrCb(QQFG%7uj0e&*c2%ioX|Uc0RIqlrN*Y-F7% zz$Fu5<7(M2gi!|xf>QL}O1P@q04U7)AKu~P&uVuM-#|ZljWCG5FKuS2%GFlg;w!b< z(n6Q20|c`+4T$<;{JTpoOd^V{geo)?zpHQ%)zDehS3jEUY`@rvC$J=`e8Rh&*0n#+ zq#%k%Shl(v&NyuIpLFC_&3B0}X>zsh(SGlTbP*TfbJZm(`=(#s{WLn2mDQq1h!p63 z9!#{~>>sOCW7_yP^GmHGT_@RZ9B1+}c5_nh#clRqzEwNi3qSFFlKkW!T~$nM2?^Wd z)p$GRuVf`Vh#7~D@_G|Q&QJZglMFa;huU?H5d1ENb##f3pxLIG*pp*Ex;lA|^7c*+{NoLXS9Re$@-gpxk2JN~ z(tdSF4B7HVZ$~nNcGbt&zScY&YS@^J+FYHDeQ_*s`$s?O6jrawWbFTE-Nb^x`t9Yu z^2kPQr^h>7vB_v0dmn1p3#(nHbd2gD>4P$lWpj)wyX8*gLM`2|?tgz2k_HypYZE4t z;L+^%wLD6%QwCDH1*l~H+a(d7FrgEaHR^a^P`@M;ersWt3EEuxcG`POGyYQ-3-eYE z&w@M#@S?>&+5ew~&m8@FRlh%~w{sL|(r-=&cYje@pDZdb!g<5xw2I2Us8$i_?5X`H z<3(!X-JAV6-*wRU9jz>qy0+IlYoMO~SiqL9^J?u6F3o4++Y3_Y%W+pk^95hVd)yUq zVz#9@)rqisBwW6&%3~Cj5G5s0BESlw1e!947SFTf`0tC^(cKT-LsD4X*j6Hk?tVM3 zdv#<9sM*AbD=r6;!3Jr`~6$IE6xxlB>MB<<$w^yIt4JQGE*3{^r9g zp=cZ{Pass$L-%a7go8pJ??)$N? ztGn*C-4FoARUMhNmJo@j9~e*dkNrQ3`!KiViW-+glZYwNA>(Kh87)ef(fKGtU{GgJ z?S8@Dt!{rG;L-sd1@~_N0kw|rUaiO-qzr;2$6`D;$M8DL$i*stxG?;xzuC4IkkJcT z?H<4Q>mD2I@Dd*&;3b!^YSX^0b}*Y4G5%XHJj?zZdj;7wHT)Sv?OHg!9$Cg?EmA@*26LoV{xwVRyVY$8> z$>kbHA1A>6S1vgcXZ8E`mfE5}6p-?`Uy@~?7G8kosQml=*I#pkuJ)5}9BI7Q<>}tm zUV*|YHJ#WMC;f5n_QxTw75ZiZQDe7T>$E1dsTz~rKuMs_bx+`__X}Rb6%6>QtMdxv z$ZNYF+4~q2$`V|1uV2;JI+x^yISck${)S<1J&ovkE|Zj5#B znTk%R31G2{`;ZVf{!{lR7%1oN*cZCmC+Y1bkF_xxQ2a^q^HKGLTLj>{>oLbP0*dvF zc6guATYqZ1;N?@>H4ZT*dXGrm{Nr(&?0I8j&Nv|u<>lJoHs@X4Uq4m)Yw>_HF{tUG z46ojm^}TrRqwtao#dW=x3)?JFo1X7cB|T$>Es}2^hnA;2uHnt;au_Ls){oj0iG5u> zhNA|ma0Ts(1nPbY%qXl~r_%4`r%ocK@}8XEcvcl>>8?g2eP8d@O8ei1#tKkgYf`o< zRussUL~`G|Y$98ysW1I}5~SHdX#uv25hEoBcWzJy}oPb3Z}JWf=PHAN8P3Q5fP%(*(!oK z>A?L=19wj#x4#CdYlrt(rH{scO&UjA>hx6poTW42Y$kS{LJ0NCjWq)48*baXEn-ypc&gFjKjbJ0jm_tcqSti+r;k z-Ef5%T=Gq14;d!sfa+(fa!+lr&ew6IZ?I}XlGTw1Zvh{W1f`h{ZJ zl_bP79l42twYP(y`t7qq z@5U7^6Ta^Z4b(c`hJ_AE1T;fNM-gnHcIHTCCxx1*>AONxLbi;n&ox95_Y z+#f_u^CqN&dW_ERbZyyee z0I_2StS}Xc>YDd^^r27kpPPB^vJ~pC#YE?*Gvw6;^uziUr>R)+W_b38a zC!qM9{ugFh1f`Q#CENCz#i#g_C8_0g-6e`E$#DHFoM~&FCWqqw$+s@;Dxtv z*mbnRyJQ#SH-XP%qxOX+L`rFSo>kH*D2{|X_Ksi~k1Rx$2Q8;R{$;>vP#Ci+!SLhv z1;*2?OTIbZv%di&B>6E8KXZO5z})bCK_-?cUnIT7Atc$LL9v-3c6Be6Q;m%s#hfe@gy}LE?aoFDYRTT*vG+KA=;h_a$~xKV!!75;WGY`II-U zK1<4hT{zEX?lpGFC5)#(sRyN|)l8AC!h=S&9D_b2f@{|(Z+$Np2OtO`C|>e-b^k%v zacKH;ZQDEXJwWHq_GNrzrx1+X*x$iZOLpTo_}}yY`5M}#?kmF3lwe#_ilxBNoNoL z4JoFov+(@TQ4U8!v9`=QP%iw5=O>TL~lKv0Bts zLu3Ys7uJ>V{=Yx6<{_dx_&w)z`=yW6N|@}S3>mrASpTSNUU&P%`0q$-(B zpW{g(3njg-8S5AHVk3m*2|Qo6@sx2w1z4N7uSKlLa5kxqy@D9)O#ZOk6giJGv3?1|%{jk51 zWwa;>6_z{OU&W-~<*>xaY?-RH@#%U2t(({Y@#7Qszwa5ZP^x9GLj-ZO|G=>Itoe-g zb`M6ck1D?CCZJQj1Q)uhQs_&z7h|?muRlfy*^l;S`O81OTH%Z*@>p8`rO+ocCjs7~ zz}9-<$$Es;YurXfF0(L!!PaR_Bm4G23p5I{gResU{Zp*8UZNBTe29pXB8v=LPr29g zSAsM5$!AtVOK=UFtp*EFq5cg-bft|L{HUA_m#e!U&|}FjdhdRiX~XNO2<<)ckYW8} zmB}dYPdT9HwbiOcBiSA|$Un@YTuf9=7gU1Vm+QVf~3&?^SFU^1+Kg?UJ3qJpQ|1byecG+v; z8nYc?2##hzSF}n<>|K;W-&hyUTO1ER8cj~`(FNh92;sakIAjH&HQSOpllAQ<@YZuIY07-Q9P`i&Y_i z3A>ZLsnQu=&4|b7Ri{^&2};zq1tTn^>IxcpVKUvvM8Z=~3fR3vIB|>YAxZToaR7goI7XLca^HQP875UO5KuIlF}@H&BE3w2eNc2^>Fua%ELVB{qL%`1H15D z*`VrP7)V(!>W^v;gw!wRr3OIw@ENc9KiHtJ`) zD{A~RM+p#{A6H^^8)e|EynnQqfbJ*Jkeplk!>_lDN^j zeKFOLmNE9nhBB@e#k!j)jMH5cyQrdYNrzk`0rx^;SAbiq3Dn`h={=mr?AhuVvk?0_ zhtA)1zu_N4^6_xbn#(|1<(eB#j8&&XTU^3>Zm%NUZ;-DC(qDJV1AXepOoC3!8{Y=V`< zZ?WNbwc-BXEoD{AXc9sX#s=x9(K_}@uQqj%dOKj=UQE1xT;GauL7(H3Q1;*%+JpB* zbuTv~THD=fgPsIR{Y4Tk48Bzh<{H7zvQA=1#5M^9Kf{^QZ}+<3jvS${?yB()C^vb| zrh}XiL0^U;fF31wS_lPn0{w)USXCS2ZCaFBZX21vc^V|UIKlnm!_w-TZ#5(yx2$Q4 zK_LLE;GzEcH;9BDaa(FwcY~&TC4h)_jhB52t@R{TGh_3F0%nGevG~-wTf>bzaVp=k z>c)=4+|if}YWIok60J;MPU~0z#+DY;4%B(Zi_Jg?m2gwzuZ5;s?Q+NaXJ2bx0f0Ee z;6$n&pHH>E0o-w5>j3G$xnFt`VfO++={Y2-W9UE2=(5%ExPPwOzBbls&5XZ$Ve0m; zZPB0Tx~3d^&tuXIBgOva3Y+f*idh6Ua^JFBHH65*K89OlU>P2A-zZp;>$%R#y1%bm zDmiUW0Zjd_-XY5Jk++XeV<6uDb=~)i4z8c$AygKsPbJ~i0}PI0*XdlbSkU{AqE`T> z$pSU;{UE45VhLX1+dkt7qez4gfgxC;kOTb%{|9a98Bd)+VKX4ZLs{#D_WvWY^rv5Q zjlf4J=W8y|V^)v`qDI!oTAZNs2|&@Cm9rX}zFqqvm>*`8S%5v9V2kXn%6_8+)R|=P zE~R$>_GxzgzIwBZwEzply5L$T1Hy zVWf)(<~kZAET(=a)6bn%YV-%HCQHZB*YEg!09K>c5U|$k2f%iiT=%!DB<#1uxP{2~ zG^|87RfmE=yiO+~9%|7lnh|RMPJ8k9S86Mbq8fP}*Wqq}X2b~%S|>N;2w9}pG0^&b zPJ)EB`B!V& z&w5k+cjdF)mQCUB_O66%yq33^=x+)U{V8!FWR3WxuML3RR_jPF<~|7lwO}MY7dl=4 z-bYosPsr|ap0C<)pl56Ih)kqTgTCP?)t>No>e~{8toST@{&m6h1g8zW(guXC&oSZ4 zkarniUD}=vw7wd_X80QB0VBAfo zIT5NB^nU!^-mpAZpzf=lN>hPkVt39yo4&B$!N=&1#y-EzBTZM$IP2)wBJ{H44rjc& z_Wo-ufZJ*;a?p;a(t<*+Q|K6I7JLdx*k78%sW*Srv3GI7R}bOlV+NMR8Kq3O${C=J10)v%D6Db0?Itk;MJ`eSlq4yx_evhCx|x~C4( zwEmIJtj6oR8Y`}-!dmF+g$_n)Kqd9mpkjmN z*?DAS&!W2Y!<33` zH4DFr6351ytd{0|RASJd2N?yWaG5PCtzut4ru(yN+nq=k)bOY_xW5TZ@ICC_XMPWG zy*ay=F~!oX42<%y@9MRkU}>|7&BW;}3Fwn?0MTj<9Tmu1h#2d376+td=s0_5g>x8b%*16ut6k5O${3f4Fsgr=@v~lak?i($ zMM^w1e`q%s3}GD&^F~jcVMKp?MLpC@;occ)T?N1)3y>>---#R><>F{H$mn+U0$+hrv7AZayqpEajMy{3#1=D^Ds9-3h*%`AXNp75 zw2zoFk#m%C%|Oo-e{=kg3G*2N<^f7N&smFQT9~G8Y_w?akihyEKF3ON_AGetdzkRW zyrJ4P8KsvWLyX(h@d}cWX>M_)D3kieg*uPZG0F$j_&vhnx-rr0xMM8$R6Ji#;LLTb zlX}}ukKXvLAiQJZk{B_2#^SVP#WFDF?4M?X61}QRrHpZ8bhTS?Pr*g~R9g?_Q@uv13VRQv+?Mn>P+q3q zZ!@{+`|K?3*zJH`6xKI*zVt+Wr?2|Tj9E)Nv>G^^OjVv~=K|07Pqs0Yel_&{_*#L> zP90ZrZ^Y5yk{3PJ2>vUDc8p8LGw%mh|KVbq?2ptAt5|4kk zMq`NcTH^+?NZ0C3M0*r_Qtv?mvF9Kk6NrA+ra-*UPix=n&R%6}c&|AzI85+2@GgjJ zo5Bm-fLi4m`UJoLbhZ3%2u8oK-CDicn7ma0 zg9dQhOEOMPmeIBmW7W*P`8-@S9F3}bJ3|%jch44}Yp>wnQX| zeJS!W-$rx3JlJP_^QqJ(ap;LB39PELdBd1WMS>`^*C0}{W6wOslr`ysmtIH|&!m?1 z)kH-{pN_Oy5ES|tnQQ+?ZJK0R5dB@6JWQt_3mY7E&^DZtfavDw-}#u@{Ka{EUFn9_ z;$M)r-lkC-J$Oy6PXl>8z4+FQa|M*7u@9&hMvpTJLI}+|O7_H>Ov`Cdy%fzA8+#2V z(vHIZcZ-|MmkHOXn2$L6gnilTk)uHE%*@OgQ6$GBF+=Rdpn<|#vP+P(10H~P4G6=Y zYmouL*b7-xe{xT|!++j>C_#;P`xUon?$D@8R6@E*i7@?BFp*$*!1no%C?Mm2Ww2J_ zZ`Utmht{#CWoR>wJ=IVNAjoTtzx&?jE>$w@7V8_5wrzXo7%Gl1_@5#w_+J4As}W2! zRLThcKaW>Z&udp_+<@|Yi2|r;V-`=Wcf%72Ysy7xRJ~w94PoIr#&$EBT5G*vXAZ>p z;hG~F?3GAXhPy%BJo;PjwteBndVP`Fc$GX-~8c5eng2N&$ z`O!`UZRPhzu$ON|KmE6?`hM=71@UoJ=_?#6I(Uk=Vfl>-V(hp+B|pNdhlQ2MBFupu zvm)HB**Ju{=;Uit1PAi%U*$3Cts5*%((MN36UUF)Ackn_WB}+nb@%<6>VI{MEyL(x z%x>L#+fpr->4AY)-+yrJZSK=Wjd^D0jW@A6+{h>TMk>IJ{fY4w_S`y5HIM`FvoSyU z1{NbKqAd#IWuK^d@5PbKH9TjTvcv}k@J;1>PS>AhU>C79DGNPpKbvZ+b3gOWoGcpw z)!4_hc{D|L*!5NFm~@``jb~wK&b@OOT$GuawrOwS6g(3yB8=!OQ>AyFwVduV%Y4uI zx>YZQKsrQe)>&Cu5kMc25!7+60Mn>h5g?pVoL-@xG>Z7GP^Dlhi^^fYswRZ`X^G9^ zt}18*@bu;i1xMJ69&)mzE0y}gtf1ff zZ8|ZK*A4S91A)@5)7jq<6mJD4tW;ck)UxYv&|6|2V!o#2IEoT)R;*P)iktQv&2caY z_i<7xbkxp#1|g>C=|RQmPW;w_8qJj5ns@kRMcUk`<#0v0cB~U?S7oCbt_1|{+9+fL z74JD&BzuOI{TsIRC4d`d;vAfHo74OC)|U|e*yiXBOp2p=69Wy32X@_Zq!4=w9i~J) zagEHFY7~OYgjv8d>N;9kejZjjCyu4)|8~*iKZjYAb-_u}lqwQ|W0v)bFJn71xx2_F z{hp1caxM5B#(Krv3a6!d?p+g_Ng?V&RTdYN0H(-?>L@k?dpbN7WRJoP`hQ1?*fjeC z230yZ<@?ejvMB2r6upyTWUckTX8^3K!c7}n$L9i`sza~4n4^sxB5EEM5G5(-%j+Br z>}$HOdU@ap{M5a~Vp{|oN50`yXsspymi%hUG7$JH$dd|XEnyt>)|2ioCX&S&Go~d` zrZ=w+u-DuMSG2avyia@yRWKedyC#%JhwPb(2DxP!eDk@PkN zev}=LDFw-LnF_h*F^BcXVIa29@OsUpb19QMC zPnie(wXdajRuyi}VC6vwpxbSX zZClEzv4w=&yITUcs$|WeTl`?AdvS~3C33fcQ=JtpWHZY;Y@PZN1Pa8Vy(p`H7o$2) zobS^;hq^KARgbhf%Ogr8OW_xFtmWTo)D-;!ZF$vWYuNH&#-|jh+Y$cJ5-YHYvsv3) zTJ&Nwm~NA~+s*58POnmMC3Z6^{9SV?4{Fnk8NQAysy}&P#JIP{89*-H;&3uy*DI%x z+xVRKqTPTB8++XH)^#60x!;af<{lf#7r)Qw!)o-ab$>9i@A6MF+t7=wQKpZ3EGHOI z?7dIH(@to81BOveN<@FQJ+%l4N@+-;Gn67k`DLrZ+Qaoh^5ygxXLbuUBgQA?W?(_4 zeFTz}m<4vWhQG!;cN!io(E)-mj=f}M3r;N=%}bHGH@(* zFD&``dD&`mwFJ{yur7B~{n8YK=S^U_j323D!$D+;yS;-oKv!I*)*Ak%&~BkM47aez zew@tNF; zIq8}NUrdAU;!<0|K8M@kncJ>k5Y)14Cku}@lEsa?TBu}g4)vL@AYko9Fc|dn{y<%t zb8Z86<(b@HFC}#u)$xbeV<)*6O_n>rl3K_p0h2`Q5;BT?$V*!(V0Y)CNV)<9o2_Ar8khrCVbNdfMVkbqClbXV?z7j^j(H z793^yozs=iSrPLh{m(i%mm1KUS$5q~yTZe8+Elw>e-w1olLw?)fLb6utu_nGqVXbc z8eOR%mKxr%SS6J~YTHWE6@66bYK)C$xowb#{{Bq&;~2h~GxZPCFWsW2=TKp7+&^^b zC*0*c9W`vvoFdUwn8WVe3b&$@uC(M!P_?oT)f!Xq0T~Kg81C9w0_vn%7Dto}0R64H z_ZHTy4aAFb{7fZAj|*;S%TR&bRQ`%k!;7-Oj*-+g-13k5XGkoUXA*+nbj0ben%@$n zyg3-A~PmMz{t zjNQ5KKkS4067Am-`L$#&#;7_^GL%A@^h|_E{^rw)l}!u=okw(B1}E8kn$Fp<*qu7M zk%L;*2@uDrvQ(suRb}7P198=g^T*w%H3VGijONA3S&jGbK)>)4uJQovTLZr#ZZXpw z0c~4x7J&X7imek4!w9c$U^Jo8-JTi<@Wwt&o|jmwrgyj{5M1@{bkyTRKBXTwI!4UB z!y7Bjdy845W;BS=c*^8Aaf0JcvQF^>=e(V~r0mxEzuFH}E6MbR%|#7{ z!1;ZBf=s6OHP#!~KE-pLC2M?3_a!5Rk}>ah1~e`SuPha1H6V87g(@q->6U{xc_8 zs=<38TuaOh_?6X&*R2+OXDjwQnG%5W;eEBdvd3$TQ3=%xM78#Suxy^H>FXCNBSR;C zYo*_Xiy%krVcgCGThRpxxv!a=fR@xSg3cL!L01yF>m;1hnk%x^-^)<%GTUu21u@}k zrDhE&k|YtfzgB~9YSe~Z`uO@;M*Jaghf3espN-B7RC%7IV`HCoWgeX|-K$tJB4)03 z6pC)HjwifkYCb-)@4A_>gw{U3t{PkL1|R?Ec*3Q}7Kzwp8zUe*vL}#qFIJcB8PbW1 z0S_SYDsUhcqS(ksbnT@?i6_g!mv`UlXND_JCK}7o-FO+z4m-+RqkgtUPoTnR)j;7p zp`|shXwT%UQ6Z3NKQY)H8<|lvXR7_4-XBca6oLBkNk_%Q+v4dsY&?b(pF_wUrTbIi zIZyrz^rTf6UIwOG;j1jps|4r^`iZea2&Xk#mOF#z-tXe@(3Uu!ojG>pm6vvL3j+i zuR(Nsf5s#Wig3k{Yy1gXGd!0x`(+BlIw1tT{VCM~a=at8@?FNy;!U-LP;uX-KT{fB zX`3cZJCAX}P$l#ni}Vf-Cb}jQk*(n5+mAAcFSVV~dTePE$1{8-sLg3KR;0pqP4zDI zaU8goqLZ*YUlQ))zF2Co)mJGGIck)er!bAv*k+ii=&^SYTBcV%-}10 ztw*(WLU8)8qOaTwRo8*A`^Q2rl!2~Lhog!g4y+k8-tm3y%a8kC9mBBEBCCD_fGE=jg84)nIr~IBkfzt=a!cj-2?>ad^yaP zv7rjX)K+)>?Jp}aj`ckE`F%N0;1D{nIa-ddtlhMeTGFO0a(p?0ZFU#OXW1C zo!*Zd$DkIXSABzJSf1LZ+g=5RCMx*xbB}s#A!E?`yVHgc?C&yj9h< z<)?!ukuVHGn-VAQ$^3H5qji)4N$jiJnA4T31b5aWF(N5QtC(`mKXGW-rw;YxqJKxX zQYBFW(s)okv!`wFD)#%gBdlgu35q(aL(!|ldGJV=k~+3~1p1(9Vu@Gi7-X+s zx_4@^B2t_Qh(c=10qnrU$KU~x;%&gicn_#sh)e)8K+M0>UKzXG2_C=s8s6Jo96wT$ z)wXD^pU_^n(RwWL)-vAPxWC@x?`j0iz0AZl1_PzL$I!&6Y#oOWfW&gyEu~{7r7f8( zdIhx}%O42G+A}D#1eJ;WASK`NCgmxWxD(zr(LZx7Ul}Hr^8)m-A4Edc&eGw@)w5^y zH=~?9Ii$rMxcQ?sG9!_L#`z!hDl%jMHn#1kiZyW?KtPTZPbrbH@32zoZ%f{GjWgSL za;ctjNf)djVlJ~z&d3piIq5J3zH?btjIwfAmzpGpV`p)08G?^JMQ0Z+E(pK=8A>{5 zv-=pA${EsxKI2mooUv@UlTT81P3%ZEs>%CxfQL9N*&5)Pl0d25wkxgtRVgkl3JR>Q zWKe8{7q|qqP*`g<(UmTx)nWHM8Cu}VEmiGt7ASKvCjnohNWIZn)Mfna&q0OE$;d1h z$ca&P&Yu6upel`T)w-vq2-zRvHF|kayV4nQS%dC2=+flO6)2dlC6qXBA?sbA*;Lki z;Hd^>ICL#c^Ky+=%FLd%>Q=?no<5(tmq~B+m@3Qv5;=Aq;GD@e*&zX(lp;l=HRd64*=oJa$jHB0qklYcj)Xw5>NTrPMx0H^ayf6&# zwI3hTX9bwczbg#*LTBJ-tqB9Jhy4rz=B?=#(vfvwzJ)mDAh|Ztl{yQ4c{&1^LfUj zlD_!Fqg=>E6#$BKQx_Benu0U@ZJFU*!wYyZppB)ecIbGDZg_^Y4#-C=E%z7b-xWc) zVx~$FDRhlxI?2`mA}BK*rsf9|oWK+~}}X`7IG_w(IR-Ji&*lAn(<-Ux8en- zUJV_vmD+O?d>EC^I^({!$hhDT{W_l7e`bPb^5eEPE_H-xr$1BJ$_H|NnNQsjYLCf| zQHt99i4?!O2cK+9(v8t);b}%6h755fEnT!BY6zG&gP&3E@emLe7xhim>_=j2*cx0F zF^3&#-e+I6vbT~O+_J?8vkFf#Q^SwNNG`$%a%sG2ks-&0mB2bk{Z{v8v?t8x5+PR` zs--f~4|5EvjBe1#t3%mN@JSX6?Csn$8HH@ABk|FLakCh>&OvChP1NQ=u5Nq+PmQ2~zl z*H->PL;9!;;C1p!`YwQ9teB_0D2RuXUdtg+qh>O_W?*s;AAk`Z2JC|$F6D$K7B*w1 zh!e_Vog=tWbAmf(%(N07gAv-cRca~Y1LevS1D0u1wu~GnhgltPS4>MxZlAK;J4e)S z#-5`>GYm;)#$h6KJfp~n>;4%L0u}O80R{07?OTdOZq1R4Qr60GVnC8& zbJyvQ#BmtMKto-Sm1ZGYbGP;!Kg!TCJhI2j8wH%~-iVPx>44G6GO}{e*H%a#DW<*f z&zk)pDB~u;G|M&uxlk<}C-w31m_jBV*lAOY2&=^URcU%>R*9Rf7#$?WO&gV!_ zb?|}nLxXNHnmlLl`C~2*si<&}knWE&m%&|i*Ar{4D^3wlnBn8`8Db8jsBI21?~je?Mwiit)h^#kGa;&{tW}xtpD~i%%e^037vww~ouZKD3WEUc`-~O||7VYk zZYlHI8*_mIb&bo^IB!f|mpOc`vJmbS2VSm7<5JU<@M)HITpGgRoy_rkm2*GV2TAW{3M{00r-#y#PZ&)?}p z+ab9Z3ox)w-|bL{sypGFWMTcrLs7bi9gVDS<8#xfnJ=;fh;-Dz)spGi-DVRS$LS}S zfCnJS(dt{|Vm8Ix861~lQh00nj7j^Qj;YG5I$xsYM@ONG@$)-Fy1Vr*}jmaO%73dG$(~A7G_JC%J9}oDHit2#0kU@ zECc{&ZPET1ASC!9$9+wS84ceW^Va9C>NX>b789Ut+RQQQBaS5s+G`|ndq9{Zq^%X< z2%|SbS(4y*l$kycTtgSb`~97Z-tk~ur%3{qEjE1yU9-{R`bZha+^yET3Ma7(=ekv& zB}j9NSrdq64C(v+hkGZ)Z5@+bBUkElb{CqXGJDk<&Y_e!!c~&K$I?0hO?DvR&#|HLg_h07V1nf{@W$SXHnb-tIC};Fhw* zy>HuRcIFzqi&;k7+w`*!Szo(p_l<&W6M-xmGr`~ESb-x1mTYI+>k;6M9xC$do+kW( z1%y^ffgP;YNZ3}IM`vp>(r_CPn`DO6dXzDf04{fDQO;2mRXR5)aO@0}lqKCDM$cA0 zCg$3Hr-eQPxQ>KZ`jr(t8lP5o2>a*CBh#gxX|Al6p?|<7mpyhaleg#WJwh8-!J ze!^Y|Fe|mW#jS=HSgT*}6C%hCiT+i#g`7G1g-3VPulXM++a3|A&wM7kcWfAJ8_+bF zOR19+adam{^{Rwu8{j#U?fR5FTM?Ih(0!D4gNmCWX@HG$rggh;GCd|Vo(b-EY!IWz zEN#OT!_A9x-P+1~B-w_9J6SEI8Fojd*4ldOsI=)~&QQMOW(|>B%U5ogQpwE-3#tanLhpgLue$ zJ!d92fTl43IwKk#VRlbO_&ldyJ8|`{3rokiZPPfKeN;-(WJy(ktC}9-7&dyL21z{Z z*Veku&>Qm2%{+adwE)C}iS!x6)wyZcs4$%PMOi0lH@t#{v>JzzT_xT!D$^ps%avsZ z1|evrP+KShABEUVp+6#7%5--25i+BMr?v!l1BK0WN@?8JFc?--zn14PA0B{RD2YYb4k?d;ie48{JH)LopR z5HPX5dQG#;w%ttg90~0AB-_~jQ0F5({}8y@4R$gf*!(EZ;G6eP!n$udmPan3^Q>aP zc(g@Ao>{=mxkW5_qD-$t9+5t=Icnv-zM4Glylo*I)|5gmi+EbtVP=36y)}i$oUE(Onj~6LJ*@{e=7wQBgK=0%bW;fCX>4@%H+~*riD!)MEbDvJ`*JOiK>CS&!Ed)f*^@?|Y=f1KgXY?)zs z4ZC}}6Xz*|?z22@7{Nf`C_0tc6`ie#*nkQptv`p>GzJD0-*TiZjY})9$}&J);&+I` z*2>(J<|Y|u`oTK$&p7(6N%d{(INvyV$u~!Fn^Q0rw{~OT#?SrAj?Inl_=`<_Jl9PBB@eRqm5Kp`!2^cV^!ll$bjuGfGW0U zM`Gk>0GVWR7n1mcsw+`LW%w-OV`o&x5f;0sjHi6!bEl;^O2m4FiAxNO7Fg|D3tKh9 zxJ9L2mG!n{)_<(AN2rey?|9P9n6XZ0F;cU^ciifZqVHSk0Q0r4$n#uBOI^mRlvYAP zq@5}X@qg}!m5*8v+AhuzCNIFP8pCY4@Qq6L!g3kzFc@A91WINZc^(OY@oRBV*LXz7 zNwJzEH6q%BLujwrGl0=40b*#iUC~()FM0Rf!C4coh(~fnKT%<03aXX>zIC4wYzv}T zR0qzDR>nw4cvcLG_88YwaJ(xwUh)0*0w!WC(_ZsABqTtBA;$Q!mIFe4qCWR(8B7it z_Ty{TJS4Grad-Db>)V1`piqWb*Nly}S50D%>8HC4lVtv)>&$grjflAK_>h%jLSmWZ zx4r~Q$Ur37+XQ@QG_CPOLFLJZJC!>773{sr7Qc0hCk{I-rDAFq7B9|o@eM?$=l{pvI>=vr$U3P!5RmUTN zyG~&@?~@}${Wp-yT;hnEh1;MU8t+(?BqH@Xr*9Hl6MV&c+97KzEqhaiN(=0NoZff| zkXG7xdSR4g)1&HFj$HWC7lM)&@}N56JHrd%nI=H}gc8PwIxsf_UQ*EUWtonwBAJO8 zc^}v5tvhxV4BBO-6yFIbJ;!Nq)w%4hV!&|E+A4NqOOhV$>G-)B8u!Dr#d@wzeg)?f zjdgM#bG(o}NwaXw4@a=w^(ZhS6~MT@ksu$A-q>q(d=u4~;IJRPiVw>vl(U ztSuLvn91Al>m4O$(&4cZLrAGY$LC+}%t6Ye1YJ>NLQHd}y$!|&5zw_wUo-xq=Fsd& zOO1w$pCm6)vDNM}B<}we3q7t(Rp?O@?IDJgE8I4s*+E5fmx-v8R&XZDt+s`lOWN>Pzp0ChXxD)DRw{dr!K2 zIcLWb%aq01nUHo1`$=vOu}2;Lke&#(u>Gp8=a2TF&0TGF6TInLKrUCv*vs)e(%7L- z*KinxOLjMpfjW+W;D}q2TbBwK$4E2p+xO5UC)~OdBT8B5FYbpR^v+GDltaR;(1v7M zYzX5CJg?wY^d|Eo6pjJ6JsI%b5pb6~O@HV>9d;A?=&=H^Q*i~2K8mHIPg>%c;2;}M zOr4n2UOonUd29+h6`&1ssq$c<>8I*jfoH`^W)Rib%@T>#;fH64Z_2m0OfAl+IuLk5 zLoIqEZhaep-_@s`Et?RVZY%ExU59aonqygB42yfCvh%z95y)=5i|7UNzT=U z8e8rUYpB6+f4ZU~w6^Fa)pKjxiX9yPaTJa7CFP!XrV6HJ&t19%sOfNcJEryWCSFqN zpzf&HMq#e9GCh;*RcTNg`~AiQW#Tm>MPN<(@-hYIRO<*2SN^|>srLKHh@v?hhlzy7 z0L41xA{NT!Pcz0Kf-^K~6svK!w^9VZ=slHMBD>EvlSQze>DcWB3C7dr= zBxaI@_j24s)p@jV+ss>st>t(WHE{0u`tzhXy80o&r;rPL8>X7+3r-{D;>BR zn>4ddor`qU3GLRa*UD3o+$v(!?9jgg4%ClIbD@B-&p4Pce6BV}N zAlUQdljA;`JMqsgHH3MJ)EP{Gatn>+cO1@=9WjkG^7gZA$4cBXB+;R1=1Q%PvuKKr zkXOPr;T)`zrUUxCV*zSq3rbsQRs1|9lq=^t(HH8VW!0xk} zDrTADyYUp9yrg|Z?XXjmi#|$owFQC`_DS^|Y|gJ(w@~TMbEw6TJmQqn>s?+VP!TEq zeDfeTs(k+NB_rCig5y?p)fB>a#uoBe_uBqp7qT2%SyGp?*A@Tb`1fAhgpwX5g|!W| zDmEQNkFLRxd+rIvx`nLKF4rw(k$zJwLqWPNjz!@?Br>}uTR0v`=(x*LDytvf2*5oC zG>=K=q`4zOS!yV(gHNF!s?_XR=~nhYIOk`C%alPS{>(frt3rXg8D-59093FTN-q^P zy@RRP6G5iV8DpDFClbnahG#kIcH-@Ppd!iR2#xUfN!7W+`N*)jmCYJH%jW%)+|`sI znO^aBINna=jvr$rD!yb<67J-g$8Gjp)=9*6$}Az$lqQtGOSkeQt**cqen26=KqT;BM`(zD;Y-a@zbwkQVk)qc zP!*fRm7&NQcM6~dWUw;wkpGPZdA`(zw}}TBD`TDYZDNAL9bmvzy%o5XLZun96*kmY z#L273WVjNKDap{Jl)X#ltAfP!misw{r z_fz%?J4f?6jk9^a>D})tACi8yfMg~YCuXL6Z#`UXW;oQ5)$;7j_Aqw$UFXCZ_?#U9+o0NTs zd+dp~)w4|Mw-!BKQEi(JL@oLx?kX%_ z>7h7-+*i)7!vaUrlYZ*;Xq5q1<~Y+{wK2BBNQ6QaEWAgeAe+9$398(}Oqk7vxO^4S z6zzD6Da%^UmyYKri9k`Sl+&@v6cd>PaleiRObw`CNGSDfA0U3qeymu%$AOoj-9F>O zm&_A%GS^-6!&ELCfAI5-Au=Zjw2FmL60a@h>4wT8Q_z(bn0Sva)@H&~bNSU3Qcap5abL5=jxR&i}|t?BC_mt4^ut z5rT}=l)B`pH?P15;bjyaLUVbfqq!%yehsxQf}C~c<^b`my=eEVQ-kl|n!CETrCc}A z=7#?4Z0C#NB+j9-LdM!=y|%?#uuLfUtL$qOCB}=TQC@hAsQs^Z=S6-bZ%{tbRS*ty z`8e|Lg(BMUR3UU}PtmHkWV8!9VAb&afYr7rQ5@^Y&yxj+&n^+3z;GO!gpMvWft590 z!_j^M{!;I0S_?Ke4VxH)-f~UGZzgR;cj{NODx3(n zViJL){X%0YhTD7(*8!KsQ0$#GT^`ic6_Cg}YI?WOP#D90Q4lC%kBXDJqFkNxkQc{zxvpvR%gE<0B+G4-GjkUu& zI~LH17APk0nt;e&bc`}KAH`*raNdUD`n;*u&=~UE8kLUx#Xh#`W zp%l_U$z!z#OrPMumCy)Th)CkR*PyoVC?Zv7yFxt|@Xf3o+r{&%118kQmA= zu1ZI+Fw01;AJz40lKM`$_%;_ynf)obyMx*dKW0=&%V{5}h=-2_OGM?H(6(VaLbc1k z%mH-V#c5~PnB+{L;Q$-Jdr?oABu-E2W@@wgS!Fud;a4~&I+|MdiY zGiP{zlGJDHJsGY(z1!P8TXrvZjz1)ANKn~U2785NUn9OSIJM6~RVXyAMt$yl4V8Gw zqx9XZD0KnZW@SHM3KY#8M%z->1*%7-Uk1SI8{>q+Er#G3zfZSzCTXf)DwX$sM6?(s zMKu0n?lUV$0#4idqj@lM?mB=RrMfthgpbIbq~gewT5IJjMbyuQRUFX%g5k(Pvfoyvduiz6*&NlrS#9yO5HL4+f^FI*Qf0Oz z@Q#Mujxz9W`HV}2PPCC`iJWmye!!CG52P6Dvi$kvsIXtY(>K?ZQ{x$6Woa zS>!{<1XVyAcXt3uwjkRgj;cqD$nT@J8yl&F{1o{&PToA1!FWJ8NFFgkTnqdVEvKBIZZ5xCnBcm7(jVyg|*bd`lopR!^)}`if}41 zwx-PGMR200VofVw_t(A#L=IzkuG0N1VczWQaBFUkVvK6IC)2L@&5p&Jo&_v$m48Cq z{rTpfO9>y=c)%xQds*-DJBlj`T7YZeoY~eGQ`?gC-|`7?by|&2!sccYsnbL}HUlskUrfu-7tJm(y4+Mh~1i+GKf3b_Si{Y1ci&9~%kL#v3LO zNdUW;32FY$rwk;FT18|Czc_-i<=y=r?1$s9JWp%SvDh`6jgweIX#{1g z*K{#}3%7v%Y&L6zt62-XW9;?gHIIKqj-hSmL)S-eQb zW8glMbaf^u;%>2CF2IOaI|(Wzz0Sq$8rUGAW!=(&caqt|d9lhvl!-g+!?j?)4AEJX z`hO_MjFA^9mzd*em8EEZVp@vHJyp5!$CjCHHqokxfFIq0>-qsxJB_6O@wv~*_XOw0 z=a^%|Bqt-y8Q;X`qpSlMKt(Hf-IFJ_$6l+&OY<&J>JGj9h5+&>n1Ux{5)6|P@48cy zt>sQ$w@HG%*+Hh>yvvRi3o#ih_jG$;d$tYN+E6dXB4d9qy344s$n>d$-4_7t85X0m z7VRA+(=@RryAZE+O~anhK6KMUh-Zy+1i9A2Z56a@y+!-O&Z@H3kFZ88pI+0n^QZ0; zS}TB!+U73qGS66)DSj9g;RwuPs|I#W#!1BUOZs+1zasR?jfj4Ci%VK{N(zE>1QLzL zkTV21g-^0s3bQrbGfG{CJ7^4g5)b{f5R9kidvxL%f$TG>h1q9h{ahr<0@{Fy(uz1{ zedY*W`9=hDmUfnZZ@j?nC?bnnfgQv91O>A^G=U{!^yjGDN^fV<7$B$;CezRnH?wN1 zheB=4Pk`@5#d>Pi^gDz!^-PooM_|z;g}%ZiEZ@HMp(N!h)z^?PET3XuCiNNV{mvou zw$$t{1YI2wsg%e6I;eOo<7%dzjM^+eM@dZ{xDh%vaM~&WE~J@Qi>N)SpkTCM8o0N; zr1GcVhd~RGy5^)7V$!6!m}&P6MCDXsp`xPOo{yu%tZHtzs8bZl1Hk3j|ypgbru< zjB61SUyamSOcER+cFhc$ri;p!!1^U5;qqxS>)YXALQ#u#$q?PXC*sm-`+8+T+k% z3L4py&!?lpL|Nwc!M9u25#cdq!Y%7HDW6BBdnSe)k48~dbJuIKbBS{Qf8 zZeq$doobYh=z%KGr<6nocSVGNY3|h$b5f;6qbi)G-D>*A};xb%t6m z@}>IWAvkU;_kDCt3lu}MSTc9g`j&i|qL>kIV3>>FoFOSEh{xMmi1SNMPi?qKr~_zS zQ+Cl<`3{PBjUQ3>m)Izm!GNvVtCEL1@m*9$HHyZRC(^LDh>*2^YfL! zwpwNMWUf3+A-7sj3X3&qC{=d%C+QN|;iJ@*^{R7olN0kkD} zc`Hm-Ji!?NjV!$o=uR$<6s$Fvvf=OoZ$>vYRu$R0Qo~S0J~I!U^PXtnYx+&1k2CrdWHY^hXdaf# zF3sJFy|IOqky3iC#E2Lgvc1H%s0%qU)Zh-$mIeJEamfgW{H!x;8yR`IKkh=2ZC>nY zb~e+*f{k_zv3C~wWe>zPf{q=eTNn-Plc6KlkuIeyRs*9!>b7Dp9Ak9*L|4C74RKx> z#^#nQ;x%&ZNx{IWsFkTPxpufdNvJF`4IIX#Uhh>b!#$&F=D4c88VF#C>?paEE#=8_ zi$+i@eGshHzN%8+X}Sw;FoGO{}*f6Oo}dxnz+6)$z7vQqq7>6?#g6 zd9vlveb#^+!gKMIjg4hFnq0{PSmRVtG{7+tHjN0aq`Fkrdw1-l&l!0@P=cR)1xgr` zLX%T$8N!(9NFf_5)ZS+;lVnoFBeA|?l3E&!*c>V6pVA$&hkP>F!)|(}VK8&sM*!Pd z9tB6tL_rY#XQhp0N8; zH%jl79K$fbxNKLl-A3_$SBT`vSKf0*B8=4vActLsuk@t4h@U@l;yzQV;+RZ^Q2>&u1Jh|(2YyRMEd*do zd~2K|ifEYdsXNrTHUpuvqKwp!=|tti=!HROnny{p9Nfqx5Qt)tZ%f?g3`6_&DzcA3 zBgY+7k(r*eMSd6B)lpOL-N6$oV`3kRCbA|)ki&R|p{-nOO(r5@tym2V|K1gBp-j{~ zT6Dt`{nXl~K@7@7|Nm7RbHGc)q}AD5Jd?-0gnXPt@pGyE`xx<^uD|Op^@jU`2Uyaq zj`{IIOcExsA9ZBLrH{oSCeZ9zTeL6EUv#i@S~8SV$uT87_F>?R!Sb);HWwc=UmE^V zn{UZbQmiJ8wo=UFeBmtYpS&1fx;*I%aw1LNk2$$fc8C8e3}CB4n|%o%?_4*d;{ zWywKseQ;uMj4P}oHG!AeA7uFu0iJhkOPP>u8a4Ob&h>Yh43RT2n;>c^Te(;TB@Vu_ z?PX@yUj#RH(bU{|W_Doaw1-Ji`Av&Gj;R_dTfJQ1`kEjTe21Dm1!rKE`^@R1+mQ3i z76ap1Vlr_H<1m>_M#49)0=}ZU%HE_1ta@kq1PsT!%zo}j=^*v@U9PnbETo%cSr$WI zeh*ebGjlKL^&X{?;Hw+2r8-UDjpGsb-LZZ^l1pUOJUgeGCKJ~?jD>ZW$I_zn5?@)7 z)JUH(Fk5zDK!2bA;$QsJzxgNs=3oDxfB3h5{#XB@|Bt`@cmL$S|C@g{Ft;7Vy)$o$ zr2X9f_P-{oeJ zr~~Gik8(}+;3l*x!+>l4Ywju9rWW7SbD#e_3lNflY4U9KYo6Z?_X5KhSlclkqpo@BJSsB#A!(ijh6134|q?K&l;Ag=#%fkAI4ZQq_KJClY!HtXqy)Hg7SU7NBlB2 zJy2hM^Itdexrm5aA-_L<9Qy>q?(n{?<{w4%21aM)gm{^ee ze26J{L!1u>Wc>K*XMli_m>u{1J|9(loDj!Q_4fOH;7n6(Hj0q%k)RY}D0*qyn)yk# zq$ox?quhVjBD#ni9T+Y8{rva%D~Zpc58vOfxeB&ux%c|3YVU6eas6uX!`SB+4KUj9 zZ%@Ix{kWU|o@UJT6Hm*&Oy?6Y`n+K~_(Rt=AHf|Q_K1(jHdn6CP8xXP4x1FtKZ>^k zcad0XJ)0XNjwQqF(!v_wL3WXoeypbcv7W{PB^nEnF5t+;?py%o%3n zbm+C#{O5MW6FCDV!n;RZLB8o3*Yx#$ef;HtEN5Ic+xxaLHn$M%TKs_L`!HTRg81qY z-(SzRITD8$6t3ScCGs%s(c2&M=bLSJ#^1#lsP|CsulE2&(X^xr&My_etJFEy`9Ahg z1Iu&wf7wbDi|fY-v4JFr8bJ5Kq)nZzbg4 z@0q(1OuX9z9%0sFfU<4xmkQ}VdX=a~-iNS7V*@71lxekUR?ERf?UbDH`y~o+;ecz!o zFR2~c@m{q)If9_T3`6U8#Ty34s)>5+JyA|G%@z`~@z?vyjnWzPN?~i)cNJ1v35FxJ z8fDmBPz`4WP{aA7_6G$bVi09ZYJLA|EHzPmSLUSOrxBBrTZP&7cN?s^!XG4zyD#GJ znB)w+MdD@p@g9>L>@s4{8UG#~ln1fZB$m7PZzsU=(Rkyo^HnEs>!DVW@AY*S2ne`) z5bO7?!fcE~olmTL-s96?5JQtjq@OSE6TbBd4IIv?^?YxeTkk`_A3yweclFI@5by0?nq`6!u@8>Vb%ZG#6=kJzcZzrOZZ ztGH%LzJ5&&Zk3cTO2+ki-}V`~M5H^|_1~je6H5z=FmC)d+L0hfvXNx-}AZvjh7NCYEx&3^n??;FXY zSd90U+l+eG#3-Mq+wpzAH%rQvYXyw@epQCXs0T(b%U6$~Xd6FcvmtkUrPLNNG2rH{ z>V~4r1O=D!Y+rM1oRLub0Xx0tlAGHkAv2M-euZaTn4LYn@AwYM;cl&n^EBrJrcc2y z;<&tiEp|gO0HQ%Um-p<9N|nypi2wd7X0;yY9pP>tM1-PyX*@T^@;@$lw2m-BcP6*r zak8rQ0}{8dY239{>xem1ufGrdlwggOA$0HCdP#s;2OoHOD;?(s1{l=>n)g>pu8eyi zpEQ8mx4@3k{xQV<4jFktTY79y{oAOk&3-w2V_y8eQote}m#w#a4csMm@dyj4wI2oM z#kMo~NTzddnQCqqmUgV=zwQ7XleaJJ_x_%z#x1cn$9uZoRmb>dT{2H(LHB+Z9wAy= zeb08Z^+$yXG-DDfb@=@~Yepu~G9GVPw6ibEEQ+Jk`Q56LIPL*xUhhYD6)@43S70A~ zprW`43xlTrD??6pGGu2~$9HA|%1I;T&2lf_)rK7*xIE+2Z+DTmsy7MF ze5?0!Qy;oe6GQ%$ZGB(FQH8S)^3#tVTcio+m-wE)Kj$`o`H;5M*YNS*VoK4Hmhe@1 z#!yF$-g~vTyw!shE(IO8rF{^C=0~G;Atqnn<6`JC*z};n)}1`7LU$qZ#OLKaPCUDV`%J!27q| zF>^)HLR$78+g)R~CoGLw(FYPSY#^96#NZeH9vkQ#7Af`c*QnI%{^83~?^+b^%eDS!d$zF?fN68X9?6^x!$O40_2R$!=?s>`{dr$*Zt>y! zhjH~@xA+*x6Yr<5xT=`00bf6}CODZE%6vlC<|g^|eJaK@Yrxg7trmTYkBA-deoefC zD=V8p?0Zcf$+n{Xci(&fXtJVVbA0zE7=H->K7+=q_P?(gmZ4Jr7Q(nsFelBMs3?=V z<(w0Mr`CxVC)-b((KFAJ* zVa*(r_1g7<#YoX%CbEuuj}DuWSu!7ueu=!_lW%9pzL8&kE7FHR3#UMMf4gHW!z?Dh z$g=N9!;m`cQghtpU0R!PHidv`&<8Fj$85L~k-5HqG*(=)j|Fy4`~m!Z*ptd*UG@779=1`GVACip%&KuH*)oTBegQa-XoPc?LY#|gd@KL(ns@0p8N zgat{%+}8JRQpg}eA)Ni#Eig<_MT=o)K;;zyx{HqHM~a^V1VJ}{=kGY5^oN4NhE#eT;lND5qht06>UV7%>A|G=lcCo5iQ(N z`=$7^2_Z7p886@E&Z3J5n0o4XzmFm15>Fndvs(6>EX8gT^%ma$j)fwEaa<;_>U)g- z?F}RqXMBAi*%>FS%!v2v{7y8}nTUX&?L(^&mW^*SlDf{X)V@MKHL}($eK*(^WD=*! zxcLJf*7(Y5q@_u{PljU%rHAa}uU}^Dsw)8loIh^&Yl(lC`PJIaAICBJ+!;!`ZsF@K zj~Id9M6q9A9O0zhK@9Mm_qUG-+6l6W7u$DVE{`SS7F@nIH|ZeCRUOT?y-+Dx8L38M zwzJrCd?#@2j-3a$zxTO=%yq<28XS31abJzYlYSflt>S`oRZl zTpW#2B-v?v$h|c7g}t#^_4EDQ!Ntmv9OT|V1pLkwOT^D2vmM{=U{jKSMcT#>Boj4# zGXz|YAGlv4ehf_+j(vBSkXS-ID_i35x0~34`?iK;KZ*=^k{bxS>-l!PSQLgUH`wyM zW!4aBGQ(#lK94r2@19X8?#cb#CU|>`z`kW(J+93qxea=^?fmXLgUHlo9BG&L?U~$% ztGt3neqa~S_G=Hjv>o4o8N#v%!3neNgHKgbAkJixe_tP;1ic#S{_U}wLm#4BV6fwZ z&BE7VTX1uQ{f_q4V998QsPWx1p%z}l74CU|i|iECzhrKIod}EKL`O@t+S<2Vo)Dc; z?Ay0ms8$WY^+o%>e%+jW6crCn!uo+}r;<#G!N2(1rZP;mGUAP%_o_-6N>RqAV}Glu zm?+2iQmEQGwsV6d#H`M!kGAzV*WtGlQfkN zb8kO3fUU)C#E9egN;MEGOj@zMZ{Kj7=$YrD7VJlrDQHj|noa$6L+ED1R5_pW!!MKD z4gq7XaQ-+0HO%CwabuZ326Ulx@^Lx8>vVAng??_cen{<|RndR7$>uk@a%uA4ac3XL zdwdHQzm5#>C>`G)CnC*AKEk;dhghzdMfLT;T=oYlKu)*C;pKxg6Ofp^^`Ko}w;6Q8 zWDKvpeOU6Iq^8r)zgE_mBJPJ5=H53NhUhDq5fj__0j#?;O-^3{(7ytS<>{7wXO%(o zaa)4uEOFJ|eoiT6jN;$zhO+M|6-OX=Y0E`B7M?>56(T|EcU!+wm}0!eo&xvzqf}iX zWSoxP7pdL{6@L#qdjBzbMcO2ywpflAOEzT21JQ0R$*cIvczXXpIEHc`QX9R%mbfMC zyZ0ER9l4g_+x@=OWsFSQ?$f`&17SO1rY58FqnsmzF(eDX|NA}7!r&Hsq^@tGvL%L3 zn~0OpzU{bhdm?Y=iu5+?M{isZ}Kn5`R>&J3FqQBu+uiW|3l>;DE z{?GMe!~(bOPmp}>UViX&7FZC0hjp>8?~1!JkRx1&@7zD;b?GwrDD$X!FK;|moZvOa zz%O5s$BdUObbGnKg8>jvxB?;nCI;v@T6pxhQ$%h1=WMA#$z>GW~ z`YQ5f@f4N;a^XV}1->8&Oim%>nMT?4SPI1oY~q4N9wy*f-MK2+Gta*{cK1Hbx@93G zqcB=%%hL4j-ZMx0KujMLmBYw~)*OQQfKL5%phRdATVQJJAW4)`8COCV^@=sA{ZE^Z z$+LG=5b7fFf?9TX;V(64bcF^&C!WF&iGrfSKqvpo>|pi_;|S1dhQk!$W_s`vp&|_}1exiX96I%brh4gJ3Fk%pORWbfjsX zbjd%|xDpgOufvXB!0;aXtF=>iDvKl?O^Iup?XR?@gqo@PbalC^mAHyK?3 zcuwT2t=K+*bC5}}2eoOBZE6`(6{i2$8=w0V_n83r>Zl%JX{gmKSZ+zng9FnH)P5$j zd+$II7BF^(vgvJKbmL*1En`)BJT8(!r1jJ0)eYl~3zy7EH$1W5U$3Iqf{I%8#e!f5 zWd?!<%^??b@ph6*bq9aov6|}g^QP6va|@ejD0$y+g`|6Xt)ETxfQTfwSn3X*g3}^U z(SmZ018xT_N`Svb;%tsX6DM`akv99D#JIFVpBA>Eh1LST1Oos+K)}DETk@Go*JwAZ zi;%BmO8^|8{ZL`Z$2<^##@768!o9y9RuvGLp$A5Ee%e%6;OJ-#G<`i5(rrX0gy-c4 zc2)C+?g+{eeyS>Il1XD=p^i#(!w-w_!Fbh!Z# zTxA_kC=LsOz<`&th_nDBLzsul(p*O7t-0>H9NE;W7S z@txpv3Ge0nHU377u~#u!y`G z#2sT7EbDgxpJRd2c^Y8q+4jkgvWa*qp5PKY_hY3kY30LXyb5MIn0V{a^CX${RxoA@ z1hpo8hv^IfVt#>2NQCetl~#mOF%mNGKTlA-$U?Gp{JxYVJJu>cIKS&nW&vaiN7!O8 z$*VL+P@fOb{v~$+)E68=$cTVL9KDgk{)r$Ma!bZT?K*HKo|XVK?MY7LDcZCybJ3vb$cVYJ2Z$Z~ zl(<{hwX0y@BY9drUzWx*bA3>t-fT7B*&%JeUIQ*2{G60NY0f%)kn;hOMv?NAw;&K$ zQD%sQMJV}H>sMOi`>usf5z-P}-q6rv96Lm+gv|&5X@^9LR8BFy!*iJOh~y5?DoHHM z$BP{Ns4WYjckzOZ@Qml|JlcfgA_04LUXc(`L!ypD zjEkr1a6^QWv@qcN`hDp*Z~zZU+40y(P*$4o7$13Q1RsA&G9R&9=cvJN01b!pyYCsy z^t#maAS{HvftVP|YG9iK9GkA}O5#w3F;rn{BjuhE>Sp-%SSuni)g5=xq;9dFDBV)u zc0;{q>4nq8@CcTm#SXDV$bqlpL>9XO7aabWi&&dzcq2KM!_n4xa#eGs(5V230)hZu zleg@b#2$*=Gkw9q6ygX+l>I6=8eIdKOhZhlwRo$FS`^kUKKGulW`u!I4e06m@qTY6 z#aVLeGqk0#w1ZR03UEvIQfR!B0cc)P>k%ffXfk7gRFcOLdIU4Ob+Op4>i>t+;|AlSLDjM91bmula2`GT48q9L^zO? zvLof8xhOHk7%sGTk#QdyDj-*%10inGks=Mu*CK}%3%e0C8{iEuWa!X|CkNWPII<{9 z-9II%cP)x5;fFz0@$%h{yD4Nm;Lom)OW$=7-Qk#_29QBRjzLd4H6RPMlQ+QC8awo66gB~#I z-bB!21U`pR_6jm9r1*hs3QGX}Eij=@U6v8hKD(iqbl+Rp!kQ_CaFAEJ1DHeYeEJvu zok)w845>2KY!1;P5e61Th34&Kv`(3BB75p77C4`xO(f}*?rqQ^=pW?gSwf2n^b0yt zOx**c1q3cQX?g`dvcM%1UM|!g2c40@6(&1Colt4$;n3attIBSogpZXpJtjU4G_wOH zWz!}T?B=NyX&qy4qDDaIfo7w-N3*wHQ#>XI;u#dDQgMCYivi;x%IT|!Oo^i$3Wm4J zWW&w?2WP47aP@`h^&uC5y#hQdk^njECA8B#zr>EFsPoJ`*GFMHq)4p-9FF&Ir}V|q z4sOf4SyUwZ%~6M}fj|vRgb5xQC*{hx&T`0HgN6&z^QW4@U|30(N;ZQ||8-~Sopp1D z=wYB@qIhU1lI?jI!6F$M3Wk+ToW1u0zY`y75a{7Bm$ngkHkFWh`N$PmYcP z^Z`OI$$``l5&Y2*yTyq6Dm)l()7&z-g+~DFK*Ey>7M^Hd0X>5pExm14 zQD{UqF)0f|kfViDUeJ^U`_fr}2i)f={Xf>eUPRx zg5>$y2fGp^LmiraGAQ!!$n;FhiTEA}y*q}|L$|PxI7koy8V5-_!GkQ+6y_~dFy6jpfQYFCqB+%( zL4z3pURI|5?&Ac^1@TzR@Lzn|wj@Nw$g(@8V|+ltF28EV^j9)KOB|dQynH%%9+$GL z8W6r;kB@Xm2xa*hEurH|w1kY3s5{5O3djY4lI6pVPFMA@`;yxPl-;9wf=tV~X?lV; zu4h9$UAzJ*kW$DJ;DsS~kNIFJ-YSCoA-qMVl=tEbg$iU#g+aZ38&^j8DWK*u0tt*Z zJBs(o_AgpDH;98Lb14kJOqg(B9Vjou{yq;@0)$ZIHZ?D7z&4e~9` z;`+sU6o=AW}c9Z_+rl!M03+dPf^e zQ#g=GF0=r9WMJRBoE`^7l+1oGhXhpQDH!}2ysS{nbjU9fqn;j; zIs-D8s>}#Inrfmk#KqqmzC4OGU<9LeSCyyNK-7afLXEiJ-A@SF3TNrNgKzdChjEJF zbdVF9YtSsotN|-VCME3PB4KICH8{fxIy{>ZdDzhC1RjclE7YXn&=!bsz|i5Vj^tfe z#IJFUl$Yhazz%5HfuR60k{K3J}NgV^svxk*JQs+2J8vwip}*Af#OpaRz|b9FjZU0eDw~u7f7; zChVZZC5q^ztojuqz!BPI9C^jWP>a{^s7CU4ido0wYF%f2!ti}nk7L;0fX|ngiYFE_ zccF96C)R*MR7pOnzUSc8^xUhnP>)_eU zplF~2L;>qrNLuEZxj#I6FQsZ4K_fBY-0t0_UF}j}LyefBufYkCvYen+er+6t+E7EW z79ziYQ-QFt=;pZw;stSicBTBTX+)>{kXKWfR;V**Bl8_tXA0-4Tm*2sAwD06gRvEW z!5q@qbCywtN5CAI=}dq$@7rD2^MzAx}ux zjKi$9BpT0s20&k-SVI~VgtmFE>L;fjXj(=t=vIh6F9N;?ib?(iV z(ti#SzJ%AYa48}7L*(*tEImA6n^MYfnJIV&Na*qvT0rDe49V#L7a*_X0bY|_qg*^g zJFH6h7X*&Pldp?JW3DRfYo~a*1q6p53ViBN8Hn+=E$4p&dNLF`$SB%o7G5Fjl2^ zkoWoMs&b1?y11qkD&B+{d~?8?ta%5kA|Q6trm3);wIu zqR5*MXe%BVKA;ff>X2|Olw4n|z|b&=#gSk*lRvRH5Z?i6SUrIOJWpi}c&mCV6(j(L zIXXc+B2LUqsd?eitRU%9<#JE*$T4>W7%R&oE!jyp2b7sHy2MLN1ZPa519cqI3@ok) z0m%s(J_XbzmqzDJJ4fL5z|O!Ekc$D*)@BJzYIt5~`ax`3epFvFHzg@^Ro77SW7l1RJinP0?i4QW)p)S2P}UV#93k8G^y$O zkO6LOm1IE!{^=)lX|mmAi+IWpkeb=}UQHyJUbI8|z#A7m4o7T5;0F#OqGJo1nN?Eq z*kLhKZ{ZYLQX)r4Cv2r9FQ|mxwxV#Jfvn?jHskA1nxQLwt=-6as(zVS1L1UyY0(h$3P;Fyj><6#CTey)n>K*zQ-$@XJgqOr(v?yb69 zI$_Cew#(LYT-ZYv6RzlFSTs+)DIo5etc7tk^W@!aB(bJ-nSOWI_kdX|& zlr^Zx(ACHK2cCvQ-u`s%NWg#t2x*+CQ^b>Rk_~mW z;v&6*EFVuKQH#q%f^>j)C8a{Al8|=q3a=x8S7+eTE-nU}w>liJ0a0x#Ht<$Q)VdK{ zlK>x(pobA9fI=@1`E!_jHtiV*DF34KjinLHKU+1!Pb~&NQhb!@xZhBcJ+Q`@0f!sS z0Rov5UECSC2Pj7h>o2vO9s~d~n^;JD*Z;*gm>ynJug1B#xE_K`L`BI(QJU0R(biiR z@c@>rOj zFI)k2)QwBP98&$2?6e$oBe4-e=*4sLB5UKMnrL3lU53i6FSKGuRMcWdXyMA+8A;}v z9mq}cfC}l3nVRCsJOX*6%EU&4&Y&@wG{?nqc={*+qNR`)WE8#n7A}Z+u)h!@td8zQ zBnGnpx&*EWix?q2?F@G=Eq`!HLe7k{Boh@{wJ6^fEQZphHg_;C!aortiu2o>6x~@F zjBi^>f$)c|^a8RANV!KDUgWild^&uWLB(JWv#=RTK^}8I=}D}HS?yny{G617A>(>{ z_krjYX(Ub>^U4(Vq64VQSnLpu*2~_i);uw9UbBA2O+;^`IHtsoeFBI@$v$X#mp;Or zh)A%5$&?t52YuvIP;XRYI`)|{_CpA$L`Ad-C_)KpUitOPk*`V(K0|1O+7I2#p$jwx zc zaby$(`meI7otE45;DD*5!$IN=-3NG3%JY4{NL{6iTfjkE9mKo~X|Nbe8j!wPsDXDS zD*37&R84nakQhDmvG7d?3M6#@u$2$FFZeNe2(lwb)HK46VrlMGoy3POAt4Px&e*-B zeGSY(Qb!I*P@RXHG?LTVvkLksr$6m#K`LOeSx`|2lHrR0dM7HBSQ{UTXf65gjZpnmW2!FQ%qhbWOlMc(=96Wrc%EmGWmC5r?a^$1-6P`8}Ou&ce9K9K;z z94g~-PfT`0|_(4fV@8nH42pJoe5B}Eu=%d`TnuY~7Xl@=1h7S6y#v)YJNa6-Lr z3+5EWK2&M1Xu*r3VGU!ZA3+EWDm*6|HFgikCcwg+r0^)bc*nazU^tYZ0+Rt)zf{U; z9tB1OdOL%ZR0DbkWZ}jkDbYMUnlj;l`;V#0xYj9*1?pj*1$sXLr%WBd84b5Fl+k+3 zq}b>{1fI9>SoDlgI`DXxz#TEbN>sr?`+HZql~i!_6_f0J8H63^m&3sV2&U~_Ts6^- z5FQ$z__*hJHNVk=VQx%RuFhm;HjRx6jv2sUf}8<$AxkeMXDsX|gC;s{nn;dprnkC|guv$F_8G#`PFhi44qeM1^Srx3(!Bo6CF?z<-C~UM-G_#H zv`pgVtQmwIQH5aaPi^C{;Q(9()}(x`rc+NBo^``C%hHuMLyjRV zki8}s>zkk~nV3Nc3RrCf@tM!8Dz}rcUW$aO5w~=a7Ia0B&?yo$@NobMGZ-Kl$aYt5 zplM2Wff@F4^Cid?0w~ZHDIOZ>DP-Zvkal%eJ%TWRhlOQxM))BOor1x{3gg}cN(pGx z5onQzWVk6DFsg{o0x8SPYvcf1VRNBx397C*1!+P{1Um?oCjh{3XFzjVQrE+|q)hEZ z#jBWm`Zqm5&j5E#z`X)s4B~hKYr2oC8CX?-Y~|}pYu-C!6Ah6f3{gL!nf9tJjy>SrWojbfS z&*fBEs8z{9JH($<0RKOX|E@@YzhXzBm$YP+tDCrgJppY)?DL90P>*#>@U1WFa@99*W>``*!$r<9K4pA5LURHAFBq!Iw(wu+R~>S z&9R*5>`V1~2D1`QFND;nAVWirlZvgh7%zIliY*;#cPJJmZX2svmqJ0 zrM~&``m7AI3j?Cf#c+;kd?^I;qk%kKb$l$q2v{LM9R{{s#PTOB#d`D%{ zA_E;J=79$p^nS2`WCjtf-%v?J{&G5;P)IeX#LNp(Gk55Ktb^%@V9M)xKLRR@YziJf z4%<%B6M~Tk%g%98cz<^xmyW}?7$aB2uIS>oAiWJ74RZ__T|tjR_7^13`=o6lU!wr0 zSs<)ayCUJfTo8>=x%ayCRf;n20kM~K&U+BTlSoEp1y3~4Fw%iLP^t7$FVqiI*};_r zydMZ+Qfm5zu2BLF6Wh0gR=!6hlE@BP0lIy(R6-sRPs%B&@c@l8o%RGe+?R!#XKk37 z+Xz$VC7n5R395s+X?S-jD5I1#9MVQzYSN{iXpn(3E|^JX9#hSFbFx7*IN*^>VuAN>0wq|+xU1VG>YjPL>IUMak;6Rui!&Sf= z-=`stRZ$mkd91y2JK1g*0@*dpGA0Q0b2zMAouexKBtHOZQ5;UpB&(q+muiUiRfXo`LX{>dy21=BC@~`KBB7^3 zLU!#+8jeJLrtSU}NFPx2QwKAcvcM28(k~&*1faAO8mLf5@RM0pGVUmkVpZG1lq+kD zMh)wu;SThRsVnM#4n@K$4jmG9VOv3+Y+|#^<)HuOXB&=Tph;_J4JD3sGzyvL28nc5 z!O>MMr4mG7@Yk!zj?ky^w;?N``LS}x5O+oi;RpkricBq;0a)D%L^tzk;T!1xGVGgH>g%B)s)fd&o#iu7oaY4vs& z0u43u|3NDPmJPNYI?lS63-CDzZ%cPBWPdmY!ZfG-ULKK?3^$qB0mCfgGMQxFjLlV1 zA?c#oU7~VTCcB{{hdLYIQPwL#ig8DgZnNmpR+w2$6i}KFGy(U^aCWeR!J7vKdx9XY zJC*t8YnCwtYKHrmLIlVMEvZv5q&wV!f)iC!4%g@p39OpD0u{KRGhPHX(s4UO4_}`3 zAj0X9hJF`X#EHC0)ES2if=UN=H^rQAGIh30wKih5*wHu{lH!-nepF4WA8F2X4=YF9Cu1nNm}PwkcRauesKzqJLjt>2*!D03GG>8laB^vt9 z;Ie|oAUB4X>*Nzc8=z+#T2nB+K}{(kG$a$WFw=$ko8@8ebLS$ZaG8~_kSPJL7)Y%q zHTlE4%PR~vLR~)PLBa@Cn}sx*1Em*}RURnX9z96f6=`!wAZ5;g)0Er#l%Zk}-9J!Z zVCpni7>a-~MmCeDJ;DKpWW`=FlS6Vk@eP*lq!^Ls%nE|25O)j2v{fQLjrJqM-r~L^ zP(Y`;BTb&%hqsS-`#gkMsX;7*Tn?S%MX&rinwXw3_`;E3tk4l&6O~cIR7=ejQ&0l| zhE5A3cvE#jjYvfHIdKFj0rWVV@Dc&xjU6mNIqvR`KfZxcOHljJ=`NBs8j_+w|JSjC zl_oP1cji43tp1vA4fYMeLJr)q^idClnY@Bq>^Od;4s$4`mu&8Us&cV@T*yMkaPT>e?r_H;3lhQRyrKOu`h-$!2%e#6Y#$s+=6LLl{J!BqCtr~LHI%D7<@+{Sj9U8pzy8f6`C^ipBqRysJ=do zl_fA9WGp*xOfplV?ZT9E&M>tQGZh7gRNIWgsTpTiDG50&6k#hY+teKx4thsX8F@m0 zw0>)o#2XcS@(f0~jAy#Y(4zJPs+P261paiK0;Ljryu(Wi}4*t^M%zyN(U{)$}nrp zqX*^ceH-AY77>9TBT}#0F&T>d9GAZ&6rU!$yrqcc?}B|f3fPBNwSZVi29t~%O6CUI+Up)G2+lW!6m)^;{`gbsXloWMg{h)EX+;vNv%#j+lD&5ghZ8FwKrSIS!LJHL z?9rO5q|!lz+=*+5&!hn$1H2l!;+8JxE? zCG`n1;TZ0a3Mw38AV(Qpe^1URO%714U_pYSghXV9EY$4BSEt*Y=az^h=rqq@j+UkA zvcHz&r8sa4x*T9sa?tg%9as=MywnjpRW`FEGOy)ge-Q8zcA`m(J_sOFo{<=2>_~li zP+AP^EKtkv#29*n*+?vROBVi#C?tvBBS;sMS^xzFEfW}pNjl2VAzM^KprzLJ?Im#L z{Wk2pk%&c)#ktE%_~55TUdDNr84Hje^L zRy0#uW_7!;gS415cYF zO+5t^Ue^Hxki67~bYx5sPbf*VHmIBkH=TIZYnrv_eB&Moa@5a$X3Fw-5Jk#?F~Eq4 zTC|0_E58B?fqY0&={19FNN{FN&<9rp>!Q?2=r5OM2M=j*&HKC}4*O()b{A?5q&5CE zNgp_u_&Z0o@3)5k%osTL70>-iUp33=xd!Jp@pdf3K0*^~YIs99k5~g}!bk#F@aVoR zdQo~;(TU9~SVAKUskELnCyKf!lPM+62X2+%b2F=L=}#X{D@6@8V|NpkhFCGQ6mTou z8^xNUBFG|2bXezpx(!V+8J%};ox<%R=&&e)`B7&yfrhw} zYZs%R(0-WdrPt&I7=}@#%SZfhx+~Ld&E{!$g{qbgHtU(uY3L9h@+g#}RgFd-SR*h6 z0{s-#PWS4DIf3G?7t%y{M9_w^q8?5Re7i_DRtl2>jXqFXQIZsV)bK7wm9i9Xz)~2} z*`c9^ICl?y9Z`e@o#-`qRW{B47$Si=XraY`um|G|M^%V`GXx8z?-|g0_&Wpi%o4A! zj?P(hc2m{K;jK_`dmP3HQ3uXbK}m#xIGy}RcElAhYuuHhaNvYh-lhBD`8j-W3MIL$ zxz-B7mK2e>;TWO*!$xdkeFFg}D2d5{2Fc1AO->qOeT*lR;dsUdX^M#x6H-3|^qUC8 z(NYQtF++Dc^8=k6zg4(qV^EKA)hOqfx_bgw&h zB2IBe+Xx=PG|h_g;TepKi7Pmlk#xHe#-w)%0UN?)3mr-1pBqC14imTcrDUxVZJq9? zLhdT!d7*C>?yb#}vPsAWfov%((o!!eSd1cOQLBXIXRx$m<^=>EW!gS{?UII}M>iOH zq#Qhi=)0owN>=7kjW!~_qm6vW0j9v+ju>>wG_N~q6*Lp`S5PB)ImZ`34zhSy5-(o>G84ws zC6TT>Y7rU`1n!FFg@;{I@}?1SD=`}5Bu&Z;emA(Ka4B-UB|C6h!;%BizW5d_M8Iuj z5toacvqLQb5eK$isX-(ia5q~N&-%kHW7RUD)eUFc`)9bE_0GTugsoNWJy z2JbK%SSq=@f*=#TiE7&L=9E+fr1aH|Al8EhdsDSbX&5pUV` zP)!Hsx}d5%qj`VOS=kIdyBcgt5m04}y|0jC$=?R#N>!P@AJgOSFU$zpwcLPtIGzmO z9(ZQh<+LMA47yAfh@r`)M4ReAzAretu%cC??CPuJkamG2$mY3oJ?I1eA%wibbfC?k z)^nil2>W`#ee)hC@S*}XrR1;xpo|b1CBbt9+u1HFV*1)nf-z`P5yv}+I)V;U!8|Lu z&;{PI48jo(sOdOfLv$rbK%~+EKCYGv;QVz2k1diy{4eFl7notl4#?i5K+GQ>PQ6C*xB=ma@X^F?l- zAIOSAqC3nOFV&U3VnM45LbHy*7dy-m3gp0WN*Bp58Z-I~*9Jn}i)K9rn69S{NRSkQ zQnHmC2CqqKLTEk&PBsAhBvMK8b@hZvxt`AXM@ zzS_(Im(5JLQAx|=z(0~v86a00Bi^4HvNe+1P;;N1F61PzcMz(C?YfG+lY?5a@bO)m>3upFY=X3IR6lth9-V0^GK8&2fd%2IT@q7NCrD#Q zisV8mU1`W1Bq9<`4i+egl5zw^DvT%OHqcHHq|n4(G9JTUiLQYcfq({>EHS@zA}CvC zin4%hi=ReFl0zNNnmg8Dvp(Q)W3{n@dKhg~M56XX$N`E67F)8Vl1A~95xBw*r5yQJ zUyp@7#odE$k$hoO8D?~&1_x4*NnlsVt`}-@hz&IBmvpWB@U9`6Dn(+AKsgahO|cpHj)=`d$SuKA!LCC_C7?$3?ADM zxS1I9sJ-P)iMix!DAY&u>X3d~kro;d{2UZ^z-5CuWZLVU!$pe)1p&cWM=+Kl+JVrb zW#wCn6I?p?Gg|pTm|)C>HfNnK1&$gFL?!Z=928DT_cDrZ_)7a(%3zH?CheLP*f_wJ z)Zmm7OuwQvToe(I=@vgmYl)1*uAr?JTmdrH1S0mWRNSyz?Kg=qP^q|R0ptvnzoZNi zeSKcKqGYox%xv4FnHzB zI5CNvg%;6HYdS6&YN`Kh$FbRJ`0kY}0A*snTI0p|~it?|N7WQxr+iFGl&xO7?;XjNpOW6p~}3s3fSt? zrY`XZaOh&Z`D&liMp<&+O=0Gbk|aV;U4ukV;;4YuG&sx%2wnitAc^zzUM_AVC=k=E zTs;6)yR@RP7$J=c3Z!^N0V6~&Kf1RJF#(g(>Y&hzC=~nU$e}I=dW^f6$Q9lVq>zA@ z3LNGN$~&u=sL4^P0mhrYH&FGWjSDg%2M4GI!UeclZZ6A?R?5DL3Z^kTO*mnm0AK~4 z`*8m+dIk*R@xV%+D(uYxrvxdXaX2h8q1*%k5o|{B!kk`D7jK)aBZZa{tZWz?bpCvu zPH9dy2-7q4=8{xEK?60BlzjZj(iNHt(ZFVVOj|dK~z$Za-()aO&mn2yTZ>9Jw ztEiI+2dv3Fr#p6$VHQ7U(3QNcNYjI9EOYSc-XlwZ@oq-H(Fss&ZtgJ2YXI*t`C)Z2 zzHny~t{v0Y9d8*%90a`e`pE?m61JJcdDPM6DEIJa6O;$VJScJuah}RMWwFMu+0E58G#j+OEVe$YC#g>CXvifXxt7-%SSV92&fX& z4HDRBdBdS2+if9_u6TDagJJd_B(RZkV1JMo2Uw#79RT#9;Nh+>==IWvORvRVQtOsP ziUt@Ljop9|LQP0s5)L^4Vco)c8YNAzHONB^?bpEi@?ocqP?viz!4Rp|xptQmP@6mD zlQ-#AZyk8=@Sx%4u2A68sLLWU=ir}$r`b+Xc5py!E)cGyLP|xw@Vs+%D8#852qobb zgA(vz7AlgMa@Jv%2&F{%_qpNSMqEshF~hWXI;fQxNH~J-xZ=ElF?1z>^~>xUs8s_P z--Lwmxzd^f`R>z*YjoUD6S$-|QFmI*h5>;{(X_{`pRol2ocykZf)1yV#qhGfW&tT0 zJPqyXgGe<2MUn3TDk|r5{5a9xY9k=ZaC4zr#(%=|Oxk*#%%-B6ADmXP1;8vM^N^09 z$Gc4amlWL1jo=9q)=>djBbaeZn;bYj1!xc{I*?&q(zkZovKlZDDBV%13>LAJ?o!+-fkq1`Xz|7z z_+pKo^U^l8o=*Amv^qf5kq;M8ydS6g(KNNTDN$>47~x zH0ftqdRK@Fn!s2^p}#^GS-}*jvSruOQVPHq1=BG+v#XL^qA4y1;Q)LPrb^}G?jtXGFe7j5tMrfXuuop?B zh(`t8nP5@C8~{=t4?83&a6UQWP2kb7kTCN=6*G2E9$sil1JX$vuSdaTW{Tn$PnU}# z20&7gUZLZ;u}$*JW*R|k!%EXf0*R6>IejCdCLye@|dAOtei8Y5@+ zlN)$6;MNW?!3U@Si>hS&InccSBQYEZY^iyYM&*UVDE@UJcPD+Mf6&_DqRx7x9t11WrTN$b|C`g7Z?CjIcmd!?p48uOy$ARNTr_m}YIGogJcT9*#l)9Xcc$!IY~6A_WFg6yAtvctB$-KtrAaq=hytu^ugm7+Ww~ zsq}~&o#`4l!H`rm<(on13|UNB$N?PgPU}LJ=g4|Yq!TSDQ6tMLh<1?i$R;z1Y{E6L zmAHpQmxNjOpg4=8@g=^P>^_(<0M|xdyfGSb72}N8 z21a_hge+h(4JYNJA~_tthCDN7C%DR7f%4PEJ;HGXV;~ z4AVzT1&#|New)+!;Ckl|bWnzr>Y`3NhX`WOwEu$b<>X|Ma)jr`@G(0(rlM!+co`%d zQDp?I6}sTEcN80NMB*#jCWGlWCWS3%PmqeDxyFzp%+}FAKg4<~VV}ZAT!^|e=dRIB zNSf|o;-Q-LI7Wma#WdhGf*?g(eJ~ z52l7e&bu<~z&w27QXWPp2h-P;k!14jM1>6uc#5C4px%SrFnGekJkf#f03E2HC#kz{ z1bQ+_ZmDvD2@TLB-bIE3;6-(kt^xiOeFo|YU?QzRzEO2Nk$A6FTGVu7yue%R5EwfU zmRv@hJ&h(J^+i|;QV}MB0JPCO!E30KAac&Aw&oy{1{9PB9;BDJ8t_iA@(VUy4w2ye zq&-Lk4Op|1CGZD{c=-Z^6Fprh1b2r86aw7%BO%YE9IrXKo_u(jMHF32@9{0|y91e@3xI_> z1e$kEYDC8h$ufG&as&bRv_gd4r-gb`2=({S&C!S-@%ml@)DSq0;#QIp&lLn{L;?Fy z(GmSO+fdFn*p4fxdUt6}BKF;)wg-MoC}o0Xbv82oO31I^b?$IW0M4LH0pXO91Mmm0 zbQ703AFDRfB76Ou;Ie5AIa&c_F;hMHYdP0&iS`S^;td6$t(O5Sb2Em59B zNM#E_{aW-w27DrA7@mhz}sITdu|Mw$qoP`JHnTN zF6e2>cA1S zlE7vj4y+j3$xI9~o!)@%rCGW|#sc2zS!2=E97tdfvUt>`>RSw1FVx`ORX|e}6>Y&} z11=NCT9XOMrBF7gXO~$qxB3`hhhR8^p4BmX;PXg38NZAX92|gKi4va$&-BIGO zMrf#EsZy7hRJ$+%2o6yAGTVmod;%&Ho#M;13ipJ@K!;)13N~jG7<|J$Xa*&$eM7TM zHz)@@ca@MR>TIFID+dTVXM*4zD}|g6_9ISe6z^UIK%arfBLDQjk^vdZ@G~{dDX%k% zM5;;SUK(;PyR&5OxbMsy?hFAFrm@G#(~5)B3Zr|`x+z3`YIR5fiIn7sYCJ{a=JK=a z#a;UF^cDI5=2cWyC*JOITXx$c%*@#hq;CYI5}+Mj`v|xhz#pHNz*u9?;CP!{6?q@J zQ@UgsK|W{almGOoemM^DZYV62y_6oYdN+6DnBR0RZ|P&C0MJbWS>gqB~lC3|^a>IX!5gR9etWbm9^Ku|TBD)F8=E z*vfFSs~5$H;0P4W-lY1vqw*1(cc7SvNO|_4+`*DQ)LBAJFqj5XlA|JmhPz9hBhf?G z0b%bH=#>mny7WiMWqqbeGmp|dxoMz0d;r=)BO!qfY*aEaGIIbVL2h-LC68EBLB~sS zS0oHFobepySYkp;QnJK$XBAY8K)C!n(Hd1(f1lJPG<+-B5h|WS@P}3g!%K;nzO^E#!TjL;y*NJ@OL0SMwrtn8=(>sV%%w}OR!i?2YjMI z&zTbdx0xESo&<(0Kl^cbGoJ7*xII{??6h`hFI1%rqNOR23{~ZbOmbF+6*?848Z6=QOnmrLqQ2N`k=_zbABK7=9*o`$xzO zcglrpcwHl`{3bidg_=TuM2nv)CNgH%v7feZ>4aHYgZmiW>aN(dD;_6X9CW?+l1PZp zdZ!9sO6F(4Q}=V5(R3Rx&LW?N*h^~SW>w*)vSY3#SBS+Q^P+sI)(Bb+FKBvj#jO`f zC}E!Hlm)S?B(`A0w~~%RZ-E1iDX>p;DGEjZWd-CQ06=1FqEl1{BM6yHqq>sydz+2K zb(pRUr>x_hpiU+!!p}f{0CmuD^f_`+!7_6haI)0!>0%ZXZVT0<+_Su0X_nFq7v@S~ zqANkGgaJrUC#B*t0aC^;J#K%VlP2sl6uX0IPnCfDD2_Wq0I;cY6>QN-p}_G(bf3xf ziBMZ&lslBvtRUwj4PmrpT6X=mg&89*y-=5`o4QblHyNCpC~UqosEj~p zL7*Deez_Xj9KS8A>O)p}M(A=;Vla>9V{-{6;FAOuBye45;SA=4J-Kq6EMHM!>QZ1{Eew>8&T+}!=m zJwQ}-yfZ2EOCP;KG{u_~1ij_WFiu3JIdWs7iW$lVf;16n5jrAC|I{8`J_ie8`lqA! zPV&$~L^O%C_MhKf=G;H(AC0Qv&rge6h#=EeoW@=W4ke;8Ajy`#2z;3CO{W}Pn+dsX z9QhHe4w$z3yV@Nogo~)=~T!qG^YN zK0~w2S{&ku7kM+iZzD4PK-bi4d-QN{{25>Jt_63e#GIcc_f02S>vb(FFQqjyDi5!= z(G}sv+d?zicVBB#Lj^q8{z!K5S;&4~-$T7-b3tFNk#^T;MigitNp7I=M@?5##iKEw zLhmh@do`6|fPg-KcZdbi z1guRa5u^m)Xr(zdu-g+QMn@${r>5zE*w*3JbwYR2bf!UH7@k~{@)jz&vP{;o()5Dq zKK3Q3g)XM)+xooSF0qbE0f>-s4x?BiS68D7QKh#?#0-e;H~+CjqPf zv7#Q*NG4GH-~q$&R2U)JRvmyCxMg$t;-#oDm2^p39}# zMBxzaGDBs|pkl(hj_!M&WWIbTb%uGm$Svg}Tk3}v@${#K;DS+OPn0lFz zF-ed0;-}>yN-`n&zW6f`2T!r91v{nBVNo0K;E?R#{odI)xInFMQ)EBk=oQRYE%T1W zyGGv1PjN61#WwZ1ZQlK~XFs6_vM(cZQ(IjJGdBAbJfO(cWR+;&*bM;=jv|z%7(4p* zQk}?C_sJ4MN=w9WJvJ1~7jm^`X^wEW)R|n-{YN|h3KMWnCtDK`tcm7O)yNb0c!c*Z z2?;f-m#H{k^irO8j9kY^HXWE@y$irM4m4J1i`dJ`3FGudXFfT#fRYGkV+fl)Jj&;W zC$cv6?u5O`D}S$k%@_?ZozEaI60@JOM@60C{1fQ>4#^1d&6SEoi&M2AS&wJ$XP>P= z_HS!*+Z#rUjE?_CYaZqD+Bt6OLfu@J_RE@o<|j@1jAKz8A@&IUgq7Pw`OgZ?w38WM zcOqKR7uLF2BDp09v-N{?t*9*iR(?$TAl$T%`O}NJOh3WuZJF9bE9b;_OZ=q63sKb{ zYKa9=6AH(RD%5l_!HjWb3tY>*Gsk8%J9;QQ#JqQFV3rcEJZlhsq!6>9j;H+~}t7t<#uIl}cf*O694NFEGNnG&L7(8a6{;n(oRVPHU zb!s_C$Ou)BL-I2=AcLtMaiyc9YjPv;&z`+~pLVKgh$vQyEMthcLKso--p`s3h6b!kf&hu+2-aAY zrj}28u&_JozX)uY1j>P2)+TOb^Sl}AF|g$ZrJblQ+Dd5>`y~~$Beogi;;83V<-?aR zVfEp?hg!Pn9Fz?s_D}rKJg%p(d=Y83qhc%psi!{Pk>5V3T`95(8BY2C7%z$P%c9bWX9ef7A>xNp0(hJ6ZjH6q1fo)QJkgf<+v_lL=to(-*KM?{d zy~H)$SGvN!&&2B#tZORgTDV0pv7*7=O074^O)n)A2DK#6eN{lklN5QF#v*c2DnE9U z)AzweC<&|aI6+EHVF^Ohw42rk>)_~WorPsC-r}<{P#Ju~n$DJ2LwAToxJJJ!68Ryz zNV1~${pmx@OjH6T=&KegO4+wB$uvnkIPb}}H>6%DL4q?RG(V!*2BTQt%ZYGoA$qY+ zIU>f27&+LO&o1?YVITl<3N<&%h;*gn z=P2DJkuDJ{Fp7ev#^E8pg?M3es--%aH1z$CBlqugfPm?=KFE`(e`rh>S0jtDT=%%` zyDjOD2p8t;n z`!+&i%Dd5ojf!;#9}4JNMAOH{u-LE#5yF2S<8oHW}%X zKFIGLFdNd*--iaU_PKocHnKf{r%y>fVr@+x{`;Mqd%>c)2z`X*=Siu=<|EPtKI#6F zq&#Y*vN<_7LTSY6vt1Vh2g_UtI6|RI6c9dF)ZKfZ^{;f8Hhn$a$5Y=)E`L8OPsb=M1 zM97C`S+v-Ih--mM3&SELL=4s-X|>M{Nj_Ger;ZyUdQo)JOcL>ig5}d}5!yt2Q3?&b z&y(!a-&CGnV7bJUDG;_t;17vg!x3 zd16lfsjQ*QrbUT7nTyxho0rcZh8YAKS~ZnwPUR5t({InlZ-N&8z1)SYF|B;p3rG4s z?NiB-u@f^%X@gjh5JwlC$UdQGsFNot2_D80!*pm5=qa_V->qM@7XN> zKu_1pY0h4Pv-fx@k%rZi$~{Qwh1*$;(bKjRg!hI?>aPaRF?&n>z@9oL**9ik`-U4D zwqv{neXOUZ)^^c*qe8CSH!{*`F{6=2MdQY(R4_!G9x_#XnF9~mzO~_~BWFYxlUS%i zF5&{+sEu3b_{q;BZWJ6ww-)0Z7QiB%s9HYMHhIIT`7~npaXRtH#T3D9_u5=PBhu6W zDUbeU3xk<}R<6sq-dNdiCHwtvejy~rtdq5t?QWwz(Kl%^yFGJOs24g>a=1d|rV&3v z@s_ab5=j|nRfsjm^t{B0gt(MC?ft}JkAgE%Xt<2Itj^}HOb5vAa4-a;Ns@=qe@DHW zCPaI!5amR6@T;`MQ+Xay%{SkCBjFkHTP3BR;f(5MAy?1af1N|=PZg9lFbGBCJL^JO z{aViPpxe5eH-@bz7VBWjM!136w+^#lo3=WR=Q16-?vS5qVve=2z_YRlLTvdm`PC zl-fMYlzuM(Et6u*Ut0hvJ8gQ2mCm8NOp$hk)mMT`_I);-W6~+JcPr)>M__~SqS}=r zjC)UMnwlH#cu1kEBuU;imCC^uKHdU)#*v|NA&j1%`vOt*w&RY!{hRjJS;W7hYY0zV z_;Oj-lkoK^txtJHX^JGJVk6g9<5e~yqEOn%Eb^?B;c=6pAn*h(|AYHOb_!n!%e9sj zVxFFvW>{iZp=W0=ojv8h>(G|aoDigJP>XrjX>Qj ziMd2jGmIf?H9yp<+5lW>a_P1us9n@1tmH=(*k={oYZY%Zzc&$6q@H?sFdz^zpBnt- zhN6drN3l&5Or4={jk}##U1+dp7^-}#Otu{P(x)0>qi9Bn+&^Nv^>ENi@fp$2oSr*B zv9Zuo&XqD&$Y!wZjSi8lP827H^TB!v-n<>!n;=RXE4uf;Fu9$CtEZ9EU4l?Yy#`}@ zl)`ZHFZ*-Z&ZIHe$2JNN>CZj~X7zj-KS4ku!#9IiGulO_T{+qzk5-PQXVf%Ut9NFU2wRM4*P&t4IQpAt*X zFlt1G&{Y(W%TX&MP~n3z7V;T7PAY)Poo?^6z(!WHvJJ9lLwU_ccwLA%k3P)D1FiS9 z3fIUduTMR)4q^W2{ER^|)IQXEfZZ1o+F5~^BN>VAB=v`i#Xu>Xl#bBJLA)rlm*nYl zgv!r%h{B3OVwv4a_dlJD2rxbxl%-C9FO`cbFt>I|XcD#<=d7-Nvktf$*U6O@OPV1yr_BqWH0x@8>=$gS4fSqUtCloDz)Yq#L-5V2<)(U_y^sBzZT5du z*^&ncc-m}G2l26Q0hj!bf)p-M`~3gJBjI*cyiV}uUqa;rnYN8EM8}dVr|Dz0iAhWi=3_EUCOP4-@JdT^?>I{I~lg%bE^eZPa zK!N1i1amBg70e$%iR9WO^B9Je4uMZ!e1Ewnd#=#aUgCZ`+yzW7qIF$khqb3$gcX!y(?^`V>x&n_`Z>S)XtU@Tr>`3 zZDb#_JLLrTjSsiQKn(2cE%kC%Gy%ZYhYNG zeW|5h)ykp`a4^fh*;4Pfcixx5jX_O2AY-=ugr#27il#l#BHMo5Qm=JI)4>p6_1!E5 zg;}p-aM9sUvz{G<_`kA@`E-*T|4UIimttPf)fd&(j+{SN*y(^Kf&P6mj;Wv4wk^x6 z>_87WLFC_{nA&yI|EK>uJ3HBG5B=|=dXK=HF{AU{!~N=PlDa$>bO`cMe%I5@@wzlG zhmnwf`|HgDEL#ijDI4yqlppYX3_qk1^t*KIx_#cC>zFe7S9RAB((QA*a~%QiKjaV= z+`O)aZ8tlXJjJC3CN8$DJ}(=W zZ9Z|_vA~bl%W2@Z%u`1GuiN{FG@z%Q;n#<203MB>$IH&xX}8cO#|#ntYe2KpG6{$hXz|Hti~nM%eG_V&`DtrL~U1;9tOcz~%nt?t1>H%frnzi6>ry;}tw2;ZE=M zY+EV78MgBX#}k5|%_RuIFFAJf0z*`+h2sG&T!LWhmvzV|?DOG%@hrUK0Dx%c@wJi= z?0G@XzjWBQRM;>_t2A<2EM>y*yZ!^KymD`R-W}fjbaSu}ke3hg$?AH%dK}IVEOET< zeWCXG`ETk8f?%Wl>q^3~n@8rlR>IdgG*9727A-||ucr%_;XIJr&F#Ojy?j|YynnXN z|6=?X?`be3=r5m9UpD;aQ9A$p>+NyOA)b=3uh$>wgtTM4qi;~BPZgf;<;CtHhY@Jn z1AfyOp_C^$H)V8^Wz>;Vj>lo>_q2mWk~SU6rtsB2MP~aF>a0PtxzXwMbR3^bv+h@B z>(eHDa1i#%N^6PGS~5lKGaco-se2^Y%7k>jaOd+*!_(f&!C*|bk)N}Q|9@Hp--%oD zeLW$`XXz1w9nEEHNvk0|kH!Vu-G5WdU4PkG_~l;!f54*gggss-7z)1b?NbORG2V{y za(2VE4_O2|W`y%WkbJzUyd$HKz>PJ3ud!|@$!q5vM@db$qf$Ug_smpTz+KI2a-aqL zDBvFj_{AQS1d@5x_k8+snBTHLI_z_75_p{!ILprfi(|g441`g^|12M#kW>lXPIIg& z!JGXf(BOlh0Eb@z&nX~`(oMk z)SrK{?vVF9t(j%m6>#C%O_4qkBAA}-xFVhvSlL~zh`{{@Haa92YXm=%MNsmQG_n|;Z58v`rh2;Kbp2THv8?i zoSwf#IZXQ0PZUvI7TC5bY)hRFNCvi#z4msE`&@zQABOQ9yDY(nqeiQPCr{zf=7@_; z-ZARxl^!_s*L6o zN3}8cnZd8l4*b+8{F-Gj2>vzWyH4#3Y<$@@!Fqh1-Mn=Lyk2zs9f+Q5p6)O16tmxf z!NQIBx1Kb!?mH-GcOy9c0N}IzU0ENiv@3EYmRlHoS2^AYu&)(;bjOU zrMsc{-<#{Df{E=tQ1CJTE_89p&i;9)6SDbO$=-dpUG4qJ8eZ^_wqEYU2h3l_@mC}C z?GNy)v$$LAa1okNIzxXg6TAf5n4CvBR(iwyJGkLTLp&`3)k5EjH(u|ifZDHo0!e}-*J{-uG57nB|dkyE> zf)}sod7e;^EW*|WJE~rZub5zf`v_l8OVt(yv_t#Ahes0XL<^E1&?osRj?Kq7P;;8Q zrKd;G@a6-vwueV}I_z2o2D$c{&;Lm8tFHnSaTMfeH2VxzV1o)6ZaNG2c`y5drTqKJ z{rc@;?KCu=kK(=wm-&5y&8=C{xw3&fczfGAUaiD_SG9uRll$XN=7g2XuFDYNI5e0B zR-ea1PW7N-(~E%-T{>+}+no=<;p)8jR*O$ygABgWFc=u7@pzu?zw3EoQkQwE4CLC1 z{hCfG*ywis0^7Vra~^5~3{N=1XK|sgFJTHM&OM7~b^*phjoEw~T-Pwuoh!DS9!Ea; zfWgSefb3`Z;<+C5nC9iTg?zV#G}wP6(=Ke;B6rNrE6pn~r~An_UBu4@BYn`eg9{LF zd(QA=;Jxi@&h5xovkBev2GlrO}rrvFs9B~Bi`dv*0 z{5;Ej_`CSDsh8uo0}$zg1spxCg*8@n?z5jm*gsNt!t?O}zwNBS6Hsqr*G+*V0d1eg zJb3TxP1?)H*Xi^Hz2AE#g5FnUEjQ~IPWKy2u#dS4rNe_a>%sw#VTz}YJ*$uH~B+E;n1`8*UK8!z~PP$>0vd`GfI;$?SOg@P(m(T_{y=laVQXWd*`(X z5C6oQpwjj#t%O<2mS_XJ}ZoU}!hWuJQ%?LE>emFajrpCJNH}doHeOZe>zrN^rEwm4(T6t!x z?by6K;)niYu;?aKAMt**0nM7puV3qcJ6v6yI z{;2ht)B!hZlT}Jnkrv{-z7>VO7VPM9AP4(oXD+dGySe6DSYAR7QPTpyH$1O4dJ2@{ z41DJ>wp6PQ6Gi{|jc4%i@wV#;Ml9J4doBNPCl|7?5%~N0`S@Ck87Zg~q3lv*_sVz` zHz}Xx>UA~Y<*^pNc-rkaBG`jRBiP;H{d9rnDcttBH;ktw8g!Nr7yvju<#oY@=dqll zZDQXZU$`!*0wV%GRR%ujjvz*OVxHHwnU){hKW;!@8kg!0^XgqZ;}c$Byr3pwtZTh& zw0s)B>wb=#A%{8Ubff3u{m>UMFFeX!&*61s3DOVI5f*;zhrT4d+Vwu27Vci3)%8Oa z4W=5my{qiiPT6vPIeXza&^?T+ocllf!OnPHAN=me@v>~3wfyG}w~9n*WHbS|}5&BAf70cR-@0k7E`hi9>R=)lns_mAguo8iFz z3I5;SD_`}-Mu>NQb*^~brMGzB=0Pt4S{~4V2}XVoqpuHjhY4LT<#n6YPIcW6#4V5a zx|AGuyn^A~m^ibZYlDruPvPaajKTph!F&jNx6^A~0QDin8*JbUTLN`>U-7?Q4BiX( zZ+1VoAAW<`w#~HgLcOYZhf|m#_Pj?qy4_Q_y1ThCPg=`=9Sx!!+j3iW&4#JgskiYw zhhcV^!=m@T>&qOEsn>Y0XB$T&FM#R&oxA7U1{bUUimdLwJ@^bxy!@+X^1dSq8d$vv zP{~nSgHd)hBj!;%ywW}g$LUPg6;zOfM?qhPpv@#N6L^_i0x#d`d+MALHrsToW@0T-_h`JJ0Fn}D(IC363Jcw^k1ucLM>v-;J<)UiNSU>M3{ z{O;vIZ|5OJmsapi8Q=n#aso#>(d+N&&^*v{JfY}P=_G3LGa}EOT_=Z<{GNyO;+4#?``|EA_JZ8LZ>?4e7E4}O|;1qFgW{q~T zt@uruAH$Ir>kjEao2Y^JPky`heupNGRAv19QJY?E9v$cB8?I5WKC-n&?2b=MG_TM< zy_@x{|L!J0-dt-m3E~&OI!d~IZy#f_6Y@6vT&`z0423+qh)Zrqw0T+QZt2Zx%ki9q z#Aqnk3m2Ydr>6CL<8X%e7s3gB_k%-q`O|CIuYIwOf$gf5s%fN5@xNH3t%)=W)#dR{ zL}E?5iTUROj+U&hGE@Hj4N_G_B{O=oTV(R~m}BetRERB6d1s{HU(@vM5-xR8p&k(4 z(H}G~@vFgQ20B4E_WkBbIfZMc*i3!i1pk(N*u>+UXwb0BNm^!Fo_IQ*xsOe@4o)g|0pBq=>2A-rx zCn-7SvG=iafns#-hgCOH(5h#DAi!8YwkB?!&F$__q{OoQ%DqBFNr+17F9eLb!B_Ha z&Nmtt4GqH`Mse0tdqEy*ttnj|I;2B13wyp7)zZ0SgZF>jve90Imt)@Rr)u)7e=)C) z7rMJ1fo0tU9G#B6p=w@o*QK3+n-N0(Z2zeXqB)o;G176yXq*yk7dZKxc3PZEty`;w zEi_Y0xZ*APFih(VS5<6=D1nsRW@_MdB%$H`Nl4_3h^|B4m~lKSK=nuE5&Xhc+#W3i^wN4ZgDj?v;r<0Ifis4kTH^zO z6`$SFUC&JSt>e*w=hhQw2A{s|Tm4Q0E`9H!kNl-MP~F^@lqbiBM$4>ge=Vyn@O9ut zi)BocA(+N8umh^t7$@w!#YR9$0}Q^i9UIszql1`I#Q557)IDUKm|Z?)XOtf>gvf%Y z->iUUBMHVnSwJg3?V9@)=?SHsez_8F;(}862GqpzQVbD~C{W#zohH(LtD0;+DQ%Ue zos7~lqA4Sd$s87ecbd`F7^N)Z%b^(kfRn$_N9Gdlz5W6bLhRMGF!`BQAOn916lh8k z4huc2tINV&81Z=1q)iHjkR*|;z)rPk! zzs8VoBAm+bom<$>_!h;cTwL`oF7>T!G-9#Oa4Ih>Fw-KV4!UhJQJO;Z&c>F5P}mLT zCnoE)W~eBiN7zyvt-%Nrtnon`JogLR0TtnlyquNXTPI!=Q5; zshi)X{zOzShc4r!CF_Yh(qz-~AMkCyr5-E-Xi~LZCweJ0Ud)}r&Osv@u8F4bQmY{O zm5r!na08M;rdvb~)eS+k;R@1H*Y8hXHg(OVtrE=W5@R$2L@8(9%b0vER*fVy`O1ty z)vREeGuHL*c(IX;5ym*sV88wsKSQgCI-h0=JbU8H?)>C2 zaVlBhlXzz`RfKsmpOH^FU=x#`{FR^Yw&6|E_%K$rIVJI11)&7*)DGO63S~2YeUh-y zpn|oL^0VX_A@FHfs979Wl?)bUgvBJ^yG=>^?rnqFw<8M;iBW2h5plZt|9Am5lPW?{ zq6O2)tSuQ<@M`k!4;rTxyrdxb3r+A^Nc?Y)5kMxPtlF2z=S@E`-? zv40wH0i7mhKN7U)#eE?WL~ zV3GdCgc{c2zXg7``Wo{E+WRda$K|)`=4Ljf^%@tJ&R;iTO>1AW{&thYj|=3V;(Xb;Sw64YmeS~4Sc%hK#EN|Qbi>%cZ6Yl90NU>8oQijd`lvdk zkY0!`1-{>Un>10}9&0v~$s;4-wB72od<*FoN_^NUar_QzKE)%PNTOKgx6IjaT;|z% zDW~7j_m_?4FC2B~eL+wR&!sHT4qU|dW2>R`YG24RmwNU!-HBX^-G>}~FNikrYv5g+?K#z#X2l&ea!yi-hL0M zp^z^8lJr)9%I0occf@tNvZYnuVtz1<-q28K@F;g1Kh@mE8n4#0Tx_zbWl5k|yq~;f zH*jGtlfEKjyXTXg1lGqgl1^7?Tu(M9C)yPwTu=1(8H~?INl+tePO~F`N>JCPVY9(# zGnE_P+110_DxioY27{<=lT199Ek{+QvgRaA-md&hhkBzD)}J|1uRtbdA8|J*A6B8k zhSRu7_Y6DA5GjdgOc?)3CO@PdGuvQ(`|j-*B~kuGvZ__|(|lj{vB79c+*dN+9beZS z*HDEInk-fN4+&A&@E)F@>zn@4eGzQ@KwOx9XgZqd zwr{QWMi>AGDcCp6^?8CzP<*<`jISRTD2~&A0ry&|eeB#*l=-2JM9|FsVf%QFsIL-a zFTIl;JSU~1-*ioWYa*Y&Tu~~3YI3tivHE?Gvj8;bDbrugW_D-VL721_JjP-Au>Xa4iVh4+283^nnjCQKVl~+%-(He&JV;*R38XoW;!Yst&EFM}JVRpCL?YZS(6xr=Lr~2m z?c9&6T&7FlTzBq}GHDO{SiqfHVU9t_&UYQV)at(*>NxD~Y(a@)WExXQY*OoQzpAjB zHrlOSH;Kv;Axh>w6_-=ObD};63Ub*wpetU&%+&rEfo%f<5m-o(1_7oCx?jHli4>cz zssBWN{$ZaGwSxQU#(!4q-RjP|xdEMGn=8Y>=2TT;*?>?W6Uw8tdt;*xqnPjM3h!5# z@Xh7DJ>MK<7xnF=*aiHLVyY40`w#rL-%;vsXGRwQ%C zDYKPRKVHB1jJ=mB$~u=!0_b)-mP@alfg)!7fFBz_txRi4wB0ub)CeN=;)bM)gv|5&wP%nqCNte*M*NVl>-CT{>QP z&k5n{(7>uOp!u0*B4iXpz=a7nFW((6*hgtZ=9BGT&(cnN6DqGQ32JWlNn1io4=+8D zbNlE&YWmrR|H>H{N4zr_xkHa|0(R&(O7yD>1L(3 zQ~+0DzvjCU(@Q)jW)E%<4*Nr)~aL`_r%|8Eidg5rz}*PcnfmTyMD=&1N$?MmkVV^ZcH$}E ziR<(ak7}LYk|0dH26Ne)kYy`1vd(0a0z0&d|Y$) z%>3GXVRf+mzTq{Tpj3AR{Pwly0}j{PY;C_%QvB_#VX_P)JLK;ML#7WVNAfN=!oPJs zR#$aVKO9L#T{h{JNx7IfjFDpzY$PY_D8gIL9!hfQu75HtAkAeQaw5 zfoTA{djp{Bx1?ccQJX zBOg@#5l$7M;nU zZB@}^f|Z_$?R*w}`P9#yo;0%CJ3Bf$vg{(L7mOD1o_TF!(#uB2B2w-4x6kL6Nx-}s zxMX)icEIm!T$zTMEF(WR)1S%wKq?LJEq-`ZNs_mli^a%aEM@(1STdKD-)Eh%Vi1wT$d+HacZ2#UZ7<7}DnkuM*Fmuz z_s*-%EEL1*tY!jsy9~T?E+0>x*w7QA-8)TdZje^A7JaOBCoVZ4uHumU~GX{El6L`b+UEOy-%_T`G? z_3VG#dx~}}52%)bjw4nnXf1V@;SnU9Vyg1!vRw?2pRm}WppSCuJ~=O@?|)Krp6FP% z2^@}u%MUNXxGw(+mY~osdJbjPuiSCBsMusgN0HeQS&rWmkFw~}WM2~TufMME`UT}y zQUarS()p{!1Mi;9YjylvhwGxjpsx8m%)5)g>JP-=HcJ;Xzm`1&jmhuid6XKUAw|*_ zvhCj(omH#r$iIV8HePJxF>>veR9kC4ZIqysYvii?irUltKFBP0g9a2M1QB(Na`092 zxe?X?3RX+W0Sn0(tI0nhSskq}*g{0v&7HWBC7lc8;7rL%0C7|^vF~|ihhS3xL}Xz} zm)EpRztOT`;cbi7$z^oD9%j987&d!DJos3TikbPrBQ$^1@#*5t;+<0bLvc5ToRYwB zF>=lMQo+1?GgkM90rxgn196IXzO%evv?t=a`Bvb zfyKTJ0tGn9hSB#@L+{aPm!AfmWV!|C(=$cyi5So7LFGUb3zHkkkNV=nv|8?v2?w@$ zBugrX6CsI(sh0QZqfVj?)g0zMfg3;4Ac1vMn)V=>LZS4Rg4Mz0-#%}d#qfMiJ7txL zGis2)CzfvSMMQB&Q$79pK&9Jq{6p1$)1Ffd3Y2{L4WXPk*jB0mH<~A364nZkbPuyy zjp(=JW)|cckZ+2kR(p(4^g$4f76SR!0-nNp9>@$6*U1N3`M$g@W2bpFE{Q?`sY&wV zj!7OB=Gub#p%=T3$FA~5R%RGNYDC? zE1=yFnxpzv`>ROSo^&PHP&rrKev@?!tl20ZQ(Q!Ub;OuWLQ+hnR_nMd=~3f}1ELih zYD-)`$#ebK3`KNgL6Yd$YPPFZFD$Ln{T2zS=z?6-($rMbO+)skpudF%0DR+`r8Vm) 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

UWb1G6S7cx<9hTIsOG}# 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 K4qj(oHw^&L#^cfe diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_energies.py b/openfe/tests/protocols/openmm_abfe/test_abfe_energies.py index b842eaf7a..cfd4678d5 100644 --- a/openfe/tests/protocols/openmm_abfe/test_abfe_energies.py +++ b/openfe/tests/protocols/openmm_abfe/test_abfe_energies.py @@ -20,8 +20,8 @@ from openfe.protocols.openmm_afe import ( AbsoluteBindingComplexUnit, ) -from openfe.protocols.openmm_septop.utils import deserialize from openfe.protocols.openmm_utils.omm_settings import OpenMMSolvationSettings +from openfe.protocols.openmm_utils.serialization import deserialize class AlchemStateRest(AlchemicalState): diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py index b98bc949f..bd7a1f72f 100644 --- a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py @@ -30,6 +30,11 @@ from openfe import setup from openfe.protocols import openmm_rfe from openfe.protocols.openmm_rfe._rfe_utils import topologyhelpers +from openfe.protocols.openmm_rfe.hybridtop_units import ( + HybridTopologyMultiStateAnalysisUnit, + HybridTopologyMultiStateSimulationUnit, + HybridTopologySetupUnit, +) from openfe.protocols.openmm_utils import omm_compute, system_creation from openfe.protocols.openmm_utils.charge_generation import ( HAS_ESPALOMA_CHARGE, @@ -38,6 +43,13 @@ ) +def _get_units(protocol_units, unit_type): + """ + Helper method to extract setup units + """ + return [pu for pu in protocol_units if isinstance(pu, unit_type)] + + @pytest.fixture() def vac_settings(): settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() @@ -163,6 +175,42 @@ def test_serialize_protocol(): assert protocol == ret +def test_repeat_units(benzene_system, toluene_system, benzene_to_toluene_mapping): + settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() + + protocol = openmm_rfe.RelativeHybridTopologyProtocol( + settings=settings, + ) + + dag = protocol.create( + stateA=benzene_system, + stateB=toluene_system, + mapping=benzene_to_toluene_mapping, + ) + + # 9 protocol units, 3 per repeat + pus = list(dag.protocol_units) + assert len(pus) == 9 + + # Aggregate some info for each repeat + setup = _get_units(pus, HybridTopologySetupUnit) + simulation = _get_units(pus, HybridTopologyMultiStateSimulationUnit) + analysis = _get_units(pus, HybridTopologyMultiStateAnalysisUnit) + + # Should be 3 of everything + assert len(setup) == len(simulation) == len(analysis) == 3 + + # Check that the dag chain is correct + for analysis_pu in analysis: + repeat_id = analysis_pu.inputs["repeat_id"] + setup_pu = [s for s in setup if s.inputs["repeat_id"] == repeat_id][0] + sim_pu = [s for s in simulation if s.inputs["repeat_id"] == repeat_id][0] + + assert analysis_pu.inputs["setup_results"] == setup_pu + assert analysis_pu.inputs["simulation_results"] == sim_pu + assert sim_pu.inputs["setup_results"] == setup_pu + + def test_create_independent_repeat_ids(benzene_system, toluene_system, benzene_to_toluene_mapping): # if we create two dags each with 3 repeats, they should give 6 repeat_ids # this allows multiple DAGs in flight for one Transformation that don't clash on gather @@ -183,7 +231,6 @@ def test_create_independent_repeat_ids(benzene_system, toluene_system, benzene_t ) repeat_ids = set() - u: openmm_rfe.RelativeHybridTopologyProtocolUnit for u in dag1.protocol_units: repeat_ids.add(u.inputs["repeat_id"]) for u in dag2.protocol_units: @@ -193,7 +240,7 @@ def test_create_independent_repeat_ids(benzene_system, toluene_system, benzene_t @pytest.mark.parametrize("method", ["repex", "sams", "independent", "InDePeNdENT"]) -def test_dry_run_default_vacuum( +def test_setup_dry_sim_default_vacuum( benzene_vacuum_system, toluene_vacuum_system, benzene_to_toluene_mapping, @@ -214,17 +261,27 @@ def test_dry_run_default_vacuum( stateB=toluene_vacuum_system, mapping=benzene_to_toluene_mapping, ) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] + dag_sim_unit = _get_units(dag.protocol_units, HybridTopologyMultiStateSimulationUnit)[0] with tmpdir.as_cwd(): - debug = dag_unit.run(dry=True)["debug"] - sampler = debug["sampler"] + # Manually run the units + setup_results = dag_setup_unit.run(dry=True) + + sim_results = dag_sim_unit.run( + system=setup_results["hybrid_system"], + positions=setup_results["hybrid_positions"], + selection_indices=setup_results["selection_indices"], + dry=True, + ) + + sampler = sim_results["sampler"] assert isinstance(sampler, MultiStateSampler) assert not sampler.is_periodic assert sampler._thermodynamic_states[0].barostat is None # Check hybrid OMM and MDTtraj Topologies - htf = debug["hybrid_factory"] + htf = setup_results["hybrid_factory"] # 16 atoms: # 11 common atoms, 1 extra hydrogen in benzene, 4 extra in toluene # 12 bonds in benzene + 4 extra toluene bonds @@ -263,9 +320,13 @@ def test_dry_run_default_vacuum( ) -def test_dry_run_gaff_vacuum( +def test_setup_gaff_vacuum( benzene_vacuum_system, toluene_vacuum_system, benzene_to_toluene_mapping, vac_settings, tmpdir ): + """ + Simple dry run of the setup unit to make sure that parameterisation + will work with gaff. + """ vac_settings.forcefield_settings.small_molecule_forcefield = "gaff-2.11" protocol = openmm_rfe.RelativeHybridTopologyProtocol( @@ -278,9 +339,10 @@ def test_dry_run_gaff_vacuum( stateB=toluene_vacuum_system, mapping=benzene_to_toluene_mapping, ) - unit = list(dag.protocol_units)[0] + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] + with tmpdir.as_cwd(): - _ = unit.run(dry=True)["debug"]["sampler"] + _ = dag_setup_unit.run(dry=True) @pytest.mark.slow @@ -292,7 +354,7 @@ def test_dry_many_molecules_solvent( tmpdir, ): """ - A basic test flushing "will it work if you pass multiple molecules" + A basic setup test flushing "will it work if you pass multiple molecules" """ protocol = openmm_rfe.RelativeHybridTopologyProtocol( settings=solv_settings, @@ -304,10 +366,10 @@ def test_dry_many_molecules_solvent( stateB=toluene_many_solv_system, mapping=benzene_to_toluene_mapping, ) - unit = list(dag.protocol_units)[0] + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] with tmpdir.as_cwd(): - sampler = unit.run(dry=True)["debug"]["sampler"] + _ = dag_setup_unit.run(dry=True) BENZ = """\ @@ -376,7 +438,7 @@ def test_dry_many_molecules_solvent( """ -def test_dry_core_element_change(vac_settings, tmpdir): +def test_setup_core_element_change(vac_settings, tmpdir): benz = openfe.SmallMoleculeComponent(Chem.MolFromMolBlock(BENZ, removeHs=False)) pyr = openfe.SmallMoleculeComponent(Chem.MolFromMolBlock(PYRIDINE, removeHs=False)) @@ -392,11 +454,11 @@ def test_dry_core_element_change(vac_settings, tmpdir): mapping=mapping, ) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] with tmpdir.as_cwd(): - sampler = dag_unit.run(dry=True)["debug"]["sampler"] - system = sampler._hybrid_system + results = dag_setup_unit.run(dry=True) + system = results["hybrid_system"] assert system.getNumParticles() == 12 # Average mass between nitrogen and carbon assert system.getParticleMass(1) == 12.0127235 * omm_unit.amu @@ -425,10 +487,21 @@ def test_dry_run_ligand( stateB=toluene_system, mapping=benzene_to_toluene_mapping, ) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] + dag_sim_unit = _get_units(dag.protocol_units, HybridTopologyMultiStateSimulationUnit)[0] with tmpdir.as_cwd(): - sampler = dag_unit.run(dry=True)["debug"]["sampler"] + # Manually run the units + setup_results = dag_setup_unit.run(dry=True) + + sim_results = dag_sim_unit.run( + system=setup_results["hybrid_system"], + positions=setup_results["hybrid_positions"], + selection_indices=setup_results["selection_indices"], + dry=True, + ) + + sampler = sim_results["sampler"] assert isinstance(sampler, MultiStateSampler) assert sampler.is_periodic assert isinstance(sampler._thermodynamic_states[0].barostat, MonteCarloBarostat) @@ -489,18 +562,18 @@ def tip4p_hybrid_factory( stateB=toluene_system, mapping=benzene_to_toluene_mapping, ) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = [pu for pu in dag.protocol_units if isinstance(pu, HybridTopologySetupUnit)][0] shared_temp = tmp_path_factory.mktemp("tip4p_shared") scratch_temp = tmp_path_factory.mktemp("tip4p_scratch") - dag_unit_result = dag_unit.run( + dag_unit_setup_result = dag_setup_unit.run( dry=True, scratch_basepath=scratch_temp, shared_basepath=shared_temp, ) - return dag_unit_result["debug"]["hybrid_factory"] + return dag_unit_setup_result["hybrid_factory"] def test_tip4p_particle_count(tip4p_hybrid_factory): @@ -585,7 +658,7 @@ def test_tip4p_check_vsite_parameters(tip4p_hybrid_factory): 0.9 * unit.nanometer, ], ) -def test_dry_run_ligand_system_cutoff( +def test_setup_ligand_system_cutoff( cutoff, benzene_system, toluene_system, benzene_to_toluene_mapping, solv_settings, tmpdir ): """ @@ -597,16 +670,17 @@ def test_dry_run_ligand_system_cutoff( protocol = openmm_rfe.RelativeHybridTopologyProtocol( settings=solv_settings, ) + dag = protocol.create( stateA=benzene_system, stateB=toluene_system, mapping=benzene_to_toluene_mapping, ) - dag_unit = list(dag.protocol_units)[0] + + dag_setup_unit = [pu for pu in dag.protocol_units if isinstance(pu, HybridTopologySetupUnit)][0] with tmpdir.as_cwd(): - sampler = dag_unit.run(dry=True)["debug"]["sampler"] - hs = sampler._hybrid_system + hs = dag_setup_unit.run(dry=True)["hybrid_system"] nbfs = [ f @@ -646,7 +720,7 @@ def test_dry_run_ligand_system_cutoff( ), ], ) -def test_dry_run_charge_backends( +def test_setup_charge_backends( CN_molecule, tmpdir, method, backend, ref_key, vac_settings, am1bcc_ref_charges ): vac_settings.partial_charge_settings.partial_charge_method = method @@ -670,13 +744,12 @@ def test_dry_run_charge_backends( dag = protocol.create(stateA=systemA, stateB=systemB, mapping=mapping) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = [pu for pu in dag.protocol_units if isinstance(pu, HybridTopologySetupUnit)][0] with tmpdir.as_cwd(): - debug = dag_unit.run(dry=True)["debug"] - sampler = debug["sampler"] - htf = debug["hybrid_factory"] - hybrid_system = sampler._hybrid_system + results = dag_setup_unit.run(dry=True) + htf = results["hybrid_factory"] + hybrid_system = results["hybrid_system"] # get the standard nonbonded force nonbond = [f for f in hybrid_system.getForces() if isinstance(f, NonbondedForce)] @@ -710,7 +783,7 @@ def test_dry_run_charge_backends( np.testing.assert_allclose(c, ref, rtol=1e-4) -def test_dry_run_same_mol_different_charges(benzene_modifications, vac_settings, tmpdir): +def test_setup_same_mol_different_charges(benzene_modifications, vac_settings, tmpdir): """ Issue #1120 - make sure we can do an RFE of a system with different parameters but the same molecule. @@ -741,13 +814,12 @@ def test_dry_run_same_mol_different_charges(benzene_modifications, vac_settings, stateB=openfe.ChemicalSystem({"l": stateB_mol}), mapping=mapping, ) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = [pu for pu in dag.protocol_units if isinstance(pu, HybridTopologySetupUnit)][0] with tmpdir.as_cwd(): - debug = dag_unit.run(dry=True)["debug"] - sampler = debug["sampler"] - htf = debug["hybrid_factory"] - hybrid_system = sampler._hybrid_system + results = dag_setup_unit.run(dry=True) + htf = results["hybrid_factory"] + hybrid_system = results["hybrid_system"] # get the standard nonbonded force nonbond = [f for f in hybrid_system.getForces() if isinstance(f, NonbondedForce)] @@ -776,7 +848,7 @@ def test_dry_run_same_mol_different_charges(benzene_modifications, vac_settings, @pytest.mark.flaky(reruns=3) # bad minimisation can happen -def test_dry_run_user_charges(benzene_modifications, vac_settings, tmpdir): +def test_setup_user_charges(benzene_modifications, vac_settings, tmpdir): """ Create a hybrid system with a set of fictitious user supplied charges and ensure that they are properly passed through to the constructed @@ -830,13 +902,12 @@ def check_propchgs(smc, charge_array): stateB=openfe.ChemicalSystem({"l": toluene_smc}), mapping=mapping, ) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = [pu for pu in dag.protocol_units if isinstance(pu, HybridTopologySetupUnit)][0] with tmpdir.as_cwd(): - debug = dag_unit.run(dry=True)["debug"] - sampler = debug["sampler"] - htf = debug["hybrid_factory"] - hybrid_system = sampler._hybrid_system + results = dag_setup_unit.run(dry=True) + htf = results["hybrid_factory"] + hybrid_system = results["hybrid_system"] # get the standard nonbonded force nonbond = [f for f in hybrid_system.getForces() if isinstance(f, NonbondedForce)] @@ -924,15 +995,23 @@ def test_virtual_sites_no_reassign( stateB=toluene_system, mapping=benzene_to_toluene_mapping, ) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] + dag_sim_unit = _get_units(dag.protocol_units, HybridTopologyMultiStateSimulationUnit)[0] with tmpdir.as_cwd(): + # Manually run the units + setup_results = dag_setup_unit.run(dry=True) errmsg = "Simulations with virtual sites without velocity" with pytest.raises(ValueError, match=errmsg): - dag_unit.run(dry=True) + sim_results = dag_sim_unit.run( + system=setup_results["hybrid_system"], + positions=setup_results["hybrid_positions"], + selection_indices=setup_results["selection_indices"], + dry=True, + ) -def test_dodecahdron_ligand_box( +def test_setup_dodecahdron_ligand_box( benzene_system, toluene_system, benzene_to_toluene_mapping, solv_settings, tmpdir ): """ @@ -947,11 +1026,10 @@ def test_dodecahdron_ligand_box( stateB=toluene_system, mapping=benzene_to_toluene_mapping, ) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] with tmpdir.as_cwd(): - sampler = dag_unit.run(dry=True)["debug"]["sampler"] - hs = sampler._hybrid_system + hs = dag_setup_unit.run(dry=True)["hybrid_system"] vectors = hs.getDefaultPeriodicBoxVectors() @@ -989,10 +1067,18 @@ def test_dry_run_complex( stateB=toluene_complex_system, mapping=benzene_to_toluene_mapping, ) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] + dag_sim_unit = _get_units(dag.protocol_units, HybridTopologyMultiStateSimulationUnit)[0] with tmpdir.as_cwd(): - sampler = dag_unit.run(dry=True)["debug"]["sampler"] + setup_results = dag_setup_unit.run(dry=True) + sim_results = dag_sim_unit.run( + system=setup_results["hybrid_system"], + positions=setup_results["hybrid_positions"], + selection_indices=setup_results["selection_indices"], + dry=True, + ) + sampler = sim_results["sampler"] assert isinstance(sampler, MultiStateSampler) assert sampler.is_periodic assert isinstance(sampler._thermodynamic_states[0].barostat, MonteCarloBarostat) @@ -1016,7 +1102,7 @@ def test_lambda_schedule(windows): assert len(lambdas.lambda_schedule) == windows -def test_ligand_overlap_warning( +def test_setup_ligand_overlap_warning( benzene_vacuum_system, toluene_vacuum_system, benzene_to_toluene_mapping, vac_settings, tmpdir ): protocol = openmm_rfe.RelativeHybridTopologyProtocol( @@ -1048,9 +1134,11 @@ def test_ligand_overlap_warning( stateB=toluene_vacuum_system, mapping=mapping, ) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = [ + pu for pu in dag.protocol_units if isinstance(pu, HybridTopologySetupUnit) + ][0] with tmpdir.as_cwd(): - dag_unit.run(dry=True) + dag_setup_unit.run(dry=True) @pytest.fixture @@ -1069,28 +1157,109 @@ def solvent_protocol_dag(benzene_system, toluene_system, benzene_to_toluene_mapp def test_unit_tagging(solvent_protocol_dag, tmpdir): # test that executing the Units includes correct generation and repeat info dag_units = solvent_protocol_dag.protocol_units - with mock.patch( - "openfe.protocols.openmm_rfe.equil_rfe_methods.RelativeHybridTopologyProtocolUnit.run", - return_value={"nc": "file.nc", "last_checkpoint": "chk.nc"}, + with ( + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.HybridTopologySetupUnit.run", + return_value={ + "system": Path("system.xml.bz2"), + "positions": Path("positions.npy"), + "pdb_structure": Path("hybrid_system.pdb"), + "selection_indices": np.zeros(100), + }, + ), + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.np.load", + return_value=np.zeros(100), + ), + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.deserialize", + return_value={ + "item": "foo", + }, + ), + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.HybridTopologyMultiStateSimulationUnit.run", + return_value={ + "nc": Path("file.nc"), + "checkpoint": Path("chk.chk"), + }, + ), + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.HybridTopologyMultiStateAnalysisUnit.run", + return_value={ + "foo": "bar", + }, + ), ): - results = [] - for u in dag_units: - ret = u.execute(context=gufe.Context(tmpdir, tmpdir)) - results.append(ret) - repeats = set() - for ret in results: - assert isinstance(ret, gufe.ProtocolUnitResult) - assert ret.outputs["generation"] == 0 - repeats.add(ret.outputs["repeat_id"]) + setup_results = {} + sim_results = {} + analysis_results = {} + + setup_units = _get_units(dag_units, HybridTopologySetupUnit) + sim_units = _get_units(dag_units, HybridTopologyMultiStateSimulationUnit) + analysis_units = _get_units(dag_units, HybridTopologyMultiStateAnalysisUnit) + + for u in setup_units: + rid = u.inputs["repeat_id"] + setup_results[rid] = u.execute(context=gufe.Context(tmpdir, tmpdir)) + + for u in sim_units: + rid = u.inputs["repeat_id"] + sim_results[rid] = u.execute( + context=gufe.Context(tmpdir, tmpdir), setup_results=setup_results[rid] + ) + + for u in analysis_units: + rid = u.inputs["repeat_id"] + analysis_results[rid] = u.execute( + context=gufe.Context(tmpdir, tmpdir), + setup_results=setup_results[rid], + simulation_results=sim_results[rid], + ) + for results in [setup_results, sim_results, analysis_results]: + for ret in results.values(): + assert isinstance(ret, gufe.ProtocolUnitResult) + assert ret.outputs["generation"] == 0 + # repeats are random ints, so check we got 3 individual numbers - assert len(repeats) == 3 + assert len(setup_results) == len(sim_results) == len(analysis_results) == 3 def test_gather(solvent_protocol_dag, tmpdir): # check .gather behaves as expected - with mock.patch( - "openfe.protocols.openmm_rfe.equil_rfe_methods.RelativeHybridTopologyProtocolUnit.run", - return_value={"nc": "file.nc", "last_checkpoint": "chk.nc"}, + with ( + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.HybridTopologySetupUnit.run", + return_value={ + "system": Path("system.xml.bz2"), + "positions": Path("positions.npy"), + "pdb_structure": Path("hybrid_system.pdb"), + "selection_indices": np.zeros(100), + }, + ), + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.np.load", + return_value=np.zeros(100), + ), + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.deserialize", + return_value={ + "item": "foo", + }, + ), + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.HybridTopologyMultiStateSimulationUnit.run", + return_value={ + "nc": Path("file.nc"), + "checkpoint": Path("chk.chk"), + }, + ), + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.HybridTopologyMultiStateAnalysisUnit.run", + return_value={ + "foo": "bar", + }, + ), ): dagres = gufe.protocols.execute_DAG( solvent_protocol_dag, @@ -1401,13 +1570,13 @@ def tyk2_xml(tmp_path_factory): stateB=openfe.ChemicalSystem({"ligand": lig55}), mapping=mapping, ) - pu = list(dag.protocol_units)[0] + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] tmp = tmp_path_factory.mktemp("xml_reg") - dryrun = pu.run(dry=True, shared_basepath=tmp) + setup_results = dag_setup_unit.run(dry=True, shared_basepath=tmp) - system = dryrun["debug"]["sampler"]._hybrid_system + system = setup_results["hybrid_system"] return ET.fromstring(XmlSerializer.serialize(system)) @@ -1897,11 +2066,12 @@ def test_dry_run_alchemwater_solvent(benzene_to_benzoic_mapping, solv_settings, stateB=stateB_system, mapping=benzene_to_benzoic_mapping, ) - unit = list(dag.protocol_units)[0] + + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] with tmpdir.as_cwd(): - debug = unit.run(dry=True)["debug"] - htf = debug["hybrid_factory"] + results = dag_setup_unit.run(dry=True) + htf = results["hybrid_factory"] _assert_total_charge(htf.hybrid_system, htf._atom_classes, 0, 0) assert len(htf._atom_classes["core_atoms"]) == 14 @@ -1922,7 +2092,7 @@ def test_dry_run_alchemwater_solvent(benzene_to_benzoic_mapping, solv_settings, ["benzoic_to_benzene_mapping", 0, 1, False, 11, 1, 3], ], ) -def test_dry_run_complex_alchemwater_totcharge( +def test_setup_complex_alchemwater_totcharge( mapping_name, chgA, chgB, @@ -1966,11 +2136,11 @@ def test_dry_run_complex_alchemwater_totcharge( stateB=stateB_system, mapping=mapping, ) - unit = list(dag.protocol_units)[0] + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] with tmpdir.as_cwd(): - debug = unit.run(dry=True)["debug"] - htf = debug["hybrid_factory"] + setup_results = dag_setup_unit.run(dry=True) + htf = setup_results["hybrid_factory"] _assert_total_charge(htf.hybrid_system, htf._atom_classes, chgA, chgB) assert len(htf._atom_classes["core_atoms"]) == core_atoms @@ -1980,8 +2150,11 @@ def test_dry_run_complex_alchemwater_totcharge( def test_structural_analysis_error(tmpdir): with tmpdir.as_cwd(): - ret = openmm_rfe.RelativeHybridTopologyProtocolUnit.structural_analysis( - Path("."), Path("."), "foo", "bar" + ret = openmm_rfe.hybridtop_units.HybridTopologyMultiStateAnalysisUnit._structural_analysis( + Path("."), + Path("."), + Path("."), + True, ) assert "structural_analysis_error" in ret @@ -2021,10 +2194,21 @@ def test_dry_run_vacuum_write_frequency( stateB=toluene_vacuum_system, mapping=benzene_to_toluene_mapping, ) - dag_unit = list(dag.protocol_units)[0] + dag_setup_unit = _get_units(dag.protocol_units, HybridTopologySetupUnit)[0] + dag_sim_unit = _get_units(dag.protocol_units, HybridTopologyMultiStateSimulationUnit)[0] with tmpdir.as_cwd(): - sampler = dag_unit.run(dry=True)["debug"]["sampler"] + # Manually run the units + setup_results = dag_setup_unit.run(dry=True) + + sim_results = dag_sim_unit.run( + system=setup_results["hybrid_system"], + positions=setup_results["hybrid_positions"], + selection_indices=setup_results["selection_indices"], + dry=True, + ) + + sampler = sim_results["sampler"] reporter = sampler._reporter if positions_write_frequency: assert reporter.position_interval == positions_write_frequency.m diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py index 0cd31b08c..555bd10e2 100644 --- a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py @@ -62,24 +62,37 @@ def test_openmm_run_engine( r = execute_DAG(dag, shared_basedir=cwd, scratch_basedir=cwd, keep_shared=True) assert r.ok() + + # Get the path to the simulation unit shared + for pur in r.protocol_unit_results: + if "Simulation" in pur.name: + sim_shared = tmpdir / f"shared_{pur.source_key}_attempt_0" + assert sim_shared.exists() + assert pathlib.Path(sim_shared).is_dir() + for pur in r.protocol_unit_results: - unit_shared = tmpdir / f"shared_{pur.source_key}_attempt_0" - assert unit_shared.exists() - assert pathlib.Path(unit_shared).is_dir() + if "Analysis" not in pur.name: + continue + + analysis_shared = tmpdir / f"shared_{pur.source_key}_attempt_0" + assert analysis_shared.exists() + assert pathlib.Path(analysis_shared).is_dir() # Check the checkpoint file exists - checkpoint = pur.outputs["last_checkpoint"] - assert checkpoint == "checkpoint.chk" - assert (unit_shared / checkpoint).exists() + checkpoint = pur.outputs["checkpoint"] + assert checkpoint.name == "checkpoint.chk" + assert checkpoint == sim_shared / "checkpoint.chk" + assert checkpoint.exists() # Check the nc simulation file exists # TODO: assert the number of frames - nc = pur.outputs["nc"] - assert nc == unit_shared / "simulation.nc" + nc = pur.outputs["trajectory"] + assert nc.name == "simulation.nc" + assert nc == sim_shared / "simulation.nc" assert nc.exists() # Check structural analysis contents - structural_analysis_file = unit_shared / "structural_analysis.npz" + structural_analysis_file = analysis_shared / "structural_analysis.npz" assert (structural_analysis_file).exists() assert pur.outputs["structural_analysis"] == structural_analysis_file diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_tokenization.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_tokenization.py index 322c0f950..404b03c06 100644 --- a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_tokenization.py +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_tokenization.py @@ -6,6 +6,11 @@ from openff.units import unit from openfe.protocols import openmm_rfe +from openfe.protocols.openmm_rfe.hybridtop_units import ( + HybridTopologyMultiStateAnalysisUnit, + HybridTopologyMultiStateSimulationUnit, + HybridTopologySetupUnit, +) """ todo: @@ -23,7 +28,7 @@ def rfe_protocol(): @pytest.fixture -def rfe_protocol_other_units(): +def rfe_protocol_other_input_units(): """Identical to rfe_protocol, but with `kcal / mol` as input unit instead of `kilocalorie_per_mole`.""" new_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() new_settings.simulation_settings.early_termination_target_error = 0.0 * unit.kilocalorie/unit.mol # fmt: skip @@ -31,13 +36,34 @@ def rfe_protocol_other_units(): @pytest.fixture -def protocol_unit(rfe_protocol, benzene_system, toluene_system, benzene_to_toluene_mapping): +def protocol_units(rfe_protocol, benzene_system, toluene_system, benzene_to_toluene_mapping): pus = rfe_protocol.create( stateA=benzene_system, stateB=toluene_system, mapping=[benzene_to_toluene_mapping], ) - return list(pus.protocol_units)[0] + return list(pus.protocol_units) + + +@pytest.fixture +def protocol_setup_unit(protocol_units): + for pu in protocol_units: + if isinstance(pu, HybridTopologySetupUnit): + return pu + + +@pytest.fixture +def protocol_simulation_unit(protocol_units): + for pu in protocol_units: + if isinstance(pu, HybridTopologyMultiStateSimulationUnit): + return pu + + +@pytest.fixture +def protocol_analysis_unit(protocol_units): + for pu in protocol_units: + if isinstance(pu, HybridTopologyMultiStateAnalysisUnit): + return pu @pytest.mark.skip @@ -51,14 +77,14 @@ def instance(self): pass -class TestRelativeHybridTopologyProtocolOtherUnits(GufeTokenizableTestsMixin): +class TestRelativeHybridTopologyProtocolOtherInputUnits(GufeTokenizableTestsMixin): cls = openmm_rfe.RelativeHybridTopologyProtocol key = None repr = "" -def test_serialize_gz(tmpdir): - filename = pathlib.Path(tmpdir / "file.xml.gz") - expected = "" - - with mock.patch("openmm.XmlSerializer.serialize", return_value=expected): - serialize(object(), filename) - - with gzip.open(filename, "rb") as f: - read_back = f.read().decode() - assert read_back == expected - - def test_serialize_bz2(tmpdir): filename = pathlib.Path(tmpdir / "file.xml.bz2") expected = "" @@ -61,19 +49,6 @@ def test_deserialize_xml(tmpdir): assert result == "DESERIALIZED" -def test_deserialize_gz(tmpdir): - filename = pathlib.Path(tmpdir / "file.xml.gz") - expected_serialized = "gz" - with gzip.open(filename, "wb") as f: - f.write(expected_serialized.encode()) - - with mock.patch("openmm.XmlSerializer.deserialize", return_value="FROM_GZ") as deser: - result = deserialize(filename) - - deser.assert_called_once_with(expected_serialized) - assert result == "FROM_GZ" - - def test_deserialize_bz2(tmpdir): filename = pathlib.Path(tmpdir / "file.xml.bz2") expected_serialized = "bz2" diff --git a/openfecli/commands/gather.py b/openfecli/commands/gather.py index a99e4ee8e..cbb2b93f6 100644 --- a/openfecli/commands/gather.py +++ b/openfecli/commands/gather.py @@ -220,9 +220,16 @@ def _get_names(result: dict) -> tuple[str, str]: # TODO: I don't like this [0][0] indexing, but I can't think of a better way currently protocol_data = list(result["protocol_result"]["data"].values())[0][0] - - name_A = protocol_data["inputs"]["ligandmapping"]["componentA"]["molprops"]["ofe-name"] - name_B = protocol_data["inputs"]["ligandmapping"]["componentB"]["molprops"]["ofe-name"] + try: + name_A = protocol_data["inputs"]["setup_results"]["inputs"]["ligandmapping"]["componentA"][ + "molprops" + ]["ofe-name"] + name_B = protocol_data["inputs"]["setup_results"]["inputs"]["ligandmapping"]["componentB"][ + "molprops" + ]["ofe-name"] + except KeyError: + name_A = protocol_data["inputs"]["ligandmapping"]["componentA"]["molprops"]["ofe-name"] + name_B = protocol_data["inputs"]["ligandmapping"]["componentB"]["molprops"]["ofe-name"] return str(name_A), str(name_B) @@ -231,9 +238,17 @@ def _get_type(result: dict) -> Literal["vacuum", "solvent", "complex"]: """Determine the simulation type based on the component types.""" protocol_data = list(result["protocol_result"]["data"].values())[0][0] - component_types = [ - x["__module__"] for x in protocol_data["inputs"]["stateA"]["components"].values() - ] + try: + component_types = [ + x["__module__"] + for x in protocol_data["inputs"]["setup_results"]["inputs"]["stateA"][ + "components" + ].values() + ] + except KeyError: + component_types = [ + x["__module__"] for x in protocol_data["inputs"]["stateA"]["components"].values() + ] if "gufe.components.solventcomponent" not in component_types: return "vacuum" elif "gufe.components.proteincomponent" in component_types: diff --git a/openfecli/tests/test_rbfe_tutorial.py b/openfecli/tests/test_rbfe_tutorial.py index aac32d421..1ace81cd2 100644 --- a/openfecli/tests/test_rbfe_tutorial.py +++ b/openfecli/tests/test_rbfe_tutorial.py @@ -8,8 +8,10 @@ import os from importlib import resources from os import path +from pathlib import Path from unittest import mock +import numpy as np import pytest from click.testing import CliRunner from openff.units import unit @@ -89,25 +91,6 @@ def test_plan_tyk2(tyk2_ligands, tyk2_protein, expected_transformations): assert "n_protocol_repeats=3" in result.output -@pytest.fixture -def mock_execute(expected_transformations): - def fake_execute(*args, **kwargs): - return { - "repeat_id": kwargs["repeat_id"], - "generation": kwargs["generation"], - "nc": "file.nc", - "last_checkpoint": "checkpoint.nc", - "unit_estimate": 4.2 * unit.kilocalories_per_mole, - } - - with mock.patch( - "openfe.protocols.openmm_rfe.equil_rfe_methods.RelativeHybridTopologyProtocolUnit._execute" - ) as m: - m.side_effect = fake_execute - - yield m - - @pytest.fixture def ref_gather(): return """\ @@ -125,10 +108,67 @@ def ref_gather(): """ -def test_run_tyk2(tyk2_ligands, tyk2_protein, expected_transformations, mock_execute, ref_gather): +@pytest.fixture +def fake_setup_execute_results(): + """Use for mocking the expensive _execute step and instead directly return plausible results.""" + + def _fake_execute_results(*args, **kwargs): + return { + "repeat_id": kwargs["repeat_id"], + "generation": kwargs["generation"], + "system": Path("system.xml.bz2"), + "positions": Path("positions.npy"), + "pdb_structure": Path("hybrid_system.pdb"), + "selection_indices": np.arange(50), + } + + return _fake_execute_results + + +@pytest.fixture +def fake_sim_execute_results(): + """Use for mocking the expensive _execute step and instead directly return plausible results.""" + + def _fake_execute_results(*args, **kwargs): + return { + "repeat_id": kwargs["repeat_id"], + "generation": kwargs["generation"], + "nc": Path("file.nc"), + "checkpoint": Path("chk.chk"), + } + + return _fake_execute_results + + +@pytest.fixture +def fake_analysis_execute_results(): + """Use for mocking the expensive _execute step and instead directly return plausible results.""" + + def _fake_execute_results(*args, **kwargs): + return { + "repeat_id": kwargs["repeat_id"], + "generation": kwargs["generation"], + "pdb_structure": Path("hybrid_system.pdb"), + "checkpoint": Path("chk.chk"), + "selection_indices": np.arange(50), + "unit_estimate": 4.2 * unit.kilocalories_per_mole, + } + + return _fake_execute_results + + +def test_run_tyk2( + tyk2_ligands, + tyk2_protein, + expected_transformations, + fake_setup_execute_results, + fake_sim_execute_results, + fake_analysis_execute_results, + ref_gather, +): runner = CliRunner() with runner.isolated_filesystem(): - result = runner.invoke( + result_setup = runner.invoke( plan_rbfe_network, [ "-M", tyk2_ligands, @@ -136,14 +176,27 @@ def test_run_tyk2(tyk2_ligands, tyk2_protein, expected_transformations, mock_exe ], ) # fmt: skip - assert_click_success(result) - - for f in expected_transformations: - fn = path.join("alchemicalNetwork/transformations", f) - result2 = runner.invoke(quickrun, [fn]) - assert_click_success(result2) - - gather_result = runner.invoke(gather, ["--report", "ddg", ".", "--tsv"]) - - assert_click_success(gather_result) - assert gather_result.stdout == ref_gather + assert_click_success(result_setup) + with ( + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.HybridTopologySetupUnit._execute", + side_effect=fake_setup_execute_results, + ), + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.HybridTopologyMultiStateSimulationUnit._execute", + side_effect=fake_sim_execute_results, + ), + mock.patch( + "openfe.protocols.openmm_rfe.hybridtop_units.HybridTopologyMultiStateAnalysisUnit._execute", + side_effect=fake_analysis_execute_results, + ), + ): + for f in expected_transformations: + fn = path.join("alchemicalNetwork/transformations", f) + result_run = runner.invoke(quickrun, [fn]) + assert_click_success(result_run) + + result_gather = runner.invoke(gather, ["--report", "ddg", ".", "--tsv"]) + + assert_click_success(result_gather) + assert result_gather.stdout == ref_gather From 2d9e77d9e18c5534d17efe559a3434b1eb270d95 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Fri, 16 Jan 2026 08:00:27 -0800 Subject: [PATCH 32/47] update docs for multiple protocol units (#1793) --- docs/reference/api/openmm_rfe.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/reference/api/openmm_rfe.rst b/docs/reference/api/openmm_rfe.rst index 8e03d1d36..18a3a0322 100644 --- a/docs/reference/api/openmm_rfe.rst +++ b/docs/reference/api/openmm_rfe.rst @@ -16,7 +16,9 @@ Protocol API specification :toctree: generated/ RelativeHybridTopologyProtocol - RelativeHybridTopologyProtocolUnit + HybridTopologySetupUnit + HybridTopologyMultiStateSimulationUnit + HybridTopologyMultiStateAnalysisUnit RelativeHybridTopologyProtocolResult Protocol Settings From ebaefb09f66adf47aa33b8e1752af14d32a79d54 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Fri, 16 Jan 2026 16:12:55 +0000 Subject: [PATCH 33/47] Update docstring for hybridtop classes (#1794) * Update docstring for classes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../openmm_rfe/hybridtop_protocol_results.py | 4 +++- openfe/protocols/openmm_rfe/hybridtop_units.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py b/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py index ca865f8e0..c4da8e2a7 100644 --- a/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py +++ b/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py @@ -20,7 +20,9 @@ class RelativeHybridTopologyProtocolResult(gufe.ProtocolResult): - """Dict-like container for the output of a RelativeHybridTopologyProtocol""" + """ + Protocol results with the output of a RelativeHybridTopologyProtocol. + """ def __init__(self, **data): super().__init__(**data) diff --git a/openfe/protocols/openmm_rfe/hybridtop_units.py b/openfe/protocols/openmm_rfe/hybridtop_units.py index 588ce3151..c7afbd9c4 100644 --- a/openfe/protocols/openmm_rfe/hybridtop_units.py +++ b/openfe/protocols/openmm_rfe/hybridtop_units.py @@ -146,7 +146,7 @@ def _get_settings( class HybridTopologySetupUnit(gufe.ProtocolUnit, HybridTopologyUnitMixin): """ - Calculates the relative free energy of an alchemical ligand transformation. + Setup unit for Hybrid Topology Protocol transformations. """ @staticmethod @@ -810,6 +810,11 @@ def _execute( class HybridTopologyMultiStateSimulationUnit(gufe.ProtocolUnit, HybridTopologyUnitMixin): + """ + Multi-state simulation (e.g. multi replica methods like hamiltonian + replica exchange) unit for Hybrid Topology Protocol transformations. + """ + @staticmethod def _get_integrator( integrator_settings: IntegratorSettings, @@ -1284,6 +1289,10 @@ def _execute( class HybridTopologyMultiStateAnalysisUnit(gufe.ProtocolUnit, HybridTopologyUnitMixin): + """ + Analysis unit for multi-state Hybrid Topology Protocol transformations. + """ + @staticmethod def _analyze_multistate_energies( trajectory: pathlib.Path, From cbc118d24519b3bf3463654554526b9840b3cd3a Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Tue, 20 Jan 2026 15:17:15 +0000 Subject: [PATCH 34/47] Small improvement in RelativeHybridTopologyProtocol docstring (#1797) Updated class docstring to clarify the use of Hybrid Topology scheme. --- openfe/protocols/openmm_rfe/hybridtop_protocols.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openfe/protocols/openmm_rfe/hybridtop_protocols.py b/openfe/protocols/openmm_rfe/hybridtop_protocols.py index 984cc2f99..12b6b168d 100644 --- a/openfe/protocols/openmm_rfe/hybridtop_protocols.py +++ b/openfe/protocols/openmm_rfe/hybridtop_protocols.py @@ -82,7 +82,8 @@ class RelativeHybridTopologyProtocol(gufe.Protocol): """ - Relative Free Energy calculations using OpenMM and OpenMMTools. + Relative Free Energy calculations using a Hybrid Topology scheme + using OpenMM and OpenMMTools. Based on `Perses `_ From e2edb74301696c0f3f397b9f242e64253bb414ee Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:09:27 -0800 Subject: [PATCH 35/47] turn off zenodo retry (#1802) * turn off zenodo retry * remove retry_if_failed arg --- openfe/tests/protocols/restraints/test_geometry_boresch.py | 1 - .../tests/protocols/restraints/test_geometry_boresch_host.py | 1 - openfe/tests/protocols/restraints/test_geometry_utils.py | 1 - openfe/tests/protocols/restraints/test_omm_restraints.py | 1 - openfe/tests/protocols/test_openmmutils.py | 1 - openfecli/tests/commands/test_gather.py | 3 --- 6 files changed, 8 deletions(-) diff --git a/openfe/tests/protocols/restraints/test_geometry_boresch.py b/openfe/tests/protocols/restraints/test_geometry_boresch.py index d08749bc6..b533db1ac 100644 --- a/openfe/tests/protocols/restraints/test_geometry_boresch.py +++ b/openfe/tests/protocols/restraints/test_geometry_boresch.py @@ -242,7 +242,6 @@ def test_get_boresch_restraint_dssp(eg5_protein_ligand_universe, eg5_ligands): registry={ "industry_benchmark_systems.zip": "sha256:2bb5eee36e29b718b96bf6e9350e0b9957a592f6c289f77330cbb6f4311a07bd" }, - retry_if_failed=5, ) diff --git a/openfe/tests/protocols/restraints/test_geometry_boresch_host.py b/openfe/tests/protocols/restraints/test_geometry_boresch_host.py index 1eefd0c5f..78815a5e9 100644 --- a/openfe/tests/protocols/restraints/test_geometry_boresch_host.py +++ b/openfe/tests/protocols/restraints/test_geometry_boresch_host.py @@ -34,7 +34,6 @@ registry={ "t4_lysozyme_trajectory.zip": "sha256:e985d055db25b5468491e169948f641833a5fbb67a23dbb0a00b57fb7c0e59c8" }, - retry_if_failed=5, ) diff --git a/openfe/tests/protocols/restraints/test_geometry_utils.py b/openfe/tests/protocols/restraints/test_geometry_utils.py index 172de80aa..c7b3156d4 100644 --- a/openfe/tests/protocols/restraints/test_geometry_utils.py +++ b/openfe/tests/protocols/restraints/test_geometry_utils.py @@ -56,7 +56,6 @@ def eg5_protein_ligand_universe(eg5_protein_pdb, eg5_ligands): registry={ "t4_lysozyme_trajectory.zip": "sha256:e985d055db25b5468491e169948f641833a5fbb67a23dbb0a00b57fb7c0e59c8" }, - retry_if_failed=5, ) diff --git a/openfe/tests/protocols/restraints/test_omm_restraints.py b/openfe/tests/protocols/restraints/test_omm_restraints.py index f06940afa..55b75f5ad 100644 --- a/openfe/tests/protocols/restraints/test_omm_restraints.py +++ b/openfe/tests/protocols/restraints/test_omm_restraints.py @@ -105,7 +105,6 @@ def test_verify_geometry(): registry={ "industry_benchmark_systems.zip": "sha256:2bb5eee36e29b718b96bf6e9350e0b9957a592f6c289f77330cbb6f4311a07bd" }, - retry_if_failed=5, ) diff --git a/openfe/tests/protocols/test_openmmutils.py b/openfe/tests/protocols/test_openmmutils.py index 9511f2bcd..8251970e2 100644 --- a/openfe/tests/protocols/test_openmmutils.py +++ b/openfe/tests/protocols/test_openmmutils.py @@ -1044,7 +1044,6 @@ def test_openeye_import_error(self, monkeypatch, uncharged_mol): "simulation.nc": "md5:bc4e842b47de17704d804ae345b91599", "simulation_real_time_analysis.yaml": "md5:68a7d81462c42353a91bbbe5e64fd418", }, - retry_if_failed=5, ) diff --git a/openfecli/tests/commands/test_gather.py b/openfecli/tests/commands/test_gather.py index 9ea27b59b..1cae95aa8 100644 --- a/openfecli/tests/commands/test_gather.py +++ b/openfecli/tests/commands/test_gather.py @@ -29,13 +29,11 @@ "rbfe_results_serial_repeats.tar.gz": "md5:2355ecc80e03242a4c7fcbf20cb45487", "rbfe_results_parallel_repeats.tar.gz": "md5:ff7313e14eb6f2940c6ffd50f2192181", }, - retry_if_failed=5, ) ZENODO_CMET_DATA = pooch.create( path=POOCH_CACHE, base_url="doi:10.5281/zenodo.15200083", registry={"cmet_results.tar.gz": "md5:a4ca67a907f744c696b09660dc1eb8ec"}, - retry_if_failed=5, ) @@ -451,7 +449,6 @@ def test_allow_partial_msg_not_printed(self, results_paths_serial_missing_legs: path=POOCH_CACHE, base_url="doi:10.5281/zenodo.17435569", registry={"septop_results.zip": "md5:2cfa18da59a20228f5c75a1de6ec879e"}, - retry_if_failed=2, ) From d37940541fb0f62ced21a91f9390ad2dcb48bbf7 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:14:13 -0800 Subject: [PATCH 36/47] only run CI on macos python 3.12 (#1803) --- .github/workflows/ci.yaml | 5 ++++- .github/workflows/conda_cron.yaml | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2d47c3784..627910d2f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - os: ["ubuntu-latest", "macos-latest"] + os: ["ubuntu-latest"] openeye: ["no"] python-version: - "3.11" @@ -45,6 +45,9 @@ jobs: - os: "ubuntu-latest" python-version: "3.13" openeye: "yes" + - os: "macos-latest" + python-version: "3.12" + openeye: "no" env: OE_LICENSE: ${{ github.workspace }}/oe_license.txt diff --git a/.github/workflows/conda_cron.yaml b/.github/workflows/conda_cron.yaml index c96f00a94..c31b3fb13 100644 --- a/.github/workflows/conda_cron.yaml +++ b/.github/workflows/conda_cron.yaml @@ -20,11 +20,15 @@ jobs: strategy: fail-fast: false matrix: - os: ['ubuntu-latest', 'macos-latest'] + os: ['ubuntu-latest'] python-version: - "3.11" - "3.12" - "3.13" + include: + - os: "macos-latest" + python-version: "3.12" + openeye: "no" steps: - name: Checkout Code uses: actions/checkout@v4 From a4a4da7fc51464235d2cfab84a8f9f87660072a1 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Thu, 22 Jan 2026 13:38:57 +0000 Subject: [PATCH 37/47] Turn AFE protocols into multiple units (#1776) * Split the AFE protocol units into setup, simulation, and analysis. --- docs/reference/api/openmm_binding_afe.rst | 8 +- docs/reference/api/openmm_solvation_afe.rst | 8 +- news/multi-unit-afe.rst | 26 + openfe/protocols/openmm_afe/__init__.py | 32 +- openfe/protocols/openmm_afe/abfe_units.py | 97 +- .../openmm_afe/afe_protocol_results.py | 12 +- openfe/protocols/openmm_afe/ahfe_units.py | 94 +- openfe/protocols/openmm_afe/base_afe_units.py | 1375 ++++++++++------- .../openmm_afe/equil_binding_afe_method.py | 94 +- .../openmm_afe/equil_solvation_afe_method.py | 90 +- .../protocols/openmm_utils/serialization.py | 25 + .../ABFEProtocol_json_results.json.gz | Bin 102891 -> 110381 bytes .../openmm_afe/AHFEProtocol_json_results.gz | Bin 45026 -> 54688 bytes openfe/tests/protocols/conftest.py | 15 + .../openmm_abfe/test_abfe_energies.py | 22 +- .../openmm_abfe/test_abfe_protocol.py | 210 ++- .../openmm_abfe/test_abfe_protocol_results.py | 135 +- .../protocols/openmm_abfe/test_abfe_slow.py | 40 +- .../openmm_abfe/test_abfe_tokenization.py | 135 +- openfe/tests/protocols/openmm_abfe/utils.py | 30 + .../openmm_ahfe/test_ahfe_protocol.py | 384 ++--- .../openmm_ahfe/test_ahfe_protocol_results.py | 278 ++++ .../protocols/openmm_ahfe/test_ahfe_slow.py | 40 +- .../openmm_ahfe/test_ahfe_tokenization.py | 135 +- .../openmm_ahfe/test_ahfe_validation.py | 7 - openfe/tests/protocols/openmm_ahfe/utils.py | 31 + openfecli/commands/gather_abfe.py | 7 +- 27 files changed, 2169 insertions(+), 1161 deletions(-) create mode 100644 news/multi-unit-afe.rst create mode 100644 openfe/tests/protocols/openmm_abfe/utils.py create mode 100644 openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol_results.py create mode 100644 openfe/tests/protocols/openmm_ahfe/utils.py diff --git a/docs/reference/api/openmm_binding_afe.rst b/docs/reference/api/openmm_binding_afe.rst index d5548fee3..4c7a89ad5 100644 --- a/docs/reference/api/openmm_binding_afe.rst +++ b/docs/reference/api/openmm_binding_afe.rst @@ -16,8 +16,12 @@ Protocol API specification :toctree: generated/ AbsoluteBindingProtocol - AbsoluteBindingComplexUnit - AbsoluteBindingSolventUnit + ABFEComplexAnalysisUnit + ABFEComplexSetupUnit + ABFEComplexSimUnit + ABFESolventAnalysisUnit + ABFESolventSetupUnit + ABFESolventSimUnit AbsoluteBindingProtocolResult Protocol Settings diff --git a/docs/reference/api/openmm_solvation_afe.rst b/docs/reference/api/openmm_solvation_afe.rst index de8ef1181..c4a4f3de7 100644 --- a/docs/reference/api/openmm_solvation_afe.rst +++ b/docs/reference/api/openmm_solvation_afe.rst @@ -16,8 +16,12 @@ Protocol API specification :toctree: generated/ AbsoluteSolvationProtocol - AbsoluteSolvationVacuumUnit - AbsoluteSolvationSolventUnit + AHFESolventAnalysisUnit + AHFESolventSetupUnit + AHFESolventSimUnit + AHFEVacuumAnalysisUnit + AHFEVacuumSetupUnit + AHFEVacuumSimUnit AbsoluteSolvationProtocolResult Protocol Settings diff --git a/news/multi-unit-afe.rst b/news/multi-unit-afe.rst new file mode 100644 index 000000000..6831b07f2 --- /dev/null +++ b/news/multi-unit-afe.rst @@ -0,0 +1,26 @@ +**Added:** + +* + +**Changed:** + +* The absolute free energy protocols have been broken into multiple + protocol units, allowing for setup, run, and analysis to happen + separately in the future when relevant changes to protocol execution are + made (PR #1776). + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/openfe/protocols/openmm_afe/__init__.py b/openfe/protocols/openmm_afe/__init__.py index 314179c56..6995d8ed0 100644 --- a/openfe/protocols/openmm_afe/__init__.py +++ b/openfe/protocols/openmm_afe/__init__.py @@ -6,16 +6,24 @@ """ from .abfe_units import ( - AbsoluteBindingComplexUnit, - AbsoluteBindingSolventUnit, + ABFEComplexAnalysisUnit, + ABFEComplexSetupUnit, + ABFEComplexSimUnit, + ABFESolventAnalysisUnit, + ABFESolventSetupUnit, + ABFESolventSimUnit, ) from .afe_protocol_results import ( AbsoluteBindingProtocolResult, AbsoluteSolvationProtocolResult, ) from .ahfe_units import ( - AbsoluteSolvationSolventUnit, - AbsoluteSolvationVacuumUnit, + AHFESolventAnalysisUnit, + AHFESolventSetupUnit, + AHFESolventSimUnit, + AHFEVacuumAnalysisUnit, + AHFEVacuumSetupUnit, + AHFEVacuumSimUnit, ) from .equil_binding_afe_method import ( AbsoluteBindingProtocol, @@ -30,11 +38,19 @@ "AbsoluteSolvationProtocol", "AbsoluteSolvationSettings", "AbsoluteSolvationProtocolResult", - "AbsoluteVacuumUnit", - "AbsoluteSolventUnit", + "AHFESolventSetupUnit", + "AHFESolventSimUnit", + "AHFESolventAnalysisUnit", + "AHFEVacuumSetupUnit", + "AHFEVacuumSimUnit", + "AHFEVacuumAnalysisUnit", "AbsoluteBindingProtocol", "AbsoluteBindingSettings", "AbsoluteBindingProtocolResult", - "AbsoluteBindingComplexUnit", - "AbsoluteBindingSolventUnit", + "ABFEComplexSetupUnit", + "ABFEComplexSimUnit", + "ABFEComplexAnalysisUnit", + "ABFESolventSetupUnit", + "ABFESolventSimUnit", + "ABFESolventAnalysisUnit", ] diff --git a/openfe/protocols/openmm_afe/abfe_units.py b/openfe/protocols/openmm_afe/abfe_units.py index 0cad0bb66..f35fa8084 100644 --- a/openfe/protocols/openmm_afe/abfe_units.py +++ b/openfe/protocols/openmm_afe/abfe_units.py @@ -1,7 +1,5 @@ # This code is part of OpenFE and is licensed under the MIT license. # For details, see https://github.com/OpenFreeEnergy/openfe -# This code is part of OpenFE and is licensed under the MIT license. -# For details, see https://github.com/OpenFreeEnergy/openfe """ABFE Protocol Units --- :mod:`openfe.protocols.openmm_afe.abfe_units` ======================================================================== This module defines the ProtocolUnits for the @@ -23,7 +21,7 @@ from openmm import System from openmm import unit as ommunit from openmm.app import Topology as omm_topology -from openmmtools.states import GlobalParameterState, ThermodynamicState +from openmmtools.states import ThermodynamicState from rdkit import Chem from openfe.protocols.openmm_afe.equil_afe_settings import ( @@ -36,18 +34,16 @@ from openfe.protocols.restraint_utils.openmm import omm_restraints from openfe.protocols.restraint_utils.openmm.omm_restraints import BoreschRestraint -from .base_afe_units import BaseAbsoluteUnit +from .base_afe_units import ( + BaseAbsoluteMultiStateAnalysisUnit, + BaseAbsoluteMultiStateSimulationUnit, + BaseAbsoluteSetupUnit, +) logger = logging.getLogger(__name__) -class AbsoluteBindingComplexUnit(BaseAbsoluteUnit): - """ - Protocol Unit for the complex phase of an absolute binding free energy - """ - - simtype = "complex" - +class ComplexComponentsMixin: def _get_components(self): """ Get the relevant components for a complex transformation. @@ -75,7 +71,9 @@ def _get_components(self): # Similarly we don't need to check prot_comp return alchem_comps, solv_comp, prot_comp, off_comps - def _handle_settings(self) -> dict[str, SettingsBaseModel]: + +class ComplexSettingsMixin: + def _get_settings(self) -> dict[str, SettingsBaseModel]: """ Extract the relevant settings for a complex transformation. @@ -97,7 +95,7 @@ def _handle_settings(self) -> dict[str, SettingsBaseModel]: * output_settings: MultiStateOutputSettings * restraint_settings: BaseRestraintSettings """ - prot_settings = self._inputs["protocol"].settings + prot_settings = self._inputs["protocol"].settings # type: ignore[attr-defined] settings = {} settings["forcefield_settings"] = prot_settings.forcefield_settings @@ -116,6 +114,15 @@ def _handle_settings(self) -> dict[str, SettingsBaseModel]: return settings + +class ABFEComplexSetupUnit(ComplexComponentsMixin, ComplexSettingsMixin, BaseAbsoluteSetupUnit): + """ + Setup unit for the complex phase of absolute binding free energy + transformations. + """ + + simtype = "complex" + @staticmethod def _get_mda_universe( topology: omm_topology, @@ -261,7 +268,6 @@ def _add_restraints( comp_resids: dict[Component, npt.NDArray], settings: dict[str, SettingsBaseModel], ) -> tuple[ - GlobalParameterState, Quantity, System, geometry.HostGuestRestraintGeometry, @@ -295,9 +301,6 @@ def _add_restraints( Returns ------- - restraint_parameter_state : RestraintParameterState - A RestraintParameterState object that defines the control - parameter for the restraint. correction : openff.units.Quantity The standard state correction for the restraint. system : openmm.System @@ -380,10 +383,7 @@ def _add_restraints( rest_geom, ) - # Get the GlobalParameterState for the restraint - restraint_parameter_state = omm_restraints.RestraintParameterState(lambda_restraints=1.0) return ( - restraint_parameter_state, correction, # Remove the thermostat, otherwise you'll get an # Andersen thermostat by default! @@ -392,13 +392,28 @@ def _add_restraints( ) -class AbsoluteBindingSolventUnit(BaseAbsoluteUnit): +class ABFEComplexSimUnit( + ComplexComponentsMixin, ComplexSettingsMixin, BaseAbsoluteMultiStateSimulationUnit +): """ - Protocol Unit for the solvent phase of an absolute binding free energy + Multi-state simulation (e.g. multi replica methods like Hamiltonian + replica exchange) unit for the complex phase of absolute binding + free energy transformations. """ - simtype = "solvent" + simtype = "complex" + +class ABFEComplexAnalysisUnit(ComplexSettingsMixin, BaseAbsoluteMultiStateAnalysisUnit): + """ + Analysis unit for multi-state simulations with the complex phase + of absolute binding free energy transformations. + """ + + simtype = "complex" + + +class SolventComponentsMixin: def _get_components(self): """ Get the relevant components for a solvent transformation. @@ -426,7 +441,9 @@ def _get_components(self): # Similarly we don't need to check prot_comp just return None return alchem_comps, solv_comp, None, off_comps - def _handle_settings(self) -> dict[str, SettingsBaseModel]: + +class SolventSettingsMixin: + def _get_settings(self) -> dict[str, SettingsBaseModel]: """ Extract the relevant settings for a solvent transformation. @@ -447,7 +464,7 @@ def _handle_settings(self) -> dict[str, SettingsBaseModel]: * simulation_settings : MultiStateSimulationSettings * output_settings: MultiStateOutputSettings """ - prot_settings = self._inputs["protocol"].settings + prot_settings = self._inputs["protocol"].settings # type: ignore[attr-defined] settings = {} settings["forcefield_settings"] = prot_settings.forcefield_settings @@ -464,3 +481,33 @@ def _handle_settings(self) -> dict[str, SettingsBaseModel]: settings["output_settings"] = prot_settings.solvent_output_settings return settings + + +class ABFESolventSetupUnit(SolventComponentsMixin, SolventSettingsMixin, BaseAbsoluteSetupUnit): + """ + Setup unit for the solvent phase of absolute binding free energy + transformations. + """ + + simtype = "solvent" + + +class ABFESolventSimUnit( + SolventComponentsMixin, SolventSettingsMixin, BaseAbsoluteMultiStateSimulationUnit +): + """ + Multi-state simulation (e.g. multi replica methods like Hamiltonian + replica exchange) unit for the solvent phase of absolute binding + free energy transformations. + """ + + simtype = "solvent" + + +class ABFESolventAnalysisUnit(SolventSettingsMixin, BaseAbsoluteMultiStateAnalysisUnit): + """ + Analysis unit for multi-state simulations with the solvent phase + of absolute binding free energy transformations. + """ + + simtype = "solvent" diff --git a/openfe/protocols/openmm_afe/afe_protocol_results.py b/openfe/protocols/openmm_afe/afe_protocol_results.py index 14012a335..38d1e1879 100644 --- a/openfe/protocols/openmm_afe/afe_protocol_results.py +++ b/openfe/protocols/openmm_afe/afe_protocol_results.py @@ -214,8 +214,8 @@ def get_replica_state(nc, chk): for key in [self.bound_state, self.unbound_state]: for pus in self.data[key].values(): # type: ignore[attr-defined] states = get_replica_state( - pus[0].outputs["nc"], - pus[0].outputs["last_checkpoint"], + pus[0].outputs["trajectory"], + pus[0].outputs["checkpoint"], ) replica_states[key].append(states) @@ -295,7 +295,9 @@ def selection_indices(self) -> dict[str, list[Optional[npt.NDArray]]]: class AbsoluteSolvationProtocolResult(gufe.ProtocolResult, AbsoluteProtocolResultMixin): - """Dict-like container for the output of a AbsoluteSolvationProtocol""" + """ + Protocol results with the output of a AbsoluteSolvationProtocol + """ bound_state = "solvent" unbound_state = "vacuum" @@ -375,7 +377,9 @@ def _get_stdev(estimates): class AbsoluteBindingProtocolResult(gufe.ProtocolResult, AbsoluteProtocolResultMixin): - """Dict-like container for the output of a AbsoluteBindingProtocol""" + """ + Protocol results with the output of a AbsoluteBindingProtocol. + """ bound_state = "complex" unbound_state = "solvent" diff --git a/openfe/protocols/openmm_afe/ahfe_units.py b/openfe/protocols/openmm_afe/ahfe_units.py index 9fb9b03da..d496dd5ff 100644 --- a/openfe/protocols/openmm_afe/ahfe_units.py +++ b/openfe/protocols/openmm_afe/ahfe_units.py @@ -1,9 +1,9 @@ # This code is part of OpenFE and is licensed under the MIT license. # For details, see https://github.com/OpenFreeEnergy/openfe -# This code is part of OpenFE and is licensed under the MIT license. -# For details, see https://github.com/OpenFreeEnergy/openfe -"""AHFE Protocol Units --- :mod:`openfe.protocols.openmm_afe.ahfe_units` -======================================================================== +""" +AHFE Protocol Units --- :mod:`openfe.protocols.openmm_afe.ahfe_units` +===================================================================== + This module defines the ProtocolUnits for the :class:`AbsoluteSolvationProtocol`. """ @@ -15,18 +15,16 @@ ) from ..openmm_utils import system_validation -from .base_afe_units import BaseAbsoluteUnit +from .base_afe_units import ( + BaseAbsoluteMultiStateAnalysisUnit, + BaseAbsoluteMultiStateSimulationUnit, + BaseAbsoluteSetupUnit, +) logger = logging.getLogger(__name__) -class AbsoluteSolvationVacuumUnit(BaseAbsoluteUnit): - """ - Protocol Unit for the vacuum phase of an absolute solvation free energy - """ - - simtype = "vacuum" - +class VacuumComponentsMixin: def _get_components(self): """ Get the relevant components for a vacuum transformation. @@ -59,7 +57,9 @@ def _get_components(self): # (of stateA since we enforce only one disappearing ligand) return alchem_comps, None, prot_comp, off_comps - def _handle_settings(self) -> dict[str, SettingsBaseModel]: + +class VacuumSettingsMixin: + def _get_settings(self) -> dict[str, SettingsBaseModel]: """ Extract the relevant settings for a vacuum transformation. @@ -80,7 +80,7 @@ def _handle_settings(self) -> dict[str, SettingsBaseModel]: * simulation_settings : SimulationSettings * output_settings: MultiStateOutputSettings """ - prot_settings = self._inputs["protocol"].settings + prot_settings = self._inputs["protocol"].settings # type: ignore[attr-defined] settings = {} settings["forcefield_settings"] = prot_settings.vacuum_forcefield_settings @@ -99,13 +99,37 @@ def _handle_settings(self) -> dict[str, SettingsBaseModel]: return settings -class AbsoluteSolvationSolventUnit(BaseAbsoluteUnit): +class AHFEVacuumSetupUnit(VacuumComponentsMixin, VacuumSettingsMixin, BaseAbsoluteSetupUnit): """ - Protocol Unit for the solvent phase of an absolute solvation free energy + Setup unit for the vacuum phase of absolute hydration free energy + transformations. """ - simtype = "solvent" + simtype = "vacuum" + + +class AHFEVacuumSimUnit( + VacuumComponentsMixin, VacuumSettingsMixin, BaseAbsoluteMultiStateSimulationUnit +): + """ + Multi-state simulation (e.g. multi replica methods like Hamiltonian + replica exchange) unit for the vacuum phase of absolute hydration + free energy transformations. + """ + + simtype = "vacuum" + + +class AHFEVacuumAnalysisUnit(VacuumSettingsMixin, BaseAbsoluteMultiStateAnalysisUnit): + """ + Analysis unit for multi-state simulations with the vacuum phase + of absolute hydration free energy transformations. + """ + simtype = "vacuum" + + +class SolventComponentsMixin: def _get_components(self): """ Get the relevant components for a solvent transformation. @@ -134,7 +158,9 @@ def _get_components(self): # disallowed on create return alchem_comps, solv_comp, prot_comp, off_comps - def _handle_settings(self) -> dict[str, SettingsBaseModel]: + +class SolventSettingsMixin: + def _get_settings(self) -> dict[str, SettingsBaseModel]: """ Extract the relevant settings for a solvent transformation. @@ -155,7 +181,7 @@ def _handle_settings(self) -> dict[str, SettingsBaseModel]: * simulation_settings : MultiStateSimulationSettings * output_settings: MultiStateOutputSettings """ - prot_settings = self._inputs["protocol"].settings + prot_settings = self._inputs["protocol"].settings # type: ignore[attr-defined] settings = {} settings["forcefield_settings"] = prot_settings.solvent_forcefield_settings @@ -172,3 +198,33 @@ def _handle_settings(self) -> dict[str, SettingsBaseModel]: settings["output_settings"] = prot_settings.solvent_output_settings return settings + + +class AHFESolventSetupUnit(SolventComponentsMixin, SolventSettingsMixin, BaseAbsoluteSetupUnit): + """ + Setup unit for the solvent phase of absolute hydration free energy + transformations. + """ + + simtype = "solvent" + + +class AHFESolventSimUnit( + SolventComponentsMixin, SolventSettingsMixin, BaseAbsoluteMultiStateSimulationUnit +): + """ + Multi-state simulation (e.g. multi replica methods like Hamiltonian + replica exchange) unit for the solvent phase of absolute hydration + free energy transformations. + """ + + simtype = "solvent" + + +class AHFESolventAnalysisUnit(SolventSettingsMixin, BaseAbsoluteMultiStateAnalysisUnit): + """ + Analysis unit for multi-state simulations with the solvent phase + of absolute hydration free energy transformations. + """ + + simtype = "solvent" diff --git a/openfe/protocols/openmm_afe/base_afe_units.py b/openfe/protocols/openmm_afe/base_afe_units.py index d91f0afa8..4095982ab 100644 --- a/openfe/protocols/openmm_afe/base_afe_units.py +++ b/openfe/protocols/openmm_afe/base_afe_units.py @@ -15,14 +15,12 @@ * Allow for a more flexible setting of Lambda regions. """ -from __future__ import annotations - import abc import copy import logging import os import pathlib -from typing import Any, Optional +from typing import Any import gufe import mdtraj as mdt @@ -31,14 +29,14 @@ import openmm import openmmtools from gufe import ( - ChemicalSystem, ProteinComponent, SmallMoleculeComponent, SolventComponent, ) from gufe.components import Component from openff.toolkit.topology import Molecule as OFFMolecule -from openff.units import Quantity, unit +from openff.units import Quantity +from openff.units import unit as offunit from openff.units.openmm import ensure_quantity, from_openmm, to_openmm from openmm import app from openmm import unit as ommunit @@ -76,67 +74,108 @@ system_creation, ) from openfe.protocols.openmm_utils.omm_settings import ( - BasePartialChargeSettings, SettingsBaseModel, ) +from openfe.protocols.openmm_utils.serialization import ( + deserialize, + make_vec3_box, + serialize, +) from openfe.protocols.restraint_utils import geometry +from openfe.protocols.restraint_utils.openmm import omm_restraints from openfe.utils import log_system_probe, without_oechem_backend logger = logging.getLogger(__name__) -class BaseAbsoluteUnit(gufe.ProtocolUnit): - """ - Base class for ligand absolute free energy transformations. - """ - - def __init__( +class AbsoluteUnitMixin: + def _prepare( self, - *, - protocol: gufe.Protocol, - stateA: ChemicalSystem, - stateB: ChemicalSystem, - alchemical_components: dict[str, list[Component]], - generation: int = 0, - repeat_id: int = 0, - name: Optional[str] = None, + verbose: bool, + scratch_basepath: pathlib.Path | None, + shared_basepath: pathlib.Path | None, ): """ + Set basepaths and do some initial logging. + Parameters ---------- - protocol : gufe.Protocol - protocol used to create this Unit. Contains key information such - as the settings. - stateA : ChemicalSystem - ChemicalSystem containing the components defining the state at - lambda 0. - stateB : ChemicalSystem - ChemicalSystem containing the components defining the state at - lambda 1. - alchemical_components : dict[str, Component] - the alchemical components for each state in this Unit - name : str, optional - Human-readable identifier for this Unit - repeat_id : int, optional - Identifier for which repeat (aka replica/clone) this Unit is, - default 0 - generation : int, optional - Generation counter which keeps track of how many times this repeat - has been extended, default 0. + verbose : bool + Verbose output of the simulation progress. Output is provided via + INFO level logging. + scratch_basepath : pathlib.Path | None + Optional base path to write scratch files to. + shared_basepath : pathlib.Path | None + Optional base path to write shared files to. """ - super().__init__( - name=name, - protocol=protocol, - stateA=stateA, - stateB=stateB, - alchemical_components=alchemical_components, - repeat_id=repeat_id, - generation=generation, - ) + self.verbose = verbose + + if self.verbose: + self.logger.info("setting up alchemical system") # type: ignore[attr-defined] + + # set basepaths + def _set_optional_path(basepath): + if basepath is None: + return pathlib.Path(".") + return basepath + + self.scratch_basepath = _set_optional_path(scratch_basepath) + self.shared_basepath = _set_optional_path(shared_basepath) + + @abc.abstractmethod + def _get_settings(self) -> dict[str, SettingsBaseModel]: + """ + Get a dictionary with the following entries: + * forcefield_settings : OpenMMSystemGeneratorFFSettings + * thermo_settings : ThermoSettings + * solvation_settings : BaseSolvationSettings + * alchemical_settings : AlchemicalSettings + * lambda_settings : LambdaSettings + * engine_settings : OpenMMEngineSettings + * integrator_settings : IntegratorSettings + * equil_simulation_settings : MDSimulationSettings + * equil_output_settings : MDOutputSettings + * simulation_settings : MultiStateSimulationSettings + * output_settings : MultiStateOutputSettings + + Settings may change depending on what type of simulation you are + running. Cherry pick them and return them to be available later on. + + This method should also add various validation checks as necessary. + + Note + ---- + Must be implemented in the child class. + """ + ... + + +class BaseAbsoluteSetupUnit(gufe.ProtocolUnit, AbsoluteUnitMixin): + """ + Base class for setting up an absolute free energy transformations. + """ + + @abc.abstractmethod + def _get_components( + self, + ) -> tuple[ + dict[str, list[Component]], + gufe.SolventComponent | None, + gufe.ProteinComponent | None, + dict[SmallMoleculeComponent, OFFMolecule], + ]: + """ + Get the relevant components to create the alchemical system with. + + Note + ---- + Must be implemented in the child class. + """ + ... @staticmethod def _get_alchemical_indices( - omm_top: openmm.Topology, + omm_top: openmm.app.Topology, comp_resids: dict[Component, npt.NDArray], alchem_comps: dict[str, list[Component]], ) -> list[int]: @@ -287,84 +326,39 @@ def _pre_equilibrate( return equilibrated_positions, box - def _prepare( - self, - verbose: bool, - scratch_basepath: Optional[pathlib.Path], - shared_basepath: Optional[pathlib.Path], - ): + @staticmethod + def _assign_partial_charges( + partial_charge_settings: OpenFFPartialChargeSettings, + small_mols: dict[SmallMoleculeComponent, OFFMolecule], + ) -> None: """ - Set basepaths and do some initial logging. + Assign partial charges to the OpenFF Molecules associated with + all the SmallMoleculeComponents in the transformation. Parameters ---------- - verbose : bool - Verbose output of the simulation progress. Output is provided via - INFO level logging. - basepath : Optional[pathlib.Path] - Optional base path to write files to. - """ - self.verbose = verbose - - if self.verbose: - self.logger.info("setting up alchemical system") - - # set basepaths - def _set_optional_path(basepath): - if basepath is None: - return pathlib.Path(".") - return basepath - - self.scratch_basepath = _set_optional_path(scratch_basepath) - self.shared_basepath = _set_optional_path(shared_basepath) - - @abc.abstractmethod - def _get_components( - self, - ) -> tuple[ - dict[str, list[Component]], - Optional[gufe.SolventComponent], - Optional[gufe.ProteinComponent], - dict[SmallMoleculeComponent, OFFMolecule], - ]: - """ - Get the relevant components to create the alchemical system with. - - Note - ---- - Must be implemented in the child class. - """ - ... - - @abc.abstractmethod - def _handle_settings(self) -> dict[str, SettingsBaseModel]: - """ - Get a dictionary with the following entries: - * forcefield_settings : OpenMMSystemGeneratorFFSettings - * thermo_settings : ThermoSettings - * solvation_settings : BaseSolvationSettings - * alchemical_settings : AlchemicalSettings - * lambda_settings : LambdaSettings - * engine_settings : OpenMMEngineSettings - * integrator_settings : IntegratorSettings - * equil_simulation_settings : MDSimulationSettings - * equil_output_settings : MDOutputSettings - * simulation_settings : MultiStateSimulationSettings - * output_settings : MultiStateOutputSettings - - Settings may change depending on what type of simulation you are - running. Cherry pick them and return them to be available later on. - - This method should also add various validation checks as necessary. - - Note - ---- - Must be implemented in the child class. + charge_settings : OpenFFPartialChargeSettings + Settings for controlling how the partial charges are assigned. + small_mols : dict[SmallMoleculeComponent, openff.toolkit.Molecule] + Dictionary of OpenFF Molecules to add, keyed by their + associated SmallMoleculeComponent. """ - ... + for mol in small_mols.values(): + charge_generation.assign_offmol_partial_charges( + offmol=mol, + overwrite=False, + method=partial_charge_settings.partial_charge_method, + toolkit_backend=partial_charge_settings.off_toolkit_backend, + generate_n_conformers=partial_charge_settings.number_of_conformers, + nagl_model=partial_charge_settings.nagl_model, + ) + @staticmethod def _get_system_generator( - self, settings: dict[str, SettingsBaseModel], solvent_comp: Optional[SolventComponent] + settings: dict[str, SettingsBaseModel], + solvent_component: SolventComponent | None, + openff_molecules: list[OFFMolecule], + ffcache: pathlib.Path | None, ) -> SystemGenerator: """ Get a system generator through the system creation @@ -374,63 +368,42 @@ def _get_system_generator( ---------- settings : dict[str, SettingsBaseModel] A dictionary of settings object for the unit. - solvent_comp : Optional[SolventComponent] + solvent_comp : SolventComponent | None The solvent component of this system, if there is one. + openff_molecules : list[openff.toolkit.Molecule] | None + A list of OpenFF Molecules to generate templates for, if any. + ffcache : pathlib.Path | None + Path to the force field parameter cache. Returns ------- system_generator : openmmforcefields.generator.SystemGenerator System Generator to parameterise this unit. """ - ffcache = settings["output_settings"].forcefield_cache - if ffcache is not None: - ffcache = self.shared_basepath / ffcache + system_generator = system_creation.get_system_generator( + forcefield_settings=settings["forcefield_settings"], + integrator_settings=settings["integrator_settings"], + thermo_settings=settings["thermo_settings"], + cache=ffcache, + has_solvent=solvent_component is not None, + ) - # Block out oechem backend to avoid any issues with - # smiles roundtripping between rdkit and oechem - with without_oechem_backend(): - system_generator = system_creation.get_system_generator( - forcefield_settings=settings["forcefield_settings"], - integrator_settings=settings["integrator_settings"], - thermo_settings=settings["thermo_settings"], - cache=ffcache, - has_solvent=solvent_comp is not None, - ) - return system_generator + # Handle openff Molecule templates + # TODO: revisit this once the SystemGenerator update happens + if openff_molecules is None: + return system_generator - @staticmethod - def _assign_partial_charges( - partial_charge_settings: OpenFFPartialChargeSettings, - smc_components: dict[SmallMoleculeComponent, OFFMolecule], - ) -> None: - """ - Assign partial charges to SMCs. + # Register all the templates, pass unique molecules to avoid clashes + system_generator.add_molecules(list(set(openff_molecules))) - Parameters - ---------- - charge_settings : OpenFFPartialChargeSettings - Settings for controlling how the partial charges are assigned. - smc_components : dict[SmallMoleculeComponent, openff.toolkit.Molecule] - Dictionary of OpenFF Molecules to add, keyed by - SmallMoleculeComponent. - """ - for mol in smc_components.values(): - charge_generation.assign_offmol_partial_charges( - offmol=mol, - overwrite=False, - method=partial_charge_settings.partial_charge_method, - toolkit_backend=partial_charge_settings.off_toolkit_backend, - generate_n_conformers=partial_charge_settings.number_of_conformers, - nagl_model=partial_charge_settings.nagl_model, - ) + return system_generator + @staticmethod def _get_modeller( - self, - protein_component: Optional[ProteinComponent], - solvent_component: Optional[SolventComponent], - smc_components: dict[SmallMoleculeComponent, OFFMolecule], + protein_component: ProteinComponent | None, + solvent_component: SolventComponent | None, + small_mols: dict[SmallMoleculeComponent, OFFMolecule], system_generator: SystemGenerator, - partial_charge_settings: BasePartialChargeSettings, solvation_settings: BaseSolvationSettings, ) -> tuple[app.Modeller, dict[Component, npt.NDArray]]: """ @@ -439,18 +412,15 @@ def _get_modeller( Parameters ---------- - protein_component : Optional[ProteinComponent] + protein_component : ProteinComponent | None Protein Component, if it exists. - solvent_component : Optional[ProteinCompoinent] + solvent_component : SolventComponent | None Solvent Component, if it exists. - smc_components : dict[SmallMoleculeComponent, openff.toolkit.Molecule] + small_mols : dict[SmallMoleculeComponent, openff.toolkit.Molecule] Dictionary of OpenFF Molecules to add, keyed by SmallMoleculeComponent. system_generator : openmmforcefields.generator.SystemGenerator System Generator to parameterise this unit. - partial_charge_settings : BasePartialChargeSettings - Settings detailing how to assign partial charges to the - SMCs of the system. solvation_settings : BaseSolvationSettings Settings detailing how to solvate the system. @@ -462,39 +432,23 @@ def _get_modeller( comp_resids : dict[Component, npt.NDArray] Dictionary of residue indices for each component in system. """ - if self.verbose: - self.logger.info("Parameterizing molecules") - - # Assign partial charges to smcs - self._assign_partial_charges(partial_charge_settings, smc_components) - - # TODO: guard the following from non-RDKit backends - # force the creation of parameters for the small molecules - # this is necessary because we need to have the FF generated ahead - # of solvating the system. - # Block out oechem backend to avoid any issues with - # smiles roundtripping between rdkit and oechem - with without_oechem_backend(): - for mol in smc_components.values(): - system_generator.create_system(mol.to_topology().to_openmm(), molecules=[mol]) - - # get OpenMM modeller + dictionary of resids for each component - system_modeller, comp_resids = system_creation.get_omm_modeller( - protein_comp=protein_component, - solvent_comp=solvent_component, - small_mols=smc_components, - omm_forcefield=system_generator.forcefield, - solvent_settings=solvation_settings, - ) + # get OpenMM modeller + dictionary of resids for each component + system_modeller, comp_resids = system_creation.get_omm_modeller( + protein_comp=protein_component, + solvent_comp=solvent_component, + small_mols=small_mols, + omm_forcefield=system_generator.forcefield, + solvent_settings=solvation_settings, + ) return system_modeller, comp_resids def _get_omm_objects( self, settings: dict[str, SettingsBaseModel], - protein_component: Optional[ProteinComponent], - solvent_component: Optional[SolventComponent], - smc_components: dict[SmallMoleculeComponent, OFFMolecule], + protein_component: ProteinComponent | None, + solvent_component: SolventComponent | None, + small_mols: dict[SmallMoleculeComponent, OFFMolecule], ) -> tuple[ app.Topology, openmm.System, @@ -509,12 +463,13 @@ def _get_omm_objects( ---------- settings : dict[str, SettingsBaseModel] Protocol settings - protein_component : Optional[ProteinComponent] + protein_component : ProteinComponent | None Protein component for the system. - solvent_component : Optional[SolventComponent] + solvent_component : SolventComponent | None Solvent component for the system. - smc_components : dict[str, OFFMolecule] - SmallMoleculeComponents defining ligands to be added to the system + small_mols : dict[str, openff.toolkit.Molecule] + Dictionary of SmallMoleculeComponents and OpenFF Molecules + defining the ligands to be added to the system Returns ------- @@ -530,76 +485,33 @@ def _get_omm_objects( if self.verbose: self.logger.info("Parameterizing system") - system_generator = self._get_system_generator( - settings=settings, solvent_comp=solvent_component - ) - - modeller, comp_resids = self._get_modeller( - protein_component=protein_component, - solvent_component=solvent_component, - smc_components=smc_components, - system_generator=system_generator, - partial_charge_settings=settings["charge_settings"], - solvation_settings=settings["solvation_settings"], - ) + with without_oechem_backend(): + system_generator = self._get_system_generator( + settings=settings, + solvent_component=solvent_component, + openff_molecules=list(small_mols.values()), + ffcache=self.shared_basepath / settings["output_settings"].forcefield_cache, + ) - topology = modeller.getTopology() - # roundtrip positions to remove vec3 issues - positions = to_openmm(from_openmm(modeller.getPositions())) + modeller, comp_resids = self._get_modeller( + protein_component=protein_component, + solvent_component=solvent_component, + small_mols=small_mols, + system_generator=system_generator, + solvation_settings=settings["solvation_settings"], + ) - # Block out oechem backend to avoid any issues with - # smiles roundtripping between rdkit and oechem - with without_oechem_backend(): system = system_generator.create_system( topology=modeller.topology, - molecules=list(smc_components.values()), + molecules=list(small_mols.values()), ) - # Check and fail early on the presence of virtual sites - # and multistate sampler not using velocity restart - if not settings["integrator_settings"].reassign_velocities: - has_vsite = any(system.isVirtualSite(i) for i in range(system.getNumParticles())) - if has_vsite: - errmsg = "Simulations with virtual sites without velocity reassignment are unstable" - raise ValueError(errmsg) + topology = modeller.getTopology() + # roundtrip positions to remove vec3 issues + positions = to_openmm(from_openmm(modeller.getPositions())) return topology, system, positions, comp_resids - def _get_lambda_schedule( - self, settings: dict[str, SettingsBaseModel] - ) -> dict[str, list[float]]: - """ - Create the lambda schedule - - Parameters - ---------- - settings : dict[str, SettingsBaseModel] - Settings for the unit. - - Returns - ------- - lambdas : dict[str, list[float]] - - TODO - ---- - * Augment this by using something akin to the RFE protocol's - LambdaProtocol - """ - lambdas = dict() - - lambda_elec = settings["lambda_settings"].lambda_elec - lambda_vdw = settings["lambda_settings"].lambda_vdw - lambda_rest = settings["lambda_settings"].lambda_restraints - - # Reverse lambda schedule for vdw, elect, and restraints - # since in AbsoluteAlchemicalFactory 1 means fully - # interacting (which would be non-interacting for us) - lambdas["lambda_electrostatics"] = [1 - x for x in lambda_elec] - lambdas["lambda_sterics"] = [1 - x for x in lambda_vdw] - lambdas["lambda_restraints"] = [x for x in lambda_rest] - - return lambdas - def _add_restraints( self, system: openmm.System, @@ -609,15 +521,14 @@ def _add_restraints( comp_resids: dict[Component, npt.NDArray], settings: dict[str, SettingsBaseModel], ) -> tuple[ - Optional[GlobalParameterState], - Optional[Quantity], - Optional[openmm.System], - Optional[geometry.BaseRestraintGeometry], + Quantity | None, + openmm.System | None, + geometry.BaseRestraintGeometry | None, ]: """ Placeholder method to add restraints if necessary """ - return None, None, system, None + return None, system, None def _get_alchemical_system( self, @@ -685,61 +596,307 @@ def _get_alchemical_system( return alchemical_factory, alchemical_system, alchemical_indices - def _get_states( - self, - alchemical_system: openmm.System, + @staticmethod + def _subsample_topology( + topology: openmm.app.Topology, positions: openmm.unit.Quantity, - box_vectors: openmm.unit.Quantity, - settings: dict[str, SettingsBaseModel], - lambdas: dict[str, list[float]], - solvent_comp: Optional[SolventComponent], - restraint_state: Optional[GlobalParameterState], - ) -> tuple[list[SamplerState], list[ThermodynamicState]]: + output_selection: str, + output_file: pathlib.Path, + ) -> npt.NDArray: """ - Get a list of sampler and thermodynmic states from an - input alchemical system. + Subsample the system based on user-selected output selection + and write the subsampled topology to a PDB file. Parameters ---------- - alchemical_system : openmm.System - Alchemical system to get states for. + topology : openmm.app.Topology + The system topology to subsample. positions : openmm.unit.Quantity - Positions of the alchemical system. - box_vectors : openmm.unit.Quantity - Box vectors of the alchemical system. - settings : dict[str, SettingsBaseModel] - A dictionary of settings for the protocol unit. - lambdas : dict[str, list[float]] - A dictionary of lambda scales. - solvent_comp : Optional[SolventComponent] - The solvent component of the system, if there is one. - restraint_state : Optional[GlobalParameterState] - The restraint parameter control state, if there is one. + The system positions. + output_selection : str + An MDTraj selection string to subsample the topology with. + output_file : pathlib.Path + Path to the file to write the PDB to. Returns ------- - sampler_states : list[SamplerState] - A list of SamplerStates for each replica in the system. - cmp_states : list[ThermodynamicState] - A list of ThermodynamicState for each replica in the system. + selection_indices : npt.NDArray + The indices of the subselected system. + """ + mdt_top = mdt.Topology.from_openmm(topology) + selection_indices = mdt_top.select(output_selection) + + # Write out the subselected structure to PDB if not empty + if len(selection_indices) > 0: + traj = mdt.Trajectory( + positions[selection_indices, :], + mdt_top.subset(selection_indices), + ) + traj.save_pdb(output_file) + + return selection_indices + + def run( + self, + dry: bool = False, + verbose: bool = True, + scratch_basepath: pathlib.Path | None = None, + shared_basepath: pathlib.Path | None = None, + ) -> dict[str, Any]: + """Run the setup phase of an absolute free energy calculation. + + Parameters + ---------- + dry : bool + Do a dry run of the calculation, creating all necessary alchemical + system components (topology, system, etc...) but without + running the simulation, default False + verbose : bool + Verbose output of the simulation progress. Output is provided via + INFO level logging, default True + scratch_basepath : pathlib.Path | None + Path to the scratch (temporary) directory space. Defaults to the + current working directory if ``None``. + shared_basepath : pathlib.Path | None + Path to the shared (persistent) directory space. Defaults to the + current working directory if ``None``. + + Returns + ------- + dict + Outputs created in the basepath directory or the debug objects + (i.e. sampler) if ``dry==True``. + """ + # General preparation tasks + self._prepare(verbose, scratch_basepath, shared_basepath) + + # Get components + alchem_comps, solv_comp, prot_comp, small_mols = self._get_components() + + # Get settings + settings = self._get_settings() + + # Assign partial charges now to avoid any discrepancies later + self._assign_partial_charges(settings["charge_settings"], small_mols) + + # Get OpenMM topology, positions, system, and comp_resids + omm_topology, omm_system, positions, comp_resids = self._get_omm_objects( + settings=settings, + protein_component=prot_comp, + solvent_component=solv_comp, + small_mols=small_mols, + ) + + # Pre-equilbrate System (Test + Avoid NaNs + get stable system) + positions, box_vectors = self._pre_equilibrate( + omm_system, omm_topology, positions, settings, dry + ) + + # Add restraints + # Note: when no restraint is applied, restrained_omm_system == omm_system + ( + standard_state_corr, + restrained_omm_system, + restraint_geometry, + ) = self._add_restraints( + omm_system, + omm_topology, + positions, + alchem_comps, + comp_resids, + settings, + ) + + # Get alchemical system + alchem_factory, alchem_system, alchem_indices = self._get_alchemical_system( + topology=omm_topology, + system=restrained_omm_system, + comp_resids=comp_resids, + alchem_comps=alchem_comps, + alchemical_settings=settings["alchemical_settings"], + ) + + # Subselect system based on user inputs & write initial PDB + selection_indices = self._subsample_topology( + topology=omm_topology, + positions=positions, + output_selection=settings["output_settings"].output_indices, + output_file=self.shared_basepath / settings["output_settings"].output_structure, + ) + + # Serialize relevant outputs + system_outfile = self.shared_basepath / "alchemical_system.xml.bz2" + serialize(alchem_system, system_outfile) + + positions_outfile = self.shared_basepath / "system_positions.npy" + npy_positions = from_openmm(positions).to("nanometer").m + np.save(positions_outfile, npy_positions) + + # Set the PDB file name + if len(selection_indices) > 0: + pdb_structure = self.shared_basepath / settings["output_settings"].output_structure + else: + pdb_structure = None + + unit_results_dict = { + "system": system_outfile, + "positions": positions_outfile, + "pdb_structure": pdb_structure, + "selection_indices": selection_indices, + "box_vectors": from_openmm(box_vectors), + } + + if standard_state_corr is not None: + unit_results_dict["standard_state_correction"] = standard_state_corr.to( + "kilocalorie_per_mole" + ) + else: + unit_results_dict["standard_state_correction"] = 0 * offunit.kilocalorie_per_mole + + if restraint_geometry is not None: + unit_results_dict["restraint_geometry"] = restraint_geometry.model_dump() + else: + unit_results_dict["restraint_geometry"] = None + + if dry: + unit_results_dict |= { + "standard_system": omm_system, + "restrained_system": restrained_omm_system, + "alchem_system": alchem_system, + "alchem_indices": alchem_indices, + "alchem_factory": alchem_factory, + "debug_positions": positions, + } + return unit_results_dict + + def _execute( + self, + ctx: gufe.Context, + **inputs, + ) -> dict[str, Any]: + log_system_probe(logging.INFO, paths=[ctx.scratch]) + + outputs = self.run(scratch_basepath=ctx.scratch, shared_basepath=ctx.shared) + + return { + "repeat_id": self._inputs["repeat_id"], + "generation": self._inputs["generation"], + "simtype": self.simtype, + **outputs, + } + + +class BaseAbsoluteMultiStateSimulationUnit(gufe.ProtocolUnit, AbsoluteUnitMixin): + @abc.abstractmethod + def _get_components( + self, + ) -> tuple[ + dict[str, list[Component]], + gufe.SolventComponent | None, + gufe.ProteinComponent | None, + dict[SmallMoleculeComponent, OFFMolecule], + ]: + """ + Get the relevant components to create the alchemical system with. + + Note + ---- + Must be implemented in the child class. + """ + ... + + def _get_lambda_schedule( + self, settings: dict[str, SettingsBaseModel] + ) -> dict[str, list[float]]: + """ + Create the lambda schedule + + Parameters + ---------- + settings : dict[str, SettingsBaseModel] + Settings for the unit. + + Returns + ------- + lambdas : dict[str, list[float]] + + TODO + ---- + * Augment this by using something akin to the RFE protocol's + LambdaProtocol + """ + lambdas = dict() + + lambda_elec = settings["lambda_settings"].lambda_elec + lambda_vdw = settings["lambda_settings"].lambda_vdw + lambda_rest = settings["lambda_settings"].lambda_restraints + + # Reverse lambda schedule for vdw, end elec, + # since in AbsoluteAlchemicalFactory 1 means fully + # interacting (which would be non-interacting for us) + lambdas["lambda_electrostatics"] = [1 - x for x in lambda_elec] + lambdas["lambda_sterics"] = [1 - x for x in lambda_vdw] + lambdas["lambda_restraints"] = [x for x in lambda_rest] + + return lambdas + + def _get_states( + self, + alchemical_system: openmm.System, + positions: openmm.unit.Quantity, + box_vectors: openmm.unit.Quantity, + thermodynamic_settings: ThermoSettings, + lambdas: dict[str, list[float]], + solvent_component: SolventComponent | None, + alchemically_restrained: bool, + ) -> tuple[list[SamplerState], list[ThermodynamicState]]: + """ + Get a list of sampler and thermodynmic states from an + input alchemical system. + + Parameters + ---------- + alchemical_system : openmm.System + Alchemical system to get states for. + positions : openmm.unit.Quantity + Positions of the alchemical system. + box_vectors : openmm.unit.Quantity + Box vectors of the alchemical system. + thermodynamic_settings : ThermoSettings + Settings controlling the thermodynamic parameters. + lambdas : dict[str, list[float]] + A dictionary of lambda scales. + solvent_component : SolventComponent | None + The solvent component of the system, if there is one. + alchemically_restrained : bool + Whether or not the system requires a control parameter + for any alchemical restraints. + + Returns + ------- + sampler_states : list[SamplerState] + A list of SamplerStates for each replica in the system. + cmp_states : list[ThermodynamicState] + A list of ThermodynamicState for each replica in the system. """ # Fetch an alchemical state alchemical_state = AlchemicalState.from_system(alchemical_system) # Set up the system constants - temperature = settings["thermo_settings"].temperature - pressure = settings["thermo_settings"].pressure + temperature = thermodynamic_settings.temperature + pressure = thermodynamic_settings.pressure constants = dict() constants["temperature"] = ensure_quantity(temperature, "openmm") - if solvent_comp is not None: + if solvent_component is not None: constants["pressure"] = ensure_quantity(pressure, "openmm") # Get the thermodynamic parameter protocol param_protocol = copy.deepcopy(lambdas) # Get the composable states - if restraint_state is not None: + if alchemically_restrained: + restraint_state = omm_restraints.RestraintParameterState(lambda_restraints=1.0) composable_states = [alchemical_state, restraint_state] else: composable_states = [alchemical_state] @@ -763,10 +920,67 @@ def _get_states( return sampler_states, cmp_states + @staticmethod + def _get_integrator( + integrator_settings: IntegratorSettings, + simulation_settings: MultiStateSimulationSettings, + system: openmm.System, + ) -> openmmtools.mcmc.LangevinDynamicsMove: + """ + Return a LangevinDynamicsMove integrator + + Parameters + ---------- + integrator_settings : IntegratorSettings + Settings controlling the Langevin integrator + simulation_settings : MultiStateSimulationSettings + Settings controlling the simulation. + system : openmm.System + The OpenMM System. + + Returns + ------- + integrator : openmmtools.mcmc.LangevinDynamicsMove + A configured integrator object. + + Raises + ------ + ValueError + If there are virtual sites in the system, but + velocities are not being reassigned after every MCMC move. + """ + steps_per_iteration = settings_validation.convert_steps_per_iteration( + simulation_settings, integrator_settings + ) + + integrator = openmmtools.mcmc.LangevinDynamicsMove( + timestep=to_openmm(integrator_settings.timestep), + collision_rate=to_openmm(integrator_settings.langevin_collision_rate), + n_steps=steps_per_iteration, + reassign_velocities=integrator_settings.reassign_velocities, + n_restart_attempts=integrator_settings.n_restart_attempts, + constraint_tolerance=integrator_settings.constraint_tolerance, + ) + + # Validate for known issue when dealing with virtual sites + # and mutltistate simulations + if not integrator_settings.reassign_velocities: + for particle_idx in range(system.getNumParticles()): + if system.isVirtualSite(particle_idx): + errmsg = ( + "Simulations with virtual sites without velocity " + "reassignments are unstable with MCMC integrators. " + "You can set `reassign_velocities` to ``True`` in the " + "`integrator_settings` to avoid this issue." + ) + raise ValueError(errmsg) + + return integrator + + @staticmethod def _get_reporter( - self, - topology: app.Topology, - positions: openmm.unit.Quantity, + storage_path: pathlib.Path, + selection_indices: npt.NDArray, simulation_settings: MultiStateSimulationSettings, output_settings: MultiStateOutputSettings, ) -> multistate.MultiStateReporter: @@ -775,10 +989,10 @@ def _get_reporter( Parameters ---------- - topology : app.Topology - A Topology of the system being created. - positions : openmm.unit.Quantity - Positions of the pre-alchemical simulation system. + storage_path : pathlib.Path + Path to the directory where files should be written. + selection_indices : npt.NDArray + Array of system particle indices to subsample the system by. simulation_settings : MultiStateSimulationSettings Multistate simulation control settings, specifically containing the amount of time per state sampling iteration. @@ -790,18 +1004,10 @@ def _get_reporter( reporter : multistate.MultiStateReporter The reporter for the simulation. """ - mdt_top = mdt.Topology.from_openmm(topology) - - # Store the selection indices in self to use later - # when storing them in the unit results - self.selection_indices = mdt_top.select(output_settings.output_indices) - - nc = self.shared_basepath / output_settings.output_filename + nc = storage_path / output_settings.output_filename + # The checkpoint file in openmmtools is taken as a file relative + # to the location of the nc file, so you only want the filename chk = output_settings.checkpoint_storage_filename - chk_intervals = settings_validation.convert_checkpoint_interval_to_iterations( - checkpoint_interval=output_settings.checkpoint_interval, - time_per_iteration=simulation_settings.time_per_iteration, - ) if output_settings.positions_write_frequency is not None: pos_interval = settings_validation.divmod_time_and_check( @@ -823,110 +1029,31 @@ def _get_reporter( else: vel_interval = 0 + chk_intervals = settings_validation.convert_checkpoint_interval_to_iterations( + checkpoint_interval=output_settings.checkpoint_interval, + time_per_iteration=simulation_settings.time_per_iteration, + ) + reporter = multistate.MultiStateReporter( storage=nc, - analysis_particle_indices=self.selection_indices, + analysis_particle_indices=selection_indices, checkpoint_interval=chk_intervals, checkpoint_storage=chk, position_interval=pos_interval, velocity_interval=vel_interval, ) - # Write out the structure's PDB whilst we're here - if len(self.selection_indices) > 0: - traj = mdt.Trajectory( - positions[self.selection_indices, :], - mdt_top.subset(self.selection_indices), - ) - traj.save_pdb(self.shared_basepath / output_settings.output_structure) - return reporter - def _get_ctx_caches( - self, - forcefield_settings: OpenMMSystemGeneratorFFSettings, - engine_settings: OpenMMEngineSettings, - ) -> tuple[openmmtools.cache.ContextCache, openmmtools.cache.ContextCache]: - """ - Set the context caches based on the chosen platform - - Parameters - ---------- - forcefield_settings: OpenMMSystemGeneratorFFSettings - engine_settings : OpenMMEngineSettings - - Returns - ------- - energy_context_cache : openmmtools.cache.ContextCache - The energy state context cache. - sampler_context_cache : openmmtools.cache.ContextCache - The sampler state context cache. - """ - # Get the compute platform - # Set the number of CPUs to 1 if running a vacuum simulation - restrict_cpu = forcefield_settings.nonbonded_method.lower() == "nocutoff" - platform = omm_compute.get_openmm_platform( - platform_name=engine_settings.compute_platform, - gpu_device_index=engine_settings.gpu_device_index, - restrict_cpu_count=restrict_cpu, - ) - - energy_context_cache = openmmtools.cache.ContextCache( - capacity=None, - time_to_live=None, - platform=platform, - ) - - sampler_context_cache = openmmtools.cache.ContextCache( - capacity=None, - time_to_live=None, - platform=platform, - ) - - return energy_context_cache, sampler_context_cache - - @staticmethod - def _get_integrator( - integrator_settings: IntegratorSettings, simulation_settings: MultiStateSimulationSettings - ) -> openmmtools.mcmc.LangevinDynamicsMove: - """ - Return a LangevinDynamicsMove integrator - - Parameters - ---------- - integrator_settings : IntegratorSettings - simulation_settings : MultiStateSimulationSettings - - Returns - ------- - integrator : openmmtools.mcmc.LangevinDynamicsMove - A configured integrator object. - """ - steps_per_iteration = settings_validation.convert_steps_per_iteration( - simulation_settings, integrator_settings - ) - - integrator = openmmtools.mcmc.LangevinDynamicsMove( - timestep=to_openmm(integrator_settings.timestep), - collision_rate=to_openmm(integrator_settings.langevin_collision_rate), - n_steps=steps_per_iteration, - reassign_velocities=integrator_settings.reassign_velocities, - n_restart_attempts=integrator_settings.n_restart_attempts, - constraint_tolerance=integrator_settings.constraint_tolerance, - ) - - return integrator - @staticmethod def _get_sampler( integrator: openmmtools.mcmc.LangevinDynamicsMove, reporter: openmmtools.multistate.MultiStateReporter, simulation_settings: MultiStateSimulationSettings, - thermo_settings: ThermoSettings, - cmp_states: list[ThermodynamicState], + thermodynamic_settings: ThermoSettings, + compound_states: list[ThermodynamicState], sampler_states: list[SamplerState], - energy_context_cache: openmmtools.cache.ContextCache, - sampler_context_cache: openmmtools.cache.ContextCache, + platform: openmm.Platform, ) -> multistate.MultiStateSampler: """ Get a sampler based on the equilibrium sampling method requested. @@ -939,16 +1066,14 @@ def _get_sampler( The reporter to hook up to the sampler. simulation_settings : MultiStateSimulationSettings Settings for the alchemical sampler. - thermo_settings : ThermoSettings + thermodynamic_settings : ThermoSettings Thermodynamic settings - cmp_states : list[ThermodynamicState] + compound_states : list[ThermodynamicState] A list of thermodynamic states to sample. sampler_states : list[SamplerState] A list of sampler states. - energy_context_cache : openmmtools.cache.ContextCache - Context cache for the energy states. - sampler_context_cache : openmmtool.cache.ContextCache - Context cache for the sampler states. + platform : openmm.Platform + The compute platform to use. Returns ------- @@ -959,7 +1084,7 @@ def _get_sampler( simulation_settings=simulation_settings, ) et_target_err = settings_validation.convert_target_error_from_kcal_per_mole_to_kT( - thermo_settings.temperature, + thermodynamic_settings.temperature, simulation_settings.early_termination_target_error, ) @@ -989,11 +1114,22 @@ def _get_sampler( ) sampler.create( - thermodynamic_states=cmp_states, sampler_states=sampler_states, storage=reporter + thermodynamic_states=compound_states, + sampler_states=sampler_states, + storage=reporter, + ) + + sampler.energy_context_cache = openmmtools.cache.ContextCache( + capacity=None, + time_to_live=None, + platform=platform, ) - sampler.energy_context_cache = energy_context_cache - sampler.sampler_context_cache = sampler_context_cache + sampler.sampler_context_cache = openmmtools.cache.ContextCache( + capacity=None, + time_to_live=None, + platform=platform, + ) return sampler @@ -1002,7 +1138,6 @@ def _run_simulation( sampler: multistate.MultiStateSampler, reporter: multistate.MultiStateReporter, settings: dict[str, SettingsBaseModel], - standard_state_corr: Optional[Quantity], dry: bool, ): """ @@ -1016,16 +1151,8 @@ def _run_simulation( The reporter associated with the sampler. settings : dict[str, SettingsBaseModel] The dictionary of settings for the protocol. - standard_state_corr : Optional[openff.units.Quantity] - The standard state correction, if available. dry : bool Whether or not to dry run the simulation - - Returns - ------- - unit_results_dict : Optional[dict] - A dictionary containing all the free energy results, - if not a dry run. """ # Get the relevant simulation steps mc_steps = settings_validation.convert_steps_per_iteration( @@ -1065,26 +1192,6 @@ def _run_simulation( if self.verbose: self.logger.info("production phase complete") - if self.verbose: - self.logger.info("post-simulation result analysis") - - analyzer = multistate_analysis.MultistateEquilFEAnalysis( - reporter, - sampling_method=settings["simulation_settings"].sampler_method.lower(), - result_units=unit.kilocalorie_per_mole, - ) - analyzer.plot(filepath=self.shared_basepath, filename_prefix="") - analyzer.close() - - return_dict = analyzer.unit_results_dict - - if standard_state_corr is not None: - return_dict["standard_state_correction"] = standard_state_corr.to( - "kilocalorie_per_mole" - ) - - return return_dict - else: # close reporter when you're done, prevent file handle clashes reporter.close() @@ -1097,130 +1204,118 @@ def _run_simulation( for fn in fns: os.remove(fn) - return None - def run( - self, dry=False, verbose=True, scratch_basepath=None, shared_basepath=None + self, + *, + system: openmm.System, + positions: openmm.unit.Quantity, + box_vectors: Quantity, + selection_indices: npt.NDArray, + alchemical_restraints: bool, + dry: bool = False, + verbose: bool = True, + scratch_basepath: pathlib.Path | None = None, + shared_basepath: pathlib.Path | None = None, ) -> dict[str, Any]: - """Run the absolute free energy calculation. + """ + Run the free energy calculation using a multistate sampler. Parameters ---------- - dry : bool - Do a dry run of the calculation, creating all necessary alchemical - system components (topology, system, sampler, etc...) but without - running the simulation, default False + system : openmm.System + The System to simulate. + positions : openmm.unit.Quantity + The positions of the System. + box_vectors : openff.units.Quantity + The box vectors of the System. + selection_indices : npt.NDArray + Indices of the System particles to write to file. + alchemical_restraints: bool, + Whether or not the system has alchemical restraints. + dry: bool + Do a dry run of the calculation, creating all the necessary + components, but without running the simulation. verbose : bool - Verbose output of the simulation progress. Output is provided via - INFO level logging, default True - scratch_basepath : pathlib.Path - Path to the scratch (temporary) directory space. - shared_basepath : pathlib.Path - Path to the shared (persistent) directory space. + Verbose output of the simulation progress. Output is provided at + the INFO logging level. + scratch_basepath : pathlib.Path | None + Where to store temporary files, defaults to the current working + directory if ``None``. + shared_basepath : pathlib.Path | None + Where to store calculation outputs, defaults to the current working + directory if ``None``. Returns ------- dict - Outputs created in the basepath directory or the debug objects - (i.e. sampler) if ``dry==True``. + Outputs created by the unit, including the debug objects + (i.e. sampler) if ``dry==True`` """ - # 0. Generaly preparation tasks + # Prepare paths & verbosity self._prepare(verbose, scratch_basepath, shared_basepath) - # 1. Get components - alchem_comps, solv_comp, prot_comp, smc_comps = self._get_components() - - # 2. Get settings - settings = self._handle_settings() + # Get the settings + settings = self._get_settings() - # 3. Get OpenMM topology, positions, system, and comp_resids - omm_topology, omm_system, positions, comp_resids = self._get_omm_objects( - settings=settings, - protein_component=prot_comp, - solvent_component=solv_comp, - smc_components=smc_comps, - ) - - # 4. Pre-equilbrate System (Test + Avoid NaNs + get stable system) - positions, box_vectors = self._pre_equilibrate( - omm_system, omm_topology, positions, settings, dry - ) + # Get the components + alchem_comps, solv_comp, prot_comp, small_mols = self._get_components() - # 5. Get lambdas + # Get the lambda schedule lambdas = self._get_lambda_schedule(settings) - # 6. Add restraints - # Note: when no restraint is applied, restrained_omm_system == omm_system - ( - restraint_parameter_state, - standard_state_corr, - restrained_omm_system, - restraint_geometry, - ) = self._add_restraints( - omm_system, - omm_topology, - positions, - alchem_comps, - comp_resids, - settings, - ) - - # 7. Get alchemical system - alchem_factory, alchem_system, alchem_indices = self._get_alchemical_system( - topology=omm_topology, - system=restrained_omm_system, - comp_resids=comp_resids, - alchem_comps=alchem_comps, - alchemical_settings=settings["alchemical_settings"], + # Get the compute platform + restrict_cpu = settings["forcefield_settings"].nonbonded_method.lower() == "nocutoff" + platform = omm_compute.get_openmm_platform( + platform_name=settings["engine_settings"].compute_platform, + gpu_device_index=settings["engine_settings"].gpu_device_index, + restrict_cpu_count=restrict_cpu, ) - # 8. Get compound and sampler states + # Get compound and sampler states sampler_states, cmp_states = self._get_states( - alchem_system, - positions, - box_vectors, - settings, - lambdas, - solv_comp, - restraint_parameter_state, + alchemical_system=system, + positions=positions, + # convert the box vectors to vec3 from openff + box_vectors=make_vec3_box(box_vectors), + thermodynamic_settings=settings["thermo_settings"], + lambdas=lambdas, + solvent_component=solv_comp, + alchemically_restrained=alchemical_restraints, ) - # 9. Create the multistate reporter & create PDB - reporter = self._get_reporter( - omm_topology, - positions, - settings["simulation_settings"], - settings["output_settings"], + # Get the integrator + integrator = self._get_integrator( + integrator_settings=settings["integrator_settings"], + simulation_settings=settings["simulation_settings"], + system=system, ) - # Wrap in try/finally to avoid memory leak issues try: - # 10. Get context caches - energy_ctx_cache, sampler_ctx_cache = self._get_ctx_caches( - settings["forcefield_settings"], settings["engine_settings"] - ) - - # 11. Get integrator - integrator = self._get_integrator( - settings["integrator_settings"], - settings["simulation_settings"], + # Create or get the multistate reporter + reporter = self._get_reporter( + storage_path=self.shared_basepath, + selection_indices=selection_indices, + simulation_settings=settings["simulation_settings"], + output_settings=settings["output_settings"], ) - # 12. Get sampler + # Get sampler sampler = self._get_sampler( - integrator, - reporter, - settings["simulation_settings"], - settings["thermo_settings"], - cmp_states, - sampler_states, - energy_ctx_cache, - sampler_ctx_cache, + integrator=integrator, + reporter=reporter, + simulation_settings=settings["simulation_settings"], + thermodynamic_settings=settings["thermo_settings"], + compound_states=cmp_states, + sampler_states=sampler_states, + platform=platform, ) - # 13. Run simulation - unit_result_dict = self._run_simulation( - sampler, reporter, settings, standard_state_corr, dry + # Run simulation + self._run_simulation( + sampler=sampler, + reporter=reporter, + settings=settings, + dry=dry, ) finally: @@ -1229,15 +1324,15 @@ def run( # clear GPU context # Note: use cache.empty() when openmmtools #690 is resolved - for context in list(energy_ctx_cache._lru._data.keys()): - del energy_ctx_cache._lru._data[context] - for context in list(sampler_ctx_cache._lru._data.keys()): - del sampler_ctx_cache._lru._data[context] + for context in list(sampler.energy_context_cache._lru._data.keys()): + del sampler.energy_context_cache._lru._data[context] + for context in list(sampler.sampler_context_cache._lru._data.keys()): + del sampler.sampler_context_cache._lru._data[context] # cautiously clear out the global context cache too for context in list(openmmtools.cache.global_context_cache._lru._data.keys()): del openmmtools.cache.global_context_cache._lru._data[context] - del sampler_ctx_cache, energy_ctx_cache + del sampler.sampler_context_cache, sampler.energy_context_cache # Keep these around in a dry run so we can inspect things if not dry: @@ -1245,41 +1340,193 @@ def run( if not dry: nc = self.shared_basepath / settings["output_settings"].output_filename - chk = settings["output_settings"].checkpoint_storage_filename - unit_result_dict["nc"] = nc - unit_result_dict["last_checkpoint"] = chk - unit_result_dict["selection_indices"] = self.selection_indices - - if restraint_geometry is not None: - unit_result_dict["restraint_geometry"] = restraint_geometry.model_dump() - - return unit_result_dict + chk = self.shared_basepath / settings["output_settings"].checkpoint_storage_filename + return { + "trajectory": nc, + "checkpoint": chk, + } else: return { - # Add in various objects we can used to test the system - "debug": { - "sampler": sampler, - "system": omm_system, - "restrained_system": restrained_omm_system, - "alchem_system": alchem_system, - "alchem_indices": alchem_indices, - "alchem_factory": alchem_factory, - "positions": positions, - } + "sampler": sampler, + "integrator": integrator, } def _execute( self, ctx: gufe.Context, - **kwargs, + *, + setup_results, + **inputs, ) -> dict[str, Any]: log_system_probe(logging.INFO, paths=[ctx.scratch]) - outputs = self.run(scratch_basepath=ctx.scratch, shared_basepath=ctx.shared) + system = deserialize(setup_results.outputs["system"]) + positions = to_openmm(np.load(setup_results.outputs["positions"]) * offunit.nanometer) + selection_indices = setup_results.outputs["selection_indices"] + box_vectors = setup_results.outputs["box_vectors"] + + if setup_results.outputs["restraint_geometry"] is not None: + alchemical_restraints = True + else: + alchemical_restraints = False + + outputs = self.run( + system=system, + positions=positions, + box_vectors=box_vectors, + selection_indices=selection_indices, + alchemical_restraints=alchemical_restraints, + scratch_basepath=ctx.scratch, + shared_basepath=ctx.shared, + ) + + return { + "repeat_id": self._inputs["repeat_id"], + "generation": self._inputs["generation"], + "simtype": self.simtype, + **outputs, + } + + +class BaseAbsoluteMultiStateAnalysisUnit(gufe.ProtocolUnit, AbsoluteUnitMixin): + @staticmethod + def _analyze_multistate_energies( + trajectory: pathlib.Path, + checkpoint: pathlib.Path, + sampler_method: str, + output_directory: pathlib.Path, + dry: bool, + ): + """ + Analyze multistate energies and generate plots. + + Parameters + ---------- + trajectory : pathlib.Path + Path to the NetCDF trajectory file. + checkpoint : pathlib.Path + The name of the checkpoint file. Note this is + relative in path to the trajectory file. + sampler_method : str + The multistate sampler method used. + output_directory : pathlib.Path + The path to where plots will be written. + dry : bool + Whether or not we are running a dry run. + """ + reporter = multistate.MultiStateReporter( + storage=trajectory, + # Note: openmmtools only wants the name of the checkpoint + # file, it assumes it to be in the same place as the trajectory + checkpoint_storage=checkpoint.name, + open_mode="r", + ) + + analyzer = multistate_analysis.MultistateEquilFEAnalysis( + reporter=reporter, + sampling_method=sampler_method, + result_units=offunit.kilocalorie_per_mole, + ) + + # Only create plots when not doing a dry run + if not dry: + analyzer.plot(filepath=output_directory, filename_prefix="") + + analyzer.close() + reporter.close() + return analyzer.unit_results_dict + + def run( + self, + *, + trajectory: pathlib.Path, + checkpoint: pathlib.Path, + dry: bool = False, + verbose: bool = True, + scratch_basepath: pathlib.Path | None = None, + shared_basepath: pathlib.Path | None = None, + ) -> dict[str, Any]: + """Analyze the multistate simulation. + + Parameters + ---------- + trajectory : pathlib.Path + Path to the MultiStateReporter generated NetCDF file. + checkpoint : pathlib.Path + Path to the checkpoint file generated by MultiStateReporter. + dry : bool + Do a dry run of the calculation, creating all necessary hybrid + system components (topology, system, sampler, etc...) but without + running the simulation. + verbose : bool + Verbose output of the simulation progress. Output is provided via + INFO level logging. + scratch_basepath: pathlib.Path | None + Where to store temporary files, defaults to current working directory + shared_basepath : pathlib.Path | None + Where to run the calculation, defaults to current working directory + + Returns + ------- + dict + Outputs created in the basepath directory or the debug objects + (i.e. sampler) if ``dry==True``. + """ + # Prepare paths & verbosity + self._prepare(verbose, scratch_basepath, shared_basepath) + + # Get the settings + settings = self._get_settings() + + # Energies analysis + if verbose: + self.logger.info("Analyzing energies") + + energy_analysis = self._analyze_multistate_energies( + trajectory=trajectory, + checkpoint=checkpoint, + sampler_method=settings["simulation_settings"].sampler_method.lower(), + output_directory=self.shared_basepath, + dry=dry, + ) + + return energy_analysis + + def _execute( + self, + ctx: gufe.Context, + *, + setup_results, + simulation_results, + **inputs, + ) -> dict[str, Any]: + log_system_probe(logging.INFO, paths=[ctx.scratch]) + + pdb_file = setup_results.outputs["pdb_structure"] + selection_indices = setup_results.outputs["selection_indices"] + restraint_geometry = setup_results.outputs["restraint_geometry"] + standard_state_corr = setup_results.outputs["standard_state_correction"] + trajectory = simulation_results.outputs["trajectory"] + checkpoint = simulation_results.outputs["checkpoint"] + + outputs = self.run( + trajectory=trajectory, + checkpoint=checkpoint, + scratch_basepath=ctx.scratch, + shared_basepath=ctx.shared, + ) return { "repeat_id": self._inputs["repeat_id"], "generation": self._inputs["generation"], "simtype": self.simtype, + # We re-include things here also to make + # life easier when gathering results. + "pdb_structure": pdb_file, + "trajectory": trajectory, + "checkpoint": checkpoint, + "selection_indices": selection_indices, + "restraint_geometry": restraint_geometry, + "standard_state_correction": standard_state_corr, **outputs, } diff --git a/openfe/protocols/openmm_afe/equil_binding_afe_method.py b/openfe/protocols/openmm_afe/equil_binding_afe_method.py index c619f285c..28054a14d 100644 --- a/openfe/protocols/openmm_afe/equil_binding_afe_method.py +++ b/openfe/protocols/openmm_afe/equil_binding_afe_method.py @@ -55,9 +55,19 @@ OpenMMEngineSettings, OpenMMSolvationSettings, ) -from openfe.protocols.openmm_utils import settings_validation, system_validation +from openfe.protocols.openmm_utils import ( + settings_validation, + system_validation, +) -from .abfe_units import AbsoluteBindingComplexUnit, AbsoluteBindingSolventUnit +from .abfe_units import ( + ABFEComplexAnalysisUnit, + ABFEComplexSetupUnit, + ABFEComplexSimUnit, + ABFESolventAnalysisUnit, + ABFESolventSetupUnit, + ABFESolventSimUnit, +) from .afe_protocol_results import AbsoluteBindingProtocolResult due.cite( @@ -422,36 +432,58 @@ def _create( # Get the name of the alchemical species alchname = alchem_comps["stateA"][0].name + unit_classes = { + "solvent": { + "setup": ABFESolventSetupUnit, + "simulation": ABFESolventSimUnit, + "analysis": ABFESolventAnalysisUnit, + }, + "complex": { + "setup": ABFEComplexSetupUnit, + "simulation": ABFEComplexSimUnit, + "analysis": ABFEComplexAnalysisUnit, + }, + } - # Create list units for complex and solvent transforms - - solvent_units = [ - AbsoluteBindingSolventUnit( - protocol=self, - stateA=stateA, - stateB=stateB, - alchemical_components=alchem_comps, - generation=0, - repeat_id=int(uuid.uuid4()), - name=(f"Absolute Binding, {alchname} solvent leg: repeat {i} generation 0"), - ) - for i in range(self.settings.protocol_repeats) - ] - - complex_units = [ - AbsoluteBindingComplexUnit( - protocol=self, - stateA=stateA, - stateB=stateB, - alchemical_components=alchem_comps, - generation=0, - repeat_id=int(uuid.uuid4()), - name=(f"Absolute Binding, {alchname} complex leg: repeat {i} generation 0"), - ) - for i in range(self.settings.protocol_repeats) - ] + protocol_units: dict[str, list[gufe.ProtocolUnit]] = {"solvent": [], "complex": []} + + for phase in ["solvent", "complex"]: + for i in range(self.settings.protocol_repeats): + repeat_id = int(uuid.uuid4()) + + setup = unit_classes[phase]["setup"]( + protocol=self, + stateA=stateA, + stateB=stateB, + alchemical_components=alchem_comps, + generation=0, + repeat_id=repeat_id, + name=f"ABFE Setup: {alchname} {phase} leg: repeat {i} generation 0", + ) + + simulation = unit_classes[phase]["simulation"]( + protocol=self, + # only need state A & alchem comps + stateA=stateA, + alchemical_components=alchem_comps, + setup_results=setup, + generation=0, + repeat_id=repeat_id, + name=f"ABFE Simulation: {alchname} {phase} leg: repeat {i} generation 0", + ) + + analysis = unit_classes[phase]["analysis"]( + protocol=self, + setup_results=setup, + simulation_results=simulation, + generation=0, + repeat_id=repeat_id, + name=f"ABFE Analysis: {alchname} {phase} leg, repeat {i} generation 0", + ) + + protocol_units[phase] += [setup, simulation, analysis] - return solvent_units + complex_units + return protocol_units["solvent"] + protocol_units["complex"] def _gather( self, protocol_dag_results: Iterable[gufe.ProtocolDAGResult] @@ -463,7 +495,7 @@ def _gather( for d in protocol_dag_results: pu: gufe.ProtocolUnitResult for pu in d.protocol_unit_results: - if not pu.ok(): + if ("Analysis" not in pu.name) or (not pu.ok()): continue if pu.outputs["simtype"] == "solvent": unsorted_solvent_repeats[pu.outputs["repeat_id"]].append(pu) diff --git a/openfe/protocols/openmm_afe/equil_solvation_afe_method.py b/openfe/protocols/openmm_afe/equil_solvation_afe_method.py index 7abcde6f7..72d129f41 100644 --- a/openfe/protocols/openmm_afe/equil_solvation_afe_method.py +++ b/openfe/protocols/openmm_afe/equil_solvation_afe_method.py @@ -63,8 +63,12 @@ from ..openmm_utils import settings_validation, system_validation from .afe_protocol_results import AbsoluteSolvationProtocolResult from .ahfe_units import ( - AbsoluteSolvationSolventUnit, - AbsoluteSolvationVacuumUnit, + AHFESolventAnalysisUnit, + AHFESolventSetupUnit, + AHFESolventSimUnit, + AHFEVacuumAnalysisUnit, + AHFEVacuumSetupUnit, + AHFEVacuumSimUnit, ) due.cite( @@ -445,36 +449,58 @@ def _create( # Get the name of the alchemical species alchname = alchem_comps["stateA"][0].name - # Create list units for vacuum and solvent transforms - solvent_units = [ - AbsoluteSolvationSolventUnit( - protocol=self, - stateA=stateA, - stateB=stateB, - alchemical_components=alchem_comps, - generation=0, - repeat_id=int(uuid.uuid4()), - name=(f"Absolute Solvation, {alchname} solvent leg: repeat {i} generation 0"), - ) - for i in range(self.settings.protocol_repeats) - ] - - vacuum_units = [ - AbsoluteSolvationVacuumUnit( - # These don't really reflect the actual transform - # Should these be overriden to be ChemicalSystem{smc} -> ChemicalSystem{} ? - protocol=self, - stateA=stateA, - stateB=stateB, - alchemical_components=alchem_comps, - generation=0, - repeat_id=int(uuid.uuid4()), - name=(f"Absolute Solvation, {alchname} vacuum leg: repeat {i} generation 0"), - ) - for i in range(self.settings.protocol_repeats) - ] + unit_classes = { + "solvent": { + "setup": AHFESolventSetupUnit, + "simulation": AHFESolventSimUnit, + "analysis": AHFESolventAnalysisUnit, + }, + "vacuum": { + "setup": AHFEVacuumSetupUnit, + "simulation": AHFEVacuumSimUnit, + "analysis": AHFEVacuumAnalysisUnit, + }, + } + + protocol_units: dict[str, list[gufe.ProtocolUnit]] = {"solvent": [], "vacuum": []} + + for phase in ["solvent", "vacuum"]: + for i in range(self.settings.protocol_repeats): + repeat_id = int(uuid.uuid4()) + + setup = unit_classes[phase]["setup"]( + protocol=self, + stateA=stateA, + stateB=stateB, + alchemical_components=alchem_comps, + generation=0, + repeat_id=repeat_id, + name=f"AHFE Setup: {alchname} {phase} leg: repeat {i} generation 0", + ) + + simulation = unit_classes[phase]["simulation"]( + protocol=self, + # only need state A & alchem comps + stateA=stateA, + alchemical_components=alchem_comps, + setup_results=setup, + generation=0, + repeat_id=repeat_id, + name=f"AHFE Simulation: {alchname} {phase} leg: repeat {i} generation 0", + ) + + analysis = unit_classes[phase]["analysis"]( + protocol=self, + setup_results=setup, + simulation_results=simulation, + generation=0, + repeat_id=repeat_id, + name=f"AHFE Analysis: {alchname} {phase} leg, repeat {i} generation 0", + ) + + protocol_units[phase] += [setup, simulation, analysis] - return solvent_units + vacuum_units + return protocol_units["solvent"] + protocol_units["vacuum"] def _gather( self, protocol_dag_results: Iterable[gufe.ProtocolDAGResult] @@ -486,7 +512,7 @@ def _gather( for d in protocol_dag_results: pu: gufe.ProtocolUnitResult for pu in d.protocol_unit_results: - if not pu.ok(): + if ("Analysis" not in pu.name) or (not pu.ok()): continue if pu.outputs["simtype"] == "solvent": unsorted_solvent_repeats[pu.outputs["repeat_id"]].append(pu) diff --git a/openfe/protocols/openmm_utils/serialization.py b/openfe/protocols/openmm_utils/serialization.py index 9b624291d..e0f2c28e5 100644 --- a/openfe/protocols/openmm_utils/serialization.py +++ b/openfe/protocols/openmm_utils/serialization.py @@ -1,6 +1,11 @@ import os import pathlib +from gufe.settings.typing import NanometerArrayQuantity +from openff.units import Quantity +from openmm import Vec3 +from openmm import unit as ommunit + def serialize(item, filename: pathlib.Path): """ @@ -62,3 +67,23 @@ def deserialize(filename: pathlib.Path): item = XmlSerializer.deserialize(serialized_thing) return item + + +def make_vec3_box(dimensions: NanometerArrayQuantity) -> Vec3: + """ + Convert an OpenFF box dimensions Quantity back into Vec3 format. + + Parameters + ---------- + dimensions : gufe.settings.typing.NanometerArrayQuantity + United array to turn to Vec3 format. + + Returns + ------- + openmm.Vec3 + The input array in Vec3 format. + """ + return [ + Vec3(float(row[0]), float(row[1]), float(row[2])) * ommunit.nanometer + for row in dimensions.m_as("nanometer") + ] diff --git a/openfe/tests/data/openmm_afe/ABFEProtocol_json_results.json.gz b/openfe/tests/data/openmm_afe/ABFEProtocol_json_results.json.gz index 8d4fe9d15de831444982aab26f735c89a524f618..5cbc5d9fb06b13cc06e6e4964e25b0794f0223ee 100644 GIT binary patch literal 110381 zcmX_ncRW@9AHVBf*QJXq>)IC?A)BmpFHzaqGOx|OW=I)vag|*mt56Xkdz6vuTG`oq zR*JY}kNW(M@8kFT{c}3!cF#HQ_xtr+uQ&J;U@$&6J5MSwMpH-IIKVg1*Vz~E;C0W} z$05M=UJyR;p49mT)$bO%==qMXQfHQlAm>#Y@0$q>ujK8gNcibQmYZw0s6`j#_*iaf zMx2TUr6<2{%3Ei*UTFI(LTQP1*B!?N-QV2bpIrMKc5u}6@7J$QccWL=LskE+dRc76 zUy`2r@GIC(Ug7pU!)aN`Y5i9GYtBIjmBYhb=~wCx=RPYx`a5?Vx=LBgyB@mpbMNc@ zzvP=kuM8iZ)wZbrJ@)+1K;pl!)1C2OL#w__heNNnx)%&}lu6HUzkbX;cTbsRX%ACr z=evGlbG_Zr$o^n;kx%2SukT=I^x*b?+siS_$!E>SKfcs#eRux1lJi3Ak@t17gKa1V z8T!QkZ_~eToyx=wAu2%0mn3uYo9k96BSN~ze zV3h5_XtuDYQQoyIi*FkheO1$|JHEft?|nHMx@UT|VR7i)e>{t=YX3z0Hg*>ClK;~! zVqBxhAGe$Y4LGc=Y=o{)UVdcvxMI=hO`h+!HS4`ylZX#SwJSpsJI4_9|90jbvMRW1 zCF^W6R)4M~HN|glJYXB`uf>t^y1`;z<(Og)z1FCG;v%bLh(2{y(g|a?_QASH z=$KC-C3NblWcTh2(8@n=@;3aj^CB;+l_doGWuWK{=f4Hj-u1QJ>HcqqiwX6w`jU!j z&Ohs;XvaP8Gibf(E}!UV_cc(-E~d0(u4-<^xh|T#HUZt(cyzR&+8;R44qH;tiph%n zGb5ZYO^Lv27%j6a@PTMPOJ95uxwN(mWi8P|FHgv)M79suoSwcq`Mb<;5PbFQFS?gT1vYiOuYY95Ud%aM_?#2yqr>I6RzIbBv5*su2 zZcv7>5BuCeO?nu;Xd`>+l+ONZ^e%NbbD4bDeQl-*&eo>aw749~JAVl74W&D@xa?(X z#n3W}vFD}IWQQeXwEWqdXk+WiJ^!=N4OD5~(Buu_tH<_d_da$WF6gb2s|I(ix62fX7q^zzU4P7lyd>R{zf1SOA+e&duH5>Tep8a%oluEpGxMSR^muIVFp(fqsuOp$;cWHoI z7S|7fPk*&+m3u6XgwEgn8dZ4w&`4Xk?0w+{JbruPN>`g4vxHDD{82&>f#Ky#E_g@{1Y@0 zmfcn-m1cSO_wQUGDpBZD!YB^Q%n-lWn=)-|d?dd_GYJ_yTUKlB1!B37O%-RxvwkO3 zrNQ{f(`k4NVTtqQ@?o5*OG(dG=WK(5ixRjX&~bi+QUXhuo>=V^T0CtIfq=GGnBjhL zH)X=?c?5Z2WRrBx_M>lGmZOKGEeQvt2FqAxN!Mxb{Zn$Qb#Ha3#LVHi7&F8ng_?PU zMr&*D#eIuF?EKRGeJMuRD-q#8Efu;?P*nWUFr8IX9Fjbexiia0s$_d3p)*e9stW;& znF=B&NUfzQ>wb^9QZBf&>_G2Yk9MuK3&Hp}7(ZX%Yr6X z4%U6A_iID(9WO~dJ-W$#P<8Y%#4_Yt=zsJns7MKQLV+636q1%Utt0fk`%1LVSIM2c zbr|8$j7(utTT^=+J4?&w#0*)~h6}#EK%Db9)v@?gnYrEckb@R7bX)RI3q_b6Lp5Rn z5%PIeHKso7HXm zlq~Rv&F$#{F2zxxUCvo=?o$fl_bi)oLTntQxl<5UuVJgL{QKTkDVe+o4hlh%T0ify zA=?VY2-un>+)3rQ*!f{u;-qrSt)8Dq#Pa`E43CWFB~>cT1#}L};Poc6HOj7it{7VN zOOqVW`lvpYSsxWV+HaC;o#Dek+NF+N67h~B+}fAN&^n)1;uLOypD8~mFkJE?NhR-(vG)$Ict@*tX<^O(+$vue}7cdDb<=gantf*~ZeX<*7o1Ymz zyNcK5ukV}wdt_mCQWHukh0*@08F!axZ7{4i)W1Los{OSe??{!d3n8n{MQz&4l-2M~ zZPAcbXa0Bb9Jt^VYIr%cH?Mpn!%AG?9^0Io2K(LCdp3`Po2f~=EW}0r*P)Vsr(4)I zZA4Eq;!~xN0TrLajJlOkA!pX#Li4UCps4;yCSrdLWwr|;>+OL88xspLHLSzK**UYA z41nPDFXVu^^j40Y&&3z8mhzu%Bk!i@J!r@yPiEQiNWZn=idPKz%o z8Rep}uPqahcY^AQ$_$&IKD(wc@B@7xz*(UC_Nnmt@aRkJnZuU@g0N4=;FO+`mnviL z93lPu@xSWFB%155jU=! zL`=i3zg3!Q-SiE{(GV&`l2AupCXfBBO!aUFzvkJlSv4H~1&&aI!3;!K6mPPB)RcR1{$K(F`C={<4V3j+}B(DdaE z_OxCdYk6ke(Bja<-pG{LLuuNXI#!P#$tO$BZ?we=>eL&?+yifFBh;C>BOW!r(KYHw zuB9YQF+*%R3@sg};CJf*=c(G)V6<_kb^CC%kTQEgXv#n9MD+^nV5bO4fW?QmnuZs$ zvx)}$cA89!D@<5u8!IXGW`9(Ec90!4{rcejFSTXVNgJc+gvp!R*L>&({`#=#?okl4Rw&6`qe zRQ_{s^@7t=l_P=`qM$O+!@`gDl{U7iz?#G>T+7e#M3kuAde~VBBU~YpVuNHNxwRl< zrN?C?zuyAlT>|4z3U%dK2Ql8l3a#w*{4@SMB*ta4&kg~LZ>3^y&K!Er0OD4}i=k55 zgh4AkwGzo)g$q~sju$?63c((>$}xbCF2>37T(4z=CEvDjn4#&sZC&qXRWFwDA+sr` z7C+FIn|m1tb-U-4rBO-tudDUS7z1DV5?gMrW*@b5oYhWwVQUgFMCqKpfA$q1WBnV? zdTOwXP@?0{YNP@kN4=qU=V9wRPYCHi!7j>iw$7Vqi}C+Gb27qbj89Rxxqio(N5|Vh z+E65&?cU{FtyVY30|5ucaM5)={;B1TlL${r_?g=7wOUb*Ht8#brC%n1ZLw^e7Lq7T z_v-g?0JR5%rs?6hrj&bZtChvvDbnsjcslRQ&aSmSz&9KK-;iAITJ00sv}p;G#84Op4urc7C=?P3zy-0|{wzj+&`PFUeCM~2P-z2YQ7oh%BJ z{ZWP-Q1*j;lsC6?RyE(-@;z+nh)+3sdlSnHN!1Zmzo3`n7FRQiq@AgJo(I6r=raOS zX;{yu`~e#onGYkDE89h5)h?G?iP^klN2Lm{mdyFyi`O_^d3DUkwRk~|X=aBWGNeZr z@km)aL*)Ej;nIaL0;TyPrqV!2yuJEDTY`Eb)uk;|SJ#1#^XFX|;y_Xb$uDz<4XGsE zTEvw42;{_5-Es19*lI}e8t&hgH(II|M##|W{@1DzR`GrKYiq-lpPqbZTcu(Rw@rGm zght%hIgnZ{wL;i4Z^N(G1VTtgS9m$^l$hob3TCw!k%J}RWY5YJ zvW1O!?cwU;?Cs&bBpZau%XT*SQWTQZ6GU$*L;te9028vhhH?JxUg)B9NZAI4JycDA zE+pmAUK!Cyts*1qowHpzhh@tucW8hQ*Mnr)(N8!sQpc-Pts zmJE(=eQ-e+l~+h`k{H6zMiZU5wSk0L!{$-HiP56_R#SV2u{&t>_RQ|#aDUVE(KEpO zp4S4iSpBqfX%mQr27H+9E3;V0&De|&ZI#Il;R0xGN>}o_Egp_LRO`?ADP@Pe+_ zn+{QvnfD5_^7dL)n*v!2dO_%~f08YSBW*=A@&Ly=QaB0^KU!EF72G@xyZaWbNNG&+ zvocrvbg%QU-XN5Q8qp|(Tnf1g z{$=**0megAQ(2N3wh4bkq1=Oz7S;l3pr5+P$e&y{0jVBrPIBY+qDi{%`okB1)~pAx z6mR5x-w17o7Sa)^X(YT-oi3jB^7gK2z9Cjl(%3wA=-|=Um5F@gltpGwp z-`tc{?ykfs5L2y4rrrAB3lFF7`^wWNS!ZMYP)mU9j9&&EOFQ&Jv4 zs0f>++4~!%s=F8@FxUDrMBOF|+cJ4*i8|UM0tPTIL!iFMqt##={hoz9~21Km%rOaq>$*2WXH&{+syD()ys)k%cZfB1HWn<#bwvV zQtQ;;`czbnu=Emt^mo~1a^VZ$$!fqz!`{7xQ9qspnl#bb6`TUxI95I}s@G3PrtD3) zgSIk0wos#H$_iM=@cI*gb*M!w5;OFq2upRJ3?n+zSVw{87=(~*^D>X`O!wBb1xq9A zxAe7(8DaJxBoExn3YeR#pTFj7# zxtoBjv{nOMONJzA@`;Y#^}r?qtMz&f(9{fFb0Wav6noWu$vuay4Oj@Nl{^Z-#Z{dE za=I_j^*=aGTi9xu`lJJx!fY#XMLe4NIXC+g7@y8fM3_4nM%ct>0z$x$lNIkjrR)&I ze!O7$QX3JuE}0l*=FSY?D4(zl#`tO0?ibn_=l&c}BnP~{I@v!@_tjorW{uB5UgqY} z2yL+PhYZG~E!W;5OD%AS>#yOmZ%Ooo@*Ac=Z)B+#2ZWr%iJf{A2`^*@{eW(HNL!`3 z2b*z$wi|;j2cjLjf#;oxr~H+KGeZKO$H}7gVI9hd=XuDaa)M%Qy#-44E`)UNPShfQ z{fto}yXWHK+AUgk>3^@{{jA1DX4e20)VPU=EK(qVeE!G~H$#esPSD7VlYRtL!UE^v zT`unM@*-w?>@v^CLqJxU>S(dPYpYjjT;L!R`2hb?p{-PWl?|dJA{Vd{+qDK z|FhyC(Qf&`yK_1euf%xl6HinC%}Q%nl35S59z56-Xp5 ziw*9@-ZjQkmh~yF3!i69UUJ5NzxY5=!Dr&he^xgU&D~rP4*ac$d>cT~oPi={0ec%j zxNj)`^z<&rL{)3)eSdVZ9U&=g9Q>PFn3;1{$QEYxX=5pY_`UWSdhECZXuUo2ySbts0I(B@p<&AM}vfvjRb$ zgK3LbK)i785Q4RND-%F-w}p_A%)sV^jyeHbIxw*ZvSk-Uww*DPDzD^7=(xuD-Yqm# zgyZ@s707hn<_8Y^x)K&OWUVfHQ@XaD>j90BK>r*gq>!F`X8uCZaQ z&7`5%&ssKvi=@929CzK1>ES4sn=<%+`r66}VxfwB4C$Z^nmy_@y9-oT^VdZvu=U}a zvGNf_D~$rq(rr2$jI}_BZ-YtGB0gW9ghA9cpUnXM zP~7JBg@Eem)+k~xl=WL8D#VZokjv+kMnz~7%&C@0sGaK-r@u+xLHEOO!r9%PzH z3}brxTZZ~sH5qxUm(lr%^pP1NTGt5d1I1r)eTO4{S4LE1smaCMFisV?l=w_d)D{TZ zjVG!K9~x%SO*n8M(AAVzSO5Kc{$cH{(})nN8@D*%eXM_8CHted)z3R0KHxho)zyS3 z;0iebnsg7(Pi$}Marc(2^;3#{^c3Jz0#C%F|9uD;Eyn{uGka>-w5rX;OX92W(eF&@ z9{EZP?U~yl@GpDmA>H5VoGu10$akai^Oc`4?6Qd|8GT6EKv!qbCxtMH_6 z`8DLub;U(3oRf364_^hEinEJ!*!TM3Z8O159K8Yq=|H3DjUZ1(&wZ>?w{@5f^YLk#t>Pq~}VP_j;3np9%L(%DcOM z>cMA|-Td4T4?Z5|un2YHvg|iRfjvS!S$!fpsQF1@LIG8gI&lT}MSwVkNRdcEO?^@b z_opEaz37(7)onWQqT|VtH`@PxQCSIRxSohiek1LmSU=Kbt*F#(bDMuQz{G?ikjI=~ z=zn9|;SaD2-{J@mU>hXrxKkd?s2h`U@TH2)vAcbsz7qm_$Brq&E$ctQ06eBKoGk40AjY>Ti{UwH?GlF-jQtn&sJDnC({OYvmnYWZ>cPeYy z<r`a!4AoeXR3=Ob^s1V1*G zhP~?N+aO)vk%j-)p_LejsY(l!`)0k_!6?vDdcWE+M5zHu$n%gUg6t2G^tb9bw5p8G z-rMuc|K?&n4KkX{k7WJG%xYtB-!iW+;5}Nb#6PF+-3It6eY|jz;}PjC469;}%2pVO zrqFYcD>r3vGI%!)0GSskS@D9w!=sZo5O|uGt-RFa`tENyyjuPMO>^{0aR9 zp12$bMY(QD&^Wd7PGM=+5A{F5p|N${*~YN4uuACFg3JAWQ;&=M%v47Fr0oX}V=8l( zl7>caKBStSMs!u1MZ`uU3BD;$uu}(_feiGw=GnAB8kGpw>459ExgQ*TyQ~C%htjiw zlV<}SSK>eBy}L3I{rS5r+`*HDT$!$flR5h>jnKjqU4vRuWK2T4_5OQy(@qjr-<{hU zP7SvE!^|4^<)$&u=qwkj;-ko9Cx<_!#*w`uM5j``#s2Fbh~YbB$h4pUc|;<34i&~M66%YsEY1dd3b_8TRbW9*-JKmuKfxkZu5OMQnR;k!HVTI;e zKWu95)ihsFH%_LxlJ+rbf_+RB4C`zDd-t_$jG2QCgYf7iJNHj{J&gNA#+3_~mCW3O z{nmj;OB=ue+D?oNo{LYOm!&g*Fd!!zcqjY#{uOKEIk9m?u?bHmd^5VrFw}9C zCcZq`Qa0n1bU)uYrHM5sSAGf2LEcW=L0|=R7NevlqSvL$ou+z^0%_t4^K0x3ywhCO z@C@@@kVPs0dklU!86J1f@Ga_YIu~2T2R8h^AG6;pi`6;9X}qzRpk5iWrxP{tIt;5&P5o-6usDDjf%GZtphiJ%O(n;V#`vIt?2l8upV}rauy+4wB_v8v((}BTr;>Bc|4HzZx9uN1uAH+= zKkV&(xb1L^B>Dyb7Q`4-?{BPCHQ40M)A3`zKjmJMz|nd(5^6jN=+P$til29YiF+rt zwo`YjP)c=v`0eA^+Xi{okhF@qV_nwrN;WpYdDVmKICYg*UXI_MBjS6icfKDXiK5+F zG2bCJy`MsH4=eMI745A2FIG<^Bs+QfxKzEM>6AKn_M^Uf%OXttV|iJqQ6~^a5OaJw z+f3nbFwY5+UU^TJb;*N83zV#cD780ftuVnEw{eu;;b-D! z?4(>@FJ-5wWY(iWj255P%KeV85m#H8?3v5M3I+R={FbEfv61z>shKmh@TI>e_VPm) z#KdA*F65MBzFzY+W1ZjC(Y|xdH|@Ll#qnL8O)}H7c&VS|w7urpRbpVoPGHGpY!;|I;{(JYmETsHWeKJULTb9)^ zDy2YK!2jeGjK_>Yu(~T_VsMz^?l1?a2GYh#Rqgd6z@0%%Of1hmquhl3rG$I3@WnMi z7f#E^}5S`8E7I2!m9cETF#)B5c7Uft0i+$>?BV_!*=r6EMPs8?{l(J&` zjY$vgE9o!O3$;6e4^Zuztr=Zg3)y^Qb0Xrz?AaUSO7r0RW{Iwz=U-9-)+V?b_#t2r z+2*6O87r>4d`^~@Rqb&l)lNk*Q`FMziWBlDF(p5e%%=q^c_$2L0|(&i7pG1>ttujQ>D*|6WS+*=|B$J*T8g+VvEhX5 zDVI}c_-R3B+~M<6I(qQa33?)js96i1@Yw7 zz&A|Z)3-l=f+SR_1gA_zu3uz^16Mb!U?UY$%p0V&-%dykcYwMZZJ0z?wa8W}B4{s2 zmK8P7Y3!B9=`{W;ZVn%>kn7d}<*560OUO5j1r6#ETG*5#%-<8CX0{7XYD` zS|0&JTtu;usr~`;TXNvg0%hG6=&g?S5i46rYQq%xGxN;$`7jWMdV#81L@*Fd#eK$a z3`vG)g6*wo0@w+z-h#6ulvx6O-0-jpnS5 z#H4~L!#W_e+j={RwT%NN$$Q*%(w1TBYMCuFJLG%0e%cp5bPkVK6%0454AXfpa8n8( zQI!ZGHnD(N?|*bQBYJ8VC)|h%tZl>H#$npta6sg{5Mg?f9mVX4y}}oRM_+vtYZR!v zc}X_0OZehrKh(1klhJvbuhE4?H7hJ;@YyDDyJ%`^EU+%TKAcu9+)*q&__r%lzD;cO z`701P?h@swejRZ`RLxVOGi}EQVB-q_!O$Eg@%!7Ut)NjRr6abf0GpYa^I_Vzg8{W> z)w8~Dq>Xr9ncU?1)BLp3Y&S#2`ej_uif?&ar9x0f`fQh#(l8S-Ng(pBCh+?s-QC8K zBf#WsR^`H@Xp+5?`t}O0*yM#?y>`0JK2?18$3FwW2z&q|7|tj!v-zsEytoG_S&9-7 ztpj4E6MqAHZA;?mcuwCp%mM#MTZs^txEGy~_2zEl`uNq;_fMv10nolFn7kiA z@_#m2h!a`8RG~N7-(_I`F2!4g6!GViG-YJA2*y}5nHN{?_5DHTp6NUp~tV6U;Hm41sl?! zexsNJLdi(kUt0VFbZZ$v_OssDD$69ktRK{UUHsbW2U6n63h4WL-X4?bf z%KVNjR%0X;2qhIhkQAm9c|_jYCB zZ3ErL_>ItdXs7D>{XP4PE4B0Mc+*eut5y!XipsCf_%N08>wDoG$oexNw|w!bar^V2 z?d-#f@~c{$QrB&V7eK&pj&M*^ilf@Ax)P^+`1a^{%=y$W{nR83LN;^&TqADEWahkO zpy+Xvx5GW1k*(6+c&-ei!oOagV(dolpYO`YpL$Ji1)UIKa0C~p7AVXP;P6aKLlK9K z_u;~O``Wi1O6{84H6vR`=ueI1TyFd%T_BQ>pFVO z6Jyy$zuS1dY(GqTI2luyFD3GQ78F&)Ozyd)Q{?m*^L-}2(!M4;(6={VAu&l+tc_wA2-+HMswXo2(o zC99VEy))%5N|S>WBR%q;M@HvlQzcRNEUg<+O__bzm!;N!Xahrsx>0#d79ALjGcg$# zbH6AM&YN0<3n0-9NWA`jgCpN?frJGcpV&?3k1JA5bq5X)GjdUNAZMk& z>Tpv14m(?gIwfwt@L*=3VxH?bZPU)RH=pX>3voifN%FFtre8Q*HI`5i2lssKlZ~(= zi8@i|KR^Qa;fplMic~j*p^^2$UPZ)keJW)VU8Is%^(E4>p%Tm7@sxfN)pM0MIyhG< zHmmtx8&F{2OS@HfRP(Yuhan79c5=|jU_UvIF|K?}ZZasJHy2K)xN=K`if^l9N5oG{ zh>kCAo4sW7*{xg_=oB3QZ(bMsT z?4q))(&ZmKbY_ConTQei?f?tyk5^)BoUH$e%8v#H*asrW)$g;&Kcio`l{JMj&)!r; z15qjYSRUc%rQ2T&qAR7rgfd=d?C_0Ul}k!s0-P^;X-PEL%yT%Bz9UB#mLN9TtHnsb zq~b*^Mi{*K?X3-0@;))^g;w%&BGV?mQIB5`cQ7LK( zMi;}K+XMYm0B;ZHM9jH@-^+T;q;Z*4fH1*HResSq+YySXuSI^SdZV~> zLm20`Duc^GuBKePxYtQM0qI36!HgeWp#ss-m>z+}K9mQuJ~dek*y`no&KG`)o@%## zA8ueS`xv>c2|;D}r$G;f&#)(`OKzc#83p z5cN^6HBN1i4LS(uA<4lQoi%#|9^FFWD@A~*e{o~T5~e;cWh17h`DXwM@y*e|Vry6v zI~wjojM@@|7U&J3zNOlTAk1#bvGaA^0G81Wu~nMYLQ)^@NRwdTLby~RA(N?G9af_Z zRL5daOuN!6D&1%(naU3)pUwV7q3cu#RBPS+vFgw}jlCd{~;0N2JG%(BJ~2sHGg zI^vNrJk=!tzOd(<`yShWC3>kD7Q-#M4;2NLk`rPi4E%Oh_NOlbEIp{^W)_fHsuC>b z#bo4mvtSdvuB7OL2ctDJ_4qKUSk4FI7Xu;SJkFm~9`lI-RHgIkJP7bh-Pn*^HD&}(0Xd;%Au4Rt(Z?OewPz+h+A>2 zgC>n4_qc>4Zz-AD(2M~X`t+Dn$=uQS`@!B?y5>^8e0iuN5d!1~Nu09y7%(lXIf!K0THS-;@gkDenKH?}uvO218UQZX^;|A7EVTMUmMyP}298MP}$98R*nf7leJ1 zU~}J8-I#KW)0zpjz~CH;6#zMcrvP-B?9ndN@$W4Q*&3~rjrnOH3!|xZWq}1a0LqFj zg5RHx%%{o4gdhPCQ%Xm3yJ)#c-il+%bD=K-mFe&|lL(ho89qeL$wi@9JamlFS7f75 zmYxXuo`{?od~cDNYDNJ=k6$*mJ&lVpXG~0Zv@!a_Gj!^xGhhO%h;uTh;$e`I`zyE$ zKegLXx2T|yEVOBC+4} zP+bS0!?ysTdP0>io{5>VT!=(f8NdrC996n+Tt@w_bXbpG>|gevzh!sK;PTes!CQuN zDo9<6u8dN(qQ=mgb+zYzIplrFECWj!j?C{n>nIXuxCnd6N{tzutF4QYl#NF4X0P8W zNRP8Eyu4TwH79p&95D>v#5rS5b@#CP0`PNH`?iOKI17sm+TeyG5o>Lz#y(RU?w*VC zu&x08L~D=l1tybYnbT;#9L)AxeHeF-IY7IG$ZeUTdZqJXB-hIVN_f{BSdrH-ND4S= z>5pMgz^nkj_!deB$+CfBW~MUH5+OVytygRNj9tCr z?eM6Z9jK6S0~mMlm@C4TfqEI6_LR9@F}Jbze7zuxX>?d{P$h)$njI6Sd;G zquVHX?&$R#0%YKZ7R2Di0kD0s3PJA2&{#&J{TS$CEZ@)>alUsq_;7Uby*nqwQb^vE zSc*j(-|hjv=4QXGj6|4SIoG?D0==H1i?H_uz9b--Q1avAz5he`i@<@oQDLrrBFx2IVLBP(%KXGtPRjX)Y&A}*4qjWe@q?#w7 z4?-vt0^o7=$vL~H!AM9$fLCdZk^`WCMamK3-wR%kyR=Ca@JmLcutqRp7gL}f!sk;y zxFFX6A?Tx`QBMr#{ODAysMEJk1SK1ZEN}2855NG~&LD(VS~owDuMLa&APnO~Vt$Oe zQWWYQ4x!uAmYFs!FcZmBcKfw$d!{55Q!Q&tf1X7+4bLB51UDx^2xkJ(G7~rMWXcL5 z=FW|>;!A_A=7(oM4FSUUx%mmhSKSeOVe-zGQMb5RH0gvHpX+e?#(())>S$1V2jl(z zOQQ{DUov|5t2~U&aNq`NE?)!42lVgVfjWsn1=baBhn8R-ZL`4EQUoP4N1a!262;NN z2s4yC>}q3H)!Bo>6HW7n(SVj@Onr+i&lgEWb3T+{(4;xGhb|j<0#t%f0q7)s7e?3? zOUk(dmfc^>cJ5@mXwDe=a}O56?_o2)DW*Q8i@E86ZX{{?`cY#5g9QRbJA==;ijDz( zc9ALy1=624Gp3iJv(ajTr+*TuaHC281R>nH1uY-|l>UDzN{F`r!qG+8RX|z3g`Ve+ z8aioKpuh`X=;dVKnh%wtjWGj&_i%Bg$|ZU#q5!vPBtn3&w}ZdykdzGg?z;*_W(6a~EOn@&plvX!8xg6Tk7mAih-3N9fOe%|*Uy5|9)i z$LBzo|6GL8bpiuB7j6s1O7@!>wLy(bPx_NuTo;O*#FLd z$@J}$uCB}Ub7?L{KXjtT{S0kG?G*vYd0i}I z&}0B`L6V3CQq-s>rUe*=CMS^IHlD;;oQTWw;2`Zh_=K8;5X!+;>xFM_yh z53GA>*3otEn53Dd4hCmLxxhE=oG08q=kHQ(rz-%g=X~`5<;tOpIRJF%)Cu8JIl`Tb zPK|a&ym|pzhT>X)@ciHo6caCqc=*ELDnp=~$4HNq7PKhqC zq_eWX471RF1?M@zX)Uc)gzqjny9j=~;O4}@-E&1A_A0Gi0d^{g$PHjdcHP)L_cg$0 zBpWZA+A!>S057cuVN`Q716;RRhKd-R?`r+58rDB^0IOI*1(JJE3g_u&tJDm@&^VgI z`hUza{HS0`ggByg9av!N;+zmV$MvEpN%rU^3xE-814~>#1^ZAyC*osmz3`;P;v~Bf z%V72%rNx~p>?1Dks;mhn_?tirqRxq`sBmudrSK$#`)XchGqlfFqVkK+pmVTnFLBplz<^Ihr}mE3jxWhU0M`1_Gm4p7#61;ZNqEmtr4U3r{FL{< zTyE4%CK%Bo86*Gi#&cz=ugB_C=^%6HCW9HA@Pjwq3V{<14lnlj)WdRgNvcjkFH4>A7#@Wo8;8uFrUt&8ODh;BWPG|mZa zGlRMqa(=5|L<8BbLzv;2dM!+wuU#P_vQOYrtcq)61POkF!nT}vT{a7Wc)^q@n+_WP z`c=~{17oW=Tu^on|DeLDCJ5n(UC z-=@|O>J>ZLeWf~WvA(v*-X)KL42*`Nf&SKikUtIbm74mr%ZcdbZAb^_DQ7W5Pa^xI zj3a7r_BwbcQe2Bpx%)4?yK1EG2n?O6-xz5MUZI9U z-Orv()*DjW=ybke)qgEuTaNPV?<@7ohTl*MY3F>jQi4QDxhpz*f#q-)9(Y1m+AU~N z_Ay0tinB<4S8+&3&cJ{;_&tM&hz!(e)Odsi`Q9lKF%5F z$F%oT;2q&|2wx>oorx<^LH4Q!IDOy)c))GvZ^vBY<=^Sz>A*uw_*e|zrzK`bh19}< z8i|28PMlC~Iqvx*cuubgs)E@zP;vkBNZlIqVk2wQ<|59JfZ>tssnw8vPkoTcmBvCq!)`GOH)I$|7`32s^)xb+2hxx zd%KFWcrPmk>hj~sM}O!FwWYjqR=j|Wts)L89?pgOFro-%)4$g%^GFSx4&p?8u+teI zHtC-kK)YJ5x3zQYm~D*8yymJj)O@3;5L3u#dLnM_6)}Jo0~vppEPOu!H_INN1`A5D z=~2Nu_TK`F74jz50Gl=gH_H_6>acOkJY$uggLe=*;W6#-biPrUoZ$#1*J7iZyv$`K zn&uJKPnmM5^l8U$6yXgfuklcEn~4}`TBlBFnz+1Ti-~RNI#o=&rw)x})x}U~d#roD zQoe}l-Lij5<_#loc@KHS#X8b$|0H0#DqWnWoQNybmt>&g`>veD@M5KN^u>6JFlJ`z zCk9-NCf$=a&Y{PytU6ll!$q#=MCl6Kk^NS^eB_Nwig2uJjsGmN@5I`>_WePKd__#0 zWmkJ#{ty{VrTJuV>1|`z6ue{ZD6s_Ilgm~X|0*h^5BB)jX4Ekos}TVePe7x?QWGtB zC&ksM(%A%2uiT&BXn@PVE;_S{Pw?lB%q(UA{wmrhH!3sKIbR8NZ+wZq8Q#IO3h%h! zt1!no(JhaV*8|{n_g*x12O$0Z*r;B02G5LPsUEa9ns`#V?+A6`60gz96Gtp&Lpu5^ zJY_of2Z~9olT3OBoXY9?i9WV=5HI%UMPE%<-+R%bT1n#fn6T4PQJLPa0G4O{?Wl|3 zd|bCKZ|qr5r-+NRItS{5i-@Z>qEQ-jkEh+BPSaSZlbo!a39%DwDkrH)XLU)obJ4_h zZIh}4Z{hsY0-M7i)4fjNGzBR^St<}<*(@33EL{MU7Oz}PP);Bjrp@6Se|S2^B-21u&5u2?)o&1vj5YS+%dRWkx4AT67i>+%SITe8APANB6|UCzfGQBgH4x27?vn$cND}5DMv2EaF{C_8qj&+f@BaIB?kT z8^DO?Mazl)A3uSCH3oDnOS^6E2;Om@8oYeX#cS=?02j(4cL?2F4;d?R8l(@7#G-&7 zbu7!4`mBE}Z1h+F!Ge6DqE{-BoJjIM_=QxZMrwj#@CHcA{(B z%V3}4bcL>5kzY3{w+4vhgxXt|(;gp-y{hV5Zbat9g=c8P8dcyhsy?dJVkSMRqHlKp z2eUv-zltmA%|MtMgK$z@#_j;b1V%t*K$Tf|%*W5bASRwlWp)KK`7;o2b`@H_stGh} z7*P#KYl|6fQw1)?72*y+s97>NDK3mR12GFbV=4nOi-37*24W_E1}amjLd_bW!tyXs znI{32X6sO&28exJW*{ao2I70j_|+6xm@#$z!jymu%%!G~U5X3S9e|Li4Ni(nq|HFg zaLigF=BZ~Oo+km70U5S=gZbMT2v6Az#QSL&Znaqh#GLVr36CHH zwW*ke$70@vdMU2JHv?fZWWq^tJ-q`^ z8Eq39kf8*uIG=%d`51^d+c;^ceU>3iS_Hg8Mwhz5vcn97GOxi&aRI&=h!x5V)COcW z8>_!(AYMKKV$Rsq_0yX*K+NCHm_Z=k6(d;S8hQsHramKeY%mKu1C>P}AifjICQ1pI zk2gi`Qe1{@2G;nL8CHFRdFmO6*Oq|VfXrFJo+>j?87%>oX3g?pws{6BeG(9F83D2N zU_-nn77u11CTap|&6d&q#C*Id6`0~$f(IbfP8ytagSl}6mI^z`m`8xpN+A2$kWh!Qcui^~emKSoGG23i#(h_U(O<_ZI%QhiQq9Lr0Hx&X?TnumqVrOfE zla@F$3Ij0<%Rps7M*Ur4ZhQvfW(kNHQPW^rVzzk(;$2}NUUe&Opqd&6vu7%vFQAHwI#D^9;lbUp1j-4G?q2Gf-K0 z1H>$>sZ^)-NkGisG7zh1XCS6q^ViRv|Mt<4wGW@4JUg9$ z`)5ywXD3hklNTfG>+=s@CP|Zj`m0Yq``z@`&gfe(sI8hx&~x z=iV4`PfyyX51*ZM{q*;D+s7~Z@$WCbZJ+n!?-x^39xuOi<=l_w{1MIu$=a3o5U|Ke)&CcKy z=Lp68%&u%7JTUE=5BCOX*j#+@;2L*8L;0g=cw>gcohHfhMW35)HdZ&!uWa8p!1Qls z%!T=;H_+;eXx^apyU)#tYV&Qs0Fr9?bZ^>j)4S$VGpoULJ#-hhA3iX1NV3ZYx%&0h z1re^miNQ>kbsE}#{>l8dKQ*JsFKzF4H_W7md!LB;;uRTo)KsGETDN&qS?vk9e`{&q zZnrb-4$VK!2Q78-%}xO+q{+-k`me=skS9I*K^7t7p=W$wyk>&~Rv z+%~@${C+QJW#5>Cv@;vnn3mJ_t))x)Dk*+#{?lJ2Dx`5!KVA8~X|=gvy4u&G^>nA5 zh@@Y>^0qPE{#6-N(+NWC_kaZdY<@|XujWJck0aWoJ~hMcGmFaKqoFa)4Ca4kZ2M)& z_toL3pUbu%&;R}-bu7qjFk|ToYhgM;xK^)+^Ol=|HD+Ve+FEz>c_(I-Hrl@Ix&x&# z@w8dCCGt6}^Os_i8bduz6Z+YuN3_U?^JV%Sh?1@T8*`oY!#83<8ncSaLH?GuL{PK& zcT<^*HO+{?&(TiNv9oA=zDfi!G|BRR#QZv#t!|0TT6uumGTSc1^|6?In}aK!4uKiP{>Th6O@3>Jxhvi$Vs{V1S8_r3 zypZy8pegT$9}Y)r_h2Zt`Siap#VU%Mlb;vUWPgMX-qU>h&*gfazHa3CKnKzg&X61h zcP!b^EZbJ@%_qDR;vE#9F8}1P%-p&ejsHmV>)qE!VmkKuH3P5c zj&Ew~U@il%TPCRN0a2kDYP!E~+Ay0t-^GV3bVzi?3>-%`2;8eVSE;$l4CEk{bv#~QH z-PhppHa{D&is~itNxHp+E&}tHTAoDz{cdOgxjPs2EQ}B2R^8OkL}$c~wY0e-bN#_9 z;+$*_FUViUJKMdHp9;zx3fd|2$>Ak7!==e4;$BEgIrhKONZWcI0?|mzg#7h^#Wa66 zJtW+w$}aBq>GqCC1;g$GrgwB((@R1Z4^L_XCS}H2GDUXQ1Kke1ur9A{R=;$c$6Pn_ zOR3Ka>{XQRkC+jZTvpk#!}}uDE0G>V*spPW9$? z9+EwGd9|CVYD^EEqI4(DQUQpUmWg^>9ypr&cb5qn-cj^lvHZ@H`|c&8Lt-eL*W=9v zucmfw=84HcjJ^K{@6yn|0heZ9@zTF5w?k5#`FHIty)6OMH8t$m(PwMkyFkxqj48 zfO}T(_>y?jy>$h@3OsPc5G=dWHAl;!9lmORb-P0?UtY$gKb;>L%wqX88Ib&b8Q$V2 zypvn&#jlAvnIr$sUvfsjMpxF}r@c=eoPS-Rk(ai*2rk%PsAZa>%fVa}tFl9KVZyiJ zD>E&dR?;Vzw#6h^nvU|h6|BNNC1~X7yhx8hu5FF^xtkZlw7DGo8?;vpbZ&(k7sJYu zT`ravDyZmTr(NC?sDj)UlLR|t8Off12=6(ffc=1wg=(1EqNh6h3E6A=GpT7{MnmZRSc9e=?*H>EEI z?{?{m6Oru132q0(bcp)_&ye~d8CocV=X)N}jrrNoR`cxv3^%(yOX`VO3Y!}fB!-nq zOfz5ks0L7+%eRZv`3W;i&YNO>G0QMt!y1d-ZL+N|!Zy`4!G#Sqn1^|DD{8zdQ%~G7xOCk3T74!5B#4?Agp`-u z)`;!LN8&eNp`R_=%(R9y?H{tp_qs~6$@W!yfBwPSc6f<*xnKgIrOW)KOtvq5khu=( zUR==X;q2!0aZ3$~cy*kOTX5rh{1WAj_ z^f!{$)I;6Q#%MBRi7E4-AhF888aqsw$y(xnNclX*JoZ3LY~cskZZkXmOHU zp7I~5r-@v4A7}w3Yix^(-4J7)K3Qhq3BTs+j24FtvMr~RqHdhQ1>xw)u z+O*gT)9zzISy0TO2AHKWcU@lSW+sD^h_#2zgF&<$p0vY~rnC&*#&LlWT{sd))3g3m zHE+DT*&<3ll~1E4U?-bdJL}w^=EEJejGRW&PLL6Nc?rxbZA=%Q_drQkad{D4l=47z z3MKX#58qZUwfhpPqGxMSN}|tc)dTX7)QDhFTRL5kGxFKt|A_ickKRY2?{to;-#yK5F?%TSfp zL|cx}8!@o+;~uo}^r>`(gx;RKNcuCh&~<<%GYHZj)m63qz1kv){+XPL_r&wy-FvRs zzNc-C`8PdBf?LsEK+})oWAVx+Fi_zn67yLUr{2{UQuF235ok?!dr3&z)st@4IYSrQ zH&u5KOD~F4g3^20^@w>Ckl9J|Ap}Z~H@TuWe5u&#_p#gpC<hSiX6i?@32LxM{Y_ z!a&~8G+ae?l}5{5Aj=8u6?dkZxTP-1)yDo>?0vvlw^`x%OFyNe5=vBmTjiMWqV}9M zmQUr5cq+sa?uhwhGoD*l7bA}s7Br;$y(YJ}Z5=pY;sy&jFw$26^(GN1w)$GvWUsQA zJmw&a+b|OaH3~0iakw+x(!?DB-T%-xVL#lx@Am1eYRg}17kr&JTjWu)quX@uOMTGx z{Nh8=aB6g4(^imvz|p(T=5u%%pYnYau)x+^ERrsiL;dOpajSSrme+M-1a)36%?ET8 z(kseg1&?o-F0cd8GDhC%%JHWZm$l>lIj+l=KLsbG7iM^si|J)piyOsZKB$TCBSB%a zeg2{Oj~&%?@Wpl|c=HQCk%7gLVF*ajpB=Knx$4HJQAQFRdM93XH}uF(Dg(bX2lJ5z zw(7M+@AB+8WRNtC&VD1fWlxP*Zrw1k%PzfyIpwaA>>$$I?D!$WF$@n*7##%1>}Q8* z{TZe!L3%yqwuGjKnD{AJU;|bR~%y!%!^a#;%XNF(0{? zOdVC!@&&0rAKe`BflkrBAQn)tTZVA@>`3)F*a{+&1op-z=D=-+v!SKXg(w~_a(qvB z*F!Wn{U&^s0;=BlrR&%$8g`I4vX+o8)oKtzEsUEqqZw{&T@}o8T9+d!+lZkF0r?L+ zDmweYc%Y#?Qk~6y%jd&3)a)Bt)-?ftL#k5-waq-~1$SpzE79(nuzNB)clY6b>bW29 zU8d$K0>yC=pKq=s*Q9a6WB(muv1sXCnb@?-cO}uB8d7CGl9~_Vnl$I%dc)fRm2%YM zktUtijbL6?=kli3h;c`5b1+DTTYj`0h=ZHm_!#IiMp6ds3%rd>q{yXrXuXl6k)RK{ ztCgpTSJrj4+ufvv1rBGldP%m8PH0;RMHwzb&3D)xeS@H@863$}dWy z`$K2^Y&jy_%cHQj4tcXDx$osKBdIh~f0}LS{q__H=D%4iwovq;8WPi%`q0Ck^?P*f4+B4h2Um(Fe`o8yvXsN2I&yt>X|KecP|HU|zMg4G zRNHn|_r%JN=T#@UnPq{p*}!|`1=H^IbtN*B$|V?Sf3(Feh`Wg9V=ITP<(3Wl!bi@>uinkv)Ki-0_J0S^@;#XAy!|#6S|ShaXblW$|(s9wPP=?MWf=1 zdvQIW9^n;H!>;xlooPz@>tVZb%OT*4gM3}e)9;#puCG!8kCO*M82g#WfLcW&3NJ~6 zS)#NFd$a6X`BV`f;n*bKj!FATzkPvTcG0p7Vg2jdVK){IfhRIR4wys1wBTMR)bK6^ zDTwHVwD8owBexAj*_fZ%bu~nUW|!`1ACS;4Z*IB{9KrI(>stzZaE^8|gnchkX^mm| z^f0#X8%2pi&zS3{nn|%(=UP~x2RCCs4qYdft`Y6Kk2dSVV1#S3*~&1u^W2Qfr`%!1 zh^kYklSB;%@<9o$lzMYa0#32`pPEK8nW2ewPnya&-4iu&7_7p*cGPg;2|DJJMEEtq z*r*pQaYK4DEjL@0gMIfRK4Uk$2-k6JTQ|Cli4sM!c3lG%Hc7r}MOZ@#45zz2ZlN$j+t=+o8xruPThD-IQw_$pSrQ@`UJqUu*Mb?nt(d?0tnwBZ;JbzxrlZXyf zwY}%c+fV%)l)0#&u4FeeV<@Y|1r{SzS=qLH_?mIozHIuakMN{qQ&4fsK2n{v;+)5- zarI1}@TQ&vE&a3HJ^~}-cM-ey3_{nV$49c6Kj4$g+r9jPM4lf+{yyX$vnzBp&$vGH z6hj5|r&2YBqOWL2N`0xvy97N+-^f^ICnU}Mw^a-!BLQkbblo6~?l{7w4k-*z=(rw| zZtPk~W55}#PdBS|zs#G!OZ?ZOA0#h!?AOM55Zpk}Q@Si|(?{3v3Ii8`W>YD?RNRyk zV%AmClO4wCRQl{n-ZrGv>^iAb4@&mChP|w7cW|ctjr&&9ge9*Yb4hg!3zP+m1wzk5 zl+QO%XEgf?#%dCAahqyH;^*Sxz{J8%JSctJSJa+E$4x8UU%aF{>@x}dSq+4)AJuMJ z@8;%>Wf9yX&A4@lWnxE#U4ZePZ#LI+wiHaM`J_9is#KbcTk#SC@&nx=Rhju^uj_Bk zif7{H6}cL7&kxlMathZ5R@ZjEXRtFz5j8ZvoTY%>U`NjM%s=a4%I5D$u|=`(J=;bR zm-9%=Z1o}=49%o#Vc66{vQ+$LTbV%F1bfF3fNg?F#u;|&2E2XCoG5DJ! z=zjXXbb4^uzCPP--6Sz`Z5-c_UyG*Tq{cylByfByXGb`h8`FrRGyi7+r4ZA=too;B z9e=J>-q63>fGl}S+nnsf4VvSo14;@$JDT2-9}@Nfa3gGD4m-3CS%}LtGcxNEFvN2| z;4*6K4CY6jSK`Pbl|y>8nJip~NKUqoB4)TBJ57JGpY|-!RrIrM%}BPt-iv?JDl*42ft5XWC2lj5l~l|o~7Zz^C%ZA$i8$q z*b2(j((r{7NJ{Ay2tNC<;qW6PbK@gXteKbJzG=FMdp-NWyZJoDIrTC>jMS(*U=ctp zU@*-^FV)@nwL2lsZ>V5IrTNLPSj3y5RHu~o>>-Waaz&VmPMLpZqS3X*wXUBxbCr&B z=$`9)>Ft8T_^wxLyh&zm1(DlFg7j6J^Ar@KXX!Bj$;|gmpAvJ z+n^WKAY%<6P36#rx)7&y<$~^xUrP4b6NL*XiINgdoU4FC2=5)f?02k7fZ4y!30l^3 zeZ>4QP79sb|HxSvRR5yi-s30*Cmmz82!O6H6w9cHS5o!3 zNzPN((#iCrP!_S7TTylnZ~7U-fLPt^`Q0Sx#z_hzpENBXeAmiD8C1x&Hg^fx9FO-aWQNPJ|7)@|R9ci!4+ zaZ8j6c3yC)SQL?A0oTAJpxF|vuxe`f-^f_qxo~r8_rD^{3~DaW$l`JWrS| z+wf=6VxOREj1pxdS|8+N1exXU)+~FQr2&`C^Xu|`%aQ=t_DT$y?ZTbPY~msx!?$vs zgLW@$_A=!Zavj|dydXb14kT&B$?OwB=4#|WQQAj`5MuamMdyk zzaDDU%#vHy{f#boW5uZ*kEAgbJf>-ONq1>A|L)|q=y||M|3~D^V7}j7ls*$DlJxb! z;7hpM-&}+Y(eK3>#Wf#fZc(6WP0}~uN%$2v{G3+H{Ly;NTb>TC{r0Bfe!_3V-X?O| zXfaY;KH*K>u+TKJeubW2kT=zH<`nK2r<@lHad6M%Yp)x2>Co<5puKx0-l#yuK+%t@ zKX`gscP4)DlpmG|iz}LM8raSy7{l7kT(u+>1326_g36VQeQG#HSF9O*YcVWJ4Gu4$ zR}RbdACYVWuGO(vfy^G^;sqWj6@W0wH>%4hAiY2f~d=MGT&=aEdD=VJCCKnSI-reSlCd!!IZp)KgCdt3W*MZYQ27-4ff&n;?<}tux+q7N(P&DYV zEU%;eATxth|DhUbySYYMqAa*6NB=UQJ0&yJoaqfv+)iHDK#Igglch)yOROsO5kS1? zQ)W#y-4iXmwN&t-Gs+7rzQCO*I zbmZWKTHefDRyJ@s-(w~t5d768S7cQvgDp)5t2lS8kG>o;>MpbpvG5GF+#>Tfnlz)^Q>!uf+SLqb5gMmtnJi8`q_Z%9e(ltKy~)>S0bL7mQ54dW|&Z& z(5AP~dCD$+EJI}$#S*B_&}p+J2X`Qkej$lpm)Mwi0@=Ik?Pmt$tLr&Wwzv^C%C$R@ENWD^eIove4sMa{8KdMTV$oPj=n#y1l*M5 zYXWUVLJ-x11k!$Mh5|(sX6hpB6>p4gdU<7=FBN&EX&{mjA*M^FIlY1f*6H7wETduj zP&czLpsM}v>+uMBMsdmgYi=cdG4H*5>R-`BHhB&-@4P;jy1n8#;AA-|j#^N7$i+bj zhz`6qn$oSF!$Ofx`Yovd(a)Ng%bJ878K}JBH`g{;nREj{e&;@ z137|)bAFwgj-^cEN7PMB!+amkLCuST6JVP!WwfbNyWA47q4atI*A~3!t~A9K=u_iQ zUC!zar!etoLS2ox?}QXT<66p1HjgluiY5uvAf>SO<+xf+$~+Qy8u~>{aldVt*9wZ9S+667zo2-ir`T{Hw^~4cNz1e zc%{7-_Wl{qfn4XvsZf$enC{zGRiEF+(|Dp3Om!&^M&MC=Rb%!z|Gvsr|5dafGtmpZo{Rq<4ybUoPIdc@{Ncg z_RD4YIN^crUlzIVW`(&De0H*6Lyp1xyOVdF#-;D_3+uFXr-BY`ZJfFf!|-Y##Ep-s z2my@AU@0}HChZ-GMwG46V<&0Br6v#85#$FH)&@%C^Q*H9f}O^XG#i#xJNHM+%2`Ho zv9~zy2Q`e9CDOJfiwl^(_?7p(1}}LEW5fK;wK&`)4uZe~Qs(R1C;mR0e~X)k{>`~4 z+?pFR{RJI;o0Q#H!VI#kD}syS%p6%r3mJdRUM{u^FHbmctc)6<7L|NlOz|KtXkW`` zOaw%I$>K*L^l~;rq=^d^jsY9KqrSw|eA6DM~bAB|W`JflLdrqI&mBReX3AhHL!8C4be5oFqb>|zHq z8L+0`J0#>`&k&+jGrIw%R+Uv(-Ml5H)z=Zd&q)ud4UbQig<_%3@3l2ASkD=Q(4})f zVz%mgdb_ZL*D_dow!GO%kDEi1zM*|h^+e?gxQPrPlv#K{B+#(G;ChFNb*CZ#BI4nk z)GY%!I^ow^FMN|Eu3y&s(t|p2FitOQ1J4+xp$sr77xdvCzZ!G=Hyz3_QuM+Reex7g z;;Cl$itdL?{n3yYN{C5we3#&`&I@XY2)+ArM_LHvuH^%-6^KZ?H}vYBb2=qZU9YRe ziS&82*8-tKL@Gp#NFn*PxTDnUa&1Raftz?+W~d27MKI;-Dkh!)%;3(Mc!FV1`yH>3 zroGwkXaa${JLcd1shzc7&I@Q-*PpzjB6{zQrAqi&yxj+ z?#=Op(7TLv!L;kDB;D1AMjARrdBwM;aO zZav!CVvUBz3I_vDt6elU-Yg?X*lQGM((SH=SHW z_E9$>nfWK-b@fSH@=fM(HV(zi`|A(B^r}(M@xBtR2P{)c);4KYLRYk$Ay$Udi=`)M zoMRrL=182oknqXp*R`mzJfXi~f+~Y95n6H@;-BU1pawMcvN(+UGJU{aF(M7P=|$#( zbj_!txd3UIZ@b7A%%;aYKX^^>Sq7nViI#bdU-$`$CBBs3SR3Arjd{#>a4PFKtP84qQ zW>0wQVt!=XW$eqH`pwcHe#&Ide4Ox?jc*>&P-}!O>|&xlEpW#SKH_B|S0BFQ^6J@0 zbjy|G&A@RJtAew43aY{71m29@qGE5Y5?4x;(j}!gRd%jKMfnbBE;VK!6YY|#9A!ytR}lUG*{>Mz3| zG{c3U@|49N?`{?s1FCYn+r8>4P`Kt1zpmQrKWyYeL*31*dX6D_-1$##3I%Rhzbi;I z_ELNb0(P&2wvs@od}AA*E}8JDTR4TgvQ3y4=S}6^N_rI9tnj8V$ zt5w_HGrL_1WVcUJ$l!Of!F_#T4FAZQMk~HqcW=w6?z+(i@%+- zz69OD+T(@`G^8_D9vQtOGh?}C5hL);(*U-?TI8|q}5!?0tD`&GjgDN?95yC3_DBuR*fnCr{f>RJ-5H+=n~I(vOoW(q`93%kgkYRJ18yf@?5u z4&8uUU37@tGP+8zVmFw8Y(Q%`2rnsSk;Gq!T0_yw%dQW)`cYn`T57!{r`^Lfx^hzR=y9deAIB@m@lu{b*Wc!?n6aCASXW?@{J)mU zZRjrJ=VT{kUe)*W<9S(tBU9U5jTu`zN;d=ZQi~PAnGNZr26BWNaCxL1(r&|Dn3rGA z^USG-bw<_vDL(>&SU(0&AKOwUP#ovd<^pe_c;9>{rUQJ}jt3F+EiE(caM z7YuuzkZpgcH)%LOGv}KTEF>tfxd5$O?g3x%UX+(PDzqjl6ErJX92u`mV-*@kzR&aC z6C*@YK-Z<~RCP$6Z|az7^o;~2XMMVC64&N|Vu~b0x1P*fo_0dMI95q=>00aUAYVvY zs$ZU))TFq49qVU4Q`*_{N%<*krNKrPJu|snSP5bQNVL15VtA~s)}+jV-8+J{5G04s zo)+-LGckgE_ol0W&nj>3Ax^Z(r7GR6jh2csce5tN$$4QK*-QCQlc}0Iiv+kNna7L& zI(A7XL$2w26* zNavPHmwBk5InWh!bEi!}wz#Hel1FO0bXROwxW;o$a4bQbii-`t@cS$xeWiSBBUkhn zOw=g*)OEdKHuU;chCY%qGn&r_`4%ZNjOTsI)i~X>Xi*>Lf99IjbWj#u4iGAi&g{D(?y->H7k(u!9k+(HSPksRbIjpG^gZ1WyuFA za0OCjWFb%Kr&}jN<5!Xub;kHrwhfwu$zL_C!|sfYBtcKrmx$ce1#g(18%%7PYSW(D zRM}wjT{5`r>F46=aCotXrTf79l!rZJ8(C`1bFq0VmdvWr`Iv6jVjVdu!z%$d1)jb7 zklQ{~E*ErJU5sX4j31p`2Rug4qIxytOMw%(5H$)aKxUpAM`x^khca<2X*zaSOBGFGJa4E!fv~_SBO64rO>@ucs(F} zal>G&yev!>T8;6xr}cmanzwid4U(dw5-W|ZtA(2FkY&-q^QtjRz(*mHnuLou!f?!9 zH?qxzLtuJU_iXL01XnP;PbM92U9)%r`5{gSmgw56mdRK(V$k%8ZRx4*PnL{sJDGn~ zHHFko5mC^wG#b-X$jsoZ*Y~Z+yBzA1%y%5=y|o|M+Dh;RwC8<8q~K&kCDrgqCdHEB zE|7WJpm3$rK){1(vxkmAg?JJ?-#}(tkOk(o2fJ`G#=Q(E#0eHRs?#V1awK`+qTnbi zB&`iQT9G6VXBs3GI(Ew92rD&2hh|Vl-l)4HJ1vjUl5}1yj^{?ci5c_LDj=vcxu{Cp zbfmm!VqY&WUZ9fjZb$o+Aj71=8}CfjK=V1bj0_JixnZb5CJ&rIRt=HK$yEerrDDcb zQW(s)gwC&(kn8>I6=+eERl;0V+F$rdM%KO`Uq!mJoh&bcQ(D(Lk8tH2PCWOmSUwo7 z{`yFSIK5!RsK+~TerX>^F4ci7U9afcu!>X6=S`hnVje=;`-Lxo=pC<%CF)D$sIQ^V)h{6<{xUdi$-?gn?K zcH?TOPa4-TaoP3n1nD+BHvF?|8t<|C%{h%36vsDK9KD7*)s$1rg>nfmeIUJFKk|5b zu><0Fpa=3!)~ph1{Erl8pgpJaL7iwSY-MXXp+PJaeCJA_S4AoXKQA|lhrx`uX>(4N z{160&@4z&Cd=qxq_0*{oQY%#qU|V)Y5*hLD&IOzhg#6|OcyVoCH?00#;Nmqc%w?cK zeTsqWQ$&-ldUq!ciieARtvSC1@{4|*Th%kd?nOosoY^eb?GAjUh79|%2!_Lc{g^^i zz;D@SHprF!Svdi9@s_R)o9H(%RGBOS^5EudC3^eaEHJCw{`b7vu15ZE*$N|jot)EI zI`i*N92zf?UW!@BR|8}%Gx6Z;Ra`rL=Q`oga!V%%aa(woPvS|v27iNnJD5uidWY_^ zWZP-7b{c0qs0+_OcKf+(%P-YXY(GJqm58bH@<W_gL^rZP?;MM^O(JV9kl}VZFNb zvc^3)!I~#Vc3zE7k~KFMtNNksU}qI~b9eJC>Fm%4N`$cN3-OLv+S6-hI|91?vKQ>JU*tfBE6IDYIhAw zDnw(7{-2MaS3UGCn#l0rsb5)C2$um0PPf3*W#o4URfW@5Qo?i}^UDE^0UrdMd|-7Q zfE`z;S5n}=G|EmhP{I7}#k+`AtCvDC#KdnQPpLKl*VABZ0#d8{#2Q zYA0R^1{H5TBC>a1D3TS8d#W%o52D1+mpPK|90h>hk5VIPToEWM2VhKiD%>RY&%S$$>FDmtomUg)W*;5PFpHIAtxQ0Z-BW|^h$huf$5IrOzFGjkU4suoocE~(wQ0^_U-G9d+81R zk?t=mjydEK4Pr$k4pV-y;ib?C>(Z;Sb*LioT|;vuvaa%crJl{TSntx<`%GjUFf1>7 zidBxoC{5|HMXY3VtwmN^J-`>H`^@JvUkSylGcgOfG*?f=lpfBrOqkzg!ZS5hPG7#I zyo=1s>ZQZXPflNJev*GAGEQApbtUuhCGjm1=py2qaR^otj`?n~SJoJh$;wUr(BDPc zC{{sgxzXw=m^$pqPHuv`K^L{Iz#HQDm6)Zp6VV>(xoevwX3M7Dq5WmfPXS%J(^$qK zx^lu^-4w+>=Qq-8u_pmz#yMTtxC~9kUE*T&X-sF?2DI*G#4^?-!sD>`wRlKSGMHMw z&K7_S)x?$7FY6;Ut!2FDx@N=R$WU4B9d0z$!w+pSJq6w4nAaykDGYzkh}`mliQZPP=aSOVPle; zvR5WF@oO0s0ltTm3aV+!=LkKR8;0W~BB9pgI4yYvOE0Zd2m*Nba-p@H(U;DnUf))8a+{j0H&N={bw%%v&$Tw>Z?h_D^a z_?YUE+W87AUXr}3ZZ^^2D1eHBq8U2T1Dnnxd87K>5z^mxtg3X(3vI%)M>$@4%JJeA z5=;V&1J}@Sqg?0=(g;u}&x@KVl(TOy)7>}^r1xT}c9)|18<+HA3PXM*e85gNVv#;R=W@~X z8Ij|1=u`v4&~Ds701B1Zhvhz^4p}NMu&w2)II>RK9GSt#Q81O45ycFK?F_WYx^OO; z4cA8AR&)fsbkiH1$1yv^@FiVYNS%x@`gXgF*^#jlE30^M`FGp@V3&rxgU5F!5;Dmx({<7y+x4(Nr&tg<0l*TKdRoHA^c#}N z0%d0Cv@DZ^pI+4u@NVR*+|n4d`_v|M^EY@KwPtNP7cz5A^1Ps;`X;_pN}clVaMn!Z zPQH-Jyui9_Ug=8BFNk>wSW=gUlx!1aOx}HcyG3qKtl4sirp*EYX$eqYjhg-S?b5N(;Jg-elMxi|c=~^PBc{ybf4?R;#p??#;d{rdPj>^CX}Az$8@ORFlg)Lg-k~*mJ;FrH zselpuSJ$^+JDTM;T^A%uW-AnGIEj9<4OH5_2n8albtzT+K}KS~A<7Rsgt5hbz=~uh z9UM8w_ko)6heeJ^I$_ECd0`wka29Lk-o1O(v?pAPTxR^_?La+{d<*&gi}EH%GV1g^ zd~>+5#z}|m-Ce3HWtx5C>ORdS&&e)yiw7&{6eIqPsOa}HnM*3UB+zHBdA{Zw)!h`@ z4d%i15?^!8YqAw0iLnV=Z_|v;dPw&{TzWL)(I4K9ne=vwY2mP)thH+u-^6~n9y505 zs|u`&&X9yvkF*isYH3Gc)rPqyxMmY;TBbT&FMJo<1Xi|z^buWMgK&5z9^|>z?~YtI ziN9a>5}26}X6H9V%hFs!*Nz^&a6J}jpGTChGedv48S0YGpAg@2yI`-Mp*@dDS4Olsy~&kWu9Y9 z^!q_DaseH&uhV@_AL$R+vPts7gQEC;;IMGDkYYC+xjvSHczyjTElI#feW;G2F=>KK zBMuOa=Jb}tL~NIc)Y``T9o^fJgx%AF81VTQb5gCaeff6a66KPYFqudfZtq(YOc3E&-m5C-q51k&-I`kZ%8+_R5=e$%p<8)Ka zwpUlLZiU5D4qkm+=&0NX+u-|hE;EIA%D2ufdjJj4w{Ktz4UtX6rBYr-#I4wAZo8D1 zANt49QQDrOEFEIlYAyMuep{!^QLH$xU*hIN8h~IjL?$-V&F~IRaQ1q{E^Yo|Z80_K zFT#-B#4h{It5Tcgo+ha1{q82VO_?Un~j3e}-J3YneHubE;mJn3I4*gHIc)FCN0C4O?euul%i z(0F6w9_IFiqVVxn&)GxN^=iO`u&nfMfO)ze>}V;pLal2lwj+0QbkGJqXsS`Q-M{L( z{E^b37vAy0a79aS(=!@9xqy{nbFD7ZFfebni<^O4lIFDK z-H=CuR)hvNbLh~oGtqDD`$t?T|EMZ~7^QFgnd7$H+3 zPmhzhWtta4n{q7@83xou<~#DU3T23;j*x)$w|64SPOciN#y^Z)nlK@UC9pnuCy?wU z4PE8pEyD_nBh7?kMz!VHf)#L^cjcUuca%&I)3>xZ*rsbB^Rwt!DQPR;_Fe?jKps$# ztzin=?CnUR(Mb{W5^1ma@zIWb%NGOo+u_`wN+MlhO2f(R;=JamE*y>b!+GjpS<-j5 zp))_|BKd_^m$9P5UGo#g0D1|tiYvDUPSZF>w5W%hOuOGGvu-I z@}RYZ%Hdygk`Ya5G7L9$#W5MxSf?L#(u$i#F(`D!{-wLR9%_pHER)&zJ08v1$zito4;+;Q^*(qMpWnt5} zr|NY0k3OgE#ZmX1MHog$@v+g?aMul4OZ+p40BB})S z%CTz;p@lGCc3$Qi-W}vPeq*cZ&QsPTuG9NN1UW}L-z+Y!G;}HU<*}|7 zB1+%|*2!`DDsu@cpz7?8b@%vJL~lyn4lyCXQ?{EBv7PFCL?iuVNPh7VANQs zw1q*cWy|QQ>C>&Q#(n4YNMxRsNEqz#uH1%qujLah&*OW+)GWLl0RbDE+f6fcWQDjL zC}njF^;`pvAWSW@i~jr`hYYxV?$>)=1{uf?@a8S36K3<+@)L*F$S!U=^!wx(*8?9m zM=-A1w8gR`$~x2T_|R+Xp3bbF8lNpaOt1#{SgAokR3Ed4Nas%7Y~F&709`LldMx8gi3&5)Bdzf#6NotUOS|UP*P1yXi|{^%%!_}yT)Y0@#d?F4og!iN?AjDbsSN5 za+Z`>kIa*R(>iq9HO{B9C^Z)AnU8SO_MRRGv_Rw>$0ja2lWRS^zAO+@)$LMhSuLx` z7fsN3FyYh}s3XK59&J^ZT4KvOVWh(*2%(AbP{bP-$m(S@ZC_jtdujfu@m zgNg-PY=jyhx+`u5uJhT`+oFu_#=|*Tt%kR2m|>2j0VAJxTyv4R=5UI*irJNLUIKCh zVbgEmN@m7LKUU}YHVJ6aUlOBnAX8{nky^e)4AidPbrj}icuo#S{NX%{=NjF#t0o~6 zTVQLr$XMJD3mbQ28({d|)vXia3q4FcCj`)R!b zVu5i+C`%sQQk zokdsd#Jof-%rqMcjNw>{iO@vD>hT%)3HIGUh&|GxKH5R(@WQ^UyB%?PF^BT(NLOKq z&Q$78%iB@%oIWnyIG=tmmGOW*>gr=vTTOo(8F~;*u#=ULuu7I21|}kU{>FyIHF1kI zp=_`*>Q;!_TESVV7L2E-Zua9gp+y)>e{ybh$w6ug&hc5D04ndoI& zq$B5@eE+owISousbjF=f)S*FnD4R;WKv1>!`#Wp+-*M8o!kDF^s~lzNZKx1VWrSFu zUuih8E3%|D4cd$RAUBspx9g{TWZm4z+uOSSbT*di^$;V}o{ZEEjSWemrtPiggxcs; z>S?r*JjpYwriD4|TDf`1^{O5TPGy>imWVj$@33;_yo>0VeQ?{0tL$cv5PaiG{uRj@ z^>BN&qHPXXLWJGOo}fF_A8*I>hrRH4){Vil787{HPh<}7af+hh#8mnhvnFvdgf+L< zDEsCVny?lo`;bBD;X?9i^AH65F{&Ybz{G^WSQx)xlwJa`d`>(fnjp zB)aXsy1iy$I?j}#3Z}CHdP!eb-U;XhXL>Zv3{DtREEe-Gw<#)%A^=#id)*@}OJw7X z1$SgS+zK4?SuEDSm`NM$;I-Rpv`(}4ug|Cm>T$HE>3Gv#pmjDq_&&=W$t4k4aK43A z-N3td6#8q0t_;^l5Pd@F`!Z38ml(X;A9>a4SaaiQAb)*ljleqxzu@bUuW5~a*jFUH z4;9YP^fIyH3IdlqGe53v`o~*(-b-1~A>gVbYl8K1_srkPYpJh}BOZY=c@KPa6T!`@ z+)u-5doK`t`oeaSed*oOavOP4_LJ2Vw;~Da=X^Zqvb)1u0PB$5(uLw*yUP>Mi3(X4 zT30`1_aL|4;2{vikt=C^Wvf63voDU70)Xzw1KwE7b1i8On`7O}b0H78ov;MxJMwjQ z+>Fxv*pvEA2ZcNEh^hKuoQF^Mao9?nSRwQQjm*pC#SZW++hBOOIMT7Q#**XE&UM13 z4b`K+Ev!=XNHhK

Rm{iOn=PB3&p;{Ok`_-x0BZwQP=;^gkUGa-5zZ{R^a7Q?%0J9WmKnk z&+?m!mT8YuyA+DWIvXKpV4fvCXC>Rn1$FOkZZ*MzGj5548a4uv z`tKlYYvROie>0%^l^OJKp#bw!<~mVr1y{H5wj^X(zO6|6t6P6TXZl44ZBwMBt5|OCVuFo zV+Sm?Z5?~lAc(7|tD9S(dD^p2l|a_w?Ku+^?~;=#vfl^d3}uEuJsjy{m)um%c*?@^0gGG~Ugx<-2VBfKtM{xy>6|l-l)YhD)<>yROs9MDw{1~7$zPn&v zG%K5fb_dqgQS1fh%Nyl=eoqpKX->PJy7Fy}W)AH!S~#PLJi#!o^pl&Or~a}U^UV@S zI4pO?j6rbCw`me;{(9!vgtM`yyd(YpZm=`R_j^uEt9wb9mScGa@WOAe!VV93C-N%Cn;Q&(}tq4)=F zc$yyt3dG4HMLQtaExxmWA*`=Co2hC=lSdIN{G*5H*EiI2%4K>u)rKG1vnn6Kxm5%> zvpYY*?3w^_*+jGc)9gq?A69G)B5=fshe{GoJlxwajdWwhiG_%o1a0oz+k}(A!h<69 z#Jb;~)Lp}4`A1qMs54v+$!tQ?<6Oi%tn27)o#&j5^I^ND_Mk%W9w zBp4LJY{m@ZrxKm0O}`sylTu&ZedQN8xFOp&r&~`~!FRki z;k3KHs)iri)xr+`t=Oq>I+Hf3f2tGtn&6t*lR#baSL;yZ%gf|Y$6!f+JdXiq`w5iE zF#k@M?gH=S-0LAjW|LocYu3wxQft`7juNckc(UsO8B%5*@`AIbfsm^`axd=Vb?f<6 zfSzQ1h4b-06u<*G7}31?hS@P7Gp6L7{{R?k(cjCbLQ6?%Jqqi3?| zs-pm7<50D0hNN}j95qfdm7zY;>sZgk22N4B?g%NH zpGwMKYDaxi=&sAAWokc-MUl`k5o2HU4O{~yB1rCKUfZzm@;nP?a*Zp{{A5BlPuh9BAZD%ss;5Mv??BWI>#dOaxao2n zrW4AiVBo$3qocAPsuE{}mC_taB1(sCR%#%c`F*}-ExPguGG@}Z0|nLX`Lf^Wsam#YBx`=1MwJ!=6wcUfDyr-wnn@yg6w+L%nfemSvstE+$1&X?26MN|w%5!*bS2=aqJ?Ch zR-@QzpuxpL=1KgFUXx?ewb8zK# z+;mL0K~Ebs9u5y{n}xyxKK0jNeefiW7?V2TqJj0Qv@tN&GZ`W-=tFsg?0v+#I9?oJJL>wHTQP`K zg_h|dRbHViR-SPm@gf-SYc*3#mW!f+<9KvU;3%Q#u;#5bk($E}>Z`~)Bd-ZidUn|J zRpF+o7>c)(|x)x&m{NB)!v$JLP2T5uO$|nb72dk_yeMBGN){Cjm>5>yVqi- zKhr)-up3yIgH~_uTxR9wB zd!T|lH|(SoIJ$tCg)})AE8umAWOFqZbXmb%`^&QOaG^o#ncxTY3u}9UF6*U=8POZx z;g0@0tzKUmmH7f+Un9cKd8P)<$3${Dxcup>Sd}r4&K6YN%jBAi-T4(+Tl$ zhqXD*ZSPsjI)X`;7q5;HvFyIG{btdtwnw-!=S#un>eY9thFm0;E@AtD=P_zNXJ*$m z{8P)3QahjxxQ_rg9~NAgBU22QyRGK0TKCMvuToLzmrGabnvcgU*|?R+0|hzE6uSBdMcwQY zoF|qXcc^%KAW(A1^pRfS3PcXoQDJ$$HuR{u9x6?rU0cW_P;QL&&sCg}|I&Ewv8=sx(%NoJ=C~e<4wf83g)jwBH3u|E|=}=q_dJ5tgx#@?J($oJKhFi?=9l?yoB=R zAH!RsbyQ}yXT9E5S!UknVRxkZeG{*}EFzlDceh!NLW|oSkFHkrnQEM8Jc|Un@ zx;5%k=p(<3WuG9$E0NJ`e6NH7X1B^kV-`W7ExOu%>8^;iRO5A_N&r#_3+5*a=!52` zLA9m7%PfvC)fI|c6={|&2OX)FMMsF)Ht0OqcSoT__vU9H19ilF--#;*SE%KF0yovE ze$5ZW(!q!C&j+#&C}Ars7Gy`kS&d^CMgJbWuOhRAF-!R#*RPU= zAvK;{-4#y4Kmv|XO>m=@et^$(*oksrRshZABXI-+CTMniSW|$Ch!@Mae44^(BKY-> z7<6X|5cv|lU>+eNUmEBpCNU(vzEy^9NS!A1nz+sjQJN}9@#~PS;g4qdIc&^L8`smB zr-Ex@E(4n_=HUz#B){SKq<~`9{3Vf{p`zk*0mbO~U+&M(S;K-01oPq=0L(d0BO)?) z+_i}PU>%6#$n-X^^zwvSYz5plQgWg$tRvceB56h0TbId!_1{5|M&qFD?N0 z8YDe_AP9`w+%DVc%6GC@Sip{S^2Q2fktXVL$yFvK`6gco1@4i@rOJ}cFSS8|t|b?@ z%N_-+wrY#dB0iv0Ru;IFU!aTVwu`KKt0? z=#q2^dPTk3h{c;-2VJ$e`PtI7^*Y&H9Xxn@x&+}q3{^9ji{wc(!`00#k? zBVBUelh2PI)?lDPA64_Y*l);1>^2u;S)oL*TFir4Trpm)q)Wg_p5Fi%V9<+pQTzZk z@p?=M6AuAOO|rM5aSghNJioNny-xc&66!v$2Kwq(7h{@p5lXD&kL7*YI*tJ3%o2!q z=fpMGIZ6t1(BgDydt2qsttSR)>{7stUWqDrnM!ayHR*bS0_5*riYl%JwdpBAWKcUa z%T%xbR4xtYCFc*I(vICn@KI;PSlT9DO<2o{+K(hvlQHA&Q*LDn6EM2PVnsw)PaYO% z=97UuJrWM?)7_pVtEFaEG@paZqGfYSET5WqJz(YG3(Md1Oc?3dnd)33$-ZfVt~0V9 zd{3?lEE}S4S)Ux?tAU~w0$%4bTa2dZ*QAp=zq)nxY2-@Gn;B7BB+xCT8}Fzv;=V%` zJ09--$Pt3ZZv3o@FOB$)lg4sB8%IJfGdGZdi`Pf|zJ}_kwg*HzbX67lh>qqk9+gQO z#|bW~Zo_W+dPI~#R#T>&daGrA=`t^RnO3nq3dE4}4y%g7t2B&Aoa`u8{n^`S$u+YH z5IwwIcUe!FW1UYuioX{O0eqPWA&GF+JqLZRush62GTfyt)2*7EMTW|{iN<f0j}4{rMFFN+w&IoF)-xz`^BZ3BBITxeL>=gU5!Y|_lvx3Oe2S3*f! z=<4wl%THy(jE|F`E;nF>H{|Q-adM@0Fx`WpoaaXt`51`ci`t%7(|wJTov%cA3jMq2 zOhvti4OsABq0FlDnE*3xc4)x-AO}JVL>4g1sk(@&yQR>s(*LZshK>EeDZvteSGwyG z%eBVB63EBZc6^t1&}hu_P_II%V|2^4{x)5w+!u=sHSbm}Esf=Aa(3@hQ#8)&<=JOM z|GEF#%DOp9`( za_#$%mb1g1I~J#7zMu41HFwXUp@S;w@HvN8y0;?i>?iJ|0#z+tDo5ACt}F1%FA;G9 zC~+CP{6SJ~p$5Wj*bmz^zX*;{jT~uP-EVW3%5~NwGWm*-H9ELaB-b0v(xMbSZLqgZ z(3o1|9B8wvNBF(ilNT{cfY8tJ7`;;uijQK1CFwI!K=v;Cmh{%`Nq%pZ6~j>$vB$Y< zq%fOWmavrSGr4VnOd_n~3}iN_BZ88Z?hsd47MAcXb(qKx3&pihwykJVSwjbsAkL|y z)iM>PFZCLi$HsH{<_OR3@aKn;kX45PS!Y^LvUG)7*WC)F;u*fyv;3>pU1=J2$Wp`8 ztVC;AryldvHIpdofLUO6d<&mUS#kn70qyE4;@2q`Y*kQ~ysdZgoAENP13~HH9C-fg zr*g*T-*ye{cA%>v7Q}B*C2>alEyTm3`^UZrlrVT#E}@$E%PKvn+Q(Y97)NrT@KXZS zfRb}U>9U)wUgf?gSs7hWIw!u;G%rnEu5xl>u@^gi5X_MiuhzOT|BhTKog`&!M`oRj z$F#k)BQ#v-U5825NFei#r``cWtyQz%uxrru7;7M_a|tEG zZ(JaD0l9(Qg-+#}LB=i4jUr?vY~;P68&nf_gnIhER_r*bJw0LW#`Yt?n>VGLi1}%| z0~Rsz)FEKvl?Szd;1k zE_;oeiAW5hH&riUDmZHPkhHn5G!cm|vF=Hr%$`akLJbIqY}n62yAd>nh4()+5wB3I z4r(ODy4ZN1g3ogyK6L@iU+At8V!!rU=viKFgl;W92!RYR&Ya;-x|bX=UN6m=)*@cn)x}zj ztD};7`HsilUlk!p5g({o;QbBQkY{g)u72i!A=|^-XFE!D#oODv;2>O*KBBrZsJ~$vTXJ7LwhrDZ+ z1LBC82kt<-K8NOQKhexf;gO-=-!f?=a)w9-8hI>fzSJSYi&9Xli2D4T%bHeT!yn1> zAh*#PNg_f`!wHq^{ZYgXO>!Zwa_}>m)WpBV3<%) zqi~Atu8|?s$Y1RDTV(m_D!wH|E*jAiR5JW+8y3}5$tWX3eV2%g6{-QI+fY9^B;b|s znm3!7j8D8{la{}`sZ-n+-14u?2Wq$`-+5l$yzL4-tR0b4H)1ug8{pQW@N4%yKsG9Z)0h=#oHLLiHh7E(wF%->Sz1XzLk;sB(1sibFxPs%C+N2$`Ps0 z2qZ7&)F#sK@qk9J1xZN<3EicJ^-7vw-_Y|pa}6zNAWx1UAC)8DSNPVxB+$@7mPPi20qmw$5$kQ)~XYXbgW)DKSCZT>BE zJi1}?iWm)2>^8-5w6cXmui?Z3LnC{{(Yf15{T4}GSV|IoxYzg}kD)nlb}kHFp1q`4 z_3+l^c8wFa<@ezFo(c8{skw)28#3XDQ{H##^~bMeB)UKFWPY{G+`5FW@XUb1qZ_Xd z6hBImo>WA5K{%U`^UV9StI%!R^fJh?x*?XlML}LJ8BaE>At_yrpeB@CN0-IIFUY*$dTjo^98Aw^hK2(kb6OzyI(%nYtWY`NPU_K+IqB7#x?a0$X8j7V z&9|Mj8ES%KlH63o(?}h3^L8i#Q?MFdUSsM0qKl8w!HZ^4Qnxyt zg$YOQzJ7aa{t`*qBeSmnF~IhjvuzV;ay#mZ@l-T#tGDYx8Tf;zw<8u|%X=Q#2ev}` z@ooLLw{A`l^LJLXtNh!E^qJD1lG~nTlUNZMxQ*1vS9qfxgBPD5sk7$;iD=;^H-a>F zx2YwZ&{}G_!%|wi?tOZ3TQ@Dzk#M_p?KGFLFXQj1;Xq0?=@OvJtYSZq4BlmYGs5U! z*$1q>Tm`O_KSb$U-0i-LJ;MO&u;<$Z#)FtvN_Ww*`av8Y_XYYj4L+8OPlP~tciT=} zDNrPQ?RKC@?d%-y#C7~bX6Qwd(5}6ukDS#Y)P%6i?)Xv&rr_%V@6YJB#CG9IVJlff7;7Dt#sVDumB;&7lTvoN|ImKnhy7h9(Ry~8spgTOlQ zVgin$v19HI*S(UlTXr3prr~W>WjX^+xpm;xW!bF4B3jz;K+vnXpPoOBx90?R{2YE6 z8f!Ci>5mt6(b8%qpE7Xz$>kt)?vJA zpiNXt@OtRGXH29q-w7u$-O1LGNVT7js57)T?kZ*>CJ0@$w?*ILq+*~^*pZ_*?<+kH9Au6Ux8g`lN z-CjCzfl$W2xC0|j-kMuo;$6&FGjR!h*cW$$k-TDbu#Ef-FOZ@-aoaK_^B<{j5#;&Y+YfVR-x4f+6hj@K;5#o`*|Er2Q@(i(_) zOPTJy+c?6AsnYQ*`j)4xhM#oJ9qblvdL;4|4;*%Xmrp-3!*A|FE6*&F|I_+wKfhzZ z+oG@T=r8g125TbX0BnU{3{`0#b{eiTUIeL!= zr}7~V8US`Vza~33Et1f?;k!@1DFILNJjJQHfHV4sSUpu2`K^YP6*m5CiN|5dA>SiWGOvbNLfP4fhlQu*x}>k5jGN9AKtUT!V=oWfEWTZ(CzqXfXXtNl zVO~h04xNwwS{ksC~Sr_iQi5fo>+X~AXb)ro#ha_La$bn`7a8VQWc5DjiF$lCXZG9&Yabdc^Sw3kt zybxZdxVwsb+$oG~c!vk6@;~XjYfQVm7+x)4US%h{$K!W#Cf;$rK-g<~(Gh&X|M4zV zpZ*rr_InEBO?FMpp33SK$j5ynuVQdA)8=-VR>;SdyMPzTYs_nDffKv_)*2N?F9b%n z>g`bRG=dJSd$wyw<}zpTLDzOS8_;R@fMk^31{+5Q;|Nf9Gcdyovdnc{KyVNZVIWbM0U&TcecfnewP>{%rpnWe1cypoY6h*2dOWv--)nX z%%-@GOfBo(w9Lp2^WqT^X>Q%khR27FL|g2IYNX^@?P2}|97MWIN)HE}N#6#1Hmp)8 zdXT~@vE-BENc}vS+e|I{+E`4GOR8@kV-hyE969Q7`y%X3T#Y^`QH>$0Gw z==}yS!niBec`1|*v@=~^y-FAb>ebx<9nYUc>?|osd~g0-HB+?GXdNTP4wlW zDK|p^EZnXTt;^&2c)N`yh>|A=`CZ(SUI?0-NjhGZkg`pKMAcFP4_c%84R?t+RHy-xQQm?LxQSPDk)?BDfw_{40y`1*Axt;dMmA;hWT>#-ccFu&zMQl43v%Mx(_9$ zKe5aOR;1;OAF4KrbF%X>m9e4HmkpZxkDw#4`5PG|_y%@sJTn!0VW===Ph&NO4EE`K zERY7~HZ_U^RIKCxTJ%j*iR;*P9C`5INH;Q?4okVb&e>?>MX%lCefc(4Pcrv{vKZ}3 zc~c1KxT0ih!CY$Y%_E!AT=e!i<-KytR?HJe)xGw`M&HySp?r9_YNlLItPSTuYl7g= zy}ib&lqv%J+cGRgPi`5@ax}jRh6DPNhpaQ!ymx31%oi3*0)n#BLI&v|5x9IOq?q96 zP_IHQ&D`#Rd-EOq-k3jTay4k}9H%bcaPTM&)s1Z=Q-m2QFV3R4y(C!^&6M4h*SFe^ zaIV9c3j8mkpMWv+?KKKm>9P>0_QxrTf>nmoIv9yKF!Y0)TEy}Qu;(XQOP`^{LD~>JUl6Y?L{XmZ;4n8zr@Cny^Qn*XYf9Uk>c!eDl4c*8r zSgve)a&wK*RMM{+4c^XU)-+4Q=H^e)I=Mv|nEH_Ix81!R(Q`_r2nwa%r<_1ayOjI! zcKoMMN=xC%Wwc}pA?Mj`nQ>6>$?>T0_3foc7tN~jT9e2{gq}a_b=8bX5rHzp6}*rB zDzD>%%_41YL{;0obFKpO*CM(amg6?@GCMZNf5`JXrOHI<9p)da^VGJrmPjEQPE|MQf|&0TpA0 z+(OQ+qBvF}`)W%R$$zE4U zbJufqA&1HNiV!mAo=AUr`gdYh~-ucrV{nn0>59RWGZ7 zk0a?F#QKW|=zZR()M}8&E(R9J0ViIG0FP5Dg>b|Z;wjkXtwRY6Ub`MvayO-D87U9D zOcqYiH0@pG(cDd$<~_8bzz04}XxO3h1H9Xsexe&*m)qK`aUPPdF4X3|!JS5oYgLS| z<|Rd!<0WzT!MpJ@o(c#W=;dCyY{NA^cUrcOUAKWw)4!KjT_|r49R@Qtl;4YMpbKuo z{7WR{@mx;0XV3ZE(>MIcUuw)HS4b9pxq&9{=i&5tX=XhzYG9fqA?ygm>)`Ir(vt2H z(q!`4)zr;(wOQk*D_s==4)07+4)60|SHzeft6Z}a1={Un<>m~yqE15DD5W2WTQGzV z>tZ(eU=uDZAr#LtJBnw=qV@F*ic?e6FmqEak+Vvgylnb3V{j&;*fc%`U_|lD8xF) z(uKzyfZom3EV%nQ3vM{D)T-vIduwE?Zi&G=MCQ+lN47(*TVbfyB`aA2dPwO>=0}bf zkT)6YmEz289V=@+8uSZt=`?=*u&J(Ox0f^^`{)|JimI}i?xeX;TK z6wv_?6N=<3mnP|`hZ#&Q7n6YJomOKF`43iN zE6swiOQflko=Z0$TcaghSiefMMnUl$@faiXce=g>^AqzZ;``XmGe;LjDBzkW;wrPG zZ@3#LMi(j@s(ngs3+9LOr6QAi43L7X?sMoaV}2GfN%6I169ccsXQj8HzZT~Ud+NyR zAHQ{*K;djDi#Z%DM>vC&})>Fmm~-&r37?~k~)P= zhniQTe|k?Dp*tnbwsGB?`YqzmHEsTWuHPHHkOvM6S}0d&-|t<8fw0uooOaRkAAjjw zI;rEvaFyPZ0m)$V(x8LzJGsSzhq!V%Dhs3 zJ{!hIC);0{f2p1qLQ9}L#VmnnR->e%o$6q^ ze*}#}O57~jZTSj-<&XLy92VOr#JGlqnEgg}q!P42plhr@(x_xt?UP;Tz%{eQDbF`TV{)3P|C!TriO_xnS{$)HKP&xexV`dtpzl);YAOv5>+o z3%87gj&~yR6or^JrJpP2hTK%`nM(v4!p+;pVl$ksNTB@Ic%}nA6I!3)gac_n<>Qhi zbV*GTFR}QJ4iv6Y?b4u>6-ZxS!zgVUWft`t5@UwfZ_h5YAnc{tkVwc@b2M=23m;C^ z!r|aau4N+Y3+SIiE(82VgcMfO2Xqk#zqoUp69rHaOT^sr*T^Y;cjTkZLT$0t9Nv0T zA~8!GRuB&9(dQE1V%)Dp{95OIHQFd$8IZzTo`~!cbrDerNq_l3zA~DR`H#ltwB;Kx zOPG`qR1%rf(jP6WDDsnC)IFr<)>l{oB+uYx#7HY)J)E|`m1p<~@YeWFdNsMuW9`zZ z*R&>o%@yR9m9RLB2V)QG3us3!uQ~Cl8&Le(nj$q;1_8uOwsr;1;?@G6f#X}R?HUaKfFR@lBgT(DRl@!BdTkPjvj-l=$I z12e%BvAiln=}#odx^kd-M+RmpF9PwGeDKn4`;8N{A=D&#@}5p)^j3YK$Xzq-)ubTo zL?KfKezP?4of9PcJs-gk7%ggMuq0|GI-%^mrb1Ps;d zzJKM?8Fz!POb*S|bEQ-cZcNU+LMz5rI%>cRWW7|YFup3mXBm$H2dxHi>kod@ARb3rb&{v zMaIT})`{uKs}A$>zB&a5~y0^t+a?_x>GiEfXUee~23z5bTc zmu8sN8VAfFbId>O=A7z7NEcaBY*#p3bRvs|6KXkfo#%D6csJM#suPpb5YWRYZ^Si)W{+qERpCr6+zA8FEM>Io|5$|YY+A0&N|6PhmjjBj1|SsuiV z9T32K&pdybSHLs~lPxX_vX7|=4nH|6A!1GGb5epknW6@vTue4uPG*M-hf~aeFEoaDhDiJpb9J4+d44nRYVj z$@S@KRJV(9v7f@{Q3ij+im}spb*%wk^6<^MD^?d?6i>N~1u{*2N-IDuz}gA3o=MMG zO7443LJwBXuptqu^iR2Iy<+lPfkPo(elU+v#y+?jaB@8Vx^7+kaM_rExopcOP{+a6 zbXLDgUF)Vs(-L1#nue~~Uy*0=f?H1DZQ02A-(NX*{^7&FylfvoZJ+cHAD+K{?)+yb z&rbVCPj5ea^8DG;{^{v?^UafI-OI=P#Wyd9{@Q+qzqt0CF!mDT`gi@wi$~9%ULO(C zAIE;aKL6#D_VMFSo;~jOV@rqTujXZ*?u#Vf)VnDzYg0FUoo$+A(+x${_4U}=-_N(F z&z`&(oBI0e`pUV){J)`n{Gy+K`TLJo&J|NHr320geE!L^$Dg$)k4}FW8_BefBW-Em z)03ALH2?WR_0{_?cijB<)-tXBown1+E=N{u?SkUQBdZbtrz$u-1OGR`({;If_Ozp$ zX#8_({-1$QxZ^*vjs2(8TtUuXz&EA+rx7Rj{*qPxpKAM0BU0i0C2PDt<%#{Lf(Dh@ ze`=gRjh~dxpT^%S|4*sYJKd^P?lcb2dAdmR6iJL@-n;*8$>?lfk8&z$DWAKj6d zuX6Cm+}Q&2Q33wuE}i*zMItJ&7Mf>zupL}Lw7-nNVX@Zi!ztQzC_9t&#KljGl zL;c2;b8n2Orzh>xhtE#Be){{nbk5#*@ooFOAAi4?Po#0_%DErU{j2}ux=y$fKHX8+C+%6LuH(>NHQU|Jmvg6*SA-Q2j~5Kc`)qL=FlDrWed_rCY20}1)~1G ztG8qU?dostJThYvM&n+%XOXILXgPOOCnM7uv)v>kQTkCHcSBO5?}baghr?H=MnGWw zlojzz!2{&bLoyhV4@vM%%PF;3Q2hGb@iKU}%M1>!nlJaf{6>u4SC^Pp{?27d_58)l z7BU$*_JCF#qXSudLbx|K+aG>QNF-sB?!>YSI5yH&P$C7`H$~5TEZ#x@yB!B624MSXvsPbKtekKp(e-zGAs- znY2IjYp!#KPg17~EBKHdFZ1uEwgpT3+4lF|3$e>gYS3YGXtM?yXr`BaS*nG#me@)V0_N&BQ)MVls%8q)ZjbvwF>Qx!)YQK0ab9q#&<3D~+Je zeZ}@nGW?P=xlo*sgk=E2H)7_0&0lEUi+|8iX(yL%;U2er88JM~jzAa4gNDoqw20aD z`DS+Ae>q>3>gOkmC*g#dOt}AO$7_$)nylNqfl|**Y}<0Y*tWM@<~MX~8I`sKeP7MD zhm=M$aT=9;$BE~U&r#iTwTiiifBR_2+K27S z(`QfG=MNu0`lg-qD&o^eUGTGKLw|KrS{wZ`P-A0lb6lDIV#0gEcrlgs?Qi=hkM`~3 zFMoJ(+CRBkc4?Yyc6FU+BMpp8y04qQ$m=94%Czn}qsm_TGE2tFbMx=((_pUtx&Prh zQp8`)n_W@tvOLT7W9vmz?z?^2)>XP`x*~7d^V2%Kc=q_aX~2GaUlL?#FDrLNk{4~) zbi1w@n(<;D_E}T+ZQT_`Go1f%YLFNC{CTj*_WZTQ<&Rz*K8mkoyXm^JZFYU$=i?yT zVHomh5t~h!?KVS(R+5MVP5f-V1yoy6(=J>C+R_4r0>#}WxVw9bJG5wVDXzsGiaQib z3KVw@#hpTMcXtUAlKi~iz29B;zxS^7By-m6d1miqXPvXonaPYPkBCa0_!4=Y=s=sx zIsgbaOvv*{$P3^eLo9gpS6R`-oh{ZX-AZd;yVnq5bSzm&cb<(D61&y`0S{2Y3U-T?L-rRp2(b>$0*ceKVIJp}@0ss`EdB~gdur3I4zpn=| zLb%nzsruBW$?KM!5J;Yvdko)DM>YHYPPkc}-(txBr_nhlega&CS~Hw= zmM1LyIbB|D_%J0tE8iFhyY4$sghxi+KQU{}qMxrnRh{3@ z=wAeMZ!13V>YLF9McRi<2;`Th+k->k<+lEpTL>4$V+5aX@vl8RM7zjA2T?r6<6<$h zn9Ah@V(Y9QbAK(fpx_bNhdFJ^+`8C+T2)fZ_ZLRqAB+qnFif4aS+9xviJ2q=ffNl1 zM3jzyBcOpx(2KID{I>g%`~dlO5q};87m~$Cd~}No&fj2z)#Lr#d({J~x&zp17z@nd zh-)~2N62xCNw&FY!C^}QBJgnXyv`i8tsd*19?NC77pAP)91Z8j5Q;PAy#xR`qJq$n zOB9p0!*)@{ z8v|S1GYvtq@Oi&;Q&SFdd2x-dYf1)a*GqAFnx%#zS5LP5fRDQ0B$Mot3I8?>jePY% zJRPFBb>9IW?+{$0XRVM*q8%XWL!NGn;ArI{>8_rW7icRXPI&5q&CsjFyKAA~B+2kT zfo%#>{pT_j?wxGjD0nR2a1X%ySz}{5GRnSVl^wlEe_od58Q7PGG87*rxB)`Euj>=U z+&#yvQ8S*7t!;9O`x{pmwK;!|r?}^>>Gu7L`VL>0G=?pF6xCM+MJ%wRR{cAtttrWck**HzrC4p4XQ=``ebN}f;@gD-a=N3TMT#Y8jKB3PPS%<^e9%^pS+u@Psq;| zDU{8z7)5p0eu@sf?|qyhnN+w(m6Q+-?e_J26`2IWFRIUg^tj$UL&19#;_zMM43TR9 z@;Vzfl*}jCehH0%KVC5A68C+hCc3C=I)WfQ5GdG%&~Kgz@~XOlqvH!9-*zNofQ8NP z@6o~tbPoWOepoj=TSXifLxlbPC;T2w=U6T>=t)jjUqkkm2Ko(}QH^V;`};bZ9WCaY zhHF%29FBj&`<(i(_OEfgLAiuGgknY$dTA�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

+ef(duN+T-pTjI1m~Az3GAMn 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 Kth3vN4+#LC5$dG? literal 45026 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 set[str]: return working_platforms +class ModGufeTokenizableTestsMixin(GufeTokenizableTestsMixin): + """ + A modified gufe tokenizable tests mixin which allows + for repr to be lazily evaluated. + """ + + def test_repr(self, instance): + """ + Overwrites the base `test_repr` call. + """ + assert isinstance(repr(instance), str) + assert self.repr in repr(instance) + + def compute_energy( system: openmm.System, positions: openmm.unit.Quantity, diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_energies.py b/openfe/tests/protocols/openmm_abfe/test_abfe_energies.py index cfd4678d5..55b3cd426 100644 --- a/openfe/tests/protocols/openmm_abfe/test_abfe_energies.py +++ b/openfe/tests/protocols/openmm_abfe/test_abfe_energies.py @@ -17,8 +17,8 @@ ) from openfe.protocols import openmm_afe -from openfe.protocols.openmm_afe import ( - AbsoluteBindingComplexUnit, +from openfe.protocols.openmm_afe.abfe_units import ( + ABFEComplexSetupUnit, ) from openfe.protocols.openmm_utils.omm_settings import OpenMMSolvationSettings from openfe.protocols.openmm_utils.serialization import deserialize @@ -147,11 +147,11 @@ def t4_validation_data(self, benzene_modifications, T4_protein_component, tmpdir dag = protocol.create(stateA=stateA, stateB=stateB, mapping=None) - complex_units = [u for u in dag.protocol_units if isinstance(u, AbsoluteBindingComplexUnit)] + complex_units = [u for u in dag.protocol_units if isinstance(u, ABFEComplexSetupUnit)] with tmpdir.as_cwd(): - data = complex_units[0].run(dry=True)["debug"] - return data + results = complex_units[0].run(dry=True) + return results @staticmethod def get_energy_components( @@ -182,7 +182,7 @@ def test_energies_regression(self, lambda_val, t4_reference_system, t4_validatio energies_ref = self.get_energy_components( t4_reference_system, t4_validation_data["alchem_indices"], - t4_validation_data["positions"], + t4_validation_data["debug_positions"], lambda_val, lambda_val, lambda_val, @@ -191,7 +191,7 @@ def test_energies_regression(self, lambda_val, t4_reference_system, t4_validatio energies_val = self.get_energy_components( t4_validation_data["alchem_system"], t4_validation_data["alchem_indices"], - t4_validation_data["positions"], + t4_validation_data["debug_positions"], lambda_val, lambda_val, lambda_val, @@ -223,7 +223,7 @@ def assert_energies(actual, expected, nonbonded_lower: bool): energies = self.get_energy_components( t4_validation_data["alchem_system"], t4_validation_data["alchem_indices"], - t4_validation_data["positions"], + t4_validation_data["debug_positions"], lambda_sterics=1.0, lambda_electrostatics=1.0, lambda_restraints=1.0, @@ -236,7 +236,7 @@ def assert_energies(actual, expected, nonbonded_lower: bool): energies = self.get_energy_components( t4_validation_data["alchem_system"], t4_validation_data["alchem_indices"], - t4_validation_data["positions"], + t4_validation_data["debug_positions"], lambda_sterics=1.0, lambda_electrostatics=1.0, lambda_restraints=0.0, @@ -249,7 +249,7 @@ def assert_energies(actual, expected, nonbonded_lower: bool): energies = self.get_energy_components( t4_validation_data["alchem_system"], t4_validation_data["alchem_indices"], - t4_validation_data["positions"], + t4_validation_data["debug_positions"], lambda_sterics=1.0, lambda_electrostatics=0.0, lambda_restraints=0.0, @@ -270,7 +270,7 @@ def assert_energies(actual, expected, nonbonded_lower: bool): energies = self.get_energy_components( t4_validation_data["alchem_system"], t4_validation_data["alchem_indices"], - t4_validation_data["positions"], + t4_validation_data["debug_positions"], lambda_sterics=0.0, lambda_electrostatics=0.0, lambda_restraints=0.0, diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py b/openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py index d5d963ff9..21d1009a8 100644 --- a/openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py +++ b/openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py @@ -1,6 +1,5 @@ # This code is part of OpenFE and is licensed under the MIT license. # For details, see https://github.com/OpenFreeEnergy/openfe -from importlib import resources from math import sqrt from unittest import mock @@ -23,9 +22,7 @@ ) from openmm import unit as ommunit from openmmtools.alchemy import ( - AbsoluteAlchemicalFactory, AlchemicalRegion, - AlchemicalState, ) from openmmtools.multistate.multistatesampler import MultiStateSampler from openmmtools.tests.test_alchemy import ( @@ -38,11 +35,16 @@ from openfe import ChemicalSystem, SmallMoleculeComponent, SolventComponent from openfe.protocols import openmm_afe from openfe.protocols.openmm_afe import ( - AbsoluteBindingComplexUnit, AbsoluteBindingProtocol, - AbsoluteBindingSolventUnit, ) -from openfe.protocols.openmm_utils.omm_settings import OpenMMSolvationSettings +from openfe.protocols.openmm_afe.abfe_units import ( + ABFEComplexSetupUnit, + ABFEComplexSimUnit, + ABFESolventSetupUnit, + ABFESolventSimUnit, +) + +from .utils import UNIT_TYPES, _get_units @pytest.fixture() @@ -75,37 +77,53 @@ def test_serialize_protocol(default_settings): assert protocol == ret -def test_unit_tagging(benzene_complex_dag, tmpdir): - # test that executing the units includes correct gen and repeat info +def test_repeat_units(benzene_modifications, T4_protein_component): + protocol = openmm_afe.AbsoluteBindingProtocol( + settings=openmm_afe.AbsoluteBindingProtocol.default_settings() + ) - dag_units = benzene_complex_dag.protocol_units + stateA = gufe.ChemicalSystem( + { + "protein": T4_protein_component, + "benzene": benzene_modifications["benzene"], + "solvent": gufe.SolventComponent(), + } + ) - with ( - mock.patch( - "openfe.protocols.openmm_afe.equil_binding_afe_method.AbsoluteBindingSolventUnit.run", - return_value={"nc": "file.nc", "last_checkpoint": "chck.nc"}, - ), - mock.patch( - "openfe.protocols.openmm_afe.equil_binding_afe_method.AbsoluteBindingComplexUnit.run", - return_value={"nc": "file.nc", "last_checkpoint": "chck.nc"}, - ), - ): - results = [] - for u in dag_units: - ret = u.execute(context=gufe.Context(tmpdir, tmpdir)) - results.append(ret) - - solv_repeats = set() - complex_repeats = set() - for ret in results: - assert isinstance(ret, gufe.ProtocolUnitResult) - assert ret.outputs["generation"] == 0 - if ret.outputs["simtype"] == "complex": - complex_repeats.add(ret.outputs["repeat_id"]) - else: - solv_repeats.add(ret.outputs["repeat_id"]) - # Repeat ids are random ints so just check their lengths - assert len(complex_repeats) == len(solv_repeats) == 3 + stateB = gufe.ChemicalSystem( + { + "protein": T4_protein_component, + "solvent": gufe.SolventComponent(), + } + ) + + dag = protocol.create( + stateA=stateA, + stateB=stateB, + mapping=None, + ) + + # 6 protocol unit, 3 per repeat + pus = list(dag.protocol_units) + assert len(pus) == 18 + + # Check info for each repeat + for phase in ["solvent", "complex"]: + setup = _get_units(pus, UNIT_TYPES[phase]["setup"]) + sim = _get_units(pus, UNIT_TYPES[phase]["sim"]) + analysis = _get_units(pus, UNIT_TYPES[phase]["analysis"]) + + # Should be 3 of each set + assert len(setup) == len(sim) == len(analysis) == 3 + + # Check that the dag chain is correct + for analysis_pu in analysis: + repeat_id = analysis_pu.inputs["repeat_id"] + setup_pu = [s for s in setup if s.inputs["repeat_id"] == repeat_id][0] + sim_pu = [s for s in sim if s.inputs["repeat_id"] == repeat_id][0] + assert analysis_pu.inputs["setup_results"] == setup_pu + assert analysis_pu.inputs["simulation_results"] == sim_pu + assert sim_pu.inputs["setup_results"] == setup_pu def test_create_independent_repeat_ids(benzene_modifications, T4_protein_component): @@ -137,9 +155,12 @@ def test_create_independent_repeat_ids(benzene_modifications, T4_protein_compone repeat_ids = set() for dag in dags: + # 3 sets of 6 units + assert len(list(dag.protocol_units)) == 18 for u in dag.protocol_units: repeat_ids.add(u.inputs["repeat_id"]) + # squashed by repeat_id, that's 2 sets of 6 assert len(repeat_ids) == 12 @@ -149,7 +170,7 @@ def test_mda_universe_error(): when calling the mda Universe getter. """ with pytest.raises(ValueError, match="No positions to create"): - _ = openmm_afe.AbsoluteBindingComplexUnit._get_mda_universe( + _ = openmm_afe.ABFEComplexSetupUnit._get_mda_universe( topology="foo", positions=None, trajectory=None ) @@ -201,17 +222,32 @@ def dag(self, protocol, benzene_wcharges, T4_protein_component): ) @pytest.fixture(scope="class") - def complex_units(self, dag): - return [u for u in dag.protocol_units if isinstance(u, AbsoluteBindingComplexUnit)] + def complex_setup_units(self, dag): + return _get_units(dag.protocol_units, UNIT_TYPES["complex"]["setup"]) @pytest.fixture(scope="class") - def solvent_units(self, dag): - return [u for u in dag.protocol_units if isinstance(u, AbsoluteBindingSolventUnit)] + def complex_sim_units(self, dag): + return _get_units(dag.protocol_units, UNIT_TYPES["complex"]["sim"]) - def test_number_of_units(self, dag, complex_units, solvent_units): - assert len(list(dag.protocol_units)) == 2 - assert len(complex_units) == 1 - assert len(solvent_units) == 1 + @pytest.fixture(scope="class") + def solvent_setup_units(self, dag): + return _get_units(dag.protocol_units, UNIT_TYPES["solvent"]["setup"]) + + @pytest.fixture(scope="class") + def solvent_sim_units(self, dag): + return _get_units(dag.protocol_units, UNIT_TYPES["solvent"]["sim"]) + + def test_number_of_units( + self, + dag, + complex_setup_units, + complex_sim_units, + solvent_setup_units, + solvent_sim_units, + ): + assert len(list(dag.protocol_units)) == 6 + assert len(complex_setup_units) == len(complex_sim_units) == 1 + assert len(solvent_setup_units) == len(solvent_sim_units) == 1 def _assert_force_num(self, system, forcetype, number): forces = [f for f in system.getForces() if isinstance(f, forcetype)] @@ -356,83 +392,99 @@ def _test_energies(reference_system, alchemical_system, alchemical_regions, posi positions=positions, ) - def test_complex_dry_run(self, complex_units, settings, tmpdir): + def test_complex_dry_run(self, complex_setup_units, complex_sim_units, settings, tmpdir): with tmpdir.as_cwd(): - data = complex_units[0].run(dry=True, verbose=True)["debug"] + setup_results = complex_setup_units[0].run(dry=True, verbose=True) + sim_results = complex_sim_units[0].run( + system=setup_results["alchem_system"], + positions=setup_results["debug_positions"], + selection_indices=setup_results["selection_indices"], + box_vectors=setup_results["box_vectors"], + alchemical_restraints=True, + dry=True, + ) # Check the sampler - self._verify_sampler(data["sampler"], complexed=True, settings=settings) + self._verify_sampler(sim_results["sampler"], complexed=True, settings=settings) # Check the alchemical system self._assert_expected_alchemical_forces( - data["alchem_system"], complexed=True, settings=settings + setup_results["alchem_system"], complexed=True, settings=settings ) - self._test_dodecahedron_vectors(data["alchem_system"]) + self._test_dodecahedron_vectors(setup_results["alchem_system"]) # Check the alchemical indices expected_indices = [i + self.num_complex_atoms for i in range(self.num_solvent_atoms)] - assert expected_indices == data["alchem_indices"] + assert expected_indices == setup_results["alchem_indices"] # Check the non-alchemical system - self._assert_expected_nonalchemical_forces(data["system"], settings) - self._test_dodecahedron_vectors(data["system"]) + self._assert_expected_nonalchemical_forces(setup_results["standard_system"], settings) + self._test_dodecahedron_vectors(setup_results["standard_system"]) # Check the box vectors haven't changed (they shouldn't have because we didn't do MD) assert_allclose( - from_openmm(data["alchem_system"].getDefaultPeriodicBoxVectors()), - from_openmm(data["system"].getDefaultPeriodicBoxVectors()), + from_openmm(setup_results["alchem_system"].getDefaultPeriodicBoxVectors()), + from_openmm(setup_results["standard_system"].getDefaultPeriodicBoxVectors()), ) # Check the PDB - pdb = mdt.load_pdb("alchemical_system.pdb") + pdb = mdt.load_pdb(setup_results["pdb_structure"]) assert pdb.n_atoms == self.num_all_not_water # Check energies - alchem_region = AlchemicalRegion(alchemical_atoms=data["alchem_indices"]) + alchem_region = AlchemicalRegion(alchemical_atoms=setup_results["alchem_indices"]) self._test_energies( - reference_system=data["system"], - alchemical_system=data["alchem_system"], + reference_system=setup_results["standard_system"], + alchemical_system=setup_results["alchem_system"], alchemical_regions=alchem_region, - positions=data["positions"], + positions=setup_results["debug_positions"], ) - def test_solvent_dry_run(self, solvent_units, settings, tmpdir): + def test_solvent_dry_run(self, solvent_setup_units, solvent_sim_units, settings, tmpdir): with tmpdir.as_cwd(): - data = solvent_units[0].run(dry=True, verbose=True)["debug"] + setup_results = solvent_setup_units[0].run(dry=True, verbose=True) + sim_results = solvent_sim_units[0].run( + system=setup_results["alchem_system"], + positions=setup_results["debug_positions"], + selection_indices=setup_results["selection_indices"], + box_vectors=setup_results["box_vectors"], + alchemical_restraints=False, + dry=True, + ) # Check the sampler - self._verify_sampler(data["sampler"], complexed=False, settings=settings) + self._verify_sampler(sim_results["sampler"], complexed=False, settings=settings) # Check the alchemical system self._assert_expected_alchemical_forces( - data["alchem_system"], complexed=False, settings=settings + setup_results["alchem_system"], complexed=False, settings=settings ) - self._test_cubic_vectors(data["alchem_system"]) + self._test_cubic_vectors(setup_results["alchem_system"]) # Check the alchemical indices expected_indices = [i for i in range(self.num_solvent_atoms)] - assert expected_indices == data["alchem_indices"] + assert expected_indices == setup_results["alchem_indices"] # Check the non-alchemical system - self._assert_expected_nonalchemical_forces(data["system"], settings) - self._test_cubic_vectors(data["system"]) + self._assert_expected_nonalchemical_forces(setup_results["standard_system"], settings) + self._test_cubic_vectors(setup_results["standard_system"]) # Check the box vectors haven't changed (they shouldn't have because we didn't do MD) assert_allclose( - from_openmm(data["alchem_system"].getDefaultPeriodicBoxVectors()), - from_openmm(data["system"].getDefaultPeriodicBoxVectors()), + from_openmm(setup_results["alchem_system"].getDefaultPeriodicBoxVectors()), + from_openmm(setup_results["standard_system"].getDefaultPeriodicBoxVectors()), ) # Check the PDB - pdb = mdt.load_pdb("alchemical_system.pdb") + pdb = mdt.load_pdb(setup_results["pdb_structure"]) assert pdb.n_atoms == self.num_solvent_atoms # Check energies - alchem_region = AlchemicalRegion(alchemical_atoms=data["alchem_indices"]) + alchem_region = AlchemicalRegion(alchemical_atoms=setup_results["alchem_indices"]) self._test_energies( - reference_system=data["system"], - alchemical_system=data["alchem_system"], + reference_system=setup_results["standard_system"], + alchemical_system=setup_results["alchem_system"], alchemical_regions=alchem_region, - positions=data["positions"], + positions=setup_results["debug_positions"], ) @@ -510,15 +562,17 @@ def assign_fictitious_charges(offmol): dag = protocol.create(stateA=stateA, stateB=stateB, mapping=None) - complex_units = [u for u in dag.protocol_units if isinstance(u, AbsoluteBindingComplexUnit)] + complex_setup_units = _get_units(dag.protocol_units, UNIT_TYPES["complex"]["setup"]) with tmpdir.as_cwd(): - data = complex_units[0].run(dry=True)["debug"] + results = complex_setup_units[0].run(dry=True) - system_nbf = [f for f in data["system"].getForces() if isinstance(f, NonbondedForce)][0] + system_nbf = [ + f for f in results["standard_system"].getForces() if isinstance(f, NonbondedForce) + ][0] alchem_system_nbf = [ f - for f in data["alchem_system"].getForces() + for f in results["alchem_system"].getForces() if isinstance(f, NonbondedForce) ][0] # fmt: skip diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_protocol_results.py b/openfe/tests/protocols/openmm_abfe/test_abfe_protocol_results.py index 0314a1a14..5d815c713 100644 --- a/openfe/tests/protocols/openmm_abfe/test_abfe_protocol_results.py +++ b/openfe/tests/protocols/openmm_abfe/test_abfe_protocol_results.py @@ -3,6 +3,7 @@ import gzip import itertools import json +from pathlib import Path from unittest import mock import gufe @@ -14,25 +15,78 @@ from openfe.protocols import openmm_afe from openfe.protocols.restraint_utils.geometry.boresch import BoreschRestraintGeometry +from .utils import UNIT_TYPES, _get_units -def test_gather(benzene_complex_dag, tmpdir): - # check that .gather behaves as expected + +@pytest.fixture() +def patcher(): with ( mock.patch( - "openfe.protocols.openmm_afe.equil_binding_afe_method.AbsoluteBindingSolventUnit.run", - return_value={"nc": "file.nc", "last_checkpoint": "chck.nc"}, + "openfe.protocols.openmm_afe.abfe_units.ABFESolventSetupUnit.run", + return_value={ + "system": Path("system.xml.bz2"), + "positions": Path("positions.npy"), + "pdb_structure": Path("hybrid_system.pdb"), + "selection_indices": np.zeros(100), + "box_vectors": [np.zeros(3), np.zeros(3), np.zeros(3)] * offunit.nm, + "standard_state_correction": 0 * offunit.kilocalorie_per_mole, + "restraint_geometry": None, + }, + ), + mock.patch( + "openfe.protocols.openmm_afe.abfe_units.ABFEComplexSetupUnit.run", + return_value={ + "system": Path("system.xml.bz2"), + "positions": Path("positions.npy"), + "pdb_structure": Path("hybrid_system.pdb"), + "selection_indices": np.zeros(100), + "box_vectors": [np.zeros(3), np.zeros(3), np.zeros(3)] * offunit.nm, + "standard_state_correction": 0 * offunit.kilocalorie_per_mole, + "restraint_geometry": True, + }, + ), + mock.patch( + "openfe.protocols.openmm_afe.base_afe_units.np.load", + return_value=np.zeros(100), + ), + mock.patch( + "openfe.protocols.openmm_afe.base_afe_units.deserialize", + return_value="foo", + ), + mock.patch( + "openfe.protocols.openmm_afe.abfe_units.ABFEComplexSimUnit.run", + return_value={ + "trajectory": Path("file.nc"), + "checkpoint": Path("chk.chk"), + }, + ), + mock.patch( + "openfe.protocols.openmm_afe.abfe_units.ABFESolventSimUnit.run", + return_value={ + "trajectory": Path("file.nc"), + "checkpoint": Path("chk.chk"), + }, + ), + mock.patch( + "openfe.protocols.openmm_afe.abfe_units.ABFEComplexAnalysisUnit.run", + return_value={"foo": "bar"}, ), mock.patch( - "openfe.protocols.openmm_afe.equil_binding_afe_method.AbsoluteBindingComplexUnit.run", - return_value={"nc": "file.nc", "last_checkpoint": "chck.nc"}, + "openfe.protocols.openmm_afe.abfe_units.ABFESolventAnalysisUnit.run", + return_value={"foo": "bar"}, ), ): - dagres = gufe.protocols.execute_DAG( - benzene_complex_dag, - shared_basedir=tmpdir, - scratch_basedir=tmpdir, - keep_shared=True, - ) + yield + + +def test_gather(benzene_complex_dag, patcher, tmpdir): + # check that .gather behaves as expected + dagres = gufe.protocols.execute_DAG( + benzene_complex_dag, + shared_basedir=tmpdir, + scratch_basedir=tmpdir, + keep_shared=True, + ) protocol = openmm_afe.AbsoluteBindingProtocol( settings=openmm_afe.AbsoluteBindingProtocol.default_settings(), @@ -43,6 +97,47 @@ def test_gather(benzene_complex_dag, tmpdir): assert isinstance(res, openmm_afe.AbsoluteBindingProtocolResult) +def test_unit_tagging(benzene_complex_dag, patcher, tmpdir): + # test that executing the units includes correct gen and repeat info + + dag_units = benzene_complex_dag.protocol_units + + for phase in ["solvent", "complex"]: + setup_results = {} + sim_results = {} + analysis_results = {} + + setup_units = _get_units(dag_units, UNIT_TYPES[phase]["setup"]) + sim_units = _get_units(dag_units, UNIT_TYPES[phase]["sim"]) + a_units = _get_units(dag_units, UNIT_TYPES[phase]["analysis"]) + + for u in setup_units: + rid = u.inputs["repeat_id"] + setup_results[rid] = u.execute(context=gufe.Context(tmpdir, tmpdir)) + + for u in sim_units: + rid = u.inputs["repeat_id"] + sim_results[rid] = u.execute( + context=gufe.Context(tmpdir, tmpdir), + setup_results=setup_results[rid], + ) + + for u in a_units: + rid = u.inputs["repeat_id"] + analysis_results[rid] = u.execute( + context=gufe.Context(tmpdir, tmpdir), + setup_results=setup_results[rid], + simulation_results=sim_results[rid], + ) + + for results in [setup_results, sim_results, analysis_results]: + for ret in results.values(): + assert isinstance(ret, gufe.ProtocolUnitResult) + assert ret.outputs["generation"] == 0 + + assert len(setup_results) == len(sim_results) == len(analysis_results) == 3 + + class TestProtocolResult: @pytest.fixture() def protocolresult(self, abfe_transformation_json_path): @@ -62,7 +157,7 @@ def test_get_estimate(self, protocolresult): est = protocolresult.get_estimate() assert est - assert est.m == pytest.approx(-21.71, abs=0.01) + assert est.m == pytest.approx(-21.35, abs=0.01) assert isinstance(est, offunit.Quantity) assert est.is_compatible_with(offunit.kilojoule_per_mole) @@ -70,7 +165,7 @@ def test_get_uncertainty(self, protocolresult): est = protocolresult.get_uncertainty() assert est - assert est.m == pytest.approx(0.73, abs=0.01) + assert est.m == pytest.approx(1.04, abs=0.01) assert isinstance(est, offunit.Quantity) assert est.is_compatible_with(offunit.kilojoule_per_mole) @@ -176,12 +271,12 @@ def test_restraint_geometry(self, protocolresult): assert isinstance(geom[0], BoreschRestraintGeometry) assert geom[0].guest_atoms == [1779, 1778, 1777] assert geom[0].host_atoms == [880, 865, 864] - assert pytest.approx(geom[0].r_aA0) == 1.083558 * offunit.nanometer - assert pytest.approx(geom[0].theta_A0) == 0.6786444 * offunit.radian - assert pytest.approx(geom[0].theta_B0) == 1.649905 * offunit.radian - assert pytest.approx(geom[0].phi_A0) == -0.3640583 * offunit.radian - assert pytest.approx(geom[0].phi_B0) == 1.892376 * offunit.radian - assert pytest.approx(geom[0].phi_C0) == -0.6106747 * offunit.radian + assert pytest.approx(geom[0].r_aA0, rel=1e-2) == 1.083558 * offunit.nanometer + assert pytest.approx(geom[0].theta_A0, rel=1e-2) == 0.711876 * offunit.radian + assert pytest.approx(geom[0].theta_B0, rel=1e-2) == 1.687366 * offunit.radian + assert pytest.approx(geom[0].phi_A0, rel=1e-2) == -0.2164231 * offunit.radian + assert pytest.approx(geom[0].phi_B0, rel=1e-2) == 1.892376 * offunit.radian + assert pytest.approx(geom[0].phi_C0, rel=1e-2) == -0.522031870 * offunit.radian @pytest.mark.parametrize( "key, expected_size", diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_slow.py b/openfe/tests/protocols/openmm_abfe/test_abfe_slow.py index c4c64bd76..3fb58178e 100644 --- a/openfe/tests/protocols/openmm_abfe/test_abfe_slow.py +++ b/openfe/tests/protocols/openmm_abfe/test_abfe_slow.py @@ -96,16 +96,36 @@ def test_openmm_run_engine( r = openfe.execute_DAG(dag, shared_basedir=cwd, scratch_basedir=cwd, keep_shared=True) assert r.ok() - for pur in r.protocol_unit_results: - unit_shared = tmpdir / f"shared_{pur.source_key}_attempt_0" - assert unit_shared.exists() - assert pathlib.Path(unit_shared).is_dir() - checkpoint = pur.outputs["last_checkpoint"] - assert checkpoint == f"{pur.outputs['simtype']}_checkpoint.nc" - assert (unit_shared / checkpoint).exists() - nc = pur.outputs["nc"] - assert nc == unit_shared / f"{pur.outputs['simtype']}.nc" - assert nc.exists() + + # Check outputs of solvent & complex results + for phase in ["solvent", "complex"]: + purs = [pur for pur in r.protocol_unit_results if pur.outputs["simtype"] == phase] + + # get the path to the simulation unit shared dict + for pur in purs: + if "Simulation" in pur.name: + sim_shared = tmpdir / f"shared_{pur.source_key}_attempt_0" + assert sim_shared.exists() + assert pathlib.Path(sim_shared).is_dir() + + # check the analysis outputs + for pur in purs: + if "Analysis" not in pur.name: + continue + + unit_shared = tmpdir / f"shared_{pur.source_key}_attempt_0" + assert unit_shared.exists() + assert pathlib.Path(unit_shared).is_dir() + + # Does the checkpoint file exist? + checkpoint = pur.outputs["checkpoint"] + assert checkpoint == sim_shared / f"{pur.outputs['simtype']}_checkpoint.nc" + assert checkpoint.exists() + + # Does the trajectory file exist? + nc = pur.outputs["trajectory"] + assert nc == sim_shared / f"{pur.outputs['simtype']}.nc" + assert nc.exists() # Test results methods that need files present results = protocol.gather([r]) diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_tokenization.py b/openfe/tests/protocols/openmm_abfe/test_abfe_tokenization.py index 4bfdb1550..46390ca48 100644 --- a/openfe/tests/protocols/openmm_abfe/test_abfe_tokenization.py +++ b/openfe/tests/protocols/openmm_abfe/test_abfe_tokenization.py @@ -3,16 +3,21 @@ import gzip import pytest -from gufe.tests.test_tokenization import GufeTokenizableTestsMixin import openfe from openfe.protocols.openmm_afe import ( - AbsoluteBindingComplexUnit, + ABFEComplexAnalysisUnit, + ABFEComplexSetupUnit, + ABFEComplexSimUnit, + ABFESolventAnalysisUnit, + ABFESolventSetupUnit, + ABFESolventSimUnit, AbsoluteBindingProtocol, AbsoluteBindingProtocolResult, - AbsoluteBindingSolventUnit, ) +from ..conftest import ModGufeTokenizableTestsMixin + @pytest.fixture def protocol(): @@ -33,18 +38,40 @@ def protocol_units(protocol, benzene_complex_system, T4_protein_component): return list(pus.protocol_units) -@pytest.fixture -def solvent_protocol_unit(protocol_units): - for pu in protocol_units: - if isinstance(pu, AbsoluteBindingSolventUnit): +def _filter_units(pus, classtype): + for pu in pus: + if isinstance(pu, classtype): return pu @pytest.fixture -def complex_protocol_unit(protocol_units): - for pu in protocol_units: - if isinstance(pu, AbsoluteBindingComplexUnit): - return pu +def complex_protocol_setup_unit(protocol_units): + return _filter_units(protocol_units, ABFEComplexSetupUnit) + + +@pytest.fixture +def complex_protocol_sim_unit(protocol_units): + return _filter_units(protocol_units, ABFEComplexSimUnit) + + +@pytest.fixture +def complex_protocol_analysis_unit(protocol_units): + return _filter_units(protocol_units, ABFEComplexAnalysisUnit) + + +@pytest.fixture +def solvent_protocol_setup_unit(protocol_units): + return _filter_units(protocol_units, ABFESolventSetupUnit) + + +@pytest.fixture +def solvent_protocol_sim_unit(protocol_units): + return _filter_units(protocol_units, ABFESolventSimUnit) + + +@pytest.fixture +def solvent_protocol_analysis_unit(protocol_units): + return _filter_units(protocol_units, ABFESolventAnalysisUnit) @pytest.fixture @@ -54,7 +81,7 @@ def protocol_result(abfe_transformation_json_path): return pr -class TestAbsoluteBindingProtocol(GufeTokenizableTestsMixin): +class TestAbsoluteBindingProtocol(ModGufeTokenizableTestsMixin): cls = AbsoluteBindingProtocol key = None repr = "AbsoluteBindingProtocol-" @@ -63,49 +90,68 @@ class TestAbsoluteBindingProtocol(GufeTokenizableTestsMixin): def instance(self, protocol): return protocol - def test_repr(self, instance): - """ - Overwrites the base `test_repr` call. - """ - assert isinstance(repr(instance), str) - assert self.repr in repr(instance) + +class TestABFESolventSetupUnit(ModGufeTokenizableTestsMixin): + cls = ABFESolventSetupUnit + repr = "ABFESolventSetupUnit(ABFE Setup: benzene solvent leg" + key = None + + @pytest.fixture() + def instance(self, solvent_protocol_setup_unit): + return solvent_protocol_setup_unit + + +class TestABFESolventSimUnit(ModGufeTokenizableTestsMixin): + cls = ABFESolventSimUnit + repr = "ABFESolventSimUnit(ABFE Simulation: benzene solvent leg" + key = None + + @pytest.fixture() + def instance(self, solvent_protocol_sim_unit): + return solvent_protocol_sim_unit -class TestAbsoluteBindingSolventUnit(GufeTokenizableTestsMixin): - cls = AbsoluteBindingSolventUnit - repr = "AbsoluteBindingSolventUnit(Absolute Binding, benzene solvent leg" +class TestABFESolventAnalysisUnit(ModGufeTokenizableTestsMixin): + cls = ABFESolventAnalysisUnit + repr = "ABFESolventAnalysisUnit(ABFE Analysis: benzene solvent leg" key = None @pytest.fixture() - def instance(self, solvent_protocol_unit): - return solvent_protocol_unit + def instance(self, solvent_protocol_analysis_unit): + return solvent_protocol_analysis_unit - def test_repr(self, instance): - """ - Overwrites the base `test_repr` call. - """ - assert isinstance(repr(instance), str) - assert self.repr in repr(instance) +class TestABFEComplexSetupUnit(ModGufeTokenizableTestsMixin): + cls = ABFEComplexSetupUnit + repr = "ABFEComplexSetupUnit(ABFE Setup: benzene complex leg" + key = None + + @pytest.fixture() + def instance(self, complex_protocol_setup_unit): + return complex_protocol_setup_unit -class TestAbsoluteBindingComplexUnit(GufeTokenizableTestsMixin): - cls = AbsoluteBindingComplexUnit - repr = "AbsoluteBindingComplexUnit(Absolute Binding, benzene complex leg" + +class TestABFEComplexSimUnit(ModGufeTokenizableTestsMixin): + cls = ABFEComplexSimUnit + repr = "ABFEComplexSimUnit(ABFE Simulation: benzene complex leg" key = None @pytest.fixture() - def instance(self, complex_protocol_unit): - return complex_protocol_unit + def instance(self, complex_protocol_sim_unit): + return complex_protocol_sim_unit - def test_repr(self, instance): - """ - Overwrites the base `test_repr` call. - """ - assert isinstance(repr(instance), str) - assert self.repr in repr(instance) +class TestABFEComplexAnalysisUnit(ModGufeTokenizableTestsMixin): + cls = ABFEComplexAnalysisUnit + repr = "ABFEComplexAnalysisUnit(ABFE Analysis: benzene complex leg" + key = None -class TestAbsoluteBindingProtocolResult(GufeTokenizableTestsMixin): + @pytest.fixture() + def instance(self, complex_protocol_analysis_unit): + return complex_protocol_analysis_unit + + +class TestAbsoluteBindingProtocolResult(ModGufeTokenizableTestsMixin): cls = AbsoluteBindingProtocolResult key = None repr = "AbsoluteBindingProtocolResult-" @@ -113,10 +159,3 @@ class TestAbsoluteBindingProtocolResult(GufeTokenizableTestsMixin): @pytest.fixture() def instance(self, protocol_result): return protocol_result - - def test_repr(self, instance): - """ - Overwrites the base `test_repr` call. - """ - assert isinstance(repr(instance), str) - assert self.repr in repr(instance) diff --git a/openfe/tests/protocols/openmm_abfe/utils.py b/openfe/tests/protocols/openmm_abfe/utils.py new file mode 100644 index 000000000..89ded7f8c --- /dev/null +++ b/openfe/tests/protocols/openmm_abfe/utils.py @@ -0,0 +1,30 @@ +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +from openfe.protocols.openmm_afe.abfe_units import ( + ABFEComplexAnalysisUnit, + ABFEComplexSetupUnit, + ABFEComplexSimUnit, + ABFESolventAnalysisUnit, + ABFESolventSetupUnit, + ABFESolventSimUnit, +) + +UNIT_TYPES = { + "solvent": { + "setup": ABFESolventSetupUnit, + "sim": ABFESolventSimUnit, + "analysis": ABFESolventAnalysisUnit, + }, + "complex": { + "setup": ABFEComplexSetupUnit, + "sim": ABFEComplexSimUnit, + "analysis": ABFEComplexAnalysisUnit, + }, +} + + +def _get_units(protocol_units, unit_type): + """ + Helper method to extract setup units. + """ + return [pu for pu in protocol_units if isinstance(pu, unit_type)] diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py b/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py index cf471130a..b2f2e387d 100644 --- a/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py +++ b/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py @@ -1,12 +1,9 @@ # This code is part of OpenFE and is licensed under the MIT license. # For details, see https://github.com/OpenFreeEnergy/openfe -import itertools -import json import sys from math import sqrt from unittest import mock -import gufe import mdtraj as mdt import numpy as np import pytest @@ -18,7 +15,6 @@ CustomNonbondedForce, HarmonicAngleForce, HarmonicBondForce, - MonteCarloBarostat, NonbondedForce, PeriodicTorsionForce, ) @@ -29,16 +25,15 @@ from openfe.protocols import openmm_afe from openfe.protocols.openmm_afe import ( AbsoluteSolvationProtocol, - AbsoluteSolvationSolventUnit, - AbsoluteSolvationVacuumUnit, ) -from openfe.protocols.openmm_utils import system_validation from openfe.protocols.openmm_utils.charge_generation import ( HAS_ESPALOMA_CHARGE, HAS_NAGL, HAS_OPENEYE, ) +from .utils import UNIT_TYPES, _get_units + @pytest.fixture() def protocol_dry_settings(): @@ -68,6 +63,40 @@ def test_serialize_protocol(default_settings): assert protocol == ret +def test_repeat_units(benzene_system): + protocol = openmm_afe.AbsoluteSolvationProtocol( + settings=openmm_afe.AbsoluteSolvationProtocol.default_settings() + ) + + dag = protocol.create( + stateA=benzene_system, + stateB=ChemicalSystem({"solvent": SolventComponent()}), + mapping=None, + ) + + # 6 protocol unit, 3 per repeat + pus = list(dag.protocol_units) + assert len(pus) == 18 + + # Check info for each repeat + for phase in ["solvent", "vacuum"]: + setup = _get_units(pus, UNIT_TYPES[phase]["setup"]) + sim = _get_units(pus, UNIT_TYPES[phase]["sim"]) + analysis = _get_units(pus, UNIT_TYPES[phase]["analysis"]) + + # Should be 3 of each set + assert len(setup) == len(sim) == len(analysis) == 3 + + # Check that the dag chain is correct + for analysis_pu in analysis: + repeat_id = analysis_pu.inputs["repeat_id"] + setup_pu = [s for s in setup if s.inputs["repeat_id"] == repeat_id][0] + sim_pu = [s for s in sim if s.inputs["repeat_id"] == repeat_id][0] + assert analysis_pu.inputs["setup_results"] == setup_pu + assert analysis_pu.inputs["simulation_results"] == sim_pu + assert sim_pu.inputs["setup_results"] == setup_pu + + def test_create_independent_repeat_ids(benzene_system): protocol = openmm_afe.AbsoluteSolvationProtocol( settings=openmm_afe.AbsoluteSolvationProtocol.default_settings() @@ -88,9 +117,12 @@ def test_create_independent_repeat_ids(benzene_system): repeat_ids = set() for dag in dags: + # 3 sets of 6 units + assert len(list(dag.protocol_units)) == 18 for u in dag.protocol_units: repeat_ids.add(u.inputs["repeat_id"]) + # squashed by repeat_id, that's 2 sets of 6 assert len(repeat_ids) == 12 @@ -143,7 +175,7 @@ def _verify_alchemical_sterics_force_parameters( @pytest.mark.parametrize("method", ["repex", "sams", "independent", "InDePeNdENT"]) -def test_dry_run_vac_benzene(benzene_system, method, protocol_dry_settings, tmpdir): +def test_setup_dry_sim_vac_benzene(benzene_system, method, protocol_dry_settings, tmpdir): protocol_dry_settings.vacuum_simulation_settings.sampler_method = method protocol = openmm_afe.AbsoluteSolvationProtocol(settings=protocol_dry_settings) @@ -161,21 +193,32 @@ def test_dry_run_vac_benzene(benzene_system, method, protocol_dry_settings, tmpd ) prot_units = list(dag.protocol_units) - assert len(prot_units) == 2 + assert len(prot_units) == 6 - vac_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationVacuumUnit)] - sol_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationSolventUnit)] + vac_setup_unit = _get_units(prot_units, UNIT_TYPES["vacuum"]["setup"]) + vac_sim_unit = _get_units(prot_units, UNIT_TYPES["vacuum"]["sim"]) - assert len(vac_unit) == 1 - assert len(sol_unit) == 1 + assert len(vac_setup_unit) == 1 + assert len(vac_sim_unit) == 1 with tmpdir.as_cwd(): - debug = vac_unit[0].run(dry=True)["debug"] - vac_sampler = debug["sampler"] - assert not vac_sampler.is_periodic + setup_results = vac_setup_unit[0].run(dry=True) + sim_results = vac_sim_unit[0].run( + system=setup_results["alchem_system"], + positions=setup_results["debug_positions"], + selection_indices=setup_results["selection_indices"], + box_vectors=setup_results["box_vectors"], + alchemical_restraints=False, + dry=True, + ) + + sampler = sim_results["sampler"] + assert isinstance(sampler, MultiStateSampler) + assert not sampler.is_periodic + assert sampler._thermodynamic_states[0].barostat is None # standard system - system = debug["system"] + system = setup_results["standard_system"] assert system.getNumParticles() == 12 assert len(system.getForces()) == 4 _assert_num_forces(system, NonbondedForce, 1) @@ -184,7 +227,7 @@ def test_dry_run_vac_benzene(benzene_system, method, protocol_dry_settings, tmpd _assert_num_forces(system, PeriodicTorsionForce, 1) # alchemical system - alchem_system = debug["alchem_system"] + alchem_system = setup_results["alchem_system"] assert alchem_system.getNumParticles() == 12 assert len(alchem_system.getForces()) == 12 _assert_num_forces(alchem_system, NonbondedForce, 1) @@ -212,7 +255,7 @@ def test_dry_run_vac_benzene(benzene_system, method, protocol_dry_settings, tmpd [0.35, 2.2, 1.5, 0, False], ], ) -def test_alchemical_settings_dry_run_vacuum( +def test_alchemical_settings_setup_vacuum( alpha, a, b, c, correction, benzene_system, protocol_dry_settings, tmpdir ): """ @@ -238,18 +281,18 @@ def test_alchemical_settings_dry_run_vacuum( ) prot_units = list(dag.protocol_units) - assert len(prot_units) == 2 + assert len(prot_units) == 6 - vac_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationVacuumUnit)] - sol_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationSolventUnit)] + vac_setup_unit = _get_units(prot_units, UNIT_TYPES["vacuum"]["setup"]) + vac_sim_unit = _get_units(prot_units, UNIT_TYPES["vacuum"]["sim"]) - assert len(vac_unit) == 1 - assert len(sol_unit) == 1 + assert len(vac_setup_unit) == 1 + assert len(vac_sim_unit) == 1 with tmpdir.as_cwd(): - debug = vac_unit[0].run(dry=True)["debug"] + results = vac_setup_unit[0].run(dry=True) - alchem_system = debug["alchem_system"] + alchem_system = results["alchem_system"] _assert_num_forces(alchem_system, NonbondedForce, 1) _assert_num_forces(alchem_system, CustomNonbondedForce, 4) _assert_num_forces(alchem_system, CustomBondForce, 4) @@ -291,16 +334,16 @@ def test_confgen_fail_AFE(benzene_system, protocol_dry_settings, tmpdir): mapping=None, ) prot_units = list(dag.protocol_units) - vac_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationVacuumUnit)] + vac_setup_unit = _get_units(prot_units, UNIT_TYPES["vacuum"]["setup"]) with tmpdir.as_cwd(): with mock.patch("rdkit.Chem.AllChem.EmbedMultipleConfs", return_value=0): - vac_sampler = vac_unit[0].run(dry=True)["debug"]["sampler"] - - assert vac_sampler + # If this worked, the system will have been built + system = vac_setup_unit[0].run(dry=True)["alchem_system"] + assert system -def test_dry_run_solv_benzene(benzene_system, protocol_dry_settings, tmpdir): +def test_setup_solv_benzene(benzene_system, protocol_dry_settings, tmpdir): protocol_dry_settings.solvent_output_settings.output_indices = "resname UNK" protocol = openmm_afe.AbsoluteSolvationProtocol(settings=protocol_dry_settings) @@ -318,19 +361,25 @@ def test_dry_run_solv_benzene(benzene_system, protocol_dry_settings, tmpdir): ) prot_units = list(dag.protocol_units) - assert len(prot_units) == 2 + sol_setup_unit = _get_units(prot_units, UNIT_TYPES["solvent"]["setup"]) + sol_sim_unit = _get_units(prot_units, UNIT_TYPES["solvent"]["sim"]) - vac_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationVacuumUnit)] - sol_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationSolventUnit)] - - assert len(vac_unit) == 1 - assert len(sol_unit) == 1 + assert len(sol_setup_unit) == len(sol_sim_unit) == 1 with tmpdir.as_cwd(): - sol_sampler = sol_unit[0].run(dry=True)["debug"]["sampler"] + setup_results = sol_setup_unit[0].run(dry=True) + sim_results = sol_sim_unit[0].run( + system=setup_results["alchem_system"], + positions=setup_results["debug_positions"], + selection_indices=setup_results["selection_indices"], + box_vectors=setup_results["box_vectors"], + alchemical_restraints=False, + dry=True, + ) + sol_sampler = sim_results["sampler"] assert sol_sampler.is_periodic - pdb = mdt.load_pdb("hybrid_system.pdb") + pdb = mdt.load_pdb(setup_results["pdb_structure"]) assert pdb.n_atoms == 12 @@ -363,14 +412,23 @@ def test_dry_run_vsite_fail(benzene_system, tmpdir, protocol_dry_settings): ) prot_units = list(dag.protocol_units) - sol_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationSolventUnit)] + sol_setup_unit = _get_units(prot_units, UNIT_TYPES["solvent"]["setup"]) + sol_sim_unit = _get_units(prot_units, UNIT_TYPES["solvent"]["sim"]) with tmpdir.as_cwd(): + setup_results = sol_setup_unit[0].run(dry=True) with pytest.raises(ValueError, match="are unstable"): - _ = sol_unit[0].run(dry=True) + sim_results = sol_sim_unit[0].run( + system=setup_results["alchem_system"], + positions=setup_results["debug_positions"], + selection_indices=setup_results["selection_indices"], + box_vectors=setup_results["box_vectors"], + alchemical_restraints=False, + dry=True, + ) -def test_dry_run_solv_benzene_tip4p(benzene_system, protocol_dry_settings, tmpdir): +def test_setup_dry_sim_solv_benzene_tip4p(benzene_system, protocol_dry_settings, tmpdir): protocol_dry_settings.vacuum_forcefield_settings.forcefields = [ "amber/ff14SB.xml", # ff14SB protein force field "amber/tip4pew_standard.xml", # FF we are testsing with the fun VS @@ -399,10 +457,20 @@ def test_dry_run_solv_benzene_tip4p(benzene_system, protocol_dry_settings, tmpdi ) prot_units = list(dag.protocol_units) - sol_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationSolventUnit)] + sol_setup_units = _get_units(prot_units, UNIT_TYPES["solvent"]["setup"]) + sol_sim_units = _get_units(prot_units, UNIT_TYPES["solvent"]["sim"]) with tmpdir.as_cwd(): - sol_sampler = sol_unit[0].run(dry=True)["debug"]["sampler"] + setup_results = sol_setup_units[0].run(dry=True) + sim_results = sol_sim_units[0].run( + system=setup_results["alchem_system"], + positions=setup_results["debug_positions"], + selection_indices=setup_results["selection_indices"], + box_vectors=setup_results["box_vectors"], + alchemical_restraints=False, + dry=True, + ) + sol_sampler = sim_results["sampler"] assert sol_sampler.is_periodic @@ -425,11 +493,11 @@ def test_dry_run_solv_benzene_noncubic(benzene_system, protocol_dry_settings, tm ) prot_units = list(dag.protocol_units) - sol_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationSolventUnit)] + sol_setup_units = _get_units(prot_units, UNIT_TYPES["solvent"]["setup"]) with tmpdir.as_cwd(): - sampler = sol_unit[0].run(dry=True)["debug"]["sampler"] - system = sampler._thermodynamic_states[0].system + results = sol_setup_units[0].run(dry=True) + system = results["alchem_system"] vectors = system.getDefaultPeriodicBoxVectors() width = float(from_openmm(vectors)[0][0].to("nanometer").m) @@ -486,13 +554,13 @@ def assign_fictitious_charges(offmol): dag = protocol.create(stateA=stateA, stateB=stateB, mapping=None) prot_units = list(dag.protocol_units) - vac_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationVacuumUnit)][0] - sol_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationSolventUnit)][0] + vac_setup_units = _get_units(prot_units, UNIT_TYPES["vacuum"]["setup"]) + sol_setup_units = _get_units(prot_units, UNIT_TYPES["solvent"]["setup"]) # check sol_unit charges with tmpdir.as_cwd(): - sampler = sol_unit.run(dry=True)["debug"]["sampler"] - system = sampler._thermodynamic_states[0].system + results = sol_setup_units[0].run(dry=True) + system = results["alchem_system"] nonbond = [f for f in system.getForces() if isinstance(f, NonbondedForce)] assert len(nonbond) == 1 @@ -506,8 +574,8 @@ def assign_fictitious_charges(offmol): # check vac_unit charges with tmpdir.as_cwd(): - sampler = vac_unit.run(dry=True)["debug"]["sampler"] - system = sampler._thermodynamic_states[0].system + results = vac_setup_units[0].run(dry=True) + system = results["alchem_system"] nonbond = [f for f in system.getForces() if isinstance(f, CustomNonbondedForce)] assert len(nonbond) == 4 @@ -572,12 +640,12 @@ def test_dry_run_charge_backends( dag = protocol.create(stateA=stateA, stateB=stateB, mapping=None) prot_units = list(dag.protocol_units) - vac_unit = [u for u in prot_units if isinstance(u, AbsoluteSolvationVacuumUnit)][0] + vac_setup_units = _get_units(prot_units, UNIT_TYPES["vacuum"]["setup"]) # check vac_unit charges with tmpdir.as_cwd(): - sampler = vac_unit.run(dry=True)["debug"]["sampler"] - system = sampler._thermodynamic_states[0].system + results = vac_setup_units[0].run(dry=True) + system = results["alchem_system"] nonbond = [f for f in system.getForces() if isinstance(f, CustomNonbondedForce)] assert len(nonbond) == 4 @@ -609,187 +677,6 @@ def benzene_solvation_dag(benzene_system, protocol_dry_settings): return protocol.create(stateA=stateA, stateB=stateB, mapping=None) -def test_unit_tagging(benzene_solvation_dag, tmpdir): - # test that executing the units includes correct gen and repeat info - - dag_units = benzene_solvation_dag.protocol_units - - with ( - mock.patch( - "openfe.protocols.openmm_afe.equil_solvation_afe_method.AbsoluteSolvationSolventUnit.run", - return_value={"nc": "file.nc", "last_checkpoint": "chck.nc"}, - ), - mock.patch( - "openfe.protocols.openmm_afe.equil_solvation_afe_method.AbsoluteSolvationVacuumUnit.run", - return_value={"nc": "file.nc", "last_checkpoint": "chck.nc"}, - ), - ): - results = [] - for u in dag_units: - ret = u.execute(context=gufe.Context(tmpdir, tmpdir)) - results.append(ret) - - solv_repeats = set() - vac_repeats = set() - for ret in results: - assert isinstance(ret, gufe.ProtocolUnitResult) - assert ret.outputs["generation"] == 0 - if ret.outputs["simtype"] == "vacuum": - vac_repeats.add(ret.outputs["repeat_id"]) - else: - solv_repeats.add(ret.outputs["repeat_id"]) - # Repeat ids are random ints so just check their lengths - assert len(vac_repeats) == len(solv_repeats) == 3 - - -def test_gather(benzene_solvation_dag, tmpdir): - # check that .gather behaves as expected - with ( - mock.patch( - "openfe.protocols.openmm_afe.equil_solvation_afe_method.AbsoluteSolvationSolventUnit.run", - return_value={"nc": "file.nc", "last_checkpoint": "chck.nc"}, - ), - mock.patch( - "openfe.protocols.openmm_afe.equil_solvation_afe_method.AbsoluteSolvationVacuumUnit.run", - return_value={"nc": "file.nc", "last_checkpoint": "chck.nc"}, - ), - ): - dagres = gufe.protocols.execute_DAG( - benzene_solvation_dag, - shared_basedir=tmpdir, - scratch_basedir=tmpdir, - keep_shared=True, - ) - - protocol = AbsoluteSolvationProtocol( - settings=AbsoluteSolvationProtocol.default_settings(), - ) - - res = protocol.gather([dagres]) - - assert isinstance(res, openmm_afe.AbsoluteSolvationProtocolResult) - - -class TestProtocolResult: - @pytest.fixture() - def protocolresult(self, afe_solv_transformation_json): - d = json.loads(afe_solv_transformation_json, cls=gufe.tokenization.JSON_HANDLER.decoder) - - pr = openfe.ProtocolResult.from_dict(d["protocol_result"]) - - return pr - - def test_reload_protocol_result(self, afe_solv_transformation_json): - d = json.loads(afe_solv_transformation_json, cls=gufe.tokenization.JSON_HANDLER.decoder) - - pr = openmm_afe.AbsoluteSolvationProtocolResult.from_dict(d["protocol_result"]) - - assert pr - - def test_get_estimate(self, protocolresult): - est = protocolresult.get_estimate() - - assert est - assert est.m == pytest.approx(-2.47, abs=0.5) - assert isinstance(est, offunit.Quantity) - assert est.is_compatible_with(offunit.kilojoule_per_mole) - - def test_get_uncertainty(self, protocolresult): - est = protocolresult.get_uncertainty() - - assert est - assert est.m == pytest.approx(0.2, abs=0.2) - assert isinstance(est, offunit.Quantity) - assert est.is_compatible_with(offunit.kilojoule_per_mole) - - def test_get_individual(self, protocolresult): - inds = protocolresult.get_individual_estimates() - - assert isinstance(inds, dict) - assert isinstance(inds["solvent"], list) - assert isinstance(inds["vacuum"], list) - assert len(inds["solvent"]) == len(inds["vacuum"]) == 3 - for e, u in itertools.chain(inds["solvent"], inds["vacuum"]): - assert e.is_compatible_with(offunit.kilojoule_per_mole) - assert u.is_compatible_with(offunit.kilojoule_per_mole) - - @pytest.mark.parametrize("key", ["solvent", "vacuum"]) - def test_get_forwards_etc(self, key, protocolresult): - far = protocolresult.get_forward_and_reverse_energy_analysis() - - assert isinstance(far, dict) - assert isinstance(far[key], list) - far1 = far[key][0] - assert isinstance(far1, dict) - - for k in ["fractions", "forward_DGs", "forward_dDGs", "reverse_DGs", "reverse_dDGs"]: - assert k in far1 - - if k == "fractions": - assert isinstance(far1[k], np.ndarray) - - @pytest.mark.parametrize("key", ["solvent", "vacuum"]) - def test_get_frwd_reverse_none_return(self, key, protocolresult): - # fetch the first result of type key - data = [i for i in protocolresult.data[key].values()][0][0] - # set the output to None - data.outputs["forward_and_reverse_energies"] = None - - # now fetch the analysis results and expect a warning - wmsg = f"were found in the forward and reverse dictionaries of the repeats of the {key}" - with pytest.warns(UserWarning, match=wmsg): - protocolresult.get_forward_and_reverse_energy_analysis() - - @pytest.mark.parametrize("key", ["solvent", "vacuum"]) - def test_get_overlap_matrices(self, key, protocolresult): - ovp = protocolresult.get_overlap_matrices() - - assert isinstance(ovp, dict) - assert isinstance(ovp[key], list) - assert len(ovp[key]) == 3 - - ovp1 = ovp[key][0] - assert isinstance(ovp1["matrix"], np.ndarray) - assert ovp1["matrix"].shape == (14, 14) - - @pytest.mark.parametrize("key", ["solvent", "vacuum"]) - def test_get_replica_transition_statistics(self, key, protocolresult): - rpx = protocolresult.get_replica_transition_statistics() - - assert isinstance(rpx, dict) - assert isinstance(rpx[key], list) - assert len(rpx[key]) == 3 - rpx1 = rpx[key][0] - assert "eigenvalues" in rpx1 - assert "matrix" in rpx1 - assert rpx1["eigenvalues"].shape == (14,) - assert rpx1["matrix"].shape == (14, 14) - - @pytest.mark.parametrize("key", ["solvent", "vacuum"]) - def test_equilibration_iterations(self, key, protocolresult): - eq = protocolresult.equilibration_iterations() - - assert isinstance(eq, dict) - assert isinstance(eq[key], list) - assert len(eq[key]) == 3 - assert all(isinstance(v, float) for v in eq[key]) - - @pytest.mark.parametrize("key", ["solvent", "vacuum"]) - def test_production_iterations(self, key, protocolresult): - prod = protocolresult.production_iterations() - - assert isinstance(prod, dict) - assert isinstance(prod[key], list) - assert len(prod[key]) == 3 - assert all(isinstance(v, float) for v in prod[key]) - - def test_filenotfound_replica_states(self, protocolresult): - errmsg = "File could not be found" - - with pytest.raises(ValueError, match=errmsg): - protocolresult.get_replica_states() - - @pytest.mark.parametrize( "positions_write_frequency,velocities_write_frequency", [ @@ -821,7 +708,6 @@ def test_dry_run_vacuum_write_frequency( stateB = ChemicalSystem({"solvent": SolventComponent()}) # Create DAG from protocol, get the vacuum and solvent units - # and eventually dry run the first solvent unit dag = protocol.create( stateA=stateA, stateB=stateB, @@ -829,11 +715,23 @@ def test_dry_run_vacuum_write_frequency( ) prot_units = list(dag.protocol_units) - assert len(prot_units) == 2 - - with tmpdir.as_cwd(): - for u in prot_units: - sampler = u.run(dry=True)["debug"]["sampler"] + assert len(prot_units) == 6 + + for phase in ["solvent", "vacuum"]: + setup_units = _get_units(prot_units, UNIT_TYPES[phase]["setup"]) + sim_units = _get_units(prot_units, UNIT_TYPES[phase]["sim"]) + + with tmpdir.as_cwd(): + setup_results = setup_units[0].run(dry=True) + sim_results = sim_units[0].run( + system=setup_results["alchem_system"], + positions=setup_results["debug_positions"], + selection_indices=setup_results["selection_indices"], + box_vectors=setup_results["box_vectors"], + alchemical_restraints=False, + dry=True, + ) + sampler = sim_results["sampler"] reporter = sampler._reporter if positions_write_frequency: assert reporter.position_interval == positions_write_frequency.m diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol_results.py b/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol_results.py new file mode 100644 index 000000000..0cb2d2d25 --- /dev/null +++ b/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol_results.py @@ -0,0 +1,278 @@ +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +import itertools +import json +from pathlib import Path +from unittest import mock + +import gufe +import numpy as np +import pytest +from openff.units import unit as offunit + +import openfe +from openfe import ChemicalSystem, SolventComponent +from openfe.protocols import openmm_afe + +from .utils import UNIT_TYPES, _get_units + + +@pytest.fixture() +def protocol_dry_settings(): + settings = openmm_afe.AbsoluteSolvationProtocol.default_settings() + settings.vacuum_engine_settings.compute_platform = None + settings.solvent_engine_settings.compute_platform = None + settings.protocol_repeats = 1 + return settings + + +@pytest.fixture +def benzene_solvation_dag(benzene_system, protocol_dry_settings): + protocol_dry_settings.protocol_repeats = 3 + protocol = openmm_afe.AbsoluteSolvationProtocol(settings=protocol_dry_settings) + + stateA = benzene_system + + stateB = ChemicalSystem({"solvent": SolventComponent()}) + + return protocol.create(stateA=stateA, stateB=stateB, mapping=None) + + +@pytest.fixture +def patcher(): + with ( + mock.patch( + "openfe.protocols.openmm_afe.ahfe_units.AHFESolventSetupUnit.run", + return_value={ + "system": Path("system.xml.bz2"), + "positions": Path("positions.npy"), + "pdb_structure": Path("hybrid_system.pdb"), + "selection_indices": np.zeros(100), + "box_vectors": [np.zeros(3), np.zeros(3), np.zeros(3)] * offunit.nm, + "standard_state_correction": 0 * offunit.kilocalorie_per_mole, + "restraint_geometry": None, + }, + ), + mock.patch( + "openfe.protocols.openmm_afe.ahfe_units.AHFEVacuumSetupUnit.run", + return_value={ + "system": Path("system.xml.bz2"), + "positions": Path("positions.npy"), + "pdb_structure": Path("hybrid_system.pdb"), + "selection_indices": np.zeros(100), + "box_vectors": [np.zeros(3), np.zeros(3), np.zeros(3)] * offunit.nm, + "standard_state_correction": 0 * offunit.kilocalorie_per_mole, + "restraint_geometry": None, + }, + ), + mock.patch( + "openfe.protocols.openmm_afe.base_afe_units.np.load", + return_value=np.zeros(100), + ), + mock.patch( + "openfe.protocols.openmm_afe.base_afe_units.deserialize", + return_value="foo", + ), + mock.patch( + "openfe.protocols.openmm_afe.ahfe_units.AHFESolventSimUnit.run", + return_value={ + "trajectory": Path("file.nc"), + "checkpoint": Path("chk.chk"), + }, + ), + mock.patch( + "openfe.protocols.openmm_afe.ahfe_units.AHFEVacuumSimUnit.run", + return_value={ + "trajectory": Path("file.nc"), + "checkpoint": Path("chk.chk"), + }, + ), + mock.patch( + "openfe.protocols.openmm_afe.ahfe_units.AHFESolventAnalysisUnit.run", + return_value={"foo": "bar"}, + ), + mock.patch( + "openfe.protocols.openmm_afe.ahfe_units.AHFEVacuumAnalysisUnit.run", + return_value={"foo": "bar"}, + ), + ): + yield + + +def test_gather(benzene_solvation_dag, patcher, tmpdir): + # check that .gather behaves as expected + dagres = gufe.protocols.execute_DAG( + benzene_solvation_dag, + shared_basedir=tmpdir, + scratch_basedir=tmpdir, + keep_shared=True, + ) + + protocol = openmm_afe.AbsoluteSolvationProtocol( + settings=openmm_afe.AbsoluteSolvationProtocol.default_settings(), + ) + + res = protocol.gather([dagres]) + + assert isinstance(res, openmm_afe.AbsoluteSolvationProtocolResult) + + +def test_unit_tagging(benzene_solvation_dag, patcher, tmpdir): + # test that executing the units includes correct gen and repeat info + + dag_units = benzene_solvation_dag.protocol_units + + for phase in ["solvent", "vacuum"]: + setup_results = {} + sim_results = {} + analysis_results = {} + + setup_units = _get_units(dag_units, UNIT_TYPES[phase]["setup"]) + sim_units = _get_units(dag_units, UNIT_TYPES[phase]["sim"]) + a_units = _get_units(dag_units, UNIT_TYPES[phase]["analysis"]) + + for u in setup_units: + rid = u.inputs["repeat_id"] + setup_results[rid] = u.execute(context=gufe.Context(tmpdir, tmpdir)) + + for u in sim_units: + rid = u.inputs["repeat_id"] + sim_results[rid] = u.execute( + context=gufe.Context(tmpdir, tmpdir), + setup_results=setup_results[rid], + ) + + for u in a_units: + rid = u.inputs["repeat_id"] + analysis_results[rid] = u.execute( + context=gufe.Context(tmpdir, tmpdir), + setup_results=setup_results[rid], + simulation_results=sim_results[rid], + ) + + for results in [setup_results, sim_results, analysis_results]: + for ret in results.values(): + assert isinstance(ret, gufe.ProtocolUnitResult) + assert ret.outputs["generation"] == 0 + + assert len(setup_results) == len(sim_results) == len(analysis_results) == 3 + + +class TestProtocolResult: + @pytest.fixture() + def protocolresult(self, afe_solv_transformation_json): + d = json.loads(afe_solv_transformation_json, cls=gufe.tokenization.JSON_HANDLER.decoder) + + pr = openfe.ProtocolResult.from_dict(d["protocol_result"]) + + return pr + + def test_reload_protocol_result(self, afe_solv_transformation_json): + d = json.loads(afe_solv_transformation_json, cls=gufe.tokenization.JSON_HANDLER.decoder) + + pr = openmm_afe.AbsoluteSolvationProtocolResult.from_dict(d["protocol_result"]) + + assert pr + + def test_get_estimate(self, protocolresult): + est = protocolresult.get_estimate() + + assert est + assert est.m == pytest.approx(-2.47, abs=0.5) + assert isinstance(est, offunit.Quantity) + assert est.is_compatible_with(offunit.kilojoule_per_mole) + + def test_get_uncertainty(self, protocolresult): + est = protocolresult.get_uncertainty() + + assert est + assert est.m == pytest.approx(0.2, abs=0.2) + assert isinstance(est, offunit.Quantity) + assert est.is_compatible_with(offunit.kilojoule_per_mole) + + def test_get_individual(self, protocolresult): + inds = protocolresult.get_individual_estimates() + + assert isinstance(inds, dict) + assert isinstance(inds["solvent"], list) + assert isinstance(inds["vacuum"], list) + assert len(inds["solvent"]) == len(inds["vacuum"]) == 3 + for e, u in itertools.chain(inds["solvent"], inds["vacuum"]): + assert e.is_compatible_with(offunit.kilojoule_per_mole) + assert u.is_compatible_with(offunit.kilojoule_per_mole) + + @pytest.mark.parametrize("key", ["solvent", "vacuum"]) + def test_get_forwards_etc(self, key, protocolresult): + far = protocolresult.get_forward_and_reverse_energy_analysis() + + assert isinstance(far, dict) + assert isinstance(far[key], list) + far1 = far[key][0] + assert isinstance(far1, dict) + + for k in ["fractions", "forward_DGs", "forward_dDGs", "reverse_DGs", "reverse_dDGs"]: + assert k in far1 + + if k == "fractions": + assert isinstance(far1[k], np.ndarray) + + @pytest.mark.parametrize("key", ["solvent", "vacuum"]) + def test_get_frwd_reverse_none_return(self, key, protocolresult): + # fetch the first result of type key + data = [i for i in protocolresult.data[key].values()][0][0] + # set the output to None + data.outputs["forward_and_reverse_energies"] = None + + # now fetch the analysis results and expect a warning + wmsg = f"were found in the forward and reverse dictionaries of the repeats of the {key}" + with pytest.warns(UserWarning, match=wmsg): + protocolresult.get_forward_and_reverse_energy_analysis() + + @pytest.mark.parametrize("key", ["solvent", "vacuum"]) + def test_get_overlap_matrices(self, key, protocolresult): + ovp = protocolresult.get_overlap_matrices() + + assert isinstance(ovp, dict) + assert isinstance(ovp[key], list) + assert len(ovp[key]) == 3 + + ovp1 = ovp[key][0] + assert isinstance(ovp1["matrix"], np.ndarray) + assert ovp1["matrix"].shape == (14, 14) + + @pytest.mark.parametrize("key", ["solvent", "vacuum"]) + def test_get_replica_transition_statistics(self, key, protocolresult): + rpx = protocolresult.get_replica_transition_statistics() + + assert isinstance(rpx, dict) + assert isinstance(rpx[key], list) + assert len(rpx[key]) == 3 + rpx1 = rpx[key][0] + assert "eigenvalues" in rpx1 + assert "matrix" in rpx1 + assert rpx1["eigenvalues"].shape == (14,) + assert rpx1["matrix"].shape == (14, 14) + + @pytest.mark.parametrize("key", ["solvent", "vacuum"]) + def test_equilibration_iterations(self, key, protocolresult): + eq = protocolresult.equilibration_iterations() + + assert isinstance(eq, dict) + assert isinstance(eq[key], list) + assert len(eq[key]) == 3 + assert all(isinstance(v, float) for v in eq[key]) + + @pytest.mark.parametrize("key", ["solvent", "vacuum"]) + def test_production_iterations(self, key, protocolresult): + prod = protocolresult.production_iterations() + + assert isinstance(prod, dict) + assert isinstance(prod[key], list) + assert len(prod[key]) == 3 + assert all(isinstance(v, float) for v in prod[key]) + + def test_filenotfound_replica_states(self, protocolresult): + errmsg = "File could not be found" + + with pytest.raises(ValueError, match=errmsg): + protocolresult.get_replica_states() diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_slow.py b/openfe/tests/protocols/openmm_ahfe/test_ahfe_slow.py index 81ba1f4ca..353becde3 100644 --- a/openfe/tests/protocols/openmm_ahfe/test_ahfe_slow.py +++ b/openfe/tests/protocols/openmm_ahfe/test_ahfe_slow.py @@ -79,16 +79,36 @@ def test_openmm_run_engine( r = execute_DAG(dag, shared_basedir=cwd, scratch_basedir=cwd, keep_shared=True) assert r.ok() - for pur in r.protocol_unit_results: - unit_shared = tmpdir / f"shared_{pur.source_key}_attempt_0" - assert unit_shared.exists() - assert pathlib.Path(unit_shared).is_dir() - checkpoint = pur.outputs["last_checkpoint"] - assert checkpoint == f"{pur.outputs['simtype']}_checkpoint.nc" - assert (unit_shared / checkpoint).exists() - nc = pur.outputs["nc"] - assert nc == unit_shared / f"{pur.outputs['simtype']}.nc" - assert nc.exists() + + # Check outputs of solvent & vacuum results + for phase in ["solvent", "vacuum"]: + purs = [pur for pur in r.protocol_unit_results if pur.outputs["simtype"] == phase] + + # get the path to the simulation unit shared dict + for pur in purs: + if "Simulation" in pur.name: + sim_shared = tmpdir / f"shared_{pur.source_key}_attempt_0" + assert sim_shared.exists() + assert pathlib.Path(sim_shared).is_dir() + + # check the analysis outputs + for pur in purs: + if "Analysis" not in pur.name: + continue + + unit_shared = tmpdir / f"shared_{pur.source_key}_attempt_0" + assert unit_shared.exists() + assert pathlib.Path(unit_shared).is_dir() + + # Does the checkpoint file exist? + checkpoint = pur.outputs["checkpoint"] + assert checkpoint == sim_shared / f"{pur.outputs['simtype']}_checkpoint.nc" + assert checkpoint.exists() + + # Does the trajectory file exist? + nc = pur.outputs["trajectory"] + assert nc == sim_shared / f"{pur.outputs['simtype']}.nc" + assert nc.exists() # Test results methods that need files present results = protocol.gather([r]) diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_tokenization.py b/openfe/tests/protocols/openmm_ahfe/test_ahfe_tokenization.py index b444d8656..8c9194437 100644 --- a/openfe/tests/protocols/openmm_ahfe/test_ahfe_tokenization.py +++ b/openfe/tests/protocols/openmm_ahfe/test_ahfe_tokenization.py @@ -4,10 +4,19 @@ import gufe import pytest -from gufe.tests.test_tokenization import GufeTokenizableTestsMixin import openfe from openfe.protocols import openmm_afe +from openfe.protocols.openmm_afe import ( + AHFESolventAnalysisUnit, + AHFESolventSetupUnit, + AHFESolventSimUnit, + AHFEVacuumAnalysisUnit, + AHFEVacuumSetupUnit, + AHFEVacuumSimUnit, +) + +from ..conftest import ModGufeTokenizableTestsMixin @pytest.fixture @@ -27,18 +36,40 @@ def protocol_units(protocol, benzene_system): return list(pus.protocol_units) -@pytest.fixture -def solvent_protocol_unit(protocol_units): - for pu in protocol_units: - if isinstance(pu, openmm_afe.AbsoluteSolvationSolventUnit): +def _filter_units(pus, classtype): + for pu in pus: + if isinstance(pu, classtype): return pu @pytest.fixture -def vacuum_protocol_unit(protocol_units): - for pu in protocol_units: - if isinstance(pu, openmm_afe.AbsoluteSolvationVacuumUnit): - return pu +def solvent_protocol_setup_unit(protocol_units): + return _filter_units(protocol_units, AHFESolventSetupUnit) + + +@pytest.fixture +def solvent_protocol_sim_unit(protocol_units): + return _filter_units(protocol_units, AHFESolventSimUnit) + + +@pytest.fixture +def solvent_protocol_analysis_unit(protocol_units): + return _filter_units(protocol_units, AHFESolventAnalysisUnit) + + +@pytest.fixture +def vacuum_protocol_setup_unit(protocol_units): + return _filter_units(protocol_units, AHFEVacuumSetupUnit) + + +@pytest.fixture +def vacuum_protocol_sim_unit(protocol_units): + return _filter_units(protocol_units, AHFEVacuumSimUnit) + + +@pytest.fixture +def vacuum_protocol_analysis_unit(protocol_units): + return _filter_units(protocol_units, AHFEVacuumAnalysisUnit) @pytest.fixture @@ -48,7 +79,7 @@ def protocol_result(afe_solv_transformation_json): return pr -class TestAbsoluteSolvationProtocol(GufeTokenizableTestsMixin): +class TestAbsoluteSolvationProtocol(ModGufeTokenizableTestsMixin): cls = openmm_afe.AbsoluteSolvationProtocol key = None repr = "AbsoluteSolvationProtocol-" @@ -57,49 +88,68 @@ class TestAbsoluteSolvationProtocol(GufeTokenizableTestsMixin): def instance(self, protocol): return protocol - def test_repr(self, instance): - """ - Overwrites the base `test_repr` call. - """ - assert isinstance(repr(instance), str) - assert self.repr in repr(instance) + +class TestAHFESolventSetupUnit(ModGufeTokenizableTestsMixin): + cls = AHFESolventSetupUnit + repr = "AHFESolventSetupUnit(AHFE Setup: benzene solvent leg" + key = None + + @pytest.fixture() + def instance(self, solvent_protocol_setup_unit): + return solvent_protocol_setup_unit + + +class TestAHFESolventSimUnit(ModGufeTokenizableTestsMixin): + cls = AHFESolventSimUnit + repr = "AHFESolventSimUnit(AHFE Simulation: benzene solvent leg" + key = None + + @pytest.fixture() + def instance(self, solvent_protocol_sim_unit): + return solvent_protocol_sim_unit -class TestAbsoluteSolvationSolventUnit(GufeTokenizableTestsMixin): - cls = openmm_afe.AbsoluteSolvationSolventUnit - repr = "AbsoluteSolvationSolventUnit(Absolute Solvation, benzene solvent leg" +class TestAHFESolventAnalysisUnit(ModGufeTokenizableTestsMixin): + cls = AHFESolventAnalysisUnit + repr = "AHFESolventAnalysisUnit(AHFE Analysis: benzene solvent leg" key = None @pytest.fixture() - def instance(self, solvent_protocol_unit): - return solvent_protocol_unit + def instance(self, solvent_protocol_analysis_unit): + return solvent_protocol_analysis_unit - def test_repr(self, instance): - """ - Overwrites the base `test_repr` call. - """ - assert isinstance(repr(instance), str) - assert self.repr in repr(instance) +class TestAHFEVacuumSetupUnit(ModGufeTokenizableTestsMixin): + cls = AHFEVacuumSetupUnit + repr = "AHFEVacuumSetupUnit(AHFE Setup: benzene vacuum leg" + key = None + + @pytest.fixture() + def instance(self, vacuum_protocol_setup_unit): + return vacuum_protocol_setup_unit -class TestAbsoluteSolvationVacuumUnit(GufeTokenizableTestsMixin): - cls = openmm_afe.AbsoluteSolvationVacuumUnit - repr = "AbsoluteSolvationVacuumUnit(Absolute Solvation, benzene vacuum leg" + +class TestAHFEVacuumSimUnit(ModGufeTokenizableTestsMixin): + cls = AHFEVacuumSimUnit + repr = "AHFEVacuumSimUnit(AHFE Simulation: benzene vacuum leg" key = None @pytest.fixture() - def instance(self, vacuum_protocol_unit): - return vacuum_protocol_unit + def instance(self, vacuum_protocol_sim_unit): + return vacuum_protocol_sim_unit - def test_repr(self, instance): - """ - Overwrites the base `test_repr` call. - """ - assert isinstance(repr(instance), str) - assert self.repr in repr(instance) +class TestAHFEVacuumAnalysisUnit(ModGufeTokenizableTestsMixin): + cls = AHFEVacuumAnalysisUnit + repr = "AHFEVacuumAnalysisUnit(AHFE Analysis: benzene vacuum leg" + key = None -class TestAbsoluteSolvationProtocolResult(GufeTokenizableTestsMixin): + @pytest.fixture() + def instance(self, vacuum_protocol_analysis_unit): + return vacuum_protocol_analysis_unit + + +class TestAbsoluteSolvationProtocolResult(ModGufeTokenizableTestsMixin): cls = openmm_afe.AbsoluteSolvationProtocolResult key = None repr = "AbsoluteSolvationProtocolResult-" @@ -107,10 +157,3 @@ class TestAbsoluteSolvationProtocolResult(GufeTokenizableTestsMixin): @pytest.fixture() def instance(self, protocol_result): return protocol_result - - def test_repr(self, instance): - """ - Overwrites the base `test_repr` call. - """ - assert isinstance(repr(instance), str) - assert self.repr in repr(instance) diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_validation.py b/openfe/tests/protocols/openmm_ahfe/test_ahfe_validation.py index 9e7474722..4db8c4ea4 100644 --- a/openfe/tests/protocols/openmm_ahfe/test_ahfe_validation.py +++ b/openfe/tests/protocols/openmm_ahfe/test_ahfe_validation.py @@ -8,15 +8,8 @@ from openfe.protocols import openmm_afe from openfe.protocols.openmm_afe import ( AbsoluteSolvationProtocol, - AbsoluteSolvationSolventUnit, - AbsoluteSolvationVacuumUnit, ) from openfe.protocols.openmm_utils import system_validation -from openfe.protocols.openmm_utils.charge_generation import ( - HAS_ESPALOMA_CHARGE, - HAS_NAGL, - HAS_OPENEYE, -) @pytest.fixture() diff --git a/openfe/tests/protocols/openmm_ahfe/utils.py b/openfe/tests/protocols/openmm_ahfe/utils.py new file mode 100644 index 000000000..391081888 --- /dev/null +++ b/openfe/tests/protocols/openmm_ahfe/utils.py @@ -0,0 +1,31 @@ +# This code is part of OpenFE and is licensed under the MIT license. +# For details, see https://github.com/OpenFreeEnergy/openfe +from openfe.protocols.openmm_afe import ( + AbsoluteSolvationProtocol, + AHFESolventAnalysisUnit, + AHFESolventSetupUnit, + AHFESolventSimUnit, + AHFEVacuumAnalysisUnit, + AHFEVacuumSetupUnit, + AHFEVacuumSimUnit, +) + +UNIT_TYPES = { + "solvent": { + "setup": AHFESolventSetupUnit, + "sim": AHFESolventSimUnit, + "analysis": AHFESolventAnalysisUnit, + }, + "vacuum": { + "setup": AHFEVacuumSetupUnit, + "sim": AHFEVacuumSimUnit, + "analysis": AHFEVacuumAnalysisUnit, + }, +} + + +def _get_units(protocol_units, unit_type): + """ + Helper method to extract setup units. + """ + return [pu for pu in protocol_units if isinstance(pu, unit_type)] diff --git a/openfecli/commands/gather_abfe.py b/openfecli/commands/gather_abfe.py index a541bfc52..3a49f6432 100644 --- a/openfecli/commands/gather_abfe.py +++ b/openfecli/commands/gather_abfe.py @@ -33,7 +33,12 @@ def _get_name(result: dict) -> str: """ solvent_data = list(result["protocol_result"]["data"]["solvent"].values())[0][0] - name = solvent_data["inputs"]["alchemical_components"]["stateA"][0]["molprops"]["ofe-name"] + try: + name = solvent_data["inputs"]["setup_results"]["inputs"]["alchemical_components"]["stateA"][ + 0 + ]["molprops"]["ofe-name"] + except KeyError: + name = solvent_data["inputs"]["alchemical_components"]["stateA"][0]["molprops"]["ofe-name"] return str(name) From f49957cb90e902575ca460083c05b336735178d4 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:42:35 -0800 Subject: [PATCH 38/47] move to src layout (#1805) * Move openfe project layout to `src`. --- .github/workflows/check_for_api_break.yaml | 4 +- .github/workflows/cpu-long-tests.yaml | 2 +- .github/workflows/gpu-integration-tests.yaml | 2 +- .../workflows/test-feedstock-pkg-build.yaml | 2 +- .pre-commit-config.yaml | 1 + MANIFEST.in | 36 +++++++++--------- pyproject.toml | 15 ++++---- {openfe => src/openfe}/__init__.py | 0 {openfe => src/openfe}/analysis/__init__.py | 0 {openfe => src/openfe}/analysis/plotting.py | 0 {openfe => src/openfe}/due.py | 0 .../openfe}/orchestration/__init__.py | 0 {openfe => src/openfe}/protocols/__init__.py | 0 .../openfe}/protocols/openmm_afe/__init__.py | 0 .../protocols/openmm_afe/abfe_units.py | 0 .../openmm_afe/afe_protocol_results.py | 0 .../protocols/openmm_afe/ahfe_units.py | 0 .../protocols/openmm_afe/base_afe_units.py | 0 .../openmm_afe/equil_afe_settings.py | 0 .../openmm_afe/equil_binding_afe_method.py | 0 .../openmm_afe/equil_solvation_afe_method.py | 0 .../openfe}/protocols/openmm_md/__init__.py | 0 .../protocols/openmm_md/plain_md_methods.py | 0 .../protocols/openmm_md/plain_md_settings.py | 0 .../openfe}/protocols/openmm_rfe/__init__.py | 0 .../openmm_rfe/_rfe_utils/__init__.py | 0 .../openmm_rfe/_rfe_utils/lambdaprotocol.py | 0 .../openmm_rfe/_rfe_utils/multistate.py | 0 .../openmm_rfe/_rfe_utils/relative.py | 0 .../openmm_rfe/_rfe_utils/topologyhelpers.py | 0 .../protocols/openmm_rfe/equil_rfe_methods.py | 0 .../openmm_rfe/equil_rfe_settings.py | 0 .../openmm_rfe/hybridtop_protocol_results.py | 0 .../openmm_rfe/hybridtop_protocols.py | 0 .../protocols/openmm_rfe/hybridtop_units.py | 0 .../protocols/openmm_septop/__init__.py | 0 .../openfe}/protocols/openmm_septop/base.py | 0 .../openmm_septop/equil_septop_method.py | 0 .../openmm_septop/equil_septop_settings.py | 0 .../openfe}/protocols/openmm_septop/utils.py | 0 .../protocols/openmm_utils/__init__.py | 0 .../openmm_utils/charge_generation.py | 0 .../openmm_utils/multistate_analysis.py | 0 .../protocols/openmm_utils/omm_compute.py | 0 .../protocols/openmm_utils/omm_settings.py | 0 .../protocols/openmm_utils/serialization.py | 0 .../openmm_utils/settings_validation.py | 0 .../protocols/openmm_utils/system_creation.py | 0 .../openmm_utils/system_validation.py | 0 .../protocols/restraint_utils/__init__.py | 0 .../restraint_utils/geometry/__init__.py | 0 .../restraint_utils/geometry/base.py | 0 .../geometry/boresch/__init__.py | 0 .../geometry/boresch/geometry.py | 0 .../restraint_utils/geometry/boresch/guest.py | 0 .../restraint_utils/geometry/boresch/host.py | 0 .../restraint_utils/geometry/flatbottom.py | 0 .../restraint_utils/geometry/harmonic.py | 0 .../restraint_utils/geometry/utils.py | 0 .../restraint_utils/openmm/__init__.py | 0 .../restraint_utils/openmm/omm_forces.py | 0 .../restraint_utils/openmm/omm_restraints.py | 0 .../protocols/restraint_utils/settings.py | 0 {openfe => src/openfe}/setup/__init__.py | 0 .../alchemical_network_planner/__init__.py | 0 .../abstract_alchemical_network_planner.py | 0 .../relative_alchemical_network_planner.py | 0 .../openfe}/setup/atom_mapping/__init__.py | 0 .../setup/atom_mapping/ligandatommapper.py | 0 .../setup/atom_mapping/lomap_mapper.py | 0 .../setup/atom_mapping/lomap_scorers.py | 0 .../setup/atom_mapping/perses_mapper.py | 0 .../setup/atom_mapping/perses_scorers.py | 0 .../chemicalsystem_generator/__init__.py | 0 .../abstract_chemicalsystem_generator.py | 0 .../easy_chemicalsystem_generator.py | 0 .../openfe}/setup/ligand_network_planning.py | 0 {openfe => src/openfe}/storage/__init__.py | 0 .../openfe}/storage/metadatastore.py | 0 .../openfe}/storage/resultclient.py | 0 .../openfe}/storage/resultserver.py | 0 {openfe => src/openfe}/tests/__init__.py | 0 .../openfe}/tests/analysis/__init__.py | 0 .../openfe}/tests/analysis/test_plotting.py | 0 {openfe => src/openfe}/tests/conftest.py | 0 .../openfe}/tests/data/181l_only.pdb | 0 {openfe => src/openfe}/tests/data/6CZJ.pdb.gz | Bin {openfe => src/openfe}/tests/data/CN.sdf | 0 {openfe => src/openfe}/tests/data/__init__.py | 0 .../tests/data/benzene_modifications.sdf | 0 .../openfe}/tests/data/cdk8/__init__.py | 0 .../openfe}/tests/data/cdk8/cdk8_ligands.sdf | 0 .../openfe}/tests/data/cdk8/cdk8_protein.pdb | 0 .../openfe}/tests/data/eg5/__init__.py | 0 .../openfe}/tests/data/eg5/eg5_cofactor.sdf | 0 .../openfe}/tests/data/eg5/eg5_ligands.sdf | 0 .../openfe}/tests/data/eg5/eg5_protein.pdb | 0 .../tests/data/external_formats/__init__.py | 0 .../external_formats/somebenzenes_edges.edge | 0 .../external_formats/somebenzenes_nes.dat | 0 .../openfe}/tests/data/htf/__init__.py | 0 .../htf/t4_lysozyme_data/chlorobenzene.sdf | 0 .../htf/t4_lysozyme_data/fluorobenzene.sdf | 0 .../t4_lysozyme_solvated.pdb.gz | Bin .../1,3,7-trimethylnaphthalene.mol2 | 0 .../lomap_basic/1-butyl-4-methylbenzene.mol2 | 0 .../lomap_basic/2,6-dimethylnaphthalene.mol2 | 0 .../2-methyl-6-propylnaphthalene.mol2 | 0 .../data/lomap_basic/2-methylnaphthalene.mol2 | 0 .../tests/data/lomap_basic/2-naftanol.mol2 | 0 .../openfe}/tests/data/lomap_basic/README.md | 0 .../tests/data/lomap_basic/__init__.py | 0 .../data/lomap_basic/methylcyclohexane.mol2 | 0 .../tests/data/lomap_basic/toluene.mol2 | 0 .../openfe}/tests/data/multi_molecule.sdf | 0 .../ABFEProtocol_json_results.json.gz | Bin .../openmm_afe/AHFEProtocol_json_results.gz | Bin .../data/openmm_afe/T4_abfe_system.xml.bz2 | Bin .../openfe}/tests/data/openmm_afe/__init__.py | 0 .../data/openmm_md/MDProtocol_json_results.gz | Bin .../openfe}/tests/data/openmm_md/__init__.py | 0 .../openmm_rfe/RHFEProtocol_json_results.gz | Bin .../openfe}/tests/data/openmm_rfe/__init__.py | 0 .../hybrid_topology_atoms.csv | 0 .../hybrid_topology_bonds.txt | 0 .../data/openmm_rfe/charged_benzenes.sdf | 0 .../openmm_rfe/dummy_charge_ligand_23.sdf | 0 .../openmm_rfe/dummy_charge_ligand_55.sdf | 0 .../tests/data/openmm_rfe/ligand_23.sdf | 0 .../tests/data/openmm_rfe/ligand_55.sdf | 0 .../malt1_shapefit_1832577-09-9.sdf | 0 .../malt1_shapefit_Pfizer-01-01.sdf | 0 .../tests/data/openmm_rfe/reference.xml | 0 .../tests/data/openmm_rfe/vacuum_nocoord.nc | Bin .../openmm_rfe/vacuum_nocoord_checkpoint.nc | Bin .../SepTopProtocol_json_results.gz | Bin .../tests/data/openmm_septop/__init__.py | 0 .../tests/data/openmm_septop/system.xml.bz2 | Bin .../tests/data/serialization/__init__.py | 0 .../data/serialization/ethane_template.sdf | 0 .../serialization/network_template.graphml | 0 {openfe => src/openfe}/tests/dev/__init__.py | 0 .../tests/dev/serialization_test_templates.py | 0 .../openfe}/tests/protocols/__init__.py | 0 .../openfe}/tests/protocols/conftest.py | 0 .../tests/protocols/openmm_abfe/__init__.py | 0 .../tests/protocols/openmm_abfe/conftest.py | 0 .../openmm_abfe/test_abfe_energies.py | 0 .../openmm_abfe/test_abfe_protocol.py | 0 .../openmm_abfe/test_abfe_protocol_results.py | 0 .../openmm_abfe/test_abfe_settings.py | 0 .../protocols/openmm_abfe/test_abfe_slow.py | 0 .../openmm_abfe/test_abfe_tokenization.py | 0 .../openmm_abfe/test_abfe_validation.py | 0 .../tests/protocols/openmm_abfe/utils.py | 0 .../tests/protocols/openmm_ahfe/__init__.py | 0 .../openmm_ahfe/test_ahfe_protocol.py | 0 .../openmm_ahfe/test_ahfe_protocol_results.py | 0 .../openmm_ahfe/test_ahfe_settings.py | 0 .../protocols/openmm_ahfe/test_ahfe_slow.py | 0 .../openmm_ahfe/test_ahfe_tokenization.py | 0 .../openmm_ahfe/test_ahfe_validation.py | 0 .../tests/protocols/openmm_ahfe/utils.py | 0 .../tests/protocols/openmm_md/__init__.py | 0 .../openmm_md/test_plain_md_protocol.py | 0 .../protocols/openmm_md/test_plain_md_slow.py | 0 .../openmm_md/test_plain_md_tokenization.py | 0 .../tests/protocols/openmm_rfe/__init__.py | 0 .../tests/protocols/openmm_rfe/helpers.py | 0 .../openmm_rfe/test_hybrid_factory.py | 0 .../openmm_rfe/test_hybrid_top_protocol.py | 0 .../openmm_rfe/test_hybrid_top_slow.py | 0 .../test_hybrid_top_tokenization.py | 0 .../openmm_rfe/test_hybrid_top_validation.py | 0 .../tests/protocols/openmm_septop/__init__.py | 0 .../openmm_septop/test_septop_protocol.py | 0 .../openmm_septop/test_septop_slow.py | 0 .../openmm_septop/test_septop_tokenization.py | 0 .../tests/protocols/restraints/__init__.py | 0 .../restraints/test_geometry_base.py | 0 .../restraints/test_geometry_boresch.py | 0 .../restraints/test_geometry_boresch_guest.py | 0 .../restraints/test_geometry_boresch_host.py | 0 .../restraints/test_geometry_flatbottom.py | 0 .../restraints/test_geometry_harmonic.py | 0 .../restraints/test_geometry_utils.py | 0 .../restraints/test_omm_restraints.py | 0 .../restraints/test_openmm_forces.py | 0 .../protocols/restraints/test_settings.py | 0 .../tests/protocols/test_openmm_settings.py | 0 .../tests/protocols/test_openmmutils.py | 0 .../test_openmmutils_serialization.py | 0 .../openfe}/tests/setup/__init__.py | 0 .../alchemical_network_planner/__init__.py | 0 .../alchemical_network_planner/edge_types.py | 0 ...est_relative_alchemical_network_planner.py | 0 .../tests/setup/atom_mapping/__init__.py | 0 .../tests/setup/atom_mapping/conftest.py | 0 .../setup/atom_mapping/test_atommapper.py | 0 .../atom_mapping/test_lomap_atommapper.py | 0 .../setup/atom_mapping/test_lomap_scorers.py | 0 .../atom_mapping/test_perses_atommapper.py | 0 .../setup/atom_mapping/test_perses_scorers.py | 0 .../chemicalsystem_generator/__init__.py | 0 .../component_checks.py | 0 .../test_easy_chemicalsystem_generator.py | 0 .../tests/setup/test_network_planning.py | 0 .../openfe}/tests/storage/__init__.py | 0 .../openfe}/tests/storage/conftest.py | 0 .../tests/storage/test_metadatastore.py | 0 .../tests/storage/test_resultclient.py | 0 .../tests/storage/test_resultserver.py | 0 .../openfe}/tests/utils/__init__.py | 0 .../openfe}/tests/utils/conftest.py | 0 .../test_atommapping_network_plotting.py | 0 .../openfe}/tests/utils/test_duecredit.py | 0 .../openfe}/tests/utils/test_log_control.py | 0 .../tests/utils/test_network_plotting.py | 0 .../tests/utils/test_optional_imports.py | 0 .../openfe}/tests/utils/test_remove_oechem.py | 0 .../openfe}/tests/utils/test_system_probe.py | 0 .../tests/utils/test_visualization_3D.py | 0 {openfe => src/openfe}/utils/__init__.py | 0 .../utils/atommapping_network_plotting.py | 0 {openfe => src/openfe}/utils/custom_typing.py | 0 {openfe => src/openfe}/utils/ligand_utils.py | 0 .../openfe}/utils/logging_control.py | 0 .../openfe}/utils/network_plotting.py | 0 .../openfe}/utils/optional_imports.py | 0 {openfe => src/openfe}/utils/remove_oechem.py | 0 .../openfe}/utils/silence_root_logging.py | 0 {openfe => src/openfe}/utils/system_probe.py | 0 .../openfe}/utils/visualization_3D.py | 0 {openfecli => src/openfecli}/README.md | 0 {openfecli => src/openfecli}/__init__.py | 0 {openfecli => src/openfecli}/cli.py | 0 .../openfecli}/clicktypes/__init__.py | 0 .../openfecli}/clicktypes/hyphenchoice.py | 0 .../openfecli}/commands/__init__.py | 0 .../openfecli}/commands/atommapping.py | 0 .../openfecli}/commands/fetch.py | 0 .../openfecli}/commands/gather.py | 0 .../openfecli}/commands/gather_abfe.py | 0 .../openfecli}/commands/gather_septop.py | 0 .../commands/generate_partial_charges.py | 0 .../openfecli}/commands/plan_rbfe_network.py | 0 .../openfecli}/commands/plan_rhfe_network.py | 0 .../openfecli}/commands/quickrun.py | 0 {openfecli => src/openfecli}/commands/test.py | 0 .../commands/view_ligand_network.py | 0 {openfecli => src/openfecli}/fetchables.py | 0 {openfecli => src/openfecli}/fetching.py | 0 .../openfecli}/parameters/__init__.py | 0 .../openfecli}/parameters/mapper.py | 0 .../openfecli}/parameters/misc.py | 0 .../openfecli}/parameters/mol.py | 0 .../openfecli}/parameters/molecules.py | 0 .../openfecli}/parameters/output.py | 0 .../openfecli}/parameters/output_dir.py | 0 .../parameters/plan_network_options.py | 0 .../openfecli}/parameters/protein.py | 0 .../openfecli}/parameters/utils.py | 0 .../plan_alchemical_networks_utils.py | 0 {openfecli => src/openfecli}/plugins.py | 0 .../openfecli}/tests/__init__.py | 0 .../tests/clicktypes/test_hyphenchoice.py | 0 .../openfecli}/tests/commands/__init__.py | 0 .../openfecli}/tests/commands/conftest.py | 0 .../tests/commands/test_atommapping.py | 0 .../tests/commands/test_charge_generation.py | 0 .../openfecli}/tests/commands/test_gather.py | 0 .../test_abfe_full_results_dg_.tsv | 0 .../test_abfe_full_results_raw_.tsv | 0 .../test_abfe_single_repeat_dg_.tsv | 0 .../test_abfe_single_repeat_raw_.tsv | 0 .../test_cmet_failed_edge_ddg_.tsv | 0 .../test_cmet_failed_edge_raw_.tsv | 0 .../test_cmet_full_results_ddg_.tsv | 0 .../test_cmet_full_results_dg_.tsv | 0 .../test_cmet_full_results_raw_.tsv | 0 ...ng_all_complex_legs_allow_partial_ddg_.tsv | 0 ...met_missing_all_complex_legs_fail_ddg_.tsv | 0 ...cmet_missing_all_complex_legs_fail_dg_.tsv | 0 .../test_cmet_missing_complex_leg_ddg_.tsv | 0 .../test_cmet_missing_complex_leg_dg_.tsv | 0 .../test_cmet_missing_complex_leg_raw_.tsv | 0 .../test_cmet_missing_edge_ddg_.tsv | 0 .../test_cmet_missing_edge_dg_.tsv | 0 .../test_cmet_missing_edge_raw_.tsv | 0 .../test_septop_full_results_ddg_.tsv | 0 .../test_septop_full_results_dg_.tsv | 0 .../test_septop_full_results_raw_.tsv | 0 .../test_septop_single_repeat_ddg_.tsv | 0 .../test_septop_single_repeat_dg_.tsv | 0 .../test_septop_single_repeat_raw_.tsv | 0 .../commands/test_ligand_network_viewer.py | 0 .../tests/commands/test_plan_rbfe_network.py | 0 .../tests/commands/test_plan_rhfe_network.py | 0 .../tests/commands/test_quickrun.py | 0 .../openfecli}/tests/commands/test_test.py | 0 .../openfecli}/tests/conftest.py | 0 .../openfecli}/tests/data/__init__.py | 0 .../tests/data/bad_transformation.json | 0 .../openfecli}/tests/data/rbfe_results.tar.gz | Bin .../tests/data/rbfe_tutorial/__init__.py | 0 .../tests/data/rbfe_tutorial/tyk2_ligands.sdf | 0 .../tests/data/rbfe_tutorial/tyk2_protein.pdb | 0 .../openfecli}/tests/data/transformation.json | 0 .../openfecli}/tests/dev/__init__.py | 0 .../tests/dev/write_transformation_json.py | 0 .../openfecli}/tests/parameters/__init__.py | 0 .../tests/parameters/test_mapper.py | 0 .../openfecli}/tests/parameters/test_mol.py | 0 .../tests/parameters/test_molecules.py | 0 .../tests/parameters/test_output.py | 0 .../tests/parameters/test_output_dir.py | 0 .../parameters/test_plan_network_options.py | 0 .../tests/parameters/test_protein.py | 0 .../openfecli}/tests/parameters/test_utils.py | 0 .../openfecli}/tests/test_cli.py | 0 .../openfecli}/tests/test_fetchables.py | 0 .../openfecli}/tests/test_fetching.py | 0 .../openfecli}/tests/test_plugins.py | 0 .../openfecli}/tests/test_rbfe_tutorial.py | 0 .../openfecli}/tests/test_utils.py | 0 {openfecli => src/openfecli}/tests/utils.py | 0 {openfecli => src/openfecli}/utils.py | 0 327 files changed, 32 insertions(+), 30 deletions(-) rename {openfe => src/openfe}/__init__.py (100%) rename {openfe => src/openfe}/analysis/__init__.py (100%) rename {openfe => src/openfe}/analysis/plotting.py (100%) rename {openfe => src/openfe}/due.py (100%) rename {openfe => src/openfe}/orchestration/__init__.py (100%) rename {openfe => src/openfe}/protocols/__init__.py (100%) rename {openfe => src/openfe}/protocols/openmm_afe/__init__.py (100%) rename {openfe => src/openfe}/protocols/openmm_afe/abfe_units.py (100%) rename {openfe => src/openfe}/protocols/openmm_afe/afe_protocol_results.py (100%) rename {openfe => src/openfe}/protocols/openmm_afe/ahfe_units.py (100%) rename {openfe => src/openfe}/protocols/openmm_afe/base_afe_units.py (100%) rename {openfe => src/openfe}/protocols/openmm_afe/equil_afe_settings.py (100%) rename {openfe => src/openfe}/protocols/openmm_afe/equil_binding_afe_method.py (100%) rename {openfe => src/openfe}/protocols/openmm_afe/equil_solvation_afe_method.py (100%) rename {openfe => src/openfe}/protocols/openmm_md/__init__.py (100%) rename {openfe => src/openfe}/protocols/openmm_md/plain_md_methods.py (100%) rename {openfe => src/openfe}/protocols/openmm_md/plain_md_settings.py (100%) rename {openfe => src/openfe}/protocols/openmm_rfe/__init__.py (100%) rename {openfe => src/openfe}/protocols/openmm_rfe/_rfe_utils/__init__.py (100%) rename {openfe => src/openfe}/protocols/openmm_rfe/_rfe_utils/lambdaprotocol.py (100%) rename {openfe => src/openfe}/protocols/openmm_rfe/_rfe_utils/multistate.py (100%) rename {openfe => src/openfe}/protocols/openmm_rfe/_rfe_utils/relative.py (100%) rename {openfe => src/openfe}/protocols/openmm_rfe/_rfe_utils/topologyhelpers.py (100%) rename {openfe => src/openfe}/protocols/openmm_rfe/equil_rfe_methods.py (100%) rename {openfe => src/openfe}/protocols/openmm_rfe/equil_rfe_settings.py (100%) rename {openfe => src/openfe}/protocols/openmm_rfe/hybridtop_protocol_results.py (100%) rename {openfe => src/openfe}/protocols/openmm_rfe/hybridtop_protocols.py (100%) rename {openfe => src/openfe}/protocols/openmm_rfe/hybridtop_units.py (100%) rename {openfe => src/openfe}/protocols/openmm_septop/__init__.py (100%) rename {openfe => src/openfe}/protocols/openmm_septop/base.py (100%) rename {openfe => src/openfe}/protocols/openmm_septop/equil_septop_method.py (100%) rename {openfe => src/openfe}/protocols/openmm_septop/equil_septop_settings.py (100%) rename {openfe => src/openfe}/protocols/openmm_septop/utils.py (100%) rename {openfe => src/openfe}/protocols/openmm_utils/__init__.py (100%) rename {openfe => src/openfe}/protocols/openmm_utils/charge_generation.py (100%) rename {openfe => src/openfe}/protocols/openmm_utils/multistate_analysis.py (100%) rename {openfe => src/openfe}/protocols/openmm_utils/omm_compute.py (100%) rename {openfe => src/openfe}/protocols/openmm_utils/omm_settings.py (100%) rename {openfe => src/openfe}/protocols/openmm_utils/serialization.py (100%) rename {openfe => src/openfe}/protocols/openmm_utils/settings_validation.py (100%) rename {openfe => src/openfe}/protocols/openmm_utils/system_creation.py (100%) rename {openfe => src/openfe}/protocols/openmm_utils/system_validation.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/__init__.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/geometry/__init__.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/geometry/base.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/geometry/boresch/__init__.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/geometry/boresch/geometry.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/geometry/boresch/guest.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/geometry/boresch/host.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/geometry/flatbottom.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/geometry/harmonic.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/geometry/utils.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/openmm/__init__.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/openmm/omm_forces.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/openmm/omm_restraints.py (100%) rename {openfe => src/openfe}/protocols/restraint_utils/settings.py (100%) rename {openfe => src/openfe}/setup/__init__.py (100%) rename {openfe => src/openfe}/setup/alchemical_network_planner/__init__.py (100%) rename {openfe => src/openfe}/setup/alchemical_network_planner/abstract_alchemical_network_planner.py (100%) rename {openfe => src/openfe}/setup/alchemical_network_planner/relative_alchemical_network_planner.py (100%) rename {openfe => src/openfe}/setup/atom_mapping/__init__.py (100%) rename {openfe => src/openfe}/setup/atom_mapping/ligandatommapper.py (100%) rename {openfe => src/openfe}/setup/atom_mapping/lomap_mapper.py (100%) rename {openfe => src/openfe}/setup/atom_mapping/lomap_scorers.py (100%) rename {openfe => src/openfe}/setup/atom_mapping/perses_mapper.py (100%) rename {openfe => src/openfe}/setup/atom_mapping/perses_scorers.py (100%) rename {openfe => src/openfe}/setup/chemicalsystem_generator/__init__.py (100%) rename {openfe => src/openfe}/setup/chemicalsystem_generator/abstract_chemicalsystem_generator.py (100%) rename {openfe => src/openfe}/setup/chemicalsystem_generator/easy_chemicalsystem_generator.py (100%) rename {openfe => src/openfe}/setup/ligand_network_planning.py (100%) rename {openfe => src/openfe}/storage/__init__.py (100%) rename {openfe => src/openfe}/storage/metadatastore.py (100%) rename {openfe => src/openfe}/storage/resultclient.py (100%) rename {openfe => src/openfe}/storage/resultserver.py (100%) rename {openfe => src/openfe}/tests/__init__.py (100%) rename {openfe => src/openfe}/tests/analysis/__init__.py (100%) rename {openfe => src/openfe}/tests/analysis/test_plotting.py (100%) rename {openfe => src/openfe}/tests/conftest.py (100%) rename {openfe => src/openfe}/tests/data/181l_only.pdb (100%) rename {openfe => src/openfe}/tests/data/6CZJ.pdb.gz (100%) rename {openfe => src/openfe}/tests/data/CN.sdf (100%) rename {openfe => src/openfe}/tests/data/__init__.py (100%) rename {openfe => src/openfe}/tests/data/benzene_modifications.sdf (100%) rename {openfe => src/openfe}/tests/data/cdk8/__init__.py (100%) rename {openfe => src/openfe}/tests/data/cdk8/cdk8_ligands.sdf (100%) rename {openfe => src/openfe}/tests/data/cdk8/cdk8_protein.pdb (100%) rename {openfe => src/openfe}/tests/data/eg5/__init__.py (100%) rename {openfe => src/openfe}/tests/data/eg5/eg5_cofactor.sdf (100%) rename {openfe => src/openfe}/tests/data/eg5/eg5_ligands.sdf (100%) rename {openfe => src/openfe}/tests/data/eg5/eg5_protein.pdb (100%) rename {openfe => src/openfe}/tests/data/external_formats/__init__.py (100%) rename {openfe => src/openfe}/tests/data/external_formats/somebenzenes_edges.edge (100%) rename {openfe => src/openfe}/tests/data/external_formats/somebenzenes_nes.dat (100%) rename {openfe => src/openfe}/tests/data/htf/__init__.py (100%) rename {openfe => src/openfe}/tests/data/htf/t4_lysozyme_data/chlorobenzene.sdf (100%) rename {openfe => src/openfe}/tests/data/htf/t4_lysozyme_data/fluorobenzene.sdf (100%) rename {openfe => src/openfe}/tests/data/htf/t4_lysozyme_data/t4_lysozyme_solvated.pdb.gz (100%) rename {openfe => src/openfe}/tests/data/lomap_basic/1,3,7-trimethylnaphthalene.mol2 (100%) rename {openfe => src/openfe}/tests/data/lomap_basic/1-butyl-4-methylbenzene.mol2 (100%) rename {openfe => src/openfe}/tests/data/lomap_basic/2,6-dimethylnaphthalene.mol2 (100%) rename {openfe => src/openfe}/tests/data/lomap_basic/2-methyl-6-propylnaphthalene.mol2 (100%) rename {openfe => src/openfe}/tests/data/lomap_basic/2-methylnaphthalene.mol2 (100%) rename {openfe => src/openfe}/tests/data/lomap_basic/2-naftanol.mol2 (100%) rename {openfe => src/openfe}/tests/data/lomap_basic/README.md (100%) rename {openfe => src/openfe}/tests/data/lomap_basic/__init__.py (100%) rename {openfe => src/openfe}/tests/data/lomap_basic/methylcyclohexane.mol2 (100%) rename {openfe => src/openfe}/tests/data/lomap_basic/toluene.mol2 (100%) rename {openfe => src/openfe}/tests/data/multi_molecule.sdf (100%) rename {openfe => src/openfe}/tests/data/openmm_afe/ABFEProtocol_json_results.json.gz (100%) rename {openfe => src/openfe}/tests/data/openmm_afe/AHFEProtocol_json_results.gz (100%) rename {openfe => src/openfe}/tests/data/openmm_afe/T4_abfe_system.xml.bz2 (100%) rename {openfe => src/openfe}/tests/data/openmm_afe/__init__.py (100%) rename {openfe => src/openfe}/tests/data/openmm_md/MDProtocol_json_results.gz (100%) rename {openfe => src/openfe}/tests/data/openmm_md/__init__.py (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/RHFEProtocol_json_results.gz (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/__init__.py (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/benzene_toluene_hybrid_top/hybrid_topology_atoms.csv (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/benzene_toluene_hybrid_top/hybrid_topology_bonds.txt (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/charged_benzenes.sdf (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/dummy_charge_ligand_23.sdf (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/dummy_charge_ligand_55.sdf (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/ligand_23.sdf (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/ligand_55.sdf (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/malt1_shapefit_1832577-09-9.sdf (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/malt1_shapefit_Pfizer-01-01.sdf (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/reference.xml (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/vacuum_nocoord.nc (100%) rename {openfe => src/openfe}/tests/data/openmm_rfe/vacuum_nocoord_checkpoint.nc (100%) rename {openfe => src/openfe}/tests/data/openmm_septop/SepTopProtocol_json_results.gz (100%) rename {openfe => src/openfe}/tests/data/openmm_septop/__init__.py (100%) rename {openfe => src/openfe}/tests/data/openmm_septop/system.xml.bz2 (100%) rename {openfe => src/openfe}/tests/data/serialization/__init__.py (100%) rename {openfe => src/openfe}/tests/data/serialization/ethane_template.sdf (100%) rename {openfe => src/openfe}/tests/data/serialization/network_template.graphml (100%) rename {openfe => src/openfe}/tests/dev/__init__.py (100%) rename {openfe => src/openfe}/tests/dev/serialization_test_templates.py (100%) rename {openfe => src/openfe}/tests/protocols/__init__.py (100%) rename {openfe => src/openfe}/tests/protocols/conftest.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_abfe/__init__.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_abfe/conftest.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_abfe/test_abfe_energies.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_abfe/test_abfe_protocol.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_abfe/test_abfe_protocol_results.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_abfe/test_abfe_settings.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_abfe/test_abfe_slow.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_abfe/test_abfe_tokenization.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_abfe/test_abfe_validation.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_abfe/utils.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_ahfe/__init__.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_ahfe/test_ahfe_protocol.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_ahfe/test_ahfe_protocol_results.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_ahfe/test_ahfe_settings.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_ahfe/test_ahfe_slow.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_ahfe/test_ahfe_tokenization.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_ahfe/test_ahfe_validation.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_ahfe/utils.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_md/__init__.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_md/test_plain_md_protocol.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_md/test_plain_md_slow.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_md/test_plain_md_tokenization.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_rfe/__init__.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_rfe/helpers.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_rfe/test_hybrid_factory.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_rfe/test_hybrid_top_slow.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_rfe/test_hybrid_top_tokenization.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_rfe/test_hybrid_top_validation.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_septop/__init__.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_septop/test_septop_protocol.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_septop/test_septop_slow.py (100%) rename {openfe => src/openfe}/tests/protocols/openmm_septop/test_septop_tokenization.py (100%) rename {openfe => src/openfe}/tests/protocols/restraints/__init__.py (100%) rename {openfe => src/openfe}/tests/protocols/restraints/test_geometry_base.py (100%) rename {openfe => src/openfe}/tests/protocols/restraints/test_geometry_boresch.py (100%) rename {openfe => src/openfe}/tests/protocols/restraints/test_geometry_boresch_guest.py (100%) rename {openfe => src/openfe}/tests/protocols/restraints/test_geometry_boresch_host.py (100%) rename {openfe => src/openfe}/tests/protocols/restraints/test_geometry_flatbottom.py (100%) rename {openfe => src/openfe}/tests/protocols/restraints/test_geometry_harmonic.py (100%) rename {openfe => src/openfe}/tests/protocols/restraints/test_geometry_utils.py (100%) rename {openfe => src/openfe}/tests/protocols/restraints/test_omm_restraints.py (100%) rename {openfe => src/openfe}/tests/protocols/restraints/test_openmm_forces.py (100%) rename {openfe => src/openfe}/tests/protocols/restraints/test_settings.py (100%) rename {openfe => src/openfe}/tests/protocols/test_openmm_settings.py (100%) rename {openfe => src/openfe}/tests/protocols/test_openmmutils.py (100%) rename {openfe => src/openfe}/tests/protocols/test_openmmutils_serialization.py (100%) rename {openfe => src/openfe}/tests/setup/__init__.py (100%) rename {openfe => src/openfe}/tests/setup/alchemical_network_planner/__init__.py (100%) rename {openfe => src/openfe}/tests/setup/alchemical_network_planner/edge_types.py (100%) rename {openfe => src/openfe}/tests/setup/alchemical_network_planner/test_relative_alchemical_network_planner.py (100%) rename {openfe => src/openfe}/tests/setup/atom_mapping/__init__.py (100%) rename {openfe => src/openfe}/tests/setup/atom_mapping/conftest.py (100%) rename {openfe => src/openfe}/tests/setup/atom_mapping/test_atommapper.py (100%) rename {openfe => src/openfe}/tests/setup/atom_mapping/test_lomap_atommapper.py (100%) rename {openfe => src/openfe}/tests/setup/atom_mapping/test_lomap_scorers.py (100%) rename {openfe => src/openfe}/tests/setup/atom_mapping/test_perses_atommapper.py (100%) rename {openfe => src/openfe}/tests/setup/atom_mapping/test_perses_scorers.py (100%) rename {openfe => src/openfe}/tests/setup/chemicalsystem_generator/__init__.py (100%) rename {openfe => src/openfe}/tests/setup/chemicalsystem_generator/component_checks.py (100%) rename {openfe => src/openfe}/tests/setup/chemicalsystem_generator/test_easy_chemicalsystem_generator.py (100%) rename {openfe => src/openfe}/tests/setup/test_network_planning.py (100%) rename {openfe => src/openfe}/tests/storage/__init__.py (100%) rename {openfe => src/openfe}/tests/storage/conftest.py (100%) rename {openfe => src/openfe}/tests/storage/test_metadatastore.py (100%) rename {openfe => src/openfe}/tests/storage/test_resultclient.py (100%) rename {openfe => src/openfe}/tests/storage/test_resultserver.py (100%) rename {openfe => src/openfe}/tests/utils/__init__.py (100%) rename {openfe => src/openfe}/tests/utils/conftest.py (100%) rename {openfe => src/openfe}/tests/utils/test_atommapping_network_plotting.py (100%) rename {openfe => src/openfe}/tests/utils/test_duecredit.py (100%) rename {openfe => src/openfe}/tests/utils/test_log_control.py (100%) rename {openfe => src/openfe}/tests/utils/test_network_plotting.py (100%) rename {openfe => src/openfe}/tests/utils/test_optional_imports.py (100%) rename {openfe => src/openfe}/tests/utils/test_remove_oechem.py (100%) rename {openfe => src/openfe}/tests/utils/test_system_probe.py (100%) rename {openfe => src/openfe}/tests/utils/test_visualization_3D.py (100%) rename {openfe => src/openfe}/utils/__init__.py (100%) rename {openfe => src/openfe}/utils/atommapping_network_plotting.py (100%) rename {openfe => src/openfe}/utils/custom_typing.py (100%) rename {openfe => src/openfe}/utils/ligand_utils.py (100%) rename {openfe => src/openfe}/utils/logging_control.py (100%) rename {openfe => src/openfe}/utils/network_plotting.py (100%) rename {openfe => src/openfe}/utils/optional_imports.py (100%) rename {openfe => src/openfe}/utils/remove_oechem.py (100%) rename {openfe => src/openfe}/utils/silence_root_logging.py (100%) rename {openfe => src/openfe}/utils/system_probe.py (100%) rename {openfe => src/openfe}/utils/visualization_3D.py (100%) rename {openfecli => src/openfecli}/README.md (100%) rename {openfecli => src/openfecli}/__init__.py (100%) rename {openfecli => src/openfecli}/cli.py (100%) rename {openfecli => src/openfecli}/clicktypes/__init__.py (100%) rename {openfecli => src/openfecli}/clicktypes/hyphenchoice.py (100%) rename {openfecli => src/openfecli}/commands/__init__.py (100%) rename {openfecli => src/openfecli}/commands/atommapping.py (100%) rename {openfecli => src/openfecli}/commands/fetch.py (100%) rename {openfecli => src/openfecli}/commands/gather.py (100%) rename {openfecli => src/openfecli}/commands/gather_abfe.py (100%) rename {openfecli => src/openfecli}/commands/gather_septop.py (100%) rename {openfecli => src/openfecli}/commands/generate_partial_charges.py (100%) rename {openfecli => src/openfecli}/commands/plan_rbfe_network.py (100%) rename {openfecli => src/openfecli}/commands/plan_rhfe_network.py (100%) rename {openfecli => src/openfecli}/commands/quickrun.py (100%) rename {openfecli => src/openfecli}/commands/test.py (100%) rename {openfecli => src/openfecli}/commands/view_ligand_network.py (100%) rename {openfecli => src/openfecli}/fetchables.py (100%) rename {openfecli => src/openfecli}/fetching.py (100%) rename {openfecli => src/openfecli}/parameters/__init__.py (100%) rename {openfecli => src/openfecli}/parameters/mapper.py (100%) rename {openfecli => src/openfecli}/parameters/misc.py (100%) rename {openfecli => src/openfecli}/parameters/mol.py (100%) rename {openfecli => src/openfecli}/parameters/molecules.py (100%) rename {openfecli => src/openfecli}/parameters/output.py (100%) rename {openfecli => src/openfecli}/parameters/output_dir.py (100%) rename {openfecli => src/openfecli}/parameters/plan_network_options.py (100%) rename {openfecli => src/openfecli}/parameters/protein.py (100%) rename {openfecli => src/openfecli}/parameters/utils.py (100%) rename {openfecli => src/openfecli}/plan_alchemical_networks_utils.py (100%) rename {openfecli => src/openfecli}/plugins.py (100%) rename {openfecli => src/openfecli}/tests/__init__.py (100%) rename {openfecli => src/openfecli}/tests/clicktypes/test_hyphenchoice.py (100%) rename {openfecli => src/openfecli}/tests/commands/__init__.py (100%) rename {openfecli => src/openfecli}/tests/commands/conftest.py (100%) rename {openfecli => src/openfecli}/tests/commands/test_atommapping.py (100%) rename {openfecli => src/openfecli}/tests/commands/test_charge_generation.py (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather.py (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_abfe_full_results_dg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_abfe_full_results_raw_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_abfe_single_repeat_dg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_abfe_single_repeat_raw_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_failed_edge_ddg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_failed_edge_raw_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_full_results_ddg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_full_results_dg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_full_results_raw_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_missing_all_complex_legs_allow_partial_ddg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_ddg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_dg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_missing_complex_leg_ddg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_missing_complex_leg_dg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_missing_complex_leg_raw_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_missing_edge_ddg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_missing_edge_dg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_cmet_missing_edge_raw_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_septop_full_results_ddg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_septop_full_results_dg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_septop_full_results_raw_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_septop_single_repeat_ddg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_septop_single_repeat_dg_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_gather/test_septop_single_repeat_raw_.tsv (100%) rename {openfecli => src/openfecli}/tests/commands/test_ligand_network_viewer.py (100%) rename {openfecli => src/openfecli}/tests/commands/test_plan_rbfe_network.py (100%) rename {openfecli => src/openfecli}/tests/commands/test_plan_rhfe_network.py (100%) rename {openfecli => src/openfecli}/tests/commands/test_quickrun.py (100%) rename {openfecli => src/openfecli}/tests/commands/test_test.py (100%) rename {openfecli => src/openfecli}/tests/conftest.py (100%) rename {openfecli => src/openfecli}/tests/data/__init__.py (100%) rename {openfecli => src/openfecli}/tests/data/bad_transformation.json (100%) rename {openfecli => src/openfecli}/tests/data/rbfe_results.tar.gz (100%) rename {openfecli => src/openfecli}/tests/data/rbfe_tutorial/__init__.py (100%) rename {openfecli => src/openfecli}/tests/data/rbfe_tutorial/tyk2_ligands.sdf (100%) rename {openfecli => src/openfecli}/tests/data/rbfe_tutorial/tyk2_protein.pdb (100%) rename {openfecli => src/openfecli}/tests/data/transformation.json (100%) rename {openfecli => src/openfecli}/tests/dev/__init__.py (100%) rename {openfecli => src/openfecli}/tests/dev/write_transformation_json.py (100%) rename {openfecli => src/openfecli}/tests/parameters/__init__.py (100%) rename {openfecli => src/openfecli}/tests/parameters/test_mapper.py (100%) rename {openfecli => src/openfecli}/tests/parameters/test_mol.py (100%) rename {openfecli => src/openfecli}/tests/parameters/test_molecules.py (100%) rename {openfecli => src/openfecli}/tests/parameters/test_output.py (100%) rename {openfecli => src/openfecli}/tests/parameters/test_output_dir.py (100%) rename {openfecli => src/openfecli}/tests/parameters/test_plan_network_options.py (100%) rename {openfecli => src/openfecli}/tests/parameters/test_protein.py (100%) rename {openfecli => src/openfecli}/tests/parameters/test_utils.py (100%) rename {openfecli => src/openfecli}/tests/test_cli.py (100%) rename {openfecli => src/openfecli}/tests/test_fetchables.py (100%) rename {openfecli => src/openfecli}/tests/test_fetching.py (100%) rename {openfecli => src/openfecli}/tests/test_plugins.py (100%) rename {openfecli => src/openfecli}/tests/test_rbfe_tutorial.py (100%) rename {openfecli => src/openfecli}/tests/test_utils.py (100%) rename {openfecli => src/openfecli}/tests/utils.py (100%) rename {openfecli => src/openfecli}/utils.py (100%) diff --git a/.github/workflows/check_for_api_break.yaml b/.github/workflows/check_for_api_break.yaml index e3d74f141..661ea52dd 100644 --- a/.github/workflows/check_for_api_break.yaml +++ b/.github/workflows/check_for_api_break.yaml @@ -27,8 +27,8 @@ jobs: id: check run: | pip install griffe - griffe check "openfe" --verbose -a origin/main - griffe check "openfecli" --verbose -a origin/main + griffe check "src/openfe" --verbose -a origin/main + griffe check "src/openfecli" --verbose -a origin/main - name: Manage PR Comments uses: actions/github-script@v7 diff --git a/.github/workflows/cpu-long-tests.yaml b/.github/workflows/cpu-long-tests.yaml index 346e3a934..8cc1e480f 100644 --- a/.github/workflows/cpu-long-tests.yaml +++ b/.github/workflows/cpu-long-tests.yaml @@ -92,7 +92,7 @@ jobs: DUECREDIT_ENABLE: 'yes' OFE_INTEGRATION_TESTS: FALSE run: | - pytest -n logical -vv --durations=10 --runslow openfecli/tests/ openfe/tests/ + pytest -n logical -vv --durations=10 --runslow src/openfecli/tests/ src/openfe/tests/ stop-aws-runner: runs-on: ubuntu-latest diff --git a/.github/workflows/gpu-integration-tests.yaml b/.github/workflows/gpu-integration-tests.yaml index 22db41163..535e526fa 100644 --- a/.github/workflows/gpu-integration-tests.yaml +++ b/.github/workflows/gpu-integration-tests.yaml @@ -96,7 +96,7 @@ jobs: OFE_INTEGRATION_TESTS: TRUE run: | # The -m flag will only run tests with @pytest.mark.integration - pytest -n logical -vv --durations=10 -m integration openfecli/tests/ openfe/tests/ + pytest -n logical -vv --durations=10 -m integration src/openfecli/tests/ src/openfe/tests/ stop-aws-runner: runs-on: ubuntu-latest diff --git a/.github/workflows/test-feedstock-pkg-build.yaml b/.github/workflows/test-feedstock-pkg-build.yaml index bb4e901bf..b152bcc20 100644 --- a/.github/workflows/test-feedstock-pkg-build.yaml +++ b/.github/workflows/test-feedstock-pkg-build.yaml @@ -46,7 +46,7 @@ jobs: # TODO just checkout the repo where we need it? - name: Copy source code to recipe folder - run: cp -r openfe openfe-feedstock/recipe/openfe_source + run: cp -r src/openfe openfe-feedstock/recipe/openfe_source - name: Modify feedstock to use local path run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e890a1cdc..d548167b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,7 @@ repos: rev: v6.0.0 hooks: - id: check-added-large-files + args: ["--maxkb=900"] - id: check-case-conflict - id: check-executables-have-shebangs - id: check-symlinks diff --git a/MANIFEST.in b/MANIFEST.in index 6e18d4c72..d2479fcec 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,18 +1,18 @@ -recursive-include openfe/tests/data/ *.sdf -recursive-include openfe/tests/data/ *.bz2 -recursive-include openfe/tests/data/ *.csv -recursive-include openfe/tests/data/ *.pdb -recursive-include openfe/tests/data/ *.mol2 -recursive-include openfe/tests/data/ *.xml -recursive-include openfe/tests/data/ *.graphml -recursive-include openfe/tests/data/ *.edge -recursive-include openfe/tests/data/ *.dat -recursive-include openfe/tests/data/ *.txt -recursive-include openfe/tests/data/ *.gz -recursive-include openfe/tests/data/ *json_results.gz -include openfecli/tests/data/*.json -include openfecli/tests/data/*.tar.gz -include openfecli/tests/commands/test_gather/*.tsv -recursive-include openfecli/tests/ *.sdf -recursive-include openfecli/tests/ *.pdb -include openfe/tests/data/openmm_rfe/vacuum_nocoord.nc +recursive-include src/openfe/tests/data/ *.sdf +recursive-include src/openfe/tests/data/ *.bz2 +recursive-include src/openfe/tests/data/ *.csv +recursive-include src/openfe/tests/data/ *.pdb +recursive-include src/openfe/tests/data/ *.mol2 +recursive-include src/openfe/tests/data/ *.xml +recursive-include src/openfe/tests/data/ *.graphml +recursive-include src/openfe/tests/data/ *.edge +recursive-include src/openfe/tests/data/ *.dat +recursive-include src/openfe/tests/data/ *.txt +recursive-include src/openfe/tests/data/ *.gz +recursive-include src/openfe/tests/data/ *json_results.gz +include src/openfecli/tests/data/*.json +include src/openfecli/tests/data/*.tar.gz +include src/openfecli/tests/commands/test_gather/*.tsv +recursive-include src/openfecli/tests/ *.sdf +recursive-include src/openfecli/tests/ *.pdb +include src/openfe/tests/data/openmm_rfe/vacuum_nocoord.nc diff --git a/pyproject.toml b/pyproject.toml index 784d16912..8f995c09e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,11 +38,12 @@ scripts.openfe = "openfecli.cli:main" zip-safe = false include-package-data = true -[tool.setuptools.packages] -find = { namespaces = false } +[tool.setuptools.packages.find] +where = [ "src" ] +namespaces = false [tool.setuptools.package-data] -openfe = [ '"./openfe/tests/data/lomap_basic/toluene.mol2"' ] +openfe = [ '"./src/openfe/tests/data/lomap_basic/toluene.mol2"' ] [tool.setuptools_scm] fallback_version = "0.0.0" @@ -71,9 +72,9 @@ lint.isort.known-first-party = [ "openfe" ] [tool.coverage.run] omit = [ - "openfe/due.py", - "*/tests/dev/*py", - "*/tests/protocols/test_openmm_rfe_slow.py", + "src/openfe/due.py", + "src/*/tests/dev/*py", + "src/*/tests/protocols/test_openmm_rfe_slow.py", ] [tool.coverage.report] @@ -86,6 +87,6 @@ exclude_lines = [ ] [tool.mypy] -files = "openfe" +files = "src/openfe" # TODO: add src/openfecli ignore_missing_imports = true warn_unused_ignores = true diff --git a/openfe/__init__.py b/src/openfe/__init__.py similarity index 100% rename from openfe/__init__.py rename to src/openfe/__init__.py diff --git a/openfe/analysis/__init__.py b/src/openfe/analysis/__init__.py similarity index 100% rename from openfe/analysis/__init__.py rename to src/openfe/analysis/__init__.py diff --git a/openfe/analysis/plotting.py b/src/openfe/analysis/plotting.py similarity index 100% rename from openfe/analysis/plotting.py rename to src/openfe/analysis/plotting.py diff --git a/openfe/due.py b/src/openfe/due.py similarity index 100% rename from openfe/due.py rename to src/openfe/due.py diff --git a/openfe/orchestration/__init__.py b/src/openfe/orchestration/__init__.py similarity index 100% rename from openfe/orchestration/__init__.py rename to src/openfe/orchestration/__init__.py diff --git a/openfe/protocols/__init__.py b/src/openfe/protocols/__init__.py similarity index 100% rename from openfe/protocols/__init__.py rename to src/openfe/protocols/__init__.py diff --git a/openfe/protocols/openmm_afe/__init__.py b/src/openfe/protocols/openmm_afe/__init__.py similarity index 100% rename from openfe/protocols/openmm_afe/__init__.py rename to src/openfe/protocols/openmm_afe/__init__.py diff --git a/openfe/protocols/openmm_afe/abfe_units.py b/src/openfe/protocols/openmm_afe/abfe_units.py similarity index 100% rename from openfe/protocols/openmm_afe/abfe_units.py rename to src/openfe/protocols/openmm_afe/abfe_units.py diff --git a/openfe/protocols/openmm_afe/afe_protocol_results.py b/src/openfe/protocols/openmm_afe/afe_protocol_results.py similarity index 100% rename from openfe/protocols/openmm_afe/afe_protocol_results.py rename to src/openfe/protocols/openmm_afe/afe_protocol_results.py diff --git a/openfe/protocols/openmm_afe/ahfe_units.py b/src/openfe/protocols/openmm_afe/ahfe_units.py similarity index 100% rename from openfe/protocols/openmm_afe/ahfe_units.py rename to src/openfe/protocols/openmm_afe/ahfe_units.py diff --git a/openfe/protocols/openmm_afe/base_afe_units.py b/src/openfe/protocols/openmm_afe/base_afe_units.py similarity index 100% rename from openfe/protocols/openmm_afe/base_afe_units.py rename to src/openfe/protocols/openmm_afe/base_afe_units.py diff --git a/openfe/protocols/openmm_afe/equil_afe_settings.py b/src/openfe/protocols/openmm_afe/equil_afe_settings.py similarity index 100% rename from openfe/protocols/openmm_afe/equil_afe_settings.py rename to src/openfe/protocols/openmm_afe/equil_afe_settings.py diff --git a/openfe/protocols/openmm_afe/equil_binding_afe_method.py b/src/openfe/protocols/openmm_afe/equil_binding_afe_method.py similarity index 100% rename from openfe/protocols/openmm_afe/equil_binding_afe_method.py rename to src/openfe/protocols/openmm_afe/equil_binding_afe_method.py diff --git a/openfe/protocols/openmm_afe/equil_solvation_afe_method.py b/src/openfe/protocols/openmm_afe/equil_solvation_afe_method.py similarity index 100% rename from openfe/protocols/openmm_afe/equil_solvation_afe_method.py rename to src/openfe/protocols/openmm_afe/equil_solvation_afe_method.py diff --git a/openfe/protocols/openmm_md/__init__.py b/src/openfe/protocols/openmm_md/__init__.py similarity index 100% rename from openfe/protocols/openmm_md/__init__.py rename to src/openfe/protocols/openmm_md/__init__.py diff --git a/openfe/protocols/openmm_md/plain_md_methods.py b/src/openfe/protocols/openmm_md/plain_md_methods.py similarity index 100% rename from openfe/protocols/openmm_md/plain_md_methods.py rename to src/openfe/protocols/openmm_md/plain_md_methods.py diff --git a/openfe/protocols/openmm_md/plain_md_settings.py b/src/openfe/protocols/openmm_md/plain_md_settings.py similarity index 100% rename from openfe/protocols/openmm_md/plain_md_settings.py rename to src/openfe/protocols/openmm_md/plain_md_settings.py diff --git a/openfe/protocols/openmm_rfe/__init__.py b/src/openfe/protocols/openmm_rfe/__init__.py similarity index 100% rename from openfe/protocols/openmm_rfe/__init__.py rename to src/openfe/protocols/openmm_rfe/__init__.py diff --git a/openfe/protocols/openmm_rfe/_rfe_utils/__init__.py b/src/openfe/protocols/openmm_rfe/_rfe_utils/__init__.py similarity index 100% rename from openfe/protocols/openmm_rfe/_rfe_utils/__init__.py rename to src/openfe/protocols/openmm_rfe/_rfe_utils/__init__.py diff --git a/openfe/protocols/openmm_rfe/_rfe_utils/lambdaprotocol.py b/src/openfe/protocols/openmm_rfe/_rfe_utils/lambdaprotocol.py similarity index 100% rename from openfe/protocols/openmm_rfe/_rfe_utils/lambdaprotocol.py rename to src/openfe/protocols/openmm_rfe/_rfe_utils/lambdaprotocol.py diff --git a/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py b/src/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py similarity index 100% rename from openfe/protocols/openmm_rfe/_rfe_utils/multistate.py rename to src/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py diff --git a/openfe/protocols/openmm_rfe/_rfe_utils/relative.py b/src/openfe/protocols/openmm_rfe/_rfe_utils/relative.py similarity index 100% rename from openfe/protocols/openmm_rfe/_rfe_utils/relative.py rename to src/openfe/protocols/openmm_rfe/_rfe_utils/relative.py diff --git a/openfe/protocols/openmm_rfe/_rfe_utils/topologyhelpers.py b/src/openfe/protocols/openmm_rfe/_rfe_utils/topologyhelpers.py similarity index 100% rename from openfe/protocols/openmm_rfe/_rfe_utils/topologyhelpers.py rename to src/openfe/protocols/openmm_rfe/_rfe_utils/topologyhelpers.py diff --git a/openfe/protocols/openmm_rfe/equil_rfe_methods.py b/src/openfe/protocols/openmm_rfe/equil_rfe_methods.py similarity index 100% rename from openfe/protocols/openmm_rfe/equil_rfe_methods.py rename to src/openfe/protocols/openmm_rfe/equil_rfe_methods.py diff --git a/openfe/protocols/openmm_rfe/equil_rfe_settings.py b/src/openfe/protocols/openmm_rfe/equil_rfe_settings.py similarity index 100% rename from openfe/protocols/openmm_rfe/equil_rfe_settings.py rename to src/openfe/protocols/openmm_rfe/equil_rfe_settings.py diff --git a/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py b/src/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py similarity index 100% rename from openfe/protocols/openmm_rfe/hybridtop_protocol_results.py rename to src/openfe/protocols/openmm_rfe/hybridtop_protocol_results.py diff --git a/openfe/protocols/openmm_rfe/hybridtop_protocols.py b/src/openfe/protocols/openmm_rfe/hybridtop_protocols.py similarity index 100% rename from openfe/protocols/openmm_rfe/hybridtop_protocols.py rename to src/openfe/protocols/openmm_rfe/hybridtop_protocols.py diff --git a/openfe/protocols/openmm_rfe/hybridtop_units.py b/src/openfe/protocols/openmm_rfe/hybridtop_units.py similarity index 100% rename from openfe/protocols/openmm_rfe/hybridtop_units.py rename to src/openfe/protocols/openmm_rfe/hybridtop_units.py diff --git a/openfe/protocols/openmm_septop/__init__.py b/src/openfe/protocols/openmm_septop/__init__.py similarity index 100% rename from openfe/protocols/openmm_septop/__init__.py rename to src/openfe/protocols/openmm_septop/__init__.py diff --git a/openfe/protocols/openmm_septop/base.py b/src/openfe/protocols/openmm_septop/base.py similarity index 100% rename from openfe/protocols/openmm_septop/base.py rename to src/openfe/protocols/openmm_septop/base.py diff --git a/openfe/protocols/openmm_septop/equil_septop_method.py b/src/openfe/protocols/openmm_septop/equil_septop_method.py similarity index 100% rename from openfe/protocols/openmm_septop/equil_septop_method.py rename to src/openfe/protocols/openmm_septop/equil_septop_method.py diff --git a/openfe/protocols/openmm_septop/equil_septop_settings.py b/src/openfe/protocols/openmm_septop/equil_septop_settings.py similarity index 100% rename from openfe/protocols/openmm_septop/equil_septop_settings.py rename to src/openfe/protocols/openmm_septop/equil_septop_settings.py diff --git a/openfe/protocols/openmm_septop/utils.py b/src/openfe/protocols/openmm_septop/utils.py similarity index 100% rename from openfe/protocols/openmm_septop/utils.py rename to src/openfe/protocols/openmm_septop/utils.py diff --git a/openfe/protocols/openmm_utils/__init__.py b/src/openfe/protocols/openmm_utils/__init__.py similarity index 100% rename from openfe/protocols/openmm_utils/__init__.py rename to src/openfe/protocols/openmm_utils/__init__.py diff --git a/openfe/protocols/openmm_utils/charge_generation.py b/src/openfe/protocols/openmm_utils/charge_generation.py similarity index 100% rename from openfe/protocols/openmm_utils/charge_generation.py rename to src/openfe/protocols/openmm_utils/charge_generation.py diff --git a/openfe/protocols/openmm_utils/multistate_analysis.py b/src/openfe/protocols/openmm_utils/multistate_analysis.py similarity index 100% rename from openfe/protocols/openmm_utils/multistate_analysis.py rename to src/openfe/protocols/openmm_utils/multistate_analysis.py diff --git a/openfe/protocols/openmm_utils/omm_compute.py b/src/openfe/protocols/openmm_utils/omm_compute.py similarity index 100% rename from openfe/protocols/openmm_utils/omm_compute.py rename to src/openfe/protocols/openmm_utils/omm_compute.py diff --git a/openfe/protocols/openmm_utils/omm_settings.py b/src/openfe/protocols/openmm_utils/omm_settings.py similarity index 100% rename from openfe/protocols/openmm_utils/omm_settings.py rename to src/openfe/protocols/openmm_utils/omm_settings.py diff --git a/openfe/protocols/openmm_utils/serialization.py b/src/openfe/protocols/openmm_utils/serialization.py similarity index 100% rename from openfe/protocols/openmm_utils/serialization.py rename to src/openfe/protocols/openmm_utils/serialization.py diff --git a/openfe/protocols/openmm_utils/settings_validation.py b/src/openfe/protocols/openmm_utils/settings_validation.py similarity index 100% rename from openfe/protocols/openmm_utils/settings_validation.py rename to src/openfe/protocols/openmm_utils/settings_validation.py diff --git a/openfe/protocols/openmm_utils/system_creation.py b/src/openfe/protocols/openmm_utils/system_creation.py similarity index 100% rename from openfe/protocols/openmm_utils/system_creation.py rename to src/openfe/protocols/openmm_utils/system_creation.py diff --git a/openfe/protocols/openmm_utils/system_validation.py b/src/openfe/protocols/openmm_utils/system_validation.py similarity index 100% rename from openfe/protocols/openmm_utils/system_validation.py rename to src/openfe/protocols/openmm_utils/system_validation.py diff --git a/openfe/protocols/restraint_utils/__init__.py b/src/openfe/protocols/restraint_utils/__init__.py similarity index 100% rename from openfe/protocols/restraint_utils/__init__.py rename to src/openfe/protocols/restraint_utils/__init__.py diff --git a/openfe/protocols/restraint_utils/geometry/__init__.py b/src/openfe/protocols/restraint_utils/geometry/__init__.py similarity index 100% rename from openfe/protocols/restraint_utils/geometry/__init__.py rename to src/openfe/protocols/restraint_utils/geometry/__init__.py diff --git a/openfe/protocols/restraint_utils/geometry/base.py b/src/openfe/protocols/restraint_utils/geometry/base.py similarity index 100% rename from openfe/protocols/restraint_utils/geometry/base.py rename to src/openfe/protocols/restraint_utils/geometry/base.py diff --git a/openfe/protocols/restraint_utils/geometry/boresch/__init__.py b/src/openfe/protocols/restraint_utils/geometry/boresch/__init__.py similarity index 100% rename from openfe/protocols/restraint_utils/geometry/boresch/__init__.py rename to src/openfe/protocols/restraint_utils/geometry/boresch/__init__.py diff --git a/openfe/protocols/restraint_utils/geometry/boresch/geometry.py b/src/openfe/protocols/restraint_utils/geometry/boresch/geometry.py similarity index 100% rename from openfe/protocols/restraint_utils/geometry/boresch/geometry.py rename to src/openfe/protocols/restraint_utils/geometry/boresch/geometry.py diff --git a/openfe/protocols/restraint_utils/geometry/boresch/guest.py b/src/openfe/protocols/restraint_utils/geometry/boresch/guest.py similarity index 100% rename from openfe/protocols/restraint_utils/geometry/boresch/guest.py rename to src/openfe/protocols/restraint_utils/geometry/boresch/guest.py diff --git a/openfe/protocols/restraint_utils/geometry/boresch/host.py b/src/openfe/protocols/restraint_utils/geometry/boresch/host.py similarity index 100% rename from openfe/protocols/restraint_utils/geometry/boresch/host.py rename to src/openfe/protocols/restraint_utils/geometry/boresch/host.py diff --git a/openfe/protocols/restraint_utils/geometry/flatbottom.py b/src/openfe/protocols/restraint_utils/geometry/flatbottom.py similarity index 100% rename from openfe/protocols/restraint_utils/geometry/flatbottom.py rename to src/openfe/protocols/restraint_utils/geometry/flatbottom.py diff --git a/openfe/protocols/restraint_utils/geometry/harmonic.py b/src/openfe/protocols/restraint_utils/geometry/harmonic.py similarity index 100% rename from openfe/protocols/restraint_utils/geometry/harmonic.py rename to src/openfe/protocols/restraint_utils/geometry/harmonic.py diff --git a/openfe/protocols/restraint_utils/geometry/utils.py b/src/openfe/protocols/restraint_utils/geometry/utils.py similarity index 100% rename from openfe/protocols/restraint_utils/geometry/utils.py rename to src/openfe/protocols/restraint_utils/geometry/utils.py diff --git a/openfe/protocols/restraint_utils/openmm/__init__.py b/src/openfe/protocols/restraint_utils/openmm/__init__.py similarity index 100% rename from openfe/protocols/restraint_utils/openmm/__init__.py rename to src/openfe/protocols/restraint_utils/openmm/__init__.py diff --git a/openfe/protocols/restraint_utils/openmm/omm_forces.py b/src/openfe/protocols/restraint_utils/openmm/omm_forces.py similarity index 100% rename from openfe/protocols/restraint_utils/openmm/omm_forces.py rename to src/openfe/protocols/restraint_utils/openmm/omm_forces.py diff --git a/openfe/protocols/restraint_utils/openmm/omm_restraints.py b/src/openfe/protocols/restraint_utils/openmm/omm_restraints.py similarity index 100% rename from openfe/protocols/restraint_utils/openmm/omm_restraints.py rename to src/openfe/protocols/restraint_utils/openmm/omm_restraints.py diff --git a/openfe/protocols/restraint_utils/settings.py b/src/openfe/protocols/restraint_utils/settings.py similarity index 100% rename from openfe/protocols/restraint_utils/settings.py rename to src/openfe/protocols/restraint_utils/settings.py diff --git a/openfe/setup/__init__.py b/src/openfe/setup/__init__.py similarity index 100% rename from openfe/setup/__init__.py rename to src/openfe/setup/__init__.py diff --git a/openfe/setup/alchemical_network_planner/__init__.py b/src/openfe/setup/alchemical_network_planner/__init__.py similarity index 100% rename from openfe/setup/alchemical_network_planner/__init__.py rename to src/openfe/setup/alchemical_network_planner/__init__.py diff --git a/openfe/setup/alchemical_network_planner/abstract_alchemical_network_planner.py b/src/openfe/setup/alchemical_network_planner/abstract_alchemical_network_planner.py similarity index 100% rename from openfe/setup/alchemical_network_planner/abstract_alchemical_network_planner.py rename to src/openfe/setup/alchemical_network_planner/abstract_alchemical_network_planner.py diff --git a/openfe/setup/alchemical_network_planner/relative_alchemical_network_planner.py b/src/openfe/setup/alchemical_network_planner/relative_alchemical_network_planner.py similarity index 100% rename from openfe/setup/alchemical_network_planner/relative_alchemical_network_planner.py rename to src/openfe/setup/alchemical_network_planner/relative_alchemical_network_planner.py diff --git a/openfe/setup/atom_mapping/__init__.py b/src/openfe/setup/atom_mapping/__init__.py similarity index 100% rename from openfe/setup/atom_mapping/__init__.py rename to src/openfe/setup/atom_mapping/__init__.py diff --git a/openfe/setup/atom_mapping/ligandatommapper.py b/src/openfe/setup/atom_mapping/ligandatommapper.py similarity index 100% rename from openfe/setup/atom_mapping/ligandatommapper.py rename to src/openfe/setup/atom_mapping/ligandatommapper.py diff --git a/openfe/setup/atom_mapping/lomap_mapper.py b/src/openfe/setup/atom_mapping/lomap_mapper.py similarity index 100% rename from openfe/setup/atom_mapping/lomap_mapper.py rename to src/openfe/setup/atom_mapping/lomap_mapper.py diff --git a/openfe/setup/atom_mapping/lomap_scorers.py b/src/openfe/setup/atom_mapping/lomap_scorers.py similarity index 100% rename from openfe/setup/atom_mapping/lomap_scorers.py rename to src/openfe/setup/atom_mapping/lomap_scorers.py diff --git a/openfe/setup/atom_mapping/perses_mapper.py b/src/openfe/setup/atom_mapping/perses_mapper.py similarity index 100% rename from openfe/setup/atom_mapping/perses_mapper.py rename to src/openfe/setup/atom_mapping/perses_mapper.py diff --git a/openfe/setup/atom_mapping/perses_scorers.py b/src/openfe/setup/atom_mapping/perses_scorers.py similarity index 100% rename from openfe/setup/atom_mapping/perses_scorers.py rename to src/openfe/setup/atom_mapping/perses_scorers.py diff --git a/openfe/setup/chemicalsystem_generator/__init__.py b/src/openfe/setup/chemicalsystem_generator/__init__.py similarity index 100% rename from openfe/setup/chemicalsystem_generator/__init__.py rename to src/openfe/setup/chemicalsystem_generator/__init__.py diff --git a/openfe/setup/chemicalsystem_generator/abstract_chemicalsystem_generator.py b/src/openfe/setup/chemicalsystem_generator/abstract_chemicalsystem_generator.py similarity index 100% rename from openfe/setup/chemicalsystem_generator/abstract_chemicalsystem_generator.py rename to src/openfe/setup/chemicalsystem_generator/abstract_chemicalsystem_generator.py diff --git a/openfe/setup/chemicalsystem_generator/easy_chemicalsystem_generator.py b/src/openfe/setup/chemicalsystem_generator/easy_chemicalsystem_generator.py similarity index 100% rename from openfe/setup/chemicalsystem_generator/easy_chemicalsystem_generator.py rename to src/openfe/setup/chemicalsystem_generator/easy_chemicalsystem_generator.py diff --git a/openfe/setup/ligand_network_planning.py b/src/openfe/setup/ligand_network_planning.py similarity index 100% rename from openfe/setup/ligand_network_planning.py rename to src/openfe/setup/ligand_network_planning.py diff --git a/openfe/storage/__init__.py b/src/openfe/storage/__init__.py similarity index 100% rename from openfe/storage/__init__.py rename to src/openfe/storage/__init__.py diff --git a/openfe/storage/metadatastore.py b/src/openfe/storage/metadatastore.py similarity index 100% rename from openfe/storage/metadatastore.py rename to src/openfe/storage/metadatastore.py diff --git a/openfe/storage/resultclient.py b/src/openfe/storage/resultclient.py similarity index 100% rename from openfe/storage/resultclient.py rename to src/openfe/storage/resultclient.py diff --git a/openfe/storage/resultserver.py b/src/openfe/storage/resultserver.py similarity index 100% rename from openfe/storage/resultserver.py rename to src/openfe/storage/resultserver.py diff --git a/openfe/tests/__init__.py b/src/openfe/tests/__init__.py similarity index 100% rename from openfe/tests/__init__.py rename to src/openfe/tests/__init__.py diff --git a/openfe/tests/analysis/__init__.py b/src/openfe/tests/analysis/__init__.py similarity index 100% rename from openfe/tests/analysis/__init__.py rename to src/openfe/tests/analysis/__init__.py diff --git a/openfe/tests/analysis/test_plotting.py b/src/openfe/tests/analysis/test_plotting.py similarity index 100% rename from openfe/tests/analysis/test_plotting.py rename to src/openfe/tests/analysis/test_plotting.py diff --git a/openfe/tests/conftest.py b/src/openfe/tests/conftest.py similarity index 100% rename from openfe/tests/conftest.py rename to src/openfe/tests/conftest.py diff --git a/openfe/tests/data/181l_only.pdb b/src/openfe/tests/data/181l_only.pdb similarity index 100% rename from openfe/tests/data/181l_only.pdb rename to src/openfe/tests/data/181l_only.pdb diff --git a/openfe/tests/data/6CZJ.pdb.gz b/src/openfe/tests/data/6CZJ.pdb.gz similarity index 100% rename from openfe/tests/data/6CZJ.pdb.gz rename to src/openfe/tests/data/6CZJ.pdb.gz diff --git a/openfe/tests/data/CN.sdf b/src/openfe/tests/data/CN.sdf similarity index 100% rename from openfe/tests/data/CN.sdf rename to src/openfe/tests/data/CN.sdf diff --git a/openfe/tests/data/__init__.py b/src/openfe/tests/data/__init__.py similarity index 100% rename from openfe/tests/data/__init__.py rename to src/openfe/tests/data/__init__.py diff --git a/openfe/tests/data/benzene_modifications.sdf b/src/openfe/tests/data/benzene_modifications.sdf similarity index 100% rename from openfe/tests/data/benzene_modifications.sdf rename to src/openfe/tests/data/benzene_modifications.sdf diff --git a/openfe/tests/data/cdk8/__init__.py b/src/openfe/tests/data/cdk8/__init__.py similarity index 100% rename from openfe/tests/data/cdk8/__init__.py rename to src/openfe/tests/data/cdk8/__init__.py diff --git a/openfe/tests/data/cdk8/cdk8_ligands.sdf b/src/openfe/tests/data/cdk8/cdk8_ligands.sdf similarity index 100% rename from openfe/tests/data/cdk8/cdk8_ligands.sdf rename to src/openfe/tests/data/cdk8/cdk8_ligands.sdf diff --git a/openfe/tests/data/cdk8/cdk8_protein.pdb b/src/openfe/tests/data/cdk8/cdk8_protein.pdb similarity index 100% rename from openfe/tests/data/cdk8/cdk8_protein.pdb rename to src/openfe/tests/data/cdk8/cdk8_protein.pdb diff --git a/openfe/tests/data/eg5/__init__.py b/src/openfe/tests/data/eg5/__init__.py similarity index 100% rename from openfe/tests/data/eg5/__init__.py rename to src/openfe/tests/data/eg5/__init__.py diff --git a/openfe/tests/data/eg5/eg5_cofactor.sdf b/src/openfe/tests/data/eg5/eg5_cofactor.sdf similarity index 100% rename from openfe/tests/data/eg5/eg5_cofactor.sdf rename to src/openfe/tests/data/eg5/eg5_cofactor.sdf diff --git a/openfe/tests/data/eg5/eg5_ligands.sdf b/src/openfe/tests/data/eg5/eg5_ligands.sdf similarity index 100% rename from openfe/tests/data/eg5/eg5_ligands.sdf rename to src/openfe/tests/data/eg5/eg5_ligands.sdf diff --git a/openfe/tests/data/eg5/eg5_protein.pdb b/src/openfe/tests/data/eg5/eg5_protein.pdb similarity index 100% rename from openfe/tests/data/eg5/eg5_protein.pdb rename to src/openfe/tests/data/eg5/eg5_protein.pdb diff --git a/openfe/tests/data/external_formats/__init__.py b/src/openfe/tests/data/external_formats/__init__.py similarity index 100% rename from openfe/tests/data/external_formats/__init__.py rename to src/openfe/tests/data/external_formats/__init__.py diff --git a/openfe/tests/data/external_formats/somebenzenes_edges.edge b/src/openfe/tests/data/external_formats/somebenzenes_edges.edge similarity index 100% rename from openfe/tests/data/external_formats/somebenzenes_edges.edge rename to src/openfe/tests/data/external_formats/somebenzenes_edges.edge diff --git a/openfe/tests/data/external_formats/somebenzenes_nes.dat b/src/openfe/tests/data/external_formats/somebenzenes_nes.dat similarity index 100% rename from openfe/tests/data/external_formats/somebenzenes_nes.dat rename to src/openfe/tests/data/external_formats/somebenzenes_nes.dat diff --git a/openfe/tests/data/htf/__init__.py b/src/openfe/tests/data/htf/__init__.py similarity index 100% rename from openfe/tests/data/htf/__init__.py rename to src/openfe/tests/data/htf/__init__.py diff --git a/openfe/tests/data/htf/t4_lysozyme_data/chlorobenzene.sdf b/src/openfe/tests/data/htf/t4_lysozyme_data/chlorobenzene.sdf similarity index 100% rename from openfe/tests/data/htf/t4_lysozyme_data/chlorobenzene.sdf rename to src/openfe/tests/data/htf/t4_lysozyme_data/chlorobenzene.sdf diff --git a/openfe/tests/data/htf/t4_lysozyme_data/fluorobenzene.sdf b/src/openfe/tests/data/htf/t4_lysozyme_data/fluorobenzene.sdf similarity index 100% rename from openfe/tests/data/htf/t4_lysozyme_data/fluorobenzene.sdf rename to src/openfe/tests/data/htf/t4_lysozyme_data/fluorobenzene.sdf diff --git a/openfe/tests/data/htf/t4_lysozyme_data/t4_lysozyme_solvated.pdb.gz b/src/openfe/tests/data/htf/t4_lysozyme_data/t4_lysozyme_solvated.pdb.gz similarity index 100% rename from openfe/tests/data/htf/t4_lysozyme_data/t4_lysozyme_solvated.pdb.gz rename to src/openfe/tests/data/htf/t4_lysozyme_data/t4_lysozyme_solvated.pdb.gz diff --git a/openfe/tests/data/lomap_basic/1,3,7-trimethylnaphthalene.mol2 b/src/openfe/tests/data/lomap_basic/1,3,7-trimethylnaphthalene.mol2 similarity index 100% rename from openfe/tests/data/lomap_basic/1,3,7-trimethylnaphthalene.mol2 rename to src/openfe/tests/data/lomap_basic/1,3,7-trimethylnaphthalene.mol2 diff --git a/openfe/tests/data/lomap_basic/1-butyl-4-methylbenzene.mol2 b/src/openfe/tests/data/lomap_basic/1-butyl-4-methylbenzene.mol2 similarity index 100% rename from openfe/tests/data/lomap_basic/1-butyl-4-methylbenzene.mol2 rename to src/openfe/tests/data/lomap_basic/1-butyl-4-methylbenzene.mol2 diff --git a/openfe/tests/data/lomap_basic/2,6-dimethylnaphthalene.mol2 b/src/openfe/tests/data/lomap_basic/2,6-dimethylnaphthalene.mol2 similarity index 100% rename from openfe/tests/data/lomap_basic/2,6-dimethylnaphthalene.mol2 rename to src/openfe/tests/data/lomap_basic/2,6-dimethylnaphthalene.mol2 diff --git a/openfe/tests/data/lomap_basic/2-methyl-6-propylnaphthalene.mol2 b/src/openfe/tests/data/lomap_basic/2-methyl-6-propylnaphthalene.mol2 similarity index 100% rename from openfe/tests/data/lomap_basic/2-methyl-6-propylnaphthalene.mol2 rename to src/openfe/tests/data/lomap_basic/2-methyl-6-propylnaphthalene.mol2 diff --git a/openfe/tests/data/lomap_basic/2-methylnaphthalene.mol2 b/src/openfe/tests/data/lomap_basic/2-methylnaphthalene.mol2 similarity index 100% rename from openfe/tests/data/lomap_basic/2-methylnaphthalene.mol2 rename to src/openfe/tests/data/lomap_basic/2-methylnaphthalene.mol2 diff --git a/openfe/tests/data/lomap_basic/2-naftanol.mol2 b/src/openfe/tests/data/lomap_basic/2-naftanol.mol2 similarity index 100% rename from openfe/tests/data/lomap_basic/2-naftanol.mol2 rename to src/openfe/tests/data/lomap_basic/2-naftanol.mol2 diff --git a/openfe/tests/data/lomap_basic/README.md b/src/openfe/tests/data/lomap_basic/README.md similarity index 100% rename from openfe/tests/data/lomap_basic/README.md rename to src/openfe/tests/data/lomap_basic/README.md diff --git a/openfe/tests/data/lomap_basic/__init__.py b/src/openfe/tests/data/lomap_basic/__init__.py similarity index 100% rename from openfe/tests/data/lomap_basic/__init__.py rename to src/openfe/tests/data/lomap_basic/__init__.py diff --git a/openfe/tests/data/lomap_basic/methylcyclohexane.mol2 b/src/openfe/tests/data/lomap_basic/methylcyclohexane.mol2 similarity index 100% rename from openfe/tests/data/lomap_basic/methylcyclohexane.mol2 rename to src/openfe/tests/data/lomap_basic/methylcyclohexane.mol2 diff --git a/openfe/tests/data/lomap_basic/toluene.mol2 b/src/openfe/tests/data/lomap_basic/toluene.mol2 similarity index 100% rename from openfe/tests/data/lomap_basic/toluene.mol2 rename to src/openfe/tests/data/lomap_basic/toluene.mol2 diff --git a/openfe/tests/data/multi_molecule.sdf b/src/openfe/tests/data/multi_molecule.sdf similarity index 100% rename from openfe/tests/data/multi_molecule.sdf rename to src/openfe/tests/data/multi_molecule.sdf diff --git a/openfe/tests/data/openmm_afe/ABFEProtocol_json_results.json.gz b/src/openfe/tests/data/openmm_afe/ABFEProtocol_json_results.json.gz similarity index 100% rename from openfe/tests/data/openmm_afe/ABFEProtocol_json_results.json.gz rename to src/openfe/tests/data/openmm_afe/ABFEProtocol_json_results.json.gz diff --git a/openfe/tests/data/openmm_afe/AHFEProtocol_json_results.gz b/src/openfe/tests/data/openmm_afe/AHFEProtocol_json_results.gz similarity index 100% rename from openfe/tests/data/openmm_afe/AHFEProtocol_json_results.gz rename to src/openfe/tests/data/openmm_afe/AHFEProtocol_json_results.gz diff --git a/openfe/tests/data/openmm_afe/T4_abfe_system.xml.bz2 b/src/openfe/tests/data/openmm_afe/T4_abfe_system.xml.bz2 similarity index 100% rename from openfe/tests/data/openmm_afe/T4_abfe_system.xml.bz2 rename to src/openfe/tests/data/openmm_afe/T4_abfe_system.xml.bz2 diff --git a/openfe/tests/data/openmm_afe/__init__.py b/src/openfe/tests/data/openmm_afe/__init__.py similarity index 100% rename from openfe/tests/data/openmm_afe/__init__.py rename to src/openfe/tests/data/openmm_afe/__init__.py diff --git a/openfe/tests/data/openmm_md/MDProtocol_json_results.gz b/src/openfe/tests/data/openmm_md/MDProtocol_json_results.gz similarity index 100% rename from openfe/tests/data/openmm_md/MDProtocol_json_results.gz rename to src/openfe/tests/data/openmm_md/MDProtocol_json_results.gz diff --git a/openfe/tests/data/openmm_md/__init__.py b/src/openfe/tests/data/openmm_md/__init__.py similarity index 100% rename from openfe/tests/data/openmm_md/__init__.py rename to src/openfe/tests/data/openmm_md/__init__.py diff --git a/openfe/tests/data/openmm_rfe/RHFEProtocol_json_results.gz b/src/openfe/tests/data/openmm_rfe/RHFEProtocol_json_results.gz similarity index 100% rename from openfe/tests/data/openmm_rfe/RHFEProtocol_json_results.gz rename to src/openfe/tests/data/openmm_rfe/RHFEProtocol_json_results.gz diff --git a/openfe/tests/data/openmm_rfe/__init__.py b/src/openfe/tests/data/openmm_rfe/__init__.py similarity index 100% rename from openfe/tests/data/openmm_rfe/__init__.py rename to src/openfe/tests/data/openmm_rfe/__init__.py diff --git a/openfe/tests/data/openmm_rfe/benzene_toluene_hybrid_top/hybrid_topology_atoms.csv b/src/openfe/tests/data/openmm_rfe/benzene_toluene_hybrid_top/hybrid_topology_atoms.csv similarity index 100% rename from openfe/tests/data/openmm_rfe/benzene_toluene_hybrid_top/hybrid_topology_atoms.csv rename to src/openfe/tests/data/openmm_rfe/benzene_toluene_hybrid_top/hybrid_topology_atoms.csv diff --git a/openfe/tests/data/openmm_rfe/benzene_toluene_hybrid_top/hybrid_topology_bonds.txt b/src/openfe/tests/data/openmm_rfe/benzene_toluene_hybrid_top/hybrid_topology_bonds.txt similarity index 100% rename from openfe/tests/data/openmm_rfe/benzene_toluene_hybrid_top/hybrid_topology_bonds.txt rename to src/openfe/tests/data/openmm_rfe/benzene_toluene_hybrid_top/hybrid_topology_bonds.txt diff --git a/openfe/tests/data/openmm_rfe/charged_benzenes.sdf b/src/openfe/tests/data/openmm_rfe/charged_benzenes.sdf similarity index 100% rename from openfe/tests/data/openmm_rfe/charged_benzenes.sdf rename to src/openfe/tests/data/openmm_rfe/charged_benzenes.sdf diff --git a/openfe/tests/data/openmm_rfe/dummy_charge_ligand_23.sdf b/src/openfe/tests/data/openmm_rfe/dummy_charge_ligand_23.sdf similarity index 100% rename from openfe/tests/data/openmm_rfe/dummy_charge_ligand_23.sdf rename to src/openfe/tests/data/openmm_rfe/dummy_charge_ligand_23.sdf diff --git a/openfe/tests/data/openmm_rfe/dummy_charge_ligand_55.sdf b/src/openfe/tests/data/openmm_rfe/dummy_charge_ligand_55.sdf similarity index 100% rename from openfe/tests/data/openmm_rfe/dummy_charge_ligand_55.sdf rename to src/openfe/tests/data/openmm_rfe/dummy_charge_ligand_55.sdf diff --git a/openfe/tests/data/openmm_rfe/ligand_23.sdf b/src/openfe/tests/data/openmm_rfe/ligand_23.sdf similarity index 100% rename from openfe/tests/data/openmm_rfe/ligand_23.sdf rename to src/openfe/tests/data/openmm_rfe/ligand_23.sdf diff --git a/openfe/tests/data/openmm_rfe/ligand_55.sdf b/src/openfe/tests/data/openmm_rfe/ligand_55.sdf similarity index 100% rename from openfe/tests/data/openmm_rfe/ligand_55.sdf rename to src/openfe/tests/data/openmm_rfe/ligand_55.sdf diff --git a/openfe/tests/data/openmm_rfe/malt1_shapefit_1832577-09-9.sdf b/src/openfe/tests/data/openmm_rfe/malt1_shapefit_1832577-09-9.sdf similarity index 100% rename from openfe/tests/data/openmm_rfe/malt1_shapefit_1832577-09-9.sdf rename to src/openfe/tests/data/openmm_rfe/malt1_shapefit_1832577-09-9.sdf diff --git a/openfe/tests/data/openmm_rfe/malt1_shapefit_Pfizer-01-01.sdf b/src/openfe/tests/data/openmm_rfe/malt1_shapefit_Pfizer-01-01.sdf similarity index 100% rename from openfe/tests/data/openmm_rfe/malt1_shapefit_Pfizer-01-01.sdf rename to src/openfe/tests/data/openmm_rfe/malt1_shapefit_Pfizer-01-01.sdf diff --git a/openfe/tests/data/openmm_rfe/reference.xml b/src/openfe/tests/data/openmm_rfe/reference.xml similarity index 100% rename from openfe/tests/data/openmm_rfe/reference.xml rename to src/openfe/tests/data/openmm_rfe/reference.xml diff --git a/openfe/tests/data/openmm_rfe/vacuum_nocoord.nc b/src/openfe/tests/data/openmm_rfe/vacuum_nocoord.nc similarity index 100% rename from openfe/tests/data/openmm_rfe/vacuum_nocoord.nc rename to src/openfe/tests/data/openmm_rfe/vacuum_nocoord.nc diff --git a/openfe/tests/data/openmm_rfe/vacuum_nocoord_checkpoint.nc b/src/openfe/tests/data/openmm_rfe/vacuum_nocoord_checkpoint.nc similarity index 100% rename from openfe/tests/data/openmm_rfe/vacuum_nocoord_checkpoint.nc rename to src/openfe/tests/data/openmm_rfe/vacuum_nocoord_checkpoint.nc diff --git a/openfe/tests/data/openmm_septop/SepTopProtocol_json_results.gz b/src/openfe/tests/data/openmm_septop/SepTopProtocol_json_results.gz similarity index 100% rename from openfe/tests/data/openmm_septop/SepTopProtocol_json_results.gz rename to src/openfe/tests/data/openmm_septop/SepTopProtocol_json_results.gz diff --git a/openfe/tests/data/openmm_septop/__init__.py b/src/openfe/tests/data/openmm_septop/__init__.py similarity index 100% rename from openfe/tests/data/openmm_septop/__init__.py rename to src/openfe/tests/data/openmm_septop/__init__.py diff --git a/openfe/tests/data/openmm_septop/system.xml.bz2 b/src/openfe/tests/data/openmm_septop/system.xml.bz2 similarity index 100% rename from openfe/tests/data/openmm_septop/system.xml.bz2 rename to src/openfe/tests/data/openmm_septop/system.xml.bz2 diff --git a/openfe/tests/data/serialization/__init__.py b/src/openfe/tests/data/serialization/__init__.py similarity index 100% rename from openfe/tests/data/serialization/__init__.py rename to src/openfe/tests/data/serialization/__init__.py diff --git a/openfe/tests/data/serialization/ethane_template.sdf b/src/openfe/tests/data/serialization/ethane_template.sdf similarity index 100% rename from openfe/tests/data/serialization/ethane_template.sdf rename to src/openfe/tests/data/serialization/ethane_template.sdf diff --git a/openfe/tests/data/serialization/network_template.graphml b/src/openfe/tests/data/serialization/network_template.graphml similarity index 100% rename from openfe/tests/data/serialization/network_template.graphml rename to src/openfe/tests/data/serialization/network_template.graphml diff --git a/openfe/tests/dev/__init__.py b/src/openfe/tests/dev/__init__.py similarity index 100% rename from openfe/tests/dev/__init__.py rename to src/openfe/tests/dev/__init__.py diff --git a/openfe/tests/dev/serialization_test_templates.py b/src/openfe/tests/dev/serialization_test_templates.py similarity index 100% rename from openfe/tests/dev/serialization_test_templates.py rename to src/openfe/tests/dev/serialization_test_templates.py diff --git a/openfe/tests/protocols/__init__.py b/src/openfe/tests/protocols/__init__.py similarity index 100% rename from openfe/tests/protocols/__init__.py rename to src/openfe/tests/protocols/__init__.py diff --git a/openfe/tests/protocols/conftest.py b/src/openfe/tests/protocols/conftest.py similarity index 100% rename from openfe/tests/protocols/conftest.py rename to src/openfe/tests/protocols/conftest.py diff --git a/openfe/tests/protocols/openmm_abfe/__init__.py b/src/openfe/tests/protocols/openmm_abfe/__init__.py similarity index 100% rename from openfe/tests/protocols/openmm_abfe/__init__.py rename to src/openfe/tests/protocols/openmm_abfe/__init__.py diff --git a/openfe/tests/protocols/openmm_abfe/conftest.py b/src/openfe/tests/protocols/openmm_abfe/conftest.py similarity index 100% rename from openfe/tests/protocols/openmm_abfe/conftest.py rename to src/openfe/tests/protocols/openmm_abfe/conftest.py diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_energies.py b/src/openfe/tests/protocols/openmm_abfe/test_abfe_energies.py similarity index 100% rename from openfe/tests/protocols/openmm_abfe/test_abfe_energies.py rename to src/openfe/tests/protocols/openmm_abfe/test_abfe_energies.py diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py b/src/openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py similarity index 100% rename from openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py rename to src/openfe/tests/protocols/openmm_abfe/test_abfe_protocol.py diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_protocol_results.py b/src/openfe/tests/protocols/openmm_abfe/test_abfe_protocol_results.py similarity index 100% rename from openfe/tests/protocols/openmm_abfe/test_abfe_protocol_results.py rename to src/openfe/tests/protocols/openmm_abfe/test_abfe_protocol_results.py diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_settings.py b/src/openfe/tests/protocols/openmm_abfe/test_abfe_settings.py similarity index 100% rename from openfe/tests/protocols/openmm_abfe/test_abfe_settings.py rename to src/openfe/tests/protocols/openmm_abfe/test_abfe_settings.py diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_slow.py b/src/openfe/tests/protocols/openmm_abfe/test_abfe_slow.py similarity index 100% rename from openfe/tests/protocols/openmm_abfe/test_abfe_slow.py rename to src/openfe/tests/protocols/openmm_abfe/test_abfe_slow.py diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_tokenization.py b/src/openfe/tests/protocols/openmm_abfe/test_abfe_tokenization.py similarity index 100% rename from openfe/tests/protocols/openmm_abfe/test_abfe_tokenization.py rename to src/openfe/tests/protocols/openmm_abfe/test_abfe_tokenization.py diff --git a/openfe/tests/protocols/openmm_abfe/test_abfe_validation.py b/src/openfe/tests/protocols/openmm_abfe/test_abfe_validation.py similarity index 100% rename from openfe/tests/protocols/openmm_abfe/test_abfe_validation.py rename to src/openfe/tests/protocols/openmm_abfe/test_abfe_validation.py diff --git a/openfe/tests/protocols/openmm_abfe/utils.py b/src/openfe/tests/protocols/openmm_abfe/utils.py similarity index 100% rename from openfe/tests/protocols/openmm_abfe/utils.py rename to src/openfe/tests/protocols/openmm_abfe/utils.py diff --git a/openfe/tests/protocols/openmm_ahfe/__init__.py b/src/openfe/tests/protocols/openmm_ahfe/__init__.py similarity index 100% rename from openfe/tests/protocols/openmm_ahfe/__init__.py rename to src/openfe/tests/protocols/openmm_ahfe/__init__.py diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py b/src/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py similarity index 100% rename from openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py rename to src/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol_results.py b/src/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol_results.py similarity index 100% rename from openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol_results.py rename to src/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol_results.py diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_settings.py b/src/openfe/tests/protocols/openmm_ahfe/test_ahfe_settings.py similarity index 100% rename from openfe/tests/protocols/openmm_ahfe/test_ahfe_settings.py rename to src/openfe/tests/protocols/openmm_ahfe/test_ahfe_settings.py diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_slow.py b/src/openfe/tests/protocols/openmm_ahfe/test_ahfe_slow.py similarity index 100% rename from openfe/tests/protocols/openmm_ahfe/test_ahfe_slow.py rename to src/openfe/tests/protocols/openmm_ahfe/test_ahfe_slow.py diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_tokenization.py b/src/openfe/tests/protocols/openmm_ahfe/test_ahfe_tokenization.py similarity index 100% rename from openfe/tests/protocols/openmm_ahfe/test_ahfe_tokenization.py rename to src/openfe/tests/protocols/openmm_ahfe/test_ahfe_tokenization.py diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_validation.py b/src/openfe/tests/protocols/openmm_ahfe/test_ahfe_validation.py similarity index 100% rename from openfe/tests/protocols/openmm_ahfe/test_ahfe_validation.py rename to src/openfe/tests/protocols/openmm_ahfe/test_ahfe_validation.py diff --git a/openfe/tests/protocols/openmm_ahfe/utils.py b/src/openfe/tests/protocols/openmm_ahfe/utils.py similarity index 100% rename from openfe/tests/protocols/openmm_ahfe/utils.py rename to src/openfe/tests/protocols/openmm_ahfe/utils.py diff --git a/openfe/tests/protocols/openmm_md/__init__.py b/src/openfe/tests/protocols/openmm_md/__init__.py similarity index 100% rename from openfe/tests/protocols/openmm_md/__init__.py rename to src/openfe/tests/protocols/openmm_md/__init__.py diff --git a/openfe/tests/protocols/openmm_md/test_plain_md_protocol.py b/src/openfe/tests/protocols/openmm_md/test_plain_md_protocol.py similarity index 100% rename from openfe/tests/protocols/openmm_md/test_plain_md_protocol.py rename to src/openfe/tests/protocols/openmm_md/test_plain_md_protocol.py diff --git a/openfe/tests/protocols/openmm_md/test_plain_md_slow.py b/src/openfe/tests/protocols/openmm_md/test_plain_md_slow.py similarity index 100% rename from openfe/tests/protocols/openmm_md/test_plain_md_slow.py rename to src/openfe/tests/protocols/openmm_md/test_plain_md_slow.py diff --git a/openfe/tests/protocols/openmm_md/test_plain_md_tokenization.py b/src/openfe/tests/protocols/openmm_md/test_plain_md_tokenization.py similarity index 100% rename from openfe/tests/protocols/openmm_md/test_plain_md_tokenization.py rename to src/openfe/tests/protocols/openmm_md/test_plain_md_tokenization.py diff --git a/openfe/tests/protocols/openmm_rfe/__init__.py b/src/openfe/tests/protocols/openmm_rfe/__init__.py similarity index 100% rename from openfe/tests/protocols/openmm_rfe/__init__.py rename to src/openfe/tests/protocols/openmm_rfe/__init__.py diff --git a/openfe/tests/protocols/openmm_rfe/helpers.py b/src/openfe/tests/protocols/openmm_rfe/helpers.py similarity index 100% rename from openfe/tests/protocols/openmm_rfe/helpers.py rename to src/openfe/tests/protocols/openmm_rfe/helpers.py diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_factory.py b/src/openfe/tests/protocols/openmm_rfe/test_hybrid_factory.py similarity index 100% rename from openfe/tests/protocols/openmm_rfe/test_hybrid_factory.py rename to src/openfe/tests/protocols/openmm_rfe/test_hybrid_factory.py diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py b/src/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py similarity index 100% rename from openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py rename to src/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py b/src/openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py similarity index 100% rename from openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py rename to src/openfe/tests/protocols/openmm_rfe/test_hybrid_top_slow.py diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_tokenization.py b/src/openfe/tests/protocols/openmm_rfe/test_hybrid_top_tokenization.py similarity index 100% rename from openfe/tests/protocols/openmm_rfe/test_hybrid_top_tokenization.py rename to src/openfe/tests/protocols/openmm_rfe/test_hybrid_top_tokenization.py diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py b/src/openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py similarity index 100% rename from openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py rename to src/openfe/tests/protocols/openmm_rfe/test_hybrid_top_validation.py diff --git a/openfe/tests/protocols/openmm_septop/__init__.py b/src/openfe/tests/protocols/openmm_septop/__init__.py similarity index 100% rename from openfe/tests/protocols/openmm_septop/__init__.py rename to src/openfe/tests/protocols/openmm_septop/__init__.py diff --git a/openfe/tests/protocols/openmm_septop/test_septop_protocol.py b/src/openfe/tests/protocols/openmm_septop/test_septop_protocol.py similarity index 100% rename from openfe/tests/protocols/openmm_septop/test_septop_protocol.py rename to src/openfe/tests/protocols/openmm_septop/test_septop_protocol.py diff --git a/openfe/tests/protocols/openmm_septop/test_septop_slow.py b/src/openfe/tests/protocols/openmm_septop/test_septop_slow.py similarity index 100% rename from openfe/tests/protocols/openmm_septop/test_septop_slow.py rename to src/openfe/tests/protocols/openmm_septop/test_septop_slow.py diff --git a/openfe/tests/protocols/openmm_septop/test_septop_tokenization.py b/src/openfe/tests/protocols/openmm_septop/test_septop_tokenization.py similarity index 100% rename from openfe/tests/protocols/openmm_septop/test_septop_tokenization.py rename to src/openfe/tests/protocols/openmm_septop/test_septop_tokenization.py diff --git a/openfe/tests/protocols/restraints/__init__.py b/src/openfe/tests/protocols/restraints/__init__.py similarity index 100% rename from openfe/tests/protocols/restraints/__init__.py rename to src/openfe/tests/protocols/restraints/__init__.py diff --git a/openfe/tests/protocols/restraints/test_geometry_base.py b/src/openfe/tests/protocols/restraints/test_geometry_base.py similarity index 100% rename from openfe/tests/protocols/restraints/test_geometry_base.py rename to src/openfe/tests/protocols/restraints/test_geometry_base.py diff --git a/openfe/tests/protocols/restraints/test_geometry_boresch.py b/src/openfe/tests/protocols/restraints/test_geometry_boresch.py similarity index 100% rename from openfe/tests/protocols/restraints/test_geometry_boresch.py rename to src/openfe/tests/protocols/restraints/test_geometry_boresch.py diff --git a/openfe/tests/protocols/restraints/test_geometry_boresch_guest.py b/src/openfe/tests/protocols/restraints/test_geometry_boresch_guest.py similarity index 100% rename from openfe/tests/protocols/restraints/test_geometry_boresch_guest.py rename to src/openfe/tests/protocols/restraints/test_geometry_boresch_guest.py diff --git a/openfe/tests/protocols/restraints/test_geometry_boresch_host.py b/src/openfe/tests/protocols/restraints/test_geometry_boresch_host.py similarity index 100% rename from openfe/tests/protocols/restraints/test_geometry_boresch_host.py rename to src/openfe/tests/protocols/restraints/test_geometry_boresch_host.py diff --git a/openfe/tests/protocols/restraints/test_geometry_flatbottom.py b/src/openfe/tests/protocols/restraints/test_geometry_flatbottom.py similarity index 100% rename from openfe/tests/protocols/restraints/test_geometry_flatbottom.py rename to src/openfe/tests/protocols/restraints/test_geometry_flatbottom.py diff --git a/openfe/tests/protocols/restraints/test_geometry_harmonic.py b/src/openfe/tests/protocols/restraints/test_geometry_harmonic.py similarity index 100% rename from openfe/tests/protocols/restraints/test_geometry_harmonic.py rename to src/openfe/tests/protocols/restraints/test_geometry_harmonic.py diff --git a/openfe/tests/protocols/restraints/test_geometry_utils.py b/src/openfe/tests/protocols/restraints/test_geometry_utils.py similarity index 100% rename from openfe/tests/protocols/restraints/test_geometry_utils.py rename to src/openfe/tests/protocols/restraints/test_geometry_utils.py diff --git a/openfe/tests/protocols/restraints/test_omm_restraints.py b/src/openfe/tests/protocols/restraints/test_omm_restraints.py similarity index 100% rename from openfe/tests/protocols/restraints/test_omm_restraints.py rename to src/openfe/tests/protocols/restraints/test_omm_restraints.py diff --git a/openfe/tests/protocols/restraints/test_openmm_forces.py b/src/openfe/tests/protocols/restraints/test_openmm_forces.py similarity index 100% rename from openfe/tests/protocols/restraints/test_openmm_forces.py rename to src/openfe/tests/protocols/restraints/test_openmm_forces.py diff --git a/openfe/tests/protocols/restraints/test_settings.py b/src/openfe/tests/protocols/restraints/test_settings.py similarity index 100% rename from openfe/tests/protocols/restraints/test_settings.py rename to src/openfe/tests/protocols/restraints/test_settings.py diff --git a/openfe/tests/protocols/test_openmm_settings.py b/src/openfe/tests/protocols/test_openmm_settings.py similarity index 100% rename from openfe/tests/protocols/test_openmm_settings.py rename to src/openfe/tests/protocols/test_openmm_settings.py diff --git a/openfe/tests/protocols/test_openmmutils.py b/src/openfe/tests/protocols/test_openmmutils.py similarity index 100% rename from openfe/tests/protocols/test_openmmutils.py rename to src/openfe/tests/protocols/test_openmmutils.py diff --git a/openfe/tests/protocols/test_openmmutils_serialization.py b/src/openfe/tests/protocols/test_openmmutils_serialization.py similarity index 100% rename from openfe/tests/protocols/test_openmmutils_serialization.py rename to src/openfe/tests/protocols/test_openmmutils_serialization.py diff --git a/openfe/tests/setup/__init__.py b/src/openfe/tests/setup/__init__.py similarity index 100% rename from openfe/tests/setup/__init__.py rename to src/openfe/tests/setup/__init__.py diff --git a/openfe/tests/setup/alchemical_network_planner/__init__.py b/src/openfe/tests/setup/alchemical_network_planner/__init__.py similarity index 100% rename from openfe/tests/setup/alchemical_network_planner/__init__.py rename to src/openfe/tests/setup/alchemical_network_planner/__init__.py diff --git a/openfe/tests/setup/alchemical_network_planner/edge_types.py b/src/openfe/tests/setup/alchemical_network_planner/edge_types.py similarity index 100% rename from openfe/tests/setup/alchemical_network_planner/edge_types.py rename to src/openfe/tests/setup/alchemical_network_planner/edge_types.py diff --git a/openfe/tests/setup/alchemical_network_planner/test_relative_alchemical_network_planner.py b/src/openfe/tests/setup/alchemical_network_planner/test_relative_alchemical_network_planner.py similarity index 100% rename from openfe/tests/setup/alchemical_network_planner/test_relative_alchemical_network_planner.py rename to src/openfe/tests/setup/alchemical_network_planner/test_relative_alchemical_network_planner.py diff --git a/openfe/tests/setup/atom_mapping/__init__.py b/src/openfe/tests/setup/atom_mapping/__init__.py similarity index 100% rename from openfe/tests/setup/atom_mapping/__init__.py rename to src/openfe/tests/setup/atom_mapping/__init__.py diff --git a/openfe/tests/setup/atom_mapping/conftest.py b/src/openfe/tests/setup/atom_mapping/conftest.py similarity index 100% rename from openfe/tests/setup/atom_mapping/conftest.py rename to src/openfe/tests/setup/atom_mapping/conftest.py diff --git a/openfe/tests/setup/atom_mapping/test_atommapper.py b/src/openfe/tests/setup/atom_mapping/test_atommapper.py similarity index 100% rename from openfe/tests/setup/atom_mapping/test_atommapper.py rename to src/openfe/tests/setup/atom_mapping/test_atommapper.py diff --git a/openfe/tests/setup/atom_mapping/test_lomap_atommapper.py b/src/openfe/tests/setup/atom_mapping/test_lomap_atommapper.py similarity index 100% rename from openfe/tests/setup/atom_mapping/test_lomap_atommapper.py rename to src/openfe/tests/setup/atom_mapping/test_lomap_atommapper.py diff --git a/openfe/tests/setup/atom_mapping/test_lomap_scorers.py b/src/openfe/tests/setup/atom_mapping/test_lomap_scorers.py similarity index 100% rename from openfe/tests/setup/atom_mapping/test_lomap_scorers.py rename to src/openfe/tests/setup/atom_mapping/test_lomap_scorers.py diff --git a/openfe/tests/setup/atom_mapping/test_perses_atommapper.py b/src/openfe/tests/setup/atom_mapping/test_perses_atommapper.py similarity index 100% rename from openfe/tests/setup/atom_mapping/test_perses_atommapper.py rename to src/openfe/tests/setup/atom_mapping/test_perses_atommapper.py diff --git a/openfe/tests/setup/atom_mapping/test_perses_scorers.py b/src/openfe/tests/setup/atom_mapping/test_perses_scorers.py similarity index 100% rename from openfe/tests/setup/atom_mapping/test_perses_scorers.py rename to src/openfe/tests/setup/atom_mapping/test_perses_scorers.py diff --git a/openfe/tests/setup/chemicalsystem_generator/__init__.py b/src/openfe/tests/setup/chemicalsystem_generator/__init__.py similarity index 100% rename from openfe/tests/setup/chemicalsystem_generator/__init__.py rename to src/openfe/tests/setup/chemicalsystem_generator/__init__.py diff --git a/openfe/tests/setup/chemicalsystem_generator/component_checks.py b/src/openfe/tests/setup/chemicalsystem_generator/component_checks.py similarity index 100% rename from openfe/tests/setup/chemicalsystem_generator/component_checks.py rename to src/openfe/tests/setup/chemicalsystem_generator/component_checks.py diff --git a/openfe/tests/setup/chemicalsystem_generator/test_easy_chemicalsystem_generator.py b/src/openfe/tests/setup/chemicalsystem_generator/test_easy_chemicalsystem_generator.py similarity index 100% rename from openfe/tests/setup/chemicalsystem_generator/test_easy_chemicalsystem_generator.py rename to src/openfe/tests/setup/chemicalsystem_generator/test_easy_chemicalsystem_generator.py diff --git a/openfe/tests/setup/test_network_planning.py b/src/openfe/tests/setup/test_network_planning.py similarity index 100% rename from openfe/tests/setup/test_network_planning.py rename to src/openfe/tests/setup/test_network_planning.py diff --git a/openfe/tests/storage/__init__.py b/src/openfe/tests/storage/__init__.py similarity index 100% rename from openfe/tests/storage/__init__.py rename to src/openfe/tests/storage/__init__.py diff --git a/openfe/tests/storage/conftest.py b/src/openfe/tests/storage/conftest.py similarity index 100% rename from openfe/tests/storage/conftest.py rename to src/openfe/tests/storage/conftest.py diff --git a/openfe/tests/storage/test_metadatastore.py b/src/openfe/tests/storage/test_metadatastore.py similarity index 100% rename from openfe/tests/storage/test_metadatastore.py rename to src/openfe/tests/storage/test_metadatastore.py diff --git a/openfe/tests/storage/test_resultclient.py b/src/openfe/tests/storage/test_resultclient.py similarity index 100% rename from openfe/tests/storage/test_resultclient.py rename to src/openfe/tests/storage/test_resultclient.py diff --git a/openfe/tests/storage/test_resultserver.py b/src/openfe/tests/storage/test_resultserver.py similarity index 100% rename from openfe/tests/storage/test_resultserver.py rename to src/openfe/tests/storage/test_resultserver.py diff --git a/openfe/tests/utils/__init__.py b/src/openfe/tests/utils/__init__.py similarity index 100% rename from openfe/tests/utils/__init__.py rename to src/openfe/tests/utils/__init__.py diff --git a/openfe/tests/utils/conftest.py b/src/openfe/tests/utils/conftest.py similarity index 100% rename from openfe/tests/utils/conftest.py rename to src/openfe/tests/utils/conftest.py diff --git a/openfe/tests/utils/test_atommapping_network_plotting.py b/src/openfe/tests/utils/test_atommapping_network_plotting.py similarity index 100% rename from openfe/tests/utils/test_atommapping_network_plotting.py rename to src/openfe/tests/utils/test_atommapping_network_plotting.py diff --git a/openfe/tests/utils/test_duecredit.py b/src/openfe/tests/utils/test_duecredit.py similarity index 100% rename from openfe/tests/utils/test_duecredit.py rename to src/openfe/tests/utils/test_duecredit.py diff --git a/openfe/tests/utils/test_log_control.py b/src/openfe/tests/utils/test_log_control.py similarity index 100% rename from openfe/tests/utils/test_log_control.py rename to src/openfe/tests/utils/test_log_control.py diff --git a/openfe/tests/utils/test_network_plotting.py b/src/openfe/tests/utils/test_network_plotting.py similarity index 100% rename from openfe/tests/utils/test_network_plotting.py rename to src/openfe/tests/utils/test_network_plotting.py diff --git a/openfe/tests/utils/test_optional_imports.py b/src/openfe/tests/utils/test_optional_imports.py similarity index 100% rename from openfe/tests/utils/test_optional_imports.py rename to src/openfe/tests/utils/test_optional_imports.py diff --git a/openfe/tests/utils/test_remove_oechem.py b/src/openfe/tests/utils/test_remove_oechem.py similarity index 100% rename from openfe/tests/utils/test_remove_oechem.py rename to src/openfe/tests/utils/test_remove_oechem.py diff --git a/openfe/tests/utils/test_system_probe.py b/src/openfe/tests/utils/test_system_probe.py similarity index 100% rename from openfe/tests/utils/test_system_probe.py rename to src/openfe/tests/utils/test_system_probe.py diff --git a/openfe/tests/utils/test_visualization_3D.py b/src/openfe/tests/utils/test_visualization_3D.py similarity index 100% rename from openfe/tests/utils/test_visualization_3D.py rename to src/openfe/tests/utils/test_visualization_3D.py diff --git a/openfe/utils/__init__.py b/src/openfe/utils/__init__.py similarity index 100% rename from openfe/utils/__init__.py rename to src/openfe/utils/__init__.py diff --git a/openfe/utils/atommapping_network_plotting.py b/src/openfe/utils/atommapping_network_plotting.py similarity index 100% rename from openfe/utils/atommapping_network_plotting.py rename to src/openfe/utils/atommapping_network_plotting.py diff --git a/openfe/utils/custom_typing.py b/src/openfe/utils/custom_typing.py similarity index 100% rename from openfe/utils/custom_typing.py rename to src/openfe/utils/custom_typing.py diff --git a/openfe/utils/ligand_utils.py b/src/openfe/utils/ligand_utils.py similarity index 100% rename from openfe/utils/ligand_utils.py rename to src/openfe/utils/ligand_utils.py diff --git a/openfe/utils/logging_control.py b/src/openfe/utils/logging_control.py similarity index 100% rename from openfe/utils/logging_control.py rename to src/openfe/utils/logging_control.py diff --git a/openfe/utils/network_plotting.py b/src/openfe/utils/network_plotting.py similarity index 100% rename from openfe/utils/network_plotting.py rename to src/openfe/utils/network_plotting.py diff --git a/openfe/utils/optional_imports.py b/src/openfe/utils/optional_imports.py similarity index 100% rename from openfe/utils/optional_imports.py rename to src/openfe/utils/optional_imports.py diff --git a/openfe/utils/remove_oechem.py b/src/openfe/utils/remove_oechem.py similarity index 100% rename from openfe/utils/remove_oechem.py rename to src/openfe/utils/remove_oechem.py diff --git a/openfe/utils/silence_root_logging.py b/src/openfe/utils/silence_root_logging.py similarity index 100% rename from openfe/utils/silence_root_logging.py rename to src/openfe/utils/silence_root_logging.py diff --git a/openfe/utils/system_probe.py b/src/openfe/utils/system_probe.py similarity index 100% rename from openfe/utils/system_probe.py rename to src/openfe/utils/system_probe.py diff --git a/openfe/utils/visualization_3D.py b/src/openfe/utils/visualization_3D.py similarity index 100% rename from openfe/utils/visualization_3D.py rename to src/openfe/utils/visualization_3D.py diff --git a/openfecli/README.md b/src/openfecli/README.md similarity index 100% rename from openfecli/README.md rename to src/openfecli/README.md diff --git a/openfecli/__init__.py b/src/openfecli/__init__.py similarity index 100% rename from openfecli/__init__.py rename to src/openfecli/__init__.py diff --git a/openfecli/cli.py b/src/openfecli/cli.py similarity index 100% rename from openfecli/cli.py rename to src/openfecli/cli.py diff --git a/openfecli/clicktypes/__init__.py b/src/openfecli/clicktypes/__init__.py similarity index 100% rename from openfecli/clicktypes/__init__.py rename to src/openfecli/clicktypes/__init__.py diff --git a/openfecli/clicktypes/hyphenchoice.py b/src/openfecli/clicktypes/hyphenchoice.py similarity index 100% rename from openfecli/clicktypes/hyphenchoice.py rename to src/openfecli/clicktypes/hyphenchoice.py diff --git a/openfecli/commands/__init__.py b/src/openfecli/commands/__init__.py similarity index 100% rename from openfecli/commands/__init__.py rename to src/openfecli/commands/__init__.py diff --git a/openfecli/commands/atommapping.py b/src/openfecli/commands/atommapping.py similarity index 100% rename from openfecli/commands/atommapping.py rename to src/openfecli/commands/atommapping.py diff --git a/openfecli/commands/fetch.py b/src/openfecli/commands/fetch.py similarity index 100% rename from openfecli/commands/fetch.py rename to src/openfecli/commands/fetch.py diff --git a/openfecli/commands/gather.py b/src/openfecli/commands/gather.py similarity index 100% rename from openfecli/commands/gather.py rename to src/openfecli/commands/gather.py diff --git a/openfecli/commands/gather_abfe.py b/src/openfecli/commands/gather_abfe.py similarity index 100% rename from openfecli/commands/gather_abfe.py rename to src/openfecli/commands/gather_abfe.py diff --git a/openfecli/commands/gather_septop.py b/src/openfecli/commands/gather_septop.py similarity index 100% rename from openfecli/commands/gather_septop.py rename to src/openfecli/commands/gather_septop.py diff --git a/openfecli/commands/generate_partial_charges.py b/src/openfecli/commands/generate_partial_charges.py similarity index 100% rename from openfecli/commands/generate_partial_charges.py rename to src/openfecli/commands/generate_partial_charges.py diff --git a/openfecli/commands/plan_rbfe_network.py b/src/openfecli/commands/plan_rbfe_network.py similarity index 100% rename from openfecli/commands/plan_rbfe_network.py rename to src/openfecli/commands/plan_rbfe_network.py diff --git a/openfecli/commands/plan_rhfe_network.py b/src/openfecli/commands/plan_rhfe_network.py similarity index 100% rename from openfecli/commands/plan_rhfe_network.py rename to src/openfecli/commands/plan_rhfe_network.py diff --git a/openfecli/commands/quickrun.py b/src/openfecli/commands/quickrun.py similarity index 100% rename from openfecli/commands/quickrun.py rename to src/openfecli/commands/quickrun.py diff --git a/openfecli/commands/test.py b/src/openfecli/commands/test.py similarity index 100% rename from openfecli/commands/test.py rename to src/openfecli/commands/test.py diff --git a/openfecli/commands/view_ligand_network.py b/src/openfecli/commands/view_ligand_network.py similarity index 100% rename from openfecli/commands/view_ligand_network.py rename to src/openfecli/commands/view_ligand_network.py diff --git a/openfecli/fetchables.py b/src/openfecli/fetchables.py similarity index 100% rename from openfecli/fetchables.py rename to src/openfecli/fetchables.py diff --git a/openfecli/fetching.py b/src/openfecli/fetching.py similarity index 100% rename from openfecli/fetching.py rename to src/openfecli/fetching.py diff --git a/openfecli/parameters/__init__.py b/src/openfecli/parameters/__init__.py similarity index 100% rename from openfecli/parameters/__init__.py rename to src/openfecli/parameters/__init__.py diff --git a/openfecli/parameters/mapper.py b/src/openfecli/parameters/mapper.py similarity index 100% rename from openfecli/parameters/mapper.py rename to src/openfecli/parameters/mapper.py diff --git a/openfecli/parameters/misc.py b/src/openfecli/parameters/misc.py similarity index 100% rename from openfecli/parameters/misc.py rename to src/openfecli/parameters/misc.py diff --git a/openfecli/parameters/mol.py b/src/openfecli/parameters/mol.py similarity index 100% rename from openfecli/parameters/mol.py rename to src/openfecli/parameters/mol.py diff --git a/openfecli/parameters/molecules.py b/src/openfecli/parameters/molecules.py similarity index 100% rename from openfecli/parameters/molecules.py rename to src/openfecli/parameters/molecules.py diff --git a/openfecli/parameters/output.py b/src/openfecli/parameters/output.py similarity index 100% rename from openfecli/parameters/output.py rename to src/openfecli/parameters/output.py diff --git a/openfecli/parameters/output_dir.py b/src/openfecli/parameters/output_dir.py similarity index 100% rename from openfecli/parameters/output_dir.py rename to src/openfecli/parameters/output_dir.py diff --git a/openfecli/parameters/plan_network_options.py b/src/openfecli/parameters/plan_network_options.py similarity index 100% rename from openfecli/parameters/plan_network_options.py rename to src/openfecli/parameters/plan_network_options.py diff --git a/openfecli/parameters/protein.py b/src/openfecli/parameters/protein.py similarity index 100% rename from openfecli/parameters/protein.py rename to src/openfecli/parameters/protein.py diff --git a/openfecli/parameters/utils.py b/src/openfecli/parameters/utils.py similarity index 100% rename from openfecli/parameters/utils.py rename to src/openfecli/parameters/utils.py diff --git a/openfecli/plan_alchemical_networks_utils.py b/src/openfecli/plan_alchemical_networks_utils.py similarity index 100% rename from openfecli/plan_alchemical_networks_utils.py rename to src/openfecli/plan_alchemical_networks_utils.py diff --git a/openfecli/plugins.py b/src/openfecli/plugins.py similarity index 100% rename from openfecli/plugins.py rename to src/openfecli/plugins.py diff --git a/openfecli/tests/__init__.py b/src/openfecli/tests/__init__.py similarity index 100% rename from openfecli/tests/__init__.py rename to src/openfecli/tests/__init__.py diff --git a/openfecli/tests/clicktypes/test_hyphenchoice.py b/src/openfecli/tests/clicktypes/test_hyphenchoice.py similarity index 100% rename from openfecli/tests/clicktypes/test_hyphenchoice.py rename to src/openfecli/tests/clicktypes/test_hyphenchoice.py diff --git a/openfecli/tests/commands/__init__.py b/src/openfecli/tests/commands/__init__.py similarity index 100% rename from openfecli/tests/commands/__init__.py rename to src/openfecli/tests/commands/__init__.py diff --git a/openfecli/tests/commands/conftest.py b/src/openfecli/tests/commands/conftest.py similarity index 100% rename from openfecli/tests/commands/conftest.py rename to src/openfecli/tests/commands/conftest.py diff --git a/openfecli/tests/commands/test_atommapping.py b/src/openfecli/tests/commands/test_atommapping.py similarity index 100% rename from openfecli/tests/commands/test_atommapping.py rename to src/openfecli/tests/commands/test_atommapping.py diff --git a/openfecli/tests/commands/test_charge_generation.py b/src/openfecli/tests/commands/test_charge_generation.py similarity index 100% rename from openfecli/tests/commands/test_charge_generation.py rename to src/openfecli/tests/commands/test_charge_generation.py diff --git a/openfecli/tests/commands/test_gather.py b/src/openfecli/tests/commands/test_gather.py similarity index 100% rename from openfecli/tests/commands/test_gather.py rename to src/openfecli/tests/commands/test_gather.py diff --git a/openfecli/tests/commands/test_gather/test_abfe_full_results_dg_.tsv b/src/openfecli/tests/commands/test_gather/test_abfe_full_results_dg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_abfe_full_results_dg_.tsv rename to src/openfecli/tests/commands/test_gather/test_abfe_full_results_dg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_abfe_full_results_raw_.tsv b/src/openfecli/tests/commands/test_gather/test_abfe_full_results_raw_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_abfe_full_results_raw_.tsv rename to src/openfecli/tests/commands/test_gather/test_abfe_full_results_raw_.tsv diff --git a/openfecli/tests/commands/test_gather/test_abfe_single_repeat_dg_.tsv b/src/openfecli/tests/commands/test_gather/test_abfe_single_repeat_dg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_abfe_single_repeat_dg_.tsv rename to src/openfecli/tests/commands/test_gather/test_abfe_single_repeat_dg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_abfe_single_repeat_raw_.tsv b/src/openfecli/tests/commands/test_gather/test_abfe_single_repeat_raw_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_abfe_single_repeat_raw_.tsv rename to src/openfecli/tests/commands/test_gather/test_abfe_single_repeat_raw_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_failed_edge_ddg_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_failed_edge_ddg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_failed_edge_ddg_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_failed_edge_ddg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_failed_edge_raw_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_failed_edge_raw_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_failed_edge_raw_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_failed_edge_raw_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_full_results_ddg_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_full_results_ddg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_full_results_ddg_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_full_results_ddg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_full_results_dg_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_full_results_dg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_full_results_dg_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_full_results_dg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_full_results_raw_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_full_results_raw_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_full_results_raw_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_full_results_raw_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_allow_partial_ddg_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_allow_partial_ddg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_allow_partial_ddg_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_allow_partial_ddg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_ddg_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_ddg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_ddg_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_ddg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_dg_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_dg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_dg_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_missing_all_complex_legs_fail_dg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_ddg_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_ddg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_ddg_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_ddg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_dg_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_dg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_dg_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_dg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_raw_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_raw_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_raw_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_missing_complex_leg_raw_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_edge_ddg_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_missing_edge_ddg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_missing_edge_ddg_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_missing_edge_ddg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_edge_dg_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_missing_edge_dg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_missing_edge_dg_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_missing_edge_dg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_cmet_missing_edge_raw_.tsv b/src/openfecli/tests/commands/test_gather/test_cmet_missing_edge_raw_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_cmet_missing_edge_raw_.tsv rename to src/openfecli/tests/commands/test_gather/test_cmet_missing_edge_raw_.tsv diff --git a/openfecli/tests/commands/test_gather/test_septop_full_results_ddg_.tsv b/src/openfecli/tests/commands/test_gather/test_septop_full_results_ddg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_septop_full_results_ddg_.tsv rename to src/openfecli/tests/commands/test_gather/test_septop_full_results_ddg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_septop_full_results_dg_.tsv b/src/openfecli/tests/commands/test_gather/test_septop_full_results_dg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_septop_full_results_dg_.tsv rename to src/openfecli/tests/commands/test_gather/test_septop_full_results_dg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_septop_full_results_raw_.tsv b/src/openfecli/tests/commands/test_gather/test_septop_full_results_raw_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_septop_full_results_raw_.tsv rename to src/openfecli/tests/commands/test_gather/test_septop_full_results_raw_.tsv diff --git a/openfecli/tests/commands/test_gather/test_septop_single_repeat_ddg_.tsv b/src/openfecli/tests/commands/test_gather/test_septop_single_repeat_ddg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_septop_single_repeat_ddg_.tsv rename to src/openfecli/tests/commands/test_gather/test_septop_single_repeat_ddg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_septop_single_repeat_dg_.tsv b/src/openfecli/tests/commands/test_gather/test_septop_single_repeat_dg_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_septop_single_repeat_dg_.tsv rename to src/openfecli/tests/commands/test_gather/test_septop_single_repeat_dg_.tsv diff --git a/openfecli/tests/commands/test_gather/test_septop_single_repeat_raw_.tsv b/src/openfecli/tests/commands/test_gather/test_septop_single_repeat_raw_.tsv similarity index 100% rename from openfecli/tests/commands/test_gather/test_septop_single_repeat_raw_.tsv rename to src/openfecli/tests/commands/test_gather/test_septop_single_repeat_raw_.tsv diff --git a/openfecli/tests/commands/test_ligand_network_viewer.py b/src/openfecli/tests/commands/test_ligand_network_viewer.py similarity index 100% rename from openfecli/tests/commands/test_ligand_network_viewer.py rename to src/openfecli/tests/commands/test_ligand_network_viewer.py diff --git a/openfecli/tests/commands/test_plan_rbfe_network.py b/src/openfecli/tests/commands/test_plan_rbfe_network.py similarity index 100% rename from openfecli/tests/commands/test_plan_rbfe_network.py rename to src/openfecli/tests/commands/test_plan_rbfe_network.py diff --git a/openfecli/tests/commands/test_plan_rhfe_network.py b/src/openfecli/tests/commands/test_plan_rhfe_network.py similarity index 100% rename from openfecli/tests/commands/test_plan_rhfe_network.py rename to src/openfecli/tests/commands/test_plan_rhfe_network.py diff --git a/openfecli/tests/commands/test_quickrun.py b/src/openfecli/tests/commands/test_quickrun.py similarity index 100% rename from openfecli/tests/commands/test_quickrun.py rename to src/openfecli/tests/commands/test_quickrun.py diff --git a/openfecli/tests/commands/test_test.py b/src/openfecli/tests/commands/test_test.py similarity index 100% rename from openfecli/tests/commands/test_test.py rename to src/openfecli/tests/commands/test_test.py diff --git a/openfecli/tests/conftest.py b/src/openfecli/tests/conftest.py similarity index 100% rename from openfecli/tests/conftest.py rename to src/openfecli/tests/conftest.py diff --git a/openfecli/tests/data/__init__.py b/src/openfecli/tests/data/__init__.py similarity index 100% rename from openfecli/tests/data/__init__.py rename to src/openfecli/tests/data/__init__.py diff --git a/openfecli/tests/data/bad_transformation.json b/src/openfecli/tests/data/bad_transformation.json similarity index 100% rename from openfecli/tests/data/bad_transformation.json rename to src/openfecli/tests/data/bad_transformation.json diff --git a/openfecli/tests/data/rbfe_results.tar.gz b/src/openfecli/tests/data/rbfe_results.tar.gz similarity index 100% rename from openfecli/tests/data/rbfe_results.tar.gz rename to src/openfecli/tests/data/rbfe_results.tar.gz diff --git a/openfecli/tests/data/rbfe_tutorial/__init__.py b/src/openfecli/tests/data/rbfe_tutorial/__init__.py similarity index 100% rename from openfecli/tests/data/rbfe_tutorial/__init__.py rename to src/openfecli/tests/data/rbfe_tutorial/__init__.py diff --git a/openfecli/tests/data/rbfe_tutorial/tyk2_ligands.sdf b/src/openfecli/tests/data/rbfe_tutorial/tyk2_ligands.sdf similarity index 100% rename from openfecli/tests/data/rbfe_tutorial/tyk2_ligands.sdf rename to src/openfecli/tests/data/rbfe_tutorial/tyk2_ligands.sdf diff --git a/openfecli/tests/data/rbfe_tutorial/tyk2_protein.pdb b/src/openfecli/tests/data/rbfe_tutorial/tyk2_protein.pdb similarity index 100% rename from openfecli/tests/data/rbfe_tutorial/tyk2_protein.pdb rename to src/openfecli/tests/data/rbfe_tutorial/tyk2_protein.pdb diff --git a/openfecli/tests/data/transformation.json b/src/openfecli/tests/data/transformation.json similarity index 100% rename from openfecli/tests/data/transformation.json rename to src/openfecli/tests/data/transformation.json diff --git a/openfecli/tests/dev/__init__.py b/src/openfecli/tests/dev/__init__.py similarity index 100% rename from openfecli/tests/dev/__init__.py rename to src/openfecli/tests/dev/__init__.py diff --git a/openfecli/tests/dev/write_transformation_json.py b/src/openfecli/tests/dev/write_transformation_json.py similarity index 100% rename from openfecli/tests/dev/write_transformation_json.py rename to src/openfecli/tests/dev/write_transformation_json.py diff --git a/openfecli/tests/parameters/__init__.py b/src/openfecli/tests/parameters/__init__.py similarity index 100% rename from openfecli/tests/parameters/__init__.py rename to src/openfecli/tests/parameters/__init__.py diff --git a/openfecli/tests/parameters/test_mapper.py b/src/openfecli/tests/parameters/test_mapper.py similarity index 100% rename from openfecli/tests/parameters/test_mapper.py rename to src/openfecli/tests/parameters/test_mapper.py diff --git a/openfecli/tests/parameters/test_mol.py b/src/openfecli/tests/parameters/test_mol.py similarity index 100% rename from openfecli/tests/parameters/test_mol.py rename to src/openfecli/tests/parameters/test_mol.py diff --git a/openfecli/tests/parameters/test_molecules.py b/src/openfecli/tests/parameters/test_molecules.py similarity index 100% rename from openfecli/tests/parameters/test_molecules.py rename to src/openfecli/tests/parameters/test_molecules.py diff --git a/openfecli/tests/parameters/test_output.py b/src/openfecli/tests/parameters/test_output.py similarity index 100% rename from openfecli/tests/parameters/test_output.py rename to src/openfecli/tests/parameters/test_output.py diff --git a/openfecli/tests/parameters/test_output_dir.py b/src/openfecli/tests/parameters/test_output_dir.py similarity index 100% rename from openfecli/tests/parameters/test_output_dir.py rename to src/openfecli/tests/parameters/test_output_dir.py diff --git a/openfecli/tests/parameters/test_plan_network_options.py b/src/openfecli/tests/parameters/test_plan_network_options.py similarity index 100% rename from openfecli/tests/parameters/test_plan_network_options.py rename to src/openfecli/tests/parameters/test_plan_network_options.py diff --git a/openfecli/tests/parameters/test_protein.py b/src/openfecli/tests/parameters/test_protein.py similarity index 100% rename from openfecli/tests/parameters/test_protein.py rename to src/openfecli/tests/parameters/test_protein.py diff --git a/openfecli/tests/parameters/test_utils.py b/src/openfecli/tests/parameters/test_utils.py similarity index 100% rename from openfecli/tests/parameters/test_utils.py rename to src/openfecli/tests/parameters/test_utils.py diff --git a/openfecli/tests/test_cli.py b/src/openfecli/tests/test_cli.py similarity index 100% rename from openfecli/tests/test_cli.py rename to src/openfecli/tests/test_cli.py diff --git a/openfecli/tests/test_fetchables.py b/src/openfecli/tests/test_fetchables.py similarity index 100% rename from openfecli/tests/test_fetchables.py rename to src/openfecli/tests/test_fetchables.py diff --git a/openfecli/tests/test_fetching.py b/src/openfecli/tests/test_fetching.py similarity index 100% rename from openfecli/tests/test_fetching.py rename to src/openfecli/tests/test_fetching.py diff --git a/openfecli/tests/test_plugins.py b/src/openfecli/tests/test_plugins.py similarity index 100% rename from openfecli/tests/test_plugins.py rename to src/openfecli/tests/test_plugins.py diff --git a/openfecli/tests/test_rbfe_tutorial.py b/src/openfecli/tests/test_rbfe_tutorial.py similarity index 100% rename from openfecli/tests/test_rbfe_tutorial.py rename to src/openfecli/tests/test_rbfe_tutorial.py diff --git a/openfecli/tests/test_utils.py b/src/openfecli/tests/test_utils.py similarity index 100% rename from openfecli/tests/test_utils.py rename to src/openfecli/tests/test_utils.py diff --git a/openfecli/tests/utils.py b/src/openfecli/tests/utils.py similarity index 100% rename from openfecli/tests/utils.py rename to src/openfecli/tests/utils.py diff --git a/openfecli/utils.py b/src/openfecli/utils.py similarity index 100% rename from openfecli/utils.py rename to src/openfecli/utils.py From ada0784e36f88d8fbc2b5192a9d1dd4f6c308f1d Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:20:28 -0800 Subject: [PATCH 39/47] Temporarily build pooch from main w/ hotfix (#1806) * build with pooch@main to see if hotfix works * add link --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 3a536497d..0d563c254 100644 --- a/environment.yml +++ b/environment.yml @@ -27,7 +27,6 @@ dependencies: - plugcli - pint>=0.24.0 - pip - - pooch - py3dmol - pydantic >= 2.0.0, <2.12.0 # https://github.com/openforcefield/openff-interchange/issues/1346 - pygraphviz @@ -53,3 +52,4 @@ dependencies: - threadpoolctl - pip: - git+https://github.com/OpenFreeEnergy/gufe@main + - git+https://github.com/fatiando/pooch@main # related to https://github.com/fatiando/pooch/issues/502 From 1a7c906fea9a774e91f348fb899012e28cf53806 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:27:08 -0800 Subject: [PATCH 40/47] make CLI starting guide easier to find from the landing page (#1787) * make CLI starting guide easier to find from the landing page * Python API -> API Docs * bump ci * bump example notebooks pin to 2026.01.26 * change a word * clarifying language --- docs/conf.py | 2 +- docs/index.rst | 12 ++++++------ docs/reference/api/index.rst | 2 +- src/openfecli/parameters/plan_network_options.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1e6bd07df..f54790c22 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -193,7 +193,7 @@ else: repo = git.Repo.clone_from( "https://github.com/OpenFreeEnergy/ExampleNotebooks.git", - branch="2025.12.04", + branch="2026.01.26", to_path=example_notebooks_path, ) except Exception as e: diff --git a/docs/index.rst b/docs/index.rst index 600d84310..4a0cb6f69 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,12 +25,12 @@ Using this toolkit you can plan, execute, and analyze free energy calculations u Follow our installation guide to get **openfe** running on your machine! - .. grid-item-card:: :fas:`laptop-code` CLI + .. grid-item-card:: :fas:`laptop-code` CLI Quickstart :text-align: center - :link: reference/cli/index + :link: tutorials/rbfe_cli_tutorial :link-type: doc - Documentation for **openfe**\'s simple command line interface. + Get started with **openfe**\'s command line interface. .. grid-item-card:: :fas:`person-chalkboard` Tutorials :text-align: center @@ -53,12 +53,12 @@ Using this toolkit you can plan, execute, and analyze free energy calculations u How-to guides for common tasks. - .. grid-item-card:: :fas:`code` Python API + .. grid-item-card:: :fas:`code` API Reference :text-align: center - :link: reference/api/index + :link: reference/index :link-type: doc - Comprehensive details of the **openfe** Python API. + Comprehensive details of the **openfe** Python and CLI APIs. .. grid-item-card:: :fas:`gears` Protocols :text-align: center diff --git a/docs/reference/api/index.rst b/docs/reference/api/index.rst index 5b93b8739..d74b545dc 100644 --- a/docs/reference/api/index.rst +++ b/docs/reference/api/index.rst @@ -4,7 +4,7 @@ We have reproduced API documentation from the `gufe`_ package here for convenience. `gufe`_ serves as a foundation layer for openfe, providing abstract base classes and object models, and so might be more useful for developers. -OpenFE API Reference +Python API Reference ==================== .. toctree:: diff --git a/src/openfecli/parameters/plan_network_options.py b/src/openfecli/parameters/plan_network_options.py index 5798d69e4..b349d8972 100644 --- a/src/openfecli/parameters/plan_network_options.py +++ b/src/openfecli/parameters/plan_network_options.py @@ -205,7 +205,7 @@ def load_yaml_planner_options(path: Optional[str], context) -> PlanNetworkOption # TODO: do we want this in the docs anywhere? DEFAULT_YAML = """ - mapper: KartografAtomMapper + mapper: kartograf settings: atom_max_distance: 0.95 atom_map_hydrogens: true From 573426a136a75da4fbb76c0e85d2ea79668ce381 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Wed, 28 Jan 2026 16:02:59 +0000 Subject: [PATCH 41/47] Update pyproject.toml (#1813) * Update pyproject.toml * update classifiers --- pyproject.toml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8f995c09e..baed98d00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,16 +12,14 @@ description = "" readme = "README.md" license = "MIT" license-files = [ "LICENSE" ] -authors = [ { name = "The OpenFE developers", email = "openfe@omsf.io" } ] -requires-python = ">=3.10" +authors = [ { name = "The OpenFE developers", email = "openfreeenergy@omsf.io" } ] +requires-python = ">=3.11" classifiers = [ - "Development Status :: 1 - Planning", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", - "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", From 300c1749a258c68fe3bd377d753df64792119420 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Thu, 29 Jan 2026 07:08:04 -0800 Subject: [PATCH 42/47] refactor: clean up test data handling (#1815) * import pooch_cache from conftest * remove unused files from pooch cache * move zenodo cache to conftest * remove duplicate code --- src/openfe/tests/conftest.py | 3 ++ src/openfe/tests/protocols/conftest.py | 50 +++++++++++++++++-- .../restraints/test_geometry_boresch.py | 21 +------- .../restraints/test_geometry_boresch_host.py | 32 ++---------- .../restraints/test_geometry_utils.py | 27 +--------- .../restraints/test_omm_restraints.py | 32 +++--------- .../tests/protocols/test_openmmutils.py | 21 +------- 7 files changed, 63 insertions(+), 123 deletions(-) diff --git a/src/openfe/tests/conftest.py b/src/openfe/tests/conftest.py index f2358757f..4a75fc03b 100644 --- a/src/openfe/tests/conftest.py +++ b/src/openfe/tests/conftest.py @@ -12,6 +12,7 @@ import numpy as np import openmm import pandas as pd +import pooch import pytest from gufe import AtomMapper, LigandAtomMapping, ProteinComponent, SmallMoleculeComponent from openff.toolkit import ForceField @@ -26,6 +27,8 @@ from openfe.protocols.openmm_utils.serialization import deserialize from openfe.tests.protocols.openmm_rfe.helpers import make_htf +POOCH_CACHE = pooch.os_cache("openfe") + class SlowTests: """Plugin for handling fixtures that skips slow tests diff --git a/src/openfe/tests/protocols/conftest.py b/src/openfe/tests/protocols/conftest.py index ccf83d7e4..cfd225a98 100644 --- a/src/openfe/tests/protocols/conftest.py +++ b/src/openfe/tests/protocols/conftest.py @@ -1,9 +1,11 @@ # This code is part of OpenFE and is licensed under the MIT license. # For details, see https://github.com/OpenFreeEnergy/openfe import gzip +import pathlib from importlib import resources from typing import Optional +import MDAnalysis as mda import openmm import pooch import pytest @@ -16,6 +18,8 @@ import openfe +from ..conftest import POOCH_CACHE + @pytest.fixture def available_platforms() -> set[str]: @@ -280,15 +284,51 @@ def septop_json() -> str: return f.read().decode() # type: ignore +zenodo_industry_benchmarks_data = pooch.create( + path=POOCH_CACHE, + base_url="doi:10.5281/zenodo.15212342", + registry={ + "industry_benchmark_systems.zip": "sha256:2bb5eee36e29b718b96bf6e9350e0b9957a592f6c289f77330cbb6f4311a07bd" + }, +) + + +@pytest.fixture +def industry_benchmark_files(): + zenodo_industry_benchmarks_data.fetch("industry_benchmark_systems.zip", processor=pooch.Unzip()) + cache_dir = pathlib.Path( + POOCH_CACHE / "industry_benchmark_systems.zip.unzip/industry_benchmark_systems" + ) + return cache_dir + + +zenodo_restraint_data = pooch.create( + path=POOCH_CACHE, + base_url="doi:10.5281/zenodo.15212342", + registry={ + "t4_lysozyme_trajectory.zip": "sha256:e985d055db25b5468491e169948f641833a5fbb67a23dbb0a00b57fb7c0e59c8" + }, +) + + +@pytest.fixture +def t4_lysozyme_trajectory_universe(): + zenodo_restraint_data.fetch("t4_lysozyme_trajectory.zip", processor=pooch.Unzip()) + cache_dir = pathlib.Path( + POOCH_CACHE / "t4_lysozyme_trajectory.zip.unzip/t4_lysozyme_trajectory" + ) + universe = mda.Universe( + str(cache_dir / "t4_toluene_complex.pdb"), + str(cache_dir / "t4_toluene_complex.xtc"), + ) + return universe + + RFE_OUTPUT = pooch.create( - path=pooch.os_cache("openfe_analysis"), + path=POOCH_CACHE, base_url="doi:10.6084/m9.figshare.24101655", registry={ - "checkpoint.nc": "5af398cb14340fddf7492114998b244424b6c3f4514b2e07e4bd411484c08464", - "db.json": "b671f9eb4daf9853f3e1645f9fd7c18150fd2a9bf17c18f23c5cf0c9fd5ca5b3", - "hybrid_system.pdb": "07203679cb14b840b36e4320484df2360f45e323faadb02d6eacac244fddd517", "simulation.nc": "92361a0864d4359a75399470135f56642b72c605069a4c33dbc4be6f91f28b31", - "simulation_real_time_analysis.yaml": "65706002f371fafba96037f29b054fd7e050e442915205df88567f48f5e5e1cf", }, ) diff --git a/src/openfe/tests/protocols/restraints/test_geometry_boresch.py b/src/openfe/tests/protocols/restraints/test_geometry_boresch.py index b533db1ac..493de92ac 100644 --- a/src/openfe/tests/protocols/restraints/test_geometry_boresch.py +++ b/src/openfe/tests/protocols/restraints/test_geometry_boresch.py @@ -14,7 +14,7 @@ find_boresch_restraint, ) -from ...conftest import HAS_INTERNET +from ...conftest import HAS_INTERNET, POOCH_CACHE @pytest.fixture() @@ -235,25 +235,6 @@ def test_get_boresch_restraint_dssp(eg5_protein_ligand_universe, eg5_ligands): assert -0.02396901 == pytest.approx(restraint_geometry.phi_C0.to("radians").m) -POOCH_CACHE = pooch.os_cache("openfe") -zenodo_restraint_data = pooch.create( - path=POOCH_CACHE, - base_url="doi:10.5281/zenodo.15212342", - registry={ - "industry_benchmark_systems.zip": "sha256:2bb5eee36e29b718b96bf6e9350e0b9957a592f6c289f77330cbb6f4311a07bd" - }, -) - - -@pytest.fixture -def industry_benchmark_files(): - zenodo_restraint_data.fetch("industry_benchmark_systems.zip", processor=pooch.Unzip()) - cache_dir = pathlib.Path( - pooch.os_cache("openfe") / "industry_benchmark_systems.zip.unzip/industry_benchmark_systems" - ) - return cache_dir - - @pytest.mark.skipif( not os.path.exists(POOCH_CACHE) and not HAS_INTERNET, reason="Internet seems to be unavailable and test data is not cached locally.", diff --git a/src/openfe/tests/protocols/restraints/test_geometry_boresch_host.py b/src/openfe/tests/protocols/restraints/test_geometry_boresch_host.py index 78815a5e9..1c3121f1f 100644 --- a/src/openfe/tests/protocols/restraints/test_geometry_boresch_host.py +++ b/src/openfe/tests/protocols/restraints/test_geometry_boresch_host.py @@ -2,7 +2,6 @@ # For details, see https://github.com/OpenFreeEnergy/openfe import os -import pathlib import MDAnalysis as mda import numpy as np @@ -25,31 +24,7 @@ is_collinear, ) -from ...conftest import HAS_INTERNET - -POOCH_CACHE = pooch.os_cache("openfe") -zenodo_restraint_data = pooch.create( - path=POOCH_CACHE, - base_url="doi:10.5281/zenodo.15212342", - registry={ - "t4_lysozyme_trajectory.zip": "sha256:e985d055db25b5468491e169948f641833a5fbb67a23dbb0a00b57fb7c0e59c8" - }, -) - - -@pytest.fixture(scope="module") -def t4_lysozyme_trajectory_universe(): - zenodo_restraint_data.fetch("t4_lysozyme_trajectory.zip", processor=pooch.Unzip()) - cache_dir = pathlib.Path( - pooch.os_cache("openfe") / "t4_lysozyme_trajectory.zip.unzip/t4_lysozyme_trajectory" - ) - universe = mda.Universe( - str(cache_dir / "t4_toluene_complex.pdb"), - str(cache_dir / "t4_toluene_complex.xtc"), - ) - # guess bonds for the protein atoms - universe.select_atoms("protein").guess_bonds() - return universe +from ...conftest import HAS_INTERNET, POOCH_CACHE @pytest.fixture @@ -358,7 +333,10 @@ class TestFindAnchorBondedTrajectory(TestFindAnchorMulti): @pytest.fixture(scope="class") def universe(self, t4_lysozyme_trajectory_universe): - return t4_lysozyme_trajectory_universe + universe = t4_lysozyme_trajectory_universe + # guess bonds for the protein atoms + universe.select_atoms("protein").guess_bonds() + return universe @pytest.fixture(scope="class") def host_anchor(self, universe): diff --git a/src/openfe/tests/protocols/restraints/test_geometry_utils.py b/src/openfe/tests/protocols/restraints/test_geometry_utils.py index c7b3156d4..34db6a347 100644 --- a/src/openfe/tests/protocols/restraints/test_geometry_utils.py +++ b/src/openfe/tests/protocols/restraints/test_geometry_utils.py @@ -3,12 +3,10 @@ import itertools import os -import pathlib from importlib import resources import MDAnalysis as mda import numpy as np -import pooch import pytest from openff.units import unit from rdkit import Chem @@ -32,7 +30,7 @@ stable_secondary_structure_selection, ) -from ...conftest import HAS_INTERNET +from ...conftest import HAS_INTERNET, POOCH_CACHE @pytest.fixture(scope="module") @@ -49,29 +47,6 @@ def eg5_protein_ligand_universe(eg5_protein_pdb, eg5_ligands): return mda.Merge(protein.atoms, lig.atoms) -POOCH_CACHE = pooch.os_cache("openfe") -zenodo_restraint_data = pooch.create( - path=POOCH_CACHE, - base_url="doi:10.5281/zenodo.15212342", - registry={ - "t4_lysozyme_trajectory.zip": "sha256:e985d055db25b5468491e169948f641833a5fbb67a23dbb0a00b57fb7c0e59c8" - }, -) - - -@pytest.fixture -def t4_lysozyme_trajectory_universe(): - zenodo_restraint_data.fetch("t4_lysozyme_trajectory.zip", processor=pooch.Unzip()) - cache_dir = pathlib.Path( - pooch.os_cache("openfe") / "t4_lysozyme_trajectory.zip.unzip/t4_lysozyme_trajectory" - ) - universe = mda.Universe( - str(cache_dir / "t4_toluene_complex.pdb"), - str(cache_dir / "t4_toluene_complex.xtc"), - ) - return universe - - @pytest.fixture def beta_barrel_universe(): with resources.as_file(resources.files("openfe.tests.data")) as d: diff --git a/src/openfe/tests/protocols/restraints/test_omm_restraints.py b/src/openfe/tests/protocols/restraints/test_omm_restraints.py index 55b75f5ad..99ad1b5fa 100644 --- a/src/openfe/tests/protocols/restraints/test_omm_restraints.py +++ b/src/openfe/tests/protocols/restraints/test_omm_restraints.py @@ -2,10 +2,8 @@ # For details, see https://github.com/OpenFreeEnergy/openfe import os -import pathlib import openmm -import pooch import pytest from gufe import SmallMoleculeComponent from openff.units import unit @@ -28,7 +26,7 @@ FlatBottomRestraintSettings, ) -from ...conftest import HAS_INTERNET +from ...conftest import HAS_INTERNET, POOCH_CACHE def test_parameter_state_default(): @@ -98,34 +96,18 @@ def test_verify_geometry(): restraint._verify_geometry(geometry) -POOCH_CACHE = pooch.os_cache("openfe") -zenodo_restraint_data = pooch.create( - path=POOCH_CACHE, - base_url="doi:10.5281/zenodo.15212342", - registry={ - "industry_benchmark_systems.zip": "sha256:2bb5eee36e29b718b96bf6e9350e0b9957a592f6c289f77330cbb6f4311a07bd" - }, -) - - @pytest.fixture -def tyk2_protein_ligand_system(): - zenodo_restraint_data.fetch("industry_benchmark_systems.zip", processor=pooch.Unzip()) - cache_dir = pathlib.Path( - pooch.os_cache("openfe") / "industry_benchmark_systems.zip.unzip/industry_benchmark_systems" - ) - with open(str(cache_dir / "jacs_set" / "tyk2" / "protein_ligand_system.xml")) as xml: +def tyk2_protein_ligand_system(industry_benchmark_files): + with open( + str(industry_benchmark_files / "jacs_set" / "tyk2" / "protein_ligand_system.xml") + ) as xml: return openmm.XmlSerializer.deserialize(xml.read()) @pytest.fixture -def tyk2_rdkit_ligand(): - zenodo_restraint_data.fetch("industry_benchmark_systems.zip", processor=pooch.Unzip()) - cache_dir = pathlib.Path( - pooch.os_cache("openfe") / "industry_benchmark_systems.zip.unzip/industry_benchmark_systems" - ) +def tyk2_rdkit_ligand(industry_benchmark_files): ligand = SmallMoleculeComponent.from_sdf_file( - str(cache_dir / "jacs_set" / "tyk2" / "test_ligand.sdf") + str(industry_benchmark_files / "jacs_set" / "tyk2" / "test_ligand.sdf") ) return ligand.to_rdkit() diff --git a/src/openfe/tests/protocols/test_openmmutils.py b/src/openfe/tests/protocols/test_openmmutils.py index 8251970e2..46528f912 100644 --- a/src/openfe/tests/protocols/test_openmmutils.py +++ b/src/openfe/tests/protocols/test_openmmutils.py @@ -40,7 +40,7 @@ HAS_NAGL, HAS_OPENEYE, ) -from openfe.tests.conftest import HAS_INTERNET +from openfe.tests.conftest import HAS_INTERNET, POOCH_CACHE @pytest.mark.parametrize( @@ -1033,25 +1033,6 @@ def test_openeye_import_error(self, monkeypatch, uncharged_mol): ) -POOCH_CACHE = pooch.os_cache("openfe") -RFE_OUTPUT = pooch.create( - path=POOCH_CACHE, - base_url="doi:10.5281/zenodo.15375081", - registry={ - "checkpoint.nc": "md5:3cfd70a4cbe463403d6ec7cca84fc31a", - "db.json": "md5:33c8c1a0b629a52dcc291beff59fabc6", - "hybrid_system.pdb": "md5:44a1e78294360037acf419b95be18fb3", - "simulation.nc": "md5:bc4e842b47de17704d804ae345b91599", - "simulation_real_time_analysis.yaml": "md5:68a7d81462c42353a91bbbe5e64fd418", - }, -) - - -@pytest.fixture -def simulation_nc(): - return RFE_OUTPUT.fetch("simulation.nc") - - @pytest.mark.slow @pytest.mark.skipif( not os.path.exists(POOCH_CACHE) and not HAS_INTERNET, From 7a3eef1c90bf02d72d830415122844fe5ae42c17 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Mon, 2 Feb 2026 08:24:47 -0800 Subject: [PATCH 43/47] min pin pooch to fix data fetching (#1820) --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 0d563c254..155b4645d 100644 --- a/environment.yml +++ b/environment.yml @@ -27,6 +27,7 @@ dependencies: - plugcli - pint>=0.24.0 - pip + - pooch >= 1.9.0 # min needed for https://github.com/fatiando/pooch/issues/502 - py3dmol - pydantic >= 2.0.0, <2.12.0 # https://github.com/openforcefield/openff-interchange/issues/1346 - pygraphviz @@ -52,4 +53,3 @@ dependencies: - threadpoolctl - pip: - git+https://github.com/OpenFreeEnergy/gufe@main - - git+https://github.com/fatiando/pooch@main # related to https://github.com/fatiando/pooch/issues/502 From 9d50ca799fdc819d2820afb80dea4aa32c53a88a Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Tue, 3 Feb 2026 07:52:34 -0800 Subject: [PATCH 44/47] fix ligand network cropping bug (#1822) * fix ligand network cropping bug * move to upstream func * whitespace --- news/fix_image_crop.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 news/fix_image_crop.rst diff --git a/news/fix_image_crop.rst b/news/fix_image_crop.rst new file mode 100644 index 000000000..3d1f1d31c --- /dev/null +++ b/news/fix_image_crop.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Fixed bug in ligand network visualization (such as with ``openfe view-ligand-network``) so that ligand names are no longer cut off by the plot border (`PR 1822 `_). + +**Security:** + +* From 8d794e39da239e3b3ee6b6313724fd20028022f5 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:21:58 -0800 Subject: [PATCH 45/47] fix api break check path (#1825) --- .github/workflows/check_for_api_break.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check_for_api_break.yaml b/.github/workflows/check_for_api_break.yaml index 661ea52dd..e3d74f141 100644 --- a/.github/workflows/check_for_api_break.yaml +++ b/.github/workflows/check_for_api_break.yaml @@ -27,8 +27,8 @@ jobs: id: check run: | pip install griffe - griffe check "src/openfe" --verbose -a origin/main - griffe check "src/openfecli" --verbose -a origin/main + griffe check "openfe" --verbose -a origin/main + griffe check "openfecli" --verbose -a origin/main - name: Manage PR Comments uses: actions/github-script@v7 From 33426ee9ecb0e70573117fc44513b56a7fd91da3 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:25:52 -0800 Subject: [PATCH 46/47] fix pydantic deprecation of copy method (#1829) --- src/openfe/protocols/openmm_rfe/hybridtop_protocols.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openfe/protocols/openmm_rfe/hybridtop_protocols.py b/src/openfe/protocols/openmm_rfe/hybridtop_protocols.py index 12b6b168d..47ac3779e 100644 --- a/src/openfe/protocols/openmm_rfe/hybridtop_protocols.py +++ b/src/openfe/protocols/openmm_rfe/hybridtop_protocols.py @@ -173,7 +173,7 @@ def _adaptive_settings( # use initial settings or default settings # this is needed for the CLI so we don't override user settings if initial_settings is not None: - protocol_settings = initial_settings.copy(deep=True) + protocol_settings = initial_settings.model_copy(deep=True) else: protocol_settings = cls.default_settings() From 05cc0d1045470ec2fa9996b429c1e83b14608899 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:45:16 -0800 Subject: [PATCH 47/47] fix scope mismatch with zenodo data (#1828) * fix scope mismatch with zenodo data * fix session scope --- src/openfe/tests/protocols/conftest.py | 11 ++++------- .../restraints/test_geometry_boresch_host.py | 8 ++++++-- .../tests/protocols/restraints/test_geometry_utils.py | 10 ++++++++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/openfe/tests/protocols/conftest.py b/src/openfe/tests/protocols/conftest.py index cfd225a98..ab1405aa7 100644 --- a/src/openfe/tests/protocols/conftest.py +++ b/src/openfe/tests/protocols/conftest.py @@ -311,17 +311,14 @@ def industry_benchmark_files(): ) -@pytest.fixture -def t4_lysozyme_trajectory_universe(): +# session scope for downstream reuse +@pytest.fixture(scope="session") +def t4_lysozyme_trajectory_dir(): zenodo_restraint_data.fetch("t4_lysozyme_trajectory.zip", processor=pooch.Unzip()) cache_dir = pathlib.Path( POOCH_CACHE / "t4_lysozyme_trajectory.zip.unzip/t4_lysozyme_trajectory" ) - universe = mda.Universe( - str(cache_dir / "t4_toluene_complex.pdb"), - str(cache_dir / "t4_toluene_complex.xtc"), - ) - return universe + return cache_dir RFE_OUTPUT = pooch.create( diff --git a/src/openfe/tests/protocols/restraints/test_geometry_boresch_host.py b/src/openfe/tests/protocols/restraints/test_geometry_boresch_host.py index 1c3121f1f..d511b3d81 100644 --- a/src/openfe/tests/protocols/restraints/test_geometry_boresch_host.py +++ b/src/openfe/tests/protocols/restraints/test_geometry_boresch_host.py @@ -332,8 +332,12 @@ class TestFindAnchorBondedTrajectory(TestFindAnchorMulti): ref_h1h2_distance = 1.55881 @pytest.fixture(scope="class") - def universe(self, t4_lysozyme_trajectory_universe): - universe = t4_lysozyme_trajectory_universe + def universe(self, t4_lysozyme_trajectory_dir): + cache_dir = t4_lysozyme_trajectory_dir + universe = mda.Universe( + str(cache_dir / "t4_toluene_complex.pdb"), + str(cache_dir / "t4_toluene_complex.xtc"), + ) # guess bonds for the protein atoms universe.select_atoms("protein").guess_bonds() return universe diff --git a/src/openfe/tests/protocols/restraints/test_geometry_utils.py b/src/openfe/tests/protocols/restraints/test_geometry_utils.py index 34db6a347..d3cf312b0 100644 --- a/src/openfe/tests/protocols/restraints/test_geometry_utils.py +++ b/src/openfe/tests/protocols/restraints/test_geometry_utils.py @@ -400,6 +400,16 @@ def test_atomgroup_has_bonds(eg5_protein_pdb): assert _atomgroup_has_bonds(ag) +@pytest.fixture() +def t4_lysozyme_trajectory_universe(t4_lysozyme_trajectory_dir): + cache_dir = t4_lysozyme_trajectory_dir + universe = mda.Universe( + str(cache_dir / "t4_toluene_complex.pdb"), + str(cache_dir / "t4_toluene_complex.xtc"), + ) + return universe + + @pytest.mark.skipif( not os.path.exists(POOCH_CACHE) and not HAS_INTERNET, reason="Internet seems to be unavailable and test data is not cached locally.",

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%0Dp-XC#c!|YpI#9SqwM&EYtw8$v0!C@m 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 z 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Ξ)VAXA^TJ=x4Bs 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 dJ2W);W_i8ceY@HGOT0ueE2s?OKgyAgKLSk#4j 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