vecadvisor 0.1.0a1__py3-none-any.whl

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.
@@ -0,0 +1,440 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import math
5
+ from collections import Counter
6
+ from collections.abc import Mapping, Sequence
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .sweep import SweepReport, sweep_report_to_json
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class StrategyPointSummary:
16
+ strategy: str
17
+ recall_at_k: float
18
+ returns_k_rate: float
19
+ latency_ms_mean: float
20
+ latency_ms_p95: float
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class SweepAnalysisPoint:
25
+ target_filter_selectivity: float
26
+ target_correlation: float
27
+ observed_filter_selectivity: float
28
+ s_local_p10: float
29
+ s_local_median: float
30
+ measured_best: str
31
+ predicted_best: str | None
32
+ prediction_match: bool | None
33
+ postfilter_recall_at_k: float | None
34
+ postfilter_returns_k_rate: float | None
35
+ postfilter_viable: bool | None
36
+ strategies: tuple[StrategyPointSummary, ...]
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class WinnerRegion:
41
+ kind: str
42
+ correlation: float
43
+ strategy: str
44
+ selectivity_min: float
45
+ selectivity_max: float
46
+ point_count: int
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class WinnerCrossover:
51
+ kind: str
52
+ correlation: float
53
+ lower_selectivity: float
54
+ upper_selectivity: float
55
+ estimated_selectivity: float
56
+ from_strategy: str
57
+ to_strategy: str
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class CrossoverAnalysis:
62
+ backend: str
63
+ point_count: int
64
+ correlations: tuple[float, ...]
65
+ recall_target: float
66
+ returns_k_target: float
67
+ prediction_match_rate: float | None
68
+ measured_win_counts: Mapping[str, int]
69
+ predicted_win_counts: Mapping[str, int]
70
+ regions: tuple[WinnerRegion, ...]
71
+ measured_crossovers: tuple[WinnerCrossover, ...]
72
+ predicted_crossovers: tuple[WinnerCrossover, ...]
73
+ postfilter_failure_count: int
74
+ points: tuple[SweepAnalysisPoint, ...]
75
+ notes: tuple[str, ...] = ()
76
+
77
+
78
+ def load_sweep_payload(path: Path) -> Mapping[str, Any]:
79
+ """Load a JSON sweep report produced by benchmark-sweep or benchmark-sweep-db."""
80
+
81
+ try:
82
+ raw = json.loads(path.read_text(encoding="utf-8"))
83
+ except OSError as exc:
84
+ raise ValueError(f"could not read sweep report: {path}") from exc
85
+ except json.JSONDecodeError as exc:
86
+ raise ValueError(f"sweep report JSON is invalid: {exc}") from exc
87
+ if not isinstance(raw, Mapping):
88
+ raise ValueError("sweep report must be a JSON object")
89
+ return raw
90
+
91
+
92
+ def analyze_sweep_report(report: SweepReport) -> CrossoverAnalysis:
93
+ """Analyze an in-memory sweep report."""
94
+
95
+ return analyze_sweep_payload(sweep_report_to_json(report, calibration_source="in-memory"))
96
+
97
+
98
+ def analyze_sweep_payload(payload: Mapping[str, Any]) -> CrossoverAnalysis:
99
+ """Compute winner regions, crossovers, and prediction quality from sweep JSON."""
100
+
101
+ sweep = _mapping_value(payload, "sweep")
102
+ backend = _str_value(sweep, "backend")
103
+ recall_target = _float_value(sweep, "recall_target")
104
+ returns_k_target = _float_value(sweep, "returns_k_target")
105
+ points = tuple(
106
+ _analysis_point(point, recall_target=recall_target, returns_k_target=returns_k_target)
107
+ for point in _sequence_value(payload, "points")
108
+ )
109
+ if not points:
110
+ raise ValueError("sweep report must contain at least one point")
111
+
112
+ correlations = tuple(sorted({point.target_correlation for point in points}))
113
+ predicted_points = [point for point in points if point.predicted_best is not None]
114
+ prediction_match_rate = (
115
+ sum(1 for point in predicted_points if point.prediction_match) / len(predicted_points)
116
+ if predicted_points
117
+ else None
118
+ )
119
+ measured_win_counts = dict(Counter(point.measured_best for point in points))
120
+ predicted_winners: list[str] = []
121
+ for point in predicted_points:
122
+ assert point.predicted_best is not None
123
+ predicted_winners.append(point.predicted_best)
124
+ predicted_win_counts = dict(Counter(predicted_winners))
125
+ postfilter_failure_count = sum(1 for point in points if point.postfilter_viable is False)
126
+
127
+ measured_regions = _winner_regions(points, kind="measured")
128
+ predicted_regions = _winner_regions(points, kind="predicted")
129
+ measured_crossovers = _winner_crossovers(points, kind="measured")
130
+ predicted_crossovers = _winner_crossovers(points, kind="predicted")
131
+
132
+ return CrossoverAnalysis(
133
+ backend=backend,
134
+ point_count=len(points),
135
+ correlations=correlations,
136
+ recall_target=recall_target,
137
+ returns_k_target=returns_k_target,
138
+ prediction_match_rate=prediction_match_rate,
139
+ measured_win_counts=measured_win_counts,
140
+ predicted_win_counts=predicted_win_counts,
141
+ regions=(*measured_regions, *predicted_regions),
142
+ measured_crossovers=measured_crossovers,
143
+ predicted_crossovers=predicted_crossovers,
144
+ postfilter_failure_count=postfilter_failure_count,
145
+ points=points,
146
+ notes=(
147
+ "crossovers are estimated between adjacent sampled selectivity points",
148
+ "estimated_selectivity uses the geometric midpoint for positive selectivities",
149
+ "postfilter_failure_count uses the sweep recall and returns-k targets",
150
+ ),
151
+ )
152
+
153
+
154
+ def crossover_analysis_to_json(analysis: CrossoverAnalysis) -> dict[str, object]:
155
+ return {
156
+ "analysis": {
157
+ "backend": analysis.backend,
158
+ "point_count": analysis.point_count,
159
+ "correlations": list(analysis.correlations),
160
+ "recall_target": analysis.recall_target,
161
+ "returns_k_target": analysis.returns_k_target,
162
+ "prediction_match_rate": analysis.prediction_match_rate,
163
+ "postfilter_failure_count": analysis.postfilter_failure_count,
164
+ },
165
+ "measured_win_counts": dict(analysis.measured_win_counts),
166
+ "predicted_win_counts": dict(analysis.predicted_win_counts),
167
+ "regions": [_region_to_json(region) for region in analysis.regions],
168
+ "measured_crossovers": [
169
+ _crossover_to_json(crossover) for crossover in analysis.measured_crossovers
170
+ ],
171
+ "predicted_crossovers": [
172
+ _crossover_to_json(crossover) for crossover in analysis.predicted_crossovers
173
+ ],
174
+ "points": [_point_to_json(point) for point in analysis.points],
175
+ "notes": list(analysis.notes),
176
+ }
177
+
178
+
179
+ def write_crossover_analysis(analysis: CrossoverAnalysis, path: Path) -> None:
180
+ path.parent.mkdir(parents=True, exist_ok=True)
181
+ path.write_text(
182
+ json.dumps(crossover_analysis_to_json(analysis), indent=2) + "\n",
183
+ encoding="utf-8",
184
+ )
185
+
186
+
187
+ def _analysis_point(
188
+ raw: object,
189
+ *,
190
+ recall_target: float,
191
+ returns_k_target: float,
192
+ ) -> SweepAnalysisPoint:
193
+ point = _as_mapping(raw, "sweep point")
194
+ dataset = _mapping_value(point, "dataset")
195
+ local = _mapping_value(point, "local_selectivity")
196
+ strategies = tuple(
197
+ _strategy_summary(strategy) for strategy in _sequence_value(point, "strategies")
198
+ )
199
+ postfilter = next(
200
+ (strategy for strategy in strategies if strategy.strategy == "postfilter"),
201
+ None,
202
+ )
203
+ postfilter_viable = (
204
+ postfilter.recall_at_k >= recall_target
205
+ and postfilter.returns_k_rate >= returns_k_target
206
+ if postfilter is not None
207
+ else None
208
+ )
209
+ return SweepAnalysisPoint(
210
+ target_filter_selectivity=_float_value(point, "target_filter_selectivity"),
211
+ target_correlation=_float_value(point, "target_correlation"),
212
+ observed_filter_selectivity=_float_value(dataset, "observed_filter_selectivity"),
213
+ s_local_p10=_float_value(local, "s_local_p10"),
214
+ s_local_median=_float_value(local, "s_local_median"),
215
+ measured_best=_str_value(point, "measured_best"),
216
+ predicted_best=_optional_str_value(point, "predicted_best"),
217
+ prediction_match=_optional_bool_value(point, "prediction_match"),
218
+ postfilter_recall_at_k=None if postfilter is None else postfilter.recall_at_k,
219
+ postfilter_returns_k_rate=None if postfilter is None else postfilter.returns_k_rate,
220
+ postfilter_viable=postfilter_viable,
221
+ strategies=strategies,
222
+ )
223
+
224
+
225
+ def _strategy_summary(raw: object) -> StrategyPointSummary:
226
+ strategy = _as_mapping(raw, "strategy")
227
+ latency = _mapping_value(strategy, "latency_ms")
228
+ return StrategyPointSummary(
229
+ strategy=_str_value(strategy, "strategy"),
230
+ recall_at_k=_float_value(strategy, "recall_at_k"),
231
+ returns_k_rate=_float_value(strategy, "returns_k_rate"),
232
+ latency_ms_mean=_float_value(latency, "mean"),
233
+ latency_ms_p95=_float_value(latency, "p95"),
234
+ )
235
+
236
+
237
+ def _winner_regions(
238
+ points: Sequence[SweepAnalysisPoint],
239
+ *,
240
+ kind: str,
241
+ ) -> tuple[WinnerRegion, ...]:
242
+ regions: list[WinnerRegion] = []
243
+ for correlation in sorted({point.target_correlation for point in points}):
244
+ group = _points_for_correlation(points, correlation=correlation, kind=kind)
245
+ if not group:
246
+ continue
247
+ start = group[0]
248
+ current_strategy = _winner(start, kind=kind)
249
+ assert current_strategy is not None
250
+ current_points = [start]
251
+ for point in group[1:]:
252
+ strategy = _winner(point, kind=kind)
253
+ if strategy == current_strategy:
254
+ current_points.append(point)
255
+ continue
256
+ regions.append(_region_from_points(kind, correlation, current_strategy, current_points))
257
+ assert strategy is not None
258
+ current_strategy = strategy
259
+ current_points = [point]
260
+ regions.append(_region_from_points(kind, correlation, current_strategy, current_points))
261
+ return tuple(regions)
262
+
263
+
264
+ def _winner_crossovers(
265
+ points: Sequence[SweepAnalysisPoint],
266
+ *,
267
+ kind: str,
268
+ ) -> tuple[WinnerCrossover, ...]:
269
+ crossovers: list[WinnerCrossover] = []
270
+ for correlation in sorted({point.target_correlation for point in points}):
271
+ group = _points_for_correlation(points, correlation=correlation, kind=kind)
272
+ for lower, upper in zip(group, group[1:], strict=False):
273
+ lower_winner = _winner(lower, kind=kind)
274
+ upper_winner = _winner(upper, kind=kind)
275
+ if lower_winner is None or upper_winner is None or lower_winner == upper_winner:
276
+ continue
277
+ crossovers.append(
278
+ WinnerCrossover(
279
+ kind=kind,
280
+ correlation=correlation,
281
+ lower_selectivity=lower.target_filter_selectivity,
282
+ upper_selectivity=upper.target_filter_selectivity,
283
+ estimated_selectivity=_midpoint_selectivity(
284
+ lower.target_filter_selectivity,
285
+ upper.target_filter_selectivity,
286
+ ),
287
+ from_strategy=lower_winner,
288
+ to_strategy=upper_winner,
289
+ )
290
+ )
291
+ return tuple(crossovers)
292
+
293
+
294
+ def _points_for_correlation(
295
+ points: Sequence[SweepAnalysisPoint],
296
+ *,
297
+ correlation: float,
298
+ kind: str,
299
+ ) -> list[SweepAnalysisPoint]:
300
+ return sorted(
301
+ (
302
+ point
303
+ for point in points
304
+ if point.target_correlation == correlation and _winner(point, kind=kind) is not None
305
+ ),
306
+ key=lambda point: point.target_filter_selectivity,
307
+ )
308
+
309
+
310
+ def _winner(point: SweepAnalysisPoint, *, kind: str) -> str | None:
311
+ if kind == "measured":
312
+ return point.measured_best
313
+ if kind == "predicted":
314
+ return point.predicted_best
315
+ raise ValueError("kind must be measured or predicted")
316
+
317
+
318
+ def _region_from_points(
319
+ kind: str,
320
+ correlation: float,
321
+ strategy: str,
322
+ points: Sequence[SweepAnalysisPoint],
323
+ ) -> WinnerRegion:
324
+ selectivities = [point.target_filter_selectivity for point in points]
325
+ return WinnerRegion(
326
+ kind=kind,
327
+ correlation=correlation,
328
+ strategy=strategy,
329
+ selectivity_min=min(selectivities),
330
+ selectivity_max=max(selectivities),
331
+ point_count=len(points),
332
+ )
333
+
334
+
335
+ def _midpoint_selectivity(lower: float, upper: float) -> float:
336
+ if lower > 0.0 and upper > 0.0:
337
+ return math.sqrt(lower * upper)
338
+ return (lower + upper) / 2.0
339
+
340
+
341
+ def _point_to_json(point: SweepAnalysisPoint) -> dict[str, object]:
342
+ return {
343
+ "target_filter_selectivity": point.target_filter_selectivity,
344
+ "target_correlation": point.target_correlation,
345
+ "observed_filter_selectivity": point.observed_filter_selectivity,
346
+ "s_local_p10": point.s_local_p10,
347
+ "s_local_median": point.s_local_median,
348
+ "measured_best": point.measured_best,
349
+ "predicted_best": point.predicted_best,
350
+ "prediction_match": point.prediction_match,
351
+ "postfilter_recall_at_k": point.postfilter_recall_at_k,
352
+ "postfilter_returns_k_rate": point.postfilter_returns_k_rate,
353
+ "postfilter_viable": point.postfilter_viable,
354
+ "strategies": [_strategy_to_json(strategy) for strategy in point.strategies],
355
+ }
356
+
357
+
358
+ def _strategy_to_json(strategy: StrategyPointSummary) -> dict[str, object]:
359
+ return {
360
+ "strategy": strategy.strategy,
361
+ "recall_at_k": strategy.recall_at_k,
362
+ "returns_k_rate": strategy.returns_k_rate,
363
+ "latency_ms_mean": strategy.latency_ms_mean,
364
+ "latency_ms_p95": strategy.latency_ms_p95,
365
+ }
366
+
367
+
368
+ def _region_to_json(region: WinnerRegion) -> dict[str, object]:
369
+ return {
370
+ "kind": region.kind,
371
+ "correlation": region.correlation,
372
+ "strategy": region.strategy,
373
+ "selectivity_min": region.selectivity_min,
374
+ "selectivity_max": region.selectivity_max,
375
+ "point_count": region.point_count,
376
+ }
377
+
378
+
379
+ def _crossover_to_json(crossover: WinnerCrossover) -> dict[str, object]:
380
+ return {
381
+ "kind": crossover.kind,
382
+ "correlation": crossover.correlation,
383
+ "lower_selectivity": crossover.lower_selectivity,
384
+ "upper_selectivity": crossover.upper_selectivity,
385
+ "estimated_selectivity": crossover.estimated_selectivity,
386
+ "from_strategy": crossover.from_strategy,
387
+ "to_strategy": crossover.to_strategy,
388
+ }
389
+
390
+
391
+ def _mapping_value(payload: Mapping[str, Any], key: str) -> Mapping[str, Any]:
392
+ return _as_mapping(payload.get(key), key)
393
+
394
+
395
+ def _sequence_value(payload: Mapping[str, Any], key: str) -> Sequence[object]:
396
+ value = payload.get(key)
397
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
398
+ raise ValueError(f"{key} must be a list")
399
+ return value
400
+
401
+
402
+ def _as_mapping(value: object, name: str) -> Mapping[str, Any]:
403
+ if not isinstance(value, Mapping):
404
+ raise ValueError(f"{name} must be an object")
405
+ return value
406
+
407
+
408
+ def _str_value(payload: Mapping[str, Any], key: str) -> str:
409
+ value = payload.get(key)
410
+ if not isinstance(value, str) or not value:
411
+ raise ValueError(f"{key} must be a non-empty string")
412
+ return value
413
+
414
+
415
+ def _optional_str_value(payload: Mapping[str, Any], key: str) -> str | None:
416
+ value = payload.get(key)
417
+ if value is None:
418
+ return None
419
+ if not isinstance(value, str) or not value:
420
+ raise ValueError(f"{key} must be null or a non-empty string")
421
+ return value
422
+
423
+
424
+ def _optional_bool_value(payload: Mapping[str, Any], key: str) -> bool | None:
425
+ value = payload.get(key)
426
+ if value is None:
427
+ return None
428
+ if not isinstance(value, bool):
429
+ raise ValueError(f"{key} must be null or boolean")
430
+ return value
431
+
432
+
433
+ def _float_value(payload: Mapping[str, Any], key: str) -> float:
434
+ value = payload.get(key)
435
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
436
+ raise ValueError(f"{key} must be numeric")
437
+ number = float(value)
438
+ if not math.isfinite(number):
439
+ raise ValueError(f"{key} must be finite")
440
+ return number
@@ -0,0 +1,246 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class SyntheticDataset:
11
+ vectors: Any
12
+ filter_mask: Any
13
+ cluster_ids: Any
14
+ centers: Any
15
+ filter_probabilities: tuple[float, ...]
16
+ filter_selectivity: float
17
+ correlation: float
18
+ seed: int
19
+ dataset_id: str = "synthetic"
20
+
21
+ @property
22
+ def n_rows(self) -> int:
23
+ return int(self.vectors.shape[0])
24
+
25
+ @property
26
+ def dim(self) -> int:
27
+ return int(self.vectors.shape[1])
28
+
29
+ @property
30
+ def observed_selectivity(self) -> float:
31
+ return float(self.filter_mask.mean())
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class SyntheticQueries:
36
+ vectors: Any
37
+ cluster_ids: Any
38
+ seed: int
39
+ cluster_policy: str
40
+
41
+ @property
42
+ def n_queries(self) -> int:
43
+ return int(self.vectors.shape[0])
44
+
45
+
46
+ def generate_synthetic_dataset(
47
+ *,
48
+ n_rows: int = 10_000,
49
+ dim: int = 64,
50
+ n_clusters: int = 16,
51
+ filter_selectivity: float = 0.1,
52
+ correlation: float = 0.0,
53
+ seed: int = 0,
54
+ cluster_std: float = 0.08,
55
+ ) -> SyntheticDataset:
56
+ """Generate clustered vectors with a tunable correlated boolean filter."""
57
+
58
+ if n_rows <= 0:
59
+ raise ValueError("n_rows must be positive")
60
+ if dim <= 0:
61
+ raise ValueError("dim must be positive")
62
+ if n_clusters <= 0:
63
+ raise ValueError("n_clusters must be positive")
64
+ if not 0.0 < filter_selectivity < 1.0:
65
+ raise ValueError("filter_selectivity must be in (0, 1)")
66
+ if not -1.0 <= correlation <= 1.0:
67
+ raise ValueError("correlation must be in [-1, 1]")
68
+ if cluster_std <= 0.0:
69
+ raise ValueError("cluster_std must be positive")
70
+
71
+ np = _numpy()
72
+ rng = np.random.default_rng(seed)
73
+ centers = rng.normal(0.0, 1.0, size=(n_clusters, dim)).astype("float32")
74
+ centers /= np.maximum(np.linalg.norm(centers, axis=1, keepdims=True), 1e-12)
75
+
76
+ cluster_ids = rng.integers(0, n_clusters, size=n_rows, dtype="int32")
77
+ vectors = centers[cluster_ids] + rng.normal(0.0, cluster_std, size=(n_rows, dim)).astype(
78
+ "float32"
79
+ )
80
+ probabilities = _cluster_filter_probabilities(
81
+ filter_selectivity=filter_selectivity,
82
+ correlation=correlation,
83
+ n_clusters=n_clusters,
84
+ )
85
+ probability_array = np.asarray(probabilities, dtype="float64")
86
+ filter_mask = rng.random(n_rows) < probability_array[cluster_ids]
87
+ if not bool(filter_mask.any()):
88
+ filter_mask[int(rng.integers(0, n_rows))] = True
89
+ if bool(filter_mask.all()):
90
+ filter_mask[int(rng.integers(0, n_rows))] = False
91
+
92
+ return SyntheticDataset(
93
+ vectors=vectors.astype("float32", copy=False),
94
+ filter_mask=filter_mask,
95
+ cluster_ids=cluster_ids,
96
+ centers=centers,
97
+ filter_probabilities=probabilities,
98
+ filter_selectivity=filter_selectivity,
99
+ correlation=correlation,
100
+ seed=seed,
101
+ )
102
+
103
+
104
+ def generate_synthetic_queries(
105
+ dataset: SyntheticDataset,
106
+ *,
107
+ n_queries: int = 100,
108
+ seed: int = 1,
109
+ cluster_policy: str = "uniform",
110
+ query_std: float = 0.04,
111
+ ) -> SyntheticQueries:
112
+ """Generate query vectors around dataset clusters."""
113
+
114
+ if n_queries <= 0:
115
+ raise ValueError("n_queries must be positive")
116
+ if query_std <= 0.0:
117
+ raise ValueError("query_std must be positive")
118
+
119
+ np = _numpy()
120
+ rng = np.random.default_rng(seed)
121
+ n_clusters = int(dataset.centers.shape[0])
122
+ if cluster_policy == "uniform":
123
+ probabilities = None
124
+ elif cluster_policy == "filter_hot":
125
+ weights = np.asarray(dataset.filter_probabilities, dtype="float64")
126
+ probabilities = weights / weights.sum()
127
+ elif cluster_policy == "filter_cold":
128
+ weights = 1.0 - np.asarray(dataset.filter_probabilities, dtype="float64")
129
+ probabilities = weights / weights.sum()
130
+ else:
131
+ raise ValueError("cluster_policy must be one of: uniform, filter_hot, filter_cold")
132
+
133
+ cluster_ids = rng.choice(n_clusters, size=n_queries, replace=True, p=probabilities).astype(
134
+ "int32"
135
+ )
136
+ vectors = dataset.centers[cluster_ids] + rng.normal(
137
+ 0.0,
138
+ query_std,
139
+ size=(n_queries, dataset.dim),
140
+ ).astype("float32")
141
+ return SyntheticQueries(
142
+ vectors=vectors.astype("float32", copy=False),
143
+ cluster_ids=cluster_ids,
144
+ seed=seed,
145
+ cluster_policy=cluster_policy,
146
+ )
147
+
148
+
149
+ def load_file_dataset(
150
+ *,
151
+ vectors_path: Path,
152
+ filter_mask_path: Path,
153
+ dataset_id: str = "file",
154
+ ) -> SyntheticDataset:
155
+ """Load vectors and a boolean filter mask from .npy files."""
156
+
157
+ np = _numpy()
158
+ vectors = np.load(vectors_path, mmap_mode="r")
159
+ if int(vectors.ndim) != 2:
160
+ raise ValueError("vectors .npy must be a 2D array")
161
+ if int(vectors.shape[0]) <= 0 or int(vectors.shape[1]) <= 0:
162
+ raise ValueError("vectors .npy must have at least one row and one dimension")
163
+ if vectors.dtype.kind not in {"f", "i", "u"}:
164
+ raise ValueError("vectors .npy must contain numeric values")
165
+
166
+ raw_mask = np.load(filter_mask_path, mmap_mode="r")
167
+ if int(raw_mask.ndim) != 1:
168
+ raise ValueError("filter mask .npy must be a 1D array")
169
+ if int(raw_mask.shape[0]) != int(vectors.shape[0]):
170
+ raise ValueError("filter mask length must match vector row count")
171
+ if raw_mask.dtype.kind not in {"b", "i", "u", "f"}:
172
+ raise ValueError("filter mask .npy must contain boolean or numeric values")
173
+
174
+ filter_mask = np.asarray(raw_mask, dtype="bool")
175
+ observed_selectivity = float(filter_mask.mean())
176
+ cluster_ids = np.zeros(int(vectors.shape[0]), dtype="int32")
177
+ centers = np.zeros((1, int(vectors.shape[1])), dtype="float32")
178
+ return SyntheticDataset(
179
+ vectors=vectors,
180
+ filter_mask=filter_mask,
181
+ cluster_ids=cluster_ids,
182
+ centers=centers,
183
+ filter_probabilities=(observed_selectivity,),
184
+ filter_selectivity=observed_selectivity,
185
+ correlation=0.0,
186
+ seed=0,
187
+ dataset_id=dataset_id,
188
+ )
189
+
190
+
191
+ def load_file_queries(
192
+ *,
193
+ query_vectors_path: Path,
194
+ expected_dim: int,
195
+ ) -> SyntheticQueries:
196
+ """Load benchmark query vectors from a .npy file."""
197
+
198
+ np = _numpy()
199
+ vectors = np.load(query_vectors_path, mmap_mode="r")
200
+ if int(vectors.ndim) != 2:
201
+ raise ValueError("query vectors .npy must be a 2D array")
202
+ if int(vectors.shape[0]) <= 0:
203
+ raise ValueError("query vectors .npy must contain at least one query")
204
+ if int(vectors.shape[1]) != expected_dim:
205
+ raise ValueError("query vector dimension must match dataset vector dimension")
206
+ if vectors.dtype.kind not in {"f", "i", "u"}:
207
+ raise ValueError("query vectors .npy must contain numeric values")
208
+ return SyntheticQueries(
209
+ vectors=vectors,
210
+ cluster_ids=np.zeros(int(vectors.shape[0]), dtype="int32"),
211
+ seed=0,
212
+ cluster_policy="file",
213
+ )
214
+
215
+
216
+ def _cluster_filter_probabilities(
217
+ *,
218
+ filter_selectivity: float,
219
+ correlation: float,
220
+ n_clusters: int,
221
+ ) -> tuple[float, ...]:
222
+ np = _numpy()
223
+ if abs(correlation) < 1e-12:
224
+ return tuple(float(filter_selectivity) for _ in range(n_clusters))
225
+
226
+ scores = np.linspace(1.0, -1.0, n_clusters, dtype="float64")
227
+ if correlation < 0.0:
228
+ scores = -scores
229
+ weights = np.exp(abs(correlation) * 4.0 * scores)
230
+ low = 0.0
231
+ high = max(1.0, filter_selectivity / float(weights.mean()))
232
+ while float(np.minimum(1.0, high * weights).mean()) < filter_selectivity:
233
+ high *= 2.0
234
+ for _ in range(64):
235
+ mid = (low + high) / 2.0
236
+ mean = float(np.minimum(1.0, mid * weights).mean())
237
+ if mean < filter_selectivity:
238
+ low = mid
239
+ else:
240
+ high = mid
241
+ probabilities = np.minimum(1.0, high * weights)
242
+ return tuple(float(value) for value in probabilities)
243
+
244
+
245
+ def _numpy() -> Any:
246
+ return importlib.import_module("numpy")