scgraph 2.2.0__tar.gz → 2.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scgraph
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Determine an approximate route between two points on earth.
5
5
  Author-email: Connor Makowski <conmak@mit.edu>
6
6
  Project-URL: Homepage, https://github.com/connor-makowski/scgraph
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://connor-makowski.github.io/scgraph/core.html
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: OS Independent
12
- Requires-Python: >=3.6
12
+ Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
 
@@ -57,7 +57,9 @@ Low Level: https://connor-makowski.github.io/scgraph/scgraph/core.html
57
57
 
58
58
  ## Setup
59
59
 
60
- Make sure you have Python 3.6.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/).
60
+ Make sure you have Python 3.10.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/).
61
+
62
+ Support for python3.6-3.9 is available up to version 2.2.0
61
63
 
62
64
  ## Installation
63
65
 
@@ -67,7 +69,10 @@ pip install scgraph
67
69
 
68
70
  ## Use with Google Colab
69
71
 
70
- See the example [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example.ipynb)
72
+ - [Getting Started](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/getting_started.ipynb)
73
+ - [Creating A Multi Path Geojson](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/multi_path_geojson.ipynb)
74
+ - [Modifying A Geograph](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/geograph_modifications.ipynb)
75
+
71
76
 
72
77
  # Getting Started
73
78
 
@@ -101,7 +106,7 @@ In the above example, the `output` variable is a dictionary with three keys: `le
101
106
  - `ft` (feet)
102
107
  - `coordinate_path`: A list of lists [`latitude`,`longitude`] that make up the shortest path
103
108
 
104
- For more examples including viewing the output on a map, see the [example notebook](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example.ipynb).
109
+ For more examples including viewing the output on a map, see the [example notebook](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/getting_started.ipynb).
105
110
 
106
111
  ## Included GeoGraphs
107
112
 
@@ -165,7 +170,7 @@ output = marnet_geograph.get_shortest_path(
165
170
  get_line_path(output, filename='output.geojson')
166
171
  ```
167
172
 
168
- Modify an existing geograph: See the notebook [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example_making_modifications.ipynb)
173
+ Modify an existing geograph: See the notebook [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/geograph_modifications.ipynb)
169
174
 
170
175
 
171
176
  You can specify your own custom graphs for direct access to the solving algorithms. This requires the use of the low level `Graph` class
@@ -173,9 +178,9 @@ You can specify your own custom graphs for direct access to the solving algorith
173
178
  ```py
174
179
  from scgraph import Graph
175
180
 
176
- # Define a graph
181
+ # Define an arbitrary graph
177
182
  # See the graph definitions here:
178
- # https://connor-makowski.github.io/scgraph/scgraph/core.html
183
+ # https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
179
184
  graph = [
180
185
  {1: 5, 2: 1},
181
186
  {0: 5, 2: 2, 3: 1},
@@ -200,25 +205,67 @@ from scgraph import GeoGraph
200
205
 
201
206
  # Define nodes
202
207
  # See the nodes definitions here:
203
- # https://connor-makowski.github.io/scgraph/scgraph/core.html
208
+ # https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
204
209
  nodes = [
205
- [0,0],
206
- [0,1],
207
- [1,0],
208
- [1,1],
209
- [1,2],
210
- [2,1]
210
+ # London
211
+ [51.5074, -0.1278],
212
+ # Paris
213
+ [48.8566, 2.3522],
214
+ # Berlin
215
+ [52.5200, 13.4050],
216
+ # Rome
217
+ [41.9028, 12.4964],
218
+ # Madrid
219
+ [40.4168, -3.7038],
220
+ # Lisbon
221
+ [38.7223, -9.1393]
211
222
  ]
212
223
  # Define a graph
213
224
  # See the graph definitions here:
214
- # https://connor-makowski.github.io/scgraph/scgraph/core.html
225
+ # https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
215
226
  graph = [
216
- {1: 5, 2: 1},
217
- {0: 5, 2: 2, 3: 1},
218
- {0: 1, 1: 2, 3: 4, 4: 8},
219
- {1: 1, 2: 4, 4: 3, 5: 6},
220
- {2: 8, 3: 3},
221
- {3: 6}
227
+ # From London
228
+ {
229
+ # To Paris
230
+ 1: 311,
231
+ },
232
+ # From Paris
233
+ {
234
+ # To London
235
+ 0: 311,
236
+ # To Berlin
237
+ 2: 878,
238
+ # To Rome
239
+ 3: 1439,
240
+ # To Madrid
241
+ 4: 1053
242
+ },
243
+ # From Berlin
244
+ {
245
+ # To Paris
246
+ 1: 878,
247
+ # To Rome
248
+ 3: 1181,
249
+ },
250
+ # From Rome
251
+ {
252
+ # To Paris
253
+ 1: 1439,
254
+ # To Berlin
255
+ 2: 1181,
256
+ },
257
+ # From Madrid
258
+ {
259
+ # To Paris
260
+ 1: 1053,
261
+ # To Lisbon
262
+ 5: 623
263
+ },
264
+ # From Lisbon
265
+ {
266
+ # To Madrid
267
+ 4: 623
268
+ }
222
269
  ]
223
270
 
224
271
  # Create a GeoGraph object
@@ -231,23 +278,31 @@ my_geograph.validate_graph()
231
278
  my_geograph.validate_nodes()
232
279
 
233
280
  # Get the shortest path between two points
281
+ # In this case, Birmingham England and Zaragoza Spain
282
+ # Since Birmingham and Zaragoza are not in the graph,
283
+ # the algorithm will add them into the graph.
284
+ # See: https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph.get_shortest_path
285
+ # Expected output would be to go from
286
+ # Birmingham -> London -> Paris -> Madrid -> Zaragoza
287
+
234
288
  output = my_geograph.get_shortest_path(
235
- origin_node = {'latitude': 0, 'longitude': 0},
236
- destination_node = {'latitude': 2, 'longitude': 1}
289
+ # Birmingham England
290
+ origin_node = {'latitude': 52.4862, 'longitude': -1.8904},
291
+ # Zaragoza Spain
292
+ destination_node = {'latitude': 41.6488, 'longitude': -0.8891}
237
293
  )
238
- #=>
294
+ print(output)
239
295
  # {
240
- # "coordinate_path": [
241
- # [0,0],
242
- # [0,0],
243
- # [1,0],
244
- # [0,1],
245
- # [1,1],
246
- # [2,1],
247
- # [2,1]
248
- # ],
249
- # "length": 10
296
+ # 'length': 1799.4323,
297
+ # 'coordinate_path': [
298
+ # [52.4862, -1.8904],
299
+ # [51.5074, -0.1278],
300
+ # [48.8566, 2.3522],
301
+ # [40.4168, -3.7038],
302
+ # [41.6488, -0.8891]
303
+ # ]
250
304
  # }
305
+
251
306
  ```
