{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# cadCAD Template: Robot and the Marbles - Part 5\n",
    "\n",
    "![](images/Overview.png)\n",
    "![](images/Mech1.png)\n",
    "\n",
    "To expand upon our previous examples, we will introduce the concept of using a graph network object that is updated during each state update. The ability to essential embed a graph 'database' into a state is a game changer for scalability, allowing increased complexity with multiple agents or components is represented, easily updated. Below, building upon our previous examples, we will represent the Robots and Marbles example with n boxes, and a variable number of marbles.\n",
    "\n",
    "## Behavior and Mechanisms:\n",
    "* A network of robotic arms is capable of taking a marble from their one of their boxes and dropping it into the other one.\n",
    "* Each robotic arm in the network only controls two boxes and they act by moving a marble from one box to the other.\n",
    "* Each robotic arm is programmed to take one marble at a time from the box containing the most significant number of marbles and drop it in the other box. It repeats that process until the boxes contain an equal number of marbles.\n",
    "* For our analysis of this system, suppose we are only interested in monitoring the number of marbles in only their two boxes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from cadCAD.engine import ExecutionMode, ExecutionContext, Executor\n",
    "from cadCAD.configuration import Configuration\n",
    "import networkx as nx\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "#from copy import deepcopy\n",
    "\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# define global variables\n",
    "T = 25 #iterations in our simulation\n",
    "boxes=5 #number of boxes in our network\n",
    "m= 2 #for barabasi graph type number of edges is (n-2)*m\n",
    "\n",
    "# Settings of general simulation parameters, unrelated to the system itself\n",
    "# `T` is a range with the number of discrete units of time the simulation will run for;\n",
    "# `N` is the number of times the simulation will be run (Monte Carlo runs)\n",
    "simulation_parameters = {\n",
    "    'T': range(T),\n",
    "    'N': 1,\n",
    "    'M': {}\n",
    "}\n",
    "\n",
    "# We create a Barabási–Albert graph and then fill the 5 boxes with between 1 and 10 balls.\n",
    "# create graph object with the number of boxes as nodes\n",
    "network = nx.barabasi_albert_graph(boxes, m)\n",
    "\n",
    "# add balls to box nodes\n",
    "for node in network.nodes:\n",
    "    network.nodes[node]['balls'] = np.random.randint(1,10)\n",
    "    \n",
    "    \n",
    "\n",
    "#Behavior: node by edge dimensional operator\n",
    "#input the states of the boxes output the deltas along the edges\n",
    "\n",
    "# We specify the robotic networks logic in a Policy/Behavior Function\n",
    "# unlike previous examples our policy controls a vector valued action, defined over the edges of our network\n",
    "def robotic_network(params, step, sL, s):\n",
    "    network = s['network']\n",
    "    delta_balls = {}\n",
    "    for e in network.edges:\n",
    "        src = e[0]\n",
    "        dst = e[1]\n",
    "        #transfer one ball across the edge in the direction of more balls to less\n",
    "        delta_balls[e] = np.sign(network.nodes[src]['balls']-network.nodes[dst]['balls'])\n",
    "    return({'delta': delta_balls})    \n",
    "    \n",
    "\n",
    "#mechanism: edge by node dimensional operator\n",
    "#input the deltas along the edges and update the boxes\n",
    "\n",
    "# We make the state update functions less \"intelligent\",\n",
    "# ie. they simply add the number of marbles specified in _input \n",
    "# (which, per the policy function definition, may be negative)\n",
    "\n",
    "def update_network(params, step, sL, s, _input):\n",
    "    network = s['network'] #deepcopy(s['network']) \n",
    "    delta_balls = _input['delta']\n",
    "    for e in network.edges:\n",
    "        move_ball = delta_balls[e]\n",
    "        src = e[0]\n",
    "        dst = e[1]\n",
    "        if (network.nodes[src]['balls'] >= move_ball) and (network.nodes[dst]['balls'] >= -move_ball):\n",
    "            network.nodes[src]['balls'] = network.nodes[src]['balls']-move_ball\n",
    "            network.nodes[dst]['balls'] = network.nodes[dst]['balls']+move_ball\n",
    "            \n",
    "    return ('network', network)\n",
    "\n",
    "    \n",
    "# we initialize the cadCAD state as a network object\n",
    "initial_conditions = {'network':network}\n",
    "\n",
    "\n",
    "# wire up the mechanisms and states\n",
    "partial_state_update_blocks = [\n",
    "    { \n",
    "        'policies': { # The following policy functions will be evaluated and their returns will be passed to the state update functions\n",
    "            'action': robotic_network\n",
    "        },\n",
    "        'variables': { # The following state variables will be updated simultaneously\n",
    "            'network': update_network\n",
    "            \n",
    "        }\n",
    "    }\n",
    "]\n",
    "\n",
    "\n",
    "# The configurations above are then packaged into a `Configuration` object\n",
    "config = Configuration(initial_state=initial_conditions, #dict containing variable names and initial values\n",
    "                       partial_state_update_blocks=partial_state_update_blocks, #dict containing state update functions\n",
    "                       sim_config=simulation_parameters #dict containing simulation parameters\n",
    "                      )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we will plot the network of boxes and with their labels showing how many balls are in each box."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/lib/python3.7/site-packages/networkx/drawing/nx_pylab.py:579: MatplotlibDeprecationWarning: \n",
      "The iterable function was deprecated in Matplotlib 3.1 and will be removed in 3.3. Use np.iterable instead.\n",
      "  if not cb.iterable(width):\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 16080 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# plot of boxes and balls\n",
    "nx.draw_kamada_kawai(network,labels=nx.get_node_attributes(network,'balls'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "single_proc: [<cadCAD.configuration.Configuration object at 0x11a8c1828>]\n",
      "[<cadCAD.configuration.Configuration object at 0x11a8c1828>]\n"
     ]
    }
   ],
   "source": [
    "# Run the simulations\n",
    "exec_mode = ExecutionMode()\n",
    "exec_context = ExecutionContext(exec_mode.single_proc)\n",
    "executor = Executor(exec_context, [config]) # Pass the configuration object inside an array\n",
    "raw_result, tensor = executor.execute() # The `execute()` method returns a tuple; its first elements contains the raw results\n",
    "df = pd.DataFrame(raw_result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We create some helper functions to extract the networkx graph object from the Pandas dataframe and plot it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "#NetworkX helper functions\n",
    "def get_nodes(g):\n",
    "    return [node for node in g.nodes if g.nodes[node]]\n",
    "\n",
    "\n",
    "def pad(vec, length,fill=True):\n",
    "    if fill:\n",
    "        padded = np.zeros(length,)\n",
    "    else:\n",
    "        padded = np.empty(length,)\n",
    "        padded[:] = np.nan\n",
    "        \n",
    "    for i in range(len(vec)):\n",
    "        padded[i]= vec[i]\n",
    "        \n",
    "    return padded\n",
    "\n",
    "def make2D(key, data, fill=False):\n",
    "    maxL = data[key].apply(len).max()\n",
    "    newkey = 'padded_'+key\n",
    "    data[newkey] = data[key].apply(lambda x: pad(x,maxL,fill))\n",
    "    reshaped = np.array([a for a in data[newkey].values])\n",
    "    \n",
    "    return reshaped"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using our helper function get_nodes() we pull out the boxes ball quantity and save it to a new dataframe column."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['Balls'] = df.network.apply(lambda g: np.array([g.nodes[j]['balls'] for j in get_nodes(g)]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next we will plot the number of balls in each box over the simulation time period. We can see an oscillation occurs never reaching an equilibrium due to the uneven nature of the boxes and balls."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.legend.Legend at 0x11ce29320>"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 58348 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.plot(df.timestep,make2D('Balls', df))\n",
    "plt.title('Number of balls in boxes over simulation period')\n",
    "plt.ylabel('Qty')\n",
    "plt.xlabel('Iteration')\n",
    "plt.legend(['Box #'+str(node) for node in range(boxes)], ncol = 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In summary, we created a graph network of boxes and robotic arms to transfer balls between the boxes, striving for an unachievable equilibrium state. The ability to embed a graph, virtually a graph database, into a cadCAD state allows for tremendous scalability and flexibility as a modeling tool."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
