diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 00000000..97b3f5b5 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,29 @@ +name: CI - Ruff Linting + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + ruff: + name: Ruff Linting + runs-on: ubuntu-latest + if: | + github.event_name == 'push' || + ( + github.event_name == 'pull_request' && + github.repository != github.event.pull_request.head.repo.full_name + ) + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Ruff + uses: astral-sh/ruff-action@v3 + with: + version: "0.15.6" + args: "check" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2bc61f88..d5c0a7a4 100644 --- a/.gitignore +++ b/.gitignore @@ -129,6 +129,7 @@ venv/ ENV/ env.bak/ venv.bak/ +*env/ # Spyder project settings .spyderproject @@ -164,4 +165,4 @@ cython_debug/ *.sif *.bak -docs/source/generated/ +docs/source/generated/ \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index d67937d2..d3fcfc1f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -78,7 +78,6 @@ "cv2", "pandas", "hydra", - "omegaconf", "jaxtyping", "plotly", "optree", diff --git a/examples/dandi_experanto_example.ipynb b/examples/dandi_experanto_example.ipynb index bfd880ab..4df05582 100644 --- a/examples/dandi_experanto_example.ipynb +++ b/examples/dandi_experanto_example.ipynb @@ -137,7 +137,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "id": "QYrgfZC2J573" }, @@ -158,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "id": "t8hE0WuTO10X" }, @@ -189,7 +189,7 @@ " elif \"ImageSeries\" in t or \"TwoPhotonSeries\" in t:\n", " modalities[name.lower()] = \"ScreenInterpolator\"\n", "\n", - " for mod_name, module in nwb.processing.items():\n", + " for _mod_name, module in nwb.processing.items():\n", " for dname, dobj in module.data_interfaces.items():\n", " t = type(dobj).__name__\n", " if \"LFP\" in t:\n", @@ -1149,7 +1149,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -1180,7 +1180,7 @@ " \"50 ms — balanced (default-like)\",\n", " \"200 ms — smooth firing rate estimate, low resolution\"]\n", "\n", - "for ax, w, label in zip(axes, windows, labels):\n", + "for ax, w, label in zip(axes, windows, labels, strict=False):\n", " si = SpikeInterpolator(\n", " \"experanto_000623/spikes\",\n", " cache_data=True,\n", @@ -1214,7 +1214,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", @@ -1247,7 +1247,7 @@ "seq_lfp = SequenceInterpolator(\"experanto_000623/lfp_macro\")\n", "t_window = np.linspace(t_623[0], t_623[0] + 10, max(n_points)) # 10s window\n", "\n", - "for ax, n, label in zip(axes, n_points, labels):\n", + "for ax, n, label in zip(axes, n_points, labels, strict=False):\n", " t_query = np.linspace(t_623[0], t_623[0] + 10, n)\n", " result = seq_lfp.interpolate(t_query)\n", " ax.plot(t_query, result[:, 0], lw=0.8)\n", diff --git a/examples/demo.ipynb b/examples/demo.ipynb index 1acb502e..5cda3d82 100644 --- a/examples/demo.ipynb +++ b/examples/demo.ipynb @@ -22,7 +22,6 @@ "\n", "# Standard imports\n", "import sys\n", - "import os\n", "from pathlib import Path\n", "\n", "# Add project root to path if needed\n", @@ -45,13 +44,9 @@ "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", - "from os import path\n", "\n", - "from tqdm import tqdm\n", - "import torch\n", - "from omegaconf import OmegaConf, open_dict\n", + "from omegaconf import OmegaConf\n", "\n", - "from experanto.datasets import ChunkDataset\n", "from experanto.dataloaders import get_multisession_dataloader" ] }, @@ -198,7 +193,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "3eb3e584-077f-4124-abb2-4fb8d43070c0", "metadata": {}, "outputs": [ @@ -211,7 +206,7 @@ } ], "source": [ - "ls /data/test_upsampling_without_hamming_30.0Hz/dynamic29515-10-12-Video-021a75e56847d574b9acbcc06c675055_30hz" + "!ls /data/test_upsampling_without_hamming_30.0Hz/dynamic29515-10-12-Video-021a75e56847d574b9acbcc06c675055_30hz" ] }, { @@ -238,7 +233,6 @@ } ], "source": [ - "from experanto.dataloaders import get_multisession_dataloader\n", "\n", "paths = [\"/data/test_upsampling_without_hamming_30.0Hz/dynamic29515-10-12-Video-021a75e56847d574b9acbcc06c675055_30hz\"]\n", "train_dl = get_multisession_dataloader(paths, cfg)" @@ -321,7 +315,7 @@ "fig, axs = plt.subplots(1, 4, figsize=(10, 2))\n", "for i, ax in enumerate(axs.ravel()):\n", " ax.imshow(batch[\"screen\"][i, 0, 0]) # video frames that the mouse sees\n", - " ax.axis(\"off\");" + " ax.axis(\"off\")" ] }, { diff --git a/examples/sensorium/data.ipynb b/examples/sensorium/data.ipynb deleted file mode 100644 index 5324dea4..00000000 --- a/examples/sensorium/data.ipynb +++ /dev/null @@ -1,396 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "0a12c378-bbfb-4e7c-a6d4-ec183f03312c", - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "%load_ext autoreload\n", - "%autoreload 2\n", - "\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import torch" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "3f0c9b3c-a59b-4085-8b30-907608471dae", - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "import os\n", - "\n", - "p = !pwd\n", - "p = os.path.dirname(p[0])\n", - "if p not in sys.path:\n", - " sys.path.append(p)\n", - "\n", - "from experanto.data import Mouse2pChunkedDataset" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "c4b0a0e2-2c11-484e-964e-fe5415417f9e", - "metadata": {}, - "outputs": [], - "source": [ - "root_folder = \"/home/nibecker/datasets/dynamic29228-2-10-Video-konsti_wip05\"\n", - "sampling_rate = 10 # Hz?\n", - "chunk_size = 20\n", - "dataset = Mouse2pChunkedDataset(root_folder, sampling_rate, chunk_size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2459346c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "24" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "len(dataset)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "db09356f-b536-40b7-afc8-653e2201e9a3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "('responses', 'screen', 'treadmill', 'eye_tracker')" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dataset.device_names" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "0bee46f3-a46d-4913-a796-3c81e3fc1d30", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "((20, 7928), (20, 36, 64), (20, 1), (20, 3))" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "res, scr, tr, eye = dataset[0]\n", - "res.shape, scr.shape, tr.shape, eye.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "c8fa6d49-7344-4cd9-b309-c3a1bffdd986", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(20, 36, 64)" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data = dataset[0]\n", - "data.screen.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "168cc158-fe83-45f1-9fbe-67178c44166d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.imshow(scr[-1])" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "e67f6ce6-002e-4578-b4d5-d012e673f371", - "metadata": {}, - "outputs": [], - "source": [ - "from torch.utils.data import DataLoader\n", - "\n", - "dataloader = DataLoader(dataset, batch_size=8, shuffle=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "3ce900af-e377-4b01-aee3-38df29c3fb70", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "torch.Size([8, 20, 7928])" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "minibatch = next(iter(dataloader))\n", - "len(minibatch)\n", - "minibatch[0].shape" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "cf0ee29b-9ebd-424c-bdeb-9fbcb4873622", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "responses: torch.Size([8, 20, 7928])\n", - "screen: torch.Size([8, 20, 36, 64])\n", - "treadmill: torch.Size([8, 20, 1])\n", - "eye_tracker: torch.Size([8, 20, 3])\n" - ] - } - ], - "source": [ - "for d, v in zip(dataset.device_names, minibatch):\n", - " print(\"{}: {}\".format(d, v.shape))" - ] - }, - { - "cell_type": "markdown", - "id": "a17feec7-6e71-4f26-8f5c-d6294cfdf74c", - "metadata": {}, - "source": [ - "# Static image dataset" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "874e0928-a72d-44c0-944b-16874b0c9524", - "metadata": {}, - "outputs": [], - "source": [ - "from experanto.data import Mouse2pStaticImageDataset" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "id": "62300e27-ca66-4d4c-9b11-2451eb13be5f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "8" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "offset = 0.1 # sec\n", - "stim_duration = 0.5 # sec\n", - "train_dataset = Mouse2pStaticImageDataset(root_folder, \"train\", offset, stim_duration)\n", - "len(train_dataset)" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "4cc2be32-1aac-4d2d-91b2-65128a74a6fb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "val_dataset = Mouse2pStaticImageDataset(\n", - " root_folder, \"validation\", offset, stim_duration\n", - ")\n", - "len(val_dataset)" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "f62bab07-1b7e-4d9f-b954-a7b1fd85071f", - "metadata": {}, - "outputs": [], - "source": [ - "train_dataloader = DataLoader(train_dataset, batch_size=8, shuffle=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 60, - "id": "01e0eff3-517f-4997-843b-b48a81ed3d7f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "4" - ] - }, - "execution_count": 60, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "minibatch = next(iter(train_dataloader))\n", - "len(minibatch)" - ] - }, - { - "cell_type": "code", - "execution_count": 61, - "id": "f3d4ade1-adb1-4070-b200-d67b4272fa04", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(torch.Size([8, 7928]), torch.Size([8, 36, 64]), torch.Size([8, 3]))" - ] - }, - "execution_count": 61, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "minibatch.responses.shape, minibatch.screen.shape, minibatch.eye_tracker.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 76, - "id": "239ab84f-4af7-4d9d-88e6-26dad6e65d07", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA7YAAAEYCAYAAABoc2ovAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAACgQElEQVR4nO3dZ9B2V3Xf/+047h2bJhCqqOuRUK8IhChGYBsCARfiybhMnIkTkhnbrxJ7xjOeiZ3YSZyxnRnHZcYlxGBkU2UQCDUk1HuXQCAMxrj3xv/N/975nJVrHV2PSMJ9Jb/vq/Vc9yn77LL2Ps/5rbU/7zOf+cxnRgghhBBCCCGEsKP8vc91AUIIIYQQQgghhM+GvNiGEEIIIYQQQthp8mIbQgghhBBCCGGnyYttCCGEEEIIIYSdJi+2IYQQQgghhBB2mrzYhhBCCCGEEELYafJiG0IIIYQQQghhp8mLbQghhBBCCCGEnebvb3vgU5/61Gl/9Vd/9bS/9Eu/dHHcOeecM+2PfOQj0z7hhBOm/eVf/uUbz/+bv/mbxbW+7Mu+bONx999//7QffPDBaX/mM5+Z9l/+5V8urvXXf/3X0z7yyCOnfcghh0z7Ax/4wLT/5E/+ZNpPe9rTFtc6/PDDxyY+//M/f+P95BOf+MTi31/wBV8w7a/8yq/ceI78vb/X/1/ERz/60WlbX9aj9/jbv/3bxfmW3/v4+8c+9rGNZf+qr/qqrcp8wQUXTPuSSy6Z9u/+7u9O+6Uvfeni/N///d+f9t/93d9N2/Z+ylOeMu3f+73fm/bnfd7nLa71V3/1VxvLWI87WI499thp33vvvZ/Vtf5386Y3vWnaX/iFXzhtx+UYyz5s/Wxj1/H39//+/3A1P/mTPzntq6++etrf+73fO+2LL7542u9+97sX13rWs541bfuz9//DP/zDab/qVa+a9oc//OHFtf7iL/5i2o75o48+euOzWJYv/uIvXlzr9NNPn7Z193Vf93XTto7tv3VcX3nllWMTnb+sfuUZz3jGtB2/tvezn/3sjWXRHmNZr5bT8e85Prtjd4wx3vzmN2887s/+7M+mbZv6HI8//vjiWv7tvPPOm/ZFF120sezWg2Wv2N5f+7Vf2x73ucY+2rVR/be2Y1Jb9Lf1fNcEX/RFXzTtI444Ytqvec1rpn3yyScvrmX7O5Ysi7/X9YHo1//4j/942p/+9Kc3/q5/WOMrvuIrpu08an//nd/5nWk/9NBDi/NPPfXUaf/RH/3RtB977LFpf+pTn5p2bTvvqe/wd8e0Psx5cIx+vHld+7tzqvcbY1nfXlf/ZJ/UP1X/4tzvdW1v687y2qZjLMe1a5Wf+7mfG/uZ7/zO75y2bfPnf/7ni+P+9E//dNrd/OxYdI1X52THtmPO3+2Pa2sk+4dj1rI435x77rnTfu5zn7u4ln7F53eudn487rjjpl37vGv6u+++e9r2M8tonVqOMZZjwzZ6+tOfPm3nen3HGGN8/OMfn7b91nax7qz71772tYtr2a762/e85z3T/uVf/uVpHzhwYNp33HHH4lpf8iVfMm3bwrFkv9OuPrkby/qo+t7R0b0n/uIv/uITnpsvtiGEEEIIIYQQdpq82IYQQgghhBBC2Gm2liIrT1iTBClX8JPzDTfcMO2v+ZqvmfbZZ5897SoP8xO9ch/lAl6rk9rUaynX6SS3Pm8tl8f5jN7f36ucSyynMgTlOt5/TYqsNM/n7WRm9Vresyuz17Xuq7zAMtsnlJErGVRmUmVi3lNJQid5tH+u1dfBUiVUUiX5+xmlJ7aTkpwxemm6dPKkerzXes5znjNt69SxoKSv9gf/przxt3/7t6etPEnpTZVjKR2yD9r/PcfnqD6mGzP+vm19Kd2xb3fyoCrBUgZl6IhsK7+3bN5HaZXhHW9961vbcnmO8jX7ntJM5UjeY4wxHn744Wnfd99907btlYlJDRXp/NX/zdgv9d/bjPsxlm2mhM1wk06WWNEniVJEx2ENKbGf2bbdmNRf17nLsefYsV849p0Ha325bnFMGrqjZLbiMyur7sKL9GEV69JzOumqvrmu+azjtZCIPWyfP/iDP1j8TZnxJz/5yWnbDtZdN3+NMcYjjzwy7bW62G84x9lOa3NM1z6dnLvWRzf3O/49v5Mu13t6Xc/RlzuWakiP99R/20+6edv5ZYxl2JLPZUiS1/WY6pMsp+GIltE5qvo7z3E8uJ5xbOgXfv7nf35xre///u+ftvJfQx9cH9i/LrzwwsW1HPP2t27MGvJY/avybe9vn+7quM67azL6JyJfbEMIIYQQQggh7DR5sQ0hhBBCCCGEsNM8KSnymrRWWYCSBKUKnu/n7vrpXxmQn6IfeOCBjffopCtjLLOQKT1YkwFtum4tvyjPsR66DIL1b13GXj/Jd1nTxljKk6yXTqZSn6u7/zbZRD1mjKWsQMmMsgklh7bDmtTdPlKz+O6h/Gutr4pl7Mq+xlqW1f2GfajLbFv/Zvt24Qb2p9q3pJP3mUX9mc985rTrWPa49773vdM+88wzN5bXPldRYqOPOOyww6atpMjQiSphdDx2IQqdH12TIitBtCzWYx27XbbobWS2a33etvC5zFJpPa5lXrfMlstnf/TRR6ddsyKL1zIruX3dsleJn1ItQ1WU1e43ukzIT0ZK3bX52rWck5X8ma1ZH13nvi5DuBI272FZan93zlAm6XXtl9ZX9Q/HH3/8tO2/nu/918IT9FUnnXTStO2XzmnOXWP0fkAptX7EPl6zunqc4Q2eYz3q/x0T9RyfXymxY8rr1rnBv7mGcQ2llNE2qf3Aa9cwiP1MN1/UcelxXYZz68TzqzTWc7xuJ0dfq3fLUqXFe9jOSoFPO+20xXFm4/a6tq1jyb5s5uUxlvOgfdhsyUcdddS09Ul1TnY8GUqgrY+qa8Iu27Xl93f9QpXvX3fdddM287yyZn2EoTo1NMk68v5dCKS7xNx2222LazlmHcu2fRcqWNd5+pVt1+Hzugd1dAghhBBCCCGEsM/Ii20IIYQQQgghhJ1maymyn5j9RF4lCX5u7zbi7bJwVdmTGfaUrSop6CQUVYJqObsN4P0s3mWfG2Mpk/RzuVIhM4dZdmXQYyzlFZ30s8tYV6XbyhWUFChJ8NmVfI3Ry7m8lvIeJQxVdrGNdMD6kvpcXkt5U7cpeScJ3ZYnI0veVSmy1DHjv+2n1kOXcbtmsbNNOttxdeKJJ07bbN9jLGWPZtk266DHKL2pUj/lMkphtsniV6WVjp9OJrptSIf3tPzWt31OedAYSz9z3nnnTVt/qwTTPlH7h/f0uvoCfZzUfnDKKadMWxlTJ3+z7qpf8Jyrrrpq2vrBxx57bNqdFGyMZV3aD/azFLnLWLytbKs7fy37qdlI/ZtZkZ1v7JfVF9tOXbb7Ku3truUYUU6oLF0psf2yzsmuO7qx7xgxE2sd097H5+3uX/uldWG/7GShXqtKfi3zXXfdtfEetoMSzSpf9Bylo5axypf3qP3Tclrf+mb9S1ePYyyfcZeyInc+b+24TnLc+dK6vravdjt9eI7jvfoF/22b+Cz+bjvVfuo9lbZ7jvczK7n+aYxlf3btaLiE87b1UMvVhffYDq5pzdBdz3GcuG6xnzu/eswYy+e0jQ855JCN13LHiC7kcIxlXdo/HOM+Rw3l1P+4Ju9CWvQj1fdJt2btyBfbEEIIIYQQQgg7TV5sQwghhBBCCCHsNHmxDSGEEEIIIYSw02wdY2ucm1r5LgZmjGWMhTp2Y7vUXp9wwgmL843bUS9uem114B5Tt4xR66/WvovDULdeY7vUoasjV5Ovdlyt/Fp9danCu+06alxbt63Q137t1077kksumbZxNmOMcffdd0/bWAPjGbrU6jW2yPJr26ZdvEhN/26cnDp8Y226eJ7Plm3j1Wrs5n6m2+KntmG3zVMXi70WG+T5z3nOczYe8+CDD0774YcfbsvlcV7r/PPPn7ZxQsb51VgPj9MXGCtS48s6urrs4rzXYmwdv5bF+tYP1q0cHPPG+3fbMviMNZ7FMec47fIeGLtf/bDPYhm9RxeLVePpuvgxtzzoYrHqVihea81H7wJrW/Rss91Ut0XSGMu5+/Wvf/20bUv7bhe/OcayzvUP9uVuHqzbX1hmY2S7fB72t+q3vJbH2X+Mq/V+NdeC/c/51XHgmKrn20bWpc/VbTdU50Tr1fZy7nLsuD44/fTTF9ey7byWa6CuH7r+GmP5LJbRvmfs4tq2Mz5zvc9+5slst/XZ0uWR6XLVdHkj6jliLKhzzHHHHTft2uftj/Zt5xXj6I3DrTG2nl9j6ffo8jCcddZZi+P8W7cNl9vTWa4x+m2trJcun87JJ5+8uNa55547bevPPAL6Jcte69t6sVyurfSDPm99Z3MrVvtUt/XPmh+Wup3oE5EvtiGEEEIIIYQQdpq82IYQQgghhBBC2Gm2liJ328SsfdZWJqLMz2v5e91+5sYbb9z4N7fyMI210rwqDfVTtvdUuuJncaUVNQ215yvV8NmVASgPMGX4GMvtL5TgKsOxjj2/bjei5FApp/IGJRxVRnDnnXduLNev/dqvTdvnVfbh1gBjLOWfSrH/2T/7Z9O+7rrrpn3PPfdsPHeMXpYpSi261PX/q7FddkmKvK2so5MZb7P1T90OwPOVgVqWD37wg9O23ap0WbnckUceOW3HnFsHKUnTd4yxlLcqHeq27nkydLLkNZm793z6058+7W67oSpFVk7pVgH2UyVQXrf2D/2fMnCv1fn36qP0Bc4P3l8ft7Z9ie2qX3Nceg/70UknnbS41jbS8f1GJ0fdtr/6zMqEu/CeMZZtq1TVtlGye9FFF2287hjLPqv/ts383T5S1x3Osc7p/t5dt0p27Uv2E8/Rdg1S1zBuk2Ed6Xdqv5ZujFeJ/x72g1rfXeiB97d9b7nllmkrox5j2cb6Meuuk05bJ2Msn8Ux3a2bfC6PqffcpZAC/ad2Db+QbtuWbhufNWxD7a6frd3TfqYvct4/9thjp/34448vrtVtHeQ93NbGvlx9t/2pG/8+o6Fy1Y/qcz784Q9P23nQMeq8PcZyHrUuLIvXcizVvqyf8W+OB8vimDOEcIxlHSnLtl26UFDbod7fub7bFtJ1Ye3Ptku2+wkhhBBCCCGE8P8UebENIYQQQgghhLDTbC1F9rO4mRGVpIyxlAWYPUuUOig1UJo6xjKTlvjp3U/yytaqREVpr5IGP5F3sosqA7DMSj2UOSphOuKII6ZdZZVKZ7pnUerkvZVzjDHG5ZdfPm0/6ysd+NCHPjTtKov+/u///mkrOfyhH/qhaSsvUMLl72Ms5TTWn9ks7Uc+b5Ud2F7WhbITZSP/p6SEa5Ko/UyX3bBK15SodbJHf+/kyhX/pgTSsWAfqNI1++1999037Ze//OXT7sZyzfKq3MZzPvaxj027SuO3wXrp5GRrUmTbQnmSdae/qRln/bd1WbNG7qEfrBIsr/W85z1v2o5F68txXfuBfkFf2LWDdeccNMZSOt5JrQ477LBp6wdrBkafedtM6J9rOv9X26+TJtvHbJe17JS2jeEj+vXjjz9+2gcOHJh2bT+v1ckquzmxjimvpR/xOH20z1tlzfYrZfzeY5vs0mMsx8Khhx46bWWCtqN9dIx+5wDHa5ehtWL5feYuc/XRRx+98TnqOa4D7Gs+SycDHWO5ZvRaPosydOuurhHdtWFtDtpvdDta1HnMvtbNcfaNTmK86d97bJPFv+4m0slGXaMarlazmotj1vAiw46sI5+jZiJ2zDtHWEbDlpwf61j8yEc+Mm2zH9tnrXtDA8cY4/nPf/7Gc+zD55xzzrTf8Y53TFvp8xhL36us2fpyLJ999tnTNpRrjGVd6qNtI9vbvlpDL6w/x7l+0XJ53drv7FMHu+tJvtiGEEIIIYQQQthp8mIbQgghhBBCCGGneVJSZDfkrtKbbtNn8ZOzn+6rNNZP1koIOxmD8poqX/Rzu5/ulSQoW/MTeS2X0mI3ce42V7ZOlDCMsZTzdRmHrSM/ySsrruX0M74yBq9VNzDvssparhe96EXTto7dmHmMZcZM5UGd/LjLODnGsh8oD1Ea8rmQD+5K9tRKld7tsZZBsWbdfaLz67VsHzMPKjGzPyphfOMb37i41k033TRtMw9qd/L3tSyaSt+Ux3Xn12e0/D5XF1IhVVppBkXrzt/1Bd5vjKVcyPHXbRK/bfZL768v6UIfqlTJueOYY46Ztn1SCZe+5+Mf//jiWvpufZ9yNOvFeytrq/c52M3g9wNr7ddlpJRO5lrnd/9tO59//vnTPvPMM6etLLlmyNZ/en/HmMeYmbNKl7tdGzqJtWOqSrX1dYYBOA/5u/2llqvLZNxJ/6t8tpNrd3JVx1EN6fEcx75zsmWpGU+7a1nf3lN/6JprTSJsG3kt77e2ztNX79L8rP+3neu6xvaxD/ms3XOvzcmdzL7zK9Uv2CZK2LvwQNuzZul1nOuzOz+2tqZ1TWDfMAzGud46qddy7d49i/PQC1/4wsX5trHzqPOg1zI8p4YCHHfccdO2/pzrDSc0pEJJ9BhjXHbZZdO+5pprpq2P0Pc53tfWU47fbtcS71HXCva9g81wni+2IYQQQgghhBB2mrzYhhBCCCGEEELYabaWIvuJWamBUqMxljKyLpuon6j9FF0z+ikXu/nmm6etVOPSSy+dtpl566dr5S9dVmblt0qI6qd7ZQCdRNNP9MoDKl1WZetbaYj1Ve+trFnZxTOe8YxpK82oEmvlVWY5veKKK6b9wz/8w9NWnlBlIkotrFelgWbJe81rXjPtmp339ttvn/Ztt902bTPi3n///dNWivi/U460i5lUx+jDBdbkRZ203t/X6kBZiZITZUBKTZXPVwmqUkfHdZWy7GFfrGEMXQZYx9Zb3/rWadt/6wbsSuPt22aO91ne9773Tbtm/dP3Kc2yTXxe5UxjLJ/Lcuoj9UuO3ypVt4619bGd/LjKPPXxSsq7LNDeo0q6lSH6/F0YifVYs3r6zLskYdxE9Z/OJWvZrzedXzMZ2wavetWrpv2KV7xi2soHred6P//W7VRgeIyyxiqRdLx2Ps3+4u917Nln7IvWo793ssSK9+nCsepz+e/O1zn3uTZSfjjGGCeffPK0ff5twh7qWk7f4TN3WWjtR3We8D5et7u/fbBKv11rVCn2fsbnqOO3o5MPd3PatuEmnt/tgFDXe7aJf9PnKo31HaL6GM93TdzJUR0LtW85xzqnGg6lH7L/1vWEz2g4ov7O+d1wvDGWfsX1gdf9rd/6rY3lqm135ZVXTtv2ch4znNF3gxo2aN9xfvYdwHv4PrE2lm1H+1EXOlF3rPBv2/bdWd6DOjqEEEIIIYQQQthn5MU2hBBCCCGEEMJOs7UUWcmg2fKqzFa5jBuSK0PyE79SBT/Pj7H8FG5Gsle+8pXTVlqn1KfKOSxXJxfy0/mJJ5447ZpBU7qNypURmNHMTGdjLOtIqZWf4ZU9PfbYY9NWajDGUhauLNKMhMqHlfLW6ynJsO4tl/LDmkFOlCd0WTItS90YW5nZKaecMm1lR8qe7r777mkr0xrjf61keJfkx7LtxvXdRu/KUjrZZh1jnZTEPucxa3IX76+8x77pONFHVXmR41TZ0j/8h/9w4z30K3Wj805q5Tnvfve7p+0YrTJN608Jls94xhlnTPuRRx5ZnO/zf/CDH5y20k5l1dZplbJZ/11m1i6ree0H1oV1r4+wLpSpVZ/u3GG4RCeV937/7b/9t8W1LKfnGAax3+iyN6/5Jcer53ey5OqLL7roomkrD9WndJnDq2y0y5TpXOKcaBmdx+rfnC/sV/oXQ5DWJNI+i+O1k2vWa/mM+lN/VzK7lt3cZ/E4Q6Nsr5ox2DpzreIz2nd8Lu89xv+cxX0P/ZN+t8uEOsbyGT3fe7oGMqNtDcf6bDKp7hfWshL7t87PdVLmTqK8xjZZmMfoQ2RsH/21c5ehY2Msy//www9Puwtp836Gqo2xnJMdG/ZB68V+5rvFGGPce++90zYcyvcU1wp1neWaQCnzBz7wgWk7V+v76rrFdzCf2WfxfG3rdIzlmDdsyvbyWQwTU4Zd6bK92yf0IzV0wD697Zp1j3yxDSGEEEIIIYSw0+TFNoQQQgghhBDCTpMX2xBCCCGEEEIIO83WMbbqotVC120TjI0zxkHtujrwww47bNo1TtMYttNOO23a6vvVjhsDUGMvanzPpt+NOzHN+Fr6ddNoe77311bDPsYyPbixsD6X8XLWidr+er6xdG7BZNyLbTXGMp7De1r3pgp3u46qj++2MHC7ILdqMtV3bStjub7pm75p2rbLu971ro33rqnkjZlb25rh/2a6rTVqDI71Y4xDNx66WMwxlv3D/mCsi9tqveQlL2nLZR/2b17LcWab11gxx5OxJ/oi48LtizXO6NZbb5228W36vi62qI5l41u6OObXvva10/6FX/iFxd/0RdvEA3pMjRPstojpttNYG1f2Eeuliyvz2Wu8Wfe3Lg7N42scqv+uMYD7lS4utv7us2mvbd+xR61zjzPuy/gz+779sMbFdmPXdYN+xPPrtezXzsk+77Z9wbgvn9fxaoyf93ZOHGO5PnrhC1+48fy17aWsS8tlvXh/66WugXxO1xfe3/J6TM3n4bW78lt3+ora1+yvtr19z23aujjxMZY+fFdjbLel24rnYI8ZY9kG1ns3X2zbt4w5td1dT9QYW/uKY9ZrORacR4z/rGU2Lrbzfa4Xa7yu6+U3velN037pS1867csvv3zavtuMMcazn/3sabteve6666ZtP3etUtvO9YFb9BgX7DG2aX0H8dr6Ydfk3Zy8tiWSdPmPnAPquqHrL9uQL7YhhBBCCCGEEHaavNiGEEIIIYQQQthptpYiK0nwc3lNKa+01vT4Sm9OOOGEaSs7MAX2GEs5rdLgbpuD7vc1vL8py++8885p1+1+/CzvNkjKF++5555pK52s2yMpC1D+q9Sj2wbF+hljWcduI+RWGF3a7jHGeN7znjftb/zGb5y2MiS32zF9ujLMWn7L9dznPnfablOgHKPKh63XW265ZdqnnnrqtJUoK6FQ8jHGsr285//tsiXpZHhV7qE0RFlIDT/YRJWVKB3SNl38P/2n/3TabulQpciW02vZz+wDbv/0G7/xG4trKd158YtfPG19kWXR31V5kOc4tpRGHnvssdP+nu/5nmm/7W1vW1zLtP8+7+GHHz5t/bDyrzGWPqPbtqn7vdJJ/zzH9vb4Klvv5HCdfLiT0T4Z1rapUrK2Fnqyn7A+Oll5pZOBdc/sVjBjjHHttddO2+0r7Iv2V+V4VSKtn1dO2G0Ts7a1hNJcfYJyPuertS1VOmmu6wPPd+44/fTT2zK61nBO1QfVduj8rvVieMRaP/C5nFM7KXO3bcsYS5mjdW9dWt+2bw2B6vprN14NH6khL7syditrstNt6OrQa9VjujHQ+Vz7r7Lg+u/Ol2/btyyzW/w4ru1/hvHULXq+5Vu+ZdqG6lkX9iGvW2W1+qsLL7xw2vq7F7zgBdOukl/DFFwvW3eveMUrNt7vqquuWlzLOvJ818euO5yf63vDNlu12fZr4UFr7bqHsuS1sEHbu9susiNfbEMIIYQQQggh7DR5sQ0hhBBCCCGEsNNsLUXusgsqC6nHifKT7tP3gw8+uDhHOa1yHyUCXRYuZUNj9JIMr+sxZkRT6jPGGJdccsnG872nklc/41fJ6/vf//5pK/O1vvx07+d6s7zV46xjJb9+3q/SbetfaYwSKKUhxxxzzLSVfo6xlEQo0VQ+rHTUzG4105pSkTe/+c3Ttl6VOCvXtK3GWEqTzVKp9LPKWf5vQ1mK0rUq67J/eI7jp5MwVemI/c7j3vCGN0zbdnNc137qtRxz2pb3wIED0/65n/u5xbUsi7JJpb2OK8dCzczqmLcslt96sR6V/o+xlHYqpVbqqI9ay0Tc3bOTvFXJmnXkM+u7u6zza9mHt5EZd7K2J8Pa+Wt9d79inXeyr0qX5VS6DKdjLOWlzhfXX3/9tM8666xpO6fVOUI/rzS2k7opW6vzu/Jlz/c4+66/1/7uOPa6yhSduwxVqH3MuuykzF3YUcW/OUc511vHtU9sswuAc6prjd/93d9dHNeFsFiXnXRafzrGsh/q92zvQw45ZOO1qqxyV+nm5G6MjtGHEhysj127lvd3XNWwpW6cKpN1fu/mx4rXMsxR3/G+971v2q4Dx1iOB/u2c5d17+/6ujH6zO2+A/l7zfbseHCnEkMAX/nKV067y+g8xhiPPvrotA0PcW3Ure2qH3Ctr8/odsLodjyp+LzWvfewLPXdyHtWf/9E5IttCCGEEEIIIYSdJi+2IYQQQgghhBB2mq2lyGYlVv6hhHSMpczET/l+slbW4qdo5UhjLD8/K/3psrL6ibzK7PzMrfTBz92dTOud73zn4lrKVs3ketRRR01bCcRv/dZvTbtmD7b+PF+Zr/WtNKpKqCy/sg8//SsJUM4wxrItzECpHM1sr8qhlGONMcYb3/jGaT/96U+ftu34jne8Y9pKSyzjGMv2toz/6T/9p2m/+tWv3lgWZdhjLOVVv/qrvzrtc889d9pmrzOb42eblXW/0GX6rPi3Th64jWxpjF5qpfy328y9ylwdW46BLjOkY8ns2WMsx4z37yQyP/VTPzXtKsMxy7HjVPm+db8mM/M461hZv9nOa9+0LrpMxmJ71XJ1mXS9luX199oPDlYytzbmumttI1neViK9n+n6SB3T3eb2XVbltX6phE4cr48//vi0H3744WmffPLJi3OU9nqO472TLyq5G2O5dnCOcS5xHGjXNYzoB5TsOfb93XuP0del6yTHlPPNGEtpZJdxtPMp22a59/6uNZzf1/qUc6pyb/2mdVzXeba3dSmGZp122mnTvvrqqxfHubb0Gfc7XT+pc4z/3mYXkG4HhHrPTl5q29i2dU7uwoCc37udTWrmdceQZbHfuCa1LFWKbBihvssMyUqGfcYakuZOJ5b/sssum7ZjqYYwGjrkff71v/7X07aNHDPXXHPN4lq+UyjRtl66Mb6Wddt5xHbs+l0dY/oofYQ+St9lWaoU2XXeNmEUki+2IYQQQgghhBB2mrzYhhBCCCGEEELYabbWaigVUmZXP0UrPegyEStzvf3226et7GiM/vNzt7nyNueOsfzc76dzP7H7Gbxm3lOO+6pXvWraL3rRi6b98pe/fNqvec1rpm2WyDGWkiQlu8qE3ehZOYfSiDGWkkdlG57zlKc8ZdpKwcZYZiRUhqFUxLpXyvLsZz97ca0jjzxy2vadt7zlLdOumRb3qBKqLpOz8gZlxT/wAz8w7SoNUx6ifPm9733vtM8+++xpKx/pZHhj7I58cYzlmNlWura2Kfcm6vjz3/Znx5ntbD+rmc/1OZ2EVGmU/f+iiy5aXMv+1PVH5XFmTK1ySMes9Wqfs+60a6ZQs4dbLu/puFyTj26T/XZbKap0GYvXZL2dZHjb8zu64z5bifN+xnI6puuzdTLlLvSm6xdrOL4feOCBjeVy3TDGUnrm31wfWF7npOrXu2fx2R3H+jPnpzGWIQk+i2sFn8u5r/qqNVn+Hs7JNXO099FvKrHuMs5X+bB1oRRSv68P8/z6XJ7jPfVJzs+eX8vlcfpE+4c+8GMf+9i0a5+SXZqTuz5b59ptxuw2fW6MPku393Aedt6t87tt5Rz5dV/3ddO2/7quqrh2dE3qHOlazNCzGu7gOliZstfS9rkeeeSRxbV8D+h2JDB7dw1x6DKh33HHHdP+jd/4jWlfddVV067Z6btdKqSTIle6jNjdtbqwsjGWfmEbqftauIB+7WDn5HyxDSGEEEIIIYSw0+TFNoQQQgghhBDCTrO1FNnP8spYlA2Nsfz83G08btZZ5bM1K5afos2w3MmuvEeVKnWZgX2WLiNfzW6mPEJ5w7vf/e5pv+1tb9t4j5e+9KWLa5m118/41ouZEpVzVDnIoYceOm3bwXpVKlSllD6LkgqzHnp/ZUBVxvmTP/mT07ZeO+lBJ3sdY9leysGsV5/r537u56b9Xd/1XYtr2Y/MQq1tljupco6PfOQj095Wsrcf2EZKPEYvdbJ9OmlU7Vves8ra9lCSZjvVeu8kZpbF/uTYr5kkHRv2J6U/9vkf//Efn3YNg1D2pLxIGY11Zxl/+7d/e3Etr6286pZbbpm2Esbapp6/lhlzE/UY7+Mz1jbeo5MY12t3ZelkR2sS245O4vxE194FuuevfaHzTV2oUJdRe+1a/n700UdP275z7733Ls5R2mcIk1JGx64y3Softv2cY1wHOEcpZaySweuvv37azvVdCEQ3DsZYyj31Nc71nT+t5zj2bHvnPtv+nHPOWVzLebybA7os+XVOti48x99dN3WZ1cdYSr9d6yg59hzXHbVcXajULrEWHtRJi7v52d9rva/1u03ndH1ujGXfcn63/Gbx9/wagtj1+S5zv2vq+j5ixmBDGbo52Xqo7yPe050WTjrppGnff//9067rc+tCf6e/ec973jNt+/laKIDrJstsm+hHK50Mvgs3WJMu+zd9TN3pZFO5qh9dk+Q/EfliG0IIIYQQQghhp8mLbQghhBBCCCGEnSYvtiGEEEIIIYQQdpqtY2zVW6uJdyuNMZa68i6dv9upqKOu28+o11ZHbvyY8RlSYwa6GAJ13WrSpcY1GOfp/a0X43nUkdctY6wjdejGJnm+MSjGBI6xjDsyHqnb4qFu0WMKc21joXyuLkZrjDGe+tSntuXcVBbTwte2sk8Ze6nu3voyZurf/tt/u7jWv/yX/3LjPe2Hr3vd66b97//9v5+2W7CMsYwnMt52v9NthVXjDG2HLiali8erY8Z/ey3HtbEetm03ditdOn3H2IUXXrg450d+5EemfeDAgWnrYxxzb3/72zceU/9tbJ9jsYuF8h5jLJ/Z7QzuuuuuaT/jGc+Ydo3F0pd0ce36Psuif6vHdVvMyLZb6WyzFc9aHO02xz2Z2NldibftylnH3sHGGq7FSNuXuusak25/vfHGGxfHGUtrbJxj3/WF47jGnOnzzU/R9d0aIyj6pC6HgPaaf+rq0rWOZakxrkccccS09Wlub+e8f+aZZ067zsld/Jv5BIxpdK6tayPL4tqui1G0vWocpOi39GnOWfbB2te33UpkP7Pm17p4yM7/rW3zss0Wb9an96g5bPybaz/H5d133z1t+0D1Yw8++OC07Wfe07wTrv1qf/A+PqPrNcecY0k/MsZynJjPxmudfvrpG38fY+lXzKlx8cUXT9utjuSxxx5b/NuxZRs7fvx92y34ujVYN5Zqv/Pf3ZZEvg94vxqHu03/7sgX2xBCCCGEEEIIO01ebEMIIYQQQggh7DRbazXclsZPyTUltigL8BzTbvt5vqZ07qQzSpg8X8lflQf5KVt5kud4PyU5VZKgPEg5q/IE73HttddOu0qN3GLHz/1KE7sU+LXu3WbBc0y7bz0qcRxjKfu67777pq0kyDpSllylAraL5ystse6sr1pHypiURdunbG/PrynE/+t//a/T/o7v+I5pK2dR9qGUppbr2GOPnfYuyZ46eV59vk4+rG2927eqJMj+oKRIeU83Fmvf6uSB3tP2sP9Vya5Yfm37rPer8jz7jen8u7T1a1tTKFV0Kyqf3Wes/a8bDx6nHzUswe3Yapll2610Og72/G3v8WTKsotbg9iunZys/s229PduTK1JzTrckur222+fdp2vzjjjjI3lcl7SJzi+3caj8slPfnLa9vcPf/jD0+6eveJWdda3fsu5tq4VrEvL4txpuRyTYyzlwNalYQD6mjWZr89suIH3l65PjLFcw1mWzoevSb+VIHofr7ttaEq3ZtzvbFvWbXxbt7XS2tZr3RZB24SO1H+7RZb3t88df/zx065j2T7gnKr8uAtJc1yOsZyj3bLU59UvOX6q/Nc5stv+1HXzo48+ujjfeyrft/+7NureWcboZeTdtj7d2qaer4/q1kBdKFsts+d7j25rubX1Z7fNVUe+2IYQQgghhBBC2GnyYhtCCCGEEEIIYafZWj/p5/ZOGlr/5mfpTqqpNLBmxVLW4udvr+s5SlSqVEK5gc/ip/ATTzxx2kolzPJWz+nkhP/hP/yHaZsBrUqV7rzzzo3HmVnSZ1fSWTPe2RZKaJUNWUf1uV7xildM+/nPf/60lQE8/PDD077++uunXTPAmdnOTG9dJlVlCH/yJ3+y+JtyEjPBdrJO66tmZFZi/lM/9VPT/uZv/uZpK/uwTZROj7GUZygX3e9YP2vSpm2ygG4jV673sd46qVSX0W9bPEcfUyVxL3/5y6f9wAMPTNs+0D1vlQ2KfknfYz1aFqXwY4xx8803T/ucc87ZeF3lylWeZ19V3tRlSHeM1czt/s3xaxt1UrbPtu3WMoQe7LW3ldKtZRLdTzindmNnW7pM5/VaXTuL/dJjaiZjQzmUI3bSe+eFmrlbybBSyE5y6Piu4Qn2cceb9/T8hx56aNrHHHPM4lqOHa/lOsA5xvm9nm996UMNZ6rni8/vmsC679ZQVX5onem7tL1fJ7Gs13KXiW7NuK0scVfG8RjLtYz9ZE2i3MmEu7XQWrhFJxWt8tBN9644Fm2rU045Zdr6gjond39z7db5grpLi9JkjzN8wDWl966yXtfXXRt5TB2Lji19zPvf//5pGzZ49dVXT7uOP8vm+OmyWOuvqt/uxkm3BluT+1vOLnzAY6yHOt49v0qxn4jdGfkhhBBCCCGEEMIG8mIbQgghhBBCCGGneVJSZCUmfsYfY/m538/lygiUPZjdrEpMPKeTNR9yyCHTNtNglby6cflpp502bSV7fmJ3M+ltMx2aDdL7KxWqn9Q7yaNSBz/dP/3pTx8dnTxBSYQZJ2sGuTe96U3TPvvss6etBMryKmd63etet7hWJ+e98sorn7DsFdtemaR9x7rzWlUOYttZrz67/UMpWO2fn63873OFbeMzVVmJfVtJUid3WZMiW9ddWIHyICVpdTP4LgNsl+XR56j97Lzzzpv2NddcM23b80Mf+tC0zSBquEK9ZycBc1wq2axSfuWY1otj1nCFGsZhG1lmj3P86BdqHRnW0Mnftt1MfZvzt2Wba3222Zr3M11W5CfzzB1PRv7dySer5PAXf/EXp+08/E3f9E3T7rLkmvl4jKU0UZ9veTtJfp0HnUtc63T+3nF84403Lq6lf/H+hjEo0az161g0Y6vP77N73dp2ro9cW3XZj63vtZ0KbG/L5TmGldW2M/tsl7W7k8pWeW2X1XW/4/p6rdxdpljbugsbqv3B87eRHK/tLuD5/s3+6JzYZS4fY9lXXHu69nUsdKEyYywziTuPnXDCCdN2reExNVu497G9LL/n1zpyvva42267bdrWYxceUu/ZhaR08uO1HWO6HS86yfBahmTx/O5atd+vrU2fiN1ZjYcQQgghhBBCCBvIi20IIYQQQgghhJ1maymychclbDWDraxlp93DT+T1Wn5K95O1siXlNcoWLrjggsW1lAH4uV9JgBmKzVSmVGCMMb7hG75h2mYB/PVf//VpK5WwjMoKx1hmcbOOOnnRmnzPc/yMr6RAKW+VEdSybbqWGe+UGP/Mz/zM4hylzG5GrXz567/+66f9sY99bNpKu8ZYSstse8vSSTiqHKTLcKs069prrx3bYLm6/r0f6aTIVaKyTVa6bTIkj7GsK9vaUALHaNe2Y/TZMqWTZtb+YLmOPvroadtPzbJq1tOamXUbuZwbuxvuUMfeC1/4wmnbDt7T8VulyD6nmRY9TjmhPmpNftrJdDuJ6pp8f5t7bCux3UZ+vG1W5P+VUt7/U9Sx23Gw7bd23Db1vPa7PleZ37333jttx552nbv0F9riONJf2/fHWI5d1zpKZvVJzs+O7zGW4/CMM86Ydicxru3TtZfhUV2W4LqeqlLwTWVxBwOl0zU8ocv23GVdV9JtO46xlKXaRt6jY20u2HZM7Ae6dl6TZ24TkrPmu7fZeaCrwxri5Q4b9md3Gnn7298+bcfVi1/84o33qGV0vjITufdz144xljsdeL5yWCXHhiUoUR5jjPvuu2/a3fuEPq36IX2Ox20T7lCxvbxPFya2Fuon+tVu1wPvUftnl1W969/byo0PNtQvX2xDCCGEEEIIIew0ebENIYQQQgghhLDTbC1FVgKnxKVKFP187+d6P7cr3bnnnnumXT83e76fsv18/Q/+wT+Ytpk967X89P/mN7954z2UzHq/9773vYtruQmzz6XsyGOsrypZVdKhJMrMcEodlEjWDMlKeg499NCNZVE6XaWUxx133NjErbfeOm3lTEqVanZCy3LFFVdMWymxMqLXvOY10zaT3Rhj3HTTTdO2v3WZeqXKtGwvZRNeS5mK7bMmbdo2O9x+oJMi1zHjv7VtN89XBqRsb4ylzP+f/JN/Mm0lSdr2pyeTmVUsY82a6N9OPfXUaZsl2/HrGFXaNMZSHujz3nHHHdO2nzhGlWxVlBp1fbbSZYLWR9mmygPXZLqd1KmTv6211TbHPZkMx9uUpV5r26zO+4munGuyre5vT0b+LZ0cdlscY+95z3umfckll0xbX6wfH2NZ/q6PdhLlKmfVR1gu50vLol3v4dzp+uIFL3jBtJ1H6zzm36q/2aPzbzW8oQspOumkk6atD9ZXrIWD6Ks7v62s+MCBA4true646667pu18UtcXe9T+vCtjt2If0l/X5zvYTPTdurleu6tfz7eMtVzOS+ecc8607Wc+l+vAuiZWDmx/vO6666atZN6QnhrGZtiTY851cLejhrucjLFclx5++OHTdsz6PlPfjT7+8Y+PTdimXT+oUmLHedcPHLNd+67RZeDu5PBr9+nmFJ9jbWeHbcISJF9sQwghhBBCCCHsNHmxDSGEEEIIIYSw0+TFNoQQQgghhBDCTrN1jK2xlWqsjZcdY6mTViNtfIhxZsaQVN2+8S0vetGLpu1WPmq6jfuoMSHGt5x//vnTNg7Fshvj+uCDD7bl6uIQxevWYyynMbPd9jH+XmMmujTc2qY2r1ufGGdnmW17y3/YYYdNu8afuu2B26i4vVEXI3vKKacsrvXsZz972lddddW03W7Icqnbt39VuvgT+1Tt32LcQ4352s9YP9o1PsWtoR5//PGNtv1J1mIYjaM688wzp62PsD/UbSu6Mec5+gLjVt797ncvrmWsjzH6jjO3BDP+z/ifWn6v5Viy/5988snTrluKGcNj3/JZ7Ntr24Ro27fdJuGGG26Ydo0Xty392zZb9KzRlXGbeNt63MFuPbNtufYzT6bOZZu5q84x3X26rSG6LUnGWPp/5xj9i3Fp+tsay+q/HSP6BMvSbQ9X8RzzdPgsXQz8GMuxrx+78sorp+3WeM94xjPa87Vte/2D65wa42fsor5KP9bl9ljLvyDdmPb4ujazXU877bRpd9ukbRsPv0vbdlkH2/ZH+3YX2/hkYt+7rYOs6xqX7lzinHrNNddM23Z3LNR8HPZBx5NxlvoFt7N79NFHF9dyHrbM3frWfDquU8ZYro+7HDpSfcE22yy6JnftWWNMfRbHtWuV7vjaV7pYa9eGXT/aZuvFMbaLsa1bSFkut5PahnyxDSGEEEIIIYSw0+TFNoQQQgghhBDCTrO1FNlPzp1UYIxe2qvtJ3Y/o9eU1t/2bd827ec+97kby+Wne+16Lbch8vO3MmNldg899NC0q6RF6YSSQVFSpGSkyi78W7cViFuMeLzSqDGWcgXv4zYFyherzFapxTZbjHRbONV/m1rd8tv2pmyvW5+4JdPLX/7yaX/oQx+atlLOTupQy7+WCn8P5RBVuu05a1uv7Df+43/8j9N2W6YnI/XsZEtrW464/Y39sUpRumsp6XGce3/lQY5xt/QZY9k3Pe7YY4+d9g/+4A9O220G9E9jjPGud71r2sqMlc87Zu+///5p11T3hiVoK/3WJ9e2s2920j37s/LP2perpHGPJ7NFzzby2W23m/lst6jp7rnWd/cTtvFaX9hGNtodU+f3bWSKXRlru3QyYaWMlkvJYPUV3t8wAudOr+VzVd/vtb2n13VO8rq1HrpQGMeec3WVNRqGYxs792q7HWCtI9dtlsvncuxvK0U+2LG3toWNz3j66adP22c0BGmtr+/KOB6jDx2rz7eN/LiT/29bH924dszUe7t2VSbsdjuG4djnXHeOsWxr+6BrasOm3ve+903beWyMZQij91Sy79rR7Xp8BxhjOWY7f2PZa0iPa+rOXxsW4Bq6hiZaFv/mtVxr6AvqOrYbv90WP6651rYR6uaE7py61Zn3OeaYY9r7bGJ3Rn4IIYQQQgghhLCBvNiGEEIIIYQQQthptpYid5mMlbmOscy2p8TAz8zK/5QXfPu3f/viWkpru6x6VXK8R81S+/DDD28sVydN9fP+mgxAeY+SPSVBZmqr1zKLmc/SZZtVzqHMaoylZFEJk3IQJQVmGhxj2RbKhJVH2Q9sx5ql0udUdqGcRamEWfXWMq3ZXmeddda07StmnFyTUPlcllf5snVc287+0vXD/UiV6+xRZWRrMpNNWLf2xTGW0l59hGPJvmV71mzNtkOXGdh+poylyvNsN8MdLO8v/MIvbCxjlfRceuml077wwgs3lstx4r31F2Msx5bt8I//8T+etv3c8bqG5e9kalV63EmVtslqvJatuZOpbisx7v62jbRq7Vq7MpZrmMUe1X92WbG7Y2TbbLjbsCYbtc7ty/fdd9+09Ru1jXxm66WT0TsO6lrB8Aznm0MOOWRj2fVBNXup11JmaGjUWkiM0shOGqivs16sr3pcF2Kzlj1Vul0EOolyJwOv5+uflWKaOfryyy/fePwTlXk/043lOmZ8vi4sQLqMt/XajodOWq9dQ8/sa9dee+20TzjhhGkbYmD/rzsCOIac610T33rrrdM2jO34449fXMvQIzMZv+1tb5u2kmPDker8bht1WZUdF1WK3GWO78ZllwW9ls33mW4sW5a6Vu8k1t16am0O8LmsL8vS+Yg6lrsM0duQL7YhhBBCCCGEEHaavNiGEEIIIYQQQthptpYi+ynaT+RKccdYfub287cSWuVBZhatMp4uQ59ym07KqARojOXnej95K5XosqvVz+Ceb2ZTP6v7jN1zjLHMqqw0VnmUMt1uk+sxlpkalX57jtJx5SBjLKVS3Wb0nRyqyrm6jIq2l2WpWWGlkzppKx21f1xxxRWLaymvUELmtXwWJaFVQqUEpcpOdpEqVVI+Yt9Q2qt89+ijj552HTOODceWY+68886bthty2wZjLDdOtw1tH8e7G7hXCaP9zr/ZB175ylduPGZNUqeE3T6vX/CYKhtUZuyzHDhwYNr6oTVpbSft9boeU2Vx9u1tMiFvK/ntft8m2+7aOZ38eK0cuyhF7tqs1pm+uJNqdhLHtfrvZI7dPaqErQs5Mauq2Ut9jsMOO2xxznOe85xpO/d32Wad36qMvwuL8f6dTLBmQTa8x/mio/ZX/W6X7d/fu10ixujXcJ20sMuuW6/t+daX569l8+1k5Na360znife+970by16vu9/ppKlr4UGdpLO7bu1bnTS285mWq8pGbTfHn+t+17q2TR0zXR92HjIztteqO2p0azTXEO7k4pq6vtt0OKd3oQ9jLPuzddyNpbUdOXyWzsdtK8u37TxnLQv2pvvV47a5/5qPkbo+eiLyxTaEEEIIIYQQwk6TF9sQQgghhBBCCDvN1lqNZz7zmRt/r7I1ZQh+PlZm/JKXvGTafu6v0j4lBn6KVyqmnPXqq6/e+Hu9tmVRcqhs6IYbbph2lZkqI+g2RFaiedxxx208fox+Q3VlxWY4lpp51szPSpiUQNheVcpsXXSZFjvJbs3q2mWQ81raZqNTClaxXZWYWl5laRdccMHifCWeXSbOLtP1WpY763u/Y3sceeSR0z7qqKMWx9mHrVPlJ3XM7lH7uZITpcXvete7pv3Sl7502soRa38wO6mZEh0PSqFttxoK4Fj2uWxbs4pblirlV0qjBMpxWcMt9vjv//2/L/5t337ggQem/cu//MvTVkpcpfydNM0ybiuf30bW/NlmMu6kcJ2ccdO1n+zv9W9V9rVf6ep/rZ667LSdrHFNCtnJyDq5ZC2XvtVrGc7knOqYrvO7kuWPfvSj03bd4v2dr5RI1vJ3faaTIld/qI9wvvHZ1ySPjtFuPSTevx7jv7tQIekyH9e/dZL0tYy80o1x69Wyu06rc5briF2SIvt81m19hm2ymnfjvZ7bHddd1wzFhn6NsRxzrlcPPfTQaV911VXTdq1re9Z/e5z14px80UUXbSzvGGPcfvvtYxPOqV1IT50fbaNupxDvX+u0C/HwfP2FPqKGYzn3ex+vtSZp78rVzQ9r50v3btRdy3tXf2Xfr2utJyJfbEMIIYQQQggh7DR5sQ0hhBBCCCGEsNNsrdVQuqI8yIxi9Thle2eccca0/azcZdSr/+4yoil97ja5HmMplVCGpDRQ+Z9SwirhsPyf/vSnp62kwfuZGa7KjpQcKzf41Kc+tbG8nq80ZIylPEGphO1lfVcJlm2n1KMrbyezGmMpKXJjbjPB2qZuRP+BD3xgca3v/u7v3nj/Dp9Rqe0YS8nxLbfcsvEcy+Vz1UyAnmMd73f+1b/6V9NWIlLHn39TFtNt4N5lWax4H/uDMrIjjjhi2lUS5Dh1/P/mb/7mtL/lW75l472r3EVJYydV1K889thj01YGPcYyQ/ojjzwy7XvvvXfayru9luN9jDF+4id+Ytof/OAHp22IhJlhqy+w/J20vwuDqFgv28gGOzlS/Zvnd/LjbTMsH6wsek1atUsSxj26Z65/68auY31NgtbVcydF7rJsjtH3H23bQolyXXd0uxDYrx3HhlbUuctyWkbL0kl+q98z46v36cZLDZXosoF22ao7KWEtf1ff3XqsXmubEJQuw+uaJNZ7ugbTv1mnZuIfY7mO6OS1+53PVkLa+YK1OVm645QVu+4dY7n+Of3006d95513Tts1qevjum4/++yzp+34NQO2UmjXAHV3DuvFMlsW66jzg2MsfUEXRtH5xFqWLiTI8/UX1Q90Ps7rdtnwtw3J6STDa5nyu7m/8ytr4RXujHHqqae299zEbo78EEIIIYQQQgjh/ycvtiGEEEIIIYQQdpq82IYQQgghhBBC2Gm2DiYyftNYlxrzqMbaVOzaXar4GhOh/lpN/k033TRt423dcqXGPLrlhrEbHnfaaadNW313jYtV066O3ZhTtx5Rn17L5TMbY1tjbfZ49NFHp13j6tThW3dq9X32Gldn/Kn6eJ/FmEip+nhjl71Pt42QxxiTO8YY73//+6d93nnnTdsY4U73X+MkTjrppGl/4hOfmLbby3QxD/UZPa62xX6me75t43nsW13sxdq1PO7kk0+e9g//8A9P++d//uenbTzLGMux8bznPW/axlP/yq/8yrRtW7cFGWO5hYh90D7vOXfddde077jjjsW1rAu3AzE256GHHpq22xrUrbuMy9V36oeljmX9svXdbV+1Fn/axcJ28a9dzFG91mcTI7v2t21iadf6565s97PtFj3bxCxvGyfWbTeyzfZSazGfXex2F9tf51HjxbsYabchca41ZnOMPg68i7/r4toqBxufXu/TtVEXv7a27ZN9fJutXqoP7uJv9SmdP6xbk3Vxdvpdz/F+5lsYY+n36haE+xmfu4vfHGO7tu762bZ+wXOsd/NG1O22zOPgWtt5zZwU3q/OaV7bPBTG0nZlrOPnmGOOmfY73vGOaW+zBdra9o5rcaYd22zP5u/25erTrFfrqMsP05Vj7W/WpfXSxeeP0c8Pnu9a2eNred2qs+YDeiLyxTaEEEIIIYQQwk6TF9sQQgghhBBCCDvN1lLkLr17RRnp+eefP20/kXfXqmm/Tcn9wAMPbLSf+cxnTlv5a5U3KCNQ9qF0yE//J5xwwrSrFNltSZRN+IymHVcurVx5jOWnfMui3NutQA455JCN547Rp2lXPqxcu376f/zxx6etZLmTRHQSxTGWbalURNmEdWEd++z1/E5Guy2W37Tyb3vb26bdyS3XqLKv/Uz3TFWStiaR20QnQxmjlz0qD7zsssumrZT4da973eJaSuaVESovfMMb3jBt26aW0X561VVXTfuNb3zjtB1/+pi61YRjs6uvrp9Un6jsynHZbXdVpczex7HsmNNHrW0L0UmPunPWtvvprivbyorXJM8Hc90xlv1iV6TInTxsrZ6637s2q/1ybYw/0T0q3VZjzoP6e+cF1wa1XD6L/d3tsQxBqDJb79PJjJ071/p7V1/2sW2lgdKtFayvOo49zrAl51fLpV23G+nkw15Lf+wxNWxCH65PMuxDfMYqa3Y7tWuuuWbj+fsR26oL+6l0MuPOX2/r18WxsSaRtn+4FZfrPfus5xuSM8Zyvnr7298+bed9290+4zZeYyy3CDIkybWOY7nKj8U62iaca62+xbrTX2nXed/y+66zzbY+a+Eln61/3+Zatp19ovZ123ItxGMT+WIbQgghhBBCCGGnyYttCCGEEEIIIYSdZmspsp/ClSTUz+1mrVUq0X3iX8uKZfYsz1fuXGXCe1SpkucrhalSlj3M4LaWHc3Me8ptlCgrZawyHD+3K0n4yEc+svF3UeozRi916KSISkZq+a0Xr2t26jVZsyiVUHbUZbZT7lnPMSu096+ZlLfBvqP0/Oabb572mgRiTYq9n+nksE9GoqIUZdtwBceTfe6iiy6a9jnnnDNtszGOsczAqC8yi2Yn9XMsjrFsazMW2588R79iSMIYy3p1XFsv20oQlXB1GcYdc4Y7jLHsj2YO1acqcV6TrG2TwXVbOVaXlXeba9X66sbcwWZIrtfath9/rumkvPU5uzb7bLJK1/tL5yvWpHldZt7OB9V7dH30Wc961rTNrKnfqCFQXVm6MJxt674rf3e/imsK6961mXOl8+MYy10ADE9Slu21OlnkGEvf4zrC87fNyur5ykqVTm/Tb8cY47jjjpu2u2fsd+xD28qPu6zVa5mQpRun25xf2/bTn/70xuM833nM/lTX185lH/vYx6Zt37L/u1audWfYomXx2V1fu1ZeW9N17dBlW65ldpzql9yNxXFpyOUYyzpSou2z6NfWJMpdPzhYWXK9ts/rOtp61UfVcCqf5WDX1/liG0IIIYQQQghhp8mLbQghhBBCCCGEnWZrKbJSAz8LVymvn8/dRL3LQraWgdJP/MpG3XRZGYJSmypF9m9KGpRHKHtQ5ls3+rYulA54z042VDMwKtNVZtjJUZRA1E2yrSPlPUo017KHKiNQsuj51ov3r1LII444YuO1bHvr5dBDD914jzGWz2Xfs747ueealNg+qYTpvvvum7ZyKGVS9fxdkiJvmxX5YDM1Ppks1fbn5z3vedN2XCrbqffv2lcZjv26yvNOP/30aTuWlelaD8qg77zzzsW1HHP2zWOPPXbaSrDW6tR/e04nDatyLmU8SpVsY/3zmqx+G2nwNnLXyjbX3Tbz8cHKauu1/NuaFHA/sa2ctTuuq5u19usyqT4Zaa10Y1q/rl0z53cZomvm8k3nr8nlve42kr1aX11G9q6P1XHsmuDEE0+ctvOoz2Ld17WGc7KZaA31MARjTa7t/TtJuWuCNXm/83WXbdpn9Ji6BhLDi/Y7+mvbvNa7/aPz2Z2fXJPvd/NlN65rP+3mmBpWtodryvoOYTZs7+86wPN957jnnnsW11Ja3Pk+1/Per/ZZ68u/dZLf+uz+zXcY1x2uL9wxorapYVP6kk6evkbX3t36b62OundDy+gxrvV9Bxhj6a8OdqeCfLENIYQQQgghhLDT5MU2hBBCCCGEEMJOs7Xmyk/6fop2Q+wxelmM9raSRT9Ze77SVu+nJKBKFDtJxMknnzxtP90rET7yyCMX5yhdMEtvlzVRu8psvY+f9ZWmKPvwGa2HWhYlGR5n3St7GGMpn7RcZivrJAW1fpVvK//UVhLuM9Zss8qElaMoBVWSdNhhh01bqWnFurC/2N7eo2YCrNLkXcE+pMSjyp462WGX+c/+W+WIndzFsaHk/IYbbph2lRN2Eh3v3/mOmpXcfmff1K95jP2/ynCuv/76adtX7r333mkrAVSOVOXW1tE28s8qgfTfPnPnS6Rmze5kxp1ktStvPa47Z9sMvZ3MeZusvmvXejLZID8XrMm8pZMGd/Ow/XotE3U3x3X3ruXo1gT2UY9x7FVpmn3cuUg5nHOEYQN114Hu/vr7ztdVaaz15TzmusXyGlo1xhhnnXXWtH3mq666atpmPbe9anjQmWee2ZZzD9d5nTx2jOVzdeEgrnVs3+p3Osmk6wNDyfT/lrfe86STTtp43V1mW0noHt24GmPZDt18ue21/Hcn37fPG15T11HXXnvttA0PtA90oQS17Nv48m3n1G4e9Nmdx+taX6mt6wDHtb9L3cHENYVleTJr0m3qaNvs9F1dduEKjuW6Vvf95GDn5HyxDSGEEEIIIYSw0+TFNoQQQgghhBDCTrO1FFmpkRKRunHwNpuub5s1UYlK9yna+/sZvkptzPbnpu3KeJTeKAmqUhnv6bOYGc77KwW27sYY48Mf/vC0lS4o7+lkXrfddtvi30pAlA9XGdEeVT7cZcbz+d3Y3WevciTlFV02OiUcykxqlkpl0Uo9lF6bQc3rVrldJ43znspQb7zxxmlXuU+3AfV+x/p5Mtlgu024txn7a38zS68hArb/GMu+ZR9W/qsv8Pjat7pMj/qLTnJbMyybhdOsjZ3U7pZbbtnqWsq2rOMuq+UY22XC9RyffS1L7DbZNztZ8dr522TlXZPebiPLXSvXLmZFtv3Wss4erNRs7fdOgmuddZlb6zzW+eJuTK5lFfZvZknVP3SSReft+jfDjryuYTvaNaSgky97XCdxHKMPb1Ji7e8eX8uiNLk7p8vIW8Oeuv7Sna9v1x5j6R8NYeqyMlundRzbdtWn7me2Dc9bk8o+0XXX1kLbsK3/tA/bhp5vf1JuPMYYn/rUp6bt89oHumzj1Q9uE4qybRbpbl5y7ee6wTXMGMtQK895znOeM23XMI7Xutb/+Mc/Pu0uHMxxvdbWXR1tI3WvdO8t3t93LsPMXMOPsayvg52T88U2hBBCCCGEEMJOkxfbEEIIIYQQQgg7TV5sQwghhBBCCCHsNFsLl029vBYzuk3a/y7+q2IshdptU7xbFjXZNa7U8017f8ghh2wsu3Egxn2MsdTBG+ti/Kl0qf3HWMaBqKm3Xoy9tVzG8Y2xfGY18er+jVP46Ec/uji/S+fexQkZu9htHzDG8plth9tvv33jMbVPGQPVxWEaU9ylTB+jjzmxvm3fpz71qdOu/cC62KWtf+zn28b2eJzxDl182FpMRrctj/1Xu27H0W1R1J1vrFmNhTa+xFg540CME/LZa2yKY7nzg8aF61ONox1jjLvvvntjmY3zc7sgy1vLZvn1lw899NDG8tayd7FFNR5wm2O22ULqycTVHuw2QmvXOtjYs88VXe6Ebdl2Cwfp5vHOdqxWX9PF4m7D2nZijivHjn5Hu8bA+zfnIufhBx54YOPxtVxe+5Of/OS09WmPPPLItNe2MRLL5brBMta4WP2b85XnOI/rz6o/1+9qO3daR8YE+rwV13b6bdcw3bpwjN3ctmuMfk5eixnttlPprvVktjjrciLU4zufa59zbLj2XNsSTrr1Wre9UGWbOWKtvryP/dztIZ2Ta990/XjGGWdM2/cWt/Fxix/XA7Wc2/rujm1ibDvW1o9dDgbvZy4R626Mpf862Dl5N2bwEEIIIYQQQgihIS+2IYQQQgghhBB2mq2lyEpq3O5mTcazzefjTv46xlIW89hjj027kyUr03P7mDGWshY/hd98880br7smd1F+rFTIeyoPUu6shGGMXjbVSRKs0/qMfrpXHqGc5P777592TbuvLMCy+Ly2ke3jMWMst2jptt+xLnxGpRljLNvObZiUYivtsu2q9Hsb6a11bNspp6rlrNut7Gccs2vbFHXypm4rJ+Uma9vqdBJWefjhh6d96qmnLv6mtL/bDsNntLxV2uVxSt71JZ10em0rm66+3GrMfnrPPfcsrqWPrTLjPdyKqkoYleO7fZXt3W37tCYf7rb+6c6v462Tem0jGV4bu9tca22Lh222f9hv6NvWZL3d1hT+vq180fGyjRRdqrTtf5dUVFm/c4x+wzFd68s60scrq9Q/eN26hunGhVtbKFH0HmMs5zV9qLK9448/fuP91vr4XXfdNW0lw0p+fZa10BLrzznStZHtoD8aY9mnPMc1lL652zalnr8rIQVjLOtwTSbfbXmzjS+u462bk7v1muVaC+t4MtvAdXQ+oguJWONg5cf1uoceeui0Hb/ddnpr62tD3Oznhuet+YVOii3ds1S/3Unau3pda7tuu6Gzzjpr2j677wm1r+vHD3au2J2RH0IIIYQQQgghbCAvtiGEEEIIIYQQdprP+8y2moAQQgghhBBCCGEfki+2IYQQQgghhBB2mrzYhhBCCCGEEELYafJiG0IIIYQQQghhp8mLbQghhBBCCCGEnSYvtiGEEEIIIYQQdpq82IYQQgghhBBC2GnyYhtCCCGEEEIIYafJi20IIYQQQgghhJ0mL7YhhBBCCCGEEHaavNiGEEIIIYQQQthp8mIbQgghhBBCCGGnyYttCCGEEEIIIYSdJi+2IYQQQgghhBB2mrzYhhBCCCGEEELYafJiG0IIIYQQQghhp8mLbQghhBBCCCGEnSYvtiGEEEIIIYQQdpq82IYQQgghhBBC2GnyYhtCCCGEEEIIYafJi20IIYQQQgghhJ0mL7YhhBBCCCGEEHaavNiGEEIIIYQQQthp8mIbQgghhBBCCGGnyYttCCGEEEIIIYSdJi+2IYQQQgghhBB2mrzYhhBCCCGEEELYafJiG0IIIYQQQghhp8mLbQghhBBCCCGEnSYvtiGEEEIIIYQQdpq82IYQQgghhBBC2GnyYhtCCCGEEEIIYafJi20IIYQQQgghhJ3m72974DOf+cxpf9EXfdG0/+Zv/mZx3J/+6Z9O++u+7uum/aIXvWjaH/vYx/5HAf7+/yjC533e5y2u9eijj077mGOO2XjPv/u7v5v2H/3RH037ox/96OJaz33uc6d91llnTftLv/RLp/3Od75z2rfccktbrq/6qq+a9ud//udP+5BDDtlYxk9/+tMbjx9jjK/8yq+c9l//9V9P+9nPfva0f/d3f3ej/QVf8AWLaz3rWc+a9hd+4RdO+/HHH5/2X/3VX037y7/8yxfn//Ef//G0rUuf3zJ+zdd8zbRtxzHG+Nu//dtp/+Vf/uVG++lPf3p7fofH/d7v/d60P/WpT037aU972rS/4iu+YnG+bWRdfuQjH5m29WjbWfYxxnjGM54x7S/+4i+e9tVXX/0ET/G55Uu+5Eum/ZnPfKY9zr/9vb/3P/4PzP5ge3iMdsUxa73ZVt77qU996uL8Qw89dNq2m237J3/yJ9O2n1d/9ed//ucb7/Nnf/Zn0/7DP/zDaT/lKU+Zdu2zXtvxZz//7d/+7Y2/f/jDH15cy2exP37Zl33ZxrJXX/Dwww9P+84775y2fsX61ldb9jHG+NCHPjRtn/mkk07aWP77779/2r/zO7+zuJbtapn1zxdeeOG0/+AP/mB0eC3H/GmnnTZt61vfW8ul77v22mun/b73va+9/+eaH/7hH562Y6rOV1deeeW0H3rooWk7RuxX559//rQ/8YlPLK71yCOPTNv+d+aZZ07bOeaee+6ZtuO23v8v/uIvNpbfeci5p45j5xv7hX3hla985bS/+qu/etrOwWMs/YBzveuezje6nhhjjNtvv31jGe37l19++bSt0zGW84++x7o7/PDDp+2caDuMsWxjx3tdk+yhH6jzhP/u5pDaD7vfrRfHoXVhHTtnfPzjH19cy/Wn9n4ex2OM8ZrXvGbaH/zgB6dd68q67vqDc5d91r48xnJe9Hxx7nFOcu0zxnKcen/nHse449exOMbS559zzjnTvuCCCzae7xqgzsnO3fYzf7efdHP1GGOcccYZ03bu87lchzr2x1g+l+P0a7/2a6d97LHHTlufvsaBAwemrV/pfFf1C/YDz7/pppum/dM//dPT9hlrn/rkJz85bcfyEUccMW3f62yv6tN9/te97nXT/qVf+qXxROSLbQghhBBCCCGEnSYvtiGEEEIIIYQQdpqtpcid9MTPzWMsZS3KR/zcrSRAGUBF6afSYq+lNEqpkhKK+m/PUbJ32223bXyOKpXwuZRdeC3lCcoAqjTW45QkKBewHixX/XTvccoelLN4P6UsYyylSt7H6yoPUJqyJjm0LEoGPWZNLmof81pKkpRNKXmrEhtlK/Y929jr2nZVFqSMwnrd71i/nQxtjOXz2u5dH/T4KkX2/K7e7PO2Z5WjKnv1HH2EZbE9K95nTdK0x4MPPthe12vZb5RjKVVUarfmB5XMG1ZgvVT58H333Tft7ln0XXfddde09aNjLH2WUiVlzY899ti0jzrqqGlX2ZP1ouzr9NNPn7bjvfOPtVz6Mv27snv9s+Ew9d/6tf1MJ/X0OcdYSup+//d/f9rPec5zpm1fVipW61zZrvJD28lrKVXzuhX7nPfswmBcQ2z69x7O6do+x3HHHbc45yUvecnGvxmOZX+zT9f1kJLDe++9d9pXXXXVxrJXKbNt5DzchTo53uqc5H0cO/pq/XTns8fo+14XglJDJbpzLJf3dM7xdyXVYyz73ppP3W/op7qQmjGW7e7fujq0neq1lON6Xddl1q996/rrr19cq84Zezh36S+8h/13jOW8aP/3GV1vOt/UNnc8ef9urS61L994443T1q8pH9bHGfZRr6fPcGw4Dzm/H3nkkYtrGS5huIjPaz9wfVB9lGXxHUC6NVRdd+hjHLNKlPUr9qm19bXrg23IF9sQQgghhBBCCDtNXmxDCCGEEEIIIew0W0uRlRp02erGWH4i95OzUjfP8VN0lbOaeU2pRJd99ZRTTpl2zcDo53blEUqSlF0oDakSDu954oknTls5rTI7JRBKBOv9zfxs5jElg2YirhIOZWbaXXbDmjlauYNtZwY4s5tZpzWrq3IH5STK4rrsvDXLopIE2846ViamHMPjx1jKuZQfK9uwvbxWlW53x+0SjusqN7MPdHI1pSf2syoN9Nrd+UpROmnhGL00337j7/bFteyeYlkco163yq98LiVRnuMxSohq3ZuVvcv+6xiv/dxzlHZ6TzMR275VmqXsSp+sbKoLUTj55JPbclkv+hJtn7fKmj1faZl+RZ+sv/OYMZbjXz+8n3GMXHfdddOu2Tjti7aT84pyPmVjayEm8sADD0y7y2pcpWbeXzlel7F0LYO7faHzCY4x7+1YG2OMO+64Y9r2C/uM/l67jmPXSttk+a24XtCPdZK/7vcxlmso69s68nxliTUz/WGHHbbxb463rlzVt1sXa5LlPboQhHrtmrF1VzCTtxnpx1j2Fdel3U4lHl/7mfXTSYk9x3WcGYor+hil+M5R+gLH+xhLCa79zP5vaKHjso4//Y/14rPo76yHGjboOtjjvIfr9hNOOGFxvr7UNYy/u74327H+qt7H9xbP766rPcYyjMi53/Jbx47R6mO63TO6rPdr+G5VM1Q/EfliG0IIIYQQQghhp8mLbQghhBBCCCGEnWZrKbIysE6GM8ZShqCEz8/PnUxWaekYS+mBn7yVvZqRTNlC3bhb6UAn51BmZ9mrbM1P/Eqw/MTvp3dlslXS432Uaig1EGVDdQPnLstdl+mworTZzaiVBNgP3BheWeMYy/r2mZUKdVLK2nZmo1MeYlZWn11pWc0A5/Mrq+4y1FqPlneMZZ9ck5PtN+xDykrWJCJKTDp535p8uMpI9+hkul3oQS2nf7MNvZbHr214393DvtllMxxj+Sye77NUSdEeypnqtfQf+hUlWzUbpD7Lce1Y1I/qh6tP955KI60720FpV+0Tyhu9rpJj61ufWqWFXRZr5ZDKsJX4VfmbvmxbqdTnGvu7/rY+m3WjBM9QjE66fu211y6upc+0nm0nx8ha5u4uJMk+02XmrT7Be24jRbbuant77W5HANcN1v1aqIPP1WVIrTsCSOerLdeaXNvzO18rPlcNdVBi2mX0VS7aZWit97ft9BWu8/y9+mDrcpewb/p8VTZqf1SCah1280Jdx9q+zkuuZbpdC+o81snxnW+cx6TKTL2211J2ru9xLDiux+h3afF3r3X33XdP+1u+5VsW17JvWRav5e+G/YyxbCPXrvZtM6d7fl1fdhJtfb/vU/ab2g6uD3y3se4d12vze+dLnsyceumll07713/916f9oz/6o094br7YhhBCCCGEEELYafJiG0IIIYQQQghhp8mLbQghhBBCCCGEnWbrGFt15MZT1hhG4yeMWTVGw3gLtdc1vbYxQOr7jW8xnsB416r1Nh7LeDa152rdxa0kajmNgbAsxiYYQ1DTkT/++OPTNobM2BH19daXMRb1OGN0u+1OjO8dYxmDYP1ZRuvLZ6xxXcaJdFvCeA/jcbTHGOOiiy7aeC1jEGx7f7/pppsW1/JvXZyRsQn+XtvOfmicxn7H5zbOZi0OoovTrrFue9Q08N02FGsxYZvOred08Vnd1hZrz9htOeU59v8az+P4657LOCd9Wj3eZzbm3PZa29qj22al2yrJOlrry13MlX7Q82tcrL7EmHX70aOPPrqx7DU/gc/i1kN1Htl0v+r7nB/q3/Yrxr8a/1XHpPOEscy2uX7RfmEs2BhLH2jbOlc7Doy9q2sF+3IX/2YfX8tj4H0slzGG9t1u26kxlvVivLH30Ac6DmouAe/ZbW0m9Rm7saR/c061jm3reo5xcpbZ53IcVZ/QrTUcR12sdR3H3Xi17h566KGN967xuradfercc8/deI/9QrcFpbGNY/T1q/+1fhz71uEYyz7oOLGf2u7GYtYcMF3ftj08xmvV3CWuv04//fRp63scv9Zd9TH2Lf/m3K3ve+ELX9iWy9jfbu1of6xbUbmuveeee6btlk62l76n+otuO03HmWsN1xB1Oz/b3jWtbWeeHftn3b60Ww/KtvG2lvORRx7Z6pxZjoM6OoQQQgghhBBC2GfkxTaEEEIIIYQQwk6ztRTZT/fKvmq6dWUQSo2UEfjp+6ijjpq2KeTHWEonOkmPn+4/+tGPbizvGEu5gBJpr+s5ygDq53WvdcEFF0xbSdM73vGOafvsVSLtZ/0uhbiyD+V3yg7GWNaL5e/qrqZsP/7446etzKVKMvawH7zlLW9Z/E2piNdS2nbJJZdM235UJdY+i/aBAwc2Xlfpd5U9Wf/KM26//fZpK02x7FV+4z27bVz2I53sb20rHKU3SvIc4/bNKh/2nttIELvtquo52t22F0rt1rb26OSJ3TYdlW5Lo+6YtWt19bKNdHuMfmsP+7N1Yf+tPsp2te30Xfp0j68hCvpY61WJsuPf7Q/0N2Ms5Vw+V7ddjH6shjv4XGtbou0nOl9W5+TuuG5eUsa+tn2NkkH7q1tGrNWr57gFhX7WZ7G8VbKrn9c/Ka2TtS1yPN/tjZyfnXvtV3XsON6rBHAPx3eV5VYp+B6OMevRsVO3d7Fs3Xi1vTymjmOfq/P71r1SYNdpYyzltpbfdvEcn1cJ/BhL6azhUPudLgymrl9cI7v+8Tjlym6VWNfXhnx0YTGuld1i5s4772zLZT/vbCW7Rx999OJa3ZZNypftZ46Zuu2dfVDZrH3oda973bTX/JXrY+XPDz744LS77YnGWD6X17Lu9Kn6lUMPPXRxLZ/fbfv0i9aXvqCOP2Xsrq+V/xq6YN25Td4Yy7Z829veNm2fsQvzqnzgAx+Y9sHOyfliG0IIIYQQQghhp8mLbQghhBBCCCGEnWZrKbKfn/1EXmUEfjL2E7lyG2UTyhOqtFZ5WidfVGrgtWqmQ7OrKa8ws6TPdffdd7fXUm6gbFWpxjd8wzdM+7rrrtt4bqWTaijPsYxVGqukQImOGdSUJCg5G2MpLbQstql1oeyjyp6UNipVUtJgeys5q5It29h+oHxGScSaHKmTWikpf/vb3z7t2267bdpVIqMc5cgjjxy7gnVif6zyfWUiyvNsH6Uvjsvaz6s0edPvtk0nQVwrs793ctQq/91GCt2xlsnYuvMe/t6FatT7a3d1XCVYPou2/sPyd5lVa5m9VidZte1q1lLLrL/RF3hd/ZjjbYxlP9TfWBeW3b6qTxpj6UvMsL6fOfnkk6dtZs210Bnb1rZRCiyGdYyxrDfrVsluV/9Vqu982Unhu/OV7I2xlNDpE5yXujmmypqdb60Xz9Efet06Djv/1IVQVGwjJYT6GmX1rgmqfNj1Qc1Wvgll6/W5Op9q//CYtQzuyhS7daJSTKXLdd2xTVbW/UgXelPb0Lpytw6f235if6i+2L7ZSUK7HQjq8Y4Hx1MnB7fs9ssxln3osccem3aXvdvnMAxijKWc1vnG9bmyavvcCSecsLiW849zt/gsNdzFdakhNtal7yPevz6Xz+/62LpzvWr71LWG860+7pZbbpm2daxdQyf0w0q8f+VXfmVjedfCvG6++eZp1777ROymFwghhBBCCCGEEP5/8mIbQgghhBBCCGGn2VqK7Ody5Ql+4h5j+Yncz+JKyvxc7yf2KldUluK1lJ/4+Vo5lDKnMZafyJU9dRmD1z7dWy6fV6lCJxms8l/lHd0m3UoYlARV+VgnF1V2ZDtWuaXndFKpToJUZaydzLFKhzYdX6XIyj5sI+WLZkhVHlE3qVceL9bRG97whmmbvc4sbWMs29VMhPsdpXZrWXo7eam/d9Kv2jetH8dPJwXuJIRj9LK2TgqsHKrKIbuszmI7d3Lner7H2f+7bKZr2Y59FsffWtt12R278m8jS6vX0n9Yxz5LlWwprVMW3fUj625NDtkd53X9fS3bb5VK7lfMmKufq9JW/anSxEsvvXTaz33uc6f9fd/3fe09HcfOZWZYtY9br1W21klY7aNdtljn8zH6OdJ1h/O+965yyS5TvH7I+dk+XX2CskzP97rWYw13sS2VmPq8ton16NxV/9b5UNdN1rHSyXqtDtvRHS5qdl6zx1pf1oV157VqOawXr/WN3/iNT1jezyX6UsdJDQs5++yzp+2axf7sGHdc16zyXbiMY8Y1tX3RMMExluv4s846a9r2QXee6MJzxliOR9u9Cw9y7qp965hjjpm2axLLq/zYEIfrr7++LZfXMgzO0I3qoxxbnmMYifWl367zldJ8x69jzr7T7YYwxrJeLb/9qNtppGad7+Zhs6K7VrdP1nJZ5mRFDiGEEEIIIYTw/xR5sQ0hhBBCCCGEsNNsLUU+9thjp61ktsoD/Lefkv0srgRNKUmVCXaZTZUb+OlbeVDN5uhndWUU3t/zlWtWaZrPaIZlMaOgx9dP995TCYif+y2v8pr66V5Jk/dUsuKz1M2VlX920jBRTqKsrZ7TSbu0bet6P8tiu3qc7WVfqxkvu+yZypot73nnnTftKpc023XN0rmfUeqk3EUZ1xh9lmKld7aNx5j9eoxl3+76g/ezLLU/2KZKiqr/2EMfUUMnlCd2WUs7qU+VB/mMPkuXGbLLUDxGHzLQybHWZORdZkufq8q9u7J0v1uPnVx57XzLr+24uvXWWxfnW+YuS2UnK659Rd/XXWu/YT3Zx2tfes5znjPt7/7u7562UmbbzDo3Y2g9Tgzx6GSq3m+MPit2J5dfy9zdZdfsytuFE42xHK/d/fVPXRbvem3P77KD1+eyXvW7yvz04bZdXZt04Qau07aZ98dYyiqVEnsP/ZvzQfUJntPJsp23u90fxlhKNKuv3xXsc3Us2W/POeecjb9b157vDh5j9NnurTfH0loWf3eFuPDCC6ftvOCY8bpVvv/pT3962s7vnq//dn2sdHqMMY477rhp+/w++2mnnbbx9zrfdHOZ62j7qWv4MZZj1mc+5ZRTpq38WZ9ovx5jmRnc9rJe9AtdSMYYy/c5+04nE7a+a3Z1y+zfrIsTTzxx2t274BhjnHrqqdM2Q/M25IttCCGEEEIIIYSdJi+2IYQQQgghhBB2mq2lyH6+dmP4KvVUeuPn6y4jX5fFa4zlp/+aEXMPP7d3WddqOTt5UpdltH4id+NlpUKdhERpWJUBKE+q0oU9lBQpz6mZ6ZQBdHVsPShBGGMpadhGxtNJSsdYtl2X/bWTeK5JoJRjKR/0ebWrxNJ/H3XUUdO2TyhZsX3MMDjGUkJi39vvXHTRRdM2o6bZJsdYjocu26R9UEldlXMqhfG6nWTXeq+ZxO1DZoP1WsrblK5VaaqZRu1Pnm9ftm/WDKZnnnnmtK0X+38nk6+SHmVAjkVlWmsZzi1bzVi+R5fBcE2WLJ1E2fFXs6Bbli7DunIyy7Uma+7u32W1rVS/vAt0z1nnvpNOOmnaStjEeUz/t5btXp/QZeG2H+of6rUcl9q2udd97LHHFtcyFEXJs77D+urk+bXM1qvjsNsZovoX66XL/K9PuO+++xbnG0LltSyjvqNKFkXfpXzYMns/ZaD1ufQdnR/T59tXzKA9xnLd4hzkPSyv80QNAdLfrK0j9hv2zS6cb4wxHnjggWlbP47xLrxubacC68oxb982DKyGGpkJ2XY3bNAQKMtlPxtjKee95pprpm1YmH3G9Uz1fa7L9PGu/QwPdH6t6z3/ZrtYfs+p/c81gePJenFN6VisIYzvf//7p22oiePE94b7779/2jVMUx/p+HMs6bt8/zHr9Bj9zjLWveV63vOeN+2ahfquu+4aT5Z8sQ0hhBBCCCGEsNPkxTaEEEIIIYQQwk6TF9sQQgghhBBCCDvN1kEIareNo6hxscZSGCdmTImxImrwK8YKdPFcXTxOjf31uG7rEZ9RDX3VyhvrY8yScUpq4k1pXVPwG1frPT3H340tqHGMxlyIdd+1wxjL2IouftUYBuu7tk+3pYxa/ZrmfdMxYyzb0jIbb2J9mW6+xkcbG2KM8Xve855pW8eWpcbo2XZ166T9zIEDB6bt+K3PYCysfd44Z33B2jZYtqHnG2tj/7X/nHDCCYtr2YbGtHTjz/TybsM1xrINjaHptoLSd/gcYyy3B7Bcxv/5vNZvjVnyb8acGqtiPIz1NcayvbqtNuzP+sE6/hxzHtdtMWMZa2ye84N+QR/7/Oc/f9qXXXbZtKuP8Rkd59a3bWed1tg1qXFa+xXrvJsfx1jWbRdHftVVV037/PPPn3aNPdbnOqYdb/5uv6y5LSx/Fw/ZxUjXucu2tc/pn7qtyWo+CfuofbyLI/Y56jZSXRvpd41ptr/We3bbs1jfdd0jPrNtb73ce++903ZOrf7FMeJ19Y8+u3kOKsb/OSd7f2Np17a26vKn7HdsZ5/bbXTGWMZdXnvttdN++OGHp+0cZ1+uY9l5sdvKp4s/rVvYOUfbPm4Taj933WyfG+N/3sJpD2NZ3erIvlnnd8dD5we7mHpjScdYjhP7o+8N3u+RRx5ZnO9a45577pm2ayXzA1gu/e4YyzryuYyX1a85J9b69v6+j+mjbC/zktQ60i+Z/8T7P/jggxvPr2vGuiY6GHZjBg8hhBBCCCGEEBryYhtCCCGEEEIIYafZWorsJ3IleFVKrJREeYPbNvi538/VVUZz9913byyL5ys90a6SIKUHXTr1ThZdt6wwvbap65WQeH+lAjW1uVIPy6+8ws/41qnSjjGW9eczei3PV6ZSy+n5VU64Dd22HmuSuT2qvMh/K+X0eU1h3kkzxlimXDfV+Otf//ppKwezT9S2syxKhPY7SoqOP/74aVcZnFKlrt2sE6UvbiUwxrI/6EtMF680XVlKDXdwPNqflScpfVM2V8dyt52BMlmlVfbluk2IY9n7208uvvjiad95553TVrI1xlIu5H2UiysPMuxjjGU/139020r4vLWtlZ1Zf27L0Mmaq1zbfmBZXvayl01bmZZt5/0qziO2qc/iczgGxlj6SKVl+xmfTbv6z6uvvnratrlzlGPf/q4seYylBO+mm26atuO1m8eq/Nu+4HFK/rq5o0qXbfMuXKYLK6lbxohl6bats4w1vMb7a3vdNel+53u8j/Og569t1WT96UO9Vte/xlj6C9d2jjGfy2ev0m/9k9s4eV3lj967bpNWJdO7gu3sVjTXXXfd4jj7qlv8OA/bT23bup2k82rnv52HnNNOP/30xbWUxrrllOFBzknOI3VOtp9bftcNzvUf+tCHpr0Wxmbf9D1FP3jcccdNu4axOUcbHmQZfR+o519xxRXTdr5znFpG66uGibn277YjdE3guKhjRt/vu4190vpyDVPX1937nH3H+dUyul3iGGO8/e1vn/Y27w2SL7YhhBBCCCGEEHaavNiGEEIIIYQQQthptpYiK1uoEhdR4qUMTRmEElhlF2ufyLVf9apXTdtMpHfccce066dr5QZ+YldC6md85RhVsqskzr9ZR12Wx5rlzmx2yl67LI8eU6WQypOURXYSrppZ0vOrpGMTaxLjLuOpKJnpMiTXsnic8hnLomyiXtf2NhvjWWedNe1OtqEUpt7/5JNPbsu/37CuHMtKs8cY44Mf/OC0lZh0WWP9vUprvaeSX+tUKb+Z82qWxK7fObae+cxnTlt50Ktf/erFtZQDK5FT2tVlPa0ZGH2WCy64YNr2E+tIqVKVbvvM1r0SrN/8zd+c9lpmSX1nJ7O1XHW8dtJv5VjWi+On+oVbbrll2l1WTrMhrmVTFX26Y/awww6btu1Y57DO3+5nOmlsrXPniSuvvHLj+V2W29pflD/qS61/5yjrsparu79zaicFdn4Zo8+c30nYPL5KYzuJvnOJ5zjXVvmhY8/r+ixr8lnnMqW91pHP4v3r3NdlAtdX+CyWXVlw/bdjRyllF+ZRw8SUsetrfC59ghliqxTy67/+66e9S7Jk146uL+t60XXKaaedNu2bb7552vo2x+hLX/rSxbXe9KY3TVuJsxn9b7vttmm7hrcc9Xz7jX1Qma/t2a0Px1j6G9vWdwvHQl23ug5w/BnGI/Y5Q4jGWPZtx5k7anhMHTPWmW2kv/Ucy1LfQewvPrN171jyGNcTYyzfQawX20jpuWVfy/ysLxHP9xlrm/i3tXfOTeSLbQghhBBCCCGEnSYvtiGEEEIIIYQQdpqtpch+lldiUjOW+ilcGZif0r2WMpT66V55w9FHHz1tP1Er6VEGUGW64mdtpVJKHP29SlqUdyhV6mQ0UsvlPZXQeU/rTrvKAJT4eB8lAUrGqlSg2xi7ywDZHTPGdlnMuvp+omvvYdt7LSWtNeOeMjUzLZ5xxhnTtu7t31XaZZnN7Ljfueaaa6atpOe8885bHOeYe+ihh6btc5sh781vfvO0lamOsey3XT+zPZVgeY8xxrj++uunbZ+3rdz42/FaN/1WwqtsSd9z9tlnT/vyyy+f9ote9KLFtcxiqOzPsvzUT/3UtO2L3mOMZV0oR7zvvvumbft4zBh9hnjl0vpq/VgNUbAufBZ9jBIk2+4tb3nL4lq2pdIjZdWdnKnONbaX409plz5VP1KlXWbvXMuSu5/ostyuHbdNiEknSx5jOfebqdMsncqXq+xU9CPa3bM4p9Z5yHnNucc+04XHVJmbY7dbKyi3dt1S5wjnIuWL9rG1+dX673Z2cBxbR7Xtunq1/Jalk2TX4/Sv1ov16LXqOq+Tf1qX1pd+1nCiMZY+scp49zPKzG3PmslYbE+z2V566aXTNiSo7vqhjPvHfuzHpv3Lv/zL0+6y0leZrscZxqMEvdtpo/pbJcvf/d3fPW3nAtcj+vgqt7ev6btc4zkP6TvqWLavGYbUhV7U0DWPO+GEE8YmbBMlw/q3em3r2LWVvtd6cd4cY7kOtr7se57veF+bK10ruKawjnzG6mMMVTID/zbki20IIYQQQgghhJ0mL7YhhBBCCCGEEHaaraXISreUStSsdEp/lIL4WdtP+n5id7PeMfpMm0pRlB8rjaqSX89RYqCcQ1t5TJXwdPJnZZ1m8/TZq3zY+/i8Sj26jJNVStxlg1TepF1lul2W4k7y5j3WpMid/K0rY5Uxd+dbfiWOSr5q/1Tmcsopp0xbyZnSFM9fk78pZdzvOH5tz5qZV7mMMhFlMDfccMO0lbSsSRAdD0pnlGN5b8fVGEvZuBJcZTRKYL/hG75h2ldfffXiWt5Hn+H4s7yvfOUrN5Z9jKX82kzM+h77nOfXfurf9IudXLpmdtUvKdO1jfU9yseqzEy5duc7lReZtbRmOjS74q//+q9PW79oeIrZxqscq+vH1qW+QCldzcBvHZldfz/TZduufsq68bjOR69lx++kdp3/s4/VOVl5m/6ly/Csv69zhGXWD+jXO59Uf9cPeR+P6+q0yuV9Zseoz+s9akiO/de67OZE/Y5zXb2n6F9tk+73MZb+xbWZ0mvP8ZgaNmGZXcPYjp3EuMpr7QeWcb9jO+uL9VljLMe8fcv61U+6u4BhLGMsQ4/uuuuuadc5cg99Zq1bx4xt4C4p+ljXrnUe/Y7v+I5p+4zWkdfymOoXrAvr0ns63ygxtk7GWM5R3sfrKr+tvsBs085d1qV+xZCOKrn3nvfcc8/G8/WXvo/UcllHjsVTTz112ta9Y+78889fXKsLx7S9nZ+se/v9GMu1gpmbtyFfbEMIIYQQQggh7DR5sQ0hhBBCCCGEsNNsLUWumez2qBmD3UTZT+lKFZTk+Bm9SuD8rP3YY49NW1m0n6iPOuqoadfsp366NxOXchllNEp9quyi2yxYWaOf7r1WleZZr5ZRqYMSELNPrsl//dzfZV2sm8krVeo2N/d8JViVLhOntuevZevsMtV1WZVt03pdpU533333tJU6iX31hS98YVvGTua1H1HOdMwxx0y7ZgG0fpTYKMlRunLBBRdMu2adVSKjNNd6U16ktPH973//4lr//J//82kr73nnO985bWXRymDMIDjG8pmPP/74aTvmPcYsk1XKr1TROvLZlVH77PrBMZZ9/iUvecm09RHKimvbKVNWmquUWUmS46LKfpQ0epxyKq+l71BKPMYY11133bTvvPPOaSuVsk8qd62SUecX60s/bj/UF9Rn1H84h+1n9HM1k7Vsk33e+uvmkTGWsj/7mLZjzDar48Vx6XXty530ss7Jtm2Xvdg6Uia35rutY6/bSYSrFLIbb1071EzG9l+f2bFnG+lrPKaW03mtk7Q7JmsddbseuJ5Rhmr/qGsLJc+ebx27/tM/1H6/ltV2P+OzOn5qeNAHPvCBaXdZrr3W2lrIdviJn/iJaVvXznfuclLD4FyXel3DeOxb9od/9I/+0eJaXVZy7S5Deg3JOXDgwMbjnDvEubKOH8dJF9Ln2qbOfdJl67dNuzm14t+su66Mde7zHP11F57iWtm5doxldu4PfvCD09aXuIbRr9Q+5XrQdtyGfLENIYQQQgghhLDT5MU2hBBCCCGEEMJOkxfbEEIIIYQQQgg7zdYxtjUGbI+ajrxL6+yWFYceeui0jamocSfGGnhdbXXgpsSuMcFq6o2xVbdvCnBjG2osabfNhbEJxhCY3r7Gqhi72KXEv/baa6fdaeBrOWts1KZjapyM8SpdzOtaLOw2Zem2mOi2JKr/9rrd1hO2aa1v436MWfK6xnJ5/G233ba4lqnKjfnY79i3jJF96UtfujjOPmydGK9hvJN2TeHvFjDG3eg/jIs1jsP4jDHGuOmmm6btlgWXXHLJxrLrR0466aTFtYyJ0UcY46svOffcc6ft9j5jLPMI2B+sr1tuuWXaxs/VWDG3UTIuVx9nHVU/bOxLF09oPJK+p8Yv+m+3Q6gxk3sYV6jvGmPZxo7Zs846a2N5jXNa88NdXKhzhc9RY6z8dxdvv9+wz2yb76Crsy4esW7zYpyZ5xh/59ipWzR15bIv1dwPm65Vy+W19FXOET6746Bey7p0LvH+Xa6KGgtnfZlDRJ+yFitd847sYX35LGvbrFlO+7vPZYyh967l6rZhqXGJm8pV4yCdK/yba7BPfepT0/Z5qw9yy5BdirH1mSy38+YYS990xRVXTLvLqeIcU7c467YF6nJwuN6p2yw5/9g3XN/ahsbV1j7umss40y4W3HvUZ7zmmmumbbywawrfTZyfnM/r/e3zlt+y1/Od77yW7eA4832izsnGpet7HX/adYstsc6MkXUedw1lX6lrIOdb1yD60S4nQI0Zt8z2w23IF9sQQgghhBBCCDtNXmxDCCGEEEIIIew0W0uRlcusSZjcGkJZy4033jhtZSl+0vczfj3Oz/3KNjzfz931073SBeWXXldJgHIOn2OM5TMrHVDe4O+WSzlC/ZsynG6LBj/1r0ltbKNOvlvprtel9t+WTr7clXHtfp18WWlGJ5cbYynNUeZif7GObJ8qv1Gas61Eez+g9KvbPmaMZX90myrrxO12/L1uW+G/rfdTTz112srKLIvS5TGWfsKtgJTUXHjhhdN2668aUqFk+eabb562W//cdddd01YeU2WW1pEyIPtWJ8mpcmvL1W2x8653vWvadWsBZUTdVjj2Z/tvlVNar5bTa917770b7SqBUup16aWXTlv5se1tuarMsZPfKqdS8unYr3JX55qf/dmfnfarX/3qjffYD9gv1vx6F2Ji/XWhI1WC2m0xYh/pJOpKFMdY9jPv6XGWq5P3j7HsG/p8+6s+yHm0276v4rrDvtRtsVHptt/wGfWNYyzby/K7FUYXAlW3XPP+jn3Hgu27FibWSaG7ttMH1XGsf6zbh2y6f7e13xhLGbw+fJewfpxfx1j2h8MPP3zarn+6sC635Blj6fM87uKLL27vv0fdTtPwPvuW612343TdbZuN0YclON+69vLeyrPHWD6Xsm7lx64nnK/WQo08zt8dMzU8zWfxOMecftS12Zqs2fK7ZtN3Ww/VLzhOrVclzvpIn6O+G3UycvuL/tk6quFU3Raz25AvtiGEEEIIIYQQdpq82IYQQgghhBBC2Gm2liIrbVP+UWUlDz300LTvvPPOafuJ2sx1fgavsic/U3dSlE6WUuWs3sdP4Uo4vL/3rhJU5SDe3+v6GV15QZXPKYny/tZXlQ7sYaayMZZSDcvSybyqXLST9nXyIp+9SrC89po8axNV1uuzeH+Ps72t+9qnujJrez9lu2bVG2M9+/J+RvmI0pUq63r44YenbR9WOqO8SbmM8rh6LX2J7fPiF7942vbtyy+/fHEt5WrWu89l9l7lr1WafuKJJ07bPmD/PXDgwLTtczUDqfImZbY+i7I7JT36oTGWkkIlPUqFzCRc2667ln1WOaNSoSovMrOkWaS7jLf33XfftOv4++Zv/uaN9+98nPWt5GyM5TyijMnxr6xNP1alXfZvM23vZ6ybtQzJXSbkLpPwWuhKF3KhrFiJpH2/ymwdY5bL8npMlxV1jP850+4e1ovP6Nit5fL+nVzT63rvtTAax3uXYbX2y25O7qTMyjqrXNtQiU663vnzmuVeH65P0Q91oU11PWCf0g/q6xzTystr2IRlqeuj/UwXIlBl8raP4Tb6XP2idfCa17xmcS37nfd01xJxjqjHeB/HmeU3RMTnqCGIzgXOXfoVj7HPOwePsdwFwTWI13VOtu7cgaCW33AZ+6NzVJXS2lf1E75rdBn91zK3OxYtv2XUp1UZub7IunD82le8rmN0jDEeeeSRjX+zvV2P2Y/qeLUuXCtsQ77YhhBCCCGEEELYafJiG0IIIYQQQghhp9laiixKb5Qej7H8ZH3CCSdMW6mEn8KVAdWMi51Uyk/nShL8vF9lgkoiumspdZAqc1Lu0GVS1lYuWbOEKkNwA2vlz9aRsocqT/AZlRp1MpcqJbT+uk3uO1lzlWB5z05KLJ0Urp7fyY+VEirHqLIn/6188SUvecm0O8nJr/3ary2upaSiSqL2M1WCvkcNK/DZu+zd/m5/qn3LDJXKgBynZiy+4IILpl0znN90003TVnKsXM7fu2yEY4xx2WWXTdvxr6RGaa6yH8frGMssir/wC78wbSXs9l9lOFU26N/sZ8oJu2zLYywlw1ddddW0zVpvvThmqjRM/6Nf0vcrmzLT9bd+67curlXbcg/Hpf5SyWdtO/uhZdYv6LvMxLkWHtHJovcbnZS3yoWdV322Lhuwx9drdffRZ5977rnTfve73z3tOl66bLyd7RxR2687zv7m7/axOt/U7M2b7qmvtE5q37H/Oo9ZXu9ffbPjzfsoeXTu0+/W8eLaQ7/r83bhWFUK6HznOqSTvfscdW5wTfOUpzxlY7mcP9Z2UOjWXfsd61OfVXcq6OrHsWU/83iz+4+x7MNmwD3jjDOm7dzn8bVvKVvXz3ZttbYO1S85l9hnrRfnnuqv/Jv9zPKaYdh5sIYgdpmFbS/XHdb9GMt51LWGZbSOLVcN6XGt04VYuKbw99p2XXiSvtPf7Ss183MnEbd/Wvf2g7oGcp3YvTd05IttCCGEEEIIIYSdJi+2IYQQQgghhBB2mq2lyH7KVjrg5/0xlhlLlQUoT+qyKptFeYxlVjKz5XWbyfspu36693O9tmVUWrcmR/P+1ovSF59ReVHdhNjP/UofLGO3obGSj3qcZVQepAygStFsO+vP9q7Sgz2qNMyybJMdcU0aWCXqe1hHN99888bfqzTF+yj/UfqqtMQ+qLxzjKWM1br8kR/5kY3l3S9YP2aiq5Ig200JrhmPHb9HHnnktGsogLJRZb6OmYsuumja73nPe6ZdZd5nn332tG0f+7Zj2fFnlsQxlv3ZrInKgLSVZ1dJjyEWSmMd18rcayZkUWqlL7Lun/e85037xhtvXJyv7EsJ4k/+5E9O2/GuTKq2neNPf2WoiW3y3Oc+t72W/7Zdlbx1oR7VJzsevae/e4+10IG1DNX7ldNPP33aaxnqnScc011mXOu5tp/1ZB+znewvSvvuvvvuxbW2kfZ1x9e5y/a0j+r/XR9Y9poVWZ/kcZ18eS07vv/2ufRJ3Zxaz7de/d1xbHmr7N/n9Hz9pmW0Tut6yrbo5uduZ4Q6jp2PlG/6XJ7ThTmNsfTJdX20n1F2KXV9bTvoz2yDrj/VsEHb5+KLL552t16zzziux+hl+qeccsq09SW33XbbtGvGbXcqsN09p8vY6xpkjGUI5FFHHTVt+7xrcncz+Jmf+ZnFtaw/x4ZrI/tvzT6sj1Smq8zX9u7W/RX/Zl1aL9p1Tey7SrfTgWPRMlZ/57rHuvf+tqn3q/1A/2Udb0O+2IYQQgghhBBC2GnyYhtCCCGEEEIIYafZWorcZe6rshIlCTfccMO0zbRmxk9lPAcOHFhcy0/5yhv8RK+kQJlGzRjsJ/NuQ2Q/i/vpvUptlDQpfek2TVfKWDMuKsHrMiz7LMoKq+Ssky+ffPLJ01YWaQa3MZbyErOYdfImpSVVvlLlDpuuJdZXbTuv5f21lbparto//bft5T2U+InZbcdYyi5qXe5nuj5b5ZlKuZThODYcl/afWofWj2Ne6dCtt946bdupSgW7zeQdp8961rOmrTRVufEYyzGkxMZntG+ZwbD6BWXK+jJlxUqnlVxVGc4tt9wy7UsuuWRj+a27mtH6ve9977RtF+VY1qubqZ955pmLax199NHT7iTpll9fUEMvbHsl6c4p+vE1OaHj3GdUov3oo49OW5mY7TjG0q+a9XE/Y2ZNJaSdlHeMZf0rD7Oe9YXVJzhHOZd0YTAvfvGLp33FFVcsrqXk3Da3XR17XbbiitfqQlzsb1XmZt9wjPjs3XqoltH5zrVKJ4Wuc1+XVdnfO1l0zTLqtTu5dbfrgf5wjGW72He8luU1c3P159a3fsR+2IXCVSyz6679TtcGdR3l8zl+pMuqXOcI/Ufd+WPTOZalrqvsd/p815G2h/NN9VfON4ZK2U+2zayrFFp/5fnWkbsG/MAP/MDiWtdee+203/e+901bP2qd1jWx93F94rj03cQ2qZJf+0g3X9peSp+rrNk27vyCda8vePDBBxfXcr61Xlyr214+Y+1TzsNdBv+OfLENIYQQQgghhLDT5MU2hBBCCCGEEMJOkxfbEEIIIYQQQgg7zdYxtqZuvvfee6ddY8NMW66u2vg5t/XxfNOXj7GMiTKezdjbLp6oxlCpSVe77dYOXQrxGvdiTI06ds9Rx66GvUvrXqn33HS/Gk9g2vBuOwBjVQ455JDF+S960Yum7XYrxmR22ybVeMMuHsryGyvzile8Ytq1jox/M07D5zfe0N/XtPm2kX36bW9727SNATCmdowxXvayl03bePL9js9h3LCxLWMsY3iMiXna05427QceeGDaxq3UmGO3Y5EuftvYHO89xhi/9Eu/tLH8+hLjXR0XtW8Zn+K4/tCHPjRt/Y2xMXXrIGN5jT/Vr7zgBS+YtvEpdVsV402sywsvvHDaV1999bSNYavP4jZKjh/Lr7+psUHWhdsViX1KX9D5sTH6XAX6dHMQGJszxhhHHHHEtC+//PJpX3XVVdO2H/yLf/Evpl37p/7u9ttvb8u8n7Buuxi5MZZzgf3CdvL3Gs8ldV7dwzbTxxu3/p3f+Z2Lc372Z3922raH/cJ4TON96/M6v9dtuPbotgmsW6roR5x7uvh67Trv+bcujrjr72Ms69K5zHI5j3bxerVsnmNZ7Af6zTqOu3hHsR3tUzVu1LI4H2h3Zaw+QT9o+fc7tpXtVHOS+OzWr+1h/+/GwhhLP2/7uo71fOfkGrvumLGf+rvjzHm4zu+Oh87f2Laur+uaWJ/hc/mu4drP+7361a9eXOslL3nJtF3PfOADH5i2uTFq3zR+t2tjf7eO67U8zr/pY7oY9Rov362RLZe5TFyb1C2NzFdgXeqvbV/XU3VLsQ9+8IPTtr2+//u/f2N5JV9sQwghhBBCCCHsNHmxDSGEEEIIIYSw02wtRe5SkNfP2H7WV97gp2wlZH5urrIJJS5eS0mDUmI/z1e5i5IgZU9eVxmD8pgqt3b7nBtvvHHjOUol6qd/8dqdVMnn9RmrfFHZVidjUBJU0+7ffffd0+62FaqSiD2qfLGTpnkt28T+oT3Gsh8oAfEe1qOSk1pHnqOsxxT1SkusB+XOtZzf933fN3aFmlZ9D7f0GWM5tj/xiU9M25Tybh9jfRpuMMZyzFcJ7x7KyLxHlQ8rAbc/WsZ77rln2m4rU6Vdtunzn//8jce5lYz9ocoGPcf+qExX2ZRlr5Ie+7xbCCjX9v7V333Xd33XxuO6e9qOdfsT/aXP4nGd/LTKWvVfdfuJPfQLblNwzDHHLI6zX7z+9a+f9m/+5m9O+/3vf/+0f+InfmLadesupchuC7GfsS6rLxfbRj+tb7SdnIeqtNb+U33+HlUOuMdpp522+Pf3fM/3TPunf/qnp21/9f767noPx4vrCPuSPkVfVbePcYxts3WQ802tr07+222rU+nmTn2S4QX6nVre7lm8v/Wi/69zhs/ide2H+g2vuyab97oeZ/va72yrMbbrn/uRLqyr1nvXPm6L020FVdduhvo5FzgPuy5yrV3nK9GvOL/al51vvO4Yyz50sNt0Vumyz9L1DdfqhrFUif3FF188bcP4nHsMzXLrm7Uy6698Xsu1Nic5BvSRztX2rzonW68+s/Jf+5THu53bGEu5uqFW3t915s033zztGq7mHF3n/iciX2xDCCGEEEIIIew0ebENIYQQQgghhLDTbC1F9nO5n/GV/42xlFEoJfF8P1H7eb5KZzrZapfNUamBv4+xlG10Gegso5/kqzTPbLFKNbyHMjvrocqezDBrXZqhWGmHUu+aRayTmSkBsV7q+TfddNN4IpQ6rEl9uuyGtpHlUgatdHSMpdRLyaDXtR0uuOCCadf69v5KNZQ1Ki+///77p10zwv6bf/Nvpq1kqMscu19QBqS0tkrilLV1dW3/tW6V64+xzBjseDDTYJeBsWawfd/73jdt+7n92XZXDlXHsqEIv/IrvzLtV77yldPWD5nBsWbCtg/ZH5TmKq31+DqWTj311Glbx2aEPv7446d95ZVXLs5/+OGHp21/VNb8vOc9b9rK6s2AP8ZSuqSc0zL7jNZXzcxa5e57mHnZ+ylb95gxlv1C++Uvf/nGeyhRNkPlGMvnqqEn+5WapX2PmqG+zoV7OHYc39vKZLu5oMu2Wuf3c845Z9r6jv/yX/7LtPVPXfbPMZa+w7nX/me9mC21hicohXSO8h6d/Njf69+sly60q4YUWH/6BP2bskh9c5WeKvtz7Dt2XfdYX7XtbHvre5uM1GZCHWMppfRaykoto+ukKtG0vrqwqf2IO3XYh2q4hmNW2x0N9AvKjdeyZBsSaH9Syuvva1nJu/GvtNZnrPLhbh3dhQfZZ6tfUOZseJRlce5Q/urOBmMs59hLL7102vZf/Uo933rpdlNxfeKaqYYzWv+ulbqM8j5vlZFbfuvYkBD9zW233baxHGP0c+dHPvKRjb/bP2rYoO86J5xwwsbzO3Zn5IcQQgghhBBCCBvIi20IIYQQQgghhJ1maymyG937ub9++vff3UbRfi73U7ZysHot5QldBjk/o1f5VZVnbTqnkzRUqUS36bSyKeUBfuqvn9u7jcctr5JFP+lX2Y/yhk4CtU2WxzF6aZr36CThYyyfy/bu5FjWnTKnepzZ/5QaKW9XormWOVoJiJklO5lZ7UPf/u3fPu0uw+t+RDms8iLlPWMsn8l67LLw2bY1Y7D+Q0mdUqlbb7112rZnzUatNLeTIjtmlRA5LsdYZmv+5m/+5mm/9a1v3fi7/fTss89eXMv7u1G7mRKVMivLrdJtx7bSTCVn11xzzbTNAj3GGBdddNG0bWMlRT6LMrUqG1JepY/UdzsWlVDVsIIuRMOxpURVqWENK9CvKK9XSq2s+cd//Menfe655y6uZb1Wyd5+pfPL1X86FrqwkG4uqPO7eM9OUtbNF2MsfbFSsze+8Y3T/tEf/dFp64urfNhy2ubalstrVcmq8sUu469zRJe5uN6/k8baXmsSa+9pGbvworqechxbZse0Y0qf7bw7xjLjq2EX9oOu7s22WsvpOs9rWV7LWOddy1Kzv+5nDHnS/1RfpCTVtnbutJ+ceeaZ0z5w4MDiWvaHbo1oO+uva1byLiu+40w/ZHmVyY6xXDs4zi2XY0ZZfb2Wfcis7I7Fu+66a9rOMa5NxliuFXz+LuyuZlV2zPqMHmefda1Q11NdlnDXdta9/cb1wBhLab/1ZX0b2mQ/qKEAjm3r0jWjZbFPuOYbY4xTTjll2sqftyFfbEMIIYQQQggh7DR5sQ0hhBBCCCGEsNN83mdqGr6Gb/u2b5u2Uq+6MbwZg5U6KB3osvRWeZH4ud3P8N2m5xVlBUqJ/XTeSQrMDFf/bZZRs4/6iV4pYpXh+InevykJeOlLXzpt5UFve9vbFtcyu5tylC7LZW36TgptXVjf3TH12t351tGaNMx/ey37RHdMlcx0sinlsbaJ2QJrJlVlL8p4f+M3fmPsZ+xDtkeVrlkP1puSGqUkjnGl3WMs2+q7vuu7pu1YvOOOO6atBO9d73rX4lqdbL3LeO3G3zVrqf9+7WtfO22fXTnty172smkbIjDGsv6U8SgjUoajBMgMgGMsx4916bV8xuqj9HddplT9s/eobadESJmyMkD7kWO5SqiUMenHfRYzQ2pXiXQnQ7dNlZ5bp2adH2M5fpWD/eAP/uDYr1x//fXTXgvD6UIE7OO22Vrme6/dZT/tpLVr4Sr2HyV4l1122bT/83/+z9O271b0D/Yr76/v1h+OsXwW1zP2Jfvu2rqlW5N4jv2yzn3WcefrvJZhODWjtXXcyXSVvnrvGsJh/VkX9i/L6Jisc7LHeV3LaJv4vEqXx1iGrbhmffWrXz32M936o/p1M9paP64dzbBsdv/a5o4h27dm4N3D/lQl88432j6L4SLOMXX8WC7nSN81XOsaxuA6ZYzl3Ge5utce5/Q6frqwAq/lnFjXga4jut1guiz0+sR6H8vSzYn6PttkjGXbu55zzNm/vF/1na7p9QWOef3Fmuzech1zzDHT/tVf/dXxROSLbQghhBBCCCGEnSYvtiGEEEIIIYQQdpqtsyIrS1nbuFsZgZIIPz8rZdwWZX5+Iu/ks1XC4adwJT0e131ur/IGs74pmfTTf7fRc312s5hZl95fKabtUGViPpcSDJ99LUulddltdG5ddJmTx+ilYtZRlxG6yrEsp3IFn9fzvW7NEKqcxGuZKa7bMFtZ2hjLrLY1c/Z+xj6gXKTKdJWudVkTle44fqqk5oILLpi28kDb+owzzpj2j/3Yj027ZrBVfmaGZJ/LbI4veMELRkeXKVVJm75LeVHNeuj59i19l3VaN0oXMz3qP+xzSsae9axnLc53zCiz9bkOPfTQadu+NTt2l8HRPq+0zOeqWRO7jLVmdVZmVjM4ivWqxNk6tq/Yb6+++urFtTo55H7GPtZJNcdY9hP/1mV8d0zUzKDOJR5nH/G6zgN17rP83l+frd+wX7z//e9fXMv2s18oX/QezttVIm1f1v/rHzu7siZT3lT2Ol9Z/6effvq0DYHqMkKbmb3+zTHS7WTR9ZUx+l0AXB/4XGvZte1HXte1WZfNu7adbV/XgPuZLsRKvzbG8nltQzP+2mfW1rH2W8ev86j9wbYxhGiM5TpW2/lZP+Q8Un28Gbjtd64xbWf9dZXZ2uddE9g3Ooly3VFD/2EbdRnKXYOPsVwfWS7nUa9r6FsN+XQet5zWve3rddfC8/Q/trfXst/VNZD9zTnJttPve90671qvdR3xROSLbQghhBBCCCGEnSYvtiGEEEIIIYQQdpq82IYQQgghhBBC2GmeVIytsVmm1h9jqdFWk65W3hhV4+3q1hAep0bbuB3vYWxMjXtRo97p27tYlxrzaayAZbn55pun/fa3v33axpfUmAn1/Z3W3biBtdTga9sGbIOaeO/p7+rzPaam6u5ivrptITo9fqXbOsi60K4xTl67u5Z92lhpt40ZYxlDsbb9xH6ji0Gu9W78hKnX7cOOM+Neavzoe97znmkbn3PiiSdO+/nPf/60f+iHfmhjOcbotyPQXxhb6XY1N9544+IcfYHta2yQ23jddttt0zb+bow+hu/++++ftnHa+oVTTjllca3f+q3f2lh+72Fskunwx+i323Kc6nu7bXzGWMb1eh/vb5sYD1NjY3z+Cy+8cNr2iS5es26V5lYDtoVxTvorn73GLDn+a2zUfqXzOXUe1U/7nPYLr9XFilcc+11/67b0GaOPZe62lHPLlnruW9/61mk797lNhuVd27rEvmwsnuseMfa3+lDvo9907LgGqFuyGGtvzJ2213I9U9cHN91007Stiy5XRhdXV4+zjZw7bXv7YF0DOQd1Wy7aP+vYFeeguhXQfsY66GKhx1jOkbaB840xqv5eY9zdSqeLrbQ/OcbXdgm13byHz+X93A5zjKVf95yTTz552vaBbg0yxnJs2U+7/CFrsZz6An2Mc5drlbpFj/Xi8x84cGDatpG5LlyHjrGsF+fnuibZdK06LhybNW/PHo5ln73WVxfXbt3bXvavus5zPVfjnZ+IfLENIYQQQgghhLDT5MU2hBBCCCGEEMJOs7UUuZMnKY0YY4y3vOUt077rrrum7ad3t6ZQ0lBT3Sup8jO+n/6VJCkvqFuziNsQPf7449P2c7kStjXZmuXyWfwkrzyiptpWltNtX6FEuZMK1HL5LEoHPKamf+8ki8qEO/lZlaZ00jTLorxgTX6sVKzbFqIrS5Uid1J5f1fOoTyiSjgs/8GmI98vKEOpMjilqt3WHOLWPzUUwDH3vve9b9pKlvUX+ogqAbTMto+SX6W1+ohadiU6yq3dPuGKK66Y9nXXXTftOpY9x7Lor84888xp69/qtXyuiy66aNrKxKyH9773vYvz7ave3zbVrx111FHTrv3gnHPOmbbj12sZhuG4OOmkkxbXso68j2Pc9lIWWuca204Jl77EbRGU5SlzqvdRyrefcS7otrsZY1kf9rNuyxr9fZ0jOglitxWH843XrVj/SsF9Lsfu2WefvThfOeOtt9668R6dFLCuO8RxaB05v1t2+1jl6KOPnrZj2rmj1pHSf8ebW5co3dRXvv71r19cy3Ps4z6jft4xWeXa+h7Xhl3/WqtvZcZdqFK3TWEtl3/rpOP7Efumz1THn7LVK6+8ctq2m9JU5bBr20N22965FvL8Kg31Wp0v0d/rL5x3x1iGkiglVtZs/7MsjzzyyOiwP1hf3dY/NeTJcWI/67ZHWlsDKcF17rJclrduwacv8VquD/Qr3qOuNVyHWK+u52xf26GuiW1X29G+4++uTWr/tC4PNjwoX2xDCCGEEEIIIew0ebENIYQQQgghhLDTPKmsyH6K9lPyGL1M0c/PSmqUKtQMjErSlOaeeuqp01bGoiSgfro2c6DPcvzxx2+8h5/ra2a6TtKkbGotm6Qoo7COuixklqtmPVTC4rU6mW4937ZTNqKkwftrV3mRdebfvKfHWPYqc/E+XTbGTrJWpXPep8sw7bNbliqVUJqyJmfbb9h/lZJUeabjz7pSHmVddRl3x1jKA83oaaZOwwKU3iihG2M5Tm0fn8usxrfffvu01/rp937v907bTKf2mVe+8pUby1HxGZUTKrdW/luzyyuP2iZjb5UqWWbbwrHcyble8IIXLK51xx13bLyn7eUYtY5q9lql54899ti0bZfueZVGjbEc2yeccMK0lehJF85S/7at7/5co4/XrtgX9HNd6I11UeV0Vfq5h/NwJ31eC1fpwl26nQqqZNd+om3fV7Ln2K1rlk5CZ3n1/Uqva0Zq69uxoE+wv5r5tV6vyzhsHb/sZS+btmubMcY477zzpv2Od7xj2raXkkV9aJUc6vedD/Rbte/sUevbf1sWbdvetqshQNbXLu1U0K29aib4N7/5zRuPM+TCLP4PP/zwtKsv9t/65cMPP3zaH/7wh6etz6zjz7CWTlatbahOle875uxb+mXHopLhI444YnEtpcWW2TFun9Hf1Gfswg4dM/bH6u88X1/kPNwdU+cr52THv2PDnRYMFao7pnRrAo9T4ry21u3Gn/ewHZVnV9+pDL6uTZ+IfLENIYQQQgghhLDT5MU2hBBCCCGEEMJOs7UU+cUvfvG0uyx4Y4zxkpe8ZONxfuJWBqDcpEo9uwyMfsr2Hn4uV44xRv/5XHmEz7K2ubfl7DawVp6wlsVPSZOf/r2W9aU0o27GrHTC61pHXYbkMZbyJq+1jfS6ZjVWVtDJn9faXroNy7s66n4fY9nGtktnS5Vw+O+afXk/o8TM9qxjuZN9d9n2lJ4897nPXVzLdlM2qyTP3+0PyqHGWLap5VfSpI8wa6ghCWMsJcPiMzrmlMNWH6EE0nCN66+/ftqd5LLKxKwLx6UZJO+8885pK2seow+L6DIzWsdV8mudKdG2f1jHyt/qpuu2kXWshLGbK+65557FtXxGs7yaMVK5s33KzKFjLPtn1yf2G47JTp4/Rh8K0mVCtY/W+cr72K/0910W++ojvadSsy4rs31nbb7wnt2ctpbh1ePqHLnp967u6n1sB8ehkuF6vy5ExzAAZaRnnHHGtM2IO8ZSimx2d+vVuVMZbL2Wbe84tLyeoz+sWdeVcutTLIs+YS1sSb+pT1Oqux/R/+vja4iK6HOVpj744IPTdvxUabbj0bay3k888cRp289rGJv/tj8pUXYucO6poR+ec8MNN0y7ynH3cAeFulZwTrYuDF3p1rTV9zkX6WMcs10W9TGWPtJy2ee9p77Deaw+i+PfclnHzuFV+t3tpmKbmqHZ9yyl3rWcPot9r9tZZVsfsw35YhtCCCGEEEIIYafJi20IIYQQQgghhJ1maymyUhCpElTleX66V36i3WVA3PTvPZQqbCOJGWMpmfQzfpdBV8lVlV14nHKOTh6lzK7KXD2/3mcP5TbWt5KAWq7aLpuOqTIe26LLkqlsYU2ypbzDNtpG/lvb3TJbx52UucsC/UR/23Td7t5j9Fny9ju2jVlDa0ZO60HZe5etUhmY422MpdTw/PPPn7bSIaUo+psqZ73lllumbT+1P/q7YQk1REFp7eWXX77x/vZfZbZ1/OlzlIlZfseJ8rOPfexji2spF/K5zPDcZagdY5mZ2GexzMoDfcYqGTUrtTJl/bA+3XZ873vfu7iWfUw5mPViGfVJz372s0eHx3VZKvXDdbzaX6644oppv/a1r23v+blGCZ7PvxbS49+c42xz/W+VLzq/2/5dxtFtQzQ6+Xm3VlibRztpnePQZ69hSp18WLpwlSrNc05XSmnf9xk/8pGPbLzuGL1ksZtfq+TwwIED037DG94w7X/37/7dE56vPHaM5XPWEJZN5XLsmsV2jGW76l+cm1yr+Ow1S7zHXXbZZdO++OKLN5Zxv2B9rvVz5az2c2XmnuNc6c4EY4xx7rnnTrtb03oPfYHy13pOJ6ft5o667vVvjhn9uuV1vqhZpO13Povzrb7LdYphlWMs/ZJl7uae6ocNu3KcdztLOF9V+b717fuMITnOD/r0up7SF3Y7gnS7htSxb9+zv9im1r1ZrJU7j7H0E8qqtyFfbEMIIYQQQggh7DR5sQ0hhBBCCCGEsNNsLUXuMpZW6Y2SIo/r5CprG3p7vraSAqUKa5kS/SzuZ/UuO6Kf+qusVxmEn/i7bM9+uq/SEqVOnTRXqUAns6pl7rIfe7+1jZalk/l2kt36t+5atsM2EuE1tpW8edxnY+8yyrfMllczSJtJUgmu7dbJzaqE0ay9ylKUrSrNVc5k/x9jKV/x/mbXtJ95rSqpU5ajj/N35V+nnnrqtGumTeUyyvg8Xx/XZTOs11IyrOzIbKo1Y6TPYnv5/F2245ph2bKYidk21tanu0l8xX7gOY4zZWI1Q6j3tE/bj5U3KeOu0kTl1l1IyH7D/qMvt17HWNZnF1Lk3OdcXSVwnc/uJKxKl2u9+jdtfYLju5M41rJ4H8vl2OnOHaOf06XLtlzDA6xLx6gyW89fK0s3p5th2WPWsp+effbZ037jG9847Z/92Z+dtv604n1qH9l0jHXvWB9jKSV1TFt3Xsvwgjo32I9qeMd+xjqxnzjXjTHGAw88sPF86+f5z3/+tF2f33jjjYtz/Lc+33Mcf0pD6/jz387jzneOjbWdK7ynvqyb0+0ztc+b/d7rdn2oC1McYymldixZL64b6vmOE+erKuvewzat2daVPFt/1sX9998/bWXJda1gOR955JGN1+3WY3VN7LWtSyXijutrrrlm2rVP+fyugbYhX2xDCCGEEEIIIew0ebENIYQQQgghhLDT5MU2hBBCCCGEEMJOs3WMrdtkGMNSY0q6rWm61NHdlj5j9DGzxhOpb/f3el314trqwP29i3et5Vcv3sW/rsVmdlsxdDGnXexq/Zvn2EZ1i5+uLP+7tq+xXbqY4MrBxrluGwvbpbjv6mGtTnYp/taU9o8//vi0az833sLYPI8zDsvrGkM2xhg333zztB2nxoqcddZZ0zYdfo3NM9bSbYi8p2PBvlW3IfL5fUZjc4x/Mt7U+hljGU9kPI1xX91YrM/otexbxuZ0cbT1WYzBc8uPww8/fNrGVVneMZZxS/rbLgbGdqhxjV1s02OPPbaxLPa16rue9rSnTds68jjjvYxTuvvuuxfXsvzW137GcWR/r3G0xnhbN9atc5/H19wYxkOuzZF7dGNijD421Wfpjqn5OKyLblxp219qroku94S/6xMsb41vtiz33HPPtN16xbi0Oo6ts24u0ifce++9065bZFhm4+vdgutbv/Vbp/3Od75z2jXe1mt3MYJd3HNd19nfjAu0vMZHet36jM4HdVuT/Yz1a66KmhPCnAH24Xe/+93Ttm31pTW22bnQce35tpv+2jYbY7n2ta08x3s4ltb6vO1ru/tcXRz6GGMcd9xx03ZrQcdclx/CrXPGWPrVbnsm5xHrcYz/ee2xh23f5d2o71n6nG4bJOd3r1vr2zbyHOdx8x5YjzXG3TFnXVgut0K0jvWPYyzbrq5Jnoh8sQ0hhBBCCCGEsNPkxTaEEEIIIYQQwk6ztRRZ6cGahNTP58qIlCp0suAqZ+qkUt0WOVKlodvKYjYdX+W/lsXjOtlQd921+1iva/Ljjk7a67Wq5Kpr105m+79yK5xOCrz2t7VztrnPtlLoTcfXf9d07PsZ5SKdZH2MpfSmypg2oTS3XkspSydLUVqrbKnKmjvJcCcPUjpT+4l10ckTlSqt+QW3l1Ae1F3XPlf9mOn5lcbqY7qtMcZYyhOPPvroaSvxdrsHJcbnnHPO4lpeW7m4Mkvb3nsce+yxi2vpO/XD9gPLYr+p0kIl0m4noPxYyafSZeejMZaS6W77kv2G9e94q/LvbvsMfZb9r5PvjtFv39GFJ63NEfYF5+Fbbrll2o6RtbCQbku9TsK65vstf7flTBcCVZ9Rn2Td6x+URdbz7fOWq2tTx2F9LvtFt13IaaedNu277rpr2lW6b1l8fn93jFmPte1se+vVNaf3UG5c12/OFd3acD9iWznGqrS962snnHDCtG1nJahrW0lZb8pD3W7Ivqgkeozl+POenmM/U2Jcwwp8Ln255e+2oqpb2SiHdY6xvF7L3+sc4RxjP1WOq0+tftjzbUfnMZ/RkKsq+XV91K1Dva7+Yi0csQtndE637LXtHOeuh7p3GLdQqiENrvO67WI78sU2hBBCCCGEEMJOkxfbEEIIIYQQQgg7zed95n9X+tsQQgghhBBCCOH/APliG0IIIYQQQghhp8mLbQghhBBCCCGEnSYvtiGEEEIIIYQQdpq82IYQQgghhBBC2GnyYhtCCCGEEEIIYafJi20IIYQQQgghhJ0mL7YhhBBCCCGEEHaavNiGEEIIIYQQQthp8mIbQgghhBBCCGGn+f8AA0o75E3dMVoAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, axes = plt.subplots(2, 4, figsize=(12, 3.5))\n", - "for ax, im in zip(axes.flatten(), minibatch.screen):\n", - " ax.imshow(im, cmap=\"gray\")\n", - " ax.axis(\"off\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "19b5871c-39a4-4cf0-b3ec-ec036d824b09", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/sensorium/experiment.ipynb b/examples/sensorium/experiment.ipynb index 54dc7c75..e0f5bf25 100644 --- a/examples/sensorium/experiment.ipynb +++ b/examples/sensorium/experiment.ipynb @@ -14,7 +14,6 @@ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import matplotlib.animation as animation\n", - "from pathlib import Path\n", "from IPython.display import HTML" ] }, @@ -33,9 +32,7 @@ "if p not in sys.path:\n", " sys.path.append(p)\n", "\n", - "from experanto.experiment import Experiment\n", - "from experanto.interpolators import Interpolator\n", - "from experanto.interpolators import ScreenInterpolator" + "from experanto.experiment import Experiment # noqa: E402" ] }, { @@ -93,7 +90,7 @@ "metadata": {}, "outputs": [], "source": [ - "plt.plot(data[\"running_speed\"], \"o-k\")" + "plt.plot(data[\"running_speed\"], \"o-k\") # noqa: F821 - `data` is defined in prior cell" ] }, { @@ -103,8 +100,8 @@ "metadata": {}, "outputs": [], "source": [ - "vid1 = e[b : b + time_steps][\"screen\"]\n", - "vid2 = e[b : b + time_steps, \"screen\"]\n", + "vid1 = e[b : b + time_steps][\"screen\"] # noqa: F821 - `b` is defined in prior cell\n", + "vid2 = e[b : b + time_steps, \"screen\"] # noqa: F821\n", "np.sum(vid1 - vid2)" ] }, diff --git a/examples/sensorium/images.ipynb b/examples/sensorium/images.ipynb index 67358220..afd56b29 100644 --- a/examples/sensorium/images.ipynb +++ b/examples/sensorium/images.ipynb @@ -30,7 +30,7 @@ " sys.path.append(p)\n", "\n", "# from experanto.experiment import ImageInterpolator\n", - "from experanto.interpolators import ImageInterpolator" + "from experanto.interpolators import ImageInterpolator # noqa: E402" ] }, { diff --git a/examples/sensorium/interpolator_demo.ipynb b/examples/sensorium/interpolator_demo.ipynb index fcbbd40c..a6b3216d 100644 --- a/examples/sensorium/interpolator_demo.ipynb +++ b/examples/sensorium/interpolator_demo.ipynb @@ -48,13 +48,13 @@ "source": [ "import yaml\n", "\n", - "meta = dict(\n", - " modality=\"time_series\",\n", - " start_time=float(si.timestamps[0]),\n", - " end_time=float(si.timestamps[-1]),\n", - " time_delta=float((si.timestamps[-1] - si.timestamps[0]) / (len(si.timestamps) - 1)),\n", - " phase_shift_per_signal=False,\n", - ")\n", + "meta = {\n", + " \"modality\": \"time_series\",\n", + " \"start_time\": float(si.timestamps[0]),\n", + " \"end_time\": float(si.timestamps[-1]),\n", + " \"time_delta\": float((si.timestamps[-1] - si.timestamps[0]) / (len(si.timestamps) - 1)),\n", + " \"phase_shift_per_signal\": False,\n", + "}\n", "\n", "with open(\"/Users/fabee/Data/sinzlab-data/dataset0/eye_tracker/meta.yml\", \"w\") as f:\n", " yaml.safe_dump(meta, f)" diff --git a/examples/sensorium/sensorium_min_example.ipynb b/examples/sensorium/sensorium_min_example.ipynb index 0b1d1ee2..f5926aa3 100644 --- a/examples/sensorium/sensorium_min_example.ipynb +++ b/examples/sensorium/sensorium_min_example.ipynb @@ -32,7 +32,6 @@ "import numpy as np\n", "from tqdm import tqdm\n", "import matplotlib.pyplot as plt\n", - "from torch.utils.data import DataLoader\n", "from collections import OrderedDict" ] }, @@ -429,80 +428,78 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "id": "70c2c2fa", "metadata": {}, "outputs": [], "source": [ "seed = 42\n", - "import sys\n", + "import sys # noqa: E402\n", "\n", "sys.path.append(\"/srv/user/turishcheva/sensorium_replicate/sensorium_2023/\")\n", "sys.path.append(\"/srv/user/turishcheva/sensorium_replicate/neuralpredictors/\")\n", - "import torch\n", - "from nnfabrik.utility.nn_helpers import set_random_seed\n", + "import torch # noqa: E402\n", + "from nnfabrik.utility.nn_helpers import set_random_seed # noqa: E402\n", "\n", "set_random_seed(seed)\n", "\n", - "from sensorium.datasets.mouse_video_loaders import mouse_video_loader\n", - "from sensorium.utility.scores import get_correlations\n", - "from nnfabrik.builder import get_trainer\n", - "from sensorium.models.make_model import make_video_model" + "from nnfabrik.builder import get_trainer # noqa: E402\n", + "from sensorium.models.make_model import make_video_model # noqa: E402" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "78705901", "metadata": {}, "outputs": [], "source": [ - "factorised_3D_core_dict = dict(\n", - " input_channels=1, # increase if behaviour is used\n", - " hidden_channels=[32, 64, 128],\n", - " spatial_input_kernel=(11, 11),\n", - " temporal_input_kernel=11,\n", - " spatial_hidden_kernel=(5, 5),\n", - " temporal_hidden_kernel=5,\n", - " stride=1,\n", - " layers=3,\n", - " gamma_input_spatial=10,\n", - " gamma_input_temporal=0.01,\n", - " bias=True,\n", - " hidden_nonlinearities=\"elu\",\n", - " x_shift=0,\n", - " y_shift=0,\n", - " batch_norm=True,\n", - " laplace_padding=None,\n", - " input_regularizer=\"LaplaceL2norm\",\n", - " padding=False,\n", - " final_nonlin=True,\n", - " momentum=0.7,\n", - ")\n", + "factorised_3D_core_dict = {\n", + " \"input_channels\": 1, # increase if behaviour is used\n", + " \"hidden_channels\": [32, 64, 128],\n", + " \"spatial_input_kernel\": (11, 11),\n", + " \"temporal_input_kernel\": 11,\n", + " \"spatial_hidden_kernel\": (5, 5),\n", + " \"temporal_hidden_kernel\": 5,\n", + " \"stride\": 1,\n", + " \"layers\": 3,\n", + " \"gamma_input_spatial\": 10,\n", + " \"gamma_input_temporal\": 0.01,\n", + " \"bias\": True,\n", + " \"hidden_nonlinearities\": \"elu\",\n", + " \"x_shift\": 0,\n", + " \"y_shift\": 0,\n", + " \"batch_norm\": True,\n", + " \"laplace_padding\": None,\n", + " \"input_regularizer\": \"LaplaceL2norm\",\n", + " \"padding\": False,\n", + " \"final_nonlin\": True,\n", + " \"momentum\": 0.7,\n", + "}\n", "\n", "\n", "shifter_dict = None\n", "\n", "\n", - "readout_dict = dict(\n", - " bias=True,\n", - " init_mu_range=0.2,\n", - " init_sigma=1.0,\n", - " gamma_readout=0.0,\n", - " gauss_type=\"full\",\n", - " # grid_mean_predictor=None,\n", - " grid_mean_predictor={\n", + "readout_dict = {\n", + " \"bias\": True,\n", + " \"init_mu_range\": 0.2,\n", + " \"init_sigma\": 1.0,\n", + " \"gamma_readout\": 0.0,\n", + " \"gauss_type\": \"full\",\n", + " # grid_mean_predictor=None,\n", + " \"grid_mean_predictor\": {\n", " \"type\": \"cortex\",\n", " \"input_dimensions\": 2,\n", " \"hidden_layers\": 1,\n", " \"hidden_features\": 30,\n", " \"final_tanh\": True,\n", " },\n", - " share_features=False,\n", - " share_grid=False,\n", - " shared_match_ids=None,\n", - " gamma_grid_dispersion=0.0,\n", - ")" + " \"share_features\": False,\n", + " \"share_grid\": False,\n", + " \"shared_match_ids\": None,\n", + " \"gamma_grid_dispersion\": 0.0,\n", + "}" ] }, { @@ -744,7 +741,6 @@ "source": [ "import yaml\n", "import os\n", - "from tqdm import tqdm\n", "from collections import Counter" ] }, @@ -763,6 +759,8 @@ "metadata": {}, "outputs": [], "source": [ + "import pickle\n", + "\n", "path_to_old = ...\n", "path_to_new = ...\n", "path_to_save_matching = ...\n", @@ -782,7 +780,7 @@ " )\n", "\n", " for file in tqdm(os.listdir(yaml_pre_path)):\n", - " with open(f\"{yaml_pre_path}{file}\", \"r\") as f:\n", + " with open(f\"{yaml_pre_path}{file}\") as f:\n", " data = yaml.safe_load(f)\n", " if data[\"modality\"] != \"blank\":\n", " if len(np.where(trial_idx_prev == data[\"trial_idx\"])[0]) == 0:\n", @@ -817,6 +815,8 @@ "metadata": {}, "outputs": [], "source": [ + "import pickle\n", + "\n", "for m in [\n", " \"dynamic29623-4-9-Video-full\",\n", " \"dynamic29647-19-8-Video-full\",\n", @@ -845,7 +845,7 @@ " trial_idx_prev = np.asarray(trial_idx_prev)\n", "\n", " for file in tqdm(os.listdir(yaml_pre_path)):\n", - " with open(f\"{yaml_pre_path}{file}\", \"r\") as f:\n", + " with open(f\"{yaml_pre_path}{file}\") as f:\n", " data = yaml.safe_load(f)\n", " if data[\"modality\"] != \"blank\":\n", " if len(np.where(trial_idx_prev == data[\"trial_idx\"])[0]) == 0:\n", diff --git a/examples/sensorium/videos.ipynb b/examples/sensorium/videos.ipynb deleted file mode 100644 index 2837f876..00000000 --- a/examples/sensorium/videos.ipynb +++ /dev/null @@ -1,96 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "15ef39fb-1ea6-4b35-ba65-6c6770103025", - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2\n", - "\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7aaa77e3-d1aa-42a2-8d9e-b91232b515b4", - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "import os\n", - "\n", - "p = !pwd\n", - "p = os.path.dirname(p[0])\n", - "if p not in sys.path:\n", - " sys.path.append(p)\n", - "\n", - "from experanto.interpolators import VideoInterpolator" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "49453f7d-6d26-4209-9650-94592bf5ca8d", - "metadata": {}, - "outputs": [], - "source": [ - "data_path = \"/Volumes/datasets/funct_foundational_data/mocked_data/dataset0/screen/0001\"\n", - "vid = VideoInterpolator(data_path)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e7bf1d79-dcf4-4c97-8320-001bc2f1bbde", - "metadata": {}, - "outputs": [], - "source": [ - "# Generate some timestamps within a few videos and get those frames\n", - "vids = [0, 3, 12]\n", - "frames = [1, 50, 200]\n", - "t = [vid.timestamps[vid._first_frame_idx[v] + f] + 0.01 for v, f in zip(vids, frames)]\n", - "x, _ = vid.interpolate(t)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f222a6fb-5315-4316-8b8e-2db14fabff4f", - "metadata": {}, - "outputs": [], - "source": [ - "# Verify it returned the correct frames\n", - "for i, (v, f) in enumerate(zip(vids, frames)):\n", - " fig, ax = plt.subplots(1, 2, figsize=(8, 2))\n", - " ax[0].imshow(x[i])\n", - " tmp = np.load(vid._video_files[v])\n", - " ax[1].imshow(tmp[f])" - ] - } - ], - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/experanto/configs.py b/experanto/configs.py index 5f3a0beb..de7293ba 100644 --- a/experanto/configs.py +++ b/experanto/configs.py @@ -37,8 +37,7 @@ from pathlib import Path -from hydra import compose, initialize, initialize_config_dir -from omegaconf import OmegaConf, open_dict +from omegaconf import OmegaConf # get config relative to this file script_dir = Path(__file__).parent diff --git a/experanto/dataloaders.py b/experanto/dataloaders.py index f891bd57..9dfc17d6 100644 --- a/experanto/dataloaders.py +++ b/experanto/dataloaders.py @@ -1,13 +1,8 @@ import logging -import os -import time import warnings -from pathlib import Path -from typing import Any, Dict, List, Optional, Type, Union +from typing import Optional -import numpy as np from omegaconf import DictConfig -from torch.utils.data import DataLoader from .datasets import ChunkDataset from .utils import ( @@ -21,8 +16,8 @@ def get_multisession_dataloader( - paths: List[str], - configs: Optional[Union[DictConfig, Dict, List[Union[DictConfig, Dict]]]] = None, + paths: list[str], + configs: DictConfig | dict | list[DictConfig | dict] | None = None, shuffle_keys: bool = False, **kwargs, ) -> LongCycler: @@ -77,7 +72,7 @@ def get_multisession_dataloader( assert configs is not None dataloaders = {} - for i, (path, cfg) in enumerate(zip(paths, configs)): + for i, (path, cfg) in enumerate(zip(paths, configs, strict=True)): # TODO use saved meta dict to find data key if "dynamic" in path: dataset_name = path.split("dynamic")[1].split("-Video")[0] @@ -95,10 +90,10 @@ def get_multisession_dataloader( def get_multisession_concat_dataloader( - paths: List[str], - configs: Optional[Union[Dict, List[Dict]]] = None, - seed: Optional[int] = 0, - dataloader_config: Optional[Dict] = None, + paths: list[str], + configs: dict | list[dict] | None = None, + seed: int | None = 0, + dataloader_config: dict | None = None, **kwargs, ) -> Optional["FastSessionDataLoader"]: """Create a concatenated multi-session dataloader. @@ -158,8 +153,7 @@ def get_multisession_concat_dataloader( datasets = [] session_names = [] - start_time = time.time() - for i, (path, cfg) in enumerate(zip(paths, configs)): + for _i, (path, cfg) in enumerate(zip(paths, configs, strict=True)): # Create dataset with deterministic seed path_hash = hash(path) % 10000 dataset_seed = seed + path_hash if seed is not None else None @@ -179,7 +173,11 @@ def get_multisession_concat_dataloader( datasets.append(dataset) session_names.append(session_name) except Exception as e: - warnings.warn(f"Error creating dataset for {path}: {str(e)}") + warnings.warn( + f"Error creating dataset for {path}: {str(e)}", + UserWarning, + stacklevel=2, + ) if not datasets: return None diff --git a/experanto/datasets.py b/experanto/datasets.py index 31682cda..d0d289ed 100644 --- a/experanto/datasets.py +++ b/experanto/datasets.py @@ -1,35 +1,29 @@ from __future__ import annotations -import functools import importlib import json import logging import os from collections.abc import Iterable from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any import numpy as np import torch import torchvision from hydra.utils import instantiate -from omegaconf import DictConfig, ListConfig, OmegaConf +from omegaconf import DictConfig, ListConfig from torch.utils.data import Dataset -from torchvision.transforms import v2 from torchvision.transforms.v2 import Compose, Lambda, ToTensor from .configs import DEFAULT_MODALITY_CONFIG from .experiment import Experiment -from .interpolators import ImageTrial, VideoTrial from .intervals import ( TimeInterval, find_intersection_between_two_interval_arrays, get_stats_for_valid_interval, ) -from .utils import add_behavior_as_channels, replace_nan_with_batch_mean - -# see .configs.py for the definition of DEFAULT_MODALITY_CONFIG -DEFAULT_MODALITY_CONFIG = dict() +from .utils import add_behavior_as_channels logger = logging.getLogger(__name__) @@ -218,15 +212,15 @@ class ChunkDataset(Dataset): def __init__( self, root_folder: str, - global_sampling_rate: Optional[float] = None, - global_chunk_size: Optional[int] = None, + global_sampling_rate: float | None = None, + global_chunk_size: int | None = None, add_behavior_as_channels: bool = False, replace_nans_with_means: bool = False, cache_data: bool = False, - out_keys: Optional[Iterable] = None, + out_keys: Iterable | None = None, normalize_timestamps: bool = True, modality_config: dict = DEFAULT_MODALITY_CONFIG, - seed: Optional[int] = None, + seed: int | None = None, safe_interval_threshold: float = 0.5, interpolate_precision: int = 5, ) -> None: @@ -310,7 +304,7 @@ def __init__( def _read_trials(self) -> None: screen = self._experiment.devices["screen"] - self._trials = [t for t in screen.trials] + self._trials = list(screen.trials) start_idx = np.array([t.first_frame_idx for t in self._trials]) self._start_times = screen.timestamps[start_idx] self._end_times = np.append(screen.timestamps[start_idx[1:]], np.inf) @@ -368,8 +362,8 @@ def initialize_statistics(self) -> None: None, ... ] elif mode == "screen_default": - means = np.array((80)) - stds = np.array((60)) + means = np.array(80) + stds = np.array(60) self._statistics[device_name]["mean"] = means.reshape( 1, -1 @@ -391,7 +385,7 @@ def initialize_transforms(self): for device_name in self.device_names: if device_name == "screen": add_channel = Lambda(self.add_channel_function) - transform_list: List[Any] = [] + transform_list: list[Any] = [] for v in self.modality_config.screen.transforms.values(): # type: ignore[union-attr] if isinstance(v, dict): # config dict @@ -401,7 +395,7 @@ def initialize_transforms(self): transform_list.insert(0, add_channel) else: - transform_list: List[Any] = [ToTensor()] + transform_list: list[Any] = [ToTensor()] # Normalization. if self.modality_config[device_name].transforms.get("normalization", False): @@ -466,7 +460,7 @@ def _get_callable_filter(self, filter_config): except (ImportError, AttributeError, KeyError, TypeError) as e: raise TypeError( f"Failed to manually instantiate filter from config {filter_config}: {e}" - ) + ) from e raise TypeError( f"Filter config must be either callable or a valid config dict with __target__, got {type(filter_config)}" @@ -474,8 +468,8 @@ def _get_callable_filter(self, filter_config): def get_valid_intervals_from_filters( self, visualize: bool = False - ) -> List[TimeInterval]: - valid_intervals: Optional[List[TimeInterval]] = None + ) -> list[TimeInterval]: + valid_intervals: list[TimeInterval] | None = None for modality in self.modality_config: if "filters" in self.modality_config[modality]: device = self._experiment.devices[modality] @@ -484,7 +478,7 @@ def get_valid_intervals_from_filters( ].items(): # Get the final callable filter function filter_function = self._get_callable_filter(filter_config) - valid_intervals_: List[TimeInterval] = filter_function(device_=device) # type: ignore[assignment] + valid_intervals_: list[TimeInterval] = filter_function(device_=device) # type: ignore[assignment] if visualize: logger.info("modality: %s, filter: %s", modality, filter_name) visualization_string = get_stats_for_valid_interval( @@ -501,7 +495,7 @@ def get_valid_intervals_from_filters( return valid_intervals if valid_intervals is not None else [] def get_condition_mask_from_meta_conditions( - self, valid_conditions_sum_of_product: List[dict] + self, valid_conditions_sum_of_product: list[dict] ) -> np.ndarray: """Create a boolean mask for trials satisfying given conditions. @@ -523,7 +517,7 @@ def get_condition_mask_from_meta_conditions( ``[{'tier': 'train', 'stim_type': 'natural'}, {'tier': 'blank'}]`` matches trials that are either (train AND natural) OR blank. """ - all_conditions: Optional[np.ndarray] = None + all_conditions: np.ndarray | None = None for valid_conditions_product in valid_conditions_sum_of_product: conditions_of_product = None for k, valid_condition in valid_conditions_product.items(): @@ -546,7 +540,7 @@ def get_condition_mask_from_meta_conditions( def get_screen_sample_mask_from_meta_conditions( self, satisfy_for_next: int, - valid_conditions_sum_of_product: List[dict], + valid_conditions_sum_of_product: list[dict], filter_for_valid_intervals: bool = True, ) -> np.ndarray: """Create a boolean mask for screen samples satisfying given conditions. @@ -585,7 +579,8 @@ def get_screen_sample_mask_from_meta_conditions( # Create TimeIntervals from starts and ends trial_intervals = [ - TimeInterval(start, end) for start, end in zip(starts, ends) + TimeInterval(start, end) + for start, end in zip(starts, ends, strict=True) ] # If we have filter_valid_intervals, find intersection with trial intervals @@ -708,7 +703,7 @@ def get_data_key_from_root_folder(self, root_folder): # Check if the file exists before trying to open it if os.path.isfile(meta_file_path): try: - with open(meta_file_path, "r") as file: + with open(meta_file_path) as file: meta = json.load(file) # Get data_key from meta if it exists @@ -768,7 +763,6 @@ def __getitem__(self, idx: int) -> dict: for device_name in self.device_names: sampling_rate = self.sampling_rates[device_name] chunk_size = self.chunk_sizes[device_name] - chunk_s = chunk_size / sampling_rate # convert everything to int to avoid numerical issues start_time = int(round(s * self.scale_precision)) @@ -818,14 +812,14 @@ def __getitem__(self, idx: int) -> dict: return final_out - def get_state(self) -> Dict[str, Any]: + def get_state(self) -> dict[str, Any]: """Return the current state of the dataset's RNG.""" return { "rng_state": self._rng.get_state() if self.seed is not None else None, "valid_screen_times": self._valid_screen_times.copy(), } - def set_state(self, state: Dict[str, Any]) -> None: + def set_state(self, state: dict[str, Any]) -> None: """Restore the dataset's RNG state.""" if state["rng_state"] is not None and self.seed is not None: self._rng.set_state(state["rng_state"]) diff --git a/experanto/experiment.py b/experanto/experiment.py index faa2fed1..abfda7a9 100644 --- a/experanto/experiment.py +++ b/experanto/experiment.py @@ -1,11 +1,8 @@ from __future__ import annotations import logging -import re import warnings -from collections.abc import Sequence from pathlib import Path -from typing import Optional, Union import numpy as np from hydra.utils import instantiate @@ -69,7 +66,7 @@ def __init__( cache_data: bool = False, ) -> None: self.root_folder = Path(root_folder) - self.devices = dict() + self.devices = {} self.start_time = np.inf self.end_time = -np.inf self.modality_config = modality_config @@ -113,6 +110,7 @@ def _load_devices(self) -> None: warnings.warn( "Falling back to original Interpolator creation logic.", UserWarning, + stacklevel=2, ) dev = Interpolator.create( d, @@ -132,9 +130,9 @@ def device_names(self): def interpolate( self, times: np.ndarray, - device: Union[str, Interpolator, None] = None, + device: str | Interpolator | None = None, return_valid: bool = False, - ) -> Union[tuple[dict, dict], dict, tuple[np.ndarray, np.ndarray], np.ndarray]: + ) -> tuple[dict, dict] | dict | tuple[np.ndarray, np.ndarray] | np.ndarray: """Interpolate data from one or all devices at specified time points. Parameters @@ -202,7 +200,7 @@ def interpolate( else: return values elif isinstance(device, str): - assert device in self.devices, "Unknown device '{}'".format(device) + assert device in self.devices, f"Unknown device '{device}'" res = self.devices[device].interpolate(times, return_valid=return_valid) return res else: diff --git a/experanto/interpolators.py b/experanto/interpolators.py index 4928fd85..4e6a077d 100644 --- a/experanto/interpolators.py +++ b/experanto/interpolators.py @@ -4,15 +4,13 @@ import logging import os import re -import typing import warnings from abc import abstractmethod from pathlib import Path -from typing import Union, cast +from typing import cast import cv2 import numpy as np -import numpy.lib.format as fmt import yaml from numba import njit, prange from scipy.ndimage import gaussian_filter1d @@ -72,7 +70,7 @@ def load_meta(self): @abstractmethod def interpolate( self, times: np.ndarray, return_valid: bool = False - ) -> Union[tuple[np.ndarray, np.ndarray], np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray] | np.ndarray: """Map an array of time points to interpolated data values.""" ... @@ -86,7 +84,7 @@ def __exit__(self, *exc): self.close() @staticmethod - def create(root_folder: str, cache_data: bool = False, **kwargs) -> "Interpolator": + def create(root_folder: str, cache_data: bool = False, **kwargs) -> Interpolator: """Factory method to create the appropriate interpolator for a modality. Reads the ``meta.yml`` file in the folder to determine the modality type @@ -111,7 +109,7 @@ def create(root_folder: str, cache_data: bool = False, **kwargs) -> "Interpolato ValueError If the modality type is not supported. """ - with open(Path(root_folder) / "meta.yml", "r") as file: + with open(Path(root_folder) / "meta.yml") as file: meta_data = yaml.safe_load(file) modality = meta_data.get("modality") @@ -198,7 +196,7 @@ def __init__( interpolation_mode: str = "nearest_neighbor", normalize: bool = False, normalize_subtract_mean: bool = False, - normalize_std_threshold: typing.Optional[float] = None, # or 0.01 + normalize_std_threshold: float | None = None, # or 0.01 **kwargs, ) -> None: super().__init__(root_folder) @@ -263,13 +261,15 @@ def normalize_data(self, data): def interpolate( self, times: np.ndarray, return_valid: bool = False - ) -> Union[tuple[np.ndarray, np.ndarray], np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray] | np.ndarray: valid = self.valid_times(times) valid_times = times[valid] if len(valid_times) == 0: warnings.warn( - "Sequence interpolation returns empty array, no valid times queried" + "Sequence interpolation returns empty array, no valid times queried", + UserWarning, + stacklevel=2, ) return ( (np.empty((0, self._data.shape[1])), valid) @@ -292,7 +292,9 @@ def interpolate( if np.any(idx_lower < 0): # should not be possible warnings.warn( - f"Interpolation index {idx_lower} is negative. This should not happen." + f"Interpolation index {idx_lower} is negative. This should not happen.", + UserWarning, + stacklevel=2, ) overflow_mask = overflow_mask | idx_lower < 0 @@ -326,7 +328,7 @@ def interpolate( else: raise NotImplementedError( - f"interpolation_mode should be linear or nearest_neighbor" + "interpolation_mode should be linear or nearest_neighbor" ) def close(self) -> None: @@ -362,7 +364,7 @@ def __init__( interpolation_mode: str = "nearest_neighbor", normalize: bool = False, normalize_subtract_mean: bool = False, - normalize_std_threshold: typing.Optional[float] = None, # or 0.01 + normalize_std_threshold: float | None = None, # or 0.01 **kwargs, ) -> None: super().__init__( @@ -386,13 +388,15 @@ def __init__( def interpolate( self, times: np.ndarray, return_valid: bool = False - ) -> Union[tuple[np.ndarray, np.ndarray], np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray] | np.ndarray: valid = self.valid_times(times) valid_times = times[valid] if len(valid_times) == 0: warnings.warn( - "Sequence interpolation returns empty array, no valid times queried" + "Sequence interpolation returns empty array, no valid times queried", + UserWarning, + stacklevel=2, ) return ( (np.empty((0, self._data.shape[1])), valid) @@ -419,7 +423,9 @@ def interpolate( if np.any(idx_lower < 0): # should not be possible warnings.warn( - f"Interpolation index {idx_lower} is negative. This should not happen." + f"Interpolation index {idx_lower} is negative. This should not happen.", + UserWarning, + stacklevel=2, ) overflow_mask = overflow_mask | idx_lower < 0 @@ -454,7 +460,7 @@ def interpolate( else: raise NotImplementedError( - f"interpolation_mode should be linear or nearest_neighbor" + "interpolation_mode should be linear or nearest_neighbor" ) @@ -501,7 +507,7 @@ def __init__( root_folder: str, cache_data: bool = False, # New parameter rescale: bool = False, - rescale_size: typing.Optional[tuple[int, int]] = None, + rescale_size: tuple[int, int] | None = None, normalize: bool = False, **kwargs, ) -> None: @@ -566,7 +572,7 @@ def is_numbered_yml(file_name): # Read each YAML file and store under its filename for meta_file in meta_files: - with open(meta_file, "r") as file: + with open(meta_file) as file: file_base_name = meta_file.stem yaml_content = yaml.safe_load(file) all_data[file_base_name] = yaml_content @@ -580,7 +586,7 @@ def read_combined_meta(self) -> tuple[list, list]: logger.info("Combining metadata files...") self._combine_metadatas() - with open(self.root_folder / "combined_meta.json", "r") as file: + with open(self.root_folder / "combined_meta.json") as file: self.combined_meta = json.load(file) metadatas = [] @@ -595,7 +601,7 @@ def _parse_trials(self) -> None: self.trials = [] metadatas, keys = self.read_combined_meta() - for key, metadata in zip(keys, metadatas): + for key, metadata in zip(keys, metadatas, strict=True): data_file_name = self.root_folder / "data" / f"{key}.npy" # Pass the cache_trials parameter when creating trials self.trials.append( @@ -606,7 +612,7 @@ def _parse_trials(self) -> None: def interpolate( self, times: np.ndarray, return_valid: bool = False - ) -> Union[tuple[np.ndarray, np.ndarray], np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray] | np.ndarray: valid = self.valid_times(times) valid_times = times[valid] valid_times += 1e-4 # add small offset to avoid numerical issues @@ -725,7 +731,7 @@ def __init__(self, root_folder: str, cache_data: bool = False, **kwargs): def interpolate( self, times: np.ndarray, return_valid: bool = False - ) -> Union[tuple[np.ndarray, np.ndarray], np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray] | np.ndarray: valid = self.valid_times(times) valid_times = times[valid] @@ -734,7 +740,9 @@ def interpolate( if n_times == 0: warnings.warn( - "TimeIntervalInterpolator returns an empty array, no valid times queried." + "TimeIntervalInterpolator returns an empty array, no valid times queried.", + UserWarning, + stacklevel=2, ) return ( (np.empty((0, n_labels), dtype=bool), valid) @@ -751,14 +759,18 @@ def interpolate( if len(intervals) == 0: warnings.warn( - f"TimeIntervalInterpolator found no intervals for label: {label}" + f"TimeIntervalInterpolator found no intervals for label: {label}", + UserWarning, + stacklevel=2, ) continue for start, end in intervals: if start > end: warnings.warn( - f"Invalid interval found for label: {label}, interval: ({start}, {end})" + f"Invalid interval found for label: {label}, interval: ({start}, {end})", + UserWarning, + stacklevel=2, ) continue # Half-open interval [start, end): inclusive start, exclusive end @@ -792,7 +804,7 @@ class ScreenTrial: def __init__( self, - data_file_name: Union[str, Path], + data_file_name: str | Path, meta_data: dict, image_size: tuple, first_frame_idx: int, @@ -812,10 +824,10 @@ def __init__( @staticmethod def create( - data_file_name: Union[str, Path], + data_file_name: str | Path, meta_data: dict, cache_data: bool = False, - ) -> "ScreenTrial": + ) -> ScreenTrial: modality = meta_data.get("modality") assert modality is not None class_name = modality.lower().capitalize() + "Trial" @@ -1051,7 +1063,7 @@ def __init__( def interpolate( self, times: np.ndarray, return_valid: bool = False - ) -> Union[tuple[np.ndarray, np.ndarray], np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray] | np.ndarray: # 1. Filter for valid times valid = self.valid_times(times) valid_times = times[valid] diff --git a/experanto/intervals.py b/experanto/intervals.py index 16079857..3e242c7c 100644 --- a/experanto/intervals.py +++ b/experanto/intervals.py @@ -1,5 +1,5 @@ import typing -from typing import List, Optional +from typing import Optional import numpy as np @@ -46,7 +46,7 @@ def intersect(self, times: np.ndarray) -> np.ndarray: return np.where((times >= self.start) & (times <= self.end))[0] -def uniquefy_interval_array(interval_array: List[TimeInterval]) -> List[TimeInterval]: +def uniquefy_interval_array(interval_array: list[TimeInterval]) -> list[TimeInterval]: """Merge overlapping or adjacent intervals into non-overlapping intervals. Parameters @@ -82,8 +82,8 @@ def uniquefy_interval_array(interval_array: List[TimeInterval]) -> List[TimeInte def find_intersection_between_two_interval_arrays( - interval_array_1: List[TimeInterval], interval_array_2: List[TimeInterval] -) -> List[TimeInterval]: + interval_array_1: list[TimeInterval], interval_array_2: list[TimeInterval] +) -> list[TimeInterval]: """Find the intersection of two interval arrays. Parameters @@ -124,8 +124,8 @@ def find_intersection_between_two_interval_arrays( def find_intersection_across_arrays_of_intervals( - intervals_array: List[List[TimeInterval]], -) -> List[TimeInterval]: + intervals_array: list[list[TimeInterval]], +) -> list[TimeInterval]: """Find the common intersection across multiple interval arrays. Parameters @@ -149,8 +149,8 @@ def find_intersection_across_arrays_of_intervals( def find_union_across_arrays_of_intervals( - intervals_array: List[List[TimeInterval]], -) -> List[TimeInterval]: + intervals_array: list[list[TimeInterval]], +) -> list[TimeInterval]: """Find the union of multiple interval arrays. Parameters @@ -170,8 +170,8 @@ def find_union_across_arrays_of_intervals( def find_complement_of_interval_array( - start: float, end: float, interval_array: List[TimeInterval] -) -> List[TimeInterval]: + start: float, end: float, interval_array: list[TimeInterval] +) -> list[TimeInterval]: """Find gaps not covered by intervals within a range. Parameters @@ -215,7 +215,7 @@ def find_complement_of_interval_array( def get_stats_for_valid_interval( - intervals: List[TimeInterval], start_time: float, end_time: float + intervals: list[TimeInterval], start_time: float, end_time: float ) -> str: """Calculate statistics about valid and invalid intervals within a range. @@ -236,7 +236,7 @@ def get_stats_for_valid_interval( """ total_duration = end_time - start_time if total_duration <= 0: - return f"Error: Invalid time range (end_time <= start_time). Total duration must be positive." + return "Error: Invalid time range (end_time <= start_time). Total duration must be positive." # Ensure intervals are unique and sorted, then clamp them to the analysis window unique_intervals = uniquefy_interval_array(intervals) diff --git a/experanto/utils.py b/experanto/utils.py index bd0c7f85..03475546 100644 --- a/experanto/utils.py +++ b/experanto/utils.py @@ -1,35 +1,22 @@ import bisect import logging -import math -import multiprocessing # inbuilt libraries -import os -import queue -import random -import threading -import time -import warnings from collections import defaultdict -from copy import deepcopy -from functools import partial -from typing import Any, Dict, Iterator, List, Optional, Tuple, Union # third-party libraries import numpy as np import torch -from omegaconf import DictConfig -from torch.utils.data import ConcatDataset, DataLoader, Dataset, Sampler +from torch.utils.data import DataLoader, Dataset, Sampler # local libraries -from .intervals import TimeInterval logger = logging.getLogger(__name__) def replace_nan_with_batch_mean(data: np.ndarray) -> np.ndarray: row, col = np.where(np.isnan(data)) - for i, j in zip(row, col): + for i, j in zip(row, col, strict=True): new_value = np.nanmean(data[:, j]) data[i, j] = new_value if not np.isnan(new_value) else 0 return data @@ -58,7 +45,7 @@ def add_behavior_as_channels(data: dict[str, torch.Tensor]) -> dict: # Process eye_tracker if len(eye_tracker.shape) == 2: # (t, c_eye) - c_eye = eye_tracker.shape[1] + # Reshape to (c_eye, t, h, w) eye_tracker = eye_tracker.transpose(0, 1) # (c_eye, t) eye_tracker = eye_tracker.unsqueeze(-1).unsqueeze(-1) # (c_eye, t, 1, 1) @@ -69,7 +56,7 @@ def add_behavior_as_channels(data: dict[str, torch.Tensor]) -> dict: # Process treadmill if len(treadmill.shape) == 2: # (t, c_tread) - c_tread = treadmill.shape[1] + # Reshape to (c_tread, t, h, w) treadmill = treadmill.transpose(0, 1) # (c_tread, t) treadmill = treadmill.unsqueeze(-1).unsqueeze(-1) # (c_tread, t, 1, 1) @@ -90,7 +77,7 @@ def add_behavior_as_channels(data: dict[str, torch.Tensor]) -> dict: return data -class MultiEpochsDataLoader(torch.utils.data.DataLoader): +class MultiEpochsDataLoader(DataLoader): """DataLoader that keeps workers alive across epochs. Solves a bug where worker processes are re-spawned at the start of each @@ -135,7 +122,7 @@ def __iter__(self): # type: ignore[override] self.dataset, "shuffle_valid_screen_times" ): self.dataset.shuffle_valid_screen_times() # type: ignore[union-attr] - for i in range(len(self)): + for _i in range(len(self)): yield next(self.iterator) @@ -204,6 +191,7 @@ def __iter__(self): cycle(self.loaders.keys()), (cycle(cycles)), range(len(self.loaders) * self.max_batches), + strict=True, ): yield k, next(loader) @@ -238,6 +226,7 @@ def __iter__(self): cycle(self.loaders.keys()), (cycle(cycles)), range(len(self.loaders) * self.min_batches), + strict=True, ): yield k, next(loader) @@ -245,7 +234,7 @@ def __len__(self): return len(self.loaders) * self.min_batches -class _RepeatSampler(object): +class _RepeatSampler: """Simple sampler that repeats indefinitely.""" def __init__(self, sampler): @@ -277,7 +266,7 @@ def __init__(self, datasets, session_names=None): self.session_names = session_names # Log dataset sizes for debugging - for i, (name, dataset) in enumerate(zip(session_names, datasets)): + for i, (name, dataset) in enumerate(zip(session_names, datasets, strict=True)): logger.debug("Dataset %s: %s, length = %s", i, name, len(dataset)) # Compute cumulative sizes for efficient indexing @@ -550,7 +539,7 @@ def __init__( # State tracking variables self.current_batch = 0 self.epoch = 0 - self.session_positions = {name: 0 for name in self.session_names} + self.session_positions = dict.fromkeys(self.session_names, 0) self.batches_from_session = defaultdict( int ) # Tracks batches yielded per session in the current epoch iteration diff --git a/pyproject.toml b/pyproject.toml index 8242e7d8..646e20e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dev = [ "pytest-cov>=7.0.0", "pyright", "hypothesis>=6.0", + "ruff==0.15.6", ] docs = [ @@ -69,3 +70,19 @@ known_first_party = ["experanto"] include = ["experanto"] typeCheckingMode = "basic" pythonVersion = "3.12" + +[tool.ruff] +line-length = 88 +target-version = "py312" +src = ["experanto"] +exclude = [ + ".git", + ".venv", + "build", + "dist", + ".ipynb_checkpoints", +] + +[tool.ruff.lint] +select = ["E", "F", "B", "UP", "C4"] +ignore = ["E501"] \ No newline at end of file diff --git a/tests/test_sequence_interpolator.py b/tests/test_sequence_interpolator.py index 789b92a0..8ec69930 100644 --- a/tests/test_sequence_interpolator.py +++ b/tests/test_sequence_interpolator.py @@ -16,12 +16,12 @@ @pytest.mark.parametrize("use_mem_mapped", [False, True]) def test_nearest_neighbor_interpolation(n_signals, sampling_rate, use_mem_mapped): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=n_signals, - use_mem_mapped=use_mem_mapped, - t_end=5.0, - sampling_rate=sampling_rate, - ) + data_kwargs={ + "n_signals": n_signals, + "use_mem_mapped": use_mem_mapped, + "t_end": 5.0, + "sampling_rate": sampling_rate, + } ) as (timestamps, data, _, seq_interp): assert isinstance( seq_interp, SequenceInterpolator @@ -51,14 +51,14 @@ def test_nearest_neighbor_interpolation(n_signals, sampling_rate, use_mem_mapped @pytest.mark.parametrize("keep_nans", [False, True]) def test_nearest_neighbor_interpolation_handles_nans(n_signals, keep_nans): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=n_signals, - use_mem_mapped=True, - t_end=5.0, - sampling_rate=10.0, - contain_nans=True, - ), - interp_kwargs=dict(keep_nans=keep_nans), + data_kwargs={ + "n_signals": n_signals, + "use_mem_mapped": True, + "t_end": 5.0, + "sampling_rate": 10.0, + "contain_nans": True, + }, + interp_kwargs={"keep_nans": keep_nans}, ) as (timestamps, data, _, seq_interp): assert isinstance( seq_interp, SequenceInterpolator @@ -85,12 +85,12 @@ def test_nearest_neighbor_interpolation_handles_nans(n_signals, keep_nans): @pytest.mark.parametrize("sampling_rate", [3.0, 10.0, 100.0]) def test_nearest_neighbor_interpolation_with_inbetween_times(n_signals, sampling_rate): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=n_signals, - use_mem_mapped=True, - t_end=5.0, - sampling_rate=sampling_rate, - ) + data_kwargs={ + "n_signals": n_signals, + "use_mem_mapped": True, + "t_end": 5.0, + "sampling_rate": sampling_rate, + } ) as (timestamps, data, _, seq_interp): assert isinstance( seq_interp, SequenceInterpolator @@ -122,13 +122,13 @@ def test_nearest_neighbor_interpolation_with_phase_shifts( n_signals, sampling_rate, use_mem_mapped ): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=n_signals, - use_mem_mapped=use_mem_mapped, - t_end=5.0, - sampling_rate=sampling_rate, - shifts_per_signal=True, - ) + data_kwargs={ + "n_signals": n_signals, + "use_mem_mapped": use_mem_mapped, + "t_end": 5.0, + "sampling_rate": sampling_rate, + "shifts_per_signal": True, + } ) as (timestamps, data, shift, seq_interp): assert isinstance( seq_interp, PhaseShiftedSequenceInterpolator @@ -180,15 +180,15 @@ def test_nearest_neighbor_interpolation_with_phase_shifts_handles_nans( n_signals, keep_nans ): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=n_signals, - use_mem_mapped=True, - t_end=5.0, - sampling_rate=10.0, - shifts_per_signal=True, - contain_nans=True, - ), - interp_kwargs=dict(keep_nans=keep_nans), + data_kwargs={ + "n_signals": n_signals, + "use_mem_mapped": True, + "t_end": 5.0, + "sampling_rate": 10.0, + "shifts_per_signal": True, + "contain_nans": True, + }, + interp_kwargs={"keep_nans": keep_nans}, ) as (timestamps, data, _, seq_interp): assert isinstance( seq_interp, PhaseShiftedSequenceInterpolator @@ -220,14 +220,14 @@ def test_linear_interpolation( n_signals, sampling_rate, use_mem_mapped, contain_nans, keep_nans ): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=n_signals, - use_mem_mapped=use_mem_mapped, - t_end=5.0, - sampling_rate=sampling_rate, - contain_nans=contain_nans, - ), - interp_kwargs=dict(keep_nans=keep_nans), + data_kwargs={ + "n_signals": n_signals, + "use_mem_mapped": use_mem_mapped, + "t_end": 5.0, + "sampling_rate": sampling_rate, + "contain_nans": contain_nans, + }, + interp_kwargs={"keep_nans": keep_nans}, ) as (timestamps, data, _, seq_interp): assert isinstance( seq_interp, SequenceInterpolator @@ -235,7 +235,7 @@ def test_linear_interpolation( seq_interp.interpolation_mode = "linear" delta_t = 1.0 / sampling_rate - idx = [i for i in range(1, DEFAULT_SEQUENCE_LENGTH + 1)] + idx = list(range(1, DEFAULT_SEQUENCE_LENGTH + 1)) times = timestamps[idx] + 0.5 * delta_t t1, t2 = ( @@ -273,14 +273,14 @@ def test_linear_interpolation_with_phase_shifts( n_signals, sampling_rate, use_mem_mapped, keep_nans ): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=n_signals, - use_mem_mapped=use_mem_mapped, - t_end=5.0, - sampling_rate=sampling_rate, - shifts_per_signal=True, - ), - interp_kwargs=dict(keep_nans=keep_nans), + data_kwargs={ + "n_signals": n_signals, + "use_mem_mapped": use_mem_mapped, + "t_end": 5.0, + "sampling_rate": sampling_rate, + "shifts_per_signal": True, + }, + interp_kwargs={"keep_nans": keep_nans}, ) as (timestamps, data, shift, seq_interp): assert isinstance( seq_interp, PhaseShiftedSequenceInterpolator @@ -339,13 +339,13 @@ def test_linear_interpolation_with_phase_shifts( def test_interpolation_for_invalid_times(interpolation_mode, end_time, keep_nans): n_signals = 10 with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=n_signals, - use_mem_mapped=True, - t_end=end_time, - sampling_rate=10.0, - ), - interp_kwargs=dict(keep_nans=keep_nans), + data_kwargs={ + "n_signals": n_signals, + "use_mem_mapped": True, + "t_end": end_time, + "sampling_rate": 10.0, + }, + interp_kwargs={"keep_nans": keep_nans}, ) as (_, _, _, seq_interp): assert isinstance( seq_interp, SequenceInterpolator @@ -380,14 +380,14 @@ def test_interpolation_with_phase_shifts_for_invalid_times( ): n_signals = 10 with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=n_signals, - use_mem_mapped=True, - t_end=end_time, - sampling_rate=10.0, - shifts_per_signal=True, - ), - interp_kwargs=dict(keep_nans=keep_nans), + data_kwargs={ + "n_signals": n_signals, + "use_mem_mapped": True, + "t_end": end_time, + "sampling_rate": 10.0, + "shifts_per_signal": True, + }, + interp_kwargs={"keep_nans": keep_nans}, ) as (_, _, phase_shifts, seq_interp): assert isinstance( seq_interp, PhaseShiftedSequenceInterpolator @@ -414,13 +414,13 @@ def test_interpolation_with_phase_shifts_for_invalid_times( @pytest.mark.parametrize("phase_shifts", [True, False]) def test_interpolation_for_empty_times(interpolation_mode, phase_shifts): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=10, - use_mem_mapped=True, - t_end=5.0, - sampling_rate=10.0, - shifts_per_signal=phase_shifts, - ) + data_kwargs={ + "n_signals": 10, + "use_mem_mapped": True, + "t_end": 5.0, + "sampling_rate": 10.0, + "shifts_per_signal": phase_shifts, + } ) as (_, _, _, seq_interp): assert isinstance( seq_interp, SequenceInterpolator @@ -440,12 +440,12 @@ def test_interpolation_for_empty_times(interpolation_mode, phase_shifts): def test_nearest_neighbor_interpolation_return_valid_false(): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=10, - use_mem_mapped=False, - t_end=5.0, - sampling_rate=10.0, - ) + data_kwargs={ + "n_signals": 10, + "use_mem_mapped": False, + "t_end": 5.0, + "sampling_rate": 10.0, + } ) as (timestamps, data, _, seq_interp): times = timestamps[:DEFAULT_SEQUENCE_LENGTH] + 1e-9 @@ -464,12 +464,12 @@ def test_nearest_neighbor_interpolation_return_valid_false(): def test_nearest_neighbor_interpolation_default_return_valid(): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=10, - use_mem_mapped=False, - t_end=5.0, - sampling_rate=10.0, - ) + data_kwargs={ + "n_signals": 10, + "use_mem_mapped": False, + "t_end": 5.0, + "sampling_rate": 10.0, + } ) as (timestamps, data, _, seq_interp): times = timestamps[:DEFAULT_SEQUENCE_LENGTH] + 1e-9 @@ -488,12 +488,12 @@ def test_nearest_neighbor_interpolation_default_return_valid(): def test_linear_interpolation_return_valid_false(): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=10, - use_mem_mapped=False, - t_end=5.0, - sampling_rate=10.0, - ) + data_kwargs={ + "n_signals": 10, + "use_mem_mapped": False, + "t_end": 5.0, + "sampling_rate": 10.0, + } ) as (timestamps, data, _, seq_interp): seq_interp.interpolation_mode = "linear" @@ -515,12 +515,12 @@ def test_linear_interpolation_return_valid_false(): def test_linear_interpolation_default_return_valid(): with sequence_data_and_interpolator( - data_kwargs=dict( - n_signals=10, - use_mem_mapped=False, - t_end=5.0, - sampling_rate=10.0, - ) + data_kwargs={ + "n_signals": 10, + "use_mem_mapped": False, + "t_end": 5.0, + "sampling_rate": 10.0, + } ) as (timestamps, data, _, seq_interp): seq_interp.interpolation_mode = "linear" diff --git a/tests/test_time_intervals_interpolator.py b/tests/test_time_intervals_interpolator.py index afabbe8f..2949fe2e 100644 --- a/tests/test_time_intervals_interpolator.py +++ b/tests/test_time_intervals_interpolator.py @@ -112,10 +112,10 @@ def test_time_interval_interpolation(): interval it falls within. """ with time_interval_data_and_interpolator( - data_kwargs=dict( - duration=10.0, - sampling_rate=30.0, - ) + data_kwargs={ + "duration": 10.0, + "sampling_rate": 30.0, + } ) as (timestamps, intervals_dict, time_interp): run_interval_interpolation_test(timestamps, intervals_dict, time_interp) @@ -135,13 +135,13 @@ def test_time_interval_interpolation_overlap2(): The overlap region [2.0, 2.5) should be marked as True for both test and train. """ with time_interval_data_and_interpolator( - data_kwargs=dict( - duration=10.0, - sampling_rate=30.0, - test_intervals=[[0.0, 2.5]], - train_intervals=[[2.0, 4.0], [6.0, 8.0]], - validation_intervals=[[4.0, 6.0], [8.0, 10.0]], - ) + data_kwargs={ + "duration": 10.0, + "sampling_rate": 30.0, + "test_intervals": [[0.0, 2.5]], + "train_intervals": [[2.0, 4.0], [6.0, 8.0]], + "validation_intervals": [[4.0, 6.0], [8.0, 10.0]], + } ) as (timestamps, intervals_dict, time_interp): run_interval_interpolation_test(timestamps, intervals_dict, time_interp) @@ -160,13 +160,13 @@ def test_time_interval_interpolation_overlap3(): Multiple overlap regions exist between different label pairs. """ with time_interval_data_and_interpolator( - data_kwargs=dict( - duration=10.0, - sampling_rate=30.0, - test_intervals=[[0.0, 2.5]], - train_intervals=[[1.78, 4.03], [5.57, 8.23]], - validation_intervals=[[3.75, 6.01], [7.89, 10.0]], - ) + data_kwargs={ + "duration": 10.0, + "sampling_rate": 30.0, + "test_intervals": [[0.0, 2.5]], + "train_intervals": [[1.78, 4.03], [5.57, 8.23]], + "validation_intervals": [[3.75, 6.01], [7.89, 10.0]], + } ) as (timestamps, intervals_dict, time_interp): run_interval_interpolation_test(timestamps, intervals_dict, time_interp) @@ -186,13 +186,13 @@ def test_time_interval_interpolation_gap(): Timestamps in the gaps should be marked as False for all labels. """ with time_interval_data_and_interpolator( - data_kwargs=dict( - duration=10.0, - sampling_rate=30.0, - test_intervals=[[0.0, 1.8]], - train_intervals=[[2.0, 4.0], [6.32, 8.0]], - validation_intervals=[[4.2, 6.0], [8.27, 10.0]], - ) + data_kwargs={ + "duration": 10.0, + "sampling_rate": 30.0, + "test_intervals": [[0.0, 1.8]], + "train_intervals": [[2.0, 4.0], [6.32, 8.0]], + "validation_intervals": [[4.2, 6.0], [8.27, 10.0]], + } ) as (timestamps, intervals_dict, time_interp): run_interval_interpolation_test(timestamps, intervals_dict, time_interp) @@ -212,13 +212,13 @@ def test_time_interval_interpolation_gap_and_overlap(): Gap from [3.9, 4.0) and overlap from [2.0, 2.5). """ with time_interval_data_and_interpolator( - data_kwargs=dict( - duration=10.0, - sampling_rate=30.0, - test_intervals=[[0.0, 2.5]], - train_intervals=[[2.0, 3.9], [6.0, 8.0]], - validation_intervals=[[4.0, 6.0], [8.0, 10.0]], - ) + data_kwargs={ + "duration": 10.0, + "sampling_rate": 30.0, + "test_intervals": [[0.0, 2.5]], + "train_intervals": [[2.0, 3.9], [6.0, 8.0]], + "validation_intervals": [[4.0, 6.0], [8.0, 10.0]], + } ) as (timestamps, intervals_dict, time_interp): run_interval_interpolation_test(timestamps, intervals_dict, time_interp) @@ -248,13 +248,13 @@ def test_time_interval_interpolation_nans(): timestamps[nan_start_idx:nan_end_idx] = np.nan with time_interval_data_and_interpolator( - data_kwargs=dict( - duration=duration, - sampling_rate=sampling_rate, - test_intervals=[[0.0, 1.5]], - train_intervals=[[2.0, 4.0], [6.0, 8.0]], - validation_intervals=[[4.0, 6.0], [8.0, 10.0]], - ) + data_kwargs={ + "duration": duration, + "sampling_rate": sampling_rate, + "test_intervals": [[0.0, 1.5]], + "train_intervals": [[2.0, 4.0], [6.0, 8.0]], + "validation_intervals": [[4.0, 6.0], [8.0, 10.0]], + } ) as (_, intervals_dict, time_interp): assert isinstance(time_interp, TimeIntervalInterpolator) @@ -296,13 +296,13 @@ def test_time_interval_interpolation_zero_length(): - validation: [4.0, 6.0) and [8.0, 10.0) """ with time_interval_data_and_interpolator( - data_kwargs=dict( - duration=10.0, - sampling_rate=30.0, - test_intervals=[[1.0, 1.0]], - train_intervals=[[2.0, 4.0], [6.0, 8.0]], - validation_intervals=[[4.0, 6.0], [8.0, 10.0]], - ) + data_kwargs={ + "duration": 10.0, + "sampling_rate": 30.0, + "test_intervals": [[1.0, 1.0]], + "train_intervals": [[2.0, 4.0], [6.0, 8.0]], + "validation_intervals": [[4.0, 6.0], [8.0, 10.0]], + } ) as (timestamps, intervals_dict, time_interp): assert isinstance(time_interp, TimeIntervalInterpolator) @@ -336,13 +336,13 @@ def test_time_interval_interpolation_multi_zero_length(): - validation: [2.0, 2.0) and [8.0, 10.0) - one zero-length, one normal """ with time_interval_data_and_interpolator( - data_kwargs=dict( - duration=10.0, - sampling_rate=30.0, - test_intervals=[[1.0, 1.0], [5.0, 6.0]], - train_intervals=[[4.0, 4.0], [6.0, 6.0]], - validation_intervals=[[2.0, 2.0], [8.0, 10.0]], - ) + data_kwargs={ + "duration": 10.0, + "sampling_rate": 30.0, + "test_intervals": [[1.0, 1.0], [5.0, 6.0]], + "train_intervals": [[4.0, 4.0], [6.0, 6.0]], + "validation_intervals": [[2.0, 2.0], [8.0, 10.0]], + } ) as (timestamps, intervals_dict, time_interp): assert isinstance(time_interp, TimeIntervalInterpolator) @@ -376,13 +376,13 @@ def test_time_interval_interpolation_full_range(): All timestamps should be marked True for the test label. """ with time_interval_data_and_interpolator( - data_kwargs=dict( - duration=10.0, - sampling_rate=30.0, - test_intervals=[[0.0, 10.0]], - train_intervals=[[2.0, 4.0], [6.0, 8.0]], - validation_intervals=[[4.0, 6.0], [8.0, 10.0]], - ) + data_kwargs={ + "duration": 10.0, + "sampling_rate": 30.0, + "test_intervals": [[0.0, 10.0]], + "train_intervals": [[2.0, 4.0], [6.0, 8.0]], + "validation_intervals": [[4.0, 6.0], [8.0, 10.0]], + } ) as (timestamps, intervals_dict, time_interp): run_interval_interpolation_test(timestamps, intervals_dict, time_interp) @@ -404,13 +404,13 @@ def test_time_interval_interpolation_inverted_interval(): interval for the test label. """ with time_interval_data_and_interpolator( - data_kwargs=dict( - duration=10.0, - sampling_rate=30.0, - test_intervals=[[5.0, 2.0]], - train_intervals=[[2.0, 4.0], [6.0, 8.0]], - validation_intervals=[[4.0, 6.0], [8.0, 10.0]], - ) + data_kwargs={ + "duration": 10.0, + "sampling_rate": 30.0, + "test_intervals": [[5.0, 2.0]], + "train_intervals": [[2.0, 4.0], [6.0, 8.0]], + "validation_intervals": [[4.0, 6.0], [8.0, 10.0]], + } ) as (timestamps, intervals_dict, time_interp): assert isinstance(time_interp, TimeIntervalInterpolator) @@ -447,13 +447,13 @@ def test_time_interval_interpolation_multi_inverted(): intervals for test, train (two warnings), and validation labels. """ with time_interval_data_and_interpolator( - data_kwargs=dict( - duration=10.0, - sampling_rate=30.0, - test_intervals=[[8.0, 3.0], [5.0, 6.0]], - train_intervals=[[7.0, 2.0], [9.0, 1.0]], - validation_intervals=[[6.0, 4.0], [8.0, 10.0]], - ) + data_kwargs={ + "duration": 10.0, + "sampling_rate": 30.0, + "test_intervals": [[8.0, 3.0], [5.0, 6.0]], + "train_intervals": [[7.0, 2.0], [9.0, 1.0]], + "validation_intervals": [[6.0, 4.0], [8.0, 10.0]], + } ) as (timestamps, intervals_dict, time_interp): assert isinstance(time_interp, TimeIntervalInterpolator)