Commit fe960a4f authored by jlaura's avatar jlaura Committed by GitHub
Browse files

Merge pull request #236 from evindunn/dev

Adds add_node() and add_edge() to CandidateGraph
parents 3a354bae fa983d9a
Loading
Loading
Loading
Loading
+59 −150
Original line number Diff line number Diff line
@@ -236,175 +236,84 @@ class CandidateGraph(nx.Graph):
            matches.append(match)
        return matches

    '''def add_image(self, image_name, adjacency=None, basepath=None, apply_func=None):
    def add_node(self, n=None, **attr):
        """
        Adds an image node to the graph.

        Parameters
        ----------
        image_name : str
                     The file name of or path to the image to add

        adjacency : string or Node list
                    The list of adjacent Nodes or image files for this image
                     The file name of the node

        adjacency : str list
                    List of files names of adjacent images that correspond
                    to names in CandidateGraph.graph["node_name_map"]
        basepath : str
                   The directory path for the image

        apply_func : function
                     A static function that takes an Edge as its parameter
                     Function will be applied to all Edges generated when adding
                     the image

                    The base path to the node image file
        """

        image_name = attr.pop("image_name", None)
        adj = attr.pop("adjacency", None)
        new_node = None


        # Check if image is already in the graph
        if image_name in self.nodes:
            warnings.warn("{} is already in the graph".format(image_name))
            return

        # Basepath resolution
        if basepath:
            image_path = os.path.join(basepath, image_name)
        # If image name is provided, build the node from the image before
        # calling nx.add_node()
        if image_name is not None:
            if "basepath" in attr.keys():
                image_path = os.path.join(attr.pop("basepath"), image_name)
            else:
                image_path = image_name
            image_name = os.path.basename(image_path)

        # Create new node within graph
        new_node = Node(image_name, image_path)
        new_node = self.add_node(image_name, data=new_node)

        # If adjacency supplied make sure it's the right type
        if adjacency:
            # Type check
            try:
                assert type(adjacency) is list
            except AssertionError:
                raise TypeError("Named parameter 'adjacency' must be a list of"
                                "adjacent Node objects or list of adjacent "
                                "images; Could not add {} to "
                                "CandidateGraph".format(image_name))
        # If adjacency not supplied, figure it out from footprints
        else:
            # Create empty adjacency list
            adjacency = list()

            # Make sure new node has valid footprint; If not, it will be a
            # disconnected node on the graph
            if not new_node.geodata.footprint or not \
                    new_node.geodata.footprint.IsValid():
                warnings.warn('Missing or invalid geospatial data for '
                              '{0}; {0} will be added to the CandidateGraph'
                              'as a disconnected Node'.format(image_name))
            if not os.path.exists(image_path):
                warnings.warn("Cannot find {}".format(image_path))
                return

            # Detect adjacency between our new node and the CG's nodes
            target_nodes = [self.node[idx] for idx in self.nodes()]  # This is broken too
            valid_datasets = list()
            datasets = [node.geodata for node in target_nodes]

            # Make sure target nodes have valid footprints
            for ds in datasets:
                # Skip the source node if it's in the list of target nodes
                if ds.file_name == new_node['image_path']:
            n = self.graph["node_counter"]
            self.graph["node_counter"] += 1
            new_node = Node(image_name=image_name,
                            image_path=image_path,
                            node_id=n)
            self.graph["node_name_map"][new_node["image_name"]] = new_node["node_id"]
            attr["data"] = new_node

        # Add the new node to the graph using networkx
        super(CandidateGraph, self).add_node(n, **attr)

        # Populate adjacency, if provided
        if new_node is not None and adj is not None:
            for adj_img in adj:
                if adj_img not in self.graph["node_name_map"].keys():
                    warnings.warn("{} not found in the graph".format(adj_img))
                    continue
                # Grab footprints from nodes that have them
                fp = ds.footprint
                if fp and fp.IsValid():
                    valid_datasets.append(ds)
                else:
                    warnings.warn('Missing or invalid geospatial data for '
                                  '{}'.format(os.path.basename(ds.file_name)))
                new_idx = new_node["node_id"]
                adj_idx = self.graph["node_name_map"][adj_img]
                self.add_edge(adj_img, new_node["image_name"])

            # Grab the footprints and test for intersection
            for ds in valid_datasets:
                ds_file_name = os.path.basename(ds.file_name)
                try:
                    if new_node.geodata.footprint.Intersects(ds.footprint):
                        adjacency.append(ds_file_name)
                except:
                    warnings.warn('Failed to calculate intersection between {} '
                                  'and {}'.format(image_name, ds_file_name))

        # Build new edge(s) from adjacency

        for a_img in adjacency:
            # If string (image name)
            if isinstance(a_img, str):
                if a_img > new_node['image_name']:
                    a = new_node['image_name']
                    b = a_img
                else:
                    a = a_img
                    b = new_node['image_name']
                edge = Edge(source=a, destination=b)
                self.add_edge()
                # If adjacent img is already in the graph
                if a_img in self.graph['node_name_map'].keys():
                    # Set the nodes for the new edge
                    a_node_idx = self.graph['node_name_map'][a_img]
                    s = self.node[a_node_idx]
                    d = new_node

                # If adjacent img isnt already in graph, add it
                else:
                    # Set the nodes for the new graph
                    s = new_node
                    d = add_node(os.path.join(basepath, a_img))
            # If Node
            elif isinstance(a_img, Node):
                # If it's already in the graph, it'll be the source node,
                # since its idx is lower than our new node
                if a_img['image_name'] in [self.node[idx]['image_name'] for idx in self.nodes()]:
                    s = a_img
                    d = new_node
                # Otherwise, can't create edge
                else:
                    warnings.warn("{0} is not in the graph; No Edge between"
                                  "{0} and {1} can be "
                                  "created".format(a_img['image_name'],
                                                   image_name))
                    continue
            else:
                raise TypeError("Adjacency list contains Node objects or image "
                                "names; Could not add {} to "
                                "CandidateGraph".format(image_name))

            # Create the new edge
            new_edge = Edge(s, d)

            # If there's a clean func for the new edge, apply it
            if apply_func:
                ERR = "Named parameter 'apply_func' must be a static " \
                      "function or list of static functions; These " \
                      "function(s) are applied to all new edges generated " \
                      "when adding {0}; Could not add {0} to " \
                      "CandidateGraph".format(image_name)
                # Type Check
                try:
                    assert callable(apply_func) or type(apply_func) is list
                    # If it's a function, apply it
                    if callable(apply_func):
                        apply_func(new_edge)
                    # If it's a list of functions, apply all of them
                    else:
                        [func(new_edge) for func in apply_func]
                except AssertionError:
                    raise TypeError(ERR)

            # Grab node ids
            s_id = s['node_id']
            d_id = d['node_id']
    def add_edge(self, u, v, **attr):
        """
        Adds an edge with the given src and dst nodes to the graph

            # Make sure source node is a key in the edge lookup
            if s_id not in self.edge.keys():
                self.edge[s_id] = dict()
        Parameters
        ----------
        u : str
            The filename of the source image for the edge

        v : Node
            The filename of the destination image for the edge
        """
        if ("node_name_map" in self.graph.keys() and
            u in self.graph["node_name_map"].keys() and
            v in self.graph["node_name_map"].keys()):
            # Grab node ids & create edge obj
            s_id = self.graph["node_name_map"][u]
            d_id = self.graph["node_name_map"][v]
            new_edge = Edge(self.node[s_id]["data"], self.node[d_id]["data"])
            # Prepare data for networkx
            u = s_id
            v = d_id
            attr["data"] = new_edge
        # Add the new edge to the graph using networkx
        super(CandidateGraph, self).add_edge(u, v, **attr)

            # Add the new edge to the graph
            self.edge[s_id][d_id] = new_edge
'''
    def extract_features(self, band=1, *args, **kwargs):  # pragma: no cover
        """
        Extracts features from each image in the graph and uses the result to assign the
+47 −104
Original line number Diff line number Diff line
@@ -133,118 +133,61 @@ def test_from_adjacency():
    for s, d, e in g.edges.data('data'):
        assert isinstance(e, edge.Edge)
        assert isinstance(g.nodes[s]['data'], node.Node)
"""
def test_add_image(graph):
    # apply_func
    def extract_and_match(edge):
        for n in [edge.source, edge.destination]:
            n.extract_features(n.get_array(band=1),
                               extractor_parameters={'nfeatures': 800})
        edge.match()

