Instruction:
Given the following Python 3 function, write the documentation
def graph_from_polygon(polygon, network_type='all_private', simplify=True,
                       retain_all=False, truncate_by_edge=False, name='unnamed',
                       timeout=180, memory=None,
                       max_query_area_size=50*1000*50*1000,
                       clean_periphery=True, infrastructure='way["highway"]',
                       custom_filter=None):
    """
    Create a networkx graph from OSM data within the spatial boundaries of the
    passed-in shapely polygon.

    Parameters
    ----------
    polygon : shapely Polygon or MultiPolygon
        the shape to get network data within. coordinates should be in units of
        latitude-longitude degrees.
    network_type : string
        what type of street network to get
    simplify : bool
        if true, simplify the graph topology
    retain_all : bool
        if True, return the entire graph even if it is not connected
    truncate_by_edge : bool
        if True retain node if it's outside bbox but at least one of node's
        neighbors are within bbox
    name : string
        the name of the graph
    timeout : int
        the timeout interval for requests and to pass to API
    memory : int
        server memory allocation size for the query, in bytes. If none, server
        will use its default allocation size
    max_query_area_size : float
        max size for any part of the geometry, in square degrees: any polygon
        bigger will get divided up for multiple queries to API
    clean_periphery : bool
        if True (and simplify=True), buffer 0.5km to get a graph larger than
        requested, then simplify, then truncate it to requested spatial extent
    infrastructure : string
        download infrastructure of given type (default is streets
        (ie, 'way["highway"]') but other infrastructures may be selected
        like power grids (ie, 'way["power"~"line"]'))
    custom_filter : string
        a custom network filter to be used instead of the network_type presets

    Returns
    -------
    networkx multidigraph
    """

    # verify that the geometry is valid and is a shapely Polygon/MultiPolygon
    # before proceeding
    if not polygon.is_valid:
        raise TypeError('Shape does not have a valid geometry')
    if not isinstance(polygon, (Polygon, MultiPolygon)):
        raise TypeError('Geometry must be a shapely Polygon or MultiPolygon. If you requested '
                         'graph from place name or address, make sure your query resolves to a '
                         'Polygon or MultiPolygon, and not some other geometry, like a Point. '
                         'See OSMnx documentation for details.')

    if clean_periphery and simplify:
        # create a new buffered polygon 0.5km around the desired one
        buffer_dist = 500
        polygon_utm, crs_utm = project_geometry(geometry=polygon)
        polygon_proj_buff = polygon_utm.buffer(buffer_dist)
        polygon_buffered, _ = project_geometry(geometry=polygon_proj_buff, crs=crs_utm, to_latlong=True)

        # get the network data from OSM,  create the buffered graph, then
        # truncate it to the buffered polygon
        response_jsons = osm_net_download(polygon=polygon_buffered, network_type=network_type,
                                          timeout=timeout, memory=memory,
                                          max_query_area_size=max_query_area_size,
                                          infrastructure=infrastructure, custom_filter=custom_filter)
        G_buffered = create_graph(response_jsons, name=name, retain_all=True,
                                  bidirectional=network_type in settings.bidirectional_network_types)
        G_buffered = truncate_graph_polygon(G_buffered, polygon_buffered, retain_all=True, truncate_by_edge=truncate_by_edge)

        # simplify the graph topology
        G_buffered = simplify_graph(G_buffered)

        # truncate graph by polygon to return the graph within the polygon that
        # caller wants. don't simplify again - this allows us to retain
        # intersections along the street that may now only connect 2 street
        # segments in the network, but in reality also connect to an
        # intersection just outside the polygon
        G = truncate_graph_polygon(G_buffered, polygon, retain_all=retain_all, truncate_by_edge=truncate_by_edge)

        # count how many street segments in buffered graph emanate from each
        # intersection in un-buffered graph, to retain true counts for each
        # intersection, even if some of its neighbors are outside the polygon
        G.graph['streets_per_node'] = count_streets_per_node(G_buffered, nodes=G.nodes())

    else:
        # download a list of API responses for the polygon/multipolygon
        response_jsons = osm_net_download(polygon=polygon, network_type=network_type,
                                          timeout=timeout, memory=memory,
                                          max_query_area_size=max_query_area_size,
                                          infrastructure=infrastructure, custom_filter=custom_filter)

        # create the graph from the downloaded data
        G = create_graph(response_jsons, name=name, retain_all=True,
                         bidirectional=network_type in settings.bidirectional_network_types)

        # truncate the graph to the extent of the polygon
        G = truncate_graph_polygon(G, polygon, retain_all=retain_all, truncate_by_edge=truncate_by_edge)

        # simplify the graph topology as the last step. don't truncate after
        # simplifying or you may have simplified out to an endpoint beyond the
        # truncation distance, in which case you will then strip out your entire
        # edge
        if simplify:
            G = simplify_graph(G)

    log('graph_from_polygon() returning graph with {:,} nodes and {:,} edges'.format(len(list(G.nodes())), len(list(G.edges()))))
    return G

Response: Create a networkx graph from OSM data within the specified shapely polygon.


Instruction:
How would you explain what the following Python 3 function does
def graph_from_place(query, network_type='all_private', simplify=True,
                     retain_all=False, truncate_by_edge=False, name='unnamed',
                     which_result=1, buffer_dist=None, timeout=180, memory=None,
                     max_query_area_size=50*1000*50*1000, clean_periphery=True,
                     infrastructure='way["highway"]', custom_filter=None):
    """
    Create a networkx graph from OSM data within the spatial boundaries of some
    geocodable place(s).

    The query must be geocodable and OSM must have polygon boundaries for the
    geocode result. If OSM does not have a polygon for this place, you can
    instead get its street network using the graph_from_address function, which
    geocodes the place name to a point and gets the network within some distance
    of that point. Alternatively, you might try to vary the which_result
    parameter to use a different geocode result. For example, the first geocode
    result (ie, the default) might resolve to a point geometry, but the second
    geocode result for this query might resolve to a polygon, in which case you
    can use graph_from_place with which_result=2.

    Parameters
    ----------
    query : string or dict or list
        the place(s) to geocode/download data for
    network_type : string
        what type of street network to get
    simplify : bool
        if true, simplify the graph topology
    retain_all : bool
        if True, return the entire graph even if it is not connected
    truncate_by_edge : bool
        if True retain node if it's outside bbox but at least one of node's
        neighbors are within bbox
    name : string
        the name of the graph
    which_result : int
        max number of results to return and which to process upon receipt
    buffer_dist : float
        distance to buffer around the place geometry, in meters
    timeout : int
        the timeout interval for requests and to pass to API
    memory : int
        server memory allocation size for the query, in bytes. If none, server
        will use its default allocation size
    max_query_area_size : float
        max size for any part of the geometry, in square degrees: any polygon
        bigger will get divided up for multiple queries to API
    clean_periphery : bool
        if True (and simplify=True), buffer 0.5km to get a graph larger than
        requested, then simplify, then truncate it to requested spatial extent
    infrastructure : string
        download infrastructure of given type (default is streets (ie, 'way["highway"]') but other
        infrastructures may be selected like power grids (ie, 'way["power"~"line"]'))
    custom_filter : string
        a custom network filter to be used instead of the network_type presets

    Returns
    -------
    networkx multidigraph
    """

    # create a GeoDataFrame with the spatial boundaries of the place(s)
    if isinstance(query, str) or isinstance(query, dict):
        # if it is a string (place name) or dict (structured place query), then
        # it is a single place
        gdf_place = gdf_from_place(query, which_result=which_result, buffer_dist=buffer_dist)
        name = query
    elif isinstance(query, list):
        # if it is a list, it contains multiple places to get
        gdf_place = gdf_from_places(query, buffer_dist=buffer_dist)
    else:
        raise TypeError('query must be a string or a list of query strings')

    # extract the geometry from the GeoDataFrame to use in API query
    polygon = gdf_place['geometry'].unary_union
    log('Constructed place geometry polygon(s) to query API')

    # create graph using this polygon(s) geometry
    G = graph_from_polygon(polygon, network_type=network_type, simplify=simplify,
                           retain_all=retain_all, truncate_by_edge=truncate_by_edge,
                           name=name, timeout=timeout, memory=memory,
                           max_query_area_size=max_query_area_size,
                           clean_periphery=clean_periphery, infrastructure=infrastructure,
                           custom_filter=custom_filter)

    log('graph_from_place() returning graph with {:,} nodes and {:,} edges'.format(len(list(G.nodes())), len(list(G.edges()))))
    return G

Response: Create a networkx graph from OSM data within a given place.


Instruction:
Can you tell what is the following Python 3 function doing
def graph_from_file(filename, bidirectional=False, simplify=True,
                    retain_all=False, name='unnamed'):
    """
    Create a networkx graph from OSM data in an XML file.

    Parameters
    ----------
    filename : string
        the name of a file containing OSM XML data
    bidirectional : bool
        if True, create bidirectional edges for one-way streets
    simplify : bool
        if True, simplify the graph topology
    retain_all : bool
        if True, return the entire graph even if it is not connected
    name : string
        the name of the graph

    Returns
    -------
    networkx multidigraph
    """
    # transmogrify file of OSM XML data into JSON
    response_jsons = [overpass_json_from_file(filename)]

    # create graph using this response JSON
    G = create_graph(response_jsons, bidirectional=bidirectional,
                     retain_all=retain_all, name=name)

    # simplify the graph topology as the last step.
    if simplify:
        G = simplify_graph(G)

    log('graph_from_file() returning graph with {:,} nodes and {:,} edges'.format(len(list(G.nodes())), len(list(G.edges()))))
    return G

Response: Create a networkx graph from OSM XML data in an XML file.


Instruction:
Implement a function in Python 3 to
download OpenStreetMap footprints from the server.

Response:
def osm_footprints_download(polygon=None, north=None, south=None, east=None, west=None,
                            footprint_type='building', timeout=180, memory=None,
                            max_query_area_size=50*1000*50*1000):
    """
    Download OpenStreetMap footprint data.

    Parameters
    ----------
    polygon : shapely Polygon or MultiPolygon
        geographic shape to fetch the footprints within
    north : float
        northern latitude of bounding box
    south : float
        southern latitude of bounding box
    east : float
        eastern longitude of bounding box
    west : float
        western longitude of bounding box
    footprint_type : string
        type of footprint to be downloaded. OSM tag key e.g. 'building', 'landuse', 'place', etc.
    timeout : int
        the timeout interval for requests and to pass to API
    memory : int
        server memory allocation size for the query, in bytes. If none, server
        will use its default allocation size
    max_query_area_size : float
        max area for any part of the geometry, in the units the geometry is in:
        any polygon bigger will get divided up for multiple queries to API
        (default is 50,000 * 50,000 units (ie, 50km x 50km in area, if units are
        meters))

    Returns
    -------
    list
        list of response_json dicts
    """

    # check if we're querying by polygon or by bounding box based on which
    # argument(s) where passed into this function
    by_poly = polygon is not None
    by_bbox = not (north is None or south is None or east is None or west is None)
    if not (by_poly or by_bbox):
        raise ValueError('You must pass a polygon or north, south, east, and west')

    response_jsons = []

    # pass server memory allocation in bytes for the query to the API
    # if None, pass nothing so the server will use its default allocation size
    # otherwise, define the query's maxsize parameter value as whatever the
    # caller passed in
    if memory is None:
        maxsize = ''
    else:
        maxsize = '[maxsize:{}]'.format(memory)

    # define the query to send the API
    if by_bbox:
        # turn bbox into a polygon and project to local UTM
        polygon = Polygon([(west, south), (east, south), (east, north), (west, north)])
        geometry_proj, crs_proj = project_geometry(polygon)

        # subdivide it if it exceeds the max area size (in meters), then project
        # back to lat-long
        geometry_proj_consolidated_subdivided = consolidate_subdivide_geometry(geometry_proj, max_query_area_size=max_query_area_size)
        geometry, _ = project_geometry(geometry_proj_consolidated_subdivided, crs=crs_proj, to_latlong=True)
        log('Requesting footprints data within bounding box from API in {:,} request(s)'.format(len(geometry)))
        start_time = time.time()

        # loop through each polygon rectangle in the geometry (there will only
        # be one if original bbox didn't exceed max area size)
        for poly in geometry:
            # represent bbox as south,west,north,east and round lat-longs to 8
            # decimal places (ie, within 1 mm) so URL strings aren't different
            # due to float rounding issues (for consistent caching)
            west, south, east, north = poly.bounds
            query_template = ('[out:json][timeout:{timeout}]{maxsize};'
                              '((way["{footprint_type}"]({south:.8f},{west:.8f},{north:.8f},{east:.8f});'
                              '(._;>;););'
                              '(relation["{footprint_type}"]({south:.8f},{west:.8f},{north:.8f},{east:.8f});'
                              '(._;>;);););out;')
            query_str = query_template.format(north=north, south=south, east=east, west=west, timeout=timeout,
                                              maxsize=maxsize, footprint_type=footprint_type)
            response_json = overpass_request(data={'data':query_str}, timeout=timeout)
            response_jsons.append(response_json)
        msg = ('Got all footprint data within bounding box from '
               'API in {:,} request(s) and {:,.2f} seconds')
        log(msg.format(len(geometry), time.time()-start_time))

    elif by_poly:
        # project to utm, divide polygon up into sub-polygons if area exceeds a
        # max size (in meters), project back to lat-long, then get a list of polygon(s) exterior coordinates
        geometry_proj, crs_proj = project_geometry(polygon)
        geometry_proj_consolidated_subdivided = consolidate_subdivide_geometry(geometry_proj, max_query_area_size=max_query_area_size)
        geometry, _ = project_geometry(geometry_proj_consolidated_subdivided, crs=crs_proj, to_latlong=True)
        polygon_coord_strs = get_polygons_coordinates(geometry)
        log('Requesting footprint data within polygon from API in {:,} request(s)'.format(len(polygon_coord_strs)))
        start_time = time.time()

        # pass each polygon exterior coordinates in the list to the API, one at
        # a time
        for polygon_coord_str in polygon_coord_strs:
            query_template = ('[out:json][timeout:{timeout}]{maxsize};('
                              'way(poly:"{polygon}")["{footprint_type}"];(._;>;);'
                              'relation(poly:"{polygon}")["{footprint_type}"];(._;>;););out;')
            query_str = query_template.format(polygon=polygon_coord_str, timeout=timeout, maxsize=maxsize,
                                              footprint_type=footprint_type)
            response_json = overpass_request(data={'data':query_str}, timeout=timeout)
            response_jsons.append(response_json)
        msg = ('Got all footprint data within polygon from API in '
               '{:,} request(s) and {:,.2f} seconds')
        log(msg.format(len(polygon_coord_strs), time.time()-start_time))

    return response_jsons


Instruction:
Implement a function in Python 3 to
create a GeoDataFrame containing the footprints from OSM.

Response:
def create_footprints_gdf(polygon=None, north=None, south=None, east=None, west=None,
                          footprint_type='building', retain_invalid=False):
    """
    Get footprint data from OSM then assemble it into a GeoDataFrame.

    Parameters
    ----------
    polygon : shapely Polygon or MultiPolygon
        geographic shape to fetch the footprints within
    north : float
        northern latitude of bounding box
    south : float
        southern latitude of bounding box
    east : float
        eastern longitude of bounding box
    west : float
        western longitude of bounding box
    footprint_type : string
        type of footprint to be downloaded. OSM tag key e.g. 'building', 'landuse', 'place', etc.
    retain_invalid : bool
        if False discard any footprints with an invalid geometry

    Returns
    -------
    GeoDataFrame
    """

    responses = osm_footprints_download(polygon, north, south, east, west, footprint_type)

    # list of polygons to removed at the end of the process
    pop_list = []

    vertices = {}
    for response in responses:
        for result in response['elements']:
            if 'type' in result and result['type']=='node':
                vertices[result['id']] = {'lat' : result['lat'],
                                          'lon' : result['lon']}

    footprints = {}
    for response in responses:
        for result in response['elements']:
            if 'type' in result and result['type']=='way':
                nodes = result['nodes']
                try:
                    polygon = Polygon([(vertices[node]['lon'], vertices[node]['lat']) for node in nodes])
                except Exception:
                    log('Polygon has invalid geometry: {}'.format(nodes))
                footprint = {'nodes' : nodes,
                            'geometry' : polygon}

                if 'tags' in result:
                    for tag in result['tags']:
                        footprint[tag] = result['tags'][tag]

                # if polygons are untagged or not tagged with the footprint_type
                # add them to pop_list to be removed from the final dictionary
                if 'tags' not in result:
                    pop_list.append(result['id'])
                elif footprint_type not in result['tags']:
                    pop_list.append(result['id'])

                footprints[result['id']] = footprint

    # Create multipolygon footprints and pop untagged supporting polygons from footprints
    for response in responses:
        for result in response['elements']:
            if 'type' in result and result['type']=='relation':

                outer_polys = []
                inner_polys = []
                multipoly = []

                for member in result['members']:
                    if 'role' in member and member['role']=='outer':
                        outer_polys.append(member['ref'])
                    if 'role' in member and member['role']=='inner':
                        inner_polys.append(member['ref'])

                # osm allows multiple outer polygons in a relation
                for outer_poly in outer_polys:
                    temp_poly=footprints[outer_poly]['geometry']

                    for inner_poly in inner_polys:
                        temp_poly=temp_poly.difference(footprints[inner_poly]['geometry'])

                    multipoly.append(temp_poly)

                footprint = {'geometry' : MultiPolygon(multipoly)}

                if 'tags' in result:
                    for tag in result['tags']:
                        footprint[tag] = result['tags'][tag]

                footprints[result['id']] = footprint

    # remove supporting geometry from footprints dictionary
    for item in pop_list:
        footprints.pop(item)

    gdf = gpd.GeoDataFrame(footprints).T
    gdf.crs = settings.default_crs

    if not retain_invalid:
        # drop all invalid geometries
        gdf = gdf[gdf['geometry'].is_valid]

    return gdf


Instruction:
Can you create a Python 3 function that
returns a GeoDataFrame containing footprints within some distance north south east and west of a point.

Response:
def footprints_from_point(point, distance, footprint_type='building', retain_invalid=False):
    """
    Get footprints within some distance north, south, east, and west of
    a lat-long point.

    Parameters
    ----------
    point : tuple
        a lat-long point
    distance : numeric
        distance in meters
    footprint_type : string
        type of footprint to be downloaded. OSM tag key e.g. 'building', 'landuse', 'place', etc.
    retain_invalid : bool
        if False discard any footprints with an invalid geometry

    Returns
    -------
    GeoDataFrame
    """

    bbox = bbox_from_point(point=point, distance=distance)
    north, south, east, west = bbox
    return create_footprints_gdf(north=north, south=south, east=east, west=west,
                                 footprint_type=footprint_type, retain_invalid=retain_invalid)


Instruction:
Create a Python 3 function for
getting footprints within some distance north south east and west of a node.

Response:
def footprints_from_address(address, distance, footprint_type='building', retain_invalid=False):
    """
    Get footprints within some distance north, south, east, and west of
    an address.

    Parameters
    ----------
    address : string
        the address to geocode to a lat-long point
    distance : numeric
        distance in meters
    footprint_type : string
        type of footprint to be downloaded. OSM tag key e.g. 'building', 'landuse', 'place', etc.
    retain_invalid : bool
        if False discard any footprints with an invalid geometry

    Returns
    -------
    GeoDataFrame
    """

    # geocode the address string to a (lat, lon) point
    point = geocode(query=address)

    # get footprints within distance of this point
    return footprints_from_point(point, distance, footprint_type=footprint_type,
                                 retain_invalid=retain_invalid)


Instruction:
Write a Python 3 function that can
return a GeoDataFrame containing footprints within some polygon.

Response:
def footprints_from_polygon(polygon, footprint_type='building', retain_invalid=False):
    """
    Get footprints within some polygon.

    Parameters
    ----------
    polygon : shapely Polygon or MultiPolygon
        the shape to get data within. coordinates should be in units of
        latitude-longitude degrees.
    footprint_type : string
        type of footprint to be downloaded. OSM tag key e.g. 'building', 'landuse', 'place', etc.
    retain_invalid : bool
        if False discard any footprints with an invalid geometry

    Returns
    -------
    GeoDataFrame
    """

    return create_footprints_gdf(polygon=polygon, footprint_type=footprint_type,
                                 retain_invalid=retain_invalid)


Instruction:
Here you have a function in Python 3, explain what it does
def footprints_from_place(place, footprint_type='building', retain_invalid=False):

    city = gdf_from_place(place)
    polygon = city['geometry'].iloc[0]
    return create_footprints_gdf(polygon, retain_invalid=retain_invalid,
                                 footprint_type=footprint_type)

Response: Returns a list of footprints within the boundaries of some place.


