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()`
This commit is contained in:
Brian Litwin 2019-02-27 21:44:21 -05:00 committed by GitHub
parent 656a2d1543
commit b16c374a2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 127 additions and 5 deletions

View File

@ -6,6 +6,7 @@ import {toCompat, fromCompat, type Compatible} from "../util/compat";
import { import {
Graph, Graph,
type Edge, type Edge,
type EdgesOptions,
type NodeAddressT, type NodeAddressT,
type EdgeAddressT, type EdgeAddressT,
type GraphJSON, type GraphJSON,
@ -237,15 +238,18 @@ export class PagerankGraph {
/** /**
* Provides edge and weight for every edge in the underlying graph. * 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<WeightedEdge> { edges(options?: EdgesOptions): Iterator<WeightedEdge> {
this._verifyGraphNotModified(); this._verifyGraphNotModified();
return this._edgesIterator(); const iterator = this._graph.edges(options);
return this._edgesIterator(iterator);
} }
*_edgesIterator(): Iterator<WeightedEdge> { *_edgesIterator(iterator: Iterator<Edge>): Iterator<WeightedEdge> {
for (const edge of this._graph.edges()) { for (const edge of iterator) {
const weight = NullUtil.get(this._edgeWeights.get(edge.address)); const weight = NullUtil.get(this._edgeWeights.get(edge.address));
yield {edge, weight}; yield {edge, weight};
} }

View File

@ -7,6 +7,7 @@ import {
EdgeAddress, EdgeAddress,
type NodeAddressT, type NodeAddressT,
type Edge, type Edge,
type EdgesOptions,
} from "./graph"; } from "./graph";
import {PagerankGraph} from "./pagerankGraph"; import {PagerankGraph} from "./pagerankGraph";
import {advancedGraph} from "./graphTestUtil"; 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", () => { describe("runPagerank", () => {
// The mathematical semantics of PageRank are thoroughly tested // The mathematical semantics of PageRank are thoroughly tested
// in the markovChain module. The goal for these tests is just // in the markovChain module. The goal for these tests is just