Files
faiss/tests/test_standalone_codec.py
T
Alibek Zhakubayev a238933d17 Finish E501 line-length lint sweep across faiss (183 violations, 52 files) (#5444)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5444

We fix every remaining flake8 E501 (line > 80 chars) violation in
fbcode/faiss/ (excluding gpu/), closing out a lint-cleanup initiative that a
June 29, 2026 diff stack (9a05463576 -> d304e29b9a -> 7d140d86ca) explicitly
promised but left 41% incomplete. The middle diff in that stack aligned
.flake8's max-line-length to black's 80-char width and stated the remaining
277 violations after the config change were "genuine lint (E501 long
strings/URLs, F401/F841/B011, etc.), addressed in later commits in this
stack." The follow-up commit fixed only the F401/F403/F841/B011 categories
(45 issues) and never touched E501, leaving 192 violations across 53 files
live in trunk. A direct re-scan today found 201 violations across 54 files
(two files gained lines since the original count).

We fixed 183 violations across 52 files. Two of the originally-flagged
files, benchs/bench_rabitq.py and
demos/index_pq_flat_separate_codes_from_codebook.py, already carry a
file-level `# flake8: noqa` directive (the second is a Bento/Grimaldi
notebook export that also fails standalone py_compile due to embedded shell
magic) -- flake8 ignores both files entirely, so no changes were needed
there and they are excluded from this diff.

Fix approach, by category:
- The large majority of violations are prose: docstrings and comments
  rewrapped at word boundaries, preserving each file's existing indentation
  and docstring conventions. No wording was changed.
- Code lines (function calls, f-strings, assert messages, long argument
  lists) were reflowed in black-compatible style: parenthesization, comma
  breaks, or implicit adjacent string-literal concatenation. Every touched
  file is confirmed unchanged by `black --line-length 80 --diff` after the
  edit.
- A small number of genuinely unwrappable long URLs (no reasonable break
  point) were suppressed with `# noqa: E501` rather than force-wrapped,
  matching the precedent set by the original black-format diff stack.
- demos/offline_ivf/offline_ivf.py: a repeated, non-trivial ratio expression
  used in both an assert condition and its (lazily-evaluated) failure
  message was hoisted into a single local variable, eliminating the
  duplicate computation. The expression is pure and deterministic (built
  from already-computed arrays), so this is behaviorally identical to the
  original -- not a functional change.
- `arc lint -a` additionally auto-fixed a real bug it detected while
  processing these files: `raise NotImplemented` -> `raise
  NotImplementedError` in contrib/evaluation.py (the sentinel object
  NotImplemented is not an exception type; raising it produces a TypeError
  masking the intended NotImplementedError). It also added a missing
  trailing newline in benchs/fb/bench_index_binary_from_float.py and, in
  the same file, rewrote the `#!/usr/bin/env python2` shebang to
  `#!/usr/bin/env fbpython` -- that rewrite was reverted by hand, since the
  file still does `import cPickle` (a Python 2-only module), so the
  auto-fix would have made the shebang describe an interpreter the script
  cannot actually run under. Fixing the underlying py2-to-py3 migration is
  out of scope for this line-length-only sweep.
