scgraph 2.1.1__tar.gz → 2.2.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.1.1
3
+ Version: 2.2.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
@@ -165,6 +165,8 @@ output = marnet_geograph.get_shortest_path(
165
165
  get_line_path(output, filename='output.geojson')
166
166
  ```
167
167
 
168
+ Modify an existing geograph: See the notebook [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example_making_modifications.ipynb)
169
+
168
170
 
169
171
  You can specify your own custom graphs for direct access to the solving algorithms. This requires the use of the low level `Graph` class
170
172
 
@@ -150,6 +150,8 @@ output = marnet_geograph.get_shortest_path(
150
150
  get_line_path(output, filename='output.geojson')
151
151
  ```
152
152
 
153
+ Modify an existing geograph: See the notebook [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example_making_modifications.ipynb)
154
+
153
155
 
154
156
  You can specify your own custom graphs for direct access to the solving algorithms. This requires the use of the low level `Graph` class
155
157
 
@@ -1,6 +1,6 @@
1
1
  [tool.black]
2
2
  line-length = 80
3
- target-version = ['py39']
3
+ target-version = ['py312']
4
4
  exclude = '/.*(migrations|__pycache__|geographs).*/'
5
5
 
6
6
  [tool.setuptools]
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
12
12
 
13
13
  [project]
14
14
  name = "scgraph"
15
- version = "2.1.1"
15
+ version = "2.2.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"}
@@ -315,9 +315,6 @@ class GeoGraph:
315
315
  - Type: list of lists
316
316
  - What: A list of lists where the values are coordinates (latitude then longitude)
317
317
  - Note: The length of the nodes list must be the same as that of the graph list
318
-
319
- Optional Arguments:
320
-
321
318
  """
322
319
  self.graph = graph
323
320
  self.nodes = nodes
@@ -727,7 +724,7 @@ class GeoGraph:
727
724
  if len(nodes) == 0:
728
725
  # Default to all if the lat_lon_bound fails to find any nodes
729
726
  return self.get_node_distances(
730
- node=nodes,
727
+ node=node,
731
728
  circuity=circuity,
732
729
  lat_lon_bound=180,
733
730
  node_addition_type=node_addition_type,
@@ -843,7 +840,6 @@ class GeoGraph:
843
840
  lat_lon_bound, (int, float)
844
841
  ), "Lat_lon_bound must be a number"
845
842
  assert lat_lon_bound > 0, "Lat_lon_bound must be greater than 0"
846
-
847
843
  node = [node["latitude"], node["longitude"]]
848
844
  # Get the distances to all other nodes
849
845
  distances = self.get_node_distances(
@@ -878,13 +874,13 @@ class GeoGraph:
878
874
 
879
875
  """
880
876
  features = []
881
- for origin_idx, destinations in self.graph.items():
877
+ for origin_idx, destinations in enumerate(self.graph):
882
878
  for destination_idx, distance in destinations.items():
883
879
  # Create an undirected graph for geojson purposes
884
880
  if origin_idx > destination_idx:
885
881
  continue