Instruction:
Can you generate the documentation for the following Python 3 function
def plot_footprints(gdf, fig=None, ax=None, figsize=None, color='#333333', bgcolor='w',
                   set_bounds=True, bbox=None, save=False, show=True, close=False,
                   filename='image', file_format='png', dpi=600):
    """
    Plot a GeoDataFrame of footprints.

    Parameters
    ----------
    gdf : GeoDataFrame
        footprints
    fig : figure
    ax : axis
    figsize : tuple
    color : string
        the color of the footprints
    bgcolor : string
        the background color of the plot
    set_bounds : bool
        if True, set bounds from either passed-in bbox or the spatial extent of the gdf
    bbox : tuple
        if True and if set_bounds is True, set the display bounds to this bbox
    save : bool
        whether to save the figure to disk or not
    show : bool
        whether to display the figure or not
    close : bool
        close the figure (only if show equals False) to prevent display
    filename : string
        the name of the file to save
    file_format : string
        the format of the file to save (e.g., 'jpg', 'png', 'svg')
    dpi : int
        the resolution of the image file if saving

    Returns
    -------
    fig, ax : tuple

    """

    if fig is None or ax is None:
        fig, ax = plt.subplots(figsize=figsize, facecolor=bgcolor)
        ax.set_facecolor(bgcolor)

    # extract each polygon as a descartes patch, and add to a matplotlib patch
    # collection
    patches = []
    for geometry in gdf['geometry']:
        if isinstance(geometry, Polygon):
            patches.append(PolygonPatch(geometry))
        elif isinstance(geometry, MultiPolygon):
            for subpolygon in geometry: #if geometry is multipolygon, go through each constituent subpolygon
                patches.append(PolygonPatch(subpolygon))
    pc = PatchCollection(patches, facecolor=color, edgecolor=color, linewidth=0, alpha=1)
    ax.add_collection(pc)

    if set_bounds:
        if bbox is None:
            # set the figure bounds to the polygons' bounds
            left, bottom, right, top = gdf.total_bounds
        else:
            top, bottom, right, left = bbox
        ax.set_xlim((left, right))
        ax.set_ylim((bottom, top))

    # turn off the axis display set the margins to zero and point the ticks in
    # so there's no space around the plot
    ax.axis('off')
    ax.margins(0)
    ax.tick_params(which='both', direction='in')
    fig.canvas.draw()

    # make everything square
    ax.set_aspect('equal')
    fig.canvas.draw()

    fig, ax = save_and_show(fig=fig, ax=ax, save=save, show=show, close=close,
                            filename=filename, file_format=file_format, dpi=dpi, axis_off=True)

    return fig, ax

Response: Plots a GeoDataFrame of footprints.


Instruction:
Can you generate a brief explanation for the following Python 3 code
def add_node_elevations(G, api_key, max_locations_per_batch=350,
                        pause_duration=0.02): # pragma: no cover
    """
    Get the elevation (meters) of each node in the network and add it to the
    node as an attribute.

    Parameters
    ----------
    G : networkx multidigraph
    api_key : string
        your google maps elevation API key
    max_locations_per_batch : int
        max number of coordinate pairs to submit in each API call (if this is
        too high, the server will reject the request because its character
        limit exceeds the max)
    pause_duration : float
        time to pause between API calls

    Returns
    -------
    G : networkx multidigraph
    """

    # google maps elevation API endpoint
    url_template = 'https://maps.googleapis.com/maps/api/elevation/json?locations={}&key={}'

    # make a pandas series of all the nodes' coordinates as 'lat,lng'
    # round coorindates to 5 decimal places (approx 1 meter) to be able to fit
    # in more locations per API call
    node_points = pd.Series({node:'{:.5f},{:.5f}'.format(data['y'], data['x']) for node, data in G.nodes(data=True)})
    log('Requesting node elevations from the API in {} calls.'.format(math.ceil(len(node_points) / max_locations_per_batch)))

    # break the series of coordinates into chunks of size max_locations_per_batch
    # API format is locations=lat,lng|lat,lng|lat,lng|lat,lng...
    results = []
    for i in range(0, len(node_points), max_locations_per_batch):
        chunk = node_points.iloc[i : i + max_locations_per_batch]
        locations = '|'.join(chunk)
        url = url_template.format(locations, api_key)

        # check if this request is already in the cache (if global use_cache=True)
        cached_response_json = get_from_cache(url)
        if cached_response_json is not None:
            response_json = cached_response_json
        else:
            try:
                # request the elevations from the API
                log('Requesting node elevations: {}'.format(url))
                time.sleep(pause_duration)
                response = requests.get(url)
                response_json = response.json()
                save_to_cache(url, response_json)
            except Exception as e:
                log(e)
                log('Server responded with {}: {}'.format(response.status_code, response.reason))

        # append these elevation results to the list of all results
        results.extend(response_json['results'])

    # sanity check that all our vectors have the same number of elements
    if not (len(results) == len(G.nodes()) == len(node_points)):
        raise Exception('Graph has {} nodes but we received {} results from the elevation API.'.format(len(G.nodes()), len(results)))
    else:
        log('Graph has {} nodes and we received {} results from the elevation API.'.format(len(G.nodes()), len(results)))

    # add elevation as an attribute to the nodes
    df = pd.DataFrame(node_points, columns=['node_points'])
    df['elevation'] = [result['elevation'] for result in results]
    df['elevation'] = df['elevation'].round(3) # round to millimeter
    nx.set_node_attributes(G, name='elevation', values=df['elevation'].to_dict())
    log('Added elevation data to all nodes.')

    return G

Response: Add node elevations to the multidigraph G.


Instruction:
Given the following Python 3 function, write the documentation
def add_edge_grades(G, add_absolute=True): # pragma: no cover

    # for each edge, calculate the difference in elevation from origin to
    # destination, then divide by edge length
    for u, v, data in G.edges(keys=False, data=True):
        elevation_change = G.nodes[v]['elevation'] - G.nodes[u]['elevation']

        # round to ten-thousandths decimal place
        grade = round(elevation_change / data['length'], 4)
        data['grade'] = grade
        if add_absolute:
            data['grade_abs'] = abs(grade)

    log('Added grade data to all edges.')
    return G

Response: Adds the directed grade to each edge in the network and returns the resulting networkx multidigraph.


Instruction:
How would you explain what the following Python 3 function does
def save_gdf_shapefile(gdf, filename=None, folder=None):

    if folder is None:
        folder = settings.data_folder

    if filename is None:
        filename = make_shp_filename(gdf.gdf_name)

    # give the save folder a filename subfolder to make the full path to the
    # files
    folder_path = os.path.join(folder, filename)

    # make everything but geometry column a string
    for col in [c for c in gdf.columns if not c == 'geometry']:
        gdf[col] = gdf[col].fillna('').map(make_str)

    # if the save folder does not already exist, create it with a filename
    # subfolder
    if not os.path.exists(folder_path):
        os.makedirs(folder_path)
    gdf.to_file(folder_path)

    if not hasattr(gdf, 'gdf_name'):
        gdf.gdf_name = 'unnamed'
    log('Saved the GeoDataFrame "{}" as shapefile "{}"'.format(gdf.gdf_name, folder_path))

Response:
Save a GeoDataFrame of place shapes or footprints as an ESRI
    shapefile.


Instruction:
Make a summary of the following Python 3 code
def save_graph_shapefile(G, filename='graph', folder=None, encoding='utf-8'):

    start_time = time.time()
    if folder is None:
        folder = settings.data_folder

    # convert directed graph G to an undirected graph for saving as a shapefile
    G_save = get_undirected(G.copy())

    # create a GeoDataFrame of the nodes and set CRS
    nodes, data = zip(*G_save.nodes(data=True))
    gdf_nodes = gpd.GeoDataFrame(list(data), index=nodes)
    gdf_nodes.crs = G_save.graph['crs']

    # create a geometry column then drop the x and y columns
    gdf_nodes['geometry'] = gdf_nodes.apply(lambda row: Point(row['x'], row['y']), axis=1)
    gdf_nodes = gdf_nodes.drop(['x', 'y'], axis=1)

    # make everything but geometry column a string
    for col in [c for c in gdf_nodes.columns if not c == 'geometry']:
        gdf_nodes[col] = gdf_nodes[col].fillna('').map(make_str)

    # create a list to hold our edges, then loop through each edge in the graph
    edges = []
    for u, v, key, data in G_save.edges(keys=True, data=True):

        # for each edge, add key and all attributes in data dict to the
        # edge_details
        edge_details = {'key':key}
        for attr_key in data:
            edge_details[attr_key] = data[attr_key]

        # if edge doesn't already have a geometry attribute, create one now
        if 'geometry' not in data:
            point_u = Point((G_save.nodes[u]['x'], G_save.nodes[u]['y']))
            point_v = Point((G_save.nodes[v]['x'], G_save.nodes[v]['y']))
            edge_details['geometry'] = LineString([point_u, point_v])

        edges.append(edge_details)

    # create a geodataframe from the list of edges and set the CRS
    gdf_edges = gpd.GeoDataFrame(edges)
    gdf_edges.crs = G_save.graph['crs']

    # make everything but geometry column a string
    for col in [c for c in gdf_edges.columns if not c == 'geometry']:
        gdf_edges[col] = gdf_edges[col].fillna('').map(make_str)

    # if the save folder does not already exist, create it with a filename
    # subfolder
    folder = os.path.join(folder, filename)
    if not os.path.exists(folder):
        os.makedirs(folder)

    # save the nodes and edges as separate ESRI shapefiles
    gdf_nodes.to_file('{}/nodes'.format(folder), encoding=encoding)
    gdf_edges.to_file('{}/edges'.format(folder), encoding=encoding)
    log('Saved graph "{}" to disk as shapefiles at "{}" in {:,.2f} seconds'.format(G_save.name, folder, time.time()-start_time))

Response: Save a networkx multidigraph G as ESRI shapefiles to disk.


Instruction:
Implement a function in Python 3 to
save a networkx graph as an OSM XML formatted file.

Response:
def save_graph_osm(G, node_tags=settings.osm_xml_node_tags,
                   node_attrs=settings.osm_xml_node_attrs,
                   edge_tags=settings.osm_xml_way_tags,
                   edge_attrs=settings.osm_xml_way_attrs,
                   oneway=True, filename='graph.osm',
                   folder=None):
    """
    Save a graph as an OSM XML formatted file. NOTE: for very large
    networks this method can take upwards of 30+ minutes to finish.

    Parameters
    __________
    G : networkx multidigraph or multigraph
    filename : string
        the name of the osm file (including file extension)
    folder : string
        the folder to contain the file, if None, use default data folder

    Returns
    -------
    None
    """
    start_time = time.time()
    if folder is None:
        folder = settings.data_folder

    # create a copy to convert all the node/edge attribute values to string
    G_save = G.copy()

    gdf_nodes, gdf_edges = graph_to_gdfs(
        G_save, node_geometry=False, fill_edge_geometry=False)

    # rename columns per osm specification
    gdf_nodes.rename(
        columns={'osmid': 'id', 'x': 'lon', 'y': 'lat'}, inplace=True)
    if 'uniqueid' in gdf_edges.columns:
        gdf_edges = gdf_edges.rename(columns={'uniqueid': 'id'})
    else:
        gdf_edges = gdf_edges.reset_index().rename(columns={'index': 'id'})

    # add default values for required attributes
    for table in [gdf_nodes, gdf_edges]:
        table['uid'] = '1'
        table['user'] = 'osmnx'
        table['version'] = '1'
        table['changeset'] = '1'
        table['timestamp'] = '2017-01-01T00:00:00Z'

    # convert all datatypes to str
    nodes = gdf_nodes.applymap(str)
    edges = gdf_edges.applymap(str)

    # misc. string replacements to meet OSM XML spec
    if 'oneway' in edges.columns:
        edges.loc[:, 'oneway'] = oneway
        edges.loc[:, 'oneway'] = edges['oneway'].astype(str)
        edges.loc[:, 'oneway'] = edges['oneway'].str.replace(
            'False', 'no').replace('True', 'yes')

    # initialize XML tree with an OSM root element
    root = etree.Element('osm')

    # append nodes to the XML tree
    for i, row in nodes.iterrows():
        node = etree.SubElement(
            root, 'node', attrib=row[node_attrs].dropna().to_dict())
        for tag in node_tags:
            etree.SubElement(
                node, 'tag', attrib={'k': tag, 'v': row[tag]})

    # append edges to the XML tree
    for i, row in edges.iterrows():
        edge = etree.SubElement(
            root, 'way', attrib=row[edge_attrs].dropna().to_dict())
        etree.SubElement(edge, 'nd', attrib={'ref': row['u']})
        etree.SubElement(edge, 'nd', attrib={'ref': row['v']})
        for tag in edge_tags:
            etree.SubElement(
                edge, 'tag', attrib={'k': tag, 'v': row[tag]})

    et = etree.ElementTree(root)

    if not os.path.exists(folder):
        os.makedirs(folder)

    et.write(os.path.join(folder, filename))

    log('Saved graph "{}" to disk as OSM at "{}" in {:,.2f} seconds'.format(
        G_save.name, os.path.join(folder, filename), time.time() - start_time))


