PagerankGraph: Add toJSON/fromJSON (#1088)
* PagerankGraph: Add toJSON/fromJSON This commit adds serialization logic to `PagerankGraph`. As with many things in PagerankGraph, it's based on the corresponding logic in `Graph`. Much like graph, it stores data associated with nodes and edges (in this case, the scores and edge weights) in an ordered array rather than a map, so as to avoid repetitiously serializing the node and edge addresses. Test plan: Unit tests added, and they should be sufficient. Also take a look at the included snapshot.
This commit is contained in:
parent
7851c1b007
commit
17345fcca9
|
@ -0,0 +1,76 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`core/pagerankGraph to/from JSON matches expected snapshot 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"type": "sourcecred/pagerankGraph",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
Object {
|
||||
"froWeights": Array [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"graphJSON": Array [
|
||||
Object {
|
||||
"type": "sourcecred/graph",
|
||||
"version": "0.4.0",
|
||||
},
|
||||
Object {
|
||||
"edges": Array [
|
||||
Object {
|
||||
"address": Array [
|
||||
"hom",
|
||||
"1",
|
||||
],
|
||||
"dstIndex": 0,
|
||||
"srcIndex": 3,
|
||||
},
|
||||
Object {
|
||||
"address": Array [
|
||||
"hom",
|
||||
"2",
|
||||
],
|
||||
"dstIndex": 0,
|
||||
"srcIndex": 3,
|
||||
},
|
||||
Object {
|
||||
"address": Array [
|
||||
"loop",
|
||||
],
|
||||
"dstIndex": 2,
|
||||
"srcIndex": 2,
|
||||
},
|
||||
],
|
||||
"nodes": Array [
|
||||
Array [
|
||||
"dst",
|
||||
],
|
||||
Array [
|
||||
"isolated",
|
||||
],
|
||||
Array [
|
||||
"loop",
|
||||
],
|
||||
Array [
|
||||
"src",
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
"scores": Array [
|
||||
0.25,
|
||||
0.25,
|
||||
0.25,
|
||||
0.25,
|
||||
],
|
||||
"syntheticLoopWeight": 0.001,
|
||||
"toWeights": Array [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
],
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -2,7 +2,16 @@
|
|||
|
||||
import deepEqual from "lodash.isequal";
|
||||
|
||||
import {Graph, type Edge, type NodeAddressT, type EdgeAddressT} from "./graph";
|
||||
import {toCompat, fromCompat, type Compatible} from "../util/compat";
|
||||
import {
|
||||
Graph,
|
||||
type Edge,
|
||||
type NodeAddressT,
|
||||
type EdgeAddressT,
|
||||
type GraphJSON,
|
||||
sortedEdgeAddressesFromJSON,
|
||||
sortedNodeAddressesFromJSON,
|
||||
} from "./graph";
|
||||
import {
|
||||
distributionToNodeDistribution,
|
||||
createConnections,
|
||||
|
@ -25,6 +34,20 @@ export type WeightedEdge = {|
|
|||
+weight: EdgeWeight,
|
||||
|};
|
||||
|
||||
export opaque type PagerankGraphJSON = Compatible<{|
|
||||
+graphJSON: GraphJSON,
|
||||
// Score for every node, ordered by the sorted node address.
|
||||
+scores: $ReadOnlyArray<number>,
|
||||
// Weights for every edge, ordered by sorted edge address.
|
||||
// We could save the EdgeWeights directly rather than having separate
|
||||
// arrays for toWeights and froWeights, but this would lead to an inflated
|
||||
// JSON representation because we would be needlessly duplicating the keys
|
||||
// "toWeight" and "froWeight" themselves.
|
||||
+toWeights: $ReadOnlyArray<number>,
|
||||
+froWeights: $ReadOnlyArray<number>,
|
||||
+syntheticLoopWeight: number,
|
||||
|}>;
|
||||
|
||||
/**
|
||||
* Options to control how PageRank runs and when it stops
|
||||
*/
|
||||
|
@ -46,6 +69,8 @@ export type PagerankConvergenceReport = {|
|
|||
|
||||
export const DEFAULT_SYNTHETIC_LOOP_WEIGHT = 1e-3;
|
||||
|
||||
const COMPAT_INFO = {type: "sourcecred/pagerankGraph", version: "0.1.0"};
|
||||
|
||||
/**
|
||||
* PagerankGraph is a wrapper over the Graph class, which adds
|
||||
* the ability to run PageRank to compute scores on the Graph.
|
||||
|
@ -296,6 +321,79 @@ export class PagerankGraph {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize this graph into a PagerankJSON object.
|
||||
*
|
||||
* Returns a plain JavaScript object.
|
||||
*
|
||||
* For space efficency, we store the node scores as an array of numbers in
|
||||
* node-address-sorted order, and we store the edge weights as two arrays of
|
||||
* numbers in edge-address-sorted-order.
|
||||
*/
|
||||
toJSON(): PagerankGraphJSON {
|
||||
this._verifyGraphNotModified();
|
||||
|
||||
const graphJSON = this.graph().toJSON();
|
||||
const nodes = sortedNodeAddressesFromJSON(graphJSON);
|
||||
const scores: number[] = nodes.map((x) =>
|
||||
NullUtil.get(this._scores.get(x))
|
||||
);
|
||||
|
||||
const edgeAddresses = sortedEdgeAddressesFromJSON(graphJSON);
|
||||
const edgeWeights: EdgeWeight[] = edgeAddresses.map((x) =>
|
||||
NullUtil.get(this._edgeWeights.get(x))
|
||||
);
|
||||
const toWeights: number[] = edgeWeights.map((x) => x.toWeight);
|
||||
const froWeights: number[] = edgeWeights.map((x) => x.froWeight);
|
||||
|
||||
const rawJSON = {
|
||||
graphJSON,
|
||||
scores,
|
||||
toWeights,
|
||||
froWeights,
|
||||
syntheticLoopWeight: this.syntheticLoopWeight(),
|
||||
};
|
||||
|
||||
return toCompat(COMPAT_INFO, rawJSON);
|
||||
}
|
||||
|
||||
static fromJSON(json: PagerankGraphJSON): PagerankGraph {
|
||||
const {
|
||||
toWeights,
|
||||
froWeights,
|
||||
scores,
|
||||
graphJSON,
|
||||
syntheticLoopWeight,
|
||||
} = fromCompat(COMPAT_INFO, json);
|
||||
const graph = Graph.fromJSON(graphJSON);
|
||||
|
||||
const nodes = sortedNodeAddressesFromJSON(graphJSON);
|
||||
const scoreMap: Map<NodeAddressT, number> = new Map();
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
scoreMap.set(nodes[i], scores[i]);
|
||||
}
|
||||
|
||||
const edges = sortedEdgeAddressesFromJSON(graphJSON);
|
||||
const edgeWeights: Map<EdgeAddressT, EdgeWeight> = new Map();
|
||||
for (let i = 0; i < edges.length; i++) {
|
||||
const toWeight = toWeights[i];
|
||||
const froWeight = froWeights[i];
|
||||
edgeWeights.set(edges[i], {toWeight, froWeight});
|
||||
}
|
||||
|
||||
function evaluator(e: Edge): EdgeWeight {
|
||||
return NullUtil.get(edgeWeights.get(e.address));
|
||||
}
|
||||
|
||||
const prg = new PagerankGraph(graph, evaluator, syntheticLoopWeight);
|
||||
// TODO(#1020): It's a little hacky to force the scores in like this;
|
||||
// consider adding an optional constructor argument to allow manually
|
||||
// setting the scores at construction time, if we ever find a use case
|
||||
// that needs it.
|
||||
prg._scores = scoreMap;
|
||||
return prg;
|
||||
}
|
||||
|
||||
_verifyGraphNotModified() {
|
||||
if (this._graph.modificationCount() !== this._graphModificationCount) {
|
||||
throw new Error(
|
||||
|
|
|
@ -229,4 +229,33 @@ describe("core/pagerankGraph", () => {
|
|||
expect(() => pg.equals(pg)).toThrowError("has been modified");
|
||||
});
|
||||
});
|
||||
|
||||
describe("to/from JSON", () => {
|
||||
it("to->fro is identity", async () => {
|
||||
const pg = examplePagerankGraph();
|
||||
await pg.runPagerank({maxIterations: 1, convergenceThreshold: 0.01});
|
||||
const pgJSON = pg.toJSON();
|
||||
const pg_ = PagerankGraph.fromJSON(pgJSON);
|
||||
expect(pg.equals(pg_)).toBe(true);
|
||||
});
|
||||
it("fro->to is identity", async () => {
|
||||
const pg = examplePagerankGraph();
|
||||
await pg.runPagerank({maxIterations: 1, convergenceThreshold: 0.01});
|
||||
const pgJSON = pg.toJSON();
|
||||
const pg_ = PagerankGraph.fromJSON(pgJSON);
|
||||
const pgJSON_ = pg_.toJSON();
|
||||
expect(pgJSON).toEqual(pgJSON_);
|
||||
});
|
||||
it("is canonical with respect to the graph's history", async () => {
|
||||
const pg1 = new PagerankGraph(advancedGraph().graph1(), defaultEvaluator);
|
||||
const pg2 = new PagerankGraph(advancedGraph().graph2(), defaultEvaluator);
|
||||
const pg1JSON = pg1.toJSON();
|
||||
const pg2JSON = pg2.toJSON();
|
||||
expect(pg1JSON).toEqual(pg2JSON);
|
||||
});
|
||||
it("matches expected snapshot", () => {
|
||||
const pgJSON = examplePagerankGraph().toJSON();
|
||||
expect(pgJSON).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue