Skip to content

Commit 44bcf85

Browse files
committed
refactor(python): wrap BuilderStatus in a facade and replace "progress" callback string with show_progress
1 parent b05315f commit 44bcf85

10 files changed

Lines changed: 137 additions & 43 deletions

File tree

‎docs/tutorials/building_graphs.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ To build an index from a 2D NumPy array in a single call:
155155
k=30, # Number of edges per vertex
156156
eps=0.1, # Exploration margin during build
157157
threads=0, # 0 = use all available CPU cores
158-
callback="progress", # Print progress bar to stdout
158+
show_progress=True, # Print progress bar to stdout
159159
)
160160
161161
---

‎examples/dynamic_data/main.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def build_dynamic_graph(
273273
builder.add_entry(int(all_labels[rem_idx]), base_vecs[rem_idx : rem_idx + 1])
274274

275275
build_start = time.perf_counter()
276-
builder.build(callback="progress")
276+
builder.build(show_progress=True)
277277
build_time = time.perf_counter() - build_start
278278
print(f"Graph built in {build_time:.2f} seconds ({graph_mut.size()} vertices).")
279279

‎examples/sliding_window/main.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def run_sliding_window_benchmark(
133133
initial_labels = np.arange(0, window_size, dtype=np.uint32)
134134
initial_features = base_vectors[:window_size]
135135
builder.add_entry(initial_labels, initial_features)
136-
builder.build(callback="progress")
136+
builder.build(show_progress=True)
137137

138138
build_time = time.perf_counter() - t0
139139
print(f"Initial graph built in {build_time:.2f}s (Throughput: {window_size / build_time:,.0f} vec/s)\n")

‎examples/static_data/main.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def run_static_benchmark(
194194

195195
labels = np.arange(base_vecs.shape[0], dtype=np.uint32)
196196
builder.add_entry(labels, base_vecs)
197-
builder.build(callback="progress")
197+
builder.build(show_progress=True)
198198

199199
build_time = time.perf_counter() - build_start
200200
print(f"Graph built in {build_time:.2f} seconds ({graph_mut.size()} vertices).")

‎examples/vibe/module.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def fit(self, X: np.ndarray, cache_dir: Path | None = None):
133133
X,
134134
metric=self.metric_enum,
135135
threads=self.threads,
136-
callback="progress",
136+
show_progress=True,
137137
)
138138

139139
# 2. Build graph in FP32
@@ -146,7 +146,7 @@ def fit(self, X: np.ndarray, cache_dir: Path | None = None):
146146
seed=7,
147147
optimization_target=self.opt_enum,
148148
thread_count=self.threads,
149-
callback="progress",
149+
show_progress=True,
150150
)
151151

152152
print(f"Saving graph to cache {cache_file}...", flush=True)
@@ -251,7 +251,7 @@ def fit(self, X: np.ndarray, cache_dir: Path | None = None):
251251
X,
252252
metric=self.base_metric,
253253
threads=self.threads,
254-
callback="progress",
254+
show_progress=True,
255255
)
256256

257257
# 2. Build graph in FP32
@@ -264,7 +264,7 @@ def fit(self, X: np.ndarray, cache_dir: Path | None = None):
264264
seed=7,
265265
optimization_target=self.opt_enum,
266266
thread_count=self.threads,
267-
callback="progress",
267+
show_progress=True,
268268
)
269269

270270
print(f"Saving graph to cache {cache_file}...", flush=True)

‎python/src/deglib/__init__.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from . import search
88
from .search import Searcher, create_searcher
99

10-
from .builder import GraphBuilder, build_from_data
10+
from .builder import GraphBuilder, BuilderStatus, build_from_data
1111
from .graph import (
1212
DynamicExplorationGraph,
1313
create_empty,
@@ -30,6 +30,7 @@
3030
"load_dynamic_graph",
3131
"load_mutable_graph",
3232
"GraphBuilder",
33+
"BuilderStatus",
3334
"build_from_data",
3435
"FloatSpace",
3536
"Metric",

‎python/src/deglib/builder.py‎

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,74 @@ class OptimizationTarget(enum.IntEnum):
2626
LowLID = deglib_cpp.OptimizationTarget.LowLID
2727

2828

29+
class BuilderStatus:
30+
"""
31+
Snapshot of the graph build progress.
32+
33+
Instances are created internally and passed to the ``callback`` of
34+
:meth:`GraphBuilder.build`, and returned by :meth:`GraphBuilder.build`.
35+
This facade wraps the native status object so that end users never have to
36+
interact with the ``deglib_cpp`` bindings directly.
37+
38+
The instance handed to a progress ``callback`` is only valid for the
39+
duration of that callback invocation.
40+
"""
41+
42+
def __init__(self, status_cpp: deglib_cpp.BuilderStatus):
43+
self._status_cpp = status_cpp
44+
45+
@property
46+
def step(self) -> int:
47+
"""Number of graph manipulation steps completed."""
48+
return self._status_cpp.step
49+
50+
@property
51+
def added(self) -> int:
52+
"""Total number of vertices added so far."""
53+
return self._status_cpp.added
54+
55+
@property
56+
def deleted(self) -> int:
57+
"""Total number of vertices deleted so far."""
58+
return self._status_cpp.deleted
59+
60+
@property
61+
def improved(self) -> int:
62+
"""Total number of successful edge improvements so far."""
63+
return self._status_cpp.improved
64+
65+
@property
66+
def tries(self) -> int:
67+
"""Total number of improvement attempts so far."""
68+
return self._status_cpp.tries
69+
70+
@property
71+
def step_added_ids(self):
72+
"""External labels added during the current build step."""
73+
return self._status_cpp.step_added_ids
74+
75+
@property
76+
def step_deleted_ids(self):
77+
"""External labels deleted during the current build step."""
78+
return self._status_cpp.step_deleted_ids
79+
80+
@property
81+
def total_added_ids(self):
82+
"""All external labels added across the entire build."""
83+
return self._status_cpp.total_added_ids
84+
85+
@property
86+
def total_deleted_ids(self):
87+
"""All external labels deleted across the entire build."""
88+
return self._status_cpp.total_deleted_ids
89+
90+
def __repr__(self) -> str:
91+
return (
92+
f"BuilderStatus(step={self.step}, added={self.added}, deleted={self.deleted}, "
93+
f"improved={self.improved}, tries={self.tries})"
94+
)
95+
96+
2997
class GraphBuilder:
3098
"""
3199
Constructs a GraphBuilder for building and optimizing a regular graph.
@@ -186,26 +254,33 @@ def get_batch_size(self) -> int:
186254
return self.builder_cpp.get_batch_size()
187255

188256
def build(
189-
self, callback: Callable[[deglib_cpp.BuilderStatus], None] | str | None = None, infinite: bool = False
190-
) -> deglib_cpp.BuilderStatus:
257+
self,
258+
callback: Callable[[BuilderStatus], None] | None = None,
259+
show_progress: bool = False,
260+
infinite: bool = False,
261+
) -> BuilderStatus:
191262
"""
192263
Build the graph. This could be run on a separate thread in an infinite loop. Call stop() to end this process.
193264
194265
:param callback: The callback that is called after each step of the build process. A BuilderStatus
195-
is the only argument to the function.
196-
If None nothing is printed.
197-
If callback is the string "progress", a simple progress bar is printed to stdout.
266+
is the only argument to the function. If None nothing is printed.
267+
:param show_progress: If True and no callback is given, a simple progress bar is printed to stdout.
268+
Ignored when running in infinite mode (total workload is unknown).
198269
:param infinite: If set to True, blocks indefinitely, until the stop() function is called. Can be used, if
199270
build() is run in a separate thread.
200271
:return: BuilderStatus containing build metrics and ID vectors for added/deleted vertices.
201-
:rtype: deglib_cpp.BuilderStatus
272+
:rtype: BuilderStatus
202273
"""
274+
if callback is None and show_progress and not infinite:
275+
callback = ProgressCallback(self.get_num_new_entries(), self.get_num_remove_entries())
276+
203277
if callback is None:
204-
return self.builder_cpp.build_silent(infinite)
205-
else:
206-
if not infinite and callback == "progress":
207-
callback = ProgressCallback(self.get_num_new_entries(), self.get_num_remove_entries())
208-
return self.builder_cpp.build(callback, infinite)
278+
return BuilderStatus(self.builder_cpp.build_silent(infinite))
279+
280+
def _callback(status_cpp) -> None:
281+
callback(BuilderStatus(status_cpp))
282+
283+
return BuilderStatus(self.builder_cpp.build(_callback, infinite))
209284

210285
def stop(self):
211286
"""
@@ -241,7 +316,8 @@ def build_from_data(
241316
max_path_length: int = 5,
242317
improve_tries: int = 0,
243318
thread_count: int = 0,
244-
callback: Callable[[deglib_cpp.BuilderStatus], None] | str | None = None,
319+
callback: Callable[[BuilderStatus], None] | None = None,
320+
show_progress: bool = False,
245321
) -> DynamicExplorationGraph:
246322
"""
247323
Create a new graph built from the given data using a GraphBuilder.
@@ -282,8 +358,10 @@ def build_from_data(
282358
:type improve_tries: int
283359
:param thread_count: Number of threads to use for parallel building. If 0, uses hardware concurrency.
284360
:type thread_count: int
285-
:param callback: Callback function for build progress reporting. If "progress", shows progress bar
286-
:type callback: Callable[[deglib_cpp.BuilderStatus], None] | str | None
361+
:param callback: Callback function for build progress reporting, receiving a BuilderStatus. If None, nothing is reported.
362+
:type callback: Callable[[BuilderStatus], None] | None
363+
:param show_progress: If True and no callback is given, a simple progress bar is printed to stdout.
364+
:type show_progress: bool
287365
:return: The constructed and optimized graph
288366
:rtype: DynamicExplorationGraph
289367
"""
@@ -311,7 +389,7 @@ def build_from_data(
311389
if thread_count > 0:
312390
builder.set_thread_count(thread_count)
313391

314-
builder.build(callback=callback)
392+
builder.build(callback=callback, show_progress=show_progress)
315393

316394
return graph
317395

@@ -347,15 +425,15 @@ def __init__(
347425
self.last_print_time = 0
348426
self.min_print_interval = min_print_interval
349427

350-
def __call__(self, builder_status: deglib_cpp.BuilderStatus):
428+
def __call__(self, builder_status: BuilderStatus):
351429
"""
352430
Display the current build progress as a formatted progress bar.
353431
354432
Called by the builder during the build process to report status. Updates are throttled
355433
by min_print_interval to avoid excessive output, except for the final step.
356434
357435
:param builder_status: Current status of the build process containing step counts
358-
:type builder_status: deglib_cpp.BuilderStatus
436+
:type builder_status: BuilderStatus
359437
"""
360438
current_time = time.time()
361439
num_steps = builder_status.added + builder_status.deleted

‎python/src/deglib/optimization.py‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ def presort(
3939
space_or_metric: FloatSpace | Metric | None = None,
4040
radius_decay: float = 0.9,
4141
threads: int = 0,
42-
callback: typing.Callable[[float], typing.Union[bool, None]] | str | None = None,
42+
callback: typing.Callable[[float], typing.Union[bool, None]] | None = None,
4343
*,
4444
metric: Metric | None = None,
4545
space: FloatSpace | None = None,
46+
show_progress: bool = False,
4647
) -> np.ndarray:
4748
"""
4849
Perform 1D pre-sorting of dataset feature vectors using Fast Linear Alignment Scheme (FLAS).
@@ -54,11 +55,11 @@ def presort(
5455
:param space_or_metric: FloatSpace instance or Metric type used for distance computation during sorting.
5556
:param radius_decay: Decay factor per iteration for neighborhood radius (default 0.9).
5657
:param threads: Number of worker threads (0 uses all available CPU cores).
57-
:param callback: Optional callback for reporting sorting progress.
58-
If ``'progress'``, prints progress to stdout.
59-
If a function, receives progress float in range [0.0, 1.0]. Returning True cancels sorting.
58+
:param callback: Optional callback for reporting sorting progress. Receives a progress float in range
59+
[0.0, 1.0]. Returning True cancels sorting.
6060
:param metric: Explicit Metric enum (cannot be combined with space or space_or_metric).
6161
:param space: Explicit FloatSpace instance (cannot be combined with metric or space_or_metric).
62+
:param show_progress: If True and no callback is given, prints progress to stdout.
6263
:return: 1D uint32 NumPy array containing the sorted permutation of original vector indices.
6364
"""
6465
# Count how many of the three ways to specify distance/space were provided
@@ -87,7 +88,9 @@ def presort(
8788
raise TypeError(f"Expected Metric or FloatSpace, got {type(target).__name__}")
8889

8990
cb_fn = None
90-
if callback == "progress":
91+
if callback is not None:
92+
cb_fn = callback
93+
elif show_progress:
9194
last_pct = [-1]
9295

9396
def progress_cb(prog: float) -> bool:
@@ -102,8 +105,6 @@ def progress_cb(prog: float) -> bool:
102105
return False
103106

104107
cb_fn = progress_cb
105-
elif callable(callback):
106-
cb_fn = callback
107108

108109
return deglib_cpp.presort(vectors_f32, cpp_space, radius_decay, threads, cb_fn)
109110

‎python/tests/experiments.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def main():
2020
vec: np.ndarray
2121
builder.add_entry(i, vec)
2222

23-
builder.build(callback="progress")
23+
builder.build(show_progress=True)
2424

2525
valid_labels = np.random.choice(graph.size(), size=5, replace=False)
2626

@@ -41,7 +41,7 @@ def main2():
4141

4242
# build index
4343
data = np.random.random((samples, dims)).astype(np.float32)
44-
index = deglib.builder.build_from_data(data, extend_eps=0.1, callback="progress")
44+
index = deglib.builder.build_from_data(data, extend_eps=0.1, show_progress=True)
4545

4646
# search
4747
query = np.random.random(dims).astype(np.float32)
@@ -89,7 +89,7 @@ def do_build_with_remove(seed, edges_per_vertex):
8989
for label in range(0, data.shape[0], 2):
9090
builder.remove_entry(label)
9191

92-
builder.build(callback="progress")
92+
builder.build(show_progress=True)
9393

9494

9595
KNOWN_CRASHES = {(1, 10)}

0 commit comments

Comments
 (0)