252
307
 
253
308
  ## Attributions and Thanks
@@ -42,7 +42,9 @@ Low Level: https://connor-makowski.github.io/scgraph/scgraph/core.html
42
42
 
43
43
  ## Setup
44
44
 
45
- Make sure you have Python 3.6.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/).
45
+ Make sure you have Python 3.10.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/).
46
+
47
+ Support for python3.6-3.9 is available up to version 2.2.0
46
48
 
47
49
  ## Installation
48
50
 
@@ -52,7 +54,10 @@ pip install scgraph
52
54
 
53
55
  ## Use with Google Colab
54
56
 
55
- See the example [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example.ipynb)
57
+ - [Getting Started](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/getting_started.ipynb)
58
+ - [Creating A Multi Path Geojson](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/multi_path_geojson.ipynb)
59
+ - [Modifying A Geograph](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/geograph_modifications.ipynb)
60
+
56
61
 
57
62
  # Getting Started
58
63
 
@@ -86,7 +91,7 @@ In the above example, the `output` variable is a dictionary with three keys: `le
86
91
  - `ft` (feet)
87
92
  - `coordinate_path`: A list of lists [`latitude`,`longitude`] that make up the shortest path
88
93
 
89
- For more examples including viewing the output on a map, see the [example notebook](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example.ipynb).
94
+ For more examples including viewing the output on a map, see the [example notebook](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/getting_started.ipynb).
90
95
 
91
96
  ## Included GeoGraphs
92
97
 
@@ -150,7 +155,7 @@ output = marnet_geograph.get_shortest_path(
150
155
  get_line_path(output, filename='output.geojson')
151
156
  ```
152
157
 
153
- Modify an existing geograph: See the notebook [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example_making_modifications.ipynb)
158
+ Modify an existing geograph: See the notebook [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/geograph_modifications.ipynb)
154
159
 
155
160
 
156
161
  You can specify your own custom graphs for direct access to the solving algorithms. This requires the use of the low level `Graph` class
@@ -158,9 +163,9 @@ You can specify your own custom graphs for direct access to the solving algorith
158
163
  ```py
159
164
  from scgraph import Graph
160
165
 
161
- # Define a graph
166
+ # Define an arbitrary graph
162
167
  # See the graph definitions here:
163
- # https://connor-makowski.github.io/scgraph/scgraph/core.html
168
+ # https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
164
169
  graph = [
165
170
  {1: 5, 2: 1},
166
171
  {0: 5, 2: 2, 3: 1},
@@ -185,25 +190,67 @@ from scgraph import GeoGraph
185
190
 
186
191
  # Define nodes
187
192
  # See the nodes definitions here:
188
- # https://connor-makowski.github.io/scgraph/scgraph/core.html
193
+ # https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
189
194
  nodes = [
190
- [0,0],
191
- [0,1],
192
- [1,0],
193
- [1,1],
194
- [1,2],
195
- [2,1]
195
+ # London
196
+ [51.5074, -0.1278],
197
+ # Paris
198
+ [48.8566, 2.3522],
199
+ # Berlin
200
+ [52.5200, 13.4050],
201
+ # Rome
202
+ [41.9028, 12.4964],
203
+ # Madrid
204
+ [40.4168, -3.7038],
205
+ # Lisbon
206
+ [38.7223, -9.1393]
196
207
  ]
197
208
  # Define a graph
198
209
  # See the graph definitions here:
199
- # https://connor-makowski.github.io/scgraph/scgraph/core.html
210
+ # https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
200
211
  graph = [
201
- {1: 5, 2: 1},
202
- {0: 5, 2: 2, 3: 1},
203
- {0: 1, 1: 2, 3: 4, 4: 8},
204
- {1: 1, 2: 4, 4: 3, 5: 6},
205
- {2: 8, 3: 3},
206
- {3: 6}
212
+ # From London
213
+ {
214
+ # To Paris
215
+ 1: 311,
216
+ },
217
+ # From Paris
218
+ {
219
+ # To London
220
+ 0: 311,
221
+ # To Berlin
222
+ 2: 878,
223
+ # To Rome
224
+ 3: 1439,
225
+ # To Madrid
226
+ 4: 1053
227
+ },
228
+ # From Berlin
229
+ {
230
+ # To Paris
231
+ 1: 878,
232
+ # To Rome
233
+ 3: 1181,
234
+ },
235
+ # From Rome
236
+ {
237
+ # To Paris
238
+ 1: 1439,
239
+ # To Berlin
240
+ 2: 1181,
241
+ },
242
+ # From Madrid
243
+ {
244
+ # To Paris
245
+ 1: 1053,
246
+ # To Lisbon
247
+ 5: 623
248
+ },
249
+ # From Lisbon
250
+ {
251
+ # To Madrid
252
+ 4: 623
253
+ }
207
254
  ]
208
255
 
209
256
  # Create a GeoGraph object
@@ -216,23 +263,31 @@ my_geograph.validate_graph()
216
263
  my_geograph.validate_nodes()
217
264
 
218
265
  # Get the shortest path between two points
266
+ # In this case, Birmingham England and Zaragoza Spain
267
+ # Since Birmingham and Zaragoza are not in the graph,
268
+ # the algorithm will add them into the graph.
269
+ # See: https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph.get_shortest_path
270
+ # Expected output would be to go from
271
+ # Birmingham -> London -> Paris -> Madrid -> Zaragoza
272
+
219
273
  output = my_geograph.get_shortest_path(
220
- origin_node = {'latitude': 0, 'longitude': 0},
221
- destination_node = {'latitude': 2, 'longitude': 1}
274
+ # Birmingham England
275
+ origin_node = {'latitude': 52.4862, 'longitude': -1.8904},
276
+ # Zaragoza Spain
277
+ destination_node = {'latitude': 41.6488, 'longitude': -0.8891}
222
278
  )
223
- #=>
279
+ print(output)
224
280
  # {
225
- # "coordinate_path": [
226
- # [0,0],
227
- # [0,0],
228
- # [1,0],
229
- # [0,1],
230
- # [1,1],
231
- # [2,1],
232
- # [2,1]
233
- # ],
234
- # "length": 10
281
+ # 'length': 1799.4323,
282
+ # 'coordinate_path': [
283
+ # [52.4862, -1.8904],
284
+ # [51.5074, -0.1278],
285
+ # [48.8566, 2.3522],
286
+ # [40.4168, -3.7038],
287
+ # [41.6488, -0.8891]
288
+ # ]
235
289
  # }
290
+
236
291
  ```
