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:
parent
2335c5d844
commit
cb236eff5d
|
@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -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});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,11 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as NullUtil from "../util/null";
|
import type {Edge} from "../core/graph";
|
||||||
import type {Edge, NodeAddressT} from "../core/graph";
|
|
||||||
import type {NodeAndEdgeTypes} from "./types";
|
import type {NodeAndEdgeTypes} from "./types";
|
||||||
import type {Weights} from "./weights";
|
import type {Weights} from "./weights";
|
||||||
import type {EdgeEvaluator} from "./pagerank";
|
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
|
* 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
|
* The node and edge types are required so that we know what the default weights
|
||||||
* are for types whose weights are not manually specified.
|
* 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(
|
export function weightsToEdgeEvaluator(
|
||||||
weights: Weights,
|
weights: Weights,
|
||||||
types: NodeAndEdgeTypes
|
types: NodeAndEdgeTypes
|
||||||
): EdgeEvaluator {
|
): EdgeEvaluator {
|
||||||
const {nodeTypeWeights, edgeTypeWeights, nodeManualWeights} = weights;
|
const nodeWeight = nodeWeightEvaluator(types.nodeTypes, weights);
|
||||||
const nodeTrie = new NodeTrie();
|
const edgeWeight = edgeWeightEvaluator(types.edgeTypes, weights);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return function evaluator(edge: Edge) {
|
return function evaluator(edge: Edge) {
|
||||||
const srcWeight = nodeWeight(edge.src);
|
const srcWeight = nodeWeight(edge.src);
|
||||||
const dstWeight = nodeWeight(edge.dst);
|
const dstWeight = nodeWeight(edge.dst);
|
||||||
const edgeWeight = NullUtil.orElse(edgeTrie.getLast(edge.address), {
|
const {forwards, backwards} = edgeWeight(edge.address);
|
||||||
forwards: 1,
|
|
||||||
backwards: 1,
|
|
||||||
});
|
|
||||||
const {forwards, backwards} = edgeWeight;
|
|
||||||
return {
|
return {
|
||||||
forwards: dstWeight * forwards,
|
forwards: dstWeight * forwards,
|
||||||
backwards: srcWeight * backwards,
|
backwards: srcWeight * backwards,
|
||||||
|
|
Loading…
Reference in New Issue