// Catch2 integration tests for TurboQuant with VectorDatabase // Tests TurboQuant quantization within the full vector storage pipeline // Paper: arXiv:2504.19874 - TurboQuant // // NOTE: These tests focus on unit testing TurboQuant directly since // VectorDatabase doesn't expose setIndexConfig/getIndexConfig. // Full integration testing requires VectorDatabase to be extended with // TurboQuant configuration support. #include #include #include #include #include #include #include using namespace yams::vector; namespace { struct TurboQuantIntegrationFixture { TurboQuantIntegrationFixture() : rng(42) {} std::mt19937 rng; // Generate random unit vector std::vector generateUnitVector(size_t dim) { std::vector v(dim); std::normal_distribution dist(0.0f, 1.0f); float norm_sq = 0.0f; for (size_t i = 0; i < dim; ++i) { v[i] = dist(rng); norm_sq += v[i] * v[i]; } float norm = std::sqrt(norm_sq); for (size_t i = 0; i < dim; ++i) { v[i] /= norm; } return v; } // L2 distance float l2Distance(const std::vector& a, const std::vector& b) { float sum = 0.0f; for (size_t i = 0; i < a.size(); ++i) { float diff = a[i] - b[i]; sum += diff * diff; } return std::sqrt(sum); } // Cosine similarity float cosineSimilarity(const std::vector& a, const std::vector& b) { float dot = 0.0f, norm_a = 0.0f, norm_b = 0.0f; for (size_t i = 0; i < a.size(); ++i) { dot += a[i] * b[i]; norm_a += a[i] * a[i]; norm_b += b[i] * b[i]; } return dot / (std::sqrt(norm_a) * std::sqrt(norm_b) + 1e-8f); } // MSE between vectors double mse(const std::vector& a, const std::vector& b) { double sum = 0.0; for (size_t i = 0; i < a.size(); ++i) { double diff = static_cast(a[i]) - static_cast(b[i]); sum += diff * diff; } return sum / a.size(); } }; } // namespace // ============================================================================= // Test Group: TurboQuant Direct Usage with Different Dimensions // ============================================================================= TEST_CASE_METHOD(TurboQuantIntegrationFixture, "TurboQuant encode/decode dimension 128", "[turboquant][integration][dimensions][catch2]") { TurboQuantConfig config; config.dimension = 128; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(128); std::vector indices = quantizer.encode(original); std::vector reconstructed = quantizer.decode(indices); float dist = l2Distance(original, reconstructed); double mse_val = mse(original, reconstructed); INFO("128d: dist=" << dist << ", mse=" << mse_val); // L2 distance = sqrt(d * MSE), so expect ~0.57 for MSE 0.00258 REQUIRE(dist < 0.7f); REQUIRE(mse_val < 0.005); } TEST_CASE_METHOD(TurboQuantIntegrationFixture, "TurboQuant encode/decode dimension 384", "[turboquant][integration][dimensions][catch2]") { TurboQuantConfig config; config.dimension = 384; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(384); std::vector indices = quantizer.encode(original); std::vector reconstructed = quantizer.decode(indices); float dist = l2Distance(original, reconstructed); double mse_val = mse(original, reconstructed); INFO("384d: dist=" << dist << ", mse=" << mse_val); // L2 distance = sqrt(d * MSE), so expect ~0.61 for MSE 0.00097 REQUIRE(dist < 0.8f); REQUIRE(mse_val < 0.002); } TEST_CASE_METHOD(TurboQuantIntegrationFixture, "TurboQuant encode/decode dimension 768", "[turboquant][integration][dimensions][catch2]") { TurboQuantConfig config; config.dimension = 768; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(768); std::vector indices = quantizer.encode(original); std::vector reconstructed = quantizer.decode(indices); float dist = l2Distance(original, reconstructed); double mse_val = mse(original, reconstructed); INFO("768d: dist=" << dist << ", mse=" << mse_val); // L2 distance = sqrt(d * MSE), so expect ~0.64 for MSE 0.00053 REQUIRE(dist < 0.8f); REQUIRE(mse_val < 0.001); } // ============================================================================= // Test Group: Different Bit-widths // ============================================================================= TEST_CASE_METHOD(TurboQuantIntegrationFixture, "TurboQuant 2-bit compression", "[turboquant][integration][bitwidth][catch2]") { TurboQuantConfig config; config.dimension = 384; config.bits_per_channel = 2; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(384); std::vector reconstructed = quantizer.roundTrip(original); float dist = l2Distance(original, reconstructed); double mse_val = mse(original, reconstructed); INFO("2-bit: dist=" << dist << ", mse=" << mse_val); REQUIRE(dist < 0.8f); REQUIRE(mse_val < 0.003); } TEST_CASE_METHOD(TurboQuantIntegrationFixture, "TurboQuant 3-bit compression", "[turboquant][integration][bitwidth][catch2]") { TurboQuantConfig config; config.dimension = 384; config.bits_per_channel = 3; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(384); std::vector reconstructed = quantizer.roundTrip(original); float dist = l2Distance(original, reconstructed); double mse_val = mse(original, reconstructed); INFO("3-bit: dist=" << dist << ", mse=" << mse_val); REQUIRE(dist < 0.8f); REQUIRE(mse_val < 0.002); } TEST_CASE_METHOD(TurboQuantIntegrationFixture, "TurboQuant 4-bit compression", "[turboquant][integration][bitwidth][catch2]") { TurboQuantConfig config; config.dimension = 384; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(384); std::vector reconstructed = quantizer.roundTrip(original); float dist = l2Distance(original, reconstructed); double mse_val = mse(original, reconstructed); INFO("4-bit: dist=" << dist << ", mse=" << mse_val); REQUIRE(dist < 0.8f); REQUIRE(mse_val < 0.002); } // ============================================================================= // Test Group: Batch Processing // ============================================================================= TEST_CASE_METHOD(TurboQuantIntegrationFixture, "TurboQuant batch encode/decode 100 vectors", "[turboquant][integration][batch][catch2]") { TurboQuantConfig config; config.dimension = 256; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); constexpr size_t kNumVectors = 100; std::vector> originals; std::vector> all_indices; std::vector> reconstructed; originals.reserve(kNumVectors); all_indices.reserve(kNumVectors); reconstructed.reserve(kNumVectors); for (size_t i = 0; i < kNumVectors; ++i) { std::vector orig = generateUnitVector(256); originals.push_back(orig); all_indices.push_back(quantizer.encode(orig)); reconstructed.push_back(quantizer.decode(all_indices.back())); } // Check all reconstructions are reasonable double total_mse = 0.0; for (size_t i = 0; i < kNumVectors; ++i) { total_mse += mse(originals[i], reconstructed[i]); } double avg_mse = total_mse / kNumVectors; INFO("Batch MSE: " << avg_mse); REQUIRE(avg_mse < 0.02); } // ============================================================================= // Test Group: Storage Size Verification // ============================================================================= TEST_CASE_METHOD(TurboQuantIntegrationFixture, "TurboQuant storage size is correct", "[turboquant][integration][storage][catch2]") { TurboQuantConfig config; config.dimension = 1536; config.bits_per_channel = 3; config.seed = 42; TurboQuantMSE quantizer(config); // 1536 dims * 3 bits = 4608 bits = 576 bytes REQUIRE(quantizer.storageSize() == 576); // Compare to 8-bit baseline: 1536 bytes size_t baseline_size = 1536; double compression_ratio = static_cast(quantizer.storageSize()) / baseline_size; INFO("Compression ratio: " << compression_ratio); REQUIRE(compression_ratio > 0.35); REQUIRE(compression_ratio < 0.4); // ~37.5% } // ============================================================================= // Test Group: Search Quality (Cosine Similarity Preservation) // ============================================================================= TEST_CASE_METHOD(TurboQuantIntegrationFixture, "TurboQuant preserves cosine similarity", "[turboquant][integration][similarity][catch2]") { TurboQuantConfig config; config.dimension = 384; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); // Generate two vectors std::vector v1 = generateUnitVector(384); std::vector v2 = generateUnitVector(384); // Original cosine similarity float orig_sim = cosineSimilarity(v1, v2); // Quantize and dequantize std::vector q1 = quantizer.roundTrip(v1); std::vector q2 = quantizer.roundTrip(v2); // Quantized cosine similarity float quant_sim = cosineSimilarity(q1, q2); // Similarity should be preserved float sim_error = std::abs(orig_sim - quant_sim); INFO("Original similarity: " << orig_sim); INFO("Quantized similarity: " << quant_sim); INFO("Similarity error: " << sim_error); REQUIRE(sim_error < 0.1f); // Within 10% error } // ============================================================================= // Test Group: Edge Cases // ============================================================================= TEST_CASE_METHOD(TurboQuantIntegrationFixture, "TurboQuant high dimensionality 3072", "[turboquant][integration][edge][catch2]") { TurboQuantConfig config; config.dimension = 3072; config.bits_per_channel = 4; config.seed = 42; TurboQuantMSE quantizer(config); std::vector original = generateUnitVector(3072); std::vector reconstructed = quantizer.roundTrip(original); float dist = l2Distance(original, reconstructed); double mse_val = mse(original, reconstructed); INFO("3072d: dist=" << dist << ", mse=" << mse_val); // L2 distance = sqrt(d * MSE), expect ~0.64 for MSE 0.00013 REQUIRE(dist < 0.8f); REQUIRE(mse_val < 0.0003); }