Repository: datamol-io/safe (language: python)

Issue:
embedding layer
Hi, 
What is the easiest way to extract the embedding layer so we can get a  768-dim or x-dim for every molecule ? 


Discussion / hints:

Fix patch:
diff --git a/docs/tutorials/extracting-representation-molfeat.ipynb b/docs/tutorials/extracting-representation-molfeat.ipynb
new file mode 100644
index 0000000..17977c2
--- /dev/null
+++ b/docs/tutorials/extracting-representation-molfeat.ipynb
@@ -0,0 +1,336 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "%load_ext autoreload\n",
+    "%autoreload 2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import safe\n",
+    "import torch\n",
+    "import datamol as dm\n",
+    "import types\n",
+    "from molfeat.trans.pretrained import PretrainedMolTransformer\n",
+    "from molfeat.trans.pretrained import PretrainedHFTransformer\n",
+    "\n",
+    "from molfeat.trans.pretrained.hf_transformers import HFModel\n",
+    "from safe.trainer.model import SAFEDoubleHeadsModel\n",
+    "from safe.tokenizer import SAFETokenizer\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Loading the SAFE-GPT model into molfeat\n",
+    "\n",
+    "Because the SAFE model is not a standard HuggingFace `transformers` model, we need to wrap it.\n",
+    "\n",
+    "Why are we doing this ? Because we want to leverage the structure of `molfeat` and not have to write our own pooling for the model. This can be done by using the huggingface molecule transformer `PretrainedHFTransformer`  rather than the general purpose pretrained model class `PretrainedMolTransformer` where we will have to define our own `_embed` and `_convert` function."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "safe_model = SAFEDoubleHeadsModel.from_pretrained(\"datamol-io/safe-gpt\")\n",
+    "safe_tokenizer = SAFETokenizer.from_pretrained(\"datamol-io/safe-gpt\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We now need to build the `molfeat`'s `HFModel` instance by wrapping our model."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "safe_hf_model = HFModel.from_pretrained(safe_model, safe_tokenizer.get_pretrained())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "You can put the above process in the `__init__` of the `SAFEMolTransformer` if you wish as we will be doing below."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Building the SAFE Molecule Transformers\n",
+    "\n",
+    "We have multiple options here, we can override the `_convert` method or even the `_embed` method but the best thing about `molfeat` is how flexible it is and all the shortcuts it provides. \n",
+    "\n",
+    "In this case, we just need to change the custom \n",
+    "# so really we just need our custom converter"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "\u001b[32m2023-12-20 22:57:39.310\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mmolfeat.trans.base\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m51\u001b[0m - \u001b[33m\u001b[1mThe 'SAFEMolTransformer' interaction has been superseded by a new class with id 0x2ad77d6a0\u001b[0m\n"
+     ]
+    }
+   ],
+   "source": [
+    "class SAFEMolTransformer(PretrainedHFTransformer):\n",
+    "    \"\"\"Build the SAFE Molecule transformers, the only thing we need to define is \n",
+    "    how we convert the input molecules into the safe format\"\"\"\n",
+    "    def __init__(self, kind=None, notation=\"safe\", **kwargs):\n",
+    "        if kind is None:\n",
+    "            # we load the default SAFE model if the exact SAFE GPT model \n",
+    "            # to use is not provided\n",
+    "            safe_model = SAFEDoubleHeadsModel.from_pretrained(\"datamol-io/safe-gpt\")\n",
+    "            safe_tokenizer = SAFETokenizer.from_pretrained(\"datamol-io/safe-gpt\")\n",
+    "            kind = HFModel.from_pretrained(safe_model, safe_tokenizer.get_pretrained())\n",
+    "        super().__init__(kind, notation=None, **kwargs)\n",
+    "        # now we change the internal converter\n",
+    "        # overriding the internal converter of SmilesConverter leverages the exception handling\n",
+    "        # The SAFE-GPT model was trained on a slightly different splitting algorithm compared to the default BRICS\n",
+    "        # this does not change anything in theory, it just try harder to break bonds even if there are no BRICS bonds.\n",
+    "        self.converter.converter = types.SimpleNamespace(decode=safe.decode, encode=safe.utils.convert_to_safe)\n",
+    "        # you could also do any of the following:\n",
+    "        # self.converter = types.SimpleNamespace(decode=safe.decode, encode=safe.encode)\n",
+    "        # self.converter = safe # the safe module\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Let's use the GPT pooler which uses the last non padding token (often `eos`) since the model is GPT2 like. For other options, see: https://molfeat-docs.datamol.io/stable/api/molfeat.utils.html#pooling"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 116,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<style>#sk-container-id-14 {color: black;}#sk-container-id-14 pre{padding: 0;}#sk-container-id-14 div.sk-toggleable {background-color: white;}#sk-container-id-14 label.sk-toggleable__label {cursor: pointer;display: block;width: 100%;margin-bottom: 0;padding: 0.3em;box-sizing: border-box;text-align: center;}#sk-container-id-14 label.sk-toggleable__label-arrow:before {content: \"▸\";float: left;margin-right: 0.25em;color: #696969;}#sk-container-id-14 label.sk-toggleable__label-arrow:hover:before {color: black;}#sk-container-id-14 div.sk-estimator:hover label.sk-toggleable__label-arrow:before {color: black;}#sk-container-id-14 div.sk-toggleable__content {max-height: 0;max-width: 0;overflow: hidden;text-align: left;background-color: #f0f8ff;}#sk-container-id-14 div.sk-toggleable__content pre {margin: 0.2em;color: black;border-radius: 0.25em;background-color: #f0f8ff;}#sk-container-id-14 input.sk-toggleable__control:checked~div.sk-toggleable__content {max-height: 200px;max-width: 100%;overflow: auto;}#sk-container-id-14 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {content: \"▾\";}#sk-container-id-14 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {background-color: #d4ebff;}#sk-container-id-14 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {background-color: #d4ebff;}#sk-container-id-14 input.sk-hidden--visually {border: 0;clip: rect(1px 1px 1px 1px);clip: rect(1px, 1px, 1px, 1px);height: 1px;margin: -1px;overflow: hidden;padding: 0;position: absolute;width: 1px;}#sk-container-id-14 div.sk-estimator {font-family: monospace;background-color: #f0f8ff;border: 1px dotted black;border-radius: 0.25em;box-sizing: border-box;margin-bottom: 0.5em;}#sk-container-id-14 div.sk-estimator:hover {background-color: #d4ebff;}#sk-container-id-14 div.sk-parallel-item::after {content: \"\";width: 100%;border-bottom: 1px solid gray;flex-grow: 1;}#sk-container-id-14 div.sk-label:hover label.sk-toggleable__label {background-color: #d4ebff;}#sk-container-id-14 div.sk-serial::before {content: \"\";position: absolute;border-left: 1px solid gray;box-sizing: border-box;top: 0;bottom: 0;left: 50%;z-index: 0;}#sk-container-id-14 div.sk-serial {display: flex;flex-direction: column;align-items: center;background-color: white;padding-right: 0.2em;padding-left: 0.2em;position: relative;}#sk-container-id-14 div.sk-item {position: relative;z-index: 1;}#sk-container-id-14 div.sk-parallel {display: flex;align-items: stretch;justify-content: center;background-color: white;position: relative;}#sk-container-id-14 div.sk-item::before, #sk-container-id-14 div.sk-parallel-item::before {content: \"\";position: absolute;border-left: 1px solid gray;box-sizing: border-box;top: 0;bottom: 0;left: 50%;z-index: -1;}#sk-container-id-14 div.sk-parallel-item {display: flex;flex-direction: column;z-index: 1;position: relative;background-color: white;}#sk-container-id-14 div.sk-parallel-item:first-child::after {align-self: flex-end;width: 50%;}#sk-container-id-14 div.sk-parallel-item:last-child::after {align-self: flex-start;width: 50%;}#sk-container-id-14 div.sk-parallel-item:only-child::after {width: 0;}#sk-container-id-14 div.sk-dashed-wrapped {border: 1px dashed gray;margin: 0 0.4em 0.5em 0.4em;box-sizing: border-box;padding-bottom: 0.4em;background-color: white;}#sk-container-id-14 div.sk-label label {font-family: monospace;font-weight: bold;display: inline-block;line-height: 1.2em;}#sk-container-id-14 div.sk-label-container {text-align: center;}#sk-container-id-14 div.sk-container {/* jupyter's `normalize.less` sets `[hidden] { display: none; }` but bootstrap.min.css set `[hidden] { display: none !important; }` so we also need the `!important` here to be able to override the default hidden behavior on the sphinx rendered scikit-learn.org. See: https://github.com/scikit-learn/scikit-learn/issues/21755 */display: inline-block !important;position: relative;}#sk-container-id-14 div.sk-text-repr-fallback {display: none;}</style><div id=\"sk-container-id-14\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>SAFEMolTransformer(dtype=np.float32)</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-14\" type=\"checkbox\" checked><label for=\"sk-estimator-id-14\" class=\"sk-toggleable__label sk-toggleable__label-arrow\">SAFEMolTransformer</label><div class=\"sk-toggleable__content\"><pre>SAFEMolTransformer(dtype=np.float32)</pre></div></div></div></div></div>"
+      ],
+      "text/plain": [
+       "SAFEMolTransformer(dtype=np.float32)"
+      ]
+     },
+     "execution_count": 116,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Let's use the GPT pooling method and only take the last hidden layer\n",
+    "safe_transformers = SAFEMolTransformer(pooling=\"gpt\", concat_layers=[-1])\n",
+    "safe_transformers"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 117,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "mols = dm.data.freesolv().iloc[:10].smiles.values"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 118,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([[ 0.05216356,  0.10754181,  0.07509107, ...,  0.04756968,\n",
+       "        -0.08228929, -0.11568106],\n",
+       "       [ 0.02449008,  0.04048932,  0.14489463, ...,  0.11410899,\n",
+       "        -0.02203353,  0.08706839],\n",
+       "       [-0.07425696,  0.11859665,  0.19010407, ...,  0.10526019,\n",
+       "         0.08878426, -0.06609854],\n",
+       "       ...,\n",
+       "       [ 0.07867863,  0.19300285,  0.23054805, ..., -0.00737952,\n",
+       "         0.07542405,  0.00289541],\n",
+       "       [ 0.12092628, -0.01785688,  0.19791883, ...,  0.13796932,\n",
+       "         0.11520796, -0.15333697],\n",
+       "       [-0.02005584,  0.13946685,  0.18568742, ...,  0.07080407,\n",
+       "         0.06991849, -0.07151204]], dtype=float32)"
+      ]
+     },
+     "execution_count": 118,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "safe_transformers(mols)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Basic Test"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 119,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from sklearn.ensemble import RandomForestRegressor\n",
+    "from sklearn.model_selection import train_test_split\n",
+    "from sklearn.pipeline import Pipeline\n",
+    "\n",
+    "df = dm.data.freesolv()\n",
+    "df[\"safe\"]  = df[\"smiles\"].apply(safe_transformers.converter.encode)\n",
+    "df = df.dropna(subset=\"safe\")\n",
+    "# we have to remove the molecules that cannot be converted \n",
+    "# (no breakable bonds with our default methodology)\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 120,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "\n",
+    "X, y = df[\"smiles\"].values, df[\"expt\"].values\n",
+    "X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=25, test_size=0.2)\n",
+    "\n",
+    "# The Molfeat transformer seemingly integrates with Scikit-learn Pipeline!\n",
+    "pipe = Pipeline([(\"feat\", safe_transformers), (\"rf\", RandomForestRegressor())])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 121,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "with dm.without_rdkit_log():\n",
+    "    pipe.fit(X_train, y_train)\n",
+    "    score = pipe.score(X_test, y_test)\n",
+    "    y_pred = pipe.predict(X_test)\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 122,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "R2 score: 0.4971483821661925\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\"R2 score:\", score)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 123,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "Text(0, 0.5, 'Preds')"
+      ]
+     },
+     "execution_count": 123,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAj4AAAGwCAYAAACpYG+ZAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/SrBM8AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA9uUlEQVR4nO3df3RU9Z3/8dckQhKQjIQICRJJBFuN0SJRJFUr0IJxKUXbdRcVVr5HUdDUotYVjlp+tCxSsLVrK6C16B6K9UdpldVyxIWjgiBCpBqpP6AgLGTUJDhDo0kwud8/shMzyfy48/vOvc/HOXNOZ3Jn8pkhu/Py8/68Px+XYRiGAAAAHCAr3QMAAABIFYIPAABwDIIPAABwDIIPAABwDIIPAABwDIIPAABwDIIPAABwjJPSPQCr6ejo0NGjRzVgwAC5XK50DwcAAJhgGIaOHz+uoUOHKisr9LwOwaeHo0ePqqSkJN3DAAAAMTh8+LCGDRsW8ucEnx4GDBggqfODy8/PT/NoAACAGT6fTyUlJV3f46EQfHrwl7fy8/MJPgAAZJhIy1RY3AwAAByD4AMAAByD4AMAAByD4AMAAByD4AMAAByD4AMAAByD4AMAAByD4AMAAByD4AMAAByDnZsBAEDc2jsM7TzQpE+Ot2jwgFyNKStQdpb1Dvu2VfBZunSp1q9fr/fee095eXn65je/qWXLlunrX/96uocGAIBtbayr16INe1Xvbel6rNidqwVTylVdUZzGkfVmq1LXK6+8oltvvVU7duzQpk2b9OWXX2rSpElqbm5O99AAALCljXX1mrO2NiD0SJLH26I5a2u1sa4+TSMLzmUYhpHuQSTLp59+qsGDB+uVV17Rt771LVPP8fl8crvd8nq9HFIKAEAY7R2GLlm2uVfo6e6Ufn30m2tGa+yIQUktfZn9/rZVqasnr9crSSooKAh5TWtrq1pbW7vu+3y+pI8LAAA72HmgKWzokaTPPj+h6x57wzKlL1uVurozDEN33HGHLrnkElVUVIS8bunSpXK73V23kpKSFI4SAIDM9cnx8KGnO6uUvmwbfGpqavT222/rySefDHvd/Pnz5fV6u26HDx9O0QgBAMhsgwfkmr7Wv65m0Ya9au9I3yobWwafH/7wh3r++ee1ZcsWDRs2LOy1OTk5ys/PD7gBAIDIxpQVqNidK7MrdwxJ9d4W7TzQlMxhhWWr4GMYhmpqarR+/Xpt3rxZZWVl6R4SAAC2lZ3l0oIp5ZJkOvxI0ZXIEs1WwefWW2/V2rVrtW7dOg0YMEAej0cej0dffPFFuocGAIAtVVcUa+X00Spymy97RVMiSzRbtbO7XMHz5po1azRz5kxTr0E7OwAA0WvvMLTj74269fe1+uyLE0GvcUkqcudq690TEt7a7sh2dhtlOAAAMkp2lksXjyzU/T84V3PW1kr6akGz9FUpbMGU8rQeZWGrUhcAAEivUKWvIneuVk4fnfZ9fGw14wMAANKvuqJYE8uLLHloKcEHAAAkXHaWS1UjBqV7GL1Q6gIAAI5B8AEAAI5B8AEAAI5B8AEAAI5B8AEAAI5B8AEAAI5B8AEAAI5B8AEAAI7BBoYAACBm7R2GJXdoDoXgAwAAYrKxrl6LNuxVvbel67Fid64WTClP+5lcoVDqAgAAUdtYV685a2sDQo8kebwtmrO2Vhvr6tM0svAIPgAAICrtHYYWbdgrI8jP/I8t2rBX7R3Brkgvgg8AAIjKzgNNvWZ6ujMk1XtbtPNAU9dj7R2Gtu9v1HN7jmj7/sa0hSLW+AAAgKh8cjx06Al2nZXWAjHjAwAAojJ4QK7p66y2FojgAwAAojKmrEDF7lyFalp3qXNGp3L4wLBrgQylfi0QwQcAAEQlO8ulBVPKJalX+PHfXzClXLs/OhZ2LZDUey1QshF8AABA1KorirVy+mgVuQPLXkXuXK2cPlrVFcXy+MytBTJ7XSKwuBkAAMSkuqJYE8uLQu7c3PSPVlOvY/a6RCD4AADgYPEeOZGd5VLViEFBf1bQv6+p1zB7XSIQfAAAcKhkt5kXufMSel0isMYHAAAHSkabec9NCiuHD1SxO3zre7G7c5YpVZjxAQDAYSIdOeFSZ5v5xPIi02WvULNH3/tGsR559UDQ3+VSZ/dXKk9zZ8YHAACHieXIiXDCzR498uoB3fStsl4zP8Xdur9SiRkfAAAcJtojJ8IxM3v0/F/r9cpd47X7o2MxL6JOFIIPAAAOE82RE5GYnT3a/dGxkN1fqUSpCwAAhzF75ISZRceJnD1KBYIPAAAOY/bICTOlqETOHqUCwQcAAAcyc+SEGYmcPUoF1vgAAOBQkY6cMMM/ezRnba1cUsAi52hnj1LBZRhG6s6CzwA+n09ut1ter1f5+fnpHg4AABkh2btAR2L2+5sZHwAAELdEzB6lAsEHAAAkRLgDS62Cxc0AAMAxCD4AAMAxCD4AAMAxCD4AAMAxWNwMAIBDtXcYlu/CSjSCDwAADpTufXfSxZalrocfflhlZWXKzc1VZWWlXnvttXQPCQAAy9hYV685a2t7naru8bZoztpabayrT9PIks92weepp57S3Llzdc899+itt97SpZdeqiuuuEKHDh1K99AAAEi79g5DizbsVbBjG/yPLdqwV+0d9jzYwXbB5xe/+IVuuOEG3XjjjTr77LP14IMPqqSkRCtXrgx6fWtrq3w+X8ANAAC72nmgqddMT3eGpHpvi3YeaErdoFLIVsGnra1Nu3fv1qRJkwIenzRpkl5//fWgz1m6dKncbnfXraSkJBVDBQAgLT45Hjr0xHJdprFV8GloaFB7e7uGDBkS8PiQIUPk8XiCPmf+/Pnyer1dt8OHD6diqAAApMXgAbkJvS7T2LKry+UKbMUzDKPXY345OTnKyclJxbAAAEi7MWUFKnbnyuNtCbrOxyWpyN3Z2m5HtprxKSwsVHZ2dq/ZnU8++aTXLBAAAFbU3mFo+/5GPbfniLbvb4x6kXGk52dnubRgSrmkzpDTnf/+ginltt3Px1YzPn379lVlZaU2bdqkq666quvxTZs2aerUqWkcGQAAkcW7t47Z51dXFGvl9NG9ri1ywD4+LsMwbNWv9tRTT2nGjBlatWqVqqqq9Mgjj+jRRx/Vu+++q+HDh0d8vs/nk9vtltfrVX5+fgpGDADAV3vr9PxS9s+7rJw+OmwgieX5dtq52ez3t61mfCTpX//1X9XY2KjFixervr5eFRUVevHFF02FHgAAUqFn4KgcPjDs3joude6tM+GsIdr90bFeQSXS3jz+508sLwoINtlZLlWNGJSU92hVtpvxiRczPgCAZApWjiro30dNzSciPrfndf4yljuvr655dEfE5z85a6xtg47Z729bLW4GAMDKQh0VYSb0BLvOf8TEy3uDb9nSk1335okGwQcAgBQIV46Klf+1/rTniKnr7bo3TzQIPgAApECkoyJiZahzJqigf99e7el+LnWWxey6N080CD4AAKRAsstMV44aKsmZe/NEg+ADAEAKmC0zFfTvG3B/UI/7oUwsL9LK6aNV5A78PUXu3Iit8E5iu3Z2AACsyOxREa/cNV67Pzomj69FTf9o1Sl5fbTkL+/pWHNbxCMmsrNcmlheZJu9eZKB4AMAQAr4j4qYs7ZWLikgxHQvR/U9KUveL9r0843vRVwTFKyM5cS9eaJBqQsAgBTxHxURrhwVquU9GMpY0WPGBwCAFKquKA5ZjjLT8l7Qv4/u++45KsqnjBULgg8AACkWqhxlpuW9qfmEivJzKWfFiFIXAAAWYbblnR2YY8eMDwAAadT9wNKG462mnsMOzLEj+AAAkCbBDizNckkdIRb5dG9dR2wIPgAAR+s+45LKfW/83Vs9M0640COxA3O8CD4AAMcKNuNS7M7VginlSW0Rj+XAUpdLmnVpWVTjSleoszKCDwDAkULNuHi8LZqzttb0/jixhItYDiztMKRHXj2g808faGpc6Qp1VkfwAQA4TrgZF0OdZaVFG/ZqYnlR2BATa7iIpyvL7LgSEersiHZ2AIDjRJpxMSTVe1u080BTyGtC7bDsDxcb6+oDHm/vMLR9f6Oe23PEdPdWLOOKFOqkzvDUHmoxkc0x4wMAcJx498uJdsYo2u6tWMclRRfqnLgJIsEHAOA4ZvfBCXVdNOHC+0VbVN1b8YxLYhPESCh1AQAcZ0xZgYrduQq1SsalzrU6ofbLMRsaPN4vInZv9VyqE25ddKRxSfGHOrtjxgcA4DjZWS4tmFKuOWtr5ZICgomZ/XLMhoam5raI3VsdhnTf5LNVOCBHgwfk6lhzm25dVyvFMC7pq1Dn8bYEDVzJ3AQxE9rnCT4AAEeqrijWyumje629KTLRlWU2XBScnGNqLIUDcjR11Gld91dmxTYuKf5QF6tMaZ93GYbhzGXdIfh8Prndbnm9XuXn56d7OACAJIt1lsLf1SUFDxcrp4+WO6+vrnl0R8TXenLW2F4LjeOdPUllEAnVPt/9s0h2+DH7/U3w6YHgAwDOE0/4CRcu2jsMXbJsc8SZoa13T0hKSSgVpSf/ewxV0kv2e/Qz+/1NqQsA4GjxzIxUVxRrYnlRyHCRrrKTX3aWK+kt65nWPk9XFwDAsaLdhDAYf7iYOuo0VY0Y1CvE+NcSFbkDF0QXuXNtsYNyprXPM+MDAHCkRB1bYUakmaFMlmnt8wQfAIAjpbpEk4qyUzqks30+FpS6AACOlGklGqvyr2OS1GtDyFSsY4oWwQcA4EipLNF0P6B0+/5G2x0QmknrmCh1AQAcKVUlGitt7JfM9vZMWcfEPj49sI8PADiHmU0I4wknVtjYr/tYrBLAksHs9zelLgCAYyWzRBOpa0zq7BpLRdkrEW37dkGpCwDgaMkq0VhlY79Utu1nAoIPAMDxktFqbpWuMasEMKug1AUAQBIcbGg2dV2yN/azSgCzCoIPAAAJ1t5h6MmdhyJeV5yCjf0ybWflZKPUBQCwnVScSh7OzgNN8vhaI1437cLTkz6uTNtZOdkIPgAAW7FC27bZslFpYb8kjyT9J8RbDaUuAIBtWKVt22rlpUzaWTnZmPEBANiCldq2rVheypSdlZPNNjM+Bw8e1A033KCysjLl5eVpxIgRWrBggdra2tI9NABACkTTtp1sZg/ulJTSM7z8bftTR52mqhGDHBd6JBvN+Lz33nvq6OjQ6tWrNXLkSNXV1WnWrFlqbm7WihUr0j08AECSWaFtu+ei6t9ce75++sLfAgJZ0f+tN5KkS5Zttu0RElZlm+BTXV2t6urqrvtnnHGG3n//fa1cuTJs8GltbVVr61cr730+X1LHCQBIjnSvqwm1qPq+yeUa2L9vQHlp015P0DO8/GuRnLbuJpVsU+oKxuv1qqAgfP106dKlcrvdXbeSkpIUjQ4AkEj+dTWhijcuJW/fnHCLqm9dVyvvF21d5SVJljnDy4lsG3z279+vhx56SLNnzw573fz58+X1ertuhw8fTtEIAQCJZHZdTaLXtUR7GKmV1iI5keWDz8KFC+VyucLedu3aFfCco0ePqrq6WldffbVuvPHGsK+fk5Oj/Pz8gBsAIDOlo2072iBjhbVIobR3GCldbJ0Oll/jU1NTo2nTpoW9prS0tOt/Hz16VOPHj1dVVZUeeeSRJI8OAGA1qW7bjjbIFJ6cY+p6s9clihU2fkwFywefwsJCFRYWmrr2yJEjGj9+vCorK7VmzRplZVl+QgsAkATJOG09lKgXVZudREnhZIt/jZITFlvbJhkcPXpU48aNU0lJiVasWKFPP/1UHo9HHo8n3UMDAARhl7JKtIuqG5ojn+EVzXXxinaNUqaz/IyPWS+99JL27dunffv2adiwYQE/Mwx7/GMBgF0EK6sU9O+jn02t0D+dNzSNI4tetGdhpbvtvqdo1iilahYtmWwz4zNz5kwZhhH0BgCwjlCt303NJ3TLure09MW9aRpZ7PyLqofkR15Unc62+2CsvNg6GWwTfAAA1heurOK3+tUDevHtxB0mmtqSWuBrB/uP73S13YditRmoZLNNqQsAYH2Ryip+9z1Xp8sr4j9MNFWdSqEWB3/saw26ONg/Q9RzbEVp6KKy4oGqyUTwAQCkjNlySWNzW9xrSlLVqRTrqfBWOS092jVKmY5SFwAgZaIpl8SzpiSVnUrx7MRsldPS07HxY7ow4wPAkXqeop2O/9J2ojFlBSro30dNzSciXhvPmpJUdirZZXGwVWagko3gA8BxnLJDrRVlZ7n0s6kVumXdW2Gvi7erKZVhxGxAK+yfo+37Gy0dKlK58WO6EHwAOIqTdqi1qn86b6hu/t/PtPrVA0F/7lL8a0pS2alkZnGwu18f3fnMX+XxZUbYtvOMqMtgo5sAPp9PbrdbXq+XA0sBm2nvMHTJss0hSyD+7pWtd08I+f/k7fyFkGovvl2ve5+rU1NzW9djiQoD/n/rSJ1K4f6to+EP1FLvxcGhvmT9v9VqYTtTZ0TNfn8TfHog+AD2tX1/o655dEfE656cNTbodH+mfiFYWTKDZLgwIiU+cAT7+yjKz1HLlx367PPga5oSHcDiFWpG1KohrTuz39+UugA4RjzrPiiRJUcy15SE2itnYP8+umrUaXLn9VV7h5GwwBFscXCHYei6374R8jlWOg4i1rb8TEPwAeAYsa77cMoXgh11DyOb9nr05z1H1dTcpse2HdRj2w4mfMauZ5B7bs8RU8+zQseXU87sYh8fAI4R6xlJ8ezTgvTLznLJ+0Wb1mw7GLCeSPpqxm5jXeKOyOguk46DsEtbfiQEHwCOEesZSU75QrCrVG5m2JPVDiQNJ5NCWjwIPgAcJZYdap3yhWBXZmfsdvy9MeG/22oHkoaTSSEtHqzxAeA40e5Q67RDHO3G7Ezcrb+v1f0/ODfhi9StdCBpOE45s4t29h5oZwcQTKpbozOJ1fc2MruNgdT575msf0urf05+mbptA/v4xIjgAyCUTP1CSKZM+EwibWbYndX21UmXTAlp3RF8YkTwARBOJn4hJEukze5+c+1oDezf1xKfVaixhhJqE0tYFxsYAkASOOEQRzPMdErVPFmr7o1S6ZwJ8q+zmffHd/TZF5FPhqdDz77o6gIARC1Sp5Qk9ewO93hbNHttrRZveFfb9zcmpX08nOqKYv3m2tGmrqVDz76Y8QEARC2WGRF/zPndtoP6XRJ2TZYilyLHjhhEh57DEXwAAFFLxIxIos85M7PQ2ikt2wiNUhcAIGqRNrszw/i/27w/vqNt+xqiLn21dxjavr9Rz+05ol+9/KHmrK3tVX4LdiRFLJtYxqP7ONNR4kMgurp6oKsLAMwJtbdRrKIpfQWb3QklVIu6vyzm8X6hpuY2FZyco6L8xHafZUK7v13Qzh4jgg8AmBfsiz3L1XthsxlmN4OMtjXdL1iLejKDSaR2fydvepkMtLMDAJIu2PEfx5pbdeu6tyRFNxNkqDMULNqwVxPLi4LOuoRro4+k54LsUMEkEWuPIrX7R3qfSB7W+AAA4uLf2+i75w2VJJ3oMDT3O1/TkPzoF0D7DwzdeaAp6M/NtNGH0n1BdnuHoYXPJ+/EdrMHo4Z6n0geZnwAAHELVjIqys/R7d85U74vTuixbQejer1Q7fKxtNEHa1H/9eYP5fGZCyaxbFhpdpxslJh6zPgAAOLiLxn1nOH42NeqB1/+UBeWFWjV9NEqdpufAQrVLh9tG32wFvWNdfX65csfmnp+rMHE7DgL++fE9PqIHcEHABAzM0dX+NeybL17gn5/w0U6Ja9PyNdzqXNxcagNBKNto+/Zou4fr1mx7ldkdpx3PvPXgFZ7JB/BBwAQs2jWsmRnuXTxmYX61wuHhX3NcBsI+jcglNQrVPjv3/6dM/WraaP05Kyx2nr3hIAFytGsEQoXwCIJN87uPvb13mcIyUXwAQDELNq1LBvr6vXIqwdCXnfTt8o0sbwo7IZ/4TYgXDV9tH70na9p6qjTVDViUK8AFU3pKt4dnP3jHJIfupyViIXUiA6LmwEAkiKfcxWM2VLQ4AG5plrRn971v3puz1F5fK1djwXbVydYG30ix/vPo4dpYnmRqWvDqa4o1oDcPrrut2+EvCbehdSIDsEHABDzRn7+tSxmDv00U2Y69vmJXo+F2lfH30YfjUjj9Xu29n+1bX9DQjYybPhHa+SLRIdXqlDqAgCHC9WVFeycq57MrLnxl4xi/WJPZDnI7Nobydz7NyOaWTEkH8EHABzMbFdWuMARas3NwP599Jtrz++aMYnniz2RG/6FGm+w3ynFH7gidXhF6mRDYhF8AMDBErXDcHVFse6bXK6C/n27HmtqPqGfvvC3rhkTfwCIR6LKQdUVxdp69wTdN/nssNclInBFMyuG5CP4AICDJWqH4Y119bp1Xa2amtsCHu9eLsrOcum+yeUxj1VKbDkoO8ulwgHmNhCMN3CF60TjsNLUYnEzADhYItafRHMg58BuM0LRCHbsRCKkcv1NrJ1oSCyCDwA4WDRdWaFEUy6L9awtKfpykJn2/ES8/2jE0omGxIop+Bw+fFgul0vDhnXuvrlz506tW7dO5eXluummmxI6QABA8vjXn8xZWyuXFPDlbzZwRFMui2XmpMhEW31P4drze8663De5XLesqw36OoZYf2M3MQWfa6+9VjfddJNmzJghj8ejiRMn6pxzztHatWvl8Xj0k5/8JNHjBAAkiX/9Sa/T1U0GjmjKRWPKCnRKvz76LMh+PVJn2BqSn6MH/mWUGv7RGlM5yN+e33MGx+Nt0ey1tb1+/yn9Qp8dBvuJKfjU1dVpzJgxkqSnn35aFRUV2rZtm1566SXNnj077cGntbVVF110kf7617/qrbfe0qhRo9I6HgCwunjWn0RTLtq01xMy9EidMywLv3eOLh5ZGNP7MNOe3/P3hxtP9/VJzPrYQ0xdXSdOnFBOTudK+Jdfflnf+973JElnnXWW6uvTf9Dav//7v2vo0KHpHgYAZBT/+pNQ51yFe56Zdm1JEU9GP6Vfn7iOiojmEFIzErl/EKwhpuBzzjnnaNWqVXrttde0adMmVVdXS5KOHj2qQYPSu2jrL3/5i1566SWtWLHC1PWtra3y+XwBNwBAdMy0a5sJJZ99fiKukJGsYx84TsI+Yip1LVu2TFdddZWWL1+u66+/Xt/4xjckSc8//3xXCSwdPv74Y82aNUt//vOf1a9fP1PPWbp0qRYtWpTkkQGA/UUqlyVqz6BwknXsA8dJ2EdMwWfcuHFqaGiQz+fTwIEDux6/6aabTAeORDMMQzNnztTs2bN1wQUX6ODBg6aeN3/+fN1xxx1d930+n0pKSpI0SgBOFsvp51ZiZvzh2rVTsWeO2UNIzUrW/kFIn5j38cnOzg4IPZJUWloa73h6WbhwYcQZmTfffFOvv/66fD6f5s+fH9Xr5+TkdK1XAoBkifX0c6tIxPhTsWdOuPb8aHGchD25DMMw9Xdx/vnny+Uy9w9fWxt8P4RYNDQ0qKGhIew1paWlmjZtmjZs2BAwxvb2dmVnZ+u6667TE088Yer3+Xw+ud1ueb1e5efnxzV2APYS64xNqPZq/zOtfmRBIsfvfy0p+J5BZl7LzL9DsKA2sF8fHfv8RND9igypV5t7JgVTmP/+Nh18us+6tLS06OGHH1Z5ebmqqqokSTt27NC7776rW265RUuXLo1z+NE7dOhQwMLko0eP6vLLL9ezzz6riy66qGuzxUgIPgCCiXXGo73D0CXLNodc1Ouf5dh694SUzSpEE+CSMf54Zo821tVr4fN75fF1228oP1cLv9f7ucHe56a9HtMbG2ZaKdLpEh58urvxxhtVXFysn/70pwGPL1iwQIcPH9bvfve76EecYAcPHlRZWVnU+/gQfAD0FM+Mx/b9jbrm0R0Rf8eTs8am5CiDaENHssYfy+zZxrp6zV4buqKwyuTMU6avtUJwZr+/Y1rj88wzz2jXrl29Hp8+fbouuOACSwQfAIhWsC9ESaYP4Az25ZmKTiazwu1oPGdtbdAAl6zxR3tmVXuHoXnr3wl7zfz175jaaJDzspwtpuCTl5enrVu36swzzwx4fOvWrcrNtUbLX2lpqWKYzALgUKFmQqZdeLrpAziDfZmm8vTvcKI5Qb17cLDK+Hf8vTHsDsuSdOzzE9rx98aYd32GM8QUfObOnas5c+Zo9+7dGjt2rKTONT6/+93v0n5cBQBEK9xMyC9f/sDUa4Sa8Uj16d+hRHOCevcAZ5Xxb9/faPq6YMGH8hb8Ygo+8+bN0xlnnKFf/epXWrdunSTp7LPP1uOPP65/+Zd/SegAASCZzJztZEaoGY9YTj9Pxpd0rCWrRJzenhjm/jU++Pi4tu9vDPjMMn0rASRWTIub7YzFzYCzmF28G4rZriazX77J+pKOd5FyusPDtg8bdN1jb5i+3j82SRm9lQDMS+riZkn67LPP9Oyzz+rvf/+7fvzjH6ugoEC1tbUaMmSITjvttFhfFgBSKppFufHMeJg5/TyWxcdmxVuyiuf09kQYO2JQr312wvF4WzR7ba1O6dcn5oXpsKeYDil9++239bWvfU3Lli3T8uXL9dlnn0mS/vSnP0W9czIA62nvMLR9f6Oe23NE2/c3qr3DvhPDZhfl3v6dM8MewGlGuNPPzZTcFm3YG/O/hdkT1MMFgFhPb0+E7CyX7v/+uaav939K4YISJ687U0wzPnfccYdmzpypn//85xowYEDX41dccYWuvfbahA0OQOqlu6SRamZnQmomnKmaCWcmbcYj1sXH0fCfoN7z37coQ/59qyuKtWr6aC18/l15fK0Je91t+z5lsbODxBR83nzzTa1evbrX46eddpo8Hk/cgwKQHskstVhVtIt3k7X/S6r2+0l3ySpe3cf/l7p6/df2j+J+zV9v2a8/1h7JiPCH+MVU6srNzQ04HsLv/fff16mnnhr3oACkXrJLLVbmnwmJppSV6HJgKvfLSVXJKlklU//4r0hgSPGH+4119Ql7TVhTTDM+U6dO1eLFi/X0009Lklwulw4dOqR58+bpBz/4QUIHCCA1UlFqsbJoZkKSUQ60yn45iZKKkqmZz8zdr4+8/7fOJ1zsYrGzc8Q047NixQp9+umnGjx4sL744gtddtllGjlypAYMGKAlS5YkeowAUsBKRyuki5mZEH85sGdIjHfGIBGLj60iWZ9RT2Y+s/u/f27Q2bxgWOzsDDEFn/z8fG3dulV//OMfdf/996umpkYvvviiXnnlFfXv3z/RYwSQAlY5msDKkl0OjKXkZjWpLpma+cyqK4q19e4Jqhk/0tRr2jncI4ZS15dffqnc3Fzt2bNHEyZM0IQJE5IxLgApZrdSSzKkqvMq2YuPk3l8QzpKpmY+s+wsly4eWahfb9kX8fWcHO6dIOrgc9JJJ2n48OFqb29PxngApIl1jiawrlSVA5N5eniy196kq2Rq5jMj3EOKsdR17733av78+Wpqog4K2IkdSi3JlOnlwFSsvbHyZ2SndVSIXUxdXf/5n/+pffv2aejQoRo+fHivdT21tbUJGRyA1Mv0fV6SKZNnDCKtvene0SQp5n9/q39Gmb6JI+IXU/C58sor5XK5xPmmgD3FWmpJ5toRK8jkcqDZtTe/3rxPf3jzUMylsEz4jAj3zhbV6eyff/657rrrLv35z3/WiRMn9O1vf1sPPfSQCgsLkznGlOJ0diA2TjrqIhPf63N7juhHf9gT03NjOck8Ez8jZDaz399RBZ+77rpLDz/8sK677jrl5eVp3bp1GjdunJ555pmEDNoKCD5A9EIddRHLF2YqJGJmKtNmt7bvb9Q1j+6I+fn+EtXWuyeYfp+Z9hkhs5n9/o6q1LV+/Xo99thjmjZtmiTpuuuu08UXX6z29nZlZ2fHN2IAGSmatSNW+NJL1ExEMjuvkiHS2ptIYmlDz7TPCM4QVVfX4cOHdemll3bdHzNmjE466SQdPXo04QMDkBmi2bcl3VK1o7AVmeloMoPN/ZDpogo+7e3t6tu3b8BjJ510kr788suEDgpA5siUoy6cfAirX7jtCm7/zpmmXsOqrfqAWVGVugzD0MyZM5WTk9P1WEtLi2bPnh3Q0r5+/frEjRCApVl535bunH4Iq1+ojiZJ+sObhy3bhg4kSlTB5/rrr+/12PTp0xM2GACZx+r7tvhlysxUKoRae2P1NnQgEaIKPmvWrEnWOABkqEzYt0XKnJmpdGJzPzhBTBsYAkB3mfCFmSkzU+nG5n6wu6j28XEC9vEBYmf1fVv8XV1S8Jkpq+03BMC8pGxg6AQEH8De2FEYsKekbGAIAJmOUg7gbAQfAI7DjsJfsXp5Ekg0gg8AOJRdyn6xhjdCnzMRfADAgUIdLOs/vsO/0Nvq4SDW8GaX0Ifosbi5BxY3A7C79g5DlyzbHHIna39r/32Tz9ZPX/ibZcNBqPAWqUsv1ufB2sx+f0d1VhcAIPOZPb7jlnVvJf1A1/YOQ9v3N+q5PUe0fX+j6bPSYj17jTPbQKkLABwmnmM5DHXOjCzasFcTzhqi3R8di7kMFk+5Kdaz1zizDQQfAHCYeI/l8IeDsUtfVlPzia7HoymDmV1jFEqsZ69xZhsodQHIKLGWRvAV//Ed8S5R7h56JPNlsESUm2I9e40z28CMD4CMQSdOYkQ6WDbWKNm9DDaxvChk2SsR5aZYz17jzDYw4wMgI/hLI8lebOsU/oNli9yBMxtF7lw9fO3omGeEuoeWUBJRbvKHN0m9xum/v2BKea/wFevzYB/M+ACwvEilETOzDFaU7j1ywh3fkZWloDNCZoULLQcbPjf1GpHKTf7w1nMWsCjCLGCsz4M9EHwAWJ4dO3GsUrYLdXxHqHAwqH9fNTa3RXzdUKGlvcPQkzsPRXx+UX6OqXJTrGevcWabcxF8AFie3Tpx4u1oSpVg4aBy+EBdtnxLzGtkdh5okscX+d/pmjGnmw4hsZ69xpltzsQaHwCWZ6dOnEzbQM8fDqaOOk1VIwap70lZca2RMRtOSwv7xzhiIDzbBZ8XXnhBF110kfLy8lRYWKjvf//76R4SgDhFar92qbNMlAmdONGU7awq3MLoSLNVdgqxyEy2KnX98Y9/1KxZs/Qf//EfmjBhggzD0DvvvJPuYQGIU6T2aylzOnHsUraLdY0M7eRIN9sEny+//FI/+tGPtHz5ct1www1dj3/9618P+7zW1la1trZ23ff5fEkbI4DY2aUTx04zHrGskbFTiEVmsk3wqa2t1ZEjR5SVlaXzzz9fHo9Ho0aN0ooVK3TOOeeEfN7SpUu1aNGiFI4UQKzs0InDjId9Qiwyk8swDGusoIvTH/7wB11zzTU6/fTT9Ytf/EKlpaV64IEH9NJLL+mDDz5QQUHw/ycSbManpKQk4rH2ABArf1eXFHzGwypdXcmW7n2MYC8+n09utzvi97flFzcvXLhQLpcr7G3Xrl3q6OiQJN1zzz36wQ9+oMrKSq1Zs0Yul0vPPPNMyNfPyclRfn5+wA0AkimexcF20rNjjNCDVLB8qaumpkbTpk0Le01paamOHz8uSSovL+96PCcnR2eccYYOHYq8WRYApJIdynZAJrJ88CksLFRhYWHE6yorK5WTk6P3339fl1xyiSTpxIkTOnjwoIYPH57sYQJA1NhAD0g9ywcfs/Lz8zV79mwtWLBAJSUlGj58uJYvXy5Juvrqq9M8OgAAYAW2CT6StHz5cp100kmaMWOGvvjiC1100UXavHmzBg4cmO6hAYBtsUgZmcQ2XV2JYnZVOADAOoetAma/v2014wMA8Yg0c5EpMxupGmemHLYKdEfwAQBFnrnIlJmNYOM8Ja+P/t/FpaqZcGbCAlCkw1Zd6jxsdWJ5kSXDIZyLUlcPlLoA5wk1c+H/ur7pW2V65NUDIX9ulZmNUO/D75R+fXT/989NyFi372/UNY/uiHjdk7PG0rmGlLDNBoYAkEyRZi4MSY++1jv0+H8udc5stHek978hw70Pv88+P6E5a2u1sa4+7t9nl8NW4TwEHwCOtvNAU0BZKJhwmcaQVO9t0c4DTYkdWJTMvA+pc7yJCGp2OmwVzkLwAeBoiZqRSPfMRjS/PxFBzX/YaqjVOy51roGy82GryEwEHwCOlqgZiXTPbET7+7fta4hr1ic7y6UFUzqPCOoZfvz3F0wpZ2EzLIfgA8DRIs1cSFKWq/eXu59VZjb878OsX2/Zp0uWbY5rvQ+HrSIT0dXVA11dgPP4u6EkBSwO7tnVFernVvmS31hXr9n/9z7MSNT4M2V/I9gbXV0AYFKkmYv5/1SeETMb1RXFWjV9tE7p18fU9YnqSvMftjp11GmqGjGI0ANLY8anB2Z8AOey087Nv968T2u2HdBnX5ww9Rz220Gm48gKAIiSf+Yi1p9bRXaWSz/6zpmqmTBSv9z0vn69ZX/E56S7Kw1IFUpdAGBT2VkuXTzyVFPXprsrDUgVgg8A2Bj77QCBCD4AEEF7h6Ht+xv13J4j2r6/Me3HU0SD/XaAQKzxAYAwMuVU9nD8XWs930dRhr0PIBHo6uqBri4AfpFObbdSK7sZmdKVBsSCri4AiEOkU9td6tz/ZmJ5UcaEh0zpSgOSiTU+ABBEpNPOrXIqO4DoEHwAIAiz+9qw/w2QWQg+ABCE2X1t2P8GyCys8UkBFhQCmce//43H2xJ0nY9LnV1R7H8DZBaCT5LZoRUWcCL//jdz1tbKpeCnsrP/DZB5KHUlkb8VtucCSY+3RXPW1mpjXX2aRgbAjEintvMfL0DmYcYnSezYCgs4UXVFsSaWF2nH3xu1fX+jJENVZxRqbIa0hVNqBwIRfJIkmlZY9tUArG3TXk9AyfrXW/YntWSdqLBCqR3ojeCTJLTCAvYQavdmf8k60SWvRIWVVI8byBSs8UkSWmGBzBepZC11lqwTdWhpotYFpnrcQCYh+CSJvxU21OS0S53/FUcrLGBdqdy9OZFhhV2ngdAIPknib4WV1Cv80AoLZIZUlqwTGVYotQOhEXySiFZYILOlsmSdyLBCqR0IjcXNSeZvhaWdFMg8qdy9OZFhhV2ngdCY8UmB7CyXqkYM0tRRp6lqxCBCD5AhUlmyTuS6QErtQGgEHwAII1Ul60SHFUrtQHAuwzDoZ+zG5/PJ7XbL6/UqPz8/3cMBYBGp2gE50ZsOsnMznMLs9zfBpweCD4B0I6wA0TP7/c3iZgCwGP+6QACJxxofAADgGAQfAADgGAQfAADgGAQfAADgGAQfAADgGLYKPh988IGmTp2qwsJC5efn6+KLL9aWLVvSPSwAAGARtgo+kydP1pdffqnNmzdr9+7dGjVqlL773e/K4/Gke2gAAMACbLOBYUNDg0499VS9+uqruvTSSyVJx48fV35+vl5++WV9+9vfDvq81tZWtba2dt33+XwqKSlhA0MAADKI2Q0MbTPjM2jQIJ199tn6r//6LzU3N+vLL7/U6tWrNWTIEFVWVoZ83tKlS+V2u7tuJSUlKRw1AABIJdvM+EjSkSNHNHXqVNXW1iorK0tDhgzRCy+8oFGjRoV8DjM+AABkPtvM+CxcuFAulyvsbdeuXTIMQ7fccosGDx6s1157TTt37tTUqVP13e9+V/X19SFfPycnR/n5+QE3AABgT5af8WloaFBDQ0PYa0pLS7Vt2zZNmjRJx44dCwgvZ555pm644QbNmzfP1O/jkFIAADKPbQ4pLSwsVGFhYcTrPv/8c0lSVlbgJFZWVpY6OjqSMjYAAJBZLF/qMquqqkoDBw7U9ddfr7/+9a/64IMPdNddd+nAgQOaPHlyuocHAAAswDbBp7CwUBs3btQ//vEPTZgwQRdccIG2bt2q5557Tt/4xjfSPTwAAGABll/jk2qs8QEAIPPYpqsLAAAgUQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMQg+AADAMU5K9wAAAPbX3mFo54EmfXK8RYMH5GpMWYGys1zpHhYciOADAEiqjXX1WrRhr+q9LV2PFbtztWBKuaoritM4MjgRpS4AQNJsrKvXnLW1AaFHkjzeFs1ZW6uNdfVpGhmciuADAEiK9g5DizbslRHkZ/7HFm3Yq/aOYFcAyUHwAQAkxc4DTb1merozJNV7W7TzQFPqBgXHI/gAAJLik+OhQ08s1wGJQPABACTF4AG5Cb0OSASCDwAgKcaUFajYnatQTesudXZ3jSkrSOWw4HAEHwBAUmRnubRgSrkk9Qo//vsLppSznw9SiuADAEia6opirZw+WkXuwHJWkTtXK6ePZh8fpBwbGAIAkqq6olgTy4vYuRmWQPABACRddpZLVSMGpXsYAKUuAADgHAQfAADgGAQfAADgGAQfAADgGAQfAADgGAQfAADgGAQfAADgGAQfAADgGAQfAADgGBkTfJYsWaJvfvOb6tevn0455ZSg1xw6dEhTpkxR//79VVhYqNtuu01tbW2pHSgAALCsjDmyoq2tTVdffbWqqqr02GOP9fp5e3u7Jk+erFNPPVVbt25VY2Ojrr/+ehmGoYceeigNIwYAAFaTMcFn0aJFkqTHH3886M9feukl7d27V4cPH9bQoUMlSQ888IBmzpypJUuWKD8/P+jzWltb1dra2nXf5/MlduAAAMAyMqbUFcn27dtVUVHRFXok6fLLL1dra6t2794d8nlLly6V2+3uupWUlKRiuAAAIA1sE3w8Ho+GDBkS8NjAgQPVt29feTyekM+bP3++vF5v1+3w4cPJHioAAEiTtAafhQsXyuVyhb3t2rXL9Ou5XK5ejxmGEfRxv5ycHOXn5wfcAACAPaV1jU9NTY2mTZsW9prS0lJTr1VUVKQ33ngj4LFjx47pxIkTvWaCAACAM6U1+BQWFqqwsDAhr1VVVaUlS5aovr5excXFkjoXPOfk5KiysjIhvwMAAGS2jOnqOnTokJqamnTo0CG1t7drz549kqSRI0fq5JNP1qRJk1ReXq4ZM2Zo+fLlampq0o9//GPNmjWL8hUAAJCUQcHnJz/5iZ544omu++eff74kacuWLRo3bpyys7P1wgsv6JZbbtHFF1+svLw8XXvttVqxYkW6hgwAACzGZRiGke5BWInP55Pb7ZbX62WmCACADGH2+9s27ewAAACREHwAAIBjEHwAAIBjEHwAAIBjEHwAAIBjEHwAAIBjZMw+PgBgRe0dhnYeaNInx1s0eECuxpQVKDsr9PmAANKL4AMAMdpYV69FG/aq3tvS9VixO1cLppSruqI4jSMDEAqlLgCIwca6es1ZWxsQeiTJ423RnLW12lhXn6aRAQiH4AMAUWrvMLRow14F2/be/9iiDXvV3sHG+IDVEHwAIEo7DzT1munpzpBU723RzgNNqRsUAFMIPgAQpU+Ohw49sVwHIHUIPgAQpcEDchN6HYDUIfgAQJTGlBWo2J2rUE3rLnV2d40pK0jlsACYQPABgChlZ7m0YEq5JPUKP/77C6aUs58PYEEEHwCIQXVFsVZOH60id2A5q8idq5XTR7OPD2BRbGAIADGqrijWxPIidm4GMgjBBwDikJ3lUtWIQekeBgCTKHUBAADHIPgAAADHIPgAAADHIPgAAADHIPgAAADHIPgAAADHIPgAAADHIPgAAADHIPgAAADHYOfmHgzDkCT5fL40jwQAAJjl/972f4+HQvDp4fjx45KkkpKSNI8EAABE6/jx43K73SF/7jIiRSOH6ejo0NGjRzVgwAC5XBw0GC+fz6eSkhIdPnxY+fn56R6OLfCZJh6faWLxeSYen2lkhmHo+PHjGjp0qLKyQq/kYcanh6ysLA0bNizdw7Cd/Px8/o81wfhME4/PNLH4PBOPzzS8cDM9fixuBgAAjkHwAQAAjkHwQVLl5ORowYIFysnJSfdQbIPPNPH4TBOLzzPx+EwTh8XNAADAMZjxAQAAjkHwAQAAjkHwAQAAjkHwAQAAjkHwQcIdPHhQN9xwg8rKypSXl6cRI0ZowYIFamtrC7ju0KFDmjJlivr376/CwkLddtttva7BV5YsWaJvfvOb6tevn0455ZSg17hcrl63VatWpXagGcTMZ8rfafxKS0t7/V3Omzcv3cPKKA8//LDKysqUm5uryspKvfbaa+keUsZi52Yk3HvvvaeOjg6tXr1aI0eOVF1dnWbNmqXm5matWLFCktTe3q7Jkyfr1FNP1datW9XY2Kjrr79ehmHooYceSvM7sKa2tjZdffXVqqqq0mOPPRbyujVr1qi6urrrvpmdTJ0q0mfK32niLF68WLNmzeq6f/LJJ6dxNJnlqaee0ty5c/Xwww/r4osv1urVq3XFFVdo7969Ov3009M9vMxjACnw85//3CgrK+u6/+KLLxpZWVnGkSNHuh578sknjZycHMPr9aZjiBljzZo1htvtDvozScaf/vSnlI7HDkJ9pvydJsbw4cONX/7yl+keRsYaM2aMMXv27IDHzjrrLGPevHlpGlFmo9SFlPB6vSooKOi6v337dlVUVGjo0KFdj11++eVqbW3V7t270zFE26ipqVFhYaEuvPBCrVq1Sh0dHekeUsbi7zRxli1bpkGDBmnUqFFasmQJ5UKT2tratHv3bk2aNCng8UmTJun1119P06gyG6UuJN3+/fv10EMP6YEHHuh6zOPxaMiQIQHXDRw4UH379pXH40n1EG3jpz/9qb797W8rLy9P//M//6M777xTDQ0Nuvfee9M9tIzE32li/OhHP9Lo0aM1cOBA7dy5U/Pnz9eBAwf029/+Nt1Ds7yGhga1t7f3+jscMmQIf4MxYsYHpi1cuDDo4tnut127dgU85+jRo6qurtbVV1+tG2+8MeBnLper1+8wDCPo43YVy2cazr333quqqiqNGjVKd955pxYvXqzly5cn8R1YT6I/U/5Og4vmc7799tt12WWX6bzzztONN96oVatW6bHHHlNjY2Oa30Xm6Pn3xt9g7JjxgWk1NTWaNm1a2GtKS0u7/vfRo0c1fvx4VVVV6ZFHHgm4rqioSG+88UbAY8eOHdOJEyd6/ZeNnUX7mUZr7Nix8vl8+vjjjx3zuSbyM+XvNLR4PuexY8dKkvbt26dBgwYlemi2UlhYqOzs7F6zO5988onj/wZjRfCBaYWFhSosLDR17ZEjRzR+/HhVVlZqzZo1ysoKnFysqqrSkiVLVF9fr+LiYknSSy+9pJycHFVWViZ87FYVzWcai7feeku5ubkhW7XtKJGfKX+nocXzOb/11luS1PWZIrS+ffuqsrJSmzZt0lVXXdX1+KZNmzR16tQ0jixzEXyQcEePHtW4ceN0+umna8WKFfr000+7flZUVCSpc2FeeXm5ZsyYoeXLl6upqUk//vGPNWvWLOXn56dr6JZ26NAhNTU16dChQ2pvb9eePXskSSNHjtTJJ5+sDRs2yOPxqKqqSnl5edqyZYvuuece3XTTTZzoHEKkz5S/0/ht375dO3bs0Pjx4+V2u/Xmm2/q9ttv1/e+9z1asU264447NGPGDF1wwQVdM+iHDh3S7Nmz0z20zJTmrjLY0Jo1awxJQW/dffTRR8bkyZONvLw8o6CgwKipqTFaWlrSNGrru/7664N+plu2bDEMwzD+8pe/GKNGjTJOPvlko1+/fkZFRYXx4IMPGidOnEjvwC0s0mdqGPydxmv37t3GRRddZLjdbiM3N9f4+te/bixYsMBobm5O99Ayym9+8xtj+PDhRt++fY3Ro0cbr7zySrqHlLFchmEYqY9bAAAAqUdXFwAAcAyCDwAAcAyCDwAAcAyCDwAAcAyCDwAAcAyCDwAAcAyCDwAAcAyCDwAAcAyCDwAAcAyCDwBLcrlcYW8zZ85M29hKS0v14IMPpu33A4gdh5QCsKT6+vqu//3UU0/pJz/5id5///2ux/Ly8qJ6vba2NvXt2zdh4wOQmZjxAWBJRUVFXTe32y2Xy9V1v0+fPpo9e7aGDRumfv366dxzz9WTTz4Z8Pxx48appqZGd9xxhwoLCzVx4kRJ0vPPP68zzzxTeXl5Gj9+vJ544gm5XC599tlnXc99/fXX9a1vfUt5eXkqKSnRbbfdpubm5q7X/eijj3T77bd3zT4ByBwEHwAZp6WlRZWVlfrv//5v1dXV6aabbtKMGTP0xhtvBFz3xBNP6KSTTtK2bdu0evVqHTx4UP/8z/+sK6+8Unv27NHNN9+se+65J+A577zzji6//HJ9//vf19tvv62nnnpKW7duVU1NjSRp/fr1GjZsmBYvXqz6+vqAmSkA1sfp7AAs7/HHH9fcuXMDZmV6mjx5ss4++2ytWLFCUufMjNfr1VtvvdV1zbx58/TCCy/onXfe6Xrs3nvv1ZIlS3Ts2DGdcsop+rd/+zfl5eVp9erVXdds3bpVl112mZqbm5Wbm6vS0lLNnTtXc+fOTfh7BZBcrPEBkHHa29t1//3366mnntKRI0fU2tqq1tZW9e/fP+C6Cy64IOD++++/rwsvvDDgsTFjxgTc3717t/bt26ff//73XY8ZhqGOjg4dOHBAZ599doLfDYBUIvgAyDgPPPCAfvnLX+rBBx/Uueeeq/79+2vu3Llqa2sLuK5nEDIMo9eanJ6T3h0dHbr55pt122239fq9p59+eoLeAYB0IfgAyDivvfaapk6dqunTp0vqDCsffvhhxNmYs846Sy+++GLAY7t27Qq4P3r0aL377rsaOXJkyNfp27ev2tvbYxw9gHRicTOAjDNy5Eht2rRJr7/+uv72t7/p5ptvlsfjifi8m2++We+9957uvvtuffDBB3r66af1+OOPS1LXTNDdd9+t7du369Zbb9WePXv04Ycf6vnnn9cPf/jDrtcpLS3Vq6++qiNHjqihoSEp7xFAchB8AGSc++67T6NHj9bll1+ucePGqaioSFdeeWXE55WVlenZZ5/V+vXrdd5552nlypVdXV05OTmSpPPOO0+vvPKKPvzwQ1166aU6//zzdd9996m4uLjrdRYvXqyDBw9qxIgROvXUU5PyHgEkB11dABxtyZIlWrVqlQ4fPpzuoQBIAdb4AHCUhx9+WBdeeKEGDRqkbdu2afny5V179ACwP4IPAEf58MMP9bOf/UxNTU06/fTTdeedd2r+/PnpHhaAFKHUBQAAHIPFzQAAwDEIPgAAwDEIPgAAwDEIPgAAwDEIPgAAwDEIPgAAwDEIPgAAwDEIPgAAwDH+P3wmWFcIP57pAAAAAElFTkSuQmCC",
+      "text/plain": [
+       "<Figure size 640x480 with 1 Axes>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "\n",
+    "fig, ax = plt.subplots()\n",
+    "ax.scatter(y_test, y_pred)\n",
+    "ax.set_xlabel(\"Target\")\n",
+    "ax.set_ylabel(\"Preds\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Not really a great result. Any other model in `molfeat` would do better."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Tips\n",
+    "\n",
+    "1. Make sure that your inputs are SMILES or RDKit Molecules.\n",
+    "2. If you are getting an error coming from some tokenization step, that means that you are likely getting `None` molecules at some steps in the conversion to SAFE. This can happen if there your slicing algorithm of choice is not working. In that case, please filter your datasets to remove molecules that fails the encoding steps first. You can always use the very robus `safe.utils.convert_to_safe`, which augment default BRICS slicing with some graph partitioning algorithm.\n"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "safe",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index d302e8d..379083b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -16,6 +16,7 @@ nav:
       - Getting Started: tutorials/getting-started.ipynb
       - Molecular design: tutorials/design-with-safe.ipynb
       - How it works: tutorials/how-it-works.ipynb
+      - Extracting representation (molfeat): tutorials/extracting-representation-molfeat.ipynb
   - API:
       - SAFE: api/safe.md
       - Visualization: api/safe.viz.md


Test patch:
diff --git a/tests/test_notebooks.py b/tests/test_notebooks.py
index e4e4d9d..23a0ab5 100644
--- a/tests/test_notebooks.py
+++ b/tests/test_notebooks.py
@@ -13,7 +13,7 @@
 NOTEBOOK_PATHS = list(filter(lambda x: x.name not in DISABLE_NOTEBOOKS, NOTEBOOK_PATHS))
 
 # Discard some notebooks
-NOTEBOOKS_TO_DISCARD = ["Basic_Concepts.ipynb"]
+NOTEBOOKS_TO_DISCARD = ["extracting-representation-molfeat.ipynb"]
 NOTEBOOK_PATHS = list(filter(lambda x: x.name not in NOTEBOOKS_TO_DISCARD, NOTEBOOK_PATHS))