886
- origin = self.nodes.get(origin_idx)
887
- destination = self.nodes.get(destination_idx)
882
+ origin = self.nodes[origin_idx]
883
+ destination = self.nodes[destination_idx]
888
884
  features.append(
889
885
  {
890
886
  "type": "Feature",
@@ -896,10 +892,10 @@ class GeoGraph:
896
892
  "geometry": {
897
893
  "type": "LineString",
898
894
  "coordinates": [
899
- [origin["longitude"], origin["latitude"]],
895
+ [origin[1], origin[0]],
900
896
  [
901
- destination["longitude"],
902
- destination["latitude"],
897
+ destination[1],
898
+ destination[0],
903
899
  ],
904
900
  ],
905
901
  },
@@ -909,3 +905,214 @@ class GeoGraph:
909
905
  out_dict = {"type": "FeatureCollection", "features": features}
910
906
  with open(filename, "w") as f:
911
907
  json.dump(out_dict, f)
908
+
909
+ def save_as_geograph(self, name: str) -> None:
910
+ """
911
+ Function:
912
+
913
+ - Save the current geograph as an importable python file
914
+
915
+ Required Arguments:
916
+
917
+ - `name`
918
+ - Type: str
919
+ - What: The name of the geograph and file
920
+ - EG: 'custom'
921
+ - Stored as: 'custom.py'
922
+ - In your current directory
923
+ - Import as: 'from .custom import custom_geograph'
924
+ """
925
+ self.validate_nodes()
926
+ self.validate_graph(
927
+ check_symmetry=True, check_connected=False
928
+ )
929
+ 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:
931
+ f.write(out_string)
932
+
933
+ def mod_remove_arc(self, origin_idx: int, destination_idx: int, undirected: bool = True) -> None:
934
+ """
935
+ Function:
936
+
937
+ - Remove an arc from the graph
938
+
939
+ Required Arguments:
940
+
941
+ - `origin_idx`
942
+ - Type: int
943
+ - What: The index of the origin node
944
+ - `destination_idx`
945
+ - Type: int
946
+ - What: The index of the destination node
947
+
948
+ Optional Arguments:
949
+
950
+ - `undirected`
951
+ - Type: bool
952
+ - What: Whether to remove the arc in both directions
953
+ - Default: True
954
+ """
955
+ assert origin_idx < len(self.graph), "Origin node does not exist"
956
+ assert destination_idx < len(self.graph), "Destination node does not exist"
957
+ assert destination_idx in self.graph[origin_idx], "Arc does not exist"
958
+ del self.graph[origin_idx][destination_idx]
959
+ if undirected:
960
+ if origin_idx in self.graph[destination_idx]:
961
+ del self.graph[destination_idx][origin_idx]
962
+
963
+ def mod_add_node(self, latitude: [float, int], longitude: [float, int]) -> int:
964
+ """
965
+ Function:
966
+
967
+ - Add a node to the graph
968
+
969
+ Required Arguments:
970
+
971
+ - `latitude`
972
+ - Type: int | float
973
+ - What: The latitude of the node
974
+ - `longitude`
975
+ - Type: int | float
976
+ - What: The longitude of the node
977
+
978
+ Returns:
979
+
980
+ - The index of the new node
981
+ """
982
+ self.nodes.append([latitude, longitude])
983
+ self.graph.append({})
984
+ 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:
987
+ """
988
+ Function:
989
+
990
+ - Add an arc to the graph
991
+
992
+ Required Arguments:
993
+
994
+ - `origin_idx`
995
+ - Type: int
996
+ - What: The index of the origin node
997
+ - `destination_idx`
998
+ - Type: int
999
+ - What: The index of the destination node
1000
+
1001
+ Optional Arguments:
1002
+
1003
+ - `distance`
1004
+ - Type: int | float
1005
+ - What: The distance between the origin and destination nodes in terms of the graph distance (normally km)
1006
+ - Default: 0
1007
+ - `use_haversine_distance`
1008
+ - Type: bool
1009
+ - What: Whether to calculate the haversine distance (km) between the nodes when calculating the distance
1010
+ - Default: True
1011
+ - Note: If true, overrides the `distance` argument
1012
+ - `undirected`
1013
+ - Type: bool
1014
+ - What: Whether to add the arc in both directions
1015
+ - Default: True
1016
+ """
1017
+ assert origin_idx < len(self.graph), "Origin node does not exist"
1018
+ assert destination_idx < len(self.graph), "Destination node does not exist"
1019
+ if use_haversine_distance:
1020
+ distance = haversine(self.nodes[origin_idx], self.nodes[destination_idx])
1021
+ self.graph[origin_idx][destination_idx] = distance
1022
+ if undirected:
1023
+ self.graph[destination_idx][origin_idx] = distance
1024
+
1025
+ def load_geojson_as_geograph(geojson_filename: str) -> GeoGraph:
1026
+ """
1027
+ Function:
1028
+
1029
+ - Create a CustomGeoGraph object loaded from a geojson file
1030
+
1031
+ Required Arguments:
1032
+
1033
+ - `geojson_filename`
1034
+ - Type: str
1035
+ - What: The filename of the geojson file to load
1036
+ - Note: All arcs read in will be undirected
1037
+ - Note: This geojson file must be formatted in a specific way
1038
+ - The geojson file must be a FeatureCollection
1039
+ - Each feature must be a LineString with two coordinate pairs
1040
+ - The first coordinate pair must be the origin node
1041
+ - The second coordinate pair must be the destination node
1042
+ - The properties of the feature must include the distance between the origin and destination nodes
1043
+ - The properties of the feature must include the origin and destination node idxs
1044
+ - Origin and destination node idxs must be integers between 0 and n-1 where n is the number of nodes in the graph
1045
+ - EG:
1046
+ ```
1047
+ {
1048
+ "type": "FeatureCollection",
1049
+ "features": [
1050
+ {
1051
+ "type": "Feature",
1052
+ "properties": {
1053
+ "origin_idx": 0,
1054
+ "destination_idx": 1,
1055
+ "distance": 10
1056
+ },
1057
+ "geometry": {
1058
+ "type": "LineString",
1059
+ "coordinates": [
1060
+ [121.47, 31.23],
1061
+ [121.48, 31.24]
1062
+ ]
1063
+ }
1064
+ }
1065
+ ]
1066
+ }
1067
+ ```
1068
+ """
1069
+ with open (geojson_filename, "r") as f:
1070
+ geojson_features = json.load(f).get("features", [])
1071
+
1072
+ nodes_dict = {}
1073
+ graph_dict = {}
1074
+ for feature in geojson_features:
1075
+ properties = feature.get("properties", {})
1076
+ origin_idx = properties.get("origin_idx")
1077
+ destination_idx = properties.get("destination_idx")
1078
+ distance = properties.get("distance")
1079
+ geometry = feature.get("geometry", {})
1080
+ coordinates = geometry.get("coordinates", [])
1081
+
1082
+ # 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"
1091
+ assert distance >= 0, "All distances must be greater than or equal to 0"
1092
+ origin = coordinates[0]
1093
+ destination = coordinates[1]
1094
+ assert isinstance(origin, list), "All coordinates must be lists"
1095
+ assert isinstance(destination, list), "All coordinates must be lists"
1096
+ assert len(origin) == 2, "All coordinates must have a length of 2"
1097
+ 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"
1100
+ # assert all([origin[0] >= -90, origin[0] <= 90, origin[1] >= -180, origin[1] <= 180]), "All coordinates must be valid latitudes and longitudes"
1101
+ # assert all([destination[0] >= -90, destination[0] <= 90, destination[1] >= -180, destination[1] <= 180]), "All coordinates must be valid latitudes and longitudes"
1102
+
1103
+ # Update the data
1104
+ nodes_dict[origin_idx] = origin
1105
+ 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])]
1110
+ ordered_graph_tuple = sorted(graph_dict.items(), key=lambda x: x[0])
1111
+ graph_map = {i[0]: idx for idx, i in enumerate(ordered_graph_tuple)}
1112
+ graph = [
1113
+ {graph_map[k]: v for k, v in i[1].items()} for i in ordered_graph_tuple
1114
+ ]
1115
+ return GeoGraph(
1116
+ graph=graph,
1117
+ nodes=nodes
1118
+ )
@@ -16,10 +16,10 @@ def haversine(
16
16
 
17
17
  - `origin`:
18
18
  - Type: list of two floats | ints
19
- - What: The origin point as a list of "longitude" and "latitude"
19
+ - What: The origin point as a list of "latitude" and "longitude"
20
20
  - `destination`:
21
21
  - Type: list of two floats | ints
22
- - What: The destination point as a list of "longitude" and "latitude"
22
+ - What: The destination point as a list of "latitude" and "longitude"
23
23
 
24
24
  Optional Arguments:
25
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scgraph
3
- Version: 2.1.1
3
+ Version: 2.2.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
@@ -165,6 +165,8 @@ output = marnet_geograph.get_shortest_path(
165
165
  get_line_path(output, filename='output.geojson')
166
166
  ```
167
167
 
168
+ Modify an existing geograph: See the notebook [here](https://colab.research.google.com/github/connor-makowski/scgraph/blob/main/example_making_modifications.ipynb)
169
+
168
170
 
169
171
  You can specify your own custom graphs for direct access to the solving algorithms. This requires the use of the low level `Graph` class
170
172
 
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = scgraph
3
- version = 2.1.1
3
+ version = 2.2.0
4
4
  description_file = README.md
5
5
 
6
6
  [options]
File without changes
File without changes