237
292
 
238
293
  ## Attributions and Thanks
@@ -12,13 +12,13 @@ build-backend = "setuptools.build_meta"
12
12
 
13
13
  [project]
14
14
  name = "scgraph"
15
- version = "2.2.0"
15
+ version = "2.3.0"
16
16
  description = "Determine an approximate route between two points on earth."
17
17
  authors = [
18
18
  {name="Connor Makowski", email="conmak@mit.edu"}
19
19
  ]
20
20
  readme = "README.md"
21
- requires-python = ">=3.6"
21
+ requires-python = ">=3.10"
22
22
  classifiers = [
23
23
  "Programming Language :: Python :: 3",
24
24
  'License :: OSI Approved :: MIT License',
@@ -1,4 +1,4 @@
1
- from .utils import haversine, hard_round, distance_converter
1
+ from .utils import haversine, hard_round, distance_converter, get_line_path
2
2
  import json
3
3
 
4
4
 
@@ -16,11 +16,10 @@ class Graph:
16
16
 
17
17
  Required Arguments:
18
18
 
19
- - `graph`
19
+ - `graph`:
20
20
  - Type: list of dictionaries
21
- - What: A list of dictionaries where the indicies are origin node ids and the values are dictionaries of destination node indices and distances
22
- - Note: All nodes must be included as origins in the graph regardless of if they have any connected destinations
23
-
21
+ - See: https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
22
+
24
23
  Optional Arguments:
25
24
 
26
25
  - `check_symmetry`
@@ -75,10 +74,9 @@ class Graph:
75
74
 
76
75
  Required Arguments:
77
76
 
78
- - `graph`
77
+ - `graph`:
79
78
  - Type: list of dictionaries
80
- - What: A list of dictionaries where the indicies are origin node ids and the values are dictionaries of destination node ids and distances
81
- - Note: All nodes must be included as origins in the graph regardless of if they have any connected destinations
79
+ - See: https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
82
80
 
83
81
  Optional Arguments:
84
82
 
@@ -121,9 +119,9 @@ class Graph:
121
119
 
122
120
  Required Arguments:
123
121
 
124
- - `graph`
125
- - Type: list[dict]
126
- - What: A list of dictionaries where the indicies are origin node ids and the values are dictionaries of destination node ids and distances
122
+ - `graph`:
123
+ - Type: list of dictionaries
124
+ - See: https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
127
125
  - `origin_id`
128
126
  - Type: int
129
127
  - What: The id of the origin node from the graph dictionary to start the shortest path from
@@ -167,9 +165,9 @@ class Graph:
167
165
 
168
166
  Required Arguments:
169
167
 
170
- - `graph`
171
- - Type: list[dict]
172
- - What: A list of dictionaries where the indicies are origin node ids and the values are dictionaries of destination node ids and distances
168
+ - `graph`:
169
+ - Type: list of dictionaries
170
+ - See: https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
173
171
  - `origin_id`
174
172
  - Type: int
175
173
  - What: The id of the origin node from the graph dictionary to start the shortest path from
@@ -242,9 +240,9 @@ class Graph:
242
240
 
243
241
  Required Arguments:
244
242
 
245
- - `graph`
246
- - Type: list[dict]
247
- - What: A list of dictionaries where the indicies are origin node ids and the values are dictionaries of destination node ids and distances
243
+ - `graph`:
244
+ - Type: list of dictionaries
245
+ - See: https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
248
246
  - `origin_id`
249
247
  - Type: int
250
248
  - What: The id of the origin node from the graph dictionary to start the shortest path from
@@ -298,7 +296,7 @@ class Graph:
298
296
 
299
297
  class GeoGraph:
300
298
  def __init__(
301
- self, graph: list[dict], nodes: list[list[int, float]]
299
+ self, graph: list[dict], nodes: list[list[float|int]]
302
300
  ) -> None:
303
301
  """
304
302
  Function:
@@ -311,10 +309,74 @@ class GeoGraph:
311
309
  - Type: list of dictionaries
312
310
  - What: A list of dictionaries where the indicies are origin node ids and the values are dictionaries of destination node indices and distances
313
311
  - Note: All nodes must be included as origins in the graph regardless of if they have any connected destinations
312
+ - EG:
313
+ ```
314
+ [
315
+ # From London
316
+ {
317
+ # To Paris
318
+ 1: 311,
319
+ },
320
+ # From Paris
321
+ {
322
+ # To London
323
+ 0: 311,
324
+ # To Berlin
325
+ 2: 878,
326
+ # To Rome
327
+ 3: 1439,
328
+ # To Madrid
329
+ 4: 1053
330
+ },
331
+ # From Berlin
332
+ {
333
+ # To Paris
334
+ 1: 878,
335
+ # To Rome
336
+ 3: 1181,
337
+ },
338
+ # From Rome
339
+ {
340
+ # To Paris
341
+ 1: 1439,
342
+ # To Berlin
343
+ 2: 1181,
344
+ },
345
+ # From Madrid
346
+ {
347
+ # To Paris
348
+ 1: 1053,
349
+ # To Lisbon
350
+ 5: 623
351
+ },
352
+ # From Lisbon
353
+ {
354
+ # To Madrid
355
+ 4: 623
356
+ }
357
+ ]
358
+ ```
314
359
  - `nodes`
315
- - Type: list of lists
360
+ - Type: list of lists of ints or floats
316
361
  - What: A list of lists where the values are coordinates (latitude then longitude)
317
362
  - Note: The length of the nodes list must be the same as that of the graph list
363
+ - EG:
364
+ ```
365
+ [
366
+ # London
367
+ [51.5074, 0.1278],
368
+ # Paris
369
+ [48.8566, 2.3522],
370
+ # Berlin
371
+ [52.5200, 13.4050],
372
+ # Rome
373
+ [41.9028, 12.4964],
374
+ # Madrid
375
+ [40.4168, 3.7038],
376
+ # Lisbon
377
+ [38.7223, 9.1393]
378
+ ]
379
+ ```
318
380
  """
319
381
  self.graph = graph
320
382
  self.nodes = nodes
@@ -394,17 +456,17 @@ class GeoGraph:
394
456
 
395
457
  def get_shortest_path(
396
458
  self,
397
- origin_node: dict[int, float],
398
- destination_node: dict[int, float],
459
+ origin_node: dict[float|int],
460
+ destination_node: dict[float|int],
399
461
  output_units: str = "km",
400
462
  algorithm_fn=Graph.dijkstra_makowski,
401
- off_graph_circuity: [int, float] = 1,
463
+ off_graph_circuity: [float|int] = 1,
402
464
  node_addition_type: str = "quadrant",
403
- node_addition_circuity: [int, float] = 4,
465
+ node_addition_circuity: [float|int] = 4,
404
466
  geograph_units: str = "km",
405
467
  output_coordinate_path: str = "list_of_lists",
406
468
  output_path: bool = False,
407
- node_addition_lat_lon_bound: [int, float] = 5,
469
+ node_addition_lat_lon_bound: [float|int] = 5,
408
470
  node_addition_math: str = "euclidean",
409
471
  **kwargs,
410
472
  ) -> dict:
@@ -447,6 +509,7 @@ class GeoGraph:
447
509
  - 'Graph.dijkstra_makowski': A modified dijkstra algorithm that uses a sparse distance matrix to identify the shortest path
448
510
  - Any user defined algorithm that takes the arguments:
449
511
  - `graph`: A dictionary of dictionaries where the keys are origin node ids and the values are dictionaries of destination node ids and distances
512
+ - See: https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
450
513
  - `origin`: The id of the origin node from the graph dictionary to start the shortest path from
451
514
  - `destination`: The id of the destination node from the graph dictionary to end the shortest path at
452
515
  - `off_graph_circuity`
@@ -579,9 +642,9 @@ class GeoGraph:
579
642
  def adujust_circuity_length(
580
643
  self,
581
644
  output: dict,
582
- node_addition_circuity: [float, int],
583
- off_graph_circuity: [float, int],
584
- ) -> [float, int]:
645
+ node_addition_circuity: [float|int],
646
+ off_graph_circuity: [float|int],
647
+ ) -> [float|int]:
585
648
  """
586
649
  Function:
587
650
 
@@ -616,7 +679,7 @@ class GeoGraph:
616
679
  4,
617
680
  )
618
681
 
619
- def get_coordinate_path(self, path: list[int]) -> list[dict[int, float]]:
682
+ def get_coordinate_path(self, path: list[int]) -> list[dict[float|int]]:
620
683
  """
