From b16c374a2bc4bcc877ef7da53ac4a457f79ada7e Mon Sep 17 00:00:00 2001 From: Brian Litwin Date: Wed, 27 Feb 2019 21:44:21 -0500 Subject: [PATCH] pagerankGraph: add edge filter (#1105) Part of ongoing work for #1020. Test plan: Added tests that mirror the edge filtering tests in `graph.test` to check that `graph` and `pagerankGraph` return the same edges with the given `EdgesOptions` parameter. Also added a sanity check that a `weight` prop is returned from the iterator along with the edge. Given the dependence on a helper function to test the edge iterator's equality between graphs, I would suggest reviewers give particular attention to that function: `expectConsistentEdges()` --- src/core/pagerankGraph.js | 14 ++-- src/core/pagerankGraph.test.js | 118 +++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/src/core/pagerankGraph.js b/src/core/pagerankGraph.js index 78fb409..8bf2f8a 100644 --- a/src/core/pagerankGraph.js +++ b/src/core/pagerankGraph.js @@ -6,6 +6,7 @@ import {toCompat, fromCompat, type Compatible} from "../util/compat"; import { Graph, type Edge, + type EdgesOptions, type NodeAddressT, type EdgeAddressT, type GraphJSON, @@ -237,15 +238,18 @@ export class PagerankGraph { /** * Provides edge and weight for every edge in the underlying graph. * - * TODO(#1020): Allow optional filtering, as in Graph.edges. + * Optionally, provide an EdgesOptions parameter to return an + * iterator containing edges matching the EdgesOptions prefix + * filter parameters. See Graph.edges for details. */ - edges(): Iterator { + edges(options?: EdgesOptions): Iterator { this._verifyGraphNotModified(); - return this._edgesIterator(); + const iterator = this._graph.edges(options); + return this._edgesIterator(iterator); } - *_edgesIterator(): Iterator { - for (const edge of this._graph.edges()) { + *_edgesIterator(iterator: Iterator): Iterator { + for (const edge of iterator) { const weight = NullUtil.get(this._edgeWeights.get(edge.address)); yield {edge, weight}; } diff --git a/src/core/pagerankGraph.test.js b/src/core/pagerankGraph.test.js index 65c9d35..92f99e5 100644 --- a/src/core/pagerankGraph.test.js +++ b/src/core/pagerankGraph.test.js @@ -7,6 +7,7 @@ import { EdgeAddress, type NodeAddressT, type Edge, + type EdgesOptions, } from "./graph"; import {PagerankGraph} from "./pagerankGraph"; import {advancedGraph} from "./graphTestUtil"; @@ -230,6 +231,123 @@ describe("core/pagerankGraph", () => { }); }); + describe("edge filtering", () => { + const src1 = NodeAddress.fromParts(["src", "1"]); + const src2 = NodeAddress.fromParts(["src", "2"]); + const dst1 = NodeAddress.fromParts(["dst", "1"]); + const dst2 = NodeAddress.fromParts(["dst", "2"]); + const e11 = { + src: src1, + dst: dst1, + address: EdgeAddress.fromParts(["e", "1", "1"]), + }; + const e12 = { + src: src1, + dst: dst2, + address: EdgeAddress.fromParts(["e", "1", "2"]), + }; + const e21 = { + src: src2, + dst: dst1, + address: EdgeAddress.fromParts(["e", "2", "1"]), + }; + const e22 = { + src: src2, + dst: dst2, + address: EdgeAddress.fromParts(["e", "2", "2"]), + }; + const graph = () => { + const g = new Graph(); + [src1, src2, dst1, dst2].forEach((n) => g.addNode(n)); + [e11, e12, e21, e22].forEach((e) => g.addEdge(e)); + return g; + }; + const pagerankGraph = () => new PagerankGraph(graph(), defaultEvaluator); + + function expectConsistentEdges(options: EdgesOptions | void) { + const pagerankGraphEdges = Array.from(pagerankGraph().edges(options)); + pagerankGraphEdges.forEach((e) => { + expect(e.weight.froWeight).toBe(0); + expect(e.weight.toWeight).toBe(1); + }); + const graphEdges = Array.from(graph().edges(options)); + expect(pagerankGraphEdges.map((e) => e.edge)).toEqual(graphEdges); + } + + describe("edge filter matches graph edge filter", () => { + it("finds all edges when no options are specified", () => { + expectConsistentEdges(undefined); + }); + it("finds all edges when all-inclusive filters are specified", () => { + expectConsistentEdges({ + addressPrefix: EdgeAddress.fromParts(["e"]), + srcPrefix: NodeAddress.fromParts(["src"]), + dstPrefix: NodeAddress.fromParts(["dst"]), + }); + }); + it("finds edges by address prefix", () => { + expectConsistentEdges({ + addressPrefix: EdgeAddress.fromParts(["e", "1"]), + srcPrefix: NodeAddress.empty, + dstPrefix: NodeAddress.empty, + }); + }); + it("finds edges by src prefix", () => { + expectConsistentEdges({ + addressPrefix: EdgeAddress.empty, + srcPrefix: NodeAddress.fromParts(["src", "1"]), + dstPrefix: NodeAddress.empty, + }); + }); + it("finds edges by dst prefix", () => { + expectConsistentEdges({ + addressPrefix: EdgeAddress.empty, + srcPrefix: NodeAddress.empty, + dstPrefix: NodeAddress.fromParts(["dst", "1"]), + }); + }); + it("yields nothing for disjoint filters", () => { + expectConsistentEdges({ + addressPrefix: EdgeAddress.fromParts(["e", "1"]), + srcPrefix: NodeAddress.fromParts(["src", "2"]), + dstPrefix: NodeAddress.empty, + }); + }); + it("yields appropriate filter intersection", () => { + expectConsistentEdges({ + addressPrefix: EdgeAddress.empty, + srcPrefix: NodeAddress.fromParts(["src", "1"]), + dstPrefix: NodeAddress.fromParts(["dst", "2"]), + }); + }); + }); + + describe("edge filter options", () => { + it("requires `addressPrefix` to be present in provided options", () => { + expect(() => { + pagerankGraph() + // $ExpectFlowError + .edges({srcPrefix: src1, dstPrefix: dst1}); + }).toThrow("Invalid address prefix: undefined"); + }); + it("requires `srcPrefix` to be present in provided options", () => { + expect(() => { + pagerankGraph() + // $ExpectFlowError + .edges({addressPrefix: e11, dstPrefix: dst1}); + }).toThrow("Invalid src prefix: undefined"); + }); + + it("requires `dstPrefix` to be present in provided options", () => { + expect(() => { + pagerankGraph() + // $ExpectFlowError + .edges({addressPrefix: e11, srcPrefix: dst1}); + }).toThrow("Invalid dst prefix: undefined"); + }); + }); + }); + describe("runPagerank", () => { // The mathematical semantics of PageRank are thoroughly tested // in the markovChain module. The goal for these tests is just