def test_add_node():
    basepath = get_path('Apollo15')
    cube_adjacency = {"AS15-M-0297_crop.cub": ["AS15-M-0298_crop.cub"],
                      "AS15-M-0298_crop.cub": ["AS15-M-0297_crop.cub"]}
    cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath)

    # Test with all optional args
    cub_img = "AS15-M-0299_crop.cub"
    png_img = "AS15-M-0299_SML.png"

    cub_adj = ["AS15-M-0298_crop.cub", "AS15-M-0297_crop.cub"]

    png_adj = ["AS15-M-0298_crop.cub", "AS15-M-0297_crop.cub"]
    cang.add_image(cub_img, adjacency=cub_adj, basepath=basepath,
                   apply_func=extract_and_match)

    # Assert everything worked properly
    assert cub_img in cang.graph['node_name_map'].keys()
    new_node_idx = cang.graph['node_name_map'][cub_img]
    assert new_node_idx in cang.node.keys()
    assert cang.node[new_node_idx]['image_name'] == cub_img
    assert sorted(cang.nodes()) == [0, 1, 2]
    assert sorted(cang.edges()) == [(0, 1), (0, 2), (1, 2)]
    assert cang[0][2].destination['image_name'] == cang.edge[0][2].destination['image_name'] == cub_img
    assert cang[1][2].destination['image_name'] == cang.edge[1][2].destination['image_name'] == cub_img

    # Test when img is already in graph
    cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                 basepath=basepath)
    cang.add_image(cub_adj[0], basepath=basepath)

    # Test for file not found
    with pytest.raises(FileNotFoundError):
        cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                     basepath=basepath)
        cang.add_image(cub_img, basepath=None)

    # Test with auto-detect adjacency
    cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                 basepath=basepath)
    cang.add_image(cub_img, basepath=basepath)
    a = 'AS15-M-0297_crop.cub'
    b = 'AS15-M-0298_crop.cub'
    c = 'AS15-M-0299_crop.cub'
    adjacency = {a:[b],
                 b:[a]}
    g = network.CandidateGraph.from_adjacency(adjacency, basepath=basepath)

    # Test auto-detect when there are nodes w/ invalid geospacial data
    cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                 basepath=basepath)
    cang.add_image(png_img, adjacency=png_adj, basepath=basepath)  # Invalid
    cang.add_image(cub_img, basepath=basepath)  # Autodetect
    # Test without "image_name" arg (networkx parent method)
    g.add_node(2, data=node.Node(image_name=c,
                                    image_path=os.path.join(basepath, c),
                                    node_id=2))
    assert len(g.nodes) == 3
    assert g.node[2]["data"]["image_name"] == c

    # Test auto-detect when new node does not intersect
    # Need a non-intersecting cube file
    # Test with "image_name" (cg method)
    g = network.CandidateGraph.from_adjacency(adjacency, basepath=basepath)
    g.add_node(image_name=c, basepath=basepath)
    assert len(g.nodes) == 3
    assert g.node[2]["data"]["image_name"] == c
    assert g.node[0].keys() == g.node[1].keys() == g.node[2].keys()

    # Test when adjacency is list of nodes
    cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                 basepath=basepath)
    cub_adj2 = [cang.node[0], cang.node[1]]
    cang.add_image(cub_img, adjacency=cub_adj2, basepath=basepath)
    # Test when "image_name" not found
    node_len = len(g.nodes)
    g.add_node(image_name="nonexistent.jpg")
    assert len(g.nodes) == node_len

    # Test when an adjacency node is not in graph
    cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                 basepath=basepath)
    cub_adj2 = [cang.node[0], cang.node[1]]
    not_there = node.Node("_" + cub_img, os.path.join(basepath, cub_img), 15)
    adj = [not_there]
    edges_bf = cang.edges()
    cang.add_image(cub_img, adjacency=adj, basepath=basepath)
    assert cang.edges() == edges_bf     # Should be no change in edges

    # Test when adjacency is of wrong type
    with pytest.raises(TypeError):
        cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                     basepath=basepath)
        cang.add_image(png_img, adjacency=1, basepath=basepath)  # Invalid
    with pytest.raises(TypeError):
        cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                     basepath=basepath)
        cang.add_image(png_img, adjacency=[1], basepath=basepath)  # Invalid
def test_add_edge():
    basepath = get_path('Apollo15')
    a = 'AS15-M-0297_crop.cub'
    b = 'AS15-M-0298_crop.cub'
    c = 'AS15-M-0299_crop.cub'
    adjacency = {a:[b],
                 b:[a]}
    c_adj = ['AS15-M-0297_crop.cub', 'AS15-M-0298_crop.cub']
    g =  network.CandidateGraph.from_adjacency(adjacency, basepath=basepath)
    g.add_node(image_name=c, basepath=basepath, adjacency=c_adj)

    # Test when no adjacency supplied, but image doesn't have footprint;
    # This results in a disconnected node added to the graph
    cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                 basepath=basepath)
    edges_bf = cang.edges()
    cang.add_image(png_img, basepath=basepath)
    assert cang.edges() == edges_bf    # Assert no change in edges

    # Test when adjacency includes a node not already in the graph
    adj = cub_adj
    adj.append("AS15-M-0300_crop.cub")
    cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                 basepath=basepath)
    cang.add_image(cub_img, adjacency=adj, basepath=basepath)
    assert len(g.edges) == 3
    assert g.edges[0, 1]["data"].source == g.node[0]["data"]
    assert g.edges[0, 1]["data"].destination == g.node[1]["data"]
    assert g.edges[0, 2]["data"].source == g.node[0]["data"]
    assert g.edges[0, 2]["data"].destination == g.node[2]["data"]
    assert g.edges[1, 2]["data"].source == g.node[1]["data"]
    assert g.edges[1, 2]["data"].destination == g.node[2]["data"]
    assert g.edges[0, 1].keys() == g.edges[0, 2].keys() == g.edges[1, 2].keys()

    # Test when adj img not found
    g =  network.CandidateGraph.from_adjacency(adjacency, basepath=basepath)
    edge_len = len(g.edges)
    g.add_node(image_name=c, basepath=basepath, adjacency=["nonexistent.jpg"])
    assert len(g.edges) == edge_len

    # Test when apply_func is not a function / list of functions
    with pytest.raises(TypeError):
        cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                     basepath=basepath)
        cang.add_image(cub_img, adjacency=cub_adj, basepath=basepath,
                       apply_func=1)
    with pytest.raises(TypeError):
        cang = network.CandidateGraph.from_adjacency(cube_adjacency,
                                                     basepath=basepath)
        cang.add_image(cub_img, adjacency=cub_adj, basepath=basepath,
                       apply_func=[extract_and_match, 1])
"""
def test_equal(candidategraph):
    cg = copy.deepcopy(candidategraph)
    assert candidategraph == cg