621
684
  Function:
622
685
 
@@ -660,11 +723,11 @@ class GeoGraph:
660
723
  def get_node_distances(
661
724
  self,
662
725
  node: list,
663
- circuity: [int, float],
726
+ circuity: [float|int],
664
727
  node_addition_type: str,
665
728
  node_addition_math: str,
666
- lat_lon_bound: [int, float],
667
- ) -> dict[int, float]:
729
+ lat_lon_bound: [float|int],
730
+ ) -> dict[float|int]:
668
731
  """
669
732
  Function:
670
733
 
@@ -764,11 +827,11 @@ class GeoGraph:
764
827
 
765
828
  def add_node(
766
829
  self,
767
- node: dict[int, float],
768
- circuity: [int, float],
830
+ node: dict[float|int],
831
+ circuity: [float|int],
769
832
  node_addition_type: str = "quadrant",
770
833
  node_addition_math: str = "euclidean",
771
- lat_lon_bound: [int, float] = 5,
834
+ lat_lon_bound: [float|int] = 5,
772
835
  ) -> int:
773
836
  """
774
837
  Function:
@@ -923,14 +986,14 @@ class GeoGraph:
923
986
  - Import as: 'from .custom import custom_geograph'
924
987
  """
925
988
  self.validate_nodes()
926
- self.validate_graph(
927
- check_symmetry=True, check_connected=False
928
- )
989
+ self.validate_graph(check_symmetry=True, check_connected=False)
929
990
  out_string = f"""from scgraph.core import GeoGraph\ngraph={str(self.graph)}\nnodes={str(self.nodes)}\n{name}_geograph = GeoGraph(graph=graph, nodes=nodes)"""
930
- with open(name+".py", "w") as f:
991
+ with open(name + ".py", "w") as f:
931
992
  f.write(out_string)
932
-
933
- def mod_remove_arc(self, origin_idx: int, destination_idx: int, undirected: bool = True) -> None:
993
+
994
+ def mod_remove_arc(
995
+ self, origin_idx: int, destination_idx: int, undirected: bool = True
996
+ ) -> None:
934
997
  """
