mosstool.trip.gmns

Convertor to https://github.com/zephyr-data-specs/GMNS and Static traffic assignment

1"""
2Convertor to https://github.com/zephyr-data-specs/GMNS
3and
4Static traffic assignment
5"""
6
7from .sta import STA
8
9__all__ = ["STA"]
class STA:
 26class STA:
 27    """
 28    run static traffic assignment on GMNS map with persons
 29    """
 30
 31    def __init__(self, map: Map, work_dir: str):
 32        self._map_convertor = MapConvertor(map)
 33        self._work_dir = work_dir
 34
 35    def _get_od(self, start: Position, end: Position):
 36        """
 37        get origin and destination of a trip
 38
 39        Args:
 40        - t: Trip, trip
 41
 42        Return:
 43        - (origin, destination): (str, str), origin and destination
 44        """
 45
 46        oid, did = None, None
 47        if start.HasField("aoi_position"):
 48            oid = f"{start.aoi_position.aoi_id}-start"
 49        elif start.HasField("lane_position"):
 50            lane = self._map_convertor._lanes[start.lane_position.lane_id]
 51            if lane.parent_id not in self._map_convertor._road2nodes:
 52                raise ValueError(f"lane {lane.id} has invalid parent node")
 53            # choose the successor node as oid
 54            _, oid = self._map_convertor._road2nodes[lane.parent_id]
 55        else:
 56            raise ValueError("start position is invalid")
 57
 58        if end.HasField("aoi_position"):
 59            did = f"{end.aoi_position.aoi_id}-end"
 60        elif end.HasField("lane_position"):
 61            lane = self._map_convertor._lanes[end.lane_position.lane_id]
 62            if lane.parent_id not in self._map_convertor._road2nodes:
 63                raise ValueError(f"lane {lane.id} has invalid parent node")
 64            # choose the predecessor node as did
 65            did, _ = self._map_convertor._road2nodes[lane.parent_id]
 66        else:
 67            raise ValueError("end position is invalid")
 68
 69        return oid, did
 70
 71    def _check_connection(self, start_road_id: int, end_road_id: int):
 72        """
 73        check if two road segments are connected
 74
 75        Args:
 76        - start_road_id: int, start road segment id
 77        - end_road_id: int, end road segment id
 78
 79        Return:
 80        - bool, True if two road segments are connected, False otherwise
 81        """
 82        return (start_road_id, end_road_id) in self._map_convertor._connected_road_pairs
 83
 84    def run(
 85        self,
 86        persons: Persons,
 87        time_interval: int = 60,
 88        reset_routes: bool = False,
 89        column_gen_num: int = 10,
 90        column_update_num: int = 10,
 91    ):
 92        """
 93        run static traffic assignment on GMNS map with persons.
 94        trips of persons will be sliced into time intervals.
 95        static traffic assignment will be run for each time interval.
 96        route results will be saved into persons.
 97
 98        EXPERIMENTAL: the method is only for trip with deterministic departure time. Other cases will be skipped.
 99
100        Args:
101        - persons: Persons, persons with trips
102        - time_interval: int, time interval (minutes) for static traffic assignment slice. Try to set time_interval larger than trip's travel time.
103        - reset_routes: bool, reset routes of persons before running static traffic assignment
104        - column_gen_num: int, number of column generation iterations for static traffic assignment
105        - column_update_num: int, number of column update iterations for static traffic assignment
106
107        Return:
108        - Persons, persons with route results
109        - dict, statistics of static traffic assignment
110        """
111
112        # step 1. convert map to GMNS map
113        self._map_convertor.save(self._work_dir)
114
115        # step 2. get all persons' driving trips with deterministic departure time
116        from_trips = []  # (pi, si, ti, departure_time, start, end)
117        for pi, p in enumerate(persons.persons):
118            departure_time = None
119            now = cast(Position, p.home)
120            for si, s in enumerate(p.schedules):
121                s = cast(Schedule, s)
122                if s.HasField("departure_time"):
123                    departure_time = s.departure_time
124                for ti, t in enumerate(s.trips):
125                    end = cast(Position, t.end)
126                    if t.HasField("departure_time"):
127                        departure_time = t.departure_time
128                    if (
129                        departure_time is not None
130                        and t.mode == TripMode.TRIP_MODE_DRIVE_ONLY
131                        and (reset_routes or len(t.routes) == 0)
132                        and s.loop_count == 1
133                    ):
134                        t.ClearField("routes")
135                        from_trips.append((pi, si, ti, departure_time, now, end))
136                    departure_time = None
137                    now = end
138        from_trips.sort(key=lambda x: x[3])
139
140        from_trips_i = 0
141        success_cnt = 0
142        total_volumes = 0
143        valid_volumes = 0
144        disjointed_cnt = 0
145        while from_trips_i < len(from_trips):
146            # choose first slice
147            demands = defaultdict(int)  # (origin, destination) -> count
148            start_t = from_trips[from_trips_i][3]
149            end_t = start_t + time_interval * 60
150            this_from_trips = []
151            while from_trips_i < len(from_trips):
152                pi, si, ti, departure_time, start, end = from_trips[from_trips_i]
153                if departure_time >= end_t:
154                    break
155                this_from_trips.append((pi, si, ti, departure_time, start, end))
156                oid, did = self._get_od(start, end)
157                demands[(oid, did)] += 1
158                from_trips_i += 1
159            od_df = pd.DataFrame(
160                [
161                    {
162                        "o_zone_id": o,
163                        "d_zone_id": d,
164                        "volume": volume,
165                    }
166                    for (o, d), volume in demands.items()
167                ]
168            )
169            od_df.sort_values(["o_zone_id", "d_zone_id"], inplace=True)
170            # write to demand.csv
171            od_df.to_csv(os.path.join(self._work_dir, "demand.csv"), index=False)
172
173            # step 3: run traffic assignment
174            network = pg.read_network(
175                input_dir=self._work_dir, length_unit="km", speed_unit="kph"
176            )
177            pg.load_demand(network, input_dir=self._work_dir, demand_period_str="AM")
178            pg.perform_column_generation(column_gen_num, column_update_num, network)
179            pg.output_columns(network, output_geometry=False, output_dir=self._work_dir)
180
181            # step 4: get route results
182            agent_df = pd.read_csv(
183                os.path.join(self._work_dir, "agent.csv"), index_col=None
184            )
185            # (o_zone_id, d_zone_id) -> [{volume, node_sequence, link_sequence}]
186            agent_pairs: Dict[Tuple[str, str], List[Dict[str, Any]]] = defaultdict(list)
187            for _, row in agent_df.iterrows():
188                volume = int(row["volume"])
189                total_volumes += volume
190                link_sequence = cast(List[str], row["link_sequence"].split(";"))
191                # check path is valid and convert to road_ids for route .pb format
192                start_with_aoi = link_sequence[0].find("start") != -1
193                end_with_aoi = link_sequence[-1].find("end") != -1
194                road_ids = []
195                if start_with_aoi:
196                    # {aoiid}-start-{roadid}-{junctionid}
197                    road_id = link_sequence[0].split("-")[2]
198                    road_ids.append(int(road_id))
199                    link_sequence = link_sequence[1:]
200                if end_with_aoi:
201                    # replace the last link with the road id
202                    road_id = link_sequence[-1].split("-")[2]
203                    link_sequence[-1] = road_id
204                road_ids.extend([int(link) for link in link_sequence])
205                # check connection
206                bad = False
207                for i in range(1, len(road_ids)):
208                    if not self._check_connection(road_ids[i - 1], road_ids[i]):
209                        bad = True
210                        break
211                if bad:
212                    disjointed_cnt += volume
213                    continue
214                valid_volumes += volume
215                agent_pairs[(row["o_zone_id"], row["d_zone_id"])].append(
216                    {
217                        "volume": volume,
218                        "road_ids": road_ids,
219                        "start_with_aoi": start_with_aoi,
220                        "end_with_aoi": end_with_aoi,
221                    }
222                )
223            # assign route results to persons
224            for pi, si, ti, departure_time, start, end in this_from_trips:
225                oid, did = self._get_od(start, end)
226                if (oid, did) not in agent_pairs:
227                    continue
228                t = cast(Trip, persons.persons[pi].schedules[si].trips[ti])
229                result = agent_pairs[(oid, did)][0]
230                road_ids: List[int] = copy(result["road_ids"])
231                if not result["start_with_aoi"]:
232                    start_lane = self._map_convertor._lanes[start.lane_position.lane_id]
233                    road_ids.insert(0, start_lane.parent_id)
234                    # check connection
235                    if not self._check_connection(start_lane.id, road_ids[1]):
236                        disjointed_cnt += 1
237                        continue
238                if not result["end_with_aoi"]:
239                    end_lane = self._map_convertor._lanes[end.lane_position.lane_id]
240                    road_ids.append(end_lane.parent_id)
241                    # check connection
242                    if not self._check_connection(road_ids[-2], end_lane.id):
243                        disjointed_cnt += 1
244                        continue
245                assert len(road_ids) >= 2
246                assert len(t.routes) == 0, f"routes should be empty, but got {t.routes}"
247                t.routes.append(
248                    Journey(
249                        type=JourneyType.JOURNEY_TYPE_DRIVING,
250                        driving=DrivingJourneyBody(road_ids=road_ids),
251                    )
252                )
253                success_cnt += 1
254                # update volume
255                result["volume"] -= 1
256                if result["volume"] <= 0:
257                    agent_pairs[(oid, did)].pop(0)
258                    if len(agent_pairs[(oid, did)]) == 0:
259                        agent_pairs.pop((oid, did))
260
261        return persons, {
262            "trip_cnt": len(from_trips),
263            "total_volumes": total_volumes,
264            "valid_volumes": valid_volumes,
265            "successful_cnt": success_cnt,
266            "disjointed_cnt": disjointed_cnt,
267        }

run static traffic assignment on GMNS map with persons

STA(map: city.map.v2.map_pb2.Map, work_dir: str)
31    def __init__(self, map: Map, work_dir: str):
32        self._map_convertor = MapConvertor(map)
33        self._work_dir = work_dir
def run( self, persons: city.person.v2.person_pb2.Persons, time_interval: int = 60, reset_routes: bool = False, column_gen_num: int = 10, column_update_num: int = 10):
 84    def run(
 85        self,
 86        persons: Persons,
 87        time_interval: int = 60,
 88        reset_routes: bool = False,
 89        column_gen_num: int = 10,
 90        column_update_num: int = 10,
 91    ):
 92        """
 93        run static traffic assignment on GMNS map with persons.
 94        trips of persons will be sliced into time intervals.
 95        static traffic assignment will be run for each time interval.
 96        route results will be saved into persons.
 97
 98        EXPERIMENTAL: the method is only for trip with deterministic departure time. Other cases will be skipped.
 99
100        Args:
101        - persons: Persons, persons with trips
102        - time_interval: int, time interval (minutes) for static traffic assignment slice. Try to set time_interval larger than trip's travel time.
103        - reset_routes: bool, reset routes of persons before running static traffic assignment
104        - column_gen_num: int, number of column generation iterations for static traffic assignment
105        - column_update_num: int, number of column update iterations for static traffic assignment
106
107        Return:
108        - Persons, persons with route results
109        - dict, statistics of static traffic assignment
110        """
111
112        # step 1. convert map to GMNS map
113        self._map_convertor.save(self._work_dir)
114
115        # step 2. get all persons' driving trips with deterministic departure time
116        from_trips = []  # (pi, si, ti, departure_time, start, end)
117        for pi, p in enumerate(persons.persons):
118            departure_time = None
119            now = cast(Position, p.home)
120            for si, s in enumerate(p.schedules):
121                s = cast(Schedule, s)
122                if s.HasField("departure_time"):
123                    departure_time = s.departure_time
124                for ti, t in enumerate(s.trips):
125                    end = cast(Position, t.end)
126                    if t.HasField("departure_time"):
127                        departure_time = t.departure_time
128                    if (
129                        departure_time is not None
130                        and t.mode == TripMode.TRIP_MODE_DRIVE_ONLY
131                        and (reset_routes or len(t.routes) == 0)
132                        and s.loop_count == 1
133                    ):
134                        t.ClearField("routes")
135                        from_trips.append((pi, si, ti, departure_time, now, end))
136                    departure_time = None
137                    now = end
138        from_trips.sort(key=lambda x: x[3])
139
140        from_trips_i = 0
141        success_cnt = 0
142        total_volumes = 0
143        valid_volumes = 0
144        disjointed_cnt = 0
145        while from_trips_i < len(from_trips):
146            # choose first slice
147            demands = defaultdict(int)  # (origin, destination) -> count
148            start_t = from_trips[from_trips_i][3]
149            end_t = start_t + time_interval * 60
150            this_from_trips = []
151            while from_trips_i < len(from_trips):
152                pi, si, ti, departure_time, start, end = from_trips[from_trips_i]
153                if departure_time >= end_t:
154                    break
155                this_from_trips.append((pi, si, ti, departure_time, start, end))
156                oid, did = self._get_od(start, end)
157                demands[(oid, did)] += 1
158                from_trips_i += 1
159            od_df = pd.DataFrame(
160                [
161                    {
162                        "o_zone_id": o,
163                        "d_zone_id": d,
164                        "volume": volume,
165                    }
166                    for (o, d), volume in demands.items()
167                ]
168            )
169            od_df.sort_values(["o_zone_id", "d_zone_id"], inplace=True)
170            # write to demand.csv
171            od_df.to_csv(os.path.join(self._work_dir, "demand.csv"), index=False)
172
173            # step 3: run traffic assignment
174            network = pg.read_network(
175                input_dir=self._work_dir, length_unit="km", speed_unit="kph"
176            )
177            pg.load_demand(network, input_dir=self._work_dir, demand_period_str="AM")
178            pg.perform_column_generation(column_gen_num, column_update_num, network)
179            pg.output_columns(network, output_geometry=False, output_dir=self._work_dir)
180
181            # step 4: get route results
182            agent_df = pd.read_csv(
183                os.path.join(self._work_dir, "agent.csv"), index_col=None
184            )
185            # (o_zone_id, d_zone_id) -> [{volume, node_sequence, link_sequence}]
186            agent_pairs: Dict[Tuple[str, str], List[Dict[str, Any]]] = defaultdict(list)
187            for _, row in agent_df.iterrows():
188                volume = int(row["volume"])
189                total_volumes += volume
190                link_sequence = cast(List[str], row["link_sequence"].split(";"))
191                # check path is valid and convert to road_ids for route .pb format
192                start_with_aoi = link_sequence[0].find("start") != -1
193                end_with_aoi = link_sequence[-1].find("end") != -1
194                road_ids = []
195                if start_with_aoi:
196                    # {aoiid}-start-{roadid}-{junctionid}
197                    road_id = link_sequence[0].split("-")[2]
198                    road_ids.append(int(road_id))
199                    link_sequence = link_sequence[1:]
200                if end_with_aoi:
201                    # replace the last link with the road id
202                    road_id = link_sequence[-1].split("-")[2]
203                    link_sequence[-1] = road_id
204                road_ids.extend([int(link) for link in link_sequence])
205                # check connection
206                bad = False
207                for i in range(1, len(road_ids)):
208                    if not self._check_connection(road_ids[i - 1], road_ids[i]):
209                        bad = True
210                        break
211                if bad:
212                    disjointed_cnt += volume
213                    continue
214                valid_volumes += volume
215                agent_pairs[(row["o_zone_id"], row["d_zone_id"])].append(
216                    {
217                        "volume": volume,
218                        "road_ids": road_ids,
219                        "start_with_aoi": start_with_aoi,
220                        "end_with_aoi": end_with_aoi,
221                    }
222                )
223            # assign route results to persons
224            for pi, si, ti, departure_time, start, end in this_from_trips:
225                oid, did = self._get_od(start, end)
226                if (oid, did) not in agent_pairs:
227                    continue
228                t = cast(Trip, persons.persons[pi].schedules[si].trips[ti])
229                result = agent_pairs[(oid, did)][0]
230                road_ids: List[int] = copy(result["road_ids"])
231                if not result["start_with_aoi"]:
232                    start_lane = self._map_convertor._lanes[start.lane_position.lane_id]
233                    road_ids.insert(0, start_lane.parent_id)
234                    # check connection
235                    if not self._check_connection(start_lane.id, road_ids[1]):
236                        disjointed_cnt += 1
237                        continue
238                if not result["end_with_aoi"]:
239                    end_lane = self._map_convertor._lanes[end.lane_position.lane_id]
240                    road_ids.append(end_lane.parent_id)
241                    # check connection
242                    if not self._check_connection(road_ids[-2], end_lane.id):
243                        disjointed_cnt += 1
244                        continue
245                assert len(road_ids) >= 2
246                assert len(t.routes) == 0, f"routes should be empty, but got {t.routes}"
247                t.routes.append(
248                    Journey(
249                        type=JourneyType.JOURNEY_TYPE_DRIVING,
250                        driving=DrivingJourneyBody(road_ids=road_ids),
251                    )
252                )
253                success_cnt += 1
254                # update volume
255                result["volume"] -= 1
256                if result["volume"] <= 0:
257                    agent_pairs[(oid, did)].pop(0)
258                    if len(agent_pairs[(oid, did)]) == 0:
259                        agent_pairs.pop((oid, did))
260
261        return persons, {
262            "trip_cnt": len(from_trips),
263            "total_volumes": total_volumes,
264            "valid_volumes": valid_volumes,
265            "successful_cnt": success_cnt,
266            "disjointed_cnt": disjointed_cnt,
267        }

run static traffic assignment on GMNS map with persons. trips of persons will be sliced into time intervals. static traffic assignment will be run for each time interval. route results will be saved into persons.

EXPERIMENTAL: the method is only for trip with deterministic departure time. Other cases will be skipped.

Args:

  • persons: Persons, persons with trips
  • time_interval: int, time interval (minutes) for static traffic assignment slice. Try to set time_interval larger than trip's travel time.
  • reset_routes: bool, reset routes of persons before running static traffic assignment
  • column_gen_num: int, number of column generation iterations for static traffic assignment
  • column_update_num: int, number of column update iterations for static traffic assignment

Return:

  • Persons, persons with route results
  • dict, statistics of static traffic assignment