Skip to content

Commit f807cff

Browse files
committed
Add some documentation + iterator for result class
1 parent 5118a2a commit f807cff

8 files changed

Lines changed: 597 additions & 24 deletions

File tree

src/birdnet/acoustic/inference/core/encoding/encoding_result.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ def __init__(
7272

7373
@property
7474
def memory_size_mb(self) -> float:
75+
"""Return the total result memory usage including embeddings buffers.
76+
77+
Returns:
78+
float: Memory size in megabytes.
79+
"""
7580
return super().memory_size_mb + (
7681
(
7782
self._embeddings.nbytes
@@ -83,21 +88,46 @@ def memory_size_mb(self) -> float:
8388

8489
@property
8590
def embeddings(self) -> np.ndarray:
91+
"""Return the raw embedding tensor produced by the encoder.
92+
93+
Returns:
94+
np.ndarray: Embeddings with shape `(n_inputs, n_segments, emb_dim)`.
95+
"""
8696
return self._embeddings
8797

8898
@property
8999
def embeddings_masked(self) -> np.ndarray:
100+
"""Return the mask that marks relevant segments across files.
101+
102+
Returns:
103+
np.ndarray: Boolean mask of the same shape as `embeddings`.
104+
"""
90105
return self._embeddings_masked
91106

92107
@property
93-
def emd_dim(self) -> int:
108+
def emb_dim(self) -> int:
109+
"""Return the embedding dimensionality.
110+
111+
Returns:
112+
int: Number of coefficients per embedding vector.
113+
"""
94114
return self._embeddings.shape[-1]
95115

96116
@property
97117
def max_n_segments(self) -> int:
118+
"""Return the maximum segment count reserved per input.
119+
120+
Returns:
121+
int: Number of overlapping windows available per file.
122+
"""
98123
return self._embeddings.shape[1]
99124

100125
def to_structured_array(self) -> np.ndarray:
126+
"""Convert the embeddings and timing metadata into a structured array.
127+
128+
Returns:
129+
np.ndarray: Array with fields for input path, start/end times, and embedding.
130+
"""
101131
valid_mask_per_segment = ~(self._embeddings_masked).all(axis=2)
102132
valid_file_idx, valid_seg_idx = np.where(valid_mask_per_segment)
103133
n_embeddings = len(valid_file_idx)
@@ -108,7 +138,7 @@ def to_structured_array(self) -> np.ndarray:
108138
(VAR_INPUT, self._input_dtype),
109139
(VAR_START_TIME, self._input_durations.dtype),
110140
(VAR_END_TIME, self._input_durations.dtype),
111-
(VAR_EMBEDDING, self._embeddings.dtype, self.emd_dim),
141+
(VAR_EMBEDDING, self._embeddings.dtype, self.emb_dim),
112142
]
113143

114144
structured_array = np.empty(n_embeddings, dtype=dtype)
@@ -154,6 +184,11 @@ def to_structured_array(self) -> np.ndarray:
154184
return structured_array
155185

156186
def to_arrow_table(self) -> pa.Table:
187+
"""Produce a PyArrow table that serializes each embedding with timing metadata.
188+
189+
Returns:
190+
pa.Table: Table containing dictionary-encoded inputs and embeddings lists.
191+
"""
157192
import pyarrow as pa
158193

159194
structured = self.to_structured_array()
@@ -194,7 +229,7 @@ def to_arrow_table(self) -> pa.Table:
194229
"model_fmax": str(self._model_fmax[0]),
195230
"model_sr": str(self._model_sr[0]),
196231
"model_precision": str(self._model_precision[0]),
197-
"embedding_dim": str(self.emd_dim),
232+
"embedding_dim": str(self.emb_dim),
198233
}
199234
schema_with_metadata = pa.schema(fields, metadata=metadata)
200235
table = pa.table(arrow_arrays, schema=schema_with_metadata)
@@ -208,6 +243,14 @@ def to_csv(
208243
buffer_size_kb: int = 1024,
209244
silent: bool = False,
210245
) -> None:
246+
"""Dump the structured embeddings to a CSV file for downstream analysis.
247+
248+
Args:
249+
path: File path where the CSV will be written (must end with .csv).
250+
encoding: Text encoding for the output file.
251+
buffer_size_kb: Buffer size used when writing the file.
252+
silent: Suppress progress messages when True.
253+
"""
211254
if not silent:
212255
print("Preparing CSV export...") # noqa: T201
213256

@@ -264,6 +307,11 @@ def to_csv(
264307
f.writelines(block)
265308

266309
def unprocessable_inputs(self) -> np.ndarray:
310+
"""Return the indices of inputs that could not be processed.
311+
312+
Returns:
313+
np.ndarray: Boolean mask or indices for skipped inputs.
314+
"""
267315
return self._unprocessable_inputs
268316

269317
def _get_extra_save_data(self) -> dict[str, np.ndarray]:
@@ -322,9 +370,6 @@ def _input_dtype(self) -> type:
322370
# -> pointer to python string is more efficient
323371
return object
324372

325-
def _format_input_for_csv(self, input_value: str) -> str:
326-
return f'"{input_value}"'
327-
328373

329374
class AcousticDataEncodingResult(AcousticEncodingResultBase):
330375
def __init__(

src/birdnet/acoustic/inference/core/prediction/prediction_result.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -380,9 +380,6 @@ def _input_dtype(self) -> type:
380380
# -> pointer to python string is more efficient
381381
return object
382382

383-
def _format_input_for_csv(self, input_value: str) -> str:
384-
return f'"{input_value}"'
385-
386383

387384
class AcousticDataPredictionResult(AcousticPredictionResultBase):
388385
def __init__(
@@ -418,6 +415,3 @@ def __init__(
418415
model_precision=model_precision,
419416
model_version=model_version,
420417
)
421-
422-
def _format_input_for_csv(self, input_value: Any) -> str: # noqa: ANN401
423-
return f"{input_value}"

src/birdnet/acoustic/inference/core/result_base.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import time
55
from abc import ABC, abstractmethod
6+
from collections.abc import Iterable
67
from multiprocessing import current_process
78
from pathlib import Path
89
from threading import current_thread
@@ -36,6 +37,8 @@
3637

3738

3839
class AcousticResultBase(ResultBase):
40+
"""Base container for shared acoustic model result metadata and helpers."""
41+
3942
def __init__(
4043
self,
4144
model_path: Path,
@@ -50,6 +53,21 @@ def __init__(
5053
model_fmax: int,
5154
model_sr: int,
5255
) -> None:
56+
"""Capture model metadata plus per-input timing information.
57+
58+
Args:
59+
model_path: Path to the acoustic model binary used for inference.
60+
model_version: Version identifier of the model.
61+
model_precision: Precision string (float16/float32) backed by the model.
62+
inputs: Typed array of inputs (paths or indices) that were encoded/predicted.
63+
input_durations: Duration of each input in seconds.
64+
segment_duration_s: Segment length used by the inference pipeline.
65+
overlap_duration_s: Overlap between consecutive segments.
66+
speed: Speed multiplier applied to the inputs during preprocessing.
67+
model_fmin: Lower frequency bound used by the model.
68+
model_fmax: Upper frequency bound used by the model.
69+
model_sr: Sampling rate that the model expects.
70+
"""
5371
super().__init__(
5472
model_path=model_path,
5573
model_version=model_version,
@@ -79,38 +97,47 @@ def _input_dtype(self) -> type:
7997

8098
@property
8199
def segment_duration_s(self) -> float:
100+
"""Segment duration as configured on the inference pipeline."""
82101
return float(self._segment_duration_s[0])
83102

84103
@property
85104
def overlap_duration_s(self) -> float:
105+
"""Overlap duration between sliding windows in seconds."""
86106
return float(self._overlap_duration_s[0])
87107

88108
@property
89109
def speed(self) -> float:
110+
"""Speed multiplier that was applied to the inputs."""
90111
return float(self._speed[0])
91112

92113
@property
93114
def inputs(self) -> np.ndarray:
115+
"""Identifiers for each input processed by the result."""
94116
return self._inputs
95117

96118
@property
97119
def n_inputs(self) -> int:
120+
"""Number of inputs in the result payload."""
98121
return self._inputs.shape[0]
99122

100123
@property
101124
def input_durations(self) -> np.ndarray:
125+
"""Durations of each input in seconds."""
102126
return self._input_durations
103127

104128
@property
105129
def model_fmin(self) -> int:
130+
"""Lower bound of the model's bandpass filter."""
106131
return int(self._model_fmin[0])
107132

108133
@property
109134
def model_fmax(self) -> int:
135+
"""Upper bound of the model's bandpass filter."""
110136
return int(self._model_fmax[0])
111137

112138
@property
113139
def model_sr(self) -> int:
140+
"""Sampling rate expected by the model."""
114141
return int(self._model_sr[0])
115142

116143
def _get_extra_save_data(self) -> dict[str, np.ndarray]:
@@ -138,6 +165,11 @@ def _set_extra_load_data(cls, data: dict[str, np.ndarray]) -> None:
138165

139166
@property
140167
def memory_size_mb(self) -> float:
168+
"""Memory usage for the base result metadata.
169+
170+
Returns:
171+
float: Memory used by metadata buffers in megabytes.
172+
"""
141173
return super().memory_size_mb + (
142174
(
143175
self._inputs.nbytes
@@ -169,6 +201,7 @@ def to_csv(
169201
) -> None: ...
170202

171203
def to_dataframe(self) -> pd.DataFrame:
204+
"""Convert the structured array into a pandas DataFrame."""
172205
import pandas as pd
173206

174207
structured = self.to_structured_array()
@@ -192,6 +225,7 @@ def to_parquet(
192225
compression_level: int | None = None,
193226
silent: bool = False,
194227
) -> None:
228+
"""Write the contents to disk as an Arrow Parquet file."""
195229
import pyarrow.parquet as pq
196230

197231
path = Path(path)
@@ -219,6 +253,10 @@ def to_parquet(
219253
compression_ratio = original_size / file_size if file_size > 0 else 0
220254
print(f"Parquet file: {file_size:.1f} MB (compression: {compression_ratio:.1f}x)") # noqa: T201
221255

256+
def __iter__(self) -> Iterable[np.ndarray]:
257+
"""Iterate over the structured records via the iterator protocol."""
258+
yield from self.to_structured_array()
259+
222260

223261
class SessionBase(ABC):
224262
def __init__(self) -> None:

src/birdnet/acoustic/inference/session.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import shutil
44
from abc import ABC
55
from collections.abc import Callable, Collection, Iterable
6-
from contextlib import contextmanager, suppress
7-
from multiprocessing import shared_memory
86
from pathlib import Path
97
from typing import Any, ContextManager, Generic, Literal, Self, cast
108

@@ -35,7 +33,6 @@
3533
AcousticDataPredictionResult,
3634
AcousticFilePredictionResult,
3735
)
38-
from birdnet.acoustic.inference.core.shm import RingField, create_shm_ring
3936
from birdnet.acoustic.inference.encoding_strategy import (
4037
EncodingStrategy,
4138
)
@@ -50,7 +47,7 @@
5047
from birdnet.acoustic.inference.strategy import InferenceStrategyBase
5148
from birdnet.core.backends import VersionedAcousticBackendProtocol
5249
from birdnet.core.base import SessionBase
53-
from birdnet.globals import ACOUSTIC_MODEL_VERSIONS, WRITABLE_FLAG
50+
from birdnet.globals import ACOUSTIC_MODEL_VERSIONS
5451

5552

5653
class AcousticSessionBase(
@@ -180,7 +177,7 @@ def end(self) -> None:
180177
assert self._resources is not None
181178
self._resources.processing_resources.end_event.set()
182179

183-
def __exit__(self, *args) -> None:
180+
def __exit__(self, *args) -> None: # noqa: ANN002
184181
assert self._is_initialized
185182

186183
assert self._resources is not None

src/birdnet/acoustic/models/base.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@
1212
from birdnet.acoustic.inference.session import (
1313
AcousticEncodingSession,
1414
AcousticPredictionSession,
15-
AcousticSessionBase,
1615
)
17-
from birdnet.core.base import ModelBase, ResultBase
16+
from birdnet.core.base import ModelBase
1817
from birdnet.globals import ACOUSTIC_MODEL_VERSIONS
1918

2019

@@ -29,12 +28,22 @@ def __init__(
2928

3029
@classmethod
3130
@abstractmethod
32-
def get_version(cls) -> ACOUSTIC_MODEL_VERSIONS: ...
31+
def get_version(cls) -> ACOUSTIC_MODEL_VERSIONS: # noqa: ANN002
32+
"""Return the string label that identifies the acoustic model version.
33+
34+
Returns:
35+
ACOUSTIC_MODEL_VERSIONS: Registered enum constant for the supported version.
36+
"""
37+
...
3338

3439
@abstractmethod
3540
def predict(self, *args, **kwargs) -> AcousticPredictionResultBase: # noqa: ANN002, ANN003
3641
...
3742

43+
@abstractmethod
44+
def predict_arrays(self, *args, **kwargs) -> AcousticPredictionResultBase: # noqa: ANN002, ANN003
45+
...
46+
3847
@abstractmethod
3948
def predict_session(self, *args, **kwargs) -> AcousticPredictionSession: # noqa: ANN002, ANN003
4049
...
@@ -43,6 +52,10 @@ def predict_session(self, *args, **kwargs) -> AcousticPredictionSession: # noqa
4352
def encode(self, *args, **kwargs) -> AcousticEncodingResultBase: # noqa: ANN002, ANN003
4453
...
4554

55+
@abstractmethod
56+
def encode_arrays(self, *args, **kwargs) -> AcousticEncodingResultBase: # noqa: ANN002, ANN003
57+
...
58+
4659
@abstractmethod
4760
def encode_session(self, *args, **kwargs) -> AcousticEncodingSession: # noqa: ANN002, ANN003
4861
...

0 commit comments

Comments
 (0)