sameer-graph-lib 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,174 @@
1
+ """Topology analysis for route affinity graphs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterable, List, Sequence
6
+
7
+ import networkx as nx
8
+
9
+
10
+ class TopologyAnalyzer:
11
+ """Split a graph into its main trunk and residual minor branches."""
12
+
13
+ def __init__(self, graph_or_affinity) -> None:
14
+ self.graph = graph_or_affinity if isinstance(graph_or_affinity, nx.Graph) else graph_or_affinity.graph
15
+
16
+ def decompose_branches(self, seed_hexes: Sequence[str] | None = None) -> dict:
17
+ if self.graph.number_of_nodes() == 0:
18
+ return {
19
+ "main_branch": {
20
+ "node_count": 0,
21
+ "total_count": 0,
22
+ "total_value": 0,
23
+ "hexes": [],
24
+ },
25
+ "minor_branches": [],
26
+ }
27
+
28
+ forest = self._maximum_spanning_forest()
29
+ main_branch = self._choose_main_branch(forest, seed_hexes=seed_hexes)
30
+ main_set = set(main_branch)
31
+
32
+ residual = self.graph.copy()
33
+ residual.remove_nodes_from(main_set)
34
+
35
+ branches = []
36
+ for component in nx.connected_components(residual):
37
+ hexes = self._stable_component_order(component)
38
+ branches.append(
39
+ {
40
+ "node_count": len(hexes),
41
+ "total_count": self._sum_node_attr(hexes, "count"),
42
+ "total_value": self._sum_node_attr(hexes, "value"),
43
+ "hexes": hexes,
44
+ "connects_to_main_at": self._main_attachment(component, main_set),
45
+ }
46
+ )
47
+
48
+ branches.sort(key=lambda item: (item["total_count"], item["node_count"]), reverse=True)
49
+ for idx, branch in enumerate(branches, start=1):
50
+ branch["branch_id"] = idx
51
+
52
+ return {
53
+ "main_branch": {
54
+ "node_count": len(main_branch),
55
+ "total_count": self._sum_node_attr(main_branch, "count"),
56
+ "total_value": self._sum_node_attr(main_branch, "value"),
57
+ "hexes": main_branch,
58
+ },
59
+ "minor_branches": branches,
60
+ }
61
+
62
+ def _maximum_spanning_forest(self) -> nx.Graph:
63
+ weighted = nx.Graph()
64
+ weighted.add_nodes_from(self.graph.nodes(data=True))
65
+
66
+ for u, v, data in self.graph.edges(data=True):
67
+ copied = dict(data)
68
+ copied["_trunk_strength"] = self._edge_strength(u, v, data)
69
+ weighted.add_edge(u, v, **copied)
70
+
71
+ if weighted.number_of_edges() == 0:
72
+ return weighted
73
+
74
+ return nx.maximum_spanning_tree(weighted, weight="_trunk_strength")
75
+
76
+ def _choose_main_branch(
77
+ self,
78
+ forest: nx.Graph,
79
+ seed_hexes: Sequence[str] | None = None,
80
+ ) -> List[str]:
81
+ if forest.number_of_nodes() == 1:
82
+ return list(forest.nodes)
83
+
84
+ candidates = []
85
+ seed_path = self._seed_path(forest, seed_hexes)
86
+ if seed_path:
87
+ candidates.append(seed_path)
88
+
89
+ for component in nx.connected_components(forest):
90
+ subgraph = forest.subgraph(component)
91
+ candidates.append(self._diameter_path(subgraph))
92
+
93
+ return max(
94
+ candidates,
95
+ key=lambda path: (len(path), self._sum_node_attr(path, "count")),
96
+ default=[],
97
+ )
98
+
99
+ def _seed_path(self, forest: nx.Graph, seed_hexes: Sequence[str] | None) -> List[str]:
100
+ if not seed_hexes:
101
+ return []
102
+
103
+ present = [cell for cell in seed_hexes if cell in forest]
104
+ if len(present) < 2:
105
+ return []
106
+
107
+ best_path: List[str] = []
108
+ # Limit pair scans for very long seed routes while keeping endpoint intent.
109
+ candidates = present[:25] + present[-25:]
110
+ for idx, source in enumerate(candidates):
111
+ for target in candidates[idx + 1 :]:
112
+ if not nx.has_path(forest, source, target):
113
+ continue
114
+ path = nx.shortest_path(forest, source, target)
115
+ if len(path) > len(best_path):
116
+ best_path = path
117
+
118
+ return best_path
119
+
120
+ def _diameter_path(self, graph: nx.Graph) -> List[str]:
121
+ if graph.number_of_nodes() == 0:
122
+ return []
123
+ if graph.number_of_nodes() == 1:
124
+ return list(graph.nodes)
125
+
126
+ start = next(iter(graph.nodes))
127
+ first = self._farthest_by_hops(graph, start)
128
+ second = self._farthest_by_hops(graph, first)
129
+ return nx.shortest_path(graph, first, second)
130
+
131
+ def _farthest_by_hops(self, graph: nx.Graph, source: str) -> str:
132
+ lengths = nx.single_source_shortest_path_length(graph, source)
133
+ return max(lengths, key=lambda node: (lengths[node], self._node_metric(node)))
134
+
135
+ def _edge_strength(self, u: str, v: str, data: dict) -> float:
136
+ traversal_count = float(data.get("count", 0) or 0)
137
+ endpoint_strength = (self._node_metric(u) + self._node_metric(v)) / 2.0
138
+ distance = max(float(data.get("distance", data.get("weight", 1)) or 1), 1.0)
139
+ return traversal_count * 1000.0 + endpoint_strength - distance * 0.001
140
+
141
+ def _node_metric(self, node: str) -> float:
142
+ return float(self.graph.nodes[node].get("count", 0) or 0)
143
+
144
+ def _sum_node_attr(self, nodes: Iterable[str], attr: str) -> float:
145
+ return sum(float(self.graph.nodes[node].get(attr, 0) or 0) for node in nodes)
146
+
147
+ def _stable_component_order(self, component: Iterable[str]) -> List[str]:
148
+ return sorted(
149
+ component,
150
+ key=lambda node: (
151
+ -float(self.graph.nodes[node].get("count", 0) or 0),
152
+ str(node),
153
+ ),
154
+ )
155
+
156
+ def _main_attachment(self, component: Iterable[str], main_set: set[str]) -> str | None:
157
+ options = []
158
+ for node in component:
159
+ for neighbor in self.graph.neighbors(node):
160
+ if neighbor not in main_set:
161
+ continue
162
+ data = self.graph[node][neighbor]
163
+ options.append(
164
+ (
165
+ -float(data.get("count", 0) or 0),
166
+ float(data.get("distance", data.get("weight", 0)) or 0),
167
+ neighbor,
168
+ )
169
+ )
170
+
171
+ if not options:
172
+ return None
173
+ options.sort()
174
+ return options[0][2]
@@ -0,0 +1,197 @@
1
+ Metadata-Version: 2.4
2
+ Name: sameer-graph-lib
3
+ Version: 0.1.0
4
+ Summary: H3 and NetworkX based route affinity graph toolkit.
5
+ Author-email: Sameer <sameerkumarroy073@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/iams31/sameer_graph_lib
8
+ Project-URL: Repository, https://github.com/iams31/sameer_graph_lib
9
+ Project-URL: Issues, https://github.com/iams31/sameer_graph_lib/issues
10
+ Keywords: h3,networkx,geospatial,routing,graph
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: GIS
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: h3>=4.0.0
22
+ Requires-Dist: networkx>=3.0
23
+ Provides-Extra: plot
24
+ Requires-Dist: matplotlib>=3.7; extra == "plot"
25
+ Provides-Extra: geo
26
+ Requires-Dist: contextily>=1.5; extra == "geo"
27
+ Requires-Dist: geopandas>=0.14; extra == "geo"
28
+ Requires-Dist: shapely>=2.0; extra == "geo"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8.0; extra == "dev"
31
+ Requires-Dist: build>=1.2; extra == "dev"
32
+ Requires-Dist: twine>=5.1; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # sameer-graph-lib
36
+
37
+ `sameer-graph-lib` is an editable Python library for building H3-based route affinity graphs with NetworkX.
38
+
39
+ It turns H3 arrays, latitude/longitude sequences, and encoded polylines into connected hex chains, inserts them into a weighted graph, extracts high-affinity corridors, and decomposes the graph into a main trunk plus minor branches.
40
+
41
+ ## Editable install
42
+
43
+ ```powershell
44
+ python -m pip install -e ".[dev,plot]"
45
+ ```
46
+
47
+ Because the install is editable, changes you make inside `src/sameer_graph_lib` are picked up immediately by Python without reinstalling.
48
+
49
+ If your machine uses `uv`, run commands through the managed environment:
50
+
51
+ ```powershell
52
+ uv run --extra dev --extra plot python -c "import sameer_graph_lib; print(sameer_graph_lib.__version__)"
53
+ ```
54
+
55
+ ## Install
56
+
57
+ From PyPI after publication:
58
+
59
+ ```powershell
60
+ pip install sameer-graph-lib
61
+ ```
62
+
63
+ With optional plotting and geospatial extras:
64
+
65
+ ```powershell
66
+ pip install "sameer-graph-lib[plot,geo]"
67
+ ```
68
+
69
+ ## Quick start
70
+
71
+ ```python
72
+ from sameer_graph_lib import HexGraph
73
+
74
+ graph = HexGraph(hex_resolution=9)
75
+
76
+ route = graph.add_latlng_sequence([
77
+ (12.9716, 77.5946),
78
+ (12.9760, 77.5990),
79
+ ])
80
+
81
+ print(graph.get_graph_stats())
82
+ selected = graph.get_appropriate_hexes(cutoff=0.8)
83
+ fig = graph.visualize_graph(title="80% compact cluster", highlight_hexes=selected)
84
+ print(graph.decompose_branches())
85
+ ```
86
+
87
+ ## Main APIs
88
+
89
+ - `SpatialIngestor`: converts H3 arrays, lat/lng sequences, and encoded polylines into contiguous H3 chains.
90
+ - `AffinityGraph`: NetworkX wrapper for array-based insertion, nearest attachment, affinity scoring, editing, and JSON persistence.
91
+ - `CorridorExtractor`: uses exact all-node Dijkstra selection to extract the most compact cluster covering a target percentage of graph traversal volume.
92
+ - `TopologyAnalyzer`: separates the main branch from residual minor branches.
93
+ - `HexGraph`: backwards-compatible convenience class for your original code style.
94
+
95
+ Graph creation follows the original per-node procedure: H3 arrays are normalized, then each hex is inserted with `add_node`/`add_hex`. Lat/lng sequences and encoded polylines are first converted into H3 arrays at the requested resolution, then inserted the same way.
96
+
97
+ For QC, use:
98
+
99
+ ```python
100
+ fig = graph.visualize_graph(highlight_hexes=selected)
101
+ fig.savefig("graph_qc.png", dpi=150, bbox_inches="tight")
102
+
103
+ fig = graph.visualize_step_by_step(route[:5], labels=["A", "B", "C", "D", "E"])
104
+ fig.savefig("insertion_steps.png", dpi=150, bbox_inches="tight")
105
+ ```
106
+
107
+ To plot actual H3 hex boundaries as geospatial polygons:
108
+
109
+ ```python
110
+ from sameer_graph_lib import plot_h3_cells, plot_h3_cells_map
111
+
112
+ fig = plot_h3_cells("88618c4f29fffff", label_full_hex=True)
113
+ fig.savefig("single_h3_cell.png", dpi=150, bbox_inches="tight")
114
+
115
+ fig = graph.plot_h3_cells(highlight_hexes=selected, show_labels=False)
116
+ fig.savefig("h3_cell_footprint.png", dpi=150, bbox_inches="tight")
117
+
118
+ fig = plot_h3_cells_map(route, selected_cells=selected)
119
+ fig.savefig("h3_cell_basemap.png", dpi=150, bbox_inches="tight")
120
+ ```
121
+
122
+ `plot_h3_cells_map` uses GeoPandas + Contextily. Install it with:
123
+
124
+ ```powershell
125
+ python -m pip install -e ".[plot,geo]"
126
+ uv run --extra plot --extra geo python -c "from sameer_graph_lib import plot_h3_cells_map"
127
+ ```
128
+
129
+ To get H3 centers and a convex hull:
130
+
131
+ ```python
132
+ from sameer_graph_lib import getLatLng, h3_convex_hull
133
+
134
+ points = getLatLng(route) # [(lat, lng), ...]
135
+ hull = h3_convex_hull(route) # Shapely geometry in (lng, lat)
136
+ graph_hull = graph.convex_hull() # Same, using graph nodes
137
+ ```
138
+
139
+ ## Useful commands
140
+
141
+ ```powershell
142
+ python -m pytest
143
+ python -m build
144
+ uv run --extra dev pytest -q
145
+ uv run --extra dev python -m build
146
+ uv run --extra dev python -m twine check dist/*
147
+ ```
148
+
149
+ Build artifacts will appear in `dist/` after `python -m build`.
150
+
151
+ ## Publish To PyPI
152
+
153
+ 1. Build the package:
154
+
155
+ ```powershell
156
+ uv run --extra dev python -m build
157
+ ```
158
+
159
+ 2. Validate the package metadata:
160
+
161
+ ```powershell
162
+ uv run --extra dev python -m twine check dist/*
163
+ ```
164
+
165
+ 3. Upload to PyPI:
166
+
167
+ ```powershell
168
+ uv run --extra dev python -m twine upload dist/*
169
+ ```
170
+
171
+ After upload, users can install it with:
172
+
173
+ ```powershell
174
+ pip install sameer-graph-lib
175
+ ```
176
+
177
+ ## Publish From GitHub
178
+
179
+ This repo also includes a Trusted Publishing workflow in
180
+ [.github/workflows/publish.yml](C:/Users/rrran/Desktop/sameer_graph_lib/.github/workflows/publish.yml:1).
181
+
182
+ To finish that setup:
183
+
184
+ 1. Create the project on PyPI, or reserve the name `sameer-graph-lib`.
185
+ 2. On PyPI, open the project settings and add a Trusted Publisher for:
186
+ `owner`: `iams31`
187
+ `repository`: `sameer_graph_lib`
188
+ `workflow`: `publish.yml`
189
+ `environment`: `pypi`
190
+ 3. Create a GitHub Release, or run the workflow manually from the Actions tab.
191
+
192
+ After that, GitHub Actions can publish without storing a long-lived PyPI token.
193
+
194
+ Official references:
195
+
196
+ - PyPI Trusted Publishing: https://docs.pypi.org/trusted-publishers/
197
+ - Packaging guide upload flow: https://packaging.python.org/tutorials/packaging-projects/
@@ -0,0 +1,14 @@
1
+ sameer_graph_lib/__init__.py,sha256=rfzOeoVj_WmdLCH1j2Is2p6mXlvg9ohJ1CAvlH-uFbM,791
2
+ sameer_graph_lib/_h3.py,sha256=Vlch4zX9SR6hg4S-5lYGDNEwEH9IVosRIEg_n2Emq0I,3655
3
+ sameer_graph_lib/affinity_graph.py,sha256=DCFLbRJ3rTmHR5oBtxNAyAZWE7sxn-rFwf3pPgmfptk,28681
4
+ sameer_graph_lib/corridor_extractor.py,sha256=V77VtxPHWBAJYzQz553TdoRpuo3KdnEeI6H3iPex998,2667
5
+ sameer_graph_lib/geometry.py,sha256=unCcwqZU96kewJTNpaym31B2o32v6xsmiP2hdv2Y6VY,2288
6
+ sameer_graph_lib/hex_graph.py,sha256=51VHsD5_xVS95UW4viiBjffHdr9xziDg_YQeIHplWsU,454
7
+ sameer_graph_lib/plotting.py,sha256=DPuUdSmoJMUQloWqEV-bauZXJjUFvKWUCtTs2WI8dvM,6597
8
+ sameer_graph_lib/spatial_ingestor.py,sha256=3GDaTWT5etbC55fIzSTB5D0gS3HzyRzyMRKR0tjEsw0,5608
9
+ sameer_graph_lib/topology_analyzer.py,sha256=JfZtbBeIrTZaZdzG41fsnoNZgcssml4iSZWVWAevBSc,6367
10
+ sameer_graph_lib-0.1.0.dist-info/licenses/LICENSE,sha256=5VuvbSI3M1-d6PVSlQm5_zjokepvD9eFuWOvXS0udls,1069
11
+ sameer_graph_lib-0.1.0.dist-info/METADATA,sha256=8UElHhnErBPw92gnA7-P8uHFsHd1S2tRdSKdMr9qmTs,6282
12
+ sameer_graph_lib-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
13
+ sameer_graph_lib-0.1.0.dist-info/top_level.txt,sha256=tgFV-BCa4PLQQ0AF9nOM1VscPuEX4LlO-DTe6H_Xnv8,17
14
+ sameer_graph_lib-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sameer Kumar
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.
@@ -0,0 +1 @@
1
+ sameer_graph_lib