Files
faiss/faiss/python/array_conversions.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

198 lines
5.7 KiB
Python

# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
# @nolint
# not linting this file because it imports * from swigfaiss, which
# causes a ton of useless warnings.
import numpy as np
import array
import warnings
from faiss.loader import *
###########################################
# Utility to add a deprecation warning to
# classes from the SWIG interface
###########################################
def _make_deprecated_swig_class(deprecated_name, base_name):
"""
Dynamically construct deprecated classes as wrappers around renamed ones
The deprecation warning added in their __new__-method will trigger upon
construction of an instance of the class, but only once per session.
We do this here (in __init__.py) because the base classes are defined in
the SWIG interface, making it cumbersome to add the deprecation there.
Parameters
----------
deprecated_name : string
Name of the class to be deprecated; _not_ present in SWIG interface.
base_name : string
Name of the class that is replacing deprecated_name; must already be
imported into the current namespace.
Returns
-------
None
However, the deprecated class gets added to the faiss namespace
"""
base_class = globals()[base_name]
def new_meth(cls, *args, **kwargs):
msg = (
f"The class faiss.{deprecated_name} is deprecated in favour of "
f"faiss.{base_name}!"
)
warnings.warn(msg, DeprecationWarning, stacklevel=2)
instance = super(base_class, cls).__new__(cls, *args, **kwargs)
return instance
# three-argument version of "type" uses (name, tuple-of-bases,
# dict-of-attributes)
klazz = type(deprecated_name, (base_class,), {"__new__": new_meth})
# this ends up adding the class to the "faiss" namespace, in a way that it
# is available both through "import faiss" and "from faiss import *"
globals()[deprecated_name] = klazz
###########################################
# numpy array / std::vector conversions
###########################################
sizeof_long = array.array("l").itemsize
deprecated_name_map = {
# deprecated: replacement
"Float": "Float32",
"Double": "Float64",
"Char": "Int8",
"Int": "Int32",
"Long": "Int32" if sizeof_long == 4 else "Int64",
"LongLong": "Int64",
"Byte": "UInt8",
# previously misspelled variant
"Uint64": "UInt64",
}
for depr_prefix, base_prefix in deprecated_name_map.items():
_make_deprecated_swig_class(depr_prefix + "Vector", base_prefix + "Vector")
# same for the three legacy *VectorVector classes
if depr_prefix in ["Float", "Long", "Byte"]:
_make_deprecated_swig_class(
depr_prefix + "VectorVector", base_prefix + "VectorVector"
)
# mapping from vector names in swigfaiss.swig and the numpy dtype names
# TODO: once deprecated classes are removed, remove the dict and just use
# .lower() below
vector_name_map = {
"Float32": "float32",
"Float64": "float64",
"Int8": "int8",
"Int16": "int16",
"Int32": "int32",
"Int64": "int64",
"UInt8": "uint8",
"UInt16": "uint16",
"UInt32": "uint32",
"UInt64": "uint64",
**{k: v.lower() for k, v in deprecated_name_map.items()},
}
def vector_to_array(v):
"""convert a C++ vector to a numpy array"""
classname = v.__class__.__name__
if classname.startswith("AlignedTable"):
return AlignedTable_to_array(v)
if classname.startswith("MaybeOwnedVector"):
dtype = np.dtype(vector_name_map[classname[16:]])
a = np.empty(v.size(), dtype=dtype)
if v.size() > 0:
memcpy(swig_ptr(a), v.data(), a.nbytes)
return a
assert classname.endswith("Vector")
dtype = np.dtype(vector_name_map[classname[:-6]])
a = np.empty(v.size(), dtype=dtype)
if v.size() > 0:
memcpy(swig_ptr(a), v.data(), a.nbytes)
return a
def vector_float_to_array(v):
return vector_to_array(v)
def copy_array_to_vector(a, v):
"""copy a numpy array to a vector"""
(n,) = a.shape
classname = v.__class__.__name__
if classname.startswith("MaybeOwnedVector"):
assert v.is_owned, "cannot copy to an non-owned MaybeOwnedVector"
dtype = np.dtype(vector_name_map[classname[16:]])
assert (
dtype == a.dtype
), "cannot copy a %s array to a %s (should be %s)" % (
a.dtype,
classname,
dtype,
)
v.resize(n)
if n > 0:
memcpy(v.data(), swig_ptr(a), a.nbytes)
return
assert classname.endswith("Vector")
dtype = np.dtype(vector_name_map[classname[:-6]])
assert dtype == a.dtype, "cannot copy a %s array to a %s (should be %s)" % (
a.dtype,
classname,
dtype,
)
v.resize(n)
if n > 0:
memcpy(v.data(), swig_ptr(a), a.nbytes)
# same for AlignedTable
def copy_array_to_AlignedTable(a, v):
(n,) = a.shape
# TODO check class name
assert v.itemsize() == a.itemsize
v.resize(n)
if n > 0:
memcpy(v.get(), swig_ptr(a), a.nbytes)
def array_to_AlignedTable(a):
if a.dtype == "uint16":
v = AlignedTableUint16(a.size)
elif a.dtype == "uint8":
v = AlignedTableUint8(a.size)
else:
assert False
copy_array_to_AlignedTable(a, v)
return v
def AlignedTable_to_array(v):
"""convert an AlignedTable to a numpy array"""
classname = v.__class__.__name__
assert classname.startswith("AlignedTable")
dtype = classname[12:].lower()
a = np.empty(v.size(), dtype=dtype)
if a.size > 0:
memcpy(swig_ptr(a), v.data(), a.nbytes)
return a