From cb236eff5df62fe8eacbc6520efe96a51fbd52df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Mon, 8 Jul 2019 13:28:19 +0100 Subject: [PATCH] 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` --- src/analysis/weightEvaluator.js | 65 +++++++++++++++ src/analysis/weightEvaluator.test.js | 110 +++++++++++++++++++++++++ src/analysis/weightsToEdgeEvaluator.js | 35 +++----- 3 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 src/analysis/weightEvaluator.js create mode 100644 src/analysis/weightEvaluator.test.js diff --git a/src/analysis/weightEvaluator.js b/src/analysis/weightEvaluator.js new file mode 100644 index 0000000..21f0c57 --- /dev/null +++ b/src/analysis/weightEvaluator.js @@ -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, + 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, + 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, + }); + }; +} diff --git a/src/analysis/weightEvaluator.test.js b/src/analysis/weightEvaluator.test.js new file mode 100644 index 0000000..5b3b217 --- /dev/null +++ b/src/analysis/weightEvaluator.test.js @@ -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}); + }); + }); +}); diff --git a/src/analysis/weightsToEdgeEvaluator.js b/src/analysis/weightsToEdgeEvaluator.js index eede448..c6171a6 100644 --- a/src/analysis/weightsToEdgeEvaluator.js +++ b/src/analysis/weightsToEdgeEvaluator.js @@ -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,