Skip to content
Snippets Groups Projects
Commit 66c2cfae authored by Brady James Garvin's avatar Brady James Garvin
Browse files

Added an overlay that shows the transit systems' forecasted ridership based on...

Added an overlay that shows the transit systems' forecasted ridership based on the passengers' current plans.
parent 36043345
No related branches found
No related tags found
No related merge requests found
...@@ -27,6 +27,16 @@ body { ...@@ -27,6 +27,16 @@ body {
text-align: center; text-align: center;
} }
.visualization .overlay {
position: absolute;
left: 0;
top: 0;
z-index: 1;
padding: 0.5em;
font-size: 18pt;
background: rgba(0, 255, 255, 0.25);
}
.visualization svg { .visualization svg {
position: absolute; position: absolute;
width: 100%; width: 100%;
......
import './utility.js';
// Besides the usual identity (identitySummary) and binary operator (combineSummaries) that every monoid has, here we also include a function (summarizeValue)
// to convert values in a leafy tree's leaves into summaries. That way, leafy trees can have one type for their elements and a different type for their
// summaries. (And when different types are not required, summarizeValue can just be set to the identity function.)
export class Monoid {
constructor(identitySummary, summarizeValue, combineSummaries) {
this.identitySummary = identitySummary;
this.summarizeValue = summarizeValue;
this.combineSummaries = combineSummaries;
}
}
// Likewise, we not only give an ordered monoid a way to compare its summaries (combineSummaries), we also include a function (summarizePosition) to convert
// from a position type to the summary type. That way, leafy trees can have one type for positions and a different type for summaries. (Yet again, if
// different types are not required, then summarizePosition can just be set to the identity function.)
//
// Note that, by convention, compareSummaries should be defined so that compareSummaries(x, y) is true iff x ≼ y.
export class OrderedMonoid extends Monoid {
constructor(identitySummary, summarizeValue, combineSummaries, summarizePosition, compareSummaries) {
super(identitySummary, summarizeValue, combineSummaries);
this.summarizePosition = summarizePosition;
this.compareSummaries = compareSummaries;
}
}
// An AnnotatedMonoid is a product monoid that is ordered only by its left coordinate. (So its positions are exactly the positions of the left monoid.) An
// AnnotatedMonoid is normally used to annotate the summaries in an ordered monoid with additional summary information from an unordered monoid on the right.
export class AnnotatedMonoid extends OrderedMonoid {
constructor(orderedMonoid, unorderedMonoid) {
super(
[orderedMonoid.identitySummary, unorderedMonoid.identitySummary],
(value) => [orderedMonoid.summarizeValue(value), unorderedMonoid.summarizeValue(value)],
([left, leftAnnotation], [right, rightAnnotation]) =>
[orderedMonoid.combineSummaries(left, right), unorderedMonoid.combineSummaries(leftAnnotation, rightAnnotation)],
(position) => [orderedMonoid.summarizePosition(position), undefined],
([left, leftAnnotation], [right, rightAnnotation]) => orderedMonoid.compareSummaries(left, right) // eslint-disable-line no-unused-vars
);
}
}
// A KeyValueMonoid is a Monoid for leafy trees that store key/value pairs sorted by key. (Hence, the keys must belong to an ordered monoid, and the values
// usually belong to a different, not necessarily ordered monoid.) When specifying positions in a leafy tree using a KeyValueMonoid, it is only necessary to
// give a position for the key monoid.
export class KeyValueMonoid extends AnnotatedMonoid {
constructor(keyMonoid, valueMonoid) {
super(
new OrderedMonoid(
keyMonoid.identitySummary,
([key, value]) => keyMonoid.summarizeValue(key), // eslint-disable-line no-unused-vars
keyMonoid.combineSummaries,
keyMonoid.summarizePosition,
keyMonoid.compareSummaries
),
new Monoid(
valueMonoid.identitySummary,
([key, value]) => valueMonoid.summarizeValue(value), // eslint-disable-line no-unused-vars
valueMonoid.combineSummaries
)
);
}
}
// COUNT_MONOID is a monoid meant to be used as the ordered part of an annotated monoid for leafy trees that act like lists. It's positions can be used to
// index the tree's leaves, with indices counting from zero from left to right.
export const COUNT_MONOID = new OrderedMonoid(
0,
(value) => 1, // eslint-disable-line no-unused-vars
(left, right) => left + right,
(index) => index + 1,
(left, right) => left <= right
);
// RANGE_MONOID is a monoid meant to be used as the ordered part of an annotated monoid for leafy trees that act like ordered sets or as the key part of a
// key/value monoid for leafy trees that act like ordered dictionaries. It's positions can be used to index the set or dictionary much like a dictionary key.
export const RANGE_MONOID = new OrderedMonoid(
[Infinity, -Infinity],
(value) => [value, value], // eslint-disable-line no-unused-vars
([leftMinimum, leftMaximum], [rightMinimum, rightMaximum]) => [Math.min(leftMinimum, rightMinimum), Math.max(leftMaximum, rightMaximum)],
(position) => [position, position],
([leftMinimum, leftMaximum], [rightMinimum, rightMaximum]) => leftMaximum <= rightMaximum // eslint-disable-line no-unused-vars
);
class LeafyTreeNode {
constructor(tree, parent) {
this.tree = tree;
this.parent = parent;
}
summarize(summaryOfBeginPosition, summaryOfEndPosition, summaryOfLeftSide) {
const orderedMonoid = this.tree.orderedMonoid;
const minimum = orderedMonoid.combineSummaries(summaryOfLeftSide, this.summaryOfLeftmostLeaf);
const maximum = orderedMonoid.combineSummaries(summaryOfLeftSide, this.summary);
// containment case:
if (orderedMonoid.compareSummaries(summaryOfBeginPosition, minimum) &&
!orderedMonoid.compareSummaries(summaryOfEndPosition, maximum)) {
return this.summary;
}
// disjointness case:
if (!orderedMonoid.compareSummaries(summaryOfBeginPosition, maximum) ||
orderedMonoid.compareSummaries(summaryOfEndPosition, minimum)) {
return orderedMonoid.identitySummary;
}
// partial overlap case:
console.assert(this.children !== undefined, 'Tried to summarize part of a leaf in a leafy tree.');
let result = orderedMonoid.identitySummary;
let accumulator = summaryOfLeftSide;
for (const child of this.children) {
result = orderedMonoid.combineSummaries(result, child.summarize(summaryOfBeginPosition, summaryOfEndPosition, accumulator));
accumulator = orderedMonoid.combineSummaries(accumulator, child.summary);
}
return result;
}
}
class Branch extends LeafyTreeNode {
constructor(tree, parent, children) {
super(tree, parent);
console.assert(children.every((child) => child.height === 0), 'Tried to create a new branch above height one in a leafy tree.');
if (parent !== undefined) {
for (let index = parent.children.length; index--;) {
if (children.includes(parent.children[index])) {
parent.children[index] = this;
}
}
console.assert(
parent.children.filter((child) => child === this).length === 1,
'Tried to create a leafy tree branch that does not replace exactly one node.'
);
} else {
console.assert(
children.includes(this.tree._root), // eslint-disable-line no-underscore-dangle, (read by friend)
'Tried to add a second root to a leafy tree.'
);
this.tree._root = this; // eslint-disable-line no-underscore-dangle, (write by friend)
}
this.children = children;
for (const child of children) {
child.parent = this;
}
this._updateSummaries();
}
_updateSummaries() {
console.assert(this.children.length > 0, 'Found a branch-type vertex in a leafy tree with no children.');
if (this.children.length === 1) {
if (this.parent !== undefined) {
const ownIndex = this.parent.children.indexOf(this);
console.assert(ownIndex >= 0, 'Found a branch in a leafy tree that is not a child of its parent.');
this.parent.children[ownIndex] = this.children[0];
this.children[0].parent = this.parent;
} else {
this.tree._root = this.children[0]; // eslint-disable-line no-underscore-dangle, (write by friend)
this.children[0].parent = undefined;
}
} else {
this.height = 1 + Math.max(...this.children.map((child) => child.height));
this.summaryOfLeftmostLeaf = this.children[0].summaryOfLeftmostLeaf;
this.summary = this.children.reduce((left, right) => this.tree.orderedMonoid.combineSummaries(left.summary, right.summary));
}
if (this.parent !== undefined) {
this.parent._updateSummaries(); // eslint-disable-line no-underscore-dangle, (call from friend)
}
}
findLeafAndSummaryOfLeftSide(positionSummary, summaryOfLeftSide) {
const orderedMonoid = this.tree.orderedMonoid;
let accumulator = summaryOfLeftSide;
for (const child of this.children.slice(0, -1)) {
const candidate = orderedMonoid.combineSummaries(accumulator, child.summary);
if (orderedMonoid.compareSummaries(positionSummary, candidate)) {
return child.findLeafAndSummaryOfLeftSide(positionSummary, accumulator);
}
accumulator = candidate;
}
return this.children.top().findLeafAndSummaryOfLeftSide(positionSummary, accumulator);
}
dump(indentation) {
const newIndentation = indentation + 2;
const children = this.children.map((child) => `${child.dump(newIndentation)}\n`).join('');
return `${' '.repeat(indentation)}Branch (height=${this.height}) ` +
`{\n${' '.repeat(newIndentation)}summary: ${this.summary}\n${children}${' '.repeat(indentation)}}`;
}
}
class Leaf extends LeafyTreeNode {
constructor(tree, value) {
super(tree, undefined);
this.value = value;
this.summary = tree.orderedMonoid.summarizeValue(value);
}
get _successor() {
let [child, current] = [this, this.parent];
while (current !== undefined) {
const childIndex = current.children.indexOf(child);
if (childIndex + 1 < current.children.length) {
for (current = current.children[childIndex + 1]; // eslint-disable-line curly
current.children !== undefined;
current = current.children[0]);
return current;
}
[child, current] = [current, current.parent];
}
return undefined;
}
get height() { // eslint-disable-line class-methods-use-this
return 0;
}
get summaryOfLeftmostLeaf() {
return this.summary;
}
findLeafAndSummaryOfLeftSide(positionSummary, summaryOfLeftSide) {
return [this, summaryOfLeftSide];
}
add(positionSummary, value, summaryOfLeftSide) {
const orderedMonoid = this.tree.orderedMonoid;
const sibling = new Leaf(this.tree, value);
// For now, only use binary branching:
if (orderedMonoid.compareSummaries(positionSummary, orderedMonoid.combineSummaries(summaryOfLeftSide, this.summary))) {
new Branch(this.tree, this.parent, [sibling, this]); // eslint-disable-line no-new
} else {
new Branch(this.tree, this.parent, [this, sibling]); // eslint-disable-line no-new
}
return sibling;
}
_matchesPosition(positionSummary, summaryOfLeftSide) {
const orderedMonoid = this.tree.orderedMonoid;
const summaryOfLeftSideAndSelf = orderedMonoid.combineSummaries(summaryOfLeftSide, this.summary);
return orderedMonoid.compareSummaries(positionSummary, summaryOfLeftSideAndSelf) &&
orderedMonoid.compareSummaries(summaryOfLeftSideAndSelf, positionSummary);
}
_delete() {
if (this.parent !== undefined) {
this.parent.children.delete(this);
this.parent._updateSummaries(); // eslint-disable-line no-underscore-dangle, (call from friend)
} else {
this.tree._root = undefined; // eslint-disable-line no-underscore-dangle, (write by friend)
}
}
delete(positionSummary, summaryOfLeftSide) {
if (this._matchesPosition(positionSummary, summaryOfLeftSide)) {
this._delete();
return true;
}
return false;
}
deleteMatch(positionSummary, criterion, summaryOfLeftSide) {
if (this._matchesPosition(positionSummary, summaryOfLeftSide)) {
const orderedMonoid = this.tree.orderedMonoid;
if (criterion(this.value)) {
this._delete();
return true;
}
const successor = this._successor;
if (successor !== undefined) {
return successor.deleteMatch(positionSummary, criterion, orderedMonoid.combineSummaries(summaryOfLeftSide, this.summary));
}
}
return false;
}
dump(indentation) {
return `${' '.repeat(indentation)}Leaf { value: ${this.value}, summary: ${this.summary} }`;
}
}
export class LeafyTree {
constructor(orderedMonoid) {
this.orderedMonoid = orderedMonoid;
this._root = undefined;
}
summarize(begin, end) {
if (this._root !== undefined) {
return this._root.summarize(this.orderedMonoid.summarizePosition(begin), this.orderedMonoid.summarizePosition(end), this.orderedMonoid.identitySummary);
}
return this.orderedMonoid.identitySummary;
}
add(position, value) {
if (this._root !== undefined) {
const positionSummary = this.orderedMonoid.summarizePosition(position);
const [leaf, accumulation] = this._root.findLeafAndSummaryOfLeftSide(positionSummary, this.orderedMonoid.identitySummary);
return leaf.add(positionSummary, value, accumulation);
}
this._root = new Leaf(this, value);
return this._root;
}
// syntactic sugar for dictionary-like leafy trees where we want to store key/value pairs, not just values
set(position, value) {
this.add(position, [position, value]);
}
delete(position) {
if (this._root !== undefined) {
const positionSummary = this.orderedMonoid.summarizePosition(position);
const [leaf, accumulation] = this._root.findLeafAndSummaryOfLeftSide(positionSummary, this.orderedMonoid.identitySummary);
return leaf.delete(positionSummary, accumulation);
}
return false;
}
deleteMatch(position, criterion) {
if (this._root !== undefined) {
const positionSummary = this.orderedMonoid.summarizePosition(position);
const [leaf, accumulation] = this._root.findLeafAndSummaryOfLeftSide(positionSummary, this.orderedMonoid.identitySummary);
return leaf.deleteMatch(positionSummary, criterion, accumulation);
}
return false;
}
toString() {
if (this._root !== undefined) {
return `LeafyTree {\n${this._root.dump(2)}\n}`;
}
return 'LeafyTree {}';
}
}
import './utility.js'; import './utility.js';
import {Simulation, Decision, Agent} from './simulation.js'; import {Simulation, Decision, Agent} from './simulation.js';
import {shortestUndirectedPath} from './undirected_graph.js'; import {shortestUndirectedPath} from './undirected_graph.js';
import {Monoid, KeyValueMonoid, RANGE_MONOID, LeafyTree} from './leafy_tree.js';
const SERIES_MAX_MONOID = new Monoid(
[-Infinity, 0],
(value) => [value, value],
([leftMaximum, leftSum], [rightMaximum, rightSum]) => [
Math.max(leftMaximum, leftSum + rightMaximum),
leftSum + rightSum,
]
);
const KEYED_SERIES_MAX_MONOID = new KeyValueMonoid(RANGE_MONOID, SERIES_MAX_MONOID);
export class Vertex { export class Vertex {
constructor(name) { constructor(name) {
...@@ -38,6 +49,25 @@ export class City extends Simulation { ...@@ -38,6 +49,25 @@ export class City extends Simulation {
this._passengers = []; this._passengers = [];
this._inBulkEdit = false; this._inBulkEdit = false;
this._needsRestartAfterBulkEdit = false; this._needsRestartAfterBulkEdit = false;
this._forecast = new LeafyTree(KEYED_SERIES_MAX_MONOID);
}
get passengerCount() {
return this._passengers.length;
}
get forecastedRidership() {
const [[_, __], forecast] = this._forecast.summarize(-Infinity, Infinity);
const [maximum, ___] = SERIES_MAX_MONOID.combineSummaries(SERIES_MAX_MONOID.summarizeValue(0), forecast);
return maximum;
}
addForecastedRidershipChange(time, change) {
this._forecast.set(time, change);
}
deleteForecastedRidershipChange(time, change) {
this._forecast.deleteMatch(time, ([key, value]) => value === change); // eslint-disable-line no-unused-vars
} }
chooseRandomWalkVertex() { chooseRandomWalkVertex() {
...@@ -604,6 +634,7 @@ export class Passenger extends Agent { ...@@ -604,6 +634,7 @@ export class Passenger extends Agent {
this._arrivalTime = undefined; this._arrivalTime = undefined;
this.inactiveTime = inactiveTime; this.inactiveTime = inactiveTime;
this.plan = [new PlanningVertex(undefined, this.vertex, this.inactiveTime)]; this.plan = [new PlanningVertex(undefined, this.vertex, this.inactiveTime)];
this.ridershipForecast = [];
this.vertex = vertex; this.vertex = vertex;
} }
...@@ -746,9 +777,38 @@ export class Passenger extends Agent { ...@@ -746,9 +777,38 @@ export class Passenger extends Agent {
this.vertex = this.immediateDestination; this.vertex = this.immediateDestination;
} }
_deleteOldPlanFromForecast() {
for (const [time, change] of this.ridershipForecast) {
this.simulation.deleteForecastedRidershipChange(time, change);
}
this.ridershipForecast = [];
}
_addNewPlanToForecast() {
this.ridershipForecast = [];
let eta = 0;
let previousTime = -Infinity;
for (const vertex of this.plan) {
if (vertex.route !== undefined) {
if (previousTime === this.simulation.currentTime + eta) {
this.ridershipForecast.pop();
} else {
this.ridershipForecast.push([this.simulation.currentTime + eta, 1]);
}
previousTime = this.simulation.currentTime + vertex.eta;
this.ridershipForecast.push([previousTime, -1]);
}
eta = vertex.eta;
}
for (const [time, change] of this.ridershipForecast) {
this.simulation.addForecastedRidershipChange(time, change);
}
}
_plan() { _plan() {
this._departureTime = undefined; this._departureTime = undefined;
this._arrivalTime = undefined; this._arrivalTime = undefined;
this._deleteOldPlanFromForecast();
console.assert(this.vertex !== undefined, console.assert(this.vertex !== undefined,
`Attempted to plan a route for the passenger ${this} using _plan while they are still in transit (use _planFromBus instead).`); `Attempted to plan a route for the passenger ${this} using _plan while they are still in transit (use _planFromBus instead).`);
if (this.destination === undefined) { if (this.destination === undefined) {
...@@ -764,6 +824,7 @@ export class Passenger extends Agent { ...@@ -764,6 +824,7 @@ export class Passenger extends Agent {
console.assert(discarded === starter, `The computed plan ${discarded}, ${this.plan} does not begin at the vertex ${this.vertex}.`); console.assert(discarded === starter, `The computed plan ${discarded}, ${this.plan} does not begin at the vertex ${this.vertex}.`);
console.assert(this.plan.top().destination === this.destination, console.assert(this.plan.top().destination === this.destination,
`The computed plan ${discarded}, ${this.plan} does not end at the destination ${this.destination}.`); `The computed plan ${discarded}, ${this.plan} does not end at the destination ${this.destination}.`);
this._addNewPlanToForecast();
return; return;
} }
} }
...@@ -773,6 +834,7 @@ export class Passenger extends Agent { ...@@ -773,6 +834,7 @@ export class Passenger extends Agent {
_planFromBus() { _planFromBus() {
console.assert(this.vertex === undefined, console.assert(this.vertex === undefined,
`Attempted to plan a route for the passenger ${this} using _planFromBus while they are not in transit (use _plan instead).`); `Attempted to plan a route for the passenger ${this} using _planFromBus while they are not in transit (use _plan instead).`);
this._deleteOldPlanFromForecast();
const nextStop = this.bus.vertex || this.bus.arc.next.originalSource.vertex; const nextStop = this.bus.vertex || this.bus.arc.next.originalSource.vertex;
const starter = new PlanningVertex(this.bus.arc.route, nextStop, this.bus.getETA(nextStop), true); const starter = new PlanningVertex(this.bus.arc.route, nextStop, this.bus.getETA(nextStop), true);
this.plan = shortestUndirectedPath(new PlanningGraph(this.simulation), starter, (vertex) => vertex.destination === this.destination, this.plan = shortestUndirectedPath(new PlanningGraph(this.simulation), starter, (vertex) => vertex.destination === this.destination,
...@@ -783,9 +845,11 @@ export class Passenger extends Agent { ...@@ -783,9 +845,11 @@ export class Passenger extends Agent {
console.assert(discarded === starter, `The computed plan ${discarded}, ${this.plan} does not begin at the source ${nextStop}.`); console.assert(discarded === starter, `The computed plan ${discarded}, ${this.plan} does not begin at the source ${nextStop}.`);
} }
console.assert(this.plan.top().destination === this.destination, `The computed plan ${this.plan} does not end at the destination ${this.destination}.`); console.assert(this.plan.top().destination === this.destination, `The computed plan ${this.plan} does not end at the destination ${this.destination}.`);
this._addNewPlanToForecast();
return; return;
} }
this.plan = [starter]; this.plan = [starter];
this._addNewPlanToForecast();
} }
_decide() { _decide() {
......
...@@ -32,3 +32,7 @@ Array.prototype.delete = function(element) { // eslint-disable-line func-names, ...@@ -32,3 +32,7 @@ Array.prototype.delete = function(element) { // eslint-disable-line func-names,
this.remove(this.indexOf(element)); this.remove(this.indexOf(element));
return this; return this;
}; };
Array.prototype.sum = function sum() {
return this.reduce((left, right) => left + right, 0);
};
...@@ -309,6 +309,8 @@ $.widget('transit.visualization', { ...@@ -309,6 +309,8 @@ $.widget('transit.visualization', {
this._trigger('ready'); this._trigger('ready');
}, },
}); });
this.overlay = $('<div class="overlay"></div>');
this.element.append(this.overlay);
}, },
_populate() { _populate() {
...@@ -397,6 +399,10 @@ $.widget('transit.visualization', { ...@@ -397,6 +399,10 @@ $.widget('transit.visualization', {
for (const dot of this._passengerDots.values()) { for (const dot of this._passengerDots.values()) {
dot.refresh(); dot.refresh();
} }
const ridership = this._city.forecastedRidership;
const passengerCount = this._city.passengerCount;
const ridershipPercent = 100 * ridership / passengerCount;
this.overlay.text(`Ridership Forecast: ${ridership}/${passengerCount} passenger(s) (${ridershipPercent.toFixed(0)}%)`);
}, },
getSlowness() { getSlowness() {
......
import {identity} from '../js/utility.js';
import {Monoid, AnnotatedMonoid, KeyValueMonoid, COUNT_MONOID, RANGE_MONOID, LeafyTree} from '../js/leafy_tree.js';
/* globals QUnit */
QUnit.module('test_leafy_tree.js');
/* eslint-disable no-magic-numbers, no-underscore-dangle */
// Purposely use a noncommutative monoid to better catch ordering bugs.
const HOLD = 'HOLD'; // do nothing to the lightswitch
const OFF = 'OFF'; // turn the lightswitch off
const ON = 'ON'; // turn the lightswitch on
const FLIP = 'FLIP'; // toggle the lightswitch's state
const OPPOSITES = new Map([[HOLD, FLIP], [OFF, ON], [ON, OFF], [FLIP, HOLD]]);
const LIGHTSWITCH_MONOID = new Monoid(
HOLD,
identity,
(left, right) => {
switch (right) {
case HOLD:
return left;
case OFF:
case ON:
return right;
case FLIP:
return OPPOSITES.get(left);
default:
console.assert(false, 'Tried to apply the lightswitch monoid to an invalid lightswitch action (check use of `add` vs. `set`).');
return undefined;
}
}
);
const INDEXED_LIGHTSWITCH_MONOID = new AnnotatedMonoid(COUNT_MONOID, LIGHTSWITCH_MONOID);
const KEYED_LIGHTSWITCH_MONOID = new KeyValueMonoid(RANGE_MONOID, LIGHTSWITCH_MONOID);
QUnit.test('summarize an empty tree as the identity', (assert) => {
const tree = new LeafyTree(INDEXED_LIGHTSWITCH_MONOID);
assert.deepEqual(tree.summarize(-Infinity, Infinity), [0, HOLD]);
});
QUnit.test('summarize a one-element tree as that element', (assert) => {
const tree = new LeafyTree(INDEXED_LIGHTSWITCH_MONOID);
tree.add(0, OFF);
assert.deepEqual(tree.summarize(-Infinity, Infinity), [1, OFF]);
});
QUnit.test('summarize subranges of a one-element tree', (assert) => {
const tree = new LeafyTree(INDEXED_LIGHTSWITCH_MONOID);
tree.add(0, OFF);
assert.deepEqual(tree.summarize(-Infinity, 0), [0, HOLD]);
assert.deepEqual(tree.summarize(0, 0), [0, HOLD]);
assert.deepEqual(tree.summarize(0, 1), [1, OFF]);
assert.deepEqual(tree.summarize(1, Infinity), [0, HOLD]);
});
QUnit.test('summarize subranges of a two-element tree', (assert) => {
const tree = new LeafyTree(INDEXED_LIGHTSWITCH_MONOID);
tree.add(0, OFF);
tree.add(1, FLIP);
assert.deepEqual(tree.summarize(-Infinity, 0), [0, HOLD]);
assert.deepEqual(tree.summarize(0, 1), [1, OFF]);
assert.deepEqual(tree.summarize(1, 2), [1, FLIP]);
assert.deepEqual(tree.summarize(0, 2), [2, ON]);
assert.deepEqual(tree.summarize(2, Infinity), [0, HOLD]);
});
QUnit.test('break position-ordering ties by placing new elements to the left', (assert) => {
const tree = new LeafyTree(INDEXED_LIGHTSWITCH_MONOID);
tree.add(0, FLIP);
tree.add(0, OFF);
assert.deepEqual(tree.summarize(-Infinity, Infinity), [2, ON]);
});
QUnit.test('summarize subranges of a two-element tree where the elements are tied in the order', (assert) => {
const tree = new LeafyTree(KEYED_LIGHTSWITCH_MONOID);
tree.set(0, FLIP);
tree.set(0, OFF);
assert.deepEqual(tree.summarize(-Infinity, 0), [[Infinity, -Infinity], HOLD]);
assert.deepEqual(tree.summarize(0, 0), [[Infinity, -Infinity], HOLD]);
assert.deepEqual(tree.summarize(0, 1), [[0, 0], ON]);
assert.deepEqual(tree.summarize(1, Infinity), [[Infinity, -Infinity], HOLD]);
});
QUnit.test('summarize subranges of a many-element tree', (assert) => {
const tree = new LeafyTree(INDEXED_LIGHTSWITCH_MONOID);
tree.add(0, FLIP);
tree.add(1, OFF);
tree.add(2, FLIP);
tree.add(3, FLIP);
tree.add(4, OFF);
tree.add(5, ON);
assert.deepEqual(tree.summarize(-1, 0), [0, HOLD]);
assert.deepEqual(tree.summarize(0, 1), [1, FLIP]);
assert.deepEqual(tree.summarize(0, 2), [2, OFF]);
assert.deepEqual(tree.summarize(2, 4), [2, HOLD]);
assert.deepEqual(tree.summarize(1, 4), [3, OFF]);
assert.deepEqual(tree.summarize(3, 6), [3, ON]);
assert.deepEqual(tree.summarize(0, 6), [6, ON]);
});
QUnit.test('summarize subranges of a many-element tree built out of order', (assert) => {
const tree = new LeafyTree(INDEXED_LIGHTSWITCH_MONOID);
tree.add(0, FLIP); // [FLIP]
tree.add(1, OFF); // [FLIP, OFF]
tree.add(0, OFF); // [OFF, FLIP, OFF]
tree.add(3, ON); // [OFF, FLIP, OFF, ON]
tree.add(0, FLIP); // [FLIP, OFF, FLIP, OFF, ON]
tree.add(2, FLIP); // [FLIP, OFF, FLIP, FLIP, OFF, ON]
assert.deepEqual(tree.summarize(-1, 0), [0, HOLD]);
assert.deepEqual(tree.summarize(0, 1), [1, FLIP]);
assert.deepEqual(tree.summarize(0, 2), [2, OFF]);
assert.deepEqual(tree.summarize(2, 4), [2, HOLD]);
assert.deepEqual(tree.summarize(1, 4), [3, OFF]);
assert.deepEqual(tree.summarize(3, 6), [3, ON]);
assert.deepEqual(tree.summarize(0, 6), [6, ON]);
});
QUnit.test('summarize subranges of a many-element tree built out of order using nonconsecutive keys', (assert) => {
const tree = new LeafyTree(KEYED_LIGHTSWITCH_MONOID);
tree.set(12, FLIP); // [12: FLIP]
tree.set(18, OFF); // [12: FLIP, 18: OFF]
tree.set(2, OFF); // [2: OFF, 12: FLIP, 18: OFF]
tree.set(31, ON); // [2: OFF, 12: FLIP, 18: OFF, 31: ON]
tree.set(-1, FLIP); // [-1: FLIP, 2: OFF, 12: FLIP, 18: OFF, 31: ON]
tree.set(9, FLIP); // [-1: FLIP, 2: OFF, 9: FLIP, 12: FLIP, 18: OFF, 31: ON]
assert.deepEqual(tree.summarize(-10, -1), [[Infinity, -Infinity], HOLD]);
assert.deepEqual(tree.summarize(-1, 1), [[-1, -1], FLIP]);
assert.deepEqual(tree.summarize(-4, 3), [[-1, 2], OFF]);
assert.deepEqual(tree.summarize(8, 18), [[9, 12], HOLD]);
assert.deepEqual(tree.summarize(1, 14), [[2, 12], OFF]);
assert.deepEqual(tree.summarize(11, 32), [[12, 31], ON]);
assert.deepEqual(tree.summarize(-64, 64), [[-1, 31], ON]);
});
QUnit.test('summarize a tree as elements are removed', (assert) => {
const tree = new LeafyTree(INDEXED_LIGHTSWITCH_MONOID);
tree.add(0, FLIP);
tree.add(1, OFF);
tree.add(2, ON);
tree.add(3, FLIP);
tree.add(4, OFF);
tree.add(5, FLIP);
let result = tree.delete(4);
assert.deepEqual(result, true);
assert.deepEqual(tree.summarize(-Infinity, Infinity), [5, ON]);
result = tree.delete(2);
assert.deepEqual(result, true);
assert.deepEqual(tree.summarize(-Infinity, Infinity), [4, OFF]);
result = tree.delete(1);
assert.deepEqual(result, true);
assert.deepEqual(tree.summarize(-Infinity, Infinity), [3, FLIP]);
});
QUnit.test('break position-ordering ties by deleting elements from the left', (assert) => {
const tree = new LeafyTree(KEYED_LIGHTSWITCH_MONOID);
tree.set(0, FLIP);
tree.set(0, OFF);
tree.delete(0);
assert.deepEqual(tree.summarize(-Infinity, Infinity), [[0, 0], FLIP]);
});
QUnit.test('break a position-ordering tie during a deletion by matching an element', (assert) => {
const tree = new LeafyTree(KEYED_LIGHTSWITCH_MONOID);
tree.set(0, FLIP);
tree.set(0, OFF);
tree.deleteMatch(0, ([key, value]) => value === FLIP); // eslint-disable-line no-unused-vars
assert.deepEqual(tree.summarize(-Infinity, Infinity), [[0, 0], OFF]);
});
QUnit.test('break multiple position-ordering ties during a deletion by matching an element', (assert) => {
const tree = new LeafyTree(KEYED_LIGHTSWITCH_MONOID);
tree.set(0, FLIP);
tree.set(0, OFF);
tree.set(0, HOLD);
tree.deleteMatch(0, ([key, value]) => value === FLIP); // eslint-disable-line no-unused-vars
assert.deepEqual(tree.summarize(-Infinity, Infinity), [[0, 0], OFF]);
});
QUnit.test('empty a tree by deleting its elements', (assert) => {
const tree = new LeafyTree(KEYED_LIGHTSWITCH_MONOID);
tree.set(0, FLIP);
tree.set(0, OFF);
tree.delete(0);
tree.delete(0);
assert.deepEqual(tree.summarize(-Infinity, Infinity), [[Infinity, -Infinity], HOLD]);
});
QUnit.test('assign heights to vertices in a one-element tree', (assert) => {
const tree = new LeafyTree(KEYED_LIGHTSWITCH_MONOID);
tree.set(0, FLIP);
assert.deepEqual(tree._root.height, 0);
});
QUnit.test('assign heights to vertices in a two-element tree', (assert) => {
const tree = new LeafyTree(KEYED_LIGHTSWITCH_MONOID);
tree.set(0, FLIP);
tree.set(1, OFF);
assert.deepEqual(tree._root.height, 1);
assert.deepEqual(tree._root.children[0].height, 0);
assert.deepEqual(tree._root.children[1].height, 0);
});
QUnit.test('assign heights to vertices in a two-element tree', (assert) => {
const tree = new LeafyTree(KEYED_LIGHTSWITCH_MONOID);
tree.set(0, FLIP);
tree.set(1, OFF);
tree.set(2, HOLD);
assert.deepEqual(tree._root.height, 2);
assert.deepEqual(tree._root.children[0].height, 0);
assert.deepEqual(tree._root.children[1].height, 1);
assert.deepEqual(tree._root.children[1].children[0].height, 0);
assert.deepEqual(tree._root.children[1].children[1].height, 0);
});
QUnit.test('assign heights to vertices in a many-element tree', (assert) => {
const tree = new LeafyTree(KEYED_LIGHTSWITCH_MONOID);
// intentionally build the tree balanced, so that we're not also testing rotation code:
tree.set(2, FLIP);
tree.set(3, FLIP);
tree.set(1, OFF);
tree.set(4, OFF);
tree.set(0, FLIP);
tree.set(5, ON);
assert.deepEqual(tree._root.height, 3);
assert.deepEqual(tree._root.children[0].height, 2);
assert.deepEqual(tree._root.children[0].children[0].height, 1);
assert.deepEqual(tree._root.children[0].children[0].children[0].height, 0);
assert.deepEqual(tree._root.children[0].children[0].children[1].height, 0);
assert.deepEqual(tree._root.children[0].children[1].height, 0);
assert.deepEqual(tree._root.children[1].height, 2);
assert.deepEqual(tree._root.children[1].children[0].height, 0);
assert.deepEqual(tree._root.children[1].children[1].height, 1);
assert.deepEqual(tree._root.children[1].children[1].children[0].height, 0);
assert.deepEqual(tree._root.children[1].children[1].children[1].height, 0);
});
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
<script type="module" src="test_priority_queue.js"></script> <script type="module" src="test_priority_queue.js"></script>
<script type="module" src="test_simulation.js"></script> <script type="module" src="test_simulation.js"></script>
<script type="module" src="test_undirected_graph.js"></script> <script type="module" src="test_undirected_graph.js"></script>
<script type="module" src="test_leafy_tree.js"></script>
<script type="module" src="test_transit.js"></script> <script type="module" src="test_transit.js"></script>
<script type="module" src="test_patching.js"></script> <script type="module" src="test_patching.js"></script>
<script type="module" src="test_heat_map.js"></script> <script type="module" src="test_heat_map.js"></script>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment