Source code for path4gmns.io

import csv
import os
import warnings

from .classes import Node, Link, Zone, Network, Column, ColumnVec, VDFPeriod, \
                     AgentType, DemandPeriod, Demand, SpecialEvent, Assignment, UI

from .colgen import update_links_using_columns
from .consts import EPSILON
from .utils import InvalidRecord, _convert_boundaries, _convert_str_to_float, \
                   _convert_str_to_int, _get_time_stamp, get_len_unit_conversion_factor, \
                   get_spd_unit_conversion_factor
from .zonesyn import network_to_zones


__all__ = [
    'read_network',
    'read_demand',
    'read_measurements',
    'load_demand',
    'load_columns',
    'output_columns',
    'output_link_performance',
    'output_agent_paths',
    'output_agent_trajectory',
    'output_synthetic_zones',
    'output_synthetic_demand'
]


# for precheck on connectivity of each OD pair
# 0: isolated, has neither outgoing links nor incoming links
# 1: has at least one outgoing link
# 2: has at least one incoming link
# 3: has both outgoing and incoming links
_zone_degrees = {}


def _update_orig_zone(oz_id):
    if oz_id not in _zone_degrees:
        _zone_degrees[oz_id] = 1
    elif _zone_degrees[oz_id] == 2:
        _zone_degrees[oz_id] = 3


def _update_dest_zone(dz_id):
    if dz_id not in _zone_degrees:
        _zone_degrees[dz_id] = 2
    elif _zone_degrees[dz_id] == 1:
        _zone_degrees[dz_id] = 3


def _are_od_connected(oz_id, dz_id):
    connected = True

    # at least one node in O must have outgoing links
    if oz_id not in _zone_degrees or _zone_degrees[oz_id] == 2:
        connected = False
        print(f'WARNING! {oz_id} has no outgoing links to route volume '
              f'between OD: {oz_id} --> {dz_id}')

    # at least one node in D must have incoming links
    if dz_id not in _zone_degrees or _zone_degrees[dz_id] == 1:
        if connected:
            connected = False
        print(f'WARNING! {dz_id} has no incoming links to route volume '
              f'between OD: {oz_id} --> {dz_id}')

    return connected


def read_nodes(input_dir,
               nodes,
               map_id_to_no,
               map_no_to_id,
               zones,
               load_demand):

    """ step 1: read input_node """
    with open(input_dir+'/node.csv', 'r') as fp:
        print('read node.csv')

        reader = csv.DictReader(fp)
        node_no = 0
        for line in reader:
            # set up node_id
            node_id = line['node_id']
            # node_id should be unique
            if node_id in map_id_to_no:
                print(f'Duplicate node id found {node_id}. Record discarded!')
                continue

            # set up zone_id
            try:
                zone_id = line['zone_id']
            except KeyError:
                zone_id = ''

            # treat them as string
            coord_x = line['x_coord']
            coord_y = line['y_coord']

            # activity node
            is_activity_node = False
            try:
                b = _convert_str_to_int(line['boundary'])
                if b:
                    is_activity_node = True
            except (KeyError, InvalidRecord):
                pass

            # construct node object
            node = Node(node_no, node_id, zone_id, coord_x, coord_y, is_activity_node)
            nodes.append(node)

            # set up mapping between node_no and node_id
            map_id_to_no[node_id] = node_no
            map_no_to_id[node_no] = node_id

            # bin_index for equity evaluation
            try:
                bin_index = _convert_str_to_int(line['bin_index'])
            except (KeyError, InvalidRecord):
                bin_index = 0

            # associate node_id with corresponding zone
            if zone_id not in zones:
                # only take the value of bin_index from the first node
                # associated with each zone
                z = Zone(zone_id, bin_index)
                zones[zone_id] = z

            zones[zone_id].add_node(node_id)
            if is_activity_node:
                zones[zone_id].add_activity_node(node_id)

            node_no += 1

        print(f'the number of nodes is {node_no:,d}')

        if load_demand:
            zone_size = len(zones)
            if '' in zones:
                zone_size -= 1

            if zone_size == 0:
                raise Exception('there are NO VALID zones from node.csv')

            print(f'the number of zones is {zone_size:,d}\n')


def read_links(input_dir,
               links,
               nodes,
               map_id_to_no,
               link_ids,
               demand_period_size,
               len_conversion_factor,
               spd_conversion_factor,
               load_demand):
    """ step 2: read input_link """
    with open(input_dir+'/link.csv', 'r') as fp:
        print('read link.csv')

        reader = csv.DictReader(fp)
        link_no = 0
        # a temporary container (set) to validate the uniqueness of a link id
        link_ids_ = set()
        for line in reader:
            # link id shall be unique
            link_id = line['link_id']
            # binary search shall be fast enough
            if link_id in link_ids_:
                print(f'Duplicate link id found {link_id}. Record discarded!')
                continue

            # validity check
            from_node_id = line['from_node_id']
            to_node_id = line['to_node_id']

            try:
                from_node_no = map_id_to_no[from_node_id]
            except KeyError:
                print(
                    f'Exception: Node ID {from_node_id}'
                    ' NOT in the network!!'
                )
                continue

            try:
                to_node_no = map_id_to_no[to_node_id]
            except KeyError:
                print(
                    f'Exception: Node ID {to_node_id}'
                    ' NOT in the network!!'
                )
                continue

            try:
                length = _convert_str_to_float(line['length'])
            except InvalidRecord:
                continue

            # pass validity check

            # for the following attributes,
            # if they are not None, convert them to the corresponding types
            # if they are None's, set them using the default values
            try:
                lanes = _convert_str_to_int(line['lanes'])
            except InvalidRecord:
                lanes = 1

            try:
                link_type = _convert_str_to_int(line['link_type'])
            except (KeyError, InvalidRecord):
                link_type = 1

            try:
                free_speed = _convert_str_to_float(line['free_speed'])
            except InvalidRecord:
                free_speed = 60

            # issue: int??
            try:
                capacity = _convert_str_to_int(line['capacity'])
            except (KeyError, InvalidRecord):
                capacity = 1999

            try:
                toll = _convert_str_to_float(line['toll'])
            except (KeyError, InvalidRecord):
                toll = 0

            # if link.csv does not have no column 'allowed_uses',
            # set allowed_uses to 'all'
            # developer's note:
            # we may need to change this implementation as we cannot deal with
            # cases a link which is not open to any modes
            try:
                allowed_uses = line['allowed_uses']
                if not allowed_uses:
                    raise InvalidRecord
            except (KeyError, InvalidRecord):
                allowed_uses = 'all'

            # if link.csv does not have no column 'geometry',
            # set geometry to ''
            try:
                geometry = line['geometry']
            except KeyError:
                geometry = ''

            link_ids[link_id] = link_no

            # unit conversion
            length = length / len_conversion_factor
            free_speed = free_speed / spd_conversion_factor

            # construct link object
            link = Link(link_id,
                        link_no,
                        from_node_no,
                        to_node_no,
                        from_node_id,
                        to_node_id,
                        length,
                        lanes,
                        link_type,
                        free_speed,
                        capacity,
                        toll,
                        allowed_uses,
                        geometry,
                        demand_period_size)

            # VDF Attributes
            for i in range(demand_period_size):
                dp_id_str = str(i+1)
                header_vdf_alpha = 'VDF_alpha' + dp_id_str
                header_vdf_beta = 'VDF_beta' + dp_id_str
                header_vdf_mu = 'VDF_mu' + dp_id_str
                header_vdf_fftt = 'VDF_fftt' + dp_id_str
                header_vdf_cap = 'VDF_cap' + dp_id_str
                header_vdf_phf = 'VDF_phf' + dp_id_str

                # case i: link.csv does not VDF attributes at all
                # case ii: link.csv only has partial VDF attributes
                # under case i, we will set up only one VDFPeriod object using
                # default values
                # under case ii, we will set up some VDFPeriod objects up to
                # the number of complete set of VDF_alpha, VDF_beta, and VDF_mu
                try:
                    VDF_alpha = _convert_str_to_float(line[header_vdf_alpha])
                except (KeyError, InvalidRecord):
                    if i == 0:
                        # default value will be applied in the constructor
                        VDF_alpha = 0.15
                    else:
                        break

                try:
                    VDF_beta = _convert_str_to_float(line[header_vdf_beta])
                except (KeyError, InvalidRecord):
                    if i == 0:
                        # default value will be applied in the constructor
                        VDF_beta = 4
                    else:
                        break

                try:
                    VDF_mu = _convert_str_to_float(line[header_vdf_mu])
                except (KeyError, InvalidRecord):
                    if i == 0:
                        # default value will be applied in the constructor
                        VDF_mu = 1000
                    else:
                        break

                try:
                    VDF_fftt = _convert_str_to_float(line[header_vdf_fftt])
                except (KeyError, InvalidRecord):
                    # set it up using length and free_speed from link
                    VDF_fftt = length / max(EPSILON, free_speed) * 60

                try:
                    VDF_cap = _convert_str_to_float(line[header_vdf_cap])
                except (KeyError, InvalidRecord):
                    # set it up using capacity from link
                    VDF_cap = capacity * lanes

                # not a mandatory column
                try:
                    VDF_phf = _convert_str_to_float(line[header_vdf_phf])
                except (KeyError, InvalidRecord):
                    # default value will be applied in the constructor
                    VDF_phf = -1

                # construct VDFPeriod object
                vdf = VDFPeriod(i, VDF_alpha, VDF_beta, VDF_mu,
                                VDF_fftt, VDF_cap, VDF_phf)

                link.vdfperiods.append(vdf)

            # set up outgoing links and incoming links
            from_node = nodes[from_node_no]
            to_node = nodes[to_node_no]
            from_node.add_outgoing_link(link)
            to_node.add_incoming_link(link)
            links.append(link)

            # set up zone degrees
            if load_demand:
                oz_id = from_node.get_zone_id()
                dz_id = to_node.get_zone_id()
                _update_orig_zone(oz_id)
                _update_dest_zone(dz_id)

            link_no += 1
            link_ids_.add(link_id)

        print(f'the number of links is {link_no:,d}\n')


def _read_demand(input_dir,
                 file,
                 agent_type_id,
                 demand_period_id,
                 zones,
                 column_pool,
                 check_connectivity=False):
    """ step 3:read input_agent """
    with open(input_dir+'/'+file, 'r') as fp:
        print('read '+file)

        at = agent_type_id
        dp = demand_period_id

        reader = csv.DictReader(fp)
        valid_vol = 0
        invalid_vol = 0
        invalid_od_num = 0
        for line in reader:
            oz_id = line['o_zone_id']
            # o_zone_id does not exist in node.csv, discard it
            if oz_id not in zones:
                continue

            dz_id = line['d_zone_id']
            # d_zone_id does not exist in node.csv, discard it
            if dz_id not in zones:
                continue

            try:
                vol = _convert_str_to_float(line['volume'])
            except InvalidRecord:
                continue

            # discard invalid OD pair, case I: invalid volume
            if vol <= 0:
                invalid_od_num += 1
                continue

            # discard invalid OD pair, case II: O and D are the same
            if oz_id == dz_id:
                invalid_od_num += 1
                invalid_vol += vol
                continue

            # precheck on connectivity of each OD pair
            if check_connectivity and not (_are_od_connected(oz_id, dz_id)):
                continue

            # set up volume for ColumnVec
            if (at, dp, oz_id, dz_id) not in column_pool:
                column_pool[(at, dp, oz_id, dz_id)] = ColumnVec()
            column_pool[(at, dp, oz_id, dz_id)].increase_volume(vol)

            valid_vol += vol

        print(
            f'the total valid demand is {valid_vol:,.3f}\n'
            f'{invalid_od_num:,d} invalid OD pairs are found. '
            f'Total discarded volume: {invalid_vol:,.2f}\n'
        )

        if valid_vol == 0:
            # there are four cases.
            # Case I:   zones is empty
            #           This shall be caught by read_node().
            # Case II:  zones is not empty and invalid_od_num == 0
            #           Every zone_id present in demand.csv is not found in
            #           node.csv. This implies data inconsistency between these
            #           two files on zone_id. Another possibility is that volume
            #           is not numerical.
            # Case III: zones is not empty, invalid_od_num > 0, and invalid_vol == 0
            #           All OD pairs in demand.csv have invalid volume (i.e.,
            #           volume is zero or less).
            # Case IV:  zones is not empty, invalid_od_num > 0, and invalid_vol > 0
            #           This implies either O and D are the same for every OD
            #           pair or no OD pair is not connected.
            if invalid_od_num == 0:
                message = 'volume is not encoded as numerical or Every zone id ' \
                          'present in demand.csv is not found in node.csv.\n\n' \
                          'For the latter one, different zone id representations ' \
                          'could be the reason. For example, zone id is encoded ' \
                          'as integer in demand.csv but decimal in node.csv. ' \
                          'Note that zone_id CANNOT be decimal per GMNS specification!\n'\
                          'Hint: Use an advanced text editor (not Excel) to verify. '
            elif invalid_od_num > 0 and invalid_vol == 0:
                message = 'At lease one OD pair shall have positive volume!\n'
            else:
                message = 'No connected OD pairs are found (i.e., no valid paths ' \
                          'between O and D for each OD pair) or Every OD pair ' \
                          'have the same O and D!\n'

            raise Exception(message)


[docs]def load_demand(ui, agent_type_str='a', demand_period_str='AM', input_dir='.', filename='demand.csv'): """ load demand for an agent type and a demand period this is an user interface while _read_demand() is intended for internal use. """ A = ui._base_assignment at = A.get_agent_type_id(agent_type_str) dp = A.get_demand_period_id(demand_period_str) # do not check connectivity of OD pairs _read_demand(input_dir, filename, at, dp, A.network.zones, A.column_pool, False)
def _read_zones(ui, input_dir='.', filename='syn_zone.csv'): """ read syn_zone.csv to set up (synthetic) zones """ with open(input_dir+'/'+filename, 'r') as fp: print('read syn_zone.csv') A = ui._base_assignment zones = A.network.zones zones.clear() node_objs = A.get_nodes() reader = csv.DictReader(fp) for line in reader: zone_id = line['zone_id'] if not zone_id: continue nodes = line['activity_nodes'] if not nodes: continue # just in case that there is empty space in between ';' node_ids = [x.strip() for x in nodes.split(';') if x.strip()] if not node_ids: continue try: bin_index = _convert_str_to_int(line['bin_index']) except (KeyError, InvalidRecord): bin_index = 0 try: x = _convert_str_to_float(line['x_coord']) y = _convert_str_to_float(line['y_coord']) except (KeyError, InvalidRecord): x = 91 y = 181 try: U, D, L, R = _convert_boundaries(line['geometry']) except (KeyError, InvalidRecord): U = D = L = R = '' try: prod = _convert_str_to_int(line['production']) except (KeyError, InvalidRecord): prod = 0 if zone_id not in zones: z = Zone(zone_id, bin_index) z.activity_nodes = [x for x in node_ids] z.nodes = [x for x in node_ids] z.set_coord(x, y) z.set_geo(U, D, L, R) z.set_production(prod) zones[zone_id] = z # update zone info for each node for id in node_ids: try: no = A.get_node_no(id) node_objs[no].zone_id = zone_id except (IndexError, KeyError): continue else: raise Exception(f'DUPLICATE zone id: {zone_id}') print(f'the number of zones is {len(zones):,d}') def _auto_setup(assignment): """ automatically set up one demand period and one agent type The two objects will be set up using the default constructors using the default values. See class DemandPeriod and class AgentType for details """ at = AgentType() dp = DemandPeriod() d = Demand() assignment.update_agent_types(at) assignment.update_demand_periods(dp) assignment.update_demands(d) def read_settings(input_dir, assignment): try: import yaml as ym with open(input_dir+'/settings.yml') as file: print('read settings.yml\n') settings = ym.full_load(file) # agent types agents = settings['agents'] for i, a in enumerate(agents): agent_type = a['type'] agent_name = a['name'] # possible duplication check if agent_type in assignment.map_atstr_id: warnings.warn(f'duplicate agent type found: {agent_type}') continue agent_vot = a['vot'] agent_flow_type = a['flow_type'] agent_pce = a['pce'] agent_ffs = a['free_speed'] try: agent_use_link_ffs = a['use_link_ffs'] except KeyError: agent_use_link_ffs = True at = AgentType(i, agent_type, agent_name, agent_vot, agent_flow_type, agent_pce, agent_ffs, agent_use_link_ffs) assignment.update_agent_types(at) # add the default mode if it does not exist if AgentType.get_default_type_str() not in assignment.map_atstr_id: assignment.update_agent_types(AgentType()) # demand periods demand_periods = settings['demand_periods'] for i, d in enumerate(demand_periods): period = d['period'] time_period = d['time_period'] dp = DemandPeriod(i, period, time_period) # special event try: s = d['special_event'] enable = s['enable'] # no need to set up a special event if it is off if not enable: raise KeyError name = s['name'] se = SpecialEvent(name) links = s['affected_links'] for link in links: link_id = str(link['link_id']) ratio = link['capacity_ratio'] se.affected_links[link_id] = ratio dp.special_event = se except KeyError: pass assignment.update_demand_periods(dp) # demand files demands = settings['demand_files'] for i, d in enumerate(demands): demand_file = d['file_name'] demand_period = d['period'] demand_type = d['agent_type'] if demand_type not in assignment.map_atstr_id: raise Exception( f'{demand_type} is not found as an entry of agents in settings.yml' ) demand = Demand(i, demand_period, demand_type, demand_file) assignment.update_demands(demand) # simulation setup try: simulation = settings['simulation'] # simulation resolution res = simulation['resolution'] assert int(res) > 1 assignment.set_simu_resolution(int(res)) # simulation timings dp_str = simulation['period'] dp = assignment.get_demand_period(dp_str) st = dp.get_start_time() dur = dp.get_duration() assignment.set_simu_start_time(st) assignment.set_simu_duration(dur) except KeyError: pass except ImportError: # just in case user does not have pyyaml installed warnings.warn( 'Please install pyyaml next time!\n' 'Engine will set up one demand period and one agent type using ' 'default values for you, which might NOT reflect your case!' ) _auto_setup(assignment) except FileNotFoundError: # just in case user does not provide settings.yml warnings.warn( 'Please provide settings.yml next time!\n' 'Engine will set up one demand period and one agent type using ' 'default values for you, which might NOT reflect your case!' ) _auto_setup(assignment) except Exception as e: raise e
[docs]def read_network(length_unit='mile', speed_unit='mph', input_dir='.'): # exception handlings on units are taken care by the following two functions len_cf = get_len_unit_conversion_factor(length_unit) spd_cf = get_spd_unit_conversion_factor(speed_unit) assignm = Assignment() network = Network() network.len_unit = length_unit network.len_unit_cf = len_cf read_settings(input_dir, assignm) read_nodes(input_dir, network.nodes, network.map_id_to_no, network.map_no_to_id, network.zones, load_demand) read_links(input_dir, network.links, network.nodes, network.map_id_to_no, network.link_ids, assignm.get_demand_period_count(), len_cf, spd_cf, load_demand) network.update() assignm.network = network return UI(assignm)
[docs]def load_columns(ui, input_dir='.'): with open(input_dir+'/route_assignment.csv', 'r') as f: print('read route_assignment.csv') A = ui._base_assignment cp = A.get_column_pool() # just in case agent_id was not output last_agent_id = 0 reader = csv.DictReader(f) for line in reader: # critical info oz_id = line['o_zone_id'] dz_id = line['d_zone_id'] try: vol = _convert_str_to_float(line['volume']) except InvalidRecord: continue # skip zero-volume column if not vol: continue node_seq = line['node_sequence'] if not node_seq: continue link_seq = line['link_sequence'] if not link_seq: continue # non-critical info try: agent_id = _convert_str_to_int(line['agent_id']) except InvalidRecord: agent_id = last_agent_id + 1 last_agent_id = agent_id # it could be empty # path_id = line['path_id'] at = line['agent_type'] if not at: continue else: # back-compatible on 'p' and 'passenger' try: at = A.get_agent_type_id(at) except Exception: # replace 'p' with 'a' if at.startswith(AgentType.get_legacy_type_str()): at = A.get_agent_type_id(AgentType.get_default_type_str()) else: warnings.warn( f'agent_type {at} is not existing in settings.yml.' 'this record is discarded' ) continue dp = line['demand_period'] if not dp: continue else: dp = A.get_demand_period_id(dp) try: toll = _convert_str_to_float(line['toll']) except InvalidRecord: toll = 0 try: tt = _convert_str_to_float(line['travel_time']) except InvalidRecord: tt = 0 try: dist = _convert_str_to_float(line['distance']) except InvalidRecord: dist = 0 # it could be empty geo = line['geometry'] if (at, dp, oz_id, dz_id) not in cp: cp[(at, dp, oz_id, dz_id)] = ColumnVec() cv = A.get_column_vec(at, dp, oz_id, dz_id) path_id = cv.get_column_num() col = Column(path_id) try: col.nodes = [A.get_node_no(x) for x in reversed(node_seq.split(';')) if x] except KeyError: raise Exception( 'Invalid node found on column!!' 'Did you use route_assignment.csv from a different network?' ) try: # if x is only needed for columns generated from DTALite, # which have the trailing ';' and leads to '' after split col.links = [ A.get_link_no(x) for x in reversed(link_seq.split(';')) if x ] except KeyError: raise Exception( 'INVALID link found on column!!' 'Did you use route_assignment.csv from a different network?' ) except ValueError: raise Exception( f'INVALID LINK PATH found for agent id: {agent_id}' ) # the following four are non-critical info col.set_volume(vol) col.set_toll(toll) col.set_travel_time(tt) col.set_geometry(geo) # deprecate node_sum and adopt the same implementation in colgen.py existing = False if dist == 0: sum(A.get_link(x).get_length() for x in col.links) for col_ in cv.get_columns(): if col_.get_distance() != dist: continue if col_.get_links() == col.links: col_.increase_volume(vol) existing = True break if not existing: col.set_distance(dist) cv.add_new_column(col) cv.increase_volume(vol) update_links_using_columns(ui)
[docs]def output_columns(ui, output_geometry=False, output_dir='.'): with open(output_dir+'/route_assignment.csv', 'w', newline='') as fp: base = ui._base_assignment nodes = base.get_nodes() links = base.get_links() column_pool = base.get_column_pool() writer = csv.writer(fp) line = ['agent_id', 'o_zone_id', 'd_zone_id', 'path_id', 'agent_type', 'demand_period', 'volume', 'toll', 'travel_time', 'distance', 'node_sequence', 'link_sequence', 'geometry'] writer.writerow(line) path_sep = ';' i = 0 for k, cv in column_pool.items(): # k = (at_id, dp_id, oz_id, dz_id) at_id = k[0] dp_id = k[1] oz_id = k[2] dz_id = k[3] at_str = base.get_agent_type_str(at_id) dp_str = base.get_demand_period_str(dp_id) for col in cv.get_columns(): # skip zero-volume column if not col.get_volume(): continue i += 1 node_seq = path_sep.join( nodes[x].get_node_id() for x in reversed(col.nodes) ) link_seq = path_sep.join( links[x].get_link_id() for x in reversed(col.links) ) geometry = '' if output_geometry: geometry = ', '.join( nodes[x].get_coordinate() for x in reversed(col.nodes) ) geometry = 'LINESTRING (' + geometry + ')' line = [i, oz_id, dz_id, col.get_id(), at_str, dp_str, '{:.4f}'.format(col.get_volume()), col.get_toll(), col.get_travel_time(), col.get_distance(), node_seq, link_seq, geometry] writer.writerow(line) if output_dir == '.': print(f'\ncheck route_assignment.csv in {os.getcwd()} for path finding results') else: print( f'\ncheck route_assignment.csv in {os.path.join(os.getcwd(), output_dir)}' ' for path finding results' )
[docs]def output_agent_paths(ui, output_geometry=True, output_dir='.'): """ output unique agent path use it with find_path_for_agents() (which has been DEPRECATED) """ with open(output_dir+'/agent_paths.csv', 'w', newline='') as f: writer = csv.writer(f) line = ['agent_id', 'o_zone_id', 'd_zone_id', 'origin node', 'destination node', 'path_id', 'agent_type', 'demand_period', 'OD volume', 'distance', 'node_sequence', 'link_sequence', 'geometry'] writer.writerow(line) base = ui._base_assignment nodes = base.get_nodes() agents = base.get_agents() agents.sort(key=lambda agent: agent.get_orig_node_id()) at_str = '' dp_str = '' for a in agents: if not a.get_node_path(): continue at_str = base.get_agent_type_str(a.get_at_id()) dp_str = base.get_demand_period_str(a.get_dp_id()) break pre_dest_node_id = -1 for a in agents: if not a.get_node_path(): continue if a.get_dest_node_id() == pre_dest_node_id: continue pre_dest_node_id = a.get_dest_node_id() id = a.get_id() at = a.get_at_id() dp = a.get_dp_id() oz = a.get_orig_zone_id() dz = a.get_dest_zone_id() vol = base.column_pool[(at, dp, oz, dz)].get_od_volume() geometry = '' if output_geometry: geometry = ', '.join( nodes[x].get_coordinate() for x in reversed(a.get_node_path()) ) geometry = 'LINESTRING (' + geometry + ')' line = [id, oz, dz, a.get_orig_node_id(), a.get_dest_node_id(), 0, at_str, dp_str, vol, a.get_path_cost(), base.get_agent_node_path(id, True), base.get_agent_link_path(id, True), geometry] writer.writerow(line) if output_dir == '.': print(f'\ncheck agent_paths.csv in {os.getcwd()} for unique agent paths') else: print( f'\ncheck agent_paths.csv in {os.path.join(os.getcwd(), output_dir)}' ' for unique agent paths' )
[docs]def output_synthetic_zones(ui, output_dir='.'): with open(output_dir+'/syn_zone.csv', 'w', newline='') as f: writer = csv.writer(f) line = ['zone_id', 'bin_index', 'activity_nodes', 'x_coord', 'y_coord', 'geometry', 'production', 'attraction'] writer.writerow(line) base = ui._base_assignment network = base.network zones = network.zones for k, v in zones.items(): bi = v.get_bin_index() nodes = '; '.join(str(x) for x in v.get_activity_nodes()) # [U, D, L, R] = v.get_boundaries() x, y = v.get_coordinate() prod = v.get_production() geo = v.get_geo() line = [k, bi, nodes, x, y, geo, prod, prod] writer.writerow(line) if output_dir == '.': print(f'check zone.csv in {os.getcwd()} for synthetic zones') else: print( f'check zone.csv in {os.path.join(os.getcwd(), output_dir)}' ' for synthetic zones' )
[docs]def output_synthetic_demand(ui, output_dir='.'): with open(output_dir+'/syn_demand.csv', 'w', newline='') as f: writer = csv.writer(f) line = ['o_zone_id', 'd_zone_id', 'volume'] writer.writerow(line) column_pool = ui.get_column_pool() for k, v in column_pool.items(): # k = (at_id, dp_id, oz_id, dz_id) line = [k[2], k[3], v.get_od_volume()] writer.writerow(line) if output_dir == '.': print(f'check demand.csv in {os.getcwd()} for synthetic demand\n') else: print( f'check demand.csv in {os.path.join(os.getcwd(), output_dir)}' ' for synthetic demand\n' )
[docs]def output_agent_trajectory(ui, output_dir='.'): with open(output_dir+'/trajectory.csv', 'w', newline='') as f: writer = csv.writer(f) line = ['agent_id', 'o_zone_id', 'd_zone_id', 'departure_time_in_min', 'arrival_time_in_min', 'complete_trip', 'travel_time_in_min', 'PCE', 'distance', 'node_sequence', 'geometry', 'time_sequence', 'time_sequence_hhmmss'] writer.writerow(line) A = ui._base_assignment nodes = A.get_nodes() agents = A.get_agents() st = A.get_simu_start_time() pre_dt = -1 pre_od = -1, -1 for a in agents: if a.get_node_path() is None: continue # do not output agents of the same OD pair with the same departure time if a.get_dep_time() == pre_dt and a.get_od() == pre_od: continue pre_dt = a.get_dep_time() pre_od = a.get_od() at = A.cast_interval_to_minute_float(a.link_arr_interval[-1]) + st dt = A.cast_interval_to_minute_float(a.link_dep_interval[-1]) + st time_seq1 = [at, dt] time_seq2 = [_get_time_stamp(at), _get_time_stamp(dt)] num = len(a.link_arr_interval) - 2 # if num < 0, the following loop will be skipped for i in range(num, -1, -1): k = a.link_dep_interval[i] if k < 0: break dt_ = A.cast_interval_to_minute_float(k) + st time_seq1.append(dt_) time_seq2.append(_get_time_stamp(dt_)) time_seq1_str = ';'.join(str(t) for t in time_seq1) time_seq2_str = ';'.join(time_seq2) complete_trip = 'c' if a.link_dep_interval[0] < 0: complete_trip = 'n' # arrival time to the last node or departure time from the last link at_ = time_seq1[-1] # the original implementation using arrival time to the last link # to calculate trip time does not make sense tt = at_ - a.get_dep_time() node_path_str = A.get_agent_node_path(a.get_id()) geometry = ', '.join( nodes[x].get_coordinate() for x in reversed(a.get_node_path()) ) geometry = 'LINESTRING (' + geometry + ')' line = [a.get_id(), a.get_orig_zone_id(), a.get_dest_zone_id(), a.get_dep_time(), at_, complete_trip, tt, a.PCE_factor, a.get_path_cost(), node_path_str, geometry, time_seq1_str, time_seq2_str] writer.writerow(line) if output_dir == '.': print(f'\ncheck trajectory.csv in {os.getcwd()} for trajectories') else: print( f'\ncheck trajectory.csv in {os.path.join(os.getcwd(), output_dir)}' ' for trajectories' )
[docs]def read_measurements(ui, input_dir='.'): """ load traffic observations specified in measurement.csv """ with open(input_dir+'/measurement.csv') as fp: print('read measurement.csv') base = ui._base_assignment map_id_to_no = base.network.map_id_to_no zones = base.network.zones links = base.get_links() # a temporary lookup table to retrieve a link using its head and tail link_lookup = { (link.from_node_no, link.to_node_no) : link for link in links } reader = csv.DictReader(fp) record_no = 0 for line in reader: # get measurement type, which could be link, production, and attraction meas_type = line['measurement_type'] if not meas_type: continue try: count = _convert_str_to_float(line['count']) except KeyError: count = _convert_str_to_float(line['count1']) except InvalidRecord: continue try: ub = line['upper_bound_flag'] except KeyError: ub = 'false' is_upper_bounded = False ub_lowercase = ub.lower() if ub_lowercase.startswith('true') or ub_lowercase.startswith('1'): is_upper_bounded = True # all the other strings will be taken as False if meas_type.startswith('link'): from_node_id = line['from_node_id'] to_node_id = line['to_node_id'] try: from_node_no = map_id_to_no[from_node_id] except KeyError: print( f'Exception: Node ID {from_node_id}' ' NOT in the network!!' ) continue try: to_node_no = map_id_to_no[to_node_id] except KeyError: print( f'Exception: Node ID {to_node_id}' ' NOT in the network!!' ) continue # need to retrieve the link using from node and to node link_key = (from_node_no, to_node_no) if link_key not in link_lookup: continue link = link_lookup[link_key] link.obs = count link.is_obs_upper_bounded = is_upper_bounded elif meas_type.startswith('production'): try: zone_id = line['o_zone_id'] except KeyError: continue if not zone_id or zone_id not in zones: continue zone = zones[zone_id] zone.prod_obs = count zone.is_prod_obs_upper_bounded = is_upper_bounded elif meas_type.startswith('attraction'): try: zone_id = line['d_zone_id'] except KeyError: continue if not zone_id or zone_id not in zones: continue zone = zones[zone_id] zone.attr_obs = count zone.is_attr_obs_upper_bounded = is_upper_bounded else: continue record_no += 1 print(f'the number of valid measurements is {record_no}\n')
[docs]def read_demand(ui, use_synthetic_data = False, save_synthetic_data=True, input_dir='.'): """ a dedicated API to read demand and zone information """ A = ui._base_assignment if not use_synthetic_data: # set up capacity ratio of affected links from special event for dp in A.demand_periods: se = dp.special_event if se is None: continue # k is link id and v is capacity ratio for k, v in se.get_affected_links(): A.set_capacity_ratio(dp.get_id(), k, v) print('load demand') print('Step 1: try to load the default demand file: demand.csv') demand_loaded = False for d in A.get_demands(): try: at = A.get_agent_type_id(d.get_agent_type_str()) dp = A.get_demand_period_id(d.get_period()) _read_demand(input_dir, d.get_file_name(), at, dp, A.network.zones, A.column_pool) if not demand_loaded: demand_loaded = True except FileNotFoundError: continue if demand_loaded: return print('the default demand files are NOT found!\n') # try to load the synthetic demand filename = 'syn_demand.csv' if use_synthetic_data: print(f'attempt to load the synthetic data: {filename} and syn_zone.csv') else: print(f'Step 2: attempt to load the synthetic data: {filename} and syn_zone.csv') for d in A.get_demands(): try: at = A.get_agent_type_id(d.get_agent_type_str()) dp = A.get_demand_period_id(d.get_period()) _read_zones(ui) _read_demand( input_dir, filename, at, dp, A.network.zones, A.column_pool ) # early termination to load only one synthetic demand file return except FileNotFoundError: break print('the synthetic data is missing or incomplete!\n') if use_synthetic_data: print('start to synthesize zones and demand!') else: print('Step 3: start to synthesize zones and demand!') # synthesize zones and demand network_to_zones(ui) print('data synthesis is complete!') if save_synthetic_data: output_synthetic_zones(ui, input_dir) output_synthetic_demand(ui, input_dir)