add `analysis/weightEvaluator`

This commit adds new weight evaluators for nodes and edges. Unlike the
previous evaluator, edges and nodes are handled as separate concerns,
rather than composing the node weights into the edge weights. I think
this separation is cleaner.

Both evaluators use only the address, not the full (Node or Edge)
object. Although we may want to give the edge evaluator access to the
full Edge later, if we decide we want node-type-differentiated edge
weights (e.g. if a hasParent edge has a different weight depending on
whether it is connected to an Issue or a Repository).

weightsToEdgeEvaluator has been refactored to use the new evaluators,
and has been given a deprecation notice.

Test plan: `yarn test`
This commit is contained in:
Dandelion Mané 2019-07-08 13:28:19 +01:00
parent 2335c5d844
commit cb236eff5d
3 changed files with 185 additions and 25 deletions

View File

@ -0,0 +1,65 @@
// @flow
import * as NullUtil from "../util/null";
import type {NodeAddressT, EdgeAddressT} from "../core/graph";
import type {NodeType, EdgeType} from "./types";
import type {Weights, EdgeWeight} from "./weights";
import {NodeTrie, EdgeTrie} from "../core/trie";
export type NodeWeightEvaluator = (NodeAddressT) => number;
export type EdgeWeightEvaluator = (EdgeAddressT) => EdgeWeight;
/**
* Given the weights and types, produces a NodeEvaluator, which assigns a weight to a
* NodeAddressT based on its type and whether it has any manual weight specified.
*
* Every node address is assigned a weight based on its most specific matching
* type (i.e. the type with the longest shared prefix). If that type has a
* weight specified in the typeWeights map, the specified weight will be used.
* If not, then the type's default weight is used. If no type matches a given
* node, then it will get a default weight of 1.
*
* If the node address has a manual weight specified in the manualWeights map,
* that weight will be multiplied by its type weight.
*/
export function nodeWeightEvaluator(
types: $ReadOnlyArray<NodeType>,
weights: Weights
): NodeWeightEvaluator {
const {
nodeTypeWeights: typeWeights,
nodeManualWeights: manualWeights,
} = weights;
const nodeTrie = new NodeTrie();
for (const {prefix, defaultWeight} of types) {
const weight = NullUtil.orElse(typeWeights.get(prefix), defaultWeight);
nodeTrie.add(prefix, weight);
}
return function nodeWeight(a: NodeAddressT): number {
const typeWeight = NullUtil.orElse(nodeTrie.getLast(a), 1);
const manualWeight = NullUtil.orElse(manualWeights.get(a), 1);
return typeWeight * manualWeight;
};
}
/**
* Given the weights and types, produce an EdgeEvaluator, which assigns a toWeight and froWeight
* to an edge address based only on its type.
*/
export function edgeWeightEvaluator(
types: $ReadOnlyArray<EdgeType>,
weights: Weights
): EdgeWeightEvaluator {
const typeWeights = weights.edgeTypeWeights;
const edgeTrie = new EdgeTrie();
for (const {prefix, defaultWeight} of types) {
const weight = NullUtil.orElse(typeWeights.get(prefix), defaultWeight);
edgeTrie.add(prefix, weight);
}
return function evaluator(address: EdgeAddressT) {
return NullUtil.orElse(edgeTrie.getLast(address), {
forwards: 1,
backwards: 1,
});
};
}

View File

