Skip to content

Commit

Permalink
Share default TimelineCredParameters (#1376)
Browse files Browse the repository at this point in the history
At present, every place in the codebase that needs
TimelineCredParameters constructs them ad-hoc, meaning we don't have any
shared defaults across different consumers.

This commit adds a new type, `PartialTimelineCredParameters`, which
is basically `TimelineCredParameters` with every field marked optional.
Callers can then choose to override any fields where they want
non-default values. A new internal `partialParams` function promotes
these partial parameters to full parameters.

All the public interfaces for using params (namely,
`TimelineCred.compute` and `TimelineCred.reanalyze`) now accept optional
partial params. If the params are not specified, default values are
used; if partial params are provided, all the explicitly provided values
are used, and unspecified values are initialized to default values.

Test plan: A simple unit test was added to ensure that weights overrides
work as intended. `git grep "intervalDecay: "` reveals that there are no
other explicit parameter constructions in the codebase. All existing
unit tests pass.
  • Loading branch information
teamdandelion committed Sep 12, 2019
1 parent def1fef commit 0a0010f
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 42 deletions.
44 changes: 44 additions & 0 deletions src/analysis/timeline/params.js
Expand Up @@ -5,6 +5,7 @@ import {
type WeightsJSON,
toJSON as weightsToJSON,
fromJSON as weightsFromJSON,
defaultWeights,
} from "../weights";

/**
Expand All @@ -31,6 +32,20 @@ export type TimelineCredParameters = {|
+weights: Weights,
|};

export const DEFAULT_ALPHA = 0.05;
export const DEFAULT_INTERVAL_DECAY = 0.5;

/**
* The PartialTimelineCredParameters are a version of TimelineCredParameters
* where every field has been marked optional, to make it convenient for API
* clients to override just the parameters they want to.
*/
export type PartialTimelineCredParameters = {|
+alpha?: number,
+intervalDecay?: number,
+weights?: Weights,
|};

export type TimelineCredParametersJSON = {|
+alpha: number,
+intervalDecay: number,
Expand All @@ -56,3 +71,32 @@ export function paramsFromJSON(
weights: weightsFromJSON(p.weights),
};
}

/**
* Exports the default TimelineCredParameters.
*
* End consumers of SourceCred will not need to depend on this; it's
* provided for implementation of SourceCred's APIs.
*/
export function defaultParams(): TimelineCredParameters {
return {
alpha: DEFAULT_ALPHA,
intervalDecay: DEFAULT_INTERVAL_DECAY,
weights: defaultWeights(),
};
}

/**
* Promote PartialTimelineCredParameters to TimelineCredParameters.
*
* This takes PartialTimelineCredParameters and mixes them with the
* default parameters to provide a full TimelineCredParameters.
*
* End consumers of SourceCred will not need to depend on this; it's
* provided for implementation of SourceCred's APIs.
*/
export function partialParams(
partial: PartialTimelineCredParameters
): TimelineCredParameters {
return {...defaultParams(), ...partial};
}
54 changes: 51 additions & 3 deletions src/analysis/timeline/params.test.js
@@ -1,19 +1,67 @@
// @flow

import {paramsToJSON, paramsFromJSON} from "./params";
import {
paramsToJSON,
paramsFromJSON,
defaultParams,
partialParams,
type TimelineCredParameters,
DEFAULT_ALPHA,
DEFAULT_INTERVAL_DECAY,
} from "./params";
import {defaultWeights} from "../weights";
import {NodeAddress} from "../../core/graph";

describe("analysis/timeline/params", () => {
it("JSON round trip", () => {
const customWeights = () => {
const weights = defaultWeights();
// Ensure it works with non-default weights
weights.nodeManualWeights.set(NodeAddress.empty, 33);
const p = {alpha: 0.5, intervalDecay: 0.5, weights};
return weights;
};
it("JSON round trip", () => {
const p: TimelineCredParameters = {
alpha: 0.1337,
intervalDecay: 0.31337,
weights: customWeights(),
};
const j = paramsToJSON(p);
const p_ = paramsFromJSON(j);
const j_ = paramsToJSON(p_);
expect(j).toEqual(j_);
expect(p).toEqual(p_);
});
it("defaultParams", () => {
const expected: TimelineCredParameters = {
alpha: DEFAULT_ALPHA,
intervalDecay: DEFAULT_INTERVAL_DECAY,
weights: defaultWeights(),
};
expect(defaultParams()).toEqual(expected);
});
describe("partialParams", () => {
it("uses default values if no overrides provided", () => {
const params = partialParams({});
expect(params).toEqual(defaultParams());
});
it("accepts an alpha override", () => {
const params = partialParams({alpha: 0.99});
expect(params.weights).toEqual(defaultWeights());
expect(params.alpha).toEqual(0.99);
expect(params.intervalDecay).toEqual(DEFAULT_INTERVAL_DECAY);
});
it("accepts weights override", () => {
const weights = customWeights();
const params = partialParams({weights});
expect(params.weights).toEqual(weights);
expect(params.alpha).toEqual(DEFAULT_ALPHA);
expect(params.intervalDecay).toEqual(DEFAULT_INTERVAL_DECAY);
});
it("accepts intervalDecay override", () => {
const params = partialParams({intervalDecay: 0.1});
expect(params.weights).toEqual(defaultWeights());
expect(params.alpha).toEqual(DEFAULT_ALPHA);
expect(params.intervalDecay).toEqual(0.1);
});
});
});
32 changes: 21 additions & 11 deletions src/analysis/timeline/timelineCred.js
Expand Up @@ -21,10 +21,12 @@ import {
paramsToJSON,
paramsFromJSON,
type TimelineCredParametersJSON,
type PartialTimelineCredParameters,
partialParams,
defaultParams,
} from "./params";

export type {Interval} from "./interval";
export type {TimelineCredParameters} from "./params";

/**
* A Graph Node wrapped with cred information.
Expand Down Expand Up @@ -85,8 +87,14 @@ export class TimelineCred {
*
* This returns a new TimelineCred; it does not modify the existing one.
*/
async reanalyze(newParams: TimelineCredParameters): Promise<TimelineCred> {
return await TimelineCred.compute(this._graph, newParams, this._plugins);
async reanalyze(
newParams: PartialTimelineCredParameters
): Promise<TimelineCred> {
return await TimelineCred.compute({
graph: this._graph,
params: newParams,
plugins: this._plugins,
});
}

/**
Expand Down Expand Up @@ -217,21 +225,23 @@ export class TimelineCred {
return new TimelineCred(graph, intervalsJSON, cred, params, pluginsJSON);
}

static async compute(
static async compute(opts: {|
graph: Graph,
params: TimelineCredParameters,
plugins: $ReadOnlyArray<PluginDeclaration>
): Promise<TimelineCred> {
params?: PartialTimelineCredParameters,
plugins: $ReadOnlyArray<PluginDeclaration>,
|}): Promise<TimelineCred> {
const {graph, params, plugins} = opts;
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,
params.weights,
params.intervalDecay,
params.alpha
fullParams.weights,
fullParams.intervalDecay,
fullParams.alpha
);
const cred = distributionToCred(
distribution,
Expand All @@ -249,7 +259,7 @@ export class TimelineCred {
graph,
intervals,
addressToCred,
params,
fullParams,
plugins
);
return preliminaryCred.reduceSize({
Expand Down
7 changes: 4 additions & 3 deletions src/analysis/timeline/timelineCred.test.js
Expand Up @@ -11,8 +11,8 @@ import {
EdgeAddress,
} from "../../core/graph";
import {TimelineCred} from "./timelineCred";
import {defaultParams} from "./params";
import {type PluginDeclaration} from "../pluginDeclaration";
import {defaultWeights} from "../weights";
import {type NodeType} from "../types";

describe("src/analysis/timeline/timelineCred", () => {
Expand Down Expand Up @@ -84,8 +84,9 @@ describe("src/analysis/timeline/timelineCred", () => {
const scores = intervals.map((_) => i);
addressToCred.set(address, scores);
}
const params = {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()};
return new TimelineCred(graph, intervals, addressToCred, params, [plugin]);
return new TimelineCred(graph, intervals, addressToCred, defaultParams(), [
plugin,
]);
}

it("JSON serialization works", () => {
Expand Down
12 changes: 6 additions & 6 deletions src/api/load.js
Expand Up @@ -6,10 +6,9 @@ import path from "path";
import {TaskReporter} from "../util/taskReporter";
import {Graph} from "../core/graph";
import {loadGraph} from "../plugins/github/loadGraph";
import {
type TimelineCredParameters,
TimelineCred,
} from "../analysis/timeline/timelineCred";
import {TimelineCred} from "../analysis/timeline/timelineCred";
import {defaultParams, partialParams} from "../analysis/timeline/params";
import {type PartialTimelineCredParameters} from "../analysis/timeline/params";

import {type Project} from "../core/project";
import {setupProjectDirectory} from "../core/project_io";
Expand All @@ -19,7 +18,7 @@ import * as NullUtil from "../util/null";

export type LoadOptions = {|
+project: Project,
+params: TimelineCredParameters,
+params: ?PartialTimelineCredParameters,
+plugins: $ReadOnlyArray<PluginDeclaration>,
+sourcecredDirectory: string,
+githubToken: string | null,
Expand All @@ -45,6 +44,7 @@ export async function load(
taskReporter: TaskReporter
): Promise<void> {
const {project, params, plugins, sourcecredDirectory, githubToken} = options;
const fullParams = params == null ? defaultParams() : partialParams(params);
const loadTask = `load-${options.project.id}`;
taskReporter.start(loadTask);
const cacheDirectory = path.join(sourcecredDirectory, "cache");
Expand Down Expand Up @@ -101,7 +101,7 @@ export async function load(
await fs.writeFile(graphFile, JSON.stringify(graph.toJSON()));

taskReporter.start("compute-cred");
const cred = await TimelineCred.compute(graph, params, plugins);
const cred = await TimelineCred.compute({graph, params: fullParams, plugins});
const credJSON = cred.toJSON();
const credFile = path.join(projectDirectory, "cred.json");
await fs.writeFile(credFile, JSON.stringify(credJSON));
Expand Down
18 changes: 9 additions & 9 deletions src/api/load.test.js
Expand Up @@ -19,6 +19,10 @@ import {NodeAddress, Graph} from "../core/graph";
import {node} from "../core/graphTestUtil";
import {TestTaskReporter} from "../util/taskReporter";
import {load, type LoadOptions} from "./load";
import {
type PartialTimelineCredParameters,
partialParams,
} from "../analysis/timeline/params";

type JestMockFn = $Call<typeof jest.fn>;
jest.mock("../plugins/github/loadGraph", () => ({
Expand Down Expand Up @@ -70,7 +74,7 @@ describe("api/load", () => {
// Tweaks the weights so that we can ensure we aren't overriding with default weights
weights.nodeManualWeights.set(NodeAddress.empty, 33);
// Deep freeze will freeze the weights, too
const params = deepFreeze({alpha: 0.05, intervalDecay: 0.5, weights});
const params: PartialTimelineCredParameters = {weights};
const plugins = deepFreeze([]);
const example = () => {
const sourcecredDirectory = tmp.dirSync().name;
Expand Down Expand Up @@ -139,14 +143,10 @@ describe("api/load", () => {
it("calls TimelineCred.compute with the right graph and options", async () => {
const {options, taskReporter} = example();
await load(options, taskReporter);
expect(timelineCredCompute).toHaveBeenCalledWith(
expect.anything(),
params,
plugins
);
expect(timelineCredCompute.mock.calls[0][0].equals(combinedGraph())).toBe(
true
);
const args = timelineCredCompute.mock.calls[0][0];
expect(args.graph.equals(combinedGraph())).toBe(true);
expect(args.params).toEqual(partialParams(params));
expect(args.plugins).toEqual(plugins);
});

it("saves the resultant cred.json to disk", async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/cli/load.js
Expand Up @@ -9,6 +9,7 @@ import {defaultWeights, fromJSON as weightsFromJSON} from "../analysis/weights";
import {load} from "../api/load";
import {specToProject} from "../plugins/github/specToProject";
import fs from "fs-extra";
import {partialParams} from "../analysis/timeline/params";
import {DEFAULT_PLUGINS} from "./defaultPlugins";

function usage(print: (string) => void): void {
Expand Down Expand Up @@ -105,7 +106,7 @@ const loadCommand: Command = async (args, std) => {
const projects = await Promise.all(
projectSpecs.map((s) => specToProject(s, githubToken))
);
const params = {alpha: 0.05, intervalDecay: 0.5, weights};
const params = partialParams({weights});
const plugins = DEFAULT_PLUGINS;
const optionses = projects.map((project) => ({
project,
Expand Down
7 changes: 4 additions & 3 deletions src/cli/load.test.js
Expand Up @@ -11,6 +11,7 @@ import type {LoadOptions} from "../api/load";
import {defaultWeights, toJSON as weightsToJSON} from "../analysis/weights";
import * as Common from "./common";
import {DEFAULT_PLUGINS} from "./defaultPlugins";
import {defaultParams, partialParams} from "../analysis/timeline/params";

import {makeRepoId, stringToRepoId} from "../core/repoId";

Expand Down Expand Up @@ -78,7 +79,7 @@ describe("cli/load", () => {
discourseServer: null,
},
plugins: DEFAULT_PLUGINS,
params: {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()},
params: defaultParams(),
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken,
discourseKey: fakeDiscourseKey,
Expand All @@ -102,7 +103,7 @@ describe("cli/load", () => {
repoIds: [stringToRepoId(projectId)],
discourseServer: null,
},
params: {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()},
params: defaultParams(),
plugins: DEFAULT_PLUGINS,
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken,
Expand Down Expand Up @@ -140,7 +141,7 @@ describe("cli/load", () => {
repoIds: [makeRepoId("foo", "bar")],
discourseServer: null,
},
params: {alpha: 0.05, intervalDecay: 0.5, weights},
params: partialParams({weights}),
plugins: DEFAULT_PLUGINS,
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken,
Expand Down
4 changes: 2 additions & 2 deletions src/explorer/TimelineCredViewInspectionTest.js
Expand Up @@ -6,7 +6,7 @@ import type {Assets} from "../webutil/assets";
import {TimelineCredView} from "./TimelineCredView";
import {Graph, NodeAddress} from "../core/graph";
import {type Interval, TimelineCred} from "../analysis/timeline/timelineCred";
import {defaultWeights} from "../analysis/weights";
import {defaultParams} from "../analysis/timeline/params";

export default class TimelineCredViewInspectiontest extends React.Component<{|
+assets: Assets,
Expand Down Expand Up @@ -46,7 +46,7 @@ export default class TimelineCredViewInspectiontest extends React.Component<{|
const scores = intervals.map((_unuesd, i) => generator(i));
addressToCred.set(address, scores);
}
const params = {alpha: 0.05, intervalDecay: 0.5, weights: defaultWeights()};
const params = defaultParams();
return new TimelineCred(graph, intervals, addressToCred, params, []);
}

Expand Down
6 changes: 2 additions & 4 deletions src/explorer/TimelineExplorer.js
Expand Up @@ -4,10 +4,8 @@ import React from "react";
import deepEqual from "lodash.isequal";
import {type Weights, copy as weightsCopy} from "../analysis/weights";
import {type NodeAddressT} from "../core/graph";
import {
TimelineCred,
type TimelineCredParameters,
} from "../analysis/timeline/timelineCred";
import {TimelineCred} from "../analysis/timeline/timelineCred";
import {type TimelineCredParameters} from "../analysis/timeline/params";
import {TimelineCredView} from "./TimelineCredView";
import Link from "../webutil/Link";
import {WeightConfig} from "./weights/WeightConfig";
Expand Down

0 comments on commit 0a0010f

Please sign in to comment.