From 53e94d389a138d4e2e0fec26b8db24556849cd2d Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Sat, 11 May 2024 20:03:56 -0700 Subject: [PATCH 1/9] CPUAggregator --- .../cpu-aggregator/aggregate.ts | 85 ++++++++ .../cpu-aggregator/cpu-aggregator.ts | 189 ++++++++++++++++++ .../cpu-aggregator/sort.ts | 58 ++++++ .../cpu-aggregator/vertex-accessor.ts | 82 ++++++++ modules/aggregation-layers/src/index.ts | 5 + 5 files changed, 419 insertions(+) create mode 100644 modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts create mode 100644 modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts create mode 100644 modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort.ts create mode 100644 modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/vertex-accessor.ts diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts new file mode 100644 index 00000000000..089dd98b26a --- /dev/null +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts @@ -0,0 +1,85 @@ +import type {Bin} from './cpu-aggregator'; +import type {AggregationOperation} from '../aggregator'; + +type AggregationFunc = (pointIndices: number[], getValue: (index: number) => number) => number; + +const sum: AggregationFunc = (pointIndices, getValue) => { + let result = 0; + for (const i of pointIndices) { + result += getValue(i); + } + return result; +}; + +const mean: AggregationFunc = (pointIndices, getValue) => { + if (pointIndices.length === 0) { + return NaN; + } + return sum(pointIndices, getValue) / pointIndices.length; +}; + +const min: AggregationFunc = (pointIndices, getValue) => { + let result = Infinity; + for (const i of pointIndices) { + const value = getValue(i); + if (value < result) { + result = value; + } + } + return result; +}; + +const max: AggregationFunc = (pointIndices, getValue) => { + let result = -Infinity; + for (const i of pointIndices) { + const value = getValue(i); + if (value > result) { + result = value; + } + } + return result; +}; + +const AGGREGATION_FUNC: Record = { + SUM: sum, + MEAN: mean, + MIN: min, + MAX: max +} as const; + +/** + * Performs aggregation + * @returns Floa32Array of aggregated values, one for each bin, and the [min,max] of the values + */ +export function aggregateChannel({ + bins, + getWeight, + operation, + target +}: { + bins: Bin[]; + getWeight: (index: number) => number; + operation: AggregationOperation; + /** Optional typed array to pack values into */ + target?: Float32Array; +}): { + value: Float32Array; + domain: [min: number, max: number]; +} { + if (!target || target.length < bins.length) { + target = new Float32Array(bins.length); + } + let min = Infinity; + let max = -Infinity; + + const aggregationFunc = AGGREGATION_FUNC[operation]; + + for (let j = 0; j < bins.length; j++) { + const {points} = bins[j]; + target[j] = aggregationFunc(points, getWeight); + if (target[j] < min) min = target[j]; + if (target[j] > max) max = target[j]; + } + + return {value: target, domain: [min, max]}; +} diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts new file mode 100644 index 00000000000..2fb7d2d5b50 --- /dev/null +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts @@ -0,0 +1,189 @@ +import type {Aggregator, AggregationProps} from '../aggregator'; +import {_deepEqual as deepEqual, BinaryAttribute} from '@deck.gl/core'; +import {sortBins, packBinIds} from './sort'; +import {aggregateChannel} from './aggregate'; +import {VertexAccessor, evaluateVertexAccessor} from './vertex-accessor'; + +/** Settings used to construct a new CPUAggregator */ +export type CPUAggregatorSettings = { + /** Size of bin IDs */ + dimensions: number; + /** Accessor to map each data point to a bin ID. + * If dimensions=1, bin ID should be a number; + * If dimensions>1, bin ID should be an array with [dimensions] elements; + * The data point will be skipped if bin ID is null. + */ + getBin: VertexAccessor; + /** Accessor to map each data point to a weight value, defined per channel */ + getWeight: VertexAccessor[]; +}; + +/** Options used to run CPU aggregation, can be changed at any time */ +export type CPUAggregationProps = AggregationProps & {}; + +export type Bin = { + id: number | number[]; + index: number; + /** list of data point indices */ + points: number[]; +}; + +/** An Aggregator implementation that calculates aggregation on the CPU */ +export class CPUAggregator implements Aggregator { + dimensions: number; + numChannels: number; + + props: CPUAggregationProps = { + binOptions: {}, + pointCount: 0, + operations: [], + attributes: {} + }; + + protected getBinId: CPUAggregatorSettings['getBin']; + protected getWeight: CPUAggregatorSettings['getWeight']; + /** Dirty flag + * If true, redo sorting + * If array, redo aggregation on the specified channel + */ + protected needsUpdate: boolean[] | boolean; + + protected bins: Bin[] = []; + protected binIds: Float32Array | null = null; + protected results: {value: Float32Array; domain: [min: number, max: number]}[] = []; + + constructor({dimensions, getBin, getWeight}: CPUAggregatorSettings) { + this.dimensions = dimensions; + this.numChannels = getWeight.length; + this.getBinId = getBin; + this.getWeight = getWeight; + this.needsUpdate = true; + } + + get numBins() { + return this.bins.length; + } + + /** Update aggregation props */ + setProps(props: Partial) { + const oldProps = this.props; + + if (props.binOptions) { + if (!deepEqual(props.binOptions, oldProps.binOptions, 2)) { + this.setNeedsUpdate(); + } + } + if (props.operations) { + for (let channel = 0; channel < this.numChannels; channel++) { + if (props.operations[channel] !== oldProps.operations[channel]) { + this.setNeedsUpdate(channel); + } + } + } + if (props.pointCount !== undefined && props.pointCount !== oldProps.pointCount) { + this.setNeedsUpdate(); + } + if (props.attributes) { + props.attributes = {...oldProps.attributes, ...props.attributes}; + } + Object.assign(this.props, props); + } + + /** Flags a channel to need update + * This is called internally by setProps() if certain props change + * Users of this class still need to manually set the dirty flag sometimes, because even if no props changed + * the underlying buffers could have been updated and require rerunning the aggregation + * @param {number} channel - mark the given channel as dirty. If not provided, all channels will be updated. + */ + setNeedsUpdate(channel?: number) { + if (channel === undefined) { + this.needsUpdate = true; + } else if (this.needsUpdate !== true) { + this.needsUpdate = this.needsUpdate || []; + this.needsUpdate[channel] = true; + } + } + + /** Run aggregation */ + update() { + if (this.needsUpdate === true) { + this.bins = sortBins({ + pointCount: this.props.pointCount, + getBinId: evaluateVertexAccessor( + this.getBinId, + this.props.attributes, + this.props.binOptions + ) + }); + this.binIds = packBinIds({ + bins: this.bins, + dimensions: this.dimensions, + target: this.binIds + }); + } + for (let channel = 0; channel < this.numChannels; channel++) { + if (this.needsUpdate === true || this.needsUpdate[channel]) { + this.results[channel] = aggregateChannel({ + bins: this.bins, + getWeight: evaluateVertexAccessor( + this.getWeight[channel], + this.props.attributes, + undefined + ), + operation: this.props.operations[channel], + target: this.results[channel]?.value + }); + } + } + } + + /** Returns an accessor to the bins. */ + getBins(): BinaryAttribute | null { + if (!this.binIds) { + return null; + } + return {value: this.binIds, type: 'float32', size: this.dimensions}; + } + + /** Returns an accessor to the output for a given channel. */ + getResult(channel: number): BinaryAttribute | null { + const result = this.results[channel]; + if (!result) { + return null; + } + return {value: result.value, type: 'float32', size: 1}; + } + + /** Returns the [min, max] of aggregated values for a given channel. */ + getResultDomain(channel: number): [min: number, max: number] { + return this.results[channel]?.domain ?? [Infinity, -Infinity]; + } + + /** Returns the information for a given bin. */ + getBin(index: number): { + /** The original id */ + id: number | number[]; + /** Aggregated values by channel */ + value: number[]; + /** Count of data points in this bin */ + count: number; + /** List of data point indices that fall into this bin. */ + points?: number[]; + } | null { + const bin = this.bins[index]; + if (!bin) { + return null; + } + const value = new Array(this.numChannels); + for (let i = 0; i < value.length; i++) { + const result = this.results[i]; + value[i] = result?.value[index]; + } + return { + id: bin.id, + value, + count: bin.points.length, + points: bin.points + }; + } +} diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort.ts new file mode 100644 index 00000000000..06803857013 --- /dev/null +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort.ts @@ -0,0 +1,58 @@ +import type {Bin} from './cpu-aggregator'; + +/** Group data points into bins */ +export function sortBins({ + pointCount, + getBinId +}: { + pointCount: number; + getBinId: (index: number) => number | number[] | null; +}): Bin[] { + const binsById: Map = new Map(); + + for (let i = 0; i < pointCount; i++) { + const id = getBinId(i); + if (id === null) { + continue; + } + let bin = binsById.get(String(id)); + if (bin) { + bin.points.push(i); + } else { + bin = { + id, + index: binsById.size, + points: [i] + }; + binsById.set(String(id), bin); + } + } + return Array.from(binsById.values()); +} + +/** Pack bin ids into a typed array */ +export function packBinIds({ + bins, + dimensions, + target +}: { + bins: Bin[]; + /** Size of bin IDs */ + dimensions: number; + /** Optional typed array to pack values into */ + target: Float32Array | null; +}): Float32Array { + const targetLength = bins.length * dimensions; + if (!target || target.length < targetLength) { + target = new Float32Array(targetLength); + } + for (let i = 0; i < bins.length; i++) { + const {id} = bins[i]; + if (Array.isArray(id)) { + target.set(id, i * dimensions); + } else { + target[i] = id; + } + } + return target; +} diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/vertex-accessor.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/vertex-accessor.ts new file mode 100644 index 00000000000..e8de34c75f3 --- /dev/null +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/vertex-accessor.ts @@ -0,0 +1,82 @@ +import type {Attribute} from '@deck.gl/core'; +import type {TypedArray} from '@luma.gl/core'; + +/** This is designed to mirror a vertex shader function + * For each vertex, calculates a value from attribtes, vertex index and options (uniforms) + */ +export type VertexAccessor = { + /** Attribute ids that provide input to getValue, used to index into the attributes map. + * For example `['position', 'size']` + */ + sources?: string[]; + /** Called for each data point to retrieve a value during update. */ + getValue: ( + /** Attributes at the vertex index */ + data: any, + /** Vertex index */ + index: number, + /** Shared options across all vertices */ + options: OptionsT + ) => ValueT; +}; + +/** Evaluate a VertexAccessor with a set of attributes */ +export function evaluateVertexAccessor( + accessor: VertexAccessor, + attributes: Record, + options: OptionsT +): (vertexIndex: number) => ValueT { + const vertexReaders: {[id: string]: (i: number) => number | number[]} = {}; + for (const id of accessor.sources || []) { + const attribute = attributes[id]; + if (attribute) { + vertexReaders[id] = getVertexReader(attribute); + } else { + throw new Error(`Cannot find attribute ${id}`); + } + } + const data: {[id: string]: number | number[]} = {}; + + return (vertexIndex: number) => { + for (const id in vertexReaders) { + data[id] = vertexReaders[id](vertexIndex); + } + return accessor.getValue(data, vertexIndex, options); + }; +} + +/** Read value out of a deck.gl Attribute by vertex */ +function getVertexReader(attribute: Attribute): (vertexIndex: number) => number | number[] { + const value = attribute.value as TypedArray; + const {offset = 0, stride, size} = attribute.getAccessor(); + const bytesPerElement = value.BYTES_PER_ELEMENT; + const elementOffset = offset / bytesPerElement; + const elementStride = stride ? stride / bytesPerElement : size; + + if (size === 1) { + // Size 1, returns (i: number) => number + if (attribute.isConstant) { + return () => value[0]; + } + return (vertexIndex: number) => { + const i = elementOffset + elementStride * vertexIndex; + return value[i]; + }; + } + + // Size >1, returns (i: number) => number[] + let result: number[]; + if (attribute.isConstant) { + result = Array.from(value); + return () => result; + } + + result = new Array(size); + return (vertexIndex: number) => { + const i = elementOffset + elementStride * vertexIndex; + for (let j = 0; j < size; j++) { + result[j] = value[i + j]; + } + return result; + }; +} diff --git a/modules/aggregation-layers/src/index.ts b/modules/aggregation-layers/src/index.ts index 8cbd48ee334..be70f136f11 100644 --- a/modules/aggregation-layers/src/index.ts +++ b/modules/aggregation-layers/src/index.ts @@ -34,6 +34,7 @@ export {default as _AggregationLayer} from './aggregation-layer'; export {default as _BinSorter} from './utils/bin-sorter'; export {WebGLAggregator} from './aggregation-layer-v9/gpu-aggregator/webgl-aggregator'; +export {CPUAggregator} from './aggregation-layer-v9/cpu-aggregator/cpu-aggregator'; // types export type {ContourLayerProps} from './contour-layer/contour-layer'; @@ -48,3 +49,7 @@ export type { WebGLAggregationProps, WebGLAggregatorOptions } from './aggregation-layer-v9/gpu-aggregator/webgl-aggregator'; +export type { + CPUAggregationProps, + CPUAggregatorSettings +} from './aggregation-layer-v9/cpu-aggregator/cpu-aggregator'; From 61298923891987e58096038b5725b3ac7f8d973c Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Sun, 12 May 2024 23:41:07 -0700 Subject: [PATCH 2/9] tests --- modules/main/src/index.ts | 3 +- .../cpu-aggregator.spec.ts | 179 ++++++++++++++++++ test/modules/aggregation-layers/index.ts | 1 + 3 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts diff --git a/modules/main/src/index.ts b/modules/main/src/index.ts index 71d0af760e0..82bc9395bcb 100644 --- a/modules/main/src/index.ts +++ b/modules/main/src/index.ts @@ -121,7 +121,8 @@ export { GPUGridLayer, AGGREGATION_OPERATION, HeatmapLayer, - WebGLAggregator + WebGLAggregator, + CPUAggregator } from '@deck.gl/aggregation-layers'; export { diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts new file mode 100644 index 00000000000..b6a98ba829d --- /dev/null +++ b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts @@ -0,0 +1,179 @@ +import test from 'tape-promise/tape'; +import {Attribute} from '@deck.gl/core'; +import {CPUAggregator} from '@deck.gl/aggregation-layers'; +import {device} from '@deck.gl/test-utils'; + +import {IncomeSurvey} from './data-sample'; +import {binaryAttributeToArray} from './test-utils'; + +test('CPUAggregator#1D', t => { + // An aggregator that calculates: + // [0] total count [1] average income [2] highest education, grouped by age + const aggregator = new CPUAggregator({ + dimensions: 1, + getBin: { + sources: ['age'], + getValue: ({age}, index, {ageGroupSize}) => Math.floor(age / ageGroupSize) + }, + getWeight: [ + {getValue: () => 1}, + {sources: ['income'], getValue: ({income}) => income}, + {sources: ['education'], getValue: ({education}) => education} + ] + }); + + const attributes = { + age: new Attribute(device, {id: 'age', size: 1, type: 'float32', accessor: 'getAge'}), + income: new Attribute(device, {id: 'income', size: 1, type: 'float32', accessor: 'getIncome'}), + education: new Attribute(device, { + id: 'education', + size: 1, + type: 'float32', + accessor: 'getEducation' + }) + }; + attributes.age.setData({value: new Float32Array(IncomeSurvey.map(d => d.age))}); + attributes.income.setData({value: new Float32Array(IncomeSurvey.map(d => d.income))}); + attributes.education.setData({value: new Float32Array(IncomeSurvey.map(d => d.education))}); + + aggregator.setProps({ + pointCount: IncomeSurvey.length, + attributes, + operations: ['SUM', 'MEAN', 'MAX'], + binOptions: {ageGroupSize: 5} + }); + + aggregator.update(); + + t.is(aggregator.numBins, 14, 'numBins'); + + t.deepEqual( + binaryAttributeToArray(aggregator.getBins(), aggregator.numBins), + // prettier-ignore + [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + 'getBins()' + ); + + t.deepEqual( + binaryAttributeToArray(aggregator.getResult(0), aggregator.numBins), + // prettier-ignore + [1, 5, 5, 3, 2, 3, 2, 2, 2, 1, 2, 1, 1, 2], + 'getResult() - total counts' + ); + t.deepEqual(aggregator.getResultDomain(0), [1, 5], 'getResultDomain() - counts'); + + t.deepEqual( + binaryAttributeToArray(aggregator.getResult(1), aggregator.numBins), + // prettier-ignore + [25, 48, 54, 100, 145, 250, 72.5, 252.5, 107.5, 0, 127.5, 0, 40, 25], + 'getResult() - mean income' + ); + t.deepEqual(aggregator.getResultDomain(1), [0, 252.5], 'getResultDomain() - mean income'); + + t.deepEqual( + binaryAttributeToArray(aggregator.getResult(2), aggregator.numBins), + // prettier-ignore + [1, 3, 4, 5, 4, 5, 3, 3, 5, 3, 4, 1, 2, 3], + 'getResult() - max education' + ); + t.deepEqual(aggregator.getResultDomain(2), [1, 5], 'getResultDomain() - max education'); + + // {age: 40, household: 4, income: 140, education: 4}, + // {age: 42, household: 2, income: 110, education: 5}, + // {age: 44, household: 4, income: 500, education: 4}, + t.deepEqual( + aggregator.getBin(5), + {id: 8, count: 3, value: [3, 250, 5], points: [16, 17, 18]}, + 'getBin()' + ); + + attributes.age.delete(); + attributes.income.delete(); + attributes.education.delete(); + + t.end(); +}); + +test('CPUAggregator#2D', t => { + // An aggregator that calculates: + // [0] total count [1] average income, grouped by [age, education] + const aggregator = new CPUAggregator({ + dimensions: 2, + getBin: { + sources: ['age', 'education'], + getValue: ({age, education}, index, {ageGroupSize}) => { + // age: 20..59 + if (age >= 20 && age < 60) { + return [Math.floor(age / ageGroupSize), education]; + } + return null; + } + }, + getWeight: [{getValue: () => 1}, {sources: ['income'], getValue: ({income}) => income}] + }); + + const attributes = { + age: new Attribute(device, {id: 'age', size: 1, type: 'float32', accessor: 'getAge'}), + income: new Attribute(device, {id: 'income', size: 1, type: 'float32', accessor: 'getIncome'}), + education: new Attribute(device, { + id: 'education', + size: 1, + type: 'float32', + accessor: 'getEducation' + }) + }; + attributes.age.setData({value: new Float32Array(IncomeSurvey.map(d => d.age))}); + attributes.income.setData({value: new Float32Array(IncomeSurvey.map(d => d.income))}); + attributes.education.setData({value: new Float32Array(IncomeSurvey.map(d => d.education))}); + + aggregator.setProps({ + pointCount: IncomeSurvey.length, + attributes, + operations: ['SUM', 'MEAN'], + binOptions: {ageGroupSize: 10} + }); + + aggregator.update(); + + t.is(aggregator.numBins, 12, 'numBins'); + + t.deepEqual( + binaryAttributeToArray(aggregator.getBins(), aggregator.numBins), + // prettier-ignore + [ 2, 2, 2, 3, 2, 4, + 3, 3, 3, 4, 3, 5, + 4, 4, 4, 5, 4, 3, 4, 2, + 5, 3, 5, 5 ], + 'getBins()' + ); + + t.deepEqual( + binaryAttributeToArray(aggregator.getResult(0), aggregator.numBins), + // prettier-ignore + [ 4, 4, 2, 2, 2, 1, 2, 1, 1, 1, 3, 1 ], + 'getResult() - total counts' + ); + t.deepEqual(aggregator.getResultDomain(0), [1, 4], 'getResultDomain() - counts'); + + t.deepEqual( + binaryAttributeToArray(aggregator.getResult(1), aggregator.numBins), + // prettier-ignore + [25, 97.5, 10, 90, 175, 60, 320, 110, 80, 65, 200, 120 ], + 'getResult() - mean income' + ); + t.deepEqual(aggregator.getResultDomain(1), [10, 320], 'getResultDomain() - mean income'); + + // {age: 40, household: 4, income: 140, education: 4}, + // {age: 44, household: 4, income: 500, education: 4}, + t.deepEqual( + aggregator.getBin(6), + {id: [4, 4], count: 2, value: [2, 320], points: [16, 18]}, + 'getBin()' + ); + + attributes.age.delete(); + attributes.income.delete(); + attributes.education.delete(); + + t.end(); +}); diff --git a/test/modules/aggregation-layers/index.ts b/test/modules/aggregation-layers/index.ts index 6713fe7b763..33d40699aaf 100644 --- a/test/modules/aggregation-layers/index.ts +++ b/test/modules/aggregation-layers/index.ts @@ -42,3 +42,4 @@ import './utils/color-utils.spec'; import './utils/scale-utils.spec'; import './aggregation-layer-v9/webgl-aggregator.spec'; +import './aggregation-layer-v9/cpu-aggregator.spec'; From 6abf9d2959a56a0754844c82abbc7a84fe17ae32 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Sun, 9 Jun 2024 17:21:07 -0700 Subject: [PATCH 3/9] interface updates --- .../cpu-aggregator/aggregate.ts | 11 ++++++++--- .../cpu-aggregator/cpu-aggregator.ts | 18 +++++++++++------- .../cpu-aggregator.spec.ts | 8 ++++---- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts index 089dd98b26a..5407eaa4200 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts @@ -3,6 +3,10 @@ import type {AggregationOperation} from '../aggregator'; type AggregationFunc = (pointIndices: number[], getValue: (index: number) => number) => number; +const count: AggregationFunc = pointIndices => { + return pointIndices.length; +}; + const sum: AggregationFunc = (pointIndices, getValue) => { let result = 0; for (const i of pointIndices) { @@ -41,6 +45,7 @@ const max: AggregationFunc = (pointIndices, getValue) => { }; const AGGREGATION_FUNC: Record = { + COUNT: count, SUM: sum, MEAN: mean, MIN: min, @@ -53,12 +58,12 @@ const AGGREGATION_FUNC: Record = { */ export function aggregateChannel({ bins, - getWeight, + getValue, operation, target }: { bins: Bin[]; - getWeight: (index: number) => number; + getValue: (index: number) => number; operation: AggregationOperation; /** Optional typed array to pack values into */ target?: Float32Array; @@ -76,7 +81,7 @@ export function aggregateChannel({ for (let j = 0; j < bins.length; j++) { const {points} = bins[j]; - target[j] = aggregationFunc(points, getWeight); + target[j] = aggregationFunc(points, getValue); if (target[j] < min) min = target[j]; if (target[j] > max) max = target[j]; } diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts index 2fb7d2d5b50..009267f2dfd 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts @@ -15,7 +15,7 @@ export type CPUAggregatorSettings = { */ getBin: VertexAccessor; /** Accessor to map each data point to a weight value, defined per channel */ - getWeight: VertexAccessor[]; + getValue: VertexAccessor[]; }; /** Options used to run CPU aggregation, can be changed at any time */ @@ -41,7 +41,7 @@ export class CPUAggregator implements Aggregator { }; protected getBinId: CPUAggregatorSettings['getBin']; - protected getWeight: CPUAggregatorSettings['getWeight']; + protected getValue: CPUAggregatorSettings['getValue']; /** Dirty flag * If true, redo sorting * If array, redo aggregation on the specified channel @@ -52,14 +52,16 @@ export class CPUAggregator implements Aggregator { protected binIds: Float32Array | null = null; protected results: {value: Float32Array; domain: [min: number, max: number]}[] = []; - constructor({dimensions, getBin, getWeight}: CPUAggregatorSettings) { + constructor({dimensions, getBin, getValue}: CPUAggregatorSettings) { this.dimensions = dimensions; - this.numChannels = getWeight.length; + this.numChannels = getValue.length; this.getBinId = getBin; - this.getWeight = getWeight; + this.getValue = getValue; this.needsUpdate = true; } + destroy() {} + get numBins() { return this.bins.length; } @@ -125,8 +127,8 @@ export class CPUAggregator implements Aggregator { if (this.needsUpdate === true || this.needsUpdate[channel]) { this.results[channel] = aggregateChannel({ bins: this.bins, - getWeight: evaluateVertexAccessor( - this.getWeight[channel], + getValue: evaluateVertexAccessor( + this.getValue[channel], this.props.attributes, undefined ), @@ -137,6 +139,8 @@ export class CPUAggregator implements Aggregator { } } + preDraw() {} + /** Returns an accessor to the bins. */ getBins(): BinaryAttribute | null { if (!this.binIds) { diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts index b6a98ba829d..16558a2072a 100644 --- a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts +++ b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts @@ -15,7 +15,7 @@ test('CPUAggregator#1D', t => { sources: ['age'], getValue: ({age}, index, {ageGroupSize}) => Math.floor(age / ageGroupSize) }, - getWeight: [ + getValue: [ {getValue: () => 1}, {sources: ['income'], getValue: ({income}) => income}, {sources: ['education'], getValue: ({education}) => education} @@ -94,7 +94,7 @@ test('CPUAggregator#1D', t => { t.end(); }); -test('CPUAggregator#2D', t => { +test.only('CPUAggregator#2D', t => { // An aggregator that calculates: // [0] total count [1] average income, grouped by [age, education] const aggregator = new CPUAggregator({ @@ -109,7 +109,7 @@ test('CPUAggregator#2D', t => { return null; } }, - getWeight: [{getValue: () => 1}, {sources: ['income'], getValue: ({income}) => income}] + getValue: [{getValue: () => 1}, {sources: ['income'], getValue: ({income}) => income}] }); const attributes = { @@ -129,7 +129,7 @@ test('CPUAggregator#2D', t => { aggregator.setProps({ pointCount: IncomeSurvey.length, attributes, - operations: ['SUM', 'MEAN'], + operations: ['COUNT', 'MEAN'], binOptions: {ageGroupSize: 10} }); From 83602bd5e4bec225f0da2dfa246576f2380ffe9d Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Sun, 9 Jun 2024 17:43:44 -0700 Subject: [PATCH 4/9] type improvements --- .../cpu-aggregator/cpu-aggregator.ts | 34 ++++++++----------- .../cpu-aggregator/sort.ts | 2 +- modules/aggregation-layers/src/index.ts | 2 +- .../cpu-aggregator.spec.ts | 6 ++-- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts index 009267f2dfd..25745d7b11c 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts @@ -1,11 +1,11 @@ -import type {Aggregator, AggregationProps} from '../aggregator'; +import type {Aggregator, AggregationProps, AggregatedBin} from '../aggregator'; import {_deepEqual as deepEqual, BinaryAttribute} from '@deck.gl/core'; import {sortBins, packBinIds} from './sort'; import {aggregateChannel} from './aggregate'; import {VertexAccessor, evaluateVertexAccessor} from './vertex-accessor'; -/** Settings used to construct a new CPUAggregator */ -export type CPUAggregatorSettings = { +/** Options used to construct a new CPUAggregator */ +export type CPUAggregatorOptions = { /** Size of bin IDs */ dimensions: number; /** Accessor to map each data point to a bin ID. @@ -13,16 +13,16 @@ export type CPUAggregatorSettings = { * If dimensions>1, bin ID should be an array with [dimensions] elements; * The data point will be skipped if bin ID is null. */ - getBin: VertexAccessor; + getBin: VertexAccessor; /** Accessor to map each data point to a weight value, defined per channel */ getValue: VertexAccessor[]; }; -/** Options used to run CPU aggregation, can be changed at any time */ +/** Props used to run CPU aggregation, can be changed at any time */ export type CPUAggregationProps = AggregationProps & {}; export type Bin = { - id: number | number[]; + id: number[]; index: number; /** list of data point indices */ points: number[]; @@ -40,8 +40,8 @@ export class CPUAggregator implements Aggregator { attributes: {} }; - protected getBinId: CPUAggregatorSettings['getBin']; - protected getValue: CPUAggregatorSettings['getValue']; + protected getBinId: CPUAggregatorOptions['getBin']; + protected getValue: CPUAggregatorOptions['getValue']; /** Dirty flag * If true, redo sorting * If array, redo aggregation on the specified channel @@ -52,7 +52,7 @@ export class CPUAggregator implements Aggregator { protected binIds: Float32Array | null = null; protected results: {value: Float32Array; domain: [min: number, max: number]}[] = []; - constructor({dimensions, getBin, getValue}: CPUAggregatorSettings) { + constructor({dimensions, getBin, getValue}: CPUAggregatorOptions) { this.dimensions = dimensions; this.numChannels = getValue.length; this.getBinId = getBin; @@ -164,16 +164,12 @@ export class CPUAggregator implements Aggregator { } /** Returns the information for a given bin. */ - getBin(index: number): { - /** The original id */ - id: number | number[]; - /** Aggregated values by channel */ - value: number[]; - /** Count of data points in this bin */ - count: number; - /** List of data point indices that fall into this bin. */ - points?: number[]; - } | null { + getBin(index: number): + | (AggregatedBin & { + /** List of data point indices that fall into this bin. */ + points?: number[]; + }) + | null { const bin = this.bins[index]; if (!bin) { return null; diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort.ts index 06803857013..ead5222a605 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort.ts @@ -6,7 +6,7 @@ export function sortBins({ getBinId }: { pointCount: number; - getBinId: (index: number) => number | number[] | null; + getBinId: (index: number) => number[] | null; }): Bin[] { const binsById: Map = new Map(); diff --git a/modules/aggregation-layers/src/index.ts b/modules/aggregation-layers/src/index.ts index be70f136f11..ba86939e3ef 100644 --- a/modules/aggregation-layers/src/index.ts +++ b/modules/aggregation-layers/src/index.ts @@ -51,5 +51,5 @@ export type { } from './aggregation-layer-v9/gpu-aggregator/webgl-aggregator'; export type { CPUAggregationProps, - CPUAggregatorSettings + CPUAggregatorOptions } from './aggregation-layer-v9/cpu-aggregator/cpu-aggregator'; diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts index 16558a2072a..e51c3ed6a57 100644 --- a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts +++ b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts @@ -13,7 +13,7 @@ test('CPUAggregator#1D', t => { dimensions: 1, getBin: { sources: ['age'], - getValue: ({age}, index, {ageGroupSize}) => Math.floor(age / ageGroupSize) + getValue: ({age}, index, {ageGroupSize}) => [Math.floor(age / ageGroupSize)] }, getValue: [ {getValue: () => 1}, @@ -83,7 +83,7 @@ test('CPUAggregator#1D', t => { // {age: 44, household: 4, income: 500, education: 4}, t.deepEqual( aggregator.getBin(5), - {id: 8, count: 3, value: [3, 250, 5], points: [16, 17, 18]}, + {id: [8], count: 3, value: [3, 250, 5], points: [16, 17, 18]}, 'getBin()' ); @@ -94,7 +94,7 @@ test('CPUAggregator#1D', t => { t.end(); }); -test.only('CPUAggregator#2D', t => { +test('CPUAggregator#2D', t => { // An aggregator that calculates: // [0] total count [1] average income, grouped by [age, education] const aggregator = new CPUAggregator({ From 114442adb6f002f8f2b0ea8d7cd7a91c93ffa16d Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 11 Jun 2024 07:32:54 -0700 Subject: [PATCH 5/9] renames --- .../cpu-aggregator/cpu-aggregator.ts | 12 ++++++------ .../cpu-aggregator.spec.ts | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts index 25745d7b11c..1caed37c3a8 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts @@ -31,7 +31,7 @@ export type Bin = { /** An Aggregator implementation that calculates aggregation on the CPU */ export class CPUAggregator implements Aggregator { dimensions: number; - numChannels: number; + channelCount: number; props: CPUAggregationProps = { binOptions: {}, @@ -54,7 +54,7 @@ export class CPUAggregator implements Aggregator { constructor({dimensions, getBin, getValue}: CPUAggregatorOptions) { this.dimensions = dimensions; - this.numChannels = getValue.length; + this.channelCount = getValue.length; this.getBinId = getBin; this.getValue = getValue; this.needsUpdate = true; @@ -62,7 +62,7 @@ export class CPUAggregator implements Aggregator { destroy() {} - get numBins() { + get binCount() { return this.bins.length; } @@ -76,7 +76,7 @@ export class CPUAggregator implements Aggregator { } } if (props.operations) { - for (let channel = 0; channel < this.numChannels; channel++) { + for (let channel = 0; channel < this.channelCount; channel++) { if (props.operations[channel] !== oldProps.operations[channel]) { this.setNeedsUpdate(channel); } @@ -123,7 +123,7 @@ export class CPUAggregator implements Aggregator { target: this.binIds }); } - for (let channel = 0; channel < this.numChannels; channel++) { + for (let channel = 0; channel < this.channelCount; channel++) { if (this.needsUpdate === true || this.needsUpdate[channel]) { this.results[channel] = aggregateChannel({ bins: this.bins, @@ -174,7 +174,7 @@ export class CPUAggregator implements Aggregator { if (!bin) { return null; } - const value = new Array(this.numChannels); + const value = new Array(this.channelCount); for (let i = 0; i < value.length; i++) { const result = this.results[i]; value[i] = result?.value[index]; diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts index e51c3ed6a57..3eb05e285fd 100644 --- a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts +++ b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts @@ -45,17 +45,17 @@ test('CPUAggregator#1D', t => { aggregator.update(); - t.is(aggregator.numBins, 14, 'numBins'); + t.is(aggregator.binCount, 14, 'binCount'); t.deepEqual( - binaryAttributeToArray(aggregator.getBins(), aggregator.numBins), + binaryAttributeToArray(aggregator.getBins(), aggregator.binCount), // prettier-ignore [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], 'getBins()' ); t.deepEqual( - binaryAttributeToArray(aggregator.getResult(0), aggregator.numBins), + binaryAttributeToArray(aggregator.getResult(0), aggregator.binCount), // prettier-ignore [1, 5, 5, 3, 2, 3, 2, 2, 2, 1, 2, 1, 1, 2], 'getResult() - total counts' @@ -63,7 +63,7 @@ test('CPUAggregator#1D', t => { t.deepEqual(aggregator.getResultDomain(0), [1, 5], 'getResultDomain() - counts'); t.deepEqual( - binaryAttributeToArray(aggregator.getResult(1), aggregator.numBins), + binaryAttributeToArray(aggregator.getResult(1), aggregator.binCount), // prettier-ignore [25, 48, 54, 100, 145, 250, 72.5, 252.5, 107.5, 0, 127.5, 0, 40, 25], 'getResult() - mean income' @@ -71,7 +71,7 @@ test('CPUAggregator#1D', t => { t.deepEqual(aggregator.getResultDomain(1), [0, 252.5], 'getResultDomain() - mean income'); t.deepEqual( - binaryAttributeToArray(aggregator.getResult(2), aggregator.numBins), + binaryAttributeToArray(aggregator.getResult(2), aggregator.binCount), // prettier-ignore [1, 3, 4, 5, 4, 5, 3, 3, 5, 3, 4, 1, 2, 3], 'getResult() - max education' @@ -135,10 +135,10 @@ test('CPUAggregator#2D', t => { aggregator.update(); - t.is(aggregator.numBins, 12, 'numBins'); + t.is(aggregator.binCount, 12, 'binCount'); t.deepEqual( - binaryAttributeToArray(aggregator.getBins(), aggregator.numBins), + binaryAttributeToArray(aggregator.getBins(), aggregator.binCount), // prettier-ignore [ 2, 2, 2, 3, 2, 4, 3, 3, 3, 4, 3, 5, @@ -148,7 +148,7 @@ test('CPUAggregator#2D', t => { ); t.deepEqual( - binaryAttributeToArray(aggregator.getResult(0), aggregator.numBins), + binaryAttributeToArray(aggregator.getResult(0), aggregator.binCount), // prettier-ignore [ 4, 4, 2, 2, 2, 1, 2, 1, 1, 1, 3, 1 ], 'getResult() - total counts' @@ -156,7 +156,7 @@ test('CPUAggregator#2D', t => { t.deepEqual(aggregator.getResultDomain(0), [1, 4], 'getResultDomain() - counts'); t.deepEqual( - binaryAttributeToArray(aggregator.getResult(1), aggregator.numBins), + binaryAttributeToArray(aggregator.getResult(1), aggregator.binCount), // prettier-ignore [25, 97.5, 10, 90, 175, 60, 320, 110, 80, 65, 200, 120 ], 'getResult() - mean income' From 6160b98bd335507f8a2cfb2388e77bc7283cd7e2 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Wed, 19 Jun 2024 08:36:04 -0700 Subject: [PATCH 6/9] address comments --- .../src/aggregation-layer-v9/aggregator.ts | 2 ++ .../cpu-aggregator/aggregate.ts | 7 +++++-- .../cpu-aggregator/cpu-aggregator.ts | 17 ++++++----------- .../cpu-aggregator/{sort.ts => sort-bins.ts} | 0 4 files changed, 13 insertions(+), 13 deletions(-) rename modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/{sort.ts => sort-bins.ts} (100%) diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/aggregator.ts index 5f432d8dc89..b90187c7520 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/aggregator.ts @@ -23,6 +23,8 @@ export type AggregatedBin = { value: number[]; /** Count of data points in this bin */ count: number; + /** Indices of data points in this bin. Only available if using CPU aggregation. */ + pointIndices?: number[]; }; /** diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts index 5407eaa4200..ab5a3d3f948 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts @@ -53,17 +53,20 @@ const AGGREGATION_FUNC: Record = { } as const; /** - * Performs aggregation + * Performs the aggregation step. See interface Aggregator comments. * @returns Floa32Array of aggregated values, one for each bin, and the [min,max] of the values */ -export function aggregateChannel({ +export function aggregate({ bins, getValue, operation, target }: { + /** Data points sorted by bins */ bins: Bin[]; + /** Given the index of a data point, returns its value */ getValue: (index: number) => number; + /** Method used to reduce a list of values to one number */ operation: AggregationOperation; /** Optional typed array to pack values into */ target?: Float32Array; diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts index 1caed37c3a8..72e72562183 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts @@ -1,7 +1,7 @@ import type {Aggregator, AggregationProps, AggregatedBin} from '../aggregator'; import {_deepEqual as deepEqual, BinaryAttribute} from '@deck.gl/core'; -import {sortBins, packBinIds} from './sort'; -import {aggregateChannel} from './aggregate'; +import {sortBins, packBinIds} from './sort-bins'; +import {aggregate} from './aggregate'; import {VertexAccessor, evaluateVertexAccessor} from './vertex-accessor'; /** Options used to construct a new CPUAggregator */ @@ -97,7 +97,7 @@ export class CPUAggregator implements Aggregator { * the underlying buffers could have been updated and require rerunning the aggregation * @param {number} channel - mark the given channel as dirty. If not provided, all channels will be updated. */ - setNeedsUpdate(channel?: number) { + setNeedsUpdate(channel?: number): void { if (channel === undefined) { this.needsUpdate = true; } else if (this.needsUpdate !== true) { @@ -125,7 +125,7 @@ export class CPUAggregator implements Aggregator { } for (let channel = 0; channel < this.channelCount; channel++) { if (this.needsUpdate === true || this.needsUpdate[channel]) { - this.results[channel] = aggregateChannel({ + this.results[channel] = aggregate({ bins: this.bins, getValue: evaluateVertexAccessor( this.getValue[channel], @@ -164,12 +164,7 @@ export class CPUAggregator implements Aggregator { } /** Returns the information for a given bin. */ - getBin(index: number): - | (AggregatedBin & { - /** List of data point indices that fall into this bin. */ - points?: number[]; - }) - | null { + getBin(index: number): AggregatedBin | null { const bin = this.bins[index]; if (!bin) { return null; @@ -183,7 +178,7 @@ export class CPUAggregator implements Aggregator { id: bin.id, value, count: bin.points.length, - points: bin.points + pointIndices: bin.points }; } } diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort-bins.ts similarity index 100% rename from modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort.ts rename to modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort-bins.ts From 5e69351fa29667cb014007dd26f1e99c73479e54 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Wed, 19 Jun 2024 09:00:38 -0700 Subject: [PATCH 7/9] test --- .../aggregation-layer-v9/cpu-aggregator.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts index 3eb05e285fd..48d883c4675 100644 --- a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts +++ b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts @@ -83,7 +83,7 @@ test('CPUAggregator#1D', t => { // {age: 44, household: 4, income: 500, education: 4}, t.deepEqual( aggregator.getBin(5), - {id: [8], count: 3, value: [3, 250, 5], points: [16, 17, 18]}, + {id: [8], count: 3, value: [3, 250, 5], pointIndices: [16, 17, 18]}, 'getBin()' ); @@ -167,7 +167,7 @@ test('CPUAggregator#2D', t => { // {age: 44, household: 4, income: 500, education: 4}, t.deepEqual( aggregator.getBin(6), - {id: [4, 4], count: 2, value: [2, 320], points: [16, 18]}, + {id: [4, 4], count: 2, value: [2, 320], pointIndices: [16, 18]}, 'getBin()' ); From 29a12a91c3e9a3a692d87a8a9fb64b32139f2559 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Fri, 21 Jun 2024 17:23:04 -0700 Subject: [PATCH 8/9] address comments --- .../cpu-aggregator/cpu-aggregator.ts | 39 +++++++++---------- .../webgl-aggregation-transform.ts | 14 +++---- .../gpu-aggregator/webgl-aggregator.ts | 39 ++++++++++--------- .../gpu-aggregator/webgl-bin-sorter.ts | 17 ++++---- modules/aggregation-layers/src/index.ts | 10 +---- 5 files changed, 58 insertions(+), 61 deletions(-) diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts index 72e72562183..c367af3ad89 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts @@ -5,7 +5,7 @@ import {aggregate} from './aggregate'; import {VertexAccessor, evaluateVertexAccessor} from './vertex-accessor'; /** Options used to construct a new CPUAggregator */ -export type CPUAggregatorOptions = { +export type CPUAggregatorProps = { /** Size of bin IDs */ dimensions: number; /** Accessor to map each data point to a bin ID. @@ -16,10 +16,10 @@ export type CPUAggregatorOptions = { getBin: VertexAccessor; /** Accessor to map each data point to a weight value, defined per channel */ getValue: VertexAccessor[]; -}; +} & Partial; /** Props used to run CPU aggregation, can be changed at any time */ -export type CPUAggregationProps = AggregationProps & {}; +type CPUAggregationProps = AggregationProps & {}; export type Bin = { id: number[]; @@ -30,18 +30,11 @@ export type Bin = { /** An Aggregator implementation that calculates aggregation on the CPU */ export class CPUAggregator implements Aggregator { - dimensions: number; - channelCount: number; + readonly dimensions: number; + readonly channelCount: number; - props: CPUAggregationProps = { - binOptions: {}, - pointCount: 0, - operations: [], - attributes: {} - }; + props: CPUAggregatorProps & CPUAggregationProps; - protected getBinId: CPUAggregatorOptions['getBin']; - protected getValue: CPUAggregatorOptions['getValue']; /** Dirty flag * If true, redo sorting * If array, redo aggregation on the specified channel @@ -52,12 +45,18 @@ export class CPUAggregator implements Aggregator { protected binIds: Float32Array | null = null; protected results: {value: Float32Array; domain: [min: number, max: number]}[] = []; - constructor({dimensions, getBin, getValue}: CPUAggregatorOptions) { - this.dimensions = dimensions; - this.channelCount = getValue.length; - this.getBinId = getBin; - this.getValue = getValue; + constructor(props: CPUAggregatorProps) { + this.dimensions = props.dimensions; + this.channelCount = props.getValue.length; + this.props = { + ...props, + binOptions: {}, + pointCount: 0, + operations: [], + attributes: {} + }; this.needsUpdate = true; + this.setProps(props); } destroy() {} @@ -112,7 +111,7 @@ export class CPUAggregator implements Aggregator { this.bins = sortBins({ pointCount: this.props.pointCount, getBinId: evaluateVertexAccessor( - this.getBinId, + this.props.getBin, this.props.attributes, this.props.binOptions ) @@ -128,7 +127,7 @@ export class CPUAggregator implements Aggregator { this.results[channel] = aggregate({ bins: this.bins, getValue: evaluateVertexAccessor( - this.getValue[channel], + this.props.getValue[channel], this.props.attributes, undefined ), diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts index 742a18737be..1f438b29770 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts @@ -2,7 +2,7 @@ import {BufferTransform} from '@luma.gl/engine'; import {glsl, createRenderTarget} from './utils'; import type {Device, Framebuffer, Buffer, Texture} from '@luma.gl/core'; -import type {WebGLAggregatorOptions} from './webgl-aggregator'; +import type {WebGLAggregatorProps} from './webgl-aggregator'; import type {AggregationOperation} from '../aggregator'; import {TEXTURE_WIDTH} from './webgl-bin-sorter'; @@ -26,10 +26,10 @@ export class WebGLAggregationTransform { /** Aggregated [min, max] for each channel */ private _domains: [min: number, max: number][] | null = null; - constructor(device: Device, settings: WebGLAggregatorOptions) { + constructor(device: Device, props: WebGLAggregatorProps) { this.device = device; - this.channelCount = settings.channelCount; - this.transform = createTransform(device, settings); + this.channelCount = props.channelCount; + this.transform = createTransform(device, props); this.domainFBO = createRenderTarget(device, 2, 1); } @@ -112,7 +112,7 @@ export class WebGLAggregationTransform { } } -function createTransform(device: Device, settings: WebGLAggregatorOptions): BufferTransform { +function createTransform(device: Device, props: WebGLAggregatorProps): BufferTransform { const vs = glsl`\ #version 300 es #define SHADER_NAME gpu-aggregation-domain-vertex @@ -228,8 +228,8 @@ void main() { blendAlphaOperation: 'max' }, defines: { - NUM_DIMS: settings.dimensions, - NUM_CHANNELS: settings.channelCount, + NUM_DIMS: props.dimensions, + NUM_CHANNELS: props.channelCount, SAMPLER_WIDTH: TEXTURE_WIDTH }, uniforms: { diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregator.ts index a16ff4fe694..cf70c354b11 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregator.ts @@ -8,7 +8,7 @@ import type {Device, Buffer, BufferLayout, TypedArray} from '@luma.gl/core'; import type {ShaderModule} from '@luma.gl/shadertools'; /** Options used to construct a new WebGLAggregator */ -export type WebGLAggregatorOptions = { +export type WebGLAggregatorProps = { /** Size of bin IDs */ dimensions: 1 | 2; /** How many properties to perform aggregation on */ @@ -30,10 +30,10 @@ export type WebGLAggregatorOptions = { modules?: ShaderModule[]; /** Shadertool module defines */ defines?: Record; -}; +} & Partial; /** Props used to run GPU aggregation, can be changed at any time */ -export type WebGLAggregationProps = AggregationProps & { +type WebGLAggregationProps = AggregationProps & { /** Limits of binId defined for each dimension. Ids outside of the [start, end) are ignored. */ binIdRange: [start: number, end: number][]; @@ -51,18 +51,12 @@ export class WebGLAggregator implements Aggregator { ); } - dimensions: 1 | 2; - channelCount: 1 | 2 | 3; + readonly dimensions: 1 | 2; + readonly channelCount: 1 | 2 | 3; binCount: number = 0; - device: Device; - props: WebGLAggregationProps = { - pointCount: 0, - binIdRange: [[0, 0]], - operations: [], - attributes: {}, - binOptions: {} - }; + readonly device: Device; + props: WebGLAggregatorProps & WebGLAggregationProps; /** Dirty flag per channel */ protected needsUpdate: boolean[]; @@ -71,13 +65,22 @@ export class WebGLAggregator implements Aggregator { /** Step 2. (optional) calculate the min/max across all bins */ protected aggregationTransform: WebGLAggregationTransform; - constructor(device: Device, settings: WebGLAggregatorOptions) { + constructor(device: Device, props: WebGLAggregatorProps) { this.device = device; - this.dimensions = settings.dimensions; - this.channelCount = settings.channelCount; + this.dimensions = props.dimensions; + this.channelCount = props.channelCount; + this.props = { + ...props, + pointCount: 0, + binIdRange: [[0, 0]], + operations: [], + attributes: {}, + binOptions: {} + }; this.needsUpdate = new Array(this.channelCount).fill(true); - this.binSorter = new WebGLBinSorter(device, settings); - this.aggregationTransform = new WebGLAggregationTransform(device, settings); + this.binSorter = new WebGLBinSorter(device, props); + this.aggregationTransform = new WebGLAggregationTransform(device, props); + this.setProps(props); } getBins(): BinaryAttribute | null { diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts index e42c6113dc2..84d18e4277b 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts @@ -2,7 +2,7 @@ import {Model, ModelProps} from '@luma.gl/engine'; import {glsl, createRenderTarget} from './utils'; import type {Device, Framebuffer, Texture} from '@luma.gl/core'; -import type {WebGLAggregatorOptions} from './webgl-aggregator'; +import type {WebGLAggregatorProps} from './webgl-aggregator'; import type {AggregationOperation} from '../aggregator'; const COLOR_CHANNELS = [0x1, 0x2, 0x4, 0x8]; // GPU color mask RED, GREEN, BLUE, ALPHA @@ -30,9 +30,9 @@ export class WebGLBinSorter { */ private binsFBO: Framebuffer | null = null; - constructor(device: Device, settings: WebGLAggregatorOptions) { + constructor(device: Device, props: WebGLAggregatorProps) { this.device = device; - this.model = createModel(device, settings); + this.model = createModel(device, props); } get texture(): Texture | null { @@ -175,10 +175,10 @@ function getMaskByOperation( return result; } -function createModel(device: Device, settings: WebGLAggregatorOptions): Model { - let userVs = settings.vs; +function createModel(device: Device, props: WebGLAggregatorProps): Model { + let userVs = props.vs; - if (settings.dimensions === 2) { + if (props.dimensions === 2) { // If user provides 2d bin IDs, convert them to 1d indices for data packing userVs += glsl` void getBin(out int binId) { @@ -248,8 +248,9 @@ void main() { } `; const model = new Model(device, { - ...settings, - defines: {...settings.defines, NON_INSTANCED_MODEL: 1, NUM_CHANNELS: settings.channelCount}, + bufferLayout: props.bufferLayout, + modules: props.modules, + defines: {...props.defines, NON_INSTANCED_MODEL: 1, NUM_CHANNELS: props.channelCount}, isInstanced: false, vs, fs, diff --git a/modules/aggregation-layers/src/index.ts b/modules/aggregation-layers/src/index.ts index ba86939e3ef..6439b3606f1 100644 --- a/modules/aggregation-layers/src/index.ts +++ b/modules/aggregation-layers/src/index.ts @@ -45,11 +45,5 @@ export type {GridLayerProps} from './grid-layer/grid-layer'; export type {GPUGridLayerProps} from './gpu-grid-layer/gpu-grid-layer'; export type {ScreenGridLayerProps} from './screen-grid-layer/screen-grid-layer'; -export type { - WebGLAggregationProps, - WebGLAggregatorOptions -} from './aggregation-layer-v9/gpu-aggregator/webgl-aggregator'; -export type { - CPUAggregationProps, - CPUAggregatorOptions -} from './aggregation-layer-v9/cpu-aggregator/cpu-aggregator'; +export type {WebGLAggregatorProps} from './aggregation-layer-v9/gpu-aggregator/webgl-aggregator'; +export type {CPUAggregatorProps} from './aggregation-layer-v9/cpu-aggregator/cpu-aggregator'; From 59e0e2d9cdd76312e669e843c6c53a6aa1c1b076 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Fri, 21 Jun 2024 19:19:02 -0700 Subject: [PATCH 9/9] more tests --- .../cpu-aggregator/aggregate.ts | 9 +- .../cpu-aggregator/cpu-aggregator.ts | 7 +- .../cpu-aggregator/sort-bins.ts | 11 +- .../cpu-aggregator.spec.ts | 81 ++++++++++- .../cpu-aggregator/vertex-accessor.spec.ts | 130 ++++++++++++++++++ test/modules/aggregation-layers/index.ts | 3 +- 6 files changed, 218 insertions(+), 23 deletions(-) rename test/modules/aggregation-layers/aggregation-layer-v9/{ => cpu-aggregator}/cpu-aggregator.spec.ts (66%) create mode 100644 test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator/vertex-accessor.spec.ts diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts index ab5a3d3f948..2b0ee1f0b6c 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/aggregate.ts @@ -59,8 +59,7 @@ const AGGREGATION_FUNC: Record = { export function aggregate({ bins, getValue, - operation, - target + operation }: { /** Data points sorted by bins */ bins: Bin[]; @@ -68,15 +67,11 @@ export function aggregate({ getValue: (index: number) => number; /** Method used to reduce a list of values to one number */ operation: AggregationOperation; - /** Optional typed array to pack values into */ - target?: Float32Array; }): { value: Float32Array; domain: [min: number, max: number]; } { - if (!target || target.length < bins.length) { - target = new Float32Array(bins.length); - } + const target = new Float32Array(bins.length); let min = Infinity; let max = -Infinity; diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts index c367af3ad89..3b7f882db15 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.ts @@ -118,8 +118,7 @@ export class CPUAggregator implements Aggregator { }); this.binIds = packBinIds({ bins: this.bins, - dimensions: this.dimensions, - target: this.binIds + dimensions: this.dimensions }); } for (let channel = 0; channel < this.channelCount; channel++) { @@ -131,11 +130,11 @@ export class CPUAggregator implements Aggregator { this.props.attributes, undefined ), - operation: this.props.operations[channel], - target: this.results[channel]?.value + operation: this.props.operations[channel] }); } } + this.needsUpdate = false; } preDraw() {} diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort-bins.ts b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort-bins.ts index ead5222a605..a099f1d7790 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort-bins.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/cpu-aggregator/sort-bins.ts @@ -33,19 +33,14 @@ export function sortBins({ /** Pack bin ids into a typed array */ export function packBinIds({ bins, - dimensions, - target + dimensions }: { bins: Bin[]; /** Size of bin IDs */ dimensions: number; - /** Optional typed array to pack values into */ - target: Float32Array | null; }): Float32Array { - const targetLength = bins.length * dimensions; - if (!target || target.length < targetLength) { - target = new Float32Array(targetLength); - } + const target = new Float32Array(bins.length * dimensions); + for (let i = 0; i < bins.length; i++) { const {id} = bins[i]; if (Array.isArray(id)) { diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.spec.ts similarity index 66% rename from test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts rename to test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.spec.ts index 48d883c4675..416cf427119 100644 --- a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator.spec.ts +++ b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator/cpu-aggregator.spec.ts @@ -1,10 +1,10 @@ import test from 'tape-promise/tape'; -import {Attribute} from '@deck.gl/core'; +import {Attribute, BinaryAttribute, _deepEqual} from '@deck.gl/core'; import {CPUAggregator} from '@deck.gl/aggregation-layers'; import {device} from '@deck.gl/test-utils'; -import {IncomeSurvey} from './data-sample'; -import {binaryAttributeToArray} from './test-utils'; +import {IncomeSurvey} from '../data-sample'; +import {binaryAttributeToArray} from '../test-utils'; test('CPUAggregator#1D', t => { // An aggregator that calculates: @@ -177,3 +177,78 @@ test('CPUAggregator#2D', t => { t.end(); }); + +test('CPUAggregator#setNeedsUpdate', t => { + const aggregator = new CPUAggregator({ + dimensions: 1, + getBin: { + sources: ['age'], + getValue: ({age}, index, {ageGroupSize}) => [Math.floor(age / ageGroupSize)] + }, + getValue: [ + {sources: ['income'], getValue: ({income}) => income}, + {sources: ['education'], getValue: ({education}) => education} + ] + }); + + const attributes = { + age: new Attribute(device, {id: 'age', size: 1, type: 'float32', accessor: 'getAge'}), + income: new Attribute(device, {id: 'income', size: 1, type: 'float32', accessor: 'getIncome'}), + education: new Attribute(device, { + id: 'education', + size: 1, + type: 'float32', + accessor: 'getEducation' + }) + }; + attributes.age.setData({value: new Float32Array(IncomeSurvey.map(d => d.age))}); + attributes.income.setData({value: new Float32Array(IncomeSurvey.map(d => d.income))}); + attributes.education.setData({value: new Float32Array(IncomeSurvey.map(d => d.education))}); + + aggregator.setProps({ + pointCount: IncomeSurvey.length, + attributes, + operations: ['MIN', 'MAX'], + binOptions: {ageGroupSize: 5} + }); + + aggregator.update(); + + let binIds = aggregator.getBins(); + let result0 = aggregator.getResult(0); + let result1 = aggregator.getResult(1); + + t.ok(binIds, 'calculated bin IDs'); + t.ok(result0, 'calculated channel 0'); + t.ok(result1, 'calculated channel 1'); + + aggregator.update(); + t.ok(binaryAttributeEqual(aggregator.getBins(), binIds), 'did not update bins'); + t.ok(binaryAttributeEqual(aggregator.getResult(0), result0), 'did not update channel 0'); + t.ok(binaryAttributeEqual(aggregator.getResult(1), result1), 'did not update channel 1'); + + aggregator.setNeedsUpdate(1); + aggregator.update(); + t.ok(binaryAttributeEqual(aggregator.getBins(), binIds), 'did not update bins'); + t.ok(binaryAttributeEqual(aggregator.getResult(0), result0), 'did not update channel 0'); + t.notOk(binaryAttributeEqual(aggregator.getResult(1), result1), 'updated channel 1'); + + aggregator.setNeedsUpdate(); + aggregator.update(); + t.notOk(binaryAttributeEqual(aggregator.getBins(), binIds), 'updated bins'); + t.notOk(binaryAttributeEqual(aggregator.getResult(0), result0), 'updated channel 0'); + t.notOk(binaryAttributeEqual(aggregator.getResult(1), result1), 'updated channel 1'); + + attributes.age.delete(); + attributes.income.delete(); + attributes.education.delete(); + + t.end(); +}); + +function binaryAttributeEqual( + attr0: BinaryAttribute | null, + attr1: BinaryAttribute | null +): boolean { + return _deepEqual(attr0, attr1, 1); +} diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator/vertex-accessor.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator/vertex-accessor.spec.ts new file mode 100644 index 00000000000..864d5868e29 --- /dev/null +++ b/test/modules/aggregation-layers/aggregation-layer-v9/cpu-aggregator/vertex-accessor.spec.ts @@ -0,0 +1,130 @@ +import test from 'tape-promise/tape'; +import {Attribute} from '@deck.gl/core'; +import { + VertexAccessor, + evaluateVertexAccessor +} from '@deck.gl/aggregation-layers/aggregation-layer-v9/cpu-aggregator/vertex-accessor'; +import {device} from '@deck.gl/test-utils'; + +test('evaluateVertexAccessor#sources', t => { + const attributes = { + size: new Attribute(device, { + id: 'size', + size: 1, + accessor: 'getSize' + }), + position: new Attribute(device, { + id: 'position', + size: 3, + type: 'float64', + accessor: 'getPosition' + }) + }; + + attributes.size.setData({value: new Float32Array([1])}); + attributes.position.setData({value: new Float32Array(3)}); + + let getter = evaluateVertexAccessor( + { + sources: ['size'], + getValue: data => { + t.ok(data.size, 'size is present in data'); + t.notOk(data.position, 'position is not present in data'); + } + }, + attributes, + {} + ); + getter(0); + + getter = evaluateVertexAccessor( + { + sources: ['size', 'position'], + getValue: data => { + t.ok(data.size, 'size is present in data'); + t.ok(data.position, 'position is present in data'); + } + }, + attributes, + {} + ); + getter(0); + + t.throws( + () => + evaluateVertexAccessor( + { + sources: ['count'], + getValue: data => 0 + }, + attributes, + {} + ), + 'should throw on missing attribute' + ); + + attributes.size.delete(); + attributes.position.delete(); + t.end(); +}); + +test('evaluateVertexAccessor#size=1', t => { + const attributes = { + size: new Attribute(device, { + id: 'size', + size: 1, + accessor: 'getSize' + }) + }; + const accessor: VertexAccessor = { + sources: ['size'], + getValue: ({size}) => size + }; + + attributes.size.setData({value: new Float32Array([6, 7, 8, 9])}); + let getter = evaluateVertexAccessor(accessor, attributes, {}); + t.is(getter(1), 7, 'Basic attribute'); + + attributes.size.setData({value: new Float32Array([6, 7, 8, 9]), stride: 8, offset: 4}); + getter = evaluateVertexAccessor(accessor, attributes, {}); + t.is(getter(1), 9, 'With stride and offset'); + + attributes.size.setData({value: new Float32Array([6]), constant: true}); + getter = evaluateVertexAccessor(accessor, attributes, {}); + t.is(getter(1), 6, 'From constant'); + + attributes.size.delete(); + t.end(); +}); + +test('evaluateVertexAccessor#size=3', t => { + const attributes = { + position: new Attribute(device, { + id: 'position', + size: 3, + type: 'float64', + accessor: 'getPosition' + }) + }; + const accessor: VertexAccessor = { + sources: ['position'], + getValue: ({position}) => position + }; + + // prettier-ignore + attributes.position.setData({value: new Float64Array([0, 0, 0.5, 1, 0, 0.75, 1, 1, 0.25, 0, 1, 0.45])}); + let getter = evaluateVertexAccessor(accessor, attributes, {}); + t.deepEqual(getter(1), [1, 0, 0.75], 'Basic attribute'); + + // prettier-ignore + attributes.position.setData({value: new Float64Array([0, 0, 0.5, 1, 0, 0.75, 1, 1, 0.25, 0, 1, 0.45]), stride: 48, offset: 8}); + getter = evaluateVertexAccessor(accessor, attributes, {}); + t.deepEqual(getter(1), [1, 0.25, 0], 'With stride and offset'); + + attributes.position.setData({value: new Float32Array([0, 1, 0.5]), constant: true}); + getter = evaluateVertexAccessor(accessor, attributes, {}); + t.deepEqual(getter(1), [0, 1, 0.5], 'With stride and offset'); + + attributes.position.delete(); + t.end(); +}); diff --git a/test/modules/aggregation-layers/index.ts b/test/modules/aggregation-layers/index.ts index 33d40699aaf..45ab137f8b3 100644 --- a/test/modules/aggregation-layers/index.ts +++ b/test/modules/aggregation-layers/index.ts @@ -42,4 +42,5 @@ import './utils/color-utils.spec'; import './utils/scale-utils.spec'; import './aggregation-layer-v9/webgl-aggregator.spec'; -import './aggregation-layer-v9/cpu-aggregator.spec'; +import './aggregation-layer-v9/cpu-aggregator/cpu-aggregator.spec'; +import './aggregation-layer-v9/cpu-aggregator/vertex-accessor.spec';