|
503 | 503 | " # To display the DataFrame with interactive features, including sticky headers AND frozen first column\n", |
504 | 504 | " show(df, scrollY=\"300px\", scrollCollapse=True, fixedColumns=True, pageLength=-1)" |
505 | 505 | ] |
| 506 | + }, |
| 507 | + { |
| 508 | + "cell_type": "markdown", |
| 509 | + "id": "8575f2b2-a73e-400f-af14-f6c833657a43", |
| 510 | + "metadata": {}, |
| 511 | + "source": [ |
| 512 | + "## Resolution parameter on Sierpinski" |
| 513 | + ] |
| 514 | + }, |
| 515 | + { |
| 516 | + "cell_type": "code", |
| 517 | + "execution_count": null, |
| 518 | + "id": "fbc4947f-38e1-4086-9c2f-522bacf63e75", |
| 519 | + "metadata": {}, |
| 520 | + "outputs": [], |
| 521 | + "source": [ |
| 522 | + "#import igraph as ig\n", |
| 523 | + "#import matplotlib.pyplot as plt\n", |
| 524 | + "#import numpy as np\n", |
| 525 | + "#import ipywidgets as widgets\n", |
| 526 | + "#from IPython.display import display, clear_output\n", |
| 527 | + "#import matplotlib.cm as cm \n", |
| 528 | + "#import matplotlib.colors as mcolors\n", |
| 529 | + "#import colorcet as cc\n", |
| 530 | + "#ig.config[\"plotting.backend\"] = \"matplotlib\"" |
| 531 | + ] |
| 532 | + }, |
| 533 | + { |
| 534 | + "cell_type": "code", |
| 535 | + "execution_count": 1, |
| 536 | + "id": "a9181bf9-eaf5-488b-a600-c5e3db066240", |
| 537 | + "metadata": {}, |
| 538 | + "outputs": [], |
| 539 | + "source": [ |
| 540 | + "# Global state for node mapping (needed because igraph uses integer IDs)\n", |
| 541 | + "_coord_to_id = {}\n", |
| 542 | + "_coords_list = [] # Stores (x, y) tuples in order of their assigned ID\n", |
| 543 | + "_next_id = 0\n", |
| 544 | + "\n", |
| 545 | + "def _get_or_create_vertex(G, coord):\n", |
| 546 | + " \"\"\"\n", |
| 547 | + " Helper function to get the igraph vertex ID for a given coordinate.\n", |
| 548 | + " If the coordinate doesn't exist, it creates a new vertex in G and assigns an ID.\n", |
| 549 | + " \"\"\"\n", |
| 550 | + " global _coord_to_id, _coords_list, _next_id\n", |
| 551 | + " coord_tuple = tuple(coord) # Ensure the coordinate is a hashable tuple\n", |
| 552 | + "\n", |
| 553 | + " if coord_tuple not in _coord_to_id:\n", |
| 554 | + " _coord_to_id[coord_tuple] = _next_id\n", |
| 555 | + " _coords_list.append(coord_tuple)\n", |
| 556 | + " G.add_vertex() # Add a new vertex to the igraph graph\n", |
| 557 | + " _next_id += 1\n", |
| 558 | + " return _coord_to_id[coord_tuple]\n", |
| 559 | + "\n", |
| 560 | + "def _sierpinski_igraph(G, p1, p2, p3, depth):\n", |
| 561 | + " \"\"\"\n", |
| 562 | + " Recursive function to build the Sierpiński triangle structure in an igraph Graph.\n", |
| 563 | + " This function uses integer IDs for vertices after mapping from coordinates.\n", |
| 564 | + " \"\"\"\n", |
| 565 | + " if depth == 0:\n", |
| 566 | + " id1 = _get_or_create_vertex(G, p1)\n", |
| 567 | + " id2 = _get_or_create_vertex(G, p2)\n", |
| 568 | + " id3 = _get_or_create_vertex(G, p3)\n", |
| 569 | + " if not G.are_adjacent(id1, id2): G.add_edge(id1, id2)\n", |
| 570 | + " if not G.are_adjacent(id2, id3): G.add_edge(id2, id3)\n", |
| 571 | + " if not G.are_adjacent(id3, id1): G.add_edge(id3, id1)\n", |
| 572 | + " else:\n", |
| 573 | + " a = ((p1[0]+p2[0])/2, (p1[1]+p2[1])/2)\n", |
| 574 | + " b = ((p2[0]+p3[0])/2, (p2[1]+p3[1])/2)\n", |
| 575 | + " c = ((p3[0]+p1[0])/2, (p3[1]+p1[1])/2)\n", |
| 576 | + " _sierpinski_igraph(G, p1, a, c, depth-1)\n", |
| 577 | + " _sierpinski_igraph(G, a, p2, b, depth-1)\n", |
| 578 | + " _sierpinski_igraph(G, c, b, p3, depth-1)\n", |
| 579 | + "\n", |
| 580 | + "def draw_sierpinski_igraph_on_axes(depth, ax):\n", |
| 581 | + " \"\"\"\n", |
| 582 | + " Generates and draws a Sierpiński triangle using igraph onto a given Matplotlib Axes.\n", |
| 583 | + " \"\"\"\n", |
| 584 | + " global _coord_to_id, _coords_list, _next_id\n", |
| 585 | + " \n", |
| 586 | + " _coord_to_id = {}\n", |
| 587 | + " _coords_list = []\n", |
| 588 | + " _next_id = 0\n", |
| 589 | + "\n", |
| 590 | + " G = ig.Graph()\n", |
| 591 | + " p1, p2, p3 = (0, 0), (1, 0), (0.5, np.sqrt(3)/2) \n", |
| 592 | + " _sierpinski_igraph(G, p1, p2, p3, depth)\n", |
| 593 | + "\n", |
| 594 | + " ax.clear()\n", |
| 595 | + "\n", |
| 596 | + " if not G.vcount():\n", |
| 597 | + " ax.set_title(f\"Sierpiński Triangle - Depth {depth} (No vertices)\")\n", |
| 598 | + " ax.set_xticks([])\n", |
| 599 | + " ax.set_yticks([])\n", |
| 600 | + " ax.axis('off')\n", |
| 601 | + " return\n", |
| 602 | + "\n", |
| 603 | + " layout = _coords_list\n", |
| 604 | + " ig.plot(G, target=ax, layout=layout, vertex_size=5, vertex_color=\"black\", \n", |
| 605 | + " vertex_label=None, edge_color=\"blue\", edge_width=1,\n", |
| 606 | + " bbox=(0, 0, 600, 600), margin=20)\n", |
| 607 | + " \n", |
| 608 | + " ax.set_title(f\"Sierpiński Triangle - Depth {depth}\")\n", |
| 609 | + " ax.set_aspect('equal', adjustable='box')\n", |
| 610 | + " ax.set_xticks([])\n", |
| 611 | + " ax.set_yticks([])\n", |
| 612 | + " ax.axis('off')\n", |
| 613 | + "\n", |
| 614 | + "# Global state for node mapping (needed because igraph uses integer IDs)\n", |
| 615 | + "_coord_to_id = {}\n", |
| 616 | + "_coords_list = [] # Stores (x, y) tuples in order of their assigned ID\n", |
| 617 | + "_next_id = 0\n", |
| 618 | + "\n", |
| 619 | + "def _get_or_create_vertex(G, coord):\n", |
| 620 | + " \"\"\"\n", |
| 621 | + " Helper function to get the igraph vertex ID for a given coordinate.\n", |
| 622 | + " If the coordinate doesn't exist, it creates a new vertex in G and assigns an ID.\n", |
| 623 | + " \"\"\"\n", |
| 624 | + " global _coord_to_id, _coords_list, _next_id\n", |
| 625 | + " coord_tuple = tuple(coord) # Ensure the coordinate is a hashable tuple\n", |
| 626 | + "\n", |
| 627 | + " if coord_tuple not in _coord_to_id:\n", |
| 628 | + " _coord_to_id[coord_tuple] = _next_id\n", |
| 629 | + " _coords_list.append(coord_tuple)\n", |
| 630 | + " G.add_vertex() # Add a new vertex to the igraph graph\n", |
| 631 | + " _next_id += 1\n", |
| 632 | + " return _coord_to_id[coord_tuple]\n", |
| 633 | + "\n", |
| 634 | + "def _sierpinski_igraph_builder(G, p1, p2, p3, depth):\n", |
| 635 | + " \"\"\"\n", |
| 636 | + " Recursive function to build the Sierpiński triangle structure in an igraph Graph.\n", |
| 637 | + " This function uses integer IDs for vertices after mapping from coordinates.\n", |
| 638 | + " \"\"\"\n", |
| 639 | + " if depth == 0:\n", |
| 640 | + " id1 = _get_or_create_vertex(G, p1)\n", |
| 641 | + " id2 = _get_or_create_vertex(G, p2)\n", |
| 642 | + " id3 = _get_or_create_vertex(G, p3)\n", |
| 643 | + " if not G.are_adjacent(id1, id2): G.add_edge(id1, id2)\n", |
| 644 | + " if not G.are_adjacent(id2, id3): G.add_edge(id2, id3)\n", |
| 645 | + " if not G.are_adjacent(id3, id1): G.add_edge(id3, id1)\n", |
| 646 | + " else:\n", |
| 647 | + " a = ((p1[0]+p2[0])/2, (p1[1]+p2[1])/2)\n", |
| 648 | + " b = ((p2[0]+p3[0])/2, (p2[1]+p3[1])/2)\n", |
| 649 | + " c = ((p3[0]+p1[0])/2, (p3[1]+p1[1])/2)\n", |
| 650 | + " _sierpinski_igraph_builder(G, p1, a, c, depth-1)\n", |
| 651 | + " _sierpinski_igraph_builder(G, a, p2, b, depth-1)\n", |
| 652 | + " _sierpinski_igraph_builder(G, c, b, p3, depth-1)\n", |
| 653 | + "\n", |
| 654 | + "def get_sierpinski_graph_and_layout(depth):\n", |
| 655 | + " \"\"\"\n", |
| 656 | + " Generates a Sierpiński triangle graph and its coordinate layout.\n", |
| 657 | + " Returns the igraph.Graph object and the list of (x,y) coordinates for its layout.\n", |
| 658 | + " \"\"\"\n", |
| 659 | + " import numpy as np\n", |
| 660 | + " import igraph as ig\n", |
| 661 | + " \n", |
| 662 | + " global _coord_to_id, _coords_list, _next_id\n", |
| 663 | + " \n", |
| 664 | + " _coord_to_id = {}\n", |
| 665 | + " _coords_list = []\n", |
| 666 | + " _next_id = 0\n", |
| 667 | + "\n", |
| 668 | + " G = ig.Graph()\n", |
| 669 | + " p1, p2, p3 = (0, 0), (1, 0), (0.5, np.sqrt(3)/2) \n", |
| 670 | + " _sierpinski_igraph_builder(G, p1, p2, p3, depth)\n", |
| 671 | + "\n", |
| 672 | + " if not G.vcount():\n", |
| 673 | + " print(f\"Warning: Sierpiński graph at depth {depth} has no vertices.\")\n", |
| 674 | + " return G, [] # Return empty layout if no vertices\n", |
| 675 | + "\n", |
| 676 | + " return G, _coords_list\n", |
| 677 | + "\n", |
| 678 | + "# --- Leiden Clustering and Plotting Function ---\n", |
| 679 | + "\n", |
| 680 | + "def plot_leiden_communities_on_axes(graph, layout, resolution, ax, title_suffix=\"\"):\n", |
| 681 | + " \"\"\"\n", |
| 682 | + " Clusters a given graph using Leiden with a specified resolution and plots it\n", |
| 683 | + " onto the provided Matplotlib Axes, coloring vertices by community.\n", |
| 684 | + " \"\"\"\n", |
| 685 | + " import colorcet as cc\n", |
| 686 | + " \n", |
| 687 | + " ax.clear() # Clear the axes for the new plot\n", |
| 688 | + "\n", |
| 689 | + " communities = graph.community_leiden(objective_function=\"modularity\", resolution=resolution)\n", |
| 690 | + " \n", |
| 691 | + " num_communities = len(communities)\n", |
| 692 | + "\n", |
| 693 | + " palette = cc.glasbey_dark\n", |
| 694 | + " vertex_colors = [palette[membership_id % len(palette)] for membership_id in communities.membership]\n", |
| 695 | + "\n", |
| 696 | + " # Handle case where there are no communities or no vertices (though Leiden usually finds at least 1)\n", |
| 697 | + " if not vertex_colors and graph.vcount() > 0:\n", |
| 698 | + " vertex_colors = [\"lightgray\"] * graph.vcount()\n", |
| 699 | + " elif graph.vcount() == 0:\n", |
| 700 | + " ax.set_title(f\"Sierpiński Triangle - No vertices (Depth {SIERPINSKI_DEPTH})\")\n", |
| 701 | + " ax.axis('off')\n", |
| 702 | + " return\n", |
| 703 | + "\n", |
| 704 | + " ig.plot(\n", |
| 705 | + " graph,\n", |
| 706 | + " target=ax,\n", |
| 707 | + " layout=layout,\n", |
| 708 | + " vertex_size=32, # Adjust node size (smaller for higher depth for clarity)\n", |
| 709 | + " vertex_color=vertex_colors, # Use community-assigned colors\n", |
| 710 | + " vertex_label=None, # No labels for vertices\n", |
| 711 | + " edge_color=\"black\", # Edge color (can be made less prominent)\n", |
| 712 | + " edge_width=1.5,\n", |
| 713 | + " bbox=(0, 0, 600, 600), # Bounding box for the internal renderer\n", |
| 714 | + " margin=20, # Margin around the plot area\n", |
| 715 | + " )\n", |
| 716 | + " \n", |
| 717 | + " # Set plot title to indicate resolution and number of communities\n", |
| 718 | + " ax.set_title(f\"Resolution: {resolution:.3f} ({num_communities} comms) {title_suffix}\")\n", |
| 719 | + " ax.set_aspect('equal', adjustable='box') # Ensure the aspect ratio is 1:1\n", |
| 720 | + " ax.set_xticks([]) # Remove x-axis ticks\n", |
| 721 | + " ax.set_yticks([]) # Remove y-axis ticks\n", |
| 722 | + " ax.axis('off') # Turn off axis lines and labels completely\n", |
| 723 | + "\n", |
| 724 | + "def create_interactive_resolution_param_tabs():\n", |
| 725 | + " import numpy as np\n", |
| 726 | + " import ipywidgets as widgets\n", |
| 727 | + " import matplotlib.pyplot as plt\n", |
| 728 | + " # --- Main Tabbed Interface for Leiden Resolutions ---\n", |
| 729 | + " \n", |
| 730 | + " # 1. Generate the Sierpiński graph (depth 3) ONCE\n", |
| 731 | + " # This graph will be reused for all resolution settings.\n", |
| 732 | + " SIERPINSKI_DEPTH = 4\n", |
| 733 | + " sierpinski_graph, sierpinski_layout = get_sierpinski_graph_and_layout(SIERPINSKI_DEPTH)\n", |
| 734 | + " \n", |
| 735 | + " print(f\"Generated Sierpiński Graph (Depth {SIERPINSKI_DEPTH}):\")\n", |
| 736 | + " print(f\" Vertices: {sierpinski_graph.vcount()}\")\n", |
| 737 | + " print(f\" Edges: {sierpinski_graph.ecount()}\")\n", |
| 738 | + " \n", |
| 739 | + " # Define a set of resolution parameters to display in different tabs\n", |
| 740 | + " # 10**-1.5, 10**-0.75, 10**0, 10**0.75, 10**1.5\n", |
| 741 | + " resolution_values = [10 ** i for i in np.arange(-1.5, 1.51, 0.75)]\n", |
| 742 | + " \n", |
| 743 | + " # Create an Output widget for each resolution value. Each Output widget will hold one plot.\n", |
| 744 | + " output_widgets = [widgets.Output() for _ in resolution_values]\n", |
| 745 | + " \n", |
| 746 | + " # Populate the content for each tab\n", |
| 747 | + " tab_titles = []\n", |
| 748 | + " for i, res in enumerate(resolution_values):\n", |
| 749 | + " with output_widgets[i]: # Direct output to the current Output widget\n", |
| 750 | + " fig, ax = plt.subplots(figsize=(7, 7)) # Create a new figure and axes for this tab's plot\n", |
| 751 | + " \n", |
| 752 | + " # Call the plotting function with the pre-generated graph and layout,\n", |
| 753 | + " # and the current resolution parameter.\n", |
| 754 | + " plot_leiden_communities_on_axes(\n", |
| 755 | + " sierpinski_graph,\n", |
| 756 | + " sierpinski_layout,\n", |
| 757 | + " resolution=res,\n", |
| 758 | + " ax=ax,\n", |
| 759 | + " title_suffix=f\"(Depth {SIERPINSKI_DEPTH})\" # Optional suffix for the title\n", |
| 760 | + " )\n", |
| 761 | + " plt.tight_layout() # Adjust layout to prevent labels/titles from overlapping\n", |
| 762 | + " plt.show() # Display the Matplotlib figure within this Output widget\n", |
| 763 | + " \n", |
| 764 | + " tab_titles.append(f\"Res: {res:.3f}\") # Store the title for this tab\n", |
| 765 | + " \n", |
| 766 | + " # 4. Create the Tab widget\n", |
| 767 | + " leiden_tabs = widgets.Tab()\n", |
| 768 | + " leiden_tabs.children = output_widgets # Assign the list of Output widgets as children\n", |
| 769 | + " \n", |
| 770 | + " # 5. Set the titles for each tab\n", |
| 771 | + " for i, title in enumerate(tab_titles):\n", |
| 772 | + " leiden_tabs.set_title(i, title)\n", |
| 773 | + " \n", |
| 774 | + " # 6. Display the Tab widget in your Jupyter Notebook\n", |
| 775 | + " print(\"\\nSierpiński Graph with Leiden Communities (varying resolution):\")\n", |
| 776 | + " display(leiden_tabs)" |
| 777 | + ] |
506 | 778 | } |
507 | 779 | ], |
508 | 780 | "metadata": { |
|
0 commit comments