diff --git a/notebooks/PyG_BGR_Classification.ipynb b/notebooks/PyG_BGR_Classification.ipynb
new file mode 100644
index 0000000..9ef07e9
--- /dev/null
+++ b/notebooks/PyG_BGR_Classification.ipynb
@@ -0,0 +1,3028 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "f6f4b050",
+ "metadata": {},
+ "source": [
+ "# Graph Classification Using TopologicPy and PyTorch Geometric\n",
+ "### Professor Wassim Jabi"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7cb27b44",
+ "metadata": {},
+ "source": [
+ "## Instructions\n",
+ "1. Make sure you have first fully installed the latest version of topologicpy (minimum 0.7.39 or newer).\n",
+ "2. Make sure you have pip installed torch and torch_geometric per the instructions for your OS.\n",
+ "3. The datasets needed for this to work are on github at: https://github.com/wassimj/topologicpy/tree/main/assets/MachineLearning\n",
+ "4. Clone the github repository or download these files and make a note of where you saved them\n",
+ "5. Replace the paths below where indicated with the location of these saved files."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ea59c450",
+ "metadata": {},
+ "source": [
+ "### The cell below is only for the development machine. Do NOT run it. Instead, make sure you have pip installed topologicpy"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "7250aac1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sys\n",
+ "sys.path.append(r\"C:\\Users\\sarwj\\OneDrive - Cardiff University\\Documents\\GitHub\\topologicpy\\src\") # change the path"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c9cec647",
+ "metadata": {},
+ "source": [
+ "## Import Libraries"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "34a1ca2d",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "from topologicpy.Plotly import Plotly\n",
+ "from topologicpy.PyG import PyG\n",
+ "import torch\n",
+ "import torch_geometric\n",
+ "from topologicpy.Helper import Helper"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5035f0bc",
+ "metadata": {},
+ "source": [
+ "## Check CUDA availability and Version Numbers"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "5f845142",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "TopologicPy version: 0.7.39\n",
+ "PyTorch Version: 2.4.0+cu121\n",
+ "PyTorch Geometric Version: 2.5.3\n",
+ "CUDA available: True\n",
+ "CUDA version: 12.1\n",
+ "Number of CUDA devices available: 1\n",
+ "Using device: cuda\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Print topologicpy version\n",
+ "print(\"TopologicPy version:\", Helper.Version())\n",
+ "\n",
+ "# Print PyTorch version\n",
+ "print(\"PyTorch Version:\", torch.__version__)\n",
+ "\n",
+ "# Print PyTorch Geometric version\n",
+ "print(\"PyTorch Geometric Version:\", torch_geometric.__version__)\n",
+ "\n",
+ "# Check if CUDA is available\n",
+ "cuda_available = torch.cuda.is_available()\n",
+ "print(f\"CUDA available: {cuda_available}\")\n",
+ "if cuda_available:\n",
+ " print(\"CUDA version:\", torch.version.cuda)\n",
+ " # Print the number of available CUDA devices\n",
+ " print(f\"Number of CUDA devices available: {torch.cuda.device_count()}\")\n",
+ "\n",
+ "# Get the default device\n",
+ "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
+ "print(f\"Using device: {device}\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "20dbbc9d",
+ "metadata": {},
+ "source": [
+ "## Specify paths and global variables"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "b7efd4c9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "path_to_training_dataset_folder = r\"C:\\Users\\sarwj\\OneDrive - Cardiff University\\Documents\\GitHub\\topologicpy\\assets\\MachineLearning\\training_dataset\"\n",
+ "path_to_testing_dataset_folder = r\"C:\\Users\\sarwj\\OneDrive - Cardiff University\\Documents\\GitHub\\topologicpy\\assets\\MachineLearning\\training_dataset\"\n",
+ "path_to_pretrained_model = r\"C:\\Users\\sarwj\\OneDrive - Cardiff University\\Documents\\GitHub\\topologicpy\\assets\\MachineLearning\\training_dataset\\pretrained-model.pt\"\n",
+ "\n",
+ "# defining the type of labels by setting the GRAPH_LEVEL and NODE_LEVEL as global variables\n",
+ "# if we set GRAPH_LEVEL to be true then labels in the dataset are related with graph labels\n",
+ "# else if we set NODE_LEVEL to be true then the labels in the dataset are relates with node labels\n",
+ "GRAPH_LEVEL = True\n",
+ "NODE_LEVEL = False\n",
+ "# input type is selected as graph if graph_level is true otherwise selcted as node\n",
+ "INPUT_TYPE = 'graph' if GRAPH_LEVEL else 'node'"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "31194ee9",
+ "metadata": {},
+ "source": [
+ "# Phase 1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d8e16b76",
+ "metadata": {},
+ "source": [
+ "## Create a Dataset"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "86bc4a49",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Number of graphs in dataset 1496\n",
+ "data samples\n",
+ "Data(x=[52, 1], edge_index=[2, 234], y=[1])\n",
+ "Data(x=[115, 1], edge_index=[2, 688], y=[1])\n",
+ "Number of classes 5\n"
+ ]
+ }
+ ],
+ "source": [
+ "dataset = PyG.DatasetByCSVPath(path=path_to_training_dataset_folder, graph_level=GRAPH_LEVEL, node_level=NODE_LEVEL)\n",
+ "\n",
+ "print(f\"Number of graphs in dataset {len(dataset)}\")\n",
+ "print(\"data samples\")\n",
+ "print(dataset[0])\n",
+ "print(dataset[-1])\n",
+ "print(f\"Number of classes {dataset.num_classes}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2d766187",
+ "metadata": {},
+ "source": [
+ "## Specify Hyperparameters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "81057484",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Define the Hyper-parameters\n",
+ "# Define the Hyper-parameters\n",
+ "optimizer = PyG.Optimizer(lr=0.001)\n",
+ "hparams_class = PyG.Hyperparameters(optimizer=optimizer,\n",
+ " model_type=\"Classifier\", # \"Regressor\", # \"Classifier\",\n",
+ " cv_type=\"Holdout\", # \"KFold\", \"Holdout\",\n",
+ " k_folds=3, #not used in Holdout\n",
+ " split=[0.8,0.1,0.1], #ignored in K-Folds or if separate datasets are provided\n",
+ " hl_widths=[32,32,32],\n",
+ " conv_layer_type=\"SAGEConv\",\n",
+ " epochs=40,\n",
+ " pooling=\"AvgPooling\", # pooling will be note used in case of node classification or regression\n",
+ " batch_size=1,\n",
+ " input_type=INPUT_TYPE # \"graph\" # \"node\"\n",
+ " )\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2c4a512a",
+ "metadata": {},
+ "source": [
+ "## Train the Model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 46,
+ "id": "fd8a9987",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "518cecabcbd6457a93a540bb82bb8614",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Epochs: 2%|2 | 1/40 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "45a850c946194d918f2f65f90e2c458c",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "f3ba0ee46e4f489ca343a1028e7b7285",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "e07b9098c09942e89808be2c8d0d29bc",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "1f39e0a7299845e38d17cc9433082c99",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "74eb97ddbe96484cbb194a85e839a5f1",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "2bbd4e39ff8248ebb4cd77387670efd9",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "615a8e75b47542a68c3682d390d618d4",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "31b19e35a49d4feaa14fdd0907a75b5b",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "9acc8e1b9385458c9b8fa7da7427626e",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "10ef8ac3051c42b3a509118610f417c9",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "c5aae0b086b145aeb1043c1b617df9e7",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "17dffdfecdd94810bedf5b41b4d61474",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "48f743efe2384d30802dfc2abcbf5f27",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "763c232fd8134b33aa7d7fdc6e3b37a3",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "c7372c1db6114e9ab9996e7499207985",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "6d5e41a1651f499da0e36f45ae337b48",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "49fb0578f46b4f4f822d65592c8f9f82",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "2fc0857ad83147ce8f474118bcdebdbc",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "872fae3b67aa44e7bada71e0d0c1cb41",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "74968309adde4237922945f8b52b4028",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "9165e3c1e25c4b68a234a840051aac07",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "4a5dd51d58334d0f95b25a1c1a6dca41",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "d63d558eec42454b888f014910c89dfe",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "8a8d84494a644377a4a22e0c53efe6b9",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "0552a73daf2d4f2fbbdf7e9a37811e37",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "f04aca8d5ead4bcab0a538324837176b",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "56cb5beea3e448939330706465123b7e",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "b214e5acd03941efa33932bcdec57e20",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "458c08a55b354fadb2fafbf36c4e311d",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "d69aa9985855454f832930e83ca59365",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "7582f12deab14b4ebb28b5fb5b8af0aa",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "272397b8b3b34ade8b5a21dc611151ae",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "bcd88263a61e4d178bd2fee9dd5b7ec9",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "f915935ab8784f118c580dc113482b79",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "c010c2adf0c749da8c3f1e54d53a94ce",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "1b8aee4ad915464db1cfbc49a3e281ca",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "3d1ff1330124470c8228cfa2998a1020",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "b7ecfde84ef74126a649db11fcc085ef",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "e398f7061aa04505aad7549fe6672b8b",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "cb3b70ab65ca44879fb2b1964acf340f",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "6d32cd3b73de4de8b84740d801e286f8",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "bf0aa4b4f03744aeafb5d7ef06226f17",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "229218efdbee490fa70bf74d75fb7139",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "afcfc7bab8724c519121e2195bd1c8f3",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "6dadb6929a3944bc8efcfb0e7555d886",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "754091dbf308498e856b33dfef0a8439",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "33fa30b8dee04274a5377cfda7b8c23b",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "0b3080e232254253be27cb189fb629bc",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "e11cefa4d8f646ac86023cac4a7b451a",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "baaa79c9551a415b9120067b71c97569",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "7ec5a6f6c5eb4d97a097a0b7e2a63d13",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "87ccf57dc1734320a0feb77da01672b4",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "4309a2edf8d84dada1b77b7fa46a1795",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "41788ed4cb4b4ddab436c539eacea587",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "2ede9803eba046aeaceb05f9a7fb4b10",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "3752eadc5cd44a84adcc1887d83a2229",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "3507ce60cf74402f9cca7e4950bc94a4",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "ab9db8e1fea24e95b769ef6a5004a1c5",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "f3d05b3d11e2441386e3a8dd53148b5b",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "9c23af5f37444cd38e91b71e745e7b1f",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "34623f81bda34886ad931b17df211f57",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "8c4e35845c4842789a9b6777041c03c2",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "b54b091060524dec9bdf7a566bb1db6e",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "b38d756443d44b9795db85c7c61ca44f",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "cac41aa7d0474f418ae6a6253eceaab2",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "79f21a8d433c475a910e294e826aafb4",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "70fffe2f3823440cab23b6158dabae87",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "b2bbfed2c0c1433d8342636ff0d59655",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "cbf45f59a7be437fa3bd390bc1d43262",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "f315b4474dc744649b6b5dc6a61fa3b7",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "b58cf820a024420ea75f64a69d00e1eb",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "ec685ff0949943ca843944dbce8e1ef9",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "9676458a554547368ccad6deb7d40e1b",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "b405c364d8554777bf02ade9aeddcf37",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "5aacfaaf5ad84f75b5792c70febccf01",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "1b570d1ff7c0466ca4cb96355f766981",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "8179190773ec498d8a42f0befaf95f14",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "08f87f7c9f1a47f0b3e88b16cf6c2b0a",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "d4917dc524fe491eaee43787807dfbd2",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Training: 0%| | 0/1196 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "644c888e2848491fb025a7117b7b07f5",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Validating: 0%| | 0/149 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "CPU times: total: 24.7 s\n",
+ "Wall time: 3min 21s\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Train/Validate/Test your data\n",
+ "model = PyG.Model(hparams=hparams_class, trainingDataset=dataset, validationDataset=None)\n",
+ "%time model = PyG.ModelTrain(model)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ae03acb2",
+ "metadata": {},
+ "source": [
+ "## Test the Model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 47,
+ "id": "2163a81a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "d3f2f026b6d244d5a7ca0e60d885e792",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Testing: 0%| | 0/151 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Validation Accuracy: 0.98\n",
+ "Testing Accuracy: 0.99\n"
+ ]
+ }
+ ],
+ "source": [
+ "model = PyG.ModelTest(model)\n",
+ "if hparams_class.model_type.lower() == \"regressor\":\n",
+ " print(\"Loss:\", round(model.testing_loss, 2))\n",
+ "else:\n",
+ " print(\"Validation Accuracy:\", round(model.validation_accuracy, 2))\n",
+ " print(\"Testing Accuracy:\", round(model.testing_accuracy, 2))\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a61067c5",
+ "metadata": {},
+ "source": [
+ "## Save the Model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 48,
+ "id": "31384ceb",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "True\n"
+ ]
+ }
+ ],
+ "source": [
+ "status = PyG.ModelSave(model, path=path_to_pretrained_model, overwrite=True) # Change the path!\n",
+ "print(status)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1da8d6a4",
+ "metadata": {},
+ "source": [
+ "## Plot the results"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 49,
+ "id": "d1d81871",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "
"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "data = PyG.ModelData(model)\n",
+ "# Show the results\n",
+ "if hparams_class.model_type == \"regressor\":\n",
+ " labels = [\"Epochs\", \"Training Loss\", \"Validation Loss\"]\n",
+ "else:\n",
+ " labels = [\"Epochs\", \"Training Accuracy\", \"Validation Accuracy\", \"Training Loss\", \"Validation Loss\"]\n",
+ "PyG.Show(data, labels, width=1024, height=800)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f2a4b2f7",
+ "metadata": {},
+ "source": [
+ "# Phase 2\n",
+ "## Load Testing Dataset"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "d2d99433",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "1496\n",
+ "Data(x=[52, 1], edge_index=[2, 234], y=[1])\n",
+ "Data(x=[115, 1], edge_index=[2, 688], y=[1])\n",
+ "Length of actual labels 1496\n"
+ ]
+ }
+ ],
+ "source": [
+ "test_dataset = PyG.DatasetByCSVPath(path=path_to_testing_dataset_folder, graph_level=GRAPH_LEVEL, node_level=NODE_LEVEL)\n",
+ "print(len(test_dataset))\n",
+ "print(test_dataset[0])\n",
+ "print(test_dataset[-1])\n",
+ "\n",
+ "actual = []\n",
+ "for i in range(len(test_dataset)):\n",
+ " actual.extend(test_dataset[i].y.tolist())\n",
+ "\n",
+ "print(f\"Length of actual labels {len(actual)}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0dfc4e0f",
+ "metadata": {},
+ "source": [
+ "## Load Model from File Path"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "be9f07b6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "model = PyG.Model(hparams=hparams_class, trainingDataset=dataset)\n",
+ "model = PyG.ModelLoad(path=path_to_pretrained_model, model=model) # Change the path!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e645b033-d79d-4a97-b211-6dc0950398ae",
+ "metadata": {},
+ "source": [
+ "## Model Prediction"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "14b61927-5987-4b71-9cb6-4b6a3f351890",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "594d7b43131f41b6a72cb73c63a1a6f2",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "Classifying: 0%| | 0/1496 [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Accuracy: 0.992647\n"
+ ]
+ }
+ ],
+ "source": [
+ "if hparams_class.model_type.lower() == \"regressor\":\n",
+ " predicted = PyG.ModelPredict(model, test_dataset)\n",
+ " result = PyG.MSE(actual=actual, predicted=predicted)\n",
+ " print(\"MSE:\", result['mse'])\n",
+ "else:\n",
+ " dictionary = PyG.ModelClassify(model, test_dataset)\n",
+ " predicted = dictionary['predictions']\n",
+ " result = PyG.Accuracy(actual=actual, predicted=predicted)\n",
+ " print(\"Accuracy:\", result['accuracy'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3d85f76d",
+ "metadata": {},
+ "source": [
+ "## Plot the Confusion Matrix"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "01e6b7a9",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[[524 3 0 0 1]\n",
+ " [ 1 540 0 0 0]\n",
+ " [ 0 0 70 0 4]\n",
+ " [ 0 0 0 56 0]\n",
+ " [ 0 0 2 0 295]]\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.plotly.v1+json": {
+ "config": {
+ "plotlyServerURL": "https://plot.ly"
+ },
+ "data": [
+ {
+ "colorscale": [
+ [
+ 0,
+ "rgb(68, 1, 84)"
+ ],
+ [
+ 0.1111111111111111,
+ "rgb(72, 40, 120)"
+ ],
+ [
+ 0.2222222222222222,
+ "rgb(62, 73, 137)"
+ ],
+ [
+ 0.3333333333333333,
+ "rgb(49, 104, 142)"
+ ],
+ [
+ 0.4444444444444444,
+ "rgb(38, 130, 142)"
+ ],
+ [
+ 0.5555555555555556,
+ "rgb(31, 158, 137)"
+ ],
+ [
+ 0.6666666666666666,
+ "rgb(53, 183, 121)"
+ ],
+ [
+ 0.7777777777777778,
+ "rgb(110, 206, 88)"
+ ],
+ [
+ 0.8888888888888888,
+ "rgb(181, 222, 43)"
+ ],
+ [
+ 1,
+ "rgb(253, 231, 37)"
+ ]
+ ],
+ "showscale": true,
+ "type": "heatmap",
+ "x": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "y": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "z": [
+ [
+ 0.992424,
+ 0.005682,
+ 0,
+ 0,
+ 0.001894
+ ],
+ [
+ 0.001848,
+ 0.998152,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0.945946,
+ 0,
+ 0.054054
+ ],
+ [
+ 0,
+ 0,
+ 0,
+ 1,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0.006734,
+ 0,
+ 0.993266
+ ]
+ ],
+ "zmax": 540
+ }
+ ],
+ "layout": {
+ "annotations": [
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "524",
+ "x": 0,
+ "xref": "x",
+ "y": 0,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "3",
+ "x": 1,
+ "xref": "x",
+ "y": 0,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 2,
+ "xref": "x",
+ "y": 0,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 3,
+ "xref": "x",
+ "y": 0,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "1",
+ "x": 4,
+ "xref": "x",
+ "y": 0,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "1",
+ "x": 0,
+ "xref": "x",
+ "y": 1,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "540",
+ "x": 1,
+ "xref": "x",
+ "y": 1,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 2,
+ "xref": "x",
+ "y": 1,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 3,
+ "xref": "x",
+ "y": 1,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 4,
+ "xref": "x",
+ "y": 1,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 0,
+ "xref": "x",
+ "y": 2,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 1,
+ "xref": "x",
+ "y": 2,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "70",
+ "x": 2,
+ "xref": "x",
+ "y": 2,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 3,
+ "xref": "x",
+ "y": 2,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "4",
+ "x": 4,
+ "xref": "x",
+ "y": 2,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 0,
+ "xref": "x",
+ "y": 3,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 1,
+ "xref": "x",
+ "y": 3,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 2,
+ "xref": "x",
+ "y": 3,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "56",
+ "x": 3,
+ "xref": "x",
+ "y": 3,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 4,
+ "xref": "x",
+ "y": 3,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 0,
+ "xref": "x",
+ "y": 4,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 1,
+ "xref": "x",
+ "y": 4,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "2",
+ "x": 2,
+ "xref": "x",
+ "y": 4,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "0",
+ "x": 3,
+ "xref": "x",
+ "y": 4,
+ "yref": "y"
+ },
+ {
+ "bgcolor": "white",
+ "font": {
+ "color": "black"
+ },
+ "opacity": 0.5,
+ "showarrow": false,
+ "text": "295",
+ "x": 4,
+ "xref": "x",
+ "y": 4,
+ "yref": "y"
+ }
+ ],
+ "autosize": true,
+ "height": 500,
+ "margin": {
+ "b": 40,
+ "l": 40,
+ "r": 40,
+ "t": 40
+ },
+ "paper_bgcolor": "rgba(0,0,0,0)",
+ "plot_bgcolor": "rgba(0,0,0,0)",
+ "scene": {
+ "camera": {
+ "center": {
+ "x": 0,
+ "y": 0,
+ "z": 0
+ },
+ "eye": {
+ "x": -1.25,
+ "y": -1.25,
+ "z": 1.25
+ },
+ "projection": {
+ "type": "perspective"
+ },
+ "up": {
+ "x": 0,
+ "y": 0,
+ "z": 1
+ }
+ }
+ },
+ "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"
+ }
+ ],
+ "heatmapgl": [
+ {
+ "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": "heatmapgl"
+ }
+ ],
+ "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"
+ }
+ ],
+ "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": "Confusion Matrix"
+ },
+ "width": 950,
+ "xaxis": {
+ "tickvals": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "title": {
+ "text": "Actual"
+ }
+ },
+ "yaxis": {
+ "autorange": "reversed",
+ "tickvals": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "title": {
+ "text": "Predicted"
+ }
+ }
+ }
+ }
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "if hparams_class.model_type.lower() != \"regressor\":\n",
+ " cf = PyG.ConfusionMatrix(actual = actual, predicted = predicted)\n",
+ " print(cf)\n",
+ " fig = Plotly.FigureByConfusionMatrix(cf)\n",
+ " Plotly.Show(fig)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/src/topologicpy/ANN.py b/src/topologicpy/ANN.py
index 98319f5..b65b591 100644
--- a/src/topologicpy/ANN.py
+++ b/src/topologicpy/ANN.py
@@ -471,27 +471,11 @@ def DatasetBySampleName(name):
----------
name : str
The name of the dataset. This can be one of ['breast_cancer', 'california_housing', 'digits', 'iris', 'wine']
-
- trainRatio : float , optional
- The ratio of the data to use for training and validation vs. the ratio to use for testing. The default is 0.6
- which means that 60% of the data will be used for training and validation while 40% of the data will be reserved for testing.
- randomState : int , optional
- The randomState parameter is used to ensure reproducibility of the results. When you set the randomState parameter to a specific integer value,
- it controls the shuffling of the data before splitting it into training and testing sets.
- This means that every time you run your code with the same randomState value and the same dataset, you will get the same split of the data.
- The default is 42 which is just a randomly picked integer number. Specify None for random sampling.
Returns
-------
- dict
- Returns the following dictionary:
- XTrain, XTest, yTrain, yTest, inputSize, outputSize
- XTrain is the list of features used for training
- XTest is the list of features used for testing
- yTrain is the list of targets used for training
- yTest is the list of targets used for testing
- inputSize is the size (length) of the input
- outputSize is the size (length) of the output
+ sklearn.utils._bunch.Bunch
+ The created dataset.
"""
# Load dataset
if name == 'breast_cancer':
diff --git a/src/topologicpy/Face.py b/src/topologicpy/Face.py
index 40b3f09..b4299d1 100644
--- a/src/topologicpy/Face.py
+++ b/src/topologicpy/Face.py
@@ -521,8 +521,8 @@ def ByThickenedWire(wire, offsetA: float = 1.0, offsetB: float = 0.0, tolerance:
Returns
-------
- topologic_core.Cell
- The created cell.
+ topologic_core.Face
+ The created face.
"""
from topologicpy.Vertex import Vertex
@@ -2170,19 +2170,15 @@ def Squircle(origin = None, radius: float = 0.5, sides: int = 121, a: float = 2.
origin : topologic_core.Vertex , optional
The location of the origin of the squircle. The default is None which results in the squircle being placed at (0, 0, 0).
radius : float , optional
- The radius of the squircle. The default is 0.5.
+ The desired radius of the squircle. The default is 0.5.
sides : int , optional
- The number of sides of the squircle. The default is 121.
+ The desired number of sides of the squircle. The default is 121.
a : float , optional
The "a" factor affects the x position of the points to interpolate between a circle and a square.
A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0.
b : float , optional
The "b" factor affects the y position of the points to interpolate between a circle and a square.
A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0.
- radius : float , optional
- The desired radius of the squircle. The default is 0.5.
- sides : int , optional
- The desired number of sides for the squircle. The default is 100.
direction : list , optional
The vector representing the up direction of the circle. The default is [0, 0, 1].
placement : str , optional
diff --git a/src/topologicpy/Graph.py b/src/topologicpy/Graph.py
index 5847e39..02d2140 100644
--- a/src/topologicpy/Graph.py
+++ b/src/topologicpy/Graph.py
@@ -4686,6 +4686,8 @@ def ExportToAdjacencyMatrixCSV(adjacencyMatrix, path):
Parameters
----------
+ adjacencyMatrix: list
+ The input adjacency matrix.
path : str
The desired path to the output folder where the graphs, edges, and nodes CSV files will be saved.
@@ -4936,7 +4938,6 @@ def ExportToCSV(graph, path, graphLabel, graphFeatures="",
The desired graph label column header. The default is "label".
graphFeaturesHeader : str , optional
The desired graph features column header. The default is "feat".
-
edgeLabelKey : str , optional
The edge label dictionary key saved in each graph edge. The default is "label".
defaultEdgeLabel : int , optional
@@ -4971,7 +4972,6 @@ def ExportToCSV(graph, path, graphLabel, graphFeatures="",
This value is ignored if an edgeMaskKey is foud.
bidirectional : bool , optional
If set to True, a reversed edge will also be saved for each edge in the graph. Otherwise, it will not. The default is True.
-
nodeFeaturesKeys : list , optional
The list of features keys saved in the dicitonaries of nodes. The default is [].
nodeLabelKey : str , optional
@@ -7444,7 +7444,7 @@ def PyvisGraph(graph, path, overwrite: bool = True, height: int = 900, backgroun
The desired default vertex size. The default is 6.
vertexSizeKey : str , optional
If not set to None, the vertex size will be derived from the dictionary value set at this key. If set to "degree", the size of the vertex will be determined by its degree (number of neighbors). The default is None.
- vertexColor : int , optional
+ vertexColor : str , optional
The desired default vertex color. his can be a named color or a hexadecimal value. The default is 'black'.
vertexColorKey : str , optional
If not set to None, the vertex color will be derived from the dictionary value set at this key. The default is None.
@@ -7884,6 +7884,8 @@ def Show(graph, vertexColor="black", vertexSize=6, vertexLabelKey=None, vertexGr
The dictionary key to use to display the edge label. The default is None.
edgeGroupKey : str , optional
The dictionary key to use to display the edge group. The default is None.
+ edgeGroups : list , optional
+ The list of edge groups against which to index the color of the edge. The default is [].
showEdges : bool , optional
If set to True the edges will be drawn. Otherwise, they will not be drawn. The default is True.
showEdgeLegend : bool , optional
diff --git a/src/topologicpy/Grid.py b/src/topologicpy/Grid.py
index 25aaac2..2ef82ca 100644
--- a/src/topologicpy/Grid.py
+++ b/src/topologicpy/Grid.py
@@ -307,8 +307,6 @@ def VerticesByParameters(face=None, uRange=[0.0,0.25,0.5,0.75,1.0], vRange=[0.0,
----------
face : topologic_core.Face , optional
The input face. If set to None, the grid will be created on the XY plane. The default is None.
- origin : topologic_core.Vertex , optional
- The origin of the grid vertices. If set to None: if the face is set, the origin will be set to vertex at the face's 0,0 paratmer. If the face is set to None, the origin will be set to (0, 0, 0). The default is None.
uRange : list , optional
A list of *u* parameters for the *u* grid lines from the uOrigin. The default is [0.0,0.25,0.5,0.75,1.0].
vRange : list , optional
diff --git a/src/topologicpy/Helper.py b/src/topologicpy/Helper.py
index 552771b..3a7726f 100644
--- a/src/topologicpy/Helper.py
+++ b/src/topologicpy/Helper.py
@@ -163,40 +163,6 @@ def onestep(cur,y,base):
iterated_list.append(y)
return iterated_list
- @staticmethod
- def K_Means(data, k=4, maxIterations=100):
- import random
- def euclidean_distance(p, q):
- return sum((pi - qi) ** 2 for pi, qi in zip(p, q)) ** 0.5
-
- # Initialize k centroids randomly
- centroids = random.sample(data, k)
-
- for _ in range(maxIterations):
- # Assign each data point to the nearest centroid
- clusters = [[] for _ in range(k)]
- for point in data:
- distances = [euclidean_distance(point, centroid) for centroid in centroids]
- nearest_centroid_index = distances.index(min(distances))
- clusters[nearest_centroid_index].append(point)
-
- # Compute the new centroids as the mean of the points in each cluster
- new_centroids = []
- for cluster in clusters:
- if not cluster:
- # If a cluster is empty, keep the previous centroid
- new_centroids.append(centroids[clusters.index(cluster)])
- else:
- new_centroids.append([sum(dim) / len(cluster) for dim in zip(*cluster)])
-
- # Check if the centroids have converged
- if new_centroids == centroids:
- break
-
- centroids = new_centroids
-
- return {'clusters': clusters, 'centroids': centroids}
-
@staticmethod
def MergeByThreshold(listA, threshold=0.0001):
"""
diff --git a/src/topologicpy/Neo4j.py b/src/topologicpy/Neo4j.py
index 5d33896..b8135bb 100644
--- a/src/topologicpy/Neo4j.py
+++ b/src/topologicpy/Neo4j.py
@@ -276,7 +276,7 @@ def randomVertex(vertices, minDistance):
return Graph.ByVerticesEdges(vertices,edges)
@staticmethod
- def AddGraph(neo4jGraph, graph, labelKey=None, relationshipKey=None, bidirectional=True, deleteAll=True, mantissa: int = 6, tolerance: float = 0.0001):
+ def AddGraph(neo4jGraph, graph, labelKey=None, relationshipKey=None, mantissa: int = 6, tolerance: float = 0.0001):
"""
Adds the input topologic graph to the input neo4j graph
@@ -286,8 +286,10 @@ def AddGraph(neo4jGraph, graph, labelKey=None, relationshipKey=None, bidirection
The input neo4j graph.
graph : topologic_core.Graph
The input topologic graph.
- categoryKey : str
- The category key in the dictionary under which to look for the category value.
+ labelKey : str , optional
+ The label key in the dictionary under which to look for the label value.
+ relationshipKey: str , optional
+ The relationship key in the dictionary under which to look for the relationship value.
mantissa : int, optional
The desired length of the mantissa. The default is 6.
tolerance : float , optional
diff --git a/src/topologicpy/Polyskel.py b/src/topologicpy/Polyskel.py
index 1b5ad5d..79f345a 100644
--- a/src/topologicpy/Polyskel.py
+++ b/src/topologicpy/Polyskel.py
@@ -115,7 +115,11 @@ def intersect(self, other):
return None # Rays are parallel and do not intersect
# Calculate the intersection point using vector algebra
- t = (other.p - self.p).cross(other.v) / self.v.cross(other.v)
+ denom = self.v.cross(other.v)
+ if not denom == 0:
+ t = (other.p - self.p).cross(other.v) / self.v.cross(other.v)
+ else:
+ return None
if t >= 0:
return self.p + self.v * t # Intersection point
else:
diff --git a/src/topologicpy/PyG.py b/src/topologicpy/PyG.py
new file mode 100644
index 0000000..9431999
--- /dev/null
+++ b/src/topologicpy/PyG.py
@@ -0,0 +1,2067 @@
+# Copyright (C) 2024
+# Wassim Jabi
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along with
+# this program. If not, see .
+
+import os
+import copy
+import numpy as np
+import pandas as pd
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from torch_geometric.data import Data, Dataset
+from torch_geometric.loader import DataLoader
+from torch_geometric.nn import SAGEConv, global_mean_pool, global_max_pool, global_add_pool
+from torch.utils.data.sampler import SubsetRandomSampler
+from sklearn.model_selection import KFold
+from sklearn.metrics import accuracy_score
+from tqdm.auto import tqdm
+import gc
+
+
+class CustomGraphDataset(Dataset):
+ def __init__(self, root, node_level=False, graph_level=True, node_attr_key='feat',
+ edge_attr_key='feat', transform=None, pre_transform=None):
+ super(CustomGraphDataset, self).__init__(root, transform, pre_transform)
+ assert not (node_level and graph_level), "Both node_level and graph_level cannot be True at the same time"
+ assert node_level or graph_level, "Both node_level and graph_level cannot be False at the same time"
+
+ self.node_level = node_level
+ self.graph_level = graph_level
+ self.node_attr_key = node_attr_key
+ self.edge_attr_key = edge_attr_key
+
+ self.graph_df = pd.read_csv(os.path.join(root, 'graphs.csv'))
+ self.nodes_df = pd.read_csv(os.path.join(root, 'nodes.csv'))
+ self.edges_df = pd.read_csv(os.path.join(root, 'edges.csv'))
+
+ self.data_list = self.process_all()
+
+ @property
+ def raw_file_names(self):
+ return ['graphs.csv', 'nodes.csv', 'edges.csv']
+
+ def process_all(self):
+ data_list = []
+ for graph_id in self.graph_df['graph_id'].unique():
+ graph_nodes = self.nodes_df[self.nodes_df['graph_id'] == graph_id]
+ graph_edges = self.edges_df[self.edges_df['graph_id'] == graph_id]
+
+ if self.node_attr_key in graph_nodes.columns and not graph_nodes[self.node_attr_key].isnull().all():
+ x = torch.tensor(graph_nodes[self.node_attr_key].values.tolist(), dtype=torch.float)
+ if x.ndim == 1:
+ x = x.unsqueeze(1) # Ensure x has shape [num_nodes, *]
+ else:
+ x = None
+
+ edge_index = torch.tensor(graph_edges[['src_id', 'dst_id']].values.T, dtype=torch.long)
+
+ if self.edge_attr_key in graph_edges.columns and not graph_edges[self.edge_attr_key].isnull().all():
+ edge_attr = torch.tensor(graph_edges[self.edge_attr_key].values.tolist(), dtype=torch.float)
+ else:
+ edge_attr = None
+
+ if self.graph_level:
+ y = torch.tensor([self.graph_df[self.graph_df['graph_id'] == graph_id]['label'].values[0]], dtype=torch.long)
+ elif self.node_level:
+ y = torch.tensor(graph_nodes['label'].values, dtype=torch.long)
+
+ data = Data(x=x, edge_index=edge_index, y=y)
+ if edge_attr is not None:
+ data.edge_attr = edge_attr
+
+ data_list.append(data)
+
+ return data_list
+
+ def len(self):
+ return len(self.data_list)
+
+ def get(self, idx):
+ return self.data_list[idx]
+
+ def __getitem__(self, idx):
+ return self.get(idx)
+
+class _Hparams:
+ def __init__(self, model_type="ClassifierHoldout", optimizer_str="Adam", amsgrad=False, betas=(0.9, 0.999), eps=1e-6, lr=0.001, lr_decay= 0, maximize=False, rho=0.9, weight_decay=0, cv_type="Holdout", split=[0.8,0.1, 0.1], k_folds=5, hl_widths=[32], conv_layer_type='SAGEConv', pooling="AvgPooling", batch_size=32, epochs=1,
+ use_gpu=False, loss_function="Cross Entropy", input_type="graph"):
+ """
+ Parameters
+ ----------
+ cv : str
+ A string to define the method of cross-validation
+ "Holdout": Holdout
+ "K-Fold": K-Fold cross validation
+ k_folds : int
+ An int value in the range of 2 to X to define the number of k-folds for cross-validation. Default is 5.
+ split : list
+ A list of three item in the range of 0 to 1 to define the split of train,
+ validate, and test data. A default value of [0.8,0.1,0.1] means 80% of data will be
+ used for training, 10% will be used for validation, and the remaining 10% will be used for training
+ hl_widths : list
+ List of hidden neurons for each layer such as [32] will mean
+ that there is one hidden layers in the network with 32 neurons
+ optimizer : torch.optim object
+ This will be the selected optimizer from torch.optim package. By
+ default, torch.optim.Adam is selected
+ learning_rate : float
+ a step value to be used to apply the gradients by optimizer
+ batch_size : int
+ to define a set of samples to be used for training and testing in
+ each step of an epoch
+ epochs : int
+ An epoch means training the neural network with all the training data for one cycle. In an epoch, we use all of the data exactly once. A forward pass and a backward pass together are counted as one pass
+ use_GPU : use the GPU. Otherwise, use the CPU
+ input_type : str
+ selects the input_type of model such as graph, node or edge
+
+ Returns
+ -------
+ None
+
+ """
+
+ self.model_type = model_type
+ self.optimizer_str = optimizer_str
+ self.amsgrad = amsgrad
+ self.betas = betas
+ self.eps = eps
+ self.lr = lr
+ self.lr_decay = lr_decay
+ self.maximize = maximize
+ self.rho = rho
+ self.weight_decay = weight_decay
+ self.cv_type = cv_type
+ self.split = split
+ self.k_folds = k_folds
+ self.hl_widths = hl_widths
+ self.conv_layer_type = conv_layer_type
+ self.pooling = pooling
+ self.batch_size = batch_size
+ self.epochs = epochs
+ self.use_gpu = use_gpu
+ self.loss_function = loss_function
+ self.input_type = input_type
+
+class _SAGEConv(nn.Module):
+ def __init__(self, in_feats, h_feats, num_classes, pooling=None):
+ super(_SAGEConv, self).__init__()
+ assert isinstance(h_feats, list), "h_feats must be a list"
+ h_feats = [x for x in h_feats if x is not None]
+ assert len(h_feats) != 0, "h_feats is empty. unable to add hidden layers"
+ self.list_of_layers = nn.ModuleList()
+ dim = [in_feats] + h_feats
+
+ # Convolution (Hidden) Layers
+ for i in range(1, len(dim)):
+ self.list_of_layers.append(SAGEConv(dim[i-1], dim[i]))
+
+ # Final Layer
+ self.final = nn.Linear(dim[-1], num_classes)
+
+ # Pooling layer
+ if pooling is None:
+ self.pooling_layer = None
+ else:
+ if "av" in pooling.lower():
+ self.pooling_layer = global_mean_pool
+ elif "max" in pooling.lower():
+ self.pooling_layer = global_max_pool
+ elif "sum" in pooling.lower():
+ self.pooling_layer = global_add_pool
+ else:
+ raise NotImplementedError
+
+ def forward(self, data):
+ x, edge_index, batch = data.x, data.edge_index, data.batch
+ h = x
+ # Generate node features
+ for layer in self.list_of_layers:
+ h = layer(h, edge_index)
+ h = F.relu(h)
+ # h will now be a matrix of dimension [num_nodes, h_feats[-1]]
+ h = self.final(h)
+ # Go from node-level features to graph-level features by pooling
+ if self.pooling_layer:
+ h = self.pooling_layer(h, batch)
+ # h will now be a vector of dimension [num_classes]
+ return h
+
+class _GraphRegressorHoldout:
+ def __init__(self, hparams, trainingDataset, validationDataset=None, testingDataset=None):
+ self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
+ self.trainingDataset = trainingDataset
+ self.validationDataset = validationDataset
+ self.testingDataset = testingDataset
+ self.hparams = hparams
+ if hparams.conv_layer_type.lower() == 'sageconv':
+ self.model = _SAGEConv(trainingDataset[0].num_node_features, hparams.hl_widths, 1, hparams.pooling).to(self.device)
+ else:
+ raise NotImplementedError
+
+ if hparams.optimizer_str.lower() == "adadelta":
+ self.optimizer = torch.optim.Adadelta(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, rho=hparams.rho, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adagrad":
+ self.optimizer = torch.optim.Adagrad(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, lr_decay=hparams.lr_decay, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adam":
+ self.optimizer = torch.optim.Adam(self.model.parameters(), amsgrad=hparams.amsgrad, betas=hparams.betas, eps=hparams.eps,
+ lr=hparams.lr, maximize=hparams.maximize, weight_decay=hparams.weight_decay)
+
+ self.use_gpu = hparams.use_gpu
+ self.training_loss_list = []
+ self.validation_loss_list = []
+ self.node_attr_key = trainingDataset[0].x.shape[1]
+
+ # Train, validate, test split
+ num_train = int(len(trainingDataset) * hparams.split[0])
+ num_validate = int(len(trainingDataset) * hparams.split[1])
+ num_test = len(trainingDataset) - num_train - num_validate
+ idx = torch.randperm(len(trainingDataset))
+ train_sampler = SubsetRandomSampler(idx[:num_train])
+ validate_sampler = SubsetRandomSampler(idx[num_train:num_train+num_validate])
+ test_sampler = SubsetRandomSampler(idx[num_train+num_validate:])
+
+ if validationDataset:
+ self.train_dataloader = DataLoader(trainingDataset,
+ batch_size=hparams.batch_size,
+ drop_last=False)
+ self.validate_dataloader = DataLoader(validationDataset,
+ batch_size=hparams.batch_size,
+ drop_last=False)
+ else:
+ self.train_dataloader = DataLoader(trainingDataset, sampler=train_sampler,
+ batch_size=hparams.batch_size,
+ drop_last=False)
+ self.validate_dataloader = DataLoader(trainingDataset, sampler=validate_sampler,
+ batch_size=hparams.batch_size,
+ drop_last=False)
+
+ if testingDataset:
+ self.test_dataloader = DataLoader(testingDataset,
+ batch_size=len(testingDataset),
+ drop_last=False)
+ else:
+ self.test_dataloader = DataLoader(trainingDataset, sampler=test_sampler,
+ batch_size=hparams.batch_size,
+ drop_last=False)
+
+ def train(self):
+ # Init the loss and accuracy reporting lists
+ self.training_loss_list = []
+ self.validation_loss_list = []
+
+ # Run the training loop for defined number of epochs
+ for _ in tqdm(range(self.hparams.epochs), desc='Epochs', total=self.hparams.epochs, leave=False):
+ # Iterate over the DataLoader for training data
+ for data in tqdm(self.train_dataloader, desc='Training', leave=False):
+ data = data.to(self.device)
+ # Make sure the model is in training mode
+ self.model.train()
+ # Zero the gradients
+ self.optimizer.zero_grad()
+
+ # Perform forward pass
+ pred = self.model(data).to(self.device)
+ # Compute loss
+ loss = F.mse_loss(torch.flatten(pred), data.y.float())
+
+ # Perform backward pass
+ loss.backward()
+
+ # Perform optimization
+ self.optimizer.step()
+
+ self.training_loss_list.append(torch.sqrt(loss).item())
+ self.validate()
+ self.validation_loss_list.append(torch.sqrt(self.validation_loss).item())
+ gc.collect()
+
+ def validate(self):
+ self.model.eval()
+ for data in tqdm(self.validate_dataloader, desc='Validating', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data).to(self.device)
+ loss = F.mse_loss(torch.flatten(pred), data.y.float())
+ self.validation_loss = loss
+
+ def test(self):
+ self.model.eval()
+ for data in tqdm(self.test_dataloader, desc='Testing', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data).to(self.device)
+ loss = F.mse_loss(torch.flatten(pred), data.y.float())
+ self.testing_loss = torch.sqrt(loss).item()
+
+ def save(self, path):
+ if path:
+ # Make sure the file extension is .pt
+ ext = path[-3:]
+ if ext.lower() != ".pt":
+ path = path + ".pt"
+ torch.save(self.model.state_dict(), path)
+
+ def load(self, path):
+ #self.model.load_state_dict(torch.load(path))
+ self.model.load_state_dict(torch.load(path, weights_only=True, map_location=self.device))
+
+class _GraphRegressorKFold:
+ def __init__(self, hparams, trainingDataset, testingDataset=None):
+ self.trainingDataset = trainingDataset
+ self.testingDataset = testingDataset
+ self.hparams = hparams
+ self.losses = []
+ self.min_loss = 0
+ self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
+
+ self.model = self._initialize_model(hparams, trainingDataset)
+ self.optimizer = self._initialize_optimizer(hparams)
+
+ self.use_gpu = hparams.use_gpu
+ self.training_loss_list = []
+ self.validation_loss_list = []
+ self.node_attr_key = trainingDataset.node_attr_key
+
+ # Train, validate, test split
+ num_train = int(len(trainingDataset) * hparams.split[0])
+ num_validate = int(len(trainingDataset) * hparams.split[1])
+ num_test = len(trainingDataset) - num_train - num_validate
+ idx = torch.randperm(len(trainingDataset))
+ test_sampler = SubsetRandomSampler(idx[num_train+num_validate:num_train+num_validate+num_test])
+
+ if testingDataset:
+ self.test_dataloader = DataLoader(testingDataset, batch_size=len(testingDataset), drop_last=False)
+ else:
+ self.test_dataloader = DataLoader(trainingDataset, sampler=test_sampler, batch_size=hparams.batch_size, drop_last=False)
+
+ def _initialize_model(self, hparams, dataset):
+ if hparams.conv_layer_type.lower() == 'sageconv':
+ return _SAGEConv(dataset.num_node_features, hparams.hl_widths, 1, hparams.pooling).to(self.device)
+ else:
+ raise NotImplementedError
+
+ def _initialize_optimizer(self, hparams):
+ if hparams.optimizer_str.lower() == "adadelta":
+ return torch.optim.Adadelta(self.model.parameters(), eps=hparams.eps, lr=hparams.lr, rho=hparams.rho, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adagrad":
+ return torch.optim.Adagrad(self.model.parameters(), eps=hparams.eps, lr=hparams.lr, lr_decay=hparams.lr_decay, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adam":
+ return torch.optim.Adam(self.model.parameters(), amsgrad=hparams.amsgrad, betas=hparams.betas, eps=hparams.eps, lr=hparams.lr, maximize=hparams.maximize, weight_decay=hparams.weight_decay)
+
+ def reset_weights(self):
+ self.model = self._initialize_model(self.hparams, self.trainingDataset)
+ self.optimizer = self._initialize_optimizer(self.hparams)
+
+ def train(self):
+ k_folds = self.hparams.k_folds
+ torch.manual_seed(42)
+
+ kfold = KFold(n_splits=k_folds, shuffle=True)
+ models, weights, losses, train_dataloaders, validate_dataloaders = [], [], [], [], []
+
+ for fold, (train_ids, validate_ids) in tqdm(enumerate(kfold.split(self.trainingDataset)), desc="Fold", total=k_folds, leave=False):
+ epoch_training_loss_list, epoch_validation_loss_list = [], []
+ train_subsampler = SubsetRandomSampler(train_ids)
+ validate_subsampler = SubsetRandomSampler(validate_ids)
+
+ self.train_dataloader = DataLoader(self.trainingDataset, sampler=train_subsampler, batch_size=self.hparams.batch_size, drop_last=False)
+ self.validate_dataloader = DataLoader(self.trainingDataset, sampler=validate_subsampler, batch_size=self.hparams.batch_size, drop_last=False)
+
+ self.reset_weights()
+ best_rmse = np.inf
+
+ for _ in tqdm(range(self.hparams.epochs), desc='Epochs', total=self.hparams.epochs, leave=False):
+ for batched_graph in tqdm(self.train_dataloader, desc='Training', leave=False):
+ self.model.train()
+ self.optimizer.zero_grad()
+
+ batched_graph = batched_graph.to(self.device)
+ pred = self.model(batched_graph)
+ loss = F.mse_loss(torch.flatten(pred), batched_graph.y.float())
+ loss.backward()
+ self.optimizer.step()
+
+ epoch_training_loss_list.append(torch.sqrt(loss).item())
+ self.validate()
+ epoch_validation_loss_list.append(torch.sqrt(self.validation_loss).item())
+ gc.collect()
+
+ models.append(self.model)
+ weights.append(copy.deepcopy(self.model.state_dict()))
+ losses.append(torch.sqrt(self.validation_loss).item())
+ train_dataloaders.append(self.train_dataloader)
+ validate_dataloaders.append(self.validate_dataloader)
+ self.training_loss_list.append(epoch_training_loss_list)
+ self.validation_loss_list.append(epoch_validation_loss_list)
+
+ self.losses = losses
+ self.min_loss = min(losses)
+ ind = losses.index(self.min_loss)
+ self.model = models[ind]
+ self.model.load_state_dict(weights[ind])
+ self.model.eval()
+ self.training_loss_list = self.training_loss_list[ind]
+ self.validation_loss_list = self.validation_loss_list[ind]
+
+ def validate(self):
+ self.model.eval()
+ for batched_graph in tqdm(self.validate_dataloader, desc='Validating', leave=False):
+ batched_graph = batched_graph.to(self.device)
+ pred = self.model(batched_graph)
+ loss = F.mse_loss(torch.flatten(pred), batched_graph.y.float())
+ self.validation_loss = loss
+
+ def test(self):
+ self.model.eval()
+ for batched_graph in tqdm(self.test_dataloader, desc='Testing', leave=False):
+ batched_graph = batched_graph.to(self.device)
+ pred = self.model(batched_graph)
+ loss = F.mse_loss(torch.flatten(pred), batched_graph.y.float())
+ self.testing_loss = torch.sqrt(loss).item()
+
+ def save(self, path):
+ if path:
+ ext = path[-3:]
+ if ext.lower() != ".pt":
+ path = path + ".pt"
+ torch.save(self.model.state_dict(), path)
+
+ def load(self, path):
+ self.model.load_state_dict(torch.load(path, weights_only=True, map_location=self.device))
+
+class _GraphClassifierKFold:
+ def __init__(self, hparams, trainingDataset, testingDataset=None):
+ self.trainingDataset = trainingDataset
+ self.testingDataset = testingDataset
+ self.hparams = hparams
+ self.testing_accuracy = 0
+ self.accuracies = []
+ self.max_accuracy = 0
+ self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
+
+ if hparams.conv_layer_type.lower() == 'sageconv':
+ self.model = _SAGEConv(trainingDataset.num_node_features, hparams.hl_widths,
+ trainingDataset.num_classes, hparams.pooling).to(self.device)
+ else:
+ raise NotImplementedError
+
+ if hparams.optimizer_str.lower() == "adadelta":
+ self.optimizer = torch.optim.Adadelta(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, rho=hparams.rho, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adagrad":
+ self.optimizer = torch.optim.Adagrad(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, lr_decay=hparams.lr_decay, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adam":
+ self.optimizer = torch.optim.Adam(self.model.parameters(), amsgrad=hparams.amsgrad, betas=hparams.betas, eps=hparams.eps,
+ lr=hparams.lr, maximize=hparams.maximize, weight_decay=hparams.weight_decay)
+ self.use_gpu = hparams.use_gpu
+ self.training_loss_list = []
+ self.validation_loss_list = []
+ self.training_accuracy_list = []
+ self.validation_accuracy_list = []
+
+ def reset_weights(self):
+ if self.hparams.conv_layer_type.lower() == 'sageconv':
+ self.model = _SAGEConv(self.trainingDataset.num_node_features, self.hparams.hl_widths,
+ self.trainingDataset.num_classes, self.hparams.pooling).to(self.device)
+ else:
+ raise NotImplementedError
+
+ if self.hparams.optimizer_str.lower() == "adadelta":
+ self.optimizer = torch.optim.Adadelta(self.model.parameters(), eps=self.hparams.eps,
+ lr=self.hparams.lr, rho=self.hparams.rho, weight_decay=self.hparams.weight_decay)
+ elif self.hparams.optimizer_str.lower() == "adagrad":
+ self.optimizer = torch.optim.Adagrad(self.model.parameters(), eps=self.hparams.eps,
+ lr=self.hparams.lr, lr_decay=self.hparams.lr_decay, weight_decay=self.hparams.weight_decay)
+ elif self.hparams.optimizer_str.lower() == "adam":
+ self.optimizer = torch.optim.Adam(self.model.parameters(), amsgrad=self.hparams.amsgrad, betas=self.hparams.betas, eps=self.hparams.eps,
+ lr=self.hparams.lr, maximize=self.hparams.maximize, weight_decay=self.hparams.weight_decay)
+
+ def train(self):
+ k_folds = self.hparams.k_folds
+
+ # Init the loss and accuracy reporting lists
+ self.training_accuracy_list = []
+ self.training_loss_list = []
+ self.validation_accuracy_list = []
+ self.validation_loss_list = []
+
+ # Set fixed random number seed
+ torch.manual_seed(42)
+
+ # Define the K-fold Cross Validator
+ kfold = KFold(n_splits=k_folds, shuffle=True)
+
+ models = []
+ weights = []
+ accuracies = []
+ train_dataloaders = []
+ validate_dataloaders = []
+
+ # K-fold Cross-validation model evaluation
+ for fold, (train_ids, validate_ids) in tqdm(enumerate(kfold.split(self.trainingDataset)), desc="Fold", initial=1, total=k_folds, leave=False):
+ epoch_training_loss_list = []
+ epoch_training_accuracy_list = []
+ epoch_validation_loss_list = []
+ epoch_validation_accuracy_list = []
+ # Sample elements randomly from a given list of ids, no replacement.
+ train_subsampler = torch.utils.data.SubsetRandomSampler(train_ids)
+ validate_subsampler = torch.utils.data.SubsetRandomSampler(validate_ids)
+
+ # Define data loaders for training and testing data in this fold
+ self.train_dataloader = DataLoader(self.trainingDataset, sampler=train_subsampler,
+ batch_size=self.hparams.batch_size,
+ drop_last=False)
+ self.validate_dataloader = DataLoader(self.trainingDataset, sampler=validate_subsampler,
+ batch_size=self.hparams.batch_size,
+ drop_last=False)
+ # Init the neural network
+ self.reset_weights()
+
+ # Run the training loop for defined number of epochs
+ for _ in tqdm(range(0,self.hparams.epochs), desc='Epochs', initial=1, total=self.hparams.epochs, leave=False):
+ temp_loss_list = []
+ temp_acc_list = []
+
+ # Iterate over the DataLoader for training data
+ for data in tqdm(self.train_dataloader, desc='Training', leave=False):
+ data = data.to(self.device)
+ # Make sure the model is in training mode
+ self.model.train()
+
+ # Zero the gradients
+ self.optimizer.zero_grad()
+
+ # Perform forward pass
+ pred = self.model(data)
+
+ # Compute loss
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+
+ # Save loss information for reporting
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+
+ # Perform backward pass
+ loss.backward()
+
+ # Perform optimization
+ self.optimizer.step()
+
+ epoch_training_accuracy_list.append(np.mean(temp_acc_list).item())
+ epoch_training_loss_list.append(np.mean(temp_loss_list).item())
+ self.validate()
+ epoch_validation_accuracy_list.append(self.validation_accuracy)
+ epoch_validation_loss_list.append(self.validation_loss)
+ gc.collect()
+ models.append(self.model)
+ weights.append(copy.deepcopy(self.model.state_dict()))
+ accuracies.append(self.validation_accuracy)
+ train_dataloaders.append(self.train_dataloader)
+ validate_dataloaders.append(self.validate_dataloader)
+ self.training_accuracy_list.append(epoch_training_accuracy_list)
+ self.training_loss_list.append(epoch_training_loss_list)
+ self.validation_accuracy_list.append(epoch_validation_accuracy_list)
+ self.validation_loss_list.append(epoch_validation_loss_list)
+ self.accuracies = accuracies
+ max_accuracy = max(accuracies)
+ self.max_accuracy = max_accuracy
+ ind = accuracies.index(max_accuracy)
+ self.model = models[ind]
+ self.model.load_state_dict(weights[ind])
+ self.model.eval()
+ self.training_accuracy_list = self.training_accuracy_list[ind]
+ self.training_loss_list = self.training_loss_list[ind]
+ self.validation_accuracy_list = self.validation_accuracy_list[ind]
+ self.validation_loss_list = self.validation_loss_list[ind]
+
+ def validate(self):
+ temp_loss_list = []
+ temp_acc_list = []
+ self.model.eval()
+ for data in tqdm(self.validate_dataloader, desc='Validating', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data)
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+ self.validation_accuracy = np.mean(temp_acc_list).item()
+ self.validation_loss = np.mean(temp_loss_list).item()
+
+ def test(self):
+ if self.testingDataset:
+ self.test_dataloader = DataLoader(self.testingDataset,
+ batch_size=len(self.testingDataset),
+ drop_last=False)
+ temp_loss_list = []
+ temp_acc_list = []
+ self.model.eval()
+ for data in tqdm(self.test_dataloader, desc='Testing', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data)
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+ self.testing_accuracy = np.mean(temp_acc_list).item()
+ self.testing_loss = np.mean(temp_loss_list).item()
+
+ def save(self, path):
+ if path:
+ # Make sure the file extension is .pt
+ ext = path[-3:]
+ if ext.lower() != ".pt":
+ path = path + ".pt"
+ torch.save(self.model.state_dict(), path)
+
+ def load(self, path):
+ #self.model.load_state_dict(torch.load(path))
+ self.model.load_state_dict(torch.load(path, weights_only=True, map_location=self.device))
+
+class _GraphClassifierHoldout:
+ def __init__(self, hparams, trainingDataset, validationDataset=None, testingDataset=None):
+ self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
+ self.trainingDataset = trainingDataset
+ self.validationDataset = validationDataset
+ self.testingDataset = testingDataset
+ self.hparams = hparams
+ gclasses = trainingDataset.num_classes
+ nfeats = trainingDataset.num_node_features
+
+ if hparams.conv_layer_type.lower() == 'sageconv':
+ self.model = _SAGEConv(nfeats, hparams.hl_widths, gclasses, hparams.pooling).to(self.device)
+ else:
+ raise NotImplementedError
+
+ if hparams.optimizer_str.lower() == "adadelta":
+ self.optimizer = torch.optim.Adadelta(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, rho=hparams.rho, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adagrad":
+ self.optimizer = torch.optim.Adagrad(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, lr_decay=hparams.lr_decay, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adam":
+ self.optimizer = torch.optim.Adam(self.model.parameters(), amsgrad=hparams.amsgrad, betas=hparams.betas, eps=hparams.eps,
+ lr=hparams.lr, maximize=hparams.maximize, weight_decay=hparams.weight_decay)
+ self.use_gpu = hparams.use_gpu
+ self.training_loss_list = []
+ self.validation_loss_list = []
+ self.training_accuracy_list = []
+ self.validation_accuracy_list = []
+ self.node_attr_key = trainingDataset[0].x.shape[1]
+
+ # train, validate, test split
+ num_train = int(len(trainingDataset) * hparams.split[0])
+ num_validate = int(len(trainingDataset) * hparams.split[1])
+ num_test = len(trainingDataset) - num_train - num_validate
+ idx = torch.randperm(len(trainingDataset))
+ train_sampler = SubsetRandomSampler(idx[:num_train])
+ validate_sampler = SubsetRandomSampler(idx[num_train:num_train+num_validate])
+ test_sampler = SubsetRandomSampler(idx[num_train+num_validate:num_train+num_validate+num_test])
+
+ if validationDataset:
+ self.train_dataloader = DataLoader(trainingDataset, batch_size=hparams.batch_size, drop_last=False)
+ self.validate_dataloader = DataLoader(validationDataset, batch_size=hparams.batch_size, drop_last=False)
+ else:
+ self.train_dataloader = DataLoader(trainingDataset, sampler=train_sampler, batch_size=hparams.batch_size, drop_last=False)
+ self.validate_dataloader = DataLoader(trainingDataset, sampler=validate_sampler, batch_size=hparams.batch_size, drop_last=False)
+
+ if testingDataset:
+ self.test_dataloader = DataLoader(testingDataset, batch_size=len(testingDataset), drop_last=False)
+ else:
+ self.test_dataloader = DataLoader(trainingDataset, sampler=test_sampler, batch_size=hparams.batch_size, drop_last=False)
+
+ def train(self):
+ # Init the loss and accuracy reporting lists
+ self.training_accuracy_list = []
+ self.training_loss_list = []
+ self.validation_accuracy_list = []
+ self.validation_loss_list = []
+
+ # Run the training loop for defined number of epochs
+ for _ in tqdm(range(self.hparams.epochs), desc='Epochs', initial=1, leave=False):
+ temp_loss_list = []
+ temp_acc_list = []
+ # Make sure the model is in training mode
+ self.model.train()
+ # Iterate over the DataLoader for training data
+ for data in tqdm(self.train_dataloader, desc='Training', leave=False):
+ data = data.to(self.device)
+
+ # Zero the gradients
+ self.optimizer.zero_grad()
+
+ # Perform forward pass
+ pred = self.model(data)
+
+ # Compute loss
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+
+ # Save loss information for reporting
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+
+ # Perform backward pass
+ loss.backward()
+
+ # Perform optimization
+ self.optimizer.step()
+
+ self.training_accuracy_list.append(np.mean(temp_acc_list).item())
+ self.training_loss_list.append(np.mean(temp_loss_list).item())
+ self.validate()
+ self.validation_accuracy_list.append(self.validation_accuracy)
+ self.validation_loss_list.append(self.validation_loss)
+ gc.collect()
+
+ def validate(self):
+ temp_loss_list = []
+ temp_acc_list = []
+ self.model.eval()
+ for data in tqdm(self.validate_dataloader, desc='Validating', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data)
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+ self.validation_accuracy = np.mean(temp_acc_list).item()
+ self.validation_loss = np.mean(temp_loss_list).item()
+
+ def test(self):
+ if self.test_dataloader:
+ temp_loss_list = []
+ temp_acc_list = []
+ self.model.eval()
+ for data in tqdm(self.test_dataloader, desc='Testing', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data)
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+ self.testing_accuracy = np.mean(temp_acc_list).item()
+ self.testing_loss = np.mean(temp_loss_list).item()
+
+ def save(self, path):
+ if path:
+ # Make sure the file extension is .pt
+ ext = path[-3:]
+ if ext.lower() != ".pt":
+ path = path + ".pt"
+ torch.save(self.model.state_dict(), path)
+
+ def load(self, path):
+ #self.model.load_state_dict(torch.load(path))
+ self.model.load_state_dict(torch.load(path, weights_only=True, map_location=self.device))
+
+class _NodeClassifierHoldout:
+ def __init__(self, hparams, trainingDataset, validationDataset=None, testingDataset=None):
+ self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
+ self.trainingDataset = trainingDataset
+ self.validationDataset = validationDataset
+ self.testingDataset = testingDataset
+ self.hparams = hparams
+ gclasses = trainingDataset.num_classes
+ nfeats = trainingDataset.num_node_features
+
+ if hparams.conv_layer_type.lower() == 'sageconv':
+ # pooling is set None for Node classifier
+ self.model = _SAGEConv(nfeats, hparams.hl_widths, gclasses, None).to(self.device)
+ else:
+ raise NotImplementedError
+
+ if hparams.optimizer_str.lower() == "adadelta":
+ self.optimizer = torch.optim.Adadelta(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, rho=hparams.rho, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adagrad":
+ self.optimizer = torch.optim.Adagrad(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, lr_decay=hparams.lr_decay, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adam":
+ self.optimizer = torch.optim.Adam(self.model.parameters(), amsgrad=hparams.amsgrad, betas=hparams.betas, eps=hparams.eps,
+ lr=hparams.lr, maximize=hparams.maximize, weight_decay=hparams.weight_decay)
+ self.use_gpu = hparams.use_gpu
+ self.training_loss_list = []
+ self.validation_loss_list = []
+ self.training_accuracy_list = []
+ self.validation_accuracy_list = []
+ self.node_attr_key = trainingDataset[0].x.shape[1]
+
+ # train, validate, test split
+ num_train = int(len(trainingDataset) * hparams.split[0])
+ num_validate = int(len(trainingDataset) * hparams.split[1])
+ num_test = len(trainingDataset) - num_train - num_validate
+ idx = torch.randperm(len(trainingDataset))
+ train_sampler = SubsetRandomSampler(idx[:num_train])
+ validate_sampler = SubsetRandomSampler(idx[num_train:num_train+num_validate])
+ test_sampler = SubsetRandomSampler(idx[num_train+num_validate:num_train+num_validate+num_test])
+
+ if validationDataset:
+ self.train_dataloader = DataLoader(trainingDataset, batch_size=hparams.batch_size, drop_last=False)
+ self.validate_dataloader = DataLoader(validationDataset, batch_size=hparams.batch_size, drop_last=False)
+ else:
+ self.train_dataloader = DataLoader(trainingDataset, sampler=train_sampler, batch_size=hparams.batch_size, drop_last=False)
+ self.validate_dataloader = DataLoader(trainingDataset, sampler=validate_sampler, batch_size=hparams.batch_size, drop_last=False)
+
+ if testingDataset:
+ self.test_dataloader = DataLoader(testingDataset, batch_size=len(testingDataset), drop_last=False)
+ else:
+ self.test_dataloader = DataLoader(trainingDataset, sampler=test_sampler, batch_size=hparams.batch_size, drop_last=False)
+
+ def train(self):
+ # Init the loss and accuracy reporting lists
+ self.training_accuracy_list = []
+ self.training_loss_list = []
+ self.validation_accuracy_list = []
+ self.validation_loss_list = []
+
+ # Run the training loop for defined number of epochs
+ for _ in tqdm(range(self.hparams.epochs), desc='Epochs', initial=1, leave=False):
+ temp_loss_list = []
+ temp_acc_list = []
+ # Iterate over the DataLoader for training data
+ for data in tqdm(self.train_dataloader, desc='Training', leave=False):
+ data = data.to(self.device)
+ # Make sure the model is in training mode
+ self.model.train()
+
+ # Zero the gradients
+ self.optimizer.zero_grad()
+
+ # Perform forward pass
+ pred = self.model(data)
+
+ # Compute loss
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+
+ # Save loss information for reporting
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+
+ # Perform backward pass
+ loss.backward()
+
+ # Perform optimization
+ self.optimizer.step()
+
+ self.training_accuracy_list.append(np.mean(temp_acc_list).item())
+ self.training_loss_list.append(np.mean(temp_loss_list).item())
+ self.validate()
+ self.validation_accuracy_list.append(self.validation_accuracy)
+ self.validation_loss_list.append(self.validation_loss)
+ gc.collect()
+
+ def validate(self):
+ temp_loss_list = []
+ temp_acc_list = []
+ self.model.eval()
+ for data in tqdm(self.validate_dataloader, desc='Validating', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data)
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+ self.validation_accuracy = np.mean(temp_acc_list).item()
+ self.validation_loss = np.mean(temp_loss_list).item()
+
+ def test(self):
+ if self.test_dataloader:
+ temp_loss_list = []
+ temp_acc_list = []
+ self.model.eval()
+ for data in tqdm(self.test_dataloader, desc='Testing', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data)
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+ self.testing_accuracy = np.mean(temp_acc_list).item()
+ self.testing_loss = np.mean(temp_loss_list).item()
+
+ def save(self, path):
+ if path:
+ # Make sure the file extension is .pt
+ ext = path[-3:]
+ if ext.lower() != ".pt":
+ path = path + ".pt"
+ torch.save(self.model.state_dict(), path)
+
+ def load(self, path):
+ #self.model.load_state_dict(torch.load(path))
+ self.model.load_state_dict(torch.load(path, weights_only=True, map_location=self.device))
+
+class _NodeRegressorHoldout:
+ def __init__(self, hparams, trainingDataset, validationDataset=None, testingDataset=None):
+ self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
+ self.trainingDataset = trainingDataset
+ self.validationDataset = validationDataset
+ self.testingDataset = testingDataset
+ self.hparams = hparams
+ if hparams.conv_layer_type.lower() == 'sageconv':
+ # pooling is set None for Node regressor
+ self.model = _SAGEConv(trainingDataset[0].num_node_features, hparams.hl_widths, 1, None).to(self.device)
+ else:
+ raise NotImplementedError
+
+ if hparams.optimizer_str.lower() == "adadelta":
+ self.optimizer = torch.optim.Adadelta(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, rho=hparams.rho, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adagrad":
+ self.optimizer = torch.optim.Adagrad(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, lr_decay=hparams.lr_decay, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adam":
+ self.optimizer = torch.optim.Adam(self.model.parameters(), amsgrad=hparams.amsgrad, betas=hparams.betas, eps=hparams.eps,
+ lr=hparams.lr, maximize=hparams.maximize, weight_decay=hparams.weight_decay)
+
+ self.use_gpu = hparams.use_gpu
+ self.training_loss_list = []
+ self.validation_loss_list = []
+ self.node_attr_key = trainingDataset[0].x.shape[1]
+
+ # Train, validate, test split
+ num_train = int(len(trainingDataset) * hparams.split[0])
+ num_validate = int(len(trainingDataset) * hparams.split[1])
+ num_test = len(trainingDataset) - num_train - num_validate
+ idx = torch.randperm(len(trainingDataset))
+ train_sampler = SubsetRandomSampler(idx[:num_train])
+ validate_sampler = SubsetRandomSampler(idx[num_train:num_train+num_validate])
+ test_sampler = SubsetRandomSampler(idx[num_train+num_validate:])
+
+ if validationDataset:
+ self.train_dataloader = DataLoader(trainingDataset,
+ batch_size=hparams.batch_size,
+ drop_last=False)
+ self.validate_dataloader = DataLoader(validationDataset,
+ batch_size=hparams.batch_size,
+ drop_last=False)
+ else:
+ self.train_dataloader = DataLoader(trainingDataset, sampler=train_sampler,
+ batch_size=hparams.batch_size,
+ drop_last=False)
+ self.validate_dataloader = DataLoader(trainingDataset, sampler=validate_sampler,
+ batch_size=hparams.batch_size,
+ drop_last=False)
+
+ if testingDataset:
+ self.test_dataloader = DataLoader(testingDataset,
+ batch_size=len(testingDataset),
+ drop_last=False)
+ else:
+ self.test_dataloader = DataLoader(trainingDataset, sampler=test_sampler,
+ batch_size=hparams.batch_size,
+ drop_last=False)
+
+ def train(self):
+ # Init the loss and accuracy reporting lists
+ self.training_loss_list = []
+ self.validation_loss_list = []
+
+ # Run the training loop for defined number of epochs
+ for _ in tqdm(range(self.hparams.epochs), desc='Epochs', total=self.hparams.epochs, leave=False):
+ # Iterate over the DataLoader for training data
+ for data in tqdm(self.train_dataloader, desc='Training', leave=False):
+ data = data.to(self.device)
+ # Make sure the model is in training mode
+ self.model.train()
+ # Zero the gradients
+ self.optimizer.zero_grad()
+
+ # Perform forward pass
+ pred = self.model(data).to(self.device)
+ # Compute loss
+ loss = F.mse_loss(torch.flatten(pred), data.y.float())
+
+ # Perform backward pass
+ loss.backward()
+
+ # Perform optimization
+ self.optimizer.step()
+
+ self.training_loss_list.append(torch.sqrt(loss).item())
+ self.validate()
+ self.validation_loss_list.append(torch.sqrt(self.validation_loss).item())
+ gc.collect()
+
+ def validate(self):
+ self.model.eval()
+ for data in tqdm(self.validate_dataloader, desc='Validating', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data).to(self.device)
+ loss = F.mse_loss(torch.flatten(pred), data.y.float())
+ self.validation_loss = loss
+
+ def test(self):
+ self.model.eval()
+ for data in tqdm(self.test_dataloader, desc='Testing', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data).to(self.device)
+ loss = F.mse_loss(torch.flatten(pred), data.y.float())
+ self.testing_loss = torch.sqrt(loss).item()
+
+ def save(self, path):
+ if path:
+ # Make sure the file extension is .pt
+ ext = path[-3:]
+ if ext.lower() != ".pt":
+ path = path + ".pt"
+ torch.save(self.model.state_dict(), path)
+
+ def load(self, path):
+ #self.model.load_state_dict(torch.load(path))
+ self.model.load_state_dict(torch.load(path, weights_only=True, map_location=self.device))
+
+class _NodeClassifierKFold:
+ def __init__(self, hparams, trainingDataset, testingDataset=None):
+ self.trainingDataset = trainingDataset
+ self.testingDataset = testingDataset
+ self.hparams = hparams
+ self.testing_accuracy = 0
+ self.accuracies = []
+ self.max_accuracy = 0
+ self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
+
+ if hparams.conv_layer_type.lower() == 'sageconv':
+ # pooling is set None for Node classifier
+ self.model = _SAGEConv(trainingDataset.num_node_features, hparams.hl_widths,
+ trainingDataset.num_classes, None).to(self.device)
+ else:
+ raise NotImplementedError
+
+ if hparams.optimizer_str.lower() == "adadelta":
+ self.optimizer = torch.optim.Adadelta(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, rho=hparams.rho, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adagrad":
+ self.optimizer = torch.optim.Adagrad(self.model.parameters(), eps=hparams.eps,
+ lr=hparams.lr, lr_decay=hparams.lr_decay, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adam":
+ self.optimizer = torch.optim.Adam(self.model.parameters(), amsgrad=hparams.amsgrad, betas=hparams.betas, eps=hparams.eps,
+ lr=hparams.lr, maximize=hparams.maximize, weight_decay=hparams.weight_decay)
+ self.use_gpu = hparams.use_gpu
+ self.training_loss_list = []
+ self.validation_loss_list = []
+ self.training_accuracy_list = []
+ self.validation_accuracy_list = []
+
+ def reset_weights(self):
+ if self.hparams.conv_layer_type.lower() == 'sageconv':
+ # pooling is set None for Node classifier
+ self.model = _SAGEConv(self.trainingDataset.num_node_features, self.hparams.hl_widths,
+ self.trainingDataset.num_classes, None).to(self.device)
+ else:
+ raise NotImplementedError
+
+ if self.hparams.optimizer_str.lower() == "adadelta":
+ self.optimizer = torch.optim.Adadelta(self.model.parameters(), eps=self.hparams.eps,
+ lr=self.hparams.lr, rho=self.hparams.rho, weight_decay=self.hparams.weight_decay)
+ elif self.hparams.optimizer_str.lower() == "adagrad":
+ self.optimizer = torch.optim.Adagrad(self.model.parameters(), eps=self.hparams.eps,
+ lr=self.hparams.lr, lr_decay=self.hparams.lr_decay, weight_decay=self.hparams.weight_decay)
+ elif self.hparams.optimizer_str.lower() == "adam":
+ self.optimizer = torch.optim.Adam(self.model.parameters(), amsgrad=self.hparams.amsgrad, betas=self.hparams.betas, eps=self.hparams.eps,
+ lr=self.hparams.lr, maximize=self.hparams.maximize, weight_decay=self.hparams.weight_decay)
+
+ def train(self):
+ k_folds = self.hparams.k_folds
+
+ # Init the loss and accuracy reporting lists
+ self.training_accuracy_list = []
+ self.training_loss_list = []
+ self.validation_accuracy_list = []
+ self.validation_loss_list = []
+
+ # Set fixed random number seed
+ torch.manual_seed(42)
+
+ # Define the K-fold Cross Validator
+ kfold = KFold(n_splits=k_folds, shuffle=True)
+
+ models = []
+ weights = []
+ accuracies = []
+ train_dataloaders = []
+ validate_dataloaders = []
+
+ # K-fold Cross-validation model evaluation
+ for fold, (train_ids, validate_ids) in tqdm(enumerate(kfold.split(self.trainingDataset)), desc="Fold", initial=1, total=k_folds, leave=False):
+ epoch_training_loss_list = []
+ epoch_training_accuracy_list = []
+ epoch_validation_loss_list = []
+ epoch_validation_accuracy_list = []
+ # Sample elements randomly from a given list of ids, no replacement.
+ train_subsampler = torch.utils.data.SubsetRandomSampler(train_ids)
+ validate_subsampler = torch.utils.data.SubsetRandomSampler(validate_ids)
+
+ # Define data loaders for training and testing data in this fold
+ self.train_dataloader = DataLoader(self.trainingDataset, sampler=train_subsampler,
+ batch_size=self.hparams.batch_size,
+ drop_last=False)
+ self.validate_dataloader = DataLoader(self.trainingDataset, sampler=validate_subsampler,
+ batch_size=self.hparams.batch_size,
+ drop_last=False)
+ # Init the neural network
+ self.reset_weights()
+
+ # Run the training loop for defined number of epochs
+ for _ in tqdm(range(0,self.hparams.epochs), desc='Epochs', initial=1, total=self.hparams.epochs, leave=False):
+ temp_loss_list = []
+ temp_acc_list = []
+
+ # Iterate over the DataLoader for training data
+ for data in tqdm(self.train_dataloader, desc='Training', leave=False):
+ data = data.to(self.device)
+ # Make sure the model is in training mode
+ self.model.train()
+
+ # Zero the gradients
+ self.optimizer.zero_grad()
+
+ # Perform forward pass
+ pred = self.model(data)
+
+ # Compute loss
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+
+ # Save loss information for reporting
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+
+ # Perform backward pass
+ loss.backward()
+
+ # Perform optimization
+ self.optimizer.step()
+
+ epoch_training_accuracy_list.append(np.mean(temp_acc_list).item())
+ epoch_training_loss_list.append(np.mean(temp_loss_list).item())
+ self.validate()
+ epoch_validation_accuracy_list.append(self.validation_accuracy)
+ epoch_validation_loss_list.append(self.validation_loss)
+ gc.collect()
+ models.append(self.model)
+ weights.append(copy.deepcopy(self.model.state_dict()))
+ accuracies.append(self.validation_accuracy)
+ train_dataloaders.append(self.train_dataloader)
+ validate_dataloaders.append(self.validate_dataloader)
+ self.training_accuracy_list.append(epoch_training_accuracy_list)
+ self.training_loss_list.append(epoch_training_loss_list)
+ self.validation_accuracy_list.append(epoch_validation_accuracy_list)
+ self.validation_loss_list.append(epoch_validation_loss_list)
+ self.accuracies = accuracies
+ max_accuracy = max(accuracies)
+ self.max_accuracy = max_accuracy
+ ind = accuracies.index(max_accuracy)
+ self.model = models[ind]
+ self.model.load_state_dict(weights[ind])
+ self.model.eval()
+ self.training_accuracy_list = self.training_accuracy_list[ind]
+ self.training_loss_list = self.training_loss_list[ind]
+ self.validation_accuracy_list = self.validation_accuracy_list[ind]
+ self.validation_loss_list = self.validation_loss_list[ind]
+
+ def validate(self):
+ temp_loss_list = []
+ temp_acc_list = []
+ self.model.eval()
+ for data in tqdm(self.validate_dataloader, desc='Validating', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data)
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+ self.validation_accuracy = np.mean(temp_acc_list).item()
+ self.validation_loss = np.mean(temp_loss_list).item()
+
+ def test(self):
+ if self.testingDataset:
+ self.test_dataloader = DataLoader(self.testingDataset,
+ batch_size=len(self.testingDataset),
+ drop_last=False)
+ temp_loss_list = []
+ temp_acc_list = []
+ self.model.eval()
+ for data in tqdm(self.test_dataloader, desc='Testing', leave=False):
+ data = data.to(self.device)
+ pred = self.model(data)
+ if self.hparams.loss_function.lower() == "negative log likelihood":
+ logp = F.log_softmax(pred, 1)
+ loss = F.nll_loss(logp, data.y)
+ elif self.hparams.loss_function.lower() == "cross entropy":
+ loss = F.cross_entropy(pred, data.y)
+ temp_loss_list.append(loss.item())
+ temp_acc_list.append(accuracy_score(data.y.cpu(), pred.argmax(1).cpu()))
+ self.testing_accuracy = np.mean(temp_acc_list).item()
+ self.testing_loss = np.mean(temp_loss_list).item()
+
+ def save(self, path):
+ if path:
+ # Make sure the file extension is .pt
+ ext = path[-3:]
+ if ext.lower() != ".pt":
+ path = path + ".pt"
+ torch.save(self.model.state_dict(), path)
+
+ def load(self, path):
+ #self.model.load_state_dict(torch.load(path))
+ self.model.load_state_dict(torch.load(path, weights_only=True, map_location=self.device))
+
+class _NodeRegressorKFold:
+ def __init__(self, hparams, trainingDataset, testingDataset=None):
+ self.trainingDataset = trainingDataset
+ self.testingDataset = testingDataset
+ self.hparams = hparams
+ self.losses = []
+ self.min_loss = 0
+ self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
+
+ self.model = self._initialize_model(hparams, trainingDataset)
+ self.optimizer = self._initialize_optimizer(hparams)
+
+ self.use_gpu = hparams.use_gpu
+ self.training_loss_list = []
+ self.validation_loss_list = []
+ self.node_attr_key = trainingDataset.node_attr_key
+
+ # Train, validate, test split
+ num_train = int(len(trainingDataset) * hparams.split[0])
+ num_validate = int(len(trainingDataset) * hparams.split[1])
+ num_test = len(trainingDataset) - num_train - num_validate
+ idx = torch.randperm(len(trainingDataset))
+ test_sampler = SubsetRandomSampler(idx[num_train+num_validate:num_train+num_validate+num_test])
+
+ if testingDataset:
+ self.test_dataloader = DataLoader(testingDataset, batch_size=len(testingDataset), drop_last=False)
+ else:
+ self.test_dataloader = DataLoader(trainingDataset, sampler=test_sampler, batch_size=hparams.batch_size, drop_last=False)
+
+ def _initialize_model(self, hparams, dataset):
+ if hparams.conv_layer_type.lower() == 'sageconv':
+ # pooling is set None for Node
+ return _SAGEConv(dataset.num_node_features, hparams.hl_widths, 1, None).to(self.device)
+ else:
+ raise NotImplementedError
+
+ def _initialize_optimizer(self, hparams):
+ if hparams.optimizer_str.lower() == "adadelta":
+ return torch.optim.Adadelta(self.model.parameters(), eps=hparams.eps, lr=hparams.lr, rho=hparams.rho, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adagrad":
+ return torch.optim.Adagrad(self.model.parameters(), eps=hparams.eps, lr=hparams.lr, lr_decay=hparams.lr_decay, weight_decay=hparams.weight_decay)
+ elif hparams.optimizer_str.lower() == "adam":
+ return torch.optim.Adam(self.model.parameters(), amsgrad=hparams.amsgrad, betas=hparams.betas, eps=hparams.eps, lr=hparams.lr, maximize=hparams.maximize, weight_decay=hparams.weight_decay)
+
+ def reset_weights(self):
+ self.model = self._initialize_model(self.hparams, self.trainingDataset)
+ self.optimizer = self._initialize_optimizer(self.hparams)
+
+ def train(self):
+ k_folds = self.hparams.k_folds
+ torch.manual_seed(42)
+
+ kfold = KFold(n_splits=k_folds, shuffle=True)
+ models, weights, losses, train_dataloaders, validate_dataloaders = [], [], [], [], []
+
+ for fold, (train_ids, validate_ids) in tqdm(enumerate(kfold.split(self.trainingDataset)), desc="Fold", total=k_folds, leave=False):
+ epoch_training_loss_list, epoch_validation_loss_list = [], []
+ train_subsampler = SubsetRandomSampler(train_ids)
+ validate_subsampler = SubsetRandomSampler(validate_ids)
+
+ self.train_dataloader = DataLoader(self.trainingDataset, sampler=train_subsampler, batch_size=self.hparams.batch_size, drop_last=False)
+ self.validate_dataloader = DataLoader(self.trainingDataset, sampler=validate_subsampler, batch_size=self.hparams.batch_size, drop_last=False)
+
+ self.reset_weights()
+ best_rmse = np.inf
+
+ for _ in tqdm(range(self.hparams.epochs), desc='Epochs', total=self.hparams.epochs, leave=False):
+ for batched_graph in tqdm(self.train_dataloader, desc='Training', leave=False):
+ self.model.train()
+ self.optimizer.zero_grad()
+
+ batched_graph = batched_graph.to(self.device)
+ pred = self.model(batched_graph)
+ loss = F.mse_loss(torch.flatten(pred), batched_graph.y.float())
+ loss.backward()
+ self.optimizer.step()
+
+ epoch_training_loss_list.append(torch.sqrt(loss).item())
+ self.validate()
+ epoch_validation_loss_list.append(torch.sqrt(self.validation_loss).item())
+ gc.collect()
+
+ models.append(self.model)
+ weights.append(copy.deepcopy(self.model.state_dict()))
+ losses.append(torch.sqrt(self.validation_loss).item())
+ train_dataloaders.append(self.train_dataloader)
+ validate_dataloaders.append(self.validate_dataloader)
+ self.training_loss_list.append(epoch_training_loss_list)
+ self.validation_loss_list.append(epoch_validation_loss_list)
+
+ self.losses = losses
+ self.min_loss = min(losses)
+ ind = losses.index(self.min_loss)
+ self.model = models[ind]
+ self.model.load_state_dict(weights[ind])
+ self.model.eval()
+ self.training_loss_list = self.training_loss_list[ind]
+ self.validation_loss_list = self.validation_loss_list[ind]
+
+ def validate(self):
+ self.model.eval()
+ for batched_graph in tqdm(self.validate_dataloader, desc='Validating', leave=False):
+ batched_graph = batched_graph.to(self.device)
+ pred = self.model(batched_graph)
+ loss = F.mse_loss(torch.flatten(pred), batched_graph.y.float())
+ self.validation_loss = loss
+
+ def test(self):
+ self.model.eval()
+ for batched_graph in tqdm(self.test_dataloader, desc='Testing', leave=False):
+ batched_graph = batched_graph.to(self.device)
+ pred = self.model(batched_graph)
+ loss = F.mse_loss(torch.flatten(pred), batched_graph.y.float())
+ self.testing_loss = torch.sqrt(loss).item()
+
+ def save(self, path):
+ if path:
+ ext = path[-3:]
+ if ext.lower() != ".pt":
+ path = path + ".pt"
+ torch.save(self.model.state_dict(), path)
+
+ def load(self, path):
+ self.model.load_state_dict(torch.load(path, weights_only=True, map_location=self.device))
+
+class PyG:
+ @staticmethod
+ def DatasetByCSVPath(path, numberOfGraphClasses=0, nodeATTRKey='feat', edgeATTRKey='feat', nodeOneHotEncode=False,
+ nodeFeaturesCategories=[], edgeOneHotEncode=False, edgeFeaturesCategories=[], addSelfLoop=False,
+ node_level=False, graph_level=True):
+ """
+ Returns PyTorch Geometric dataset according to the input CSV folder path. The folder must contain "graphs.csv",
+ "edges.csv", "nodes.csv", and "meta.yml" files according to conventions.
+
+ Parameters
+ ----------
+ path : str
+ The path to the folder containing the necessary CSV and YML files.
+
+ Returns
+ -------
+ PyG Dataset
+ The PyG dataset
+ """
+ if not isinstance(path, str):
+ print("PyG.DatasetByCSVPath - Error: The input path parameter is not a valid string. Returning None.")
+ return None
+ if not os.path.exists(path):
+ print("PyG.DatasetByCSVPath - Error: The input path parameter does not exist. Returning None.")
+ return None
+
+ return CustomGraphDataset(root=path, node_level=node_level, graph_level=graph_level, node_attr_key=nodeATTRKey, edge_attr_key=edgeATTRKey)
+
+ @staticmethod
+ def Optimizer(name="Adam", amsgrad=True, betas=(0.9,0.999), eps=0.000001, lr=0.001, maximize=False, weightDecay=0.0, rho=0.9, lr_decay=0.0):
+ """
+ Returns the parameters of the optimizer
+
+ Parameters
+ ----------
+ amsgrad : bool , optional.
+ amsgrad is an extension to the Adam version of gradient descent that attempts to improve the convergence properties of the algorithm, avoiding large abrupt changes in the learning rate for each input variable. The default is True.
+ betas : tuple , optional
+ Betas are used as for smoothing the path to the convergence also providing some momentum to cross a local minima or saddle point. The default is (0.9, 0.999).
+ eps : float . optional.
+ eps is a term added to the denominator to improve numerical stability. The default is 0.000001.
+ lr : float
+ The learning rate (lr) defines the adjustment in the weights of our network with respect to the loss gradient descent. The default is 0.001.
+ maximize : float , optional
+ maximize the params based on the objective, instead of minimizing. The default is False.
+ weightDecay : float , optional
+ weightDecay (L2 penalty) is a regularization technique applied to the weights of a neural network. The default is 0.0.
+
+ Returns
+ -------
+ dict
+ The dictionary of the optimizer parameters. The dictionary contains the following keys and values:
+ - "name" (str): The name of the optimizer
+ - "amsgrad" (bool):
+ - "betas" (tuple):
+ - "eps" (float):
+ - "lr" (float):
+ - "maximize" (bool):
+ - weightDecay (float):
+
+ """
+ return {"name":name, "amsgrad":amsgrad, "betas":betas, "eps":eps, "lr": lr, "maximize":maximize, "weight_decay":weightDecay, "rho":rho, "lr_decay":lr_decay}
+
+ @staticmethod
+ def Hyperparameters(optimizer, model_type="classifier", cv_type="Holdout", split=[0.8,0.1,0.1], k_folds=5,
+ hl_widths=[32], conv_layer_type="SAGEConv", pooling="AvgPooling",
+ batch_size=1, epochs=1, use_gpu=False, loss_function="Cross Entropy",
+ input_type="graph"):
+ """
+ Creates a hyperparameters object based on the input settings.
+
+ Parameters
+ ----------
+ model_type : str , optional
+ The desired type of model. The options are:
+ - "Classifier"
+ - "Regressor"
+ The option is case insensitive. The default is "classifierholdout"
+ optimizer : Optimizer
+ The desired optimizer.
+ cv_type : str , optional
+ The desired cross-validation method. This can be "Holdout" or "K-Fold". It is case-insensitive. The default is "Holdout".
+ split : list , optional
+ The desired split between training validation, and testing. [0.8, 0.1, 0.1] means that 80% of the data is used for training 10% of the data is used for validation, and 10% is used for testing. The default is [0.8, 0.1, 0.1].
+ k_folds : int , optional
+ The desired number of k-folds. The default is 5.
+ hl_widths : list , optional
+ The list of hidden layer widths. A list of [16, 32, 16] means that the model will have 3 hidden layers with number of neurons in each being 16, 32, 16 respectively from input to output. The default is [32].
+ conv_layer_type : str , optional
+ The desired type of the convolution layer. The options are "Classic", "GraphConv", "GINConv", "SAGEConv", "TAGConv", "DGN". It is case insensitive. The default is "SAGEConv".
+ pooling : str , optional
+ The desired type of pooling. The options are "AvgPooling", "MaxPooling", or "SumPooling". It is case insensitive. The default is "AvgPooling".
+ batch_size : int , optional
+ The desired batch size. The default is 1.
+ epochs : int , optional
+ The desired number of epochs. The default is 1.
+ use_gpu : bool , optional
+ If set to True, the model will attempt to use the GPU. The default is False.
+ loss_function : str , optional
+ The desired loss function. The options are "Cross-Entropy" or "Negative Log Likelihood". It is case insensitive. The default is "Cross-Entropy".
+ input_type : str
+ selects the input_type of model such as graph, node or edge
+ Returns
+ -------
+ Hyperparameters
+ The created hyperparameters object.
+
+ """
+
+ if optimizer['name'].lower() == "adadelta":
+ optimizer_str = "Adadelta"
+ elif optimizer['name'].lower() == "adagrad":
+ optimizer_str = "Adagrad"
+ elif optimizer['name'].lower() == "adam":
+ optimizer_str = "Adam"
+ return _Hparams(model_type,
+ optimizer_str,
+ optimizer['amsgrad'],
+ optimizer['betas'],
+ optimizer['eps'],
+ optimizer['lr'],
+ optimizer['lr_decay'],
+ optimizer['maximize'],
+ optimizer['rho'],
+ optimizer['weight_decay'],
+ cv_type,
+ split,
+ k_folds,
+ hl_widths,
+ conv_layer_type,
+ pooling,
+ batch_size,
+ epochs,
+ use_gpu,
+ loss_function,
+ input_type)
+
+ @staticmethod
+ def Model(hparams, trainingDataset, validationDataset=None, testingDataset=None):
+ """
+ Creates a neural network classifier.
+
+ Parameters
+ ----------
+ hparams : HParams
+ The input hyperparameters
+ trainingDataset : DGLDataset
+ The input training dataset.
+ validationDataset : DGLDataset
+ The input validation dataset. If not specified, a portion of the trainingDataset will be used for validation according the to the split list as specified in the hyper-parameters.
+ testingDataset : DGLDataset
+ The input testing dataset. If not specified, a portion of the trainingDataset will be used for testing according the to the split list as specified in the hyper-parameters.
+
+ Returns
+ -------
+ Classifier
+ The created classifier
+
+ """
+
+ model = None
+ if hparams.model_type.lower() == "classifier":
+ if hparams.input_type == 'graph':
+ if hparams.cv_type.lower() == "holdout":
+ model = _GraphClassifierHoldout(hparams=hparams, trainingDataset=trainingDataset, validationDataset=validationDataset, testingDataset=testingDataset)
+ elif "k" in hparams.cv_type.lower():
+ model = _GraphClassifierKFold(hparams=hparams, trainingDataset=trainingDataset, testingDataset=testingDataset)
+ elif hparams.input_type == 'node':
+ if hparams.cv_type.lower() == "holdout":
+ model = _NodeClassifierHoldout(hparams=hparams, trainingDataset=trainingDataset, validationDataset=validationDataset, testingDataset=testingDataset)
+ elif "k" in hparams.cv_type.lower():
+ model = _NodeClassifierKFold(hparams=hparams, trainingDataset=trainingDataset, testingDataset=testingDataset)
+ elif hparams.model_type.lower() == "regressor":
+ if hparams.input_type == 'graph':
+ if hparams.cv_type.lower() == "holdout":
+ model = _GraphRegressorHoldout(hparams=hparams, trainingDataset=trainingDataset, validationDataset=validationDataset, testingDataset=testingDataset)
+ elif "k" in hparams.cv_type.lower():
+ model = _GraphRegressorKFold(hparams=hparams, trainingDataset=trainingDataset, testingDataset=testingDataset)
+ elif hparams.input_type == 'node':
+ if hparams.cv_type.lower() == "holdout":
+ model = _NodeRegressorHoldout(hparams=hparams, trainingDataset=trainingDataset, validationDataset=validationDataset, testingDataset=testingDataset)
+ elif "k" in hparams.cv_type.lower():
+ model = _NodeRegressorKFold(hparams=hparams, trainingDataset=trainingDataset, testingDataset=testingDataset)
+ else:
+ raise NotImplementedError
+ return model
+
+ @staticmethod
+ def ModelTrain(model):
+ """
+ Trains the neural network model.
+
+ Parameters
+ ----------
+ model : Model
+ The input model.
+
+ Returns
+ -------
+ Model
+ The trained model
+
+ """
+ if not model:
+ return None
+ model.train()
+ return model
+
+ @staticmethod
+ def ModelTest(model):
+ """
+ Tests the neural network model.
+
+ Parameters
+ ----------
+ model : Model
+ The input model.
+
+ Returns
+ -------
+ Model
+ The tested model
+
+ """
+ if not model:
+ return None
+ model.test()
+ return model
+
+ @staticmethod
+ def ModelSave(model, path, overwrite=False):
+ """
+ Saves the model.
+
+ Parameters
+ ----------
+ model : Model
+ The input model.
+ path : str
+ The file path at which to save the model.
+ overwrite : bool, optional
+ If set to True, any existing file will be overwritten. Otherwise, it won't. The default is False.
+
+ Returns
+ -------
+ bool
+ True if the model is saved correctly. False otherwise.
+
+ """
+ import os
+
+ if model == None:
+ print("DGL.ModelSave - Error: The input model parameter is invalid. Returning None.")
+ return None
+ if path == None:
+ print("DGL.ModelSave - Error: The input path parameter is invalid. Returning None.")
+ return None
+ if not overwrite and os.path.exists(path):
+ print("DGL.ModelSave - Error: a file already exists at the specified path and overwrite is set to False. Returning None.")
+ return None
+ if overwrite and os.path.exists(path):
+ os.remove(path)
+ # Make sure the file extension is .pt
+ ext = path[len(path)-3:len(path)]
+ if ext.lower() != ".pt":
+ path = path+".pt"
+ model.save(path)
+ return True
+
+ @staticmethod
+ def ModelData(model):
+ """
+ Returns the data of the model
+
+ Parameters
+ ----------
+ model : Model
+ The input model.
+
+ Returns
+ -------
+ dict
+ A dictionary containing the model data. The keys in the dictionary are:
+ 'Model Type'
+ 'Optimizer'
+ 'CV Type'
+ 'Split'
+ 'K-Folds'
+ 'HL Widths'
+ 'Conv Layer Type'
+ 'Pooling'
+ 'Learning Rate'
+ 'Batch Size'
+ 'Epochs'
+ 'Training Accuracy'
+ 'Validation Accuracy'
+ 'Testing Accuracy'
+ 'Training Loss'
+ 'Validation Loss'
+ 'Testing Loss'
+ 'Accuracies' (Classifier and K-Fold only)
+ 'Max Accuracy' (Classifier and K-Fold only)
+ 'Losses' (Regressor and K-fold only)
+ 'min Loss' (Regressor and K-fold only)
+
+ """
+ from topologicpy.Helper import Helper
+
+ data = {'Model Type': [model.hparams.model_type],
+ 'Optimizer': [model.hparams.optimizer_str],
+ 'CV Type': [model.hparams.cv_type],
+ 'Split': model.hparams.split,
+ 'K-Folds': [model.hparams.k_folds],
+ 'HL Widths': model.hparams.hl_widths,
+ 'Conv Layer Type': [model.hparams.conv_layer_type],
+ 'Pooling': [model.hparams.pooling],
+ 'Learning Rate': [model.hparams.lr],
+ 'Batch Size': [model.hparams.batch_size],
+ 'Epochs': [model.hparams.epochs]
+ }
+
+ if model.hparams.model_type.lower() == "classifier":
+ testing_accuracy_list = [model.testing_accuracy] * model.hparams.epochs
+ try:
+ testing_loss_list = [model.testing_loss] * model.hparams.epochs
+ except:
+ testing_loss_list = [0.] * model.hparams.epochs
+ metrics_data = {
+ 'Training Accuracy': [model.training_accuracy_list],
+ 'Validation Accuracy': [model.validation_accuracy_list],
+ 'Testing Accuracy' : [testing_accuracy_list],
+ 'Training Loss': [model.training_loss_list],
+ 'Validation Loss': [model.validation_loss_list],
+ 'Testing Loss' : [testing_loss_list]
+ }
+ if model.hparams.cv_type.lower() == "k-fold":
+ accuracy_data = {
+ 'Accuracies' : [model.accuracies],
+ 'Max Accuracy' : [model.max_accuracy]
+ }
+ metrics_data.update(accuracy_data)
+ data.update(metrics_data)
+
+ elif model.hparams.model_type.lower() == "regressor":
+ testing_loss_list = [model.testing_loss] * model.hparams.epochs
+ metrics_data = {
+ 'Training Loss': [model.training_loss_list],
+ 'Validation Loss': [model.validation_loss_list],
+ 'Testing Loss' : [testing_loss_list]
+ }
+ if model.hparams.cv_type.lower() == "k-fold":
+ loss_data = {
+ 'Losses' : [model.losses],
+ 'Min Loss' : [model.min_loss]
+ }
+ metrics_data.update(loss_data)
+ data.update(metrics_data)
+
+ return data
+
+ @staticmethod
+ def Show(data,
+ labels,
+ title="Training/Validation",
+ xTitle="Epochs",
+ xSpacing=1,
+ yTitle="Accuracy and Loss",
+ ySpacing=0.1,
+ useMarkers=False,
+ chartType="Line",
+ width=950,
+ height=500,
+ backgroundColor='rgba(0,0,0,0)',
+ gridColor='lightgray',
+ marginLeft=0,
+ marginRight=0,
+ marginTop=40,
+ marginBottom=0,
+ renderer = "notebook"):
+ """
+ Shows the data in a plolty graph.
+
+ Parameters
+ ----------
+ data : list
+ The data to display.
+ labels : list
+ The labels to use for the data.
+ width : int , optional
+ The desired width of the figure. The default is 950.
+ height : int , optional
+ The desired height of the figure. The default is 500.
+ title : str , optional
+ The chart title. The default is "Training and Testing Results".
+ xTitle : str , optional
+ The X-axis title. The default is "Epochs".
+ xSpacing : float , optional
+ The X-axis spacing. The default is 1.0.
+ yTitle : str , optional
+ The Y-axis title. The default is "Accuracy and Loss".
+ ySpacing : float , optional
+ The Y-axis spacing. The default is 0.1.
+ useMarkers : bool , optional
+ If set to True, markers will be displayed. The default is False.
+ chartType : str , optional
+ The desired type of chart. The options are "Line", "Bar", or "Scatter". It is case insensitive. The default is "Line".
+ backgroundColor : str , optional
+ The desired background color. This can be any plotly color string and may be specified as:
+ - A hex string (e.g. '#ff0000')
+ - An rgb/rgba string (e.g. 'rgb(255,0,0)')
+ - An hsl/hsla string (e.g. 'hsl(0,100%,50%)')
+ - An hsv/hsva string (e.g. 'hsv(0,100%,100%)')
+ - A named CSS color.
+ The default is 'rgba(0,0,0,0)' (transparent).
+ gridColor : str , optional
+ The desired grid color. This can be any plotly color string and may be specified as:
+ - A hex string (e.g. '#ff0000')
+ - An rgb/rgba string (e.g. 'rgb(255,0,0)')
+ - An hsl/hsla string (e.g. 'hsl(0,100%,50%)')
+ - An hsv/hsva string (e.g. 'hsv(0,100%,100%)')
+ - A named CSS color.
+ The default is 'lightgray'.
+ marginLeft : int , optional
+ The desired left margin in pixels. The default is 0.
+ marginRight : int , optional
+ The desired right margin in pixels. The default is 0.
+ marginTop : int , optional
+ The desired top margin in pixels. The default is 40.
+ marginBottom : int , optional
+ The desired bottom margin in pixels. The default is 0.
+ renderer : str , optional
+ The desired plotly renderer. The default is "notebook".
+
+ Returns
+ -------
+ None.
+
+ """
+ from topologicpy.Plotly import Plotly
+
+ dataFrame = Plotly.DataByDGL(data, labels)
+ fig = Plotly.FigureByDataFrame(dataFrame,
+ labels=labels,
+ title=title,
+ xTitle=xTitle,
+ xSpacing=xSpacing,
+ yTitle=yTitle,
+ ySpacing=ySpacing,
+ useMarkers=useMarkers,
+ chartType=chartType,
+ width=width,
+ height=height,
+ backgroundColor=backgroundColor,
+ gridColor=gridColor,
+ marginRight=marginRight,
+ marginLeft=marginLeft,
+ marginTop=marginTop,
+ marginBottom=marginBottom
+ )
+ Plotly.Show(fig, renderer=renderer)
+
+ @staticmethod
+ def ModelLoad(path, model):
+ """
+ Returns the model found at the input file path.
+
+ Parameters
+ ----------
+ path : str
+ File path for the saved classifier.
+ model : torch.nn.module
+ Initialized instance of model
+
+ Returns
+ -------
+ PyG Classifier
+ The classifier.
+
+ """
+ if not path:
+ return None
+
+ model.load(path)
+ return model
+
+ @staticmethod
+ def ConfusionMatrix(actual, predicted, normalize=False):
+ """
+ Returns the confusion matrix for the input actual and predicted labels. This is to be used with classification tasks only not regression.
+
+ Parameters
+ ----------
+ actual : list
+ The input list of actual labels.
+ predicted : list
+ The input list of predicts labels.
+ normalized : bool , optional
+ If set to True, the returned data will be normalized (proportion of 1). Otherwise, actual numbers are returned. The default is False.
+
+ Returns
+ -------
+ list
+ The created confusion matrix.
+
+ """
+
+ try:
+ from sklearn import metrics
+ from sklearn.metrics import accuracy_score
+ except:
+ print("DGL - Installing required scikit-learn (sklearn) library.")
+ try:
+ os.system("pip install scikit-learn")
+ except:
+ os.system("pip install scikit-learn --user")
+ try:
+ from sklearn import metrics
+ from sklearn.metrics import accuracy_score
+ print("DGL - scikit-learn (sklearn) library installed correctly.")
+ except:
+ warnings.warn("DGL - Error: Could not import scikit-learn (sklearn). Please try to install scikit-learn manually. Returning None.")
+ return None
+
+ if not isinstance(actual, list):
+ print("DGL.ConfusionMatrix - ERROR: The actual input is not a list. Returning None")
+ return None
+ if not isinstance(predicted, list):
+ print("DGL.ConfusionMatrix - ERROR: The predicted input is not a list. Returning None")
+ return None
+ if len(actual) != len(predicted):
+ print("DGL.ConfusionMatrix - ERROR: The two input lists do not have the same length. Returning None")
+ return None
+ if normalize:
+ cm = np.transpose(metrics.confusion_matrix(y_true=actual, y_pred=predicted, normalize="true"))
+ else:
+ cm = np.transpose(metrics.confusion_matrix(y_true=actual, y_pred=predicted))
+ return cm
+
+ @staticmethod
+ def ModelPredict(model, dataset, nodeATTRKey="feat"):
+ """
+ Predicts the value of the input dataset.
+
+ Parameters
+ ----------
+ dataset : PyGDataset
+ The input PyG dataset.
+ model : Model
+ The input trained model.
+ nodeATTRKey : str , optional
+ The key used for node attributes. The default is "feat".
+
+ Returns
+ -------
+ list
+ The list of predictions
+ """
+ try:
+ model = model.model #The inoput model might be our wrapper model. In that case, get its model attribute to do the prediciton.
+ except:
+ pass
+ values = []
+ dataloader = DataLoader(dataset, batch_size=1, drop_last=False)
+ device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
+ model.eval()
+ for data in tqdm(dataloader, desc='Predicting', leave=False):
+ data = data.to(device)
+ pred = model(data)
+ values.extend(list(np.round(pred.detach().cpu().numpy().flatten(), 3)))
+ return values
+
+ @staticmethod
+ def ModelClassify(model, dataset, nodeATTRKey="feat"):
+ """
+ Predicts the classification the labels of the input dataset.
+
+ Parameters
+ ----------
+ dataset : PyGDataset
+ The input PyG dataset.
+ model : Model
+ The input trained model.
+ nodeATTRKey : str , optional
+ The key used for node attributes. The default is "feat".
+
+ Returns
+ -------
+ dict
+ Dictionary containing labels and probabilities. The included keys and values are:
+ - "predictions" (list): the list of predicted labels
+ - "probabilities" (list): the list of probabilities that the label is one of the categories.
+
+ """
+ try:
+ model = model.model #The inoput model might be our wrapper model. In that case, get its model attribute to do the prediciton.
+ except:
+ pass
+ labels = []
+ probabilities = []
+ dataloader = DataLoader(dataset, batch_size=1, drop_last=False)
+ device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
+ for data in tqdm(dataloader, desc='Classifying', leave=False):
+ data = data.to(device)
+ pred = model(data)
+ labels.extend(pred.argmax(1).tolist())
+ probability = (torch.nn.functional.softmax(pred, dim=1).tolist())
+ probability = probability[0]
+ temp_probability = []
+ for p in probability:
+ temp_probability.append(round(p, 3))
+ probabilities.extend(temp_probability)
+ return {"predictions":labels, "probabilities":probabilities}
+
+ @staticmethod
+ def Accuracy(actual, predicted, mantissa: int = 6):
+ """
+ Computes the accuracy of the input predictions based on the input labels. This is to be used only with classification not with regression.
+
+ Parameters
+ ----------
+ actual : list
+ The input list of actual values.
+ predicted : list
+ The input list of predicted values.
+ mantissa : int , optional
+ The desired length of the mantissa. The default is 6.
+
+ Returns
+ -------
+ dict
+ A dictionary returning the accuracy information. This contains the following keys and values:
+ - "accuracy" (float): The number of correct predictions divided by the length of the list.
+ - "correct" (int): The number of correct predictions
+ - "mask" (list): A boolean mask for correct vs. wrong predictions which can be used to filter the list of predictions
+ - "size" (int): The size of the predictions list
+ - "wrong" (int): The number of wrong predictions
+
+ """
+ if len(predicted) < 1 or len(actual) < 1 or not len(predicted) == len(actual):
+ return None
+ correct = 0
+ mask = []
+ for i in range(len(predicted)):
+ if predicted[i] == actual[i]:
+ correct = correct + 1
+ mask.append(True)
+ else:
+ mask.append(False)
+ size = len(predicted)
+ wrong = len(predicted)- correct
+ accuracy = round(float(correct) / float(len(predicted)), mantissa)
+ return {"accuracy":accuracy, "correct":correct, "mask":mask, "size":size, "wrong":wrong}
+
+ @staticmethod
+ def MSE(actual, predicted, mantissa: int = 6):
+ """
+ Computes the Mean Squared Error (MSE) of the input predictions based on the input labels. This is to be used with regression models.
+
+ Parameters
+ ----------
+ actual : list
+ The input list of actual values.
+ predicted : list
+ The input list of predicted values.
+ mantissa : int , optional
+ The desired length of the mantissa. The default is 6.
+
+ Returns
+ -------
+ dict
+ A dictionary returning the MSE information. This contains the following keys and values:
+ - "mse" (float): The mean squared error rounded to the specified mantissa.
+ - "size" (int): The size of the predictions list.
+ """
+ if len(predicted) < 1 or len(actual) < 1 or not len(predicted) == len(actual):
+ return None
+
+ mse = np.mean((np.array(predicted) - np.array(actual)) ** 2)
+ mse = round(mse, mantissa)
+ size = len(predicted)
+
+ return {"mse": mse, "size": size}
diff --git a/src/topologicpy/Shell.py b/src/topologicpy/Shell.py
index 2920b7b..47602a3 100644
--- a/src/topologicpy/Shell.py
+++ b/src/topologicpy/Shell.py
@@ -349,8 +349,8 @@ def ByThickenedWire(wire, offsetA: float = 1.0, offsetB: float = 1.0, tolerance:
Returns
-------
- topologic_core.Cell
- The created cell.
+ topologic_core.Shell
+ The created shell.
"""
from topologicpy.Edge import Edge
diff --git a/src/topologicpy/Topology.py b/src/topologicpy/Topology.py
index e02d2bc..fa8718b 100644
--- a/src/topologicpy/Topology.py
+++ b/src/topologicpy/Topology.py
@@ -1594,12 +1594,6 @@ def ByBIMPath(path, guidKey: str = "guid", colorKey: str = "color", typeKey: str
----------
path :str
The path to the .bim file.
- path : str
- The input file path.
- overwrite : bool , optional
- If set to True the output file will overwrite any pre-existing file. Otherwise, it won't. The default is False.
- version : str , optional
- The desired version number for the BIM file. The default is "1.0.0".
guidKey : str , optional
The key to use to store the the guid of the topology. The default is "guid".
colorKey : str , optional
@@ -1658,12 +1652,6 @@ def ByBIMString(string, guidKey: str = "guid", colorKey: str = "color", typeKey:
----------
string :str
The input dotbim str (in JSON format).
- path : str
- The input file path.
- overwrite : bool , optional
- If set to True the output file will overwrite any pre-existing file. Otherwise, it won't. The default is False.
- version : str , optional
- The desired version number for the BIM file. The default is "1.0.0".
guidKey : str , optional
The key to use to store the the guid of the topology. The default is "guid".
colorKey : str , optional
@@ -1754,12 +1742,6 @@ def ByBIMFile(file, guidKey: str = "guid", colorKey: str = "color", typeKey: str
----------
file : dotbimpy.file.File
The input dotbim file.
- path : str
- The input file path.
- overwrite : bool , optional
- If set to True the output file will overwrite any pre-existing file. Otherwise, it won't. The default is False.
- version : str , optional
- The desired version number for the BIM file. The default is "1.0.0".
guidKey : str , optional
The key to use to store the the guid of the topology. The default is "guid".
colorKey : str , optional
@@ -4988,8 +4970,6 @@ def OBJString(*topologies, nameKey="name", colorKey="color", opacityKey="opacity
----------
topologies : list or comma separated topologies
The input list of topologies.
- path : str
- The input file path.
nameKey : str , optional
The topology dictionary key under which to find the name of the topology. The default is "name".
colorKey : str, optional
@@ -5021,8 +5001,6 @@ def OBJString(*topologies, nameKey="name", colorKey="color", opacityKey="opacity
The desired length of the mantissa. The default is 6.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
- overwrite : bool , optional
- If set to True the ouptut file will overwrite any pre-existing file. Otherwise, it won't. The default is False.
Returns
-------
diff --git a/src/topologicpy/Wire.py b/src/topologicpy/Wire.py
index 4fd776d..d030720 100644
--- a/src/topologicpy/Wire.py
+++ b/src/topologicpy/Wire.py
@@ -602,7 +602,7 @@ def Circle(origin= None, radius: float = 0.5, sides: int = 16, fromAngle: float
radius : float , optional
The radius of the circle. The default is 0.5.
sides : int , optional
- The number of sides of the circle. The default is 16.
+ The desired number of sides of the circle. The default is 16.
fromAngle : float , optional
The angle in degrees from which to start creating the arc of the circle. The default is 0.
toAngle : float , optional
@@ -3019,19 +3019,15 @@ def Squircle(origin = None, radius: float = 0.5, sides: int = 121, a: float = 2.
origin : topologic_core.Vertex , optional
The location of the origin of the squircle. The default is None which results in the squircle being placed at (0, 0, 0).
radius : float , optional
- The radius of the squircle. The default is 0.5.
+ The desired radius of the squircle. The default is 0.5.
sides : int , optional
- The number of sides of the squircle. The default is 121.
+ The desired number of sides of the squircle. The default is 121.
a : float , optional
The "a" factor affects the x position of the points to interpolate between a circle and a square.
A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0.
b : float , optional
The "b" factor affects the y position of the points to interpolate between a circle and a square.
A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0.
- radius : float , optional
- The desired radius of the squircle. The default is 0.5.
- sides : int , optional
- The desired number of sides for the squircle. The default is 100.
direction : list , optional
The vector representing the up direction of the circle. The default is [0, 0, 1].
placement : str , optional
diff --git a/src/topologicpy/version.py b/src/topologicpy/version.py
index ab9137e..dfebef3 100644
--- a/src/topologicpy/version.py
+++ b/src/topologicpy/version.py
@@ -1 +1 @@
-__version__ = '0.7.38'
+__version__ = '0.7.39'