├── .gitignore ├── Chapter_0_Networkx_Introduction.ipynb ├── Chapter_1_Motivating_Problems_and_Systems.ipynb ├── Chapter_2_Elements_of_matrix_theory.ipynb ├── Chapter_3_Elements_of_graph_theory.ipynb ├── Chapter_4_elements_of_algebraic_graph_theory.ipynb ├── Chapter_5_discrete_time_averaging_systems.ipynb ├── Chapter_6_the_laplacian_matrix.ipynb ├── Chapter_7_continuous_time_averaging_system.ipynb ├── LICENSE ├── README.md ├── UCSB.jpg ├── ch1_lib.py ├── img ├── bookcover.png └── layouts.png ├── lib.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Own exemptions added 132 | LNS-suggestions-5oct21.txt 133 | ExistingProgramming/ 134 | Own_suggestion.txt 135 | *.idea/ 136 | Misc/ 137 | WormNet.v3.benchmark.txt 138 | -------------------------------------------------------------------------------- /Chapter_1_Motivating_Problems_and_Systems.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "toc": true 7 | }, 8 | "source": [ 9 | "

Table of Contents

\n", 10 | "
" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "%matplotlib widget\n", 20 | "\n", 21 | "# Import packages\n", 22 | "import numpy as np\n", 23 | "import matplotlib.pyplot as plt\n", 24 | "import matplotlib as mpl\n", 25 | "import networkx as nx\n", 26 | "\n", 27 | "# Import self defined functions\n", 28 | "import ch1_lib # Chapter 1 specific library\n", 29 | "import lib # General library\n", 30 | "\n", 31 | "# For interactive graphs\n", 32 | "import ipywidgets as widgets\n", 33 | "\n", 34 | "# Settings\n", 35 | "custom_figsize= (6, 4) # Might need to change this value to fit the figures to your screen\n", 36 | "custom_figsize_square = (5, 5) " 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "# Chapter 1 - Motivating Problems and Systems\n", 44 | "These Jupyter Notebook scripts contain some examples, visualization and supplements accompanying the book \"Lectures on Network Systems\" by Francesco Bullo http://motion.me.ucsb.edu/book-lns/. These scripts are published with the MIT license. **Make sure to run the first cell above to import all necessary packages and functions and adapt settings in case.** After that, you can jump to each example and follow the description there to execute cell by cell (Tip: Use the shortcut Shift+Enter to execute each cell). Most of the functions are kept in separate files to keep this script neat.\n", 45 | "\n", 46 | "## 1.2 Averaging algorithms in wireless sensor networks\n", 47 | "\n", 48 | "This example is based on first work of Sean Jaffe. It contains the example from Chapter 1.2 for discrete averaging network dynamics with wireless sensor network, enhanced examples and discussion will occur in Chapter 5, this is an introductory visulization. \n", 49 | "\n", 50 | "\n", 51 | "### Create the Graph in networkx and define $A$\n", 52 | "\n", 53 | "Execute next cell to create the example graph and define the matrix $A_{wsn}$.\n", 54 | "\n", 55 | "Note: In Chapter 5 we will introduce an elegant algorithm do define the weights as in this example, the equal-neighbour-model." 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "# Creating the Network System example as in Chapter 1.2\n", 65 | "G = nx.DiGraph()\n", 66 | "G.add_edges_from([(0,1), (1,0), (1,2), (2,1), (2,3), (3,2), (1,3), (3,1)])\n", 67 | "\n", 68 | "# Define position of nodes in graph plot\n", 69 | "pos = {0:[0.2,0.2],1:[.4,.2],2:[.4,.6],3:[.7,.6]}\n", 70 | "\n", 71 | "# Define the adjacency matrix A\n", 72 | "A = np.array([[0.5,0.5, 0., 0.],\n", 73 | " [1/4, 1/4, 1/4, 1/4],\n", 74 | " [0., 1/3, 1/3, 1/3],\n", 75 | " [0., 1/3, 1/3, 1/3]\n", 76 | "])\n", 77 | "\n", 78 | "fig, ax1 = plt.subplots(figsize = custom_figsize) # Init figure\n", 79 | "nx.draw_networkx(G, pos, node_size=200, ax = ax1) # Draw network\n", 80 | "ax1.margins(0.05) # Zooming out for better visualization\n", 81 | "\n", 82 | "print(\"A matrix: \\n\", A)" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": {}, 88 | "source": [ 89 | "### Simulate and visualize Process\n", 90 | "Choose the initial state vector x (randomized or chosen) and the number to time steps to simulate.\n", 91 | "\n", 92 | "We plot our simulation with the initial average as dashed-line to show, that we do not reach average-consensus (more on that in Chapter 5)\n", 93 | "\n", 94 | "Note: Discrete-time linear system $x(k+1) = A x(k)$, $x(0)=x_0$ or, equivalently $x(k) = A^k x_0$" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "# Initialize random state vector or define your own one\n", 104 | "x_0 = np.random.rand(4)\n", 105 | "#x_0 = np.array([0.2, 0.4, 0.6, 0.8])\n", 106 | "# Choosing the range of time step\n", 107 | "t = 15\n", 108 | "\n", 109 | "# Initialize figure\n", 110 | "fig, ax2 = plt.subplots(figsize=custom_figsize)\n", 111 | "\n", 112 | "states = lib.simulate_network(A,x_0, t) # Simulate network and save states for each time step in a t*n np.array\n", 113 | "lib.plot_node_val_2D(states, x_0, t, ax2) # Visualize states in a 2D Graph" 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "metadata": {}, 119 | "source": [ 120 | "### Interactive network presentation\n", 121 | "Below you can find an interactive network simulation and different visualization, where you can use the slide bar to visualize all values at each node for each time step." 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": null, 127 | "metadata": { 128 | "scrolled": false 129 | }, 130 | "outputs": [], 131 | "source": [ 132 | "fig, ax3 = plt.subplots(figsize=custom_figsize)\n", 133 | "fig, v_bound, pos = lib.init_network_sim_plot(G, states, fig, pos=pos)\n", 134 | "\n", 135 | "def inter(timestep):\n", 136 | " lib.update_network(timestep['new'], G=G, states_m=states, ax=ax3, vbound=v_bound, pos=pos)\n", 137 | " #ax3.margins(0.20) # Zooming out for better visualization\n", 138 | " return None\n", 139 | "\n", 140 | "# Plot initial configuration\n", 141 | "lib.update_network(0, G=G, states_m=states, ax=ax3, vbound=v_bound, pos=pos)\n", 142 | "\n", 143 | "\n", 144 | "# Widget\n", 145 | "# If this cell is executed twice we are making sure in the following, that the previous widget instances are all closed\n", 146 | "try:\n", 147 | " [c.close() for c in widget12.children] # Note: close_all() does also affect plot, thus list compr.\n", 148 | "except NameError: # Only want to except not defined variable error\n", 149 | " pass\n", 150 | "\n", 151 | "widget12 = lib.create_widgets_play_slider(fnc=inter, minv=0, maxv=t-1, step=1, play_speed=1000)\n", 152 | "\n", 153 | "display(widget12)" 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "## Exercise 1.4 Simulating the averaging dynamics\n", 161 | "\n", 162 | "In this chapter the results for exercise 1.4 are presented for comparison and discussing the averaging dynamics of these different systems." 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "metadata": {}, 169 | "outputs": [], 170 | "source": [ 171 | "# Define x_0\n", 172 | "xinitial = np.array([-2., -1., 0., +1., +2.])\n", 173 | "\n", 174 | "# Defining the 3 different systems.\n", 175 | "# Complete graph\n", 176 | "Acomplete = np.ones((5,5)) / 5\n", 177 | "\n", 178 | "# Cycle graph\n", 179 | "Acycle = np.array([\n", 180 | " [1/3, 1/3, 0, 0, 1/3],\n", 181 | " [1/3, 1/3, 1/3, 0, 0],\n", 182 | " [0, 1/3, 1/3, 1/3, 0],\n", 183 | " [0, 0, 1/3, 1/3, 1/3],\n", 184 | " [1/3, 0, 0, 1/3, 1/3] ] )\n", 185 | "\n", 186 | "\n", 187 | "# Star topology. center = node 1\n", 188 | "Astar = np.array([\n", 189 | " [1/5, 1/5, 1/5, 1/5, 1/5],\n", 190 | " [1/2, 1/2, 0, 0, 0], \n", 191 | " [1/2, 0, 1/2, 0, 0],\n", 192 | " [1/2, 0, 0, 1/2, 0],\n", 193 | " [1/2, 0, 0, 0, 1/2] ])\n", 194 | "\n", 195 | "# Defining simulation time\n", 196 | "ts = 10" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "metadata": {}, 202 | "source": [ 203 | "**Complete Graph Simulation**" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": null, 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "# Initialize figure\n", 213 | "fig, ax141 = plt.subplots(figsize=custom_figsize)\n", 214 | "\n", 215 | "states_complete = lib.simulate_network(Acomplete,xinitial, ts) # Simulate network and save states for each time step in a t*n np.array\n", 216 | "lib.plot_node_val_2D(states_complete, xinitial, ts, ax141) # Visualize states in a 2D Graph" 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "metadata": {}, 222 | "source": [ 223 | "**Cycle graph Simulation**" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": null, 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "# Initialize figure\n", 233 | "fig, ax142 = plt.subplots(figsize=custom_figsize)\n", 234 | "\n", 235 | "states_cycle = lib.simulate_network(Acycle,xinitial, ts) # Simulate network and save states for each time step in a t*n np.array\n", 236 | "lib.plot_node_val_2D(states_cycle, xinitial, ts, ax142) # Visualize states in a 2D Graph" 237 | ] 238 | }, 239 | { 240 | "cell_type": "markdown", 241 | "metadata": {}, 242 | "source": [ 243 | "**Star Graph Simulation**" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "# Initialize figure\n", 253 | "fig, ax143 = plt.subplots(figsize=custom_figsize)\n", 254 | "\n", 255 | "states_star = lib.simulate_network(Astar,xinitial, ts) # Simulate network and save states for each time step in a t*n np.array\n", 256 | "lib.plot_node_val_2D(states_star, xinitial, ts, ax143) # Visualize states in a 2D Graph" 257 | ] 258 | }, 259 | { 260 | "cell_type": "code", 261 | "execution_count": null, 262 | "metadata": {}, 263 | "outputs": [], 264 | "source": [ 265 | "# Verfying the results (this will be discussed in more detail in Chapter 5)\n", 266 | "eigval, eigvec = np.linalg.eig( Astar.transpose() )\n", 267 | "dom_eigvec = eigvec[0:5,1]/eigvec[0:5,1].sum()\n", 268 | "print(\"Dominant eigenvector: \", dom_eigvec)\n", 269 | "print(\"Final values :\", xinitial@dom_eigvec*np.ones(5))" 270 | ] 271 | }, 272 | { 273 | "cell_type": "markdown", 274 | "metadata": {}, 275 | "source": [ 276 | "## Exercise 1.5 (Section 1.6 ff.) Bugs Dynamic\n", 277 | "In this example the bugs dynamics as in exercise 1.5 are visualized for the cyclic pursuit or the cyclic balancing case.\n", 278 | "### Cyclic Pursuit\n", 279 | "Initialize and simulate the bugs results. If wanted, change some of the parameters." 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": null, 285 | "metadata": {}, 286 | "outputs": [], 287 | "source": [ 288 | "radius = 1.0 # Circle radius\n", 289 | "n_bugs = 6 # Number of bugs\n", 290 | "n_steps = 50 # Number of simulation steps (states)\n", 291 | "gain = 0.4 # Gain kappa\n", 292 | "\n", 293 | "# Initialize the bugs\n", 294 | "bugs_init, states_bugs_init = ch1_lib.init_bugs(radius, n_bugs, n_steps)\n", 295 | "# Simulate the bugs\n", 296 | "states_bugs = ch1_lib.simulate_bugs_cyclic_pursuit(radius, n_bugs, n_steps, gain, bugs_init, states_bugs_init)" 297 | ] 298 | }, 299 | { 300 | "cell_type": "markdown", 301 | "metadata": {}, 302 | "source": [ 303 | "Below you find an interactive visulization of the bugs dynamics for cyclic pursuit." 304 | ] 305 | }, 306 | { 307 | "cell_type": "code", 308 | "execution_count": null, 309 | "metadata": { 310 | "scrolled": false 311 | }, 312 | "outputs": [], 313 | "source": [ 314 | "fig, ax151 = plt.subplots(figsize=custom_figsize)\n", 315 | "\n", 316 | "def interactive_bugs(timestep):\n", 317 | " ax151.clear()\n", 318 | " ch1_lib.plot_circle_and_bugs(ax151, radius, states_bugs[timestep['new']]) # Take the new value received from the slider dict\n", 319 | " return None\n", 320 | "\n", 321 | "# Plot initial configuration\n", 322 | "ch1_lib.plot_circle_and_bugs(ax151, radius, states_bugs[0])\n", 323 | "\n", 324 | "# Widget\n", 325 | "# If this cell is executed twice we are making sure in the following, that the previous widget instances are all closed\n", 326 | "try:\n", 327 | " [c.close() for c in widget151.children] # Note: close_all() does also affect plot, thus list compr.\n", 328 | "except NameError: # Only want to except not defined variable error\n", 329 | " pass\n", 330 | "\n", 331 | "widget151 = lib.create_widgets_play_slider(fnc=interactive_bugs, minv=0, maxv=n_steps-1, step=1, play_speed=500)\n", 332 | "\n", 333 | "display(widget151)" 334 | ] 335 | }, 336 | { 337 | "cell_type": "markdown", 338 | "metadata": {}, 339 | "source": [ 340 | "### Cyclic Balancing\n", 341 | "\n", 342 | "Using some of the previous defined values, showing cycling balancing dynamics." 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": null, 348 | "metadata": {}, 349 | "outputs": [], 350 | "source": [ 351 | "# Initialize the bugs\n", 352 | "bugs_init2, states_bugs_init2 = ch1_lib.init_bugs(radius, n_bugs, n_steps)\n", 353 | "# Simulate the bugs\n", 354 | "states_bugs2 = ch1_lib.simulate_bugs_cyclic_balancing(radius, n_bugs, n_steps, gain, bugs_init2, states_bugs_init2)\n", 355 | "\n", 356 | "fig, ax152 = plt.subplots(figsize=custom_figsize)\n", 357 | "\n", 358 | "def interactive_bugs2(timestep):\n", 359 | " ax152.clear()\n", 360 | " ch1_lib.plot_circle_and_bugs(ax152, radius, states_bugs2[timestep['new']]) # Take the new value received from the slider dict\n", 361 | " return None\n", 362 | "\n", 363 | "# Plot initial configuration\n", 364 | "ch1_lib.plot_circle_and_bugs(ax152, radius, states_bugs2[0])\n", 365 | "\n", 366 | "# Widget\n", 367 | "# If this cell is executed twice we are making sure in the following, that the previous widget instances are all closed\n", 368 | "try:\n", 369 | " [c.close() for c in widget152.children] # Note: close_all() does also affect plot, thus list compr.\n", 370 | "except NameError: # Only want to except not defined variable error\n", 371 | " pass\n", 372 | "\n", 373 | "widget152 = lib.create_widgets_play_slider(fnc=interactive_bugs2, minv=0, maxv=n_steps-1, step=1, play_speed=500)\n", 374 | "\n", 375 | "display(widget152)" 376 | ] 377 | }, 378 | { 379 | "cell_type": "markdown", 380 | "metadata": {}, 381 | "source": [ 382 | "## Exercise 1.6 Robotic coordination\n", 383 | "\n", 384 | "Choose the initial position below and run the cell to visualize the rendezvous coordination. As to be proven in exercise 1.6, if the robots start in a square position, they will stay in a square position.\n", 385 | "\n", 386 | "First, setting up and running the simulation for the results." 387 | ] 388 | }, 389 | { 390 | "cell_type": "code", 391 | "execution_count": null, 392 | "metadata": {}, 393 | "outputs": [], 394 | "source": [ 395 | "# Set up initial position matrix\n", 396 | "initial_positions = 2*np.random.random((4,2))-1\n", 397 | "\n", 398 | "# If you want to use square matrix\n", 399 | "#initial_positions = np.array([[.9, .9], [-.9, .9], [-.9, -.9], [.9, -.9]], dtype=float)\n", 400 | "\n", 401 | "# Number of timesteps and dt\n", 402 | "n_dt = 200\n", 403 | "dt = 0.02\n", 404 | "\n", 405 | "current_positions = initial_positions.copy()\n", 406 | "\n", 407 | "all_positions = np.zeros((n_dt, 4, 2))\n", 408 | "all_positions[0] = initial_positions.copy()\n", 409 | "\n", 410 | "# Simulate the robotic coordination dynamics and save the values\n", 411 | "for ii in range(1, n_dt):\n", 412 | " neighbor_position = np.array([1, 2, 3, 0])\n", 413 | " current_velocities = np.zeros((4, 2))\n", 414 | " \n", 415 | " current_velocities = current_positions[neighbor_position] - current_positions\n", 416 | " current_positions = current_positions + current_velocities*dt # Implicit Euler Integration sufficient here\n", 417 | " all_positions[ii] = current_positions\n" 418 | ] 419 | }, 420 | { 421 | "cell_type": "markdown", 422 | "metadata": {}, 423 | "source": [ 424 | "In this cell the interactive plot for the path visualization of the robots is called." 425 | ] 426 | }, 427 | { 428 | "cell_type": "code", 429 | "execution_count": null, 430 | "metadata": {}, 431 | "outputs": [], 432 | "source": [ 433 | "fig, ax16 = plt.subplots(figsize=custom_figsize_square)\n", 434 | "line, = ax16.plot([], [])\n", 435 | "ax16.set_xlim(( -1, 1))\n", 436 | "ax16.set_ylim((-1, 1))\n", 437 | "\n", 438 | "# Setup interactive plot function\n", 439 | "def interactive_robots(timestep):\n", 440 | " # Only reset when set back to zero\n", 441 | " if timestep['new'] == 0:\n", 442 | " ax16.clear()\n", 443 | " ax16.set_xlim(( -1, 1))\n", 444 | " ax16.set_ylim((-1, 1))\n", 445 | " line, = ax16.plot(all_positions[timestep['new'],0:4,0], all_positions[timestep['new'],0:4,1], 'b.', markersize=2)\n", 446 | " return None\n", 447 | "\n", 448 | "# Widget\n", 449 | "# If this cell is executed twice we are making sure in the following, that the previous widget instances are all closed\n", 450 | "try:\n", 451 | " [c.close() for c in widget16.children] # Note: close_all() does also affect plot, thus list compr.\n", 452 | "except NameError: # Only want to except not defined variable error\n", 453 | " pass\n", 454 | "widget16 = lib.create_widgets_play_slider(fnc=interactive_robots, minv=0, maxv=n_dt-1, step=1, play_speed=500)\n", 455 | "\n", 456 | "display(widget16)" 457 | ] 458 | }, 459 | { 460 | "cell_type": "code", 461 | "execution_count": null, 462 | "metadata": {}, 463 | "outputs": [], 464 | "source": [] 465 | } 466 | ], 467 | "metadata": { 468 | "kernelspec": { 469 | "display_name": "Python 3 (ipykernel)", 470 | "language": "python", 471 | "name": "python3" 472 | }, 473 | "language_info": { 474 | "codemirror_mode": { 475 | "name": "ipython", 476 | "version": 3 477 | }, 478 | "file_extension": ".py", 479 | "mimetype": "text/x-python", 480 | "name": "python", 481 | "nbconvert_exporter": "python", 482 | "pygments_lexer": "ipython3", 483 | "version": "3.8.11" 484 | }, 485 | "toc": { 486 | "base_numbering": 1, 487 | "nav_menu": {}, 488 | "number_sections": false, 489 | "sideBar": true, 490 | "skip_h1_title": false, 491 | "title_cell": "Table of Contents", 492 | "title_sidebar": "Contents", 493 | "toc_cell": true, 494 | "toc_position": { 495 | "height": "calc(100% - 180px)", 496 | "left": "10px", 497 | "top": "150px", 498 | "width": "216.594px" 499 | }, 500 | "toc_section_display": true, 501 | "toc_window_display": false 502 | }, 503 | "widgets": { 504 | "application/vnd.jupyter.widget-state+json": { 505 | "state": { 506 | "02121bef571d4f7f9ab0acc635935746": { 507 | "model_module": "@jupyter-widgets/base", 508 | "model_module_version": "1.2.0", 509 | "model_name": "LayoutModel", 510 | "state": {} 511 | }, 512 | "03ef88295e5741f6a1a04ce029bca410": { 513 | "model_module": "@jupyter-widgets/controls", 514 | "model_module_version": "1.5.0", 515 | "model_name": "SliderStyleModel", 516 | "state": { 517 | "description_width": "" 518 | } 519 | }, 520 | "045c7730e401413c8f5bad5335b8aeb9": { 521 | "model_module": "@jupyter-widgets/controls", 522 | "model_module_version": "1.5.0", 523 | "model_name": "VBoxModel", 524 | "state": { 525 | "children": [ 526 | "IPY_MODEL_9c8121a5398b4e4597c03e1456723c55", 527 | "IPY_MODEL_fa95b56b0fd44b3e824addaf8f818743" 528 | ], 529 | "layout": "IPY_MODEL_7361cc0455754df4ade6bba8325ddfa6" 530 | } 531 | }, 532 | "06151703772e4174a36ff5f72c400d08": { 533 | "model_module": "@jupyter-widgets/controls", 534 | "model_module_version": "1.5.0", 535 | "model_name": "VBoxModel", 536 | "state": { 537 | "children": [ 538 | "IPY_MODEL_6700248decd14e06a21b7b224b3e73f7", 539 | "IPY_MODEL_ccf091fdcfc64566832baf6a16fb315f" 540 | ], 541 | "layout": "IPY_MODEL_93614acf6d404dac87f624047f6e0359" 542 | } 543 | }, 544 | "068025a3e59b40cfb7c38806fd0828cd": { 545 | "model_module": "@jupyter-widgets/base", 546 | "model_module_version": "1.2.0", 547 | "model_name": "LayoutModel", 548 | "state": {} 549 | }, 550 | "06e8ae5d085f47c18a006dc7d92b113d": { 551 | "model_module": "@jupyter-widgets/controls", 552 | "model_module_version": "1.5.0", 553 | "model_name": "IntSliderModel", 554 | "state": { 555 | "description": "timestep", 556 | "layout": "IPY_MODEL_f7c499e22cd9462a9c11a9c8404b1efd", 557 | "max": 49, 558 | "style": "IPY_MODEL_c43fc1a4739340c2874d37a292574a7c" 559 | } 560 | }, 561 | "0b107b92953b43c1ba749e35db9df80d": { 562 | "model_module": "@jupyter-widgets/controls", 563 | "model_module_version": "1.5.0", 564 | "model_name": "VBoxModel", 565 | "state": { 566 | "children": [ 567 | "IPY_MODEL_2bef745b3aee4ccab345966ace7d915a", 568 | "IPY_MODEL_103970cfd00b4fec84d9af844cfbe914" 569 | ], 570 | "layout": "IPY_MODEL_f53067e7b1ca439f864a73f269ea8ae0" 571 | } 572 | }, 573 | "0f816e67dfad494a9917ebc40d91b93f": { 574 | "model_module": "@jupyter-widgets/controls", 575 | "model_module_version": "1.5.0", 576 | "model_name": "VBoxModel", 577 | "state": { 578 | "children": [ 579 | "IPY_MODEL_9c8121a5398b4e4597c03e1456723c55", 580 | "IPY_MODEL_fa95b56b0fd44b3e824addaf8f818743" 581 | ], 582 | "layout": "IPY_MODEL_950a07a98b5d40cfb944ea9e8315a448" 583 | } 584 | }, 585 | "0f83cd7ca5c74014b791f65f595cd170": { 586 | "model_module": "@jupyter-widgets/controls", 587 | "model_module_version": "1.5.0", 588 | "model_name": "VBoxModel", 589 | "state": { 590 | "children": [ 591 | "IPY_MODEL_427217b5d78c4fb384266cc363f14ad6", 592 | "IPY_MODEL_3ca6e897af0d44fc8aeff15784fb5048" 593 | ], 594 | "layout": "IPY_MODEL_13946dadbfc44ebb8fe4224bc051967c" 595 | } 596 | }, 597 | "0ff9089ee50d469c82a3834535b0119b": { 598 | "model_module": "@jupyter-widgets/controls", 599 | "model_module_version": "1.5.0", 600 | "model_name": "VBoxModel", 601 | "state": { 602 | "children": [ 603 | "IPY_MODEL_667ebb20982543dda4a9c01446b50767", 604 | "IPY_MODEL_8a95318deca84f4b9d6323a85947a163" 605 | ], 606 | "layout": "IPY_MODEL_a3ecae38db0e4212b517c09cc99fc435" 607 | } 608 | }, 609 | "103970cfd00b4fec84d9af844cfbe914": { 610 | "model_module": "@jupyter-widgets/controls", 611 | "model_module_version": "1.5.0", 612 | "model_name": "IntSliderModel", 613 | "state": { 614 | "layout": "IPY_MODEL_6edd9f1443584ec6ac3f3d32e7eb3078", 615 | "max": 49, 616 | "min": 1, 617 | "style": "IPY_MODEL_d84b849a8de5415bbccbcd40878e0e51", 618 | "value": 100 619 | } 620 | }, 621 | "12a763f92a0d4497a142d57b6919b67c": { 622 | "model_module": "@jupyter-widgets/controls", 623 | "model_module_version": "1.5.0", 624 | "model_name": "IntSliderModel", 625 | "state": { 626 | "layout": "IPY_MODEL_8d82cc9619ec46aabbc8abcf50022f91", 627 | "max": 49, 628 | "min": 1, 629 | "style": "IPY_MODEL_184edeee36114b459cdb745a49081665", 630 | "value": 100 631 | } 632 | }, 633 | "12f775ec25bd47a78d5e0af100a6c648": { 634 | "model_module": "@jupyter-widgets/controls", 635 | "model_module_version": "1.5.0", 636 | "model_name": "VBoxModel", 637 | "state": { 638 | "_dom_classes": [ 639 | "widget-interact" 640 | ], 641 | "children": [ 642 | "IPY_MODEL_ec331978ce9b4f4c9a0f00578b7a4ef7", 643 | "IPY_MODEL_6b62c56b687b42a3816972ac64aeb4cf" 644 | ], 645 | "layout": "IPY_MODEL_f19ae5f338ae4e2fa7925f6745488878" 646 | } 647 | }, 648 | "13946dadbfc44ebb8fe4224bc051967c": { 649 | "model_module": "@jupyter-widgets/base", 650 | "model_module_version": "1.2.0", 651 | "model_name": "LayoutModel", 652 | "state": {} 653 | }, 654 | "141ae15175be4e79b4a78a02b62b3a54": { 655 | "model_module": "@jupyter-widgets/controls", 656 | "model_module_version": "1.5.0", 657 | "model_name": "SliderStyleModel", 658 | "state": { 659 | "description_width": "" 660 | } 661 | }, 662 | "15fc20cf216542739f628e4612fc623e": { 663 | "model_module": "@jupyter-widgets/controls", 664 | "model_module_version": "1.5.0", 665 | "model_name": "DescriptionStyleModel", 666 | "state": { 667 | "description_width": "" 668 | } 669 | }, 670 | "167104bfe3af4bfb882bdcef8a6b33df": { 671 | "model_module": "@jupyter-widgets/controls", 672 | "model_module_version": "1.5.0", 673 | "model_name": "DescriptionStyleModel", 674 | "state": { 675 | "description_width": "" 676 | } 677 | }, 678 | "16a59263c5e04a39a9b9c919ed17b77a": { 679 | "model_module": "@jupyter-widgets/base", 680 | "model_module_version": "1.2.0", 681 | "model_name": "LayoutModel", 682 | "state": {} 683 | }, 684 | "17c12b8c568d435a939ae29c06429066": { 685 | "model_module": "@jupyter-widgets/base", 686 | "model_module_version": "1.2.0", 687 | "model_name": "LayoutModel", 688 | "state": {} 689 | }, 690 | "184edeee36114b459cdb745a49081665": { 691 | "model_module": "@jupyter-widgets/controls", 692 | "model_module_version": "1.5.0", 693 | "model_name": "SliderStyleModel", 694 | "state": { 695 | "description_width": "" 696 | } 697 | }, 698 | "19c7075413ee4ea5a695552d6aea214a": { 699 | "model_module": "@jupyter-widgets/controls", 700 | "model_module_version": "1.5.0", 701 | "model_name": "SliderStyleModel", 702 | "state": { 703 | "description_width": "" 704 | } 705 | }, 706 | "1b7e931fe23045c385e1713b98f6b02c": { 707 | "model_module": "@jupyter-widgets/controls", 708 | "model_module_version": "1.5.0", 709 | "model_name": "DescriptionStyleModel", 710 | "state": { 711 | "description_width": "" 712 | } 713 | }, 714 | "1bd87a02e14c4f30858fda5aa273fef4": { 715 | "model_module": "@jupyter-widgets/base", 716 | "model_module_version": "1.2.0", 717 | "model_name": "LayoutModel", 718 | "state": {} 719 | }, 720 | "1d0e6f7851b440a38216ba404f1d3947": { 721 | "model_module": "@jupyter-widgets/controls", 722 | "model_module_version": "1.5.0", 723 | "model_name": "VBoxModel", 724 | "state": { 725 | "children": [ 726 | "IPY_MODEL_6700248decd14e06a21b7b224b3e73f7", 727 | "IPY_MODEL_ccf091fdcfc64566832baf6a16fb315f" 728 | ], 729 | "layout": "IPY_MODEL_cdd6ad3dacdb4adbae85a83f5617f305" 730 | } 731 | }, 732 | "1dbd7e6b9a3b449f9cb85e1f4edd3925": { 733 | "model_module": "@jupyter-widgets/controls", 734 | "model_module_version": "1.5.0", 735 | "model_name": "SliderStyleModel", 736 | "state": { 737 | "description_width": "" 738 | } 739 | }, 740 | "1e89ae55debb4883af3c801621bdb40d": { 741 | "model_module": "@jupyter-widgets/controls", 742 | "model_module_version": "1.5.0", 743 | "model_name": "LinkModel", 744 | "state": { 745 | "source": [ 746 | "IPY_MODEL_90a05fc2c083438ab03437482b6cd390", 747 | "value" 748 | ], 749 | "target": [ 750 | "IPY_MODEL_4300d005e9854e9c9f36e64469bb7e11", 751 | "value" 752 | ] 753 | } 754 | }, 755 | "1e9eb89a4759436cb7471709f8f202bd": { 756 | "model_module": "@jupyter-widgets/base", 757 | "model_module_version": "1.2.0", 758 | "model_name": "LayoutModel", 759 | "state": {} 760 | }, 761 | "203eb09e10a94e649b2db810695e52f4": { 762 | "model_module": "@jupyter-widgets/controls", 763 | "model_module_version": "1.5.0", 764 | "model_name": "IntSliderModel", 765 | "state": { 766 | "layout": "IPY_MODEL_a0a444805e164a2a95b989bec7773833", 767 | "max": 14, 768 | "min": 1, 769 | "style": "IPY_MODEL_d5590832e7ee4ecdac5e08ee239b1e06", 770 | "value": 18 771 | } 772 | }, 773 | "21ecb99c79d5431c81b986020b98c5ae": { 774 | "model_module": "@jupyter-widgets/controls", 775 | "model_module_version": "1.5.0", 776 | "model_name": "PlayModel", 777 | "state": { 778 | "interval": 500, 779 | "layout": "IPY_MODEL_afb77d4fe5544a23a257d27ef31d2225", 780 | "min": 1, 781 | "style": "IPY_MODEL_6b6622b276394a4d8fb95df28235b4e9", 782 | "value": 1 783 | } 784 | }, 785 | "22c0f9d8e8aa4b44bf58133e2519e9f4": { 786 | "model_module": "@jupyter-widgets/controls", 787 | "model_module_version": "1.5.0", 788 | "model_name": "DescriptionStyleModel", 789 | "state": { 790 | "description_width": "" 791 | } 792 | }, 793 | "22fbdf4a146f432e8ed6f2d2f2b47623": { 794 | "model_module": "@jupyter-widgets/controls", 795 | "model_module_version": "1.5.0", 796 | "model_name": "VBoxModel", 797 | "state": { 798 | "children": [ 799 | "IPY_MODEL_d725c694d5c0448f8366761bd4a6f225", 800 | "IPY_MODEL_203eb09e10a94e649b2db810695e52f4" 801 | ], 802 | "layout": "IPY_MODEL_7e6a8015328b4cae8dd3b2e236ba711a" 803 | } 804 | }, 805 | "233daea90d564f278c2d3730af426524": { 806 | "model_module": "@jupyter-widgets/base", 807 | "model_module_version": "1.2.0", 808 | "model_name": "LayoutModel", 809 | "state": {} 810 | }, 811 | "2608cf66a19442a58042bda6c4bfaa2f": { 812 | "model_module": "@jupyter-widgets/base", 813 | "model_module_version": "1.2.0", 814 | "model_name": "LayoutModel", 815 | "state": {} 816 | }, 817 | "26b5b85a1d67469cabd8c02aff573354": { 818 | "model_module": "@jupyter-widgets/controls", 819 | "model_module_version": "1.5.0", 820 | "model_name": "DescriptionStyleModel", 821 | "state": { 822 | "description_width": "" 823 | } 824 | }, 825 | "26de9692ef7046dbb933f0fb8cf81fc4": { 826 | "model_module": "@jupyter-widgets/base", 827 | "model_module_version": "1.2.0", 828 | "model_name": "LayoutModel", 829 | "state": {} 830 | }, 831 | "27503e5d3b8e446091c7e5554a0ff40a": { 832 | "model_module": "@jupyter-widgets/controls", 833 | "model_module_version": "1.5.0", 834 | "model_name": "VBoxModel", 835 | "state": { 836 | "children": [ 837 | "IPY_MODEL_851f861e11dc4f5aa8c7da29a7f1458d", 838 | "IPY_MODEL_c554c48bdf014edcb2930035ca2b0601" 839 | ], 840 | "layout": "IPY_MODEL_4310c342c432485a94c0ce1448a883c6" 841 | } 842 | }, 843 | "27b3bfbcd87d4d9fb00b16c205aded4e": { 844 | "model_module": "@jupyter-widgets/controls", 845 | "model_module_version": "1.5.0", 846 | "model_name": "LinkModel", 847 | "state": { 848 | "source": [ 849 | "IPY_MODEL_667ebb20982543dda4a9c01446b50767", 850 | "value" 851 | ], 852 | "target": [ 853 | "IPY_MODEL_8a95318deca84f4b9d6323a85947a163", 854 | "value" 855 | ] 856 | } 857 | }, 858 | "282dee2ecb554bd1a354e4849acfb5c2": { 859 | "model_module": "@jupyter-widgets/controls", 860 | "model_module_version": "1.5.0", 861 | "model_name": "VBoxModel", 862 | "state": { 863 | "children": [ 864 | "IPY_MODEL_90a05fc2c083438ab03437482b6cd390", 865 | "IPY_MODEL_4300d005e9854e9c9f36e64469bb7e11" 866 | ], 867 | "layout": "IPY_MODEL_1bd87a02e14c4f30858fda5aa273fef4" 868 | } 869 | }, 870 | "290e4105af4d4269a4dc24a7932e875c": { 871 | "model_module": "@jupyter-widgets/controls", 872 | "model_module_version": "1.5.0", 873 | "model_name": "VBoxModel", 874 | "state": { 875 | "children": [ 876 | "IPY_MODEL_427217b5d78c4fb384266cc363f14ad6", 877 | "IPY_MODEL_3ca6e897af0d44fc8aeff15784fb5048" 878 | ], 879 | "layout": "IPY_MODEL_17c12b8c568d435a939ae29c06429066" 880 | } 881 | }, 882 | "2bef745b3aee4ccab345966ace7d915a": { 883 | "model_module": "@jupyter-widgets/controls", 884 | "model_module_version": "1.5.0", 885 | "model_name": "PlayModel", 886 | "state": { 887 | "interval": 500, 888 | "layout": "IPY_MODEL_c38b76a7a5a749e4ab08610a621c5467", 889 | "min": 1, 890 | "style": "IPY_MODEL_167104bfe3af4bfb882bdcef8a6b33df", 891 | "value": 100 892 | } 893 | }, 894 | "2e458c8cd0de4ace9d38b7db7955fb8d": { 895 | "model_module": "@jupyter-widgets/controls", 896 | "model_module_version": "1.5.0", 897 | "model_name": "VBoxModel", 898 | "state": { 899 | "children": [ 900 | "IPY_MODEL_5ebb17b54836406288bda29694194d41", 901 | "IPY_MODEL_cc6e3ce1e1ee40cfbe3cc0c29b28f2d4" 902 | ], 903 | "layout": "IPY_MODEL_f1787ab740f345d19f568d7ac5fdb432" 904 | } 905 | }, 906 | "2f50e5d4751f4361be0daf3846627ca6": { 907 | "model_module": "@jupyter-widgets/controls", 908 | "model_module_version": "1.5.0", 909 | "model_name": "DescriptionStyleModel", 910 | "state": { 911 | "description_width": "" 912 | } 913 | }, 914 | "304d9c4ed2464c9cb9f29bc0f24046d4": { 915 | "model_module": "@jupyter-widgets/controls", 916 | "model_module_version": "1.5.0", 917 | "model_name": "VBoxModel", 918 | "state": { 919 | "children": [ 920 | "IPY_MODEL_2bef745b3aee4ccab345966ace7d915a", 921 | "IPY_MODEL_103970cfd00b4fec84d9af844cfbe914" 922 | ], 923 | "layout": "IPY_MODEL_a8369add432f40799ef45a0674bdc91a" 924 | } 925 | }, 926 | "307782a04e72463bb42115181b2a31b1": { 927 | "model_module": "@jupyter-widgets/base", 928 | "model_module_version": "1.2.0", 929 | "model_name": "LayoutModel", 930 | "state": {} 931 | }, 932 | "311429386125447c94374694795cade9": { 933 | "model_module": "@jupyter-widgets/output", 934 | "model_module_version": "1.0.0", 935 | "model_name": "OutputModel", 936 | "state": { 937 | "layout": "IPY_MODEL_c6c5d281b57a49bbbf8828c204062eb3" 938 | } 939 | }, 940 | "344d20e788ee46c99d560f7a296519c5": { 941 | "model_module": "@jupyter-widgets/controls", 942 | "model_module_version": "1.5.0", 943 | "model_name": "LinkModel", 944 | "state": { 945 | "source": [ 946 | "IPY_MODEL_c00642c3bdda4a8e907371b822f87d27", 947 | "value" 948 | ], 949 | "target": [ 950 | "IPY_MODEL_867a76f01d024c2696c98a2a2a27b95e", 951 | "value" 952 | ] 953 | } 954 | }, 955 | "34ab7cfa385e4cddbfd6d923309c4b6c": { 956 | "model_module": "@jupyter-widgets/controls", 957 | "model_module_version": "1.5.0", 958 | "model_name": "LinkModel", 959 | "state": { 960 | "source": [ 961 | "IPY_MODEL_b9f1f86df304477296d6b798cf50aa02", 962 | "value" 963 | ], 964 | "target": [ 965 | "IPY_MODEL_35d7209a6a654d95996f859fbcd79675", 966 | "value" 967 | ] 968 | } 969 | }, 970 | "35d7209a6a654d95996f859fbcd79675": { 971 | "model_module": "@jupyter-widgets/controls", 972 | "model_module_version": "1.5.0", 973 | "model_name": "IntSliderModel", 974 | "state": { 975 | "layout": "IPY_MODEL_d2472da3b0f9497fb556e473e03a404a", 976 | "max": 14, 977 | "min": 1, 978 | "style": "IPY_MODEL_622c22f17bd143988265909fee0b8f76", 979 | "value": 100 980 | } 981 | }, 982 | "36b73045608e43cfa67cef869d764404": { 983 | "model_module": "@jupyter-widgets/controls", 984 | "model_module_version": "1.5.0", 985 | "model_name": "SliderStyleModel", 986 | "state": { 987 | "description_width": "" 988 | } 989 | }, 990 | "36d5799c6aed468fa996960aaa7c7493": { 991 | "model_module": "@jupyter-widgets/controls", 992 | "model_module_version": "1.5.0", 993 | "model_name": "PlayModel", 994 | "state": { 995 | "interval": 2000, 996 | "layout": "IPY_MODEL_4dc172ba08444e7899e368dac99600fc", 997 | "min": 1, 998 | "style": "IPY_MODEL_49200a9bcc8b4f2991e36b2cc16775ce", 999 | "value": 1 1000 | } 1001 | }, 1002 | "3a46882bf81640528f95ed7a4f6b5af8": { 1003 | "model_module": "@jupyter-widgets/controls", 1004 | "model_module_version": "1.5.0", 1005 | "model_name": "VBoxModel", 1006 | "state": { 1007 | "children": [ 1008 | "IPY_MODEL_b9f1f86df304477296d6b798cf50aa02", 1009 | "IPY_MODEL_35d7209a6a654d95996f859fbcd79675" 1010 | ], 1011 | "layout": "IPY_MODEL_1e9eb89a4759436cb7471709f8f202bd" 1012 | } 1013 | }, 1014 | "3aa8448a80c642f28a688e3ad9db3192": { 1015 | "model_module": "@jupyter-widgets/controls", 1016 | "model_module_version": "1.5.0", 1017 | "model_name": "IntSliderModel", 1018 | "state": { 1019 | "layout": "IPY_MODEL_f5557fedda1c41a59c6db82bfc5f7ea4", 1020 | "max": 14, 1021 | "min": 1, 1022 | "style": "IPY_MODEL_9653825182d74a84b405c8a35645121c", 1023 | "value": 1 1024 | } 1025 | }, 1026 | "3ca6e897af0d44fc8aeff15784fb5048": { 1027 | "model_module": "@jupyter-widgets/controls", 1028 | "model_module_version": "1.5.0", 1029 | "model_name": "IntSliderModel", 1030 | "state": { 1031 | "layout": "IPY_MODEL_f75e937f05a44d13b1bc95852a1f990b", 1032 | "max": 14, 1033 | "min": 1, 1034 | "style": "IPY_MODEL_141ae15175be4e79b4a78a02b62b3a54", 1035 | "value": 6 1036 | } 1037 | }, 1038 | "40c4a86eca9c4225900b431fb01c531e": { 1039 | "model_module": "@jupyter-widgets/base", 1040 | "model_module_version": "1.2.0", 1041 | "model_name": "LayoutModel", 1042 | "state": {} 1043 | }, 1044 | "427217b5d78c4fb384266cc363f14ad6": { 1045 | "model_module": "@jupyter-widgets/controls", 1046 | "model_module_version": "1.5.0", 1047 | "model_name": "PlayModel", 1048 | "state": { 1049 | "interval": 1000, 1050 | "layout": "IPY_MODEL_7bdf3f11632b4214bbb5130ed78a8544", 1051 | "min": 1, 1052 | "style": "IPY_MODEL_4f3bfafdad1f41d591d0a59558c07f64", 1053 | "value": 6 1054 | } 1055 | }, 1056 | "42f40d1570454ce3ac8d1327f15eebea": { 1057 | "model_module": "@jupyter-widgets/controls", 1058 | "model_module_version": "1.5.0", 1059 | "model_name": "IntSliderModel", 1060 | "state": { 1061 | "layout": "IPY_MODEL_4deede4126ba43239617279cb8730aeb", 1062 | "min": 1, 1063 | "style": "IPY_MODEL_d168d280cda94e9da7198d98c30ef6e1", 1064 | "value": 1 1065 | } 1066 | }, 1067 | "4300d005e9854e9c9f36e64469bb7e11": { 1068 | "model_module": "@jupyter-widgets/controls", 1069 | "model_module_version": "1.5.0", 1070 | "model_name": "IntSliderModel", 1071 | "state": { 1072 | "layout": "IPY_MODEL_8c1fe06f34674d80bf243eeb0b58692c", 1073 | "max": 49, 1074 | "min": 1, 1075 | "style": "IPY_MODEL_1dbd7e6b9a3b449f9cb85e1f4edd3925", 1076 | "value": 9 1077 | } 1078 | }, 1079 | "4310c342c432485a94c0ce1448a883c6": { 1080 | "model_module": "@jupyter-widgets/base", 1081 | "model_module_version": "1.2.0", 1082 | "model_name": "LayoutModel", 1083 | "state": {} 1084 | }, 1085 | "439d0a85dbfd480a826512277908dd1d": { 1086 | "model_module": "@jupyter-widgets/controls", 1087 | "model_module_version": "1.5.0", 1088 | "model_name": "VBoxModel", 1089 | "state": { 1090 | "_dom_classes": [ 1091 | "widget-interact" 1092 | ], 1093 | "children": [ 1094 | "IPY_MODEL_06e8ae5d085f47c18a006dc7d92b113d", 1095 | "IPY_MODEL_83e1fcb083694700b5da0d2fb35c57d5" 1096 | ], 1097 | "layout": "IPY_MODEL_9fd0bbf009e04b8ea046b4a95d33e2ee" 1098 | } 1099 | }, 1100 | "47a6440af436474a8b99d53d8fc736db": { 1101 | "model_module": "@jupyter-widgets/controls", 1102 | "model_module_version": "1.5.0", 1103 | "model_name": "LinkModel", 1104 | "state": { 1105 | "source": [ 1106 | "IPY_MODEL_74bfcbf937ec4008884662d73c9073fa", 1107 | "value" 1108 | ], 1109 | "target": [ 1110 | "IPY_MODEL_a32c562f1f4a443e9494c6e851d757a3", 1111 | "value" 1112 | ] 1113 | } 1114 | }, 1115 | "4804809a56dc4725bbc93e8848a14bc3": { 1116 | "model_module": "@jupyter-widgets/controls", 1117 | "model_module_version": "1.5.0", 1118 | "model_name": "VBoxModel", 1119 | "state": { 1120 | "children": [ 1121 | "IPY_MODEL_c00642c3bdda4a8e907371b822f87d27", 1122 | "IPY_MODEL_867a76f01d024c2696c98a2a2a27b95e" 1123 | ], 1124 | "layout": "IPY_MODEL_c9bf513cb5ef4605af748b1f15b47fba" 1125 | } 1126 | }, 1127 | "49200a9bcc8b4f2991e36b2cc16775ce": { 1128 | "model_module": "@jupyter-widgets/controls", 1129 | "model_module_version": "1.5.0", 1130 | "model_name": "DescriptionStyleModel", 1131 | "state": { 1132 | "description_width": "" 1133 | } 1134 | }, 1135 | "4a5dce6e78874aa0ac1f68e4d2ccbfde": { 1136 | "model_module": "@jupyter-widgets/base", 1137 | "model_module_version": "1.2.0", 1138 | "model_name": "LayoutModel", 1139 | "state": {} 1140 | }, 1141 | "4b8bf88790054f6a884d11f339c63a40": { 1142 | "model_module": "@jupyter-widgets/base", 1143 | "model_module_version": "1.2.0", 1144 | "model_name": "LayoutModel", 1145 | "state": {} 1146 | }, 1147 | "4dc172ba08444e7899e368dac99600fc": { 1148 | "model_module": "@jupyter-widgets/base", 1149 | "model_module_version": "1.2.0", 1150 | "model_name": "LayoutModel", 1151 | "state": {} 1152 | }, 1153 | "4deede4126ba43239617279cb8730aeb": { 1154 | "model_module": "@jupyter-widgets/base", 1155 | "model_module_version": "1.2.0", 1156 | "model_name": "LayoutModel", 1157 | "state": {} 1158 | }, 1159 | "4ea0a0aef3b84f73b575b2be36c405f5": { 1160 | "model_module": "@jupyter-widgets/controls", 1161 | "model_module_version": "1.5.0", 1162 | "model_name": "SliderStyleModel", 1163 | "state": { 1164 | "description_width": "" 1165 | } 1166 | }, 1167 | "4f3bfafdad1f41d591d0a59558c07f64": { 1168 | "model_module": "@jupyter-widgets/controls", 1169 | "model_module_version": "1.5.0", 1170 | "model_name": "DescriptionStyleModel", 1171 | "state": { 1172 | "description_width": "" 1173 | } 1174 | }, 1175 | "501caf2c6da94a3391a53306e82e7ab4": { 1176 | "model_module": "@jupyter-widgets/controls", 1177 | "model_module_version": "1.5.0", 1178 | "model_name": "SliderStyleModel", 1179 | "state": { 1180 | "description_width": "" 1181 | } 1182 | }, 1183 | "502691d1993c4475a9fc7fd435b6bb36": { 1184 | "model_module": "@jupyter-widgets/controls", 1185 | "model_module_version": "1.5.0", 1186 | "model_name": "DescriptionStyleModel", 1187 | "state": { 1188 | "description_width": "" 1189 | } 1190 | }, 1191 | "51380319e5634a94b65d8d708dd6e284": { 1192 | "model_module": "@jupyter-widgets/controls", 1193 | "model_module_version": "1.5.0", 1194 | "model_name": "VBoxModel", 1195 | "state": { 1196 | "_dom_classes": [ 1197 | "widget-interact" 1198 | ], 1199 | "children": [ 1200 | "IPY_MODEL_a69c205ab0f2434daea9ed20c25bbbcf", 1201 | "IPY_MODEL_311429386125447c94374694795cade9" 1202 | ], 1203 | "layout": "IPY_MODEL_b8ace946840a44929e21f9aa15b7cdc0" 1204 | } 1205 | }, 1206 | "54b89a7b641c436bb2617f1a6278b314": { 1207 | "model_module": "@jupyter-widgets/controls", 1208 | "model_module_version": "1.5.0", 1209 | "model_name": "DescriptionStyleModel", 1210 | "state": { 1211 | "description_width": "" 1212 | } 1213 | }, 1214 | "56c87032b19f4f65873fa3f5b2265e6a": { 1215 | "model_module": "@jupyter-widgets/base", 1216 | "model_module_version": "1.2.0", 1217 | "model_name": "LayoutModel", 1218 | "state": {} 1219 | }, 1220 | "5928e8ae503540868d12d94fedd71dda": { 1221 | "model_module": "@jupyter-widgets/base", 1222 | "model_module_version": "1.2.0", 1223 | "model_name": "LayoutModel", 1224 | "state": {} 1225 | }, 1226 | "5a1ae91e11004d5c913ee2aee34225dc": { 1227 | "model_module": "@jupyter-widgets/base", 1228 | "model_module_version": "1.2.0", 1229 | "model_name": "LayoutModel", 1230 | "state": {} 1231 | }, 1232 | "5a2ec4c5856946479694ca27f75b98c2": { 1233 | "model_module": "@jupyter-widgets/base", 1234 | "model_module_version": "1.2.0", 1235 | "model_name": "LayoutModel", 1236 | "state": {} 1237 | }, 1238 | "5a5ab206c5e44ccc9bb66edee98c1169": { 1239 | "model_module": "@jupyter-widgets/controls", 1240 | "model_module_version": "1.5.0", 1241 | "model_name": "SliderStyleModel", 1242 | "state": { 1243 | "description_width": "" 1244 | } 1245 | }, 1246 | "5abe6261e6f24057ae741ec67143c215": { 1247 | "model_module": "@jupyter-widgets/base", 1248 | "model_module_version": "1.2.0", 1249 | "model_name": "LayoutModel", 1250 | "state": {} 1251 | }, 1252 | "5ae2e02dc8e0433cac881948dbe65b01": { 1253 | "model_module": "@jupyter-widgets/base", 1254 | "model_module_version": "1.2.0", 1255 | "model_name": "LayoutModel", 1256 | "state": {} 1257 | }, 1258 | "5b0a068a7e0d4fdfb9822006d4782d4b": { 1259 | "model_module": "@jupyter-widgets/controls", 1260 | "model_module_version": "1.5.0", 1261 | "model_name": "LinkModel", 1262 | "state": { 1263 | "source": [ 1264 | "IPY_MODEL_427217b5d78c4fb384266cc363f14ad6", 1265 | "value" 1266 | ], 1267 | "target": [ 1268 | "IPY_MODEL_3ca6e897af0d44fc8aeff15784fb5048", 1269 | "value" 1270 | ] 1271 | } 1272 | }, 1273 | "5cd80f4b81bf4cf5a78d708d202f163a": { 1274 | "model_module": "@jupyter-widgets/controls", 1275 | "model_module_version": "1.5.0", 1276 | "model_name": "DescriptionStyleModel", 1277 | "state": { 1278 | "description_width": "" 1279 | } 1280 | }, 1281 | "5ebb17b54836406288bda29694194d41": { 1282 | "model_module": "@jupyter-widgets/controls", 1283 | "model_module_version": "1.5.0", 1284 | "model_name": "PlayModel", 1285 | "state": { 1286 | "interval": 500, 1287 | "layout": "IPY_MODEL_8d58ed4f8add41cb9bba8301e406c487", 1288 | "min": 1, 1289 | "style": "IPY_MODEL_9ffde5664969422a96f31bcd82f6ca4d", 1290 | "value": 1 1291 | } 1292 | }, 1293 | "60379ec4e7a047e38284df72a4874b42": { 1294 | "model_module": "@jupyter-widgets/base", 1295 | "model_module_version": "1.2.0", 1296 | "model_name": "LayoutModel", 1297 | "state": {} 1298 | }, 1299 | "622c22f17bd143988265909fee0b8f76": { 1300 | "model_module": "@jupyter-widgets/controls", 1301 | "model_module_version": "1.5.0", 1302 | "model_name": "SliderStyleModel", 1303 | "state": { 1304 | "description_width": "" 1305 | } 1306 | }, 1307 | "646c9e9396c7447082a3d977864c5776": { 1308 | "model_module": "@jupyter-widgets/controls", 1309 | "model_module_version": "1.5.0", 1310 | "model_name": "LinkModel", 1311 | "state": { 1312 | "source": [ 1313 | "IPY_MODEL_2bef745b3aee4ccab345966ace7d915a", 1314 | "value" 1315 | ], 1316 | "target": [ 1317 | "IPY_MODEL_103970cfd00b4fec84d9af844cfbe914", 1318 | "value" 1319 | ] 1320 | } 1321 | }, 1322 | "64c4e1853978468eaea15d70b2a4c8bf": { 1323 | "model_module": "@jupyter-widgets/controls", 1324 | "model_module_version": "1.5.0", 1325 | "model_name": "IntSliderModel", 1326 | "state": { 1327 | "layout": "IPY_MODEL_56c87032b19f4f65873fa3f5b2265e6a", 1328 | "max": 49, 1329 | "min": 1, 1330 | "style": "IPY_MODEL_501caf2c6da94a3391a53306e82e7ab4", 1331 | "value": 1 1332 | } 1333 | }, 1334 | "651a568f9cf5497a947403580244ba47": { 1335 | "model_module": "@jupyter-widgets/controls", 1336 | "model_module_version": "1.5.0", 1337 | "model_name": "IntSliderModel", 1338 | "state": { 1339 | "layout": "IPY_MODEL_7a72ff23762b4e76953d9a0d1b07348b", 1340 | "max": 49, 1341 | "min": 1, 1342 | "style": "IPY_MODEL_4ea0a0aef3b84f73b575b2be36c405f5", 1343 | "value": 5 1344 | } 1345 | }, 1346 | "667ebb20982543dda4a9c01446b50767": { 1347 | "model_module": "@jupyter-widgets/controls", 1348 | "model_module_version": "1.5.0", 1349 | "model_name": "PlayModel", 1350 | "state": { 1351 | "interval": 49, 1352 | "layout": "IPY_MODEL_6f871cdab1da4df3a7a0354259ed7e97", 1353 | "min": 1, 1354 | "style": "IPY_MODEL_67d99c6f95664024a76bae80d4bfe18a", 1355 | "value": 6 1356 | } 1357 | }, 1358 | "6700248decd14e06a21b7b224b3e73f7": { 1359 | "model_module": "@jupyter-widgets/controls", 1360 | "model_module_version": "1.5.0", 1361 | "model_name": "PlayModel", 1362 | "state": { 1363 | "interval": 500, 1364 | "layout": "IPY_MODEL_b478e1091a7c4c1ba5cfbcda34bce80d", 1365 | "min": 1, 1366 | "style": "IPY_MODEL_71977aa35c6544c1b17a88f0ec205028", 1367 | "value": 14 1368 | } 1369 | }, 1370 | "6731ef4edcad49b4873c321b63349c72": { 1371 | "model_module": "@jupyter-widgets/controls", 1372 | "model_module_version": "1.5.0", 1373 | "model_name": "VBoxModel", 1374 | "state": { 1375 | "children": [ 1376 | "IPY_MODEL_b9f1f86df304477296d6b798cf50aa02", 1377 | "IPY_MODEL_35d7209a6a654d95996f859fbcd79675" 1378 | ], 1379 | "layout": "IPY_MODEL_cf511e1b1cd448ccb440810e7da13bcc" 1380 | } 1381 | }, 1382 | "67d99c6f95664024a76bae80d4bfe18a": { 1383 | "model_module": "@jupyter-widgets/controls", 1384 | "model_module_version": "1.5.0", 1385 | "model_name": "DescriptionStyleModel", 1386 | "state": { 1387 | "description_width": "" 1388 | } 1389 | }, 1390 | "69ea8f19d79c428fa1dcd2e01fd478c5": { 1391 | "model_module": "@jupyter-widgets/controls", 1392 | "model_module_version": "1.5.0", 1393 | "model_name": "PlayModel", 1394 | "state": { 1395 | "interval": 500, 1396 | "layout": "IPY_MODEL_da194ba0933b48529f5be949ca406377", 1397 | "min": 1, 1398 | "style": "IPY_MODEL_22c0f9d8e8aa4b44bf58133e2519e9f4", 1399 | "value": 1 1400 | } 1401 | }, 1402 | "6a89089153c24c8687faa720f2d84b93": { 1403 | "model_module": "@jupyter-widgets/base", 1404 | "model_module_version": "1.2.0", 1405 | "model_name": "LayoutModel", 1406 | "state": {} 1407 | }, 1408 | "6b62c56b687b42a3816972ac64aeb4cf": { 1409 | "model_module": "@jupyter-widgets/output", 1410 | "model_module_version": "1.0.0", 1411 | "model_name": "OutputModel", 1412 | "state": { 1413 | "layout": "IPY_MODEL_cdbccef4e1e14fea9d836a2c13bea84f" 1414 | } 1415 | }, 1416 | "6b6622b276394a4d8fb95df28235b4e9": { 1417 | "model_module": "@jupyter-widgets/controls", 1418 | "model_module_version": "1.5.0", 1419 | "model_name": "DescriptionStyleModel", 1420 | "state": { 1421 | "description_width": "" 1422 | } 1423 | }, 1424 | "6ca60fed3e8548cfadb18875da255bb3": { 1425 | "model_module": "@jupyter-widgets/controls", 1426 | "model_module_version": "1.5.0", 1427 | "model_name": "PlayModel", 1428 | "state": { 1429 | "interval": 500, 1430 | "layout": "IPY_MODEL_c159d03f63624620957122f77213309d", 1431 | "min": 1, 1432 | "style": "IPY_MODEL_96c0b055b4c0452db8913426f0a47ba4", 1433 | "value": 17 1434 | } 1435 | }, 1436 | "6cda058abdd34538b4b254bbbc7c82a0": { 1437 | "model_module": "@jupyter-widgets/controls", 1438 | "model_module_version": "1.5.0", 1439 | "model_name": "SliderStyleModel", 1440 | "state": { 1441 | "description_width": "" 1442 | } 1443 | }, 1444 | "6edd9f1443584ec6ac3f3d32e7eb3078": { 1445 | "model_module": "@jupyter-widgets/base", 1446 | "model_module_version": "1.2.0", 1447 | "model_name": "LayoutModel", 1448 | "state": {} 1449 | }, 1450 | "6f871cdab1da4df3a7a0354259ed7e97": { 1451 | "model_module": "@jupyter-widgets/base", 1452 | "model_module_version": "1.2.0", 1453 | "model_name": "LayoutModel", 1454 | "state": {} 1455 | }, 1456 | "707bcc001d8f4c6c8f8893768c11ece7": { 1457 | "model_module": "@jupyter-widgets/controls", 1458 | "model_module_version": "1.5.0", 1459 | "model_name": "LinkModel", 1460 | "state": { 1461 | "source": [ 1462 | "IPY_MODEL_36d5799c6aed468fa996960aaa7c7493", 1463 | "value" 1464 | ], 1465 | "target": [ 1466 | "IPY_MODEL_42f40d1570454ce3ac8d1327f15eebea", 1467 | "value" 1468 | ] 1469 | } 1470 | }, 1471 | "71977aa35c6544c1b17a88f0ec205028": { 1472 | "model_module": "@jupyter-widgets/controls", 1473 | "model_module_version": "1.5.0", 1474 | "model_name": "DescriptionStyleModel", 1475 | "state": { 1476 | "description_width": "" 1477 | } 1478 | }, 1479 | "7361cc0455754df4ade6bba8325ddfa6": { 1480 | "model_module": "@jupyter-widgets/base", 1481 | "model_module_version": "1.2.0", 1482 | "model_name": "LayoutModel", 1483 | "state": {} 1484 | }, 1485 | "738781321d7b4de0b8f69d8a94471d77": { 1486 | "model_module": "@jupyter-widgets/controls", 1487 | "model_module_version": "1.5.0", 1488 | "model_name": "VBoxModel", 1489 | "state": { 1490 | "children": [ 1491 | "IPY_MODEL_f90ddba3f7404005aa1f43e3ba31f21b", 1492 | "IPY_MODEL_9cc1625077584709a88770648f8e2335" 1493 | ], 1494 | "layout": "IPY_MODEL_f4c053ae387a4777accfcb3ca72a2bab" 1495 | } 1496 | }, 1497 | "74bfcbf937ec4008884662d73c9073fa": { 1498 | "model_module": "@jupyter-widgets/controls", 1499 | "model_module_version": "1.5.0", 1500 | "model_name": "PlayModel", 1501 | "state": { 1502 | "interval": 500, 1503 | "layout": "IPY_MODEL_fd5e5a4ca31e410bb0d3cb46473306ce", 1504 | "min": 1, 1505 | "style": "IPY_MODEL_f2e87c45ddfa4aaabcd8d4e1e623b500", 1506 | "value": 1 1507 | } 1508 | }, 1509 | "75a4851cbe7a4f4e97c526e46ef913f1": { 1510 | "model_module": "@jupyter-widgets/base", 1511 | "model_module_version": "1.2.0", 1512 | "model_name": "LayoutModel", 1513 | "state": {} 1514 | }, 1515 | "75fb258a84f545e7949c271c8655e957": { 1516 | "model_module": "@jupyter-widgets/base", 1517 | "model_module_version": "1.2.0", 1518 | "model_name": "LayoutModel", 1519 | "state": {} 1520 | }, 1521 | "772c60f851674776bdf21204aed3ba25": { 1522 | "model_module": "@jupyter-widgets/controls", 1523 | "model_module_version": "1.5.0", 1524 | "model_name": "VBoxModel", 1525 | "state": { 1526 | "children": [ 1527 | "IPY_MODEL_b46a869eb8864d708eab11957f3deae6", 1528 | "IPY_MODEL_88d5181e5a014d8c9817d1aa061b97a1" 1529 | ], 1530 | "layout": "IPY_MODEL_ee36997568954e41885387cabbe29d6a" 1531 | } 1532 | }, 1533 | "7a72ff23762b4e76953d9a0d1b07348b": { 1534 | "model_module": "@jupyter-widgets/base", 1535 | "model_module_version": "1.2.0", 1536 | "model_name": "LayoutModel", 1537 | "state": {} 1538 | }, 1539 | "7bdf3f11632b4214bbb5130ed78a8544": { 1540 | "model_module": "@jupyter-widgets/base", 1541 | "model_module_version": "1.2.0", 1542 | "model_name": "LayoutModel", 1543 | "state": {} 1544 | }, 1545 | "7e6a8015328b4cae8dd3b2e236ba711a": { 1546 | "model_module": "@jupyter-widgets/base", 1547 | "model_module_version": "1.2.0", 1548 | "model_name": "LayoutModel", 1549 | "state": {} 1550 | }, 1551 | "7ea9d8f6b9f2404488df9cbb4929a7c8": { 1552 | "model_module": "@jupyter-widgets/controls", 1553 | "model_module_version": "1.5.0", 1554 | "model_name": "IntSliderModel", 1555 | "state": { 1556 | "layout": "IPY_MODEL_8f8540b787b34bda920ba242361735a1", 1557 | "max": 49, 1558 | "min": 1, 1559 | "style": "IPY_MODEL_dfe94442f7284d8a8edabc32aa688c57", 1560 | "value": 1 1561 | } 1562 | }, 1563 | "82c5aa8c6bd141fb8451abdd39bb7256": { 1564 | "model_module": "@jupyter-widgets/controls", 1565 | "model_module_version": "1.5.0", 1566 | "model_name": "LinkModel", 1567 | "state": { 1568 | "source": [ 1569 | "IPY_MODEL_851f861e11dc4f5aa8c7da29a7f1458d", 1570 | "value" 1571 | ], 1572 | "target": [ 1573 | "IPY_MODEL_c554c48bdf014edcb2930035ca2b0601", 1574 | "value" 1575 | ] 1576 | } 1577 | }, 1578 | "83e1fcb083694700b5da0d2fb35c57d5": { 1579 | "model_module": "@jupyter-widgets/output", 1580 | "model_module_version": "1.0.0", 1581 | "model_name": "OutputModel", 1582 | "state": { 1583 | "layout": "IPY_MODEL_9e6c1af01f4d423b86b824752f7d7a74" 1584 | } 1585 | }, 1586 | "84447aeff6bb4fe4a2622d474d354b2a": { 1587 | "model_module": "@jupyter-widgets/base", 1588 | "model_module_version": "1.2.0", 1589 | "model_name": "LayoutModel", 1590 | "state": {} 1591 | }, 1592 | "847c5b67b961413f93102cf5bef9cdf5": { 1593 | "model_module": "@jupyter-widgets/base", 1594 | "model_module_version": "1.2.0", 1595 | "model_name": "LayoutModel", 1596 | "state": {} 1597 | }, 1598 | "851f861e11dc4f5aa8c7da29a7f1458d": { 1599 | "model_module": "@jupyter-widgets/controls", 1600 | "model_module_version": "1.5.0", 1601 | "model_name": "PlayModel", 1602 | "state": { 1603 | "interval": 300, 1604 | "layout": "IPY_MODEL_4a5dce6e78874aa0ac1f68e4d2ccbfde", 1605 | "min": 1, 1606 | "style": "IPY_MODEL_502691d1993c4475a9fc7fd435b6bb36", 1607 | "value": 100 1608 | } 1609 | }, 1610 | "867a76f01d024c2696c98a2a2a27b95e": { 1611 | "model_module": "@jupyter-widgets/controls", 1612 | "model_module_version": "1.5.0", 1613 | "model_name": "IntSliderModel", 1614 | "state": { 1615 | "layout": "IPY_MODEL_16a59263c5e04a39a9b9c919ed17b77a", 1616 | "max": 14, 1617 | "min": 1, 1618 | "style": "IPY_MODEL_d413792c29254e94bd8010d580e36bbf", 1619 | "value": 1 1620 | } 1621 | }, 1622 | "88d5181e5a014d8c9817d1aa061b97a1": { 1623 | "model_module": "@jupyter-widgets/controls", 1624 | "model_module_version": "1.5.0", 1625 | "model_name": "IntSliderModel", 1626 | "state": { 1627 | "layout": "IPY_MODEL_40c4a86eca9c4225900b431fb01c531e", 1628 | "max": 49, 1629 | "min": 1, 1630 | "style": "IPY_MODEL_eee4ba0a606f4d919477b41c220df12f", 1631 | "value": 100 1632 | } 1633 | }, 1634 | "88e41cac65b945d2bb7d221e29784c7d": { 1635 | "model_module": "@jupyter-widgets/controls", 1636 | "model_module_version": "1.5.0", 1637 | "model_name": "PlayModel", 1638 | "state": { 1639 | "interval": 300, 1640 | "layout": "IPY_MODEL_2608cf66a19442a58042bda6c4bfaa2f", 1641 | "min": 1, 1642 | "style": "IPY_MODEL_26b5b85a1d67469cabd8c02aff573354", 1643 | "value": 1 1644 | } 1645 | }, 1646 | "8a95318deca84f4b9d6323a85947a163": { 1647 | "model_module": "@jupyter-widgets/controls", 1648 | "model_module_version": "1.5.0", 1649 | "model_name": "IntSliderModel", 1650 | "state": { 1651 | "layout": "IPY_MODEL_02121bef571d4f7f9ab0acc635935746", 1652 | "max": 49, 1653 | "min": 1, 1654 | "style": "IPY_MODEL_d6cbc6a88ba94fde830221ea8b6cba5a", 1655 | "value": 6 1656 | } 1657 | }, 1658 | "8b70f6762bb841a3819c6c4d38ebdbbf": { 1659 | "model_module": "@jupyter-widgets/controls", 1660 | "model_module_version": "1.5.0", 1661 | "model_name": "LinkModel", 1662 | "state": { 1663 | "source": [ 1664 | "IPY_MODEL_69ea8f19d79c428fa1dcd2e01fd478c5", 1665 | "value" 1666 | ], 1667 | "target": [ 1668 | "IPY_MODEL_7ea9d8f6b9f2404488df9cbb4929a7c8", 1669 | "value" 1670 | ] 1671 | } 1672 | }, 1673 | "8c1fe06f34674d80bf243eeb0b58692c": { 1674 | "model_module": "@jupyter-widgets/base", 1675 | "model_module_version": "1.2.0", 1676 | "model_name": "LayoutModel", 1677 | "state": {} 1678 | }, 1679 | "8c4a6c08eecb476a9b9fa5076a94c5cd": { 1680 | "model_module": "@jupyter-widgets/controls", 1681 | "model_module_version": "1.5.0", 1682 | "model_name": "IntSliderModel", 1683 | "state": { 1684 | "layout": "IPY_MODEL_26de9692ef7046dbb933f0fb8cf81fc4", 1685 | "max": 49, 1686 | "min": 1, 1687 | "style": "IPY_MODEL_6cda058abdd34538b4b254bbbc7c82a0", 1688 | "value": 17 1689 | } 1690 | }, 1691 | "8d58ed4f8add41cb9bba8301e406c487": { 1692 | "model_module": "@jupyter-widgets/base", 1693 | "model_module_version": "1.2.0", 1694 | "model_name": "LayoutModel", 1695 | "state": {} 1696 | }, 1697 | "8d82cc9619ec46aabbc8abcf50022f91": { 1698 | "model_module": "@jupyter-widgets/base", 1699 | "model_module_version": "1.2.0", 1700 | "model_name": "LayoutModel", 1701 | "state": {} 1702 | }, 1703 | "8dc3d00c2cd54e218b4bf6fd8ad1b488": { 1704 | "model_module": "@jupyter-widgets/controls", 1705 | "model_module_version": "1.5.0", 1706 | "model_name": "DescriptionStyleModel", 1707 | "state": { 1708 | "description_width": "" 1709 | } 1710 | }, 1711 | "8f8540b787b34bda920ba242361735a1": { 1712 | "model_module": "@jupyter-widgets/base", 1713 | "model_module_version": "1.2.0", 1714 | "model_name": "LayoutModel", 1715 | "state": {} 1716 | }, 1717 | "8fa7cd03684b413187ea3bb3cbfe75a7": { 1718 | "model_module": "@jupyter-widgets/controls", 1719 | "model_module_version": "1.5.0", 1720 | "model_name": "SliderStyleModel", 1721 | "state": { 1722 | "description_width": "" 1723 | } 1724 | }, 1725 | "8fb4b405ac9740eda43e96e28565057f": { 1726 | "model_module": "@jupyter-widgets/controls", 1727 | "model_module_version": "1.5.0", 1728 | "model_name": "LinkModel", 1729 | "state": { 1730 | "source": [ 1731 | "IPY_MODEL_d725c694d5c0448f8366761bd4a6f225", 1732 | "value" 1733 | ], 1734 | "target": [ 1735 | "IPY_MODEL_203eb09e10a94e649b2db810695e52f4", 1736 | "value" 1737 | ] 1738 | } 1739 | }, 1740 | "9009b319cb7b4d59be4a1e362eb07f1a": { 1741 | "model_module": "@jupyter-widgets/base", 1742 | "model_module_version": "1.2.0", 1743 | "model_name": "LayoutModel", 1744 | "state": {} 1745 | }, 1746 | "90a05fc2c083438ab03437482b6cd390": { 1747 | "model_module": "@jupyter-widgets/controls", 1748 | "model_module_version": "1.5.0", 1749 | "model_name": "PlayModel", 1750 | "state": { 1751 | "interval": 500, 1752 | "layout": "IPY_MODEL_a949fcfe3777441d94461f287c22be9b", 1753 | "min": 1, 1754 | "style": "IPY_MODEL_e2ed718b0ca24bb8876338c61275874e", 1755 | "value": 9 1756 | } 1757 | }, 1758 | "90ce32ee0c8a41b3a151df3e1969ceab": { 1759 | "model_module": "@jupyter-widgets/controls", 1760 | "model_module_version": "1.5.0", 1761 | "model_name": "VBoxModel", 1762 | "state": { 1763 | "children": [ 1764 | "IPY_MODEL_b46a869eb8864d708eab11957f3deae6", 1765 | "IPY_MODEL_88d5181e5a014d8c9817d1aa061b97a1" 1766 | ], 1767 | "layout": "IPY_MODEL_9009b319cb7b4d59be4a1e362eb07f1a" 1768 | } 1769 | }, 1770 | "93614acf6d404dac87f624047f6e0359": { 1771 | "model_module": "@jupyter-widgets/base", 1772 | "model_module_version": "1.2.0", 1773 | "model_name": "LayoutModel", 1774 | "state": {} 1775 | }, 1776 | "950a07a98b5d40cfb944ea9e8315a448": { 1777 | "model_module": "@jupyter-widgets/base", 1778 | "model_module_version": "1.2.0", 1779 | "model_name": "LayoutModel", 1780 | "state": {} 1781 | }, 1782 | "957cd71d281544f6ad89a3e0ea855930": { 1783 | "model_module": "@jupyter-widgets/controls", 1784 | "model_module_version": "1.5.0", 1785 | "model_name": "LinkModel", 1786 | "state": { 1787 | "source": [ 1788 | "IPY_MODEL_c0aa7d49770a4c5dbd58af0c5d419737", 1789 | "value" 1790 | ], 1791 | "target": [ 1792 | "IPY_MODEL_12a763f92a0d4497a142d57b6919b67c", 1793 | "value" 1794 | ] 1795 | } 1796 | }, 1797 | "9653825182d74a84b405c8a35645121c": { 1798 | "model_module": "@jupyter-widgets/controls", 1799 | "model_module_version": "1.5.0", 1800 | "model_name": "SliderStyleModel", 1801 | "state": { 1802 | "description_width": "" 1803 | } 1804 | }, 1805 | "96c0b055b4c0452db8913426f0a47ba4": { 1806 | "model_module": "@jupyter-widgets/controls", 1807 | "model_module_version": "1.5.0", 1808 | "model_name": "DescriptionStyleModel", 1809 | "state": { 1810 | "description_width": "" 1811 | } 1812 | }, 1813 | "99b805c2a0144d31849fce4d65746d10": { 1814 | "model_module": "@jupyter-widgets/base", 1815 | "model_module_version": "1.2.0", 1816 | "model_name": "LayoutModel", 1817 | "state": {} 1818 | }, 1819 | "9b21503505d04c768f9003ca9573d8a9": { 1820 | "model_module": "@jupyter-widgets/base", 1821 | "model_module_version": "1.2.0", 1822 | "model_name": "LayoutModel", 1823 | "state": {} 1824 | }, 1825 | "9b57be62cf14469caec7cafd168f8ed7": { 1826 | "model_module": "@jupyter-widgets/controls", 1827 | "model_module_version": "1.5.0", 1828 | "model_name": "DescriptionStyleModel", 1829 | "state": { 1830 | "description_width": "" 1831 | } 1832 | }, 1833 | "9bfc7fc2b6d6489a94b86a71ea4e39dd": { 1834 | "model_module": "@jupyter-widgets/base", 1835 | "model_module_version": "1.2.0", 1836 | "model_name": "LayoutModel", 1837 | "state": {} 1838 | }, 1839 | "9c8121a5398b4e4597c03e1456723c55": { 1840 | "model_module": "@jupyter-widgets/controls", 1841 | "model_module_version": "1.5.0", 1842 | "model_name": "PlayModel", 1843 | "state": { 1844 | "interval": 500, 1845 | "layout": "IPY_MODEL_e88e93e19ec04c87865f802688097090", 1846 | "min": 1, 1847 | "style": "IPY_MODEL_2f50e5d4751f4361be0daf3846627ca6", 1848 | "value": 8 1849 | } 1850 | }, 1851 | "9cc1625077584709a88770648f8e2335": { 1852 | "model_module": "@jupyter-widgets/controls", 1853 | "model_module_version": "1.5.0", 1854 | "model_name": "IntSliderModel", 1855 | "state": { 1856 | "layout": "IPY_MODEL_60379ec4e7a047e38284df72a4874b42", 1857 | "max": 49, 1858 | "min": 1, 1859 | "style": "IPY_MODEL_a1d62a20f70a46e1ad10750a4de7c9ab", 1860 | "value": 1 1861 | } 1862 | }, 1863 | "9e6c1af01f4d423b86b824752f7d7a74": { 1864 | "model_module": "@jupyter-widgets/base", 1865 | "model_module_version": "1.2.0", 1866 | "model_name": "LayoutModel", 1867 | "state": {} 1868 | }, 1869 | "9eaff4325dce41bdbdb08dc460c98b00": { 1870 | "model_module": "@jupyter-widgets/base", 1871 | "model_module_version": "1.2.0", 1872 | "model_name": "LayoutModel", 1873 | "state": {} 1874 | }, 1875 | "9fd0bbf009e04b8ea046b4a95d33e2ee": { 1876 | "model_module": "@jupyter-widgets/base", 1877 | "model_module_version": "1.2.0", 1878 | "model_name": "LayoutModel", 1879 | "state": {} 1880 | }, 1881 | "9ffde5664969422a96f31bcd82f6ca4d": { 1882 | "model_module": "@jupyter-widgets/controls", 1883 | "model_module_version": "1.5.0", 1884 | "model_name": "DescriptionStyleModel", 1885 | "state": { 1886 | "description_width": "" 1887 | } 1888 | }, 1889 | "a0a444805e164a2a95b989bec7773833": { 1890 | "model_module": "@jupyter-widgets/base", 1891 | "model_module_version": "1.2.0", 1892 | "model_name": "LayoutModel", 1893 | "state": {} 1894 | }, 1895 | "a116c27efb5a4d888f7981cffa478a0f": { 1896 | "model_module": "@jupyter-widgets/base", 1897 | "model_module_version": "1.2.0", 1898 | "model_name": "LayoutModel", 1899 | "state": {} 1900 | }, 1901 | "a11a50af04f84a24b40850d9979ae74a": { 1902 | "model_module": "@jupyter-widgets/base", 1903 | "model_module_version": "1.2.0", 1904 | "model_name": "LayoutModel", 1905 | "state": {} 1906 | }, 1907 | "a1d62a20f70a46e1ad10750a4de7c9ab": { 1908 | "model_module": "@jupyter-widgets/controls", 1909 | "model_module_version": "1.5.0", 1910 | "model_name": "SliderStyleModel", 1911 | "state": { 1912 | "description_width": "" 1913 | } 1914 | }, 1915 | "a32c562f1f4a443e9494c6e851d757a3": { 1916 | "model_module": "@jupyter-widgets/controls", 1917 | "model_module_version": "1.5.0", 1918 | "model_name": "IntSliderModel", 1919 | "state": { 1920 | "layout": "IPY_MODEL_9b21503505d04c768f9003ca9573d8a9", 1921 | "max": 49, 1922 | "min": 1, 1923 | "style": "IPY_MODEL_be5463885f8940f7b9d6bd0664aedc3d", 1924 | "value": 1 1925 | } 1926 | }, 1927 | "a36799c4e8e744dd86093a17e8c1eafd": { 1928 | "model_module": "@jupyter-widgets/controls", 1929 | "model_module_version": "1.5.0", 1930 | "model_name": "VBoxModel", 1931 | "state": { 1932 | "children": [ 1933 | "IPY_MODEL_5ebb17b54836406288bda29694194d41", 1934 | "IPY_MODEL_cc6e3ce1e1ee40cfbe3cc0c29b28f2d4" 1935 | ], 1936 | "layout": "IPY_MODEL_5a1ae91e11004d5c913ee2aee34225dc" 1937 | } 1938 | }, 1939 | "a3ecae38db0e4212b517c09cc99fc435": { 1940 | "model_module": "@jupyter-widgets/base", 1941 | "model_module_version": "1.2.0", 1942 | "model_name": "LayoutModel", 1943 | "state": {} 1944 | }, 1945 | "a3f58def97504071a45e81d5c41f72d6": { 1946 | "model_module": "@jupyter-widgets/controls", 1947 | "model_module_version": "1.5.0", 1948 | "model_name": "LinkModel", 1949 | "state": { 1950 | "source": [ 1951 | "IPY_MODEL_b46a869eb8864d708eab11957f3deae6", 1952 | "value" 1953 | ], 1954 | "target": [ 1955 | "IPY_MODEL_88d5181e5a014d8c9817d1aa061b97a1", 1956 | "value" 1957 | ] 1958 | } 1959 | }, 1960 | "a61fd02ee2c54be1b3cbd79df9ef92d6": { 1961 | "model_module": "@jupyter-widgets/base", 1962 | "model_module_version": "1.2.0", 1963 | "model_name": "LayoutModel", 1964 | "state": {} 1965 | }, 1966 | "a69c205ab0f2434daea9ed20c25bbbcf": { 1967 | "model_module": "@jupyter-widgets/controls", 1968 | "model_module_version": "1.5.0", 1969 | "model_name": "IntSliderModel", 1970 | "state": { 1971 | "description": "timestep", 1972 | "layout": "IPY_MODEL_5ae2e02dc8e0433cac881948dbe65b01", 1973 | "max": 14, 1974 | "style": "IPY_MODEL_d4fbba9350c241fd8da9d1b0683c717a", 1975 | "value": 7 1976 | } 1977 | }, 1978 | "a82eccdf86644326a3bf785aec6a2bb0": { 1979 | "model_module": "@jupyter-widgets/controls", 1980 | "model_module_version": "1.5.0", 1981 | "model_name": "VBoxModel", 1982 | "state": { 1983 | "children": [ 1984 | "IPY_MODEL_69ea8f19d79c428fa1dcd2e01fd478c5", 1985 | "IPY_MODEL_7ea9d8f6b9f2404488df9cbb4929a7c8" 1986 | ], 1987 | "layout": "IPY_MODEL_84447aeff6bb4fe4a2622d474d354b2a" 1988 | } 1989 | }, 1990 | "a8369add432f40799ef45a0674bdc91a": { 1991 | "model_module": "@jupyter-widgets/base", 1992 | "model_module_version": "1.2.0", 1993 | "model_name": "LayoutModel", 1994 | "state": {} 1995 | }, 1996 | "a87ded50208e4a31b28b44bfed23f06b": { 1997 | "model_module": "@jupyter-widgets/controls", 1998 | "model_module_version": "1.5.0", 1999 | "model_name": "VBoxModel", 2000 | "state": { 2001 | "children": [ 2002 | "IPY_MODEL_c00642c3bdda4a8e907371b822f87d27", 2003 | "IPY_MODEL_867a76f01d024c2696c98a2a2a27b95e" 2004 | ], 2005 | "layout": "IPY_MODEL_feb176aad55d46498da4ee0b3dc5865f" 2006 | } 2007 | }, 2008 | "a949fcfe3777441d94461f287c22be9b": { 2009 | "model_module": "@jupyter-widgets/base", 2010 | "model_module_version": "1.2.0", 2011 | "model_name": "LayoutModel", 2012 | "state": {} 2013 | }, 2014 | "ad5721c4d53a4cac8077fd33cb7f5e4d": { 2015 | "model_module": "@jupyter-widgets/controls", 2016 | "model_module_version": "1.5.0", 2017 | "model_name": "LinkModel", 2018 | "state": { 2019 | "source": [ 2020 | "IPY_MODEL_6ca60fed3e8548cfadb18875da255bb3", 2021 | "value" 2022 | ], 2023 | "target": [ 2024 | "IPY_MODEL_8c4a6c08eecb476a9b9fa5076a94c5cd", 2025 | "value" 2026 | ] 2027 | } 2028 | }, 2029 | "afb77d4fe5544a23a257d27ef31d2225": { 2030 | "model_module": "@jupyter-widgets/base", 2031 | "model_module_version": "1.2.0", 2032 | "model_name": "LayoutModel", 2033 | "state": {} 2034 | }, 2035 | "b46a869eb8864d708eab11957f3deae6": { 2036 | "model_module": "@jupyter-widgets/controls", 2037 | "model_module_version": "1.5.0", 2038 | "model_name": "PlayModel", 2039 | "state": { 2040 | "interval": 500, 2041 | "layout": "IPY_MODEL_cb56c52903974e10ac0b06c719ef0216", 2042 | "min": 1, 2043 | "style": "IPY_MODEL_15fc20cf216542739f628e4612fc623e", 2044 | "value": 100 2045 | } 2046 | }, 2047 | "b478e1091a7c4c1ba5cfbcda34bce80d": { 2048 | "model_module": "@jupyter-widgets/base", 2049 | "model_module_version": "1.2.0", 2050 | "model_name": "LayoutModel", 2051 | "state": {} 2052 | }, 2053 | "b5a6465e220d4bca9d14609c857056ec": { 2054 | "model_module": "@jupyter-widgets/controls", 2055 | "model_module_version": "1.5.0", 2056 | "model_name": "LinkModel", 2057 | "state": { 2058 | "source": [ 2059 | "IPY_MODEL_f90ddba3f7404005aa1f43e3ba31f21b", 2060 | "value" 2061 | ], 2062 | "target": [ 2063 | "IPY_MODEL_9cc1625077584709a88770648f8e2335", 2064 | "value" 2065 | ] 2066 | } 2067 | }, 2068 | "b85d0e804aae4a258fcc0f8a70224e11": { 2069 | "model_module": "@jupyter-widgets/output", 2070 | "model_module_version": "1.0.0", 2071 | "model_name": "OutputModel", 2072 | "state": { 2073 | "layout": "IPY_MODEL_5a2ec4c5856946479694ca27f75b98c2" 2074 | } 2075 | }, 2076 | "b8ace946840a44929e21f9aa15b7cdc0": { 2077 | "model_module": "@jupyter-widgets/base", 2078 | "model_module_version": "1.2.0", 2079 | "model_name": "LayoutModel", 2080 | "state": {} 2081 | }, 2082 | "b9f1f86df304477296d6b798cf50aa02": { 2083 | "model_module": "@jupyter-widgets/controls", 2084 | "model_module_version": "1.5.0", 2085 | "model_name": "PlayModel", 2086 | "state": { 2087 | "interval": 300, 2088 | "layout": "IPY_MODEL_9eaff4325dce41bdbdb08dc460c98b00", 2089 | "min": 1, 2090 | "style": "IPY_MODEL_8dc3d00c2cd54e218b4bf6fd8ad1b488", 2091 | "value": 100 2092 | } 2093 | }, 2094 | "bbc5e8a3b65d4b29879cd859b3ed55bb": { 2095 | "model_module": "@jupyter-widgets/controls", 2096 | "model_module_version": "1.5.0", 2097 | "model_name": "VBoxModel", 2098 | "state": { 2099 | "children": [ 2100 | "IPY_MODEL_74bfcbf937ec4008884662d73c9073fa", 2101 | "IPY_MODEL_a32c562f1f4a443e9494c6e851d757a3" 2102 | ], 2103 | "layout": "IPY_MODEL_4b8bf88790054f6a884d11f339c63a40" 2104 | } 2105 | }, 2106 | "bcc064ba20594a9c8d88b254777fdd46": { 2107 | "model_module": "@jupyter-widgets/base", 2108 | "model_module_version": "1.2.0", 2109 | "model_name": "LayoutModel", 2110 | "state": {} 2111 | }, 2112 | "be2073f00ff84da99f73bddc0fab78f3": { 2113 | "model_module": "@jupyter-widgets/base", 2114 | "model_module_version": "1.2.0", 2115 | "model_name": "LayoutModel", 2116 | "state": {} 2117 | }, 2118 | "be5463885f8940f7b9d6bd0664aedc3d": { 2119 | "model_module": "@jupyter-widgets/controls", 2120 | "model_module_version": "1.5.0", 2121 | "model_name": "SliderStyleModel", 2122 | "state": { 2123 | "description_width": "" 2124 | } 2125 | }, 2126 | "c00642c3bdda4a8e907371b822f87d27": { 2127 | "model_module": "@jupyter-widgets/controls", 2128 | "model_module_version": "1.5.0", 2129 | "model_name": "PlayModel", 2130 | "state": { 2131 | "interval": 1000, 2132 | "layout": "IPY_MODEL_5abe6261e6f24057ae741ec67143c215", 2133 | "min": 1, 2134 | "style": "IPY_MODEL_5cd80f4b81bf4cf5a78d708d202f163a", 2135 | "value": 1 2136 | } 2137 | }, 2138 | "c0aa7d49770a4c5dbd58af0c5d419737": { 2139 | "model_module": "@jupyter-widgets/controls", 2140 | "model_module_version": "1.5.0", 2141 | "model_name": "PlayModel", 2142 | "state": { 2143 | "interval": 500, 2144 | "layout": "IPY_MODEL_a11a50af04f84a24b40850d9979ae74a", 2145 | "min": 1, 2146 | "style": "IPY_MODEL_9b57be62cf14469caec7cafd168f8ed7", 2147 | "value": 100 2148 | } 2149 | }, 2150 | "c159d03f63624620957122f77213309d": { 2151 | "model_module": "@jupyter-widgets/base", 2152 | "model_module_version": "1.2.0", 2153 | "model_name": "LayoutModel", 2154 | "state": {} 2155 | }, 2156 | "c38b76a7a5a749e4ab08610a621c5467": { 2157 | "model_module": "@jupyter-widgets/base", 2158 | "model_module_version": "1.2.0", 2159 | "model_name": "LayoutModel", 2160 | "state": {} 2161 | }, 2162 | "c3afe4a448fc452495a8e1b6e1b2dbb6": { 2163 | "model_module": "@jupyter-widgets/controls", 2164 | "model_module_version": "1.5.0", 2165 | "model_name": "DescriptionStyleModel", 2166 | "state": { 2167 | "description_width": "" 2168 | } 2169 | }, 2170 | "c40f5e7444534af1ae2aba4d8f73a9ee": { 2171 | "model_module": "@jupyter-widgets/controls", 2172 | "model_module_version": "1.5.0", 2173 | "model_name": "PlayModel", 2174 | "state": { 2175 | "interval": 49, 2176 | "layout": "IPY_MODEL_307782a04e72463bb42115181b2a31b1", 2177 | "min": 1, 2178 | "style": "IPY_MODEL_1b7e931fe23045c385e1713b98f6b02c", 2179 | "value": 5 2180 | } 2181 | }, 2182 | "c43fc1a4739340c2874d37a292574a7c": { 2183 | "model_module": "@jupyter-widgets/controls", 2184 | "model_module_version": "1.5.0", 2185 | "model_name": "SliderStyleModel", 2186 | "state": { 2187 | "description_width": "" 2188 | } 2189 | }, 2190 | "c554c48bdf014edcb2930035ca2b0601": { 2191 | "model_module": "@jupyter-widgets/controls", 2192 | "model_module_version": "1.5.0", 2193 | "model_name": "IntSliderModel", 2194 | "state": { 2195 | "layout": "IPY_MODEL_068025a3e59b40cfb7c38806fd0828cd", 2196 | "max": 14, 2197 | "min": 1, 2198 | "style": "IPY_MODEL_d5f57217e7fb46d7b4ede486f980e8a2", 2199 | "value": 100 2200 | } 2201 | }, 2202 | "c6c5d281b57a49bbbf8828c204062eb3": { 2203 | "model_module": "@jupyter-widgets/base", 2204 | "model_module_version": "1.2.0", 2205 | "model_name": "LayoutModel", 2206 | "state": {} 2207 | }, 2208 | "c9bf513cb5ef4605af748b1f15b47fba": { 2209 | "model_module": "@jupyter-widgets/base", 2210 | "model_module_version": "1.2.0", 2211 | "model_name": "LayoutModel", 2212 | "state": {} 2213 | }, 2214 | "cb56c52903974e10ac0b06c719ef0216": { 2215 | "model_module": "@jupyter-widgets/base", 2216 | "model_module_version": "1.2.0", 2217 | "model_name": "LayoutModel", 2218 | "state": {} 2219 | }, 2220 | "cba32987459441a8948ec3715e7aaf16": { 2221 | "model_module": "@jupyter-widgets/base", 2222 | "model_module_version": "1.2.0", 2223 | "model_name": "LayoutModel", 2224 | "state": {} 2225 | }, 2226 | "cc6e3ce1e1ee40cfbe3cc0c29b28f2d4": { 2227 | "model_module": "@jupyter-widgets/controls", 2228 | "model_module_version": "1.5.0", 2229 | "model_name": "IntSliderModel", 2230 | "state": { 2231 | "layout": "IPY_MODEL_847c5b67b961413f93102cf5bef9cdf5", 2232 | "max": 49, 2233 | "min": 1, 2234 | "style": "IPY_MODEL_19c7075413ee4ea5a695552d6aea214a", 2235 | "value": 1 2236 | } 2237 | }, 2238 | "ccf091fdcfc64566832baf6a16fb315f": { 2239 | "model_module": "@jupyter-widgets/controls", 2240 | "model_module_version": "1.5.0", 2241 | "model_name": "IntSliderModel", 2242 | "state": { 2243 | "layout": "IPY_MODEL_be2073f00ff84da99f73bddc0fab78f3", 2244 | "max": 49, 2245 | "min": 1, 2246 | "style": "IPY_MODEL_36b73045608e43cfa67cef869d764404", 2247 | "value": 14 2248 | } 2249 | }, 2250 | "cdbccef4e1e14fea9d836a2c13bea84f": { 2251 | "model_module": "@jupyter-widgets/base", 2252 | "model_module_version": "1.2.0", 2253 | "model_name": "LayoutModel", 2254 | "state": {} 2255 | }, 2256 | "cdd6ad3dacdb4adbae85a83f5617f305": { 2257 | "model_module": "@jupyter-widgets/base", 2258 | "model_module_version": "1.2.0", 2259 | "model_name": "LayoutModel", 2260 | "state": {} 2261 | }, 2262 | "ceff27309e054284b8dcc1e602892ed0": { 2263 | "model_module": "@jupyter-widgets/controls", 2264 | "model_module_version": "1.5.0", 2265 | "model_name": "VBoxModel", 2266 | "state": { 2267 | "children": [ 2268 | "IPY_MODEL_c40f5e7444534af1ae2aba4d8f73a9ee", 2269 | "IPY_MODEL_651a568f9cf5497a947403580244ba47" 2270 | ], 2271 | "layout": "IPY_MODEL_233daea90d564f278c2d3730af426524" 2272 | } 2273 | }, 2274 | "cf3dd02fd4e94079a9f49daeb893d064": { 2275 | "model_module": "@jupyter-widgets/controls", 2276 | "model_module_version": "1.5.0", 2277 | "model_name": "VBoxModel", 2278 | "state": { 2279 | "children": [ 2280 | "IPY_MODEL_851f861e11dc4f5aa8c7da29a7f1458d", 2281 | "IPY_MODEL_c554c48bdf014edcb2930035ca2b0601" 2282 | ], 2283 | "layout": "IPY_MODEL_a116c27efb5a4d888f7981cffa478a0f" 2284 | } 2285 | }, 2286 | "cf511e1b1cd448ccb440810e7da13bcc": { 2287 | "model_module": "@jupyter-widgets/base", 2288 | "model_module_version": "1.2.0", 2289 | "model_name": "LayoutModel", 2290 | "state": {} 2291 | }, 2292 | "d168d280cda94e9da7198d98c30ef6e1": { 2293 | "model_module": "@jupyter-widgets/controls", 2294 | "model_module_version": "1.5.0", 2295 | "model_name": "SliderStyleModel", 2296 | "state": { 2297 | "description_width": "" 2298 | } 2299 | }, 2300 | "d1e3aef6acbe453899d61b720e5a92d6": { 2301 | "model_module": "@jupyter-widgets/base", 2302 | "model_module_version": "1.2.0", 2303 | "model_name": "LayoutModel", 2304 | "state": {} 2305 | }, 2306 | "d2472da3b0f9497fb556e473e03a404a": { 2307 | "model_module": "@jupyter-widgets/base", 2308 | "model_module_version": "1.2.0", 2309 | "model_name": "LayoutModel", 2310 | "state": {} 2311 | }, 2312 | "d413792c29254e94bd8010d580e36bbf": { 2313 | "model_module": "@jupyter-widgets/controls", 2314 | "model_module_version": "1.5.0", 2315 | "model_name": "SliderStyleModel", 2316 | "state": { 2317 | "description_width": "" 2318 | } 2319 | }, 2320 | "d4365af283684eb4a8ffd0bad8992c39": { 2321 | "model_module": "@jupyter-widgets/controls", 2322 | "model_module_version": "1.5.0", 2323 | "model_name": "VBoxModel", 2324 | "state": { 2325 | "children": [ 2326 | "IPY_MODEL_d725c694d5c0448f8366761bd4a6f225", 2327 | "IPY_MODEL_203eb09e10a94e649b2db810695e52f4" 2328 | ], 2329 | "layout": "IPY_MODEL_9bfc7fc2b6d6489a94b86a71ea4e39dd" 2330 | } 2331 | }, 2332 | "d4609d8834e04ab2ab48479e88e55a58": { 2333 | "model_module": "@jupyter-widgets/base", 2334 | "model_module_version": "1.2.0", 2335 | "model_name": "LayoutModel", 2336 | "state": {} 2337 | }, 2338 | "d4fbba9350c241fd8da9d1b0683c717a": { 2339 | "model_module": "@jupyter-widgets/controls", 2340 | "model_module_version": "1.5.0", 2341 | "model_name": "SliderStyleModel", 2342 | "state": { 2343 | "description_width": "" 2344 | } 2345 | }, 2346 | "d5590832e7ee4ecdac5e08ee239b1e06": { 2347 | "model_module": "@jupyter-widgets/controls", 2348 | "model_module_version": "1.5.0", 2349 | "model_name": "SliderStyleModel", 2350 | "state": { 2351 | "description_width": "" 2352 | } 2353 | }, 2354 | "d5f57217e7fb46d7b4ede486f980e8a2": { 2355 | "model_module": "@jupyter-widgets/controls", 2356 | "model_module_version": "1.5.0", 2357 | "model_name": "SliderStyleModel", 2358 | "state": { 2359 | "description_width": "" 2360 | } 2361 | }, 2362 | "d66cc8c2add04244aca2c60999302369": { 2363 | "model_module": "@jupyter-widgets/controls", 2364 | "model_module_version": "1.5.0", 2365 | "model_name": "VBoxModel", 2366 | "state": { 2367 | "children": [ 2368 | "IPY_MODEL_36d5799c6aed468fa996960aaa7c7493", 2369 | "IPY_MODEL_42f40d1570454ce3ac8d1327f15eebea" 2370 | ], 2371 | "layout": "IPY_MODEL_6a89089153c24c8687faa720f2d84b93" 2372 | } 2373 | }, 2374 | "d6cbc6a88ba94fde830221ea8b6cba5a": { 2375 | "model_module": "@jupyter-widgets/controls", 2376 | "model_module_version": "1.5.0", 2377 | "model_name": "SliderStyleModel", 2378 | "state": { 2379 | "description_width": "" 2380 | } 2381 | }, 2382 | "d725c694d5c0448f8366761bd4a6f225": { 2383 | "model_module": "@jupyter-widgets/controls", 2384 | "model_module_version": "1.5.0", 2385 | "model_name": "PlayModel", 2386 | "state": { 2387 | "interval": 1000, 2388 | "layout": "IPY_MODEL_5928e8ae503540868d12d94fedd71dda", 2389 | "min": 1, 2390 | "style": "IPY_MODEL_c3afe4a448fc452495a8e1b6e1b2dbb6", 2391 | "value": 18 2392 | } 2393 | }, 2394 | "d84b849a8de5415bbccbcd40878e0e51": { 2395 | "model_module": "@jupyter-widgets/controls", 2396 | "model_module_version": "1.5.0", 2397 | "model_name": "SliderStyleModel", 2398 | "state": { 2399 | "description_width": "" 2400 | } 2401 | }, 2402 | "da194ba0933b48529f5be949ca406377": { 2403 | "model_module": "@jupyter-widgets/base", 2404 | "model_module_version": "1.2.0", 2405 | "model_name": "LayoutModel", 2406 | "state": {} 2407 | }, 2408 | "dfe94442f7284d8a8edabc32aa688c57": { 2409 | "model_module": "@jupyter-widgets/controls", 2410 | "model_module_version": "1.5.0", 2411 | "model_name": "SliderStyleModel", 2412 | "state": { 2413 | "description_width": "" 2414 | } 2415 | }, 2416 | "e0ad4dceb76449c4b5363fc0dc71950c": { 2417 | "model_module": "@jupyter-widgets/controls", 2418 | "model_module_version": "1.5.0", 2419 | "model_name": "LinkModel", 2420 | "state": { 2421 | "source": [ 2422 | "IPY_MODEL_6700248decd14e06a21b7b224b3e73f7", 2423 | "value" 2424 | ], 2425 | "target": [ 2426 | "IPY_MODEL_ccf091fdcfc64566832baf6a16fb315f", 2427 | "value" 2428 | ] 2429 | } 2430 | }, 2431 | "e2ed718b0ca24bb8876338c61275874e": { 2432 | "model_module": "@jupyter-widgets/controls", 2433 | "model_module_version": "1.5.0", 2434 | "model_name": "DescriptionStyleModel", 2435 | "state": { 2436 | "description_width": "" 2437 | } 2438 | }, 2439 | "e3ddd647b1704639b33f0931c2710a4e": { 2440 | "model_module": "@jupyter-widgets/controls", 2441 | "model_module_version": "1.5.0", 2442 | "model_name": "VBoxModel", 2443 | "state": { 2444 | "children": [ 2445 | "IPY_MODEL_90a05fc2c083438ab03437482b6cd390", 2446 | "IPY_MODEL_4300d005e9854e9c9f36e64469bb7e11" 2447 | ], 2448 | "layout": "IPY_MODEL_d4609d8834e04ab2ab48479e88e55a58" 2449 | } 2450 | }, 2451 | "e51b297e19b243d4a56431fe70c0690f": { 2452 | "model_module": "@jupyter-widgets/controls", 2453 | "model_module_version": "1.5.0", 2454 | "model_name": "VBoxModel", 2455 | "state": { 2456 | "children": [ 2457 | "IPY_MODEL_6ca60fed3e8548cfadb18875da255bb3", 2458 | "IPY_MODEL_8c4a6c08eecb476a9b9fa5076a94c5cd" 2459 | ], 2460 | "layout": "IPY_MODEL_a61fd02ee2c54be1b3cbd79df9ef92d6" 2461 | } 2462 | }, 2463 | "e541c9408b364de9b8ca466cbae8aee0": { 2464 | "model_module": "@jupyter-widgets/controls", 2465 | "model_module_version": "1.5.0", 2466 | "model_name": "VBoxModel", 2467 | "state": { 2468 | "children": [ 2469 | "IPY_MODEL_f90ddba3f7404005aa1f43e3ba31f21b", 2470 | "IPY_MODEL_9cc1625077584709a88770648f8e2335" 2471 | ], 2472 | "layout": "IPY_MODEL_f4258931adfa4977b00ff504ceb4eb0f" 2473 | } 2474 | }, 2475 | "e88e93e19ec04c87865f802688097090": { 2476 | "model_module": "@jupyter-widgets/base", 2477 | "model_module_version": "1.2.0", 2478 | "model_name": "LayoutModel", 2479 | "state": {} 2480 | }, 2481 | "ec331978ce9b4f4c9a0f00578b7a4ef7": { 2482 | "model_module": "@jupyter-widgets/controls", 2483 | "model_module_version": "1.5.0", 2484 | "model_name": "IntSliderModel", 2485 | "state": { 2486 | "description": "timestep", 2487 | "layout": "IPY_MODEL_d1e3aef6acbe453899d61b720e5a92d6", 2488 | "max": 14, 2489 | "style": "IPY_MODEL_8fa7cd03684b413187ea3bb3cbfe75a7", 2490 | "value": 7 2491 | } 2492 | }, 2493 | "ee36997568954e41885387cabbe29d6a": { 2494 | "model_module": "@jupyter-widgets/base", 2495 | "model_module_version": "1.2.0", 2496 | "model_name": "LayoutModel", 2497 | "state": {} 2498 | }, 2499 | "eee4ba0a606f4d919477b41c220df12f": { 2500 | "model_module": "@jupyter-widgets/controls", 2501 | "model_module_version": "1.5.0", 2502 | "model_name": "SliderStyleModel", 2503 | "state": { 2504 | "description_width": "" 2505 | } 2506 | }, 2507 | "ef1f8c53f21c4b7c97df157b419994b9": { 2508 | "model_module": "@jupyter-widgets/controls", 2509 | "model_module_version": "1.5.0", 2510 | "model_name": "LinkModel", 2511 | "state": { 2512 | "source": [ 2513 | "IPY_MODEL_9c8121a5398b4e4597c03e1456723c55", 2514 | "value" 2515 | ], 2516 | "target": [ 2517 | "IPY_MODEL_fa95b56b0fd44b3e824addaf8f818743", 2518 | "value" 2519 | ] 2520 | } 2521 | }, 2522 | "f1787ab740f345d19f568d7ac5fdb432": { 2523 | "model_module": "@jupyter-widgets/base", 2524 | "model_module_version": "1.2.0", 2525 | "model_name": "LayoutModel", 2526 | "state": {} 2527 | }, 2528 | "f19ae5f338ae4e2fa7925f6745488878": { 2529 | "model_module": "@jupyter-widgets/base", 2530 | "model_module_version": "1.2.0", 2531 | "model_name": "LayoutModel", 2532 | "state": {} 2533 | }, 2534 | "f2e87c45ddfa4aaabcd8d4e1e623b500": { 2535 | "model_module": "@jupyter-widgets/controls", 2536 | "model_module_version": "1.5.0", 2537 | "model_name": "DescriptionStyleModel", 2538 | "state": { 2539 | "description_width": "" 2540 | } 2541 | }, 2542 | "f4258931adfa4977b00ff504ceb4eb0f": { 2543 | "model_module": "@jupyter-widgets/base", 2544 | "model_module_version": "1.2.0", 2545 | "model_name": "LayoutModel", 2546 | "state": {} 2547 | }, 2548 | "f44dcaf5ef894b7c9e1ababd1a1bfabc": { 2549 | "model_module": "@jupyter-widgets/controls", 2550 | "model_module_version": "1.5.0", 2551 | "model_name": "IntSliderModel", 2552 | "state": { 2553 | "description": "timestep", 2554 | "layout": "IPY_MODEL_cba32987459441a8948ec3715e7aaf16", 2555 | "max": 49, 2556 | "style": "IPY_MODEL_5a5ab206c5e44ccc9bb66edee98c1169" 2557 | } 2558 | }, 2559 | "f4c053ae387a4777accfcb3ca72a2bab": { 2560 | "model_module": "@jupyter-widgets/base", 2561 | "model_module_version": "1.2.0", 2562 | "model_name": "LayoutModel", 2563 | "state": {} 2564 | }, 2565 | "f53067e7b1ca439f864a73f269ea8ae0": { 2566 | "model_module": "@jupyter-widgets/base", 2567 | "model_module_version": "1.2.0", 2568 | "model_name": "LayoutModel", 2569 | "state": {} 2570 | }, 2571 | "f5557fedda1c41a59c6db82bfc5f7ea4": { 2572 | "model_module": "@jupyter-widgets/base", 2573 | "model_module_version": "1.2.0", 2574 | "model_name": "LayoutModel", 2575 | "state": {} 2576 | }, 2577 | "f6665cdb7150447cbcb39773a101b19c": { 2578 | "model_module": "@jupyter-widgets/controls", 2579 | "model_module_version": "1.5.0", 2580 | "model_name": "LinkModel", 2581 | "state": { 2582 | "source": [ 2583 | "IPY_MODEL_c40f5e7444534af1ae2aba4d8f73a9ee", 2584 | "value" 2585 | ], 2586 | "target": [ 2587 | "IPY_MODEL_651a568f9cf5497a947403580244ba47", 2588 | "value" 2589 | ] 2590 | } 2591 | }, 2592 | "f75e937f05a44d13b1bc95852a1f990b": { 2593 | "model_module": "@jupyter-widgets/base", 2594 | "model_module_version": "1.2.0", 2595 | "model_name": "LayoutModel", 2596 | "state": {} 2597 | }, 2598 | "f7c499e22cd9462a9c11a9c8404b1efd": { 2599 | "model_module": "@jupyter-widgets/base", 2600 | "model_module_version": "1.2.0", 2601 | "model_name": "LayoutModel", 2602 | "state": {} 2603 | }, 2604 | "f846270746204e8abe73c686d131d894": { 2605 | "model_module": "@jupyter-widgets/controls", 2606 | "model_module_version": "1.5.0", 2607 | "model_name": "VBoxModel", 2608 | "state": { 2609 | "children": [ 2610 | "IPY_MODEL_c0aa7d49770a4c5dbd58af0c5d419737", 2611 | "IPY_MODEL_12a763f92a0d4497a142d57b6919b67c" 2612 | ], 2613 | "layout": "IPY_MODEL_75fb258a84f545e7949c271c8655e957" 2614 | } 2615 | }, 2616 | "f90ddba3f7404005aa1f43e3ba31f21b": { 2617 | "model_module": "@jupyter-widgets/controls", 2618 | "model_module_version": "1.5.0", 2619 | "model_name": "PlayModel", 2620 | "state": { 2621 | "interval": 500, 2622 | "layout": "IPY_MODEL_99b805c2a0144d31849fce4d65746d10", 2623 | "min": 1, 2624 | "style": "IPY_MODEL_54b89a7b641c436bb2617f1a6278b314", 2625 | "value": 1 2626 | } 2627 | }, 2628 | "f9dd6140df4d408ea0cd6310875c793b": { 2629 | "model_module": "@jupyter-widgets/controls", 2630 | "model_module_version": "1.5.0", 2631 | "model_name": "LinkModel", 2632 | "state": { 2633 | "source": [ 2634 | "IPY_MODEL_5ebb17b54836406288bda29694194d41", 2635 | "value" 2636 | ], 2637 | "target": [ 2638 | "IPY_MODEL_cc6e3ce1e1ee40cfbe3cc0c29b28f2d4", 2639 | "value" 2640 | ] 2641 | } 2642 | }, 2643 | "fa28aeb9fb9949d2ad4dc3bcecf42da1": { 2644 | "model_module": "@jupyter-widgets/controls", 2645 | "model_module_version": "1.5.0", 2646 | "model_name": "VBoxModel", 2647 | "state": { 2648 | "_dom_classes": [ 2649 | "widget-interact" 2650 | ], 2651 | "children": [ 2652 | "IPY_MODEL_f44dcaf5ef894b7c9e1ababd1a1bfabc", 2653 | "IPY_MODEL_b85d0e804aae4a258fcc0f8a70224e11" 2654 | ], 2655 | "layout": "IPY_MODEL_75a4851cbe7a4f4e97c526e46ef913f1" 2656 | } 2657 | }, 2658 | "fa95b56b0fd44b3e824addaf8f818743": { 2659 | "model_module": "@jupyter-widgets/controls", 2660 | "model_module_version": "1.5.0", 2661 | "model_name": "IntSliderModel", 2662 | "state": { 2663 | "layout": "IPY_MODEL_bcc064ba20594a9c8d88b254777fdd46", 2664 | "max": 49, 2665 | "min": 1, 2666 | "style": "IPY_MODEL_03ef88295e5741f6a1a04ce029bca410", 2667 | "value": 8 2668 | } 2669 | }, 2670 | "fd5e5a4ca31e410bb0d3cb46473306ce": { 2671 | "model_module": "@jupyter-widgets/base", 2672 | "model_module_version": "1.2.0", 2673 | "model_name": "LayoutModel", 2674 | "state": {} 2675 | }, 2676 | "feb176aad55d46498da4ee0b3dc5865f": { 2677 | "model_module": "@jupyter-widgets/base", 2678 | "model_module_version": "1.2.0", 2679 | "model_name": "LayoutModel", 2680 | "state": {} 2681 | } 2682 | }, 2683 | "version_major": 2, 2684 | "version_minor": 0 2685 | } 2686 | } 2687 | }, 2688 | "nbformat": 4, 2689 | "nbformat_minor": 4 2690 | } 2691 | -------------------------------------------------------------------------------- /Chapter_2_Elements_of_matrix_theory.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "toc": true 7 | }, 8 | "source": [ 9 | "

Table of Contents

\n", 10 | "
" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "%matplotlib widget\n", 20 | "\n", 21 | "# Import packages\n", 22 | "import numpy as np\n", 23 | "import matplotlib.pyplot as plt\n", 24 | "import matplotlib as mpl\n", 25 | "import networkx as nx\n", 26 | "import scipy.linalg as spla\n", 27 | "from sympy import Matrix\n", 28 | "\n", 29 | "# For interactive graphs\n", 30 | "import ipywidgets as widgets\n", 31 | "\n", 32 | "# Import self defined functions\n", 33 | "import lib # General library\n", 34 | "\n", 35 | "# Settings\n", 36 | "custom_figsize= (6, 4) # Might need to change this value to fit the figures to your screen\n", 37 | "custom_figsize_square = (5, 5) " 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "# Chapter 2 - Elements of Matrix Theory\n", 45 | "These Jupyter Notebook scripts contain some examples, visualization and supplements accompanying the book \"Lectures on Network Systems\" by Francesco Bullo http://motion.me.ucsb.edu/book-lns/. These scripts are published with the MIT license. **Make sure to run the first cell above to import all necessary packages and functions and adapt settings in case.** In this script it is necessary to execute cell by cell chronologically due to reocurring examples (excepts for e.g. the Exercises in the end). (Tip: Use the shortcut Shift+Enter to execute each cell). Most of the functions are kept in separate files to keep this script neat." 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "## 2.1.2 The Jordan Normal Form\n", 53 | "### Example 2.5 Revisiting the wireless sensor network example\n", 54 | "The following cells are showing the computation of the Jordan Normal Form $J$, the invertible transformation matrix $T$ and some of its dependencies. " 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "# Defining the A matrix again\n", 64 | "A = np.array([[1/2, 1/2, 0., 0.],\n", 65 | " [1/4, 1/4, 1/4, 1/4],\n", 66 | " [0., 1/3, 1/3, 1/3],\n", 67 | " [0., 1/3, 1/3, 1/3]\n", 68 | "])" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "There is the possibility to calculate the Jordan Normal Form directly with the package SymPy https://docs.sympy.org/latest/index.html. However, we are determining the Jordan Normal Form via determining the generalized eigenvectors (read more for literature recommendations about generalized eigenvectors in the book) with the SciPy package first to discuss some possibilities and problems with non symbolic toolboxes.\n", 76 | "\n", 77 | "### NumPy/ SciPy approach\n", 78 | "\n", 79 | "From the documentation of scipy.linalg.eig: *'Solve an ordinary or generalized eigenvalue problem of a square matrix.'*" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "# Right eigenvectors\n", 89 | "lambdas, eigv = spla.eig(A)\n", 90 | "\n", 91 | "# Left eigenvectors\n", 92 | "lambdas2, eigw = spla.eig(A.T)" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "Due to numerical instabilities, the zero values are not reflected and it can be seen, how the expected eigenvalue of 1 is not precise. The zeros can be fixed with:" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "def correct_close_to_zero(M, tol=1e-12):\n", 109 | " M.real[abs(M.real) < tol] = 0.0\n", 110 | " if M.imag.any():\n", 111 | " M.imag[abs(M.imag) < tol] = 0.0\n", 112 | " return M\n", 113 | "\n", 114 | "eigv_cor = correct_close_to_zero(eigv)\n", 115 | "eigw_cor = correct_close_to_zero(eigw)\n", 116 | "lambdas_cor = correct_close_to_zero(lambdas)\n", 117 | "lambdas2_cor = correct_close_to_zero(lambdas2)\n", 118 | "\n", 119 | "print(\"Right eigenvectors:\")\n", 120 | "lib.matprint(eigv_cor)\n", 121 | "print(\"\\n\")\n", 122 | "print(\"Left eigenvectors:\")\n", 123 | "lib.matprint(eigw_cor)\n", 124 | "print(\"\\n\")\n", 125 | "print(\"Eigenvalues (right):\")\n", 126 | "lib.matprint(lambdas_cor)\n", 127 | "print(\"\\n\")\n", 128 | "print(\"Eigenvalues (left) for matching later:\")\n", 129 | "lib.matprint(lambdas2_cor)" 130 | ] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "metadata": {}, 135 | "source": [ 136 | "There are two options now for $T^{-1}$: Taking the inverse of the right eigenvectors (which contains again numerical instabilities) or building it from the left eigenvectors, what would include some sorting to match the eigenvalue order from the right eigenvector (often it is the case, that they are already aligned since calling scipy.linalg.eig twice on a matrix with the same eigenvalues)." 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "T = eigv_cor.copy()*-1 # Rescale the eigenvectors to match eigenvalues later\n", 146 | "# Sorting if necessary, remember to use transpose, since in T^-1 the rows represent the left eigenvectors.\n", 147 | "Tinv = eigw_cor.T.copy()" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "Now we can simply compute J, when compared, is fairly close to the solution in the book, however, due to numerical intabilities not precise. Further on, the order of the eigenvalues might be different than the on from the book." 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": null, 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "J = correct_close_to_zero(Tinv@A@T)\n", 164 | "print(\"Jordan Normal Form via SciPy/Numpy:\")\n", 165 | "lib.matprint(J)" 166 | ] 167 | }, 168 | { 169 | "cell_type": "markdown", 170 | "metadata": {}, 171 | "source": [ 172 | "### SymPy approach\n", 173 | "\n", 174 | "Now we use a symbolic toolbox package SymPy from python as a blackbox. Note, that also here the order of the eigenvalues might be different!" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": null, 180 | "metadata": {}, 181 | "outputs": [], 182 | "source": [ 183 | "Asym = Matrix(A) # Sympy Matrix toolbox object\n", 184 | "Tsym, Jsym = Asym.jordan_form()" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "Here we can compare them with our previous results:" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": null, 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "print(\"Jordan Normal Form SymPy:\")\n", 201 | "lib.matprint(np.array(Jsym).astype(np.float64))\n", 202 | "print(\"Jordan Normal Form SciPy:\")\n", 203 | "lib.matprint(J)" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "metadata": {}, 209 | "source": [ 210 | "## 2.1.3 Semi-convergence and convergence for discrete-time linear systems\n" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "metadata": {}, 216 | "source": [ 217 | "### Definition 2.6 (Spectrum and spectral radius of a matrix)\n", 218 | "We display the spectrum of the previous A matrix with the spectrum radius for visualization purpose. Additionally, we also show how the spectrum of a randomly generated matrix." 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": null, 224 | "metadata": {}, 225 | "outputs": [], 226 | "source": [ 227 | "fig, ax213 = plt.subplots(figsize=custom_figsize_square)\n", 228 | "lib.plot_spectrum(A, ax213);" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": null, 234 | "metadata": {}, 235 | "outputs": [], 236 | "source": [ 237 | "n_M1=8\n", 238 | "\n", 239 | "# A unifornmly distributed, positive, row stochastic matrix vs not row stochastic\n", 240 | "M1 = np.random.uniform(0, 1,(n_M1,n_M1))\n", 241 | "M1 = M1 / M1.sum(axis=1, keepdims=1) # Row-stochastic\n", 242 | "M2 = M1 - 0.05 # Not row-stochastic\n", 243 | "\n", 244 | "fig, (ax2131, ax2132) = plt.subplots(1,2, figsize=(custom_figsize_square[0]*2, custom_figsize_square[1]))\n", 245 | "lib.plot_spectrum(M1, ax2131);\n", 246 | "lib.plot_spectrum(M2, ax2132);" 247 | ] 248 | }, 249 | { 250 | "cell_type": "markdown", 251 | "metadata": {}, 252 | "source": [ 253 | "## 2.2.1 The spectral radius for row-stochastic matrices\n", 254 | "### Theorem 2.8 (Geršgorin Disks Theorem)\n", 255 | "\n", 256 | "Similar to before, the Geršgorin Disks are now visualized for a row-stochastic matrix and another matrix." 257 | ] 258 | }, 259 | { 260 | "cell_type": "code", 261 | "execution_count": null, 262 | "metadata": {}, 263 | "outputs": [], 264 | "source": [ 265 | "fig, (ax2211, ax2212) = plt.subplots(1,2, figsize=(custom_figsize_square[0]*2, custom_figsize_square[1]))\n", 266 | "lib.plot_gersgorin_disks(M1, ax2211)\n", 267 | "lib.plot_gersgorin_disks(M2, ax2212)" 268 | ] 269 | }, 270 | { 271 | "cell_type": "markdown", 272 | "metadata": {}, 273 | "source": [ 274 | "## 2.3.3 Applications to matrix powers and averaging systems\n", 275 | "\n", 276 | "### Theorem 2.13 (Powers of non-negative matrices with a simple and strictly dominant eigenvalue)\n", 277 | "\n", 278 | "Here is an example for Theorem 2.13, which shows, how the powers of primitive, row-stochastic matrices converges to rank 1. This is also done for the wireless sensor network example.\n", 279 | "\n", 280 | "#### Example 2.14 Wireless sensor network\n", 281 | "In the book it is shown, that the wireless sensor network matrix is primitive. Here, the eigenvectors and eigenvalues are printed again and compared for the semi convergence result $\\lim_{k \\to \\infty} A^k = \\mathbb{1}_n w^T$ to demonstrate Theorem 2.13 for a row-stochastic matrix. " 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": null, 287 | "metadata": {}, 288 | "outputs": [], 289 | "source": [ 290 | "print(\"Left eigenvectors of A:\")\n", 291 | "lib.matprint(eigw_cor)\n", 292 | "print(\"\\n\")\n", 293 | "\n", 294 | "print(\"Eigenvalues (left) of A:\")\n", 295 | "lib.matprint(lambdas2_cor)\n", 296 | "print(\"\\n\")\n", 297 | "\n", 298 | "print(\"Normalizing dominant eigenvector:\")\n", 299 | "dom_eigv = eigw_cor[:, 0] / sum(eigw_cor[:, 0])\n", 300 | "lib.matprint(dom_eigv)\n", 301 | "print(\"\\n\")\n", 302 | "\n", 303 | "print(\"Convergence result of A:\")\n", 304 | "lib.matprint(np.linalg.matrix_power(A, 50))\n", 305 | "print(\"\\n\")\n", 306 | "\n", 307 | "print(\"equals 1n*w^T\")\n", 308 | "lib.matprint(np.ones((4,1))@dom_eigv[:, None].T)" 309 | ] 310 | }, 311 | { 312 | "cell_type": "markdown", 313 | "metadata": {}, 314 | "source": [ 315 | "Below is a randomly generated example to show, that primitive, row-stochastic matrices always converge to rank 1. *Note: The code is not robust for semisimple eigenvalue of 1*" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": null, 321 | "metadata": {}, 322 | "outputs": [], 323 | "source": [ 324 | "# Creating a new random primitive (positiv), row stochastic matrix here\n", 325 | "n_M11=5\n", 326 | "M11 = np.random.uniform(0, 1,(n_M11,n_M11))\n", 327 | "M11 = M11 / M11.sum(axis=1, keepdims=1) # Row-stochastic\n", 328 | "print(\"Random primitive row-stochastic matrix M:\")\n", 329 | "lib.matprint(M11)\n", 330 | "print(\"\\n\")\n", 331 | "\n", 332 | "print(\"Left eigenvectors of M:\")\n", 333 | "l_M, m_eigv = spla.eig(M11.T)\n", 334 | "m_eigv = correct_close_to_zero(m_eigv)\n", 335 | "l_M = correct_close_to_zero(l_M)\n", 336 | "lib.matprint(m_eigv)\n", 337 | "print(\"\\n\")\n", 338 | "\n", 339 | "print(\"Eigenvalues (left) of M:\")\n", 340 | "lib.matprint(l_M)\n", 341 | "print(\"\\n\")\n", 342 | "\n", 343 | "# Here we check the position with numerical unprecision of the eigenvalue 1\n", 344 | "print(\"Normalizing dominant eigenvector:\")\n", 345 | "idx_dom = np.where(abs(l_M - 1) < 0.005)[0][0]\n", 346 | "dom_eigv_M = m_eigv[:, 0] / sum(m_eigv[:, 0])\n", 347 | "lib.matprint(dom_eigv_M)\n", 348 | "print(\"\\n\")\n", 349 | "\n", 350 | "print(\"Convergence result of M:\")\n", 351 | "lib.matprint(np.linalg.matrix_power(M11, 500))\n", 352 | "print(\"\\n\")\n", 353 | "\n", 354 | "print(\"equals 1n*w^T\")\n", 355 | "lib.matprint(np.ones((n_M11,1))@dom_eigv_M[:, None].T)" 356 | ] 357 | }, 358 | { 359 | "cell_type": "markdown", 360 | "metadata": {}, 361 | "source": [ 362 | "## Exercises 2.18\n", 363 | "\n", 364 | "This section is similar to exercise 1.4, however, here we actually visualize the graph and its node values. Additionally, we show that the values converge to the initial values multiplied b the dominant left eigenvector as presented in Theorem 2.13 for row stochastic matrices.\n", 365 | "\n", 366 | "First, we define the graphs and their adjacency Matrix A and simulate the results. Then, for each graph a cell can be executed for the interactive visualization. A plot of the states is available in the Jupyter Notebook script for Chapter 1." 367 | ] 368 | }, 369 | { 370 | "cell_type": "code", 371 | "execution_count": null, 372 | "metadata": {}, 373 | "outputs": [], 374 | "source": [ 375 | "# Define x_0\n", 376 | "xinitial = np.array([1., -1., 1., -1., 1.])\n", 377 | "\n", 378 | "# Defining the 3 different systems.\n", 379 | "# Complete graph\n", 380 | "A_complete = np.ones((5,5)) / 5\n", 381 | "\n", 382 | "# Cycle graph\n", 383 | "A_cycle = np.array([\n", 384 | " [1/3, 1/3, 0, 0, 1/3],\n", 385 | " [1/3, 1/3, 1/3, 0, 0],\n", 386 | " [0, 1/3, 1/3, 1/3, 0],\n", 387 | " [0, 0, 1/3, 1/3, 1/3],\n", 388 | " [1/3, 0, 0, 1/3, 1/3] ] )\n", 389 | "\n", 390 | "\n", 391 | "# Star topology. center = node 1\n", 392 | "A_star = np.array([\n", 393 | " [1/5, 1/5, 1/5, 1/5, 1/5],\n", 394 | " [1/2, 1/2, 0, 0, 0], \n", 395 | " [1/2, 0, 1/2, 0, 0],\n", 396 | " [1/2, 0, 0, 1/2, 0],\n", 397 | " [1/2, 0, 0, 0, 1/2] ])\n", 398 | "\n", 399 | "\n", 400 | "# Defining simulation time\n", 401 | "ts = 15\n", 402 | "\n", 403 | "# Defining graphs for plotting later\n", 404 | "n = 5\n", 405 | "G_star = nx.star_graph(n-1)\n", 406 | "pos_star = {0:[0.5,0.8], 1:[0.2,0.6],2:[.4,.2],3:[.6,.2],4:[.8,.6]}\n", 407 | "G_cycle = nx.cycle_graph(n)\n", 408 | "pos_cycle = {0:[0.5,0.8], 1:[0.35,0.6],2:[.4,.3],3:[.6,.3],4:[.65,.6]}\n", 409 | "G_complete = nx.complete_graph(n)\n", 410 | "pos_complete = pos_cycle.copy()\n", 411 | "\n", 412 | "# Simulating and saving each network\n", 413 | "states_complete = lib.simulate_network(A_complete,xinitial, ts)\n", 414 | "states_star = lib.simulate_network(A_star,xinitial, ts)\n", 415 | "states_cycle = lib.simulate_network(A_cycle,xinitial, ts)" 416 | ] 417 | }, 418 | { 419 | "cell_type": "markdown", 420 | "metadata": {}, 421 | "source": [ 422 | "**Complete graph**\n", 423 | "\n", 424 | "Showing complete graph interactive simulation and Theorem 2.13" 425 | ] 426 | }, 427 | { 428 | "cell_type": "code", 429 | "execution_count": null, 430 | "metadata": {}, 431 | "outputs": [], 432 | "source": [ 433 | "fig, ax2181 = plt.subplots(figsize=custom_figsize)\n", 434 | "\n", 435 | "# If this cell is executed twice we are making sure in the following, that the previous widget instances are all closed\n", 436 | "try:\n", 437 | " [c.close() for c in widget2181.children] # Note: close_all() does also affect plot, thus list compr.\n", 438 | "except NameError: # Only want to except not defined variable error\n", 439 | " pass\n", 440 | "\n", 441 | "widget2181 = lib.interactive_network_plot(G_complete, states_complete, pos_complete, ts, fig, ax2181)\n", 442 | "\n", 443 | "display(widget2181)\n", 444 | "\n", 445 | "# Verifying the results\n", 446 | "eigval, eigvec = np.linalg.eig(A_complete.transpose())\n", 447 | "idx_dom = np.argmax(eigval)\n", 448 | "dom_eigvec = eigvec[0:5,idx_dom]/eigvec[0:5,idx_dom].sum()\n", 449 | "print(\"Showing Theorem 2.13 for the complete graph\")\n", 450 | "print(\"Dominant eigenvector: \\n\", dom_eigvec)\n", 451 | "print(\"Final values : \\n\", xinitial@dom_eigvec*np.ones(5))" 452 | ] 453 | }, 454 | { 455 | "cell_type": "markdown", 456 | "metadata": {}, 457 | "source": [ 458 | "**Star graph**\n", 459 | "\n", 460 | "Showing star graph interactive simulation and Theorem 2.13" 461 | ] 462 | }, 463 | { 464 | "cell_type": "code", 465 | "execution_count": null, 466 | "metadata": {}, 467 | "outputs": [], 468 | "source": [ 469 | "fig, ax2182 = plt.subplots(figsize=custom_figsize)\n", 470 | "\n", 471 | "# If this cell is executed twice we are making sure in the following, that the previous widget instances are all closed\n", 472 | "try:\n", 473 | " [c.close() for c in widget2182.children] # Note: close_all() does also affect plot, thus list compr.\n", 474 | "except NameError: # Only want to except not defined variable error\n", 475 | " pass\n", 476 | "\n", 477 | "widget2182 = lib.interactive_network_plot(G_star, states_star, pos_star, ts, fig, ax2182)\n", 478 | "\n", 479 | "display(widget2182)\n", 480 | "\n", 481 | "# Verifying the results\n", 482 | "eigval, eigvec = np.linalg.eig(A_star.transpose() )\n", 483 | "idx_dom = np.argmax(eigval)\n", 484 | "dom_eigvec = eigvec[0:5,idx_dom]/eigvec[0:5,idx_dom].sum()\n", 485 | "print(\"Showing Theorem 2.13 for the star graph\")\n", 486 | "print(\"Dominant eigenvector: \\n\", dom_eigvec)\n", 487 | "print(\"Final values : \\n\", xinitial@dom_eigvec*np.ones(5))" 488 | ] 489 | }, 490 | { 491 | "cell_type": "markdown", 492 | "metadata": {}, 493 | "source": [ 494 | "**Cycle graph**\n", 495 | "\n", 496 | "Showing cycle graph interactive simulation and Theorem 2.13" 497 | ] 498 | }, 499 | { 500 | "cell_type": "code", 501 | "execution_count": null, 502 | "metadata": {}, 503 | "outputs": [], 504 | "source": [ 505 | "fig, ax2183 = plt.subplots(figsize=custom_figsize)\n", 506 | "\n", 507 | "# If this cell is executed twice we are making sure in the following, that the previous widget instances are all closed\n", 508 | "try:\n", 509 | " [c.close() for c in widget2183.children] # Note: close_all() does also affect plot, thus list compr.\n", 510 | "except NameError: # Only want to except not defined variable error\n", 511 | " pass\n", 512 | "\n", 513 | "widget2183 = lib.interactive_network_plot(G_cycle, states_cycle, pos_cycle, ts, fig, ax2183)\n", 514 | "\n", 515 | "display(widget2183)\n", 516 | "\n", 517 | "# Verifying the results\n", 518 | "eigval, eigvec = np.linalg.eig(A_cycle.transpose())\n", 519 | "idx_dom = np.argmax(eigval)\n", 520 | "dom_eigvec = eigvec[0:5,idx_dom]/eigvec[0:5,idx_dom].sum()\n", 521 | "print(\"Showing Theorem 2.13 for the cycle graph\")\n", 522 | "print(\"Dominant eigenvector: \\n\", dom_eigvec)\n", 523 | "print(\"Final values : \\n\", xinitial@dom_eigvec*np.ones(5))" 524 | ] 525 | }, 526 | { 527 | "cell_type": "markdown", 528 | "metadata": {}, 529 | "source": [ 530 | "## Exercises 2.19\n", 531 | "\n", 532 | "This exercise is about $n$ robots moving on the line trying to gather at a common location (i.e., reach rendezvous), where each robot heads for the centroid of its neighbors. The visualization of the algorithm deals with the discrete part of the task, where on can explore values of the sampling period $T$ for the Euler discretization." 533 | ] 534 | }, 535 | { 536 | "cell_type": "code", 537 | "execution_count": null, 538 | "metadata": {}, 539 | "outputs": [], 540 | "source": [ 541 | "# Setup - change these parameters if wanted\n", 542 | "n_robots = 8\n", 543 | "# Number of timesteps and sampling period T (If T is too small, need very high n_dt)\n", 544 | "n_dt = 25\n", 545 | "T = 0.3 # Play around with this value, something interesting is happening around (2(n_robots-1)/n_robots)\n", 546 | "#T = 2*(n_robots-1)/n_robots\n", 547 | "\n", 548 | "# Set up initial position matrix and further saving variables\n", 549 | "current_positions = 2*np.random.random((n_robots,1))-1\n", 550 | "new_position = current_positions.copy()\n", 551 | "all_positions = np.zeros((n_dt, n_robots, 1))\n", 552 | "all_positions[0] = current_positions.copy()\n", 553 | "\n", 554 | "for tt in range(1, n_dt):\n", 555 | " for index, own_pos in np.ndenumerate(current_positions):\n", 556 | " new_position[index] = own_pos + T*(1/(n_robots-1)*(np.sum(current_positions)-own_pos) - own_pos)\n", 557 | " all_positions[tt] = new_position.copy()\n", 558 | " current_positions = new_position.copy()" 559 | ] 560 | }, 561 | { 562 | "cell_type": "code", 563 | "execution_count": null, 564 | "metadata": { 565 | "scrolled": false 566 | }, 567 | "outputs": [], 568 | "source": [ 569 | "fig, ax219 = plt.subplots(figsize=custom_figsize)\n", 570 | "# Set colors of robots for tracking\n", 571 | "all_colors = np.random.rand(n_robots,3)\n", 572 | "\n", 573 | "\n", 574 | "def plot_robot_pos(ax, pos):\n", 575 | " # Set xlim, ylim and aspect ratio\n", 576 | " ax219.set_xlim(-1.5, 1.5)\n", 577 | " ax219.set_ylim(-0.5, 0.5)\n", 578 | " ax219.set_aspect('equal')\n", 579 | " # Add horizontal line\n", 580 | " ax219.axhline(y=0.0, color='k', linestyle='-')\n", 581 | " for i in range(0, pos.shape[0]):\n", 582 | " # Add a robot as circle\n", 583 | " bug = mpl.patches.Circle((pos[i], 0), radius=0.06, ec='black', color=all_colors[i])\n", 584 | " ax.add_patch(bug)\n", 585 | "\n", 586 | "def interactive_robots(timestep):\n", 587 | " ax219.clear()\n", 588 | " plot_robot_pos(ax219, all_positions[timestep['new'], :]) # Take the new value received from the slider dict \n", 589 | " return None\n", 590 | "\n", 591 | "# Plot initial configuration\n", 592 | "plot_robot_pos(ax219, all_positions[0, :])\n", 593 | "\n", 594 | "# Widget\n", 595 | "# If this cell is executed twice we are making sure in the following, that the previous widget instances are all closed\n", 596 | "try:\n", 597 | " [c.close() for c in widget219.children] # Note: close_all() does also affect plot, thus list compr.\n", 598 | "except NameError: # Only want to except not defined variable error\n", 599 | " pass\n", 600 | "\n", 601 | "widget219 = lib.create_widgets_play_slider(fnc=interactive_robots, minv=0, maxv=n_dt-1, step=1, play_speed=500)\n", 602 | "\n", 603 | "display(widget219)" 604 | ] 605 | }, 606 | { 607 | "cell_type": "markdown", 608 | "metadata": {}, 609 | "source": [ 610 | "We can even compute the convergence solution in advance. Please refer and solve the given Excersice first to try to understand the following calculations." 611 | ] 612 | }, 613 | { 614 | "cell_type": "code", 615 | "execution_count": null, 616 | "metadata": {}, 617 | "outputs": [], 618 | "source": [ 619 | "A_robot = 1/(n_robots-1) * np.ones((n_robots, n_robots)) - n_robots/(n_robots-1)*np.identity(n_robots)\n", 620 | "eigval, eigvec = np.linalg.eig(A_robot)\n", 621 | "idx = np.argmin(abs(eigval))\n", 622 | "z_eigvec = eigvec[:,idx]/np.sqrt(np.sum(eigvec[:,idx]**2))\n", 623 | "\n", 624 | "final_values = z_eigvec[None, :] @ all_positions[0] @ z_eigvec[None, :]\n", 625 | "print(\"Final values :\", final_values)" 626 | ] 627 | }, 628 | { 629 | "cell_type": "code", 630 | "execution_count": null, 631 | "metadata": {}, 632 | "outputs": [], 633 | "source": [] 634 | } 635 | ], 636 | "metadata": { 637 | "kernelspec": { 638 | "display_name": "Python 3 (ipykernel)", 639 | "language": "python", 640 | "name": "python3" 641 | }, 642 | "language_info": { 643 | "codemirror_mode": { 644 | "name": "ipython", 645 | "version": 3 646 | }, 647 | "file_extension": ".py", 648 | "mimetype": "text/x-python", 649 | "name": "python", 650 | "nbconvert_exporter": "python", 651 | "pygments_lexer": "ipython3", 652 | "version": "3.8.11" 653 | }, 654 | "toc": { 655 | "base_numbering": 1, 656 | "nav_menu": {}, 657 | "number_sections": false, 658 | "sideBar": true, 659 | "skip_h1_title": false, 660 | "title_cell": "Table of Contents", 661 | "title_sidebar": "Contents", 662 | "toc_cell": true, 663 | "toc_position": {}, 664 | "toc_section_display": true, 665 | "toc_window_display": false 666 | } 667 | }, 668 | "nbformat": 4, 669 | "nbformat_minor": 4 670 | } 671 | -------------------------------------------------------------------------------- /Chapter_3_Elements_of_graph_theory.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "e36f992f", 6 | "metadata": { 7 | "toc": true 8 | }, 9 | "source": [ 10 | "

Table of Contents

\n", 11 | "
" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "239dc9b9", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "%matplotlib widget\n", 22 | "\n", 23 | "# Import packages\n", 24 | "import numpy as np\n", 25 | "import matplotlib.pyplot as plt\n", 26 | "import networkx as nx\n", 27 | "# For interactive graphs\n", 28 | "import ipywidgets as widgets\n", 29 | "\n", 30 | "# Import self defined functions\n", 31 | "import lib # General library\n", 32 | "\n", 33 | "# Settings\n", 34 | "custom_figsize= (6, 4) # Might need to change this value to fit the figures to your screen\n", 35 | "custom_figsize_square = (5, 5) " 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "id": "956024aa", 41 | "metadata": {}, 42 | "source": [ 43 | "# Chapter 3 - Elements of Graph Theory\n", 44 | "These Jupyter Notebook scripts contain some examples, visualization and supplements accompanying the book \"Lectures on Network Systems\" by Francesco Bullo http://motion.me.ucsb.edu/book-lns/. These scripts are published with the MIT license. **Make sure to run the first cell above to import all necessary packages and functions and adapt settings in case.** In this script it is necessary to execute cell by cell chronologically due to reocurring examples (Tip: Use the shortcut Shift+Enter to execute each cell). Most of the functions are kept in separate files to keep this script neat." 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "id": "628ad019", 50 | "metadata": {}, 51 | "source": [ 52 | "## 3.1 Graphs and Digraphs\n", 53 | "\n", 54 | "In this section the basic graphs are loaded and displayed with the package Networkx https://networkx.org/, which are presented in the book.\n", 55 | "\n", 56 | "Please note, without precise node positioning, Networkx will automatically chose a feasible and random node position for visualization purpose." 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "id": "c8b044bd", 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "n = 6\n", 67 | "# Path Graph\n", 68 | "G_path = nx.path_graph(n)\n", 69 | "# Cycle Graph\n", 70 | "G_cycle = nx.cycle_graph(n)\n", 71 | "# Star Graph\n", 72 | "G_star = nx.star_graph(n-1)\n", 73 | "# Complete Graph\n", 74 | "G_complete = nx.complete_graph(n)\n", 75 | "# Complete bipartite Graph\n", 76 | "G_bipartite = nx.complete_bipartite_graph(n//2, n//2)\n", 77 | "# Two dimensional grid graph\n", 78 | "G_grid = nx.generators.lattice.grid_2d_graph(n//2, n//2)\n", 79 | "# Petersen graph\n", 80 | "G_petersen = nx.petersen_graph()\n", 81 | "\n", 82 | "# Digraph Example from section 3.5 with weights, self_loops etc.\n", 83 | "G_di = nx.DiGraph()\n", 84 | "edges = [(1,2), (2,1), (2,4), (1,3), (3,5), (5,1), (5,5), (3,4), (5,4)]\n", 85 | "weights = [3.7, 8.9, 1.2, 2.6, 2.3, 4.4, 4.4, 1.9, 2.7]\n", 86 | "for edge, weight in zip(edges, weights):\n", 87 | " G_di.add_edge(*edge, weight=weight)\n", 88 | "pos_di = {1:[0.1,0.2],2:[.4,.5],3:[.5,.2],4:[.8,.5], 5:[.9,.2]} # Define position of nodes in digraph plot\n", 89 | "\n", 90 | "# Balanced Tree Graph\n", 91 | "G_tree = nx.balanced_tree(2, 2)\n", 92 | "\n", 93 | "all_graphs = {\"Path Graph\": G_path,\n", 94 | " \"Cycle Graph\": G_cycle,\n", 95 | " \"Star Graph\":G_star,\n", 96 | " \"Complete Graph\": G_complete,\n", 97 | " \"Complete bipartite Graph\": G_bipartite,\n", 98 | " \"Two-dim grid Graph\": G_grid,\n", 99 | " \"Petersen Graph\":G_petersen,\n", 100 | " \"Digraph Example\": G_di,\n", 101 | " \"Balanced Tree Graph\": G_tree\n", 102 | " }" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "id": "22efae94", 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "fig, axs = plt.subplots(3, 3, figsize=custom_figsize)\n", 113 | "for count, (key, graph) in enumerate(all_graphs.items()):\n", 114 | " axis = axs[count//3, count%3]\n", 115 | " # Here for example we make sure to visualize the bipartite graph or DiGraph nicely.\n", 116 | " if key == \"Complete bipartite Graph\":\n", 117 | " nx.draw_networkx(graph, node_size=100, ax=axis, pos=nx.drawing.layout.bipartite_layout(graph, list(range(0, n//2))))\n", 118 | " elif key == \"Digraph Example\":\n", 119 | " nx.draw_networkx(graph, node_size=100, ax=axis, pos=pos_di)\n", 120 | " else:\n", 121 | " nx.draw_networkx(graph, node_size=100, ax=axis)\n", 122 | " axis.set_xlabel(key)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "id": "0a0ba8d2", 128 | "metadata": {}, 129 | "source": [ 130 | "## 3.2 Neighbours\n", 131 | "In this section it is shown hot to access the neighbors and in and out degree of a specific node of a graph. Note that the given functions are different for undirected and directed graph.\n", 132 | "\n", 133 | "**Undirected Graph**" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "id": "d2d6ac3a", 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "# Numbers of neighbors for node 0 in cycle graph or in star graph\n", 144 | "print(\"Node labels of neighbors for node 0 in star graph: \\n\", *G_star.neighbors(0), \"\\n Total number: \\n\", len(list(G_star.neighbors(0))))\n", 145 | "print(\"Node labels of neighbors for node 0 in bipartite graph: \\n\", *G_bipartite.neighbors(0), \"\\n Total number: \\n\", len(list(G_bipartite.neighbors(0))))" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "id": "69fb8df5", 151 | "metadata": {}, 152 | "source": [ 153 | "**Directed Graph**" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": null, 159 | "id": "631d47f2", 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "# out- and in-degree functions returns for all nodes!\n", 164 | "print(\"Out-degree of DiGraph Example without weights in Format (Node, out-degree): \\n\" , G_di.out_degree())\n", 165 | "print(\"\\n In-degree of DiGraph Example without weights in Format (Node, in-degree): \\n\" , G_di.in_degree())" 166 | ] 167 | }, 168 | { 169 | "cell_type": "markdown", 170 | "id": "0592a057", 171 | "metadata": {}, 172 | "source": [ 173 | "## 3.4 Paths and Connectivity in Digraphs \n", 174 | "\n", 175 | "This section shows the determination of whether a graph is strongly connected and if yes, whether it is aperiodic or not. We use hereby another example as in section 3.4.1. Further useful resources: https://networkx.org/documentation/stable/reference/algorithms/component.html\n" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": null, 181 | "id": "5850bc27", 182 | "metadata": {}, 183 | "outputs": [], 184 | "source": [ 185 | "fig, ax34 = plt.subplots(figsize=custom_figsize)\n", 186 | "G_di2 = nx.DiGraph()\n", 187 | "edges2 = [(2,1), (2,3), (3,4), (4,3), (4,5), (5,4), (5,6), (1,6), (5,2), (6,2)]\n", 188 | "pos_di2 = {1:[0.3,0.2],2:[.2,.5],3:[.3,.8],4:[.7,.8], 5:[.8,.5], 6:[0.7,0.2]} # Define position of nodes in digraph plot\n", 189 | "G_di2.add_edges_from(edges2)\n", 190 | "nx.draw_networkx(G_di2, node_size=100, ax=ax34, pos=pos_di2, connectionstyle='arc3, rad = 0.1')\n", 191 | "ax34.set_title(\"2nd Digraph example\")\n", 192 | "print(\"The weighted Digraph2 example is strongly connected: \", nx.is_strongly_connected(G_di2))\n", 193 | "\n", 194 | "# Using self defined function, available at lib\n", 195 | "print(\"The weighted Digraph2 example is periodic: \", lib.is_periodic(G_di2))" 196 | ] 197 | }, 198 | { 199 | "cell_type": "markdown", 200 | "id": "2771fd49", 201 | "metadata": {}, 202 | "source": [ 203 | "### 3.4.3 Condensation Digraphs\n", 204 | "\n", 205 | "In this section a randomly generated digraph is displayed, its strongly connected component and its condensation digraph. We can execute the cell below multiple times ant watch multiple random graphs and their condensation." 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": null, 211 | "id": "b5ce5ba8", 212 | "metadata": {}, 213 | "outputs": [], 214 | "source": [ 215 | "# Play with the following parameters to generate a random graph:\n", 216 | "#G_random = nx.random_k_out_graph(10, 2, 1)\n", 217 | "G_random = nx.random_k_out_graph(50, 5, 1)\n", 218 | "\n", 219 | "fig, axs343 = plt.subplots(1, 3, figsize=(custom_figsize[0]*1.2, custom_figsize[1]))\n", 220 | "\n", 221 | "lib.plot_condensated_graph(G_random, axs343)" 222 | ] 223 | }, 224 | { 225 | "cell_type": "markdown", 226 | "id": "dbf73b51", 227 | "metadata": {}, 228 | "source": [ 229 | "## 3.5 Weighted Digraphs\n", 230 | "\n", 231 | "Note here: Networkx does not support in this package version a good representation of the weight labeling, for visulization another tool like graphviz is recommended, please refer to the book." 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": null, 237 | "id": "44496a08", 238 | "metadata": {}, 239 | "outputs": [], 240 | "source": [ 241 | "# Plot first digraph again with weights visualization from section 3.5\n", 242 | "fig, ax35 = plt.subplots(figsize=custom_figsize)\n", 243 | "nx.draw_networkx(G_di, node_size=100, ax=ax35, pos=pos_di, connectionstyle='arc3, rad = 0.1')\n", 244 | "labels = nx.get_edge_attributes(G_di,'weight')\n", 245 | "nx.draw_networkx_edge_labels(G_di,pos=pos_di,edge_labels=labels, label_pos=0.2)\n", 246 | "\n", 247 | "print(\"\\n Out-degree of DiGraph Example with weights in Format (Node, out-degree): \\n\" , G_di.out_degree(weight = \"weight\"))\n", 248 | "print(\"\\n In-degree of DiGraph Example with weights in Format (Node, in-degree): \\n\" , G_di.in_degree(weight = \"weight\"))" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": null, 254 | "id": "dd237487", 255 | "metadata": {}, 256 | "outputs": [], 257 | "source": [] 258 | } 259 | ], 260 | "metadata": { 261 | "kernelspec": { 262 | "display_name": "Python 3 (ipykernel)", 263 | "language": "python", 264 | "name": "python3" 265 | }, 266 | "language_info": { 267 | "codemirror_mode": { 268 | "name": "ipython", 269 | "version": 3 270 | }, 271 | "file_extension": ".py", 272 | "mimetype": "text/x-python", 273 | "name": "python", 274 | "nbconvert_exporter": "python", 275 | "pygments_lexer": "ipython3", 276 | "version": "3.8.11" 277 | }, 278 | "toc": { 279 | "base_numbering": 1, 280 | "nav_menu": {}, 281 | "number_sections": false, 282 | "sideBar": true, 283 | "skip_h1_title": false, 284 | "title_cell": "Table of Contents", 285 | "title_sidebar": "Contents", 286 | "toc_cell": true, 287 | "toc_position": { 288 | "height": "calc(100% - 180px)", 289 | "left": "10px", 290 | "top": "150px", 291 | "width": "165px" 292 | }, 293 | "toc_section_display": true, 294 | "toc_window_display": false 295 | } 296 | }, 297 | "nbformat": 4, 298 | "nbformat_minor": 5 299 | } 300 | -------------------------------------------------------------------------------- /Chapter_4_elements_of_algebraic_graph_theory.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "fd2b1168", 6 | "metadata": { 7 | "toc": true 8 | }, 9 | "source": [ 10 | "

Table of Contents

\n", 11 | "
" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "dfe65cd4", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "%matplotlib widget\n", 22 | "\n", 23 | "# Import packages\n", 24 | "import numpy as np\n", 25 | "import matplotlib.pyplot as plt\n", 26 | "import networkx as nx\n", 27 | "from pylab import MaxNLocator\n", 28 | "\n", 29 | "# For interactive graphs\n", 30 | "import ipywidgets as widgets\n", 31 | "\n", 32 | "# Import self defined functions\n", 33 | "import lib # General library\n", 34 | "\n", 35 | "# Settings\n", 36 | "custom_figsize= (6, 4) # Might need to change this value to fit the figures to your screen\n", 37 | "custom_figsize_square = (5, 5) " 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "6b9bebd2", 43 | "metadata": {}, 44 | "source": [ 45 | "# Chapter 4 - Elements of Algebraic Graph Theory\n", 46 | "These Jupyter Notebook scripts contain some examples, visualization and supplements accompanying the book \"Lectures on Network Systems\" by Francesco Bullo http://motion.me.ucsb.edu/book-lns/. These scripts are published with the MIT license. **Make sure to run the first cell above to import all necessary packages and functions and adapt settings in case.** In this script it is necessary to execute cell by cell chronologically due to reocurring examples (Tip: Use the shortcut Shift+Enter to execute each cell). Most of the functions are kept in separate files to keep this script neat." 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "id": "50060a77", 52 | "metadata": {}, 53 | "source": [ 54 | "## 4.1 The adjacency matrix\n", 55 | "First it is shown how to access the weighted adjacency matrix for the example in the script, when the graph is already created in NetworkX:" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "id": "36db0bcb", 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "# Digraph Example from section 3.5 with weights, self_loops etc.\n", 66 | "G_di = nx.DiGraph()\n", 67 | "edges = [(1,2), (2,1), (2,4), (1,3), (3,5), (5,1), (5,5), (3,4), (5,4)]\n", 68 | "weights = [3.7, 8.9, 1.2, 2.6, 2.3, 4.4, 4.4, 1.9, 2.7]\n", 69 | "for edge, weight in zip(edges, weights):\n", 70 | " G_di.add_edge(*edge, weight=weight)\n", 71 | "pos_di = {1:[0.1,0.2],2:[.4,.5],3:[.5,.2],4:[.8,.5], 5:[.9,.2]} # Define position of nodes in digraph plot\n", 72 | "\n", 73 | "# Plot first digraph again with weights visualization from section 3.5\n", 74 | "fig, ax41 = plt.subplots(figsize=custom_figsize)\n", 75 | "nx.draw_networkx(G_di, node_size=100, ax=ax41, pos=pos_di, connectionstyle='arc3, rad = 0.1')\n", 76 | "labels = nx.get_edge_attributes(G_di,'weight')\n", 77 | "nx.draw_networkx_edge_labels(G_di,pos=pos_di,edge_labels=labels, label_pos=0.2)\n", 78 | "\n", 79 | "# This will always result in a sparse matrix, nodelist argument important to keep order of nodes\n", 80 | "A_di = nx.linalg.graphmatrix.adjacency_matrix(G_di, nodelist=range(1, G_di.number_of_nodes()+1)).toarray()\n", 81 | "print(\"Adjacency Matrix determined by Networkx:\")\n", 82 | "lib.matprint(A_di)" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "id": "58d1da3b", 88 | "metadata": {}, 89 | "source": [ 90 | "### Basic Graphs, their Adjacency Matrix and Spectrum (Table 4.1)\n", 91 | "\n", 92 | "Below are further basic graphs and their adjacency matrix and their positive matrix entries in a binary representation similar to Figure 4.2. Further on, the spectrum is plotted and can be compared with the given formula in Table 4.1." 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "id": "522e3d5a", 99 | "metadata": {}, 100 | "outputs": [], 101 | "source": [ 102 | "n_m = 6 # Can change this value\n", 103 | "# Path Graph\n", 104 | "G_path = nx.path_graph(n_m)\n", 105 | "# Cycle Graph\n", 106 | "G_cycle = nx.cycle_graph(n_m)\n", 107 | "# Star Graph\n", 108 | "G_star = nx.star_graph(n_m-1)\n", 109 | "# Complete Graph\n", 110 | "G_complete = nx.complete_graph(n_m)\n", 111 | "# Complete bipartite Graph\n", 112 | "G_bipartite = nx.complete_bipartite_graph(n_m//2, n_m//2)\n", 113 | "\n", 114 | "all_basic_graphs = {\n", 115 | " \"Path Graph\": G_path,\n", 116 | " \"Cycle Graph\": G_cycle,\n", 117 | " \"Star Graph\":G_star,\n", 118 | " \"Complete Graph\": G_complete,\n", 119 | " \"Complete bipartite Graph\": G_bipartite,\n", 120 | " }\n", 121 | "spectrums = [\"$\\{2 cos(\\pi i/(n + 1)) | i ∈ \\{1, . . . , n\\}\\}$\",\n", 122 | " \"$\\{2 cos(2\\pi i/n)) | i ∈ \\{1, . . . , n\\}\\}$\",\n", 123 | " \"$\\{ \\sqrt{n − 1}, 0, . . . , 0, − \\sqrt{n − 1}\\}$\",\n", 124 | " \"$\\{(n − 1), −1, . . . , −1\\}$\",\n", 125 | " \"$\\{\\sqrt{nm}, 0, . . . , 0, − \\sqrt{nm}\\}$\"]" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": null, 131 | "id": "678be351", 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "# Plotting the graph itself, the binary adjacency matrix visualization with the actual values written inside\n", 136 | "fig, axs412 = plt.subplots(len(all_basic_graphs), 3, figsize=(custom_figsize[0]*1.2, custom_figsize[1]*3))\n", 137 | "for count, (key, graph) in enumerate(all_basic_graphs.items()):\n", 138 | " if key == \"Complete bipartite Graph\":\n", 139 | " nx.draw_networkx(graph, node_size=100, ax=axs412[count, 0], pos=nx.drawing.layout.bipartite_layout(graph, list(range(0, n_m//2))))\n", 140 | " else:\n", 141 | " nx.draw_networkx(graph, node_size=100, ax=axs412[count, 0], connectionstyle='arc3, rad = 0.1')\n", 142 | " A = nx.linalg.graphmatrix.adjacency_matrix(graph, nodelist=range(0, graph.number_of_nodes())).toarray()\n", 143 | " lib.plot_matrix_binary(A, axs412[count, 1])\n", 144 | " axs412[count, 0].set_xlabel(key)\n", 145 | " lib.plot_spectrum(A, axs412[count, 2]);\n", 146 | " axs412[count, 2].set(title=spectrums[count])\n", 147 | "fig.subplots_adjust(hspace=0.5)" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "id": "dc63668d", 153 | "metadata": {}, 154 | "source": [ 155 | "## 4.3.2 Connectivity and adjacency powers\n", 156 | "In this little example we want to see Lemma 4.4 for the given graph in numbers. The graph is already defined in this Jupyter Notebook script at section 4.1. Since Node 4 of $G$ is globally reachable, we expect the 4th column of the sum of matrix powers $\\sum_{k=0}^{n-1} A^k$ to be positive." 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": null, 162 | "id": "78376f60", 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "n = np.size(A_di, 1)\n", 167 | "p = 1.0/3\n", 168 | "A_di_sum_of_powers = lib.sum_of_powers(A_di, n-1)\n", 169 | "print(\"4th column is positive, since it is the only globally reachable node:\")\n", 170 | "lib.matprint(A_di_sum_of_powers)" 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "id": "b1447473", 176 | "metadata": {}, 177 | "source": [ 178 | "## 4.4. Graph theoretical characterization of primitive matrices\n", 179 | "\n", 180 | "In this section we want to show pixel pictures of different matrix powers similar to figure 4.3. Remember, that (i) $G$ is strongly connected and aperiodic and (ii) $A$ is primitive are equivilant statements. We generate random examples by building the adjacency matrix first." 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": null, 186 | "id": "d3ce6775", 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "# Choose size of matrices here\n", 191 | "n = 4\n", 192 | "# Set random seed here\n", 193 | "np.random.seed(2)\n", 194 | "\n", 195 | "def plot_graph_and_matrix_powers(A, npwr=None):\n", 196 | " \"\"\" Function to visualize graph and matrix power of given matrix A only used in this Jupyter Notebook\"\"\"\n", 197 | " if npwr is None:\n", 198 | " npwr = A.shape[0]\n", 199 | " fig, ax = plt.subplots(figsize=(4,4))\n", 200 | " fig, axs = plt.subplots(1, npwr, figsize=(custom_figsize[0]*1.5, custom_figsize[1]/2))\n", 201 | "\n", 202 | " lib.draw_adj_matrix(A, ax)\n", 203 | "\n", 204 | " for i in range(npwr):\n", 205 | " lib.plot_matrix_binary(np.linalg.matrix_power(A, i+1), ax=axs[i])\n", 206 | " axs[i].set(title='${}^{{{}}}$'.format(\"M\", i+1))" 207 | ] 208 | }, 209 | { 210 | "cell_type": "markdown", 211 | "id": "75035852", 212 | "metadata": {}, 213 | "source": [ 214 | "**Random Primitive Adjacency Matrix Example**" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": null, 220 | "id": "9052b441", 221 | "metadata": {}, 222 | "outputs": [], 223 | "source": [ 224 | "# Randomly generate primitive matrix\n", 225 | "A_rand_prim = np.zeros((n, n))\n", 226 | "while(not lib.is_primitive(A_rand_prim)):\n", 227 | " A_rand_prim = np.random.choice([0, 1], size=(n,n), p=[1-p, p])" 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": null, 233 | "id": "701b1496", 234 | "metadata": {}, 235 | "outputs": [], 236 | "source": [ 237 | "plot_graph_and_matrix_powers(A_rand_prim, npwr=n)" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "id": "dfe7cd49", 243 | "metadata": {}, 244 | "source": [ 245 | "**Random Irreducible not Primitive Adjacency Matrix Example**\n", 246 | "\n", 247 | "Note: Sometimes can take some time to generate a irreducible, but not positive matrix!" 248 | ] 249 | }, 250 | { 251 | "cell_type": "code", 252 | "execution_count": null, 253 | "id": "9d9ea80e", 254 | "metadata": {}, 255 | "outputs": [], 256 | "source": [ 257 | "# Randomly generate irreducible, but not positive, matrix\n", 258 | "A_rand_irr = np.zeros((n, n))\n", 259 | "while(not lib.is_irreducible(A_rand_irr) or lib.is_primitive(A_rand_irr)):\n", 260 | " A_rand_irr = np.random.choice([0, 1], size=(n,n), p=[1-p, p])" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": null, 266 | "id": "dd2da387", 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "plot_graph_and_matrix_powers(A_rand_irr, npwr=2*n)" 271 | ] 272 | }, 273 | { 274 | "cell_type": "markdown", 275 | "id": "3fcf9619", 276 | "metadata": {}, 277 | "source": [ 278 | "**Random Reducible Matrix Example**" 279 | ] 280 | }, 281 | { 282 | "cell_type": "code", 283 | "execution_count": null, 284 | "id": "5f5b34ee", 285 | "metadata": {}, 286 | "outputs": [], 287 | "source": [ 288 | "# Randomly generate reducible adjacency matrix\n", 289 | "A_rand_red = np.random.choice([0, 1], size=(n,n), p=[1-p, p])\n", 290 | "while(lib.is_irreducible(A_rand_red)):\n", 291 | " A_rand_red = np.random.choice([0, 1], size=(n,n), p=[1-p, p])" 292 | ] 293 | }, 294 | { 295 | "cell_type": "code", 296 | "execution_count": null, 297 | "id": "5eff3509", 298 | "metadata": {}, 299 | "outputs": [], 300 | "source": [ 301 | "plot_graph_and_matrix_powers(A_rand_red, npwr=2*n)" 302 | ] 303 | }, 304 | { 305 | "cell_type": "markdown", 306 | "id": "026fe59b", 307 | "metadata": {}, 308 | "source": [ 309 | "## 4.5 Elements of Spectral Graph Theory\n", 310 | "\n", 311 | "As in Figure 4.5, an illustration of the Theorem 4.11 Bounds on the spectral radius of non-negative matrices, II. We visualize the examples from the book as a graph and plot the corresponding spectrum next to it. By Theorem 4.11, the bounds are given in the spectrum plot title." 312 | ] 313 | }, 314 | { 315 | "cell_type": "code", 316 | "execution_count": null, 317 | "id": "9f283f88", 318 | "metadata": {}, 319 | "outputs": [], 320 | "source": [ 321 | "# Two dimensional grid graph\n", 322 | "G_grid = nx.generators.lattice.grid_2d_graph(7, 4)\n", 323 | "# Extract the necessary adjacency matrices\n", 324 | "A_grid = nx.linalg.graphmatrix.adjacency_matrix(G_grid).toarray()\n", 325 | "A_bipartite = nx.linalg.graphmatrix.adjacency_matrix(G_bipartite, nodelist=range(0, G_bipartite.number_of_nodes())).toarray()\n", 326 | "\n", 327 | "# Drawing the two graphs\n", 328 | "fig, axs45 = plt.subplots(2, 2, figsize=(custom_figsize[0]*1., custom_figsize[1]*1.5))\n", 329 | "nx.draw_networkx(G_bipartite, node_size=100, ax=axs45[0, 0], pos=nx.drawing.layout.bipartite_layout(graph, list(range(0, n_m//2))))\n", 330 | "pos_grid = {(x,y):(y,-x) for x,y in G_grid.nodes()} # Smooth way of extracting coordinates of created grid graph\n", 331 | "nx.draw_networkx(G_grid, pos= pos_grid, node_size=100, ax=axs45[1, 0], with_labels=False)\n", 332 | "\n", 333 | "# Plot spectrums and add titels\n", 334 | "lib.plot_spectrum(A_bipartite, axs45[0,1]);\n", 335 | "lib.plot_spectrum(A_grid, axs45[1,1]);\n", 336 | "axs45[0, 1].set(title='$\\\\rho(A_{bipartite}) = 3$')\n", 337 | "axs45[1, 1].set(title=\"$2 < \\\\rho(A_{grid}) < 4$\")\n", 338 | "\n", 339 | "# Add vertical space between plots\n", 340 | "fig.subplots_adjust(hspace=0.5)" 341 | ] 342 | }, 343 | { 344 | "cell_type": "markdown", 345 | "id": "684d614b", 346 | "metadata": {}, 347 | "source": [ 348 | "## Exercise 4.19 Leslie Population Model\n", 349 | "\n", 350 | "In this section the opportunity is given to verify the results obtained from Exercise 4.19 in a simulation. In the cell below the parameters $\\alpha_1 ... \\alpha_n$ and $\\beta_1 ... \\beta_{n-1}$ can be changed for a given Leslie Population Model Matrix of size $n=5$, meaning we have 5 age classes $i$." 351 | ] 352 | }, 353 | { 354 | "cell_type": "code", 355 | "execution_count": null, 356 | "id": "520702d0", 357 | "metadata": {}, 358 | "outputs": [], 359 | "source": [ 360 | "# Define the parameters here!\n", 361 | "alpha = np.array([0.01, 0.2, 0.4, 0.2, 0.05])\n", 362 | "beta = np.array([0.99, 0.95, 0.7, 0.5])\n", 363 | "n_dt = 20 # Number of time simulation steps\n", 364 | "\n", 365 | "\n", 366 | "# Building the Leslie Population Matrix\n", 367 | "A_leslie = np.vstack((alpha[None, :], np.hstack((np.diag(beta), np.zeros(4)[:, None]))))\n", 368 | "print(\"Leslie Population Model:\")\n", 369 | "lib.matprint(A_leslie)\n", 370 | "# Initialize random population Distribution\n", 371 | "x = np.random.randint(low=1, high=100, size=5)\n", 372 | "\n", 373 | "# Simulate for n_dt timesteps and plot result\n", 374 | "fig, ax419 = plt.subplots(figsize=custom_figsize)\n", 375 | "states_complete = lib.simulate_network(A_leslie, x, n_dt) # Simulate network and save states for each time step in a t*n np.array\n", 376 | "lib.plot_node_val_2D(states_complete, x, n_dt, ax419, avg=False) # Visualize states in a 2D Graph\n", 377 | "ax419.get_xaxis().set_major_locator(MaxNLocator(integer=True))\n", 378 | "ax419.grid('on')" 379 | ] 380 | }, 381 | { 382 | "cell_type": "code", 383 | "execution_count": null, 384 | "id": "1bc48312", 385 | "metadata": {}, 386 | "outputs": [], 387 | "source": [] 388 | } 389 | ], 390 | "metadata": { 391 | "kernelspec": { 392 | "display_name": "Python 3 (ipykernel)", 393 | "language": "python", 394 | "name": "python3" 395 | }, 396 | "language_info": { 397 | "codemirror_mode": { 398 | "name": "ipython", 399 | "version": 3 400 | }, 401 | "file_extension": ".py", 402 | "mimetype": "text/x-python", 403 | "name": "python", 404 | "nbconvert_exporter": "python", 405 | "pygments_lexer": "ipython3", 406 | "version": "3.8.11" 407 | }, 408 | "toc": { 409 | "base_numbering": 1, 410 | "nav_menu": {}, 411 | "number_sections": false, 412 | "sideBar": true, 413 | "skip_h1_title": false, 414 | "title_cell": "Table of Contents", 415 | "title_sidebar": "Contents", 416 | "toc_cell": true, 417 | "toc_position": {}, 418 | "toc_section_display": true, 419 | "toc_window_display": false 420 | } 421 | }, 422 | "nbformat": 4, 423 | "nbformat_minor": 5 424 | } 425 | -------------------------------------------------------------------------------- /Chapter_5_discrete_time_averaging_systems.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "c8c6c8bb", 6 | "metadata": { 7 | "toc": true 8 | }, 9 | "source": [ 10 | "

Table of Contents

\n", 11 | "
" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "50ac1eb2", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "%matplotlib widget\n", 22 | "\n", 23 | "# Import packages\n", 24 | "import numpy as np\n", 25 | "import matplotlib.pyplot as plt\n", 26 | "import networkx as nx\n", 27 | "\n", 28 | "# For interactive graphs\n", 29 | "import ipywidgets as widgets\n", 30 | "\n", 31 | "# Import self defined functions\n", 32 | "import lib # General library\n", 33 | "\n", 34 | "# Settings\n", 35 | "custom_figsize= (6, 4) # Might need to change this value to fit the figures to your screen\n", 36 | "custom_figsize_square = (5, 5) " 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "id": "e444bc32", 42 | "metadata": {}, 43 | "source": [ 44 | "# Chapter 5 - Discrete-time Averaging Systems\n", 45 | "\n", 46 | "These Jupyter Notebook scripts contain some examples, visualization and supplements accompanying the book \"Lectures on Network Systems\" by Francesco Bullo http://motion.me.ucsb.edu/book-lns/. These scripts are published with the MIT license. **Make sure to run the first cell above to import all necessary packages and functions and adapt settings in case.** In this script it is necessary to execute cell by cell chronologically due to reocurring examples (Tip: Use the shortcut Shift+Enter to execute each cell). Most of the functions are kept in separate files to keep this script neat." 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "id": "6476113b", 52 | "metadata": {}, 53 | "source": [ 54 | "## Averaging systems achieving consensus\n", 55 | "\n", 56 | "In this section the three examples of the book are presented again to verify the results obtained and which lead as an introduction to Theorem 5.1 .\n", 57 | "\n", 58 | "**First Example**\n", 59 | "\n", 60 | "The first example is again the wireless network system from Chapter 1.2. First, we create some plots that include tha graph, binary matrix representation, spectrum and the network simulation. " 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "id": "1e38e2fd", 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "# Creating the Network System example as in Chapter 1.2 and as first example in chapter 5.1\n", 71 | "G = nx.DiGraph()\n", 72 | "G.add_edges_from([(0,1), (1,0), (0,0), (2,2), (1,2), (2,1), (2,3), (3,2), (1,3), (3,1), (3,3)])\n", 73 | "\n", 74 | "# Define position of nodes in graph plot\n", 75 | "pos = {0:[0.2,0.2],1:[.4,.2],2:[.4,.6],3:[.7,.6]}\n", 76 | "\n", 77 | "# Define the adjacency matrix A\n", 78 | "A = np.array([[0.5,0.5, 0., 0.],\n", 79 | " [1/4, 1/4, 1/4, 1/4],\n", 80 | " [0., 1/3, 1/3, 1/3],\n", 81 | " [0., 1/3, 1/3, 1/3]\n", 82 | "])\n", 83 | "\n", 84 | "fig, axs51 = plt.subplots(2, 2, figsize = (custom_figsize[0]*1.2, custom_figsize[1]*1.5)) # Init figure\n", 85 | "\n", 86 | "# Draw network\n", 87 | "nx.draw_networkx(G, pos, node_size=200, ax = axs51[0, 0], connectionstyle='arc3, rad = 0.1')\n", 88 | "axs51[0, 0].margins(0.05) # Zooming out for better visualization\n", 89 | "\n", 90 | "# Draw binary matrix representation\n", 91 | "lib.plot_matrix_binary(A, axs51[1, 0])\n", 92 | "\n", 93 | "# Draw spectrum\n", 94 | "lib.plot_spectrum(A, axs51[0, 1]);\n", 95 | "\n", 96 | "# Draw simulation results\n", 97 | "x_0 = np.array([0.9, 0.3, 0.8, 0.6])\n", 98 | "# Choosing the range of time step\n", 99 | "t = 15\n", 100 | "states = lib.simulate_network(A,x_0, t) # Simulate network and save states for each time step in a t*n np.array\n", 101 | "lib.plot_node_val_2D(states, x_0, t, axs51[1, 1]) # Visualize states in a 2D Graph\n", 102 | "axs51[1, 1].set_title(\"Simulation of Network\")\n", 103 | "axs51[1,1].legend(loc=4, ncol=2)\n", 104 | "fig.subplots_adjust(hspace=0.5)\n" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "id": "6197f89e", 110 | "metadata": {}, 111 | "source": [ 112 | "As expected, we do not reach average consensus, since $A$ is not doubly-stochastic. However, we can calculate the consensus value by $w^T x(0)$, where $w$ is the left dominant eigenvector normed to 1 as demonstrated in the book." 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "id": "f5173e97", 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "# Left dominant eigenvectors\n", 123 | "lambdas, eigv = np.linalg.eig(A.T)\n", 124 | "# Get position of dom eigv\n", 125 | "\n", 126 | "idx = np.argmax(lambdas) # Since we expect 1 to be simple\n", 127 | "left_dom = eigv[:, idx] / sum(abs(eigv[:, idx])) # With Norm1 = 1\n", 128 | "\n", 129 | "print(\"Left dominant eigenvector w:\")\n", 130 | "lib.matprint(left_dom)\n", 131 | "\n", 132 | "print(\"\\nFinal consensus value w^T x(0):\")\n", 133 | "print(left_dom.T @ x_0)" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "id": "e8149ac5", 139 | "metadata": {}, 140 | "source": [ 141 | "**Second Example**\n", 142 | "\n", 143 | "The second example is the robotic cyclic pursuit from section 1.6 of the book. Here we show, how average consensus is achieved." 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "id": "c32daaf9", 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "# Creating the Network System example as in Chapter 1.2 and as first example in chapter 5.1\n", 154 | "G2 = nx.DiGraph()\n", 155 | "G2.add_edges_from([(0,0), (0,1), (1,1), (1,2), (2,2), (2,3), (3,3), (3,4), (4,4), (4,5), (5,5), (5,0)])\n", 156 | "\n", 157 | "# Define position of nodes in graph plot\n", 158 | "pos2 = {0:[0.3,0.2],1:[.2,.5],2:[.3,.8],3:[.7,.8], 4:[.8,.5], 5:[0.7,0.2]}\n", 159 | "\n", 160 | "# Define the adjacency matrix A\n", 161 | "A2 = np.array([[1/2,1/2, 0., 0., 0., 0.],\n", 162 | " [0, 1/2, 1/2, 0., 0., 0.],\n", 163 | " [0., 0., 1/2, 1/2, 0., 0.],\n", 164 | " [0., 0., 0., 1/2, 1/2, 0.],\n", 165 | " [0., 0., 0., 0., 1/2, 1/2],\n", 166 | " [1/2, 0., 0., 0., 0., 1/2]\n", 167 | "])\n", 168 | "\n", 169 | "fig, axs512 = plt.subplots(2, 2, figsize = (custom_figsize[0]*1.2, custom_figsize[1]*1.5)) # Init figure\n", 170 | "\n", 171 | "# Draw network\n", 172 | "nx.draw_networkx(G2, pos2, node_size=200, ax = axs512[0, 0], connectionstyle='arc3, rad = 0.1')\n", 173 | "axs512[0, 0].margins(0.05) # Zooming out for better visualization\n", 174 | "\n", 175 | "# Draw binary matrix representation\n", 176 | "lib.plot_matrix_binary(A2, axs512[1, 0])\n", 177 | "\n", 178 | "# Draw spectrum\n", 179 | "lib.plot_spectrum(A2, axs512[0, 1]);\n", 180 | "\n", 181 | "# Draw simulation results\n", 182 | "x_0_2 = np.array([0.1, 0.3, 0.4, 0.5, 0.6, 0.8])\n", 183 | "# Choosing the range of time step\n", 184 | "t = 50\n", 185 | "states2 = lib.simulate_network(A2,x_0_2, t) # Simulate network and save states for each time step in a t*n np.array\n", 186 | "lib.plot_node_val_2D(states2, x_0_2, t, axs512[1, 1]) # Visualize states in a 2D Graph\n", 187 | "axs512[1, 1].set_title(\"Simulation of Network\")\n", 188 | "axs512[1,1].legend(loc=4, ncol=3)\n", 189 | "fig.subplots_adjust(hspace=0.5)\n", 190 | "\n", 191 | "print(\"\\nFinal average consensus value avg(x(0)):\")\n", 192 | "print(np.mean(x_0_2))" 193 | ] 194 | }, 195 | { 196 | "cell_type": "markdown", 197 | "id": "94d236da", 198 | "metadata": {}, 199 | "source": [ 200 | "**Third Example**\n", 201 | "\n", 202 | "In this example a not strongly connected matrix with its assciated digraph is shown. As mentioned in the book, we call such row-stochastic matrices indecomposable, since we still achieve consensus." 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": null, 208 | "id": "af7ea02d", 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "# Creating the Network System example as in Chapter 1.2 and as first example in chapter 5.1\n", 213 | "G3 = nx.DiGraph()\n", 214 | "G3.add_edges_from([(0,1), (0,2), (1,4), (1,8), (2,4), (2,6), (2,7), (3,1), (3,6), (3,7), (4,1), (5,4), (6,2), (6,9), (7,0), (7,6), (8,0), (9,0), (9,2)])\n", 215 | "\n", 216 | "# Define positon for nodes by layout\n", 217 | "pos3 = nx.drawing.layout.spring_layout(G3)\n", 218 | "# Extract Adjacency Matrix here and make it row stochastic\n", 219 | "A3 = nx.linalg.graphmatrix.adjacency_matrix(G3, nodelist=range(0, G3.number_of_nodes())).toarray()\n", 220 | "A3 = A3 / np.sum(A3, axis=1)[:, None]\n", 221 | "\n", 222 | "fig, axs513 = plt.subplots(2, 2, figsize = (custom_figsize[0]*1.2, custom_figsize[1]*1.5)) # Init figure\n", 223 | "\n", 224 | "# Draw network\n", 225 | "nx.draw_networkx(G3, pos3, node_size=200, ax = axs513[0, 0], connectionstyle='arc3, rad = 0.1')\n", 226 | "axs513[0, 0].margins(0.05) # Zooming out for better visualization\n", 227 | "\n", 228 | "# Draw binary matrix representation\n", 229 | "lib.plot_matrix_binary(A3, axs513[1, 0])\n", 230 | "\n", 231 | "# Draw spectrum\n", 232 | "lib.plot_spectrum(A3, axs513[0, 1]);\n", 233 | "\n", 234 | "# Draw simulation results\n", 235 | "x_0_3 = np.random.rand(10)\n", 236 | "# Choosing the range of time step\n", 237 | "t = 20\n", 238 | "states3 = lib.simulate_network(A3,x_0_3, t) # Simulate network and save states for each time step in a t*n np.array\n", 239 | "lib.plot_node_val_2D(states3, x_0_3, t, axs513[1, 1]) # Visualize states in a 2D Graph\n", 240 | "axs513[1, 1].set_title(\"Simulation of Network\")\n", 241 | "axs513[1,1].get_legend().remove()\n", 242 | "fig.subplots_adjust(hspace=0.5)\n" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "id": "5cfd90c9", 248 | "metadata": {}, 249 | "source": [ 250 | "## 5.2 Averaging system reaching asymptotic disagreement\n", 251 | "\n", 252 | "In this section a minimum example for averaging systems reaching asymptotic disagreement is given. See in the plot, how different nodes reach different states, based on if the node belongs to a sink our which sink it senses. Below, it is even presented in an interactive environment." 253 | ] 254 | }, 255 | { 256 | "cell_type": "code", 257 | "execution_count": null, 258 | "id": "b5ff2331", 259 | "metadata": {}, 260 | "outputs": [], 261 | "source": [ 262 | "# Creating the Network System example as in Chapter 1.2 and as first example in chapter 5.1\n", 263 | "G4 = nx.DiGraph()\n", 264 | "G4.add_edges_from([(0,1), (1,3), (1,2), (2,1), (2,3), (3,1), (3,2), (4,2), (4,3), (4,5), (5,6), (6,5), (5,7), (7,5), (7,6), (6,7)])\n", 265 | "\n", 266 | "# Define position of nodes in graph plot\n", 267 | "pos4 = {0:[0.4,0.8],1:[.3,.5],2:[.5,.2],3:[.5,.5], 4:[.7,.8], 5:[0.7,0.5], 6:[0.8,0.4], 7:[0.7,0.3]}\n", 268 | "\n", 269 | "# Extract Adjacency Matrix here and make it row stochastic\n", 270 | "A4 = nx.linalg.graphmatrix.adjacency_matrix(G4, nodelist=range(0, G4.number_of_nodes())).toarray()\n", 271 | "A4 = A4 / np.sum(A4, axis=1)[:, None]\n", 272 | "\n", 273 | "fig, axs52 = plt.subplots(3, 1, figsize = (custom_figsize[0]*1.0, custom_figsize[1]*1.8)) # Init figure\n", 274 | "\n", 275 | "# Draw network\n", 276 | "nx.draw_networkx(G4, pos4, node_size=200, ax = axs52[0], connectionstyle='arc3, rad = 0.1')\n", 277 | "axs52[0].margins(0.05) # Zooming out for better visualization\n", 278 | "\n", 279 | "# Draw binary matrix representation\n", 280 | "lib.plot_matrix_binary(A4, axs52[1])\n", 281 | "\n", 282 | "# Draw spectrum\n", 283 | "lib.plot_spectrum(A4, axs52[2]);\n", 284 | "\n", 285 | "fig.subplots_adjust(hspace=0.5)\n", 286 | "\n", 287 | "\n", 288 | "\n", 289 | "fig, ax521 = plt.subplots(figsize = (custom_figsize[0]*1.2, custom_figsize[1]*1.2)) # Init figure\n", 290 | "\n", 291 | "# Draw simulation results\n", 292 | "#x_0_4 = np.random.rand(8)\n", 293 | "x_0_4 = np.array([0.1, 0.4, 0.45, 0.5, 0.9, 1, 0.8, 0.9])\n", 294 | "# Choosing the range of time step\n", 295 | "t4 = 20\n", 296 | "states4 = lib.simulate_network(A4,x_0_4, t4) # Simulate network and save states for each time step in a t*n np.array\n", 297 | "lib.plot_node_val_2D(states4, x_0_4, t4, ax521) # Visualize states in a 2D Graph\n", 298 | "ax521.set_title(\"Simulation of Network\")\n", 299 | "ax521.legend(loc=4, ncol=3);\n" 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": null, 305 | "id": "f0f6b401", 306 | "metadata": {}, 307 | "outputs": [], 308 | "source": [ 309 | "fig, ax523 = plt.subplots(figsize=custom_figsize)\n", 310 | "fig, v_bound, pos4 = lib.init_network_sim_plot(G4, states4, fig, pos=pos4)\n", 311 | "\n", 312 | "def inter(timestep):\n", 313 | " lib.update_network(timestep['new'], G=G4, states_m=states4, ax=ax523, vbound=v_bound, pos=pos4)\n", 314 | " #ax3.margins(0.20) # Zooming out for better visualization\n", 315 | " return None\n", 316 | "\n", 317 | "# Plot initial configuration\n", 318 | "lib.update_network(0, G=G4, states_m=states4, ax=ax523, vbound=v_bound, pos=pos4)\n", 319 | "\n", 320 | "\n", 321 | "# Widget\n", 322 | "# If this cell is executed twice we are making sure in the following, that the previous widget instances are all closed\n", 323 | "try:\n", 324 | " [c.close() for c in widget52.children] # Note: close_all() does also affect plot, thus list compr.\n", 325 | "except NameError: # Only want to except not defined variable error\n", 326 | " pass\n", 327 | "\n", 328 | "widget52 = lib.create_widgets_play_slider(fnc=inter, minv=0, maxv=t4-1, step=1, play_speed=1000)\n", 329 | "\n", 330 | "display(widget52)" 331 | ] 332 | }, 333 | { 334 | "cell_type": "markdown", 335 | "id": "afd64cf4", 336 | "metadata": {}, 337 | "source": [ 338 | "## Apendix 5.5 Design and computation of centrality measures\n", 339 | "\n", 340 | "The results from 5.13 are visualized again here:" 341 | ] 342 | }, 343 | { 344 | "cell_type": "code", 345 | "execution_count": null, 346 | "id": "e944aee5", 347 | "metadata": {}, 348 | "outputs": [], 349 | "source": [ 350 | "# define graph\n", 351 | "G55 = nx.Graph(); \n", 352 | "G55.add_nodes_from(range(0,10)); \n", 353 | "G55.add_edges_from([(0,1), (1,5), (1,6), (1,2), (2,3), (2,7), (6,7), (7,8), (3,8), (8,9), (9,4), (9,10)])\n", 354 | "pos55 = {0:[0.1,0.8],1:[.2,.8],2:[.3,.8],3:[.4,.8], 4:[.5,.8], 5:[0.1,0.4], 6:[0.2,0.4], 7:[0.3,0.4], 8:[0.4,0.4], 9:[0.5,0.4], 10:[0.6,0.4]}\n", 355 | "\n", 356 | "# Node 2 has the highest degree centrality\n", 357 | "degree_centrality = nx.degree_centrality(G55)\n", 358 | "\n", 359 | "# Node 3 has the highest eigenvector centrality\n", 360 | "eigenvector_centrality = nx.eigenvector_centrality_numpy(G55)\n", 361 | "\n", 362 | "# Node 8 has highest closeness centrality\n", 363 | "closeness_centrality = nx.closeness_centrality(G55)\n", 364 | "\n", 365 | "# Node 9 has highest closeness centrality\n", 366 | "betweenness_centrality = nx.betweenness_centrality(G55)" 367 | ] 368 | }, 369 | { 370 | "cell_type": "code", 371 | "execution_count": null, 372 | "id": "2101c816", 373 | "metadata": {}, 374 | "outputs": [], 375 | "source": [ 376 | "deg_cen = np.array(list(degree_centrality.values()))[None, :]\n", 377 | "eig_cen = np.array(list(eigenvector_centrality.values()))[None, :]\n", 378 | "clo_cen = np.array(list(closeness_centrality.values()))[None, :]\n", 379 | "bet_cen = np.array(list(betweenness_centrality.values()))[None, :]\n", 380 | "\n", 381 | "all_cen = np.vstack((deg_cen, eig_cen, clo_cen, bet_cen))\n", 382 | "\n", 383 | "lib.matprint(all_cen)\n", 384 | "\n", 385 | "titles = [\"Degree Centrality\", \"Eigenvector Centrality\", \"Closeness Centrality\", \"Betweenness Centrality\"]" 386 | ] 387 | }, 388 | { 389 | "cell_type": "code", 390 | "execution_count": null, 391 | "id": "a0b531e6", 392 | "metadata": {}, 393 | "outputs": [], 394 | "source": [ 395 | "for i in range(len(titles)):\n", 396 | " fig, ax55 = plt.subplots(figsize=custom_figsize)\n", 397 | " fig, v_bound, pos55 = lib.init_network_sim_plot(G55, all_cen[i, :][None, :], fig, pos=pos55)\n", 398 | " lib.update_network(0, G55, all_cen[i, :][None, :], ax55, v_bound, pos55, labels=False)\n", 399 | " ax55.set_title(titles[i])" 400 | ] 401 | }, 402 | { 403 | "cell_type": "markdown", 404 | "id": "44cdf910", 405 | "metadata": {}, 406 | "source": [ 407 | "## Note Exercise 5.1\n", 408 | "\n", 409 | "Exercise 5.1 can be found in the Jupyter Notebook for Chapter 7, as it is used as an example there." 410 | ] 411 | }, 412 | { 413 | "cell_type": "code", 414 | "execution_count": null, 415 | "id": "5f1cd23b", 416 | "metadata": {}, 417 | "outputs": [], 418 | "source": [] 419 | } 420 | ], 421 | "metadata": { 422 | "kernelspec": { 423 | "display_name": "Python 3 (ipykernel)", 424 | "language": "python", 425 | "name": "python3" 426 | }, 427 | "language_info": { 428 | "codemirror_mode": { 429 | "name": "ipython", 430 | "version": 3 431 | }, 432 | "file_extension": ".py", 433 | "mimetype": "text/x-python", 434 | "name": "python", 435 | "nbconvert_exporter": "python", 436 | "pygments_lexer": "ipython3", 437 | "version": "3.8.11" 438 | }, 439 | "toc": { 440 | "base_numbering": 1, 441 | "nav_menu": {}, 442 | "number_sections": false, 443 | "sideBar": true, 444 | "skip_h1_title": false, 445 | "title_cell": "Table of Contents", 446 | "title_sidebar": "Contents", 447 | "toc_cell": true, 448 | "toc_position": {}, 449 | "toc_section_display": true, 450 | "toc_window_display": false 451 | } 452 | }, 453 | "nbformat": 4, 454 | "nbformat_minor": 5 455 | } 456 | -------------------------------------------------------------------------------- /Chapter_6_the_laplacian_matrix.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "c8c6c8bb", 6 | "metadata": { 7 | "toc": true 8 | }, 9 | "source": [ 10 | "

Table of Contents

\n", 11 | "
" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "50ac1eb2", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "%matplotlib widget\n", 22 | "\n", 23 | "# Import packages\n", 24 | "import numpy as np\n", 25 | "import scipy.sparse.linalg as sla\n", 26 | "import matplotlib.pyplot as plt\n", 27 | "import networkx as nx\n", 28 | "\n", 29 | "# For interactive graphs\n", 30 | "import ipywidgets as widgets\n", 31 | "\n", 32 | "# Import self defined functions\n", 33 | "import lib # General library\n", 34 | "\n", 35 | "# Settings\n", 36 | "custom_figsize= (6, 4) # Might need to change this value to fit the figures to your screen\n", 37 | "custom_figsize_square = (5, 5) " 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "e444bc32", 43 | "metadata": {}, 44 | "source": [ 45 | "# Chapter 6 - The Laplacian Matrix\n", 46 | "\n", 47 | "These Jupyter Notebook scripts contain some examples, visualization and supplements accompanying the book \"Lectures on Network Systems\" by Francesco Bullo http://motion.me.ucsb.edu/book-lns/. These scripts are published with the MIT license. **Make sure to run the first cell above to import all necessary packages and functions and adapt settings in case.** In this script it is necessary to execute cell by cell chronologically due to reocurring examples (Tip: Use the shortcut Shift+Enter to execute each cell). Most of the functions are kept in separate files to keep this script neat." 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "id": "6476113b", 53 | "metadata": {}, 54 | "source": [ 55 | "## 6.1 The Laplacian Matrix example\n", 56 | "\n", 57 | "\n", 58 | "In this section we repeat the example from the book. The adjacancy Matrix $A$ is shown and how we retrieve the the Laplacian Matrix $L$ \n", 59 | "\n", 60 | "Note: This example occurred first in Chapter 3.5\n", 61 | "Note: The adjacency Matrix can be determined via networkx as below and shown in Chapter 4.1. However, the Laplacian Matrix must be calculated via $L = D_{out} - A$, since there is no built in function for directed graphs in networkx yet." 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "id": "8459e967", 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "# Digraph Example from section 3.5 with weights, self_loops etc.\n", 72 | "G_di = nx.DiGraph()\n", 73 | "edges = [(1,2), (2,1), (2,4), (1,3), (3,5), (5,1), (5,5), (3,4), (5,4)]\n", 74 | "weights = [3.7, 8.9, 1.2, 2.6, 2.3, 4.4, 4.4, 1.9, 2.7]\n", 75 | "for edge, weight in zip(edges, weights):\n", 76 | " G_di.add_edge(*edge, weight=weight)\n", 77 | "pos_di = {1:[0.1,0.2],2:[.4,.5],3:[.5,.2],4:[.8,.5], 5:[.9,.2]} # Define position of nodes in digraph plot\n", 78 | "\n", 79 | "# Plot first digraph again with weights visualization from section 3.5\n", 80 | "fig, ax61 = plt.subplots(figsize=custom_figsize)\n", 81 | "nx.draw_networkx(G_di, node_size=100, ax=ax61, pos=pos_di, connectionstyle='arc3, rad = 0.1')\n", 82 | "labels = nx.get_edge_attributes(G_di,'weight')\n", 83 | "nx.draw_networkx_edge_labels(G_di,pos=pos_di,edge_labels=labels, label_pos=0.2)\n", 84 | "\n", 85 | "# Determining the adjacancy matrix via networkx\n", 86 | "# This will always result in a sparse matrix, nodelist argument important to keep order of nodes\n", 87 | "A_di = nx.linalg.graphmatrix.adjacency_matrix(G_di, nodelist=range(1, G_di.number_of_nodes()+1)).toarray()\n", 88 | "print(\"Adjacency Matrix determined by Networkx:\")\n", 89 | "lib.matprint(A_di)\n", 90 | "\n", 91 | "# Determining the Laplacian Matrix via the adjacency matrix from previous step\n", 92 | "# This calculates the outdegree with the weights via networkx, convert it outdegrees to a diagonal array matrix\n", 93 | "# Remember, the alternative way is to take the row sums\n", 94 | "D_out_dict = dict(G_di.out_degree(range(1, G_di.number_of_nodes()+1), weight='weight')) # Keeping the order of the nodes\n", 95 | "D_out = np.diag(list(D_out_dict.values())) # Note: Could also just take the rowsums of A, but working with networkx is shown here\n", 96 | "L_di = D_out - A_di\n", 97 | "\n", 98 | "print(\"\\nLaplacian Matrix:\")\n", 99 | "lib.matprint(L_di)" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "id": "aa768fc8", 105 | "metadata": {}, 106 | "source": [ 107 | "### 6.2.3 Spectrum of L\n", 108 | "\n", 109 | "As reviewied in the book, we can also look at the spectrum of $L$ and visualize the result with the Geršgorin Disks Theorem. Given a weighted digraph $G$ with Laplacian $L$, the eigenvalues of $L$ different from 0 have strictly-positive real part. " 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": null, 115 | "id": "898d1bb4", 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "fig, ax623 = plt.subplots(figsize=custom_figsize_square)\n", 120 | "lib.plot_gersgorin_disks(L_di, ax623, patch_spectrum=False)" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "id": "7ec23835", 126 | "metadata": {}, 127 | "source": [ 128 | "## Example 6.10: Basic Graphs and their algebraic connectivity (and spectrum)\n", 129 | "\n", 130 | "Similar to the basic graphs, their adjacency matrices and spectra from (Table 4.1), we present the algebraic connectivity and the Laplacian Matrix spectrum. Note that the algebraic connectivity value is presented in the legend of the second column subplots! " 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "id": "342e1d8b", 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "n_m = 6 # Can change this value\n", 141 | "# Path Graph\n", 142 | "G_path = nx.path_graph(n_m)\n", 143 | "# Cycle Graph\n", 144 | "G_cycle = nx.cycle_graph(n_m)\n", 145 | "# Star Graph\n", 146 | "G_star = nx.star_graph(n_m-1)\n", 147 | "# Complete Graph\n", 148 | "G_complete = nx.complete_graph(n_m)\n", 149 | "# Complete bipartite Graph\n", 150 | "G_bipartite = nx.complete_bipartite_graph(n_m//2, n_m//2)\n", 151 | "\n", 152 | "all_basic_graphs = {\n", 153 | " \"Path Graph\": G_path,\n", 154 | " \"Cycle Graph\": G_cycle,\n", 155 | " \"Star Graph\":G_star,\n", 156 | " \"Complete Graph\": G_complete,\n", 157 | " \"Complete bipartite Graph\": G_bipartite,\n", 158 | " }\n", 159 | "spectrums = [\n", 160 | " \"$\\{0\\} \\cup \\{2(1-cos(\\pi i / n)) | i ∈ \\{1, . . . , n-1\\}\\}$\",\n", 161 | " \"$\\{0\\} \\cup \\{2(1-cos(2\\pi i/n)) | i ∈ \\{1, . . . , n-1\\}\\}$\",\n", 162 | " \"$\\{ 0,1,...,1,n \\}$\",\n", 163 | " \"$\\{0,n,...,n\\}$\",\n", 164 | " \"$\\{0, m, ... , m, n, ..., n, m+n\\}$\"\n", 165 | "]\n", 166 | "\n", 167 | "alg_conn = [\n", 168 | " \"$2(1-cos(\\pi/n)) $\",\n", 169 | " \"$2(1-cos(2\\pi/n)) $\",\n", 170 | " \"$1$\",\n", 171 | " \"$n$\",\n", 172 | " \"$min(n, m)$\"\n", 173 | "]\n", 174 | "\n", 175 | "# Storing executable equations in a ord list:\n", 176 | "alg_conn_func = {\n", 177 | " \"Path Graph\": (lambda n, m: 2*(1-np.cos(np.pi/n))),\n", 178 | " \"Cycle Graph\": (lambda n, m: 2*(1-np.cos(2*np.pi/n))),\n", 179 | " \"Star Graph\": (lambda n, m: 1),\n", 180 | " \"Complete Graph\": (lambda n, m: n),\n", 181 | " \"Complete bipartite Graph\": (lambda n, m: min(m,n))\n", 182 | "}" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": null, 188 | "id": "ce9f2224", 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [ 192 | "# Plotting the graph itself and its spectrum, with additionally visualizing the algebraic connectivity \n", 193 | "fig, axs610 = plt.subplots(len(all_basic_graphs), 2, figsize=(custom_figsize[0]*1.0, custom_figsize[1]*4))\n", 194 | "for count, (key, graph) in enumerate(all_basic_graphs.items()):\n", 195 | " # First subplot\n", 196 | " if key == \"Complete bipartite Graph\":\n", 197 | " nx.draw_networkx(graph, node_size=100, ax=axs610[count, 0], pos=nx.drawing.layout.bipartite_layout(graph, list(range(0, n_m//2))))\n", 198 | " else:\n", 199 | " nx.draw_networkx(graph, node_size=100, ax=axs610[count, 0], connectionstyle='arc3, rad = 0.1')\n", 200 | " axs610[count, 0].set_xlabel(key)\n", 201 | " # Plotting spectrum of L\n", 202 | " A_tmp = nx.linalg.graphmatrix.adjacency_matrix(graph, nodelist=range(0, graph.number_of_nodes())).toarray()\n", 203 | " D_out_dict_tmp = dict(graph.degree(range(0, graph.number_of_nodes()))) # Keeping the order of the nodes\n", 204 | " D_out_tmp = np.diag(list(D_out_dict_tmp.values()))\n", 205 | " L = D_out_tmp - A_tmp\n", 206 | " lib.plot_spectrum(L, axs610[count, 1]);\n", 207 | " axs610[count, 1].set(title=spectrums[count])\n", 208 | " # Algebraic connectivity\n", 209 | " alg_conn_value = alg_conn_func[key](n_m, n_m)\n", 210 | " axs610[count, 1].plot(alg_conn_value, 0, 'r.', ms=15, label = \"Alg. connectivity:\\n\" + alg_conn[count] + \" = \" + \"%.4g\" % alg_conn_value)\n", 211 | " axs610[count, 1].legend()\n", 212 | " \n", 213 | " #axs610[count, 1].set_xlabel(\"Alg. conn: \" + alg_conn[count] + \" = \" + \"%.4g\" % alg_conn_value)\n", 214 | "fig.subplots_adjust(hspace=0.4)" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "id": "bdf4e460", 220 | "metadata": {}, 221 | "source": [ 222 | "## 6.4 Appendix: Community Detection via algebraic connectivity\n", 223 | "\n", 224 | "In this section the Matlab Code example is implemented in Python below. Remember, that we are looking for the \"bottleneck\" of the graph by optimizing the size of the cut's minima. From the book:\n", 225 | "\n", 226 | "To illustrate these concepts, we borrow an example problem with the corresponding Matlab code from (Gleich, 2006). We construct a randomly generated graph as follows. First, we partition $n$ = 1000 nodes in two groups $V_1$ and $V_2$ of sizes 450 and 550 nodes, respectively. Second, we connect any pair of nodes in the set $V_1$ (respectively $V2$) with probability 0.3 (respectively 0.2). Third and finally, any two nodes in distinct groups, $i ∈ V_1$ and $j ∈ V_2$, are connected with a probability of 0.1. The sparsity pattern of the associated adjacency matrix is shown in the left panel of Figure 6.6. No obvious partition is visible at first glance since the indices are not necessarily sorted, that is, $V_1$ is not necessarily {1, . . . , 450}. The second panel displays the sorted entries of the eigenvector $V_2$ showing a sharp transition between positive and negative entries. Finally, the third panel displays the correspondingly sorted adjacency matrix\n", 227 | "Figure 6.6 is in the Table 6.2 below. " 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": null, 233 | "id": "75d0aaca", 234 | "metadata": {}, 235 | "outputs": [], 236 | "source": [ 237 | "# for a given graph size, randomly assign the nodes to two groups\n", 238 | "n = 1000\n", 239 | "group_size = 450;\n", 240 | "A = np.zeros([1000, 1000])\n", 241 | "x = np.random.permutation(n) - 1 # Random permutation of indices for groups\n", 242 | "group1 = x[0:group_size]\n", 243 | "group2 = x[group_size:];\n", 244 | "\n", 245 | "# assign probabilities of connecting nodes\n", 246 | "# Note: a little bit adapted for better visualization\n", 247 | "p_group1 = 0.4\n", 248 | "p_group2 = 0.3\n", 249 | "p_between_groups = 0.2\n", 250 | "\n", 251 | "#construct adjacency matrix\n", 252 | "A[np.ix_(group1,group1)] = (np.random.rand(group_size, group_size) < p_group1) * 1 # Ensure cast to integer\n", 253 | "A[np.ix_(group2,group2)] = (np.random.rand(n-group_size,n-group_size) < p_group2) * 1\n", 254 | "A[np.ix_(group1,group2)] = (np.random.rand(group_size, n-group_size) < p_between_groups) * 1\n", 255 | "# Ensure symmetry by copying the just created upper triangle part\n", 256 | "A = np.triu(A,1)\n", 257 | "A = A + A.T\n", 258 | "\n", 259 | "# construct Laplacian and its spectrum\n", 260 | "L = np.diag(np.sum(A, 1)) - A;\n", 261 | "\n", 262 | "# Retrieve both 2nd smallest eigenvalues\n", 263 | "D, V = sla.eigs(L, 2, which='SM')\n", 264 | "\n", 265 | "# Get indices for sorting the eigenvector of the corresponding fiedler eigenvalue\n", 266 | "V_sort_ind = np.argsort(V[:, 1])\n", 267 | "\n", 268 | "# Init plot\n", 269 | "fig, axs64 = plt.subplots(3, 1, figsize=(custom_figsize[0]*1.2, custom_figsize[1]*3))\n", 270 | "\n", 271 | "# Plot binary matrix\n", 272 | "lib.plot_matrix_binary(A, axs64[0])\n", 273 | "axs64[0].set_xlabel(\"$A$\")\n", 274 | "\n", 275 | "# Plot the eigenvector values sorted by magnitude\n", 276 | "axs64[1].plot(np.sort(V[:, 1]))\n", 277 | "axs64[1].set_aspect(1 / axs64[1].get_data_ratio()) # Workaround to make it square without equal axis ticks\n", 278 | "axs64[1].set_xlabel(r\"$\\tilde{v}_2$\")\n", 279 | "\n", 280 | "# Plot the adjacency matrix sorted by the eigenvector\n", 281 | "lib.plot_matrix_binary(A[np.ix_(V_sort_ind,V_sort_ind)], axs64[2])\n", 282 | "axs64[2].set_xlabel(r\"$\\widetilde{A}$\");" 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": null, 288 | "id": "cd529553", 289 | "metadata": {}, 290 | "outputs": [], 291 | "source": [] 292 | } 293 | ], 294 | "metadata": { 295 | "kernelspec": { 296 | "display_name": "Python 3 (ipykernel)", 297 | "language": "python", 298 | "name": "python3" 299 | }, 300 | "language_info": { 301 | "codemirror_mode": { 302 | "name": "ipython", 303 | "version": 3 304 | }, 305 | "file_extension": ".py", 306 | "mimetype": "text/x-python", 307 | "name": "python", 308 | "nbconvert_exporter": "python", 309 | "pygments_lexer": "ipython3", 310 | "version": "3.8.11" 311 | }, 312 | "toc": { 313 | "base_numbering": 1, 314 | "nav_menu": {}, 315 | "number_sections": false, 316 | "sideBar": true, 317 | "skip_h1_title": false, 318 | "title_cell": "Table of Contents", 319 | "title_sidebar": "Contents", 320 | "toc_cell": true, 321 | "toc_position": {}, 322 | "toc_section_display": true, 323 | "toc_window_display": false 324 | } 325 | }, 326 | "nbformat": 4, 327 | "nbformat_minor": 5 328 | } 329 | -------------------------------------------------------------------------------- /Chapter_7_continuous_time_averaging_system.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "c8c6c8bb", 6 | "metadata": { 7 | "toc": true 8 | }, 9 | "source": [ 10 | "

Table of Contents

\n", 11 | "
" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "50ac1eb2", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "%matplotlib widget\n", 22 | "\n", 23 | "# Import packages\n", 24 | "import numpy as np\n", 25 | "import matplotlib.pyplot as plt\n", 26 | "import networkx as nx\n", 27 | "\n", 28 | "# For interactive graphs\n", 29 | "import ipywidgets as widgets\n", 30 | "\n", 31 | "# Import self defined functions\n", 32 | "import lib # General library\n", 33 | "\n", 34 | "# Settings\n", 35 | "custom_figsize= (6, 4) # Might need to change this value to fit the figures to your screen\n", 36 | "custom_figsize_square = (5, 5) " 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "id": "e444bc32", 42 | "metadata": {}, 43 | "source": [ 44 | "# Chapter 7 - Continuous-time averaging systems\n", 45 | "\n", 46 | "These Jupyter Notebook scripts contain some examples, visualization and supplements accompanying the book \"Lectures on Network Systems\" by Francesco Bullo http://motion.me.ucsb.edu/book-lns/. These scripts are published with the MIT license. **Make sure to run the first cell above to import all necessary packages and functions and adapt settings in case.** In this script it is necessary to execute cell by cell chronologically due to reocurring examples (Tip: Use the shortcut Shift+Enter to execute each cell). Most of the functions are kept in separate files to keep this script neat." 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "id": "2bc39308", 52 | "metadata": {}, 53 | "source": [ 54 | "## 7.1 Example Systems\n", 55 | "\n", 56 | "In this section we repeat some example covered in the book for visualization and experimentation. Since we use continuous time models, the simulation must be done in some way of discretized manner. These are shown below.\n", 57 | "\n", 58 | "### 7.1.1 Example #1: Continuous-time opinion dynamics\n", 59 | "\n", 60 | "For a simulating example of a French-Harary-DeGroot discrete-time averaging model to a Abelson's continuous timeopinion dynamics model, we actually take a look at **Exercise 5.1** and simulate the results here. The simulation of the continuous-time model is done with $x(t+\\tau) = x(t) + -\\bar{L} x(t) \\tau$.\n", 61 | "\n", 62 | "First we define our network, plot the graph and as for Exercise 5.1 (i), we draw the condensation graph." 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "id": "e4a30124", 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "# Adjacency Matrix\n", 73 | "A_711 = np.array([\n", 74 | " [0.15, 0.15, 0.1, 0.2, 0.4],\n", 75 | " [0, 0.55, 0, 0, 0.45],\n", 76 | " [0.3, 0.05, 0.05, 0, 0.6],\n", 77 | " [0, 0.4, 0.1, 0.5, 0],\n", 78 | " [0, 0.3, 0, 0, 0.7]\n", 79 | "])\n", 80 | "\n", 81 | "# Giving pre defined positions away\n", 82 | "pos711 = {0:[0.1,0.2],1:[.4,.5],2:[.5,.2],3:[.8,.5], 4:[.9,.2]}\n", 83 | "\n", 84 | "G_711 = lib.create_G_from_adj_matrix(A_711)\n", 85 | "\n", 86 | "fig, axs711 = plt.subplots(1, 3, figsize=(custom_figsize[0]*1.2, custom_figsize[1]))\n", 87 | "\n", 88 | "lib.plot_condensated_graph(G_711, axs711, pos711)" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "id": "817ad2a1", 94 | "metadata": {}, 95 | "source": [ 96 | "The next step is to actually simulate the system and plot the results. We can play with the simulation parameters and realize, that as expected the same result as for discretized and continuous system are achieved." 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "id": "a9f564c5", 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "# Settings, can change these\n", 107 | "tau = 0.2\n", 108 | "t_total = 200\n", 109 | "\n", 110 | "x_init = np.diag(A_711) # Initial condition\n", 111 | "\n", 112 | "# Laplace\n", 113 | "L_711 = np.diag(np.sum(A_711, 1)) - A_711\n", 114 | "\n", 115 | "# Simulation\n", 116 | "states = np.zeros((t_total,L_711.shape[0]))\n", 117 | "states[0,:]=x_init\n", 118 | "for i in range(1,t_total):\n", 119 | " states[i,:] = states[i-1,:] + -L_711 @ states[i-1,:] * tau\n", 120 | "\n", 121 | "# From here, interactive network representation\n", 122 | "fig, ax711 = plt.subplots(figsize=custom_figsize)\n", 123 | "fig, v_bound, pos = lib.init_network_sim_plot(G_711, states, fig, pos=pos711)\n", 124 | "\n", 125 | "def inter(timestep):\n", 126 | " lib.update_network(timestep['new'], G=G_711, states_m=states, ax=ax711, vbound=v_bound, pos=pos711)\n", 127 | " return None\n", 128 | "\n", 129 | "# Plot initial configuration\n", 130 | "lib.update_network(0, G=G_711, states_m=states, ax=ax711, vbound=v_bound, pos=pos711)\n", 131 | "\n", 132 | "\n", 133 | "# Widget\n", 134 | "# If this cell is executed twice we are making sure in the following, that the previous widget instances are all closed\n", 135 | "try:\n", 136 | " [c.close() for c in widget711.children] # Note: close_all() does also affect plot, thus list compr.\n", 137 | "except NameError: # Only want to except not defined variable error\n", 138 | " pass\n", 139 | "\n", 140 | "widget711 = lib.create_widgets_play_slider(fnc=inter, minv=0, maxv=t_total-1, step=1, play_speed=1000)\n", 141 | "\n", 142 | "display(widget711)" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "id": "c375c67f", 148 | "metadata": {}, 149 | "source": [ 150 | "### 7.1.3 Example #3: Discretization of partial differential equations\n", 151 | "\n", 152 | "For some intuition and exploration, the heat equation presented in the book is presented below in a minimum example." 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": null, 158 | "id": "b260dc41", 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "# Settings for grid\n", 163 | "n = 4\n", 164 | "\n", 165 | "# Simulation Parameter can change these\n", 166 | "tau2 = 0.01\n", 167 | "t_total2 = 200\n", 168 | "c = 0.1 # thermal diffusivity\n", 169 | "h = 0.2 # Grid length\n", 170 | "\n", 171 | "# Initial\n", 172 | "x_init2 = np.random.rand(n**2) * 100\n", 173 | "\n", 174 | "# Two dimensional grid graph\n", 175 | "G_grid = nx.generators.lattice.grid_graph([n, n])\n", 176 | "# Save Positions for plot later and normalize coordinates from original node coordinate\n", 177 | "pos712 = dict(zip(range(0, n**2), [tuple([y/n for y in x]) for x in G_grid.nodes]))\n", 178 | "\n", 179 | "# Relabel for simplicity\n", 180 | "G_grid = nx.relabel.relabel_nodes(G_grid, dict(zip(G_grid.nodes, range(0, n**2))))\n", 181 | "\n", 182 | "A_grid = nx.linalg.graphmatrix.adjacency_matrix(G_grid, nodelist=G_grid.nodes).toarray()\n", 183 | "L_grid = np.diag(np.sum(A_grid, 1)) - A_grid\n", 184 | "\n", 185 | "# Simulation\n", 186 | "states2 = np.zeros((t_total2,L_grid.shape[0]))\n", 187 | "states2[0,:] = x_init2\n", 188 | "for i in range(1,t_total2):\n", 189 | " states2[i,:] = states2[i-1,:] + -(c/h**2)*L_grid @ states2[i-1,:] * tau2" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": null, 195 | "id": "e86a949f", 196 | "metadata": {}, 197 | "outputs": [], 198 | "source": [ 199 | "# From here, interactive network representation\n", 200 | "fig, ax712 = plt.subplots(figsize=custom_figsize)\n", 201 | "fig, v_bound, pos712 = lib.init_network_sim_plot(G_grid, states2, fig, pos=pos712)\n", 202 | "\n", 203 | "def inter2(timestep):\n", 204 | " lib.update_network(timestep['new'], G=G_grid, states_m=states2, ax=ax712, vbound=v_bound, pos=pos712)\n", 205 | " return None\n", 206 | "\n", 207 | "# Plot initial configuration\n", 208 | "lib.update_network(0, G=G_grid, states_m=states2, ax=ax712, vbound=v_bound, pos=pos712)\n", 209 | "\n", 210 | "\n", 211 | "# Widget\n", 212 | "# If this cell is executed twice we are making sure in the following, that the previous widget instances are all closed\n", 213 | "try:\n", 214 | " [c.close() for c in widget712.children] # Note: close_all() does also affect plot, thus list compr.\n", 215 | "except NameError: # Only want to except not defined variable error\n", 216 | " pass\n", 217 | "\n", 218 | "widget712 = lib.create_widgets_play_slider(fnc=inter2, minv=0, maxv=t_total2-1, step=1, play_speed=1000)\n", 219 | "\n", 220 | "display(widget712)" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": null, 226 | "id": "28b5793c", 227 | "metadata": {}, 228 | "outputs": [], 229 | "source": [] 230 | } 231 | ], 232 | "metadata": { 233 | "kernelspec": { 234 | "display_name": "Python 3 (ipykernel)", 235 | "language": "python", 236 | "name": "python3" 237 | }, 238 | "language_info": { 239 | "codemirror_mode": { 240 | "name": "ipython", 241 | "version": 3 242 | }, 243 | "file_extension": ".py", 244 | "mimetype": "text/x-python", 245 | "name": "python", 246 | "nbconvert_exporter": "python", 247 | "pygments_lexer": "ipython3", 248 | "version": "3.8.11" 249 | }, 250 | "toc": { 251 | "base_numbering": 1, 252 | "nav_menu": {}, 253 | "number_sections": false, 254 | "sideBar": true, 255 | "skip_h1_title": false, 256 | "title_cell": "Table of Contents", 257 | "title_sidebar": "Contents", 258 | "toc_cell": true, 259 | "toc_position": {}, 260 | "toc_section_display": true, 261 | "toc_window_display": false 262 | } 263 | }, 264 | "nbformat": 4, 265 | "nbformat_minor": 5 266 | } 267 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Erik Suer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lectures on Network Systems [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/Erikx3/Lectures-on-Network-Systems/main) 2 | 3 | These Jupyter Notebook scripts contain some examples, visualization and supplements accompanying the book "Lectures on Network Systems" by Francesco Bullo http://motion.me.ucsb.edu/book-lns/. It is initialized during visiting the lecture UCSB course ME/ECE 269 “Network Systems”, Fall 2021. These scripts are available on binders https://mybinder.org/v2/gh/Erikx3/Lectures-on-Network-Systems/main aswell to run it without installing an own environment. Otherwise make sure to have the packages installed as listed in the [requirements](requirements.txt) file. 4 | 5 | ![Book Cover Lectures on Network System](/img/bookcover.png?raw=true "Lectures on Network System") 6 | 7 | Book information: 8 | ``` 9 | Francesco Bullo 10 | Department of Mechanical Engineering 11 | Center for Control, Dynamical-Systems, and Computation 12 | University of California, Santa Barbara 13 | bullo at ucsb.edu 14 | 15 | Edition 1.5, Sep 1, 2021 16 | 332 pages and 178 exercises 17 | Kindle Direct Publishing 18 | ISBN 978-1-986425-64-3 19 | ``` 20 | 21 | Citation information 22 | ``` 23 | @Book{FB-LNS, 24 | author = {F. Bullo}, 25 | title = {Lectures on Network Systems}, 26 | year = 2021, 27 | edition = 1.5, 28 | publisher = {Kindle Direct Publishing}, 29 | ISBN = {978-1986425643}, 30 | url = {http://motion.me.ucsb.edu/book-lns}, 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /UCSB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erikx3/Lectures-on-Network-Systems/989e0f3d012ad149ea37ad158a972ce3de75f178/UCSB.jpg -------------------------------------------------------------------------------- /ch1_lib.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib as mpl 3 | 4 | # Bug example specific 5 | def plot_circle_and_bugs(ax, radius, bugs): 6 | # Set xlim, ylim and aspect ratio 7 | ax.set_xlim(-1.5, 1.5) 8 | ax.set_ylim(-1.5, 1.5) 9 | ax.set_aspect('equal') 10 | 11 | # Add a circle 12 | circle = mpl.patches.Circle((0, 0), radius=radius, fill=False) 13 | ax.add_patch(circle) 14 | np.random.seed(4) 15 | for i in range(0, bugs.shape[0]): 16 | # Add a bug 17 | bug = mpl.patches.Circle((bugs[i, 1], bugs[i, 2]), radius=radius * 0.08, color=np.random.rand(3,)) 18 | ax.add_patch(bug) 19 | 20 | 21 | def init_bugs(radius, n_bugs, n_steps): 22 | np.random.seed() 23 | 24 | # For each bug store its theta position and the resulting (x, y) position on the plane 25 | bugs = np.zeros((n_bugs, 3)) 26 | 27 | # Each state contains the position of all bugs 28 | states_bugs = np.zeros((n_steps, n_bugs, 3)) 29 | 30 | # Set initial positions and state 31 | for i in range(0, n_bugs): 32 | theta_i = (i * np.pi/n_bugs + np.random.uniform(-np.pi/(n_bugs*3), np.pi/(n_bugs*3))) % (2*np.pi) 33 | bugs[i] = [theta_i, radius * np.cos(theta_i), radius * np.sin(theta_i)] 34 | 35 | # Save the first state 36 | states_bugs[0] = bugs 37 | return bugs, states_bugs 38 | 39 | 40 | def simulate_bugs_cyclic_pursuit(radius, n_bugs, n_steps, gain, bugs, states_bugs): 41 | for k in range(1, n_steps): 42 | 43 | old_bugs = bugs.copy() 44 | # Update bugs's positions 45 | for i in range(0, n_bugs): 46 | dist_cc = (old_bugs[(i+1) % n_bugs,0] - old_bugs[i,0]) % (2*np.pi) 47 | u = gain * dist_cc 48 | theta_new = (old_bugs[i,0] + u) % (2*np.pi) 49 | bugs[i] = [theta_new, radius * np.cos(theta_new), radius * np.sin(theta_new)] 50 | 51 | states_bugs[k] = bugs 52 | return states_bugs 53 | 54 | 55 | def simulate_bugs_cyclic_balancing(radius, n_bugs, n_steps, gain, bugs, states_bugs): 56 | for k in range(1, n_steps): 57 | old_bugs = bugs.copy() 58 | # Update bugs's positions 59 | for i in range(0, n_bugs): 60 | dist_cc = (old_bugs[(i+1) % n_bugs,0] - old_bugs[i,0]) % (2*np.pi) 61 | dist_c = (old_bugs[i,0] - old_bugs[(i-1) % n_bugs,0]) % (2*np.pi) 62 | u = gain * dist_cc - gain*dist_c 63 | theta_new = (old_bugs[i,0] + u) % (2*np.pi) 64 | bugs[i] = [theta_new, radius * np.cos(theta_new), radius * np.sin(theta_new)] 65 | 66 | states_bugs[k] = bugs 67 | return states_bugs 68 | -------------------------------------------------------------------------------- /img/bookcover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erikx3/Lectures-on-Network-Systems/989e0f3d012ad149ea37ad158a972ce3de75f178/img/bookcover.png -------------------------------------------------------------------------------- /img/layouts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erikx3/Lectures-on-Network-Systems/989e0f3d012ad149ea37ad158a972ce3de75f178/img/layouts.png -------------------------------------------------------------------------------- /lib.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import networkx as nx 3 | import matplotlib.pyplot as plt 4 | import matplotlib as mpl 5 | import ipywidgets as widgets 6 | import scipy.linalg as spla 7 | from matplotlib.colors import ListedColormap 8 | from math import gcd 9 | from functools import reduce 10 | 11 | 12 | def is_periodic(G): 13 | """ 14 | https://stackoverflow.com/questions/54030163/periodic-and-aperiodic-directed-graphs 15 | Own function to test, whether a given Graph is aperiodic: 16 | """ 17 | if not nx.is_strongly_connected(G): 18 | print("G is not strongly connected, periodicity not defined.") 19 | return False 20 | cycles = list(nx.algorithms.cycles.simple_cycles(G)) 21 | cycles_sizes = [len(c) for c in cycles] # Find all cycle sizes 22 | cycles_gcd = reduce(gcd, cycles_sizes) # Find greatest common divisor of all cycle sizes 23 | is_periodic = cycles_gcd > 1 24 | return is_periodic 25 | 26 | 27 | def plot_node_val_2D(states, x_0, t, ax, legend=True, avg=True): 28 | """ 29 | Function to plot the states on a 2d-axis 30 | """ 31 | x_axis = np.arange(t) 32 | for i in range(states.shape[1]): 33 | ax.plot(x_axis, states[:,i], label=str(i)) 34 | if avg: 35 | average = np.ones(t) * np.sum(x_0)/states.shape[1] 36 | ax.plot(x_axis, average, '--', label='mean(x_0)') 37 | if legend: 38 | ax.legend() 39 | 40 | 41 | def init_network_sim_plot(G, states, fig, vbound=None, pos=None): 42 | """ 43 | Function to initialize Network: 44 | - Setting up the figure with colorbar 45 | - Determine position of Nodes and boundaries for values 46 | 47 | Parameters 48 | ---------- 49 | G : nx.graph 50 | Graph that will be displayed 51 | states: np.array 52 | Values of nodes through the simulation 53 | ax: plt.axes 54 | Previously defined axis handle 55 | vbound: 56 | Value boundaries for the coloring 57 | pos : np.array 58 | Position of nodes if already provided for plot 59 | 60 | Returns 61 | ------- 62 | fig, ax 63 | figure and axes where the graph can be plotted on 64 | vbound 65 | Boundary values that should be used from now on 66 | pos 67 | position of the node for the graph plot 68 | """ 69 | 70 | if pos is None: 71 | pos = nx.drawing.layout.spring_layout(G) 72 | if vbound is None: 73 | vmax_s = np.amax(states) 74 | vmin_s = np.amin(states) 75 | else: 76 | vmax_s = vbound[1] 77 | vmin_s = vbound[0] 78 | norm = mpl.colors.Normalize(vmin=vmin_s, vmax=vmax_s) 79 | cmap = plt.get_cmap('viridis') 80 | sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm) 81 | sm.set_array([]) 82 | fig.colorbar(sm) 83 | return fig, [vmin_s, vmax_s], pos 84 | 85 | 86 | def update_network(i, G, states_m, ax, vbound, pos, labels=True): 87 | """ 88 | Function to plot and visualize the state values in a graph 89 | Requirements: 90 | - Graph label of nodes must go from 0...n 91 | - states is a timesteps*n np.array 92 | 93 | Parameters 94 | ---------- 95 | i: int 96 | timestep 97 | G : nx.graph 98 | Graph that will be displayed 99 | states: np.array 100 | Values of nodes through the simulation 101 | ax: plt.axes 102 | Previously defined axis handle 103 | vbound: 104 | Value boundaries for the coloring 105 | pos : np.array 106 | Position of nodes if already provided for plot 107 | 108 | Returns 109 | ------- 110 | None 111 | """ 112 | ax.clear() # Clear axes 113 | colors = states_m[i] 114 | cmap = plt.get_cmap('viridis') 115 | # draw with new color map 116 | nx.draw_networkx(G, pos, node_size=200, cmap=cmap, 117 | node_color=colors, vmin=vbound[0], vmax=vbound[1], ax=ax) 118 | if labels: 119 | # From here: Add actual state value as label 120 | pos_higher = {} 121 | x_off = 0.00 # offset on the x axis 122 | y_off = 0.03 # offset on the y axis 123 | for k, v in pos.items(): 124 | pos_higher[k] = (v[0] + x_off, v[1] + y_off) 125 | labels = ["%.4g" % num for num in states_m[i]] # Casting node value to string 126 | labels = dict(zip(list(range(0, len(labels))), labels)) # Matching node value to node number in a dict 127 | try: 128 | nx.draw_networkx_labels(G, pos_higher, labels, ax=ax) 129 | except KeyError as e: 130 | raise Exception('Make sure ur Nodes are labeled from 0 ... n-1') from e 131 | # If there is a failure due to key error, it is required for the labels to be numbered from 0 .. n!! 132 | 133 | 134 | def simulate_network(A, x_0, t): 135 | """ 136 | Function to simulate the network and save the state 137 | """ 138 | states = np.zeros((t,A.shape[0])) 139 | states[0,:]=x_0 140 | for i in range(1,t): 141 | states[i,:] = A @ states[i-1,:] 142 | return states 143 | 144 | 145 | def create_widgets_play_slider(fnc, minv, maxv, step=1, play_speed=200): 146 | """ 147 | Create an integer slider and a playbutton for interactive plots 148 | Parameters 149 | ---------- 150 | fnc: function 151 | function where the widget will be applied on 152 | minv : int 153 | minimum value for the slider 154 | maxv: int 155 | maximum value for the slider 156 | step: int 157 | step for the slider 158 | play_speed: int 159 | Speed for incrementing the integer for the play button in ms 160 | 161 | Returns 162 | ------- 163 | widgets 164 | """ 165 | 166 | slider = widgets.IntSlider(min=minv, max=maxv, step=step, continuous_update=True) 167 | play = widgets.Play(min=minv,max=maxv, step=step, interval=play_speed) 168 | 169 | slider.observe(fnc, 'value') 170 | widgets.jslink((play, 'value'), (slider, 'value')) 171 | widgets.VBox([play, slider]) 172 | return widgets.VBox([play, slider]) 173 | 174 | 175 | # Putting some simulations parts into custom functions 176 | def interactive_network_plot(G, states, pos, t, fig, ax): 177 | fig, v_bound, pos = init_network_sim_plot(G, states, fig, pos=pos) 178 | 179 | def inter(timestep): 180 | update_network(timestep['new'], G=G, states_m=states, ax=ax, vbound=v_bound, pos=pos) 181 | return None 182 | 183 | # Plot initial configuration 184 | update_network(0, G=G, states_m=states, ax=ax, vbound=v_bound, pos=pos) 185 | 186 | widget = create_widgets_play_slider(fnc=inter, minv=0, maxv=t-1, step=1, play_speed=1000) 187 | 188 | return widget 189 | 190 | 191 | # Linear Algebra related functions 192 | 193 | def matprint(mat, fmt="g"): 194 | """ 195 | Own defined function to beautiful print matrices in the output. 196 | """ 197 | # Handling 1d-arrays 198 | if np.ndim(mat) == 1: 199 | mat = mat.reshape(mat.shape[0], 1) 200 | # Handling column spaces 201 | col_maxes = [max([len(("{:" + fmt + "}").format(x)) for x in col]) for col in mat.T] 202 | 203 | for x in mat: 204 | for i, y in enumerate(x): 205 | if i == 0: 206 | print(("| {:" + str(col_maxes[i]) + fmt + "}").format(y), end=" ") 207 | else: 208 | print(("{:" + str(col_maxes[i]) + fmt + "}").format(y), end=" ") 209 | print("|") 210 | 211 | 212 | def plot_spectrum(M, ax, patch=True): 213 | """ 214 | Scatter plot of eigs in complex plane overlayed by radius circle 215 | """ 216 | eigvals = spla.eig(M)[0] # Extract only eigenvalues 217 | 218 | ax.scatter(eigvals.real, eigvals.imag) 219 | if max(abs(eigvals)) < 1: 220 | ax.set_xlim(-1.05, 1.05) 221 | ax.set_ylim(-1.05, 1.05) 222 | 223 | r = max(abs(eigvals)) # Note: abs() automatically calculated the magnitude of complex values 224 | circle = mpl.patches.Circle((0, 0), radius=r, alpha=.2, ec='black') 225 | if patch: 226 | ax.add_patch(circle) 227 | ax.axhline(0, color='black', ls='--', linewidth=1) 228 | ax.axvline(0, color='black', ls='--', linewidth=1) 229 | ax.axis('equal') 230 | return eigvals, ax 231 | 232 | 233 | def plot_gersgorin_disks(M, ax, patch_spectrum=True): 234 | """ 235 | Scatter plot of eigenvalues overlayed with gersgorin disks (marked with green edges) 236 | """ 237 | eigvals, ax = plot_spectrum(M, ax, patch_spectrum) 238 | 239 | row_sums = M.sum(axis=1) 240 | for i in range(M.shape[0]): 241 | radius = row_sums[i] - M[i, i] 242 | ax.add_patch(mpl.patches.Circle((M[i, i], 0), radius=radius, 243 | alpha=.6 / M.shape[0], ec='green')) 244 | ax.autoscale_view() 245 | 246 | 247 | def sum_of_powers(M, n): 248 | """Returns the sum of the [0,n) powers of M""" 249 | result = np.zeros(M.shape) 250 | for i in range(n): 251 | result += np.linalg.matrix_power(M, i) 252 | return result 253 | 254 | 255 | def is_irreducible(M): 256 | """Returns whether or not given square, positive matrix is irreducible""" 257 | Mk = sum_of_powers(M, M.shape[0]) 258 | return not np.any(Mk == 0) 259 | 260 | 261 | def is_node_globally_reachable(M, i): 262 | """Returns whether or not given node in given square, positive matrix is globally reachable""" 263 | power_sum = sum_of_powers(M, M.shape[0]) 264 | return not np.any(power_sum[:, i] == 0) 265 | 266 | 267 | def is_primitive(M): 268 | """ 269 | Returns whether of not a given square is primitive 270 | 271 | Corollary 8.5.8 in Horn & Johnson, Matrix Analysis: 272 | Let A be an n×n non-negative matrix. Then A is primitive if and only if A^(n2−2n+2) has only positive entries. 273 | """ 274 | n = M.shape[0] 275 | return not np.any(np.linalg.matrix_power(M, n ** 2 - 2 * n + 2) == 0) 276 | 277 | 278 | def create_G_from_adj_matrix(A): 279 | """ 280 | 281 | :param A: Adjacency Matrix 282 | :return: Graph with node 0 ... n based on adjacency matrix 283 | """ 284 | G = nx.DiGraph() 285 | for i in range(len(A)): 286 | for j in range(len(A[i])): 287 | if (A[i, j]): 288 | G.add_edge(i, j, weight=A[i, j]) 289 | return G 290 | 291 | 292 | def draw_adj_matrix(A, ax): 293 | """ 294 | Draw network based on given adjacency matrix 295 | """ 296 | G = create_G_from_adj_matrix(A) 297 | pos = nx.drawing.layout.spring_layout(G, seed=4) 298 | nx.draw_networkx(G, pos=pos, node_size=100, ax=ax, connectionstyle='arc3, rad = 0.1') 299 | return G 300 | 301 | 302 | def plot_matrix_binary(M, ax, name=''): 303 | """ 304 | Drawing binary plot of adjacency matrix 305 | """ 306 | blue_map = ListedColormap(["blue", "white"]) 307 | zeros = M == 0 308 | im = ax.imshow(zeros, cmap=blue_map) 309 | ax.set_xticks([]) 310 | ax.set_yticks([]) 311 | 312 | 313 | def plot_condensated_graph(G, axs3, pos=None): 314 | """ 315 | 316 | :param G: Graph which condensation should be drawn 317 | :param axs3: 1 dim axis with at least 3 subplots locations 318 | """ 319 | 320 | # Visualization of initial Graph 321 | if pos is None: 322 | pos_rand = nx.spring_layout(G) #setting the positions with respect to G, not k. 323 | else: 324 | pos_rand = pos 325 | nx.draw_networkx(G, pos=pos_rand, node_size=40, ax=axs3[0], connectionstyle='arc3, rad = 0.2', with_labels=False) 326 | 327 | # Algorithm to find the condensed graph: 328 | G_conden = nx.algorithms.components.condensation(G) 329 | 330 | all_col = [] 331 | # We do the following for coloring scheme and saving that coloring scheme for the condensated graph 332 | for u, node in G_conden.nodes(data=True): 333 | sg = node['members'] # This contains a set of nodes from previous graph, that belongs to the condensated node 334 | co = np.random.rand(1,3) 335 | all_col.append(co) 336 | nx.draw_networkx_nodes(G.subgraph(sg), pos=pos_rand, node_size=40, node_color=co, ax=axs3[1]) 337 | nx.draw_networkx_edges(G, pos=pos_rand, edgelist=G.edges(sg), edge_color=co, ax=axs3[1], connectionstyle='arc3, rad = 0.2') 338 | 339 | nx.draw_networkx(G_conden, node_size=40, ax=axs3[2], node_color=all_col, connectionstyle='arc3, rad = 0.2', with_labels=False) 340 | axs3[0].set_xlabel("Original Digraph") 341 | axs3[1].set_xlabel("Strongly connected components") 342 | axs3[2].set_xlabel("Condensation"); -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Python = 3.8.11 2 | numpy==1.20.3 3 | matplotlib==3.4.2 4 | networkx==2.6.3 5 | scipy==1.7.2 6 | sympy==1.6.2 7 | ipykernel==6.4.1 8 | ipython==7.31.1 9 | ipython_genutils==0.2.0 10 | ipywidgets==7.6.5 11 | ipympl==0.7.0 12 | --------------------------------------------------------------------------------