Remove types from TimelinePagerank (#1627)
This commit modifies the timelinePagerank module so that it no longer takes in node/edge types. Instead, the timelinePagerank just takes a WeightedGraph and uses weights from that WeightedGraph. This is a key part of decoupling the core cred computation logic from the plugin logic, as described in #1557. I also modified the timelinePagerank module's immediate dependencies (the weightEvaluator module) to do the same. Since the weight evaluators now have a simpler contract (no overriding, etc), the unit tests have been simplified. Test plan: It's a simple refactor, so `yarn test` should be sufficient. As a bit of added caution, I manually tested changing weights in the frontend, and verified that cred updates as expected.
This commit is contained in:
parent
9deeb93142
commit
5b2f38114b
|
@ -8,7 +8,7 @@ import {toCompat, fromCompat, type Compatible} from "../../util/compat";
|
|||
import {type Interval} from "./interval";
|
||||
import {timelinePagerank} from "./timelinePagerank";
|
||||
import {distributionToCred} from "./distributionToCred";
|
||||
import {type PluginDeclaration, combineTypes} from "../pluginDeclaration";
|
||||
import {type PluginDeclaration} from "../pluginDeclaration";
|
||||
import {type NodeAddressT, NodeAddress, type Node} from "../../core/graph";
|
||||
import * as WeightedGraph from "../../core/weightedGraph";
|
||||
import {type Weights as WeightsT} from "../../core/weights";
|
||||
|
@ -189,16 +189,13 @@ export class TimelineCred {
|
|||
plugins: $ReadOnlyArray<PluginDeclaration>,
|
||||
|}): Promise<TimelineCred> {
|
||||
const {weightedGraph, params, plugins} = opts;
|
||||
const {graph, weights} = weightedGraph;
|
||||
const {graph} = weightedGraph;
|
||||
const fullParams = params == null ? defaultParams() : partialParams(params);
|
||||
const nodeOrder = Array.from(graph.nodes()).map((x) => x.address);
|
||||
const types = combineTypes(plugins);
|
||||
const userTypes = [].concat(...plugins.map((x) => x.userTypes));
|
||||
const scorePrefixes = userTypes.map((x) => x.prefix);
|
||||
const distribution = await timelinePagerank(
|
||||
graph,
|
||||
types,
|
||||
weights,
|
||||
weightedGraph,
|
||||
fullParams.intervalDecay,
|
||||
fullParams.alpha
|
||||
);
|
||||
|
|
|
@ -7,8 +7,7 @@ import deepFreeze from "deep-freeze";
|
|||
import {sum} from "d3-array";
|
||||
import * as NullUtil from "../../util/null";
|
||||
import {Graph, type NodeAddressT, type Edge, type Node} from "../../core/graph";
|
||||
import {type NodeAndEdgeTypes} from "../types";
|
||||
import {type Weights} from "../../core/weights";
|
||||
import {type WeightedGraph} from "../../core/weightedGraph";
|
||||
import {type Interval, partitionGraph} from "./interval";
|
||||
import {
|
||||
nodeWeightEvaluator,
|
||||
|
@ -98,9 +97,7 @@ export type TimelineDistributions = $ReadOnlyArray<{|
|
|||
* the pieces and run PageRank for each interval.
|
||||
*/
|
||||
export async function timelinePagerank(
|
||||
graph: Graph,
|
||||
types: NodeAndEdgeTypes,
|
||||
weights: Weights,
|
||||
weightedGraph: WeightedGraph,
|
||||
intervalDecay: number,
|
||||
alpha: number
|
||||
): Promise<TimelineDistributions> {
|
||||
|
@ -112,24 +109,26 @@ export async function timelinePagerank(
|
|||
}
|
||||
// Produce the evaluators we will use to get the baseline weight for each
|
||||
// node and edge
|
||||
const nodeEvaluator = nodeWeightEvaluator(types.nodeTypes, weights);
|
||||
const edgeEvaluator = edgeWeightEvaluator(types.edgeTypes, weights);
|
||||
const nodeEvaluator = nodeWeightEvaluator(weightedGraph.weights);
|
||||
const edgeEvaluator = edgeWeightEvaluator(weightedGraph.weights);
|
||||
|
||||
const graphPartitionSlices = partitionGraph(graph);
|
||||
const graphPartitionSlices = partitionGraph(weightedGraph.graph);
|
||||
if (graphPartitionSlices.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const intervals = graphPartitionSlices.map((x) => x.interval);
|
||||
const nodeCreationHistory = graphPartitionSlices.map((x) => x.nodes);
|
||||
const edgeCreationHistory = graphPartitionSlices.map((x) => x.edges);
|
||||
const nodeOrder = Array.from(graph.nodes()).map((x) => x.address);
|
||||
const nodeOrder = Array.from(weightedGraph.graph.nodes()).map(
|
||||
(x) => x.address
|
||||
);
|
||||
const nodeWeightIterator = _timelineNodeWeights(
|
||||
nodeCreationHistory,
|
||||
nodeEvaluator,
|
||||
intervalDecay
|
||||
);
|
||||
const markovChainIterator = _timelineMarkovChain(
|
||||
graph,
|
||||
weightedGraph.graph,
|
||||
edgeCreationHistory,
|
||||
edgeEvaluator,
|
||||
intervalDecay
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
// @flow
|
||||
|
||||
import type {NodeAddressT, EdgeAddressT} from "../core/graph";
|
||||
import type {NodeType, EdgeType} from "./types";
|
||||
import type {
|
||||
Weights as WeightsT,
|
||||
EdgeWeight,
|
||||
NodeWeight,
|
||||
} from "../core/weights";
|
||||
import * as Weights from "../core/weights";
|
||||
import {NodeTrie, EdgeTrie} from "../core/trie";
|
||||
|
||||
export type NodeWeightEvaluator = (NodeAddressT) => NodeWeight;
|
||||
|
@ -26,19 +24,9 @@ export type EdgeWeightEvaluator = (EdgeAddressT) => EdgeWeight;
|
|||
* legacy affordance; shortly we will remove the NodeTypes and require that the
|
||||
* plugins provide the type weights when the Weights object is constructed.
|
||||
*/
|
||||
export function nodeWeightEvaluator(
|
||||
types: $ReadOnlyArray<NodeType>,
|
||||
weights: WeightsT
|
||||
): NodeWeightEvaluator {
|
||||
const {nodeWeights} = Weights.copy(weights);
|
||||
|
||||
for (const {prefix, defaultWeight} of types) {
|
||||
if (!nodeWeights.has(prefix)) {
|
||||
nodeWeights.set(prefix, defaultWeight);
|
||||
}
|
||||
}
|
||||
export function nodeWeightEvaluator(weights: WeightsT): NodeWeightEvaluator {
|
||||
const nodeTrie: NodeTrie<NodeWeight> = new NodeTrie();
|
||||
for (const [prefix, weight] of nodeWeights.entries()) {
|
||||
for (const [prefix, weight] of weights.nodeWeights.entries()) {
|
||||
nodeTrie.add(prefix, weight);
|
||||
}
|
||||
return function nodeWeight(a: NodeAddressT): NodeWeight {
|
||||
|
@ -61,18 +49,9 @@ export function nodeWeightEvaluator(
|
|||
* directly in the weights object, so that producing weight evaluators will no
|
||||
* longer depend on having plugin declarations on hand.
|
||||
*/
|
||||
export function edgeWeightEvaluator(
|
||||
types: $ReadOnlyArray<EdgeType>,
|
||||
weights: WeightsT
|
||||
): EdgeWeightEvaluator {
|
||||
const {edgeWeights} = Weights.copy(weights);
|
||||
for (const {prefix, defaultWeight} of types) {
|
||||
if (!edgeWeights.has(prefix)) {
|
||||
edgeWeights.set(prefix, defaultWeight);
|
||||
}
|
||||
}
|
||||
export function edgeWeightEvaluator(weights: WeightsT): EdgeWeightEvaluator {
|
||||
const edgeTrie: EdgeTrie<EdgeWeight> = new EdgeTrie();
|
||||
for (const [prefix, weight] of edgeWeights.entries()) {
|
||||
for (const [prefix, weight] of weights.edgeWeights.entries()) {
|
||||
edgeTrie.add(prefix, weight);
|
||||
}
|
||||
return function evaluator(address: EdgeAddressT): EdgeWeight {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import deepFreeze from "deep-freeze";
|
||||
import {NodeAddress, EdgeAddress} from "../core/graph";
|
||||
import {nodeWeightEvaluator, edgeWeightEvaluator} from "./weightEvaluator";
|
||||
import * as Weights from "../core/weights";
|
||||
|
@ -11,79 +10,33 @@ describe("src/analysis/weightEvaluator", () => {
|
|||
const foo = NodeAddress.fromParts(["foo"]);
|
||||
const foobar = NodeAddress.fromParts(["foo", "bar"]);
|
||||
|
||||
const fooNodeType = deepFreeze({
|
||||
name: "",
|
||||
pluralName: "",
|
||||
prefix: foo,
|
||||
defaultWeight: 2,
|
||||
description: "",
|
||||
});
|
||||
|
||||
const fooBarNodeType = deepFreeze({
|
||||
name: "",
|
||||
pluralName: "",
|
||||
prefix: foobar,
|
||||
defaultWeight: 3,
|
||||
description: "",
|
||||
});
|
||||
|
||||
const types = deepFreeze([fooNodeType, fooBarNodeType]);
|
||||
|
||||
it("gives every node weight 1 with empty types and weights", () => {
|
||||
const evaluator = nodeWeightEvaluator([], Weights.empty());
|
||||
const evaluator = nodeWeightEvaluator(Weights.empty());
|
||||
expect(evaluator(empty)).toEqual(1);
|
||||
expect(evaluator(foo)).toEqual(1);
|
||||
});
|
||||
it("composes matching weights multiplicatively", () => {
|
||||
const evaluator = nodeWeightEvaluator(types, Weights.empty());
|
||||
const weights = Weights.empty();
|
||||
weights.nodeWeights.set(foo, 2);
|
||||
weights.nodeWeights.set(foobar, 3);
|
||||
const evaluator = nodeWeightEvaluator(weights);
|
||||
expect(evaluator(empty)).toEqual(1);
|
||||
expect(evaluator(foo)).toEqual(2);
|
||||
expect(evaluator(foobar)).toEqual(6);
|
||||
});
|
||||
it("explicitly set weights on type prefixes override the type weights", () => {
|
||||
const weights = Weights.empty();
|
||||
weights.nodeWeights.set(foo, 3);
|
||||
weights.nodeWeights.set(foobar, 4);
|
||||
const evaluator = nodeWeightEvaluator(types, weights);
|
||||
expect(evaluator(empty)).toEqual(1);
|
||||
expect(evaluator(foo)).toEqual(3);
|
||||
expect(evaluator(foobar)).toEqual(12);
|
||||
});
|
||||
it("uses manually-specified weights", () => {
|
||||
const weights = Weights.empty();
|
||||
weights.nodeWeights.set(foo, 3);
|
||||
const evaluator = nodeWeightEvaluator([], weights);
|
||||
expect(evaluator(empty)).toEqual(1);
|
||||
expect(evaluator(foo)).toEqual(3);
|
||||
expect(evaluator(foobar)).toEqual(3);
|
||||
});
|
||||
});
|
||||
describe("edgeEvaluator", () => {
|
||||
const foo = EdgeAddress.fromParts(["foo"]);
|
||||
const foobar = EdgeAddress.fromParts(["foo", "bar"]);
|
||||
const fooType = deepFreeze({
|
||||
forwardName: "",
|
||||
backwardName: "",
|
||||
defaultWeight: {forwards: 2, backwards: 3},
|
||||
prefix: foo,
|
||||
description: "",
|
||||
});
|
||||
const fooBarType = deepFreeze({
|
||||
forwardName: "",
|
||||
backwardName: "",
|
||||
defaultWeight: {forwards: 4, backwards: 5},
|
||||
prefix: foobar,
|
||||
description: "",
|
||||
});
|
||||
it("gives default 1,1 weights if no matching type", () => {
|
||||
const evaluator = edgeWeightEvaluator([], Weights.empty());
|
||||
const evaluator = edgeWeightEvaluator(Weights.empty());
|
||||
expect(evaluator(foo)).toEqual({forwards: 1, backwards: 1});
|
||||
});
|
||||
it("composes weights multiplicatively for all matching types", () => {
|
||||
const evaluator = edgeWeightEvaluator(
|
||||
[fooType, fooBarType],
|
||||
Weights.empty()
|
||||
);
|
||||
const weights = Weights.empty();
|
||||
weights.edgeWeights.set(foo, {forwards: 2, backwards: 3});
|
||||
weights.edgeWeights.set(foobar, {forwards: 4, backwards: 5});
|
||||
const evaluator = edgeWeightEvaluator(weights);
|
||||
expect(evaluator(foo)).toEqual({forwards: 2, backwards: 3});
|
||||
expect(evaluator(foobar)).toEqual({forwards: 8, backwards: 15});
|
||||
expect(evaluator(EdgeAddress.fromParts(["foo", "bar", "qox"]))).toEqual({
|
||||
|
@ -91,12 +44,5 @@ describe("src/analysis/weightEvaluator", () => {
|
|||
backwards: 15,
|
||||
});
|
||||
});
|
||||
it("explicit weights override defaults from types", () => {
|
||||
const weights = Weights.empty();
|
||||
weights.edgeWeights.set(foo, {forwards: 99, backwards: 101});
|
||||
const evaluator = edgeWeightEvaluator([fooType, fooBarType], weights);
|
||||
expect(evaluator(foo)).toEqual({forwards: 99, backwards: 101});
|
||||
expect(evaluator(foobar)).toEqual({forwards: 4 * 99, backwards: 5 * 101});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import type {Edge} from "../core/graph";
|
||||
import type {NodeAndEdgeTypes} from "./types";
|
||||
import type {Weights} from "../core/weights";
|
||||
import type {EdgeEvaluator} from "./pagerank";
|
||||
import {nodeWeightEvaluator, edgeWeightEvaluator} from "./weightEvaluator";
|
||||
|
@ -27,12 +26,9 @@ import {nodeWeightEvaluator, edgeWeightEvaluator} from "./weightEvaluator";
|
|||
* cred weighting) rather than as a component of the edge weight. This method
|
||||
* will be removed when the 'legacy cred' UI is removed.
|
||||
*/
|
||||
export function weightsToEdgeEvaluator(
|
||||
weights: Weights,
|
||||
types: NodeAndEdgeTypes
|
||||
): EdgeEvaluator {
|
||||
const nodeWeight = nodeWeightEvaluator(types.nodeTypes, weights);
|
||||
const edgeWeight = edgeWeightEvaluator(types.edgeTypes, weights);
|
||||
export function weightsToEdgeEvaluator(weights: Weights): EdgeEvaluator {
|
||||
const nodeWeight = nodeWeightEvaluator(weights);
|
||||
const edgeWeight = edgeWeightEvaluator(weights);
|
||||
|
||||
return function evaluator(edge: Edge) {
|
||||
const srcWeight = nodeWeight(edge.src);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import deepFreeze from "deep-freeze";
|
||||
import {NodeAddress, EdgeAddress} from "../core/graph";
|
||||
import {type Weights as WeightsT} from "../core/weights";
|
||||
import * as Weights from "../core/weights";
|
||||
|
@ -16,57 +15,19 @@ describe("analysis/weightsToEdgeEvaluator", () => {
|
|||
timestampMs: 0,
|
||||
};
|
||||
|
||||
const fallbackNodeType = deepFreeze({
|
||||
name: "",
|
||||
pluralName: "",
|
||||
prefix: NodeAddress.empty,
|
||||
defaultWeight: 1,
|
||||
description: "",
|
||||
});
|
||||
|
||||
const srcNodeType = deepFreeze({
|
||||
name: "",
|
||||
pluralName: "",
|
||||
prefix: src,
|
||||
defaultWeight: 2,
|
||||
description: "",
|
||||
});
|
||||
|
||||
const fallbackEdgeType = deepFreeze({
|
||||
forwardName: "",
|
||||
backwardName: "",
|
||||
defaultWeight: {forwards: 1, backwards: 1},
|
||||
prefix: EdgeAddress.empty,
|
||||
description: "",
|
||||
});
|
||||
|
||||
function evaluateEdge(weights: WeightsT) {
|
||||
const evaluator = weightsToEdgeEvaluator(weights, {
|
||||
nodeTypes: [fallbackNodeType, srcNodeType],
|
||||
edgeTypes: [fallbackEdgeType],
|
||||
});
|
||||
const evaluator = weightsToEdgeEvaluator(weights);
|
||||
return evaluator(edge);
|
||||
}
|
||||
|
||||
it("applies default weights when none are specified", () => {
|
||||
expect(evaluateEdge(Weights.empty())).toEqual({forwards: 1, backwards: 2});
|
||||
expect(evaluateEdge(Weights.empty())).toEqual({forwards: 1, backwards: 1});
|
||||
});
|
||||
|
||||
it("matches all prefixes of the nodes in scope", () => {
|
||||
const weights = Weights.empty();
|
||||
weights.nodeWeights.set(NodeAddress.empty, 99);
|
||||
expect(evaluateEdge(weights)).toEqual({forwards: 99, backwards: 2 * 99});
|
||||
});
|
||||
|
||||
it("takes manually specified edge type weights into account", () => {
|
||||
const weights = Weights.empty();
|
||||
// Note that here we grab the fallout edge type. This also verifies that
|
||||
// we are doing prefix matching on the types (rather than exact matching).
|
||||
weights.edgeWeights.set(EdgeAddress.empty, {
|
||||
forwards: 6,
|
||||
backwards: 12,
|
||||
});
|
||||
expect(evaluateEdge(weights)).toEqual({forwards: 6, backwards: 24});
|
||||
expect(evaluateEdge(weights)).toEqual({forwards: 99, backwards: 99});
|
||||
});
|
||||
|
||||
it("an explicit weight on a prefix overrides the type weight", () => {
|
||||
|
@ -76,10 +37,7 @@ describe("analysis/weightsToEdgeEvaluator", () => {
|
|||
});
|
||||
|
||||
it("uses 1 as a default weight for unmatched nodes and edges", () => {
|
||||
const evaluator = weightsToEdgeEvaluator(Weights.empty(), {
|
||||
nodeTypes: [],
|
||||
edgeTypes: [],
|
||||
});
|
||||
const evaluator = weightsToEdgeEvaluator(Weights.empty());
|
||||
expect(evaluator(edge)).toEqual({forwards: 1, backwards: 1});
|
||||
});
|
||||
|
||||
|
|
|
@ -15,10 +15,7 @@ import {TimelineCred} from "../../analysis/timeline/timelineCred";
|
|||
|
||||
import type {Weights} from "../../core/weights";
|
||||
import {weightsToEdgeEvaluator} from "../../analysis/weightsToEdgeEvaluator";
|
||||
import {
|
||||
combineTypes,
|
||||
type PluginDeclarations,
|
||||
} from "../../analysis/pluginDeclaration";
|
||||
import {type PluginDeclarations} from "../../analysis/pluginDeclaration";
|
||||
|
||||
/*
|
||||
This models the UI states of the credExplorer/App as a state machine.
|
||||
|
@ -157,11 +154,10 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
|
|||
this.setState(loadingState);
|
||||
const graph = state.timelineCred.weightedGraph().graph;
|
||||
let newState: ?AppState;
|
||||
const types = combineTypes(state.pluginDeclarations);
|
||||
try {
|
||||
const pagerankNodeDecomposition = await this.pagerank(
|
||||
graph,
|
||||
weightsToEdgeEvaluator(weights, types),
|
||||
weightsToEdgeEvaluator(weights),
|
||||
{
|
||||
verbose: true,
|
||||
totalScoreNodePrefix: totalScoreNodePrefix,
|
||||
|
|
Loading…
Reference in New Issue