Instruction:
Can you implement a function in Python 3 that
saves a networkx multidigraph to disk.

Response:
def save_graphml(G, filename='graph.graphml', folder=None, gephi=False):
    """
    Save graph as GraphML file to disk.

    Parameters
    ----------
    G : networkx multidigraph
    filename : string
        the name of the graphml file (including file extension)
    folder : string
        the folder to contain the file, if None, use default data folder
    gephi : bool
        if True, give each edge a unique key to work around Gephi's
        restrictive interpretation of the GraphML specification

    Returns
    -------
    None
    """

    start_time = time.time()
    if folder is None:
        folder = settings.data_folder

    # create a copy to convert all the node/edge attribute values to string
    G_save = G.copy()

    if gephi:

        gdf_nodes, gdf_edges = graph_to_gdfs(G_save, nodes=True, edges=True, node_geometry=True,
                                             fill_edge_geometry=True)

        # turn each edge's key into a unique ID for Gephi compatibility
        gdf_edges['key'] = range(len(gdf_edges))

        # gephi doesn't handle node attrs named x and y well, so rename
        gdf_nodes['xcoord'] = gdf_nodes['x']
        gdf_nodes['ycoord'] = gdf_nodes['y']
        G_save = gdfs_to_graph(gdf_nodes, gdf_edges)

        # remove graph attributes as Gephi only accepts node and edge attrs
        G_save.graph = {}

    else:
        # if not gephi, keep graph attrs and stringify them
        for dict_key in G_save.graph:
            # convert all the graph attribute values to strings
            G_save.graph[dict_key] = make_str(G_save.graph[dict_key])

    # stringify node and edge attributes
    for _, data in G_save.nodes(data=True):
        for dict_key in data:
            if gephi and dict_key in ['xcoord', 'ycoord']:
                # don't convert x y values to string if saving for gephi
                continue
            else:
                # convert all the node attribute values to strings
                data[dict_key] = make_str(data[dict_key])

    for _, _, data in G_save.edges(keys=False, data=True):
        for dict_key in data:
            # convert all the edge attribute values to strings
            data[dict_key] = make_str(data[dict_key])

    if not os.path.exists(folder):
        os.makedirs(folder)

    nx.write_graphml(G_save, os.path.join(folder, filename))
    log('Saved graph "{}" to disk as GraphML at "{}" in {:,.2f} seconds'.format(G_save.name, os.path.join(folder, filename), time.time()-start_time))


Instruction:
Write a Python 3 script to
load a GraphML file from disk and convert the node and edge attributes to correct data types.

Response:
def load_graphml(filename, folder=None, node_type=int):
    """
    Load a GraphML file from disk and convert the node/edge attributes to
    correct data types.

    Parameters
    ----------
    filename : string
        the name of the graphml file (including file extension)
    folder : string
        the folder containing the file, if None, use default data folder
    node_type : type
        (Python type (default: int)) - Convert node ids to this type

    Returns
    -------
    networkx multidigraph
    """
    start_time = time.time()

    # read the graph from disk
    if folder is None:
        folder = settings.data_folder
    path = os.path.join(folder, filename)
    G = nx.MultiDiGraph(nx.read_graphml(path, node_type=node_type))

    # convert graph crs attribute from saved string to correct dict data type
    G.graph['crs'] = ast.literal_eval(G.graph['crs'])

    if 'streets_per_node' in G.graph:
        G.graph['streets_per_node'] = ast.literal_eval(G.graph['streets_per_node'])

    # convert numeric node tags from string to numeric data types
    log('Converting node and edge attribute data types')
    for _, data in G.nodes(data=True):
        data['osmid'] = node_type(data['osmid'])
        data['x'] = float(data['x'])
        data['y'] = float(data['y'])

    # convert numeric, bool, and list node tags from string to correct data types
    for _, _, data in G.edges(data=True, keys=False):

        # first parse oneway to bool and length to float - they should always
        # have only 1 value each
        data['oneway'] = ast.literal_eval(data['oneway'])
        data['length'] = float(data['length'])

        # these attributes might have a single value, or a list if edge's
        # topology was simplified
        for attr in ['highway', 'name', 'bridge', 'tunnel', 'lanes', 'ref', 'maxspeed', 'service', 'access', 'area', 'landuse', 'width', 'est_width']:
            # if this edge has this attribute, and it starts with '[' and ends
            # with ']', then it's a list to be parsed
            if attr in data and data[attr][0] == '[' and data[attr][-1] == ']':
                # try to convert the string list to a list type, else leave as
                # single-value string (and leave as string if error)
                try:
                    data[attr] = ast.literal_eval(data[attr])
                except:
                    pass

        # osmid might have a single value or a list
        if 'osmid' in data:
            if data['osmid'][0] == '[' and data['osmid'][-1] == ']':
                # if it's a list, eval the list then convert each element to node_type
                data['osmid'] = [node_type(i) for i in ast.literal_eval(data['osmid'])]
            else:
                # if it's not a list, convert it to the node_type
                data['osmid'] = node_type(data['osmid'])

        # if geometry attribute exists, load the string as well-known text to
        # shapely LineString
        if 'geometry' in data:
            data['geometry'] = wkt.loads(data['geometry'])

    # remove node_default and edge_default metadata keys if they exist
    if 'node_default' in G.graph:
        del G.graph['node_default']
    if 'edge_default' in G.graph:
        del G.graph['edge_default']

    log('Loaded graph with {:,} nodes and {:,} edges in {:,.2f} seconds from "{}"'.format(len(list(G.nodes())),
                                                                                len(list(G.edges())),
                                                                                time.time()-start_time,
                                                                                path))
    return G


Instruction:
Explain what the following Python 3 code does
def is_duplicate_edge(data, data_other):

    is_dupe = False

    # if either edge's OSM ID contains multiple values (due to simplification), we want
    # to compare as sets so they are order-invariant, otherwise uv does not match vu
    osmid = set(data['osmid']) if isinstance(data['osmid'], list) else data['osmid']
    osmid_other = set(data_other['osmid']) if isinstance(data_other['osmid'], list) else data_other['osmid']

    if osmid == osmid_other:
        # if they contain the same OSM ID or set of OSM IDs (due to simplification)
        if ('geometry' in data) and ('geometry' in data_other):
            # if both edges have a geometry attribute
            if is_same_geometry(data['geometry'], data_other['geometry']):
                # if their edge geometries have the same coordinates
                is_dupe = True
        elif ('geometry' in data) and ('geometry' in data_other):
            # if neither edge has a geometry attribute
            is_dupe = True
        else:
            # if one edge has geometry attribute but the other doesn't, keep it
            pass

    return is_dupe

Response: Checks if two edge data dictionaries are the same based on OSM ID and geometry attributes.


Instruction:
Can you write a function in Python 3 where it
checks if the geometries in two edges are the same in normal or reversed order of points.

Response:
def is_same_geometry(ls1, ls2):
    """
    Check if LineString geometries in two edges are the same, in
    normal or reversed order of points.

    Parameters
    ----------
    ls1 : LineString
        the first edge's geometry
    ls2 : LineString
        the second edge's geometry

    Returns
    -------
    bool
    """

    # extract geometries from each edge data dict
    geom1 = [list(coords) for coords in ls1.xy]
    geom2 = [list(coords) for coords in ls2.xy]

    # reverse the first edge's list of x's and y's to look for a match in
    # either order
    geom1_r = [list(reversed(list(coords))) for coords in ls1.xy]

    # if the edge's geometry matches its reverse's geometry in either order,
    # return True
    return (geom1 == geom2 or geom1_r == geom2)


Instruction:
Implement a function in Python 3 to
update the keys of edges that share a u v with another edge but differ in geometry.

Response:
def update_edge_keys(G):
    """
    Update the keys of edges that share a u, v with another edge but differ in
    geometry. For example, two one-way streets from u to v that bow away from
    each other as separate streets, rather than opposite direction edges of a
    single street.

    Parameters
    ----------
    G : networkx multidigraph

    Returns
    -------
    networkx multigraph
    """

    # identify all the edges that are duplicates based on a sorted combination
    # of their origin, destination, and key. that is, edge uv will match edge vu
    # as a duplicate, but only if they have the same key
    edges = graph_to_gdfs(G, nodes=False, fill_edge_geometry=False)
    edges['uvk'] = edges.apply(lambda row: '_'.join(sorted([str(row['u']), str(row['v'])]) + [str(row['key'])]), axis=1)
    edges['dupe'] = edges['uvk'].duplicated(keep=False)
    dupes = edges[edges['dupe']==True].dropna(subset=['geometry'])

    different_streets = []
    groups = dupes[['geometry', 'uvk', 'u', 'v', 'key', 'dupe']].groupby('uvk')

    # for each set of duplicate edges
    for label, group in groups:

        # if there are more than 2 edges here, make sure to compare all
        if len(group['geometry']) > 2:
            l = group['geometry'].tolist()
            l.append(l[0])
            geom_pairs = list(zip(l[:-1], l[1:]))
        # otherwise, just compare the first edge to the second edge
        else:
            geom_pairs = [(group['geometry'].iloc[0], group['geometry'].iloc[1])]

        # for each pair of edges to compare
        for geom1, geom2 in geom_pairs:
            # if they don't have the same geometry, flag them as different streets
            if not is_same_geometry(geom1, geom2):
                # add edge uvk, but not edge vuk, otherwise we'll iterate both their keys
                # and they'll still duplicate each other at the end of this process
                different_streets.append((group['u'].iloc[0], group['v'].iloc[0], group['key'].iloc[0]))

    # for each unique different street, iterate its key + 1 so it's unique
    for u, v, k in set(different_streets):
        # filter out key if it appears in data dict as we'll pass it explicitly
        attributes = {k:v for k, v in G[u][v][k].items() if k != 'key'}
        G.add_edge(u, v, key=k+1, **attributes)
        G.remove_edge(u, v, key=k)

    return G


