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:
Dandelion Mané 2020-02-05 11:14:04 -08:00 committed by GitHub
parent 9deeb93142
commit 5b2f38114b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 35 additions and 164 deletions

View File

@ -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
);

View File

@ -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

View File

@ -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 {

View File

@ -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});
});
});
});

View File

@ -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);

View File

@ -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});
});

View File

@ -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,