TripoSplat-WebGPU / tests /triposplatAlgorithms.test.ts
Yosun's picture
Deploy TripoSplat WebGPU demo
31c7d49 verified
Raw
History Blame Contribute Delete
13 kB
import assert from 'node:assert/strict'
import test from 'node:test'
import {
buildTripoSplatEncoderTensors,
compositeRgbaOnBlack,
erodeAlpha,
resizeRgbaLanczos,
type RgbaImage,
} from '../src/models/triposplat/preprocess.ts'
import {
createShiftedFlowSchedule,
sampleFlow4Steps,
type FlowModelInvocation,
} from '../src/models/triposplat/flowSampler.ts'
import {
buildOctreeParentCenters,
expandOctreeFrontier,
expandOctreeLeavesToPoints,
octreeSoftmax,
sampleOctree,
systematicResample,
TRIPOSPLAT_OCTREE_LEVELS,
type OctreeFrontier,
} from '../src/models/triposplat/octree.ts'
function assertFloatArrayClose(
actual: ArrayLike<number>,
expected: ArrayLike<number>,
tolerance = 1e-6,
): void {
assert.equal(actual.length, expected.length)
for (let index = 0; index < actual.length; index += 1) {
assert.ok(
Math.abs(actual[index] - expected[index]) <= tolerance,
`index ${index}: expected ${actual[index]} to be within ${tolerance} of ${expected[index]}`,
)
}
}
test('RGBA Lanczos resize follows Pillow premultiplied-alpha behavior', () => {
const source: RgbaImage = {
width: 2,
height: 1,
data: Uint8ClampedArray.from([255, 0, 0, 0, 0, 0, 255, 255]),
}
const resized = resizeRgbaLanczos(source, 8, 1)
assert.deepEqual([...resized.data], [
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 255, 28,
0, 0, 255, 93,
0, 0, 255, 162,
0, 0, 255, 227,
0, 0, 255, 255,
0, 0, 255, 255,
])
})
test('alpha erosion and black composition match Pillow integer semantics', () => {
const image: RgbaImage = { width: 5, height: 5, data: new Uint8ClampedArray(5 * 5 * 4) }
for (let pixel = 0; pixel < 25; pixel += 1) image.data[pixel * 4 + 3] = pixel
const eroded = erodeAlpha(image, 1)
const alpha = [...eroded.data].filter((_value, index) => index % 4 === 3)
assert.deepEqual(alpha, [
0, 0, 1, 2, 3,
0, 0, 1, 2, 3,
5, 5, 6, 7, 8,
10, 10, 11, 12, 13,
15, 15, 16, 17, 18,
])
assert.equal(
compositeRgbaOnBlack({
width: 1,
height: 1,
data: Uint8ClampedArray.from([127, 0, 0, 127]),
}).data[0],
63,
)
})
test('encoder tensor builders produce NCHW RGB, DINO, and VAE domains', () => {
const tensors = buildTripoSplatEncoderTensors({
width: 1,
height: 1,
data: Uint8ClampedArray.from([0, 127, 255]),
})
assert.deepEqual(tensors.rgb.dims, [1, 3, 1, 1])
assert.equal(tensors.rgb.data[0], 0)
assert.ok(Math.abs(tensors.rgb.data[1] - 127 / 255) < 1e-7)
assert.equal(tensors.rgb.data[2], 1)
assert.ok(Math.abs(tensors.dinov3.data[0] - (0 - 0.485) / 0.229) < 1e-6)
assert.ok(Math.abs(tensors.vae.data[1] - (2 * 127) / 255 + 1) < 1e-6)
assert.deepEqual([...tensors.vae.data], [-1, tensors.vae.data[1], 1])
})
test('shifted schedule and four-step CFG Euler loop match official equations', async () => {
assert.deepEqual(
createShiftedFlowSchedule(4, 3).map(({ timestep }) => timestep).concat(0),
[1, 0.9, 0.75, 0.5, 0],
)
const calls: FlowModelInvocation<string>[] = []
const noise = { latent: Float32Array.of(10) }
const result = await sampleFlow4Steps(
(invocation) => {
calls.push(invocation)
return { latent: Float32Array.of(invocation.pass === 'conditional' ? 2 : 1) }
},
noise,
{ condition: 'image', negativeCondition: 'zeros', guidanceScale: 3, shift: 3 },
)
assert.equal(calls.length, 8)
assert.equal(calls[0].timestepTensor[0], 1000)
assert.ok(Math.abs(result.latent[0] - 6) < 1e-6)
assert.equal(noise.latent[0], 10)
})
test('systematic resampling preserves exact counts', () => {
assert.deepEqual([...systematicResample([0.1, 0.2, 0.7], 10, () => 0.5)], [1, 2, 7])
assert.deepEqual([...systematicResample([0, 0, 0, 0, 0, 0, 0, 0], 8, () => 0.5)], [
1, 1, 1, 1, 1, 1, 1, 1,
])
})
test('systematic resampling matches official underfull, zero-row, and CDF-boundary behavior', () => {
// Official sample_probs divides by row_sums.clamp_min_(1), so a positive row
// below unit mass is not scaled up. Any targets past its CDF clamp to the last child.
assert.deepEqual(
[...systematicResample([0.2, 0.2, 0, 0, 0, 0, 0, 0], 4, () => 0.5)],
[1, 1, 0, 0, 0, 0, 0, 2],
)
assert.deepEqual([...systematicResample([2, 2], 4, () => 0.5)], [2, 2])
// Negative entries clamp to zero; a resulting all-zero row is replaced by uniform mass.
assert.deepEqual([...systematicResample([-4, -1, 0], 3, () => 0.5)], [1, 1, 1])
// torch.searchsorted(..., right=False) assigns an exact CDF boundary to the left bin.
assert.deepEqual([...systematicResample([0.5, 0.5], 1, () => 0.5)], [1, 0])
assert.deepEqual([...systematicResample([0, 1], 1, () => 0)], [1, 0])
let rngCalls = 0
assert.deepEqual([...systematicResample([0.5, 0.5], 0, () => {
rngCalls += 1
return 0.5
})], [0, 0])
assert.equal(rngCalls, 0)
})
test('octree systematic offsets follow official ascending count-group RNG order', () => {
const random = [0.9, 0.1]
const result = expandOctreeFrontier(
{
coordinates: Int32Array.of(0, 0, 0, 1, 0, 0),
counts: Uint32Array.of(2, 1),
logProbabilities: Float32Array.of(0, 0),
},
{
probabilities: Float32Array.of(
0.5, 0.5, 0, 0, 0, 0, 0, 0,
0.5, 0.5, 0, 0, 0, 0, 0, 0,
),
logProbabilities: new Float32Array(16),
},
() => random.shift() ?? 0,
)
assert.deepEqual([...result.coordinates], [0, 0, 0, 1, 0, 0, 3, 0, 0])
assert.deepEqual([...result.counts], [1, 1, 1])
})
test('octree expansion preserves parent-major compaction and x-fastest child order', () => {
const frontier: OctreeFrontier = {
coordinates: Int32Array.of(0, 0, 0, 1, 0, 1),
counts: Uint32Array.of(4, 3),
logProbabilities: Float32Array.of(-0.25, -1),
}
const probabilities = Float32Array.of(
0.25, 0.25, 0, 0, 0, 0, 0, 0.5,
0, 0, 1, 0, 0, 0, 0, 0,
)
const logProbabilities = Float32Array.from(probabilities, (probability) =>
probability === 0 ? -100 : Math.log(probability),
)
const expanded = expandOctreeFrontier(
frontier,
{ probabilities, logProbabilities },
() => 0.5,
)
// Official child_offset order is [x, y, z] with x changing fastest.
assert.deepEqual([...expanded.coordinates], [
0, 0, 0,
1, 0, 0,
1, 1, 1,
2, 1, 2,
])
assert.deepEqual([...expanded.counts], [1, 1, 2, 3])
assertFloatArrayClose(expanded.logProbabilities, [
-0.25 + Math.log(0.25),
-0.25 + Math.log(0.25),
-0.25 + Math.log(0.5),
-1,
])
})
test('octree leaf expansion repeats compact leaves and jitters xyz in official order', () => {
const leaves: OctreeFrontier = {
coordinates: Int32Array.of(1, 2, 3, 0, 0, 0),
counts: Uint32Array.of(2, 1),
logProbabilities: Float32Array.of(-0.5, -1),
}
assert.deepEqual([...buildOctreeParentCenters(leaves, 4)], [
0.375, 0.625, 0.875,
0.125, 0.125, 0.125,
])
const randomValues = [0, 0.25, 0.5, 0.75, 0.125, 0.875, 0.5, 0.5, 0.5]
let randomIndex = 0
const expanded = expandOctreeLeavesToPoints(leaves, 4, () => randomValues[randomIndex++])
assert.deepEqual([...expanded.points], [
0.25, 0.5625, 0.875,
0.4375, 0.53125, 0.96875,
0.125, 0.125, 0.125,
])
assert.deepEqual([...expanded.logProbabilities], [-0.5, -0.5, -1])
assert.equal(randomIndex, 9)
})
test('octree softmax is row-local, temperature-scaled, and numerically stable', () => {
const distribution = octreeSoftmax(Float32Array.of(
1000, 999, 998, 997, 996, 995, 994, 993,
-4, -4, -4, -4, -4, -4, -4, -4,
), 2, 2)
const firstSum = distribution.probabilities.slice(0, 8).reduce((sum, value) => sum + value, 0)
const secondSum = distribution.probabilities.slice(8).reduce((sum, value) => sum + value, 0)
assert.ok(Math.abs(firstSum - 1) < 1e-6)
assert.ok(Math.abs(secondSum - 1) < 1e-6)
assert.deepEqual([...distribution.probabilities.slice(8)], new Array(8).fill(0.125))
for (let index = 0; index < distribution.probabilities.length; index += 1) {
assert.ok(Math.abs(
Math.log(distribution.probabilities[index]) - distribution.logProbabilities[index]
) < 1e-6)
}
})
test('eight-level octree forced path matches the official compaction fixture exactly', async () => {
// Recorded from OctreeProbabilityFixedlenDecoder.sample at official commit
// a78fa12d06dbf1381ca548bfac32bb68cb8c451d with level N forced to child N-1
// and both torch.rand/systematic offsets and final torch.rand_like jitter fixed at 0.5.
const expectedCenters = [
[0.5, 0.5, 0.5],
[0.25, 0.25, 0.25],
[0.375, 0.125, 0.125],
[0.3125, 0.1875, 0.0625],
[0.34375, 0.21875, 0.03125],
[0.328125, 0.203125, 0.046875],
[0.3359375, 0.1953125, 0.0546875],
[0.33203125, 0.19921875, 0.05859375],
]
const progress: Array<[number, number, number]> = []
let rngCalls = 0
const result = await sampleOctree(
({ parentCenters, parentCount, level, resolution, targetPointCount }) => {
assert.equal(parentCount, 1)
assert.equal(resolution, 2 ** level)
assert.equal(targetPointCount, 3)
assert.deepEqual([...parentCenters], expectedCenters[level - 1])
const logits = new Float32Array(8).fill(-100)
logits[level - 1] = 0
return { logits }
},
{
condition: 'fixture',
numPoints: 3,
levels: TRIPOSPLAT_OCTREE_LEVELS,
rng: () => {
rngCalls += 1
return 0.5
},
onLevel: ({ level, resolution, occupiedVoxels }) => {
progress.push([level, resolution, occupiedVoxels])
},
},
)
assert.deepEqual([...result.leaves.coordinates], [85, 51, 15])
assert.deepEqual([...result.leaves.counts], [3])
assert.deepEqual([...result.leaves.logProbabilities], [0])
assert.deepEqual([...result.points], [
0.333984375, 0.201171875, 0.060546875,
0.333984375, 0.201171875, 0.060546875,
0.333984375, 0.201171875, 0.060546875,
])
assert.deepEqual([...result.logProbabilities], [0, 0, 0])
assert.deepEqual(progress, Array.from({ length: 8 }, (_unused, index) => [
index + 1,
2 ** (index + 1),
1,
]))
// One systematic offset per level, then one xyz jitter triplet per final point.
assert.equal(rngCalls, 8 + 3 * 3)
})
test('eight-level nontrivial octree fixture matches official sampling and path log probabilities', async () => {
// Independent oracle from the same official commit: seven samples, logits
// (((parent*5 + child*3 + level*2) % 11) - 5) / 2, systematic U=0.5,
// and final xyz jitter 0.25. The official active-parent counts are 1,4,7,7,7,7,7,7.
const parentCounts: number[] = []
let rngCalls = 0
const result = await sampleOctree(
({ parentCount, level }) => {
parentCounts.push(parentCount)
const logits = new Float32Array(parentCount * 8)
for (let parent = 0; parent < parentCount; parent += 1) {
for (let child = 0; child < 8; child += 1) {
logits[parent * 8 + child] = (
((parent * 5 + child * 3 + level * 2) % 11) - 5
) / 2
}
}
return { logits }
},
{
condition: null,
numPoints: 7,
levels: 8,
// The eight frontiers consume 1+4+6*7 = 47 systematic offsets.
rng: () => rngCalls++ < 47 ? 0.5 : 0.25,
},
)
assert.deepEqual(parentCounts, [1, 4, 7, 7, 7, 7, 7, 7])
assert.equal(rngCalls, 47 + 7 * 3)
assert.deepEqual([...result.leaves.counts], new Array(7).fill(1))
assert.deepEqual([...result.leaves.coordinates], [
175, 76, 51,
21, 169, 54,
50, 133, 110,
254, 48, 205,
23, 166, 153,
10, 148, 251,
57, 194, 247,
])
assert.deepEqual([...result.points], [
0.6845703125, 0.2978515625, 0.2001953125,
0.0830078125, 0.6611328125, 0.2119140625,
0.1962890625, 0.5205078125, 0.4306640625,
0.9931640625, 0.1884765625, 0.8017578125,
0.0908203125, 0.6494140625, 0.5986328125,
0.0400390625, 0.5791015625, 0.9814453125,
0.2236328125, 0.7587890625, 0.9658203125,
])
assertFloatArrayClose(result.logProbabilities, [
-10.276244163513184,
-11.039340019226074,
-9.35226058959961,
-11.298528671264648,
-8.880032539367676,
-9.087876319885254,
-13.00637149810791,
], 2e-6)
})
test('eight-level octree loop keeps numPoints and jitters inside level-8 voxels', async () => {
const levelsSeen: number[] = []
const result = await sampleOctree(
({ parentCount, level }) => {
levelsSeen.push(level)
return { logits: new Float32Array(parentCount * 8) }
},
{
condition: null,
numPoints: 8,
levels: TRIPOSPLAT_OCTREE_LEVELS,
rng: () => 0.5,
},
)
assert.deepEqual(levelsSeen, [1, 2, 3, 4, 5, 6, 7, 8])
assert.equal(result.resolution, 256)
assert.equal(result.points.length, 8 * 3)
assert.equal(result.logProbabilities.length, 8)
assert.equal(
[...result.leaves.counts].reduce((sum, count) => sum + count, 0),
8,
)
for (const coordinate of result.points) {
assert.ok(coordinate >= 0 && coordinate < 1)
}
})