Instruction:
Write a Python 3 function that can
convert a directed graph to an undirected graph that maintains parallel edges if geometries differ.

Response:
def get_undirected(G):
    """
    Convert a directed graph to an undirected graph that maintains parallel
    edges if geometries differ.

    Parameters
    ----------
    G : networkx multidigraph

    Returns
    -------
    networkx multigraph
    """

    start_time = time.time()

    # set from/to nodes before making graph undirected
    G = G.copy()
    for u, v, k, data in G.edges(keys=True, data=True):
        G.edges[u, v, k]['from'] = u
        G.edges[u, v, k]['to'] = v

        # add geometry if it doesn't already exist, to retain parallel
        # edges' distinct geometries
        if 'geometry' not in data:
            point_u = Point((G.nodes[u]['x'], G.nodes[u]['y']))
            point_v = Point((G.nodes[v]['x'], G.nodes[v]['y']))
            data['geometry'] = LineString([point_u, point_v])

    # update edge keys so we don't retain only one edge of sets of parallel edges
    # when we convert from a multidigraph to a multigraph
    G = update_edge_keys(G)

    # now convert multidigraph to a multigraph, retaining all edges in both
    # directions for now, as well as all graph attributes
    H = nx.MultiGraph()
    H.add_nodes_from(G.nodes(data=True))
    H.add_edges_from(G.edges(keys=True, data=True))
    H.graph = G.graph
    H.name = G.name

    # the previous operation added all directed edges from G as undirected
    # edges in H. this means we have duplicate edges for every bi-directional
    # street. so, look through the edges and remove any duplicates
    duplicate_edges = []
    for u, v, key, data in H.edges(keys=True, data=True):

        # if we haven't already flagged this edge as a duplicate
        if not (u, v, key) in duplicate_edges:

            # look at every other edge between u and v, one at a time
            for key_other in H[u][v]:

                # don't compare this edge to itself
                if not key_other == key:

                    # compare the first edge's data to the second's to see if
                    # they are duplicates
                    data_other = H.edges[u, v, key_other]
                    if is_duplicate_edge(data, data_other):

                        # if they match up, flag the duplicate for removal
                        duplicate_edges.append((u, v, key_other))

    H.remove_edges_from(duplicate_edges)
    log('Made undirected graph in {:,.2f} seconds'.format(time.time() - start_time))

    return H


Instruction:
Can you implement a function in Python 3 that
converts a networkx multidigraph into a list of GeoDataFrames.

Response:
def graph_to_gdfs(G, nodes=True, edges=True, node_geometry=True, fill_edge_geometry=True):
    """
    Convert a graph into node and/or edge GeoDataFrames

    Parameters
    ----------
    G : networkx multidigraph
    nodes : bool
        if True, convert graph nodes to a GeoDataFrame and return it
    edges : bool
        if True, convert graph edges to a GeoDataFrame and return it
    node_geometry : bool
        if True, create a geometry column from node x and y data
    fill_edge_geometry : bool
        if True, fill in missing edge geometry fields using origin and
        destination nodes

    Returns
    -------
    GeoDataFrame or tuple
        gdf_nodes or gdf_edges or both as a tuple
    """

    if not (nodes or edges):
        raise ValueError('You must request nodes or edges, or both.')

    to_return = []

    if nodes:

        start_time = time.time()

        nodes, data = zip(*G.nodes(data=True))
        gdf_nodes = gpd.GeoDataFrame(list(data), index=nodes)
        if node_geometry:
            gdf_nodes['geometry'] = gdf_nodes.apply(lambda row: Point(row['x'], row['y']), axis=1)
        gdf_nodes.crs = G.graph['crs']
        gdf_nodes.gdf_name = '{}_nodes'.format(G.graph['name'])

        to_return.append(gdf_nodes)
        log('Created GeoDataFrame "{}" from graph in {:,.2f} seconds'.format(gdf_nodes.gdf_name, time.time()-start_time))

    if edges:

        start_time = time.time()

        # create a list to hold our edges, then loop through each edge in the
        # graph
        edges = []
        for u, v, key, data in G.edges(keys=True, data=True):

            # for each edge, add key and all attributes in data dict to the
            # edge_details
            edge_details = {'u':u, 'v':v, 'key':key}
            for attr_key in data:
                edge_details[attr_key] = data[attr_key]

            # if edge doesn't already have a geometry attribute, create one now
            # if fill_edge_geometry==True
            if 'geometry' not in data:
                if fill_edge_geometry:
                    point_u = Point((G.nodes[u]['x'], G.nodes[u]['y']))
                    point_v = Point((G.nodes[v]['x'], G.nodes[v]['y']))
                    edge_details['geometry'] = LineString([point_u, point_v])
                else:
                    edge_details['geometry'] = np.nan

            edges.append(edge_details)

        # create a GeoDataFrame from the list of edges and set the CRS
        gdf_edges = gpd.GeoDataFrame(edges)
        gdf_edges.crs = G.graph['crs']
        gdf_edges.gdf_name = '{}_edges'.format(G.graph['name'])

        to_return.append(gdf_edges)
        log('Created GeoDataFrame "{}" from graph in {:,.2f} seconds'.format(gdf_edges.gdf_name, time.time()-start_time))

    if len(to_return) > 1:
        return tuple(to_return)
    else:
        return to_return[0]


Instruction:
How would you explain what the following Python 3 function does
def gdfs_to_graph(gdf_nodes, gdf_edges):

    G = nx.MultiDiGraph()
    G.graph['crs'] = gdf_nodes.crs
    G.graph['name'] = gdf_nodes.gdf_name.rstrip('_nodes')

    # add the nodes and their attributes to the graph
    G.add_nodes_from(gdf_nodes.index)
    attributes = gdf_nodes.to_dict()
    for attribute_name in gdf_nodes.columns:
        # only add this attribute to nodes which have a non-null value for it
        attribute_values = {k:v for k, v in attributes[attribute_name].items() if pd.notnull(v)}
        nx.set_node_attributes(G, name=attribute_name, values=attribute_values)

    # add the edges and attributes that are not u, v, key (as they're added
    # separately) or null
    for _, row in gdf_edges.iterrows():
        attrs = {}
        for label, value in row.iteritems():
            if (label not in ['u', 'v', 'key']) and (isinstance(value, list) or pd.notnull(value)):
                attrs[label] = value
        G.add_edge(row['u'], row['v'], key=row['key'], **attrs)

    return G

Response: Convert node and edge GeoDataFrames into a networkx graphx object.


Instruction:
How would you explain what the following Python 3 function does
def make_shp_filename(place_name):
    name_pieces = list(reversed(place_name.split(', ')))
    filename = '-'.join(name_pieces).lower().replace(' ','_')
    filename = re.sub('[^0-9a-zA-Z_-]+', '', filename)
    return filename

Response: Create a filename string from a place name string.


