Add `identity/contractIdentities` (#1601)
This commit adds a `contractIdentities` method to the Identity plugin, which allows contracting a WeightedGraph using the provided identities. The method does not attempt to contract weights together, although as a safety check it will error if weights have been explicitly provided for any of the contracted nodes. This PR replaces #1591; see that pull for some context on why this method is defined on the identity plugin rather than as part of the WeightedGraph module. Test plan: For ease of testing, `contractIdentities` is a thin wrapper around `nodeContractions` (which is already tested) and a new private `_contractWeightedGraph` method (for which tests have been added). Since `contractIdentities` is a trivial oneline composition, it does not need any additional explicit testing. `yarn test` passes.
This commit is contained in:
parent
50a6a5f6a7
commit
02b072699e
|
@ -0,0 +1,67 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import * as Weights from "../../core/weights";
|
||||||
|
import {type WeightedGraph as WeightedGraphT} from "../../core/weightedGraph";
|
||||||
|
import {type NodeContraction, NodeAddress} from "../../core/graph";
|
||||||
|
import {nodeContractions} from "./nodeContractions";
|
||||||
|
import {type Identity} from "./identity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies nodeContractions to a WeightedGraph.
|
||||||
|
*
|
||||||
|
* This functionality is defined as part of the identity plugin rather than as
|
||||||
|
* a feature of WeightedGraph because it doesn't attempt to contract weights
|
||||||
|
* (it's not clear how to do this in a general principled way). Within the
|
||||||
|
* identity plugin, we do not expect user identities to have weights
|
||||||
|
* associated, so we can contract the graph without attempting to contract the
|
||||||
|
* weights.
|
||||||
|
*
|
||||||
|
* As a safety measure, this method will error if any of the node addresses
|
||||||
|
* being contracted has an explicitly set weight. It will not error if there
|
||||||
|
* are matching type weights, so that it is still possible (e.g.) to apply
|
||||||
|
* a weighting to an entire plugin.
|
||||||
|
*
|
||||||
|
* For more context on this decision, see discussion in #1591.
|
||||||
|
*/
|
||||||
|
export function _contractWeightedGraph(
|
||||||
|
wg: WeightedGraphT,
|
||||||
|
contractions: $ReadOnlyArray<NodeContraction>
|
||||||
|
): WeightedGraphT {
|
||||||
|
const {graph, weights} = wg;
|
||||||
|
for (const {old} of contractions) {
|
||||||
|
for (const address of old) {
|
||||||
|
const weight = weights.nodeWeights.get(address) || 0;
|
||||||
|
if (weight !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Explicit weight ${weight} on contracted node ${NodeAddress.toString(
|
||||||
|
address
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
graph: graph.contractNodes(contractions),
|
||||||
|
weights: Weights.copy(weights),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a WeightedGraph and identity information, produce a contracted
|
||||||
|
* WeightedGraph where all of an identitiy's aliases have been contracted into
|
||||||
|
* a unified identity.
|
||||||
|
*
|
||||||
|
* An error will be thrown if any of the aliases had an explicitly set weight,
|
||||||
|
* since we don't currently support weight contraction.
|
||||||
|
*
|
||||||
|
* Note: This function has no unit tests, because it is a trivial composition
|
||||||
|
* of two tested functions. Thus, flow typechecking in sufficient. If adding
|
||||||
|
* any complexity to this function, please also add tests.
|
||||||
|
*/
|
||||||
|
export function contractIdentities(
|
||||||
|
wg: WeightedGraphT,
|
||||||
|
identities: $ReadOnlyArray<Identity>,
|
||||||
|
discourseUrl: string | null
|
||||||
|
): WeightedGraphT {
|
||||||
|
return _contractWeightedGraph(wg, nodeContractions(identities, discourseUrl));
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {node} from "../../core/graphTestUtil";
|
||||||
|
import {Graph, NodeAddress} from "../../core/graph";
|
||||||
|
import * as Weights from "../../core/weights";
|
||||||
|
import {_contractWeightedGraph} from "./contractIdentities";
|
||||||
|
|
||||||
|
describe("plugins/identity/contractIdentities", () => {
|
||||||
|
const a = node("a");
|
||||||
|
const b = node("b");
|
||||||
|
const c = node("c");
|
||||||
|
const graph = () => new Graph().addNode(a).addNode(b);
|
||||||
|
const contractions = () => [{old: [a.address, b.address], replacement: c}];
|
||||||
|
const weights = () => {
|
||||||
|
const w = Weights.empty();
|
||||||
|
w.nodeWeights.set(NodeAddress.empty, 3);
|
||||||
|
return w;
|
||||||
|
};
|
||||||
|
describe("_contractWeightedGraph", () => {
|
||||||
|
it("contracts the graph", () => {
|
||||||
|
const wg = {graph: graph(), weights: weights()};
|
||||||
|
const contracted = _contractWeightedGraph(wg, contractions());
|
||||||
|
const expected = graph().contractNodes(contractions());
|
||||||
|
expect(expected.equals(contracted.graph)).toBe(true);
|
||||||
|
});
|
||||||
|
it("returns a copy of the weights", () => {
|
||||||
|
const ws = weights();
|
||||||
|
const wg = {graph: graph(), weights: ws};
|
||||||
|
const contracted = _contractWeightedGraph(wg, contractions());
|
||||||
|
expect(contracted.weights).not.toBe(ws);
|
||||||
|
expect(ws).toEqual(contracted.weights);
|
||||||
|
// check they can be modified independently
|
||||||
|
ws.nodeWeights.set(a.address, 5);
|
||||||
|
expect(ws).not.toEqual(contracted.weights);
|
||||||
|
});
|
||||||
|
it("throws an error if a contracted node has an explicit weight", () => {
|
||||||
|
const ws = weights();
|
||||||
|
ws.nodeWeights.set(a.address, 5);
|
||||||
|
const wg = {graph: graph(), weights: ws};
|
||||||
|
expect(() => _contractWeightedGraph(wg, contractions())).toThrow(
|
||||||
|
"Explicit weight 5 on contracted node"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue