Skip to content

Instantly share code, notes, and snippets.

@jGaboardi
Last active June 6, 2025 20:13
Show Gist options
  • Select an option

  • Save jGaboardi/15298656c407d558da677593774ab7c6 to your computer and use it in GitHub Desktop.

Select an option

Save jGaboardi/15298656c407d558da677593774ab7c6 to your computer and use it in GitHub Desktop.
component_logic_gh214
Display the source blob
Display the rendered blob
Raw
{"metadata":{"kernelspec":{"display_name":"Python 3 (ipykernel)","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.13.3"},"gist_info":{"create_date":"2025-06-06T16:31:59Z","gist_id":"15298656c407d558da677593774ab7c6","gist_url":"https://gist.github.com/jGaboardi/15298656c407d558da677593774ab7c6"}},"nbformat_minor":5,"nbformat":4,"cells":[{"id":"566b7674-dacd-48ad-ae13-d388428a6f16","cell_type":"markdown","source":"# Removing interstitial nodes while considering an extremely rare edge case\n\n## Need to:\n\n* Determine which individual edges to merge when removing interstitial nodes\n* Here components means \"component geometries\" to merge into 1, not disconnected network components\n* We do this by first filtering out loops to isolate edges touching only 2 other endpoints\n* However, there seems to be 1 isolated case that throws a wrench into this logic:\n * \"The island bowtie\"\n * A disconnected component composed of:\n * 2 loops\n * 2 single edges that share start & end nodes with each other, and therefore the associated loops like so:\n ```mermaid\n graph LR\n A((Loop1))-- Edge 1 --- B((Loop2))\n B-- Edge 2 --- A\n ```\n * [gh:neatnet#214](https://github.com/uscuni/neatnet/issues/214#issuecomment-2914566461)\n* So prior to determining which edges to treat for merging (removing interstitial nodes), we need to also filter out this edge case\n* Basically:\n * Do current logic, but also\n * Filter out edges that start and end at a loop ***and*** are not loops themselves\n * This seems to not be trivial ","metadata":{}},{"id":"6d9229c4-10ee-4e59-b914-15ef2be6b4fd","cell_type":"code","source":"%load_ext watermark\n%watermark","metadata":{"execution":{"iopub.status.busy":"2025-06-06T20:12:53.503616Z","iopub.execute_input":"2025-06-06T20:12:53.503964Z","iopub.status.idle":"2025-06-06T20:12:53.531684Z","shell.execute_reply.started":"2025-06-06T20:12:53.503932Z","shell.execute_reply":"2025-06-06T20:12:53.531300Z"},"trusted":true},"outputs":[{"name":"stdout","output_type":"stream","text":"Last updated: 2025-06-06T16:12:53.520923-04:00\n\nPython implementation: CPython\nPython version : 3.13.3\nIPython version : 9.2.0\n\nCompiler : Clang 18.1.8 \nOS : Darwin\nRelease : 24.5.0\nMachine : arm64\nProcessor : arm\nCPU cores : 8\nArchitecture: 64bit\n\n"}],"execution_count":1},{"id":"c14a7ae4-01bf-4099-9728-51d3dd06d952","cell_type":"code","source":"import geopandas\nimport networkx\nimport numpy\nimport pandas\nimport shapely\nfrom shapely import LineString, Point\n\n%watermark -w\n%watermark -iv","metadata":{"execution":{"iopub.status.busy":"2025-06-06T20:12:54.150509Z","iopub.execute_input":"2025-06-06T20:12:54.151661Z","iopub.status.idle":"2025-06-06T20:12:54.566145Z","shell.execute_reply.started":"2025-06-06T20:12:54.151599Z","shell.execute_reply":"2025-06-06T20:12:54.565888Z"},"trusted":true},"outputs":[{"name":"stdout","output_type":"stream","text":"Watermark: 2.5.0\n\ngeopandas: 1.0.1\nnumpy : 2.2.6\npandas : 2.2.3\nnetworkx : 3.4.2\nshapely : 2.1.1\n\n"}],"execution_count":2},{"id":"95a6bdd2-83cc-4a68-80c4-d4496cb2e89c","cell_type":"markdown","source":"-----------------------------------------------------\n\n## Synthetic\n\n### Mimic empirical features","metadata":{}},{"id":"2847b2de-6500-40aa-a8df-70342684efab","cell_type":"code","source":"p00, p01, p02, p03 = Point(1, 0), Point(1, 1), Point(1, 3), Point(1, 5)\n\np04, p05 = Point(3, 3), Point(3, 1)\n\np06, p07 = Point(0, 1), Point(5, 1)\n\n\nl00 = LineString((p00, p01))\nl01 = LineString((p01, p02))\nl02 = LineString((p02, p03))\nl03 = LineString((p02, p04))\nl04 = LineString((p04, p05))\nl05 = LineString((p06, p01))\nl06 = LineString((p01, p05))\nl07 = LineString((p05, p07))\n\n\n# left edge loop points\n_p01, _p02, _p03 = Point(6, 1), Point(6, 3), Point(7, 2)\n\n# right edge loop points\n_p04, _p05, _p06 = Point(12, 1), Point(12, 3), Point(11, 2)\n\n# two middle edges points\n_p07, _p08, _p09, _p10 = Point(8, 3), Point(10, 3), Point(10, 1), Point(8, 1)\n\n# left and right loops\nloop1, loop2 = LineString((_p03, _p01, _p02, _p03)), LineString((_p06, _p04, _p05, _p06))\n\n# lower & upper middle edges\nmid1 = LineString((_p03, _p07, _p08, _p06))\nmid2 = LineString((_p06, _p09, _p10, _p03))\n\n# upper\nu01, u02, u03, u04 = Point(6, 4), Point(8, 4), Point(10, 4), Point(12, 4)\nups = [LineString((u01, u02)), LineString((u02, u03)), LineString((u03, u04))]\n\n\nedges = [l00, l01, l02, l03, l04, l05, l06, l07, loop1, loop2, mid1, mid2] + ups\nsynthetic_edges = geopandas.GeoDataFrame(geometry=edges)\nsynthetic_edges.plot(cmap=\"tab20\", lw=5)\nsynthetic_edges","metadata":{"execution":{"iopub.status.busy":"2025-06-06T20:12:54.811143Z","iopub.execute_input":"2025-06-06T20:12:54.811898Z","iopub.status.idle":"2025-06-06T20:12:55.064502Z","shell.execute_reply.started":"2025-06-06T20:12:54.811852Z","shell.execute_reply":"2025-06-06T20:12:55.064296Z"},"trusted":true},"outputs":[{"execution_count":3,"output_type":"execute_result","data":{"text/plain":" geometry\n0 LINESTRING (1 0, 1 1)\n1 LINESTRING (1 1, 1 3)\n2 LINESTRING (1 3, 1 5)\n3 LINESTRING (1 3, 3 3)\n4 LINESTRING (3 3, 3 1)\n5 LINESTRING (0 1, 1 1)\n6 LINESTRING (1 1, 3 1)\n7 LINESTRING (3 1, 5 1)\n8 LINESTRING (7 2, 6 1, 6 3, 7 2)\n9 LINESTRING (11 2, 12 1, 12 3, 11 2)\n10 LINESTRING (7 2, 8 3, 10 3, 11 2)\n11 LINESTRING (11 2, 10 1, 8 1, 7 2)\n12 LINESTRING (6 4, 8 4)\n13 LINESTRING (8 4, 10 4)\n14 LINESTRING (10 4, 12 4)","text/html":"<div>\n<style scoped>\n .dataframe tbody tr th:only-of-type {\n vertical-align: middle;\n }\n\n .dataframe tbody tr th {\n vertical-align: top;\n }\n\n .dataframe thead th {\n text-align: right;\n }\n</style>\n<table border=\"1\" class=\"dataframe\">\n <thead>\n <tr style=\"text-align: right;\">\n <th></th>\n <th>geometry</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <th>0</th>\n <td>LINESTRING (1 0, 1 1)</td>\n </tr>\n <tr>\n <th>1</th>\n <td>LINESTRING (1 1, 1 3)</td>\n </tr>\n <tr>\n <th>2</th>\n <td>LINESTRING (1 3, 1 5)</td>\n </tr>\n <tr>\n <th>3</th>\n <td>LINESTRING (1 3, 3 3)</td>\n </tr>\n <tr>\n <th>4</th>\n <td>LINESTRING (3 3, 3 1)</td>\n </tr>\n <tr>\n <th>5</th>\n <td>LINESTRING (0 1, 1 1)</td>\n </tr>\n <tr>\n <th>6</th>\n <td>LINESTRING (1 1, 3 1)</td>\n </tr>\n <tr>\n <th>7</th>\n <td>LINESTRING (3 1, 5 1)</td>\n </tr>\n <tr>\n <th>8</th>\n <td>LINESTRING (7 2, 6 1, 6 3, 7 2)</td>\n </tr>\n <tr>\n <th>9</th>\n <td>LINESTRING (11 2, 12 1, 12 3, 11 2)</td>\n </tr>\n <tr>\n <th>10</th>\n <td>LINESTRING (7 2, 8 3, 10 3, 11 2)</td>\n </tr>\n <tr>\n <th>11</th>\n <td>LINESTRING (11 2, 10 1, 8 1, 7 2)</td>\n </tr>\n <tr>\n <th>12</th>\n <td>LINESTRING (6 4, 8 4)</td>\n </tr>\n <tr>\n <th>13</th>\n <td>LINESTRING (8 4, 10 4)</td>\n </tr>\n <tr>\n <th>14</th>\n <td>LINESTRING (10 4, 12 4)</td>\n </tr>\n </tbody>\n</table>\n</div>"},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"<Figure size 640x480 with 1 Axes>","image/png":"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MEErV67s9vann35aK1as0MqVK7VlyxYVFBTolltu6fgOqlTS27Y6efKktm3bph//+Mfatm2b1qxZo7179+ob3/hGYoYzksBXv/pVY+7cuZ2WjR492li8eLFNE7lDY2OjIcmora21exTHam5uNkaNGmXU1NQYN954o7FgwQK7R3KcRYsWGVOmTLF7DFe49dZbjQcffLDTslmzZhn33XefTRM5lyTj9ddf77geiUSMgoIC46mnnupYdvr0acPv9xvPPfecDRM6x/nbqjubN282JBmHDh2K+zyu32PR2tqquro6lZeXd1peXl6u9957z6ap3CEQCEiScnNzbZ7EuebNm6dbb71VN998s92jONbatWtVVlamb33rW8rPz9fEiRP1/PPP2z2WI02ZMkV//etftXfvXknS3//+d7377rv6+te/bvNkznfw4EEdPXq003O9z+fTjTfeyHN9FAKBgDweT0L2JCb8S8is1tTUpHA4rCFDhnRaPmTIEB09etSmqZzPMAxVVFRoypQpGjt2rN3jONKqVau0bds2bdmyxe5RHO3AgQOqqqpSRUWFHnnkEW3evFkPP/ywfD6f7r//frvHc5RFixYpEAho9OjR8nq9CofDWrZsme6++267R3M88/m8u+f6Q4cO2TGSa5w+fVqLFy/WPffck5AvcXN9WJjO/wp2wzAs/Vr2ZDN//nx98MEHevfdd+0exZEaGhq0YMEC/eUvf1FmZqbd4zhaJBJRWVmZli9fLkmaOHGidu3apaqqKsLiPKtXr9Yrr7yiV199VSUlJdqxY4cWLlyowsJCzZkzx+7xXIHn+ti0tbXprrvuUiQS0bPPPpuQx3R9WAwaNEher7fL3onGxsYuZYszHnroIa1du1YbN26M61fYu1ldXZ0aGxtVWlrasSwcDmvjxo1auXKlQqGQvF6vjRM6x9ChQzVmzJhOy6688kq99tprNk3kXD/4wQ+0ePFi3XXXXZKkcePG6dChQ6qsrCQsLqCgoEDSmT0XQ4cO7VjOc33P2tradMcdd+jgwYNav359wr5y3vXnWGRkZKi0tFQ1NTWdltfU1Gjy5Mk2TeVMhmFo/vz5WrNmjdavX6/i4mK7R3KsadOmaefOndqxY0fHpaysTPfee6927NhBVJzjuuuu6/K25b1792rEiBE2TeRcJ0+eVFpa56ddr9fL202jUFxcrIKCgk7P9a2traqtreW5vhtmVOzbt09vvfWW8vLyEvbYrt9jIUkVFRWaPXu2ysrKNGnSJFVXV6u+vl5z5861ezRHmTdvnl599VW98cYbys7O7tjL4/f71a9fP5unc5bs7Owu555kZWUpLy+Pc1LO873vfU+TJ0/W8uXLdccdd2jz5s2qrq5WdXW13aM5zowZM7Rs2TINHz5cJSUl2r59u1asWKEHH3zQ7tEc4cSJE9q/f3/H9YMHD2rHjh3Kzc3V8OHDtXDhQi1fvlyjRo3SqFGjtHz5cvXv31/33HOPjVPbo7dtVVhYqNtvv13btm3Tn/70J4XD4Y7n+9zcXGVkZMR3uLi/7yRBfv3rXxsjRowwMjIyjKuvvpq3UHZDUreXF154we7RXIG3m/bsj3/8ozF27FjD5/MZo0ePNqqrq+0eyZGCwaCxYMECY/jw4UZmZqYxcuRIY+nSpUYoFLJ7NEd4++23u32OmjNnjmEYZ95y+uijjxoFBQWGz+czbrjhBmPnzp32Dm2T3rbVwYMHe3y+f/vtt+M+G1+bDgAALOP6cywAAIBzEBYAAMAyhAUAALAMYQEAACxDWAAAAMsQFgAAwDKEBQAAsAxhAQAALENYAAAAyxAWAADAMoQFAACwDGEBAAAs8/8BgBybJk8tME4AAAAASUVORK5CYII="},"metadata":{}}],"execution_count":3},{"id":"a5a9c027-276d-48f7-bbeb-8e7aa6bfc435","cell_type":"markdown","source":"--------------------------------------------------------\n--------------------------------------------------------\n--------------------------------------------------------","metadata":{}},{"id":"070ead2a-d2c7-4cf8-9360-640674e88d07","cell_type":"code","source":"ignore = geopandas.GeoSeries([u03])\nignore","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T20:12:55.559201Z","iopub.execute_input":"2025-06-06T20:12:55.560281Z","iopub.status.idle":"2025-06-06T20:12:55.569129Z","shell.execute_reply.started":"2025-06-06T20:12:55.560227Z","shell.execute_reply":"2025-06-06T20:12:55.568417Z"}},"outputs":[{"execution_count":4,"output_type":"execute_result","data":{"text/plain":"0 POINT (10 4)\ndtype: geometry"},"metadata":{}}],"execution_count":4},{"id":"53954379-06a9-4aa1-a592-2652910bc837","cell_type":"markdown","source":"--------------------------------------------------------\n--------------------------------------------------------\n--------------------------------------------------------\n\n## adapted `nodes.get_components()`\n\n* [here](https://github.com/uscuni/neatnet/blob/84a1dcac84b50d88be1eed294d3e00546a6c5f73/neatnet/nodes.py#L383)","metadata":{}},{"id":"1ba04bb6-1fbe-4877-b20e-f5d5820f0db1","cell_type":"code","source":"# 1. convert edges geoseries to numpy array\nedgelines = numpy.array(synthetic_edges.explode(ignore_index=True).geometry)\nn_edgelines = len(edgelines)\nix_edgelines = numpy.arange(n_edgelines)\n\n# 2. fetch edge starting & ending points\nstart_points = shapely.get_point(edgelines, 0)\nend_points = shapely.get_point(edgelines, -1)\n\n# 3. generate array for all bounds of edgelines\nall_bounds_edgelines = numpy.array(\n list(\n zip(\n shapely.get_coordinates(start_points),\n shapely.get_coordinates(end_points),\n strict=True,\n )\n )\n)\n\n# 4. generate array for all bounding points of edgelines\nall_bounding_points = all_bounds_edgelines.reshape(n_edgelines * 2, 2)\n\n# 5. create loop mask for *bounding points* of edgelines\nall_bounding_points_in_loop_mask = numpy.repeat(shapely.is_closed(edgelines), 2)\n\n# 6. isolate unique non-loop bounding point coordinates\nunique_non_loop_bounding_coords = numpy.unique(\n all_bounding_points[~all_bounding_points_in_loop_mask], axis=0\n)\n\n# 7. query non-loop edgelines for intersecting non-loop bounding points\nunique_nls_elb_ix, _ = shapely.STRtree(shapely.boundary(edgelines)).query(\n shapely.points(unique_non_loop_bounding_coords), predicate=\"intersects\"\n)\n\n# 8. isolate unique non-loop bounding points indices & counts\nunique, counts = numpy.unique(unique_nls_elb_ix, return_counts=True)\n\n# 9. submask A - a mask for intersecting 2 points\n_broadcaster = unique_non_loop_bounding_coords[unique[counts == 2]]\nmask_count_2_points = (all_bounding_points == _broadcaster[:, None]).all(2).any(0)\n\n# 10. submask B - points that are not part of loops\nin_loops = all_bounding_points[all_bounding_points_in_loop_mask]\nmask_not_loop_points = ~numpy.isin(all_bounding_points, in_loops)[:, 0]\n\n# 11. initial mask - preliminary points filter to consider for merging\nmask_consider_for_merge = mask_count_2_points & mask_not_loop_points\n\n# 12. preliminary translation of point to edges filter to consider for merging\n_mask_points_to_edgelines = mask_consider_for_merge.reshape(n_edgelines, 2)\n\n# 13. edges filter to consider for merging\nmask_edgelines_to_merge = ~numpy.all(~_mask_points_to_edgelines, axis=1)\n\n# 14. nearly final edges to merge\nprelim_edgelines_to_merge = edgelines[mask_edgelines_to_merge]\n\n# 15. candidate bounds of edgelines to merge\nedgelines_points_candidates = all_bounds_edgelines[\n numpy.isin(edgelines, prelim_edgelines_to_merge)\n].reshape(prelim_edgelines_to_merge.shape[0] * 2, 2)\n\n# 16. isolate \"final\" set of nodes to merge – coords and labels\npoints = shapely.points(numpy.unique(edgelines_points_candidates, axis=0))\n\n# 17. remove known points to ignore from consideration\nif ignore is not None:\n mask_ignore = numpy.isin(points, ignore)\n points = points[~mask_ignore]\n\n# 18. query final set of nodes & edges to create relationship\nto_merge_node_ix, to_merge_edge_ix = shapely.STRtree(edgelines).query(\n points, predicate=\"intersects\"\n)\n\n# 19. final masking of edges sharing degree-2 nodes\nunique, counts = numpy.unique(to_merge_node_ix, return_counts=True)\nfinal_mask = numpy.isin(to_merge_node_ix, unique[counts == 2])\nto_merge_node_ix = to_merge_node_ix[final_mask]\nto_merge_edge_ix = to_merge_edge_ix[final_mask]\n\n# 20. generate graph topology & label components\ng = networkx.Graph(\n list(zip((to_merge_node_ix * -1) - 1, to_merge_edge_ix, strict=True))\n)\ncomponents = {\n i: {v for v in k if v > -1} for i, k in enumerate(networkx.connected_components(g))\n}\ncomponent_labels = {value: key for key in components for value in components[key]}\nlabels = pandas.Series(component_labels, index=ix_edgelines)\nmax_label = n_edgelines - 1 if pandas.isna(labels.max()) else int(labels.max())\nfilling = pandas.Series(range(max_label + 1, max_label + n_edgelines + 1))\nlabels.fillna(filling)","metadata":{"execution":{"iopub.status.busy":"2025-06-06T20:12:56.483894Z","iopub.execute_input":"2025-06-06T20:12:56.484740Z","iopub.status.idle":"2025-06-06T20:12:56.510158Z","shell.execute_reply.started":"2025-06-06T20:12:56.484662Z","shell.execute_reply":"2025-06-06T20:12:56.509862Z"},"trusted":true},"outputs":[{"execution_count":5,"output_type":"execute_result","data":{"text/plain":"0 2.0\n1 3.0\n2 4.0\n3 0.0\n4 0.0\n5 7.0\n6 8.0\n7 9.0\n8 10.0\n9 11.0\n10 12.0\n11 13.0\n12 1.0\n13 1.0\n14 16.0\ndtype: float64"},"metadata":{}}],"execution_count":5},{"id":"17cac245-cbab-41a0-b330-19a2f4666725","cell_type":"markdown","source":"---------------------------------------------------------------","metadata":{}},{"id":"23ce4acf-1c4e-473e-b4c7-30343962a086","cell_type":"code","source":"synthetic_edges[\"comp\"] = labels.fillna(filling)\nsynthetic_edges","metadata":{"execution":{"iopub.status.busy":"2025-06-06T20:12:57.546357Z","iopub.execute_input":"2025-06-06T20:12:57.546931Z","iopub.status.idle":"2025-06-06T20:12:57.558926Z","shell.execute_reply.started":"2025-06-06T20:12:57.546883Z","shell.execute_reply":"2025-06-06T20:12:57.558563Z"},"trusted":true},"outputs":[{"execution_count":6,"output_type":"execute_result","data":{"text/plain":" geometry comp\n0 LINESTRING (1 0, 1 1) 2.0\n1 LINESTRING (1 1, 1 3) 3.0\n2 LINESTRING (1 3, 1 5) 4.0\n3 LINESTRING (1 3, 3 3) 0.0\n4 LINESTRING (3 3, 3 1) 0.0\n5 LINESTRING (0 1, 1 1) 7.0\n6 LINESTRING (1 1, 3 1) 8.0\n7 LINESTRING (3 1, 5 1) 9.0\n8 LINESTRING (7 2, 6 1, 6 3, 7 2) 10.0\n9 LINESTRING (11 2, 12 1, 12 3, 11 2) 11.0\n10 LINESTRING (7 2, 8 3, 10 3, 11 2) 12.0\n11 LINESTRING (11 2, 10 1, 8 1, 7 2) 13.0\n12 LINESTRING (6 4, 8 4) 1.0\n13 LINESTRING (8 4, 10 4) 1.0\n14 LINESTRING (10 4, 12 4) 16.0","text/html":"<div>\n<style scoped>\n .dataframe tbody tr th:only-of-type {\n vertical-align: middle;\n }\n\n .dataframe tbody tr th {\n vertical-align: top;\n }\n\n .dataframe thead th {\n text-align: right;\n }\n</style>\n<table border=\"1\" class=\"dataframe\">\n <thead>\n <tr style=\"text-align: right;\">\n <th></th>\n <th>geometry</th>\n <th>comp</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <th>0</th>\n <td>LINESTRING (1 0, 1 1)</td>\n <td>2.0</td>\n </tr>\n <tr>\n <th>1</th>\n <td>LINESTRING (1 1, 1 3)</td>\n <td>3.0</td>\n </tr>\n <tr>\n <th>2</th>\n <td>LINESTRING (1 3, 1 5)</td>\n <td>4.0</td>\n </tr>\n <tr>\n <th>3</th>\n <td>LINESTRING (1 3, 3 3)</td>\n <td>0.0</td>\n </tr>\n <tr>\n <th>4</th>\n <td>LINESTRING (3 3, 3 1)</td>\n <td>0.0</td>\n </tr>\n <tr>\n <th>5</th>\n <td>LINESTRING (0 1, 1 1)</td>\n <td>7.0</td>\n </tr>\n <tr>\n <th>6</th>\n <td>LINESTRING (1 1, 3 1)</td>\n <td>8.0</td>\n </tr>\n <tr>\n <th>7</th>\n <td>LINESTRING (3 1, 5 1)</td>\n <td>9.0</td>\n </tr>\n <tr>\n <th>8</th>\n <td>LINESTRING (7 2, 6 1, 6 3, 7 2)</td>\n <td>10.0</td>\n </tr>\n <tr>\n <th>9</th>\n <td>LINESTRING (11 2, 12 1, 12 3, 11 2)</td>\n <td>11.0</td>\n </tr>\n <tr>\n <th>10</th>\n <td>LINESTRING (7 2, 8 3, 10 3, 11 2)</td>\n <td>12.0</td>\n </tr>\n <tr>\n <th>11</th>\n <td>LINESTRING (11 2, 10 1, 8 1, 7 2)</td>\n <td>13.0</td>\n </tr>\n <tr>\n <th>12</th>\n <td>LINESTRING (6 4, 8 4)</td>\n <td>1.0</td>\n </tr>\n <tr>\n <th>13</th>\n <td>LINESTRING (8 4, 10 4)</td>\n <td>1.0</td>\n </tr>\n <tr>\n <th>14</th>\n <td>LINESTRING (10 4, 12 4)</td>\n <td>16.0</td>\n </tr>\n </tbody>\n</table>\n</div>"},"metadata":{}}],"execution_count":6},{"id":"1d290d63-195e-470d-9b9f-d14af42cd126","cell_type":"markdown","source":"## Success while ignoring","metadata":{}},{"id":"026cf998-13c5-4d55-b955-b6e03650eeee","cell_type":"code","source":"ax = synthetic_edges.plot(cmap=\"tab20\", column=\"comp\", lw=5, zorder=0)\nignore.plot(ax=ax, markersize=100, fc=\"w\", ec=\"k\", zorder=1)","metadata":{"execution":{"iopub.status.busy":"2025-06-06T20:12:58.600217Z","iopub.execute_input":"2025-06-06T20:12:58.600803Z","iopub.status.idle":"2025-06-06T20:12:58.675838Z","shell.execute_reply.started":"2025-06-06T20:12:58.600758Z","shell.execute_reply":"2025-06-06T20:12:58.675581Z"},"trusted":true},"outputs":[{"execution_count":7,"output_type":"execute_result","data":{"text/plain":"<Axes: >"},"metadata":{}},{"output_type":"display_data","data":{"text/plain":"<Figure size 640x480 with 1 Axes>","image/png":"iVBORw0KGgoAAAANSUhEUgAAAhYAAAD6CAYAAAD9Xg4DAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAH8xJREFUeJzt3X90FIXd7/HPZEM2JCSBRCDkSaChUIMgYIG2IKi9aLgUbXlUKP6k/jrHcwGhubaAtvXHo8TqI4+0lHhAr9XHa4EWpNZWaVQEPV4EA1QetCKPEYL8EiTZEGST7M79AycSSMJumN2Z2X2/zplz2NnZ7NcBN+/MzGYN0zRNAQAA2CDF6QEAAEDiICwAAIBtCAsAAGAbwgIAANiGsAAAALYhLAAAgG0ICwAAYBvCAgAA2CY13k8YDoe1b98+ZWVlyTCMeD89AADoBNM0VV9fr4KCAqWktH9cIu5hsW/fPhUVFcX7aQEAgA1qampUWFjY7v1xD4usrCxJJwfLzs6O99MDAIBOCAQCKioqavk+3p64h4V1+iM7O5uwAADAY852GQMXbwIAANsQFgAAwDZRhcX9998vwzBaLfn5+bGaDQAAeEzU11gMHjxYr732Wsttn89n60AAAMC7og6L1NRUjlIAAIA2RX2Nxccff6yCggIVFxdr2rRp+uSTTzrcPhgMKhAItFoAAEBiiuqIxXe/+10999xz+ta3vqWDBw/qoYce0pgxY7Rjxw7l5eW1+Zjy8nI98MADtgzrJVW178g0zTPWG4ahEd3HODARAACxZ5htffeLUENDg775zW/q5z//ucrKytrcJhgMKhgMtty2fsFGXV1dQv8ei2WfLlRYoTPWp8inO77R9r4CAMCtAoGAcnJyzvr9+5x+QVZmZqYuvPBCffzxx+1u4/f75ff7z+VpAACAR5zT77EIBoP68MMP1adPH7vmAQAAHhZVWNx9991av369qqur9e677+raa69VIBDQ9OnTYzUfAADwkKhOhezdu1fXXXedDh8+rJ49e+p73/ueNm7cqH79+sVqPgAA4CFRhcXy5ctjNQcAAEgAfFYIAACwDWEBAABsQ1gAAADbEBYAAMA2hAUAALANYQEAAGxDWAAAANsQFgAAwDaEBQAAsA1hAQAAbENYAAAA2xAWAADANoQFAACwDWEBAABsQ1gAAADbEBYAAMA2hAUAALANYQEAAGxDWAAAANsQFgAAwDaEBQAAsA1hAQAAbENYAAAA26Q6PQAAAO0xTVNHjhzRsWPH1K1bN+Xl5ckwDKfHQgc4YgEAcJ3a2lotWrRIgwYNUs+ePVVcXKyePXtq0KBBWrRokWpra50eEe0gLAAArrJ27Vr169dPd999t4YPH66VK1eqsrJSK1eu1PDhw3X33XerX79+Wrt2rdOjog3nFBbl5eUyDENz5syxaRwAQDJbu3atrrzySo0bN041NTVavny5pkyZossvv1xTpkzR8uXLVVNTo3HjxunKK68kLlyo02GxefNmLV26VEOHDrVzHgBAkqqtrdXUqVM1YcIErVmzRvn5+W1ul5+frzVr1mjChAmaOnUqp0VcplMXbx47dkw33HCDli1bpoceesjumQB42D/3HtNHnzU4PYYn5GZ10Rf1TU6P4Rp/WbFMx48f11NPPaXU1I6/PaWmpmrZsmXq27evZj7+W10x/bY4Tel+w3pkaXhelmPP36kjFjNmzNCkSZN0+eWX2z0PAI8zTSnMEtHCvvp6CYVNrV39rK6+5pp2j1Scrk+fPvrXq6/WuheeVShsOv7f4JbFlBnj/8s7FvURi+XLl2vLli3avHlzRNsHg0EFg8GW24FAINqnBAAkuPq6L7R39y5d+9iCqB537TXX6I8rV6qh9qi69ciN0XSIRlRHLGpqajR79mw9//zzSk9Pj+gx5eXlysnJaVmKioo6NSgAIHGd+PLk6bMePXpE9Thr+xMNnH5zi6jCoqqqSocOHdKIESOUmpqq1NRUrV+/Xr/5zW+UmpqqUCh0xmPmz5+vurq6lqWmpsa24QEAiSG9a6Yk6ejRo1E9zto+PTPT9pnQOVGdChk/fry2b9/eat0tt9yikpISzZ07Vz6f74zH+P1++f3+c5sSAJDQsnJyVdhvgP70p1WaMmVKxI/706pVKuj/TWV2j+5IB2InqrDIysrSkCFDWq3LzMxUXl7eGesBJCfDkFL4jcsRYV+dwjA04erpeva3D+jAgQMRXcC5f/9+vbh6tX4871fysSNbGHJ2X/BZIQBsVVLYTSWF3ZweAx506fkz9cenH9Ptt9+uNWvWdPiW0+bmZt1xxx3KyMjQ4v89S927d4/foOjQOf9K7zfffFNPPPGEDaMAAJJZ9+7dtXLlSq1du1aTJ0/W/v3729xu//79mjx5stauXas//vGPRIXLcMQCAOAaEyZM0Msvv6ypU6eqb9++uvrqq3XNNdeoR48eOnr0qFatWqXVq1crIyNDf/3rX1VaWur0yDgNYQEAcJUJEyZo9+7deu6557RkyRKtXLmy5b7zzz9fjz/+uKZPn66cnBwHp0R7CAsAgOt0795dd911l2bNmqUvvvhC9fX1ysrKUm5urgyDCzXdjLAAALiWYRjKy8tTXl6e06MgQud88SYAAICFsAAAALYhLAAAgG0ICwAAYBvCAgAA2IawAAAAtiEsAACAbQgLAABgG8ICAADYhrAAAAC2ISwAAIBtCAsAAGAbwgIAANiGsAAAALYhLAAAgG0ICwAAYBvCAgAA2IawAAAAtiEsAACAbQgLAABgG8ICAADYhrAAAAC2ISwAAIBtUqPZuKKiQhUVFfr0008lSYMHD9avfvUrTZw4MRazAfCQUEOTmg8dly87Tal5XZ0ex9Wam5vV0NCgUCjk9CiekZaWpoyMDKWk8PNwe8xwWOaxoBQKy8jJkJFiODJHVGFRWFioRx55RAMGDJAkPfvss/rRj36krVu3avDgwTEZMBG8XXWe3qk6r+X2Y8YrDk7jbjP/xwDdNX6g02MgCqZpKvD33ap/a6/UbEqSMi7qpe4//KZSukb1EpPwTNPUwYMHtXv3bpmm6fQ4npOenq6BAwcqMzPT6VFcJ1x7XE3b954MC0lK76K0oYVKyesW91mi+r/+qquuanX74YcfVkVFhTZu3EhYdMAMGwqFv67skMIOTuNuoTAvtl5zvOqQ6tfVtF639ZCaPj+unrddSFx8xTRN7d27V5999pnTo3jWiRMn9MEHH6ikpERZWVlOj+MaoSPH1FT1qRQ65fXzRJMat+6Rf9xAGf4ucZ2n08eUQqGQli9froaGBo0ePbrd7YLBoAKBQKsFQOI4/v7nba5v2ntMnz+9XeEvm+M8kfsQFfYJhUL65z//qfr6eqdHcYU2o8LSFFL48LG4zxT1jxLbt2/X6NGjdeLECXXr1k0vvviiLrjggna3Ly8v1wMPPHBOQ3rRt8O9ZOrkX/Quk/PNkdr4yRH9R+VOSZIvxeC0iAc0Hzre7n1WXCTzkQuiwn5WXCT7kYsOo+Ir4WNB+eI3kiTJMKM80dfY2Kg9e/aotrZWq1at0lNPPaX169e3GxfBYFDBYLDldiAQUFFRkerq6pSdnX1u07vZ67+QzJMXZv3Hf/fVouq+Dg/kPWm+FO18mAuD3W7/I5sUqg12uE2Xwm5JGRdERWz5fL6kjYtIokKSfP17qsv5+bY8ZyAQUE5Ozlm/f0d9KiQtLU0DBgzQyJEjVV5ermHDhmnRokXtbu/3+5Wdnd1qAZBckvG0CFERe8l6WiTSqHDKOf/4YJpmqyMSOJPPMJVmnHLBZkpy/dR2No0hLmZNBsl0WiSaqMjLy5NhOPO2QC8IBoMdhkOynRZxe1RIUYbFPffco4kTJ6qoqEj19fVavny53nzzTb366quxmi8h3NW/Rnf1/+qqecMnjX/I2YFc5lv3vkJcJIlkiItoomLgwIHKy8uLw1TeZZqmdu3apSNHjrS7TbLEhReiQoryVMjBgwd100036fzzz9f48eP17rvv6tVXX9UVV1wRq/kAJJhEPi1CVNjPMAwNGDDgrPsq0U+LeCUqpCiPWDz99NOxmgNAEknEIxdERexYcSEpKY9ceCkqJD4rBIBDEunIBVERe8l65MJrUSERFgBiKCU7rcP7EyEuiIr4Sba4iDgq/O466kdYAIiZ834yWL689A638XJcEBXxlyxxEfHvqSjsodT+PeMzVIQICwAxk9rdr553DE3IuCAqnJPocRFVVAz5F8ll71YmLADEVCLGBVHhvESNi2ijwo2/A4WwABBziRQXRIV7JFpcJEJUSIQFgDhJhLggKtwnUeIiUaJCIiwAxJGX44KocC+vx0UiRYVEWACIMy/GBVHhfl6Ni0SLComwAOAAL8UFUeEdXouLRIwKibAA4BAvxAVR4T1eiYtEjQqJsADgIDfHBVHhXW6Pi0SOComwAOAwN8YFUeF9bo2LRI8KibAA4AJuiguiInG4LS6SISokwgKAS7ghLoiKxOOWuEiWqJAICwAu4mRcEBWJy+m4SKaokAgLAC7jRFwQFYnPqbhItqiQCAsALhTPuCAqkke84yIZo0IiLAC4VDzigqhIPvGKi2SNComwAOBisYwLoiJ5xToukjkqJMICgMvFIi6ICsQqLpI9KiTCAoAH2BkXRAUsdscFUXESYQHAE+yIC6ICp7MrLoiKrxEWADzjXOKCqEB7zjUuiIrWCAsAntKZuCAqcDadjQui4kyEBQDPiSYuDj39vmo+3UNU4KyijYu6zw4RFW0gLAB4UiRxYUo60u1L7Tu4/6xfj6iAFF1cfLTnEzWo43chJVtUSFGGRXl5uUaNGqWsrCz16tVLkydP1kcffRSr2QCgQx3FhSkpUJKi+hLfWb8OUYFTRRoXYUP676wTavCF2rw/GaNCijIs1q9frxkzZmjjxo2qrKxUc3OzSktL1dDQEKv5AKBD7cVFOE1q+MbXL3GmGVZj08syzdavV0QF2tJeXIRCH6g59H7L7bAhHfafedQiWaNCklKj2fjVV19tdfuZZ55Rr169VFVVpUsuucTWwQAgUlZcfL7sfYWOnJAk+Rqlnm836/OxqQr5TTU2/V7Nza8rFHpX6f55MoxMogIdsuJCko4cOaJQ6AOdCP67pLDkL1Oqb6iyG30qOp7W6nHJHBXSOV5jUVdXJ0nKzc1td5tgMKhAINBqAQC7WXFxqi7HpPPeblRT47Nqbn5dkhQOf6ITwUdU3D+fqMBZWXGRmbn3q6gISmpSMLhQ/hP/pW80+JWirwPC6OZP6qiQziEsTNNUWVmZxo4dqyFDhrS7XXl5uXJyclqWoqKizj4lALTLNE01bD7Qep3C+qLoP9UUeq3V+nD4E+3ePUdNTfygg7OrrX1Xh4/cr5NRYWnS0dC/q87Y1mpb88smmXVfxnE69+l0WMycOVPvv/++/vCHP3S43fz581VXV9ey1NTUdPYpAaBNpmkq8Noe1b++5+t1CuvgoP9UXdG6Nh9TX79dW7fdTFygQ0ePbtS2f9yucPjMWDCNJn3YZYGOGlu/XhkKq3FztcK1x+M4pbt0KixmzZqll156SevWrVNhYWGH2/r9fmVnZ7daAMAubUWFJIW6NOh43o4OH0tcoCMdRYXFNJr0hW9T65XNyR0XUYWFaZqaOXOmVq9erTfeeEPFxcWxmgsAzqq9qJCk1KYsFW2eqy7He3X4NYgLtCWSqJCkXqHx6t98x5l3JHFcRBUWM2bM0PPPP68XXnhBWVlZOnDggA4cOKAvv0zu80kA4q+jqLB0CeapqGqu/CkdH1klLnCqSKOid9pEDWieIaO9b6VJGhdRhUVFRYXq6up02WWXqU+fPi3LihUrYjUfAJwhkqiQJKVIvf/1Yo383h/UtWvfDjclLiBFHhV9+kzRBaMXydenR8dfMAnjIupTIW0tP/nJT2I0HgC0Fk1U5E4rUcbQnkpPL9C3L/q/xAU6FE1UDCpZoBSfT12GFimlT07HXzjJ4oLPCgHgGZ2JCgtxgY5EGxWGcfLbp5FiEBenISwAeMK5RIWFuEBbOhsVFuKiNcICgOvZERUW4gKnOteosBAXXyMsALianVFhIS4g2RcVFuLiJMICgGvFIiosxEVyszsqLMQFYQHApWIZFRbiIjnFKiosyR4XhAUA14lHVFiIi+QS66iwJHNcEBYAXCWeUWEhLpJDvKLCkqxxQVgAcA0nosJCXCS2eEeFJRnjgrAA4ApORoWFuEhMTkWFJdnigrAA4Dg3RIWFuEgsTkeFJZnigrAA4Cg3RYWFuEgMbokKS7LEBWEBwDFujAoLceFtbosKSzLEBWEBwBFujgoLceFNbo0KS6LHBWEBIO68EBUW4sJb3B4VlkSOC8ICQFx5KSosxIU3eCUqLIkaF4QFgLjxYlRYiAt381pUWBIxLtyxZwEkPC9HhYW4cCevRoUl0eLCXXsXQEJKhKiwEBfu4vWosCRSXLhzDwNIGIkUFRbiwh0SJSosiRIX7t7LADzNNJVwUWEhLpyVaFFhSYS48MaeBuBJgdd2J2RUWIgLZyRqVFiij4uO90O8eWtvA/CUhv+3v+MNPBwVFuIivhI9KizRxEV4X21cZoqUN/c4AO9LgKiwEBfxkSxRYYk4LlzG23sdgDclUFRYiIvYSraosHgxLhJjzwPwjgSMCgtxERvJGhUWr8VFarQP2LBhgx577DFVVVVp//79evHFFzV58uQYjAYg4SRwVFisuNiy9QZ9+WX7F65acTGopFxGStQvxUnjy+PV+q8dZUkbFRYrLpokhffXOT1Oh6L+19zQ0KBhw4bplltu0TXXXBOLmTpt81+r9d4rnzo9xknhy1r+2DPzFe3du/Pr+/7P5LiP42a3N4db/mxcMFajCjK/uiG980yFQ1O5X9HwkSq6aJTTY0QuCaLCEk1cbNp8ZRwnS0yJHhUWr8RF1GExceJETZw4MRaznDMzbCrcbDo9xle+/gduhqVQ+JS5ws0OzONevlNvGFKXU14bzHD49M3xFdN0y7/1CCRRVFgijQucm2SJCosX4iLmfxPBYFCBQKDVAiCJJGFUWCK95gKdk2xRYXH7NRcx/9soLy9XTk5Oy1JUVBTrp3Qfw3B6As8o7N7V6REQpZR0Xwd3Jm9UWIiL2EjWqLBEGhdGavz3T8yfcf78+aqrq2tZampqYv2U7lO71+kJPKOwB2HhNWnfaOeFjahoQVzYK9mjwhJJXKT0yIzjRF89Z6yfwO/3Kzs7u9UCIHFkj+8rX3d/65WpKUTFaay4yMgodnoUTysomEZUnKIlLgq6n3Gf7196yOiREfeZEuo9TkaKoZRUl5x2aG5s+aNhSD7jlIsQfWkODOQNRkqKlMILRiQMl5xi82Wlqdf/GqZjG/eraV+DfDlpyrqkUKl5HH06XXp6gb4z6iXt3vO06mrfU3PomNMjeUZ6eoF695qkXr3+p9OjuM7JuChUuHe2QgfqpFBYvvwcpfTp7sjrRNRhcezYMe3atavldnV1tbZt26bc3Fz17evsYb5Rk4o1apJLfhr4t55SqPHM9b406Zefx38eIIZ82X7llH7D6TE8wefLUP/iWU6PgQRjGIZ8+Tny5Tt/QWfUYfHee+/p+9//fsvtsrIySdL06dP1+9//3rbBAACA90QdFpdddpm33j8PAADihpPZAADANoQFAACwDWEBAABsQ1gAAADbEBYAAMA2hAUAALANYQEAAGxDWAAAANsQFgAAwDaEBQAAsA1hAQAAbENYAAAA2xAWAADANoQFAACwDWEBAABsQ1gAAADbEBYAAMA2hAUAALANYQEAAGxDWAAAANsQFgAAwDaEBQAAsA1hAQAAbENYAAAA2xAWAADANoQFAACwDWEBAABs06mwWLJkiYqLi5Wenq4RI0borbfesnsuAADgQVGHxYoVKzRnzhzde++92rp1q8aNG6eJEydqz549sZgPAAB4SNRhsXDhQt122226/fbbNWjQID3xxBMqKipSRUVFLOYDAAAeElVYNDY2qqqqSqWlpa3Wl5aW6p133mnzMcFgUIFAoNUCAAASU2o0Gx8+fFihUEi9e/dutb537946cOBAm48pLy/XAw880PkJveqSn0nh0JnrU3zxnwUAgDiJKiwshmG0um2a5hnrLPPnz1dZWVnL7UAgoKKios48rbdc+nOnJwAAIO6iCovzzjtPPp/vjKMThw4dOuMohsXv98vv93d+QgAA4BlRXWORlpamESNGqLKystX6yspKjRkzxtbBAACA90R9KqSsrEw33XSTRo4cqdGjR2vp0qXas2eP7rzzzljMBwAAPCTqsPjxj3+sI0eO6MEHH9T+/fs1ZMgQ/e1vf1O/fv1iMR8AAPAQwzRNM55PGAgElJOTo7q6OmVnZ8fzqQEAQCdF+v2bzwoBAAC26dTbTc+FdYCEX5QFAIB3WN+3z3aiI+5hUV9fL0nJ8bssAABIMPX19crJyWn3/rhfYxEOh7Vv3z5lZWW1+0u1OsP6xVs1NTVcu3EW7KvIsa+iw/6KHPsqcuyryMVyX5mmqfr6ehUUFCglpf0rKeJ+xCIlJUWFhYUx+/rZ2dn8w4sQ+ypy7KvosL8ix76KHPsqcrHaVx0dqbBw8SYAALANYQEAAGyTMGHh9/t133338bkkEWBfRY59FR32V+TYV5FjX0XODfsq7hdvAgCAxJUwRywAAIDzCAsAAGAbwgIAANiGsAAAALZJmLBYsmSJiouLlZ6erhEjRuitt95yeiTXKS8v16hRo5SVlaVevXpp8uTJ+uijj5weyxPKy8tlGIbmzJnj9Ciu9Nlnn+nGG29UXl6eMjIyNHz4cFVVVTk9lus0NzfrF7/4hYqLi9W1a1f1799fDz74oMLhsNOjucKGDRt01VVXqaCgQIZhaM2aNa3uN01T999/vwoKCtS1a1dddtll2rFjhzPDOqyjfdXU1KS5c+fqwgsvVGZmpgoKCnTzzTdr3759cZktIcJixYoVmjNnju69915t3bpV48aN08SJE7Vnzx6nR3OV9evXa8aMGdq4caMqKyvV3Nys0tJSNTQ0OD2aq23evFlLly7V0KFDnR7FlY4ePaqLL75YXbp00SuvvKIPPvhAjz/+uLp37+70aK7z61//Wk8++aQWL16sDz/8UI8++qgee+wx/fa3v3V6NFdoaGjQsGHDtHjx4jbvf/TRR7Vw4UItXrxYmzdvVn5+vq644oqWz6BKJh3tq+PHj2vLli365S9/qS1btmj16tXauXOnfvjDH8ZnODMBfOc73zHvvPPOVutKSkrMefPmOTSRNxw6dMiUZK5fv97pUVyrvr7eHDhwoFlZWWleeuml5uzZs50eyXXmzp1rjh071ukxPGHSpEnmrbfe2mrd1Vdfbd54440OTeRekswXX3yx5XY4HDbz8/PNRx55pGXdiRMnzJycHPPJJ590YEL3OH1ftWXTpk2mJHP37t0xn8fzRywaGxtVVVWl0tLSVutLS0v1zjvvODSVN9TV1UmScnNzHZ7EvWbMmKFJkybp8ssvd3oU13rppZc0cuRITZkyRb169dJFF12kZcuWOT2WK40dO1avv/66du7cKUn6xz/+obfffls/+MEPHJ7M/aqrq3XgwIFWr/V+v1+XXnopr/URqKurk2EYcTmSGPcPIbPb4cOHFQqF1Lt371bre/furQMHDjg0lfuZpqmysjKNHTtWQ4YMcXocV1q+fLm2bNmizZs3Oz2Kq33yySeqqKhQWVmZ7rnnHm3atEl33XWX/H6/br75ZqfHc5W5c+eqrq5OJSUl8vl8CoVCevjhh3Xdddc5PZrrWa/nbb3W796924mRPOPEiROaN2+err/++rh8iJvnw8Jy+kewm6Zp68eyJ5qZM2fq/fff19tvv+30KK5UU1Oj2bNn6+9//7vS09OdHsfVwuGwRo4cqQULFkiSLrroIu3YsUMVFRWExWlWrFih559/Xi+88IIGDx6sbdu2ac6cOSooKND06dOdHs8TeK2PTlNTk6ZNm6ZwOKwlS5bE5Tk9HxbnnXeefD7fGUcnDh06dEbZ4qRZs2bppZde0oYNG2L6EfZeVlVVpUOHDmnEiBEt60KhkDZs2KDFixcrGAzK5/M5OKF79OnTRxdccEGrdYMGDdKqVascmsi9fvazn2nevHmaNm2aJOnCCy/U7t27VV5eTlicRX5+vqSTRy769OnTsp7X+vY1NTVp6tSpqq6u1htvvBG3j5z3/DUWaWlpGjFihCorK1utr6ys1JgxYxyayp1M09TMmTO1evVqvfHGGyouLnZ6JNcaP368tm/frm3btrUsI0eO1A033KBt27YRFae4+OKLz3jb8s6dO9WvXz+HJnKv48ePKyWl9cuuz+fj7aYRKC4uVn5+fqvX+sbGRq1fv57X+jZYUfHxxx/rtddeU15eXtye2/NHLCSprKxMN910k0aOHKnRo0dr6dKl2rNnj+68806nR3OVGTNm6IUXXtCf//xnZWVltRzlycnJUdeuXR2ezl2ysrLOuPYkMzNTeXl5XJNymp/+9KcaM2aMFixYoKlTp2rTpk1aunSpli5d6vRornPVVVfp4YcfVt++fTV48GBt3bpVCxcu1K233ur0aK5w7Ngx7dq1q+V2dXW1tm3bptzcXPXt21dz5szRggULNHDgQA0cOFALFixQRkaGrr/+egendkZH+6qgoEDXXnuttmzZopdfflmhUKjl9T43N1dpaWmxHS7m7zuJk9/97ndmv379zLS0NPPb3/42b6Fsg6Q2l2eeecbp0TyBt5u27y9/+Ys5ZMgQ0+/3myUlJebSpUudHsmVAoGAOXv2bLNv375menq62b9/f/Pee+81g8Gg06O5wrp169p8jZo+fbppmiffcnrfffeZ+fn5pt/vNy+55BJz+/btzg7tkI72VXV1dbuv9+vWrYv5bHxsOgAAsI3nr7EAAADuQVgAAADbEBYAAMA2hAUAALANYQEAAGxDWAAAANsQFgAAwDaEBQAAsA1hAQAAbENYAAAA2xAWAADANoQFAACwzf8HxQXGZjpcObsAAAAASUVORK5CYII="},"metadata":{}}],"execution_count":7}]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment