usearch 0.18.8 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +1,23 @@
1
+ from __future__ import annotations
2
+
1
3
  # The purpose of this file is to provide Pythonic wrapper on top
2
4
  # the native precompiled CPython module. It improves compatibility
3
5
  # Python tooling, linters, and static analyzers. It also embeds JIT
4
6
  # into the primary `Index` class, connecting USearch with Numba.
5
7
  import os
6
- from math import sqrt
7
- from typing import Optional, Callable, Union, NamedTuple, List, Iterable
8
+ from typing import Optional, Union, NamedTuple, List, Iterable
8
9
 
9
10
  import numpy as np
11
+ from tqdm import tqdm
10
12
 
11
- from usearch.compiled import Index as _CompiledIndex
13
+ from usearch.compiled import Index as _CompiledIndex, IndexMetadata
12
14
  from usearch.compiled import SparseIndex as _CompiledSetsIndex
13
15
 
14
- from usearch.compiled import MetricKind, ScalarKind
16
+ from usearch.compiled import MetricKind, ScalarKind, MetricSignature
15
17
  from usearch.compiled import (
16
18
  DEFAULT_CONNECTIVITY,
17
19
  DEFAULT_EXPANSION_ADD,
18
20
  DEFAULT_EXPANSION_SEARCH,
19
-
20
21
  USES_OPENMP,
21
22
  USES_SIMSIMD,
22
23
  USES_NATIVE_F16,
@@ -33,32 +34,71 @@ SparseIndex = _CompiledSetsIndex
33
34
  Label = np.uint32
34
35
 
35
36
 
36
- def _normalize_dtype(dtype) -> ScalarKind:
37
- if dtype is None:
38
- return ScalarKind.F32
37
+ def _normalize_dtype(dtype, metric: MetricKind = MetricKind.IP) -> ScalarKind:
38
+ if dtype is None or dtype == "":
39
+ return ScalarKind.B1 if metric in MetricKindBitwise else ScalarKind.F32
40
+
39
41
  if isinstance(dtype, ScalarKind):
40
42
  return dtype
41
43
 
42
44
  if isinstance(dtype, str):
45
+ dtype = dtype.lower()
46
+
47
+ _normalize = {
48
+ "f64": ScalarKind.F64,
49
+ "f32": ScalarKind.F32,
50
+ "f16": ScalarKind.F16,
51
+ "f8": ScalarKind.F8,
52
+ "b1": ScalarKind.B1,
53
+ "b1x8": ScalarKind.B1,
54
+ np.float64: ScalarKind.F64,
55
+ np.float32: ScalarKind.F32,
56
+ np.float16: ScalarKind.F16,
57
+ np.int8: ScalarKind.F8,
58
+ }
59
+ return _normalize[dtype]
60
+
61
+
62
+ def _to_numpy_compatible_dtype(dtype: ScalarKind) -> ScalarKind:
63
+ _normalize = {
64
+ ScalarKind.F64: ScalarKind.F64,
65
+ ScalarKind.F32: ScalarKind.F32,
66
+ ScalarKind.F16: ScalarKind.F16,
67
+ ScalarKind.F8: ScalarKind.F16,
68
+ ScalarKind.B1: ScalarKind.B1,
69
+ }
70
+ return _normalize[dtype]
71
+
72
+
73
+ def _to_numpy_dtype(dtype: ScalarKind):
74
+ _normalize = {
75
+ ScalarKind.F64: np.float64,
76
+ ScalarKind.F32: np.float32,
77
+ ScalarKind.F16: np.float16,
78
+ ScalarKind.F8: np.float16,
79
+ ScalarKind.B1: np.uint8,
80
+ }
81
+ return _normalize[dtype]
82
+
83
+
84
+ def _normalize_metric(metric):
85
+ if metric is None:
86
+ return MetricKind.IP
87
+
88
+ if isinstance(metric, str):
43
89
  _normalize = {
44
- 'f64': ScalarKind.F64,
45
- 'f32': ScalarKind.F32,
46
- 'f16': ScalarKind.F16,
47
- 'f8': ScalarKind.F8,
48
- 'b1': ScalarKind.B1,
49
- }
50
- return _normalize[dtype.lower()]
51
-
52
- if not isinstance(dtype, ScalarKind):
53
- _normalize = {
54
- np.float64: ScalarKind.F64,
55
- np.float32: ScalarKind.F32,
56
- np.float16: ScalarKind.F16,
57
- np.int8: ScalarKind.F8,
90
+ "cos": MetricKind.Cos,
91
+ "ip": MetricKind.IP,
92
+ "l2_sq": MetricKind.L2sq,
93
+ "haversine": MetricKind.Haversine,
94
+ "perason": MetricKind.Pearson,
95
+ "hamming": MetricKind.Hamming,
96
+ "tanimoto": MetricKind.Tanimoto,
97
+ "sorensen": MetricKind.Sorensen,
58
98
  }
59
- return _normalize[dtype]
99
+ return _normalize[metric.lower()]
60
100
 
61
- return dtype
101
+ return metric
62
102
 
63
103
 
64
104
  class Matches(NamedTuple):
@@ -67,7 +107,7 @@ class Matches(NamedTuple):
67
107
  counts: Union[np.ndarray, int]
68
108
 
69
109
  @property
70
- def is_batch(self) -> bool:
110
+ def is_multiple(self) -> bool:
71
111
  return isinstance(self.counts, np.ndarray)
72
112
 
73
113
  @property
@@ -79,9 +119,8 @@ class Matches(NamedTuple):
79
119
  return np.sum(self.counts)
80
120
 
81
121
  def to_list(self, row: Optional[int] = None) -> Union[List[dict], List[List[dict]]]:
82
-
83
- if not self.is_batch:
84
- assert row is None, 'Exporting a single sequence is only for batch requests'
122
+ if not self.is_multiple:
123
+ assert row is None, "Exporting a single sequence is only for batch requests"
85
124
  labels = self.labels
86
125
  distances = self.distances
87
126
 
@@ -94,17 +133,26 @@ class Matches(NamedTuple):
94
133
  distances = self.distances[row, :count]
95
134
 
96
135
  return [
97
- {'label': int(label), 'distance': float(distance)}
136
+ {"label": int(label), "distance": float(distance)}
98
137
  for label, distance in zip(labels, distances)
99
138
  ]
100
139
 
101
140
  def recall_first(self, expected: np.ndarray) -> float:
102
- best_matches = self.labels if not self.is_batch else self.labels[:, 0]
141
+ best_matches = self.labels if not self.is_multiple else self.labels[:, 0]
103
142
  return np.sum(best_matches == expected) / len(expected)
104
143
 
105
144
  def __repr__(self) -> str:
106
- return f'usearch.Matches({self.total_matches})' if self.is_batch else \
107
- f'usearch.Matches({self.total_matches} across {self.batch_size} queries)'
145
+ return (
146
+ f"usearch.Matches({self.total_matches})"
147
+ if self.is_multiple
148
+ else f"usearch.Matches({self.total_matches} across {self.batch_size} queries)"
149
+ )
150
+
151
+
152
+ class CompiledMetric(NamedTuple):
153
+ pointer: int
154
+ kind: MetricKind
155
+ signature: MetricSignature
108
156
 
109
157
 
110
158
  class Index:
@@ -120,15 +168,13 @@ class Index:
120
168
  def __init__(
121
169
  self,
122
170
  ndim: int,
123
- metric: Union[MetricKind, Callable, str] = MetricKind.IP,
171
+ metric: Union[str, MetricKind, CompiledMetric] = MetricKind.IP,
124
172
  dtype: Optional[str] = None,
125
173
  jit: bool = False,
126
-
127
174
  connectivity: int = DEFAULT_CONNECTIVITY,
128
175
  expansion_add: int = DEFAULT_EXPANSION_ADD,
129
176
  expansion_search: int = DEFAULT_EXPANSION_SEARCH,
130
177
  tune: bool = False,
131
-
132
178
  path: Optional[os.PathLike] = None,
133
179
  view: bool = False,
134
180
  ) -> None:
@@ -171,7 +217,7 @@ class Index:
171
217
 
172
218
  :param expansion_search: Traversal depth on queries, defaults to None
173
219
  :type expansion_search: Optional[int], optional
174
- Hyper-parameter for the search depth when querying
220
+ Hyper-parameter for the search depth when querying
175
221
  nearest neighbors. The original paper calls it "ef".
176
222
  Can be changed afterwards, as the `.expansion_search`.
177
223
 
@@ -184,64 +230,45 @@ class Index:
184
230
  :type view: bool, optional
185
231
  """
186
232
 
187
- if metric in MetricKindBitwise:
188
- assert dtype is None or dtype == ScalarKind.B1
189
- dtype = ScalarKind.B1
190
- else:
191
- dtype = _normalize_dtype(dtype)
192
-
193
-
194
- if metric is None:
195
- metric = MetricKind.IP
196
- elif isinstance(metric, str):
197
- _normalize = {
198
- 'cos': MetricKind.Cos,
199
- 'ip': MetricKind.IP,
200
- 'l2_sq': MetricKind.L2sq,
201
- 'haversine': MetricKind.Haversine,
202
- 'perason': MetricKind.Pearson,
203
- 'hamming': MetricKind.Hamming,
204
- 'tanimoto': MetricKind.Tanimoto,
205
- 'sorensen': MetricKind.Sorensen,
206
- }
207
- metric = _normalize[metric.lower()]
208
-
209
- if isinstance(metric, Callable):
210
- self._metric_kind = MetricKind.Unknown
211
- self._metric_jit = metric
212
- self._metric_pointer = int(metric.address)
213
-
214
- elif isinstance(metric, MetricKind):
215
- if jit:
216
-
217
- try:
218
- from usearch.numba import jit
219
- except ImportError:
220
- raise ModuleNotFoundError(
221
- 'To use JIT install Numba with `pip install numba`.'
222
- 'Alternatively, reinstall usearch with `pip install usearch[jit]`')
223
-
224
- self._metric_kind = metric
225
- self._metric_jit = jit(
226
- ndim=ndim,
227
- metric=metric,
228
- dtype=dtype,
233
+ metric = _normalize_metric(metric)
234
+ if isinstance(metric, MetricKind) and jit:
235
+ try:
236
+ from usearch.numba import jit
237
+ except ImportError:
238
+ raise ModuleNotFoundError(
239
+ "To use JIT install Numba with `pip install numba`."
240
+ "Alternatively, reinstall with `pip install usearch[jit]`"
229
241
  )
230
- self._metric_pointer = self._metric_jit.address if \
231
- self._metric_jit else 0
232
- else:
233
- self._metric_kind = metric
234
- self._metric_jit = None
235
- self._metric_pointer = 0
242
+
243
+ metric = jit(
244
+ ndim=ndim,
245
+ metric=metric,
246
+ dtype=_normalize_dtype(dtype, metric),
247
+ )
248
+
249
+ if isinstance(metric, MetricKind):
250
+ self._metric_kind = metric
251
+ self._metric_jit = None
252
+ self._metric_pointer = 0
253
+ self._metric_signature = MetricSignature.ArrayArraySize
254
+ elif isinstance(metric, CompiledMetric):
255
+ self._metric_jit = metric
256
+ self._metric_kind = metric.kind
257
+ self._metric_pointer = metric.pointer
258
+ self._metric_signature = metric.signature
236
259
  else:
237
260
  raise ValueError(
238
- 'The `metric` must be Numba callback or a `MetricKind`')
261
+ "The `metric` must be a `CompiledMetric` or a `MetricKind`"
262
+ )
239
263
 
264
+ # Validate, that the right scalar type is defined
265
+ dtype = _normalize_dtype(dtype, self._metric_kind)
240
266
  self._compiled = _CompiledIndex(
241
267
  ndim=ndim,
268
+ dtype=dtype,
242
269
  metric=self._metric_kind,
243
270
  metric_pointer=self._metric_pointer,
244
- dtype=dtype,
271
+ metric_signature=self._metric_signature,
245
272
  connectivity=connectivity,
246
273
  expansion_add=expansion_add,
247
274
  expansion_search=expansion_search,
@@ -255,17 +282,47 @@ class Index:
255
282
  else:
256
283
  self._compiled.load(path)
257
284
 
285
+ @staticmethod
286
+ def metadata(path: os.PathLike) -> IndexMetadata:
287
+ return IndexMetadata(path)
288
+
289
+ @staticmethod
290
+ def restore(path: os.PathLike, view: bool = False) -> Index:
291
+ meta = Index.metadata(path)
292
+ bits_per_scalar = {
293
+ ScalarKind.F8: 8,
294
+ ScalarKind.F16: 16,
295
+ ScalarKind.F32: 32,
296
+ ScalarKind.F64: 64,
297
+ ScalarKind.B1: 1,
298
+ }[meta.scalar_kind]
299
+ ndim = meta.bytes_for_vectors * 8 // meta.size // bits_per_scalar
300
+ return Index(
301
+ ndim=ndim,
302
+ connectivity=meta.connectivity,
303
+ metric=meta.metric,
304
+ path=path,
305
+ view=view,
306
+ )
307
+
258
308
  def add(
259
- self, labels, vectors, *,
260
- copy: bool = True, threads: int = 0) -> Union[int, np.ndarray]:
309
+ self,
310
+ labels,
311
+ vectors,
312
+ *,
313
+ copy: bool = True,
314
+ threads: int = 0,
315
+ log: Union[str, bool] = False,
316
+ batch_size: int = 0,
317
+ ) -> Union[int, np.ndarray]:
261
318
  """Inserts one or move vectors into the index.
262
319
 
263
320
  For maximal performance the `labels` and `vectors`
264
321
  should conform to the Python's "buffer protocol" spec.
265
322
 
266
- To index a single entry:
323
+ To index a single entry:
267
324
  labels: int, vectors: np.ndarray.
268
- To index many entries:
325
+ To index many entries:
269
326
  labels: np.ndarray, vectors: np.ndarray.
270
327
 
271
328
  When working with extremely large indexes, you may want to
@@ -284,30 +341,70 @@ class Index:
284
341
  :return: Inserted label or labels
285
342
  :type: Union[int, np.ndarray]
286
343
  """
287
- assert isinstance(vectors, np.ndarray), 'Expects a NumPy array'
288
- assert vectors.ndim == 1 or vectors.ndim == 2, 'Expects a matrix or vector'
289
- is_batch = vectors.ndim == 2
344
+ assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
345
+ assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
346
+ count_vectors = vectors.shape[0] if vectors.ndim == 2 else 1
347
+ is_multiple = count_vectors > 1
348
+ if not is_multiple:
349
+ vectors = vectors.flatten()
350
+
351
+ # Validate of generate teh labels
290
352
  generate_labels = labels is None
291
-
292
- # If no `labels` were provided, generate some
293
353
  if generate_labels:
294
354
  start_id = len(self._compiled)
295
- if is_batch:
296
- labels = np.arange(start_id, start_id + vectors.shape[0])
355
+ if is_multiple:
356
+ labels = np.arange(start_id, start_id + count_vectors, dtype=Label)
297
357
  else:
298
358
  labels = start_id
299
- if isinstance(labels, np.ndarray):
300
- labels = labels.astype(Label)
301
- elif isinstance(labels, Iterable):
302
- labels = np.array(labels, dtype=Label)
359
+ else:
360
+ if isinstance(labels, np.ndarray):
361
+ if not is_multiple:
362
+ labels = int(labels[0])
363
+ else:
364
+ labels = labels.astype(Label)
365
+ count_labels = len(labels) if isinstance(labels, Iterable) else 1
366
+ assert count_labels == count_vectors
367
+
368
+ # Split into batches and log progress, if needed
369
+ if batch_size:
370
+ labels = [
371
+ labels[start_row : start_row + batch_size]
372
+ for start_row in range(0, count_vectors, batch_size)
373
+ ]
374
+ vectors = [
375
+ vectors[start_row : start_row + batch_size, :]
376
+ for start_row in range(0, count_vectors, batch_size)
377
+ ]
378
+ tasks = zip(labels, vectors)
379
+ name = log if isinstance(log, str) else "Add"
380
+ pbar = tqdm(
381
+ tasks,
382
+ desc=name,
383
+ total=count_vectors,
384
+ unit="Vector",
385
+ disable=log is False,
386
+ )
387
+ for labels, vectors in tasks:
388
+ self._compiled.add(labels, vectors, copy=copy, threads=threads)
389
+ pbar.update(len(labels))
390
+
391
+ pbar.close()
303
392
 
304
- self._compiled.add(labels, vectors, copy=copy, threads=threads)
393
+ else:
394
+ self._compiled.add(labels, vectors, copy=copy, threads=threads)
305
395
 
306
396
  return labels
307
397
 
308
398
  def search(
309
- self, vectors, k: int = 10, *,
310
- threads: int = 0, exact: bool = False) -> Matches:
399
+ self,
400
+ vectors,
401
+ k: int = 10,
402
+ *,
403
+ threads: int = 0,
404
+ exact: bool = False,
405
+ log: Union[str, bool] = False,
406
+ batch_size: int = 0,
407
+ ) -> Matches:
311
408
  """Performs approximate nearest neighbors search for one or more queries.
312
409
 
313
410
  :param vectors: Query vector or vectors.
@@ -323,26 +420,28 @@ class Index:
323
420
  :rtype: Matches
324
421
  """
325
422
  tuple_ = self._compiled.search(
326
- vectors, k,
327
- exact=exact, threads=threads,
423
+ vectors,
424
+ k,
425
+ exact=exact,
426
+ threads=threads,
328
427
  )
329
428
  return Matches(*tuple_)
330
429
 
331
430
  @property
332
431
  def specs(self) -> dict:
333
432
  return {
334
- 'Class': 'usearch.Index',
335
- 'Connectivity': self.connectivity,
336
- 'Size': self.size,
337
- 'Dimensions': self.ndim,
338
- 'Expansion@Add': self.expansion_add,
339
- 'Expansion@Search': self.expansion_search,
340
- 'OpenMP': USES_OPENMP,
341
- 'SimSIMD': USES_SIMSIMD,
342
- 'NativeF16': USES_NATIVE_F16,
343
- 'JIT': self.jit,
344
- 'DType': self.dtype,
345
- 'Path': self.path,
433
+ "Class": "usearch.Index",
434
+ "Connectivity": self.connectivity,
435
+ "Size": self.size,
436
+ "Dimensions": self.ndim,
437
+ "Expansion@Add": self.expansion_add,
438
+ "Expansion@Search": self.expansion_search,
439
+ "OpenMP": USES_OPENMP,
440
+ "SimSIMD": USES_SIMSIMD,
441
+ "NativeF16": USES_NATIVE_F16,
442
+ "JIT": self.jit,
443
+ "DType": self.dtype,
444
+ "Path": self.path,
346
445
  }
347
446
 
348
447
  def __len__(self) -> int:
@@ -396,33 +495,46 @@ class Index:
396
495
  def expansion_search(self, v: int):
397
496
  self._compiled.expansion_search = v
398
497
 
399
- def save(self, path: os.PathLike):
498
+ def save(self, path: Optional[os.PathLike] = None):
499
+ path = path if path else self.path
500
+ if path is None:
501
+ raise Exception("Define `path` argument")
400
502
  self._compiled.save(path)
401
503
 
402
- def load(self, path: os.PathLike):
504
+ def load(self, path: Optional[os.PathLike] = None):
505
+ path = path if path else self.path
506
+ if path is None:
507
+ raise Exception("Define `path` argument")
403
508
  self._compiled.load(path)
404
509
 
405
- def view(self, path: os.PathLike):
510
+ def view(self, path: Optional[os.PathLike] = None):
511
+ path = path if path else self.path
512
+ if path is None:
513
+ raise Exception("Define `path` argument")
406
514
  self._compiled.view(path)
407
515
 
408
516
  def clear(self):
409
517
  self._compiled.clear()
410
518
 
519
+ def close(self):
520
+ self._compiled.close()
521
+
411
522
  @property
412
523
  def labels(self) -> np.ndarray:
413
524
  return self._compiled.labels
414
525
 
415
526
  def get_vectors(
416
- self,
417
- labels: np.ndarray,
418
- dtype: ScalarKind = ScalarKind.F32) -> np.ndarray:
419
-
420
- dtype = _normalize_dtype(dtype)
421
- return np.vstack([self._compiled.__getitem__(l, dtype) for l in labels])
527
+ self, labels: np.ndarray, dtype: ScalarKind = ScalarKind.F32
528
+ ) -> np.ndarray:
529
+ dtype = _normalize_dtype(dtype, self._metric_kind)
530
+ get_dtype = _to_numpy_compatible_dtype(dtype)
531
+ vectors = np.vstack([self._compiled.__getitem__(l, get_dtype) for l in labels])
532
+ view_dtype = _to_numpy_dtype(dtype)
533
+ return vectors.view(view_dtype)
422
534
 
423
535
  @property
424
536
  def vectors(self) -> np.ndarray:
425
- return self.get_vectors(self.labels)
537
+ return self.get_vectors(self.labels, self.dtype)
426
538
 
427
539
  def __delitem__(self, label: int):
428
540
  raise NotImplementedError()
@@ -431,4 +543,8 @@ class Index:
431
543
  return self._compiled.__contains__(label)
432
544
 
433
545
  def __getitem__(self, label: int) -> np.ndarray:
434
- return self._compiled.__getitem__(label)
546
+ dtype = self.dtype
547
+ get_dtype = _to_numpy_compatible_dtype(dtype)
548
+ vector = self._compiled.__getitem__(label, get_dtype)
549
+ view_dtype = _to_numpy_dtype(dtype)
550
+ return None if vector is None else vector.view(view_dtype)
@@ -4,9 +4,8 @@ import numpy as np
4
4
 
5
5
 
6
6
  def load_matrix(
7
- filename: str,
8
- start_row: int = 0, count_rows: int = None,
9
- view: bool = False) -> np.ndarray:
7
+ filename: str, start_row: int = 0, count_rows: int = None, view: bool = False
8
+ ) -> np.ndarray:
10
9
  """Read *.ibin, *.bbib, *.hbin, *.fbin, *.dbin files with matrices.
11
10
 
12
11
  :param filename: path to the matrix file
@@ -19,36 +18,37 @@ def load_matrix(
19
18
  """
20
19
  dtype = np.float32
21
20
  scalar_size = 4
22
- if filename.endswith('.fbin'):
21
+ if filename.endswith(".fbin"):
23
22
  dtype = np.float32
24
23
  scalar_size = 4
25
- elif filename.endswith('.dbin'):
24
+ elif filename.endswith(".dbin"):
26
25
  dtype = np.float64
27
26
  scalar_size = 8
28
- elif filename.endswith('.hbin'):
27
+ elif filename.endswith(".hbin"):
29
28
  dtype = np.float16
30
29
  scalar_size = 2
31
- elif filename.endswith('.ibin'):
30
+ elif filename.endswith(".ibin"):
32
31
  dtype = np.int32
33
32
  scalar_size = 4
34
- elif filename.endswith('.bbin'):
35
- dtype = np.int8
33
+ elif filename.endswith(".bbin"):
34
+ dtype = np.uint8
36
35
  scalar_size = 1
37
36
  else:
38
- raise Exception('Unknown file type')
37
+ raise Exception("Unknown file type")
39
38
 
40
- with open(filename, 'rb') as f:
39
+ with open(filename, "rb") as f:
41
40
  rows, cols = np.fromfile(f, count=2, dtype=np.int32)
42
41
  rows = (rows - start_row) if count_rows is None else count_rows
43
42
  row_offset = start_row * scalar_size * cols
44
43
 
45
44
  if view:
46
45
  return np.memmap(
47
- f, dtype=dtype, mode='r', offset=8+row_offset, shape=(rows, cols))
46
+ f, dtype=dtype, mode="r", offset=8 + row_offset, shape=(rows, cols)
47
+ )
48
48
  else:
49
49
  return np.fromfile(
50
- f, count=rows * cols,
51
- dtype=dtype, offset=row_offset).reshape(rows, cols)
50
+ f, count=rows * cols, dtype=dtype, offset=row_offset
51
+ ).reshape(rows, cols)
52
52
 
53
53
 
54
54
  def save_matrix(vectors: np.ndarray, filename: str):
@@ -60,22 +60,22 @@ def save_matrix(vectors: np.ndarray, filename: str):
60
60
  :type filename: str
61
61
  """
62
62
  dtype = np.float32
63
- if filename.endswith('.fbin'):
63
+ if filename.endswith(".fbin"):
64
64
  dtype = np.float32
65
- elif filename.endswith('.dbin'):
65
+ elif filename.endswith(".dbin"):
66
66
  dtype = np.float64
67
- elif filename.endswith('.hbin'):
67
+ elif filename.endswith(".hbin"):
68
68
  dtype = np.float16
69
- elif filename.endswith('.ibin'):
69
+ elif filename.endswith(".ibin"):
70
70
  dtype = np.int32
71
- elif filename.endswith('.bbin'):
72
- dtype = np.int8
71
+ elif filename.endswith(".bbin"):
72
+ dtype = np.uint8
73
73
  else:
74
- raise Exception('Unknown file type')
74
+ raise Exception("Unknown file type")
75
75
 
76
- assert len(vectors.shape) == 2, 'Input array must have 2 dimensions'
77
- with open(filename, 'wb') as f:
76
+ assert len(vectors.shape) == 2, "Input array must have 2 dimensions"
77
+ with open(filename, "wb") as f:
78
78
  count, dim = vectors.shape
79
- f.write(struct.pack('<i', count))
80
- f.write(struct.pack('<i', dim))
79
+ f.write(struct.pack("<i", count))
80
+ f.write(struct.pack("<i", dim))
81
81
  vectors.astype(dtype).flatten().tofile(f)
@@ -7,7 +7,6 @@ from index import Index, Matches, Label
7
7
 
8
8
 
9
9
  class bidict(dict):
10
-
11
10
  def __init__(self, *args, **kwargs):
12
11
  super(bidict, self).__init__(*args, **kwargs)
13
12
  self.inverse = {}
@@ -37,10 +36,10 @@ def index_in_array(array: np.ndarray, item: Label) -> int:
37
36
 
38
37
 
39
38
  def semantic_join(a: Index, b: Index, resolution: int = 10) -> bidict:
40
- """Performs a Semantic Join, matching one entry from `a` with
39
+ """Performs a Semantic Join, matching one entry from `a` with
41
40
  one entry from `b`, converging towards a Stable Marriage.
42
41
  Assuming the collections can be different in size, classical solution
43
- doesn't provide
42
+ doesn't provide
44
43
 
45
44
  :param resolution: Approximate matches per member to consider, defaults to 10
46
45
  :type resolution: int, optional
@@ -65,7 +64,6 @@ def semantic_join(a: Index, b: Index, resolution: int = 10) -> bidict:
65
64
  idle_cycles: int = 0
66
65
  hopeless_men_count = len(men) - len(women)
67
66
  while len(free_men) > hopeless_men_count:
68
-
69
67
  # In the worst case scenario we may need to match more candidates for every
70
68
  # remaining man. This, however, may drastically increase the runtime.
71
69
  if len(free_men) == idle_cycles: