Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simulation.randomSource. #175

Merged
merged 7 commits into from
Aug 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ simulation.force("charge", null);

Returns the node closest to the position ⟨*x*,*y*⟩ with the given search *radius*. If *radius* is not specified, it defaults to infinity. If there is no node within the search area, returns undefined.

<a name="simulation_randomSource" href="#simulation_randomSource">#</a> <i>simulation</i>.<b>randomSource</b>([<i>source</i>]) [<>](https://github.com/d3/d3-force/blob/master/src/simulation.js "Source"))

If *source* is specified, sets the function used to generate random numbers; this should be a function that returns a number between 0 (inclusive) and 1 (exclusive). If *source* is not specified, returns this simulation’s current random source which defaults to a fixed-seed [linear congruential generator](https://en.wikipedia.org/wiki/Linear_congruential_generator). See also [*random*.source](https://github.com/d3/d3-random/blob/master/README.md#random_source).

<a name="simulation_on" href="#simulation_on">#</a> <i>simulation</i>.<b>on</b>(<i>typenames</i>, [<i>listener</i>]) [<>](https://github.com/d3/d3-force/blob/master/src/simulation.js#L145 "Source")

If *listener* is specified, sets the event *listener* for the specified *typenames* and returns this simulation. If an event listener was already registered for the same type and name, the existing listener is removed before the new listener is added. If *listener* is null, removes the current event listeners for the specified *typenames*, if any. If *listener* is not specified, returns the first currently-assigned listener matching the specified *typenames*, if any. When a specified event is dispatched, each *listener* will be invoked with the `this` context as the simulation.
Expand Down Expand Up @@ -171,9 +175,9 @@ Forces may optionally implement [*force*.initialize](#force_initialize) to recei

Applies this force, optionally observing the specified *alpha*. Typically, the force is applied to the array of nodes previously passed to [*force*.initialize](#force_initialize), however, some forces may apply to a subset of nodes, or behave differently. For example, [d3.forceLink](#links) applies to the source and target of each link.

<a name="force_initialize" href="#force_initialize">#</a> <i>force</i>.<b>initialize</b>(<i>nodes</i>) [<>](https://github.com/d3/d3-force/blob/master/src/simulation.js#L77 "Source")
<a name="force_initialize" href="#force_initialize">#</a> <i>force</i>.<b>initialize</b>(<i>nodes</i>, <i>random</i>) [<>](https://github.com/d3/d3-force/blob/master/src/simulation.js#L77 "Source")

Assigns the array of *nodes* to this force. This method is called when a force is bound to a simulation via [*simulation*.force](#simulation_force) and when the simulation’s nodes change via [*simulation*.nodes](#simulation_nodes). A force may perform necessary work during initialization, such as evaluating per-node parameters, to avoid repeatedly performing work during each application of the force.
Supplies the array of *nodes* and *random* source to this force. This method is called when a force is bound to a simulation via [*simulation*.force](#simulation_force) and when the simulation’s nodes change via [*simulation*.nodes](#simulation_nodes). A force may perform necessary work during initialization, such as evaluating per-node parameters, to avoid repeatedly performing work during each application of the force.

#### Centering

Expand Down
10 changes: 6 additions & 4 deletions src/collide.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function y(d) {
export default function(radius) {
var nodes,
radii,
random,
strength = 1,
iterations = 1;

Expand Down Expand Up @@ -46,8 +47,8 @@ export default function(radius) {
y = yi - data.y - data.vy,
l = x * x + y * y;
if (l < r * r) {
if (x === 0) x = jiggle(), l += x * x;
if (y === 0) y = jiggle(), l += y * y;
if (x === 0) x = jiggle(random), l += x * x;
if (y === 0) y = jiggle(random), l += y * y;
l = (r - (l = Math.sqrt(l))) / l * strength;
node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj));
node.vy += (y *= l) * r;
Expand Down Expand Up @@ -77,8 +78,9 @@ export default function(radius) {
for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes);
}

force.initialize = function(_) {
nodes = _;
force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};

Expand Down
4 changes: 2 additions & 2 deletions src/jiggle.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default function() {
return (Math.random() - 0.5) * 1e-6;
export default function(random) {
return (random() - 0.5) * 1e-6;
}
9 changes: 9 additions & 0 deletions src/lcg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use
const a = 1664525;
const c = 1013904223;
const m = 4294967296; // 2^32

export default function() {
let s = 1;
return () => (s = (a * s + c) % m) / m;
}
10 changes: 6 additions & 4 deletions src/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default function(links) {
nodes,
count,
bias,
random,
iterations = 1;

if (links == null) links = [];
Expand All @@ -32,8 +33,8 @@ export default function(links) {
for (var k = 0, n = links.length; k < iterations; ++k) {
for (var i = 0, link, source, target, x, y, l, b; i < n; ++i) {
link = links[i], source = link.source, target = link.target;
x = target.x + target.vx - source.x - source.vx || jiggle();
y = target.y + target.vy - source.y - source.vy || jiggle();
x = target.x + target.vx - source.x - source.vx || jiggle(random);
y = target.y + target.vy - source.y - source.vy || jiggle(random);
l = Math.sqrt(x * x + y * y);
l = (l - distances[i]) / l * alpha * strengths[i];
x *= l, y *= l;
Expand Down Expand Up @@ -86,8 +87,9 @@ export default function(links) {
}
}

force.initialize = function(_) {
nodes = _;
force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};

Expand Down
14 changes: 8 additions & 6 deletions src/manyBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {x, y} from "./simulation.js";
export default function() {
var nodes,
node,
random,
alpha,
strength = constant(-30),
strengths,
Expand Down Expand Up @@ -63,8 +64,8 @@ export default function() {
// Limit forces for very close nodes; randomize direction if coincident.
if (w * w / theta2 < l) {
if (l < distanceMax2) {
if (x === 0) x = jiggle(), l += x * x;
if (y === 0) y = jiggle(), l += y * y;
if (x === 0) x = jiggle(random), l += x * x;
if (y === 0) y = jiggle(random), l += y * y;
if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
node.vx += x * quad.value * alpha / l;
node.vy += y * quad.value * alpha / l;
Expand All @@ -77,8 +78,8 @@ export default function() {

// Limit forces for very close nodes; randomize direction if coincident.
if (quad.data !== node || quad.next) {
if (x === 0) x = jiggle(), l += x * x;
if (y === 0) y = jiggle(), l += y * y;
if (x === 0) x = jiggle(random), l += x * x;
if (y === 0) y = jiggle(random), l += y * y;
if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
}

Expand All @@ -89,8 +90,9 @@ export default function() {
} while (quad = quad.next);
}

force.initialize = function(_) {
nodes = _;
force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};

Expand Down
10 changes: 8 additions & 2 deletions src/simulation.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {dispatch} from "d3-dispatch";
import {timer} from "d3-timer";
import lcg from "./lcg.js";

export function x(d) {
return d.x;
Expand All @@ -21,7 +22,8 @@ export default function(nodes) {
velocityDecay = 0.6,
forces = new Map(),
stepper = timer(step),
event = dispatch("tick", "end");
event = dispatch("tick", "end"),
random = lcg();

if (nodes == null) nodes = [];

Expand Down Expand Up @@ -75,7 +77,7 @@ export default function(nodes) {
}

function initializeForce(force) {
if (force.initialize) force.initialize(nodes);
if (force.initialize) force.initialize(nodes, random);
return force;
}

Expand Down Expand Up @@ -116,6 +118,10 @@ export default function(nodes) {
return arguments.length ? (velocityDecay = 1 - _, simulation) : 1 - velocityDecay;
},

randomSource: function(_) {
return arguments.length ? (random = _, forces.forEach(initializeForce), simulation) : random;
},

force: function(name, _) {
return arguments.length > 1 ? ((_ == null ? forces.delete(name) : forces.set(name, initializeForce(_))), simulation) : forces.get(name);
},
Expand Down
9 changes: 9 additions & 0 deletions test/collide-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,12 @@ tape("forceCollide jiggles equal positions", function(test) {
test.equal(a.vy, -b.vy);
test.end();
});

tape("forceCollide jiggles in a reproducible way", function(test) {
const nodes = Array.from({length:10}, () => ({x:0,y:0}));
force.forceSimulation()
.nodes(nodes)
.force("collide", force.forceCollide()).stop().tick(50);
test.nodeEqual(nodes[0], {x: -5.371433857229194, y: -2.6644608278592576, index: 0, vy: 0, vx: 0});
test.end();
});
2 changes: 1 addition & 1 deletion test/simulation-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ require("./nodeEqual.js");

tape("forceSimulation() returns a simulation", function(test) {
const f = force.forceSimulation().stop();
test.deepEqual(Object.keys(f).sort(), [ 'alpha', 'alphaDecay', 'alphaMin', 'alphaTarget', 'find', 'force', 'nodes', 'on', 'restart', 'stop', 'tick', 'velocityDecay' ]);
test.deepEqual(Object.keys(f).sort(), [ 'alpha', 'alphaDecay', 'alphaMin', 'alphaTarget', 'find', 'force', 'nodes', 'on', 'randomSource', 'restart', 'stop', 'tick', 'velocityDecay' ]);
test.end();
});

Expand Down