Features Module¶
Dwell time extraction and signal feature computation.
Overview¶
The features module provides functions for extracting dwell times from move tables and computing signal-level statistics.
Move Table Parsing¶
MoveTable
dataclass
¶
Parsed move table from basecaller output.
Attributes:
| Name | Type | Description |
|---|---|---|
stride |
int
|
Neural network downsampling factor |
moves |
ndarray
|
Binary array where 1 indicates a new base |
read_id |
str
|
Read identifier |
num_samples |
int
|
Total number of raw signal samples (from ns tag) |
trim_offset |
int
|
Signal trim offset (from ts tag) |
to_seq_to_sig_map
¶
Convert move table to sequence-to-signal mapping.
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of shape (num_bases + 1,) giving signal position for each base. |
ndarray
|
The last element is num_samples (end of basecalled signal region). |
Each move=1 at position i means a new base starts at signal index
i * stride (+ trim_offset for untrimmed signal). This matches the
Remora convention: query_to_signal = np.nonzero(mv)[0] * stride.
Source code in src/leech/features.py
Dwell Time Computation¶
compute_dwell_times
¶
Compute per-base dwell times from move table.
Dwell time = number of signal samples assigned to each base.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
move_table
|
MoveTable
|
MoveTable object from extract_move_table() |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of shape (num_bases,) with dwell time for each base in signal samples |
Examples:
>>> # Move array: [1,1,0,1,0,0,0,1,...] with stride=5
>>> # Base 0: 1 move = 1 * 5 = 5 samples
>>> # Base 1: 2 moves (1,1) = 2 * 5 = 10 samples
>>> # Base 2: 4 moves (0,1,0,0,0) = 4 * 5 = 20 samples
Source code in src/leech/features.py
Signal Features¶
compute_signal_levels
¶
Compute per-base signal level statistics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
Normalized signal array |
required |
seq_to_sig_map
|
ndarray
|
Mapping from bases to signal indices (from MoveTable.to_seq_to_sig_map()) |
required |
stat
|
str
|
Statistic to compute ('mean', 'median', 'std', 'min', 'max') |
'mean'
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of shape (num_bases,) with signal level statistic for each base |
Source code in src/leech/features.py
Signal Normalization¶
normalize_read_signal
¶
normalize_read_signal(raw_signal: ndarray, method: str = 'median_mad', pa_mean: float | None = None, pa_stdev: float | None = None, cal_offset: float | None = None, cal_scale: float | None = None) -> tuple[np.ndarray, dict[str, float]]
Normalize raw signal data.
Named to avoid colliding with :func:escapepod.normalize_signal, which is a
different transform: it takes int16 DAC input and divides by the bare MAD,
with no 1.4826 factor. The median_mad method here delegates to
:func:escapepod.mad_normalize instead, which is the matching one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw_signal
|
ndarray
|
Raw DAC signal values |
required |
method
|
str
|
Normalization method ('median_mad', 'zscore', 'quantile', 'pa_scaling') |
'median_mad'
|
pa_mean
|
float | None
|
Global shift for pa_scaling normalization (from basecaller model) |
None
|
pa_stdev
|
float | None
|
Global scale for pa_scaling normalization (from basecaller model) |
None
|
cal_offset
|
float | None
|
POD5 calibration offset (DAC -> pA conversion) |
None
|
cal_scale
|
float | None
|
POD5 calibration scale (DAC -> pA conversion) |
None
|
Returns:
| Type | Description |
|---|---|
tuple[ndarray, dict[str, float]]
|
Tuple of (normalized_signal, normalization_params) |
Source code in src/leech/features.py
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 | |
Feature Computation¶
compute_dwell_features
¶
Compute windowed dwell time features.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dwells
|
ndarray
|
Per-base dwell times |
required |
window
|
int
|
Size of sliding window for context features |
5
|
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
Dictionary with feature arrays: - 'dwell': raw dwell times - 'dwell_log': log-transformed dwell times - 'dwell_mean': local mean in window - 'dwell_std': local std in window - 'dwell_ratio': ratio to local mean |
Source code in src/leech/features.py
compute_signal_features
¶
Compute comprehensive per-base signal features.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
Normalized signal array |
required |
seq_to_sig_map
|
ndarray
|
Base to signal mapping |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
Dictionary with per-base features: - 'level_mean': mean signal level - 'level_median': median signal level - 'level_std': signal standard deviation - 'level_range': max - min signal |
Source code in src/leech/features.py
Helper Functions¶
Levels for Mapped Bases¶
Fits a per-sequence expected-level array to the per-mapped-base feature grid.
The two counts differ under anchor="reference" when an alignment ends in a
non-match CIGAR op; see LeechRead.num_mapped_bases.
levels_for_mapped_bases
¶
Fit a per-sequence level array to the per-mapped-base feature grid.
extract_levels returns one level per base of the sequence, but every
feature array is indexed by mapped base — len(seq_to_sig_map) - 1 — and
the two differ. Under anchor="reference" the sequence is the aligned
reference slice [reference_start:reference_end] while the map comes from
compute_ref_to_signal, which strips trailing non-match CIGAR ops first,
so an alignment ending in a deletion yields a map shorter than its
sequence. See LeechRead.num_mapped_bases.
Levels past the map are dropped and a short array is zero-filled, which is
what the Rust pipeline's compute_kmer_residual_features does by zipping.
Before this existed, the mismatch raised ValueError out of a numpy
broadcast, the prepare workers caught it, and the whole read was dropped —
on the Python backend only. That is issue #185's failure mode with the
backends swapped, and it selects the same population: indel-heavy and
supplementary alignments.
extract_move_table
¶
Extract move table from BAM alignment record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alignment
|
AlignedSegment
|
pysam AlignedSegment with mv, ns, and ts tags |
required |
Returns:
| Type | Description |
|---|---|
MoveTable
|
MoveTable object |
Raises:
| Type | Description |
|---|---|
ValueError
|
If required tags are missing |