935
998
  Function:
936
999
 
@@ -953,14 +1016,18 @@ class GeoGraph:
953
1016
  - Default: True
954
1017
  """
955
1018
  assert origin_idx < len(self.graph), "Origin node does not exist"
956
- assert destination_idx < len(self.graph), "Destination node does not exist"
1019
+ assert destination_idx < len(
1020
+ self.graph
1021
+ ), "Destination node does not exist"
957
1022
  assert destination_idx in self.graph[origin_idx], "Arc does not exist"
958
1023
  del self.graph[origin_idx][destination_idx]
959
1024
  if undirected:
960
1025
  if origin_idx in self.graph[destination_idx]:
961
1026
  del self.graph[destination_idx][origin_idx]
962
1027
 
963
- def mod_add_node(self, latitude: [float, int], longitude: [float, int]) -> int:
1028
+ def mod_add_node(
1029
+ self, latitude: [float|int], longitude: [float|int]
1030
+ ) -> int:
964
1031
  """
965
1032
  Function:
966
1033
 
@@ -982,8 +1049,15 @@ class GeoGraph:
982
1049
  self.nodes.append([latitude, longitude])
983
1050
  self.graph.append({})
984
1051
  return len(self.graph) - 1
985
-
986
- def mod_add_arc(self, origin_idx: int, destination_idx: int, distance: [int, float]=0, use_haversine_distance=True, undirected: bool = True) -> None:
1052
+
1053
+ def mod_add_arc(
1054
+ self,
1055
+ origin_idx: int,
1056
+ destination_idx: int,
1057
+ distance: [float|int] = 0,
1058
+ use_haversine_distance=True,
1059
+ undirected: bool = True,
1060
+ ) -> None:
987
1061
  """
988
1062
  Function:
989
1063
 
@@ -1015,13 +1089,18 @@ class GeoGraph:
1015
1089
  - Default: True
1016
1090
  """
1017
1091
  assert origin_idx < len(self.graph), "Origin node does not exist"
1018
- assert destination_idx < len(self.graph), "Destination node does not exist"
1092
+ assert destination_idx < len(
1093
+ self.graph
1094
+ ), "Destination node does not exist"
1019
1095
  if use_haversine_distance:
1020
- distance = haversine(self.nodes[origin_idx], self.nodes[destination_idx])
1096
+ distance = haversine(
1097
+ self.nodes[origin_idx], self.nodes[destination_idx]
1098
+ )
1021
1099
  self.graph[origin_idx][destination_idx] = distance
1022
1100
  if undirected:
1023
1101
  self.graph[destination_idx][origin_idx] = distance
1024
1102
 
1103
+
1025
1104
  def load_geojson_as_geograph(geojson_filename: str) -> GeoGraph:
1026
1105
  """
1027
1106
  Function:
@@ -1066,9 +1145,9 @@ def load_geojson_as_geograph(geojson_filename: str) -> GeoGraph:
1066
1145
  }
1067
1146
  ```
1068
1147
  """
1069
- with open (geojson_filename, "r") as f:
1148
+ with open(geojson_filename, "r") as f:
1070
1149
  geojson_features = json.load(f).get("features", [])
1071
-
1150
+
1072
1151
  nodes_dict = {}
1073
1152
  graph_dict = {}
1074
1153
  for feature in geojson_features:
@@ -1080,14 +1159,30 @@ def load_geojson_as_geograph(geojson_filename: str) -> GeoGraph:
1080
1159
  coordinates = geometry.get("coordinates", [])
1081
1160
 
1082
1161
  # Validations
1083
- assert feature.get("type") == "Feature", "All features must be of type 'Feature'"
1084
- assert geometry.get("type") == "LineString", "All geometries must be of type 'LineString'"
1085
- assert len(coordinates) == 2, "All LineStrings must have exactly 2 coordinates"
1086
- assert isinstance(origin_idx, int), "All features must have an 'origin_idx' property that is an integer"
1087
- assert isinstance(destination_idx, int), "All features must have a 'destination_idx' property that is an integer"
1088
- assert isinstance(distance, (int, float)), "All features must have a 'distance' property that is a number"
1089
- assert origin_idx >= 0, "All origin_idxs must be greater than or equal to 0"
1090
- assert destination_idx >= 0, "All destination_idxs must be greater than or equal to 0"
1162
+ assert (
1163
+ feature.get("type") == "Feature"
1164
+ ), "All features must be of type 'Feature'"
1165
+ assert (
1166
+ geometry.get("type") == "LineString"
1167
+ ), "All geometries must be of type 'LineString'"
1168
+ assert (
1169
+ len(coordinates) == 2
1170
+ ), "All LineStrings must have exactly 2 coordinates"
1171
+ assert isinstance(
1172
+ origin_idx, int
1173
+ ), "All features must have an 'origin_idx' property that is an integer"
1174
+ assert isinstance(
1175
+ destination_idx, int
1176
+ ), "All features must have a 'destination_idx' property that is an integer"
1177
+ assert isinstance(
1178
+ distance, (int, float)
1179
+ ), "All features must have a 'distance' property that is a number"
1180
+ assert (
1181
+ origin_idx >= 0
1182
+ ), "All origin_idxs must be greater than or equal to 0"
1183
+ assert (
1184
+ destination_idx >= 0
1185
+ ), "All destination_idxs must be greater than or equal to 0"
1091
1186
  assert distance >= 0, "All distances must be greater than or equal to 0"
1092
1187
  origin = coordinates[0]
1093
1188
  destination = coordinates[1]
@@ -1095,24 +1190,173 @@ def load_geojson_as_geograph(geojson_filename: str) -> GeoGraph:
1095
1190
  assert isinstance(destination, list), "All coordinates must be lists"
1096
1191
  assert len(origin) == 2, "All coordinates must have a length of 2"
1097
1192
  assert len(destination) == 2, "All coordinates must have a length of 2"
1098
- assert all([isinstance(i, (int, float)) for i in origin]), "All coordinates must be numeric"
1099
- assert all([isinstance(i, (int, float)) for i in destination]), "All coordinates must be numeric"
1193
+ assert all(
1194
+ [isinstance(i, (int, float)) for i in origin]
1195
+ ), "All coordinates must be numeric"
1196
+ assert all(
1197
+ [isinstance(i, (int, float)) for i in destination]
1198
+ ), "All coordinates must be numeric"
1100
1199
  # assert all([origin[0] >= -90, origin[0] <= 90, origin[1] >= -180, origin[1] <= 180]), "All coordinates must be valid latitudes and longitudes"
1101
1200
  # assert all([destination[0] >= -90, destination[0] <= 90, destination[1] >= -180, destination[1] <= 180]), "All coordinates must be valid latitudes and longitudes"
1102
-
1201
+
1103
1202
  # Update the data
1104
1203
  nodes_dict[origin_idx] = origin
1105
1204
  nodes_dict[destination_idx] = destination
1106
- graph_dict[origin_idx] = {**graph_dict.get(origin_idx, {}), destination_idx: distance}
1107
- graph_dict[destination_idx] = {**graph_dict.get(destination_idx, {}), origin_idx: distance}
1108
- assert len(nodes_dict) == len(graph_dict), "All nodes must be included as origins in the graph dictionary"
1109
- nodes = [[i[1][1],i[1][0]] for i in sorted(nodes_dict.items(), key=lambda x: x[0])]
1205
+ graph_dict[origin_idx] = {
1206
+ **graph_dict.get(origin_idx, {}),
1207
+ destination_idx: distance,
1208
+ }
1209
+ graph_dict[destination_idx] = {
1210
+ **graph_dict.get(destination_idx, {}),
1211
+ origin_idx: distance,
1212
+ }
1213
+ assert len(nodes_dict) == len(
1214
+ graph_dict
1215
+ ), "All nodes must be included as origins in the graph dictionary"
1216
+ nodes = [
1217
+ [i[1][1], i[1][0]]
1218
+ for i in sorted(nodes_dict.items(), key=lambda x: x[0])
1219
+ ]
1110
1220
  ordered_graph_tuple = sorted(graph_dict.items(), key=lambda x: x[0])
1111
1221
  graph_map = {i[0]: idx for idx, i in enumerate(ordered_graph_tuple)}
1112
1222
  graph = [
1113
1223
  {graph_map[k]: v for k, v in i[1].items()} for i in ordered_graph_tuple
1114
1224
  ]
