From 93d46da1d5c01fb6e069eecf1cb16a600188a944 Mon Sep 17 00:00:00 2001 From: mwever Date: Mon, 30 Mar 2026 11:44:02 +0200 Subject: [PATCH] Added interface for optuna. --- .gitignore | 1 + examples/ablation-example.ipynb | 29 +-- examples/optuna-example.ipynb | 261 +++++++++++++++++++++ examples/tunability-example.ipynb | 78 +++---- pyproject.toml | 9 + src/hypershap/__init__.py | 2 + src/hypershap/optuna_task.py | 265 ++++++++++++++++++++++ tests/test_optuna_task.py | 362 ++++++++++++++++++++++++++++++ uv.lock | 180 +++++++++++++++ 9 files changed, 1124 insertions(+), 63 deletions(-) create mode 100644 examples/optuna-example.ipynb create mode 100644 src/hypershap/optuna_task.py create mode 100644 tests/test_optuna_task.py diff --git a/.gitignore b/.gitignore index a6786d3..5501253 100644 --- a/.gitignore +++ b/.gitignore @@ -212,3 +212,4 @@ __marimo__/ # IDE project files .idea +.DS_Store diff --git a/examples/ablation-example.ipynb b/examples/ablation-example.ipynb index 3c4619a..f7e072c 100644 --- a/examples/ablation-example.ipynb +++ b/examples/ablation-example.ipynb @@ -18,9 +18,8 @@ "id": "initial_id", "metadata": { "collapsed": true, - "ExecuteTime": { - "end_time": "2025-08-19T15:17:31.718520Z", - "start_time": "2025-08-19T15:17:31.336310Z" + "jupyter": { + "is_executing": true } }, "source": [ @@ -73,28 +72,8 @@ "print(\"\")\n", "print(\"Config of interest\", config_of_interest, \"\\nValue\", eval_fun(config_of_interest))" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Baseline Configuration(values={\n", - " 'a': 0.6243561663863,\n", - " 'b': 4,\n", - " 'c': np.str_('Y'),\n", - "}) \n", - "Value 0.7004003054122951\n", - "\n", - "Config of interest Configuration(values={\n", - " 'a': 1.45774946311,\n", - " 'b': 10,\n", - " 'c': np.str_('X'),\n", - "}) \n", - "Value 10.99361700532433\n" - ] - } - ], - "execution_count": 1 + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", diff --git a/examples/optuna-example.ipynb b/examples/optuna-example.ipynb new file mode 100644 index 0000000..62ba70e --- /dev/null +++ b/examples/optuna-example.ipynb @@ -0,0 +1,261 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "74b6c2461143b273", + "metadata": {}, + "source": [ + "# HyperSHAP: Example for Optuna Integration\n", + "\n", + "In this example, we demonstrate how to load data from an [optuna](https://optuna.org/) study directly into HyperSHAP for downstream hyperparameter analysis.\n", + "\n", + "This is useful when you have already run an optuna HPO study and want to understand *why* certain hyperparameters matter more than others \u2014 without having to redefine a `ConfigSpace` manually.\n", + "\n", + "> **Prerequisites:** `optuna` must be installed.\n", + "> ```bash\n", + "> pip install optuna\n", + "> # or\n", + "> pip install hypershap[optuna]\n", + "> ```\n", + "\n", + "## Step 1 \u2014 Run an optuna study\n", + "\n", + "We first set up a small synthetic objective and run an optuna study to mimic a realistic HPO scenario.\n", + "The objective uses a float, an integer, and a categorical hyperparameter \u2014 the same types supported by the optuna integration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "initial_id", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "import math\n", + "\n", + "import optuna\n", + "\n", + "optuna.logging.set_verbosity(optuna.logging.WARNING) # suppress per-trial logs\n", + "\n", + "\n", + "def objective(trial: optuna.Trial) -> float:\n", + " \"\"\"Synthetic objective that mimics a tunable ML algorithm.\n", + "\n", + " Hyperparameters\n", + " ---------------\n", + " a : float in [0.1, 1.5] \u2014 learning-rate-like continuous parameter\n", + " b : int in [2, 10] \u2014 depth-like integer parameter\n", + " c : str in {\"X\", \"Y\"} \u2014 algorithm variant (categorical)\n", + " \"\"\"\n", + " a = trial.suggest_float(\"a\", 0.1, 1.5)\n", + " b = trial.suggest_int(\"b\", 2, 10)\n", + " c = trial.suggest_categorical(\"c\", [\"X\", \"Y\"])\n", + "\n", + " # Variant X: performance mainly driven by b, slightly by a\n", + " if c == \"X\":\n", + " return math.sin(a) + b\n", + " # Variant Y: interaction between a and b dominates\n", + " return math.cos(a * b) + 1.5\n", + "\n", + "\n", + "# Run a maximisation study with 200 trials\n", + "study = optuna.create_study(direction=\"maximize\", sampler=optuna.samplers.TPESampler(seed=42))\n", + "study.optimize(objective, n_trials=200)\n", + "\n", + "print(f\"Completed trials : {len(study.trials)}\")\n", + "print(f\"Best value : {study.best_value:.4f}\")\n", + "print(f\"Best params : {study.best_params}\")" + ] + }, + { + "cell_type": "markdown", + "id": "97bf13e91a18e32e", + "metadata": {}, + "source": [ + "## Step 2 \u2014 Load the study into HyperSHAP\n", + "\n", + "`from_optuna_study` is the main entry point for the optuna integration. It:\n", + "\n", + "1. Extracts a `ConfigurationSpace` from the trial distributions.\n", + "2. Converts all completed trial results into `(Configuration, float)` pairs.\n", + "3. Fits a surrogate model (default: `RandomForestRegressor`) on those pairs.\n", + "4. Returns an `ExplanationTask` ready for HyperSHAP analysis.\n", + "\n", + "For **minimisation** studies (`direction=\"minimize\"`) the objective values are automatically negated so that HyperSHAP's *higher-is-better* convention is respected. Pass `negate=False` to disable this." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31b290fdd2788b74", + "metadata": {}, + "outputs": [], + "source": [ + "from hypershap import HyperSHAP, from_optuna_study\n", + "\n", + "# One-liner: study \u2192 ExplanationTask\n", + "explanation_task = from_optuna_study(study)\n", + "\n", + "print(\"Config space HPs :\", explanation_task.get_hyperparameter_names())\n", + "print(\"Number of HPs :\", explanation_task.get_num_hyperparameters())\n", + "\n", + "hypershap = HyperSHAP(explanation_task=explanation_task)" + ] + }, + { + "cell_type": "markdown", + "id": "6d398a1ada8f99a2", + "metadata": {}, + "source": "## Step 3 \u2014 Tunability analysis\n\nFor tunability we need a **baseline configuration** \u2014 the starting point from which we measure how much tuning each hyperparameter can improve performance. A natural choice is the *default configuration* of the inferred `ConfigurationSpace` (i.e. the midpoint/default of each hyperparameter range), which represents the algorithm before any tuning has taken place." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a248ad7cf028ced8", + "metadata": {}, + "outputs": [], + "source": [ + "# Use the default configuration of the inferred ConfigSpace as the baseline \u2014\n", + "# this represents the algorithm with no tuning applied.\n", + "default_config = explanation_task.config_space.get_default_configuration()\n", + "print(\"Default (baseline) config:\", default_config)\n", + "\n", + "iv_tunability = hypershap.tunability(baseline_config=default_config)\n", + "print(iv_tunability)" + ] + }, + { + "cell_type": "markdown", + "id": "9c0b58a6ff43f6f1", + "metadata": {}, + "source": [ + "### Visualisations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27ff3fe327082acf", + "metadata": {}, + "outputs": [], + "source": [ + "hypershap.plot_si_graph()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87fcba5335b5aa7c", + "metadata": {}, + "outputs": [], + "source": [ + "hypershap.plot_stacked_bar()" + ] + }, + { + "cell_type": "markdown", + "id": "c3a1b2d0e4f5a6b7", + "metadata": {}, + "source": "## Step 4 \u2014 Ablation analysis\n\nWe can also run an ablation analysis to understand which hyperparameters are responsible for the performance gain from the default configuration to the best configuration found by optuna." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5e6f7a8b9c0d1e2", + "metadata": {}, + "outputs": [], + "source": [ + "from ConfigSpace import Configuration\n", + "\n", + "best_config = Configuration(\n", + " explanation_task.config_space,\n", + " values=study.best_params,\n", + ")\n", + "\n", + "iv_ablation = hypershap.ablation(\n", + " config_of_interest=best_config, # optimized config found by optuna\n", + " baseline_config=default_config, # default / untuned starting point\n", + ")\n", + "print(iv_ablation)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0a1b2c3d4e5f6a7", + "metadata": {}, + "outputs": [], + "source": [ + "hypershap.plot_waterfall()" + ] + }, + { + "cell_type": "markdown", + "id": "b8c9d0e1f2a3b4c5", + "metadata": {}, + "source": [ + "## Step 5 \u2014 Advanced: using the lower-level helpers\n", + "\n", + "If you need more control \u2014 e.g. to inspect the inferred `ConfigurationSpace`, filter trials manually, or pass a custom surrogate model \u2014 you can use the lower-level helpers directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6e7f8a9b0c1d2e3", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.ensemble import GradientBoostingRegressor\n", + "\n", + "from hypershap.optuna_task import study_to_config_space, study_to_data\n", + "\n", + "# 1. Inspect the inferred configuration space\n", + "cs = study_to_config_space(study)\n", + "print(\"Inferred ConfigurationSpace:\")\n", + "print(cs)\n", + "\n", + "# 2. Convert trials to (Configuration, float) pairs \u2014 apply custom filtering if needed\n", + "data = study_to_data(study, config_space=cs)\n", + "print(f\"\\nConverted {len(data)} trials to (Configuration, float) pairs.\")\n", + "\n", + "# 3. Build an ExplanationTask with a custom surrogate model\n", + "from hypershap.task import ExplanationTask\n", + "\n", + "custom_task = ExplanationTask.from_data(\n", + " config_space=cs,\n", + " data=data,\n", + " base_model=GradientBoostingRegressor(n_estimators=200, random_state=0),\n", + ")\n", + "\n", + "hs_custom = HyperSHAP(explanation_task=custom_task)\n", + "iv_custom = hs_custom.tunability(baseline_config=default_config)\n", + "print(\"\\nTunability with GradientBoostingRegressor surrogate:\")\n", + "print(iv_custom)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbformat_minor": 5, + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/tunability-example.ipynb b/examples/tunability-example.ipynb index 0940988..9167367 100644 --- a/examples/tunability-example.ipynb +++ b/examples/tunability-example.ipynb @@ -15,14 +15,29 @@ }, { "cell_type": "code", + "execution_count": 1, "id": "initial_id", "metadata": { - "collapsed": true, "ExecuteTime": { "end_time": "2025-08-14T08:12:25.618012Z", "start_time": "2025-08-14T08:12:25.252223Z" - } + }, + "collapsed": true }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Baseline Configuration(values={\n", + " 'a': 0.6243561663863,\n", + " 'b': 4,\n", + " 'c': np.str_('Y'),\n", + "}) \n", + "Value 0.7004003054122951\n" + ] + } + ], "source": [ "from __future__ import annotations\n", "\n", @@ -56,22 +71,7 @@ "baseline_config = cs.sample_configuration()\n", "\n", "print(\"Baseline\", baseline_config, \"\\nValue\", eval_fun(baseline_config))" - ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Baseline Configuration(values={\n", - " 'a': 0.6243561663863,\n", - " 'b': 4,\n", - " 'c': np.str_('Y'),\n", - "}) \n", - "Value 0.7004003054122951\n" - ] - } - ], - "execution_count": 1 + ] }, { "cell_type": "markdown", @@ -85,6 +85,7 @@ }, { "cell_type": "code", + "execution_count": null, "id": "31b290fdd2788b74", "metadata": { "ExecuteTime": { @@ -92,13 +93,6 @@ "start_time": "2025-08-14T08:12:25.631169Z" } }, - "source": [ - "from hypershap.hypershap import HyperSHAP\n", - "from hypershap.task import ExplanationTask\n", - "\n", - "explanation_task = ExplanationTask.from_function(config_space=cs, function=eval_fun)\n", - "hypershap = HyperSHAP(explanatiown_task=explanation_task)" - ], "outputs": [ { "name": "stderr", @@ -109,16 +103,25 @@ ] } ], - "execution_count": 2 + "source": [ + "from hypershap.hypershap import HyperSHAP\n", + "from hypershap.task import ExplanationTask\n", + "\n", + "explanation_task = ExplanationTask.from_function(config_space=cs, function=eval_fun)\n", + "hypershap = HyperSHAP(explanation_task=explanation_task)" + ] }, { "cell_type": "markdown", "id": "6d398a1ada8f99a2", "metadata": {}, - "source": "# HyperSHAP Tunability" + "source": [ + "# HyperSHAP Tunability" + ] }, { "cell_type": "code", + "execution_count": 3, "id": "a248ad7cf028ced8", "metadata": { "ExecuteTime": { @@ -126,10 +129,6 @@ "start_time": "2025-08-14T08:12:26.469906Z" } }, - "source": [ - "iv = hypershap.tunability(baseline_config=baseline_config)\n", - "print(iv)" - ], "outputs": [ { "name": "stdout", @@ -150,7 +149,10 @@ ] } ], - "execution_count": 3 + "source": [ + "iv = hypershap.tunability(baseline_config=baseline_config)\n", + "print(iv)" + ] }, { "cell_type": "markdown", @@ -164,6 +166,7 @@ }, { "cell_type": "code", + "execution_count": 4, "id": "27ff3fe327082acf", "metadata": { "ExecuteTime": { @@ -171,22 +174,21 @@ "start_time": "2025-08-14T08:12:26.728764Z" } }, - "source": [ - "hypershap.plot_si_graph()" - ], "outputs": [ { "data": { + "image/png": "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uRVBFgAAoB6jRo2KbLRau3at/KS6ulrLli0jyAIAANiq9njaVLQXpNLq1asjm70IsgAAAJYyJ2n17t1bixcvlp8sXrw4stFrxIgRciuCLAAAQAOOP/54Pf/8877qk33++ec1bNgwtW7dWm5FkAUAAGjAZZddpjVr1uj111+XH2zevFnPPvusLr/8crkZQRYAAKABxx13nIYMGaKZM2fKD2bNmqUWLVroggsukJsRZAEAABpgekWnTp0aqVKaaqWXVVZW6t57740cvuDmtgKDIAsAABADU51s2bJlpFrpZc8884y2bdsWCe5uR5AFAACIgalOmiqlCbKlpaXyonA4rDvuuEPjxo3T4MGD5XYEWQAAgBhde+21Ki4u1o033igvuv/++/Xee+/p5z//uWxAkE2GGpeO6vDRCBEAAOKhX79++sMf/qC//e1vkcDnJZs2bdJ1112nyZMn67TTTpMNAmFTQ0ZiTf6b9NR8uU6vDtLyO1O9CgAArFJTUxOZK7tr1y59+OGHkd39tguHwzr99NO1fPlyrVy5Um3atJENqMgmQ1WNXMmt6wIAwMXS0tIit+A3btzomRaD+++/X3Pnzo1MK7AlxBoE2WSodukt/GqCLAAATTFgwIAvWwzeeecd2WzDhg1fthScccYZsglBNhmqquVKVGQBAGgyE/5Mi8GZZ54ZuR1vo507d0b6YfPz83X77bfLNgRZP1c+3bouAAAsaTEwM1e7d++uk08+WevWrZNNiouLIyG2qKhI8+bNs6qloBZB1s+VT7dWigEAsISpZL7yyiuRgxJOPPFEffrpp7LBnj17dMopp0TCt1l///79ZSOCrJ8Do1sDNgAAFunUqZPefPNNtWrVKtJqYCYZuNnWrVt1wgknRELsa6+9pqFDh8pWBFk/b/Zy63xbAAAs061bN7399tvq0aNHJCTOnj07MtLKbebOnasxY8ZE2grMJrWRI0fKZgRZv/eiurVaDACAZdq3b6833nhDZ599ti677LLIXFZzyIAbFBcX69JLL430xA4aNEgffPCBjjrqKNmOIOv3sEh7AQAAcdO6dWvdd999evHFF7VixQoNGTJEc+bMSWl1du7cuZF1PP7445o1a1bk380GNS8gyPo9LLp5bQAAWMrMYzVB9qyzzvqyEjp//vykBtpPPvlEF110UeS1Bw4cGFmPqRQHAgF5BUHW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", "text/plain": [ "
" - ], - "image/png": "iVBORw0KGgoAAAANSUhEUgAAArIAAAKyCAYAAAApeT2AAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjUsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvWftoOwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAUkxJREFUeJzt3Ql8VOXZ9/H/THaWhLDvi4DIIluAalFRimtbaqu+VusjuCOor0v32vp01da+2iqgIrjXVq37VsR9ZQuIbCoIssoeAmRPZt7PPYdYxEwySWY59zm/7/PMJxommfsM6cd/rnPd1x0Ih8NhAQAAAJYJpnoBAAAAQFMQZAEAAGAlgiwAAACsRJAFAACAlQiyAAAAsBJBFgAAAFYiyAIAAMBKBFkAAABYiSALAAAAKxFkAQAAYCWCLAAAAKxEkAUAAICVCLIAAACwEkEWAAAAViLIAgAAwEoEWQAAAFiJIAsAAAArEWQBAABgJYIsAAAArESQBQAAgJUIsgAAALASQRYAAABWIsgCAADASgRZAAAAWIkgCwAAACsRZAEAAGAlgiwAAACsRJAFAACAlQiyAAAAsFJ6qheAOtSEpMpq51FV+6iRwmHnoYAUCEjpQSkzXcowjzQpK0NKT0v16gEAAJKCIOuG0FpS7jz2l0l7S6Q9B5wQW13jBFjzscYE2UO+LmDq6UEnwKYffJh/btNKattKapUttcyWWuU4nwcAAPCYQDgcKfEhmUxILTog7dkvbd0jFZdK5ZVSWaUTbJvLVGtzMqXsTCfQdm0rtWvtBNycrHhcAQAAQMoRZJPFBNTd+6QviqQte6T9pU4FNpSEt99Ub011tnWO1DnfCbYd85yWBAAAAEsRZBPNVFm3F0kbd0nbipzqa6rfchNo2+dKvTtKndtIrVukdj0AAABNQJBNZIDdtFNau82pxJp/dxtTkTXtBn06OaE2l0ALAADsQZBNRP/r5l3Smi+cCmxFlVwvLSh1yJP6d5F6dZRa0EcLAADcjyAbT1t3Sx9vdnpg3ViBjaVC26mNdFQ3qVcHKY1pBwAAwL0IsvFQWiGt2Sp9ukUqKpH1zMawvp2lo7pLbVqmejUAAAB1IsjGowq7arPTD2tmvnpFMOBUZwf1lHpTnQUAAO5DkG0qc0iBqcKu2OjMhI2nvQekzbulTbulLbulA2VOSDYnfJkxXqantfYABDMv1ozT6t5e6t5O6tgm/tVZ02pgAq15LQAAAJcgyDaF6X9dtVFavdlpK2iOUEj6ZKv0+Q5pyy4nwB4ob/r3M8fUdjPBtp3Uo4M0qIfzueYwgblfF2lobymPVgMAAOAOBNnGKi6RPvpcWvtF81oJ9pVKH3wiLfhE2lemhDEhdmRfaexRzmEIzTktrGd7aVif5n0fAACAOCHINoY5UnbxWmnDzqYfamAC8HurpZUbk3Oq16HMvNhvHuVUVk17QlOYvlkTjHu0j/fqAAAAGoUgG6vdJsSukTaaENuEr1/wqfTWCmlHsVKuVbZ07FHSSUdLmU04prZDrlTQT+rZIRGrAwAAiAlBNtZK7CJTid3RtK997F3ps21Nemnz17MtXK4SVas8XKMKhZShoLIDacpRmroEspUeaGJ1tV1r6YfHO5XapoTZ0f2dTWYAAAApQJCNpZfVVFM/3974SqxpIXhxsXPaVwzMX8X6cIkKa4pUGNqjwvDeyKOoOvrmr5xghoant1FBOE8FaW1VEMzXwGBu7OE2IOm4QdIZo5xNXY3RMU86ZgA9swAAICUIsvUpr3R6Ys1pXY3pZ21kFfbDmiLNrFqrJ8NbtedgaO3WsbMKxozWqDGjNXToUOXl5Sk7O1uZmZmqrq5WWVmZSkpK9PHHH2vx4sUqnL9Qn67/7Mtwe0paJ01N76sJaZ0UNBu1ElWdNe0FJsxycAIAAEgygmw0Zl7rsvXOhIIYK6oRH3wsPb+owa8xbQL/rt6kmaF1+qByp7p16qxJl1yssWPHqqCgQJ06Nf52/759+7R06VItXLhQjzzwoD5atVL9MvJ0ZbCPJmf0VttAVmzV2e+Mjn0zmAnJZs7sqP7MmQUAAElFkI3mky1ONbakETNdX1gkvbmi3qfsC1fplsrVujf0uXZVl2nCSeM19eqr9N3vflfp6U3YeBWF+Wv94IMPNHP6DD3xxBMKhsI6L9hDv8karN7BBqqnA7pJk8bHvhHMtCSYSQjD+3ACGAAASBqCbF127JXeXS3t2hfb881b+O/3nV7aerxSvU2XVhdqT1qNLptyhaZMmaIBAwYo0Xbs2KH77rtP0//2d+3dtVu3pg/VFRl962856NNRuvjk2Kus5gQw02LQt3Pc1g0AAFAfguzhKqqc9oBPt8b2fPP2Pfq2tHRdvVXYH1d8qHur1kUqsLPvv0+9evVSsu3fv18/+fGPdc+sWRqf2VlzMkbVX53t1k6aclrsYbZLvnT8YPplAQBAUjRxbpOHmQMLzIEHsXrqg3pDrKnCDql4Rf9M36a7775br7z2akpCrNG6dWvdfc89mjdvnta2T9OQ8rm6q3KtQtF+l9myW5ozL/Ye4W17pdWbpOpmnHgGAAAQI4Ls4S0FZkKBqcrG4qXFzjGzdTCF7t9XrNSpZW9pwHFjtGL1Kl1xxRUKxDJBIMEmTJigFR+v1gWXXKSpFYU6p+IDVYSjhM/Pd0gPvO5sfmuICcTrtjXuFwEAAIAmIsjWqqlx2gnMCV6xWPKZ9PryqCH2hooP9ZvKFfr973+f0ipsQ9XZZ555Ri8Fdui7Fe+pJByl8vrpFun5hbF945IK55eBA43YJAcAANAEBNlaW4ukTbtie+7+Munp+VFD7JSKQv2teo1mzpypG2+80RVV2Gi+973v6T+vzNX8jH06pfIdHQhHqUabzW+m2hprZdsc5QsAAJBABFmjssrpjTUBNRZmQkFZZZ0h9qcVyzSr6jPdf//9uvLKK2WDcePG6dU3Xtfy9BL9oL42A3PIQyz9slU10tqtzqloAAAACUKQNbbucTY2xdpSsHJjnX/058qP9deqT3TnnXdq0qRJssmYMWP0/Esv6p3AHp1fsaDuDWCm7cIcuRuLnfuk9dvjvk4AAIBaBFlTPTRHyZZWNKulYH7NLv2qanmkleCqq66SjUxl9rEnHtdTVZt0V9Xaup/0XowtBmZzmAmyVGUBAECCEGR3FUs7ipvVUlAWrtbkqkKNGlGgm266STabOHGirpwyRT+rXqF1oQPNazEoOiBtK4r7GgEAAAyC7ObdsfXGrtoYtaXgpoqV+lyleuCRh+J6zGyq/OXWW9WhSyddUrU4eovBax/FVu0247tinUMLAADQCP4OsntLnCAbi3dWR20p+H/Vn+i3v/+dBg4cKC9o1aqV5jz4gN6s3B69xWD+J7EdfGCO+TVTDAAAAOLM30HWBCxz+7shO4ulNVvrbSm44YYb5CXjx4+vv8WgpFxa9nnD38jMk92yJyFrBAAA/ubfIGtumZtpBbFUFd//uM5Pz6lar7U1+3X/ww96oqWgrhaDth3b66bKFXU/4f26q9Rfs32vc1ACAABAHPk3yBaXSntiqMaa/s7Fa+ucGTsztE7fP/NMDRo0SF5kWgyuvu5aPV6zWbtCdQRRcxRtLNVW04NcFOOJaQAAADHyb5A1LQWxbPJauq7OSQVv1ezU6qq9mnrVNHnZRRddpEBamu6rWtf0qmxpeexH/wIAAMTIv0HWjNyqiHIcawxBbWb1Wh3Vr79OPPFEeVn79u117g/P1d3hz1UTDsUc9L/CDD4wbRzmBDUAAIA48WeQNYHKzI+NZTNYHbfOt4bK9HT1Fk295moFAgF53dRp07S+ap/m1myru/ViRd1jyb626cs8AAAA4sSfQfZAhVQWQ3Vw4646Pz27ap2ysrJ04YUXyg/M8bUjhw7TjOrP6n7Cpp0Nf5PySoIsAACIK38GWTM6qiyGXfSb6w6yL2qbJn7/TOXl5ckPTNX5wosv0qs121UZrmPKQyyzeMurCLIAACCu/BlkD5Q5waohdQS0qnBIy6r3RqqUfjJ69GhVhmq0IlRHS8YXe6RQHSeAHT7ubA8bvgAAQPwEfTt6q66jV+uaM3uYVaFiVYSqNWrUKCXL5MmTNWTIEKXS8OHDFQwGVVhTVPdRtGZWbEOKS6QqjqsFAADx4c8gG8stbjPVwGxkOkxhqChyq90EOz9p0aKFBvUfELn+xrRhfC3wVhBkAQBAfPgvyEbCVAxtBZvqDmamInlU3/6RwwL8puCYMVocKG7U+/UV5hQ1KrIAACBOgr4cvRXLsbRb697AVBgojgS6VHj55ZcjLQbZ2dkqKCjQ/Pnzk/r65jWXVxfVveGrjjaMOn+JIMgCAIA48V+QNWEqliBbWveQ/zWhAyk5kvaLL77Q1KlT9ZOf/ESPP/54ZPzXqaeeqh07diRtDea6zYavjeHSr/9hQ4ciGOZ9r6NdAwAAoCn8F2RNmKoJxfa8OpSFqtWyZUsl2549ezRnzhxNmjRJEydOjFRnw+Gwbr/99qStofa6y+qqyMZSaTWTDWL5JQIAACAG/guyZlhBQxMLaiu3h39pOKyyUJVycnKUbGZm7fjx47/y7xMmTNCCBQuStoba6y5XXUE2hoBq3vcY3noAAIBY+C/IxpqkVn792NXqg1+bkZGhZOvQocPXPtepU6dIy0Gy1F53ZbiOivb+svj9EgEAABADHwbZQJO/Ml2ByP9VVMRwKlic7dz59WNgt2/fri5duiRtDbXXnRVIa/pbH2z6+w8AAODvIGtyVKBpYcrMj81JS1d5efKPWi0uLtbrr7/+lX9/9dVX9Y1vfCNpa6i97hw1NchG3vz4LgoAAPiW/4JsepqUFsNlt6l7Q1fLYIb27dunZGvbtq0uueQSPfTQQ3ruued0+umnR3p2r7322qStofa6WzS1Imve93T//cgBAIDE8F+qyEx3wmxDenes89ODAq310bJlSjbTQjB9+nTdcsstOueccyLV0blz50b6ZJPlo48+Usu0TPUMtGjaNzDvu3n/AQAA4sB/qSLWIDu4p/Th+q99uiCcp6cXLFIyPfDAA1/+87e//W2lSuHixRqe1kZpgTp+/+nXOcYgm/yNcgAAwJv8V5E1YSo7hjDVo32dny5Ia6v1mzdG5rr6TeGCRZEgX6fhRzT8DTLSnAcAAEAc+C/IGq1jmAPbPrfOTxek5Uc+LlmyRH5iNpet3bBeo9La1v2EvjFMTzAhNouKLAAAiA9/Btm8lrFt+KpD/0BrtU7L1OLFi+UntcG9IOgE+a/pUHfw/4r8VrG1dQAAAMTAn0G2VbaUndnw83K+/pxgIKARafn64L335Sfvv/++WqRlaECwddO+gZkf27ZVvJcFAAB8zJ9BtqUJsjHc4h7ep85PnxXoqpdefimpp2qlUigU0v33ztb3g13r3ugVC/OLg3nfAQAA4sS/Fdk6qq1fU9C3zk9fmNFbmQpq9uzZ8oN58+bpsw2fa2pGv0YF/q8w7zdBFgAAxJE/g6zp0+zUpuHn9a57RmubQKZ+FOyhe2bMVHV1tbxu5vTpGpbZVscG29X9hHFDYttgF8smOwAAgBj5M8ga7fNiq8pGYaqTW7Zv0/PPPy8v27Bhg1548SVNDRwROaK3Tp2jbACrZb6ua1s2egEAgLjyb5A1G49iqRAO6Vnnp4en5evYzA6aeed0edmsWbPUKi1D52fU/T5ENDQb1rRytIthqgEAAEAj+DfImhAbZVbsV3SMcgCApGnBI/TqG6/r7bfflhdt27ZNd02focmBnmoViLI5rn+X2N7r/JZxXx8AAPA3/wZZo0vbhgf0d4geZM9L76XjMjrq4gsnqaSkRF4SDoc15fLLlVFapd9kDY7+xIYqraYbwbQVxDLuDAAAoBH8HWQ75TVcKaxn9qmZKXtf5iht3bxFv/zlL+Uljz76qJ59/nndnT5C7QJZ0Z/YroG5srktnCALAAAQZ/4Osq1ypJ4dnM1I0bStP6j1D7bWn9IH64477vBMi4FpKbh66jSdl9FL38/oXv+TGwqypqIdSwsHAABAI/k7yBqmWpjXIvqft2npnEpVj2syjvRMi8GhLQV3Zo1o+AvqO63LtG307si0AgAAkBAEWdPjWd/4KFOtza//aNUvWww2bdGVV0yJnIRlq+nTp8fWUhBLxdr8WSzzegEAAJqAIJsWlI7oVP8orgbaC2pbDEyYfeQf/9B1114bqWza5pFHHtE111yjGzIGNNxSYJg5vC2ihN2MdKlfZ07zAgAACUOQrZ1e0LN90/tAD/phRk/dlTVSd9x5p379619bFWafeuopTZ40WZdkHKFbs4bF9kX1tRWYSmyvjnFbHwAAwOHSv/YZv1Zl+3aRtuyR9pY0OcgaV2T20wFV68d//KP279+v22+/XcGgu39feOCBB3TJxZfo3PQeuierIPoJXrFWqrMzpP5do1drAQAA4sDdCSuZaiuIdYW4GFoLDnVD5lG6K6tAd95xpyZfOEmlpaVyI9PLe+utt+qiiy7Spem99XDWGKUFGvEjEe19MRXuHu3itk4AAIC6EGRrmQB7ZFdntuzh2tW/2asuUzL76dHsY/TEP/+l4UOO1rvvvis3Wb9+vb514kn66U9/ql9kDtTdWaMaF2KjvS9mAsRR3TkAAQAAJBxB9lBmOsGgnl+/Jd7IiuyhPbMf5pysDpsP6IQTTtB1112X8uqsqcLOmDFDRw8erPXzl+q1nBP1p6yhsbcT1Pe+mBYN01LQnWosAABIPILs4czc0yM6f7XFwARbs0O/CQYEc/V21jj9NXOY7r5jekqrs7VV2KuuukoXVnfV8qyTNT69U9O/4eG9w93bS0d2q/+ACQAAgDghyB7ODO8f2F3q3Cb2HfoNMLfsr88c8GV19vjjj9fpp5yq559/XjU1NUokMzlhwYIFmnThhRo44Kgvq7Azs0epdSCj6d84cFhFNq+lNKiH1IpxWwAAIDkIstFaDIb2dk71amZ7QV3V2Qezx2j3W0s0ceJEHdGzl26++Wbt2LFD8WRaGObMmaNRw0fomGOO0dv/eka/DRzV/CpsrdwWTiuBYfphB/egpQAAACRVIGzTsNNkW7VJKlwrlVVKzy+S3loR12+/qGa37qr8TP8MbVJNUJFg+82xY1VQUKARI0YoNzc35u9VUVGh5cuXq7CwUIsWLdKTjz+h4v37dUZGF01N76tT0zo3fjNXfcwhElPPcMLskF7SyL5SBkfRAgCA5CHI1qe6Rlq6TlqxQXpzhfT0/IS8zJ5whR6o+lxPhLdoWfVelYWqIpuvjuzTVwXHjNHQoUOVl5en7OxsZWVlqaqqSmVlZZGq6+rVq1W4YKGWr1qpqurqSFgdnJGv09RBUzL6qk+w6S0R9RrVTzrveGdz15gjmRkLAACSjiDbkLIKafFapyJ7938S/nLV4ZBWh/apMFSkwpo9Kgzs08pQsUpqqlQTDn35vIACyklLV7+0XBWEclWQlq9RaW01NJinnEASzrk4dYR0yQRpzIBm9Q8DAAA0FUE2FgfKpdeXSRffKVVWp2wZJuSWq0aZCipDwaaNzIqX350v/Wic1KGOubsAAABJwGavWJid+McPlr4zSspqxk7/ZkoPBNUqkKHMQFrqQqx53ZOOlk4YTIgFAAApRZBtzCSDrm2dMNsyy7+jySYMc+bs9mif6tUAAACfI8g2xqdfSJnp0pnHSB3z/FeV/u5oZ8TWOyul9rFPVAAAAEgEgmxjtG/tTC7YVyp9Z7R0ZFf5Qqc2TnjPzpCeXei0F3B6FwAASDGCbGP07iSVV0kvLpbWbJXGDZHGDnRuuXvV4J5OaN9b4oT4PfudY3wBAABSLAlzmjykVwfnYygsvbNK2rVPOvYo53b7WyulbUXyjNY5TlDvki+t2Cgt+MS5bqMXQRYAAKQeQbYx+hx2tOvqzdLWPU7gM/2jyzdIi9ZINf+d92qlQT2cQw4qKp3qs7nGQ1GRBQAALkCQbUpF9lDFpdJzC6Wje0mj+ztHt5oDFNZ8Idk2ordbO2lMf2cjlzmed8GnzulmhyPIAgAAFyDINkbnfGfDk+mTPZypxm7Y4YRZU6Ed1sepzn6+Q65ngqsJsCbIbt/rnGJWX5sEQRYAALgAJ3s11qjrpU+31v+cdq2dW/Omd9b00a7cKH22zX0tB2YWrNnMZT4WHXCC94adDX/dlvudHloAAIAUIsg21tl/ll5ZGttzzQEKQ3s7QbGiSvpki/TxZqcdIVVMRXlAN2lgDyeMmqBtqslrv4jt601IX39volcJAADQIFoLGqsxt9XNJinzMIFxYHdpQHcn2O7eL23c6bQdmCCZaLk5zqQB8+jcxpk+YCrEr30k7Sxu3PeirQAAALgEQbaxmhLk9pdJC9c4m8DM1/fs4EwGGHGEVFrhhF0TaHfucz7WtcEqVsGA1La10/faIdfp623T0mlrMK/z7mpp/XanQtwUjN4CAAAuQZBtrOZUJE0ldN1252FOxjInZplJCCZsmtFeaQfPpygukQ6UOyG39mE2mIVCkmkEMYdqBYNSRprUMltqkeU8WmZJbVo5YdZ0jJi+V7Npy/S+bt7dvIBcqw9BFgAAuANBNlUVSRM0TcisnQ5ggm1+S6eSaiqqrbKl3BZOyDUhtTbkHv49Dg272/Y6s21NZdecwJWIzWWmmgwAAOACBNnGMiOqEsGE0j0HnEddTJA1lVZTia2tzMajwtpY3dsn/zUBAADqQJBtrLatnAqpqYAmk6muRnJrCsJrMoI8AABAI9VxvxoxjdXyK4IsAABwCYJsU5iDDvzIjBHLa5HqVQAAAEQQZJvCr1VJv143AABwJYJsU/g10Pm5pQIAALgOQbYpuvk00Pm1pQIAALgSQbYp/FqR9et1AwAAVyLINoVfZ6lSkQUAAC5CkG0Kv1Ym/XrdAADAlQiyTWFGUJlRVH5DkAUAAC5CkG0qP4Y6P14zAABwLYJsU/ltFFWbllKr7FSvAgAA4EsE2aby28YnqrEAAMBlCLJN5bdg57cKNAAAcD2CbFP5rSLrt+sFAACuR5BtKr9VZP12vQAAwPUIsk3lt2Dnt+sFAACuR5BtKr8FO1oLAACAyxBkm8qMojIjqfyCzV4AAMBlCLLN4aeqbPf2qV4BAADAVxBkm8MvQbZtayknM9WrAAAA+AqCbHN088ntdvpjAQCACxFkm8MvFVn6YwEAgAsRZJvDL0HWL9cJAACsQpBtDr/ccvfLdQIAAKsQZJvDL5VKv1wnAACwCkG2OfwS8KjIAgAAFyLINocZSWVGU3kdm70AAIALEWSby+vVykDAP5VnAABgFYJsc3k95LXPlbIyUr0KAACAryHINpfXb7t3zU/1CgAAAOpEkG2ujnnytI5tUr0CAACAOhFkm6uTx4Oe168PAABYiyDbXF4Pep08XnEGAADWIsg2F60FAAAAKUGQbS7PV2Q9fn0AAMBaBNnm8nrQ8/r1AQAAaxFkm8vMWG3TUp7l9dYJAABgLYJsPHg57FGRBQAALkWQjQevhr0WWVJui1SvAgAAoE4E2Xjw6s5+L1eaAQCA9Qiy8eDVWasEWQAA4GIE2XjwamuBV68LAAB4AkE2HjzbWuDR6wIAAJ5AkI0Hr1YuvXpdAADAEwiy8eDVwOfV3l8AAOAJBNl48Grg82pABwAAnkCQjYf2uVKaB99KemQBAICLeTB9pUAw6IRZr6EiCwAAXIwgGy9eDH1evCYAAOAZBNl48drhAfmtpMz0VK8CAAAgKoJsvHiteum16wEAAJ5DkI0XrwU/r05iAAAAnkGQjRevtRYwsQAAALgcQTZePFeR9dj1AAAAzyHIxovXgp/XrgcAAHgOQTZevBb8vNYqAQAAPIcgGy9e6yn1WjAHAACeQ5CNl7wWUk6mPIMgCwAAXI4gG09euh1PkAUAAC5HkI0nr7QXpKdJ7VqnehUAAAD1IsjGk1eqmB1ypUAg1asAAACoF0E2nrzSWuCV6wAAAJ5GkI2ntq3kCW1pKwAAAO5HkI0nrwTAfI8EcgAA4GkE2XjySgD0SiAHAACeRpCNJ8+0FnjkOgAAgKcRZOOpXa48gdFbAADAAgTZePJKJZPWAgAAYAGCbDx5JQB6JZADAABPI8jGU5sWUtADBwl4JZADAABPI8jGUzAotfFANZOKLAAAsABBNt68sFHKC9cAAAA8jyAbb7ZXMzPSpNwWqV4FAABAgwiy8WZ7f6lXDnUAAACeR5CNN9srsrYHcQAA4BsE2XizPshavn4AAOAbBNl4s/10LzZ6AQAASxBk4832iiatBQAAwBIE2XizPQjaHsQBAIBvEGTjzfYgaHsQBwAAvkGQjTfbg6DtQRwAAPgGQTbebN8sZfv6AQCAbxBk4832iqbtFWUAAOAbBNl4y0iXWufIWrYHcQAA4BsE2USwOQxSkQUAAJYgyCaCrWEwEJDyLQ7hAADAVwiyiWBrkM1rIaXxIwEAAOxAakkEW1sLbA3gAADAlwiyiWBrILQ1gAMAAF8iyCaCrYHQ1gAOAAB8iSCbCNYGWUvXDQAAfIkgmwi2VjYJsgAAwCIE2USw9ZjXdrmpXgEAAEDMCLKJ0KalrGTrugEAgC8RZBPB1iNqbV03AADwJYJsog4WsJGt6wYAAL5EkE2E1pYGQiqyAADAIgTZRMjJlDLSZJ1cSwM4AADwJYJsothYlaUiCwAALEKQTZRcC0OhjWsGAAC+RZBNFBtv09u4ZgAA4FsE2USx7TZ9doaUkZ7qVQAAAMSMIJsotlU3bVsvAADwPYJsothWkbVtvQAAwPcIsoliWzCkIgsAACxDkE0U207JYmIBAACwDEE2UWyryNo49xYAAPgaQTZRbLtVT0UWAABYhiCbKFRkAQAAEoogmyi2VThtC94AAMD3CLKJYltrgW2b0wAAgO8RZBPFtgqnbesFAAC+R5BNFNsqsratFwAA+B5BNlHokQUAAEgogmyi2DYFwLbgDQAAfI8gmyiZ6VJ2hqxBawEAALAMQTaRbKrK0loAAAAsQ5BNJJtu11ORBQAAliHIJpJN4dCm0A0AAECQTTBbbte3zJKC/CgAAAC7kF4SyZaKrC3rBAAAOARBNpFsqcjask4AAIBDEGQTyZZKpy3rBAAAOARBNpFsqXTask4AAIBDEGQTyZZJALasEwAA4BAE2URqlS0rtCLIAgAA+xBkEyknU1bItmSdAAAAhyDIJlJOlqzQgiALAADsQ5BNJFsqsrYEbgAAgEMQZBPJmiBryToBAAAOQZBNJFsqnQRZAABgIYJsItkSEG1ZJwAAwCEIsolkS0C0pXIMAABwCIJsIrWwJCDask4AAIBDEGQTyZb5rNkZqV4BAABAoxFkE8mWSqct6wQAADgEQTbRPbKBgFyPHlkAAGAhgmyi2XDb3pZNaQAAAIcgyCaaDdVOgiwAALAQQTbRcqjIAgAAJAJBNtGsqMhasEYAAIDDEGQTzYaJAC2oyAIAAPsQZP2+2Ss9TcpIT/UqAAAAGo0g6/eKLP2xAADAUgRZv5/u5fb1AQAAREGQ9Xv/qdvXBwAAEAVB1u8TAdy+PgAAgCgIsn6/dU+PLAAAsBRB1u+37gmyAADAUgRZv9+6d/v6AAAAoiDI+r3i6fb1AQAAREGQ9XtQdPv6AAAAoiDI+j0oun19AAAAURBk/d6D6vb1AQAAREGQ9fsRtW6fqgAAABAFQTbRsjPkam6fcwsAABAFQVZ+r8i6fH0AAABREGQTLdPlFdnM9FSvAAAAoEkIsomWkSZXc/v6AAAAoiDI+j0oZlCRBQAAdiLI+j0oprs8aAMAAERBkE20NJe/xQRZAABgKZenLA9wfWuBy9cHAAAQBUHW760FBFkAAGApgqzfgyKtBQAAwFIEWb8HRbdXjAEAAKIgyPo9yKbzIwAAAOxEivF7awEVWQAAYCmCrN+DotsrxgAAAFEQZJMxRzYQkGu5vWIMAAAQBUHW72HR7RVjAACAKAiyfr99z2YvAABgKcpxfq/IujlkAwDgI/v27dPSpUtVWFioVatWqaSkROXl5aqoqFBGRoZycnIij379+qmgoCDy6NChg/yMIOv3sEhrAQAAKbFu3To988wzWrx4cSS8fvrpp5HPm7A6ePBg5ebmKjs7O/KoqqrSrl27IuH26aefVnFxceS5PXv2/DLUnnHGGRoxYoT8JBAOh8OpXoTnHXmltK1IrrR6htStXapXAQCAL9TU1Og///mPZsyYEfloQurw4cO/DKOjRo3SUUcdpfT06IUmE90+++yzSPg99GHC7THHHKOpU6fqnHPOiXxvryPIJsOgadLm3XKlNXdLndqkehUAAHjazp07dd999+nuu+/W559/rpEjR2ratGn64Q9/qBYtWjT7+1dXV+uFF17QzJkzNW/ePLVr106XXHKJpkyZoj59+sir2Onj99YCN68NAAAPVGD/8pe/RFoAbrrpJo0bN04LFiyItBNcfPHFcQmxhqngnnnmmXrllVf0ySef6MILL9SsWbPUt29fXX311ZGWBC+iIpsMBddLa7bKlTbfJ+XG539EAADgv1avXq2LLrpICxcu1PXXX69f/OIXkUppspSWluqee+7Rr371K3Xp0iVSETZB2kuoyPp9agGbvQAASEgV1my8Kioq0nvvvae//vWvSQ2xhqn2Xnfddfroo4/UrVs3nXjiiZ6rzhJk5fcg6+K1AQBgmfXr12vs2LH6+c9/HgmNH374oY499tiUrqlfv35688039fe//11z5szR0KFDI+0NXkCQTYY0F4dFemQBAIiLlStXRkKs2dhlqrC33nprZJSWGwSDQV1zzTWR6mzHjh01fvz4SD+t7Qiyfq56EmIBAIgLs3nrhBNOiBxQ8P7776e8Cltfdfb111/XSSedpO985zt68sknZTOCrJ+DrFvXBQCARUyV8+STT9aRRx4ZuYXfqVMnuVlOTk7kUIWzzz5b5557bmRsl63Y6ePnDVUEWQAAmmXt2rU65ZRTdMQRR0QOOMjLy5MNMjIy9NBDD0WOvzWHJ7z88suRzWC2oSLr51v49ZwaAgAA6md2/59++unKz8+3KsQeOnv20Ucf1fHHH6+JEydGDmqwDUHW10GWv34AAJrKzIXdsmWLnnvuuUhvrI2ysrL073//OxLGL7300sjxtzYhyfj5Fr5b1wUAgMu99dZbuvPOO/WnP/1J/fv3l81yc3M1e/Zsvfbaa5EDFGzCyV7JMPnv0lMfyHV6d5Q+uiPVqwAAwLqWAjOLtWvXrpFAa0ZbecHll1+uf/7zn1q+fLl69+4tG3jjnXc7t1Y+3dryAACAi/3yl7/UF198ETny1Ssh1jCnj7Vt29aqFgPvvPtu5tYf8kAg1SsAAMAqCxcu1B133OGJloL6WgweeOAB2cClCQsAAMB9br/99si8WHNKlhedfPLJOvPMM3XbbbdZUZUlyAIAAMRg27ZtkZOwrrzySk+1FBxu2rRpWrFihd599125nXf/FgAAAOJozpw5kdmrkyZNkpeNHz8+UnWeOXOm3I4gCwAA0IDq6urIaKrzzz8/MnPVy4LBYKTqbKrPpgrtZgRZAACABrz44ovatGmTpk6dKj+YNGlSpPpsqtBuRpAFAABowP33368xY8Zo5MiR8oP8/Hydd955ket2M4IsAABAPczu/fnz50d29PvJySefrM8++0y7du2SWxFkAQAA6rF161Zt375dBQUF8pNRo0ZFPhYWFsqtCLIAAAD1qA1ytcHOL/r27au8vDyCLAAAgK0WL16sDh06qHv37vKTQCAQ6QkmyAIAAFjKBDnTVmCCXTJ98MEHmjhxorp27aqWLVtq+PDhevjhh5O6hoKCAoIsAACArZYuXZqSaQUbNmzQ2LFjNXv2bD3//PM666yzdMkll+jBBx9MapDdsGGD9uzZIzdKT/UCAAAA3MyEuM6dOyf9dX/4wx9+ZXLCCSecoM2bN0cOZkjW6WKdOnWKfCwqKlLbtm3lNgRZAACAKEyArKioUE5OTtJf24THm266Sc8++6y2bNmimpqayOfbtWuXtDVkZ2dHPpaVlcmNCLIAAABRmBB7aKBLpsmTJ+v999/Xb37zGw0ePFi5ubm666679NhjjyVtDTkHAzxBFgAAwDK1G7xMZTaZysvL9cILL+i2227T1Vdf/eXnQ6FQUtcROvh6waA7t1W5c1UAAAAukJmZGQmzya5ImkqwCZHm9Wvt379fzz33XNIDtZGK1opYUJEFAACIwoRY01aQ7CBrDiIYPXq0brnllsgM2/T09Mg/m8/v2LEjaesoO3jdbg2yVGQBAAAa2Lm/cePGpL/uo48+qn79+kUmFFxzzTU6++yzdeGFFyZ1DRs3boyEeROm3YiKLAAAgAsPBTAh9rXXXvva5//3f/83aWsoLCzUgAED1KpVK7kRFVkAAIAGguySJUuSvtHKTaeauRVBFgAAoB6jRo2KbLRau3at/KS6ulrLli0jyAIAANiq9njaVLQXpNLq1asjm70IsgAAAJYyJ2n17t1bixcvlp8sXrw4stFrxIgRciuCLAAAQAOOP/54Pf/8877qk33++ec1bNgwtW7dWm5FkAUAAGjAZZddpjVr1uj111+XH2zevFnPPvusLr/8crkZQRYAAKABxx13nIYMGaKZM2fKD2bNmqUWLVroggsukJsRZAEAABpgekWnTp0aqVKaaqWXVVZW6t57740cvuDmtgKDIAsAABADU51s2bJlpFrpZc8884y2bdsWCe5uR5AFAACIgalOmiqlCbKlpaXyonA4rDvuuEPjxo3T4MGD5XYEWQAAgBhde+21Ki4u1o033igvuv/++/Xee+/p5z//uWxAkE2GGpeO6vDRCBEAAOKhX79++sMf/qC//e1vkcDnJZs2bdJ1112nyZMn67TTTpMNAmFTQ0ZiTf6b9NR8uU6vDtLyO1O9CgAArFJTUxOZK7tr1y59+OGHkd39tguHwzr99NO1fPlyrVy5Um3atJENqMgmQ1WNXMmt6wIAwMXS0tIit+A3btzomRaD+++/X3Pnzo1MK7AlxBoE2WSodukt/GqCLAAATTFgwIAvWwzeeecd2WzDhg1fthScccYZsglBNhmqquVKVGQBAGgyE/5Mi8GZZ54ZuR1vo507d0b6YfPz83X77bfLNgRZP1c+3bouAAAsaTEwM1e7d++uk08+WevWrZNNiouLIyG2qKhI8+bNs6qloBZB1s+VT7dWigEAsISpZL7yyiuRgxJOPPFEffrpp7LBnj17dMopp0TCt1l///79ZSOCrJ8Do1sDNgAAFunUqZPefPNNtWrVKtJqYCYZuNnWrVt1wgknRELsa6+9pqFDh8pWBFk/b/Zy63xbAAAs061bN7399tvq0aNHJCTOnj07MtLKbebOnasxY8ZE2grMJrWRI0fKZgRZv/eiurVaDACAZdq3b6833nhDZ599ti677LLIXFZzyIAbFBcX69JLL430xA4aNEgffPCBjjrqKNmOIOv3sEh7AQAAcdO6dWvdd999evHFF7VixQoNGTJEc+bMSWl1du7cuZF1PP7445o1a1bk380GNS8gyPo9LLp5bQAAWMrMYzVB9qyzzvqyEjp//vykBtpPPvlEF110UeS1Bw4cGFmPqRQHAgF5BUHW760F1S6uFgMAYDEzzspUZ1966SWtXbtWxx57rAoKCiL9syUlJQl5zerqaj311FOaMGFCpHXghRde+LIK27NnT3kNQVZ+D7Js+AIAIJFMr+yaNWsigdZsCrv88ssjH6+99lotX75coVDz/lscDoe1fv16/e53v1Pv3r0jVeCysjI98sgj2rx5s+eqsIcKhN24pc5r+k+Rtu+VK62aLnVvn+pVAADgGyZ0miqpqczu2rUr0lc7YsQIjRo1KlKxNQ8z1zUY/Hq90cQ2s4GssLBQixcvjnw0D/N9WrRooQsuuEBXXnmlhg8fLj8gyCZD78ukPfvlSsv+LvXplOpVAADgOxUVFZGRXbVh1DxMyDWys7Mjc2nNx6ysLFVVVUWqrKWlpV+2JXTu3PnL4GtCsBn7lZeXJz8hyCZD94ukfWVypcLbpP5dU70KAAAgaffu3VqyZIlWrVoVCa0mvJrAm5mZGQm1OTk56tevXyS4du3Kf78JssnQ6UKprFKuNP9WaVCPVK8CAACg0djsJb9v9nLx2gAAAOpBkPX7rFY3rw0AAKAeBFm/VzyZIwsAACxFkPV7xdPt6wMAAIiCICu/V2Rdvj4AAIAoCLJ+r3i6fX0AAABREGT93oNKRRYAAFiKIOv3iqfb1wcAABAFQdbvQbHK5RVjAACAKAiyfr91Xx1K9QoAAACahCArvwdZKrIAAMBOBFn5vbXA5esDAACIgiDr9x5UgiwAALAUQdbvPahuD9oAAABREGQTrbxSrlZRleoVAAAANAlBNtHKXB5k3b4+AACAKAiyiVZWIVcjyAIAAEsRZBOt1OVBsdTlQRsAACAKgqzfe2Tdvj4AAIAoCLJ+r3i6vWIMAAAQBUHW7z2obu/hBQAAiIIgK78HWZevDwAAIAqCrN8rngRZAABgKYKs3zdTuX19AAAAURBk/b6Zyu2b0QAAAKIgyCYarQUAAAAJQZD1e1B0+/oAAACiIMj6PSi6vWIMAAAQBUFWfg+yLl8fAABAFARZv1c8Q2GpoirVqwAAAGg0gqzfpxYYTC4AAAAWIsgmmg1zWm1YIwAAwGEIsolmQ7XThqoxAADAYQiyiWbDZiq39/ECAADUgSCbSKGQHRupbAjbAAAAhyHIJpItAdGWdQIAAByCIJtItmyiIsgCAAALEWQTyZZNVPTIAgAACxFkE8mWgGhL4AYAADgEQTaRbLllb0sLBAAAwCEIsolkS5C1YdYtAADAYQiyiWRLkKUiCwAALESQTSR6ZAEAABKGIJtItgREWwI3AADAIQiyiWTLLXtb1gkAAHAIgmwi2dIja0vlGAAA4BAE2UTaXyYrHLBknQAAAIcgyCbS/lJZwZbADQAAcAiCbCIVWxIQiy0J3AAAAIcgyCYSFVkAAICEIcgm0j5LAuI+SwI3AADAIQiyiURFFgAAIGEIsolkS0A0Y8Kqa1K9CgAAgEYhyCaSTZuobForAAAAQTbBbKnI2rZWAAAAgmyC2RQO2fAFAAAsQ5BNlPJKqbJa1rApdAMAABBkE8i2YEhFFgAAWIYgmyi2bZ6yZeYtAADAQQTZRLGtImvLzFsAAICDCLKJYtuteiqyAADAMgTZRKEiCwAAkFAE2USxrUe22LLgDQAAfI8gmyhUZAEAABKKIJso1gVZy9YLAAB8jyCbKNZt9rJsvQAAwPcIsoliW4XTtvUCAADfI8gminWbvSxbLwAA8D2CbKLYVuG0bb0AAMD3CLKJYtsUAIIsAACwDEE2UWw7KauyWiqvTPUqAAAAYkaQTRTbKrIGVVkAAGARgmyi2FaRNdjwBQAALEKQTRQbq5s2rhkAAPgWQTYRSsqlmpCsQ5AFAAAWIcgmgq2nZNm6bgAA4EsE2USwtbJpY18vAADwLYJsItgaCKnIAgAAixBkE2HPflmp6ECqVwAAABAzgmwi7LE0ENq6bgAA4EsE2USwtSJr67oBAIAvEWQTwdbKpq3rBgAAvkSQTQRbAyEVWQAAYBGCbCLs2ScrEWQBAIBFCLKJYG1F1tJ1AwAAXyLIJoKtgdAc5FBVnepVAAAAxIQgmwg236K3NYQDAADfIcgmgs1h0OYQDgAAfIUgG29llVJphay1myALAADsQJCNN9srmjZXkwEAgK8QZOPN9iBoexAHAAC+QZCNN9uDoO1BHAAA+AZBNt5sD4K2B3EAAOAbBNl4223pqV612OwFAAAsQZCNN+srspavHwAA+AZBNt5svzVv+/oBAIBvEGTjzfaKpu3rBwAAvkGQjTfbgyAVWQAAYAmCbLzZvtlrb4kUCqV6FQAAAA0iyMab7RXZmpC0tzTVqwAAAGgQQTbeiiwPsgbtBQAAwAIE2XhXM4s9UM20vaoMAAB8gSAb72psOCzrUZEFAAAWIMjGk1dOxfLKdQAAAE8jyMaTVyqZXujzBQAAnkeQjSev9JZ6JZADAABPI8jGk1cCoFcCOQAA8DSCbDx5JQB6JZADAABPI8jGk+2netVisxcAALAAQTaePFOR9ch1AAAATyPIxtOOYnnC9r2pXgEAAECDCLLx5JUAaHpkq2tSvQoAAIB6EWTjaYdHgmwoLO30SHUZAAB4FkE2XszRtDs9stnL2E6QBQAA7kaQjecGqcpqeYZX2iQAAIBnEWTjxSttBV69HgAA4DkE2XjxWgXTa9cDAAA8hyAbL17rKfXa9QAAAM8hyMaL127Fe+16AACA5xBk48Vrt+K9dj0AAMBzCLLx4rXg57XrAQAAnkOQjRevHE/r1esBAACeQ5CNF69VMPeXSaUVqV4FAABAVATZePFakPXqNQEAAM8gyMZDdY1zspfXMLkAAAC4GEE2Xv2k4bA8h1myAADAxQiy8eDVW/BevS4AAOAJBNl48Grg8+p1AQAATyDIxoNXe0m9el0AAMATCLLx4NVeUq9eFwAA8ASCbDx49Ra8V68LAAB4AkE2Hrx6C96r1wUAADyBIBsPXq1cckwtAABwMYJsPHi1l7Sy2psHPQAAAE8gyMaDl2/Be/naAACA1QiyzVVSLh0ol2d5tW0CAABYjyDbXF4Pel6/PgAAYC2CbHN5fUOU168PAABYiyDbXF6vWHr9+gAAgLUIss3l9aDn9esDAADWIsg2l9eDntevDwAAWIsg21xe7yH1+vUBAABrEWSba8tueZrXrw8AAFiLINtcXg96e0ucWbkAAAAuQ5BtLq8HWb9cIwAAsA5Btjn2lUr7yuR5W/akegUAAABfQ5Btjq0+CXhUZAEAgAsRZJtjs08Cnl+uEwAAWIUg2xx+qVT65ToBAIBVCLLN4ZdKpV+uEwAAWCU91QuwWqIqlelpUsc8qUu+1KmN1Kal87naR01Iqq6Rqmqc0VjbipwTuMzH8qr4r2crQRYAALgPQTZVQTYQkPp2lkYcIQ3rIw3oKnXOd8Jru1wpUMfXRAJsSEoLSulRiunFpU6g/aJIWrddWrZe+nCdtGqTE3ybgtYCAADgQgTZ5mhMwGvbWjppiDSirxNeh/aWWmc7f7Zhp7Ryo7TwU2mbqawerK5GHnulogNOBfZwwYCU10LqlC91buME4c4Hq7jm4zFHSpPHO8+rrHZeY+l6J9y+tUL6fEdsazcjxsyosdwWsV8vAABAggXC4XA40S/iWV0mSSUV0f+8XxfpjALptJHSMQOcQGlC69J1ByulB0PlngOJW2OLLGlIL2lEH6fya0L0gG7OWkyV9uVC6eUlUuFnUn0/CgtulQb2SNw6AQAAGokg21SmStrr0q9/vqCv9L1vOAHWBFnTs/rGcicwzl3q9LKmWusc6aSjpdNNyB4h5beSdhQ7gfbFRdKry6TQYT8WT/1CmjAsVSsGAAD4GloL4tFWkJMpnT1WumSCNLzPf0Phjf9wbuGXVcpV9pdJzy10HqbfdsyR0ukjnWA76SRp027p/lelh96Qdu1zvoY+WQAA4DJUZJvKVFd/8ZB08QTpR+Oc/tFXlkpz5tVd0bSFaT0w13TOWCfkPrtAmj1PGjdE+tU5qV4dAADAlwiyTbVpl9StrbR7v1O5fOA1p//VK/JbSuePky45WTqikzNLtmeHVK8KAADgSwTZxgqFnNFZZqPU356Tnp7vTATwKnOtJw6Rpp0hTRjuXL+ZZQsAAJBiBNlYmbcpfPBwgN8+Jj3+bv27/L3ouEHS7853NrSZ1gnTegAAAJAiBNlYmIMIikukm590WghMBdaPb5upzhrfHiX99jxnKoN5H4IEWgAAkHwE2fqYk7DMQQS3PyfNeFE6UP7fP/NzkDVMNfaHx0s3/VBq15p2AwAAkHQE2brUviUffCxNvVtaX8cJWH4PsrVyc6Q//o90wYnOe0K7AQAASBKCbLQq7I2PSHNejR5Y/fi2RXJsHWHW+NYwacYVUodcqrMAACApCLJ1VWGvvFv6fEdsz/eT+oKsQXUWAAAkEUG2djOXGSv1y0eke1+JIaQenGDgR3W1F9RVnZ09TWrdQsqgOgsAABKDIGtaCfaVSuf9VVrwaYxfRJBtUI/20mM/kQZ0o9UAAAAkhL+DrOmF/XizdO6tzslVsfLxWxZzkDVaZEl3XSl9b0zjvg4AACAG/g2y5rKfWeBMJSitaPzX+lVTAulPvi/98hynks3MWQAAECf+C7K1J3T98Qnpr083/Xv4VVMrq98ZJd17lZSZTqsBAACIC38F2UiIDUtX3ys98mbzvo9fNadFYHR/6ZlfSNmZhFkAANBs/gmyobAzmWDKXdIT7zXve/nkLatTc3tdh/eRnrtRaplFmAUAAM3ijyBrLtEE2UvulJ6eH5/v51cNzZKNxZCe0ou/kVplE2YBAECTBX3TTmAqsfEIsWi+FRul7/1RKq90ZvgCAAA0gQ+CrKRr50iPvxvHb+hj8br8D9dLZ94sVVQ5LR8AAACN5O0ga9oJbntGevD1+H1Pn+fYuFq0RrpsevNbFQAAgC8FPX3Ywdwl0h+eSPVKUJ8XFks3/9vffccAAKBJgp49dnbddumyGQQkG9z6tPTsQqmmJtUrAQAAFvFekDWbh8xJXf/nz9L+slSvBrEwv2xceZf08RbnlxAAAABfBlkTiv7nNmn9jlSvBI1hfvk491ZpfymTDAAAgA+DrNn9/uenpLdWpnolHpegdo1Nu6RLZzT/0AUAAOALQU9t7lq1Wbrt2QS+CP22CffaMuf4YPP3CQAA4Isga1wxgwCUDInO8796WNq5jxYDAADggyBb21JgToxKJAqyybGvTJp2Ny0GAADA40E2KS0FSLrXPpIefoMKOwAA8HCQNWgp8KYbH5F27eMIWwAA4MEga8KrOX420S0FSF2Lwe8eo8UAAAB4NMj+5elUr8KnktQw/M+3pTVfsPELAAB8TbpsZYLN9BelbUXyslA4rM3hUh0IV6tcNapQSBkKKkdpygmkqXsgR5mBNHlWKCz97z+lf1yf6pUAAACXCYTD5igsy5glm9vOR18tFZcm60UTXoQ0oXVNeL8W1+xRYU2RCgPFWlpTpP01lVG/JjOYpqPT81UQztWoYFsVpOVrSDAvOeE2mbf8X/u9NLyPlO7h0A4AAHwQZM3mn5v+Kd3xQhJfNDFB1rz9i0J7NLNyrZ4Kb/0ytPbp3lMF3xitUaNHa+jQocrLy1N2drYyMzNVXV2tsrIylZSU6OOPP9bixYtVOH+hVq35RKFQKBJuv5XWSVPT++r0tM5KCwTtD7JjB0ov/FoK0i8LAABsDbJmuTuKpaHXSOVVyX3dOCoNV+tf1Rs1M7RehZW71Ktbd1102aUaO3asRo4cqbZt2zb+e5aWatmyZVq4cKEeeeBBLf5wqXpltNaUQB9dnNFHHYPZcb2GpG/CevLn0rghUgZVWQAAYGOQNb2xf3pC+uszyX3dOL1NReFK/bFileaEN6i4pkKnn3Kqpl59lU477TSlpcU3oC1atEgzZ8zQv/75L4Wqq3VOWnf9b+Zg9Qu2tjPInnS09PQvmGIAAAAsDbJV1dKgq5yqbDLF4W16sXqrLq9eogOZAU2ZNlVXXHGFjjjiCCXa7t279cADD+iO227Xzm07dHPGEF2d0V/B5gbCZAdK83pLb5d6daTFAAAAWDZ+q6pGenZh8kNsHKqwk8sX6Dtl72joiWO18uPV+vOf/5yUEGu0a9dON9xwg1Z9+okumXqFrq1YqhMr39Ta0H5ZxfwyMWtu3Ns8AACAnewKsulBafYrsompwg6peEVPZ+7Sfffdp5fm/kfdu3dPyVpatmypO++8U2+++aa2dM7R0LJ5+nvlp5FpCU2TgkD56NtOVR4AAPhe0KpJBZ9skT74RDYwHRu/rlj+lSrsRRddpIAL+jvHjRunj1at/LI6+/2K91UetuSI370l0uPvEmYBAIBFQVYB57ZySjSu8mgqnFdXLNEfKlfplltuSWkVtqHq7Isvvqh5wV06veId7Q83cgpEqu7wz3lVivPGOAAAYB+7KrJPfZCa1w43LsReWrFIM6s/06xZs/Szn/3MFVXYaM444wy98uo8Lcks0YTKtxsfZlPhw/XS+m2pXgUAAEixoDUjtxZ8KhWVyO3tBNdVLNUD1Z/r4Ycf1mWXXSYbHHfccXrj7bf0cUa5Jla8p7KwBbftn1tIewEAAD5nR5A1XlgktzOtBHdUrdHMmTP1ox/9SDYxhzC8+J+XtSBYrHMr5jdjA1iSvLyE42oBAPA5O4KsuTX/nyVys3erd+qmypX67W9/qylTpshGpjL776ee1PNVW3Rn1Rq52qI1zsYvAADgW3YE2XXbpHXb5VbmuNmLqwt1zOgx+tWvfiWbmZ7Zq6++Wr+oXuHuObOhsPTSYtoLAADwsaAVhyCYfkgXu7FiuTYFynX/ww/G/ZjZVLj55pvVpVtXXVy1OIYWgxS2INBeAACArwWtOARh3ocpXEC4wZaCv1Wt0R/+9EcNGDBAXmBGc9330IN6p3JHwy0GqWylff0jZyMgAADwJfcHWXML2YxbcnlLwbXXXisvMYcmuL7FoKRCWvtFqlcBAABSJGhFf2xpRepev56K4+yqdVofOuCZloK6Wgw6du4U2cTmWovW0icLAIBPBV3fH2uCiktnxs4MrddZZ53lmZaCuloMrrn+Ov27ZrN2hMrlSsvWS2nu/jEGAACJ4e4EEAw4QcWF3qjZoU+q9mrqtGnyssmTJyuYnq77qtz59xBpO3HxyWkAAMDPQdal/bEzqj/T4AFH6fjjj5eXtW3bVuedf57uDq9XTTjkvh1fKzY4fdQAAMB33B1kzein5Z+ncgF1fnZzqFTPVm/R1GuuVsAH1UBTdd5QtV8v1UTZWJXKHFlWyYYvAAB8yt1BdtteZ2d6qkQJaPdWrVNOdrYuuOAC+cGoUaM0esRIzaxeJ1cyVdmamlSvAgAAJJm7g+yW3XKjl7VDE79/pnJzc+UXF0yepNeqt6si7MLA+EWRVEN7AQAAfuPeIBsKuTLIVoVD+qi6SGPGjJGfmOutCtdoRahYrrOj2OmnBgAAvuLeIFsdcloLXGZlqFgVoerI7XY/GTZsmNKCaSqsKZIrK7KM4AIAwHfc+19/U2Db7r4ga4JcMBjU8OHD5Sc5OTkadOQALQ7tifKMFN7a3+7CcA0AAHwcZNPTpG2pDCh1B7PC0B4d1bd/5LAAvyk4ZowKA/vq/sNwiiuytMgCAOA77g2ySnFFNkowMkHOBLpU+OCDD3TKKadENpm1bt1a3/jGNzRv3rykvX5BQYGWVxW5b8OXCyv3AAAg8dLlZsWlcpu1oQM6c+DApL/ue++9p/Hjx+uYY47R7Nmz1aZNGy1evFgbN25M2hoGDRoU2fC1MVyq/oHWco19ZaleAQAASAF3B9maaCdJpU5ZqDolbQU//elP1a9fP73++utKS0uLfM5UZ5OpRYsWkY/lbqvImoMzzCPSWA0AAPzC3a0F1e4KTOFwWGWhqsjGp2QqLS3V/PnzNWnSpC9DbCrUXne53PX34tZfegAAgJ+DrMvCSc3Bxtn09OQWsouKihQKhdS1a1elUkZGRuRjZdhdfy8RkYosAADwE3cHWZfNBk1TQEEFVFlZmdTXNf2wZuTX1q1blUoVFc5xwVmB1FWFo+JABAAAfMddSfFwGe4KTIFAQNlp6SovL0/q65qe3GOPPVYPPfSQampSd1u/9rpz5K6/Fzf+0gMAAPy+2cvMknWZVsEMFRcn/5jWW265JTK1YMKECZo6dary8/O1ZMkStW/fXhdffHFS1rBvnzNDtoXbKrKRaiwVWQAA/MbdZaw2reQ2g9RaHy1blvTXPe644/Tmm29GqsKTJ0/WD37wAz399NPq1atX0tawbNkytUzLUM+AM73ANVz4cwIAAPxeke2Sn7rXNgW+OvYPFaiNnlywKBUr0je/+c3I+K1UKVy8WCPS8pUWCLrv54SCLAAAvuOyRHLY6K1ObVK4gLqTUUFavj7fskm7d++W3xQuWKSCcF7df5jKINk5hb/wAACAlHFvkDXV0M6pDLJ1K0hrG/lo+lP9ZO/evfps4+cadfD6XZVkXfhzAgAA/BxkzUYvF1ba+gVaqXVaZuR4WD+pDe4FwWhBNsWtBS47PAMAAPg5yJqd6N3byW2CgUCkveD9d9+Tn7z//vuRjV5HBl24sapjmzr7mQEAgLe5N8gaXVNc/Ytyt/ysQDe9/J//pPyAgmQxp4rdN+te/SDYzX0bvYyu+VK6C9cFAAASyt3/9W+XK+W3cl2S/Z+MXsoOpOnee++VH8ydO1frN23U1Iy+dT8h1RMDhveRgu7+UQYAAPHn7v/6m4A0oo/cJi+QqQuCPTRr5l2qqqqS182cPl0jMtvpG8ForR4pTLJtWkrd2qfu9QEAQMq4O8jWhKThR6R2DVEy2pUZfbV1x3Y999xz8rLPP/9cL778sqYG+kQOY3CdYb1TXxEGAAAp4e4gawLKsFRXZOtOScPS8jU2s6Nm3jldXnbPPfcoNy1T52VEOUEs1eHW/KJjfuEBAAC+4+4gm5Ymje4nt5oWPEKvv/Wm3njjDXmR2cx21/QZuijQSy0DLj0EzlRkAQCAL7k7yBpd2qZ4w1f0quO56T01LqOTLr5wkg4cOCAvCYfDuvzSS5VTXqNfZw2q+0luuKU/ur8zcxgAAPiO+4Ns4GBYcSEzU3ZO5ijt+GKbfv6zn8lLHn744Uhv7KyMkWobyJIrk2z7XKk7G70AAPAr9wdZc2LTqSNSvYqoVdm+wVa6JX2IZsyc6ZkWA9NS8H+vuloXZPTSd9O7ubcaa34u3LAOAACQEu4Pshnp0sTRcrNpGf0902JQ21KQXVatv2fV9wuECxLkGQVs9AIAwMfcH2SNDnnO0HuXVmUPbTG47NJLIydh2er2229vuKUg1ZMKjOwM6VvD6I8FAMDH7Aiypup22ki5QpQMZ1oMHswYrccff1xXTZsWqWza5r777tMNN9ygn2cOdHdLgXHCECfMAgAA37IjyKYFpYlj5A7Rk9zZGT00K3OU7rr7bv3sZz+zKsw+9thjuuzSyzQlo6/+lHl0Pc90SZI9faTTPw0AAHzLpcNB67iVPbCH1L2dtHm3O9YTJaReknmESlSt/3vrrTqwf7+mz5ihYNDdvy/ce++9uuKKK3RBei9NzxoZ/QQvN7QU1K7jO6Oc/mkAAOBb7k5YhwqHpPNOkGvUk+muyTxSs7NGR07F+tH557t2A1hNTY3++Mc/6vLLL9fU9L56IGuM0gJRfiRckmEjxh/t9E0DAABfsyfImlO+LjvFaTNwhUC94c5UZh/LOlbP/fspDR002HWjudasWaNxxx2vG2+8Ub/JHKw7s0ZGNq1F56Ike+kpUrW9G+oAAEB8uCUVxqZjntMb6Rr1h1nTM/tR9inqsa1C48eP17SpU1NenTVVWDOZYOiQo/VF4Uq9lXOSfps1JHo7gZtaCowe7Z35sRlMKwAAwO/sCrJmesHlp8pd6g95ZprBG1njdEfWCD0wa3ZKq7O1Vdjrr79el4d76qOsk3VCesf6v8hNIdaYPD5qfzIAAPAXu4KsmRl6/GCpf1e5igl79eQ9c8v+6swjv1Kd/daJJ+nJJ59UVVVVQpdmJie8++67Ov+88zR40KAvq7B/zx6ploF0u0JsZrp08QRmxwIAAAuDbG1V9qJvyX3qD7OHVmcfzT5GFR+s0tlnn63e3Xvod7/7XeRY2Hjav3+/7r77bg0bPETHH3+8Fj/5sm5JGxJbFdaNIdYwI9jyW6V6FQAAwCUCYZuGndYqrZCGXC3t3i/3CUf+PxbLaop0V9VneiS0SRWq0Rmnn6FvHjdWBQUFGjlypNq2bRvzq5aWlmrZsmUqLCzUokWL9PS/n1RJWakmpneLTCT4VlqnBjZzHRR5igtDrFn7e7dIR3VzNv4BAADfszPImkH498yVfvmwXKsRb2txuFIPV23QE+EtWlpTpP01lZHP9+neUwXfGK2hw4YpLy9P2dnZkUdlZaXKy8tVUlKi1atXq3D+Qq1a80nkaNzMYJqGpufrNHXU5Rl91SPYIvY1u7EKW+usb0pzrnL3GgEAQFLZGWSNymqp4Dpp4y65V+zV2VqhcFhrwvtVWFOkwpo9Whwo1srwPpXWVKss9N9+2qxgurKDaeoXbK1R4TwVBPNVkJavIcE8ZQYaWbF0axW2lplQsOR2qVs7F41fAwAAqWZvkDVV2cffla68W64Xp7fY/FVVKaR0BWNrE4iFDRXOS0+Wbr3I7JpL9UoAAICL2BtkjVBIGvtzadUmWcFNb7Xbq7C1WmZJH90htWttR+gGAABJY/d92pqw9JtzZY3ImK6AS9ZgSSi88nRnUkGq3zcAAOA6dgdZ0zt52kjplBGySm2YTFY2C7gkRDdWz/bSDWfSFwsAAOpkf0IIhaXpl0ttWso+gcSF2q+EV8sCrGHWPXOKlNHAoQ0AAMC37A+yplpn+if/eIHsdmiobWS4PTS02hxeD3Xxt6TjBjlVdwAAAE8GWcMcWXr+OPtaDJoUbgPeC62H69VB+oPtv5gAAIBE80aQrZ0IYG2LAb5kgvmMgy0FtvX0AgCApPJOkK1tMbj5f1K9EjTHJROk4wbSUgAAAHwUZGtbDM47wQlDsM/o/tLNF6Z6FQAAwBLeCrK1/jJZGjsw1atAY3RtK/3rx87pXbQUAAAAXwbZ2g1Qj17vbBqC++VkSv/6iZTX0qmqAwAA+DLI1vbLtsyRHvuJc8Qp3G36FdKQHvTFAgCARvFmkDVMKOrfVbpnKreq3ey6idJZx0pphFgAANA43g2yhrlN/e3R0p/ZQORKZmPeb87lFw0AANAk3g6yhtk8dPmp0u/OT/VKcChThZ1xBSEWAAA0mfeDrGHC0jXfkX51TqpXAmPiGGnWtENOJgMAAGi8QDhsjsTyCXOpd7wg/ebRVK/Ev876pjRrqlMpD/rj9ygAAJAY/gqyhrncWXOlnz3k/DOS5/wT/ttOQCUWAAA0k/+CrBEKSy8ukq6YKZVUpHo13mdC68/Pkn76A8nkV0IsAACIA38GWaO6RlqzVfo/f5E27kr1arzLzPE1/bBnjHLaCQAAAOLEv0HWqKqRDpRJP7pNem91qlfjPeZkNXMohZnny4ldAAAgzvwdZI2aGsm8Az++X7r/tVSvxjuOGyj943rnhDVO7AIAAAlAkDVq34JH35Z+8ZBUXJrqFdl9PPC135V+eY7TC2v+HQAAIAEIsof3ze7aJ027R3p1WapXY5+B3Z0jgY/uTT8sAABIOILs4WpCTiXxH29Kv3qE6mxjqrA/P9sJsPTDAgCAJCDINlSdvWqWNO/DVK/GvajCAgCAFCHIxlKdnbtE+u2/pNWbU70i9+iQJ/3k+9IlE5x/pwoLAACSjCAb65iutID0z3ekm/8tbfLx3NncHOmqb0tXf0fKTCfAAgCAlCHINrbdIHTwiNv/96y0Z798IytDuvRk53Su1jlMIwAAAClHkG1qoK2ocubO3veq9Nk2eVb7XOl/TpSmnCZ1zHNaLThiFgAAuABBttktB0HpnZVOlfblJU5frReM6e9UYL9/rHONZiMXARYAALgIQTZeFVoT9rbvlWbPkx59S9qyR9Zp01I68xvSFadKA3s415WRnupVAQAA1Ikgm4gjbwNBadVG6flF0n+WSB+ul2v17iidXiB9e5R07AApaHpfwwc/AgAAuBdBNlHM22raDEyldkexE2rnLpUWfSoVlaRuXTmZ0vA+0oRh0sQxUr+u/z2ilw1cAADAIgTZZKmqdkZVmXd7625p0Vpp2XqnWvvhusSEWxNaj+4lDT9CGtZbGt1f6tfF6XetDknpQfpeAQCAtQiyqWKqteatN1VQ8zews1jaukfavEvattd5bC9yPhYdcPpVI4+DVV4TQtPSnLmunfOlzm2kjm2kLvlSt3b//WhCqxkZZh4ZzHwFAADeQZB1m1DICaumUNqYwwZMMDZh1YRcWgQAAIAPEGQBAABgJUp3AAAAsBJBFgAAAFYiyAIAAMBKBFkAAABYiSALAAAAKxFkAQAAYCWCLAAAAKxEkAUAAICVCLIAAACwEkEWAAAAViLIAgAAwEoEWQAAAFiJIAsAAAArEWQBAABgJYIsAAAArESQBQAAgJUIsgAAALASQRYAAABWIsgCAADASgRZAAAAWIkgCwAAACsRZAEAAGAlgiwAAACsRJAFAACAlQiyAAAAsBJBFgAAAFYiyAIAAMBKBFkAAABYiSALAAAA2ej/Ax5c4zL0qZxTAAAAAElFTkSuQmCC" + ] }, "metadata": {}, "output_type": "display_data" } ], - "execution_count": 4 + "source": [ + "hypershap.plot_si_graph()" + ] } ], "metadata": { diff --git a/pyproject.toml b/pyproject.toml index 7eb4d9f..becc17a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,10 @@ Homepage = "https://automl.github.io/hypershap/" Repository = "https://github.com/automl/hypershap" Documentation = "https://automl.github.io/hypershap/" +[project.optional-dependencies] +optuna = ["optuna>=3.0"] +examples = ["notebook>=6.5.7"] + [dependency-groups] test = [ "pytest>=8.3.5", @@ -119,6 +123,11 @@ required-imports = ["from __future__ import annotations"] [tool.ruff.format] preview = true +[tool.deptry.per_rule_ignores] +# notebook is an optional extra for running example notebooks; deptry cannot detect +# implicit usage in .ipynb files so we suppress the false-positive DEP002 here. +DEP002 = ["notebook"] + [tool.coverage.report] skip_empty = true diff --git a/src/hypershap/__init__.py b/src/hypershap/__init__.py index 81399aa..9b20f13 100644 --- a/src/hypershap/__init__.py +++ b/src/hypershap/__init__.py @@ -8,6 +8,7 @@ from __future__ import annotations from .hypershap import HyperSHAP +from .optuna_task import from_optuna_study from .task import ExplanationTask from .utils import ConfigSpaceSearcher @@ -15,4 +16,5 @@ "ConfigSpaceSearcher", "ExplanationTask", "HyperSHAP", + "from_optuna_study", ] diff --git a/src/hypershap/optuna_task.py b/src/hypershap/optuna_task.py new file mode 100644 index 0000000..bd7f408 --- /dev/null +++ b/src/hypershap/optuna_task.py @@ -0,0 +1,265 @@ +"""Optuna integration for HyperSHAP. + +Provides utilities to convert an optuna ``Study`` into an :class:`~hypershap.task.ExplanationTask` +so that optuna HPO results can be analysed with HyperSHAP directly. + +Optuna is an **optional** dependency. Install it with:: + + pip install optuna + # or + pip install hypershap[optuna] +""" + +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + import optuna # type: ignore[import-untyped] + from ConfigSpace import Configuration, ConfigurationSpace + from sklearn.base import BaseEstimator + +from hypershap.task import ExplanationTask + + +def _require_optuna() -> None: + """Raise a helpful ``ImportError`` when optuna is not installed.""" + try: + import optuna # type: ignore[import-untyped] # noqa: F401 + except ImportError as exc: + msg = "optuna is required for this functionality. Install it with: pip install optuna (or: pip install hypershap[optuna])" + raise ImportError(msg) from exc + + +def _distribution_to_hp(name: str, dist: Any) -> Any: + """Convert a single optuna distribution to a ConfigSpace hyperparameter. + + Args: + name: The hyperparameter name. + dist: The optuna distribution object. + + Returns: + A ConfigSpace hyperparameter instance. + + Raises: + TypeError: If ``dist`` is not a supported distribution type. + + """ + from ConfigSpace import Categorical, Float, Integer + from optuna.distributions import ( # type: ignore[import-untyped] + CategoricalDistribution, + FloatDistribution, + IntDistribution, + ) + + if isinstance(dist, FloatDistribution): + if dist.step is not None: + msg = ( + f"Hyperparameter '{name}' has a step size ({dist.step}) which is not " + "supported by ConfigSpace. The step will be ignored." + ) + warnings.warn(msg, UserWarning, stacklevel=3) + return Float(name, bounds=(dist.low, dist.high), log=dist.log) + + if isinstance(dist, IntDistribution): + if dist.step != 1: + msg = ( + f"Hyperparameter '{name}' has a step size ({dist.step}) which is not " + "supported by ConfigSpace. The step will be ignored." + ) + warnings.warn(msg, UserWarning, stacklevel=3) + return Integer(name, bounds=(dist.low, dist.high), log=dist.log) + + if isinstance(dist, CategoricalDistribution): + return Categorical(name, items=list(dist.choices)) + + msg = ( + f"Unsupported optuna distribution type for hyperparameter '{name}': {type(dist).__name__}. " + "Supported types are FloatDistribution, IntDistribution, CategoricalDistribution." + ) + raise TypeError(msg) + + +def study_to_config_space(study: optuna.Study) -> ConfigurationSpace: + """Build a :class:`~ConfigSpace.ConfigurationSpace` from an optuna :class:`~optuna.Study`. + + The hyperparameter distributions are inferred from the completed trials in the study. + Supported distribution types: + + * :class:`~optuna.distributions.FloatDistribution` → ``Float`` hyperparameter + * :class:`~optuna.distributions.IntDistribution` → ``Integer`` hyperparameter + * :class:`~optuna.distributions.CategoricalDistribution` → ``Categorical`` hyperparameter + + Args: + study: A completed (or partially completed) optuna study. + + Returns: + A :class:`~ConfigSpace.ConfigurationSpace` covering all hyperparameters + observed in the study's completed trials. + + Raises: + ImportError: If ``optuna`` or ``ConfigSpace`` are not installed. + ValueError: If no completed trials are found in the study. + TypeError: If an unsupported optuna distribution type is encountered. + + """ + _require_optuna() + + import optuna as opt # type: ignore[import-untyped] + from ConfigSpace import ConfigurationSpace + + completed_trials = [t for t in study.trials if t.state == opt.trial.TrialState.COMPLETE] + if not completed_trials: + msg = "No completed trials found in the study." + raise ValueError(msg) + + # Collect distributions; later trials may add new HPs (conditional case). + distributions: dict[str, object] = {} + for trial in completed_trials: + for name, dist in trial.distributions.items(): + if name not in distributions: + distributions[name] = dist + + hyperparameters = [_distribution_to_hp(name, dist) for name, dist in distributions.items()] + + cs = ConfigurationSpace() + cs.add(hyperparameters) + return cs + + +def study_to_data( + study: optuna.Study, + config_space: ConfigurationSpace | None = None, + negate: bool | None = None, +) -> list[tuple[Configuration, float]]: + """Convert completed optuna trials into hypershap-compatible ``(Configuration, float)`` pairs. + + Args: + study: A completed (or partially completed) optuna study. + config_space: The target :class:`~ConfigSpace.ConfigurationSpace`. If ``None``, it is + inferred automatically via :func:`study_to_config_space`. + negate: Whether to negate the objective values before storing them. + HyperSHAP follows the convention that *higher values are better*, so for + minimisation studies the values should be negated. + If ``None`` (default), the sign is determined automatically from + ``study.direction``. + + Returns: + A list of ``(Configuration, float)`` tuples suitable for + :meth:`~hypershap.task.ExplanationTask.from_data`. + + Raises: + ImportError: If ``optuna`` or ``ConfigSpace`` are not installed. + ValueError: If no completed trials can be converted to valid configurations. + + """ + _require_optuna() + + import optuna as opt # type: ignore[import-untyped] + from ConfigSpace import Configuration + + if config_space is None: + config_space = study_to_config_space(study) + + # Auto-detect sign from study direction (study.directions is always a list in optuna >= 3). + if negate is None: + if len(study.directions) > 1: + msg = "Multi-objective optuna studies are not supported. Please select a single objective before creating an ExplanationTask." + raise ValueError(msg) + negate = study.directions[0] == opt.study.StudyDirection.MINIMIZE + + hp_names = list(config_space.keys()) + completed_trials = [t for t in study.trials if t.state == opt.trial.TrialState.COMPLETE and t.value is not None] + + data: list[tuple[Configuration, float]] = [] + skipped = 0 + for trial in completed_trials: + # Some HPs may be absent in trials that use conditional search (e.g. early stopping). + missing = [name for name in hp_names if name not in trial.params] + if missing: + skipped += 1 + continue + + try: + values = {name: trial.params[name] for name in hp_names} + config = Configuration(config_space, values=values) + trial_value: float = trial.value # type: ignore[assignment] # filtered above + value = -trial_value if negate else trial_value + data.append((config, value)) + except Exception: # noqa: BLE001 + skipped += 1 + continue + + if skipped: + msg = ( + f"{skipped} trial(s) were skipped because they could not be converted to valid " + "ConfigSpace configurations (missing hyperparameter values or constraint violations)." + ) + warnings.warn(msg, UserWarning, stacklevel=2) + + if not data: + msg = ( + "No trials could be converted to valid configurations. " + "Check that the study has completed trials and that the hyperparameter " + "distributions are supported." + ) + raise ValueError(msg) + + return data + + +def from_optuna_study( + study: optuna.Study, + negate: bool | None = None, + base_model: BaseEstimator | None = None, +) -> ExplanationTask: + """Create an :class:`~hypershap.task.ExplanationTask` directly from an optuna study. + + This is the main entry point for the optuna integration. It automatically: + + 1. Extracts the hyperparameter search space from the study's trial distributions. + 2. Converts completed trial results into ``(Configuration, float)`` pairs. + 3. Fits a surrogate model on those pairs and wraps everything in an + :class:`~hypershap.task.ExplanationTask`. + + For minimisation studies (``study.direction == StudyDirection.MINIMIZE``) the + objective values are negated so that HyperSHAP's *higher-is-better* convention + is respected. Pass ``negate=False`` to disable this behaviour. + + Example:: + + import optuna + from hypershap import HyperSHAP + from hypershap.optuna_task import from_optuna_study + + study = optuna.load_study(study_name="my_study", storage="sqlite:///my_study.db") + task = from_optuna_study(study) + + hs = HyperSHAP(task) + iv = hs.tunability() + hs.plot_stacked_bar(iv) + + Args: + study: A completed (or partially completed) optuna study. + negate: Whether to negate objective values. Defaults to ``None``, meaning + minimisation studies are negated automatically. + base_model: An optional sklearn-compatible regressor to use as the surrogate + model. Defaults to ``RandomForestRegressor`` (same as the rest of + HyperSHAP). + + Returns: + An :class:`~hypershap.task.ExplanationTask` ready for downstream HyperSHAP + analysis. + + Raises: + ImportError: If ``optuna`` is not installed. + ValueError: If the study contains no usable completed trials. + TypeError: If the study uses unsupported distribution types. + + """ + _require_optuna() + + config_space = study_to_config_space(study) + data = study_to_data(study, config_space=config_space, negate=negate) + return ExplanationTask.from_data(config_space, data, base_model=base_model) diff --git a/tests/test_optuna_task.py b/tests/test_optuna_task.py new file mode 100644 index 0000000..fa884d7 --- /dev/null +++ b/tests/test_optuna_task.py @@ -0,0 +1,362 @@ +"""Tests for the optuna integration (hypershap.optuna_task). + +All tests are skipped automatically when optuna is not installed. +""" + +from __future__ import annotations + +import pytest + +optuna = pytest.importorskip("optuna", reason="optuna is not installed") + +import math # noqa: E402 +from typing import TYPE_CHECKING # noqa: E402 + +from ConfigSpace import ( # noqa: E402 + CategoricalHyperparameter, + ConfigurationSpace, + Float, + UniformFloatHyperparameter, + UniformIntegerHyperparameter, +) + +if TYPE_CHECKING: + import optuna as opt + +from hypershap import ExplanationTask, HyperSHAP, from_optuna_study # noqa: E402 +from hypershap.optuna_task import study_to_config_space, study_to_data # noqa: E402 + +# --------------------------------------------------------------------------- +# Shared objective (matches SimpleBlackboxFunction: 0.7*a + 2.0*b) +# --------------------------------------------------------------------------- + +A_COEFF = 0.7 +B_COEFF = 2.0 +N_TRIALS = 300 +N_HPS = 3 # a, b, c +B_UPPER = 10 # upper bound for integer HP "b" + + +def _linear_objective_max(trial: opt.Trial) -> float: + """Maximisation objective: 0.7*a + 2.0*b (float + int + categorical HPs).""" + a = trial.suggest_float("a", 0.0, 1.0) + b = trial.suggest_int("b", 0, 10) + c = trial.suggest_categorical("c", ["X", "Y"]) + bonus = math.sin(a) * 0.01 if c == "X" else math.cos(a) * 0.01 # tiny noise + return A_COEFF * a + B_COEFF * b + bonus + + +def _linear_objective_min(trial: opt.Trial) -> float: + """Minimisation objective: -(0.7*a + 2.0*b).""" + return -_linear_objective_max(trial) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def maximize_study() -> opt.Study: + """Optuna maximisation study with float, int and categorical HPs.""" + optuna.logging.set_verbosity(optuna.logging.WARNING) + study = optuna.create_study( + direction="maximize", + sampler=optuna.samplers.TPESampler(seed=0), + ) + study.optimize(_linear_objective_max, n_trials=N_TRIALS) + return study + + +@pytest.fixture(scope="module") +def minimize_study() -> opt.Study: + """Optuna minimisation study (negated objective, same landscape).""" + optuna.logging.set_verbosity(optuna.logging.WARNING) + study = optuna.create_study( + direction="minimize", + sampler=optuna.samplers.TPESampler(seed=0), + ) + study.optimize(_linear_objective_min, n_trials=N_TRIALS) + return study + + +@pytest.fixture(scope="module") +def float_only_study() -> opt.Study: + """Minimal study with a single float HP for targeted conversion tests.""" + optuna.logging.set_verbosity(optuna.logging.WARNING) + + def obj(trial: opt.Trial) -> float: + x = trial.suggest_float("x", -1.0, 1.0) + return -(x**2) # maximise at x=0 + + study = optuna.create_study(direction="maximize") + study.optimize(obj, n_trials=50) + return study + + +# --------------------------------------------------------------------------- +# study_to_config_space +# --------------------------------------------------------------------------- + + +def test_config_space_hp_count(maximize_study: opt.Study) -> None: + """The inferred ConfigSpace must contain exactly the HPs used in the study.""" + cs = study_to_config_space(maximize_study) + assert len(cs) == N_HPS + + +def test_config_space_hp_names(maximize_study: opt.Study) -> None: + """HP names must match the suggest_* names used in the study.""" + cs = study_to_config_space(maximize_study) + assert set(cs.keys()) == {"a", "b", "c"} + + +def test_config_space_float_bounds(maximize_study: opt.Study) -> None: + """Float HP bounds must be transferred correctly.""" + cs = study_to_config_space(maximize_study) + hp: UniformFloatHyperparameter = cs["a"] # type: ignore[assignment] + assert isinstance(hp, UniformFloatHyperparameter) + assert hp.lower == pytest.approx(0.0) + assert hp.upper == pytest.approx(1.0) + + +def test_config_space_int_bounds(maximize_study: opt.Study) -> None: + """Integer HP bounds must be transferred correctly.""" + cs = study_to_config_space(maximize_study) + hp: UniformIntegerHyperparameter = cs["b"] # type: ignore[assignment] + assert isinstance(hp, UniformIntegerHyperparameter) + assert hp.lower == 0 + assert hp.upper == B_UPPER + + +def test_config_space_categorical_choices(maximize_study: opt.Study) -> None: + """Categorical choices must be transferred correctly.""" + cs = study_to_config_space(maximize_study) + hp: CategoricalHyperparameter = cs["c"] # type: ignore[assignment] + assert isinstance(hp, CategoricalHyperparameter) + assert set(hp.choices) == {"X", "Y"} + + +def test_config_space_float_only(float_only_study: opt.Study) -> None: + """Single-float study produces a one-dimensional ConfigSpace.""" + cs = study_to_config_space(float_only_study) + assert len(cs) == 1 + assert "x" in cs + hp: UniformFloatHyperparameter = cs["x"] # type: ignore[assignment] + assert hp.lower == pytest.approx(-1.0) + assert hp.upper == pytest.approx(1.0) + + +def test_config_space_no_completed_trials_raises() -> None: + """study_to_config_space must raise ValueError when no completed trials exist.""" + empty_study = optuna.create_study() + with pytest.raises(ValueError, match="No completed trials"): + study_to_config_space(empty_study) + + +def test_config_space_unsupported_distribution_raises() -> None: + """study_to_config_space must raise TypeError for unsupported distributions.""" + from unittest.mock import MagicMock + + unknown_dist = MagicMock() + unknown_dist.__class__ = type("UnknownDist", (), {}) # type: ignore[assignment] + + mock_trial = MagicMock() + mock_trial.state = optuna.trial.TrialState.COMPLETE + mock_trial.distributions = {"z": unknown_dist} + mock_trial.params = {"z": 0.5} + + mock_study = MagicMock() + mock_study.trials = [mock_trial] + + with pytest.raises(TypeError, match="Unsupported optuna distribution"): + study_to_config_space(mock_study) + + +# --------------------------------------------------------------------------- +# study_to_data +# --------------------------------------------------------------------------- + + +def test_data_length(maximize_study: opt.Study) -> None: + """study_to_data must return one entry per completed trial.""" + data = study_to_data(maximize_study) + completed = sum(1 for t in maximize_study.trials if t.value is not None) + assert len(data) == completed + + +def test_data_tuples_structure(maximize_study: opt.Study) -> None: + """Each element must be a (Configuration, float) tuple.""" + from ConfigSpace import Configuration + + data = study_to_data(maximize_study) + for config, value in data: + assert isinstance(config, Configuration) + assert isinstance(value, float) + + +def test_data_config_belongs_to_space(maximize_study: opt.Study) -> None: + """Every configuration in the converted data must be valid in the inferred CS.""" + cs = study_to_config_space(maximize_study) + data = study_to_data(maximize_study, config_space=cs) + for config, _ in data: + # ConfigSpace raises if the config doesn't belong to the space + cs.check_configuration_vector_representation(config.get_array()) + + +def test_data_maximize_not_negated(maximize_study: opt.Study) -> None: + """Values from a maximisation study must NOT be negated.""" + data = study_to_data(maximize_study) + trial_values = [t.value for t in maximize_study.trials if t.value is not None] + data_values = [v for _, v in data] + assert sorted(trial_values) == pytest.approx(sorted(data_values), rel=1e-6) + + +def test_data_minimize_negated(minimize_study: opt.Study) -> None: + """Values from a minimisation study must be negated (sign-flipped).""" + data = study_to_data(minimize_study) + trial_values = [t.value for t in minimize_study.trials if t.value is not None] + data_values = [v for _, v in data] + assert sorted([-v for v in trial_values]) == pytest.approx(sorted(data_values), rel=1e-6) + + +def test_data_negate_override(maximize_study: opt.Study) -> None: + """Passing negate=True must negate even a maximisation study.""" + data_normal = study_to_data(maximize_study, negate=False) + data_negated = study_to_data(maximize_study, negate=True) + for (_, v_normal), (_, v_negated) in zip(data_normal, data_negated, strict=True): + assert v_normal == pytest.approx(-v_negated) + + +def test_data_no_completed_trials_raises() -> None: + """study_to_data must raise ValueError when no completed trials can be converted.""" + empty_study = optuna.create_study() + cs = ConfigurationSpace() + cs.add(Float("x", bounds=(0.0, 1.0))) + with pytest.raises(ValueError, match="No trials could be converted"): + study_to_data(empty_study, config_space=cs) + + +def test_data_multi_objective_raises() -> None: + """study_to_data must raise ValueError for multi-objective studies.""" + multi_study = optuna.create_study(directions=["minimize", "maximize"]) + + def multi_obj(trial: opt.Trial) -> tuple[float, float]: + x = trial.suggest_float("x", 0.0, 1.0) + return x, 1.0 - x + + multi_study.optimize(multi_obj, n_trials=10) + with pytest.raises(ValueError, match="Multi-objective"): + study_to_data(multi_study) + + +# --------------------------------------------------------------------------- +# from_optuna_study +# --------------------------------------------------------------------------- + + +def test_from_optuna_study_returns_explanation_task(maximize_study: opt.Study) -> None: + """from_optuna_study must return a valid ExplanationTask.""" + task = from_optuna_study(maximize_study) + assert isinstance(task, ExplanationTask) + + +def test_from_optuna_study_hp_names(maximize_study: opt.Study) -> None: + """ExplanationTask must contain all HPs from the study.""" + task = from_optuna_study(maximize_study) + assert set(task.get_hyperparameter_names()) == {"a", "b", "c"} + + +def test_from_optuna_study_hp_count(maximize_study: opt.Study) -> None: + """ExplanationTask must have the correct number of HPs.""" + task = from_optuna_study(maximize_study) + assert task.get_num_hyperparameters() == N_HPS + + +def test_from_optuna_study_single_surrogate(maximize_study: opt.Study) -> None: + """ExplanationTask from a single study must have a single (not list) surrogate.""" + task = from_optuna_study(maximize_study) + assert not task.is_multi_data() + model = task.get_single_surrogate_model() + assert model is not None + + +def test_from_optuna_study_surrogate_quality(maximize_study: opt.Study) -> None: + """The surrogate must be a reasonable fit: b matters more than a.""" + task = from_optuna_study(maximize_study) + cs = task.config_space + + # Evaluate two configs that differ only in b (0 vs 10). + from ConfigSpace import Configuration + + low_b = Configuration(cs, values={"a": 0.5, "b": 0, "c": "X"}) + high_b = Configuration(cs, values={"a": 0.5, "b": 10, "c": "X"}) + + val_low = task.get_single_surrogate_model().evaluate_config(low_b) + val_high = task.get_single_surrogate_model().evaluate_config(high_b) + + # The linear objective grows strongly with b, so high_b must score higher. + assert val_high > val_low, "Surrogate should predict higher value for b=10 vs b=0" + + +def test_from_optuna_study_minimize_auto_negate(minimize_study: opt.Study) -> None: + """from_optuna_study on a minimisation study must auto-negate values. + + After negation, high-b configs must still score better than low-b configs. + """ + task = from_optuna_study(minimize_study) + cs = task.config_space + + from ConfigSpace import Configuration + + low_b = Configuration(cs, values={"a": 0.5, "b": 0, "c": "X"}) + high_b = Configuration(cs, values={"a": 0.5, "b": 10, "c": "X"}) + + val_low = task.get_single_surrogate_model().evaluate_config(low_b) + val_high = task.get_single_surrogate_model().evaluate_config(high_b) + + assert val_high > val_low, "After auto-negation of a minimisation study, higher b should still score better" + + +def test_from_optuna_study_custom_base_model(maximize_study: opt.Study) -> None: + """from_optuna_study must accept a custom sklearn surrogate model.""" + from sklearn.ensemble import GradientBoostingRegressor + + task = from_optuna_study(maximize_study, base_model=GradientBoostingRegressor(n_estimators=50, random_state=0)) + assert isinstance(task, ExplanationTask) + assert not task.is_multi_data() + + +# --------------------------------------------------------------------------- +# HyperSHAP integration +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def optuna_hypershap(maximize_study: opt.Study) -> HyperSHAP: + """HyperSHAP instance built from an optuna study.""" + task = from_optuna_study(maximize_study) + return HyperSHAP(explanation_task=task) + + +def test_hypershap_tunability(optuna_hypershap: HyperSHAP) -> None: + """Tunability must run end-to-end and return non-None InteractionValues.""" + baseline = optuna_hypershap.explanation_task.config_space.get_default_configuration() + iv = optuna_hypershap.tunability(baseline_config=baseline, n_samples=5_000) + assert iv is not None + # b (index 1) must be identified as more important than a (index 0) + assert iv.dict_values[(1,)] > iv.dict_values[(0,)], ( + "b should have higher tunability than a (B_COEFF=2.0 > A_COEFF=0.7)" + ) + + +def test_hypershap_ablation(optuna_hypershap: HyperSHAP, maximize_study: opt.Study) -> None: + """Ablation must run end-to-end and return non-None InteractionValues.""" + from ConfigSpace import Configuration + + cs = optuna_hypershap.explanation_task.config_space + baseline = cs.get_default_configuration() + config_of_interest = Configuration(cs, values=maximize_study.best_params) + + iv = optuna_hypershap.ablation(config_of_interest=config_of_interest, baseline_config=baseline) + assert iv is not None diff --git a/uv.lock b/uv.lock index 4ea14df..29f67aa 100644 --- a/uv.lock +++ b/uv.lock @@ -38,6 +38,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, ] +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + [[package]] name = "alive-progress" version = "3.3.0" @@ -605,6 +620,9 @@ dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/bc/2a53b259ec14101a42690992ea8fc2456edcacc73ede62f803dd3c4b0bdc/configspace-1.2.1.tar.gz", hash = "sha256:2fc1c4477f7839d38b53d8813c2581967df57ccd7d8b4b506e879a8835cb964c", size = 130983, upload-time = "2024-11-21T10:18:39.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d0/e899691a9cdec2b866fbf7c39747195b9402e64260632f415c851ae8e503/configspace-1.2.1-py3-none-any.whl", hash = "sha256:78d3cef1bdb447bf25c2433891580bf6c2b468d47b729356a8646d5b7cd9eb5a", size = 117557, upload-time = "2025-12-16T13:10:39.616Z" }, +] [[package]] name = "contourpy" @@ -1211,6 +1229,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/18/36503ea63e1ecd0a95590d7b6b8b7d227a1e4541a154e1612a231def1bdc/graphemeu-0.7.2-py3-none-any.whl", hash = "sha256:1444520f6899fd30114fc2a39f297d86d10fa0f23bf7579f772f8bc7efaa2542", size = 22670, upload-time = "2025-01-15T09:48:57.241Z" }, ] +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, + { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" }, + { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, + { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +] + [[package]] name = "griffe" version = "1.9.0" @@ -1329,6 +1407,14 @@ dependencies = [ { name = "shapiq" }, ] +[package.optional-dependencies] +examples = [ + { name = "notebook" }, +] +optuna = [ + { name = "optuna" }, +] + [package.dev-dependencies] carps = [ { name = "carps" }, @@ -1371,10 +1457,13 @@ requires-dist = [ { name = "configspace", specifier = ">=1.2.1" }, { name = "matplotlib", specifier = ">=3.10.5" }, { name = "networkx", specifier = ">=3.4.2" }, + { name = "notebook", marker = "extra == 'examples'", specifier = ">=6.5.7" }, { name = "numpy", specifier = ">=2.2.6" }, + { name = "optuna", marker = "extra == 'optuna'", specifier = ">=3.0" }, { name = "scikit-learn", specifier = ">=1.7.1" }, { name = "shapiq", specifier = "==1.4.1" }, ] +provides-extras = ["optuna", "examples"] [package.metadata.requires-dev] carps = [{ name = "carps", specifier = ">=1.0.4" }] @@ -1994,6 +2083,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/56/ed35668130e32dbfad2eb37356793b0a95f23494ab5be7d9bf5cb75850ee/llvmlite-0.45.1-cp313-cp313-win_amd64.whl", hash = "sha256:080e6f8d0778a8239cd47686d402cb66eb165e421efa9391366a9b7e5810a38b", size = 38132232, upload-time = "2025-10-01T18:05:14.477Z" }, ] +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + [[package]] name = "markdown" version = "3.8.2" @@ -2708,6 +2809,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, ] +[[package]] +name = "optuna" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "colorlog" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/9b/62f120fb2ecbc4338bee70c5a3671c8e561714f3aa1a046b897ff142050e/optuna-4.8.0.tar.gz", hash = "sha256:6f7043e9f8ecb5e607af86a7eb00fb5ec2be26c3b08c201209a73d36aff37a38", size = 482603, upload-time = "2026-03-16T04:59:58.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/24/7c731839566d30dc70556d9824ef17692d896c15e3df627bce8c16f753e1/optuna-4.8.0-py3-none-any.whl", hash = "sha256:c57a7682679c36bfc9bca0da430698179e513874074b71bebedb0334964ab930", size = 419456, upload-time = "2026-03-16T04:59:56.977Z" }, +] + [[package]] name = "overrides" version = "7.7.0" @@ -4215,6 +4335,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/ec/c85f95289e4638faf889ca02b588ad3af0f3ef8f92b99bee3ce936ef3860/sparse_transform-0.2.1-py3-none-any.whl", hash = "sha256:d01b19f8add68e2b4c98a17272830af0e03d163667f03b1dc2b0c9eb8471727b", size = 27034, upload-time = "2025-04-03T08:11:38.551Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/67/1235676e93dd3b742a4a8eddfae49eea46c85e3eed29f0da446a8dd57500/sqlalchemy-2.0.48-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7001dc9d5f6bb4deb756d5928eaefe1930f6f4179da3924cbd95ee0e9f4dce89", size = 2157384, upload-time = "2026-03-02T15:38:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/fa728b856daa18c10e1390e76f26f64ac890c947008284387451d56ca3d0/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a89ce07ad2d4b8cfc30bd5889ec40613e028ed80ef47da7d9dd2ce969ad30e0", size = 3236981, upload-time = "2026-03-02T15:58:53.53Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/6c4395649a212a6c603a72c5b9ab5dce3135a1546cfdffa3c427e71fd535/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10853a53a4a00417a00913d270dddda75815fcb80675874285f41051c094d7dd", size = 3235232, upload-time = "2026-03-02T15:52:25.654Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/58f845e511ac0509765a6f85eb24924c1ef0d54fb50de9d15b28c3601458/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fac0fa4e4f55f118fd87177dacb1c6522fe39c28d498d259014020fec9164c29", size = 3188106, upload-time = "2026-03-02T15:58:55.193Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/6dcc7bfa5f5794c3a095e78cd1de8269dfb5584dfd4c2c00a50d3c1ade44/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3713e21ea67bca727eecd4a24bf68bcd414c403faae4989442be60994301ded0", size = 3209522, upload-time = "2026-03-02T15:52:27.407Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5a/b632875ab35874d42657f079529f0745410604645c269a8c21fb4272ff7a/sqlalchemy-2.0.48-cp310-cp310-win32.whl", hash = "sha256:d404dc897ce10e565d647795861762aa2d06ca3f4a728c5e9a835096c7059018", size = 2117695, upload-time = "2026-03-02T15:46:51.389Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/9752eb2a41afdd8568e41ac3c3128e32a0a73eada5ab80483083604a56d1/sqlalchemy-2.0.48-cp310-cp310-win_amd64.whl", hash = "sha256:841a94c66577661c1f088ac958cd767d7c9bf507698f45afffe7a4017049de76", size = 2140928, upload-time = "2026-03-02T15:46:52.992Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, + { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, + { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, + { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" }, + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +] + [[package]] name = "sshtunnel" version = "0.4.0"