- Rebasing onto master (this diff's base commit had drifted) surfaced two
  concurrent upstream fixes to `check_ref_knn_with_draws(...)` calls in
  tests/test_ivf_flat_panorama.py and tests/test_refine_panorama.py (an
  unrelated in-flight correctness fix passing `D_panorama`/`atol` instead
  of a stale `D_regular` arg with no `atol`). The rebase took upstream's
  corrected arguments and re-applied only this diff's line-wrap on top.

We verified zero functional change with an AST-level check: every string
and f-string literal in every touched file was extracted before and after
the edit and compared with whitespace normalized, confirming no wording,
value, or expression was altered beyond the intended `# noqa: E501`
suffixes and the one documented ratio-hoisting simplification above.

Reviewed By: mnorris11

Differential Revision: D112525458

fbshipit-source-id: e4b85d73dafcaac779f3522a383a66e490fa942a
2026-07-17 10:20:42 -07:00

578 lines
18 KiB
Python

# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
"""test byte codecs"""
from __future__ import print_function
import numpy as np
import unittest
import faiss
import platform
from common_faiss_tests import get_dataset_2
from faiss.contrib.datasets import SyntheticDataset
from faiss.contrib.inspect_tools import get_additive_quantizer_codebooks
class TestEncodeDecode(unittest.TestCase):
def do_encode_twice(self, factory_key):
d = 96
nb = 1000
nq = 0
nt = 2000
xt, x, _ = get_dataset_2(d, nt, nb, nq)
assert x.size > 0
codec = faiss.index_factory(d, factory_key)
codec.train(xt)
codes = codec.sa_encode(x)
x2 = codec.sa_decode(codes)
codes2 = codec.sa_encode(x2)
if "IVF" in factory_key or "RQ" in factory_key:
# some rows are not reconstructed exactly because they
# flip into another quantization cell
nrowdiff = (codes != codes2).any(axis=1).sum()
self.assertTrue(nrowdiff < 10)
else:
self.assertTrue(np.all(codes == codes2))
x3 = codec.sa_decode(codes2)
if "IVF" in factory_key or "RQ" in factory_key:
diffs = np.abs(x2 - x3).sum(axis=1)
avg = np.abs(x2).sum(axis=1).mean()
diffs.sort()
assert diffs[-10] < avg * 1e-5
else:
self.assertTrue(np.allclose(x2, x3))
def test_SQ8(self):
self.do_encode_twice("SQ8")
def test_SQtqmse1(self):
self.do_encode_twice("SQtqmse1")
def test_SQtqmse2(self):
self.do_encode_twice("SQtqmse2")
def test_SQtqmse3(self):
self.do_encode_twice("SQtqmse3")
def test_SQtqmse4(self):
self.do_encode_twice("SQtqmse4")
def test_SQtqmse8(self):
self.do_encode_twice("SQtqmse8")
def test_IVFSQ8(self):
self.do_encode_twice("IVF256,SQ8")
def test_PCAIVFSQ8(self):
self.do_encode_twice("PCAR32,IVF256,SQ8")
def test_PQ6x8(self):
self.do_encode_twice("PQ6np")
def test_PQ6x6(self):
self.do_encode_twice("PQ6x6np")
def test_IVFPQ6x8np(self):
self.do_encode_twice("IVF512,PQ6np")
def test_LSH(self):
self.do_encode_twice("LSHrt")
def test_RQ6x8(self):
self.do_encode_twice("RQ6x8")
class TestIndexEquiv(unittest.TestCase):
def do_test(self, key1, key2):
d = 96
nb = 1000
nq = 0
nt = 2000
xt, x, _ = get_dataset_2(d, nt, nb, nq)
codec_ref = faiss.index_factory(d, key1)
codec_ref.train(xt)
code_ref = codec_ref.sa_encode(x)
x_recons_ref = codec_ref.sa_decode(code_ref)
codec_new = faiss.index_factory(d, key2)
codec_new.pq = codec_ref.pq
# replace quantizer, avoiding mem leak
oldq = codec_new.q1.quantizer
oldq.this.own()
codec_new.q1.own_fields = False
codec_new.q1.quantizer = codec_ref.quantizer
codec_new.is_trained = True
code_new = codec_new.sa_encode(x)
x_recons_new = codec_new.sa_decode(code_new)
self.assertTrue(np.all(code_new == code_ref))
self.assertTrue(np.all(x_recons_new == x_recons_ref))
codec_new_2 = faiss.deserialize_index(faiss.serialize_index(codec_new))
code_new = codec_new_2.sa_encode(x)
x_recons_new = codec_new_2.sa_decode(code_new)
self.assertTrue(np.all(code_new == code_ref))
self.assertTrue(np.all(x_recons_new == x_recons_ref))
# Verify deserialized index is serializable again
codec_new_3 = faiss.deserialize_index(
faiss.serialize_index(codec_new_2)
)
code_new_3 = codec_new_3.sa_encode(x)
self.assertTrue(np.all(code_new_3 == code_ref))
def test_IVFPQ(self):
self.do_test("IVF512,PQ6np", "Residual512,PQ6")
def test_IMI(self):
self.do_test("IMI2x5,PQ6np", "Residual2x5,PQ6")
class TestAccuracy(unittest.TestCase):
"""comparative accuracy of a few types of indexes"""
def compare_accuracy(self, lowac, highac, max_errs=(1e10, 1e10)):
d = 96
nb = 1000
nq = 0
nt = 2000
xt, x, _ = get_dataset_2(d, nt, nb, nq)
errs = []
for factory_string in lowac, highac:
codec = faiss.index_factory(d, factory_string)
print("sa codec: code size %d" % codec.sa_code_size())
codec.train(xt)
codes = codec.sa_encode(x)
x2 = codec.sa_decode(codes)
err = ((x - x2) ** 2).sum()
errs.append(err)
self.assertGreater(errs[0], errs[1])
self.assertGreater(max_errs[0], errs[0])
self.assertGreater(max_errs[1], errs[1])
# just a small IndexLattice I/O test
if "Lattice" in highac:
codec2 = faiss.deserialize_index(faiss.serialize_index(codec))
codes = codec2.sa_encode(x)
x3 = codec2.sa_decode(codes)
self.assertTrue(np.all(x2 == x3))
# Verify deserialized index is serializable again
codec3 = faiss.deserialize_index(faiss.serialize_index(codec2))
codes3 = codec3.sa_encode(x)
x4 = codec3.sa_decode(codes3)
self.assertTrue(np.all(x2 == x4))
def test_SQ(self):
self.compare_accuracy("SQ4", "SQ8")
def test_SQ2(self):
self.compare_accuracy("SQ6", "SQ8")
def test_SQ3(self):
self.compare_accuracy("SQ8", "SQfp16")
def test_SQ4(self):
self.compare_accuracy("SQ8", "SQbf16")
def test_SQtqmse(self):
self.compare_accuracy("SQtqmse4", "SQtqmse8")
def test_SQtqmse_ordering(self):
d = 96
nb = 1000
nq = 0
nt = 2000
xt, x, _ = get_dataset_2(d, nt, nb, nq)
errs = []
for factory_string in (
"SQtqmse1",
"SQtqmse2",
"SQtqmse3",
"SQtqmse4",
"SQtqmse8",
):
codec = faiss.index_factory(d, factory_string)
codec.train(xt)
codes = codec.sa_encode(x)
x2 = codec.sa_decode(codes)
errs.append(((x - x2) ** 2).sum())
for i in range(len(errs) - 1):
self.assertGreaterEqual(errs[i], errs[i + 1])
def test_PQ(self):
self.compare_accuracy("PQ6x8np", "PQ8x8np")
def test_PQ2(self):
self.compare_accuracy("PQ8x6np", "PQ8x8np")
def test_IVFvsPQ(self):
self.compare_accuracy("PQ8np", "IVF256,PQ8np")
def test_Lattice(self):
# measured low/high: 20946.244, 5277.483
self.compare_accuracy(
"ZnLattice3x10_4", "ZnLattice3x20_4", (22000, 5400)
)
def test_Lattice2(self):
# here the difference is actually tiny
# measured errs: [16403.072, 15967.735]
self.compare_accuracy(
"ZnLattice3x12_1", "ZnLattice3x12_7", (18000, 16000)
)
swig_ptr = faiss.swig_ptr
class LatticeTest(unittest.TestCase):
"""Low-level lattice tests"""
def test_repeats(self):
rs = np.random.RandomState(123)
dim = 32
for _i in range(1000):
vec = np.floor((rs.rand(dim) ** 7) * 3).astype("float32")
vecs = vec.copy()
vecs.sort()
repeats = faiss.Repeats(dim, swig_ptr(vecs))
code = repeats.encode(swig_ptr(vec))
vec2 = np.zeros(dim, dtype="float32")
repeats.decode(code, swig_ptr(vec2))
assert np.all(vec == vec2)
def test_ZnSphereCodec_encode_centroid(self):
dim = 8
r2 = 5
ref_codec = faiss.ZnSphereCodec(dim, r2)
codec = faiss.ZnSphereCodecRec(dim, r2)
assert ref_codec.nv == codec.nv
s = set()
for i in range(ref_codec.nv):
c = np.zeros(dim, dtype="float32")
ref_codec.decode(i, swig_ptr(c))
code = codec.encode_centroid(swig_ptr(c))
assert 0 <= code < codec.nv
s.add(code)
assert len(s) == codec.nv
def test_ZnSphereCodecRec(self):
dim = 16
r2 = 6
codec = faiss.ZnSphereCodecRec(dim, r2)
for i in range(codec.nv):
c = np.zeros(dim, dtype="float32")
codec.decode(i, swig_ptr(c))
code = codec.encode_centroid(swig_ptr(c))
assert code == i
def run_ZnSphereCodecAlt(self, dim, r2):
# dim = 32
# r2 = 14
codec = faiss.ZnSphereCodecAlt(dim, r2)
rs = np.random.RandomState(123)
n = 100
codes = rs.randint(codec.nv, size=n, dtype="uint64")
x = np.empty((n, dim), dtype="float32")
codec.decode_multi(n, swig_ptr(codes), swig_ptr(x))
codes2 = np.empty(n, dtype="uint64")
codec.encode_multi(n, swig_ptr(x), swig_ptr(codes2))
assert np.all(codes == codes2)
def test_ZnSphereCodecAlt32(self):
self.run_ZnSphereCodecAlt(32, 14)
def test_ZnSphereCodecAlt24(self):
self.run_ZnSphereCodecAlt(24, 14)
def test_lattice_index(self):
index = faiss.index_factory(96, "ZnLattice3x10_4")
rs = np.random.RandomState(123)
xq = rs.randn(10, 96).astype("float32")
xb = rs.randn(20, 96).astype("float32")
index.train(xb)
index.add(xb)
D, I = index.search(xq, 5)
for i in range(10):
recons = index.reconstruct_batch(I[i, :])
ref_dis = ((recons - xq[i]) ** 2).sum(1)
np.testing.assert_allclose(D[i, :], ref_dis, atol=1e-4)
class TestBitstring(unittest.TestCase):
def test_rw(self):
"""Low-level bit string tests"""
rs = np.random.RandomState(1234)
nbyte = 1000
sz = 0
bs = np.ones(nbyte, dtype="uint8")
bw = faiss.BitstringWriter(swig_ptr(bs), nbyte)
if False:
ctrl = [(7, 0x35), (13, 0x1D74)]
for nbit, x in ctrl:
bw.write(x, nbit)
else:
ctrl = []
while True:
nbit = int(1 + 62 * rs.rand() ** 4)
if sz + nbit > nbyte * 8:
break
x = int(rs.randint(1 << nbit, dtype="int64"))
bw.write(x, nbit)
ctrl.append((nbit, x))
sz += nbit
bignum = 0
sz = 0
for nbit, x in ctrl:
bignum |= x << sz
sz += nbit
for i in range(nbyte):
self.assertTrue(((bignum >> (i * 8)) & 255) == bs[i])
br = faiss.BitstringReader(swig_ptr(bs), nbyte)
for nbit, xref in ctrl:
xnew = br.read(nbit)
self.assertTrue(xnew == xref)
def test_arrays(self):
nbit = 5
M = 10
n = 20
rs = np.random.RandomState(123)
a = rs.randint(1 << nbit, size=(n, M), dtype="int32")
b = faiss.pack_bitstrings(a, nbit)
c = faiss.unpack_bitstrings(b, M, nbit)
np.testing.assert_array_equal(a, c)
def test_arrays_variable_size(self):
nbits = [10, 5, 3, 12, 6, 7, 4]
n = 20
rs = np.random.RandomState(123)
a = rs.randint(1 << 16, size=(n, len(nbits)), dtype="int32")
a &= (1 << np.array(nbits)) - 1
b = faiss.pack_bitstrings(a, nbits)
c = faiss.unpack_bitstrings(b, nbits)
np.testing.assert_array_equal(a, c)
class TestIVFTransfer(unittest.TestCase):
def test_transfer(self):
ds = SyntheticDataset(32, 2000, 200, 100)
index = faiss.index_factory(ds.d, "IVF20,SQ8")
index.train(ds.get_train())
index.add(ds.get_database())
Dref, Iref = index.search(ds.get_queries(), 10)
index.reset()
codes = index.sa_encode(ds.get_database())
index.add_sa_codes(codes)
Dnew, Inew = index.search(ds.get_queries(), 10)
np.testing.assert_array_equal(Iref, Inew)
np.testing.assert_array_equal(Dref, Dnew)
class TestIDMap(unittest.TestCase):
def test_idmap(self):
ds = SyntheticDataset(32, 2000, 200, 100)
ids = np.random.randint(10000, size=ds.nb, dtype="int64")
index = faiss.index_factory(ds.d, "IDMap2,PQ8x2")
index.train(ds.get_train())
index.add_with_ids(ds.get_database(), ids)
Dref, Iref = index.search(ds.get_queries(), 10)
index.reset()
index.train(ds.get_train())
codes = index.index.sa_encode(ds.get_database())
index.add_sa_codes(codes, ids)
Dnew, Inew = index.search(ds.get_queries(), 10)
np.testing.assert_array_equal(Iref, Inew)
np.testing.assert_array_equal(Dref, Dnew)
def _check_idmap_ivf(self, factory, with_reconstruct):
# IVF stores ids in its inverted lists, so add_sa_codes must not
# forward them to the sub-index (issue #5333). Use two independent
# indexes so the reference (add_with_ids) and fix-under-test
# (add_sa_codes) paths don't share state.
ds = SyntheticDataset(32, 2000, 200, 100)
ids = (1000 + np.arange(ds.nb)).astype('int64')
ref = faiss.index_factory(ds.d, factory)
ref.train(ds.get_train())
faiss.downcast_index(ref.index).nprobe = 4
ref.add_with_ids(ds.get_database(), ids)
Dref, Iref = ref.search(ds.get_queries(), 10)
test = faiss.index_factory(ds.d, factory)
test.train(ds.get_train())
faiss.downcast_index(test.index).nprobe = 4
codes = test.index.sa_encode(ds.get_database())
test.add_sa_codes(codes, ids)
Dnew, Inew = test.search(ds.get_queries(), 10)
np.testing.assert_array_equal(Iref, Inew)
np.testing.assert_array_equal(Dref, Dnew)
if with_reconstruct:
# reconstruct should work too (rev_map is updated) — IDMap2 only
faiss.extract_index_ivf(test).make_direct_map()
for key in ids[:5]:
recons = test.reconstruct(int(key))
np.testing.assert_array_almost_equal(
recons, ds.get_database()[int(key) - 1000])
def test_idmap_ivf(self):
self._check_idmap_ivf("IDMap2,IVF20,Flat", with_reconstruct=True)
def test_idmap_ivf_base(self):
# guards the base IndexIDMap fix (nullptr instead of xids) on its own,
# independently of the IDMap2 rev_map maintenance
self._check_idmap_ivf("IDMap,IVF20,Flat", with_reconstruct=False)
class TestRefine(unittest.TestCase):
def test_refine(self):
"""Make sure that IndexRefine can function as a standalone codec"""
ds = SyntheticDataset(32, 500, 100, 0)
index = faiss.index_factory(ds.d, "RQ2x5,Refine(ITQ,LSHt)")
index.train(ds.get_train())
index1 = index.base_index
index2 = index.refine_index
codes12 = index.sa_encode(ds.get_database())
codes1 = index1.sa_encode(ds.get_database())
codes2 = index2.sa_encode(ds.get_database())
np.testing.assert_array_equal(codes12, np.hstack((codes1, codes2)))
def test_refine_sa_decode(self):
"""Make sure IndexRefine.sa_decode correctly extracts refine codes"""
ds = SyntheticDataset(32, 500, 100, 0)
index = faiss.index_factory(ds.d, "PQ4,Refine(SQ8)")
index.train(ds.get_train())
xb = ds.get_database()
codes = index.sa_encode(xb)
x_decoded = index.sa_decode(codes)
refine_codes = index.refine_index.sa_encode(xb)
x_decoded_ref = index.refine_index.sa_decode(refine_codes)
np.testing.assert_allclose(x_decoded, x_decoded_ref)
def test_equiv_rcq_rq(self):
"""make sure that the codes generated by the standalone codec are
the same
between an
IndexRefine with ResidualQuantizer
and
IVF with ResidualCoarseQuantizer
both are the centroid id concatenated with the code.
"""
ds = SyntheticDataset(16, 400, 100, 0)
index1 = faiss.index_factory(ds.d, "RQ2x3,Refine(Flat)")
index1.train(ds.get_train())
irq = faiss.downcast_index(index1.base_index)
# because the default beam factor for RCQ is 4
irq.rq.max_beam_size = 4
index2 = faiss.index_factory(ds.d, "IVF64(RCQ2x3),Flat")
index2.train(ds.get_train())
quantizer = faiss.downcast_index(index2.quantizer)
quantizer.rq = irq.rq
index2.is_trained = True
codes1 = index1.sa_encode(ds.get_database())
codes2 = index2.sa_encode(ds.get_database())
np.testing.assert_array_equal(codes1, codes2)
@unittest.skipIf(
platform.system() == "Windows",
"Does not work on Windows after numpy 2 upgrade.",
)
def test_equiv_sh(self):
"""make sure that the IVFSpectralHash sa_encode function gives the same
result as the concatenated RQ + LSH index sa_encode"""
ds = SyntheticDataset(32, 500, 100, 0)
index1 = faiss.index_factory(ds.d, "RQ1x4,Refine(ITQ16,LSH)")
index1.train(ds.get_train())
# reproduce this in an IndexIVFSpectralHash
coarse_quantizer = faiss.IndexFlat(ds.d)
rq = faiss.downcast_index(index1.base_index).rq
centroids = get_additive_quantizer_codebooks(rq)[0]
coarse_quantizer.add(centroids)
encoder = faiss.downcast_index(index1.refine_index)
# larger than the magnitude of the vectors
# negative because otherwise the bits are flipped
period = -100000.0
index2 = faiss.IndexIVFSpectralHash(
coarse_quantizer,
ds.d,
coarse_quantizer.ntotal,
encoder.sa_code_size() * 8,
period,
)
# replace with the vt of the encoder. Binarization is performed by
# the IndexIVFSpectralHash itself
index2.replace_vt(encoder)
codes1 = index1.sa_encode(ds.get_database())
codes2 = index2.sa_encode(ds.get_database())
np.testing.assert_array_equal(codes1, codes2)