1115
- return GeoGraph(
1116
- graph=graph,
1117
- nodes=nodes
1118
- )
1225
+ return GeoGraph(graph=graph, nodes=nodes)
1226
+
1227
+
1228
+ def get_multi_path_geojson(
1229
+ routes: list[dict],
1230
+ filename: [str, None] = None,
1231
+ show_progress: bool = False,
1232
+ ) -> dict:
1233
+ """
1234
+ Creates a GeoJSON file with the shortest path between the origin and destination of each route.
1235
+
1236
+ Required Parameters:
1237
+
1238
+ - `routes`: list[dict]
1239
+ - List of dictionaries with the following keys:
1240
+ - geograph: GeoGraph
1241
+ - Geograph object to use for the shortest path calculation.
1242
+ - origin: dict[float|float]
1243
+ - Origin coordinates
1244
+ - EG: {"latitude":39.2904, "longitude":-76.6122}
1245
+ - destination: dict[float|int]
1246
+ - Destination coordinates
1247
+ - EG: {"latitude":39.2904, "longitude":-76.6122}
1248
+ - properties: dict
1249
+ - Dictionary with the properties of the route
1250
+ - Everything in this dictionary will be included in the output GeoJSON file as properties of the route.
1251
+ - EG: {"id":"route_1", "name":"Route 1", "color":"#ff0000"}
1252
+
1253
+ Optional Parameters:
1254
+
1255
+ - `filename`: str | None
1256
+ - Name of the output GeoJSON file.
1257
+ - If None, the function will not save the file
1258
+ - Default: None
1259
+ - `show_progress`: bool
1260
+ - Whether to show basic progress information
1261
+ - Default: False
1262
+
1263
+ Returns
1264
+
1265
+ - `output`: dict
1266
+ - Dictionary with the GeoJSON file content.
1267
+ """
1268
+ assert isinstance(routes, list), "Routes must be a list"
1269
+ assert all(
1270
+ [isinstance(i, dict) for i in routes]
1271
+ ), "Routes must be a list of dictionaries"
1272
+ assert all(
1273
+ [isinstance(i.get("geograph"), GeoGraph) for i in routes]
1274
+ ), "All routes must have a 'geograph' key with a GeoGraph object"
1275
+ assert all(
1276
+ [isinstance(i.get("origin"), dict) for i in routes]
1277
+ ), "All routes must have an 'origin' key with a dictionary"
1278
+ assert all(
1279
+ [isinstance(i.get("destination"), dict) for i in routes]
1280
+ ), "All routes must have a 'destination' key with a dictionary"
1281
+ assert all(
1282
+ [isinstance(i.get("properties"), dict) for i in routes]
1283
+ ), "All routes must have a 'properties' key with a dictionary"
1284
+ assert all(
1285
+ [isinstance(i["origin"].get("latitude"), (int, float)) for i in routes]
1286
+ ), "All origins must have a 'latitude' key with a number"
1287
+ assert all(
1288
+ [isinstance(i["origin"].get("longitude"), (int, float)) for i in routes]
1289
+ ), "All origins must have a 'longitude' key with a number"
1290
+ assert all(
1291
+ [
1292
+ isinstance(i["destination"].get("latitude"), (int, float))
1293
+ for i in routes
1294
+ ]
1295
+ ), "All destinations must have a 'latitude' key with a number"
1296
+ assert all(
1297
+ [
1298
+ isinstance(i["destination"].get("longitude"), (int, float))
1299
+ for i in routes
1300
+ ]
1301
+ ), "All destinations must have a 'longitude' key with a number"
1302
+ assert all(
1303
+ [
1304
+ (
1305
+ i["origin"].get("latitude") >= -90
1306
+ and i["origin"].get("latitude") <= 90
1307
+ )
1308
+ for i in routes
1309
+ ]
1310
+ ), "All origin latitudes must be between -90 and 90"
1311
+ assert all(
1312
+ [
1313
+ (
1314
+ i["origin"].get("longitude") >= -180
1315
+ and i["origin"].get("longitude") <= 180
1316
+ )
1317
+ for i in routes
1318
+ ]
1319
+ ), "All origin longitudes must be between -180 and 180"
1320
+ assert all(
1321
+ [
1322
+ (
1323
+ i["destination"].get("latitude") >= -90
1324
+ and i["destination"].get("latitude") <= 90
1325
+ )
1326
+ for i in routes
1327
+ ]
1328
+ ), "All destination latitudes must be between -90 and 90"
1329
+ assert all(
1330
+ [
1331
+ (
1332
+ i["destination"].get("longitude") >= -180
1333
+ and i["destination"].get("longitude") <= 180
1334
+ )
1335
+ for i in routes
1336
+ ]
1337
+ ), "All destination longitudes must be between -180 and 180"
1338
+
1339
+ output = {"type": "FeatureCollection", "features": []}
1340
+ len_routes = len(routes)
1341
+ for idx, route in enumerate(routes):
1342
+ shortest_path = route["geograph"].get_shortest_path(
1343
+ route["origin"], route["destination"]
1344
+ )
1345
+ shortest_line_path = get_line_path(shortest_path)
1346
+ output["features"].append(
1347
+ {
1348
+ "type": "Feature",
1349
+ "properties": route["properties"],
1350
+ "geometry": shortest_line_path,
1351
+ }
1352
+ )
1353
+ if show_progress:
1354
+ print(
1355
+ f"[{'='*(int((idx+1)/len_routes*20))}>{' '*(20-int((idx+1)/len_routes*20))}] {idx+1}/{len_routes}",
1356
+ end="\r",
1357
+ )
1358
+ if show_progress:
1359
+ print()
1360
+ if filename is not None:
1361
+ json.dump(output, open(filename, "w"))
1362
+ return output
@@ -2,10 +2,10 @@ import math, json
2
2
 
3
3
 
4
4
  def haversine(
5
- origin: list[float, int],
6
- destination: list[float, int],
5
+ origin: list[float|int],
6
+ destination: list[float|int],
7
7
  units: str = "km",
8
- circuity: [int, float] = 1,
8
+ circuity: [float|int] = 1,
9
9
  ):
10
10
  """
11
11
  Function:
@@ -69,7 +69,7 @@ def haversine(
69
69
  raise Exception()
70
70
 
71
71
 
72
- def hard_round(decimal_places: int, a: [int, float]):
72
+ def hard_round(decimal_places: int, a: [float|int]):
73
73
  """
74
74
  Function:
75
75
 
@@ -90,7 +90,7 @@ def hard_round(decimal_places: int, a: [int, float]):
90
90
 
91
91
 
92
92
  def distance_converter(
93
- distance: [int, float], input_units: str, output_units: str
93
+ distance: [float|int], input_units: str, output_units: str
94
94
  ):
