NREL-erad 0.1.0__py3-none-any.whl → 1.0.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.
Files changed (72) hide show
  1. erad/__init__.py +1 -1
  2. erad/constants.py +20 -68
  3. erad/cypher_queries/load_data_v1.cypher +212 -0
  4. erad/data/World_Earthquakes_1960_2016.csv +23410 -0
  5. erad/db/__init__.py +0 -0
  6. erad/db/assets/__init__.py +0 -0
  7. erad/db/assets/critical_infras.py +171 -0
  8. erad/db/assets/distribution_lines.py +101 -0
  9. erad/db/credential_model.py +20 -0
  10. erad/db/disaster_input_model.py +23 -0
  11. erad/db/inject_earthquake.py +52 -0
  12. erad/db/inject_flooding.py +53 -0
  13. erad/db/neo4j_.py +162 -0
  14. erad/db/utils.py +14 -0
  15. erad/exceptions.py +68 -0
  16. erad/metrics/__init__.py +0 -0
  17. erad/metrics/check_microgrid.py +208 -0
  18. erad/metrics/metric.py +178 -0
  19. erad/programs/__init__.py +0 -0
  20. erad/programs/backup.py +62 -0
  21. erad/programs/microgrid.py +45 -0
  22. erad/scenarios/__init__.py +0 -0
  23. erad/scenarios/abstract_scenario.py +103 -0
  24. erad/scenarios/common.py +93 -0
  25. erad/scenarios/earthquake_scenario.py +161 -0
  26. erad/scenarios/fire_scenario.py +160 -0
  27. erad/scenarios/flood_scenario.py +494 -0
  28. erad/scenarios/flows.csv +671 -0
  29. erad/scenarios/utilities.py +76 -0
  30. erad/scenarios/wind_scenario.py +89 -0
  31. erad/utils/__init__.py +0 -0
  32. erad/utils/ditto_utils.py +252 -0
  33. erad/utils/hifld_utils.py +147 -0
  34. erad/utils/opendss_utils.py +357 -0
  35. erad/utils/overpass.py +76 -0
  36. erad/utils/util.py +178 -0
  37. erad/visualization/__init__.py +0 -0
  38. erad/visualization/plot_graph.py +218 -0
  39. {nrel_erad-0.1.0.dist-info → nrel_erad-1.0.0.dist-info}/METADATA +39 -29
  40. nrel_erad-1.0.0.dist-info/RECORD +42 -0
  41. {nrel_erad-0.1.0.dist-info → nrel_erad-1.0.0.dist-info}/WHEEL +1 -2
  42. {nrel_erad-0.1.0.dist-info → nrel_erad-1.0.0.dist-info}/licenses/LICENSE.txt +28 -28
  43. erad/default_fragility_curves/__init__.py +0 -15
  44. erad/default_fragility_curves/default_fire_boundary_dist.py +0 -94
  45. erad/default_fragility_curves/default_flood_depth.py +0 -108
  46. erad/default_fragility_curves/default_flood_velocity.py +0 -101
  47. erad/default_fragility_curves/default_fragility_curves.py +0 -23
  48. erad/default_fragility_curves/default_peak_ground_acceleration.py +0 -163
  49. erad/default_fragility_curves/default_peak_ground_velocity.py +0 -94
  50. erad/default_fragility_curves/default_wind_speed.py +0 -94
  51. erad/enums.py +0 -40
  52. erad/gdm_mapping.py +0 -83
  53. erad/models/__init__.py +0 -1
  54. erad/models/asset.py +0 -287
  55. erad/models/asset_mapping.py +0 -20
  56. erad/models/fragility_curve.py +0 -116
  57. erad/models/hazard/__init__.py +0 -5
  58. erad/models/hazard/base_models.py +0 -12
  59. erad/models/hazard/common.py +0 -26
  60. erad/models/hazard/earthquake.py +0 -93
  61. erad/models/hazard/flood.py +0 -83
  62. erad/models/hazard/wild_fire.py +0 -121
  63. erad/models/hazard/wind.py +0 -143
  64. erad/models/probability.py +0 -73
  65. erad/probability_builder.py +0 -35
  66. erad/quantities.py +0 -25
  67. erad/runner.py +0 -122
  68. erad/systems/__init__.py +0 -2
  69. erad/systems/asset_system.py +0 -414
  70. erad/systems/hazard_system.py +0 -122
  71. nrel_erad-0.1.0.dist-info/RECORD +0 -35
  72. nrel_erad-0.1.0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,76 @@
