{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# 15. Graph Machine Learning\n\nConvert a small TopologicPy graph into PyTorch Geometric data and train one graph convolution layer for a few steps. This is a mechanics demonstration, not evidence of useful predictive performance.\n\n**Goal:** Complete one small workflow. Run the cells from top to bottom in a fresh kernel.\n\n**Extra dependencies:** CPU `torch==2.14.1` and `torch-geometric==2.8.1`. Package installation is separate from execution.\n\nAll examples use metres unless stated otherwise.\n",
   "id": "tutorial-15-00"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": "import os\nos.environ[\"TOPOLOGICPY_CORE_BACKEND\"] = \"pythonocc\"\n\nimport topologicpy\nfrom topologicpy.Core import Core\n\nassert topologicpy.__version__ == \"0.9.86\", \"Use TopologicPy 0.9.86 for these tutorials.\"\nassert type(Core.Backend()).__name__ == \"PythonOCCBackend\", \"Restart with the pythonocc backend.\"\nprint(\"TopologicPy\", topologicpy.__version__, \"—\", type(Core.Backend()).__name__)\n",
   "outputs": [],
   "execution_count": null,
   "id": "tutorial-15-01"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Build and convert a graph\n\nThe four-space graph comes from TopologicPy. Graph edges are undirected, so PyG receives an index pair for each direction. Features describe X and Y coordinates; labels distinguish the hall from the other spaces.\n",
   "id": "tutorial-15-02"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": "from topologicpy.Vertex import Vertex\nfrom topologicpy.TGraph import TGraph\n\nentrance = Vertex.ByCoordinates(0, 0, 0)\nhall = Vertex.ByCoordinates(2, 0, 0)\nstudio = Vertex.ByCoordinates(4, 0, 0)\noffice = Vertex.ByCoordinates(2, 2, 0)\nvertices = [entrance, hall, studio, office]\nedges = [{\"src\": 0, \"dst\": 1},\n         {\"src\": 1, \"dst\": 2},\n         {\"src\": 1, \"dst\": 3}]\ngraph = TGraph.ByVerticesEdges(vertices, edges, directed=False)\nassert isinstance(graph, TGraph)\nassert len(TGraph.Vertices(graph)) == 4\nassert len(TGraph.Edges(graph)) == 3\n\n\nimport torch\nfrom torch_geometric.data import Data\nfrom torch_geometric.nn import GCNConv\n\ntorch.manual_seed(7)\ntorch.set_num_threads(1)\nmesh = TGraph.MeshData(graph)\nfeatures = []\nfor coordinates in mesh[\"vertices\"]:\n    features.append(coordinates[:2])\nedge_pairs = []\nfor start, end in mesh[\"edges\"]:\n    edge_pairs.append([start, end])\n    edge_pairs.append([end, start])\ndata = Data(x=torch.tensor(features, dtype=torch.float32),\n            edge_index=torch.tensor(edge_pairs, dtype=torch.long).t().contiguous(),\n            y=torch.tensor([0, 1, 0, 0], dtype=torch.long))\nassert data.num_nodes == len(TGraph.Vertices(graph))\nassert data.num_edges == 2 * len(TGraph.Edges(graph))\nprint(data)\n",
   "outputs": [],
   "execution_count": null,
   "id": "tutorial-15-03"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Run a tiny training loop\n\nA graph convolution mixes information from neighbours. The model returns two scores per node; cross entropy compares those scores with the labels. We use all four nodes for training, so the result below is training behaviour only. A real experiment needs separate training, validation and test data.\n",
   "id": "tutorial-15-04"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": "layer = GCNConv(2, 2)\noptimiser = torch.optim.Adam(layer.parameters(), lr=0.05)\ninitial_loss = torch.nn.functional.cross_entropy(layer(data.x, data.edge_index), data.y).item()\nfor epoch in range(30):\n    optimiser.zero_grad()\n    scores = layer(data.x, data.edge_index)\n    loss = torch.nn.functional.cross_entropy(scores, data.y)\n    loss.backward()\n    optimiser.step()\nprint(\"Initial loss:\", round(initial_loss, 4), \"Final loss:\", round(loss.item(), 4))\nwith torch.no_grad():\n    predictions = layer(data.x, data.edge_index).argmax(dim=1)\nprint(\"Training predictions:\", predictions.tolist())\nassert torch.isfinite(loss).item()\nassert loss.item() < initial_loss\nassert len(predictions) == 4\n",
   "outputs": [],
   "execution_count": null,
   "id": "tutorial-15-05"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Try it yourself\n\nChange the node features and observe the training loss. For a real task, first define an independent test set and a baseline; then explore `topologicpy.PyG` for larger dataset workflows.\n",
   "id": "tutorial-15-06"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (TopologicPy tutorials)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
