Compute the V1 output format from TimelineCred (#1782)
This commit builds on #1781, adding the logic for computing the first output format from TimelineCred. See #1773 for context. Test plan: The logic is simple, but has a couple interesting edge cases. I've added unit tests to cover them. `yarn test` passes.
This commit is contained in:
parent
b5fa5abb49
commit
610b9c4827
|
@ -4,9 +4,14 @@
|
|||
* This module defines a rich output format for cred scores, so that we can use
|
||||
* it to drive UIs and data analysis.
|
||||
*/
|
||||
import * as NullUtil from "../util/null";
|
||||
import {NodeAddress} from "../core/graph";
|
||||
import type {Alias} from "../plugins/identity/alias";
|
||||
import type {PluginDeclaration} from "./pluginDeclaration";
|
||||
import type {TimestampMs} from "../util/timestamp";
|
||||
import * as Timestamp from "../util/timestamp";
|
||||
import {TimelineCred} from "./timeline/timelineCred";
|
||||
import {nodeWeightEvaluator} from "../core/algorithm/weightEvaluator";
|
||||
|
||||
export type Index = number;
|
||||
export type CredFlow = {|+forwards: number, +backwards: number|};
|
||||
|
@ -42,6 +47,33 @@ export type OutputV1 = {|
|
|||
+plugins: $ReadOnlyArray<PluginDeclaration>,
|
||||
|};
|
||||
|
||||
export function fromTimelineCredAndPlugins(
|
||||
tc: TimelineCred,
|
||||
plugins: $ReadOnlyArray<PluginDeclaration>
|
||||
): OutputV1 {
|
||||
const {graph, weights} = tc.weightedGraph();
|
||||
const nodeEvaluator = nodeWeightEvaluator(weights);
|
||||
const orderedNodes = Array.from(graph.nodes()).map(
|
||||
({description, address, timestampMs}) => {
|
||||
const cred = NullUtil.get(tc.credNode(address)).total;
|
||||
// In TimelineCred, a node with a null timestamp will never mint cred, because we don't
|
||||
// know what period to mint it in.
|
||||
// When we transition to CredRank, we should remove this check.
|
||||
const minted = timestampMs == null ? 0 : nodeEvaluator(address);
|
||||
const timestamp =
|
||||
timestampMs == null ? null : Timestamp.fromNumber(timestampMs);
|
||||
return {
|
||||
address: NodeAddress.toParts(address),
|
||||
cred,
|
||||
minted,
|
||||
description,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
);
|
||||
return {orderedNodes, plugins};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data for Contributors. Note that each Contributor corresponds
|
||||
* to a specific node in the graph, which is retrievable via the NodeIndex.
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
// @flow
|
||||
|
||||
import deepFreeze from "deep-freeze";
|
||||
import {Graph, NodeAddress, EdgeAddress} from "../core/graph";
|
||||
import {TimelineCred} from "./timeline/timelineCred";
|
||||
import {defaultParams} from "./timeline/params";
|
||||
import {nodeWeightEvaluator} from "../core/algorithm/weightEvaluator";
|
||||
import {fromTimelineCredAndPlugins} from "./output";
|
||||
|
||||
describe("src/analysis/output", () => {
|
||||
const nodeType = {
|
||||
name: "node",
|
||||
pluralName: "nodes",
|
||||
prefix: NodeAddress.fromParts(["node"]),
|
||||
defaultWeight: 2,
|
||||
description: "a node",
|
||||
};
|
||||
const userType = {
|
||||
name: "user",
|
||||
pluralName: "users",
|
||||
prefix: NodeAddress.fromParts(["user"]),
|
||||
defaultWeight: 5,
|
||||
description: "a user",
|
||||
};
|
||||
const plugin = deepFreeze({
|
||||
name: "a plugin",
|
||||
nodePrefix: NodeAddress.empty,
|
||||
nodeTypes: [nodeType, userType],
|
||||
edgePrefix: EdgeAddress.empty,
|
||||
edgeTypes: [],
|
||||
userTypes: [userType],
|
||||
});
|
||||
|
||||
function example() {
|
||||
const aNode = {
|
||||
address: NodeAddress.fromParts(["node", "a"]),
|
||||
description: "a node",
|
||||
timestampMs: 123,
|
||||
};
|
||||
const bNode = {
|
||||
address: NodeAddress.fromParts(["node", "b"]),
|
||||
description: "b node",
|
||||
timestampMs: 125,
|
||||
};
|
||||
const userNode = {
|
||||
address: NodeAddress.fromParts(["user", "steven"]),
|
||||
description: "a steven",
|
||||
timestampMs: null,
|
||||
};
|
||||
const graph = new Graph().addNode(aNode).addNode(bNode).addNode(userNode);
|
||||
const edgeWeights = new Map();
|
||||
const nodeWeights = new Map()
|
||||
.set(NodeAddress.empty, 5)
|
||||
.set(bNode.address, 7);
|
||||
const weights = {nodeWeights, edgeWeights};
|
||||
const weightedGraph = {graph, weights};
|
||||
const intervals = [
|
||||
{startTimeMs: 0, endTimeMs: 1000},
|
||||
{startTimeMs: 1000, endTimeMs: 2000},
|
||||
];
|
||||
const addressToCred = new Map()
|
||||
.set(aNode.address, [1, 2])
|
||||
.set(bNode.address, [2, 4])
|
||||
.set(userNode.address, [5, 5]);
|
||||
const params = defaultParams();
|
||||
const plugins = [plugin];
|
||||
const timelineCred = new TimelineCred(
|
||||
weightedGraph,
|
||||
intervals,
|
||||
addressToCred,
|
||||
params,
|
||||
plugins
|
||||
);
|
||||
|
||||
const output = fromTimelineCredAndPlugins(timelineCred, plugins);
|
||||
|
||||
return {aNode, bNode, userNode, intervals, timelineCred, output};
|
||||
}
|
||||
|
||||
describe("output via fromTimelineCredAndPlugins", () => {
|
||||
it("contains plugins", () => {
|
||||
const {output} = example();
|
||||
expect(output.plugins).toEqual([plugin]);
|
||||
});
|
||||
it("nodes have address, timestamp, description, and ordering from the graph", () => {
|
||||
const {output, timelineCred} = example();
|
||||
const nodes = Array.from(timelineCred.weightedGraph().graph.nodes());
|
||||
expect(
|
||||
output.orderedNodes.map((n) => ({
|
||||
address: NodeAddress.fromParts(n.address),
|
||||
description: n.description,
|
||||
timestampMs: n.timestamp,
|
||||
}))
|
||||
).toEqual(nodes);
|
||||
});
|
||||
it("nodes' minted cred is computed correctly", () => {
|
||||
// The minted cred is equal to the node weight, except in the special
|
||||
// case where the timetsamp is null, in which case the minted cred is
|
||||
// zero (per semantics of TimelineCred).
|
||||
const {output, timelineCred} = example();
|
||||
const {weights} = timelineCred.weightedGraph();
|
||||
const nodeEvaluator = nodeWeightEvaluator(weights);
|
||||
let foundEdgeCase = false;
|
||||
for (const {address, minted, timestamp} of output.orderedNodes) {
|
||||
const weight = nodeEvaluator(NodeAddress.fromParts(address));
|
||||
if (timestamp == null && weight !== 0) {
|
||||
foundEdgeCase = true;
|
||||
expect(minted).toBe(0);
|
||||
} else {
|
||||
expect(minted).toBe(weight);
|
||||
}
|
||||
}
|
||||
expect(foundEdgeCase).toBe(true);
|
||||
});
|
||||
it("nodes' cred is equal to the total cred across time slices", () => {
|
||||
const {output, timelineCred} = example();
|
||||
for (const {address, cred} of output.orderedNodes) {
|
||||
const credNode = timelineCred.credNode(NodeAddress.fromParts(address));
|
||||
if (credNode == null) {
|
||||
throw new Error("Can't find node");
|
||||
}
|
||||
expect(cred).toEqual(credNode.total);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue