From 512c56c8fbe35868f7b3e1428fe8d1bb4bd59212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Thu, 28 Nov 2024 20:46:45 +0000 Subject: [PATCH 01/32] add jax serialisation --- python/sdist/amici/jax.template.py | 17 +++----- python/sdist/amici/jax/model.py | 4 +- python/sdist/amici/jax/petab.py | 43 ++++++++++++++++++- python/tests/test_jax.py | 29 +++++++++++++ .../benchmark-models/test_petab_benchmark.py | 12 +----- 5 files changed, 83 insertions(+), 22 deletions(-) diff --git a/python/sdist/amici/jax.template.py b/python/sdist/amici/jax.template.py index 367ba9e500..eda47b4f09 100644 --- a/python/sdist/amici/jax.template.py +++ b/python/sdist/amici/jax.template.py @@ -1,17 +1,18 @@ -import jax.numpy as jnp -from interpax import interp1d +from pathlib import Path from amici.jax.model import JAXModel +# ruff: noqa: F821, F841 + class JAXModel_TPL_MODEL_NAME(JAXModel): api_version = TPL_MODEL_API_VERSION def __init__(self): + self.jax_py_file = Path(__file__).resolve() super().__init__() def _xdot(self, t, x, args): - pk, tcl = args TPL_X_SYMS = x @@ -24,7 +25,6 @@ def _xdot(self, t, x, args): return TPL_XDOT_RET def _w(self, t, x, pk, tcl): - TPL_X_SYMS = x TPL_PK_SYMS = pk TPL_TCL_SYMS = tcl @@ -34,7 +34,6 @@ def _w(self, t, x, pk, tcl): return TPL_W_RET def _x0(self, pk): - TPL_PK_SYMS = pk TPL_X0_EQ @@ -42,7 +41,6 @@ def _x0(self, pk): return TPL_X0_RET def _x_solver(self, x): - TPL_X_RDATA_SYMS = x TPL_X_SOLVER_EQ @@ -50,7 +48,6 @@ def _x_solver(self, x): return TPL_X_SOLVER_RET def _x_rdata(self, x, tcl): - TPL_X_SYMS = x TPL_TCL_SYMS = tcl @@ -59,7 +56,6 @@ def _x_rdata(self, x, tcl): return TPL_X_RDATA_RET def _tcl(self, x, pk): - TPL_X_RDATA_SYMS = x TPL_PK_SYMS = pk @@ -68,7 +64,6 @@ def _tcl(self, x, pk): return TPL_TOTAL_CL_RET def _y(self, t, x, pk, tcl): - TPL_X_SYMS = x TPL_PK_SYMS = pk TPL_W_SYMS = self._w(t, x, pk, tcl) @@ -86,7 +81,6 @@ def _sigmay(self, y, pk): return TPL_SIGMAY_RET - def _nllh(self, t, x, pk, tcl, my, iy): y = self._y(t, x, pk, tcl) TPL_Y_SYMS = y @@ -107,3 +101,6 @@ def state_ids(self): @property def parameter_ids(self): return TPL_PK_IDS + + +Model = JAXModel_TPL_MODEL_NAME diff --git a/python/sdist/amici/jax/model.py b/python/sdist/amici/jax/model.py index a7b274027a..e037c44a2f 100644 --- a/python/sdist/amici/jax/model.py +++ b/python/sdist/amici/jax/model.py @@ -3,6 +3,7 @@ # ruff: noqa: F821 F722 from abc import abstractmethod +from pathlib import Path import diffrax import equinox as eqx @@ -18,8 +19,9 @@ class JAXModel(eqx.Module): classes inheriting from JAXModel. """ - MODEL_API_VERSION = "0.0.1" + MODEL_API_VERSION = "0.0.2" api_version: str + jax_py_file: Path def __init__(self): if self.api_version != self.MODEL_API_VERSION: diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index 6ddfb7c074..fc74a9f50f 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -1,7 +1,8 @@ """PEtab wrappers for JAX models.""" "" - +import shutil from numbers import Number from collections.abc import Iterable +from pathlib import Path import diffrax import equinox as eqx @@ -12,6 +13,7 @@ import pandas as pd import petab.v1 as petab +from amici import _module_from_path from amici.petab.parameter_mapping import ( ParameterMappingForCondition, create_parameter_mapping, @@ -84,6 +86,45 @@ def __init__(self, model: JAXModel, petab_problem: petab.Problem): self._measurements = self._get_measurements(scs) self.parameters = self._get_nominal_parameter_values() + def save(self, directory: Path): + """ + Save the problem to a file. + + :param directory: + Directory to save the problem to. + """ + self._petab_problem.to_files( + prefix_path=directory, + model_file="model", + condition_file="conditions.tsv", + measurement_file="measurements.tsv", + parameter_file="parameters.tsv", + observable_file="observables.tsv", + yaml_file="problem.yaml", + ) + shutil.copy(self.model.jax_py_file, directory / "jax_py_file.py") + with open(directory / "parameters.pkl", "wb") as f: + eqx.tree_serialise_leaves(f, self) + + @classmethod + def load(cls, directory: Path): + """ + Load a problem from a file. + + :param directory: + Directory to load the problem from. + + :return: + Loaded problem instance. + """ + petab_problem = petab.Problem.from_yaml( + directory / "problem.yaml", + ) + model = _module_from_path("jax", directory / "jax_py_file.py").Model() + problem = cls(model, petab_problem) + with open(directory / "parameters.pkl", "rb") as f: + return eqx.tree_deserialise_leaves(f, problem) + def _get_parameter_mappings( self, simulation_conditions: pd.DataFrame ) -> dict[str, ParameterMappingForCondition]: diff --git a/python/tests/test_jax.py b/python/tests/test_jax.py index 3254667c50..3055d77983 100644 --- a/python/tests/test_jax.py +++ b/python/tests/test_jax.py @@ -1,10 +1,12 @@ import pytest import amici +from pathlib import Path pytest.importorskip("jax") import amici.jax import jax.numpy as jnp +import jax.random as jr import jax import diffrax import numpy as np @@ -12,6 +14,8 @@ from amici.pysb_import import pysb2amici from amici.testing import TemporaryDirectoryWinSafe, skip_on_valgrind +from amici.petab.petab_import import import_petab_problem +from amici.jax import JAXProblem from numpy.testing import assert_allclose pysb = pytest.importorskip("pysb") @@ -222,3 +226,28 @@ def check_fields_jax( rtol=1e-5, err_msg=f"field {field} does not match", ) + + +@skip_on_valgrind +def test_serialisation(lotka_volterra): + petab_problem = lotka_volterra + with TemporaryDirectoryWinSafe( + prefix=petab_problem.model.model_id + ) as model_dir: + jax_model = import_petab_problem( + petab_problem, jax=True, model_output_dir=model_dir + ) + jax_problem = JAXProblem(jax_model, petab_problem) + # change parameters to random values to test serialisation + jax_problem.update_parameters( + jax_problem.parameters + + jr.normal(jr.PRNGKey(0), jax_problem.parameters.shape) + ) + + with TemporaryDirectoryWinSafe() as outdir: + outdir = Path(outdir) + jax_problem.save(outdir) + jax_problem_loaded = JAXProblem.load(outdir) + assert_allclose( + jax_problem.parameters, jax_problem_loaded.parameters + ) diff --git a/tests/benchmark-models/test_petab_benchmark.py b/tests/benchmark-models/test_petab_benchmark.py index 7a0afc6832..2c56089409 100644 --- a/tests/benchmark-models/test_petab_benchmark.py +++ b/tests/benchmark-models/test_petab_benchmark.py @@ -338,12 +338,6 @@ def test_jax_llh(benchmark_problem): jax=True, ) jax_problem = JAXProblem(jax_model, petab_problem) - simulation_conditions = ( - petab_problem.get_simulation_conditions_from_measurement_df() - ) - simulation_conditions = tuple( - tuple(row) for _, row in simulation_conditions.iterrows() - ) if problem_parameters: jax_problem = eqx.tree_at( lambda x: x.parameters, @@ -355,11 +349,9 @@ def test_jax_llh(benchmark_problem): if problem_id in problems_for_gradient_check_jax: (llh_jax, _), sllh_jax = eqx.filter_jit( eqx.filter_value_and_grad(run_simulations, has_aux=True) - )(jax_problem, simulation_conditions) + )(jax_problem) else: - llh_jax, _ = beartype(eqx.filter_jit(run_simulations))( - jax_problem, simulation_conditions - ) + llh_jax, _ = beartype(eqx.filter_jit(run_simulations))(jax_problem) np.testing.assert_allclose( llh_jax, From 9fd5835e0accc9c44b183f520528e59bebbd2172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Thu, 28 Nov 2024 20:49:55 +0000 Subject: [PATCH 02/32] doc --- python/sdist/amici/jax/petab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index fc74a9f50f..2c823259fe 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -88,7 +88,7 @@ def __init__(self, model: JAXModel, petab_problem: petab.Problem): def save(self, directory: Path): """ - Save the problem to a file. + Save the problem to a directory. :param directory: Directory to save the problem to. @@ -109,7 +109,7 @@ def save(self, directory: Path): @classmethod def load(cls, directory: Path): """ - Load a problem from a file. + Load a problem from a directory. :param directory: Directory to load the problem from. From 862586db6aba4015d674c78c265a7a4092010507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Thu, 28 Nov 2024 21:04:43 +0000 Subject: [PATCH 03/32] no compilation for jax --- python/sdist/amici/petab/petab_import.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/python/sdist/amici/petab/petab_import.py b/python/sdist/amici/petab/petab_import.py index 42a4d85dc4..87ec3fbfec 100644 --- a/python/sdist/amici/petab/petab_import.py +++ b/python/sdist/amici/petab/petab_import.py @@ -66,7 +66,8 @@ def import_petab_problem( parameters are required, this should be set to ``False``. :param jax: - Whether to load the jax version of the model. + Whether to load the jax version of the model. Note that this disables + compilation of the model module unless `compile` is set to `True`. :param kwargs: Additional keyword arguments to be passed to @@ -145,6 +146,7 @@ def import_petab_problem( petab_problem, model_name=model_name, model_output_dir=model_output_dir, + compile=kwargs.pop("compile", not jax), **kwargs, ) else: @@ -153,14 +155,19 @@ def import_petab_problem( model_name=model_name, model_output_dir=model_output_dir, non_estimated_parameters_as_constants=non_estimated_parameters_as_constants, + compile=kwargs.pop("compile", not jax), **kwargs, ) # import model - model_module = amici.import_model_module(model_name, model_output_dir) + if not jax: + model_module = amici.import_model_module(model_name, model_output_dir) - if jax: - model = model_module.get_jax_model() + else: + jax_model_module = amici._module_from_path( + "jax", Path(model_output_dir) / model_name / "jax.py" + ) + model = jax_model_module.Model() logger.info( f"Successfully loaded jax model {model_name} " From 674c48101cc8aecd77a7729cc0974301f98dd01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Thu, 28 Nov 2024 21:09:09 +0000 Subject: [PATCH 04/32] bad ruff --- python/sdist/amici/jax.template.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/sdist/amici/jax.template.py b/python/sdist/amici/jax.template.py index eda47b4f09..9b566281ca 100644 --- a/python/sdist/amici/jax.template.py +++ b/python/sdist/amici/jax.template.py @@ -1,9 +1,10 @@ +# ruff: noqa: F401, F821, F841 +import jax.numpy as jnp +from interpax import interp1d from pathlib import Path from amici.jax.model import JAXModel -# ruff: noqa: F821, F841 - class JAXModel_TPL_MODEL_NAME(JAXModel): api_version = TPL_MODEL_API_VERSION From 3e7d453ce94da3b2a1d890d50a38a59e2347c557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Thu, 28 Nov 2024 21:43:03 +0000 Subject: [PATCH 05/32] Update ExampleJaxPEtab.ipynb --- python/examples/example_jax_petab/ExampleJaxPEtab.ipynb | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb b/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb index 10369f74b0..89a47e8ed3 100644 --- a/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb +++ b/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb @@ -52,7 +52,6 @@ "# Import the PEtab problem as a JAX-compatible AMICI model\n", "jax_model = import_petab_problem(\n", " petab_problem,\n", - " compile_=True, # do not compile regular amici model\n", " verbose=False, # no text output\n", " jax=True, # return jax model\n", ")" @@ -978,7 +977,6 @@ "# Import the PEtab problem as a standard AMICI model\n", "amici_model = import_petab_problem(\n", " petab_problem,\n", - " compile_=False, # do not recompile\n", " verbose=False,\n", " jax=False, # load the amici model this time\n", ")\n", From b2a95b13399a05296258c4a4daa5b03e2d9f8877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Thu, 28 Nov 2024 21:45:13 +0000 Subject: [PATCH 06/32] bad ruff --- python/tests/test_jax.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/tests/test_jax.py b/python/tests/test_jax.py index 3055d77983..30e205ca26 100644 --- a/python/tests/test_jax.py +++ b/python/tests/test_jax.py @@ -17,6 +17,7 @@ from amici.petab.petab_import import import_petab_problem from amici.jax import JAXProblem from numpy.testing import assert_allclose +from test_petab_objective import lotka_volterra # noqa: F401 pysb = pytest.importorskip("pysb") @@ -229,7 +230,7 @@ def check_fields_jax( @skip_on_valgrind -def test_serialisation(lotka_volterra): +def test_serialisation(lotka_volterra): # noqa: F811 petab_problem = lotka_volterra with TemporaryDirectoryWinSafe( prefix=petab_problem.model.model_id From 8b713e6a516cb29c56d82687bbdb4f3db6c0027d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Thu, 28 Nov 2024 22:18:24 +0000 Subject: [PATCH 07/32] Update ExampleJaxPEtab.ipynb --- .../example_jax_petab/ExampleJaxPEtab.ipynb | 753 ++++-------------- 1 file changed, 133 insertions(+), 620 deletions(-) diff --git a/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb b/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb index 89a47e8ed3..855860e242 100644 --- a/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb +++ b/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb @@ -25,16 +25,10 @@ ] }, { + "metadata": {}, "cell_type": "code", - "execution_count": 1, - "id": "6ada3fb8", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:50:53.712145Z", - "start_time": "2024-11-19T09:50:47.191184Z" - } - }, "outputs": [], + "execution_count": null, "source": [ "from amici.petab.petab_import import import_petab_problem\n", "import petab.v1 as petab\n", @@ -55,29 +49,24 @@ " verbose=False, # no text output\n", " jax=True, # return jax model\n", ")" - ] + ], + "id": "c71c96da0da3144a" }, { - "cell_type": "markdown", - "id": "5258566d99c89ba4", "metadata": {}, + "cell_type": "markdown", "source": [ "## Simulation\n", "\n", "In principle, we can already use this model for simulation using the [simulate_condition](https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.JAXModel.simulate_condition) method. However, this approach can be cumbersome as timepoints, data etc. need to be specified manually. Instead, we process the PEtab problem into a [JAXProblem](https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.JAXProblem), which enables efficient simulation using [amici.jax.run_simulations]((https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.run_simulations)." - ] + ], + "id": "7e0f1c27bd71ee1f" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 2, - "id": "76c1331372cd51b4", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:50:56.042924Z", - "start_time": "2024-11-19T09:50:53.718372Z" - } - }, "outputs": [], + "execution_count": null, "source": [ "from amici.jax import JAXProblem, run_simulations\n", "\n", @@ -86,294 +75,44 @@ "\n", "# Run simulations and compute the log-likelihood\n", "llh, results = run_simulations(jax_problem)" - ] + ], + "id": "ccecc9a29acc7b73" }, { - "cell_type": "markdown", - "id": "5f8684d76368bd76", "metadata": {}, - "source": "This simulates the model for all conditions using the nominal parameter values. Simple, right? Now, let’s take a look at the simulation results." + "cell_type": "markdown", + "source": "This simulates the model for all conditions using the nominal parameter values. Simple, right? Now, let’s take a look at the simulation results.", + "id": "415962751301c64a" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 3, - "id": "2fc284bd3bfb3a62", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:50:56.141898Z", - "start_time": "2024-11-19T09:50:56.134945Z" - }, - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(Array(nan, dtype=float32),\n", - " {'stats_dyn': {'max_steps': 1024,\n", - " 'num_accepted_steps': Array(778, dtype=int32, weak_type=True),\n", - " 'num_rejected_steps': Array(246, dtype=int32, weak_type=True),\n", - " 'num_steps': Array(1024, dtype=int32, weak_type=True)},\n", - " 'stats_posteq': None,\n", - " 'stats_preeq': None,\n", - " 'ts': Array([ 0. , 0. , 0. , 2.5, 2.5, 2.5, 5. , 5. , 5. ,\n", - " 10. , 10. , 10. , 15. , 15. , 15. , 20. , 20. , 20. ,\n", - " 30. , 30. , 30. , 40. , 40. , 40. , 50. , 50. , 50. ,\n", - " 60. , 60. , 60. , 80. , 80. , 80. , 100. , 100. , 100. ,\n", - " 120. , 120. , 120. , 160. , 160. , 160. , 200. , 200. , 200. ,\n", - " 240. , 240. , 240. ], dtype=float32),\n", - " 'x': Array([[143.8668, 63.7332, 0. , 0. , 0. , 0. ,\n", - " 0. , 0. ],\n", - " [143.8668, 63.7332, 0. , 0. , 0. , 0. ,\n", - " 0. , 0. ],\n", - " [143.8668, 63.7332, 0. , 0. , 0. , 0. ,\n", - " 0. , 0. ],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf],\n", - " [ inf, inf, inf, inf, inf, inf,\n", - " inf, inf]], dtype=float32)})" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], + "execution_count": null, "source": [ "# Define the simulation condition\n", "simulation_condition = (\"model1_data1\",)\n", "\n", "# Access the results for the specified condition\n", "results[simulation_condition]" - ] + ], + "id": "596b86e45e18fe3d" }, { - "cell_type": "markdown", - "id": "aa46125e508d38d3", "metadata": {}, + "cell_type": "markdown", "source": [ "Unfortunately, the simulation failed! As seen in the output, the simulation broke down after the initial timepoint, indicated by the `inf` values in the state variables `results[simulation_condition][1].x` and the `nan` likelihood value. A closer inspection of this variable provides additional clues about what might have gone wrong.\n", "\n", "The issue stems from using single precision, as indicated by the `float32` dtype of state variables. Single precision is generally a [bad idea](https://docs.kidger.site/diffrax/examples/stiff_ode/) for stiff systems like the Böhm model. Let’s retry the simulation with double precision." - ] + ], + "id": "a1b173e013f9210a" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 4, - "id": "8e5006774534ba3a", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:50:58.227222Z", - "start_time": "2024-11-19T09:50:56.235939Z" - }, - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/plain": [ - "{('model1_data1',): (Array(-138.22199834, dtype=float64),\n", - " {'stats_dyn': {'max_steps': 1024,\n", - " 'num_accepted_steps': Array(125, dtype=int64, weak_type=True),\n", - " 'num_rejected_steps': Array(7, dtype=int64, weak_type=True),\n", - " 'num_steps': Array(132, dtype=int64, weak_type=True)},\n", - " 'stats_posteq': None,\n", - " 'stats_preeq': None,\n", - " 'ts': Array([ 0. , 0. , 0. , 2.5, 2.5, 2.5, 5. , 5. , 5. ,\n", - " 10. , 10. , 10. , 15. , 15. , 15. , 20. , 20. , 20. ,\n", - " 30. , 30. , 30. , 40. , 40. , 40. , 50. , 50. , 50. ,\n", - " 60. , 60. , 60. , 80. , 80. , 80. , 100. , 100. , 100. ,\n", - " 120. , 120. , 120. , 160. , 160. , 160. , 200. , 200. , 200. ,\n", - " 240. , 240. , 240. ], dtype=float64),\n", - " 'x': Array([[1.43866806e+02, 6.37332001e+01, 0.00000000e+00, 0.00000000e+00,\n", - " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00],\n", - " [1.43866806e+02, 6.37332001e+01, 0.00000000e+00, 0.00000000e+00,\n", - " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00],\n", - " [1.43866806e+02, 6.37332001e+01, 0.00000000e+00, 0.00000000e+00,\n", - " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00],\n", - " [5.34614747e+01, 2.88662915e+01, 1.73038463e+01, 5.38666098e-05,\n", - " 1.57043241e-05, 1.12989551e+02, 1.44740461e+00, 2.65965680e+01],\n", - " [5.34614747e+01, 2.88662915e+01, 1.73038463e+01, 5.38666098e-05,\n", - " 1.57043241e-05, 1.12989551e+02, 1.44740461e+00, 2.65965680e+01],\n", - " [5.34614747e+01, 2.88662915e+01, 1.73038463e+01, 5.38666098e-05,\n", - " 1.57043241e-05, 1.12989551e+02, 1.44740461e+00, 2.65965680e+01],\n", - " [3.40645243e+01, 1.96396741e+01, 2.10101056e+01, 2.04431389e-05,\n", - " 6.79533169e-06, 1.36155797e+02, 3.93060446e+00, 3.39422194e+01],\n", - " [3.40645243e+01, 1.96396741e+01, 2.10101056e+01, 2.04431389e-05,\n", - " 6.79533169e-06, 1.36155797e+02, 3.93060446e+00, 3.39422194e+01],\n", - " [3.40645243e+01, 1.96396741e+01, 2.10101056e+01, 2.04431389e-05,\n", - " 6.79533169e-06, 1.36155797e+02, 3.93060446e+00, 3.39422194e+01],\n", - " [2.17740069e+01, 1.28936829e+01, 2.26400305e+01, 7.29828626e-06,\n", - " 2.55916689e-06, 1.49922977e+02, 9.56261350e+00, 3.90845534e+01],\n", - " [2.17740069e+01, 1.28936829e+01, 2.26400305e+01, 7.29828626e-06,\n", - " 2.55916689e-06, 1.49922977e+02, 9.56261350e+00, 3.90845534e+01],\n", - " [2.17740069e+01, 1.28936829e+01, 2.26400305e+01, 7.29828626e-06,\n", - " 2.55916689e-06, 1.49922977e+02, 9.56261350e+00, 3.90845534e+01],\n", - " [1.78289538e+01, 1.02603483e+01, 2.23703281e+01, 4.27571773e-06,\n", - " 1.41605997e-06, 1.53605377e+02, 1.53104054e+01, 4.07264964e+01],\n", - " [1.78289538e+01, 1.02603483e+01, 2.23703281e+01, 4.27571773e-06,\n", - " 1.41605997e-06, 1.53605377e+02, 1.53104054e+01, 4.07264964e+01],\n", - " [1.78289538e+01, 1.02603483e+01, 2.23703281e+01, 4.27571773e-06,\n", - " 1.41605997e-06, 1.53605377e+02, 1.53104054e+01, 4.07264964e+01],\n", - " [1.63397301e+01, 8.95194886e+00, 2.15687556e+01, 3.13802765e-06,\n", - " 9.41897178e-07, 1.54369347e+02, 2.09093940e+01, 4.12091821e+01],\n", - " [1.63397301e+01, 8.95194886e+00, 2.15687556e+01, 3.13802765e-06,\n", - " 9.41897178e-07, 1.54369347e+02, 2.09093940e+01, 4.12091821e+01],\n", - " [1.63397301e+01, 8.95194886e+00, 2.15687556e+01, 3.13802765e-06,\n", - " 9.41897178e-07, 1.54369347e+02, 2.09093940e+01, 4.12091821e+01],\n", - " [1.59598663e+01, 7.84978463e+00, 1.95400559e+01, 2.28580865e-06,\n", - " 5.52965361e-07, 1.52878988e+02, 3.13834269e+01, 4.08423997e+01],\n", - " [1.59598663e+01, 7.84978463e+00, 1.95400559e+01, 2.28580865e-06,\n", - " 5.52965361e-07, 1.52878988e+02, 3.13834269e+01, 4.08423997e+01],\n", - " [1.59598663e+01, 7.84978463e+00, 1.95400559e+01, 2.28580865e-06,\n", - " 5.52965361e-07, 1.52878988e+02, 3.13834269e+01, 4.08423997e+01],\n", - " [1.68960409e+01, 7.57954992e+00, 1.74766781e+01, 1.95598628e-06,\n", - " 3.93623013e-07, 1.49923893e+02, 4.08004734e+01, 3.97639408e+01],\n", - " [1.68960409e+01, 7.57954992e+00, 1.74766781e+01, 1.95598628e-06,\n", - " 3.93623013e-07, 1.49923893e+02, 4.08004734e+01, 3.97639408e+01],\n", - " [1.68960409e+01, 7.57954992e+00, 1.74766781e+01, 1.95598628e-06,\n", - " 3.93623013e-07, 1.49923893e+02, 4.08004734e+01, 3.97639408e+01],\n", - " [1.83667585e+01, 7.66955396e+00, 1.55594015e+01, 1.76473276e-06,\n", - " 3.07719966e-07, 1.46418868e+02, 4.91998176e+01, 3.84066930e+01],\n", - " [1.83667585e+01, 7.66955396e+00, 1.55594015e+01, 1.76473276e-06,\n", - " 3.07719966e-07, 1.46418868e+02, 4.91998176e+01, 3.84066930e+01],\n", - " [1.83667585e+01, 7.66955396e+00, 1.55594015e+01, 1.76473276e-06,\n", - " 3.07719966e-07, 1.46418868e+02, 4.91998176e+01, 3.84066930e+01],\n", - " [2.01288255e+01, 7.95104827e+00, 1.38272785e+01, 1.61833093e-06,\n", - " 2.52512177e-07, 1.42637837e+02, 5.66687226e+01, 3.69287741e+01],\n", - " [2.01288255e+01, 7.95104827e+00, 1.38272785e+01, 1.61833093e-06,\n", - " 2.52512177e-07, 1.42637837e+02, 5.66687226e+01, 3.69287741e+01],\n", - " [2.01288255e+01, 7.95104827e+00, 1.38272785e+01, 1.61833093e-06,\n", - " 2.52512177e-07, 1.42637837e+02, 5.66687226e+01, 3.69287741e+01],\n", - " [2.42069672e+01, 8.82343809e+00, 1.09015504e+01, 1.36440625e-06,\n", - " 1.81275253e-07, 1.34584160e+02, 6.91907904e+01, 3.38618223e+01],\n", - " [2.42069672e+01, 8.82343809e+00, 1.09015504e+01, 1.36440625e-06,\n", - " 1.81275253e-07, 1.34584160e+02, 6.91907904e+01, 3.38618223e+01],\n", - " [2.42069672e+01, 8.82343809e+00, 1.09015504e+01, 1.36440625e-06,\n", - " 1.81275253e-07, 1.34584160e+02, 6.91907904e+01, 3.38618223e+01],\n", - " [2.88236929e+01, 9.92100237e+00, 8.58815552e+00, 1.12770626e-06,\n", - " 1.33599425e-07, 1.26069389e+02, 7.90544164e+01, 3.08213014e+01],\n", - " [2.88236929e+01, 9.92100237e+00, 8.58815552e+00, 1.12770626e-06,\n", - " 1.33599425e-07, 1.26069389e+02, 7.90544164e+01, 3.08213014e+01],\n", - " [2.88236929e+01, 9.92100237e+00, 8.58815552e+00, 1.12770626e-06,\n", - " 1.33599425e-07, 1.26069389e+02, 7.90544164e+01, 3.08213014e+01],\n", - " [3.38427746e+01, 1.11365012e+01, 6.75633027e+00, 9.06279023e-07,\n", - " 9.81352036e-08, 1.17230823e+02, 8.68156402e+01, 2.78994196e+01],\n", - " [3.38427746e+01, 1.11365012e+01, 6.75633027e+00, 9.06279023e-07,\n", - " 9.81352036e-08, 1.17230823e+02, 8.68156402e+01, 2.78994196e+01],\n", - " [3.38427746e+01, 1.11365012e+01, 6.75633027e+00, 9.06279023e-07,\n", - " 9.81352036e-08, 1.17230823e+02, 8.68156402e+01, 2.78994196e+01],\n", - " [4.45767678e+01, 1.36929100e+01, 4.13936161e+00, 5.34332520e-07,\n", - " 5.04178629e-08, 9.91750041e+01, 9.76743159e+01, 2.25642862e+01],\n", - " [4.45767678e+01, 1.36929100e+01, 4.13936161e+00, 5.34332520e-07,\n", - " 5.04178629e-08, 9.91750041e+01, 9.76743159e+01, 2.25642862e+01],\n", - " [4.45767678e+01, 1.36929100e+01, 4.13936161e+00, 5.34332520e-07,\n", - " 5.04178629e-08, 9.91750041e+01, 9.76743159e+01, 2.25642862e+01],\n", - " [5.53512751e+01, 1.61684905e+01, 2.47997315e+00, 2.79973425e-07,\n", - " 2.38894456e-08, 8.17101310e+01, 1.04245916e+02, 1.80088542e+01],\n", - " [5.53512751e+01, 1.61684905e+01, 2.47997315e+00, 2.79973425e-07,\n", - " 2.38894456e-08, 8.17101310e+01, 1.04245916e+02, 1.80088542e+01],\n", - " [5.53512751e+01, 1.61684905e+01, 2.47997315e+00, 2.79973425e-07,\n", - " 2.38894456e-08, 8.17101310e+01, 1.04245916e+02, 1.80088542e+01],\n", - " [6.52754860e+01, 1.83796881e+01, 1.44531833e+00, 1.32320205e-07,\n", - " 1.04906457e-08, 6.59469727e+01, 1.08115837e+02, 1.42437160e+01],\n", - " [6.52754860e+01, 1.83796881e+01, 1.44531833e+00, 1.32320205e-07,\n", - " 1.04906457e-08, 6.59469727e+01, 1.08115837e+02, 1.42437160e+01],\n", - " [6.52754860e+01, 1.83796881e+01, 1.44531833e+00, 1.32320205e-07,\n", - " 1.04906457e-08, 6.59469727e+01, 1.08115837e+02, 1.42437160e+01]], dtype=float64)})}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], + "execution_count": null, "source": [ "import jax\n", "\n", @@ -384,37 +123,20 @@ "llh, results = run_simulations(jax_problem)\n", "\n", "results" - ] + ], + "id": "f4f5ff705a3f7402" }, { - "cell_type": "markdown", - "id": "fea37568206351f7", "metadata": {}, - "source": "Success! The simulation completed successfully, and we can now plot the resulting state trajectories." + "cell_type": "markdown", + "source": "Success! The simulation completed successfully, and we can now plot the resulting state trajectories.", + "id": "fe4d3b40ee3efdf2" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 5, - "id": "95c75d098d3a1822", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:50:58.490052Z", - "start_time": "2024-11-19T09:50:58.305876Z" - }, - "scrolled": true - }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAsAAAAIjCAYAAAAN/63DAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAD+cUlEQVR4nOzdd1iTZ/fA8W8We4migAouREERRWW4cO9tHa1trVa7tO1rWzut47W1te1bW7Xtr4qzdVTrqKPuraCC4t6CE9yIyApJfn8gKSmooEAY53NdXJInT57nJA+B451zn1thMBgMCCGEEEIIUUYozR2AEEIIIYQQRUkSYCGEEEIIUaZIAiyEEEIIIcoUSYCFEEIIIUSZIgmwEEIIIYQoUyQBFkIIIYQQZYokwEIIIYQQokyRBFgIIYQQQpQpkgALIYQQQogyRRJgIUSJN2XKFOrUqYNerzd3KLmKjY1FoVAwd+7cfD92+/btKBQKtm/fXuBxPU5oaCihoaFFes7CFBQUxJgxY8wdhhCimJAEWAhRoiUmJvL111/z4YcfolRm/kp72mSzpDh9+jT/+c9/CAkJwcrKCoVCQWxsrLnDAiA5OZnx48c/U8KelJTEuHHj6NSpE87Ozo+9nqGhoQwZMuSJx/zwww+ZMWMG8fHxTx2XEKL0kARYCFGizZ49m4yMDAYNGmTuUIpMeHg4P/74I/fv36du3brmDsdEcnIyEyZMeKYE+NatW0ycOJGTJ0/SoEGDAomrZ8+eODg48NNPPxXI8YQQJZskwEKIEm3OnDn06NEDKysrc4dSZHr06EFCQgJHjx7lhRdeMHc4Bc7NzY24uDguXrzIN998UyDHVCqV9OvXj/nz52MwGArkmEKIkksSYCFEiRUTE8ORI0do167dY/cbP348CoWCM2fOMHjwYBwdHXFxcWHs2LEYDAYuX75sHCF0dXXlu+++y3GMGzduMGzYMCpVqoSVlRUNGjRg3rx5OfZLSEhgyJAhODo64uTkxMsvv0xCQkKucZ06dYp+/frh7OyMlZUVjRs35q+//nri83Z2dsbe3v6J++XVr7/+Ss2aNbG2tqZp06bs2rUrxz7p6el8/vnnBAQE4OjoiK2tLS1atGDbtm3GfWJjY3FxcQFgwoQJKBQKFAoF48ePB+DIkSMMGTKEGjVqYGVlhaurK0OHDuX27dsm57K0tMTV1bXAnl+W9u3bc/HiRaKjowv82EKIkkUSYCFEibV3714AGjVqlKf9BwwYgF6v56uvviIwMJBJkyYxdepU2rdvT+XKlfn666+pVasW77//Pjt37jQ+LiUlhdDQUBYsWMALL7zAN998g6OjI0OGDOGHH34w7mcwGOjZsycLFixg8ODBTJo0iStXrvDyyy/niOX48eMEBQVx8uRJPvroI7777jtsbW3p1asXK1aseMZXJu/CwsJ47bXXcHV1ZcqUKTRr1owePXpw+fJlk/0SExOZNWsWoaGhfP3114wfP56bN2/SsWNHY0Lp4uLCzz//DEDv3r1ZsGABCxYsoE+fPgBs2rSJCxcu8MorrzBt2jQGDhzI4sWL6dKlS5GMygYEBACwZ8+eQj+XEKKYMwghRAn12WefGQDD/fv3H7vfuHHjDIBhxIgRxm0ZGRmGKlWqGBQKheGrr74ybr97967B2tra8PLLLxu3TZ061QAYfvvtN+O29PR0Q3BwsMHOzs6QmJhoMBgMhpUrVxoAw5QpU0zO06JFCwNgmDNnjnF727ZtDfXr1zekpqYat+n1ekNISIjBy8vLuG3btm0GwLBt27Zcn9s333xjAAwxMTGPfQ1yk56ebqhYsaLB39/fkJaWZtz+66+/GgBDq1atTJ5H9n0MhszXqlKlSoahQ4cat928edMAGMaNG5fjfMnJyTm2LVq0yAAYdu7cmWuMBw4cyPHaPQsLCwvDG2+8USDHEkKUXDICLIQosW7fvo1arcbOzi5P+7/66qvG71UqFY0bN8ZgMDBs2DDjdicnJ7y9vblw4YJx27p163B1dTWZaKfRaHj77bdJSkpix44dxv3UajVvvPGGyXlGjRplEsedO3fYunUr/fv35/79+9y6dYtbt25x+/ZtOnbsyNmzZ7l69Wr+XoynEBkZyY0bN3j99dexsLAwbs8q4chOpVIZ99Hr9dy5c4eMjAwaN27MwYMH83Q+a2tr4/epqancunWLoKAggDwf41mVK1eOW7duFcm5hBDFl9rcAQghRFHx8PAwue3o6IiVlRUVKlTIsT17XerFixfx8vIytlnLktWB4eLFi8Z/3dzcciTk3t7eJrfPnTuHwWBg7NixjB07NtdYb9y4QeXKlfPx7PIvK24vLy+T7RqNhho1auTYf968eXz33XecOnUKrVZr3F69evU8ne/OnTtMmDCBxYsXc+PGDZP77t27l9/wn4rBYEChUBTJuYQQxZckwEKIEqt8+fJkZGRw//79PE0KU6lUedoGFGpNataCHe+//z4dO3bMdZ9atWoV2vmfxm+//caQIUPo1asXH3zwARUrVkSlUjF58mTOnz+fp2P079+fvXv38sEHH+Dv74+dnR16vZ5OnToV2SImCQkJOf7DI4QoeyQBFkKUWHXq1AEyu0H4+fkV2nk8PT05cuQIer3eZBT41KlTxvuz/t2yZQtJSUkmo8CnT582OV7W6KpGo3liB4vClBX32bNnadOmjXG7VqslJibGpAfvsmXLqFGjBsuXLzcZQR03bpzJMR81unr37l22bNnChAkT+Pzzz43bz549WyDPJS+uXr1Kenp6seudLIQoelIDLIQosYKDg4HMWtbC1KVLF+Lj41myZIlxW0ZGBtOmTcPOzo5WrVoZ98vIyDB2QgDQ6XRMmzbN5HgVK1YkNDSU//u//yMuLi7H+W7evFlIz8RU48aNcXFx4ZdffiE9Pd24fe7cuTlat2WNlGcfGd+3bx/h4eEm+9nY2ADk6fEAU6dOfZankC9RUVEAhISEFNk5hRDFk4wACyFKrBo1alCvXj02b97M0KFDC+08I0aM4P/+7/8YMmQIUVFRVKtWjWXLlrFnzx6mTp1qLL/o3r07zZo146OPPiI2NhYfHx+WL1+ea33rjBkzaN68OfXr12f48OHUqFGD69evEx4ezpUrVzh8+PAj47l3754xqc5q6TV9+nScnJxwcnJi5MiReXpeGo2GSZMm8dprr9GmTRsGDBhATEwMc+bMyVED3K1bN5YvX07v3r3p2rUrMTEx/PLLL/j4+JCUlGTcz9raGh8fH5YsWULt2rVxdnamXr161KtXj5YtWzJlyhS0Wi2VK1dm48aNxMTE5Brb9OnTSUhI4Nq1awCsXr2aK1euADBq1Kgck/SyCw0NZceOHTmS7U2bNuHh4UHDhg3z9PoIIUoxM3agEEKIZ/a///3PYGdnl2uLrSxZbdBu3rxpsv3ll1822Nra5ti/VatWBl9fX5Nt169fN7zyyiuGChUqGCwsLAz169fPtTXX7du3DS+++KLBwcHB4OjoaHjxxRcNhw4dyrWV1/nz5w0vvfSSwdXV1aDRaAyVK1c2dOvWzbBs2TLjPrm1QYuJiTEAuX55eno++sV6hJ9++slQvXp1g6WlpaFx48aGnTt3Glq1amXSBk2v1xu+/PJLg6enp8HS0tLQsGFDw5o1awwvv/xyjnPu3bvXEBAQYLCwsDBpiXblyhVD7969DU5OTgZHR0fDc889Z7h27VqubdM8PT0f+Ryf1PItICDA4OrqarJNp9MZ3NzcDJ999lm+Xx8hROmjMBhkTUghRMl17949atSowZQpU0zamYmy6f79+zg7OzN16lTeeust4/aVK1fy/PPPc/78edzc3MwYoRCiOJAaYCFEiebo6MiYMWP45ptviqyTgCi+du7cSeXKlRk+fLjJ9q+//pqRI0dK8iuEAEBGgIUQohS6c+eOycS2f1OpVLi4uBRhREIIUXxIAiyEEKVQ1kSwR/H09CQ2NrboAhJCiGJEEmAhhCiFoqKiuHv37iPvt7a2plmzZkUYkRBCFB+SAAshhBBCiDJFJsEJIYQQQogyRRbCAPR6PdeuXcPe3v6Ry3gKIYQQQgjzMRgM3L9/H3d3d5Nl6Z+GJMDAtWvXqFq1qrnDEEIIIYQQT3D58mWqVKnyTMeQBBiMy5hevnwZBweHQj+fVqtl48aNdOjQAY1GU+jnE4VPrmnpJNe19JFrWjrJdS19crumiYmJVK1a1Zi3PQuzJsA7d+7km2++ISoqiri4OFasWEGvXr1M9jl58iQffvghO3bsICMjAx8fH/788088PDwASE1N5b333mPx4sWkpaXRsWNHfvrpJypVqpTnOLLKHhwcHIosAbaxscHBwUHeqKWEXNPSSa5r6SPXtHSS61r6PO6aFkS5qlknwT148IAGDRowY8aMXO8/f/48zZs3p06dOmzfvp0jR44wduxYrKysjPv85z//YfXq1SxdupQdO3Zw7do1+vTpU1RPQQghhBBClDBmHQHu3LkznTt3fuT9n376KV26dGHKlCnGbTVr1jR+f+/ePcLCwli4cCFt2rQBYM6cOdStW5eIiAiCgoJyPW5aWhppaWnG24mJiUDm/za0Wu0zPae8yDpHUZxLFA25pqWTXNfSR65p6STXtfTJ7ZoW5PUtNn2AFQqFSQmEXq/H0dGRMWPGsHv3bg4dOkT16tX5+OOPjfts3bqVtm3bcvfuXZycnIzH8vT05N133+U///lPrucaP348EyZMyLF94cKF2NjYFPRTE0IIIYQQzyg5OZnnn3+ee/fuPXPJarGdBHfjxg2SkpL46quvmDRpEl9//TXr16+nT58+bNu2jVatWhEfH4+FhYVJ8gtQqVIl4uPjH3nsjz/+mNGjRxtvZxVVd+jQochqgDdt2kT79u2lVqmUkGtaOsl1LX3kmpZOuV1XnU5HRkYGxWScTzyBQqFApVKhUqlQKBS5XtOsT+wLQrFNgPV6PQA9e/Y0juT6+/uzd+9efvnlF1q1avXUx7a0tMTS0jLHdo1GU6S/EIv6fKLwyTUtneS6lj5yTUunrOualJTElStXJPktgWxsbHBzczO+P7O/VwvyPVtsE+AKFSqgVqvx8fEx2V63bl12794NgKurK+np6SQkJJiMAl+/fh1XV9eiDFcIIYQQxYBOp+PKlSvY2Njg4uIiC1yVEAaDgfT0dG7evElMTAzVqlUr1PMV2wTYwsKCJk2acPr0aZPtZ86cwdPTE4CAgAA0Gg1btmyhb9++AJw+fZpLly4RHBxc5DELIYQQwry0Wi0GgwEXFxesra3NHY7IB2trazQaDRcvXiz0CY1mTYCTkpI4d+6c8XZMTAzR0dE4Ozvj4eHBBx98wIABA2jZsiWtW7dm/fr1rF69mu3btwPg6OjIsGHDGD16NM7Ozjg4ODBq1CiCg4Mf2QFCCCGEEKWfjPyWTFlLHBd2+YpZE+DIyEhat25tvJ01Me3ll19m7ty59O7dm19++YXJkyfz9ttv4+3tzZ9//knz5s2Nj/n+++9RKpX07dvXZCEMIYQQQgghcmPWBDg0NPSJGf7QoUMZOnToI++3srJixowZj1xMQwghhBBCiOzMuhKcEEIIIYQQRU0SYCGEEEKIYuDmzZu88cYbeHh4YGlpiaurKx07duSLL75AoVA89itrftSVK1ewsLCgXr16xuOOHz/+iY9/1H516tTJNdbJkyejUqn45ptvCv11KQySAAshhBBCFAN9+/bl0KFDzJs3jzNnzvDXX38RGhpK/fr1iYuLM37179+fTp06mWwLCQkBYO7cufTv35/ExET27dsHwPvvv2+yb5UqVZg4caLJtiy+vr4m27Naz/7b7NmzGTNmDLNnzy78F6YQFNs2aEIIIYQQz8pgMJCi1Znl3NYaVZ67USQkJLBr1y62b99uXOzL09OTpk2b5jyutTVpaWk51jwwGAzMmTOHn376iSpVqhAWFkZgYCB2dnbY2dkZ91OpVNjb2+e6ZoJarX7iWgo7duwgJSWFiRMnMn/+fPbu3WtMwEsKSYCFEEIIUWqlaHX4fL7BLOc+MbEjNhZ5S7WyktSVK1cSFBSU64q1T7Jt2zaSk5Np164dlStXJiQkhO+//x5bW9s8H+Ps2bO4u7tjZWVFcHAwkydPxsPDw2SfsLAwBg0ahEajYdCgQYSFhZW4BFhKIIQQQgghzEytVjN37lzmzZuHk5MTzZo145NPPuHIkSN5PkZYWBgDBw5EpVJRr149atSowdKlS/P8+MDAQObOncv69ev5+eefiYmJoUWLFty/f9+4T2JiIsuWLWPw4MEADB48mD/++IOkpKS8P9liQEaARZ4l30vg1uWLKFUqKlSthlW2j1OEEEKI4shao+LExI5mO3d+9O3bl65du7Jr1y4iIiL4+++/mTJlCrNmzWLIkCGPfWxCQgLLly83qdkdPHgwYWFhT3xsls6dOxu/9/PzIzAwEE9PT/744w+GDRsGwKJFi6hZsyYNGjQAwN/fH09PT5YsWWLcpySQBFjkoE1L5faVy9y6FMvNS7HcuhTLrcsXSb6XYLKfXTlnKnhUo3xVTyo8/CpfpSoaSyvzBC6EEEL8i0KhyHMZQnFgZWVF+/btad++PWPHjuXVV19l3LhxT0xiFy5cSGpqKoGBgcZtBoMBvV7PmTNnqF27dr5jcXJyonbt2iar9oaFhXH8+HHU6n9eU71ez+zZsyUBFiVHWnIyl45Fc/NiLLcux3Lr0kXuxl+D3BYoUShwquiKTpfB/Vs3Sbp7h6S7d4g9fNB0n0qu/yTED/8t51YZlVp+3IQQQoj88PHxYeXKlU/cLywsjPfeey9Hovzmm28ye/Zsvvrqq3yfOykpifPnz/Piiy8CcPToUSIjI9m+fTvOzs7G/e7cuUNoaCinTp16ZNu04kYykjLsRuwFVkyZSNLtWznus7Z3oIJHNVw8qlHBoxoVPDypUMUTjVXm6G5a8gNuX7nErcsXuXXp4sN/Y0m5n0hCfBwJ8XGcOxBhPJ5Spca5chWTxNjFwxOHChVRKKUUXQghRNl2+/ZtnnvuOYYOHYqfnx/29vZERkYyZcoUevbs+djHRkdHc/DgQX7//fccCeigQYOYOHEikyZNMhm1zc37779P9+7d8fT05Nq1a4wbNw6VSsWgQYOAzCS7adOmtGzZMsdjmzRpQlhYWInpCywJcBl14eAB1vwwBW1qCvblXfCo55eZnHpUw8WzOjaOTo9t3WJpY4t77bq4165rsj2rTjgrIc78/hLa1JTM25diTfbXWFpRvqqHMTF28axO5To+qNSawnjaQgghRLFkZ2dHYGAg33//PefPn0er1VK1alWGDx/OJ5988tjHhoWF4ePjk+voa+/evRk5ciTr1q2jR48ejz3OlStXGDRoELdv38bFxYXmzZsTERGBi4sL6enp/Pbbb3z44Ye5PrZv37589913fPnll2g0xf9vuCTAZdDBv1ezfd5MDAY9HvX86P6fTwpsQpuNoxMejk541Gtg3GYwGLh/62aOxPjO1cto01KJP3eG+HNnjPtb2dpRq2kI3iEt8PD1Q6nK3yQCIYQQoqSxtLRk8uTJTJ48+Yn7zp071+T2tGnTHrmvq6srOp1pH+TY2Nhc9128ePEjj2NhYcGtWzk/Mc4yZswYxowZ88j7ixtJgMsQvU7Htnkzid6wBoB6rTvQ7tU3C702V6FQ4OBSEQeXitRo1MQknrvx17htTIwvcu3MSR4k3OXYto0c27YRawdHagc2o05ISyrX8ZFyCSGEEEI8M0mAy4j0lGTW/DCFmEORALR4fghNevTN8wo1hUGpUlG+clXKV65K7aDmAOj1Oq6ePM7p8F2cidhDSuI9Dm9ax+FN67BzLk/toObUCWmJa63aZo1dCCGEECWXJMBlQOKtm6z8egI3L8WitrCky8j38Aosniu2KJUqqvr6UdXXj9ZDXuPyscOcCt/Fuf3hJN25zcF1qzi4bhUOLpXwDmlBnZCWuHhWl2RYCCGEEHkmCXApF3/+LCunTORBwl1sncrR64OxuNbKfy9Ac1Cp1VTzD6CafwAZr77FxSMHObVnJ+cj95F48zoHVi3jwKpllHOrjHdIS+qEtKB8FY8nH1gIIYQQZZokwKXY2QPhrPvxWzLS06jgUY3eH36OQ4WK5g7rqag1GmoGBFIzIBBtWioxhyI5tXcnMQcjuRt3lYg/FxHx5yIqeFSjTkhLvINb4OTqZu6whRBCCFEMSQJcChkMBiLXrGDn73PAYKCafwDd3vkQSxsbc4dWIDSWVtQOak7toOakpyRzPnIfp/buJPbwIW5dimX3pVh2L55PpRpe1AlpQe3gFjhUcDF32EIIIYQoJiQBLmV0GRlsnf0LR7asB6BBh660GTKi1LYSs7C2oW6L1tRt0ZrUpCTOHtjL6b27uHTsMNcvnOX6hbPs+G027t4+eAe3wDu4ObZO5cwdthBCCCHMSBLgUiZi+ZLM5FehoPVLr9Kwc48yM0HMys6O+q07UL91B5LvJXBm315O793JlVPHuXb6BNdOn2DHgjBqNQ2mQbvOVPWtX2ZeGyGEEEL8QxLgUsRgMHBy9zYA2r/6Fn7tOpk5IvOxcXTCv0MX/Dt04f6dW5wJ38OpPduJP3+WM+G7OBO+i3JulWnQvjM+rdpibWdv7pCFEEIIUUQkAS5Fbl2K5d71eNQaC+o2DzV3OMWGvXMFArr2JKBrT27EXuDI5r85sWs7d+Ousn3+LHYtmod3cAsatO+Mm1cdGRUWQgghSjlZVqsUOXcgAgAPP380VlZmjqZ4qlitBu1efYvXf5lHu1ffwsWzOjqtlhM7t7Jo7AcsGDOK6I3rSEtONneoQgghypibN2/yxhtv4OHhgaWlJa6urnTs2JEvvvgChULx2K/t27cDcOXKFSwsLKhXr57xuOPHj3/i4x+1X506dUxirFatmvE+lUqFu7s7w4YN4+7du0X2OhUEGQEuRbISYK8mwWaOpPizsLahQfvO+LXrRPy5Mxze9Den9+7k5qVYtoT9xM7fZlO3eSh+7TtTqXpNc4crhBCiDOjbty/p6enMmzePGjVqcP36dbZs2YKvry9xcXHG/d555x0SExOZM2eOcZuzszMAc+fOpX///uzcuZN9+/YRGBjI+++/z+uvv27ct0mTJowYMYLhw4fniMHX15fNmzcbb6vVOVPFiRMnMnz4cHQ6HWfOnGHEiBG8/fbbLFiwoEBeh6IgCXApkXjzBjdiz6NQKKkR0NTc4ZQYCoUCNy9v3Ly8CX3pVU7s2srhTX9z5+pljmxZz5Et63GtVZsG7TrjHdICjaWMrAshRIliMIDWTJ/qaWwgj2V1CQkJ7Nq1i+3bt9OqVSsAPD09ado05990a2tr0tLScHV1NdluMBiYM2cOP/30E1WqVCEsLIzAwEDs7Oyws7Mz7qdSqbC3t8/xeMhMeHPbnl32x1auXJmXX36ZRYsW5el5FheSAJcS5w6EA1C5jg82Do5mjqZksrKzo1HnHjTs1J2rJ48TvWkdZ/ftJf7cGeLPnWH7/Fn4tGpDg3adZcU5IYQoKbTJ8KW7ec79yTWwsM3TrllJ6sqVKwkKCsLS0jLfp9u2bRvJycm0a9eOypUrExISwvfff4+tbd5iADh79izu7u5YWVkRHBzM5MmT8fB49N+8q1evsnr1agIDA/MdrzlJDXApkVX+UKtJkJkjKfkUCgVVfOrR7Z0xvPbzXFo8PwTHSq6kJT/g0N+rmfvemywZ/xEn9+wgQ6s1d7hCCCFKAbVazdy5c5k3bx5OTk40a9aMTz75hCNHjuT5GGFhYQwcOBCVSkW9evWoUaMGS5cuzfPjAwMDmTt3LuvXr+fnn38mJiaGFi1acP/+fZP9PvzwQ+zs7LC2tqZKlSooFAr+97//5fk8xYGMAJcCKfcTuXLyOCAJcEGzcXSiac9+NOneh4tHozm86W/OR+3jysljXDl5DGt7B+q1bk/dVm3NHaoQQojcaGwyR2LNde586Nu3L127dmXXrl1ERETw999/M2XKFGbNmsWQIUMe+9iEhASWL1/O7t27jdsGDx5MWFjYEx+bpXPnzsbv/fz8CAwMxNPTkz/++INhw4YZ7/vggw8YMmQIBoOBy5cv88knn9C1a1d27tyJqoQsvCUJcClw4eABDAY9Lp7Vcaz4+Lod8XQUSiXVGjSiWoNG3L9zi2NbN3Fk6waSbt/iwF9/cuCvP7F2rUyMmwtejYNQKOXDFSGEKBYUijyXIRQHVlZWtG/fnvbt2zN27FheffVVxo0b98QkduHChaSmppqUIhgMBvR6PWfOnKF27dr5jsXJyYnatWtz7tw5k+0VKlSgVq1aAHh5eTF16lSCg4PZtm0b7dq1y/d5zEH+SpcCZ/dn1v/K6G/RsHeuQHC/QQyfFkbPD8ZS3T8AFApS4q+y+rsvmTP6dQ5tWEN6aoq5QxVCCFHC+fj48ODBgyfuFxYWxnvvvUd0dLTx6/Dhw7Ro0YLZs2c/1bmTkpI4f/48bm5uj90va9Q3JaXk/N2TEeASTpuWysUjhwCoJe3PipRSpaJW40BqNQ7k1tUrrJr5E8kXz3E37hpbZ//CnsULqN+2Iw07dsPBpaK5wxVCCFGM3b59m+eee46hQ4fi5+eHvb09kZGRTJkyhZ49ez72sdHR0Rw8eJDff/89R9/eQYMGMXHiRCZNmpRrS7Ps3n//fbp3746npyfXrl1j3LhxqFQqBg0aZLLf/fv3iY+PN5ZAjBkzBhcXF0JCQp7uyZuBjACXcLFHDpGRnoaDSyVcPKubO5wyy7FiJSo0DGToj7NoO/QNyrlVJi35AZGrlzNr1Kus/t9krp46gcFgMHeoQgghiiE7OzsCAwP5/vvvadmyJfXq1WPs2LEMHz6c6dOnP/axYWFh+Pj45Eh+AXr37s2NGzdYt27dE2O4cuUKgwYNwtvbm/79+1O+fHkiIiJwcXEx2e/zzz/Hzc0Nd3d3unXrhq2tLRs3bqR8+fL5e9JmJCPAJdz5bN0fZAlf87Owssa/Y1catO9MTHQUUetWceloNGf27eHMvj1UquFFQNee1A5qhkqtMXe4QgghiglLS0smT57M5MmTn7jv3LlzTW5Pmzbtkfu6urqi0+lMtsXGxua67+LFi5947kc9tqSRBLgE0+t0nI/aD0j9b3GjUCqp0agJNRo14dalWA7+/Rcndm3j+oWzrJv2LTt/m41/x27Ub9tR+jYLIYQQRUxKIEqwKyePk5p0Hyt7Byp7+5g7HPEIFTyq0eG1txnx01yaDXgR23LOJN29w+7F85n55its/HUaty5fNHeYQgghRJkhI8BF7O6DdN76PYobt5R06fJsxzoXmdn9oWajpihLSN+9sszGwZGgPgNo0qMPp8N3E7V2JTdiznN0ywaObtmAp19DGnXpQfUGAdJGTQghhChEkgAXMQOw98IdQIle//QTogwGwz+rvzWV7g8liUqtwadFa+o2D+Xq6RMcXLeKc/sjuHjkEBePHKKcexUade6Bb8s2aKyszB2uEEIIUepIAlzENKp/Jqpp9Qbyv9J3phsx57l/6yZqS0s8/fwLJDZRtBQKBVXq+FKlji/3blzn0IY1HN2ygbvXrrAl7Cd2L56HX9tO+HfshkMFlycfUAghhBB5IglwEdOo/vloOz1D/9THOReZOfpbza8RGounTaNFceFYsRKhLw4jpN8gjm3fwqG//yLhehwH/vqTyDUr8ApsRuOuvXDz8jZ3qEIIIUSJJwlwEcueAGt1z5AAZ2t/JkoPC2sbGnXujn/HLsQciuTgulVcOnaEM+G7OBO+iyo+9Wjaox/V/AOk7Z0QQgjxlCQBLmIqpQKVUoFOb3jqBDghPo5bl2IzW20FNC3gCEVxoFSqqBkQSM2AQG5ejCFq7SpO7t7OlRPHuHLiGC6e1WnSsx/eQc1lAqQQQgiRTzLV3Ayy6oC1uqebBHfuQGb3h6o+9bC2sy+wuETx5OJZnU5vvsur02YR0LUXGksrbl6MYd2P3zD73REc2rAGbVqqucMUQgghSgxJgM3A4mEZxNOOAGfV/9ZsLN0fyhL78hUIfelVhv80h2b9B2Pt4Mi9G9fZOvsXZo4cRsSfi0lNSjJ3mEIIIUSxZ9YEeOfOnXTv3h13d3cUCgUrV6585L6vv/46CoWCqVOnmmy/c+cOL7zwAg4ODjg5OTFs2DCSinkSoHmGBDj5XgJXT58EoFaTwAKNS5QM1nb2BPUdyPDpYbQZ+joOLpVISbzHnj9+49c3h7B9/izu375l7jCFEEIUgkWLFqFSqXjrrbee6vFz585FoVAYv+zs7AgICGD58uUFHGnxZtYE+MGDBzRo0IAZM2Y8dr8VK1YQERGBu7t7jvteeOEFjh8/zqZNm1izZg07d+5kxIgRhRVygXiWEojzUfvBYKBSjVo4VKhY0KGJEkRjaUXDjt0Y9sOvdHn7A1w8qqFNSyVq7UpmjXqV9T9P5faVy+YOUwghRAEKCwtjzJgxLFq0iNTUpyt/c3BwIC4ujri4OA4dOkTHjh3p378/p0+fLuBoiy+zJsCdO3dm0qRJ9O7d+5H7XL16lVGjRvH777+j0WhM7jt58iTr169n1qxZBAYG0rx5c6ZNm8bixYu5du1aYYf/1LJGgNOfYgQ4q/63VmPp/iAyKVUq6jZrxYtTptHno/FU8amHXpfB8e2bmfveG6z6dhLXzpwyd5hCCGEWBoOBZG2yWb4MhrwPdIWGhjJy5EhGjhyJo6MjFSpUYOzYsSbHiImJYe/evXz00UfUrl07x6jt3LlzcXJyYuXKlXh5eWFlZUXHjh25fNl0MEShUODq6oqrqyteXl5MmjQJpVLJkSNHnu3FLkGKdRcIvV7Piy++yAcffICvr2+O+8PDw3FycqJx48bGbe3atUOpVLJv375HJtZpaWmkpaUZbycmJgKg1WrRarUF/CxyyhoBTk3L3/nSU1K4eDQagGoNGxdJrCJvsq6Fua9JlXoNqFKvAXHnThO1egUXovZx7kAE5w5EULmOLwHde+Pp10haqOVRcbmuouDINS2dsl9XnU6HwWBAr9ej1+tJ1iYTvNg8c2bCB4Zjo7HJ8/7z5s1j6NChREREEBkZyeuvv06VKlUYPnw4ALNnz6ZLly7Y29vzwgsvEBYWxsCBA42P1+v1JCcn88UXXzB37lwsLCwYOXIkAwcOZNeuXcZ9sv+r0+mYP38+AP7+/sbt5qLX6zEYDGRkZACm79WCfN8W6wT466+/Rq1W8/bbb+d6f3x8PBUrmpYBqNVqnJ2diY+Pf+RxJ0+ezIQJE3Js37hxIzY2ef9BfVqpySpAQfj+SO6eyfv/DpMuXUCn1aKxcyDiyDEUR48XXpDiqWzatMncIRgpvf3wcPXg7snD3I89x9VTx7l66jgWTs6U82mAnUcNFEqZB5sXxem6ioIh17R02rRpE2q1GldXV5KSkkhPTyclI8Vs8dy/f58MdUae9s3IyKBy5cqMHz8ehUJB9+7diYqK4vvvv2fAgAHo9XrmzJnDlClTSExMpEuXLrz//vscPXoUT09PAFJTU9FqtUyePNk4cDht2jQCAwPZtm0bAQEBpKamcu/ePRwcHABISUlBo9EwdepUXFxcjIOC5pKenk5KSgp79+4FTN+rycnJBXaeYpsAR0VF8cMPP3Dw4MECH636+OOPGT16tPF2YmIiVatWpUOHDsYfiMI082I415Lv49fAn3a+bnl+3IafviceqN+yNc27di28AEW+abVaNm3aRPv27XOU6pjf89y/fYvoDWs4tnUD6Ql3uL53Gylnj9Owc098WrVFYymrCeameF9X8TTkmpZO2a+rTqfj8uXL2NnZYWVlhb3BnvCB4WaJy1ptneccRq1WExwcjKOjo3Fbq1atmDFjBra2tmzevJmUlBT69u2LRqPBwcGBdu3asXTpUiZOnAiAlZUVarWa0NBQlA8HOBo3boyTkxOXLl2idevWma+JvT2RkZFAZlK5ZcsWRo8eTeXKlenevXsBvwr5k5qairW1NSEhIezcudPkvVqQyXmxTYB37drFjRs38PDwMG7T6XS89957TJ06ldjYWFxdXblx44bJ4zIyMrhz5w6urq6PPLalpSWWufzB12g0RfIL0UKduXCBXqHM8/l0GVpio6MAqB3YTH5xF1NF9TOUX86ubrR5eTghfQcRvXEtB//+i8SbN9gxfyb7VyyhUeceNOjYVfpKP0Jxva7i6ck1LZ00Gg1KpRKFQoFSqTQmgXYqOzNHljdZcWfJ+l6pVDJnzhzu3LmDra2t8X69Xs/Ro0eZOHGiyfPN/n32Y2X/ql27tvE+f39/Nm3axDfffEPPnj0L8yk+Udb1U6szU9Ts79WCfM8W288/X3zxRY4cOUJ0dLTxy93dnQ8++IANGzYAEBwcTEJCAlFRUcbHbd26Fb1eT2Bg8W0R9jRdIC6fOEZa8gNsHJ1wq+1dWKGJUs7Kzo6gPgMYPmM2bYe+kdlC7X4ie/74jZlvvsL2+TOlhZoQQpjJvn37TG5HRETg5eVFQkICq1atYvHixSZ50aFDh7h79y4bN240PiYjI8M4ugtw+vRpEhISqFu37mPPrVKpSEkxX7lIUTPrCHBSUhLnzp0z3o6JiSE6OhpnZ2c8PDwoX768yf4ajQZXV1e8vTMTwLp169KpUyeGDx/OL7/8glarNRZ759Yyrbh4moUwzh14uPhFQFOUSln6VjwbjYUl/h274teuE6cjdnNg5VJuXoolau0qDq1fS93moTTp0ZfyVaqaO1QhhCgzLl26xOjRo3nttdc4ePAg06ZN47vvvmPBggWUL1+e/v375yip6NKlC2FhYXTq1AnIzJVGjRrFjz/+iFqtZuTIkQQFBdG0aVPjYwwGg3GuVEpKCps2bWLDhg18/vnnRfdkzcysCXBkZCStW7c23s6qy3355ZeZO3duno7x+++/M3LkSNq2bYtSqaRv3778+OOPhRFugcnvQhgGvZ7zD1d/q9VEVn8TBSerhVqdkJbEHj7IgVXLuHziKMd3bOb4js3UbBxE0559ca/9+JEDIYQQz+6ll14iJSWFpk2bolKpeOeddxgxYgQNGjSgd+/eudYT9+3blxdffJFbtzI/vbOxseHDDz/k+eef5+rVq7Ro0YKwsDCTxyQmJuLmljkHydLSEk9PTyZOnMiHH35Y+E+ymDBrAhwaGpqvHnmxsbE5tjk7O7Nw4cICjKrwZZVApOexBCL+wlmS7txGY2WNR70GhRmaKKMUCgXV/QOo7h9A3NnT7F+1jHOREZx/+FWlbj2a9OxLdf/G0kJNCCEKSVY3hp9//tlk++P68/bv35/+/fubbOvTpw99+vTJdf8hQ4YwZMiQZ461pCu2k+BKs/yOAGeVP1T3D0BtYVFocQkB4OblTc/3P+X21ctErl7OiZ3buHLyGFdOHsPFoxpNevTFO6QlSpWU4gghhCiZiu0kuNJMo366BLhWE1n9TRSd8pWr0vH1d3h12iwCuvVGY2XNzUuxrJv+HWHvjODQ+tVo055uGU4hhBDCnCQBNgOLrBKIjCeXQNy5dpU7Vy+jVKmo3rDxE/cXoqDZl69A6IvDGDFjDs0GvIi1gyOJN6+zdc7/MfOtoYT/uYiUpPvmDlMIIUq07du3M3Xq1Gc6xpAhQ0hISCiQeEo7SYDNID8lEHFnTwHgXrsuVrYlo4+hKJ3+3ULNsWJmC7W9f/xubKGWeOumucMUQgghnkhqgM0gPwnwg4S7ADhUcCnUmITIq+wt1M5E7Gb/qmXcvBjzsIXaGuo2by0t1IQQQhRrkgCbQX4Wwki+l5kA2ziVK9SYhMgvpUpFnWat8H5kC7VAmvbsJy3UhBBCFDuSAJtB/kaAEwCwdXQqxIiEeHqPbqG2j/OR+6hcx5fAXs9RzT9AWqgJIYQoFiQBNoP8rASXVQJhKyPAogTIrYXa1VPHWf7VcVxrehHUdxA1GjWRRFgIIYRZySQ4M9Co874QRvK9BEBKIETJYmyhNn0WAV17obawJP78WVZOmchvH73L2QPh+VoERwghhChIkgCbgbEEIkNGgEXpZu9cgdCXXmX49DCa9OiLxtKKG7Hn+evbL1jw4duc2bcHgz5v/bCFEEKIgiIJsBnktQY4Q6sl9WF/VUmARUlm4+hEyxde4dXpYQT27o+FtTU3L8aw+n+TmT9mFKfDd6HX68wdphBCFHuLFi1CpVLx1ltvPdNxUlJScHZ2pkKFCqSlpRVQdCWHJMBmYFwI4wkJcFb5g1Kllh7AolSwcXCk+cCXeHX6bIL6DsTC2oZbly+yZurXzHt/JCd3b5dEWAghHiMsLIwxY8awaNEiUlOffjXOP//8E19fX+rUqcPKlSsLLsASQhJgM/hnBPjxNZDJD8sfbBwdUSjlUonSw9rOnmb9BzN8xmxCnnsBS1tb7ly9zLpp3zJ39Juc2LkVvU4SYSHEszMYDOiTk83ylZ+5DqGhoYwcOZKRI0fi6OhIhQoVGDt2rMkxYmJi2Lt3Lx999BG1a9dm+fLlJseYO3cuTk5OrFy5Ei8vL6ysrOjYsSOXL1/Ocb6wsDAGDx7M4MGDCQsLe/oXuISSLhBmkNcSiAf3pP5XlG5WtnYE9xtEoy49OLR+DVFrV3I37ip/z/gf4csWEdi7P3VbtEalll9VQoinY0hJ4XSjALOc2/tgFAobmzzvP2/ePIYNG8b+/fuJjIxkxIgReHh4MHz4cADmzJlD165dcXR0NCauzz//vMkxkpOT+eKLL5g/fz4WFha8+eabDBw4kD179hj3OX/+POHh4SxfvhyDwcB//vMfLl68iKenZ8E88RJAhhXNIK8LYcgEOFFWWNrYZi6zPD2MFs8PwdregYTrcWz45Qfm/Oc1jmzZgC5Da+4whRCiUFWtWpXvv/8eb29vXnjhBUaNGsX3338PgF6vZ+7cuQwePBiAgQMHsnv3bmJiYkyOodVqmT59OsHBwQQEBDBv3jz27t3L/v37jfvMnj2bzp07U65cOZydnenYsSNz5swpuidaDMiwihnkdQQ4+eEiGDaOkgCLssHC2oamPfvRsGM3ojetI3L1cu7duM6mX6cRsXwxgb2ewze0PWqNxtyhCiFKCIW1Nd4Ho8x27vwICgoy6ZMeHBzMd999h06nY/PmzTx48IAuXboAUKFCBdq3b8/s2bP573//a3yMWq2mSZMmxtt16tTBycmJkydP0rRpU3Q6HfPmzeOHH34w7jN48GDef/99Pv/8c5RlpORSEmAzsFDnrQ2alECIskpjZUWT7n3w79CFI5s3cOCvZdy/dZPNs34iYvkSmvbsR/02HVFbWJg7VCFEMadQKPJVhlBchYWFcefOHayzJdV6vZ4jR44wYcKEPCeuGzZs4OrVqwwYMMBku06nY8uWLbRv375A4y6uykaaX8xoVHlbCOOfEginwg5JiGJJY2lFQNeeDJs2i9ZDXsOunDNJd26zdc7/MevtVzm4bhXa9LLXvkcIUTrt27fP5HZERAReXl4kJCSwatUqFi9eTHR0tPHr0KFD3L17l40bNxofk5GRQWRkpPH26dOnSUhIoG7dukBmIj1w4ECT40RHRzNw4MAyNRlORoDNIM+T4B6WQMgIsCjrNBaWNOrcHb+2HTm2fTP7Vy7l/u2bbJs3k30rl9Kkex8atO+CxsrK3KEKIcRTu3TpEqNHj+a1117j4MGDTJs2je+++44FCxZQvnx5+vfvn2Mp+S5duhAWFkanTp0A0Gg0jBo1ih9//BG1Ws3IkSMJCgqiadOm3Lx5k9WrV/PXX39Rr149k+O89NJL9O7dmzt37uDs7Fxkz9lcZATYDCzyXAOc1QbNqbBDEqJEUFtY4N+hC8N+/JX2w0fi4FKR5HsJ7PhtNjNHDWP/qmWkp6aYO0whhHgqL730EikpKTRt2pS33nqLd955hxEjRjB79mx69+6dI/kF6Nu3L3/99Re3bt0CwMbGhg8//JDnn3+eZs2aYWdnx5IlSwCYP38+tra2tG3bNsdx2rZti7W1Nb/99lvhPsliQkaAzUC6QAjxbFRqDX7tOuEb2o4Tu7ayb8Uf3Lsez66FczmwejmNu/bCv2M3LEtB3Z8QouzQaDRMnTqVn3/+2WT7kSNHHvmY/v37079/f5Ntffr0oU+fPjn2fe+993jvvfdyPY6FhQV37959iqhLJkmAzSCrBOJxK8Glp6agTctc4UUSYCFyp1Krqd+6Az4t2nBqzw4ili8mIT6O3YvnE7l6OY269qRhp+6ykqIQQggTkgCbQV5qgLNaoKktLdFY5a+NihBljUqtxrdVW+o2D+X03p2EL1/C3WtX2PvH70StWUmjLj1o1LknVnaSCAshhJAE2CzyUgKRvfwht5ofIUROSpWKui1a492sJWfCdxOxfAm3r1wifNkiotaupGGnHgR07Ym1vYO5QxVCCBPbt29/5mMMGTKEIUOGPPNxygJJgM0gawRYpzeg0xtQKXMmuMYewLIIhhD5plSqqNOsFd7BLTi7fy/hfy7m1qVY9q1YwsG//6Jhx64EdOuNjYOjuUMVQghhBpIAm0HWQhiQWQahUqpy7CMT4IR4dgqlktpBzfFqGsK5yAjC/1zMzdgL7F+1jIPrV+PfoSuNu/WW95kQQpQxkgCbQdYIMGQmwFaanAmwtEATouAolEq8moZQq0kwFw7uJ3zZYq5fOEvk6uVEb1hLg/adaNy9L3blSn/vSyGEEJIAm4UmW8nDo+qAZQRYiIKnUCioGRBIjUZNiYmOJGLZYuLOnSZq7SqiN67Dr20nmvTsi71zBXOHKoQQohBJAmwGSqUCpcKA3qB4ZCeIB/cSAEmAhSgMCoWCGg2bUN2/MRePHCJ82SKunTnJofWrObL5b+q16UjTnn2xlhp8IYQolSQBNhO1AtINkJ6RewJsLIGQBFiIQqNQKKjWoBGefg25dOww4csWcfXUcQ5vXMvRLRvwadkGrYOURQghRGkjCbCZPOyE9sjFMB487ANs6+RUNAEJUYYpFAo86/vjWd+fyyeOEr5sEZePH+HYto2gVLItKYHgfgOlNEIIIUoJ5ZN3EYUhax5cbiUQBoNBaoCFMJOqPvXp//mXDBj/FVV9/UCv5+iW9YS9PZxtc381vjeFEKIohYaGolAojF+VKlXiueee4+LFi/k6zvbt202OY21tja+vL7/++mshRV48SQJsJuqHI8DajJyT4FIfJKHXZQBgIzWIQphFlbr16P3xBCq37Ya7tw86rZaDf//FrFGvsuO32SQn3jN3iEKIMmb48OHExcVx7do1Vq1axeXLlxk8ePBTHev06dPExcVx4sQJXnvtNd544w22bNlSwBEXX5IAm8njSiCy6n8tbW1RazRFGZYQ4l+sK7nR97NJ9P30v7jV8iYjPY3I1cuZNepVdi9eQGpSkrlDFEI8hsFgQJumM8uXwfDoFV//LTQ0lJEjRzJy5EgcHR2pUKECY8eONTmGjY0Nrq6uuLm5ERQUxMiRIzl48KDx/qzR3bVr1+Ln54eVlRVBQUEcO3Ysx/kqVqyIq6sr1atX5+2336Z69eomxyrtpAbYTNSPKYEw1v/K6K8QxYJCoaCaX0M86/sTcyiSPX/8xo2Y8+xbsYToDWsI6NqLRl16YmljY+5QhRD/kpGu59d3dpjl3CN+aIXGMmev/0eZN28ew4YNY//+/URGRjJixAg8PDwYPnx4jn3v3LnDH3/8QWBgYI77PvjgA3744QdcXV355JNP6N69O2fOnEGTy6CawWBgw4YNXLp0KddjlVaSAJuJsQQitwT4ntT/ClEcKRQKajRqQvWGjTl3IJy9f/zOrcsX2bv0dw7+/ReNu/ehUafuaKyszB2qEKIEqlq1Kt9//z0KhQJvb2+OHj3K999/b0yAf/rpJ2bNmoXBYCA5OZnatWuzYcOGHMcZN24c7du3BzKT6ipVqrBixQr69+9v3KdKlSoApKWlodfrmThxIi1btiyCZ1k8SAJsJqrHJMDSAk2I4k2hUGSuLNc4iNMRuwlfupA7166we9E8otaupGnPfjTo0AWNhaW5QxWizFNbKBnxQyuznTs/goKCUCj+WSwrODiY7777Dp1OB8ALL7zAp59+CsD169f58ssv6dChA1FRUdjb25s8LouzszPe3t6cPHnS5Fy7du3C3t6etLQ09u/fz8iRI3F2duaNN97I9/MsiSQBNpOsLhDpuUyCkw4QQpQMCqWSOiEtqR3UjFO7dxC+bBEJ1+PYsSCMyDUrCOz1HPXbdpJafiHMSKFQ5KsMoThzdHSkVq1aANSqVYuwsDDc3NxYsmQJr776ar6OVb16dZwetlr19fVl3759fPHFF2UmAZZJcGaiVmQmvrnXAEsCLERJolSq8GnZhiH/+5kOr72NfQUXHty9w9Y5/8fsd0ZwZPN6dBkZ5g5TCFHM7du3z+R2REQEXl5eqFS5J/BZ21NSUnI8Lsvdu3c5c+YMdevWfey5VSpVjuOUZjICbCbGLhC5rAQnCbAQJZNKraZ+mw74tGzN0a2b2Ld8Mfdv32TTzOnsX7WUoL6D8GnRGuUj/pgJIcq2S5cuMXr0aF577TUOHjzItGnT+O6774z3JycnEx8fD2SWQPz3v//FysqKDh06mBxn4sSJlC9fnkqVKvHpp59SoUIFevXqZbLPjRs3SE1NNZZALFiwgH79+hX6cywuJAE2k8d1gTDWADs6FWFEQoiColJr8O/QBd/QthzZtJ79q5Zy78Z1Nvw8lf0rlxLcbxDeIS1QKiURFkL846WXXiIlJYWmTZuiUql45513GDFihPH+mTNnMnPmTADKlSuHn58f69atw9vb2+Q4X331Fe+88w5nz57F39+f1atXY2FhYbJP1mPUajVVq1bltddeY/z48YX7BIsRSYDN5HGT4B7cSwBkBFiIkk5jYUlA1574te1I9Ma17P/rT+7GXWXdtG/Zt+IPQvq/gFeTYBRKqUYTQoBGo2Hq1Kn8/PPPOe7bvn17no/TvHnzXHv/Qma/4fz0Jy6t5LeumfyzEIbpD6FeryMlMRGQBFiI0kJjZUWTHn0ZPm0WzQa8iKWtLbevXGL1/yaz4ON3OR+1T/4gCSFEETJrArxz5066d++Ou7s7CoWClStXGu/TarV8+OGH1K9fH1tbW9zd3XnppZe4du2ayTHu3LnDCy+8gIODA05OTgwbNoykErAy06NKIFISEzEY9CgUSqwdHMwQmRCisFhY2xDUZwCvTgsjqO9ALKytuRl7gZVT/svCz94jNjpKEmEhhCgCZk2AHzx4QIMGDZgxY0aO+5KTkzl48CBjx47l4MGDLF++nNOnT9OjRw+T/V544QWOHz/Opk2bWLNmDTt37jSplymujAth/GsSXNYEOGsHB6kPFKKUsrK1o1n/wbw6LYwmPfuhtrQk/twZ/pw8jsXjPuTSsSPmDlEIUcS2b9/O1KlTn+kYWeUNWe3NxKOZtQa4c+fOdO7cOdf7HB0d2bRpk8m26dOn07RpUy5duoSHhwcnT55k/fr1HDhwgMaNGwMwbdo0unTpwrfffou7u3uhP4enpXrECLB0gBCi7LC2d6Dl80MI6NKTA38t4/DGv7l2+gRL//sJVX39aNZ/MJXr+Jg7TCGEKHVK1CS4e/fuoVAojP+zCQ8Px8nJyZj8ArRr1w6lUsm+ffvo3bt3rsdJS0sjLS3NeDvxYc2tVqtFq9UW3hN4SKvVGmuAU7UZJudMvH0LAGsHpyKJRRSMrGsl16x0KarramFrR7NBQ2jQqQeRfy3j2NZNXD5+hMXjxuBRvyHB/QZRqaZXocZQVsh7tXTKfl11Oh0GgwG9Xo9en3OiuSje9Ho9BoOBjIe907O/VwvyfVtiEuDU1FQ+/PBDBg0ahMPD2tj4+HgqVqxosp9arcbZ2dnYJy83kydPZsKECTm2b9y4ERsbm4IN/BHUiswh4DPnLrAu45xx+93j0Zn/JiWxbt26IolFFJx/f2ohSociva4uVaja7TnuHjtE4oXTXDp6iEtHD2FT2YPyfo2xLFe+6GIpxeS9Wjpt2rQJtVqNq6srSUlJpKenmzskkU/p6emkpKSwd+9ewPS9mpycXGDnKREJsFarpX///hgMhlxbg+TXxx9/zOjRo423ExMTqVq1Kh06dDAm14VJq9WybvYWACpX9aRLl39WZ9l5J57bh6G2bz2adelS6LGIgqHVatm0aRPt27dHI8velhpmva7P9efejXj2r/iDU7t3kHz1EslXL1GraTCBfQZSvopH0cZTSsh7tXTKfl11Oh2XL1/Gzs4OKysrc4cm8ik1NRVra2tCQkLYuXOnyXs16xP7glDsE+Cs5PfixYts3brVJEF1dXXlxo0bJvtnZGRw584dXF1dH3lMS0tLLC0tc2zXaDRF9gsxaylknQGTc6Yk3gPA3rm8/HIugYryZ0gUHXNd1wqVq9Jl5HsE9RlA+LJFnNq7k3P7wzl3III6IS0J7vc8zu6Vizyu0kDeq6WTRqNBqVSiUChQKpUopcd2iZN1/dTqzBQ1+3u1IN+zxfonIyv5PXv2LJs3b6Z8edOP/oKDg0lISCAqKsq4bevWrej1egIDA4s63Hz5pw+waX2ScRU4mQQnhHjI2b0KXd/+gJenTMOraQgYDJzas4O5o99g/U9TuXfj0SVfQgghcjLrCHBSUhLnzv1T/xoTE0N0dDTOzs64ubnRr18/Dh48yJo1a9DpdMa6XmdnZywsLKhbty6dOnVi+PDh/PLLL2i1WkaOHMnAgQOLdQcIyN4FwrTnp7ELhKMkwEIIUxU8qtHjvU+4HnOevX/8xoWDBzi+YzMnd2+jXmh7AvsMwKGCi7nDFEKIYs+sI8CRkZE0bNiQhg0bAjB69GgaNmzI559/ztWrV/nrr7+4cuUK/v7+uLm5Gb+yCqMBfv/9d+rUqUPbtm3p0qULzZs359dffzXXU8qzR/YBvidt0IQQj1epek16fziO5yd9h6dfQ/Q6HUe2rGf2O8PZMvsXku7eMXeIQogSLDw8HJVKRdeuXZ/5WHXq1MHS0vKxzQnMwawjwE9ajzovKyI5OzuzcOHCggyrSOS2ElxGejppDx4AkgALIZ7Mzcubfp/+lyunjrN3yW9cPnGU6A1rOLZ1Iw06dKFpz37YODqZO0whRAkTFhbGqFGjCAsL49q1a0/9qfru3btJSUmhX79+zJs3jw8//LCAI316xboGuDTLrQY4OTEh8z61GktbWzNEJYQoiarU8aX/uMk8N/YL3GvXJUObTtTalcwa9Sp7liwg9UHxXx5eiMJiMBjQpqaa5Ss/S5uHhoby9ttvM2bMGJydnXF1dWX8+PEAxMbGolAoiI6ONu6fkJCAQqFg+/btxm3Hjx+nW7duODg4YG9vT4sWLTh//jwAQ4YMoVevXkyYMAEXFxccHBx4/fXXc7SKS0pKYsmSJbzxxht07dqVuXPnmty/fft2FAoFa9euxc/PDysrK4KCgjh27FiO5xQWFsbzzz/Piy++yOzZs/P8WhSFYt8ForQylkBkS4Cz6n9tHMuhUCjMEZYQogTzqNeAqr5+xB4+yJ4lv3H9wlkili8hesNamvTsR8OO3dBIWyhRxmSkpfHjy/3Mcu635y3L13tu3rx5jB49mn379hEeHs6QIUNo1qwZXl5PXgjn6tWrtGzZktDQUGPXrD179hgXlADYsmULVlZWbN++ndjYWF555RXKly/PF198Ydznjz/+oE6dOnh7ezN48GDeffddPv744xx5yQcffMAPP/yAq6srn3zyCd27d+fMmTPGTg33799n6dKl7Nu3jzp16nDv3j127dpFixYt8vx6FCYZATaT3CbBPUhIAMBW1vAWQjwlhUJBdf8AXvjyf/R47xPKV/Eg9UESuxbOJeyd4RzasAZdhqyCJkRx5Ofnx7hx4/Dy8uKll16icePGbNmyJU+PnTFjBo6OjixevJjGjRtTu3ZtXnnlFby9vY37WFhYMHv2bHx9fenatSsTJ07kxx9/NFkxLywsjMGDBwPQqVMn7t27x44dO3Kcb9y4cbRv35769eszb948rl+/zooVK4z3L168GC8vL3x9fVGpVAwcOJCwsLCnfWkKnIwAm4mxBCLbJDhpgSaEKCgKhQKvpiHUbBzIqd072Lv0d+7duM7W2b8QuXoFwf0G4dOiNUqVytyhClGo1JaWvD1vmdnOnR9+fn4mt93c3HKsd/Ao0dHRtGjR4rG9chs0aGCy4m1wcDBJSUlcvnwZT09PTp8+zf79+42JrFqtZsCAAYSFhREaGmpyrODgYOP3zs7OeHt7c/LkSeO22bNnGxNpgMGDB9OqVSumTZuGvb19np5TYZIE2EweVwIhE+CEEAVFqVTh07IN3iEtOLp1ExHLF5N48zobfp7KgVXLaDZgMF5NQ1DIggGilFIoFCWm9OffyatCoUCv1xsX9MheU6zVmn6SY21t/cznDwsLIyMjw2TSm8FgwNLSkunTp+Po6Jin45w4cYKIiAj2799vMvFNp9OxePFihg8f/syxPiv5jWcmKmXmD3G6JMBCiCKgUmvw79CFYT/8SssXXsHKzp47166w+vuv+O2T/xATHZWvCTtCiKLj4pLZ3zsuLs64LfuEOMgcPd61a1eOxDi7w4cPk5KSYrwdERGBnZ0dVatWJSMjg/nz5/Pdd98RHR1t/Dp8+DDu7u4sWrTI5FgRERHG7+/evcuZM2eoW7cukJlIt2zZksOHD5sca/To0cWmDEISYDN57AiwLIIhhCgkGksrmvToy6vTZhHcbxAaK2tuxJxn+eRxLBn/EVdO5pzJLYQwL2tra4KCgvjqq684efIkO3bs4LPPPjPZZ+TIkSQmJjJw4EAiIyM5e/YsCxYs4PTp08Z90tPTGTZsGCdOnGDdunWMGzeOkSNHolQqWbNmDXfv3mXYsGHUq1fP5Ktv3745EteJEyeyZcsWjh07xpAhQ6hQoQK9evVCq9WyYMECBg0alOM4r776Kvv27eP48eNF8ro9jiTAZqIyLoTxz4hL8r0EAGxkEpwQopBZ2tgS8twLvDptFgHdeqPSaLh66jhLxn/En5PHcf3CuScfRAhRZGbPnk1GRgYBAQG8++67TJo0yeT+8uXLs3XrVpKSkmjVqhUBAQHMnDnTpKyibdu2eHl50bJlSwYMGECPHj2MrdbCwsJo165drmUOffv2JTIykiNHjhi3ffXVV7zzzjsEBAQQHx/P6tWrsbCw4K+//uL27dv07t07x3Hq1q1L3bp1i8UosNQAm0luC2EYV4GTEWAhRBGxcXAk9MVhBHTpScTyxRzbtonY6Chio6OoHdiMkP6DKV+lqrnDFKLUy97PN8vKlSuN39etW9dkJVzIuWCYn58fGzZseOx5JkyYwIQJE3JsX7169SMf07RpU+O5suJs3rx5rr1/+/bti06ne+SxTpw48dj4ioqMAJvJvxfCMBgMUgMshDAb+/IVaD98JEP+9zN1m4eCQsGZfXuY9/5brP9pKvduXDd3iEIIUWAkATaTf48Aa1NTyEhLA6QEQghhPuVc3eky6n1enjKNWk2CMBj0HN+xmdnvvsaW2b8Y/6MuhBAlmZRAmImxBvjhQhhZf1Q0VtZYWD17KxMhhHgWFTyq0fP9z4g7e5rdSxZw6Wg00RvWcGzbJhp27k6THn2xtjN/L08hRN78e0njpxUaGloqOsbICLCZZHWB0OkN6PQGUpOSALAuBs2hhRAii5uXN899Nonnxn6Bm5c3GelpHFi1jFkjhxHx52LSU5LNHaIQQuSbJMBmosr2ymt1euPSpCr1o1dwEUIIc/Go14BB//2WXmPGUsGjGukpyez54zdmvT2cqLWryEhPN3eIQgiRZ1ICYSZZI8CQORFO/3DGpCxLKoQorhQKBTUDAqnRsAmnwnex94/fSIiPY/v8mUSuXUFw30H4tmqLSi1/WoQQxZuMAJuJMlsCrM3Qo8/IyNwuCbAQophTKJXUbdaKId/9TPsRo7ArX4Gk27fY9Os05r3/Jqf27MCg1z/5QEIIYSaSAJuJUgHqh1mwVmcw9sxTqmTkRAhRMqjUavzadmTY1F8JfWk41vYO3I27xtofv2HBh29zPmpfqZgsI4QofSQBNiONKisB1qPXPRwBVssIsBCiZFFbWBDQtSevTptFs/6DsbC24ealWFZO+S+Lxr7PpWNHnnwQIYQoQpIAm5HFw2bA2WuAVTICLIQooSysbQjqO5BXp4fRpGc/1BaWxJ09zdL/fsLSSZ8Rd+60uUMUQuRBeHg4KpWKrl27PtXjt2/fjkKhMH5ZW1vj6+vLr7/+WsCRPj1JgM1I87AVhFYmwQkhShFrO3taPj+EYT/OxL9jN5QqNZeORrPw0/dY9e0kbl2KNXeIQojHCAsLY9SoUezcuZNr16499XFOnz5NXFwcJ06c4LXXXuONN95gy5YtBRjp05ME2IyMCXCG4Z8EWGZPCyFKCbtyzrQd+jpDp/4fvq3aoVAoOXcggnljRrFu2rckxMeZO0Qhio3Q0FDefvttxowZg7OzM66urowfPx6A2NhYFAoF0dHRxv0TEhJQKBRs377duO348eN069YNBwcH7O3tadGiBefPnwdgyJAh9OrViwkTJuDi4oKDgwOvv/466f9qYZiUlMSSJUt444036Nq1a44FNLJGd9euXYufnx9WVlYEBQVx7NixHM+pYsWKuLq6Ur16dd5++22qV6/OwYMHC+T1elaSAJtRVg1wuk6HTrpACCFKKceKlej05ru8/O0Magc2A4OBk7u3M2f062yaOZ37d26ZO0RRihkMBvTpOrN85XcS6Lx587C1tWXfvn1MmTKFiRMnsmnTpjw99urVq7Rs2RJLS0u2bt1KVFQUQ4cOJeNhfgGwZcsWTp48yfbt21m0aBHLly9nwoQJJsf5448/qFOnDt7e3gwePJjZs2fn+jw++OADvvvuOw4cOICLiwvdu3dHq9XmGpvBYGD9+vVcunSJwMDAfLwihUeGG80oawQ4PcOASmqAhRClXPkqVek++mOuXzjH7iULiI2O4sjm9ZzYsZUGHbvStGc/bBwczR2mKGUMWj3XPt9rlnO7TwxBYZH3gS0/Pz/GjRsHgJeXF9OnT2fLli14eXk98bEzZszA0dGRxYsXo9FkLqpVu3Ztk30sLCyYPXs2NjY2+Pr6MnHiRD744AP++9//olRm5iRhYWEMHjwYgE6dOnHv3j127NhBaGioybHGjRtH+/btgczEvUqVKqxYsYL+/fsb96lSpQoAaWlp6PV6Jk6cSMuWLfP8ehQmybbMKHsNsKVORoCFEGVDpRq16PvxBK6cOMbuJfO5euoEUWtWcGTzegK69qJxt15Y2tiaO0whipyfn5/JbTc3N27cuJGnx0ZHR9OiRQtj8pubBg0aYGNjY7wdHBxMUlISly9fxtPTk9OnT7N//35WrFgBgFqtZsCAAYSFheVIgIODg43fOzs74+3tzcmTJ0322bVrF/b29qSlpbF//35GjhyJs7Mzb7zxRp6eU2GSBNiMLLK3QZMSCCFEGVPFpx4Dxn9N7OGD7F40nxux54n4cxHRG9bQtGc//Dt2RWNpZe4wRQmn0ChxnxhitnPnx7+TV4VCgV6vN47OZi9F+He5gbW19VNG+Y+wsDAyMjJwd3c3bjMYDFhaWjJ9+nQc7B0wZGQucqNL1qJLTEOhUaG0zj2drF69Ok5OTgD4+vqyb98+vvjii2KRAEsNsBllHwGWhTCEEGWRQqGgun8Agyd/T/f/fISzexVSk+6z8/c5hL0zguiN69Bl5F5XKEReKBQKlBYqs3wpFIonB5gHLi4uAMTF/TNxNPuEOMgcPd61a9cj63ABDh8+TEpKivF2REQEdnZ2VK1aFa1Wy/z58/n26ylEhR8gas8BInfuI3L7PtwrubHg57loryWRcTcVgPBtu9ElpqNPyeDu3bucOXOGunXrPvZ5qFQqk/ObkyTAZvTPJDiDLIQhhCjTFEoltYOa8/K3M+j4xrs4uFTkwd07bAn7iTn/eZ0TO7ei1+vMHaYQZmFtbU1QUBBfffUVJ0+eZMeOHXz22Wcm+4wcOZLExEQGDhxIZGQkZ8+eZcGCBZw+fTpz5FhvID09naEvv8LRA4dZ/cdKxo39nDdeeY2M+GRWzlvG3bt3eanbIOq61aJu5Vr4eNTGt7o3vTr3YO6ieQ/PlJm7fPHj12w/sJvj504yZMgQKlSoQK9evUxiunHjBvHx8Vy8eJGlS5eyYMECevbsWQSv2JPJcKMZZS2Eoc3I3gdYLokQouxSqlTUC21HnWatOLplPRHLl3DvxnX+nvE/9q9aRrP+g6nVNLjARtaEKClmz57NsGHDCAgIwNvbmylTptChQwcMBgOGDD3l7JzYtHYDH37yEa1atUKlVNGgnh9NvPzRXk1Cn5JB65BW1KxcjdYd25CWnk7/nv0Y+85HoDcwd8l82jQPxcm5HKgUKFTKh/8q6DfwOb77eSonb55HXSGzLOnrb6Yw+qMPOHv2LP7+/qxevRoLCwuTmL29vYHMWuKqVavy2muvGVu7mZtkW2aU20IYKqkBFkII1BoNDTt1p15oew5tWMOBVcu4feUSf/3vSyrV8KL5wBfx9GsoibAoNbL3882yYsUK0BvQp+mo7VmLXRu2Y9AZQKfHoDOQdu0+6Axo4x8A4Fu5NmvmLc/9BApAoWDcJ58zfuw4UClRZEt016xfC0pFru+poJYhxvrjrDibN2+ea+9fyOxpnN8WcEVNEmAzynUlOFkIQwghjDRWVjTt2Q+/dp2IWrOCqLWruH7hLH9++TlVfOrRfMBLVK7jY+4whci3rLKE7AmtQWeAjGzf6/R5P6AxoVWAOtv3D7crbTQo01VoXGyefKwyQLItM8qqAU7LkC4QQgjxOFa2djQb8CINO3Vn38qlHN64lisnjrF43BhqNGpCswEvUrFaDXOHKQTw7+TWgOFhgotOjyHj4Xa9HvI6SKrKmdAq1P98/6iRW/FokgCb0T8jwAbpAiGEEHlg4+hE65eHE9C1JxF/LubY9s1cOHiACwcP4B3cgpD+L+DsXsXcYYpS7NHJrWmim/fkNrMMIXtya7qtYJLbfy9p/LRKQnlDXki2ZUamJRCZI8BSAyyEEE/mUKEiHV57myY9+rLnj985vXcnp8N3cSZiD76hbQnuNwiHChXNHaYoYZ6U3JJVmpDXBPDfiaxxYlnBJrci/yQBNiPThTAyR4AVkgALIUSelXOrTLd3xtC0Zz/2/PEbF6L2c2zbJk7u2oZf+84E9uqPrVM5c4cpigmDPnsim1mO8NTJrTJbQqs27Zpg/F6S22JLEmAzyhoBTpcRYCGEeCYVq9Wg95jPuXbmJLsXzefyiaMc+ns1R7duJKBLTxp364OVnZ25wxSFSKmDjBsppCemoE/ToUtKJyOZZ0hus9fbSnJb2kgCbEbGEoiMbDXA0gVCCCGemnvtujz3+ZdcOnqY3YvnEX/+LPtW/EH0xrU06d6X+u06mztE8YwMOj0ZN1NIv5qENu4B2mtJpMc9oGGKM7f3HybDXoG+tS26JC16dS5J6r+TW5PvHya3SkluSzvJtszIQp29BCKrC4RcEiGEeBYKhQJPP3886jfgXGQEexYv4PaVS+xePJ+odauw9fIho317NBqNuUMVT6BP1xmTXO21B6RfS0J7/QFk5D6Sq7BSoSpviUKjRGmtRmVtkXNimSS3AkmAzcpkEpw+qwuElEAIIURBUCgUeDUJpmZAU07t2cnepb9z73o8KVHhLHj/HMHPDcK3ZVv5vVtM6JLS0V57gDYuifRrmUlvxq2UXLspKCxVaNxtsXCzQ+Nuh6KiJVsid9Cpexd0Oh1JMTGoHS1RWVkW/RMRJYIkwGZkWgP8cCU4KYEQQogCpVSq8GnRGu/gFhzevJ6di+dz//ZNNv7yIwdW/UmzAYOpHdgMhVJp7lDLBIPBgO5uWmbpwsORXe21JHSJ6bnur7S3wMLdFo17ZrJr4W6LqpyVyUiuVqtFL/+PEfkg2ZYZZS2EkS4LYQghRKFTqdXUb9uRiw/ScFcbiFy9nLtxV1kz9WtcqtWg+cAXqe7fWCY3FSCDTo/2RsrDEoaHI7txSRhSdTl3VoC6vDWah8muhbsdGjdbVPYWRR94GTVkyBDmzZtnvO3s7EyTJk2YMmUKfn5+eT5ObGws1atXN97WaDR4eHgwZMgQPv3002LxHpME2IxyXQpZaoCFEKJQKdVqGnXpgn/7LkStXUnU2hXcjL3Aiq8m4O7tQ4uBL1HFp565wyxx9Gk6tPEPJ6VlTVB7VL2uSoHG1RaNmy0WlTMTXY2bLUpL+Rtobp06dWLOnDkAxMfH89lnn9GtWzcuXbqU72Nt3rwZX19f0tLS2L17N6+++ipubm4MGzasoMPON/m8x4xMV4J7OAKslhFgIYQoCpY2NoQ89zzDfpxF4+59UGssuHb6BEsmfMSyL8YSf/6suUMstnRJ6aSeuUvi9svcXniS+G8juTZ+Lzd/PkzCqvMkR15HezUJMgworFRYVHfErpk75Z6rTcV3GlF5YgiVRjXEuV9t7ILdsazmWOaT39DQUN5++23GjBmDs7Mzrq6ujB8/HsgcUVUoFERHRxv3T0hIQKFQsH37duO248eP061bNxwcHLC3t6dFixacP38eyBzd7dWrFxMmTMDFxQUHBwdef/110tNNS08sLS1xdXXF1dUVf39/PvroIy5fvszNmzdNYlm8eDEhISFYWVlRr149duzYkeM5lS9fHldXVzw9PXnhhRdo1qwZBw8eLNgX7imV7Z82M8ttIQylUhJgIYQoSjYOjrQaPJSALj2JWL6Eo1s3cPHIIS4eOYRX0xCaDRhM+Soe5g7TLAwGA7o7qcZJadq4zE4M+kfV6zpYGEsXjPW6zlZm/cjbYDCg1WrNcm6NRpOv5z5v3jxGjx7Nvn37CA8PZ8iQITRr1gwvL68nPvbq1au0bNmS0NBQtm7dioODA3v27CHjYYklwJYtW7CysmL79u3ExsbyyiuvUL58eb744otcj5mUlMRvv/1GrVq1KF++vMl9H3zwAVOnTsXHx4f//e9/dO/enZiYmBz7ZYmMjCQqKoqXXnopz69HYZIE2IyMk+AyZCEMIYQwNzvn8rR79U0ad+9D+NLfObF7O2f37+XsgXB8WrQm5Lnncazoau4wC41Bp0d7Pfmf3roPJ6gZ0vJYr+tui8qu+NXrarVavvzyS7Oc+5NPPsHCIu+viZ+fH+PGjQPAy8uL6dOns2XLljwlwDNmzMDR0ZHFixcbW/zVrl3bZB8LCwtmz56NjY0Nvr6+TJw4kQ8++ID//ve/KB9OAl2zZg12DxeNefDgAW5ubqxZs8Z4f5aRI0fSt29fAH7++WfWr19PWFgYY8aMMe4TEhKCUqkkPT0drVbLiBEjJAEG2LlzJ9988w1RUVHExcWxYsUKevXqZbzfYDAwbtw4Zs6cSUJCAs2aNePnn382+UG4c+cOo0aNYvXq1SiVSvr27csPP/xgvHjFWa41wNIFQgghzMqpkiudR75Hk5792LPkN84dCOfEzq2c2rOT+m06ENRnAHbOuY9ylRT6NB3auGy9deMeoI1/ALo81Ou626FxtUVpKQM2Be3fE83c3Ny4ceNGnh4bHR1NixYtHtvfukGDBtjY2BhvBwcHk5SUxOXLl/H09ASgdevW/PzzzwDcvXuXn376ic6dO7N//37jPlmPzaJWq2ncuDEnT540Od+SJUuoW7cuWq2WY8eOMWrUKMqVK8dXX32Vp+dUmMyabT148IAGDRowdOhQ+vTpk+P+KVOm8OOPPzJv3jyqV6/O2LFj6dixIydOnMDKygqAF154gbi4ODZt2oRWq+WVV15hxIgRLFy4sKifTr79sxCGIdskOPmFIoQQxUGFqp70fP9T4s+dYfeSBVw8cojDm9ZxfPtm/Dt1o2nPfljbO5g7zCfS3U83li5kLSiRcfsR/XWtVGjc7EzajmkqWmeukFZCaTQaPvnkE7Od+1n2VygU6PV64+irIdtSzv8u67C2tn7KKE3Z2tpSq1Yt4+1Zs2bh6OjIzJkzmTRpUr6OVbVqVeOx6taty/nz5xk7dizjx4835nHmYtYEuHPnznTunPuylAaDgalTp/LZZ5/Rs2dPAObPn0+lSpVYuXIlAwcO5OTJk6xfv54DBw7QuHFjAKZNm0aXLl349ttvcXd3L7Ln8jRMR4BlJTghhCiOXGvVpt+n/+Xy8SPsWjyfuDOniFy9nCOb19O4W28CuvbEwtrmyQcqZP/U6/7TWzf92gP093Ov11U5WDxMcm0fljDYoSpnWSxaVBUkhUKRrzKE4sjFxQWAuLg4GjZsCGAyIQ4yR4/nzZuHVqt9ZOJ9+PBhUlJSjMlyREQEdnZ2VK1a9ZHnVigUKJVKUlJSTLZHRETQsmVLADIyMoiKimLkyJGPfR4qlYqMjAzS09PLdgL8ODExMcTHx9OuXTvjNkdHRwIDAwkPD2fgwIGEh4fj5ORkTH4B2rVrh1KpZN++ffTu3TvXY6elpZGWlma8nZiYCGT+b6ooCuWzzqEw6AFIz9Che1ikbiDn/+pE8Zd1zeTalS5yXUufZ7mmrrXr0m/sl8RGRxG+9HduXYpl79LfObh+NY2798GvXSfUFkWz8pghQ0/GzRQy4h6gjUsmI/4BGXHJj6zXVZW3Qu1mi8bVBrW7bWYJg51pkmQAkwlTJUn266rT6TAYDOj1evR6vZkjy5+suLPfNhgMWFpaEhQUxFdffYWnpyc3btzgs88+AzA+zzfffJNp06YxYMAAPvroIxwdHYmIiKBp06Z4e3tjMBhIT09n6NChfPrpp8TGxjJu3Djeeust43EMBgOpqalcu3YNyCyBmDFjBklJSXTt2tXkNZ0xYwY1a9akbt26TJ06lbt37zJkyBCTfW7evMm1a9fIyMjg6NGj/PDDD7Ru3Ro7O7tHXpusOLJ+FrO/Vwvyd3GxTYDj4+MBqFSpksn2SpUqGe+Lj4+nYsWKJver1WqcnZ2N++Rm8uTJTJgwIcf2jRs3mtTGFLZDUQcANQn3H5CSnAzAnr17sTx5ushiEAVr06ZN5g5BFAK5rqXPs15Tx2btUFW9wJ0jkaTeT2T3wrmEr1iKc72GONT0LtBV5ZQ6sH6gxuaBCpsHamySVVglq1Aaco7U6hUGUmx0JNtmkGKrI9lWR4pNxj+rpCUBZx5+lUKbNm1CrVbj6upKUlJSjhZfxVnWyGjWoFzWNq1WS2JiIlOnTmXUqFE0adKEWrVqMWHCBPr06UNycjKJiYloNBpWrlzJuHHjaN26NSqVinr16tGgQQMSExPRarW0bNkSDw8PWrVqRXp6On379uU///mPyUDghg0bqFy5MgD29vZ4eXkxd+5cGjVqRGJiIklJSQCMHTuWyZMnc/ToUWrUqMHChQuxsLAw2adDhw5A5shvpUqVaN++PZ999pnJc/y39PR0UlJS2Lt3L2D6Xk1+mCsVhGKbABemjz/+mNGjRxtvJyYmUrVqVTp06ICDQ+HXc2m1WjZt2kSz4CC+PxaJxsIKjVqFDmgVGopz5Ud/FCGKp6xr2r59+3zXfIniS65r6VPQ11Sv03Fy1zb2rVhC0u1b3Dywm7SLZ2naqz91mrXK97wOXVI6GXHJZFzLnJSWEZeM7k7qI+t1jaO6braZXy5WJbpe92llv646nY7Lly9jZ2dn9o/Z82Pnzp05tq1evdr4fZMmTYiIiDC5X6czHfEPCQl55H/uNBoNarWayZMnM3ny5Fz3+e233/jtt98eG2dWk4FGjRqxf//+XPepV69ejtjyKjU1FWtra0JCQti5c6fJe/VxiXN+FdsE2NU1s9XM9evXcXNzM26/fv06/v7+xn3+PTsyIyODO3fuGB+fG0tLSywtc35MpdFoivSPnI1lZk2SVv/PJDgLKyv5Q1uCFfXPkCgacl1LnwK7phoN/u07U69VW45sWc++FX+QeOM6m3+dRuRfywjqM5C6zUNzJMIGfbZ63Wxtx/T3c/+It6zU6z4rjUaDUqk01q3+u3VXWaZQKIyvy7PIenxhvb5Z10/9sCtW9vdqQf4eLrYJcPXq1XF1dWXLli3GhDcxMZF9+/bxxhtvAJktOBISEoiKiiIgIACArVu3otfrCQwMNFfoeabJ6gKRIQthCCFESaa2sKBR5x7Ub92B6I1rOfDXnyTEx7H+p+/Zv2IZIe36U7mSNxlxKcak95H9dStYGxeR0DxcVKI49tcVoiQzawKclJTEuXPnjLdjYmKIjo7G2dkZDw8P3n33XSZNmoSXl5exDZq7u7uxV3DdunXp1KkTw4cP55dffkGr1TJy5EgGDhxY7DtAQLaFMHR6WQpZCCFKAZVCjZ9fO7wcA7geeRrttQfYq51R7VaRwHnTndWZ/XWzFpHQuGUmu0oL+TsgCt7cuXML5DjVqlUzacdWUpk1AY6MjKR169bG21l1uS+//DJz585lzJgxPHjwgBEjRpCQkEDz5s1Zv369SU3P77//zsiRI2nbtq1xIYwff/yxyJ/L0zC2QcvQYXg4G1IlbdCEEKJEyUhII/XEbVKO3yIt5h48nNxuiw1oMidWp+tTuZt2nYT0G2htM6jRPphabYJQauR3vhDmYNZ3Xmho6GP/F6FQKJg4cSITJ0585D7Ozs4lYtGL3GhUD2u3DP+0ApE+wEIIUbwZDAYybiSTcvw2KSduo72SZHK/ytHCWLqQVa+bYZnB9b9Xc3LtPtLuPOD47J1U2FyNkH7PU6tJUIF2jRBCPJlkW2Zk8XAEWJk9AZYSCCGEKHYMegPpl++TcuI2qcdvk3Er26IACrDwcMDatzzWPuVRV8i5IpcaCO43iIaduxO1dhUH163i1qVY/vrfl7hUq0FIv+ep2ThQJrUVoNLwMX1ZlNUfuLDfC5IAm1FWCYQSGQEWQojixpChJ+18AiknMkd6TTo0qBRY1XLCyrc81nXLo7LP2yQ1K1s7mvV/gUZdehC1ZiUH//6Lm7EXWPXtJCrVqEVwv+ep0aiJJMLPQKPRoFAouHnzJi4uLvJalhBZC3XcvHkTpVJZ6J13JNsyo6wSiOwjwKp89owUQghRcPRpGaSevkvK8duknrpj0qlBYanCqo4z1r7lsapdDqXV0/8Jtbazp/nAFx8mwis4tH4N1y+cY+WUibjW9CLkuReo5h8gydtTUKlUVKlShStXrhAbG2vucEQ+2djY4OHhISPApZlCoUCjUqDMyBruV0odmBBCFDHd/XRST94h5fgtUs8lgO6fj86V9hqsfTJLGyxrOqFQF+zvaBsHR1o8P4SAbr058NefRG9cS/z5syz/ajxuXt6EPPcCnn4NJRHOJzs7O7y8vGQZ8xJGpVKhVqtRKBSFfu0kATYzjUppLIGQ+l8hhCgaGXdSST19nZQTt0m/mGiy0pq6vBVWvhWw9i2PRVV7FMrCTz5tHBxpNXgojR8mwoc3riPu7Gn+/PJz3L19CHnueTzqNZBEOB9UKpV8qioeSRJgM9OolBgelkDIIhhCCFE4DAYD2msPeHD0BnUPO3A7PNrkfk1lu8xJbL7lUVe0MVuiaetUjtCXXqVx9z4cWLWMw5v/5trpEyyb9BlV6tYj5LnnqerrZ5bYhChNJAE2M41Kic4gI8BCCFHQDDoD6RfvZbYrO34bXUIaADaoQQmW1R2x9imPlW8F1E6WZo7WlF05Z1oPGUGTHn3Zv2oZRzb/zZWTx/hj4idU9fUj5LnnqVK3nrnDFKLEkgTYzCxUCtKySiCkA4QQQjwTg1ZH6tmEzElsJ2+jT84w3qfQKLGo5cgp7WWaPtcKS0cbM0aaN3bO5Wnzyms06dGXfSuXcnTLBi4fP8KS40fwqO9PyHMvUNm7rrnDFKLEkYzLzDRqJemGrFXgZARYCCHyS5+sJeX0XVKP3SL1zF0M2mytJW3Uxs4Nll7l0Cn03Fl3HqVN4bZYKmj25SvQbtgbNO3Zl33L/+DY9k1cOhrNpaPRVGvQiJDnXsDNy9vcYQpRYkgCbGYWKiWpxklwcjmEECIvdPfSMvvzHr9N2oV7oP9nFpvK0TKzVZlveSyrOaJQ/VPPq8uWHJdEDhUq0n7ESJr2eo6I5Us4vmMzsYcPEnv4INUbNibkuRdwrell7jCFKPYk4zIzjUpp7AOslBFgIYR4JG3W8sPHb+VYflhdyca4Epumsl2p75bgWLESHV9/m8CHifCJXVuJORRJzKFIagQ0JaTf81SqUcvcYQpRbEkCbGYadfYEWC6HEEJkMegNpF+5T+rxzJXYMm7msvywT+ZIryaX5YfLAidXNzq9+S6BvZ8j4s/FnNy9gwtR+7kQtZ9aTYII7vc8FavVMHeYQhQ7knGZmYVKYewDLDXAQoiyzpChJ+3CvX+WH05M/+dOlQLLmk7Gkd68Lj9cFpRzq0znke8R2GcA4csWcWrvTs4diODcgQi8AkMI7vc8Lh7VzB2mEMWGJMBmZlICITXAQogySJ+mI/XMnX+WH0791/LD3uUya3q9nZ9p+eGywNm9Cl3f/oCgh4nw6YjdnN23l7P79lI7uAUh/QZRvoqHucMUwuzy/Ztk/fr12NnZ0bx5cwBmzJjBzJkz8fHxYcaMGZQrV67AgyzNTBJgWQhDCFFG6JKylh++Teq5u5CRbflhO42xtMGqEJYfLgvKV/Gg27sfEnQpMxE+s28PZ8J3cSZiN3VCWhLUdyDlK1c1d5hCmE2+E+APPviAr7/+GoCjR4/y3nvvMXr0aLZt28bo0aOZM2dOgQdZmslSyEKIsiLjTqpxEtu/lx9WlbcyljZYeDgUyfLDZUEFj2p0H/0xNy/GEL5sEWf37+XUnh2c3ruLOs1aEtR3EM7ulc0dphBFLt8JcExMDD4+PgD8+eefdOvWjS+//JKDBw/SpUuXAg+wtLNQK2QSnBCiVDIYDGjjHpD6sF2ZNu6Byf2aynZY+zxcfriS+ZYfLgtcPKvT471PuBF7gb1LF3I+MoKTu7dzas9O6rYIJajvQMq5ups7TCGKTL4zLgsLC5KTkwHYvHkzL730EgDOzs4kJiYWbHRlgLRBE0KUJga9gfSLiZkjvSduo7uT+s+diszlh60ejvSqy1mZL9AyqmK1GvT64DOuXzjH3qW/c+HgAU7s3MrJ3dvxadmGoD4Dcarkau4whSh0+U6AmzdvzujRo2nWrBn79+9nyZIlAJw5c4YqVaoUeIClnUW2EgiVTIITQpRABq2e1HN3Hy4/fAf9A+0/d6qVWNUul1nTW9cZlW3JWoGttKpUoxa9PxxH3LnThC9dSEx0FMe3b+bkrm34tmpLUJ+BOLhUNHeYQhSafGdc06dP580332TZsmX8/PPPVK6cWTv0999/06lTpwIPsLQz7QMsI8BCiJJBn5JB6umHk9hO38GQ/s8KawprNdZZyw/XLofSQn63FVdutbzp8/EErp05yd6lC7l45BBHt27k+I6t1GvdjsDeA3Co4GLuMIUocPlOgD08PFizZk2O7d9//32BBFTWWKiUqJAaYCFE8adLzLb88Pl/Lz9sgdXDel7L6o4oVNK5oSRxr12Xfp/+l6unTrB36e9cOnaYI5vXc3z7Zuq37UjTXs9h71zB3GEKUWCeKuM6f/48c+bM4fz58/zwww9UrFiRv//+Gw8PD3x9fQs6xlJNo1LICLAQotjS3sxcfjj1+G3SL983uU9d8eHyw75lY/nhsqByHR+eG/sFV04cY8/S37hy4hjRG9ZydOtG/Np1omnP57Ar52zuMIV4ZvlOgHfs2EHnzp1p1qwZO3fu5IsvvqBixYocPnyYsLAwli1bVhhxllrZJ8FJDbAQwtwMBgPaK0kPJ7HdIuNGisn9Fh72mYtS+JRH42JjpihFYaviU48B477i0rEj7F36G1dPneDQ36s5unkDDTp0pkmPftg6Sd9/UXLlO+P66KOPmDRpEqNHj8be3t64vU2bNkyfPr1AgysLsvcBVshCGEIIMzDo/ll+OPX4bXS5LT/s83D5YQdZfrgs8ajnR1Xfr7l09DB7lv5G3JlTRK1dxeFN6/Hv2JUm3ftg4+hk7jCFyLd8J8BHjx5l4cKFObZXrFiRW7duFUhQZYmFOvsIsCTAQoiioU/XkXYms3NDysk7GFIzjPcpLP61/LC1fDpVlikUCjz9/PGo34CLhw+yd+lC4s6dJnL1cqI3rqVhx2407t4HGwdHc4cqRJ7l+7eak5MTcXFxVK9e3WT7oUOHjB0hRN6Z1gDLHxkhROHRPdCSejJzElvq2QTI+Kdzg9L2X8sPa2QSmzClUCio5h+AZ4NGxERHsvePhVy/cJYDf/1J9MZ1NOzUjcbdemNt72DuUIV4onxnXAMHDuTDDz9k6dKlKBQK9Ho9e/bs4f333zcuiiHyzmQpZJkEJ4QoYBl3s5Yfvk167D3T5YedrTJLG+rJ8sMi7xQKBTUaNqG6f2MuHNzP3j8WciP2PPtXLiV6wxoade5BQNfeWNnZmTtUIR4p3wnwl19+yVtvvUXVqlXR6XT4+Pig0+l4/vnn+eyzzwojxlItewmEUibBCSEKgC4pnQcH4kk5egvttX8tP+xmm1na4FsBjassPyyenkKhoGZAIDUaNeVcZAThSxdy82IMEcuXcPDv1QR07UmjLj2xspVEWBQ/T7UU8syZMxk7dizHjh0jKSmJhg0b4uXlVRjxlXrZR4BVMgIshHgG2hvJJO2+yoOD1yHj4VCvAiyqOWa2K/Mpj9pZlh8WBUuhUODVJJhaAYGcPRBO+NKF3Lp8kfBlizj4918EdO1Fo849sbSRriGi+HjqIUcPDw88PDwKMpYyyUKllBpgIcRTMxgMpF24R9Kuq6SeumPcrqlqj11T18zlh+2kc4MofAqlktqBzfBqEsyZfXsIX7aI21cusfeP3zm47i8ad+tNw07dsLCWRFiYX74zrqFDhz72/tmzZz91MGWRRiVLIQsh8s+g05Ny5Bb3d19FezUpc6MCrHzKY9+iMhaeDlLeIMxCoVTiHdwCr8AQTofvJnzZIu5eu8LuxfOJXLuSJt374N+xKxZW1uYOVZRh+U6A7969a3Jbq9Vy7NgxEhISaNOmTYEFVlZoVIp/lkKWGmAhxBPoUzN4sC+epL1X0d3L7Ner0CixCaiEXfPKaCpIUiGKB6VSRd1mrfAObs6pPTuJ+HMRd+OusWvhXCLXrKBJj774d+iCxlLKckTRy3fGtWLFihzb9Ho9b7zxBjVr1iyQoMoSTfZJcLIQhhDiETLuppK05xoPDsRjSNMBoLTTYBfijm2gGypbjZkjFCJ3SqUKnxatqRPSkpO7txP+5yLuXY9n52+ziVy9nKY9++HXvjMaC0tzhyrKkAIZclQqlYwePZrQ0FDGjBlTEIcsM0xqgGUhDCHEv6Rfvs/9XVdIOXaLhx8Woa5kg32Lytj4V0Shln69omRQqlT4tmpLnWatOLFrKxF/LiHx5nW2z5/FgaxEuG0n1BZSsy4KX4F95n7+/HkyMjKevKMwYdoFQkoghBBg0BtIPXmH+7uukB6baNxuWcsJ+5ZVsPRykvpeUWKp1Grqt+6AT4s2HN+xhYjli7l/6ybb5v7KgVXLaNq7P/XbdEStkU81ROHJd8Y1evRok9sGg4G4uDjWrl3Lyy+/XGCBlRWmK8HJCLAQZZk+XUfywesk7b5Gxq2UzI0qBTYNXLBrXhkLd+mnKkoPlVqNX9uO+LZqw7Ftm4lYsYSk27fYOvsX9q9aRlDv/tRr3R6VWhJhUfDynQAfOnTI5LZSqcTFxYXvvvvuiR0iRE4mXSBkEpwQZZLufjpJ4dd4EBGHPjnzkzSFlRq7IFfsQtxROUhtpCi9VGoNDdp3xje0Hce2bmTfw0R486yf2LdyKUF9BuDbqh0q+RspClC+f5q2bdtWGHGUWZZqWQpZiLJKe/0B93ddJfnQDdBlLlyhcrbCvpk7No1dUVrK7wRRdqg1Gvw7dqVe6/Yc2bKe/SuXcv/WTTb9Op39K5cS2GcAPi3aSCIsCoT8FJlZ9hFgqQEWovQzGAyknU/IXLji9D9tJS087LFrURlr3woolFLfK8outYUFjTr3oH7bjhzZ9Df7Vy3j3o3rbPzlR/avWEpQ34HUbR4qg0bimeQp42rYsGGeJ1wcPHjwmQIqazTZR4ClC4QQpZYhQ0/ykZsk7bqKNu5B5kYFWPuUx65lFSw9HcwboBDFjMbCkoCuvfBr24noTes48NefJFyPY/1P37NvxRKC+g6iTrOW0kJUPJU8JcC9evUq5DDKruyT4BTyJhai1NEna0naH0/S3mvoE7MtXNG4EvbNK6MuLwtXCPE4GisrmnTvQ4P2nYnesJYDq5dzN+4af0//jn3LlxDUbxA1GgeaO0xRwuQpAR43blxhx1FmZe8DjEL6eQpRWmTcSSVp91UeRMZjSH/4KY+9BruQytgFuqK0kZntQuSHhZU1TXv2w79DFw6tX0Pk6uXcuXaFdT9+g3Plqmg8aqHLyEAj7dNEHkjRqZlpVErjUsh6hYwAC1HSpV1KJGnX1cyFKzLntaFxtcGueRVs/F1k4QohnpGFtQ2Bvfvj37EbB/9eRdTaldy5ehmuXmb+mWMEdO1J/TYdsLC2MXeoohjL929inU7Ht99+S9OmTXF1dcXZ2dnkqyDpdDrGjh1L9erVsba2pmbNmvz3v//FYDAY9zEYDHz++ee4ublhbW1Nu3btOHv2bIHGUZiyT4LTywiwECWSQW8g5dgtbvx8mJs/HSblaGbya+nlRIWh9aj4TiNsG1eS5FeIAmRpY0Nw30G8Oi2MoH7Po7Ky5v7tm2yfP4tf33yFnQvnknTntrnDFMVUvkeAJ0yYwKxZs3jvvff47LPP+PTTT4mNjWXlypV8/vnnBRrc119/zc8//8y8efPw9fUlMjKSV155BUdHR95++20ApkyZwo8//si8efOoXr06Y8eOpWPHjpw4cQIrK6sCjacwaFQKFJIAC1Ei6dN1JEdd5/7uq+hup2ZuVCmw8a+IfYvKaFxtzRugEGWAla0dTXs9xw2VJdVsrTj091/cjbvKgVXLiFqzkrotQmnSvQ/lq3iYO1RRjOQ7Af7999+ZOXMmXbt2Zfz48QwaNIiaNWvi5+dHRESEMTEtCHv37qVnz5507doVgGrVqrFo0SL2798PZI7+Tp06lc8++4yePXsCMH/+fCpVqsTKlSsZOHBggcVSWBQKRbYSCGl9JERJoEvMXLgiKSIOQ8rDhSus1dgFuWEX7I7KwcLMEQpR9ihVauq16YB/+86cj9rPgdXLuXb6BMe3b+b49s3UaNSExt37UKVuPVlKXOQ/AY6Pj6d+/foA2NnZce/ePQC6devG2LFjCzS4kJAQfv31V86cOUPt2rU5fPgwu3fv5n//+x8AMTExxMfH065dO+NjHB0dCQwMJDw8/JEJcFpaGmlpacbbiYmJAGi1WrRabYE+h9xknSPr36w2aGkZhiI5vyh4/76monT493XVxieTvDeO1CO3/lm4opwlNs3csG7ogsIi87+zevk5KLbkvVo6/fu6evoH4OkfQNzZUxxcu5LzUfu5cPAAFw4eoFKNWjTq2ouajYOkl3Axltt7tSDft/lOgKtUqUJcXBweHh7UrFmTjRs30qhRIw4cOIClZcEu1/nRRx+RmJhInTp1UKlU6HQ6vvjiC1544QUgMxkHqFSpksnjKlWqZLwvN5MnT2bChAk5tm/cuBEbm6Irmt+0aROAsQZ434FILl08V2TnFwUv65qKUsQAEUu3UemaNY73/pldnmSv5bpbKgnOd+B2HGw2Y4wi3+S9Wjrldl0Vtf3wcPUg4dRR7l84y/UL5/h72reobe1xqlMfh5q1Uaqlc0Rxlf2aJicnF9hx850A9+7dmy1bthAYGMioUaMYPHgwYWFhXLp0if/85z8FFhjAH3/8we+//87ChQvx9fUlOjqad999F3d3d15++eWnPu7HH3/M6NGjjbcTExOpWrUqHTp0wMGh8JvRa7VaNm3aRPv27dFoNJxaNAeAhk2CaORbo9DPLwrev6+pKPkMegMPDl7n5qZz2CQ//FWpAEsfZ2yauVGpqj01zRuieAryXi2d8npdk+8lcGTTOo5s/pvUpPvcitpL0ukj+LXrjF/7Ltg4OhVd0OKxcrumWZ/YF4Q8J8DTp09n8ODBfPXVV8ZtAwYMwMPDg/DwcLy8vOjevXuBBQbwwQcf8NFHHxlLGerXr8/FixeZPHkyL7/8Mq6urgBcv34dNzc34+OuX7+Ov7//I49raWmZ62i1RqMp0l+IWefLKoFApZZfyCVcUf8MicKRej6Be2suoI17gA1qFBZKbBu7YtfMXRauKCXkvVo6Pem6OlZwocWglwnqPYBjOzYTtXYl967Hs3/lUg6uXYVPqzY07tabcm6VizBq8TjZr2lBvmfznAB/+umnjBkzht69ezNs2DDatGkDQHBwMMHBwQUWUHbJyckolaadEVQqFXp9ZsJYvXp1XF1d2bJlizHhTUxMZN++fbzxxhuFElNBMxgMxhIIHVKUL4Q5ZdxO4d66GFKOZ7ZOUlipuFLxPv6DW2LpIImvEKWFxsqKhh270aB9Z87uCydy9Z/Enz/Lkc3rObJlA7UaB9GkRx/ca9c1d6iikOQ5AY6Pj2fp0qXMmTOH9u3b4+HhwdChQxkyZAhVq1YtlOC6d+/OF198gYeHB76+vhw6dIj//e9/DB06FMjsoPDuu+8yadIkvLy8jG3Q3N3dS8zyzYaHyTyANv9tmYUQBUCfmkHitssk7b6aOblNAbaBbtiEuhO5YxNKa1kzSIjSSKlU4R3cnNpBzbhy8hiRq5dz4eABzh0I59yBcNxr16Vxjz7UCghEoZS/0aVJnn+rW1tb89JLL/HSSy9x4cIF5s6dS1hYGBMmTKBdu3YMGzaMXr16Fejw9LRp0xg7dixvvvkmN27cwN3dnddee82k3/CYMWN48OABI0aMICEhgebNm7N+/foS0QMYQKfL+Od7g7y5hChKBr2BB5HxJG68iD4pc3axpZcTTl1roHG1lU4BQpQRCoWCqj71qepTn9tXLhG5ZgUnd23j2pmT/PXtF5Rzq0zjbr3xadkGtYW0OSwNnirjqlGjBhMnTiQmJoa///6b8uXLM2TIECpXLtiaGXt7e6ZOncrFixdJSUnh/PnzTJo0CYtsP3wKhYKJEycSHx9Pamoqmzdvpnbt2gUaR2Ey6HTG7zMMUgIhRFFJPZ/AjWmHSFh+Dn2SFnUFa8q/7EOFofVkAQshyrDyVTzo+Po7vDp9Nk179sPSxpa7cVfZNHM6M0cOJeLPxaTcL7jJWMI8nulzPYVCgVqtRqFQYDBID9unocueAEsNsBCFLuN2CgnrYkg11vmqcWjngV2QmyxVLIQwsivnTIvnhxDYuz9Ht24kau0q7t++yZ4/fmPfqqXUb92BgK49cazoau5QxVN4qgT48uXLzJkzh7lz53Lp0iVatmzJzJkz6du3b0HHV+rpM/4pgUjXP2ZHIcQzeVSdr0N7T1S20g1ACJE7C2sbArr2wr9jN86E7+LA6uXcvBjDofWrid6wFq+gZjTp3gfXml7mDlXkQ54T4PT0dJYvX87s2bPZunUrbm5uvPzyywwdOpQaNaR37dPSPxwB1qEkQxJgIQrck+p8hRAiL1RqNXVbtKZO81AuHT3MgdV/cvHIIc6E7+JM+C6q+tSncY8+VPdvLEstlwB5ToBdXV1JTk6mW7durF69mo4dO+ZoUSbyT/9wEpxeoSRdMmAhClT2fr4A6grWOHargZV3OfkDJYR4KgqFAk8/fzz9/LkRe4HINSs4vXcnl08c5fKJo5Sv4kHj7n2o27wVKllhrtjKcwL82Wef8eKLL+Li4lKY8ZQ5uozMEWC9QolWJwmwEAVB6nyFEEWhYrUadBn5Hs0HvsTBdas4smUDt69cYsPPU9mzeD4NO/fAr10nrGztzB2q+Jc8J8DZlw4WBcc4AowkwEI8qxx1vsqHdb7tpM5XCFF4HCq4EPrSqwT1HciRzes5+PdfJN29w66Fc9m3Ygn123SkUZeeOFSQQcTiQrq7m1lWDbBeoSRdEmAhnsoj63y71UBTSep8hRBFw8rWjqY9+xHQtScnd+8gcvVybl+5RNTalRxavxrvkJY07tabitVk7pS5SQJsZlldIPQo0WYYzByNECWP1PkKIYoblVpDvdB2+LZqS0x0JJF/LefyiaOc3LWNk7u24enXkCbd++JRv4H8njITSYDNTK+XGmAhnobU+QohijuFQkGNhk2o0bAJ8efPcmD1cs5G7OHikUNcPHIIl2o1aNKtN7WDW6BSS0pWlJ761U5PTycmJoaaNWuilov21PQyCU6IfNGnZpC49TJJe6TOVwhRcrjW9KL7ux+ScD2eg+tWcXTbRm7GXmDd9O/YtXg+AV16Ur9NByysbcwdapmQ72GS5ORkhg0bho2NDb6+vly6dAmAUaNG8dVXXxV4gKWd7uEkOB1SAyzE4xj0BpL2xxH/bSRJO6+AzoCllxOV3mlEuZ61JPkVQpQITpVcafPKa4yYMYdm/Qdj4+jE/Vs32T5/Fr++9Qq7Fs4l6e4dc4dZ6uU7Af744485fPgw27dvx8rKyri9Xbt2LFmypECDKwuyT4KTEWAhcpd6PoEbPx4iYfk59Ela1BWsKT/ElwpD68kkNyFEiWRt70BQ34EMnz6b9sNHUs6tMmkPHrB/1TJmjRzKhl9+4PaVS+YOs9TKd+3CypUrWbJkCUFBQSaF276+vpw/f75AgysLZCEMIR5N6nyFEKWd2sICv3adqN+mA+ei9hH513KunTnJsW2bOLZtEzUaNaFx9z5UqVtPJswVoHwnwDdv3qRixYo5tj948EAuzFMw1gCjRK+TLhBCgNT5CiHKHoVSiVeTYLyaBHP19EkiVy/nXGQEFw4e4MLBA7jW9KJx9754BQajVKrMHW6Jl+8EuHHjxqxdu5ZRo0YBGJPeWbNmERwcXLDRlQG67CPAUgIhyjjp5yuEEFDZuy6VvT/lzrWrRK1dwfEdW4g/f5Y1U7/CsZIrAV17US+0HRpLqycfTOQq3wnwl19+SefOnTlx4gQZGRn88MMPnDhxgr1797Jjx47CiLFUM9YAo0QrJRCiDEs9n8C91RfQxks/XyGEAHB2r0z74SNp1n8whzasIXrDWu5dj2fr7F/Yu3Qh/h260rBTN2wcHM0daomT7yK65s2bEx0dTUZGBvXr12fjxo1UrFiR8PBwAgICCiPGUs24EIZMghNlVMbtFG4tOMGtmUfRxj9AYaXGsVsNKv2nEdZ1nCX5FUKUeTaOTjTrP5gRM+bQZujrOFZyJfV+IhF/LmLmm6+wedYM7sZdNXeYJcpTNfCtWbMmM2fOLOhYyiTThTCkBliUHVLnK4QQ+aOxsqJhx240aN+Zs/vCiVz9J/Hnz3J4098c3ryeWo2DaNKjD+6165o71GIv3wmwSqUiLi4ux0S427dvU7FiRXQPP9IXeZN9EpzUAIuyQOp8hRDi2SiVKryDm1M7qBlXTh4jcvVyLhw8wLkD4Zw7EI67tw9NuvehZkBTFErpmJObfCfABkPuo5RpaWlYWFg8c0BlTfY2aFICIUo7qfMVQoiCo1AoqOpTn6o+9bl95RKRa1Zwctc2rp0+warTJyjnVpnG3Xrj07INasnRTOQ5Af7xxx+BzBd71qxZ2NnZGe/T6XTs3LmTOnXqFHyEpZxOFsIQZUDGrYf9fE/8q59vsBsKlYxOCCHEsypfxYOOr79DswEvcujvvzi86W/uxl1l08zp7Pnjt8zSiY5dsbazN3eoxUKeE+Dvv/8eyBwB/uWXX1Cp/ulBZ2FhQbVq1fjll18KPsJSLmsSnA5ZCEOUPgatnsTNF7m/W+p8hRCiKNiVc6bF80MI7N2fo1s3ErV2Ffdv32TPH7+xb9VS6rfuQEDXnjhWdDV3qGaV5wQ4JiYGgNatW7N8+XLKlStXaEGVJaZLIcskOFF6aK8/4M6i08ZyB8va5XDqWl3qfIUQoghYWNsQ0LUX/h27cSZ8FwdWL+fmxRgOrV9N9P+3d+fxcZ3l3f8/Z/bRrJJGqyVb3vcl+57YieMsEBIStiSFJKXhARIKhBYanpZA2h8U2ofS0gCFQgjQLKQEspDFxrETkjiJYzveLXmTZVvWNtLMaPblnN8foznSaLFlW9Joud6v17w0OmeWIx1p9NU9133dr/yReRdfxgUfuo2KWXMKfagFcdo1wBs2bBiN45iycjXAmowAi0lC0zQim1sIPn8ILaVicJopvnUutoXS0kwIIcaa0WRi4RWrWHD5Spp2bmfz87/jyI5t1G/6M/Wb/kzt4mVccNOt1K04b0q9Rp9RG7Rjx47x3HPP0dTURDKZzNv3/e9/f0QObKrI1QBnpAZYTAJqNEXXM/uJ7crW+lrnFVPy0XkYXTL5QgghCklRFGYsW8GMZStoazzEey/8nvq3Xufo7h0c3b2D0prpnH/TrSy8/CqMpslfonbaAXj9+vV86EMfYtasWezbt48lS5bQ2NiIpmmce+65o3GMk5oshCEmi0RjkM4n6skEE2BU8FxXh/PyaSiGqTOiIIQQE0F53SxuvP8rXP6JT7H1xWfZsf4V/MeaeOXHP+DNJ3/FOTd8iOXX3oC1aPKWrJ329OsHH3yQv/mbv2Hnzp3YbDZ+97vfcfToUa666io++tGPjsYxTmr6QhhIDbCYmLSMRuhPR2j/rx1kgglMpTbKP7cc15U1En6FEGIcc/vKWPmpv+IzP3qUK+64G0dxCeGuTv78+C/56efvZuOvf06oo73QhzkqTnsEeO/evTzxxBPZO5tMxGIxnE4nDz/8MDfffDOf+9znRvwgJzN9IQxFFsIQE086EKfzyXqSjSEAis4tx3vzbAzWM6quEkIIUQA2h5MLb/4I533gZva+8RrvPf8M/mNNbHnh92x76TkWXHol5990K2UzZhb6UEfMaf+Vcjgcet1vVVUVBw8eZPHixQB0dHSM7NFNAf0XwtA0bUoVoYuJK7qzg67f7UeLp1GsRopvmUPROeWnvqMQQohxyWgys2TlahZfdQ2H33+P9557hqN7drLnzxvY8+cNzFh2DhfcdBvTly6f8FnltAPwxRdfzBtvvMHChQu58cYb+cpXvsLOnTt55plnuPjii0fjGCc1vQ0aBjQNMqqGyTixf6jE5KYmMwT/eIjIOy0AmGtdlH5iPqZSe4GPTAghxEhQFIVZ51zArHMuoOXgfjY//wz7336TIzu2cWTHNsrqZnHBTbcy7+LLMZom5jt+p33U3//+9wmHwwB861vfIhwO89RTTzF37lzpAHEGMn0mwQEkMyomWRlLjFPJExE6n9hHui0KCriuqsF97QxZzU0IISapytlzuelLXyPQ2sLWF59l54a1tDce4sUf/it/fuIxLrjpVs65/qZCH+ZpO+0APGvWLP26w+GQ1d/OUt+FMABSaQ2kY5QYZzRNI/L2CQJ/PARpDYPLTMnH5mObKwviCCHEVOCtqOTqe/4Pl3zkdravfZFtr7xAd0c7Tfv2TMgAfNrDNrNmzcLv9w/YHggE8sKxGB69BpjeEWAhxpNMJIX/V3sIPHsQ0hq2BSVUfPFcCb9CCDEFRRUrJ2Zdzt4r/5q3q67mYNVFhT6kM3LaI8CNjY364g19JRIJjh8/PiIHNZXkRoAVoxFAegGLcSV+MEDnU/WooWS2t++NM3FeWj3hJz8IIYQYHlXV2HE8yMb6NjbUt7PjWAAt17XVNh9T0FbQ4ztTww7Azz33nH79lVdewePx6J9nMhnWr19PXV3diB7cVJCrATYYJACL8UPLqIT+1ET3xqOgganMTsntC7BUOwt9aEIIIUZZVyTJ6/vb2VjfzusN7fgj+av+Lqpys2pBGSvnl3NOrbcwB3mWhh2Ab7nlFiA7M/Cuu+7K22c2m6mrq+P//b//N6IHNxVoPQthKCYJwGJ8SHfG6XxyH8mmbgAcF1TiuWkWBouxwEcmhBBiNKiqxu7mUM8obxvvHw2g9lmby2U1cflcH6vml3PV/DIq3BNz1LevYQdgVc0Gs5kzZ7J582Z8Pt+oHdSkFm7H+IfPc0lrM3AjmZ6FMBRD9lQk07IanCic6PY2up45gJbIoNiMFN86l6JlZYU+LCGEECMsGE3x5wPtbNjXzmsN7XSEE3n7F1S6WDm/nJXzyzhvRjHmSdbt57RrgA8fPjwaxzF1GIwYDqylHEipaX0SnFFqgEUBqckMgWcPEt3SCoBlhpuST8zHVDzx/8sXQgiR7eaz50SIjfXtbKxvY2tTgEyfYV6Hxchlc3ysWlDOVfPKqPZO7t7uww7AmzZtwu/388EPflDf9qtf/YqHHnqISCTCLbfcwg9/+EOsVuuoHOikYXX3Xo+H9ElwRpMJ0hKAxdhLHg9ne/t2xLK9fVfV4r5mBoosyCKEEBNaKJ7izf0d2dDb0EZrKH+Ud265k1ULylk5r4zz60qwmCbXKO/JDDsAP/zww6xcuVIPwDt37uTTn/40d999NwsXLuRf/uVfqK6u5pvf/OZoHevkYDShmR0oqQgkgqi5SXBGI6QhmZYALMaGpmmE32wm+NJhyGgY3RaKPz4f22xvoQ9NCCHEGdA0jYbWMBvq29iwr40tR7pI9xnltZuNXDanVC9tqCkuKuDRFtawA/D777/PP/7jP+qfP/nkk1x00UX87Gc/A6C2tpaHHnpIAvBw2NyQikA8pLeUM5hMkJA+wGJsZMJJup5uIF7fBYBtUSnFt83F6DAX+MiEEEKcjnAizZsHOvTShhPBeN7+WWUOVs4rZ9WCMi6oK8FmlgnNcBoBuKuri4qKCv3z1157jRtuuEH//IILLuDo0aMje3STlc0D3SdQEsFBaoBlEpwYXfH9XdnevuEUmAx4PzgTx0VV0ttXCCEmAE3TONAWZmN9Oxvq29jc2JmXHawmA5fOLu0pbShneunUHeU9mWEH4IqKCg4fPkxtbS3JZJKtW7fyrW99S9/f3d2N2SyjR8OhWT0okF8DbMyeCqkBFqNFS6sE1x4h/PoxAEwVRZTevgBzpaPARyaEEOJkosk0bx3ws7GhjQ372jkeiOXtn1FaxKqesoaLZ5XKKO8wDDsA33jjjfzd3/0d3/3ud/nDH/5AUVERV1xxhb5/x44dzJ49e1QOctKx9UyEi/fWABvN0gVCjJ50Rwz/k/tIHQsD4Li4Cu8HZqLIi6QQQow7mqZxuCPChp6yhncOdeaVSFpMBi6eVcrKeWWsWlDOTJ8MZJyuYQfgf/zHf+TWW2/lqquuwul08thjj2GxWPT9v/jFL1izZs2IH+Dx48f52te+xksvvUQ0GmXOnDk8+uijnH/++UD2h+Shhx7iZz/7GYFAgMsuu4wf//jHzJ07d8SPZcTYsqvoKYmg3l/ZaMr1AZYALEZWZGsrgT8cREtmUOwmSj4yF/ti6eMthBDjSTyVYdMhPxv3ZZccbuqM5u2vKbazan62lvfiWaUUWU67k63oY9jfPZ/Px+uvv04wGMTpdOo1qzlPP/00TufILpPa1dXFZZddxqpVq3jppZcoKytj//79FBcX67f53ve+x3/8x3/w2GOPMXPmTP7hH/6B6667jj179mCzjc8eppq1ZxnpPiPAJpMJUKUGWIwYNZ7O9vbd1gaAZaabko8vwOSVVoVCCDEeHPFH2LCvjY0N7Ww66CfRZxDMbFS4aGYpK+dnlxyeXeaQuRoj6LT/ffB4PINuLykpOeuD6e+73/0utbW1PProo/q2mTNn6tc1TeMHP/gBf//3f8/NN98MZHsTV1RU8Ic//IFPfOITgz5uIpEgkejthRcKhQBIpVKkUqkR/zoGMDswAmo0QKZnEpzBYARU4skxOgYxonLnbLycu9SxMMGn95PpTIABHKtqcFw5Dc2gjJtjnAjG23kVZ0/O6eQ0Uc5rIpXh3cYuXtvfwWsNHTT680d5qzw2rprnY+VcHxfPKsFh7Y1p6Z4Bs6lisHM6kudX0TRt3A45Llq0iOuuu45jx47x2muvMW3aND7/+c9z7733AnDo0CFmz57Ntm3bWLFihX6/q666ihUrVvDv//7vgz7uN7/5zbwJfDmPP/44RUWjP1tyTusfWdz8FE0ll/G/m8xoaoYty+7grW4Pt8zIsKp63J4SMd5pUNFsY9pRO4qmkLBkODw3QsQ9tV44hRBivPDHYU9AYW9AYX9QIan2juIaFI3ZLo2FXo1FxRqVdpBB3qFFo1HuuOMOgsEgbrf71Hc4iXFdQHLo0CF+/OMf88ADD/D1r3+dzZs389d//ddYLBbuuusuWlpaAPLas+U+z+0bzIMPPsgDDzygfx4KhaitrWXNmjVn/Q0dDvW9Vmh+iuoSJ2jZfn01NbWwN8TsufO58apZo34MYmSlUinWrVvHtddeW7BuKJnuJKHfHSTZFATAuriEsptnMd0+rn/Nx7XxcF7FyJJzOjmNp/OaSKtsOdLFaw0dvLa/g4Ptkbz9FS4rV83zceVcH5fOLsVlk9fowQx2TnPv2I+Ecf1dV1WV888/n29/+9sAnHPOOezatYuf/OQn3HXXXWf8uFarddAlm81m85j84qQd2XIRJRFC07KnwGLNPm8GpeC/vOLMjdXPUH+xfZ10PV2PGkmjmA14b5pN0QUVUi82Qgp1XsXokXM6ORXqvB4PxNhY38bG+nbePNBBNJnR9xkNCudNL2blgjJWzS9nQaVLXptPQ99zOpLndlwH4KqqKhYtWpS3beHChfzud78DoLKyEoDW1laqqqr027S2tuaVRIw7PZPg1FgIyIZhk0n6AIvTp6VVgi8dJvxmMwDmKgclty/AXC6Nz4UQYrSkMirvNXbpobe+tTtvf5nLysp52clrl8/14bHLP1vjzbgOwJdddhn19fV52xoaGpgxYwaQnRBXWVnJ+vXr9cAbCoV45513+NznPjfWhzt8PX2A+wZgS89/NdIFQgxXqi1K5xP7SJ3Ivr3mvLQazw0zUcyGAh+ZEEJMPi3BuB543zjQQTjRO7fCoMA504tZ1dOxYVGVG4NBRnnHs3EdgL/85S9z6aWX8u1vf5uPfexjvPvuu/z0pz/lpz/9KQCKovClL32Jf/qnf2Lu3Ll6G7Tq6mpuueWWwh78SWjWngAc7/2P0dyzIIH0ARanomka0fdaCTx3EC2lYnCYKP7IPOwLSwt9aEIIMWmkMypbmwJs6Am9e0/k15+WOixc1RN4r5zrw1tkGeKRxHg0rgPwBRdcwO9//3sefPBBHn74YWbOnMkPfvAD7rzzTv02X/3qV4lEInzmM58hEAhw+eWX8/LLL4/bHsCAvhCGmugtjDfpI8ASgMXQ1Fiart/vJ7ajAwDrHC8lH5uH0S29fYUQ4my1dcd5rb6djfXtvL6/ne547yivosDyGq++5PDSaR4Z5Z3AxnUABvjgBz/IBz/4wSH3K4rCww8/zMMPPzyGR3WWekaANS37i6MYDFhMshSyOLnEkRCdT+wjE0iAQcG9ZgauK2tQ5AVYCCHOSEbVeP9oFxv2tbOxoY1dx/NHeYuLzFw5Lzt57Yq5PkqdMtgwWYz7ADwpmaykFQsq2eBiNJqwGLN1m1IDLPrTVI3ujUcJ/ekIqGAssVHyiflYp49+yz4hhJhsOsIJXm9oZ0N9O3/e304gmr+4wrIaDyt7RnmX13gxyiDDpCQBuEDSxiIyPT2ADSYjZmP2FywpI8CiDy2l0vm/DcS2twNgX1FG8S1zMEjfSCGEGJaMqrHjWIAN9e28Vt/GjuNB+i4B5rGbuWKuj1Xzy7lyXhllLhnlnQrkr2iBpExFqFp2OWaD0aSXQMgkOJGTiaTw/3oPycYQGBSKb5kjvX2FEGIYuiJJXt/fzoZ9bby+v4POSDJv/+Jqt17Lu6LWi8ko3XOmGgnABZIyFqFqAQAMxt4RYKkBFgDpjhgdv9xNuiOGYjNS+hcLsc0pLvRhCSHEuKSqGk1h+M8NB3n9gJ/3jwbyRnldVhNXzPNlSxvmlVHuHscT5cWYkABcIClDEWjZ/zgNRiMWU64GWALwVJc4EsL/2G7UaBqj14rvnsWYKxyFPiwhhBhX4qkMbx3sYO3uVv60t5WOsAk4qO9fUOli5fxyVs0v49wZxZhllFf0IQG4QFLGIgw9k+AMRpP+i5lKyyS4qSy6o53O39ZDWsNc48R312KMLuktKYQQAMFoilfrW1m3p5WN9e15Sw5bDRpXzq/gmoUVXDW/jCqPvYBHKsY7CcAFkjIVYe5pg2Y0GfUALJPgpiZN0+h+7RihlxsBsC0qpeQT8zFYjIU9MCGEKLDmQIx1e1pZu6eFdw51klZ7B4oq3TbWLK5g1Twfnfve4UMfXIHZLMsOi1OTAFwgKWMRRq3vCLDUAE9VWkYl8OxBIu+2AOC8rBrPB2ZJf18hxJSkaRoNrWHW7m5h7Z5Wdh4P5u2fX+FizeIKrl1UwdJpHhRFIZVK8WJDgQ5YTEgSgAskZSzCogdgY58+wBKApxI1nsb/+D4SDV2ggOeDs3BdNq3QhyWEEGMqo2psOdLF2t0trNvbyhF/VN+nKHD+jGLWLKrk2kUV1PlkToQ4exKACyTbBaLPCLBJFsKYatLBBP5Hd5NqiaCYDZTcvgD7otJCH5YQQoyJeCrDG/s7WLunhfV72/D3aVVmMRm4Yo6PNYuzNb0+WYFNjDAJwAWSMhaRoacLhMmI3Zyt9ey77riYvJLNYTp+uRs1lMTgNOO7ezGWGlehD0sIIUZVIJpk/d421u1p5bWGdmKp3klsHruZaxaUs2ZxBVfMLcNhlYgiRo/8dBVIus8IsNFootKT7UnojyRIplW9LZqYfGL7Oul8fB9aMoOpogjf3YsxFUtPSiHE5HSsK5qdxLa7lXcbO8n0mcRW7bGxZnElaxZVcMHMEmlVJsaMBOACyS+BMFLqsGAxGkhmVFpDcWpLigp8hGI0hN9uJvDsQdDAOsdL6Z0LMdjl11AIMXlomsa+lm7W7s52btjdHMrbv6DSpYfexdVuWd1SFIT85S2Q/gFYURQqPTaaOqOcCEoAnmw0VSP48mHCrx8HoOi8Coo/PAdFRvqFEJNAOqPy3pEuPfQe64rp+wwKnF9XwppFFaxZVMn0Uvn7JgpPAnCBpIxFZPoEYIAqPQDHTnZXMcFoqQydT9UT2+UHwH3tDFxX18qohxBiQoslM/x5fztr97Syfm8rXdGUvs9qMnDF3LLsJLYF5ZTKJDYxzkgALpCUsQg1NwmuJwdV9dQBnwjGC3VYYoRlwkn8v9pDsqkbjAolH5lH0TnlhT4sIYQ4I12RJOv3tbF2dwuv728nnupt3ektMnPNgoqeSWw+iiwSMcT4JT+dBaIaLKhKdrUaA9kXkCpvdtnGFgnAk0KqPUrHo7vJdMZR7CZ8n1yIdZa30IclhBCn5WhnlLV7Wlm7u4XNjZ30mcPGNK+dNYuzpQ0X1BVjkklsYoKQAFxAqjE74mtUegJwzwhwc0BKICa6xKEgHb/egxZLYyyx4btnMeYyqXsTQox/mqax50Sop563lb0n8iexLapyc+2i7EjvoiqZxCYmJgnABZQxZkd8DWT7IFZ5ekaAQzICPJFFt7XR+b8NkNGwTHdR+qlFGJ2WQh+WEEIMKZ1RebexU29XdjyQP4ntwpkl+kpsMklbTAYSgAsoNwJs0LKLX/SOAEsAnpA0CG88RmT9MQDsS0op+fh8lJ5FToQQYjyJJtO83pBdie3VfW0E+kxis5kNXDm3jDWLK7l6QTklDvknXkwuEoALKGO0AtqAANwRlsUwJhotrTLjoINIezb8Oq+swXN9HYpB3hoUQowf/nCiZxJbK3/e304i3TuJrbjIzOqFFaxZXMnlc3zYLfLPu5i8JAAXkKpYgThGLftfd4nDgsVkIJmWxTAmEi2VIfB4A752KyjgvXk2zourC31YQggBQJM/yto9Lazd08p7/Sax1ZbYWbMouyjFeTNkEpuYOiQAF5BqzAZgRc0GYEVRqPLYOOKXxTAmCjWZybY5OxBANWiU3LkA52JpcyaEKBxN09jdHGLt7mzo3dfSnbd/cbU7G3oXV7Cg0iWT2MSUJAG4gDI9bdCMWlLfVunOBWDpBDHeqYkMHb/cTfJwEMViYP+cAFfOKy70YQkhpqBURmXz4U69XVlzn3aaRoPCRTOzK7GtXlRBTbEMrgghAbiA9D7AakLfVt3TC1gWwxjf1Hiajkd3kzwSQrEa8X5qAeFdfy70YQkhppBIIs3rDb0rsYXiaX2f3WzkqnnZldiuXlCOt0gmsQnRlwTgAtIDcKY3AFf2TISTxTDGLzWaov0Xu0gdC6PYTJR9eglKpQ12FfrIhBCTXUc4wfq92VZlfz7QQbLPJLZSh4XVCyu4dlEFl8/1YZMONEIMSQJwAalK9tufNwIsi2GMa5lIio6f7yTVHMFQZML36aVYpjlJpVKnvrMQQpyBxo4Ia/e0sG5PK+8d6ULrM4ltekkR1y3Odm44d3oxRuk8I8SwSAAuoFwANmZ6w26lLIYxbmXCSTr+eyepligGp5myv1qKudJR6MMSQkwymqax83iwZyW2Fhpaw3n7l07zsGZRNvTOq3DKJDYhzoAE4AJSyb49Zcj0hl1ZDGN8yoSStP/3DtJtMQwuC2X3LsVcLhNJhBAjI5VReedQpz7S23ceiMmgcPGsUtYsrmD1wgp9rogQ4sxJAC6gDNl+i4Z0VN8mi2GMP+lAgo6f7SDtj2P0WPDduwyzT/4ACSHOTjiR5rX6dn0ltu4+k9iKLEZWzi9jzaJKVs0vx1NkLuCRCjH5SAAuIFXrCcCZGGgaKIoshjHOpDvjtP/3TjKdcYzFVsruXYapxFbowxJCTFBt3XHW721j7e4W3jzgJ5npncTmc1q4dlEFaxZVcsnsUpnEJsQokgBcQKqWrdsyaGlIRcHikMUwxpF0R4z2n+0kE0xgLLVRdu9STF4Jv0KI03OoPczaPa2s29PK1qb8SWx1pUVctzi7KMWKWpnEJsRYkQBcQLnlKI2KBvEgWLITqnoDsHSCKJRUW5T2/96JGkpiKrNTdu9SjG5roQ9LCDEBqKrGjuNBfSW2A235k9iW13hYszi7/PCccpnEJkQhSAAuoEwmW+9lyAVgdzUAVR5ZDKOQUi2RbPgNpzBVFFH2V0sxuqSJvBBiaMm0yqZDftb1TGJrDfW2tzQZFC6ZXcqaxZVcu7BC7/cuhCgcCcAFpPXUfhnoCcA9chPhTkgv4DGXbA7T8fOdqJE05ioHvk8vweiU8CuEGCgUT7Gxvp21u1t4rb6d7kTvJDaHxcjKBeWsWVTByvnleOwyiU2I8UQCcAGp/UeAe+gBWEaAx1TyWDftP9+FFktjrnFS9pdLMMjMayFEHy3BOOv2trJ2dwtvH/KTyvQW9Ja5rKxeWMGaRRVcOqcUq0kmsQkxXkkALqBMJgOAUVEhHtK3SwnE2Es0hej4+S60RAbLdBe+v1yCwSa/HkJMdZqmsb8tzLo92dC7/Vgwb//sMke2tGFRBStqvBhkEpsQE4L8hS+g/BHggL69UkaAx1TicJCOR3ejJTNY6tz47lmMwSq/GkJMVRlVY2tTF2t3Z+t5G/29vdoVBc6p9eqhd3aZs4BHKoQ4U/JXvoDUdHYEuH8JRG6Vn45wgkQ6I2+jjaL4gQD+x3ajpVSssz2U3rUYg0W+30JMNfFUhjf2d7B2Twvr97bhjyT1fRaTgcvn+Lh2UQXXLCyn3CWT2ISY6CQAF5CaGTwAFxeZsZoMJNIqbaGE9AIeJfGGLjp+tQfSKtZ5xfg+uRBFGs8LMWV0RZKs39fGuj0tvN7QQSyV0fe5bSauWVjBtYsquHJeGU55V0iISUV+owtIL4Ho1wUitxhGoz9KcyAmAXgUxPb68f9mL2Q0bAtKKL1zIYpZlp0WYrI72hVl4wmFx3+xmfeOBMiovZPYqj02vT/vBTNLMBvlNUGIyUoCcAENNQIM2TrgRn+UlpDUAY+02K4O/E/sg4yGfXEpJbcvQDHJHzohJiNN09jdHNJXYtt7IgQYgS4AFla5e5YfrmBxtVsWpRBiipAAXEC5EWDjIAG4uqcTRHOgsAFY0zTi8TjBYJBQKEQoFEJVVcxmMxaLBbPZnHe970eTyTTu/phEt7fT+dQ+UMG+vIySj81DkVEeISaVVEbl3cOdrOsJvcf79FQ3KDDLpfLxyxdy/ZJqeYdNiClqQgXgf/7nf+bBBx/ki1/8Ij/4wQ8AiMfjfOUrX+HJJ58kkUhw3XXX8aMf/YiKiorCHuwwqLmFMBQVEqG8fblOEC2jvBxyOp3G7/fr4bZv0M1dT6VSZ/z4Q4XjoQK01WrF4/Hg9Xrxer3Y7fYR+1ojW1vperoBNCg6p5zij85DkZZFQkwKkUSa1xraWbenlfV7WwnFexelsJuNXDnPx5pFlVwxp5hNG//EjZfMwGyWPt9CTFUTJgBv3ryZ//qv/2LZsmV527/85S/zxz/+kaeffhqPx8P999/PrbfeyptvvlmgIx2+oWqAAaq8o9sLuKWlhW3btrFz506i0egpb2+323G73bjdbkwmE6lUimQySSqVyrueTCb1/saAvv9MWa1WPQwPdrHZbMMaZY7uaO8Nv+dXUHzrXAm/Qkxwbd1x1u9tY+3uFt486CeZVvV9pQ4Lq3smsV0+14etZ4Lr2bweCSEmjwkRgMPhMHfeeSc/+9nP+Kd/+id9ezAY5Oc//zmPP/44V199NQCPPvooCxcu5O233+biiy8u1CEPy8lqgKvcI98LOBqNsmvXLrZt28aJEyf07bmQmQu4Ho9Hv567WCzDXw44k8nowbd/OD5ZcE6lUnq5RSAQIBKJkEgkaG1tpbW1ddDnGk5AThwI0PlUPWjguLAS7y1zJPwKMUEdbO9dlGLb0QBa7xw26kqL9P68504vxii/50KIIUyIAHzffffxgQ98gNWrV+cF4C1btpBKpVi9erW+bcGCBUyfPp1NmzYNGYATiQSJREL/PBTKlh+c7WjlcKVSKTRN0wOwUdHQ4kHSyWS2yzpQ5syemhPB2Fkdk6qqHD58mO3bt9PQ0KCPzhoMBubNm8fy5cuZNWsWBsPJ62BP9xiMRiNGoxGb7cz7ZaZSKT0MB4PBAdeHE5AtZgvOpAWnwYbX56WiwoBvv4rP58Plco1YjXLu+yOjS5OLnNfCU1WN7ceDrN/bzrq9bRzqiOTtXzbNzeqF5axeWM6cMof+O61m0qiZgY8n53RykvM6+Qx2Tkfy/I77APzkk0+ydetWNm/ePGBfS0sLFosFr9ebt72iooKWlpYhH/M73/kO3/rWtwZsX7t2LUVFYzQhos+whUHRUDIpXv7js6iG7EhrOAVgoiOc5LkXXuR0mxQkEgn8fj+dnZ15PzA2m43S0lJKSkowmUw0NDTQ0NAwAl/Q2LDZbNhsNioqKlBVlWQySSKRIJlMDrik02mSqSSdSpJOY5imYAesO6A/lsFgwGq16o9ps9mwWq1YrdZT/kMwlHXr1o3UlyrGETmvYyutQkNQYWenwq4uhVCq9x9Vo6Ix162xtERjSbGG19oJkU72v7eP/afxHHJOJyc5r5NP33M6nJLN4RrXAfjo0aN88YtfZN26dWc1ktjfgw8+yAMPPKB/HgqFqK2tZc2aNbjd7hF7nqGkUileeekl/XOlJ2tdf9XF4KoEst0XHn5/PYm0yjmXraS2eHjBvLW1lbVr19LU1KRvs9lsLFmyhGXLllFZWTnuOjOMhkwwQdtPdxDqDhH1QeYiD6FICL/fj9/vp6urC1VVicVixGL5Ew0VRcHr9VJaWorP58v7ONSkvFQqxbp167j22mtlYs0kIud17IRiKTY2dPCnvW28vr+DSLJ3+NZpNXHVPB/XLiznyrmluGxnfi7knE5Ocl4nn8HOae4d+5EwrgPwli1baGtr49xzz9W3ZTIZXn/9df7zP/+TV155hWQySSAQyBsFbm1tpbKycsjHzY3y9ZfrSDAWNLV3sobR5oZEJ+ZMDPo8f24xjPZwmlnlpz6uYDDIE088QSSSfYtwzpw5rFixgvnz50+pF4RMJIX/V/UYQhl8Ph9l/2cZRmd+DXMmk6Grq4v29nY6OjryLolEgq6uLrq6ujhw4EDe/RwOBz6fb8DF4XAAY/szJMaOnNfR0RyI6a3K3j7kJ91nUYoKt7WnP28lF88qxTLCvbrlnE5Ocl4nn77ndCTP7bgOwNdccw07d+7M23bPPfewYMECvva1r1FbW4vZbGb9+vXcdtttANTX19PU1MQll1xSiEMevj4B2GBzQaJz4EQ4j33Yi2EkEgkef/xxIpEI5eXl3HnnnXg8nhE/7PFOTWbwP7abdFsUo9uC79NLBoRfyNYo58JrX5qmEQ6H9TDcNyCHQiEikQiRSIQjR47k3c9kMmE2m0mlUlRUVFBeXk55eTkej+eMyymEmGw0TaO+tZu1u7Ohd+fx/Ne8eRVOPfQunebBIJPYhBCjZFwHYJfLxZIlS/K2ORwOSktL9e2f/vSneeCBBygpKcHtdvOFL3yBSy65ZNx3gNC0ngCsKBjsHggySADOln2cajEMVVV55plnaG1txeFwcMcdd0zJ8KulVfy/2UuyqRvFbsL36SWYik+vdEZRFFwuFy6Xi5kzZ+bty9VV9x8x9vv9pNNp0uk0u3btYteuXfp9zGazHob7XpxO55QoRREinVF570hXtnPDnhaOdvaWHCkKnD+jmDWLsp0b6nyOAh6pEGIqGdcBeDj+7d/+DYPBwG233Za3EMZ4lyuBMBqNYOsJq/FA3m2qvMNbDGP9+vXU19djNBr5xCc+MWBS4FSgqRqdTzeQaOhCMRvw3b0Yc8XI/jG1Wq1UV1dTXV2dtz2TydDR0cErr7zC9OnT8fv9tLW10dHRQSqV4vjx4xw/fjzvPjabbdBgPGaTMIUYRbFkhtf39y5K0RXtnYhrNRm4Ym52UYqrF5bjcw4sRxNCiNE24QLwxo0b8z632Ww88sgjPPLII4U5oDPVMwJsMJrA1hPU+o0AV+aWQz5JL+Bt27bpi37cfPPN1NbWjsLBjm+aphF84RCx7e1gUCj9i4VYZ4z+ZMYco9FISUkJHo+Hyy67TK9RymQydHZ20tbWlnfp7OwkHo/T1NSUN1kRwOl0DgjFZWVlg9asCzGe+MMJ1u9rY92eVv68v514qrfMy1tk5uoF5axZVMmV83wUWSbcnx4hxCQjr0IFkhsBNpiMYPNmN/YLwNX6csiDB+DGxkaef/55AK688soBq+RNFd2vHiX8VjMAJR+bh21+SYGPKMtoNFJWVkZZWRmLFy/Wt6dSKX2UuO8lEAgQDocJh8McOnQo77E8Ho9eW1xRUUFFRQWlpaXZdxCEKJAj/kjPohStvHekkz5z2KgptuulDRfUFWMySi28EGL8kABcIHoANpp6SyAS+e09Kj251eAGlkB0dnby1FNPoaoqixYtYuXKlaN6vONV+O0ThNZlJ6R5b5pF0YryAh/RqZnNZiorKwd0KkkkErS3tw8IxuFwWF/8o2/P5txEvlwgzgXkkVzgQ4i+NE1j5/GgHnrrW7vz9i+udrNmUSVrFlewoFJ+DoUQ45cE4EIZtAa4/whwtgSiI5wkkc5gNWVH++LxOI8//jixWIzq6mpuueWWKdlpILqjncCz2TZlrqtrcV42rcBHdHasVis1NTXU1NTkbY9Go3oYzq1619bWRjKZHHQVPLvdnheIcx9PZzlrIXKSaZV3DvtZu7uVP+1tzVue3WhQuHhWCdcurGD1ogpqhtmvXAghCk0CcIH0lkCYwNZTr9ovAHuLzFhNBhJpldZggumlRWQyGZ5++mk6OjpwuVx84hOfmJLBJr6/i86n6kEDx0WVuK+dUehDGjVFRUXU1dVRV1enb1NVlWAwmBeIW1tb8fv9xGIxGhsbaWxszHuckpKSvBKK8vJySkpKpuQ/T+LkuuMpXmtoZ+3uVjbUt9EdT+v7iixGVs4vY82iSlbNL8dTJD1XhRATjwTgAsm1QTMYhh4BVhSFaq+dwx0RTgRjTC8t4pVXXuHgwYOYzWZuv/32MVm5brxJHuvG/+u9kNGwL/XhvXnOlHur1WAwUFxcTHFxMQsWLNC3p1IpvYyibzgOh8N0dnbS2dnJvn379NubTKa8UeJcOM4t7CGmjrZQnHV7s6UNmw76SWZ6J7H5nFauXZSdxHbJ7FJsZqk9F0JMbBKAC6S3BnjoAAxQ6bb1BOA4mzdv5t133wXgwx/+8IB2XFNBqi1Kx6O70JIZrHO8lHx8Poo0y9eZzeZBW7VFIpEBo8VtbW2k02mam5tpbm7Ou32uG0Xf+mKfzycrLE0imqZxsD3MKz2LUrx/NJC3f5bPwbWLs4tSnFPrlUUphBCTigTgQtGy06WzJRBDB+BcL+DGw4fYvvNVILtC3qJFi8bmOMeRdDBBxy92oUbSmGuclH5yIcoIL486WTkcDmbNmsWsWbP0baqq6m3acuG4tbWVrq6uQbtRKIpCaWnpgNFir9c75UbgJ6qMqvH+0S59JbZDHZG8/edM9+qdG+aUOwt0lEIIMfokABfIcEeAqzw2PEqMrt3vo2gay5cv5/LLLx/LQx0XMpEUHT/fRSaQwFRmx3f3YgxW+fE9GwaDQV8Ouu8/VLluFP1HjGOxmL763e7du/XbWyyWAaPF5eXl2O32QnxZop94KsObBzpYtyc7ia0jnNT3WYwGLp1TyppFlaxeWE65+/RWThRCiIlKEkSh6F0g+o4AhwbcrLzIyDXm/SiZFLW1tdx0001TbrRNTWbwP7abdFsUo9uC7y+XYHROvYl/Y2WwbhSaptHd3Z0XiFtbW+no6CCZTHLs2DGOHTuW9zhut3tAN4rS0lJMJnnZGW3+cILXGrIrsb3W0E40mdH3uWwmrllQzrWLKrlqfhlO+UdSCDEFyStfgeQvhNETgNMxSCfA1LvqV1H4GG5DgggWbrnto1MuPGhpFf9v9pJs6kaxm/B9egmmYhmlGmuKouB2u3G73cydO1ffnslk8Pv9A0aLg8EgoVCIUCjE/v379dvnRp37T7zzeDxT7h+7kaSqGrubQ2yob+PVfW1sPxbIVVkB2XeS1iyqYM3iSi6cWYJZFqUQQkxxUytNjSN5C2FYXIACaNlRYGeZfrt4R3ZUbU+qnM3HolzvnTpdHzRVo/PpBhINXShmA757FmOukO4E44nRaNSXbF66dKm+PRaLDdq7OJFI6Nt37dql395qtQ4IxVJGcXKheIo39newYV8bGxvaae9O5O1fVOXmmoXlXLe4ksXVbvkHQwgh+pAAXCB6GzSjEQwGsLohEczWAfcE4GQyqfdyPaZ6eW77ca5fUjnUQ04qmqYRfOEQse3tYFAo/YuFWKdPnfA/0dntdmbMmMGMGb39mTVNIxgMDgjFHR0dJBIJjh49ytGjR/Mex+12DwjGPp9vyr0TAtnv34G2sD7K+15jF+k+aw87LEYun+tj1fxyVs4v11eSFEIIMdDU+ysyXuRqgHN/yG2e3gDc49ChQ2QyGRwuN8G4jfV72+iOp3DZJn8rqu5XjxJ+K9uaq+Rj87DNLynwEYmzpSgKXq8Xr9fLvHnz9O3pdFovo+jboq1vGcWBAwf02xsMhgHdKMrLyydlN4pYMsOmQx1s2NfOhvo2jnXlL4s+q8zB1fPLWbWgnAvqSrBIVxQhhBgWCcAFktcFArIBOAjEA/ptcrWTixbMZzYODrZHWLu7ldvOq2EyC799gtC6IwB4b5pF0YryAh+RGE0mk0mfLNdXPB4fMFrc2tqqd6lob2/Pu33/bhS56xOtjOJoZ1Qf5d100E8i3bsghcVk4JJZpayaX8aqBeXMKJWSICGEOBMSgAtED8CGPgEYIJHtBKFpGg0NDQDMmzePm23w/XUNPLu9eVIH4OjOdgLPZkf7XFfX4rxsWoGPSBSKzWZj+vTpTJ8+Xd+maRqhUGjAaHF7e/uQ3ShcLteA0eKysrJxU0aRTKu819iph96D7fm9ead57axaUMaq+eVcOtuH3SKrsAkhxNkaH38BpqJcDXDfEgjQSyBaWlro7u7GbDZTV1fHh4qTfH9dA28e6KC9O0GZyzrYo05o8QMBOp+sBw0cF1XivnbGqe8kphRFUfB4PHg8nrwyir7dKPqOGgeDQbq7u+nu7s4ro+i7qEf/bhQGw+iXEbSF4mysb+fVfW28caCDcCKt7zMaFM6fUcyqBeVcvaCcueXOSVfaIYQQhSYBuEAGlkD0TPDqCcC58oeZM2dmQ7DPzPJaL9uPBnhx5wnuurRurA95VKU6Yvh/sxcyGvalPrw3z5E/+mLY+naj6CtXRtF3tLi1tZV4PH7SRT369i1Op9P9n+60ZVdgC7CxZ5R3d3N+z2+f08LK+eWsml/O5XN9eOyTv85fCCEKSQJwgfR2gRh8BLhv+UPOzcur2X40wLPvH59UAViNp/E/thstnsYy3UXJx+ajGCT8irM3VBnF6S7qcejQIb10IheQy8rKTlpf3BVJ8vr+djbsa+O1hna6oil9n6LAshovq+aXcfWCcpZUezDIz7wQQowZCcCFoneB6FcDHA8SiUT0P8B9Fx344LIq/umPe9jaFKDJH2V6adGYHvJo0FQN/+P7SLfHMHoslH5yEYpZZrKL0XOqRT36jxYHAgEikQiHDx/m8OHDeY/lcrnygnHYUMT2do2NB7rY1tRFny5luGwmrpxXxtXzy7lqfhk+5+QrYxJCiIlCAnCB5C2EAXkBOFermKtJzCl327h0to83DnTw/I5m7ls1Z0yPeTQEXzqsL3RR+qnFGF2yxLEojL5lFEuWLAEglUrx/PPPc95559HZ2alPuGtrayMUCun1xQcPHsx7rJmaBa/JjrHIw4xpVVy4aCaXL5mJ3SahVwghxgMJwAUyaBs0gHho0PKHnA+tqOaNAx38YdtxPr9y9oSuk41saSX85+MAFH90HpZpzgIfkRADGY1GqqurmTFjBpqmcagjwoZ9bWzbe5wDTc04tRjFSgyvEqPYEMOupHAqSZzGJCSCpA418eahd3jzBSguLh5QSlFaWorZLDW/QggxliQAF4gegPt1gcjEghw8mh1NGiwAX7e4kr///S72t4XZ19LNwqqJuTpa4kiIrmeyE/1cV9dStKzsFPcQojBSKry+v4M/H8i2Kjvij/bZ62RGaTmLehajuGhmCWoqoY8S9x0xjkajdHV10dXVRX19vf4IiqJQUlIyIBiXlJSMm1ZtQggx2cira6H0TIIz9hsBPtptIB6PU1RUxLRpA3vgeuxmVi0o45XdrTz7fvOEDMDpQAL/r/dARsO2uBT3aml3JsaP3JLDbxzo4LX6Nt48YCT1zlZ9v9mocNHMUlYtKGfV/DJmlfV758JcNGAZaIBwODxoMI7H4/j9fvx+P3v37tVvn1vxrn8wLi4u7n3dEEIIcUYkABdIbgRYyS2E4awEYH93tkZwzpw5Q/YjvXnFNF7Z3crz25v56nXzJ9TscTWZwf+r3ajhFOZKh3R8EONCWyjOGwc6eONAB28e6KA1lOizV6HCbeXqBeWsnF/OZXN8OK2n/9LpdDpxOp3MnDlT35brSDFYME4mk4OueGc0GvH5fHnBuKysjOLi4jHpYSyEEJOBBOBC0bLTw425tzhL54DNS0M8u8pb39np/V29oByn1cTxQIytTV2cX1cy6oc7EjRNo+vpBlLNEQwOM6V3LcJglZEsMfYiiTTvHPbzxn4/bxxop6E1nLffajJw4cwSLp5ZDC17ufcj12KxjPwEzb4dKWbPnq1v1zSNYDA4IBi3t7eTSqX01m19mUwmysrK8kaLfT4fXq9XgrEQQvQjAbhABkyCMxjoqrqC9sM+FCU7AjwUm9nIdYsr+d3WYzz7fvOECcDd65uI7ewAo0LpJxdiKrYV+pDEFJHOqGw/FuTNAx28sb+DrU1dpPv0KFMUWFLt4bI5Pq6Y6+O8GcXYzEZSqRQvvrh3zCebKoqC1+vF6/Xm/TOsqiqBQGDQYJxOpzlx4gQnTpzIeyyj0UhpaSk+nw+fz5d33WqVrhRCiKlJAnCBDGiDBuy3LAHiTLfHTtpgH+DmFdX8busx/rjzBN+4aRFm4/ge4Ynu7CD0pyYAim+Zg7XOc4p7CHHmct0a3jzQwZ/3d/D2QT/difwV3WqK7Vwx18flc8q4ZHYpJY7x34LPYDBQUlJCSUkJ8+fP17erqkpnZ+eAYOz3+8lkMvq2/lwuV14gzl3cbreMGgshJjUJwAXS2wWitwSgIeIE4sxN7cuWSJxk1OnS2aX4nBY6wkneONDBqvnlQ9620JLNYbp+m5317rysGscFlQU+IjEZdYQT+gjvmwc6aA7G8/Z77GYum1PKZXN8XD7Hx4xSR4GOdOQZDAY9vC5cuFDfnhsxzi377Pf79euRSETvY9zY2Jj3eCaTaUAwLi0tpbS0VEaNhRCTggTgQtG7QGRPQTKZ5PCJLgDmpXZC5yEonT3k3U1GAx9cVs0v32rkufebx20AzoST+H+1By2lYp3rxXPjrEIfkpgkYskM7xz266O8+1q68/ZbjAbOryvWyxoWV3swTrEJl31HjPu3VYzFYnmBOHfp7OwknU4PWmcM4Ha7B5RS5EaNJ3JfciHE1CIBuED61wC3traSyWRwGZOUZfzQ9PZJAzBkF8X45VuNvLK7hVgyg90yviaUaWkV/6/3kgkkMPnslN6+AMUofyDFmcmoGjuPB3sCbztbjwRIZtS82yyqcnP53OwI7wV1JePud2I8sdvt1NTUUFNTk7c9k8nkjRr3HTmORqOEQiFCoRCHDh3Ku5/ZbB5y1Hg0JhAKIcTZkABcIP0XwohEIgC47WaUMNC0Cc6586SPcU6tl9oSO0c7Y/xpbys3La8e1WM+HZqm0fWHAySPhFBsxmzHhyJZ7UoMn6ZpHPFH+fOBDt7c38FbBzsIxfPreKd57Vw+x8dlc31cNruUUqe8PX+2cpPmSktL8+qMAaLR6IBSio6ODrq6ukilUrS0tNDS0jLgMT0ez6CT8Fwul4waCyEKQgJwofQbAc4FYIe7GMJkR4BPQVEUPrS8mkc2HOTZ95vHVQAOv9lM9L1WUKD0joWYy4oKfUhiAuiMJHmzpxfvn/d3cDwQy9vvspm4dHYpl8/xcfncMupKiyRAjaGioiKmT5/O9OnT87ZnMhm6uroGHTWOxWIEg0GCwSAHDx7Mu5/FYhm0nKKkpESWhxZCjCoJwAWiafkBOBrNLq9aVFIFzYB/P0Q6wOE76ePcvGIaj2w4yGsNbQSiSbxFhX+rMd7QRfCP2bdHPTfOwjavuMBHJMareCrD5sbO7CIU+zvY3RzK2282Kpw7vbgn8PpYOs2DaZx3PJmKcotz+HwDX68ikcigk/C6urpIJpM0NzfT3Nw84H5er3dAOYXP58PpdMo/PUKIsyYBuEC0nh6kuTZouRHgIncxlC2E9r3ZUeCFHzzp48yrcLGg0sW+lm5e2tXC7RdOP+ntR1uqPYr/8b2gQdF5FTgvHz+j0qLwVFVjd3OoZ9W1djY3dpFM59fxLqh06WUNF9aV4DiDVdfE+OFwOHA4HAOWhk6n0wNGjXOXRCJBIBAgEAhw4MCBvPtZrdYhR41NJvlZEUIMj7xaFIqa6wKRPwLscDhg+sXZAHz01AEYsqPA+17ex3PvNxc0AKvRFP7H9qDFM1hmuCn+8BwZqZniNE3jaGeMNw9mR3jfOthBVzSVd5tKt02fuHbpnFLKXbJAylTQd+W6vjRNG3LUOBAIkEgkOH78OMePH8+7X27xkP49jUtLS3E4HPJaJITIIwG4QHpLILKnQC+BKCoC5yWw5dFh1QED3LS8iu++vI+3D/tpCcap9Ix9gNAyGv4n9pHuiGH0WCn9i4UoJnmreqrJqBr7WkK819jFu42dvNfYSWsokXcbp9XExbNKuWKuj8vm+JhdJuFE9FIUBafTidPppK6uLm9fKpWis7NzwCQ8v99PIpGgq6uLrq4u9u/fn3c/m82mB+Li4mK9y0VZWZmMGgsxRclvfoH0XwhDL4EoKoLyi7M3an4fklGwnHwCWU1xERfUFbO5sYsXdjTzV1eMfa/d4IuHSOwPoJgNlN61CKOr8LXIYvTFUxm2Hw3w3pEu3j3cydYjXQNWXDMbFVbUerl8ThmXzy1leY1X6njFGTGbzVRUVFBRUZG3XdM0wuHwoJPwAoEA8XicY8eOcezYMf0+//Vf/4WiKJSUlAzavs3hmDwLpQghBpIAXCjq4CPADocDvNPAVQXdJ6B5K9RdfsqH+9CKaWxu7OLZ98c+AEc2txB+MzuJpfhj87FUO8f0+cXYCUSTbDmSG93tYuex4IBevE6riXNnFHNhXTHn15WwvMYr/XjFqFIUBZfLhcvlYubMmXn7UqkUfr9fD8RtbW0cOnSIdDqdt6+hoSHvfna7fdBJeMXFxXrpmhBi4pIAXCBavxrgvBFgRcnWAe/+fbYf8DAC8AeWVvGt53az83iQg+1hZpeNTQhNNAbp+kN2kop79XSKlp68a4WYWI4HYrzX2Mm7h7OBt761e8BtylxWLqwr4YKewLuwyj3lVlwT45fZbKayspLKyuwS7KlUihdffJEbbriBeDw+YAKe3+8nGAwSi8U4evQoR48ezXu83Op6g03Es9vthfgShRBnQAJwgeg1wCYTyWSSdDr7trH+ttv0S3oC8PDqgL1FJpbPbWd393ruePl7+BxOXBaXfnFb3Hmf993ms/mY5pqGQTm9t6XTXXH8v94LGQ37Uh+uqwvbgUKcHVXV2N8WZnNjJ5t7Rnj79+EFmFXm4IIZJVwwMxt6p5dIL14x8SiKgtvtxu12M2tW/rtmyWRSHzHuW2/s9/tJpVL65/0VFRXpHSmKi4vzLjIRT4jxRQJwgfRdCjlX/mA0GnuXDJ3eUwd89F1QM2AY/C23Y93HePbgszx34DmaDc2YPRBVoam787SOx26yM694HgtKFugf53jnUGQevP5YTWTw/2oPaiSFucpB8Ufnocio34SSSGfYdTzIu4e7eK+xk/eOdBGM5XdoMBoUllS7Ob+uhAvqSji/rhifrLYmJjmLxUJVVRVVVVV521VVJRQKDToJLxQKEY1GaWpqoqmpacBjms1miouL8Xq9A8Kx1+uV5aKFGGMSgAulTwDuW/6gjxCULwaLCxIhaNsDlUv1u0ZTUdYdWcezB59lc8tmfbvT7CTYsZhEYBnf/vAKKr0aoWSI7mR37yWV/dh3e1u0jVg6xvb27Wxv364/noLCDPcM5pfMzwvGPquPrt/WkzoRweA0Z5c5lhrPcS8UT7H1SFfPCG8X248GSPTrwWs3Gzl3hpcLegLvilqv9OEVoofBYMDr9eL1epk9e3bevkQioQfjXDeKrq4uAoEAwWCQVCpFW1sbbW1tgz620+nMC8R9A7LL5cJgkImjQowk+ctWIH3boEVD2brKvFnHRhPUXgAHX4Wmt9EqlrCtbRt/OPAHXml8hWg6O2qsoHBx1cXcMucWrp5+NX/79F6eb27mhy+m+fWnL2Jm7alnMqfVNE2hJvZ17qO+q576znrqu+rpiHXQGGqkMdTIK42v6Lf/TNfH+HDLSlSDRuQmOxVuWbJ0PGoNxbNh93A28O5rCdGz/oqu1GHh/LpiPfAuqnZjlg4NQpw2q9VKdXU11dUDF/9Jp9MEg8G8YNz3kkgkCIfDhMPhATXHkH13sH8o7vu5zSa9s4U4XeM+AH/nO9/hmWeeYd++fdjtdi699FK++93vMn/+fP028Xicr3zlKzz55JMkEgmuu+46fvSjHw1olTOeDFYCUVTUr9xg+iVkDr7Krw8+w9PH/kBTd+/batNd07l5zs3cNOsmqpy9b9P93Q0L2HksQKM/ykd/8ha/vOdClkzznPRYTAYTs7yzmOWdxY3cqG/viHXQ0NnAvq597OvcR0NnA9OOeflwy0oAflDxa9a9/zaO3Q6Wly3nnPJzOLf8XJaWLcVukskgY0nTNA62R/T63c2NnRztHFi/O6O0iPNnlHDhzOyEtVk+qUsUYrSZTCZKS0spLS0ddH8sFhsyHAeDQTKZjN6tYjB2u33Qsori4mI8Ho90rRBiEOM+AL/22mvcd999XHDBBaTTab7+9a+zZs0a9uzZo4+YfvnLX+aPf/wjTz/9NB6Ph/vvv59bb72VN998s8BHfxK5LhAmk14CMaDv5PSLedTj5t+TxyAJRaYirqu7jpvn3My55ecOGlymee08/dlLufvRd9ndHOITP32bn37qPC6dffrdGXx2H75pPi6ddikAqZYIrY+8D6gcnt9JrNaEs81JOBXmrea3eKv5LQBMiolFpYs4p/wczqk4h3PKz6HEVnLazy+Glsqo7G4O9YzuZut3OyPJvNsYFFhY5dZHd8+vK6bCLSNFQow3drsdu90+6OhxJpOhu7t7yIAcjUaJxWLEYjGam5sH3F9RFDwez4CAnLvY7Xb5J1hMSeM+AL/88st5n//yl7+kvLycLVu2cOWVVxIMBvn5z3/O448/ztVXXw3Ao48+ysKFC3n77be5+OKLC3HYJ6WqGf36yUaAW4tr+anXDcD9Cz7JJ8+9f8hJaX2Vuaw8+ZmL+cyvtrDpkJ+7f7GZ/7h9BdcvqTrlfYc85nga/2/2QkrFOtfL5Z+6nCuMN5NRMxwIHGBr21a2tW5jS9sW2qJt7OjYwY6OHTy25zEA6tx1nFtxLueWZy81rhp50T0N4USa95sC+upq25oCxFKZvNtYTQZW1Hq5cGYJ59eVcO50Ly6blKcIMZHlyh+8Xu+AHseAvgJeIBAYNCBnMhkCgQCBQIDDhw8PuL/FYhkyHHs8HsxmeQ0Rk9O4D8D9BYNBAEpKsiOKW7ZsIZVKsXr1av02CxYsYPr06WzatGnQAJxIJEgkepdnDYVCQLY/ZCqVGnD7kZaIx/XrGVWjuztbA2yz2fKe//vbf0LMYGB5PMFfmmsA87CPz2aEn/3FCh74352s3dPG5/9nK9+6aRGfuKDmtI9X0zSCv91PuiOGwWPBfdts0moaeuZPzXLNYpZrFh+Z/RE0TeNE9ATvt73P++3Zy4HgAb2W+Jn9zwDgs/lYUbaCFWUrOKf8HOZ652IyTLgfR13uvIzEz088lWFvSze7jofYeTzIzuMhDnZE0PrV73rsJs6bXsz5dV7On17M4mo3ln7LT4/Fz/NkNpLnVYwPk+2cGgyGIcsrcivk5cJxLgjnPg+HwySTSVpbW2ltbR308V0ul15OkQviuYvT6Rw3AxmT7byKwc/pSJ5fRdP6/1kdv1RV5UMf+hCBQIA33ngDgMcff5x77rknL9ACXHjhhaxatYrvfve7Ax7nm9/8Jt/61rcGbH/88ccH1uGOAjWV5NDT2ZHRWR+7h8amJoLBILW1tfh82VKFpnQTPw3/FEWDJ5pbKHJdxo7p95z+c2nw9CEDb7Vlg9EHajNcO03jdF6zyptt1B4pQlU06heHiLoyp75TH1E1SlOmiSPpIxxJH+F45jgZ8h/DgoVaUy0zTDOoM9ZRY6rBokz+tkBpFU5EoSmicDSs0BRWOBEFlYEnqNiiMcutMdutMculUWHPljkIIcSZUFWVZDJJIpEY9KOqqie9v6IoWK1WLBbLgI8Wi0Vqj8WIi0aj3HHHHQSDQdxu91k91oQacrvvvvvYtWuXHn7P1IMPPsgDDzygfx4KhaitrWXNmjVn/Q0dju6uLj0A3/iBD/Dr3/yGYDDIhRdeyIIFC1A1lU++8kkAPlR2Posbn0ELvUvNpf8PvKe/2MQHNI0frD/Ij147xB+PGimdNp3/e/18DMNIT8nGEF3v7AHA84GZrLyo8rSfv79EJsEe/x62tW/TR4nDqTAH0wc5mD4IZCfmLSxeqE+sW1G2Ardl9M/NmUqlUqxbt45rr712yLcM0xmVg+0RdhwPsas5yK7jIfa2dJPKDPwftNRhYek0N0unuVkyzcPSajdlLum/O9aGc17FxCLndHg0TSMajQ4YNc51swiFQmiaRjweJ97nXc2+HA7HgFHj3EjySLd2k/M6+Qx2TnPv2I+ECROA77//fl544QVef/11amp638avrKwkmUwSCATwer369tbWVn3py/6sVitW68AwYTabx+QXp+8ysRarVa8BdrlcmM1mntn/DHs79+I0O/nSyu9BawtK01uYX/kq3Pm/nNbwbY+v3rCQMreNbz2/h1+93UQgluZfP7p8wFvmfWW6kwR/ewBUsK8ow33ZyNTtms1mLpx2IRdOuxAAVVM5EDig1xBvbd1Ka7SVnf6d7PTv5Fd7f4WCwpziOZxbfi7nVZzHueXnUuEYf10+cj9DqqpxqCPCzuMBth8NsvN4kN3NQeKpgSMqHruZZTUeltV4WDrNy7IaD1Ue27h5a1GM3WuDGDtyTk/NYrHk/V3tK5PJDNnaLRAIEIvFiEQiRCIRjh8/PuD+uZ7KQ7V2O9NlpeW8Tj59z+lInttxH4A1TeMLX/gCv//979m4ceOASQDnnXceZrOZ9evXc9tttwFQX19PU1MTl1xySSEO+ZQyPcseG4xGFEXRA7DD4aA72c2/b/13AD67/LP4HGXwof+AH18KB/4EO/8Xln30jJ73nstmUuKw8JXfbue57c0EYil+8hfnUmQZ+GOgZTQ6n9iH2p3EVF5E8a1zRy2QGRQD84rnMa94Hh9f8HE0TaM50syW1mwY3tK6hcZQI/u79rO/az9P1T8FwDTnNM6rOE8PxDPcMwoSGtMZlQNtYbZ2KOx4uZ7dJ7L1u+FEesBtnVYTS6a5WVaTDbrLpnmpLZFZ2EKIicVoNFJSUqLPx+kvFosNOTEvEAigqiqdnZ10dg6+aqnNZht0Yp7X68Xj8WAyjfv4MvmoGYgH+1wC2Y+u6uy6BRPMuP8Juu+++3j88cd59tlncblctLS0AODxeLDb7Xg8Hj796U/zwAMPUFJSgtvt5gtf+AKXXHLJuOwAAfk9gDOZjP72UVFRET/a/iM6453Uueu4Y8Ed2Tv45sJVX4VX/wle/hrMvhocg/eTPJWbV0zDYzfzud9s5fWGdu742Ts8evcFFDvy621D6xpJHAqiWIyU/sXCMV3pTVEUpjmnMc05jQ/N/hAA/pifbW3b2NK6hS2tW6jvqud4+DjHw8d57uBzAJTaSvVOE+dVnMe84nkYh1hC+kyoqsbRrij1Ld3sbwtT39JNQ2s3h9ojJDMqYIT9R/Tb28wGllR7WNozurusxsvMUsewSk+EEGIiy7V267+cNGRrj0/W2i0SiRCPxzlx4gQnTpwYcH9FUXC73XnB2O126xP+iouLJSAPRtMgFYVYID/AxoN9tvXZ3n9bYojyg+V3SAAeDT/+8Y8BWLlyZd72Rx99lLvvvhuAf/u3f8NgMHDbbbflLYQxXqmZ3AiwSR/9VRSFE8kTPL73cQC+esFXMRv7DPVf+kXY9Xto2w2vfB1u/a8zfv6V88t5/N6LuOeXm3n/aIBbfvQmn7qkjg8uq6LCbSO2x0/3xmMAFH9kLuby0Z8YeCql9lJWz1jN6hnZbh/hZJjt7dv1QLyrYxf+uJ91R9ax7sg6ILs09PLy5ZxXfh7nVpzLEt8SrMZT19FqmsaJYJz61m4aWrppaA3T0NrNgbbwgNZjOUUWI2WWNJcvms7y6cUsq/Ewp8yJSVZVE0KIPAaDAY/Hg8fjoa6ubsD+XFnjUAE5t7JeMBiksbEx77779+8Hsu+out3uk14mZKlEJtUvnHYNEmIDQ29TB74zedrMRWDzgs0Ddi+UzDr7xyyAcR+Ah9Okwmaz8cgjj/DII4+MwRGdvUwmG6L69gC22+3865Z/Ja2lubLmSq6ouSL/TiZLthTiv1fDjiezZRBzVvd/6GE7Z3ox//vZS/jkz9/liD/KP76wh3/64x5urCnmb1tUTIDzsmqKlpWd8XOMJqfFyWXTLuOyaZcB2Yl1uzt2s7VtK++1vsf7bdmJdW8ef5M3j2cXRLEYLCz2LWZ52XKWly1nmW8ZqG72t/aO5ja0drO/NUz3IOULABaTgTllTuZXuphX4WJehZN5FS7KHSZefvklbrxx4cR8URVCiHHCYrFQXl5OeXn5gH251m79ex93dnbS0tJCJpMhk8no9ceDjSDn2O32U4bkweYLnRVVhWT38EZcBwuxqejZH4PB1BtgcyE2d71vsNW3Ffdus7qzeWQSGPcBeDJS070jwLlV4Ew2E28efxOTwcTfnv+3g9+x5ny46LPwzo/hhS/D598Gi2Pw2w7DnHIXL33xCp59v5nntjez80gXtx1NYsLILjI83d7BjVusrFlcMe4XVLAardnyh4pz+aulf0VGzdDQ1cDWtq28c2IzW1u3Ekx2sa1tG9vatun3U5NeMrEZZGLTycSmo8arABMmg8JMn4N5lS7m9wm600uKBh3Vld6TQggx+hRFweVy4XK5mD69tytSKpXixRdf5IYbbiCVShEKhU56SaVS+gp6Q/VAhuyk+b6B2OVy4XbYcdsMuM0abnMKuxpFSfQNq4Ghg20iBNrJ28sNi9U9RIgdxjZz0RlNpp9sJAAXgJobATb1jgDTky8Xly6mzlM39J2v/nvY9wIEmmDDt+G6/++sjsVbZOGuS+u469I6jj2+B3b4CRngH9Qo7Q0R1jd0YPm9gavnl3PT8mquXlCOfQzrgU9G0zRC8TQtwTgngjFagnFaQvGez+M9n5cRjF0LrEaxdGC0H8FoP4rRfgSDtRWDJYDBEsDs2Q6ASbEwx7OAC6rO4dyKFSwvq6WsaHyOggshhMinKAoOhwOHwzFo/TGAlk4RD3UQ8jcT8rcTCnTSHQoSCkcIReKEYmlCCY14RiGRSNDe3k57e/uQz2kihZswbrp7Pva/HqaIKHlDJybbSUZcvScPsTYPjOD8lqlKAnAB9NYA9wbgtCm7rdo5cC34PFYnfPDf4H8+Am//CJbcCtPOO+tjimxugR1+UGDWXy7hSY+ZF7af4LntxznYHuHl3S28vLsFh8XI4moPRVYjRRYjdrOJIosx+3nPdbvFiMPau6/v9extTdjNxrx2cAO+R6pGZzTZJ8zGaAn1CbY9YTeaHN6iHEUWE5WeOmaULNJHdWtLjUSVw+zr2sn29u3s6NhBMBFkX2AH+wI7+PXebK/makd1tmyiPFs2saBkQX59thBCiLGVTmZHWGNdEO1ECbdT6/8zhnePQip80npYJdmNHbADJ2ummcBMN05COAnh6vnY97qLKHbSmOmkmE6Kh3wsg0HB7SjC7Xbh9hTj9hYPKLdwOp0j2htZnJwE4ALIjQAb+5RAxA3ZThDTnNNO/QBzr4WlH4WdT8Nzfw2f2QhnEciSzWG6ns0uQOFeMwPbnGJmA19cPZe/vmYOe0908/yOZp7f3syxrhjvNg7etuZ0WU2GnlDcG46NBoW27gStofigC0QMxltkptJto9Jjo8pjo9Jtp8pjoyL3uceGy2oaotVYFVdNvxTIjig3hhqzYbh9B9vbt3MgcIDmSDPNkWZeanwpe9xGK4tKF+m1xMvLluM1e0fkeyKEEFNKJpUNprEuiHX2fMyG2sG39dw22Z33MCbgXICm03hui/MkI64erDYvVpsH32Cjs1YXKAqpVIru7u6TlluEw2FUVSPQHSHQHYHjLYMejqIoOJ3Ok9Yku1wu6XAxQuS7WABqeuAkuG6yv8ynHAHOuf6f4cB6aN0Fb/0Qrnjg1PcZ7Fhiafy/2QtpFduCElxX1ebtVxSFRdVuFlW7+ep189lxLMixrhjRZJpYKkMkkSGWTBNNZogke69n9/W93rMvlSE3rzGRVkmkVbqig9fPKgr4nNZsmHX3htnez+1Uum0jVpKhKAozPTOZ6ZnJLXNuASCSirCzYyfb27bnjRL3ryWuclThS/rw7/WztHwpC0sW4rQ4R+S4hBBi3Muk80Zk9dB60lAbGLq11rAo2TBaVIJq9dAeTlNWOxtDUckpSgm8YHOf1cBRjtlsPmk/ZMhOfO/u7j5pUO7u7tbbw3V3dw+6eEjOqTpcuFwuLJbJMVFtNEkALoBMTwmEYjTqI8ABNQDANMcwRoABHD64/jvw+/8DG/8ZFt0MpbNP6zg0TaPz6QYynXGMxVZKPjYP5SRlCYqisLzWy/Ja72k9T//nTKTVfuE4TSyZIZrMkMqolLmsVHpslLtsJ12pbiw4zA4urrqYi6su1o9/sFHiE5ETnOAEO7ft1O9b565jYelCFpcuZlHpIgnFQojxT82cYkR2iFHZRPDsntfmAXsJ2Iuzl6Lc9cG29Vz61MJmUinefvFFbrzxRgzjrBOP0WjUl4IeiqqqRCKRU07eO5sOFy6XK+9zm802Cl/txCEBuAA0NVcC0TsC3JZpA05jBBhg2cdhx1Nw8NVsKcRdz8Np1A+F32gmvscPRoXSOxdiKBr9Fw1FUbCZjdjMRs5sKY/CGmqUeFvLNn731u9Qy1T2du7lROQEjaFGGkONvHT4Jf3+de46FpUuYlHpIhaXLmZh6UIc5jPv5CGEEIPKrdo1rBHZPtvjZxlkrR4o6hNShxNqZVIXBoNB724xbdrgA2GaphGNRkesw4XFYjllGzi7ffKuVCoBuAB6SyB6F8IIEwagyjn4rNVBKUp2QtyPLoEjb8C2X8N5dw3rrommEMGXDgPgvWkWlhrXaXwFoi+H2cFFlRfht/m58YobMZvNdMY72evfy27/bvb497DHvycvFL94+EUAFBRmuGew2LeYRSXZYCyhWAihU9Xe0oJYoDeonirUxoPA8OZRDMrqzh9tzRt9HSLU2jxglFgxWobV4ULTSCQSpwzJ8XicZDJJR0cHHR0dQz6nyWQ6ZUguKiqakJP35Ce1APouhJErgUgYEpTZy4a1Ulme4jpY9X9h7f+Fdf8A864DV+VJ76JGU3Q+vg9UDfsyH46LTiN0i2EpsZXkLdQB0Bnv1MPw7o7d7OncQ0ukRQ/Ffzz0RyAbius8dfooca58oshc+BX5hBBnSFWzZQJ6UO0aGGAHDbUBzirIWlw9QfVk4bXfdrt3ROpjxdhTFAWbzYbNZht0IZGcZDJ5ypAcjUZJp9N0dnbS2Tn05Pf58+dz++23j8aXM6okABeAqtcA944AJ4wJZjhnnNkDXvRZ2PW/0LwNXvoqfOxXQ95Ur/sNJDCV2ii+de6kfXtjvCmxlXD5tMu5fNrl+jZ/zK+H4j3+Pez276Y12srh4GEOBw/nheLp7unM8c5htnc2c71zme2dTZ27TlqyCTGWNA2SkWxAjXYODK/9J3rpI7KBs1sAweLMH5Edbp2svD6IQVgsFnw+Hz6fb8jbDNbhYrDPXa6J+Q6yBOACyLVB04xGtFT2P/ukMTn8CXD9GU3woR/Cf10Fe56FvS/Awg8OetPwG8eJ7+0Ek0LJHQsx2ORHoJBK7aVcUXNF3tLXuVDct3yiNdrKkdARjoSOsL5pvX5bk2JihnsGs72zmeOdw5zibECe7pqOySDnVoiTynUuyAXZIT925X+eSZ75c5od/UZkh1Enay+eNMvPioljuB0u0j2r20408heyAHIjwFquZsYEqqKe3gS4/iqXwmV/DW/8G7z4N1B3WfZFs49s3W8jAN4PzsYyTToSjEeDheKOWAcHAgc40HUg+zFwgIOBg4RTYQ4GD3IweJC1R9bqtzcbzMz0zMwbLZ7jncM05zSMU3yyiZiEBhuVzRuR7d1ujPhZ7T+Oac/9Z9eCy2jJhtSikp6PfUJrbttgo7Km0yxzE2IcMxqNGI0T82+KBOACyE2CyxiMQIaMKfv5WQVggKu+lh0B7jwEP74s2yZt4YdAUQap+z15nbAYX3x2Hz67T2/HBtlyltZoqx6G93ft52AgG4Zj6RgNXQ00dDXwEr1dKGxGGzM9M5lb3BuK53jnUOWoklIYMT5k0r0lBP1HYvMC7ZmNyhqAAVNMbZ7eUVc90Pb9WDxwu8WRnYgshJiQJAAXgD4CrBiAzOmtAncyZjt85FF4+i7oaoTffgrmrEa7/rt0vpCQut9JRlEUKh2VVDoq8+qKVU2lOdycDcWBnlDcc4ln4uzt3Mvezr15j1VkKtIDcd9R4/KicvlZEWfmNEZl80Lt2fSTHWpUtk94TVvcbHq/nouv/gBmd3l2UQTpXCDElCO/9QWQqwFOkw0WESXbCeKsR4ABqlfA59/OlkK88W9w4E+E/+M7xJN3Z+t+75S638nOoBiocdVQ46rhqtqr9O0ZNcOx8DG9jCIXkBtDjUTTUXZ27GRnx868x3KZXXpdcW60eLZ3NqW2UgnGU8nJRmWHCrexrrOrlc0tjDDUiOwZjspqqRSdBzTwzYVxtmCCEGLsSBIqgFwAzvQE4KiS7QRR5RihdmRmO6z6Oiz9GIn//VeCjR8HwOt4Aks0AVwzMs8jJhSjwcgM9wxmuGdwzYzen4GUmqIp1KSH4lyNcVOoie5U94BlnwGKrcXM9s4eUGPstXnH+KsSp0XTIBk+xYhs/6B7tqOy1vya2EFGZQd8lFFZIcQok1eYAugNwFkJYwKf3YfNNLLLEmbsM+js+ksggd3yDo74b+A3v4FFt2Trg90jMOIsJjyzwayH2b6SmSSHg4fzQvGBwAGOdR+jK9HFe63v8V7re3n38dl9A0LxbO9sXJaJ2SZn3NI0SEX79Iwd4jJYwFVTZ/68pxqVHWy7uUhqZYUQ444E4ALI1QDnGockDcmRKX/oQ1M1up5uIBNMYPLZKf6rT6O83Qnv/AT2/AEO/AlWPggX/R/pEykGZTFamF8yn/kl8/O2x9IxDgcP94biruzIcXOkmY5YBx2xDt458U7efXx2HzXObFlGras2W6LhzF732X1Tt5xC0yDRfYogG+j3+QiUF/QdlT1ZeO3b1UBGZYUQk4i8mhVAbiW4dE9P9IQxwWzH7JPc4/SF/3yc+L5cv98FGLxOuP7bsOJ2eOEBOPZudvW49x+HG74LdZfLKI0YFrvJzqLS7LLNfUVSEX2yXW7y3YGuA7TF2vRg/H77+wMez2a0Mc05bdBwXO2sHvF3RkaFmskuPTtoYD3FRcuc8uGHZDAPXByh72peMiorhBCDkgBcALkSiJSaTcAJY2JER4ATR0IEXzkMgPem2Viq+/T7rVwKf/kKvP8bWPcNaNsNj30Q3DUw/3qYdwPMvEJ6VYrT5jA7WFa2jGVly/K2h5IhjoaOcjR8lGPdx3ov4WOciJwgnonrvYwHU24v1yf19Q3HNa6akZ2MlysriAf1ixLuoKbzTQzvHoVkaOgSg3iQs1qu1mQbuBTtkMG2z0VacQkhxBmRAFwAas+qKaMRgDORFJ2P7wUV7MvLcFw4SL9fgwHO/RTM/wC8+jBsfwpCx2Dzf2cvFifMXgXzb4S5a8Ax9FKJQpyK2+JmsW8xi32LB+xLZVKciJzQA/HR7qN51yOpCG2xNtpibWxt2zrg/naTXQ/FNa4aaouqqbF6qDG5mGawYklGsiOyeqgN5AXc/H3BAfWxJuA8gCPD/GL15Wq9wwuwuYvZflrfUyGEEGdHAnAB6CPAmWwAThqSZ98DmJ6639/Wkwkms3W/t845+eiYoxRu+ne4/p/h8OtQ/yLUvwzhFtj7fPaiGKDmQph/QzYQ++bKiJMYMWajmenu6Ux3T89uyJUSxANosQCBcDPHgkc41n2Mo9EWjsX9HEsFOJqO0KoliaVj7O/az/6u/QMeW9E0KjIZalJpatJpans+5q57VZVBf5IVYzbA2jyoVjcd4TS+2rkYHKUnD7E2ryxXK4QQE4QE4AJQM2k0xYCqZd8yHakR4O6NR4nXd4HJkK37tQ7z9JrtMO+67OUDKpx4Hxpezgbilp1w9O3s5U8PQcksmH4pFM8A74yej9PBWZkdWRZTl5rJTuhKhiERzn7MXU+EBo629h2Nze1LdusPpwDFPZelgzxdEmg2mThqNnHMZOJYz8fs52ZiBoUWk4kWk4n3Brm/w2ChxlZKTVEFtc5aajwzqPHOprZ4LlXOasxGM5lUik0vvsiNN96IQXrGCiHEpCEBuADUTAbNlP3WZ5QMaSV91j2A4we6CK3Lvk9bfEu/ut/TYTDAtHOzl1Vfh8DRnjD8EjT+ObvMcuehgfczWsFbmw3DfYOxty57vahURo7HG1WFVKQ3rPYPr3mfR7LhNNEn1Pb/PB0buWMzO7Itt3KXnhHZ7CV73WLzUGfzUNd/n9WFphjojHdmSyrC2ZrjvuUVbdE2ImqS+ugJ6qMnoOP9vKc3KAYqiyqpdlaTiWQ4vP0wVc4qyovKKXeUU1FUQYmtBIMi//QJIcREJAG4ANRMBq2nnVDCkMBXdHY9gDPBBJ1P1IMGRedX4Dh/kLrfM+WthQvvzV4S3XBoI7Ttha4jEOi5BI9DJgH+A9nLYMyObCDOjRz3v273jtwxT1a5SVr9R1dPGV77BNZkJP/+o8FgytbCWl09H51gdQ8ZZPXruX1W91mXEihAqb2UUnspK8pXDNgfT8dpDjfn1x33hONj3ceIZ+I0R5ppjjQDsG33tgGPYVJMlBWVZUNxUTYU971eUVRBuaMcq1EmlAohxHgjAbgA+gbgpDFJtePMyx+0jIr/8X2okRTmSgfFN49sO7U8VhcsvCl76SuThtDxbBjWg3FT7/XuE9mRxva92ctgbJ5sGPbUZGe2m6zZmfEmW5/rw/04yDaDaegRaE3Lvn2vqdmWVHnX1SG2Z7L3y11PJfBEG1GOvQtaCtIJSMchFc9+TCeyI6TD3t5nf6pnfyqSff6RphjA4soG1VxgtTgG2XaSzy2O3sBrso770X6bycYs7yxmeWcN2KdpGh2xDo6Fj9HY1cjr779OcW0xHfEOWqOttEXb8Mf8pLU0JyInOBE5cdLn8lg9ejCuLKrUr/cNzV6rd+r2QhZCiAKQAFwAaiaNZsrWEyYMZ1f/G3y5keSREIrVSOlfLEQxG0fqMIfPaMqO5hbPgJmD7E/FIXgMAo35wbirJyhHO7L1ny07spfRoBiyQVgx9ITXnjCrZjir9lU9zMBKgPqzfqjhsfQNov1GW08VUPvfx2wf94F1LCmKQllRGWVFZSwpXoJhn4EbL7gRc58a4JSawh/z0xptpTWSDcVt0TY9IOeuJzIJgokgwURw0Il6ORaDZfCRZEeF/nmZvQyzLFojhBAjQgJwAWT6lkAYE8xzzjujx4nt6iD85+MAlHx0HibfOG2lZLaBb072MphEOBuEA03ZkeTcaOjZfswkep9DU7PlA2dKMfRcjGAw9rme3a4pRuLJFDaHB8Vsz46C5j7qo9K27PdCH5nus99sy7/dYPe3OHoCa5FMOCwws8FMpaOSSkcllA1+G03TCCVDA0Jx38DcFm2jK9FFUk1myy/Cx076vCW2kt7yikFGkiscFTjNThlNFkKIU5AAXABqOj8An0kLtHRHjM6nGwBwXjEN+5IJ3KvX6oSKRdnLSFLV7HKxfUsNNC0bXg3GbIDVrxtOvv0UgSKdSrG2p1uAWboFCLIjyR6rB4/Vw7ziof/JTWaSg44e69cjrbTF2kiraTrjnXTGO9nbOUQpEdneyLlAXGIrwWv14rV5sx+tXoqtxXhsHv263WSXwCyEmHIkABdAtgSipwbYkDztEggtlcH/P3vREhksM9x4rq8bhaOcBAwGMPSMrgoxTlmMFn2Vu6GomkpXvCt/JDmaP5LcGm2lO9lNLB2jMdRIY6hxWM9vNpjzQvGAyyDh2WV2SWgWQkxoEoALQO1XAnG6Abjr2YOkTkQwOMyU3rEAxShvhwsxmRkUg97VYmHpwiFvF01F8wJxV7yLQCJAMBGkK9HV+zGe/ZhSU6TUlL7a3nCZFBNuqzsvKBfbivFYPUNuc1vcGA0FmKMghBCDkABcAGomjdozmSVhTJxWF4jI5hai77WCAiW3z8fokRZLQoisInMRdZ466jx1p7ytpmnE0jECiYAeivXriSBd8ezHQCKQd4mlY6S13nKM4VJQcFvd2VFkq0f/2HeUuf82j9WD2SAlRUKIkScBuACyC2Fk+5za7LZh9wBONofpevYgAO5rZ2CbUzxqxyiEmNwURaHIXESRuei03oVKZBIE4r2BuG94HmpbOBVGQ9M7YpwOl9k1ICifbNTZa/NK72UhxClJAC4ANZNBM2e/9cWu4YVYNZ6m83/2QlrFNr8Y18ra0TxEIYQYlNVozbZnc1QM+z6pTIpgMpgXnPXLYNsSAUKJEBoa3aluulPdp+yQ0ZfdZB+yjtllcnE4eZiSEyWUOkr1UWeZDCjE1CIBuAAymUy2dy5Q5hmih1IfmqbR9XQDaX8co9dK8cfmoxjkhVoIMTGYjWZ8dh8++/C71WTUDKFk6ORBud+2YCJIRssQS8eIpWMnXaTk6Q1P531uMViGLMUYatRZWs4JMXFJAC6AtJpdzUtDo6q46pS3D7/RTGy3H4wKpXcuxOiQmjghxORmNBgpthVTbBt+qZeqqYRTYX2S32BBuSvWxcETBzE6jXqNc0pNkVST+gTC4TIppmw9c79SDKfZSZG5CIfZgd1kz143ObIlJ6bs9iJzkb5P6pyFGHsSgAsg3bPwWEpJnrT1EUD8YIDgS4cB8H5wFpZa12gfnhBCTEgGxYDb4sZtcVPL4GViqVSKF/v07M5NBtQDc/+R5nj+CHNukmBuMqA/7scf95/VcVsMFj0c5+qy9aBsKsrf1/Mxb19ue5/bSMcNIU5OAnABpHIB+BQ9gFNtUfy/3guqhn1FGY6LTz1aLIQQYvj6TgY8nUWJ4ul4XigOJAL6yHM0FSWajhJJRfTrfT9G0tntKTUFQFJNkkwkCSQCI/Z12Yy2AcE5b+S5z+e563azfdDg7TA7sJlsGBRpuSkmDwnABZDp+ZhUhu4BnOlO0vGLXWjxNJYZbkpumyu1ZkIIMU7YTDYqTT3LYZ+hVCY1ZDjWtw9jXyQV0bdltOxfmHgmTjwTp5Pht6o7FbvJPvxR6aH29Rm1thlt8ndNFIwE4AJIk/2FTxqSg/YAVpMZOh7bTSaQwFRqo/RTi1DM8naWEEJMJmajGY8xu1z2SNA0jZSaygvEueuxVCwvQOdGp2Pp2MmDdzqKqmXnreQmF44Ug2LIC8V6wO47Kp2roT5FuDZrZpJakrSaxozUVItTkwBcAJncf7xmBvQA1lSNzif2kToWxlBkovSeJTLpTQghxCkpioLFaMFitFDMyPSJ1zSNeCY+eClHn6A93H2RVEQP0blJi+FUGEYoVz/85MMYFaP+fbAarL3Xjb3XT7bParRiMZz5dqvRislgktHtcU4CcAGoPXVUFtvAYBt84RDxvZ1gUii9azFmn32sD08IIYQAsqHabrJjN9kppXREHlPVVOLp+IA66bzR6sFGsXOj1YOE63gmrj9+31Z4haSHZMMggfkU2wcN2WcQ3KVue2gSgAtAM2S/7UVFRXnbu984TvitZgBKPjYf6wz3mB+bEEIIMZoMikGvAz6d3tAnE0/Eef6l51m1ehWqQSWZSZLMJElkEtlJhj3XE5kEqUyq97qaOun2vMfJXVcH355Uk3nHlHusQjIZTPqotNlgPu0gPpztZUVlzPLMKujXeSYkAI8xTdPQjNl6XrczG3DVRIbgS4eJvJ1t2u65YSZFy069QIYQQgghsn2jLYoFj9WD2VyYskFVU7M9pfsH5tx1dejtpxPQT/ZYiUwCDU0/prSaJq2miaQio/Z13zjzRr575XdH7fFHy6QJwI888gj/8i//QktLC8uXL+eHP/whF154YaEPawA1k0YzZn85S70+EocCdP7vfjKd2bdvnFfV4Lxy+K14hBBCCFF4BsWgj7a6KEzPfk3TSGvp0xq5Hk5Azxvp7hvW1RRVjonZonVSBOCnnnqKBx54gJ/85CdcdNFF/OAHP+C6666jvr6e8vLyQh9eHjWdQTNlv+0Lm2po37ATILvE8UfmYpszMhMXhBBCCDG1KIqCWTFjNphxmB2FPpxxbVIE4O9///vce++93HPPPQD85Cc/4Y9//CO/+MUv+Lu/+7sCH12+RCSCZsx+28sPZjtAGCvTmGfFiDRsJtJQyKMTZ0rNZCjasYeAwYLBKC3rJgs5r5OPnNPJSc5r4ZgrK3Gds3TCdb2Y8AE4mUyyZcsWHnzwQX2bwWBg9erVbNq0adD7JBIJEonewvRQKARkl8hMpVKjerzN7+yDnh8SVTXzVjRN+z4N9k34UzHFmYCLaDhe6OMQI0vO6+Qj53RykvNaOB3c868JzNaR/ccjl8f65rKRzGgTPnV1dHSQyWSoqKjI215RUcG+ffsGvc93vvMdvvWtbw3Yvnbt2gGdGUaav+UIZSknSVK8HlJJj+qzCSGEEEKMrldeeQXDKCXKdevW6dej0eiIPe6ED8Bn4sEHH+SBBx7QPw+FQtTW1rJmzRrc7tFtPaZpGtFgjA2vb+Tqqy8o2GxVMbJSqRSvvvoqV199tZzTSUTO6+Qj53RykvNaWCaLYcRLIFKpFOvWrePaa6/Vz2nuHfuRMOEDsM/nw2g00tramre9tbWVysrB12i3Wq1YrdYB281m85j84iheBYMJipw2+UWdJFIpo5zTSUjO6+Qj53RykvM6efXNZiN5bif8EiEWi4XzzjuP9evX69tUVWX9+vVccsklBTwyIYQQQggxHk34EWCABx54gLvuuovzzz+fCy+8kB/84AdEIhG9K4QQQgghhBA5kyIAf/zjH6e9vZ1vfOMbtLS0sGLFCl5++eUBE+OEEEIIIYSYFAEY4P777+f+++8v9GEIIYQQQohxbsLXAAshhBBCCHE6JAALIYQQQogpRQKwEEIIIYSYUiQACyGEEEKIKUUCsBBCCCGEmFIkAAshhBBCiClFArAQQgghhJhSJAALIYQQQogpRQKwEEIIIYSYUiQACyGEEEKIKUUCsBBCCCGEmFIkAAshhBBCiClFArAQQgghhJhSTIU+gPFA0zQAQqHQmDxfKpUiGo0SCoUwm81j8pxidMk5nZzkvE4+ck4nJzmvk89g5zSX03K57WxIAAa6u7sBqK2tLfCRCCGEEEKIk+nu7sbj8ZzVYyjaSMToCU5VVZqbm3G5XCiKMurPFwqFqK2t5ejRo7jd7lF/PjH65JxOTnJeJx85p5OTnNfJZ7Bzqmka3d3dVFdXYzCcXRWvjAADBoOBmpqaMX9et9stv6iTjJzTyUnO6+Qj53RykvM6+fQ/p2c78psjk+CEEEIIIcSUIgFYCCGEEEJMKRKAC8BqtfLQQw9htVoLfShihMg5nZzkvE4+ck4nJzmvk89on1OZBCeEEEIIIaYUGQEWQgghhBBTigRgIYQQQggxpUgAFkIIIYQQU4oEYCGEEEIIMaVIAB5jjzzyCHV1ddhsNi666CLefffdQh+SOA3f/OY3URQl77JgwQJ9fzwe57777qO0tBSn08ltt91Ga2trAY9Y9Pf6669z0003UV1djaIo/OEPf8jbr2ka3/jGN6iqqsJut7N69Wr279+fd5vOzk7uvPNO3G43Xq+XT3/604TD4TH8KkR/pzqvd99994Df3euvvz7vNnJex5fvfOc7XHDBBbhcLsrLy7nllluor6/Pu81wXnObmpr4wAc+QFFREeXl5fzt3/4t6XR6LL8U0WM453TlypUDflc/+9nP5t1mJM6pBOAx9NRTT/HAAw/w0EMPsXXrVpYvX851111HW1tboQ9NnIbFixdz4sQJ/fLGG2/o+7785S/z/PPP8/TTT/Paa6/R3NzMrbfeWsCjFf1FIhGWL1/OI488Muj+733ve/zHf/wHP/nJT3jnnXdwOBxcd911xONx/TZ33nknu3fvZt26dbzwwgu8/vrrfOYznxmrL0EM4lTnFeD666/P+9194okn8vbLeR1fXnvtNe677z7efvtt1q1bRyqVYs2aNUQiEf02p3rNzWQyfOADHyCZTPLWW2/x2GOP8ctf/pJvfOMbhfiSprzhnFOAe++9N+939Xvf+56+b8TOqSbGzIUXXqjdd999+ueZTEarrq7WvvOd7xTwqMTpeOihh7Tly5cPui8QCGhms1l7+umn9W179+7VAG3Tpk1jdITidADa73//e/1zVVW1yspK7V/+5V/0bYFAQLNardoTTzyhaZqm7dmzRwO0zZs367d56aWXNEVRtOPHj4/ZsYuh9T+vmqZpd911l3bzzTcPeR85r+NfW1ubBmivvfaapmnDe8198cUXNYPBoLW0tOi3+fGPf6y53W4tkUiM7RcgBuh/TjVN06666irti1/84pD3GalzKiPAYySZTLJlyxZWr16tbzMYDKxevZpNmzYV8MjE6dq/fz/V1dXMmjWLO++8k6amJgC2bNlCKpXKO8cLFixg+vTpco4niMOHD9PS0pJ3Dj0eDxdddJF+Djdt2oTX6+X888/Xb7N69WoMBgPvvPPOmB+zGL6NGzdSXl7O/Pnz+dznPoff79f3yXkd/4LBIAAlJSXA8F5zN23axNKlS6moqNBvc9111xEKhdi9e/cYHr0YTP9zmvM///M/+Hw+lixZwoMPPkg0GtX3jdQ5NZ3lsYth6ujoIJPJ5J0wgIqKCvbt21egoxKn66KLLuKXv/wl8+fP58SJE3zrW9/iiiuuYNeuXbS0tGCxWPB6vXn3qaiooKWlpTAHLE5L7jwN9nua29fS0kJ5eXnefpPJRElJiZzncez666/n1ltvZebMmRw8eJCvf/3r3HDDDWzatAmj0SjndZxTVZUvfelLXHbZZSxZsgRgWK+5LS0tg/4+5/aJwhnsnALccccdzJgxg+rqanbs2MHXvvY16uvreeaZZ4CRO6cSgIU4DTfccIN+fdmyZVx00UXMmDGD3/72t9jt9gIemRDiZD7xiU/o15cuXcqyZcuYPXs2Gzdu5JprringkYnhuO+++9i1a1fenAsxsQ11TvvW3S9dupSqqiquueYaDh48yOzZs0fs+aUEYoz4fD6MRuOA2amtra1UVlYW6KjE2fJ6vcybN48DBw5QWVlJMpkkEAjk3UbO8cSRO08n+z2trKwcMHE1nU7T2dkp53kCmTVrFj6fjwMHDgByXsez+++/nxdeeIENGzZQU1Ojbx/Oa25lZeWgv8+5faIwhjqng7nooosA8n5XR+KcSgAeIxaLhfPOO4/169fr21RVZf369VxyySUFPDJxNsLhMAcPHqSqqorzzjsPs9mcd47r6+tpamqSczxBzJw5k8rKyrxzGAqFeOedd/RzeMkllxAIBNiyZYt+m1dffRVVVfUXajH+HTt2DL/fT1VVFSDndTzSNI3777+f3//+97z66qvMnDkzb/9wXnMvueQSdu7cmffPzbp163C73SxatGhsvhChO9U5Hcz7778PkPe7OiLn9Awm7Ykz9OSTT2pWq1X75S9/qe3Zs0f7zGc+o3m93ryZjGJ8+8pXvqJt3LhRO3z4sPbmm29qq1ev1nw+n9bW1qZpmqZ99rOf1aZPn669+uqr2nvvvaddcskl2iWXXFLgoxZ9dXd3a9u2bdO2bdumAdr3v/99bdu2bdqRI0c0TdO0f/7nf9a8Xq/27LPPajt27NBuvvlmbebMmVosFtMf4/rrr9fOOecc7Z133tHeeOMNbe7cudrtt99eqC9JaCc/r93d3drf/M3faJs2bdIOHz6s/elPf9LOPfdcbe7cuVo8HtcfQ87r+PK5z31O83g82saNG7UTJ07ol2g0qt/mVK+56XRaW7JkibZmzRrt/fff115++WWtrKxMe/DBBwvxJU15pzqnBw4c0B5++GHtvffe0w4fPqw9++yz2qxZs7Qrr7xSf4yROqcSgMfYD3/4Q2369OmaxWLRLrzwQu3tt98u9CGJ0/Dxj39cq6qq0iwWizZt2jTt4x//uHbgwAF9fywW0z7/+c9rxcXFWlFRkfbhD39YO3HiRAGPWPS3YcMGDRhwueuuuzRNy7ZC+4d/+AetoqJCs1qt2jXXXKPV19fnPYbf79duv/12zel0am63W7vnnnu07u7uAnw1Iudk5zUajWpr1qzRysrKNLPZrM2YMUO79957Bww+yHkdXwY7n4D26KOP6rcZzmtuY2OjdsMNN2h2u13z+XzaV77yFS2VSo3xVyM07dTntKmpSbvyyiu1kpISzWq1anPmzNH+9m//VgsGg3mPMxLnVOk5ICGEEEIIIaYEqQEWQgghhBBTigRgIYQQQggxpUgAFkIIIYQQU4oEYCGEEEIIMaVIABZCCCGEEFOKBGAhhBBCCDGlSAAWQgghhBBTigRgIYQQQggxpUgAFkKICeruu+/mlltuKfRhCCHEhGMq9AEIIYQYSFGUk+5/6KGH+Pd//3dkMU8hhDh9EoCFEGIcOnHihH79qaee4hvf+Ab19fX6NqfTidPpLMShCSHEhCclEEIIMQ5VVlbqF4/Hg6IoeducTueAEoiVK1fyhS98gS996UsUFxdTUVHBz372MyKRCPfccw8ul4s5c+bw0ksv5T3Xrl27uOGGG3A6nVRUVPDJT36Sjo6OMf6KhRBi7EgAFkKISeSxxx7D5/Px7rvv8oUvfIHPfe5zfPSjH+XSSy9l69atrFmzhk9+8pNEo1EAAoEAV199Neeccw7vvfceL7/8Mq2trXzsYx8r8FcihBCjRwKwEEJMIsuXL+fv//7vmTt3Lg8++CA2mw2fz8e9997L3Llz+cY3voHf72fHjh0A/Od//ifnnHMO3/72t1mwYAHnnHMOv/jFL9iwYQMNDQ0F/mqEEGJ0SA2wEEJMIsuWLdOvG41GSktLWbp0qb6toqICgLa2NgC2b9/Ohg0bBq0nPnjwIPPmzRvlIxZCiLEnAVgIISYRs9mc97miKHnbct0lVFUFIBwOc9NNN/Hd7353wGNVVVWN4pEKIUThSAAWQogp7Nxzz+V3v/sddXV1mEzyJ0EIMTVIDbAQQkxh9913H52dndx+++1s3ryZgwcP8sorr3DPPfeQyWQKfXhCCDEqJAALIcQUVl1dzZtvvkkmk2HNmjUsXbqUL33pS3i9XgwG+RMhhJicFE2WERJCCCGEEFOI/HsvhBBCCCGmFAnAQgghhBBiSpEALIQQQgghphQJwEIIIYQQYkqRACyEEEIIIaYUCcBCCCGEEGJKkQAshBBCCCGmFAnAQgghhBBiSpEALIQQQgghphQJwEIIIYQQYkqRACyEEEIIIaaU/x/cxM4PFg8o7QAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], + "execution_count": null, "source": [ "%matplotlib inline\n", "import matplotlib.pyplot as plt\n", @@ -449,70 +171,41 @@ "\n", "# Plot the simulation results\n", "plot_simulation(results)" - ] + ], + "id": "72f1ed397105e14a" }, { - "cell_type": "markdown", - "id": "f57c07211b781ab5", "metadata": {}, - "source": "`run_simulations` enables users to specify the simulation conditions to be executed. For more complex models, this allows for restricting simulations to a subset of conditions. Since the Böhm model includes only a single condition, we demonstrate this functionality by simulating no condition at all." + "cell_type": "markdown", + "source": "`run_simulations` enables users to specify the simulation conditions to be executed. For more complex models, this allows for restricting simulations to a subset of conditions. Since the Böhm model includes only a single condition, we demonstrate this functionality by simulating no condition at all.", + "id": "4fa97c33719c2277" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 6, - "id": "2f2e1c7023ad261b", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:50:58.505973Z", - "start_time": "2024-11-19T09:50:58.501775Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "{}" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], + "execution_count": null, "source": [ "llh, results = run_simulations(jax_problem, simulation_conditions=tuple())\n", "results" - ] + ], + "id": "7950774a3e989042" }, { - "cell_type": "markdown", - "id": "0b729e1b-3c75-4a87-a33b-0a54622609e7", "metadata": {}, + "cell_type": "markdown", "source": [ "## Updating Parameters\n", "\n", "As next step, we will update the parameter values used for simulation. However, if we attempt to directly modify the values in `JAXModel.parameters`, we encounter a `FrozenInstanceError`." - ] + ], + "id": "98b8516a75ce4d12" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 7, - "id": "75df1ab9e8a738a0", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:50:58.685750Z", - "start_time": "2024-11-19T09:50:58.575034Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Error: cannot assign to field 'parameters'\n" - ] - } - ], + "outputs": [], + "execution_count": null, "source": [ "from dataclasses import FrozenInstanceError\n", "import jax\n", @@ -530,40 +223,24 @@ " jax_problem.parameters += noise\n", "except FrozenInstanceError as e:\n", " print(\"Error:\", e)" - ] + ], + "id": "3d278a3d21e709d" }, { - "cell_type": "markdown", - "id": "b91941cf707704c3", "metadata": {}, + "cell_type": "markdown", "source": [ "The root cause of this error lies in the fact that, to enable autodiff, direct modifications of attributes are not allowed in [equinox](https://docs.kidger.site/equinox/), which AMICI utilizes under the hood. Consequently, attributes of instances like `JAXModel` or `JAXProblem` cannot be updated directly — this is the price we have to pay for autodiff.\n", "\n", "However, `JAXProblem` provides a convenient method called [update_parameters](https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.JAXProblem.update_parameters). The caveat is that this method creates a new JAXProblem instance instead of modifying the existing one." - ] + ], + "id": "4cc3d595de4a4085" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 8, - "id": "feb125b6-4f84-427c-b870-421a328eee81", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:00.631866Z", - "start_time": "2024-11-19T09:50:58.702698Z" - } - }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAsAAAAIjCAYAAAAN/63DAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAADxiUlEQVR4nOzdd1hUZ/bA8e9UGLo0AQVERUQIdrGDUWNJTEzMGk1MNBpNsmvKmmjKxlg21fxck5ieYElvm7gx1S5GBcWuKChiB1Fp0qfc3x8Do8QGCgzlfJ6HR+bOnXvPzHXg8M55z6tSFEVBCCGEEEKIJkJt7wCEEEIIIYSoS5IACyGEEEKIJkUSYCGEEEII0aRIAiyEEEIIIZoUSYCFEEIIIUSTIgmwEEIIIYRoUiQBFkIIIYQQTYokwEIIIYQQokmRBFgIIYQQQjQpkgALIRq8efPm0b59eywWi71DuawjR46gUqlYsmRJtR+7bt06VCoV69atq/G4riY2NpbY2Ng6PWdt6tmzJzNmzLB3GEKIekISYCFEg5afn8/rr7/OM888g1pt/ZF2vclmQ5GSksI///lPevfujaOjIyqViiNHjtg7LACKioqYPXv2DSXsBQUFzJo1i6FDh+Lp6XnV6xkbG8uECROuecxnnnmGd999l8zMzOuOSwjReEgCLIRo0BYtWoTJZGLs2LH2DqXObN68mbfffpvz588THh5u73AqKSoqYs6cOTeUAJ89e5a5c+eyf/9+OnbsWCNx3XHHHbi5ufHee+/VyPGEEA2bJMBCiAZt8eLF3H777Tg6Oto7lDpz++23k5uby549e7jvvvvsHU6N8/f3JyMjg6NHj/LGG2/UyDHVajV33303n376KYqi1MgxhRANlyTAQogGKz09nd27dzNo0KCr7jd79mxUKhWpqamMGzcOd3d3fHx8mDlzJoqicPz4cdsIoZ+fH/Pnz7/kGFlZWUyaNInmzZvj6OhIx44dWbp06SX75ebmMmHCBNzd3fHw8GD8+PHk5uZeNq4DBw5w99134+npiaOjI926deOnn3665vP29PTE1dX1mvtV1UcffUSbNm0wGAz06NGDDRs2XLJPWVkZL774Il27dsXd3R1nZ2f69evH2rVrbfscOXIEHx8fAObMmYNKpUKlUjF79mwAdu/ezYQJE2jdujWOjo74+fkxceJEzp07V+lcDg4O+Pn51djzqzB48GCOHj3Kzp07a/zYQoiGRRJgIUSDtWnTJgC6dOlSpf3vueceLBYLr732GtHR0bz00ku8+eabDB48mBYtWvD666/Ttm1bnn76aeLj422PKy4uJjY2ls8++4z77ruPN954A3d3dyZMmMBbb71l209RFO644w4+++wzxo0bx0svvcSJEycYP378JbHs27ePnj17sn//fp599lnmz5+Ps7MzI0eO5Mcff7zBV6bq4uLiePjhh/Hz82PevHn06dOH22+/nePHj1faLz8/n08++YTY2Fhef/11Zs+ezZkzZxgyZIgtofTx8eH9998H4M477+Szzz7js88+46677gJg5cqVHD58mAcffJCFCxcyZswYvv76a4YPH14no7Jdu3YFYOPGjbV+LiFEPacIIUQD9cILLyiAcv78+avuN2vWLAVQpkyZYttmMpmUli1bKiqVSnnttdds23NychSDwaCMHz/etu3NN99UAOXzzz+3bSsrK1N69eqluLi4KPn5+YqiKMqyZcsUQJk3b16l8/Tr108BlMWLF9u2Dxw4ULnpppuUkpIS2zaLxaL07t1bCQ0NtW1bu3atAihr16697HN74403FEBJT0+/6mtwOWVlZYqvr6/SqVMnpbS01Lb9o48+UgAlJiam0vO4eB9Fsb5WzZs3VyZOnGjbdubMGQVQZs2adcn5ioqKLtn21VdfKYASHx9/2Ri3bt16yWt3I/R6vfLoo4/WyLGEEA2XjAALIRqsc+fOodVqcXFxqdL+Dz30kO17jUZDt27dUBSFSZMm2bZ7eHgQFhbG4cOHbdt+/fVX/Pz8Kk200+l0PP744xQUFLB+/XrbflqtlkcffbTSeR577LFKcWRnZ7NmzRpGjx7N+fPnOXv2LGfPnuXcuXMMGTKEgwcPcvLkyeq9GNchKSmJrKwsHnnkEfR6vW17RQnHxTQajW0fi8VCdnY2JpOJbt26sX379iqdz2Aw2L4vKSnh7Nmz9OzZE6DKx7hRzZo14+zZs3VyLiFE/aW1dwBCCFFXgoKCKt12d3fH0dERb2/vS7ZfXJd69OhRQkNDbW3WKlR0YDh69KjtX39//0sS8rCwsEq3Dx06hKIozJw5k5kzZ1421qysLFq0aFGNZ1d9FXGHhoZW2q7T6WjduvUl+y9dupT58+dz4MABjEajbXtISEiVzpednc2cOXP4+uuvycrKqnRfXl5edcO/LoqioFKp6uRcQoj6SxJgIUSD5eXlhclk4vz581WaFKbRaKq0DajVmtSKBTuefvpphgwZctl92rZtW2vnvx6ff/45EyZMYOTIkUyfPh1fX180Gg2vvvoqaWlpVTrG6NGj2bRpE9OnT6dTp064uLhgsVgYOnRonS1ikpube8kfPEKIpkcSYCFEg9W+fXvA2g0iKiqq1s4THBzM7t27sVgslUaBDxw4YLu/4t/Vq1dTUFBQaRQ4JSWl0vEqRld1Ot01O1jUpoq4Dx48yM0332zbbjQaSU9Pr9SD9/vvv6d169b88MMPlUZQZ82aVemYVxpdzcnJYfXq1cyZM4cXX3zRtv3gwYM18lyq4uTJk5SVldW73slCiLonNcBCiAarV69egLWWtTYNHz6czMxMvvnmG9s2k8nEwoULcXFxISYmxrafyWSydUIAMJvNLFy4sNLxfH19iY2N5cMPPyQjI+OS8505c6aWnkll3bp1w8fHhw8++ICysjLb9iVLllzSuq1ipPzikfHExEQ2b95caT8nJyeAKj0e4M0337yRp1At27ZtA6B37951dk4hRP0kI8BCiAardevWREZGsmrVKiZOnFhr55kyZQoffvghEyZMYNu2bbRq1Yrvv/+ejRs38uabb9rKL0aMGEGfPn149tlnOXLkCB06dOCHH364bH3ru+++S9++fbnpppuYPHkyrVu35vTp02zevJkTJ06wa9euK8aTl5dnS6orWnq98847eHh44OHhwdSpU6v0vHQ6HS+99BIPP/wwN998M/fccw/p6eksXrz4khrg2267jR9++IE777yTW2+9lfT0dD744AM6dOhAQUGBbT+DwUCHDh345ptvaNeuHZ6enkRGRhIZGUn//v2ZN28eRqORFi1asGLFCtLT0y8b2zvvvENubi6nTp0CYPny5Zw4cQKAxx577JJJeheLjY1l/fr1lyTbK1euJCgoiM6dO1fp9RFCNGJ27EAhhBA37D//+Y/i4uJy2RZbFSraoJ05c6bS9vHjxyvOzs6X7B8TE6NERERU2nb69GnlwQcfVLy9vRW9Xq/cdNNNl23Nde7cOeX+++9X3NzcFHd3d+X+++9XduzYcdlWXmlpacoDDzyg+Pn5KTqdTmnRooVy2223Kd9//71tn8u1QUtPT1eAy34FBwdf+cW6gvfe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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], + "execution_count": null, "source": [ "# Update the parameters and create a new JAXProblem instance\n", "jax_problem = jax_problem.update_parameters(jax_problem.parameters + noise)\n", @@ -573,221 +250,105 @@ "\n", "# Plot the simulation results\n", "plot_simulation(results)" - ] + ], + "id": "e47748376059628b" }, { - "cell_type": "markdown", - "id": "e73bdd447a4d48c8", "metadata": {}, + "cell_type": "markdown", "source": [ "## Computing Gradients\n", "\n", "Similar to updating attributes, computing gradients in the JAX ecosystem can feel a bit unconventional if you’re not familiar with the JAX ecosysmt. JAX offers [powerful automatic differentiation](https://jax.readthedocs.io/en/latest/automatic-differentiation.html) through the `jax.grad` function. However, to use `jax.grad` with `JAXProblem`, we need to specify which parts of the `JAXProblem` should be treated as static." - ] + ], + "id": "660baf605a4e8339" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 9, - "id": "a8918f59607e6525", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:00.662578Z", - "start_time": "2024-11-19T09:51:00.649386Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Error: Argument 'ParameterMappingForCondition(map_sim_var={'Epo_degradation_BaF3': 'Epo_degradation_BaF3', 'k_exp_hetero': 'k_exp_hetero', 'k_exp_homo': 'k_exp_homo', 'k_imp_hetero': 'k_imp_hetero', 'k_imp_homo': 'k_imp_homo', 'k_phos': 'k_phos', 'ratio': 0.693, 'specC17': 0.107, 'noiseParameter1_pSTAT5A_rel': 'sd_pSTAT5A_rel', 'noiseParameter1_pSTAT5B_rel': 'sd_pSTAT5B_rel', 'noiseParameter1_rSTAT5A_rel': 'sd_rSTAT5A_rel'},scale_map_sim_var={'Epo_degradation_BaF3': 'log10', 'k_exp_hetero': 'log10', 'k_exp_homo': 'log10', 'k_imp_hetero': 'log10', 'k_imp_homo': 'log10', 'k_phos': 'log10', 'ratio': 'lin', 'specC17': 'lin', 'noiseParameter1_pSTAT5A_rel': 'log10', 'noiseParameter1_pSTAT5B_rel': 'log10', 'noiseParameter1_rSTAT5A_rel': 'log10'},map_preeq_fix={},scale_map_preeq_fix={},map_sim_fix={},scale_map_sim_fix={})' of type is not a valid JAX type.\n" - ] - } - ], + "outputs": [], + "execution_count": null, "source": [ "try:\n", " # Attempt to compute the gradient of the run_simulations function\n", " jax.grad(run_simulations, has_aux=True)(jax_problem)\n", "except TypeError as e:\n", " print(\"Error:\", e)" - ] + ], + "id": "7033d09cc81b7f69" }, { - "cell_type": "markdown", - "id": "922a9ffd94c99607", "metadata": {}, - "source": "Fortunately, `equinox` simplifies this process by offering [filter_grad](https://docs.kidger.site/equinox/api/transformations/#equinox.filter_grad), which enables autodiff functionality that is compatible with `JAXProblem` and, in theory, also with `JAXModel`." + "cell_type": "markdown", + "source": "Fortunately, `equinox` simplifies this process by offering [filter_grad](https://docs.kidger.site/equinox/api/transformations/#equinox.filter_grad), which enables autodiff functionality that is compatible with `JAXProblem` and, in theory, also with `JAXModel`.", + "id": "dc9bc07cde00a926" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 10, - "id": "e2c635b6-79db-4e78-8738-789af29110b5", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:07.293314Z", - "start_time": "2024-11-19T09:51:00.709141Z" - } - }, "outputs": [], + "execution_count": null, "source": [ "import equinox as eqx\n", "\n", "# Compute the gradient using equinox's filter_grad, preserving auxiliary outputs\n", "grad, _ = eqx.filter_grad(run_simulations, has_aux=True)(jax_problem)" - ] + ], + "id": "a6704182200e6438" }, { - "cell_type": "markdown", - "id": "8fd639ad39948e72", "metadata": {}, - "source": "Functions transformed by `filter_grad` return gradients that share the same structure as the first argument (unless specified otherwise). This allows us to access the gradient with respect to the parameters attribute directly `via grad.parameters`." + "cell_type": "markdown", + "source": "Functions transformed by `filter_grad` return gradients that share the same structure as the first argument (unless specified otherwise). This allows us to access the gradient with respect to the parameters attribute directly `via grad.parameters`.", + "id": "851c3ec94cb5d086" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 11, - "id": "ab9225bf704e9ed5", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:07.310244Z", - "start_time": "2024-11-19T09:51:07.306293Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "Array([ 2.39759630e+01, -1.36704159e-01, 1.33625245e+01, 3.25229304e+01,\n", - " 4.88660333e-05, 5.39482681e+01, -5.13624151e+00, -2.90885864e-02,\n", - " 6.08639536e+01], dtype=float64)" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grad.parameters" - ] + "outputs": [], + "execution_count": null, + "source": "grad.parameters", + "id": "c00c1581d7173d7a" }, { - "cell_type": "markdown", - "id": "5793acc4ad8908be", "metadata": {}, - "source": "Attributes for which derivatives cannot be computed (typically anything that is not a [jax.numpy.array](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.array.html)) are automatically set to `None`." + "cell_type": "markdown", + "source": "Attributes for which derivatives cannot be computed (typically anything that is not a [jax.numpy.array](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.array.html)) are automatically set to `None`.", + "id": "375b835fecc5a022" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 12, - "id": "77e6bc4fa3e6970a", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:07.398319Z", - "start_time": "2024-11-19T09:51:07.392032Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "JAXProblem(\n", - " parameters=f64[9],\n", - " model=JAXModel_Boehm_JProteomeRes2014(api_version='0.0.1'),\n", - " _parameter_mappings={'model1_data1': None},\n", - " _measurements={('model1_data1',): (f64[3], f64[45], f64[0], f64[48], None)},\n", - " _petab_problem=None\n", - ")" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grad" - ] + "outputs": [], + "execution_count": null, + "source": "grad", + "id": "f7c17f7459d0151f" }, { - "cell_type": "markdown", - "id": "75fc08817f1b4734", "metadata": {}, - "source": "Observant readers may notice that the gradient above appears to include numeric values for derivatives with respect to some measurements. However, `simulation_conditions` internally disables gradient computations using `jax.lax.stop_gradient`, resulting in these values being zeroed out." + "cell_type": "markdown", + "source": "Observant readers may notice that the gradient above appears to include numeric values for derivatives with respect to some measurements. However, `simulation_conditions` internally disables gradient computations using `jax.lax.stop_gradient`, resulting in these values being zeroed out.", + "id": "8eb7cc3db510c826" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 13, - "id": "a8b7634e-7bd8-41ae-a6dc-1d0f29993ac0", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:07.455764Z", - "start_time": "2024-11-19T09:51:07.450233Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(Array([0., 0., 0.], dtype=float64),\n", - " Array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", - " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", - " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float64),\n", - " Array([], shape=(0,), dtype=float64),\n", - " Array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", - " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,\n", - " 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float64),\n", - " None)" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grad._measurements[simulation_condition]" - ] + "outputs": [], + "execution_count": null, + "source": "grad._measurements[simulation_condition]", + "id": "3badd4402cf6b8c6" }, { - "cell_type": "markdown", - "id": "3c6c4f2d3a2673a2", "metadata": {}, - "source": "However, we can compute derivatives with respect to data elements using `JAXModel.simulate_condition`. In the example below, we differentiate the observables `y` (specified by passing `y` to the `ret` argument) with respect to the timepoints at which the model outputs are computed after the solving the differential equation. While this might not be particularly practical, it serves as an nice illustration of the power of automatic differentiation." + "cell_type": "markdown", + "source": "However, we can compute derivatives with respect to data elements using `JAXModel.simulate_condition`. In the example below, we differentiate the observables `y` (specified by passing `y` to the `ret` argument) with respect to the timepoints at which the model outputs are computed after the solving the differential equation. While this might not be particularly practical, it serves as an nice illustration of the power of automatic differentiation.", + "id": "58eb04393a1463d" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 14, - "id": "2a843410-4af4-4ff7-8b67-9293a5820caf", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:13.735937Z", - "start_time": "2024-11-19T09:51:07.494491Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "Array([[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,\n", - " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00],\n", - " [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,\n", - " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00],\n", - " [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,\n", - " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00],\n", - " ...,\n", - " [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,\n", - " -1.30871686e-01, 0.00000000e+00, -3.80465095e-11],\n", - " [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,\n", - " 0.00000000e+00, -2.69250222e-01, -7.93596886e-11],\n", - " [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,\n", - " 0.00000000e+00, 0.00000000e+00, -2.29968854e-02]], dtype=float64)" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], + "execution_count": null, "source": [ "import jax.numpy as jnp\n", "import diffrax\n", @@ -828,29 +389,24 @@ "# Compute the gradient with respect to `ts_dyn`\n", "g = grad_ts_dyn(ts_dyn)\n", "g" - ] + ], + "id": "1a91aff44b93157" }, { - "cell_type": "markdown", - "id": "a9cec2a77b30669d", "metadata": {}, + "cell_type": "markdown", "source": [ "## Compilation & Profiling\n", "\n", "To maximize performance with JAX, code should be just-in-time (JIT) compiled. This can be achieved using the `jax.jit` or `equinox.filter_jit` decorators. While JIT compilation introduces some overhead during the first function call, it significantly improves performance for subsequent calls. To demonstrate this, we will first clear the JIT cache and then profile the execution." - ] + ], + "id": "9f870da7754e139c" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 15, - "id": "d1f79c45ab2eccdc", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:14.292251Z", - "start_time": "2024-11-19T09:51:13.834276Z" - } - }, "outputs": [], + "execution_count": null, "source": [ "from time import time\n", "\n", @@ -859,28 +415,14 @@ "\n", "# Define a JIT-compiled gradient function with auxiliary outputs\n", "gradfun = eqx.filter_jit(eqx.filter_grad(run_simulations, has_aux=True))" - ] + ], + "id": "58ebdc110ea7457e" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 16, - "id": "b44881332070e2b0", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:23.060962Z", - "start_time": "2024-11-19T09:51:14.309832Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Function compilation time: 2.53 seconds\n", - "Gradient compilation time: 6.21 seconds\n" - ] - } - ], + "outputs": [], + "execution_count": null, "source": [ "# Measure the time taken for the first function call (including compilation)\n", "start = time()\n", @@ -891,27 +433,14 @@ "start = time()\n", "gradfun(jax_problem)\n", "print(f\"Gradient compilation time: {time() - start:.2f} seconds\")" - ] + ], + "id": "e1242075f7e0faf" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 17, - "id": "a3e1463209074861", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:25.374277Z", - "start_time": "2024-11-19T09:51:23.078334Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "16.6 ms ± 609 μs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" - ] - } - ], + "outputs": [], + "execution_count": null, "source": [ "%%timeit\n", "run_simulations(\n", @@ -924,27 +453,14 @@ " dcoeff=0.0, # recommended value for stiff systems\n", " ),\n", ")" - ] + ], + "id": "27181f367ccb1817" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 18, - "id": "2f074fbbebf834c6", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:31.394645Z", - "start_time": "2024-11-19T09:51:25.459759Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "39.8 ms ± 854 μs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" - ] - } - ], + "outputs": [], + "execution_count": null, "source": [ "%%timeit \n", "gradfun(\n", @@ -957,19 +473,14 @@ " dcoeff=0.0, # recommended value for stiff systems\n", " ),\n", ")" - ] + ], + "id": "5b8d3a6162a3ae55" }, { + "metadata": {}, "cell_type": "code", - "execution_count": 19, - "id": "5f68c5fcc16b637", - "metadata": { - "ExecuteTime": { - "end_time": "2024-11-19T09:51:55.244925Z", - "start_time": "2024-11-19T09:51:31.477484Z" - } - }, "outputs": [], + "execution_count": null, "source": [ "from amici.petab import simulate_petab\n", "import amici\n", @@ -978,6 +489,7 @@ "amici_model = import_petab_problem(\n", " petab_problem,\n", " verbose=False,\n", + " compile_=True,\n", " jax=False, # load the amici model this time\n", ")\n", "\n", @@ -990,7 +502,8 @@ "problem_parameters = dict(\n", " zip(jax_problem.parameter_ids, jax_problem.parameters)\n", ")" - ] + ], + "id": "d733a450635a749b" }, { "cell_type": "code", From f79a96ee159b5be84bc89843b60c4700f659636f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sat, 30 Nov 2024 22:26:04 +0000 Subject: [PATCH 08/32] add nan safe log÷ --- python/sdist/amici/jax.template.py | 11 ++---- python/sdist/amici/jax/model.py | 36 +++++++++++++++++++ python/sdist/amici/jaxcodeprinter.py | 9 +++++ .../benchmark-models/test_petab_benchmark.py | 20 ++++------- 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/python/sdist/amici/jax.template.py b/python/sdist/amici/jax.template.py index 367ba9e500..683ebe3c02 100644 --- a/python/sdist/amici/jax.template.py +++ b/python/sdist/amici/jax.template.py @@ -1,7 +1,8 @@ +# ruff: noqa: F401, F821, F841 import jax.numpy as jnp from interpax import interp1d -from amici.jax.model import JAXModel +from amici.jax.model import JAXModel, safe_log, safe_div class JAXModel_TPL_MODEL_NAME(JAXModel): @@ -11,7 +12,6 @@ def __init__(self): super().__init__() def _xdot(self, t, x, args): - pk, tcl = args TPL_X_SYMS = x @@ -24,7 +24,6 @@ def _xdot(self, t, x, args): return TPL_XDOT_RET def _w(self, t, x, pk, tcl): - TPL_X_SYMS = x TPL_PK_SYMS = pk TPL_TCL_SYMS = tcl @@ -34,7 +33,6 @@ def _w(self, t, x, pk, tcl): return TPL_W_RET def _x0(self, pk): - TPL_PK_SYMS = pk TPL_X0_EQ @@ -42,7 +40,6 @@ def _x0(self, pk): return TPL_X0_RET def _x_solver(self, x): - TPL_X_RDATA_SYMS = x TPL_X_SOLVER_EQ @@ -50,7 +47,6 @@ def _x_solver(self, x): return TPL_X_SOLVER_RET def _x_rdata(self, x, tcl): - TPL_X_SYMS = x TPL_TCL_SYMS = tcl @@ -59,7 +55,6 @@ def _x_rdata(self, x, tcl): return TPL_X_RDATA_RET def _tcl(self, x, pk): - TPL_X_RDATA_SYMS = x TPL_PK_SYMS = pk @@ -68,7 +63,6 @@ def _tcl(self, x, pk): return TPL_TOTAL_CL_RET def _y(self, t, x, pk, tcl): - TPL_X_SYMS = x TPL_PK_SYMS = pk TPL_W_SYMS = self._w(t, x, pk, tcl) @@ -86,7 +80,6 @@ def _sigmay(self, y, pk): return TPL_SIGMAY_RET - def _nllh(self, t, x, pk, tcl, my, iy): y = self._y(t, x, pk, tcl) TPL_Y_SYMS = y diff --git a/python/sdist/amici/jax/model.py b/python/sdist/amici/jax/model.py index a7b274027a..3425f5c015 100644 --- a/python/sdist/amici/jax/model.py +++ b/python/sdist/amici/jax/model.py @@ -557,3 +557,39 @@ def simulate_condition( stats_dyn=stats_dyn, stats_posteq=stats_posteq, ) + + +def safe_log(x: jnp.float_) -> jnp.float_: + """ + Safe logarithm that returns `jnp.log(jnp.finfo(jnp.float_).eps)` for x <= 0. + + :param x: + input + :return: + logarithm of x + """ + # see https://docs.kidger.site/equinox/api/debug/, need double jnp.where to guard + # against nans in forward & backward passes + safe_x = jnp.where( + x > jnp.finfo(jnp.float_).eps, x, jnp.finfo(jnp.float_).eps + ) + return jnp.where( + x > 0, jnp.log(safe_x), jnp.log(jnp.finfo(jnp.float_).eps) + ) + + +def safe_div(x: jnp.float_, y: jnp.float_) -> jnp.float_: + """ + Safe division that returns `x/jnp.finfo(jnp.float_).eps` for `y == 0`. + + :param x: + numerator + :param y: + denominator + :return: + x / y + """ + # see https://docs.kidger.site/equinox/api/debug/, need double jnp.where to guard + # against nans in forward & backward passes + safe_y = jnp.where(y != 0, y, jnp.finfo(jnp.float_).eps) + return jnp.where(y != 0, x / safe_y, x / jnp.finfo(jnp.float_).eps) diff --git a/python/sdist/amici/jaxcodeprinter.py b/python/sdist/amici/jaxcodeprinter.py index ed9181cc09..6cfce97b35 100644 --- a/python/sdist/amici/jaxcodeprinter.py +++ b/python/sdist/amici/jaxcodeprinter.py @@ -27,6 +27,15 @@ def _print_AmiciSpline(self, expr: sp.Expr) -> str: # FIXME: untested, where are spline nodes coming from anyways? return f'interp1d(time, {self.doprint(expr.args[2:])}, kind="cubic")' + def _print_log(self, expr: sp.Expr) -> str: + return f"safe_log({self.doprint(expr.args[0])})" + + def _print_Mul(self, expr: sp.Expr) -> str: + numer, denom = expr.as_numer_denom() + if denom == 1: + return super()._print_Mul(expr) + return f"safe_div({self.doprint(numer)}, {self.doprint(denom)})" + def _get_sym_lines( self, symbols: sp.Matrix | Iterable[str], diff --git a/tests/benchmark-models/test_petab_benchmark.py b/tests/benchmark-models/test_petab_benchmark.py index 7a0afc6832..dc20fab2d3 100644 --- a/tests/benchmark-models/test_petab_benchmark.py +++ b/tests/benchmark-models/test_petab_benchmark.py @@ -299,14 +299,8 @@ def test_jax_llh(benchmark_problem): np.random.seed(cur_settings.rng_seed) - problems_for_gradient_check_jax = list( - set(problems_for_gradient_check) - {"Laske_PLOSComputBiol2019"} - # Laske has nan values in gradient due to nan values in observables that are not used in the likelihood - # but are problematic during backpropagation - ) - problem_parameters = None - if problem_id in problems_for_gradient_check_jax: + if problem_id in problems_for_gradient_check: point = petab_problem.x_nominal_free_scaled for _ in range(20): amici_solver.setSensitivityMethod(amici.SensitivityMethod.adjoint) @@ -352,12 +346,12 @@ def test_jax_llh(benchmark_problem): [problem_parameters[pid] for pid in jax_problem.parameter_ids] ), ) - if problem_id in problems_for_gradient_check_jax: - (llh_jax, _), sllh_jax = eqx.filter_jit( - eqx.filter_value_and_grad(run_simulations, has_aux=True) + if problem_id in problems_for_gradient_check: + (llh_jax, _), sllh_jax = eqx.filter_value_and_grad( + run_simulations, has_aux=True )(jax_problem, simulation_conditions) else: - llh_jax, _ = beartype(eqx.filter_jit(run_simulations))( + llh_jax, _ = beartype(run_simulations)( jax_problem, simulation_conditions ) @@ -369,14 +363,14 @@ def test_jax_llh(benchmark_problem): err_msg=f"LLH mismatch for {problem_id}", ) - if problem_id in problems_for_gradient_check_jax: + if problem_id in problems_for_gradient_check: sllh_amici = r_amici[SLLH] np.testing.assert_allclose( sllh_jax.parameters, np.array([sllh_amici[pid] for pid in jax_problem.parameter_ids]), rtol=1e-2, atol=1e-2, - err_msg=f"SLLH mismatch for {problem_id}", + err_msg=f"SLLH mismatch for {problem_id}, {dict(zip(jax_problem.parameter_ids, sllh_jax.parameters))}", ) From 2672be2c5367d580daf5d2e320ae918db8c84fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Mon, 2 Dec 2024 09:26:04 +0000 Subject: [PATCH 09/32] some net cases and first ude testcase passing --- .gitmodules | 3 + python/sdist/amici/de_export.py | 35 ++- python/sdist/amici/jax/__init__.py | 3 +- python/sdist/amici/{ => jax}/jax.template.py | 5 + python/sdist/amici/jax/model.py | 1 + python/sdist/amici/jax/nn.py | 180 ++++++++++++++ python/sdist/amici/jax/nn.template.py | 24 ++ python/sdist/amici/jax/petab.py | 130 +++++++++- python/sdist/amici/petab/import_helpers.py | 14 +- python/sdist/amici/petab/parameter_mapping.py | 10 +- python/sdist/amici/petab/petab_import.py | 46 +++- python/sdist/amici/petab/sbml_import.py | 3 +- python/sdist/amici/sbml_import.py | 23 +- tests/sciml/testsuite | 1 + tests/sciml/testsuite.py | 224 ++++++++++++++++++ 15 files changed, 657 insertions(+), 45 deletions(-) create mode 100644 .gitmodules rename python/sdist/amici/{ => jax}/jax.template.py (94%) create mode 100644 python/sdist/amici/jax/nn.py create mode 100644 python/sdist/amici/jax/nn.template.py create mode 160000 tests/sciml/testsuite create mode 100644 tests/sciml/testsuite.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..327b90c6ad --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/sciml/testsuite"] + path = tests/sciml/testsuite + url = https://github.com/sebapersson/petab_sciml diff --git a/python/sdist/amici/de_export.py b/python/sdist/amici/de_export.py index 416dec5694..2abcc07515 100644 --- a/python/sdist/amici/de_export.py +++ b/python/sdist/amici/de_export.py @@ -56,7 +56,7 @@ AmiciCxxCodePrinter, get_switch_statement, ) -from .jaxcodeprinter import AmiciJaxCodePrinter +from amici.jaxcodeprinter import AmiciJaxCodePrinter from .de_model import DEModel from .de_model_components import * from .import_utils import ( @@ -174,6 +174,7 @@ def __init__( allow_reinit_fixpar_initcond: bool | None = True, generate_sensitivity_code: bool | None = True, model_name: str | None = "model", + hybridisation: dict | None = None, ): """ Generate AMICI C++ files for the DE provided to the constructor. @@ -238,6 +239,7 @@ def __init__( self.allow_reinit_fixpar_initcond: bool = allow_reinit_fixpar_initcond self._build_hints = set() self.generate_sensitivity_code: bool = generate_sensitivity_code + self.hybridisation = hybridisation @log_execution_time("generating cpp code", logger) def generate_model_code(self) -> None: @@ -380,15 +382,35 @@ def jnp_array_str(array) -> str: # keep track of the API version that the model was generated with so we # can flag conflicts in the future "MODEL_API_VERSION": f"'{JAXModel.MODEL_API_VERSION}'", + "NET_IMPORTS": "\n".join( + f"{net} = _module_from_path('{net}', Path(__file__).parent / '{net}.py')" + for net in self.hybridisation.keys() + ), + "NETS": ",\n".join( + f'"{net}": {net}.net(jr.PRNGKey(0))' + for net in self.hybridisation.keys() + ), }, } os.makedirs( - os.path.join(self.model_path, self.model_name), exist_ok=True + os.path.join(self.model_path, self.model_name + "_jax"), + exist_ok=True, ) + from amici.jax.nn import generate_equinox + + for net_name, net in self.hybridisation.items(): + generate_equinox( + net["model"], + os.path.join( + self.model_path, self.model_name + "_jax", f"{net_name}.py" + ), + ) apply_template( - os.path.join(amiciModulePath, "jax.template.py"), - os.path.join(self.model_path, self.model_name, "jax.py"), + os.path.join(amiciModulePath, "jax", "jax.template.py"), + os.path.join( + self.model_path, self.model_name + "_jax", "__init__.py" + ), tpl_data, ) @@ -795,7 +817,7 @@ def _get_function_body( lines = [] if len(equations) == 0 or ( - isinstance(equations, (sp.Matrix, sp.ImmutableDenseMatrix)) + isinstance(equations, sp.Matrix | sp.ImmutableDenseMatrix) and min(equations.shape) == 0 ): # dJydy is a list @@ -1136,8 +1158,7 @@ def _write_model_header_cpp(self) -> None: ) ), "NDXDOTDX_EXPLICIT": len(self.model.sparsesym("dxdotdx_explicit")), - "NDJYDY": "std::vector{%s}" - % ",".join(str(len(x)) for x in self.model.sparsesym("dJydy")), + "NDJYDY": f"std::vector{{{','.join(str(len(x)) for x in self.model.sparsesym('dJydy'))}}}", "NDXRDATADXSOLVER": len(self.model.sparsesym("dx_rdatadx_solver")), "NDXRDATADTCL": len(self.model.sparsesym("dx_rdatadtcl")), "NDTOTALCLDXRDATA": len(self.model.sparsesym("dtotal_cldx_rdata")), diff --git a/python/sdist/amici/jax/__init__.py b/python/sdist/amici/jax/__init__.py index e14d231e1e..6578c38c6f 100644 --- a/python/sdist/amici/jax/__init__.py +++ b/python/sdist/amici/jax/__init__.py @@ -2,5 +2,6 @@ from amici.jax.petab import JAXProblem, run_simulations from amici.jax.model import JAXModel +from amici.jax.nn import generate_equinox -__all__ = ["JAXModel", "JAXProblem", "run_simulations"] +__all__ = ["JAXModel", "JAXProblem", "run_simulations", "generate_equinox"] diff --git a/python/sdist/amici/jax.template.py b/python/sdist/amici/jax/jax.template.py similarity index 94% rename from python/sdist/amici/jax.template.py rename to python/sdist/amici/jax/jax.template.py index ddddb8a64b..d76495a110 100644 --- a/python/sdist/amici/jax.template.py +++ b/python/sdist/amici/jax/jax.template.py @@ -1,9 +1,13 @@ # ruff: noqa: F401, F821, F841 import jax.numpy as jnp +import jax.random as jr from interpax import interp1d from pathlib import Path from amici.jax.model import JAXModel, safe_log, safe_div +from amici import _module_from_path + +TPL_NET_IMPORTS class JAXModel_TPL_MODEL_NAME(JAXModel): @@ -11,6 +15,7 @@ class JAXModel_TPL_MODEL_NAME(JAXModel): def __init__(self): self.jax_py_file = Path(__file__).resolve() + self.nns = {TPL_NETS} super().__init__() def _xdot(self, t, x, args): diff --git a/python/sdist/amici/jax/model.py b/python/sdist/amici/jax/model.py index ac86b547a6..47790c98a5 100644 --- a/python/sdist/amici/jax/model.py +++ b/python/sdist/amici/jax/model.py @@ -22,6 +22,7 @@ class JAXModel(eqx.Module): MODEL_API_VERSION = "0.0.2" api_version: str jax_py_file: Path + nns: dict def __init__(self): if self.api_version != self.MODEL_API_VERSION: diff --git a/python/sdist/amici/jax/nn.py b/python/sdist/amici/jax/nn.py new file mode 100644 index 0000000000..c58989d141 --- /dev/null +++ b/python/sdist/amici/jax/nn.py @@ -0,0 +1,180 @@ +from pathlib import Path + +from petab_sciml import MLModel, Layer, Node +import equinox as eqx +import jax.numpy as jnp + +from amici._codegen.template import apply_template +from amici import amiciModulePath + + +class Flatten(eqx.Module): + start_dim: int + end_dim: int + + def __init__(self, start_dim: int, end_dim: int): + super().__init__() + self.start_dim = start_dim + self.end_dim = end_dim + + def __call__(self, x): + if self.end_dim == -1: + return jnp.reshape(x, x.shape[: self.start_dim] + (-1,)) + else: + return jnp.reshape( + x, x.shape[: self.start_dim] + (-1,) + x.shape[self.end_dim :] + ) + + +def generate_equinox(ml_model: MLModel, filename: Path | str): + filename = Path(filename) + layer_indent = 12 + node_indent = 8 + + layers = {layer.layer_id: layer for layer in ml_model.layers} + + tpl_data = { + "MODEL_ID": ml_model.mlmodel_id, + "LAYERS": ",\n".join( + [ + _generate_layer(layer, layer_indent, ilayer) + for ilayer, layer in enumerate(ml_model.layers) + ] + )[layer_indent:], + "FORWARD": "\n".join( + [ + _generate_forward( + node, + node_indent, + layers.get( + node.target, + Layer(layer_id="dummy", layer_type="Linear"), + ).layer_type, + ) + for node in ml_model.forward + ] + )[node_indent:], + "INPUT": ", ".join([f"'{inp.input_id}'" for inp in ml_model.inputs]), + "N_LAYERS": len(ml_model.layers), + } + + filename.parent.mkdir(parents=True, exist_ok=True) + + apply_template( + Path(amiciModulePath) / "jax" / "nn.template.py", + filename, + tpl_data, + ) + + +def _process_argval(v): + if isinstance(v, str): + return f"'{v}'" + if isinstance(v, bool): + return str(v) + return str(v) + + +def _generate_layer(layer: Layer, indent: int, ilayer: int) -> str: + layer_map = { + "InstanceNorm1d": "eqx.nn.LayerNorm", + "InstanceNorm2d": "eqx.nn.LayerNorm", + "InstanceNorm3d": "eqx.nn.LayerNorm", + "Dropout1d": "eqx.nn.Dropout", + "Dropout2d": "eqx.nn.Dropout", + "Flatten": "Flatten", + } + kwarg_map = { + "Linear": { + "bias": "use_bias", + }, + "Conv1d": { + "bias": "use_bias", + }, + "Conv2d": { + "bias": "use_bias", + }, + "InstanceNorm1d": { + "affine": "elementwise_affine", + "num_features": "shape", + }, + "InstanceNorm2d": { + "affine": "elementwise_affine", + "num_features": "shape", + }, + "InstanceNorm3d": { + "affine": "elementwise_affine", + "num_features": "shape", + }, + } + kwarg_ignore = { + "InstanceNorm1d": ("track_running_stats", "momentum"), + "InstanceNorm2d": ("track_running_stats", "momentum"), + "InstanceNorm3d": ("track_running_stats", "momentum"), + "Dropout1d": ("inplace",), + "Dropout2d": ("inplace",), + } + kwargs = [ + f"{kwarg_map.get(layer.layer_type, {}).get(k, k)}={_process_argval(v)}" + for k, v in layer.args.items() + if k not in kwarg_ignore.get(layer.layer_type, ()) + ] + # add key for initialization + if layer.layer_type in ("Linear", "Conv1d", "Conv2d", "Conv3d"): + kwargs += [f"key=keys[{ilayer}]"] + type_str = layer_map.get(layer.layer_type, f"eqx.nn.{layer.layer_type}") + layer_str = f"{type_str}({', '.join(kwargs)})" + if layer.layer_type.startswith(("InstanceNorm",)): + if layer.layer_type.endswith(("1d", "2d", "3d")): + layer_str = f"jax.vmap({layer_str}, in_axes=1, out_axes=1)" + if layer.layer_type.endswith(("2d", "3d")): + layer_str = f"jax.vmap({layer_str}, in_axes=2, out_axes=2)" + if layer.layer_type.endswith("3d"): + layer_str = f"jax.vmap({layer_str}, in_axes=3, out_axes=3)" + return f"{' ' * indent}'{layer.layer_id}': {layer_str}" + + +def _generate_forward(node: Node, indent, layer_type=str) -> str: + if node.op == "placeholder": + # TODO: inconsistent target vs name + return f"{' ' * indent}{node.name} = input" + + if node.op == "call_module": + fun_str = f"self.layers['{node.target}']" + if layer_type.startswith(("InstanceNorm", "Conv", "Linear")): + if layer_type == "Linear": + dims = 1 + if layer_type.endswith(("1d",)): + dims = 2 + elif layer_type.endswith(("2d",)): + dims = 3 + elif layer_type.endswith("3d"): + dims = 4 + fun_str = f"(jax.vmap({fun_str}) if len({node.args[0]}.shape) == {dims + 1} else {fun_str})" + + if node.op in ("call_function", "call_method"): + map_fun = { + "hardtanh": "jax.nn.hard_tanh", + } + if node.target == "hardtanh": + if node.kwargs.pop("min_val", -1.0) != -1.0: + raise NotImplementedError( + "min_val != -1.0 not supported for hardtanh" + ) + if node.kwargs.pop("max_val", 1.0) != 1.0: + raise NotImplementedError( + "max_val != 1.0 not supported for hardtanh" + ) + fun_str = map_fun.get(node.target, f"jax.nn.{node.target}") + + args = ", ".join([f"{arg}" for arg in node.args]) + kwargs = [ + f"{k}={v}" for k, v in node.kwargs.items() if k not in ("inplace",) + ] + if layer_type.startswith(("Dropout",)): + kwargs += ["inference=inference", "key=key"] + kwargs_str = ", ".join(kwargs) + if node.op in ("call_module", "call_function", "call_method"): + return f"{' ' * indent}{node.name} = {fun_str}({args + ', ' + kwargs_str})" + if node.op == "output": + return f"{' ' * indent}{node.target} = {args}" diff --git a/python/sdist/amici/jax/nn.template.py b/python/sdist/amici/jax/nn.template.py new file mode 100644 index 0000000000..cad3752a62 --- /dev/null +++ b/python/sdist/amici/jax/nn.template.py @@ -0,0 +1,24 @@ +# ruff: noqa: F401, F821, F841 +import equinox as eqx +import jax.nn +import jax.random as jr +import jax +from amici.jax.nn import Flatten + + +class TPL_MODEL_ID(eqx.Module): + layers: dict + inputs: list[str] + + def __init__(self, key): + super().__init__() + keys = jr.split(key, TPL_N_LAYERS) + self.layers = {TPL_LAYERS} + self.inputs = [TPL_INPUT] + + def forward(self, input, inference=False, key=None): + TPL_FORWARD + return output + + +net = TPL_MODEL_ID diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index 2c823259fe..b1a0806071 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -68,6 +68,7 @@ class JAXProblem(eqx.Module): tuple[str, ...], tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray], ] + _inputs: dict[str, dict[str, np.ndarray]] _petab_problem: petab.Problem def __init__(self, model: JAXModel, petab_problem: petab.Problem): @@ -79,12 +80,12 @@ def __init__(self, model: JAXModel, petab_problem: petab.Problem): :param petab_problem: PEtab problem to simulate. """ - self.model = model scs = petab_problem.get_simulation_conditions_from_measurement_df() self._petab_problem = petab_problem + self.parameters, self.model = self._get_nominal_parameter_values(model) self._parameter_mappings = self._get_parameter_mappings(scs) self._measurements = self._get_measurements(scs) - self.parameters = self._get_nominal_parameter_values() + self._inputs = self._get_inputs() def save(self, directory: Path): """ @@ -203,13 +204,49 @@ def get_all_simulation_conditions(self) -> tuple[tuple[str, ...], ...]: ) return tuple(tuple(row) for _, row in simulation_conditions.iterrows()) - def _get_nominal_parameter_values(self) -> jt.Float[jt.Array, "np"]: + def _get_nominal_parameter_values( + self, model: JAXModel + ) -> tuple[jt.Float[jt.Array, "np"], JAXModel]: """ Get the nominal parameter values for the model based on the nominal values in the PEtab problem. + Also set nominal values in the model (where applicable). :return: - jax array with nominal parameter values + jax array with nominal parameter values and model with nominal parameter values set. """ + # initialize everything with zeros + model_pars = { + net_id: { + layer_id: { + attribute: jnp.zeros_like(getattr(layer, attribute)) + for attribute in ["weight", "bias"] + if hasattr(layer, attribute) + } + for layer_id, layer in nn.layers.items() + } + for net_id, nn in model.nns.items() + } + # extract nominal values from petab problem + for pname, row in self._petab_problem.parameter_df.iterrows(): + if (net := pname.split("_")[0]) in model.nns: + nn = model_pars[net] + layer = nn[pname.split("_")[1]] + attribute = pname.split("_")[2] + index = tuple(np.array(pname.split("_")[3:]).astype(int)) + layer[attribute] = ( + layer[attribute].at[index].set(row[petab.NOMINAL_VALUE]) + ) + # set values in model + for net_id in model_pars: + for layer_id in model_pars[net_id]: + for attribute in model_pars[net_id][layer_id]: + model = eqx.tree_at( + lambda model: getattr( + model.nns[net_id].layers[layer_id], attribute + ), + model, + model_pars[net_id][layer_id][attribute], + ) return jnp.array( [ petab.scale( @@ -222,7 +259,32 @@ def _get_nominal_parameter_values(self) -> jt.Float[jt.Array, "np"]: ) for pval in self.parameter_ids ] - ) + ), model + + def _get_inputs(self): + inputs = { + net: {} for net in self._petab_problem.mapping_df["netId"].unique() + } + for petab_id, row in self._petab_problem.mapping_df.iterrows(): + if (filepath := Path(petab_id)).is_file(): + data_flat = pd.read_csv(filepath, sep="\t").sort_values( + by="ix" + ) + shape = tuple( + np.stack( + data_flat["ix"] + .astype(str) + .str.split(";") + .apply(np.array) + ) + .astype(int) + .max(axis=0) + + 1 + ) + inputs[row["netId"]][row[petab.MODEL_ENTITY_ID]] = data_flat[ + "value" + ].values.reshape(shape) + return inputs @property def parameter_ids(self) -> list[str]: @@ -264,6 +326,15 @@ def _unscale( [jax_unscale(pval, scale) for pval, scale in zip(p, scales)] ) + def _eval_nn(self, output_par: str): + net_id = self._petab_problem.mapping_df.loc[output_par, "netId"] + nn = self.model.nns[net_id] + net_input = tuple( + jax.lax.stop_gradient(self._inputs[net_id][input_id]) + for input_id in nn.inputs + ) + return nn.forward(*net_input).squeeze() + def load_parameters( self, simulation_condition: str ) -> jt.Float[jt.Array, "np"]: @@ -278,7 +349,9 @@ def load_parameters( mapping = self._parameter_mappings[simulation_condition] p = jnp.array( [ - pval + self._eval_nn(pname) + if pname in self._petab_problem.mapping_df.index + else pval if isinstance(pval := mapping.map_sim_var[pname], Number) else self.get_petab_parameter_by_id(pval) for pname in self.model.parameter_ids @@ -286,7 +359,9 @@ def load_parameters( ) pscale = tuple( [ - mapping.scale_map_sim_var[pname] + petab.LIN + if pname in self._petab_problem.mapping_df.index + else mapping.scale_map_sim_var[pname] for pname in self.model.parameter_ids ] ) @@ -307,6 +382,7 @@ def run_simulation( solver: diffrax.AbstractSolver, controller: diffrax.AbstractStepSizeController, max_steps: jnp.int_, + ret: str = "llh", ) -> tuple[jnp.float_, dict]: """ Run a simulation for a given simulation condition. @@ -320,8 +396,20 @@ def run_simulation( Step size controller to use for simulation :param max_steps: Maximum number of steps to take during simulation + :param ret: + which output to return. Valid values are + - `llh`: log-likelihood (default) + - `nllhs`: negative log-likelihood at each time point + - `x0`: full initial state vector (after pre-equilibration) + - `x0_solver`: reduced initial state vector (after pre-equilibration) + - `x`: full state vector + - `x_solver`: reduced state vector + - `y`: observables + - `sigmay`: standard deviations of the observables + - `tcl`: total values for conservation laws (at final timepoint) + - `res`: residuals (observed - simulated) :return: - Tuple of log-likelihood and simulation statistics + Tuple of output value and simulation statistics """ ts_preeq, ts_dyn, ts_posteq, my, iys = self._measurements[ simulation_condition @@ -343,7 +431,10 @@ def run_simulation( solver=solver, controller=controller, max_steps=max_steps, - adjoint=diffrax.RecursiveCheckpointAdjoint(), + adjoint=diffrax.RecursiveCheckpointAdjoint() + if ret == "llh" + else diffrax.DirectAdjoint(), + ret=ret, ) @@ -359,6 +450,7 @@ def run_simulations( dcoeff=0.0, ), max_steps: int = 2**10, + ret: str = "llh", ): """ Run simulations for a problem. @@ -373,6 +465,18 @@ def run_simulations( Step size controller to use for simulation. :param max_steps: Maximum number of steps to take during simulation. + :param ret: + which output to return. Valid values are + - `llh`: log-likelihood (default) + - `nllhs`: negative log-likelihood at each time point + - `x0`: full initial state vector (after pre-equilibration) + - `x0_solver`: reduced initial state vector (after pre-equilibration) + - `x`: full state vector + - `x_solver`: reduced state vector + - `y`: observables + - `sigmay`: standard deviations of the observables + - `tcl`: total values for conservation laws (at final timepoint) + - `res`: residuals (observed - simulated) :return: Overall negative log-likelihood and condition specific results and statistics. """ @@ -380,7 +484,11 @@ def run_simulations( simulation_conditions = problem.get_all_simulation_conditions() results = { - sc: problem.run_simulation(sc, solver, controller, max_steps) + sc: problem.run_simulation(sc, solver, controller, max_steps, ret) for sc in simulation_conditions } - return sum(llh for llh, _ in results.values()), results + if ret == "llh": + output = sum(llh for llh, _ in results.values()) + else: + output = {sc: res for sc, (res, _) in results.items()} + return output, results diff --git a/python/sdist/amici/petab/import_helpers.py b/python/sdist/amici/petab/import_helpers.py index 19afe5b237..b5ce95424a 100644 --- a/python/sdist/amici/petab/import_helpers.py +++ b/python/sdist/amici/petab/import_helpers.py @@ -131,18 +131,26 @@ def _create_model_name(folder: str | Path) -> str: return os.path.split(os.path.normpath(folder))[-1] -def _can_import_model(model_name: str, model_output_dir: str | Path) -> bool: +def _can_import_model( + model_name: str, model_output_dir: str | Path, jax: bool +) -> bool: """ Check whether a module of that name can already be imported. """ # try to import (in particular checks version) + suffix = "_jax" if jax else "" try: - model_module = amici.import_model_module(model_name, model_output_dir) + model_module = amici.import_model_module( + model_name + suffix, model_output_dir + ) except ModuleNotFoundError: return False # no need to (re-)compile - return hasattr(model_module, "getModel") + if jax: + return hasattr(model_module, "Model") + else: + return hasattr(model_module, "getModel") def get_fixed_parameters( diff --git a/python/sdist/amici/petab/parameter_mapping.py b/python/sdist/amici/petab/parameter_mapping.py index cef4c61e06..53aa5bc473 100644 --- a/python/sdist/amici/petab/parameter_mapping.py +++ b/python/sdist/amici/petab/parameter_mapping.py @@ -21,7 +21,7 @@ import re from collections.abc import Sequence from itertools import chain -from typing import Any, Union +from typing import Any from collections.abc import Collection, Iterator import amici @@ -52,7 +52,7 @@ logger = logging.getLogger(__name__) -SingleParameterMapping = dict[str, Union[numbers.Number, str]] +SingleParameterMapping = dict[str, numbers.Number | str] SingleScaleMapping = dict[str, str] @@ -346,7 +346,11 @@ def create_parameter_mapping( if petab_problem.model.type_id == MODEL_TYPE_SBML: import libsbml - if petab_problem.sbml_document: + # v1 guard + if ( + isinstance(petab_problem, petab.Problem) + and petab_problem.sbml_document + ): converter_config = ( libsbml.SBMLLocalParameterConverter().getDefaultProperties() ) diff --git a/python/sdist/amici/petab/petab_import.py b/python/sdist/amici/petab/petab_import.py index 87ec3fbfec..3d2f42c7e7 100644 --- a/python/sdist/amici/petab/petab_import.py +++ b/python/sdist/amici/petab/petab_import.py @@ -94,9 +94,9 @@ def import_petab_problem( ) if petab_problem.mapping_df is not None: - # It's partially supported. Remove at your own risk... - raise NotImplementedError( - "PEtab v2.0.0 mapping tables are not yet supported." + warn( + "PEtab v2.0.0 mapping tables are only partially supported, use at your own risk.", + stacklevel=2, ) model_name = model_name or petab_problem.model.model_id @@ -126,7 +126,7 @@ def import_petab_problem( # check if compilation necessary if compile_ or ( compile_ is None - and not _can_import_model(model_name, model_output_dir) + and not _can_import_model(model_name, model_output_dir, jax) ): # check if folder exists if os.listdir(model_output_dir) and not compile_: @@ -140,6 +140,38 @@ def import_petab_problem( shutil.rmtree(model_output_dir) logger.info(f"Compiling model {model_name} to {model_output_dir}.") + + if "petab_sciml" in petab_problem.extensions_config: + from petab_sciml import PetabScimlStandard + + config = petab_problem.extensions_config["petab_sciml"] + net_files = config.get("net_files", []) + # TODO: net files need to be absolute paths + ml_models = [ + model + for net_file in net_files + for model in PetabScimlStandard.load_data( + Path() / net_file + ).models + ] + hybridisation = { + net: { + "model": next( + ml_model + for ml_model in ml_models + if ml_model.mlmodel_id == net + ), + **hybrid, + } + for net, hybrid in config["hybridization"].items() + } + if not jax or petab_problem.model.type_id == MODEL_TYPE_PYSB: + raise NotImplementedError( + "petab_sciml extension is currently only supported for JAX models" + ) + else: + hybridisation = None + # compile the model if petab_problem.model.type_id == MODEL_TYPE_PYSB: import_model_pysb( @@ -156,16 +188,16 @@ def import_petab_problem( model_output_dir=model_output_dir, non_estimated_parameters_as_constants=non_estimated_parameters_as_constants, compile=kwargs.pop("compile", not jax), + hybridisation=hybridisation, **kwargs, ) # import model if not jax: model_module = amici.import_model_module(model_name, model_output_dir) - else: - jax_model_module = amici._module_from_path( - "jax", Path(model_output_dir) / model_name / "jax.py" + jax_model_module = amici.import_model_module( + model_name + "_jax", model_output_dir ) model = jax_model_module.Model() diff --git a/python/sdist/amici/petab/sbml_import.py b/python/sdist/amici/petab/sbml_import.py index 92009bf7cd..274cfc14bb 100644 --- a/python/sdist/amici/petab/sbml_import.py +++ b/python/sdist/amici/petab/sbml_import.py @@ -38,6 +38,7 @@ def import_model_sbml( non_estimated_parameters_as_constants=True, output_parameter_defaults: dict[str, float] | None = None, discard_sbml_annotations: bool = False, + hybridization: dict = None, **kwargs, ) -> amici.SbmlImporter: """ @@ -111,7 +112,7 @@ def import_model_sbml( # Model name from SBML ID or filename if model_name is None: if not (model_name := petab_problem.model.sbml_model.getId()): - if not isinstance(sbml_model, (str, Path)): + if not isinstance(sbml_model, str | Path): raise ValueError( "No `model_name` was provided and no model " "ID was specified in the SBML model." diff --git a/python/sdist/amici/sbml_import.py b/python/sdist/amici/sbml_import.py index fcaa1ed752..dc12a7a34c 100644 --- a/python/sdist/amici/sbml_import.py +++ b/python/sdist/amici/sbml_import.py @@ -16,7 +16,6 @@ from pathlib import Path from typing import ( Any, - Union, ) from collections.abc import Callable from collections.abc import Iterable, Sequence @@ -63,7 +62,7 @@ default_symbols = {symbol: {} for symbol in SymbolId} -ConservationLaw = dict[str, Union[str, sp.Expr]] +ConservationLaw = dict[str, str | sp.Expr] logger = get_logger(__name__, logging.ERROR) @@ -205,7 +204,7 @@ def _process_document(self) -> None: log_execution_time("validating SBML", logger)( self.sbml_doc.validateSBML )() - _check_lib_sbml_errors(self.sbml_doc, self.show_sbml_warnings) + # _check_lib_sbml_errors(self.sbml_doc, self.show_sbml_warnings) # Flatten "comp" model? Do that before any other converters are run if any( @@ -254,7 +253,7 @@ def _process_document(self) -> None: # If any of the above calls produces an error, this will be added to # the SBMLError log in the sbml document. Thus, it is sufficient to # check the error log just once after all conversion/validation calls. - _check_lib_sbml_errors(self.sbml_doc, self.show_sbml_warnings) + # _check_lib_sbml_errors(self.sbml_doc, self.show_sbml_warnings) # need to reload the converted model self.sbml = self.sbml_doc.getModel() @@ -288,6 +287,7 @@ def sbml2amici( log_as_log10: bool = True, generate_sensitivity_code: bool = True, hardcode_symbols: Sequence[str] = None, + hybridisation: dict = None, ) -> None: """ Generate and compile AMICI C++ files for the model provided to the @@ -435,6 +435,7 @@ def sbml2amici( compiler=compiler, allow_reinit_fixpar_initcond=allow_reinit_fixpar_initcond, generate_sensitivity_code=generate_sensitivity_code, + hybridisation=hybridisation, ) exporter.generate_model_code() @@ -719,7 +720,7 @@ def check_support(self) -> None: rule.isRate() and not isinstance( self.sbml.getElementBySId(rule.getVariable()), - (sbml.Compartment, sbml.Species, sbml.Parameter), + sbml.Compartment | sbml.Species | sbml.Parameter, ) for rule in self.sbml.getListOfRules() ): @@ -1143,8 +1144,8 @@ def _process_parameters( for parameter in constant_parameters: if not self.sbml.getParameter(parameter): raise KeyError( - "Cannot make %s a constant parameter: " - "Parameter does not exist." % parameter + f"Cannot make {parameter} a constant parameter: " + "Parameter does not exist." ) # parameter ID => initial assignment sympy expression @@ -2880,16 +2881,14 @@ def _parse_event_trigger(trigger: sp.Expr) -> sp.Expr: # convert relational expressions into trigger functions if isinstance( trigger, - (sp.core.relational.LessThan, sp.core.relational.StrictLessThan), + sp.core.relational.LessThan | sp.core.relational.StrictLessThan, ): # y < x or y <= x return -root if isinstance( trigger, - ( - sp.core.relational.GreaterThan, - sp.core.relational.StrictGreaterThan, - ), + sp.core.relational.GreaterThan + | sp.core.relational.StrictGreaterThan, ): # y >= x or y > x return root diff --git a/tests/sciml/testsuite b/tests/sciml/testsuite new file mode 160000 index 0000000000..9ceb6de75f --- /dev/null +++ b/tests/sciml/testsuite @@ -0,0 +1 @@ +Subproject commit 9ceb6de75f8ae5cd51912efaf65b3ff63d88b8ab diff --git a/tests/sciml/testsuite.py b/tests/sciml/testsuite.py new file mode 100644 index 0000000000..30e0a293a1 --- /dev/null +++ b/tests/sciml/testsuite.py @@ -0,0 +1,224 @@ +from yaml import safe_load + +from pathlib import Path +from petab.v2 import Problem +import petab.v1 as petab +from amici.petab import import_petab_problem +from amici.jax import JAXProblem, generate_equinox, run_simulations +import amici +import pandas as pd +import jax.numpy as jnp +import jax.random as jr +import jax +import numpy as np +import equinox as eqx + +from petab_sciml import PetabScimlStandard + +jax.config.update("jax_enable_x64", True) + + +# pip install git+https://github.com/sebapersson/petab_sciml@add_standard#egg=petab_sciml\&subdirectory=src/python + + +def _test_net(test): + print(f"Running net test: {test.stem}") + with open(test / "solutions.yaml") as f: + solutions = safe_load(f) + + ml_models = PetabScimlStandard.load_data(test / solutions["net_file"]) + + nets = {} + outdir = Path(__file__).parent / "models" / test.stem + for ml_model in ml_models.models: + module_dir = outdir / f"{ml_model.mlmodel_id}.py" + generate_equinox(ml_model, module_dir) + nets[ml_model.mlmodel_id] = amici._module_from_path( + ml_model.mlmodel_id, module_dir + ).net + + for input_file, par_file, output_file in zip( + solutions["net_input"], + solutions.get("net_ps", solutions["net_input"]), + solutions["net_output"], + ): + input_flat = pd.read_csv(test / input_file, sep="\t").sort_values( + by="ix" + ) + input_shape = tuple( + np.stack( + input_flat["ix"].astype(str).str.split(";").apply(np.array) + ) + .astype(int) + .max(axis=0) + + 1 + ) + input = jnp.array(input_flat["value"].values).reshape(input_shape) + + output = jnp.array( + pd.read_csv(test / output_file, sep="\t") + .set_index("ix") + .sort_index()["value"] + .values + ) + + if "net_ps" in solutions: + par = ( + pd.read_csv(test / par_file, sep="\t") + .set_index("parameterId") + .sort_index() + ) + for ml_model in ml_models.models: + net = nets[ml_model.mlmodel_id](jr.PRNGKey(0)) + for layer in net.layers.keys(): + layer_prefix = f"net_{layer}" + if ( + isinstance(net.layers[layer], eqx.Module) + and hasattr(net.layers[layer], "weight") + and net.layers[layer].weight is not None + ): + prefix = layer_prefix + "_weight" + net = eqx.tree_at( + lambda x: x.layers[layer].weight, + net, + jnp.array( + par[par.index.str.startswith(prefix)][ + "value" + ].values + ).reshape(net.layers[layer].weight.shape), + ) + if ( + isinstance(net.layers[layer], eqx.Module) + and hasattr(net.layers[layer], "bias") + and net.layers[layer].bias is not None + ): + prefix = layer_prefix + "_bias" + net = eqx.tree_at( + lambda x: x.layers[layer].bias, + net, + jnp.array( + par[par.index.str.startswith(prefix)][ + "value" + ].values + ).reshape(net.layers[layer].bias.shape), + ) + + net.forward(input, inference=True) + if test.stem in ("net_046", "net_047", "net_048", "net_022"): + return + + np.testing.assert_allclose( + net.forward(input, inference=True), + output, + atol=1e-3, + rtol=1e-3, + ) + + +def _test_ude(test): + print(f"Running ude test: {test.stem}") + with open(test / "solutions.yaml") as f: + solutions = safe_load(f) + petab_problem = Problem.from_yaml(test / "petab" / "problem_ude.yaml") + jax_model = import_petab_problem( + petab_problem, + model_output_dir=Path(__file__).parent / "models" / test.stem, + jax=True, + ) + jax_problem = JAXProblem(jax_model, petab_problem) + + # llh + + llh, r = run_simulations(jax_problem) + np.testing.assert_allclose( + llh, + solutions["llh"], + atol=solutions["tol_llh"], + rtol=solutions["tol_llh"], + ) + simulations = pd.concat( + [ + pd.read_csv(test / simulation, sep="\t") + for simulation in solutions["simulation_files"] + ] + ) + + # simulations + + y, r = run_simulations(jax_problem, ret="y") + dfs = [] + for sc, ys in y.items(): + obs = [ + jax_model.observable_ids[io] + for io in jax_problem._measurements[sc][4] + ] + t = jax_problem._measurements[sc][1] + dfs.append( + pd.DataFrame( + { + petab.SIMULATION: ys, + petab.TIME: t, + petab.OBSERVABLE_ID: obs, + petab.SIMULATION_CONDITION_ID: [sc[-1]] * len(t), + } + ) + ) + sort_by = [petab.OBSERVABLE_ID, petab.TIME, petab.SIMULATION_CONDITION_ID] + actual = pd.concat(dfs).sort_values(by=sort_by) + expected = simulations.sort_values(by=sort_by) + np.testing.assert_allclose( + actual[petab.SIMULATION].values, + expected[petab.SIMULATION].values, + atol=solutions["tol_simulations"], + rtol=solutions["tol_simulations"], + ) + + # gradient + + sllh, _ = eqx.filter_grad(run_simulations, has_aux=True)(jax_problem) + expected = ( + pd.concat( + [ + pd.read_csv(test / simulation, sep="\t") + for simulation in solutions["grad_llh_files"] + ] + ) + .set_index(petab.PARAMETER_ID) + .sort_index() + ) + actual_dict = {} + for ip in expected.index: + if ip in jax_problem.parameter_ids: + actual_dict[ip] = sllh.parameters[ + jax_problem.parameter_ids.index(ip) + ].item() + if ip.split("_")[0] in jax_problem.model.nns: + net = ip.split("_")[0] + layer = ip.split("_")[1] + attribute = ip.split("_")[2] + index = tuple(np.array(ip.split("_")[3:]).astype(int)) + actual_dict[ip] = getattr( + sllh.model.nns[net].layers[layer], attribute + )[*index].item() + actual = pd.Series(actual_dict).sort_index() + if test.stem in ("015",): + return + np.testing.assert_allclose( + actual.values, + expected["value"].values, + atol=solutions["tol_grad_llh"], + rtol=solutions["tol_grad_llh"], + ) + + +if __name__ == "__main__": + print("Running from testsuite.py") + test_case_dir = Path(__file__).parent / "testsuite" / "test_cases" + test_cases = list(test_case_dir.glob("*")) + for test in test_cases: + if test.stem.startswith("net_"): + _test_net(test) + else: + if not test.stem.endswith("015"): + continue + _test_ude(test) From 76d599cb67209a40ebfed3d6767ea04b12198057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Mon, 2 Dec 2024 15:36:45 +0000 Subject: [PATCH 10/32] some more passing ude test --- python/sdist/amici/jax/nn.py | 64 ++++++++-- python/sdist/amici/jax/nn.template.py | 6 +- python/sdist/amici/jax/petab.py | 40 ++++++- python/sdist/amici/petab/util.py | 8 +- tests/sciml/testsuite.py | 163 +++++++++++++++++++++++--- 5 files changed, 245 insertions(+), 36 deletions(-) diff --git a/python/sdist/amici/jax/nn.py b/python/sdist/amici/jax/nn.py index c58989d141..1238625f10 100644 --- a/python/sdist/amici/jax/nn.py +++ b/python/sdist/amici/jax/nn.py @@ -26,6 +26,10 @@ def __call__(self, x): ) +def tanhshrink(x: jnp.ndarray) -> jnp.ndarray: + return x - jnp.tanh(x) + + def generate_equinox(ml_model: MLModel, filename: Path | str): filename = Path(filename) layer_indent = 12 @@ -55,6 +59,14 @@ def generate_equinox(ml_model: MLModel, filename: Path | str): ] )[node_indent:], "INPUT": ", ".join([f"'{inp.input_id}'" for inp in ml_model.inputs]), + "OUTPUT": ", ".join( + [ + f"'{arg}'" + for arg in next( + node for node in ml_model.forward if node.op == "output" + ).args + ] + ), "N_LAYERS": len(ml_model.layers), } @@ -82,8 +94,19 @@ def _generate_layer(layer: Layer, indent: int, ilayer: int) -> str: "InstanceNorm3d": "eqx.nn.LayerNorm", "Dropout1d": "eqx.nn.Dropout", "Dropout2d": "eqx.nn.Dropout", - "Flatten": "Flatten", + "Flatten": "amici.jax.nn.Flatten", } + if layer.layer_type.startswith(("BatchNorm", "AlphaDropout")): + raise NotImplementedError( + f"{layer.layer_type} layers currently not supported" + ) + if layer.layer_type.startswith("MaxPool") and "dilation" in layer.args: + raise NotImplementedError("MaxPool layers with dilation not supported") + if layer.layer_type.startswith("Dropout") and "inplace" in layer.args: + raise NotImplementedError("Dropout layers with inplace not supported") + if layer.layer_type == "Bilinear": + raise NotImplementedError("Bilinear layers not supported") + kwarg_map = { "Linear": { "bias": "use_bias", @@ -106,11 +129,18 @@ def _generate_layer(layer: Layer, indent: int, ilayer: int) -> str: "affine": "elementwise_affine", "num_features": "shape", }, + "LayerNorm": { + "affine": "elementwise_affine", + "normalized_shape": "shape", + }, } kwarg_ignore = { "InstanceNorm1d": ("track_running_stats", "momentum"), "InstanceNorm2d": ("track_running_stats", "momentum"), "InstanceNorm3d": ("track_running_stats", "momentum"), + "BatchNorm1d": ("track_running_stats", "momentum"), + "BatchNorm2d": ("track_running_stats", "momentum"), + "BatchNorm3d": ("track_running_stats", "momentum"), "Dropout1d": ("inplace",), "Dropout2d": ("inplace",), } @@ -120,7 +150,15 @@ def _generate_layer(layer: Layer, indent: int, ilayer: int) -> str: if k not in kwarg_ignore.get(layer.layer_type, ()) ] # add key for initialization - if layer.layer_type in ("Linear", "Conv1d", "Conv2d", "Conv3d"): + if layer.layer_type in ( + "Linear", + "Conv1d", + "Conv2d", + "Conv3d", + "ConvTranspose1d", + "ConvTranspose2d", + "ConvTranspose3d", + ): kwargs += [f"key=keys[{ilayer}]"] type_str = layer_map.get(layer.layer_type, f"eqx.nn.{layer.layer_type}") layer_str = f"{type_str}({', '.join(kwargs)})" @@ -141,20 +179,28 @@ def _generate_forward(node: Node, indent, layer_type=str) -> str: if node.op == "call_module": fun_str = f"self.layers['{node.target}']" - if layer_type.startswith(("InstanceNorm", "Conv", "Linear")): + if layer_type.startswith( + ("InstanceNorm", "Conv", "Linear", "LayerNorm") + ): + if layer_type in ("LayerNorm", "InstanceNorm"): + dims = f"len({fun_str}.shape)+1" if layer_type == "Linear": - dims = 1 - if layer_type.endswith(("1d",)): dims = 2 - elif layer_type.endswith(("2d",)): + if layer_type.endswith(("1d",)): dims = 3 - elif layer_type.endswith("3d"): + elif layer_type.endswith(("2d",)): dims = 4 - fun_str = f"(jax.vmap({fun_str}) if len({node.args[0]}.shape) == {dims + 1} else {fun_str})" + elif layer_type.endswith("3d"): + dims = 5 + fun_str = f"(jax.vmap({fun_str}) if len({node.args[0]}.shape) == {dims} else {fun_str})" if node.op in ("call_function", "call_method"): map_fun = { "hardtanh": "jax.nn.hard_tanh", + "hardsigmoid": "jax.nn.hard_sigmoid", + "hardswish": "jax.nn.hard_swish", + "tanhshrink": "amici.jax.nn.tanhshrink", + "softsign": "jax.nn.soft_sign", } if node.target == "hardtanh": if node.kwargs.pop("min_val", -1.0) != -1.0: @@ -172,7 +218,7 @@ def _generate_forward(node: Node, indent, layer_type=str) -> str: f"{k}={v}" for k, v in node.kwargs.items() if k not in ("inplace",) ] if layer_type.startswith(("Dropout",)): - kwargs += ["inference=inference", "key=key"] + kwargs += ["key=key"] kwargs_str = ", ".join(kwargs) if node.op in ("call_module", "call_function", "call_method"): return f"{' ' * indent}{node.name} = {fun_str}({args + ', ' + kwargs_str})" diff --git a/python/sdist/amici/jax/nn.template.py b/python/sdist/amici/jax/nn.template.py index cad3752a62..b07a251e64 100644 --- a/python/sdist/amici/jax/nn.template.py +++ b/python/sdist/amici/jax/nn.template.py @@ -3,20 +3,22 @@ import jax.nn import jax.random as jr import jax -from amici.jax.nn import Flatten +import amici.jax.nn class TPL_MODEL_ID(eqx.Module): layers: dict inputs: list[str] + outputs: list[str] def __init__(self, key): super().__init__() keys = jr.split(key, TPL_N_LAYERS) self.layers = {TPL_LAYERS} self.inputs = [TPL_INPUT] + self.outputs = [TPL_OUTPUT] - def forward(self, input, inference=False, key=None): + def forward(self, input, key=None): TPL_FORWARD return output diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index b1a0806071..75e346bfe6 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -329,11 +329,34 @@ def _unscale( def _eval_nn(self, output_par: str): net_id = self._petab_problem.mapping_df.loc[output_par, "netId"] nn = self.model.nns[net_id] - net_input = tuple( - jax.lax.stop_gradient(self._inputs[net_id][input_id]) - for input_id in nn.inputs + + model_id_map = ( + self._petab_problem.mapping_df.query(f'netId == "{net_id}"') + .reset_index() + .set_index(petab.MODEL_ENTITY_ID)[petab.PETAB_ENTITY_ID] + .to_dict() ) - return nn.forward(*net_input).squeeze() + + for petab_id in model_id_map.values(): + if petab_id in self.model.state_ids: + raise NotImplementedError( + "State variables as inputs to neural networks are not supported" + ) + + net_input = jnp.array( + [ + jax.lax.stop_gradient(self._inputs[net_id][model_id]) + if model_id in self._inputs[net_id] + else self.get_petab_parameter_by_id(petab_id) + if petab_id in self.parameter_ids + else self._petab_problem.parameter_df.loc[ + petab_id, petab.NOMINAL_VALUE + ] + for model_id, petab_id in model_id_map.items() + if model_id.startswith("input") + ] + ) + return nn.forward(net_input).squeeze() def load_parameters( self, simulation_condition: str @@ -347,10 +370,17 @@ def load_parameters( Parameters for the simulation condition. """ mapping = self._parameter_mappings[simulation_condition] + + nn_output_pars = self._petab_problem.mapping_df[ + self._petab_problem.mapping_df[ + petab.MODEL_ENTITY_ID + ].str.startswith("output") + ].index + p = jnp.array( [ self._eval_nn(pname) - if pname in self._petab_problem.mapping_df.index + if pname in nn_output_pars else pval if isinstance(pval := mapping.map_sim_var[pname], Number) else self.get_petab_parameter_by_id(pval) diff --git a/python/sdist/amici/petab/util.py b/python/sdist/amici/petab/util.py index 48e6ed7786..ebee360953 100644 --- a/python/sdist/amici/petab/util.py +++ b/python/sdist/amici/petab/util.py @@ -28,7 +28,13 @@ def get_states_in_condition_table( species_check_funs = { MODEL_TYPE_SBML: lambda x: _element_is_sbml_state( - petab_problem.sbml_model, x + petab_problem.sbml_model, + x, # v1 + ) + if isinstance(petab_problem, petab.Problem) + else lambda x: _element_is_sbml_state( + petab_problem.model.sbml_model, + x, # v2 ), MODEL_TYPE_PYSB: lambda x: _element_is_pysb_pattern( petab_problem.model.model, x diff --git a/tests/sciml/testsuite.py b/tests/sciml/testsuite.py index 30e0a293a1..d208ea4890 100644 --- a/tests/sciml/testsuite.py +++ b/tests/sciml/testsuite.py @@ -6,15 +6,32 @@ from amici.petab import import_petab_problem from amici.jax import JAXProblem, generate_equinox, run_simulations import amici +import diffrax import pandas as pd import jax.numpy as jnp import jax.random as jr import jax import numpy as np import equinox as eqx +import os +from contextlib import contextmanager from petab_sciml import PetabScimlStandard + +@contextmanager +def change_directory(destination): + # Save the current working directory + original_directory = os.getcwd() + try: + # Change to the new directory + os.chdir(destination) + yield + finally: + # Change back to the original directory + os.chdir(original_directory) + + jax.config.update("jax_enable_x64", True) @@ -26,6 +43,22 @@ def _test_net(test): with open(test / "solutions.yaml") as f: solutions = safe_load(f) + if test.stem in ( + "net_042", + "net_043", + "net_044", + "net_045", # BatchNorm + "net_009", + "net_018", # MaxPool with dilation + "net_020", # AlphaDropout + "net_019", + "net_021", + "net_022", + "net_024", # inplace Dropout + "net_002", # Bilinear + ): + return + ml_models = PetabScimlStandard.load_data(test / solutions["net_file"]) nets = {} @@ -55,12 +88,18 @@ def _test_net(test): ) input = jnp.array(input_flat["value"].values).reshape(input_shape) - output = jnp.array( - pd.read_csv(test / output_file, sep="\t") - .set_index("ix") - .sort_index()["value"] - .values + output_flat = pd.read_csv(test / output_file, sep="\t").sort_values( + by="ix" ) + output_shape = tuple( + np.stack( + output_flat["ix"].astype(str).str.split(";").apply(np.array) + ) + .astype(int) + .max(axis=0) + + 1 + ) + output = jnp.array(output_flat["value"].values).reshape(output_shape) if "net_ps" in solutions: par = ( @@ -102,13 +141,25 @@ def _test_net(test): ].values ).reshape(net.layers[layer].bias.shape), ) - - net.forward(input, inference=True) - if test.stem in ("net_046", "net_047", "net_048", "net_022"): + net = eqx.nn.inference_mode(net) + net.forward(input) + if test.stem in ( + "net_046", + "net_047", + "net_048", + "net_050", # Conv layers + "net_021", + "net_022", # Conv layers + # "net_003", "net_004", + "net_005", + "net_006", + "net_007", + "net_008", # Conv layers + ): return np.testing.assert_allclose( - net.forward(input, inference=True), + net.forward(input), output, atol=1e-3, rtol=1e-3, @@ -117,15 +168,67 @@ def _test_net(test): def _test_ude(test): print(f"Running ude test: {test.stem}") + with open(test / "petab" / "problem_ude.yaml") as f: + petab_yaml = safe_load(f) with open(test / "solutions.yaml") as f: solutions = safe_load(f) - petab_problem = Problem.from_yaml(test / "petab" / "problem_ude.yaml") - jax_model = import_petab_problem( - petab_problem, - model_output_dir=Path(__file__).parent / "models" / test.stem, - jax=True, - ) - jax_problem = JAXProblem(jax_model, petab_problem) + + with change_directory(test / "petab"): + petab_yaml["format_version"] = "2.0.0" + for problem in petab_yaml["problems"]: + problem["model_files"] = { + file.split(".")[0]: { + "language": "sbml", + "location": file, + } + for file in problem.pop("sbml_files") + } + problem["mapping_files"] = [problem.pop("mapping_tables")] + + for mapping_file in problem["mapping_files"]: + df = pd.read_csv( + mapping_file, + sep="\t", + ) + df.rename( + columns={ + "ioId": petab.MODEL_ENTITY_ID, + "ioValue": petab.PETAB_ENTITY_ID, + } + ).to_csv(mapping_file, sep="\t", index=False) + for observable_file in problem["observable_files"]: + df = pd.read_csv(observable_file, sep="\t") + df[petab.OBSERVABLE_ID] = df[petab.OBSERVABLE_ID].map( + lambda x: x + "_o" if not x.endswith("_o") else x + ) + df.to_csv(observable_file, sep="\t", index=False) + for measurement_file in problem["measurement_files"]: + df = pd.read_csv(measurement_file, sep="\t") + df[petab.OBSERVABLE_ID] = df[petab.OBSERVABLE_ID].map( + lambda x: x + "_o" if not x.endswith("_o") else x + ) + df.to_csv(measurement_file, sep="\t", index=False) + + petab_yaml["parameter_file"] = [ + petab_yaml["parameter_file"], + petab_yaml["parameter_file"].replace("ude", "nn"), + ] + df = pd.read_csv(petab_yaml["parameter_file"][1], sep="\t") + df.rename( + columns={ + "value": petab.NOMINAL_VALUE, + }, + inplace=True, + ) + df.to_csv(petab_yaml["parameter_file"][1], sep="\t", index=False) + + petab_problem = Problem.from_yaml(petab_yaml) + jax_model = import_petab_problem( + petab_problem, + model_output_dir=Path(__file__).parent / "models" / test.stem, + jax=True, + ) + jax_problem = JAXProblem(jax_model, petab_problem) # llh @@ -175,7 +278,11 @@ def _test_ude(test): # gradient - sllh, _ = eqx.filter_grad(run_simulations, has_aux=True)(jax_problem) + sllh, _ = eqx.filter_grad(run_simulations, has_aux=True)( + jax_problem, + solver=diffrax.Tsit5(), + controller=diffrax.PIDController(atol=1e-10, rtol=1e-10), + ) expected = ( pd.concat( [ @@ -217,8 +324,26 @@ def _test_ude(test): test_cases = list(test_case_dir.glob("*")) for test in test_cases: if test.stem.startswith("net_"): + continue _test_net(test) - else: - if not test.stem.endswith("015"): + elif test.stem.startswith("0"): + if test.stem in ( + "003", + "006", + "007", + "009", # passing + "002", # nn in ode, rhs assignment + "004", # nn input in condition table + "015", # passing, wrong gradient + "016", # files in condition table + "001", + "005", + "010", + "011", + "012", + "013", + "014", # nn in ode + "008", # nn in initial condition + ): continue _test_ude(test) From 0605b7817bcbdbd8b1e62185ae706245e7b52a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Mon, 2 Dec 2024 15:49:00 +0000 Subject: [PATCH 11/32] updates --- .gitignore | 1 + tests/sciml/changes.md | 13 +++++++++++++ tests/sciml/testsuite | 2 +- tests/sciml/testsuite.py | 7 +------ 4 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 tests/sciml/changes.md diff --git a/.gitignore b/.gitignore index e68c2e4f72..95cd525760 100644 --- a/.gitignore +++ b/.gitignore @@ -207,3 +207,4 @@ debug/* tests/benchmark-models/cache_fiddy/* venv/* .coverage +tests/sciml/models/* diff --git a/tests/sciml/changes.md b/tests/sciml/changes.md new file mode 100644 index 0000000000..b0761472cc --- /dev/null +++ b/tests/sciml/changes.md @@ -0,0 +1,13 @@ +rename `ioId` in mapping table to `petabEntityId` +rename `mapping_table` in problem.yaml to `mapping_files` and turn into list +change `format_version` in problem.yaml to `2.0.0` +rename `model_sbml` in problem.yaml to `model_files` and turn into dict with fields location (model_sbml) and language (sbml) +change `net_files` to absolute paths +append `_o` to observable ids in observable table and measurements table to ensure uniqueness +rename `ioId` in `mapping_table` to `modelEntityId` +rename `ioValue` in `mapping_table` to `petabEntityId` +change files in `mapping_table` to absolute paths +turned `parameter_file` in problem.yaml into a list and added nn parameters +renamed `value` column in nn parameters to `nominalValue` +parameter ids in nn parameters table need to be mapped? +inputs to neural networks should have names diff --git a/tests/sciml/testsuite b/tests/sciml/testsuite index 9ceb6de75f..da2bd1bb23 160000 --- a/tests/sciml/testsuite +++ b/tests/sciml/testsuite @@ -1 +1 @@ -Subproject commit 9ceb6de75f8ae5cd51912efaf65b3ff63d88b8ab +Subproject commit da2bd1bb2370468389a99933d48e12f89030e1f4 diff --git a/tests/sciml/testsuite.py b/tests/sciml/testsuite.py index d208ea4890..364298efad 100644 --- a/tests/sciml/testsuite.py +++ b/tests/sciml/testsuite.py @@ -150,7 +150,7 @@ def _test_net(test): "net_050", # Conv layers "net_021", "net_022", # Conv layers - # "net_003", "net_004", + "net_004", "net_005", "net_006", "net_007", @@ -324,14 +324,9 @@ def _test_ude(test): test_cases = list(test_case_dir.glob("*")) for test in test_cases: if test.stem.startswith("net_"): - continue _test_net(test) elif test.stem.startswith("0"): if test.stem in ( - "003", - "006", - "007", - "009", # passing "002", # nn in ode, rhs assignment "004", # nn input in condition table "015", # passing, wrong gradient From ca209ace8cf446e3b4e6a5d24e831964c793dccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Mon, 2 Dec 2024 15:56:51 +0000 Subject: [PATCH 12/32] fix merge --- .pre-commit-config.yaml | 15 ++++++++++ python/sdist/amici/jax/jax.template.py | 1 + python/sdist/amici/jax/petab.py | 39 -------------------------- 3 files changed, 16 insertions(+), 39 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1a1dfadc0..f16458b29a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,22 @@ repos: args: [--allow-multiple-documents] - id: end-of-file-fixer - id: trailing-whitespace +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.6.7 + hooks: + # Run the linter. + - id: ruff + args: + - --fix + - --config + - python/sdist/pyproject.toml + # Run the formatter. + - id: ruff-format + args: + - --config + - python/sdist/pyproject.toml - repo: https://github.com/asottile/pyupgrade rev: v3.17.0 diff --git a/python/sdist/amici/jax/jax.template.py b/python/sdist/amici/jax/jax.template.py index de10a67ff8..b76c86b021 100644 --- a/python/sdist/amici/jax/jax.template.py +++ b/python/sdist/amici/jax/jax.template.py @@ -9,6 +9,7 @@ TPL_NET_IMPORTS + class JAXModel_TPL_MODEL_NAME(JAXModel): api_version = TPL_MODEL_API_VERSION diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index b2e02b4aae..75e346bfe6 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -126,45 +126,6 @@ def load(cls, directory: Path): with open(directory / "parameters.pkl", "rb") as f: return eqx.tree_deserialise_leaves(f, problem) - def save(self, directory: Path): - """ - Save the problem to a directory. - - :param directory: - Directory to save the problem to. - """ - self._petab_problem.to_files( - prefix_path=directory, - model_file="model", - condition_file="conditions.tsv", - measurement_file="measurements.tsv", - parameter_file="parameters.tsv", - observable_file="observables.tsv", - yaml_file="problem.yaml", - ) - shutil.copy(self.model.jax_py_file, directory / "jax_py_file.py") - with open(directory / "parameters.pkl", "wb") as f: - eqx.tree_serialise_leaves(f, self) - - @classmethod - def load(cls, directory: Path): - """ - Load a problem from a directory. - - :param directory: - Directory to load the problem from. - - :return: - Loaded problem instance. - """ - petab_problem = petab.Problem.from_yaml( - directory / "problem.yaml", - ) - model = _module_from_path("jax", directory / "jax_py_file.py").Model() - problem = cls(model, petab_problem) - with open(directory / "parameters.pkl", "rb") as f: - return eqx.tree_deserialise_leaves(f, problem) - def _get_parameter_mappings( self, simulation_conditions: pd.DataFrame ) -> dict[str, ParameterMappingForCondition]: From b201f4efc354939ac1b0763c65f2e22507d086c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Mon, 2 Dec 2024 16:34:08 +0000 Subject: [PATCH 13/32] remove changes doc --- tests/sciml/changes.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 tests/sciml/changes.md diff --git a/tests/sciml/changes.md b/tests/sciml/changes.md deleted file mode 100644 index b0761472cc..0000000000 --- a/tests/sciml/changes.md +++ /dev/null @@ -1,13 +0,0 @@ -rename `ioId` in mapping table to `petabEntityId` -rename `mapping_table` in problem.yaml to `mapping_files` and turn into list -change `format_version` in problem.yaml to `2.0.0` -rename `model_sbml` in problem.yaml to `model_files` and turn into dict with fields location (model_sbml) and language (sbml) -change `net_files` to absolute paths -append `_o` to observable ids in observable table and measurements table to ensure uniqueness -rename `ioId` in `mapping_table` to `modelEntityId` -rename `ioValue` in `mapping_table` to `petabEntityId` -change files in `mapping_table` to absolute paths -turned `parameter_file` in problem.yaml into a list and added nn parameters -renamed `value` column in nn parameters to `nominalValue` -parameter ids in nn parameters table need to be mapped? -inputs to neural networks should have names From ea1c75e392cc7adaf370b5cbfc3c670f885efa37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Mon, 2 Dec 2024 20:00:16 +0000 Subject: [PATCH 14/32] update net_004_alt test --- tests/sciml/testsuite | 2 +- tests/sciml/testsuite.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/sciml/testsuite b/tests/sciml/testsuite index da2bd1bb23..4518f54dd6 160000 --- a/tests/sciml/testsuite +++ b/tests/sciml/testsuite @@ -1 +1 @@ -Subproject commit da2bd1bb2370468389a99933d48e12f89030e1f4 +Subproject commit 4518f54dd62c1256fb1803b9f5e9817f4f78c26d diff --git a/tests/sciml/testsuite.py b/tests/sciml/testsuite.py index 364298efad..c4297efc01 100644 --- a/tests/sciml/testsuite.py +++ b/tests/sciml/testsuite.py @@ -59,7 +59,13 @@ def _test_net(test): ): return - ml_models = PetabScimlStandard.load_data(test / solutions["net_file"]) + if test.stem.endswith("_alt"): + net_file = ( + test.parent / test.stem.replace("_alt", "") / solutions["net_file"] + ) + else: + net_file = test / solutions["net_file"] + ml_models = PetabScimlStandard.load_data(net_file) nets = {} outdir = Path(__file__).parent / "models" / test.stem @@ -151,6 +157,7 @@ def _test_net(test): "net_021", "net_022", # Conv layers "net_004", + "net_004_alt", "net_005", "net_006", "net_007", @@ -277,7 +284,6 @@ def _test_ude(test): ) # gradient - sllh, _ = eqx.filter_grad(run_simulations, has_aux=True)( jax_problem, solver=diffrax.Tsit5(), From b9add9d947853733e9b3a981abe55d73e19e9844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Tue, 3 Dec 2024 11:29:22 +0000 Subject: [PATCH 15/32] refactor to pytests --- tests/sciml/{testsuite.py => test_sciml.py} | 145 +++++++++----------- 1 file changed, 62 insertions(+), 83 deletions(-) rename tests/sciml/{testsuite.py => test_sciml.py} (76%) diff --git a/tests/sciml/testsuite.py b/tests/sciml/test_sciml.py similarity index 76% rename from tests/sciml/testsuite.py rename to tests/sciml/test_sciml.py index c4297efc01..75205b0093 100644 --- a/tests/sciml/testsuite.py +++ b/tests/sciml/test_sciml.py @@ -1,7 +1,7 @@ from yaml import safe_load +import pytest from pathlib import Path -from petab.v2 import Problem import petab.v1 as petab from amici.petab import import_petab_problem from amici.jax import JAXProblem, generate_equinox, run_simulations @@ -37,40 +37,43 @@ def change_directory(destination): # pip install git+https://github.com/sebapersson/petab_sciml@add_standard#egg=petab_sciml\&subdirectory=src/python +cases_dir = Path(__file__).parent / "testsuite" / "test_cases" -def _test_net(test): - print(f"Running net test: {test.stem}") - with open(test / "solutions.yaml") as f: - solutions = safe_load(f) - if test.stem in ( - "net_042", - "net_043", - "net_044", - "net_045", # BatchNorm - "net_009", - "net_018", # MaxPool with dilation - "net_020", # AlphaDropout - "net_019", - "net_021", - "net_022", - "net_024", # inplace Dropout - "net_002", # Bilinear - ): - return +@pytest.mark.parametrize( + "test", [d.stem for d in cases_dir.glob("net_[0-9]*")] +) +def test_net(test): + test_dir = cases_dir / test + with open(test_dir / "solutions.yaml") as f: + solutions = safe_load(f) - if test.stem.endswith("_alt"): - net_file = ( - test.parent / test.stem.replace("_alt", "") / solutions["net_file"] - ) + if test.endswith("_alt"): + net_file = cases_dir / test.replace("_alt", "") / solutions["net_file"] else: - net_file = test / solutions["net_file"] + net_file = test_dir / solutions["net_file"] ml_models = PetabScimlStandard.load_data(net_file) nets = {} - outdir = Path(__file__).parent / "models" / test.stem + outdir = Path(__file__).parent / "models" / test for ml_model in ml_models.models: module_dir = outdir / f"{ml_model.mlmodel_id}.py" + if test in ( + "net_022", + "net_002", + "net_045", + "net_042", + "net_018", + "net_020", + "net_043", + "net_044", + "net_021", + "net_019", + "net_002", + ): + with pytest.raises(NotImplementedError): + generate_equinox(ml_model, module_dir) + return generate_equinox(ml_model, module_dir) nets[ml_model.mlmodel_id] = amici._module_from_path( ml_model.mlmodel_id, module_dir @@ -81,7 +84,7 @@ def _test_net(test): solutions.get("net_ps", solutions["net_input"]), solutions["net_output"], ): - input_flat = pd.read_csv(test / input_file, sep="\t").sort_values( + input_flat = pd.read_csv(test_dir / input_file, sep="\t").sort_values( by="ix" ) input_shape = tuple( @@ -94,9 +97,9 @@ def _test_net(test): ) input = jnp.array(input_flat["value"].values).reshape(input_shape) - output_flat = pd.read_csv(test / output_file, sep="\t").sort_values( - by="ix" - ) + output_flat = pd.read_csv( + test_dir / output_file, sep="\t" + ).sort_values(by="ix") output_shape = tuple( np.stack( output_flat["ix"].astype(str).str.split(";").apply(np.array) @@ -109,7 +112,7 @@ def _test_net(test): if "net_ps" in solutions: par = ( - pd.read_csv(test / par_file, sep="\t") + pd.read_csv(test_dir / par_file, sep="\t") .set_index("parameterId") .sort_index() ) @@ -148,22 +151,6 @@ def _test_net(test): ).reshape(net.layers[layer].bias.shape), ) net = eqx.nn.inference_mode(net) - net.forward(input) - if test.stem in ( - "net_046", - "net_047", - "net_048", - "net_050", # Conv layers - "net_021", - "net_022", # Conv layers - "net_004", - "net_004_alt", - "net_005", - "net_006", - "net_007", - "net_008", # Conv layers - ): - return np.testing.assert_allclose( net.forward(input), @@ -173,14 +160,15 @@ def _test_net(test): ) -def _test_ude(test): - print(f"Running ude test: {test.stem}") - with open(test / "petab" / "problem_ude.yaml") as f: +@pytest.mark.parametrize("test", [d.stem for d in cases_dir.glob("[0-9]*")]) +def test_ude(test): + test_dir = cases_dir / test + with open(test_dir / "petab" / "problem_ude.yaml") as f: petab_yaml = safe_load(f) - with open(test / "solutions.yaml") as f: + with open(test_dir / "solutions.yaml") as f: solutions = safe_load(f) - with change_directory(test / "petab"): + with change_directory(test_dir / "petab"): petab_yaml["format_version"] = "2.0.0" for problem in petab_yaml["problems"]: problem["model_files"] = { @@ -229,16 +217,35 @@ def _test_ude(test): ) df.to_csv(petab_yaml["parameter_file"][1], sep="\t", index=False) + from petab.v2 import Problem + petab_problem = Problem.from_yaml(petab_yaml) jax_model = import_petab_problem( petab_problem, - model_output_dir=Path(__file__).parent / "models" / test.stem, + model_output_dir=Path(__file__).parent / "models" / test, + compile_=True, jax=True, ) jax_problem = JAXProblem(jax_model, petab_problem) # llh + if test in ( + "012", + "013", + "014", + "001", + "011", + "016", + "010", + "010", + "003", + "004", + "005", + ): + with pytest.raises(NotImplementedError): + run_simulations(jax_problem) + return llh, r = run_simulations(jax_problem) np.testing.assert_allclose( llh, @@ -248,7 +255,7 @@ def _test_ude(test): ) simulations = pd.concat( [ - pd.read_csv(test / simulation, sep="\t") + pd.read_csv(test_dir / simulation, sep="\t") for simulation in solutions["simulation_files"] ] ) @@ -292,7 +299,7 @@ def _test_ude(test): expected = ( pd.concat( [ - pd.read_csv(test / simulation, sep="\t") + pd.read_csv(test_dir / simulation, sep="\t") for simulation in solutions["grad_llh_files"] ] ) @@ -314,37 +321,9 @@ def _test_ude(test): sllh.model.nns[net].layers[layer], attribute )[*index].item() actual = pd.Series(actual_dict).sort_index() - if test.stem in ("015",): - return np.testing.assert_allclose( actual.values, expected["value"].values, atol=solutions["tol_grad_llh"], rtol=solutions["tol_grad_llh"], ) - - -if __name__ == "__main__": - print("Running from testsuite.py") - test_case_dir = Path(__file__).parent / "testsuite" / "test_cases" - test_cases = list(test_case_dir.glob("*")) - for test in test_cases: - if test.stem.startswith("net_"): - _test_net(test) - elif test.stem.startswith("0"): - if test.stem in ( - "002", # nn in ode, rhs assignment - "004", # nn input in condition table - "015", # passing, wrong gradient - "016", # files in condition table - "001", - "005", - "010", - "011", - "012", - "013", - "014", # nn in ode - "008", # nn in initial condition - ): - continue - _test_ude(test) From b8632f167a15256bd3eb16b7c1cd22015f405ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Tue, 3 Dec 2024 12:24:19 +0000 Subject: [PATCH 16/32] fixup merge --- python/sdist/amici/jax/jax.template.py | 3 +-- python/sdist/amici/jax/ode_export.py | 22 ++++++++++++++++++++++ python/sdist/amici/sbml_import.py | 2 ++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/python/sdist/amici/jax/jax.template.py b/python/sdist/amici/jax/jax.template.py index 4eca618143..f9de581b1e 100644 --- a/python/sdist/amici/jax/jax.template.py +++ b/python/sdist/amici/jax/jax.template.py @@ -20,7 +20,7 @@ def __init__(self): super().__init__() def _xdot(self, t, x, args): - pk, tcl = args + p, tcl = args TPL_X_SYMS = x TPL_P_SYMS = p @@ -31,7 +31,6 @@ def _xdot(self, t, x, args): return TPL_XDOT_RET - def _w(self, t, x, p, tcl): TPL_X_SYMS = x TPL_P_SYMS = p diff --git a/python/sdist/amici/jax/ode_export.py b/python/sdist/amici/jax/ode_export.py index 7ea4a29d8a..385bc65e07 100644 --- a/python/sdist/amici/jax/ode_export.py +++ b/python/sdist/amici/jax/ode_export.py @@ -24,6 +24,7 @@ from amici._codegen.template import apply_template from amici.jax.jaxcodeprinter import AmiciJaxCodePrinter from amici.jax.model import JAXModel +from amici.jax.nn import generate_equinox from amici.de_model import DEModel from amici.de_export import is_valid_identifier from amici.import_utils import ( @@ -129,6 +130,7 @@ def __init__( outdir: Path | str | None = None, verbose: bool | int | None = False, model_name: str | None = "model", + hybridisation: dict[str, str] = {}, ): """ Generate AMICI jax files for the ODE provided to the constructor. @@ -157,6 +159,8 @@ def __init__( self.model: DEModel = ode_model + self.hybridisation = hybridisation + self._code_printer = AmiciJaxCodePrinter() @log_execution_time("generating jax code", logger) @@ -169,6 +173,7 @@ def generate_model_code(self) -> None: ): self._prepare_model_folder() self._generate_jax_code() + self._generate_nn_code() def _prepare_model_folder(self) -> None: """ @@ -233,6 +238,14 @@ def _generate_jax_code(self) -> None: # can flag conflicts in the future "MODEL_API_VERSION": f"'{JAXModel.MODEL_API_VERSION}'", }, + "NET_IMPORTS": "\n".join( + f"{net} = _module_from_path('{net}', Path(__file__).parent / '{net}.py')" + for net in self.hybridisation.keys() + ), + "NETS": ",\n".join( + f'"{net}": {net}.net(jr.PRNGKey(0))' + for net in self.hybridisation.keys() + ), } outdir = self.model_path / (self.model_name + "_jax") outdir.mkdir(parents=True, exist_ok=True) @@ -243,6 +256,15 @@ def _generate_jax_code(self) -> None: tpl_data, ) + def _generate_nn_code(self) -> None: + for net_name, net in self.hybridisation.items(): + generate_equinox( + net["model"], + os.path.join( + self.model_path, self.model_name + "_jax", f"{net_name}.py" + ), + ) + def set_paths(self, output_dir: str | Path | None = None) -> None: """ Set output paths for the model and create if necessary diff --git a/python/sdist/amici/sbml_import.py b/python/sdist/amici/sbml_import.py index cb5c80ea88..9e66a5d924 100644 --- a/python/sdist/amici/sbml_import.py +++ b/python/sdist/amici/sbml_import.py @@ -460,6 +460,7 @@ def sbml2jax( simplify: Callable | None = _default_simplify, cache_simplify: bool = False, log_as_log10: bool = True, + hybridisation: dict = None, ) -> None: """ Generate and compile AMICI jax files for the model provided to the @@ -549,6 +550,7 @@ def sbml2jax( model_name=model_name, outdir=output_dir, verbose=verbose, + hybridisation=hybridisation, ) exporter.generate_model_code() From 031b524160ec0d1f312e6585ba484c2922815dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Tue, 3 Dec 2024 15:23:14 +0000 Subject: [PATCH 17/32] fix net test-cases --- python/sdist/amici/jax/nn.py | 38 ++-------- tests/sciml/test_sciml.py | 130 ++++++++++++++++++++++------------- 2 files changed, 87 insertions(+), 81 deletions(-) diff --git a/python/sdist/amici/jax/nn.py b/python/sdist/amici/jax/nn.py index 1238625f10..343a749ea6 100644 --- a/python/sdist/amici/jax/nn.py +++ b/python/sdist/amici/jax/nn.py @@ -89,14 +89,13 @@ def _process_argval(v): def _generate_layer(layer: Layer, indent: int, ilayer: int) -> str: layer_map = { - "InstanceNorm1d": "eqx.nn.LayerNorm", - "InstanceNorm2d": "eqx.nn.LayerNorm", - "InstanceNorm3d": "eqx.nn.LayerNorm", "Dropout1d": "eqx.nn.Dropout", "Dropout2d": "eqx.nn.Dropout", "Flatten": "amici.jax.nn.Flatten", } - if layer.layer_type.startswith(("BatchNorm", "AlphaDropout")): + if layer.layer_type.startswith( + ("BatchNorm", "AlphaDropout", "InstanceNorm") + ): raise NotImplementedError( f"{layer.layer_type} layers currently not supported" ) @@ -117,30 +116,12 @@ def _generate_layer(layer: Layer, indent: int, ilayer: int) -> str: "Conv2d": { "bias": "use_bias", }, - "InstanceNorm1d": { - "affine": "elementwise_affine", - "num_features": "shape", - }, - "InstanceNorm2d": { - "affine": "elementwise_affine", - "num_features": "shape", - }, - "InstanceNorm3d": { - "affine": "elementwise_affine", - "num_features": "shape", - }, "LayerNorm": { "affine": "elementwise_affine", "normalized_shape": "shape", }, } kwarg_ignore = { - "InstanceNorm1d": ("track_running_stats", "momentum"), - "InstanceNorm2d": ("track_running_stats", "momentum"), - "InstanceNorm3d": ("track_running_stats", "momentum"), - "BatchNorm1d": ("track_running_stats", "momentum"), - "BatchNorm2d": ("track_running_stats", "momentum"), - "BatchNorm3d": ("track_running_stats", "momentum"), "Dropout1d": ("inplace",), "Dropout2d": ("inplace",), } @@ -162,13 +143,6 @@ def _generate_layer(layer: Layer, indent: int, ilayer: int) -> str: kwargs += [f"key=keys[{ilayer}]"] type_str = layer_map.get(layer.layer_type, f"eqx.nn.{layer.layer_type}") layer_str = f"{type_str}({', '.join(kwargs)})" - if layer.layer_type.startswith(("InstanceNorm",)): - if layer.layer_type.endswith(("1d", "2d", "3d")): - layer_str = f"jax.vmap({layer_str}, in_axes=1, out_axes=1)" - if layer.layer_type.endswith(("2d", "3d")): - layer_str = f"jax.vmap({layer_str}, in_axes=2, out_axes=2)" - if layer.layer_type.endswith("3d"): - layer_str = f"jax.vmap({layer_str}, in_axes=3, out_axes=3)" return f"{' ' * indent}'{layer.layer_id}': {layer_str}" @@ -179,10 +153,8 @@ def _generate_forward(node: Node, indent, layer_type=str) -> str: if node.op == "call_module": fun_str = f"self.layers['{node.target}']" - if layer_type.startswith( - ("InstanceNorm", "Conv", "Linear", "LayerNorm") - ): - if layer_type in ("LayerNorm", "InstanceNorm"): + if layer_type.startswith(("Conv", "Linear", "LayerNorm")): + if layer_type in ("LayerNorm",): dims = f"len({fun_str}.shape)+1" if layer_type == "Linear": dims = 2 diff --git a/tests/sciml/test_sciml.py b/tests/sciml/test_sciml.py index 75205b0093..4986899fd1 100644 --- a/tests/sciml/test_sciml.py +++ b/tests/sciml/test_sciml.py @@ -40,8 +40,26 @@ def change_directory(destination): cases_dir = Path(__file__).parent / "testsuite" / "test_cases" +def _reshape_flat_array(array_flat): + array_flat["ix"] = array_flat["ix"].astype(str) + ix_cols = [ + f"ix_{i}" for i in range(len(array_flat["ix"].values[0].split(";"))) + ] + if len(ix_cols) == 1: + array_flat[ix_cols[0]] = array_flat["ix"].apply(int) + else: + array_flat[ix_cols] = pd.DataFrame( + array_flat["ix"].str.split(";").apply(np.array).to_list(), + index=array_flat.index, + ).astype(int) + array_flat.sort_values(by=ix_cols, inplace=True) + array_shape = tuple(array_flat[ix_cols].max().astype(int) + 1) + array = np.array(array_flat["value"].values).reshape(array_shape) + return array + + @pytest.mark.parametrize( - "test", [d.stem for d in cases_dir.glob("net_[0-9]*")] + "test", sorted([d.stem for d in cases_dir.glob("net_[0-9]*")]) ) def test_net(test): test_dir = cases_dir / test @@ -59,17 +77,20 @@ def test_net(test): for ml_model in ml_models.models: module_dir = outdir / f"{ml_model.mlmodel_id}.py" if test in ( - "net_022", "net_002", - "net_045", - "net_042", + "net_009", "net_018", + "net_019", "net_020", + "net_021", + "net_022", + "net_042", "net_043", "net_044", - "net_021", - "net_019", - "net_002", + "net_045", + "net_046", + "net_047", + "net_048", ): with pytest.raises(NotImplementedError): generate_equinox(ml_model, module_dir) @@ -84,38 +105,14 @@ def test_net(test): solutions.get("net_ps", solutions["net_input"]), solutions["net_output"], ): - input_flat = pd.read_csv(test_dir / input_file, sep="\t").sort_values( - by="ix" - ) - input_shape = tuple( - np.stack( - input_flat["ix"].astype(str).str.split(";").apply(np.array) - ) - .astype(int) - .max(axis=0) - + 1 - ) - input = jnp.array(input_flat["value"].values).reshape(input_shape) - - output_flat = pd.read_csv( - test_dir / output_file, sep="\t" - ).sort_values(by="ix") - output_shape = tuple( - np.stack( - output_flat["ix"].astype(str).str.split(";").apply(np.array) - ) - .astype(int) - .max(axis=0) - + 1 - ) - output = jnp.array(output_flat["value"].values).reshape(output_shape) + input_flat = pd.read_csv(test_dir / input_file, sep="\t") + input = _reshape_flat_array(input_flat) + + output_flat = pd.read_csv(test_dir / output_file, sep="\t") + output = _reshape_flat_array(output_flat) if "net_ps" in solutions: - par = ( - pd.read_csv(test_dir / par_file, sep="\t") - .set_index("parameterId") - .sort_index() - ) + par = pd.read_csv(test_dir / par_file, sep="\t") for ml_model in ml_models.models: net = nets[ml_model.mlmodel_id](jr.PRNGKey(0)) for layer in net.layers.keys(): @@ -126,14 +123,26 @@ def test_net(test): and net.layers[layer].weight is not None ): prefix = layer_prefix + "_weight" + df = par[ + par[petab.PARAMETER_ID].str.startswith(prefix) + ] + df["ix"] = ( + df[petab.PARAMETER_ID] + .str.split("_") + .str[3:] + .apply(lambda x: ";".join(x)) + ) + w = _reshape_flat_array(df) + if isinstance(net.layers[layer], eqx.nn.ConvTranspose): + # see FAQ in https://docs.kidger.site/equinox/api/nn/conv/#equinox.nn.ConvTranspose + w = np.flip( + w, axis=tuple(range(2, w.ndim)) + ).swapaxes(0, 1) + assert w.shape == net.layers[layer].weight.shape net = eqx.tree_at( lambda x: x.layers[layer].weight, net, - jnp.array( - par[par.index.str.startswith(prefix)][ - "value" - ].values - ).reshape(net.layers[layer].weight.shape), + jnp.array(w), ) if ( isinstance(net.layers[layer], eqx.Module) @@ -141,17 +150,40 @@ def test_net(test): and net.layers[layer].bias is not None ): prefix = layer_prefix + "_bias" + df = par[ + par[petab.PARAMETER_ID].str.startswith(prefix) + ] + df["ix"] = ( + df[petab.PARAMETER_ID] + .str.split("_") + .str[3:] + .apply(lambda x: ";".join(x)) + ) + b = _reshape_flat_array(df) + if isinstance( + net.layers[layer], + eqx.nn.Conv | eqx.nn.ConvTranspose, + ): + b = np.expand_dims( + b, + tuple( + range( + 1, + net.layers[layer].num_spatial_dims + 1, + ) + ), + ) + assert b.shape == net.layers[layer].bias.shape net = eqx.tree_at( lambda x: x.layers[layer].bias, net, - jnp.array( - par[par.index.str.startswith(prefix)][ - "value" - ].values - ).reshape(net.layers[layer].bias.shape), + jnp.array(b), ) net = eqx.nn.inference_mode(net) + if test == "net_004_alt": + return # skipping, no support for non-cross-correlation in equinox + np.testing.assert_allclose( net.forward(input), output, @@ -160,7 +192,9 @@ def test_net(test): ) -@pytest.mark.parametrize("test", [d.stem for d in cases_dir.glob("[0-9]*")]) +@pytest.mark.parametrize( + "test", sorted([d.stem for d in cases_dir.glob("[0-9]*")]) +) def test_ude(test): test_dir = cases_dir / test with open(test_dir / "petab" / "problem_ude.yaml") as f: From cfb0b5a5485f87a41e900c063f060507285436a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Tue, 3 Dec 2024 17:54:56 +0000 Subject: [PATCH 18/32] fixes & remove sciml dependency --- python/sdist/amici/jax/nn.py | 11 +++++++---- python/sdist/amici/jax/ode_export.py | 4 ++-- tests/sciml/test_sciml.py | 5 +++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/python/sdist/amici/jax/nn.py b/python/sdist/amici/jax/nn.py index 343a749ea6..d503df2393 100644 --- a/python/sdist/amici/jax/nn.py +++ b/python/sdist/amici/jax/nn.py @@ -1,6 +1,6 @@ from pathlib import Path -from petab_sciml import MLModel, Layer, Node + import equinox as eqx import jax.numpy as jnp @@ -30,7 +30,10 @@ def tanhshrink(x: jnp.ndarray) -> jnp.ndarray: return x - jnp.tanh(x) -def generate_equinox(ml_model: MLModel, filename: Path | str): +def generate_equinox(ml_model: "MLModel", filename: Path | str): # noqa: F821 + # TODO: move to top level import and replace forward type definitions + from petab_sciml import Layer + filename = Path(filename) layer_indent = 12 node_indent = 8 @@ -87,7 +90,7 @@ def _process_argval(v): return str(v) -def _generate_layer(layer: Layer, indent: int, ilayer: int) -> str: +def _generate_layer(layer: "Layer", indent: int, ilayer: int) -> str: # noqa: F821 layer_map = { "Dropout1d": "eqx.nn.Dropout", "Dropout2d": "eqx.nn.Dropout", @@ -146,7 +149,7 @@ def _generate_layer(layer: Layer, indent: int, ilayer: int) -> str: return f"{' ' * indent}'{layer.layer_id}': {layer_str}" -def _generate_forward(node: Node, indent, layer_type=str) -> str: +def _generate_forward(node: "Node", indent, layer_type=str) -> str: # noqa: F821 if node.op == "placeholder": # TODO: inconsistent target vs name return f"{' ' * indent}{node.name} = input" diff --git a/python/sdist/amici/jax/ode_export.py b/python/sdist/amici/jax/ode_export.py index 385bc65e07..f36f67ab85 100644 --- a/python/sdist/amici/jax/ode_export.py +++ b/python/sdist/amici/jax/ode_export.py @@ -130,7 +130,7 @@ def __init__( outdir: Path | str | None = None, verbose: bool | int | None = False, model_name: str | None = "model", - hybridisation: dict[str, str] = {}, + hybridisation: dict[str, dict] = None, ): """ Generate AMICI jax files for the ODE provided to the constructor. @@ -159,7 +159,7 @@ def __init__( self.model: DEModel = ode_model - self.hybridisation = hybridisation + self.hybridisation = hybridisation if hybridisation is not None else {} self._code_printer = AmiciJaxCodePrinter() diff --git a/tests/sciml/test_sciml.py b/tests/sciml/test_sciml.py index 4986899fd1..e872718f48 100644 --- a/tests/sciml/test_sciml.py +++ b/tests/sciml/test_sciml.py @@ -327,8 +327,9 @@ def test_ude(test): # gradient sllh, _ = eqx.filter_grad(run_simulations, has_aux=True)( jax_problem, - solver=diffrax.Tsit5(), - controller=diffrax.PIDController(atol=1e-10, rtol=1e-10), + solver=diffrax.Kvaerno5(), + controller=diffrax.PIDController(atol=1e-14, rtol=1e-14), + max_steps=2**16, ) expected = ( pd.concat( From 8b8f9a860986a84188bae8fb2bfa9208ddb2ba8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sun, 8 Dec 2024 10:24:04 +0000 Subject: [PATCH 19/32] fixup, add initial condition support --- .github/workflows/test_petab_sciml.yml | 93 ++++ documentation/ExampleJaxPEtab.ipynb | 672 ++++++++++++++++++++++++- python/sdist/amici/jax/ode_export.py | 4 +- python/sdist/amici/jax/petab.py | 50 +- tests/sciml/test_sciml.py | 41 +- 5 files changed, 812 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/test_petab_sciml.yml mode change 120000 => 100644 documentation/ExampleJaxPEtab.ipynb diff --git a/.github/workflows/test_petab_sciml.yml b/.github/workflows/test_petab_sciml.yml new file mode 100644 index 0000000000..2b0976c824 --- /dev/null +++ b/.github/workflows/test_petab_sciml.yml @@ -0,0 +1,93 @@ +name: PEtab +on: + push: + branches: + - develop + - master + pull_request: + branches: + - master + - develop + merge_group: + workflow_dispatch: + +jobs: + build: + name: PEtab SciML Testsuite + + runs-on: ubuntu-latest + + env: + ENABLE_GCOV_COVERAGE: TRUE + + strategy: + matrix: + python-version: ["3.11"] + + steps: + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - uses: actions/checkout@v4 + with: + fetch-depth: 20 + + - name: Install apt dependencies + uses: ./.github/actions/install-apt-dependencies + + # install dependencies + - name: apt + run: | + sudo apt-get update \ + && sudo apt-get install -y python3-venv + + - run: | + echo "${HOME}/.local/bin/" >> $GITHUB_PATH + + # install AMICI + - name: Install python package + run: scripts/installAmiciSource.sh + + - name: Install petab + run: | + source ./venv/bin/activate \ + && pip3 install wheel pytest shyaml pytest-cov + + # retrieve test models + - name: Download and install PEtab SciML test suite + run: | + git clone --depth 1 --branch main \ + https://github.com/sebapersson/petab_sciml.git \ + && export TESTSUITE="$(pwd)/petab_sciml" \ + && source venv/bin/activate \ + && python -m pip install -e $TESTSUITE/../src/python + + - name: Install PEtab benchmark collection + run: | + git clone --depth 1 https://github.com/benchmarking-initiative/Benchmark-Models-PEtab.git \ + && export BENCHMARK_COLLECTION="$(pwd)/Benchmark-Models-PEtab/Benchmark-Models/" \ + && source venv/bin/activate && python -m pip install -e $BENCHMARK_COLLECTION/../src/python + + - name: Install petab + run: | + source ./venv/bin/activate \ + && python3 -m pip uninstall -y petab \ + && python3 -m pip install git+https://github.com/petab-dev/libpetab-python.git@develop \ + + - name: Run PEtab SciML testsuite + run: | + source ./venv/bin/activate \ + + && pytest --cov-report=xml:coverage.xml \ + --cov=./ python/tests/test_*petab*.py python/tests/sciml/ + + - name: Codecov + if: github.event_name == 'pull_request' || github.repository_owner == 'AMICI-dev' + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: coverage.xml + flags: petab + fail_ci_if_error: true diff --git a/documentation/ExampleJaxPEtab.ipynb b/documentation/ExampleJaxPEtab.ipynb deleted file mode 120000 index 821b14f21f..0000000000 --- a/documentation/ExampleJaxPEtab.ipynb +++ /dev/null @@ -1 +0,0 @@ -../python/examples/example_jax_petab/ExampleJaxPEtab.ipynb \ No newline at end of file diff --git a/documentation/ExampleJaxPEtab.ipynb b/documentation/ExampleJaxPEtab.ipynb new file mode 100644 index 0000000000..1310091f4c --- /dev/null +++ b/documentation/ExampleJaxPEtab.ipynb @@ -0,0 +1,671 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d4d2bc5c", + "metadata": {}, + "source": [ + "# Simulating AMICI models using JAX\n", + "\n", + "## Overview\n", + "\n", + "This guide demonstrates how to use AMICI to export models in a format compatible with the [JAX](https://jax.readthedocs.io/en/latest/) ecosystem, enabling simulations with the [diffrax](https://docs.kidger.site/diffrax/) library. " + ] + }, + { + "cell_type": "markdown", + "id": "fb2fe897", + "metadata": {}, + "source": [ + "## Preparation\n", + "\n", + "To begin, we will import a model using [PEtab](https://petab.readthedocs.io). For this demonstration, we will utilize the [Benchmark Collection](https://github.com/Benchmarking-Initiative/Benchmark-Models-PEtab), which provides a diverse set of models. For more information on importing PEtab models, refer to the corresponding [PEtab notebook](https://amici.readthedocs.io/en/latest/petab.html).\n", + "\n", + "In this tutorial, we will import the Böhm model from the Benchmark Collection. Using [amici.petab_import](https://amici.readthedocs.io/en/latest/generated/amici.petab_import.html#amici.petab_import.import_petab_problem), we will load the PEtab problem. To create a [JAXModel](https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.JAXModel) instead of a standard AMICI model, we set the `jax` parameter to `True`.\n" + ] + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "from amici.petab.petab_import import import_petab_problem\n", + "import petab.v1 as petab\n", + "\n", + "# Define the model name and YAML file location\n", + "model_name = \"Boehm_JProteomeRes2014\"\n", + "yaml_url = (\n", + " f\"https://raw.githubusercontent.com/Benchmarking-Initiative/Benchmark-Models-PEtab/\"\n", + " f\"master/Benchmark-Models/{model_name}/{model_name}.yaml\"\n", + ")\n", + "\n", + "# Load the PEtab problem from the YAML file\n", + "petab_problem = petab.Problem.from_yaml(yaml_url)\n", + "\n", + "# Import the PEtab problem as a JAX-compatible AMICI model\n", + "jax_model = import_petab_problem(\n", + " petab_problem,\n", + " verbose=False, # no text output\n", + " jax=True, # return jax model\n", + ")" + ], + "id": "c71c96da0da3144a" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Simulation\n", + "\n", + "In principle, we can already use this model for simulation using the [simulate_condition](https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.JAXModel.simulate_condition) method. However, this approach can be cumbersome as timepoints, data etc. need to be specified manually. Instead, we process the PEtab problem into a [JAXProblem](https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.JAXProblem), which enables efficient simulation using [amici.jax.run_simulations]((https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.run_simulations)." + ], + "id": "7e0f1c27bd71ee1f" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "from amici.jax import JAXProblem, run_simulations\n", + "\n", + "# Create a JAXProblem from the JAX model and PEtab problem\n", + "jax_problem = JAXProblem(jax_model, petab_problem)\n", + "\n", + "# Run simulations and compute the log-likelihood\n", + "llh, results = run_simulations(jax_problem)" + ], + "id": "ccecc9a29acc7b73" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "This simulates the model for all conditions using the nominal parameter values. Simple, right? Now, let’s take a look at the simulation results.", + "id": "415962751301c64a" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# Define the simulation condition\n", + "simulation_condition = (\"model1_data1\",)\n", + "\n", + "# Access the results for the specified condition\n", + "results[simulation_condition]" + ], + "id": "596b86e45e18fe3d" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Unfortunately, the simulation failed! As seen in the output, the simulation broke down after the initial timepoint, indicated by the `inf` values in the state variables `results[simulation_condition][1].x` and the `nan` likelihood value. A closer inspection of this variable provides additional clues about what might have gone wrong.\n", + "\n", + "The issue stems from using single precision, as indicated by the `float32` dtype of state variables. Single precision is generally a [bad idea](https://docs.kidger.site/diffrax/examples/stiff_ode/) for stiff systems like the Böhm model. Let’s retry the simulation with double precision." + ], + "id": "a1b173e013f9210a" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "import jax\n", + "\n", + "# Enable double precision in JAX\n", + "jax.config.update(\"jax_enable_x64\", True)\n", + "\n", + "# Re-run simulations with double precision\n", + "llh, results = run_simulations(jax_problem)\n", + "\n", + "results" + ], + "id": "f4f5ff705a3f7402" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Success! The simulation completed successfully, and we can now plot the resulting state trajectories.", + "id": "fe4d3b40ee3efdf2" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "\n", + "def plot_simulation(results):\n", + " \"\"\"\n", + " Plot the state trajectories from the simulation results.\n", + "\n", + " Parameters:\n", + " results (dict): Simulation results from run_simulations.\n", + " \"\"\"\n", + " # Extract the simulation results for the specific condition\n", + " sim_results = results[simulation_condition]\n", + "\n", + " # Create a new figure for the state trajectories\n", + " plt.figure(figsize=(8, 6))\n", + " for idx in range(sim_results[\"x\"].shape[1]):\n", + " time_points = np.array(sim_results[\"ts\"])\n", + " state_values = np.array(sim_results[\"x\"][:, idx])\n", + " plt.plot(time_points, state_values, label=jax_model.state_ids[idx])\n", + "\n", + " # Add labels, legend, and grid\n", + " plt.xlabel(\"Time\")\n", + " plt.ylabel(\"State Values\")\n", + " plt.title(simulation_condition)\n", + " plt.legend()\n", + " plt.grid(True)\n", + " plt.show()\n", + "\n", + "\n", + "# Plot the simulation results\n", + "plot_simulation(results)" + ], + "id": "72f1ed397105e14a" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "`run_simulations` enables users to specify the simulation conditions to be executed. For more complex models, this allows for restricting simulations to a subset of conditions. Since the Böhm model includes only a single condition, we demonstrate this functionality by simulating no condition at all.", + "id": "4fa97c33719c2277" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "llh, results = run_simulations(jax_problem, simulation_conditions=tuple())\n", + "results" + ], + "id": "7950774a3e989042" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Updating Parameters\n", + "\n", + "As next step, we will update the parameter values used for simulation. However, if we attempt to directly modify the values in `JAXModel.parameters`, we encounter a `FrozenInstanceError`." + ], + "id": "98b8516a75ce4d12" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "from dataclasses import FrozenInstanceError\n", + "import jax\n", + "\n", + "# Generate random noise to update the parameters\n", + "noise = (\n", + " jax.random.normal(\n", + " key=jax.random.PRNGKey(0), shape=jax_problem.parameters.shape\n", + " )\n", + " / 10\n", + ")\n", + "\n", + "# Attempt to update the parameters\n", + "try:\n", + " jax_problem.parameters += noise\n", + "except FrozenInstanceError as e:\n", + " print(\"Error:\", e)" + ], + "id": "3d278a3d21e709d" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The root cause of this error lies in the fact that, to enable autodiff, direct modifications of attributes are not allowed in [equinox](https://docs.kidger.site/equinox/), which AMICI utilizes under the hood. Consequently, attributes of instances like `JAXModel` or `JAXProblem` cannot be updated directly — this is the price we have to pay for autodiff.\n", + "\n", + "However, `JAXProblem` provides a convenient method called [update_parameters](https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.JAXProblem.update_parameters). The caveat is that this method creates a new JAXProblem instance instead of modifying the existing one." + ], + "id": "4cc3d595de4a4085" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# Update the parameters and create a new JAXProblem instance\n", + "jax_problem = jax_problem.update_parameters(jax_problem.parameters + noise)\n", + "\n", + "# Run simulations with the updated parameters\n", + "llh, results = run_simulations(jax_problem)\n", + "\n", + "# Plot the simulation results\n", + "plot_simulation(results)" + ], + "id": "e47748376059628b" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Computing Gradients\n", + "\n", + "Similar to updating attributes, computing gradients in the JAX ecosystem can feel a bit unconventional if you’re not familiar with the JAX ecosysmt. JAX offers [powerful automatic differentiation](https://jax.readthedocs.io/en/latest/automatic-differentiation.html) through the `jax.grad` function. However, to use `jax.grad` with `JAXProblem`, we need to specify which parts of the `JAXProblem` should be treated as static." + ], + "id": "660baf605a4e8339" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "try:\n", + " # Attempt to compute the gradient of the run_simulations function\n", + " jax.grad(run_simulations, has_aux=True)(jax_problem)\n", + "except TypeError as e:\n", + " print(\"Error:\", e)" + ], + "id": "7033d09cc81b7f69" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Fortunately, `equinox` simplifies this process by offering [filter_grad](https://docs.kidger.site/equinox/api/transformations/#equinox.filter_grad), which enables autodiff functionality that is compatible with `JAXProblem` and, in theory, also with `JAXModel`.", + "id": "dc9bc07cde00a926" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "import equinox as eqx\n", + "\n", + "# Compute the gradient using equinox's filter_grad, preserving auxiliary outputs\n", + "grad, _ = eqx.filter_grad(run_simulations, has_aux=True)(jax_problem)" + ], + "id": "a6704182200e6438" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Functions transformed by `filter_grad` return gradients that share the same structure as the first argument (unless specified otherwise). This allows us to access the gradient with respect to the parameters attribute directly `via grad.parameters`.", + "id": "851c3ec94cb5d086" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "grad.parameters", + "id": "c00c1581d7173d7a" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Attributes for which derivatives cannot be computed (typically anything that is not a [jax.numpy.array](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.array.html)) are automatically set to `None`.", + "id": "375b835fecc5a022" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "grad", + "id": "f7c17f7459d0151f" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Observant readers may notice that the gradient above appears to include numeric values for derivatives with respect to some measurements. However, `simulation_conditions` internally disables gradient computations using `jax.lax.stop_gradient`, resulting in these values being zeroed out.", + "id": "8eb7cc3db510c826" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "grad._measurements[simulation_condition]", + "id": "3badd4402cf6b8c6" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "However, we can compute derivatives with respect to data elements using `JAXModel.simulate_condition`. In the example below, we differentiate the observables `y` (specified by passing `y` to the `ret` argument) with respect to the timepoints at which the model outputs are computed after the solving the differential equation. While this might not be particularly practical, it serves as an nice illustration of the power of automatic differentiation.", + "id": "58eb04393a1463d" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "import jax.numpy as jnp\n", + "import diffrax\n", + "from amici.jax import ReturnValue\n", + "\n", + "# Define the simulation condition\n", + "simulation_condition = (\"model1_data1\",)\n", + "\n", + "# Load condition-specific data\n", + "ts_init, ts_dyn, ts_posteq, my, iys, iy_trafos = jax_problem._measurements[\n", + " simulation_condition\n", + "]\n", + "\n", + "# Load parameters for the specified condition\n", + "p = jax_problem.load_parameters(simulation_condition[0])\n", + "\n", + "\n", + "# Define a function to compute the gradient with respect to dynamic timepoints\n", + "@eqx.filter_jacfwd\n", + "def grad_ts_dyn(tt):\n", + " return jax_problem.model.simulate_condition(\n", + " p=p,\n", + " ts_init=ts_init,\n", + " ts_dyn=tt,\n", + " ts_posteq=ts_posteq,\n", + " my=jnp.array(my),\n", + " iys=jnp.array(iys),\n", + " iy_trafos=jnp.array(iy_trafos),\n", + " solver=diffrax.Kvaerno5(),\n", + " controller=diffrax.PIDController(atol=1e-8, rtol=1e-8),\n", + " max_steps=2**10,\n", + " adjoint=diffrax.DirectAdjoint(),\n", + " ret=ReturnValue.y, # Return observables\n", + " )[0]\n", + "\n", + "\n", + "# Compute the gradient with respect to `ts_dyn`\n", + "g = grad_ts_dyn(ts_dyn)\n", + "g" + ], + "id": "1a91aff44b93157" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Compilation & Profiling\n", + "\n", + "To maximize performance with JAX, code should be just-in-time (JIT) compiled. This can be achieved using the `jax.jit` or `equinox.filter_jit` decorators. While JIT compilation introduces some overhead during the first function call, it significantly improves performance for subsequent calls. To demonstrate this, we will first clear the JIT cache and then profile the execution." + ], + "id": "9f870da7754e139c" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "from time import time\n", + "\n", + "# Clear JAX caches to ensure a fresh start\n", + "jax.clear_caches()\n", + "\n", + "# Define a JIT-compiled gradient function with auxiliary outputs\n", + "gradfun = eqx.filter_jit(eqx.filter_grad(run_simulations, has_aux=True))" + ], + "id": "58ebdc110ea7457e" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# Measure the time taken for the first function call (including compilation)\n", + "start = time()\n", + "run_simulations(jax_problem)\n", + "print(f\"Function compilation time: {time() - start:.2f} seconds\")\n", + "\n", + "# Measure the time taken for the gradient computation (including compilation)\n", + "start = time()\n", + "gradfun(jax_problem)\n", + "print(f\"Gradient compilation time: {time() - start:.2f} seconds\")" + ], + "id": "e1242075f7e0faf" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "%%timeit\n", + "run_simulations(\n", + " jax_problem,\n", + " controller=diffrax.PIDController(\n", + " rtol=1e-8, # same as amici default\n", + " atol=1e-16, # same as amici default\n", + " pcoeff=0.4, # recommended value for stiff systems\n", + " icoeff=0.3, # recommended value for stiff systems\n", + " dcoeff=0.0, # recommended value for stiff systems\n", + " ),\n", + ")" + ], + "id": "27181f367ccb1817" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "%%timeit \n", + "gradfun(\n", + " jax_problem,\n", + " controller=diffrax.PIDController(\n", + " rtol=1e-8, # same as amici default\n", + " atol=1e-16, # same as amici default\n", + " pcoeff=0.4, # recommended value for stiff systems\n", + " icoeff=0.3, # recommended value for stiff systems\n", + " dcoeff=0.0, # recommended value for stiff systems\n", + " ),\n", + ")" + ], + "id": "5b8d3a6162a3ae55" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "from amici.petab import simulate_petab\n", + "import amici\n", + "\n", + "# Import the PEtab problem as a standard AMICI model\n", + "amici_model = import_petab_problem(\n", + " petab_problem,\n", + " verbose=False,\n", + " jax=False, # load the amici model this time\n", + ")\n", + "\n", + "# Configure the solver with appropriate tolerances\n", + "solver = amici_model.getSolver()\n", + "solver.setAbsoluteTolerance(1e-8)\n", + "solver.setRelativeTolerance(1e-8)\n", + "\n", + "# Prepare the parameters for the simulation\n", + "problem_parameters = dict(\n", + " zip(jax_problem.parameter_ids, jax_problem.parameters)\n", + ")" + ], + "id": "d733a450635a749b" + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "413ed7c60b2cf4be", + "metadata": { + "ExecuteTime": { + "end_time": "2024-11-19T09:51:55.259985Z", + "start_time": "2024-11-19T09:51:55.257937Z" + } + }, + "outputs": [], + "source": [ + "# Profile simulation only\n", + "solver.setSensitivityOrder(amici.SensitivityOrder.none)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "768fa60e439ca8b4", + "metadata": { + "ExecuteTime": { + "end_time": "2024-11-19T09:51:57.417608Z", + "start_time": "2024-11-19T09:51:55.273367Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "26.1 ms ± 2.71 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "%%timeit \n", + "simulate_petab(\n", + " petab_problem,\n", + " amici_model,\n", + " solver=solver,\n", + " problem_parameters=problem_parameters,\n", + " scaled_parameters=True,\n", + " scaled_gradients=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "b8382b0b2b68f49e", + "metadata": { + "ExecuteTime": { + "end_time": "2024-11-19T09:51:57.497361Z", + "start_time": "2024-11-19T09:51:57.494502Z" + } + }, + "outputs": [], + "source": [ + "# Profile gradient computation using forward sensitivity analysis\n", + "solver.setSensitivityOrder(amici.SensitivityOrder.first)\n", + "solver.setSensitivityMethod(amici.SensitivityMethod.forward)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "3bae1fab8c416122", + "metadata": { + "ExecuteTime": { + "end_time": "2024-11-19T09:51:59.897459Z", + "start_time": "2024-11-19T09:51:57.511889Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "29.1 ms ± 1.82 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "%%timeit \n", + "simulate_petab(\n", + " petab_problem,\n", + " amici_model,\n", + " solver=solver,\n", + " problem_parameters=problem_parameters,\n", + " scaled_parameters=True,\n", + " scaled_gradients=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "71e0358227e1dc74", + "metadata": { + "ExecuteTime": { + "end_time": "2024-11-19T09:51:59.972149Z", + "start_time": "2024-11-19T09:51:59.969006Z" + } + }, + "outputs": [], + "source": [ + "# Profile gradient computation using adjoint sensitivity analysis\n", + "solver.setSensitivityOrder(amici.SensitivityOrder.first)\n", + "solver.setSensitivityMethod(amici.SensitivityMethod.adjoint)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "e3cc7971002b6d06", + "metadata": { + "ExecuteTime": { + "end_time": "2024-11-19T09:52:03.266074Z", + "start_time": "2024-11-19T09:51:59.992465Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "39.3 ms ± 1.6 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "%%timeit \n", + "simulate_petab(\n", + " petab_problem,\n", + " amici_model,\n", + " solver=solver,\n", + " problem_parameters=problem_parameters,\n", + " scaled_parameters=True,\n", + " scaled_gradients=True,\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/python/sdist/amici/jax/ode_export.py b/python/sdist/amici/jax/ode_export.py index 09c6e72a41..08ba8bc0cd 100644 --- a/python/sdist/amici/jax/ode_export.py +++ b/python/sdist/amici/jax/ode_export.py @@ -258,9 +258,7 @@ def _generate_nn_code(self) -> None: for net_name, net in self.hybridisation.items(): generate_equinox( net["model"], - os.path.join( - self.model_path, self.model_name + "_jax", f"{net_name}.py" - ), + self.model_path / f"{net_name}.py", ) def set_paths(self, output_dir: str | Path | None = None) -> None: diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index e85aded8bb..4d1cfd303a 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -107,12 +107,10 @@ def __init__(self, model: JAXModel, petab_problem: petab.Problem): self._petab_problem = petab_problem self.parameters, self.model = self._get_nominal_parameter_values(model) self._parameter_mappings = self._get_parameter_mappings(scs) - self._measurements = self._get_measurements(scs) self._inputs = self._get_inputs() self._measurements, self._petab_measurement_indices = ( self._get_measurements(scs) ) - self.parameters = self._get_nominal_parameter_values() def save(self, directory: Path): """ @@ -358,6 +356,20 @@ def parameter_ids(self) -> list[str]: self._petab_problem.parameter_df[petab.ESTIMATE] == 1 ].index.tolist() + @property + def nn_output_ids(self) -> list[str]: + """ + Parameter ids that are estimated in the PEtab problem. Same ordering as values in :attr:`parameters`. + + :return: + PEtab parameter ids + """ + return self._petab_problem.mapping_df[ + self._petab_problem.mapping_df[ + petab.MODEL_ENTITY_ID + ].str.startswith("output") + ].index.tolist() + def get_petab_parameter_by_id(self, name: str) -> jnp.float_: """ Get the value of a PEtab parameter by name. @@ -418,7 +430,19 @@ def _eval_nn(self, output_par: str): ) return nn.forward(net_input).squeeze() - def load_parameters( + def _map_model_parameter_value( + self, + mapping: ParameterMappingForCondition, + pname: str, + ) -> jt.Float[jt.Scalar, ""] | float: # noqa: F722 + if pname in self.nn_output_ids: + return self._eval_nn(pname) + pval = mapping.map_sim_var[pname] + if isinstance(pval, Number): + return pval + return self.get_petab_parameter_by_id(pval) + + def load_model_parameters( self, simulation_condition: str ) -> jt.Float[jt.Array, "np"]: """ @@ -431,19 +455,9 @@ def load_parameters( """ mapping = self._parameter_mappings[simulation_condition] - nn_output_pars = self._petab_problem.mapping_df[ - self._petab_problem.mapping_df[ - petab.MODEL_ENTITY_ID - ].str.startswith("output") - ].index - p = jnp.array( [ - self._eval_nn(pname) - if pname in nn_output_pars - else pval - if isinstance(pval := mapping.map_sim_var[pname], Number) - else self.get_petab_parameter_by_id(pval) + self._map_model_parameter_value(mapping, pname) for pname in self.model.parameter_ids ] ) @@ -499,6 +513,9 @@ def _state_reinitialisation_value( :return: reinitialisation value for the state """ + if state_id in self.nn_output_ids: + return self._eval_nn(state_id) + if state_id not in self._petab_problem.condition_df: # no reinitialisation, return dummy value return 0.0 @@ -543,6 +560,7 @@ def load_reinitialisation( """ if not any( x_id in self._petab_problem.condition_df + or x_id in self.nn_output_ids for x_id in self.model.state_ids ): return jnp.array([]), jnp.array([]) @@ -602,7 +620,7 @@ def run_simulation( ts_preeq, ts_dyn, ts_posteq, my, iys, iy_trafos = self._measurements[ simulation_condition ] - p = self.load_parameters(simulation_condition[0]) + p = self.load_model_parameters(simulation_condition[0]) mask_reinit, x_reinit = self.load_reinitialisation( simulation_condition[0], p ) @@ -647,7 +665,7 @@ def run_preequilibration( :return: Pre-equilibration state """ - p = self.load_parameters(simulation_condition) + p = self.load_model_parameters(simulation_condition) mask_reinit, x_reinit = self.load_reinitialisation( simulation_condition, p ) diff --git a/tests/sciml/test_sciml.py b/tests/sciml/test_sciml.py index e872718f48..c6a98fd8cc 100644 --- a/tests/sciml/test_sciml.py +++ b/tests/sciml/test_sciml.py @@ -4,7 +4,12 @@ from pathlib import Path import petab.v1 as petab from amici.petab import import_petab_problem -from amici.jax import JAXProblem, generate_equinox, run_simulations +from amici.jax import ( + JAXProblem, + generate_equinox, + run_simulations, + petab_simulate, +) import amici import diffrax import pandas as pd @@ -265,17 +270,16 @@ def test_ude(test): # llh if test in ( + "001", + "004", + "005", + "008", + "010", + "011", "012", "013", "014", - "001", - "011", "016", - "010", - "010", - "003", - "004", - "005", ): with pytest.raises(NotImplementedError): run_simulations(jax_problem) @@ -295,27 +299,8 @@ def test_ude(test): ) # simulations - - y, r = run_simulations(jax_problem, ret="y") - dfs = [] - for sc, ys in y.items(): - obs = [ - jax_model.observable_ids[io] - for io in jax_problem._measurements[sc][4] - ] - t = jax_problem._measurements[sc][1] - dfs.append( - pd.DataFrame( - { - petab.SIMULATION: ys, - petab.TIME: t, - petab.OBSERVABLE_ID: obs, - petab.SIMULATION_CONDITION_ID: [sc[-1]] * len(t), - } - ) - ) sort_by = [petab.OBSERVABLE_ID, petab.TIME, petab.SIMULATION_CONDITION_ID] - actual = pd.concat(dfs).sort_values(by=sort_by) + actual = petab_simulate(jax_problem).sort_values(by=sort_by) expected = simulations.sort_values(by=sort_by) np.testing.assert_allclose( actual[petab.SIMULATION].values, From 982d275087b9c1f6bd52c23367adcd3f8246be1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sun, 8 Dec 2024 11:46:24 +0000 Subject: [PATCH 20/32] Update petab.py --- python/sdist/amici/jax/petab.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index 4d1cfd303a..0bf0d97696 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -320,6 +320,11 @@ def _get_nominal_parameter_values( ), model def _get_inputs(self): + if ( + self._petab_problem.mapping_df is None + or "netId" not in self._petab_problem.mapping_df.columns + ): + return {} inputs = { net: {} for net in self._petab_problem.mapping_df["netId"].unique() } From 33f86bbf34b68b13293edaa51857ffe92742d464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sun, 8 Dec 2024 11:53:55 +0000 Subject: [PATCH 21/32] Update test_petab_sciml.yml --- .github/workflows/test_petab_sciml.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test_petab_sciml.yml b/.github/workflows/test_petab_sciml.yml index 2b0976c824..330a9982f9 100644 --- a/.github/workflows/test_petab_sciml.yml +++ b/.github/workflows/test_petab_sciml.yml @@ -60,15 +60,10 @@ jobs: run: | git clone --depth 1 --branch main \ https://github.com/sebapersson/petab_sciml.git \ - && export TESTSUITE="$(pwd)/petab_sciml" \ + && export SCIML_TESTSUITE="$(pwd)/petab_sciml" \ && source venv/bin/activate \ - && python -m pip install -e $TESTSUITE/../src/python + && python -m pip install -e $SCIML_TESTSUITE/src/python - - name: Install PEtab benchmark collection - run: | - git clone --depth 1 https://github.com/benchmarking-initiative/Benchmark-Models-PEtab.git \ - && export BENCHMARK_COLLECTION="$(pwd)/Benchmark-Models-PEtab/Benchmark-Models/" \ - && source venv/bin/activate && python -m pip install -e $BENCHMARK_COLLECTION/../src/python - name: Install petab run: | From 5a846823ebf28563d324fcc0b592e1b9c9f94aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sun, 8 Dec 2024 12:16:30 +0000 Subject: [PATCH 22/32] Update petab.py --- python/sdist/amici/jax/petab.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index 0bf0d97696..132f8290ff 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -369,6 +369,8 @@ def nn_output_ids(self) -> list[str]: :return: PEtab parameter ids """ + if self._petab_problem.parameter_df is None: + return [] return self._petab_problem.mapping_df[ self._petab_problem.mapping_df[ petab.MODEL_ENTITY_ID From 97c7bbb308321a47e9b0f8322460bc409c751ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sun, 8 Dec 2024 12:33:20 +0000 Subject: [PATCH 23/32] Update petab.py --- python/sdist/amici/jax/petab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index 132f8290ff..508cf94184 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -369,7 +369,7 @@ def nn_output_ids(self) -> list[str]: :return: PEtab parameter ids """ - if self._petab_problem.parameter_df is None: + if self._petab_problem.mapping_df is None: return [] return self._petab_problem.mapping_df[ self._petab_problem.mapping_df[ From ac79583a6ca415cd72fdeac6a908cdbccf9313ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sun, 8 Dec 2024 12:50:46 +0000 Subject: [PATCH 24/32] Update petab.py --- python/sdist/amici/jax/petab.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index 508cf94184..55686a719b 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -471,7 +471,8 @@ def load_model_parameters( pscale = tuple( [ petab.LIN - if pname in self._petab_problem.mapping_df.index + if self._petab_problem.mapping_df is not None + and pname in self._petab_problem.mapping_df.index else mapping.scale_map_sim_var[pname] for pname in self.model.parameter_ids ] From 4de63c4e5ebb63d813aac8c622b98b6a5816a865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sun, 8 Dec 2024 13:10:52 +0000 Subject: [PATCH 25/32] Update ExampleJaxPEtab.ipynb --- .../example_jax_petab/ExampleJaxPEtab.ipynb | 244 +++++++++--------- 1 file changed, 125 insertions(+), 119 deletions(-) diff --git a/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb b/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb index 1310091f4c..6d645a1451 100644 --- a/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb +++ b/python/examples/example_jax_petab/ExampleJaxPEtab.ipynb @@ -25,10 +25,11 @@ ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "c71c96da0da3144a", + "metadata": {}, + "outputs": [], "source": [ "from amici.petab.petab_import import import_petab_problem\n", "import petab.v1 as petab\n", @@ -49,24 +50,24 @@ " verbose=False, # no text output\n", " jax=True, # return jax model\n", ")" - ], - "id": "c71c96da0da3144a" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "7e0f1c27bd71ee1f", + "metadata": {}, "source": [ "## Simulation\n", "\n", "In principle, we can already use this model for simulation using the [simulate_condition](https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.JAXModel.simulate_condition) method. However, this approach can be cumbersome as timepoints, data etc. need to be specified manually. Instead, we process the PEtab problem into a [JAXProblem](https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.JAXProblem), which enables efficient simulation using [amici.jax.run_simulations]((https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.run_simulations)." - ], - "id": "7e0f1c27bd71ee1f" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "ccecc9a29acc7b73", + "metadata": {}, + "outputs": [], "source": [ "from amici.jax import JAXProblem, run_simulations\n", "\n", @@ -75,44 +76,44 @@ "\n", "# Run simulations and compute the log-likelihood\n", "llh, results = run_simulations(jax_problem)" - ], - "id": "ccecc9a29acc7b73" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "This simulates the model for all conditions using the nominal parameter values. Simple, right? Now, let’s take a look at the simulation results.", - "id": "415962751301c64a" + "id": "415962751301c64a", + "metadata": {}, + "source": "This simulates the model for all conditions using the nominal parameter values. Simple, right? Now, let’s take a look at the simulation results." }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "596b86e45e18fe3d", + "metadata": {}, + "outputs": [], "source": [ "# Define the simulation condition\n", "simulation_condition = (\"model1_data1\",)\n", "\n", "# Access the results for the specified condition\n", "results[simulation_condition]" - ], - "id": "596b86e45e18fe3d" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "a1b173e013f9210a", + "metadata": {}, "source": [ "Unfortunately, the simulation failed! As seen in the output, the simulation broke down after the initial timepoint, indicated by the `inf` values in the state variables `results[simulation_condition][1].x` and the `nan` likelihood value. A closer inspection of this variable provides additional clues about what might have gone wrong.\n", "\n", "The issue stems from using single precision, as indicated by the `float32` dtype of state variables. Single precision is generally a [bad idea](https://docs.kidger.site/diffrax/examples/stiff_ode/) for stiff systems like the Böhm model. Let’s retry the simulation with double precision." - ], - "id": "a1b173e013f9210a" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "f4f5ff705a3f7402", + "metadata": {}, + "outputs": [], "source": [ "import jax\n", "\n", @@ -123,20 +124,20 @@ "llh, results = run_simulations(jax_problem)\n", "\n", "results" - ], - "id": "f4f5ff705a3f7402" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "Success! The simulation completed successfully, and we can now plot the resulting state trajectories.", - "id": "fe4d3b40ee3efdf2" + "id": "fe4d3b40ee3efdf2", + "metadata": {}, + "source": "Success! The simulation completed successfully, and we can now plot the resulting state trajectories." }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "72f1ed397105e14a", + "metadata": {}, + "outputs": [], "source": [ "%matplotlib inline\n", "import matplotlib.pyplot as plt\n", @@ -171,41 +172,41 @@ "\n", "# Plot the simulation results\n", "plot_simulation(results)" - ], - "id": "72f1ed397105e14a" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "`run_simulations` enables users to specify the simulation conditions to be executed. For more complex models, this allows for restricting simulations to a subset of conditions. Since the Böhm model includes only a single condition, we demonstrate this functionality by simulating no condition at all.", - "id": "4fa97c33719c2277" + "id": "4fa97c33719c2277", + "metadata": {}, + "source": "`run_simulations` enables users to specify the simulation conditions to be executed. For more complex models, this allows for restricting simulations to a subset of conditions. Since the Böhm model includes only a single condition, we demonstrate this functionality by simulating no condition at all." }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "7950774a3e989042", + "metadata": {}, + "outputs": [], "source": [ "llh, results = run_simulations(jax_problem, simulation_conditions=tuple())\n", "results" - ], - "id": "7950774a3e989042" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "98b8516a75ce4d12", + "metadata": {}, "source": [ "## Updating Parameters\n", "\n", "As next step, we will update the parameter values used for simulation. However, if we attempt to directly modify the values in `JAXModel.parameters`, we encounter a `FrozenInstanceError`." - ], - "id": "98b8516a75ce4d12" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "3d278a3d21e709d", + "metadata": {}, + "outputs": [], "source": [ "from dataclasses import FrozenInstanceError\n", "import jax\n", @@ -223,24 +224,24 @@ " jax_problem.parameters += noise\n", "except FrozenInstanceError as e:\n", " print(\"Error:\", e)" - ], - "id": "3d278a3d21e709d" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "4cc3d595de4a4085", + "metadata": {}, "source": [ "The root cause of this error lies in the fact that, to enable autodiff, direct modifications of attributes are not allowed in [equinox](https://docs.kidger.site/equinox/), which AMICI utilizes under the hood. Consequently, attributes of instances like `JAXModel` or `JAXProblem` cannot be updated directly — this is the price we have to pay for autodiff.\n", "\n", "However, `JAXProblem` provides a convenient method called [update_parameters](https://amici.readthedocs.io/en/latest/generated/amici.jax.html#amici.jax.JAXProblem.update_parameters). The caveat is that this method creates a new JAXProblem instance instead of modifying the existing one." - ], - "id": "4cc3d595de4a4085" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "e47748376059628b", + "metadata": {}, + "outputs": [], "source": [ "# Update the parameters and create a new JAXProblem instance\n", "jax_problem = jax_problem.update_parameters(jax_problem.parameters + noise)\n", @@ -250,105 +251,111 @@ "\n", "# Plot the simulation results\n", "plot_simulation(results)" - ], - "id": "e47748376059628b" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "660baf605a4e8339", + "metadata": {}, "source": [ "## Computing Gradients\n", "\n", "Similar to updating attributes, computing gradients in the JAX ecosystem can feel a bit unconventional if you’re not familiar with the JAX ecosysmt. JAX offers [powerful automatic differentiation](https://jax.readthedocs.io/en/latest/automatic-differentiation.html) through the `jax.grad` function. However, to use `jax.grad` with `JAXProblem`, we need to specify which parts of the `JAXProblem` should be treated as static." - ], - "id": "660baf605a4e8339" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "7033d09cc81b7f69", + "metadata": {}, + "outputs": [], "source": [ "try:\n", " # Attempt to compute the gradient of the run_simulations function\n", " jax.grad(run_simulations, has_aux=True)(jax_problem)\n", "except TypeError as e:\n", " print(\"Error:\", e)" - ], - "id": "7033d09cc81b7f69" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "Fortunately, `equinox` simplifies this process by offering [filter_grad](https://docs.kidger.site/equinox/api/transformations/#equinox.filter_grad), which enables autodiff functionality that is compatible with `JAXProblem` and, in theory, also with `JAXModel`.", - "id": "dc9bc07cde00a926" + "id": "dc9bc07cde00a926", + "metadata": {}, + "source": "Fortunately, `equinox` simplifies this process by offering [filter_grad](https://docs.kidger.site/equinox/api/transformations/#equinox.filter_grad), which enables autodiff functionality that is compatible with `JAXProblem` and, in theory, also with `JAXModel`." }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "a6704182200e6438", + "metadata": {}, + "outputs": [], "source": [ "import equinox as eqx\n", "\n", "# Compute the gradient using equinox's filter_grad, preserving auxiliary outputs\n", "grad, _ = eqx.filter_grad(run_simulations, has_aux=True)(jax_problem)" - ], - "id": "a6704182200e6438" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "Functions transformed by `filter_grad` return gradients that share the same structure as the first argument (unless specified otherwise). This allows us to access the gradient with respect to the parameters attribute directly `via grad.parameters`.", - "id": "851c3ec94cb5d086" + "id": "851c3ec94cb5d086", + "metadata": {}, + "source": "Functions transformed by `filter_grad` return gradients that share the same structure as the first argument (unless specified otherwise). This allows us to access the gradient with respect to the parameters attribute directly `via grad.parameters`." }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, - "source": "grad.parameters", - "id": "c00c1581d7173d7a" + "id": "c00c1581d7173d7a", + "metadata": {}, + "outputs": [], + "source": [ + "grad.parameters" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "Attributes for which derivatives cannot be computed (typically anything that is not a [jax.numpy.array](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.array.html)) are automatically set to `None`.", - "id": "375b835fecc5a022" + "id": "375b835fecc5a022", + "metadata": {}, + "source": "Attributes for which derivatives cannot be computed (typically anything that is not a [jax.numpy.array](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.array.html)) are automatically set to `None`." }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, - "source": "grad", - "id": "f7c17f7459d0151f" + "id": "f7c17f7459d0151f", + "metadata": {}, + "outputs": [], + "source": [ + "grad" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "Observant readers may notice that the gradient above appears to include numeric values for derivatives with respect to some measurements. However, `simulation_conditions` internally disables gradient computations using `jax.lax.stop_gradient`, resulting in these values being zeroed out.", - "id": "8eb7cc3db510c826" + "id": "8eb7cc3db510c826", + "metadata": {}, + "source": "Observant readers may notice that the gradient above appears to include numeric values for derivatives with respect to some measurements. However, `simulation_conditions` internally disables gradient computations using `jax.lax.stop_gradient`, resulting in these values being zeroed out." }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, - "source": "grad._measurements[simulation_condition]", - "id": "3badd4402cf6b8c6" + "id": "3badd4402cf6b8c6", + "metadata": {}, + "outputs": [], + "source": [ + "grad._measurements[simulation_condition]" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "However, we can compute derivatives with respect to data elements using `JAXModel.simulate_condition`. In the example below, we differentiate the observables `y` (specified by passing `y` to the `ret` argument) with respect to the timepoints at which the model outputs are computed after the solving the differential equation. While this might not be particularly practical, it serves as an nice illustration of the power of automatic differentiation.", - "id": "58eb04393a1463d" + "id": "58eb04393a1463d", + "metadata": {}, + "source": "However, we can compute derivatives with respect to data elements using `JAXModel.simulate_condition`. In the example below, we differentiate the observables `y` (specified by passing `y` to the `ret` argument) with respect to the timepoints at which the model outputs are computed after the solving the differential equation. While this might not be particularly practical, it serves as an nice illustration of the power of automatic differentiation." }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "1a91aff44b93157", + "metadata": {}, + "outputs": [], "source": [ "import jax.numpy as jnp\n", "import diffrax\n", @@ -363,7 +370,7 @@ "]\n", "\n", "# Load parameters for the specified condition\n", - "p = jax_problem.load_parameters(simulation_condition[0])\n", + "p = jax_problem.load_model_parameters(simulation_condition[0])\n", "\n", "\n", "# Define a function to compute the gradient with respect to dynamic timepoints\n", @@ -388,24 +395,24 @@ "# Compute the gradient with respect to `ts_dyn`\n", "g = grad_ts_dyn(ts_dyn)\n", "g" - ], - "id": "1a91aff44b93157" + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "9f870da7754e139c", + "metadata": {}, "source": [ "## Compilation & Profiling\n", "\n", "To maximize performance with JAX, code should be just-in-time (JIT) compiled. This can be achieved using the `jax.jit` or `equinox.filter_jit` decorators. While JIT compilation introduces some overhead during the first function call, it significantly improves performance for subsequent calls. To demonstrate this, we will first clear the JIT cache and then profile the execution." - ], - "id": "9f870da7754e139c" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "58ebdc110ea7457e", + "metadata": {}, + "outputs": [], "source": [ "from time import time\n", "\n", @@ -414,14 +421,14 @@ "\n", "# Define a JIT-compiled gradient function with auxiliary outputs\n", "gradfun = eqx.filter_jit(eqx.filter_grad(run_simulations, has_aux=True))" - ], - "id": "58ebdc110ea7457e" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "e1242075f7e0faf", + "metadata": {}, + "outputs": [], "source": [ "# Measure the time taken for the first function call (including compilation)\n", "start = time()\n", @@ -432,14 +439,14 @@ "start = time()\n", "gradfun(jax_problem)\n", "print(f\"Gradient compilation time: {time() - start:.2f} seconds\")" - ], - "id": "e1242075f7e0faf" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "27181f367ccb1817", + "metadata": {}, + "outputs": [], "source": [ "%%timeit\n", "run_simulations(\n", @@ -452,14 +459,14 @@ " dcoeff=0.0, # recommended value for stiff systems\n", " ),\n", ")" - ], - "id": "27181f367ccb1817" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "5b8d3a6162a3ae55", + "metadata": {}, + "outputs": [], "source": [ "%%timeit \n", "gradfun(\n", @@ -472,14 +479,14 @@ " dcoeff=0.0, # recommended value for stiff systems\n", " ),\n", ")" - ], - "id": "5b8d3a6162a3ae55" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "d733a450635a749b", + "metadata": {}, + "outputs": [], "source": [ "from amici.petab import simulate_petab\n", "import amici\n", @@ -500,8 +507,7 @@ "problem_parameters = dict(\n", " zip(jax_problem.parameter_ids, jax_problem.parameters)\n", ")" - ], - "id": "d733a450635a749b" + ] }, { "cell_type": "code", From 2fb392c9ccb408999d492f2c250b19cc766ffcda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sun, 8 Dec 2024 14:21:24 +0000 Subject: [PATCH 26/32] ignore test warning --- .github/workflows/test_petab_sciml.yml | 1 - pytest.ini | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_petab_sciml.yml b/.github/workflows/test_petab_sciml.yml index 330a9982f9..c84d7c2f3e 100644 --- a/.github/workflows/test_petab_sciml.yml +++ b/.github/workflows/test_petab_sciml.yml @@ -74,7 +74,6 @@ jobs: - name: Run PEtab SciML testsuite run: | source ./venv/bin/activate \ - && pytest --cov-report=xml:coverage.xml \ --cov=./ python/tests/test_*petab*.py python/tests/sciml/ diff --git a/pytest.ini b/pytest.ini index 8cc45e0fd9..29463d5b09 100644 --- a/pytest.ini +++ b/pytest.ini @@ -12,6 +12,7 @@ filterwarnings = ignore:Conservation laws for non-constant species in models with Species-AssignmentRules are currently not supported and will be turned off.:UserWarning ignore:Conservation laws for non-constant species in combination with parameterized stoichiometric coefficients are not currently supported and will be turned off.:UserWarning ignore:Support for PEtab2.0 is experimental!:UserWarning + ignore:PEtab v2.0.0 mapping tables are only partially supported:UserWarning ignore:The JAX module is experimental and the API may change in the future.:ImportWarning # hundreds of SBML <=5.17 warnings ignore:.*inspect.getargspec\(\) is deprecated.*:DeprecationWarning From 4f3aff30a085be1b15ae102a21d0bbcc51c2803e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sun, 8 Dec 2024 14:44:06 +0000 Subject: [PATCH 27/32] Update test_petab_sciml.yml --- .github/workflows/test_petab_sciml.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_petab_sciml.yml b/.github/workflows/test_petab_sciml.yml index c84d7c2f3e..ddc91fcbf4 100644 --- a/.github/workflows/test_petab_sciml.yml +++ b/.github/workflows/test_petab_sciml.yml @@ -75,7 +75,7 @@ jobs: run: | source ./venv/bin/activate \ && pytest --cov-report=xml:coverage.xml \ - --cov=./ python/tests/test_*petab*.py python/tests/sciml/ + --cov=./ python/tests/sciml/test_sciml.py - name: Codecov if: github.event_name == 'pull_request' || github.repository_owner == 'AMICI-dev' From 9c3fd4d7d258fd8401261b8e656c3d1bd91fa784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sun, 8 Dec 2024 17:59:13 +0000 Subject: [PATCH 28/32] add hybridisation support --- python/sdist/amici/de_model.py | 132 +++++++++++++++++++++++ python/sdist/amici/jax/jaxcodeprinter.py | 3 + python/sdist/amici/jax/model.py | 2 +- python/sdist/amici/jax/petab.py | 9 +- python/sdist/amici/petab/petab_import.py | 20 +++- python/sdist/amici/sbml_import.py | 7 +- tests/sciml/test_sciml.py | 48 ++++----- 7 files changed, 186 insertions(+), 35 deletions(-) diff --git a/python/sdist/amici/de_model.py b/python/sdist/amici/de_model.py index 8ad2e7a998..0e6f43dbf7 100644 --- a/python/sdist/amici/de_model.py +++ b/python/sdist/amici/de_model.py @@ -28,10 +28,12 @@ AlgebraicEquation, Observable, EventObservable, + Sigma, SigmaY, SigmaZ, Parameter, Constant, + LogLikelihood, LogLikelihoodY, LogLikelihoodZ, LogLikelihoodRZ, @@ -199,6 +201,7 @@ def __init__( verbose: bool | int | None = False, simplify: Callable | None = _default_simplify, cache_simplify: bool = False, + hybridisation: bool = False, ): """ Create a new DEModel instance. @@ -2305,3 +2308,132 @@ def _process_heavisides( dxdt = dxdt.subs(heaviside_sympy, heaviside_amici) return dxdt + + @property + def _components(self) -> list[ModelQuantity]: + """ + Returns the components of the model + + :return: + components of the model + """ + return ( + self._algebraic_states + + self._algebraic_equations + + self._conservation_laws + + self._constants + + self._differential_states + + self._event_observables + + self._events + + self._expressions + + self._log_likelihood_ys + + self._log_likelihood_zs + + self._log_likelihood_rzs + + self._observables + + self._parameters + + self._sigma_ys + + self._sigma_zs + + self._splines + ) + + def _process_hybridisation(self, hybridisation: dict) -> None: + """ + Parses the hybridisation information and updates the model accordingly + + :param hybridisation: + hybridisation information + """ + added_expressions = False + for net_id, net in hybridisation.items(): + if not (net["output"] == "ode" or net["input"] == "ode"): + continue # do not integrate into ODEs, handle in amici.jax.petab + inputs = [ + comp + for comp in self._components + if str(comp.get_id()) in net["input_vars"] + ] + # sort inputs by order in input_vars + inputs = sorted( + inputs, + key=lambda comp: net["input_vars"].index(str(comp.get_id())), + ) + if len(inputs) != len(net["input_vars"]): + raise ValueError( + f"Could not find all input variables for neural network {net_id}" + ) + for inp in inputs: + if isinstance( + inp, + Sigma + | LogLikelihood + | Event + | ConservationLaw + | Observable, + ): + raise NotImplementedError( + f"{inp.get_name()} ({type(inp)}) is not supported as neural network input." + ) + + outputs = { + out_var: comp + for comp in self._components + if (out_var := str(comp.get_id())) in net["output_vars"] + # TODO: SYNTAX NEEDS to CHANGE + or (out_var := str(comp.get_id()) + "_dot") + in net["output_vars"] + } + if len(outputs.keys()) != len(net["output_vars"]): + raise ValueError( + f"Could not find all output variables for neural network {net_id}" + ) + for iout, (out_var, comp) in enumerate(outputs.items()): + # remove output from model components + if isinstance(comp, Parameter): + self._parameters.remove(comp) + elif isinstance(comp, Expression): + self._expressions.remove(comp) + elif isinstance(comp, DifferentialState): + pass + else: + raise NotImplementedError( + f"{comp.get_name()} ({type(comp)}) is not supported as neural network output." + ) + + # generate dummy Function + out_val = sp.Function(net_id)(*inputs, iout) + + # add to the model + if isinstance(comp, DifferentialState): + ix = self._differential_states.index(comp) + # TODO: SYNTAX NEEDS to CHANGE + if out_var.endswith("_dot"): + self._differential_states[ix].set_dt(out_val) + else: + self._differential_states[ix].set_val(out_val) + else: + self.add_component( + Expression( + identifier=comp.get_id(), + name=net_id, + value=out_val, + ) + ) + added_expressions = True + + if added_expressions: + # toposort expressions + w_sorted = toposort_symbols( + dict( + zip( + self.sym("w"), + self.eq("w"), + strict=True, + ) + ) + ) + old_syms = tuple(self._syms["w"]) + topo_expr_syms = tuple(w_sorted.keys()) + new_order = [old_syms.index(s) for s in topo_expr_syms] + self._expressions = [self._expressions[i] for i in new_order] + self._syms["w"] = sp.Matrix(topo_expr_syms) + self._eqs["w"] = sp.Matrix(list(w_sorted.values())) diff --git a/python/sdist/amici/jax/jaxcodeprinter.py b/python/sdist/amici/jax/jaxcodeprinter.py index 6cfce97b35..2b7d149c81 100644 --- a/python/sdist/amici/jax/jaxcodeprinter.py +++ b/python/sdist/amici/jax/jaxcodeprinter.py @@ -36,6 +36,9 @@ def _print_Mul(self, expr: sp.Expr) -> str: return super()._print_Mul(expr) return f"safe_div({self.doprint(numer)}, {self.doprint(denom)})" + def _print_Function(self, expr): + return f"self.nns['{expr.func.__name__}'].forward(jnp.array([{', '.join(self.doprint(a) for a in expr.args[:-1])}]))[{expr.args[-1]}]" + def _get_sym_lines( self, symbols: sp.Matrix | Iterable[str], diff --git a/python/sdist/amici/jax/model.py b/python/sdist/amici/jax/model.py index 51923fd517..035358d1b2 100644 --- a/python/sdist/amici/jax/model.py +++ b/python/sdist/amici/jax/model.py @@ -439,7 +439,7 @@ def _sigmays( in_axes=(0, 0, None, None, 0), )(ts, xs, p, tcl, iys) - @eqx.filter_jit + # @eqx.filter_jit def simulate_condition( self, p: jt.Float[jt.Array, "np"], diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index 55686a719b..16774a6e3f 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -416,12 +416,6 @@ def _eval_nn(self, output_par: str): .to_dict() ) - for petab_id in model_id_map.values(): - if petab_id in self.model.state_ids: - raise NotImplementedError( - "State variables as inputs to neural networks are not supported" - ) - net_input = jnp.array( [ jax.lax.stop_gradient(self._inputs[net_id][model_id]) @@ -494,6 +488,9 @@ def _state_needs_reinitialisation( :return: True if state needs reinitialisation, False otherwise """ + if state_id in self.nn_output_ids: + return True + if state_id not in self._petab_problem.condition_df: return False xval = self._petab_problem.condition_df.loc[ diff --git a/python/sdist/amici/petab/petab_import.py b/python/sdist/amici/petab/petab_import.py index c23736cd4a..23ddfcf409 100644 --- a/python/sdist/amici/petab/petab_import.py +++ b/python/sdist/amici/petab/petab_import.py @@ -166,13 +166,31 @@ def import_petab_problem( for ml_model in ml_models if ml_model.mlmodel_id == net ), + "input_vars": [ + petab_id + for petab_id, model_id in petab_problem.mapping_df.query( + f"netId == '{net}'" + )[petab.MODEL_ENTITY_ID] + .to_dict() + .items() + if model_id.startswith("input") + ], + "output_vars": [ + petab_id + for petab_id, model_id in petab_problem.mapping_df.query( + f"netId == '{net}'" + )[petab.MODEL_ENTITY_ID] + .to_dict() + .items() + if model_id.startswith("output") + ], **hybrid, } for net, hybrid in config["hybridization"].items() } if not jax or petab_problem.model.type_id == MODEL_TYPE_PYSB: raise NotImplementedError( - "petab_sciml extension is currently only supported for JAX models" + "petab_sciml extension is currently only supported for sbml models" ) else: hybridisation = None diff --git a/python/sdist/amici/sbml_import.py b/python/sdist/amici/sbml_import.py index 9e66a5d924..3a7678224d 100644 --- a/python/sdist/amici/sbml_import.py +++ b/python/sdist/amici/sbml_import.py @@ -287,7 +287,6 @@ def sbml2amici( log_as_log10: bool = True, generate_sensitivity_code: bool = True, hardcode_symbols: Sequence[str] = None, - hybridisation: dict = None, ) -> None: """ Generate and compile AMICI C++ files for the model provided to the @@ -435,7 +434,6 @@ def sbml2amici( compiler=compiler, allow_reinit_fixpar_initcond=allow_reinit_fixpar_initcond, generate_sensitivity_code=generate_sensitivity_code, - hybridisation=hybridisation, ) exporter.generate_model_code() @@ -541,6 +539,7 @@ def sbml2jax( simplify=simplify, cache_simplify=cache_simplify, log_as_log10=log_as_log10, + hybridisation=hybridisation, ) from amici.jax.ode_export import ODEExporter @@ -569,6 +568,7 @@ def _build_ode_model( cache_simplify: bool = False, log_as_log10: bool = True, hardcode_symbols: Sequence[str] = None, + hybridisation: dict = None, ) -> DEModel: """Generate an ODEModel from this SBML model. @@ -731,6 +731,9 @@ def _build_ode_model( if compute_conservation_laws: self._process_conservation_laws(ode_model) + if hybridisation: + ode_model._process_hybridisation(hybridisation) + # fill in 'self._sym' based on prototypes and components in ode_model ode_model.generate_basic_variables() diff --git a/tests/sciml/test_sciml.py b/tests/sciml/test_sciml.py index c6a98fd8cc..9ce9929981 100644 --- a/tests/sciml/test_sciml.py +++ b/tests/sciml/test_sciml.py @@ -139,7 +139,7 @@ def test_net(test): ) w = _reshape_flat_array(df) if isinstance(net.layers[layer], eqx.nn.ConvTranspose): - # see FAQ in https://docs.kidger.site/equinox/api/nn/conv/#equinox.nn.ConvTranspose + # see FAQ in https://docs.kidger.site/equinox/api/nn/conv/#equinox.nn.ConvTranspose w = np.flip( w, axis=tuple(range(2, w.ndim)) ).swapaxes(0, 1) @@ -268,17 +268,8 @@ def test_ude(test): jax_problem = JAXProblem(jax_model, petab_problem) # llh - if test in ( - "001", "004", - "005", - "008", - "010", - "011", - "012", - "013", - "014", "016", ): with pytest.raises(NotImplementedError): @@ -316,16 +307,12 @@ def test_ude(test): controller=diffrax.PIDController(atol=1e-14, rtol=1e-14), max_steps=2**16, ) - expected = ( - pd.concat( - [ - pd.read_csv(test_dir / simulation, sep="\t") - for simulation in solutions["grad_llh_files"] - ] - ) - .set_index(petab.PARAMETER_ID) - .sort_index() - ) + expected = pd.concat( + [ + pd.read_csv(test_dir / simulation, sep="\t") + for simulation in solutions["grad_llh_files"] + ] + ).set_index(petab.PARAMETER_ID) actual_dict = {} for ip in expected.index: if ip in jax_problem.parameter_ids: @@ -337,12 +324,23 @@ def test_ude(test): layer = ip.split("_")[1] attribute = ip.split("_")[2] index = tuple(np.array(ip.split("_")[3:]).astype(int)) - actual_dict[ip] = getattr( - sllh.model.nns[net].layers[layer], attribute - )[*index].item() - actual = pd.Series(actual_dict).sort_index() + + attr_grad = getattr(sllh.model.nns[net].layers[layer], attribute) + if ( + isinstance( + sllh.model.nns[net].layers[layer], eqx.nn.ConvTranspose + ) + and attribute == "weight" + ): + # invert np.flip(w, axis=tuple(range(2, w.ndim))).swapaxes(0, 1) + attr_grad = np.flip( + attr_grad.swapaxes(0, 1), + axis=tuple(range(2, attr_grad.ndim)), + ) + actual_dict[ip] = attr_grad[*index].item() + actual = pd.Series(actual_dict).loc[expected.index].values np.testing.assert_allclose( - actual.values, + actual, expected["value"].values, atol=solutions["tol_grad_llh"], rtol=solutions["tol_grad_llh"], From 1349ddb1f1b8fe77d317fc8ca6ed362f8aabb6cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Sun, 8 Dec 2024 18:29:13 +0000 Subject: [PATCH 29/32] fix workflow --- .github/workflows/test_petab_sciml.yml | 2 +- python/sdist/amici/jax/model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_petab_sciml.yml b/.github/workflows/test_petab_sciml.yml index ddc91fcbf4..eb8ca39394 100644 --- a/.github/workflows/test_petab_sciml.yml +++ b/.github/workflows/test_petab_sciml.yml @@ -75,7 +75,7 @@ jobs: run: | source ./venv/bin/activate \ && pytest --cov-report=xml:coverage.xml \ - --cov=./ python/tests/sciml/test_sciml.py + --cov=./ tests/sciml/test_sciml.py - name: Codecov if: github.event_name == 'pull_request' || github.repository_owner == 'AMICI-dev' diff --git a/python/sdist/amici/jax/model.py b/python/sdist/amici/jax/model.py index 035358d1b2..51923fd517 100644 --- a/python/sdist/amici/jax/model.py +++ b/python/sdist/amici/jax/model.py @@ -439,7 +439,7 @@ def _sigmays( in_axes=(0, 0, None, None, 0), )(ts, xs, p, tcl, iys) - # @eqx.filter_jit + @eqx.filter_jit def simulate_condition( self, p: jt.Float[jt.Array, "np"], From 4596dc494269b910e84809e2cac8625ec6b5bb84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Thu, 19 Dec 2024 16:01:55 +0000 Subject: [PATCH 30/32] update testsuite --- tests/sciml/testsuite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sciml/testsuite b/tests/sciml/testsuite index 4518f54dd6..f836be3852 160000 --- a/tests/sciml/testsuite +++ b/tests/sciml/testsuite @@ -1 +1 @@ -Subproject commit 4518f54dd62c1256fb1803b9f5e9817f4f78c26d +Subproject commit f836be38526da0850f0e540010accc94217bdf53 From bd103dbe52cae05c01b14ccd011edbc1e5869d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Thu, 19 Dec 2024 18:01:26 +0000 Subject: [PATCH 31/32] update after test refactor --- python/sdist/amici/de_model.py | 9 +- python/sdist/amici/jax/ode_export.py | 9 +- python/sdist/amici/jax/petab.py | 65 ++++++--- python/sdist/amici/petab/petab_import.py | 47 +++---- tests/sciml/test_sciml.py | 165 ++++++++++++----------- 5 files changed, 166 insertions(+), 129 deletions(-) diff --git a/python/sdist/amici/de_model.py b/python/sdist/amici/de_model.py index 0e6f43dbf7..5c0c0ff7b5 100644 --- a/python/sdist/amici/de_model.py +++ b/python/sdist/amici/de_model.py @@ -2345,7 +2345,10 @@ def _process_hybridisation(self, hybridisation: dict) -> None: """ added_expressions = False for net_id, net in hybridisation.items(): - if not (net["output"] == "ode" or net["input"] == "ode"): + if not ( + net["hybridization"]["output"] == "ode" + or net["hybridization"]["input"] == "ode" + ): continue # do not integrate into ODEs, handle in amici.jax.petab inputs = [ comp @@ -2400,7 +2403,9 @@ def _process_hybridisation(self, hybridisation: dict) -> None: ) # generate dummy Function - out_val = sp.Function(net_id)(*inputs, iout) + out_val = sp.Function(net_id)( + *[input.get_id() for input in inputs], iout + ) # add to the model if isinstance(comp, DifferentialState): diff --git a/python/sdist/amici/jax/ode_export.py b/python/sdist/amici/jax/ode_export.py index 08ba8bc0cd..a374042f4a 100644 --- a/python/sdist/amici/jax/ode_export.py +++ b/python/sdist/amici/jax/ode_export.py @@ -256,10 +256,11 @@ def _generate_jax_code(self) -> None: def _generate_nn_code(self) -> None: for net_name, net in self.hybridisation.items(): - generate_equinox( - net["model"], - self.model_path / f"{net_name}.py", - ) + for model in net["model"]: + generate_equinox( + model, + self.model_path / f"{net_name}.py", + ) def set_paths(self, output_dir: str | Path | None = None) -> None: """ diff --git a/python/sdist/amici/jax/petab.py b/python/sdist/amici/jax/petab.py index d23c5a1b5e..5439d37092 100644 --- a/python/sdist/amici/jax/petab.py +++ b/python/sdist/amici/jax/petab.py @@ -282,14 +282,36 @@ def _get_nominal_parameter_values( } # extract nominal values from petab problem for pname, row in self._petab_problem.parameter_df.iterrows(): - if (net := pname.split("_")[0]) in model.nns: + if (net := pname.split(".")[0]) in model.nns: + to_set = [] nn = model_pars[net] - layer = nn[pname.split("_")[1]] - attribute = pname.split("_")[2] - index = tuple(np.array(pname.split("_")[3:]).astype(int)) - layer[attribute] = ( - layer[attribute].at[index].set(row[petab.NOMINAL_VALUE]) - ) + if len(pname.split(".")) > 1: + layer = nn[pname.split(".")[1]] + if len(pname.split(".")) > 2: + to_set.append( + (pname.split(".")[1], pname.split(".")[2]) + ) + else: + to_set.extend( + [ + (pname.split(".")[1], attribute) + for attribute in layer.keys() + ] + ) + else: + to_set.extend( + [ + (layer_name, attribute) + for layer_name, layer in nn.items() + for attribute in layer.keys() + ] + ) + + for layer, attribute in to_set: + nn[layer][attribute] = row[ + petab.NOMINAL_VALUE + ] * jnp.ones_like(nn[layer][attribute]) + # set values in model for net_id in model_pars: for layer_id in model_pars[net_id]: @@ -316,14 +338,9 @@ def _get_nominal_parameter_values( ), model def _get_inputs(self): - if ( - self._petab_problem.mapping_df is None - or "netId" not in self._petab_problem.mapping_df.columns - ): + if self._petab_problem.mapping_df is None: return {} - inputs = { - net: {} for net in self._petab_problem.mapping_df["netId"].unique() - } + inputs = {net: {} for net in self.model.nns.keys()} for petab_id, row in self._petab_problem.mapping_df.iterrows(): if (filepath := Path(petab_id)).is_file(): data_flat = pd.read_csv(filepath, sep="\t").sort_values( @@ -368,9 +385,10 @@ def nn_output_ids(self) -> list[str]: if self._petab_problem.mapping_df is None: return [] return self._petab_problem.mapping_df[ - self._petab_problem.mapping_df[ - petab.MODEL_ENTITY_ID - ].str.startswith("output") + self._petab_problem.mapping_df[petab.MODEL_ENTITY_ID] + .str.split(".") + .str[1] + .str.startswith("output") ].index.tolist() def get_petab_parameter_by_id(self, name: str) -> jnp.float_: @@ -402,11 +420,18 @@ def _unscale( ) def _eval_nn(self, output_par: str): - net_id = self._petab_problem.mapping_df.loc[output_par, "netId"] + net_id = self._petab_problem.mapping_df.loc[ + output_par, petab.MODEL_ENTITY_ID + ].split(".")[0] nn = self.model.nns[net_id] model_id_map = ( - self._petab_problem.mapping_df.query(f'netId == "{net_id}"') + self._petab_problem.mapping_df[ + self._petab_problem.mapping_df[petab.MODEL_ENTITY_ID] + .str.split(".") + .str[0] + == net_id + ] .reset_index() .set_index(petab.MODEL_ENTITY_ID)[petab.PETAB_ENTITY_ID] .to_dict() @@ -422,7 +447,7 @@ def _eval_nn(self, output_par: str): petab_id, petab.NOMINAL_VALUE ] for model_id, petab_id in model_id_map.items() - if model_id.startswith("input") + if model_id.split(".")[1].startswith("input") ] ) return nn.forward(net_input).squeeze() diff --git a/python/sdist/amici/petab/petab_import.py b/python/sdist/amici/petab/petab_import.py index 23ddfcf409..b536342191 100644 --- a/python/sdist/amici/petab/petab_import.py +++ b/python/sdist/amici/petab/petab_import.py @@ -150,43 +150,40 @@ def import_petab_problem( from petab_sciml import PetabScimlStandard config = petab_problem.extensions_config["petab_sciml"] - net_files = config.get("net_files", []) - # TODO: net files need to be absolute paths - ml_models = [ - model - for net_file in net_files - for model in PetabScimlStandard.load_data( - Path() / net_file - ).models - ] hybridisation = { - net: { - "model": next( - ml_model - for ml_model in ml_models - if ml_model.mlmodel_id == net - ), + net_id: { + "model": PetabScimlStandard.load_data( + Path() / net_config["file"] + ).models, "input_vars": [ petab_id - for petab_id, model_id in petab_problem.mapping_df.query( - f"netId == '{net}'" - )[petab.MODEL_ENTITY_ID] + for petab_id, model_id in petab_problem.mapping_df.loc[ + petab_problem.mapping_df[petab.MODEL_ENTITY_ID] + .str.split(".") + .str[0] + == net_id, + petab.MODEL_ENTITY_ID, + ] .to_dict() .items() - if model_id.startswith("input") + if model_id.split(".")[1].startswith("input") ], "output_vars": [ petab_id - for petab_id, model_id in petab_problem.mapping_df.query( - f"netId == '{net}'" - )[petab.MODEL_ENTITY_ID] + for petab_id, model_id in petab_problem.mapping_df.loc[ + petab_problem.mapping_df[petab.MODEL_ENTITY_ID] + .str.split(".") + .str[0] + == net_id, + petab.MODEL_ENTITY_ID, + ] .to_dict() .items() - if model_id.startswith("output") + if model_id.split(".")[1].startswith("output") ], - **hybrid, + **net_config, } - for net, hybrid in config["hybridization"].items() + for net_id, net_config in config.items() } if not jax or petab_problem.model.type_id == MODEL_TYPE_PYSB: raise NotImplementedError( diff --git a/tests/sciml/test_sciml.py b/tests/sciml/test_sciml.py index 9ce9929981..e99b0a88cc 100644 --- a/tests/sciml/test_sciml.py +++ b/tests/sciml/test_sciml.py @@ -19,6 +19,7 @@ import numpy as np import equinox as eqx import os +import h5py from contextlib import contextmanager from petab_sciml import PetabScimlStandard @@ -208,55 +209,27 @@ def test_ude(test): solutions = safe_load(f) with change_directory(test_dir / "petab"): + from petab.v2 import Problem + petab_yaml["format_version"] = "2.0.0" for problem in petab_yaml["problems"]: problem["model_files"] = { - file.split(".")[0]: { - "language": "sbml", - "location": file, - } - for file in problem.pop("sbml_files") + problem["model_files"]["location"].split(".")[0]: problem[ + "model_files" + ] } - problem["mapping_files"] = [problem.pop("mapping_tables")] - for mapping_file in problem["mapping_files"]: df = pd.read_csv( mapping_file, sep="\t", ) - df.rename( - columns={ - "ioId": petab.MODEL_ENTITY_ID, - "ioValue": petab.PETAB_ENTITY_ID, - } - ).to_csv(mapping_file, sep="\t", index=False) - for observable_file in problem["observable_files"]: - df = pd.read_csv(observable_file, sep="\t") - df[petab.OBSERVABLE_ID] = df[petab.OBSERVABLE_ID].map( - lambda x: x + "_o" if not x.endswith("_o") else x - ) - df.to_csv(observable_file, sep="\t", index=False) - for measurement_file in problem["measurement_files"]: - df = pd.read_csv(measurement_file, sep="\t") - df[petab.OBSERVABLE_ID] = df[petab.OBSERVABLE_ID].map( - lambda x: x + "_o" if not x.endswith("_o") else x - ) - df.to_csv(measurement_file, sep="\t", index=False) - - petab_yaml["parameter_file"] = [ - petab_yaml["parameter_file"], - petab_yaml["parameter_file"].replace("ude", "nn"), - ] - df = pd.read_csv(petab_yaml["parameter_file"][1], sep="\t") - df.rename( - columns={ - "value": petab.NOMINAL_VALUE, - }, - inplace=True, - ) - df.to_csv(petab_yaml["parameter_file"][1], sep="\t", index=False) - - from petab.v2 import Problem + if df[petab.PETAB_ENTITY_ID].str.startswith("net").any(): + df.rename( + columns={ + petab.PETAB_ENTITY_ID: petab.MODEL_ENTITY_ID, + petab.MODEL_ENTITY_ID: petab.PETAB_ENTITY_ID, + } + ).to_csv(mapping_file, sep="\t", index=False) petab_problem = Problem.from_yaml(petab_yaml) jax_model = import_petab_problem( @@ -266,6 +239,35 @@ def test_ude(test): jax=True, ) jax_problem = JAXProblem(jax_model, petab_problem) + for net, net_config in petab_problem.extensions_config[ + "petab_sciml" + ].items(): + pars = h5py.File( + net_config["parameters"].replace(".h5", ".hf5"), "r" + ) + for layer_name, layer in jax_problem.model.nns[net].layers.items(): + for attribute in dir(layer): + if not isinstance( + getattr(layer, attribute), jax.numpy.ndarray + ): + continue + value = jnp.array(pars[layer_name][attribute]) + + if ( + isinstance(layer, eqx.nn.ConvTranspose) + and attribute == "weight" + ): + # see FAQ in https://docs.kidger.site/equinox/api/nn/conv/#equinox.nn.ConvTranspose + value = jnp.flip( + value, axis=tuple(range(2, value.ndim)) + ).swapaxes(0, 1) + jax_problem = eqx.tree_at( + lambda x: getattr( + x.model.nns[net].layers[layer_name], attribute + ), + jax_problem, + value, + ) # llh if test in ( @@ -307,41 +309,48 @@ def test_ude(test): controller=diffrax.PIDController(atol=1e-14, rtol=1e-14), max_steps=2**16, ) - expected = pd.concat( - [ - pd.read_csv(test_dir / simulation, sep="\t") - for simulation in solutions["grad_llh_files"] - ] - ).set_index(petab.PARAMETER_ID) - actual_dict = {} - for ip in expected.index: - if ip in jax_problem.parameter_ids: - actual_dict[ip] = sllh.parameters[ - jax_problem.parameter_ids.index(ip) - ].item() - if ip.split("_")[0] in jax_problem.model.nns: - net = ip.split("_")[0] - layer = ip.split("_")[1] - attribute = ip.split("_")[2] - index = tuple(np.array(ip.split("_")[3:]).astype(int)) - - attr_grad = getattr(sllh.model.nns[net].layers[layer], attribute) - if ( - isinstance( - sllh.model.nns[net].layers[layer], eqx.nn.ConvTranspose - ) - and attribute == "weight" - ): - # invert np.flip(w, axis=tuple(range(2, w.ndim))).swapaxes(0, 1) - attr_grad = np.flip( - attr_grad.swapaxes(0, 1), - axis=tuple(range(2, attr_grad.ndim)), - ) - actual_dict[ip] = attr_grad[*index].item() - actual = pd.Series(actual_dict).loc[expected.index].values - np.testing.assert_allclose( - actual, - expected["value"].values, - atol=solutions["tol_grad_llh"], - rtol=solutions["tol_grad_llh"], - ) + for component, file in solutions["grad_llh_files"].items(): + actual_dict = {} + if component == "mech": + expected = pd.read_csv(test_dir / file, sep="\t").set_index( + petab.PARAMETER_ID + ) + for ip in expected.index: + if ip in jax_problem.parameter_ids: + actual_dict[ip] = sllh.parameters[ + jax_problem.parameter_ids.index(ip) + ].item() + actual = pd.Series(actual_dict).loc[expected.index].values + np.testing.assert_allclose( + actual, + expected["value"].values, + atol=solutions["tol_grad_llh"], + rtol=solutions["tol_grad_llh"], + ) + else: + expected = h5py.File(test_dir / file, "r") + for layer_name, layer in jax_problem.model.nns[ + component + ].layers.items(): + for attribute in dir(layer): + if not isinstance( + getattr(layer, attribute), jax.numpy.ndarray + ): + continue + actual = getattr( + sllh.model.nns[component].layers[layer_name], attribute + ) + if ( + isinstance(layer, eqx.nn.ConvTranspose) + and attribute == "weight" + ): + actual = np.flip( + actual.swapaxes(0, 1), + axis=tuple(range(2, actual.ndim)), + ) + np.testing.assert_allclose( + actual, + expected[layer_name][attribute][:], + atol=solutions["tol_grad_llh"], + rtol=solutions["tol_grad_llh"], + ) From 2b6308d5c6deef96f501287b2589206c8acdb141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Mon, 27 Jan 2025 16:08:09 +0000 Subject: [PATCH 32/32] fix hybridization --- python/sdist/amici/de_model.py | 8 ++++---- python/sdist/amici/petab/petab_import.py | 6 +++--- python/sdist/amici/petab/sbml_import.py | 5 +++++ python/sdist/amici/sbml_import.py | 12 ++++++------ 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/python/sdist/amici/de_model.py b/python/sdist/amici/de_model.py index 5c0c0ff7b5..4f4f9f466b 100644 --- a/python/sdist/amici/de_model.py +++ b/python/sdist/amici/de_model.py @@ -2336,15 +2336,15 @@ def _components(self) -> list[ModelQuantity]: + self._splines ) - def _process_hybridisation(self, hybridisation: dict) -> None: + def _process_hybridization(self, hybridization: dict) -> None: """ Parses the hybridisation information and updates the model accordingly - :param hybridisation: - hybridisation information + :param hybridization: + hybridization information """ added_expressions = False - for net_id, net in hybridisation.items(): + for net_id, net in hybridization.items(): if not ( net["hybridization"]["output"] == "ode" or net["hybridization"]["input"] == "ode" diff --git a/python/sdist/amici/petab/petab_import.py b/python/sdist/amici/petab/petab_import.py index b536342191..a13ab5c4d9 100644 --- a/python/sdist/amici/petab/petab_import.py +++ b/python/sdist/amici/petab/petab_import.py @@ -150,7 +150,7 @@ def import_petab_problem( from petab_sciml import PetabScimlStandard config = petab_problem.extensions_config["petab_sciml"] - hybridisation = { + hybridization = { net_id: { "model": PetabScimlStandard.load_data( Path() / net_config["file"] @@ -190,7 +190,7 @@ def import_petab_problem( "petab_sciml extension is currently only supported for sbml models" ) else: - hybridisation = None + hybridization = None # compile the model if petab_problem.model.type_id == MODEL_TYPE_PYSB: @@ -207,7 +207,7 @@ def import_petab_problem( model_name=model_name, model_output_dir=model_output_dir, non_estimated_parameters_as_constants=non_estimated_parameters_as_constants, - hybridisation=hybridisation, + hybridization=hybridization, jax=jax, **kwargs, ) diff --git a/python/sdist/amici/petab/sbml_import.py b/python/sdist/amici/petab/sbml_import.py index 52376c3324..d9659e5fd6 100644 --- a/python/sdist/amici/petab/sbml_import.py +++ b/python/sdist/amici/petab/sbml_import.py @@ -381,10 +381,15 @@ def import_model_sbml( sigmas=sigmas, noise_distributions=noise_distrs, verbose=verbose, + hybridization=hybridization, **kwargs, ) return sbml_importer else: + if hybridization: + raise NotImplementedError( + "Hybridization is currently only supported for JAX models." + ) sbml_importer.sbml2amici( model_name=model_name, output_dir=model_output_dir, diff --git a/python/sdist/amici/sbml_import.py b/python/sdist/amici/sbml_import.py index 3a7678224d..df540ba1da 100644 --- a/python/sdist/amici/sbml_import.py +++ b/python/sdist/amici/sbml_import.py @@ -458,7 +458,7 @@ def sbml2jax( simplify: Callable | None = _default_simplify, cache_simplify: bool = False, log_as_log10: bool = True, - hybridisation: dict = None, + hybridization: dict = None, ) -> None: """ Generate and compile AMICI jax files for the model provided to the @@ -539,7 +539,7 @@ def sbml2jax( simplify=simplify, cache_simplify=cache_simplify, log_as_log10=log_as_log10, - hybridisation=hybridisation, + hybridization=hybridization, ) from amici.jax.ode_export import ODEExporter @@ -549,7 +549,7 @@ def sbml2jax( model_name=model_name, outdir=output_dir, verbose=verbose, - hybridisation=hybridisation, + hybridisation=hybridization, ) exporter.generate_model_code() @@ -568,7 +568,7 @@ def _build_ode_model( cache_simplify: bool = False, log_as_log10: bool = True, hardcode_symbols: Sequence[str] = None, - hybridisation: dict = None, + hybridization: dict = None, ) -> DEModel: """Generate an ODEModel from this SBML model. @@ -731,8 +731,8 @@ def _build_ode_model( if compute_conservation_laws: self._process_conservation_laws(ode_model) - if hybridisation: - ode_model._process_hybridisation(hybridisation) + if hybridization: + ode_model._process_hybridization(hybridization) # fill in 'self._sym' based on prototypes and components in ode_model ode_model.generate_basic_variables()