Skip to content

Commit

Permalink
Merge pull request #12 from xyluo25/main
Browse files Browse the repository at this point in the history
update to v0.3.6
  • Loading branch information
xyluo25 authored Oct 2, 2023
2 parents 4874194 + 9e31cca commit 7b987cd
Show file tree
Hide file tree
Showing 19 changed files with 213 additions and 1,071 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ x64/
*Tutorial_PyPI.md
*dist/*
*build/*
.ipynb_checkpoints/*
928 changes: 0 additions & 928 deletions .ipynb_checkpoints/grid2demand_tutorial-checkpoint.ipynb

This file was deleted.

77 changes: 49 additions & 28 deletions README_pkg.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@

## Project description

GRID2DEMAND: A tool for generating zone-to-zone travel demand based on grid cells


## Introduction

Grid2demand is an open-source quick demand generation tool based on the trip generation and trip distribution methods of the standard 4-step travel model for teaching transportation planning and applications. By taking advantage of OSM2GMNS tool to obtain routable transportation network from OpenStreetMap, Grid2demand aims to further utilize Point of Interest (POI) data to construct trip demand matrix aligned with standard travel models.

You can get access to the introduction video with the link: [https://www.youtube.com/watch?v=EfjCERQQGTs&t=1021s](https://www.youtube.com/watch?v=EfjCERQQGTs&t=1021s)


## Quick Start

Users can refer to the [code template and test data set](https://github.com/asu-trans-ai-lab/grid2demand) to have a quick start.


## Installation

```
Expand All @@ -32,34 +28,29 @@ from grid2demand import GRID2DEMAND


if __name__ == "__main__":
path_node = "./dataset/ASU/node.csv"
path_poi = "./dataset/ASU/poi.csv"
input_dir = "./dataset/ASU"

# Step 1: Load node and poi files from input directory
# There are two ways to load node and poi files: 1. Load from input directory; 2. Load from specified path
# Step 0: Specify input directory, if not, use current working directory as default input directory
input_dir = "./datasets/ASU"

# Initialize a GRID2DEMAND object
gd = GRID2DEMAND(input_dir)

# Step 1.1: Load from specified path
node_dict = gd.read_node("./dataset/ASU/node.csv")
poi_dict = gd.read_poi("./dataset/ASU/poi.csv")
# Step 1: Load node and poi data from input directory
node_dict, poi_dict = gd.load_network.values()

# Step 2: Generate zone dictionary from node dictionary by specifying number of x blocks and y blocks
# To be noticed: num_x_blocks and num_y_blocks have higher priority than cell_width and cell_height
# if num_x_blocks and num_y_blocks are specified, cell_width and cell_height will be ignored
zone_dict = gd.net2zone(node_dict, num_x_blocks=10, num_y_blocks=10, cell_width=0, cell_height=0)
# zone_dict = gd.net2zone(node_dict, cell_width=10, cell_height=10) # This will generate zone based on grid size 10km width and 10km height

# Step 3: synchronize zone with node and poi
# will add zone_id to node and poi dictionaries
# Will also add node_list and poi_list to zone dictionary
# Step 3.1: synchronize zone with node
update_dict = gd.sync_geometry_between_zone_and_node_poi(zone_dict, node_dict, poi_dict)
zone_dict_update = update_dict.get('zone_dict')
node_dict_update = update_dict.get('node_dict')
poi_dict_update = update_dict.get('poi_dict')

# Step 4: Generate zone-to-zone od distance matrix
zone_dict = gd.net2zone(node_dict, num_x_blocks=10, num_y_blocks=10)

# # Generate zone based on grid size with 10 km width and 10km height for each zone
# zone_dict = gd.net2zone(node_dict, cell_width=10, cell_height=10)

# Step 3: synchronize geometry info between zone, node and poi
# add zone_id to node and poi dictionaries
# also add node_list and poi_list to zone dictionary
updated_dict = gd.sync_geometry_between_zone_and_node_poi(zone_dict, node_dict, poi_dict)
zone_dict_update, node_dict_update, poi_dict_update = updated_dict.values()

# Step 4: Calculate zone-to-zone od distance matrix
zone_od_distance_matrix = gd.calc_zone_od_distance_matrix(zone_dict_update)

# Step 5: Generate poi trip rate for each poi
Expand All @@ -69,7 +60,7 @@ if __name__ == "__main__":
node_prod_attr = gd.gen_node_prod_attr(node_dict_update, poi_trip_rate)

# Step 6.1: Calculate zone production and attraction based on node production and attraction
zone_prod_attr = gd.calc_zone_production_attraction(node_prod_attr, zone_dict_update)
zone_prod_attr = gd.calc_zone_prod_attr(node_prod_attr, zone_dict_update)

# Step 7: Run gravity model to generate agent-based demand
df_demand = gd.run_gravity_model(zone_prod_attr, zone_od_distance_matrix)
Expand Down Expand Up @@ -99,3 +90,33 @@ Option 3: Import input_agent.csv to [A/B Street](https://a-b-street.github.io/do
## User guide

Users can check the [user guide](https://github.com/asu-trans-ai-lab/grid2demand/blob/main/README.md) for a detailed introduction of grid2demand.

## Call for Contributions

The grid2demand project welcomes your expertise and enthusiasm!

Small improvements or fixes are always appreciated. If you are considering larger contributions to the source code, please contact us through email:

Xiangyong Luo : [email protected]

Dr. Xuesong Simon Zhou : [email protected]

Writing code isn't the only way to contribute to grid2demand. You can also:

* review pull requests
* help us stay on top of new and old issues
* develop tutorials, presentations, and other educational materials
* develop graphic design for our brand assets and promotional materials
* translate website content
* help with outreach and onboard new contributors
* write grant proposals and help with other fundraising efforts

For more information about the ways you can contribute to grid2demand, visit [our GitHub](https://github.com/asu-trans-ai-lab/grid2demand). If you' re unsure where to start or how your skills fit in, reach out! You can ask by opening a new issue or leaving a comment on a relevant issue that is already open on GitHub.

## Citing grid2demand

If you use grid2demand in your research please use the following BibTeX entry:

```
Xiangyong Luo, Dustin Carlino, and Xuesong Simon Zhou. (2023). xyluo25/grid2demand: new lease to v0.3.5-rc.2 (0.3.5-rc.2). Zenodo. https://doi.org/10.5281/zenodo.8397105
```
2 changes: 1 addition & 1 deletion grid2demand/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .utils_lib.pkg_settings import pkg_settings
from ._grid2demand import GRID2DEMAND

print('grid2demand, version 0.3.4')
print('grid2demand, version 0.3.6')


__all__ = ["read_node", "read_poi", "read_network",
Expand Down
82 changes: 43 additions & 39 deletions grid2demand/_grid2demand.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@

class GRID2DEMAND:

def __init__(self, input_dir: str, output_dir: str = "") -> None:
self.input_dir = path2linux(input_dir)
def __init__(self, input_dir: str = "", output_dir: str = "") -> None:

# check input directory
if not input_dir:
self.input_dir = path2linux(os.getcwd())
print(f" : Input directory is not specified. Use current working directory {self.input_dir} as input directory. Please make sure node.csv and poi.csv are in {self.input_dir}.")
else:
self.input_dir = path2linux(input_dir)
self.output_dir = path2linux(output_dir) if output_dir else self.input_dir

# check input directory
Expand All @@ -44,28 +50,30 @@ def __check_input_dir(self) -> None:
Returns:
None: will generate self.path_node and self.path_poi for class instance.
"""
if not os.path.exists(self.input_dir):
print(" : Checking input directory...")
if not os.path.isdir(self.input_dir):
raise NotADirectoryError(f"Error: Input directory {self.input_dir} does not exist.")

# check required files in input directory
dir_files = get_filenames_from_folder_by_type(self.input_dir, "csv")
required_files = pkg_settings.get("required_files")
required_files = pkg_settings.get("required_files", [])
is_required_files_exist = check_required_files_exist(required_files, dir_files)
if not is_required_files_exist:
raise Exception(f"Error: Required files are not satisfied. Please check {required_files} in {self.input_dir}.")

self.path_node = path2linux(os.path.join(self.input_dir, "node.csv"))
self.path_poi = path2linux(os.path.join(self.input_dir, "poi.csv"))
print(" : Input directory is valid.\n")

def __load_pkg_settings(self) -> None:
print(" : Loading package settings...")
self.pkg_settings = pkg_settings
print(" : Package settings loaded successfully.\n")

def read_node(self, path_node: str = "") -> dict[int, Node]:
@property
def load_node(self) -> dict[int, Node]:
"""read node.csv file and return node_dict
Args:
path_node (str, optional): the path to node.csv. Defaults to "". if not specified, use self.path_node.
Raises:
FileNotFoundError: Error: File {path_node} does not exist.
Expand All @@ -75,64 +83,50 @@ def read_node(self, path_node: str = "") -> dict[int, Node]:
Examples:
>>> from grid2demand import GRID2DEMAND
>>> gd = GRID2DEMAND(input_dir)
>>> node_dict = gd.read_node()
>>> node_dict = gd.load_node
>>> node_dict[1]
Node(id=1, x_coord=121.469, y_coord=31.238, production=0, attraction=0,
boundary_flag=0, zone_id=-1, poi_id=-1, activity_type= '',
activity_location_tab='', geometry='POINT (121.469 31.238)'
"""

if not path_node:
path_node = self.path_node

if not os.path.exists(path_node):
raise FileNotFoundError(f"Error: File {path_node} does not exist.")
if not os.path.exists(self.path_node):
raise FileNotFoundError(f"Error: File {self.path_node} does not exist.")

self.node_dict = read_node(path_node)
self.node_dict = read_node(self.path_node)
return self.node_dict

def read_poi(self, path_poi: str = "") -> dict[int, POI]:
@property
def load_poi(self) -> dict[int, POI]:
"""read poi.csv file and return poi_dict
Args:
path_poi (str, optional): the path to poi.csv. Defaults to "". if not specified, use self.path_poi.
Raises:
FileExistsError: Error: File {path_poi} does not exist.
Returns:
dict[int, POI]: poi_dict {poi_id: POI}
"""

if not path_poi:
path_poi = self.path_poi
if not os.path.exists(self.path_poi):
raise FileExistsError(f"Error: File {self.path_poi} does not exist.")

if not os.path.exists(path_poi):
raise FileExistsError(f"Error: File {path_poi} does not exist.")

self.poi_dict = read_poi(path_poi)
self.poi_dict = read_poi(self.path_poi)
return self.poi_dict

def read_network(self, input_dir: str = "") -> dict[str, dict]:
@property
def load_network(self) -> dict[str, dict]:
"""read node.csv and poi.csv and return network_dict
Args:
input_dir (str, optional): the input directory that include required files. Defaults to "".
if not specified, use self.input_dir.
Raises:
FileExistsError: Error: Input directory {input_dir} does not exist.
Returns:
dict[str, dict]: network_dict {node_dict: dict[int, Node], poi_dict: dict[int, POI]}
"""

if not input_dir:
input_dir = self.input_dir

if not os.path.isdir(input_dir):
raise FileExistsError(f"Error: Input directory {input_dir} does not exist.")
network_dict = read_network(input_dir)
if not os.path.isdir(self.input_dir):
raise FileExistsError(f"Error: Input directory {self.input_dir} does not exist.")
network_dict = read_network(self.input_dir)
self.node_dict = network_dict.get('node_dict')
self.poi_dict = network_dict.get('poi_dict')
return network_dict
Expand All @@ -157,7 +151,7 @@ def net2zone(self, node_dict: dict[int, Node], num_x_blocks: int = 10, num_y_blo
Returns:
dict[str, Zone]: zone_dict {zone_name: Zone}
"""

print(" : Generating zone dictionary...")
self.zone_dict = net2zone(node_dict, num_x_blocks, num_y_blocks, cell_width, cell_height, unit)
return self.zone_dict

Expand All @@ -180,6 +174,7 @@ def sync_geometry_between_zone_and_node_poi(self, zone_dict: dict = "",
Returns:
dict[str, dict]: {"zone_dict": self.zone_dict, "node_dict": self.node_dict, "poi_dict": self.poi_dict}
"""
print(" : Synchronizing geometry between zone and node/poi...")

# if not specified, use self.zone_dict, self.node_dict, self.poi_dict as input
if not all([zone_dict, node_dict, poi_dict]):
Expand Down Expand Up @@ -208,6 +203,7 @@ def sync_geometry_between_zone_and_node_poi(self, zone_dict: dict = "",
f"Error in running {self.sync_geometry_between_zone_and_node_poi.__name__}: \
not valid zone_dict or poi_dict"
) from e

return {"zone_dict": self.zone_dict, "node_dict": self.node_dict, "poi_dict": self.poi_dict}

def calc_zone_od_distance_matrix(self, zone_dict: dict = "") -> dict[tuple, float]:
Expand Down Expand Up @@ -246,6 +242,12 @@ def gen_poi_trip_rate(self, poi_dict: dict = "", trip_rate_file: str = "", trip_
if not poi_dict:
poi_dict = self.poi_dict

# if usr provides trip_rate_file (csv file), save to self.pkg_settings["trip_rate_file"]
if trip_rate_file:
if ".csv" not in trip_rate_file:
raise Exception(f" : Error: trip_rate_file {trip_rate_file} must be a csv file.")
self.pkg_settings["trip_rate_file"] = pd.read_csv(trip_rate_file)

self.poi_dict = gen_poi_trip_rate(poi_dict, trip_rate_file, trip_purpose)
return self.poi_dict

Expand All @@ -267,7 +269,7 @@ def gen_node_prod_attr(self, node_dict: dict = "", poi_dict: dict = "") -> dict[
self.node_dict = gen_node_prod_attr(node_dict, poi_dict)
return self.node_dict

def calc_zone_production_attraction(self, node_dict: dict = "", zone_dict: dict = "") -> dict[str, Zone]:
def calc_zone_prod_attr(self, node_dict: dict = "", zone_dict: dict = "") -> dict[str, Zone]:
"""calculate zone production and attraction based on node production and attraction
Args:
Expand All @@ -281,6 +283,7 @@ def calc_zone_production_attraction(self, node_dict: dict = "", zone_dict: dict
node_dict = self.node_dict
zone_dict = self.zone_dict
self.zone = calc_zone_production_attraction(node_dict, zone_dict)

return self.zone

def run_gravity_model(self, zone_dict: dict = "",
Expand Down Expand Up @@ -340,7 +343,8 @@ def save_demand(self) -> None:
return

path_output = gen_unique_filename(path2linux(os.path.join(self.output_dir, "demand.csv")))
self.df_demand.to_csv(path_output, index=False)
df_demand_non_zero = self.df_demand[self.df_demand["volume"] > 0]
df_demand_non_zero.to_csv(path_output, index=False)
print(f" : Successfully saved demand.csv to {self.output_dir}")

@property
Expand Down
Binary file modified grid2demand/func_lib/__pycache__/gen_agent_demand.cpython-310.pyc
Binary file not shown.
Binary file modified grid2demand/func_lib/__pycache__/gen_zone.cpython-310.pyc
Binary file not shown.
Binary file modified grid2demand/func_lib/__pycache__/gravity_model.cpython-310.pyc
Binary file not shown.
Binary file modified grid2demand/func_lib/__pycache__/read_node_poi.cpython-310.pyc
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions grid2demand/func_lib/gen_agent_demand.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ def gen_agent_based_demand(node_dict: dict, zone_dict: dict,
departure_time=departure_time
)
)
print(" : Successfully generated agent-based demand data.")
return pd.DataFrame(agent_lst)
6 changes: 4 additions & 2 deletions grid2demand/func_lib/gen_zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def generate_polygon(x_min, x_max, y_min, y_max) -> shapely.geometry.Polygon:
geometry=points_lst[i]
)
zone_id_flag += 1
print(f" : Total number of zone cells: {len(zone_dict) - 4 * len(zone_upper_row)} generated, plus {4 * len(zone_upper_row)} boundary gates(points))")
print(f" : Successfully generated zone dictionary: {len(zone_dict) - 4 * len(zone_upper_row)} Zones generated, plus {4 * len(zone_upper_row)} boundary gates (points))")
return zone_dict


Expand All @@ -210,7 +210,7 @@ def sync_zone_and_node_geometry(zone_dict: dict, node_dict: dict) -> dict:
node_dict[node_id].zone_id = zone_dict[zone_name].id
zone_dict[zone_name].node_id_list.append(node_id)
break

print(" : Successfully synchronized zone and node geometry")
return {"node_dict": node_dict, "zone_dict": zone_dict}


Expand All @@ -236,6 +236,7 @@ def sync_zone_and_poi_geometry(zone_dict: dict, poi_dict: dict) -> dict:
poi_dict[poi_id].zone_id = zone_dict[zone_name].id
zone_dict[zone_name].poi_id_list.append(poi_id)
break
print(" : Successfully synchronized zone and poi geometry")
return {"poi_dict": poi_dict, "zone_dict": zone_dict}


Expand Down Expand Up @@ -276,4 +277,5 @@ def calc_zone_od_matrix(zone_dict: dict) -> dict[tuple[str, str], dict]:
}
for i, j in itertools.product(range(len_df_zone), range(len_df_zone))
}
print(" : Successfully calculated zone-to-zone distance matrix")
return dist_dict
3 changes: 3 additions & 0 deletions grid2demand/func_lib/gravity_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def calc_zone_production_attraction(node_dict: dict, zone_dict: dict) -> dict:
for node_id in zone_dict[zone_name].node_id_list:
zone_dict[zone_name].production += node_dict[node_id].production
zone_dict[zone_name].attraction += node_dict[node_id].attraction
print(" : Successfully calculated zone production and attraction based on node production and attraction.")
return zone_dict


Expand All @@ -27,6 +28,7 @@ def calc_zone_od_friction_attraction(zone_od_friction_matrix_dict: dict, zone_di
zone_od_friction_attraction_dict[zone_name[0]] = friction_val * zone_dict[zone_name[1]].attraction
else:
zone_od_friction_attraction_dict[zone_name[0]] += friction_val * zone_dict[zone_name[1]].attraction
print(" : Successfully calculated zone od friction attraction.")
return zone_od_friction_attraction_dict


Expand Down Expand Up @@ -65,4 +67,5 @@ def run_gravity_model(zone_dict: dict,
zone_od_friction_attraction_dict[zone_name_pair[0]])

# Generate demand.csv
print(" : Successfully run gravity model to generate demand.csv.")
return pd.DataFrame(list(zone_od_matrix_dict.values()))
Loading

0 comments on commit 7b987cd

Please sign in to comment.