{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "026f1edb",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pickle\n",
    "import numpy as np\n",
    "\n",
    "import modularml as mml"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d27af770",
   "metadata": {},
   "source": [
    "# Getting Started\n",
    "\n",
    "Constructing a full ModularML pipeline can be divided in to the following sub-tasks:\n",
    "1. Defining FeatureSets\n",
    "2. Constructing a ModelGraph\n",
    "3. Creating the TrainingPhases\n",
    "4. Running\n",
    "\n",
    "\n",
    "\n",
    "This notebook provides an exmaple workflow for constructing a ML model to estimate battery state of health (SOH) using voltage-based features across a wide variety of aging conditions and state of charge (SOC). \n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc409e57",
   "metadata": {},
   "source": [
    "---\n",
    "### 1 - Defining FeatureSets\n",
    "---\n",
    "\n",
    "The first step in creating a ModularML experiment is to outline the data structures. We need to decide on: \n",
    "* the data source to be used (your raw or preprocessed data),\n",
    "* how the data should be structured into features, targets, and tags for modeling, and\n",
    "* how the set of features should be split for different training stages (e.g., 'training', 'validation', 'testing')\n",
    "\n",
    "Let's import some data. \n",
    "For this example, we will be using a set of preprocessed battery aging data from our recent paper: “Fine-tuning for rapid capacity estimation of lithium-ion batteries” ([10.1016/j.ensm.2025.104425](https://doi.org/10.1016/j.ensm.2025.104425)).\n",
    "\n",
    "It contains short-duration (100-second) pulses applied to a battery over a long-term aging routine. More details can be found at the following GitHub repository: [REIL-UConn/fine-tuning-for-rapid-soh-estimation]{https://github.com/REIL-UConn/fine-tuning-for-rapid-soh-estimation.git}."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "027c4822",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Downloading data...\n",
      "Download complete.\n"
     ]
    }
   ],
   "source": [
    "# Download the data from GitHub\n",
    "import os\n",
    "import urllib.request\n",
    "\n",
    "DATA_URL = \"https://raw.githubusercontent.com/REIL-UConn/fine-tuning-for-rapid-soh-estimation/main/processed_data/UConn-ILCC-NMC/data_slowpulse_1.pkl\"\n",
    "DATA_DIR = \"downloaded_data\"\n",
    "DATA_PATH = os.path.join(DATA_DIR, \"data_slowpulse_1.pkl\")\n",
    "\n",
    "# Create local directory\n",
    "os.makedirs(DATA_DIR, exist_ok=True)\n",
    "\n",
    "# Download if not already present\n",
    "if not os.path.exists(DATA_PATH):\n",
    "    print(\"Downloading data...\")\n",
    "    urllib.request.urlretrieve(DATA_URL, DATA_PATH)\n",
    "    print(\"Download complete.\")\n",
    "else:\n",
    "    print(\"Data already downloaded.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "5542b646",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(24048,) (24048,) (24048, 101) (24048,)\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/var/folders/21/fsx4ddjs3fg2wgpl7_ksh0k00000gn/T/ipykernel_63568/143080519.py:2: DeprecationWarning: numpy.core.numeric is deprecated and has been renamed to numpy._core.numeric. The numpy._core namespace contains private NumPy internals and its use is discouraged, as NumPy internals can change without warning in any release. In practice, most real-world usage of numpy.core is to access functionality in the public NumPy API. If that is the case, use the public NumPy API. If not, you are using NumPy internals. If you would still like to access an internal attribute, use numpy._core.numeric._frombuffer.\n",
      "  data = pickle.load(open(DATA_PATH, 'rb'))\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "dict_keys(['cell_id', 'group_id', 'rpt', 'num_cycles', 'soc', 'soc - coulomb', 'pulse_type', 'voltage', 'q_dchg', 'soh', 'dcir_chg_10', 'dcir_dchg_10', 'dcir_chg_20', 'dcir_dchg_20', 'dcir_chg_30', 'dcir_dchg_30', 'dcir_chg_40', 'dcir_dchg_40', 'dcir_chg_50', 'dcir_dchg_50', 'dcir_chg_60', 'dcir_dchg_60', 'dcir_chg_70', 'dcir_dchg_70', 'dcir_chg_80', 'dcir_dchg_80', 'dcir_chg_90', 'dcir_dchg_90'])"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Loading our data\n",
    "data = pickle.load(open(DATA_PATH, 'rb'))\n",
    "print(data['cell_id'].shape, data['group_id'].shape, data['voltage'].shape, data['soh'].shape)\n",
    "data.keys()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a35796cd",
   "metadata": {},
   "source": [
    "Here we see our initial data is structured a single dictionary. \n",
    "The FeatureSet class provides several constructors, including a `FeatureSet.from_dict` that we will use here.\n",
    "We will need to decide on the dictionary keys that consitute our the features, targets, and tags to be used in the downstream modeling.\n",
    "\n",
    "In this example, we will use `voltage` as our modeling input feature. Each `voltage` element is a 100-element vector of time-series voltage readings. Our target will be the `soh` (state-of-health) associated to each feature. To maintain sample traceability, and give us future options in how we split the data, we will retain several identifying tags with each sample. These include `cell_id`, `group_id`, `pulse_type`, and `pulse_soc`.\n",
    "\n",
    "We see that the structured FeatureSet contains 24048 valid samples."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "368990ac",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "FeatureSet(label='PulseFeatures', n_samples=24048)"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from modularml.core import FeatureSet\n",
    "\n",
    "fs = FeatureSet.from_dict(\n",
    "\tlabel='PulseFeatures',\n",
    "\tdata={\n",
    "\t\t'voltage': data['voltage'],\n",
    "\t\t'soh': data['soh'],\n",
    "\t\t'cell_id': data['cell_id'],\n",
    "  \t\t'group_id': data['group_id'],\n",
    "    \t'pulse_type': data['pulse_type'],\n",
    "     \t'pulse_soc': data['soc'],\n",
    "\t},\n",
    "\tfeature_keys='voltage',\n",
    "\ttarget_keys='soh',\n",
    "\ttag_keys=['cell_id', 'group_id', 'pulse_type', 'pulse_soc']\n",
    ")\n",
    "fs"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4033cbc1",
   "metadata": {},
   "source": [
    "Now we want to split our full FeatureSet into several subsets. ModularML provides several built-in splitting methods, including:\n",
    "* `FeatureSet.split_random()`: to create splits by assigning random proportions of all samples to each split\n",
    "* `FeatureSet.split_by_condition()`: to assign samples to subsets using any number of explicit conditions.\n",
    "\n",
    "To use a random 50% train, 30% valdation, 20% test split, we could do the following:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "03b9e2d0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[FeatureSubset(label='train', n_samples=12024),\n",
       " FeatureSubset(label='val', n_samples=7214),\n",
       " FeatureSubset(label='test', n_samples=4810)]"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fs.split_random(ratios={'train': 0.5, 'val':0.3, 'test':0.2})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d79f7de",
   "metadata": {},
   "source": [
    "But to better evaluate how good our SOH-estimation model really is, we'll want to isolate the train and test sets by unique cycling conditions. Therefore, we are testing how well our model can estimate SOH even when applied to unseen cycling conditions. \n",
    "\n",
    "Let's clear those subsets and create a more effective splitting strategy:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "007ae542",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "dict_keys([])"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fs.clear_subsets()\n",
    "fs.available_subsets"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a36dcbd5",
   "metadata": {},
   "source": [
    "The `FeatureSet.available_subset` attribute is useful for double-checking which subsets are available.\n",
    "\n",
    "In our data, unique cycling conditions are indentified by the `group_id` tag (a value between 1 and 11). We can easily double check the available tags information using `FeatureSet.get_all_tags`. This function allows for the returned information to be structured in several way (e.g., a Pandas dataframe, a dictionary, a numpy array, etc). We'll use a dataframe."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "c1b7823c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "group_id\n",
      "1     2196\n",
      "2     1962\n",
      "3     2898\n",
      "4     1206\n",
      "5     3852\n",
      "6     1890\n",
      "7      954\n",
      "8     4752\n",
      "9      738\n",
      "10    1494\n",
      "11    2106\n",
      "dtype: int64\n"
     ]
    }
   ],
   "source": [
    "df_tags = fs.get_all_tags(format=mml.DataFormat.PANDAS)\n",
    "df_tags.groupby('group_id').size()\n",
    "group_counts = df_tags.groupby('group_id').size()\n",
    "print(group_counts)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "95819f3a",
   "metadata": {},
   "source": [
    "Great, we see that `group_id` tag has 11 unique values. \n",
    "Note that there are a different number of samples in each group (this is due to different cycling condition resulting in faster or slower aging). \n",
    "We will adjust for this sample imbalance later.\n",
    "\n",
    "Back to splitting, we will take 6 cycling groups for training, 3 for validation, and 2 for testing.\n",
    "Let's make a quick helper function to random select the group_ids for each subset."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "c578ec5e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Train IDs:  [7 2 6 1 5 8]\n",
      "Val IDs:  [3 9 4]\n",
      "Test IDs:  [11 10]\n"
     ]
    }
   ],
   "source": [
    "def get_group_ids(train_size:int, val_size:int, test_size:int):\n",
    "    group_ids = np.arange(1, 12, 1)\n",
    "    # Shuffle the groups\n",
    "    rng = np.random.default_rng(seed=13)\n",
    "    rng.shuffle(group_ids)\n",
    "    \n",
    "    a, b = train_size, train_size + val_size\n",
    "    return group_ids[:a], group_ids[a:b], group_ids[b:]\n",
    "\n",
    "train_ids, val_ids, test_ids = get_group_ids(train_size=6, val_size=3, test_size=2)\n",
    "print(\"Train IDs: \", train_ids)\n",
    "print(\"Val IDs: \", val_ids)\n",
    "print(\"Test IDs: \", test_ids)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7739c81a",
   "metadata": {},
   "source": [
    "To split, we'll use `FeatureSet.split_by_condition` and define the explicit `group_id` values to assign to each subset."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "c7738882",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'cell_id': array([ 1,  1,  1, ..., 44, 44, 44]),\n",
       " 'group_id': array([ 1,  1,  1, ..., 11, 11, 11]),\n",
       " 'pulse_type': array(['chg', 'dchg', 'chg', ..., 'dchg', 'chg', 'dchg'], dtype='<U4'),\n",
       " 'pulse_soc': array([10, 10, 20, ..., 80, 90, 90])}"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fs.get_all_tags()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "dff601ac",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[FeatureSubset(label='train', n_samples=15606),\n",
       " FeatureSubset(label='val', n_samples=4842),\n",
       " FeatureSubset(label='test', n_samples=3600)]"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fs.split_by_condition(\n",
    "\ttrain={'group_id': train_ids},\n",
    "\tval={'group_id': val_ids},\n",
    "\ttest={'group_id': test_ids}\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8af4ae88",
   "metadata": {},
   "source": [
    "We can also plot a quick Sankey diagram to view our subsets.\n",
    "This exmaple is fairly straight forward, but ModularML allows for an unlimited number of nested subsets to be created within a single FeatureSet object.\n",
    "In these cases, the Sankey diagram becomes much more useful to visual the flow of samples into subsets. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "2f21747e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.plotly.v1+json": {
       "config": {
        "plotlyServerURL": "https://plot.ly"
       },
       "data": [
        {
         "arrangement": "snap",
         "link": {
          "color": "rgba(100,100,200,0.4)",
          "source": [
           0,
           0,
           0
          ],
          "target": [
           2,
           3,
           1
          ],
          "value": [
           15606,
           4842,
           3600
          ]
         },
         "node": {
          "label": [
           "PulseFeatures",
           "test",
           "train",
           "val"
          ],
          "line": {
           "color": "black",
           "width": 0.5
          },
          "pad": 15,
          "thickness": 20
         },
         "type": "sankey"
        }
       ],
       "layout": {
        "font": {
         "size": 12
        },
        "template": {
         "data": {
          "bar": [
           {
            "error_x": {
             "color": "#2a3f5f"
            },
            "error_y": {
             "color": "#2a3f5f"
            },
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "bar"
           }
          ],
          "barpolar": [
           {
            "marker": {
             "line": {
              "color": "#E5ECF6",
              "width": 0.5
             },
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "barpolar"
           }
          ],
          "carpet": [
           {
            "aaxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "baxis": {
             "endlinecolor": "#2a3f5f",
             "gridcolor": "white",
             "linecolor": "white",
             "minorgridcolor": "white",
             "startlinecolor": "#2a3f5f"
            },
            "type": "carpet"
           }
          ],
          "choropleth": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "choropleth"
           }
          ],
          "contour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1,
              "#f0f921"
             ]
            ],
            "type": "contour"
           }
          ],
          "contourcarpet": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "contourcarpet"
           }
          ],
          "heatmap": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1,
              "#f0f921"
             ]
            ],
            "type": "heatmap"
           }
          ],
          "histogram": [
           {
            "marker": {
             "pattern": {
              "fillmode": "overlay",
              "size": 10,
              "solidity": 0.2
             }
            },
            "type": "histogram"
           }
          ],
          "histogram2d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1,
              "#f0f921"
             ]
            ],
            "type": "histogram2d"
           }
          ],
          "histogram2dcontour": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1,
              "#f0f921"
             ]
            ],
            "type": "histogram2dcontour"
           }
          ],
          "mesh3d": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "type": "mesh3d"
           }
          ],
          "parcoords": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "parcoords"
           }
          ],
          "pie": [
           {
            "automargin": true,
            "type": "pie"
           }
          ],
          "scatter": [
           {
            "fillpattern": {
             "fillmode": "overlay",
             "size": 10,
             "solidity": 0.2
            },
            "type": "scatter"
           }
          ],
          "scatter3d": [
           {
            "line": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatter3d"
           }
          ],
          "scattercarpet": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattercarpet"
           }
          ],
          "scattergeo": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergeo"
           }
          ],
          "scattergl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattergl"
           }
          ],
          "scattermap": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermap"
           }
          ],
          "scattermapbox": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scattermapbox"
           }
          ],
          "scatterpolar": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolar"
           }
          ],
          "scatterpolargl": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterpolargl"
           }
          ],
          "scatterternary": [
           {
            "marker": {
             "colorbar": {
              "outlinewidth": 0,
              "ticks": ""
             }
            },
            "type": "scatterternary"
           }
          ],
          "surface": [
           {
            "colorbar": {
             "outlinewidth": 0,
             "ticks": ""
            },
            "colorscale": [
             [
              0,
              "#0d0887"
             ],
             [
              0.1111111111111111,
              "#46039f"
             ],
             [
              0.2222222222222222,
              "#7201a8"
             ],
             [
              0.3333333333333333,
              "#9c179e"
             ],
             [
              0.4444444444444444,
              "#bd3786"
             ],
             [
              0.5555555555555556,
              "#d8576b"
             ],
             [
              0.6666666666666666,
              "#ed7953"
             ],
             [
              0.7777777777777778,
              "#fb9f3a"
             ],
             [
              0.8888888888888888,
              "#fdca26"
             ],
             [
              1,
              "#f0f921"
             ]
            ],
            "type": "surface"
           }
          ],
          "table": [
           {
            "cells": {
             "fill": {
              "color": "#EBF0F8"
             },
             "line": {
              "color": "white"
             }
            },
            "header": {
             "fill": {
              "color": "#C8D4E3"
             },
             "line": {
              "color": "white"
             }
            },
            "type": "table"
           }
          ]
         },
         "layout": {
          "annotationdefaults": {
           "arrowcolor": "#2a3f5f",
           "arrowhead": 0,
           "arrowwidth": 1
          },
          "autotypenumbers": "strict",
          "coloraxis": {
           "colorbar": {
            "outlinewidth": 0,
            "ticks": ""
           }
          },
          "colorscale": {
           "diverging": [
            [
             0,
             "#8e0152"
            ],
            [
             0.1,
             "#c51b7d"
            ],
            [
             0.2,
             "#de77ae"
            ],
            [
             0.3,
             "#f1b6da"
            ],
            [
             0.4,
             "#fde0ef"
            ],
            [
             0.5,
             "#f7f7f7"
            ],
            [
             0.6,
             "#e6f5d0"
            ],
            [
             0.7,
             "#b8e186"
            ],
            [
             0.8,
             "#7fbc41"
            ],
            [
             0.9,
             "#4d9221"
            ],
            [
             1,
             "#276419"
            ]
           ],
           "sequential": [
            [
             0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1,
             "#f0f921"
            ]
           ],
           "sequentialminus": [
            [
             0,
             "#0d0887"
            ],
            [
             0.1111111111111111,
             "#46039f"
            ],
            [
             0.2222222222222222,
             "#7201a8"
            ],
            [
             0.3333333333333333,
             "#9c179e"
            ],
            [
             0.4444444444444444,
             "#bd3786"
            ],
            [
             0.5555555555555556,
             "#d8576b"
            ],
            [
             0.6666666666666666,
             "#ed7953"
            ],
            [
             0.7777777777777778,
             "#fb9f3a"
            ],
            [
             0.8888888888888888,
             "#fdca26"
            ],
            [
             1,
             "#f0f921"
            ]
           ]
          },
          "colorway": [
           "#636efa",
           "#EF553B",
           "#00cc96",
           "#ab63fa",
           "#FFA15A",
           "#19d3f3",
           "#FF6692",
           "#B6E880",
           "#FF97FF",
           "#FECB52"
          ],
          "font": {
           "color": "#2a3f5f"
          },
          "geo": {
           "bgcolor": "white",
           "lakecolor": "white",
           "landcolor": "#E5ECF6",
           "showlakes": true,
           "showland": true,
           "subunitcolor": "white"
          },
          "hoverlabel": {
           "align": "left"
          },
          "hovermode": "closest",
          "mapbox": {
           "style": "light"
          },
          "paper_bgcolor": "white",
          "plot_bgcolor": "#E5ECF6",
          "polar": {
           "angularaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "radialaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "scene": {
           "xaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "yaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           },
           "zaxis": {
            "backgroundcolor": "#E5ECF6",
            "gridcolor": "white",
            "gridwidth": 2,
            "linecolor": "white",
            "showbackground": true,
            "ticks": "",
            "zerolinecolor": "white"
           }
          },
          "shapedefaults": {
           "line": {
            "color": "#2a3f5f"
           }
          },
          "ternary": {
           "aaxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "baxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           },
           "bgcolor": "#E5ECF6",
           "caxis": {
            "gridcolor": "white",
            "linecolor": "white",
            "ticks": ""
           }
          },
          "title": {
           "x": 0.05
          },
          "xaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          },
          "yaxis": {
           "automargin": true,
           "gridcolor": "white",
           "linecolor": "white",
           "ticks": "",
           "title": {
            "standoff": 15
           },
           "zerolinecolor": "white",
           "zerolinewidth": 2
          }
         }
        },
        "title": {
         "text": "FeatureSet Subset Sankey"
        }
       }
      }
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fs.plot_sankey()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "72749608",
   "metadata": {},
   "source": [
    "# COMING SOON: Adding FeatureTransform Logic\n",
    "\n",
    "The FeatureTransform class encapsulates all feature/target scaling and normalization methods.\n",
    "Its wraps all available scalers from `sklearn.preprocessing` and provides easy integration with the `FeatureSet` class.\n",
    "\n",
    "Apply transforms to the underlying features in FeatureSet are as easy as: \n",
    "``` python\n",
    "FeatureSet.fit_transform(fit='features', apply='features', transform=FeatureTransform(...))\n",
    "```\n",
    "Here we fit the transform object to `fit='features'` (i.e., fit to all features in the FeatureSet), and applied it to `apply='features'` (i.e., apply to all features in the FeatureSet)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "764a217a",
   "metadata": {},
   "source": [
    "However, to prevent data leakage in ML workflow, it is required that any data transform be fit only to the training set.\n",
    "This can be accomplished with a simple adjustment of the `fit` argument: \n",
    "``` python\n",
    "FeatureSet.fit_transform(fit='train.features', apply='features', transform=FeatureTransform(...))\n",
    "```\n",
    "Now, the transform is only fit to all features in the train subset (`fit='train.features'`)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19f48819",
   "metadata": {},
   "source": [
    "Note that all data access methods of FeatureSet (e.g., `.get_all_features()`) will return the transformed versions of the data by default.\n",
    "Receiving the unscaled data can be achieved by using the `scaled` argument of the data access methods (e.g., `.get_all_features(scaled=False)`).\n",
    "\n",
    "Additionally, theunderlying transform in the `FeatureTransform(...)` object passed to `FeatureSet.fit_transform(...)` is not mutated.\n",
    "A copy is made and stored internal to FeatureSet.\n",
    "Accessing the fit tranforms is achieved with the `FeatureSet.available_transform` property. This returns a list of fit FeatureTransform objects.\n",
    "\n",
    "They can be reused later without re-fitting using the `.transform()` method of `FeatureSet`:\n",
    "``` python\n",
    "fs = FeatureSet(...)\n",
    "my_transform = fs.available_transforms[0]\n",
    "fs.transform(apply='new_subset.features', transform=my_transform)\n",
    "```\n",
    "Here we applied the transform to the features in some other subset (`new_subset`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d766da39",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "edec6e5f",
   "metadata": {},
   "source": [
    "Now that we have our main FeatureSet and corresponding subsets, let's move on to step 2."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18885b45",
   "metadata": {},
   "source": [
    "---\n",
    "### 2 - Building a ModelGraph\n",
    "---\n",
    "\n",
    "One of most powerful aspects of ModularML is its directed-acyclic-graph (DAG)-based ModelGraph container.\n",
    "It allows for any number of ModelStages to be linked together into a larger structure.\n",
    "Even better, each ModelStage can have any backend (e.g., PyTorch, Tensorflow/Keras, Scikit-learn).\n",
    "This lets you defined a complex multi-objective evaluation pipeline all under a unified ModelGraph.\n",
    "\n",
    "But let's stop talking and get back to modeling...\n",
    "\n",
    "In this example, we'll create a multi-stage model that uses a CNN encoder to embed the voltage features into some learned latent space and a final MLP regressor to convert the embedded feature into an SOH estimate.\n",
    "\n",
    "ModularML provides pre-built classes for the more commonly used model like sequential CNNs and MLPs. We'll use those here, but any custom model can be easily defined. All you need to do is subclass the `modularml.BaseModel` class and defined the required methods.\n",
    "\n",
    "With complex ModelGraphs, it can get annoying to keep track of input/output shape of each ModelStage.\n",
    "Don't worry, ModularML can infer these for you at runtime.\n",
    "Just leave any shapes you don't know as None and ModularML will handle the rest.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "f2735a55",
   "metadata": {},
   "outputs": [],
   "source": [
    "from modularml.core import StageInput, ModelStage, ModelGraph, Optimizer\n",
    "from modularml.models import SequentialCNN, SequentialMLP\n",
    "\n",
    "\n",
    "ms_encoder = ModelStage(\n",
    "\tmodel=SequentialCNN(output_shape=(1, 32), n_layers=2, hidden_dim=16, flatten_output=True),\n",
    "\tlabel=\"Encoder\",\n",
    "\tinputs=StageInput(source='PulseFeatures'),\t\t\t\t# input from FeatureSet\n",
    "\toptimizer=Optimizer(name='adam', backend=mml.Backend.TORCH)\n",
    ")\n",
    "\n",
    "ms_regressor = ModelStage(\n",
    "\tmodel=SequentialMLP(output_shape=(1,1), n_layers=2, hidden_dim=16),\n",
    " \tlabel='Regressor',\n",
    "  \tinputs=StageInput(source='Encoder'),\t\t\t\t\t# input from Encoder\n",
    "\toptimizer=Optimizer(name='adam', backend=mml.Backend.TORCH)\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a639d87",
   "metadata": {},
   "source": [
    "With the ModelStages defined, we simply need to pass them all to a single ModelGraph instance.\n",
    "\n",
    "The ModelGraph will handle all data routing and shape inference with the `.build_all()` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "134d9fda",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Inferred shapes for `Encoder`:  torch.Size([1, 101]) -> (1, 32)\n",
      "Inferred shapes for `Regressor`:  (1, 32) -> (1, 1)\n"
     ]
    }
   ],
   "source": [
    "# Order of node arguments don't matter but make sure you include any \n",
    "# FeatureSets referenced by any ModelStage.inputs\n",
    "mg = ModelGraph(nodes=[fs, ms_regressor, ms_encoder, ])\n",
    "mg.build_all()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c09c6b0e",
   "metadata": {},
   "source": [
    "Lets run a temporary forward pass to make sure everythings connected properly.\n",
    "\n",
    "ModelGraph builds in a connection test with `.dummy_forward()`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "539e18e0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[[[-0.20918120443820953]],\n",
       " [[-0.20918120443820953]],\n",
       " [[-0.20918120443820953]],\n",
       " [[-0.20918120443820953]],\n",
       " [[-0.20918120443820953]],\n",
       " [[-0.20918120443820953]],\n",
       " [[-0.20918120443820953]],\n",
       " [[-0.20918120443820953]]]"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "mg.dummy_foward(batch_size=8)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0517e89e",
   "metadata": {},
   "source": [
    "ModelGraph also has a built in visualization method to view all node connection, although it's not too exciting for this example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "80a1ed6e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(<Figure size 600x300 with 1 Axes>, <Axes: >)"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAu0AAAD7CAYAAADerqrIAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAATCBJREFUeJzt3XlcTenjB/DPlZabIhIlLVK0kKWQYcQwpYUwY5msk3Uwfcc+M0Q0hsm+Z8ZMl68ta0NF1hhjGRrRVBKTaZAxISRR3fP7o1/n6yotVPfQ5/169Xp179mec+4jn/uc5zyPTBAEAUREREREJFk11F0AIiIiIiIqGUM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxDOxERERGRxDG0ExERERFJHEM7EREREZHEMbQTEREREUkcQzsRERERkcQxtBMRERERSRxD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",
      "text/plain": [
       "<Figure size 600x300 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "mg.visualize()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "envModularML",
   "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.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