@ -0,0 +1,110 @@
// @flow
import {NodeAddress, EdgeAddress} from "../core/graph";
import {nodeWeightEvaluator, edgeWeightEvaluator} from "./weightEvaluator";
import {defaultWeights} from "./weights";
describe("src/analysis/weightEvaluator", () => {
describe("nodeWeightEvaluator", () => {
const empty = NodeAddress.fromParts([]);
const foo = NodeAddress.fromParts(["foo"]);
const foobar = NodeAddress.fromParts(["foo", "bar"]);
const fooNodeType = Object.freeze({
name: "",
pluralName: "",
prefix: foo,
defaultWeight: 2,
description: "",
});
const fooBarNodeType = Object.freeze({
name: "",
pluralName: "",
prefix: foobar,
defaultWeight: 3,
description: "",
});
const types = Object.freeze([fooNodeType, fooBarNodeType]);
it("gives every node weight 1 with empty types and weights", () => {
const evaluator = nodeWeightEvaluator([], defaultWeights());
expect(evaluator(empty)).toEqual(1);
expect(evaluator(foo)).toEqual(1);
});
it("matches the most specific possible node type", () => {
const evaluator = nodeWeightEvaluator(types, defaultWeights());
expect(evaluator(empty)).toEqual(1);
expect(evaluator(foo)).toEqual(2);
expect(evaluator(foobar)).toEqual(3);
});
it("uses type weight overrides", () => {
const weights = defaultWeights();
weights.nodeTypeWeights.set(foo, 3);
weights.nodeTypeWeights.set(foobar, 4);
const evaluator = nodeWeightEvaluator(types, weights);
expect(evaluator(empty)).toEqual(1);
expect(evaluator(foo)).toEqual(3);
expect(evaluator(foobar)).toEqual(4);
});
it("uses manually-specified weights", () => {
const weights = defaultWeights();
weights.nodeManualWeights.set(foo, 3);
const evaluator = nodeWeightEvaluator([], weights);
expect(evaluator(empty)).toEqual(1);
expect(evaluator(foo)).toEqual(3);
expect(evaluator(foobar)).toEqual(1);
});
it("composes manual and type weights multiplicatively", () => {
const weights = defaultWeights();
weights.nodeManualWeights.set(foo, 3);
const evaluator = nodeWeightEvaluator(types, weights);
weights.nodeManualWeights.set(foo, 3);
expect(evaluator(empty)).toEqual(1);
expect(evaluator(foo)).toEqual(6);
expect(evaluator(foobar)).toEqual(3);
});
});
describe("edgeEvaluator", () => {
const foo = EdgeAddress.fromParts(["foo"]);
const foobar = EdgeAddress.fromParts(["foo", "bar"]);
const fooType = {
forwardName: "",
backwardName: "",
defaultWeight: Object.freeze({forwards: 2, backwards: 3}),
prefix: foo,
description: "",
};
const fooBarType = {
forwardName: "",
backwardName: "",
defaultWeight: Object.freeze({forwards: 4, backwards: 5}),
prefix: foobar,
description: "",
};
it("gives default 1,1 weights if no matching type", () => {
const evaluator = edgeWeightEvaluator([], defaultWeights());
expect(evaluator(foo)).toEqual({forwards: 1, backwards: 1});
});
it("uses weights for the most specific matching type", () => {
const evaluator = edgeWeightEvaluator(
[fooType, fooBarType],
defaultWeights()
);
expect(evaluator(foo)).toEqual({forwards: 2, backwards: 3});
expect(evaluator(foobar)).toEqual({forwards: 4, backwards: 5});
expect(evaluator(EdgeAddress.fromParts(["foo", "bar", "qox"]))).toEqual({
forwards: 4,
backwards: 5,
});
});
it("uses weight overrides if available", () => {
const weights = defaultWeights();
weights.edgeTypeWeights.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, backwards: 5});
});
});
});

View File

@ -1,11 +1,10 @@
// @flow
import * as NullUtil from "../util/null";
import type {Edge, NodeAddressT} from "../core/graph";
import type {Edge} from "../core/graph";
import type {NodeAndEdgeTypes} from "./types";
import type {Weights} from "./weights";
import type {EdgeEvaluator} from "./pagerank";
import {NodeTrie, EdgeTrie} from "../core/trie";
import {nodeWeightEvaluator, edgeWeightEvaluator} from "./weightEvaluator";
/**
* Given the weight choices and the node and edge types, produces an edge
@ -22,37 +21,23 @@ import {NodeTrie, EdgeTrie} from "../core/trie";
*
* The node and edge types are required so that we know what the default weights
* are for types whose weights are not manually specified.
*
* NOTE: This method is deprecated. Going forward, we should use node weights
* as a direct input of their own (e.g. as a seed vector and for determining
* 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 {nodeTypeWeights, edgeTypeWeights, nodeManualWeights} = weights;
const nodeTrie = new NodeTrie();
for (const {prefix, defaultWeight} of types.nodeTypes) {
const weight = NullUtil.orElse(nodeTypeWeights.get(prefix), defaultWeight);
nodeTrie.add(prefix, weight);
}
const edgeTrie = new EdgeTrie();
for (const {prefix, defaultWeight} of types.edgeTypes) {
const weight = NullUtil.orElse(edgeTypeWeights.get(prefix), defaultWeight);
edgeTrie.add(prefix, weight);
}
function nodeWeight(n: NodeAddressT): number {
const typeWeight = NullUtil.orElse(nodeTrie.getLast(n), 1);
const manualWeight = NullUtil.orElse(nodeManualWeights.get(n), 1);
return typeWeight * manualWeight;
}
const nodeWeight = nodeWeightEvaluator(types.nodeTypes, weights);
const edgeWeight = edgeWeightEvaluator(types.edgeTypes, weights);
return function evaluator(edge: Edge) {
const srcWeight = nodeWeight(edge.src);
const dstWeight = nodeWeight(edge.dst);
const edgeWeight = NullUtil.orElse(edgeTrie.getLast(edge.address), {
forwards: 1,
backwards: 1,
});
const {forwards, backwards} = edgeWeight;
const {forwards, backwards} = edgeWeight(edge.address);
return {
forwards: dstWeight * forwards,
backwards: srcWeight * backwards,