Instruction:
Can you tell what is the following Python 3 function doing
def basic_stats(G, area=None, clean_intersects=False, tolerance=15,
                circuity_dist='gc'):
    """
    Calculate basic descriptive metric and topological stats for a graph.

    For an unprojected lat-lng graph, tolerance and graph units should be in
    degrees, and circuity_dist should be 'gc'. For a projected graph, tolerance
    and graph units should be in meters (or similar) and circuity_dist should be
    'euclidean'.

    Parameters
    ----------
    G : networkx multidigraph
    area : numeric
        the area covered by the street network, in square meters (typically land
        area); if none, will skip all density-based metrics
    clean_intersects : bool
        if True, calculate clean intersections count (and density, if area is
        provided)
    tolerance : numeric
        tolerance value passed along if clean_intersects=True, see
        clean_intersections() function documentation for details and usage
    circuity_dist : str
        'gc' or 'euclidean', how to calculate straight-line distances for
        circuity measurement; use former for lat-lng networks and latter for
        projected networks

    Returns
    -------
    stats : dict
        dictionary of network measures containing the following elements (some
        keys may not be present, based on the arguments passed into the function):

          - n = number of nodes in the graph
          - m = number of edges in the graph
          - k_avg = average node degree of the graph
          - intersection_count = number of intersections in graph, that is,
                nodes with >1 street emanating from them
          - streets_per_node_avg = how many streets (edges in the undirected
                representation of the graph) emanate from each node (ie,
                intersection or dead-end) on average (mean)
          - streets_per_node_counts = dict, with keys of number of streets
                emanating from the node, and values of number of nodes with this
                count
          - streets_per_node_proportion = dict, same as previous, but as a
                proportion of the total, rather than counts
          - edge_length_total = sum of all edge lengths in the graph, in meters
          - edge_length_avg = mean edge length in the graph, in meters
          - street_length_total = sum of all edges in the undirected
                representation of the graph
          - street_length_avg = mean edge length in the undirected
                representation of the graph, in meters
          - street_segments_count = number of edges in the undirected
                representation of the graph
          - node_density_km = n divided by area in square kilometers
          - intersection_density_km = intersection_count divided by area in
                square kilometers
          - edge_density_km = edge_length_total divided by area in square
                kilometers
          - street_density_km = street_length_total divided by area in square
                kilometers
          - circuity_avg = edge_length_total divided by the sum of the great
                circle distances between the nodes of each edge
          - self_loop_proportion = proportion of edges that have a single node
                as its two endpoints (ie, the edge links nodes u and v, and u==v)
          - clean_intersection_count = number of intersections in street
                network, merging complex ones into single points
          - clean_intersection_density_km = clean_intersection_count divided by
                area in square kilometers
    """

    sq_m_in_sq_km = 1e6 #there are 1 million sq meters in 1 sq km
    G_undirected = None

    # calculate the number of nodes, n, and the number of edges, m, in the graph
    n = len(list(G.nodes()))
    m = len(list(G.edges()))

    # calculate the average degree of the graph
    k_avg = 2 * m / n

    if 'streets_per_node' in G.graph:
        # get the degrees saved as a graph attribute (from an undirected
        # representation of the graph). this is not the degree of the nodes in
        # the directed graph, but rather represents the number of streets
        # (unidirected edges) emanating from each node. see
        # count_streets_per_node function.
        streets_per_node = G.graph['streets_per_node']
    else:
        # count how many street segments emanate from each node in this graph
        streets_per_node = count_streets_per_node(G)

    # count number of intersections in graph, as nodes with >1 street emanating
    # from them
    node_ids = set(G.nodes())
    intersection_count = len([True for node, count in streets_per_node.items() if (count > 1) and (node in node_ids)])

    # calculate the average number of streets (unidirected edges) incident to
    # each node
    streets_per_node_avg = sum(streets_per_node.values()) / n

    # create a dict where key = number of streets (unidirected edges) incident
    # to each node, and value = how many nodes are of this number in the graph
    streets_per_node_counts = {num:list(streets_per_node.values()).count(num) for num in range(max(streets_per_node.values()) + 1)}

    # degree proportions: dict where key = each degree and value = what
    # proportion of nodes are of this degree in the graph
    streets_per_node_proportion = {num:count/n for num, count in streets_per_node_counts.items()}

    # calculate the total and average edge lengths
    edge_length_total = sum([d['length'] for u, v, d in G.edges(data=True)])
    edge_length_avg = edge_length_total / m

    # calculate the total and average street segment lengths (so, edges without
    # double-counting two-way streets)
    if G_undirected is None:
        G_undirected = G.to_undirected(reciprocal=False)
    street_length_total = sum([d['length'] for u, v, d in G_undirected.edges(data=True)])
    street_segments_count = len(list(G_undirected.edges(keys=True)))
    street_length_avg = street_length_total / street_segments_count

    # calculate clean intersection counts
    if clean_intersects:
        clean_intersection_points = clean_intersections(G, tolerance=tolerance, dead_ends=False )
        clean_intersection_count = len(clean_intersection_points)
    else:
        clean_intersection_count = None

    # we can calculate density metrics only if area is not null
    if area is not None:
        area_km = area / sq_m_in_sq_km

        # calculate node density as nodes per sq km
        node_density_km = n / area_km

        # calculate intersection density as nodes with >1 street emanating from
        # them, per sq km
        intersection_density_km = intersection_count / area_km

        # calculate edge density as linear meters per sq km
        edge_density_km = edge_length_total / area_km

        # calculate street density as linear meters per sq km
        street_density_km = street_length_total / area_km

        if clean_intersects:
            clean_intersection_density_km = clean_intersection_count / area_km
        else:
            clean_intersection_density_km = None
    else:
        # if area is None, then we cannot calculate density
        node_density_km = None
        intersection_density_km = None
        edge_density_km = None
        street_density_km = None
        clean_intersection_density_km = None

    # average circuity: sum of edge lengths divided by sum of straight-line
    # distance between edge endpoints. first load all the edges origin and
    # destination coordinates as a dataframe, then calculate the straight-line
    # distance
    coords = np.array([[G.nodes[u]['y'], G.nodes[u]['x'], G.nodes[v]['y'], G.nodes[v]['x']] for u, v, k in G.edges(keys=True)])
    df_coords = pd.DataFrame(coords, columns=['u_y', 'u_x', 'v_y', 'v_x'])
    if circuity_dist == 'gc':
        gc_distances = great_circle_vec(lat1=df_coords['u_y'],
                                        lng1=df_coords['u_x'],
                                        lat2=df_coords['v_y'],
                                        lng2=df_coords['v_x'])
    elif circuity_dist == 'euclidean':
        gc_distances = euclidean_dist_vec(y1=df_coords['u_y'],
                                          x1=df_coords['u_x'],
                                          y2=df_coords['v_y'],
                                          x2=df_coords['v_x'])
    else:
        raise ValueError('circuity_dist must be "gc" or "euclidean"')

    gc_distances = gc_distances.fillna(value=0)
    try:
        circuity_avg = edge_length_total / gc_distances.sum()
    except ZeroDivisionError:
        circuity_avg = np.nan

    # percent of edges that are self-loops, ie both endpoints are the same node
    self_loops = [True for u, v, k in G.edges(keys=True) if u == v]
    self_loops_count = len(self_loops)
    self_loop_proportion = self_loops_count / m

    # assemble the results
    stats = {'n':n,
             'm':m,
             'k_avg':k_avg,
             'intersection_count':intersection_count,
             'streets_per_node_avg':streets_per_node_avg,
             'streets_per_node_counts':streets_per_node_counts,
             'streets_per_node_proportion':streets_per_node_proportion,
             'edge_length_total':edge_length_total,
             'edge_length_avg':edge_length_avg,
             'street_length_total':street_length_total,
             'street_length_avg':street_length_avg,
             'street_segments_count':street_segments_count,
             'node_density_km':node_density_km,
             'intersection_density_km':intersection_density_km,
             'edge_density_km':edge_density_km,
             'street_density_km':street_density_km,
             'circuity_avg':circuity_avg,
             'self_loop_proportion':self_loop_proportion,
             'clean_intersection_count':clean_intersection_count,
             'clean_intersection_density_km':clean_intersection_density_km}

    # return the results
    return stats

Response: Calculates basic descriptive metric and topological stats for a single undirected network.


Instruction:
Create a Python 3 function for
calculating the extended stats and metrics for a single network.

Response:
def extended_stats(G, connectivity=False, anc=False, ecc=False, bc=False, cc=False):
    """
    Calculate extended topological stats and metrics for a graph.

    Many of these algorithms have an inherently high time complexity. Global
    topological analysis of large complex networks is extremely time consuming
    and may exhaust computer memory. Consider using function arguments to not
    run metrics that require computation of a full matrix of paths if they
    will not be needed.

    Parameters
    ----------
    G : networkx multidigraph
    connectivity : bool
        if True, calculate node and edge connectivity
    anc : bool
        if True, calculate average node connectivity
    ecc : bool
        if True, calculate shortest paths, eccentricity, and topological metrics
        that use eccentricity
    bc : bool
        if True, calculate node betweenness centrality
    cc : bool
        if True, calculate node closeness centrality

    Returns
    -------
    stats : dict
        dictionary of network measures containing the following elements (some
        only calculated/returned optionally, based on passed parameters):

          - avg_neighbor_degree
          - avg_neighbor_degree_avg
          - avg_weighted_neighbor_degree
          - avg_weighted_neighbor_degree_avg
          - degree_centrality
          - degree_centrality_avg
          - clustering_coefficient
          - clustering_coefficient_avg
          - clustering_coefficient_weighted
          - clustering_coefficient_weighted_avg
          - pagerank
          - pagerank_max_node
          - pagerank_max
          - pagerank_min_node
          - pagerank_min
          - node_connectivity
          - node_connectivity_avg
          - edge_connectivity
          - eccentricity
          - diameter
          - radius
          - center
          - periphery
          - closeness_centrality
          - closeness_centrality_avg
          - betweenness_centrality
          - betweenness_centrality_avg

    """

    stats = {}
    full_start_time = time.time()

    # create a DiGraph from the MultiDiGraph, for those metrics that require it
    G_dir = nx.DiGraph(G)

    # create an undirected Graph from the MultiDiGraph, for those metrics that
    # require it
    G_undir = nx.Graph(G)

    # get the largest strongly connected component, for those metrics that
    # require strongly connected graphs
    G_strong = get_largest_component(G, strongly=True)

    # average degree of the neighborhood of each node, and average for the graph
    avg_neighbor_degree = nx.average_neighbor_degree(G)
    stats['avg_neighbor_degree'] = avg_neighbor_degree
    stats['avg_neighbor_degree_avg'] = sum(avg_neighbor_degree.values())/len(avg_neighbor_degree)

    # average weighted degree of the neighborhood of each node, and average for
    # the graph
    avg_weighted_neighbor_degree = nx.average_neighbor_degree(G, weight='length')
    stats['avg_weighted_neighbor_degree'] = avg_weighted_neighbor_degree
    stats['avg_weighted_neighbor_degree_avg'] = sum(avg_weighted_neighbor_degree.values())/len(avg_weighted_neighbor_degree)

    # degree centrality for a node is the fraction of nodes it is connected to
    degree_centrality = nx.degree_centrality(G)
    stats['degree_centrality'] = degree_centrality
    stats['degree_centrality_avg'] = sum(degree_centrality.values())/len(degree_centrality)

    # calculate clustering coefficient for the nodes
    stats['clustering_coefficient'] = nx.clustering(G_undir)

    # average clustering coefficient for the graph
    stats['clustering_coefficient_avg'] = nx.average_clustering(G_undir)

    # calculate weighted clustering coefficient for the nodes
    stats['clustering_coefficient_weighted'] = nx.clustering(G_undir, weight='length')

    # average clustering coefficient (weighted) for the graph
    stats['clustering_coefficient_weighted_avg'] = nx.average_clustering(G_undir, weight='length')

    # pagerank: a ranking of the nodes in the graph based on the structure of
    # the incoming links
    pagerank = nx.pagerank(G_dir, weight='length')
    stats['pagerank'] = pagerank

    # node with the highest page rank, and its value
    pagerank_max_node = max(pagerank, key=lambda x: pagerank[x])
    stats['pagerank_max_node'] = pagerank_max_node
    stats['pagerank_max'] = pagerank[pagerank_max_node]

    # node with the lowest page rank, and its value
    pagerank_min_node = min(pagerank, key=lambda x: pagerank[x])
    stats['pagerank_min_node'] = pagerank_min_node
    stats['pagerank_min'] = pagerank[pagerank_min_node]

    # if True, calculate node and edge connectivity
    if connectivity:
        start_time = time.time()

        # node connectivity is the minimum number of nodes that must be removed
        # to disconnect G or render it trivial
        stats['node_connectivity'] = nx.node_connectivity(G_strong)

        # edge connectivity is equal to the minimum number of edges that must be
        # removed to disconnect G or render it trivial
        stats['edge_connectivity'] = nx.edge_connectivity(G_strong)
        log('Calculated node and edge connectivity in {:,.2f} seconds'.format(time.time() - start_time))

    # if True, calculate average node connectivity
    if anc:
        # mean number of internally node-disjoint paths between each pair of
        # nodes in G, i.e., the expected number of nodes that must be removed to
        # disconnect a randomly selected pair of non-adjacent nodes
        start_time = time.time()
        stats['node_connectivity_avg'] = nx.average_node_connectivity(G)
        log('Calculated average node connectivity in {:,.2f} seconds'.format(time.time() - start_time))

    # if True, calculate shortest paths, eccentricity, and topological metrics
    # that use eccentricity
    if ecc:
        # precompute shortest paths between all nodes for eccentricity-based
        # stats
        start_time = time.time()
        sp = {source:dict(nx.single_source_dijkstra_path_length(G_strong, source, weight='length')) for source in G_strong.nodes()}

        log('Calculated shortest path lengths in {:,.2f} seconds'.format(time.time() - start_time))

        # eccentricity of a node v is the maximum distance from v to all other
        # nodes in G
        eccentricity = nx.eccentricity(G_strong, sp=sp)
        stats['eccentricity'] = eccentricity

        # diameter is the maximum eccentricity
        diameter = nx.diameter(G_strong, e=eccentricity)
        stats['diameter'] = diameter

        # radius is the minimum eccentricity
        radius = nx.radius(G_strong, e=eccentricity)
        stats['radius'] = radius

        # center is the set of nodes with eccentricity equal to radius
        center = nx.center(G_strong, e=eccentricity)
        stats['center'] = center

        # periphery is the set of nodes with eccentricity equal to the diameter
        periphery = nx.periphery(G_strong, e=eccentricity)
        stats['periphery'] = periphery

    # if True, calculate node closeness centrality
    if cc:
        # closeness centrality of a node is the reciprocal of the sum of the
        # shortest path distances from u to all other nodes
        start_time = time.time()
        closeness_centrality = nx.closeness_centrality(G, distance='length')
        stats['closeness_centrality'] = closeness_centrality
        stats['closeness_centrality_avg'] = sum(closeness_centrality.values())/len(closeness_centrality)
        log('Calculated closeness centrality in {:,.2f} seconds'.format(time.time() - start_time))

    # if True, calculate node betweenness centrality
    if bc:
        # betweenness centrality of a node is the sum of the fraction of
        # all-pairs shortest paths that pass through node
        start_time = time.time()
        betweenness_centrality = nx.betweenness_centrality(G, weight='length')
        stats['betweenness_centrality'] = betweenness_centrality
        stats['betweenness_centrality_avg'] = sum(betweenness_centrality.values())/len(betweenness_centrality)
        log('Calculated betweenness centrality in {:,.2f} seconds'.format(time.time() - start_time))

    log('Calculated extended stats in {:,.2f} seconds'.format(time.time()-full_start_time))
    return stats


Instruction:
Can you generate the documentation for the following Python 3 function
def backward_induction(ddp, T, v_term=None):
    n = ddp.num_states
    vs = np.empty((T+1, n))
    sigmas = np.empty((T, n), dtype=int)

    if v_term is None:
        v_term = np.zeros(n)
    vs[T, :] = v_term

    for t in range(T, 0, -1):
        ddp.bellman_operator(vs[t, :], Tv=vs[t-1, :], sigma=sigmas[t-1, :])

    return vs, sigmas

Response:
r Solve by backward induction a : math:`T - finite horizon
    discrete dynamic program with stationary reward transition and probability arrays and discount factor.


Instruction:
Implement a Python 3 function for
checking whether s_indices and a_indices are sorted in lexicographic order.

Response:
def _has_sorted_sa_indices(s_indices, a_indices):
    """
    Check whether `s_indices` and `a_indices` are sorted in
    lexicographic order.

    Parameters
    ----------
    s_indices, a_indices : ndarray(ndim=1)

    Returns
    -------
    bool
        Whether `s_indices` and `a_indices` are sorted.

    """
    L = len(s_indices)
    for i in range(L-1):
        if s_indices[i] > s_indices[i+1]:
            return False
        if s_indices[i] == s_indices[i+1]:
            if a_indices[i] >= a_indices[i+1]:
                return False
    return True


Instruction:
Can you write a function in Python 3 where it
generates a_indptr array for the next state in the sequence s_indices.

Response:
def _generate_a_indptr(num_states, s_indices, out):
    """
    Generate `a_indptr`; stored in `out`. `s_indices` is assumed to be
    in sorted order.

    Parameters
    ----------
    num_states : scalar(int)

    s_indices : ndarray(int, ndim=1)

    out : ndarray(int, ndim=1)
        Length must be num_states+1.

    """
    idx = 0
    out[0] = 0
    for s in range(num_states-1):
        while(s_indices[idx] == s):
            idx += 1
        out[s+1] = idx
    out[num_states] = len(s_indices)


Instruction:
Explain what the following Python 3 code does
def _check_action_feasibility(self):
        # Check that for every state, reward is finite for some action
        R_max = self.s_wise_max(self.R)
        if (R_max == -np.inf).any():
            # First state index such that all actions yield -inf
            s = np.where(R_max == -np.inf)[0][0]
            raise ValueError(
                'for every state the reward must be finite for some action: '
                'violated for state {s}'.format(s=s)
            )

        if self._sa_pair:
            # Check that for every state there is at least one action available
            diff = np.diff(self.a_indptr)
            if (diff == 0).any():
                # First state index such that no action is available
                s = np.where(diff == 0)[0][0]
                raise ValueError(
                    'for every state at least one action must be available: '
                    'violated for state {s}'.format(s=s)
                )

Response: Checks that the action feasibility of the action is not in use.


Instruction:
Here you have a function in Python 3, explain what it does
def to_sa_pair_form(self, sparse=True):

        if self._sa_pair:
            return self
        else:
            s_ind, a_ind = np.where(self.R > - np.inf)
            RL = self.R[s_ind, a_ind]
            if sparse:
                QL = sp.csr_matrix(self.Q[s_ind, a_ind])
            else:
                QL = self.Q[s_ind, a_ind]
            return DiscreteDP(RL, QL, self.beta, s_ind, a_ind)

Response: Convert this instance of DiscreteDP to SA - pair form.


Instruction:
Make a summary of the following Python 3 code
def to_product_form(self):
        if self._sa_pair:
            ns = self.num_states
            na = self.a_indices.max() + 1
            R = np.full((ns, na), -np.inf)
            R[self.s_indices, self.a_indices] = self.R
            Q = np.zeros((ns, na, ns))
            if self._sparse:
                _fill_dense_Q(self.s_indices, self.a_indices,
                              self.Q.toarray(), Q)
            else:
                _fill_dense_Q(self.s_indices, self.a_indices, self.Q, Q)
            return DiscreteDP(R, Q, self.beta)
        else:
            return self

Response: Convert this instance of DiscreteDP to the product form.


Instruction:
Write a Python 3 script to
give a policy vector sigma return the reward vector R_sigma and transition probability matrix Q_sigma.

Response:
def RQ_sigma(self, sigma):
        """
        Given a policy `sigma`, return the reward vector `R_sigma` and
        the transition probability matrix `Q_sigma`.

        Parameters
        ----------
        sigma : array_like(int, ndim=1)
            Policy vector, of length n.

        Returns
        -------
        R_sigma : ndarray(float, ndim=1)
            Reward vector for `sigma`, of length n.

        Q_sigma : ndarray(float, ndim=2)
            Transition probability matrix for `sigma`, of shape (n, n).

        """
        if self._sa_pair:
            sigma = np.asarray(sigma)
            sigma_indices = np.empty(self.num_states, dtype=int)
            _find_indices(self.a_indices, self.a_indptr, sigma,
                          out=sigma_indices)
            R_sigma, Q_sigma = self.R[sigma_indices], self.Q[sigma_indices]
        else:
            R_sigma = self.R[np.arange(self.num_states), sigma]
            Q_sigma = self.Q[np.arange(self.num_states), sigma]

        return R_sigma, Q_sigma


Instruction:
Write a Python 3 function that can
return a function that returns the T_sigma operator.

Response:
def T_sigma(self, sigma):
        """
        Given a policy `sigma`, return the T_sigma operator.

        Parameters
        ----------
        sigma : array_like(int, ndim=1)
            Policy vector, of length n.

        Returns
        -------
        callable
            The T_sigma operator.

        """
        R_sigma, Q_sigma = self.RQ_sigma(sigma)
        return lambda v: R_sigma + self.beta * Q_sigma.dot(v)


Instruction:
Can you create a Python 3 function that
computes the v - greedy policy.

Response:
def compute_greedy(self, v, sigma=None):
        """
        Compute the v-greedy policy.

        Parameters
        ----------
        v : array_like(float, ndim=1)
            Value function vector, of length n.

        sigma : ndarray(int, ndim=1), optional(default=None)
            Optional output array for `sigma`.

        Returns
        -------
        sigma : ndarray(int, ndim=1)
            v-greedy policy vector, of length n.

        """
        if sigma is None:
            sigma = np.empty(self.num_states, dtype=int)
        self.bellman_operator(v, sigma=sigma)
        return sigma


Instruction:
Can you create a Python 3 function that
evaluates the value of a policy.

Response:
def evaluate_policy(self, sigma):
        """
        Compute the value of a policy.

        Parameters
        ----------
        sigma : array_like(int, ndim=1)
            Policy vector, of length n.

        Returns
        -------
        v_sigma : ndarray(float, ndim=1)
            Value vector of `sigma`, of length n.

        """
        if self.beta == 1:
            raise NotImplementedError(self._error_msg_no_discounting)

        # Solve (I - beta * Q_sigma) v = R_sigma for v
        R_sigma, Q_sigma = self.RQ_sigma(sigma)
        b = R_sigma

        A = self._I - self.beta * Q_sigma

        v_sigma = self._lineq_solve(A, b)

        return v_sigma


Instruction:
How would you explain what the following Python 3 function does
def operator_iteration(self, T, v, max_iter, tol=None, *args, **kwargs):
        # May be replaced with quantecon.compute_fixed_point
        if max_iter <= 0:
            return v, 0

        for i in range(max_iter):
            new_v = T(v, *args, **kwargs)
            if tol is not None and np.abs(new_v - v).max() < tol:
                v[:] = new_v
                break
            v[:] = new_v

        num_iter = i + 1

        return num_iter

Response: Iteratively apply the operator T to v.


Instruction:
How would you implement a function in Python 3 that
solves the dynamic programming problem.

Response:
def solve(self, method='policy_iteration',
              v_init=None, epsilon=None, max_iter=None, k=20):
        """
        Solve the dynamic programming problem.

        Parameters
        ----------
        method : str, optinal(default='policy_iteration')
            Solution method, str in {'value_iteration', 'vi',
            'policy_iteration', 'pi', 'modified_policy_iteration',
            'mpi'}.

        v_init : array_like(float, ndim=1), optional(default=None)
            Initial value function, of length n. If None, `v_init` is
            set such that v_init(s) = max_a r(s, a) for value iteration
            and policy iteration; for modified policy iteration,
            v_init(s) = min_(s_next, a) r(s_next, a)/(1 - beta) to guarantee
            convergence.

        epsilon : scalar(float), optional(default=None)
            Value for epsilon-optimality. If None, the value stored in
            the attribute `epsilon` is used.

        max_iter : scalar(int), optional(default=None)
            Maximum number of iterations. If None, the value stored in
            the attribute `max_iter` is used.

        k : scalar(int), optional(default=20)
            Number of iterations for partial policy evaluation in
            modified policy iteration (irrelevant for other methods).

        Returns
        -------
        res : DPSolveResult
            Optimization result represetned as a DPSolveResult. See
            `DPSolveResult` for details.

        """
        if method in ['value_iteration', 'vi']:
            res = self.value_iteration(v_init=v_init,
                                       epsilon=epsilon,
                                       max_iter=max_iter)
        elif method in ['policy_iteration', 'pi']:
            res = self.policy_iteration(v_init=v_init,
                                        max_iter=max_iter)
        elif method in ['modified_policy_iteration', 'mpi']:
            res = self.modified_policy_iteration(v_init=v_init,
                                                 epsilon=epsilon,
                                                 max_iter=max_iter,
                                                 k=k)
        else:
            raise ValueError('invalid method')

        return res


Instruction:
Create a Python 3 function for
solving the optimization problem by value iteration.

Response:
def value_iteration(self, v_init=None, epsilon=None, max_iter=None):
        """
        Solve the optimization problem by value iteration. See the
        `solve` method.

        """
        if self.beta == 1:
            raise NotImplementedError(self._error_msg_no_discounting)

        if max_iter is None:
            max_iter = self.max_iter
        if epsilon is None:
            epsilon = self.epsilon

        try:
            tol = epsilon * (1-self.beta) / (2*self.beta)
        except ZeroDivisionError:  # Raised if beta = 0
            tol = np.inf

        v = np.empty(self.num_states)
        if v_init is None:
            self.s_wise_max(self.R, out=v)
        else:
            v[:] = v_init

        # Storage array for self.bellman_operator
        Tv = np.empty(self.num_states)

        num_iter = self.operator_iteration(T=self.bellman_operator,
                                           v=v, max_iter=max_iter, tol=tol,
                                           Tv=Tv)
        sigma = self.compute_greedy(v)

        res = DPSolveResult(v=v,
                            sigma=sigma,
                            num_iter=num_iter,
                            mc=self.controlled_mc(sigma),
                            method='value iteration',
                            epsilon=epsilon,
                            max_iter=max_iter)

        return res


Instruction:
Write a Python 3 script for
solving the optimization problem by policy iteration.

Response:
def policy_iteration(self, v_init=None, max_iter=None):
        """
        Solve the optimization problem by policy iteration. See the
        `solve` method.

        """
        if self.beta == 1:
            raise NotImplementedError(self._error_msg_no_discounting)

        if max_iter is None:
            max_iter = self.max_iter

        # What for initial condition?
        if v_init is None:
            v_init = self.s_wise_max(self.R)

        sigma = self.compute_greedy(v_init)
        new_sigma = np.empty(self.num_states, dtype=int)

        for i in range(max_iter):
            # Policy evaluation
            v_sigma = self.evaluate_policy(sigma)
            # Policy improvement
            self.compute_greedy(v_sigma, sigma=new_sigma)
            if np.array_equal(new_sigma, sigma):
                break
            sigma[:] = new_sigma

        num_iter = i + 1

        res = DPSolveResult(v=v_sigma,
                            sigma=sigma,
                            num_iter=num_iter,
                            mc=self.controlled_mc(sigma),
                            method='policy iteration',
                            max_iter=max_iter)

        return res


Instruction:
Can you create a Python 3 function that
solves the optimization problem by modified policy iteration.

Response:
def modified_policy_iteration(self, v_init=None, epsilon=None,
                                  max_iter=None, k=20):
        """
        Solve the optimization problem by modified policy iteration. See
        the `solve` method.

        """
        if self.beta == 1:
            raise NotImplementedError(self._error_msg_no_discounting)

        if max_iter is None:
            max_iter = self.max_iter
        if epsilon is None:
            epsilon = self.epsilon

        def span(z):
            return z.max() - z.min()

        def midrange(z):
            return (z.min() + z.max()) / 2

        v = np.empty(self.num_states)
        if v_init is None:
            v[:] = self.R[self.R > -np.inf].min() / (1 - self.beta)
        else:
            v[:] = v_init

        u = np.empty(self.num_states)
        sigma = np.empty(self.num_states, dtype=int)

        try:
            tol = epsilon * (1-self.beta) / self.beta
        except ZeroDivisionError:  # Raised if beta = 0
            tol = np.inf

        for i in range(max_iter):
            # Policy improvement
            self.bellman_operator(v, Tv=u, sigma=sigma)
            diff = u - v
            if span(diff) < tol:
                v[:] = u + midrange(diff) * self.beta / (1 - self.beta)
                break
            # Partial policy evaluation with k iterations
            self.operator_iteration(T=self.T_sigma(sigma), v=u, max_iter=k)
            v[:] = u

        num_iter = i + 1

        res = DPSolveResult(v=v,
                            sigma=sigma,
                            num_iter=num_iter,
                            mc=self.controlled_mc(sigma),
                            method='modified policy iteration',
                            epsilon=epsilon,
                            max_iter=max_iter,
                            k=k)

        return res


Instruction:
Write a Python 3 script for
returning the controlled Markov chain for a given policy vector sigma.

Response:
def controlled_mc(self, sigma):
        """
        Returns the controlled Markov chain for a given policy `sigma`.

        Parameters
        ----------
        sigma : array_like(int, ndim=1)
            Policy vector, of length n.

        Returns
        -------
        mc : MarkovChain
            Controlled Markov chain.

        """
        _, Q_sigma = self.RQ_sigma(sigma)
        return MarkovChain(Q_sigma)


Instruction:
Write a Python 3 script for
smoothing the data in x using convolution with a window of requested size and type.

Response:
def smooth(x, window_len=7, window='hanning'):
    """
    Smooth the data in x using convolution with a window of requested
    size and type.

    Parameters
    ----------
    x : array_like(float)
        A flat NumPy array containing the data to smooth
    window_len : scalar(int), optional
        An odd integer giving the length of the window.  Defaults to 7.
    window : string
        A string giving the window type. Possible values are 'flat',
        'hanning', 'hamming', 'bartlett' or 'blackman'

    Returns
    -------
    array_like(float)
        The smoothed values

    Notes
    -----
    Application of the smoothing window at the top and bottom of x is
    done by reflecting x around these points to extend it sufficiently
    in each direction.

    """
    if len(x) < window_len:
        raise ValueError("Input vector length must be >= window length.")

    if window_len < 3:
        raise ValueError("Window length must be at least 3.")

    if not window_len % 2:  # window_len is even
        window_len += 1
        print("Window length reset to {}".format(window_len))

    windows = {'hanning': np.hanning,
               'hamming': np.hamming,
               'bartlett': np.bartlett,
               'blackman': np.blackman,
               'flat': np.ones  # moving average
               }

    # === Reflect x around x[0] and x[-1] prior to convolution === #
    k = int(window_len / 2)
    xb = x[:k]   # First k elements
    xt = x[-k:]  # Last k elements
    s = np.concatenate((xb[::-1], x, xt[::-1]))

    # === Select window values === #
    if window in windows.keys():
        w = windows[window](window_len)
    else:
        msg = "Unrecognized window type '{}'".format(window)
        print(msg + " Defaulting to hanning")
        w = windows['hanning'](window_len)

    return np.convolve(w / w.sum(), s, mode='valid')


Instruction:
Make a summary of the following Python 3 code
def ar_periodogram(x, window='hanning', window_len=7):
    # === run regression === #
    x_lag = x[:-1]  # lagged x
    X = np.array([np.ones(len(x_lag)), x_lag]).T  # add constant

    y = np.array(x[1:])  # current x

    beta_hat = np.linalg.solve(X.T @ X, X.T @ y)  # solve for beta hat
    e_hat = y - X @ beta_hat  # compute residuals
    phi = beta_hat[1]  # pull out phi parameter

    # === compute periodogram on residuals === #
    w, I_w = periodogram(e_hat, window=window, window_len=window_len)

    # === recolor and return === #
    I_w = I_w / np.abs(1 - phi * np.exp(1j * w))**2

    return w, I_w

Response: Compute periodograms at the current Fourier frequences at which periodogram is evaluated.


Instruction:
Here you have a function in Python 3, explain what it does
def _support_enumeration_gen(payoff_matrices):
    nums_actions = payoff_matrices[0].shape
    n_min = min(nums_actions)

    for k in range(1, n_min+1):
        supps = (np.arange(0, k, 1, np.int_), np.empty(k, np.int_))
        actions = (np.empty(k+1), np.empty(k+1))
        A = np.empty((k+1, k+1))

        while supps[0][-1] < nums_actions[0]:
            supps[1][:] = np.arange(k)
            while supps[1][-1] < nums_actions[1]:
                if _indiff_mixed_action(
                    payoff_matrices[0], supps[0], supps[1], A, actions[1]
                ):
                    if _indiff_mixed_action(
                        payoff_matrices[1], supps[1], supps[0], A, actions[0]
                    ):
                        out = (np.zeros(nums_actions[0]),
                               np.zeros(nums_actions[1]))
                        for p, (supp, action) in enumerate(zip(supps,
                                                               actions)):
                            out[p][supp] = action[:-1]
                        yield out
                next_k_array(supps[1])
            next_k_array(supps[0])

Response:
Main body of `support_enumeration_gen`.

    Parameters
    ----------
    payoff_matrices : tuple(ndarray(float, ndim=2))
        Tuple of payoff matrices, of shapes (m, n) and (n, m),
        respectively.

    Yields
    ------
    out : tuple(ndarray(float, ndim=1))
        Tuple of Nash equilibrium mixed actions, of lengths m and n,
        respectively.


Instruction:
How would you explain what the following Python 3 function does
def _indiff_mixed_action(payoff_matrix, own_supp, opp_supp, A, out):
    m = payoff_matrix.shape[0]
    k = len(own_supp)

    for i in range(k):
        for j in range(k):
            A[j, i] = payoff_matrix[own_supp[i], opp_supp[j]]  # transpose
    A[:-1, -1] = 1
    A[-1, :-1] = -1
    A[-1, -1] = 0
    out[:-1] = 0
    out[-1] = 1

    r = _numba_linalg_solve(A, out)
    if r != 0:  # A: singular
        return False
    for i in range(k):
        if out[i] <= 0:
            return False
    val = out[-1]

    if k == m:
        return True

    own_supp_flags = np.zeros(m, np.bool_)
    own_supp_flags[own_supp] = True

    for i in range(m):
        if not own_supp_flags[i]:
            payoff = 0
            for j in range(k):
                payoff += payoff_matrix[i, opp_supp[j]] * out[j]
            if payoff > val:
                return False
    return True

Response: Compute the indifferent mixed action for a player.


Instruction:
Given the following Python 3 function, write the documentation
def pure2mixed(num_actions, action):
    mixed_action = np.zeros(num_actions)
    mixed_action[action] = 1
    return mixed_action

Response: Convert a pure action to the corresponding mixed action.


Instruction:
Can you write a function in Python 3 where it
deletes the action set of a specific player.

Response:
def delete_action(self, action, player_idx=0):
        """
        Return a new `Player` instance with the action(s) specified by
        `action` deleted from the action set of the player specified by
        `player_idx`. Deletion is not performed in place.

        Parameters
        ----------
        action : scalar(int) or array_like(int)
            Integer or array like of integers representing the action(s)
            to be deleted.

        player_idx : scalar(int), optional(default=0)
            Index of the player to delete action(s) for.

        Returns
        -------
        Player
            Copy of `self` with the action(s) deleted as specified.

        Examples
        --------
        >>> player = Player([[3, 0], [0, 3], [1, 1]])
        >>> player
        Player([[3, 0],
                [0, 3],
                [1, 1]])
        >>> player.delete_action(2)
        Player([[3, 0],
                [0, 3]])
        >>> player.delete_action(0, player_idx=1)
        Player([[0],
                [3],
                [1]])

        """
        payoff_array_new = np.delete(self.payoff_array, action, player_idx)
        return Player(payoff_array_new)