1
+ from shapely.geometry import MultiPolygon, Point, LineString
2
+ from shapely.ops import nearest_points
3
+ import matplotlib.pyplot as plt
4
+ import scipy.stats as stats
5
+ import geopy.distance
6
+ import numpy as np
7
+ import stateplane
8
+
9
+
10
+ class GeoUtilities:
11
+
12
+ @property
13
+ def identify_stateplane_projection(self) -> str:
14
+ """ Automatically identifies stateplane projection ID """
15
+ x = self.centroid.x
16
+ y = self.centroid.y
17
+ return stateplane.identify(x, y)
18
+
19
+ def in_polygon(self, point : Point) -> bool:
20
+ return self.multipolygon.contains(point)
21
+
22
+ def distance_from_boundary(self, point : Point) -> float:
23
+ """ Calculates distance of a point to polygon boundary. Correct calculations require conversion to cartesian coordinates"""
24
+ if self.multipolygon.contains(point):
25
+ p1, p2 = nearest_points(self.boundary, point)
26
+ else:
27
+ p1, p2 = nearest_points(self.multipolygon, point)
28
+ coords_1 = (p1.y, p1.x)
29
+ coords_2 = (p2.y, p2.x)
30
+ return geopy.distance.geodesic(coords_1, coords_2).km
31
+
32
+ def distance_from_centroid(self, point : Point):
33
+ """ Calculates distance of a point to polygon centroid. Correct calculations require conversion to cartesian coordinates """
34
+ coords_1 = (self.centroid.y, self.centroid.x)
35
+ coords_2 = (point.y, point.x)
36
+ return geopy.distance.geodesic(coords_1, coords_2).km
37
+
38
+
39
+
40
+ class ProbabilityFunctionBuilder:
41
+ """Class containing utility fuctions for sceario definations."""
42
+
43
+
44
+ def __init__(self, dist, params):
45
+ """Constructor for BaseScenario class.
46
+
47
+ Args:
48
+ dist (str): Name of teh distribution. Should follow Scipy naming convention
49
+ params (list): A list of parameters for the chosen distribution function. See Scipy.stats documentation
50
+ """
51
+
52
+ self.dist = getattr(stats, dist)
53
+ self.params = params
54
+ return
55
+
56
+ def sample(self):
57
+ """Sample the distribution """
58
+ return self.dist.rvs(*self.params, size=1)[0]
59
+
60
+ def plot_cdf(self, x:np.linspace, ax =None, label="") -> None:
61
+ """Plot the cumalative distribution fuction"""
62
+ cdf = self.dist.cdf
63
+ if ax is None:
64
+ plt.plot(x,cdf(x, *self.params), label=label)
65
+ else:
66
+ ax.plot(x,cdf(x, *self.params), label=label)
67
+
68
+
69
+ def probability(self, value: float) -> float:
70
+ """Calculates survival probability of a given asset.
71
+
72
+ Args:
73
+ value (float): value for vetor of interest. Will change with scenarions
74
+ """
75
+ cdf = self.dist.cdf
76
+ return cdf(value, *self.params)
@@ -0,0 +1,89 @@
1
+ from erad.constants import FIRE_HISTORIC_GEODATAFRAME_PATH, DATA_FOLDER
2
+ from shapely.geometry import MultiPolygon, Point, LineString
3
+ from erad.scenarios.utilities import ProbabilityFunctionBuilder
4
+ from erad.scenarios.abstract_scenario import BaseScenario
5
+ from erad.exceptions import FeatureNotImplementedError
6
+ from erad.scenarios.utilities import GeoUtilities
7
+ import matplotlib.pyplot as plt
8
+ from datetime import datetime
9
+ import geopandas as gpd
10
+ import numpy as np
11
+ import random
12
+ import pyproj
13
+ import os
14
+
15
+ from erad.scenarios.common import AssetTypes
16
+ from erad.scenarios.utilities import ProbabilityFunctionBuilder
17
+
18
+
19
+ class WindScenario(BaseScenario, GeoUtilities):
20
+ """Base class for FireScenario. Extends BaseScenario and GeoUtilities
21
+
22
+ Attributes:
23
+ multipolygon (MultiPolygon): MultiPolygon enclosing wild fire regions
24
+ probability_model (dict): Dictionary mapping asset types to probability funcitons
25
+ timestamp (datetime): Scenario occurance time
26
+ """
27
+
28
+ fragility_curves = {
29
+ #Extending energy system modelling to include extreme weather risks and application to hurricane events in Puerto Rico
30
+ AssetTypes.substation.name : ProbabilityFunctionBuilder("lognorm", [0.8, 10, 5]),
31
+ AssetTypes.solar_panels.name : ProbabilityFunctionBuilder("lognorm", [0.8, 10, 5]),
32
+ AssetTypes.buried_lines.name : ProbabilityFunctionBuilder("lognorm", [0.8, 10, 5]),
33
+ AssetTypes.wind_turbines.name : ProbabilityFunctionBuilder("norm", [0.8, 10, 5]),
34
+ #AssetTypes.battery_storage.name : ProbabilityFunctionBuilder("lognorm", [0.8, 10, 5]),
35
+ #AssetTypes.transmission_poles.name : ProbabilityFunctionBuilder("lognorm", [0.8, 10, 5]),
36
+ AssetTypes.distribution_poles.name : ProbabilityFunctionBuilder("lognorm", [0.8, 10, 5]),
37
+ # AssetTypes.transmission_overhead_lines.name : ProbabilityFunctionBuilder("lognorm", [0.8, 10, 5]),
38
+ AssetTypes.distribution_overhead_lines.name : ProbabilityFunctionBuilder("beta", [0.8, 10, 5]),
39
+ }
40
+
41
+ def __init__(self, multipolygon : MultiPolygon , probability_model : dict, timestamp : datetime) -> None:
42
+ """Constructor for FireScenario.
43
+
44
+ Args:
45
+ multipolygon (MultiPolygon): MultiPolygon enclosing wild fire regions
46
+ probability_model (dict): Dictionary mapping asset types to probability funcitons
47
+ timestamp (datetime): Scenario occurance time
48
+ """
49
+
50
+ super(WindScenario, self).__init__(multipolygon, probability_model, timestamp)
51
+ return
52
+
53
+ @property
54
+ def area(self) -> float:
55
+ """Method to calculate area of affected region."""
56
+ geod = pyproj.Geod(ellps="WGS84")
57
+ area = abs(geod.geometry_area_perimeter(self.polygon)[0])
58
+ return area
59
+
60
+ @property
61
+ def polygon(self) -> MultiPolygon:
62
+ """Method to return polygon for the affected region."""
63
+ return self.multipolygon
64
+
65
+ @property
66
+ def boundary(self) -> LineString:
67
+ """Method to return boundary for the affected region."""
68
+ return self.multipolygon.boundary
69
+
70
+ @property
71
+ def centroid(self) -> Point:
72
+ """Method to return the centroid of the affected region."""
73
+ return self.polygon.centroid
74
+
75
+ def increment_time(self):
76
+ """Method to increment simulation time for time evolviong scenarios."""
77
+ raise FeatureNotImplementedError()
78
+
79
+ def calculate_survival_probability(self, assets : dict, timestamp : datetime, plot: bool) -> dict:
80
+ """Method to calculate survival probaility of asset types.
81
+
82
+ Args:
83
+ assets (dict): The dictionary of all assets and their corresponding asset types
84
+ plot (bool): Set to true to plot the fire survival model
85
+ """
86
+ return assets
87
+
88
+
89
+
erad/utils/__init__.py ADDED
File without changes
@@ -0,0 +1,252 @@
1
+ """ Utility functions for dealing with SMART DS dataset.
2
+
3
+ Examples:
4
+
5
+ >>> from erad import ditto_utils
6
+ >>> ditto_utils.download_smartds_data('P4R', '.')
7
+
8
+ """
9
+
10
+ # standard libraries
11
+ from pathlib import Path
12
+ import shutil
13
+ import logging
14
+ from typing import List
15
+
16
+ # third-party libraries
17
+ import boto3
18
+ from botocore import UNSIGNED
19
+ from botocore.config import Config
20
+ from ditto.store import Store
21
+ from ditto.readers.opendss.read import Reader
22
+ from ditto.network.network import Network
23
+ from ditto.models.power_source import PowerSource
24
+ import networkx as nx
25
+ from networkx.readwrite import json_graph
26
+
27
+
28
+ # internal libraries
29
+ from erad.constants import SMARTDS_VALID_AREAS, SMARTDS_VALID_YEARS
30
+ from erad.exceptions import SMARTDSInvalidInput, DittoException
31
+ from erad.utils.util import timeit, write_file, path_validation
32
+ from erad.utils.util import read_file, setup_logging
33
+
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ @timeit
39
+ def download_aws_dir(
40
+ bucket: str, path: str, target: str, unsigned=True, **kwargs
41
+ ) -> None:
42
+ """Utility function download data from AWS S3 directory.
43
+
44
+ Args:
45
+ bucket (str): Name of the bucket.
46
+ path (str): S3 bucket prefix
47
+ target (str): Path for downloading the data
48
+ unsigned (bool): Indicate whether to use credential or not
49
+ kwargs (dict): Keyword arguments accepted by `boto3.client`
50
+ """
51
+
52
+ target = Path(target)
53
+ if unsigned:
54
+ client = boto3.client("s3", config=Config(signature_version=UNSIGNED))
55
+ else:
56
+ if kwargs:
57
+ client = boto3.client("s3", **kwargs)
58
+ else:
59
+ client = boto3.client("s3")
60
+
61
+ # Handle missing / at end of prefix
62
+ if not path.endswith("/"):
63
+ path += "/"
64
+
65
+ paginator = client.get_paginator("list_objects_v2")
66
+ for result in paginator.paginate(Bucket=bucket, Prefix=path):
67
+
68
+ # Download each file individually
69
+ for key in result["Contents"]:
70
+
71
+ # Calculate relative path
72
+ rel_path = key["Key"][len(path) :]
73
+
74
+ # Skip paths ending in /
75
+ if not key["Key"].endswith("/"):
76
+ local_file_path = target / rel_path
77
+ local_file_path.parent.mkdir(parents=True, exist_ok=True)
78
+ client.download_file(bucket, key["Key"], str(local_file_path))
79
+
80
+
81
+ @timeit
82
+ def download_smartds_data(
83
+ smartds_region: str,
84
+ output_path: str = "./smart_ds_downloads",
85
+ year: int = 2018,
86
+ area: str = "SFO",
87
+ s3_bucket_name: str = "oedi-data-lake",
88
+ folder_name: str = "opendss_no_loadshapes",
89
+ cache_folder: str = "cache",
90
+ ) -> str:
91
+ """Utility function to download SMARTDS data from AWS S3 bucket.
92
+
93
+ Args:
94
+ smartds_region (str): SMARTDS region name
95
+ output_path (str): Path for downloaded data
96
+ year (int): Valid year input for downloading the data
97
+ area (str): Valid SMARTDS area
98
+ s3_bucket_name (str): S3 bucket name storing the SMARTDS data
99
+ folder_name (str): S3 bucket folder to download
100
+ cache_folder (str): Folder path for caching the results
101
+
102
+ Raises:
103
+ SMARTDSInvalidInput: Raises this error if year and/or area
104
+ provided is not valid.
105
+
106
+ Returns:
107
+ str: Folder path containing downloaded data.
108
+ """
109
+ if year not in SMARTDS_VALID_YEARS or area not in SMARTDS_VALID_AREAS:
110
+ raise SMARTDSInvalidInput(
111
+ f"Not valid input! year= {year} area={area}, \
112
+ valid_years={SMARTDS_VALID_YEARS}, valid_areas={SMARTDS_VALID_AREAS}"
113
+ )
114
+
115
+ output_path = Path(output_path)
116
+ cache_folder = Path(cache_folder)
117
+
118
+ output_path.mkdir(exist_ok=True)
119
+ cache_folder.mkdir(exist_ok=True)
120
+
121
+ cache_key = (
122
+ f"{smartds_region}__{year}__{area}__{s3_bucket_name}_{folder_name}"
123
+ )
124
+ cache_data_folder = cache_folder / cache_key
125
+ output_folder = output_path / cache_key
126
+
127
+ if cache_data_folder.exists():
128
+ logger.info(f"Cache hit for {cache_data_folder}")
129
+ shutil.copytree(cache_data_folder, output_folder, dirs_exist_ok=True)
130
+
131
+ else:
132
+ logger.info(
133
+ f"Cache missed reaching to AWS for downloading the data ..."
134
+ )
135
+ output_folder.mkdir(exist_ok=True)
136
+ prefix = f"SMART-DS/v1.0/{year}/{area}/{smartds_region}/scenarios/base_timeseries/{folder_name}/"
137
+ download_aws_dir(s3_bucket_name, prefix, output_folder)
138
+ shutil.copytree(output_folder, cache_data_folder, dirs_exist_ok=False)
139
+
140
+ logger.info(f"Check the folder {output_folder} for downloaded data")
141
+ return output_folder
142
+
143
+
144
+ @timeit
145
+ def _create_networkx_from_ditto(
146
+ output_path: str, file_name: str, **kwargs
147
+ ) -> List:
148
+ """Creates networkx graph from OpenDSS model using Ditto.
149
+
150
+ Args:
151
+ output_path (str): Path to store the networkx
152
+ data in json file format
153
+ file_name (str): JSON file name used to export
154
+ the network
155
+ kwargs (dict): Keyword arguments accepted
156
+ by Ditto
157
+
158
+ Raises:
159
+ DittoException: Raises if multiple sources are found.
160
+
161
+
162
+ Returns:
163
+ List: Pair of networkx graph and
164
+ path containing JSON file
165
+ """
166
+ file_name = Path(file_name).stem
167
+ logger.debug(
168
+ "Attempting to create NetworkX representation from OpenDSS \
169
+ files using DiTTo"
170
+ )
171
+
172
+ path_validation(output_path)
173
+
174
+ store = Store()
175
+ reader = Reader(
176
+ master_file=kwargs["master_file"],
177
+ buscoordinates_file=kwargs["buscoordinates_file"],
178
+ coordinates_delimiter=kwargs["coordinates_delimiter"],
179
+ )
180
+ reader.parse(store)
181
+
182
+ all_sources = []
183
+ for i in store.models:
184
+ if isinstance(i, PowerSource) and i.connecting_element is not None:
185
+ all_sources.append(i)
186
+ elif isinstance(i, PowerSource):
187
+ print(
188
+ "Warning - a PowerSource element has a None connecting element"
189
+ )
190
+
191
+ if len(all_sources) > 1:
192
+ raise DittoException(
193
+ f"This feeder has lots of sources {len(all_sources)}"
194
+ )
195
+
196
+ ditto_graph = Network()
197
+ ditto_graph.build(store, all_sources[0].connecting_element)
198
+ ditto_graph.set_attributes(store)
199
+
200
+ data = dict(ditto_graph.graph.nodes.data())
201
+ data_new = {}
202
+ for node, node_data in data.items():
203
+ try:
204
+ data_new[node] = node_data["positions"][0]._trait_values
205
+ except Exception as e:
206
+ connecting_node = node_data["connecting_element"]
207
+ data_new[node] = data[connecting_node]["positions"][0]._trait_values
208
+
209
+ adj_file = file_name + ".adjlist"
210
+ nx.write_adjlist(ditto_graph.graph, output_path / adj_file)
211
+ g = nx.read_adjlist(output_path / adj_file)
212
+ nx.set_node_attributes(g, data_new)
213
+
214
+ data = json_graph.adjacency_data(g)
215
+ json_file = file_name + ".json"
216
+ output_file = output_path / json_file
217
+ write_file(data, output_file)
218
+
219
+ logger.debug(
220
+ f"Successfully created json file representing the network \
221
+ check the file {output_file}"
222
+ )
223
+
224
+ return (g, output_file)
225
+
226
+
227
+ def create_networkx_from_ditto(
228
+ output_path: str, file_name: str, **kwargs
229
+ ) -> None:
230
+ """Creates networkx graph from OpenDSS model using Ditto.
231
+
232
+ Args:
233
+ output_path (str): Path to store the networkx
234
+ data in json file format
235
+ file_name (str): JSON file name used to export
236
+ the network
237
+ kwargs (dict): Keyword arguments accepted
238
+ by Ditto
239
+ """
240
+ try:
241
+ output_path = Path(output_path)
242
+ return _create_networkx_from_ditto(output_path, file_name, **kwargs)
243
+ finally:
244
+ for file_path in output_path.iterdir():
245
+ if file_path.suffix == ".adjlist":
246
+ file_path.unlink(missing_ok=True)
247
+
248
+
249
+ def create_networkx_from_json(json_file_path: str):
250
+ """Returns networkx graph from JSON file."""
251
+ content = read_file(json_file_path)
252
+ return json_graph.adjacency_graph(content)
@@ -0,0 +1,147 @@
1
+ """ Module for parsing Homeland infrastructure foundation level-data.
2
+
3
+ Idea is to take the bounding box and find the subset of
4
+ infrastructure in that region.
5
+ """
6
+ # standard imports
7
+ from pathlib import Path
8
+ import math
9
+ from typing import Union, List
10
+
11
+ # third-party imports
12
+ import pandas as pd
13
+ import stateplane
14
+
15
+ # internal imports
16
+ from erad.utils.util import path_validation
17
+
18
+
19
+ def get_subset_of_hifld_data(
20
+ csv_file: str,
21
+ bounds: List,
22
+ output_folder: str,
23
+ logitude_column_name: str = "X",
24
+ latitude_column_name: str = "Y",
25
+ columns_to_keep: List[str] = ["X", "Y"],
26
+ name_of_csv_file: Union[str, None] = None,
27
+ ) -> None:
28
+ """Extracts a subset of HIFLD data set.
29
+
30
+ Args:
31
+ csv_file (str): Path to HIFLD data csv file
32
+ bounds (List): Bounding box coordinates
33
+ output_folder (str): Path to output folder
34
+ logitude_column_name (str): Expects column with name 'X'
35
+ latitude_column_name (str): Expects column with name 'Y'
36
+ columns_to_keep (List): List of column names to keep
37
+ by default keeps all of them
38
+ name_of_csv_file (Union[str, None]): Name of csv file to export
39
+ filtered set
40
+ """
41
+
42
+ # Unpacking the bounds data
43
+ longitude_min, latitude_min, longitude_max, latitude_max = bounds
44
+
45
+ # Do a path validation
46
+ csv_file = Path(csv_file)
47
+ output_folder = Path(output_folder)
48
+ path_validation(csv_file, check_for_file=True, check_for_file_type=".csv")
49
+ path_validation(output_folder)
50
+
51
+ # Reading the hifld csv data
52
+ df = pd.read_csv(csv_file)
53
+
54
+ # filtering for bounds
55
+ df_filtered = df[
56
+ (df[logitude_column_name] >= longitude_min)
57
+ & (df[logitude_column_name] <= longitude_max)
58
+ & (df[latitude_column_name] >= latitude_min)
59
+ & (df[latitude_column_name] <= latitude_max)
60
+ ]
61
+
62
+ # Keep only the limited columns
63
+ df_subset = df_filtered[columns_to_keep]
64
+
65
+ # export the subset
66
+ file_name = name_of_csv_file if name_of_csv_file else csv_file.name
67
+ df_subset.to_csv(output_folder / file_name)
68
+
69
+
70
+ def get_relationship_between_hifld_infrastructures(
71
+ hifld_data_csv: str,
72
+ unique_id_column: str,
73
+ load_csv: str,
74
+ bus_csv: str,
75
+ output_csv_path: str,
76
+ distance_threshold: float = 2000.0,
77
+ ):
78
+ """Creates a relationship between consumers and HIFLD infrastructures.
79
+
80
+ Args:
81
+ hifld_data_csv (str): Path to filtered HIFLD data csv file
82
+ unique_id_column (List): Column name used as identifier
83
+ for critical infrastructures
84
+ load_csv (str): Path to load csv file
85
+ bus_csv (str): Path to bus csv file
86
+ output_csv_path (str): output csv path for storing relationship csv
87
+ distance_threshold (float): Distance threshold used for mapping
88
+ customer to critical infrastructure
89
+ """
90
+ hifld_data_csv = Path(hifld_data_csv)
91
+ bus_csv = Path(bus_csv)
92
+ load_csv = Path(load_csv)
93
+ output_csv_path = Path(output_csv_path)
94
+
95
+ path_validation(
96
+ hifld_data_csv, check_for_file=True, check_for_file_type=".csv"
97
+ )
98
+ path_validation(bus_csv, check_for_file=True, check_for_file_type=".csv")
99
+ path_validation(load_csv, check_for_file=True, check_for_file_type=".csv")
100
+ path_validation(output_csv_path.parents[0])
101
+
102
+ hifld_data_df = pd.read_csv(hifld_data_csv)
103
+ load_df = pd.read_csv(load_csv)
104
+ bus_df = pd.read_csv(bus_csv)
105
+
106
+ merged_data = pd.merge(
107
+ load_df, bus_df, how="left", left_on="source", right_on="name"
108
+ ).to_dict(orient="records")
109
+
110
+ # Container for storing shelter relationships
111
+ _relationship = []
112
+ for _record in hifld_data_df.to_dict(orient="records"):
113
+ _lon, _lat = _record["LONGITUDE"], _record["LATITUDE"]
114
+
115
+ # convert into state plane coordinates
116
+ _lon_translated, _lat_translated = stateplane.from_lonlat(_lon, _lat)
117
+
118
+ # Loop through all the loads
119
+ for load_record in merged_data:
120
+
121
+ load_lon, load_lat = (
122
+ load_record["longitude"],
123
+ load_record["latitude"],
124
+ )
125
+
126
+ # convert into state plane coordinates
127
+ load_lon_translated, load_lat_translated = stateplane.from_lonlat(
128
+ load_lon, load_lat
129
+ )
130
+
131
+ # computes distance
132
+ distance = math.sqrt(
133
+ (_lat_translated - load_lat_translated) ** 2
134
+ + (_lon_translated - load_lon_translated) ** 2
135
+ )
136
+
137
+ if distance < distance_threshold:
138
+ _relationship.append(
139
+ {
140
+ unique_id_column: _record[unique_id_column],
141
+ "load_name": load_record["name_x"],
142
+ "distance": distance,
143
+ }
144
+ )
145
+
146
+ df = pd.DataFrame(_relationship)
147
+ df.to_csv(output_csv_path)