95
95
  """
96
96
  Function:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scgraph
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Determine an approximate route between two points on earth.
5
5
  Author-email: Connor Makowski <conmak@mit.edu>
6
6
  Project-URL: Homepage, https://github.com/connor-makowski/scgraph
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://connor-makowski.github.io/scgraph/core.html
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: OS Independent
12
- Requires-Python: >=3.6
12
+ Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
 
@@ -57,7 +57,9 @@ Low Level: https://connor-makowski.github.io/scgraph/scgraph/core.html
57
57
 
58
58
  ## Setup
59
59
 
60
- Make sure you have Python 3.6.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/).
60
+ Make sure you have Python 3.10.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/).
61
+
62
+ Support for python3.6-3.9 is available up to version 2.2.0
61
63
 
62
64
  ## Installation
63
65
 
@@ -67,7 +69,10 @@ pip install scgraph
67
69
 
68
70
  ## Use with Google Colab
69
71
 
70
- See the example [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example.ipynb)
72
+ - [Getting Started](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/getting_started.ipynb)
73
+ - [Creating A Multi Path Geojson](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/multi_path_geojson.ipynb)
74
+ - [Modifying A Geograph](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/geograph_modifications.ipynb)
75
+
71
76
 
72
77
  # Getting Started
73
78
 
@@ -101,7 +106,7 @@ In the above example, the `output` variable is a dictionary with three keys: `le
101
106
  - `ft` (feet)
102
107
  - `coordinate_path`: A list of lists [`latitude`,`longitude`] that make up the shortest path
103
108
 
104
- For more examples including viewing the output on a map, see the [example notebook](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example.ipynb).
109
+ For more examples including viewing the output on a map, see the [example notebook](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/getting_started.ipynb).
105
110
 
106
111
  ## Included GeoGraphs
107
112
 
@@ -165,7 +170,7 @@ output = marnet_geograph.get_shortest_path(
165
170
  get_line_path(output, filename='output.geojson')
166
171
  ```
167
172
 
168
- Modify an existing geograph: See the notebook [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example_making_modifications.ipynb)
173
+ Modify an existing geograph: See the notebook [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/examples/geograph_modifications.ipynb)
169
174
 
170
175
 
171
176
  You can specify your own custom graphs for direct access to the solving algorithms. This requires the use of the low level `Graph` class
@@ -173,9 +178,9 @@ You can specify your own custom graphs for direct access to the solving algorith
173
178
  ```py
174
179
  from scgraph import Graph
175
180
 
176
- # Define a graph
181
+ # Define an arbitrary graph
177
182
  # See the graph definitions here:
178
- # https://connor-makowski.github.io/scgraph/scgraph/core.html
183
+ # https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
179
184
  graph = [
180
185
  {1: 5, 2: 1},
181
186
  {0: 5, 2: 2, 3: 1},
@@ -200,25 +205,67 @@ from scgraph import GeoGraph
200
205
 
201
206
  # Define nodes
202
207
  # See the nodes definitions here:
203
- # https://connor-makowski.github.io/scgraph/scgraph/core.html
208
+ # https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
204
209
  nodes = [
205
- [0,0],
206
- [0,1],
207
- [1,0],
208
- [1,1],
209
- [1,2],
210
- [2,1]
210
+ # London
211
+ [51.5074, -0.1278],
212
+ # Paris
213
+ [48.8566, 2.3522],
214
+ # Berlin
215
+ [52.5200, 13.4050],
216
+ # Rome
217
+ [41.9028, 12.4964],
218
+ # Madrid
219
+ [40.4168, -3.7038],
220
+ # Lisbon
221
+ [38.7223, -9.1393]
211
222
  ]
212
223
  # Define a graph
213
224
  # See the graph definitions here:
214
- # https://connor-makowski.github.io/scgraph/scgraph/core.html
225
+ # https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph
215
226
  graph = [
216
- {1: 5, 2: 1},
217
- {0: 5, 2: 2, 3: 1},
218
- {0: 1, 1: 2, 3: 4, 4: 8},
219
- {1: 1, 2: 4, 4: 3, 5: 6},
220
- {2: 8, 3: 3},
221
- {3: 6}
227
+ # From London
228
+ {
229
+ # To Paris
230
+ 1: 311,
231
+ },
232
+ # From Paris
233
+ {
234
+ # To London
235
+ 0: 311,
236
+ # To Berlin
237
+ 2: 878,
238
+ # To Rome
239
+ 3: 1439,
240
+ # To Madrid
241
+ 4: 1053
242
+ },
243
+ # From Berlin
244
+ {
245
+ # To Paris
246
+ 1: 878,
247
+ # To Rome
248
+ 3: 1181,
249
+ },
250
+ # From Rome
251
+ {
252
+ # To Paris
253
+ 1: 1439,
254
+ # To Berlin
255
+ 2: 1181,
256
+ },
257
+ # From Madrid
258
+ {
259
+ # To Paris
260
+ 1: 1053,
261
+ # To Lisbon
262
+ 5: 623
263
+ },
264
+ # From Lisbon
265
+ {
266
+ # To Madrid
267
+ 4: 623
268
+ }
222
269
  ]
223
270
 
224
271
  # Create a GeoGraph object
@@ -231,23 +278,31 @@ my_geograph.validate_graph()
231
278
  my_geograph.validate_nodes()
232
279
 
233
280
  # Get the shortest path between two points
281
+ # In this case, Birmingham England and Zaragoza Spain
282
+ # Since Birmingham and Zaragoza are not in the graph,
283
+ # the algorithm will add them into the graph.
284
+ # See: https://connor-makowski.github.io/scgraph/scgraph/core.html#GeoGraph.get_shortest_path
285
+ # Expected output would be to go from
286
+ # Birmingham -> London -> Paris -> Madrid -> Zaragoza
287
+
234
288
  output = my_geograph.get_shortest_path(
235
- origin_node = {'latitude': 0, 'longitude': 0},
236
- destination_node = {'latitude': 2, 'longitude': 1}
289
+ # Birmingham England
290
+ origin_node = {'latitude': 52.4862, 'longitude': -1.8904},
291
+ # Zaragoza Spain
292
+ destination_node = {'latitude': 41.6488, 'longitude': -0.8891}
237
293
  )
238
- #=>
294
+ print(output)
239
295
  # {
240
- # "coordinate_path": [
241
- # [0,0],
242
- # [0,0],
243
- # [1,0],
244
- # [0,1],
245
- # [1,1],
246
- # [2,1],
247
- # [2,1]
248
- # ],
249
- # "length": 10
296
+ # 'length': 1799.4323,
297
+ # 'coordinate_path': [
298
+ # [52.4862, -1.8904],
299
+ # [51.5074, -0.1278],
300
+ # [48.8566, 2.3522],
301
+ # [40.4168, -3.7038],
302
+ # [41.6488, -0.8891]
303
+ # ]
250
304
  # }
305
+
251
306
  ```
252
307
 
253
308
  ## Attributions and Thanks
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = scgraph
3
- version = 2.2.0
3
+ version = 2.3.0
4
4
  description_file = README.md
5
5
 
6
6
  [options]
File without changes
File without changes