diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b57f642 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.parquet filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 70d5dbc..f749552 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -50,6 +50,10 @@ jobs: - name: Test transformer training run: uv run src/train_transformer.py --profile --ci - # This will use best_model.pth for inference + # This will use transformer.pt for inference, will make predictions for all of week 9 - name: Test prediction generation - run: uv run src/generate_predictions.py --profile \ No newline at end of file + run: uv run src/generate_predictions.py --profile + + # This will use the predictions saved as parquet in stage above, only 1 plot, one animated play, and one confusion matrix is created + - name: Test process predictions + run: uv run src/process_predictions.py --profile --ci \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3191938..a957c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -194,6 +194,7 @@ log # Artifacts *.pkl +artifacts/ # Any mp4s that sneak in *.mp4 \ No newline at end of file diff --git a/README.md b/README.md index 2aa4759..19cd1d2 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,22 @@ git lfs pull ``` This will fully populate `nfl/parquet/*` with: + +`2021/`: +- `games.parquet` +- `pffScoutingData.parquet` +- `players.parquet` +- `plays.parquet` +- `tracking_week_1.parquet` +- `tracking_week_2.parquet` +- `tracking_week_3.parquet` +- `tracking_week_4.parquet` +- `tracking_week_5.parquet` +- `tracking_week_6.parquet` +- `tracking_week_7.parquet` +- `tracking_week_8.parquet` + +`2022/`: - `games.parquet` - `player_play.parquet` - `players.parquet` @@ -93,7 +109,6 @@ This will fully populate `nfl/parquet/*` with: - `tracking_week_8.parquet` - `tracking_week_9.parquet` - ## Setup python virtual environment This project uses `uv` for managing the python virtual environment. To install uv please see the official [documentation](https://docs.astral.sh/uv/getting-started/installation/) or install via CLI below: ```bash @@ -119,9 +134,10 @@ uv run src/train_transformer.py uv run src/train_transformer.py --tune # Run inference on week 9 -uv run src/generate_predictions.py +uv run src/generate_predictions.py -# From this point you can explore the notebooks/predictions.ipynb, adjust the data path as neccesary and see the animations and plots +# Create artifacts (plot on accuracy for week 9, classification report, and animations of plays) +uv run src/process_predictions.py ``` ## Activating MLflow @@ -144,11 +160,19 @@ The last command will start the MLflow GUI at your local host loopback on port 5 ## Profiler The PyTorch profiler is used for examining the training process and run time inference. +### Miscellaneous Profiling +You can profile any core scripts with: +```bash +# Generic profiling (uses the decorators to shown time the fcns take) +uv run src/the_script.py --profile + +``` ### Profiling Training + You can profile the training process with: ```bash -uv run src/train_transformer --profile +uv run src/train_transformer.py --profile ``` This will do three things: @@ -363,11 +387,8 @@ Note, if you followed instructions above to setup the git hook, ruff will run th - See if I can build a model to determine blitzer like red circle AWS at post snap - Add in confusion matrix, ROC curve, and prediction post snap accuracy to automatic predictions and not just in notebook - Infra - - Profiler - - Add in PyTorch profiler with new tensorboard (new thing, this is deprecated) traces into Chrome UI, built into CI for profiler - Optimization - Switch to flash attention layer which can accelerate training - General - Get docker working and implemented in CI - - Add git hook for ruff fix - Look back on mypy for type \ No newline at end of file diff --git a/data/parquet/2021/README.md b/data/parquet/2021/README.md new file mode 100644 index 0000000..a6adca7 --- /dev/null +++ b/data/parquet/2021/README.md @@ -0,0 +1 @@ +From the 2023 bowl: https://www.kaggle.com/competitions/nfl-big-data-bowl-2023/data?select=games.csv \ No newline at end of file diff --git a/data/parquet/2021/games.parquet b/data/parquet/2021/games.parquet new file mode 100644 index 0000000..cc811d9 --- /dev/null +++ b/data/parquet/2021/games.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bfebf0fd9c01e9fb9b88da67fff35f741ff2ed1539f1cf321d71beeeb309ed5 +size 5640 diff --git a/data/parquet/2021/pffScoutingData.parquet b/data/parquet/2021/pffScoutingData.parquet new file mode 100644 index 0000000..aaf5a49 --- /dev/null +++ b/data/parquet/2021/pffScoutingData.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0205b45612630777747df65c4abb759727138810efd2c19a9fbb98b5dd96affa +size 725007 diff --git a/data/parquet/2021/players.parquet b/data/parquet/2021/players.parquet new file mode 100644 index 0000000..9f02cd7 --- /dev/null +++ b/data/parquet/2021/players.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db51a969e49f59e2ac15847dbd799cb32b294bc48986edd0ccf5f71452e56402 +size 37897 diff --git a/data/parquet/2021/plays.parquet b/data/parquet/2021/plays.parquet new file mode 100644 index 0000000..27c168c --- /dev/null +++ b/data/parquet/2021/plays.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cea24258f7f1dcadd697a3f21640d651a67fb96bd54071e9a41f62921fa4290 +size 338282 diff --git a/data/parquet/2021/tracking_week_1.parquet b/data/parquet/2021/tracking_week_1.parquet new file mode 100644 index 0000000..808bd6d --- /dev/null +++ b/data/parquet/2021/tracking_week_1.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:933980b09ec875d7d4eeb36a8b56c53c44cb57061ba028c1430586fc44b665fd +size 13791953 diff --git a/data/parquet/2021/tracking_week_2.parquet b/data/parquet/2021/tracking_week_2.parquet new file mode 100644 index 0000000..6e4e15c --- /dev/null +++ b/data/parquet/2021/tracking_week_2.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b182f84f0f745a17f04bc93a42dc8c5c706fadbbcb51ad92ccb90d485335f9d7 +size 12881918 diff --git a/data/parquet/2021/tracking_week_3.parquet b/data/parquet/2021/tracking_week_3.parquet new file mode 100644 index 0000000..083d6ed --- /dev/null +++ b/data/parquet/2021/tracking_week_3.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73164a26ec459a2aa5a14e37b9d2fe1f1c93584d7ed0a06f83d6b14fb5963534 +size 13839782 diff --git a/data/parquet/2021/tracking_week_4.parquet b/data/parquet/2021/tracking_week_4.parquet new file mode 100644 index 0000000..ba984d9 --- /dev/null +++ b/data/parquet/2021/tracking_week_4.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3f2426aff00ca65a0f3af7337d329047f2b79ebf671652eb2396b2c6a7f9d6d +size 13284786 diff --git a/data/parquet/2021/tracking_week_5.parquet b/data/parquet/2021/tracking_week_5.parquet new file mode 100644 index 0000000..21deeed --- /dev/null +++ b/data/parquet/2021/tracking_week_5.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36111fd98d0908dfe4f54d9359033ac858fc6989aa9d1230ce6193df0f86d5f2 +size 13515983 diff --git a/data/parquet/2021/tracking_week_6.parquet b/data/parquet/2021/tracking_week_6.parquet new file mode 100644 index 0000000..606b812 --- /dev/null +++ b/data/parquet/2021/tracking_week_6.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e431fe801309b835e2bd5601f0925658613def23fbd6f81dd1ae9f5eedcf4477 +size 11910915 diff --git a/data/parquet/2021/tracking_week_7.parquet b/data/parquet/2021/tracking_week_7.parquet new file mode 100644 index 0000000..417eb10 --- /dev/null +++ b/data/parquet/2021/tracking_week_7.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a82abfaa595c3052272d4e009d959cdc3f59834d88e817f68662d8760c3691 +size 11179801 diff --git a/data/parquet/2021/tracking_week_8.parquet b/data/parquet/2021/tracking_week_8.parquet new file mode 100644 index 0000000..e9bacd7 --- /dev/null +++ b/data/parquet/2021/tracking_week_8.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5115bf4334ed142a035ba3ed24cc66212ff9d57f15d002037872d6a869f9b815 +size 11987057 diff --git a/data/parquet/games.parquet b/data/parquet/2022/games.parquet similarity index 100% rename from data/parquet/games.parquet rename to data/parquet/2022/games.parquet diff --git a/data/parquet/player_play.parquet b/data/parquet/2022/player_play.parquet similarity index 100% rename from data/parquet/player_play.parquet rename to data/parquet/2022/player_play.parquet diff --git a/data/parquet/players.parquet b/data/parquet/2022/players.parquet similarity index 100% rename from data/parquet/players.parquet rename to data/parquet/2022/players.parquet diff --git a/data/parquet/plays.parquet b/data/parquet/2022/plays.parquet similarity index 100% rename from data/parquet/plays.parquet rename to data/parquet/2022/plays.parquet diff --git a/data/parquet/tracking_week_1.parquet b/data/parquet/2022/tracking_week_1.parquet similarity index 100% rename from data/parquet/tracking_week_1.parquet rename to data/parquet/2022/tracking_week_1.parquet diff --git a/data/parquet/tracking_week_2.parquet b/data/parquet/2022/tracking_week_2.parquet similarity index 100% rename from data/parquet/tracking_week_2.parquet rename to data/parquet/2022/tracking_week_2.parquet diff --git a/data/parquet/tracking_week_3.parquet b/data/parquet/2022/tracking_week_3.parquet similarity index 100% rename from data/parquet/tracking_week_3.parquet rename to data/parquet/2022/tracking_week_3.parquet diff --git a/data/parquet/tracking_week_4.parquet b/data/parquet/2022/tracking_week_4.parquet similarity index 100% rename from data/parquet/tracking_week_4.parquet rename to data/parquet/2022/tracking_week_4.parquet diff --git a/data/parquet/tracking_week_5.parquet b/data/parquet/2022/tracking_week_5.parquet similarity index 100% rename from data/parquet/tracking_week_5.parquet rename to data/parquet/2022/tracking_week_5.parquet diff --git a/data/parquet/tracking_week_6.parquet b/data/parquet/2022/tracking_week_6.parquet similarity index 100% rename from data/parquet/tracking_week_6.parquet rename to data/parquet/2022/tracking_week_6.parquet diff --git a/data/parquet/tracking_week_7.parquet b/data/parquet/2022/tracking_week_7.parquet similarity index 100% rename from data/parquet/tracking_week_7.parquet rename to data/parquet/2022/tracking_week_7.parquet diff --git a/data/parquet/tracking_week_8.parquet b/data/parquet/2022/tracking_week_8.parquet similarity index 100% rename from data/parquet/tracking_week_8.parquet rename to data/parquet/2022/tracking_week_8.parquet diff --git a/data/parquet/tracking_week_9.parquet b/data/parquet/2022/tracking_week_9.parquet similarity index 100% rename from data/parquet/tracking_week_9.parquet rename to data/parquet/2022/tracking_week_9.parquet diff --git a/data/training/transformer.pt b/data/training/transformer.pt index 10d4210..1a78ebd 100644 Binary files a/data/training/transformer.pt and b/data/training/transformer.pt differ diff --git a/notebooks/eda.ipynb b/notebooks/eda.ipynb index 598d7d2..8fb84be 100644 --- a/notebooks/eda.ipynb +++ b/notebooks/eda.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "0fe17705", "metadata": {}, "outputs": [], @@ -17,6 +17,74 @@ "games_df, plays_df, players_df, location_data_df = rawLoader.get_data(weeks=[i for i in range(1, 10)])" ] }, + { + "cell_type": "markdown", + "id": "15113b4e", + "metadata": {}, + "source": [ + "# Exploring plays_df" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "db093750", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " gameId playId playDescription \\\n", + "0 2022102302 2655 (1:54) (Shotgun) J.Burrow pass short middle to... \n", + "1 2022091809 3698 (2:13) (Shotgun) J.Burrow pass short right to ... \n", + "2 2022103004 3146 (2:00) (Shotgun) D.Mills pass short right to D... \n", + "3 2022110610 348 (9:28) (Shotgun) P.Mahomes pass short left to ... \n", + "4 2022102700 2799 (2:16) (Shotgun) L.Jackson up the middle to TB... \n", + "\n", + " quarter down yardsToGo possessionTeam defensiveTeam yardlineSide \\\n", + "0 3 1 10 CIN ATL CIN \n", + "1 4 1 10 CIN DAL CIN \n", + "2 4 3 12 HOU TEN HOU \n", + "3 1 2 10 KC TEN TEN \n", + "4 3 2 8 BAL TB TB \n", + "\n", + " yardlineNumber ... yardsGained homeTeamWinProbabilityAdded \\\n", + "0 21 ... 9 0.004634 \n", + "1 8 ... 4 0.002847 \n", + "2 20 ... 6 0.000205 \n", + "3 23 ... 4 -0.001308 \n", + "4 27 ... -1 0.027141 \n", + "\n", + " visitorTeamWinProbilityAdded expectedPointsAdded isDropback \\\n", + "0 -0.004634 0.702717 True \n", + "1 -0.002847 -0.240509 True \n", + "2 -0.000205 -0.218480 True \n", + "3 0.001308 -0.427749 True \n", + "4 -0.027141 -0.638912 False \n", + "\n", + " pff_runConceptPrimary pff_runConceptSecondary pff_runPassOption \\\n", + "0 None None 0 \n", + "1 None None 0 \n", + "2 None None 0 \n", + "3 None None 0 \n", + "4 MAN READ OPTION 0 \n", + "\n", + " pff_passCoverage pff_manZone \n", + "0 Cover-3 Zone \n", + "1 Quarters Zone \n", + "2 Quarters Zone \n", + "3 Quarters Zone \n", + "4 Cover-1 Man \n", + "\n", + "[5 rows x 50 columns]\n" + ] + } + ], + "source": [ + "print(plays_df.head())" + ] + }, { "cell_type": "markdown", "id": "dad4cbc3", diff --git a/notebooks/extra_data_eda.ipynb b/notebooks/extra_data_eda.ipynb new file mode 100644 index 0000000..3127d8d --- /dev/null +++ b/notebooks/extra_data_eda.ipynb @@ -0,0 +1,483 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a972596b", + "metadata": {}, + "source": [ + "Games df we can merge easily" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "672396de", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " gameId season week gameDate gameTimeEastern homeTeamAbbr \\\n", + "0 2021090900 2021 1 09/09/2021 20:20:00 TB \n", + "1 2021091200 2021 1 09/12/2021 13:00:00 ATL \n", + "2 2021091201 2021 1 09/12/2021 13:00:00 BUF \n", + "3 2021091202 2021 1 09/12/2021 13:00:00 CAR \n", + "4 2021091203 2021 1 09/12/2021 13:00:00 CIN \n", + "\n", + " visitorTeamAbbr homeFinalScore visitorFinalScore \n", + "0 DAL NaN NaN \n", + "1 PHI NaN NaN \n", + "2 PIT NaN NaN \n", + "3 NYJ NaN NaN \n", + "4 MIN NaN NaN \n", + " gameId season week gameDate gameTimeEastern homeTeamAbbr \\\n", + "253 2022110607 2022 9 11/6/2022 13:00:00 WAS \n", + "254 2022110608 2022 9 11/6/2022 16:05:00 ARI \n", + "255 2022110609 2022 9 11/6/2022 16:25:00 TB \n", + "256 2022110610 2022 9 11/6/2022 20:20:00 KC \n", + "257 2022110700 2022 9 11/7/2022 20:15:00 NO \n", + "\n", + " visitorTeamAbbr homeFinalScore visitorFinalScore \n", + "253 MIN 17.0 20.0 \n", + "254 SEA 21.0 31.0 \n", + "255 LA 16.0 13.0 \n", + "256 TEN 20.0 17.0 \n", + "257 BAL 13.0 27.0 \n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "path_2021 = \"/home/sam/repos/hobby-repos/nfl/data/parquet/2021/games.parquet\"\n", + "path_2022 = \"/home/sam/repos/hobby-repos/nfl/data/parquet/2022/games.parquet\"\n", + "\n", + "df_2021 = pd.read_parquet(path_2021)\n", + "df_2022 = pd.read_parquet(path_2022)\n", + "df = pd.concat([df_2021, df_2022], ignore_index=True)\n", + "print(df.head())\n", + "print(df.tail())" + ] + }, + { + "cell_type": "markdown", + "id": "a405fc93", + "metadata": {}, + "source": [ + "We can merge players easily, we just need to add the season, I'm not even sure I use this downstream but if so will just need to add the season lookup" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cefbb3af", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " nflId height weight birthDate collegeName officialPosition \\\n", + "0 25511 6-4 225 1977-08-03 Michigan QB \n", + "1 28963 6-5 240 1982-03-02 Miami, O. QB \n", + "2 29550 6-4 328 1982-01-22 Arkansas T \n", + "3 29851 6-2 225 1983-12-02 California QB \n", + "4 30078 6-2 228 1982-11-24 Harvard QB \n", + "\n", + " displayName season position \n", + "0 Tom Brady 2021 NaN \n", + "1 Ben Roethlisberger 2021 NaN \n", + "2 Jason Peters 2021 NaN \n", + "3 Aaron Rodgers 2021 NaN \n", + "4 Ryan Fitzpatrick 2021 NaN \n", + " nflId height weight birthDate collegeName officialPosition \\\n", + "3371 55190 6-1 285 None Maryland NaN \n", + "3372 55200 6-6 266 None Indiana NaN \n", + "3373 55239 6-2 300 None Pennsylvania NaN \n", + "3374 55240 6-1 185 None Buffalo NaN \n", + "3375 55241 6-2 280 None Coastal Carolina NaN \n", + "\n", + " displayName season position \n", + "3371 Sam Okuayinonu 2022 DE \n", + "3372 Ryder Anderson 2022 DT \n", + "3373 Prince Emili 2022 DT \n", + "3374 Ja'Marcus Ingram 2022 CB \n", + "3375 C.J. Brewer 2022 DT \n" + ] + } + ], + "source": [ + "path_2021 = \"/home/sam/repos/hobby-repos/nfl/data/parquet/2021/players.parquet\"\n", + "path_2022 = \"/home/sam/repos/hobby-repos/nfl/data/parquet/2022/players.parquet\"\n", + "\n", + "df_2021 = pd.read_parquet(path_2021)\n", + "df_2021[\"season\"] = 2021\n", + "df_2022 = pd.read_parquet(path_2022)\n", + "df_2022[\"season\"] = 2022\n", + "df = pd.concat([df_2021, df_2022], ignore_index=True)\n", + "print(df.head())\n", + "print(df.tail())" + ] + }, + { + "cell_type": "markdown", + "id": "7128f488", + "metadata": {}, + "source": [ + "Ok now lets try plays df, it's same thing just add in the season column" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0c06153", + "metadata": {}, + "outputs": [], + "source": [ + "path_2021 = \"/home/sam/repos/hobby-repos/nfl/data/parquet/2021/plays.parquet\"\n", + "path_2022 = \"/home/sam/repos/hobby-repos/nfl/data/parquet/2022/plays.parquet\"\n", + "\n", + "df_2021 = pd.read_parquet(path_2021)\n", + "df_2021[\"season\"] = 2021\n", + "df_2022 = pd.read_parquet(path_2022)\n", + "df_2022[\"season\"] = 2022\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "83773635", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['gameId', 'playId', 'playDescription', 'quarter', 'down', 'yardsToGo',\n", + " 'possessionTeam', 'defensiveTeam', 'yardlineSide', 'yardlineNumber',\n", + " 'gameClock', 'preSnapHomeScore', 'preSnapVisitorScore', 'passResult',\n", + " 'penaltyYards', 'prePenaltyPlayResult', 'playResult', 'foulName1',\n", + " 'foulNFLId1', 'foulName2', 'foulNFLId2', 'foulName3', 'foulNFLId3',\n", + " 'absoluteYardlineNumber', 'offenseFormation', 'personnelO',\n", + " 'defendersInBox', 'personnelD', 'dropBackType', 'pff_playAction',\n", + " 'pff_passCoverage', 'pff_passCoverageType', 'season'],\n", + " dtype='object')" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_2021.columns\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "44e15169", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['Man' 'Zone' 'Other']\n" + ] + } + ], + "source": [ + "print(df_2021[\"pff_passCoverageType\"].unique())" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "edc49162", + "metadata": {}, + "outputs": [], + "source": [ + "df_2021[\"pff_manZone\"] = df_2021[\"pff_passCoverageType\"]\n", + "df = pd.concat([df_2021, df_2022], ignore_index=True)" + ] + }, + { + "cell_type": "markdown", + "id": "4cafda30", + "metadata": {}, + "source": [ + "Now let's look at the tracking data" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8177e7d9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Columns don't match\n", + "Only in df_2021: ['team']\n", + "Only in df_2022: ['club', 'displayName', 'frameType']\n" + ] + } + ], + "source": [ + "path_2021 = \"/home/sam/repos/hobby-repos/nfl/data/parquet/2021/tracking_week_1.parquet\"\n", + "path_2022 = \"/home/sam/repos/hobby-repos/nfl/data/parquet/2022/tracking_week_1.parquet\"\n", + "\n", + "df_2021 = pd.read_parquet(path_2021)\n", + "df_2021[\"season\"] = 2021\n", + "df_2022 = pd.read_parquet(path_2022)\n", + "df_2022[\"season\"] = 2022\n", + "\n", + "# Check if both dfs have the same columns\n", + "if df_2021.columns.equals(df_2022.columns):\n", + " print(\"Columns match!\")\n", + "else:\n", + " print(\"Columns don't match\")\n", + "\n", + "\n", + "cols_2021 = set(df_2021.columns)\n", + "cols_2022 = set(df_2022.columns)\n", + "\n", + "only_in_2021 = cols_2021 - cols_2022\n", + "only_in_2022 = cols_2022 - cols_2021\n", + "\n", + "print(\"Only in df_2021:\", sorted(only_in_2021))\n", + "print(\"Only in df_2022:\", sorted(only_in_2022))" + ] + }, + { + "cell_type": "markdown", + "id": "36c83321", + "metadata": {}, + "source": [ + "We can make sure 2021 and 2022 both have club" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ad47fbd9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Only in df_2021: ['team']\n", + "Only in df_2022: ['displayName', 'frameType']\n" + ] + } + ], + "source": [ + "df_2021[\"club\"] = df_2021[\"team\"]\n", + "\n", + "cols_2021 = set(df_2021.columns)\n", + "cols_2022 = set(df_2022.columns)\n", + "\n", + "only_in_2021 = cols_2021 - cols_2022\n", + "only_in_2022 = cols_2022 - cols_2021\n", + "\n", + "print(\"Only in df_2021:\", sorted(only_in_2021))\n", + "print(\"Only in df_2022:\", sorted(only_in_2022))" + ] + }, + { + "cell_type": "markdown", + "id": "05c31262", + "metadata": {}, + "source": [ + "We now need to figure out how to capture frameType in 2021" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "508c7a85", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[None 'ball_snap' 'autoevent_passforward' 'pass_forward'\n", + " 'autoevent_ballsnap' 'line_set' 'play_action' 'pass_arrived'\n", + " 'autoevent_passinterrupted' 'fumble' 'fumble_offense_recovered' 'qb_sack'\n", + " 'run' 'man_in_motion' 'pass_outcome_caught' 'pass_outcome_incomplete'\n", + " 'pass_tipped' 'qb_strip_sack' 'shift' 'first_contact'\n", + " 'huddle_break_offense' 'lateral' 'handoff']\n", + "['huddle_break_offense' None 'line_set' 'man_in_motion' 'ball_snap'\n", + " 'handoff' 'first_contact' 'tackle' 'pass_forward' 'pass_arrived'\n", + " 'pass_outcome_caught' 'touchdown' 'dropped_pass'\n", + " 'pass_outcome_incomplete' 'play_action' 'out_of_bounds' 'run' 'qb_sack'\n", + " 'pass_tipped' 'fumble' 'fumble_offense_recovered' 'shift'\n", + " 'fumble_defense_recovered' 'touchback' 'qb_strip_sack' 'qb_kneel'\n", + " 'timeout_away' 'snap_direct' 'pass_outcome_interception' 'qb_slide'\n", + " 'play_submit' 'pass_outcome_touchdown' 'lateral' 'qb_spike' 'pass_shovel'\n", + " 'run_pass_option' 'huddle_start_offense']\n" + ] + } + ], + "source": [ + "print(df_2021[\"event\"].unique())\n", + "print(df_2022[\"event\"].unique())" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "65199803", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "46\n" + ] + } + ], + "source": [ + "specific_play_df = df_2022[(df_2022[\"playId\"] == 64) & (df_2022[\"frameType\"] == \"SNAP\")]\n", + "print(len(specific_play_df))" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "e62d9a92", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "46\n" + ] + } + ], + "source": [ + "specific_play_df = df_2022[(df_2022[\"playId\"] == 64) & (df_2022[\"event\"] == \"ball_snap\")]\n", + "print(len(specific_play_df))" + ] + }, + { + "cell_type": "markdown", + "id": "7ccf892e", + "metadata": {}, + "source": [ + "## Can I use the \"ball_snap\" from df[\"event\"], need to investigate if this comproable to frameType" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "36d62be0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " gameId playId nflId frameId time \\\n", + "0 2021090900 97 25511.0 1 2021-09-10T00:26:31.100 \n", + "1 2021090900 97 25511.0 2 2021-09-10T00:26:31.200 \n", + "2 2021090900 97 25511.0 3 2021-09-10T00:26:31.300 \n", + "3 2021090900 97 25511.0 4 2021-09-10T00:26:31.400 \n", + "4 2021090900 97 25511.0 5 2021-09-10T00:26:31.500 \n", + "\n", + " jerseyNumber team playDirection x y s a dis o \\\n", + "0 12.0 TB right 37.77 24.22 0.29 0.30 0.03 165.16 \n", + "1 12.0 TB right 37.78 24.22 0.23 0.11 0.02 164.33 \n", + "2 12.0 TB right 37.78 24.24 0.16 0.10 0.01 160.24 \n", + "3 12.0 TB right 37.73 24.25 0.15 0.24 0.06 152.13 \n", + "4 12.0 TB right 37.69 24.26 0.25 0.18 0.04 148.33 \n", + "\n", + " dir event season displayName frameType club \n", + "0 84.99 None 2021 NaN NaN NaN \n", + "1 92.87 None 2021 NaN NaN NaN \n", + "2 68.55 None 2021 NaN NaN NaN \n", + "3 296.85 None 2021 NaN NaN NaN \n", + "4 287.55 None 2021 NaN NaN NaN \n" + ] + } + ], + "source": [ + "df = pd.concat([df_2021, df_2022], ignore_index=True)\n", + "\n", + "print(df.head())" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "97b5e069", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " gameId playId nflId frameId time \\\n", + "8222817 2022090800 3696 NaN 171 2022-09-09 03:07:37.7 \n", + "8222818 2022090800 3696 NaN 172 2022-09-09 03:07:37.8 \n", + "8222819 2022090800 3696 NaN 173 2022-09-09 03:07:37.9 \n", + "8222820 2022090800 3696 NaN 174 2022-09-09 03:07:38 \n", + "8222821 2022090800 3696 NaN 175 2022-09-09 03:07:38.1 \n", + "\n", + " jerseyNumber team playDirection x y s a dis o \\\n", + "8222817 NaN NaN left 1.70 0.62 14.42 5.36 1.47 NaN \n", + "8222818 NaN NaN left 0.59 0.01 3.60 0.06 1.27 NaN \n", + "8222819 NaN NaN left 0.23 -0.07 3.60 0.23 0.38 NaN \n", + "8222820 NaN NaN left -0.13 -0.17 3.63 0.06 0.37 NaN \n", + "8222821 NaN NaN left -0.48 -0.26 3.74 0.46 0.36 NaN \n", + "\n", + " dir event season displayName frameType \\\n", + "8222817 NaN pass_outcome_incomplete 2022 football AFTER_SNAP \n", + "8222818 NaN None 2022 football AFTER_SNAP \n", + "8222819 NaN None 2022 football AFTER_SNAP \n", + "8222820 NaN None 2022 football AFTER_SNAP \n", + "8222821 NaN None 2022 football AFTER_SNAP \n", + "\n", + " club \n", + "8222817 football \n", + "8222818 football \n", + "8222819 football \n", + "8222820 football \n", + "8222821 football \n" + ] + } + ], + "source": [ + "print(df.tail())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "nfl", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/predictions.ipynb b/notebooks/predictions.ipynb deleted file mode 100644 index 1811e06..0000000 --- a/notebooks/predictions.ipynb +++ /dev/null @@ -1,566 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "ad8489cf", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "\n", - "pd.options.mode.chained_assignment = None" - ] - }, - { - "cell_type": "markdown", - "id": "b3f9fa50", - "metadata": {}, - "source": [ - "## Load in the prediction data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e1b3a2c1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " gameId playId nflId displayName frameId frameType \\\n", - "0 2022110700 56 33131.0 Calais Campbell 1 BEFORE_SNAP \n", - "1 2022110700 56 33131.0 Calais Campbell 1 BEFORE_SNAP \n", - "2 2022110700 56 33131.0 Calais Campbell 1 BEFORE_SNAP \n", - "3 2022110700 56 33131.0 Calais Campbell 1 BEFORE_SNAP \n", - "4 2022110700 56 33131.0 Calais Campbell 1 BEFORE_SNAP \n", - "\n", - " time jerseyNumber club playDirection ... defense week \\\n", - "0 2022-11-08 01:16:09.2 93.0 BAL left ... 1 9 \n", - "1 2022-11-08 01:16:09.2 93.0 BAL left ... 1 9 \n", - "2 2022-11-08 01:16:09.2 93.0 BAL left ... 1 9 \n", - "3 2022-11-08 01:16:09.2 93.0 BAL left ... 1 9 \n", - "4 2022-11-08 01:16:09.2 93.0 BAL left ... 1 9 \n", - "\n", - " uniqueId frameUniqueId snap_frame frames_from_snap zone_prob \\\n", - "0 2022110700_56 2022110700_56_1 117 -116 0.775387 \n", - "1 2022110700_56 2022110700_56_1 117 -116 0.781979 \n", - "2 2022110700_56 2022110700_56_1 117 -116 0.781979 \n", - "3 2022110700_56 2022110700_56_1 117 -116 0.781979 \n", - "4 2022110700_56 2022110700_56_1 117 -116 0.832631 \n", - "\n", - " man_prob pred actual \n", - "0 0.224613 0 0 \n", - "1 0.218021 0 0 \n", - "2 0.218021 0 0 \n", - "3 0.218021 0 0 \n", - "4 0.167369 0 0 \n", - "\n", - "[5 rows x 42 columns]\n", - "Finished processing week 9...\n", - " gameId playId nflId displayName frameId frameType \\\n", - "0 2022110700 56 33131.0 Calais Campbell 1 BEFORE_SNAP \n", - "1 2022110700 56 33131.0 Calais Campbell 1 BEFORE_SNAP \n", - "2 2022110700 56 33131.0 Calais Campbell 1 BEFORE_SNAP \n", - "3 2022110700 56 33131.0 Calais Campbell 1 BEFORE_SNAP \n", - "4 2022110700 56 33131.0 Calais Campbell 1 BEFORE_SNAP \n", - "\n", - " time jerseyNumber club playDirection ... week \\\n", - "0 2022-11-08 01:16:09.2 93.0 BAL left ... 9 \n", - "1 2022-11-08 01:16:09.2 93.0 BAL left ... 9 \n", - "2 2022-11-08 01:16:09.2 93.0 BAL left ... 9 \n", - "3 2022-11-08 01:16:09.2 93.0 BAL left ... 9 \n", - "4 2022-11-08 01:16:09.2 93.0 BAL left ... 9 \n", - "\n", - " uniqueId frameUniqueId snap_frame frames_from_snap zone_prob \\\n", - "0 2022110700_56 2022110700_56_1 117 -116 0.775387 \n", - "1 2022110700_56 2022110700_56_1 117 -116 0.781979 \n", - "2 2022110700_56 2022110700_56_1 117 -116 0.781979 \n", - "3 2022110700_56 2022110700_56_1 117 -116 0.781979 \n", - "4 2022110700_56 2022110700_56_1 117 -116 0.832631 \n", - "\n", - " man_prob pred actual base_correct \n", - "0 0.224613 0 0 True \n", - "1 0.218021 0 0 True \n", - "2 0.218021 0 0 True \n", - "3 0.218021 0 0 True \n", - "4 0.167369 0 0 True \n", - "\n", - "[5 rows x 43 columns]\n" - ] - } - ], - "source": [ - "predictions = []\n", - "\n", - "for week_eval in [9]:\n", - "\tpreds_week = pd.read_csv(f\"/home/sam/repos/hobby-repos/nfl/data/inference/tracking_week_{week_eval}_preds.csv\")\n", - "\tprint(preds_week.head())\n", - "\tpredictions.append(preds_week)\n", - "\tprint(f\"Finished processing week {week_eval}...\")\n", - "\n", - "predictions_df = pd.concat(predictions, ignore_index=True)\n", - "predictions_df[\"base_correct\"] = predictions_df[\"pred\"] == predictions_df[\"actual\"]\n", - "print(predictions_df.head())" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "e799efa0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Index(['gameId', 'playId', 'nflId', 'displayName', 'frameId', 'frameType',\n", - " 'time', 'jerseyNumber', 'club', 'playDirection', 'x', 'y', 's', 'a',\n", - " 'dis', 'o', 'dir', 'event', 'o_clean', 'dir_clean', 'x_clean',\n", - " 'y_clean', 's_clean', 'a_clean', 'dis_clean', 'dir_radians', 'v_x',\n", - " 'v_y', 'passAttempt', 'possessionTeam', 'defensiveTeam', 'pff_manZone',\n", - " 'defense', 'week', 'uniqueId', 'frameUniqueId', 'snap_frame',\n", - " 'frames_from_snap', 'zone_prob', 'man_prob', 'pred', 'actual',\n", - " 'base_correct'],\n", - " dtype='object')" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "predictions_df.columns" - ] - }, - { - "cell_type": "markdown", - "id": "3a7e0e8b", - "metadata": {}, - "source": [ - "## Plot accuracy vs frames pre snap across Week 9" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "85892cf2", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABKQAAALDCAYAAAArAtgIAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAASdAAAEnQB3mYfeAABAABJREFUeJzs3Xd8U+X+B/DPyThJ2qaT0bKx7CVDxAkFlSUgDkRRqYogoCiu67j3Oq7zd71OFAEXQxBkiIBsERBFZC+VUaBQoEAHXUkzz++PtKFpRpM27QnJ5/168bJ9znPO9zk5SS1fnuf7CNENr5ZARERERERERERURxRyD4CIiIiIiIiIiCILE1JERERERERERFSnmJAiIiIiIiIiIqI6xYQUERERERERERHVKSakiIiIiIiIiIioTjEhRUREREREREREdYoJKSIiIiIiIiIiqlNMSBERERERERERUZ1iQoqIiIiIiIiIiOoUE1JERERERERERFSnmJAiIiIiIiIiIqI6xYRUhJj46CM4sHsb4uPj5B4KVaH8WVXHG6/9G2t+/D7IIyIiIiIiIiIKLiakasGAW27Cgd3bcFPfPm7HFi/4Bgd2b0PPq3q4HVu38gd8M/PzuhhiwDq0b4dpn3yI33/ZgG1bNmDG1I/Rtk1rv85947V/48Dubc4/e7b/ivWrluHdd97AFVe0rOWRV9+aH7/Hgd3b8Pm0KR6P33n7bc576tihXR2PLni+nfMVDuzehpEj7pB7KERERERERBQhmJCqBbv27AUAdOt2pUt7dHQ0WqVeAYvFim5du7gcS27YACkpydi1e2+djdNf7du1xeyvpqNJk8b4bMYXmDbjKzRr1hQzv5iGFs2b+XUNk8mEF/75Cl745yt45T9vYenyH9Hr6qvwzczPUb9+vVq+g+orLS3F1Vf1QFJSotuxIYMHoLS0VIZRBU+zZk3RuVNHZJ0+g1sHDZR7OERERERERBQhmJCqBRcu5OBU1ml07+qakLqySycIgoC1639yO9atW1cAl5JZoWTSxEdhMplwX/oYzJozD1/P/gb3P/gIFAoBT06a6Nc1bDYbVqxcjRUrV+OH5T/ik6nT8a+XX0esXo/eN1xfy3dQfbv37oPBYMSg/re4tDds0ADdu3XF5i2/yTSy4Bg6eCByc/Pwv/c/QtcrO6NRSorcQ/JIEASIoij3MIiIiIiIiChImJCqJbv37EX7dm2h0Wicbd26XomjGcew5det6FKWnHIeu7IL7HY7dldISA0ZPBAL5s7Cjq2b8OvGtXj3nTeQ3LCBW6zOnTpi2icfYuvmn7D9t034+ovP0O3KLm79KktJScbKHxbh+4XzkJToPgOoXPduXbF123YUFBQ623JycrFj5270ufF66HS6KmN5kpObC8CRrCoXGxuLZ596Aku+m4s/fv0Zv/+yAZ998oHH5YGj7hmBpYu+xfbfNuHXTeuwYO5MDB7Y36VPg/r18for/8Km9auwa9svWLroW9x+21C/x2g2mbF+w0YMHuR63cED+6OwsAi//va7x/Ou7tkDs76cjj9+24jfNq/Hxx+8iytatnDr163rlZj/zdfY+ftmrFq2GCPuvN3rWPx9PwRi8KABWLt+AzZu3oKi4mLcWuk+y3Xu1BFTp3yAXzetwx+/bcSSBd/g/ntHuvRp2aI5/vd/b2LzhtXYsXUTln//HZ54bLzzuLf6Vp5qZh3YvQ0vPf8sbh00AEsXfYtd27bghuuuAQA8+MB9+Gbm59jy81rs2LoJC+bOwi039/M47iGDB+LbOV853yMzv5yG667pBQB48z8v45cNa6BSKd3OmzH1Yyz//jsfrxwRERERERHVBBNStWTX7r1Qq9Xo0qmjs63blV2wZ+9+7Nm7D7F6PVq3Sr10rGsXHD+R6Uz6jBvzIN56/RWcPHUK777/EebMW4BeV1+FmV9Ohz4mxnmeI/ExDTEx0fhsxhf46JPPEKvX48sZn6JTxw5ex9e0SWPM+nIaSgwGPDR2AnLz8rz2FUU1TCaTW7uxtBSiKLrchy/x8XGIj49DUmIiruzSCf94djLy8y9i0y9bKoyrEfql9cbmX37Ff9/7CF/P/gatW7XC11985rK0787bb8NLzz+LjGPH8X//+wBTp32Ovw8dRpfOnZx9khITMW/2l7imV0/MW7AQ77z7Pk6eysLrr/4L94+6x68xA8DKVWvQpXMnNG3S2Nk2eFB/rPtpA6xWq1v/a3r1xPRPP0ZiYgI+m/YFZn/zLbpe2Rlzvp7hMgOpdatUzJjq6Dd1+hf4ftkKPDZ+LG7ql+Z2TX/fD4Ho3KkjmjdripWr18JqteKnDRtx62D3ZXvX9roas76chtQrWmDuvAX43/sf4Y8du9Cn96WZbW1at8K82V+hV8+rsHjJD3jn3Q+w4edNSOt9Q7XGBgC9rr4K/3h2MlavXY933n0fp8+cBQDcP2ok/vr7MD75bAY++uQz2GxWfPDu224z7SaMG4N33nwNVqsVn342A1OnfY7s7HO4+mpH/bblP65CQkI8rr/2GpfzkpIScXXPHlixcnW1x05ERERERES+qeQeQLiqWEdq+85dUCqV6NK5I5at+BGnsk4jJycX3bpeicNHjiIqKgqtW6Xi+x9WAHDMXJo4fiymfDoNn381y3nN9T/9jIXfzsE9d9/pbH/5ny/gj+07Mf7xyc5+Cxd/j6WLvsUTj43HuIlPuI2tZYvm+GL6Jzh//gIenfgkCouKfN7LiROZ6NK5ExQKBex2OwBApVKhS2dHsq1Bg/pVvh5RUVHY8vNal7bsc+cxbuITyM+/6Gw7fCQDtw4fAUmSnG3LV6zC8u8X4I7hwzD9868AAL1vvB5HjmbgmX+85DXmE4+Ph0KpwO0jRjkTfd8t+h7/fft1TBz/CBYu/t5joq2ybdt34MKFHAwa2B8zvvgaV7Rsgfbt2uKdd99Hk8aN3fo/M3kSCgsLcV/6IygsdMTd8PMmLPx2Nh6bMBb/fPk/AIDHJ4yDIACjH34U2dnnADie8ZLv5rpcL5D3QyCG3DoQZ89mO2flrVq9DncMH4a2bVrj0OEjAACFQoGX//UCLuTk4K6RD6CouNjjtV58/hkIgoARo0Y77wUAPvj404DHVa5F82a4/e77cOzYcZf2W4ePcHlu3y5YiO/mzcbo++/F5i2/AgCaNm2C8ePGYP1PP+Op5150eT+V2/bHDmRnn8OQWwdh0y+/OtsHD+wPhUKBFT+uqvbYiYiIiIiIyDfOkKolx44dR37+RWetqLZtWiMqKgq79+4HAOzZu89Z2Lxrl85QqVTYvWcPAODmfmlQKBRYve4n56yi+Pg45OTm4uSpU7i6p2OGR7u2bdCieTOsXL3GpZ9Op8W2P7ajR/euLssCAaB1aipmfjENZ86cxSPjJ1WZjAKA+QsXo2WL5vjPK//EFVe0RKvUK/D2G6+ifj3HjCVthWWJ3pSWluKR8Y/jkfGPY9yEJ/Dq62/DYDDgsykfoHmzps5+FovFmTxQKBSIi4uFwWjAicyT6NCurbNfUVERGjZsgE4d2nuNefNNfbFx0xYIguDy+vz62++I1etdrueL3W7HmnU/OZcD3jpoAM6ezcbOXXvc+tarl4T27dpi6bIVzmQUABw+chRbf/8DN95wnfPerrv2GmzYuNklgXPs+An8ttV1+Zq/74dAKJVKDOx/M1avXe9s27Z9B3Jz8zCkwiyp9u3aoGmTxpgzd4HXZFRCQjx69uiO739Y7nIvNbVj5263ZBQAl2RUrF6PmJgY7Nq9B+3bX3qeN6X1gVKpxGczvvSYjAIASZKwYtVqpPW+EVFRUc72IYMHYs/e/c4ZWURERERERBR8nCFVi/bs24ce3btBEAR069oFubl5OHUqy3Fs737ce89dAOBMTJXvsNe8WVMoFAqsWrbY43XLl4mVJ3Leev1Vr2PQx8S4JJ0++eh/yM3Nw7iJT8JoNPp1H98t+h7JDRviofT7MXzYEADAgYN/4quZc/Do2IdhMBiqvIbdbsfv27a7tP2y5Tf8+MNCTH7iMTz17AsAHMWr7x81EvfcfRcaN0qBSnXpLXrxYoHz669mzsE1va7G/LkzkXnyFH7bug0rV63B7r37AACJCQmIi43F3Xfdjrvv8lyXKTExwa/7B4AfV63B/aNGom2b1hg8aABWrVnnsV/5krwTmSfdjh07fgI3XH8tdFotoqKjoNNpkXnylFu/45mZ6H3jpeVn/r4fAnHdtb2QlJiI/QcOomnTJs72P3bsxKCB/fH+R59AkiQ0beI4djQjw+u1ymeJ+epTHafPnPHY3ufG6zHukYfRrm1rlxpt5bP3AKBp08aw2WzI8JDQqmjZilV45KF03NyvD5atWIUWzZuhY4f2eO2Nd4JzE0REREREROQRE1K1aNfuvejbpzfatG5VVj9qn/PYnn378OzTT6BB/fro1vVKnDt/HlmnHX8BL18aN/7xyS5/yS5nMBid/QDgf+9/jL8PH/Y4BoPRNVm07qefMXzYEAwZPBALF7sXmPbm40+nYebsuUhNvQLFxcU4cjQDTz4+AQBwwkNSxR/nzp/HicyT6NG9q7Nt7JgH8cRj47Fk6TJ8MnU6CgoKYZfseP7Zp6BQXJrtdez4CQwdPgJ9et+A66+7Frfc1Bf3jrwLn03/Ap9O+xxCWd/lK1bhhxU/eox/+PBRv8e6/8BBnDx5Cs8/+xSaNmmMH1etqdY9V4e/74dA3DpoAADg/Xff9nj8qh7dsX3HzoCv64uXiUpQKN2LigNAaan7csru3bpiyof/w85du/HG2+/iQk4OrFar8z0dqGPHjuPgn39hyOBBWLZiFYbcOghmsxlrKswcIyIiIiIiouBjQqoWldfm6db1SnTteiW+mTvfeezgn3/DZDKh51Xd0aVzR2ze8pvz2KlTp6FQKHD69BmPM2gu9XPMtiouKXGbfeTNex9Ogc1mw79efA4lJSVYuXpt1SeVKSwqctkF8JpePZGdfQ7Hj5/w+xqVKZVKRFXYpa//zf2w7Y8dePm1N1366fV6XLx40aXNWFqK1WvXY/Xa9VCpVPjovf/D2DEP4vOvZiE//yKKi0ugUCr8fm2qsnLNOowf+zAyjh131liq7MxZxzKvFs2buR1r2bI58vLzYSwthclshtFY6rJc0dmveXOX7/19P/hLp9WiX1pvrFqzDmvXb3A7/uI/nsaQwQOwfcdOnMpyvMdapaZ6fR2zTp929vGlsKgQer3erb1RSrLfY7/lpr4wmcwYN/FJWCwWZ3v5zL1yp06dhlKpROoVLb0+q3LLVqzEc08/iXr1knDrwP7Y/Muvfi1lJSIiIiIioupjDaladODgXygtLcWQwQOQ3LAB9uy7NEPKYrHgr78P4d6RdzlqS1VI9Kzf8DOsVismPPqIx+vGxcUCAA7+9TdOnjyFB0ffB12FpE65hIR4tzZJkvDq629j3foNePM/ryCtz43VureB/W9G504dMWfefK81eqrSvFlTtGjeHIcqzFSy2exuda/639wPyQ0buLSVvwblrFYrMo4dhyAIUKlUsNvtWPfTz7jlpr5olXqFW2xPr01VFn//A6ZO+xzvvv+R1z45Obn46+9DuG3orS6737VKvQLXXdMLv5QlHu12O37b+jv6pfVGcnJDZ78rWrbAddf2crmmv+8Hf93ULw1RUVH4dsEirFu/we3Pps2/4uab+kKtVuPPvw7hVNZpPHDfSK+7+eXnX8T2nbtw+21DXe6lslOnTiNWr0eb1q2cbfXqJeGmvn38HrvNboMECUrlpR9djVJS0K/SNX7auAk2mw0Txo1xez9VtnL1WkgS8MJzT6Np0ybcXY+IiIiIiKgOcIZULbJarTjw51+4qns3mEwmHPzzb5fje/bux4Oj7wNwqX4UAJzKOo0pU6fjqSceQ+NGKdjw82aUGErQuFEj3NQvDYsWL8XMOXMhSRJe+c9b+OyTD/DD4m+x9IcVOHf+Aho2qI+ePXugpLgEj09+1m1ckiThhX+9go/fj8J7//cmJkx6Cn9s9748q0f3rhg/bgx+27oNBQUF6NK5E4YPG4Jffv0N38xb4NdroVQqnUuqFAoBjRo1wt133Q6FQsBnM75w9tv8yxZMePQRvP7qv7Fn7z60aZ2KWwcNdM4GKzdj6hTk5uZi9569yM3LwxUtW+LekXdh85ZfnTWtPvz4U1zdszvmzfkKi5f8gIxjxxEXF4v27dri2l49cX1af7/GXu7s2WxMnf5Flf3e+3AKPpvyIebO+gJLli6HRqvBqHtGoLi4GFOnfe7s9+m0z3H9dddg9lfTMf+7xVAplRh1z93IOHYcbdu0dvbz9/3gr1sHD0B+/kWXJaQVbdy0GSPuHI4+N16P9Rs24vW3/g+ffvQeFi2Yg6U/rMCFnFy0bNkcra64Ao8+9iQA4J3/vo/ZX03HwnmzsWjJUmSdPoPGjVLQ+8brcdc9DwAAVq1Zi6effAwfvfd/mPvtd9DqtBh51x04kXkKHTu082vsm3/5FQ8+cB+mffoRVq5ag8TERNx79104dSrL9TU7lYUZX87EhHFjMPur6Vi/YSPMZjM6deyACxdy8OGUqc6++fkX8etvWzGw/80oKCx02XGPiIiIiIiIagcTUrVs9+69uKp7N/z5198uS4wAx5K+B0ffh+LiErdlRV9+PRuZmSfxwH33YsKjYwAA2dnn8NvWbfh502Znv+07d+G+9EcwftwY3DtyBKKidMjJzcX+/QfxnY8aUVarDU899yKmffIBpnzwLh4ZPwn7Dxz02Pf8+Quw2+x4KP1+REdF4fTpM5gydTpmzZkHm83m1+ug0WjwzpuvOb8vKirGgT//xIv/ehXb/tjhbJ/x5UzodDoMHtQfA/vfjL/+PoSJTzyNp56Y6HK9hYu/x62DB2D0/aMQFaXDuXPnMffb7zD9i6+dfXLz8nDv/Q9j/LgxuLlfGu65+05cvFiAo8eO4f2PPvVr3NXx+7btGP/4k3hs/Dg8NmEcrFYrduzahQ8++tRl57bDR47i0ceexD+emYzHJ4zDuXPn8em0z1G/XpJLcgXw//1QlcSEBFxz9dVYtWatx3pUAPD7HztgMBoxZPBArN+wEb9t3YaHx07EhEcfQfoD90GhEHAq6zQWLVnqPOfQ4SMYNXoMJk18FHePuAMaUcSZs9lYs+4nZ5+CgkI8+fTzeO6ZJ/H05MeRdfoMPpwyFc2bNfU7IfXH9p3496tvYMxDo/H8s0/h9OkzeP+jT9C4UYrba/bpZzNw+vQZjLpnBJ54bDxKS0tx+MhRLP9xldt1l61YibQ+N2LN2p/cPqdEREREREQUfEJ0w6urt96KiChM9E3rjSkfvIvRDz+KXbv3yD0cIiIiIiKisMcaUkQU8e66/TacOpXFZBQREREREVEd4ZI9IopYgwbcgjatW6FP7xvw9n/fk3s4REREREREEYNL9ogoYh3YvQ0lJSVYvXY9/vPm//ldE42IiIiIiIhqhgkpIiIiIiIiIiKqU6whRUREREREREREdYoJKSIiIiIiIiIiqlNMSBERERERERERUZ1iQoqIiIiIiIiIiOoUE1JERERERERERFSnmJAiIiIiIiIiIqI6pZJ7AOHGbrfCbi6EoNRAEAS5h0NEREREREREVKskSYJkM0EhxkKh8C/VxIRUkNnNhTAVHJV7GEREREREREREdUoT1woKbaJffZmQCjJBKQIAopOuhFId5fd5MVFRKDYYamtYIRlbrrgJcbEAgPyCwjqPHWmvtVyx5XzGQOQ9Z36WIyM2n3P4x5UrNn9mR0bcSPwsyxmbzzn848oVmz+z697l8pxtFgNKcvc6cyL+YEIqyATBUZZLqY6CSozx+zxRGwOVVZ6SXnLFliuuRhcHAFAZ7XUeO9Jea7liy/mMgch7zvwsR0ZsPufwjytXbP7Mjoy4kfhZljM2n3P4x5UrdvJ/5wEAiibdU6dxy/E5142afJbLcyL+YFFzIiIiIiIiIiKqU0xIERERERERERFRnWJCioiIiIiIiIiI6hQTUkREREREREREVKdCuqi5Wq3G4xPGYeiQQYjV63H4yFFM+XQ6tm77o8pzBw24BQ+l34/UK1qixGDAxk2/4P2PPsHFiwVufe8YPhQPPnAfGjduhOxz5zH32wWYN39hbdwSEREREREREVHEC+mE1Jv/eRm33NQP38ybj8yTpzB82K2YOuUDPDxuInbv2ev1vJEj7sC/X3oeW7f9gf++9xGSGzbAfaNGomOH9rj3gYdhNpudfUfceTte+dcLWLt+A2Z98y16dO+Kl55/FlqtFl/NnFNr9yYIAhLj4yCq1VAICqhUKuijo2stni9yxZYrriiqAQAatf/bUQaLt3u2S3aYLRbkXSyAJEl1Pi4iIiIiIiKiuhSyCalOHTtg8MD++N/7H2PmnLkAgGUrVmLpwnl4ZvLjuP/BsR7PU6lUeOLxCdi+cxfGjp/kbN+zdx8+/fh93HXHbc7ZTxqNBk88Nh6bNm/B08+9CABY/P0PUAgCxo99GIsWL0VhUVHQ700QBCTXqweNRoTNbofdbofVZg16HH/JFVuuuBZLiL3WAqBSqqARRYgqNbJzcpiUIiIiIiKikFOybK7cQ6AwErIJqf4394PVasXCJUudbWazGUt+WI7JkyYiuWEDZJ8773Ze61apiIuNxeo1613aN/3yK0pKSjBowC3OhNTVPXsgISEe8xcudun77XeLMOTWQeh94/VYsXJ10O8tMT4OGo2IwuJi5JUtIVQoFLDb7UGP5Q+5YssVV6VUAgCsNludx/Z1z4nxcYiNiUFifBxy8y/W7cCIiIiIiIiqYN65Re4hUBgJ2YRU+3ZtkHnyFEpKSlza9x84CABo27aNx4RU+XIsk8nkdqzUZEK7tm0hCAIkSUL7tm0AAAcP/uXS7+Cff8Nms6FduzY+E1KJCQlISIh3jWEowOHcA0iIi4VGF+fxvJjoaEh2OwqLip3JEUEQoBAEr7Fqk1yx5YqrUDhq+cvx5vd1z4VFxdBHR0MfEwMhyK+LRhSdn426JkfsKJ0OAFAvsU7DOsn1ekdaXDmfc6R9puSMG4nPOdLeX/yZHRlxI/GzLGdsPufwjytXbP7MrnuXy3M2GRUozA4sTsgmpOrVq4cLF3Lc2i/k5AIAGtSv7/G8zJOnYLfb0a1rFyxdtsLZ3qJ5MyQlOl7N2Fg9CgoKUa9ePVitVuTl57tcw2q14mJBgdcY5e65+05MHO+6dPDwoYMY0Nf3rCoBgFWm2VAU2mx2O9QKbn5JREREREShR9n1WscXWzfIOxAKCyGbkNJqNDBbLG7t5rKZTxqNxuN5Fy8WYM26nzBsyK04dvwEftqwEQ0aNMBLzz8Di8UCtVoNrUaLAhRCq9V4rSdkNpm9xig3/7vFWLPuJ5e2UoNjCV5+QSFURs9Jp/Ji2hWXjHHJXt0pf9OH2pI9AJDsEsxmC3Ly8r32qY7YmBgUFhcH9ZqhHLs8kx/s19Ffcr3ekRZXzuccaZ8pOeNG4nOOtPcXf2ZHRtxI/CzLGZvPOfzjyhU7YeAIAEDOj4ur6Fk7+JzrRnU+y1Zz4GMM2YRUqckEUe0+LU0sSxJ5WpJX7rU33oZGo8FzTz+J555+EgCwfMUqnMo6jVtu6guDweCIUWqCWu35JRA1os8YAJCXn+82u8puNfg8h4iIiIiIiIgo0oVsQionJwcNGjRwa69fLwkAcP7CBa/nFheX4ImnnkNyckM0bpSCM2ezcfZsNr6Z+Tly8/JQVJZdzMnJgUqlQmJCgktiSaVSIT4uzmcMIiIiIiIiIiKqnpAtVvP3oSNo3qwpoqOjXdq7dOoIADh06HCV18jOPoedu/bg7Nls6GNi0KF9O/y+bbtLDADo2LG9y3mdOraHUqnEobLjREREREREREQUPCGbkFq7fgNUKhVG3DHc2aZWqzH8tqHYu++Ac4e95OSGaNmieZXXm/zERCiVSsz55ltn27btO3DxYgFG3nWHS9+Rd90Jg9GITb/8GpybiVDXXHMd3nn7f5g48Qm5h0JEREREREREISRkl+ztP3AQq9eux5OTJiIxMQEnT2XhtqGD0SglBS+/9oaz39uvv4KeV/VAp269nG1jHhqN1qlXYN+Bg7DZbOiX1gfXX3cNPv7kMxz48y9nP5PJhClTp+PfL/0D7/33Lfy29Xd079YVQ4cMwkdTpqKwsLBO7zncdL2yG/Ly8tCsaTMkJSUhNzdX7iERERERERERUQgI2YQUALz079cwaWI2ht46CLGxehw+chSPPfk0du7a4/O8I0cycFPfPkjr0xsKpQKHjxzF08+9iLXr3bemXLBwMaxWK9IfGIW+fW5EdvY5vPPuB/hm3vxauqvaY9erYE5LhpSogZBngrgxG4oiz7sI1raEhES0aNESc+bMxO2334WuXbvjp5/WyTIWX9RqERaLWe5hEBEREREREUWUkE5Imc1mvPfhFLz34RSvfR4aO9GtbfOWX7F5i//L7RZ//wMWf/9DtcYYCiQApempMI5OBTRKZ7thcgfoZmdAOysDQh2PqVvXbjAYDPj70F/Yf2Cfx4SUVqtFv743o2PHTtDrY1FSUoyMjKNY8eMy506IKpUKaX364cqu3RAfFw+j0YiTJzOxctUK5OXl4oqWqRg3bgJmzPgMx45nOK+dEJ+A55//JxYunI+du3YAAEbcNRKdOnXBp59+iFtvvQ0tWrTA0aNHMeebmWjRoiWuu+4GNGvaDDExehQXF+PAgX1YvWYlrFbXpF79+vVxy80DccUVqdBoNLh4MR/7D+zD2rWrccUVqRg3dgLmzJmJg38ecDnvyiu74d577sPUz6bg5MnM2njZiYiIiIiIak3+KxPkHgKFkZBOSJF/StNTYRzbBpAk1wOiwtEOQDcrw8OZtadr1+44eHA/bDYb9u7djWuvuQ5NmjRFVtYpx9BEEePGTkD9+g2wY+d2nDl9GtHRUWjfviPi4uJhMBggCALS0x9G61ZtsGfvbvz66y/QaDRo3aoNkhsmIy8v8CWACoUC6eljkJl5Aj+uXOGcHdW5cxeIahG//74VBkMJmjZthmuvvR6xcXGYN2+O8/zk5BSMf3QibDY7/vjjd+Tn5yExqR7at+uAtWtX49ixDFy8mO+4/0oJqW5duyM3N4fJKCIiIiIiIop4TEiFiMK3u8HWSBfweZICsDeNcSSjhErzoAQBkCQYx7SGqX8KBLu3qwhwzLNypzhtgP6FXQGNqXGjxmjQoCGWLVsKADhx4jguXryIrl27ORNSvXunITk5xW0m0Yaff3J+3b37VWjdqg1WrPgBW379xdm+adPPAY2nIrVajQMH9mPdutWw2mzO9lWrfnSZCfXH9m3Iyc3BgP6DEBcXj4KCiwCAYUOHAxDw8ZQPnG0AsHr1j86vd+/ehRtu6A2NRguTqRQAEB0djdat22DjRvdlo0RERERERESRhgmpEGFrFAVby5jgX1gQAAGwN9cH/9pedO3WHUVFhcg4dtTZtm/fHnTr1h0//rgckiShU8cuOHP2jNssooo6deqM4uJi/LY1uLsd/vHH725tFZNRarUItVqFk5mZUCgUaNyoMQoKLiI6OhpXXJGKLb9udklGVbZr9w707XsTOnfugh07/gAAdOnSFUqlErv3BJbcIyIiIiIiChX68S8BAHLeek7mkYSHUKoDLQcmpCioBEHAlV26IuNYBhITE53tp06dRO/eaWjVqjWOHDmMpKQkHDi43+e1khKTkJNzAXa716ldAbPZbCgsLHBrj4uLR/9bBqB9+46IiopyOabVagEAiYlJAIBz2dk+Y1y4cAGnTp1Et67dnQmprl27IfPkiWotMyQiIiIiIgoFqpSmcg8hLIRiHWg5MCFFQZWa2gqxsXHoemU3dL2ym9vxrl2748iRw0GLJ3lZaigoFB7brVYrpEq1tgRBwCNjxkGni8KmTT/jwoXzMJvNiI2Lw90j7oFQeSmkH3bt3omhQ25DbGwcVCoVmjdrgaU/LAn4OkRERERERBReQrEOtByYkAoRyjMGeKvj5IuzhpQA9xpSgOMNLgGKU8XVriEViG5du6OoqAg/LPve7Vinjp3QsUMnfK9SITc3Fw0bJvu8Vm5eLpo2aQaFQuF1lpTRaAQAaHVal/aE+AS/x5ycnIL69Rvgu+++xa7dO53trVq1dulXPrupYbLvcQPA3r27cevgoeh6ZTeo1WpYrVbs27fH7zERERERERFR+LHrVTCmp/quAz06FZolmWG/fI8JqRAR++Luai9NM1bMrlZ8Q5d9r/visM/sqq+ETyBUKhU6duyM/fv34sCBfW7HCwsL0LVrd3Ro3xEHDu7DzTf1R8cOnbzWkTpwYD/at+uA66693qWoeUX5+fmw2Wxo2fIK/PnnQWf7Nddc5/e4nfde6YfB9dff6PJ9SUkJjh3LwFU9rsYvv/iuI2UwGHD48N/o1q07VCoVDh8+BIPBAIWXmVtEREREREQU/sxpyYCo9N5BEACNEuY+ydCuyKq7gcmACakwoC1LNlVefwqz3bn+tC50aN8RWq0Wf/31p8fjp06dRHFxMbp27Y75C+aic6cuGDXqAezYuR2nT2chSheF9h06YOn3i3E2+yx27dqB7t16YMiQ29CkaTOcOHEcolpEq1at8fvvv+HPvw7CZCrF/gP7cN21NwCSY1ZVu3btERPtf4H4CxfOIyc3B4MHDUFsbCxMJhM6dewMnc5918Ply5di/PjH8MSkp/DHH78jLz8PCQkJaNe2PT6e8oFL3127duL++9MBAGvXrQnglSQiIiIiIqJwZE/U+NVPSvKv3+WMCakwIMCxvlSzJBPmPsmQkjQQck0QN9Vthf6uXbvDYrHgyFHPNaIkScLff/+Frl27QaVSYcbnn+GmfregY8dO6NH9KhQXFyMj4wgKyoqOS5KEr2d+gX59b8aVXbuhU8fOMBgMOJF5HNnZZ53XXbbseygVSvTqda1jadz+vVi5cgWefsq/nR/sdjtmzfoKw4YOR9+0frBYrDj45wFs3forJj/5jEvfs9ln8enUKeh/y0Bcc821UKnUyM/Px/79e92u+9fff8JgMEAQBPz110G340RERERERBT+Ku6mZ+nd0K9zhFxTLY9KfkxIhRFFkVXWKX2z53xdZZ9Fixdg0eIFABxLBZctX4ply5d67W+1WrF23WqsXbfaax+DwYC582a7tb/w4rMu3y9ctAALFy2ASuk+PfLChfP48qsZVV4DAM6fP4dv5s7yOp5ykiTBbrfjr78OwmoN77W/REREREQU/kw7t8g9hMuKt930fJ8kAWY7xE2+d3cPB0xIEdWSDh06ISYmxqVQOhERERER0eXKsGyu3EMIaRVnQgl5JthTdCgd3cp9Nz1vyutAz84I+4LmABNSREHXtGkzJCen4KZ+N+P06SwcP35M7iERERERERFRLfE6E0qSPO+mV/F4xWN1XAdabkxIEQXZNb2uRdeu3XH27BksXLRA7uEQEREREREFhTKlmeOLvHx5BxJiStNTL+18X5G3RFSF4+LKLChPG2SpAy03JqSIgqy8VhUREREREVE4iR3/IgDg3KR7ZB5J6LDrVTCmp/qeCeWD8rQBugiZEVUZE1JERERERERERNVgTksGRD8LlnsQCbvpecOElFwCT5xSJBDgWIBMREREREREIU9K1FTzxMjZTc8bhdwDiER2yQ6Fgi89uVMqFLBLdrmHQURERERERH4Q8qoxwynCdtPzhjOkgsRcnAVLyRnn9zFRURC1MR77CoIAtVKFpIR45BcUAoCsCSq5YssVVyhb1ytHfF8xE+JioVKqYLXZEBvj+b1TXTqtNqjXC/XYGlEEgKC/jv6S6/WOtLhyPudI+0zJGTcSn3Okvb/4Mzsy4kbiZ1nO2HzO4R9Xtthlf5fiz+xLorcXw2C2A2rBcw2p8kLnFY4JZjviFpxC3OJzEKp4LS+X/zebS+0oDDAOE1JBIsY0gRjTBHarAcbcAyg2GKCyek4+FJWUILlePeijoxGt08FmL5sRI9dSLbmWickUtzwhJVXeAaFOgsP9ngXHzCiFQgFjaSmyc3JqZWyFxcVBv2aoxhZFtSxxK5IrdiTFlfs58/1VNyL1OUfS+0vuZyxn7EiKK/dz5vurbkTqc46k91fDsr+nRNI9Vxm3GFDtzoW1V333Y2UzobSzjkJx1ggpSeOym15RTWPXkup8lq1mQ8BxmJCSgSRJyM7JQWJ8HES1GgpBAZVKBatVnql6KqU8seWKq1Y73vZms6XOY3u8ZwmwWK0wWyzIu1ggT6KMiIiIiIiIAiYBsNf3MovJbIdudga0szJYRtoDJqRkIkkScvMvOr+PjYmRLdMrV2y54tZLTAAA5OTl13lsOZ8zERERERFRTRROe1vuIYQcW7s42K/Qu7QJ543QfXXUOROKPGNCioiIiIiIiIiqZDt7Uu4hhBzT4MZubbpZGdCuyJJhNJcXbvVGRERERERERBQgSVTAfEsj10aTDeKGs/IM6DLDhBQRERERERERVSlq2H2IGnaf3MMIGeYbGkDSq13axF/OcZmen5iQIiIiIiIiIqIqaXrcAE2PG+QeRsgwD27i1iauPC3DSC5PTEgREREREREREQXAXk8DS896Lm2Kc0aod+TINKLLDxNSRERERERERER+sutVKHmqA6AUXNrFNach2GUa1GWIu+wREREREREREVVBAlCangrj6FRAo3Q7Lq7icr1AMCFFRERERERERFSJXa+COS0Z9hQ9zGeLYE/RoXR0K0CSPPa39EuBalZGHY/y8sWEFBERERERERFRmcozoQzOA5LjjyB4OEmCcXQqNEsyucuen5iQIiIiIiIiIqIqWc+eknsItaJ8JpSUqIGQZ/I+E8pTIqriMY0S5j7J0K7Iqt0BhwkmpIiIiIiIiIioSkXT3pJ7CDVSOfGk3pgN8x3N3WtC+ZoJVQUpSRPEEYc3JqSIiIiIiIiIKGx5LUb+TEdApQhsJlQVhFxTtc+NNExIEREREREREVHYKk1PhXFsG/fEk7Is8VSDBJSTJAFmO8RN2TW/VoRgQoqIiIiIiIiIqpTw2mcAgJxJ98g8Ev/Z9SoY01M9L8ELRiIKcF5bNzuDBc0DwIQUEREREREREYUlc1oyICqr7uiv8llWFZNZZjt0szOgnZURvDgRgAkpIiIiIiIiIgpLUmIQi4yXzYTSzjoKxVkjpCQNhFwTxE3ZnBlVDUxIEREREREREVFYEvKqWWS8iplQQVrsF9FCOiGlVqvx+IRxGDpkEGL1ehw+chRTPp2Ordv+qPLca3r1xLgxD6F1q1QoVUpkZp7EvPkLsfzHVS79YmKiMW7MQ7ipXxoaNqiPvPx8/L5tO6ZO/wLZ2edq69aIiIiIiIiIqJaJG7NheLojoFZ471S5vlSlmVBiih7ms0WcCRVkIZ2QevM/L+OWm/rhm3nzkXnyFIYPuxVTp3yAh8dNxO49e72el9bnRnz8/n+xd99+TJ3+OSQJGND/Jrz9xquIj4/DnLnzAQCCIODzz6Yg9YqWmP/dYmSePImmTZvgnhF34bprr8GwO0bCYDDU1e0SERERERERkZ/sehXMacmQEjUQ8kwQN7onjBRFViiPF8HWJs79AuWJKJsEqLzPhIqNiUFhcXHt3kwECtmEVKeOHTB4YH/87/2PMXPOXADAshUrsXThPDwz+XHc/+BYr+eOGjkCF3Jy8PC4x2CxWAAACxd/j+VLFmD4sCHOhNSVXTqhc6eOeOPtdzH/u0XO80+cOIk3Xvs3ru3VEz/9vKkW75KIiIiIiIiIAiEBKE1PhXF0KqC5VLDcMLmD25I6SSXAlhLl+UJliSdxSSYsfZJZE6qOhWxCqv/N/WC1WrFwyVJnm9lsxpIflmPypIlIbtgA2efOezw3OjoahYVFzmQUANhsNuRfLHDrBwC5eXku7RdycgAApaZqrjUlIiIiIiIiCjMly+bKPQQAZcmosW0u1XkqJyoc7QB0ZTveWa6qB+jVLt2UhwuhWZLpknhSrsiq/YGTi5BNSLVv1waZJ0+hpKTEpX3/gYMAgLZt23hNSG3fuROPPJSOxyc+ih+W/whIEgYPGoCOHdrh2ef/6ex38M+/YDAYMGnCOBQUFODEiZNo1qwJnnnycew/cBC/b9vuc4yJCQlISIh3aSs1FOBw7gEkxMVCo/MwJdALjShCFNVVd6wFcsWWK26UTgcAqJdY56Ej7rWWK7aczxiIvOfMz3JkxOZzDv+4csXmz+zIiBuJn2U5Y/M5h39cuWKr/twJAKiXmFCncctpRBHKBC3y01u5130CHN9LEkpHt0KjDQVQltiQPai523Uazj+DmB0lgFoP+PF+jbTnXJ3PssmoQGF2YHFCNiFVr149XLiQ49Z+IScXANCgfn2v506f8RWaNGqEcWMexPixDwMADEYjnnruRfy8cbOz38WLBXj2+X/h1ZdfxFczpjrbt/y6FU899yJsNpvPMd5z952YON516eDhQwcxoO/qqm+QiIiIiIiIiKpki1ai6PokoJ4WxoZqSKKPAuWCAEkjoPi6RMT+nIPiXq7JM0WJFVF7CrycTHUpZBNSWo0G5gpL7sqZy5bRaTQar+eaLRacOHkKa9dvwE8bNkKhUOCuO4fjnTdexdgJT2Df/gPOvnn5+fj778P4dv5CHD12HO3atMZDDz6AN177N575x0s+xzj/u8VYs+4nl7ZSg+ONnV9QCJXR7vf9ylkkTa7YcsUtz/Lm5OXXeexIe63lii3nMwYi7znzsxwZsfmcwz+uXLH5Mzsy4kbiZ1nO2HzO4R9XrtiNbrkBAJCzbnmdxPNWK8ofhVobSq5Qwh7jmvZQ/XIOeefzvJzlWaQ95+p8lq3mwMcYsgmpUpMJotp9WppYlogy+ajv9M8XnkWXzp0w4t7RkMrWlK5Ztx5LF83HC889hVGjxwAAmjRuhK8+n4qX/v0a1v/0MwDg542bcfrsWbz1n1fw/fXXYsuvW73GycvPR16+6wOyW7krHxEREREREYWf6GH3Ob6oo4SU11pRfhByTTD3TXZrFzecDcbQKAh8zHOTV05ODurXr+fWXr9eEgDg/IULHs9TqVS4/bZh2PzLr85kFABYrTZs+XUrOnZoD5XKkYe7bdgQaEQRmzZvcbnGxo2/AAC6XdklKPdCRERERERERP6z61Uwpqd6rhXliyQBJhvUW87B0ruhyyGh2AL19twgj5SqK2QTUn8fOoLmzZo6d8Ir16VTRwDAoUOHPZ4XHx8HtVoFhdL91lQqJZRKxx8ASEpMhCAIUFbqW56wUqoCmxJIRERERERERDVnTksGRGVgySgAEASot56HrV08pEq766m3nIdg8b+0DtWukE1IrV2/ASqVCiPuGO5sU6vVGH7bUOzdd8C5w15yckO0bHGpan5eXj4KCgtxU980Z2IJAHQ6HdJ634hjx447l/tlnjwJhUKBAbfc7BJ78MD+AIC///ac9CIiIiIiIiKi2mOvr632uZbOiTANaeLWLv4c4DZwVKtCtobU/gMHsXrtejw5aSISExNw8lQWbhs6GI1SUvDya284+739+ivoeVUPdOrWCwBgt9sxa/ZcPPH4BMyb/SWWrVgFpVKBO4YPQ3JyQzz/0svOc5cuW4EHH7gPr/zrBbRv1xZHM46hQ/u2uGP4MBw5moH1GzbW9W0TERERERERRTRJACzX1ferr7j6NCzX1ocUJ15qTNLAkuZaP0ootkD9R04wh0k1FLIJKQB46d+vYdLEbAy9dRBiY/U4fOQoHnvyaezctcfneTO+nIms02dw/6iRmPDoGIhqEYePHMXkZ19wFi8HgIKCQoy870E8PnEc+vS+AXffdTsuFhTg+x9W4KNPpsJqtdbyHRIRERERERER4KgbZU5LhnlgY9jaxfvuLEmA2Y6oj/6EdX08iv/X02d3LtcLPSGdkDKbzXjvwyl478MpXvs8NHaix/aVq9di5eq1VcY4f+ECXn7tzWqPkYiIiIiIiIiqT0LZjnqjUwGNH7Wcywqd62ZnQFFkhfh7DtSb3YuYu1A44gRYkYpqUUgnpIiIiIiIiIgoNOS/MqFWrluangrj2DaORJMnlXfaM9uhm50B7awMZ1PUlL9QcF19QOWhVLYkwdy/MZSZJdBVOIfkxYQUEREREREREcnCrlfBmJ7qnnQqJ0mAxQ7d1EPQJETBfLYI4qZsKIpcS+wIxRbvQQQBkCQYR6dCsyTT7VySR8juskdERERERERE4c2clgyISs/JKMDRLiohlNoQ/+0paFdkeUwomdOSPc+OqngdjRLmPsne+1CdYkKKiIiIiIiIiKqkH/8S9ONfCuo1pUSNf/2SfPcL1nWo7nDJHhERERERERFVSZXSNOjXFPJM/vXL9d0vWNehusMZUkREREREREQkC/XWC96LmQOOYyYbxE3ZPq8jbswGTDbfhdH9uA7VHSakiIiIiIiIiEgWlmvre68fVVboXDc7o8pC5IoiK3SzM5wFzKt7Hao7XLJHRERERERERHVOAmC6o7n3DmY7dLMzoJ2V4df1yvsZR6cCGmW1r0N1gwkpIiIiIiIiIqpz1i4JsLWOdWkTLpRCs/QkFLkmiJuyA5rRJADQzcqAZkkmzH2SISVpIFTjOlQ3mJAiIiIiIiIiojpnuqOZW5tudga035+s0XUVRVZoV2TV6BpU+5iQIiIiIiIiIqIqmXZuCdq17EkamNOSXRtLrNCsPh20GBTamJAiIiIiIiIioioZls0N2rVMQ5sCKtd91jSrsiAYbUGLQaGNu+wRERERERERUZ2RlAJKb2vq1l7TpXp0eeEMKSIiIiIiIiKqkjKlrOZTXn61r2HXq2B8tC2k+lqXdtWOHCgzS2oyPLrMMCFFRERERERERFWKHf8iAODcpHsCPlcCUJqeCuPoVECjdDuuWcLZUZGGS/aIiIiIiIiIqFaVpqfCOLYNIHpOQ9hS9XU8IpIbE1JEREREREREVGvsehWM6amAJAGC4N5BklB6/xWw67mIK5IwIUVEREREREREtcaclgyISs/JKMDRrlHC3Ce5bgdGsmL6MUjMxVmwlJxxfh8TFQVRG+P3+TqttupOtUSu2HLF1YgiACA2xv/nEyyR9lrLFVvOZwxE3nPmZzkyYvM5h39cuWLzZ3ZkxI3Ez7Kcsfmcwz+ubLHLEkqBPmN7ih4GP/qJKXqf1+ZzrhvV+SybS+0oDDAOE1JBIsY0gRjTBHarAcbcAyg2GKCyBjYBrbC4uJZGF7qx5YgrimrZYkdiXDliy/2M5YwdSXHlfs58f9WNSH3OkfT+kvsZyxk7kuLK/Zz5/qobkfqcI+n91VCSqhXXfLbI735VXZvPufZV57NsNfuTcnTFJXtEREREREREVGvUG7MBu+S9gyQBJhvETdl1NyiSHWdIEREREREREREARwFyc1oypEQNhDwTxI3ZUBRZAQCF096u1jVt7eMBhZf6UWWFznWzM5xxKDIwIUVEREREREQU4SQApempMI5OBTRKZ7thcgfoZmdAOysDtrMnA7+uABgntPXewWx3Xp8iCxNSRERERERERBGuND0VxrFtHDOWKhIVjnYAWJ4X8HXNNzeCrXWsa2OxBdqFJ6A4VwpxUzZnRkUoJqSIiIiIiIiIIphdr4IxPdW5fM6FIACSBOPoVGjFW6Gw2IGZn/h1TfNNjWAc19rtWNS0w9AuDXy2FYUXJqSIiIiIiIiIIpg5LRkQld47CAKgUULT80YoTDafCSlvS/+clzpVAs3yU0EYNV3umJAiIiIiIiIiimBSosa/joqqu3hd+ldGfbgAgs3HjnsUMfx4OxERERERERFRuBIKLf51tFdx2NfSPwCQJJhvaAi7nnNjiAkpIiIiIiIioohm7RBXdSezHYLZd0bKufTPUzIKcC79M/dJrsYoKdwwLUlEREREREQUYex6FcxpybBcXQ+WvilV9lfkmbwuwyvn79I/KcnPJYIU1piQIiIiIiIiIooQVRUd98aerIOUq3CbJVWe2JISNTD3qufXtYRcUyBDpjDFhBQRERERERFRhKiq6DhKLNB9eRSWXvVg7VXf9VB0HnQniwBUM7ElSYDZDnFTdg3ugMIFE1JEREREREREl4nyGUn2FD3MZ4sgbsyGosjq97lVFR2HWgnNqixol55EwdwbYU+Jch7+s8UPiMq9CAxtAnuKDqWjW1W5jM/l2oIA3ewMv8dL4Y0JKSIiIiIiIqIQV3lGkqGs3TC5A3SzM6CdlQEvpcSdnEXHvREEQBRg7pMM7Yos6D47hJL/dHPpYugRD/SIdySYvCW2APdjZrtznEQAE1JEREREREREIc/rUjtR4WgHoKsi2WNvHOXzeLnyouPihmwYHjNCaqhz7+QtEVXhuLgyC8rTBgi5Joib/J/JRZGBCSkiIiIiIiKiEFOxWDhKLN6X2gkCIEkwjk6FZkmmW9Kn/Dq2ljEw3drEr9jlRcclvcpl57wemWMBADubf+7XdZSnDVUmyShyMSFFREREREREFCKqVSxcEBzL+J5oD2WWAUKeCeqN2TDf0bxGRcfNacmAWlG9GwF30yPfmJAiIiIiIiIiChFV7oLng3lQhRlQz3QEVIoaFR2vODsqINxNj/zAhBQRERERERFRCKhyF7xAKMvOr0HRcSGvGjOcuJse+SmkE1JqtRqPTxiHoUMGIVavx+EjRzHl0+nYuu2PKs+9pldPjBvzEFq3SoVSpURm5knMm78Qy39c5dY3KTERj00Yhz69r0d8XBxycnOx7Y8dePm1N2vjtoiIiIiIiIjcVLkLXiCCUHRc3JgNw+QOgKjwfL3y2VfcTY+qIaQTUm/+52XcclM/fDNvPjJPnsLwYbdi6pQP8PC4idi9Z6/X89L63IiP3/8v9u7bj6nTP4ckAQP634S333gV8fFxmDN3vrNvcsMGmPO1oyDbd4u+x/nzF1C/fj107tSh1u+PiIiIiIiIqJy9iX+74AVLVUXHFUVW6GZneF5CWDYTSjvrKBRnjZCSNNxNjwISsgmpTh07YPDA/vjf+x9j5py5AIBlK1Zi6cJ5eGby47j/wbFezx01cgQu5OTg4XGPwWKxAAAWLv4ey5cswPBhQ1wSUi//60VYbTbcc/+DKCgorN2bIiIiIiIiIqrg0i54epiGNK7T2P4UHS+f6WQcnep6oMJMqBouLqQIFbIJqf4394PVasXCJUudbWazGUt+WI7JkyYiuWEDZJ877/Hc6OhoFBYWOZNRAGCz2ZB/scClX8sWzdH7huvw+lv/h4KCQoiiCLvdBqvVViv3RERERERERARUcze9smLhus8OQZMQBUMDlWshc7+D+190XACgm5UBzZJMXBxbH7YoFaK+3c+ZUFRjIZuQat+uDTJPnkJJSYlL+/4DBwEAbdu28ZqQ2r5zJx55KB2PT3wUPyz/EZAkDB40AB07tMOzz//T2e+aXj0BALm5efhi2ie4pldPWK1WbN32B15/8784c/aszzEmJiQgISHepa3UUIDDuQeQEBcLjS7O7/vViCJEUe13/2CSK7ZccaN0OgBAvcQ6Dx1xr7VcseV8xkDkPWd+liMjNp9z+MeVKzZ/ZkdG3Ej8LMsZm885NOPaopUouj4JtgQ1lPkWWBqIMN7dJOBd8JIWnUHShkJoxFIY1DYc69cIkij4X7y84nXUeiCA5xW14GcAgGg0AgGeGwyXw3MOh9jV+SybjAoUBripYsgmpOrVq4cLF3Lc2i/k5AIAGtSv7/Xc6TO+QpNGjTBuzIMYP/ZhAIDBaMRTz72InzdudvZr3qwZAOCVf72IA3/+iWf+8RJSkpMx4dEx+GLaFNwx8j6UlnqfwnjP3Xdi4njXpYOHDx3EgL6r/b9RIiIiIiIiClsSgLyRjZF3V2NIGkWFA5Lv3fQqHRPMEhIXZSFxwWlnm7LEhsRFp5F7X1OviSfYJEDl+zpEcgjZhJRWo4G5wpK7cmaTI0Gk0Wi8nmu2WHDi5CmsXb8BP23YCIVCgbvuHI533ngVYyc8gX37DwAAoqIcWb+c3FxMnPQ0pLLM9Lnz5/HuO2/g1kEDsPj7ZV7jzP9uMdas+8mlrdTgWBaYX1AIldHu9/3GxsSgsLjY7/7BJFdsueKWZ3lz8vLrPHakvdZyxZbzGQOR95z5WY6M2HzO4R9Xrtj8mR0ZcSPxsyxnbD7n0IprTE+FsTxhVFE1dsGTiqzIrRRb+iwfOoPRfelfWY0ncUkmLH2SXYqOV7xOIBrdcgMAIGfd8mqcXXOh/JzDKXZ1PstWc+BjDNmEVKnJBFHtPi1NLEtEmUzeZy7984Vn0aVzJ4y4d7QzybRm3XosXTQfLzz3FEaNHuOIUTb7ac26n5z9yr9/6/VX0bVLF58Jqbz8fOTluz4gu9Xg5x0SERERERFROLPrVTCmp/qeCeVDVbvgAa41nsyVEk/lNZ6UK7KqM3w30cPuc3whU0KKwkvIJqRycnLQoEEDt/b69ZIAAOcvXPB4nkqlwu23DcPXs+a4JJmsVhu2/LoV9468CyqVClarFRfKrpGbm+dyDbvdjoKCAsTG6oN1O0RERERERBRhzGnJgOhnwXIP/NkFr5yiyAptkBJPRHVBUXUXefx96AiaN2uK6Ohol/YunToCAA4dOuzxvPj4OKjVKiiU7remUimhVDr+AMDBv/4GADRoUL9SPxXi4+OQl3+xprdBREREREREEUpK9F5qxveJEmCy+bULHtHlKmQTUmvXb4BKpcKIO4Y729RqNYbfNhR79x1w7rCXnNwQLVs0d/bJy8tHQWEhbuqbBpXq0gQwnU6HtN434tix487lftt37EJubh6GDBoAURSdfYcPGwKVSoWtv2+r3ZskIiIiIiKi8GXxv66wU9nyPt3sDOeSO6JwFLJL9vYfOIjVa9fjyUkTkZiYgJOnsnDb0MFolJKCl197w9nv7ddfQc+reqBTt14AHMvtZs2eiycen4B5s7/EshWroFQqcMfwYUhObojnX3rZea7FYsF7H36Mt15/FbO+nIblP65CSnIy7h81Ejt27cb6DRvr+raJiIiIiIgoTFg7xPnuUF5mpmJ9qbJi5NoqakcRXe5CNiEFAC/9+zVMmpiNobcOQmysHoePHMVjTz6Nnbv2+DxvxpczkXX6DO4fNRITHh0DUS3i8JGjmPzsC1j/088ufZetWAWLxYoxD43GM5MnoaioGN8t/h4fTfkMdns1stlEREREREQU8SzdEmFJS/HeoWwmlHbWUSjOGj0WIycKZyGdkDKbzXjvwyl478MpXvs8NHaix/aVq9di5eq1fsVZtWYdVq1ZV60xEhEREREREVUkKQUYJnfw3anCTKjA998juvyFdEKKiIiIiIiI6HJh16tgTkuGpXdD2FIr7dpeYIZu5lEgSnXZzoTKf2WC3EOgMMKEFBEREREREVENSABK01NhHJ0KaJQe+0RNPwztslN1OzCiEMaEFBEREREREVENlKanwji2zaUi5ZUIF0qhWcFkFFFFCrkHQERERERERHS5sutVMKanOouUeyLFiZCiL//5IPrxL0E//iW5h0FhggkpIiIiIiIiomoypyUDotJrMgoAICpg7pNcd4OqJaqUplClNJV7GBQmmJAiIiIiIiIiqiYpUeNfvyT/+hFFCiakiIiIiIiIiKpJyDP51y/Xv35EkYIJKSIiIiIiIqJqEjdmA2a79w6SBJhsEDdl192giC4DTEgRERERERERVZOiyArFeaPng2WFznWzM6AostbtwIhC3OVf5p+IiIiIiIhIJpaO8bA3ifZ80GyHbnYGtLMy6nZQRJcBJqSIiIiIiIiIqqn0wVS3NtWOHIjrz0LclB1WM6NMO7fIPQQKI0xIEREREREREVWDqU0MLNc2cGkTii2I+dduKIrDJxFVzrBsrtxDoDDCGlJERERERERE1XDx3mZubZqFmWGZjCIKNs6QIiIiIiIiIgqAXa9C6d0tUHptkusBgxXa707IMqa6oEwpS8Dl5cs7EAoLTEgRERERERER+UECUJqeCuPoVECjdDuuWZwJRZGl7gdWR2LHvwgAODfpHplHQuGACSkiIiIiIiIiP5Smp8I4tg0gSR6PC0IdD4joMsYaUkRERERERERVsOtVMKanOpJRnjJPkoTSES1g13PeB5E/+EkJEnNxFiwlZ5zfx0RFQdTG+H2+TqutjWGFdGy54mpEEQAQG+P/8wmWSHut5Yot5zMGIu8587McGbH5nMM/rlyx+TM7MuJG4mdZzth8zsFji1HBcEM92BLVsDTSAaL7Mj0nQQA0SigHNId+9blaGU9FsjznskQcf2aHd+zqfJbNpXYUBhiHCakgEWOaQIxpArvVAGPuARQbDFBZA5uAVlhcXEujC93YcsQVRbVssSMxrhyx5X7GcsaOpLhyP2e+v+pGpD7nSHp/yf2M5YwdSXHlfs58f9WNcHrOVdWK8sUQDUh19BrU9WvdsGypIj9T4R27Op9lq9kQcBwmpIiIiIiIiIgqqKpWlC9CrqkWRkQUfpiQIiIiIiIiIipTZa0obyQJMNshbsquvcERhREmpIiIiIiIiCii2fUqmNOSISVqYGsa5btWlCdlySvd7Awoiqy1M8gQUDjtbbmHQGGECSkiIiIiIiKKSDWpFeXCbIdudga0szKCNrZQZDt7Uu4hUBhhQoqIiIiIiIgiUk1qRYkrsxB1wQrz2SKIm7LDemYUUW1gQoqIiIiIiIgiTk1rRUVN+QvxklbW3dfqWtSw+xxfzPxE3oFQWFDIPQAiIiIiIiKiumZOS3bUigo0GRUBtaK80fS4AZoeN8g9DAoTnCFFREREREREEaFi8XJzn4aBXyBCakUR1QUmpIiIiIiIiCis1aR4ubgyC8rTBgi5JtaKIgoiJqSIiIiIiIgorFWreHmFWlFMQhEFH2tIERERERERUdjyq3h55URVhNeKIqoLnCFFREREREREYctZvNyXyokq1ooiqnVMSBEREREREVHYkhI1fvVTb86G6lAha0X5YD17Su4hUBhhQoqIiIiIiIjClnDR7Fc/9W8XoF2RVcujubwVTXtL7iFQGGFCioiIiIiIiMKWLUXnu0NZ8XJxU3bdDIiIADAhRURERERERJcpu14Fc1oy7Cl6mM8WQdzoutTO0jEepntaer8Ai5cTyYYJKSIiIiIiIrqsSABK01NhHJ0KaJQwlLUbJneAbnYGxCWZsNzSCMYxrQGVj83lWbw8IAmvfQYAyJl0j8wjoXDAhBQRERERERGFhPIZT1KiBkKeyW3GU7nS9FQYx7ZxzHCqSFTAOLYNjA+18pyIMlqhm50BKAQWLyeSGRNSREREREREJKvKM57Klc940s7KgFDWZterYExPdS63c1H+vbJSexn1jlzo5hwL/g0QUcCYkCIiIiIiIqI6VXkmlD1Fh9LRrbzOeJJEBRTZRkiJGtiaRgGi0vOFy1VOVAGAJMFydT3Y9SrOiiIKASGdkFKr1Xh8wjgMHTIIsXo9Dh85iimfTsfWbX9Uee41vXpi3JiH0LpVKpQqJTIzT2Le/IVY/uMqr+d063ol5nw9AwBwQ9/+uHixIGj3QkREREREFOm8zYSCJHmf8SRJKB2d6jnJFAhBADRKmPskQ7siq2bXIqIa81HdTX5v/udljL5/FH5cuQbvvPsB7HY7pk75AN26XunzvLQ+N2LG1I+hVqswdfrn+PiTaSg1mfD2G6/igfs8F18TBAEvPf8MDAaDx+NERERERERUM87aT2Klv4oKgveEk69j1SAlaYJ2LSKqvpBNSHXq2AGDB/bHR1Om4r0Pp2DRkqV4eNxjOHv2LJ6Z/LjPc0eNHIELOTl4eNxj+HbBIsz/bhEeefRxnDx5CsOHDfF4zog7hyO5YUMs/n5ZbdwOERERERFRRPNZ+6kOCbkm2WIT0SUhm5Dqf3M/WK1WLFyy1NlmNpux5Ifl6HplFyQ3bOD13OjoaBQWFsFisTjbbDYb8i8WoLTU/YdPbGwsJk0cj08+m4GioqKg3gcREREREREB5rRkR+2n2kxGVa5BVfmYyQZxU3btxQ9zJcvmomTZXLmHQWEiZGtItW/XBpknT6GkpMSlff+BgwCAtm3bIPvceY/nbt+5E488lI7HJz6KH5b/CEgSBg8agI4d2uHZ5//p1n/SxEeRk5uLhYu/x/ixD/s9xsSEBCQkxLu0lRoKcDj3ABLiYqHRxfl9LY0oQhTVfvcPJrliyxU3SqcDANRLrPPQEfdayxVbzmcMRN5z5mc5MmLzOYd/XLli82d2ZMSNxM+ynLFD9TnnNE9ArRdIsUmASnCfhVX2fdKiM0hS64EgvRcj7f2l+nMnAKBeYkKdxi0XaZ8puWJX52e2yahAYYC53pBNSNWrVw8XLuS4tV/IyQUANKhf3+u502d8hSaNGmHcmAedCSaD0YinnnsRP2/c7NK3TetWGHHncEyc9DTsdntAY7zn7jsxcfxYl7bDhw5iQN/VAV2HiIiIiIgoHNmilSi6PgnW+iIK+ntf5VIT+p/OI+q8FcgxIvq3PBQMSUbeXY0haS4lpASzhMRFWUhccLpWxkBEgQvZhJRWo4G5wpK7cmaTY8mdRuO9EJ3ZYsGJk6ewdv0G/LRhIxQKBe66czjeeeNVjJ3wBPbtP+Ds++I/nsaWX7fit9+3BTzG+d8txpp1P7m0lRocO/PlFxRCZfQ/wRUbE4PC4uKAxxAMcsWWK255ljcnL7/OY0faay1XbDmfMRB5z5mf5ciIzecc/nHlis2f2ZERNxI/y3LGDoXn7HU3PV/Kl9tVntlUua3iMbMdyv/thU7SorC4GBcB4LMcxH3zN8x9kiElaSDkmiBuyoZUZEVu9W7Nq0h7fzW65QYAQM665XUat1ykfabkil2dn9lWc+BjDNmEVKnJBFHtPi1NLEtEmUzeC9H984Vn0aVzJ4y4dzSksh9ga9atx9JF8/HCc09h1OgxAICB/W9G1yu7YPhd91ZrjHn5+cjLd31Adit36SMiIiIioshi16twsX8D2BLUKM2Khj1Fh9LRrXzXdKqobEmddtZRKM4anYkke6MK1/GwBE83OwOKIisQ43o5RZEV2hVZwbtBAgBED7vP8YVMCSkKLyGbkMrJyUGDBu5TOuvXSwIAnL9wweN5KpUKt982DF/PmuNMRgGA1WrDll+34t6Rd0GlUsFqteKZyZOwZt1PsFisaJSSAgDQ6/UAgOSGDaFWqz0uGyQiIiIiIiJ4mQnV1JEw8rWbXuVjZjt0szOgnZWBimdIAAST3X2mVYX+RHR5CtmE1N+HjqDnVT0QHR3tUti8S6eOAIBDhw57PC8+Pg5qtQoKpfsGgiqVEkql44/VakVKSjKGpAzEkMED3foumj8Hfx86jLvueSBId0RERERERBT67HoVzGnJkBI1EPJMEDdmO2YheVCangrj2DbuM6Gq2klPECCuzILytMG5pM5TDAGAblYGNEsy3ZbgeRsTEV0eQjYhtXb9BjyUfj9G3DEcM+c4tpVUq9UYfttQ7N13wLnDXnJyQ+i0Whw/kQkAyMvLR0FhIW7qm4ZPps6A1er4IaXT6ZDW+0YcO3bcudzviaeec4s7aGB/DBpwC17816ted/EjIiIiIiIKN97qPhkmd4BudgbEJZmwVEhUqXbkwJjuYTmdn5SnDdD5OcOJS/CIwk/IJqT2HziI1WvX48lJE5GYmICTp7Jw29DBaJSSgpdfe8PZ7+3XX0HPq3qgU7deAAC73Y5Zs+fiiccnYN7sL7FsxSoolQrcMXwYkpMb4vmXXnaeu6HSjnsA0K5tGwDAL7/+hosXC2r5LomIiIiIiEKD19lOogLGsW1gfKgVoKqwEsVuBxTuK1P8JeR6rwtMROEvZBNSAPDSv1/DpInZGHrrIMTG6nH4yFE89uTT2Llrj8/zZnw5E1mnz+D+USMx4dExENUiDh85isnPvoD1P/1cN4MnIiIiIiK6TNj1KhjTUz3Pdir/XlmpvbrJqLLd8cRN2dU7n4jCQkgnpMxmM977cAre+3CK1z4PjZ3osX3l6rVYuXptwDGnTv8CU6d/EfB5REREREREl5OKtaJsTaMAUen7hGosy3NTeXc8IopYIZ2QIiIiIiIiouDyVisq+IHKlv552U2PLj/5r0yQewgURpiQIiIiIiIiiiBea0XVROWlfmXfa2cdheKskbvjEZEbJqSIiIiIiIgihM9aUTVhlQC155lQQYxCRGGECSkiIiIiIqIwFnCtqMp8Ja/KCpTH3r8Zut7NYE0QYcy6yJlQYUo//iUAQM5bz8k8EgoHTEgRERERERHVgYqJISHPBHFj7SZtglYryiYBKsHrsjzd7AyozpYibt0FAEBOXn7NBk4hS5XSVO4hUBhhQoqIiIiIiKgavCWYytvtKXqYzxZBvTEb5juauyWGDJM7+FzWVtMEVk1qRYkrs6A8bYCQa4J6k+fxs0A5EdUEE1JEREREREQB8DbzyDC5A1T782HtnABolDCUH3imI6BSuCeGRIUjYQRAVyGp4+v6utkZEJdkwlJFoqrataLKluBFTfnL5Zq6WRnQLMmEuU8yC5QTUVAwIUVERERERBQArzOPRAWsV9Vzb1eWJYQqJ4YExzI44+hUwGAFolQQ8kywp+hQOrqV1wSW8aFWjgRXGU8zrcxpydWuFaWbneEx0aQoskK7IiuwaxIRecGEFBERERERkZ98zjwSfCSevBEEQKOE8ckOl9okyff1lZXaPcy0khI1/tyOKy7BI6I6xIQUERERERGRn6o18yhQVS2x8zHTSrMk07Hmz+jfUrqKtaK4BI+I6hITUkREREREFDFqWii8WjOP6kLZTCtzn2RIGy/CcnV93/291Ioi8sW0c4vcQ6AwwoQUERERERGFvaoKhXvb6a4yIc9Ua2MMBst1DXDupsaw9kz03qmKWlFE3hiWzZV7CBRGmJAiIiIiIqKw56sQuaed7rxR7c33vXNddY8FiaV3Q1iq6sRaUUQUApiQIiIiIiKisFZlIXIPO915W8pnHti46oRT5Tjl39skQFWDhFR1E1oFJkR9cRRSrJq1oqhGlCnNHF/k5cs7EAoLTEgREREREVFYq7IQuYed7jwt5ZN0Sphub+YjkB2q/fmwdk5wWRZYPiNJXJIJS59kSEkaoNgC48R2gKjwnGQqn8lV8Vh5QiuQxJQkAToVxPVnmISiGosd/yIA4Nyke2QeCYUDJqSIiIiIiOiy5a1IeXm7PUUPc+uowC/sYSmf6dYmkPRql27KI4UQN2a7zDyy61Uw90mGmKKH+WyRy4wk5YqsSyfHqC8tI/Qwo0o76ygUZ42QkjQQck1Qb8qG+Y7mbnWwfBIEQHQUO9dWjE1EJDMmpIiIiIiI6LLjq0h5xVlKhuoGqLCUT7MkE4LBhtK7W7h1i/rwT6j3ui5fUhRZoV2RhdiYGBQWF3sNUV7DyS3BVKHGU+V5ULpZGdAsyXTsppekgbVtHCy9G1Z5O1JSiO4OSEQRiwmpIDEXZ8FScsb5fUxUFERtjN/n67Ta2hhWSMeWK65GFAEAsTH+P59gibTXWq7Ycj5jIPKeMz/LkRGbzzn848oVmz+zIyNubTzni6Oawji6hcci5dar6rm3V0fZUj7lgOZQlNhgb+Q600o8VITEDAsEL/flz+sdt/gcbGtyYbghCbYEEcp8M6K25EJZbAW8vV4SgI0XAQBFAwXk+pGQiioB9LX8OePP7AiIXTaTjz+zwzt2dT7L5lI7CgOMw4RUkIgxTSDGNIHdaoAx9wCKDQaorIqAruHrX09qm1yx5YgrimrZYkdiXDliy/2M5YwdSXHlfs58f9WNSH3OkfT+kvsZyxk7kuIG+znb9SpcvKep9yLlFf8bBMUJCpjvbuLWrp57FEVV3JNf91wMYNFFKODINZUEMDb7mhPAhCt816Iy22Fbk4nC4tqtISX35znS4soRu2FZojeS7lnuuHLErs5n2WoOfD5qYBkTIiIiIiIimTmLlAcx6eSL6YFUSJVmRwmFZqg3nauT+L4oiqzQzc64tLtfRWUJO93sDBY0J6KQwxlSREREREQU8pxFypM0MN/SqEbXUm/OhupQYdU73ZVTuB+TYkWY7r/CWfBcTv7UoiIiCjVMSBERERERUcjyVry8JtS/Xbi045y3ne6qHNilgudyzz4S4Frs3NPufkTBUDjtbbmHQGGECSkiIiIiIgpZpemplxJG/vKWXCqrpyRuynY2eZ1dVFWCqqzgublP8qXklsz83d2PqLpsZ0/KPQQKI0xIERERERFRSLLrVTCmp/pODlU+Vv69l/bK9ZQqzy6SkjQQck2wtdbDdGeLKscoJWmqd3NERBGOCSkiIiIiIgpJzuLlvlROVJntUO3Ph7VzQkD1lMpnF5UrHeq+q57H8Lkmv/oRhYOoYfc5vpj5ibwDobDAhBQREREREYUkKdG/2UflRcqFXJOzbpJdr6pRPSVxYzYMkzt4L3juYfkfUbjT9LjB8QUTUhQETEgREREREVFIEvL8m33kUqS8TE3rKSmKrNDNzvBc8NzL8j8iIvIfE1JERERERBSSFDlVJKRqeZaS14LnVSz/IyKiqjEhRUREREREIUdSCjCOb+ujQ+3PUvJW8DzQ5X9EROSOCSkiIiIiIgo5ptubwZaq996hDmcpVS54TkRENceEFBERERERhQy7XgXT4CYwjmvjdkxcfRrKUyWcpUREFAaYkCIiIiIiItlJAErTU93rNZVRHCpA9Fv7INjrfmxE5GA9e0ruIVAYYUKKiIiIiIhkV5qeemlHOw9UfxcwGUUks6Jpb8k9BAojCrkHQEREREREkc2uV8GYnuosVO5GkmAe2Bh2Pf89nYgoXDAhRUREREREsjKnJQOi0nMyCnC0a5Qw90mu24EREVGt4T8xEBERERGRrKREjX/9kvzrR0S1I+G1zwAAOZPukXkkFA44Q4qIiIiIiGQl5Jn865frXz8iIgp9TEgREREREZGsxI3ZgN1zMXMAjtpSJhvETdl1NygiIqpVIb1kT61W4/EJ4zB0yCDE6vU4fOQopnw6HVu3/VHludf06olxYx5C61apUKqUyMw8iXnzF2L5j6ucfZIbNsDttw1F7xuvR7NmTWG32XE0IwPTv/gav2/bXpu3RkREREREZWwt9YDCS/2oskLnutkZUBRZ63ZgRERUa0J6htSb/3kZo+8fhR9XrsE7734Au92OqVM+QLeuV/o8L63PjZgx9WOo1SpMnf45Pv5kGkpNJrz9xqt44L5La137pvXGww+OxslTWZjy6XRM//wrREVF44tpn2D4sCG1fXtEREQUgex6FUqHNoExPRWlQ5tw1zAiAKXpqd4Pmu3QfX4Y2lkZdTcgIiKqdSH7G1Cnjh0weGB//O/9jzFzzlwAwLIVK7F04Tw8M/lx3P/gWK/njho5AhdycvDwuMdgsVgAAAsXf4/lSxZg+LAhmDN3PgDgj+07ccvgYbh4scB57oJFS7B4/hw8PmEcli5bUYt3SERERJFEguMv3cbRqYBG6Ww3TO4A3ewMaGdlwMv8EK/sehXMacmQEjUQ8kwQN2b7nEFS2/2JqsPaNhaWXvVdG41WaOcfh+JcKcRNfN8REYWjkE1I9b+5H6xWKxYuWepsM5vNWPLDckyeNBHJDRsg+9x5j+dGR0ejsLDImYwCAJvNhvwKiScAyDh23O1ci8WCzb/+hgcfuA9RUVEwGAzBuSEiIiKKaKXpqTCObeNYflSRqHC0A9D5OQMk0ORWbfcnqgnjaPfZUdpvjyPqq6MyjIaIiOpKyCak2rdrg8yTp1BSUuLSvv/AQQBA27ZtvCaktu/ciUceSsfjEx/FD8t/BCQJgwcNQMcO7fDs8/+sMna9pCQYjEaUlpb67JeYkICEhHiXtlJDAQ7nHkBCXCw0urgqY5XTiCJEUe13/2CSK7ZccaN0OgBAvcQ6Dx1xr7VcseV8xkDkPWd+liMjNp9zzdiilchPb+WsheNCEABJQunoVmi0oQBRFmWVcXNHNobxvqZek1tRUTokLTgdcP/yew70+jXBn9mREdfbczY11SGvT7JLm2CwofH6i1AmJgQlNn9m151w+Zl9OcSVK7Z19UIAQL0gfT4DxedcN6rzWTYZFSgMcN+JkE1I1atXDxcu5Li1X8jJBQA0qF/f7Vi56TO+QpNGjTBuzIMYP/ZhAIDBaMRTz72Inzdu9hm3adMmuLlfGtau3wC73e6z7z1334mJ412XDh4+dBAD+q72eR4RERFFlqLrkyCJPkp3CgIkjYDi6xIRtanAez84klt5Ixr7TG7l3dUY8SuyoSyxBdQflsCvT4GxRStRdH0SUE8L5JRC/2tuxL6Otmglsp+4wq09fvU5KIsj8zUhCnW2PVvlHgKFkZBNSGk1GpgrLLkrZzaZAAAajcbruWaLBSdOnsLa9Rvw04aNUCgUuOvO4XjnjVcxdsIT2Lf/gOeYWg3e/+9bMJlM+ODjT6sc4/zvFmPNup9c2koNjl8i8wsKoTL6TmhVFBsTg8LiYr/7B5NcseWKW57lzcnLr/PYkfZayxVbzmcMRN5z5mc5MmLzOdeMUePfPzHmXhkDU5wS5rNFXus1ld7Y1K/k1qnRKVBmGWBN1fvV/+yVWjTYeBHn+8b73V+7Isuv+/IlkGdcGzWt6uq97W0Z5Pmxzet0GWRt3q+v51PxOTtfi/RUQFS6XsRqh33mX8jJMwdtXPyZXXfC5Wf25RBXrtj8PbvuXS7P2WoOfIwhm5AqNZkgqt2npYlliShTWWLKk3++8Cy6dO6EEfeOhlQ21XzNuvVYumg+XnjuKYwaPcbtHIVCgXffeQOpV7TE+Mcne5ydVVlefj7y8l0fkN3KmlNERETkSsjz/ntLRdbeDXGxt+Pr8npN4pJMWMr+ko8SK0rvbuHXtcyDmgQ0RinJ8TuWPdH7P/p56l9RbRVBvxxrWlV+LewpOpSObhWUGmKhxp/nU5HXemoAoFLAPLTpZftaEIU7sccNji/WLZd3IBQWQjYhlZOTgwYNGri116+XBAA4f+GCx/NUKhVuv20Yvp41x5mMAgCr1YYtv27FvSPvgkqlgtXq+svRa/9+CX1uvAHP//MV/LF9ZxDvhIiIiCKduDEbhskdAFHhvgzO60mORIXxoVaAyseMpSARch1JM3ujKL/625pGw5ieCiHPBPXGbJjvaF5rCaNgFoSvbd6SM5Akn8sgjaNToVmS6XcCL5R2QPTn+WB5HgDHuI3pqZ5fC6BarwUR1Z3oYfc5vmBCioIgZBNSfx86gp5X9UB0dLRLYfMunToCAA4dOuzxvPj4OKjVKiiU7r+4qVRKKJWOPxUTUs9MnoTbhw/FO/99H6tWrw3ynRAREVGkUxRZod6WA0vvhv6fVP6XdWUdzP2x2SFuyoa5SQLM/ZKr7g/APLDxpW+e6ehImgUpYVQx2YISi/cERjWTObXJa3LGVyJSEACNEuY+yVUugwy12WI+E0wVno9tQwGUJTaY05Ldl+lVPsfP14KIiC5vtf/PbdW0dv0GqFQqjLhjuLNNrVZj+G1DsXffAecOe8nJDdGyRXNnn7y8fBQUFuKmvmlQqS7l23Q6HdJ634hjx467LPd7aPT9eCj9fsz44mt88+2CWr8vIiIiikz2mGr+O6CvRIanJU/VoVTAMKk9sv/bGdBVY5zlSTMfCQm7vurrSgCM6am4uLQfDM93dswQm9zRkcDw9jqUJTAMT7SHMT0VpUOb+BWrNlQ5+6cKlusaVHkPzoRX5TpfZcm/0vTU6gy92pwJpiqeT/F1joIkNVkSSkRE4SVkZ0jtP3AQq9eux5OTJiIxMQEnT2XhtqGD0SglBS+/9oaz39uvv4KeV/VAp269AAB2ux2zZs/FE49PwLzZX2LZilVQKhW4Y/gwJCc3xPMvvew896a+ffDMU5NwIvMkjh0/gSGDB7qMYevvfyA3L69ubpiIiIjClq2hFrbuSS5tQoEZqr35gc2acruwBKiCMx/GPNhDzSl/Eyt+zP4xPNEeyiyDz+VlPmsLVaFizSy5ZgtVOfunCpbeDZ3vB0/34O9spLqcLWZvqPOrnzVBdPRv7N+S0PIlpEREFL5CNiEFAC/9+zVMmpiNobcOQmysHoePHMVjTz6Nnbv2+DxvxpczkXX6DO4fNRITHh0DUS3i8JGjmPzsC1j/08/Ofm3btAYAtGjeDO+8+ZrbdR56ZAITUkRERFRj5lsaubVpF2ZCyC2tUUJKO/cYFGeNkJI0sDWJ8quQubgyC4o8M0qHNwVi3DeQqUi5Ow+atWcgJWlgTdXD0i+l2mP1ljAqV9PZRS5kqi3lb3LGLx7uIdSWu0lKAZYb3Gu+eqLKN8PUIgrmm6p4D0kSYHYsISUiovAW0gkps9mM9z6cgvc+nOK1z0NjJ3psX7l6LVZWUQ9q6vQvMHX6FzUaIxEREZEvEgDzAPeElLj2DIQic+DFzitQnDU6Ew92vQrmfiner1X2F/2oKX9BUWSFak8uit+9yntcSYKtYzzEf+6CosgKY3pqjRJSLjwUu67p7CIXtTBbyFsR8fJ2e7IOpmFNgzD4MhXuAQYrEKWCuVc9v041902GlFR7xc7L79l0WzPYWsVWfYIkoaRzHC6MbuZa6N1DPwgCdLMzQqIeGBER1a6QTkgRERERXe5sbWJha6l3aVPty4fyjAEAoJudcWmZWsXkkK+ZQh5mkSiKrFVeq+Jf9O31tQEV2hbygriEykOxa8nP2kIBxQjCbCFfRcRV+/Nh7ZzgO8nicjHp0tj8UXYPxic7BDRma6/6sPaq7xxnsJYvet1BsCqCgOI0D8m0yu9Ts91t5hwREYUvJqSIiIiIapGpv+fZUeXK//Lt9pf88vpQfiSXqryWh7/o+5sAKi8uLW7Mrno2VyDL7SoUu45bdyG4Ca+KQ6phcWyvda1EBaxX1fO/3lXZa6OddRSKs0aIKXqUtNTVrIaYP2qwfLHyrDB7ig6lo1t5v+cAl1sqd+RC89NZx2yuXBPETcGfzUVEwZX/ygS5h0BhhAkpIiIioloiKQX3+lEWO8QNZ53fCnAkCjRLMmHukwwxRQ/z2SKoN2XDfEdzv5JL3q7l6y/6/iaAyotL+zMDqzpF1suLXQuGKhIRZbPCdJ8dAqJVftfMqklx7CqLiFf8r6fxepn9IwCIjYmBuW987SekKi1fhB/5M68zoSTJe9Kp8vOpp4F5eDPfS0K7JEB8eTeTUEREEYoJKSIiIqJaYumR5DZDR731PBSFFre+iiIrtCuyEBsTg8LiYgD+J5e8XcuXKmc8eVgWWNUMLHFJJixlY/U3YaTKN8OmU8I4sZ33ThVmhekWZQLwo2YWAFhrVhy7RnWtBAHiyiwoTxu8Pje/Zp0FQ4Xli9h4scruXmeF+bHEUyi1QbsoE6VDm8AcwJJQIiKKPExIEREREdUCu14F45hWbu2aNWc89PbOn+RSdQRacwrwbwaW0t8i62UMnWJxcUAD3zvUeZgV5nP85ewSoFL494J4YK+vrfa5AKA8bfC5TM6ve/BBvTkbqkOFsLaN82umlT/LF2u622F5jECXhBLR5UE//iUAQM5bz8k8EgoHTEgRERERBZHPws8mG1S/XZBlXJ4EUnOqIn+SZP4mW4r61vfYrvk+E4ock89ZYV7HX05UwjiuDaL/74DPsXoiKQVYbqzZcjp/lgtWeQ8+qH+7AO2KLJQObeJXQsqf8dR0t8PyGIEuCSWiy4MqJYi7iVLEY0KKiIiIKIi8LncCAI0SplEtAy4uXVu81a8KVnHp6iZbFMeLEP3en1X2qzx+W7s4mIY3c+ljurUJNEtPQnWo0K/Ydr0K5r7JMN3eHLbWsb47V1FPyZ/lgp5mnaHY4ljC6OdySr+W/tmlsv6+Z31ZuyVWOWaPAh1TAK8RERGFJyakiIiIiIKkyuVOFYpLh1IhZ0/1q4LBU7LFFi/CfFdzn8Wu7Y2iYNer/H6NnDO2VmRBEhUwD65Qu0ohoPi1rhBXnYYizwRxoyPZVr6DnL28iPxGL0XkvSl/xgHsgujXPZSLUfu9nNKv2WgKAdb28cCfpW6Hyl8LW9s4xzLLQAU6pmq+RkREFF6YkCIiIiIKkiqXO0VoIeeKyZbaLnYdNf2w4zlEXfo1194kGqVj2wAADJM7QLU/H9bOCYBGCUN5p2c6OupNeZrZBnjcNa/idSq2+1ru6K9Al1P6MxvN+Fg7SJP2XLol+Fhe6k356+NlB8Ga3AMREUUWJqSIiIiIgsTuZ4HmSC7kXNvFrhW5JuhmZcA4oa3nDqIC1qvquSeeyouf+1hepvvsEBCtcqlrZderAt4F0R/+FJD31d/eUIvSES2AGLWzjy1Vj/P/6gDhlzMQN2bDdEdz78tLy+/bw8wm7ayjUJw1BjQmzeArYE0QYcy6GLTXiIiILm9MSBERERHVgHPpV6IG5v6N/Donkgs510Wxa3FlFozj23hOLpW3BbKDXNmsLaHUBu2iTJdDtbULYnWvX7G/4oIJhn90cjluvC4JuC7JUd9J6WHJoS8VZjYFsv+eosiKuHWOYv45efkBnElEROGMCSkiIiKiaqj2cqcIL+RcF8WuLTc0CCzh5KfLbWab5scsGB9p7XlWmq8C6OUEAeLKLChPG4I6+4uILl+mnVvkHgKFESakiIiIiKrB5256nrCQM4C6KXbt77LAQF1uM9ukKCUkvdrzQT8TdsrThpDZFZKI5GdYNlfuIVAYYUKKiIiIKEBV7qYHeCyCzULODrVd7NrfZYF+u0xntpnTkgG1okbXuNyScEREdPlgQoqIiIgoQFXupgdwuZMPtV3s2q9lgf4mEi/jmW01mil2mSbhiKh2KVOaOb5gPTgKAiakiIiIiALk71/0udzJt9oqdu3PskCv7TYJUIXHzDa/Z4qFURKOiGpX7PgXAQDnJt0j80goHDAhRURERBSgutgpjmrG17JA1f58WDsneFwuKC7JhKVPMqQkzWU/s82vmWK2sj+1sHSSiIjIFyakiIiIiAIkbsyG4dlOgNLHsi8ud5JVxWWBZg8JJrteBXOfZIgpepjPFrkknpQrsuQdfJD4VUD+6yNeXyMiIqLaxIRUkJiLs2ApOeP8PiYqCqI2xu/zdVptbQwrpGPLFVcjigCA2Bj/n0+wRNprLVdsOZ8xEHnPmZ/lyIjN5+zKlBqDi76SUYKA+AWnEC9pgQCHHmnvr1p/xhKAjRcrNJQ9k7J2nbYUxtLSS+11pK5ea/3icyjQiCgY2RRShVlQgtmOuAWnELf4HARoPb9GQRSqn+Vwjc2f2eEfV7bYZYlt/p4d3rGr81k2l9pRGGAcJqSCRIxpAjGmCexWA4y5B1BsMEBlDWxXk8Li4loaXejGliOuKKplix2JceWILfczljN2JMWV+znz/VU3QvU5F93b1vtJZcudhFkZAf9iVlXcusCf2eEXVzHjL8R9e8TjjLCiOhmB/M+Z76+6EanPOZLeXw0lSZa4FfE5177qfJatZkPAcZiQIiIiIgqAtV0cLNc3cG002aBdcAKKMwYud6KQpCiyQrsiC7ExMbL+pYqIiKgcE1JEREREATA+3MqtTbvgBKJmHJZhNERERESXJyakiIiIiPxg16tQem9LWK6rNDuqxArt/OPyDIqIiKgOFU57W+4hUBhhQoqIiIjIBwlAaXoqjKNTgQpFoctpFp2AotBS9wMjIiKqY7azJ+UeAoURJqSIiIiIfChNT4VxbBvH7nkeCIKX3faIiIiIyCsmpIiIiCis2fUqmNOSISVqIOSZIG50FB2vqt2eoocprwTG9FRHMspT4kmSUDqyBbTfHmMhcyIiCntRw+5zfDHzE3kHQmGBCSkiIiIKS96W2hkmd4Bqfz6snRN8tvu1ebEgABolzH2SoV2RFfR7ICIiCiWaHjc4vmBCioKACSkiIiIKS16X2okKWK+q53+7H6QkTQ1GSkRERBR5FHIPgIiIiCjY7HqV96V25d/72+4HIddUjVESERERRS7OkCIiIqKwY05LBkT3HfGCTpIAsx3ipuzaj0VEREQURjhDioiIiMKOlFgHS+jKZl/pZmewoDkRERFRgGo0Q0qhUMButwdrLERERESXD7MdutkZ0M7KkHskRERERJedGiWkNq5fibXrfsKqNeuwc9eeIA2JiIiIqPokAOarkqro5KG2VFXtZjt0nx0ColUQck0QN2VzZhQREUUU69lTcg+BwkiNElLxcXEYceftGHHn7bhwIQer1qzDytVr8dffh4I1PiIiIqKAmG9Kga2bj4RUedKpcvKpinbd7AzoFmXW3sCJiIhCXNG0t+QeAoWRGtWQulhQAEEQIAgCGjSoj9H334v533yN5d9/hwnjxqBF82bBGicRERFRlezRKhgmtfPdyWyHakcOYLb73a77/DCX5hEREREFUY1mSPW5aRC6XtkFffvciLTeN6BFi+YAgObNmmL8uDEYP24MDh0+gh9XrcHK1Wtx4UJOUAZNRERE4c+uV+Fi/wawJahRmhUNcaPvJXJ2vQrFr3eDVE/r0i6cNUA37xikWNFlqZ1dr4K5TzKkJI3HdjFFD/PZIi7NIyIiIqoFNUpISZKE3Xv2YveevXj/o0/QtGkT9EvrjcED+6N9u7YAgLZtWqNtm9aYPGkilixdhv/734cwm81BGTwRERFd3ux6FcxpyZASNRDyTBA3ZkMosqI0PRXG0amARlnWsykMkzs4i4hXrPIkAY7+6amAqHSLEfXBn9D8dsGtXVFkhXZFltf22JgYFBYXB+dGiYiIwkDCa58BAHIm3SPzSCgc1CghVZFCoUDL5s3RsUN7tGzRHJIkAQCEshoMSqUSd90xHHa7hDffeTdYYYmIiOgy5EwiuSSdAMPkDlDtz4f1qnqO2k0ViQoYx7YBAOgqLJ8rTU91tFfuX8beOhbwkJAiIiIiIvnUqIYUAHS9sjP++cJz+Hndj5jy4bsYcMtN0Gq1EAQBuXl5+GrmHDz4yASsXrsegiCg/839gjFuIiIiuow5k0hipV9FRMWlZFTl3e7KCo4bR6fCrnf8m5pdr3LMjPKxO17F/kREREQUGmr029mq5UvQKCUZwKWZUDabDZu3/IYlS5fhly2/wW53FAY9cSITA/vfjPj4OL+vr1ar8fiEcRg6ZBBi9XocPnIUUz6djq3b/qjy3Gt69cS4MQ+hdatUKFVKZGaexLz5C7H8x1Vufe8YPhQPPnAfGjduhOxz5zH32wWYN3+h3+MkIiIi3youzUOJxXsSqfx7T8ml8naNEuY+ydCuyII5LdnjMj1v/YmIiIgoNNQoIdW4UYrz6xOZJ/H9D8uxbPlK5OblufUtLinBzl27vc2m9+jN/7yMW27qh2/mzUfmyVMYPuxWTJ3yAR4eNxG79+z1el5anxvx8fv/xd59+zF1+ueQJGBA/5vw9huvIj4+DnPmznf2HXHn7XjlXy9g7foNmPXNt+jRvSteev5ZaLVafDVzjv+DJSIiIjfelubV+LpJGgCAPVkXUH8iIiIiCg01SkiVmkxYs/YnLFm6zGeCCADMZjMeGjvR72t36tgBgwf2x//e/xgz58wFACxbsRJLF87DM5Mfx/0PjvV67qiRI3AhJwcPj3sMFosFALBw8fdYvmQBhg8b4kxIaTQaPPHYeGzavAVPP/ciAGDx9z9AIQgYP/ZhLFq8FIVFRX6PmYiIiFxVVd+puoRcEyQAll71/O5PRERERKGjRjWk+tw0CP9+9fUqk1HV0f/mfrBarVi4ZKmzzWw2Y8kPy9H1yi5IbtjA67nR0dEoLCxyJqMAx1LC/IsFKC299Avp1T17ICEhHvMXLnY5/9vvFiEqKgq9b7w+eDdEREQUYaqs71RdNjvETdkwDW8GW7t4330lCTDZIG7KDl58IiIiIqqxGs2Q6tSxA3p07wqj0YhZc+a5HEt/YBR0Oh127tqD7Tt2Bnzt9u3aIPPkKZSUlLi07z9wEADQtm0bZJ877/Hc7Tt34pGH0vH4xEfxw/IfAUnC4EED0LFDOzz7/D8vxWjr2Knn4MG/XM4/+OffsNlsaNeuDVasXO11jIkJCUhIiHdpKzUU4HDuASTExUKj879elkYUIYpqv/sHk1yx5YobpXMs76iXWOehI+61liu2nM8YiLznzM9yZMSuTtyL/Rv4ru/kTXkCy1siS6mA+T9XwdCtiv/Plp2ftOgMktR6IMD3yuX0Wl/OsfkzOzLi8md2ZMSNxOccae8v62pHreV6iQl1Grccn3PdqM5n2WRUoDDAf/+rUULq0UceQs+rumP2N9+6HUuIj8dD6ffjjx07q5WQqlevHi5cyHFrv5CTCwBoUL++13Onz/gKTRo1wrgxD2L82IcBAAajEU899yJ+3rjZJYbVakVefr7L+VarFRcLCnzGAIB77r4TE8e7Lh08fOggBvT1nsQiIiKKFLaEavzyVJZE0u0tQGk7PSSN55lVhp4efhGulMASzBISF2UhccHpwMdBREREbmx7tso9BAojNUpItW6dCgDYvmOX27Fdu/fi4QcfQJvWrap1ba1GA3OFJXflzCbHkjuNxntxUrPFghMnT2Ht+g34acNGKBQK3HXncLzzxqsYO+EJ7Nt/wBFDq4HFYvV8DZPZZwwAmP/dYqxZ95NLW6mhAACQX1AIldHu8/yKYmNiUFhc7Hf/YJIrtlxxy7O8OXn5vjvWgkh7reWKLeczBiLvOfOzHBmxK8etuGuekGeCuDEbiiLX/6eWZkUDaBpYILMdutkZ0M7KgEavgmbwFTA10uLiwAZAtO8El/hjFlQHLkJK0kDINUHclA2pyIrcwEbgFCqvdbjH5s/syIjLn9mRETcSn3Okvb/4M7vuXS7P2WoOfIw1SkjFREcDADRa98SNqBFd+gSq1GSCqHb/xVMsSxKZTN6Lk/7zhWfRpXMnjLh3NKSyIqpr1q3H0kXz8cJzT2HU6DGOGKUmqNWeXwJRI/qMAQB5+flus6vsVoPPc4iIiC5n3nbNM0zu4Ewklc9REjdmw/BsJ0DppX6UJDkSUJ8dAqJVziRSeWJLKLIibt0FAIB121kU/18P77WoJAnmWxoh6tO/3RJjREREFBxijxscX6xbLu9AKCzUqKh5Tq7j3xzvvfsuqFSXfilVKpUYNXIEACA3L696187JQf367jvn1K+XBAA4f+GCx/NUKhVuv20YNv/yqzMZBQBWqw1bft2Kjh3aQ6VSOWOoVCokJiS4XSM+Ls5rDCIionBj16tQOrQJjOmpKB3aBHa953+wce6aJ1b6FUJUwDi2DUrTU51NtlaxvpNRggDd7AzoFmVCNysD2hVZXpNJ9iSN78LoggBolDD3SfZ5n0RERFR90cPuQ/Sw++QeBoWJGs2Q2r5jF4YNGYwe3bti2eIF+P2P7QCAa67uicaNG0GSJPyxPfD6UQDw96Ej6HlVD0RHR7sUNu/SqSMA4NChwx7Pi4+Pg1qtgkLpnmtTqZRQKh1/rFYr/j50BADQsWN7/LLlN2e/Th3bQ6lU4lDZcSIionAVyIwnn7vmlRUhN45OhWZJJoRSO0qe7eg9cIWleX6NM9H3MnpnvyT/+hERERGRvGo0Q+rLr2ejtGxZW+PGjXDn7bfhzttvQ+PGjSAIAsxmC76aOada1167fgNUKhVG3DHc2aZWqzH8tqHYu++Ac4e95OSGaNmiubNPXl4+CgoLcVPfNOdMKADQ6XRI630jjh077lyKt237Dly8WICRd93hEnvkXXfCYDRi0y+/VmvsREREl4uqZjwZx7ZG6dAmuDiqKQxPtnfsmudtplLZLCXDE+1R9G4P2JvHuB43WqGdk4God/YjfvgG6Coku6oi5PleRu/sl+tfPyIiIiKSV41mSB0/kYnJzzyPt/7zChIrbfuYl5ePf77yHxw7fqJa195/4CBWr12PJydNRGJiAk6eysJtQwejUUoKXn7tDWe/t19/BT2v6oFO3XoBAOx2O2bNnosnHp+AebO/xLIVq6BUKnDH8GFITm6I51962XmuyWTClKnT8e+X/oH3/vsWftv6O7p364qhQwbhoylTUVhYWK2xExERXQ78mfFUOjoVEAQEUiHRPKiJx/aoj/6CdkVWtcYqbsyGYXIHR+LMU0KsrB6VuCnA/YaJiIiISBY1SkgBwG9bt2HAkNtx3bW90KJZMwDAiZMn8dvWbVUWBa/KS/9+DZMmZmPorYMQG6vH4SNH8diTT2Pnrj0+z5vx5UxknT6D+0eNxIRHx0BUizh85CgmP/sC1v/0s0vfBQsXw2q1Iv2BUejb50ZkZ5/DO+9+gG/mza/R2ImIiEKdOS3ZMePJG181mwKkyDZA82P1klEAoCiyQjc7wzGbq3ICrUI9KhY0JyIiIro81DghBThmGv28cXMwLuXCbDbjvQ+n4L0Pp3jt89DYiR7bV65ei5Wr1/oVZ/H3P2Dx9z9Ua4xERESXK3/rMtU8kAR7ogZSjApCDRJG5fWmKte7CrQeFRERERHJLygJqS6dO6Fjh3bQ6/VQKNzLUk2b8WUwwhAREVEQ+VuXqeaBBEB07IBX3SV7ACAA0M3KgGZJJsx9kiElaSDkmiBuyubMKCIiIqLLTI0SUhqNBp989D9cfVUPn/2YkCIiIgo9zrpMGh/L9oIoWDvgKYqsNUpsERERUfXkvzJB7iFQGKnRLntjxzyIXj2vgiAIbn8AOP9LREREoUdRZIV6y7lqny+uzILu88MQV/mXHOIOeERERERUrkYzpG7ulwZJkrDl16248YbrIEkSvp79DfQxMbhj+DDs3X8AS1ibiYiIKGR5rekkSWUdvO9oFzXlLyiKrLDrVTD3S+EOeERERETktxrNkGrUKAUA8N2iJc62jZt+wetv/RfTPv8K3a7sAlGso4KpREREFDBrjyTXBrMNUf89AO2cDEdyqTwxVc7DjnblO+D525+IiIguT/rxL0E//iW5h0FhokYJKQGOfwUtKi6B1er4JTMuLg4AsG/fAQiCgPQHRtVwiERERFQb7PU0sDeNdmlT78yFdtkp6GYcge7zw4DZ7nqS2Q7d54fddrTTzsoIqD8RERFdflQpTaFKaSr3MChM1GjJXkFBAerXrwetVoPc3Dw0aFAfYx4cDbvdjvvuuRsA0LBB/aAMlIiIiILL0j3JrU21Kw+A+452Yooe5rNFXne04w54RERERBSIGiWkTp7KQv369RAfF4dde/Zi0IBbcGWXTvjkw/8BACRJwuGj/BdRIiKiUGTpnujWpt6V6/J9+Y52sTExKCwurvKa3AGPiIiIiPxRoyV7v279HZmZJ5GQEI/pX3yNkhKDy057JpMJ7773YZCGSkRERMFkrTRDSiiyQHmkUKbREBEREVEkqdEMqS+/no0vv57t/P72u0fhtqG3omGD+jhzNhsrflyF7HPnazxIIiKiSGfXq2BOS4a9fOncxpothbMl62BvFOXSptqbB8Hu5QQiIiIioiCqdkJKq9XgwdH3AwB27tqD7Tt2Ijv7HKZ//lXQBkdERBTpJACl6akwjk4FNEoYytoNkztANzsD2lkZZVuMBMbqcbleXk2GSkRERETkt2onpEpLTRj7cDpUKhWeePofwRwTERERlSlNT4VxbBtAklwPiApHOxzFxAPluaB5roeeRERERA6mnVvkHgKFkRrVkDp+IhMAoFbVaOUfEREReWDXq2BMT3Uko4RK86AEAZAkGEenwq4P7P/DEgBrN9cZUkKBGcqMohqOmIiIiMKZYdlcGJbNlXsYFCZqlJD6bPoXAIAH0+9HTEx0UAZEREREDua0ZEBUuiejygkCoFHC3Cc5oOvaG0fB3lDn0qbanQdB8nICEREREVGQ1WhqU1qf3jhz5iy6dOqIdSuXYffefcjNzXNZViBBwsuvvVnjgRIREUUaewNd1Z0ASEmagK5r8Vg/isv1iIiIyDdlSjPHF3n58g6EwkKNElK3DR0MSZIgSRKio6Nw/bW9PPZjQoqIiCgwkgBYrq/vV18h1xTQta09PNSP2s2C5kRERORb7PgXAQDnJt0j80goHNS4+JNQYRmB4GFJgVS5CCsRERF5ZderYE5LhnlgY9jaxPlxggR7vBrG9FQIeSaIG7OhKLJ67S4BsHRzTUgJeSYojxfXcORERERERP6rUULq4XETgzWOy565OAuWkjPO72OioiBqY/w+X6fV1sawQjq2XHE1oggAiI3x//kES6S91nLFlvMZA5H3nPlZDg4JQMGopigY2RSSRun/iQoBpePbOb81Tu6AuAWnEDfvFDxVnjI31SG/0hI/3f5CxFXxOvI5h39cuWLzZ3ZkxI3Ez7Kcsfmcwz+ubLHLJqHwZ3Z4x67OZ9lcakdhgHFqlJDasXN3TU4PK2JME4gxTWC3GmDMPYBigwEqa2A14wuL5fvXabliyxFXFNWyxY7EuHLElvsZyxm7LuOWz+QRU/Qwny2qcmZOsMn9nIMd15ieCuPoFi51GF142mnPUzdRgYujW8BkMkM3K8M9zpX13E/645xf98Of2eEfV47Ycj9jOWNHUly5nzPfX3UjUp9zJL2/Gpb9nhJJ9yx3XDliV+ezbDUbAo5T4yV7REQUWSQApempMI5OBTRKlP+vxzC5A3SzM6CdleFxZg55Z9erYExP9Z50kiTAYodu6iGICVEouiYB9rZelvMJAiBJMI5OhWZJpjNJ6HxuD7d2O8XWPBoSwOdGRERERHWmRgmpvTt+q7KPJEno2vP6moQhIqIQUpqeCuPYNu4zeUSFox3wODOHvDOnJQOij2V6ggCISgilNiR8ewqWnBIYvCWkyvtrlDD3SYZ2RRYAH89NkmC6uyUUBRY+NyIiIiKqMzVKSHkqYk5EROGlfGmelKgBSizeZ/J4mZlDVZMSNVV3AiCV1X6SYtUB9fc5A4vPjYiIiIhkUKOE1NnsbLd/aE2Ij4NOp4MkSSguLkZhEXftISK6HFVemucXDzNzqGpCkcW/frkmx3/zTAH192sGFp8bERERVaFw2ttyD4HCSI0SUgNuvd1je7euV+Ldd94AwJ34iIguV16XePlBSvJvxg852BpH+e4gSYDZDnFTNgAtxI3ZMEzuAIgK74XO7RLUm7MBAJaeHgqZewrD50ZEREQ+2M6elHsIFEYC2wbOT7v37MXM2XPRoH49/OOZybURgoiIalGVRbarEngOK2LZmkbDdEdz7x3KnoFudoZzOZ2iyArd7AzncjuPFAJKXumKov90hSUt2a+xlM+oIiIiIiKqbbW2y17LFs0AANf2urq2QhARUS2pcolXFYxDmwAmG6BVQsgzQdyYzdpEHkgASia3B9Q+/n3IbHfuXlhR+fe+llRae9X3cyAVZ2AREREReRY17D7HFzM/kXcgFBZqlJD6cvqnbm0KpQL1kpLQrGkTAIDVyr+AEBFdbvwtsu1VShSMk9o7vzVM7uBMqnA7DAe7XgXj2DbuSSOLHbqvjwCCACHXBHGT52SeAMduhpolmTD3SYa9oRamW5tAaqDzHbjyrDcPM7CIiIiIPNH0uMHxBRNSFAQ1Skj1vKo7JC9LBcp34Fuzbn1NQhARkQz8LZrtN1HhqEcFRxIlklVVLF7z7XHoZh/z+3qKIquzELn4czYKZ93gfZmlp/9ne5mBRURERERUm2q8ZE/w8kvvxYICLFq8FJ/N+LKmIYiIqI5VWTS7bImX7rND0CREwXy2COZr6sGaluL5gmW1joyjU6FZkuk2E8euV8GclgwpURP2S/yqKhYv2KpfgMvaMd53za+yY+LKLChPG3zOwCIiIiIiqk0122VviPsue5Ikobi4GMXFJTW5NBERyUhRZIV6ey4sNzRwP1hhiZduUSZiY2JQWFwMqBTeE1KAIxmiUcLcJ9k5o8fbbKFwXeJXZbF4SULpqJbQfne8Wkkif5daKk8bIn6mGhERERHJq0YJqbNnWfyUiOhy4+9sJFu82vMFvCzxkuK89K9ESrqUNPE6WyhMl/hVWSzeQ9IuEP4uteRuekREREQktxrWkOqBHt27wmg0YtaceS7HHnzgPmh1WuzctQfbd+ys0SCJiKjmApmNZGsSBXunBJfzhXwTdNMPey+yHWAyxOdsoSqW+F2u/J3BVDFpFwh/l1pyNz0iIiIikpuPfaar9ugjD2HCuDGol5Tkdiw+Pg4Txo3BuEcerEkIIiIKEudsJLHSj/6y2Uil6anOJlP/Rm7na+efgHZFltfkkLgxGzDZvNZGAgBIEmxNo2BIT0XR290ds4W81TyqMFsoXNT2DCZFkRW62RnOhJ4L7qZHRERENWQ9ewrWs6fkHgaFiRolpFq3dvzlZfuOXW7Hdu3eC0EQ0KZ1q5qEICKiIPB3NpJdr4IEwDywcaULSNCsPeMzhs9kSIVYpvtSUTq2DWxd3f8xw5PqzhYKReLGbMDuO2EHk61GM5i0szKg+/wwYLa7HjDbofv8MHfTIyIiomormvYWiqa9JfcwKEzUaMleTHQ0AECjdf/LgqgRXfoQEZF8AqldpDxZAnujKJfDql25UFworTJOebKj8rLAmgirekdKRVmyzstyuiDMYBLgqLulWZIJc59kSEka7qZHRERERCGnRgmpnNxcJDdsiHvvvgs/b9wEq9UGAFAqlRg1cgQAIDcvr+ajJCKiGgmkdpG5Q5xbu2a179lR5TwlQyxtY2HtXY1ld2FY78jcN9mRlPJ40HOx+OpSFFmrVRidiIiIiKgu1CghtX3HLgwbMhg9unfFssUL8Psf2wEA11zdE40bN4IkSfhjOwuaExHJze/aRQVmmO9p6dpotAacFKqYDBGGNqleQioM6x2Zb0lxa9MszoTySCFnMBEREVHIS3jtMwBAzqR7ZB4JhYMaJaS+/Ho2+t9yEzSiiMaNG+HO229zHhMEASaTGV/NnFPjQRIRUc2IG7NheLaj99k5ZSxdEyHp1a7nbj4HwWirdmx/Z2dVpjx4MazqHdmSdbB2SXRpUx4rQtQHf3pawEdEREREFNZqVNT8+IlMTH7meeTnX4QgCC5/8vLyMfnZ53Hs+IkgDZWIKDLY9SqUDm0CY3oqSoc2gV1fo387cJAA2HwU0y5judl9dz1xjX/L9bzxd3YWrK5FuINZzLxWXtMAmW92nx0lrjvDZBQRERERRaQa/0b+29ZtGDDkdlx3bS+0aNYMAHDi5En8tnUbTKaaFaJVq9X4//buO76pqv8D+Odmp5MOuthUy3QgOBAERPYSNwpSUeEHCG4fxcfx4PZxPqI4cIFbZKiogIigCOJAEBAoFCktUuieGU1yf3+kDU1v0iZpkpsmn/fr5evVnju+Jzm5Eb6c8z3z5szCxAljERcbi5yDh7DoldexbfsvzV637qtV6JAh/UsVAOQdzcf4S690/B4TE41ZN83AJcOHITWlPUrLyvDz9l+x+PU3UVh4olX9JyLyhgjAmJ0pKQhee3tvR20hX5MXpomdmi9q7k5NHVS/l/gY1U6zqRC1t/cGNArpDn+Ao1aUakcJLANTHM22ND2sp8VCdajK59iBfE+95TIhteF4kKITEREREYUWv/wTsclkwvebfvDHrZw8/shDGHnJcLz/4cfIO5qPyZPGY/GiF3DjrLn4Y+cut9c9/cwLiIpy3iEqIz0Nt86bg63btjvaBEHAklcXIbN7N3z86QrkHT2KTp06YspVV+LCgRdg0uXXoLa21u+vi4jIFWN2Jgwzs+p3YWtEo7C3w14w3FuiUoDpyi6Sdu3qo4DJCtNVXQGFm7RMtBqmad19ittAUWWBflnuqdfWOCnVaGc5odTklJACgLpBKa1KSAXqPfWWpXsMrKfFObWpdpdBedwQ8NhERERERKGoVQmpUSOG46JBF6K8ogLPvbjI6djdd9yK+Lg4/PjTVqzfsNHre/ft0xvjxozCs8+/hHff+wAA8MWar7F6+Ye46/Z5mHbDTLfXbnSRHJt18wwAwFffrHW0nXVmX5zRtw8ee/IZfPzpZ472I0eO4rGFD2Lg+efiu+83e913IiJv2WJVMGRnShM2gP13UYRheia0K/O8LnxtHpoKW6reqU31Zymin90LW6wKpiu6uE9ItSJuYw21oJrOVGq8s5yYpEXTfwIwD0rxOWEUyPfUW2ZXSyG/bd1SSCIiIiKitqxVNaSmT7sWkyaOczmLqLKyEpdOGo9p1/lWfX/UiOGwWCxYvnK1o81sNmPl51/i7LPORFpqivuLXRg/djTyC45h567djrbo6GgAQElpqdO5RcXFAABjK5ccEhF5yjwszb6kztWSNsDerlXCPNS73epEAMamu+YB0H1y5FRcVTP/K/AxruQ2sM9Eajd5I6Ke2g39khxEPbUb7SZvhL5+2ZyixATlX+VO11l7t4PNx1pSgXpPvSXCxe56Fhs033u3cyERERERUThp1Qypbl3tS0D+3LNXcmzvvv0AgO7duvp07149s5B3NB81NTVO7bvrY/XokYXCEyc9ulfPHlnI7N4Nry9527mPf+1DbW0t5s+ZhYqKChw5chSdO3fEXbfNw+49e/Hz9l+bvW9iQgISEto5tRlrK5BTsgcJ8XHQ6uM96h8AaDUaaDTqlk8MALliyxU3Sm+fKZKc2MKJARBp77VcseUcY8C311zSsZ1kdpAr+o7tkJRY4/JY07jWaCVKr8iAtXc7p/NUJ4xI32uGkJgQkLgt2loDoP5e6lig0TgJO6pQ0qS/2hFdEP9tkeQ2rsbZGq1E1aAkWBPUsJ6X4FF3hGEdIHRqB2VZHWJ/KoGypuUdBV291w1xm97H0DMGZenOy8ij/qxEihANJEZ71MfmYgcLv7PDP65csdvidzbjei8Sn2U5Y3Ocwz+uXLEta5cDAJITPftzlr9xnIPDl2fZZFCg0st/b21VQkqrtf+rdXxcnORYQ5tO59u/bCcnJ6OoqFjSXlRsL66b0r69x/eaMG4MAOCrb9Y5tZeXV+Duex/Afx5agLffWOxo3/LTNtxxzwJYrc3/pWTK1Vdg7mznpYM5B/Zi9MVr3VxBROSasqzOo/PMGVqUXNOh2eSJCKD0mg4ovbIDRK109lO7Lwsh2LyLqyoze3Rea8X8UoaSaZ2c2qrPS5AkpKzRSpQMS0RdOxVsRQbE/FSCiglpbl9zc2ouSETNBfb/2xbN7IrEz44h8ZNjHhU7d/deN9wnfk0hipu8HgCI2yz9/xsRERFRqLPu3CZ3FyiMtCohdeJkETp2yMBNM6Zjy9afUVlZCQCIi4vDjTdc7zjHFzqtFuY66V+UzPXL6BqSYS0RBAFjRo/EX/v24/DfRyTHS8vKsH9/Dj76eDkOHf4bPbNOx4wbrsdjCx/EXf+6v9l7f/zpCqz79junNmNtBQCgrKISKoPN1WUuxcXEoLK62uPz/Umu2HLFbcjyFpeWBT12pL3XcsWWc4wB316zZYsJuKWb++Vl9aqGn1qufHJmF6ed4hriGrIzYZjaSVrIu1612gJb/Xtj+6oKmNmlxR3wTF8fRrGbOkv+HGOxtAyKwtNhSztV86rmrHgUVVdAMNvc7pp3cnZX+9JDN6/Z4/gaASVTO6G21tBs7aqW3uuG+5Rc00G6JNJig/Gbv2Gu9a1uVaR9j/A7O/xjt8XvbMb1XiQ+y3LG5jiHf1y5YvM7O/jayjhbzN73sVUJqZ+2/owpV1+B0zK745svV2D3bvtyujP69kZsbCxEUcRPW3/26d5GkwkatXRamqY+EWXysL7TgP7nIC01Be998JHkWMcOGXh7yWLc/+BCbPjuewDA95t+wLHjx/HEIw9j1aCB2PKT+wxwaVkZSsucB8hm4a58ROQ90xVdWkxGSbjYKa7ZQt4AIIowTukG3cd/Q1Fl8XgHvEAX/W4gAFD/dNL+fjTQKVE3IAmarUXud81T1vfb3Xvo7v2QdMDzYuctFk1v3K/GVAqYruoSlN39iIiIiPxJ03+w/Ydvv5S3IxQWWlXU/K13lqGiwj4rKjYmBgMvOA8DLzgPMTExAICqqmq89c4yn+5dXFyM9u2TJe3tk5MAACeLPJt5NWHcaFitVny9dr3k2KWTJkCr0WDzD1uc2jdt+hEA0O+sM73tNhGR16xpepgmd/b+wkbJE1us/d8XfCnkrVuaC/2SHMDcZFan2Qb9khzHDnnBotkirQ9YNzjFswSQO94k+zwsdt7ie+0ubpMxIyIiImoroidNRfSkqXJ3g8JEK5fsncTNs+fhyUcfxmmnZUJo9AfvQ4dycf9Dj+DESc8Kjze1/8BBnDugP6Kjo50Km5/Ztw8A4MCBnBbvoVarMeKSi/Hrbztc1qNKSkyEIAhQKhVovDpQpbK/LUqVUnINEZG/GW46DVA7//uA8q9yaH46Ccvpcagb1kxipHHyZFM5bImeLWcWG+1c17ADnnZlHsxD0yAmaSGUmKDZXBi0mVGNqXaWArUWIOrU/6JMl6TDFqWyJ4B8pP6hEKoDlbD0iEfdkNQWzxdb2N1P9PC9lmg0Zro1Bb7dg4iIiIiojWvVDCkAOJBzEJdfMw1XXTsdd9/7b9x9779x1bXTcfk10xAbG4uH/n2fT/ddv2EjVCoVrrp8sqNNrVZj8qUTsevPPY4d9tLSUh27/TU1ZPCFiI+LkxQzb5B39CgUCgVGjxzh1D5uzCgAwP79LSe9iIhaw9ItBubRHZo02hDzyC7ol+ZCdbDSo/vUXZiCsqmdpfdyQyiRLntWVFmgW1MA/dJc6NYUyJKMAgChzgbV9ib/iBCtRt2IjFbdV721CPqluVBv8+wfSly9R07Hy1tX6L2lhBcRERERUTjz23qBAzkHcSDnIM48oy8mT5qAUSOHIznJvrzukcef8vp+u/fsxdr1G3Db/LlITEzA0fwCXDpxHDLS0/HQwscc5z356MM4d0B/9O13vuQe48eNgclkwrffbXQZY/UXa3DD9VPx8AP3oVfPHjiUexi9e/XA5ZMn4eChXGzYuMnrfhMRecoWq0LNg2cBCudlXdo1BVAW2OvRCaWe1curG5KKCk9OrC9Srtns5Z6sQSYIrStO7qTJa9ZsKkTt7b3dF3IHgDrX75EtVgXzsDTY0mNhOiO2Vd1qKeFFRERERBTO/JKQ6pF1OsaOHonRo0YgI92+tKRh+Z7Yih2P7n9wIebPLcTE8WMRFxeLnIOHcMttd+L3HTtbvDY6OhpDBl+IH7ZsRXV1jctzKioqcc3UGzBv7iwMHTIYV195GcorKrDq8zX438uLYbHIMzuAiMKbY6e47NPsSZHGLDbo3j3k+NWj5InHgYNfpNwXtlgV6i5M8bwQeWMeFGZvtpB7PaHCDKH61HvUdHe/FrevaK7vbSQpSEREREQUSD4npLp07oSxY0Zh7OiR6NrlVDHexnWk9h/IkRQM94bZbMZzLy7Ccy8ucnvOjJlzXbbX1NRgwMChLcY4WVSEhxY+7nMfiYi85XanOABQKWAe39GxA5snyRO3mp5vtkG/LDfoRcq95SgW7o2G12oVAVXLr7nh94YEk+R2yTqYR2ZAu/4fAC2MmSsN/QiBnQuJiIiIiEKR1wmpGdnTMHb0SPTIOt3R1pCEslptUCoFiKKIZ55/Ce9/+LH/ekpEFAaa3SkOcOzApl2Z50hYtJQ8cUsQoPm6AMpjtbIWKfeWT8XC6xNPmpV5qPOgMHvTQu51F6WgbpBzoXPD/2VBs6kQolbR/JgBgChC99ZBCKJ9KZ56cyHMl3eRjlkbSQoSEREREQWa1wmpO269BaIoNkpCWfHrbzuwfsNGfPf9Jmze8A0AoK7xtnVERATAg9k/LnZgc7ULnqc7xSmP1TpmW7UVntbNiv3uJDTHTTAUlDslnpRe7FzXUMhd+80xVLx/EWydoh3HbKl6VD3VH4pSY8sztgQBimKT0655obRzIREREZE/lD08R+4uUBjxecmeKIr4Zt23ePrZF1BWVu7HLhERha+6C1M8Os/VDmwNyRMAME7s6FFCqi0Wzm6xblZ9DaaUN/OgrLGiuLSs1TEFq4ioxftR/WR/p3bLecke36OlMSMiIiIiolMULZ/i3tjRI7Hq0w/x4P3/wvnnDXCqH0VERHa2WBWMEzui6olzUHdRy0kkoOVEkmZTIWCyuq9pJIqAydomC2c31M2CIEhfX6MaTMoaq1/jqn88CcXxFsuVu9UWk39ERERERHLxeobUZytXY8QlF6NdfDwAIDExAVdePhlXXj4ZFZWVfu8gEVFb1XRnNs8u8mwHtmaLnYdB4Wy3dbMa12BKTPBrTDFWBVuyzocLuWseERERRYbY2fcDAIqfuEfmnlA48Doh9cjjT+Pxp57FwAvOw9jRIzF82FBER0cBANrFx0Os/9fs2+bPwbkDzsH3m37AV9+s82+viYjagBZ3ZmtlIsmjpE0b5apuVqBrMJmHpQFqLycOh0Hyj4iIiMhTqvROcneBwohPNaSsViu2/LQNW37aBrVajaFDBmPs6JEYMngQtFoNACA6KgqjRgzHiOHDmJAioojjyW56El4mkpombTTpsTAfrwqrwtnBrMHUmt392nLyj4iIiIhIDj4XNW9QV1eHDd99jw3ffQ+9Xo9LLh6KcWNGYeAF50GpVLKuFBFFJI920wOg+boAymO1rZr905C0iYuJQWV1ta9djnie7u6n+boAUUWWsEv+EREREREFU6sTUo0ZDAas+Xot1ny9FnFxcRg98hKMHT3SnyGIiNoET2fbKI/VQs/ZNSHB0939ohbtQztRx+QfEREREVErtGqXveZUVlZi+YpVuHHW3ECFICIKWZ7OtuHObKHD0939OCOKiIiIiKj1/DpDioiI7BQtJaS4M1tICudC8UREREREoYQJKSKiADBO6e7+IGfbhCw5dvcjIiIiaitMv2+RuwsURpiQIiLyM8OZ8bD0S3R/AmfbhLxg7u5HRERE1FbUfvGB3F2gMMKEFBGRn5VP6yxpU28qhOpgJWfbEBERERERgQkpIiK/qjs7EaYz2zm1CeVmxDz+JwSDVZ5OERERERH5gTK9/h9eS8vk7QiFBSakiIj8xBarQs09fSTtuo//ZjKKiIiIiNq8uNkLAAAn5k+RuScUDpiQIiJqJRGAMTsThuzTAI3C+aDRAu2KPFn6RUREREREFKqYkPITc3UB6mr+cfweExUFjS7G4+v1Ol0guhXSseWKq9VoAABxMZ6Pj79E2nstV+xgj3H5dZ1gmN7VvnteUzoVcH0W4j7MD3g/Iu3zFYnPspyxOc7hH1eu2HKOMRB548xnOTJic5zDP65ssQUBAL+zwz22L8+y2WhDpZdxmJDyE01MR2hiOsJmqYWhZA+qa2uhsihavrCRyurqAPUudGPLEVejUcsWOxLjyhE70GNsi1XBPCwNYqIWqKmD4drO9mRU/f+gnYgiyq/pBHx0MCiFzCPp8xWpz7KcsTnO4R9Xjthyj7GcsSMprtzjzM9XcETqOEfS5yu1/h9gI+k1yx1Xjti+PMsWc63XcZiQIiLykGNp3vRMQKv07CJBALRKmIemQbemIKD9IyIiIiIiaiuYkCIi8pAxOxOGmVmul+a1QEzSBqBHREREREREbZN3a8qIiCKULVYFQ3am+6V5LRBKTAHoFRERERERUdvEGVJERB4wD0sDNB4u02tMFAGzDZrNhf7vFBERERFREFW+9qTcXaAwwoQUEZEHrF192EmkfjaVflluUAqaExEREREFkvX4Ubm7QGGECSkiohaIGgXMQ1O9v9Bsg35ZLnRLc/3fKSIiIiIiojaMCSkiohbUzu8JMS2q+ZPql+bpXz0AbUIUzMeroNlcyJlRRERERBQ2oiZNtf/w7svydoTCAhNSREQu2GJVMA9LQ92AJNRdktH8yY2W5uk/y0NcTAwqq6uD01EiIiIioiDR9h9s/4EJKfIDJqSIiBoRARizM2GYnglo3RQxt4mAotFOe1yaR0RERERE5BUmpIiIGjFmZ8IwM8s+68mVWgvibvoJlrMSISZpIZSYuDSPiIiIiIjIS0xIERHVs8WqYMjOdCzBkxBFQK2AotwM3ZqC4HeQiIiIiIgoTCjk7gARUagwD0sDNErXySjA3q5WwDw0LbgdIyIiIiIiCjNMSBER1RMTtZ6dl+TZeUREREREROQal+wREdUTSk2enVfi2XlEREREROHEcjxf7i5QGGFCioionmZTIWpv7w1oFO5rSJlt0GwuDH7niIiIiIhkVvXaE3J3gcIIl+wREdVTVFmg3l7sPhklCNAvy+WOekRERERERK3EGVJEFBFssSqYh6VBTNRCKDVBs6nQZWLJFuvma9Fsg35ZLnRLcwPcUyIiIiIiovDHhBQRhTURgDE7E4bpmYBW6Wivvb23I8HUMB/KmqqDtV+S0/VCuQn613Kg2ew6gUVEREREFCkSFr4KACieP0XmnlA4YEKKiMKaMTsThplZ9iV3jWkU9nYA+vpZT+aRGZLrdcvzoFtTEPB+EhERERERRZKQTkip1WrMmzMLEyeMRVxsLHIOHsKiV17Htu2/NHvduq9WoUOG9C+WAJB3NB/jL73SqS0pMRG3zJmFoUMGoV18PIpLSrD9l9/w0MLH/fZaiCj4bLEqGLIzHfWfnAgCIIowTM+EdmUehCoLzKOk3xuab48HqbdERERERESRI6QTUo8/8hBGXjIc73/4MfKO5mPypPFYvOgF3DhrLv7YucvtdU8/8wKioqKc2jLS03DrvDnYum27U3taagree2cJAODTz1bh5MkitG+fjDP69vb/CyKioDIPSwM0SvcnCAKgVcI8NA2q/RWwdo91OqzaXQblP7UB7iUREREREVHkCdmEVN8+vTFuzCg8+/xLePe9DwAAX6z5GquXf4i7bp+HaTfMdHvtxk0/SNpm3TwDAPDVN2ud2h96YAEsViumTLsBFRWVfnwFRCQ3MVHr2XlJWtezo9b/4+8uEREREREREQCF3B1wZ9SI4bBYLFi+crWjzWw2Y+XnX+Lss85EWmqKV/cbP3Y08guOYeeu3Y62bl27YMjgC/HusvdRUVEJjUYDlaqZ2RRE1KYIpSaPzzM1rR9lsUGzkcv1iIiIiIiIAiFkZ0j16pmFvKP5qKmpcWrfvWcvAKBHjywUnjjp0b169shCZvdueH3J207tF5x/LgCgpKQUb772Mi44/1xYLBZs2/4LHn38v/jnePN/GU1MSEBCQjunNmNtBXJK9iAhPg5afbxH/QMArUYDjUbt8fn+JFdsueJG6fUAgOTEoIeOuPdartgNY5y+04jDZhtEtSCtIdWI6uwUiO11Tm3Rv5cjRRkD+PA5ibRx5rMcGbE5zuEfV67Yco4xEHnjzGc5MmJznMM/rlyxhfo/UycnJgQ1bgOOc3D48iybDApUFnoXJ2QTUsnJySgqKpa0FxWXAABS2rf3+F4Txo0BAHz1zTqn9i6dOwMAHn5gAfb89Rfu+tf9SE9Lw5z/uwlvvrYIl18zFUaj+xkWU66+AnNnOy8dzDmwF6MvXuvmCiIKJmWNFTE/lqDqkua/LypHp0ra4jaXBKpbRERERERtUt03n8rdBQojIZuQ0mm1MNfVSdrNJnuCSKv1rDaMIAgYM3ok/tq3H4f/PuJ0LCrKnvUrLinB3Pl3QqzfFv7EyZN45qnHMH7saKxY9YXbe3/86Qqs+/Y7pzZjbQUAoKyiEiqDzaM+AkBcTAwqq6s9Pt+f5IotV9yGLG9xaVnQY0faey1X7MZjbKrwoSi52QrDusMwmTx/hhuLtHHmsxwZsTnO4R9XrthyjjEQeePMZzkyYnOcwz+ubLG3bQTA7+xwj+3Ls2wxe9/HkK0hZTSZoFFLp6Vp6hNRJpNntWEG9D8HaakpktlRAByzn9Z9+50jGdXwe12dBWefeWaz9y4tK0Pu4b+d/ssvKPCoX0QUeKIA1A1uMvupzgbtyjzAJrq+CAA0SpimdAts54iIiIiIiCJYyCakiouL0b59sqS9fXISAOBkUZFH95kwbjSsViu+Xrtecqyo/h4lJaVO7TabDRUVFYiLi5VcQ0Rth7VHPMQU59pQ6m0noV+S03xCShRhmJ4JW2zITiIlIiIiIgo6Tf/B0PQfLHc3KEyEbEJq/4GD6NK5E6Kjo53az+zbBwBw4EBOi/dQq9UYccnF+PW3HS7rUe3dtx8AkJLiXF9GpVKhXbt4lJaV+9h7IgoF5ouku3FqfjgJ87A0QNXM158gAFolzEPTAtg7IiIiIqK2JXrSVERPmip3NyhMhGxCav2GjVCpVLjq8smONrVajcmXTsSuP/c4dthLS0tFt65dXN5jyOALER8X53K5HgD8+tsOlJSUYsLY0dBoNI72yZMmQKVSYdvP2/33gogo6OouarJczypCve0kxETPatCJSZ6dR0RERERERN4J2fUou/fsxdr1G3Db/LlITEzA0fwCXDpxHDLS0/HQwscc5z356MM4d0B/9O13vuQe48eNgclkwrffbXQZo66uDs+9+BKeePQ/WPrWa/jyq2+QnpaGadddg992/IENGzcF6uURUYCZ03Wwdndedqv6sxSKijoIpZ7VoBNKPDuPiIiIiIiIvBOyCSkAuP/BhZg/txATx49FXFwscg4ewi233Ynfd+xs8dro6GgMGXwhftiyFdXVNW7P+2LNN6irs+CmGdNx1+3zUVVVjU9XrML/Fr0Km823HbaISH7V5ydI2jQ/2GdWajYVovb23oBGYV+e15QoAmYbNJsLA91NIiIiIiKiiBTSCSmz2YznXlyE515c5PacGTPnumyvqanBgIFDPYrzzbpv8c26b33qIxGFpuoLpAkp9ZYTAABFlQX6ZbkwzMyyJ58aJ6Xqf9cvy4WiyhKs7hIREREREUWUkE5IERH5whKvgrGn83I95cFKKI8bHL/rluYCAAzTMwGt8tSJZhv0y3Idx4mIiIiIiMj/mJAiorBTc14CoHBeiqf+8YTT7wIA/dJcaFfmwTw0DWKSFkKJCZrNhZwZRUREREREFGBMSBFRWLHFqlA2IU3SrvnxpMvzFVUW6NYUBLpbRERERERtXtnDc+TuAoURJqSIKCyIAIzZmdIleACEqjooDlbK0zEiIiIiIiKSUMjdASIifzBmZ9qLlGukX2tirBqm7EwZekVERERERESuMCFFRG2eLVYFQ3amdMe8BqIIw/RM2GI5KZSIiIiIyFexs+9H7Oz75e4GhQkmpIiozTMPSwM0StfJKMDerlXCPFRaW4qIiIiIiDyjSu8EVXonubtBYYLTBYgopNliVTAPS4OYqIVQaoJmk3QXPDFR69G9xCTPziMiIiIiIqLAYkKKiEKSuyLltbf3hn5ZLnRLc9EwH0ooNXl0T6HEs/OIiIiIiIgosLhkj4hCktsi5RoFDDOzYGxUpFyzqRAwWe01pFwRRcBkhWZzYQB7TERERERERJ5iQoqIQk6zRcoFQVKkXFFlgXpbkduC5hAE6JflSpb6ERERERERkTyYkCKikONLkXJbtJsVyGYb9EtyoFuaG4CeEhERERERkS9YQ4qIQo63RcptSVpY+yc5HVNU1UH3yn5oNkuLoBMRERERkfdMv2+RuwsURpiQIqKQ422RctPIdEDhPJuq3dcngDUFfu8bEREREVGkqv3iA7m7QGGES/aIKORoNhUCVjcFygFJkXLzqA6SU+I2FQeqe0RERERERNRKnCFFRCHHlqwD3JSPAgAIArTfHIOiygJLtxhYs+KcDmsPVkNzzBjYThIRERERRRhlemf7D6Vl8naEwgITUkQUcgw3nS5ZgteUJV0PADCPzJAc4+woIiIiIiL/i5u9AABwYv4UmXtC4YBL9ogopFh6xqNuWJpzY50NMFudmqznt0fdWQkwj2qSkLLYEPtjSYB7SURERERERK3BGVJ+Yq4uQF3NP47fY6KioNHFeHy9XqcLRLdCOrZccbUaDQAgLsbz8fGXSHuvfYldOKeXpC3u838gGK2omNbFqb32P/1ga++8I5/+j3JE1wqARiPLGAORN858liMjNsc5/OPKFVvOMQYib5z5LEdGbI5z+MeVLbZgX8XA7+zwju3Ls2w22lDpZRwmpPxEE9MRmpiOsFlqYSjZg+raWqgs3k1Aq6yuDlDvQje2HHE1GrVssSMxrqexbbEqGK7PhKl/gvOBGgsU7+wHLCKESekQ4zSOQ9YmySgAUHydD5PZ7HHcQIm0ceazHBmxOc7hH1eO2HKPsZyxIymu3OPMz1dwROo4R9LnK1UUZYnbGMc58Hx5li3mWq/jcMkeEclKBGDIzkT56uEwXdddclz38d9QVNRBUWOB7qO/m79ZnRXqLScC01EiIiIiIiLyGyakiEhWxuxMGGZmARo3X0eqU8XNdZ/lAQaL+5uplTBd3dW/HSQiIiIiIiK/Y0KKiGRji1XBkJ0JiKJjPboTUYRxSjfYYu2ri0WVAKib+doSRRimZ8IarQxQj4mIiIiIiMgfWEOKiGRjHpYGaJpJHgkCoFXCPDQNujUF9vNVzSSk6s+vvjAR8d8W+b/DREREREQRrPK1J+XuAoURJqSISDZiorQoucvzkrRenW9J0LR8EhERERERecV6/KjcXaAwwiV7RCQbodTk2XklJq/OV5WZfe4TERERERERBR4TUkQkG82mQsBktdeQckUUAZMVms2FXp0fs7U0QD0mIiIiIopcUZOmImrSVLm7QWGCCSkiko2iygL9sly3Bc0hCNAvy4WiyiI9v2lSqtH5yhprEHpPRERERBRZtP0HQ9t/sNzdoDDBhBQRyUq3NBcwWKQHzDbol+TYjzc5X78kBzDbPDqfiIiIiIiIQg+LmhORvHRKQO/8VaTcV47YO391zIxqTACgX5oL7co8mIemQUzSQigxQbO50OX5REREREREFHqYkCIiWdnS9JI29Y7SFpNLiioLdGsKAtUtIiIiIiIiCiAu2SMiWVnTdJI2RaFBhp4QERERERFRsDAhRUSysqVKZ0gxIUVERERERBTeuGSPiGRlS2dCioiIiIioLbAcz5e7CxRGmJAiIlm5qiGlZEKKiIiIiCjkVL32hNxdoDDCJXtEJKumS/aESjMEg1Wm3hAREREREVEwMCFFRLKyNpkhpTjO2VFEREREREThjkv2iEg2oloBsb3zLnuKE0xIERERERGFooSFrwIAiudPkbknFA44Q4qIZGNL0UnaFIVGGXpCREREREREwRTSM6TUajXmzZmFiRPGIi42FjkHD2HRK69j2/Zfmr1u3Ver0CEjw+WxvKP5GH/plS6P9Tv7LLz3zhsAgMEXj0J5eUXrXgARNYsFzYmIiIiIiCJTSCekHn/kIYy8ZDje//Bj5B3Nx+RJ47F40Qu4cdZc/LFzl9vrnn7mBURFRTm1ZaSn4dZ5c7B123aX1wiCgPvvvQu1tbWSa4koMFwlpBRMSBEREREREYW9kE1I9e3TG+PGjMKzz7+Ed9/7AADwxZqvsXr5h7jr9nmYdsNMt9du3PSDpG3WzTMAAF99s9blNVddMRlpqalYseoLXD+V62GJgsGa6mrJHhNSRERERERE4S5ka0iNGjEcFosFy1eudrSZzWas/PxLnH3WmUhLTfHqfuPHjkZ+wTHs3LVbciwuLg7z587Gy6++gaqqqtZ2nYg8ZEvnDCkiIiIiIqJIFLIzpHr1zELe0XzU1NQ4te/esxcA0KNHFgpPnPToXj17ZCGzeze8vuRtl8fnz/0/FJeUYPmKVZg980aP+5iYkICEhHZObcbaCuSU7EFCfBy0+niP76XVaKDRqD0+35/kii1X3Ci9PQmSnBj00BH3XrcU29Axzul3wWBFe1UMhFaOjZxjDETeOPNZjozYHOfwjytXbH5nR0bcSHyW5YzNcQ7/uHLFFgQBAJCcmBDUuA04zsHhy7NsMihQWehdnJBNSCUnJ6OoqFjSXlRcAgBIad/e43tNGDcGAPDVN+skx7JOPw1XXTEZc+ffCZvN5lUfp1x9BebOdl46mHNgL0Zf7HpZIBE5q0vROP2uLjJBkKkvRERERETUvLpvPpW7CxRGQjYhpdNqYa6rk7SbTSYAgFar9eg+giBgzOiR+Gvffhz++4jk+IJ/3YktP23D1p9dFztvzsefrsC6b79zajPW2nfmK6uohMrgeYIrLiYGldXVXvfBH+SKLVfchixvcWlZ0GNH2nvdXGxRKcCS5JyQEguq/TIuco4xEHnjzGc5MmJznMM/rlyx+Z0dGXEj8VmWMzbHOfzjyhZ720YA/M4O99i+PMsWs/d9DNmElNFkgkYtnZamqU9EmeoTUy0Z0P8cpKWm4L0PPpIcGzNqBM4+60xMvvJan/pYWlaG0jLnAbJZan26F1GksSVrAZVzGTvFCdaPIiIiIiIiigQhm5AqLi5GSoq0cHn75CQAwMmiIo/uM2HcaFitVny9dr3k2F23z8e6b79DXZ0FGenpAIDY2FgAQFpqKtRqtctlg9Q6tlgVzMPSYEuPhfl4FTSbCqGossjdLQoyW6qrguZGGXpCRERERESe0PQfbP/h2y/l7QiFhZBNSO0/cBDnDuiP6Ohop8LmZ/btAwA4cCCnxXuo1WqMuORi/PrbDpeJpfT0NExIH+OoMdXYZx+/h/0HcnDllOtb8SqoMRGAMTsThumZgFaJhrlktbf3hn5ZLnRLc1k/KILY0rjDHhERERFRWxI9aar9ByakyA9CNiG1fsNGzMiehqsun4x33/sAgD3BNPnSidj15x7HDntpaanQ63T4+0ie5B5DBl+I+Lg4l8XMAeDWO+6RtI0dMwpjR4/Eggf+4/EufuQZY3YmDDOzAFF0PqBR2NsB6JfmytAzkgMTUkRERERERJErZBNSu/fsxdr1G3Db/LlITEzA0fwCXDpxHDLS0/HQwscc5z356MM4d0B/9O13vuQe48eNgclkwrffbXQZY+OmHyRtPXvYEyM//rQV5eUVfno1ZItVwZCdaU9GCU3mQQkCIIowTM+EdmUel+9FCFcJKSUTUkRERERERBEhZBNSAHD/gwsxf24hJo4fi7i4WOQcPIRbbrsTv+/Y2eK10dHRGDL4QvywZSuqq2taPJ8CyzwsDdAo3Z8gCIBWCfPQNOjWFASvYyQba9OElNkGodSzzQqIiIiIiIiobQvphJTZbMZzLy7Ccy8ucnvOjJlzXbbX1NRgwMChXsdc/PqbWPz6m15fR80TE7WenZfk2XnU9tnSdE6/K04YIIhuTiYiIiIiIqKwomj5FKLW83Tmi1DCGTKRQIR0lz3FCS7XIyIiIiIiihRMSFFQaDYVAiartKB5A1EETFZoNhcGt2MkCzFBA2idl3CyoDkREREREVHkCOklexQ+FFUWaFcfhemabtKD9YXO9ctyWdA8QtjSXRQ0P86EFBERERFRKCt7eI7cXaAwwhlSFDSKo+6Ly+uX5EC3NDeIvSE5SQqag0v2iIiIiIiIIgkTUhQ0lnOTXR+wAbr3D0MIbndIRk3rRwGAotAoQ0+IiIiIiIhIDkxIUVCICsDSP8n1QaUAsZ0muB0iWdlczZBiDSkiIiIiopAWO/t+xM6+X+5uUJhgDSkKCmvPeIixarfHbe11UHCHvYghSUhZRSiKOEOKiIiIiCiUqdI7yd0FCiOcIUVBUeduuV49W7I2SD2hUNA0IaUoMkKwutmBkYiIiIiIiMIOE1IUFHUDWkhItdcFqSckNxHSouZcrkdERERERBRZmJCigBP1Slj6tmv2HM6QihxirAqIdl4tzIQUERERERFRZGFCigKu7uxEQN38R82WzBlSkcLlDnsnmJAiIiIiIiKKJExIUcC1VD8K4AypSGJL5w57REREREREkY677FHAWc5Ncm4w2yBUmiE2mhUlsoaUR2yxKpiHpUFM1EIoNUGzqRCKKovc3fKKZIc9AMrjTEgREREREYU60+9b5O4ChREmpCigbMlaWLvFOrWp/iwF1ApYGiWkOEOqeSIAY3YmDNMzAa3S0V57e2/ol+VCtzQXgnzd84qVS/aIiIiIiNqk2i8+kLsLFEa4ZI8CytXueurfSqAoMjq1iXEaiBp+HN0xZmfCMDMLaPoeaRQwzMyCMTtTno75wNUMKcUJo4sziYiIiIiIKFwxA0ABVTcgSdKm/rUYQrFJ0s7C5q7ZYlUwZGcCoggITeZBCQIgijBMz4Qttm1MeGxaQ0ooMUEw22TqDREREREReUqZ3hnK9M5yd4PCBBNSFDAipAkpodwMZU4lFMXSGTG29ly254p5WBqgUUqTUQ0EAdAqYR6aFtyO+cjaZIaUcJKzo4iIiIiI2oK42QsQN3uB3N2gMNE2plS0AebqAtTV/OP4PSYqChpdjMfX63XyzQ4KVGxj71inwuUAoN9VgfjoGCirBTStGqTt2A4xuXUB6YtTHI0GABAX4/n4+Isv77UtPRa1HpynSY91+5pC4fMlAii/vjMQr3E6bsuKg21WL8R/mO+3OlhyjjEg3/sdaXHb2rPc1mNznMM/rlyx+Z0dGXEj8VmWMzbHOfzjyha7/h/J+Z0d3rF9eZbNRhsqvYzDhJSfaGI6QhPTETZLLQwle1BdWwuVxbsJaJXV1QHqXXBjOwpwzzhNcqxOsKGiuhqWfLXkWE2sCFsQ3gONxh5brvfb27jm41Uen9fcveX+fBmyM2GY2kV6UAGUT+8Kk8kM/dJcv8STe4zljB1JceUeZ36+giNSxzmSPl9yj7GcsSMprtzjzM9XcETqOEfS5ytVFGWJ2xjHOfB8eZYtZk+mUTjjkj3yO0cBbqV0vkvdkFQYszOhcFFDqulsKrLTbCoETFZ7DSlXRBEwWaHZXBjcjnnBqQ5WU22wDhYRERERERG1DhNS5FfNFuAGHIkHGC3Sa5NZQ8oVRZUF+mW5bt9PCAL0y3KhqJK+p6Ei3OpgERERERERUeswIUV+5WnioW5gChRVzvWiuMuee7qluVAcdz0FUrckBzo/LXULFDHRs2SjmMSkJBERERERUSRgQor8ypvEg7LU7NRma8+ElDsCAFGjdHFAgG7lUb8VAw8UoVS6RNPleSWenUdERERERERtGwu2kF95k3hQFptR1yXa0WZL1kIEQj65IgcxSuV29pAtWSuZbRZqNJsKUXtnH0DtJgcuioDZFtJ1sIiIiIiIIl3la0/K3QUKI5whRX7lTQHupjOkoFVCjJXuvkeAtUOU22NtYZmbosoC5YEK1wfbSB0sIiIiIqJIZz1+FNbjR+XuBoUJJqTIrxRVFmg2/ONRAW6Vi532WNjcNWsn9wkpWxtISAGALV7j+oDZBn0bqINFRERERERE/sMle+R3QpGbZXtmG/TLch2JB8kMKdTXkfq7OpDda5NsHaPdH2sDCSlrpyiInZxfgyK/BroPDkOzuZAzo4iIiIiI2oCoSVPtP7z7srwdobDAhBT5nbVfonODTYT+pX3QrjvmlHhQFksTUiJnSLlk7di2Z0jVDUyRtOk+y4NuTYEMvSEiIiIiIl9o+w+2/8CEFPkBE1LkV6JWAUvvdk5typxK6D/Lk5yrcrGjGnfac625GVJtoYaUeWB7SZt620kZekJEREREREShgDWkyK8sZyRIdlJT/1Hi8lxliYsle5wh5VLzM6RCO4ln0ythOdt51pziSDWU/xhk6hERERERERHJjQkp8qu6cxIlbaodpS7PVZaZAavzbny25NBOrshBjFJBTHSfqAv1JJ6hXztJklLzc5E8nSEiIiIiIqKQwIQU+ZWlX5Jzg1WE6s8yl+cKNkAoc162F+rJFTk0t8MeEPo1pAznSZOU6q1crkdERERERBTJmJAivxH1Slh6xTu1KQ9UQFHjfgc1RXGThBRrSElYm6kfBQCIUkHUK4PTGS+JAAznJjg31ljcJimJiIiIiIgoMjAhRX5Td0YCoGpaP8r1cr0GiiKj0+9ighaiUvB739oyWzP1oxznhOgsKevpcbA26Zv6l2IIFtHNFUREREREFKosx/NhOZ4vdzcoTHCXPfIbSz8X9aPcFDRv0HSGFJQCxAQNhKbtEay5guYNbElaKAtqg9AbF7FjVTAPS4OYqIVQaoJmUyEUVfZZcXUXSnfXY/0oIiIiIqK2qeq1J+TuAoURJqTIb+rOaVI/ymKDelfzS7MUxUZJmy1ZJ01URTBbS0v2IM8MKRGAMTsThumZgPbUksHa23tDvywXuqW5qLtAmpBSb2P9KCIiIiIiokjHhBT5hahXwtojzqlNeaASgsHa7HWuEk+29lpgv1+716ZJakjZREDhvKxRTAp+7S1jdiYMM7MAscnyO40ChplZEHVKWPq0czqk3F8BRak5eJ0kIiIiIiKikMQaUuQXdWe5qB+1o/nlegAgFLmeIUV2thgVxASNU5vyUJX0vCDvTmiLVcGQnWlPRglNan4JAiCKME7pJkmccXc9IiIiIqK2K2Hhq0hY+Krc3aAwwYQU+YWlX5KkTdVCQXPAzZK99qFZoFsOtg7S+lGqXdL3NdhL9szD0gCNUpqMaiAIgFr69SImacFy5kRERERERBTSS/bUajXmzZmFiRPGIi42FjkHD2HRK69j2/Zfmr1u3Ver0CEjw+WxvKP5GH/plQCAtNQUXHbpRAy5aBA6d+4Em9WGQ7m5eP3Nd/Dz9l/9/nrCWV3TguYWG9S7m68fBQCKIhdL9mRYfhaqrJ2k9aNUByphMloB3am6TcFOSImJPsQTRZgu7QzFSSP0S3P93ykiIiIiIiJqM0I6IfX4Iw9h5CXD8f6HHyPvaD4mTxqPxYtewI2z5uKPnbvcXvf0My8gKsp5ZklGehpunTcHW7dtd7RdPGwIbrxhOjZu2ozPv/waKqUSEyeMw5uvvYwHHn4Uq79YE7DXFk7EKBWsPeKd2lR/VbRYPwoAhKo6wGR1Kopta8+EVANXBc0VBTVQlJpgyzj1GReDnJASSn0oOl+/lM8wPRPalXmOnfiIiIiIiIgo8oRsQqpvn94YN2YUnn3+Jbz73gcAgC/WfI3Vyz/EXbfPw7QbZrq9duOmHyRts26eAQD46pu1jrZffv0dI8dNQnl5haPtk89WYsXH72HenFlMSHnIfEEyoHReuqXa2XL9KAAQYC9s3nhpmhjkekihzNpRumRPWVBrf88aJaSCPUNKs6kQtXf2cbksr1mCAGiVMA9Ng25NQWA6R0RERERERCEvZGtIjRoxHBaLBctXrna0mc1mrPz8S5x91plIS03x6n7jx45GfsEx7Ny129GWe/hvp2QUANTV1eGHn7YiLS1VMsuKnIkADNmZqHnoLMkxW7LO41pBTetIcYbUKU2X7AlVdRDKzRBKnGcoifEaiN4mh1pBUWWBam+5z9cHe0YXERERERERhZaQnSHVq2cW8o7mo6amxql99569AIAePbJQeMKzHbt69shCZvdueH3J2x6dn5yUhFqDAUajtOB2Y4kJCUhIaOfUZqytQE7JHiTEx0Grj3d9oQtajQYajdrj8/3J19gl13SAYWon+05rjYkizOM6IrZMRNInx1qMW1dhQ+N948RYNRLTkqAw27zukyei9HoAQHJiCycGgLfvdXmThJSm0IT2iQkQa4DyJue2694e6iKzX+J6olahhK+L7uKMSsQnJvi1P43JOcaAfM9zpMVtS89yOMTmOId/XLli8zs7MuJG4rMsZ2yOc/jHlSu2UL+pUXIA/yzfHI5zcPjyLJsMClQWehcnZBNSycnJKCoqlrQXFduXgqW0b+/xvSaMGwMA+OqbdS2e26lTR4wYPgzrN2yEzdZ8QmTK1Vdg7mznpYM5B/Zi9MVr3VwRPqzRSpRe1cGejGq601p9raDSKzug3ZpCKGuaryWlLK2TtFmS1NAc96FOURixRithi3P+4tEctydJlWXSxJMlUeM2IeVvogIwnRbj1KYsMyNx+TEU39AFolpwvQOfKEIwi4jZ2vIOjEREREREFFrqvvlU7i5QGAnZhJROq4W5TpqoMJvsSQqt1rMlP4IgYMzokfhr334c/vtI8zF1Wjz/3ydgMpnwwkuvtHjvjz9dgXXffufUZqy1LwEsq6iEyuD5DJ+4mBhUVld7fL4/+RLbOKgjRE0zS8QEAaJWwPGzdG5rBTXENRXEA0h3OlaiMkFd2vIufb5oyPIWB+j+zfHmvbakSmfY1eWWo7i0DKb8KACdnY6Vqc2ocfOa/P35snSPgRildGpT/VgI67L90Al1MMzMkiYr63/XLTuIsnxpstmf5BxjQL7nOdLitpVnOVxic5zDP65csfmdHRlxI/FZljM2xzn848oWe9tGAPzODvfYvjzLFrP3fQzZhJTRZIJGLZ2WpqlPRJlMns2eGdD/HKSlpuC9Dz5q9jyFQoFnnnoMmd27Yfa8213OzmqqtKwMpWXOA2Sz1HrUr7ZOTPQsIehJrSBFsXQsWUcKsLrYYU9ZYF/C2rSGFBDcwuaW3u0kbaq99mSsbmkuAMAwPdNp90SYbdAvy3UcJyIiIiIiosgVsgmp4uJipKRIC5e3T04CAJwsKvLoPhPGjYbVasXXa9c3e97CB+/H0IsG495/P4xffv3d+w5HGKHUs4Sgq8RJU4oiaa0uG3fag83NDnsAoJA5IWXt007SpvyrHIB950T90lxoV+bBPDQNmvRYmI9XQbO5EIoqX6tOERERERGR3DT9B9t/+PZLeTtCYSFkd9nbf+AgunTuhOho51kiZ/btAwA4cCCnxXuo1WqMuORi/PrbjmZnPN11+3xcNnki/vvsi/imhcQV2Wk2FQKmZmpDiSJgskKzueWqZi5nSCVzhpTVRUJKUT9DytV7Fsyd6yQzpGosUOY5T9FUVFmgW1OAdh/lQ7emgMkoIiIiIqI2LnrSVERPmip3NyhMhGxCav2GjVCpVLjq8smONrVajcmXTsSuP/c4dthLS0tFt65dXN5jyOALER8X12wx8xnTp2FG9jS88eY7eP+jT/z6GsKZosoC9VY3uxzW1wrSL8v1KAmhKJbOkBI5QwrWJjvsCVV1ECrsddWECjNgca5RFqwZUqJeCWs354Lmqn3lEAKzKSIRERERERGFoZBdsrd7z16sXb8Bt82fi8TEBBzNL8ClE8chIz0dDy18zHHek48+jHMH9EfffudL7jF+3BiYTCZ8+91GlzEuuXgo7rpjPo7kHcXhv484duNrsO3nX1BSyt3A3Kp1M0PKy1pBgskGoaoOYuypmmGsISVdsqfIr0FDiXBBBIRSM8SUU+9TsBJSll7xgMJ5Bz1V/XI9IiIiIiIiIk+EbEIKAO5/cCHmzy3ExPFjERcXi5yDh3DLbXfi9x07W7w2OjoaQwZfiB+2bEV1dY3Lc3pknQ4A6NqlM556fKHk+Iyb5zAh5YYIwDogybnRZEXUi39Bs8n7WkGKIiOsjRNSEb5kzxanhhincWprqB/VQFFihFWOhFQzBc2JiIiIiIiIPBHSCSmz2YznXlyE515c5PacGTPnumyvqanBgIFDm73/4tffxOLX32xVHyOVrWMUbKl6pzb19iLovizw6X5CsQnoHnvq/slaiAAE95eENVcFzRvqRzl+LzGh8Rw1MUELUYGAL52zuChozhlSRERERERE5I2QrSFFoa2uf5KkTf1bic/3k+y0p1XCmq53fXIEsHaMlrRJZ0g1KWyuFCAmBHaWlAjpDCnFP7VQlJkDGpeIiIiIiIjCCxNS5JO6psv14HtCSgRgy5Amnyo/uAiG7EyIPt21bXO1w56yyQwpoWlCCoFftmdL1Ul28+PsKCIiIiIiIvJWSC/Zo9AkCoDlHOeElFBkhOKo61pdLTFmZ8LST5rggloBw8wsAIDewwLp4aLpLnYAoMhvYYYUAp+Qcrlcj/WjiIiIiIgiQtnDc+TuAoURzpAir1lPj4MY71xwW/1bsU/1nmyxKhiyMwHRxTwoQQBEEYbpmbDFRkbuVARgyM5E3dC0JgdEmC7v7DRbTFEsQ0LKRUFzJWdIERERERERkZeYkCKvuawf9btvy/XMw9IAjdKefHJFEACtEuamCZowZczOtM8Kc/F2GGZmwZid6fjd1Qyppsvp/M3adIZUnQ2qg5UBjUlEREREREThhwkp8ppf60clepZACXSiJRQ4zRZrmqBzMVvM5ZK95MC9T6JKgCUrzqlNebASgjnA2/oREREREVFIiJ19P2Jn3y93NyhMMCFFXhHVCljOSnRqUxypdrl8zBNCqWfXuSrgHW68nS0mlJoAm/NSx0Au2bNmxgJapVMbC5oTEREREUUOVXonqNI7yd0NChNMSJFXLH3aATrnpISvy/UAQLOpEDBZXdeQAuztJis0mwt9jtFWeDtbTLCKECrMTsdsSTq/96sBC5oTERERERGRvzAhRV5xuVyvFQkpRZUF+mW5jiVpEoIA/bJcKKosPsdoK3yZLdZ02V4glza6KmjOGVJERERERETkCyakyCuWpgXNrSJUf/iekAIA3dJc6JfkAHXShJRydxl0S3Nbdf+2wpfZYk0TUrZELdxc3Wp1fds5N1SYoThWG6BoREREREREFM6YkCKPiVEqWHrFO7UpD1S0evaSAEC/NBfxV34PWGySg24qKoUdRZUFus+OuK4hVV/ovOlsMaFp7S6NAmKc2q/9EgHU/l8WxI7Rzgdi1TBmZwYsAUZEREREREThiwkp8pj5wvaAyvkj05rlek0pS81Q5lQ6tVmz4iGqI+djqvyz3PUBsw36JTmS2WIud9rz87I9Y3YmjNdnSg8IgGFmFozZLo4RERERERERNUMldwco9ImwJyUMN54mOWZLsi8R89csJtWeclgb1yrSKGDNioNqb7mfIoQ2a+94SZv2o8Nu62i5SkiJSVrg72q/9McWq4IhO9MxQ8tJfd0vw/RMaFfmRUSdLyIiIiKiSGb6fYvcXaAwEjlTT8hnxuxMGGZmAYomCQlRhHlcR7/OkHGVeHJVTDtcSV6rTUTUO+6LurucIZXsvxlS5mFpgEbpehkhYG/XKmEemua3mEREREREFJpqv/gAtV98IHc3KEwwIUXN8nSGjC3WP5PtVHvKJG2WpsW0w5QoANaeTWp05VVDqHU/80goMUrabEk6//Up0bPkViB39yMiIiIiIqLww4QUNSvYM2QUJ4wQip2TLJY+7fxy71Bn6xgNMda5ILnyr4pmrwl0DSmhVHp/l+e56AcREREREYUXZXpnKNM7y90NChNMSFGzgj1DRoB02Z4tTe/3Qt2hyNJHWj9K9Vd5s9comu6yB//OVhKq6po/QRQBkxWazYV+i0lERERERKEpbvYCxM1eIHc3KEywqLmfmKsLUFfzj+P3mKgoaHQxHl+v1/lvmZW3most1AgweHCPqBogNsbz19tcXPGgAWVDndvUA9IQ/ZN/dvTTajQAgDgv++sPzb3XJWe2l7TF55mhbaGf5dUWiDGnHmUhJUry2nz9fB2/upn6YPXLONt9ko92og5w0005PttyjjEg3/McaXFD9VkO19gc5/CPK1dsfmdHRtxIfJbljM1xDv+4ssWuXznD7+zwju3Ls2w22lDpZRwmpPxEE9MRmpiOsFlqYSjZg+raWqgs3k1Aq6z2z85ovnAX27buCDCnO6BRuF62J4qA2QbrujxUVnu/y5qruJY/TgDo5tRW1V0H6zr/vD8ajdpt7GBwF7f2tCjnBpMVxt0nYbKKzd5PKDZCbPRFUddO5TKGt6+3rm87mM6UztpyMNugX5YLYWlui188wX6v5R5jOWNHUly5x5mfr+CI1HGOpM+X3GMsZ+xIiiv3OPPzFRyROs6R9PlKFUVZ4jbGcQ48X55li7nW6zhMSFGzFFUW6N4/DONNp0sP1s+Q0S9zvwucL1T7KwCLDVCdSui19cLmtlgVzMPSYEuPhfl4FTSbCp3eM1GjgPX0OKdrVAcqIbSQjALsdaRsXU8lpPy1vNF4vXR2lHpTIVQHKyGUmKDZXOjXcSciIiIiIqLIwYQUtUjz3XHXCan6GTK6pbl+jSeYbFDmVsHa49TsHEvPeIhKwaMETSgRARizM2GYnglolWjIGdfe3tvx3gkArKfFAmrnGXXKfeUexZAUNo9SQdQrIRisPvfb0j0GdYNSnNqEqjrEPLG72V3/iIiIiIiIiDzBoubUIluXaEmbZt0xtJu8Efr6hIq/qfaUOzdolfakTRtjzM6EYWaWfcljYxoFDDOzYMy2z0Ky9GonuVa1r/kd9hooSoySNlty62ZJGadJZ0dpV+UxGUVERERERER+wYQUtcjaRVrITPtVQUCXazXdaQ8ALH3aBSxeINhiVTBkZzqWNjoRBEAUYZieCVusCpbernbY8ywhJTSdIQXA2lGaRPSU5fRYmEekOzearNB9mufzPYmIiIiIiIga45I9apG1szS5ocyrCWhMyQwpAJa+CcDKowGN60/mYWmARun+BEEAtEqYh6bB0rud86FyMxT/tFwUToTrRF31E+dA/+4hx5JATziWF954GqBwvkp5qBJCudnDOxERERERUTiqfO1JubtAYYQzpKhFkhlSNRaXs3L8SfFPLYQy5wRIW5shJSZ6tmzOlh4FWyfnpJ9qX7lHiSRjdibqLk6XHlAJTksCPeFYXtgkGQVRhLVPglf3IiIiIiKi8GM9fhTW421nkgCFNiakqFkiAFuTGVLKvOqA1I1qTACg2lvm1GbrEAVbO02AI/uPUOpZ0k7USR9DpQf1o5yWBEqCOy8J9OpeLSwvJCIiIiIiImotJqSoWWKCBmKs2qlNeTSwy/UatFRHyhargnFiRxiyM2Gc2DHkkiWaTYWAyeo6YQTY201WCEbpbniqv8pbvL9jSWDTBFKDRksCg3kvIiIiIiIKT1GTpiJq0lS5u0FhIrT+Bk8hx1VBc2VedVBiu64j1Q7qn07al5dNzwS0p2o01d7eG/pluV7VTQokRZUFumW5MM7Mcn2CIEC95QSsp8VJDnmyw56nSwLFpJbPs/SSFlX39V5ERERERBSetP0H239492V5O0JhgQkpapa1i4uC5sGaIbW/ArCKgPJUesk8JBUQRRinnyadeaRR2GsgAdAvzQ1KH1ui/arAfUIKsBcz1zk/hopjtVBU1LV4b0+XBLZU78uWpIXZVR0qH+5FRERERERE5Aku2aNmudphTxGkhBQMVgjlzgkQW5cYGK9vO7WOrN1jmz0upkdBTHCui+XJcj3AwyWBZis0mwslhxqWO9beeBoqFp8PNFmW6fJeJtf3IiIiIiIiIvJWaPytnUKWremSPasIZUFtUGIbszMhJumkB9zVOWo4Vl/rSLemIHCd85A1s/mElCuqv1pergfYlwTql+XaZ4W5SdApcyqhqLIA9cMoAi6XOzar/t76Zbn2exERERERERG1EmdIUbOazpBS/FMLoc4W8LjN7iDngVCpdWTtJq3Bpfi7+Rpclm7R8PRV65bmQr8kBzC7HhNr91jYYk7lnY3ZmfYElqaZR9/a5F5mG/RLcqALkWWQRERERERE1PZxhhS5JWoUsKXpndqCVT/Kseubj0Kl1lHTGVJCtQUxj/+JyjcvdH2BKMI8qTOUJ4we1cESYK+XpV2ZB/PQNJiu6gJrZqMi6VEqmMZ3BL4qdk7yuZtlZhMRd90PsPRLgpikhVBigmZzIWdGERERERERkV8xIUVuWTtFAwrnxIXyaHB22PN0BznphSJgtoVErSNRAVi7Os+Q0uTVQHGsRlKs3aFRHSztyjyPE0GKKgt0awqgOlyFyjeck12mK7tA/KYY5qEeJPkUAiz9kkJiuSMREREREYUWy/F8ubtAYYQJKXLL1Q57irzgzJDydAc56YWhU+vIlhElqdOkPlJrn/3lKhnVoBV1sFR/VUC5pwzWvgmn+pEehdqBSRATPXvcQ2W5IxERERERhZaq156QuwsURlhDityydZbWPwrWkj2PdpBzcUy1vShkah252mFPc6TG49lfviaGdJ8ekbSVzuqOul7xHl0fKssdiYiIiIiIKHwxIUVuuZohpcwLzpK9hh3kGpawOamvgaRZkSft3wkDmpl7FFSuE1K1Hs/+8jUxpNl8AsIJg3NfUnWwDE5t/kJRBEzWkFjuSEREREREROGNCSlyq+kOe0K5GYqKuqDFd7uDXP2ub1H/22efRdWItaM0iSYXa3fpDDP1kRrPZn+1IjEkWEUoj3iZOKxP8oXKckciIiIiIgo9CQtfRcLCV+XuBoWJkK4hpVarMW/OLEycMBZxsbHIOXgIi155Hdu2/9Lsdeu+WoUOGRkuj+Udzcf4S690art88kTccP1UdOiQgcITJ/HBR5/gw4+X++11tEWiIE1IBaugeYOmO8i52vVNWVDrtJOdtVPoJKQsTWZICcVGKKssUFTbZ38ZZmZJd7zzQ2LIFquCpV9i87vpNWW2Qb8sN2SWOxIREREREVF4C+mE1OOPPISRlwzH+x9+jLyj+Zg8aTwWL3oBN86aiz927nJ73dPPvICoqCintoz0NNw6bw62btvu1H7VFZfh4Qfuw/oNG7H0/Y/Q/5yzcf+9d0On0+Htd98LyOtqC2ztdYDe+eOhCFL9qKYadpBzeaygxikhJbbXQdQpIRitLs8PFlGjgK2j82dQefhUQq8h8WOYnulc+NwPiSHzMA920wOg+boAymO1kiQfERERERERUaCFbEKqb5/eGDdmFJ59/iW8+94HAIAv1nyN1cs/xF23z8O0G2a6vXbjph8kbbNungEA+OqbtY42rVaLW2+Zjc0/bMGd9ywAAKxY9TkUgoDZM2/EZytWo7Kqyp8vq82wdXZVP0qehFRzlAW1aLqI0NoxCqpDrsfNFqtC+agUWBPUMBZEQ7MpMIkYa+doQOW8IlZ5+FSfPJn95StPi6Yrj9VCzxlRREREREREJIOQrSE1asRwWCwWLF+52tFmNpux8vMvcfZZZyItNcWr+40fOxr5Bcewc9duR9t55/ZHQkI7fLx8hdO5H336GaKiojDkokGteg1tmbWLfDvsecNVn2wu6kiJAAzZmShfPRwn53VHydROqL33DJSvHg5DdibcVHPymauC5qrD0iRZw+wv/dJc6NYU+CU5Fuii6UREREREREStFbIzpHr1zELe0XzU1DgnHHbv2QsA6NEjC4UnTnp0r549spDZvRteX/K2c4weWQCAvXv3ObXv/Ws/rFYrevbMwpqv18KdxIQEJCS0c2oz1lYgp2QPEuLjoNXHe9Q/ANBqNNBo1B6f70+uYp/ISpScl1yuhCYxIaBxvVVbqUDTlJQuKwmJfzonW0qu6QDD1E7SQuIaBQwzsxAVpUfSJ8da1ZfGinonS/qVVCwgNiY64ONs3WnEYZMNokZwXUNKFCGYRaTvMkLpx/F0R47PdpReDwBIln6Mg0Ku5znS4so5zqH2nR3OcSNxnCPt88Xv7MiIG4nPspyxOc7hH1eu2EL93y+Sg/D3CFc4zsHhy7NsMihQ6eW+XCGbkEpOTkZRUbGkvai4BACQ0r69x/eaMG4MAOCrb9ZJYlgsFpSWlTm1WywWlFdUtBhjytVXYO5s56WDOQf2YvTF7pNYbUVdB32TBhvUJ4zydKYZmn+kfTJn6Jx+t0YrUXpVB9dFvgUBEEWUXtkB7dYUQlnjn9pT5i7O9aNgE6HJNyAYj5yyxorEz46hpCEB56JoeuJnBX57rURERERERETeCtmElE6rhbmuaXUgwGyyz3zRaj2rkyMIAsaMHom/9u3H4b+POMfQaVFX53qJlNlkbjHGx5+uwLpvv3NqM9ZWAADKKiqhMtg86iMAxMXEoLI6uLvYNRfbkOH82pUFNSgpdk7cBSKut8RSALUWIOrUR7k2RYXi0lN9NQ7qCFHTzOpUQYCoFXD8LJ2keLotVgXzsDSIiVoIpSaPa04ZOjknxRT/1KL0eEnQxll8tQz6WoPboum2pbmQpnsDQ47PdkMmv/HnIJjkep4jLa6c4xxq39nhHDcSxznSPl/8zo6MuJH4LMsZm+Mc/nHliq35/H0A/M4O99i+PMsWs/d9DNmElNFkgkYtnZamqU8SmUye1b8Z0P8cpKWm4L0PPpLGMJqgVrt+CzRaTYsxSsvKJLOrbJZaj/oVykS9EmJKk4RKCNaPAuzFwZUFtbBmxTnarE1qSHla5FtMOnWeCMCYnSlJ6NTe3tuxC56LxXD2a6NUsKU5zzBrvMNeMDQtmq5Jj4X5eBV30yMiIiIiIp+Zf98idxcojIRsQqq4uBgpKdLC5e2TkwAAJ4uKPLrPhHGjYbVa8fXa9S5jqFQqJCYkOCWWVCoV2sXHexwj3FjbyA57DZQFNU4JKTFRCzFKBaHWnnjxpci3MTsThplZbmtOAXC7Q52lu4uC8C4KmgdDQ9F0OTP6RERERERERE2F7C57+w8cRJfOnRAd7ZwcObNvHwDAgQM5Ld5DrVZjxCUX49ffdrisR7X/wEEAQJ8+vZza+/bpBaVSiQP1xyON6x32QjeZociXJsusnU7VcNJsKgQszSyfFEXAbIVms70Cmy1WBUN2ZrM1pwzTM2GLdZ3PdbXDnlwJKSIiIiIiIn/R9B8MTf/BcneDwkTIJqTWb9gIlUqFqy6f7GhTq9WYfOlE7Ppzj2OHvbS0VHTr2sXlPYYMvhDxcXGSYuYNtv/6G8rLK3DNlZc7tV9z5RWoNRiw+cef/PNi2hhrlzY2Qypfukyy8bI9RZUFyiPNJNQEAUKNBYLJnrQyD0sDNErXO9TVnw+tEuahaS4PW13MkFIFeckeERERERGRv0VPmoroSVPl7gaFiZBdsrd7z16sXb8Bt82fi8TEBBzNL8ClE8chIz0dDy18zHHek48+jHMH9EfffudL7jF+3BiYTCZ8+91GlzFMJhMWLX4dD97/Lzz33yewddvPOKff2Zg4YSz+t2gxKisrA/b6QpmrJXuhWkMKABQF0r7ZOjrvcmeLav6jLiZoUfXUOVDtrYBpXAeP4jauOdWYZIZUnc3lLC4iIiIiIiKiSBWyCSkAuP/BhZg/txATx49FXFwscg4ewi233Ynfd+xs8dro6GgMGXwhftiyFdXV7pMBnyxfAYvFguzrr8PFQy9CYeEJPPXMC3j/w4/9+EraFmvXJjN8SoxQ1IRuIWxlgYsZUp1OJdVsCRqIGc4JKsFshahROrVZzmsPy3ntPY7buOZUAxHShJQyrxqCVZScS0RERERERBSpQjohZTab8dyLi/Dci4vcnjNj5lyX7TU1NRgwcKhHcVas+hwrVn3uUx/DiQjAcEMmbE0TUglaGLIzm91ZTk5CuRlCVR3E2FO7MjZOSFn6tpNck/jJMVT0ioJlQLL3AUURMNscNaecDiVqILbTOLUFe4c9IiIiIiIiolAXsjWkKPiM2Zkw3pzlopA3YJiZBWN2pjwda4EA6bK9xkv2LH0TJNfo91Uj6qV90l30WlJf6Fy/LBeKKumsMRY0JyIiIiIiImoZE1IEoMnOck15sLOc3Jou2xPjNbDVz5iy9G7nfLJVhO5gNSx92rkvXN4M3Zs50C3NdXnM2o0JKSIiIiIiIqKWMCFFAFq/s5zcXBUNt3WKgqgUYOkV79SuzauFwmSDmOi6KHmzBAHa9cfdLl20ZEp32OOSPSIiIiIiIiJnoTndhYLO0+SMu53l5KbMd1PY3CoCOufi5br99hlLQqm0KLknrD3ioPxHGg8ALKfFOTfUWqA4YfApDhERERERUSgpe3iO3F2gMMIZUgTA8+SMq53lQoGyQDpDytox2r4srwn9AfuMJc2mQsBkdV9Hqr54eVOWrDjpqQBqb8iErUeTYzoljNMzwT32iIiIiIiIiE5hQooAeJicMVld7iwXCpoWNQfsS/ZcJaR0++0JKUWVBfpluY4aWU7qi5frPjgM2JyPuUpItdWC8ERERERERERyYEKKAHiWnHG3s1woUFRZIJSbndqsHaMlO+wJ5Waojxsdv+uW5kK/JEc6E8psg35JDvRvHYQizznZZc2Kd5rx1NYLwhMREREREXkidvb9iJ19v9zdoDDBvyGTQ8POcYbpmYC2Ud0lsw36Zblud5YLFYqCGljbaRy/W7vFOL8OAKq95U4FyQUA+qW50K7Mg3loGsQkLYQSEzSbCx3JN1VOBczdThUrFxM0sKXooDxpT2w5CsK707gg/Kby1r5MIiIiIiIiWajSO8ndBQojTEiRgyfJmVCmzK+BtfGMKK00SaTaW+7yWkWVBbo1Ba7vm1MJjO7g1GbNinMkpNp6QXgiIiIiIiKiYGNCiiSaS86EMmWB653vGlPtKfP6vqqcSkmbJSsOmi0nAbT9gvBEREREREREwcYaUhQ2FPnSwuZOrCJU+yq8vq/yoDQhZe0R7/hZs6kQsEh343MI8YLwRERERERERMHGhBSFjZZmSCn/roJgsHp9X0W1RbKLX+Od9hRVFij+cRO7DRSEJyIiIiIiIgo2JqQobChbmCGl2lPu872bLtsT2+tgS7AXUBe1CthS9a4vrN+tL9QLwhMREREREREFE2tIUdgQDFYIJSa3xcPdFTT3hDKnEhie7tRmyYqDZnsx6vonSQqoK/eUQbumoM0UhCciIiIiImqJ6fctcneBwggTUhRWlPk1sLhLSLVihpTSRWFza1YcsL0YdRemSI7p3z4EzS/FPscjIiIiIiIKNbVffCB3FyiMcMkehZWmtZ4aCJVmt8c84XqnvXiIAOoGtnc+YLBAvbPU51hERERERERE4Y4JKQobIgBbiutaTkK52X6CjxTlZihOGJzarD3iYM2MldSPUv9aAsHczK57REREREREbZAyvTOU6Z3l7gaFCSakKGwYszNhOS/Z5TFb5xgYszNbdf+my/ZsGVEwj8qQnKfedrJVcYiIiIiIiEJR3OwFiJu9QO5uUJhgDSk/MVcXoK7mH8fvMVFR0OhiPL5er9MFolshHdufca0xKpRmnwaIIiAI0hNEEcbpmUheVwIt7LvjxcV4Pj4AYDtiRPlFzm2my7tIzkv8sxYqN/cOh/e6LcTWanwbY3+JtHGWK66c4xxpz5SccSNxnCPt88Xv7MiIG4nPspyxOc7hH1e22PV/1+J3dnjH9uVZNhttkBa6aR4TUn6iiekITUxH2Cy1MJTsQXVtLVQW7yagVVZXB6h3oRvbX3GNF3cENM2834IAUatEyYAY6LfW+BTbsqcYgHMCStQ32V0vpxK1ec3Xj2rr73VbiK3RqGWJ21ikjbMcceUeZ36+giNSxzmSPl9yj7GcsSMprtzjzM9XcETqOEfS5ytVFGWJ2xjHOfB8eZYt5lqv43DJHoUFMdH1znqS89zswOcJ1YGKFs9Rb+VyPSIiIiIiIqKWMCFFYUEoNXl2Xoln57m8ttjUYhzNtiKf709EREREREQUKZiQorCg2VQImKz2GlKuiCJgskKzudDnGAIAVY77VbFCmRnKfeU+35+IiIiIiIgoUjAhRWFBUWWBflmuvche06RUfaFz/bJcKKosrYqjzHG/bE+9vQiCrVW3JyIiIiIiIooILGpOYUO3NBcAYJieCWgbFRs326Bflus43hrKA+5nSIlqASLsM6mIiIiIiIjCTeVrT8rdBQojTEhR2BAA6JfmQrsyD+ahaRCTtBBKTNBsLmz1zKgGljMSXB8QRdRdkgHj4Wro/ZD4IiIiIiIiCjXW40fl7gKFESakKOwoqizQrSnw+31tsSqYLu/sWALopH6poGF6JrQr8/yWACMiIiIiIiIKR6whReQh87A0QKOUJqMaCAKgVcI8NC24HSMiIiIiIgqCqElTETVpqtzdoDDBhBSRh8RErWfnJXl2HhERERERUVui7T8Y2v6D5e4GhQkmpIg8JJSaPDuvxLPziIiIiIiIiCIVE1JEHtJsKgRMVnsNKVdEETBZodlcGNyOEREREREREbUxTEgReUhRZYF+Wa6jgLmT+kLn+mW5LGhORERERERE1ALuskfkBd3SXACAYXomoFWeOmC2Qb8s13GciIiIiIiIiNxjQorICwIA/dJcaFfmwTw0DWKSFkKJCZrNhZwZRUREREREROQhJqSIfKCoskC3pkDubhAREREREQWN5Xi+3F2gMMKEFBERERERERG1qOq1J+TuAoURFjUnIiIiIiIiIqKgYkKKiIiIiIiIiIiCikv2iIiIiIiIiKhFCQtfBQAUz58ic08oHIR0QkqtVmPenFmYOGEs4mJjkXPwEBa98jq2bf/Fo+vHjBqBaVOnIOv002CxWHD48N946ZXX8MuvvzvOiYmJxqybZuCS4cOQmtIepWVl+Hn7r1j8+psoLDwRqJdGRERERERERBSxQjoh9fgjD2HkJcPx/ocfI+9oPiZPGo/Fi17AjbPm4o+du5q9du7/3YzZs27C+g0b8fkXX0GlUuL00zKRmpLiOEcQBCx5dREyu3fDx5+uQN7Ro+jUqSOmXHUlLhx4ASZdfg1qa2sD/TKJiIiIiIiIiCJKyCak+vbpjXFjRuHZ51/Cu+99AAD4Ys3XWL38Q9x1+zxMu2Gm22vPPKMvZs+6Cc88/z+898HHbs8768y+OKNvHzz25DP4+NPPHO1HjhzFYwsfxMDzz8V332/234siIiIiIiIiIqLQLWo+asRwWCwWLF+52tFmNpux8vMvcfZZZyItNcXttddPnYLikhK8/+EnAAC9Xu/yvOjoaABASWmpU3tRcTEAwGgyteYlEBERERERERGRCyE7Q6pXzyzkHc1HTU2NU/vuPXsBAD16ZKHwxEmX155/3gDs3LUbU6+9Bv938wwkJLRDUVEx3njrHXz0yamZUHv/2ofa2lrMnzMLFRUVOHLkKDp37oi7bpuH3Xv24uftvzbbx8SEBCQktHNqM9ZWIKdkDxLi46DVx3v8erUaDTQatcfn+5NcseWKG1WfoExODHroiHuv5Yot5xgDkTfOfJYjIzbHOfzjyhWb39mRETcSn2U5Y3Ocwz+uXLEFQQAAJCcmBDVuA45zcPjyLJsMClQWehcnZBNSycnJKCoqlrQXFZcAAFLat3d5XVxsLBITEtDvrDNx/rn98errb+F4YSEmXzoB/77vHlgsVixfsQoAUF5egbvvfQD/eWgB3n5jseMeW37ahjvuWQCr1dpsH6dcfQXmznZeOphzYC9GX7zWq9dKRERERERERBRJQjYhpdNqYa6rk7Sb65fRabVal9dFRdkzeQkJ7XD3vf/G2vUbAADrN2zEquUf4v9unuFISAFAaVkZ9u/PwUcfL8ehw3+jZ9bpmHHD9Xhs4YO461/3N9vHjz9dgXXffufUZqytAACUVVRCZbB5+GqBuJgYVFZXe3y+P8kVW664DVne4tKyoMeOtPdarthyjjEQeePMZzkyYnOcwz+uXLH5nR0ZcSPxWZYzNsc5/OPKFVvz+fsA+J0d7rF9eZYtZu/7GLIJKaPJBI1aOi1NU5+IMrmp79RQ96murg7rN2x0tIuiiLXrN2DenFlIS0tFYeEJdOyQgbeXLMb9Dy7Ehu++BwB8v+kHHDt+HE888jBWDRqILT9tc9vH0rIylJY5D5DNwl35iIiIiIiIKPyYf98idxcojIRsUfPi4mK0b58saW+fnAQAOFlU5PK6iopKGI1GlJdXwGZznqFUWl+8PD4uDgBw6aQJ0Go02PyD80O1adOPAIB+Z53ZuhdBREREREREREQSIZuQ2n/gILp07uTYCa/BmX37AAAOHMhxeZ0oitifcxAJCe2gUjlPAGtfX3eqYVZTUmIiBEGAUun8NjRcp1QpW/9CiIiIiIiIiMKApv9gaPoPlrsbFCZCNiG1fsNGqFQqXHX5ZEebWq3G5EsnYtefexw77KWlpaJb1y5O165dtwEqlQqXThzvaNNoNJgwdjQO5R52FEvPO3oUCoUCo0eOcLp+3JhRAID9+10nvYiIiIiIiIgiTfSkqYieNFXublCYCNkaUrv37MXa9Rtw2/y5SExMwNH8Alw6cRwy0tPx0MLHHOc9+ejDOHdAf/Ttd76jbfmKVbjiskl4YME96NqlM44XFmLi+LFIT0/DvNvvdpy3+os1uOH6qXj4gfvQq2cPHMo9jN69euDyyZNw8FAuNmzcFMyXTEREREREREQUEUI2IQUA9z+4EPPn2pNJcXGxyDl4CLfcdid+37Gz2etMJhNumnUL7rx9Hi67dCL0eh32HziIubfeia3btjvOq6ioxDVTb8C8ubMwdMhgXH3lZSivqMCqz9fgfy8vhsViCfArJCIiIiIiIiKKPCGdkDKbzXjuxUV47sVFbs+ZMXOuy/bSsjI88PCjLcY4WVSEhxY+7nMfiYiIiIiIiIjIOyFbQ4qIiIiIiIiIiMITE1JERERERERERBRUIb1kry0SRRsAwFpX69V1ZqMNFrN31/iLXLHlimsy2POwFnN10GNH2nstV2w5xxiIvHHmsxwZsTnO4R9Xrtj8zo6MuJH4LMsZm+Mc/nHlil17dD8AfmeHe2xfnuWGHEhDTsQTQnTqeaJ3XaPmWIylMFUckrsbRERERERERERBpY0/DSpdokfncoaUnyk0cdDGnwZBqYUgCB5fZyjZA31S3wD2LPRiyxW3U8eOWPTis5h/+93ILygIauxIe6/lii3nGAORN858loOL4xw8kfZeyxWb39mRETcSn2U5Y3Ocwz+uXLH5nR18bWWcRVGEaDVBoYnzOA4TUn6mUKig8DAbKLlWFeXn3oR+bDni6qLikdWjD3RR8VCoSoMeP5Lea7liyz3GQOSNM5/lyIjNcQ7/uHLElnuMgcgbZz7LkRGb4xz+ceWILfcYAxznYPB5nNXRXsVhUfMQoY7OiLjYcr5muUTie81xZtxwFInPFMc5/OPKHVsukTbOHOPIiM1xDv+4cseWC8c5vDAhFSI0MR0jLracr1kukfhec5wZNxxF4jPFcQ7/uHLHlkukjTPHODJic5zDP67cseXCcQ4vTEgREREREREREVFQMSFFEaesrByLX1uCsrJyubtCAcIxjgwc58jAcQ5/HOPIwHGODBzn8McxjgzBGmchOvU8MaARiIiIiIiIiIiIGuEMKSIiIiIiIiIiCiompIiIiIiIiIiIKKiYkCIiIiIiIiIioqBiQoqIiIiIiIiIiIKKCSkiIiIiIiIiIgoqldwdIAqW5OQkTLvuGpzZty/69O6J6OhozLh5Dn79fUez18XGxGDN58uRlJiIO+5ZgG83bAxSj8lbno6xTqfF5EkTMXzYEJx+WiaiovQ4ml+Az1auxvIVq2Gz2WR6BeQJb5/ls886A3feNh+9evZATU0N1n27AS8uehUGgyHIPSd/6N2rJ26ZPRN9evdCVJQeBQXHsGL1F/jok8/47IaZC84/FzNvvAG9e/WEQiHgSF4+3ln6Htau3yB318jP/vPgAlx5+WRs/mELbrntLrm7Q35y/nkDMGHsGPTrdxZSU1JQXFKCX379DYsWv47i4hK5u0deUqvVmDdnFiZOGIu42FjkHDyERa+8jm3bf5G7a+QnfXv3wqSJ43Heuf2RkZGOivIK7Nq9B4teeQ15R/MDEpMzpChidOvSBTfPyEZKSnscPJTr8XXz5s6CXqcLYM/IXzwd444dOuD+e++CIADL3v8Iz76wCMeO/YMH778Xj/7ngSD2mHzhzbPcI+t0vPnay9DptHjm+RexYtXnuPLyyXj+mSeC1Fvyp969euL9d5cgIyMdb727DM88/xLyj/2DBf+6C/+663a5u0d+NHnSBLyx+CVYLBb87+VX8dyLi/D7jj+Qlpoqd9fIz/r07olLJ06A0WiUuyvkZ3feNg8DBpyD7zZuwlPPPIe1677F6JEj8NlH7yEpKVHu7pGXHn/kIUyfdh2++nodnnrmBdhsNixe9AL6nX2W3F0jP7lxxnSMvORibP/lVzz9zPNYvnI1BpzTD8s/WobTMrsHJCZnSFHE2LtvPy4cOhKVlZUYOWI4zj7rzBavOS2zO66+8gq8tuQtzJ/7f0HoJbWGp2NcXFKCy666DrmH/3a0LV+xCo8+/AAumzwRry15G/n5BcHqNnnJm2f5tvlzUVlVhRkz56KmpgYA8M/x41j40L9x4QXnY+vP24PVbfKDq664DACQfdNsVFZWArA/u++8+SounTgeTz3zvJzdIz/JSE/Hv++7Bx9+vJxjGgEW/OsufPnV1zj/vAFyd4X87L/PvYgdf+yCKIqOti1bt2HpW6/jumuuwqLFr8vYO/JG3z69MW7MKDz7/Et4970PAABfrPkaq5d/iLtun4dpN8yUuYfkD8ve/xD/WvAgLBaLo23t+g1Y9ekHuHnGdNz3wH/8HpMzpChi1NbWOv4C46n77rkT332/CTt27AxMp8ivPB3j8vIKp2RUgw3fbwIAdO/W1c89I3/ydJyjo6Mx8PzzsOartY5kFAB8/uXXqKmpwehRlwSymxQAMTHRMJnMqKqqcmovLi6ByWSSqVfkb1dfdRmUSgVeftX+l1W9Xi9zjyhQJk0Yi9MyM/G/l1+VuysUAL/v2OmUjGpoKy+vQPfu3WTqFfli1IjhsFgsWL5ytaPNbDZj5edf4uyzzkRaaop8nSO/2blrt1MyCgCOHs3Hody/0a1bYJ5ZJqSI3Bg1YjjOPusMPP/iy3J3hYIkOSkJAFBeXi5vR8gvsk7LhFqtwt6/9jm1WywW7D9wED179JCpZ+SrX3/7HbGxMXj4gfvQvVtXpKen4eorL8OI4cPw5ttL5e4e+cnA88/D30fycNHgQdiw9kv8unUTftq0HvPm/h8EQZC7e+QnUVFRuOPWeVjy9rsoKSmVuzsUJHq9HlFRepSVlcvdFfJCr55ZyDua7/QPfACwe89eAECPHllydIuCJCkpMWB/P+KSPSIXtFot7r7zVrz3wcf45/hxdMhIl7tLFGAqlQrXT52C/IJj2LN3X8sXUMhLbp8MAChyUTi1qLgY/fudHeQeUWt9tvJzZGZ2x9VXXIYrL58MwJ5gfOLpZ/HpZ6vk7Rz5TedOnWCzWfHYfx7A20vfx4GcgxgxfBhmz7wRKqUSLy5aLHcXyQ/mzLoJRpMJy97/SO6uUBBdP3UKNBoN1q7/Vu6ukBeSk5NRVFQsaW/4M1ZK+/bB7hIFyYRxY5CWmoJXXn0jIPdnQoraJEEQoFarPTrXbDZ7ff+bZ0yHSqXCG2+96/W15B+BHuOm/n3fPTgtszvmzL8DVqu11fcjzwRynHVardvrzGYztDqtV/cj//Jl7G02G/Lzj+GnrT9j/YbvYDKZMW7MKCz4190oLi7Bxk0/BLLL5ANfxjkqSg+lUonn//cy3n73PQDAhu++R3x8HKZeew3eeOtd1NbWBqzP5B1fxrhL506Ydt01uGfBg6irqwtk98hP/PH/6/7nnI05s27G2nXf4pdff/dn9yjAdFotzC6eVXP9cnmtln+mCkfdunbBv++7Bzt3/YnPv/wqIDGYkKI2acA5/fDOm57VG5h42dX4+0iex/fOSE/HDdOn4fGnnuG28DIK5Bg3NWP6NFx1xWS89Mpr+HHLVp/vQ94L5Dgb6/+QpNFoJMc0Gg1MRtYckpMvY3/TjOmYdu01GHfplY7v53Xffoe331iMfy+4B5t//IkJ5RDjyzibTCZERUXh67XrnY5/s3Y9Lhp0IXr1zMLvrO0YMnwZ4/v+dSd27voTG777PsC9I39p7f+vu3Xtgv89918cys3FQ49wp9u2xmgyQeMiIampT0SxjmP4SUpKxOKXnkd1dTXuuGcBbDZbQOIwIUVt0uEjR/Dvhx7x6NyiYun00ubMmzsLJ4uK8OtvvyMj3b5ULznZXlsoMaEdMtLTcbywUFKkkfwrkGPc2KUTx+OO227BJ8tX4I033/H5PuSbQI5zcf3U8vb1z29j7ZOTcdLF1HMKHl/GfspVV2D7r79J/rHg+80/4N6770BGRjp3yAwxvozzyaJidO3SWVJXqKS0DAAQFxfn305Sq3g7xued2x8XDboQt935L8efswBAqVRBq9UiIz0dFZWVklo1JK/W/P86LTUFbyx+CVXV1Zgz/w7OcGyDiouLkZIiLVze8Gesk0VFwe4SBVBMTDRee/lFxMbGYvpN/+dyuaa/MCFFbVJJSWnApg2mp6WiS+dOWPfVasmxB++/FwAw8KJLUFVdHZD4ZBfIMW5w8bAhWPjQ/diwcRMee/KZgMYi1wI5zgdzc1FXZ0Gf3r2w7tvvHO0qlQo9e5zu1EbB58vYJyUlQqlUStpVKvsfZ1QujpG8fBnnv/btR9cunZGa0h4Fx/5xtDfUKCkrK/NrH6l1vB3j9LQ0AMD/nv+v5FhaagrWf70aTz3zAt7/8GO/9ZFaz9f/X8fHx+GNxS9Bo9HgphtnodhFXUcKffsPHMS5A/ojOjraKVl8Zt8+AIADB3Lk6hr5mUajwcv/ew5dunTGzNnzcNjFzuT+xIQUURMvvfI6EtrFO7Wddlombr1lNt56Zxl2/bkbBiOX8rV1/c85G888+Sh+37ET997/EGe8haHq6hr8/MsvmDB+DF5b8rbjX2QnTRiL6OhoJqTaoCN5RzHw/PMQHx+HiopKAIBCocCYUSNQXV2D/ALOjgoHa9d9i3FjRuHyyZPw0iuvAbDXr5k8aQLKyyuw96/9MveQWmP7r7/h1jvukbQ//OACHD9eiDfefAcHD+XK0DPyN71Oh1cXvYCUlBTcOGsujh7Nl7tL5KP1GzZiRvY0XHX5ZLz73gcAALVajcmXTsSuP/eg8MRJmXtI/qBQKPDs04/jrDPOwK133oNdf+4JeEwmpCiizLp5BgDgtMzuAICJE8aiX7+zAMCxXOuPnbsk11VV2WdD7flrH4vmhjhPxjg9PQ2LXnwWomj/H+zokZc43SPn4CHkHDwUxF6TtzwZZwB46eXX8P67S/Dum6/is5WrkZqSguzrr8NPW3/GT1t/Dn7HqVXeemcZnn7iEXy07G0sX7kaJpMJ48aMQp/evfDSy6/CYmH9qHCwcdMP2Lb9F9x8YzbatWuHAzkHccnFQ9H/nLPxn0efZBHsNq6w8AQKC09I2u+95w6UlJTyz1lh5KknHsGZZ/TFytVfoHu3rujeravjWG1tLce6Ddm9Zy/Wrt+A2+bPRWJiAo7mF+DSieOQkZ6OhxY+Jnf3yE/uufM2DB82BN9v/gHxcXGYMG6M0/E1X6/1e0whOvU8TgugiLHnj+1uj/Xtd77bY+f2PwfvvPkq7rhnAb7dsDEQXSM/8WSMG8bTncWvLcHi19/0e9/If7x5lvudfRbuvO0W9OrZAzW1tVi3/ju8uGgxa1i0URcOPB8zb8xGZmZ3xERH48iRPHz06QosX7FK7q6RH+n1etx6y2yMGTUC8fFxOHIkD2+9+x6++mad3F2jAFn31SocOnQYt9x2l9xdIT9Z99UqdMjIcHns2D//YPT4y4LcI2oNjUaD+XP/DxPGjUFcXCxyDh7CosWvY+s2938mo7blnSWLce6A/m6PN/f3ZV8xIUVEREREREREREGlkLsDREREREREREQUWZiQIiIiIiIiIiKioGJCioiIiIiIiIiIgooJKSIiIiIiIiIiCiompIiIiIiIiIiIKKiYkCIiIiIiIiIioqBiQoqIiIiIiIiIiIKKCSkiIiIiIiIiIgoqJqSIiIiIiIiIiCiomJAiIiIiIiIiIqKgUsndASIiIqJwt+6rVeiQkQEA6NvvfL/dNykpEffccSvOO28AkpOSoFAo8NQzL+D9Dz/2WwwiIiKiQGBCioiIiIImNSUFc/7vJgy84DyktG8Po9GEsrIyHP77CPb8tQ+vvfGW3F1sUx5f+BAGDxoodzdaLS42FrNmzsDFQ4cgPS0VdXV1KK+oQF5ePvb+tQ9vvPkODEaj3N0kIiIiPxKiU88T5e4EERERhb+kpEQs/2gZUtq3d3ncYrHg7HMHBblXwRGIGVIqlQq///wDlEolysrK8e+HHkFVdTUKjh1DUVGxX2IEg1arxScfvIvTMru7PeeS0RNx4uTJIPaKiIiIAo0zpIiIiCgopk652pGM2rb9F3z8yWeorTUgIyMdZ/TtjeEXD5W5h21LcnISlEolAODQ4cP4YctPHl2n02lhNJoC2TWvTBg3xpGM2vvXfryz9D2UlZcjPS0NvXpmYeSI4TL3kIiIiAKBCSkiIiIKil69ejh+/u+zL+LgoVzH7ytWfY6nnnleck1CQjvcfGM2hg25COlpqTAYjdi1azdeW/I2/ty9R3L+2DGjcNXlk9Gjx+nQ63QoKi7Gjh278PjTz6C6ugaAfWbR9GnXYtyYUejcuRMECDian4+v167H0vc+hMVicdyv8cymoSPG4u7bb8WQIYOgUirx45ateOSJ/6KystJxvk6nxR233oKxY0ZBp9Vi+6+/4cmnpa+rwVVXXIYrL78U3bp2gUqlQll5OQ7/fQRbt23HO0vfd3vdYwsfxORJExy/n9v/HOz5YzsAYMbNc5CRkY7HH3kIALD4tSUoLinB9VOvRccOHfCfR5/A519+BQAYOWI4rr36SvTsmQWdVosTJ4vw45ateOOtd1BcXOIy3ux5t+OiQQMxfuwYCALw1Tfr8czz/0NSUiL+fd/dOP/cAaitNWDFqs/x8qtvQBSbn4zfu9HnYvHrS7D5hy2O31d/ATzz/P/Q+BZz/+9mzJ09EwDwwMOPIiYmBtddcyXS0lLx95E8PP3sC/jl198d5w8fNgSXTZ6I0087DQnt4qHRaFBUXIztv/yOV19/E/8cP+7ydc6cPR/9+/fDZZdOQHxcHPb8tQ9PP/MC9u0/0OzrISIiIs8wIUVERERBUVNT6/h5/i2z8c7S97F7z15HAqjprJ20tFS8/84SpKWlOto0Gg2GXDQIAy84D3fcswCbNv/oOPbIw//G5ZMnOd2jQ0YGOmRkYNHi11FdXQO1Wo03Xn0J5/Y/x+m8Hlmno0fW6Rg8aCBmzp7vlJRq8P47S9CpU0fH72NGj4TFYsF9D/zH0fbc009g6JDBjt8vHjoEvXr0gE6nk9xv4vixePiB+5zaUlNSkJqSgm5duzabkPLGxPFjnfrd4I5bb8FNM6Y7tXXq2AHXTbkKIy+5GNNuuBnH/jkuue7+f92Fzp07OX6/bspViImJRr+zz0Knjh0AAFFRUfi/mTfin+PHsWLVF832r/Hn4qYbpsNsMuOPXbscnweLxer22v+7eYbTa+uRdTpeev4ZjBp3KSqrqgAAgwcNxMVDhzhdl5GejssunYCLBg3E5VdPRWlZmeTeC+69C927dXX8PuCcfnj7jcWYMu0G5B3Nb/Y1ERERUcsUcneAiIiIIsPP2391/Dx82BC8984b+OWn77Hs7TeQff110DdJ2jy44F+OZNTnX36FWXNvxSOPP4WaGnti6dGHH3BcM+KSix3JKIvFgneWvo/Z827Hggf+g63btkOEfYrN9VOnOJJRx48X4p4FD+Ke+x5wzJI5t/85mD7tWpf91+q0uPf+h/DoE0/DbDYDsCelYmKiAQAXDjzfkYwyGIx46r/PY/4d96C4pATt2sVL7nfxMHuSpK7OgoWPPYUbZ83FvxY8iHeXfYBjx/5p9r184613ccc9Cxy/79t/ANfPmIXrZ8zCvgPOM3g6deqILT9tw/w77sGd9yxAbu5hnNG3jyMZZTQa8czz/8Mtt92F7b/8BgBo3z4ZD9z/L5exk5OT8PAjT+ChhY/BarUniyZNGAedVou77/03Xnn1Dce5V11xWbOvA3D+XJzT7ywseW0Rfv5xIz754F3MmXWTy/eu8Wt7852luOW2u7D/QA4AICYmGuPGjnacs3Xbdvzn0Sdxy6134oabZ2PW3Fvx7rIPHK/lissmubx3Wloqnvzvc5h/xz3Ys/cvAEBsbAxuv/WWFl8TERERtYwzpIiIiCgoVq7+AgPOORsTxo91tGk0GpzT7yyc0+8sXHPl5ZgybQYqq6oQFxeHiwZfCAAoKirGZys/BwAcPHQY237+BWtwgpAAAAijSURBVCMuuRgJCe0waNBAbPjue0xsdM93lr6P/738quP3L7/6xvHz+EaJisee/C82/2ivu1RbW4tXXrIvrRs3ZhTefvc9Sf8fe+K/2LjpBwDAsKEX4aJBF0KlUqFDRgYO5BzE8GGnZuF89MlyvP/RJwCA3MN/4+vPP5Pcr2EWVp2lDkfz87H3r/2oqanB12vXt/heHj2aD0vdqVlc1dXV+GPnLpfnHvvnH9xy212O5BEA3HfPnY6fP/50BZa+9yEAYNefu/Hd2i+h1WoxaOAFiIuLc1qSCADvf/gJVqyyj8f1U6/F6adlAgBeeuU1rF2/AQCQff1UxMREu5yZ1dTWn7fjzXeW4sbs66FQ2P+tVKVSoU/vXujTuxemXHMlpmXfjPyCY5Jrv/t+M158aTEAQK/T4dmnHwcAdG4U95ffdmDWzTdg+rRrkZ6WBr3eOfHZp3cvl/167/2P8MFHnwJwHsOLBg2ESqVsduYWERERtYwzpIiIiCgobDYb7nvgP7j2+hvx7rIP8Ne+/U5Jks6dO2FG9jT7z506OpIT7dsn47133nD8N+KSix3XNCyp6tqls6Nt84+nahA11aXRUrM/9+x1/Lx7z1+NzukMV377/Q/HzxXlFY6fY2NjAAAdO3RwtDXMqAHsyaOKilPnN1j9+RrYbDZE6fV46/VXsH3LRmz45gs89dh/0Kd3T7evwVs/bf3Z6X0GgC5dGr0PjWpxlZdXoKB+dpZCoXBK7DTY3ei1VTRKVu39a1+jdvvrjY+L86iPL760GJdfPRWvvvEWdv25B3WNkm1JiYmYN/f/XF732+87TvW9ovGYxDpew5uvLcIN109F925dJcmoxuc21fjz0XgMdTod2rvZKZKIiIg8xxlSREREFFS79+zF7vq/7CclJuKB+/+FkfVJpl49ezR3qUSUXu+XPjUs6WtOQ00iALA0SvAIEFq+v4vbb/15O66fMROTJ01En9490bVrF6SlpWLC+LEYPnwYLr/qOkdyqDVKSkq9Or+lIuTVVdWnzrXZTrXXF4331aHcwzj06ht45dU3EBMTjVtvmYPrplwFwP3norLy1JhYG81YEuqHpN/ZZ6J3L3ty72RREV586RUUHDuO1JT2eOapxwAACkXL4we4HkMiIiLyHWdIERERUVD0P+ds6JskkEpKSx07vgGAUmn/o8nR/ALY6pMdR4/m48z+A9G33/lO/5197oV4+dXXAQBH8o467jFk8CC3fWhcjPqMPn0cP5/Zt0+jc47CFwXHTi0pa7wMrFOnjm7rIO36cw8WPvYkrr4uG+cPuhj/fe5FAPZE26ALB/rUj6ZcJZjy8hq9D41ee3x8nKMwuc1mw9H8Ar/0oTl9+/SWvD/V1TX4bOVqx+9KpdKne6ekpDh+/vqb9fhizTfY8cdOj65t/L40HkOj0YiioiKf+kNERESncIYUERERBcVVl0/GRRcNwvpvN+K333fgZFExkpMSMfPGbMc5e/bal31VVlZiy0/bMOSiQejcuRNefvFZrFz9BWpqapGRkYaePXpgxPBhmJp9M/45fhxrvl6LSy4eCgCYkX09lEolfvntd7SLj8eE8WPwyONP4/jxQnz1zTr0yDodAPDvBXcj+qUoiKLoVKjakxpOrny/+UdMufpKAMC1U67CiZMn8c/xQsy66QaX5y+49y60T07Gtp9/QeGJE7BarOjf72zHcY1G7VM/PPH12vWYdt019r5ecyVOFhXh6NF8TJs6BVqtFgDw07afJfWjAuHioRfh+mnXYuPGTfj5l9/wz/HjiI2JwbSpUxznNF4C6Y3jx0/tEjjikoux44+diIuLwx0eFCafPnUKSkpKcLzwhNMYbvlpG+tHERER+QETUkRERBQ08XFxuOqKybjqismSY0VFxY5C4ADwyBNP4/13liAtLRVDLhqEIRe5n/n07YaNWP3FGkyeNAFqtQo3zZju2EUOOLWs7r0PPsaQiwZhwDn90CEjw7Fsq8Gvv+/Asvc/8um1/bT1Z/ywZSuGDL4QUXo97r/3bgD2WWCVVVWIa1KrSKfVYtSI4Rg1YrjkXgaDEd/XF1APhD9378Fb7yzDTTOmQ6fT4d6773A6XlRUjMee+G/A4jcVpddjwvixTgXvG9TU1GDJW+/6dN8/d+/FgZyD6JF1Ojp2yMBLLzwDANjxxy4kJSU2e21+wTH8+757JH1pXDCfiIiIfMeEFBEREQXF4jfewoGcQ7jg/HPRqWMHJCcnQaVSobDwBLb+vB1vvPmuU72jwsITuPLa63Fj9vUYNmQwMjLSYbFYcfLkSeze+xfWb9iIwhMnHOc/8PCj+Hn7r7jiskno0SMLOq0WxSUl2LFjJyqr7DN96urqMHP2fFw/dQrGjx2Nzp06QRDsywK/Wrsey97/yLH7nS/uumcB7rhtHsaOHgmtTosdO3bi6WdfwGuvvChJSK35eh2USiXOPKMvkpOTEKXXo6KiEjt3/YnX33zHL/WjmvPCS69g71/7cO01V6Jnjx7Q6bQ4cbIIP/z4E9546x0UF5cENH6DTz9bheKSElw48Hx07dIF7ZOTodVqUFRcjN9+/wNL3noXfx/J8+neNpsNc+ffifvvuxvnDjgHdXV1+Pqb9fj0s5X4ctWnzV77zPP/Q7+zzsQVl09GQrt47Nm7D/997gWf+0JERETOhOjU81iikYiIiIgi3mMLH8TkSRMAADNunoNfG+3iR0RERP7FouZERERERERERBRUTEgREREREREREVFQMSFFRERERERERERBxRpSREREREREREQUVJwhRUREREREREREQcWEFBERERERERERBRUTUkREREREREREFFRMSBERERERERERUVAxIUVEREREREREREHFhBQREREREREREQUVE1JERERERERERBRUTEgREREREREREVFQMSFFRERERERERERBxYQUEREREREREREF1f8D4ls3vhR6kDoAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from matplotlib.ticker import AutoMinorLocator, MultipleLocator\n", - "\n", - "# Take 15s before snap and 1.5s post snap then create a new human readable column\n", - "week_9_df = predictions_df[(predictions_df[\"week\"] == 9) & (predictions_df[\"frames_from_snap\"] < 15) & (predictions_df[\"frames_from_snap\"] > -150)].copy()\n", - "week_9_df[\"seconds_from_snap\"] = week_9_df[\"frames_from_snap\"] / 10\n", - "\n", - "# Group all the rows of the same time step together then take the mean of those rows (1 or 0) to yield accuracy\n", - "accuracy_by_frame = week_9_df.groupby(\"seconds_from_snap\")[\"base_correct\"].mean()\n", - "\n", - "# Colors (NGS-ish palette)\n", - "BG = \"#0b1736\" # dark navy\n", - "AX_BG = \"#121e45\" # panel navy\n", - "GRID = \"#3b4268\" # grid gray-blue\n", - "LINE = \"#00e28e\" # neon green\n", - "ACCENT = \"#39c0ff\" # cyan for highlights\n", - "TEXT = \"#dfe6ff\" # off-white text\n", - "SUBT = \"#9aa5d1\" # subdued text\n", - "\n", - "fig, ax = plt.subplots(figsize=(10, 6), dpi=120, facecolor=BG)\n", - "ax.set_facecolor(AX_BG)\n", - "\n", - "# Plot\n", - "ax.plot(accuracy_by_frame.index, accuracy_by_frame.values, marker=\"o\", linestyle=\"-\", linewidth=2.8, markersize=5, color=LINE, label=\"Accuracy\")\n", - "\n", - "\n", - "# Grid + ticks (match animation)\n", - "ax.yaxis.set_major_locator(MultipleLocator(0.1))\n", - "ax.xaxis.set_minor_locator(AutoMinorLocator(4))\n", - "ax.yaxis.set_minor_locator(AutoMinorLocator(2))\n", - "\n", - "ax.grid(True, which=\"major\", color=GRID, linewidth=1.0, alpha=0.6)\n", - "ax.grid(True, which=\"minor\", color=GRID, linewidth=0.6, alpha=0.35)\n", - "\n", - "# Vertical line at snap (t = 0)\n", - "ax.axvline(0.0, color=ACCENT, linestyle=\"--\", linewidth=1.2, alpha=0.9)\n", - "\n", - "# Labels & title (dark theme)\n", - "ax.set_xlabel(\"Seconds from Snap\", color=TEXT, fontweight=\"bold\")\n", - "ax.set_ylabel(\"Accuracy\", color=TEXT, fontweight=\"bold\")\n", - "ax.set_title(\"Week 9 Base Model Accuracy\", loc=\"left\", pad=8, color=TEXT, fontsize=10)\n", - "\n", - "# Tick colors\n", - "ax.yaxis.set_major_locator(MultipleLocator(0.02)) # tick every 0.02\n", - "ax.tick_params(axis=\"x\", colors=TEXT)\n", - "ax.tick_params(axis=\"y\", colors=TEXT)\n", - "\n", - "# Legend styling\n", - "legend = ax.legend(facecolor=AX_BG, edgecolor=GRID)\n", - "for text in legend.get_texts():\n", - "\ttext.set_color(SUBT)\n", - "\n", - "fig.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "7f89ac2d", - "metadata": {}, - "source": [ - "## Grab plays that have the largest increase in man coverage proabability, end with over 80% man probability, and are passes" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "a1575c4b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " gameId playId man_prob_start man_prob_end man_prob_change\n", - "742 2022110610 3836 0.075986 0.826214 0.750228\n", - "109 2022110600 3521 0.291647 0.934054 0.642406\n", - "701 2022110610 1617 0.198709 0.838289 0.639580\n", - "171 2022110602 369 0.273909 0.909861 0.635952\n", - "131 2022110601 1211 0.275959 0.910532 0.634573\n", - "396 2022110605 2744 0.238280 0.856732 0.618451\n", - "805 2022110700 2843 0.234192 0.849649 0.615457\n", - "766 2022110700 567 0.203215 0.811886 0.608670\n", - "607 2022110609 582 0.250615 0.854990 0.604375\n", - "515 2022110607 2409 0.235214 0.830188 0.594974\n", - "116 2022110601 220 0.322917 0.913313 0.590396\n", - "71 2022110600 1233 0.368813 0.957681 0.588868\n", - "278 2022110603 3614 0.232425 0.816197 0.583772\n", - "462 2022110606 2868 0.357498 0.914679 0.557181\n", - "100 2022110600 3074 0.359848 0.911483 0.551635\n", - "614 2022110609 1000 0.282125 0.830036 0.547911\n", - "119 2022110601 394 0.361745 0.909308 0.547563\n", - "21 2022110300 1229 0.275545 0.817196 0.541651\n", - "70 2022110600 1162 0.301591 0.838405 0.536814\n", - "466 2022110606 3040 0.280015 0.804513 0.524497\n", - "258 2022110603 2370 0.306106 0.829438 0.523332\n", - "112 2022110600 3772 0.406569 0.908079 0.501510\n", - "248 2022110603 1919 0.303070 0.800845 0.497775\n", - "699 2022110610 1461 0.405270 0.894969 0.489698\n", - "125 2022110601 845 0.458622 0.946454 0.487832\n", - "702 2022110610 1740 0.321101 0.808342 0.487240\n", - "368 2022110605 1125 0.359468 0.845988 0.486520\n", - "111 2022110600 3574 0.390580 0.870376 0.479796\n", - "422 2022110606 330 0.351204 0.819465 0.468261\n", - "88 2022110600 2223 0.394877 0.848171 0.453294\n", - "795 2022110700 2307 0.416444 0.867382 0.450938\n", - "442 2022110606 1531 0.406420 0.856893 0.450473\n", - "161 2022110601 3394 0.452907 0.900946 0.448039\n", - "690 2022110610 763 0.430892 0.853569 0.422677\n", - "245 2022110603 1473 0.515904 0.923859 0.407955\n", - "428 2022110606 796 0.449007 0.854154 0.405147\n", - "124 2022110601 802 0.441437 0.828420 0.386983\n", - "458 2022110606 2411 0.535048 0.916955 0.381907\n", - "434 2022110606 1107 0.466888 0.848738 0.381851\n", - "128 2022110601 1003 0.554024 0.933162 0.379138\n", - "157 2022110601 3188 0.545932 0.873865 0.327933\n", - "345 2022110604 3388 0.530769 0.851481 0.320712\n", - "376 2022110605 1796 0.559705 0.866196 0.306491\n", - "402 2022110605 3054 0.589068 0.885620 0.296552\n", - "801 2022110700 2637 0.646215 0.918210 0.271994\n", - "146 2022110601 2379 0.675562 0.946161 0.270599\n", - "224 2022110603 250 0.690393 0.955999 0.265605\n", - "244 2022110603 1442 0.640401 0.905547 0.265147\n", - "231 2022110603 558 0.699621 0.945901 0.246280\n", - "755 2022110610 4428 0.626490 0.868448 0.241958\n" - ] - } - ], - "source": [ - "# Get a df that is only pre snap and pass attempts, also that is sorted by frames from snap\n", - "pre_snap_df = predictions_df[(predictions_df[\"frames_from_snap\"] < 0) & (predictions_df[\"passAttempt\"] == 1)].copy()\n", - "pre_snap_df = pre_snap_df.sort_values([\"gameId\", \"playId\", \"frames_from_snap\"])\n", - "\n", - "# For each play, get the min (earliest frame) and max (latest pre-snap frame) man_prob\n", - "change_df = (\n", - "\tpre_snap_df.groupby([\"gameId\", \"playId\"])\n", - "\t.agg(\n", - "\t\tman_prob_start=(\"man_prob\", \"first\"), # Assign first value of man probability to man_prob_start\n", - "\t\tman_prob_end=(\"man_prob\", \"last\"), # Assign last value of man probability to man_prob end\n", - "\t)\n", - "\t.reset_index()\n", - ")\n", - "\n", - "# Compute the largest change and create a view that is this sorted\n", - "change_df[\"man_prob_change\"] = change_df[\"man_prob_end\"] - change_df[\"man_prob_start\"]\n", - "largest_man_prob_increase = change_df.sort_values(\"man_prob_change\", ascending=False)\n", - "largest_man_prob_increase = largest_man_prob_increase[largest_man_prob_increase[\"man_prob_end\"] > 0.8]\n", - "top_50_plays = largest_man_prob_increase.head(50)\n", - "print(top_50_plays.head(50))" - ] - }, - { - "cell_type": "markdown", - "id": "e5f9b4a7", - "metadata": {}, - "source": [ - "## Merge plays df and predictions df" - ] - }, - { - "cell_type": "markdown", - "id": "2eaad925", - "metadata": {}, - "source": [ - "Load in plays df" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "87e40720", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Index(['gameId', 'playId', 'playDescription', 'quarter', 'down', 'yardsToGo',\n", - " 'possessionTeam', 'defensiveTeam', 'yardlineSide', 'yardlineNumber',\n", - " 'gameClock', 'preSnapHomeScore', 'preSnapVisitorScore',\n", - " 'playNullifiedByPenalty', 'absoluteYardlineNumber',\n", - " 'preSnapHomeTeamWinProbability', 'preSnapVisitorTeamWinProbability',\n", - " 'expectedPoints', 'offenseFormation', 'receiverAlignment',\n", - " 'playClockAtSnap', 'passResult', 'passLength', 'targetX', 'targetY',\n", - " 'playAction', 'dropbackType', 'dropbackDistance', 'passLocationType',\n", - " 'timeToThrow', 'timeInTackleBox', 'timeToSack', 'passTippedAtLine',\n", - " 'unblockedPressure', 'qbSpike', 'qbKneel', 'qbSneak',\n", - " 'rushLocationType', 'penaltyYards', 'prePenaltyYardsGained',\n", - " 'yardsGained', 'homeTeamWinProbabilityAdded',\n", - " 'visitorTeamWinProbilityAdded', 'expectedPointsAdded', 'isDropback',\n", - " 'pff_runConceptPrimary', 'pff_runConceptSecondary', 'pff_runPassOption',\n", - " 'pff_passCoverage', 'pff_manZone'],\n", - " dtype='object')\n" - ] - } - ], - "source": [ - "plays_df = pd.read_parquet(\"/home/sam/repos/hobby-repos/nfl/data/parquet/plays.parquet\")\n", - "print(plays_df.columns)" - ] - }, - { - "cell_type": "markdown", - "id": "f86eea5b", - "metadata": {}, - "source": [ - "Merge the plays_df and the predictions_df together" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "03b48cc7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Index(['gameId', 'playId', 'nflId', 'displayName', 'frameId', 'frameType',\n", - " 'time', 'jerseyNumber', 'club', 'playDirection', 'x', 'y', 's', 'a',\n", - " 'dis', 'o', 'dir', 'event', 'o_clean', 'dir_clean', 'x_clean',\n", - " 'y_clean', 's_clean', 'a_clean', 'dis_clean', 'dir_radians', 'v_x',\n", - " 'v_y', 'passAttempt', 'possessionTeam_x', 'defensiveTeam_x',\n", - " 'pff_manZone_x', 'defense', 'week', 'uniqueId', 'frameUniqueId',\n", - " 'snap_frame', 'frames_from_snap', 'zone_prob', 'man_prob', 'pred',\n", - " 'actual', 'base_correct', 'playDescription', 'quarter', 'down',\n", - " 'yardsToGo', 'possessionTeam_y', 'defensiveTeam_y', 'yardlineSide',\n", - " 'yardlineNumber', 'gameClock', 'preSnapHomeScore',\n", - " 'preSnapVisitorScore', 'playNullifiedByPenalty',\n", - " 'absoluteYardlineNumber', 'preSnapHomeTeamWinProbability',\n", - " 'preSnapVisitorTeamWinProbability', 'expectedPoints',\n", - " 'offenseFormation', 'receiverAlignment', 'playClockAtSnap',\n", - " 'passResult', 'passLength', 'targetX', 'targetY', 'playAction',\n", - " 'dropbackType', 'dropbackDistance', 'passLocationType', 'timeToThrow',\n", - " 'timeInTackleBox', 'timeToSack', 'passTippedAtLine',\n", - " 'unblockedPressure', 'qbSpike', 'qbKneel', 'qbSneak',\n", - " 'rushLocationType', 'penaltyYards', 'prePenaltyYardsGained',\n", - " 'yardsGained', 'homeTeamWinProbabilityAdded',\n", - " 'visitorTeamWinProbilityAdded', 'expectedPointsAdded', 'isDropback',\n", - " 'pff_runConceptPrimary', 'pff_runConceptSecondary', 'pff_runPassOption',\n", - " 'pff_passCoverage', 'pff_manZone_y'],\n", - " dtype='object')\n" - ] - } - ], - "source": [ - "merged_df = predictions_df.merge(plays_df, on=[\"gameId\", \"playId\"], how=\"left\")\n", - "print(merged_df.columns)" - ] - }, - { - "cell_type": "markdown", - "id": "22b8fd8d", - "metadata": {}, - "source": [ - "## For the top man probability increase plays, plot the man probability across frames" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "58d18852", - "metadata": {}, - "outputs": [], - "source": [ - "def animate_play(game_id, play_id, quarter, play_description, actual_coverage, specific_coverage):\n", - "\timport matplotlib.animation as animation\n", - "\timport matplotlib.pyplot as plt\n", - "\tfrom matplotlib.ticker import AutoMinorLocator, MultipleLocator\n", - "\n", - "\t# Create the unique id for lookup in our df\n", - "\tuuid = f\"{game_id}_{play_id}\"\n", - "\n", - "\t# Create the plot df\n", - "\tplot_df = week_9_df[(week_9_df[\"uniqueId\"] == uuid) & (week_9_df[\"frames_from_snap\"] < 300) & (week_9_df[\"frames_from_snap\"] > -200)].copy()\n", - "\tplot_df[\"seconds_from_snap\"] = plot_df[\"frames_from_snap\"] / 10 # Create human readable seconds (frames are at 10 Hz)\n", - "\tplot_df = plot_df.sort_values(\"seconds_from_snap\") # Sort for seconds from snap\n", - "\tplot_df = plot_df.dropna(subset=[\"man_prob\"]) # Drop rows with missing y to avoid NaN issues\n", - "\tplot_df = plot_df.iloc[::10, :].reset_index(drop=True) # Downsample (since frame logging at 10 Hz, logging at 1 Hz)\n", - "\tif plot_df.empty:\n", - "\t\tprint(f\"[skip] No frames for {uuid}\")\n", - "\t\treturn\n", - "\n", - "\t# Push to numpy\n", - "\tx_all = plot_df[\"seconds_from_snap\"].to_numpy()\n", - "\ty_all = plot_df[\"man_prob\"].to_numpy()\n", - "\n", - "\t# Colors (NGS-ish palette)\n", - "\tBG = \"#0b1736\" # dark navy\n", - "\tAX_BG = \"#121e45\" # panel navy\n", - "\tGRID = \"#3b4268\" # grid gray-blue\n", - "\tLINE = \"#00e28e\" # neon green\n", - "\tACCENT = \"#39c0ff\" # cyan for highlights\n", - "\tTEXT = \"#dfe6ff\" # off-white text\n", - "\tSUBT = \"#9aa5d1\" # subdued text\n", - "\n", - "\t# Create plot\n", - "\tfig, ax = plt.subplots(figsize=(10, 6), dpi=120, facecolor=BG)\n", - "\tax.set_facecolor(AX_BG)\n", - "\t(line,) = ax.plot([], [], lw=2.8, color=LINE, label=\"Man Probability\")\n", - "\tdot = ax.plot([], [], marker=\"o\", markersize=6, color=ACCENT, lw=0)[0]\n", - "\ttxt = ax.text(0.99, 0.98, \"\", transform=ax.transAxes, ha=\"right\", va=\"top\", color=SUBT, fontsize=10)\n", - "\tax.set_xlim(x_all.min(), x_all.max())\n", - "\tax.set_ylim(0, 1)\n", - "\tax.yaxis.set_major_locator(MultipleLocator(0.1))\n", - "\tax.grid(True, which=\"major\", color=GRID, linewidth=1.0, alpha=0.6)\n", - "\tax.grid(True, which=\"minor\", color=GRID, linewidth=0.6, alpha=0.35)\n", - "\tax.xaxis.set_minor_locator(AutoMinorLocator(4))\n", - "\tax.yaxis.set_minor_locator(AutoMinorLocator(2))\n", - "\n", - "\t# Create title text\n", - "\tcoverage = \"Man\" if actual_coverage == 1 else \"Zone\"\n", - "\ttitle_text = f\"Q{quarter}: {play_description}\\n Actual Coverage: {coverage} - {specific_coverage}\\n\"\n", - "\n", - "\t# Continued formatting\n", - "\tax.set_title(title_text, loc=\"left\", pad=8, color=TEXT, fontsize=10)\n", - "\tax.set_xlabel(\"Seconds from Snap\", color=TEXT, fontweight=\"bold\")\n", - "\tax.set_ylabel(\"Man Probability\", color=TEXT, fontweight=\"bold\")\n", - "\tax.axvline(0.0, color=ACCENT, linestyle=\"--\", linewidth=1.2, alpha=0.9)\n", - "\n", - "\t# Make tick labels match dark theme\n", - "\tax.tick_params(axis=\"x\", colors=TEXT)\n", - "\tax.tick_params(axis=\"y\", colors=TEXT)\n", - "\n", - "\t##########################################################\n", - "\t# Animation\n", - "\t##########################################################\n", - "\tdef init():\n", - "\t\tline.set_data([], [])\n", - "\t\tdot.set_data([], [])\n", - "\t\ttxt.set_text(\"\")\n", - "\t\treturn (line, dot, txt)\n", - "\n", - "\tdef update(i):\n", - "\t\t# i goes from 0..len(x_all)-1\n", - "\t\tx = x_all[: i + 1] # include current frame\n", - "\t\ty = y_all[: i + 1]\n", - "\t\tline.set_data(x, y)\n", - "\t\t# IMPORTANT: wrap scalars as sequences\n", - "\t\tdot.set_data([x[-1]], [y[-1]])\n", - "\t\ttxt.set_text(f\"t = {x[-1]:.2f}s P(man) = {y[-1]:.2f}\")\n", - "\t\treturn (line, dot, txt)\n", - "\n", - "\tani = animation.FuncAnimation(fig, update, frames=len(x_all), init_func=init, blit=True, interval=1, repeat=False)\n", - "\n", - "\tfig.tight_layout()\n", - "\tani.save(f\"play_{uuid}.gif\", writer=\"pillow\", fps=60)\n", - "\tplt.close(fig)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "341bae72", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "gameId: 2022110610, playId: 3836, Q: 4, (1:47) (Shotgun) P.Mahomes pass short left to J.McKinnon to KC 40 for 4 yards (A.Adams).\n" - ] - } - ], - "source": [ - "for idx, row in top_50_plays.iterrows():\n", - "\tplay = merged_df[(merged_df[\"playId\"] == row[\"playId\"]) & (merged_df[\"gameId\"] == row[\"gameId\"])]\n", - "\tif play[\"passResult\"].values[0] == \"C\":\n", - "\t\tprint(f\"gameId: {play['gameId'].iloc[0]}, playId: {play['playId'].iloc[0]}, Q: {play['quarter'].iloc[0]}, {play['playDescription'].iloc[0]}\")\n", - "\t\tanimate_play(\n", - "\t\t\tgame_id=play[\"gameId\"].iloc[0],\n", - "\t\t\tplay_id=play[\"playId\"].iloc[0],\n", - "\t\t\tquarter=play[\"quarter\"].iloc[0],\n", - "\t\t\tplay_description=play[\"playDescription\"].iloc[0],\n", - "\t\t\tactual_coverage=play[\"actual\"].iloc[0],\n", - "\t\t\tspecific_coverage=play[\"pff_passCoverage\"].iloc[0],\n", - "\t\t)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "nfl", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/src/clean_data.py b/src/clean_data.py index 090e709..6dad7f2 100644 --- a/src/clean_data.py +++ b/src/clean_data.py @@ -256,27 +256,62 @@ def prepare_frame_data(df: pd.DataFrame, features: Sequence[str], target_column: return features_tensor, targets_tensor -def select_augmented_frames(df: pd.DataFrame, num_samples: int, sigma: int = 5) -> np.ndarray: +def select_augmented_frames(df: pd.DataFrame, sigma: int = 5) -> np.ndarray: """ Samples frames for augmentation, biased around the snap. Inputs: - df: Tracking rows with frame identifiers and snap offsets. - - num_samples: Number of frame identifiers to sample. - sigma: Spread of the snap-centered weighting. Outputs: - selected_frames: Array of frameUniqueId values to augment. """ - df_frames = df[["frameUniqueId", "frames_from_snap"]].drop_duplicates() - weights = np.exp(-((df_frames["frames_from_snap"] + 10) ** 2) / (2 * sigma**2)) + df_frames = ( + df[["frameUniqueId", "frames_from_snap"]] + .dropna(subset=["frameUniqueId", "frames_from_snap"]) + .drop_duplicates(subset=["frameUniqueId"]) # one row per frame id (critical) + .reset_index(drop=True) + ) + + n = len(df_frames) + if n == 0: + raise ValueError("select_augmented_frames: no valid frames after dropna/dedup") + + # sample size based on the actual candidate pool + num_samples = n // 3 + if num_samples <= 0: + raise ValueError(f"select_augmented_frames: num_samples={num_samples} from n={n}") + + x = df_frames["frames_from_snap"].to_numpy(dtype=np.float64) + sigma = float(sigma) + + weights = np.exp(-((x + 10.0) ** 2) / (2.0 * sigma**2)) + + # Validate weights BEFORE normalization + if not np.isfinite(weights).all(): + bad = np.sum(~np.isfinite(weights)) + raise ValueError(f"select_augmented_frames: weights contain non-finite values (count={bad})") + + s = weights.sum() + if not np.isfinite(s) or s <= 0: + raise ValueError(f"select_augmented_frames: bad weights sum={s}") + + weights = weights / s - weights /= weights.sum() + # Final probability sanity checks + if (weights < 0).any(): + raise ValueError("select_augmented_frames: weights contain negative values after normalization") + # (Optional strictness) + # if not np.isclose(weights.sum(), 1.0): + # raise ValueError(f"select_augmented_frames: weights not normalized, sum={weights.sum()}") - selected_frames = np.random.choice(df_frames["frameUniqueId"], size=num_samples, replace=False, p=weights) + frame_ids = df_frames["frameUniqueId"].to_numpy() + if num_samples > len(frame_ids): + raise ValueError(f"select_augmented_frames: num_samples={num_samples} > candidates={len(frame_ids)}") - return selected_frames + return np.random.choice(frame_ids, size=num_samples, replace=False, p=weights) def data_augmentation(df: pd.DataFrame, augmented_frames: Iterable[str]) -> pd.DataFrame: @@ -313,7 +348,7 @@ def add_frames_from_snap(df: pd.DataFrame) -> pd.DataFrame: - df_with_offsets: Dataframe including frames_from_snap column. """ - snap_frames = df[df["frameType"] == "SNAP"].groupby("uniqueId")["frameId"].first() + snap_frames = df[df["event"] == "ball_snap"].groupby("uniqueId")["frameId"].first() df = df.merge(snap_frames.rename("snap_frame"), on="uniqueId", how="left") df["frames_from_snap"] = df["frameId"] - df["snap_frame"] diff --git a/src/create_features.py b/src/create_features.py index 1e411a7..cab4178 100644 --- a/src/create_features.py +++ b/src/create_features.py @@ -48,13 +48,14 @@ def clean_df(location_df: pd.DataFrame, plays_df: pd.DataFrame, game_df: pd.Data location_df = label_offense_defense_manzone(location_df, plays_df) location_df = location_df[(location_df["club"] != "football") & (location_df["passAttempt"] == 1)] - location_df = location_df.merge(game_df[["gameId", "week"]], on="gameId", how="left") location_df["uniqueId"] = location_df["gameId"].astype(str) + "_" + location_df["playId"].astype(str) + location_df["frameUniqueId"] = location_df["gameId"].astype(str) + "_" + location_df["playId"].astype(str) + "_" + location_df["frameId"].astype(str) location_df = add_frames_from_snap(location_df) + location_df = location_df[(location_df["frames_from_snap"] >= -150) & (location_df["frames_from_snap"] <= 50)] return location_df @@ -75,22 +76,19 @@ def _process_df(location_df: pd.DataFrame, plays_df: pd.DataFrame, games_df: pd. location_df = clean_df(location_df, plays_df, games_df) location_df = location_df[location_df["frameId"] % 2 == 0] - - num_unique_frames = len(set(location_df["frameUniqueId"])) - selected_frames = select_augmented_frames(location_df, int(num_unique_frames / 3), sigma=5) + selected_frames = select_augmented_frames(location_df, sigma=5) augmented_location_df = data_augmentation(location_df, selected_frames) - combined_location_df = pd.concat([location_df, augmented_location_df]) return combined_location_df -def _load_weeks(weeks: Sequence[int], prefix: str, PROCESSED_DIR: Path) -> torch.Tensor: +def _load_weeks(prefix_seasons_weeks: dict[str, Sequence[int]], prefix: str, PROCESSED_DIR: Path) -> torch.Tensor: """ Loads and concatenates saved tensors for a set of weeks. Inputs: - - weeks: Week numbers to pull from disk. + - prefix_seasons_weeks: map that contains season as key and the weeks in a list as the values. - prefix: Base filename prefix (e.g., 'features'). - PROCESSED_DIR: Directory where tensors are stored. @@ -98,7 +96,10 @@ def _load_weeks(weeks: Sequence[int], prefix: str, PROCESSED_DIR: Path) -> torch - tensor: Concatenated tensor spanning the requested weeks. """ - tensors = [torch.load(PROCESSED_DIR / f"{prefix}_w{w}.pt") for w in weeks] + tensors = [] + for s in prefix_seasons_weeks.keys(): + for w in prefix_seasons_weeks[s]: + tensors.append(torch.load(PROCESSED_DIR / f"s{s}_w{w}_{prefix}.pt")) return torch.cat(tensors, dim=0) if len(tensors) > 1 else tensors[0] @@ -114,8 +115,10 @@ def main() -> None: - Serialized feature and target tensors for training and validation weeks. """ + # Enable logging logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") + # Parse command line args args = parse_args() if args.profile: set_time_decorators_enabled(True) @@ -124,42 +127,48 @@ def main() -> None: set_time_decorators_enabled(False) logging.info("Timing decorators disabled") - all_weeks = list(range(1, 10)) - train_weeks = list(range(1, 9)) - val_weeks = [9] + # Define training/validation + seasons_weeks = {2021: range(1, 9), 2022: range(1, 10)} + + # Features and target defintions features = ["x_clean", "y_clean", "v_x", "v_y", "defense"] target_column = "pff_manZone" + # Create dataloader and get the main data raw = RawDataLoader() - games_df, plays_df, players_df, _ = raw.get_data(weeks=all_weeks) + games_df, plays_df, players_df = raw.get_base_data() - for w in all_weeks: - logging.info(f"Processing week: {w}") - _, _, _, location_df_w = raw.get_data(weeks=[w]) + for s in seasons_weeks.keys(): + for w in seasons_weeks[s]: + logging.info(f"Processing season {s}, week: {w}") + location_df_w = raw.get_tracking_data(weeks=[w], seasons=[s]) + combined_loc_df_w = _process_df(location_df_w, plays_df, games_df) - combined_loc_df_w = _process_df(location_df_w, plays_df, games_df) + keep_cols = ["frameUniqueId", "frameId", "x_clean", "y_clean", "v_x", "v_y", "defensiveTeam", "pff_manZone", "defense"] + combined_loc_df_w = combined_loc_df_w[keep_cols].copy() - keep_cols = ["frameUniqueId", "displayName", "frameId", "frameType", "x_clean", "y_clean", "v_x", "v_y", "defensiveTeam", "pff_manZone", "defense"] - combined_loc_df_w = combined_loc_df_w[keep_cols].copy() + for c in ["x_clean", "y_clean", "v_x", "v_y", "defense"]: + combined_loc_df_w[c] = pd.to_numeric(combined_loc_df_w[c], downcast="float") - for c in ["x_clean", "y_clean", "v_x", "v_y", "defense"]: - combined_loc_df_w[c] = pd.to_numeric(combined_loc_df_w[c], downcast="float") + features_tensor_w, targets_tensor_w = prepare_frame_data(combined_loc_df_w, features=features, target_column=target_column) - features_tensor_w, targets_tensor_w = prepare_frame_data(combined_loc_df_w, features=features, target_column=target_column) + torch.save(features_tensor_w, PROCESSED_DIR / f"s{s}_w{w}_features.pt") + torch.save(targets_tensor_w, PROCESSED_DIR / f"s{s}_w{w}_targets.pt") - torch.save(features_tensor_w, PROCESSED_DIR / f"features_w{w}.pt") - torch.save(targets_tensor_w, PROCESSED_DIR / f"targets_w{w}.pt") + del location_df_w, combined_loc_df_w, features_tensor_w, targets_tensor_w + gc.collect() - del location_df_w, combined_loc_df_w, features_tensor_w, targets_tensor_w - gc.collect() + # Splitting out training and validation + train_seasons_weeks = {"2021": range(1, 9), "2022": range(1, 9)} + val_seasons_weeks = {"2022": [9]} - logging.info("Aggregrating training set (Week 1 through 8)") - train_features = _load_weeks(train_weeks, "features", PROCESSED_DIR) - train_targets = _load_weeks(train_weeks, "targets", PROCESSED_DIR) + logging.info("Aggregrating training set") + train_features = _load_weeks(train_seasons_weeks, "features", PROCESSED_DIR) + train_targets = _load_weeks(train_seasons_weeks, "targets", PROCESSED_DIR) - logging.info("Aggregrating validation set (Week 9)") - val_features = _load_weeks(val_weeks, "features", PROCESSED_DIR) - val_targets = _load_weeks(val_weeks, "targets", PROCESSED_DIR) + logging.info("Aggregrating validation set") + val_features = _load_weeks(val_seasons_weeks, "features", PROCESSED_DIR) + val_targets = _load_weeks(val_seasons_weeks, "targets", PROCESSED_DIR) train_features = train_features.to(torch.float32) val_features = val_features.to(torch.float32) diff --git a/src/generate_predictions.py b/src/generate_predictions.py index 859b504..e6ebfe8 100644 --- a/src/generate_predictions.py +++ b/src/generate_predictions.py @@ -5,8 +5,6 @@ import json import logging -import os -from pathlib import Path import numpy as np import pandas as pd @@ -21,30 +19,23 @@ from load_data import RawDataLoader from models.transformer import create_transformer_model -# ... at top of file you already import ProfilerActivity, so just add record_function, schedule if needed. - -def process_week_data_preds(week_number: int, plays: pd.DataFrame) -> pd.DataFrame: +def process_week_data_preds(rawLoader: RawDataLoader, s: int, w: int, plays: pd.DataFrame) -> pd.DataFrame: """ Loads, cleans, and labels a single week's tracking data. Inputs: - - week_number: Week to process. + - rawLoader: Data laoder object to retrieve weekly tracking data. + - s: Season number to process. + - w: Week number to process. - plays: Play metadata with labels. Outputs: - week_df: Cleaned and labeled tracking rows for the requested week. """ - # Swap the data root if it's CI (since CI runs with a local runner we are using the actual data location to avoid having the VM do git LFS pulls) - if os.getenv("CI_DATA_ROOT"): - WEEK_PARQUET_PATH = Path(os.getenv("CI_DATA_ROOT")) / f"tracking_week_{week_number}.parquet" - else: - WEEK_PARQUET_PATH = PROJECT_ROOT / "data" / "parquet" / f"tracking_week_{week_number}.parquet" - # Load data - week = pd.read_parquet(WEEK_PARQUET_PATH) - logging.info(f"Finished reading Week {week_number} data") + week = rawLoader.get_tracking_data(weeks=[w], seasons=[s]) # Apply cleaning functions week = rotate_direction_and_orientation(week) @@ -54,12 +45,12 @@ def process_week_data_preds(week_number: int, plays: pd.DataFrame) -> pd.DataFra # week = label_offense_defense_coverage(week, plays) # for specific coverage... currently set to man/zone only week = label_offense_defense_manzone(week, plays) - week["week"] = week_number + week["week"] = w week["uniqueId"] = week["gameId"].astype(str) + "_" + week["playId"].astype(str) week["frameUniqueId"] = week["gameId"].astype(str) + "_" + week["playId"].astype(str) + "_" + week["frameId"].astype(str) # Add frames_from_snap (to do: make this a function but fine for now) - snap_frames = week[week["frameType"] == "SNAP"].groupby("uniqueId")["frameId"].first() + snap_frames = week[week["event"] == "ball_snap"].groupby("uniqueId")["frameId"].first() week = week.merge(snap_frames.rename("snap_frame"), on="uniqueId", how="left") week["frames_from_snap"] = week["frameId"] - week["snap_frame"] @@ -78,7 +69,7 @@ def process_week_data_preds(week_number: int, plays: pd.DataFrame) -> pd.DataFra # week = pd.concat([week, week_aug]) - logging.info(f"Finished processing Week {week_number} data") + logging.info(f"Finished processing seasson {s} week {w} data") return week @@ -215,83 +206,85 @@ def main() -> None: # Load data rawLoader = RawDataLoader() - _, plays_df, _, _ = rawLoader.get_data(weeks=list(range(1, 10))) + games_df, plays_df, players_df = rawLoader.get_base_data() # Process + predict one week at a time (keeps RAM low) - for week_eval in [9]: - week_df = process_week_data_preds(week_eval, plays_df) - - # Filtering early to shrink memory - week_df = week_df[(week_df["club"] != "football") & (week_df["passAttempt"] == 1)].copy() - - # Polars convert optional for speed - tracking_df_polars = pl.DataFrame(week_df) - - # Stream predictions to CSV in batches - weekly_predictions_path_csv = PROJECT_ROOT / "data" / "inference" / f"week{week_eval}_preds.csv" - wrote_header = False - batch = [] - - # Iterate unique frames without building a giant Python set - list_ids = pd.unique(week_df["frameUniqueId"].values) - - logging.info(f"Starting loop for week {week_eval}...") - for idx, frame_id in enumerate(list_ids, start=1): - if idx % 20000 == 0: - logging.info(f"Processed {idx}/{len(list_ids)} frames ({100 * idx / len(list_ids):.1f}%)") - - # Grab frame rows (polars or pandas) - if tracking_df_polars is not None: - frame = tracking_df_polars.filter(pl.col("frameUniqueId") == frame_id).to_pandas() - else: - frame = week_df.loc[week_df["frameUniqueId"] == frame_id] - - # Lightweight tensor build - frame_tensor = prepare_tensor(frame) - if frame_tensor is None: - continue - - # Save the first tensor for profiling later - if idx == 1: - torch.save(frame_tensor, PROJECT_ROOT / "data" / "inference" / "example_frame_tensor.pt") - - # Move to device and run inference - frame_tensor = frame_tensor.to(device, non_blocking=True) - with torch.no_grad(): - outputs = model(frame_tensor) # [1, 2] - probabilities = torch.softmax(outputs, dim=1).cpu().numpy()[0] - zone_prob, man_prob = float(probabilities[0]), float(probabilities[1]) - pred = 0 if zone_prob > man_prob else 1 - actual = int(frame["pff_manZone"].iloc[0]) if "pff_manZone" in frame.columns and not pd.isna(frame["pff_manZone"].iloc[0]) else -1 - - play_id = "_".join(frame_id.split("_")[:2]) - frame_num = int(frame_id.split("_")[-1]) - - batch.append({"frameUniqueId": frame_id, "uniqueId": play_id, "frameId": frame_num, "zone_prob": zone_prob, "man_prob": man_prob, "pred": pred, "actual": actual}) - - # Flush batch to CSV to keep RAM low - if len(batch) >= config["batch_size"]: + seasons_weeks = {2022: [9]} + for s in seasons_weeks.keys(): + for w in seasons_weeks[s]: + week_df = process_week_data_preds(rawLoader, s, w, plays_df) + + # Filtering early to shrink memory + week_df = week_df[(week_df["club"] != "football") & (week_df["passAttempt"] == 1)].copy() + + # Polars convert optional for speed + tracking_df_polars = pl.DataFrame(week_df) + + # Stream predictions to CSV in batches + weekly_predictions_path_csv = PROJECT_ROOT / "data" / "inference" / f"week{w}_preds.csv" + wrote_header = False + batch = [] + + # Iterate unique frames without building a giant Python set + list_ids = pd.unique(week_df["frameUniqueId"].values) + + logging.info(f"Starting loop for week {w}...") + for idx, frame_id in enumerate(list_ids, start=1): + if idx % 20000 == 0: + logging.info(f"Processed {idx}/{len(list_ids)} frames ({100 * idx / len(list_ids):.1f}%)") + + # Grab frame rows (polars or pandas) + if tracking_df_polars is not None: + frame = tracking_df_polars.filter(pl.col("frameUniqueId") == frame_id).to_pandas() + else: + frame = week_df.loc[week_df["frameUniqueId"] == frame_id] + + # Lightweight tensor build + frame_tensor = prepare_tensor(frame) + if frame_tensor is None: + continue + + # Save the first tensor for profiling later + if idx == 1: + torch.save(frame_tensor, PROJECT_ROOT / "data" / "inference" / "example_frame_tensor.pt") + + # Move to device and run inference + frame_tensor = frame_tensor.to(device, non_blocking=True) + with torch.no_grad(): + outputs = model(frame_tensor) # [1, 2] + probabilities = torch.softmax(outputs, dim=1).cpu().numpy()[0] + zone_prob, man_prob = float(probabilities[0]), float(probabilities[1]) + pred = 0 if zone_prob > man_prob else 1 + actual = int(frame["pff_manZone"].iloc[0]) if "pff_manZone" in frame.columns and not pd.isna(frame["pff_manZone"].iloc[0]) else -1 + + play_id = "_".join(frame_id.split("_")[:2]) + frame_num = int(frame_id.split("_")[-1]) + + batch.append({"frameUniqueId": frame_id, "uniqueId": play_id, "frameId": frame_num, "zone_prob": zone_prob, "man_prob": man_prob, "pred": pred, "actual": actual}) + + # Flush batch to CSV to keep RAM low + if len(batch) >= config["batch_size"]: + pd.DataFrame(batch).to_csv(weekly_predictions_path_csv, mode="a", header=not wrote_header, index=False) + wrote_header = True + batch.clear() + + # Flush tail + if batch: pd.DataFrame(batch).to_csv(weekly_predictions_path_csv, mode="a", header=not wrote_header, index=False) - wrote_header = True batch.clear() - # Flush tail - if batch: - pd.DataFrame(batch).to_csv(weekly_predictions_path_csv, mode="a", header=not wrote_header, index=False) - batch.clear() - - logging.info(f"Finished week {week_eval}... saved to week{week_eval}_preds.csv\n") + logging.info(f"Finished week {w}... saved to week{w}_preds.csv\n") - # Merge week_df with preds (per-week, small) - preds_week = pd.read_csv(weekly_predictions_path_csv, usecols=["frameUniqueId", "zone_prob", "man_prob", "pred", "actual"]) - tracking_preds = week_df.merge(preds_week, on="frameUniqueId", how="left") - tracking_preds.to_csv(PROJECT_ROOT / "data" / "inference" / f"tracking_week_{week_eval}_preds.csv", index=False) + # Merge week_df with preds (per-week, small) + preds_week = pd.read_csv(weekly_predictions_path_csv, usecols=["frameUniqueId", "zone_prob", "man_prob", "pred", "actual"]) + tracking_preds = week_df.merge(preds_week, on="frameUniqueId", how="left") + tracking_preds.to_parquet(PROJECT_ROOT / "data" / "inference" / f"tracking_s{s}_w{w}_preds.parquet", index=False) - # Free RAM before next week - del week_df, tracking_df_polars, preds_week, tracking_preds - import gc + # Free RAM before next week + del week_df, tracking_df_polars, preds_week, tracking_preds + import gc - gc.collect() + gc.collect() # If profiling flag is set, run profiler if args.profile: diff --git a/src/load_data.py b/src/load_data.py index 0839d87..e351ccf 100644 --- a/src/load_data.py +++ b/src/load_data.py @@ -37,6 +37,8 @@ def __init__(self) -> None: self.players_df = None self.location_data_df = None + self._load_base_data() + def _load_parquet(self, filepath: Path) -> pd.DataFrame | None: """ Loads a parquet file with basic error handling. @@ -56,7 +58,7 @@ def _load_parquet(self, filepath: Path) -> pd.DataFrame | None: def _load_base_data(self) -> None: """ - Pulls games, plays, and players tables into memory. + Concats 2021 and 2022 base data files: games, plays, players into a single dataframe that has a new season column. Inputs: - None. @@ -65,11 +67,24 @@ def _load_base_data(self) -> None: - Populates internal DataFrame attributes. """ - self.games_df = self._load_parquet(self.DATA_PATH / "games.parquet") - self.plays_df = self._load_parquet(self.DATA_PATH / "plays.parquet") - self.players_df = self._load_parquet(self.DATA_PATH / "players.parquet") + games_2021_df = self._load_parquet(self.DATA_PATH / "2021" / "games.parquet") + games_2022_df = self._load_parquet(self.DATA_PATH / "2022" / "games.parquet") + self.games_df = pd.concat([games_2021_df, games_2022_df], ignore_index=True) + + players_2021_df = self._load_parquet(self.DATA_PATH / "2021" / "players.parquet") + players_2021_df["season"] = 2021 + players_2022_df = self._load_parquet(self.DATA_PATH / "2022" / "players.parquet") + players_2022_df["season"] = 2022 + self.players_df = pd.concat([players_2021_df, players_2022_df], ignore_index=True) - def _load_tracking_data(self, weeks: Iterable[int]) -> None: + plays_2021_df = self._load_parquet(self.DATA_PATH / "2021" / "plays.parquet") + plays_2021_df["season"] = 2021 + plays_2021_df["pff_manZone"] = plays_2021_df["pff_passCoverageType"] + plays_2022_df = self._load_parquet(self.DATA_PATH / "2022" / "plays.parquet") + plays_2022_df["season"] = 2022 + self.plays_df = pd.concat([plays_2021_df, plays_2022_df], ignore_index=True) + + def _load_tracking_data(self, weeks: list[int], seasons: list[int]) -> None: """ Loads weekly tracking parquet files and concatenates them. @@ -81,18 +96,38 @@ def _load_tracking_data(self, weeks: Iterable[int]) -> None: """ weekly_dfs = [] - - for week in weeks: - filepath = self.DATA_PATH / f"tracking_week_{week}.parquet" - df = self._load_parquet(filepath) - if df is not None: - weekly_dfs.append(df) - else: - print(f"Skipping week {week} due to load error.") + for season in seasons: + for week in weeks: + filepath = self.DATA_PATH / f"{season}" / f"tracking_week_{week}.parquet" + df = self._load_parquet(filepath) + df["season"] = season + + # 2021 data doesn't have club which we use in processing, assign that to team + if season == 2021: + df["club"] = df["team"] + if df is not None: + weekly_dfs.append(df) + else: + print(f"Skipping week {week} due to load error.") self.location_data_df = pd.concat(weekly_dfs, ignore_index=True) - def get_data(self, weeks: Iterable[int] = range(1, 10)) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]: + def get_base_data(self) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: + """ + Returns concataned games_df, players_df, plays_df across two seasons + + Inputs: + - None + + Outputs: + - games_df, plays_df, plays_df, players_df in that order. + """ + + self._load_base_data() + + return (self.games_df, self.plays_df, self.players_df) + + def get_tracking_data(self, weeks: Iterable[int] = range(1, 10), seasons: Iterable[int] = [2021, 2022]) -> pd.DataFrame: """ Loads requested datasets and returns them as a tuple. @@ -103,10 +138,9 @@ def get_data(self, weeks: Iterable[int] = range(1, 10)) -> tuple[pd.DataFrame, p - games_df, plays_df, players_df, location_data_df in that order. """ - self._load_base_data() - self._load_tracking_data(weeks=weeks) + self._load_tracking_data(weeks=weeks, seasons=seasons) - return (self.games_df, self.plays_df, self.players_df, self.location_data_df) + return self.location_data_df def main() -> None: @@ -121,7 +155,9 @@ def main() -> None: """ loader = RawDataLoader() - games_df, plays_df, players_df, location_data_df = loader.get_data(weeks=[1, 2]) + games_df, plays_df, players_df = loader.get_base_data() + tracking_df = loader.get_tracking_data(weeks=[1, 2]) + print(tracking_df.head()) if __name__ == "__main__": diff --git a/src/process_predictions.py b/src/process_predictions.py new file mode 100644 index 0000000..bb6a82a --- /dev/null +++ b/src/process_predictions.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python +""" +Processes predictions to create various plots and animations to interpret model results. +""" + +import logging + +import matplotlib.animation as animation +import matplotlib.pyplot as plt +import pandas as pd +from matplotlib.ticker import AutoMinorLocator, MultipleLocator +from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix + +from common.args import parse_args +from common.decorators import set_time_decorators_enabled, time_fcn +from common.paths import PROJECT_ROOT +from load_data import RawDataLoader + + +@time_fcn +def plot_accuracy_across_frames(s: int, w: int, df: pd.DataFrame) -> None: + """ + Plots accuracy pre snap and a few moments post SNAP for all provided plays in a week. + + Inputs: + - s: Season number, used to label plot title. + - w: Week number, , used to label plot title. + - df: Dataframe of predictions for all plays in a specific week. + + Outputs: + - Plot saved to data/inference/*.png. + """ + + # Create a new human readable column + df["seconds_from_snap"] = df["frames_from_snap"] / 10 + + # Group all the rows of the same time step together then take the mean of those rows (1 or 0) to yield accuracy + accuracy_by_frame = df.groupby("seconds_from_snap")["base_correct"].mean() + + # Colors (NGS-ish palette) + BG = "#0b1736" # dark navy + AX_BG = "#121e45" # panel navy + GRID = "#3b4268" # grid gray-blue + LINE = "#00e28e" # neon green + ACCENT = "#39c0ff" # cyan for highlights + TEXT = "#dfe6ff" # off-white text + SUBT = "#9aa5d1" # subdued text + + fig, ax = plt.subplots(figsize=(10, 6), dpi=120, facecolor=BG) + ax.set_facecolor(AX_BG) + + # Plot + ax.plot(accuracy_by_frame.index, accuracy_by_frame.values, marker="o", linestyle="-", linewidth=2.8, markersize=5, color=LINE, label="Accuracy") + + # Grid + ticks (match animation) + ax.yaxis.set_major_locator(MultipleLocator(0.1)) + ax.xaxis.set_minor_locator(AutoMinorLocator(4)) + ax.yaxis.set_minor_locator(AutoMinorLocator(2)) + + ax.grid(True, which="major", color=GRID, linewidth=1.0, alpha=0.6) + ax.grid(True, which="minor", color=GRID, linewidth=0.6, alpha=0.35) + + # Vertical line at snap (t = 0) + ax.axvline(0.0, color=ACCENT, linestyle="--", linewidth=1.2, alpha=0.9) + + # Labels & title (dark theme) + ax.set_xlabel("Seconds from Snap", color=TEXT, fontweight="bold") + ax.set_ylabel("Accuracy", color=TEXT, fontweight="bold") + ax.set_title("Season {s}: Week {w} Accuracy", loc="left", pad=8, color=TEXT, fontsize=10) + + # Tick colors + ax.yaxis.set_major_locator(MultipleLocator(0.02)) # tick every 0.02 + ax.tick_params(axis="x", colors=TEXT) + ax.tick_params(axis="y", colors=TEXT) + + # Legend styling + legend = ax.legend(facecolor=AX_BG, edgecolor=GRID) + for text in legend.get_texts(): + text.set_color(SUBT) + + fig.tight_layout() + plt.savefig(PROJECT_ROOT / "artifacts" / f"plot_s{s}_w{w}_accuracy.png", dpi=300) + + +@time_fcn +def animate_play(df: pd.DataFrame, game_id: int, play_id: int, quarter: int, play_description: str, actual_coverage: str, specific_coverage: str) -> None: + """ + Animates a specific play to show accuracy across frames, mostly before snap with a few frames shown post snap. + + Inputs: + - df: Dataframe of predictions for all plays in a specific week. + - game_id: game id to lookup in df. + - play_id: play id to lookup in df. + - quarter: quarter that we are looking at for plot. + - play_description: provided play description. + - actual_coverage: Man or Zone designation. + - specific_coverage: Precise coverage (i.e. Cover-1) + + Outputs: + - Animated plot saved to data/inference/*.gif + """ + + # Create the unique id for lookup in our df + uuid = f"{game_id}_{play_id}" + + # Create the plot df + plot_df = df[(df["uniqueId"] == uuid) & (df["frames_from_snap"] < 300) & (df["frames_from_snap"] > -200)].copy() + plot_df["seconds_from_snap"] = plot_df["frames_from_snap"] / 10 # Create human readable seconds (frames are at 10 Hz) + plot_df = plot_df.sort_values("seconds_from_snap") # Sort for seconds from snap + plot_df = plot_df.dropna(subset=["man_prob"]) # Drop rows with missing y to avoid NaN issues + plot_df = plot_df.iloc[::10, :].reset_index(drop=True) # Downsample (since frame logging at 10 Hz, logging at 1 Hz) + if plot_df.empty: + print(f"[skip] No frames for {uuid}") + return + + # Push to numpy + x_all = plot_df["seconds_from_snap"].to_numpy() + y_all = plot_df["man_prob"].to_numpy() + + # Colors (NGS-ish palette) + BG = "#0b1736" # dark navy + AX_BG = "#121e45" # panel navy + GRID = "#3b4268" # grid gray-blue + LINE = "#00e28e" # neon green + ACCENT = "#39c0ff" # cyan for highlights + TEXT = "#dfe6ff" # off-white text + SUBT = "#9aa5d1" # subdued text + + # Create plot + fig, ax = plt.subplots(figsize=(10, 6), dpi=120, facecolor=BG) + ax.set_facecolor(AX_BG) + (line,) = ax.plot([], [], lw=2.8, color=LINE, label="Man Probability") + dot = ax.plot([], [], marker="o", markersize=6, color=ACCENT, lw=0)[0] + txt = ax.text(0.99, 0.98, "", transform=ax.transAxes, ha="right", va="top", color=SUBT, fontsize=10) + ax.set_xlim(x_all.min(), x_all.max()) + ax.set_ylim(0, 1) + ax.yaxis.set_major_locator(MultipleLocator(0.1)) + ax.grid(True, which="major", color=GRID, linewidth=1.0, alpha=0.6) + ax.grid(True, which="minor", color=GRID, linewidth=0.6, alpha=0.35) + ax.xaxis.set_minor_locator(AutoMinorLocator(4)) + ax.yaxis.set_minor_locator(AutoMinorLocator(2)) + # Create title text + coverage = "Man" if actual_coverage == 1 else "Zone" + title_text = f"Q{quarter}: {play_description}\n Actual Coverage: {coverage} - {specific_coverage}\n" + + # Continued formatting + ax.set_title(title_text, loc="left", pad=8, color=TEXT, fontsize=10) + ax.set_xlabel("Seconds from Snap", color=TEXT, fontweight="bold") + ax.set_ylabel("Man Probability", color=TEXT, fontweight="bold") + ax.axvline(0.0, color=ACCENT, linestyle="--", linewidth=1.2, alpha=0.9) + + # Make tick labels match dark theme + ax.tick_params(axis="x", colors=TEXT) + ax.tick_params(axis="y", colors=TEXT) + + ########################################################## + # Animation + ########################################################## + def init() -> None: + line.set_data([], []) + dot.set_data([], []) + txt.set_text("") + return (line, dot, txt) + + def update(i: int) -> None: + # i goes from 0..len(x_all)-1 + x = x_all[: i + 1] # include current frame + y = y_all[: i + 1] + line.set_data(x, y) + # IMPORTANT: wrap scalars as sequences + dot.set_data([x[-1]], [y[-1]]) + txt.set_text(f"t = {x[-1]:.2f}s P(man) = {y[-1]:.2f}") + return (line, dot, txt) + + ani = animation.FuncAnimation(fig, update, frames=len(x_all), init_func=init, blit=True, interval=1, repeat=False) + fig.tight_layout() + ani.save(PROJECT_ROOT / "artifacts" / f"play_{uuid}.gif", writer="pillow", fps=60) + plt.close(fig) + + +@time_fcn +def get_top_man_coverage_prob_increase_plays(predictions_df: pd.DataFrame) -> pd.DataFrame: + """ + Grab plays that have the largest increase in man coverage probability, end with over 80% man prob and are passes. + + Inputs: + - df: Dataframe of predictions for all plays in a specific week. + + Outputs: + - Datafrane of top 50 plays + """ + # Get a df that is only pre snap and pass attempts, also that is sorted by frames from snap + pre_snap_df = predictions_df[(predictions_df["frames_from_snap"] < 0) & (predictions_df["passAttempt"] == 1)].copy() + pre_snap_df = pre_snap_df.sort_values(["gameId", "playId", "frames_from_snap"]) + + # For each play, get the min (earliest frame) and max (latest pre-snap frame) man_prob + change_df = ( + pre_snap_df.groupby(["gameId", "playId"]) + .agg( + man_prob_start=("man_prob", "first"), # Assign first value of man probability to man_prob_start + man_prob_end=("man_prob", "last"), # Assign last value of man probability to man_prob end + ) + .reset_index() + ) + + # Compute the largest change and create a view that is this sorted + change_df["man_prob_change"] = change_df["man_prob_end"] - change_df["man_prob_start"] + largest_man_prob_increase = change_df.sort_values("man_prob_change", ascending=False) + largest_man_prob_increase = largest_man_prob_increase[largest_man_prob_increase["man_prob_end"] > 0.8] + top_50_plays = largest_man_prob_increase.head(50) + return top_50_plays + + +@time_fcn +def main() -> None: + """ + Driver to process predictions and make plots. + + Inputs: + - None. + + Outputs: + - None. + """ + + # Set logging + logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") + + # Parse command line args + args = parse_args() + if args.profile: + set_time_decorators_enabled(True) + logging.info("Timing decorators enabled") + else: + set_time_decorators_enabled(False) + logging.info("Timing decorators disabled") + + # Make the articats + ARTIFACT_PATH = PROJECT_ROOT / "artifacts" + ARTIFACT_PATH.mkdir(parents=True, exist_ok=True) + + # Load predictions + predictions = [] + for s in [2022]: + for w in [9]: + preds_week = pd.read_parquet(PROJECT_ROOT / "data" / "inference" / f"tracking_s{s}_w{w}_preds.parquet") + predictions.append(preds_week) + print(f"Finished processing seasons {s}, week {w}...") + predictions_df = pd.concat(predictions, ignore_index=True) + predictions_df["base_correct"] = predictions_df["pred"] == predictions_df["actual"] + + # Take 15s before snap and 1.5s post snap + week_truncated_play_df = predictions_df[(predictions_df["week"] == 9) & (predictions_df["frames_from_snap"] < 15) & (predictions_df["frames_from_snap"] > -150)].copy() + + # Create plot + plot_accuracy_across_frames(s=s, w=w, df=week_truncated_play_df) + + # Get top x plays that have the large man coverage prob increase and are pass plays + top_plays_df = get_top_man_coverage_prob_increase_plays(predictions_df) + + # Load in plays df + rawLoader = RawDataLoader() + games_df, plays_df, players_df = rawLoader.get_base_data() + + # Merge + merged_df = predictions_df.merge(plays_df, on=["gameId", "playId"], how="left") + + # Create animations + animation_created = False + for _, row in top_plays_df.iterrows(): + play = merged_df[(merged_df["playId"] == row["playId"]) & (merged_df["gameId"] == row["gameId"])] + if play["passResult"].values[0] == "C": + print(f"gameId: {play['gameId'].iloc[0]}, playId: {play['playId'].iloc[0]}, Q: {play['quarter'].iloc[0]}, {play['playDescription'].iloc[0]}") + animate_play( + df=week_truncated_play_df, + game_id=play["gameId"].iloc[0], + play_id=play["playId"].iloc[0], + quarter=play["quarter"].iloc[0], + play_description=play["playDescription"].iloc[0], + actual_coverage=play["actual"].iloc[0], + specific_coverage=play["pff_passCoverage"].iloc[0], + ) + animation_created = True + + if args.ci and animation_created: + break + + # Create classification report and confusion matrix + y_true = predictions_df["actual"] + y_pred = predictions_df["pred"] + cm_norm = confusion_matrix(y_true, y_pred, labels=[0, 1], normalize="true") + class_names = ["Zone", "Man"] + disp = ConfusionMatrixDisplay(confusion_matrix=cm_norm, display_labels=class_names) + fig, ax = plt.subplots(figsize=(6, 6)) + disp.plot(ax=ax, values_format=".2f", cmap="Blues") + ax.set_title("Confusion Matrix (Normalized by True Label)") + accuracy = (y_true == y_pred).mean() + ax.set_xlabel(f"Predicted label\nAccuracy = {accuracy:.3f}") + ax.set_ylabel("True label") + plt.tight_layout() + plt.savefig(PROJECT_ROOT / "artifacts" / "confusion_matrix.png", dpi=200, bbox_inches="tight") + + +if __name__ == "__main__": + main() diff --git a/src/train_transformer.py b/src/train_transformer.py index 8e00c98..567cc4d 100644 --- a/src/train_transformer.py +++ b/src/train_transformer.py @@ -279,9 +279,7 @@ def run_trial(config: dict[str, float | int], args: argparse.Namespace) -> None: train_losses = [] val_losses = [] val_accuracies = [] - early_stopping_patience = 20 best_val_loss = float("inf") - epochs_no_improve = 0 # If running in CI mode, reduce epochs for speed, we just want to ensure it can actually train, not train a whole model in testing here (for pipelines later) if args.ci: @@ -317,17 +315,11 @@ def run_trial(config: dict[str, float | int], args: argparse.Namespace) -> None: val_losses.append(avg_val_loss) val_accuracies.append(val_accuracy) - # Adding early stopping check (effort to prevent overfitting) + # Update saved model best_model_path = PROJECT_ROOT / "data" / "training" / "transformer.pt" if avg_val_loss < best_val_loss: best_val_loss = avg_val_loss - epochs_no_improve = 0 torch.save(model.state_dict(), best_model_path) - else: - epochs_no_improve += 1 - if epochs_no_improve >= early_stopping_patience: - logging.info("Early stopping triggered") - break # Stop profiler if prof: